개발

왜 AWS OpenSearch에서 Self-hosted Elasticsearch로 전환했나

커스텀 분석기, 운영 자유도, 그리고 검색 품질을 위한 선택

왜 AWS OpenSearch에서 Self-hosted Elasticsearch로 전환했나왜 AWS OpenSearch에서 Self-hosted Elasticsearch로 전환했나

Search Platform Engineer

안녕하세요, 강남언니에서 검색 플랫폼 엔지니어로 일하고 있는 썬입니다. 강남언니 검색 시스템은 그동안 AWS Managed OpenSearch 위에서 운영되어 왔는데요, 최근 이를 Self-managed Elasticsearch로 전환하는 작업을 진행했습니다. 왜 잘 돌아가던 Managed 서비스를 떠나야 했는지, 그리고 전환 과정에서 어떤 고민과 선택이 있었는지 그 경험을 공유해 보려 합니다.

1. 왜 AWS managed OpenSearch가 아니었을까?

AWS OpenSearch Service는 관리형(Managed) 서비스입니다. AWS가 클러스터를 대신 운영해주니 편하지만, 그 편리함에는 대가가 있었습니다. 우리가 떠나야 했던 이유는 크게 두 가지입니다.

1-1. 같지만 조금 다르다

OpenSearch는 2021년 Elastic이 Elasticsearch의 라이선스를 Apache 2.0에서 SSPL로 변경하면서, AWS가 Elasticsearch 7.10.2를 포크하여 만든 프로젝트입니다. 출발점은 같은 코드베이스이지만, 포크 이후 각자의 방향으로 발전하면서 기능의 차이가 조금씩 생겼습니다.

대표적인 예가 flattened 타입입니다. Elasticsearch에서는 JSON 객체 전체를 하나의 필드로 저장하면서도 Painless 스크립트에서 서브필드에 접근하여 스코어링이나 정렬 로직을 구현할 수 있습니다. OpenSearch에도 비슷한 목적의 flat_object 타입이 구현되어 있지만, Painless 스크립트에서 서브필드 값에 접근할 수 없다는 제약이 있었습니다.

우리는 검색 랭킹에서 Painless 스크립트를 활용한 정렬 로직을 사용하고 있었기 때문에, 이 차이는 직접적인 기능의 한계로 이어졌습니다. 실제로 랭킹에 필요한 피처 데이터를 아래처럼 인덱싱을 아예 끈 object로 우회하고 있었습니다:

"ranking_feature": {
  "type": "object",
  "dynamic": "true",
  "enabled": false
}

이렇게 하면 Painless에서 params._source['ranking_feature']로 접근은 가능하지만, 필드가 인덱싱되지 않아 검색이나 필터에 활용할 수 없고 _source에 원본 JSON이 통째로 저장되어 문서 크기가 불필요하게 커집니다. flattened라면 이런 우회 없이 해결되는 문제였습니다.

이처럼 같은 뿌리에서 나왔지만, 포크 이후 생긴 기능 차이가 점점 우리의 발목을 잡고 있었습니다.

1-2. 커스텀 플러그인을 설치할 수 없다

두 번째이자 가장 결정적인 문제는 커스텀 플러그인 설치가 불가능하다는 것이었습니다. 이게 왜 치명적이었는지 이해하려면, 먼저 우리가 한국어 검색에서 겪고 있던 문제를 봐야 합니다.

"핀제거"를 검색했을 뿐인데

검색 엔진의 핵심은 사용자가 입력한 텍스트를 어떻게 분석(analyze)하느냐에 있습니다. 이걸 가장 잘 보여주는 사례가 하나 있습니다.

강남언니에서 사용자가 "핀제거"를 검색한다고 합시다. 윤곽수술 후 고정용 핀을 제거하는 시술을 찾고 있는 겁니다. 단순한 검색어죠.

Nori(Elasticsearch 기본 한국어 분석기)는 이 단어를 이렇게 분석합니다:

입력: "핀제거"
Nori 분석 결과: 핀 / 제거

여기까진 괜찮습니다. 그런데 문제는 사용자가 조금 더 구체적으로 검색할 때 발생합니다.

입력: "윤곽수술핀제거"
Nori 분석 결과: 윤곽 / 수술 /// 제거

"핀"이 사라지고 "피 + ㄴ"이 되었습니다.

형태소 분석기가 "핀제거"를 "피(blood) + ㄴ(관형형 어미) + 제거"로 해석한 겁니다. 사용자는 윤곽수술 후 핀을 제거하는 시술을 찾고 싶었는데, 검색 엔진은 "윤곽수술에서 피를 제거하는" 결과를 보여주게 됩니다.

같은 단어 "핀제거"가 앞에 "윤곽수술"이 붙느냐 안 붙느냐에 따라 전혀 다른 분석 결과를 만들어내는 것입니다. 이런 문제는 한두 개가 아니었습니다.

