TIFY 프로젝트를 진행하면서,
13여가지의 상품군에 해당하는 취향 질문에
사용자가 답변을 하면
그것을 근거로 상품을 추천해주는 기능을 만들게 되었다.
처음 이러한 기획을 들었을 때는
음 그냥 사용자가 A, B 상품군을 선택하면 A, B 추천 전략에 맞게,
X, Y 상품군을 선택하면 X, Y 추천 전략에 맞게 리턴해주면 되겠구나 했지만
조금 더 생각해보니 말이 안되는 처사였다.
이런 상황을 해결하기 위해
민준이가 알려준 디자인 패턴 중 하나인
전략 패턴(Strategy Pattern)을 사용해보기로 하였다.
추천 전략을 구현한 코드는 아래 pr들에,
[FEATURE] 상품 추천 로직 작성 by bongsh0112 · Pull Request #45 · Team-TIFY/TIFY-SERVER
📝 PR Summary 상품 추천에 대한 로직을 작성합니다. 🌲 Working Branch feat/44-favor-strategy 🌲 TODOs Related Issues #44
github.com
[FEATURE] BFPER, BFMOI, BFPLA, HCCUP, HCDIS 추천 전략 구현 by bongsh0112 · Pull Request #101 · Team-TIFY/TIFY-SERVER
📝 PR Summary BFPER, BFMOI, BFPLA, HCCUP, HCDIS 추천 전략 구현 🌲 Working Branch feat/100-favor-strategy 🌲 TODOs BFPER BFMOI BFPLA HCCUP HCDIS [:warning: 의논할 부분] 질문에 대해 답변을 하지 않았을 경우도 생각을 했었
github.com
추천 전략을 적용한 코드는 아래 pr에 자세히 나와있다.
[FEATURE] 추천 전략 적용 by bongsh0112 · Pull Request #154 · Team-TIFY/TIFY-SERVER
📝 PR Summary 추천 전략 적용 🌲 Working Branch feat/138-product-strategy 🌲 TODOs favorAnswer가 없을 때의 상황 고려 -> 만약 FRAGRANCE의 소분류 3개 모두가 답변되지 않으면 FavorAnswer_404_1 Exception 발생 BMPER -> BFPE
github.com
[FEATURE] 상품 추천 전략 추가 & 적용 · Issue #138 · Team-TIFY/TIFY-SERVER
🤖 기능 개요 유저의 취향 답변에 따른 상품 추천 전략을 추가하고, API에 직접 적용함 ✅ Implement TODO HEEXE(운동) 관련 상품 추천 전략 작성 BFPER 검토 / 수정 BFPLA 검토 / 수정 BFMOI 검토 / 수정 BMLIP
github.com
0) 전략 패턴이란?
간단하게 말하면
실행 중에 알고리즘을 선택할 수 있게 하는 디자인 패턴이다.
전략 패턴에서는
- 특정한 계열의 알고리즘들을 정의하고
- 각 알고리즘을 캡슐화하며
- 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
위키피디아에서는 위와 같이 설명하는데,
결국 인터페이스의 구현체를
context에 맞게 구현하고 그것을 적용시키는 것이 전부라고 생각했기 때문에,
필자는 그냥 인터페이스를 인터페이스답게
객체 지향 프로그래밍의 다형성을 최대한 효과적으로
상황에 맞춰 사용하는 것 이라고 간단히 이해했다.
1) 프로젝트 적용 방향
상품 카테고리가 13개이고,
13개의 카테고리마다 취향 질문이 많게는 8~9개, 적게는 4~5개씩 있으며
그 질문에 대한 답변에 따라
보여주어야 하는 상품의 특성이 결정되는 방식으로 추천 방식이 기획되었기 때문에
EX)
1_BMLIP에는 5개의 질문이 있다.
그 중 2번째 질문인 퍼스널 컬러는? 에 대한 답변이
가을딥 이라면
'가을딥'이라는 특성을 가지고 있는 상품을 추천해주어야 한다.
인터페이스에는 추천(recommendation)이라는 함수를 놓고
상품의 카테고리마다 구현체를 만들어
추천 함수를 카테고리 별 선물 추천 방식에 따라 구현하는 방식을 쓰기로 하였다.
2) 추천 전략 구현 & 적용
pr이나 소스코드를 보고 왔다면
추천 전략이 13개이고
프로젝트의 기획을 잘 모른다면 보기 힘든 로직이 많기 때문에
전략 구현에 대해서는 모두 다루지는 않으려 한다.
2-1) 추천 전략 구현
우선 전략 패턴의 핵심인 인터페이스를 만든다.
public interface ProductRecommendationStrategy {
public List<Product> recommendation(Long userId, String categoryName);
public StrategyName getStrategyName();
}
recommendation 함수는 context에 따라 여러 가지로 선택될 수 있는 함수이고,
getStrategyName 함수는 Strategy Factory에서 전략이 쉽게 선택될 수 있도록 도움을 주는 함수라고 보면 되겠다.
@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class {StrategyName}RecommendationStrategy implements ProductRecommendationStrategy {
@Override
public List<Product> recommendation(Long userId, String categoryName) {
....
}
@Override
public StrategyName getStrategyName() {
....
}
}
모든 구현체는 위와 같은 형식을 가지고 구현되어 있다.
여기에서 주의할 점은,
모든 구현체에 @Component를 붙여
Bean으로 등록해주어야 한다는 것이다.
이후 StrategyFactory를 만들어
Factory 내에서 전략들을 DI하고,
다른 모듈 (필자의 경우 API 모듈이다) 의 Service 단에서
Factory 의존성을 주입받아 사용하기 위해서는
Bean 등록이 필수적이다.
2-2) 전략 패턴 구현 - Strategy Factory
@Component
@RequiredArgsConstructor
public class ProductRecommendationStrategyFactory {
// 생성자 주입으로 DI
private final List<ProductRecommendationStrategy> strategies;
public List<ProductRecommendationStrategy> findStrategy(SmallCategory smallCategory) {
return strategies.stream()
.filter(
strategy ->
strategy.getStrategyName()
.getDetailCategory()
.getSmallCategory()
.equals(smallCategory))
.toList();
}
}
구현체들을 사용하기 위해,
Factory 클래스를 구현하여
구현체들의 List를 생성자 주입으로 DI한다.
이렇게 의존성을 주입받은 추천 전략 클래스들을findStrategy
메소드에서 SmallCategory로 찾아올 수 있도록 만든다.
(StrategyName의 필드 중 하나인 enum 클래스 DetailCategory가SmallCategory
를 가지고 있으므로 찾아올 수 있음)
2-3) 전략 패턴 적용
@UseCase
@RequiredArgsConstructor
public class RetrieveProductListUseCase {
private final ProductAdaptor productAdaptor;
private final FavorQuestionAdaptor favorQuestionAdaptor;
private final ProductRecommendationStrategyFactory strategyFactory;
@Transactional(readOnly = true)
public SliceResponse<ProductRetrieveVo> executeToSmallCategory(
ProductFilterCondition productFilterCondition, Pageable pageable) {
List<Long> categoryIdList = new ArrayList<>();
List<ProductRecommendationStrategy> strategies = new ArrayList<>();
productFilterCondition
.getSmallCategoryList()
.forEach(
category -> {
categoryIdList.addAll(
favorQuestionAdaptor.queryBySmallCategory(category).stream()
.map(FavorQuestionCategory::getId)
.toList());
strategies.addAll(strategyFactory.findStrategy(category));
});
if (productFilterCondition.getPriceOrder().equals(PriceOrder.DEFAULT)
&& productFilterCondition.getPriceFilter().equals(PriceFilter.DEFAULT)) {
List<ProductRetrieveDto> list = new ArrayList<>();
strategies.forEach(
strategy -> {
list.addAll(
strategy
.recommendation(
SecurityUtils.getCurrentUserId(),
strategy.getStrategyName().getValue())
.stream()
.map(
product -> {
return ProductRetrieveDto.of(
product,
favorQuestionAdaptor.queryCategory(
product
.getFavorQuestionCategoryId()));
})
.toList());
});
return SliceResponse.of(
new SliceImpl<>(
list.stream().map(ProductRetrieveVo::from).toList(), pageable, true));
} else {
Slice<ProductRetrieveDto> productRetrieveDTOS =
productAdaptor.searchBySmallCategoryId(
new ProductCategoryCondition(
categoryIdList,
productFilterCondition.getPriceOrder(),
productFilterCondition.getPriceFilter(),
pageable));
return SliceResponse.of(
new SliceImpl<>(
productRetrieveDTOS.stream().map(ProductRetrieveVo::from).toList(),
pageable,
true));
}
}
}
유저의 Context에 따라서 사용하는 전략이 달라야 하므로,
Context를 받아오는 UseCase(Service)
에서StrategyFactory
를 주입받았다.ProductFilterCondition
안의 List<SmallCategory>
에forEach
를 적용하여 SmallCategory
마다StrategyFactory
의 findStrategy
메소드를 이용하여
구현체를 찾아와야 한다.
이렇게 찾아온 구현체들을 미리 만들어놓은 List<ProductRecommendationStrategy>
에 addAll하여PriceOrder, PriceFilter
가 DEFAULT인 상황에
사용할 수 있도록 코드를 작성했다.
3) 전략패턴을 사용하지 않았다면..?
만약 전략 패턴을 사용하지 않고
사용자의 Context를 필자가 모두 고려하여 코드를 작성했다면,
물론 추천 전략 클래스를 모두 분리하여 만들었긴 했겠지만StrategyName
의 개수만큼 if-else 문을 사용했을 것이다.
지금도 코드가 복잡하지 않은 것은 아니나...
13개의 If-else문 보다는
가독성도 챙기고 복잡도도 떨어뜨렸다고 생각한다.
디자인 패턴을 더 공부하면서
코드에 직접 적용한 것을 더 많이 포스팅 해보자..!
'🌿 Spring' 카테고리의 다른 글
메일 발송 - 스케쥴링 간 문제 해결 with Async (1) | 2023.10.03 |
---|---|
등록된 미술품 상태 변경 with Scheduling (0) | 2023.10.03 |
사용자에게 메일 보내기 with JavaMailSender (0) | 2023.10.03 |
무한스크롤 API 구현하기 (1) | 2023.09.04 |