2026년 5월 26일

SOLID 원칙 마스터하기: 유연하고 유지보수하기 쉬운 객체 지향 설계의 핵심

60
SOLID 원칙 마스터하기: 유연하고 유지보수하기 쉬운 객체 지향 설계의 핵심

SOLID 원칙 마스터하기: 유연하고 유지보수하기 쉬운 객체 지향 설계의 핵심

SOLID 원칙 마스터하기: 유연하고 유지보수하기 쉬운 객체 지향 설계의 핵심

안녕하세요, 10년 경력의 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘은 많은 초중급 개발자들이 간과하기 쉽지만, 여러분의 코드 품질과 설계 능력을 한 단계 끌어올릴 수 있는 매우 중요한 개념, 바로 SOLID 원칙에 대해 이야기해보려 합니다.

SOLID 원칙은 로버트 C. 마틴(Uncle Bob)이 제안한 객체 지향 설계의 5가지 핵심 원칙을 모아놓은 약어입니다. 이 원칙들은 소프트웨어를 더 유연하고, 확장 가능하며, 이해하기 쉽고, 유지보수하기 쉽게 만드는 데 목적이 있습니다. 복잡한 시스템을 다루고, 여러 개발자가 협업하며, 시간이 지나도 견고하게 작동하는 코드를 만들고 싶다면, SOLID 원칙은 선택이 아닌 필수입니다.

우리는 이 글을 통해 각 원칙의 의미를 명확히 이해하고, 실제 코드에 어떻게 적용하는지 파이썬과 자바스크립트 예제를 통해 살펴보며, 실무에서 자주 발생하는 문제들을 SOLID 원칙으로 어떻게 해결할 수 있는지 알아보겠습니다.

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

SOLID는 객체 지향 프로그래밍(OOP)의 다섯 가지 기본 원칙의 앞글자를 따서 만든 용어입니다. 이 원칙들은 2000년대 초반 로버트 C. 마틴(Robert C. Martin), 일명 'Uncle Bob'이 그의 저서 "Agile Software Development, Principles, Patterns, and Practices"에서 소개하며 널리 알려지게 되었습니다.

  • Single-responsibility Principle (단일 책임 원칙)
  • Open/closed Principle (개방-폐쇄 원칙)
  • Liskov Substitution Principle (리스코프 치환 원칙)
  • Interface Segregation Principle (인터페이스 분리 원칙)
  • Dependency Inversion Principle (의존성 역전 원칙)

왜 SOLID 원칙이 중요할까요?

초기에는 작은 프로젝트로 시작했지만, 시간이 지남에 따라 기능이 추가되고, 팀원이 늘어나고, 요구사항이 변경되면서 코드가 점점 복잡해지고 엉망이 되는 경험을 해본 적이 있을 겁니다. 이를 '레거시 코드'라고 부르기도 하죠. SOLID 원칙은 이러한 문제들을 사전에 방지하고, 다음과 같은 이점을 제공합니다.

  1. 유지보수성 향상: 코드를 변경할 때 다른 부분에 미치는 영향을 최소화합니다.
  2. 확장성 증대: 새로운 기능을 쉽게 추가할 수 있도록 설계합니다.
  3. 재사용성 증가: 잘 정의된 컴포넌트들을 다양한 곳에서 재사용할 수 있게 합니다.
  4. 테스트 용이성: 각 컴포넌트가 독립적으로 작동하도록 설계하여 테스트 작성을 쉽게 만듭니다.
  5. 가독성 및 이해도 향상: 깔끔하고 예측 가능한 코드는 다른 개발자들이 이해하고 협업하기 좋습니다.

결론적으로, SOLID 원칙은 여러분이 작성하는 코드가 변화에 유연하게 대응하고, 장기적으로 안정성을 유지하며, 팀원들과의 협업 효율을 높이는 데 결정적인 역할을 합니다.

2. 핵심 원리 설명 (비유와 다이어그램 활용)

2. 핵심 원리 설명 (비유와 다이어그램 활용)

각 원칙을 비유와 함께 자세히 살펴보겠습니다.

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

정의: 클래스는 단 하나의 변경 이유만 가져야 합니다. 즉, 하나의 클래스는 하나의 책임만 가져야 합니다.

