SOLID Principles

SOLID 원칙은 2000 년 Robert C. Martin 에 의해 체계화된 객체 지향 설계의 5 대 핵심 원칙이다. 단일 책임 (SRP), 개방/폐쇄 (OCP), 리스코프 치환 (LSP), 인터페이스 분리 (ISP), 의존성 역전 (DIP) 원칙으로 구성되어 있다. 이 원칙들은 코드의 결합도를 낮추고, 변경에 유연하며, 테스트와 유지보수를 쉽게 만들어준다. SOLID 는 현대 소프트웨어 개발에서 품질 높은 시스템 구축의 표준이자 필수 지침으로 널리 사용된다

핵심 개념

SOLID 원칙의 정의

핵심 목표

배경

SOLID 원칙은 2000 년 Robert C. Martin(Uncle Bob) 이 “Design Principles and Design Patterns” 논문에서 처음 제시했다. 이후 Michael Feathers 가 SOLID 라는 약어를 도입했다. 이 원칙들은 수십 년간의 객체 지향 프로그래밍 경험과 모범 사례를 바탕으로 체계화되었으며, 애자일 소프트웨어 개발과 클린 코드 철학의 기초가 되었다.

목적 및 필요성

  1. 코드 부패 (Code Rot) 방지: 시간이 지남에 따라 코드가 경직되고, 취약해지며, 이식성이 떨어지는 문제 해결
  2. 변경 용이성: 요구사항 변화에 신속하게 대응할 수 있는 유연한 구조 제공
  3. 협업 효율성: 여러 개발자가 동시에 작업할 수 있는 모듈화된 구조 구축
  4. 기술 부채 감소: 장기적인 유지보수 비용 절감

주요 기능 및 역할

특징

핵심 원칙

원칙명정의핵심 요소적용/구현 방법
SRP (단일 책임 원칙)Single Responsibility Principle클래스는 단 하나의 변경 이유만을 가져야 함하나의 액터 (Actor) 에 대한 책임책임의 성격과 변경 요인 기준으로 분리
OCP (개방/폐쇄 원칙)Open/Closed Principle확장에는 열려 있고, 수정에는 닫혀 있어야 함기존 코드 변경 없이 확장 가능추상화, 다형성, 상속 등의 활용
LSP (리스코프 치환 원칙)Liskov Substitution Principle하위 타입은 상위 타입으로 대체 가능해야 함행동의 일관성 보장사전 조건 강화 금지, 사후 조건 약화 금지
ISP (인터페이스 분리 원칙)Interface Segregation Principle클라이언트는 사용하지 않는 메서드에 의존하지 않아야 함작고 응집력 높은 인터페이스인터페이스 최소화, 역할 중심 인터페이스 분리
DIP (의존성 역전 원칙)Dependency Inversion Principle고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 함구체 구현보다 추상화에 의존의존성 주입 (DI), 인터페이스 기반 설계

SRP (Single Responsibility Principle)–단일 책임 원칙

항목내용
개념클래스는 하나의 변경 이유만 가져야 한다.
핵심 원칙하나의 클래스는 하나의 책임 (기능) 만을 가져야 하며, 하나의 행위만을 변경해야 한다.
설명여러 책임을 가지는 클래스는 기능이 늘어나며 결합도가 높아지고, 유지보수가 어려워진다.
연관성OCP(확장성), DIP(구조 분리) 와 강하게 연계됨.
잘된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 잘된 예시: 책임이 명확히 분리됨
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_salary(self):
        """급여 계산 로직만 담당"""
        return self.salary * 1.1

class EmployeeRepository:
    def save(self, employee):
        """데이터 저장만 담당"""
        # 데이터베이스 저장 로직
        print(f"Saving {employee.name} to database")

class EmployeeReportService:
    def generate_report(self, employee):
        """보고서 생성만 담당"""
        return f"Employee Report: {employee.name}, Salary: {employee.calculate_salary()}"

class EmailService:
    def send_notification(self, employee, message):
        """이메일 발송만 담당"""
        print(f"Sending email to {employee.name}: {message}")

무엇이 잘된 것인지:

잘못된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 잘못된 예시: 다중 책임 위반
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_salary(self):
        """급여 계산"""
        return self.salary * 1.1
    
    def save_to_database(self):
        """데이터베이스 저장 - SRP 위반!"""
        # 데이터 접근 로직
        print(f"Saving {self.name} to database")
    
    def generate_report(self):
        """보고서 생성 - SRP 위반!"""
        return f"Employee Report: {self.name}"
    
    def send_email_notification(self):
        """이메일 발송 - SRP 위반!"""
        print(f"Sending email to {self.name}")
    
    def print_payslip(self):
        """급여명세서 출력 - SRP 위반!"""
        print(f"Payslip for {self.name}: {self.calculate_salary()}")

