본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[책 정리] 7장. SRP(Single Responsibility Principle), 단일 책임 원칙 - Clean Architecture

by 돼지왕왕돼지 2020. 4. 11.
반응형

-

좋은 소프트웨어 시스템은 깔끔한 코드(clean code)로부터 시작한다.

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

SOLID 원칙의 목적은 중간 수준(모듈 수준)의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

    변경에 유연하다.

    이해하기 쉽다.

    많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트 기반이 된다.(재사용성)

 

 

-

SRP: 단일 책임의 원칙. Single Responsibility Principle

    각 소프트웨어 모듈은 변경의 이유가 하나, 단 하나여야만 한다.

 

 

-

OCP : 개발, 폐쇄 원칙. Open Closed Principle

    기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다는 것.

 

 

-

LSP : 리스코프 치환 원칙. Liskov Substitution Principle

    상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야 한다.

 

 

-

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

    소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.

 

 

-

DIP : 의존성 역전 원칙. Dependency Inversion Principle

    고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안 된다. 대신 세부사항이 정책에 의존해야 한다.

 

 

-

SRP (단일 책임 원칙)은 가장 잘못 이해되는 녀석이다.

단 하나의 일만 해야 한다는 의미가 아니다. 

"단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다." 가 옳다.

변경의 이유는 사용자와 이해관계자를 가리킨다.

아래와 같이 바꿔 말할 수 있다.

"하나의 모듈은 하나의, 오직 하나의 액터(변경을 요청하는 한 명 이상의 사람들) 에 대해서만 책임져야 한다."

 

 

-

응집된(cohesive)이라는 단어가 SRP 를 암시한다.

단일 엑터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성(cohersion)이다.

이 원칙을 이해하는 가장 좋은 방법은 이 원칙을 위반하는 징후들을 살펴보는 일이다.

 

 

 

징후 1 : 우발적 중복

 

-

아래와 같은 함수를 가진 Employee 클래스가 있다.

Employee

    +calcultePay

    +reportHours

    +save

 

이 클래스는 SRP 를 위반하는데, 이들 세 가지 메서드가 서로 매우 다른 세 명의 액터를 책임지기 때문이다.

 

calculatePay() 메서드는 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.

reportHours() 메서드는 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용한다.

save() 메서드는 데이터베이스 관리자(DBA)가 기능을 정의하고, CTO 보고를 위해 사용한다.

 

이 메서드들이 단일 클래스에 배치되어 세 엑터가 서로 결합되어 버렸다.

이 결합으로 인해 CFO 팀에서 결정한 조치가 COO 팀이 의존하는 무언가에 영향을 줄 수 있다.

 

예를 들어 calculatePay() 메서드와 reportHours() 메서드가 초과 근무를 제외한 업무 시간을 계산하는 알고리즘을 공유한다고 해보자.

그리고 개발자는 코드 중복을 피하기 위해 이 알고리즘을 regularHours() 라는 메서드에 넣었다고 해보자.

이제 CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 약간 수정하기로 결정했다고 하자.

반면 인사를 담당하는 COO 팀에서는 초과근무를 제외한 업무 시간을 CFO 팀과는 다른 목적으로 사용하기 때문에, 이 같은 변경을 원치 않는다고 해보자.

이렇게 되면 regularHours 라는 함수의 변경이 CFO 와 COO의 변경에 모두 영향을 미친다.

 

이러한 문제는 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에 발생한다.

SRP 는 서로 다른 액터가 의존하는 코드를 서로 분리하라고 말한다.

 

 

 

징후 2 : 병합 (Merge)

 

-

소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생한다.

특히 이들 메서드가 서로 다른 엑터를 책임진다면 병합이 발생한 가능성은 확실히 더 높다.

 

예를 들어 DBA 가 속한 CTO 팀에서 DB 의 Employee 테이블 스키마를 약간 수정하기로 결정했다고 해보자.

이와 동시에 인사 담당자가 속한 COO 팀에서는 reportHours() 메서드의 보고서 포맷을 변경하기로 결정했다고 해보자.

두 명의 서로 다른 개발자가, 그리고 아마도 서로 다른 팀에 속했을 두 개발자가 Employee 클래스를 체크아웃 받은 후 변경사항을 적용하기 시작한다.

이들 변경사항은 충돌한다. 결과적으로 병합이 발생한다. 

 

많은 사람이 서로 다른 목적으로 동일한 소스 파일을 변경하는 경우에 해당한다.

이 문제를 벗어나는 방법은  서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

 

 

 

해결책

 

-

해결책은 여러개가 있겠지만, 가장 확실한 해결책은 데이터와 메서드를 분리하는 방식일 것이다.

즉 아무런 메서드가 없는 간단한 데이터 구조인 EmployeeData 클래스를 만들어 세 개의 클래스가 공유한다.

각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함한다.

세 클래스는 서로의 존재를 몰라야 한다.

따라서 우연한 중복을 피할 수 있다.

 

이 해결책의 단점은 개발자가 세 가지 클래스를 인스턴스화하고 추적해야 한다는 것이 단점이다.

이러한 난관에서 빠져나올 때 흔히 쓰는 기법으로 퍼사드(Facade) 패턴이 있다.

 

EmployeeFacade 에 코드는 거의 없다.

이 클래스는 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.

 

 

-

가장 중요한 업무 규칙을 데이터와 가깝게 배치하고 싶다면, 가장 중요한 메서드는 기존의 Employee 클래스에 그대로 유지하되, Employee 클래스를 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용할 수 있다.

 

 

 

결론

 

-

단일 책임 원칙은 메서드와 클래스 수준의 원칙이다.

하지만 이보다 상위의 두 수준에서도 다른 형태로 다시 등장한다.

컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle)이 된다.

아키텍처 수준에서는 경계(Architectural Boundary)의 생성을 책임지는 변경의 축(Axis of Change)이 된다.

 

 

반응형

댓글0