본문으로 건너뛰기

PostgreSQL 30년사

· 약 18분

들어가며​

2025년 6월 2일, Snowflake(스노우플레이크)가 Crunchy Data(크런치 데이터)를 약 2억 5천만 달러에 인수했고, 2025년 5월에는 Databricks(데이터브릭스)가 Neon(네온)을 10억 달러 안팎으로 가져갔어요. Microsoft(마이크로소프트)는 자체 Postgres 변형 HorizonDB를 들고나왔어요. 2026년 5월 현재, "PostgreSQL을 쓴다"는 말은 점점 모호해졌어요. Snowflake Postgres인가, Lakebase인가, HorizonDB인가, 아니면 그냥 PostgreSQL인가요.

이 글은 30년에 걸친 PostgreSQL 회사들의 인수와 이별, 그리고 그 사이를 혼자 걸어온 컨설턴트/메인테이너들의 이야기입니다. 사실에 기반해 풀고, 마지막에는 다음 5년을 가늠해 보겠습니다.

한 장으로 보는 흐름​

본문으로 들어가기 전, 큰 패턴을 표 하나로 깔아둡니다.

시기흐름대표 사건
1986~1999학교 프로젝트 → 오픈소스UC Berkeley POSTGRES, 1996 PostgreSQL
2002~2009PG 전용 회사 출현2ndQuadrant, EDB, PGX
2010~20151차 클라우드 인수Salesforce→Heroku, EMC→Greenplum
2016~20192차 인수 + 새 시장Microsoft→Citus, AWS→OpenSCG, Supabase/Aiven
2020~2023PG 회사들의 통합EDB→2ndQuadrant, Bain Capital→EDB
2024상실Simon Riggs 사망, xz backdoor, Greenplum closed-source
2025클라우드 베어 시대Databricks→Neon, Snowflake→Crunchy, HorizonDB
2026자금 단절의 그림자pgBackRest archive, 다중 스폰서 모델

PostgreSQL 생태계 30년의 네 시대

표를 위에서 아래로 따라 읽으면 한 가지 패턴이 보입니다. PostgreSQL 회사가 클라우드 회사에 흡수되는 주기가 점점 짧아집니다. Salesforce→Heroku에서 Microsoft→Citus까지 9년, Microsoft→Citus에서 Databricks→Neon까지 6년입니다.

이제 각 시기를 풀어씁니다.

Act 1. 1996~2010​

학교 프로젝트가 살아남은 시기입니다.

PostgreSQL의 출발은 UC Berkeley(버클리)의 POSTGRES (1986)입니다. Michael Stonebraker(마이클 스톤브레이커)가 INGRES의 후속으로 시작한 프로젝트가 1995년 SQL 인터페이스를 도입한 Postgres95를 거쳐 1996년 PostgreSQL로 이름을 바꿨고, 1997년 6.0이 나왔습니다. 이 시기 합류한 사람들이 이후 30년의 형태를 만들었습니다.

Tom Lane(톰 레인)은 1996년 무렵부터 활동했습니다 — PostgreSQL 역사에서 가장 많은 commit을 한 사람입니다. Great Bridge가 문을 닫은 2001년 무렵부터 Red Hat(레드햇)에서 일했고, 2013~2015년 Salesforce(세일즈포스)를 거쳐 2015년 Crunchy Data로 옮겼습니다. Crunchy는 그에게 PostgreSQL에 100% 시간을 쏟을 자리를 마련해 줬습니다.

Bruce Momjian(브루스 모미얀)은 PostgreSQL 거버넌스의 대변자입니다. 2006년 EDB(EnterpriseDB, 엔터프라이즈DB)에 합류해 지금까지 같은 자리에 있습니다. PostgreSQL 라이선스의 BSD-스타일 약속을 가장 자주 옹호하는 사람이기도 합니다.

1997년에는 Joshua Drake(조슈아 드레이크)가 미국 몬태나에 Command Prompt(커맨드 프롬프트)를 차렸습니다. 2026년 현재까지 살아남은 PostgreSQL 컨설팅 회사입니다.

회사 쪽 흐름은 두 갈래로 갈렸습니다.

EDB는 2004년 Andy Astor(앤디 애스터), Denis Lussier(데니스 루시에르), Paul T. Winn(폴 윈), Phillip Merrick(필립 메릭)가 설립했습니다. 처음부터 "엔터프라이즈용 PostgreSQL"을 표방했고 Oracle(오라클) 호환성에 무게를 뒀습니다.

2ndQuadrant(세컨드쿼드런트)는 2002년 Simon Riggs(사이먼 릭스)가 영국/이탈리아에 설립했습니다. 설립자가 PITR, Hot Standby, 동기화 replication을 만든 사람이라 회사 자체가 "코어 기능 공급자"였습니다.

2009년에는 Christophe Pettus(크리스토프 페투스)가 PGX(PostgreSQL Experts)를 차렸습니다. The Build(더 빌드) 블로그가 이 회사의 채널입니다.

이 시기 클라우드는 아직 변두리였습니다. 2010년 12월, Salesforce가 Heroku(헤로쿠)를 약 2억 1,200만 달러에 인수했습니다. Heroku는 그해 11월에 PostgreSQL을 도입했고, 다음 해 11월 "Heroku Postgres"를 분리 제품으로 내놨습니다. Ruby/Python 개발자가 PostgreSQL을 "기본"으로 받아들이기 시작한 순간이었습니다.

Act 2. 2011~2019​

클라우드가 책을 다시 쓴 시기입니다.

이 시기 PostgreSQL 회사들은 두 가지 길을 갔습니다. 하나는 클라우드 벤더에 흡수되는 길이고, 다른 하나는 새로운 시장을 만드는 길이었습니다.

흡수된 쪽:

OpenSCG → AWS(2018년 4월경)는 Denis Lussier가 만든 마이그레이션 서비스 회사의 인수 사례입니다. 회사는 AWS의 Database Migration Service에 흡수됐습니다.

Citus Data(사이터스 데이터) → Microsoft(2019-01-24)는 Y Combinator(Y 컴비네이터) S11 출신 회사의 인수 사례입니다. 이 회사는 분산 PostgreSQL extension을 만들었습니다. Microsoft가 인수한 뒤 Azure Database for PostgreSQL Hyperscale (현 Cosmos DB for PostgreSQL)로 이름이 바뀌었으며, 금액은 공개되지 않았습니다.

EDB의 자본 회전도 이어졌습니다. 2014년 Peak Equity Partners(피크 에쿼티 파트너스), 2019년 Great Hill Partners(그레이트 힐 파트너스)가 차례로 EDB의 다수 지분을 가져갔습니다.

새로 만들어진 쪽:

Crunchy Data는 2012년 Bob Laurence/Paul Laurence(폴 로런스) 형제가 창업했으며, 규제 산업/연방 정부 시장에 무게를 뒀습니다. Timescale(타임스케일)은 2015년 PostgreSQL extension으로 시계열 워크로드를 잡았습니다. Aiven(아이븐)은 2016년 핀란드 출신 3인(Oskari Saarenmaa(오스카리 사렌마), Hannu Valtonen(한누 발토넨), Heikki Nousiainen(헤이키 노우시아이넨))이 만든 관리형 오픈소스 플랫폼입니다. 2022년 시리즈 D 2억 1,000만 달러까지 모았습니다. Supabase(수파베이스)는 2020년 Paul Copplestone(폴 코플스톤)/Ant Wilson(앤트 윌슨)이 설립했습니다. "Firebase(파이어베이스)의 오픈소스 대안"이라는 슬로건으로 시작했지만, 본질은 관리형 PostgreSQL + BaaS입니다.

이 시기 가장 묵직한 인물 이동은 Andres Freund(안드레스 프룬트)입니다. 그는 Microsoft Azure Postgres 팀으로 옮겨갔고, 2024년 3월 PostgreSQL을 Debian Sid에서 벤치마킹하다가 SSH 로그인이 500ms 늦은 것을 눈치챘습니다 — 그 흔적을 추적해 xz Utils backdoor를 발견했습니다. "역사상 가장 위험한 backdoor"라는 평가가 붙은 사건의 발견자가 PostgreSQL 커미터였다는 것은 우연이 아닙니다.

2014년에는 한국에서도 작은 움직임이 있었습니다. PostgreSQL Korea User Group이 EDB, 다우기술, 펜타시스템 협업으로 출범한 기록이 남아 있습니다.

Act 3. 2020~2024​

통합과 상실의 시대입니다.

2020년 9월 30일, EDB가 2ndQuadrant 인수를 완료했습니다. 이로써 EDB는 "가장 큰 PostgreSQL 전용 회사"가 됐고, Simon Riggs는 EDB Fellow가 됐습니다. 한 시대가 다른 시대를 흡수한 순간이었습니다.

2021년에는 Neon이 설립됐습니다. 설립자는 Nikita Shamgunov(니키타 샴구노프)/Heikki Linnakangas(헤이키 린나캉가스)/Stas Kelvich(스타스 켈비치) 3인입니다. 서버리스 + 브랜칭 모델로 시작했고, 이 회사는 4년 뒤 Databricks의 손에 들어갔습니다.

2022년 6월, Bain Capital(베인 캐피털) Private Equity가 EDB의 다수 지분을 가져갔습니다. PostgreSQL 최대 후원사가 사모펀드의 자산이 된 셈입니다.

Simon Riggs는 2024년 3월 26일, 영국 Duxford 비행장에서 본인이 조종하던 Cirrus SR22-T 사고로 별세했습니다. PITR, Hot Standby, 동기화 replication을 만든 사람의 죽음을 PostgreSQL 커뮤니티는 길게 애도했습니다. 공식 부고는 postgresql.org에 남아 있습니다.

사흘 뒤인 3월 29일, Andres Freund가 xz Utils backdoor를 발견했습니다. 같은 주에 PostgreSQL은 한 사람을 잃었고, 또 한 사람의 발견은 리눅스 생태계 전체를 구했습니다.

Greenplum(그린플럼)의 운명도 이 시기에 결정됐습니다. 2010년 EMC가 인수했던 이 PostgreSQL 기반 MPP 데이터베이스는 Pivotal(피보탈)/VMware(VM웨어)/Broadcom(브로드컴)을 거쳤습니다. 2024년 5월, Broadcom이 Greenplum을 closed-source로 전환했습니다. 오픈소스 PostgreSQL 변형이 사유 제품이 된 첫 큰 사례입니다.

같은 시기 K8s 쪽에서는 CloudNativePG가 자라났습니다. EDB의 Gabriele Bartolini(가브리엘레 바르톨리니)가 이끄는 프로젝트로, 2025년 1월 CNCF Sandbox에 정식 진입했습니다. PostgreSQL이 CNCF 산하 데이터베이스가 된 첫 사례입니다.

Act 4. 2025~2026​

클라우드 베어의 시대입니다.

2025년이 무겁습니다. 클라우드 베어 둘이 같은 분기에 PostgreSQL 회사를 가져갔고, 하나는 직접 만들었습니다.

시점사건금액
2025-05-14Databricks → Neon약 10억 달러
2025-06-02Snowflake → Crunchy Data약 2억 5천만 달러
2025-11-18Microsoft → HorizonDB 프리뷰 발표 (자체 개발, Ignite 2025)—

두 인수와 하나의 자체 개발은 같은 방향을 가리킵니다. AI agent를 위한 PostgreSQL입니다. Neon은 서버리스 + 브랜칭 모델이라 agent가 매 작업마다 격리된 DB 인스턴스를 만들기에 적합합니다. Crunchy는 규제 산업 고객 기반과 안정적 코어 엔진을 가져왔습니다. HorizonDB는 wire 호환만 유지하고 스토리지는 Rust로 새로 짰습니다.

The Build 블로그의 Christophe Pettus는 이 셋을 한 줄로 정리했습니다.

"Pick the one whose adjacent data platform you already use, and do not pretend you have a choice you don't have."

선택은 이미 쓰고 있는 데이터 플랫폼이 정해 줬다는 뜻입니다. Snowflake 고객은 Snowflake Postgres를 쓰고, Databricks 고객은 Lakebase(레이크베이스)를 씁니다. Pettus의 정리에 따르면 셋 모두 PostgreSQL wire 프로토콜과 호환되지만, extension은 제품마다 일부만 지원합니다. logical replication은 Snowflake Postgres가 기본 동작에 가장 가깝고, Lakebase와 HorizonDB는 logical decoding에 미치는 영향이 아직 충분히 문서화되지 않았습니다. Pettus의 5월 12일 글은 pg_basebackup, pgBackRest, Patroni 같은 운영 도구가 셋 모두에 적용되지 않는다고 봤습니다.

다만 Pettus는 5월 24일 정정 글에서 Snowflake Postgres를 스토리지까지 upstream 그대로인 community Postgres라고 다시 설명했습니다. 정정 대상은 그 주의 table access method 글이지만, 5월 12일 글도 셋을 모두 custom storage layer 제품으로 묶었으므로 같은 전제가 흔들립니다. 그래서 운영 도구에 관한 문장은 주로 Lakebase와 HorizonDB에 해당한다고 보는 편이 자연스럽습니다.

Crunchy 인수는 빈말로 끝나지 않았습니다. Snowflake는 2026년 2월 24일 Snowflake Postgres를 정식 출시(GA)했습니다. Snowflake 콘솔에서 곧바로 Postgres 인스턴스를 만들고 관리할 수 있으며, AWS/Azure 일부 리전에서 먼저 풀렸습니다. Snowflake는 이를 "fork가 아니라" 기존 PostgreSQL 도구를 그대로 쓰는 lift-and-shift 호환 제품이라고 강조합니다. Crunchy의 엔진과 인력이 실제 상용 제품으로 녹아든 결과입니다.

같은 시기 Timescale은 2025년 6월 TigerData(타이거데이터)로 리브랜드했습니다. 시계열에서 벗어나 agent 데이터베이스를 표방합니다.

막간. 혼자 걸어온 사람들​

회사 인수 흐름과 별개로, PostgreSQL 생태계에는 큰 회사 안에 들어가지 않고 30년 가까이 자기 일을 한 사람들이 있습니다.

Christophe Pettus는 2009년 PGX를 차렸습니다. The Build 블로그는 이 회사의 채널입니다. 2026년 5월 현재도 본인이 직접 CEO, 컨설턴트, 블로거 노릇을 합니다.

Markus Winand(마르쿠스 비난트)는 책 한 권(《SQL Performance Explained》), 웹사이트 둘(use-the-index-luke.com, modern-sql.com), 비엔나에서 진행하는 5일 기업 교육으로 일합니다. 이 셋이 그의 회사 전체입니다. "구루"라는 표현이 가장 어울리는 형태입니다.

Hironobu Suzuki(스즈키 히로노부)는 일본 출신으로, 현재 유럽에 거주합니다. 《The Internals of PostgreSQL》을 비롯해 PostgreSQL 책 4권 + MySQL(마이에스큐엘) 책 3권을 썼습니다. interdb.jp는 그의 평생 작업입니다.

Hubert Lubaczewski(후베르트 루바체프스키, 닉네임 depesz)는 explain.depesz.com을 13년째 혼자 돌립니다. PostgreSQL 버전별 "Waiting for…" 시리즈는 1차 자료입니다. 회사는 본인 이름을 그대로 쓴 DEPESZ SOFTWARE HUBERT LUBACZEWSKI (폴란드)입니다.

Joshua Drake는 1997년 Command Prompt를 차렸습니다. 29년 차 PostgreSQL 컨설팅 회사이며, United States PostgreSQL의 회장이기도 합니다.

그러나 2026년 4월, 이 흐름의 어두운 그림자가 드러났습니다. David Steele(데이비드 스틸)이 13년간 단독으로 유지한 pgBackRest의 archive를 선언했습니다. 이미 pgBackRest의 종료 글에서 한 편 정리한 사건입니다. Crunchy가 후원하다가 Snowflake 인수 이후 자금이 끊어졌고, 그는 13개월간 새 직장을 찾지 못했습니다.

며칠 뒤 Steele은 입장을 일부 바꿨습니다. 다중 스폰서 연합 펀딩으로 전환해 프로젝트를 이어 갈 가능성을 발표했습니다. 구체적 후원 기업명은 아직 공개되지 않았습니다. Percona의 Jan Wieremjewicz(얀 비에렘예비치)는 2026년 4월 30일 Percona Community 글의 제목으로 이 사건을 요약했습니다.

