"You Know, for Search." — Elasticsearch 최초 공개 블로그 포스트 제목 (2010년 2월 8일)

검색창에 키워드를 입력하면 0.1초 만에 결과가 뜬다. GitHub에서 수백만 줄의 코드를 검색하고, Netflix에서 85개 이상의 클러스터가 실시간 로그를 분석하고, Wikipedia에서 수천만 문서를 즉시 찾아낸다. 이 모든 것의 뒤편에 Elasticsearch가 있다.

이 글에서는 Elasticsearch가 왜 만들어졌고, 어떻게 단순한 검색 엔진에서 엔터프라이즈 데이터 플랫폼으로 성장했는지, 그리고 2026년 현재 어떤 위치에 있는지를 정리한다.


1. 탄생: 아내의 요리 레시피를 검색하고 싶었다 (2004~2010)

Compass — 전신의 이야기

Elasticsearch의 창시자 Shay Banon은 이스라엘 출신 개발자다. 2004년, 아내가 런던의 Le Cordon Bleu 요리학교에 다니던 시절, Banon은 아내의 방대한 레시피 컬렉션을 검색할 수 있는 엔진을 만들고 싶었다. 이것이 Compass라는 Java 기반 검색 프레임워크의 시작이었다.

Compass는 Apache Lucene 위에 구축된 검색 라이브러리였다. Lucene은 Doug Cutting이 만든 강력한 전문 검색(full-text search) 엔진이지만, 그 자체로는 분산 처리도, REST API도, 클러스터링도 지원하지 않았다. Compass는 이런 갭을 메우려 했지만, Banon은 근본적인 한계를 느꼈다.

"처음부터 분산 시스템으로 다시 만들자"

Compass 3버전을 개발하던 중, Banon은 전면 재작성이 필요하다고 결론 내렸다. 핵심 설계 원칙은 세 가지였다:

  1. 처음부터 분산 아키텍처: 단일 노드가 아니라 클러스터 기반
  2. JSON over HTTP: Java뿐 아니라 모든 언어에서 접근 가능한 RESTful API
  3. 실시간(Near Real-Time) 검색: 문서 색인 후 거의 즉시 검색 가능

Banon은 당시 잘 다니던 Gigaspaces를 퇴사하고 약 2년간 전업으로 개발에 매진했다. 그리고 2010년 2월 8일, "You Know, for Search"라는 제목의 블로그 포스트와 함께 Elasticsearch 0.4.0을 오픈소스로 공개했다.


2. 성장: ELK 스택의 탄생과 폭발적 채택 (2010~2018)

커뮤니티의 자발적 생태계

Elasticsearch가 공개되자, 커뮤니티에서 자연스럽게 생태계가 만들어졌다:

  • Jordan Sissel이 다양한 소스에서 데이터를 수집·변환하는 Logstash를 개발
  • Rashid Khan이 Elasticsearch 데이터를 시각화하는 Kibana를 개발

세 사람은 교류하다가 결국 힘을 합쳤다. ELK Stack(Elasticsearch + Logstash + Kibana)의 탄생이다. 이 조합은 로그 분석의 사실상 표준이 되었다.

회사 설립과 급성장

시점이벤트
2010.02 Elasticsearch 0.4.0 오픈소스 공개
2012.02 Elasticsearch BV 암스테르담에서 설립 (Shay Banon, Steven Schuurman, Uri Boness, Simon Willnauer)
2013 Series A $10M (Benchmark Capital), Kibana 공식 출시
2014 Series C $70M, 총 투자 $104M
2015 사명을 Elastic으로 변경, Elastic Cloud 출시, Beats(경량 데이터 수집기) 추가 → ELK가 Elastic Stack으로 확장
2017 Swiftype 인수 (엔터프라이즈 검색), Opbeat 인수 (APM)
2018.10 NYSE 상장 (ESTC), 상장 첫날 주가 거의 2배, 기업가치 약 $5B

특히 인상적인 것은 2012년 당시 월 20만 다운로드라는 수치였다. Index Ventures의 투자자들은 "포트폴리오 기업의 CTO들이 Elasticsearch를 '기적 같은 소프트웨어'라고 부르며 입을 모았다"고 회상한다.

글로벌 분산 조직이라는 실험

Elastic은 설립 초기부터 완전 분산 조직을 운영했다. 개발팀의 90% 이상이 실리콘밸리 밖에 분포했다. 특정 사무실에 모이지 않고 전 세계에서 최고의 인재를 채용하는 이 모델은, Elasticsearch 자체의 "분산 아키텍처" 철학과도 맞닿아 있다.


3. Elasticsearch의 핵심 아키텍처 — 왜 빠른가

역색인(Inverted Index)

Elasticsearch가 빠른 핵심 이유는 역색인 구조다. 일반 데이터베이스가 "문서 → 단어"로 저장한다면, Elasticsearch는 **"단어 → 문서"**로 저장한다.

일반 DB 방식 (순방향):
  문서1: "Elasticsearch는 검색 엔진이다"
  문서2: "Redis는 캐시 엔진이다"

역색인 방식:
  "검색"   → [문서1]
  "캐시"   → [문서2]
  "엔진"   → [문서1, 문서2]
  "Elasticsearch" → [문서1]
  "Redis"  → [문서2]

이것은 책의 맨 뒤에 있는 **색인(Index)**과 같은 원리다. 수백만 페이지를 처음부터 읽는 대신, 색인에서 키워드를 찾아 해당 페이지로 바로 이동한다. 이 구조 덕분에 데이터가 아무리 많아도 검색 시간이 선형으로 증가하지 않는다.

분산 아키텍처: 샤드와 레플리카

 
 
Elasticsearch 클러스터
├── Node 1
│   ├── Shard 0 (Primary)
│   └── Shard 2 (Replica)
├── Node 2
│   ├── Shard 1 (Primary)
│   └── Shard 0 (Replica)
└── Node 3
    ├── Shard 2 (Primary)
    └── Shard 1 (Replica)
  • 인덱스는 여러 **샤드(Shard)**로 분할되어 노드에 분산 저장
  • 각 샤드는 레플리카를 가져서 노드 장애 시에도 데이터 유실 없음
  • 노드를 추가하면 샤드가 자동으로 재배치 → 수평 확장이 핵심

Near Real-Time 검색

문서를 색인하면 약 1초 이내에 검색 가능해진다. "실시간"이 아니라 "거의 실시간(NRT)"이라고 표현하는 이유는, 색인된 문서가 메모리 버퍼에서 세그먼트로 flush되는 refresh interval(기본 1초)이 있기 때문이다.

JSON 문서 기반 — 스키마리스

Elasticsearch는 데이터를 JSON 문서로 저장한다. 테이블 정의도, 스키마 마이그레이션도 필요 없다. 서로 다른 구조의 문서를 같은 인덱스에 넣을 수 있다.

 
json
// 상품 문서
{
  "name": "맥북 프로 16인치",
  "price": 3490000,
  "category": "노트북",
  "specs": {
    "cpu": "M4 Pro",
    "ram": "36GB"
  },
  "tags": ["apple", "laptop", "professional"],
  "created_at": "2025-11-15T09:00:00Z"
}

4. 활용 사례 — 검색 그 이상의 7가지 영역

① 전문 검색 (Full-Text Search)

Elasticsearch의 본업이다. 이커머스 상품 검색, CMS 콘텐츠 검색, 위키 검색 등에서 자동완성, 오타 보정(fuzzy), 형태소 분석, 가중치 기반 랭킹을 지원한다.

 
 
