본문 바로가기

그림으로 이해하는 객체 지향 설계 5원칙 [SOLID] 본문

Dev

그림으로 이해하는 객체 지향 설계 5원칙 [SOLID]

겨울바람_ 2024. 7. 20. 20:06

SRP (Single Responsibility Principle): 단일 책임 원칙

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다."
- 로버트 C. 마틴

위의 그림과 같이 Man 클래스에 의존하고 있는 다양한 클래스들이 존재한다고 생각해 보자. 그림에서부터 확인이 가능하듯 Man 클래스에 부과된 역할과 책임이 너무 많기 때문에 남성의 표정이 그리 밝지 않다.

 

어느 날, 여자친구와 헤어질 경우 Man 클래스는 챙길 일 없는 기념일과 사랑한다고 말할 대상이 사라져 힘들어하게 된다. 거기다 여자친구와 결별한 영향이 부모님과 직장 상사에게 까지 영향이 미칠 수 있다. 이렇듯 한 클래스에 너무 많은 책임이 집중될 경우 발생할 수 있는 악영향과 관리의 어려움을 예방하기 위해 책임을 분리하라는 것이 단일 책임 원칙이다.

 

위의 그림에서 Man 클래스에 집중되어 있는 책임을 아래의 그림처럼 분리해보자. 

Man 이라는 하나의 클래스가 역할과 책임에 따라 세 개의 클래스로 나누어진 것을 볼 수 있다. 만일 여자친구와 헤어져도 남자친구의 역할을 요구하지 않는 부모님과 직장 상사에게는 어떤 악영향도 주지 않는 구조가 만들어진 것이다.

 

위의 예시에서는 클래스의 분할에 대해서만 예를 들었지만 속성, 메소드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있는 개념이다. 

OCP (Open Closed Principle): 개방 폐쇄 원칙

"소프트웨어 엔티티는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다."
- 로버트 C. 마틴

어느 날 한 운전자가 제네시스 G80을 구입했다. 수년 동안 제네시스 G80을 운전한 운전자는 돈을 더 모아 닷지 챌린저 SRT를 구입하게 된다. 창문과 기어 조작이 자동이던 제네시스 G80에서 창문과 기어 조작이 수동인 닷지 챌린저를 운전하려고 하니 운전자의 행동에 변화가 생긴다.

 

제네시스 G80을 운전할 때는 해당 차량 인스턴스의 창문자동개방() 메소드를 사용했는데 닷지 챌린저로 차종을 변경하자 닷지 챌린저 인스턴스의 창문수동개방() 메소드를 사용하게 된다.

 

현실 세계라면 차의 기종에 맞게 운전자가 행동을 다르게 하는 것이 올바른 방법이겠지만 객체 지향 원칙이 적용되는 프로그래밍 세계에서는 굳이 운전자가 기종 변경의 영향을 받을 필요는 없다.

 

위의 그림과 같이 상위 클래스 혹은 인터페이스를 중간에 두는 것으로 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 영향을 받지 않게 된다. 

 

여기서 다양한 자동차가 생긴다는 것을 자동차 입장에서는 자신의 확장에는 개방돼 있는 것이고, 운전자의 입장에서는 변화에 폐쇄돼 있는 것이다.

 

개방 폐쇄 원칙이 적용된 것들 중 자바 개발자가 자주 접했을 대표적인 예시가 바로 JDBC다. JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 변경되더라도 Connection을 설정하는 부분 이외에는 수정할 필요가 없다. 


JDBC뿐만 아니라 Hibernate, MyBatis, iBatis 등 데이터베이스 프로그래밍을 지원하는 다른 라이브러리와 프레임워크에도 개방 폐쇄 원칙이 적용되어 있다.

 

사실 개방 폐쇄 원칙을 따르지 않는다고 해서 객체 지향 프로그램을 구현하는 것이 불가능한 것은 아니다. 하지만 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성을 얻을 수 없다.

LSP (Liskov substitution Principle): 리스코프 치환 원칙

"서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다."
- 로버트 C. 마틴

객체 지향의 상속은 다음의 조건을 만족해야 한다. 

 

1. 하위 클래스 is a kind of 상위 클래스 

2. 구현 클래스 is able to 인터페이스 

 

위 두 개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있는 것이라 할 수 있다. 위의 문장대로 구현되지 않은 상속이 존재할 수 있을까?

 

만약 상속 관계를 조직도 혹은 계층도 형태로 구현할 경우 위 문장대로 구현되지 않은 상속이 발생하게 된다. 아래의 예시를 한 번 살펴보자.

위와 같은 가계도를 상속 관계라고 생각해보자. 최상위 클래스인 GrandFather 클래스가 존재하고 해당 클래스를 상속받는 Father와 Uncle 클래스가 존재한다. 

 

이러한 형태의 상속 관계는 어째서 리스코프 치환 원칙을 잘 지키지 못하는 것일까? 

 

상위 클래스의 객체 참조 변수에는 하위 클래스의 인스턴스를 할당할 수 있다. 즉, 상위 클래스인 Father 객체 참조 변수에는 하위 클래스인 Son 혹은 Daughter의 인스턴스를 할당할 수 있어야 한다.

 

