베어_
TechBear
베어_
전체 방문자
오늘
어제
  • 분류 전체보기 (336)
    • Spring (33)
      • 개념 (13)
      • Security (5)
      • 실습 (1)
      • 토비 스프링 (11)
    • JPA (6)
    • 프로젝트 기록 (24)
    • DB (13)
    • JAVA (18)
    • 알고리즘 (50)
      • 유형정리 (8)
      • Baekjoon (21)
      • LeetCode (18)
    • 디자인패턴 (0)
    • 개발서적 (79)
      • Effective Java (78)
      • 객체지향의 사실과 오해 (1)
    • 독후감 (4)
    • 보안 (2)
    • 운영체제(OS) (53)
      • 공룡책 (53)
    • 컴퓨터 네트워크 (28)
      • 컴퓨터 네트워크 하향식 접근 (23)
    • 자료구조 (1)
    • DevOps (2)
    • 앱 개발 (20)
      • 안드로이드 스튜디오 (20)

블로그 메뉴

    공지사항

    인기 글

    태그

    • 코드업
    • 데이터베이스
    • 자바8
    • 이펙티브자바
    • BFS
    • 스프링
    • 알고리즘
    • 자바
    • 토비스프링
    • leetcode
    • java
    • 스레드
    • 백준
    • dfs
    • 운영체제
    • Spring
    • jpa
    • 스프링시큐리티
    • C++
    • 함수형인터페이스

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    베어_

    TechBear

    프로젝트 기록

    디자인패턴을 이용한 장바구니 기능 구현 - 레디스와 스프링부트 활용

    2023. 12. 16. 22:52

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

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

    기존 로직

    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 저장 클래스를 만들겠습니다.

    1. Redis 저장 로직 구현
    @Service  
    class BasketItemRedisStrategyImpl(  
        ...  
    ): BasketItemStrategy {   
      
        override fun addBasketItem(  
            basketItemRequest: BasketItemRequest,  
            request: HttpServletRequest  
        ): BasketItemDTO {  
    		... // Redis 저장 로직
        }  
    }
    
    1. 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은 장바구니에 상품을 추가하는 로직에만 더 집중할 수 있게 됩니다.

    결론

    • 전략 패턴을 사용하여 프로그램의 변경되는 부분을 캡슐화하고 동적으로 전략을 교체할 수 있도록 했습니다.
    • 팩토리 메서드 패턴을 사용하여 전략 선택 로직을 분리함으로써 서비스 레이어가 비즈니스 로직에 더 집중할 수 있도록 하였습니다.
    • 전략 패턴과 팩토리 메서드 패턴을 통해 코드의 가독성과 유지 보수성이 향상되었습니다.
      '프로젝트 기록' 카테고리의 다른 글
      • 스프링 세션 적용하기 (feat. filter)
      • 스프링 세션 - 이중 발급과, 인코딩된 세션
      • 스프링 세션의 이해 - 등장 배경과 그 원리
      • 2차 캐시를 이용한 최적화 기록
      베어_
      베어_
      Today I learned | 문제를 해결하는 개발자

      티스토리툴바