SOLID
아래 5개의 객체 지향 프로그래밍의 원칙을 줄여 SOLID 원칙이라 부른다.
- Single Reponsibility Principle
- Open Closed Principle
- Liskov Substituion Principle
- Interface Segregation Principle
- Dependency Inversion Principle
해당 포스트는 SOLID 중 SRP를 다룬다.
SRP(Single-Responsibility Principle, 단일 책임 원칙)
모든 클래스는 하나의 책임만을 가진다.
SRP는 클래스가 하나의 역할만 해야 하므로 변경해야 할 이유도 하나만 있어야 한다. 좀 더 기술적으로 설명하자면, 소프트웨어 사양에서 데이터베이스 로직, 로깅 로직 등의 변경 사항만 클래스의 사양에 영향을 미칠 수 있어야 한다.
즉, Book 클래스나 Student 클래스와 같이 클래스가 데이터 컨테이너이고 해당 엔티티와 관련된 일부 필드가 있는 경우 데이터 모델을 변경할 때만 변경해야 한다.
단일 책임 원칙이 중요한 이유는 다음과 같다:
- 여러 팀이 같은 프로젝트에서 작업하고 같은 클래스를 편집할 수 있기에 호환되지 않는 모듈이 발생할 수 있다.
- 버전 관리가 쉬워진다. 데이터베이스 작업을 처리하는 persistence 클래스가 존재하는데, 해당 클래스에 대해 깃허브 커밋에서 변경 사항이 있다고 가정해보자. SRP를 따라가면 해당 파일이 스토리지 또는 데이터 베이스와 관련된 것임을 알 수 있다.
- 여러 팀이 같은 파일을 변경하게 된다면, 병합 충돌도 일어날 수 있다. 하지만 SRP를 따른다면 파일에 변경 사유가 "하나만" 있으므로 충돌이 더 적게 일어나고, 충돌이 발생하더라도 해결하기가 더 쉬워진다.
코드 예시 1
코드 예시 2
이 코드들은 단일 책임 원칙을 위반하는 실수의 예시이다. 그리고 이를 수정하는 방법에 대해 살펴본다. 이 프로그램은 서점의 간단한 인보이스 프로그램이며 지금은 서점에서 책만 판매하고 다른 것은 판매하지 않는다고 가정한다.
이 Book 클래스는 필드 몇 개를 가지고 있으며, 특별한 건 없고 로직을 위주로 보기 위해 Getter, Setter는 제외하였다.
class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
Invoice 클래스는 인보이스를 생성하고, 총 가격을 계산하는 로직을 포함하는 클래스이다.
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
그러나 위 Invoice 클래스 디자인에는 문제점이 존재한다.
- 첫 번째는 출력 로직을 포함하는 printInvoice 메서드에 문제가 존재한다. SRP에 따르면 클래스 변경 사유는 단 한 가지여야 한다. 하지만 출력을 수정하기 위해 printInvoice 메서드를 수정하려면 Invoice 클래스 자체를 수정해야 한다. 그렇기에 출력 로직과 비즈니스 로직을 혼합하는 것은 하면 안된다.
- 두 번째 문제는 saveToFile 메서드에 있다. 이 또한 퍼시스턴스 로직과 비즈니스 로직이 혼합되어 생긴 문제다. 단순 파일에 대한 wirting 뿐만 아니라, 데이터베이스에 저장하거나 API 호출을 하거나 기다 persistence와 관련된 것일 수도 있다.
위 문제점은 Printing 관련 클래스와 Persistence 관련 클래스를 새로 작성하면 더 이상 가격 만을 계산하는 Invoice 클래스를 수정할 필요가 없다.
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void print() {
System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
System.out.println("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// 지정된 이름으로 파일을 생성하고 인보이스를 작성
}
}
이제 클래스 구조는 단일 책임 원칙을 따르며, 모든 클래스는 어플리케이션의 한 가지 측면만을 담당한다. Invoice 클래스는 해당 기존의 printInvoce, saveToFile 메서드만 삭제한 채로 그대로 두면 된다.
이러한 이유 때문에 단일 책임 원칙에서는 하나의 클래스는 하나의 일 또는 함수를 가지는 것이 중요하다고 주장한다. 이를 위해 모듈화와 코드를 가능한 작게 유지해 유지 보수력을 올리는 것이 중요하다.
참고 자료
- https://github.com/Unity-Technologies/game-programming-patterns-demo
- https://www.youtube.com/watch?v=jafa3cqoAVM&ab_channel=%EB%93%9C%EB%A6%BC%EC%BD%94%EB%94%A9
- https://www.youtube.com/watch?v=QDldZWvNK_E&ab_channel=InfallibleCode
- https://feel5ny.github.io/2017/12/09/Refactoring/
- https://www.nextree.co.kr/p6960/
- https://refactoring.guru/ko/push-down-method
- https://refactoring.guru/ko/push-down-field
- https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/