PART 2 · 강의 1/4

SOLID 원칙
심화

객체지향 설계의 5가지 핵심 원칙을 깊이 있게 이해하고, AI 시대에 맞게 재해석합니다.

00

SOLID 원칙 개요

Robert C. Martin이 정립한 객체지향 설계의 5가지 핵심 원칙

S
O
L
I
D
SOLID의 목표

SOLID 원칙은 변경에 유연하고, 이해하기 쉬우며, 재사용 가능한 소프트웨어를 만들기 위한 가이드라인입니다. 각 원칙을 클릭하면 해당 섹션으로 이동합니다.

01

Single Responsibility Principle

단일 책임 원칙

S

단일 책임 원칙

클래스는 변경되어야 할 이유가 오직 하나뿐이어야 한다

"하나의 클래스는 하나의 책임만 가져야 한다"

더 정확히 말하면, 클래스를 변경해야 하는 이유(reason to change)가 오직 하나여야 합니다. 책임 = 변경의 이유입니다.

위반 사례
class UserService {
    createUser(data) { // 비즈니스 로직 }
    saveToDatabase(user) { // DB 접근 }
    sendEmail(user) { // 이메일 발송 }
    generateReport() { // 리포트 생성 }
    logActivity(action) { // 로깅 }
}
// 5가지 변경 이유가 존재!
준수 사례
class UserService {
    createUser(data) { // 비즈니스 로직만 }
}

class UserRepository {
    save(user) { // DB 접근만 }
}

class EmailService {
    send(to, content) { // 이메일만 }
}
// 각 클래스는 하나의 변경 이유만!
AI 시대 재해석

AI가 코드를 생성할 때, 명확하게 분리된 단일 책임 클래스일수록 더 정확한 코드를 생성합니다. "UserService에 이메일 기능 추가해줘"보다 "EmailService 만들어줘"가 AI에게 더 명확한 지시입니다. 또한, 테스트 코드 생성 시에도 책임이 분리된 클래스는 더 정확한 단위 테스트를 생성할 수 있습니다.

테스트 용이성
단일 책임은 테스트 범위가 명확해져 테스트 작성이 쉬워집니다.
변경 격리
한 기능 변경이 다른 기능에 영향을 주지 않습니다.
재사용성
작고 집중된 클래스는 다른 곳에서 재사용하기 쉽습니다.
02

Open/Closed Principle

개방-폐쇄 원칙

O

개방-폐쇄 원칙

확장에는 열려있고, 수정에는 닫혀있어야 한다

"기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다"

새로운 요구사항이 생겼을 때, 기존 코드를 수정하는 것이 아니라 새로운 코드를 추가하여 해결해야 합니다. 이는 추상화와 다형성을 통해 달성됩니다.

위반 사례
class PaymentProcessor {
    process(payment) {
        if (payment.type === 'credit') {
            // 신용카드 처리
        } else if (payment.type === 'paypal') {
            // PayPal 처리
        } else if (payment.type === 'crypto') {
            // 새 결제 수단마다 수정 필요!
        }
    }
}
준수 사례
interface PaymentMethod {
    process(amount): Result;
}

class CreditCardPayment implements PaymentMethod {
    process(amount) { /* ... */ }
}

class CryptoPayment implements PaymentMethod {
    process(amount) { /* 새로 추가만! */ }
}
// 기존 코드 수정 없이 확장 가능
AI 시대 재해석

OCP를 따르는 코드는 AI에게 "새로운 결제 수단 CryptoPayment 클래스 만들어줘"라고 요청할 수 있습니다. AI는 기존 인터페이스를 참조하여 일관된 구현체를 생성할 수 있고, 기존 코드를 건드리지 않아 버그 발생 가능성이 최소화됩니다. 플러그인 아키텍처의 기반이 됩니다.

03

Liskov Substitution Principle

리스코프 치환 원칙

L

리스코프 치환 원칙

서브타입은 기반 타입을 대체할 수 있어야 한다

"자식 클래스는 부모 클래스를 대체해도 프로그램이 정상 동작해야 한다"

상속 관계에서 자식 클래스는 부모 클래스의 행동 규약(계약)을 반드시 지켜야 합니다. IS-A 관계가 행동적으로도 성립해야 합니다.

위반 사례 (유명한 정사각형-직사각형 문제)
class Rectangle {
    setWidth(w) { this.width = w; }
    setHeight(h) { this.height = h; }
}

class Square extends Rectangle {
    setWidth(w) {
        this.width = w;
        this.height = w; // 부모와 다른 동작!
    }
}
// rect.setWidth(5); rect.setHeight(10);
// 직사각형: 면적 50, 정사각형: 면적 100 (예상과 다름!)
준수 사례
interface Shape {
    getArea(): number;
}

class Rectangle implements Shape {
    constructor(w, h) { ... }
    getArea() { return this.w * this.h; }
}

class Square implements Shape {
    constructor(side) { ... }
    getArea() { return this.side ** 2; }
}
// 상속 대신 공통 인터페이스 구현
AI 시대 재해석

LSP 위반은 AI가 탐지하기 어려운 미묘한 버그를 유발합니다. AI에게 "Rectangle을 상속받는 Square 만들어줘"라고 하면 형식적으로 맞는 코드를 생성하지만, 런타임에 예상치 못한 동작이 발생합니다. 상속보다 컴포지션을 우선하고, 명확한 인터페이스를 정의하는 것이 AI와 협업할 때 더 안전합니다.

04

Interface Segregation Principle

인터페이스 분리 원칙

I

인터페이스 분리 원칙

클라이언트가 사용하지 않는 인터페이스에 의존하면 안 된다

"하나의 범용 인터페이스보다 여러 개의 구체적인 인터페이스가 낫다"

클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강제해서는 안 됩니다. 인터페이스는 클라이언트의 필요에 맞게 분리되어야 합니다.

위반 사례 (뚱뚱한 인터페이스)
interface Worker {
    work();
    eat();
    sleep();
    attendMeeting();
    writeReport();
}

class Robot implements Worker {
    work() { /* OK */ }
    eat() { throw '로봇은 먹지 않음'; }
    sleep() { throw '로봇은 자지 않음'; }
    // 불필요한 메서드를 구현해야 함!
}
준수 사례 (역할별 분리)
interface Workable {
    work();
}

interface Eatable {
    eat();
}

interface Sleepable {
    sleep();
}

class Robot implements Workable {
    work() { /* 필요한 것만! */ }
}

class Human implements Workable, Eatable, Sleepable {
    // 필요한 인터페이스만 선택적으로 구현
}
AI 시대 재해석

AI에게 코드 생성을 요청할 때, 작고 명확한 인터페이스가 더 정확한 구현을 유도합니다. "Workable 인터페이스를 구현하는 Robot 클래스 만들어줘"는 모호함 없이 명확합니다. 또한, ISP를 따르면 Mock 객체 생성이 쉬워져 AI 기반 테스트 코드 생성에도 유리합니다.

05

Dependency Inversion Principle

의존성 역전 원칙

D

의존성 역전 원칙

추상화에 의존하라, 구체화에 의존하지 마라

"고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다."

구체적인 구현체가 아닌 인터페이스(추상화)에 의존함으로써, 모듈 간의 결합도를 낮추고 유연성을 높입니다.

위반 사례 (강한 결합)
class OrderService {
    private mysqlDb = new MySQLDatabase();
    private smtpEmail = new SMTPEmailSender();

    createOrder(order) {
        this.mysqlDb.save(order);
        this.smtpEmail.send(order.email);
    }
}
// MySQL, SMTP에 강하게 결합
// 테스트하려면 실제 DB/이메일 필요!
준수 사례 (의존성 주입)
class OrderService {
    constructor(
        private db: Database,        // 인터페이스
        private emailer: EmailSender // 인터페이스
    ) {}

    createOrder(order) {
        this.db.save(order);
        this.emailer.send(order.email);
    }
}
// 추상화에만 의존
// 테스트 시 Mock 객체 주입 가능!
AI 시대 재해석

DIP는 AI 시대에 가장 중요한 원칙입니다. 의존성이 역전된 코드는:
1) AI가 생성한 새로운 구현체를 기존 코드 수정 없이 교체 가능
2) 테스트 코드 자동 생성 시 Mock 객체 주입이 용이
3) AI 서비스 등 외부 의존성을 쉽게 교체 (GPT -> Claude 등)
인터페이스 중심 설계는 AI와의 협업에서 핵심입니다.

SUMMARY

핵심 요약

  • SRP - 클래스는 하나의 변경 이유만 가져야 한다. AI에게 명확한 단위로 요청 가능.
  • OCP - 기존 코드 수정 없이 확장 가능해야 한다. 플러그인 아키텍처의 기반.
  • LSP - 서브타입은 기반 타입을 완벽히 대체해야 한다. 상속보다 컴포지션 선호.
  • ISP - 작고 구체적인 인터페이스가 뚱뚱한 인터페이스보다 낫다.
  • DIP - 구체화가 아닌 추상화에 의존하라. AI 시대에 가장 중요한 원칙.
AI 시대의 SOLID

SOLID 원칙을 따르는 코드베이스는 AI 도구와의 협업에서 더 좋은 결과를 얻습니다. 명확한 책임 분리, 추상화 중심 설계, 느슨한 결합은 AI가 이해하고 확장하기 쉬운 코드의 특징입니다.