[Clean Architecture] Chapter 7. The Single Responsibility Principle
[이 글은 Clean Architecture의 chapter 7을 읽고 정리한 글이다. 문장의 간결함을 위해서 인용체를 생략하였고, 내 생각을 조금 덧붙였다.]
SRP(The Single Responsibility Principle)는 이름과 달리 하나의 모듈이 하나의 책임만을 가져야 한다는 뜻은 아니다. 반드시 하나의 행동만 해야 하는 건 함수이다.
SRP는 다음의 문장으로 설명할 수 있다. (여기서 reason을 user, stakeholder, actor 등의 단어로 바꾸어 이해하면 된다.)
"A module should have one, and only one, reason to change."
흔히 low coupling, high cohesion을 좋은 코드라고 하는데, 이것이 바로 SRP가 의미하는 바이다.
SRP는 모듈(클래스) 단위에서의 원칙이고, (나중에 다루겠지만) 이를 컴포넌트 레벨에서 이야기한다면 Common Closure Principle, 아키텍처 레벨에서 이야기한다면 the Axis of Change responsible for the creation of Architectural Boundaries이다. (후자는 내용을 읽어봐야 번역을 제대로 할 수 있을 것 같다.)
SRP를 위반했을 때 증상
1. 우연한 코드 중복으로 인한 코드 공유
다음은 SRP를 위반한 코드이다.
class Employee
- calculatePay
- reportHours
- save
SRP의 정의가 "A module should have one, and only one, actor to change"라고 이해할 수 있다고 했는데, Employee 클래스의 세 메서드는 모두 actor가 다르다. calculatePay는 재무팀의 일이고, reportHours는 HR의 일이고, save는 데이터팀의 일이다. 각 부서는 각자의 이유로 변경할 이유들이 생긴다. 이렇게 다른 성격의 코드들이 한 클래스에 섞여있으면(coupling), 한 팀의 요구에 의해 일어난 코드 변경이 다른 팀에도 영향을 끼칠 수 있다.
calculatePay와 reportHours가 근무 시간을 계산하기 위한 알고리즘이 '우연히' 같다고 하자. 이 경우에 대부분의 개발자들은 코드 중복을 줄이고 싶은 본능에 따라 해당 로직을 공통의 함수(regularHours)에 넣고, calculatePay와 reportHours가 regularHours를 사용하도록 개발할 것이다. 만약에 재무팀의 요구에 따라 calculatePay의 동작을 바꾸게 되었다고 하자. 해당 변경을 위해서 개발자가 regularHours의 로직을 수정한다면, 안타깝게도 이는 HR이 사용하는 reportHours에도 영향을 끼치게 된다.
이런 상황은 나도 실제로 많이 겪어봤다. 가령 어떤 클래스의 메서드를 수정하려는데, 이 클래스를 사용하는 곳이 너무 다양하고 각자 내용이 너무 달라서, 내가 만든 변경이 어떤 영향을 끼칠지 확신할 수 없는 경우가 있다. 실제로 사용하는 쪽 코드를 하나하나 확인하면서 문제가 없을 것 같다고 판단했지만, 배포 후에야 문제를 일으키는 곳을 발견해서 급하게 수정한 적도 있다. 개발자가 어떤 코드를 수정했는데 그 수정의 영향 범위를 알 수 없다는 것, 이것 참 무서운 일이다. QA에서라도 혹시 모를 버그를 잡아야하는데 일일이 테스트하는 것도 다 리소스가 들고, 미처 파악하지 못한 엣지 케이스가 있다면 결국 사고가 터지게 된다.
언제는 기존 앱과 거의 동일하지만 세부사항이 조금 다른 새로운 앱이 출시할 일이 있었다. 새로운 앱을 위해서 API를 제공해야 하는데, 이걸 그냥 기존 앱의 API를 사용할지, 아니면 복붙을 하더라도 새로운 API 세트를 새로 만들지 고민이 되었다. 그때 팀이 결정한 방향은 아무리 복붙에 죄책감이 들어도 별도의 API를 한 벌 새로 만들자는 것이었다. 지금은 동작이 거의 동일하지만 언제 어떻게 스펙이 달라질지 모르므로, 무심코 코드를 공유해버리면 나중에 관리하기 어려워질 수 있다는 것이 이유였다. 지금 생각해보면 복붙이 더 좋은 선택이 맞았던 것 같다. 새로운 앱에 스펙 변경이 생겨도 현재 개발자가 느끼는 부담이 훨씬 적기 때문이다. (마찬가지로 상속도 parent와 child가 미래에 동작이 분기될 가능성이 없다는 것을 확신해야만 할 수 있는, 현업에서는 꽤 흔치 않게 내려지는 선택인 것 같다.)
2. 잦은 merge conflict
두번째 증상은 바로 잦은 merge conflict이다. 위에서 예로 든 Employee의 경우 세 개의 팀이 사용하고 있는 클래스이므로, 각 팀의 요구에 따라 변경이 일어나게 되고 이는 merge conflict를 발생시킨다. 아무리 툴이 좋아졌다고 한들, merge conflict가 잘못 해결될 가능성은 언제나 존재한다. (실제로 나도 merge conflict를 잘못 해결해서 팀원의 코드를 날려먹은 적이 있고, 다른 팀원이 내 코드를 날려먹은 적도 있다.)
해결책은?
해결책은 간단하다. actor가 다른 코드는 다른 클래스로 분리하는 것이다. 그래야 위험하게 서로 영향을 주고받는 일이 없어진다. 위 예시의 경우 calcularPay, reportHours, save에 해당하는 로직은 별도의 클래스에 각각 존재해야 한다. (자세한 아이디어는 책을 참고)