SOLID 원칙 (SOLID Principles)
한눈에 보기
객체지향 설계의 다섯 원칙인 SRP, OCP, LSP, ISP, DIP를 위반 코드와 개선 코드로 각각 설명한다. 다섯 원칙이 서로 어떻게 연결되는지와 위반 신호를 표로 정리한다.
최종 업데이트 2026.09.17
SOLID는 로버트 마틴(Robert C. Martin)이 정리한 객체지향 설계의 다섯 가지 원칙, 즉 단일 책임 원칙(SRP), 개방 폐쇄 원칙(OCP), 리스코프 치환 원칙(LSP), 인터페이스 분리 원칙(ISP), 의존관계 역전 원칙(DIP)의 머리글자를 딴 것이다. SOLID 원칙을 따르면 변경에 강하고 이해하기 쉬우며 재사용 가능한 코드를 만들 수 있어, 대부분의 디자인 패턴이 이 원칙들 위에 서 있다.
단일 책임 원칙(Single Responsibility Principle)
하나의 클래스는 하나의 책임만 가져야 하며, 클래스를 변경할 이유는 단 하나여야 한다는 원칙이다. 여기서 책임은 "변경을 요구하는 액터(actor)"를 기준으로 판단한다. 예를 들어 주문 처리, 영수증 출력, 데이터베이스 저장을 한 클래스가 모두 맡으면, 회계팀의 요구와 인프라팀의 요구가 같은 클래스를 서로 다른 이유로 수정하게 되어 충돌한다.
// 위반: 세 가지 변경 이유가 한 클래스에 있다
public class OrderManager {
public void processOrder() { /* 주문 로직 */ }
public void printReceipt() { /* 출력 형식 */ }
public void saveToDb() { /* 저장 기술 */ }
}
// 준수: 책임별로 분리
public class OrderProcessor { public void process() { } }
public class ReceiptPrinter { public void print() { } }
public class OrderRepository { public void save() { } }
개방 폐쇄 원칙(Open-Closed Principle)
소프트웨어 요소는 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙이다. 새 기능이 추가될 때 기존 코드를 고치는 것이 아니라 새 코드를 추가하는 것으로 대응할 수 있어야 한다는 뜻이며, 추상화와 다형성이 그 수단이다.
// 위반: 결제 수단이 늘 때마다 이 메서드를 수정해야 한다
public int fee(String method) {
if (method.equals("CARD")) return 300;
else if (method.equals("BANK")) return 100;
return 0;
}
// 준수: 인터페이스를 확장 지점으로 삼는다
public interface PaymentMethod {
int fee();
}
public class CardPayment implements PaymentMethod {
public int fee() { return 300; }
}
// 새 결제 수단은 클래스 추가만으로 지원된다
전략 패턴, 팩토리 메서드 패턴, 옵저버 패턴은 모두 OCP를 실현하는 대표적인 구조다.
리스코프 치환 원칙(Liskov Substitution Principle)
하위 타입의 객체는 상위 타입의 객체를 대체해도 프로그램의 정확성이 깨지지 않아야 한다는 원칙이다. 상속은 문법적으로 가능하다고 옳은 것이 아니라, 하위 클래스가 상위 클래스의 계약(사전 조건, 사후 조건, 불변식)을 지킬 때만 옳다. 고전적 반례가 직사각형과 정사각형이다.
public class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
// 위반: 정사각형은 너비와 높이를 독립적으로 바꿀 수 없다
public class Square extends Rectangle {
@Override
public void setWidth(int w) { this.width = w; this.height = w; }
@Override
public void setHeight(int h) { this.width = h; this.height = h; }
}
// 클라이언트의 기대가 깨진다
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
// r.area()가 20이길 기대하지만 16이 나온다
하위 클래스에서 예외를 던지며 기능을 막거나, 오버라이드로 동작 의미를 바꾸는 것이 전형적인 위반 신호다. 이런 경우 상속 대신 합성이나 인터페이스 재설계를 검토해야 한다.
인터페이스 분리 원칙(Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다는 원칙이다. 거대한 범용 인터페이스 하나보다, 역할별로 쪼갠 작은 인터페이스 여러 개가 낫다.
// 위반: 단순 프린터도 fax를 구현해야 한다
public interface AllInOneMachine {
void print();
void scan();
void fax();
}
// 준수: 역할별로 분리하고 필요한 것만 구현한다
public interface Printer { void print(); }
public interface Scanner { void scan(); }
public class SimplePrinter implements Printer {
public void print() { }
}
public class MultiFunctionMachine implements Printer, Scanner {
public void print() { }
public void scan() { }
}
구현체가 빈 메서드나 UnsupportedOperationException을 갖게 된다면 인터페이스가 너무 비대하다는 신호다.
의존관계 역전 원칙(Dependency Inversion Principle)
고수준 모듈(정책, 비즈니스 로직)은 저수준 모듈(세부 구현)에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙이다. "역전"이라는 이름은 전통적 구조에서 정책이 구현을 향하던 의존 방향을, 구현이 추상(정책이 정의한 인터페이스)을 향하도록 뒤집는다는 의미다.
// 위반: 고수준 서비스가 구체 클래스에 직접 의존
public class ReportService {
private final MySqlReportDao dao = new MySqlReportDao();
}
// 준수: 추상에 의존하고 구현은 외부에서 주입받는다
public interface ReportDao {
List<Report> findAll();
}
public class ReportService {
private final ReportDao dao;
public ReportService(ReportDao dao) { // 의존성 주입
this.dao = dao;
}
}
의존성 주입(Dependency Injection)은 DIP를 실현하는 대표 기법이며, Spring 컨테이너가 하는 일이 바로 이것이다. DIP 덕분에 저장 기술을 교체하거나 테스트에서 가짜 구현을 넣는 일이 쉬워진다.
다섯 원칙의 관계 요약
| 원칙 | 한 줄 요약 | 위반 신호 |
|---|---|---|
| SRP | 변경 이유는 하나여야 한다 | 서로 무관한 요구로 같은 클래스를 수정한다 |
| OCP | 수정 없이 확장한다 | 기능 추가 때마다 분기문이 늘어난다 |
| LSP | 하위 타입은 계약을 지킨다 | 오버라이드가 예외를 던지거나 의미를 바꾼다 |
| ISP | 인터페이스는 작게 나눈다 | 구현체에 빈 메서드가 생긴다 |
| DIP | 추상에 의존한다 | 비즈니스 로직 안에 new 구체클래스가 있다 |
다섯 원칙은 독립적이지 않다. OCP를 지키려면 추상화가 필요하고 그 추상화에 의존하는 것이 DIP이며, 그 추상화가 하위 타입으로 안전하게 치환되려면 LSP가 지켜져야 한다. SRP와 ISP는 각각 클래스와 인터페이스의 크기를 적절히 유지해 나머지 원칙이 작동할 토대를 만든다.
면접에서 이렇게 물어본다
- SOLID 다섯 원칙을 각각 한 문장으로 설명하고, 실제 코드에서 위반 사례를 하나씩 들어 보라.
- 개방 폐쇄 원칙을 지키기 위해 사용하는 언어 기능과 디자인 패턴은 무엇인가.
- 정사각형과 직사각형 상속 문제가 리스코프 치환 원칙을 위반하는 이유를 설명해 보라.
- 의존관계 역전 원칙에서 "역전"의 의미는 무엇이며, 의존성 주입과는 어떤 관계인가.
- SOLID를 지나치게 적용했을 때 생길 수 있는 부작용은 무엇이라고 생각하는가.
디자인 패턴에서 같이 읽기
- 어댑터 패턴 (Adapter Pattern)호환되지 않는 인터페이스를 클라이언트가 기대하는 형태로 변환해 함께 동작하게 만드는 구조 패턴이다. 타깃, 어댑티, 어댑터, 클라이언트의 협력 구조와 장단점, 유사 패턴과의 차이를 정리한다.
- 어댑터 패턴 Java 구현 (Adapter Pattern in Java)InputStreamReader, Arrays.asList 등 JDK 속 어댑터 사례를 살펴보고, 클래스 어댑터와 객체 어댑터 두 구현 변형의 차이와 Java에서 객체 어댑터가 선호되는 이유를 정리한다.
- 컴포지트 패턴 (Composite Pattern)개별 객체와 객체 묶음을 같은 인터페이스로 다루어 트리 구조를 표현하는 구조 패턴이다. 파일과 폴더 예시 코드로 재귀 구조를 설명하고 투명성과 안전성의 트레이드오프를 다룬다.
- 디자인 패턴 개요 (GoF Design Patterns)반복되는 설계 문제의 검증된 해결 구조인 디자인 패턴을 GoF의 생성, 구조, 행위 세 분류로 조망한다. 패턴 선택 기준과 오남용 경계, 면접 대비 학습 전략까지 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기