본문으로 건너뛰기

prev-link 복구 오류

· 약 7분

secondary 하나가 복구를 못 끝내고 같은 로그만 뱉고 있었어요. WAL을 archive에서 한 번 당겨오고, 곧바로 record 하나를 읽다 실패하고, 다시 같은 파일을 당겨오는 흐름이 2초 간격으로 무한히 돌았어요.

2026-07-24 13:41:10 KST LOG: record with incorrect prev-link 67F/7BAB8 at 67F/40000028
2026-07-24 13:41:12 KST LOG: restored log file "00000068...0067F..." from archive
2026-07-24 13:41:12 KST LOG: record with incorrect prev-link 67F/7BAB8 at 67F/40000028
2026-07-24 13:41:15 KST LOG: restored log file "00000068...0067F..." from archive
2026-07-24 13:41:15 KST LOG: record with incorrect prev-link 67F/7BAB8 at 67F/40000028

디스크가 깨진 것처럼 보이지만 그건 아니었습니다. 이 메시지는 대부분 "여기가 이 timeline에서 유효한 WAL의 끝"이라는 신호입니다. 문제는 secondary가 그 끝에서 다음 timeline으로 못 건너가고 제자리를 맴돌았다는 점입니다.

이 로그가 정확히 어디서 나오나

두 줄은 서로 다른 주체가 찍습니다.

restored log file ... from archive는 recovery 중인 서버가 restore_command로 archive에서 WAL 세그먼트 하나를 가져왔다는 뜻입니다. 우리 환경은 pgBackRest가 archive 역할이라, 이 줄은 pgBackRest에서 세그먼트를 복원해 왔다는 기록입니다.

record with incorrect prev-link A at B는 WAL을 읽는 xlogreader가 찍습니다. PostgreSQL의 WAL record는 헤더에 xl_prev라는 필드를 들고 있는데, 바로 앞 record가 어디서 끝났는지를 가리키는 back-link입니다. reader는 record를 하나 읽을 때마다 "이 record의 xl_prev가 방금 내가 읽은 record의 끝과 같은가"를 확인합니다. 어긋나면 이 메시지를 남기고 그 자리를 유효한 WAL의 끝으로 간주합니다.

메시지의 두 LSN은 순서대로 이렇게 읽습니다. at 뒤(67F/40000028)가 문제의 record가 놓인 위치이고, 앞(67F/7BAB8)이 그 record에 적혀 있던 xl_prev 값입니다. 즉 67F/40000028에 있는 record는 자기 앞 record가 67F/7BAB8에서 끝났다고 주장하는데, reader가 실제로 그 지점까지 읽어온 맥락과 맞지 않는 것입니다.

여기서 위치 67F/40000028이 힌트를 줍니다. WAL 세그먼트는 기본 16MB이고, 오프셋 0x40000028은 세그먼트 경계에서 딱 0x28(40바이트) 들어간 자리입니다. 40바이트는 세그먼트 첫 페이지의 long page header 크기와 같습니다. 다시 말해 이 record는 어느 세그먼트의 맨 첫 record 자리에 있습니다. 그 자리의 xl_prev가 한참 앞인 67F/7BAB8(같은 논리 파일에서 500KB쯤 되는 지점)를 가리킨다는 건, 그 세그먼트 앞부분이 과거에 쓰이고 아직 새 내용으로 덮이지 않은 recycled 세그먼트의 잔재라는 신호에 가깝습니다. PostgreSQL은 성능을 위해 WAL 파일을 지우지 않고 이름만 바꿔 재사용하는데, reader가 그 옛 바이트열을 그럴듯한 record로 오독하면 이런 back-link 불일치가 나옵니다. xlogreader 소스메일링 리스트 논의에서도 같은 진단이 나옵니다.

어쩌다 이 상태가 됐나

먼저 분명히 해두면, 아래 순서는 사후에 되짚어 본 유추입니다. 운영 중 이것저것 만지다 이 상태에 도달했고, 어느 한 단계가 범인이라고 특정하지는 못했습니다. 다만 secondary가 이 지경이 되기까지 밟은 대략의 경로는 남겨 둡니다. HA는 pg_auto_failover로 묶여 있고 백업은 pgBackRest로 받는 환경이었습니다.

순서한 일부수 효과
1최초 primary/secondary 구성timeline 시작
2failover 발생옛 secondary 승격, timeline 증가
3옛 secondary 노드 drop구 노드 제거
4새 secondary add (pgBackRest 백업본으로 seed)과거 시점 데이터로 출발
5다시 failovertimeline 한 번 더 증가

failover는 standby를 primary로 승격시키면서 새 timeline을 엽니다. 이때 .history 파일이 만들어져 "몇 번 timeline은 어느 LSN에서 갈라져 나왔다"를 기록하고, 뒤따르는 standby는 이 history를 보고 분기 지점을 넘어 새 timeline의 WAL로 갈아탑니다. 갈아타려면 recovery_target_timeline이 최신을 따라가도록 서 있어야 하고, 분기 지점 이후의 WAL과 history 파일이 archive에 제대로 올라와 있어야 합니다.

이 증상 자체는 문서화가 잘 되어 있습니다. PostgreSQL 코어 개발자 Michael Paquier(미하엘 파키에)는 같은 로그를 두고 "현재 timeline에서 유효한 WAL의 끝을 가리키는 것이며, 과거에 recycled 세그먼트로 쓰였던 영역을 읽을 때 마주칠 수 있다"고 설명합니다. 같은 스레드에는 두 가지가 더 나옵니다. 하나는 이 오류가 archive_mode가 켜져 있을 때 주로 나타난다는 관찰이고, 다른 하나는 여러 번 승격을 거친 노드를 다시 붙일 때 반복해서 겪었다는 보고입니다. archive에서 WAL을 당겨오고, failover를 여러 번 돌린 우리 상황과 겹치는 대목입니다.

그 틀에 우리 경로를 얹으면 이렇게 읽힙니다. 과거 백업본에서 출발한 secondary가 두 번째 failover로 timeline이 또 올라간 뒤, 자기가 따라가던 timeline의 끝에 도달하고도 다음 timeline으로 건너갈 연결을 archive에서 매끄럽게 잇지 못한 것으로 보입니다. 분기 지점 근처 세그먼트에 recycled 잔재가 남아 있었고, reader가 그 자리를 67F/40000028의 깨진 record로 읽었다는 그림입니다. 단정이 아니라 관측된 증상과 문서화된 메커니즘을 맞춰 본 해석입니다.

왜 하필 무한 루프였나

멈추지 않고 도는 게 이 현상의 성격을 잘 보여줍니다.

recovery는 다음 WAL을 기다리다 restore_command를 다시 부르고, archive는 같은 세그먼트를 또 건네주고, reader는 같은 자리에서 같은 back-link 불일치를 만납니다. 넘어가야 할 새 timeline의 이력을 집어오지 못하는 한, 이 고리는 스스로 풀리지 않습니다. 로그의 LSN이 67F/40000028로 매번 똑같이 고정돼 있는 게 그 증거였습니다. 앞으로 나아가는 복구라면 LSN이 조금씩이라도 커집니다.

이 대목에서 disk corruption과 헷갈리기 쉬운데, 구분 기준은 단순합니다. 같은 세그먼트를 반복 restore하면서 같은 LSN에 멈춰 있으면 timeline 경계 문제이고, 서로 다른 위치에서 읽기 자체가 깨지면 그때 물리 손상을 의심합니다.

어떻게 걷어냈나

pg_rewind나 recovery_target_timeline=latest 조정으로 붙여보는 길이 먼저 떠오릅니다. 다만 메일링 리스트에도 나오듯 pg_rewind가 timeline 충돌은 정리해도 이 back-link 오류 자체를 없애주지 못하는 경우가 있습니다. 게다가 이 노드는 4번에서 이미 과거 백업본으로 새로 붙인 상태였습니다. 어중간하게 되살리기보다 현재 timeline 기준으로 깨끗이 다시 seed하는 편이 빨랐습니다.

그래서 pg_auto_failover의 노드 재구축으로 정리했습니다.

# 꼬인 secondary 노드를 데이터까지 완전히 제거
pg_autoctl drop node --destroy

# 현재 primary 기준으로 secondary 새로 구축
pg_autoctl create postgres

--destroy는 monitor에서 노드를 지우는 데 그치지 않고 데이터 디렉토리까지 함께 정리합니다. 이어서 pg_autoctl create postgres가 현재 primary의 최신 timeline을 기준으로 base backup을 다시 받아 복제를 새로 세웁니다. recycled 잔재를 담고 있던 옛 데이터가 통째로 사라지니 back-link 불일치도 같이 사라졌고, 복제는 정상으로 따라붙었습니다.

남는 메모

재구축이 정공법은 아닐 수 있습니다. 원인을 LSN 단위로 끝까지 파고들면 어느 failover에서 archive에 어떤 공백이 생겼는지 특정할 수도 있었을 것입니다. 다만 pg_auto_failover처럼 오케스트레이션이 노드 수명주기를 쥐고 있는 환경에서는 백업본에서 노드를 다시 붙이는 비용이 낮은 만큼, 포렌식보다 재구축이 대체로 실용적이었습니다.

정리하자면 이래요. standby가 archive만 반복해서 restore하면서 고정된 LSN에 record with incorrect prev-link로 멈춰 있으면, 물리 손상보다 timeline 경계를 못 넘은 상황을 먼저 의심하는 게 맞아요. 그리고 failover를 여러 번 돌리는 사이에 과거 백업본으로 노드를 되붙이는 조합은 timeline 이력이 엉키기 딱 좋은 지점이라, 그 앞뒤로는 archive에 history 파일과 분기 이후 WAL이 온전히 올라와 있는지 한 번 더 확인해 둘 만해요.

참고

Claude Code Fullscreen

· 약 7분

입력창이 더는 안 흔들린다

Claude가 작업하는 동안 화면이 번쩍이고, 출력이 쏟아질 때 스크롤이 맨 위로 튀어 오르고, 긴 세션에서 메모리가 슬금슬금 차오르던 경험은 Claude Code를 오래 쓴 사람이면 익숙해요. Fullscreen 렌더링은 그 세 가지를 한 번에 잡으려고 나온 새 렌더링 경로로, 대화를 vim이나 htop처럼 터미널의 alternate screen buffer에 그려요.

/tui fullscreen 한 줄이면 켜집니다. 대화는 그대로 유지된 채 fullscreen으로 다시 뜹니다. 세션 중간에 켜도 맥락을 잃지 않습니다. Claude Code v2.1.89 이상에서 동작하는 리서치 프리뷰 기능입니다. (Fullscreen 렌더링 공식 문서)

주의: 이 글은 전부 리서치 프리뷰 기준입니다. 동작 방식, 키 바인딩, 설정 키 이름 모두 정식 출시 전에 바뀔 수 있습니다.

fullscreen은 창 최대화가 아닙니다

먼저 오해부터 풀고 갑니다. 여기서 fullscreen은 터미널 창을 최대화한다는 뜻이 아닙니다. Claude Code가 터미널의 그리는 표면(drawing surface)을 vim처럼 통째로 가져간다는 뜻입니다. 창 크기와 무관하게, 작은 창에서도 똑같이 동작합니다.

기존 렌더러(classic)는 대화를 터미널의 native scrollback에 차곡차곡 쌓습니다. 매 업데이트마다 화면을 다시 그리면서 터미널로 많은 데이터를 흘려보냅니다. Fullscreen은 다릅니다. 지금 화면에 보이는 메시지만 렌더 트리에 둡니다. 터미널로 보내는 데이터량이 확 줄어듭니다.

깜빡임, 메모리, 스크롤 점프를 줄입니다

세 가지 문제를 노립니다.

문제classic 렌더러fullscreen 렌더링
화면 깜빡임출력 스트리밍 중 번쩍임 발생alternate screen buffer로 제거
메모리대화가 길어질수록 증가보이는 메시지만 유지, 일정하게
스크롤 점프작업 중 맨 위로 튀어 오름입력창 하단 고정, 점프 없음

차이가 가장 크게 느껴지는 곳은 렌더링 처리량(throughput)이 병목인 터미널입니다. 공식 문서는 VS Code 통합 터미널, tmux, iTerm2를 콕 집습니다. 이런 환경에서 화면이 번쩍이거나 스크롤이 튀었다면 fullscreen이 정확히 그 증상을 겨냥합니다.

켜고 끄는 법

가장 쉬운 방법은 대화 안에서 슬래시 명령을 실행하는 것입니다.

/tui fullscreen # fullscreen 렌더링으로 전환 (대화 유지된 채 재시작)
/tui default # classic 렌더러로 복귀
/tui # 인자 없이 실행하면 현재 어떤 렌더러인지 출력

/tui 명령은 tui 설정을 ~/.claude/settings.json에 저장하고 그 값으로 다시 띄웁니다. 그래서 다음 세션에도 선택이 유지됩니다.

환경 변수로 켤 수도 있습니다. /tui 명령이 생긴 v2.1.110 이전 버전에서는 이쪽이 유일한 방법이었습니다.

CLAUDE_CODE_NO_FLICKER=1 claude

tui 설정과 CLAUDE_CODE_NO_FLICKER 환경 변수는 같은 의미입니다. /tui 명령은 재시작하는 프로세스에서 CLAUDE_CODE_NO_FLICKER를 지워, 새로 쓴 설정 값이 우선 적용되게 합니다.

fullscreen이 켜졌는지 확인하는 가장 직관적인 신호는 입력창입니다. 출력이 쏟아져도 입력창이 화면 하단에 고정돼 움직이지 않으면 fullscreen이 동작 중입니다. classic 렌더러에서는 입력창이 출력을 따라 위로 밀려 올라갑니다.

두 렌더러는 어디서 갈리나

대화가 어디에 저장되느냐가 갈림길입니다. 그 한 가지 차이에서 검색/복사 동작이 전부 달라집니다.

대화가 alternate screen buffer에 살기 때문에, 터미널의 native scrollback에 의존하던 동작이 달라집니다.