json
// 오타 허용 검색 (fuzzy)
GET /products/_search
{
  "query": {
    "match": {
      "name": {
        "query": "맥북프로",
        "fuzziness": "AUTO"
      }
    }
  },
  "highlight": {
    "fields": { "name": {} }
  }
}

한국어 검색의 경우 nori 분석기(은전한닢 기반)를 사용하면 형태소 단위로 토큰화할 수 있다:

 
 
json
PUT /products
{
  "settings": {
    "analysis": {
      "analyzer": {
        "korean": {
          "type": "custom",
          "tokenizer": "nori_tokenizer",
          "filter": ["nori_readingform", "lowercase"]
        }
      }
    }
  }
}

② 로그 분석 (ELK/Elastic Stack)

가장 널리 알려진 활용 사례다. MSA 환경에서 9개, 10개의 마이크로서비스가 각각 로그를 뿜어내면, 이를 한곳에 모아서 검색·분석·시각화해야 한다.

[서비스 A] ──┐
[서비스 B] ──┤──→ [Beats/Logstash] ──→ [Elasticsearch] ──→ [Kibana 대시보드]
[서비스 C] ──┘     (수집·변환)            (저장·색인)         (시각화·알림)

Netflix는 85개 이상의 Elasticsearch 클러스터에 800개 이상의 프로덕션 노드를 운영하며, 고객 서비스 운영과 보안 로그를 모니터링한다.

③ APM & Observability (관측 가능성)

Elastic APM은 애플리케이션의 응답 시간, 에러율, 트랜잭션 추적을 제공한다. 2026년 기준, Dimensional Research 조사에 따르면 60%의 기업이 자사의 관측 가능성 수준을 "성숙" 또는 "전문가"로 평가하고 있으며, 이는 전년 41%에서 크게 상승한 수치다.

Elastic 9.x에서는 OpenTelemetry 네이티브 지원(Managed OTLP Endpoint GA)이 추가되어, OTel SDK에서 직접 데이터를 전송할 수 있다.

④ 보안 분석 (SIEM)

Elasticsearch는 SIEM(Security Information and Event Management) 용도로도 강력하다. 실시간으로 보안 이벤트를 수집·분석하여 위협을 탐지한다. Symantec 같은 사이버보안 기업이 실시간 위협 탐지에 Elasticsearch를 사용한다.

⑤ 비즈니스 분석 & 실시간 대시보드

Kibana와 결합하면 실시간 비즈니스 대시보드를 구축할 수 있다. 사용자 행동 분석, 매출 추이, A/B 테스트 결과 모니터링 등에 활용된다.

⑥ 지리공간 검색

Elasticsearch는 geo_point, geo_shape 타입을 지원하여 위치 기반 검색이 가능하다. "반경 5km 내 음식점", "특정 지역 내 매장"과 같은 쿼리를 밀리초 단위로 처리한다.

 
 
json
GET /restaurants/_search
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": { "lat": 37.4979, "lon": 127.0276 }
    }
  }
}

⑦ AI 벡터 검색 & RAG (2024~)

2026년 현재, Elasticsearch는 Search AI 플랫폼으로의 전환을 선언했다. 핵심은 벡터 검색이다:

  • BBQ 벡터 양자화가 Elastic 9.1부터 기본값으로 적용되어 메모리를 95% 이상 절감
  • DiskBBQ는 디스크에서 벡터를 읽어도 20ms 이하 지연
  • 2025년 10월 Jina AI 인수로 다국어 임베딩 모델을 네이티브 통합

LLM의 RAG(Retrieval-Augmented Generation) 파이프라인에서 Elasticsearch가 검색·리트리벌 레이어 역할을 맡는 구조가 빠르게 확산 중이다:

 
 
[사용자 질문] → [임베딩 모델] → [Elasticsearch 벡터 검색]
                                     ↓ 관련 문서 top-K
                              [LLM에 컨텍스트로 전달] → [답변 생성]

5. 라이선스 변천: Redis와 닮은 여정

Elasticsearch의 라이선스 역사는 Redis와 놀라울 정도로 유사하다:

시점변경 내용배경
2010~2020 Apache 2.0 완전한 오픈소스
2021.01 SSPL + Elastic License 듀얼 AWS ElastiCache의 무임승차 대응
2021.04 AWS가 OpenSearch로 포크 커뮤니티 분열
2024.08 AGPLv3 추가 (트리플 라이선스) 오픈소스 복귀, 커뮤니티 신뢰 회복

Redis가 Valkey 포크를 촉발한 것처럼, Elasticsearch는 OpenSearch 포크를 촉발했다. 오픈소스 프로젝트와 상업적 지속 가능성 사이의 긴장은 인프라 소프트웨어의 구조적 문제다.


6. LabNote ELN 같은 MSA에서의 실무 적용 포인트

MSA 기반 시스템에서 Elasticsearch를 도입할 때 고려해야 할 실무 패턴들:

데이터 동기화 전략

RDBMS가 원본(Source of Truth)이고 Elasticsearch는 검색용 보조 저장소인 경우가 대부분이다. 동기화 방식은 세 가지:

1) 애플리케이션 레벨 이중 쓰기 (단순하지만 일관성 위험)
   DB UPDATE → ES INDEX (트랜잭션 보장 불가)

2) CDC (Change Data Capture) — 추천
   DB → Debezium/Kafka Connect → Elasticsearch
   (DB 변경 로그를 캡처하여 비동기 동기화)

3) 주기적 배치 동기화
   Scheduler → DB 전체/변경분 조회 → ES Bulk Index
   (지연 허용 가능한 경우)

인덱스 설계 원칙

 
json
// ❌ RDBMS 사고방식 — 정규화하여 여러 인덱스로 분리
// products 인덱스, categories 인덱스를 JOIN? → ES에는 JOIN이 없다

// ✅ ES 사고방식 — 비정규화하여 검색에 최적화된 단일 문서
{
  "product_name": "LabNote Pro",
  "category_name": "전자연구노트",   // 카테고리를 문서에 포함
  "department": "R&D",              // 부서 정보도 포함
  "author": { "name": "김연구", "email": "kim@lab.com" }
}

검색 품질 튜닝

 
json
// 다중 필드 검색 + 가중치
GET /experiments/_search
{
  "query": {
    "multi_match": {
      "query": "세포 배양 프로토콜",
      "fields": ["title^3", "content", "tags^2", "author.name"],
      "type": "best_fields",
      "fuzziness": "AUTO"
    }
  }
}

7. Elasticsearch vs 대안 — 선택 기준

도구                          강점                                                                                   약점                              적합한 경우
Elasticsearch 전문 검색 + 분석 + 보안 통합, 거대 생태계 운영 복잡성, 높은 메모리 사용 대규모 검색·로그·보안 통합
OpenSearch ES 7.x 호환, Apache 2.0, AWS 관리형 ES 최신 기능 부재 AWS 환경 + 오픈소스 선호
Meilisearch 설정 최소, 50ms 이하 응답, 단일 개발자로 운영 가능 분석·보안 기능 없음 중소규모 앱 검색
Apache Solr 18년+ 안정성, 진정한 오픈소스 ES 대비 커뮤니티 축소 복잡한 문서 검색, 이커머스
Grafana Loki 로그 전용, 저비용, 라벨 기반 인덱싱 전문 검색 불가 메트릭 중심 로그 분석
Splunk 엔터프라이즈 SIEM 최강 매우 높은 비용 보안 최우선 대기업

PostgreSQL의 Full-Text Search는?

소규모 프로젝트에서는 PostgreSQL의 tsvector, tsquery만으로도 충분할 수 있다. 별도 인프라 없이 DB 하나로 해결된다. 하지만 데이터가 수백만 건을 넘어가거나, 자동완성·오타 보정·형태소 분석·실시간 로그 분석이 필요하다면 Elasticsearch의 영역이다.


