객체 지향 설계 5원칙, SOLID [스프링 입문을 위한 자바 객체지향의 원리와 이해 Ch5]
지난 장까지 해서 객체 지향의 개념과 4대 특성을 학습했습니다. 좋은 재료를 획득했으니 올바르게 사용하는 법만 배우면 됩니다. 이 장에서는 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법인 SOLID 원칙을 알아보겠습니다.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Principle): 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 역전 원칙
SOLID를 배우기에 앞서…
SOLID도 결국엔 소프트웨어를 올바르게 설계하기 위함입니다. 좋은 소프트웨어 설계를 위해서는 일반적으로 결합도는 낮추고 응집도는 높이는 게 바람직합니다.
- 결합도는 모듈간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이해집니다.
- 응집도는 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이해집니다.
SRP - 단일 책임 원칙
“어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다” - 로버트 C. 마틴
SRP를 이해하는 데는 올바른 예보다 잘못된 예를 살펴보는 게 좋습니다. 두 가지 예를 살펴보겠습니다.
속성이 단일 원칙 책임을 지키지 못하는 경우
남자
는 반드시 군대를 가고, 여자
는 절대로 군대를 가지 않다고 가정해보겠습니다. 그런데 다음과 같이 사람
클래스에 군번
속성이 있다면 어떻게 될까요?
class 사람 {
String 군번;
...
}
...
사람 로미오 = new 사람();
사람 줄리엣 = new 사람();
줄리엣.군번 = "1573042009";
사람
형 참조 변수 줄리엣
이 가진 군번
속성에 값을 할당하거나 읽어 오는 코드를 제재할 방법이 없습니다.
그렇다면 이 코드가 단일 원칙 책임을 지키게 하려면 어떻게 해야 할까요? 다음과 같이 사람
클래스를 남자
클래스와 여자
클래스로 분할하고 남자
클래스에만 군번
속성을 갖게 하면 됩니다.
class 사람 {
...
}
class 남자 extends 사람 {
String 군번;
...
}
class 여자 extends 사람 {
...
}
남자
클래스와 여자
클래스의 공통적인 속성은 사람 클래스에 두고, 차이점만 각 클래스에 구현하면 됩니다.
또한 하나의 속성이 여러 의미를 갖는 경우도 단일 책임 원칙을 지키지 못하는 것임을 주의하세요.
메서드가 단일 책임 원칙을 지키지 못하는 경우
class 강아지 {
final static Boolean 수컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
void 소변보다() {
if(this.성별 == 수컷) {
//한 쪽 다리를 들고 소변을 본다.
} else {
//뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
}
}
}
강아지
가 수컷이냐 암컷이냐에 따라 소변보다()
메서드의 행위가 달라집니다. 하나의 메서드에는 하나의 행위만 있어야 합니다. 위 코드는 다음과 같이 리팩터링 할 수 있습니다.
abstract class 강아지 {
abstract void 소변보다();
}
class 수컷강아지 extends 강아지 {
void 소변보다() {
//한쪽 다리를 들고 소변을 본다.
}
}
class 암컷강아지 extends 강아지 {
void 소변보다() {
//뒷다리 두 개로 앉은 자세로 소변을 본다.
}
}
두 가지 경우로 SRP를 살펴봤지만, SRP는 단 한 가지 기준만 알면 됩니다. 그건 바로 변경입니다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이고, 아니면 잘 따르지 못한 겁니다.
단일 책임 원칙 - 객체 지향 4대 특성
단일 책임 원칙이 객체 지향 4대 특성 중에서 가장 관계가 깊은 건 추상화입니다. 애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 원칙 책임을 고려하는 습관을 들이는 게 중요합니다.
OCP - 개방 폐쇄 원칙
“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.” - 로버트 C. 마틴
이를 다르게 표현하면 다음과 같습니다.
“자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.”
이게 무슨 말일까요? 다음과 같은 상황이 있다고 가정해봅시다.

운전자가 마티즈
에서 쏘나타
로 바꾸면 운전자의 행동에도 변화가 생깁니다. 이는 개방 폐쇄 원칙에 위배되는 경우라고 볼 수 있습니다. 그렇다면 개방 폐쇄 원칙을 준수하기 위해서는 어떻게 해야 할까요?

