소프트웨어 아키텍처

SOLID 원칙

meellon 2026. 6. 27. 05:20

학습 목표

SOLID 다섯 원칙의 의도위반 신호를 설명할 수 있다.

SRP·OCP·ISP변경 이유·확장·인터페이스를 분리하는 방법을 예제로 설명할 수 있다.

LSP대체 가능성을 보장하는 조건임을 설명할 수 있다.

DIP헥사고날·클린의 의존 방향과 어떻게 맞물리는지 설명할 수 있다.

문제 상황

  • OrderService 800줄 — 주문·결제·알림·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, PricingServiceinterface (구현체 직접 참조 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