SOLID : 소프트웨어 설계 원칙
1. SRP : Single-Responsibility Principle
2. OCP : Open-Closed Principle
3. LSP : Liskov Substitution Principle
4. ISP : Interface Segregation Principle
5. DIP : Dependency Inversion Principle
R.C Martin이 이야기한 다섯가지 소프트웨어 설계 원칙의 각 앞글자를 따서 SOLID라 부른다.
1. The Single-Responsibility Principle (SRP)
클래스는 단하나의 책임, 변경사유 만을 가져야 한다.
A class should have one, and only one, reason to change
* SRP에서 말하는 책임, Responsibility는 class의 계약사항 또는 의무사항을 말함.
* 많은 책임 == 변경 가능성이 높음
* class변경이 많아질수록, 버그 발생 가능성도 더 커진다.
* 변경들은 다른 클래스에 영향을 미칠 수 있다.
그러므로 (하나의 클래스에) 결합된 책임을 별도의 클래스로 분리한다.
관련 지표 : 응집도 (Cohesion)
응집도란, 모듈의 다양한 책임들이 얼마나 강하게 연관되고 집중되어있는지를 말하는 것.
예를들어 위와같은 Rectangle class가 있다고 하자. 이렇게 가정된 상황에서 사각형의 넓이를 계산하는 앱은 area( )를 호출해서 사용하고, GUI로 사각형을 그리는 앱은 draw( )를 호출한다. 당장 보기엔 그럴듯 하다.
그러나, 넓이를 구하는 area( )와 관련해서 수정이 발생한다면? 넓이구하는 것과는 관련없는 Graphical App도 다시 컴파일 되어야 한다.
반대로, GUI를 그리는 draw( ) 메소드에 어떤 수정이 발생한다면, GUI와 관련없는 Computational Geometry App까지도 다시 컴파일 되어야 한다.
이 문제는 SRP를 위반해서 발생한 것, 즉, 하나의 클래스가 두가지 책임을 가지고 있기 때문에 발생하는 것이다.
이를 SRP를 준수하게 하여 해결해보자.
기존 Rectangle class는 Geometric Rectangle 이라는 class로 이름을 변경하고, 여전히 사각형의 길이와 너비를 갖고 area( )를 넓이를 계산하는 역할을 한다. 그리고, 사각형을 그리는 기능은 를 Graphic Rectangle 이라는 별도의 class로 분리했고, draw( ) 메소드는 Geometric Rectangle을 참조해서 가 GUI를 그릴 수 있다.
이제, GUI 앱에 변경이 필요하여 draw( ) 메소드를 수정하더라도, Computational Geometry App은 다시 컴파일이 필요 없게 되었다. 그러나 여전히 area( ) 와 관련한 수정을 하게되면, 넓이 계산과는 관련이 없더라도 Graphical App은 다시 컴파일이 되어야 한다.
이는 아직 완전히 SRP를 준수하지 않은 설계이기 때문에 발생하는 문제이다.
추가로 수정을 해보았다. 사각형의 길이와 너비를 표현하는 것은 Rectangle이라는 추상클래스로 분리하고, 면적을 구하는 area( ) - (책임1) - 메소드와 사각형을 GUI로 그려내는 draw( ) - (책임2) - 메소드는 각각의 클래스로 분리한 뒤, Rectangle을 generalization 하도록 설계했다.
이제, 두 앱은 서로 의존성이 없게 되었고, 한쪽의 수정이 발생해도 다른쪽까지 다시 컴파일 되어야 하는 문제는 해결되었다.
2. Open Closed Principle (OCP)
'확장에는 열려있고, 수정에는 닫혀야 한다'
Open for extension, Closed for modification
- Open for extension
- 모듈의 행동은 확장될 수 있다.
- 모듈이 무엇을 할지 변경할 수 있다.
- Closed for modification
- 확장하는 행위가 모듈의 아키텍처 변경과 같은 과도한 수정을 초래하진 않는다.
위반 하면? Rigidity smell (경직성 냄새) 발생함
→ 하나 변경하면 의존성 있는 모듈들에 연쇄적인 변경이 발생하게 됨
OCP를 위반한 안좋은 예를 하나 들어보자.
HR매니저에서는 모든 직원들의 급여의 총합을 구하고자 하는 상황이다.
Employee라는 '직원' 클래스를 만들고, staff, secretary, faculty 등 직원의 역할에 따라 하위 클래스를 만들었다.
HR매니저는 incAll( ) 이라는 메소드의 인자로 모든 직원들의 목록을 받아, 각 직원클래스의 타입을 확인하고 타입에 맞는 급여액을 인상하는 메소드를 호출하도록 구현했다.
만약, 'Engineer' 라는 새로운 역할의 직원타입을 추가해야 하는 상황이 발생했다고 가정해보자.
이와같이 if-else 문에 ENGINEER 타입을 확인하는 코드를 추가해야하고, incEngineerSalary( ) 메소드도 추가 구현해야 한다.
직원 유형이 증가한다면 점점더 if-else는 커질 것이고, 각 조건에 따른 처리를 위한 메소드도 추가될 것이다.
따라서 자칫하면 이해하기 어려워지고, 코드를 찾기도 어려워 질 수 있다. (물론 예시에서는 조건에 따라 단 한줄의 메소드 호출이라 찾기 쉽지만, 실전에서 아마도 그렇지 않을 것이다)
그렇다면, 더 좋은 설계는 어떻게 해야할까?
추상화(Abstraction)가 열쇠이다.
Employee에 추상메소드 incSalary( ) 를 선언하면, 하위 클래스들은 모두 그것을 구현해야만 하게끔 된다.
각 직원 유형클래스의 incSalary( ) 에서는 각자 알맞는 급여를 인상하는 코드를 구현하면 되고, Engineer가 추가되더라도 마찬가지이다. 이렇게 되면 HRMgr은 다시 컴파일 될 필요도 없다.
이러한 설계는 확장에는 열려있고, 수정에는 닫혀있다고 할 수 있다. (= OCP 준수)
추상화된 base class는 고정되어있고, 가능한 모든 하위 클래스들(derived classes)은 가능한 모든 동작에 대해 경계가 전혀 제한되어 있지 않다.
3. Liskov Substitution Principle (LSP)
- 파생클래스는 기본클래스로 대체 가능해야 한다.
(Derived classes must be substitutable for their base classes)
만약 C가 P의 하위유형(subtype) 이라면, 프로그램의 아무런 속성 변경을 하지 않고도 P의 객체를 C의 객체로 대체할 수 있다.
Subtyping
- IS-A 관계를 수립
- 인터페이스 상속(interface inheritance)으로도 알려져 있음
Implementation Inheritance
- 문법적인 관계와 구현만 재사용하고, '의미적'인 관계는 아니다.
- 코드 상속(code inheritance)으로도 알려져있음
Java, C++, C#과 같은 대다수의 OOP 언어들은 extends 와 같은 상속 키워드를 통해 Subtyping과 Implementation Inheritance모두를 수행한다. 그러나 몇몇 언어들은 그것들을 구분하기도 함.
상속을 사용하기로 결정할 때는 두번 생각하라!
만약 List를 상속받아 Queue를 구현한다면, Queue는 List를 대체 할 수 없으므로, LSP를 위반하게 된다.
만약 List의 구현을 재사용하기를 원한다면, 상속보다는 객체를 composition관계로 활용하는 것이 낫다
* LSP위반은 다른 위반을 초래할 수도 있다. *
위와같은 상속관계의 클래스가 있을 때, f( ) 의 인자로 PType대신 CType을 넣는것은 문법적으로 문제는 없다.
그런데, 만약 CType을 넣었을 때 f( )가 잘못된 동작을 한다면?
그것은 CType이 LSP를 위반하고있다는 의미가 된다.
이때, f( )의 담당개발자는 이런 오동작을 방지하기 위하여, 인자 x가 CType인지를 확인하는 코드를 넣기를 원할 수도 있다.
이와같이 말이다.
하지만 위와 같은 반응은 더 나쁘다.
이제, 이 코드는 OCP 또한 위반하게 되었다.
메소드 f는 PType의 모든 하위타입에 대해 닫혀있지 않은 상태가 되버렸기 때문이다.
4. Interface Segregation Principle (ISP)
클라이언트는 사용하지 않는 메소드에 의존하도록 강제되어서는 안된다.
Clients should not be forced to depend on methods t hey do not use
'인터페이스 분리 원칙'은 이름그대로 인터페이스를 나눠야 한다는 원칙이다.
어떤 기준으로 나눌 것인가? 필요한 기능만 제공하도록.
즉, 클라이언트가 실제로 사용하는 기능만 제공하게끔 나누자는 것. 클라이언트 별로 세분화된 인터페이스를 만들어야 한다는 것.
결국 결합도를 낮추고, 응집도를 높이는 방향으로 설계하자는 원칙이다.
뚱뚱한 인터페이스(Fat interface)라는 말은, 서로다른 클라이언트들에게 제공되는 여러 함수 뭉탱이들이 하나의 인터페이스에 모두 들어있는 것을 말한다. 그런데 이것은 클라이언트들 간에 불필요한 결합(coupling)을 만들게 된다.
하나의 클라이언트가 인터페이스 변경을 하게되면 다른 모든 클라이언트들은 모두 다시 컴파일 되어야만 한다.
ISP는 이런 응집력이 없는 인터페이스를 해결한다.
클라이언트는 응집력이 있는 인터페이스를 가진 추상 기본 클래스만 알아야 한다.
예를들어보자.
등록된 학생을 나타내는 StudentEnrollment라는 클래스가 있고, 이름, SSN, 송장번호에 대한 getter와 요금지불 동작을 메소드로 제공하고 있다고 가정하자.
RoasterApplication은 getInvoice( )와 postPayment( )를 호출하지 않는다고 가정하고,
AccountApplication은 getName( ) 과 getSSN( )을 호출하지 않는다고 가정해보자.
자, 요구사항이 변경되었다고 가정하자. postPayment( )에 새로운 argument를 추가해야 한다!
이런 요구사항에 의한 변경은... RoasterApplication조차도 강제로 다시 컴파일해서 다시 배포하게끔 한다. RoasterApplication 는 postPayment( )를 전혀 사용하지 않는데도 말이다.
더 나은 설계는 무엇일까?
이제, StudentEnrollment 객체의 각각의 사용자(Roaster Application과 Account Application)에게는 사용하는 메소드들만을 제공하는 인터페이스가 주어졌다.
이 설계는 상관없는 메소드의 변경으로 부터 각 앱이 불필요하게 다시 컴파일되고 다시 배포되어야 하는 짓을 하지 않아도 되도록 보호한다. 또한 사용하는 객체의 구현을 불필요하게 너무 많이 알게 되는 것도 방지한다.
5. Dependency Inversion Principle (DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안된다. 양쪽은 추상화에 의존해야 한다.
High-level moduels should not depend on low-level modules.
both should depend on abstractions.
'의존성 역전 원칙'은 하위 모듈이 (추상화된)상위모듈에 의존하도록 의존성을 역전 시키자는 설계 원칙이다.
이게 무슨말인가 하면, 일반적으로 상위모듈은 하위모듈에 의존을 하게끔 되어있다.
예를들어, 하위모듈인 '카메라센서' 모듈은 카메라센서 시작, 중지, 정지화면 촬영, 연속화면 촬영(동영상) 등의 기능을 제공할 수 있고 상위모듈인 '카메라 앱'은 이 제공되는 기능들을 사용하는 의존성 관계이다.
이 때, 카메라센서 제조사는 소니, 삼성, ST마이크로닉스 등 다양할 수 있다.
만약, 특정 카메라센서 제조사, 예를들어 삼성에서 신기능인 '초당 360장 촬영' 과 같은 기능을 추가로 제공한다던가 하는 변경(추가)이 발생하면, 상위모듈인 카메라앱은 영향을 받게 된다. 즉, 카메라앱은 삼성 카메라 모듈을 사용하는지를 확인(if-else statement) 하고 그에 따라 추가된 기능을 앱에서 제공하도록 구현을 변경하는 등의 영향을 받게 될 것.
이런 관계를 역전시키자는 원칙.
즉, 하위 모듈은 상위모듈이 제공하는 인터페이스(추상화)에 의존하여 구현하도록 강제하고, 상위 모듈은 제공했던 인터페이스를 사용하는 구조를 예를 들 수 있다.
이때 하위 모듈이 아무리 변경을 하더라도, 상위모듈은 인터페이스만 사용하면 되므로 영향을 받지 않는다.
물론, 하위모듈에서 새로운 기능을 추가로 제공하고자 한다면 상위모듈에서 인터페이스를 수정해줘야만 한다.
즉, ownership까지 역전되겍 된다.
이를 적용한 사례로는 Android의 HAL layer가 있다.
정리
이러한 SOLID 설계원칙들은...
- 의존성 관리를 도와준다.
- 더 나은 유지보수성, 유연성, 견고성, 재사용성!
- 추상화는 중요하다.
'개발자의 기록 노트' 카테고리의 다른 글
마우스 위치 강조하기 (feat. PowerToys) (0) | 2023.06.13 |
---|---|
ChatGPT : 거짓 정보를 그럴듯하게 포장해내는 인공지능 (0) | 2023.05.13 |
USB 규격 마스터링 (0) | 2023.05.07 |
[Rust] 윈도우 환경에서 컴파일 실패 : linker link.exe not found (2) | 2023.04.05 |
[Rust] main함수에서 test function 호출하는 방법? (0) | 2023.04.05 |
[Rust] 시작하기 - 개발 환경 만들기 (0) | 2023.03.26 |