방금 넣은 문서가 왜 바로 안 보일까?

2026년 07월 23일

opensearch

시리즈OpenSearch 내부 구조#5
# OpenSearch# NRT# Refresh# Flush# 튜닝
목차

들어가며

이런 코드를 짰다가 당황한 적이 있을 겁니다.

client.index(product)              // 201 Created
val result = client.search("무선")  // 0건 💥

색인은 분명 성공했는데 검색하면 없습니다. 잠시 뒤 다시 검색하면 나오고요.

OpenSearch가 Near Real-Time(NRT) 검색 엔진이기 때문입니다. Real-Time이 아니라 Near Real-Time이에요. 이번 글에서는 이 “Near”가 정확히 뭘 의미하는지, 왜 그럴 수밖에 없는지 알아봅니다.

ℹ️

이번 글의 범위

  • 다루는 것: NRT의 원인, 왜 1초인지, refresh/flush/commit 구분, 튜닝 방법
  • 다루지 않는 것: replica 간 refresh 타이밍 차이(6편)

원인은 이미 배웠다

사실 답은 4편에서 이미 나왔습니다.

세그먼트는 불변이다. 그래서 새 문서는 새 세그먼트를 만들어야 보인다.

문서를 색인하면 일단 메모리 buffer에 들어갑니다. 그런데 검색은 세그먼트만 뒤집니다. buffer는 쳐다보지도 않아요.

검색 대상 아님검색세그먼트만 조회Bufferdoc5

왜 buffer를 안 볼까요? buffer는 그냥 문서 덩어리일 뿐, 역색인이 만들어지지 않았기 때문입니다. 2편에서 봤듯 검색이 빠른 건 역색인 덕분인데, buffer에는 그게 없어요. 여기서 검색하려면 100만 건을 하나씩 다 열어봐야 합니다.

그래서 역색인을 만들어 세그먼트로 굳히는 순간까지 기다려야 하고, 그 작업이 바로 refresh입니다.

Near Real-Time = “refresh 주기만큼 늦다”


직접 겪어보기

시뮬레이터로 확인해봅시다. 이번엔 refresh에만 집중합니다.

🔬Near Real-Time 체험
📝In-memory Buffer검색 불가
비어 있음
📜Translogflush 완료
비어 있음
💾세그먼트검색 가능 · 한번 만들면 수정 불가
아직 세그먼트가 없습니다
🔍 지금 검색되는 문서
하나도 없습니다
💡 문서를 색인해보세요.

실습 — 색인과 가시성의 시차

  1. 문서 색인 을 누릅니다 → 빨간 경고가 뜹니다: “색인했는데도 아직 검색되지 않습니다”
  2. 몇 번 더 눌러도 마찬가지입니다. buffer만 쌓여요
  3. refresh 를 누릅니다 → 그 순간 전부 검색 결과에 나타납니다

실제 OpenSearch에서는 3번을 자동으로 1초마다 해줍니다. 그래서 “넣고 1초쯤 뒤엔 보인다”가 되는 거죠.


왜 하필 1초인가

여기서 좋은 질문이 나옵니다. 어차피 자동으로 할 거면 0.01초마다 하면 안 되나요? 그럼 사실상 실시간일 텐데요.

안 됩니다. refresh는 공짜가 아니기 때문입니다.

refresh를 한 번 할 때마다 세그먼트가 하나씩 생깁니다. 그리고 세그먼트가 많아지면 두 가지 비용이 발생해요.

비용내용
검색이 느려짐검색은 모든 세그먼트를 뒤져야 한다. 파일이 많을수록 느려짐
merge 부담 폭증늘어난 세그먼트를 합치느라 CPU와 디스크 I/O를 계속 소모

짧게 (0.1s)길게 (30s)1초refresh 주기세그먼트 폭증merge 부담 ↑색인 처리량 ↓세그먼트 적음색인 처리량 ↑30초 동안 안 보임절충점

1초는 “사람이 실시간이라고 느끼는 최소 단위”와 “시스템이 감당할 수 있는 부담” 사이의 절충점입니다. 물리 법칙이 아니라 실용적 기본값이에요. 그래서 바꿀 수 있습니다.


헷갈리는 3형제: refresh, flush, commit

이 세 단어가 뒤섞이면 문서를 읽어도 이해가 안 됩니다. 정리하고 갑시다.

동작하는 일검색에 영향안전해지나
refreshbuffer → 새 세그먼트를 만들어 검색에 노출✅ 여기서 보이기 시작❌ 아직 메모리
flush세그먼트를 디스크에 커밋 + translog 비움없음✅ 완전히 안전
commitLucene 용어. OpenSearch의 flush가 내부에서 하는 일없음✅

한 줄로 외우면 이렇습니다.

refresh는 “보이게 하는” 것, flush는 “안전하게 하는” 것.