또 다른 골치 아픈 사례가 "점"입니다. 강남언니 플랫폼에서 "점"은 최소 세 가지 의미로 쓰입니다:

사용자가 "점제거"를 검색하면 피부의 점을 없애는 시술을 찾는 건데, 분석기가 "점"을 지점(店)의 의미로 해석하면 "강남점", "인천점" 같은 병원 지점명이 결과에 섞입니다. 반대로 "강남점"을 검색하면 점 제거 시술 후기가 나올 수도 있습니다.

한 글자짜리 한국어 단어가 도메인 안에서 완전히 다른 의미를 가지는 이 문제는, 사전에 단어를 등록하는 것만으로는 풀기 어렵습니다. 앞뒤 문맥을 함께 보고 어떤 "점"인지 판단하는 로직이 분석기 내부에 필요합니다. (이 부분은 현재도 계속 개선 중인 과제입니다.)

이 외에도:

일반 한국어 형태소 분석기는 일상 언어에 최적화되어 있기 때문에, "핀"보다 "피+ㄴ"이 더 자연스러운 한국어라고 판단합니다. 의료미용이라는 특수한 도메인에서는 이런 판단이 완전히 틀릴 수 있는데 말이죠.

Nori로는 부족했다

AWS OpenSearch에서 사용할 수 있는 한국어 분석기는 Nori와 은전한닢(Seunjeon) 두 가지입니다. 우리는 이 중 Nori를 사용하고 있었습니다. Nori는 MeCab 기반의 형태소 분석기로, 일반적인 한국어 텍스트를 처리하는 데는 무리가 없습니다.

하지만 Nori의 사용자 사전(user dictionary) 기능은 단순히 "이 단어는 쪼개지 말아줘" 수준입니다. 의료미용 도메인에서 필요한 건 더 정교한 제어였습니다:

이 문제를 근본적으로 해결하려면 형태소 분석기 자체를 커스터마이징할 수밖에 없었습니다.

커스텀 분석기 '강모란'의 탄생

Nori의 한계를 근본적으로 해결하기 위해, 우리는 직접 한국어 형태소 분석기를 만들기로 했습니다. 이름은 '강모란' — 강남언니 + 코모란(Komoran)을 합친 이름입니다.

이게 가능했던 건 팀의 NLP 엔지니어가 코모란의 개발자였기 때문입니다. 코모란은 격자(Lattice) 기반 탐색과 확률 모델을 활용하는 오픈소스 한국어 형태소 분석기인데, 그 개발자가 팀에 있으니 사전에 단어를 추가하는 수준이 아니라 분석기의 알고리즘 자체를 우리 도메인에 맞게 개조할 수 있었습니다.

"윤곽수술핀제거"에서 "핀"이 "피+ㄴ"이 되는 문제, "강남점"과 "점제거"의 "점"을 구분하는 문제 — 이런 것들은 사전만으로는 풀 수 없습니다. 복합명사를 어디서 끊을지, 어떤 품사로 태깅할지를 결정하는 확률 모델과 탐색 알고리즘 자체를 의료미용 도메인에 맞게 튜닝해야 합니다. 코모란의 내부를 완전히 이해하는 개발자가 팀에 있었기에 가능한 일이었습니다.

그런데 올릴 수가 없다

강모란은 Elasticsearch 플러그인 형태로 패키징되어 클러스터에 설치됩니다. 그런데 당시 AWS OpenSearch Service는 커스텀 플러그인 설치를 지원하지 않았습니다.

2024년 11월, AWS OpenSearch Service에 커스텀 플러그인 기능이 추가되면서 가능성이 열리나 싶었습니다. 하지만 막상 강모란 플러그인(및 해외 서비스용 다국어 분석기들)을 설치해 보니 검증 단계에서 실패했고, 우리가 만든 커스텀 분석기를 AWS OpenSearch 위에서 동작시키는 것은 끝내 불가능했습니다.

2. 왜 Self-managed를 선택했을까?

선택지는 두 가지였습니다:

Elastic CloudSelf-managed ES
한국어 분석커스텀 분석기 사용 가능커스텀 분석기 자유롭게 사용
운영 부담Elastic이 관리직접 운영 필요
비용높음 (+ 네트워크 전송 비용)~$449/월
플러그인커스텀 플러그인 가능원하는 것 모두
네트워크외부 통신 (레이턴시 + 비용)AWS 내부 통신

Elastic Cloud는 커스텀 플러그인 설치가 가능하고 관리형 서비스의 편의성도 제공하지만, 서비스 자체 비용이 높은 데다 기존 인프라가 AWS 위에 있어 네트워크 레이턴시와 데이터 전송 비용까지 추가로 발생합니다. 검색은 모든 사용자 요청에 관여하는 만큼, 이 비용은 무시할 수 없는 수준이었습니다.