비유: 주방에서 사용하는 칼을 생각해봅시다. 스테이크를 써는 칼, 빵을 자르는 칼, 과일 껍질을 벗기는 칼 등 각각의 칼은 특정한 용도(책임)를 가지고 있습니다. 만약 이 모든 기능을 하나의 '만능 칼'에 넣는다면 어떨까요? 칼 하나의 무게는 무거워지고, 스테이크를 썰다가 날이 무뎌지면 빵을 자르는 기능까지 영향을 받을 수 있습니다.

설명: 어떤 클래스가 여러 가지 책임을 가지고 있다면, 그 책임들 중 하나라도 변경될 때마다 클래스 전체를 수정해야 할 위험이 있습니다. 이는 불필요한 사이드 이펙트를 유발하고 테스트를 어렵게 만듭니다. SRP는 책임을 분리하여 각 클래스가 자기 역할에만 집중하도록 만듭니다.

[다이어그램 묘사]

  • SRP 위반: ReportGenerator 클래스
    • fetchData() 메서드
    • formatReport() 메서드
    • saveReport() 메서드 (변경 이유가 '데이터 가져오는 방식', '보고서 형식', '저장 방식'으로 3가지)
  • SRP 적용: ReportDataFetcher 클래스 (fetchData()) ReportFormatter 클래스 (formatReport()) ReportSaver 클래스 (saveReport()) (각 클래스가 단 하나의 책임만 가짐)

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

정의: 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 합니다.

비유: 스마트폰과 앱 스토어를 생각해봅시다. 스마트폰(운영체제)은 새로운 앱(기능 확장)이 추가될 때마다 자신의 내부 코드를 수정하지 않습니다. 앱 개발자는 스마트폰의 인터페이스(API)를 활용하여 새로운 앱을 만들고, 사용자는 앱을 설치하는 방식으로 기능을 확장합니다. 스마트폰은 새로운 앱에 대해 '개방'되어 있지만, 스마트폰 자체의 코드는 '폐쇄'되어 있습니다.

설명: OCP는 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 하는 설계 원칙입니다. 이를 위해서는 주로 추상화(인터페이스, 추상 클래스)를 사용하고, 다형성을 활용하여 구현합니다.

[다이어그램 묘사]

  • OCP 위반: AreaCalculator 클래스
    • calculate(shapes) 메서드
      • if shape type == Circle: calculate circle area
      • if shape type == Rectangle: calculate rectangle area (새로운 도형 추가 시 AreaCalculator 코드 수정 필요)
  • OCP 적용: Shape 인터페이스 (추상 메서드 getArea()) Circle 클래스 (implements Shape, getArea() 구현) Rectangle 클래스 (implements Shape, getArea() 구현) AreaCalculator 클래스 (calculate(shapes) 메서드는 shape.getArea() 호출) (새로운 도형 추가 시 AreaCalculator 수정 없이 Shape 인터페이스 구현만 하면 됨)

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

정의: 서브타입은 언제나 자신의 기반 타입(부모 클래스)으로 교체할 수 있어야 합니다. 즉, 부모 클래스의 객체를 사용하는 코드에서 자식 클래스의 객체로 바꾸더라도 프로그램의 정확성에 문제가 없어야 합니다.

비유: 조류라는 추상적인 개념이 있고, 여기에 '날다'라는 기능이 있다고 가정해봅시다. 독수리, 참새는 조류의 서브타입으로 잘 날 수 있습니다. 하지만 펭귄이라는 서브타입은 '날다'라는 기능을 제대로 수행할 수 없습니다. 이 경우, 펭귄은 조류의 서브타입이지만 '날다'라는 행동에 대해 기반 타입(조류)의 기대를 만족시키지 못하므로 LSP를 위반합니다.

설명: LSP는 상속 관계에서 부모 클래스와 자식 클래스 간의 계약(Contract)을 명확히 합니다. 자식 클래스는 부모 클래스의 동작을 오버라이드할 때, 부모가 제공하는 기본적인 기능을 해치거나 예외를 발생시켜서는 안 됩니다.