8. 2026년의 Elasticsearch — Search AI Company

Elastic은 스스로를 "Search AI Company"로 포지셔닝하고 있다. 세 가지 축으로 사업을 전개한다:

  1. Enterprise Search: 기업 내 검색, 앱 검색, RAG 기반 AI 검색
  2. Observability: 로그·메트릭·트레이스 통합 관측
  3. Security: 위협 탐지, SIEM, 엔드포인트 보안

2025년 10월 Jina AI 인수로 다국어 임베딩 모델을 내재화했고, ES|QL(Elasticsearch Query Language)이 프로덕션 준비 완료 상태로 올라왔다. 검색 엔진에서 시작한 프로젝트가 AI 시대의 리트리벌 인프라로 자리잡는 중이다.


마무리: 레시피 검색에서 세계의 데이터 검색으로

Shay Banon은 아내의 요리 레시피를 검색하고 싶었을 뿐이다. 그 작은 필요가 Compass를 만들었고, Compass의 한계가 Elasticsearch를 낳았다. "You Know, for Search"라는 가벼운 제목 뒤에는, **"분산 시스템에서 빠르게 데이터를 찾는 문제"**라는 보편적이고 근본적인 과제가 있었다.

2026년 현재, 전 세계 18,000개 이상의 기업이 Elasticsearch를 사용하고 있고, Netflix의 85개 클러스터부터 개인 블로그의 검색 기능까지 그 스케일은 다양하다. Redis가 "캐시의 기본"이라면, Elasticsearch는 **"검색의 기본"**이다. 백엔드 개발자라면 언젠가 반드시 마주치게 되는 기술이고, 그 깊이를 알수록 시스템 설계의 선택지가 넓어진다.


참고 자료: Elasticsearch Wikipedia, Index Ventures Blog, Elastic 공식 블로그, Grokipedia, ByteByteGo, Knowi, Logit.io

LIST

"아내는 내가 처음 몇 년간 MacBook Air 11인치를 들고 화장실에 앉아서 Redis를 만들었다고 주장한다. 그녀가 틀렸다고 말하고 싶지만, 완벽하게 맞는 이야기다." — Salvatore Sanfilippo (antirez)

오늘날 거의 모든 웹 서비스의 뒤편에는 Redis가 있다. Stack Overflow 개발자 설문에서 4년 연속 "가장 사랑받는 데이터베이스"로 선정되고, Docker Hub에서 매일 가장 많이 실행되는 데이터베이스이며, 2026년 1월 기준 ARR(연간 반복 매출) 3억 달러를 돌파한 이 프로젝트는, 시칠리아의 한 개발자가 자기 스타트업의 성능 문제를 해결하려다 시작된 사이드 프로젝트였다.


1. 탄생: 느린 데이터베이스에 대한 분노 (2009)

LLOOGG — 모든 것의 시작

Redis의 창시자 Salvatore Sanfilippo(닉네임 antirez)는 시칠리아 출신의 이탈리아 개발자다. 2009년, 그는 LLOOGG라는 실시간 웹 로그 분석 스타트업을 운영하고 있었다. 문제는 간단했다. 실시간으로 쏟아지는 로그 데이터를 기존 관계형 데이터베이스로는 감당할 수 없었던 것이다.

MySQL이나 PostgreSQL 같은 RDBMS는 디스크 기반이다. 매 요청마다 디스크 I/O가 발생하고, 트랜잭션 오버헤드가 붙는다. 실시간 분석처럼 초당 수천 건의 읽기/쓰기가 필요한 워크로드에서는 구조적 한계가 있었다.

antirez는 먼저 Tcl로 프로토타입을 만들었다. "키-값 쌍을 메모리에 올려놓고 직접 조작하면 되지 않을까?" 이 단순한 아이디어가 Redis의 출발점이었다. 곧 C 언어로 재작성하고, 첫 번째 자료구조인 List를 구현했다. 내부에서 몇 주간 사용해본 뒤 확신이 생겨 Hacker News에 공개했다.

이름의 의미

Redis = Remote Dictionary Server. 원격에서 접근 가능한 딕셔너리(해시맵) 서버라는 뜻이다. 이름 자체가 Redis의 본질을 정확히 설명한다. 테이블도 없고, SQL도 없다. 있는 것은 키와 값, 그리고 그 위에서 동작하는 자료구조 명령어뿐이다.


2. 성장: 사이드 프로젝트에서 인프라의 표준으로

2009 — Ruby 커뮤니티의 열광

Redis가 처음 주목받은 곳은 Ruby on Rails 생태계였다. 2009년, GitHub의 CEO Chris Wanstrath가 Redis 기반의 백그라운드 잡 큐 Resque를 만들었다. Rails 세계에서 Resque는 폭발적인 인기를 끌었고, 2012년에 등장한 후속작 Sidekiq 역시 Redis 위에 구축되었다. GitHub과 Instagram이 초기 도입 기업이었다.

2010 — Twitter와 VMware

Twitter가 Redis를 타임라인 페이지에 도입하면서 Redis의 위상이 달라졌다. 흥미로운 건, antirez가 Redis 공개 직후 Retwis라는 Twitter 클론을 Redis 데모용으로 만들었다는 점이다. 본인이 만든 장난감이 실제 Twitter에 채택된 셈이다.

같은 해, VMware에서 연락이 왔다. "우리가 당신을 고용하겠다. 하던 일을 그대로 하면 된다. 웹사이트에 Redis가 VMware의 후원을 받는다고만 적어달라." 약 1년간 무급 사이드 프로젝트였던 Redis가 드디어 공식 후원을 받게 된 순간이었다.

2010~2015 — 자료구조의 확장

이 시기에 Redis는 단순한 키-값 저장소를 넘어 다목적 자료구조 서버로 진화했다:

시기                                    추가된 자료구조                                                                 용 사례
초기 String, List 캐시, 큐
2010~2011 Hash, Set, Sorted Set 세션 관리, 리더보드, 태그
2012+ HyperLogLog 고유 방문자 수 추정
2016+ Streams 이벤트 소싱, 메시지 큐
2018+ Modules (RedisJSON, RediSearch) 문서 DB, 검색 엔진
2024+ Vector Set AI 벡터 유사도 검색

VMware(2010~2013) → Pivotal(2013~2015) → Redis Labs(2015~2020)로 후원사가 바뀌면서도 antirez는 11년간 단독 메인테이너로서 프로젝트를 이끌었다.

2020 — antirez의 은퇴, 그리고 귀환

2020년 6월, antirez는 Redis 메인테이너 자리에서 물러났다. "유지보수 중심의 단계가 내 성격과 맞지 않는다"는 이유였다. 이후 AI를 소재로 한 SF 소설 Wohpe를 출판하기도 했다.

그리고 2024년 12월, antirez는 Redis 에반젤리스트라는 직함으로 복귀했다. 복귀 후 첫 작업은 Vector Set 자료구조의 구현이었다. AI 시대에 Redis가 벡터 유사도 검색까지 지원하게 된 것은 antirez의 복귀와 직결된다.


3. 라이선스 변천: 오픈소스의 빛과 그림자

Redis의 라이선스 역사는 오픈소스 생태계의 축소판이다:

  • 2009~2018: BSD-3 라이선스. 완전한 오픈소스.
  • 2018: 일부 모듈에 Commons Clause 추가. 클라우드 업체의 무임승차를 견제.
  • 2024.3: 코어 Redis 자체가 RSAL v2 + SSPL 듀얼 라이선스로 전환. AWS ElastiCache 등 서드파티 제공 제한.
  • 2025: AGPLv3를 추가한 트리플 라이선스로 전환. 다시 오픈소스로 복귀하되, 클라우드 벤더의 직접 패키징은 제한.