결국 커스텀 한국어 분석기를 쓸 수 있으면서 AWS 내부 통신이 가능한 Self-managed Elasticsearch로 가기로 결정했습니다.

3. 인프라를 어떻게 구축하고 운영했나

관리형 서비스를 버린다는 건, 그동안 AWS가 해주던 일을 직접 해야 한다는 뜻입니다. 클러스터를 EC2 위에 직접 구성한 뒤, 가장 먼저 한 일은 안정적으로 운영할 수 있는 환경을 만드는 것이었습니다.

3-1. 모니터링 — Datadog

OpenSearch 시절에는 AWS CloudWatch가 기본으로 제공됐지만, EC2 위의 ES는 직접 메트릭을 수집해야 합니다.

모니터링 스택으로는 Prometheus + Grafana 조합도 좋은 선택이지만, 우리는 이미 서비스 전반에서 Datadog을 사용하고 있었기 때문에 기존 인프라를 그대로 활용했습니다. 별도의 모니터링 시스템을 구축할 필요 없이 Datadog의 Elasticsearch 인티그레이션만 연결하면 되었습니다.

Datadog의 Elasticsearch 인티그레이션으로 클러스터 상태, JVM, 인덱싱/검색 성능, 디스크 사용률 등의 메트릭을 수집하고, 핵심 지표에 대해 알람을 설정했습니다.

Self-managed에서 모니터링이 왜 중요한지 보여주는 사례가 있습니다. Elasticsearch는 디스크 사용률이 일정 수준을 넘으면 인덱스를 자동으로 read_only_allow_delete 상태로 전환합니다. 검색 자체는 되지만 새로운 글이 색인되지 않으니, 사용자가 방금 올린 후기가 검색에 노출되지 않는 상황이 벌어집니다. 또한 관리형 서비스에서는 노드가 죽으면 자동으로 교체되지만, EC2 기반에서는 OOM이나 인스턴스 장애로 노드가 내려가도 직접 감지하고 대응해야 합니다. 모니터링 없이는 이런 상황을 알아채기 어렵습니다.

모니터링은 선택이 아니라 필수입니다.

3-2. 인프라 — 자동화된 클러스터 구축

관리형 서비스의 원클릭 배포만큼은 아니더라도, 최대한 자동화하고 싶었습니다. 그래서 TerraformGitHub Actions를 활용하여 클러스터 구축 과정을 코드로 관리합니다.

Terraform은 인프라(EC2, ALB, Route53, 보안 그룹)의 생성과 변경을 담당합니다. terraform apply 한 번이면 EC2 인스턴스(마스터, 데이터, Kibana)부터 ALB, Route53 레코드까지 클러스터에 필요한 모든 인프라가 한 번에 생성됩니다.

인프라가 생성되면, Elasticsearch 자체의 설정과 클러스터 구성은 별도의 초기화 스크립트가 담당합니다. Terraform이 EC2를 생성할 때 templatefile()을 통해 클러스터 이름, 배포 ID, 노드 역할 등의 파라미터를 주입한 user-data 스크립트를 전달하고, 인스턴스가 부팅되면 이 스크립트가 자동으로 실행됩니다:

AWS 콘솔에서 수동으로 인스턴스를 만들고 설정하는 대신, 인프라 전체를 코드로 관리할 수 있게 되었습니다.

3-3. 배포 — 블루그린 전략

분석기 사전 업데이트, ES 버전 업그레이드, 플러그인 변경 등 클러스터를 운영하다 보면 변경이 필요한 순간이 많습니다. 이런 변경사항을 안전하게 적용하기 위해 블루그린 전략으로 배포합니다.

카나리나 롤링 배포도 고려했지만, Elasticsearch 클러스터 특성상 노드 간 버전이 섞이거나 플러그인 버전이 불일치하면 클러스터 상태가 꼬일 수 있습니다. 블루그린 전략은 새 클러스터를 별도로 준비해야 하므로 데이터를 다시 투입하는 시간이 더 들지만, 이런 문제에서 자유롭고 기존 클러스터에 영향 없이 문제 발생 시 쉽게 롤백할 수 있습니다.

블루그린 상태도 Terraform 코드로 관리합니다. Blue와 Green 각각에 enabledlive 두 가지 플래그를 두었습니다. enabled로 클러스터 인프라의 생성과 삭제를 제어하고, live로 서비스 도메인(primary URL)과 검증용 도메인(candidate URL) 중 어디에 연결할지를 결정합니다.

전환 워크플로우

33ed9338-d3d4-80d3-a063-d61435ba84a9.jpg33ed9338-d3d4-80d3-a063-d61435ba84a9.jpg

