“좋은 객체지향 설계의 핵심은 SOLID에 있다.”
프로그래밍을 처음 배울 때는 “코드를 돌아가게 하는 것”이 목표였다. 하지만 개발 경험이 쌓이고 협업 프로젝트를 하다 보면,
“유지보수하기 좋은 코드”의 중요성을 절실히 느끼게 된다. 그 기준점이 바로 SOLID 원칙이다.
SOLID는 단순히 이론적인 개념이 아니라, “확장 가능한 코드”를 만들기 위한 객체지향 설계의 핵심 철학이다.
그래서 오늘은 SOLID의 다섯 가지 원칙을 ✅ 개념적 배경 → ✅ 코드 예제 → ✅ 리팩토링 실전 적용 순으로 정리해본다.

🧠 SOLID란?
SOLID는 객체지향 설계에서 유연성(Flexibility), 확장성(Extensibility), **유지보수성(Maintainability)**을 높이기 위한 5가지 원칙의 집합이다.
약어 원칙명 핵심 목표
| S | Single Responsibility Principle | 단일 책임 원칙 |
| O | Open/Closed Principle | 개방-폐쇄 원칙 |
| L | Liskov Substitution Principle | 리스코프 치환 원칙 |
| I | Interface Segregation Principle | 인터페이스 분리 원칙 |
| D | Dependency Inversion Principle | 의존 역전 원칙 |
🔹 1. SRP (Single Responsibility Principle — 단일 책임 원칙)
“하나의 클래스는 오직 하나의 이유로만 변경되어야 한다.”
이 원칙은 **응집도(cohesion)**를 높이고 **결합도(coupling)**를 낮추는 데 목적이 있다.
즉, 클래스가 여러 역할을 동시에 하면 그만큼 수정 시 리스크가 커진다.
🧩 개념적 이해
- 클래스가 2개 이상의 책임을 가지면,
하나의 변경이 다른 기능에 의도치 않은 부작용을 일으킬 수 있다. - 유지보수 시에도 “이 부분을 바꾸면 어디까지 영향이 갈까?”라는 걱정이 생긴다.
- 따라서 각 클래스는 하나의 역할만 수행해야 하며,
각 책임은 별도의 객체로 분리되어야 한다.
❌ 위반 예시
class UserService {
public void register(String user) {
saveToDatabase(user);
sendWelcomeEmail(user);
}
private void saveToDatabase(String user) { ... }
private void sendWelcomeEmail(String user) { ... }
}
이 클래스는 데이터 저장과 이메일 발송이라는 두 가지 책임을 동시에 가진다.
✅ 개선된 설계
class UserRepository {
void save(String user) { ... }
}
class EmailService {
void sendWelcomeEmail(String user) { ... }
}
class UserService {
private final UserRepository repo;
private final EmailService mailer;
void register(String user) {
repo.save(user);
mailer.sendWelcomeEmail(user);
}
}
→ 이제 UserService는 “회원 가입 관리”라는 단일 책임만 가진다.

🔹 2. OCP (Open/Closed Principle — 개방-폐쇄 원칙)
“소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.”
🧩 개념적 이해
- 기능을 확장할 때는 기존 코드를 건드리지 않아야 한다.
- 이는 인터페이스 기반 설계를 통해 가능하다.
- 새 기능을 추가할 때, 기존 클래스의 내부 로직을 수정하지 않고
**다형성(polymorphism)**으로 대체하는 구조를 만들면 된다.
✅ 예시
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
public void send(String message) { System.out.println("Email: " + message); }
}
class SlackNotifier implements Notifier {
public void send(String message) { System.out.println("Slack: " + message); }
}
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) { this.notifier = notifier; }
void alert(String msg) { notifier.send(msg); }
}
→ 새로운 알림 방식(SMSNotifier 등)을 추가해도 AlertService 수정이 필요 없다.

🔹 3. LSP (Liskov Substitution Principle — 리스코프 치환 원칙)
“자식 클래스는 언제나 부모 클래스로 대체될 수 있어야 한다.”
🧩 개념적 이해
- LSP는 상속의 올바른 사용을 다룬다.
- 자식 클래스가 부모 클래스의 규약을 위반하면,
상속은 오히려 버그의 원인이 된다. - 즉, **‘IS-A’ 관계가 성립하는가?**를 항상 고민해야 한다.
❌ 위반 예시
class Bird {
void fly() { System.out.println("날 수 있음"); }
}
class Penguin extends Bird {
@Override
void fly() { throw new UnsupportedOperationException("펭귄은 못 날아요"); }
}
→ Penguin은 Bird처럼 행동하지 않으므로 LSP 위반.
✅ 개선된 설계
interface Bird { }
interface FlyableBird extends Bird { void fly(); }
class Sparrow implements FlyableBird { public void fly() { System.out.println("참새 날다"); } }
class Penguin implements Bird { }
→ “모든 새가 날 수 있다”는 가정 자체를 분리하여 설계.

