TL;DR

  • YAGNI 원칙과 Strategy Pattern의 핵심 개념과 사용 범위를 한눈에 정리
  • 등장 배경과 필요한 이유를 짚고 실무 적용 포인트를 연결
  • 주요 특징과 체크리스트를 빠르게 확인

1. 개념

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  YAGNI = "지금 필요하지 않은 기능은 만들지 마라"           │
│                                                             │
│  핵심:                                                     │
│  ├── 현재 요구사항에만 집중                                │
│  ├── "나중에 필요할 것 같은" 코드를 미리 작성하지 않음     │
│  └── 실제로 필요해지는 시점에 구현                         │
│                                                             │
│  출처: Extreme Programming (XP) - Kent Beck, Ron Jeffries  │
│  시기: 1990년대 후반                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. 배경

YAGNI 원칙과 Strategy Pattern이(가) 등장한 배경과 기존 한계를 정리한다.

3. 이유

이 주제를 이해하고 적용해야 하는 이유를 정리한다.

4. 특징

  • YAGNI 원칙과 과잉 설계 방지
  • Strategy Pattern의 런타임 교체 구조
  • OCP 준수와 if-else 제거
  • 리팩토링 타이밍(Rule of Three)
  • 유사 패턴(Template/State)과의 차이
  • 함수형 전략 구현 방식

5. 상세 내용

YAGNI 원칙과 Strategy Pattern

작성일: 2026-02-11 카테고리: Backend / 설계 원칙 / 디자인 패턴 포함 내용: YAGNI, Strategy Pattern, OCP, 과잉 설계, GoF, 행위 패턴, if-else 제거, 런타임 교체


1. YAGNI (You Aren’t Gonna Need It)

정의

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  YAGNI = "지금 필요하지 않은 기능은 만들지 마라"           │
│                                                             │
│  핵심:                                                     │
│  ├── 현재 요구사항에만 집중                                │
│  ├── "나중에 필요할 것 같은" 코드를 미리 작성하지 않음     │
│  └── 실제로 필요해지는 시점에 구현                         │
│                                                             │
│  출처: Extreme Programming (XP) - Kent Beck, Ron Jeffries  │
│  시기: 1990년대 후반                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

배경: 왜 이 원칙이 필요한가

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  문제: 과잉 설계 (Over-Engineering)                        │
│                                                             │
│  개발자의 흔한 실수:                                       │
│  ├── "나중에 다국어 지원할 수도 있으니 미리 i18n 넣자"    │
│  ├── "확장성을 위해 추상 계층 3개 더 만들자"              │
│  ├── "언젠가 NoSQL로 갈 수 있으니 Repository 추상화하자" │
│  └── "혹시 모르니 설정을 전부 config로 빼자"              │
│                                                             │
│  결과:                                                     │
│  ├── 코드 복잡도 ↑                                        │
│  ├── 개발 시간 낭비                                        │
│  ├── 디버깅 어려움 ↑                                      │
│  ├── 실제로 안 쓰이는 코드가 쌓임                         │
│  └── 정작 필요할 때 요구사항이 달라서 다시 짜야 함        │
│                                                             │
│  통계적으로:                                               │
│  ├── 미리 만든 기능의 ~80%는 실제로 사용되지 않음         │
│  └── 사용되더라도 요구사항이 바뀌어 재작성이 필요함       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

위반 vs 준수 예시

위반: 과잉 설계

// "나중에 결제 수단이 늘어날 수 있으니까" 미리 만듦
interface PaymentProcessor {
    fun process(amount: Money): PaymentResult
}

interface PaymentValidator {
    fun validate(payment: Payment): ValidationResult
}

interface PaymentNotifier {
    fun notify(result: PaymentResult)
}

abstract class AbstractPaymentService(
    private val processor: PaymentProcessor,
    private val validator: PaymentValidator,
    private val notifier: PaymentNotifier
) {
    abstract fun createPayment(): Payment
    // ... 현재 카드 결제만 하는데 3개 인터페이스 + 추상 클래스
}

준수: 지금 필요한 것만

// 현재 카드 결제만 필요 → 카드 결제만 구현
class CardPaymentService(
    private val cardApi: CardApi
) {
    fun pay(amount: Long): PaymentResult {
        return cardApi.charge(amount)
    }
}

// 나중에 계좌이체가 추가되면 그때 리팩토링

