커맨드 패턴: 요청을 객체로 캡슐화하는 디자인 패턴
실행 취소와 재실행을 지원하는 유연한 시스템 설계를 위한 커맨드 패턴 구현
전략 패턴(Strategy Pattern)은 알고리즘 군을 정의하고 각각을 캡슐화하여 교환 가능하게 만드는 행위 디자인 패턴입니다. 이 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있으며, 런타임에 객체의 행위를 동적으로 변경할 수 있습니다.
이 글에서는 전략 패턴의 구조, 구현 방법, 실전 사용 사례, 그리고 다른 패턴과의 비교를 상세히 다룹니다.
전략 패턴 없이 여러 알고리즘을 처리하려면 조건문을 사용해야 하며, 이는 여러 문제를 야기합니다.
문제 상황:
class OrderProcessor {
fun processPayment(amount: Double, paymentType: String) {
when (paymentType) {
"CREDIT_CARD" -> {
println("신용카드로 $amount 결제 처리")
// 신용카드 검증 로직
// 신용카드 결제 API 호출
// 영수증 발급
}
"PAYPAL" -> {
println("PayPal로 $amount 결제 처리")
// PayPal 인증 로직
// PayPal API 호출
// 확인 이메일 발송
}
"CRYPTO" -> {
println("암호화폐로 $amount 결제 처리")
// 지갑 주소 검증
// 블록체인 트랜잭션 생성
// 컨펌 대기
}
else -> throw IllegalArgumentException("지원하지 않는 결제 방식")
}
}
}문제점:
// 전략 인터페이스
interface PaymentStrategy {
fun pay(amount: Double)
}
// 구체적 전략들
class CreditCardStrategy : PaymentStrategy {
override fun pay(amount: Double) {
println("신용카드로 $amount 결제 처리")
// 신용카드 특화 로직
}
}
class PayPalStrategy : PaymentStrategy {
override fun pay(amount: Double) {
println("PayPal로 $amount 결제 처리")
// PayPal 특화 로직
}
}
// 컨텍스트
class PaymentProcessor(private var strategy: PaymentStrategy) {
fun processPayment(amount: Double) {
strategy.pay(amount)
}
fun setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy
}
}개선점:
┌─────────────┐ ┌──────────────────┐
│ Context │─────────────>│ Strategy │
├─────────────┤ ├──────────────────┤
│ strategy │ │ + execute() │
├─────────────┤ └──────────────────┘
│ execute() │ ▲
└─────────────┘ │
│
┌─────────────────┼─────────────────┐
│ │ │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ConcreteA │ │ ConcreteB │ │ ConcreteC │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ + execute() │ │ + execute() │ │ + execute() │
└──────────────┘ └──────────────┘ └──────────────┘
1. Strategy (전략 인터페이스)
2. ConcreteStrategy (구체적 전략)
3. Context (컨텍스트)
다양한 인증 방식(로컬, OAuth, SAML, 생체인증)을 지원하는 시스템을 설계합니다.
interface AuthenticationStrategy {
fun authenticate(credentials: Map<String, String>): AuthResult
fun supports(method: String): Boolean
}
data class AuthResult(
val success: Boolean,
val userId: String?,
val token: String?,
val errorMessage: String?
)class LocalAuthStrategy : AuthenticationStrategy {
override fun authenticate(credentials: Map<String, String>): AuthResult {
val username = credentials["username"] ?: return AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "Username required"
)
val password = credentials["password"] ?: return AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "Password required"
)
// 데이터베이스에서 사용자 검증
val isValid = validateCredentials(username, password)
return if (isValid) {
AuthResult(
success = true,
userId = username,
token = generateJWT(username),
errorMessage = null
)
} else {
AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "Invalid credentials"
)
}
}
override fun supports(method: String) = method == "local"
private fun validateCredentials(username: String, password: String): Boolean {
// 실제 구현에서는 bcrypt 등으로 해시 검증
return true
}
private fun generateJWT(username: String): String {
// JWT 토큰 생성 로직
return "jwt_token_for_$username"
}
}
class OAuth2Strategy : AuthenticationStrategy {
override fun authenticate(credentials: Map<String, String>): AuthResult {
val code = credentials["code"] ?: return AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "OAuth code required"
)
// OAuth 제공자에게 토큰 교환 요청
val accessToken = exchangeCodeForToken(code)
val userInfo = fetchUserInfo(accessToken)
return AuthResult(
success = true,
userId = userInfo.id,
token = accessToken,
errorMessage = null
)
}
override fun supports(method: String) = method == "oauth2"
private fun exchangeCodeForToken(code: String): String {
// OAuth 제공자 API 호출
return "access_token"
}
private fun fetchUserInfo(token: String): UserInfo {
// 사용자 정보 조회
return UserInfo(id = "oauth_user_123", email = "user@example.com")
}
data class UserInfo(val id: String, val email: String)
}
class SAMLAuthStrategy : AuthenticationStrategy {
override fun authenticate(credentials: Map<String, String>): AuthResult {
val assertion = credentials["saml_assertion"] ?: return AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "SAML assertion required"
)
// SAML 응답 검증
val isValid = validateSAMLAssertion(assertion)
return if (isValid) {
val userId = extractUserIdFromAssertion(assertion)
AuthResult(
success = true,
userId = userId,
token = generateSessionToken(userId),
errorMessage = null
)
} else {
AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "Invalid SAML assertion"
)
}
}
override fun supports(method: String) = method == "saml"
private fun validateSAMLAssertion(assertion: String): Boolean {
// SAML 서명 검증
return true
}
private fun extractUserIdFromAssertion(assertion: String): String {
return "saml_user_456"
}
private fun generateSessionToken(userId: String): String {
return "session_$userId"
}
}class AuthenticationService {
private val strategies = mutableListOf<AuthenticationStrategy>()
init {
// 지원하는 전략들 등록
registerStrategy(LocalAuthStrategy())
registerStrategy(OAuth2Strategy())
registerStrategy(SAMLAuthStrategy())
}
fun registerStrategy(strategy: AuthenticationStrategy) {
strategies.add(strategy)
}
fun authenticate(method: String, credentials: Map<String, String>): AuthResult {
val strategy = strategies.find { it.supports(method) }
?: return AuthResult(
success = false,
userId = null,
token = null,
errorMessage = "Unsupported authentication method: $method"
)
return strategy.authenticate(credentials)
}
}fun main() {
val authService = AuthenticationService()
// 로컬 인증
val localResult = authService.authenticate(
method = "local",
credentials = mapOf(
"username" to "john_doe",
"password" to "secret123"
)
)
println("Local auth: ${localResult.success}, Token: ${localResult.token}")
// OAuth 인증
val oauthResult = authService.authenticate(
method = "oauth2",
credentials = mapOf(
"code" to "oauth_authorization_code"
)
)
println("OAuth auth: ${oauthResult.success}, UserId: ${oauthResult.userId}")
// SAML 인증
val samlResult = authService.authenticate(
method = "saml",
credentials = mapOf(
"saml_assertion" to "encoded_saml_response"
)
)
println("SAML auth: ${samlResult.success}, Token: ${samlResult.token}")
}// 전략 인터페이스
interface ValidationStrategy {
validate(value: string): ValidationResult;
}
interface ValidationResult {
isValid: boolean;
errors: string[];
}
// 구체적 전략들
class EmailValidationStrategy implements ValidationStrategy {
validate(value: string): ValidationResult {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(value);
return {
isValid,
errors: isValid ? [] : ['Invalid email format']
};
}
}
class PasswordValidationStrategy implements ValidationStrategy {
validate(value: string): ValidationResult {
const errors: string[] = [];
if (value.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(value)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(value)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(value)) {
errors.push('Password must contain at least one number');
}
if (!/[^A-Za-z0-9]/.test(value)) {
errors.push('Password must contain at least one special character');
}
return {
isValid: errors.length === 0,
errors
};
}
}
class PhoneNumberValidationStrategy implements ValidationStrategy {
validate(value: string): ValidationResult {
// 한국 전화번호 형식 (010-XXXX-XXXX)
const phoneRegex = /^010-\d{4}-\d{4}$/;
const isValid = phoneRegex.test(value);
return {
isValid,
errors: isValid ? [] : ['Invalid phone number format (expected: 010-XXXX-XXXX)']
};
}
}
// 컨텍스트
class FormValidator {
private strategies: Map<string, ValidationStrategy> = new Map();
registerStrategy(fieldName: string, strategy: ValidationStrategy) {
this.strategies.set(fieldName, strategy);
}
validateField(fieldName: string, value: string): ValidationResult {
const strategy = this.strategies.get(fieldName);
if (!strategy) {
return {
isValid: true,
errors: []
};
}
return strategy.validate(value);
}
validateForm(formData: Record<string, string>): Record<string, ValidationResult> {
const results: Record<string, ValidationResult> = {};
for (const [fieldName, value] of Object.entries(formData)) {
results[fieldName] = this.validateField(fieldName, value);
}
return results;
}
}
// 사용 예시
const formValidator = new FormValidator();
formValidator.registerStrategy('email', new EmailValidationStrategy());
formValidator.registerStrategy('password', new PasswordValidationStrategy());
formValidator.registerStrategy('phone', new PhoneNumberValidationStrategy());
const results = formValidator.validateForm({
email: 'user@example.com',
password: 'Weak123!',
phone: '010-1234-5678'
});
console.log('Validation results:', results);
// {
// email: { isValid: true, errors: [] },
// password: { isValid: true, errors: [] },
// phone: { isValid: true, errors: [] }
// }| 특징 | 전략 패턴 | 상태 패턴 |
|---|---|---|
| 목적 | 알고리즘 교체 | 상태에 따른 행위 변경 |
| 전략/상태 전환 | 클라이언트가 명시적으로 선택 | 내부에서 자동으로 전환 |
| 전략/상태 간 관계 | 독립적 | 상태 간 전이 규칙 존재 |
| 사용 시점 | 알고리즘이 여러 개일 때 | 객체 상태가 변할 때 |
전략 패턴 예시:
// 클라이언트가 전략 선택
val processor = PaymentProcessor(CreditCardStrategy())
processor.setStrategy(PayPalStrategy()) // 명시적 변경상태 패턴 예시:
// 내부에서 상태 자동 전환
class Order {
private var state: OrderState = PendingState()
fun process() {
state.process(this) // 상태가 내부적으로 변경됨
}
}| 특징 | 전략 패턴 | 팩토리 패턴 |
|---|---|---|
| 목적 | 행위 선택 | 객체 생성 |
| 관심사 | 알고리즘 실행 방법 | 어떤 객체를 생성할지 |
| 런타임 변경 | 가능 | 생성 후 불가 |
| 클라이언트 역할 | 전략 선택 및 실행 | 객체 생성 요청 |
| 특징 | 전략 패턴 | 템플릿 메서드 패턴 |
|---|---|---|
| 구현 방식 | 인터페이스 (조합) | 상속 |
| 유연성 | 높음 (런타임 변경) | 낮음 (컴파일 타임 결정) |
| 알고리즘 구조 | 전략마다 다를 수 있음 | 템플릿이 구조 정의 |
| 결합도 | 낮음 | 높음 (부모 클래스 의존) |
interface SortStrategy<T> {
fun sort(list: MutableList<T>, comparator: Comparator<T>)
}
class QuickSortStrategy<T> : SortStrategy<T> {
override fun sort(list: MutableList<T>, comparator: Comparator<T>) {
// Quick Sort 구현
quickSort(list, 0, list.size - 1, comparator)
}
private fun quickSort(list: MutableList<T>, low: Int, high: Int, comparator: Comparator<T>) {
if (low < high) {
val pi = partition(list, low, high, comparator)
quickSort(list, low, pi - 1, comparator)
quickSort(list, pi + 1, high, comparator)
}
}
private fun partition(list: MutableList<T>, low: Int, high: Int, comparator: Comparator<T>): Int {
val pivot = list[high]
var i = low - 1
for (j in low until high) {
if (comparator.compare(list[j], pivot) < 0) {
i++
list[i] = list[j].also { list[j] = list[i] }
}
}
list[i + 1] = list[high].also { list[high] = list[i + 1] }
return i + 1
}
}
class MergeSortStrategy<T> : SortStrategy<T> {
override fun sort(list: MutableList<T>, comparator: Comparator<T>) {
if (list.size > 1) {
mergeSort(list, 0, list.size - 1, comparator)
}
}
private fun mergeSort(list: MutableList<T>, left: Int, right: Int, comparator: Comparator<T>) {
if (left < right) {
val middle = (left + right) / 2
mergeSort(list, left, middle, comparator)
mergeSort(list, middle + 1, right, comparator)
merge(list, left, middle, right, comparator)
}
}
private fun merge(list: MutableList<T>, left: Int, middle: Int, right: Int, comparator: Comparator<T>) {
// Merge 로직 구현
}
}
class Sorter<T>(private var strategy: SortStrategy<T>) {
fun sort(list: MutableList<T>, comparator: Comparator<T>) {
strategy.sort(list, comparator)
}
fun setStrategy(strategy: SortStrategy<T>) {
this.strategy = strategy
}
}
// 사용
val numbers = mutableListOf(64, 34, 25, 12, 22, 11, 90)
val sorter = Sorter<Int>(QuickSortStrategy())
// 작은 데이터셋: Quick Sort
sorter.sort(numbers, Comparator.naturalOrder())
// 큰 데이터셋: Merge Sort로 변경
val largeNumbers = (1..10000).shuffled().toMutableList()
sorter.setStrategy(MergeSortStrategy())
sorter.sort(largeNumbers, Comparator.naturalOrder())interface DiscountStrategy {
fun calculateDiscount(price: Double): Double
}
class NoDiscountStrategy : DiscountStrategy {
override fun calculateDiscount(price: Double) = price
}
class PercentageDiscountStrategy(private val percentage: Double) : DiscountStrategy {
override fun calculateDiscount(price: Double): Double {
return price * (1 - percentage / 100)
}
}
class FixedAmountDiscountStrategy(private val amount: Double) : DiscountStrategy {
override fun calculateDiscount(price: Double): Double {
return maxOf(0.0, price - amount)
}
}
class BuyOneGetOneStrategy : DiscountStrategy {
override fun calculateDiscount(price: Double): Double {
return price / 2 // 50% 할인
}
}
// 조합 전략
class CombinedDiscountStrategy(
private val strategies: List<DiscountStrategy>
) : DiscountStrategy {
override fun calculateDiscount(price: Double): Double {
return strategies.fold(price) { currentPrice, strategy ->
strategy.calculateDiscount(currentPrice)
}
}
}
class ShoppingCart {
private var discountStrategy: DiscountStrategy = NoDiscountStrategy()
private val items = mutableListOf<CartItem>()
fun setDiscountStrategy(strategy: DiscountStrategy) {
this.discountStrategy = strategy
}
fun addItem(item: CartItem) {
items.add(item)
}
fun getTotalPrice(): Double {
val subtotal = items.sumOf { it.price * it.quantity }
return discountStrategy.calculateDiscount(subtotal)
}
}
data class CartItem(val name: String, val price: Double, val quantity: Int)
// 사용 예시
val cart = ShoppingCart()
cart.addItem(CartItem("Laptop", 1000.0, 1))
cart.addItem(CartItem("Mouse", 50.0, 2))
// VIP 고객: 20% 할인
cart.setDiscountStrategy(PercentageDiscountStrategy(20.0))
println("VIP price: ${cart.getTotalPrice()}") // 880.0
// 블랙 프라이데이: 20% + 100원 추가 할인
cart.setDiscountStrategy(
CombinedDiscountStrategy(
listOf(
PercentageDiscountStrategy(20.0),
FixedAmountDiscountStrategy(100.0)
)
)
)
println("Black Friday price: ${cart.getTotalPrice()}") // 780.0interface CompressionStrategy {
compress(data: string): string;
decompress(data: string): string;
}
class GzipCompressionStrategy implements CompressionStrategy {
compress(data: string): string {
// Gzip 압축 로직
console.log(`Compressing ${data.length} bytes with GZIP`);
return `gzip:${data}`; // 실제로는 zlib 사용
}
decompress(data: string): string {
return data.replace('gzip:', '');
}
}
class LZ4CompressionStrategy implements CompressionStrategy {
compress(data: string): string {
// LZ4 압축 (빠른 압축)
console.log(`Compressing ${data.length} bytes with LZ4`);
return `lz4:${data}`;
}
decompress(data: string): string {
return data.replace('lz4:', '');
}
}
class ZstdCompressionStrategy implements CompressionStrategy {
compress(data: string): string {
// Zstandard 압축 (높은 압축률)
console.log(`Compressing ${data.length} bytes with Zstandard`);
return `zstd:${data}`;
}
decompress(data: string): string {
return data.replace('zstd:', '');
}
}
class FileCompressor {
private strategy: CompressionStrategy;
constructor(strategy: CompressionStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: CompressionStrategy) {
this.strategy = strategy;
}
compressFile(data: string): string {
return this.strategy.compress(data);
}
decompressFile(data: string): string {
return this.strategy.decompress(data);
}
}
// 사용: 파일 크기에 따라 전략 선택
function compressWithOptimalStrategy(fileData: string): string {
const compressor = new FileCompressor(new GzipCompressionStrategy());
if (fileData.length < 1000) {
// 작은 파일: 압축 안 함
compressor.setStrategy(new GzipCompressionStrategy());
} else if (fileData.length < 10000) {
// 중간 파일: LZ4 (빠른 압축)
compressor.setStrategy(new LZ4CompressionStrategy());
} else {
// 큰 파일: Zstandard (높은 압축률)
compressor.setStrategy(new ZstdCompressionStrategy());
}
return compressor.compressFile(fileData);
}[주의] 잘못된 예:
// 단순한 차이를 위해 불필요한 전략 생성
class AddOneStrategy : MathStrategy {
override fun calculate(x: Int) = x + 1
}
class AddTwoStrategy : MathStrategy {
override fun calculate(x: Int) = x + 2
}
class AddThreeStrategy : MathStrategy {
override fun calculate(x: Int) = x + 3
}[권장] 개선된 예:
// 매개변수로 처리 가능한 경우 단일 전략 사용
class AddStrategy(private val amount: Int) : MathStrategy {
override fun calculate(x: Int) = x + amount
}
// 사용
val addOne = AddStrategy(1)
val addTwo = AddStrategy(2)[주의] 잘못된 예:
// 전략이 컨텍스트의 내부 상태에 과도하게 의존
class ReportGenerator(private var strategy: ReportStrategy) {
var data: List<Data> = emptyList()
var format: String = "PDF"
var template: Template? = null
fun generate() {
// 전략이 컨텍스트의 모든 상태를 알아야 함
strategy.generate(this)
}
}[권장] 개선된 예:
// 필요한 데이터만 전달
class ReportGenerator(private var strategy: ReportStrategy) {
fun generate(data: List<Data>, format: String, template: Template): Report {
return strategy.generate(data, format, template)
}
}[주의] 잘못된 예:
// 클라이언트가 전략 선택 로직을 알아야 함
fun processPayment(amount: Double, method: String) {
val strategy = when {
method == "credit" && amount > 1000 -> PremiumCreditStrategy()
method == "credit" -> StandardCreditStrategy()
method == "paypal" && userCountry == "US" -> USPayPalStrategy()
method == "paypal" -> InternationalPayPalStrategy()
else -> DefaultStrategy()
}
val processor = PaymentProcessor(strategy)
processor.process(amount)
}[권장] 개선된 예:
// 팩토리 패턴과 결합하여 전략 선택 로직 캡슐화
class PaymentStrategyFactory {
fun createStrategy(method: String, amount: Double, country: String): PaymentStrategy {
return when {
method == "credit" && amount > 1000 -> PremiumCreditStrategy()
method == "credit" -> StandardCreditStrategy()
method == "paypal" && country == "US" -> USPayPalStrategy()
method == "paypal" -> InternationalPayPalStrategy()
else -> DefaultStrategy()
}
}
}
// 클라이언트 코드 단순화
fun processPayment(amount: Double, method: String) {
val factory = PaymentStrategyFactory()
val strategy = factory.createStrategy(method, amount, getUserCountry())
val processor = PaymentProcessor(strategy)
processor.process(amount)
}상태를 갖지 않는 전략은 싱글톤이나 객체 풀로 재사용:
object StrategyPool {
val creditCard: PaymentStrategy = CreditCardStrategy()
val paypal: PaymentStrategy = PayPalStrategy()
val crypto: PaymentStrategy = CryptoStrategy()
}
// 매번 새 인스턴스 생성 대신 재사용
val processor = PaymentProcessor(StrategyPool.creditCard)class ReportGenerator {
private val strategies = mutableMapOf<String, ReportStrategy>()
fun getStrategy(format: String): ReportStrategy {
return strategies.getOrPut(format) {
when (format) {
"PDF" -> PDFReportStrategy()
"HTML" -> HTMLReportStrategy()
"CSV" -> CSVReportStrategy()
else -> throw IllegalArgumentException("Unknown format: $format")
}
}
}
}import kotlin.system.measureTimeMillis
fun benchmarkStrategies() {
val data = (1..100000).toList()
val sorter = Sorter<Int>()
// Quick Sort 성능 측정
val quickTime = measureTimeMillis {
sorter.setStrategy(QuickSortStrategy())
repeat(100) {
sorter.sort(data.shuffled().toMutableList(), Comparator.naturalOrder())
}
}
// Merge Sort 성능 측정
val mergeTime = measureTimeMillis {
sorter.setStrategy(MergeSortStrategy())
repeat(100) {
sorter.sort(data.shuffled().toMutableList(), Comparator.naturalOrder())
}
}
println("Quick Sort: ${quickTime}ms")
println("Merge Sort: ${mergeTime}ms")
}구현 시 확인해야 할 사항:
전략 패턴은 알고리즘을 캡슐화하고 런타임에 동적으로 선택할 수 있게 하는 강력한 디자인 패턴입니다.
핵심 장점:
적용 시기:
전략 패턴을 적절히 활용하면 유연하고 확장 가능한 코드를 작성할 수 있으며, 새로운 요구사항에 빠르게 대응할 수 있습니다.
실행 취소와 재실행을 지원하는 유연한 시스템 설계를 위한 커맨드 패턴 구현
Double-Checked Locking과 초기화 지연을 활용한 안전하고 효율적인 싱글톤 구현 가이드
의존성을 낮추고 확장성을 높이는 팩토리 메서드 패턴의 구조와 실전 구현 가이드