간단하게 요구사항 정리부터 들어가보자.
발권에는 두 가지 유형이 존재한다.
발권 유형에 따라 발권 방식을 다르게 처리할 수 있도록, 전략 패턴(Strategy Pattern)을 적용한다.
interface TicketingStrategyV1 {
fun ticketing()
}
@Service
class AutomaticTicketingStrategy : TicketingStrategyV1 {
override fun ticketing() {
}
}
@Service
class ManualTicketingStrategy : TicketingStrategyV1 {
override fun ticketing() {
}
}
@Service
class TicketingService(
private val ticketingStrategy: TicketingStrategyV1,
) {
// ....
}
TicketingStrategyV1
: 발권 로직을 정의하는 전략 패턴(Strategy Pattern) 기반 인터페이스
AutomaticTicketingStrategy
: 자동 발권 로직을 작성할 클래스
ManualTicketingStrategy
: 수동 발권 로직을 작성할 클래스
TicketingService
: 요청으로부터 티켓을 발권하는 로직을 작성할 클래스
TicketingStrategyV1 인터페이스는 엄밀히 말하면 Strategy Pattern 보다는 Composite Pattern 에 더 부합한다. TicketingStrategyV2 에서 전략 패턴으로 변경해보겠다.
스프링을 다루시는분들은 벌써 눈치채셨겠지만 TicketingService
의 DI는 이뤄지지 않는다.
TicketingStrategyV1
주입을 시도하였지만 서브 클래싱된 구현체가 2개 존재하기 때문이다.
@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class ApplicationTests(
private val applicationContext: ApplicationContext,
) {
@Test
fun ticketingStrategyV1Check() {
val beans = applicationContext.getBeansOfType(TicketingStrategyV1::class.java)
assertThat(beans)
.hasSize(2)
.satisfies({ it ->
it["automaticTicketingStrategy"]?.let { assertThat(it).isInstanceOf(AutomaticTicketingStrategy::class.java) }
it["manualTicketingStrategy"]?.let { assertThat(it).isInstanceOf(ManualTicketingStrategy::class.java) }
})
}
@Test
@DisplayName("NoUniqueBeanDefinitionException 발생")
fun noUniqueBeanDefinitionException() {
assertThatThrownBy { applicationContext.getBean(TicketingStrategyV1::class.java) }
.isInstanceOf(NoUniqueBeanDefinitionException::class.java)
}
}
@Service
class TicketingService(
private val automaticTicketingStrategy: AutomaticTicketingStrategy,
private val manualTicketingStrategy: ManualTicketingStrategy,
) {
// ....
}
class DelegatingTicketingStrategy(
private val delegates: List<TicketingStrategyV1>,
) : TicketingStrategyV1 {
override fun ticketing() {
delegates.forEach { it.ticketing() }
}
}
@Bean
fun ticketingStrategyV1(
automaticTicketingStrategy: AutomaticTicketingStrategy,
manualTicketingStrategy: ManualTicketingStrategy,
): TicketingStrategyV1 = DelegatingTicketingStrategy(listOf(automaticTicketingStrategy, manualTicketingStrategy))
@Service
@Primary
class DelegatingTicketingStrategy(
private val delegates: List<TicketingStrategyV1>,
) : TicketingStrategyV1 {
override fun ticketing() {
delegates.forEach { it.ticketing() }
}
}
DelegatingTicketingStrategy
를 @Primary로 등록하면, 스프링 DI 메커니즘은 TicketingStrategyV1
타입을 주입할 때 우선적으로 DelegatingTicketingStrategy
를 선택한다.
이때, List<TicketingStrategyV1>
로 주입받은 전략 리스트에는 자기 자신(DelegatingTicketingStrategy
) 은 포함되지 않고,
실제 구현체(AutomaticTicketingStrategy
, ManualTicketingStrategy
)만 주입된다. 스프링은 DI 주입 시, 순환 참조를 방지하기 위해 같은 타입의 자기 자신을 주입 리스트에서 자동으로 제외하는 특성이 있다. 결과적으로, 새로운 전략 클래스가 추가되더라도, 별도로 DelegatingTicketingStrategy나 TicketingService의 코드를 수정할 필요가 없다. 새 전략 클래스를 @Service로 등록하기만 하면 된다.
SOLID 원칙 준수
delegate가 아니라 Method Chaining Pattern, Composite Pattern 등 사용할 때 순서를 보장하고 싶다면 @Order annoation을 사용하면 된다.
@Service
@Order(1) // 우선순위 지정
class AutomaticTicketingStrategy : TicketingStrategyV1 {
override fun ticketing() {
// 자동 발권 로직
}
}
@Service
@Order(2) // 후순위 지정
class ManualTicketingStrategy : TicketingStrategyV1 {
override fun ticketing() {
// 수동 발권 로직
}
}
위임패턴은 개인적으로는 자주 사용하는 디자인 패턴 중 하나이다.
위임패턴과 스프링의 DI 메커니즘을 통해 어떻게 객체지향의 원칙들을 준수하면서 개발 할 수 있는지에 대해 정리해보았다.
다음엔 전략 패턴에 의거하여 TicketingStrategyV2로 최적화할 여러 방법들을 정리해보겠다.