"Open source doesn't die. It gets unfunded."

이 문장은 오픈소스는 죽는 것이 아니라 자금이 끊길 뿐이라는 뜻입니다.

커미터들은 어디에 있는가​

빅테크 흡수의 지도를 살펴봅니다.

2026년 9월 기준 postgresql.org의 커미터 목록에 오른 31명을, 기여자 페이지에 적힌 소속으로 묶으면 다음과 같습니다.

소속인원인물
Microsoft9Andres Freund/Noah Misch(노아 미시)/Thomas Munro(토마스 먼로)/Tomas Vondra(토마시 본드라)/David Rowley(데이비드 라울리)/Daniel Gustafsson(다니엘 구스타프손)/Amit Langote(아미트 랑고테)/Melanie Plageman(멜라니 플레이지먼)/Richard Guo(리처드 궈)
AWS7Joe Conway(조 콘웨이)/Michael Paquier(미카엘 파키에)/Peter Geoghegan(피터 게오게간)/Etsuro Fujita(후지타 에츠로)/John Naylor(존 네일러)/Nathan Bossart(네이선 보사트)/Masahiko Sawada(사와다 마사히코)
EDB6Bruce Momjian/Peter Eisentraut(페터 아이젠트라우트)/Álvaro Herrera(알바로 에레라)/Andrew Dunstan(앤드루 던스턴)/Robert Haas(로버트 하스)/Jacob Champion(제이콥 챔피언)
Snowflake1Tom Lane
Databricks/Neon2Jeff Davis(제프 데이비스)/Heikki Linnakangas
SRA OSS/NTT Data2Tatsuo Ishii(이시이 타츠오)/Fujii Masao(후지이 마사오)
Apple1Amit Kapila(아미트 카필라)
Supabase1Alexander Korotkov(알렉산더 코로트코프) (전 Postgres Professional(포스트그레스 프로페셔널))
Redpill Linpro1Magnus Hagander(망누스 하간데르)
소속 기재 없음1Dean Rasheed(딘 라시드)

기여자 페이지의 소속은 본인이 갱신하는 값이라 실제 현재 소속과 어긋날 수 있습니다. 예를 들어 Robert Haas는 페이지에 EDB로 적혀 있지만, 2026년 1월 본인 블로그 소개에는 Databricks로 적었습니다.

빅테크 흡수는 진행 중입니다. Microsoft와 AWS만 합쳐도 16명으로 절반을 넘고, EDB는 6명입니다. Crunchy는 이제 Snowflake 자회사라 Tom Lane도 Snowflake 소속으로 올라 있고, Databricks는 Neon 인수로 커미터를 더했습니다.

한국은 공식 커미터/코어 멤버 명단에서 확인되는 사람이 없습니다.

거버넌스​

라이선스는 안전한지 살펴봅니다.

먼저 던질 질문이 있습니다. Redis(레디스)/MongoDB(몽고DB)는 라이선스를 일방적으로 바꿨습니다. PostgreSQL도 그렇게 할 수 있을까요.

답은 사실상 불가능합니다. 이유는 단순합니다. PostgreSQL의 코드 소유권이 30년 동안 수천 명의 기여자에게 분산돼 있습니다. 라이선스 변경에는 만장일치에 가까운 동의가 필요합니다. Redis/MongoDB는 단일 회사 소유여서 가능했습니다.

PostgreSQL의 BSD-style 라이선스는 명시적으로 "perpetually free and open source"를 약속합니다. 30년의 분산이 이 약속의 진짜 보증입니다.

PostgreSQL과 단일 회사 데이터베이스의 라이선스 거버넌스 차이

위험은 다른 곳에 있습니다. 단일 메인테이너 도구의 자금 단절입니다. pgBackRest가 처음 보여줬고, 다음 사례가 어디서 나올지는 모릅니다.

향후 5년​

2031년의 풍경을 그려 봅니다.

사실에서 추측으로 넘어갑니다. 추측이지만 근거를 함께 적습니다.

가능성 높음:

  1. PostgreSQL이 사실상 표준 RDBMS 지위를 굳힙니다. Stack Overflow(스택 오버플로우) 2025 조사에서 개발자 55.6%가 PostgreSQL을 쓴다고 답했습니다. AI workload (pgvector) + OLTP + Analytics를 한 엔진이 다 받는 모델은 경쟁자가 없습니다.

  2. "PostgreSQL"이라는 단어가 흐려집니다. 사용자는 "Postgres를 쓴다"고 말하지만 실제로는 Aurora, Lakebase, HorizonDB, Snowflake Postgres 중 하나를 쓰는 식이 됩니다. extension 호환성이 가장 먼저 깨집니다.

  3. AI agent가 PostgreSQL을 "skill"로 등록하는 패턴이 표준이 됩니다. Supabase의 ChatGPT 공식 앱, Microsoft의 MCP for PostgreSQL이 출발선입니다.

갈등 지점:

  1. 단일 메인테이너 도구의 줄도산. Barman은 EDB가 받아 안정적이지만, pgBackRest 사건은 다른 도구에도 일어날 수 있습니다. Cybertec(사이버텍)의 pg_timetable, depesz.com 같은 1인 운영 자원의 지속성을 묻는 시기가 옵니다.

  2. K8s 운영자(operator)의 표준 경쟁. CloudNativePG가 CNCF 산하 단일 표준으로 가려면 시간이 더 필요합니다. Crunchy PGO는 Snowflake 인수 이후 장기 전략이 불투명하고, Zalando(잘란도)/StackGres(스택그레스)는 각자 자리를 지키고 있습니다.

  3. PostgreSQL 코어팀의 빅테크 의존도. EDB, Microsoft, AWS, Snowflake가 커미터 절대다수를 고용하고 있습니다. 라이선스는 안전해도, 로드맵의 무게중심은 조용히 이동합니다.

작게 가능한 시나리오:

  1. OrioleDB 같은 새 storage engine이 코어에 들어갑니다. 현재 public beta입니다. Alexander Korotkov가 Supabase로 옮긴 뒤 Supabase가 후원합니다. 2028년 전후로 production 후보가 될 가능성이 있습니다.

  2. 벤더 중립 우산 조직이 생깁니다. Gabriele Bartolini와 ProOpenSource(프로오픈소스)가 제안한 모델입니다. Apache Software Foundation(아파치 소프트웨어 재단)/Codeberg e.V.(코드버그)처럼 PostgreSQL 도구의 거버넌스를 단일 회사에서 분리하는 흐름이며, 아직 제안 단계입니다.

닫으며​

이 이야기에서 무엇이 남는지 생각해 봅니다.

PostgreSQL의 30년에는 두 줄이 동시에 흘러왔습니다. 한쪽에는 Heroku, Citus, OpenSCG, Crunchy, Neon, Timescale로 이어진 회사들의 인수와 흡수가 있고, 다른 쪽에는 Tom Lane, Bruce Momjian, Christophe Pettus, Markus Winand, David Steele처럼 코드와 책과 도구를 30년 가까이 자기 손으로 들고 있는 개인들이 있습니다.

2026년의 풍경은 회사 쪽이 훨씬 시끄럽지만, 무게는 결국 개인 쪽으로 다시 돌아오리라 봅니다. 클라우드 베어 둘이 같은 분기에 PostgreSQL 회사를 가져가고 하나는 직접 만들었어도, Andres Freund가 SSH 로그인의 500ms 차이로 xz backdoor를 잡아낸 순간이 어느 회사의 분기 실적보다 더 오래 기억될 것입니다.

오픈소스는 죽지 않고 자금이 끊길 뿐이라는 말처럼, 다음 30년의 PostgreSQL은 결국 어떤 사람이 어디서 누구의 자금으로 일하느냐에 달려 있습니다.

부록. 1986~2026 전체 연표​

1980년대​

  • 1986/UC Berkeley에서 POSTGRES 프로젝트 시작 (Michael Stonebraker)

1990년대​

  • 1995/Postgres95 공개. SQL 인터페이스 도입
  • 1996/Postgres95 → PostgreSQL로 이름 변경. Tom Lane/Bruce Momjian 등 코어 합류
  • 1997/PostgreSQL 6.0 첫 정식 릴리스
  • 1997/Joshua Drake, Command Prompt 설립 (미국 몬태나)

2000년대​

  • 2002/Simon Riggs, 2ndQuadrant 설립 (영국/이탈리아)
  • 2004/EnterpriseDB (EDB) 설립
  • 2006/Bruce Momjian, EDB 합류
  • 2009/Christophe Pettus, PGX 설립

2010~2015​

  • 2010/Citus Data 설립
  • 2010-07/EMC, Greenplum 인수
  • 2010-11/Heroku에 PostgreSQL 도입
  • 2010-12-08/Salesforce → Heroku 인수, 약 2억 1,200만 달러
  • 2011-11/Heroku Postgres 분리 제품 출시
  • 2012/Crunchy Data 설립 (Laurence 형제)
  • 2012/Greenplum, Pivotal로 재조직
  • 2014/Peak Equity Partners → EDB 다수 지분 인수
  • 2014/PostgreSQL Korea User Group 출범 (EDB, 다우기술, 펜타시스템)
  • 2015/Tom Lane, Crunchy Data 합류
  • 2015/Timescale 설립
  • 2015/Alexander Korotkov, Postgres Professional 공동 설립

2016~2019​

  • 2016/Aiven 설립 (Saarenmaa, Valtonen, Nousiainen)
  • 2018-04경/AWS → OpenSCG 인수
  • 2019-01-24/Microsoft → Citus Data 인수
  • 2019/Great Hill Partners → EDB 다수 지분 인수

2020~2023​

  • 2020/Supabase 설립 (Copplestone/Wilson)
  • 2020/VMware → Pivotal/Greenplum 인수
  • 2020-09-30/EDB → 2ndQuadrant 인수 완료
  • 2021/Neon 설립 (Shamgunov, Linnakangas, Kelvich)
  • 2022-05/Aiven 시리즈 D, 2억 1,000만 달러
  • 2022-06/Bain Capital → EDB 다수 지분 인수
  • 2023/Alexander Korotkov, Postgres Professional → Supabase
  • 2023-11/Broadcom → VMware 인수 (Greenplum 포함)

2024: 상실의 해​

  • 2024-03-26/Simon Riggs 별세 (영국 Duxford, Cirrus SR22-T 사고)
  • 2024-03-29/Andres Freund, xz Utils backdoor 발견 (SSH 500ms 추적)
  • 2024-05/Broadcom, Greenplum closed-source 전환
  • 2024/Barman 유지보수, 2ndQuadrant → EDB 공식 이관 완료

2025: 클라우드 베어의 분기​

  • 2025-01-21/CloudNativePG, CNCF Sandbox 진입
  • 2025-05-14/Databricks → Neon 인수, 약 10억 달러
  • 2025-06-02/Snowflake → Crunchy Data 인수, 약 2억 5천만 달러
  • 2025-06/Timescale → TigerData 리브랜드
  • 2025-10/Supabase 시리즈 E, 1억 달러, 프리머니 50억 달러 밸류에이션
  • 2025-11-18/Microsoft, Ignite 2025에서 HorizonDB 프리뷰 발표 (자체 개발)

2026: 균열의 신호​

  • 2026-02-24/Snowflake Postgres 정식 출시(GA), Crunchy 엔진 기반, AWS/Azure 일부 리전
  • 2026-04-27/David Steele, pgBackRest archive 선언
  • 2026-04-30/Percona Community, Jan Wieremjewicz: "Open source doesn't die. It gets unfunded."
  • 2026-05 초/Steele, 다중 스폰서 연합 펀딩 모델 가능성 발표
  • 2026-05-12/The Build, "Picking the Lock-In You Want" 발표
  • 2026-05-13/PostgreSQL 2026년 5월 보안 업데이트 (CVE 5건, CVSS 8.8 × 3)

1차 출처​

회사/인수:

커미터/인물:

컨설턴트/메인테이너:

클라우드 전략/미래:

PG19 동적 wal_level

· 약 7분

PostgreSQL 19에서 wal_level이 의미를 바꿨어요: 더 이상 "이 서버가 항상 쓰는 WAL 레벨"이 아니라 하한값이며, 실제 effective level은 그 위에서 slot 상태에 따라 자동으로 움직여요. logical replication slot이 하나 생기면 effective level이 logical로 올라가고, 마지막 slot이 사라지면 다음 checkpoint에서 내려가요.

이 글에서는 The Build의 Christophe Pettus(크리스토프 페투스)가 다룬 "The wal_level You Set Is Not the wal_level You Get"을 한국어로 풀고, cascading standby와 archived WAL에서 운영자가 한 번은 부딪힐 자리들을 함께 살펴봅니다.

보험성 wal_level=logical 의 비용​

기존 PostgreSQL에서 wal_level은 minimal, replica, logical 셋 중 하나로 고정돼 있었습니다. 변경하려면 서버를 재시작해야 했습니다.

운영 패턴이 자연스럽게 굳어졌습니다. 지금 당장 logical replication을 쓰지 않더라도, 나중에 쓸 가능성이 조금이라도 있으면 일단 logical로 잡아둡니다. 그래야 그날 새벽에 슬롯 하나 만들면서 재시작을 잡을 일이 없습니다. "혹시 모르니까" 라는 안전망입니다.

이 안전망에는 비용이 붙습니다.

  • wal_level = logical은 모든 변경에 대해 추가 메타데이터를 WAL에 적습니다. row의 이전 이미지, replica identity 정보, 다중행 변경의 stream 표식 등입니다.
  • 실제로 logical replication slot이 단 하나도 없어도 적힙니다. "지금 안 쓰는데 적어두는 값"이 분 단위로 디스크에 흐릅니다.
  • 운영 환경에 따라 다르지만, replica 대비 WAL 볼륨이 10~30% 더 늘어나는 케이스가 흔히 보고됩니다.
  • WAL 볼륨이 늘면 archive 비용, streaming replication 대역폭, PITR 복구 시간이 같이 늘어납니다.

"The right thing has corners." — Christophe Pettus, The Build

PostgreSQL 19는 이 안전망의 비용을 덜기 위해 wal_level을 "고정 레벨"에서 "최소 보장"으로 바꿨습니다.

PG19의 configured wal_level과 effective wal_level​

PostgreSQL 19에서 서버는 두 개의 WAL level을 가집니다.

configured wal_level은 postgresql.conf에 적힌 값입니다. 여전히 재시작 파라미터이며, "이 서버가 어떤 경우에도 떨어지지 않을 하한값"을 뜻합니다. effective wal_level은 서버가 실제로 지금 WAL에 쓰는 레벨이며, configured 값보다 위로 올라갈 수 있습니다.

규칙은 단순합니다.

상태effective wal_level
configured replica, 유효한 logical replication slot이 하나 이상 있음logical
configured replica, 유효한 logical replication slot 없음replica
configured logicallogical

즉, configured를 replica로 잡아두고도 logical replication slot을 만드는 순간 서버가 알아서 logical로 올라갑니다. 마지막 slot이 사라지면 다음 checkpoint에서 다시 내려갑니다.

상태 전이도​

중요한 지점은 두 군데입니다 — logical 진입 시점은 forced checkpoint 직후이고, 다시 내려가는 시점은 그다음 일반 checkpoint입니다. 두 시점은 비대칭입니다.

동작 단계​

올라갈 때와 내려갈 때를 따로 봅니다.

올라갈 때: logical로의 전이

  1. 운영자(또는 CREATE SUBSCRIPTION)가 첫 logical replication slot 생성을 요청합니다.
  2. 서버는 effective level이 logical 미만이면 즉시 forced checkpoint를 돕니다.
  3. 이 checkpoint 직후의 LSN을 "guaranteed LSN"으로 기록하고, slot은 그 지점부터 시작합니다.
  4. 그 LSN 이후의 WAL은 logical 레벨 메타데이터를 함께 적기 시작합니다.

내려갈 때: replica로의 하강

  1. 마지막 logical slot이 사라집니다.
  2. 서버는 다음 일반 checkpoint까지 그대로 logical을 유지합니다. 즉시 내리지 않습니다.
  3. 그 checkpoint가 돌고 나면 effective level이 configured 값으로 복귀합니다.
  4. 이후 WAL은 다시 replica 메타데이터로 가벼워집니다.

