Search
💡

SOLID 원칙 / SOLID는 구식인가?

작성일
2021/11/10 00:00
수정일
카테고리
객체지향
태그
SOLID

객체지향 5원칙 SOLID.

2000년대 초 로버트 마틴이 주창한 객체지향 5원칙을 두문법칙 기억법으로 정리해놓은 것이다.
Search
SOLID
원칙
내용
The Open Closed Principle You should be able to extend a classes behavior, without modifying it.
The Liskov Substitution Principle Derived classes must be substitutable for their base classes.
The Interface Segregation Principle Make fine grained interfaces that are client specific.
The Dependency Inversion Principle Depend on abstractions, not on concretions.

SRP Single Responsibility Principle

동일한 이유로 변경되는 것들은 함께 모으고, 서로 다른 이유로 변경되는 것들은 분리시킨다.
로버트 마틴은 책임을 변경하려는 이유로 정의하고, 어떤 클래스나 모듈은 변경하려는 단 하나 이유만을 가져야 한다고 결론 짓는다.
예를 들어서 보고서를 편집하고 출력하는 모듈을 생각해 보자. 이 모듈은 두 가지 이유로 변경될 수 있다. 첫 번째로 보고서의 내용 때문에 변경될 수 있다.
두 번째로 보고서의 형식 때문에 변경될 수 있다. 이 두 가지 변경은 하나는 실질적이고 다른 하나는 꾸미기 위한 매우 다른 원인에 기인한다. 단일 책임 원칙에 의하면 이 문제의 두 측면이 실제로 분리된 두 책임 때문이며, 따라서 분리된 클래스나 모듈로 나누어야 한다.
다른 시기에 다른 이유로 변경되어야 하는 두 가지를 묶는 것은 나쁜 설계일 수 있다.
한 클래스를 한 관심사에 집중하도록 유지하는 것이 중요한 이유는, 이것이 클래스를 더욱 튼튼하게 만들기 때문이다.
앞서 든 예를 계속 살펴보면 편집 과정에 변경이 일어나면 같은 클래스의 일부로 있는 출력 코드가 망가질 위험이 대단히 높다.
다른 예시로 Employee라는 클래스를 정의해보자.
Employee의 예시 :
public class Employee { public Money calculatePay ()public String reportHours ()public void save()}
Java
복사
어떤 프로그래머들은 한 클래스 안에 이 세 가지 기능이 함께 정의된 것이 가장 적절하다고 생각할지도 모른다. 결국 클래스라는 것은 공통의 변수들을 이용하여 연산을 수행하는 함수들의 집합체라고 생각하기 때문이다. 그러나 문제는, 이 세 가지 함수는 전혀 다른 이유로 변경될 수 있다는 것.
calculatePay 함수는 급료를 계산하는 비즈니스 룰이 바뀔 때 매번 변경되어야 한다.
reportHours 함수는 누군가가 기존과 다른 보고서 포맷을 원할 때, 그리고
save 함수는 데이터베이스 관리자가 데이터베이스 스키마를 바꿀 때마다 변경되어야 한다.
다양한 변경원인이 있는건 Employee 클래스를 불안정하게 만든다. 더 중요한 것은, Employee를 의존하고 있는 클래스들 역시 이 변경으로 영향을 받을 수 있다는 것이다.
따라서 책임은 하나의 특정 액터를 위한 기능 집합이다. (로버트 C. 마틴)
어떤 책임에 대해 액터는 해당 책임에 대한 유일한 변경의 원천이다. (로버트 C. 마틴)
'급료를 계산하는 비즈니스 룰' 을 필요로 하는 '변호사, 회계사' 객체가 액터에 해당한다.
'기존과 다른 보고서 포맷' 을 필요로 하는 '사용자' 객체가 액터에 해당한다.
'데이터베이스 스키마를 바꾸는' 역할을 하는 '데이터베이스 관리자' 객체가 액터에 해당한다.
각 액터가 필요로 하는 책임을 분리하는 방향으로 리팩토링하면 아래와 같을 것이다.
public class Employee { public Money calculatePay() ... } public class EmployeeReporter { public String reportHours(Employee e) ... } public class EmployeeRepository { public void save(Employee e) ... }
Java
복사
1차 분리를 통해 결과 보고 컴포넌트들은 보고 컴포넌트로, 데이터 베이스와 관련된 모든 클래스는 저장소 컴포넌트로, 비즈니스 룰에 대한 클래스는 비즈니스 룰 컴포넌트로 들어갈 수 있게 된다.
하지만 여전히 의존관계는 존재한다. Employee가 수정되면 다른 클래스들도 재 컴파일되고 재 배포되어야 한다. 여전히 Employee를 수정하여 독립적으로 재배포 할 수는 없다. 그러나 그외 다른 클래스들은 수정 및 독립적인 재배포가 가능해진다. 여기에서 DI를 잘 활용하면, Employee 역시 독립적으로 재배포가 가능해질 것이다.