🔹 4. ISP (Interface Segregation Principle — 인터페이스 분리 원칙)
“클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.”
🧩 개념적 이해
- 하나의 인터페이스가 너무 많은 기능을 담으면,
특정 구현체는 불필요한 의존성을 갖게 된다. - 이로 인해 “빈 메서드” 구현이나 “불필요한 의존 주입”이 발생한다.
- 따라서 작고 명확한 인터페이스 여러 개로 나누는 것이 바람직하다.
✅ 예시
interface Workable { void work(); }
interface Eatable { void eat(); }
class Human implements Workable, Eatable { ... }
class Robot implements Workable { ... }
→ 각 객체가 자신에게 필요한 인터페이스만 구현.

🔹 5. DIP (Dependency Inversion Principle — 의존 역전 원칙)
“고수준 모듈은 저수준 모듈에 의존해서는 안 되며,
둘 다 추상화에 의존해야 한다.”
🧩 개념적 이해
- DIP는 **의존성 주입(Dependency Injection)**과 밀접한 관계가 있다.
- 직접 객체를 생성하는 대신,
추상 인터페이스를 통해 외부로부터 주입받는 방식으로
코드의 결합도를 낮추는 것이 핵심이다.
✅ 예시
interface Switchable { void turnOn(); }
class Lamp implements Switchable {
public void turnOn() { System.out.println("불이 켜졌습니다."); }
}
class Button {
private Switchable device;
public Button(Switchable device) { this.device = device; }
void press() { device.turnOn(); }
}
→ Button은 이제 Lamp가 아닌 Switchable이라는 추상화에 의존한다.

🧱 SOLID 위반 사례 리팩토링 일기
🧩 1. 문제 상황
class OrderManager {
public void createOrder(String product) {
saveToDB(product);
sendNotification(product);
}
private void saveToDB(String product) { ... }
private void sendNotification(String product) { ... }
}
→ SRP 위반, OCP 위반, DIP 위반까지 종합세트였다.

🔧 2. 리팩토링 후 — SRP, OCP, DIP 적용
class OrderService {
private final Database db;
private final Notifier notifier;
public OrderService(Database db, Notifier notifier) {
this.db = db;
this.notifier = notifier;
}
public void createOrder(String product) {
db.save(product);
notifier.send(product);
}
}
interface Database { void save(String product); }
interface Notifier { void send(String product); }
class MongoDatabase implements Database {
public void save(String product) { System.out.println("MongoDB 저장 완료"); }
}
class EmailNotifier implements Notifier {
public void send(String product) { System.out.println("이메일 발송 완료"); }
}
→ 역할이 명확히 분리되고, 의존성도 추상화되었다.
→ 이제 OrderService는 데이터 저장 방식이나 알림 방식에 전혀 의존하지 않는다.
→ 변경은 닫혀 있고, 확장은 열려 있는 구조로 진화했다.

🧬 3. LSP와 ISP 적용
LSP 위반을 피하기 위해, 상속 관계보다는 행동을 기준으로 인터페이스 분리했다.
ISP 원칙을 반영해 각 기능을 인터페이스로 나누고, 필요한 기능만 구현하도록 변경했다.

4. 리팩토링 후 느낀 점
“SOLID는 복잡한 걸 더 복잡하게 만드는 게 아니라, 복잡함을 질서 있게 정리하는 원칙이다.”
- SRP로 클래스 간 의존이 명확해졌다.
- OCP와 DIP로 새 기능을 추가해도 수정이 거의 없다.
- LSP, ISP로 상속 구조가 더 유연해졌다.
💬 회고
✔ 오늘의 성취:
한 클래스에 모든 걸 몰아넣던 구조를 객체지향적으로 재설계했다.
유지보수가 쉬운 코드의 가치를 실감했다.
✔ 개선할 점:
다음엔 Spring의 DI 컨테이너를 이용해 SOLID 원칙을 프레임워크 차원에서 실천해볼 예정이다.
✔ 나만의 팁:
리팩토링 전후 구조를 UML로 시각화하면,
원칙 적용의 효과를 직관적으로 파악할 수 있다.
✨ 마무리하며
처음엔 SOLID가 어렵게 느껴졌지만, 실제 리팩토링에 적용하니 그 힘을 체감했다. "유지보수성”은 결국 설계의 품질에서 나온다는 걸 깨달았다.