객체 지향 설계 5원칙 - SOLID
객체 지향 설계 5원칙 - SOLID
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Principle): 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 역전 원칙
응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것이다.
SOLID를 잘 녹여낸 소프트웨어는 그렇지 않은 소프트웨어에 비해 상대적으로 이해하기 쉽고, 리팩터링과 유지보수가 수월할 뿐만 아니라 논리적으로 정연하다.
SOLID는 객체 지향 4대 특성을 발판으로 하고있으며, 디자인 패턴의 뼈대이며 스프링 프레임워크의 근간이기도 하다.
SRP - 단일 책임 원칙
“어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.”
예) 객체 지향 세계에서 남자는 반드시 군대를 가고, 여자는 절대로 군대를 가지 않는다고 가정했을 때, 사람 클래스에 군번 속성이 있다면 어떻게 될까?
=> 사람 클래스를 남자 클래스와 여자 클래스로 분할하고 남자 클래스에만 군번 속성을 갖게 해야 한다. 남자 클래스와 여자 클래스에 공통점이 없다면 사람 클래스는 제거하면 되고, 공통점이 많다면 사람 클래스를 상위 클래스로 해서 공통점을 사람 클래스에 두고 남자 클래스와 여자 클래스는 사람 클래스를 상속하고 차이점만 각각 구현하면 된다.
하나의 속성이 여러 의미를 갖는 경우도 단일 책임 원칙을 지키지 못하는 경우다.
데이터베이스 테이블을 설계할 때는 정규화라고 하는 과정을 거치게 되는데, 정규화 과정을 조금 더 확장해서 생각해 보면 테이블과 필드에 대한 단일 책임 원칙의 적용이라고 할 수 있다.
단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화임을 알 수 있다. 애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자. 또한 리펙토링을 통해 코드를 개선할 때도 단일 책임 원칙을 적용할 곳이 있는지 꼼꼼히 살피자.
OCP - 개방 폐쇄 원칙
“소프트웨어 엔티티(클래스,모듈,함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.” => 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
ex) JDBC - JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다. Connection 설정 부분을 별도의 설정 파일로 분리해두면 클라이언트 코드는 단 한줄도 변경할 필요가 없다. JDBC뿐만 아니라 iBatis, MyBatis, 하이버네이트 등등 데이터베이스 프로그래밍을 지원하는 라이브러리와 프레임워크에서도 개방 폐쇄 원칙의 예를 볼 수 있다.
개방 폐쇄 원칙을 따르지 않는다고 해서 객체 지향 프로그램을 구현하는 것이 불가능한 것은 아니지만 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다. 따라서 객체 지향 프로그래밍에서 개방 폐쇄 원칙은 반드시 지켜야 할 원칙이다.
LSP - 리스코프 치환의 원칙
“서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.”
객체 지향의 상속은 다음의 조건을 만족해야 한다.
- 하의 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스 할 수 있어야 한다.
위 두개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다. 하지만 위 문장대로 구현되지 않은 코드가 존재할 수 있는데 바로 상속이 조직도나 계층도 형태로 구축된 경우다.
하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
리스코프 치환 원칙 위반 사례 - 계층도/조직도
리스코프 치환 원칙 적용 사례 - 분류도
ISP - 인터페이스 분리 원칙
“클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.”
최소주의 원칙: 인터페이스를 통해 메서드를 외부에 제공할 때는 최소한의 메서드만 제공하라.
인터페이스는 그 역할에 충실한 최소한의 기능만 공개해야 한다.
DIP - 의존 역전 원칙
“고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.
“추상화 된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상회된 것에 의존해야 한다.”
“자주 변경되는 구체(Concrete) 클래스에 의존하지 마라”
의존 역전 원칙 적용 전(자주 변경되는 구체 클래스에 의존)
의존 역전 원칙 적용 후
자동차가 구체적인 타이어들(스노우타이어, 일반타이어, 광폭타이어)이 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 스노우타이어에서 일반타이어로, 또는 다른 구체적인 타이어로 변경돼도 자동차는 이제 그 영향을 받지 않는 형태로 구성된다.
기존에는 스토우타이어가 그 무엣어도 의존하지 않는 클래스였는데, 추상적인 것인 타이어 인터페이스에 의존하게 됐다. 바로 의존의 방향이 역전된 것이다.
그리고 자동차는 자신보다 변하기 쉬운 스노우타이어에 의존하던 관계를 중간에 추상화된 타이어 인터페이스를 추가해 두고 의존 관계를 역전시키고 있다. 이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.
정리 - 객체 지향 세계와 SOLID
SOLID는 객체 지향을 올바르게 프로그램에 녹여내기 위한 원칙이다.
SOLID를 이야기할 때 빼놓을 수 없는 것이 SoC이다. SoC는 관심사의 분리의 머리글자다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것이다.
하나의 속성, 하나의 메서드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어 있어야 한다는 것이 SoC다.
관심사가 다르고 변화의 시기가 다르면 분리해야 한다는 것이다.
SoC를 적용하면 자연스럽게 단일 책임 원칙(SRP), 인터페이스 분리 원칙(ISP), 개방 폐쇄 원칙(OCP)에 도달하게 된다.
- SRP(단일 책임 원칙): 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- OCP(개방 폐쇄 원칙): 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
- LSP(리스코프 치환 원칙): 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
- ISP(인터페이스 분리 원칙): 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
- DIP(의존 역전 원칙): 자신보다 변하기 쉬운 것에 의존하지 마라.
SOLID 원칙을 적용하면 소스 파일의 개수는 더 많아지는 경향이 있다. 하지만 이렇게 많아진 파일이 논리를 더욱 잘 분할하고, 잘 표현하기에 이해하기 쉽고, 개발하기 쉬우며, 유지와 관리, 보수하기 쉬운 소스가 만들어진다. SOLID 원칙을 적용함으로써 얻는 혜택에 비하면 늘어나는 소스 파일 개수에 대한 부담은 충분히 감수하고도 남을 만하다.