이 과정에서 Valkey(Linux Foundation 주도의 Redis 포크)가 탄생했다. 2026년 현재 Redis와 Valkey는 공존하며 경쟁 중이다.


4. Redis가 빠른 이유 — 아키텍처의 본질

Redis의 성능은 마법이 아니라 설계 철학의 결과다:

인메모리 스토리지

모든 데이터가 RAM에 상주한다. 디스크 I/O가 읽기/쓰기 경로에서 완전히 제거된다. 일반적인 RDBMS 쿼리가 50~200ms 걸리는 반면, Redis는 0.5~2ms 수준의 응답 시간을 보인다.

싱글 스레드 이벤트 루프

Redis는 명령 처리를 단일 스레드로 수행한다. 직관적으로는 느릴 것 같지만, 실제로는 반대다. 컨텍스트 스위칭이 없고, 락이 필요 없으며, 모든 명령이 원자적(atomic)으로 실행된다. I/O 멀티플렉싱을 통해 수십만 개의 동시 연결을 단일 스레드로 처리한다.

 
 
[클라이언트 1] ──┐
[클라이언트 2] ──┤──→ [이벤트 루프] ──→ [싱글 스레드 명령 실행] ──→ 응답
[클라이언트 3] ──┘       (epoll)          (락 없음, 원자적)

최적화된 자료구조

Redis는 C로 구현된 커스텀 자료구조를 사용한다. 작은 데이터셋에는 메모리 효율적인 인코딩(ziplist 등)을 자동 적용하고, 데이터가 커지면 일반 구조로 전환한다.

영속성 옵션

인메모리라고 해서 데이터를 잃는 건 아니다:

  • RDB: 주기적 스냅샷. fork() 시스템 콜로 자식 프로세스가 디스크에 저장하는 동안 부모 프로세스는 계속 서비스.
  • AOF: 모든 쓰기 연산을 로그로 기록. 재시작 시 재생하여 복구.
  • 혼합 모드: RDB + AOF를 함께 사용하여 빠른 복구와 데이터 안전성을 동시에 확보.

5. 활용 사례 — "캐시 그 이상"

Redis를 "캐시"라고만 부르는 것은 스위스 아미 나이프를 "칼"이라고만 부르는 것과 같다.

① 캐싱 (가장 보편적인 활용)

DB 앞에 Redis를 두고 자주 조회되는 데이터를 메모리에 저장한다. Cache-Aside, Write-Through, Write-Behind 등 다양한 전략을 조합할 수 있다.

 
 
typescript
// Cache-Aside 패턴 (Node.js)
async function getProduct(id: string) {
  const cached = await redis.get(`product:${id}`);
  if (cached) return JSON.parse(cached);

  const product = await db.query('SELECT * FROM products WHERE id = $1', [id]);
  await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 600);
  return product;
}

② 세션 스토어

MSA 환경에서 서버가 여러 대일 때, 세션을 특정 서버에 묶지 않고 Redis에 중앙 저장한다. TTL을 설정하면 비활성 세션이 자동으로 만료된다.

 
 
typescript
// Express + Redis 세션
import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 30 * 60 * 1000 } // 30분
}));

③ Rate Limiting

API 남용 방지. Sorted Set이나 단순 INCR + EXPIRE 조합으로 시간 윈도우 기반 요청 제한을 구현한다.

 
 
typescript
async function rateLimit(userId: string, limit: number = 100, windowSec: number = 60) {
  const key = `rate:${userId}`;
  const current = await redis.incr(key);
  if (current === 1) await redis.expire(key, windowSec);
  return current <= limit;
}

④ 실시간 리더보드

Sorted Set의 ZADD, ZREVRANGE, ZREVRANK 명령으로 점수 기반 순위를 실시간으로 관리한다. 게임, 이커머스 판매 순위, 경쟁 플랫폼에서 활용된다.

 
 
typescript
// 점수 업데이트
await redis.zadd('leaderboard:weekly', score, playerId);

// 상위 10명 조회
const top10 = await redis.zrevrange('leaderboard:weekly', 0, 9, 'WITHSCORES');

// 특정 플레이어 순위
const rank = await redis.zrevrank('leaderboard:weekly', playerId);

⑤ Pub/Sub & 메시지 브로커

서비스 간 실시간 이벤트 전달. 채팅, 알림, 캐시 무효화 브로드캐스트에 활용된다. 더 강력한 보장이 필요하면 Redis Streams로 컨슈머 그룹 기반 메시지 처리가 가능하다.

 
 
typescript
// Publisher
await redis.publish('notifications:user:123', JSON.stringify({
  type: 'NEW_MESSAGE',
  from: 'user:456',
  preview: '안녕하세요...'
}));

// Subscriber
subscriber.subscribe('notifications:user:123');
subscriber.on('message', (channel, message) => {
  const notification = JSON.parse(message);
  pushToClient(notification);
});

⑥ 분산 락

MSA에서 공유 리소스에 대한 동시 접근을 제어한다. SET NX EX 명령으로 간단한 뮤텍스를, 더 엄격한 환경에서는 Redlock 알고리즘을 사용한다.

⑦ 지리공간 인덱싱

GEOADD, GEODIST, GEORADIUS 명령으로 좌표 기반 검색이 가능하다. 배달 앱의 "내 주변 음식점", 차량 호출 서비스의 "가까운 드라이버 찾기" 같은 기능에 사용된다.

⑧ AI 벡터 검색 (2024~)

Redis가 Vector Set을 지원하면서, 임베딩 벡터의 유사도 검색이 가능해졌다. LLM의 RAG 파이프라인에서 벡터 DB 역할을 Redis가 대신하는 사례가 빠르게 늘고 있다. 2025년 Stack Overflow 설문에서 Redis는 **AI 에이전트 메모리 저장소로 개발자 선택 1위(42%)**를 차지했다.


6. 2026년의 Redis — AI 인프라의 중심

Redis는 단순한 캐시에서 AI 시대의 컨텍스트 엔진으로 진화하고 있다:

  • 컨텍스트 엔지니어링: LLM이 올바른 판단을 내리려면 적절한 데이터가 빠르게 제공되어야 한다. 벡터 스토어, 세션 상태, 장기 메모리를 한 곳에서 제공하는 "컨텍스트 레이어"로서 Redis가 부상 중이다.
  • LLM 응답 캐싱: 동일한 프롬프트에 대한 LLM 호출을 캐싱하여 비용과 에너지를 절감한다.
  • 에이전트 메모리: AI 에이전트의 작업 상태, 대화 히스토리, 도구 호출 결과를 저장하는 실시간 메모리로 활용된다.

Redis CEO Rowan Trollope는 이렇게 표현했다. "에이전틱 의사결정이 이루어지는 스택의 앞단, 바로 그곳이 Redis의 전통적인 자리였다. 개발자들이 자연스럽게 Redis를 AI 워크로드에 채택하고 있는 이유다."


7. 실무에서의 선택 기준

Redis가 적합한 경우

  • 밀리초 단위 응답이 필요한 읽기 Heavy 워크로드
  • 세션, 캐시, 리더보드 등 TTL 기반 임시 데이터
  • Pub/Sub 또는 Streams 기반 실시간 이벤트 처리
  • MSA 환경에서 서비스 간 공유 상태 저장
  • AI/ML 파이프라인의 벡터 검색 및 피처 서빙

Redis만으로는 부족한 경우

  • 복잡한 관계형 쿼리 (JOIN, 서브쿼리)
  • RAM 용량을 초과하는 대규모 데이터셋
  • 강한 트랜잭션 보장이 필요한 금융 원장
  • 전문 검색이 핵심인 경우 (Elasticsearch가 더 적합)