무엇이 잘못되었는지:

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

항목내용
개념확장에는 열려 있고, 변경에는 닫혀 있어야 한다.
핵심 원칙기존 코드를 수정하지 않고도 기능을 확장할 수 있어야 한다.
설명새로운 요구사항이 생겨도 기존 클래스는 그대로 유지되어야 한다.
연관성LSP(상속), DIP(의존 역전) 와 함께 자주 적용됨.
잘된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 잘된 예시: 확장에 열려있고 수정에 닫혀있음
class Shape {
    calculateArea() {
        throw new Error("calculateArea must be implemented");
    }
}

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    
    calculateArea() {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    
    calculateArea() {
        return this.width * this.height;
    }
}

class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }
    
    calculateArea() {
        return 0.5 * this.base * this.height;
    }
}

class AreaCalculator {
    calculateTotalArea(shapes) {
        return shapes.reduce((total, shape) => {
            return total + shape.calculateArea();
        }, 0);
    }
}

무엇이 잘된 것인지:

잘못된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 잘못된 예시: 수정에 열려있어 OCP 위반
class AreaCalculator {
    calculateArea(shape) {
        let area = 0;
        
        if (shape.type === "circle") {
            area = Math.PI * shape.radius * shape.radius;
        } else if (shape.type === "rectangle") {
            area = shape.width * shape.height;
        } else if (shape.type === "triangle") {
            area = 0.5 * shape.base * shape.height;
        }
        // 새로운 도형이 추가될 때마다 이 부분을 수정해야 함!
        
        return area;
    }
}

class Circle {
    constructor(radius) {
        this.type = "circle";
        this.radius = radius;
    }
}

class Rectangle {
    constructor(width, height) {
        this.type = "rectangle";
        this.width = width;
        this.height = height;
    }
}

무엇이 잘못되었는지:

LSP (Liskov Substitution Principle)–리스코프 치환 원칙

항목내용
개념서브 클래스는 부모 클래스를 대체할 수 있어야 한다.
핵심 원칙하위 클래스는 상위 클래스의 행위를 그대로 사용할 수 있어야 하며, 기대를 깨면 안 된다.
설명다형성을 사용할 수 있어야 하며, “is-a” 관계를 보장해야 한다.
연관성OCP, DIP 의 구현을 위한 핵심 원칙 중 하나
잘된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 잘된 예시: LSP 준수
abstract class Bird {
    protected String name;
    
    public Bird(String name) {
        this.name = name;
    }
    
    public abstract void move();
    public abstract void makeSound();
}

abstract class FlyingBird extends Bird {
    public FlyingBird(String name) {
        super(name);
    }
    
    public abstract void fly();
}

class Eagle extends FlyingBird {
    public Eagle(String name) {
        super(name);
    }
    
    @Override
    public void move() {
        System.out.println(name + " soars through the sky");
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " screeches");
    }
    
    @Override
    public void fly() {
        System.out.println(name + " flies majestically");
    }
}

class Penguin extends Bird {
    public Penguin(String name) {
        super(name);
    }
    
    @Override
    public void move() {
        System.out.println(name + " waddles on ice");
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " makes penguin noises");
    }
    
    public void swim() {
        System.out.println(name + " swims underwater");
    }
}

무엇이 잘된 것인지:

잘못된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 잘못된 예시: LSP 위반
class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
    
    public int getWidth() { return width; }
    public int getHeight() { return height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 정사각형이므로 높이도 같이 설정
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height; // 정사각형이므로 너비도 같이 설정
        this.height = height;
    }
}

// 문제가 되는 클라이언트 코드
class GeometryTest {
    public static void testRectangle(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        
        // 예상: 20, 실제 Square일 경우: 16
        assert rectangle.getArea() == 20; // Square일 때 실패!
    }
}

무엇이 잘못되었는지:

ISP (Interface Segregation Principle)–인터페이스 분리 원칙

항목내용
개념하나의 일반적인 인터페이스보다는 여러 개의 구체적인 인터페이스가 낫다.
핵심 원칙클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
설명인터페이스가 너무 많아도 문제지만, 너무 커도 문제 → 역할 중심 분할 필요
연관성DIP, SRP 와 함께 인터페이스 설계 시 활용
잘된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 잘된 예시: 인터페이스가 적절히 분리됨
interface Printable {
    void print(String document);
}

