Software Architecture/Principles
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 원칙들은 개별적이지 않다 — 하나를 지키면 다른 것도 자연스럽게 따라오는 경우가 많다.
LSP가 깨지는 순간 — Rectangle과 Square
LSP 위반의 고전적 사례를 본다. Rectangle에 width·height를 설정하는 메서드가 있고, Square가 이를 상속한다. Square는 width를 바꾸면 height도 같이 바꿔야 한다(정사각형이므로). Rectangle을 기대한 코드 rect.setWidth(5); rect.setHeight(3)가 area=15를 기대하는데, Square에서는 area=9가 나온다.
// LSP 위반 — Square가 Rectangle의 계약을 어김
class Rectangle { protected int w, h; void setWidth(int w) { this.w = w; } void setHeight(int h) { this.h = h; } int area() { return w * h; } }
class Square extends Rectangle {
void setWidth(int w) { this.w = w; this.h = w; } // 높이도 바꿈 — 기대 깨짐
void setHeight(int h) { this.w = h; this.h = h; }
}
Rectangle r = new Square();
r.setWidth(5); r.setHeight(3);
r.area(); // 9 (Rectangle 기대: 15) → LSP 위반
해결은 상속을 포기하는 것이다. Square는 Rectangle이 아니다 — 둘 다 "도형"이지만, Square의 제약(가로=세로)이 Rectangle의 계약(가로·세로 독립)을 위반한다. instanceof 캐스팅이 곳곳에 나타나면 LSP 위반의 신호다.
ISP — 거대 인터페이스 쪼개기
한 OrderService 인터페이스에 주문 생성·조회·취소·배송처리·환불·정산이 전부 있다. 주문 조회만 필요한 클라이언트(대시보드)가 정산 메서드 변경에 컴파일 에러를 만난다 — 쓰지도 않는 메서드에 의존하게 된다. ISP는 이 인터페이스를 용도별로 쪼갠다.
// ISP 위반 — 하나의 거대 인터페이스
interface OrderService {
Order place(OrderRequest req); // 주문 담당
Order find(OrderId id); // 조회 담당
void cancel(OrderId id); // 취소 담당
void refund(OrderId id); // 정산 담당
}
// 대시보드(조회만 필요)가 refund 변경에 영향 받음
// ISP 준수 — 용도별 분리
interface OrderPlacement { Order place(OrderRequest req); }
interface OrderQuery { Order find(OrderId id); }
interface OrderCancellation { void cancel(OrderId id); }
// 대시보드는 OrderQuery만 의존 — refund 변경과 무관
OCP 설계 사례 — 새 결제수단을 기존 코드 수정 없이 추가
OCP가 잘 지켜지면, 새 결제수단(Toss)을 추가할 때 기존 결제 코드(Stripe)를 열 필요가 없다.
// OCP 위반 — 새 결제수단마다 기존 코드 수정
class Checkout {
void pay(Order order, String pgCode) {
if ("STRIPE".equals(pgCode)) stripeGateway.charge(order.total());
else if ("TOSS".equals(pgCode)) tossGateway.charge(order.total()); // ← 새 분기
// 결제수단이 늘 때마다 if-else가 늘어남 → 기존 코드 수정 → OCP 위반
}
}
// OCP 준수 — 추상화 뒤에 새 구현 추가
interface PaymentGateway { void charge(Money amount); }
// 기존: StripePaymentGateway implements PaymentGateway
// 추가: TossPaymentGateway implements PaymentGateway ← 새 클래스만 추가
class Checkout {
private final PaymentGateway gateway; // 인터페이스
void pay(Order order) { gateway.charge(order.total()); } // 수정 없음
}
// 새 결제수단 추가 = 새 클래스 1개 추가, Checkout 수정 0줄
원칙은 나침반이다
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
'Software Architecture > Principles' 카테고리의 다른 글
| Principles - 06. 계약 설계와 API (0) | 2026.07.16 |
|---|---|
| Principles - 05. 캡슐화와 정보 은닉 (0) | 2026.07.16 |
| Principles - 04. 의존성 규칙 (0) | 2026.07.16 |
| Principles - 03. 패러다임의 함의 (0) | 2026.07.16 |
| Principles - 02. 추상화와 깊은 모듈 (0) | 2026.07.16 |