단일 책임의 원칙 (single responsibility princinple)
이 분은 Object Mentor 의 리더이자, Clean Code의 저자인 Bob 삼촌 (Uncle Bob – Robert C. Martin) 의 글이 “모든 프로그래머가 알아야할 97가지” 에 실려 있습니다.
저도 사실 뭔가 재미난 이야기를 해 줄거라고 했는데… 저희에게 너무나도 익숙한 객체지향의 중요한 원칙인 SOLID 의 S인 SRP를 이야기를 하셨네요.
익숙하지만 정말 중요한 원칙인 SRP 이야기를 다시 한번 들어보시길 바랍니다.
Single Responsibility Principle written by Uncle Bob
좋은 설계를 위한 가장 기본적인 원칙 중 하나는 다음과 같습니다.
동일한 이유로 변경되는 것들은 함께 모으고, 서로 다른 이유로 변경되는 것들은 분리시킨다.
이 원칙은 종종 단일 책임의 원칙(Single Responsibility Principle, SRP)이라고도 알려져 있습니다. 한마디로 말해서, 하나의 서브시스템, 모듈, 클래스, 또는 심지어 함수에 대해서도 한 가지 이상의 변경 이유가 있어서는 안 된다는 것을 의미합니다.
이를 설명하기 위한 전형적인 예제로, 비즈니스 룰, 결과 보고서 작성, 데이터베이스 연산을 다루는 메소드들을 함께 가진 클래스를 들 수 있습니다.
public class Employee { public Money calculatePay () … public String reportHours () … public void save() … }
- calculatePay 함수는 급료를 계산하는 비즈니스 룰이 바뀔 때 매번 변경되어야 합니다.
- reportHours 함수는 누군가가 기존과 다른 보고서 포맷을 원할 때, 그리고
- save 함수는 데이터베이스 관리자가 데이터베이스 스키마를 바꿀 때마다 변경되어야 합니다.
이렇듯 세 가지 변경 원인이 있다는 사실은 Employee 클래스를 불안정하게 만드는데, 이는 세 가지 원인 중 한 가지만 발생하여도 클래스가 바뀌어야 하기 때문입니다. 더 중요한 것은, Employee 에 의존하고 있는 클래스들 역시 이 변경으로 영향을 받을 수 있다는 것입니다.
좋은 시스템 설계는 시스템을 독립적으로 배포될 수 있는 컴포넌트들로 분할하는 것입니다. 독립적 배포란 우리가 특정 컴포넌트를 변경하더라도 그 외 다른 컴포넌트들은 재 배포될 필요가 없다는 것을 의미합니다. 그러나 위 예제의 경우, 타 컴포넌트들에 있는 다수의 클래스에서 Employee 를 사용하고 있다면, Employee 가 변경될 때마다 그 컴포넌트들 또한 재 배포 되어야 합니다. 결국 이는 컴포넌트 기반 설계(혹은 서비스 지향 아키텍처 설계)의 주요 이점을 잃게 되는 결과를 초래하게 됩니다. 다음 코드는 간단하게 클래스 분할을 통해 이 이슈를 해결하였습니다.
public class Employee { public Money calculatePay() ... } public class EmployeeReporter { public String reportHours(Employee e) ... } public class EmployeeRepository { public void save(Employee e) ... }
분리된 클래스들은 각각 자신의 컴포넌트에 위치할 수 있게 됩니다. 좀 더 정확히 말하면, 결과 보고를 담당하는 모든 클래스는 ‘보고’ 컴포넌트로, 데이터베이스와 관련된 모든 클래스는 ‘저장소’ 컴포넌트로, 그리고 모든 비즈니스 룰에 대한 클래스는 ‘비즈니스 룰’ 컴포넌트로 들어갈 수 있게 됩니다. 재빠른 독자라면 위의 해결책에도 여전히 의존관계가 존재한다는 것을 알 수 있을 겁니다.
Employee 는 여전히 다른 클래스들에 의해 참조되고 있기 때문에, Employee 가 수정되면 다른 클래스들도 재 컴파일되고 재 배포 되어야 할 것입니다. 따라서 우리는 Employee 를 수정하여 독립적으로 재 배포 할 수는 없습니다. 그러나 그 외 다른 클래스들은 수정 및 독립적인 재 배포가 가능해집니다. 그들 중 어떤 클래스가 변경되더라도, 그 외 다른 클래스들을 재 컴파일 하고 재 배포할 필요가 없게 되는 것이지요. 만약 우리가 의존관계 역전의 법칙(Dependency Inversion Principle, DIP)을 잘 사용한다면, Employee 역시 독립적으로 재 배포가 가능해 질 것입니다. 의존 관계 역전의 법칙은 다른 책*의 주제이므로 여기에서 다루지는 않겠습니다.
서로 다른 이유로 변경되는 것들을 잘 분리해야 한다는 단일 책임의 원칙을 잘 적용하는 것이 독립적으로 배포가 가능한 컴포넌트 구성을 만들 수 있는 중요한 비결 중 하나라는 것을 기억하시기 바랍니다.
*Agile Software Development, Principle, Patterns and Practices. http://www.amazon.com/dp/0135974445/
SRT에서 R의 정확한 정의가 필요합니다. 전 작은 기능단위의 R은 좋지 않다고 생각합니다. 너무 많은 Fragmentation과 Relation을 만들기 때문이죠. 실제로 상용 S/W 중에 SRT를 매우 잘지키는 것들이 있는데, 객체지향적으로는 훌륭하나 Readability가 매우 떨어집니다. Concrete를 바로바로 알기가 어렵죠.
영수님 오랜만이에요..^^
사실 사용성과 확장성(유연성)과는 자주 상충됩니다.
이 두 가지를 만족하기란 그리 쉽지가 않죠. ^^
이건 아마도 저희들의 영원한 숙제가 될지도 🙂
97 아키텍트에 도움이 될만한 글이 있습니다.
일반화 이전에 단순화, 재사용성 이전에 사용성
Simplicity Before Generality, Use Before Reuse
설계의 기준으로 불확실성을 사용해라.
https://arload.wordpress.com/2009/09/24/use-uncertainty-as-a-driver/
설계에 대해서는 잘 모르지만, 말씀하신 “상용S/W”라서, release가 회사 내부규칙에 따라 정해 지는 S/W라면 모르겠지만, 저와 같이 고객이 “변경해내!” 라고 말하면 그때 그때 배포하는 S/W라면, 조금 readability가 떨어지더라도, SRT를 적용하려 애썼습니다…. 만..
결국, 해당 기능이 잘 고쳐지지 않고, fixed된 상태를 꽤 오래 유지하면, 다시, 말씀하신대로, 하나의 class로 refactoring 되는 (?) 경우도 있었습니다.
왜냐하면, 위의 예제에서 처럼
save() 함수 같은 경우 employee객체 e 에 대한 참조가 save() 안에서 빈번하게 이루어지기 때문이죠.
file->Write(e->Name());
file->Write(e->Age());
..
이런식으로요..
외부로 공개되지않는 api 나 frmamework와 같이 api 사용성 부분이 중요하지 않으면, 당연히 확장성에 초점을 맞추어야 겠지요. 좋은 접근 입니다. 🙂
결국 서로 계속해서 호출을 주고 받아서 엮인다면 이 녀석들끼리 ParameterObject 또는 Domain Context Object 형태로 묶어서 퉁 처리하시는게 좋을듯 합니다.
저도 동의합니다. 단, 계속 해당 업무에 F/U을 할꺼라면 말이죠.
제 3자가 유지보수할꺼라는 가정아래 약간의 타협이 필요하다고 생각합니다.
물론 제 3자가 복잡한 구조라도 많은 스태미나를 사용하여 구조를 정확히 파악하여 유지보수 한다면 타협이 필요없을겁니다. 그런 제 3자는 찾아보기 어렵죠. 아마 바로 메일이나 전화를 할겁니다.
설계, 리팩토링등은 메인 개발자가 있는 상황에서는 구조적으로 좋은 방향으로 가는 것이 좋겠지만, 그 뒷일까지 생각하는게 나중에 전화를 덜 받을수 있는 현명한 길일수도 있다고 생각합니다. 그리고 확장성이 좋다는 것은 확장을 하고 나서야 알수 있지 않을까요^^;; 잘모르는데 너무 주저리 떠들었네요;; 죄송;;