interface Scannable {
    void scan(String source);
}

interface Faxable {
    void fax(String document, String number);
}

// 다기능 프린터는 모든 기능 구현
class AllInOnePrinter implements Printable, Scannable, Faxable {
    @Override
    public void print(String document) {
        System.out.println("Printing: " + document);
    }
    
    @Override
    public void scan(String source) {
        System.out.println("Scanning: " + source);
    }
    
    @Override
    public void fax(String document, String number) {
        System.out.println("Faxing " + document + " to " + number);
    }
}

// 단순 프린터는 필요한 기능만 구현
class SimplePrinter implements Printable {
    @Override
    public void print(String document) {
        System.out.println("Simple printing: " + document);
    }
}

// 클라이언트는 필요한 인터페이스만 의존
class PrintService {
    private Printable printer;
    
    public PrintService(Printable printer) {
        this.printer = printer;
    }
    
    public void printDocument(String document) {
        printer.print(document);
    }
}

무엇이 잘된 것인지:

잘못된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 잘못된 예시: 거대한 인터페이스로 ISP 위반
interface MultiFunctionDevice {
    void print(String document);
    void scan(String source);
    void fax(String document, String number);
    void copy(String source);
    void email(String document, String address);
}

class SimplePrinter implements MultiFunctionDevice {
    @Override
    public void print(String document) {
        System.out.println("Printing: " + document);
    }
    
    // 사용하지 않는 기능들을 억지로 구현해야 함
    @Override
    public void scan(String source) {
        throw new UnsupportedOperationException("Scan not supported");
    }
    
    @Override
    public void fax(String document, String number) {
        throw new UnsupportedOperationException("Fax not supported");
    }
    
    @Override
    public void copy(String source) {
        throw new UnsupportedOperationException("Copy not supported");
    }
    
    @Override
    public void email(String document, String address) {
        throw new UnsupportedOperationException("Email not supported");
    }
}

class OldFaxMachine implements MultiFunctionDevice {
    @Override
    public void print(String document) {
        throw new UnsupportedOperationException("Print not supported");
    }
    
    @Override
    public void scan(String source) {
        throw new UnsupportedOperationException("Scan not supported");
    }
    
    @Override
    public void fax(String document, String number) {
        System.out.println("Faxing: " + document + " to " + number);
    }
    
    @Override
    public void copy(String source) {
        throw new UnsupportedOperationException("Copy not supported");
    }
    
    @Override
    public void email(String document, String address) {
        throw new UnsupportedOperationException("Email not supported");
    }
}

무엇이 잘못되었는지:

DIP (Dependency Inversion Principle)–의존성 역전 원칙

항목내용
개념고수준 모듈은 저수준 모듈에 의존하지 말고, 둘 다 추상화에 의존해야 한다.
핵심 원칙구현이 아닌 인터페이스 (추상) 에 의존해야 한다.
설명유연성과 테스트 용이성을 위해 설계 계층 간 결합도를 낮춤
연관성모든 원칙과 결합되어 고수준 모듈 설계에 기초가 되는 원칙
잘된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 잘된 예시: 추상화에 의존하는 DIP 준수
from abc import ABC, abstractmethod

# 추상화 정의
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

class NotificationService(ABC):
    @abstractmethod
    def send_notification(self, message: str, recipient: str) -> bool:
        pass

# 구체적인 구현들
class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via credit card")
        return True

class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via PayPal")
        return True

class EmailNotification(NotificationService):
    def send_notification(self, message: str, recipient: str) -> bool:
        print(f"Email to {recipient}: {message}")
        return True

class SMSNotification(NotificationService):
    def send_notification(self, message: str, recipient: str) -> bool:
        print(f"SMS to {recipient}: {message}")
        return True

# 상위 수준 모듈 - 추상화에만 의존
class OrderService:
    def __init__(self, payment_processor: PaymentProcessor, 
                 notification_service: NotificationService):
        self.payment_processor = payment_processor
        self.notification_service = notification_service
    
    def process_order(self, amount: float, customer: str):
        if self.payment_processor.process_payment(amount):
            self.notification_service.send_notification(
                f"Order confirmed for ${amount}", customer
            )
            return True
        return False

# 사용 예시
def main():
    # 의존성 주입을 통한 구성
    credit_processor = CreditCardProcessor()
    email_service = EmailNotification()
    
    order_service = OrderService(credit_processor, email_service)
    order_service.process_order(100.0, "customer@email.com")
    
    # 다른 구현으로 쉽게 교체 가능
    paypal_processor = PayPalProcessor()
    sms_service = SMSNotification()
    
    order_service2 = OrderService(paypal_processor, sms_service)
    order_service2.process_order(50.0, "+1234567890")

