SRP(Single Responsibilty Principle)
SRP?
SRP는 단일책임원칙 (Single Responsibilty Principle) 의 약자로 객체는 단 하나의 책임(기능)만 담당해야한다.
다용도 공구를 사용하거나 가위/커터칼/드라이버를 따로 사용하느냐를 선택한다면 후자가 더 좋다는 입장 !
목적
“청소기 클래스는 청소 메소드만 잘 구현하면 되지
화분에 물을 주고 드라이 까지 할 책임은 없다.”
이 원칙은 기능 변경이 일어났을 때 일어나는 파급효과 측면에서 중요하다.
한 객체에 책임이 많아질수록 클래스 내부의 서로 기능의 코드끼리 강하게 결합된다.
이 때 만약 어떤 기능하나를 변경한다면, 그와 연관된 다른 코드를 모두 수정해야 한다
(실제로 기능에 변화가 없더라도).
만약 변경 영향을 받는 기능만 모아둔 클래스라면 그 책임을 지니고 있는 클래스만 수정해주어 책임 영역이 확실해지게 된다.
즉, 모듈이 변경되는 이유가 한가지여야한다 (연쇄작용 불허).
예제
class Employee{ // 직원 클래스 ;직원정보를 다루는(책임) 클래스
String name ;
String position ;
Employee(String name, String position){
this.name = name;
this.position = position;
}
// 급여 계산 메서드 (회계팀 사용)
void calculatePay() { this.calculateExtraHour(); }
// 총 근무시간 계산 메서드 (인사팀 사용)
void reportHours() { this.calculateExtraHour(); }
// 개발팀 사용
void saveDatabase() { /* */ }
// 초과근무시간 계산 메서드
void calculateExtraHour() {/* */}
}
문제
초과근무시간 계산 메서드를 회계팀, 인사팀 메서드에서 각각 사용하고 있기 때문에
만약 한쪽의 요구사항 변경으로 해당 메서드를 수정하면 다른 팀 로직에도 영향이 간다.
즉, Employee 클래스에서 회계팀, 인사팀, 기술팀 이렇게 3개의 액터에 대한 책임을 동시에 가지고 있다
(3개의 액터가 하나의 클래스를 변경할 수 있는 요인이 된다)
해결
즉 회계팀, 인사팀, 기술팀 각 책임(기능 담당)에 맞게 클래스를 분리한다.
( PayCaculator, HourReporter, EmployeeSaver)
// 회계팀 전용 클래스
class PayCalculator{
void calculateExtraHour(){ /*A*/ }
void calculatePay() {this.calculateExtraHour();} // A 사용
}
// 인사팀 전용 클래스
class HourReporter{
void calculateExtraHour() { /*B*/}
void reportHours() {this.calculateExtraHour(); } // B 사용
}
// 기술팀 전용 클래스
class EmployeeSaver{
void saveDataBase() {}
}
이렇게하면, 만일 변경 사항이 생겨도, 각각의 분리된 클래스에서만 수정하면 되기 때문에
다른 클래스의 기능을 이용하는데 있어 아무런 문제가 생기지 않는다.
ex) 인사팀의 요구사항 바뀜 : B -> B`만 변경해주면 됨
추가 : Facade 패턴
편의를 위해, 이를 통합적으로 사용하는 클래스인 EmployeeFacade 클래스를 만든다.
EmployeeFacade 클래스의 메서드에는 사실상 아무런 로직도 담겨있지 않으며,
생성자로 인스턴스를 생성하고 각 클래스의 메서드를 사용하는 역할만 담당한다.
즉, 어떠한 액터도 담당하지 않는 것이다.
// 통합 사용 클래스
class EmployeeFacade {
private String name ;
private String position ;
EmployeeFacade(String name, String position){
this.name = name ;
this.position = position;
}
void caculatePay() {
new PayCalculator().calculatePay();
}
void reportHours() {
new HourReporter().reportHours();
}
void saveDataBase() {
new EmployeeSaver().saveDataBase();
}
}
이렇게 하면 직원 관리 관련 메서드(기능) 호출은 EmployeeFacade 클래스 하나로 하되,
실제 로직은 각각 서브 클래스들에서 구현되도록 해 SRP 원칙을 준수한다.
위와 같은 구성을 디자인 패턴 중 하나인 Facade 패턴이라고 한다.
Facade(건물의 정면) 패턴
말 그대로 건물의 뒷부분이 어떻게 생겼는지는 보여주지 않고 건물의 정면만 보여준다.
예를들어 EmployeeFacade 클래스는 메서드의 구현이 어떻게 되어있는지는 보여주지 않고 어떤 메서드가 있는지만 보여준다.
클래스를 사용하는 입장에선 PayCalculator, HourReporter, EmployeeSaver 클래스가 어떤식으로 구성되어있는지 알필요 없이
EmployeeFacade 클래스를 불러와 메서드만 사용하면 되기 때문에 캡슐화(정보 은닉)도 잘 구성되어 있다
한계점
그러나 실제는 이 원리를 적용해서 직접 클래스를 설계하기 그리 쉽지만은 않다.
왜냐하면 단일 책임 기준은 사람들마다 생각하는 것이 다르고 상황에 따라서도 달라질 수 있기 때문이다.
즉 ‘단일 기능(책임)’이라는 의미가 모호하다.
class EmployeeManagement {
// * Create 작업을 담당하는 CRUD 메소드
void addEmployee(String employee) {
if(Objects.equals(employee, "")) {
postServer(employee); // 서버에 직원 정보를 보냄
logResult("[LOG] EMPLOYEE ADDED"); // 로그 출력
} else {
logResult("[ERROR] NAME MUST NOT BE EMPTY");
}
}
}
서버에 데이터를 보내고, 그와 관련된 로그를 출력하는 흔한 코드 패턴이지만, 사실 이도 SRP 원칙에 위배된 형태이다.
서버에 데이터를 보내는 동작과, 작업 결과를 파일에 기록하는 동작은 서로 관련된 동작이 아니기 때문이다.
SRP 원칙을 따르면 로깅만을 담당하는 클래스를 따로 분리하고, CRUD 클래스에서 이를 합성(주입)해서 사용해야한다.
하지만 이는 불편할 뿐더러, 코드의 가독성이 떨어진다. 이렇게 너무 많은 책임 분할로 인하여 책임이 여러군데로 파편화 되어있는 경우
책임의 범위를 변경해 하나의 책임 단위로 간주함으로써 응집력을 높여주는 작업이 추가로 필요하다(산탄총 수술).
예를 들어 로깅, 보안, 트랜잭션과 같은 시스템 안에 포함되는 부가 기능도 하나의 책임으로 보고 분리하라는 것이다.
결론
실제로 프로그램을 설계 할때 객체에 얼만큼의 책임을 넣어야 단일 책임 원칙을 위배하지 않는지 범위 기준은 개발자 스스로 정해야 한다.
그리고 실무의 프로세스는 매우 복잡 다양하고 변경 또한 빈번하기 때문에 경험이 많지 않거나 업무 이해가 부족하면
자기도 모르게 SRP원리에서 멀어져 버리게 될 수도 있다.
따라서 평소에 많은 연습과 경험이 필요한 원칙이다.
참고 사이트
💠 완벽하게 이해하는 SRP (단일 책임 원칙) (tistory.com)
댓글남기기