데이터 투입은 기본적으로 전체 재색인(Full Reindex)을 수행합니다. 필요에 따라 기존 클러스터에서 remote reindex하거나 스냅샷으로 복원하는 방식도 사용할 수 있게 해두었습니다.

현재는 각 과정을 수동으로 실행하고 있어, 향후 Airflow를 활용해 워크플로우 전체를 자동화하는 것을 계획하고 있습니다.

4. 어떻게 마이그레이션했나

검색은 서비스의 핵심 기능이기 때문에, 마이그레이션 중 다운타임은 허용할 수 없었습니다. 우리는 듀얼 라이트(Dual Write) 방식으로 무중단 전환을 설계했습니다.

4-1. 전체 흐름

우리 검색 시스템은 CQRS 패턴으로 검색기(읽기)와 색인기(쓰기)가 이미 분리되어 있었습니다. 마이그레이션을 위해 분리한 것이 아니라 원래 이런 구조였기 때문에, 읽기와 쓰기의 대상 클러스터를 독립적으로 전환할 수 있었습니다.

Phase 1. 클러스터 준비

새 ES 클러스터를 구축하고, 강모란 플러그인을 설치하고, 인덱스 매핑을 생성합니다. 서비스 트래픽은 기존 그대로입니다.

Phase 2. 전체 색인 (Full Reindex)

데이터 소스에서 새 ES 클러스터로 전체 데이터를 밀어넣습니다. 서비스는 OpenSearch를 계속 바라봅니다.

Phase 3. 듀얼 라이트 (Dual Write)

색인기가 OpenSearch와 Elasticsearch에 동시에 씁니다. 검색기는 아직 OpenSearch를 읽습니다.

Phase 4. 읽기 전환

검색기가 새 ES를 바라보도록 전환합니다. 색인기는 롤백 대비로 양쪽에 계속 씁니다. 문제가 발생하면 검색기의 읽기 대상만 OpenSearch로 되돌리면 즉시 원복됩니다.

Phase 5. 완료 — OpenSearch 종료

안정 운영 확인 후 듀얼 라이트를 중단하고, 기존 OpenSearch 클러스터를 종료합니다.

각 Phase는 검색기와 색인기의 설정 변경만으로 진행할 수 있었고, 문제가 발생하면 검색기의 설정만 되돌리면 즉시 롤백이 가능했습니다.

4-2. 전환 전 검증

트래픽을 전환하기 전에 검색 결과가 동일한지 확인해야 했습니다.

검색 결과 정합성을 검증하기 위해 어드민에 비교 도구를 만들었습니다. 주요 검색어를 OpenSearch와 Elasticsearch에 동시에 날려서, 결과 순서와 문서가 동일한지 나란히 비교할 수 있게 했습니다.

33ed9338-d3d4-80a0-982c-eafd7bb840ab.png33ed9338-d3d4-80a0-982c-eafd7bb840ab.png

이 도구로 검증을 마친 뒤 전환을 진행했습니다.

5. 마무리 — 언제 Self-managed를 고려해야 할까

솔직히 말하면, Self-managed를 다른 회사나 팀에게 추천하기는 쉽지 않습니다.

작은 팀이 클러스터 운영, 분석기 개선, 인프라 관리까지 직접 해야 하기 때문에 적지 않은 리소스가 여기에 묶입니다. 또한 관리형 서비스에서는 보이지 않던 자잘한 이슈들도 꾸준히 발생합니다. 샤드가 간헐적으로 unhealthy 상태에 빠지거나, 노드 장애 대응, 버전 업그레이드, 인증/보안 설정까지 — 알람은 울리는데 원인 파악에 시간이 걸리는 경우가 많습니다. 반복되는 패턴은 자동화하며 운영 부담을 줄여가고 있지만, 이 비용이 사라지는 것은 아닙니다.

이 비용을 감수하고도 Self-managed를 선택한 건, 그래야만 쓸 수 있는 것이 있었기 때문입니다. 팀 내에 뛰어난 검색 엔지니어분들이 많고, 형태소 분석기를 직접 만들 수 있는 NLP 개발자가 있기 때문입니다. 이미 일본·중국·태국까지 서비스를 운영하고 있어 각 언어에 맞는 커스텀 분석기가 필요하고, Self-managed의 자유도는 점점 더 중요해지고 있습니다.

만약 커스텀 플러그인이 필요 없거나, Nori의 사용자 사전 수준으로 충분하다면 관리형 서비스를 떠날 이유가 없습니다. Self-managed는 "운영할 수 있느냐"보다 "떠나야만 하는 이유가 있느냐"로 판단해야 합니다.

비슷한 고민을 하고 있는 팀에게 이 글이 조금이나마 도움이 되길 바랍니다.

이 글은 강남언니(힐링페이퍼) 검색 플랫폼 팀의 경험을 바탕으로 작성되었습니다.