무엇이 잘된 것인지:

잘못된 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 잘못된 예시: 구체적 구현에 직접 의존하는 DIP 위반
class OrderService:
    def __init__(self):
        # 구체적인 클래스에 직접 의존 - DIP 위반!
        self.payment_processor = CreditCardProcessor()
        self.notification_service = EmailNotification()
    
    def process_order(self, amount: float, customer: str):
        if self.payment_processor.process_payment(amount):
            self.notification_service.send_notification(
                f"Order confirmed for ${amount}", customer
            )
            return True
        return False

class CreditCardProcessor:
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via credit card")
        return True

class EmailNotification:
    def send_notification(self, message: str, recipient: str) -> bool:
        print(f"Email to {recipient}: {message}")
        return True

# 새로운 결제 방식 추가 시 OrderService 수정 필요
class PayPalProcessor:
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via PayPal")
        return True

# OrderService를 수정해야 함 - OCP도 위반
class OrderServiceWithPayPal:
    def __init__(self, use_paypal=False):
        if use_paypal:
            self.payment_processor = PayPalProcessor()
        else:
            self.payment_processor = CreditCardProcessor()
        self.notification_service = EmailNotification()

무엇이 잘못되었는지:

전체 원칙들의 상호작용

SOLID 원칙들은 서로 밀접하게 연관되어 있다:

  1. SRP는 다른 모든 원칙의 기초
  2. OCP는 LSP 와 DIP 를 통해 달성됨
  3. LSP는 OCP 를 안전하게 만듦
  4. ISP는 SRP 의 인터페이스 버전
  5. DIP는 OCP 와 LSP 의 조합으로 간주됨

이러한 원칙들을 함께 적용하면 유지보수 가능하고, 확장 가능하며, 테스트하기 쉬운 소프트웨어를 만들 수 있다.

sequenceDiagram
    participant C as Client
    participant A as Abstraction
    participant I1 as Implementation1
    participant I2 as Implementation2
    
    Note over C,I2: 의존성 역전 원칙 적용
    
    C->>A: 추상화를 통한 접근
    A->>I1: 구체적 구현 호출
    I1-->>A: 결과 반환
    A-->>C: 결과 전달
    
    Note over C,I2: 구현체 교체 (개방/폐쇄 원칙)
    
    C->>A: 동일한 인터페이스 사용
    A->>I2: 새로운 구현체 호출
    I2-->>A: 결과 반환
    A-->>C: 결과 전달

구조조

classDiagram
    class Client {
        -service: IService
        +doSomething()
    }
    
    class IService {
        <<interface>>
        +process()
    }
    
    class ServiceA {
        +process()
    }
    
    class ServiceB {
        +process()
    }
    
    class DIContainer {
        +register(interface, implementation)
        +resolve(interface)
    }
    
    Client --> IService : depends on abstraction
    ServiceA ..|> IService : implements
    ServiceB ..|> IService : implements
    DIContainer --> ServiceA : creates
    DIContainer --> ServiceB : creates
    Client --> DIContainer : gets dependency

장점과 단점

구분항목설명
✅ 장점유지보수성 향상코드 변경 시 영향 범위가 제한되어 안전한 수정 가능
확장성 보장기존 코드 수정 없이 새로운 기능 추가 가능
재사용성 증대모듈화된 설계로 다른 프로젝트에서 재사용 용이
테스트 용이성의존성 분리로 단위 테스트 작성이 쉬워짐
협업 효율성명확한 인터페이스로 팀 작업 시 충돌 최소화
코드 가독성단일 책임으로 각 클래스의 역할이 명확함
⚠ 단점초기 복잡성 증가설계 단계에서 더 많은 시간과 노력 필요
과도한 추상화 위험불필요한 인터페이스와 클래스 생성으로 복잡도 증가
성능 오버헤드추상화 계층으로 인한 간접 호출 비용
학습 곡선개발팀의 이해와 숙련도 필요
과잉 엔지니어링간단한 기능에 과도한 설계 패턴 적용 위험

도전 과제

