전략 패턴과 위임 패턴 사용방법에 대해 정리해보겠다.
[알아두면 쓸데 있는 스프링 잡기술] 전략 패턴과 위임 패턴으로 해결하는 다중 빈 주입
보셔야 이해하시기 수월합니다.
이전 예제에서는 단순히 순회를 돌면서 같은 인터페이스를 호출했다.
전형적인 컴포지트 패턴의 구현으로, "컴포넌트들의 그룹을 단일 컴포넌트처럼 취급"하는 방식이었다.
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는 여러 전략 구현체에 위임하며, 각 전략에서 발생하는 예외를 처리한다. 적절한 전략을 찾을 때까지 시도하며, 모든 전략이 실패하면 최종적으로 예외를 발생시킨다.
그럼 문제점은 없을까?
당연하지만 있다.
대안적 접근법
// 메서드 시그니처 변경
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) 시간 복잡도로 개선할 수 있다.
그리고 또 다른 개선 점이 보인다. 각각 구현체에서 인터페이스를 타입을 체크하고 다운캐스팅해야한다는 부담이 남아있다.
다음에 또 정리하도록 하겠다.