핵심은 **"Redis vs DB"가 아니라 "Redis + DB"**라는 것이다. Redis는 DB를 대체하는 것이 아니라, DB의 부하를 흡수하고 응답 속도를 높이는 가속기 역할을 한다.


마무리: 단순함의 힘

antirez는 인터뷰에서 반복적으로 **단순함(simplicity)**을 강조했다. "복잡한 시스템은 아무리 노력해도, 프로덕션에서 다른 복잡한 시스템과 만나면 상상할 수 없는 방식으로 실패한다."

Redis가 2009년부터 17년간 살아남은 이유는 기능이 많아서가 아니라, 핵심이 단순해서다. 키와 값, 메모리와 자료구조. 이 근본적인 설계 위에 캐시도, 큐도, 리더보드도, 벡터 검색도 자연스럽게 올라간다.

화장실에서 시작된 이 프로젝트는, "단순한 것이 오래 간다"는 소프트웨어 설계의 원칙을 17년째 증명하고 있다.


참고 자료: Redis Wikipedia, Brachiosoft Blog, Redis 공식 블로그, VentureBeat, antirez 개인 사이트

LIST

프롤로그: 그날, DB가 탔다

금요일 오후 5시 47분. 퇴근을 13분 앞두고 슬랙에 메시지가 뜬다.

🔥 [ALERT] DB CPU 98% — Connection Pool Exhausted — 응답시간 47초

모니터링 대시보드를 열었다. 그래프가 수직으로 치솟아 있다. 쿼리 큐에 3,000개가 쌓여 있고, 커넥션 풀은 진작에 바닥났다. Slow Query 로그가 폭포처럼 쏟아진다. API 서버는 504 Gateway Timeout을 뱉고 있고, 프론트엔드에는 "잠시 후 다시 시도해주세요"가 도배되어 있다.

DB가 탄 거다.

"탄다"는 과장이 아니다. 서버실 온도가 올라가고, 디스크 I/O가 포화되고, 커넥션이 고갈되면 물리적으로도 뜨거워진다. 그리고 그 열기는 고스란히 개발자의 등골을 타고 올라온다.

이 글은 그 순간에 대한 이야기다. DB가 타기 전에 무엇을 준비해야 하는지, 타는 순간에 무엇을 해야 하는지, 그리고 타고 난 뒤에 무엇을 바꿔야 하는지.


1장. 불이 나기 전 — 방화벽을 세워라

불이 나면 소방차를 부르지만, 소방차가 와도 이미 탄 건 돌아오지 않는다. DB가 타기 전에 세워두는 방화벽이 있다.

인덱스: 불쏘시개를 치워라

DB가 타는 가장 흔한 원인 1위. 인덱스가 없는 테이블에 Full Table Scan이 걸리는 것. 100만 건짜리 테이블을 매 요청마다 처음부터 끝까지 훑으면, 그건 장작 위에 휘발유를 붓는 거다.

 
 
sql
-- 이게 매 요청마다 실행되고 있다면, 불이 안 나는 게 이상하다
SELECT * FROM orders WHERE user_id = ? AND status = 'PENDING'
ORDER BY created_at DESC;

-- 이 인덱스 하나로 Full Scan → Index Seek
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at DESC);

EXPLAIN ANALYZE를 습관처럼 찍어라. Seq Scan이 보이면 그건 아직 안 터진 시한폭탄이다.

실전 체크리스트:

  • WHERE 절에 자주 등장하는 컬럼에 인덱스가 있는가?
  • 복합 인덱스의 컬럼 순서가 쿼리 패턴과 일치하는가?
  • 안 쓰는 인덱스가 쌓여서 오히려 INSERT/UPDATE를 느리게 만들고 있지 않은가?
  • SELECT * 를 쓰고 있지 않은가? 필요한 컬럼만 명시하라

커넥션 풀: 소방 호스의 수를 정하라

커넥션 풀은 DB와 애플리케이션 사이의 파이프다. 파이프가 부족하면 요청이 대기열에 쌓이고, 대기열이 넘치면 타임아웃이 연쇄 발생한다. 그렇다고 파이프를 무한정 늘리면 DB가 컨텍스트 스위칭에 허덕인다.

PostgreSQL 공식 권장 공식:

max_connections = (core_count * 2) + effective_spindle_count

 

4코어 서버에 SSD 1개라면? (4 * 2) + 1 = 9. 아홉 개. 생각보다 적다.

HikariCP의 기본값이 10인 데는 이유가 있다. 커넥션 100개를 열어놓는 것이 능사가 아니다. 적은 커넥션으로 빠르게 반환하는 것이 핵심이다.

yaml
# HikariCP 설정 예시
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 3000    # 3초 안에 못 받으면 실패
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 5000  # 5초 이상 미반환 시 경고

leak-detection-threshold는 반드시 설정하라. 커넥션을 빌려가서 안 돌려주는 코드가 있으면, 풀이 서서히 말라간다. 로그에는 아무 에러도 안 뜨는데 어느 날 갑자기 타는 경우가 이거다.

캐시: 불 자체를 예방하라

가장 확실한 방어는 DB에 불이 안 가게 하는 것이다. 변하지 않는 데이터, 자주 읽히는 데이터, 계산 비용이 높은 데이터는 캐시에 올려라.

typescript
// Redis 캐시 패턴 (Cache-Aside)
async function getUserProfile(userId: string): Promise<UserProfile> {
  // 1. 캐시에서 먼저 찾는다
  const cached = await redis.get(`user:${userId}`);
  if (cached) return JSON.parse(cached);

  // 2. 캐시에 없으면 DB 조회
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

  // 3. 결과를 캐시에 저장 (TTL 5분)
  await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 300);

  return user;
}

캐시 적중률(Hit Rate)이 90%면, DB로 가는 트래픽이 1/10로 줄어든다. 이것만으로 "탈 뻔한 DB"가 "여유로운 DB"로 바뀐다.

주의할 점:

  • Cache Invalidation은 컴퓨터 과학에서 가장 어려운 문제 중 하나다. 데이터가 변경되었을 때 캐시를 확실히 무효화하라
  • 캐시 자체가 날아가면(Redis 재시작 등) DB에 트래픽이 한꺼번에 몰리는 **캐시 스탬피드(Cache Stampede)**가 발생한다. TTL에 랜덤 지터(jitter)를 추가하라
  • 캐시에 잘못된 데이터가 올라가면 "데이터가 이상해요" 문의가 폭주한다. 캐시가 해결책인 동시에 새로운 문제의 원인이 될 수 있다

2장. 불이 난 순간 — 진화 매뉴얼

0분~2분: 상황 파악 — "어디서 타고 있는가"

불이 나면 가장 먼저 할 일은 불이 어디서 시작됐는지 파악하는 것이다. DB가 탄다고 DB가 원인이 아닐 수 있다.

확인 순서:

 
 
bash
# 1. 현재 실행 중인 쿼리 확인 (PostgreSQL)
SELECT pid, now() - pg_stat_activity.query_start AS duration,
       query, state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 seconds'
ORDER BY duration DESC;

# 2. 커넥션 상태 확인
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;

# 3. 락 대기 확인
SELECT blocked_locks.pid AS blocked_pid,
       blocking_locks.pid AS blocking_pid,
       blocked_activity.query AS blocked_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks ON ...