과제설명해결 방안
과도한 추상화 / Over-engineering간단한 기능에도 불필요한 인터페이스 및 계층 도입으로 구조 복잡도 증가YAGNI 원칙 적용, 책임 단위 기준 설계, 점진적 리팩터링
LSP (리스코프 치환 원칙) 위반 탐지 어려움위반 여부가 런타임까지 드러나지 않아 테스트 어려움계약 기반 설계 (Contract-based Design), 단위 테스트 강화
DIP 구현의 복잡성DI 컨테이너가 없을 경우 수동 구현 부담Spring 등 DI 프레임워크 활용, 기본 생성자 주입 전략 적용
레거시 코드 적용의 어려움기존 코드에 SOLID 원칙 적용 시 대규모 리팩터링 필요스트랭글러 패턴 적용, 자동화 테스트 기반 점진적 리팩터링
성능과 설계 원칙 간 균형원칙 적용으로 인한 호출 비용 증가, 추상화 계층의 성능 저하 가능성성능 크리티컬 영역 식별 후 최적화, 프로파일링 기반 성능 측정
팀 간 일관성 부족팀원 간 SOLID 원칙 이해도 및 적용 방식 차이아키텍처 가이드 정립, 코드 리뷰/교육/페어 프로그래밍 통한 공유

실무 적용 예시

적용 분야적용 원칙구체적 사례기대 효과
웹 애플리케이션SRP + DIP + OCP컨트롤러, 서비스, 레포지토리 계층 분리계층별 책임 명확화, 유연한 확장
결제 시스템OCP + DIP + ISP결제 방식별 인터페이스 설계 (신용카드, 간편결제 등)새로운 결제 수단 추가 용이
로깅 시스템ISP + DIP로그 수준 (INFO, WARN, ERROR) 별 인터페이스 분리필요한 로깅 기능만 의존, 유연한 구성
ETL 데이터 처리SRP + OCP추출, 변환, 적재 각 단계별 클래스 분리모듈별 독립적 수정 가능, 테스트 용이
게임 엔진 구조LSP + OCP다양한 게임 오브젝트가 동일 인터페이스 상속 및 교체다양한 객체 유형 유연하게 지원
마이크로서비스/API전체 SOLID 원칙 적용API Gateway + 각 서비스의 독립적 책임 및 DI 적용서비스 독립성 강화, 유지보수성과 확장성 확보
MVVM 패턴 (Android)SRP + DIPViewModel → Repository 인터페이스 의존 구조단일 책임과 의존성 역전을 통한 테스트 용이
알림 시스템DIP + DI 적용Email, SMS, Push Notification 분리테스트 및 구현체 교체 유연성
주문 시스템SRP 적용주문 처리 로직과 결제/배송 로직 분리단일 책임 기반 확장 및 변경 용이

활용 사례

사례 1: 전자상거래 플랫폼에서 주문, 결제, 알림 시스템을 SOLID 원칙에 따라 설계

시스템 구성:

시스템 구성 다이어그램

classDiagram
    class OrderService {
        +processOrder()
    }
    class PaymentService {
        >
        +pay()
    }
    class KakaoPayService {
        +pay()
    }
    class NaverPayService {
        +pay()
    }
    class NotificationService {
        >
        +notify()
    }
    class EmailNotification {
        +notify()
    }
    class SMSNotification {
        +notify()
    }
    OrderService --> PaymentService
    KakaoPayService ..|> PaymentService
    NaverPayService ..|> PaymentService
    OrderService --> NotificationService
    EmailNotification ..|> NotificationService
    SMSNotification ..|> NotificationService

Workflow:

  1. 주문 발생 → 주문 서비스 처리 (SRP)
  2. 결제 요청 → 결제 서비스 인터페이스 통해 다양한 결제 방식 확장 (OCP, ISP)
  3. 알림 발송 → 알림 서비스 인터페이스로 다양한 알림 방식 확장 (DIP)

역할:

사례 2: 전자결제 시스템 리팩토링

시나리오: OrderService 가 직접 여러 결제 방법 (Card, KakaoPay, NaverPay) 에 의존 → SRP, DIP, OCP 위반

SOLID 적용 후 구조:

구조:

classDiagram
    class OrderService {
        -IPaymentProcessor processor
        +processOrder()
    }
    class IPaymentProcessor {
        <<interface>>
        +processPayment()
    }
    class CardProcessor {
        +processPayment()
    }
    class KakaoPayProcessor {
        +processPayment()
    }

    OrderService --> IPaymentProcessor
    CardProcessor ..|> IPaymentProcessor
    KakaoPayProcessor ..|> IPaymentProcessor

워크플로우:

  1. 사용자가 결제 요청 → OrderService
  2. OrderService 는 processor 에 위임
  3. Processor 는 실제 결제 처리
  4. 결과 반환

사례 3: E-commerce 주문 처리 시스템

