SOLID Design Principles
이번 포스트는 소프트웨어 Design에 있어 5가지 주요 원칙인 SOLID Principles에 대해 다룬다.
SOLID Design Principles
Hierarchy of Pattern Knowledge
| 단계 | 개념 |
|---|---|
| OO Basics | 추상화, 캡슐화, 다형성, 상속 |
| OO Principles | 변화 캡슐화, 합성 우선, 인터페이스 기반 설계 |
| Design Pattern | 전략 패턴 (Strategy Pattern) |
| Design Smells | 나쁜 설계 징후들 |
Design Smells
- 나쁜 설계의 징후들
-
유지보수를 어렵게 하고, 변화에 취약하게 한다.
- 종류들
| 이름 | 증상 |
|---|---|
| Rigidity (경직성) | 수정 시 연쇄적 변경 발생 |
| Fragility (취약성) | 수정이 다른 부위에서 의도치 않은 오류 발생 |
| Immobility (부동성) | 재사용이 어려움 (결합도가 높음) |
| Viscosity (점착성) | 정석적인 코드 작성보다 “빠른 해킹”이 더 쉬움 |
| Needless Complexity (불필요한 복잡성) | 현재 필요하지 않은 복잡한 구조 존재 |
| Needless Repetition (불필요한 반복) | copy-paste 된 반복적 코드 |
| Opacity (불투명성) | 코드의 목적/의도가 이해하기 어려움 |
- Design Smells은 잘못 관리된 의존성 (Dependency) 에서 주로 발생한다.
- SOLID 원칙은 이러한 스멜을 줄이고 유지보수성이 높은 코드를 만들기 위한 가이드이다.
Dependency Management
- 의존성 관리 실패 -> 스파게티 코드를 유발한다.
- 어떻게 의존성을 구성해야할까? 기준이 무엇일까?
- SOLID Principles
Object-Oriented Design Principles
대표 설계 원칙들
- SOLID 원칙 : 핵심원칙
- GRASP Pattern : 책임 할당 중심 설계 원칙
- OO 원칙 : 변화 캡슐화 / 합성 우선 / 인터페이스 기반 설계
SOLID Principles (5대 원칙)
- 의존성 관리에 대한 원칙적 접근을 제공한다.
- 복잡한 디자인 패턴의 기반이 되는 튼튼한 설계 원칙이다.
- 유연성, 견고성, 재사용성이 높은 코드를 작성이 가능하다.
| 원칙 | 설명 |
|---|---|
| SRP | 클래스는 하나의 책임만 가져야 함 |
| OCP | 확장에는 열려 있고, 수정에는 닫혀 있어야 함 |
| LSP | 서브타입은 부모 타입을 대체할 수 있어야 함 |
| ISP | 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스 분리 |
| DIP | 고수준 모듈이 저수준 모듈에 의존하면 안 됨 → 둘 다 추상화에 의존해야 함 |
1. SRP
SRP = Single Responsibility Principle
-
“클래스는 하나의, 오직 하나의 변경 이유만을 가져야 한다.”
- 책임이 많을수록 변경 가능성이 높아진다.
- 이는 곧 버그 발생 가능성을 높인다.
- 하나의 책임만을 가지게 될 때 생기는 장점
- 변경 범위가 최소화된다.
- 변경 시 다른 기능에 미치는 영향이 최소화된다.
- 응집도 (Cohesion)가 높은 클래스가 되어 유지보수성이 높아진다.
Responsibility란
- 클래스의 책임 / 계약 / 의무
- 이 클래스가 왜 바뀌어야 하는지에 대한 변경 이유이다.
SRP 위반 사례 1) Student 클래스
- Student 클래스에 정렬 방법을 구현
class Student implements Comparable {
int compareTo(Object o) { ... }
}
- 문제점
- Student는 비즈니스 Entity인데 정렬 방법까지 책임을 가진다.
- 이는 SRP를 위반한다.
- 정렬 기준을 변경할 때 Student 클래스 및 모든 관련 클라이언트 재컴파일이 필요하다.
- 해결방법
정렬 책임을 분리한다.
-> 별도의 Comparator를 구현하면 된다. (이는 정렬 기준이 변경해도 Student 클래스에 영향을 주지 않는다.)
SRP 위반 사례 2) Rectangle 클래스
- Rectangle 클래스가 두 개의 서로 다른 책임을 가진다.
- Computational Geometry Application : area() 메서드 사용
- Graphical Application (GA) : draw( ) 메서드를 사용한다.
문제점
- CGA, GA가 둘 다 Rectangle에 의존하기에 한 쪽 변경 시 다른 쪽도 재 컴파일이 필요하다.
- 서로 관련 없는 두 책임이 한 클래스에 묶여 있다.
개선 방법
- Rectangle 클래스는 primitive attributes만 유지한다.
- GeometricRectangle, GraphicRectangle으로 분리 → 서로 독립적 변경 가능.
- 각 책임이 독립적으로 변경 가능하여 유지보수성이 높아진다.
Responsibility 식별이 어려운 경우는 어떻게 할 것인가?
“확장을 하다보니 늦게 깨달을 수도 있다.”
- 단일 인터페이스에 두 책임을 혼재 -> SRP 위반
- 애플리케이션 변화 방향에 따라서 책임을 구분하면 된다.
- 불필요한 복잡성 (Needless Complexity)은 피해야 한다.
- 증상이 없으면 성급하게 SRP를 적용할 필요는 없다.
2. OCP
OCP = Open-Closed Principle
- “소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.”
- 기존 코드를 수정하지 않고 새로운 기능을 추가가 가능해야 한다.
- 즉, 변경을 최소화하고, 확장 가능성을 확보해야 한다.
Open for Extension : 모듈의 기능을 확장할 수 있다.
Closed for Modification : 확장을 위한 변경이 기존 코드에 큰 영향을 주지는 않아야 한다.
Violation indicator : 변경 시에 rigidity (경직성) 발생 위험이 있다.
위반 징후
- 한 가지 변경이 여러 모듈에 연쇄적 변경을 유발하게 된다면
- 이는 Design Smell에 속한다.
Abstraction
-
확장은 가능하고, 기존 코드에 수정하지 않기 위한 핵심원리이다.
-
추상화 (Abstraction)을 사용하면 OCP 를 달성 할 수 있다.
구체 클래스가 아닌 인터페이스에 의존
- 어떤 변화에 대해서 OCP를 적용할지 선택적으로 판단해야 한다.
- 모든 경우에 과도한 abstraction 도입은 지양해야 한다.
- 비용 고려도 필요하다.
3. LSP
LSP = Liskov Substitution Principle
-
“Subtype은 언제나 Base type으로 교체가 가능해야 한다.”
- 부모 타입으로 정의된 변수에 자식 타입 객체를 할당해도 프로그램은 정상적으로 동작해야 한다.
- IS-A 관계가 의미상으로도 성립해야 한다.
Subtyping vs Implementation Inheritance
| 구분 | 설명 |
|---|---|
| Subtyping | IS-A 관계 성립 → LSP 만족 |
| Implementation Inheritance | 코드 재사용 목적 → IS-A 관계 성립 여부는 보장 안 됨 |
대부분의 OOP 언어 (Java, C++, C#)은 extends가 둘 다 구현 해준다.
LSP 위반 사례
- Queue extends List
Queue는 FIFO 구조로 작동한다.
따라서 List의 임의 접근 연산과 의미적으로 다르다. => LSP 위반
- Java.sql.Time extends java.util.Date
Time은 Date로 교체가 불가능하다.
- 만약 f(PType x) 함수가 CType이 들어오면 오작동한다. => LSP 위반
- 개발자가 if (x instanceof CType) 같은 코드를 추가하면 => OSP 위반도 발생할 수 있다.
4. DIP
DIP = Dependency Inversion Principle
- “고수준 모듈(high-level module)은 저수준 모듈(low-level module)에 의하면 안된다.”
- 둘 다 abstraction에 의존해야 한다.
Why inversion?
= 기존 structured design은 상위 계층 -> 하위 계층으로 의존성이 존재한다.
- 의존성 관리에 용이하다.
- 고수준 정책 (Policy)의 재사용이 용이하다.
- 변경에 대한 유연성이 증가한다.
5. ISP
ISP = Interface Segregation Principle
- “클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안된다”
- 많은 시스템에서 Fat Interface가 만들어진다.
- 서로 다른 목적의 client 들이 동일 Interface를 공유하게 되면
- interface가 변경할 때 불필요한 영향을 받는다.
- client가 원하지 않는 불필요한 메서드까지 구현을 알아야한다.
- 이는 설계 응집도를 낮춘다.
해결 방법
- Interface를 Client 관점에서 잘게 나누어 설계한다.
- Client는 딱 필요한 기능만 제공받는 Interface에만 의존하게 된다.