넘치게 채우기

3-1. SRP: 단일 책임 원칙(Single Responsibility Principle) 본문

개발/Clean Architecture

3-1. SRP: 단일 책임 원칙(Single Responsibility Principle)

riveroverflow 2023. 11. 9. 00:22
728x90
반응형

SRP는 아래와 같이 기술되어 왔다.

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

소프트웨어 시스템은 사용자와 이해관계자를 만족시키기 위해 변경된다.

 

이 원칙은 아래와 같이 바꿔 말할 수도 있다.

“하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져야 한다.”

 

안타깝게도, ‘사용자’와 ‘이해관계자’라는 단어를 여기에 쓰는 건 올바르지 않다.

시스템이 동일한 방식으로 변경되길 원하는 사용자나 이해관계자가 두 명 이상일 수도 있기 때문이다.

여기에서는 이런 의미보다는 집단, 즉 해당 변경을 요청하는 한 명 이상의 사람들을 가리킨다.

이러한 집단을 액터(actor)라고 부르겠다.

이제 SRP의 최종 버전은 다음과 같다.

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

 

그럼 ‘모듈’이란 또 무슨 뜻인가?

가장 단순한 정의는 바로 소스 파일이다.

 

대부분의 경우 이 정의는 잘 들어맞는다.

하지만, 일부 언어과 개발 환경에서는 코드를 소스 파일에 저장하지 않는다.

이러한 경우 모듈은 단순히 함수와 데이터 구조로 구성된 응집된 집합이다.

 

‘응집된’이라는 단어가 SRP를 암시한다.

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

 

징후 1: 우발적 중복

급여 애플리케이션의 Employee 클래스다. 이 클래스는 세 가지 메서드(calculatePay(), reportHours(), save())를 가진다.

그림 7.1: Employee 클래스

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

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

개발자가 이 세 메서드를 Employee라는 단일 클래스에 배치하여 세 액터가 서로 결합되어 버렸다.

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

 

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

  1. CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 수정하기로 했다고 하자. 반면, COO 팀에서는 다른 목적으로 사용하기에, 변경을 원하지 않는다.
  2. 이 변경을 적용하는 업무를 할당받은 개발자는 calculatePay()regularHours()를 호출한다는 것을 발견한다. 하지만 reportHours()에도 쓰이는 줄은 모른 채이다.
  3. 개발자는 요청된 변경사항을 적용하고, 테스트를 한다. CFO팀에서 새 메서드가 잘 동작하는지 확인한 후, 시스템은 배포된다. 그러나, COO팀은 모르고 있다.
  4. COO 팀 직원은 여전히 reportHours() 메서드가 생성한 보고서를 이용한다. 이는 엉터리가 되어있다.

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

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

 

징후 2: 병합

소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생하리라고 짐작하는 건 어렵지 않다.

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

 

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

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

  1. 두 명의 서로 다른 개발자가, 그리고 아마 서로 다른 팀에 속했을 두 개발자가 Employee 클래스를 체크아웃 받은 후 변경사항을 적용하기 시작한다.
  2. 이 변경사항들은 충돌한다. 결과적으로 병합이 발생한다.

병합에는 항상 위험이 따른다. 이 병합으로 CTO와 COO 모두 곤경에 빠지게 된다.

 

해결책

해결책은 간단하다.

각 메서드를 다른 클래스로 이동시키면 된다.

가장 확실한 방법은 데이터와 메서드를 분리하는 방식일 것이다.

 

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

각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함한다. 세 클래스는 서로의 존재를 몰라야 한다.

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

그림 7.2: 세 클래스는 서로의 존재를 알지 못한다.

 

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

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

그림 7.3: 퍼사드(Facade) 패턴

EmployeeFacade에 코드는 거의 없다.

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

 

 

어떤 개발자는 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호한다.

이 경우라면 가장 중요한 메서드는 기존의 Employee 클래스에 그대로 유지하되, Employee 클래스를 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용할 수도 있다.

 

그림 7.4: 가장 중요한 메서드는 시존의  Employee  클래스에 그대로 유지하되,  Employee  클래스를 덜 중요한 나머지 메서드들에 대한 퍼사들로 사용한다.

 

 

결론

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

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

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

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

728x90
반응형