위와 같이 마티즈
와 소나타
에 상위 클래스인 자동차
를 두면 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 행동의 변화가 생기지 않게 됩니다. 여기서, 다양한 자동차가 생긴다고 하는 건 자동차 입장에서는 자신의 확장에는 개방돼 있는 거고, 운전자 입장에서는 주변의 변화에 폐쇄돼 있는 겁니다.
앞서 1, 2장에서 자바의 특징인 WORA(Write Once Run Anywhere)에 대해 확인했었습니다. 이런 사실을 통해 자바에도 개방 폐쇄 원칙이 적용돼 있다는 걸 알 수 있습니다. 자바 개발자는 작성하고 있는 소스코드가 윈도우에서 구동될 지, 리눅스에서 구동될 지 또는 다른 운영체제 상에서 구동될 지에 대해서는 걱정하지 않습니다. 각 운영체제별 JVM과 목적 파일이 있기에 개발자는 다양한 구동환경에 대해서는 걱정하지 않고 본인이 작업하고 있는 개발 PC에 설치된 JVM에서 구동되는 코드만 작성하면 됩니다. 여기서, 개발자가 작성한 소스코드는 운영체제의 변화에 닫혀 있고, 각 운영체제별 JVM은 확장에 열려 있는 구조가 되는 겁니다.
개방 폐쇄 원칙을 준수하면 다음과 같은 이점을 누릴 수 있습니다.
- 유연성
- 재사용성
- 유지보수성
특히, 스프링은 개방 폐쇄 원칙을 아주 잘 준수하고 있는 프레임워크입니다. 스프링에 대해 배울 때 개방 폐쇄 원칙이 무엇인지에 대해 한 번 더 깨닫게 될 겁니다.
LSP - 리스코프 치환 원칙
“서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.” - 로버트 C. 마틴
객체 지향에서의 상속은 조직도나 계측도가 아닌 분류도가 돼야 합니다. 객체 지향의 상속은 다음의 조건을 만족해야 합니다.
- 하위 클래스 is a kind of 상위 클래스
- 하위 분류는 상위 분류의 한 종류다.
- 구현 클래스 is able to 인터페이스
- 구현 분류는 인터페이스할 수 있어야 한다.
위 두 문장대로 구현된 프로그램이라면 리스코프 치환 원칙을 잘 지키고 있는 겁니다. 하지만, 위 문장대로 구현되지 않은 프로그램이라면 상속이 조직도나 계층도 형태로 구축된 경우입니다. 그 경우를 살펴봅시다.
아버지를 상위 클래스로 하는 딸이라는 하위 클래스가 있다고 합시다. 바로 전형적인 계층도 형태이며, 객체 지향의 상속을 잘못 적용한 예입니다. 우선 상위 클래스의 객체 참조 변수에는 하위 클래스의 인스턴스를 할당할 수 있습니다.
아버지 춘향이 = new 딸()
이상합니다. 딸에게 아버지의 역할을 맡기고 있습니다. 동생을 돌봐야 한다거나 그런 상황에서 딸이 아버지의 역할을 할 수도 있을 겁니다. 하지만 계층이 좀 더 확장되면 어떻게 될까요? 딸이 할아버지의 역할을 맡는다면? 딸이 증조할아버지의 역할을 맡는다면? 맡아야 할 이유도 없을 뿐더러 그 역할을 온전히 대체할 수도 없습니다. 이것이 상속을 계층도 형태로 구축되었기 때문에 즉, 리스코프 치환원칙을 위배하고 있기 때문에 나타나는 이상함입니다.
ISP - 인터페이스 분리 원칙
“클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.” - 로버트 C. 마틴
ISP를 배우기 전에, 단일 책임 원칙(SRP)의 예를 먼저 살펴봅시다.
남자
라는 클래스가 있다고 생각해봅시다. 남자는 여자친구에게는 남자친구 역할을 하고, 어머니에게는 아들 역할을 하고, 직장상사에게는 사원 역할을 하며, 소대장에게는 소대원 역할을 합니다. 단일 책임 원칙을 준수하지 않는다면 이 모든 책임이 남자
클래스에 있게 되겠지만, 준수한다면 남자
클래스를 남자친구
클래스, 아들
클래스, 사원
클래스, 소대원
클래스로 분할하게 될 겁니다. 하지만, 이런 문제 상황을 해결하는 것이 꼭 이 방법뿐일까요? 이 때 제시할 수 있는 새로운 해결책이 ISP 즉, 인터페이스 분할 원칙입니다.
SRP에서 제시한 것처럼, 남자 클래스를 분할하는 것이 아니라 다음과 같이 하는 겁니다.
- 여자친구를 만날 때는 → 남자친구 역할만 하도록 인터페이스 제한
- 어머니와 있을 때는 → 아들 역할만 하도록 인터페이스 제한
- 직장 상사 앞에서는 → 사원 역할만 하도록 인터페이스로 제한
- 소대장 앞에서는 → 소대원 역할만 하도록 인터페이스 제한
결론적으로, 단일 책임 원칙과 인터페이스 분할 원칙은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있습니다. 설계자의 의도에 따라 둘 중 하나를 선택해도 되지만, 특별한 경우가 아니라면 SRP를 적용하는 게 더 좋은 해결책이라고 할 수 있습니다.
ISP를 이야기할 때는 항상 함께 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙이라는 게 있습니다. 인터페이스를 통해 메서드를 외부에 제공할 때는 최소한의 메서드만 제공하라는 겁니다. 남자친구
인터페이스에 사격하기()
메서드를 제공할 필요도 없고, 제공해서도 안 된다는 겁니다. 인터페이스는 그 역할에 충실한 최소한의 기능만 공개하는 것 즉, “~할 수 있는(is able to)”이라는 기준으로 만드는 것이 정석이라는 걸 기억하세요.
DIP - 의존 역전 원칙
“고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.” ”추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.” ”자주 변경되는 구체(Concrete) 클래스에 의존하지 마라” - 로버트 C. 마틴
자동차와 스노우타이어
는 다음과 같은 의존 관계가 있습니다. 자동차
가 스노우타이어
를 의존합니다.

