Amazon OpenSearch를 통한 검색 기능 개발기
Amazon OpenSearch를 통한 서비스 검색 기능 개발기

Amazon OpenSearch를 통한 검색 기능 개발기
서비스의 검색 기능을 개발하기 위한 산전수전을 기록해둔 문서 입니다.
사용자는 업로드된 수많은 콘텐츠들을 정리된 정보로 잘 검색되야 하기 때문에 , 주요 기능이었으며 , Amazon OpenSearch를 통해 이를 개발하였습니다.
RDB와 Amazon OpenSearch 간의 데이터 정합성 맞추기
먼저 RDB에 적재된 검색대상 정보들을 , Amazon OpenSearch에 Insert 시켜야만 했습니다.
서비스의 백엔드는 Spring FrameWork가 주를 이루었으며, 개인적으로도 Java 언어가 가장 친숙하였기 때문에 Spring Batch를 통해 ES에 Insert 하기로 하였습니다.
Spring Batch가 주기적으로 작동하면서 RDB에 데이터를 ES에 적재시키로 하였는데, 전체 데이터를 모두 Insert 시키기에는 많은 제약사항이 있었습니다.
그중 가장 큰 문제는 데이터 정합성 문제 입니다.
사용자가 업로드한 경우 기본적으로 데이터는 BackEnd RDB에 저장되게 됩니다. Amazon OpenSearch를 통해 검색할 경우, 검색 쿼리 대상은 RDB가 아닌 OpenSearch 이기 때문에, RDB와 Amazon OpenSearch는 항상 검색 대상 데이터의 정합성이 맞아야 합니다.
만약 Batch가 아무리 짧은 주기로 동작한다 하더라도, (짧은 주기라는것 조차 제약사항...) 동작하기 직전 업로드된 데이터들은 검색될 수 없습니다. 이러한 제약사항을 해결하기 위해 메세지 큐 서비스를 도입하는것을 고려했지만, 비용적 측면과 관리포인트 증가 이 두가지가 부담스러웠기에 포기하였습니다.
따라서 Insert API를 전체 데이터 Insert Batch , 부분 데이터 Insert Batch 2가지로 나누기로 결정하였습니다.
부분 데이터 Insert Batch와 전체 데이터 Insert Batch