⚠️

Lucene의 “flush”와 OpenSearch의 “flush”는 다른 뜻입니다.

이게 혼란의 진짜 원인입니다.

  • Lucene의 flush: 메모리 buffer를 세그먼트로 만드는 것 → OpenSearch의 refresh에 해당
  • OpenSearch의 flush: Lucene commit을 호출하고 translog를 비우는 것

같은 단어가 층에 따라 다른 의미로 쓰입니다. 블로그 글을 읽을 때 어느 층 이야기인지 확인하세요.


튜닝: refresh 주기 조절하기

기본값 1초가 모든 상황에 맞지는 않습니다.

대량 색인 — 아예 꺼버리기

수백만 건을 밀어넣는 배치 작업이라면, 색인 중에 검색될 필요가 없습니다. 그럼 refresh를 끄는 게 압도적으로 빠릅니다.

PUT /products/_settings
{ "index.refresh_interval": "-1" }

색인이 끝나면 되돌리고, 한 번 수동으로 refresh합니다.

PUT /products/_settings
{ "index.refresh_interval": "1s" }

POST /products/_refresh

로그성 데이터 — 길게 늘리기

로그나 통계처럼 “1초 안에 보일 필요가 없는” 데이터는 주기를 늘리면 색인 처리량이 올라갑니다.

PUT /logs-2026.07/_settings
{ "index.refresh_interval": "30s" }
💡

검색 요청이 없는 인덱스는 refresh를 알아서 멈춥니다.

일정 시간 동안 검색이 한 번도 안 들어온 인덱스는 refresh를 건너뜁니다. 아무도 안 보는데 세그먼트를 만들 이유가 없으니까요. 검색이 다시 들어오면 재개합니다.

덕분에 “쓰기만 하고 거의 안 읽는” 인덱스가 많아도 클러스터가 낭비하지 않습니다.


색인 직후 바로 읽어야 할 때

테스트 코드나 “저장하고 목록 새로고침” 같은 흐름에서는 시차가 곤란합니다. 세 가지 선택지가 있습니다.

① refresh=wait_for — 권장

다음 refresh가 돌 때까지 응답을 보류합니다. 최대 1초 정도 기다리지만 추가 세그먼트를 만들지 않습니다.

POST /products/_doc?refresh=wait_for
{ "name": "무선 노트북 마우스" }

② refresh=true — 주의

즉시 refresh를 강제합니다. 빠르지만 호출할 때마다 세그먼트가 하나씩 생깁니다.

POST /products/_doc?refresh=true
{ "name": "무선 노트북 마우스" }
🚨

반복문 안에서 refresh=true를 쓰지 마세요.

1만 건을 넣으면 세그먼트가 1만 개 생깁니다. merge가 폭주하면서 클러스터 전체가 느려집니다. 실무에서 자주 나오는 사고예요.

배치 색인은 _bulk + refresh 끄기가 정석입니다.

③ ID로 직접 조회 — 검색이 아니라면

문서 ID를 아는 경우엔 아예 다른 길이 있습니다.

GET /products/_doc/1234

_search가 아니라 _doc으로 ID를 지정해 가져오는 조회는 refresh를 기다리지 않습니다. 세그먼트에 없으면 translog를 뒤져서라도 최신 내용을 돌려주거든요.

ID 조회는 실시간, 검색은 준실시간.

“저장하고 그 문서를 다시 보여주기” 정도라면 이 방법이 가장 깔끔합니다.


실무에서 이걸 모르면 생기는 일

증상원인대응
테스트가 간헐적으로 실패색인 후 refresh 전에 검색테스트에서 refresh=wait_for
저장 후 목록에 안 나옴동일ID 조회 or wait_for
배치 색인이 극도로 느림반복문에서 refresh=truerefresh 끄고 _bulk
세그먼트 수가 수천 개refresh 주기가 너무 짧음refresh_interval 늘리기

정리

  1. NRT의 원인은 불변성이다. 검색은 세그먼트만 보는데, 새 문서가 세그먼트가 되려면 refresh가 필요하다.
  2. refresh 주기가 곧 “Near”의 크기다. 기본 1초.
  3. 1초는 절충점이다. 짧으면 세그먼트가 폭증하고, 길면 안 보인다.
  4. refresh는 “보이게”, flush는 “안전하게” — 완전히 다른 일이다.
  5. 대량 색인은 refresh를 끄고, 즉시 읽어야 하면 wait_for 또는 ID 조회를 쓴다.

다음 글에서는 지금까지 본 세그먼트가 replica에도 그대로 복제되는지 알아봅니다. 답은 “기본적으로는 아니다”인데, 그게 꽤 재미있는 결과를 낳습니다.

🤔색인 직후 그 문서를 반드시 읽어야 합니다. 가장 부작용이 적은 방법은?

참고자료

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