객체지향 프로그래밍(OOP)은 캡슐화, 상속, 다형성이라는 세 가지 기둥을 중심으로 동작함
하지만 OOP만으로는 좋은 설계를 만들기엔 부족함
그래서 등장한 것이 바로 SOLID 원칙임
이 글에서는 SOLID 원칙 각각의 철학, 예시, 깨지기 쉬운 사례, 리팩토링 예시 등을 포함해 완전 정리해보았음
SOLID는 객체지향 설계의 5가지 핵심 원칙의 앞 글자를 딴 것임
| 약어 | 원칙 이름 | 설명 요약 |
|---|---|---|
| S | SRP (단일 책임 원칙) | 하나의 클래스는 하나의 책임만 가져야 함 |
| O | OCP (개방/폐쇄 원칙) | 확장에는 열려 있고, 수정에는 닫혀 있어야 함 |
| L | LSP (리스코프 치환 원칙) | 하위 클래스는 상위 클래스를 대체할 수 있어야 함 |
| I | ISP (인터페이스 분리 원칙) | 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 됨 |
| D | DIP (의존 역전 원칙) | 추상화에 의존해야 하며, 구체화에 의존하면 안 됨 |
하나의 클래스는 하나의 책임만 가져야 하며, 하나의 변경 이유만 가져야 함
public class Report {
public String createReport() { ... }
public void printReport() { ... } // 출력 책임까지 가짐
public void saveToDatabase() { ... } // 저장 책임까지 가짐
}
문제:
public class Report {
public String createReport() { ... }
}
public class ReportPrinter {
public void print(String content) { ... }
}
public class ReportRepository {
public void save(String content) { ... }
}
장점:
확장에는 열려 있고, 수정에는 닫혀 있어야 한다
즉, 새로운 기능을 추가하더라도 기존 코드를 변경하지 않도록 설계해야 함
public class NotificationService {
public void send(String type, String message) {
if (type.equals("email")) {
// send email
} else if (type.equals("sms")) {
// send sms
}
}
}
문제:
send 메서드를 계속 수정해야 함public interface Notifier {
void send(String message);
}
public class EmailNotifier implements Notifier {
public void send(String message) { ... }
}
public class SMSNotifier implements Notifier {
public void send(String message) { ... }
}
public class NotificationService {
private List<Notifier> notifiers;
public NotificationService(List<Notifier> notifiers) {
this.notifiers = notifiers;
}
public void notifyAll(String message) {
for (Notifier notifier : notifiers) {
notifier.send(message);
}
}
}
장점:
프로그램의 객체는 자식 클래스로 바꿔도 정상적으로 작동해야 함
public class Bird {
public void fly() { ... }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없음");
}
}
문제:
public abstract class Bird {
public abstract void move();
}
public class Sparrow extends Bird {
public void move() {
fly();
}
private void fly() { ... }
}
public class Penguin extends Bird {
public void move() {
swim();
}
private void swim() { ... }
}
장점:
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 말아야 함
public interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
프린터 클래스에서 fax()를 쓸 일이 없음에도 불구하고 구현해야 함
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public interface Fax {
void fax();
}
장점:
고수준 모듈은 저수준 모듈에 의존하지 말고, 둘 다 추상화에 의존해야 함
즉, 구현이 아닌 인터페이스에 의존해야 함
public class MySQLDatabase {
public void save(String data) { ... }
}
public class UserService {
private MySQLDatabase db = new MySQLDatabase();
public void registerUser(String user) {
db.save(user);
}
}
문제:
public interface UserRepository {
void save(String user);
}
public class MySQLDatabase implements UserRepository {
public void save(String user) { ... }
}
public class UserService {
private UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public void registerUser(String user) {
repo.save(user);
}
}
장점:
| 원칙 | 요약 | 핵심 키워드 |
|---|---|---|
| SRP | 하나의 책임만 가지도록 | 응집도, 변경 이유 분리 |
| OCP | 확장에 열려, 변경에는 닫힘 | 유지보수성 |
| LSP | 자식은 부모를 완전히 대체 가능 | 상속의 일관성 |
| ISP | 작고 명확한 인터페이스 사용 | 클라이언트 최적화 |
| DIP | 추상화에 의존 | 느슨한 결합, DI 활용 |
SOLID는 무조건 지켜야 하는 법칙이 아니라, 지향해야 할 설계 철학임
모든 프로젝트에서 완벽하게 지킬 수는 없지만,
이 원칙들을 바탕으로 한 코드는 확장성, 유지보수성, 테스트 용이성이 매우 뛰어남
좋은 설계는 SOLID 원칙을 의식하는 것에서 시작됨
현업에서 자주 마주치는 리팩토링 이슈는 대부분 이 다섯 가지 원칙 중 하나 이상을 위반했을 가능성이 높음
코드를 읽고, 설계하고, 리팩토링할 때 항상 이 다섯 가지 질문을 떠올려보기 바람