기존 (classic)fullscreen비고
Cmd+f/tmux 검색으로 텍스트 찾기Ctrl+o로 transcript 모드 진입 후 /로 검색검색 경로가 앱 안으로
터미널 native 드래그로 선택/복사앱 안에서 선택, 마우스 떼면 자동 복사클립보드는 앱이 채움
Cmd-클릭으로 URL 열기macOS는 Cmd-클릭, 그 외는 Ctrl-클릭OS별 키 차이

마우스가 붙는다

Fullscreen은 마우스 이벤트를 캡처해 Claude Code 안에서 처리합니다. classic 렌더러에는 없던 기능입니다.

입력창을 클릭하면 입력 중인 텍스트의 원하는 위치로 커서가 옮겨집니다. / 명령이나 @ 파일 목록에서는 제안 항목을 클릭으로 바로 고르고, 호버하면 해당 행이 강조됩니다. 권한 프롬프트나 /model//config 같은 select 메뉴도 클릭으로 선택할 수 있습니다(v2.1.187 이상). 접힌 tool 결과를 클릭하면 펼쳐서 전체 출력을 보고, 다시 클릭하면 접힙니다. URL이나 파일 경로는 Cmd(macOS)/Ctrl(Linux/Windows)를 누른 채 클릭하면 링크는 브라우저로, 파일 경로는 기본 앱으로 열립니다. v2.1.181부터는 키 없이 그냥 클릭해도 링크가 열리지 않는데, native 터미널 동작에 맞춘 변경입니다. 이 밖에 드래그로 선택하고 휠로 스크롤하며, 더블클릭은 단어, 트리플클릭은 줄 단위로 선택합니다.

선택한 텍스트는 마우스를 떼는 순간 클립보드에 자동 복사됩니다. 이 동작이 거슬리면 /config에서 Copy on select를 끕니다.

스크롤과 검색

스크롤도 앱 안에서 처리합니다. 주요 단축키는 다음과 같습니다.

단축키동작
PgUp / PgDn반 화면씩 위/아래
Ctrl+Home대화 맨 처음으로
Ctrl+End최신 메시지로 + auto-follow 재개
마우스 휠몇 줄씩 스크롤

MacBook처럼 PgUp/PgDn 전용 키가 없는 키보드는 Fn과 방향키를 조합합니다. Fn+↑PgUp, Fn+↓PgDn입니다.

위로 스크롤하면 auto-follow가 멈춰, 새 출력이 다시 맨 아래로 끌어내리지 않습니다. Ctrl+End를 누르거나 맨 아래로 내리면 다시 따라가기 시작합니다.

검색은 transcript 모드에서 합니다. Ctrl+o로 일반 프롬프트와 transcript 모드를 오갑니다. transcript 모드는 less 스타일 탐색을 지원합니다.

동작
/검색 시작
n / N다음/이전 매치로 이동
g / G맨 위/맨 아래로
Ctrl+o, Esc, qtranscript 모드 종료

