객체지향 개발의 원칙 SOLID (SRP, OCP, LSP, ISP, DIP)

좋은 소프트웨어를 설계하기 위해서 우리는 나쁜 설계가 갖는 특징들을 하나씩 줄여나가야 한다.

 

우리가 줄여나가야 할 나쁜 설계들의 특징 중 하나는 소프트웨어를 바꾸기 힘들다는 것이다.

 

이런 경우 시스템의 한 부분을 변경하면 전혀 상관없는 부분의 작동이 멈춘다거나, 다른 연관된 부분이 너무 많아서 함부로 건드리기 어렵다.

전형적인 의존 관계를 잘못 관리해서 일어나는 현상이다.

 

 

 

이런 상황을 해결하기 위해서

의존관계를 끊거나 의존의 방향을 바꿀 수 있는 인터페이스를 활용하거나,

다형성을 사용해서 함수를 포함한 모듈에 의존하지 않고 그 함수를 호출할 수 있다.

그래서 우리는 설계를 할 때 다형성, 인터페이스를 갖는 객체지향 언어(UML)를 사용하는 것이다.

 

 

 

 

객체지향 언어를 사용해서 의존관계를 원하는 모양대로 만들 수 있다는 것을 알았다.

이러한 언어를 사용한 객체지향 프로그래밍에서 좋은 소프트웨어 설계를 위한 몇 가지 원칙이 있다.

 

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

 

말그대로 단 하나의 책임만을 가져야 한다.

객체지향 언어 뿐만 아니라절차적프로그래밍 기법에도 적용할 수도 있다.

 

 

여기에서 책임이란 객체가 할 수 있는 것, 해야 하는것들을 말한다.

예를 들어 이와 같은 경우는, 하나의 객체에 너무 많은 책임이 몰려있다고 볼 수 있다.

 

 

이 상황에서 변경 책임이 많다는 것은 변경될 여지가 많다는 의미이다.

객체에 연관된 것 하나만 바뀌더라도 해당 객체는 변경될 수 있다.

 

좋은 설계란, 새로운 요구사항이나 변경이 있을 때 가능한 영향 받는 부분을 줄여야 한다.

책임을 많이 질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.

연관된 기능의 변경으로 인한 영향

 

 

 

우리는 변경의 위험을 줄이기 위해서 책임을 분리하기 시작한다.

어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 회귀테스트를 통해 책임 분리를 알아보자.

 

회귀 테스트 비용을 줄이는 방법은 시스템에 변경사항이 발생했을 때 영향을 받는 부분을 적게 하는 것이다.

 

 

 

예를 들어 하나의 책임이 여러 곳에 분산된 경우, 여러 곳에 흩어진 공통 책임을 한 곳에 모으면서 응집도를 높인다.

(부가기능 클래스를 따로 만듬)

이렇게 독립 클래스를 구현하더라도 구현된 기능들을 호출하고 사용하는 코드는 해당 기능을 사용하는 코드 어딘가 에 포함될 수밖에 없다.

 

 

결국 설계할 때의 기본 원칙은 응집도는 높고 결합도는 낮게 하는 것이다.

관련된 것들을 한곳에 두어 응집도를 높이면, 결합도는 자연스럽게 알아서 낮아진다.

이게 결국 지금 배운 SRP에 따른 설계를 한 것이다.

 

 

 

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

 

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 개방되어야 하지만, 변경에 대해서는 폐쇄되어야 한다.

 

간단하게 말하면, 기존의코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계 되어야 하고, 모듈 자체를 변경하지 않고도 그 모듈을 둘러싼 환경을 바꿀 수 있어야 한다.

 

 

결국 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.

 

기존의 클래스가 영향을 받지 않으면서 개별적인 클래스를 처리하지 않도록 하고, 인터페이스로 캡슐화를 해서 구체적인 처리를 하도록 한다.

OCP 설계 예시

 

 

 

리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

 

LSP는 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미다

여기에서 일관성이 있다는 의미는, 만약 LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다는 것을 말한다. (영향 X)

 

 

어떤 클래스의 행위를 일종의 방정식 형태로 기술해서 자식 클래스의 인스턴스가 이 방정식을 만족하는지 LSP를 점검할 수 있다.

 

 

LSP를 만족하는 가장 단순한 방법은 재정의를 하지 않는 것이다.

함수를 오버라이드 하지 말자.

올바른 예시

 

 

 

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

 

DIP는 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 원칙이다.

 

 

DIP에 따르면

고차원모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.

그리고 추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다.

  • 어떤 클래스를 상속받아야 한다면, base 클래스를 추상 클래스로 만들자.
  • 어떤 클래스의 참조를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들자.
  • 어떤 함수를 호출해야 한다면, 호출되는 함수를 추상 함수로 만들자.

 

추상클래스와 인터페이스는 다른 유도된 구체적인 클래스보다 변화가 덜하다. 그래서 시스템에 영향을 덜 줄 수 있다.

 

 

정책, 전략과 같은 어떤 큰 흐름이나 개념 같은 추상적인 것변하기 어려운 것에 해당 하고 구체적인 방식, 사물 등과 같은 것은 변하기 쉬운 것으로 구분하자.

 

장난감 class에 DIP 적용 예시

여기에서 변하기 쉬운 것은 구체적인 장난감이고, 변하기 어려운 것은 아이가 장난감을 가지고 노는 사실이다.

 

이 예시의 장난감 클래스 외부에서 의존되는 것을 대상 객체의 인스턴스 변수에 주입하는 의존성 주입(dependency injection)을 통해 변화를 쉽게 수용할 수 있는 코드를 만들 수 있다.  

 

 

 

인터페이스 분리 원칙 (Interface Segregation principle, ISP)

 

인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙이다.

클라이언트는자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.

클라이언트의 관점에서 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이다.

 

이와 같은 복합기의 클래스 다이어그램에서 ISP를 적용시키면 아래와 같이 표현될 수 있다.