이커머스 프로젝트를 진행하면서 장바구니 기능에 추가 요구사항이 생겨 리팩토링 하는 과정을 기록합니다. 이번 리팩토링을 위해 전략 패턴
과 팩토리 메소드
를 사용하였습니다.
- 팩토리 메서드 패턴은 객체 생성을 위한 패턴으로, 객체를 생성하는 역할을 서브클래스에 위임하여 유연성을 제공합니다.
- 전략 패턴은 행위를 캡슐화하고 런타임에 행위를 변경할 수 있는 패턴입니다. 이 패턴은 애플리케이션의 일부를 동적으로 교체하고 다양한 전략을 적용할 수 있도록 도와줍니다.
기존 로직
Redis 저장소에 제품을 저장하는 기존 로직을 살펴보겠습니다.
@Service
class BasketItemService(
...
) {
fun addBasketItem(basketItemRequest: BasketItemRequest) {
// redis 저장 로직
}
}
기존의 컨트롤러는 다음과 같이 작성되었습니다. 그러나 addBasketItem
의 세부 로직이 추가되었습니다. 여기서 토큰이 있는 경우 .addBasketItem()
은 데이터를 데이터베이스(DB)에 저장하는 로직을 수행하고, 토큰이 없는 경우 세션에 데이터를 저장하는 로직을 수행해야 합니다.
class BasketItemController(
private val basketItemService: BasketItemService,
) {
@PostMapping
fun addBasketItem(@RequestBody basketItemRequest: BasketItemRequest, request: HttpServletRequest): ApiResponse {
// 토큰과 세션의 여부에 따라 로직이 달라짐.
val response = basketItemService.addBasketItem(basketItemRequest, request)
return ApiResponse.of(HttpStatus.CREATED, response)
}
}
전략 패턴 적용
변경되는 부분과 변경되지 않는 부분 분리
애플리케이션에서 변경되는 부분(addBasketItem
)과 변경되지 않는 부분(basketItemService
)을 분리해보겠습니다. 여기서 변경되는 부분이 전략
에 해당합니다.
변경되는 부분 캡슐화
먼저, 변경되는 부분을 캡슐화하기 위한 인터페이스를 만들겠습니다. 이렇게 달라지는 부분을 캡슐화함으로써 시스템의 유연성을 향상시킬 수 있습니다.
interface BasketItemStrategy {
fun addBasketItem(basketItemRequest: BasketItemRequest, request: HttpServletRequest): BasketItemDTO
}
구체적인 전략 구현
캡슐화된 전략을 구체적으로 구현하는 클래스를 만듭니다. Redis 저장 클래스와 DB 저장 클래스를 만들겠습니다.
- Redis 저장 로직 구현
@Service
class BasketItemRedisStrategyImpl(
...
): BasketItemStrategy {
override fun addBasketItem(
basketItemRequest: BasketItemRequest,
request: HttpServletRequest
): BasketItemDTO {
... // Redis 저장 로직
}
}
- DB 저장 로직 구현
@Service
class BasketItemDBStrategyImpl(
...
): BasketItemStrategy {
@Transactional
fun addBasketItemToDB(
basketItems: MutableMap<String, Any>,
httpSession: HttpSession
): MutableList<BasketItem> {
...// DB 저장 로직
}
}
BasketItemService 업데이트
마지막으로, BasketItemService
가 상황에 따라 동적으로 전략을 선택할 수 있도록 수정해주겠습니다.
@Service
class BasketItemService(
// 빈에 접근하기 위해 ApplicationContext를 주입받습니다.
private val applicationContext: ApplicationContext,
) {
fun addBasketItem(basketItemRequest: BasketItemRequest, request: HttpServletRequest): BasketItemDTO {
// 토큰의 존재 여부에 따라 적절한 전략을 선택합니다.
val strategy: BasketItemStrategy
if (hasToken(request)) {
strategy = applicationContext.getBean(BasketItemDBStrategyImpl::class.java)
} else {
strategy = applicationContext.getBean(BasketItemRedisStrategyImpl::class.java)
}
strategy.addBasketItem();
}
private fun hasToken(request: HttpServletRequest): Boolean {
val token = request.getHeader("Authorization")
return token != null
}
}
이렇게 전략 패턴을 사용하여 애플리케이션의 일부분을 추상화하고, 유연하게 확장 가능하게 만들었습니다. 이처럼 전략 패턴은 변화하는 부분을 캡슐화함으로써 코드의 수정을 줄이고 동적으로 다양한 전략을 수행할 수 있도록 도와줍니다.
팩토리 메소드 패턴
전략을 동적으로 선택하는 것은 addBasketItem
의 역할을 벗어나며, 각 전략이 생성될 때마다 변경될 수 있습니다. 따라서 이를 팩토리 메소드 패턴을 통해 분리해보겠습니다.
@Service
class BasketItemService(
private val basketItemStrategyFactory: BasketItemStrategyFactory,
) {
fun addBasketItem(basketItemRequest: BasketItemRequest, request: HttpServletRequest): BasketItemDTO {
val strategy = basketItemStrategyFactory.getStrategy(request)
return strategy.addBasketItem(basketItemRequest, request)
}
}
@Component
class BasketItemStrategyFactory(
private val applicationContext: ApplicationContext,
) {
fun getStrategy(request: HttpServletRequest): BasketItemStrategy {
return if (hasToken(request)) {
applicationContext.getBean(BasketItemDBStrategyImpl::class.java)
} else {
applicationContext.getBean(BasketItemRedisStrategyImpl::class.java)
}
}
private fun hasToken(request: HttpServletRequest): Boolean {
val token = request.getHeader("Authorization")
return token != null
}
}
이렇게 함으로써 addBasketItem은 장바구니에 상품을 추가하는 로직에만 더 집중할 수 있게 됩니다.
결론
- 전략 패턴을 사용하여 프로그램의 변경되는 부분을 캡슐화하고 동적으로 전략을 교체할 수 있도록 했습니다.
- 팩토리 메서드 패턴을 사용하여 전략 선택 로직을 분리함으로써 서비스 레이어가 비즈니스 로직에 더 집중할 수 있도록 하였습니다.
- 전략 패턴과 팩토리 메서드 패턴을 통해 코드의 가독성과 유지 보수성이 향상되었습니다.