본문 바로가기

BackEnd

[Spring] Enum-Strategy Pattern 적용기

Strategy Pattern란?

서로 바꿔 끼울 수 있는 알고리즘(행동)을 캡슐화하고, 런타임에 교체 가능하게 해 클라이언트(컨텍스트)와 알고리즘의 결합도를 낮추는 패턴.

즉, 전략 패턴은 “알고리즘(행동)을 갈아끼울 수 있게” 만드는 설계 패턴이에요.
같은 일을 하되, 상황에 따라 계산 방법을 바꾸고 싶을 때 if-else 덩어리 대신 교체 가능한 객체로 분리합니다.

 

Strategy Pattern 적용하게 된 이유

서비스를 만들다 보면 조건 분기가 끝없이 늘어납니다.

처음엔 if - else 가 편하지만, 조건이 추가되면서 계속해서 추가되다보면 소스 가독성은 떨어지고 수정 범위는 넓어지고 버그 리스크도 커져 불편함을 느꼈습니다.

그래서 저는 "분기를 매핑으로 바꾼다"는 관점에서 enum + strategy 패턴을 적용해 보았습니다.

결과는 기존 소스보다 더 깔끔해 졌고, 유지보수가 조금더 쉬워졌다고 느꼈습니다.

 

기존방식의 문제: if-else 사슬

연수후기 검색 기능을 개발 하던 중에 "검색 타입(전체/제목/내용/과정명)"에 따라 다른 쿼리를 실행 해야하는 상황이었습니다.

처음에는 아래와 같이 if-else문을 사용하여 개발을 진행 하였습니다.

public ReviewSearchResponse reviewSearch(ReviewSearchRequest req) {
    String type = req.getSearchType();
    if ("ST001".equals(type)) { // 전체
        long total = searchReviewCountByAll(req);
        List<ReviewDocument> docs = searchReviewByAll(req);
        return response(total, docs);
    } else if ("ST002".equals(type)) { // 제목
        long total = searchReviewCountByTitle(req);
        List<ReviewDocument> docs = searchReviewByTitle(req);
        return response(total, docs);
    } else if ("ST003".equals(type)) { // 과정명
        long total = searchReviewCountByCourseName(req);
        List<ReviewDocument> docs = searchReviewByCourseName(req);
        return response(total, docs);
    } else if ("ST004".equals(type)) { // 내용
        long total = searchReviewCountByContent(req);
        List<ReviewDocument> docs = searchReviewByContent(req);
        return response(total, docs);
    } else {
        throw new IllegalArgumentException("Unknown searchType: " + type);
    }
}

 

위와 같이 if-else 문을 사용하여 코드를 작성한 사례 입니다. 딱 보기에도 코드가 산만하고, 새로운 타입이 추가될 때마다 코드 자체를 수정해야 하고 수정 함으로써 버그 발생 리스크도 안고 가야 되는 문제가 있다는 것을 알 수 있습니다.

Enum-Strategy Pattern 적용

if-else 분기를 매핑으로 전환을 간단히 정리 해 보았습니다.

 

1. 검색 타입을 enum으로 강타입화 한다.

2. 타입별 실행 로직(count/search)를 전략 함수로 분리한다.

3. 요청이 들어오면 enum으로 파싱 → 대응 전략 실행만 한다.

 

전략 Enum (count/search를 전략으로 보유)

@RequiredArgsConstructor
@Getter
public enum ReviewSearchType {

    ALL(
        "ST001",
        ReviewSearchService::searchReviewCountByAll,
        ReviewSearchService::searchReviewByAll
    ),
    TITLE(
        "ST002",
        ReviewSearchService::searchReviewCountByTitle,
        ReviewSearchService::searchReviewByTitle
    ),
    COURSE(
        "ST003",
        ReviewSearchService::searchReviewCountByCourseName,
        ReviewSearchService::searchReviewByCourseName
    ),
    CONTENT(
        "ST004",
        ReviewSearchService::searchReviewCountByContent,
        ReviewSearchService::searchReviewByContent
    );

