Post

전략패턴과 팩토리 메소드를 활용한 장바구니 기능 리팩토링

이커머스 프로젝트를 진행하면서 장바구니 기능에 추가 요구사항이 생겨 리팩토링 하는 과정을 기록합니다. 이번 리팩토링을 위해 전략 패턴팩토리 메소드를 사용하였습니다.

  • 팩토리 메서드 패턴은 객체 생성을 위한 패턴으로, 객체를 생성하는 역할을 서브클래스에 위임하여 유연성을 제공합니다.
  • 전략 패턴은 행위를 캡슐화하고 런타임에 행위를 변경할 수 있는 패턴입니다. 이 패턴은 애플리케이션의 일부를 동적으로 교체하고 다양한 전략을 적용할 수 있도록 도와줍니다.

기존 로직

Redis 저장소에 제품을 저장하는 기존 로직을 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
@Service
class BasketItemService(
	...
) {

    fun addBasketItem(basketItemRequest: BasketItemRequest) {
		// redis 저장 로직
    }

}

기존의 컨트롤러는 다음과 같이 작성되었습니다. 그러나 addBasketItem 의 세부 로직이 추가되었습니다. 여기서 토큰이 있는 경우 .addBasketItem()은 데이터를 데이터베이스(DB)에 저장하는 로직을 수행하고, 토큰이 없는 경우 세션에 데이터를 저장하는 로직을 수행해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
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)을 분리해보겠습니다. 여기서 변경되는 부분이 전략에 해당합니다.

변경되는 부분 캡슐화

먼저, 변경되는 부분을 캡슐화하기 위한 인터페이스를 만들겠습니다. 이렇게 달라지는 부분을 캡슐화함으로써 시스템의 유연성을 향상시킬 수 있습니다.

1
2
3
4
5
interface BasketItemStrategy {  
  
    fun addBasketItem(basketItemRequest: BasketItemRequest, request: HttpServletRequest): BasketItemDTO  
  
}

구체적인 전략 구현

캡슐화된 전략을 구체적으로 구현하는 클래스를 만듭니다. Redis 저장 클래스와 DB 저장 클래스를 만들겠습니다.

  1. Redis 저장 로직 구현
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    @Service  
    class BasketItemRedisStrategyImpl(  
     ...  
    ): BasketItemStrategy {   
      
     override fun addBasketItem(  
         basketItemRequest: BasketItemRequest,  
         request: HttpServletRequest  
     ): BasketItemDTO {  
         ... // Redis 저장 로직
     }  
    }
    
  2. DB 저장 로직 구현
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    @Service  
    class BasketItemDBStrategyImpl(  
     ...
    ): BasketItemStrategy {  
    
     @Transactional  
     fun addBasketItemToDB(
         basketItems: MutableMap<String, Any>, 
         httpSession: HttpSession
     ): MutableList<BasketItem> {  
         ...// DB 저장 로직
     }  
    }
    

BasketItemService 업데이트

마지막으로, BasketItemService가 상황에 따라 동적으로 전략을 선택할 수 있도록 수정해주겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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의 역할을 벗어나며, 각 전략이 생성될 때마다 변경될 수 있습니다. 따라서 이를 팩토리 메소드 패턴을 통해 분리해보겠습니다.

1
2
3
4
5
6
7
8
9
10
@Service  
class BasketItemService(  
    private val basketItemStrategyFactory: BasketItemStrategyFactory,  
) {  
  
    fun addBasketItem(basketItemRequest: BasketItemRequest, request: HttpServletRequest): BasketItemDTO {  
        val strategy = basketItemStrategyFactory.getStrategy(request)  
        return strategy.addBasketItem(basketItemRequest, request)  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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은 장바구니에 상품을 추가하는 로직에만 더 집중할 수 있게 됩니다.

결론

  • 전략 패턴을 사용하여 프로그램의 변경되는 부분을 캡슐화하고 동적으로 전략을 교체할 수 있도록 했습니다.
  • 팩토리 메서드 패턴을 사용하여 전략 선택 로직을 분리함으로써 서비스 레이어가 비즈니스 로직에 더 집중할 수 있도록 하였습니다.
  • 전략 패턴과 팩토리 메서드 패턴을 통해 코드의 가독성과 유지 보수성이 향상되었습니다.

#팩토리메소드 #전략패턴 #장바구니

This post is licensed under CC BY 4.0 by the author.

© bear-su. Some rights reserved.