SOLID
객체지향 프로그래밍에서 유연하고 유지 보수하기 쉬운 소프트웨어 시스템을 설계하기 위한 5가지 기본 원칙의 약자로 각 원칙은 독립적이지만, 함께 적용될 때 응집도를 높이고 결합도는 낮추는 효과를 극대화한다.
단일 책임 원칙
단일 책임 원칙(Single Responsibility Principle, SRP)은 한 클래스가 단 하나의 책임만 가져야 하며, 클래스를 변경하는 이유는 단 하나여야 한다는 원칙이다.
- 상세 설명: 클래스나 모듈이 여러 책임을 가질 때 발생하는 문제를 해결하며, 변경의 파급 효과를 최소화하고 코드를 이해하기 쉬워진다.
예제 코드는 다음과 같다.
// SRP 위반: 데이터 관리, 인증, DB 저장 세 가지 책임이 한 클래스에 있다.
class User {
private String name;
private String password;
// 1. 데이터 관리 책임
public String getName() { return name; }
public void setName(String name) { this.name = name; }
// 2. 인증 책임
public boolean authenticate(String inputPassword) {
return this.password.equals(inputPassword);
}
// 3. DB 저장 책임
public void saveToDatabase() {
System.out.println("User data saved to DB.");
}
}
// SRP 준수: 책임을 명확히 분리했다.
class User {
// 1. 데이터 관리 책임만 가진다.
private String name;
private String password;
// Getters and Setters...
}
class UserAuthenticator {
// 2. 인증 책임만 가진다.
public boolean authenticate(User user, String inputPassword) {
return user.getPassword().equals(inputPassword);
}
}
class UserRepository {
// 3. DB 접근 책임만 가진다.
public void save(User user) {
System.out.println("User data saved by Repository.");
}
}
개방 폐쇄 원칙
개방 폐쇄 원칙(Open-Closed Principle, OCP)은 소프트웨어 요소(클래스, 모듈 등)는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다는 원칙이다.
- 상세 설명: 새로운 기능을 추가해야 할 때 기존의 안정적인 코드를 수정하는 것은 버그 발생 위험을 높이나, 시스템의 핵심 로직은 추상화에 의존하고, 새로운 기능은 추상화를 구현하여 추가시키면, 기존 코드를 건드리지 않고 새로운 구현체를 추가하여 기능을 확장한다.
// OCP 위반: 새로운 연산이 추가될 때마다 이 클래스의 코드를 수정해야 한다.
class Calculator {
public int calculate(String op, int a, int b) {
if (op.equals("add")) {
return a + b;
} else if (op.equals("sub")) {
return a - b;
}
// 새로운 연산 'mul'을 추가하려면 이 if-else 문을 수정해야 한다.
throw new IllegalArgumentException("Invalid operation.");
}
}
// OCP 준수: 추상화(인터페이스)에 의존한다.
interface Operation { // 확장에는 열려있다.
int apply(int a, int b);
}
class AddOperation implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
class SubOperation implements Operation {
@Override
public int apply(int a, int b) {
return a - b;
}
}
class Calculator {
// Calculator는 Operation 인터페이스에 의존한다.
public int calculate(Operation op, int a, int b) {
return op.apply(a, b);
}
// 새로운 MulOperation을 추가해도 이 클래스는 수정할 필요가 없다.
}
리스코프 치환 원칙
리스코프 치환 원칙(Liskov Substitution Principle, LSP)은 서브 타입(자식/서브 클래스)가 언제나 자신의 슈퍼 타입(부모/슈퍼 클래스)으로 교체될 수 있어야 하며, 프로그램의 정확성을 해치지 않아야 한다는 원칙이다.
- 상세 설명: 객체 지향의 상속 관계를 올바르게 사용하도록 강제하는 원칙으로, 자식 클래스가 부모 클래스의 메소드를 오버라이딩할 때, 부모 클래스가 약속한 기능적 규약을 그대로 지켜야 한다.
// LSP 위반: Square는 Rectangle의 setWidth 행위 규약을 위반한다.
class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = this.height = w; // 높이도 같이 변경
}
@Override
public void setHeight(int h) {
this.width = this.height = h; // 너비도 같이 변경
}
}
// 테스트 코드: 부모 타입으로 자식을 사용한다.
class LspTest {
public static void printArea(Rectangle r) {
r.setWidth(3);
r.setHeight(4);
// LSP가 준수된다면 면적은 3 * 4 = 12여야 하지만,
// Square는 setWidth(3)에서 height도 3으로 만들고, setHeight(4)에서 width도 4로 만들어
// 최종적으로 (4 * 4 = 16)이 되어 부모의 계약(12)을 위반한다.
System.out.println("Area: " + r.getArea());
}
}
// LSP 준수: 각 도형을 독립적으로 설계하거나 인터페이스를 통해 연결한다.
interface Shape {
int getArea();
}
class Rectangle implements Shape {
// ...
@Override
public int getArea() { /*...*/ return width * height; }
}
// Square는 Rectangle의 하위 타입이 아니다.
class Square implements Shape {
private int side;
// ...
@Override
public int getArea() { return side * side; }
}
// 이제 클라이언트 코드는 Shape 인터페이스를 통해 안전하게 객체를 치환할 수 있다.
인터페이스 분리 원칙
인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 클라이언트가 자신이 사용하지 않는 메소드에 의존해서는 안된다는 원칙으로, 범용적인 거대 인터페이스 하나보다는 특정 클라이언트를 위한 작은 인터페이스 여러 개가 낫다는 것을 의미한다.
- 상세 설명: 인터페이스를 역할 별로 분리하여 클라이언트가 자신에게 필요한 메소드만 알도록 만들어야 한다.
// ISP 위반: 너무 많은 책임을 가진 하나의 인터페이스
interface MultiPurposeWorker {
void work();
void eat();
void manage(); // 관리 책임
}
class Robot implements MultiPurposeWorker {
@Override
public void work() { /*...*/ }
@Override
public void eat() {
// 로봇은 먹지 않지만 강제로 구현해야 한다. (불필요한 구현)
}
@Override
public void manage() { /*...*/ }
}
// ISP 준수: 역할을 분리한 작은 인터페이스들로 구성한다.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Manageable {
void manage();
}
class Robot implements Workable, Manageable {
@Override
public void work() { /*...*/ }
@Override
public void manage() { /*...*/ }
// Eatable 인터페이스를 구현할 필요가 없어진다.
}
의존 역전 원칙
의존 역전 원칙(Dependency Inversion Principle, DIP)은 고수준 모듈(High-Level Module)이 저수준 모듈(Low-Level Module)에 의존해서는 안되며, 둘 다 추상화에 의존해야 하고, 세부 사항이 추상화에 의존해야 한다는 원칙이다.
- 상세 설명: 고수준 모듈은 핵심적인 비즈니스 로직을 담당하는 클래스이고, 저수준 모듈은 데이터베이스 접근, 파일 처리 등 구체적인 세부 구현을 담당하는 클래스이다. 고수준 모듈이 구체적인 저수준 클래스에 의존하는 것을 막고, 인터페이스를 통해 연결되도록 한다. 이를 통해 의존 관계가 역전되어, 변경이 잦은 저수준 모듈이 변경되어도 안정적인 고수준 모듈은 영향을 받지 않으며, 이것이 DI(의존성 주입) 프레임워크의 핵심 기반이다.
// DIP 위반: 고수준 모듈(Car)이 구체적인 저수준 모듈(DieselEngine)에 직접 의존한다.
class DieselEngine { // 저수준 모듈 (구현)
public void start() {
System.out.println("Diesel engine started.");
}
}
class Car { // 고수준 모듈 (핵심 로직)
private DieselEngine engine; // 구체적인 구현에 의존한다.
public Car() {
this.engine = new DieselEngine(); // 직접 생성하여 결합도가 높다.
}
public void start() {
engine.start();
}
}
// DIP 준수: 고수준 모듈과 저수준 모듈 모두 추상화(Engine)에 의존한다.
interface Engine { // 추상화
void start();
}
class DieselEngine implements Engine { // 저수준 모듈, 추상화에 의존한다.
@Override
public void start() {
System.out.println("Diesel engine started.");
}
}
class ElectricEngine implements Engine { // 또 다른 저수준 모듈
@Override
public void start() {
System.out.println("Electric engine started silently.");
}
}
class Car { // 고수준 모듈, 추상화에 의존한다.
private Engine engine;
// 외부에서 Engine 구현체를 결정하여 생성자를 통해 의존성을 주입받는다.
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
SOLID 정리
SOLID 핵심 요약 표는 다음과 같다.
| 원칙 | 약자 | 핵심 요약 | 예제 |
|---|---|---|---|
| 단일 책임 원칙 | SRP | 한 클래스는 단 하나의 책임만 가지며, 변경 이유는 오직 하나여야 한다. | 책임을 분리한 별도의 클래스/모듈 생성 |
| 개방 폐쇄 원칙 | OCP | 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. 기존 코드를 수정 없이 기능을 추가한다. | 인터페이스나 추상 클래스를 이용한 다형성 및 상속 |
| 리스코프 치환 원칙 | LSP | 자식 클래스는 부모 클래스의 행위 규약을 위반하지 않고 완전히 대체 가능해야 한다. | 부모 메소드 오버라이딩 시, 부모의 기능적 계약 준수 |
| 인터페이스 분리 원칙 | ISP | 클라이언트가 사용하지 않는 메소드에 의존하지 않도록 작은 역할 별 인터페이스로 분리한다. | 역할별 인터페이스 분리 |
| 의존 역전 원칙 | DIP | 고수준 모듈은 저수준 모듈이 아닌 추상화(인터페이스)에 의존해야 한다. | 인터페이스를 통한 의존성 주입(DI) 활용 |