[알아두면 쓸데 있는 디자인 패턴] 전략 패턴과 위임 패턴

1

전략 패턴과 위임 패턴 사용방법에 대해 정리해보겠다.
[알아두면 쓸데 있는 스프링 잡기술] 전략 패턴과 위임 패턴으로 해결하는 다중 빈 주입
보셔야 이해하시기 수월합니다.

이전 예제에서는 단순히 순회를 돌면서 같은 인터페이스를 호출했다.
전형적인 컴포지트 패턴의 구현으로, "컴포넌트들의 그룹을 단일 컴포넌트처럼 취급"하는 방식이었다.
Spring 의 DI 메커니즘에 대한 내용을 이야기하고 싶었던지라 예제가 부실했기에 이번엔 전략 패턴에 대해서 정리해보려고 한다.

전략 패턴과 위임 패턴 사용

interface TicketingStrategyV2 {
    fun ticketing(ticketing: Ticketing): Ticket
}

// 발권 요청 데이터를 담을 인터페이스
sealed interface Ticketing {
    class Automatic : Ticketing {
    	// ...
    }

    class Manual : Ticketing {
    	// ...
    }
    
    
}

// 통행권
data class Ticket(
	// ...
)
@Service
class AutomaticTicketingStrategy : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        check(ticketing is Ticketing.Automatic) {
            "Automatic ticketing requires an Automatic ticketing method"
        }
        // Implementation for automatic ticketing
        return Ticket(ticketing.method)
    }
}

@Service
class ManualTicketingStrategy : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        check(ticketing is Ticketing.Manual) {
            "Manual ticketing requires a Manual ticketing method"
        }

        // Implementation for manual ticketing
        return Ticket(ticketing.method)
    }
}

각 전략 구현체는 자신이 처리할 수 있는 발권 요청만 처리하고, 그렇지 않은 경우 예외를 발생시킨다.
처리하지 못하는 커맨드가 들어올 경우 예외를 발생시키는 것이 타당하다.

class DelegatingTicketingStrategy(
    private val delegates: List<TicketingStrategyV2>,
) : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        for (delegate in delegates) {
                try {
                    return delegate.ticketing(ticketing)
                } catch (e: Exception) {
                    log.error("Delegate ${delegate.javaClass.simpleName} failed: ${e.message}", e)
                    continue
            }
        }

        throw IllegalStateException("No delegate supports ticketing")
    }
}

DelegatingTicketingStrategy는 여러 전략 구현체에 위임하며, 각 전략에서 발생하는 예외를 처리한다. 적절한 전략을 찾을 때까지 시도하며, 모든 전략이 실패하면 최종적으로 예외를 발생시킨다.

그럼 문제점은 없을까?
당연하지만 있다.

  1. 모든 delegate 객체을 순차적으로 시도하면서 예외를 잡는 방식은 성능 비용이 따른다.
  2. 예외는 정상적인 제어 흐름이 아닌 예외적인 상황을 위한 메커니즘으로, 제어 흐름을 위해 예외를 사용하는 것은 부적합하다.
  3. delegate 수가 많아질수록 성능 저하가 발생한다.

대안적 접근법

// 메서드 시그니처 변경
fun ticketing(ticketing: Ticketing): Ticket?

자바에선 null을 리턴하면 되지만 코틀린은 null을 리턴하려면 ? 키워드를 붙여야 한다. 하지만 ?를 붙여 null을 리턴하게 하면 각 클래스가 자신의 책임을 온전히 다할 수 없게 된다.

// No Operation 을 인식할 특수 객체 처리?
Ticket.NOOP

그럼 처리 안할 가짜 티켓을 만들어서 개념적으로 표현이 가능한가? 음... 일단 별로다

명시적 supports 메서드 도입

interface TicketingStrategyV2 {
    fun ticketing(ticketing: Ticketing): Ticket

    fun supports(ticketing: Ticketing): Boolean
}

@Service
class AutomaticTicketingStrategy : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        check(ticketing is Ticketing.Automatic) {
            "Automatic ticketing requires an Automatic ticketing method"
        }
        // Implementation for automatic ticketing
        return Ticket(ticketing.method)
    }

    override fun supports(ticketing: Ticketing): Boolean = ticketing is Ticketing.Automatic
}

@Service
class ManualTicketingStrategy : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        check(ticketing is Ticketing.Manual) {
            "Manual ticketing requires a Manual ticketing method"
        }

        // Implementation for manual ticketing
        return Ticket(ticketing.method)
    }

    override fun supports(ticketing: Ticketing): Boolean = ticketing is Ticketing.Manual
}

@Service
@Primary
class DelegatingTicketingStrategy(
    private val delegates: List<TicketingStrategyV2>,
) : TicketingStrategyV2 {
    override fun ticketing(ticketing: Ticketing): Ticket {
        for (delegate in delegates) {
            if (delegate.supports(ticketing)) {
                return delegate.ticketing(ticketing)
            }
        }

        throw IllegalStateException("No delegate supports ticketing")
    }

    override fun supports(ticketing: Ticketing): Boolean = delegates.any { it.supports(ticketing) }
}

supports 메서드를 추가하여 내가 처리할 수 있는 상태를 검증할 수 있게 한다.

그럼 문제점은 없을까?
당연하지만 있다.
모든 delegate 객체를 순차적으로 시도하면서 예외를 잡는 방식은 해결했다.
하지만 모든 delegate 객체를 순차적으로 시도한다는 부분은 변하지 않았다.
(사실 전략 객체라 해봤자 몇개나 된다고 그냥저냥 쓰면된다.)

더 최적화의 욕심이 난다면 Registry 패턴 같은걸 사용해서 O(1) 시간 복잡도로 개선할 수 있다.
그리고 또 다른 개선 점이 보인다. 각각 구현체에서 인터페이스를 타입을 체크하고 다운캐스팅해야한다는 부담이 남아있다.

다음에 또 정리하도록 하겠다.

0개의 댓글