객체 지향 설계 5 원칙 - SOLID #
* 주관적으로 해석될 수 있다.
객체 지향의 특성을 올바르게 사용하는 방법, 즉 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법/원칙에 대한 고민이 있었다.
많은 Best Practice, 시행 착오 속에서 SOLID 개념이 탄생되었다.
이들의 기본적인 원칙은 응집도(cohension)를 높이고, 결합도(coupling)는 낮추는 것에 있다.
우리들의 프로그램(소프트웨어)에 녹여내야 하는 개념이다.
디자인 패턴, 스프링 프레임워크의 뼈대이다.
객체 지향 4대 특성을 제대로 활용하면 자연스럽게 SOLID 가 적용된다.
- 단일 책임 원칙 (Single Responsibility Principle)
- 개방 폐쇄 원칙 (Open Closed Principle)
- 리스코프 치환 원칙 (Liskov Substitution Principle)
- 인터페이스 분리 원칙 (Interface Segregation Principle)
- 의존 역전 원칙 (Dependency Inversion Principle)
단일 책임 원칙 (Single Responsibility Principle) #
한 클래스는 하나의 책임(역할)을 가져야 한다.
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
클래스뿐만 아니라, 속성, 메서드 패키지, 모듈, 컴포넌트, 프레임워크 등에서도 적용될 수 있는 개념이다.
예시 1 (클래스)
남자 클래스 |
---|
남자친구 역할 |
아들 역할 |
사원 역할 |
군인 역할 |
위의 ‘남자’ 클래스는 역할(책임)이 너무 많다. 이것을 분리할 수 있다.
남자친구 클래스 |
---|
아들 클래스 |
---|
사원 클래스 |
---|
군인 클래스 |
---|
예시 2 (멤버 변수)
class 사람 {
성별;
is남자;
is여자;
군번;
}
(남자만 군번이라는 속성을 갖는다고 가정하에) 위의 ‘사람’ 클래스는 ‘남자’, ‘여자’ 역할을 다루고 있다. 여자 역할에서 군번 속성은 필요하지 않다. 따라서, 남자 클래스와 여자 클래스로 분리할 수 있다.
class 남자 {
군번;
}
class 여자 {
}
예시 3 (메소드)
class 강아지 {
성별;
is수컷 = true;
is암컷 = false;
...
void 소변보다() {
if(수컷) { }
else { }
}
}
위의 강아지 클래스는 수컷, 암컷을 모두 구현하려 하고 있다. 그 대가로 소변보다()
메소드에서 if 분기 처리가 발생하고 있다.
수컷, 암컷 클래스로 분리할 수 있다.
abstract class 강아지 {
abstract void 소변보다();
}
class 수컷 extends 강아지 {
void 소변보다() {
...
}
}
class 암컷 extends 강아지 {
void 소변보다() {
...
}
}
개방 폐쇄 원칙 (Open Closed Principle) #
확장에 대해서는 열려 있어야 하고, 변경에 대해서는 닫혀 있어야 한다.
자신의 확장에는 열려 있고, 주변의 변경에는 닫혀 있어야 한다.
// 변경 전
운전자 ----> 쏘나타
수동 기어 조작()
창문 열기()
// 변경 후
운전자 ----> 아반떼
자동 기어 조작()
창문 열기()
위의 예시에서는 운전자가 수동 기어 조작()
에서 자동 기어 조작()
으로 변경된 메소드를 사용하게 된다. 즉 운전자의 코드가 변경된다.
OCP 에 어긋난다. 아래와 같이 개선할 수 있다.
운전자 ----> 자동차
창문 열기()
기어 조작()
| |
| |
쏘나타 아반떼
창문열기() 창문열기()
기어조작() 기어조작()
위의 예시에서는 쏘나타에서 아반떼로, 아반떼에서 쏘나타로 변경되어도 운전자의 코드는 변경되지 않는다. 또 자동차는 무한히 확장 가능하다.
OCP 가 잘 성립된다. (자동차의)확장에는 열려 있고, (운전자의)변경에는 닫혀 있다.
OCP 를 통해 유연성, 재사용성, 유지보수성 등의 이점을 얻을 수 있다.
리스코프 치환 원칙 (Liscov Substitution Principle) #
하위 타입은 언제나 자신의 기반 타입(상위 타입)으로 교체할 수 있어야 한다.
하위 클래스의 인스턴스는 상위 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
- 하위 클래스 is a kind of 상위 클래스
- 구현 클래스 is able to 인터페이스
위의 문장이 성립된다면, LSP 를 지키고 있는 것이다.
// 잘못된 예시
아버지 춘향이 = new 딸();
// 잘된 예시
동물 뽀로로 = new 펭귄();
객체 지향의 4대 특성 중 ‘상속’ 개념이 잘 적용되어 있다면, LSP 도 자연스럽게 성립되지 않을까 생각한다.
인터페이스 분리 원칙 (ISP) #
인터페이스를 분리한다.
자동차 운전자 ----------- 자동차 인터페이스 -----------
기어 조작() |
창문 열기() |
|
만능 기계
|
|
|
비행기 운전자 ----------- 비행기 인터페이스 -----------
이륙하기()
...
단일 책임 원칙(SRP) 와 인터페이스 분할 원칙(ISP)는 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다.
책에서는 특별한 경우가 아니라면, SRP 를 적용하는 것을 추천하고 있다.
- 인터페이스 최소주의 원칙
- 상위 클래스는 풍부할수록 좋고, 인터페이스(강제사항)는 작을수록 좋다.
- 위의 예시에서
만능 기계
는 기어조작(), 창문열기(), 이륙하기() 등 모두를 구현해야 한다.
의존 역전 원칙 (DIP) #
고차원 모듈(역할, 인터페이스, …)은 저차원 모듈(구현, 구현 클래스, …) 에 의존하면 안된다.
추상화된 것은 구체적인 것에 의존하면 안된다.
구체적인 것은 추상화된 것에 의존해야 한다.
자주 변경되는 구체 클래스(구현)에 의존하지 말아야 한다.
자신보다 변하기 쉬운 것에 의존하지 말아야 한다.
// 스노우 타이어는 누구에게도 의존하지 않는다.
자동차 ----> 스노우 타이어
아래와 같이 개선할 수 있다.
// 스노우 타이어는 타이어에 의존한다.
자동차 ----> 타이어
^ ^
| |
| |
일반타이어 스노우타이어
자동차가 구체 클래스(스노우 타이어, 일반 타이어)에 의존하지 않고, 추상화 클래스 혹은 인터페이스(타이어)에 의존하도록 수정했다.
이제 자동차는 구체적인 타이어(스노우 타이어, 일반 타이어)가 변경되어도 영향을 받지 않는다.
(여기까지는 OCP의 예시와 유사하다. 즉 하나의 원칙을 지키면 자연스럽게 다른 원칙까지 지켜지게 되는 경우가 많다.)
여기서 눈여겨 볼 점은, 구체 클래스(스노우타이어)의 의존 방향(관계)이 역전되었다는 것이다.
스노우 타이어는 누구에게도 의존하지 않다가 타이어라는 추상화된 것을 의존하기 시작했다. 즉 의존이 역전되었다.
구체적인 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.