이 비대칭은 의도된 설계입니다. 올라갈 때는 slot이 막 만들어진 WAL을 읽지 못하면 안 되므로 checkpoint를 즉시 실행합니다. 내려갈 때는 별도 checkpoint를 잡아 운영 영향을 만들 이유가 없습니다.

운영 영향​

가장 큰 효과는 WAL 볼륨 절감입니다.

  • "혹시 모르니까 logical"로 잡아둔 클러스터는, configured를 replica로 내리고 PG19로 올리면 그대로 WAL이 줄어듭니다. 실제로 logical을 쓸 일이 생기면 그 순간 자동으로 올라가니 운영 부담은 늘지 않습니다.
  • archive 비용, streaming replication 대역폭, PITR 복구 시간이 모두 따라 줄어듭니다.
  • 클라우드 관리형 PostgreSQL에서는 archive 저장 비용이 매월 청구되는 항목이라 체감이 큽니다.

부차 효과는 logical replication을 사용 중인 동안에만 그 비용을 낸다는 점입니다. 일회성 마이그레이션을 위해 logical을 켰다가 작업을 마치면 비용이 자동으로 사라집니다.

함정과 주의사항​

네 가지를 짚습니다.

첫째, standby의 logical slot입니다. standby는 primary가 만든 WAL을 재생하므로, standby에 만든 logical slot은 primary의 effective level에 기대어 동작합니다. PostgreSQL 19 문서는 configured wal_level이 replica일 때 primary의 마지막 logical slot을 제거하거나 무효화하면 primary의 logical decoding이 꺼지고, 그 결과 standby의 logical slot이 무효화된다고 경고합니다. 무효화는 standby가 WAL 스트림에서 이 변화를 감지하는 즉시 일어납니다. Pettus의 원문은 standby의 effective level이 primary의 slot 상태와 독립적이라고 설명하지만, 이 부분은 공식 문서의 경고를 기준으로 봐야 합니다. standby에서 logical slot을 운영한다면 primary의 마지막 slot을 정리하기 전에 이 영향을 먼저 확인해야 합니다.

Primary의 effective wal_level이 replica로 내려가면 standby의 logical slot이 무효화되는 구조

둘째, archive에 mixed-level WAL이 섞입니다. 같은 archive 디렉토리에 replica 시기의 WAL과 logical 시기의 WAL이 함께 쌓입니다. archive 자체가 깨지는 것은 아니지만, "이 archive는 통째로 logical이다" 같은 가정을 두고 작성한 PITR script가 있다면 다시 봐야 합니다.

셋째, prepared transaction입니다. 2-phase commit 자체의 동작은 변하지 않습니다. 다만 prepared transaction이 많은 환경에서는 logical 전이 시점의 forced checkpoint가 의외로 오래 걸릴 수 있습니다. 테스트 환경에서 한 번은 의도적으로 재현해 보고 timeout 설정을 검토하는 편이 좋습니다.

넷째, 첫 slot 생성 시 checkpoint 대기입니다. 새 subscription이 publisher에 logical slot을 만들면 그 호출이 forced checkpoint를 기다립니다. 평소 checkpoint 부담이 큰 시스템에서는 수십 초가 걸릴 수도 있습니다. 자동화 script가 CREATE SUBSCRIPTION 호출에 짧은 timeout을 걸어뒀다면 PG19 업그레이드 직후 한 번 흔들릴 가능성이 있습니다.

PITR / archive script 점검 포인트​

PostgreSQL 19로 올라가기 전에 한 번씩 확인할 자리들입니다.

archive level과 관련해서는 script가 "이 archive는 logical 레벨"임을 전제로 메타데이터를 파싱하지 않는지 확인합니다. slot 생성 timeout은 자동화 도구의 CREATE SUBSCRIPTION / pg_create_logical_replication_slot 호출에 30초 미만으로 설정되어 있지 않은지 살펴봅니다.

WAL 볼륨 모니터링에서는 PG19로 올린 직후 WAL 생성률이 줄어드는 변화를 정상으로 인식하는지 확인합니다. 감소를 장애로 판단해 알람을 보내지 않도록 해야 합니다. cascading 구성에서는 어느 standby에 logical slot이 있는지, primary의 effective level이 내려갈 때 그 slot이 어떤 영향을 받는지 그림으로 한 번 정리해 두는 편이 좋습니다.

"혹시 모르니까"라는 비용​

logical replication을 본격적으로 쓰기 시작한 계기는 zero-downtime upgrade와 CDC 파이프라인입니다. 한 번 켜놓으면 끝나는 것이 아니라 "혹시 다음 마이그레이션 때 또 쓸지 모르니까" wal_level = logical 그대로 두는 패턴이 굳어졌습니다. 그 사이 WAL 볼륨이 10~30% 더 흐르고, archive 비용과 streaming replication 대역폭이 같이 늘어납니다. 평소에는 의식하지 못하지만 PITR 복구 시간을 재 보면 한 번씩 드러납니다. PostgreSQL 19의 동적 wal_level은 이 "보험 비용"을 시점성 비용으로 바꿉니다. 일회성 마이그레이션이 끝나면 slot만 정리해도 effective level이 자동으로 내려가고 WAL이 가벼워집니다. 운영자가 wal_level을 내릴지 말지 회의에 올릴 일이 없어진다는 점이 PG19가 일상에 주는 가장 큰 차이입니다.

정리​

PG18 이하PG19
wal_level 의 의미고정값하한값 (floor)
logical 전환 비용항상 부담slot 있는 동안만
WAL 볼륨 (logical 미사용 시)풀 부담replica 수준
첫 logical slot 생성즉시forced checkpoint 후
마지막 slot 제거 후 하강n/a다음 checkpoint

PostgreSQL 19는 운영자가 "혹시 모르니까"라는 이유로 영구히 짊어졌던 비용을 시점성 비용으로 바꿔, logical replication이 필요한 그 순간에만 그 비용을 내게 해요. 기본 동작이 더 똑똑해진 변화지만, standby의 logical slot과 archive처럼 effective level 변화의 영향을 받는 곳에서는 모서리가 한 번씩 보여요.

참고 자료​

PG18 autovacuum 슬롯

· 약 6분

PostgreSQL 18에서 autovacuum_max_workers가 드디어 SIGHUP로 받아지는 파라미터가 됐어요. 정확히는 한 파라미터가 둘로 쪼개졌어요 — 시작 시 한 번 예약하는 autovacuum_worker_slots(재시작 필요)와 그 범위 안에서 자유롭게 움직이는 autovacuum_max_workers(reload만으로 적용)예요. 운영자가 vacuum 부하를 보다가 worker 수를 늘리려고 maintenance window를 잡아야 했던 시대가 끝났어요.

이 글은 The Build의 Christophe Pettus(크리스토프 페투스)가 "All Your GUCs in a Row" 시리즈에서 다룬 autovacuum_worker_slots 편을 한국어로 풀고, 함정 한두 가지를 더 짚습니다.

왜 max_workers는 재시작 파라미터였나​

PostgreSQL의 autovacuum launcher는 postmaster가 띄우는 백그라운드 프로세스입니다. launcher가 깨운 worker는 별도 프로세스로 동작하는데, 이 worker들이 자리 잡을 shared memory 구조는 postmaster가 시작될 때 한 번에 예약됩니다. autovacuum_max_workers가 그 크기를 결정했고, 따라서 변경하려면 재시작이 필요했습니다.

이 설정이 운영에서 만든 압박은 두 가지입니다.

첫째, 초기 과다 provisioning. "혹시 모르니 worker 16개 정도 확보해두자"는 식의 보수적 설정이 일반적이었습니다. 평소엔 절반도 안 쓰면서 shared memory를 점유합니다.

둘째, 워크로드 변동 대응 실패. 새 큰 테이블 N개를 한꺼번에 적재하는 야간 배치를 추가하면 vacuum 부담이 갑자기 커집니다. worker를 5개에서 10개로 늘리고 싶어도 그건 다음 maintenance window에서나 가능했습니다. 그동안 dead tuple은 쌓이고, bloat는 자라고, query latency는 흔들립니다.

"The cost of being wrong is a SIGHUP, not an outage." — Christophe Pettus, The Build

PostgreSQL 18은 이 비용을 SIGHUP 한 번으로 줄였습니다.

PG18의 worker_slots와 max_workers 분리​

원래 하나였던 GUC가 둘로 쪼개집니다.

  • autovacuum_worker_slots: postmaster가 시작할 때 예약할 shared memory worker slot의 개수이며 재시작 파라미터입니다.
  • autovacuum_max_workers: 위 slot 범위 안에서 실제로 동시에 깨어 있을 수 있는 worker의 상한이며 SIGHUP로 받아집니다.

쉽게 말하면 worker_slots는 주차장 크기, max_workers는 지금 동시에 받을 차의 수입니다. 주차장은 미리 지어둬야 하지만, 진입 제한은 그때그때 바꾸면 됩니다.

Before / After​

파라미터PostgreSQL 17 이하PostgreSQL 18
worker 상한 GUC 이름autovacuum_max_workersautovacuum_max_workers (역할 변경)
공유 메모리 예약 GUC없음 (= max_workers가 결정)autovacuum_worker_slots
재시작 필요예worker_slots 만 예 / max_workers는 SIGHUP
기본값max_workers = 3worker_slots = 16, max_workers = 3
변경 비용maintenance windowreload

기본값이 worker_slots = 16으로 잡혀 있는 점에 주목할 만합니다. "어차피 사후에 늘리지 못 하니 처음부터 넉넉히 잡아두자"는 의도된 권장입니다.

구조도​

worker_slots는 부팅 때 한 번 결정되고, max_workers는 SIGHUP로 그때그때 위아래로 움직입니다.

동작 메커니즘​

세 가지를 짚어두면 충분합니다.

  1. slot 예약은 postmaster 시작 시 한 번: worker_slots = 16이면 shared memory에 16개의 worker 자리가 잡힙니다. 이 크기는 실행 중 바꿀 수 없습니다.
  2. max_workers > slots는 자동 capping: worker_slots = 16인데 max_workers = 20으로 reload하면 PostgreSQL은 16으로 capping하고 서버 로그에 경고를 남깁니다. 에러가 아니어서 reload는 성공하지만, 의도대로 동작하지 않는다는 신호입니다.
  3. max_workers는 SIGHUP로 즉시 반영: pg_reload_conf() 한 번이면 다음 worker 사이클부터 새 상한이 적용됩니다.

maintenance window 없는 운영 튜닝 흐름​

새 흐름은 단순합니다.

설치할 때 worker_slots는 "최악의 상황에서 필요할 worker 수"에 맞춰 넉넉히 잡습니다. 16이면 대부분 충분하며, shared memory 비용은 worker 1개당 수 KB 수준이라 부담이 작습니다.

평상시에는 max_workers를 보수적으로 설정합니다(예: 3~5). vacuum이 늦지 않으면 그대로 유지합니다. 큰 테이블 적재, partition 증가, dead tuple 누적이 보이면 postgresql.conf에서 max_workers를 올리고 pg_reload_conf()를 실행합니다. 다음 vacuum 사이클부터 worker가 늘어납니다. 부하가 진정되면 다시 max_workers를 내리고 reload하며, shared memory는 그대로 두면 됩니다.

이 흐름에서 중요한 변화는 부하 증가에 대응하는 3번째 단계가 SIGHUP라는 점입니다. vacuum 부하를 더 받기 위해 데이터베이스를 내리고 다시 띄울 필요가 없습니다.

함정과 주의사항​

크게 넷입니다.

첫째, worker_slots의 hard ceiling. 설치 시점에 worker_slots를 짜게 잡으면 사후에 그 위로 올릴 수 없습니다. 16이 적당해 보여도, partition 1만 개에 야간 배치까지 도는 환경이라면 32로 잡아두는 편이 낫습니다. shared memory 예약 비용이 vacuum 지연 비용보다 훨씬 쌉니다.

둘째, worker_slots를 거꾸로 줄이고 싶을 때. 재시작이 필요합니다. "지금 max_workers = 5로 줄어들었으니 slot도 8 정도면 충분하지 않을까"는 자연스러운 생각이지만, 그 효과는 다음 재시작 때만 봅니다. 평소에는 그냥 두고, 다른 재시작 사유가 생겼을 때 함께 조정하는 게 실무적입니다.

셋째, parallel autovacuum과의 관계. PostgreSQL 19에서는 autovacuum_max_parallel_workers가 추가됩니다. 한 vacuum이 인덱스 정리를 위해 추가로 끌어쓰는 worker는 별도 카운팅이라, 동시에 도는 worker 수를 셀 때는 둘을 함께 봐야 합니다. 자세한 내용은 같은 저자의 "Parallel Autovacuum: It's Not About The CPU"에 잘 정리돼 있습니다.

넷째, max_workers를 올릴 때의 메모리 곱셈. worker_slots가 차지하는 shared memory는 slot 1개당 대략 5~20KB 수준입니다 — PGPROC 슬롯, PgBackendStatus 엔트리, LWLock 같은 자투리를 합산한 값입니다. worker_slots = 16으로 잡아도 총 수백 KB 안쪽이라 셋업 부담은 거의 없습니다. 하지만 slot이 깨어나서 실제 vacuum을 도는 동안에는 별도 비용이 붙습니다. autovacuum_work_mem(미설정 시 maintenance_work_mem, 기본 64MB)이 worker 프로세스의 private memory로 잡힙니다. max_workers = 16으로 올린 상태에서 16개가 동시에 도는 순간 OS RSS에 약 1GB가 추가된다는 뜻입니다. 정리하면 worker_slots는 넉넉히 키워도 무방하지만, max_workers는 RAM과 maintenance_work_mem의 곱셈을 같이 보고 결정합니다. "주차장은 크게, 동시 진입은 천천히"가 기준입니다.

maintenance window라는 비용​

OLTP 클러스터를 운영해 봤다면 maintenance window의 무게를 압니다. 사용자에게 공지를 띄우고, 야간 새벽 시간대를 잡고, 운영자 두세 명이 대기하는 비용입니다. autovacuum_max_workers 하나 늘리겠다고 그 비용을 다 치르기는 어렵습니다. 그래서 한참을 견딥니다 — dead tuple이 쌓이고, 큰 테이블이 bloat로 1.5배쯤 부풀고, query plan이 흔들리기 시작한 다음에야 다음 정기 점검에 끼워 넣습니다. PostgreSQL 18의 분리는 이 견디는 구간이 사라진다는 뜻입니다. dead tuple 알람이 뜨면 그날 안에 worker를 두세 개 더 풀어 vacuum을 따라잡게 한 뒤, 부하가 가라앉으면 다시 줄여둡니다. 운영자 입장에서는 vacuum 튜닝이 처음으로 "회의 없이 가능한 일"이 됐습니다.

정리​

PG17 이하PG18
worker 수 변경 비용restartSIGHUP
초기 셋업 부담"충분히 크게" 1회 결정worker_slots만 크게, max_workers는 작게
워크로드 변동 대응다음 maintenance window다음 reload
운영 사고 빈도bloat 누적이 잦음즉시 대응 가능

PostgreSQL 18은 "worker 풀은 사전에 크게, 사용량은 그때그때"라는 평범한 운영 패턴을 vacuum에도 들여왔어요. 작은 변화 같지만 운영 자동화 관점에서는 큰 차이예요. 한밤중 dead tuple 누적 알림에 더 이상 maintenance window를 잡지 않아도 됩니다.

참고 자료​

ClaudeBleed 권한 취약점

· 약 9분

무슨 결함인가​

Anthropic의 Chrome Extension Claude for Chrome(v1.0.69 이하)에서 권한이 전혀 없는 다른 Extension 하나가, 사용자 동의 한 번 없이 Claude 에이전트를 통째로 빌려 Gmail, Google Drive, GitHub의 데이터를 조회하고 외부로 빼낼 수 있는, 발신자 신원 검증이 빠진 결함이 공개됐어요.

LayerX Security가 2026-05-07에 공개했고, 발견자가 붙인 이름은 ClaudeBleed입니다. 2014년 OpenSSL의 Heartbleed, 2017년 Cloudflare의 Cloudbleed 계보를 잇는 "데이터가 새어 나온다"는 작명을 오마주했습니다. Anthropic은 하루 앞선 5월 6일 v1.0.70으로 부분 패치를 배포했지만, 같은 연구자가 약 3시간 만에 우회 경로를 찾아 함께 공개했습니다. 5월 13일 현재까지 추가 패치는 발표되지 않았으며, CVE 번호도 아직 할당되지 않았습니다.