시나리오: 온라인 쇼핑몰에서 주문 처리, 결제, 배송, 알림 등을 통합 관리하는 시스템

시스템 구성:

graph TB
    subgraph "Presentation Layer"
        C[OrderController]
    end
    
    subgraph "Business Layer"
        OS[OrderService]
        PS[PaymentService]
        NS[NotificationService]
        IS[InventoryService]
    end
    
    subgraph "Data Layer"
        OR[OrderRepository]
        PR[PaymentRepository]
    end
    
    subgraph "External Services"
        PG[PaymentGateway]
        ES[EmailService]
        SS[SMSService]
    end
    
    C --> OS
    OS --> PS
    OS --> NS
    OS --> IS
    OS --> OR
    PS --> PR
    PS --> PG
    NS --> ES
    NS --> SS

SOLID 원칙 적용:

  1. SRP 적용: 각 서비스가 단일 책임을 가짐

    • OrderService: 주문 처리 로직만 담당
    • PaymentService: 결제 처리만 담당
    • NotificationService: 알림 발송만 담당
  2. OCP 적용: 새로운 결제 방식이나 알림 채널 추가 시 기존 코드 수정 없이 확장

    1
    2
    3
    4
    5
    6
    
    // 새로운 결제 방식 추가
    public class CryptocurrencyPayment implements PaymentProcessor {
        public PaymentResult process(PaymentRequest request) {
            // 암호화폐 결제 로직
        }
    }
    
  3. LSP 적용: 모든 PaymentProcessor 구현체는 동일한 방식으로 동작

  4. ISP 적용: NotificationService 는 필요한 알림 방식만 의존

  5. DIP 적용: 모든 서비스는 인터페이스에 의존

Workflow:

sequenceDiagram
    participant Client
    participant OrderController
    participant OrderService
    participant PaymentService
    participant NotificationService
    participant PaymentGateway
    
    Client->>OrderController: 주문 요청
    OrderController->>OrderService: 주문 처리
    OrderService->>PaymentService: 결제 처리
    PaymentService->>PaymentGateway: 결제 게이트웨이 호출
    PaymentGateway-->>PaymentService: 결제 결과
    PaymentService-->>OrderService: 결제 결과
    OrderService->>NotificationService: 알림 발송
    NotificationService-->>OrderService: 알림 완료
    OrderService-->>OrderController: 주문 완료
    OrderController-->>Client: 응답

역할 분담:

실무에서 효과적으로 적용하기 위한 고려사항 및 권장 사항

구분고려사항설명권장 사항
설계 단계책임 경계 설정요구사항 기반으로 책임과 역할을 명확히 구분도메인 모델링을 병행하고, 점진적으로 설계
인터페이스 중심 설계 (IDD)구현보다 인터페이스부터 먼저 정의실제로 변경 가능성이 있는 영역만 추상화
과도한 추상화 방지구조가 불필요하게 복잡해질 수 있음기능 중심의 단순한 추상화 적용, YAGNI 원칙 준수
구현 단계DIP 및 DI 전략 수립구현체 교체 가능성을 염두에 둔 구조 설계생성자 주입 우선, 필요 시 세터/필드 주입 보조
테스트 단계테스트 전략 정립인터페이스 기반 테스트 구성 용이Mock, Stub 등 테스트 더블 활용, 실제 객체와의 균형 유지
자동화 테스트 확보구조 변경 시 회귀 오류 방지단위/통합 테스트 모두 작성
리팩토링주기적 리팩토링초기 설계의 한계를 점진적으로 개선작은 단위로 반복적 개선, 코드 냄새 탐지 도구 활용
팀 협업팀 내 합의 및 표준화개발자 간 설계 원칙과 구조 이해도 차이 해소 필요아키텍처 가이드 작성, 코드 리뷰와 교육으로 지속적 정렬
문서화설계 의도 및 제약사항 명시코드로 드러나지 않는 의사결정을 공유해야 할 경우변경 가능성 높은 로직이나 정책은 문서화, 과도한 문서화는 지양
적용 범위변화 가능성이 높은 영역부터 적용시스템 전체에 일괄 적용하기보단, 중요 영역 중심으로 적용 시작도메인 복잡성/변화율 기준으로 적용 우선순위 결정
성능 고려추상화와 DI 의 성능 영향런타임 성능에 미치는 영향이 크지 않지만 크리티컬 영역에선 주의 필요프로파일링을 통해 성능 민감 코드에서는 최적화 적용

최적화하기 위한 고려사항 및 주의할 점