빈번한 원인 Top 5:

  1. 인덱스 없는 쿼리가 갑자기 호출량 증가 — 마케팅팀이 프로모션 푸시를 보냄
  2. 배치 작업이 트랜잭션 시간대에 실행됨 — 크론잡 설정 실수
  3. N+1 쿼리가 트래픽 증가와 함께 폭발 — 평소에는 10건이라 안 보였는데 10,000건이 되니 터짐
  4. 락 경합 — 한 트랜잭션이 테이블 락을 잡고 안 놓아줌
  5. 디스크 I/O 포화 — 로그 파일이 디스크를 가득 채움

2분~5분: 1차 진화 — "출혈을 멈춰라"

원인을 파악했으면 즉시 출혈을 멈춘다. 완벽한 해결이 아니라 피해 확산 방지가 목표다.

킬 쿼리: 문제가 되는 장시간 실행 쿼리를 강제 종료한다.

 
 
sql
-- 특정 쿼리 강제 종료
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE duration > interval '30 seconds'
  AND query LIKE '%problematic_table%';

서킷 브레이커 발동: 장애가 전파되지 않도록 문제 엔드포인트를 차단한다.

 
 
typescript
// 서킷 브레이커 패턴
class CircuitBreaker {
  private failures = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  private readonly threshold = 5;
  private readonly resetTimeout = 30000; // 30초

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      throw new Error('Circuit is OPEN — 서비스 일시 중단');
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
      setTimeout(() => { this.state = 'HALF_OPEN'; }, this.resetTimeout);
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }
}

읽기 트래픽 분산: Read Replica가 있다면, 읽기 쿼리를 레플리카로 돌린다.

비핵심 기능 차단: 추천 알고리즘, 통계 집계, 로그 적재 같은 비핵심 기능을 일시적으로 끈다. 핵심 비즈니스 로직(결제, 인증)만 살린다.

5분~30분: 2차 진화 — "원인을 제거하라"

출혈이 멈추면 근본 원인을 처리한다.

인덱스가 원인이라면: 긴급 인덱스 생성. PostgreSQL의 CREATE INDEX CONCURRENTLY는 테이블 락 없이 인덱스를 생성한다.

 
 
sql
-- 운영 중에도 안전하게 인덱스 생성
CREATE INDEX CONCURRENTLY idx_emergency_fix
ON orders(user_id, created_at DESC);

N+1이 원인이라면: 즉시 핫픽스. JOIN이나 배치 쿼리로 교체.

 
 
typescript
// Before: N+1 — 유저 100명이면 쿼리 101번
const users = await userRepo.findAll();
for (const user of users) {
  user.orders = await orderRepo.findByUserId(user.id); // N번 실행
}

// After: JOIN으로 1번
const usersWithOrders = await userRepo
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.orders', 'order')
  .getMany();

배치 작업이 원인이라면: 해당 크론잡을 중지하고, 트래픽이 적은 시간대로 재스케줄링.


3장. 불이 꺼진 뒤 — 화재 보고서를 쓰라

Postmortem: 같은 불이 두 번 나면 그건 방화다

장애가 복구되면 반드시 사후 분석(Postmortem) 문서를 작성한다. 이건 책임 추궁이 아니라 학습을 위한 것이다.

 
 
markdown
## 장애 보고서 — 2026-03-30 DB 과부하

### 타임라인
- 17:47 — 모니터링 알림 발생 (DB CPU 98%)
- 17:49 — 원인 파악: 프로모션 푸시로 인한 트래픽 300% 급증
- 17:52 — 장시간 실행 쿼리 강제 종료
- 17:55 — 비핵심 API 엔드포인트 일시 차단
- 18:10 — 긴급 인덱스 생성 완료
- 18:15 — 정상 복구 확인

### 근본 원인
- orders 테이블의 user_id + status 복합 인덱스 부재
- 마케팅팀 프로모션 일정이 개발팀에 공유되지 않음

### 재발 방지
- [ ] orders 테이블 인덱스 추가 (완료)
- [ ] 마케팅 프로모션 일정 공유 프로세스 수립
- [ ] Slow Query 임계값 알림 추가 (5초 → 2초로 하향)
- [ ] 부하 테스트 시나리오에 프로모션 트래픽 패턴 추가

부하 테스트: 불을 미리 질러보라

"이 시스템이 트래픽 10배를 견딜 수 있는가?"를 아는 유일한 방법은 실제로 10배를 때려보는 것이다.

 
 
javascript
// k6 부하 테스트 스크립트 예시
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 100 },   // 워밍업
    { duration: '5m', target: 500 },   // 평소 트래픽
    { duration: '2m', target: 2000 },  // 프로모션 시나리오
    { duration: '5m', target: 2000 },  // 유지
    { duration: '2m', target: 0 },     // 쿨다운
  ],
};

export default function () {
  const res = http.get('https://api.example.com/orders?status=PENDING');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  sleep(1);
}

부하 테스트에서 DB가 타면, 운영에서 안 탄다. 부하 테스트에서 안 타면... 그건 부하가 부족한 거다.


4장. 화재 등급별 대응 — 연기 수준부터 대형 화재까지

Level 1: 연기가 난다 (Slow Query 증가)

증상: 응답 시간이 평소 200ms에서 800ms로 느려졌다. 사용자는 아직 못 느낀다.

대응:

  • Slow Query 로그 분석
  • EXPLAIN ANALYZE로 실행 계획 확인
  • 인덱스 추가 또는 쿼리 리팩토링
  • 이건 퇴근 후 집에서 해도 된다

Level 2: 불꽃이 보인다 (커넥션 풀 80% 이상)

증상: 일부 요청이 타임아웃된다. 사용자가 "가끔 느려요"라고 보고한다.

대응:

  • 커넥션 풀 설정 점검 (leak 여부)
  • 장시간 실행 쿼리 식별 및 최적화
  • 읽기 트래픽을 Read Replica로 분산
  • 캐시 적중률 확인 및 캐시 대상 확대
  • 지금 안 하면 Level 3이 된다

Level 3: 화염이 솟는다 (DB CPU 90%+, 커넥션 고갈)

증상: 대부분의 요청이 실패한다. "서비스 접속이 안 돼요" 문의 폭주.

대응:

  • 즉시 문제 쿼리 강제 종료
  • 서킷 브레이커 발동
  • 비핵심 기능 일시 차단
  • 캐시 워밍 (빈 캐시에 데이터 선적재)
  • 팀 전원 소집. 퇴근 없다

Level 4: 대형 화재 (DB 다운, 데이터 유실 위험)

증상: DB 프로세스가 죽었다. 연결 자체가 안 된다.

대응:

  • DB 재시작 (최후의 수단)
  • WAL(Write-Ahead Log) 기반 복구
  • 최신 백업에서 Point-in-Time Recovery
  • 서비스 점검 모드 전환
  • 이건 사고다. 경영진 보고가 필요하다

5장. 불에 강한 아키텍처 — 방화 건물을 짓는 법

CQRS: 읽기와 쓰기를 분리하라

읽기와 쓰기가 같은 DB 인스턴스를 공유하면, 읽기 트래픽 폭주가 쓰기 성능까지 잡아먹는다. 쓰기(Command)와 읽기(Query)를 분리하면 한쪽이 타도 다른 쪽은 살아남는다.

 
 
[사용자 요청]
     │
     ├── 쓰기 → Primary DB (PostgreSQL)
     │              │
     │              └── 변경 이벤트 발행
     │                       │
     └── 읽기 → Read Replica / OpenSearch / Redis
                     ↑
              이벤트 기반 동기화

Rate Limiting: 입구에서 통제하라

아무리 튼튼한 건물도 한꺼번에 만 명이 들어오면 무너진다. API Gateway에서 요청 수를 제한하라.

 
 
