본문 바로가기
일하는 중에

PostgreSQL jsonb, GIN 인덱스, 그리고 미래의 질문을 준비하는 방법

by likebnb 2026. 5. 29.

0. 왜 이 이야기를 하는가

관계형 데이터베이스는 원래 표(Table)를 다루기 위해 만들어졌다. 하지만 현대의 애플리케이션은 꼭 정형 데이터만 다루지 않는다. 사용자 설정 정보, API 응답, 이벤트 로그, IoT 데이터, AI 에이전트의 상태 정보 등은 구조가 자주 바뀌고 깊이가 일정하지 않다. 만약 이런 데이터를 모두 컬럼으로 펼쳐 관리하려고 한다면 스키마 변경은 끝없이 반복될 것이다. 

PostgreSQL은 이 문제를 해결하기 위해 jsonb라는 흥미로운 선택지를 제공한다. 하지만 진짜 재미있는 이야기는 그 다음부터 시작된다. JSON 문서를 저장하는 것만으로는 충분하지 않다. 결국 우리는 그 안에서 데이터를 찾아야 한다. 그리고 PostgreSQL은 이 문제를 해결하기 위해 GIN이라는 독특한 인덱스를 준비해 두었다.

이 글은 jsonb와 GIN의 사용법을 설명하려는 글이 아니다. 그보다 조금 더 근본적인 질문을 던져보려 한다.

PostgreSQL은 왜 이런 구조를 갖게 되었을까?


1. json과 jsonb

PostgreSQL에는 JSON을 저장하는 방법이 두 가지 있다.

json
jsonb

비슷해 보이지만 내부 구조는 완전히 다르다.

json

json은 입력된 문자열을 거의 그대로 보관한다.

{
  "name": "Barnabas",
  "age": 30
}

조회할 때마다 PostgreSQL은 이 문자열을 다시 해석해야 한다. 마치 압축된 문서를 매번 열어보는 것과 비슷하다. 원본 보존에는 유리하지만 검색에는 불리하다.


jsonb

jsonb는 저장 시점에 문서를 분해하고 정규화한다.

{
  "name": "Barnabas",
  "age": 30
}

를 입력하면 내부적으로는 효율적인 이진(Binary) 구조로 변환된다. 따라서 조회 시 매번 JSON 문자열을 다시 분석할 필요가 없다.

그 결과

  • 조회 성능 향상
  • 다양한 연산 지원
  • 인덱스 생성 가능

이라는 장점을 얻는다. 대신 저장 시점에 더 많은 일을 수행한다.


2. 세상은 언제나 Trade-off

jsonb는 읽기를 빠르게 만들기 위해 쓰기 비용을 먼저 지불한다. 대표적인 변화는 다음과 같다.

중복 키 제거

{
  "name": "Tom",
  "name": "Lane"
}

는 결국

{
  "name": "Lane"
}

만 남는다.


순서 보존 안 함

{
  "b": 1,
  "a": 2
}

는 저장 후 원래 순서를 유지하지 않는다.


쓰기 비용 증가

INSERT 또는 UPDATE 시점에 내부 변환이 수행된다.

즉,

저장할 때 더 일하고 조회할 때 편해지는 구조

라고 이해하면 된다.


3. 그런데 JSON 안은 어떻게 찾을까?

이제 문제가 하나 남는다. 다음과 같은 데이터가 있다고 가정하자.

{
  "name": "Alice",
  "country": "KR",
  "role": "admin"
}

우리는 이런 쿼리를 자주 작성한다.

SELECT *
FROM users
WHERE profile @> '{"role":"admin"}';

만약 인덱스가 없다면? 모든 행의 JSON 문서를 열어보며 검사해야 한다. 수백만 건이라면 상당한 비용이 발생한다.

바로 여기서 GIN이 등장한다.


4. GIN의 정체

GIN은 Generalized Inverted Index의 약자다. 핵심은 Inverted, 즉 역색인(Inverted Index)이다.


우리가 익숙한 인덱스

B-Tree는 보통 이런 구조다.

값 → 행 위치

예를 들면

kim  → row1
park → row2
lee  → row3

와 같은 형태다.


GIN의 사고방식

GIN은 방향을 뒤집는다. JSON 문서를 잘게 분해한다.

{
  "country":"KR",
  "role":"admin"
}

를 저장하면 내부적으로는

country=KR → row1
role=admin → row1

과 같은 색인을 만든다. 다른 문서까지 포함하면

role=admin → row1,row5,row9
role=user  → row2,row3,row8
country=KR → row1,row2
country=US → row4,row5

와 비슷한 구조가 된다. 실제로는 PostgreSQL 내부의 TID(Tuple Identifier)가 저장되지만 개념적으로는 이와 같다.


5. 책의 찾아보기와 같은 구조

GIN을 이해할 때 가장 좋은 비유는 책 뒷면의 찾아보기다.

Database .... 12,25,37
Index ....... 15,18,29
Optimizer ... 40,51

찾고 싶은 단어가 있으면 책 전체를 처음부터 읽지 않는다. 찾아보기를 먼저 본다.

GIN도 똑같다.

WHERE profile @> '{"role":"admin"}'

이라는 쿼리가 들어오면 전체 테이블을 뒤지는 것이 아니라

role=admin

이라는 색인을 먼저 확인한다. 그래서 빠르다.


6. 왜 Generalized 인가?

여기서 PostgreSQL 철학이 드러난다. GIN은 특정 데이터 타입을 위해 만든 인덱스가 아니다.

지원 대상은 매우 다양하다.

jsonb
text[]
tsvector

공통점은 하나다. 모두 "쪼갤 수 있는 데이터"라는 것이다.

