개발/개발서적

SOLID 원칙 - SRP, OCP

devriver 2021. 10. 18. 23:53

클린 아키텍쳐 3부 SOLID 원칙을 정리하였다.

 

SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법과 이들 클래스를 서로 결합하는 방법을 설명한다.

중간 수준의 소프트웨어 구조를 [변경에 유연하고, 이해하기 쉽고, 컴포넌트 기반이 되도록] 만드는 것이 SOLID의 목적이다.

여기서 중간 수준이란 모듈 수준으로 즉, SOLID 원칙은 모듈과 컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의하는 데 적용할 수 있다.

 

SRP: 단일 책임 원칙, 소프트웨어 모듈은 변경의 이유가 단 하나여야한다.

OCP: 개방-폐쇄 원칙, 기존 코드를 변경하기 보다는 반드시 새로운 코드를 추가하는 방식이여야 한다.

LSP: 리스코프 치환 원칙, 상호대체 가능한 구성요소를 이용해 시스템을 만들 수 있으려면 구성 요소는 반드시 서로 치환 가능해야한다.

ISP: 인터페이스 분리 원칙, 사용하지 않는 것에 의존하지 않는다.

DIP: 의존성 역전 원칙, 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대 의존해선 안된다.


SRP 단일 책임 원칙

단일 모듈의 변경 이유는 오직 하나뿐이어야 한다. 이는 하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다로 바꿔 말할 수 있다.

단일 액터에 중요한 힘은 응집성(cohesion)이다.

 

원칙 위반 사례

1) Employee 클래스의 calculatePay(), reportHours(), save() 메서드의 각각 액터가 다르다. 단일 클래스에 세 액터가 결합되버린 상황이다. calculatePay()와 reportHours()는 또 regularHours() 함수를 사용하고 있다. calculatePay 액터를 위해 regularHours를 수정하게 되면 reportHours의 액터에게 영향을 끼친다. -> 서로 다른 액터가 의존하는 코드는 서로 분리해야한다.

2) 서로 다른 액터를 책임지는 메서드를 포함하는 클래스는 병합이 자주 발생한다. 병합에는 항상 위험이 뒤따른다. -> 서로 다른 액터를 뒷받침하는 코드는 서로 분리해야한다.

 

해결책

각 메서드를 각기 다른 클래스로 이동시키고, 데이터와 메서드를 분리한다.

예) 메서드가 없는 EmployeeData 클래스를 만들어 분리한 세 개의 클래스가 공유하도록 한다.

 

해결책의 문제점

세 가지 클래스를 인스턴스화하고 추적해야 한다.

 

해결책의 해결책

퍼사드 패턴(Facade)

EmployeeFacade 클래스를 만든 후, 여기서 세 클래스의 객체를 생성하고 요청된 메서드를 가지는 객체로 위임하는 일을 한다.

업무 규칙과 데이터를 가깝게 배치할 수도 있다. 가장 중요한 클래스 Employee를 그대로 유지하고 EmployeeData 를 포함한다. 덜 중요한 reportHours, save를 각기 다른 클래스, HourReporter, EmployeeSaver로 분리하여 Employee 클래스 내에서 퍼사드 패턴으로 각 요청을 객체에 위임한다.

 

결론

SRP는 메서드와 클래스 수준의 원칙이다. 이보다 상위인 컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle)이 되고 아키텍처 수준에서는 아키턱처 경계의 생성을 책임지는 변경의 축이 된다.


OCP 개방-폐쇄 원칙

소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 다시 말해 소프트웨어 개체의 행위는 확장할 수 있어야 하지만 이 때 개체를 변경해서는 안된다.

 

OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다. 이를 위해 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야한다.

 

예시

재무제표를 웹 페이지로 보여주는 시스템이 있다. 웹 페이지에 표시되는 데이터는 스크롤 가능하며 음수는 빨간색으로 출력한다. 이제 액터가 동일한 정보를 보고서 형태로 변환해서 출력해달라고 요청한다. 보고서에는 페이지 번호, 머리글, 바닥글이 있어야 하며 각 열에는 레이블이 있어야 한다. 또한 음수는 감싸야한다. 당연히 새로운 코드가 추가된다. 그렇다면 원래 코드는 얼마나 많이 수정해야 할까?

이상적인 변경량은 0이다.

 

어떻게 하면 될까?

1) SRP 원칙에 따라 서로 다른 목적으로 변경되는 요소를 적절하게 분리한다.

처리과정을 클래스 단위로 분할하고, 클래스는 컴포넌트 단위로 분할한다. 즉, Controller, Interator, Database, View, Presenter로 분리한다. 

2) DIP 원칙에 따라 의존성을 체계화한다.

각 컴포넌트의 관계는 단 방향으로 이루어진다. A 컴포넌트에서 발생한 변경으로부터 B를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다. 예를 들어, Presenter에서 발생한 변경으로부터 Controller를 보호하고자 Presenter --> Controller 로 화살표가 그려진다. 이 때 Interator는 업무규칙을 포함하는 고수준의 컴포넌트이므로 어떠한 변경에도 의존하고 있지 않다.

 

방향성 제어

의존성 방향은 단방향으로 흘러야한다. 하지만 Interator --> Database의 상황은 언제든 발생할 수 있다. 이렇게 되면 데이터베이스가 변경될 때 고수준의 Interator도 변경된다. 중간에 Database를 추상화한 인터페이스를 두어 의존성을 역전시키면 Interator --> interface <-- database 가 되어 단방향으로 유지할 수 있다.

 

정보 은닉

Controller --> interface <-- Interator, 여기서 interface는 Controller가 Interator 내부에 대해 너무 많이 알지 못하도록 막기 위해 존재한다. 만약 interface가 없었다면, Controller는 추이 종속성(A->B, B->C면 A->C를 의존한다)을 가지게 되어 Interator 안의 entity 에도 의존하게 되버린다. 그러면 entity의 변경에 Controller까지 변경되어야한다. 이처럼 Interator에서 발생한 변경으로부터 Controller를 보호되는 것을 목적으로 Interator 내부를 은닉화한다.