
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 |