SOLID 원칙
머리말
Solid: 견고하고 단단한.
The SOLID principles: 견고하고 단단한 OOP 설계의 5가지 Principle (원칙).
- Single Responsibility: 단일책임
- Open-Closed: 개방-폐쇄
- Liskov Substitution: 리스코프 치환
- Interface Segregation: 인터페이스 분리
- Dependency Inversion: 의존관계 역전
Single Responsibility: 단일책임
하나의 클래스는 하나의 책임(목적)만을 가진다
Like Unity Component, 각 컴포넌트는 하나의 기능만 가진다.
플레이어의 Audio
, Input
, Movement
기능을 Player
라는 하나의 클래스로 합쳐서 만들지 않는다.
PlayerAudio
, PlayerInput
, PlayerMovement
각각의 클래스로 분리하여 단일 책임을 가지게 한다.
클래스가 제공하는 모든 기능은 이 책임과 부합한다.
클래스는 그 책임을 완전히 캡슐화해야 한다
클래스를 수정하는 이유도 단 하나의 이유여야한다.
클래스를 수정할 때 다른 클래스에 영향을 주지 않도록, 클래스의 책임을 분리해야한다.
S: Memo
- 함수도 마찬가지
- 하나의 함수는 하나의 책임(목적)만을 가진다.
- 클래스 당 코드 감소
- 클래스를 나누기 때문에, 한 클래스의 길이가 짧아진다.
- 클래스 의존성 감소
- 클래스가 수정되어도 다른 클래스에 영향을 주지 않는다.
=>
- Readability (가독성)
- 짧은 클래스가 읽기 쉽다.
- Extensibility (확장성)
- 작은 클래스로부터 상속이 쉽다.
- Reusability (재사용성)
- 부분에서 재사용할 수 있도록 작고 모듈식으로 설계
- 유지보수성 향상
Open-Closed: 개방-폐쇄
클래스 내부 수정 없이 동작을 확장할 수 있어야 한다.
확장에는 열러있고
- 모듈의 동작을 확장할 수 있다는 것을 의미
- 요구 사항이 변경될 때, 새로운 동작을 추가해 모듈을 확장
- 즉, 모듈이 하는 일을 변경할 수 있음
수정에는 닫혀있어야한다
- 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경 가능
- 모듈의 라이브러리 (ex:DLL) 의 수정이 필요 없음
O: Memo
AreaCalculator
안에 삼각형
, 사각형
의 넓이를 계산하는 코드를 직접 넣게 되면,
나중에 오각형
, 육각형
을 추가할 때 AreaCalculator
클래스를 직접 수정해야한다.
대신 CalculateArea()
추상 메서드를 가진 Shape
추상 클래스를 만들고,
삼각형
, 사각형
, 오각형
등의 클래스에서 이를 상속받아 구현한다.
이렇게 하면 AreaCalculator
클래스는 Shape.CalculateArea()
를 이용해 넓이를 계산하므로,
새로운 도형이 추가되어도 AreaCalculator
클래스를 직접 수정할 필요가 없다.
=> 코드를 직접 수정하지 않고도 새로운 기능을 추가할 수 있다.
Liskov Substitution: 리스코프 치환
파생 클래스는 기본 클래스를 대체할 수 있어야 한다
(파생, 자식, 하위) <-> (기존, 부모, 상위)
- 파생 클래스는 기존 클래스를 대체할 수 있어야 한다.
- 자식 클래스는 부모 클래스의 방향성을 유지해야 한다.
- 자식 클래스가 부모 클래스의 기능을 필요로하지 않거나 제거하는 경우, 이는 원칙 위반 (방향성을 따르지 않으므로)
부모 클래스를 자식 클래스로 바꿔도 동일하게 동작해야 한다.
- 하위 클래스를 강력하고 유연하게 만드는 원칙
- OOP의 상속을 사용하면 하위 클래스를 통해 기능을 추가할 수 있음
- 그러나, 주의하지 않으면 불필요한 복잡성이 발생
상속보다는 구성
= 상속보다는 인터페이스를 구현하도록
= is-a 보다는 has-a
- 클래스 계층 구조를 설정하기 전에 클래스 API를 고려
- 현실의 분류가 항상 클래스 계층 구조로 변환되는 것은 아님
- Ex: Car와 Train이 별도의 상위 클래스에서 상속받는 것이 더 합리적
- X:
Vehicle
(GoFoward
,Reverse
,TurnRight
,TurnLeft
) ->Car
,Train
- 기차는 길을 따라 움직이기 때문에
TurnRight
,TurnLeft
가 필요하지 않음
- 기차는 길을 따라 움직이기 때문에
- O:
IMovable
(GoFoward
,Reverse
),ITurnable
(TurnRight
,TurnLeft
)RoadVehicle
(IMovable
,ITurnable
) ->Car
RailVehicle
(IMovable
) ->Train
- X:
L: Memo
추상화를 단순하게 유지
- 하위 클래스에는 기본 클래스의 퍼블릭 멤버 존재
- 서브클래싱할 때 기능을 제거하는 경우 리스코프치환원칙 위배
- 부모 클래스의 방향성을 따르지 않는 것이므로
Interface Segregation: 인터페이스 분리
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
큰 하나의 인터페이스보다 쪼개진 작은 여러 개의 인터페이스가 좋다.
- 클라이언트가 자신이 이용하지 않는 메소드에 의존하지 않아야 한다는 원칙
- 큰 덩어리의 인터페이스들을 구체적이고 작은 단위로 분리
- 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 인터페이스를 분리해야 한다.
- 시스템 내부 의존성을 약화하고 유연성을 강화
Dependency Inversion: 의존관계 역전
객체 간의 상호작용은 추상화에 의존해야지, 구체화에 의존하면 안된다.
의존성 주입은 이 원칙을 따르는 방법 중 하나
- 소프트웨어 모듈들을 분리하는 특정 형식
- 상위(High-level) 모듈은 하위(Low-level) 모듈의 것을 직접 가져오면 안됨
- 둘 다 추상화에 의존해야 함
- 추상화는 세부 사항에 의존해서는 안됨.
- 세부 사항이 추상화에 의존해야 함.
- 클래스가 다른 클래스와 관계가 있으면 안됨
- 클래스가 다른 클래스의 작동 방식을 많이 알고 있으면 안됨
- 종속성(dependency) 또는 결합(coupling) 발생
- 종속성은 어느 잠재적인 위험
- X:
Switch
가Door
,Light
,Fan
을 모두 직접 제어 (의존)- 다른 기기가 추가되면
Switch
를 수정해야 함
- 다른 기기가 추가되면
- O:
Swtich
가ISwitchable
을 제어Door
,Light
,Fan
은 각각ISwitchable
을 구현