이종관
글 목록으로

SOLID 원칙: 객체 지향 설계의 5가지 핵심 원칙

SRP, OCP, LSP, ISP, DIP 각 원칙의 정의와 위반/적용 사례, 아키텍처 패턴과의 관계

2025년 1월 5일·14 min read·
architecture
oop
solid
design-pattern
clean-architecture

개요

SOLID는 Robert C. Martin(Uncle Bob)이 정립한 5가지 객체 지향 설계 원칙의 약어이다. 이 원칙들은 소프트웨어를 더 이해하기 쉽고, 유연하며, 유지보수 가능하게 만드는 데 목적이 있다. 2000년대 초반에 정립되었지만, 마이크로서비스와 클라우드 네이티브 시대인 2026년 현재에도 여전히 소프트웨어 설계의 근간을 이루고 있다.

S - 단일 책임 원칙 (Single Responsibility Principle, SRP)

정의

"A class should have one, and only one, reason to change."

클래스는 오직 하나의 변경 이유만 가져야 한다. 즉, 하나의 클래스는 하나의 책임(Responsibility)만 담당해야 한다.

위반 사례

java
// SRP 위반: User 클래스가 비즈니스 로직, 직렬화, DB 접근을 모두 담당
public class User {
    public void calculateSalary() { /* 급여 계산 */ }
    public String toJson() { /* JSON 직렬화 */ }
    public void saveToDatabase() { /* DB 저장 */ }
}

올바른 적용

java
public class User { /* 사용자 도메인 로직만 담당 */ }
public class SalaryCalculator { /* 급여 계산 책임 */ }
public class UserSerializer { /* 직렬화 책임 */ }
public class UserRepository { /* 데이터 접근 책임 */ }

SRP와 마이크로서비스

마이크로서비스 아키텍처에서 SRP는 서비스 경계(Service Boundary) 설정의 기본 원칙이 된다:

  • 하나의 서비스 = 하나의 비즈니스 도메인
  • 서비스가 여러 책임을 가지면 → 서비스 분리를 고려
  • DDD(Domain-Driven Design)의 Bounded Context와 직접적으로 연결

O - 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

정의

"Software entities should be open for extension, but closed for modification."

소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.

핵심 개념

기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다. 이를 위해 **추상화(Abstraction)**와 **다형성(Polymorphism)**을 활용한다.

위반 사례

java
// OCP 위반: 새로운 결제 수단 추가 시 기존 코드 수정 필요
public class PaymentProcessor {
    public void process(String type) {
        if (type.equals("card")) { /* 카드 결제 */ }
        else if (type.equals("bank")) { /* 계좌이체 */ }
        // 새 결제 수단마다 else if 추가 → 수정 필요
    }
}

올바른 적용

java
// OCP 준수: 새 결제 수단은 인터페이스 구현으로 확장
public interface PaymentStrategy {
    void process(PaymentRequest request);
}
 
public class CardPayment implements PaymentStrategy { /* 카드 */ }
public class BankTransfer implements PaymentStrategy { /* 계좌이체 */ }
public class CryptoPayment implements PaymentStrategy { /* 암호화폐 -- 확장 */ }
 
public class PaymentProcessor {
    private final PaymentStrategy strategy;
 
    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
 
    public void process(PaymentRequest request) {
        strategy.process(request); // 기존 코드 수정 없음
    }
}

OCP와 플러그인 아키텍처

OCP의 궁극적 실현 형태는 **플러그인 아키텍처(Plugin Architecture)**이다:

  • 핵심 시스템은 변경하지 않고 플러그인으로 기능 확장
  • IDE(IntelliJ, VS Code), 빌드 도구(Gradle, Webpack) 등이 대표적
  • 마이크로서비스의 API Gateway 라우팅 규칙도 OCP의 적용

L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

정의

"Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program."

상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 **정확성(Correctness)**이 유지되어야 한다. Barbara Liskov가 1987년에 제안하였다.

위반의 대표적 사례: 직사각형-정사각형 문제

java
public class Rectangle {
    protected int width;
    protected int height;
 
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}
 