간단한 코드로 표현하자면 다음과 같다. 

Father 보영 = new Daughter();

 

굉장히 이상한 코드라는 생각이 든다. 보영이는 Father 클래스를 상속받고 있기 때문에 Father 객체가 가진 메소드를 가지고 있어야 한다. 그렇다면 어떤 경우가 올바른 형태의 상속 관계를 가져 리스코프 치환 원칙을 만족할 수 있을까?

 

바로 분류도 형태의 상속 관계가 이에 해당한다. 아래의 예시를 살펴보자.

최상위 Animal 클래스를 상속받는 Mammailia 클래스와 Birds 클래스가 존재한다. 보영이 때처럼 간단히 코드를 사용해 표현해보면 다음과 같다.

Mammaila 고래 = new Whale()

 

고래는 Mammailia 클래스의 메소드를 그대로 상속받아 사용해도 이상하지 않다.

 

결론적으로 계층도/조직도 형태의 구조는 리스코프 치환 원칙을 위배하고 있는 것이며, 분류도 형태의 구조는 리스코프 치환 원칙을 만족하는 것이다.

ISP (Interface Segregation Principle): 인터페이스 분리 원칙

"클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다."
- 로버트 C. 마틴

ISP에 대한 설명 이전에, 단일 책임 원칙의 예제를 다시 기억해보자. 단일 책임 원칙을 적용하기 전 Man 클래스는 여러 책임과 역할을 가지고 있었지만, 단일 책임 원칙을 적용한 이후 각기 하나의 책임과 역할을 갖는 클래스로 나뉘었다.

 

하지만 Man 클래스를 나누지 않는 방법이 있다면 어떨까?

 

여자친구를 만날 때는 남자친구 역할만 할 수 있게 인터페이스로 제한하고, 부모님과 있을 때는 아들 역할만 할 수 있게 인터페이스로 제한하고, 직장 상사 앞에서는 사원 인터페이스로 제한하는 것이 바로 인터페이스 분리 원칙의 핵심이다.

 

결론적으로 SRP와 ISP는 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 프로젝트 요구사항과 설계자의 취향에 따라 단일 책임 원칙이나 인터페이스 분할 원칙 중 하나를 선택해서 설계할 수 있다. 하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다.

 

인터페이스 분할 원칙을 이야기할 때 항상 함께 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙이라는 것이 있다. 인터페이스를 통해 메소드를 외부에 제공할 때는 그 역할에 충실한 최소한의 메소드만 제공하라는 것이다.

DIP (Dependency Inversion Principle): 의존 역전 원칙

"고차원 모듈은 저차원 모듈에 의존하면 안된다. 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체 클래스에 의존하지 마라."
- 로버트 C. 마틴

 

이번 DIP 또한 예제를 통해 설명해보려고 한다. 우선 개방 폐쇄 원칙의 예시에서 사용한 자동차를 기억해낼 필요가 있다.

 

눈이 매우 많이 오는 날에 안전하게 운전을 하려면 스노우 타이어가 필요하다. 자동차가 스노우 타이어에 의존하는 것이다. 

하지만 스노우타이어는 계절이 바뀌면 일반 타이어로 교체해야 한다. 스노우 타이어를 일반 타이어로 교체할 때 자동차는 보다 변경이 더 자주 발생하는 스노우 타이어라는 객체에 의존하고 있기 때문에 의도치 않게 영향을 받게 된다.

 

이 구조를 개선하려면 다음과 같이 자동차가 구체적인 타이어의 종류에 의존하는 것이 아닌 추상화된 타이어의 인터페이스에만 의존하게 하는 방식으로 구조를 변경해야 한다.

해당 구조는 스노우 타이어에서 일반 타이어로, 또는 다른 구체적인 타이어로 변경돼도 자동차는 그 영향을 받지 않는 형태로 구성된다.

 

위의 그림의 형태가 OCP에서 예시로 든 자동차의 구조도와 굉장히 유사하다는 것을 느낄 수 있다. OCP와 DIP는 객체 지향 4개 특성 중 상속과 다형성을 통해 구현되기 때문에 비슷한 형태를 띄는 것이다.

 

구조도가 변경되기 전 스노우 타이어와 변경된 후의 스노우 타이어에 큰 변화가 생겼다. 바로 스노우 타이어가 그 무엇에도 의존하지 않는 클래스였는데, 구조도가 변경된 이후의 스노우 타이어는 추상적인 타이어 인터페이스에 의존하게 됐다.

 

그리고 자동차는 변경되기 쉬운 스노우 타이어에 의존하던 관계를 추상화된 타이어 인터페이스를 추가해 두고 의존 관계를 역전시키고 있다.

 

이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변화로 인한 영향을 받지 않게 하는 것이 의존 관계 역전 원칙이다.

 

상위 클래스, 인터페이스, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하는 것이다.

 

 

 

'Dev' 카테고리의 다른 글

[Go] 메소드  (2) 2024.11.09
[Go] 슬라이스  (0) 2024.11.07
CPU-Scheduling  (2) 2024.04.07
RabbitMQ  (0) 2024.03.31
Redis를 사용한 분산락  (0) 2024.03.24
Comments