SOLID 원칙 개요
- SRP(Single Responsibility Principle) - 단일 책임 원칙
- OCP(Open-Closed Principle) - 개방-폐쇄 원칙
- LSP(Liskov Substitution Principle) - 리스코프 치환 원칙
- ISP(Interface Segregation Principle) - 인터페이스 분리 원칙
- DIPDIP (Dependency Inversion Principle) - 의존성 역전 원칙
SRP (단일 책임 원칙)
"클래스는 하나의 책임만 가져야 한다."
한 클래스가 하나의 이유로만 변경되어야 합니다. 여러 개의 책임을 하나의 클래스에서 맡게 된다면 Coupling이 증가하면서 유지보수성이 떨어지는 등 여러가지 문제가 발생할 수 있기 때문입니다.
예제
class Student implements Comparable {
private String name;
private String ssn;
@Override
public int compareTo(Object o) {
// 학생을 특정 기준(이름 또는 SSN)으로 정렬하는 로직
}
}
Student 클래스는 학생 정보를 저장하는 역할뿐만 아니라 정렬 기능까지 담당하고 있습니다. 만약 정렬 기준(이름, SSN 등)이 변경되면 Student 클래스를 직접 수정해야 하며, 이는 학생 정보 자체와는 무관한 변경이 클래스에 영향을 주는 문제가 발생합니다.
이를 해결하려면 정렬 책임을 Student 클래스에서 분리해야 합니다.
class Student {
private String name;
private String ssn;
public String getName() { return name; }
public String getSSN() { return ssn; }
}
class SortStudentByName implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getName().compareTo(s2.getName());
}
}
class SortStudentBySSN implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getSSN().compareTo(s2.getSSN());
}
}
Collections.sort(group, new SortStudentByName());
Collections.sort(group, new SortStudentBySSN());
Student 클래스는 학생 데이터만 관리하며, 정렬 기능이 제거되었습니다. 정렬 방식이 변경될 경우, SortStudentByName 또는 SortStudentBySSN을 수정하면 되므로 학생 클래스가 불필요한 변경의 영향을 받지 않습니다. 새로운 정렬 방식이 필요할 때(예: 성적 순 정렬), 새로운 Comparator 클래스를 만들기만 하면 되므로 확장성이 높아집니다.
OCP (개방-폐쇄 원칙)
"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다."
기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 합니다. 즉, 새로운 요구사항이 추가될 때 기존 코드가 변경되지 않고 확장 가능해야합니다.
예제
void incAll(Employee[] emps) {
for (int i = 0; i < emps.length; i++) {
if (emps[i].empType == FACULTY)
incFacultySalary((Faculty)emps[i]);
else if (emps[i].empType == STAFF)
incStaffSalary((Staff)emps[i]);
else if (emps[i].empType == SECRETARY)
incSecretarySalary((Secretary)emps[i]);
}
}
위 메서드는 직원의 급여를 증가시키는 코드입니다. 새로운 직원 유형(예: Engineer)이 추가될 때마다 if-else 문을 추가해야 합니다. 즉, 새로운 요구사항이 추가될 때 기존 코드(incAll 메서드)를 수정해야 한다는 점에서 OCP를 위반합니다. 이는 직원 유형이 많아질수록 if-else 문이 계속 늘어나 유지보수가 어려워지게 되는 문제점이 있습니다.
이를 해결하기 위해 직원별 급여 증가 로직을 별도의 클래스에서 처리하도록 설계합니다.
abstract class Employee {
abstract void incSalary();
}
class Faculty extends Employee {
void incSalary() {
// 교수의 급여 인상 로직
}
}
class Staff extends Employee {
void incSalary() {
// 직원의 급여 인상 로직
}
}
class Secretary extends Employee {
void incSalary() {
// 비서의 급여 인상 로직
}
}
void incAll(Employee[] emps) {
for (Employee emp : emps) {
emp.incSalary(); // 다형성을 활용하여 호출
}
}
위와 같이 수정하면 새로운 직원 유형(예: Engineer)이 추가될 때 기존 incAll() 메서드를 수정할 필요가 없고, 새로운 Emplyee 클래스를 만들고 incSalary() 메서드만 정의하면 됩니다. 이로써 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
LSP (리스코프 치환 원칙)
"서브타입(자식 클래스)은 언제나 기반 타입(부모 클래스)으로 대체될 수 있어야 한다."
LSP는 부모 클래스를 사용하는 모든 곳에서 자식 클래스를 대체할 수 있어야 한다는 원칙입니다. 즉, 자식 클래스가 부모 클래스의 동작 방식을 따라야 합니다.
예제
class Rectangle {
private int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 정사각형이므로 높이도 동일하게 설정
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height); // 정사각형이므로 너비도 동일하게 설정
}
}
Square(정사각형)는 Rectangle(직사각형)을 상속받고 있지만, setWidth()를 호출하면 높이도 변경되고 setHeight()를 호출하면 너비도 변경됩니다. 즉, Rectangle의 원래 동작과 다른 방식으로 동작하므로, 부모 클래스를 대체할 수 없게 됩니다. Rectangle을 기대하고 Square를 사용하면, setWidth(10)을 호출한 후 getHeight()를 호출했을 때, 예상과 다르게 기존 값이 아닌 10이 반환됩니다. (원래 Rectangle에서는 너비만 변경되어야 합니다.)
이를 해결하기 위해 Square와 Rectangle를 별도의 클래스로 분리합니다.
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
class Square implements Shape {
private int side;
public void setSide(int side) { this.side = side; }
public int getArea() { return side * side; }
}
이제 Rectangle과 Square는 서로 독립적인 클래스로 동작하며, 부모-자식 관계를 형성하지 않으므로 LSP를 위반하지 않게 됩니다.
ISP (인터페이스 분리 원칙)
"클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다."
즉, 인터페이스가 너무 많은 기능을 포함하면 클라이언트가 필요하지 않은 기능까지 구현해야 하므로 역할별로 분리해야 합니다.
예제
interface StudentEnrollment {
String getName();
String getSSN();
String getInvoice();
void postPayment();
}
RoasterApplication클래스는 getName()과 getSSN()을 invoke하고 AccountApplication는 getInvoice()와 postPayment()를 invoke한다고 가정합시다. RoasterApplication 클래스는 getName()과 getSSN()만 사용하지만, 불필요하게 getInvoice()와 postPayment() 메서드를 포함해야 합니다. AccountApplication 클래스는 getInvoice()와 postPayment()만 사용하지만, 불필요하게 getName()과 getSSN()을 포함해야 합니다. 즉, 각 클라이언트는 자신과 관계없는 메서드에 의존하게 되어 ISP를 위반합니다.
이를 해결하기 위해 인터페이스를 역할별로 분리합니다.
interface StudentInfo {
String getName();
String getSSN();
}
interface FinancialInfo {
String getInvoice();
void postPayment();
}
class RoasterApplication implements StudentInfo {
@Override
public String getName() { return "John Doe"; }
@Override
public String getSSN() { return "123-45-6789"; }
}
class AccountApplication implements FinancialInfo {
@Override
public String getInvoice() { return "Invoice #123"; }
@Override
public void postPayment() { System.out.println("Payment posted."); }
}
RoasterApplication은 필요한 정보(StudentInfo)만 구현, AccountApplication은 금융 관련 기능(FinancialInfo)만 구현하도록 분리했습니다. 인터페이스가 역할별로 분리되어, 유지보수성과 확장성이 향상됩니다.
DIP (의존성 역전 원칙)
"고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다."
즉, 세부적인 구현이 아닌 추상화된 인터페이스를 통해 의존성을 관리해야 합니다.
예제
class PolicyService {
void applyPolicy() { System.out.println("Applying Policy"); }
}
class Client {
private PolicyService policyService = new PolicyService();
void execute() {
policyService.applyPolicy();
}
}
Client 클래스가 PolicyService의 구체적인 구현에 직접 의존하고 있습니다. 만약 정책을 변경해야 한다면, Client 클래스를 직접 수정해야 하는 문제가 발생합니다.
이를 해결하기 위해 인터페이스를 도입하여 고수준 모듈이 저수준 모듈에 직접 의존하지 않도록 변경합니다.
interface Policy {
void applyPolicy();
}
class PolicyService implements Policy {
@Override
public void applyPolicy() { System.out.println("Applying Policy"); }
}
class Client {
private Policy policy;
Client(Policy policy) { this.policy = policy; }
void execute() { policy.applyPolicy(); }
}
이러면 새로운 정책을 추가할 때 Client 클래스를 수정할 필요가 없고 테스트 시 MockPolicy 등을 사용하여 의존성을 쉽게 주입할 수 있습니다.
RoasterApplication은 필요한 정보(StudentInfo)만 구현, AccountApplication은 금융 관련 기능(FinancialInfo)만 구현하도록 분리했습니다. 인터페이스가 역할별로 분리되어, 유지보수성과 확장성이 향상됩니다.
'CS' 카테고리의 다른 글
SQL: DDL과 DML (0) | 2025.04.13 |
---|---|
웹 서버(Web Server)와 WAS(Web Application Server) (0) | 2025.03.23 |
Framework와 Library의 차이점 (0) | 2025.03.16 |
Java HashSet의 내부 동작 방식과 중복 제거 메커니즘 (0) | 2025.03.09 |
Stream API: map vs flatMap (0) | 2025.03.03 |