구분고려사항주의할 점권장 사항
메모리 사용객체 생성 비용과 재사용성과도한 객체 생성은 GC (Garbage Collection) 부하싱글톤 (Singleton), 객체 풀링 (Object Pooling) 적극 활용
호출 경로추상화 계층의 호출 깊이간접 호출로 인해 호출 스택 깊어질 수 있음성능 민감 영역은 직접 호출 또는 최소 추상화 적용
인터페이스 호출가상 함수 호출 비용JIT 최적화 (인라이닝 등) 에 제약 가능구조적으로 인라인 최적화 유도
DI 전략DI 컨테이너 초기화 및 주입 비용런타임 의존성 해결에 따른 성능 저하Lazy Initialization, 컴파일 타임 DI (예: Dagger) 고려
캐싱 전략추상화된 접근에 따른 캐싱 난이도캐시 무효화 또는 갱신 로직의 복잡성계층형 캐싱 전략 (서비스, DAO 등) 분리 적용
객체 수 증가클래스 분리 및 구성 요소 증가설계 복잡성이 성능에 영향을 미칠 수 있음SRP 기반의 관심사 분리, 필요 최소 단위로 클래스 구성
코드 복잡성추상화와 최적화 간의 균형KISS(Keep It Simple, Stupid) 원칙 무시 위험단순하고 명확한 추상화 설계 유지
프로파일링병목 구간 식별의 정확성 필요추측 기반 최적화는 오히려 비효율적일 수 있음성능 프로파일링 도구 활용 및 테스트 자동화
초기 설정 시간프레임워크 초기화 비용구동 시간 및 리소스 점유 증가profile 기반 로딩 제어 (Spring Profile 등), Lazy 주입

주제와 관련하여 주목할 내용

분류항목설명
객체지향 설계SOLID 원칙유지보수성, 확장성, 유연성을 높이는 핵심 원칙들의 집합
SRP (단일 책임 원칙)클래스는 하나의 책임만 가져야 하며, 하나의 변경 이유만 가져야 함
OCP (개방/폐쇄 원칙)기존 코드를 수정하지 않고 기능을 확장 가능해야 함
LSP (리스코프 치환 원칙)하위 타입은 상위 타입을 완전히 대체할 수 있어야 함
ISP (인터페이스 분리 원칙)클라이언트에 맞게 인터페이스를 분리하여 불필요한 의존을 줄임
DIP (의존성 역전 원칙)고수준/저수준 모듈 모두 추상화에 의존하고, DI 로 구현체를 주입
아키텍처 적용마이크로서비스 아키텍처SOLID 원칙이 서비스 경계, API 분리, 인터페이스 안정성에 적용됨
도메인 주도 설계 (DDD)바운디드 컨텍스트 및 애그리게이트 설계 시 SOLID 원칙과 함께 사용
클린 아키텍처의존성 방향을 내부로 제한하는 구조에서 SOLID 원칙과 잘 통합됨
프로그래밍 언어함수형 프로그래밍 (FP)함수 단위 책임 분리, 불변성 유지, 모듈 수준의 OCP/LSP 적용 가능
동적 타이핑 언어Python/JavaScript 에서 덕 타이핑 기반 DIP, ISP 실현 가능
기술 도구/도입DI 컨테이너Spring, Angular,.NET Core 등에서 DIP 를 쉽게 적용하는 인프라 제공
코드 분석 도구SonarQube, ESLint 등으로 SOLID 위반 탐지 및 리팩토링 포인트 확인 가능
성능 최적화컴파일 타임 최적화제네릭, 템플릿 등 정적 타입 기능으로 런타임 오버헤드 최소화
메모리 효율성객체 풀링, Flyweight 패턴 등을 통한 메모리 사용량 감소

하위 주제로 추가 학습 내용

