클린 아키텍처를 읽으며 정리한 내용입니다.
SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하고, 이들을 서로 결합하는 방법을 설명해준다. 단, 여기에서의 클래스는 함수와 데이터를 결합한 집합으로, SOLID가 객체 지향에만 적용되는 원칙은 아니다.
SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데에 있다.
여기에서 중간 수준이란 모듈 수준, 즉 코드 수준보다는 조금 상위를 의미한다. SOLID는 모듈과 컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의하는 데에 도움을 준다.
SOLID 원칙은 아래와 같다.
단일 책임 원칙의 의미는 모든 모듈이 단 하나의 일만 해야 한다는 것이 아니지만, 개발자들은 이 원칙을 '함수는 단 하나의 일만 해야 한다'는 원칙과 헷갈리곤 한다.
이 원칙의 진정한 의미는 '하나의 모듈은 하나의 액터에 대해서만 책임져야 한다'는 것이다.
여기에서 액터란 변경을 요청하는 집단을 의미하며, 모듈은 함수와 데이터 구조로 구성된 응집된 집합(보통은 소스 파일)을 의미한다.
이 원칙을 이해하기 위해 위반 징후를 먼저 살펴보자.
SRP를 위반하는 클래스의 사례로 다음과 같이 서로 매우 다른 세 명의 액터를 책임지는 Employee 클래스를 정의해볼 수 있다.
calculatePay()
메서드 - 회계팀에서 CFO 보고를 위해 사용reportHours()
메서드 - 인사팀에서 COO 보고를 위해 사용save()
메서드 - DBA가 CTO 보고를 위해 사용이 결합으로 인해 CFO 팀에서 결정한 조치는 COO 팀이 의존하는 무언가에 영향을 줄 수 있다.
만약 calculatePay()
와 reportHours()
가 정규 업무 시간을 계산하는 알고리즘을 공유하며, 이 알고리즘을 regularHours()
라는 메서드 하나로 공유한다면 어떻게 될까?
CFO에서 정규 업무 시간을 계산하는 방식을 수정 요청하고, 수정하는 개발자가 regularHours()
가 양쪽에서 호출한다는 사실을 눈치채지 못한다면 CFO 팀에서의 수정이 COO 팀의 로직에 영향을 주게 된다.
불행히도 우리는 모두 이런 상황을 목격한 경험이 있다..
SRP는 이러한 상황을 방지하기 위해 서로 다른 액터가 의존하는 코드를 분리하라고 말한다.
소스파일에 다양하고 많은 메서드를 포함할수록, 그리고 이 메서드가 서로 다른 액터를 책임질수록 병합이 발생할 가능성이 높다.
CTO 팀에서 데이터베이스의 Employee
테이블 스키마를 수정하는 동시에 COO 팀에서 reportHours()
메서드의 보고서 포맷을 변경하기로 결정한다면 어떻게 될까?
아마 서로 다른 팀에 속한 개발자가 각자 변경사항을 적용하고, 이 변경사항은 충돌할 것이다. 병합에는 당연히 위험이 뒤따른다.
이 문제에서 벗어나는 방법은, 앞서 말했듯이 서로 다른 액터를 뒷받침하는 코드를 분리하는 것이다.
이러한 문제들의 해결책은 다양한데, 모두가 메서드를 각기 다른 클래스로 이동시키는 방식이다.
아마도 가장 확실한 해결책은 데이터와 메서드를 분리하는 방식일 것이다.
예를 들어, 아무 메서드가 없는 EmployeeData
클래스를 만들어 세 클래스가 공유하도록 하고, 각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함하며 서로의 존재를 모르도록 한다면 우연한 중복을 피할 수 있다. 하지만 이 방법은 개발자가 세가지 클래스를 인스턴스화하고 추적해야 한다.
대안으로는 파사드 패턴이 있다.
EmployeeFacade
에 코드는 거의 없고, 이 클래스는 세 클래스의 객체를 생성하고 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.
가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호한다면, 가장 중요한 메서드는 기존의 Employee
클래스에 그대로 유지하되, Employee
클래스를 덜 중요한 나머지 메서드들에 대한 파사드로 사용할 수도 있다.
단일 책임 원칙은 기본적으로 메서드와 클래스 수준의 원칙이다.
하지만 같은 원칙이 컴포넌트 수준에서는 공통 폐쇄 원칙으로, 아키텍처 수준에서는 변경의 축으로 다시 등장할 것이니 기대하자.