Architecture & Design/Software Architecture

Principles - 01. SOLID 원칙

주방에 요리사가 한 명일 때 문제가 생긴다 — SOLID 다섯 원칙

레스토랑 주방을 상상해 보자. 한 요리사가 그릴·샐러드·디저트를 전부 담당한다. 그릴이 늦어지면 디저트도 밀리고, 샐러드 레시피를 바꾸면 그릴 타이밍도 흐트러진다. 이 요리사에게 "한 가지 역할만 하라"고 하는 것이 SRP다. 반대로, 새 메뉴를 추가할 때 기존 레시피를 다시 써야 한다면 주방 구조가 문제인 것이다 — 이게 OCP가 다루는 영역이다.

Robert C. Martin이 에서 정리한 SOLID는 다섯 개의 설계 원칙이다. 각 원칙은 독립된 "건강 지표"처럼, 어느 것이 빨간불인지를 보면 설계의 어디가 아픈지 진단할 수 있다. 다섯을 동시에 완벽히 지킬 필요는 없지만, 어느 것을 어기고 있는지 아는 것이 설계 개선의 출발점이다.

이 비유의 한계: 주방은 물리적 공간이라 한 요리사가 두 역할을 하면 눈에 보이지만, 코드에서는 한 클래스가 두 역할을 해도 겉보기엔 정상 동작한다. 문제는 "변경이 올 때" 드러난다.

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