학습 목표
SOLID 다섯 원칙의 의도와 위반 신호를 설명할 수 있다.
SRP·OCP·ISP가 변경 이유·확장·인터페이스를 분리하는 방법을 예제로 설명할 수 있다.
LSP가 대체 가능성을 보장하는 조건임을 설명할 수 있다.
DIP가 헥사고날·클린의 의존 방향과 어떻게 맞물리는지 설명할 수 있다.
문제 상황
OrderService800줄 — 주문·결제·알림·PDF·통계가 한 클래스- 알림 템플릿 바꿀 때마다 주문 로직 회귀 테스트
- 할인 정책 추가할 때마다
if (type == …)분기만 늘어남 Square extends Rectangle— setWidth만 바꿨는데 height도 바뀜, 테스트 깨짐Repository인터페이스에save,sendEmail,generateReport가 같이 있음- mock 구현하려면 쓰지 않는 메서드까지 stub
- Service가
JdbcTemplate직접 import — DB 교체·단위 테스트 불가
아키텍처 스타일을 골랐다. Part 3는 클래스·모듈 단위 설계 원칙 — SOLID부터다.
1. SOLID 한눈에
SOLID는 Robert C. Martin(Uncle Bob)이 정리한 객체지향 설계 원칙 다섯 가지다.

| 원칙 | 한 줄 | 핵심 질문 |
|---|---|---|
| SRP | 변경 이유는 하나 | 이 클래스가 바뀌는 이유가 몇 가지인가 |
| OCP | 확장에 열고 수정에 닫음 | 새 기능을 기존 코드 수정 없이 넣을 수 있는가 |
| LSP | 하위 타입은 대체 가능 | 자식으로 바꿔도 호출자 계약이 깨지지 않는가 |
| ISP | 쓰는 메서드만 인터페이스에 | 클라이언트가 불필요한 API에 의존하는가 |
| DIP | 추상화에 의존 | 고수준이 저수준 구현 세부에 묶여 있는가 |
- 원칙은 독립적이지 않음 — SRP가 깨지면 OCP·ISP도 같이 무너지기 쉽다
- 아키텍처 관점: 모듈·패키지 경계에도 동일하게 적용 (Richards/Ford)
2. SRP — 단일 책임 원칙
A class should have only one reason to change.
한 모듈은 한 actor(이해관계자·변경 주체) 의 요구에 대응한다.

위반 예
class OrderService {
void placeOrder(PlaceOrderCommand cmd) {
validate(cmd);
jdbc.update("INSERT INTO orders ..."); // persistence
smtp.send(buildEmail(cmd)); // notification
pdf.renderInvoice(cmd); // reporting
}
}
- 변경 이유: 주문 규칙, DB 스키마, 메일 템플릿, PDF 포맷 — 네 가지
개선 방향
| 책임 | 후보 |
|---|---|
| 주문 규칙·오케스트레이션 | PlaceOrderService |
| 저장 | OrderRepository 포트 |
| 알림 | OrderNotificationPort |
| (부가) 리포트 | 별도 유스케이스 또는 이벤트 subscriber |
- God class 신호: 파일 길이·import 다양성·테스트 setup 비대
- SRP ≠ "메서드 하나" — 응집된 변경 단위가 기준
3. OCP — 개방-폐쇄 원칙
Open for extension, closed for modification.
동작 추가는 새 타입·전략으로, 기존 if/switch 수정은 최소화한다.

interface DiscountPolicy {
Money discount(OrderLine line);
}
class PercentageDiscount implements DiscountPolicy { /* ... */ }
class FixedAmountDiscount implements DiscountPolicy { /* ... */ }
class PricingService {
private final List<DiscountPolicy> policies;
Money price(OrderLine line) {
Money totalDiscount = policies.stream()
.map(p -> p.discount(line))
.reduce(Money.ZERO, Money::add);
return line.subtotal().subtract(totalDiscount);
}
}
BundleDiscount같은 새 클래스만 추가 —PricingService·기존 정책 수정 없음- 다이어그램: 구현체 → interface,
PricingService→ interface (구현체 직접 참조 X)
| 위반 | 개선 |
|---|---|
switch (promoType) 분기 증가 |
DiscountPolicy 구현 클래스 추가 |
| Service에 외부 API URL 하드코딩 | 포트 + 어댑터로 교체 지점 분리 |
- OCP는 추상화 비용을 감수하는 트레이드오프 — 변경 빈도 높은 축에만 적용
- 이벤트 handler 추가(pub/sub)도 OCP의 아키텍처 스케일 예
4. LSP — 리스코프 치환 원칙
Subtypes must be substitutable for their base types.
기대한 계약(전후 조건·불변식)을 하위 타입이 깨면 LSP 위반.