// LSP 위반: 정사각형은 width와 height가 항상 같아야 하므로
// Rectangle의 계약(contract)을 위반함
public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w; // 부작용! 호출자가 예상 못함
    }
}

올바른 설계

java
public interface Shape {
    int area();
}
 
public class Rectangle implements Shape {
    private final int width;
    private final int height;
    public int area() { return width * height; }
}
 
public class Square implements Shape {
    private final int side;
    public int area() { return side * side; }
}

LSP의 계약 기반 설계 (Design by Contract)

LSP는 Bertrand Meyer의 계약 기반 설계와 밀접하게 연결된다:

  • 사전 조건(Precondition): 하위 타입은 상위 타입보다 약한 사전 조건을 요구할 수 있다
  • 사후 조건(Postcondition): 하위 타입은 상위 타입보다 강한 사후 조건을 보장해야 한다
  • 불변 조건(Invariant): 상위 타입의 불변 조건을 하위 타입도 유지해야 한다

I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

정의

"Clients should not be forced to depend on interfaces they do not use."

클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다.

위반 사례

java
// ISP 위반: 모든 워커가 eat()을 구현해야 함
public interface Worker {
    void work();
    void eat();
    void sleep();
}
 
// 로봇 워커는 eat()과 sleep()이 불필요
public class RobotWorker implements Worker {
    public void work() { /* 작업 수행 */ }
    public void eat() { /* 불필요한 구현 강제 */ }
    public void sleep() { /* 불필요한 구현 강제 */ }
}

올바른 적용

java
public interface Workable { void work(); }
public interface Feedable { void eat(); }
public interface Sleepable { void sleep(); }
 
public class HumanWorker implements Workable, Feedable, Sleepable {
    public void work() { /* 작업 */ }
    public void eat() { /* 식사 */ }
    public void sleep() { /* 수면 */ }
}
 
public class RobotWorker implements Workable {
    public void work() { /* 작업만 수행 */ }
}

ISP와 마이크로서비스 API 설계

ISP는 마이크로서비스의 API 설계에 직접 적용된다:

  • BFF 패턴 (Backend for Frontend): 클라이언트별로 필요한 API만 제공
  • GraphQL: 클라이언트가 필요한 필드만 요청 -- ISP의 정신
  • API 버저닝: 레거시 클라이언트가 새 필드에 의존하지 않도록 분리

D - 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

정의

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 되며, 둘 모두 **추상화(Abstraction)**에 의존해야 한다.

위반 사례

java
// DIP 위반: 상위 모듈이 구체 구현에 직접 의존
public class OrderService {
    private MySQLDatabase database = new MySQLDatabase(); // 구체 의존
    private SmtpMailer mailer = new SmtpMailer();          // 구체 의존
}

올바른 적용

java
// DIP 준수: 추상화에 의존
public class OrderService {
    private final Database database;       // 인터페이스 의존
    private final MailService mailer;      // 인터페이스 의존
 
    public OrderService(Database database, MailService mailer) {
        this.database = database;          // DI 컨테이너가 주입
        this.mailer = mailer;
    }
}

DI 컨테이너와 IoC

DIP의 실현 메커니즘은 **의존성 주입(Dependency Injection, DI)**이다:

DI 방식설명예시
생성자 주입 (Constructor Injection)생성자를 통해 의존성 전달Spring의 @Autowired
세터 주입 (Setter Injection)세터 메서드로 의존성 전달선택적 의존성에 적합
인터페이스 주입 (Interface Injection)전용 인터페이스로 의존성 전달덜 일반적

IoC 컨테이너 (Inversion of Control Container): Spring, NestJS, .NET의 DI 컨테이너가 객체 생명주기와 의존성 해결을 자동으로 관리한다.

SOLID와 아키텍처 패턴의 관계

클린 아키텍처 (Clean Architecture)

Robert C. Martin이 제안한 클린 아키텍처는 SOLID 원칙을 아키텍처 수준으로 확장한 것이다:

