SOLID 원칙
SOLID
SOLID 원칙 정리
- SOLID는 객체지향 설계의 5가지 핵심 원칙을 의미하며, 유지보수성과 확장성을 고려한 유연한 소프트웨어 설계를 가능하게 한다. 이 원칙은 로버트 C. 마틴(Robert C. Martin, 일명 ‘Uncle Bob’)이 제안했다.
단일 책임 원칙 (Single Responsibility Principle, SRP)
- 하나의 클래스는 하나의 책임만 가져야 하며, 클래스는 오직 하나의 변경 이유만 가져야 한다.
- 클래스가 여러 책임을 가지면, 하나의 변경이 다른 기능에 예기치 않은 영향을 줄 수 있다. 이는 응집도가 낮고, 유지보수가 어려운 코드로 이어진다.
// 책임이 뒤섞인 경우
class ReportManager {
public void generateReport() { ... }
public void saveToFile() { ... }
public void printReport() { ... }
}
// 책임 분리
class ReportGenerator {
public void generate() { ... }
}
class FileSaver {
public void save() { ... }
}
class ReportPrinter {
public void print() { ... }
}
위반 사례
- UserService가 사용자 등록, 로그인, 로그 기록, 이메일 발송까지 모두 수행한다면 단일 책임 원칙을 위반한 것이다.
핵심 요점
- “책임”이란 변경의 이유이다.
- 클래스는 하나의 책임만 가지도록 설계한다.
개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- 소프트웨어 구성 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
- 기능 추가 시 기존 코드를 수정하게 되면, 새로운 버그가 발생할 위험이 있다. 따라서 확장은 가능하지만 기존 코드를 건드리지 않도록 설계해야 한다.
interface DiscountPolicy {
int calculateDiscount(int amount);
}
class RateDiscountPolicy implements DiscountPolicy {
public int calculateDiscount(int amount) {
return amount * 10 / 100;
}
}
class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public int calculateFinalPrice(int amount) {
return amount - discountPolicy.calculateDiscount(amount);
}
}
위반 사례
- OrderService 내부에 할인 정책에 따라 조건문(if/else)을 사용하고, 정책이 바뀔 때마다 해당 로직을 수정하는 경우.
핵심 요점
- 인터페이스나 추상 클래스를 활용해 변경 없이 확장할 수 있도록 한다.
- 다형성과 의존성 주입을 적극 활용한다.
리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 자식 클래스는 언제나 자신의 부모 클래스가 사용되는 곳에서 문제없이 사용될 수 있어야 한다.
- 상속은 “is-a” 관계에 기반하여 사용되지만, 기능이나 의미가 다를 경우 잘못된 상속이 될 수 있다. 이는 프로그램의 안정성을 해칠 수 있다.
class Bird {
public void fly() { ... }
}
class Sparrow extends Bird {
public void fly() { ... }
}
위반 사례
class Penguin extends Bird {
public void fly() {
throw new UnsupportedOperationException();
}
}
- 펭귄은 날 수 없는 새이므로, Bird를 상속받는 것은 논리적으로 맞지 않는다. 이는 LSP를 위반한 사례이다.
핵심 요점
-자식 클래스는 부모 클래스의 기능을 그대로 사용할 수 있어야 하며, 오작동을 유발해서는 안 된다.
- 상속 시에는 행동의 일관성과 의미를 고려해야 한다.
인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
- 너무 많은 기능을 가진 인터페이스는 구현체에 불필요한 부담을 준다. 필요한 기능만 갖춘 인터페이스를 제공함으로써 유연성을 높일 수 있다.
interface Printer {
void print();
}
interface Scanner {
void scan();
}
class MultiFunctionPrinter implements Printer, Scanner {
public void print() { ... }
public void scan() { ... }
}
class BasicPrinter implements Printer {
public void print() { ... }
}
위반 사례
interface Machine {
void print();
void scan();
}
class OldPrinter implements Machine {
public void print() { ... }
public void scan() {
throw new UnsupportedOperationException();
}
}
- OldPrinter는 스캔 기능을 지원하지 않는데도 scan 메서드를 강제로 구현해야 한다.
핵심 요점
- 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스를 제공한다.
- 구현체가 필요하지 않은 기능에 의존하지 않도록 한다.
의존 역전 원칙 (Dependency Inversion Principle, DIP)
- 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다. 추상화는 구체적인 구현에 의존하지 않아야 한다.
- 구현체에 직접 의존하면 재사용성과 테스트가 어렵다. 대신 추상화에 의존하고, 구현체는 런타임에 주입되도록 구성해야 한다.
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
public void send(String message) { ... }
}
class NotificationService {
private final MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender;
}
public void notify(String message) {
sender.send(message);
}
}
위반 사례
class NotificationService {
private final EmailSender sender = new EmailSender();
public void notify(String message) {
sender.send(message);
}
}
- NotificationService가 EmailSender에 직접 의존하면, 다른 메시지 전송 방식으로 전환하기 어렵다.
핵심 요점
- 고수준 모듈은 인터페이스(추상화)에 의존해야 한다.
- 의존성 주입(DI)을 통해 구현체는 외부에서 주입되도록 한다.
전체 요약
| 원칙 | 이름 | 정의 |
|---|---|---|
| SRP | 단일 책임 원칙 | 클래스는 하나의 책임만 가져야 한다 |
| OCP | 개방-폐쇄 원칙 | 확장에 열려 있고, 변경에 닫혀 있어야 한다 |
| LSP | 리스코프 치환 원칙 | 자식 클래스는 부모를 대체할 수 있어야 한다 |
| ISP | 인터페이스 분리 원칙 | 클라이언트는 사용하지 않는 기능에 의존하면 안 된다 |
| DIP | 의존 역전 원칙 | 추상화에 의존해야 하며, 구현체에 의존하면 안 된다 |