void resize(Rectangle r, int w, int h) {
r.setWidth(w);
r.setHeight(h);
assert r.area() == w * h; // Square로 바꾸면 실패할 수 있음
}
| 실무 신호 | 대응 |
|---|---|
instanceof로 하위 타입 분기 |
역할별 인터페이스 분리 (Square ≠ Rectangle IS-A) |
| override 후 강화된 precondition | 계약 문서화·composition 우선 |
| mock이 NotImplemented 던짐 | ISP + 작은 인터페이스 |
- 포트 구현체를 바꿔도 유스케이스 테스트가 통과해야 — LSP가 DIP와 만나는 지점
5. ISP — 인터페이스 분리 원칙
Clients should not depend on methods they do not use.
Fat interface를 역할별로 쪼갠다.

// before
interface OrderRepository {
void save(Order o);
void sendReceiptEmail(Order o);
byte[] exportCsv();
}
// after
interface OrderRepository { void save(Order o); }
interface ReceiptSender { void send(Order o); }
interface OrderReportExporter { byte[] exportCsv(); }
- 읽기 전용 클라이언트에는
OrderQueryPort— CQRS 읽기 모델과도 맞음 - Spring
@Autowired하나에 10개 메서드 인터페이스 — mock 부담 = ISP 위반 신호
6. DIP — 의존성 역전 원칙
Depend on abstractions, not concretions.
고수준(정책·유스케이스)이 저수준(DB·SMTP) 구현에 의존하지 않게 추상화를 사이에 둔다.

// high-level
class CancelOrderService {
private final OrderRepository orders; // interface
private final PaymentGateway payments; // interface
}
// low-level adapters implement ports
class StripePaymentGateway implements PaymentGateway { }
class JpaOrderRepository implements OrderRepository { }
| 위반 | DIP | |
|---|---|---|
| 의존 방향 | Service → JdbcTemplate |
Service → OrderRepository ← Jpa |
| 테스트 | DB 필수 | fake / in-memory |
| 헥사고날 | — | 아웃바운드 포트가 DIP의 코드 수준 표현 |
- 헥사고날·클린에서 본 포트·어댑터는 DIP를 패키지·모듈까지 확장한 형태
7. 함께 적용하기
| 품질 속성 | SOLID 연결 |
|---|---|
| Maintainability | SRP·ISP → 변경 범위 축소 |
| Testability | DIP → 포트 mock |
| Extensibility | OCP → 정책·어댑터 추가 |
| Reliability | LSP → 대체 구현 안전 |
변경 요청 → SRP로 책임 분리 → OCP로 확장점 정의
→ ISP로 포트 크기 조절 → DIP로 의존 역전
→ LSP로 구현체 교체 검증
흔한 오해
| 오해 | 실제 |
|---|---|
| SOLID = 클래스 많이 쪼개기 | 변경 이유 기준 분리 |
| 모든 곳에 interface | 교체·테스트 필요 지점만 |
| 원칙 100% 준수 | 트레이드오프 — CRUD 한곳은 단순 유지 OK |
| SOLID = 아키텍처 대체 | 모듈 내부 규율, 스타일·분해와 병행 |
코드 예시 묶음은 code/solid-examples.java 참고.
8. 정리
- SRP — 변경 이유 하나, God class 경계
- OCP — 분기 대신 전략·이벤트·어댑터로 확장
- LSP — 하위 타입 대체 시 계약 유지
- ISP — fat interface 분리, mock·클라이언트 부담 감소
- DIP — 추상화에 의존, 헥사고날 포트와 동일 방향
- 원칙은 유지보수성·테스트를 위한 도구 — 형식만 SOLID인 anemic + interface 남발은 피한다
다음에 다룰 것
- 결합도와 응집도
- 모듈 설계, 패키지 구조, 변경 영향 범위
해당 내용은 Clean Architecture (Robert C. Martin) 및 Fundamentals of Software Architecture (Mark Richards, Neal Ford) 의 내용을 기반으로 합니다.
'소프트웨어 아키텍처' 카테고리의 다른 글
| 도메인 주도 설계 입문 (0) | 2026.06.29 |
|---|---|
| 결합도와 응집도 (0) | 2026.06.28 |
| 아키텍처 스타일 비교와 선택 (0) | 2026.06.26 |
| 이벤트 기반 아키텍처 (0) | 2026.06.25 |
| 마이크로서비스 아키텍처 (0) | 2026.06.24 |