본문 바로가기

클린 아키텍처

[개발서적] 클린 아키텍처 07 - SRP : 단일 책임 원칙 (Single Responsibility principle)

 

SRP의 오해

SRP에 대해 흔히 오해하는 것들은 모든 모듈들이 단 하나의 일만 해야 한다는 것이다. 그러나 이러한 원칙은 SRP 가 아닌 전혀 다른 원칙이다. 이 원칙 "함수는 단 하나의 일만 해야 한다"는 커다란 함수를 작은 함수로 리팩터링하는 더 저수준에서 사용되는 원칙으로 SRP가 아니며 SOLID 원칙 또한 아니다.

SRP(Single Responsibility principle) 원칙이란? 

SRP 원칙은 다음 문장으로 정의할 수 있다. 

하나의 모듈은 하나의, 오직 하나의 액터(Actor)에 대해서만 책임져야 한다.

 

여기서 액터(Actor)는 변경을 요청하는 사용자 또는 이해관계자의 집단을 의미한다. 모듈은 함수와 데이터 구조로 구성된 응집된 집합으로 단일 액터를 책임지는 코드를 함께 묶어주는 것이다.

  • 액터(actor): 변경을 요청하는 한 명 이상의 집단
  • 모듈: 함수와 데이터 구조로 구성된 응집된 집합

아래 윈칙을 위반하는 몇 가지 징후들을 통해 SRP에 대해 이해해 볼 수 있다.

 

원칙을 위반하는 징후들

징후 1: 우발적 중복

우발적 중복 징후는 하나의 클래스에 여러 액터를 책임지게 되면서 발생하는 징후다. 아래 급여 애플리케이션의 Employee 클래스의 예로 알아보자. Employee 클래스는 아래 세 가지 메서드를 가진다.

  • calculatePay() :회계팀에서 기능을 정의, CFO 보고를 위해 사용
  • reportHours() :인사팀에서 기능을 정의, COO 보고를 위해 사용
  • save() :데이터베이스 관리자가 기능을 정의, CTO 보고를 위해 사용

이 클래스는 SRP를 위배하는 데 세가지 메서드는 각기 다른 액터를 책임지기 때문이다. Employee라는 단일 클래스에 세 메서드가 배치되면 세 액터가 결합되는 문제가 생긴다. 이런 경우 한 액터를 책임지는 메서드의 알고리즘을 수정하기 위해 세 메서드에서 공유하는 알고리즘이 수정되는 경우가 발생할 수 있다.

 

예를 들어 calculatePay()와 reportHours() 메서드가 초과 근무를 제외한 업무 시간을 계산하는 알고리즘을 공유한다고 가정해보자.  여기서 CFO를 위한 메서드 calculatePay()의 업무 시간을 계산하는 알고리즘이 수정이 필요해져 CFO 개발팀은 해당 알고리즘을 수정하고 테스트를 마쳐 정상 동작하는지 확인을 마친 후 배포를 하였다. COO 팀에서는 이 변경사항을 모르는 상황에서 reportHours()를 이용하게 되는데 이 수치들은 엉뚱한 데이터를 보여주게 된다. 결국 CFO 팀에 의해 예기치 않은 문제가 COO팀에 발생하게 된다.

 

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

 

징후 2: 병합

다양하고 많은 메서드를 소스 파일에 포함하는 경우 병합이 자주 발생하리라고 짐작 할 수 있다. 여기서 메서드가 서로 다른 액터를 책임진다면 병합 가능성은 더 확실해진다. 

 

예를 들어 DBA가 속한 CTO 팀에서 Employee 테이블 스키마를 수정하기로 결정했다고 해보자, 동시에 인사담당자가 reportHours()의 포멧을 변경하려고 한다. 이 경우 두 개발자가 Employee 클래스를 체크아웃 후 수정하게 되는데 이 변경사항은 충돌이 발생하게 된다.

이는 서로 다른 목적으로 동일한 소스 파일을 변경하는 경우 발생하는 징후로 이 문제를 벗어나기 위해서는 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

 

 

해결책 

이 문제의 해결방법 중 가장 확실한 해결방법은 데이터와 메서드를 분리하는 것이다. 아무런 메서드가 없는 데이터 구조인 EmployeeData 클래스를 만들어 세 개의 클래스가 공유하도록 한다. 그리고 각 클래스는 자신의 메서드에 꼭 필요한 소스 코드만 포함한다. 그리고 세 클래스는 서로의 존재를 몰라야한다. 이로써 '우연한 중복'을 피할 수 있게 된다.

세 클래슨느 서로의 존재를 모른다.

 

이 방식은 단점이 있는 데 세 가지 클래스를 인스턴스화 하고 추적해야하는 것이다. 이 문제를 벗어나기 위한 방법으로 퍼사드(Facade) 패턴이 있다.

 

퍼사드(Facade) 패턴

 

EmployeeFacade에는 코드가 거의 없다. 이 클래슨느 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.  가장 중요한 업무 규칙을 데이터와 가까이 배치하길 원하는 경우 기존의 Employee 클래스에 그대로 유지하되, Employee 클래스에 덜 중요한 나머지 메서드들을 배치할 수 있다.

 

가장 중요한 메서드는 Employee 클래스에 유지, 덜 중요한 나머지 메서드들은 퍼사드로 사용

 

 

현실과의 차이

모든 클래스는 반드시 단 하나의 메서드를 가져야 한다는 주장은 현실과 다르다. 각 클래스의 기능을 구현하는 데 필요한 메서드의 개수는 훨씬 많을 것이다. 이들 클래스는 다수의 private 메서드를 포함할 것이다. 여러 메서드들로 이루어진 하나의 가족을 포함하는 각 클래스는 하나의 유효범위가 된다. 해당 유효범위 바깥에서는 이 가족에게 감춰진 식구(private 멤버)가 있는지를 전혀 알 수 없다.

 

결론

  • 단일 책임 원칙은 메서드와 클래스 수준의 원칙이다.
  • 컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle)이 된다.
  • 아키텍처 수준에서는 아키텍처 경계(Architecural Boundary)의 생성을 책임지는 변경의 축(Axis of Change)이 된다.