PostgreSQL은

쪼갤 수 있는 데이터라면 무엇이든 색인할 수 있어야 한다.

는 방향으로 설계되었다. 이것이 Generalized라는 이름의 이유다.


7. 그런데 인덱스는 누가 선택할까?

많은 개발자가 오해하는 부분이 있다. 인덱스를 만들었다고 반드시 사용되는 것은 아니다.

예를 들어

CREATE INDEX idx_email
ON users (lower(email));

를 만들었다고 하자.

그러면

WHERE lower(email) = 'abc@test.com'

은 인덱스를 사용할 수 있다.

하지만

WHERE email = 'abc@test.com'

은 사용할 수 없다. 인덱스에 저장된 값이 다르기 때문이다. 인덱스를 사용할 자격은 갖춰야 한다. 하지만 자격만 있다고 끝은 아니다.


8. 최종 결정권자는 쿼리 플래너

PostgreSQL에는 매우 똑똑한 브레인이 있다. 바로 Query Planner다.

플래너는 다음 정보를 종합적으로 분석한다.

  • 전체 행 수
  • 데이터 분포
  • 선택도(Selectivity)
  • 디스크 I/O 비용
  • CPU 비용
  • 메모리 사용량

그리고 질문한다.

인덱스 스캔이 빠를까?
전체 스캔이 빠를까?

예를 들어 전체 데이터의 90%가 조건에 해당한다면 인덱스를 타는 것보다 Sequential Scan이 더 빠를 수 있다. 그래서 PostgreSQL은 인덱스가 있어도 과감하게 무시한다.


9. PostgreSQL은 왜 이렇게 만들어졌을까?

오늘날 우리는 너무 당연하게 생각한다.

jsonb
GIN
Array
Full Text Search
Custom Type

하지만 1980년대 대부분의 데이터베이스는 그렇지 않았다. 새로운 데이터 타입을 추가하거나 새로운 인덱스를 넣는 것은 거의 불가능했다.

그때 한 연구자가 질문을 던졌다.

미래의 데이터는 지금 우리가 아는 형태만 존재할까?

그 사람이 바로 마이클 스톤브레이커(Michael Stonebraker)다.


데이터베이스도 확장 가능해야 한다

스톤브레이커는 UC 버클리의 POSTGRES 프로젝트를 이끌며 기존 데이터베이스와 다른 철학을 제안했다. 그는 데이터베이스를 완성품이 아니라 플랫폼으로 보았다. 새로운 데이터 타입이 등장하면 추가할 수 있어야 하고, 새로운 인덱스가 필요하면 갈아 끼울 수 있어야 하며, 새로운 연산이 필요하면 엔진 수준에서 확장할 수 있어야 한다고 생각했다.

오늘날 PostgreSQL의

  • User Defined Type
  • User Defined Function
  • Operator Class
  • Index Access Method

구조는 모두 이 철학의 결과다.


GIN은 처음부터 존재하지 않았다

GIN은 PostgreSQL의 원년 기능이 아니다. 2000년대 초, 천문 관측 데이터와 대규모 문서 검색 문제를 연구하던 러시아 과학자들이 PostgreSQL의 확장성 구조에 주목했다.

그들이 바로

  • Teodor Sigaev
  • Oleg Bartunov

이다.

이들은 PostgreSQL이 제공하는 확장 인터페이스 위에서 새로운 인덱스를 구현했다.

그 결과가 GIN이다.


우주에서 온 인덱스

흥미롭게도 GIN은 웹 서비스보다 먼저 천문학 문제를 해결하기 위해 탄생했다. 별 관측 데이터는 수많은 속성과 메타데이터를 포함한다. 이 문제는 오늘날 우리가 JSONB에서 다루는 문제와 매우 유사하다. 개발자들은 문서를 하나의 값으로 보는 대신 문서를 구성하는 단어와 속성을 먼저 색인하는 방식을 선택했다.

그 결과가 역색인이다.


그리고 플래너의 장인

좋은 인덱스만으로는 충분하지 않다. 언제 그것을 사용할지 판단해야 한다. 이 부분에서 중요한 인물이 있다.

Tom Lane.

오늘날 PostgreSQL의 비용 기반 최적화기(Cost-Based Optimizer)를 이야기할 때 빼놓을 수 없는 인물이다.

플래너는

  • 데이터 분포
  • 선택도
  • CPU 비용
  • I/O 비용

을 계산하며 지금 가장 효율적인 실행 계획을 선택한다.


10. 결국 PostgreSQL이 보여주는 철학

많은 사람들은 GIN을 하나의 인덱스 종류로 기억한다. 하지만 조금 더 깊게 들여다보면 GIN의 진짜 가치는 인덱스 자체가 아니다.

더 중요한 것은

아직 존재하지 않는 미래의 문제를 해결할 수 있도록 데이터베이스를 설계한다.

는 철학이다.

스톤브레이커는 그 철학을 위한 뼈대를 만들었다. 시가에브와 바르투노프는 그 위에 GIN을 올렸다. 톰 레인은 그것들이 가장 효율적으로 동작하도록 두뇌를 다듬었다. 그리고 그 결과 우리는 오늘도 아무렇지 않게

WHERE profile @> '{"role":"admin"}'

와 같은 쿼리를 사용하고 있다.


마무리

좋은 인덱스는 데이터를 잘 저장하는 데만 집중하는 것이 아니다. 미래에 어떤 질문이 들어올지 고민하고, 아직 존재하지 않는 질문까지도 수용할 수 있도록 탐색의 길을 준비하는 것이다. PostgreSQL은 지난 40년 동안 바로 그 길을 만들어 온 데이터베이스다.