Architecture & Design/Software Architecture
Principles - 01. SOLID 원칙
주방에 요리사가 한 명일 때 문제가 생긴다 — SOLID 다섯 원칙
레스토랑 주방을 상상해 보자. 한 요리사가 그릴·샐러드·디저트를 전부 담당한다. 그릴이 늦어지면 디저트도 밀리고, 샐러드 레시피를 바꾸면 그릴 타이밍도 흐트러진다. 이 요리사에게 "한 가지 역할만 하라"고 하는 것이 SRP다. 반대로, 새 메뉴를 추가할 때 기존 레시피를 다시 써야 한다면 주방 구조가 문제인 것이다 — 이게 OCP가 다루는 영역이다.
Robert C. Martin이
이 비유의 한계: 주방은 물리적 공간이라 한 요리사가 두 역할을 하면 눈에 보이지만, 코드에서는 한 클래스가 두 역할을 해도 겉보기엔 정상 동작한다. 문제는 "변경이 올 때" 드러난다.
SRP — 한 클래스는 변경의 이유가 하나다
SRP(Single Responsibility Principle)는 "한 클래스는 한 가지 변경 이유만 가져야 한다"다. Martin은 이를 "한 액터에 대해서만 책임진다"로 더 정확히 정의했다 — "한 일만 한다"가 아니다. Calculator가 add·subtract 둘 다 해도, 두 연산을 쓰는 액터(사용자)가 같으면 SRP를 위반하지 않는다. 반면 한 클래스가 재무팀의 요구(세금 계산)와 운영팀의 요구(로그 출력)를 동시에 담당하면, 두 액터가 각자 변경을 요구할 때 충돌한다.
// SRP 위반 — 두 액터의 요구가 한 클래스에 섞임
class OrderReport {
void generate() { /* 재무팀이 원하는 보고서 생성 */ }
void formatPDF() { /* 재무팀이 원하는 PDF 포맷 */ }
void sendEmail() { /* 운영팀이 원하는 이메일 발송 */ }
}
// 재무팀이 PDF 포맷을 바꾸면 → 이메일 발송 코드도 영향
OCP — 확장엔 열려 있고, 수정엔 닫혀 있다
OCP(Open-Closed Principle)는 "기존 코드를 수정하지 않고 새 기능을 추가할 수 있어야 한다"다. 새 결제 수단(Toss)을 추가할 때 기존 결제 코드(Stripe)를 열어 고치면 OCP 위반이다. 추상화(인터페이스)를 두면, 새 결제수단은 새 클래스를 추가하는 것만으로 끝난다 — 기존 코드를 손대지 않는다.
LSP — 하위 타입은 상위 타입을 대체할 수 있다
LSP(Liskov Substitution Principle)는 "상위 타입을 쓰는 곳에 하위 타입을 넣어도 프로그램이 올바르게 동작해야 한다"다. Rectangle을 상속한 Square가 setWidth에서 side effect를 일으키면(정사각형이므로 높이도 바꿈), Rectangle을 기대한 코드가 깨진다. 상속이 "is-a"가 아니라 "행동이 호환되는가"로 판단해야 한다.
ISP — 쓰지 않는 의존을 강제하지 않는다
ISP(Interface Segregation Principle)는 "클라이언트가 쓰지 않는 메서드에 의존하게 만들지 않는다"다. 거대한 인터페이스(OrderService에 주문·결제·배송·환불이 전부)를 한 클라이언트가 주문만 쓰는데 배속 메서드 변경에도 컴파일이 깨지면 ISP 위반이다. 인터페이스를 용도별로 쪼갠다.
DIP — 고수준이 저수준에 의존하지 않는다
DIP(Dependency Inversion Principle)는 "고수준 모듈(도메인)이 저수준 모듈(인프라)에 의존하지 않고, 둘 다 추상화에 의존해야 한다"다. OrderService가 JpaOrderRepository(구체)가 아니라 OrderRepository(인터페이스)에 의존하면, DB를 바꿔도 도메인이 흔들리지 않는다. 05-ddd-and-patterns의 헥사고날(06번)이 이 원칙의 극한 적용이다.
다섯 원칙의 관계
| 원칙 | 묻는 질문 | 위반 신호 |
|---|---|---|
| SRP | 이 클래스의 변경 이유가 하나인가? | 한 클래스를 두 팀이 동시에 수정 |
| OCP | 새 기능을 추가할 때 기존 코드를 고치는가? | if-else 분기가 계속 늘어남 |
| LSP | 하위 타입을 넣어도 동작하는가? | instanceof 캐스팅이 곳곳에 |
| ISP | 쓰지 않는 메서드에 의존하는가? | 빈 구현, UnsupportedOperationException |
| DIP | 도메인이 인프라를 아는가? | 도메인에 @Entity, @Repository 애노테이션 |
설계 사례 — SRP 위반에서 개선까지
한 OrderService가 주문 생성, 이메일 발송, 할인 계산을 전부 담당한다고 하자. 마케팅팀이 이메일 템플릿을 바꿀 때 주문 로직이 담긴 파일을 수정해야 한다 — 두 액터(주문 담당, 마케팅)의 변경이 한 클래스에서 충돌한다.
flowchart LR
subgraph Before["Before: SRP 위반"]
OS1["OrderService<br/>주문 생성 + 이메일 + 할인"] --> Code["하나의 클래스에<br/>3개 액터의 변경이 충돌"]
end
subgraph After["After: SRP 준수"]
OS2["OrderService<br/>주문 생성만"]
NS["NotificationService<br/>이메일만"]
DS["DiscountPolicy<br/>할인만"]
OS2 --> NS2["NotificationPort"]
OS2 --> DP["DiscountPort"]
end
// Before — SRP 위반: 한 클래스가 3개 역할
class OrderService {
Order place(OrderRequest req) {
// (1) 주문 생성
Order order = new Order(req.items());
// (2) 할인 계산 — 마케팅팀이 자주 바꿈
int discount = req.customerGrade() == VIP ? order.total() / 10 : 0;
order.applyDiscount(discount);
// (3) 이메일 발송 — 운영팀이 자주 바꿈
emailSender.send(req.customerEmail(), buildTemplate(order));
return order;
}
}
// 마케팅팀이 할인율을 바꾸면 → 주문 생성 코드가 담긴 파일을 수정
// 운영팀이 이메일 템플릿을 바꾸면 → 같은 파일을 또 수정 → 충돌
// After — SRP 준수: 각 책임을 별도 객체로
class OrderService {
private final DiscountPolicy discountPolicy; // 할인은 별도
private final NotificationPort notifier; // 알림은 별도
Order place(OrderRequest req) {
Order order = new Order(req.items());
order.applyDiscount(discountPolicy.calculate(order, req.customerGrade()));
notifier.notify(req.customerId(), new OrderConfirmed(order.id()));
return order;
}
}
// 마케팅팀은 DiscountPolicy 구현체만 수정 — OrderService는 모름
// 운영팀은 NotificationPort 어댑터만 수정 — OrderService는 모름
분리 후 마케팅팀은 DiscountPolicy 구현체만, 운영팀은 NotificationPort 어댑터만 수정한다. OrderService는 두 팀의 변경에 영향받지 않는다 — 변경 이유가 하나(주문 생성)로 줄었다. 이것이 SRP의 실천적 가치다: 한 클래스의 변경 이유를 하나로 좁혀, 서로 다른 팀의 변경이 충돌하지 않게 하는 것.
이 분리는 동시에 OCP와 DIP도 만족한다 — 할인 정책이나 알림 방식을 바꿀 때 OrderService를 열지 않고(OCP), OrderService가 구체 클래스가 아니라 인터페이스에 의존한다(DIP). SOLID 원칙들은 개별적이지 않다 — 하나를 지키면 다른 것도 자연스럽게 따라오는 경우가 많다.
원칙은 나침반이다
SOLID를 외우는 것은 도움이 되지 않는다 — 코드에서 "어디가 아픈가"를 느낄 때, 다섯 원칙이 진단 도구로 쓰인다. "새 기능을 추가할 때마다 기존 코드를 고쳐야 한다"면 OCP를 의심하고, "한 클래스를 두 팀이 자주 수정한다"면 SRP를 의심한다. 원칙은 목표가 아니라 나침반이다 — 완벽히 지키려다 과잉 설계에 빠지는 것보다, 어긴 곳을 인지하고 의식적으로 균형을 잡는 것이 중요하다.
참고
- Martin, Robert C. —
(Pearson, 2017), Ch.7-12 (SOLID 상세) — 접근 2026-07-15 - Martin, Robert C. — <Agile Software Development, Principles, Patterns, and Practices> (Pearson, 2002) (SOLID 원전) — 접근 2026-07-15
'Architecture & Design > Software Architecture' 카테고리의 다른 글
| Foundations - 08. 아키텍처 특성의 측정 (0) | 2026.07.15 |
|---|---|
| Foundations - 07. 빌드 vs 구매 vs 조달 (0) | 2026.07.15 |
| Foundations - 06. Conway's Law와 팀 토폴로지 (0) | 2026.07.15 |
| Foundations - 05. 기술 부채와 의사결정 (0) | 2026.07.15 |
| Foundations - 04. 아키텍처 문서화 (0) | 2026.07.15 |