[다이어그램 묘사]

  • LSP 위반 (예시): Rectangle 클래스 (width, height)
    • setWidth(w)
    • setHeight(h) Square 클래스 (inherits Rectangle)
    • setWidth(w): set width=w, height=w
    • setHeight(h): set width=h, height=h (Square는 Rectangle의 setWidth/setHeight 동작 방식을 변경하여, Rectangle 객체를 기대하는 코드에서 Square 객체가 오면 예상치 못한 결과 발생)
  • LSP 적용 (해결법 중 하나): Shape 인터페이스 (혹은 다른 추상 클래스) Rectangle 클래스 (implements Shape) Square 클래스 (implements Shape) (상속 대신 공통 인터페이스를 구현하거나, Square를 Rectangle의 자식으로 만들지 않고 독립적인 Shape으로 다룸)

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

정의: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존해서는 안 됩니다. 큰 인터페이스를 여러 개의 작은 인터페이스로 분리해야 합니다.

비유: 뷔페 식당의 메뉴판을 생각해봅시다. 모든 요리가 하나의 거대한 메뉴판에 다 적혀 있다면, 고기를 좋아하는 사람은 야채나 해산물 코너를 찾기 위해 불필요한 정보를 탐색해야 합니다. 하지만 고기 메뉴판, 해산물 메뉴판, 샐러드 메뉴판처럼 전문적으로 분리되어 있다면, 각 고객은 자신이 원하는 메뉴판만 보고 주문할 수 있어 효율적입니다.

설명: ISP는 SRP가 클래스에 적용되는 것과 유사하게, 인터페이스에 적용됩니다. '뚱뚱한' 인터페이스는 그것을 구현하는 클래스들이 불필요한 메서드까지 강제로 구현하게 만들어, 클래스의 책임이 모호해지고 변경에 취약하게 만듭니다. 인터페이스를 작고 응집도 높게 분리하면, 클라이언트는 필요한 기능만을 가진 인터페이스에 의존하게 됩니다.

[다이어그램 묘사]

  • ISP 위반: Worker 인터페이스
    • work()
    • eat()
    • sleep() HumanWorker 클래스 (implements Worker) -> 모든 메서드 구현 RobotWorker 클래스 (implements Worker) -> eat(), sleep()은 의미 없음에도 구현 강제
  • ISP 적용: Workable 인터페이스 (work()) Eatable 인터페이스 (eat()) Sleepable 인터페이스 (sleep()) HumanWorker 클래스 (implements Workable, Eatable, Sleepable) RobotWorker 클래스 (implements Workable) (로봇은 자신이 필요한 Workable만 구현)

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

정의:

  1. 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 이들 모두 추상화에 의존해야 합니다.
  2. 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 의존해야 합니다.

비유: 벽에 있는 콘센트(추상화)와 다양한 가전제품(저수준 모듈)을 생각해보세요. 콘센트는 어떤 특정 가전제품에 의존하지 않습니다. 냉장고, TV, 충전기 등 모든 가전제품은 콘센트라는 '추상화된 인터페이스'에 맞춰 설계됩니다. 만약 콘센트가 냉장고에 맞춰서만 만들어진다면 다른 가전제품을 사용할 수 없을 것입니다.

설명: DIP는 변화에 유연한 시스템을 만드는 핵심 원칙입니다. 구체적인 구현(저수준 모듈)에 직접 의존하는 대신, 인터페이스나 추상 클래스(추상화)에 의존하도록 만듭니다. 이를 통해 고수준 모듈(비즈니스 로직)은 저수준 모듈(DB 접근, 파일 시스템 등)의 변경에 영향을 받지 않고 독립적으로 존재할 수 있게 됩니다. 이는 흔히 '제어의 역전(IoC)'이라는 개념과 함께 사용되며, 의존성 주입(DI)을 통해 구현됩니다.

[다이어그램 묘사]

  • DIP 위반: HighLevelModule 클래스
    • operatesOn(ConcreteLowLevelModule) ConcreteLowLevelModule 클래스 (고수준 모듈이 특정 저수준 모듈에 직접 의존)
  • DIP 적용: Abstraction 인터페이스 HighLevelModule 클래스
    • operatesOn(Abstraction) ConcreteLowLevelModule 클래스 (implements Abstraction) (고수준 모듈이 추상화에 의존하고, 저수준 모듈이 추상화를 구현)