typescript
// Token Bucket 기반 Rate Limiter
class RateLimiter {
  private tokens: number;
  private readonly maxTokens: number;
  private readonly refillRate: number; // tokens per second
  private lastRefill: number;

  constructor(maxTokens: number, refillRate: number) {
    this.tokens = maxTokens;
    this.maxTokens = maxTokens;
    this.refillRate = refillRate;
    this.lastRefill = Date.now();
  }

  tryConsume(): boolean {
    this.refill();
    if (this.tokens > 0) {
      this.tokens--;
      return true;
    }
    return false; // 429 Too Many Requests
  }

  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
    this.lastRefill = now;
  }
}

큐 기반 비동기 처리: 불을 천천히 태워라

모든 요청을 즉시 처리할 필요는 없다. 주문 접수는 즉시 응답하되, 실제 처리는 큐에 넣고 순차적으로 하면 DB 부하를 평탄화할 수 있다.

[대량 주문 요청]
     │
     ├── 즉시 응답: "주문이 접수되었습니다" (202 Accepted)
     │
     └── 메시지 큐 (Redis Queue / RabbitMQ / Kafka)
              │
              └── Worker가 순차 처리 → DB 쓰기
                   (초당 100건으로 제한)

초당 10,000건의 요청이 들어와도, Worker가 초당 100건씩 처리하면 DB는 편안하다. 사용자는 "접수 완료" 응답을 즉시 받으니 체감 성능도 좋다.

 

Read Replica: 불을 분산시켜라

Primary에서 쓰기만 하고, 읽기는 Replica로 보내라. 트래픽의 80%가 읽기라면, 이것만으로 Primary의 부하가 1/5로 줄어든다.

 
typescript
// 읽기/쓰기 분리 DataSource 설정 (TypeORM)
const dataSource = new DataSource({
  type: 'postgres',
  replication: {
    master: {
      host: 'primary-db.internal',
      port: 5432,
      username: 'app',
      password: '***',
      database: 'production',
    },
    slaves: [
      { host: 'replica-1.internal', port: 5432, ... },
      { host: 'replica-2.internal', port: 5432, ... },
    ],
  },
});

6장. 불과 함께 사는 법 — 모니터링은 화재 경보기다

DB가 타는 것을 완전히 막을 수는 없다. 트래픽은 예측 불가능하고, 코드에는 항상 미처 발견하지 못한 구멍이 있다. 중요한 것은 탈 때 빨리 아는 것이다.

최소한의 모니터링 세트

 
 
yaml
# 이것만은 반드시 알림을 걸어라
alerts:
  - name: DB CPU 높음
    condition: cpu_usage > 80%
    duration: 5분
    severity: warning

  - name: 커넥션 풀 임계
    condition: active_connections > pool_size * 0.8
    duration: 2분
    severity: critical

  - name: Slow Query 급증
    condition: slow_query_count > 50/분
    duration: 3분
    severity: warning

  - name: 디스크 사용률
    condition: disk_usage > 85%
    duration: 10분
    severity: critical

  - name: 레플리카 지연
    condition: replication_lag > 10초
    duration: 5분
    severity: warning

화재 경보기가 울렸을 때 이미 대형 화재라면, 경보기의 임계값이 너무 높은 것이다. 경보기는 연기 단계에서 울려야 한다. CPU 98%에서 알림이 오면 이미 Level 3이다. 80%에서 울리게 하라.


에필로그: 불은 다시 난다

DB는 반드시 다시 탄다.

규모가 커지면 탄다. 사용자가 늘면 탄다. 블랙프라이데이에 탄다. 마케팅팀이 푸시를 보내면 탄다. 배치 작업 시간을 잘못 잡으면 탄다. 인턴이 WHERE 절 없이 UPDATE를 날리면 탄다.

중요한 건 "불이 안 나게 하겠다"는 환상을 버리는 것이다. 대신 이렇게 생각하라:

  • 불이 나기 전에, 작은 불이 날 수 있는 환경을 부하 테스트로 미리 만든다
  • 불이 나는 순간에, 30초 안에 어디서 타는지 파악할 수 있는 모니터링이 있다
  • 불이 난 직후에, 2분 안에 출혈을 멈추는 매뉴얼이 있다
  • 불이 꺼진 뒤에, 같은 원인으로 두 번 타지 않도록 사후 분석을 쓴다

그리고 가장 현실적인 조언 하나. 금요일 오후에 배포하지 마라. 불이 나면 주말이 날아간다.


"DB는 탄다. 문제는 타느냐 안 타느냐가 아니라, 탔을 때 3분 안에 끌 수 있느냐다."

LIST
JDBC에서 Statement와 PreparedStatement는 모두 SQL 실행을 담당하지만, 사용 방식과 성능, 보안 측면에서 차이가 존재합니다.

 

Statement 클래스는 문자열 연결을 이용해 SQL을 동적으로 구성해야 합니다. 이러한 특성으로 인해 SQL 인젝션 공격에 취약하다는 단점이 있습니다.

Statement stmt = conn.createStatement();
ResultSet rs =  30")" >stmt.executeQuery("select * from users where age > 30");

반면, PreparedStatement는 동적으로 파라미터를 바인딩할 수 있는 기능을 제공합니다. 값을 바인딩하면 내부적으로 이스케이프 처리하기 때문에 SQL 인젝션 공격을 방지할 수 있습니다.

String sql = "select * from users where age > ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 30);
ResultSet rs = pstmt.executeQuery();

또한, 쿼리 구조를 미리 확정하고 플레이스홀더를 활용하여 값을 바인딩하는 PreparedStatement를 사용하면 SQL 구문 분석 결과를 캐싱할 수 있어 반복 실행 시 Statement보다 성능이 높은 것으로 알려져 있습니다.

LIST

아래와 같이 NOT IN을 사용한 쿼리는 직관적이고 사용하기 쉽지만, 대규모 데이터셋에서 심각한 성능 저하를 일으킬 수 있습니다.

SELECT p 
FROM Post p
WHERE p.id NOT IN :postIds

문제점

  1. NOT IN은 부정 조건으로, 대부분의 DBMS에서 전체 테이블 스캔이나 인덱스 풀 스캔을 유발합니다. 전체 데이터나 테이블을 스캔한 후 조건에 맞지 않는 레코드를 필터링 해야하기 때문에 데이터베이스 옵티마이저가 효율적인 실행 계획을 세우기 어렵습니다.
  2. 인덱스를 효과적으로 활용하지 못합니다. IN 절은 인덱스 Range Scan을 통해 빠르게 처리할 수 있지만, NOT IN은 인덱스 활용도가 현저히 떨어집니다.
  3. 대량의 값을 IN 절에 넣으면 실행 계획 생성이 늘어나고, 파싱 및 최적화 단계에서 추가적인 오버헤드가 발생합니다.
  4. NULL 값 처리 로직으로 인한 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, column NOT IN (1, 2, NULL)은 항상 빈 결과를 반환합니다.

최적화 방안

1. NOT EXISTS 활용

SELECT p FROM Post p
WHERE NOT EXISTS (
    SELECT 1 FROM Post temp
    WHERE temp.id = p.id AND temp.id IN :postIds
)

NOT EXISTS는 행 단위로 평가되어 매칭되는 첫 행을 찾자마자 평가를 중단합니다. 이는 DBMS가 '존재하지 않음'을 확인하기 위해 특별히 최적화된 방식입니다. 대규모 데이터셋에서 가장 안정적이고 확장성 있는 성능을 제공합니다.

2. LEFT JOIN + IS NULL 패턴

SELECT p FROM Post p 
LEFT JOIN (
    SELECT temp.id FROM Post temp WHERE temp.id IN :postIds
) filtered ON p.id = filtered.id
WHERE filtered.id IS NULL