YAGNI와 관련 원칙들

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  함께 쓰이는 원칙들:                                       │
│                                                             │
│  YAGNI        "필요 없으면 만들지 마"                      │
│  KISS         "단순하게 유지해"                             │
│  DRY          "반복하지 마" (단, 과도한 DRY는 YAGNI 위반)  │
│                                                             │
│  주의할 점:                                                │
│  ├── YAGNI ≠ "설계를 안 해도 된다"                        │
│  ├── YAGNI ≠ "테스트를 안 짜도 된다"                      │
│  ├── YAGNI ≠ "리팩토링을 안 해도 된다"                    │
│  └── YAGNI = "추측으로 코드를 짜지 마라"                  │
│                                                             │
│  YAGNI가 적용 안 되는 경우:                                │
│  ├── 보안 (나중에 추가하면 늦음)                           │
│  ├── 로깅/모니터링 (나중에 넣으면 디버깅 불가)            │
│  └── 데이터 모델의 핵심 구조 (나중에 바꾸면 마이그레이션) │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. Strategy Pattern (전략 패턴)

정의

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Strategy Pattern = 알고리즘을 캡슐화하여                  │
│                     런타임에 교체 가능하게 만드는 패턴      │
│                                                             │
│  분류:                                                     │
│  ├── GoF (Gang of Four) 디자인 패턴                       │
│  ├── 행위(Behavioral) 패턴                                │
│  └── 1994년 출판 "Design Patterns" 책에서 정의            │
│                                                             │
│  핵심 아이디어:                                            │
│  "변하는 부분을 인터페이스로 분리하고,                     │
│   구현체를 갈아끼울 수 있게 만들어라"                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

배경: 왜 필요한가

if-else 지옥

// Before: 조건문이 계속 늘어남
class DiscountService {
    fun calculate(type: String, price: Long): Long {
        if (type == "VIP") {
            return price * 80 / 100
        } else if (type == "MEMBER") {
            return price * 90 / 100
        } else if (type == "STUDENT") {
            return price * 85 / 100
        } else if (type == "SENIOR") {
            return price * 75 / 100
        } else if (type == "EMPLOYEE") {
            return price * 70 / 100
        }
        // ... 할인 유형이 추가될 때마다 여기를 수정
        return price
    }
}
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  이 코드의 문제:                                           │
│                                                             │
│  1. OCP 위반 (Open-Closed Principle)                      │
│     새 할인 유형 추가 시 기존 코드를 수정해야 함           │
│                                                             │
│  2. 단일 책임 원칙 위반                                    │
│     하나의 메서드가 모든 할인 로직을 담당                   │
│                                                             │
│  3. 테스트 어려움                                          │
│     특정 할인만 테스트하기 힘듦                             │
│                                                             │
│  4. 런타임 교체 불가                                       │
│     상황에 따라 동적으로 전략을 바꿀 수 없음               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Strategy Pattern 적용

// 1. 전략 인터페이스 정의
interface DiscountStrategy {
    fun calculate(price: Long): Long
}

// 2. 각 전략을 별도 클래스로 구현
class VipDiscount : DiscountStrategy {
    override fun calculate(price: Long) = price * 80 / 100
}

class MemberDiscount : DiscountStrategy {
    override fun calculate(price: Long) = price * 90 / 100
}

class StudentDiscount : DiscountStrategy {
    override fun calculate(price: Long) = price * 85 / 100
}

class NoDiscount : DiscountStrategy {
    override fun calculate(price: Long) = price
}

// 3. Context 클래스: 전략을 사용하는 쪽
class DiscountService(
    private var strategy: DiscountStrategy
) {
    fun setStrategy(strategy: DiscountStrategy) {
        this.strategy = strategy
    }

    fun calculate(price: Long): Long {
        return strategy.calculate(price)
    }
}

// 4. 사용
val service = DiscountService(VipDiscount())
service.calculate(10000)  // 8000

service.setStrategy(StudentDiscount())  // 런타임에 교체
service.calculate(10000)  // 8500

구조

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Strategy Pattern 구조:                                    │
│                                                             │
│  ┌──────────┐        ┌──────────────────┐                  │
│  │ Context  │───────▶│ <<interface>>     │                  │
│  │          │        │ Strategy         │                  │
│  │ -strategy│        │                  │                  │
│  │ +execute()│       │ +algorithm()     │                  │
│  └──────────┘        └──────────────────┘                  │
│                          ▲    ▲    ▲                       │
│                          │    │    │                        │
│                 ┌────────┘    │    └────────┐               │
│                 │             │             │               │
│          ┌──────────┐ ┌──────────┐ ┌──────────┐            │
│          │StrategyA │ │StrategyB │ │StrategyC │            │
│          └──────────┘ └──────────┘ └──────────┘            │
│                                                             │
│  구성 요소:                                                │
│  ├── Strategy: 알고리즘의 인터페이스                      │
│  ├── ConcreteStrategy: 실제 알고리즘 구현체               │
│  └── Context: 전략을 사용하는 클라이언트                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

