본문 바로가기
OOP/Design Pattern

Strategy Pattern - 전략 패턴

by Ahngyuho 2023. 9. 3.

이번 포스팅 주제는 GoF 디자인 패턴 중 하나인 Strategy Patten - 전략 패턴 입니다.

게시판의 검색 서비스를 리팩터링 하기 위해 좀 더 좋은 방법이 없을까 찾아보다 책에서 읽은 전략 패턴이 기억나 적용 해보려고 합니다! 

 

리팩터링 전

@Transactional(readOnly = true)
    public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable) {
        if(searchKeyword == null || searchKeyword.isBlank()){
            return articleRepository.findAll(pageable).map(ArticleDto::from);
        }

        return switch (searchType){
            case TITLE -> articleRepository.findByTitleContaining(searchKeyword,pageable).map(ArticleDto::from);
            case CONTENT -> articleRepository.findByContentContaining(searchKeyword,pageable).map(ArticleDto::from);
            case ID -> articleRepository.findByUserAccount_UserIdContaining(searchKeyword,pageable).map(ArticleDto::from);
            case NICKNAME -> articleRepository.findByUserAccount_NicknameContaining(searchKeyword,pageable).map(ArticleDto::from);
            case HASHTAG -> articleRepository.findByHashtag("#" + searchKeyword,pageable).map(ArticleDto::from);
        };
    }

 

전 switch case 문이 많이 신경 쓰였습니다.

switch case가 무조건적으로 나쁘다는 생각은 하지 않지만 좀 더 좋은 방법이 없을까? 하는 생각에 전략 패턴을 적용해보기로 한 것입니다.

 

본격적으로 들어가기 전에 전략 패턴에 대해서 간략히 설명해 보겠습니다.

전략 패턴

전략  패턴이란 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법입니다.

 

이제 전략 패턴을 한번 이용해 보겠습니다!

 

3가지 과정이 존재하고 이는 다음과 같습니다.

  • 알고리즘을 나타내는 인터페이스 정의
  • 람다를 이용한 함수형 인터페이스의 추상 메서드 구현
  • 전략 패턴용 객체 정의

 

알고리즘을 나타내는 인터페이스 정의

@FunctionalInterface
public interface SearchStrategy {
    Page<ArticleDto> execute(ArticleRepository articleRepository, String searchKeyword, Pageable pageable);
}

 

 

람다를 이용한 함수형 인터페이스의 추상 메서드 구현

@Component
public class SearchStrategyFactory {
    private final EnumMap<SearchType, SearchStrategy> strategies = new EnumMap<>(SearchType.class);

    public static SearchStrategy createSearchStrategy(SearchType searchType) {
        SearchStrategy searchStrategy;
        switch (searchType) {
            case TITLE:
                return (articleRepository, searchKeyword, pageable) ->
                        articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from);
            case CONTENT:
                return (articleRepository, searchKeyword, pageable) ->
                        articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from);
            case ID:
                return (articleRepository, searchKeyword, pageable) ->
                        articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from);
            case NICKNAME:
                return (articleRepository, searchKeyword, pageable) ->
                        articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from);
            case HASHTAG:
                return (articleRepository, searchKeyword, pageable) ->
                        articleRepository.findByHashtag(searchKeyword, pageable).map(ArticleDto::from);
            default:
                throw new IllegalArgumentException("Invalid Search Type");
        }
    }

    public SearchStrategyFactory() {
        strategies.put(SearchType.TITLE, (articleRepository, searchKeyword, pageable) ->
                articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from));

        strategies.put(SearchType.CONTENT, (articleRepository, searchKeyword, pageable) ->
                articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from));

        strategies.put(SearchType.ID, (articleRepository, searchKeyword, pageable) ->
                articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from));

        strategies.put(SearchType.NICKNAME, (articleRepository, searchKeyword, pageable) ->
                articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from));

        strategies.put(SearchType.HASHTAG, (articleRepository, searchKeyword, pageable) ->
                articleRepository.findByHashtag(searchKeyword, pageable).map(ArticleDto::from));
    }

    public SearchStrategy createSearch(SearchType searchType) {
        if (searchType == null) {
            throw new IllegalArgumentException("Invalid Search Type");
        }
        return strategies.get(searchType);
    }
}

팩토리 메서드를 이용한 방법인데 총 2가지 방법을 통해 구현했습니다.

  • Switch case 
  • EnumMap

두 방법 다 공통적으로 SearchStrategy 인터페이스를 람다를 통해 구현하고 있습니다.

SearchType에 따라 여러개의 SearchStrategy 를 구현해야 했는데 그 이유는 Jpa를 사용해 select 하는 메서드를 SearchType에 따라 여러개 만들어 뒀기 때문입니다.

만약 이 select 하는 알고리즘을 하나로 뽑아낼 수 있다면 좀 더 코드가 유연해 지겠죠?

하지만 아직 적절한 해결책을 찾지 못해 그냥 진행하기로 했습니다.

 

차이점제어 흐름 문법을 사용하느냐 자료구조를 사용하느냐 입니다.

아마 검색 타입이 그렇게 많지 않아 Switch Case를 사용하는 것이 좀 더 좋아보이지만 검색 타입이 다양해지면 자료구조를 사용하는 방법이 좀 더 적절해 보입니다!

 

전략 패턴용 객체 정의

public class Search {
    private final SearchStrategy searchStrategy;

    public Search(SearchStrategy searchStrategy) {
        this.searchStrategy = searchStrategy;
    }

    public Page<ArticleDto> execute(ArticleRepository articleRepository, String searchType, Pageable pageable) {
        return searchStrategy.execute(articleRepository, searchType, pageable);
    }
}

 

최종 리팩터링 결과

@Transactional(readOnly = true)
    public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable)  {
        if(searchKeyword == null || searchKeyword.isBlank()){
            return articleRepository.findAll(pageable).map(ArticleDto::from);
        }
        Search search = new Search(searchStrategyFactory.createSearch(searchType));
        return search.execute(articleRepository, searchKeyword, pageable);
    }

확실히 코드가 굉장히 간결해 졌습니다.

 

제가 진행한 리팩터링의 핵심은 동적으로 알고리즘을 선택할 수 있게 하는 것입니다.

그러기 위해 제가 사용한 방법은 쿼리를 동작시키는 메서드 자체를 파라미터화 하고,

타입에 따라 미리 지정해둔 메서드(정확히는 추상 메서드를 구현한 인터페이스 구현체)를 매핑해 주었고,

실제로 실행하기 위한 객체를 생성해 주었습니다.

 

리팩터링을 진행하면서 느낀 점은 최종적으로 위의 코드가 어떤 장점을 가져다 주는지 아직 깨닫지 못한다는 것입니다.

아직 제가 하고있는 행위에 대해 완전히 이해하지 못한다는 느낌이 들어 개운하지 않지만 그건 차차 깨닫기로 하고...

 

우선 검색을 위한 객체를 생성하는 부분과 실제로 실행하는 부분을 완전히 분리, 핵심 알고리즘을 어느 정도 추상화 했다는 것에 의의를 두고 계속해서 해당 코드에 대해 고민해 보도록 하겠습니다!