요약 표​

항목내용
취약점 이름ClaudeBleed
영향 제품Claude for Chrome (Chrome 브라우저 Extension)
영향 버전v1.0.69 이하
부분 패치v1.0.70 (2026-05-06, 우회 경로 확인됨)
추가 패치미발표 (2026-05-13 기준)
CVE미할당
발견자 / 공시LayerX Security, 2026-05-07
분류Trust Boundary Violation (CWE-501 계열)
공격 전제사용자가 다른 Chrome Extension 하나를 설치하고 있을 것
사용자 상호작용불필요
권한 요구공격용 Extension에 별도 권한 불필요

externally_connectable은 누구를 신뢰하는가​

Chrome Extension 매니페스트의 externally_connectable 키는 "이 Extension의 메시지 채널을 누가 호출해도 되는가"를 선언합니다. 보통 두 가지 형태로 씁니다.

// 형태 A — 다른 Extension ID로 한정
{
"externally_connectable": {
"ids": ["<신뢰하는 Extension ID>"]
}
}

// 형태 B — 특정 웹 출처(origin)로 한정
{
"externally_connectable": {
"matches": ["*://*.claude.ai/*"]
}
}

Claude for Chrome은 두 번째 형태로, claude.ai 출처에서 실행되는 페이지 스크립트가 Extension에 메시지를 보낼 수 있도록 열어 뒀습니다. 사용자 입장에서 Claude 사이드 패널과 claude.ai가 한 몸처럼 동작해야 하니, 설계 의도는 자연스럽습니다.

문제는 origin이 답할 수 있는 질문의 범위입니다. origin은 "이 코드가 어느 페이지 컨텍스트에서 실행되는가"는 알지만, "그 컨텍스트 안에서 누가 그 코드를 실행시켰는가"는 모릅니다. Chrome의 Extension 모델은 임의의 Extension이 자기 content script를 다른 출처의 페이지에 주입할 수 있게 허용하며, 매니페스트의 "world": "MAIN" 옵션을 켜면 그 스크립트는 페이지 자체의 자바스크립트 컨텍스트에서 돕니다.

여기서 권한 0짜리 Extension이란, manifest.json에 permissions(tabs, storage, cookies 등)도 host_permissions(https://mail.google.com/* 같은 사이트 접근 권한)도 적지 않은 Extension을 말합니다. 사용자가 설치할 때 "이 Extension은 ~ 권한이 필요합니다" 같은 경고 화면이 거의 뜨지 않거나 가장 가벼운 형태로만 뜹니다. 그런데 content_scripts 항목은 권한 요청이 아니라 "어떤 페이지에서 이 자바스크립트를 실행해 달라"는 등록이라, 별도 권한 없이도 매니페스트에 적은 origin(여기서는 *://*.claude.ai/*)에 자기 스크립트를 끼워 넣을 수 있습니다.

즉 권한 0짜리 Extension 하나가:

  1. content script로 claude.ai에 스크립트를 한 장 주입하고,
  2. 그 스크립트가 페이지 컨텍스트에서 chrome.runtime.sendMessage("<Claude Extension ID>", {...}) 한 줄을 실행하면,
  3. Claude Extension 입장에서는 "claude.ai에서 들어온 정상 메시지"로 보입니다.

Claude Extension의 ID(fcoeoabgfenejglbffodgkkbkcdhcgfn)는 Chrome Web Store에 그대로 공개돼 있고, Extension ID는 비밀이 아닙니다. 결과적으로 origin 화이트리스트는 ACL이 아니라 누구나 통과시키는 표지판에 가깝습니다.

LayerX는 이 호출로 도달 가능한 핸들러 중 하나로 onboarding_task를 지목했는데, 이 핸들러는 외부에서 받은 텍스트를 그대로 Claude의 작업 프롬프트로 흘려보냅니다. 즉 메시지 한 통이면 임의 프롬프트 실행과 같습니다.

공격 사슬​

전체 동작은 한 장으로 정리됩니다.

여기까지가 진입입니다. Extension ID와 origin만 알면 누구든 Claude 에이전트의 작업 큐에 항목을 하나 밀어 넣을 수 있습니다. 그 다음 단계, 즉 "어떻게 사용자 몰래 그 작업을 끝까지 수행하게 만드는가"가 아래 4가지 공격 기법으로 나뉩니다.

4가지 공격 기법​

1. Confused Deputy​

Claude Extension은 "claude.ai에서 온 메시지면 정상"이라는 전제로 동작합니다. 그러나 실제 메시지를 만든 주체는 사용자가 띄운 claude.ai 페이지가 아니라, 사용자가 설치해 둔 다른 Extension이 페이지 위에 얹어 놓은 스크립트입니다. 정당한 권한을 가진 대리인(Claude Extension)이 자기 권한 밖의 호출자(악성 Extension)를 위해 일하게 되는, 교과서적인 confused deputy 구도입니다.

이 구도가 위험한 이유는 권한 상승의 비용이 0이라는 점입니다. 공격 Extension은 호스트 권한도, 탭 권한도, storage도 필요 없습니다. content script 하나만 선언하면 끝납니다.

2. 승인 루핑(Approval Looping)​

Claude는 민감한 작업을 수행하기 전 사용자에게 확인을 받는 단계를 거칩니다. 그런데 그 확인이 "지금 이 특정 행동에 대한 동의"가 아니라 "지금 사용자가 활성화한 모드"로만 검사되기 때문에, 같은 채널로 "Yes, proceed"에 해당하는 메시지를 반복 전송하면 확인 흐름을 그대로 통과합니다.

토큰이 특정 행동에 바인딩돼 있지 않은 탓입니다. 한 번 받은 동의가 다음 동의가 필요한 자리에서 다시 통하면, 동의 절차는 이름만 남고 실제 통제력을 잃습니다.

3. DOM 라벨 위장​

Claude 에이전트는 페이지의 DOM을 읽어 어떤 버튼이 무슨 동작인지 판단합니다. 악성 Extension이 DOM을 살짝 바꿔 두면(예를 들어 "외부에 공유" 버튼의 라벨을 "피드백 요청"으로) 에이전트의 인식과 실제 동작이 어긋납니다. 에이전트는 무해한 작업으로 알고 누르지만, 실제 클릭은 원래 핸들러를 그대로 부릅니다.

이 기법은 ClaudeBleed에만 국한되지 않는, 브라우저 안에서 도는 모든 AI 에이전트의 일반 약점에 가깝습니다. 에이전트가 의미를 DOM에서 끌어내는 한, DOM을 만질 수 있는 쪽이 의미를 재정의합니다.

4. Privileged Mode 우회​

Claude for Chrome은 두 가지 모드를 둡니다. Ask before acting(매 행동마다 확인)과 Act without asking(자율 수행). v1.0.70 패치는 Ask 모드에서 외부 메시지가 임의 행동을 일으키지 못하도록 사이드 패널 안에 명시적 승인 흐름을 추가했습니다.

하지만 LayerX는 공격자가 사이드 패널의 초기화 흐름을 가로채 Act without asking 쪽으로 진입시키는 우회 경로를 약 3시간 만에 확인했습니다. 모드 전환이 사용자에게 통보되지 않기 때문에, 사용자는 자기 Extension이 자율 모드로 돌고 있는지 알 길이 없습니다.

요약하면 패치는 증상인 승인 UI 흐름만 가렸을 뿐, 원인인 메시지 발신자 신원 검증 부재는 손대지 못했습니다. 발신자가 누구인지 확인하지 못하는 한 어느 모드에서든 우회 경로는 다시 열립니다.

이 결함으로 가능한 위협 시나리오​

이 결함이 가능하게 만드는 작업은 평범한 사용자 시나리오로 옮기면 그대로 사고로 이어집니다.

  • Gmail의 받은편지함을 요약시켜 외부 주소로 메일 발송
  • Google Drive 비공개 문서에 공유 링크를 발급해 외부 노출
  • GitHub 비공개 리포지토리의 파일 내용을 챗 응답에 끼워 빼내기
  • 사용자 대신 메일 발송, 문서 삭제로 흔적 정리
  • 외부 캘린더 초대, Slack 메시지 발송 같은 OAuth 권한 위임 작업 일반

세 가지가 함께 성립한다는 점이 무겁습니다. 공격 Extension에 별도 권한이 필요 없고, 사용자 상호작용도 필요 없으며, EDR이나 안티바이러스 관점에서는 정상 Extension의 정상 호출처럼 보입니다.

특히 사내 표준 브라우저에 권장 Extension 목록만 통제하는 조직에서는, Claude for Chrome이 그 권장 목록에 있고 사용자가 자율적으로 다른 Extension 하나를 추가했다는 사실만으로 위협 모델이 무너집니다.

공개 타임라인​

날짜이벤트
2026-04-22영향 받는 v1.0.69 배포
2026-04-27LayerX, Anthropic에 결함 보고
2026-04-28Anthropic "이전 보고와 중복, 곧 수정 예정" 응답
2026-05-06v1.0.70 부분 패치 배포
2026-05-07LayerX, 약 3시간 만에 우회 경로 확인 후 공시
2026-05-11외신, 추가 패치 부재 후속 보도
2026-05-13본 글 작성 시점 (추가 패치 미발표)

보고에서 패치까지 9일, 패치에서 우회 공개까지 1일입니다. 빠르게 막은 흔적은 있지만, "누가 메시지를 보냈는지 다시 설계하는" 종류의 수정은 그 사이에 들어가지 못한 모양새입니다.

사용자가 오늘 할 일​

근본 패치가 발표되기 전까지, 권장 조치는 단순합니다.

1) Claude for Chrome Extension 끄기 또는 제거하기​

chrome://extensions에서 Claude for Chrome 항목의 토글을 끄는 것만으로 공격 표면이 즉시 사라집니다. 자주 안 쓴다면 제거가 가장 깔끔합니다. 데스크톱 클라이언트나 웹 claude.ai 사용에는 영향이 없습니다.

2) 끌 수 없다면 민감 세션과 분리하기​

Claude for Chrome을 켜 둔 프로파일에는 Gmail, Google Drive, GitHub 같은 민감 서비스에 로그인하지 않습니다. Chrome 프로파일 분리(다른 사용자) 또는 별도 브라우저(Chrome Canary, Firefox 등)로 일/개인 작업과 AI 에이전트 작업을 분리해 둡니다.

3) 다른 Extension도 함께 점검하기​

ClaudeBleed의 트리거는 "다른 Extension 하나"입니다. 사용 중인 Extension의 수와 신뢰도를 같이 봅니다.

chrome://extensions → 세부 정보 → 사이트 액세스 / 권한

설치 출처가 불분명하거나, 더 이상 안 쓰는 Extension은 이 기회에 정리해 둡니다. content script 권한이 광범위한 Extension(예: <all_urls>)일수록 위험 가중치가 높습니다.

4) 조직 단위 통제가 가능한 경우​

ChromeOS/Chrome Enterprise 환경에서는 ExtensionInstallBlocklist로 Claude for Chrome Extension ID(fcoeoabgfenejglbffodgkkbkcdhcgfn)를 막거나, ExtensionInstallAllowlist로 허용 Extension 목록을 좁힌 뒤 차차 풀어 가는 방식이 안전합니다. BYOD 환경이면 사용자 공지와 함께 자율 비활성화를 권고합니다.

정리​

  • ClaudeBleed의 본질은 origin을 신원으로 착각한 설계에 있습니다. Chrome Extension 모델이 허용하는 content script + MAIN world 조합 앞에서 origin 화이트리스트는 ACL 노릇을 못 합니다.
  • v1.0.70 패치는 승인 UI 흐름을 추가했지만, 메시지 발신자 신원 검증 부재라는 원인은 그대로 남았습니다. 그래서 Act without asking 모드를 강제 진입시키는 우회 경로가 같은 날 다시 열렸습니다.
  • 에이전트가 강할수록 누가 부르는지를 더 정밀히 따져야 합니다. 발신자 인증(서명 토큰, Extension ID 화이트리스트), 행동-바인딩된 일회성 동의가 합쳐져야 비로소 동의가 동의로 기능합니다.
  • 추가 패치 전까지는 Claude for Chrome 사용을 잠시 멈추는 게 가장 단순하고 확실한 완화입니다. 끌 수 없다면 민감 세션과 프로파일을 분리합니다.

이번 사건이 Anthropic만의 문제로 끝나지 않을 가능성도 함께 짚어 둬요. 브라우저 안에서 도는 AI 에이전트 Extension은 모두 같은 함정(외부 메시지 채널, DOM을 통한 의미 인식, 자율 모드의 UX/보안 트레이드오프) 위에 서 있어요. ClaudeBleed는 그 함정 중 첫 번째 사례로 기록될 공산이 커요.

참고​

PG19 SQL/PGQ 그래프 쿼리

· 약 8분
2026-09-28 업데이트

SQL/PGQ 지원은 2026-09-24 PostgreSQL 19 Beta 4 공지에서 revert됐습니다. PostgreSQL 19 정식 버전에는 이 글의 CREATE PROPERTY GRAPH, GRAPH_TABLE 문법이 들어가지 않습니다. 아래 본문은 revert 전 개발 버전 기준 기록입니다. 경위와 컨테이너 비교 결과는 PG19 Beta 4 기능 철회에 정리했습니다.

들어가며​

PostgreSQL 19는 SQL/PGQ(SQL Property Graph Queries) 를 코어에 들였어요. ISO/IEC 9075-16:2023, 즉 SQL:2023 Part 16으로 표준화된 기능이에요.

그래프 워크로드를 다루려면 Neo4j나 ArangoDB 같은 전용 DB를 띄우거나, PostgreSQL 위에 Apache AGE 확장을 올리는 길이 대표적이었습니다. 데이터는 멀쩡히 관계형 테이블에 들어 있는데 권한, 조직도, 추천 같은 일부 쿼리만 그래프 모양으로 풀고 싶을 때는, 그 절반쯤만 필요한 인프라를 들이는 셈이라 늘 어색했습니다.

SQL/PGQ는 그 회색 지대를 노립니다. 기존 테이블 위에 그래프 뷰를 얹고, 표준 SQL 안에서 그래프 패턴 매칭을 그대로 씁니다.

이 글은 PostgreSQL 19 베타 직전 기준으로 작성되었으며, 정식 출시 전까지 일부 문법이 조정될 수 있습니다. 앞선 PostgreSQL 19 새 기능 총정리의 후속편으로 읽으면 좋습니다.

SQL/PGQ가 푸는 문제​

"아이유를 팔로우하는 사용자가 좋아요를 누른 게시물의 작성자"를 표준 SQL로 풀려면 어떻게 써야 할까요. users, follows, liked, posts, authored 다섯 테이블을 차례로 조인해야 합니다.

SELECT DISTINCT author.handle, p.content
FROM users iu
JOIN follows f ON f.followee_id = iu.id
JOIN users follower ON follower.id = f.follower_id
JOIN liked l ON l.user_id = follower.id
JOIN posts p ON p.id = l.post_id
JOIN authored a ON a.post_id = p.id
JOIN users author ON author.id = a.user_id
WHERE iu.handle = '아이유';

동작은 합니다. 그러나 한눈에 들어오지 않습니다. 어느 컬럼이 출발이고 어느 컬럼이 도착인지, 팔로우의 방향이 어디로 향하는지 매번 머릿속에서 다시 짚어야 합니다. 한 홉만 더 늘려도 조인이 두 줄씩 붙습니다.

SQL/PGQ는 이 코드를 시각적 패턴으로 옮깁니다. 정점은 괄호 (), 간선은 꺾쇠 -[]->. 의도가 그대로 보입니다.

다만 PostgreSQL 19의 첫 구현은 고정 깊이 패턴까지입니다. 정량 패턴(+, *, {2,5})과 가변 길이 경로는 다음 릴리스로 미뤄졌습니다. 이 글에서 다루는 범위는 그 첫 구현입니다.

CREATE PROPERTY GRAPH로 그래프 뷰 정의하기​

먼저 그래프를 선언합니다. 데이터는 옮기지 않습니다. 기존 정점 테이블과 간선 테이블을 그래프로 어떻게 볼지를 카탈로그에 기록할 뿐입니다.

아래는 소셜 서비스 도메인을 그래프로 노출하는 예시입니다. 사용자와 게시물을 정점으로, 팔로우, 작성, 좋아요를 간선으로 정의합니다.