실무 예시: Spring에서의 Strategy Pattern

예시 1: 결제 수단 분기

// 전략 인터페이스
interface PaymentStrategy {
    fun pay(amount: Long): PaymentResult
    fun supports(method: PaymentMethod): Boolean
}

// 구현체들 (각각 Spring Bean)
@Component
class CardPayment(private val cardApi: CardApi) : PaymentStrategy {
    override fun pay(amount: Long) = cardApi.charge(amount)
    override fun supports(method: PaymentMethod) = method == PaymentMethod.CARD
}

@Component
class BankTransfer(private val bankApi: BankApi) : PaymentStrategy {
    override fun pay(amount: Long) = bankApi.transfer(amount)
    override fun supports(method: PaymentMethod) = method == PaymentMethod.BANK
}

// Context: Spring이 모든 전략을 주입
@Service
class PaymentService(
    private val strategies: List<PaymentStrategy>
) {
    fun pay(method: PaymentMethod, amount: Long): PaymentResult {
        val strategy = strategies.find { it.supports(method) }
            ?: throw IllegalArgumentException("지원하지 않는 결제 수단: $method")
        return strategy.pay(amount)
    }
}
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Spring에서의 장점:                                        │
│                                                             │
│  새 결제 수단 추가 시:                                     │
│  ├── 새 클래스 하나만 만들면 됨 (@Component)              │
│  ├── PaymentService는 수정 안 함 (OCP 준수)               │
│  ├── Spring이 자동으로 List에 주입                        │
│  └── 기존 코드 영향 없음                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

예시 2: 알림 발송

interface NotificationStrategy {
    fun send(recipient: String, message: String)
    val type: NotificationType
}

@Component
class EmailNotification(private val mailSender: MailSender) : NotificationStrategy {
    override fun send(recipient: String, message: String) {
        mailSender.send(recipient, message)
    }
    override val type = NotificationType.EMAIL
}

@Component
class SmsNotification(private val smsClient: SmsClient) : NotificationStrategy {
    override fun send(recipient: String, message: String) {
        smsClient.send(recipient, message)
    }
    override val type = NotificationType.SMS
}

@Component
class SlackNotification(private val slackClient: SlackClient) : NotificationStrategy {
    override fun send(recipient: String, message: String) {
        slackClient.post(recipient, message)
    }
    override val type = NotificationType.SLACK
}

// Map으로 주입하면 더 깔끔
@Service
class NotificationService(
    strategies: List<NotificationStrategy>
) {
    private val strategyMap = strategies.associateBy { it.type }

    fun notify(type: NotificationType, recipient: String, message: String) {
        strategyMap[type]?.send(recipient, message)
            ?: throw IllegalArgumentException("지원하지 않는 알림 타입: $type")
    }
}

3. YAGNI와 Strategy Pattern의 관계

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  언제 Strategy Pattern을 쓰는 것이 YAGNI 위반인가?        │
│                                                             │
│  YAGNI 위반 (과잉 설계):                                   │
│  ├── 결제 수단이 카드 하나뿐인데 Strategy 패턴 적용       │
│  ├── 알림이 이메일뿐인데 인터페이스 + 3개 구현체 준비     │
│  └── "나중에 추가될 수 있으니까" 라는 근거만으로 적용     │
│                                                             │
│  적절한 사용:                                              │
│  ├── 이미 분기가 3개 이상이고 계속 늘어나는 상황           │
│  ├── if-else가 반복되어 가독성이 떨어지는 상황             │
│  ├── 각 분기의 로직이 복잡하여 별도 테스트가 필요한 상황  │
│  └── 런타임에 동적으로 전략을 바꿔야 하는 요구사항 존재   │
│                                                             │
│  판단 기준:                                                │
│  "현재 if-else가 2개 이하 → 그냥 if-else로 놔둬라"       │
│  "현재 if-else가 3개 이상 + 더 늘어날 예정 → Strategy"    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

