소프트웨어에서 좋은 아키텍처를 정의하는 원칙이 바로 SOLID이다.
SOLID의 원칙의 목적은 중간 수준(=모듈)의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.
1. 변경에 유연하다.
2. 이해하기 쉽다.
3. 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
** 모듈 : 독립적인 개발 단위. 라이브러리 또는 API 인터페이스 등. 단순히 함수와 데이터구조로 구성된 집합인 클래스를 의미하기도 함.
SRP: 단일 책임 원칙
SRP (The Single Reponsibility Principle)
단일 책임의 원칙
클래스는 단 하나의 책임만 가지며, 변경의 이유 또한 한가지여야 한다.
코드의 응집도는 높이고, 결합력은 낮추는 가장 기본적인 원칙
위의 설명만으로는 뭔가 부족하다.
어떠한 모듈(=클래스)을 사용하고 변경되기를 원하는 특정 집단을 액터라고 하자.
그러면 위의 설명은 아래와 같이 표현될 수 있다.
"하나의 모듈는 오직 하나의 액터에 대해서만 책임 져야 한다."
그러면 이 말이 의미하는 바를 SRP를 위반하는 징후들을 살펴보면서 판단해보자.
징후 1: 우발적 중복
Employee 클래스는 3가지의 메서드를 가진다. 이 클래스는 SRP를 위반하는데, 세가지의 메서드가 3명의 액터를 책임지고 있기 때문이다.
여기서 CalculatePay()는 CFO 보고를 위해 사용되고, reportHours()는 COO 보고를 위해 사용되며, save()는 CTO 보고를 위해 사용된다고 하자.
CFO 보고를 위해 calculatePay()를 수정하였을 때, 다른 메서드 실행에 있어 영향을 줄 수 있지 않을까?
다음과 같이 CalculatePay()와 reportHours()가 공유하고 있는 regularHours()가 있다.
CFO 쪽에서 시간 계산 방식에 대한 변경요청이 오게 되어서 개발자가 regularHours()가 변경하였다면, 어떤 일이 벌어질것인가?
COO 보고를 위해 사용되던 reportHours()는 요청이 없었는데도 불구하고 변경된 알고리즘에 의해 메서드의 결과값이 변경될 것이다.
이러한 문제를 해결하기 위해서 SRP원칙은 "서로 다른 액터가 의존하는 코드를 서로 분리하라"고 말한다.
징후2. 병합
CFO, COO, CTO 팀에서 각각의 메서드에 대한 변경요청이 들어오게 되면 어떤일이 발생할까?
각각의 팀들에 속한 개발자는 Employee 클래스를 체크아웃 받은 후 변경사항을 적용하기 시작할 것이다.
하지만, 이러한 서로의 변경사항은 충돌할 수 밖에 없고, 결과적으로 병합이 발생하게 된다.
병합에는 항상 위험이 따를 수 밖에 없다.
이러한 문제를 벗어나는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.
해결책
데이터와 메서드를 분리하는 방식
EmployeeData 클래스를 만들어, 세 개의 클래스가 공유하도록 하고, 각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함하게 한다.
세 클래스는 서로의 존재를 몰라야 하며 따라서 "우연한 중복"을 피할 수 있게 된다.
예시 코드. 3가지의 클래스는 서로에 대한 의존성이 없다.
public class EmployeeData {
private long employeeID;
private String employeeName;
private String employeePhoneNumber;
private long employeeRank;
}
public class PayCalculator {
private final EmployeeData employeeData;
public PayCalculator(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public long calculatePay() {
/* Logic */
final int regularHours = regularHours();
return 0;
}
private int regularHours() {
/* Logic */
return 0;
}
}
public class HourReporter {
private final EmployeeData employeeData;
public HourReporter(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public long reportHours() {
/* Logic */
final int regularHours = regularHours();
return 0;
}
private int regularHours() {
/* Logic */
return 0;
}
}
public class EmployeeSaver {
private final EmployeeData employeeData;
public EmployeeSaver(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public void saveEmployee() {
/* Logic */
}
}
하지만 이 원칙은 개발자가 세 가지 클래스를 인스턴스화 하고 추적해서 사용해야 된다는 단점이 있다.
이러한 난관을 해결하기 위해 아래와 같은 퍼사드(Facade) 패턴을 사용하기도 한다.
Employee Facade 클래스에는 코드는 거의 없다. 이 클래스는 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.
Facade 예시코드.
public class EmployeeFacade {
private final PayCalculator payCalculator;
private final HourReporter hourReporter;
private final EmployeeSaver employeeSaver;
public EmployeeFacade(PayCalculator payCalculator, HourReporter hourReporter, EmployeeSaver employeeSaver) {
this.payCalculator = payCalculator;
this.hourReporter = hourReporter;
this.employeeSaver = employeeSaver;
}
public long calculatePay() {
return payCalculator.calculatePay();
}
public long reportHours() {
return hourReporter.reportHours();
}
public void saveEmployee() {
return employeeSaver.saveEmployee();
}
}
public class PayCalculator {
private final EmployeeData employeeData;
public PayCalculator(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public long calculatePay() {
/* Logic */
final int regularHours = regularHours();
return 0;
}
private int regularHours() {
/* Logic */
return 0;
}
}
public class HourReporter {
private final EmployeeData employeeData;
public HourReporter(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public long reportHours() {
/* Logic */
final int regularHours = regularHours();
return 0;
}
private int regularHours() {
/* Logic */
return 0;
}
}
public class EmployeeSaver {
private final EmployeeData employeeData;
public EmployeeSaver(final EmployeeData employeeData) {
this.employeeData = employeeData;
}
public void saveEmployee() {
/* Logic */
}
}
public class EmployeeData {
private long employeeID;
private String employeeName;
private String employeePhoneNumber;
private long employeeRank;
}
또 다른 방법으로는 아래와 같이 Employee 클래스를 그대로 유지하되, 특정 메서드들에 대해 퍼사드를 적용하는 방법이다.
public class Employee {
private final EmployeeData employeeData;
private final HourReporter hourReporter;
private final EmployeeSaver employeeSaver;
public Employee(EmployeeData employeeData, HourReporter hourReporter, EmployeeSaver employeeSaver) {
this.employeeData = employeeData;
this.hourReporter = hourReporter;
this.employeeSaver = employeeSaver;
}
public long calculatePay() {
/* Logic */
return 0;
}
public long reportHours() {
return hourReporter.reportHours();
}
public void saveEmployee() {
return employeeSaver.saveEmployee();
}
}
결론
SRP 단일 책임 원칙은 "메서드"와 "클래스" 수준의 원칙이다.
하지만 이보다 상위 수준인 컴포넌트 수준에서도 공통폐쇄 원칙(Common Closure Principle)으로, 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축(Axis of Change)이 되는 등 다른 형태로 등장한다.
'프로그래밍 > 클린 아키텍처' 카테고리의 다른 글
[클린아키텍처] 3부. 설계원칙 LSP (0) | 2021.03.01 |
---|---|
[클린아키텍처] 3부. 설계원칙 OCP (0) | 2021.03.01 |
[클린아키텍처] 2부. 프로그래밍 패러다임 (0) | 2021.02.24 |
[클린아키텍처] 1부. 행위와 아키텍처 (0) | 2021.02.20 |
[클린아키텍처] 1부. 설계와 아키텍처 (0) | 2021.02.20 |