전체 데이터 Insert Batch 는 부분 데이터 Insert Batch 보다 더 긴 주기를 가지고 동작합니다.
- 예시 주기
- 전체 Insert Batch : 하루에 한번
- 부분 Insert Batch : 10분에 한번
또한 각 Batch는 AWS Lambda를 통해 배포하였으며, EventBridge Scheduler로 Lambda Function을 트리거하여 동작합니다.
둘로 나눈 이유는 아래와 같습니다.
1. 성능 최적화
빈번한 변동이 발생하지 않는 대부분의 데이터는 재색인의 부담 없이 그대로 유지되며,
변동된 부분만 색인되기 때문에 전체적인 색인 작업의 부담이 줄어듭니다.
2. 데이터 일관성
전체 데이터를 주기적으로 색인함으로써 RDB와 Amazon OpenSearch 간의 데이터 불일치 문제를 최소화할 수 있습니다.
3. 세그먼트 관리
전체 색인 작업 중에 데이터를 flush하면 세그먼트 조각화를 방지하고, 성능을 최적화하는 데 도움을 줍니다.
가장 중심이 되는것은 색인 부담을 줄이는것과 , 세그먼트 관리에 있습니다.
세그먼트 조각화
Amazon OpenSearch에서는 데이터를 Lucene 기반의 인덱스 형태로 저장합니다. 이 인덱스는 여러 세그먼트로 나뉘며, 각 세그먼트는 독립적으로 검색할 수 있는 작은 인덱스 단위입니다.
세그먼트(Segment)란?
Amazon OpenSearch의 각 문서는 내부적으로 Lucene 기반의 인덱스에 저장됩니다.
Lucene 인덱스는 다수의 세그먼트로 구성되며, 각 세그먼트는 독립적인 인덱스로 간주될 수 있습니다.
데이터가 추가/수정될 때마다 새로운 세그먼트가 생성될 수 있습니다.
이러한 세그먼트들은 시간이 지남에 따라 병합될 수도 있습니다.
세그먼트 조각화(Segment Fragmentation)란?
시간이 지나면서 데이터의 추가, 수정, 삭제 작업이 반복되면 많은 수의 작은 세그먼트들이 생성될 수 있습니다.
이렇게 많은 수의 작은 세그먼트들이 존재하면 검색 성능에 부정적인 영향을 줄 수 있습니다.
이를 세그먼트 조각화라고 합니다.
왜 문제가 되는가?
검색을 수행할 때 각 세그먼트를 개별적으로 검색해야 하므로 세그먼트의 수가 많을수록 검색 성능에 부담이 될 수 있습니다.
또한, 많은 수의 작은 세그먼트들은 디스크 공간을 비효율적으로 사용하게 만들 수 있습니다.
해결책은?
Amazon OpenSearch는 세그먼트 병합(segment merge)이라는 프로세스를 통해 주기적으로 작은 세그먼트들을 큰 세그먼트로 합치는 작업을 수행합니다.
이렇게 하면 세그먼트의 수가 줄어들고, 디스크 공간 사용도 효율적으로 되며, 검색 성능도 개선될 수 있습니다.
또한, Amazon OpenSearch에서는 force merge라는 API를 제공하여, 사용자가 수동으로 세그먼트 병합을 요청할 수도 있습니다. 그러나 force merge는 I/O 작업이 많이 발생하므로 클러스터에 부담을 주게 될 수 있으므로 주의하여 사용해야 합니다.
결론
요약하면, 세그먼트 조각화는 Amazon OpenSearch의 세그먼트 구조와 관련된 현상으로, 이로 인해 검색 성능에 부정적인 영향을 받을 수 있습니다. 따라서 적절한 인덱스 관리 및 세그먼트 병합 전략을 통해 성능을 최적화하는 것이 중요합니다.
RDB상 변동된 부분만 색인하는 SpringBatch와 , 전체 정보를 조회하여 색인하는 SpringBatch로 분리시켜서 세그먼트 조각화를 방지하였습니다.
전체 정보를 색인하며 Amazon OpenSearch의 데이터를 flush함으로써 여러 작은 세그먼트들을 병합시켜 세그먼트 조각화 방지
Batch API의 동시성 문제 해결
Batch API를 두가지로 나누면서, 두개의 API가 동시에 ES에 Insert 하면서 RDB에 대해 동시성 문제가 발생하는 경우가 있습니다.
이러한 동시성 문제는 Redis 분산 Lock 방안을 도입 하며 해결하였습니다.
상대방 Batch Application이 동작중인지를 판별하여, 동시에 동작되며 동시성이 깨지는 경우가 생기지 않도록 방지합니다.
Redis 분산 Lock
부분 데이터 색인 배치와 , 전체 데이터 색인 배치는 서로다른 주기로 작동합니다.
각 배치는 잡이 시작될 때 , Redis Key를 생성하고 , 잡이 완벽하게 완료될 경우에만 Redis Key를 제거합니다.
각 배치가 생성하고 지우는 키는 따로 존재합니다.
ex)
- partKey -> 부분 색인 배치가 생성
- allKey -> 전체 색인 배치가 생성
전체 데이터 색인
- 전체 데이터 색인 시 , 잡이 수행되기 전 부분 데이터 색인 Batch가 생성한 partKey 가 존재하는지 확인합니다.
- 만약 존재 할 경우, 해당 키가 제거될때 까지 미리 설정해둔 시간동안 전체 데이터 색인 App은
Thread.sleep(시간)을 통해 정지합니다. - 만약 정지하고 있을 때, 특정 긴 시간(timeout seconds) 이 지난다면 Timeout 에러를 발생시키고 Application이 중지됩니다.
- 키가 없다고 판별됐을 경우에만 배치 Application이 allKey를 생성 후 잡을 수행하고 , 잡이 완료될 경우에만 Redis에 등록한 allKey를 제거합니다.
부분 데이터 색인
- 전체 데이터 색인과 마찬가지로 , 잡이 수행되기 전 전체 데이터 색인 Batch가 생성한 allKey 가 존재하는지 확인합니다.
- 만약 존재 할 경우, 해당 키가 제거될때 까지 미리 설정해둔 시간동안 전체 데이터 색인 App은
Thread.sleep(시간)을 통해 정지합니다. - 만약 정지하고 있을 때, 특정 긴 시간(timeout seconds) 이 지난다면 Timeout 에러를 발생시키고 Application이 중지됩니다.
- 키가 없다고 판별됐을 경우에만 배치 Application이 partKey를 생성 후 잡을 수행하고 , 잡이 완료될 경우에만 Redis에 등록한 partKey를 제거합니다.
위같은 로직을 통해 각 색인 Batch Application이 동시에 작동하며 동시성 이슈를 발생시키지 못하도록 방지하였습니다.
위와 같은 방법들로 API 자체의 기능적 문제들은 해결하게 되었는데, 서비스의 비즈니스 로직 요구사항들을 해결하여야만 하였습니다.
RDB 와 ES 간 어떤 기준으로 동기화를 해야 하는가 ? 라는 맹점이 가장 주요하게 작용하였습니다.
Amazon OpenSearch - RDB 동기화 기준
Amazon OpenSearch와 동기화를 맞춰야되는 기준은 다음과 같을것 입니다.
- 새로운 카테고리가 등록되었을 경우
- 관리자만 가능
- 새로운 콘텐츠가 업로드 되었을 경우
- 업로드된 콘텐츠의 카테고리가 변경 (추가 , 제거) 되었을 경우
- 콘텐츠가 삭제되었을 경우
위 이벤트가 발생했을 때 , Amazon OpenSearch에도 반영되야만 합니다.
따라서 event Table을 생성하였습니다.
- eventTable
| 컬럼 명 | Data Type | 요약 정보 | 비고 |
| _id | int | row별 Id 값 | PK |
| action | varchar(20) | 어떤 이벤트가 발생했는지 체크 - UPDATE && DELETE && Create | - |
| firstIdInfo | int | 카테고리들의 식별번호 | FK |
| changesValue | varchar(500) | doc 별 실제변경내용 | 해당 칼럼으로 바로 Amazon OpenSearch에 Insert 할 수 있도록 bulk 형 json으로 넣어두기 |
| updateDateTime | timestemp | 이벤트 발생 일자 | - |
Event Table에는 변경된 정보를 Amazon OpenSearch Bulk API를 통해서 바로 Insert 할 수 있도록 Json의 형태로 저장합니다.
이후 부분 Insert Batch에서는 해당 Json 값만 꺼내와서 Bulk API로 색인합니다.
Bulk API VS 일반 색인
bulk API는 Amazon OpenSearch에서 다수(대량) 인덱스에 대해 색인, 갱신, 제거를 단일 API로 수행할 수 있게 해주는 기능 입니다.
bulk API를 사용한다면 아래와 같은 이점을 가질 수 있습니다.
- 일반 색인과 다르게 대량의 문서를 한번에 호출로 Amazon OpenSearch에 색인하기 때문에 , 속도가 훨씬 빠릅니다.
- 다수 인덱스를 색인하려고 여러번 Amazon OpenSearch를 호출하지 않기 때문에 , 네트워크 오버헤드를 최소화할 수 있습니다.
- Bulk API를 사용하여 인덱스, 업데이트, 삭제 작업을 혼합하여 실행할 수 있습니다.
결론
위와 같은 단계를 거쳐서 서비스의 검색엔진 기능을 Amazon OpenSearch로 구축하게 되었습니다.
현재의 서비스는 관리포인트와 비용적 부담으로 인해 실시간성이 부족한 서비스라 느낍니다. 만약 업로드 로직과 색인 로직 사이에 Kafka를 도입하였다면, 업로드된 순간 검색이 가능 했을 것 입니다.
허나 소규모 스타트업에선, 기능적인 완벽함보다 관리포인트를 줄이고 비용적 이점을 가져가는것이 맞다고 느낍니다. 서비스가 잘 작동하고 , 많은 사용자를 유치하여 완벽한 기능인 서비스로 가야한다 생각합니다.
발전시킬 부분이 많은 현재의 구성을 점차 보완해나갈 예정이며, 검색 서비스를 구축해야하는 개발자분들에게 도움이 되었으면 합니다.