CREATE PROPERTY GRAPH social
VERTEX TABLES (
users LABEL person PROPERTIES (id, handle, joined_at),
posts LABEL post PROPERTIES (id, content, posted_at)
)
EDGE TABLES (
follows
SOURCE KEY (follower_id) REFERENCES users (id)
DESTINATION KEY (followee_id) REFERENCES users (id)
LABEL follows PROPERTIES (followed_at),
authored
SOURCE KEY (user_id) REFERENCES users (id)
DESTINATION KEY (post_id) REFERENCES posts (id)
LABEL authored,
liked
SOURCE KEY (user_id) REFERENCES users (id)
DESTINATION KEY (post_id) REFERENCES posts (id)
LABEL liked PROPERTIES (liked_at)
);

세 가지만 짚으면 충분합니다. VERTEX TABLES는 정점 데이터를 들고 있는 실제 테이블이고, LABEL로 그래프상 노드 이름을 지정합니다. 정점 테이블은 여러 개 둘 수 있어서 여기서는 users와 posts 두 종류를 등록했습니다. EDGE TABLES는 간선 데이터를 들고 있는 실제 테이블인데, SOURCE KEY와 DESTINATION KEY가 화살표의 출발과 도착을 결정합니다. 같은 정점을 잇는 follows와 서로 다른 정점을 잇는 authored/liked가 한 그래프 안에 공존합니다. 마지막으로 PROPERTIES는 그래프 패턴에서 노출할 컬럼만 선택합니다. 모든 컬럼을 자동으로 끌어오지는 않습니다.

CREATE PROPERTY GRAPH는 새 테이블을 만들지 않습니다. users, posts, follows, authored, liked는 그대로 남고, 그 위에 social이라는 그래프 객체 하나가 더 생깁니다. DROP PROPERTY GRAPH social로 언제든 제거할 수 있습니다.

기존 관계형 테이블 위에 social property graph 뷰를 정의하는 구조

GRAPH_TABLE과 MATCH로 패턴 매칭하기​

쿼리 쪽 핵심은 GRAPH_TABLE입니다. 그래프 패턴을 받아 관계형 행 집합으로 돌려줍니다. 즉 결과를 다시 일반 SQL의 FROM 절에서 받아 쓸 수 있습니다.

1홉: 아이유를 팔로우하는 사용자​

SELECT handle
FROM GRAPH_TABLE (social
MATCH (iu IS person WHERE iu.handle = '아이유')
<-[f IS follows]-
(follower IS person)
COLUMNS (follower.handle AS handle)
);

화살표가 왼쪽으로 향합니다. "아이유를 팔로우하는 사람들"을 찾는다는 뜻입니다. 같은 의도를 오른쪽 방향으로 쓰려면 패턴의 시작점을 팔로워로 바꾸면 됩니다. 의미는 같지만 가독성은 사람에 따라 갈립니다.

COLUMNS (...) 절은 패턴 안에서 매칭한 값을 일반 컬럼으로 투영합니다. 이름이 곧 결과 컬럼명이 됩니다.

3홉: 팔로워가 좋아요 누른 게시물의 작성자​

SELECT DISTINCT author.handle, p.content
FROM GRAPH_TABLE (social
MATCH (iu IS person WHERE iu.handle = '아이유')
<-[IS follows]-
(follower IS person)
-[IS liked]->
(p IS post)
<-[IS authored]-
(author IS person)
COLUMNS (author.handle, p.content)
);

앞서 여섯 번 조인으로 풀던 쿼리가 패턴 한 덩어리로 줄었습니다. 화살표 방향이 의도와 그대로 일치합니다.

간선 변수 이름은 본문에서 쓰지 않으면 생략해 -[IS liked]->처럼 적어도 됩니다. 간선 속성을 다시 꺼내야 할 때만 -[l IS liked]->처럼 이름을 붙이면 됩니다.

양방향 매칭은 <-[e]->로 씁니다. 다만 PostgreSQL 19에서는 양방향 자유 매칭이 일부 제한되며, 단방향 두 번으로 풀어쓰는 편이 안전합니다.

내부 동작과 rewriter의 역할​

SQL/PGQ는 새 실행 엔진을 들고 오지 않습니다. 파서가 GRAPH_TABLE을 만나면 rewriter가 그래프 패턴을 표준 관계형 트리로 풀어 씁니다. 정점은 RangeTblEntry가 되고, 간선은 출발 테이블, 간선 테이블, 도착 테이블 사이의 3-way 조인이 됩니다. 그 결과 트리는 일반 플래너로 그대로 흘러갑니다.

이 설계가 가져오는 결과는 셋입니다. 우선 기존 인덱스를 그대로 씁니다. 간선 테이블의 (follower_id, followee_id)나 (user_id, post_id) 같은 복합 인덱스가 그래프 쿼리에서도 그대로 활용됩니다. MVCC와 트랜잭션도 자동으로 따라오니, 별도 그래프 엔진이라면 따로 신경 써야 했을 부분이 사라집니다. EXPLAIN도 여전히 의미가 있어서, 결국 보이는 건 일반 조인 플랜이고 평소 쓰던 튜닝 도구가 다 통합니다.

대신 그래프 쿼리의 성능은 결국 조인 최적화에 종속됩니다. 간선이 많고 선택도가 낮은 패턴은 대량 조인이 될 수 있습니다. 인덱스와 통계 정보가 평소처럼 중요합니다.

PostgreSQL 19에 들어간 것 vs 빠진 것​

항목PostgreSQL 19
고정 깊이 패턴 매칭지원
레이블 필터 (v:Person)지원
속성 투영 COLUMNS (...)지원
단방향/다방향 다홉 조합지원
가변 길이 경로 -[e*1..3]->미지원
정량 패턴 +, *, {2,5}미지원
양방향 자유 매칭 일부미지원
BFS/DFS 탐색 순서 지정미지원
CREATE PROPERTY GRAPH DDL지원

정리하면 이렇습니다. 모양이 정해진 그래프 질의는 PostgreSQL 19에서 곧장 풀 수 있고, 모양이 가변인 그래프 질의는 여전히 재귀 CTE의 영역에 남습니다.

가변 길이 지원은 다음 메이저 릴리스의 1차 후보로 거론됩니다. rewriter가 재귀 CTE 또는 자기조인 반복으로 펴는 형태가 유력합니다.

다른 옵션과 비교​

선택지가 늘었으니 정리해 둘 만합니다.

항목SQL/PGQ (PG19)Apache AGENeo4j
설치 방식PostgreSQL 코어PostgreSQL 확장별도 DB
쿼리 언어SQL + GRAPH_TABLEopenCypherCypher
데이터 저장기존 관계형 테이블AGE 전용 그래프 저장소네이티브 그래프
트랜잭션PostgreSQL MVCCPostgreSQL MVCC자체
가변 길이 경로미지원 (PG19 한정)지원지원
그래프 알고리즘—일부풍부
성숙도신규 (코어 첫 진입)중간높음

이미 PostgreSQL에 든 데이터를 가볍게 그래프로 보고 싶다면 SQL/PGQ가 첫 후보입니다. 그래프 자체가 1급 워크로드라면 여전히 Apache AGE나 Neo4j가 더 멀리 갑니다.

언제 쓰면 좋은가​

쓰기 좋은 쪽부터 짚습니다. 가장 자연스러운 건 권한/조직도 탐색입니다. "이 사용자의 상위 N단계 관리자"처럼 깊이가 정해진 질의가 여기 들어갑니다. "내가 산 상품을 산 사람이 산 다른 상품" 같은 가벼운 추천, 친구의 친구 정도로 얕은 소셜 그래프, 기존 BI 쿼리에 그래프 패턴을 끼워 넣는 분석 워크로드도 잘 맞습니다.

반대로 적합하지 않은 경우는 분명합니다. 트리 루트까지 N단계를 거슬러 오르거나 도달 가능한 모든 노드를 탐색하는 것처럼 깊이가 가변인 traversal, PageRank, 중심성, 커뮤니티 탐지 같은 본격 그래프 알고리즘은 이 범위를 벗어납니다. 수억 노드급 그래프도 마찬가지인데, 이쪽은 네이티브 그래프 저장소의 인덱스/캐시 구조가 더 유리합니다.

선택 기준 자체는 단순합니다. 그래프 모양 질의가 늘면 SQL/PGQ로 옮기고, 그래프 워크로드가 본진이 되는 순간 전용 엔진으로 갑니다.

마무리​

PostgreSQL 19의 SQL/PGQ는 화려한 신기능이라기보다 관계형 DB가 그래프 워크로드를 흡수하는 첫 발판에 가깝습니다. 첫 릴리스는 의도적으로 좁습니다. 고정 깊이부터 안정적으로 굳히고, 가변 길이는 다음으로 미루는 그림입니다.

제가 주목하는 지점은 몇 가지입니다. 표준이 코어로 들어와 Apache AGE와 SQL/PGQ가 한 데이터베이스 안에서 공존할 수 있게 됐고, 새 엔진을 들이지 않고 기존 플래너에 얹은 rewriter 기반 설계라 운영 부담도 최소이며, 가변 길이를 일단 빼고 출시한 절제 역시 신기능 폭주 대신 표준 추종을 우선한 결정으로 읽혀요.

9월 정식 출시 전 베타 단계에서 한 번쯤 만져볼 만한 기능으로, 같은 시리즈의 PostgreSQL 19 새 기능 총정리, EXPLAIN ANALYZE RDTSC, 파티션 MERGE/SPLIT도 함께 참고할 수 있어요.

참고 자료​

LazyVim 키맵 치트시트

· 약 5분

이 글은 Vim/Neovim 단축키 시리즈의 세 번째 글입니다.

  1. Vi/Vim 단축키 종합 가이드: 기본부터 실전까지
  2. Neovim 단축키 가이드: Vim을 넘어서는 기능들
  3. LazyVim 키맵 치트시트 ← 현재 글

함께 읽기: LazyVim distro로 갈아타기

들어가며​

LazyVim에서 키 외울 일은 사실 거의 없어요. <Space>를 누르고 잠깐 기다리면 which-key가 카테고리별 메뉴를 띄워 주고, <Space>sk로 키맵을 검색할 수도 있어요. 그래도 매일 자주 누르는 키는 손이 기억하는 편이 빨라요. 이 글은 그 "매일 누르는 키"만 모아 둔 치트시트예요.

Leader 키는 Space입니다. 아래 표의 <Space>가 모두 leader라고 보면 됩니다.

LazyVim Leader 키의 기능 카테고리 지도

직전 수동 셋업의 키맵과는 다른 기본값이 많습니다. 새로 외우거나 같은 키에 다른 의미를 매핑하지 말고, LazyVim 기본을 그대로 쓰는 편을 권장합니다. 이미 합의된 컨벤션을 그대로 쓰는 편이 손이 덜 갑니다.

파일 트리 (explorer)​

새로 설치한 LazyVim의 기본 탐색기는 snacks.explorer입니다. neo-tree는 editor.neo-tree extra로 빠졌고, 예전에 설치한 환경은 neo-tree를 그대로 유지합니다. 아래 키는 두 탐색기에서 같게 동작합니다.

키동작
<Space>e트리 토글 (열기/닫기 + 포커스)
<Space>Ecwd 기준 트리 (root와 다를 때)

트리 안에서:

키동작
<CR>파일 열기
a추가
d삭제
r이름 변경
q닫기
H숨김파일 토글

<Space>e는 가장 자주 누르는 키 중 하나입니다. 토글이라 한 번 더 누르면 닫힙니다.

창 이동 (트리 ↔ 에디터)​

키동작
<C-h> / <C-l>왼/오 창 이동 (트리 ↔ 에디터 복귀에 가장 자주)
<C-j> / <C-k>아래/위 창
<C-w>=창 크기 균등화
<C-w>q현재 창 닫기

<C-w> 접두사를 누르고 다음 키를 눌러도 같은 동작이지만, <C-h/j/k/l> 직접 매핑이 손가락에 더 빠릅니다. LazyVim이 이미 잡아 준 키입니다.

파일/검색 (snacks.picker)​

키동작
<Space><Space>파일 찾기 (가장 빠른 진입)
<Space>ff / <Space>fF파일 찾기 (root / cwd)
<Space>sg내용 grep (live grep)
<Space>fr최근 파일

직전 셋업에서 Telescope의 <leader>ff/<leader>fg 조합을 썼다면 의미가 거의 그대로 옮겨 옵니다. 차이는 두 가지입니다. leader는 동일하게 Space이고, grep 키는 <Space>fg 대신 <Space>sg로 옮겨졌습니다(s = search 카테고리).

가장 짧은 진입은 <Space><Space>입니다. 두 번 두드리면 바로 파일 picker가 뜹니다.

버퍼/탭​

키동작
<S-h> / <S-l>이전/다음 버퍼
<Space>,버퍼 목록에서 선택
<Space>bd현재 버퍼 닫기

<S-h>/<S-l>은 Shift + h/l. 이게 LazyVim에서 버퍼 전환의 표준입니다. 바로 옆 버퍼로 점프할 일이 압도적으로 많아서 두 키만 외워도 대부분 커버됩니다.

코드 (LSP)​