카테고리주제설명
SOLID 원칙SRP (단일 책임 원칙)클래스는 하나의 책임만 가지며, 변경 이유는 하나뿐이어야 함
OCP (개방/폐쇄 원칙)기능 확장에는 열려 있고, 코드 수정에는 닫혀 있어야 함
LSP (리스코프 치환 원칙)하위 클래스는 상위 클래스를 대체할 수 있어야 함
ISP (인터페이스 분리 원칙)클라이언트에 맞게 인터페이스를 분리하여 불필요한 의존성 제거
DIP (의존성 역전 원칙)고수준, 저수준 모듈이 추상화에 의존하도록 하며, DI 로 구현체를 주입
설계 원칙DRY, KISS, YAGNI중복 제거, 단순 설계, 필요 없는 기능 제거 등의 실용적 개발 원칙
디자인 패턴생성 패턴 (Factory 등)객체 생성 책임을 추상화하여 유연성 확보
구조 패턴 (Adapter 등)클래스/객체 간 구조적 결합을 느슨하게 만듦
행위 패턴 (Strategy 등)책임 위임과 실행 알고리즘의 유연한 교체
아키텍처 스타일레이어드 아키텍처계층 간 의존 방향을 위에서 아래로 유지하여 책임 분리
헥사고날 아키텍처내부 도메인과 외부 어댑터 간의 명확한 경계 설정
이벤트 드리븐 아키텍처이벤트를 중심으로 비동기 메시징 구조 설계
의존성 관리DI, IoC, 서비스 로케이터객체 생성 책임 분리와 결합도 감소를 위한 의존성 관리 전략
테스트 전략단위 테스트DI 기반으로 각 구성요소를 독립적으로 테스트 가능
통합 테스트실제 계층/구성요소 간 연동 테스트
계약 테스트API 및 인터페이스의 계약 위반 여부 검증
유지보수 전략리팩토링SOLID 기반 구조 개선 및 가독성, 확장성 향상
품질 보증테스트 자동화CI/CD 환경에서 자동화된 검증과 회귀 방지

관련 분야 추가 학습 내용

관련 분야주제설명
소프트웨어 공학코드 품질 메트릭응집도, 결합도, 순환 복잡도 측정 방법
리팩터링 기법코드 냄새 제거와 구조 개선 방법론
기술 부채 관리SOLID 위반으로 인한 기술 부채 식별과 해결 전략
개발 방법론애자일 개발반복 개발에서 SOLID 원칙 점진적 적용
TDD (테스트 주도 개발)테스트 우선 설계와 SOLID 원칙의 시너지
BDD (행위 주도 개발)비즈니스 요구사항과 기술 설계의 연결
성능 엔지니어링프로파일링SOLID 적용 시 성능 영향 측정 및 최적화
메모리 관리객체 생명주기와 가비지 컬렉션 최적화
보안보안 설계 원칙최소 권한 원칙과 인터페이스 분리의 연관성
의존성 보안써드파티 라이브러리 의존성 관리와 보안 취약점

용어 정리

용어설명
추상화 (Abstraction)복잡한 구현 세부사항을 숨기고 핵심 기능 또는 동작만 외부에 노출하는 설계 기법
의존성 주입 (DI, Dependency Injection)객체가 의존하는 다른 객체를 외부에서 주입받아 결합도를 낮추는 설계 기법
결합도 (Coupling)모듈 또는 클래스 간의 상호 의존성 정도. 낮을수록 모듈성이 높음
응집도 (Cohesion)모듈 내부 구성요소들이 하나의 책임에 집중하는 정도. 높을수록 유지보수에 유리함
리팩토링 (Refactoring)기능 변경 없이 코드 구조와 가독성, 유지보수성을 향상시키는 개선 작업
액터 (Actor)SRP(단일 책임 원칙) 에서 클래스의 변경을 요구하는 외부 사용자 또는 이해관계자
다형성 (Polymorphism)동일한 인터페이스로 다양한 구현체를 동적으로 사용할 수 있는 객체지향 특성
코드 냄새 (Code Smell)향후 버그나 유지보수 비용 증가를 유발할 수 있는 비정상적 또는 비효율적인 코드 구조
인버전 오브 컨트롤 (IoC)객체의 제어 권한 (생성, 관리 등) 을 개발자가 아닌 프레임워크나 컨테이너에 위임하는 원칙
테스트 더블 (Test Double)테스트 시 실제 객체 대신 사용하는 가짜 객체. Mock, Stub, Spy, Fake 등을 포함
SOLID객체지향 설계의 다섯 가지 핵심 원칙을 총칭 (SRP, OCP, LSP, ISP, DIP)
SRP (Single Responsibility Principle)단일 책임 원칙. 클래스는 하나의 책임만 가져야 하며 하나의 액터에만 영향을 받아야 함
OCP (Open/Closed Principle)확장에는 열려 있고, 기존 코드 변경에는 닫혀 있어야 하는 개방/폐쇄 원칙
LSP (Liskov Substitution Principle)리스코프 치환 원칙. 하위 클래스는 상위 클래스의 기능을 대체 가능해야 함
ISP (Interface Segregation Principle)인터페이스 분리 원칙. 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리
DIP (Dependency Inversion Principle)의존성 역전 원칙. 고수준/저수준 모듈이 추상화에 의존하고, 구체 구현에는 의존하지 않음

참고 및 출처