본문 바로가기

클린 아키텍처

3부 설계원칙-SRP

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

SRP란?

클래스는 하나의 책임을 가져야 한다.

책임 이란?

SRP에서 하나의 책임이라는 것 단 하나의 일만 해야 한다는 뜻이 아니다. (하나의 일만해야한다는 원칙은 따로 있다. 함수는 반드시 하나의 일만 해야한다는 원칙이다.)

SRP에서 하나의 책임이라는 것은 "변경의 이유가 하나, 오직 하나 뿐이여야 한다." 라는 뜻이다.

여기서 "변경 이유"는 변경을 요청하는 사람들을 가르킨다. 이러한 집단을 엑터라고 한다.

다시 정의 하면 SRP 는 "오직 하나의 액터에 대해서만 변경이 되어야 한다."

 

SRP 위반시 문제

Collision

 

예제 요구사항:

Policy Actors: business rule의 변경을 필요로 함
Architector Actors: DB Schema의 변경을 필요로 함
Operations Actors: Business rule의 변경을 원함

 

결과 :

Merge 충돌. Source Repo 충돌

잦은 충돌로 인해 지속적인 변하는 요구 사항을 수용하기 어렵게 된다.

 

Fan Out

  • Employee 클래스는 너무 많은 책임을 가지고 있음.
  • 각각의 책임은 다른 클래스를 사용하도록 만든다.(즉 많은 Fan out이 생긴다.)
  • 이는 변경에 민감하게 함.(Emplyee의 팬아웃으로 인해 사용되는 클래스에 의해 변경에 민감해진다. 하지만 여기서 던 큰 문제는 Employee를 사용하는 유저2개이다. 위두개는 Employee의 변경에 따라 더많은 변경이 생길 수 있다.)
  • 따라서 Fan out를 줄일 필요가 있다.

 

Collocation is Coupling

Operations Actor가 새로운 리포트 기능을 필요하다고 가정할 경우 새로운 리포트 기능도 Employee 클래스에 추가함. 기존 책임(Policy, Architecture)에는 변경이 없음에도 새로운 리포트가 추가되어 Employee 클래스가 변경.

  • 새로운 리포트 기능이 Employee 클래스에 추가되면 이 기능을 필요로 하지 않는 Employee 클래스를 사용하는 모든 클래스들이 다시 컴파일/배포되어야 함
  • 모든 액터들이 영향을 받게 된다(Collocation of responsibilities couples the actors)

Accidental duplication

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

예를 들어 calculatePay와 reportHours의 초과근무 알고리즘을 regularHours라는 메서드에 넣고 사용하고 있다고 가정해보자.

이때 CFO팀 초과근무시간의 계산하는 방식을 약간 수정한다고 가정할때 프로그래머가 regularHours를 호출한다는 사실을 확인하지만 reportHours가 이 함수를 사용한다는 것을 발견하지 못하고 수정한다면 COO팀의 초과근무수치는 엉터리가 되고 만다.

위와 같은 문제는 서로다는 엑터를 너무 가까운 위치에 두었기 때문이다.

 

해결법

Inverted Dependencies

의존성 역전. OOP에서 이런 의존성을 다루는 전략. 클래스를 인터페이스와 클래스로 분리시킴

  • 액터를 클래스에서 Decouple 그러나, 모든 액터들이 하나의 인터페이스에 coupled 되어 있으며, 하나의 클래스에 구현되어 구현도 coupled

Extract Classes

3개의 책임을 분리하는 방법: 3개의 클래스로 분리

  • 액터들은 분리된 3개의 클래스에 의존
  • 3개의 책임에 대한 구현은 분리
  • 하나의 책임이 변경되어도 다른 책임에 영향을 미치지 않음

문제점

  • transitive dependency (EmployeeGateway/EmployeeReporter -> Employee).
    • Employee에 변경이 있을 때, EmployeeGateway/EmployeeReporter에 영향을 미칠 수 있음.
  • Employee의 개념이 3개의 조각을 분리됨.

