글을 작성하게 된 계기
ElasticSearch를 학습하며 JVM 옵션에 대해 학습하게 되었고, 알게된 내용을 정리하기 위해 글을 작성하게 되었습니다.
1. 역색인과 역색인이 빠른 이유
역색인(Inverted Index)이란, 문서의 내용을 구성하는 각 단어를 기반으로, 해당 단어가 어떤 문서에 등장하는지 미리 저장해 두는 자료구조입니다. 즉, 단어를 중심으로 문서를 찾는 구조로 설계되어 있어, 검색 속도를 크게 향상시키는 핵심적인 기술입니다. 일반적으로 Elasticsearch, Apache Lucene 같은 검색 엔진에서 텍스트 검색 성능 향상을 위해 자주 사용하는 자료구조입니다.
역색인이 얼마나 빠른지를 정확히 이해하려면, 먼저 일반적인 방식(정방향 인덱스)과 비교하는 것이 가장 좋습니다.
1-1. 정방향 인덱스의 한계
일반적인 정방향 인덱스는 문서 중심의 구조입니다. 즉, 문서 ID가 키이고, 그 문서가 포함하고 있는 단어의 목록이 값이 되는 형태입니다. 예를 들어 다음과 같은 문서들이 있다고 가정해 봅시다.
1
2
3
문서1 - 사과 포도 바나나
문서2 - 바나나 딸기
문서3 - 사과 딸기 바나나
이 내용을 정방향 인덱스로 저장하면 다음과 같은 형태가 됩니다. 이 방식에서 “사과”라는 단어를 포함한 문서를 찾으려면, 문서 전체를 순차적으로 확인해야 합니다.
1
2
3
문서1 → [사과, 포도, 바나나]
문서2 → [바나나, 딸기]
문서3 → [사과, 딸기, 바나나]
이를 수도 코드로 보면 다음과 같습니다. 여기서 문제는 문서 수가 많아지면 검색 시간이 문서 개수에 비례하여 급격히 증가한다는 점입니다. 문서가 100만 개라면, 최대 100만 번의 순차 탐색이 필요하여 성능이 떨어집니다.
1
2
3
for 문서 in 문서1 문서2 문서3; do
grep "사과" "$문서"
done
2-2. 역색인(Inverted Index)의 구조와 예시
반면에 역색인은 단어 중심의 구조입니다. 키는 “단어”, 값은 “해당 단어가 등장하는 문서의 목록” 형태로 구성됩니다. 위에서 예시로 든 문서를 역색인으로 표현하면 다음과 같습니다.
1
2
3
4
사과 [문서1, 문서3]
포도 [문서1]
바나나 [문서1, 문서2, 문서3]
딸기 [문서2, 문서3]
역색인은 이미 각 단어가 어떤 문서에 등장하는지 미리 정리되어 있기 때문에, 검색 시 단 한 번의 탐색만으로 결과를 바로 얻을 수 있습니다. 검색 키워드 하나로 바로 문서 목록을 얻기 때문에, 문서의 수가 아무리 많아도 탐색 비용이 매우 낮고, 속도가 일정하게 유지됩니다. 즉, 문서가 많아져도 거의 일정한 속도로 빠른 검색이 가능합니다.
1
2
3
4
5
6
7
8
9
# 역색인(Inverted Index) 구조 생성
declare -A inverted_index
inverted_index[사과]="문서1 문서3"
inverted_index[포도]="문서1"
inverted_index[바나나]="문서1 문서2 문서3"
inverted_index[딸기]="문서2 문서3"
# "사과"를 포함한 문서를 찾는 검색 예시
echo "사과가 포함된 문서: ${inverted_index[사과]}"
2-3. 역색인이 빠른 이유
역색인이 빠른 이유를 정리하면 크게 다음과 같은 원리가 있습니다.
탐색의 방향 전환: 문서 중심 탐색(정방향 인덱스)에서 단어 중심 탐색(역색인)으로 방향이 바뀌어, 데이터 접근 횟수가 극적으로 줄어듭니다.
사전 분석된 데이터(Pre-analyzed Data): 문서가 저장될 때 미리 텍스트 분석(Analyzing) 과정을 거쳐 토큰화(tokenization)되어 저장되므로, 검색 시 추가 분석 과정이 필요 없게 됩니다. 검색 성능이 크게 향상됩니다.
효율적인 자료구조 사용:역색인은 내부적으로 효율적인 자료구조(B-tree, Trie 등)를 사용하여 단어 탐색 자체를 매우 빠르게 수행합니다.
빠른 연산(AND, OR): 여러 단어를 동시에 검색할 때도 각각의 단어에 연결된 문서 목록을 빠르게 병합(AND)하거나 합집합(OR)을 구할 수 있어 복잡한 검색도 매우 빠르게 처리할 수 있습니다.
실제로 Elasticsearch와 같은 검색 엔진은 역색인을 더욱 효율적으로 관리하기 위해 추가적으로 아래와 같은 최적화를 수행합니다.
- 세그먼트(Segment): 역색인을 작은 단위로 나누어 검색 요청 시 필요한 부분만 빠르게 찾아 접근하는 방식을 사용하여 성능을 극대화합니다.
- 캐싱(Caching): 자주 검색하는 결과를 미리 캐싱하여 반복적인 요청에 대해서는 더 빠르게 응답할 수 있도록 합니다.
- 압축(Compression): 역색인을 압축하여 메모리 사용량을 줄이고 I/O 성능을 높입니다. 압축된 상태에서도 검색 성능을 유지할 수 있도록 최적화된 알고리즘을 사용합니다.
3. 정리
역색인은 매우 빠른 검색 성능을 제공하지만, 모든 상황에서 완벽한 것은 아니며 몇 가지 단점과 한계를 가지고 있습니다.
추가적인 저장공간: 역색인을 저장하기 위한 공간이 필요하므로 저장소 공간이 증가합니다.업데이트 비용: 문서가 자주 수정되거나 추가될 때 역색인 갱신에 부담이 있습니다. 하지만 Elasticsearch는 이를 최소화하기 위해 세그먼트 구조를 활용하여 효율적으로 처리합니다.
가장 먼저 고려해야 할 점은 추가적인 저장 공간이 필요하다는 점입니다. 역색인은 각 단어마다 해당 단어가 등장하는 모든 문서의 목록을 별도로 저장하기 때문에, 원본 데이터를 저장하는 공간 외에도 이 색인 정보를 저장할 별도의 공간이 필수적으로 필요합니다. 따라서, 데이터가 많아지고 단어의 종류가 다양해질수록 역색인의 크기도 급격히 증가할 수 있습니다. 특히, 방대한 양의 텍스트 데이터를 가진 서비스나 애플리케이션의 경우, 역색인을 저장하기 위해 상당한 양의 디스크나 메모리 공간을 추가로 확보해야 할 수 있습니다.
두 번째로 중요한 단점은 바로 색인을 유지하고 갱신하는 비용이 높다는 점입니다. 문서가 새롭게 추가되거나, 기존의 문서가 수정 또는 삭제되면 역색인 구조도 함께 갱신되어야 합니다. 이 과정은 단순히 문서를 저장하는 것과는 달리, 기존의 색인 구조를 변경하고 새로운 데이터를 다시 분석하여 색인에 포함시키는 과정이 포함됩니다. 이로 인해 데이터가 자주 추가되거나 변경되는 시스템에서는 역색인의 유지 및 갱신 과정이 성능에 부담을 줄 수 있으며, 경우에 따라 실시간성이 떨어질 수도 있습니다.
하지만 Elasticsearch와 같은 검색 엔진에서는 이러한 단점을 최소화하기 위해 효율적인 데이터 구조와 방법을 사용합니다. 대표적으로 Elasticsearch는 세그먼트(segment)라는 구조를 활용하여, 색인의 갱신을 아주 작은 단위로 나누어서 처리합니다. 문서가 추가될 때마다 작은 세그먼트가 생성되고, 이러한 세그먼트들은 시간이 지남에 따라 점진적으로 병합(merge)됩니다. 이렇게 하면 전체 역색인을 한번에 변경하는 대신, 작은 단위의 변경만을 처리하게 되므로 갱신 비용을 크게 줄일 수 있습니다.
즉, 역색인은 분명히 추가적인 저장 공간과 관리 비용이라는 부담을 가지지만, Elasticsearch와 같은 최신 검색엔진에서는 이러한 부담을 최소화할 수 있는 다양한 전략과 방법을 적용하여 단점을 충분히 보완하고 있습니다. 따라서 역색인을 사용하는 시스템을 운영할 때는 이러한 특성을 이해하고, 데이터의 추가나 변경 빈도를 고려하여 적절한 하드웨어 자원을 할당하고 유지 관리 전략을 수립하는 것이 중요합니다.
3-1. 지나치게 긴 문서를 인덱싱할 때의 성능 저하
첫 번째로 고려할 요소는 지나치게 긴 문서입니다. 역색인은 기본적으로 문서를 분석(Analyzing)하고 단어(token) 단위로 나누어 저장하는 방식을 사용합니다. 그런데 문서의 길이가 너무 길어지면 그만큼 분석 과정에서 더 많은 메모리와 CPU 자원을 소모하게 됩니다.
긴 문서를 처리할 때의 성능 저하는 다음과 같은 이유로 발생합니다.
토큰화 비용 증가: 문서가 길어지면 내부 분석기(Analyzer)가 처리해야 할 텍스트 양이 늘어나고, 결과적으로 단어(token)의 수가 급격히 증가합니다. 이 과정에서 메모리 사용량과 분석 소요 시간이 크게 늘어납니다.
포스팅 리스트(posting list) 길이 증가: 역색인의 구조상, 각 단어마다 문서 ID 목록(포스팅 리스트)을 보관하는데, 문서 길이가 길면 등장하는 단어들이 많아져 포스팅 리스트가 길어집니다. 포스팅 리스트가 길어질수록 인덱스 조회 시간이 증가하여, 검색 속도에 악영향을 줄 수 있습니다.
3-2. 고유한 단어 수(Cardinality)가 많을 때 성능 이슈
두 번째로 고려할 점은 단어의 고유한 개수가 매우 많을 때, 즉 높은 Cardinality를 가진 데이터에서의 성능 저하 문제입니다. 예를 들어, 소스 코드, 로그 데이터 또는 각종 특수문자가 혼합된 데이터처럼 고유한 단어 수가 매우 많은 데이터는 역색인의 성능에 부정적 영향을 미칠 수 있습니다.
높은 Cardinality로 인해 생기는 성능 문제는 다음과 같은 이유 때문입니다.
메모리 소비 증가: 역색인은 각 단어마다 별도의 색인 항목을 유지합니다. 따라서 고유한 단어 수가 많아지면, 저장해야 할 색인 데이터도 많아지기 때문에 메모리 소비가 증가하여 메모리 압박과 성능 저하를 초래할 수 있습니다.
검색 과정에서 비효율성 발생: 특정 검색어가 매우 많은 문서에서 발견되거나 매우 적은 문서에서만 발견되는 극단적인 경우가 빈번해지면, Elasticsearch의 점수 산정 및 정렬 과정에서 성능 저하가 나타날 수 있습니다.
이를 해결하기 위해 다음과 같은 방법을 사용할 수 있습니다.
- 불필요한 단어(예: 특수문자나 코드 주석)를 색인에서 제외하도록 Analyzer를 최적화합니다.
3-3. 매우 빈번한 데이터 업데이트 환경에서의 성능 유지 방안
마지막으로 살펴볼 점은 데이터 업데이트 빈도입니다. 역색인은 기본적으로 정적 데이터를 빠르게 검색할 수 있도록 설계된 구조입니다. 그러나 문서가 매우 빈번하게 추가, 수정, 삭제되는 환경에서는 역색인의 성능이 저하될 수 있습니다.
빈번한 데이터 갱신이 성능에 부정적 영향을 미치는 이유는 다음과 같습니다.
색인 재구성 및 병합(Merge) 비용: Elasticsearch는 문서가 추가되거나 변경될 때마다 작은 세그먼트를 생성하여 인덱스를 업데이트합니다. 이렇게 작은 세그먼트들이 쌓이면 성능 저하를 막기 위해 주기적으로 병합(Merge) 작업이 수행되는데, 이 병합 작업은 상당한 CPU와 디스크 I/O를 소모하여 성능을 저하시킬 수 있습니다.
GC(Garbage Collection) 빈도 증가: 업데이트가 빈번한 환경에서는 JVM의 힙 메모리 사용량이 급격히 변동하면서, GC 발생 빈도가 증가하고 이는 ElasticSearch의 성능 저하로 이어질 수 있습니다.
이 문제를 해결하기 위한 방법은 다음과 같습니다.
- 업데이트 빈도가 매우 높은 데이터는 색인 주기를 조정하거나 bulk indexing을 통해 성능을 최적화합니다.
- 세그먼트 병합 정책(Merge Policy)을 적절히 조정하여 병합 작업이 자주 일어나지 않도록 설정합니다.
- JVM 옵션에서 GC 최적화를 통해, 빈번한 메모리 변동에 따른 GC 부담을 최소화합니다.