그런데 자동차
는 한 번 사면 몇 년은 타야 해서 스노우타이어
는 게절이 바뀌면 일반 타이어로 교체해야 합니다. 이런 경우 스노우타이어
를 일반 타이어로 교체할 때 자동차는 그 영향에 노출돼 있음을 알 수 있습니다.

위와 같이 자동차
가 구체적인 타이어가 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 스노우타이어
에서 일반 타이어로, 또는 다른 구체적인 타이어로 변경돼도 자동차는 이제 그 영향을 받지 않는 형태로 구성됩니다. 한편, DIP는 그 형태가 OCP와 비슷하다는 걸 알 수 있습니다.
기존에는 스노우타이어
가 그 무엇에도 의존하지 않는 클래스였지만, 추상적인 것인 타이어 인터페이스를 의존하게 됐습니다. 바로 의존의 방향이 역전된 겁니다. 이처럼 자신보다 변하기 쉬운 것에 의존하던 걸 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙입니다.
DIP를 의역해 보면 다음과 같습니다.
“자신보다 변하기 쉬운 것에 의존하지 마라”
정리
앞에서는 단순히 용어를 알아봤다면 각 용어의 의미를 정리해봅시다.
- SRP(Single Responsibility Principle)
- 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- OCP(Open Closed Principle)
- 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
- LSP(Liskov Substitution Principle)
- 서브 타입은 언제난 자신의 기반 타입으로 교체할 수 있어야 한다.
- ISP(Interface Segregation Principle)
- 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
- DIP(Dependency Inversion Principle)
- 자신보다 변하기 쉬운 것에 의존하지 마라.
SOLID 원칙을 적용하면 소스 파일의 개수는 더 많아지는 경향이 있습니다. 하지만 이렇게 많아진 파일이 논리를 더욱 잘 분할하고, 잘 표현하기에 이해하기 쉽고, 개발하기 쉬우며, 유지와 관리, 보수하기 쉬운 소스가 만들어집니다.
Uploaded by Notion2Tistory v1.1.0