[OOP] Chap5. 객체 지향 설계 5원칙 - SOLID
스프링을 입문을 위한 자바 객체 지향의 원리와 이해 를 읽고 정리한 글입니다.
도구를 올바르게 사용하는 방법이 있는 것처럼
객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계하는 5원칙이 바로 SOLID 이다.
응집도(Cohension)는 높이고, 결합도(Coupling)는 낮추라는 고전 원칙을 객체 지향의 관점에서 재정립한 것이다.
(결합도 - 모듈(클래스)간의 상호의존도, 응집도 - 하나의 모듈 내부에 존재하는 구성요소들의 기능적 관련성)
SOLID는 객체지향의 4대 특성을 발판으로 하고 있으며 디자인 패턴의 뼈대가 되고, 스프링 프레임워크의 근간이기도 하다.
이를 잘 녹여낸 소프트웨어는 상대적으로 이해하기 쉽고, 리팩터링과 유지보수가 용이하다.
SRP(Single Responsibility Principle) : 단일 책임 원칙
"어떤 클래스를 변경해야하는 이유는 오직 하나뿐이어야 한다"
하나의 클래스에 다수의 클래스가 의존 관계를 맺지 않고,
클래스의 역할(책임)을 분리하라는 것이 단일 책임 원칙이다.
클래스뿐만 아니라 속성, 메서드, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있는 개념이다.
(ex) SRP를 지키지 않는 경우
- 속성
class 사람 {
String 군번;
...
}
사람 로미오 = new 사람();
사람 줄리엣 = new 사람();
줄리엣.군번 = "1234567" // ?
여자의 경우 군번이 없는 경우가 존재할 수 있는데,
이 경우에 군번 속성에 값을 할당하거나 읽어오는 코드를 제제할 수 있는 방법이 없다.
객체지향에서는 이를 나쁜 냄새가 난다고 한다.
이를 SRP원칙에 맞게 리팩토링 하면 다음과 같다.
사람 클래스를 남자 클래스와 여자 클래스로 분할하고, 군번 속성은 남자 클래스에만 갖도록 한다.
이 때, 남자 클래스와 여자 클래스의 공통점이 많다면 사람 클래스를 상위 클래스로 하여 공통점을 두고, 상속한 후 차이점만 구현하면 된다.
공통점이 없는 경우 사람 클래스는 제거한다.
이외에도 하나의 속성이 여러 의미를 갖는 경우도 SRP를 지키지 못하는 경우이다. 자바 코드에 불필요한 if 문을 많이 사용하게 된다.
- 메서드
class 강아지{
final static Boolean 수컷 = true;
final static Boolean 암컷 = false;
Boolean 성별
void 소변보다(){
if(this.성별 == 수컷){
//
}
else{
//
}
}
}
강아지 클래스의 메서드가 수컷의 행위와 암컷의 행위를 모두 구현하려고 하기 때문에 SRP를 위반한다.
메서드가 SRP를 위배하고 있을 때 나타나는 대표적인 냄새가 분기처리를 위한 if문이다.
이럴 경우 다음과 같이 코드를 리팩터링 할 수 있다.
abstract 강아지{
abstract void 소변보다()
}
class 수컷강아지 extends 강아지{
void 소변보다(){
//
}
}
class 암컷강아지 extends 강아지{
void 소변보다(){
//
}
}
단일 책임 원칙과 가장 관련이 깊은 객체 지향 4대 특성은 모델링 과정을 담당하는 추상화이다.
추상화를 통해 클래스를 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려해야 한다.
OCP(Open Closed Principle) : 개방 폐쇄 원칙
"소프트웨어 엔티티(클래스, 모듈, 함수)는 (자신의) 확장에 대해서는 열려있어야 하지만, (주변의) 변경에 대해서는 닫혀있어야 한다."
클래스가 변경되더라도, 상위 클래스 또는 인터페이스를 중간에 둠으로써 외부 클래스가 영향을 받지 않을 수 있다.
예를 들어, 운전자의 차종이 변경되는 경우, 자동차라는 상위클래스를 통해 운전자 입장에서는 주변의 변화에 대해 폐쇄되어 있으며 자동차 입장에서 자신의 확장에는 개방되어 있는 것이다.
(JDBC인터페이스를 사용하는 클라이언트는 데이터베이스가 바뀌더라도 Connection 부분 외에는 수정할 필요가 없는 것과 같은 예시이다. 자바에서도 JVM이 있기 때문에 운영체제가 달라도 관계없는 것에도 개방 폐쇄 원칙이 적용되어 있다.)
OCP를 무시하고 프로그램을 작성하면 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.
스프링 프레임워크는 개방 폐쇄 원칙을 교과서적으로 잘 활용하고 있다.
LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
"서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다."
앞서 객체 지향의 상속은 계층도가 아닌 분류도가 되어야 한다고 언급했다.
"하위 클래스 is a kind of 상위 클래스", "구현 클래스 is able to 인터페이스" 관계를 만족해야 한다.
상속이 계층도 형태로 구축된다면 위 관계를 만족하지 않는다.
아버지 춘향이 = new 딸(); // X, 아버지-딸은 계층도 관계
동물 뽀로로 = new 펭귄(); // O, 동물-펭귄은 분류도 관계
즉 맨 첫 문장을 해석하자면,
"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 할 수 있어야 한다."라고 이해할 수 있다.
ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다."
앞서 SRP에서 하나의 클래스가 다수의 책임을 가지고 있을 때, 클래스를 분할하는 해결책을 언급하였다.
이에 대한 또다른 해결책이 ISP이다. 남자 class를 각 책임에 맞게 인터페이스로 제한하는 것이다.
Chap3에서 언급했듯이, 상위 클래스는 물려줄 특성이 풍성할 수록 좋고, 인터페이스에는 메서드가 적을 수록 좋다. 이를 자세히 알아보면 아래와 같다.
- ISP를 논할 때는 항상 인터페이스 최소주의 원칙이 등장한다. 인터페이스를 통해 외부에 메서드를 제공할 때에는 역할에 충실한 최소한의 메서드만 제공하라는 것이다.
- LSP에 의해 하위 객체는 상위 객체인 척 할 수 있다.
이 때, 상위 클래스가 빈약할 경우 형변환이 필요하여 상속의 혜택을 제대로 누리지 못한다. 만약 동일한 메서드에 대해 하위 클래스마다 다른 동작을 가질 경우 추상 메서드 기법을 사용하면 된다.
DIP(Dependency Inversion Principle) : 의존 역전 원칙
"고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체 클래스에 의존하지 마라"
자동차가 스노우타이어에 의존하고 있는 관계가 있다고 할 때,
스노우타이어는 자동차보다 더 자주 변하기 때문에 자동차는 그 영향에 노출되어있다고 말할 수 있다.
이를 개선하여,
자동차가 구체적인 타이어 하나가 아닌 추상화된 타이어 인터페이스에 의존하게 함으로써 구체적인 타이어가 변경되어도 자동차는 영향받지 않게 된다.
OCP에서 나왔던 설명과 비슷한데, 그만큼 밀접하게 설계 원칙들이 맞닿아 있는 경우가 많다.
이 때, 스노우 타이어는 아무 것에도 의존하지 않는 클래스였는데 보다 추상적인 단계인 타이어 인터페이스에 의존하게 됐다.
바로 의존의 방향이 역전된 것이다.
이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향을 받지 않도록 하는 것이 의존 역전 원칙이다.
맨 처음의 세 문장을 요약해보면 "자신보다 변하기 쉬운 것에 의존하지 마라"고 할 수 있다.
상위 클래스일록, 인터페이스일수록, 추상 클래스일 수록 변하지 않을 가능성이 높으므로 하위 클래스나 구체 클래스가 아닌 세가지를 통해 의존하라는 것이다.
정리
SOLID를 이야기 할 때 SoC(Seperation of Concerns)를 빼놓을 수 없다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고 관심이 다른 것은 가능한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것이다. 즉, 하나의 속성/메서드/클래스/모듈/패키지에는 하나의 관심사만 들어있어야 한다는 것이다.
SoC를 적용하려면 자연스럽게 SOLID 원칙에 도달하게 된다. 스프링 프레임워크는 이를 극한까지 적용하고 있다.
SRP(단일책임원칙) : 어떤 클래스를 변경해야하는 이유는 오직 하나뿐이어야 한다.
OCP(개방폐쇄원칙) : 자신의 확장에는 열려있고, 주변의 변화에는 닫혀 있어야 한다.
LSP(리스코프치환원칙) : 서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
DIP(의존역전원칙) : 자신보다 변하기 쉬운 것에 의존하지 마라