이 방식은 서브쿼리 결과가 작을 때 특히 효율적입니다. 인덱스를 효과적으로 활용할 수 있으며, PK 인덱스를 사용한 JOIN 연산이 최적화됩니다.

LIST

NoSQL 데이터베이스의 유형은 키-값, 문서 지향, 열 지향, 그래프, 시계열이 있습니다.

키-값 데이터베이스(Key-value Database) 는 키를 고유한 식별자로 사용하는 키-값 쌍의 형태로 데이터를 저장합니다. 구조가 단순하고, 빠른 읽기 및 쓰기 성능을 제공합니다. Redis, Amazon DynamoDB가 대표적인 예시이고, 세션 저장, 캐시, 실시간 순위 등으로 사용할 수 있습니다.

문서 지향 데이터베이스(Document-oriented Database) 는 JSON, BSON, XML 등의 형식으로 데이터를 저장합니다. 유연한 스키마를 가지고 있으며, 복잡한 데이터 구조를 쉽게 표현할 수 있습니다. MongoDB, CouchDB가 대표적인 예시이고, 콘텐츠 관리 시스템, 사용자 프로필 저장 등으로 사용할 수 있습니다.

열 지향 데이터베이스(Column Family Database) 는 데이터를 열 단위로 저장합니다. 대량의 데이터를 처리하는 데 적합하며, 행마다 각기 다른 수의 열과 여러 데이터 유형을 가질 수 있습니다. Apache Cassandra, HBase가 대표적인 예시이고, 대규모 데이터 분석, 로그 수집 등으로 사용할 수 있습니다.

그래프 데이터베이스(Graph Database) 는 노드, 엣지 구조로 구성된 그래프로 데이터를 저장합니다. 복잡한 관계를 표현하는 데 사용되며, 레이블(그룹화된 노드)을 통해 쿼리를 쉽게 작성하고 효율적으로 실행할 수 있습니다. Neo4j, Amazon Neptune이 대표적인 예시이고, 소셜 네트워크 분석, 추천 시스템 등으로 사용할 수 있습니다.

시계열 데이터베이스(Time Series Database) 는 시간에 따라 변화하는 데이터를 저장합니다. 타임스탬프가 있는 메트릭, 이벤트 등을 처리하기 위해 사용되며, 시간 경과에 따른 변화를 측정하는데 최적화되어 있습니다. InfluxDB, Prometheus, TimescaleDB가 대표적인 예시이고, IoT 데이터 수집, 금융 데이터 분석 등으로 사용할 수 있습니다.

실시간 채팅 앱에 적합한 NoSQL을 사용한다면 어떻게 구성하실 건가요?

실시간 채팅 앱에서는 메시지를 빠르게 주고받는 처리 속도와, 유연하고 수평 확장이 가능한 저장 구조가 중요하다고 생각합니다.
먼저, 실시간 메시지 전송은 Redis의 Pub/Sub 기능을 사용하겠습니다. 이 기능은 낮은 지연 시간으로 사용자 간 메시지를 브로드캐스트할 수 있기 때문입니다.

Redis는 영구 저장보다 캐시나 메시지 브로커 역할에 더 적합하므로 실제 메시지를 영구 저장할 때는 MongoDB를 사용하겠습니다. MongoDB는 문서 지향 데이터베이스로서 채팅 메시지나 사용자 정보 등을 JSON 형식으로 유연하게 저장할 수 있습니다. 특히, 샤딩 기능으로 수평 확장이 가능하기 때문에 사용자 수가 증가하거나 메시지 양이 많아져도 성능 저하 없이 안정적으로 확장할 수 있다고 생각합니다.

LIST

관계형 데이터베이스는 데이터를 테이블 형식으로 저장하고 관리하는 데이터베이스입니다. 각 테이블은 고정된 스키마를 가지며, 행(Row)은 개별 레코드, 열(Column)은 속성을 나타냅니다. 각 테이블은 고유한 스키마를 가지고 있어, 데이터 타입과 구조가 엄격하게 정의되어 있습니다. 대표적인 예로는 MySQL, PostgreSQL, Oracle이 있습니다.

관계형 데이터베이스는 정형화된 데이터를 다룰 때 특히 유용합니다. 미리 정의된 타입과 구조가 있기 때문에 이에 부합하는지 검증하여 데이터의 일관성을 유지하기 용이합니다. 또한, 데이터 간의 관계를 명확히 표현할 수 있는 것이 큰 장점입니다. 예를 들어 사용자와 주문 데이터를 각각 테이블로 만들고, 사용자 ID를 외래키로 설정해 두 테이블을 연결할 수 있습니다. 이를 통해 하나의 사용자에 속한 주문 내역을 정확히 조회할 수 있으며, 이 외에도 복잡한 조건의 데이터 조회나 조인을 처리하기 용이합니다.

반면, 흔히 NoSQL이라고 부르기도 하는 비관계형 데이터베이스는 전통적인 테이블 기반 구조가 아닌, 보다 유연한 데이터 모델을 사용합니다. "Key - value", "Document", "Graph" 등의 유형이 존재합니다. 비관계형 데이터베이스는 스키마가 고정되어 있지 않아서, 저장되는 데이터 구조가 일관되지 않아도 됩니다. 필요에 따라 유동적으로 속성을 추가할 수도 있습니다. 대표적인 예로는 MongoDB, Cassandra, Redis가 있습니다.

비관계형 데이터베이스의 장점은 유연성과 확장성입니다. 스키마가 고정되어 있지 않기 때문에, 초기 개발 단계에서 데이터 구조가 자주 변경될 가능성이 있는 프로젝트에 특히 적합합니다. 또한, 수평적 확장이 쉬워서 대용량의 데이터를 빠르게 처리하거나, 사용자가 급격히 늘어나는 상황에서도 안정적인 성능을 유지할 수 있다는 장점이 있습니다. 문서 기반 NoSQL에서는 하나의 객체에 필요한 데이터를 모두 담을 수 있어서, 관계를 맺고 조인하는 대신 한 번의 조회로 필요한 정보를 가져올 수 있다는 장점도 있습니다.

둘의 단점에 대해서도 간단히 설명해주실 수 있나요? 🤔

관계형 데이터베이스의 단점은 유연성이 떨어진다는 점입니다. 스키마가 고정돼 있기 때문에, 새로운 필드를 추가하거나 데이터 구조를 바꾸려면 테이블 자체를 수정해야 하고, 이로 인해 마이그레이션 과정이 복잡하고 시간이 오래 걸릴 수 있습니다. 또한, 서버를 여러 대로 분산시키는 수평적 확장이 상대적으로 어렵다는 단점도 있습니다. 관계형 데이터는 여러 테이블 간의 조인이 많기 때문에 데이터를 분산시켜 저장하면 성능 저하가 생길 수 있습니다. 그래서 대규모 트래픽을 처리해야 하는 시스템에서는 확장성이 한계로 작용할 수 있습니다.

비관계형 데이터베이스는 반대로 데이터의 일관성 유지가 어렵다는 문제가 있습니다. 스키마가 자유롭다 보니, 같은 컬렉션 안에 구조가 다른 문서들이 들어갈 수 있고, 그로 인해 나중에 데이터를 가공하거나 검증할 때 오류가 발생하기 쉽습니다. 또한, 조인 기능이 제한적이기 때문에, 데이터 간의 관계를 표현하는 데 한계가 있습니다. 만약 여러 컬렉션에 나눠 저장된 데이터를 합쳐서 조회해야 한다면, 애플리케이션 단에서 로직을 더 많이 처리해야 할 수도 있습니다.

LIST

+ Recent posts