plaintext
┌─────────────────────────────────────┐
│         Frameworks & Drivers        │  ← 가장 바깥 (DB, UI, Web)
│  ┌─────────────────────────────┐    │
│  │    Interface Adapters       │    │  ← 컨트롤러, 게이트웨이
│  │  ┌─────────────────────┐   │    │
│  │  │   Application       │   │    │  ← 유스케이스 (비즈니스 규칙)
│  │  │  ┌─────────────┐   │   │    │
│  │  │  │  Entities    │   │   │    │  ← 도메인 엔티티 (핵심)
│  │  │  └─────────────┘   │   │    │
│  │  └─────────────────────┘   │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

의존성 규칙(Dependency Rule): 의존성은 항상 바깥에서 안쪽으로만 향한다 → DIP의 직접적 적용

헥사고날 아키텍처 (Hexagonal Architecture / Ports & Adapters)

Alistair Cockburn이 제안한 헥사고날 아키텍처는 DIP를 **포트(Port)**와 **어댑터(Adapter)**로 실현한다:

plaintext
          ┌──────────────────┐
  Driving │                  │ Driven
  Adapter │    Application   │ Adapter
  (UI) ──→│    Core          │──→ (DB)
          │    (Domain)      │
  REST ──→│                  │──→ (MQ)
  API     │   Ports: 인터페이스│
          └──────────────────┘
  • Driving Port (Primary): 외부에서 애플리케이션을 호출하는 인터페이스 (API, CLI)
  • Driven Port (Secondary): 애플리케이션이 외부 인프라를 호출하는 인터페이스 (DB, 메시징)
  • Adapter: 포트의 구체적 구현

Netflix는 헥사고날 아키텍처를 적용하여 마이크로서비스의 도메인 로직을 인프라로부터 완전히 분리하였다.

CQRS (Command Query Responsibility Segregation)

CQRS는 **읽기(Query)**와 **쓰기(Command)**의 책임을 분리하는 패턴이다:

  • SRP를 데이터 접근 계층에 적용한 결과
  • 읽기 모델과 쓰기 모델을 독립적으로 최적화 가능
  • Event Sourcing과 결합하면 완전한 감사 추적(Audit Trail) 가능
  • 행위 패턴의 Event Sourcing 패턴과 연결

도메인 주도 설계 (Domain-Driven Design, DDD) 전술적 패턴

DDD의 전술적 패턴들은 SOLID 원칙을 도메인 모델링에 적용한 것이다:

DDD 패턴관련 SOLID 원칙설명
EntitySRP고유 식별자를 가진 도메인 객체
Value ObjectSRP, 불변성식별자 없이 값으로만 비교되는 객체
AggregateSRP, ISP일관성 경계를 가진 엔티티 클러스터
RepositoryDIP, ISP데이터 접근을 추상화하는 인터페이스
Domain ServiceSRP엔티티에 속하지 않는 도메인 로직
Application ServiceSRP, OCP유스케이스 조정
Domain EventOCP도메인 상태 변화 알림

SOLID 원칙의 현대적 비판과 진화

비판

  1. 과도한 추상화: SOLID를 맹목적으로 적용하면 불필요한 인터페이스와 클래스가 폭증
  2. 함수형 프로그래밍에서의 적용: FP에서는 OCP나 LSP가 다른 형태로 나타남
  3. YAGNI (You Ain't Gonna Need It): 현재 필요하지 않은 확장성을 위한 과도한 설계 경계

실용적 적용 가이드

  • 항상 적용: SRP, DIP -- 코드 품질에 가장 직접적인 영향
  • 상황에 따라 적용: OCP, ISP -- 변경 빈도가 높은 영역에 집중
  • 주의하여 적용: LSP -- 상속보다 합성(Composition)을 선호

참고 자료

  • Robert C. Martin, Clean Architecture (2017)
  • Robert C. Martin, "The Principles of OOD" (butunclebob.com)
  • Alistair Cockburn, "Hexagonal Architecture" (2005)
  • Eric Evans, Domain-Driven Design (2003)
  • Netflix Technology Blog, "Ready for changes with Hexagonal Architecture"