SOLID - 객체지향 설계 5원칙
객체지향의 가장 기저가 되는 원칙이자, 객체 지향을 하기 위해 지켜야 하는 5개의 원칙이다.
이름 그대로 5개로 이루어져 있다.
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- OCP(Open-Closed Principle) : 개방-폐쇄 원칙
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
- DIP(Dependency Inversion Principle) : 의존 역젼 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
위의 5개의 원칙을 사용하여 프로그래밍을 하면, 유지 보수와 뛰어난 확장성을 가질 수 있다고 하는데, 각각의 특징과 개념을 알아보자
SRP(Single Responsibility Principle)
하나의 모듈은 하나의 책임만을 가진다.
는 것이다.
즉, 특정 모듈은 하나의 이유로 변경되어야 한다는 것인데, 프로그램이 기본적으로 어떠한 요구사항에 대해 영향을 받는 부분이 적어져야 한다는 것이다.
이것은 응집도를 높이고 결합도를 낮게 하는 효과가 있다.
이유는 하나의 모듈이 여러 개의 역할을 한다면, 즉 모듈의 사용 이유가 여러개라면 하나의 모듈이 수행해야 하는 책임이 많아지게 되고 이는 결합도의 상승을 불러일으키게 되는데, 하나의 책임만을 갖게 된다면 코드의 가독성 향상과 유지보수 용이라는 이점을 누릴 수 있어지기 때문이다.
이 SRP의 핵심은 바로 응집성
이라고 할 수 있을 것이다.
OCP(Open-Closed Principle)
소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려 있고(open) 변경에는 닫혀있어야(closed)한다
말하자면 소프트웨어의 개체는 기존 개체를 변경하지 않은 상태에서 확장 가능해야 한다는 것이다.
이는 객체지향 설계의 장점을 극대화하는 중요한 원리인데, 어떤 요구사항의 변경이나 추가가 발생한다 해도 기존 구성요소의 수정 대신 확장을 통해 재사용할 수 있도록 하는 것이다.
이 OCP는 재사용 가능하고, 유지보수가 용이한 코드를 만드는 방식의 기초가 된다.
추가적으로 OCP에는 보통 인터페이스를 자주 사용한다.
이는 인터페이스가 가진 추상화 덕분인데, 예를 들어 어떤 내용을 적용할 때에 인터페이스 자체를 변경하지 않고 해당 메서드를 상속받으면 되기 때문이다.
악기
라는 인터페이스와 기타
, 피아노
클래스가 있다고 가정해보자.
먼저 악기
는 연주
라는 메소드를 가진다. 이를 통해 악기를 연주할 수 있다.
이 연주 방식은 매 악기마다 다르게 해야 할 것이다.
피아노
의 경우는 '건반 누르기', 기타
의 경우는 '현 치기'이다.
그러면 개방 폐쇄 원칙을 지키려면 기존 내용의 변경 없이 새로운 것이 추가됨으로서 해당 내용이 가능해야 할 것이다.
알다시피 인터페이스를 사용하여 각각의 클래스를 상속받게 되면 클래스 변경 없이 연주
를 할 때에 '건반 누르기' / '현 치기'가 상속받은 클래스에 맞추어 동작하게 될 것이다.
OCP의 핵심은 추상화
와 다형성
이라고 할 수 있을 것이다.
LSP(Liskov Substitution Principle)
하위 클래스가 상위 클래스에서 가능한 행위를 수행할 수 있어야 한다.
즉, 부모 클래스와 자식 클래스의 행위에 일관성이 있어야 한다는 것으로 부모 클래스 대신 자식 클래스의 인스턴스를 사용해도 문제가 없어야 한다는 것이다.
간단히 말하자면 어떤 행위를 설명할 때 상위 클래스는 하위 클래스로 대체 가능하다는 것이다.
예를 들어서 설명하자면
질럿
리버
마린
이라는 클래스가 있고 프로토스
라는 클래스가 있다고 해보자.
여기서 프로토스
의 몇 가지 행위를 정의해본다.
- 프로토스는 미네랄을 필요로 한다.
- 프로토스는 인구수를 필요로 한다.
- 프로토스는 아이어에 산다.
먼저 질럿
클래스가 프로토스
클래스를 상속받을 때.
- 질럿은 미네랄을 필요로 한다.
- 질럿은 인구수를 필요로 한다.
- 질럿은 아이어에 산다.
해당하는 모든 행위를 만족하고, 대체 가능하다.
다음으로 리버
클래스가 프로토스
클래스를 상속받을 때.
- 리버는 미네랄을 필요로 한다.
- 리버는 인구수를 필요로 한다.
- 리버는 아이어에 산다.
해당하는 모든 행위를 만족하고, 대체 가능하다.
다음으로 마린
클래스가 프로토스
클래스를 상속받을 때.
- 마린은 미네랄을 필요로 한다.
- 마린은 인구수를 필요로 한다.
- 마린은 아이어에 산다.
1, 2는 만족하지만 3번은 만족하지 않는다는 것을 알 수 있다.
따라서 마린을 프로토스로 상속받는다면 LSP를 만족하지 않는 것이다.
LSP를 만족하는 설계를 할 때에질럿
, 리버
는 프로토스
를 상속받을 수 있다.마린
은 따로 설계된 테란
클래스를 상속받아야 할 것이다.
혹은, 셋 모두를 만족하기 위해서는
유닛
- 유닛은 미네랄을 필요로 한다.
- 유닛은 인구수를 필요로 한다.
처럼 모두를 만족하는 공통의 연산을 제공하는 클래스를 만들어 주면 된다.
결국 리스코프 치환 원칙은 위의 개방 원칙을 따르게 된다.
즉, 해당 원칙을 어기면 자연스럽게 OCP원칙을 어기게 된다는 것이다.
LSP의 핵심은 상속
이라고 할 수 있을 것이다.
DIP(Dependency Inversion Principle)
의존 관계를 맺을 때에는 변하기 쉬운것 보다는 변하기 어려운 것에 의존해야 한다.
여기서 '변하기 쉬운것'은 '구체적인 것'즉 클래스
를, '변하기 어려운 것'은 '추상적인 것'즉 인터페이스
나 추상 클래스
를 의미한다.
즉 의존 관계를 맺을 때에는 클래스보다는 인터페이스 혹은 추상 클래스에 의존해야 한다는 것이다.
이는 저수준 모듈(클래스)이 변경되어도 고수준 모듈(인터페이스/추상클래스)의 변경이 필요없는 형태가 이상적이라는 것이다.
말하자면 이전의 OCP 내용에서 기타
, 피아노
외에 바이올린
이라는 클래스가 추가된다고 할 때에 인터페이스에서는 변경이 이루어지지 않고, 특정 메소드(예를 들면 setInstrument)를 사용하여 주입시켜주면 된다는 것이다.
이를 의존성 주입이라 하고, 아마 Spring을 사용하면서 자주 접하게 될 것이다.
ISP(Interface Segregation)
하나의 클래스는 자신이 사용하지 않는 인터페이스를 구현해서는 안된다.
말하자면 뭉뚱그려진 인터페이스가 아니라 구체적인 인터페이스를 통해 구현하라는 뜻이다.
위의 설명에서 추가로 이야기 해보자면
질럿
, 리버
, 마린
에 대해서 나는
질럿
리버
는프로토스
마린
은테란
클래스를 상속받아야 한다고 했다.
그럼 만약에 이렇게 하지 않고 그냥
스타크래프트 유닛
이라는 인터페이스를 만들어서
- 스타크래프트 유닛은 미네랄을 필요로 한다.
- 스타크래프트 유닛은 인구수를 필요로 한다.
- 스타크래프트 유닛은 아이어에 산다.
- 스타크래프트 유닛은 테란 지역에 산다.
- 스타크래프트 유닛은 지구에 산다.
- 스타크래프트 유닛은 가스를 필요로 한다.
등등....을 정의한다고 하자.
그리고 마린
이 이 인터페이스를 상속받아 사용하면
1, 2, 4만을 사용하게 될 것이다.
이렇게 되면 마린
은 자신이 사용하지 않는 기능에 영향을 받게 되며, 이는 ISP원칙에 위배된다는 것이다.
따라서 공통된 기능을 분리하여 나누어 두고, 이를 상황에 따라 사용하는 것이 좋을 것이다.
ISP의 핵심은 인터페이스 분리
이다.
참고로 SRP와의 차이점은 SRP가 클래스의 단일책임에 집중한다면 ISP는 인터페이스의 단일책임에 집중한다는 것이다.
정리하자면
SRP와 ISP는 특정한 단일 책임에 집중하여 객체가 커지는 것을 막아준다.
이러한 원칙 덕분에 특정 기능의 변경이 다른 곳에 영향을 끼치는 Side Effect를 막을 수 있어 기능 추가 및 변경이 용이해질 수 있다.
OCP는 기존 코드의 변화를 줄인 상태에서 기능 확장을 용이하게 해준다.
이는 자주 변경되는 부분을 추상화
하고 다형성
을 이용함으로서 구현된다.
LSP와 DIP는 위의 OCP의 구현을 할 수 있도록 돕는 개념이다.
LSP의 경우는 추상화
를, DIP는 다형성
을 돕는다.
SOLID원칙이 사실 개발할 때에는 그냥 자연스럽게 이러이러한 방법으로 쓰면 되겠구나~ 했었는데, 실제로 깊게 파고 보면 더욱 어려운 개념이 숨어있고 다양한 Trade-off가 있다고 한다.
이를 잘 지키면서 개발해야 할 것 같다는 생각이 들었다.
'이론 정리' 카테고리의 다른 글
캐시 교체 정책에 관해서 (0) | 2022.11.06 |
---|---|
OIDC와 OAuth에 대해.. (0) | 2022.10.10 |
RBAC과 ABAC 기초 정리 (0) | 2022.08.18 |
XSS와 sql injection에 관하여 (0) | 2022.06.11 |
MA(모놀리식)와 MSA에 관하여 (0) | 2022.04.07 |