키동작
gd정의로 점프
gr참조 찾기
gI구현으로 점프
K호버 문서
<Space>ca코드 액션
<Space>crrename
<Space>cf포맷
]d / [d다음/이전 진단

gd로 점프한 뒤 <C-o>로 직전 위치로 돌아옵니다. 이건 Vim 자체의 점프 리스트라 LazyVim과 무관하게 동작합니다.

<Space>c 접두사는 Code 그룹이라 외우기 쉽습니다. ca = code action, cr = code rename, cf = code format.

Git​

키동작
<Space>gglazygit 실행
]h / [h다음/이전 hunk
<Space>gbline blame

<Space>gg를 가장 자주 누릅니다. lazygit TUI 안에서 stage, commit, push까지 다 처리합니다. nvim 본체로 돌아오면 gitsigns가 변경된 줄에 사이드 마커를 자동으로 표시해 주고, ]h/[h로 hunk 사이를 점프합니다.

기타​

키동작
<C-/>터미널 토글 (snacks.terminal, <Esc> 두 번 또는 <C-\><C-n>으로 빠져나옵니다)
<C-;>floaterm 토글 (사용자 추가)
<Space>lLazy 플러그인 매니저
<Space>cmMason (LSP/포매터 매니저)
<C-s>저장
<Space>w창 조작 그룹 (<C-w> 프록시)
<Space>qq전체 종료
<Space>uC컬러스킴 변경 (UI에서 라이브 프리뷰)
<Space>hsearch highlight 끄기 (사용자 추가)

<C-/>와 <C-;>는 둘 다 터미널 토글이지만 띄우는 플러그인이 다릅니다(snacks.terminal vs vim-floaterm).

<Space>uC는 테마를 라이브 프리뷰로 바꿔 본 뒤 마음에 드는 것을 영구화할 때 편리합니다. 자세한 컬러스킴 셋업은 distro 갈아타기 글을 참고합니다.

자주 쓰는 명령​

치트시트는 키 위주로 정리했지만, : 명령으로 자주 들어가는 곳도 같이 외워 두면 좋습니다.

명령동작
:LazyExtras추가 언어팩 ON/OFF UI
:MasonLSP/포매터/린터 설치 상태
:LazyHealth, :checkhealth의존성/문제 진단
:RenderMarkdown toggle마크다운 렌더링 잠시 끄기
:colorscheme <name>테마 즉시 전환

학습 팁​

키를 외우려고 할 필요 없습니다. 다음 두 가지만 습관화하면 자연스럽게 익숙해집니다.

  1. <Space> 누르고 잠시 기다리기. f(파일)/b(버퍼)/c(코드)/g(git)/s(검색)/u(UI 토글) 메뉴가 뜨면 첫 글자 한 번 보고 다음 키를 누릅니다. 한 번에 다 외우려 하지 말고 매일 한 카테고리씩 살펴보면 됩니다.
  2. <Space>sk로 키맵 검색. "버퍼 닫는 키가 뭐였더라?" 싶을 때 <Space>sk → "buffer close"를 입력하면 매핑이 바로 뜹니다.

:verbose map <key>도 디버깅 단계에선 유용합니다. 어떤 플러그인이 해당 키를 가져갔는지 표시해 줍니다.

마치며​

LazyVim의 키맵은 이미 합의된 컨벤션입니다. Telescope 시절의 <leader>ff/fg, 파일 탐색기의 <leader>e, gitsigns의 ]h/[h 같은 묶음이 그대로 살아 있고, 거기에 snacks 계열 picker, dashboard, terminal이 같은 카테고리에 얹혀 있습니다. 손이 헷갈릴 일이 거의 없습니다.

새 머신에 LazyVim을 깔고 가장 먼저 손에 익히면 좋은 6개:

<Space><Space> — 파일 찾기
<Space>sg — 내용 grep
<Space>e — 트리 토글
<S-h> / <S-l> — 버퍼 이전/다음
gd — 정의 점프
<Space>gg — lazygit

이 여섯 개만 익숙해지면 매일 쓰는 키는 거의 다 커버됩니다. 나머지는 <Space> 누르고 기다리면 which-key가 알려줍니다.

전체 셋업 맥락은 LazyVim distro로 갈아타기 글에 정리해 두었어요.

참고​

LazyVim 전환기

· 약 9분

이 글은 Neovim 시리즈의 다섯 번째 글입니다.

  1. Neovim 입문: Vim을 넘어서는 첫걸음
  2. Neovim 중급: 생산성을 높이는 기능들
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기
  4. 나의 Neovim 설정 전체 공개
  5. LazyVim distro로 갈아타기 ← 현재 글

함께 읽기: LazyVim 키맵 치트시트

들어가며​

플러그인 23개를 직접 큐레이션해서 1년 가까이 굴렸지만, 결국 LazyVim distro로 갈아탔어요. 플러그인 수는 23개에서 42개로 늘었지만 제가 직접 관리해야 하는 파일은 15개에서 5개로 줄었고, 이 비대칭이 distro의 핵심이에요.

수동 Neovim 셋업과 LazyVim의 관리 범위 비교

직전 셋업 공개 글에서 정리했던 lazy.nvim 수동 구성은 잘 동작했습니다. 다만 새 언어 LSP 하나를 붙일 때마다 mason 설정, conform 포매터, nvim-cmp 소스를 손으로 동기화해야 했고, 새 머신에 그대로 옮길 때마다 의존성이 한두 개씩 빠졌습니다. distro는 그 동기화를 일괄로 위임하고, 제 영역은 "취향 차이가 나는 부분"에만 남기는 구조입니다.

이 글은 LazyVim이라는 distro가 무엇이고, 수동 셋업에서 어떤 흐름으로 옮겼고, 어디에 백업을 두고 어떻게 롤백할 수 있는지를 정리합니다.

distro란 무엇인가​

Neovim distro는 lazy.nvim, LSP, treesitter, 자동완성, UI 같은 표준 기반을 한 묶음으로 깔아주고, 사용자는 그 위에 얇게 커스텀만 얹는 셋업 패턴입니다. 직접 lua 파일을 한 줄 한 줄 짜는 대신, 프레임워크가 정해 둔 슬롯에 옵션이나 추가 spec만 끼워 넣으면 됩니다.

distro분위기
LazyVim가장 표준적/미니멀. lazy.nvim 작성자가 직접 운영. 본 글 대상
LunarVim자체 CLI(lvim)를 가진 비교적 무거운 셋업
NvChadUI/테마 중심, 화려한 시작 화면이 특징
AstroNvim대시보드, 아이콘, 통계 정보가 많은 풍성한 셋업

LazyVim을 고른 이유는 단순합니다. 이미 쓰던 lazy.nvim 위에 그대로 얹히고, 빠지더라도 ~/.config/nvim을 통째로 갈아엎지 않아도 되는 구조이기 때문입니다. distro 자체가 lazy 플러그인 하나처럼 동작합니다.

직전 상태​

수동 lazy.nvim으로 플러그인 23개를 구성한 상태였습니다.

자세한 구성은 나의 Neovim 설정 전체 공개 글에 있습니다. 요약하면:

  • init.lua 1줄 → lua/config/에서 globals/options/keymaps 분리 로드
  • lua/plugins/ 아래 플러그인별 파일 15개 (telescope, neo-tree, lsp, cmp, treesitter, conform, kanagawa 등)
  • 자체 작성한 keyMapper 유틸로 noremap=true silent=true 일괄 적용
  • 테마는 Kanagawa Dragon (저채도 다크)

문제는 없었습니다. 단지 새 언어 추가가 매번 작은 의식이었습니다. Go를 붙이려면 mason.nvim에 gopls를 추가하고, conform.nvim에 gofmt를 등록하고, nvim-lspconfig 호출 라인을 늘리고, treesitter parser 목록도 손봐야 했습니다. 그렇게 해 둔 구성을 새 노트북에 옮기면 :checkhealth에 빨간 줄이 한두 개 떴습니다.

LazyVim이 자동으로 가져오는 것​

LazyVim 15.x를 깔면 다음이 한 번에 따라옵니다.

영역들어오는 것
플러그인 매니저lazy.nvim
파일 탐색/검색snacks.picker, snacks.dashboard, snacks.explorer (neo-tree는 editor.neo-tree extra)
키 안내which-key
LSP, 포매터, 린터nvim-lspconfig + mason 자동 설치
자동완성blink.cmp (또는 nvim-cmp 옵션)
구문 파싱nvim-treesitter
Gitgitsigns, lazygit 연동
UIbufferline, lualine, noice, mini.icons
포매팅conform.nvim + format-on-save

여기에 제가 켠 LazyExtras 6종(go, typescript, markdown, json, yaml, python)이 마저 붙습니다. 합치면 42개입니다. 직접 관리할 필요는 없고, 동작을 바꾸고 싶으면 같은 spec을 opts만 덮어씌우는 식으로 가볍게 끼어듭니다.

제가 짠 파일은 5개만 남았습니다. 나머지는 distro 본체가 가져간 책임입니다.

백업과 롤백 안전망​

distro 전환은 비가역 작업이 아니어야 합니다. Neovim의 상태는 네 군데에 흩어져 있어서, 그걸 다 같이 빼두지 않으면 깔끔한 롤백이 어렵습니다.

기존 nvim 상태 4종을 .backup-2026-05-07 suffix로 옮긴 뒤 새 LazyVim을 깔았습니다.

원본백업
~/.config/nvim~/.config/nvim.backup-2026-05-07
~/.local/share/nvim (164MB, 컴파일된 플러그인)~/.local/share/nvim.backup-2026-05-07
~/.local/state/nvim~/.local/state/nvim.backup-2026-05-07
~/.cache/nvim~/.cache/nvim.backup-2026-05-07

문제 생겼을 때 롤백은 4쌍을 역순으로 mv하면 됩니다. 새 LazyVim을 같은 suffix로 옮긴 뒤 백업을 원래 자리로:

DATE=2026-05-07
mv ~/.config/nvim ~/.config/nvim.lazyvim-$DATE
mv ~/.local/share/nvim ~/.local/share/nvim.lazyvim-$DATE
mv ~/.local/state/nvim ~/.local/state/nvim.lazyvim-$DATE
mv ~/.cache/nvim ~/.cache/nvim.lazyvim-$DATE

mv ~/.config/nvim.backup-$DATE ~/.config/nvim
mv ~/.local/share/nvim.backup-$DATE ~/.local/share/nvim
mv ~/.local/state/nvim.backup-$DATE ~/.local/state/nvim
mv ~/.cache/nvim.backup-$DATE ~/.cache/nvim

LazyVim 전환 전 백업과 문제 발생 시 롤백 흐름

이 안전망이 있어야 distro 전환을 부담 없이 시도해볼 수 있습니다.

새 디렉토리 구조​

~/.config/nvim/
├── init.lua # LazyVim 부트스트랩 (starter 그대로)
├── lua/
│ ├── config/
│ │ ├── options.lua # 사용자 추가: scrolloff=10
│ │ ├── keymaps.lua # 사용자 추가: <leader>h, <C-;>
│ │ ├── autocmds.lua # 사용자 추가: 마크다운 wrap/linebreak/spell=false 등
│ │ └── lazy.lua # LazyVim 부트스트랩 (수정 X)
│ └── plugins/
│ ├── colorscheme.lua # 신규: Solarized + Everforest + Gruvbox Material
│ ├── markdown.lua # 신규: render-markdown.nvim 옵션 강화
│ ├── ufo.lua # 신규: nvim-ufo (LSP 폴딩)
│ ├── floaterm.lua # 신규: vim-floaterm + <C-;>
│ ├── lang-extras.lua # 신규: LazyExtras 6종 일괄 import
│ └── example.lua # starter 동봉 예제 (`if true then return {} end`로 비활성)
├── lazyvim.json # LazyVim 자동 생성
├── lazy-lock.json # lazy.nvim이 잠금 관리
└── stylua.toml # starter 동봉 (lua 포매터 설정)

config/는 LazyVim의 starter 템플릿이 깔아준 그대로입니다. 사용자 영역은 plugins/ 아래 5개 파일과 config/의 keymaps/options/autocmds 추가분이며, 그게 전부입니다.

커스텀 5개 플러그인 파일​

distro가 가져가지 않은 "제 취향"은 다섯 갈래로 정리됐습니다.

lua/plugins/colorscheme.lua​

테마 후보 3종을 동시에 등록했습니다.

직전엔 Kanagawa Dragon 단일 테마였는데, 마크다운 작성 비중이 늘면서 헤딩 위계와 코드블록 배경이 더 또렷한 테마가 필요해졌습니다. 즉시 비교를 위해 셋을 같이 등록했습니다.

  • maxmx03/solarized.nvim (variant=winter) — 기본
  • neanias/everforest-nvim (background=soft)
  • sainnhe/gruvbox-material (background=medium)

런타임에 :colorscheme everforest처럼 즉시 갈아끼울 수 있고, 영구화는 LazyVim 기본 colorscheme 라인을 바꾸면 됩니다. UI에서 라이브 프리뷰로 고르고 싶으면 <Space>uC.

lua/plugins/markdown.lua​

render-markdown.nvim 옵션을 강화했습니다.

LazyExtras lang.markdown이 이미 render-markdown.nvim의 spec을 등록합니다. 거기에 opts만 덮어씌워 가독성 옵션을 강화했습니다.

return {
{
"MeanderingProgrammer/render-markdown.nvim",
opts = {
heading = {
sign = false,
icons = { "◉ ", "○ ", "✸ ", "✿ ", "✤ ", "✜ " },
backgrounds = {
"RenderMarkdownH1Bg", "RenderMarkdownH2Bg",
"RenderMarkdownH3Bg", "RenderMarkdownH4Bg",
"RenderMarkdownH5Bg", "RenderMarkdownH6Bg",
},
},
code = {
style = "full",
position = "left",
width = "block",
left_pad = 2,
},
quote = { icon = "┃" },
bullet = { icons = { "●", "○", "◆", "◇" } },
checkbox = {
unchecked = { icon = "󰄱 " },
checked = { icon = "󰱒 " },
},
pipe_table = { style = "full", alignment_indicator = "━" },
link = { image = "󰥶 ", hyperlink = "󰌹 " },
},
},
}
옵션효과
heading.iconsH1~H6 아이콘으로 위계 구분
heading.backgrounds헤딩 줄 전체 배경 틴트 (테마의 RenderMarkdownH* highlight 그룹 위임)
code.style = "full"코드블록 전체 배경 + 좌측 언어 라벨
code.width = "block"코드블록 너비를 본문보다 좁게 (들여쓰기 효과)
quote.icon인용문 좌측 세로바
bullet.icons리스트 깊이별 글리프 (● ○ ◆ ◇)
checkbox[ ]/[x]를 Nerd Font 글리프로
pipe_table.style = "full"표 경계 전체 표시

색상은 테마의 highlight 그룹에 위임했기 때문에 colorscheme를 바꿔도 헤딩 틴트가 자동으로 따라옵니다.

lua/plugins/ufo.lua​

LSP 인지 폴딩을 설정했습니다.

nvim-ufo를 사용합니다. foldcolumn=0, foldlevel=99로 기본은 모두 펼친 상태로 두고, 필요할 때만 zc/zo로 접고 폅니다.

lua/plugins/floaterm.lua​

<C-;>로 토글하도록 설정했습니다.

LazyVim 기본 <C-/> snacks.terminal과 별개로, 손에 익은 floaterm을 같이 붙였습니다. <C-;>로 토글하며 둘은 충돌하지 않고 병존합니다.

lua/plugins/lang-extras.lua​

LazyExtras를 일괄 활성화했습니다.

return {
{ import = "lazyvim.plugins.extras.lang.go" },
{ import = "lazyvim.plugins.extras.lang.typescript" },
{ import = "lazyvim.plugins.extras.lang.markdown" },
{ import = "lazyvim.plugins.extras.lang.json" },
{ import = "lazyvim.plugins.extras.lang.yaml" },
{ import = "lazyvim.plugins.extras.lang.python" },
}

이 한 파일이 mason에게 marksman, gopls, ts_ls, pyright, ruff, jsonls, yamlls를 자동 설치하도록 위임합니다. 직전 셋업에서 손으로 동기화하던 그 영역입니다.

사용자 영역 추가​

keymaps (config/keymaps.lua)​

LazyVim 기본 키맵으로 이미 커버되는 건 이식하지 않았습니다. 기존에 손에 익은 두 개만 살렸습니다.

키동작
<leader>hsearch highlight 끄기
<C-;>floaterm 토글

이미 LazyVim이 잡아주는 것: <leader>e 파일 탐색기(새로 설치하면 snacks.explorer), <C-h/j/k/l> 패인 이동, < > visual indent stay, <leader>ff 파일 찾기 등. 전체는 LazyVim 키맵 치트시트 참고합니다.

마크다운 filetype 동작 (config/autocmds.lua)​

vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
vim.opt_local.wrap = true
vim.opt_local.linebreak = true
vim.opt_local.conceallevel = 2
vim.opt_local.spell = false
vim.opt_local.cursorline = true
vim.opt_local.signcolumn = "no"
end,
})
옵션효과
wrap = true한글 긴 줄 자동 래핑
linebreak = true단어 경계에서만 래핑 (어절 중간 잘림 방지)
conceallevel = 2마크업 문자(**, # 등) 숨김 (실제로는 render-markdown이 3으로 덮어씀 — 더 강한 숨김. 의도된 동작)
spell = false한글 문서에서 영어 spell 체크 끔
cursorline = true현재 줄 강조 (긴 문서 위치 추적)
signcolumn = "no"좌측 여백 줄여 본문 폭 확보

클립보드는 별도 설정 없음​

macOS 로컬에서는 비주얼 모드에서 y만 눌러도 시스템 클립보드로 복사됩니다. LazyVim 15.x가 clipboard=unnamedplus를 기본값으로 잡아주기 때문입니다.

직전 수동 셋업에서는 제 setup 글에 적었듯 opt.clipboard = "unnamedplus" 한 줄을 직접 넣어야 했는데, distro 전환 후에는 이 줄도 지웠습니다. + 레지스터(시스템 클립보드)와 무명 레지스터(")가 자동 연결돼서 y/p가 양방향 동기화됩니다.

확인:

:set clipboard?

→ clipboard=unnamedplus 출력되면 OK.

provider 레벨 진단:

:checkhealth provider

또는 직접:

:let @+ = 'test'

→ 다른 앱에서 Cmd+V 했을 때 test가 붙으면 provider 정상.

tmux 안에서만 안 될 때는 ~/.tmux.conf에 OSC 52 전달 설정이 필요할 수 있습니다.

set -g set-clipboard on

macOS 로컬 tmux + pbcopy 조합은 보통 이 설정 없이도 동작합니다. SSH 원격 nvim에서는 LazyVim이 SSH 세션을 감지해 이미 clipboard를 빈 값으로 두므로, 따로 끌 필요가 없습니다.

의존성 (brew 설치)​

LazyVim 본체와 무관하게 PATH에 있어야 하는 외부 도구입니다.

도구용도
ripgrep빠른 grep (snacks.picker <leader>sg)
fd빠른 파일 찾기 (snacks.picker <leader>ff)
lazygit<leader>gg git TUI
node, npmLSP 서버들 (ts_ls, yamlls 등)
python3pynvim, mason 도구

mason이 자동 설치하는 도구는 처음 해당 언어 파일을 열 때 받아옵니다: marksman, gopls, ts_ls, pyright, ruff, jsonls, yamlls, lua_ls, markdownlint-cli2, prettier, stylua, gofumpt, goimports, golangci-lint, shfmt 등입니다.

검증 결과​

항목결과
Lazy sync통과 (42개 플러그인 설치)
세 colorscheme 로드통과 (solarized, everforest, gruvbox-material)
마크다운 autocmd 적용통과 (ft=markdown wrap=true cole=3 spell=false)
nvim startup 에러없음

전부 첫 실행에 떨어졌습니다. 백업해 둔 .backup-2026-05-07은 한 달 정도 묵혀 두고, 회귀가 없으면 정리할 예정입니다.

사용 팁​

  • <Space> 누르고 1초 대기 → which-key가 카테고리별 메뉴를 띄웁니다. 가장 빠른 키맵 학습 방법
  • :LazyExtras → 추가 언어팩 ON/OFF UI
  • :Mason → LSP/포매터/린터 설치 상태
  • :LazyHealth, :checkhealth → 의존성/문제 진단
  • :colorscheme <name>으로 테마 즉시 전환. 영구화는 lua/plugins/colorscheme.lua 마지막 블록의 colorscheme = "..." 변경
  • :RenderMarkdown toggle — 렌더링을 잠깐 끄고 원본 마크다운(**bold**, # H1)을 보고 싶을 때

정리​

수동 lazy.nvim 셋업은 잘 동작했고, 그 자체로 충분히 좋았어요. 다만 "제가 직접 관리해야 하는 것"의 표면적이 넓을수록 새 머신/새 언어를 붙일 때마다 작은 의식이 늘어났습니다. distro는 그 표면적을 줄여줍니다. 자동으로 처리될 수 있는 영역(LSP 자동완성, 포매팅, 치트시트, Git UI)을 일괄로 위임하고, 사용자는 "취향 차이가 나는 부분"에만 집중합니다.

LazyVim의 좋은 점은 lazy.nvim 위에 그대로 얹혀 있다는 점입니다. 빠지더라도 ~/.config/nvim을 통째로 갈아엎지 않아도 되고, 위 백업 절차로 언제든 직전 상태로 돌아올 수 있습니다. 그 안전망 위에서 가볍게 시도해볼 만한 전환이었어요.

키맵은 별도 글로 분리했어요: LazyVim 키맵 치트시트.

참고​

Barman 보존 정책

· 약 6분

정책 한 줄이 디스크와 PITR을 결정한다​

보존 정책을 잘못 설정하면 두 방향 중 하나로 사고가 납니다. 너무 좁게 설정해서 PITR을 원하는 시점이 이미 사라져 있는 경우이거나, 너무 넓게 설정해서 Barman 서버 디스크가 폭발하는 경우입니다. 둘 다 운영에서 흔히 만나는 시나리오입니다.

이 글에서는 Barman의 두 가지 보존 정책인 REDUNDANCY n과 RECOVERY WINDOW OF n DAYS를 같은 lab에서 차례로 적용하며 어떤 backup이 언제 사라지는가를 직접 확인합니다. 처음 보는 독자도 따라올 수 있게 환경을 짧게 정리합니다.

호스트역할Barman 라벨
demo-pg01PostgreSQL 17 primary—
demo-barman01Barman 3.18 서버pg01

환경 셋업이 처음이라면 2편을 먼저 보고 오는 게 빠릅니다.

두 정책 한눈에​

항목REDUNDANCY nRECOVERY WINDOW OF n DAYS
기준보유할 backup 개수보장할 PITR 기간
문법 예REDUNDANCY 5RECOVERY WINDOW OF 4 WEEKS
적합 환경backup 빈도가 고정이고 작은 클러스터backup 빈도와 무관하게 PITR 보장이 필요한 운영 환경
함정backup 빈도 변경 시 보존 기간이 달라짐사용 디스크는 backup 빈도/크기에 따라 들쭉날쭉

Barman은 운영 환경에서 RECOVERY WINDOW OF 4 WEEKS를 기본 권장합니다. 운영자가 "몇 개 보관"보다 "몇 주간 PITR 보장"으로 사고하는 게 자연스럽기 때문입니다.

WAL은 backup 정책에 종속된다​

이 원리를 모르고 보존 정책을 만지면 함정에 빠집니다. wal_retention_policy = main(기본값)일 때, Barman은 살아 있는 가장 오래된 backup의 시작점까지 WAL을 보존합니다.

즉 보존 정책이 backup 5개를 남기라고 하면, 가장 오래된 backup의 시작 LSN부터 현재까지의 WAL이 모두 보관됩니다. backup 1개가 OBSOLETE로 정리되는 순간, 그 backup이 의존하던 WAL도 같이 정리 후보가 됩니다.

이 원리 때문에 보존 정책이 넓으면 가장 오래된 backup부터 현재까지의 WAL이 모두 남아 디스크 사용량이 크게 늘 수 있습니다.

가장 오래된 보존 백업이 WAL 보존 시작점을 정하는 구조

시나리오 A: REDUNDANCY 3​

backup 5개를 떠 둔 상태에서 REDUNDANCY 3을 적용해 봅니다.

/etc/barman.d/pg01.conf:

retention_policy = REDUNDANCY 3
sudo -u barman barman cron # 정책 적용 트리거
sudo -u barman barman list-backup pg01

기대 출력 (요약):

pg01 20260506T130005 - F - Wed May 6 13:00:35 2026 - Size: 41.5 MiB - WAL Size: 16.0 MiB - VALID
pg01 20260506T120005 - F - Wed May 6 12:00:35 2026 - Size: 41.5 MiB - WAL Size: 16.0 MiB - VALID
pg01 20260506T110005 - F - Wed May 6 11:00:35 2026 - Size: 41.5 MiB - WAL Size: 16.0 MiB - VALID

두 번째 칸의 F는 backup 유형(full)입니다. 줄 끝의 VALID가 보존 정책 평가 결과입니다. 정책 밖으로 밀려난 20260506T100005와 20260506T090005는 목록에 없습니다. retention_policy_mode = auto(기본값)에서는 barman cron이 OBSOLETE로 판정한 backup과 그에 종속된 WAL을 같은 실행 안에서 디스크에서 제거하기 때문입니다. cron을 실행하기 전에 list-backup을 보면 두 backup 줄 끝에 OBSOLETE가 붙어 있습니다.

여기에는 함정이 있습니다. backup 빈도가 시간당이라면 REDUNDANCY 3은 지난 3시간만 PITR이 가능하다는 뜻입니다. 사고가 6시간 전에 발생했다면 이미 늦습니다.

시나리오 B: RECOVERY WINDOW OF 7 DAYS​

같은 5개 backup을 가진 상태에서 정책을 시간 기반으로 바꿉니다.

retention_policy = RECOVERY WINDOW OF 7 DAYS
sudo -u barman barman cron
sudo -u barman barman list-backup pg01

이번에는 지난 7일 동안의 어느 시점으로도 PITR이 가능하도록 backup과 WAL이 함께 보존됩니다. 보존 기간 밖에 있는 backup만 OBSOLETE로 마크됩니다.

이 정책의 강점은 backup 빈도와 무관하게 PITR 보장이 일정하다는 점입니다. 시간당으로 떠도 일주일치, 일일로 떠도 일주일치를 유지하므로 운영자는 몇 개보다 얼마나 오래된 시점까지 보장할지를 정할 수 있습니다.

다만 디스크 사용량은 backup 빈도/크기에 따라 들쭉날쭉합니다. 시간당 backup으로 7일치를 보관하면 168개의 backup이 누적됩니다(reuse_backup = link로 dedup해도 작지 않습니다).

시나리오 C: minimum_redundancy 안전망​

정책이 너무 공격적으로 잡혀 있으면 모든 backup이 사라지는 사고도 가능합니다. 이를 막는 설정이 minimum_redundancy입니다.

retention_policy = RECOVERY WINDOW OF 1 DAY
minimum_redundancy = 2

RECOVERY WINDOW OF 1 DAY만 있으면 어제 backup 한 개만 남는 시점도 가능한데, minimum_redundancy = 2가 최소 2개는 항상 유지하도록 강제합니다. 정책과 안전망이 충돌할 때 안전망이 이깁니다.

기본값은 0, 즉 안전망이 없습니다. 운영 환경에서는 항상 1 이상으로 두는 편이 안전합니다.

시나리오 D: barman keep으로 영구 보존​

특정 backup을 retention 정책에서 영구 제외하려는 경우가 있습니다. 예를 들면 마이그레이션 직전의 안전 base나 분기 마감 시점입니다.

sudo -u barman barman keep pg01 20260506T090005 --target full

--target 옵션:

값의미
full이 backup과 모든 의존 WAL을 영구 보존하므로 full PITR 가능
standalonebackup 자체만 보존(WAL은 정책 따름), 디스크를 절약하며 backup 시점 복원만 가능
# 보존 대상 확인
sudo -u barman barman list-backup pg01
# → 'KEEP' 마크가 붙은 backup은 retention 정책에서 자동 제외된다

barman keep --release pg01 <backup_id>로 보호를 해제합니다. 운영 가이드에 "위험한 마이그레이션 직전엔 keep --target full을 걸어둔다"를 정착시키면 한 단계 단단해집니다.

OBSOLETE 판정에서 삭제까지​

list-backup 줄 끝에 붙는 보존 상태는 VALID, OBSOLETE, KEEP:FULL, KEEP:STANDALONE 등입니다. backup 자체의 상태(DONE, FAILED 등)와는 별개로, 보존 정책을 평가할 때마다 계산되는 값입니다. DELETED라는 상태는 따로 없습니다. 삭제된 backup은 목록에서 사라질 뿐입니다.

DONE (VALID) ──(barman cron 정책 평가)──▶ OBSOLETE ──(같은 cron 실행)──▶ 삭제 (디스크 정리)

Barman 백업이 OBSOLETE로 판정된 뒤 같은 cron 실행에서 삭제되는 흐름

barman cron은 매 분 한 번 도는데(/etc/cron.d/barman 기본 설정), retention_policy_mode = auto이면 한 번 실행할 때마다:

  1. 보존 정책을 평가해 정책 밖의 backup을 OBSOLETE로 판정
  2. OBSOLETE로 판정된 backup을 같은 실행 안에서 삭제
  3. 종속된 WAL도 같이 정리

정책을 바꾼 뒤 디스크가 줄어드는 시점은 다음 barman cron 실행입니다. 정책을 바꾸고 cron이 돌기 전에 list-backup을 보면 OBSOLETE 표시만 있고 디스크는 그대로라 헷갈리기 쉽습니다.

수동으로 즉시 삭제하려면:

sudo -u barman barman delete pg01 20260506T090005

상황별 결정 가이드​

상황권장
PITR 기간이 SLA에 명시된 운영 환경RECOVERY WINDOW OF N DAYS (또는 WEEKS/MONTHS), SLA 그대로 매핑
backup 빈도/크기가 안정적이고 디스크 예측이 중요한 lab/소규모REDUNDANCY n, 디스크 사용량이 거의 일정
안전망 (정책 사고 대비)minimum_redundancy = 1 또는 2 (권장)
마이그레이션/분기 마감 등 영구 보존이 필요한 시점barman keep <backup-id> --target full
WAL이 별도 정책으로 더 길게 보관 필요wal_retention_policy = main 그대로 (대부분 충분)

정리​

항목내용
두 정책REDUNDANCY n (개수) / RECOVERY WINDOW OF n DAYS (시간)
Barman 권장RECOVERY WINDOW OF 4 WEEKS (운영 환경)
핵심 원리WAL은 살아 있는 가장 오래된 backup까지 보존되며 backup이 줄면 WAL도 줄어든다
안전망minimum_redundancy ≥ 1 (정책 사고 대비)
영구 보존barman keep <backup-id> --target full 또는 standalone
라이프사이클OBSOLETE 판정 후 같은 barman cron 실행에서 삭제 (retention_policy_mode = auto)

보존 정책의 초점은 몇 개를 남기느냐보다 얼마나 오래 PITR을 보장하느냐에 있습니다. 운영자에게는 개수보다 기간으로 사고하는 편이 대체로 자연스럽습니다.

참고 자료​

Barman PITR 워크북

· 약 7분

백업이 아니라 복구가 시험이다​

2편 마지막에 이렇게 적었어요. "백업의 가치는 백업이 아니라 복구에서 결정됩니다." 이 글은 그 한 줄을 직접 돌려보는 워크북이에요. Barman의 PITR target 옵션 4가지를 한 lab에서 한 번씩 시도해 봐요.

처음 보는 독자도 따라올 수 있게 환경을 짧게 정리합니다.

호스트역할Barman 라벨
demo-pg01PostgreSQL 17 primary해당 없음
demo-barman01Barman 3.18 서버pg01

pg01은 Barman 서버 라벨이고, demo-pg01은 실제 호스트네임이므로 두 이름은 별개입니다. 환경 셋업이 처음이라면 2편을 먼저 보는 편이 빠릅니다.

참고로 barman recover와 barman restore는 같은 명령입니다. Barman 3.x에서는 restore가 새 권장 이름이지만 recover도 그대로 동작합니다. 이 글은 2편과 톤을 맞춰 recover로 통일했습니다.

시나리오 준비​

지웠다가 살릴 데이터를 만듭니다.

PITR을 그럴듯하게 돌려 보려면 의도적으로 손상된 시점이 필요합니다. 다음 SQL을 demo-pg01에서 미리 실행해 둡니다.

# T0: 기준 데이터
sudo -u postgres psql <<'SQL'
CREATE TABLE notes (
id int PRIMARY KEY,
body text,
ts timestamptz default now()
);
INSERT INTO notes(id, body)
SELECT g, 'note-' || g FROM generate_series(1, 1000) g;
SQL
# T1 (예: 14:30:00): 안전 시점 — 이후로 되감을 라벨 생성 + XID 기록
sudo -u postgres psql <<'SQL'
BEGIN;
SELECT pg_create_restore_point('safe-state'); -- 시나리오 C 용
SELECT pg_current_xact_id(); -- 시나리오 B 용 — 예: 12345
COMMIT;
SQL
# T2 (예: 14:32:00 이후): 사고 — 테이블 삭제
sudo -u postgres psql -c "DROP TABLE notes;"

이제 notes 테이블이 사라졌습니다. base backup이 T0 이전에 떠 있고 WAL이 계속 수집되고 있다고 가정합니다(2편의 lab 그대로). 이 base backup과 WAL을 가지고 T1 직후 / XID 12345 직후 / 명시 라벨 / base backup 직후 4가지 시점으로 되감아 봅니다.

base backup부터 테이블 삭제까지의 PITR 복구 좌표

복원 대상은 빈 디렉토리입니다. 매 시나리오 사이에 비우고 시작합니다.

sudo -u barman rm -rf /var/lib/barman/restore && \
sudo -u barman mkdir -p /var/lib/barman/restore

시나리오 A: --target-time​

시계로 되감는 방식이 가장 직관적입니다. 사고 직전 시각으로 돌립니다.

sudo -u barman barman recover pg01 latest /var/lib/barman/restore \
--target-time "2026-05-06 14:32:00"

기대 동작은 14:32:00 시점까지 WAL을 replay한 뒤 멈추는 것입니다. notes 테이블은 살아 있고, DROP TABLE에는 도달하지 않습니다.

타임존은 Barman 서버의 시스템 시간대가 기본입니다. 다른 TZ로 명시하려면 2026-05-06 14:32:00+09 형태로 붙입니다. 운영에서는 항상 명시하는 편이 안전합니다.

"Recovery targets must be a value after the end of the backup." — base backup 종료(End time) 이전 시점으로는 복구할 수 없습니다. 시점이 End time보다 이르면 즉시 실패합니다.

시나리오 B: --target-xid​

트랜잭션 ID로 되감습니다.

시계는 누적된 운영 환경에서 의외로 부정확합니다. Barman 호스트와 PostgreSQL 호스트의 시계가 살짝 어긋나 있거나, 동시에 여러 트랜잭션이 들어올 수 있기 때문입니다. 트랜잭션 ID(XID)가 가장 정확한 좌표입니다.

sudo -u barman barman recover pg01 latest /var/lib/barman/restore \
--target-xid 12345

XID 12345가 commit된 직후까지 replay하고 멈추는 동작을 기대합니다. T1 시점에 기록해 둔 XID가 사고 직전 마지막 안전 트랜잭션이므로 여기에서 멈추면 notes는 살아 있습니다.

항목내용
강점시점이 논리적으로 정확하며 시계 오차/동시 트랜잭션의 영향이 없음
약점사고 직후가 되어서야 그 XID를 알게 되므로 사고 직전에 미리 기록해 두는 편이 이상적
보조pg_waldump로 WAL을 검사해 commit 레코드의 XID 시퀀스 추적 가능

--exclusive 플래그를 같이 주면 그 XID 직전까지만 replay합니다(그 트랜잭션 자체는 제외). 기본값은 그 XID 포함입니다.

시나리오 C: --target-name​

명시한 라벨로 되감습니다.

운영 중 위험한 작업 직전에 라벨을 만들어 두는 패턴입니다.

-- 사고 직전(T1)에 미리 만들어 둔 라벨
SELECT pg_create_restore_point('safe-state');
sudo -u barman barman recover pg01 latest /var/lib/barman/restore \
--target-name 'safe-state'

safe-state restore point까지 replay하고 멈추는 동작을 기대합니다.

이 옵션의 가치는 언어가 자연스럽다는 점입니다. 시각/XID는 사고 후 재구성해야 하지만 라벨은 팀이 기억할 만한 이름입니다. before-migration-2026q2, pre-DROP-INDEX-experiment 같은 이름을 쓸 수 있습니다. 운영 가이드를 "위험한 ALTER 직전엔 restore point부터 만든다"로 정착시키면 PITR이 한층 평이해집니다.

시나리오 D: --target-immediate​

base backup 직후로 되감습니다.

가장 단순한 옵션입니다. 마지막 base backup이 일관성을 확보하는 그 시점까지만 replay하고 멈춥니다. 즉 base backup 종료 직후의 클러스터 상태로 일어섭니다.

sudo -u barman barman recover pg01 latest /var/lib/barman/restore \
--target-immediate

"가장 최근 base backup의 그 순간으로 일단 돌려놓고, 이후 사고 영향을 분리해 분석하고 싶다" 같은 forensic 시나리오에서 사용합니다. WAL replay를 최소화해 빠르게 일관성 상태에 도달합니다.

그 외 옵션 빠른 참조​

옵션의미비고
--target-lsnLSN(3/64000000 형식)으로 되감기XID보다 더 세밀, 특정 WAL 위치를 정확히 알 때
--target-tli특정 timeline으로 복구latest / current / 숫자 ID
--target-action도달 후 동작: shutdown / pause / promote미지정 시 PostgreSQL이 paused 후 운영자 결정 대기
--exclusivetarget 직전까지(target 자체 제외) replay기본은 target 포함
--standby-modereplica로 일어나도록 standby.signal 생성target과 무관한 옵션

--target-action이 특히 중요합니다. 지정하지 않으면 paused 상태가 되어, 운영자가 SELECT pg_wal_replay_resume()을 직접 호출할 때까지 새 트랜잭션이 돌지 않습니다. 이걸 모르면 "왜 안 살아나지" 하고 한참 헤매게 됩니다.

backup_id=auto(가장 최근 backup 자동 선택)일 때는 제약이 있습니다. --target-time, --target-lsn, --target-tli만 허용됩니다. --target-xid나 --target-name을 쓰려면 명시적인 backup-id를 지정해야 합니다(barman list-backup pg01로 확인).

시점을 정하는 법​

barman show-backup과 pg_waldump로 좌표를 찾습니다.

PITR의 절반은 어디로 되감을지 정하는 일입니다. 두 명령어가 그 좌표를 줍니다.

sudo -u barman barman show-backup pg01 latest

출력에서 다음 항목을 봅니다.

항목의미
Begin time / End timebase backup 경계이며 그 이전 시점에는 도달 불가
Begin LSN / End LSNbase backup의 LSN 경계이며 --target-lsn 사용 시 기준점
Begin Offset / End OffsetWAL 파일 내 위치

WAL을 더 세밀하게 보려면 pg_waldump로 commit 레코드를 확인합니다. streaming/에는 수신 중인 .partial 파일만 머물고, 완성된 WAL은 wals/<앞 16자리>/ 아래로 아카이브됩니다. 아래 명령은 Barman 호스트에 PostgreSQL 17 클라이언트 패키지(pg_waldump 포함)가 설치돼 있다는 전제입니다. Barman에 WAL 압축을 설정했다면 파일을 먼저 풀어야 합니다.

sudo -u barman /usr/pgsql-17/bin/pg_waldump \
/var/lib/barman/pg01/wals/0000000100000000/000000010000000000000004 \
| grep COMMIT | head

각 줄에 LSN, XID, timestamp가 함께 나오므로, 사고 직전의 어떤 좌표를 사용할지 골라잡기 쉬워집니다.

자주 만나는 함정 5가지​

증상원인해결
Recovery targets must be a value after the end of the backuptarget이 base backup 종료 이전barman show-backup의 End time 이후로 지정하거나 더 오래된 backup 사용
복구 후 PostgreSQL이 안 살아남--target-action 미지정으로 paused 상태SELECT pg_wal_replay_resume(); 또는 처음부터 --target-action promote
timeline mismatch이전 복구 후 새 timeline으로 진입--target-tli latest 또는 명시적 timeline ID
target-name 못 찾음restore point가 현재 timeline의 WAL에 없음pg_create_restore_point가 해당 timeline에 기록됐는지 확인
barman recover가 아무 진행 안 함--remote-ssh-command로 cross-host 복원인데 SSH 키 미설치barman 사용자에서 postgres@<host>로 키 기반 접속 미리 잡기

정리​

항목내용
4가지 target--target-time (시계) / --target-xid (XID) / --target-name (라벨) / --target-immediate (base 직후)
가장 정확XID이며 시계 오차/동시 트랜잭션과 무관
가장 운영 친화--target-name이며 위험 작업 직전 pg_create_restore_point를 만드는 패턴
시점 결정barman show-backup + pg_waldump 조합
필수 동반--target-action(기본 paused), --exclusive(stop-before vs include)
backup_id=auto 제약--target-time, --target-lsn, --target-tli만 허용하며 그 외는 명시 backup-id 필요

PITR은 두 단계로 나뉘어요. 시점을 정하는 일이 반이고 복구하는 일이 반인데, 보통은 시점을 정하는 쪽이 더 어려워요.

참고 자료​

Barman rsync 모드

· 약 7분

streaming은 알겠고, rsync는 언제 쓰나​

2편에서 Barman의 streaming-only 모델을 셋업했어요. 이 글에서는 같은 lab을 rsync 모델로 바꾸거나 처음부터 rsync로 셋업하고, 왜 rsync를 골랐는지 정리해 보려고 해요.

처음 보는 독자도 따라올 수 있게 환경을 짧게 정리합니다.

호스트역할Barman 라벨
demo-pg01PostgreSQL 17 primary해당 없음
demo-barman01Barman 3.18 서버pg01

pg01은 Barman 서버 라벨이고, demo-pg01은 실제 호스트네임입니다. 환경 셋업이 처음이라면 2편을 먼저 보고 오는 편이 빠릅니다.

rsync 모델 vs streaming 모델 한눈에​

항목rsync 모델 (이 글)streaming 모델 (2편)
도입 시기Barman 1.x (2012-)Barman 2.0 (2016-)
전송 채널SSH + rsyncPostgreSQL streaming replication
WAL 수집archive_command (WAL 세그먼트 완료 시 push)pg_receivewal (실시간 stream)
의존양방향 SSH 키replication slot, replication user
증분 dedup하드링크 (reuse_backup = link)(PG 17+ 블록 레벨 점진 이행)
병렬 복사parallel_jobs = N(단일 stream)
적합 환경베어메탈/VM, SSH 통제 가능컨테이너/K8s, SSH 미허용

이 글의 주제는 다섯/여섯 번째 행에 있는 하드링크 dedup과 parallel_jobs입니다. 둘 다 rsync 모델 고유의 강점입니다.

양방향 SSH 셋업​

rsync 모델은 두 방향의 SSH가 필요합니다.

방향용도
barman@demo-barman01 → postgres@demo-pg01base backup 시 rsync로 데이터 디렉토리 풀링
postgres@demo-pg01 → barman@demo-barman01archive_command로 WAL을 Barman에 푸시

비밀번호 없이 통과해야 cron이 자동으로 돌므로 SSH 키 기반입니다.

Barman rsync 모드의 양방향 SSH 흐름

첫 번째 방향은 demo-barman01의 barman 사용자에서 설정합니다.

sudo -u barman ssh-keygen -t ed25519 -N "" -f ~barman/.ssh/id_ed25519 \
-C "barman@demo-barman01"

# 공개키를 demo-pg01의 ~postgres/.ssh/authorized_keys에 등록
sudo -u barman ssh-copy-id postgres@demo-pg01

# 검증 — 비밀번호 없이 통과해야 함
sudo -u barman ssh postgres@demo-pg01 'echo ok'

두 번째 방향은 demo-pg01의 postgres 사용자에서 설정합니다.

sudo -u postgres ssh-keygen -t ed25519 -N "" -f ~postgres/.ssh/id_ed25519 \
-C "postgres@demo-pg01"
sudo -u postgres ssh-copy-id barman@demo-barman01
sudo -u postgres ssh barman@demo-barman01 'echo ok'

운영 환경에서는 authorized_keys에 from="..." 호스트 제약이나 command="..." 락다운을 거는 편이 안전합니다. 이 글은 lab이라 단순화했습니다.

PostgreSQL 측의 archive_command 활성화​

/var/lib/pgsql/17/data/postgresql.conf 핵심 항목:

listen_addresses = '*'
wal_level = replica
archive_mode = on
archive_command = 'barman-wal-archive -U barman demo-barman01 pg01 %p'

barman-wal-archive는 barman-cli 패키지의 명령이라 demo-pg01에 따로 설치해야 archive_command가 동작합니다.

# demo-pg01에서 실행
sudo dnf install -y barman-cli

내부적으로 SSH로 barman@demo-barman01에 접속해 WAL 파일을 /var/lib/barman/pg01/incoming/에 정확히 전달합니다 — cp나 직접 scp하는 것보다 안전합니다.

pg_hba.conf에는 Barman의 conninfo 접속을 열어 둡니다. replication slot은 rsync 모델에서 필수가 아니지만, barman check가 PostgreSQL 메타데이터를 읽으려면 이 접속이 필요합니다.

host postgres barman demo-barman01 scram-sha-256

PostgreSQL 재시작 후 사용자 생성:

sudo systemctl restart postgresql-17
sudo -u postgres psql <<'SQL'
CREATE USER barman WITH ENCRYPTED PASSWORD 'changeme';
GRANT pg_read_all_settings, pg_read_all_stats TO barman;
GRANT EXECUTE ON FUNCTION pg_backup_start(text, boolean) TO barman;
GRANT EXECUTE ON FUNCTION pg_backup_stop(boolean) TO barman;
GRANT EXECUTE ON FUNCTION pg_switch_wal() TO barman;
GRANT EXECUTE ON FUNCTION pg_create_restore_point(text) TO barman;
SQL

streaming 모델과 달리 REPLICATION 속성/replication slot은 생성하지 않습니다.

Barman 측의 backup_method = rsync 설정​

/etc/barman.d/pg01.conf를 다음과 같이 설정합니다.

[pg01]
description = "Production PostgreSQL primary (rsync mode)"
ssh_command = ssh postgres@demo-pg01
conninfo = host=demo-pg01 user=barman dbname=postgres
backup_method = rsync
parallel_jobs = 2
reuse_backup = link
archiver = on
retention_policy = RECOVERY WINDOW OF 4 WEEKS

핵심 다섯 줄의 의미는 이렇습니다.

키의미
backup_method = rsyncbase backup 때 SSH+rsync로 데이터 디렉토리를 가져옵니다
ssh_command = ssh postgres@demo-pg01rsync가 사용할 SSH 명령입니다. barman 사용자에서 postgres@demo-pg01로 접속합니다
parallel_jobs = 2병렬 rsync worker 수입니다. 디스크/네트워크 여유에 맞춰 조정합니다
reuse_backup = link이전 backup에서 변경되지 않은 파일을 하드링크로 재사용합니다. 핵심 dedup 옵션입니다
archiver = onarchive_command로 들어오는 WAL을 자동으로 처리합니다

streaming 모델의 streaming_conninfo, streaming_archiver, slot_name 키는 모두 사용하지 않습니다.

설정 검증:

sudo -u barman barman check pg01

receive-wal running은 streaming 모델 전용이므로 rsync 모드에서는 항목이 빠지거나 disabled로 나옵니다. 그 외 항목이 모두 OK면 셋업이 끝난 것입니다.

첫 backup + 하드링크 dedup 측정​

# 첫 base backup
sudo -u barman barman backup pg01

기대 출력 (요약):

Starting backup using rsync-over-ssh method for server pg01 ...
Copy done (time: 12 seconds)
Backup size: 41.5 MiB

이번에는 의도적으로 작은 변경만 만든 뒤 두 번째 backup을 수행합니다.

# 데이터 일부 변경 (작은 테이블 하나 추가)
sudo -u postgres psql -c "CREATE TABLE dedup_test AS SELECT g AS id FROM generate_series(1, 1000) g;"

# 두 번째 base backup — reuse_backup = link 가 동작
sudo -u barman barman backup pg01
sudo -u barman barman list-backup pg01

이제 디스크 사용량을 두 가지 방식으로 잽니다.

# 실제 디스크 사용량 (하드링크는 한 번만 카운트)
sudo du -sh /var/lib/barman/pg01/base/
# 예: 42.0 MiB

# 논리 사용량 (-l: 하드링크를 중복 카운트해 backup별로 따로 합산)
sudo du -shl /var/lib/barman/pg01/base/
# 예: 83 MiB

두 측정값의 차이인 41 MiB ≈ 첫 backup 크기가 dedup으로 절약된 디스크 공간입니다. 두 번째 backup은 변경된 파일만 새로 복사되고 나머지는 첫 backup 파일에 hardlink로 연결되므로, backup 횟수가 늘수록 누적 절약 효과가 커집니다.

두 백업이 변경 없는 파일의 디스크 데이터를 공유하는 하드링크 구조

rsync를 선택하는 기준 4가지​

기준rsync 유리streaming 유리
환경베어메탈/VM, SSH 통제 가능컨테이너/K8s, SSH 미허용
디스크 절약하드링크 dedup → 동일 backup 다수 보관 시 절약 큼블록 레벨 dedup은 PG 17+로 점진 이행 중
병렬화parallel_jobs로 N개 worker단일 stream
운영 부담양방향 SSH 키 관리replication slot 관리

베어메탈/VM에서 디스크 효율과 병렬 복사가 중요하면 rsync가 잘 맞습니다. 컨테이너/K8s에서 SSH를 피하고 운영을 단순화하려면 streaming이 유리합니다. 둘 다 가능한 환경이라면 운영자의 SSH 키 관리 부담이 적은 streaming이 무난합니다.

streaming과 rsync 사이의 전환​

같은 라벨로 두 모델을 동시에 운영할 수는 없습니다. backup_method는 하나만 지정할 수 있기 때문입니다. 전환 절차는 다음과 같습니다.

# 1단계: 기존 라벨 비활성화
[pg01]
active = false
...
# 2단계: 새 conf 파일로 다른 라벨 만들기 (예: pg01-rsync)
[pg01-rsync]
backup_method = rsync
...

새 라벨에 backup이 안정적으로 들어오는 것이 확인되면, 보통 1주일+ 경과/복구 리허설 1회 이상을 기준으로 기존 라벨을 폐기합니다.

운영 환경에서는 기존 backup을 즉시 버리지 않습니다. 보존 정책에 따라 자연 만료될 때까지 두 라벨을 함께 보관하면 이전 시점 PITR이 필요할 때 안전망이 됩니다.

정리​

항목내용
백업 모델backup_method = rsync, SSH+rsync 기반
WAL 수집archive_command = 'barman-wal-archive ...'
SSH 방향양방향 (rsync용 + archive_command용)
핵심 dedupreuse_backup = link, 하드링크 기반이며 백업 횟수가 누적될수록 절약 큼
병렬 복사parallel_jobs = N
streaming과의 관계동시 사용 불가 (라벨당 한 모델), 전환 시 새 라벨로 병행 운영 후 폐기

streaming은 운영 단순화에 강하고 rsync는 디스크 효율과 병렬화에 강해요. 환경 때문에 한쪽을 골라야 하는 상황이 아니라면 streaming부터 시도해 보는 편이 무난해요.

참고 자료​