    private final String code;

    // count: (service, request) -> long
    private final ToLongBiFunction<ReviewSearchService, ReviewSearchRequest> counter;

    // search: (service, request) -> List<ReviewDocument>
    private final BiFunction<ReviewSearchService, ReviewSearchRequest, List<ReviewDocument>> searcher;

    @FunctionalInterface
    public interface ToLongBiFunction<S, R> {
        long apply(S service, R request);
    }

    private static final Map<String, ReviewSearchType> BY_CODE =
        Arrays.stream(values())
              .collect(Collectors.toUnmodifiableMap(ReviewSearchType::getCode, e -> e));

    public static ReviewSearchType fromCodeOrDefault(String code) {
        return BY_CODE.getOrDefault(code, ALL);
    }
}

 

검색타입을 Enum으로 안전하게 파싱 하고, Method Reference 로 서비스 메서드 바인딩 되어 기존 if-else 사슬에서 문제 되었던 부분을 깔끔 하게 정리 할 수 있습니다.

@PostMapping("/reviewSearch")
@ResponseBody
public ResponseEntity<ReviewSearchResponse> reviewSearch(
        @Valid @RequestBody ReviewSearchRequest reviewSearchRequest) {

    int page = (reviewSearchRequest.getPage() != null && reviewSearchRequest.getPage() >= 0)
            ? reviewSearchRequest.getPage() : 0;
    int size  = (reviewSearchRequest.getSize() != null && reviewSearchRequest.getSize() > 0)
            ? reviewSearchRequest.getSize() : 10;

    String searchTypeCode = Optional.ofNullable(reviewSearchRequest.getSearchType())
                                    .filter(s -> !s.isBlank())
                                    .orElse("ST001");

    Pagination pagination = Optional.ofNullable(reviewSearchRequest.getPaginationVO())
                                    .orElseGet(() -> {
                                        var p = new Pagination();
                                        reviewSearchRequest.setPaginationVO(p);
                                        return p;
                                    });
    pagination.setPageIndex(page + 1);
    pagination.setRecordCountPerPage(size);

    // 핵심: 코드 → Enum → 전략
    ReviewSearchType type = ReviewSearchType.fromCodeOrDefault(searchTypeCode);

    // 총건수
    long totalCount = type.getCounter().apply(reviewSearchService, reviewSearchRequest);

    if (totalCount > 0) {
        pagination.setRecordTotalCount((int) totalCount);
        new PaginationZeroInfo().paginationProcess(pagination);
    } else {
        pagination.setRecordTotalCount(0);
        pagination.setPageLastIndex(1);
        pagination.setPageStartIndex(1);
        pagination.setPageEndIndex(1);
        pagination.setPageStartRecordIndex(0);
        pagination.setPageEndRecordIndex(0);
    }

    List<ReviewDocument> reviewDocuments =
            type.getSearcher().apply(reviewSearchService, reviewSearchRequest);

    var body = ReviewSearchResponse.builder()
            .lists(reviewDocuments)
            .build();
    body.setPaginationVO(pagination);

    return ResponseEntity.ok(body);
}

 

지금 적용한 Enum-Strategy Pattern 전개 과정을 한눈에 보기 위하여 그림으로 간단히 정리해 봤습니다.

 

마지막으로 정리 하면, enum-strategy는 거창한 패턴은 아니지만, 분기를 매핑으로 바꾸는 작은 습관이라고 생각합니다.

이 작은 습관이 코드 가독성/확장성/테스트 용이성을 눈에 띄게 끓어 올릴 수 있고, 코드 뿐만 아니라 서비스 품질 또한 향상 시킬 수 있는 시작이라고 생각합니다.

'BackEnd' 카테고리의 다른 글

[Spring] SiteMesh란?  (0) 2025.04.05
[Node.js] cookie-parser란?  (0) 2025.03.23
[Spring] RestfulA API HATEOAS 설정  (0) 2024.08.21
[Spring] Validation API 유효성 체크  (0) 2024.08.04