리팩토링 타이밍

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  올바른 진행 순서:                                          │
│                                                             │
│  1단계: 카드 결제만 → 그냥 구현                            │
│  class PaymentService {                                    │
│      fun pay(amount: Long) = cardApi.charge(amount)        │
│  }                                                         │
│                                                             │
│  2단계: 계좌이체 추가 → if-else로 분기                     │
│  fun pay(method: String, amount: Long) = when(method) {    │
│      "CARD" -> cardApi.charge(amount)                      │
│      "BANK" -> bankApi.transfer(amount)                    │
│  }                                                         │
│                                                             │
│  3단계: 페이 + 포인트 추가 요청 → Strategy 리팩토링       │
│  → 이 시점에서 패턴 적용 (YAGNI 준수)                     │
│                                                             │
│  핵심: "세 번째가 오면 추상화하라" (Rule of Three)         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4. Strategy vs 유사 패턴

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  패턴              │  목적              │  차이              │
│  ─────────────────┼────────────────────┼───────────────────│
│  Strategy         │  알고리즘 교체      │  런타임에 교체     │
│  Template Method  │  알고리즘 골격 고정 │  상속 기반         │
│  State            │  상태에 따른 행위   │  상태가 스스로 전이│
│  Factory          │  객체 생성 분기     │  생성에 초점       │
│                                                             │
│  Strategy vs State:                                        │
│  ├── Strategy: 클라이언트가 직접 전략을 선택/교체          │
│  └── State: 상태 객체가 스스로 다음 상태로 전이            │
│                                                             │
│  Strategy vs Template Method:                              │
│  ├── Strategy: 구성(Composition) = 인터페이스 + 주입      │
│  └── Template Method: 상속(Inheritance) = 부모 클래스 확장 │
│  → Strategy가 더 유연 (상속보다 구성을 선호)               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5. 함수형 스타일의 Strategy

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  현대 언어에서는 클래스 없이 함수로도 Strategy 구현 가능   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Kotlin: 함수 타입 활용

// 인터페이스 대신 함수 타입
typealias DiscountStrategy = (Long) -> Long

// 전략들을 함수로 정의
val vipDiscount: DiscountStrategy = { price -> price * 80 / 100 }
val memberDiscount: DiscountStrategy = { price -> price * 90 / 100 }
val noDiscount: DiscountStrategy = { price -> price }

// 사용
class DiscountService(private var strategy: DiscountStrategy) {
    fun calculate(price: Long) = strategy(price)
}

val service = DiscountService(vipDiscount)
service.calculate(10000)  // 8000

Python

from typing import Callable

# 전략 = 그냥 함수
def vip_discount(price: int) -> int:
    return price * 80 // 100

def member_discount(price: int) -> int:
    return price * 90 // 100

# 사용
def calculate(price: int, strategy: Callable[[int], int]) -> int:
    return strategy(price)

calculate(10000, vip_discount)  # 8000
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  함수형 vs 클래스 기반 Strategy:                           │
│                                                             │
│  함수형이 적합한 경우:                                     │
│  ├── 전략이 단순한 계산 로직일 때                          │
│  ├── 상태(필드)가 필요 없을 때                             │
│  └── 전략 수가 적고 변하지 않을 때                         │
│                                                             │
│  클래스가 적합한 경우:                                     │
│  ├── 전략마다 의존성(API 클라이언트 등)이 필요할 때        │
│  ├── Spring DI로 자동 관리하고 싶을 때                     │
│  ├── 전략 내부에 상태가 필요할 때                          │
│  └── 여러 메서드가 필요한 복합 전략일 때                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6. 정리

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  YAGNI 체크리스트:                                         │
│  □ 지금 당장 필요한 기능인가?                              │
│  □ "나중에 필요할 것 같아서"가 유일한 근거인가?            │
│  □ 추가한 추상화가 현재 복잡도를 줄여주는가?              │
│  □ 3번째 유사 케이스가 나타났는가? (Rule of Three)         │
│                                                             │
│  Strategy Pattern 체크리스트:                              │
│  □ 동일 목적의 분기(if-else/when)가 3개 이상인가?         │
│  □ 분기가 계속 늘어날 예정인가?                            │
│  □ 각 분기의 로직이 독립적으로 테스트 필요한가?            │
│  □ 런타임에 전략 교체가 필요한가?                          │
│                                                             │
│  조합 원칙:                                                │
│  "처음부터 Strategy를 쓰지 마라 (YAGNI)"                  │
│  "분기가 3개가 되면 Strategy로 리팩토링하라"               │
│  "추측이 아닌 현실의 요구사항에 반응하라"                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

관련 키워드

YAGNI, You Aren't Gonna Need It, Strategy Pattern, 전략 패턴, OCP, Open-Closed Principle, GoF, 디자인 패턴, 행위 패턴, if-else 제거, 런타임 교체, 과잉 설계, Over-Engineering, KISS, DRY, Rule of Three, Composition over Inheritance, Kent Beck, XP