SOLID 원칙
기원
- 엉클 밥은 다음과 같이 SOLID 원칙의 기원을 말한다.1
SOLID 원칙의 역사는 깊다. 나는 1980년대 후반 유즈넷(과거 버전의 페이스북)에서 다른 사람들과 소프트웨어 설계 원칙에 대해 토론하는 과정에서 이들 원칙을 모으기 시작했다. 시간이 지나면서 원칙은 교체되거나 변경되었다. 사라져 버린 원칙도 있다. 어떤 원칙들은 서로 합쳐졌다. 새롭게 추가된 원칙도 있다. 2000년대 초반 나는 안정화된 최종 버전을 내놓았는데, 이 때 원칙들의 순서는 지금과 달랐다.
2004년 무렵, 마이클 페더스(Michael Feathers)가 이메일 한 통을 보내왔는데, 원칙들을 재배열하면 각 원칙의 첫 번째 글자들로 SOLID라는 단어를 만들 수 있다는 내용이었다. 그렇게 SOLID 원칙이 탄생했다.
SOLID?
SOLID는 다섯 개의 원칙으로 이루어져 있다.
- SRP: Single Responsibility Principle. 단일 책임 원칙.
- OCP: Open-Closed Principle. 개방-폐쇄 원칙
- LSP: Liskov Substitution Principle. 리스코프 치환 원칙.
- ISP: Interface Segregation Principle. 인터페이스 분리 원칙.
- DIP: Dependency Inversion Principle. 의존성 역전 원칙.
단일 책임 원칙(SRP)
특정 코드의 책임을 파악하기 어렵다면 다음 조언을 기억해 두자.
SRP의 맥락에서, 우리는 책임(responsibility)을 '변경을 위한 이유'로 정의한다. 만약 여러분이 한 클래스를 변경하기 위한 한 가지 이상의 이유를 생각할 수 있다면, 그 클래스는 한 가지 이상의 책임을 맡고 있는 것이다. 때로 이것은 알아내기가 어려운데, 우리는 책임을 묶어서 생각하는 데 익숙해져 있기 때문이다.5
예를 들어 다음과 같은 경우는 단일 책임 원칙을 위반한 것이다.
┌─────────────┐ ┌───────────────┐ ┌───────────┐
│Computational│ │ Rectangle │ │ Graphical │
│ Geometry ├───>├───────────────┤<───┤Application│
│ Application │ │+ draw() │ └─────┬─────┘
└─────────────┘ │+ area():double│ │
└───────┬───────┘ │
↓ │
┌─────┐ │
│ GUI │<──────────────┘
└─────┘
- Rectangle 클래스가 두 가지 책임을 갖고 있기 때문이다.
- 책임1: 직사각형 모양의 수학적 모델 제공(
area()
) - 책임2: 직사각형을 그리는 것(
draw()
)
- 책임1: 직사각형 모양의 수학적 모델 제공(
따라서, 이 경우 Rectangle을 2개의 다른 클래스로 분리하여 설계하는 것을 고려할 수 있다.
┌─────────────┐ ┌───────────┐
│Computational│ │ Graphical │──────┐
│ Geometry │ │Application│ │
│ Application │ └─────┬─────┘ │
└──────┬──────┘ │ │
↓ ↓ │
┌───────────────┐ ┌───────────┐ ↓
│ Geometric │ │ Rectangle │ ┌─────┐
│ Rectangle │<───┼───────────┤──>│ GUI │
├───────────────┤ │+ draw() │ └─────┘
│+ area():double│ └───────────┘
└───────────────┘
인용: Bjarne Stroustrup
나는 우아하고 효율적인 코드를 좋아한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 깨끗한 코드는 한 가지를 제대로 한다. 6
응집도(cohesion)
단일 책임 원칙은 "응집도(cohesion)"와 관련이 있다.
단일 책임 원칙(SRP: Single-Responsibility Principle)은 톰 드마르코(Tom DeMarco)와 메이릴 페이지 존스(Meilir Page-Jones)의 연구에서 설명된 것으로, 그들은 이것을 응집도(cohesion)라 불렀다.2
응집도
하나의 클래스는 하나의 추상적인 개념을 나타내야 한다. 또한 각 클래스는 클래스의 목적과 의미를 한 줄로 기술할 수 있어야 한다. 만약 클래스를 간단하게 기술할 수 없다면, 아마도 하나 이상의 추상적인 개념을 나타내고 있을 것이다. 클래스에 추가된 책임들은 클래스의 설명에 부합해야 한다.7
개방-폐쇄 원칙(OCP)
소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.8
- 1988년 버트란드 마이어(Bertrand Meyer)가 만든 용어.
OCP는 시스템 아키텍처를 떠받치는 원동력 중 하나다. OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다. 이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.9
- OCP와 거리가 먼 소프트웨어의 경우, 변경이 단계적으로 퍼져나갈 수 있다.
- OCP를 잘 따르는 소프트웨어의 경우, 변경이 필요할 때 기존 코드를 수정하기보다는 새로운 코드를 추가하는 방법을 쓰게 된다.
- [[/pattern/template-method]]의 기반 역할 클래스가 OCP 원칙을 따르는 예라 할 수 있다.
이 원칙도 다른 원칙들처럼 지키기 어려운 편이다. 따라서 엉클 밥은 일단 변경을 기다렸다가 해결하는 전략을 제안한다.
폐쇄는 완벽할 수 없기 때문에, 전략적이어야 한다. 즉, 설계자는 자신의 설계에서 닫혀 있는 변경의 종류를 선택해야 한다. 가장 그럴 법한 종류의 변경을 추측하고, 그 변경에 대해 보호할 수 있는 추상화를 작성해야 한다.
(중략)
변경이 일어나면, 나중에 일어날 그런 종류의 변경으로부터 보호하는 추상화를 구현한다. 즉, 첫 번째 총알은 그냥 맞고, 그 총에서 쏘는 다른 총알에 대해서는 확실히 보호한다는 것이다.10
이 맥락에서 엉클 밥은 변경을 기다리는 방법, 즉 빨리 감지해내는 방법도 제안한다.
- 테스트 코드를 먼저 작성한다.
- 짧은 주기로(일 단위) 개발한다.
- 기반구조보다 기능 요소를 먼저 개발하고, 자주 이 기능 요소를 이해당사자(stakeholder)에게 보여준다.
- 가장 중요한 기능 요소를 먼저 개발한다.
- 빨리, 자주 릴리즈한다. 가능한 한 자주 고객과 사용자 앞에서 시연한다.
리스코프 치환 원칙(LSP)
1988년, Barbara Liskov가 논문 "[[/clipping/barbara-liskov/data-abstraction-and-hierarchy]]{Data Abstraction and Hierarchy}"에서 서브 타입을 다음과 같이 정의하였다.
What is wanted here is something like the following substitution property [6]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.
여기에 필요한 것은 다음과 같은 치환(substitution) 원칙이다. S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.
이 개념을 리스코프 치환 원칙이라 부른다.
논문 요약: [[/clipping/barbara-liskov/data-abstraction-and-hierarchy]]
다음은 리스코프 치환 원칙을 준수하는 예제이다.11
┌─────────┐ ┌──────< I >─┐
│ Billing ├──>│ License │
└─────────┘ ├────────────┤
│+ calcFee() │
└────────────┘
▵
┌───────┴──────┐
┌──────┴───┐ ┌───┴──────┐
│ Personal │ │ Business │
│ License │ │ License │
└──────────┘ ├──────────┤
│- users │
└──────────┘
- License의 하위 타입인 Personal License와 Business License 둘 다 License를 치환할 수 있다.
- Billing 애플리케이션의 행위가 License의 서브 타입인 Personal License와 Business License 중 어느 것에도 의존하지 않는다.
엉클 밥에 의하면 LSP는 시대의 흐름에 따라 더 넓은 의미로 재평가되었다.
객체 지향이 혁명처럼 등장한 초창기에는 앞서 본 것처럼 LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주되었다. 하지만 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해 왔다.12
인터페이스 분리 원칙(ISP)
인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.13
- 어떤 클라이언트에서는 사용하지 않고, 다른 어떤 클라이언트에서는 사용하는 메소드가 있다고 하자.
- 이 메소드의 변경이 메소드를 사용하지 않는 클라이언트에 영향을 줄 때 인터페이스를 분리하도록 한다.
- 클래스는 자신이 실제로 사용하는 메소드에만 의존해야 한다.
의존관계 역전 원칙(DIP)
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
- 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.14
요약하자면, "추상화에 의존하라"는 원칙이다.
"파이썬으로 살펴보는 아키텍처 패턴"에서는 이 원칙에 대해 단순한 설명을 덧붙인다.
DIP의 첫 부분은 비즈니스 코드가 기술적인 세부 사항에 의존해서는 안 된다는 의미다. 대신 양 쪽 모두 추상화를 사용해야 한다. 15
한편, 위에서 언급한 상위 모듈(high level module)과 하위 모듈(low level module)에 대해서도 구체적으로 설명한다.
고수준 모듈high-level module은 여러분의 조직에서 정말 중요하게 여기는 코드다. 제약 회사에 근무한다면 고수준 모듈은 환자와 임상시험을 관리한다. 은행에서 근무한다면 고수준 모듈은 거래나 외환을 관리한다. 고수준 모듈은 실세게의 개념을 처리하는 함수, 클래스, 패키지를 말한다.
반대로 저수준 모듈low-level module은 여러분의 조직에서 신경 쓰지 않는 코드다. HR 부서가 파일 시스템이나 네트워크 소켓에 관심을 갖을 가능성이 낮다. 여러분이 SMTP, HTTP, AMQP 등을 재무팀과 의논하는 경우도 드물 것이다. 기술적이지 않은 관련자들에게 이런 저수준 개념은 흥미로운 대상이 아니거나 중요하지 않다. 이런 관련자들은 고수준의 개념이 정상으로 작동되는지만 신경 쓴다. 급여 시스템이 정시에 정상적 실행되면 사업 부서는 급여 시스템이 크론 잡cron job인지, 쿠버네티스Kubernetes에서 실행되는 일시적인 함수인지에 대해 신경 쓰지 않는다.
의존성은 꼭 임포트나 호출만을 뜻하지 않는다. 대신 한 모듈이 다른 모듈을 필요로 하거나, 안다는 좀더 일반적인 생각이 의존성이다. 16
클린 아키텍처에서는 안정된 추상화를 위한 실천법을 제안한다.
- 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조하라. 이 규칙은 언어가 정적 타입이든 동적 타입이든 관계없이 모두 적용된다. 또한 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 [[/pattern/abstract-factory]]{추상 팩토리(Abstract Factory)}를 사용하도록 강제한다.
- 변동성이 큰 구체 클래스로부터 파생하지 말라. 이 규칙은 이전 규칙의 따름 정리이지만, 별도로 언급할 만한 가치가 있다. 정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 상속은 아주 신중하게 사용해야 한다. 동적 타입 언어라면 문제가 덜 되지만, 의존성을 가진다는 사실에는 변함이 없다. 따라서 신중에 신중을 거듭하는 게 가장 현명한 선택이다.
- 구체 함수를 오버라이드 하지 말라. 대체로 구체 함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다. 이러한 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
- 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라. 사실 이 실천법은 DIP 원칙을 다른 방식으로 풀어쓴 것이다.17
참고문헌
- Clean Code / 로버트 C. 마틴 저/박재호, 이해영 역 / 인사이트(insight) / 초판 1쇄 발행 2013년 12월 24일 / 원제: Clean Code
- 만들면서 배우는 클린 아키텍처 / 톰 홈버그 저/박소은 역 / 위키북스 / 초판 발행 2021년 11월 26일 / 원제: Get Your Hands Dirty On Clean Architecture
- 클린 소프트웨어 / 로버트 C. 마틴 저 / 이용원, 김정민, 정지호 공역 / 제이펍 / 초판 1쇄 2017년 05월 15일 / 원제 : Agile Software Development, Principles, Patterns, and Practices
- 클린 아키텍처 / 로버트 C. 마틴 저/송준이 역 / 인사이트(insight) / 초판 1쇄 2019년 08월 20일 / 원제 : Clean Architecture: A Craftsman's Guide to Software Structure and Design
- 파이썬으로 살펴보는 아키텍처 패턴 / 해리 퍼시벌, 밥 그레고리 저/오현석 역 / 한빛미디어 / 2021년 06월 03일 / 원서 : Architecture Patterns with Python
- 프리팩토링 / 켄 푸 저 / 서우석 역 / 한빛미디어 / 초판 발행 2006년 10월 20일
주석
-
클린 아키텍처. 3부 설계 원칙. 63쪽. ↩
-
클린 아키텍처. 7장. 66쪽. ↩
-
만들면서 배우는 클린 아키텍처. 2장. 13쪽. ↩
-
클린 소프트웨어. CHAPTER 8. 126쪽. ↩
-
클린 코드. 1장. 9쪽. 스트롭스트룹이 어디에서 한 말인지는 아직 찾지 못했다. ↩
-
프리팩토링. 6장. 116쪽. ↩
-
클린 아키텍처. 8장. 74쪽. ↩
-
클린 아키텍처. 8장. 79쪽. ↩
-
클린 소프트웨어. CHAPTER 8. 139쪽. ↩
-
클린 아키텍처. 9장. 82쪽. ↩
-
클린 아키텍처. 9장 84쪽. ↩
-
클린 소프트웨어. CHAPTER 11. 166쪽. ↩
-
파이썬으로 살펴보는 아키텍처 패턴. 0장. 31쪽. ↩
-
파이썬으로 살펴보는 아키텍처 패턴. 0장. 30쪽. ↩
-
클린 아키텍처. 11장 93쪽. ↩