strategy Pattern 사용 예제를 공유하고자합니다.
아래에서는 RDB 의 여러 테이블을 가져와서 Elastic search 로 인덱싱하는 과정을 다루겠습니다 .
1. 배경
시스템을 운영하다 보면, 상황(데이터 타입)에 따라 적재 로직이 달라지지만, 공통 흐름(예: “인덱스 생성 → 데이터 적재 → 기타 작업”)은 대체로 동일한 경우가 있습니다.
- 예) “주소 키워드 검색용 데이터”, “상품 키워드 검색용 데이터”, “날짜별 상품 검색용 데이터” 등
모두 Elasticsearch 인덱스를 만들어 적재하지만,
실제 RDB 쿼리나 적재 대상 클래스가 서로 다름.
이때 Strategy Pattern을 쓰면, “공통된 과정”은 유지하면서도 “세부 로직”을 다형성으로 분리할 수 있습니다.
2. 핵심 구조: DataManager 인터페이스와 구현체들
2.1 DataManager 인터페이스
public interface DataManager {
// 어떤 alias(인덱스 별칭)을 쓸까?
String getAliasName();
// RDB로부터 데이터를 읽어 신규 인덱스(newIndexName)에 적재
void insertEsFromRdb(String newIndexName);
// 인덱스 생성 시 사용할 ES 매핑(JSON) 경로
String getJsonBodyPath();
// (필요 시) RDB 간 동기화 메서드
void insertMainToSub() throws Exception;
}
- 전략(Strategy): “데이터 적재 로직”을 정의하는 인터페이스
- “Alias 명, 매핑 파일 경로, RDB에서 가져올 데이터 쿼리/적재 로직” 등을
구현체가 각자 다르게 구현 가능
2.1.1 구현체 예시 : AksManager
@Component
@Slf4j
public class AksManager implements DataManager {
@Autowired
MainProvideProdDao mainProvideProdDao;
@Autowired
ElasticSearchService elasticSearchService;
@Value("${resources.csv_url}")
String csvUrl;
@Override
public String getAliasName() {
return "address_keyword_search";
}
@Override
public void insertEsFromRdb(String newIndexName) {
// (1) RDB 데이터 조회
List<AddressKeywordSearchRdb> _list = mainProvideProdDao.selectAddressKeywordSearch();
// (2) ES 적재용 객체 변환
List<AddressKeywordSearch> paramList =
_list.stream().map(AddressKeywordSearch::new).collect(Collectors.toList());
// (3) Bulk 적재
elasticSearchService.bulkinsert(
pks -> String.valueOf(pks.getIdx()),
paramList,
newIndexName
);
}
@Override
public String getJsonBodyPath() {
return "elastic/address_keyword_search.json";
}
@Override
public void insertMainToSub() throws Exception {
// 예: 메인 DB에서 데이터를 CSV로 변환, 서브 DB에 넣는 등
...
}
}
2.1.2 구현체 예시 : PksManager
@Component
@Slf4j
public class PksManager implements DataManager {
@Override
public String getAliasName() {
return "product_keyword_search";
}
@Override
public void insertEsFromRdb(String newIndexName) {
List<ProductKeywordSearchRdb> _list = mainProvideProdDao.selectProductKeywordSearch();
List<ProductKeywordSearch> paramList =
_list.stream().map(ProductKeywordSearch::new).collect(Collectors.toList());
elasticSearchService.bulkinsert(
pks -> String.valueOf(pks.getIdx()),
paramList,
newIndexName
);
}
@Override
public String getJsonBodyPath() {
return "elastic/product_keyword_search.json";
}
@Override
public void insertMainToSub() throws Exception {
// RDB 간 동기화 등 구현
}
}
3. 전략을 사용하는 곳: ProvideProdService
DataManager를 구현한 여러 매니저(Strategy)들을 하나의 List로 주입받아 (@Autowired List<DataManager> dataManagers), -- ***** 단 이기능은 스프링 프레임워크의 Bean 기능을 활용한 것입니다. Bean 으로 등록된 요소들을 List 자료구조를 통해 인터페이스를 구현한 모든 bean 을 가져오는것 ****
특정 메서드에서 반복을 돌며 동일한 함수를 호출하는 방식으로 동작합니다.
@Service
@Slf4j
public class ProvideProdService {
@Autowired
List<DataManager> elasticDataManager;
@Autowired
ElasticSearchService elasticSearchService;
/**
* 신규 인덱스 생성 + RDB -> ES 적재 + alias 교체 작업
* (다양한 매니저마다 로직이 다름)
*/
public void transFormRdbToEs() throws Exception {
LocalDate currentDate = LocalDate.now();
String formattedDate = currentDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 여러 DataManager 구현체 각각에 대해
for (DataManager dataManager : elasticDataManager) {
// ElasticSearchService 의 replaceIndexProcess 에 주입
elasticSearchService.replaceIndexProcess(dataManager, formattedDate);
}
}
...
}
@Service
@Slf4j
public class ElasticSearchServiceImpl implements ElasticSearchService {
@Override
public void replaceIndexProcess(DataManager dataManager, String formattedDate) throws Exception {
final String indexBody = this.loadJsonResource(dataManager.getJsonBodyPath());
//product_keyword_search_old
final String oldIndexName = this.findIndexByAlias(dataManager.getAliasName());
log.info("findIndexByAlias : alias 로 등록된 인덱스를 조회한다. , oldIndexName =====> {}" , oldIndexName ) ;
// result : product_keyword_search_new
final String newIndexName = this.getNextVersionIndex(dataManager.getAliasName(), formattedDate, oldIndexName);
log.info("getNextVersionIndex : 기존 인덱스 기반으로 다음버전의 index 명을 구한다. , newIndexName =====> {}" , newIndexName ) ;
// [1] 인덱스 생성
this.createIndex(newIndexName, indexBody);
// [1-1 ] 첫 인덱스 생성시 데이터 집어넣기전에 alias 를 먼저지정
if( oldIndexName == null ) {
// [2] plusVersionIndexName 에 등록되어있는 alias Name 삭제 , 새로운 인덱스에 alais Name 을 추가 ( oldIndex 에서 new Index 로 alias Name 교체)
this.changeAlias(newIndexName, oldIndexName, dataManager.getAliasName());
// [3] 새로운 이름의 인덱스에 데이터를 insert
insert_product_keyword_search(dataManager, newIndexName) ;
}
// [ 1-2 ] 기존에 인덱스가 존재시에는 데이터를 모두 완성시킨다음에 alias 변경
else {
// [2] 새로운 이름의 인덱스에 데이터를 insert
insert_product_keyword_search(dataManager, newIndexName) ;
// [3] plusVersionIndexName 에 등록되어있는 alias Name 삭제 , 새로운 인덱스에 alais Name 을 추가 ( oldIndex 에서 new Index 로 alias Name 교체)
this.changeAlias(newIndexName, oldIndexName, dataManager.getAliasName());
// [4] formatted Date 와 alias 가 포함되어있을때 덱스 였을시 삭제
this.deleteIndex_v2(oldIndexName , formattedDate );
}
}
}
ElasticsearchService에 구현된 replaceIndexProcess() 메서드는 새로운 인덱스를 생성한 뒤, 기존 인덱스와 새 인덱스 사이의 Alias를 교체하여, 무중단으로 검색 대상 인덱스를 변경하는 로직을 담고 있습니다.
메서드의 **첫 번째 매개변수인 DataManager**는, 호출 시점에 주입되는 구현체 인스턴스를 가리키며, 이 메서드가 진행되는 흐름에 따라 해당 인스턴스 내부에 정의된 구체 메서드가 실행됩니다.
정리하자면, ElasticsearchService는 인덱스 교체와 같은 공통 작업을 처리하고, 각 DataManager 구현체(AksManager, PksManager 등)는 **세부적인 커스터마이징(예: RDB에서 가져올 데이터, 인덱스 매핑 설정 등)**을 담당합니다.
따라서 DataManager 인터페이스만 구현하면, 프로젝트 요구사항에 맞춰 자유롭게 커스터마이징하면서 replaceIndexProcess ( 특정한 인덱스를 교체하는 ) 기능을 사용할 수 있게 됩니다.
4. 마무리
정리하자면,
- DataManager 인터페이스 = “전략”을 추상화
- 구현체(AksManager, PksManager, PsManager, …) = 실제 “RDB → ES 적재 로직”
- 호출부(ProvideProdService 등) = “전략”을 주입받아, 공통 과정(인덱스 교체/Alias 교체 등) 실행
- 각각의 데이터 타입(주소/상품/키워드)에 최적화된 로직을 독립적으로 작성하면서도,
공통 인터페이스/메서드 구조를 재활용할 수 있게 됨
이 패턴을 통해, 검색 인덱스가 여러 가지 형태로 늘어나도,
코드 전체를 복붙하거나 복잡하게 조건문(if/else)을 추가하지 않고,
“새로운 Manager를 추가만 하면” 확장이 가능해집니다.
이것이 바로 Strategy Pattern의 장점이며,
Elasticsearch 무중단 인덱스 교체(혹은 다른 공통 로직)와도 깔끔하게 결합할 수 있는 설계 방식입니다.
'JAVA > Convention' 카테고리의 다른 글
스프링 부트에서 일관된 API 응답과 예외 처리를 구현하는 방법 (1) | 2025.01.15 |
---|