OpenSearch는 왜 이렇게 여러 겹일까?

2026년 07월 23일

opensearch

시리즈OpenSearch 내부 구조#1
# OpenSearch# Lucene# 샤드# 세그먼트# 아키텍처
목차

들어가며

OpenSearch를 처음 열어보면 용어가 쏟아집니다.

클러스터 > 노드 > 인덱스 > 샤드 > 세그먼트

다섯 겹입니다. 그냥 데이터를 넣고 검색하고 싶을 뿐인데 왜 이렇게 복잡할까요?

답부터 말하면, 각 계층이 서로 다른 문제를 풀기 위해 생겼기 때문입니다. 누가 멋있어 보이려고 층을 쌓은 게 아니라, 문제가 하나씩 생길 때마다 층이 하나씩 늘어난 겁니다. 그래서 “이 층은 무슨 문제를 푸는가”만 알면 다섯 겹이 한 줄로 꿰어집니다.

ℹ️

이번 글의 범위

  • 다루는 것: 다섯 계층이 각각 푸는 문제, 샤드와 세그먼트의 관계
  • 다루지 않는 것: 검색이 빠른 이유(2편), 색인 과정(3편), 복제 상세(6편)

도서관으로 먼저 감 잡기

추상적인 용어를 늘어놓기 전에, 도서관 하나를 떠올려 봅시다.

OpenSearch도서관특징
클러스터도서관 전체건물 여러 채를 묶어서 하나의 도서관으로 운영
노드건물 한 채실제로 책을 보관하고 사서가 일하는 곳
인덱스주제 구역“소설”, “과학” 처럼 종류별로 나눈 구역
샤드책장 하나구역이 너무 커서 책장 여러 개로 나눠 담음
세그먼트인쇄된 책 한 권한번 인쇄하면 내용을 고칠 수 없음

마지막 줄이 이 시리즈 전체에서 가장 중요합니다.

세그먼트는 “인쇄된 책”입니다. 이미 찍어낸 책의 3페이지만 살짝 고칠 수는 없죠. 고치려면 새 판을 인쇄해야 합니다. 세그먼트도 똑같습니다. 한번 만들어지면 절대 수정되지 않아요.

이 한 가지 성질에서 앞으로 다룰 거의 모든 것이 파생됩니다. 색인 중에도 검색이 안 막히는 이유(4편)도, 방금 넣은 문서가 바로 안 보이는 이유(5편)도 전부 여기서 나옵니다.


각 계층이 푸는 문제

이제 비유를 걷어내고, 각 층이 어떤 문제 때문에 생겼는지 봅시다.

문제: 데이터가 장비 한 대에 안들어간다해결: 샤드로 쪼갠다문제: 장비가 죽으면 데이터가사라진다해결: replica로 복사한다문제: 장비 여러 대를 하나처럼다뤄야 한다해결: 클러스터로 묶는다문제: 검색이 느리면 쓸모가 없다해결: 세그먼트에 미리 정리해둔다

정리하면 이렇습니다.

계층이게 없으면 생기는 문제
클러스터장비 여러 대를 각각 관리해야 함. 어디에 뭐가 있는지 사람이 추적
노드— (실제로 일하는 주체. 클러스터의 구성원)
인덱스상품과 로그가 한 덩어리에 섞임. 설정도 따로 못 줌
샤드데이터가 장비 한 대 용량을 넘으면 더 못 넣음
replica장비 한 대 죽으면 그 데이터는 영영 못 읽음
세그먼트검색할 때마다 전체 문서를 처음부터 다 뒤져야 함

가장 흔한 오해: 샤드는 노드에 “걸쳐” 있지 않다

여기서 많이들 헷갈립니다.

“샤드는 여러 노드에 분산된다”

이 문장은 절반만 맞습니다. 정확히는 이렇습니다.

  • 인덱스가 여러 샤드로 쪼개진다 (분할)
  • 그 샤드들이 여러 노드에 흩어진다 (배치)
  • 샤드 하나는 언제나 노드 하나 안에 통째로 들어있다

샤드 하나가 반으로 갈려서 노드 A와 B에 절반씩 얹히는 일은 없습니다.

클러스터Node CP2R1Node BP1R0Node AP0R2