3. 코드 예제 2개

예제 1: SRP와 OCP (Python)

SRP와 OCP를 함께 보여주는 예제입니다. 먼저 SRP를 위반한 코드를 보고, SRP를 적용한 후, OCP까지 적용하여 확장성을 높여보겠습니다.

# SRP 위반 예제: ReportGenerator가 너무 많은 책임을 가짐
class BadReportGenerator:
    def __init__(self, data):
        self.data = data

    def fetch_data(self):
        # 실제로는 DB나 API에서 데이터를 가져오는 복잡한 로직이 있다고 가정
        print("데이터를 DB에서 가져오는 중...")
        return self.data

    def format_html(self, fetched_data):
        print("HTML 형식으로 보고서를 포맷하는 중...")
        return f"<html><body><h1>보고서</h1><p>{fetched_data}</p></body></html>"

    def format_pdf(self, fetched_data):
        print("PDF 형식으로 보고서를 포맷하는 중...")
        # 실제 PDF 생성 라이브러리를 사용한다고 가정
        return f"PDF Report for: {fetched_data}"

    def save_to_file(self, content, filename):
        print(f"보고서를 {filename}에 저장하는 중...")
        with open(filename, 'w') as f:
            f.write(content)

# 사용 예시
# generator = BadReportGenerator("판매 데이터")
# data = generator.fetch_data()
# html_report = generator.format_html(data)
# generator.save_to_file(html_report, "sales_report.html")

# --- SRP 적용 ---
# 데이터 가져오기 책임 분리
class ReportDataFetcher:
    def fetch(self, source):
        print(f"데이터를 {source}에서 가져오는 중...")
        # 실제로는 DB나 API에서 데이터를 가져오는 복잡한 로직이 있다고 가정
        return f"데이터 from {source}"

# 보고서 포맷 책임 분리 (OCP 적용을 위한 준비)
class HtmlReportFormatter:
    def format(self, data):
        print("HTML 형식으로 보고서를 포맷하는 중...")
        return f"<html><body><h1>보고서</h1><p>{data}</p></body></html>"

class PdfReportFormatter:
    def format(self, data):
        print("PDF 형식으로 보고서를 포맷하는 중...")
        return f"PDF Report for: {data}"

# 보고서 저장 책임 분리
class ReportSaver:
    def save(self, content, filename):
        print(f"보고서를 {filename}에 저장하는 중...")
        with open(filename, 'w') as f:
            f.write(content)

# 이제 ReportGenerator는 오직 보고서 생성 흐름을 조율하는 책임만 가집니다.
# OCP 적용: 새로운 포맷터를 추가해도 ReportGenerator는 변경되지 않습니다.
# 이를 위해 추상화(인터페이스)를 사용합니다. Python에서는 abc 모듈을 활용합니다.
from abc import ABC, abstractmethod

class ReportFormatter(ABC): # 추상 베이스 클래스 (인터페이스 역할)
    @abstractmethod
    def format(self, data):
        pass

# 기존 포맷터들은 이 인터페이스를 구현합니다.
class HtmlReportFormatter(ReportFormatter):
    def format(self, data):
        print("HTML 형식으로 보고서를 포맷하는 중...")
        return f"<html><body><h1>보고서</h1><p>{data}</p></body></html>"

class PdfReportFormatter(ReportFormatter):
    def format(self, data):
        print("PDF 형식으로 보고서를 포맷하는 중...")
        return f"PDF Report for: {data}"

# 새로운 포맷터 추가 (기존 ReportGenerator 수정 없이 확장)
class MarkdownReportFormatter(ReportFormatter):
    def format(self, data):
        print("Markdown 형식으로 보고서를 포맷하는 중...")
        return f"# 보고서\n\n{data}"

class GoodReportGenerator:
    def __init__(self, fetcher: ReportDataFetcher, formatter: ReportFormatter, saver: ReportSaver):
        # 의존성 주입을 통해 구체적인 구현체가 아닌 추상화에 의존합니다. (DIP와도 연결)
        self.fetcher = fetcher
        self.formatter = formatter
        self.saver = saver

    def generate_and_save_report(self, data_source: str, output_filename: str):
        data = self.fetcher.fetch(data_source)
        formatted_report = self.formatter.format(data)
        self.saver.save(formatted_report, output_filename)
        print("보고서 생성 및 저장 완료.")