OCP Open Closed Principle

기능을 변경 또는 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 한다.
기능을 변경하는 데 코드는 수정하지 않는다? 조금은 현학적인 말이다. 의미를 알고 있는 사람에게는 통용되지만, 모르는 사람이 이 문장을 읽고 OCP의 요체를 알아채기는 쉽지 않다.
이해하기 쉽도록 풀어쓰자면 다음과 같다.
기능이 추가될 것이라 예상되는 메소드나 클래스는, 추상화(인터페이스화)하여 구현한다. 새로운 기능이 추가 되었을 때 해당 인터페이스를 주입한다면 원래 코드는 손대지 않을 수 있다.
예컨데 writeFile 이라는 메소드가 있다 하자. 구현한 후, 나중에 기능 요구사항이 추가되어 writeInputStream을 만들었다. 시간이 흘러 콘솔 입력을 만들어 달라는 요구사항이 들어오면 writeConsole이라는 메소드를 만들게 될 것이다.
writeInput이라는 메서드로 추상화 했으면 어떨까? Writer라는 인터페이스를 만들고, write(); 를 구현하도록 설계한다면 FileWriter, ConsoleWriter, InputStreamReader... 얼마든지 추가해서 런타임 시에 주입하여 사용할 수 있을 것이다.

Crystal Ball 은 없다.

하지만 OCP를 실제 적용하는 데에는 'Crystal Ball'이라는 문제가 있다. 우리는 미래의 요구사항 변화를 마치 요술사의 수정구 처럼 정확하게 예상할 수 없다. 불가능하다고 단언할 수 있다.
경험으로부터 변화가 예상되는 사항들이 있다. 해당 내용들은 abstraction을 통해 보호하고, 아직 오지 않은 변경사항에 대해선 고객의 요구가 있을 때 까지 기다리자.

LSP Liskov Substitution Principle

A가 B의 상속을 받는다면, B는 A의 역할을 그대로 수행할 수 있어야 한다. 하위 타입이 상위 타입의 역할을 그대로 수행할 수 없다면, 하위 타입을 사용하는 클래스에서의 버그 가능성을 암시한다.
가장 대표적인 사례는 직사각형을 상속한 정사각형이다. 정사각형은 높이와 너비가 동일해야한다는 제약사항이 있다. 직사각형의 setHeight(); 를 활용하는 순간, 정사각형은 스스로의 제약사항으로 인해 정사각형이 아니게 된다. 직사각형의 구현체로 정사각형을 사용하면 버그가 생길 수 있는 가능성을 만들게 된다.
직사각형의 setHeight와 setWidth를 없앤다면 어떨까? 불변하게 만듦으로써 정사각형의 제약사항이 무너질 가능성이 사라졌다. getHeight()와 getWidth()는 정사각형이 직사각형을 완벽하게 대체할 수 있다. 이러면 LSP를 충족하게 된다.

ISP Interface Segregation Principle

문장 하나를 보자.
클래스는 자신이 이용하지 않는 기능의 변경에 영향을 받으면 안 된다.
위 내용에 동의하는가? 그렇다면 ISP가 필요한 이유를 찾았다고 볼 수 있다.
사람이라는 인터페이스를 생각해보자. 뛰기, 밥먹기, 놀기, 여자친구와 연애하기, 프로그래밍하기, 나라 지키기, 잠자기...
프로그래머 클래스, 군인 클래스, 어린이 클래스를 활용하기 위해 사람 인터페이스를 implements 했다.
어떤 문제가 생길까? 군인 클래스를 수정하기 위해 나라 지키기 기능을 '불침번 서기'와 '사격하기'로 변경하면, 그 기능과 관련이 없는 프로그래머, 어린이 클래스도 변경의 여파를 받게 된다.
요약하면 지나치게 역할이 많은 인터페이스는 결합도를 낮춘다 고 할 수 있겠다.
예전에는 ISP를 '인터페이스 최소 원칙'으로 잘 못 알고 있었는데(인터페이스를 최소화해야한다), 의미는 일맥상통한다고 생각한다.

DIP Dependency Inversion Principle