primary 3개 / replica 1개인 인덱스는 위처럼 총 6조각으로 배치됩니다. 초록이 primary(원본), 파랑이 replica(복사본)예요.

💡

primary와 replica는 같은 노드에 놓이지 않습니다.

당연한 이야기입니다. 같은 노드에 원본과 복사본을 함께 두면, 그 노드가 죽는 순간 둘 다 사라지니까요. 위 그림에서도 P0은 Node A에, 그 복사본 R0은 Node B에 있습니다.


왜 샤드는 쪼갤 수 없을까

샤드 하나가 곧 Lucene 인덱스 하나이기 때문입니다.

ℹ️

Lucene(루씬)이란?

자바로 만든 검색 라이브러리입니다. 실제로 문서를 저장하고 검색하는 일은 전부 Lucene이 합니다. OpenSearch는 그 위에 “여러 대로 나눠서 돌리고, 복사본을 관리하고, HTTP API로 노출하는” 껍데기를 씌운 것에 가깝습니다.

Lucene 인덱스는 자기완결적입니다. 검색에 필요한 모든 것이 그 안에 들어있어요.

[P0] 샤드 하나의 실체 (= Lucene 인덱스 하나)
├── _0.cfs          ← 세그먼트 (인쇄된 책 1)
├── _1.cfs          ← 세그먼트 (인쇄된 책 2)
├── _2.cfs          ← 세그먼트 (인쇄된 책 3)
├── _2_1.liv        ← 삭제 표시 (4편에서 다룸)
└── segments_5      ← 목차 (어떤 책들이 유효한지)

이 묶음의 절반만 떼어내면 검색이 되지 않습니다. 그래서 물리적으로 쪼갤 수가 없어요.

여기서 중요한 결론이 하나 나옵니다.

검색 요청이 오면, 각 샤드는 자기 안에서만 답을 찾고, 그 부분 답들을 모아서 최종 결과를 만든다.

샤드끼리 서로를 참조하지 않습니다. 각자 독립적으로 자기 몫을 검색하고, 코디네이터 노드가 결과를 합칩니다. 이 구조 덕분에 노드를 늘리면 검색이 그만큼 빨라집니다.

검색 요청코디네이터 노드P0 자기 안에서 검색P1 자기 안에서 검색P2 자기 안에서 검색부분 결과 합치기최종 결과


직접 확인해보기

_cat API로 실제 구조를 눈으로 볼 수 있습니다.

# 클러스터에 노드가 몇 대 있나
GET _cat/nodes?v

# 인덱스 목록과 각 인덱스의 샤드/문서 수
GET _cat/indices?v

# 샤드가 어느 노드에 어떻게 배치됐나  ← 가장 유용
GET _cat/shards?v

_cat/shards 결과는 이렇게 나옵니다.

index     shard prirep state   docs  store node
products  0     p      STARTED 1200  4.2mb node-a
products  0     r      STARTED 1200  4.2mb node-b
products  1     p      STARTED 1189  4.1mb node-b
products  1     r      STARTED 1189  4.1mb node-c

prirep 컬럼의 p가 primary, r이 replica입니다. 0번 샤드의 원본은 node-a에, 복사본은 node-b에 있는 게 보이죠. 앞서 말한 “원본과 복사본은 다른 노드에” 규칙이 실제로 지켜지고 있습니다.

세그먼트까지 보고 싶다면 이렇게 합니다.

GET _cat/segments/products?v

여기서 세그먼트가 여러 개인 게 보일 텐데, 왜 하나가 아니라 여러 개인지가 3편의 주제입니다.


정리

  1. 다섯 계층은 각각 다른 문제를 푼다. 샤드는 용량 문제, replica는 장애 문제, 세그먼트는 검색 속도 문제.
  2. 샤드는 노드에 걸쳐 있지 않다. 샤드 하나 = Lucene 인덱스 하나 = 노드 하나 안에 통째로.
  3. 세그먼트는 인쇄된 책이다. 한번 만들면 고칠 수 없다. 이 성질이 앞으로 나올 거의 모든 것의 원인이다.

다음 글에서는 세그먼트 안에 정확히 뭐가 들어있길래 검색이 그렇게 빠른지 알아봅니다.

🤔샤드에 대한 설명으로 올바른 것은?

참고자료

© 2026, 미나리와 함께 만들었음