# 사용 예시:
print("\n--- SRP 및 OCP 적용 후 ---")
fetcher = ReportDataFetcher()
saver = ReportSaver()

# HTML 보고서 생성
html_formatter = HtmlReportFormatter()
html_generator = GoodReportGenerator(fetcher, html_formatter, saver)
html_generator.generate_and_save_report("판매 데이터베이스", "sales_report.html")

print("-" * 20)

# PDF 보고서 생성
pdf_formatter = PdfReportFormatter()
pdf_generator = GoodReportGenerator(fetcher, pdf_formatter, saver)
pdf_generator.generate_and_save_report("재고 데이터베이스", "inventory_report.pdf")

print("-" * 20)

# Markdown 보고서 생성 (새로운 포맷터 추가 시 기존 Generator 코드를 수정할 필요 없음)
markdown_formatter = MarkdownReportFormatter()
markdown_generator = GoodReportGenerator(fetcher, markdown_formatter, saver)
markdown_generator.generate_and_save_report("로그 데이터", "log_summary.md")

예제 2: LSP와 DIP (JavaScript)

JavaScript는 클래스 기반 상속보다는 프로토타입 기반 상속을 사용하며, 명시적인 인터페이스 개념은 없지만, 추상화와 덕 타이핑(Duck Typing)을 통해 SOLID 원칙을 적용할 수 있습니다. 여기서는 LSP 위반 사례와 DIP 적용 예시를 살펴보겠습니다.

// LSP 위반 예제: Square가 Rectangle의 동작을 깨뜨리는 경우
class Rectangle {
    constructor(width, height) {
        this._width = width;
        this._height = height;
    }

    get width() { return this._width; }
    set width(value) { this._width = value; }

    get height() { return this._height; }
    set height(value) { this._height = value; }

    getArea() {
        return this._width * this._height;
    }
}

class Square extends Rectangle {
    constructor(side) {
        super(side, side);
    }

    // LSP 위반! Square는 모든 변의 길이가 같아야 한다는 자신의 불변식을 유지하려다
    // 부모 클래스인 Rectangle의 setWidth/setHeight 계약을 변경해버립니다.
    // 즉, Rectangle 객체가 기대하는 동작(width만 변경, height는 그대로)과 달라집니다.
    set width(value) {
        this._width = value;
        this._height = value; // height도 함께 변경
    }

    set height(value) {
        this._width = value; // width도 함께 변경
        this._height = value;
    }
}

function calculateArea(rect) {
    // 이 함수는 Rectangle 객체를 기대하며, 너비와 높이를 독립적으로 설정할 수 있다고 가정합니다.
    rect.width = 5;
    rect.height = 4;
    console.log(`Expected area: 20, Actual area: ${rect.getArea()}`);
}

console.log("--- LSP 위반 예제 ---");
const myRectangle = new Rectangle(2, 3);
calculateArea(myRectangle); // Expected area: 20, Actual area: 20 (정상)

const mySquare = new Square(3);
calculateArea(mySquare); // Expected area: 20, Actual area: 25 (!!! 예상과 다른 결과, LSP 위반)
// Square의 setWidth(5)가 호출될 때 height도 5로 변경되어, 최종적으로 width=5, height=5가 됨.

// --- LSP 해결 아이디어 (DIP와 연결): 상속 대신 인터페이스(추상화)에 의존
// JavaScript에는 명시적인 인터페이스가 없지만, 추상화된 역할을 정의할 수 있습니다.
// 여기서는 '도형'이라는 추상적인 개념을 함수로 정의하고, 각 도형이 이를 따르도록 합니다.

class Shape { // 추상 클래스 역할을 하는 Base Class (혹은 그냥 독립적인 클래스)
    getArea() { throw new Error("getArea() must be implemented by subclasses"); }
}

class GoodRectangle extends Shape {
    constructor(width, height) {
        super();
        this._width = width;
        this._height = height;
    }
    getArea() { return this._width * this._height; }
}

class GoodSquare extends Shape {
    constructor(side) {
        super();
        this._side = side;
    }
    getArea() { return this._side * this._side; }
}