터미널의 Cmd+f나 tmux 검색은 대화를 보지 못합니다. 대화가 native scrollback이 아니라 alternate screen buffer에 있기 때문입니다. 내용을 터미널로 다시 넘기려면 Ctrl+o로 transcript 모드에 들어간 뒤 [를 누릅니다. 전체 대화가 native scrollback에 평범한 텍스트로 다시 쓰이고, 그때부터 Cmd+f/tmux copy mode가 다시 통합니다.

tmux와 함께 쓸 때

Fullscreen은 tmux 안에서도 동작합니다. 세 가지만 주의합니다.

첫째, 마우스 휠 스크롤은 tmux의 mouse mode가 필요합니다. ~/.tmux.conf에 아래 한 줄을 넣고 설정을 다시 읽습니다. 없으면 휠 이벤트가 Claude Code 대신 tmux로 갑니다.

set -g mouse on

둘째, iTerm2의 tmux 통합 모드(tmux -CC)와는 호환되지 않습니다. 이 모드에서는 alternate screen buffer와 마우스 추적이 제대로 동작하지 않고, 더블클릭이 터미널 상태를 깨뜨릴 수 있습니다. tmux -CC 세션에서는 fullscreen을 켜지 않는 편이 좋습니다. -CC 없는 일반 tmux는 문제없습니다.

셋째, tmux는 synchronized output을 지원하지 않아, Claude Code를 터미널에서 바로 실행할 때보다 redraw 중 깜빡임이 더 보일 수 있습니다. SSH 환경에서 특히 거슬리면 tmux 밖 별도 탭에서 실행하는 편이 낫습니다.

native 선택을 그대로 두고 싶다면

마우스 캡처는 가장 흔한 마찰 지점입니다. SSH나 tmux 안에서 특히 그렇습니다. Claude Code가 마우스 이벤트를 가져가면 터미널의 native copy-on-select가 멈춥니다. 드래그로 만든 선택은 Claude Code 안에만 있고, 터미널의 선택 버퍼에는 없습니다.

한 번만 native 선택을 쓰고 싶으면 터미널별 지정 키를 누른 채 드래그합니다. Terminal.app은 Fn, iTerm2는 Option, VS Code/Cursor는 Shift입니다.

마우스 캡처 자체가 늘 거슬린다면, 깜빡임 제거와 일정한 메모리는 유지하면서 마우스 캡처만 끌 수 있습니다.

CLAUDE_CODE_NO_FLICKER=1 CLAUDE_CODE_DISABLE_MOUSE=1 claude

이렇게 하면 PgUp, PgDn, Ctrl+Home, Ctrl+End 키보드 스크롤은 그대로 살고, 선택은 터미널이 native로 처리합니다. 대신 클릭으로 커서 옮기기, tool 결과 펼치기, URL 클릭, 앱 안 휠 스크롤은 포기해야 합니다.

DBA/개발자가 지금 해 볼 것

긴 작업을 자주 돌리는 사람일수록 효과가 큽니다. 대형 리팩터링이나 수십 개 파일을 훑는 세션에서 classic 렌더러는 출력이 쌓이며 메모리가 늘고 스크롤이 튀지만, fullscreen은 일정하게 버팁니다.

일단 /tui fullscreen을 실행하고, 출력이 쏟아질 때 입력창이 고정되는지 봅니다. 안 맞으면 /tui default로 즉시 돌아오면 됩니다. VS Code, tmux, iTerm2 사용자라면 우선순위를 높게 둘 만한데, 깜빡임/스크롤 점프가 가장 심하던 환경이라 체감 차이가 큽니다. tmux를 쓴다면 set -g mouse on을 먼저 넣어야 휠 스크롤이 tmux로 새어 나가지 않습니다. 마지막으로 검색 습관을 바꿔야 합니다. Cmd+f 대신 Ctrl+o로 transcript 모드에 들어가 /로 찾고, 내용을 터미널로 넘기려면 그 안에서 [를 누릅니다.

복귀 경로가 명확한 것도 저는 안심돼요. /tui default로 돌아가거나, 저장된 설정과 무관하게 classic 렌더러를 강제하려면 CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1을 둡니다. agent view나 claude attach로 여는 백그라운드 세션은 항상 fullscreen으로 뜬다는 점만 기억하면 됩니다.

정리

Fullscreen 렌더링은 Claude Code의 TUI를 vim/htop 계열로 한 단계 올립니다. alternate screen buffer를 써서 깜빡임이 사라지고 메모리가 일정해지며 마우스가 붙는 대신, 검색/복사는 앱 안 동작으로 바뀝니다. 손에 익히는 데 며칠 걸리지만, 긴 세션을 자주 돌리는 사람에게는 그만한 값을 합니다.

아직 리서치 프리뷰라서 흔치 않은 터미널이나 특이한 설정에서는 렌더링 문제가 생길 수 있고, 그럴 땐 /feedback이나 claude-code GitHub 저장소로 터미널 이름/버전과 함께 알리면 돼요. 동작이 바뀔 여지도 남아 있지만, 한 줄(/tui fullscreen)로 켜고 한 줄(/tui default)로 끄는 비용이면 한 번 켜 보고 판단하기에 충분히 싸요.

Slack의 Claude Tag

· 약 6분

무엇이 나왔나

2026년 6월 23일, Anthropic이 Slack 채널 안에서 한 명의 팀원처럼 일하는 Claude Tag를 베타로 공개했고, 쓰는 법은 단순해요. 채널에서 @Claude로 태그하고 할 일을 말로 던지면, Claude가 작업을 단계로 쪼개 도구를 써가며 처리한 뒤 같은 Slack 스레드에 결과를 보고해요. Anthropic은 이를 사람이 매번 묻는 챗봇이 아니라, 채널에 상주하며 문맥을 쌓는 팀원으로 설명해요.

지난 글에서 정리했듯 그동안 Claude Code의 무대는 터미널이었습니다. slash command가 skills로 통합되고, Opus 4.8이 수백 개의 subagent를 굴리는 식으로 위임의 단위가 커졌지만, 어디까지나 개발자의 CLI 안에서 벌어진 일이었습니다. Claude Tag는 그 흐름을 협업 도구로 끌어냅니다. 개발자가 아닌 사람도 매일 머무는 Slack 채널에서 같은 Claude를 부릅니다.

어떻게 동작하나

Claude Tag의 성격은 세 가지로 압축됩니다. 공유 정체성, 문맥 학습, 알아서 끼어드는 ambient 모드입니다.

채널마다 하나의 공유 Claude

기존 챗봇이 사람마다 별도의 1:1 대화였다면, Claude Tag는 채널 단위로 하나의 Claude가 모두를 상대합니다. 같은 채널에 있는 사람은 Claude가 지금 무슨 작업을 하는지 볼 수 있고, 앞사람이 멈춘 지점에서 대화를 이어받을 수 있습니다. 작업이 한 사람의 DM에 갇히지 않고 팀의 공동 작업으로 굴러간다는 뜻입니다.

채널을 따라가며 문맥을 쌓는다

Claude는 배정된 채널의 대화를 따라가며 일에 대한 문맥을 축적합니다. Anthropic의 표현으로는 "채널을 따라갈수록 그 일에 대해 점점 더 많이 알게 된다". 권한을 주면 다른 Slack 채널이나 연결된 데이터 소스에서도 사실을 끌어올 수 있습니다. 매번 처음부터 배경을 설명하지 않아도 된다는 게 핵심입니다. 단, 비공개 채널은 학습 대상에서 빠집니다.

ambient 모드는 먼저 끼어든다

ambient 동작을 켜면, Claude는 호출을 기다리지 않습니다. 연결된 채널과 도구를 살피다가 팀이 알면 좋겠다 싶은 정보를 먼저 꺼내고, 방치된 스레드나 멈춘 작업을 따라가 챙깁니다. 작업을 던져두면 사람이 다른 일을 하는 동안 비동기로 처리하고, 길게는 수 시간에서 며칠에 걸쳐 자율적으로 일을 진행하기도 합니다.

호출에서 보고까지의 흐름은 다음과 같습니다.

기존 Slack 통합과 뭐가 다른가

Claude Tag는 기존 "Claude in Slack" 앱을 대체합니다. 둘 다 Slack 안에서 Claude를 부른다는 점은 같지만, 동작 모델이 다릅니다.

기존 Claude in SlackClaude Tag
대화 단위사람별 1:1채널 단위 공유 Claude
문맥 유지세션 한정채널을 따라가며 지속 축적
능동성호출해야 응답ambient 모드로 먼저 제안
비동기 작업제한적수 시간~수일 자율 진행
권한 관리제한적관리자가 채널별 도구/데이터 범위 지정
작업 가시성개인 DM채널 구성원이 함께 관찰

가장 큰 차이는 정체성의 단위입니다. 기존 통합이 "제가 부르는 비서"였다면, Claude Tag는 "채널에 함께 있는 팀원"에 가깝습니다.

CLI 쪽 Claude Code와 비교하면 무대가 옮겨졌다는 점이 분명해집니다.

Claude Code (CLI)Claude Tag (Slack)
무대개발자 터미널팀 협업 채널
주 사용자개발자채널의 모든 구성원
호출터미널 세션@Claude 태그
협업개인 작업 위주다중 사용자 핸드오프
문맥프로젝트 파일/세션채널 대화/연결된 데이터

권한과 거버넌스

조직에 들이는 도구인 만큼 통제 장치가 함께 설계됐습니다. 관리자는 Claude가 채널별로 어떤 도구와 데이터에 접근할지 정하고, 용도에 따라 별도의 Claude 정체성을 분리해 만듭니다. 예를 들어 법무용 Claude가 쌓은 메모리는 엔지니어링 채널로 새어 들어가지 못합니다. 누적된 기억을 포함한 모든 것이 지정된 경계 안에 머뭅니다.

여기에 조직/채널별 토큰 지출 한도를 걸 수 있고, Claude의 활동과 요청자를 남기는 감사 로그도 제공됩니다. 데이터가 부서 경계를 넘지 않도록 막는 장치와 비용/이력을 추적하는 장치도 함께 들어갔습니다.

실무 활용과 한계/요금

Anthropic은 사내 활용 수치를 함께 내놨습니다. 자사 제품팀 코드의 65%를 내부판 Claude Tag가 작성한다는 것입니다. The Decoder에 따르면 용도는 엔지니어링에 그치지 않고 제품 지표 추적, 지원 티켓, 버그 진단까지 걸쳐 있습니다. 채널에 던지는 일상적인 요청을 Claude가 받아 처리하는 그림입니다.

엔진은 Opus 4.8입니다. 수백 개의 subagent를 굴리는 동적 워크플로우를 얹은 그 모델이, 이번에는 Slack 채널이라는 새 무대 위에서 돌아갑니다.

가용성과 요금 관련 사실은 다음과 같습니다.

항목내용
상태베타
제공 플랜Claude Enterprise / Claude Team
엔진Opus 4.8
기존 앱"Claude in Slack" 대체, 마이그레이션 기간 30일
비용 통제조직/채널별 토큰 지출 한도 설정
확장 계획Slack 외 다른 플랫폼으로 확대 예정

출시와 함께 적격 조직에는 도입용 크레딧이 지급됩니다. VentureBeat를 비롯한 일부 매체는 Enterprise 25,000달러, 일정 좌석 수 이상의 Team 2,500달러 수준으로 전했는데, 구체적 금액은 매체 보도이므로 실제 조건은 공식 안내로 확인하는 편이 안전합니다.

한계도 분명합니다. 우선 Enterprise와 Team 플랜에 묶인 베타라 아무나 바로 쓸 수 있는 기능이 아닙니다. 채널 대화를 따라가며 문맥을 쌓는 구조이므로, 어떤 채널에 들이고 어떤 데이터에 접근시킬지를 처음부터 신중하게 설계해야 합니다. ambient 모드로 먼저 끼어드는 동작은 편리한 만큼, 켜는 순간 Claude가 채널을 들여다보는 범위가 넓어진다는 뜻이기도 합니다. 도입에 앞서 권한 범위와 토큰 한도를 정해둘 필요가 있습니다.

정리

Claude Tag는 Claude를 터미널 밖으로 한 걸음 더 꺼냈습니다. Claude Code가 개발자의 CLI에 자리 잡고, Opus 4.8이 위임의 규모를 키웠다면, 이번에는 위임의 장소가 팀이 매일 머무는 협업 채널로 옮겨졌습니다.

요약하면 다음과 같습니다.

  • 채널에서 @Claude 태그로 호출하면 작업을 쪼개 처리하고 스레드에 보고합니다.
  • 채널마다 하나의 공유 Claude가 동작하고, 대화를 따라가며 조직 문맥을 쌓습니다.
  • ambient 모드를 켜면 먼저 정보를 꺼내고 멈춘 작업을 챙깁니다.
  • 관리자가 채널별 도구/데이터 범위와 토큰 한도를 통제합니다.
  • 현재 Enterprise/Team 베타이며, 엔진은 Opus 4.8이고, Slack 외 플랫폼으로 확대가 예고됐습니다.

비서를 부르는 단계에서 팀원을 합류시키는 단계로의 이동이에요. Slack에서 시작했지만, "다른 플랫폼으로 넓힌다"는 예고가 붙은 이상 Claude가 협업 도구 곳곳으로 번지는 흐름의 시작점으로 보는 게 맞아요.

참고 자료

pg_statistic 통계 구조

· 약 10분

PostgreSQL planner는 테이블의 실제 데이터를 들여다보지 않아요. 100만 건짜리 테이블에 조건을 걸어도, planner는 그 100만 건을 세어 보는 대신 pg_statistic에 저장된 통계 요약만 읽고 "이 조건이면 대략 몇 row가 나오겠다"고 추정해요. 이 추정값이 맞으면 좋은 계획이 나오고, 틀어지면 seq scan을 해야 할 자리에 index scan을 고르거나 nested loop가 터지는 계획이 나와요. 그래서 쿼리가 느릴 때 인덱스보다 먼저 의심해야 할 게 통계예요.

이 글은 그 통계가 어디서 와서 어디에 저장되고 planner가 어떻게 읽는지를 따라갑니다. 시드 출처(richyen.com)와 PostgreSQL 공식 문서를 교차 확인하며 한국어 실무 관점으로 다시 엮었습니다.

왜 통계가 실행계획을 좌우하나

planner의 일은 같은 쿼리를 실행하는 여러 방법(seq scan vs index scan, nested loop vs hash join, 조인 순서) 중 비용이 가장 싼 것을 고르는 것입니다. 비용을 계산하려면 각 단계에서 몇 row가 흘러갈지를 알아야 합니다. 그런데 쿼리를 실제로 돌려 보고 정할 수는 없으니, 미리 떠둔 통계로 추정합니다.

추정이 틀어지는 전형적인 예는 이렇습니다. 어떤 조건이 실제로는 4000 row를 반환하는데 planner가 8 row로 추정했다면, planner는 "8 row니까 index scan으로 한 건씩 찾아오는 게 싸겠다"고 판단합니다. 막상 실행하면 4000번을 random access로 긁느라 seq scan보다 훨씬 느려집니다. 계획 자체는 통계가 맞다는 가정 아래 합리적이었습니다. 통계가 틀렸습니다.

그래서 row 추정(row estimation)이 쿼리 성능의 출발점입니다. 시드 글의 표현을 빌리면 "planner는 쥐여 준 통계만큼만 똑똑하다".

ANALYZE가 통계를 만드는 과정

통계를 채우는 명령은 ANALYZE입니다. autovacuum이 백그라운드에서 알아서 돌려 주지만, 대량 적재나 마이그레이션 직후에는 통계가 옛날 값이라 직접 실행해 주는 게 안전합니다.

ANALYZE customers; -- 테이블 전체 컬럼 통계 갱신
ANALYZE customers (state); -- 특정 컬럼만

여기서 주목할 점은 ANALYZE가 테이블 전체를 읽지 않는다는 사실입니다. 큰 테이블을 매번 통째로 스캔하면 비용이 감당되지 않으니, 무작위 샘플을 떠서 그걸로 분포를 추정합니다.

샘플 크기는 통계 타깃(statistics target)에 비례합니다. default_statistics_target이 기본값 100일 때 ANALYZE는 약 30,000개의 row를 샘플로 뽑습니다(테이블이 작으면 그보다 적게 뽑습니다). 무작위성을 보장하기 위해 Vitter의 reservoir sampling 알고리즘을 씁니다. 샘플을 다 모으면 컬럼별로 분포를 계산해 pg_statistic에 저장합니다.

이때 테이블 전체 규모를 나타내는 reltuples(행 수)와 relpages(블록 수)는 pg_class에 따로 저장됩니다. 이 값들은 실시간으로 갱신되지 않고 VACUUM, ANALYZE, 일부 DDL 시점에만 갱신되며, 전체를 스캔하지 않은 경우 스캔한 부분으로부터 근사값을 추정해 갱신합니다.

pg_statistic은 슬롯 구조라 사람이 직접 읽기 까다롭습니다. 그래서 PostgreSQL은 같은 내용을 사람이 읽기 좋게 펼친 pg_stats 뷰를 제공하고, 일반 사용자도 자기 권한 안에서 조회할 수 있습니다. 실무에서는 거의 항상 pg_stats를 봅니다.

아래는 ANALYZE가 통계를 만들어 planner에 닿기까지의 흐름입니다.

pg_stats 주요 컬럼 읽는 법

pg_stats 한 행은 한 컬럼의 통계 요약입니다. 컬럼이 여럿이지만 실무에서 손이 가는 건 다섯 개 정도입니다.

컬럼의미무엇을 말해 주나
null_fracNULL인 행의 비율NULL이 얼마나 흔한가
avg_width값의 평균 바이트 폭행 크기/메모리 추정
n_distinct서로 다른 값의 수(또는 비율)카디널리티
most_common_vals가장 흔한 값 목록(MCV)편향된 값
most_common_freqsMCV 각 값의 빈도그 값의 점유율
histogram_boundsMCV를 뺀 나머지 분포의 경계범위 조건 추정
correlation물리 순서와 논리 순서의 상관index scan 효율

조회는 이렇게 합니다.

SELECT attname, n_distinct, null_frac,
most_common_vals, most_common_freqs,
correlation
FROM pg_stats
WHERE tablename = 'customers' AND attname = 'state';

n_distinct

n_distinct에서 눈여겨볼 것은 음수의 의미입니다 — 서로 다른 값의 추정 개수를 나타내지만 별도의 부호 규칙이 있습니다. 공식 문서 정의는 이렇습니다.

0보다 크면 컬럼의 distinct 값 추정 개수. 0보다 작으면 distinct 값 개수를 행 수로 나눈 값의 음수. (음수 형태는 테이블이 커질수록 distinct 값도 늘어날 것으로 ANALYZE가 판단할 때 쓰이고, 양수 형태는 가능한 값의 수가 고정돼 보일 때 쓰인다.) 예를 들어 -1은 distinct 값 수가 행 수와 같은 unique 컬럼을 뜻한다.

state 컬럼이 50으로 나오면 "값이 50종이고 테이블이 커져도 50종일 것"이라는 뜻이고, 기본키처럼 -1이면 "모든 값이 다 다르다"는 뜻입니다. -0.5라면 "행 두 개당 distinct 값 하나꼴"입니다.

이 값이 실제와 어긋나면 추정이 통째로 흔들립니다. 실제로는 값이 만 종인데 통계가 100종으로 잡혀 있으면 planner는 100분의 1만 걸러질 조건을 만 분의 1로 착각합니다.

most_common_vals와 most_common_freqs

여기서는 편향된 값을 읽습니다 — 분포가 한쪽으로 쏠린 컬럼에서는 흔한 값 몇 개가 통계를 지배합니다. most_common_vals(MCV)는 그 흔한 값들의 목록이고, most_common_freqs는 각 값이 차지하는 비율입니다. 두 배열은 같은 순서로 짝을 이룹니다.

예를 들어 state 컬럼에서 CA가 0.174, TX가 0.116으로 나온다면, planner는 WHERE state = 'CA'에 대해 "전체의 17.4%"라고 정확히 추정할 수 있습니다. 흔한 값은 빈도를 직접 들고 있으니 추측할 필요가 없습니다.

histogram_bounds

histogram_bounds는 나머지 값의 분포를 담습니다 — MCV에 들지 못한 값들이 대상입니다. 공식 문서 정의는 "컬럼 값들을 거의 같은 개수의 그룹으로 나누는 경계값 목록"입니다. 즉 equi-depth histogram이라, 각 구간(bucket)이 데이터의 거의 같은 비율을 담습니다. 기본 타깃 100이면 경계가 약 101개 잡혀 구간마다 전체의 약 1%를 덮습니다.

중요한 성질이 두 가지 있습니다. 첫째, MCV에 들어간 값은 histogram 계산에서 빠집니다. 흔한 값은 MCV가 맡고 나머지는 histogram이 맡습니다. 둘째, 컬럼 타입에 < 연산자가 없거나 MCV가 전체를 다 덮으면 이 값은 NULL입니다.

WHERE signup_date < '2026-03-01' 같은 범위 조건의 추정이 여기서 나옵니다. 경계가 촘촘할수록 범위 추정이 정확해집니다.

correlation

correlation으로는 index scan이 쌀지 가늠합니다 — 물리적 행 순서와 논리적 값 순서가 얼마나 일치하는지를 -1에서 +1로 나타냅니다. 공식 문서는 "값이 -1이나 +1에 가까우면 그 컬럼의 index scan이 0에 가까울 때보다 싸게 추정됩니다. random access가 줄어들기 때문"이라고 설명합니다.

값이 디스크에 정렬된 순서로 쌓여 있으면(예: 시간순 append) correlation이 1에 가깝고, index로 범위를 긁어도 디스크를 거의 순차로 읽습니다. 값이 무작위로 흩어져 있으면 0에 가깝고, index scan은 매번 다른 블록으로 점프해야 해서 비싸집니다. 같은 인덱스라도 이 값에 따라 planner의 선택이 갈립니다.

planner가 통계로 row를 추정하는 예

추정의 뼈대 공식은 하나입니다.

추정 row = reltuples × selectivity

reltuples는 테이블 전체 행 수, selectivity는 조건이 걸러 내는 비율(0~1)입니다. selectivity를 어떻게 구하느냐가 통계가 쓰이는 지점입니다. 공식 문서의 Row Estimation Examples에 나온 tenk1(1만 행) 예시로 봅니다.

MCV에 있는 등치 조건

조건 값이 most_common_vals에 있으면 그 빈도를 그대로 selectivity로 씁니다.

SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA';

'CRAAAA'most_common_freqs가 0.003이면 selectivity는 0.003, 추정 row는 10000 × 0.003 = 30입니다.

MCV에 없는 등치 조건

조건 값이 MCV 목록에 없으면, MCV가 차지하지 않은 나머지를 남은 distinct 값들이 고르게 나눠 가진다고 가정합니다.

selectivity = (1 - sum(mcv_freqs)) / (n_distinct - num_mcv)
SELECT * FROM tenk1 WHERE stringu1 = 'xxx';

MCV 빈도 합이 0.03333, n_distinct가 676, MCV 개수가 10이면,

selectivity = (1 - 0.03333) / (676 - 10) = 0.0014559
추정 row = 10000 × 0.0014559 ≈ 15

n_distinct가 추정에 직접 들어가는 게 여기서 보입니다. 이 값이 틀리면 비-MCV 등치 조건이 통째로 빗나갑니다.

histogram을 사용하는 범위 조건

<, > 같은 범위 조건은 histogram_bounds로 값이 어느 구간에 떨어지는지를 보고 비율을 보간합니다.

SELECT * FROM tenk1 WHERE unique1 < 1000;

histogram 경계가 {0, 993, 1997, 3050, ...}(10구간)이고 1000이 993~1997 구간 안에 있다면,

selectivity = (1 + (1000 - 993) / (1997 - 993)) / 10 = 0.100697
추정 row = 10000 × 0.100697 ≈ 1007

독립을 가정하는 AND 결합

여러 조건이 AND로 묶이면 planner는 기본적으로 각 조건이 서로 독립이라 보고 selectivity를 곱합니다.

SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';
selectivity = 0.100697 × 0.0014559 = 0.0001466
추정 row = 10000 × 0.00014661

이 독립 가정이 다음 절의 함정으로 이어집니다. 추정값과 실제값은 EXPLAIN으로 바로 대볼 수 있습니다.

EXPLAIN ANALYZE
SELECT * FROM customers WHERE state = 'CA';
-- Seq Scan ... (rows=1740 ...) (actual ... rows=1736 ...)
-- ↑ 추정 ↑ 실제

추정(rows=)과 실제(actual ... rows=)가 크게 벌어지는 노드가 통계 문제의 출발점입니다.

통계가 틀어질 때의 운영 점검

default_statistics_target

pg_statistic에 담기는 MCV/histogram 배열의 최대 길이는 컬럼별 ALTER TABLE ... SET STATISTICS로, 또는 전역 default_statistics_target으로 정합니다. 공식 문서 기준 기본 한도는 100입니다. 값을 키우면 샘플이 늘고 배열이 길어져 분포가 불규칙한 컬럼에서 추정이 정밀해지지만, pg_statistic 공간과 ANALYZE 시간을 더 씁니다.

-- 특정 컬럼만 정밀하게
ALTER TABLE customers ALTER COLUMN signup_date SET STATISTICS 1000;
ANALYZE customers;

특정 컬럼의 단일 추정이 계속 빗나가면, 전역값을 올리기 전에 그 컬럼만 타깃을 올리는 게 비용 대비 효과가 좋습니다.

조정 대상효과비용
전역 default_statistics_target 상향모든 컬럼 추정 정밀ANALYZE 시간/공간 전반 증가
컬럼 SET STATISTICS 상향해당 컬럼만 정밀그 컬럼만 비용 증가
컬럼별 다중 통계상관된 컬럼 조합 추정ANALYZE 시 추가 계산

다중 컬럼 상관과 extended statistics

AND 결합의 독립 가정은 컬럼들이 실제로 상관돼 있으면 깨집니다. 시드 글의 예가 명확합니다. WHERE city = 'Cheyenne' AND state = 'WY'는 도시가 정해지면 주는 사실상 결정되는데, planner는 둘을 독립으로 보고 곱해 8 row로 추정합니다. 실제는 4012 row로, 약 500배 차이가 납니다.

PostgreSQL 10부터 도입된 extended statistics가 이 문제를 풉니다. CREATE STATISTICS로 관심 컬럼 조합을 등록하면 ANALYZE가 그 조합의 통계를 함께 모읍니다.

CREATE STATISTICS customers_city_state (dependencies, ndistinct)
ON city, state FROM customers;
ANALYZE customers;

이후 같은 쿼리의 추정은 4087 row로, 실제(4012)에 거의 붙습니다. 단, dependencies(함수 종속성)는 컬럼을 상수와 비교하는 단순 등치 조건과 상수 IN 절에만 적용되고, 두 컬럼끼리 비교하거나 범위/LIKE 조건에는 쓰이지 않는다는 한계가 있습니다. 한 가지 더, extended statistics도 일반 단일 컬럼 통계와 같은 샘플로 계산되므로, 통계 타깃을 올리면 extended statistics도 함께 정밀해집니다.

통계가 의심될 때의 점검 순서

추정과 실제가 벌어지는 노드를 찾았다면 순서대로 봅니다.

  1. EXPLAIN ANALYZE로 추정 row와 실제 row의 차이를 짚습니다.
  2. 문제 컬럼의 pg_stats에서 n_distinct와 MCV를 확인합니다.
  3. 통계가 낡았으면 ANALYZE를 먼저 실행합니다.
  4. 단일 컬럼 추정이 계속 빗나가면 그 컬럼의 SET STATISTICS를 올립니다.
  5. 다중 컬럼 상관이 원인이면 extended statistics를 만듭니다.
  6. 그래도 안 되면 그때 쿼리 재작성을 검토합니다.

대량 적재나 마이그레이션 직후, 파티션 추가 직후처럼 데이터가 급변한 시점에는 autovacuum의 ANALYZE를 기다리지 말고 직접 실행해 두는 습관이 추정 사고를 가장 많이 막아 줍니다.

정리

planner는 데이터를 보지 않고 통계를 봐요. ANALYZE가 무작위 샘플로 분포를 떠서 pg_statistic에 채우고, pg_stats가 그걸 읽기 좋게 펼쳐요. 등치 조건은 MCV의 빈도나 n_distinct로, 범위 조건은 histogram_bounds로, index scan 여부는 correlation으로 추정해요. 추정이 빗나가면 인덱스를 의심하기 전에 pg_stats를 먼저 열어 보고, 단일 컬럼이면 통계 타깃을, 상관된 컬럼이면 extended statistics를 손봐요. 이 흐름만 손에 익으면 "왜 이 계획이 나왔지"의 절반은 통계 한 군데에서 답이 나와요.


참고한 출처:

PostgreSQL 코어의 빈칸

· 약 10분

들어가며

PostgreSQL은 매년 약 200개의 기능과 변경을 더하지만, "이건 당연히 되겠지" 싶은 큰 기능 몇 개는 30년째 코어에 비어 있어요. sharding, connection pooling, 내장 암호화(TDE)처럼 상용 DB라면 체크리스트에 들어가는 항목들인데, PostgreSQL을 쓰다 보면 어느 순간 "여기까지 다 되는데 왜 이건 안 되지" 하는 벽을 만나요.

Bruce Momjian(브루스 모미잔)은 2026년 발표 《What's Missing in Postgres?》에서 이 빈칸들을 정면으로 다뤘습니다. 그가 발표 슬라이드를 쓰면서 깨달은 한 가지가 있습니다. 빠진 기능의 대다수는 기능(functionality)이 없어서가 아니라 성능(performance)을 위한 것이라는 점입니다. 즉 PostgreSQL은 "못 하는" 쪽이라기보다, "더 빨리 하기 위한 장치"가 아직 코어에 없는 쪽에 가깝습니다.

이 글은 그 빈칸을 항목별로 짚습니다. 각 항목마다 무엇이 없는지, 왜 코어에 없는지, 지금은 무엇으로 메우는지, 코어 편입 전망은 어떤지 차례로 봅니다. Momjian의 발표 분류를 따라 단일 호스트(single host) 성능 항목과 다중 호스트(multi-host) 항목으로 나눕니다.

한 장으로 보는 빈칸 지도

먼저 전체 그림을 표로 깔아둡니다.

빈칸분류지금 메우는 도구코어 편입 전망
내장 암호화 (TDE)단일 호스트pg_tde, EDB(상용)논의 단계
내장 connection pooler단일 호스트PgBouncer, Pgpool-II, Supavisor패치 제안 반복
optimizer hints단일 호스트pg_hint_planPostgreSQL 19 1차 도입
columnar storage단일 호스트Citus columnar, Hydra 등미정
global index단일 호스트없음(수동 우회)미정
direct I/O단일 호스트(PostgreSQL 18 AIO 기반)진행 중
server-side threading단일 호스트없음(프로세스 모델)장기 과제
64-bit transaction ID단일 호스트Postgres Pro(fork)장기 논의
sharding다중 호스트Citus, Multigres미정
multi-master replication다중 호스트pgEdge/Spock, BDR(상용)미정
Oracle RAC 동급다중 호스트없음사실상 없음
DDL의 logical replication다중 호스트일부 extension부분 진행

표를 위에서 아래로 훑으면 패턴이 보입니다. 빈칸 대부분에 "지금 메우는 도구"가 이미 하나씩 있다는 점입니다. PostgreSQL은 코어를 작게 유지하고 extension/외부 프로세스로 확장하는 철학을 30년간 지켜 왔고, 빈칸은 그 철학의 그림자이기도 합니다.

코어와 빈칸의 경계

PostgreSQL이 무엇을 코어에 두고 무엇을 바깥에 두는지, 영역을 그림으로 나눠 봅니다.

코어는 쿼리 처리, MVCC, streaming replication, declarative partition까지를 책임집니다. 그 위로 sharding, 암호화, hint 같은 빈칸은 extension이 메우고, connection pooling은 아예 별도 프로세스가 연결 앞단에서 받습니다. 이제 각 빈칸을 풀어씁니다.

빈칸 1. 내장 암호화

첫 번째 빈칸은 TDE입니다.

무엇이 없나

데이터 파일을 디스크에 암호화해 저장하는 Transparent Data Encryption(TDE)이 코어에 없습니다. 디스크나 백업 미디어를 통째로 탈취당해도 키 없이는 못 읽게 막는 data-at-rest 보호인데, Oracle/SQL Server는 오래전부터 내장한 기능입니다.

왜 코어에 없나

암호화 자체는 어렵지 않습니다. 어려운 건 키 관리와 성능, 그리고 WAL, 임시 파일, 통계까지 빠짐없이 덮는 일관성입니다. 코어에 넣으려면 KMS 연동 모델까지 표준화해야 하는데, 이 합의가 더딥니다. Momjian은 cluster file encryption을 단일 호스트 항목으로 분류하면서, 진행은 있되 코어 합의에는 이르지 못한 상태로 짚습니다.

지금은 무엇으로 메우나

Percona의 pg_tde extension이 2025년에 production 궤도에 올랐습니다. 2025년에 WAL 암호화가 GA에 도달했고, 2025년 11월에는 pg_tde 2.1이 릴리스되며 PostgreSQL 18.1과 asynchronous I/O까지 지원합니다. HashiCorp, Thales, Fortanix, OpenBao 같은 KMS 연동도 붙었습니다. pg_tde는 구독 뒤에 숨기지 않은 오픈소스라는 점을 내세웁니다. 한편 EDB도 TDE를 제공하지만, 이쪽은 EDB Postgres Advanced Server/Extended Server의 라이선스 제품에서만 쓸 수 있습니다.

pg_tde를 쓸 때 백업 도구와의 궁합은 따로 검증이 필요합니다. 이 주제는 암호화된 PostgreSQL은 pgBackRest로 백업될까 글에서 한 편 다뤘습니다.

코어 편입 전망

당장은 어렵습니다. pg_tde가 사실상의 오픈소스 표준 자리를 먼저 굳히는 중이고, 코어 편입은 그 다음 논의가 될 가능성이 큽니다.

빈칸 2. 내장 connection pooler

무엇이 없나

PostgreSQL은 17까지도 내장 connection pooler가 없습니다. PostgreSQL은 연결 하나당 프로세스 하나(process-per-connection) 모델이라, 연결 수가 늘면 메모리와 context switch 비용이 가파르게 오릅니다. 수천 개의 짧은 연결을 받는 웹 백엔드에서 특히 아픕니다.

왜 코어에 없나

프로세스 모델 자체가 발목을 잡습니다. 내장 pooler를 제대로 넣으려면 연결 처리 구조를 손봐야 하고, 이는 server-side threading 같은 더 깊은 과제와 얽힙니다. 패치 제안은 여러 번 올라왔지만 코어 합의까지 가지 못했습니다.

지금은 무엇으로 메우나

외부 프로세스가 연결 앞단에서 받습니다. 가장 널리 쓰이는 건 PgBouncer로, transaction/session 단위 풀링을 지원하는 경량 도구이자 사실상의 표준입니다. Pgpool-II는 풀링에 더해 load balancing/query routing까지 묶은 무거운 도구고, Supavisor는 Supabase가 Elixir로 만든 멀티테넌트 pooler로 클라우드 규모를 노립니다.

클라우드 사업자들은 이걸 관리형으로 흡수했습니다. Azure Database for PostgreSQL은 PgBouncer를 서버 단위 옵션으로 켤 수 있게 내장했습니다.

코어 편입 전망

논의는 살아 있습니다. 다만 process-per-connection 구조와 threading 과제가 함께 풀려야 본격적인 내장 pooler가 가능해집니다. 단기간에 PgBouncer를 대체할 그림은 아닙니다.

빈칸 3. optimizer hints

무엇이 없나

쿼리 planner에게 "이 인덱스를 써라", "이 조인 순서로 가라" 식으로 강제하는 optimizer hint가 코어에 없었습니다. Oracle 사용자가 PostgreSQL로 옮길 때 가장 먼저 당황하는 지점 중 하나입니다.

왜 코어에 없나

PostgreSQL 커뮤니티는 hint를 의도적으로 거부해 왔습니다. planner가 통계로 최적해를 찾게 두는 편이 장기적으로 낫고, hint는 잘못된 플랜을 영구히 고착시키는 부채가 된다는 철학입니다. "hint가 필요하면 그건 planner나 통계를 고칠 신호"라는 입장이 오래 유지됐습니다.

지금은 무엇으로 메우나

pg_hint_plan extension이 그 자리를 메워 왔습니다. 주석 형태로 hint를 심어 planner 동작을 강제합니다.

코어 편입 전망

여기서 흐름이 바뀌었습니다. optimizer hint의 1차 형태가 PostgreSQL 19에 들어옵니다. 오래 거부하던 기능이 코어에 발을 들이는 사례라, 빈칸 목록에서 가장 먼저 지워질 항목입니다.

빈칸 4. 단일 호스트의 나머지 성능 항목

Momjian이 단일 호스트 묶음으로 짚은 나머지를 한 번에 정리합니다. 이들의 공통점은 기능 자체의 결핍이라기보다 더 빠르게 하기 위한 장치라는 점입니다.

columnar storage는 분석 워크로드용 열 지향 저장인데, 코어에 없고 Citus의 columnar나 Hydra 같은 extension이 메웁니다. 코어 작업은 아직 널리 알려진 움직임이 없습니다. global index는 partition 테이블 전체를 가로지르는 인덱스로, 여러 partition에 걸친 unique 보장을 한 인덱스로 처리하려는 것인데 지금은 코어에 없어 수동 우회에 의존합니다. direct I/O는 OS 페이지 캐시를 우회하는 I/O로, PostgreSQL 18이 asynchronous I/O(AIO) 서브시스템을 들이며 기반이 깔렸고 그 위에서 진행 중입니다. server-side threading은 프로세스 모델을 thread 모델로 바꾸는 장기 과제라 connection pooler 빈칸과 뿌리가 같습니다.

64-bit transaction ID는 조금 더 설명이 필요합니다. 32-bit XID는 wraparound 위험을 안고 삽니다. PostgreSQL은 epoch을 포함한 xid8 타입을 이미 갖췄지만, 내부 XID를 통째로 64-bit로 넓히는 작업은 on-disk 호환성 때문에 코어에 못 들어왔습니다. Postgres Pro fork는 내부 64-bit XID를 상용으로 돌리고 있어, 가능은 하되 코어 편입의 벽이 높다는 걸 보여줍니다.

빈칸 5. sharding

무엇이 없나

데이터를 여러 노드에 수평 분산하는 sharding이 코어에 없습니다. 단일 서버 용량을 넘어서는 순간 부딪히는 벽입니다. MySQL 진영은 Vitess라는 검증된 sharding 시스템을 오래 가졌지만, PostgreSQL은 비교 대상이 없었습니다.

왜 코어에 없나

sharding은 distributed transaction, 분산 plan, 노드 간 일관성까지 묶인 거대한 과제입니다. PostgreSQL은 declarative partition으로 단일 노드 안의 분할까지는 코어에 들였지만, 노드를 가로지르는 분산은 extension/미들웨어의 몫으로 남겨 뒀습니다.

지금은 무엇으로 메우나

Citus는 분산 PostgreSQL extension입니다. Microsoft가 인수해 Azure로 들어갔고, Azure의 Elastic Clusters가 이 오픈소스 기술 위에서 row/schema 단위 sharding을 제공합니다. Citus 14는 PostgreSQL 18을 지원합니다. Multigres는 2025년 6월 Supabase가 Vitess 공동 창시자 Sugu(수구)를 영입해 시작한 "PostgreSQL용 Vitess"입니다. PostgreSQL 앞단에 놓이는 proxy로 표준 PostgreSQL 호환을 최우선에 두며, Vitess와 같은 Apache 2.0 라이선스 오픈소스입니다.

Multigres는 분량이 커서 PostgreSQL에도 Vitess가 온다 글에서 따로 다뤘습니다.

코어 편입 전망

가까운 시일에는 어렵습니다. 코어가 분산 트랜잭션까지 흡수하기보다, Citus/Multigres 같은 미들웨어가 각자 자리를 잡는 그림이 현실적입니다.

빈칸 6. 다중 호스트의 나머지 항목

다중 호스트 묶음의 나머지를 정리합니다. multi-master replication은 여러 노드가 동시에 write를 받는 구성인데, 코어의 replication은 단일 primary 기준입니다. pgEdge의 Spock이 multi-master logical replication을 제공해 지리적으로 분산된 배치에 쓰이고, EDB의 BDR이 상용으로 그 자리를 채웁니다. Oracle RAC 동급은 공유 스토리지 위에서 여러 인스턴스가 같은 DB를 동시에 여는 RAC 모델을 말하는데, PostgreSQL에는 이에 직접 대응하는 코어 기능도, 널리 쓰이는 대체재도 사실상 없습니다. 빈칸 중 가장 비어 있는 자리입니다. DDL의 logical replication은 결이 조금 다릅니다. logical replication이 DML은 나르지만 CREATE TABLE 같은 DDL은 자동으로 나르지 못하는데, PostgreSQL 19에서 sequence 복제 같은 주변부가 채워지며 부분적으로 전진하고 있습니다.

왜 비어 있는가

빈칸들을 한 발 떨어져 보면 공통된 이유가 보입니다. 첫째는 철학입니다. 코어를 작게 두고 extension/외부 프로세스로 확장하는 노선인데, optimizer hint를 오래 거부한 것이 대표적입니다. 둘째는 구조입니다. process-per-connection 모델이 connection pooler와 threading을 동시에 막고, on-disk 포맷 호환성이 64-bit XID를 막습니다. 셋째는 합의 비용입니다. TDE의 키 관리나 sharding의 분산 트랜잭션처럼 표준화 합의가 비싼 과제는 코어 진입이 더딥니다.

그리고 Momjian의 결론처럼, 이 빈칸들은 대부분 "PostgreSQL이 못 하는 일"이라기보다 "더 빠르게/더 크게 하기 위한 장치"에 가깝습니다. 기능의 결핍보다는 성능과 규모의 천장에 걸리는 문제라, 대부분의 빈칸 옆에는 이미 그 천장을 뚫는 도구가 하나씩 서 있습니다.

닫으며

PostgreSQL의 빈칸 목록은 약점 목록이라기보다 지도에 가깝습니다. 어디까지가 코어이고 어디부터 extension/미들웨어의 영역인지, 그리고 다음 5년 동안 어느 칸이 먼저 채워질지를 보여줍니다. optimizer hint가 PostgreSQL 19에서 코어로 들어오는 것처럼, 빈칸은 고정된 게 아니라 천천히 메워집니다.

DBA 입장에서 실무적으로 남는 건 단순해요. 벽에 부딪히기 전에 어느 칸이 비어 있는지 미리 알아 두면 돼요. sharding이 필요하면 Citus나 Multigres를 일찍 검토하고, 연결 폭증이 보이면 PgBouncer를 처음부터 설계에 넣고, 규제 요건이 있으면 pg_tde를 미리 검증하면 돼요. 빈칸은 막다른 길이라기보다 무엇을 곁들여야 하는지 알려주는 표지판에 가깝습니다.

1차 출처

Momjian 발표:

암호화(TDE):

connection pooling:

optimizer hints:

sharding / multi-host:

64-bit XID:

PG19 논리 복제 시퀀스

· 약 8분

PostgreSQL 19부터 logical replication이 sequence 값을 subscriber로 동기화해요. 18까지는 테이블 데이터만 넘어가고 SERIAL/IDENTITY 뒤에 붙은 sequence는 subscriber에서 초기값에 그대로 멈춰 있어 promote 직후 첫 INSERT가 duplicate key로 터지는 사고가 흔했지만, 19는 publication에 ALL SEQUENCES 절을 들이고 ALTER SUBSCRIPTION ... REFRESH SEQUENCES 명령을 더해 이 오래된 구멍을 메웠어요.

이 글은 pgEdge의 Shaun Thomas(숀 토머스)가 쓴 "Looking Forward to Postgres 19: Logically Sequenced"를 한국어로 풀고, PostgreSQL 19 릴리스 노트와 실제 동작 데모로 사실을 교차검증합니다. DBA 관점에서 "failover 때 왜 사고가 났나", "19에서 무엇이 정확히 달라지고 무엇은 여전히 그대로인가"를 함께 봅니다.

sequence는 왜 그동안 복제 대상이 아니었나

sequence가 logical replication에서 빠져 있던 데에는 구조적인 문제가 있었습니다. logical decoding은 WAL을 트랜잭션 단위로 재조립해 commit 순서대로 replay합니다. 철저히 트랜잭션 기반입니다.

그런데 sequence는 트랜잭션과 어울리지 않습니다. nextval()로 뽑은 값은 트랜잭션을 rollback해도 되돌아오지 않습니다. 한 번 소비된 번호는 영구히 사라집니다. 이 비트랜잭션 동작과 commit 순서 기반 decoding을 깔끔하게 화해시키기가 어려웠습니다.

실제로 Tomas Vondra(토마스 본드라)가 만든 "logical decoding of sequences" 패치가 PostgreSQL 16에 한 번 들어갔다가, 트랜잭션과 비트랜잭션 동작을 조율하는 난점 때문에 되돌려진 적이 있습니다. 그만큼 묵은 숙제였습니다. (pgEdge)

결과적으로 18 이하의 logical replication에서 sequence는 공식 문서의 제약 사항에 명시된 복제 제외 대상이었습니다. 테이블 row는 넘어가지만, 그 row의 id를 발급하는 sequence는 subscriber에서 따로 놀았습니다.

failover 때 무슨 사고가 났나

운영 입장에서 이게 왜 문제였는지는 cutover 시나리오로 보면 분명합니다. zero-downtime upgrade나 마이그레이션에서 흔한 흐름입니다.

  1. publisher(구 primary)에서 subscriber(신 primary)로 logical replication을 겁니다.
  2. 테이블 데이터는 잘 넘어옵니다. row가 수백만 건 들어와 있고, id 컬럼은 publisher에서처럼 큰 값까지 차 있습니다.
  3. cutover 시점에 subscriber를 promote하고 애플리케이션을 붙입니다.
  4. 첫 INSERT가 떨어지는 순간 duplicate key value violates unique constraint로 터집니다.

원인은 단순합니다. 테이블의 id는 큰 값까지 차 있는데, 그 id를 발급하는 sequence는 subscriber에서 초기값(보통 1)에 그대로 멈춰 있었습니다. sequence가 복제 대상이 아니었으니 당연한 결과입니다. nextval()이 1을 돌려주는데, 그 자리는 이미 넘어온 데이터가 차지하고 있습니다.

그동안의 회피책은 cutover 직전에 손으로 sequence를 끌어올리는 것이었습니다.

-- 옛날 방식: 각 sequence를 publisher 값보다 높게 수동으로 밀어 올림
SELECT setval('public.orders_id_seq', 5000000 + 1000);

+ 1000 같은 인위적인 여유분은 cutover 도중 publisher에 추가로 들어올 write를 흡수하려는 안전 마진입니다. sequence가 수십/수백 개면 이걸 전부 스크립트로 긁어 돌려야 했고, 마진을 잘못 잡으면 번호가 비거나 충돌했습니다. failover runbook에서 늘 신경 쓰이던 자리였습니다.

PostgreSQL 19가 푸는 방식

19는 이 작업을 logical replication 안으로 들였습니다. 릴리스 노트는 이렇게 적습니다.

Allow sequence values stored in subscribers to match the publisher (Vignesh C) — PostgreSQL 19 Release Notes

핵심은 세 조각입니다.

  • publication에 ALL SEQUENCES 절이 생겼습니다.
  • subscriber가 sequence 값을 publisher에서 당겨오는 시점이 세 개 정해졌습니다.
  • 백그라운드에서 sequence를 배치로 당겨오는 sequencesync worker가 추가됐습니다.

publication의 ALL SEQUENCES

publication이 sequence를 포함할 수 있습니다.

-- 테이블과 sequence를 함께 발행
CREATE PUBLICATION migration_pub FOR ALL TABLES, ALL SEQUENCES;

-- sequence만 발행도 가능
CREATE PUBLICATION pubseq FOR ALL SEQUENCES;

ALL SEQUENCESALL TABLES와 함께 묶을 수 있습니다. 다만 TABLE이나 TABLES IN SCHEMA 같은 세밀한 옵션과는 함께 쓸 수 없습니다. (dbi-services)

여기서 첫 제약이 나옵니다. sequence는 개별 선택(cherry-pick)이 안 됩니다. 테이블처럼 "이 sequence만 publication에 넣겠다"가 불가능하고, 전부(ALL SEQUENCES) 아니면 전무입니다.

subscription의 동기화 시점

subscription 쪽은 평소처럼 만듭니다.

CREATE SUBSCRIPTION migration_sub
CONNECTION 'host=oldprimary dbname=app user=repl'
PUBLICATION migration_pub;

sequence 값이 subscriber로 당겨지는 시점은 정확히 셋입니다. (릴리스 노트)

-- (1) CREATE SUBSCRIPTION — 최초 1회 당겨옴

-- (2) sequence 존재 여부 + 값을 publication에 맞춰 재조정
ALTER SUBSCRIPTION migration_sub REFRESH PUBLICATION;

-- (3) 값만 갱신 (membership 은 건드리지 않음)
ALTER SUBSCRIPTION migration_sub REFRESH SEQUENCES;

REFRESH PUBLICATION은 publication에서 sequence가 추가/제거된 것을 반영하면서 값도 맞춥니다. REFRESH SEQUENCES는 membership은 그대로 두고 값만 다시 당겨옵니다. 내부적으로는 pg_subscription_rel의 모든 sequence를 INIT 상태로 되돌린 뒤 sequencesync worker가 다시 채웁니다.

연속 동기화가 아닌 시점 동기화

여기서 DBA가 반드시 머리에 둬야 할 한 가지가 있습니다. sequence 동기화는 연속이 아니라 시점 동기화입니다.

테이블 row처럼 publisher의 변경이 실시간으로 흐르지 않습니다. 위의 세 시점에만 값을 당겨오고, 그 직후부터 publisher가 새 번호를 발급하는 순간 subscriber의 값은 곧바로 낡은(stale) 값이 됩니다.

dbi-services의 데모가 이를 그대로 보여줍니다. publisher에서 sequence의 last_value가 3까지 올라가도 subscriber는 2에 머뭅니다. ALTER SUBSCRIPTION ... REFRESH SEQUENCES를 실행해야 비로소 3으로 맞춰집니다. 그 뒤 publisher에 다시 row를 넣어 sequence가 6까지 올라가도, subscriber는 또 멈춰 있고 다시 한번 refresh를 실행해야 따라옵니다. (dbi-services)

그래서 운영 원칙은 명확합니다. REFRESH SEQUENCES는 promote 직전에 실행합니다. 미리 돌려두면 그 사이 publisher가 발급한 번호만큼 다시 어긋납니다. cutover runbook에서 sequence refresh는 publisher write를 멈추고 promote로 넘어가기 바로 전 칸에 들어가야 합니다. (pgEdge)

동기화 흐름

cutover 시점의 sequence 동기화를 단계로 그리면 이렇습니다.

worker는 INIT으로 표시된 sequence를 모아 publisher에서 현재 값과 page LSN을 가져와 subscriber에 쓰고, 끝나면 해당 항목을 READY로 바꿉니다. 배치로 처리해 빠릅니다. depesz의 테스트에서는 sequence 1만 개 동기화가 약 1초, 100ms당 약 1,200개 속도였습니다. (depesz)

상태와 모니터링

sequence도 테이블처럼 pg_subscription_rel에서 상태를 가집니다. 동기화 전에는 INIT(i), 끝나면 READY(r)입니다.

SELECT c.relname, r.srsubstate, r.srsublsn
FROM pg_subscription AS s
JOIN pg_subscription_rel AS r ON s.oid = r.srsubid
JOIN pg_class AS c ON r.srrelid = c.oid;
relname | srsubstate | srsublsn
--------------+------------+------------
orders_id_seq| r | 0/04004780

srsubstater이면 그 sequence는 READY 상태로 한 번 동기화를 마쳤다는 뜻입니다. (dbi-services)

값 자체를 확인할 때는 새로 들어온 pg_get_sequence_data() 함수를 씁니다.

SELECT last_value FROM pg_get_sequence_data('public.orders_id_seq');

오류 카운트도 새로 추적됩니다. pg_stat_subscription_stats 뷰에 sync_seq_error_count 컬럼이 추가됐고, 기존 sync_error_countsync_table_error_count로 이름이 바뀌었습니다. sequence 오류가 별도로 집계되기 때문입니다. 모니터링 대시보드가 sync_error_count를 직접 참조하고 있었다면 19 업그레이드 때 컬럼명을 손봐야 합니다. (릴리스 노트)

PostgreSQL 18 이하 vs 19

PG18 이하PG19
logical replication의 sequence 처리복제 제외동기화 지원
failover 후 첫 INSERTduplicate key 위험refresh 후 정상
sequence 값 맞추기setval 수동 스크립트REFRESH SEQUENCES 명령
publication 포함 방법ALL SEQUENCES
동기화 방식시점 동기화 (3개 시점)
개별 sequence 선택불가 (전체만)
오류 추적sync_seq_error_count

운영 점검 포인트

19로 올려 sequence 동기화를 쓰기 전에 한 번씩 확인할 자리들입니다.

cutover runbook에서는 REFRESH SEQUENCES를 publisher write 중단 후, promote 직전 칸에 둡니다. 미리 돌리면 그 사이 발급된 번호만큼 다시 어긋납니다.

publication을 설계할 때는 sequence를 개별 선택할 수 없다는 점을 고려합니다. 일부 sequence만 복제하려는 설계가 있었다면 ALL SEQUENCES 전체 발행 전제로 다시 봐야 합니다.

stale 값도 인지해야 합니다. 동기화는 연속으로 이루어지지 않으므로, 평상시 replication이 도는 동안에도 subscriber의 sequence가 따라가지 않는다는 사실을 모니터링 기준에 반영합니다.

모니터링 컬럼명도 확인합니다. pg_stat_subscription_statssync_error_count를 직접 참조하던 쿼리/알람은 sync_table_error_count로 바꾸고, sync_seq_error_count도 함께 봅니다.

정리

PostgreSQL 19의 sequence 동기화는 화려한 신기능이라기보다 logical replication에 오래 남아 있던 구멍을 메운 변화입니다. failover 직후 첫 INSERT가 duplicate key로 터지던 사고, 그리고 이를 막으려고 setval 마진 스크립트를 돌리던 수작업이 REFRESH SEQUENCES 한 줄로 정리됐습니다.

다만 연속 동기화가 아니라는 점이 이 기능의 성격을 결정하며, 평상시 흐르는 게 아니라 cutover 시점에 한 번 맞추는 도구예요. 그래서 진짜 가치는 zero-downtime upgrade와 마이그레이션 cutover에서 나와요. runbook의 정해진 칸에 한 줄을 넣고 promote 직전에 실행하면, sequence가 더 이상 사고의 출처가 아니게 돼요.

참고 자료

Supabase Multigres

· 약 6분

2026-06-04, Supabase(수파베이스)가 시리즈 F로 5억 달러를 유치해 105억 달러 밸류를 찍었어요. 같은 날 PostgreSQL 수평 확장 오픈소스 레이어 Multigres를 프리뷰로 공개했어요. 한 줄로 줄이면, MySQL 진영에서 sharding 표준이 된 Vitess를 그 공동 창시자가 PostgreSQL로 옮겨 오는 일이에요.

Multigres는 PostgreSQL 앞단에 붙는 proxy 레이어입니다. 단일 인스턴스 한계에 부딪힌 팀이 다른 데이터베이스로 마이그레이션하지 않고도, 익숙한 PostgreSQL 생태계를 그대로 둔 채 sharding까지 늘려 가도록 하는 것이 목표입니다.

5억 달러와 Multigres, 한 묶음의 발표

사실관계부터 항목별로 짚어 봅니다. 1차 출처는 PR Newswire 보도자료Supabase 공식 블로그입니다.

항목내용
라운드시리즈 F
유치액5억 달러
밸류105억 달러(post-money)
리드 투자자GIC
기존 투자자Accel, Y Combinator, Craft, Felicis, Peak XV, Coatue
신규/추가Stripe(추가 투자), Salesforce Ventures(신규)
누적 유치10억 달러 이상
시점시리즈 E 7개월 만

회사가 밝힌 성장 수치도 같이 나왔습니다. CEO Paul Copplestone는 시리즈 E 이후 사용자 기반이 두 배 넘게 늘었고, 데이터베이스 수가 전년 대비 600% 증가했다고 했습니다. 고객 25만 곳 이상, 개발자 900만 명 규모입니다.

자금 발표와 한 묶음으로 나온 것이 Multigres입니다. Apache 2.0 라이선스로 공개됐고, 지금은 안정화에 집중하는 프리뷰 단계라 외부 기여는 아직 열지 않았습니다. 파트너 프로그램 신청만 받습니다.

Vitess를 PostgreSQL로 옮긴 Multigres

Multigres를 한마디로 줄이면 "Vitess for Postgres"입니다. 이 문구가 비유에 그치지 않는 이유는, 프로젝트를 이끄는 사람이 Vitess 공동 창시자 Sugu Sougoumarane(수구 수구마라네)이기 때문입니다.

Vitess는 YouTube가 MySQL을 페타바이트 규모로 굴리려고 만든 sharding 미들웨어입니다. 이후 CNCF 졸업 프로젝트가 됐고, PlanetScale 같은 서비스의 바탕이 됐습니다. MySQL 진영에서 "단일 인스턴스를 넘어선다"는 문제의 사실상 표준 답입니다. Sugu는 "한동안 Vitess를 PostgreSQL로 적응시키는 걸 고민해 왔다"며, 그 적응판으로 Multigres를 내놨습니다.

아키텍처는 Vitess의 2단 proxy 구조를 그대로 가져옵니다. Vitess의 vtgate/vttablet에 대응하는 두 컴포넌트가 있습니다.

  • MultiGateway: 분산 클러스터를 애플리케이션에서 단일 PostgreSQL 서버처럼 보이도록 묶습니다. query routing, cross-shard 쿼리의 scatter-gather, 장애 차단을 담당합니다.
  • MultiPooler: 개별 PostgreSQL 인스턴스 옆에 붙어 connection pooling과 조율을 맡습니다.

여기에 분산 조율을 위한 etcd, 컴포넌트 간 통신을 위한 gRPC가 붙어 Kubernetes 배포를 전제한 cloud-native 구성을 이룹니다.

흥미로운 건 PostgreSQL이 MySQL보다 sharding 미들웨어를 얹기에 유리한 지점이 있다는 점입니다. Sugu는 BigGo 인터뷰에서 PostgreSQL의 transactional DDL을 두고 "transactionless DDL을 다루는 게 MySQL에서 얼마나 악몽이었는지 모릅니다. transactional DDL은 Vitess에 꿈같은 일"이라고 했습니다. two-phase commit API도 PostgreSQL 쪽이 더 깔끔하다고 평가했습니다.

반대로 PostgreSQL이라서 까다로운 지점도 있습니다. pgvector, PostGIS 같은 extension이 커스텀 타입과 index를 들고 오는데, sharding 레이어 입장에서는 여기서 PostgreSQL만의 호환성 문제가 생깁니다. proxy가 모든 쿼리를 가로채 분배해야 하는데, extension이 만든 비표준 동작까지 이해해야 하기 때문입니다. Supabase가 호환성을 최우선 과제로 못 박은 배경입니다.

Citus와 갈리는 지점, extension이냐 proxy냐

PostgreSQL 수평 확장이라면 이미 Citus가 있습니다. Microsoft가 인수해 Azure Cosmos DB for PostgreSQL의 바탕이 됐고, 오픈소스로도 쓸 수 있습니다. 그렇다면 Multigres는 왜 또 만드는지 살펴볼 필요가 있습니다. 가장 큰 차이는 sharding을 어느 층에서 구현하느냐입니다.

Citus는 extension 방식입니다. PostgreSQL 안에 들어가 planner와 executor에 후크를 걸어 분산 쿼리를 처리합니다. Multigres는 proxy 방식입니다. PostgreSQL 바깥에 별도 레이어로 서서 쿼리를 가로채 shard로 분배합니다. 이 한 줄 차이가 운영 성격을 갈라놓습니다.

항목CitusMultigres
방식extension(엔진 내부)proxy(엔진 외부)
PostgreSQL 본체패치된 빌드/extension 필요표준 PostgreSQL 그대로
모태자체 설계Vitess 아키텍처 적응
버전 추종extension이 엔진 버전에 종속표준 인스턴스라 비교적 독립
성숙도프로덕션 다년 검증프리뷰(2026-06 공개)
라이선스AGPL 계열Apache 2.0
운영 단위coordinator + worker 노드MultiGateway + MultiPooler + shard

extension 방식은 PostgreSQL과 한 몸이라 쿼리 최적화가 깊게 들어가는 대신, 엔진 버전/빌드에 종속됩니다. proxy 방식은 각 shard가 손대지 않은 표준 PostgreSQL이라 extension/도구 생태계를 그대로 쓰고 버전 정책도 비교적 자유로운 대신, cross-shard 쿼리는 바깥 레이어가 풀어야 하니 그 영리함에 성패가 갈립니다. Vitess가 MySQL에서 이 방식으로 검증된 길을 닦았다는 게 Multigres가 기대를 받는 이유입니다.

지금은 방향만 읽어 두면 된다

결론부터 말하면 지금 당장 프로덕션에 올릴 물건은 아닙니다. 프리뷰이고 외부 기여조차 닫혀 있습니다. 하지만 방향만큼은 DBA가 지금 읽어 둘 가치가 있습니다.

첫째, sharding 레이어가 생긴다는 건 "PostgreSQL을 떠나지 않아도 된다"는 선택지가 늘어난다는 뜻입니다. 지금까지 단일 인스턴스 한계에 부딪힌 팀은 선택이 거칠었습니다. read replica로 읽기를 분산하거나, 애플리케이션 단에서 직접 shard를 쪼개거나, 아예 다른 분산 데이터베이스로 마이그레이션하거나. Multigres가 노리는 건 그 사이의 빈칸입니다. connection pooling부터 시작해 high availability를 거쳐 sharding까지, 같은 레이어 위에서 단계적으로 올라가는 on-ramp를 약속합니다.

둘째, proxy 방식은 운영 토폴로지가 한 겹 늘어난다는 뜻이기도 합니다. MultiGateway, MultiPooler, etcd가 새로 생기고, 각각이 장애 지점이자 모니터링 대상이 됩니다. Vitess를 운영해 본 팀이라면 익숙한 그림이지만, "PostgreSQL 한 대 + replica" 수준으로 운영해 온 팀에는 완전히 다른 운영 부담입니다. sharding이 공짜가 아니라는 건 어느 진영에서나 똑같습니다. shard key 설계를 잘못하면 hot shard가 생기고, cross-shard 조인은 여전히 비쌉니다.

셋째, 자금력과 맥락을 짚어 둘 필요가 있습니다. Supabase는 이번 라운드를 "agentic 인프라"라는 키워드로 포장했습니다. agent가 데이터베이스를 대량으로 생성/소비하는 패턴이 늘면 단일 인스턴스로는 감당이 안 되고, 그래서 수평 확장이 필요하다는 서사입니다. 같은 흐름은 Neon이 백엔드 플랫폼으로 확장한 발표에서도 읽힙니다. PostgreSQL 회사들이 5억~10억 달러 단위 자금을 들고 "AI 시대의 데이터베이스 기반"을 두고 경쟁하는 국면이고, Multigres는 그 경쟁에서 Supabase가 꺼낸 장기 베팅입니다. 5억 달러는 8년 넘게 걸릴 수도 있는 sharding 레이어를 끝까지 밀어붙일 실탄입니다.

언제 쓸 만해질지 따져 보면, 현실적으로는 Multigres가 안정화를 끝내고 외부 기여를 열어 누군가의 프로덕션에서 cross-shard 쿼리와 zero-downtime 마이그레이션이 실제로 검증되는 시점이며, 그때까지는 Vitess가 MySQL에서 걸어온 길이 PostgreSQL에서도 재현될지 지켜보는 단계예요. 다만 "PostgreSQL은 수평 확장이 약하다"는 오래된 명제에 대형 자본과 검증된 설계자가 정면으로 답을 내기 시작한 것은 분명해요.

참고

Azure HorizonDB 공개 프리뷰

· 약 6분

Microsoft가 Build 2026에서 Azure HorizonDB를 공개 프리뷰로 내놓았어요. compute와 storage를 분리한 scale-out 구조 위에 벡터 검색과 in-database 모델 호출을 얹은 PostgreSQL 호환 관리형 데이터베이스예요.

작년 12월 Ignite에서 early preview로 처음 모습을 비친 뒤, 2026-06-03 Build 발표로 공개 프리뷰에 들어갔습니다. 같은 시기 AWS는 Aurora DSQL을 정식 출시했고, Google은 AlloyDB를 계속 키우는 중입니다. 클라우드 3사가 약속이라도 한 듯 "PostgreSQL 호환"을 새로 짓고 있습니다.

발표를 사실 단위로 끊어 보면

발표를 사실 단위로 끊어 정리합니다. 1차 출처는 Microsoft 공식 블로그Azure 블로그의 Build 2026 데이터베이스 발표입니다.

  • HorizonDB는 PostgreSQL 호환 관리형 데이터베이스입니다. 기존 Azure Database for PostgreSQL과 별개의 새 서비스로 출발했습니다.
  • compute와 storage를 분리하고, 공유 storage 위에서 scale-out하는 구조입니다. Microsoft는 "compute와 storage를 완전히 분리했다"고 적었습니다.
  • scale-out compute는 primary와 replica 노드를 합쳐 최대 3,072 vCore까지, auto-scaling 공유 storage는 최대 128TB까지 지원한다고 밝혔습니다.
  • 다중 zone에 걸쳐 sub-millisecond commit latency를 낸다고 주장합니다.
  • 트랜잭션 워크로드 기준 오픈소스 PostgreSQL 대비 최대 3배 throughput을 주장합니다. Microsoft가 든 예시는 self-managed 4,200 TPS 대비 HorizonDB 11,000+ TPS입니다.
  • AI 쪽으로는 DiskANN 기반 벡터 검색, pgvector, pg_textsearch를 묶은 hybrid search, 그리고 SQL에서 모델을 직접 호출하는 azure_ai extension을 얹었습니다.

공개 프리뷰는 일부 region에서 먼저 열렸습니다. Australia East, Central US, Sweden Central, West US 2, West US 3에서 시작하고, East US, Canada Central, Japan East, Korea Central 등은 곧 추가된다고 밝혔습니다. AI 모델 관리 기능은 별도로 limited preview 단계입니다.

storage 계층을 새로 짠 아키텍처

HorizonDB가 내세우는 핵심은 storage 계층 재설계입니다. Microsoft는 이를 "database-as-logs" 설계라고 부르는데, 트랜잭션을 공유 WAL storage에 직접 commit하는 방식입니다. compute 노드는 상태를 로컬에 묶어두지 않고, 영속성은 분리된 공유 storage가 책임집니다.

이렇게 분리하면 운영 관점에서 두 가지가 달라집니다. 첫째, write latency를 낮추고 commit을 다중 zone에 걸쳐 처리하면서도 sub-millisecond를 노립니다. 둘째, failover가 예측 가능해집니다. compute 노드가 죽어도 데이터는 공유 storage에 남아 있으므로, 새 노드가 같은 storage를 붙잡으면 됩니다. 데이터를 통째로 옮기는 과정이 빠지기 때문입니다.

PostgreSQL 호환의 의미도 짚어야 합니다. HorizonDB는 PostgreSQL 엔진 위에서 storage 계층을 갈아끼운 형태에 가깝습니다. planner, executor, SQL 문법, extension 생태계 같은 PostgreSQL 상단은 그대로 두고, heap과 WAL이 디스크에 닿는 아래쪽을 클라우드 native storage로 바꾼 구조입니다. 그래서 기존 드라이버, ORM, extension을 그대로 쓰면서도, 단일 인스턴스 PostgreSQL의 storage 한계를 우회하려 합니다.

AI 기능 중 눈에 띄는 건 azure_ai extension입니다. 모델 추론을 PostgreSQL 엔진 안으로 끌어들여 SQL에서 함수로 호출하게 합니다. 임베딩 생성이나 추론을 위해 애플리케이션이 외부 모델 서비스로 따로 나갔다 오는 orchestration 계층을 줄이려는 의도입니다. 벡터 검색은 Microsoft의 DiskANN을 써서 메모리와 디스크를 함께 활용하고, 필터를 건 그래프 탐색도 빠르게 처리한다고 주장합니다.

Aurora DSQL/AlloyDB와의 구도

"PostgreSQL 호환을 새로 짓는다"는 한 문장으로 묶이지만, 세 제품이 노리는 지점은 조금씩 다릅니다. 그대로 같은 칸에 놓고 비교하면 오해가 생깁니다.

항목Azure HorizonDBAWS Aurora DSQLGoogle AlloyDB
상태공개 프리뷰(2026-06)정식 출시(2025-06 GA)정식 출시
호환 기준PostgreSQL 호환PostgreSQL 16 호환PostgreSQL 완전 호환(18 GA)
구조compute/storage 분리 scale-out컴포넌트 분리 active-active 분산 SQLcompute/storage 분리(log 기반)
강점sub-ms commit, AI 내장무한 확장, 다중 region 쓰기분석 columnar engine, HTAP
호환성 제약프리뷰, 검증 진행 중foreign key/일부 타입 미지원, 재시도 로직 필요표준에 가까움
주 경쟁 상대단일 PG 한계를 넘는 관리형 PG분산 SQL(Spanner/CockroachDB)Aurora/관리형 PG

Aurora DSQL은 결이 가장 다릅니다. query processor, adjudicator, journal 같은 컴포넌트를 쪼개 독립 확장하는 active-active 분산 SQL이고, sharding 없이 읽기/쓰기를 따로 확장한다고 내세웁니다. 대신 10,000 row 트랜잭션 제한, foreign key/일부 타입 미지원, optimistic concurrency에 따른 애플리케이션 재시도 로직 같은 제약이 따라붙습니다. 분류상 Spanner/CockroachDB 쪽 분산 SQL과 경쟁합니다.

AlloyDB는 Aurora와 같은 칸에 있는, compute/storage를 분리한 관리형 PostgreSQL입니다. 가장 큰 차별점은 columnar engine입니다. 분석 쿼리에서 오픈소스 PostgreSQL 대비 최대 100배 빠르다고 주장하며 HTAP를 노립니다. PostgreSQL 18도 이미 GA에 올렸습니다.

HorizonDB는 이 둘 사이 어딘가에 있습니다. Aurora DSQL처럼 호환성을 깨면서까지 분산 SQL로 가지 않고, AlloyDB처럼 compute/storage 분리에 머무르되, 차별점을 sub-millisecond commit과 AI 내장 쪽으로 잡았습니다. 정리하면 셋 다 "단일 인스턴스 PostgreSQL의 storage/확장 한계"라는 같은 문제를 풀되, AWS는 분산 일관성, Google은 분석, Microsoft는 AI 워크로드라는 서로 다른 입구로 들어간 셈입니다.

기존 Azure Database for PostgreSQL과 무엇이 다른가

이미 Azure Database for PostgreSQL flexible server를 쓰고 있다면, 당장 옮길 이유는 없습니다. HorizonDB는 별개의 새 서비스이고 아직 프리뷰입니다. 프로덕션 SLA, 모든 region, 익숙한 운영 도구가 다 갖춰지기 전까지는 검증 대상으로 보는 게 맞습니다.

차이를 운영 관점에서 추리면 이렇습니다.

  • storage 확장: flexible server는 단일 인스턴스 모델이라 storage/IOPS에 인스턴스 단위 상한이 있습니다. HorizonDB는 공유 storage를 최대 128TB까지 auto-scaling으로 키운다고 주장합니다. 한 데이터베이스가 수십 TB로 커지는 워크로드라면 이 차이가 큽니다.
  • 읽기 확장과 failover: scale-out compute와 공유 storage 분리는 replica 추가와 failover를 가볍게 만듭니다. 데이터를 통째로 옮기지 않고 같은 storage를 붙잡는 구조이기 때문입니다.
  • AI 워크로드: 벡터 검색과 in-database 모델 호출이 엔진에 들어 있어, 임베딩/검색을 별도 서비스로 빼지 않고 SQL 안에서 처리하려는 설계입니다. RAG나 agent용 데이터 계층을 PostgreSQL 한 곳에 모으려는 경우 매력이 있습니다.

반대로 신중하게 볼 점도 있습니다. 3배 throughput, sub-millisecond commit 같은 숫자는 모두 Microsoft가 자기 벤치마크로 든 주장입니다. 워크로드, region, 구성에 따라 결과는 달라지므로, 옮기기 전에 자기 데이터로 직접 측정하는 게 맞습니다. extension 호환 범위, 기존 flexible server에서의 마이그레이션 경로, 백업, PITR, 모니터링 도구가 어디까지 따라오는지도 프리뷰 기간에 확인할 항목입니다.

큰 그림에서 보면, 클라우드 벤더가 PostgreSQL 호환을 새로 짓는 흐름은 PostgreSQL이 사실상 관리형 데이터베이스의 공통 인터페이스가 됐다는 신호입니다. 엔진은 같은 PostgreSQL을 쓰되 storage와 확장 계층을 각자 새로 짜고, 그 위에 AI, 분석, 분산이라는 차별점을 얹습니다. DBA 입장에서는 PostgreSQL 한 가지 언어로 여러 벤더의 관리형 제품을 평가할 수 있다는 뜻이고, 동시에 "호환"이라는 단어 뒤에 숨은 제약을 제품마다 따져야 한다는 뜻이기도 합니다.

HorizonDB는 아직 프리뷰예요. 하지만 방향은 분명해요. Microsoft는 PostgreSQL 위에 storage를 갈아끼우고 AI를 엔진 안으로 끌어들이는 길을 택했고, 이 선택이 Aurora DSQL/AlloyDB와 나란히 놓이면서 "PostgreSQL 호환을 새로 짓는" 3파전이 한층 또렷해졌어요.

AlloyDB AI 에이전트 연동

· 약 6분

Google Cloud가 2026년 6월 2일 AlloyDB Remote MCP Server를 정식 출시(GA)했어요. AI 에이전트가 AlloyDB(PostgreSQL 호환) 데이터베이스에 표준 프로토콜로 직접 붙어 SQL을 실행하고 인스턴스를 관리할 수 있게 됐어요.

여기서 눈여겨볼 단어는 GA입니다. 프리뷰 단계의 실험 기능이 아니라 production에 올려도 된다는 신호이며, 곧 실무 환경에서 에이전트가 운영 데이터베이스에 접근하는 구성이 현실이 된다는 뜻입니다. 운영자와 DBA 입장에서는 "AI가 우리 DB에 직접 붙는다"는 상황을 이제 설계 대상으로 다뤄야 합니다.

표준 프로토콜 하나로 붙는다

MCP(Model Context Protocol)는 LLM이 외부 데이터/도구에 일관된 방식으로 연결되도록 만든 오픈 표준입니다. 모델마다, 도구마다 제각각이던 연동 방식을 하나의 규약으로 묶는다고 보면 됩니다. 에이전트는 MCP 서버가 노출하는 도구(tool) 목록을 받아 보고, 그중 필요한 것을 골라 호출합니다. "데이터베이스에 질의해줘"라는 요청이 들어오면, 에이전트가 MCP 서버의 SQL 실행 도구를 호출하는 식입니다.

AlloyDB Remote MCP Server는 이 규약을 AlloyDB에 입힌 것입니다. Google Cloud의 관리형 인프라 위에서 동작하고, HTTP 엔드포인트 하나를 노출합니다.

https://alloydb.googleapis.com/mcp

"Remote"라는 이름이 붙은 이유가 여기 있습니다. 예전 MCP 연동은 보통 로컬에서 stdio 스트림으로 도는 서버를 띄워 쓰는 방식이었습니다. 이번 GA는 그 서버를 Google Cloud가 직접 관리형으로 호스팅합니다. 운영자가 별도 서버를 배포하거나 패치할 필요 없이, 인증된 에이전트가 원격 엔드포인트로 붙습니다.

노출하는 도구는 크게 두 갈래입니다.

갈래대표 도구하는 일
데이터베이스execute_sql, execute_sql_read_onlySQL 실행 (쓰기/읽기 전용)
데이터베이스export_data, import_data데이터 반출/반입
사용자 관리create_user, list_users, get_user데이터베이스 사용자 관리
인스턴스 관리create_instance, update_instance, get_instance인스턴스 생성, 변경, 조회
클러스터 관리create_cluster, restore_cluster, create_backup클러스터/백업 관리

주목할 점은 도구가 단순 조회에 그치지 않는다는 것입니다. SQL 실행은 물론이고 인스턴스 변경, 데이터 반출입, 백업 생성과 클러스터 복원까지 포함합니다. 에이전트가 마음만 먹으면 운영 작업 상당수를 직접 수행할 수 있는 범위입니다. 그만큼 권한 경계가 중요해집니다.

읽기 전용 도구가 별도로 있다는 점도 눈여겨볼 만합니다. execute_sql은 쓰기까지 가능하지만, execute_sql_read_only는 수정/삭제를 막습니다. 에이전트가 의도치 않게 데이터를 바꾸는 사고를 도구 수준에서 차단하는 장치입니다.

인증/권한 모델

에이전트가 어떻게 인증하고 무엇까지 할 수 있는지가 운영의 핵심입니다. AlloyDB Remote MCP Server는 공유 비밀번호나 API 키를 쓰지 않습니다. 실제로 공식 문서는 "이 서버는 API 키를 받지 않는다"고 못박습니다.

인증은 OAuth 2.0 bearer 토큰으로 합니다. 에이전트는 Google Cloud IAM 신원으로 발급받은 토큰을 HTTP Authorization 헤더에 실어 보냅니다. 모든 Google Cloud 신원이 인증 주체가 될 수 있습니다.

권한은 IAM 역할로 갈립니다. 도구가 요구하는 역할이 작업 종류에 따라 다릅니다.

작업필요 역할
인스턴스/사용자 생성roles/alloydb.admin
SQL 실행 (쓰기 포함)roles/alloydb.admin 또는 roles/alloydb.databaseUser
읽기 전용 SQLroles/alloydb.viewer, roles/alloydb.databaseUser, roles/alloydb.admin
인스턴스/사용자 목록 조회roles/alloydb.viewer

OAuth 스코프는 https://www.googleapis.com/auth/alloydb가 기본입니다. 즉 에이전트가 할 수 있는 일의 상한은 그 에이전트에 붙은 IAM 역할이 결정합니다. 비밀번호를 공유하던 시절처럼 "DB에 붙을 수 있으면 전부 다 된다"가 아니라, 신원별로 역할을 좁혀 부여하는 구조입니다.

여기서 DBA가 정확히 이해해야 할 분리가 하나 있습니다. 에이전트가 들고 오는 IAM 신원과 실제 데이터베이스 내부의 사용자 권한은 별개 층위입니다. IAM 역할은 "이 에이전트가 AlloyDB API를 호출할 수 있는가"를 가릅니다. 그 호출이 데이터베이스 안에서 어떤 테이블/스키마까지 읽고 쓰는지는 데이터베이스 자체의 GRANT/REVOKE로 통제합니다. Google Cloud는 IAM으로 에이전트를 특정 테이블, 스키마, 뷰에 한정할 수 있다고 안내하는데, 실제 운영에서는 IAM 역할 부여와 데이터베이스 권한을 양쪽 다 좁혀야 의미가 살아납니다.

통제 지점을 어디에 두나

AI 에이전트가 production 데이터베이스에 직접 붙는다는 건, 사람 손을 거치지 않은 호출이 운영 작업을 일으킬 수 있다는 뜻입니다. 통제 지점을 정리합니다.

에이전트는 사람과 다르게 행동합니다. 잘못된 판단으로 DELETEDROP을 내릴 수 있고, 프롬프트 조작에 넘어가 의도하지 않은 질의를 던질 수도 있습니다. 권한, 감사, 입력 검증을 사람 DBA보다 더 촘촘하게 잡아야 하는 이유입니다.

에이전트마다 별도 IAM 신원을 부여하고 작업에 꼭 필요한 역할만 붙입니다. 조회만 하는 에이전트에 roles/alloydb.admin을 주는 일은 없어야 합니다. 최소 권한의 기본값은 roles/alloydb.viewer 수준에서 시작해 필요할 때 올립니다.

분석/조회가 목적이라면 execute_sql_read_only만 쓰도록 제한합니다. 쓰기가 가능한 execute_sql은 명확히 필요한 경우에만, 권한을 좁힌 별도 신원에 한정합니다. update_instance, restore_cluster, import_data 같은 도구는 데이터베이스의 형상 자체를 바꾸므로, 에이전트가 이런 도구에 닿을 이유가 없다면 IAM 역할에서 해당 권한을 빼 원천 차단합니다.

모든 질의, 동작, 도구 호출은 Cloud Audit Logs에 남습니다. 어떤 신원이 언제 무슨 도구를 호출했는지 추적할 수 있습니다. 단순히 켜두는 데서 끝내지 말고 비정상 패턴(대량 반출, 권한 밖 시도)에 대한 알림까지 엮어야 통제가 완성됩니다.

Google Cloud는 Model Armor라는 선택적 보호 계층도 제공합니다. 프롬프트 injection을 거르고 데이터 반출을 막는 용도입니다. 에이전트가 외부 입력을 받는 구성이라면 이 계층을 검토할 가치가 있습니다.

앞서 짚었듯 IAM만으로는 부족합니다. 데이터베이스 안에서도 에이전트용 role을 만들어 접근 가능한 스키마/테이블을 GRANT로 한정합니다. IAM과 데이터베이스 권한이 모두 좁아야 실효가 있습니다.

결국 권한 모델은 데이터베이스가 더 이상 사람만 붙는 곳이 아니라는 전제에서 다시 짜야 합니다. 사람은 실수해도 속도가 느리지만, 에이전트는 잘못된 동작을 빠르게 반복합니다.

흐름과 권한 경계

에이전트 요청이 데이터베이스에 닿기까지의 경로와, 그 위에 놓인 두 겹의 권한 경계를 도식으로 정리합니다.

위쪽 IAM 검사는 "이 신원이 이 도구를 호출할 자격이 있는가"를 가르는 1차 경계입니다. 통과해도 데이터베이스 안에서 GRANT로 정해진 범위를 벗어나면 2차 경계에서 막힙니다. 두 경계를 모두 통과한 작업만 실행되고, 그 흔적은 빠짐없이 감사 로그에 남습니다. DBA가 손볼 지점은 결국 이 두 경계의 폭과 마지막 로그를 읽어내는 체계입니다.

마치며

AlloyDB Remote MCP Server GA는 "AI 에이전트가 데이터베이스에 직접 붙는" 구성이 실험을 넘어 production 선택지가 됐다는 신호입니다. 편의는 분명합니다. 자연어로 질의하고, 스키마를 자동으로 파악하고, 운영 작업까지 위임할 수 있습니다.

그만큼 DBA가 쥐어야 할 통제도 늘었어요. 비밀번호 공유가 IAM 신원으로 바뀌고 사람의 클릭이 에이전트의 도구 호출로 바뀌는 동안, 권한은 더 좁게, 감사는 더 촘촘하게 가져가야 해요. 데이터베이스에 새 문이 하나 열렸으니, 그 문의 자물쇠와 출입 기록을 챙기는 일이 다음 차례예요.


출처:

PG19 온라인 체크섬 전환

· 약 8분

PostgreSQL 19에서 data_checksums가 또 하나의 재시작 파라미터에서 벗어났어요. 이제 실행 중인 클러스터에서 재시작도 정지도 없이 page checksum을 켜고 끌 수 있어요. SQL 함수 하나를 실행하면 background worker가 모든 page를 다시 쓰면서 checksum을 입히고, 그동안 클러스터는 평소처럼 트래픽을 받아요.

이 글은 The Build의 Christophe Pettus(크리스토프 페투스)가 정리한 "All Your GUCs in a Row: data_checksums"를 한국어로 풀고, 13년에 걸친 data_checksums의 진화사와 PostgreSQL 19가 정확히 무엇을 바꿨는지를 DBA 시선으로 봅니다. 그리고 "온라인"이라는 단어가 "공짜"나 "즉시"를 뜻하지 않는다는 점까지 함께 짚습니다.

dbalog에는 PostgreSQL 19의 "재시작 없이 바꾼다" 계열 글이 이미 두 편 있습니다. wal_level이 고정값에서 동적 floor로 바뀐 이야기autovacuum_worker_slots로 worker 수를 재시작 없이 조절하는 이야기입니다. data_checksums의 온라인 전환은 그 흐름 위에 올라가는 또 한 칸입니다.

data_checksums가 무엇을 막아주나

data_checksums는 page 단위 checksum 기능을 켜는 read-only GUC입니다. 켜져 있으면 PostgreSQL은 data page를 디스크에 쓸 때마다 checksum을 계산해 page 안에 함께 적고, 그 page를 다시 읽어 올릴 때 checksum을 검증합니다.

값이 맞지 않으면 PostgreSQL은 깨진 데이터를 그대로 돌려주지 않고 error를 냅니다. 여기서 막아주는 대상은 silent data corruption, 즉 조용히 번지는 손상입니다.

  • bit rot, 디스크 위 데이터가 시간이 지나며 미세하게 망가지는 현상
  • 고장 직전의 디스크가 슬그머니 잘못된 비트를 돌려주는 경우
  • "썼다"고 응답해 놓고 실제로는 쓰지 않은 storage layer의 거짓말

이런 손상은 error 없이 흘러갑니다. checksum이 없으면 PostgreSQL은 깨진 page를 멀쩡한 데이터로 믿고 그대로 읽어 들이고, 그 위에 연산을 쌓습니다. 문제를 알아챌 무렵엔 이미 backup에까지 손상이 번진 뒤입니다.

검증에 실패하면 PostgreSQL은 pg_stat_databasechecksum_failures 카운터를 올립니다. DBA는 이 값을 모니터링해 손상이 처음 감지된 시점을 잡아낼 수 있습니다.

비용은 어떻습니까. 2013년 도입 당시엔 checksum 계산 부담이 켤 가치가 없을 만큼 크다고 봤습니다. 그래서 기본값이 off였습니다. 그 뒤로 하드웨어가 좋아지면서 오버헤드는 한 자릿수 퍼센트 초반대까지 내려왔고, 손상을 조기에 잡는 가치에 비하면 충분히 감당할 만한 수준이 됐습니다.

과거엔 켜기가 왜 고통이었나

문제는 비용이 아니라 켜는 방법이었습니다. PostgreSQL 18까지 data_checksums를 켜는 길은 둘뿐이었고, 둘 다 운영 클러스터에는 무겁습니다.

첫째, initdb 시점에 정하는 것입니다. 클러스터를 처음 만들 때 checksum을 켜두면 그 클러스터는 평생 켜진 상태로 삽니다. 깔끔하지만 시점이 고약합니다. 이미 몇 년째 돌고 있는 운영 클러스터에는 적용할 길이 없습니다. "처음부터 켰어야 했다"는 후회만 남습니다.

둘째, pg_checksums로 오프라인 전환하는 것입니다. PostgreSQL 12에서 추가된 이 명령은 멈춰 있는 클러스터의 checksum 설정을 바꿔줍니다. 강조점은 "멈춰 있는"입니다.

# 반드시 클러스터를 먼저 정지한 상태에서 실행
pg_ctl -D /var/lib/pgsql/data stop
pg_checksums --enable -D /var/lib/pgsql/data
pg_ctl -D /var/lib/pgsql/data start

pg_checksums는 모든 heap과 index page를 한 장씩 읽어 checksum을 계산해 다시 씁니다. 멀티 테라바이트 클러스터라면 이 작업만 몇 시간이 걸립니다. 그리고 그 몇 시간 내내 클러스터는 내려가 있어야 합니다.

DBA에게 이건 사실상 "큰맘 먹고 잡는 점검 시간"입니다. 서비스 중단 공지를 내고, 새벽 시간을 확보하고, 작업이 예상보다 길어질 경우까지 대비해야 합니다. 그래서 많은 운영 클러스터가 checksum의 가치를 알면서도 "지금 켜기엔 다운타임이 부담"이라는 이유로 off인 채 남았습니다.

PostgreSQL 18에서 한 발 나아가긴 했습니다. initdb의 기본값이 checksum 켜짐으로 바뀌어, 새로 만드는 클러스터는 별도 조치 없이 checksum을 켠 채 출발합니다. 하지만 이는 새 클러스터 이야기입니다. 이미 돌고 있는 클러스터의 고민은 그대로였습니다.

PostgreSQL 19의 온라인 전환

PostgreSQL 19는 마지막 매듭을 풉니다. 클러스터를 멈추지 않고 재시작도 없이 SQL 함수 호출만으로 checksum을 켜고 끕니다. 새로 들어온 함수는 둘입니다.

pg_enable_data_checksums(cost_delay integer DEFAULT 0, cost_limit integer DEFAULT 100)
pg_disable_data_checksums()

켜는 동작은 이렇게 실행합니다.

SELECT pg_enable_data_checksums();

이 함수는 곧바로 반환됩니다. 밀리초 단위입니다. 하지만 그 시점에 checksum이 다 입혀진 것은 아닙니다. 실제 작업은 background에서 비동기로 흐릅니다.

내부 동작은 이렇습니다. background worker launcher가 데이터베이스마다 per-database worker를 띄웁니다. 이 worker는 storage를 가진 모든 relation의 buffer를 dirty로 표시합니다. dirty page는 디스크로 다시 쓰일 때 checksum을 계산해 함께 적게 됩니다. 모든 데이터베이스의 모든 relation이 처리되고 나면, 그제야 data_checksums 상태가 on으로 넘어갑니다.

OS의 process 목록에서도 이 launcher와 worker가 보입니다.

postgres: datachecksum launcher
postgres: datachecksum worker

진행 중에 data_checksums가 가질 수 있는 상태는 다음과 같습니다.

상태의미
offchecksum 꺼짐
inprogress-on켜는 중, page 재작성 진행 중
on켜짐, 모든 page 처리 완료
inprogress-off끄는 중

SHOW로 현재 상태를 확인합니다.

SHOW data_checksums;
-- 진행 중: inprogress-on
-- 완료 후: on

끄는 동작도 같은 방식입니다.

SELECT pg_disable_data_checksums();

이 모든 과정에서 클러스터는 멈추지 않습니다. 읽기도 쓰기도 평소처럼 받습니다. PostgreSQL 19에서는 다운타임이 사라졌습니다.

전환 흐름 비교

오프라인 전환과 온라인 전환의 차이를 한눈에 보면 이렇습니다.

PostgreSQL 18 이하의 경로는 정지-작업-재기동이라는 다운타임 구간을 통과해야 합니다. PostgreSQL 19는 그 구간 자체가 없습니다. 작업은 가동 중인 클러스터 위에서 background로 흐릅니다.

상태 전이로 보면 켜는 과정은 다음과 같이 움직입니다.

여기서 DBA가 기억할 점이 하나 있습니다. inprogress-on은 "켜지는 중"이지 "켜짐"이 아닙니다. 모든 page가 처리되기 전까지는 on으로 넘어가지 않습니다.

DBA 관점에서 보는 온라인 전환의 시간과 비용

함수가 밀리초 만에 반환된다고 해서 작업이 끝난 것은 아닙니다. 이 지점에서 운영 실수가 갈립니다. Christophe Pettus는 이 작업의 무게를 "minor version 업그레이드와 전체 클러스터 VACUUM FULL 사이 어디쯤"으로 보고 계획하라고 권합니다.

기억할 점을 정리하면 이렇습니다.

진짜 작업은 background에서 진행됩니다. 함수 반환은 시작 신호일 뿐이며, 멀티 테라바이트 클러스터에서 모든 page를 재작성하려면 몇 시간이 걸립니다.

이 작업은 다른 작업과 자원을 다툽니다. page 재작성은 autovacuum, 평상시 워크로드, backup 작업과 디스크 I/O를 두고 경쟁합니다. burst IOPS를 쓰는 클라우드 인스턴스라면 burst 예산을 일찍 소진하고 throttle 구간에 들어갑니다.

중간에 끊으면 일부 page에는 checksum이 입혀지고 일부에는 입혀지지 않은 mixed state로 남습니다. 이 상태 자체는 안전하지만, 작업이 끝나기 전까지 data_checksumson으로 넘어가지 않습니다. 오래 머물 상태는 아닙니다.

그래서 켜는 함수에는 throttle 제어가 붙어 있습니다. cost_delaycost_limit은 autovacuum의 vacuum cost 의미를 그대로 따릅니다. worker가 cost_limit만큼 작업 단위를 쌓을 때마다 cost_delay 밀리초씩 쉬게 해서, 평상시 워크로드에 주는 압박을 낮춥니다.

-- worker가 자원을 덜 차지하도록 천천히 진행
SELECT pg_enable_data_checksums(cost_delay => 1, cost_limit => 3000);

운영 현장에서 잡을 체크리스트는 단순합니다.

  • 트래픽이 한가한 시간대를 골라 시작합니다.
  • autovacuum이 급한 작업을 들고 있지 않은지 확인합니다.
  • backup window와 겹치지 않게 합니다.
  • 진행 중에는 pg_stat_io로 I/O 부하를, replication을 쓴다면 replica lag을 함께 봅니다.

PostgreSQL 19의 이번 변화를 앞선 두 글과 나란히 놓으면 방향이 또렷합니다. wal_level은 재시작 파라미터에서 동적 floor로, autovacuum worker 수는 재시작 없이 조절 가능하게, 그리고 이제 data_checksums는 클러스터를 멈추지 않고 켜고 끄는 대상이 됐습니다. "운영 중에 바꾸려면 재시작/정지가 필요하던 설정"의 목록이 한 칸씩 줄고 있습니다.

다만 방향이 같다고 비용까지 같진 않습니다. wal_level의 effective level 전환은 다음 checkpoint면 끝나지만, data_checksums의 온라인 전환은 디스크 위 모든 page를 다시 쓰는 무거운 작업입니다. "재시작이 사라졌다"와 "부담 없이 켤 수 있다"는 다른 말입니다. 다운타임은 없앴지만, I/O 비용과 소요 시간은 그대로 DBA의 계획표 위에 남습니다.

정리

  • data_checksums는 page 단위 checksum으로 silent data corruption을 조기에 잡아주는 기능입니다. 실패는 pg_stat_database.checksum_failures로 드러납니다.
  • PostgreSQL 18까지는 initdb 시점에 고정하거나, 클러스터를 정지하고 pg_checksums로 오프라인 전환해야 했습니다. 운영 클러스터엔 다운타임이 부담이었습니다.
  • PostgreSQL 18부터 initdb 기본값이 켜짐으로 바뀌어, 새 클러스터는 별도 조치 없이 checksum을 켠 채 출발합니다.
  • PostgreSQL 19는 pg_enable_data_checksums() / pg_disable_data_checksums()로 가동 중인 클러스터에서 재시작 없이 켜고 끌 수 있게 했습니다. background worker가 모든 page를 다시 쓰며, 진행 중 상태는 inprogress-on / inprogress-off로 보입니다.
  • 다운타임은 사라졌지만 작업 자체는 무거워요. 멀티 테라바이트 클러스터에서 몇 시간이 걸리고 다른 I/O와 경쟁하므로, cost_delay / cost_limit throttle과 한가한 시간대 시작이 필요해요.

출처