본문 바로가기
JAVA/Convention

Strategy pattern 사용 소스 예제

by devljy 2025. 1. 16.

 

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. 마무리

 

정리하자면,

  1. DataManager 인터페이스 = “전략”을 추상화
  2. 구현체(AksManager, PksManager, PsManager, …) = 실제 “RDB → ES 적재 로직”
  3. 호출부(ProvideProdService 등) = “전략”을 주입받아, 공통 과정(인덱스 교체/Alias 교체 등) 실행
  4. 각각의 데이터 타입(주소/상품/키워드)에 최적화된 로직을 독립적으로 작성하면서도,
    공통 인터페이스/메서드 구조를 재활용할 수 있게 됨

이 패턴을 통해, 검색 인덱스가 여러 가지 형태로 늘어나도,
코드 전체를 복붙하거나 복잡하게 조건문(if/else)을 추가하지 않고,
“새로운 Manager를 추가만 하면” 확장이 가능해집니다.

이것이 바로 Strategy Pattern의 장점이며,
Elasticsearch 무중단 인덱스 교체(혹은 다른 공통 로직)와도 깔끔하게 결합할 수 있는 설계 방식입니다.