Inverted Dependencies + Extract Classes

위 두가지 방법을 적절히 배합하면 더 나은 결과를 도출 시킬 수 있음. 우선 인터페이스를 extract 한후, 책임에 따라 인터페이스를 잘게 쪼개어 3개의 인터페이스로 분리시킴.

이후, EmployeeImpl(Employee 클래스)가 3개의 인터페이스를 구현 (또는 EmployeeImpl를 3개로 나눠도 됨) . 이를 통해 특정 액터에 의한 인터페이스 변경이 일어나더라도 다른 액터들은 영향을 받지 않게 됨.

Facade

어디에 구현이 있는지 찾기 쉬워짐.

어디에 구현되어있는지 찾기 쉬운 장점이 있지만 여전히 액터들은 Coupled 되어 있음.

Interface Segregation

각 책임에 대하여 3개의 인터페이스를 만든 후, 3개의 인터페이스를 하나의 클래스로 구현

액터들은 완전히 decoupled되어 있지만, 어디에 구현되어있는지 찾기 어려우며, 하나의 클래스에 구현되어 여전히 구현은 coupled 되어 있음.

 

좋은 아키텍쳐?

패키지 다이어그램을 살펴보면 Customer를 위한 책임이 어플리케이션 아키텍처의 중심인 것을 알 수 있음. 각 패키지는 각 액터들을 위한 책임을 구현하였음.

 

  • 패키지 간의 의존성 방향에 주의(모두 gamePlay 패키지로 향하고 있음)
  • 이 설계는 좋은 아키텍처 (어플리케이션이 중앙에, 다른 책임들은 어플리케이션에 plug into)

 

  • 각 클래스는 하나의 액터만을 위한 기능을 제공
    • 하나의 클래스는 반드시 하나의 책임만을 가짐
  • 패키지간의 의존성이 한 방향으로 향하고 있음.

 

언제 SRP 하는게 좋은가?

SRP는 시스템을 설계할 때 가장 적용하기 좋음.

시스템 설계

  • 액터파악에 주의해야 함. Actor들을 serve하는 책임들을 식별할 것
  • 책임을 모듈에 할당 (각 모듈이 반드시 하나의 책임을 갖도록 유지)

설계때 이 모든 설계가 가능한가?

Waterfall 순서로 한 것 같은가? step by step으로 이 복잡한 것들을 모두 찾아 낸 것 같은가? (액터 → 패키지 다이어 그램 → 클래스 다이어그램 → 코드)

실제로는 그렇지 않음. 실제로 한 것은

  1. 제일 먼저 테스트를 작성하고 통과하도록 했다.
  2. 스코어를 계산하는 동작하는 함수를 하나 만들고, 추측하는 로직을 위한 동작하는 함수를 하나 만들고, 설계가 드러날 때까지 이 함수 저 함수를 리팩토링 했다.
  3. 동작하는 전체 게임을 얻을 때까지 모든 동작들이 테스트에 성공하도록 설계를 적용했다.
  4. 그리고 아키텍처를 살폈다. 테스트가 당신들에게 보여준 설계의 80%를 유도했다.
  5. 그리고 테스트가 3개의 책임을 식별하는 것을 도왔다.
  6. unit test가 확보된 후에 무차별적인 리팩토링을 수행했다(디자인을 향상시키기 위해서)
  7. 이런 후에만 3개의 액터들이 식별된다. 그러면 클래스들을 3개의 패키지로 분리시킨다.
  8. 마지막으로 이쁜 다이어그램을 그린다. 이쁜 다이어그램을 그리기 가장 좋은 때는 완료된 후이다.

'클린 아키텍처' 카테고리의 다른 글

3부 설계원칙-ISP  (0) 2023.01.16
3부 설계원칙-LSP  (0) 2023.01.16
3부 설계원칙-OCP  (0) 2023.01.10
2부 패러다임  (0) 2022.12.22
1부 소개  (0) 2022.12.18