추상적인 것이 구체화된 것에 의존해선 안 된다 거나 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다 고 설명되곤 하는 DIP.
용어부터 따져보자. 왜 의존성 역전인가? 무엇이 역전되는가?
'변하기 쉬운 것에 의존하기 보다, 잘 변하지 않는 것에 의존해야 한다.'는 명제를 생각해보자. 잘 변하는 것에 의존하면 변할 때마다 의존하고 있는 객체도 영향을 받을 것이기 때문에, 잘 변하지 않는 것에 의존하는 것이 더 변경으로부터 자유로운 설계일 것이다.
그러면 구체적인 것을 추상적인 것으로 만드는 것이 좋을텐데, 그 때 사용할 수 있는 것이 '의존성 역전!'이다.
위의 예제에선 FlowController에서 FileDataReader을 의존하고 있었다. 추상화한 것을 만들고 싶으면 FileDataReader를 추상화한 ByteSource 인터페이스를 만들고 FlowController에서 FileDataReader를 의존하고 있던 것을 ByteSource로 변경하면 된다. 클래스 다이어그램 상에서 FileDataReader 클래스입장에선 의존 받던 것이, 자신의 ByteSource를 의존하고 있는 방향으로 화살표가 '역전' 되었다.

2020년, SOLID는 구식이 되었다...?

2020년 10월, 해당 레퍼런스에서 로버트 C 마틴은 다음과 같은 메일을 받게된다.
요점은 이렇다
업계가 대형 단일체 아키텍처가 아닌 마이크로 서비스로 전환하고 있기 때문에 OCP가 더이상 유효하지 않다.
SOLID원칙이 처음 등장한 20년전처럼 상속을 잘 쓰지 않는 추세이기 때문에 LSP 또한 구식이다.
https://speakerdeck.com/tastapod/why-every-element-of-solid-is-wrong 의 발표처럼, SOLID는 "단순하게 코드를 작성해라 Just write simple code"로 대체되어야 한다.
로버트 C 마틴은 다음과 같이 반박한다.
1.
SRP이 준수되지 않는다면 여러 코드가 복합적으로 엮이게 되고, 이는 마이크로 서비스로 해결할 수 있는 것이 아니다. 얽힌 마이크로 서비스가 생겨날 뿐이다. SRP는 간단한 코드작성을 위한 방법 중 하나이다.
2.
OCP는 수정없이 확장할 수 있는 모듈을 위한 기본이다. 요구사항이 변경되면 '기존 코드의 일부만' 잘못 된 것이다. 기존 코드의 대부분은 여전히 옳다. 우리는 잘못된 코드를 다시 작동하도록 하기 위해 올바른 코드를 수정하고 싶지 않을 것이다. 아이러니 하게도, Simple code is both open and closed.
3.
LSP는 세간의 오해와 달리 '클래스' 간의 법칙이 아니다. 인터페이스를 사용하는 프로그램은 해당 인터페이스의 구현과 혼동되어서는 안 된다는 원칙이다. 이는 서브 타이핑을 의미하는 것으로, 인터페이스를 사용하는 모든 사용자(개발자)는 해당 인터페이스의 의미에 동의해야 한다는 내용이다. 구현자가 인터페이스의 의미를 바꾸어버리면 if 문과 switch문이 난무하는 코드가 된다.
4.
ISP. 우리는 2020년에도 컴파일 언어로 작업한다. 여전히 어떤 모듈을 재 컴파일하고 재배포해야 하는지 결정하는데 수정날짜에 의존한다. 이런 한계 속에서 A모듈이 B모듈을 컴파일 타임에만 의존하고 실행 타임엔 의존하지 않는다고 하더라도, B모듈이 변경되면 A모듈까지 재컴파일 해야한다. 이런 문제는 정적타입 언어(Java, C++, C#, Go, Swift)에서 더 심각하고, 동적유형 언어는 덜하지만 면역은 아니다.
5.
DIP. 우리는 구현체의 디테일에 의존하는 비즈니스 규칙을 원하지 않는다. 비즈니스 로직의 돈 계산이 SQL로 오염되거나 저수준의 validation이나 표현 포맷팅 문제를 겪기를 원하지 않는다. 모듈이 역할별로 잘 분리되어 있는 코드는 모든 소스 코드 의존성, 특히 아키텍처 경계를 넘는 의존성이 DIP를 준수하도록 신중하게 관리함으로써 달성할 수 있다.
마틴은 다음과 같이 글을 마친다.
Just write simple code. 이것은 좋은 조언입니다. 그러나 오랜 세월이 우리에게 가르쳐 준 것이 있다면 단순함을 위해서는 원칙에 따른 훈련이 필요하다는 것입니다. 단순함을 정의하는 것은 바로 이러한 원칙입니다. 프로그래머가 단순성을 지향하는 코드를 생성하도록 돕는 것은 이러한 분야입니다.
멋있어 밥아저씨