// 이제 calculateArea는 특정 도형 클래스에 의존하는 대신,
// `getArea()` 메서드를 가진 어떤 객체든 받을 수 있도록 추상화에 의존합니다. (DIP 적용)
function calculateShapeArea(shape) {
    console.log(`Calculated area: ${shape.getArea()}`);
}

console.log("\n--- LSP 및 DIP 적용 후 ---");
const goodRect = new GoodRectangle(5, 4);
calculateShapeArea(goodRect); // Calculated area: 20

const goodSquare = new GoodSquare(5);
calculateShapeArea(goodSquare); // Calculated area: 25

// 이 경우 Square는 Rectangle의 서브타입이 아니므로 LSP를 위반할 여지가 없습니다.
// 대신 두 클래스 모두 Shape라는 공통의 '추상화'에 의존합니다.
// 즉, '도형은 넓이를 계산할 수 있다'는 추상적인 계약을 따릅니다.

// DIP 추가 예시: 로깅 시스템 (고수준 모듈이 저수준 모듈에 의존하지 않도록)
// Logger 인터페이스 (추상화) 역할을 하는 클래스
class Logger {
    log(message) { throw new Error("log() must be implemented."); }
    error(message) { throw new Error("error() must be implemented."); }
}

// 저수준 모듈 (구체적인 구현)
class ConsoleLogger extends Logger {
    log(message) { console.log(`[INFO] ${message}`); }
    error(message) { console.error(`[ERROR] ${message}`); }
}

class FileLogger extends Logger {
    log(message) { console.log(`[FILE LOG] ${message}`); /* 파일에 쓰는 로직 */ }
    error(message) { console.error(`[FILE ERROR] ${message}`); /* 파일에 에러 쓰는 로직 */ }
}

// 고수준 모듈: PaymentProcessor는 Logger 인터페이스에 의존합니다.
class PaymentProcessor {
    constructor(logger) { // 생성자 주입
        if (!(logger instanceof Logger)) { // 런타임 타입 체크 (JS의 한계)
            throw new Error("Logger must be an instance of Logger abstraction.");
        }
        this.logger = logger;
    }

    processPayment(amount) {
        this.logger.log(`결제 처리 시작: ${amount}원`);
        try {
            // 결제 처리 로직...
            if (amount < 0) {
                throw new Error("Invalid amount");
            }
            this.logger.log(`결제 완료: ${amount}원`);
        } catch (e) {
            this.logger.error(`결제 실패: ${e.message}`);
        }
    }
}

console.log("\n--- DIP 적용 예제 ---");
const consoleLogger = new ConsoleLogger();
const fileLogger = new FileLogger();

const paymentProcessorWithConsole = new PaymentProcessor(consoleLogger);
paymentProcessorWithConsole.processPayment(10000);
paymentProcessorWithConsole.processPayment(-500);

console.log("-" * 20);

const paymentProcessorWithFile = new PaymentProcessor(fileLogger);
paymentProcessorWithFile.processPayment(25000);

4. 실무 적용 사례

SOLID 원칙은 단순히 이론적인 개념이 아니라, 실제 소프트웨어 개발의 모든 단계에서 빛을 발합니다.

  1. 프레임워크 및 라이브러리 설계: 스프링, 리액트 등 잘 설계된 대부분의 프레임워크와 라이브러리는 SOLID 원칙을 철저히 따릅니다. 예를 들어, 스프링의 의존성 주입(DI) 컨테이너는 DIP를 기반으로 합니다. 개발자가 인터페이스에 의존하도록 유도함으로써 유연한 확장을 가능하게 합니다.
  2. 마이크로서비스 아키텍처: 각 마이크로서비스는 특정 비즈니스 도메인에 대한 '단일 책임'을 가집니다 (SRP). 서비스 간 통신 시에도 구체적인 구현체보다는 API 인터페이스에 의존하도록 설계하여 DIP를 따릅니다.
  3. 플러그인/모듈 시스템: OCP의 대표적인 사례입니다. 웹 브라우저의 확장 기능이나 IDE의 플러그인처럼, 기존 시스템을 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계됩니다.
  4. **클린 아키텍처/헥사고날 아키