<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://dbalog.dev/blog</id>
    <title>dbalog.dev Blog</title>
    <updated>2026-08-31T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://dbalog.dev/blog"/>
    <subtitle>dbalog.dev Blog</subtitle>
    <icon>https://dbalog.dev/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Hugo 7사이트, Docusaurus 통합]]></title>
        <id>https://dbalog.dev/blog/hugo-to-docusaurus-migration</id>
        <link href="https://dbalog.dev/blog/hugo-to-docusaurus-migration"/>
        <updated>2026-08-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Hugo 사이트 일곱 개를 Docusaurus 하나로 통합하며 선택 기준과 두 가지 문제를 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p><a class="" href="https://dbalog.dev/blog/hugo-papermod-setup">Hugo PaperMod 블로그 구축</a>에서 시작한 블로그를 Hugo에서 Docusaurus로 마이그레이션했어요. 원래 한 도메인에서 블로그 2개와 문서 노트 5개, 모두 Hugo 사이트 7개를 따로 돌렸는데 영역마다 빌드와 설정을 챙기는 일이 점점 무거워졌어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="docusaurus-소개">Docusaurus 소개<a href="https://dbalog.dev/blog/hugo-to-docusaurus-migration#docusaurus-%EC%86%8C%EA%B0%9C" class="hash-link" aria-label="Docusaurus 소개에 대한 직접 링크" title="Docusaurus 소개에 대한 직접 링크" translate="no">​</a></h2>
<p><a href="https://docusaurus.io/" target="_blank" rel="noopener noreferrer" class="">Docusaurus</a>는 Meta가 만든 React 기반 오픈소스 정적 사이트 생성기입니다. 코드는 <a href="https://github.com/facebook/docusaurus" target="_blank" rel="noopener noreferrer" class="">facebook/docusaurus</a>에 공개되어 있습니다. 일반 블로그보다 버전 관리, 사이드바, 문서 탐색 같은 문서 사이트 기능에 초점을 맞춥니다. <code>docs</code> 플러그인을 여러 인스턴스로 구성할 수 있어 한 사이트 안에서도 문서 영역별 경로와 사이드바를 독립적으로 운영합니다. 블로그 플러그인 역시 여러 개를 띄울 수 있으므로 성격이 다른 블로그를 분리하기 좋습니다. Mermaid, 로컬 검색 플러그인, 다크 모드까지 문서 사이트에 필요한 기능을 생태계 안에서 함께 구성할 수 있습니다.</p>
<p><a href="https://dbalog.dev/assets/files/docusaurus-home-1a18ba4431794da1485a8c7ab35ee4c0.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Docusaurus 공식 사이트" src="https://dbalog.dev/assets/images/docusaurus-home-thumb-e548e4c9eee0d05bd6b283fb191ebf64.png" width="480" height="276" class="img_ev3q"></a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="후보와-선택-기준">후보와 선택 기준<a href="https://dbalog.dev/blog/hugo-to-docusaurus-migration#%ED%9B%84%EB%B3%B4%EC%99%80-%EC%84%A0%ED%83%9D-%EA%B8%B0%EC%A4%80" class="hash-link" aria-label="후보와 선택 기준에 대한 직접 링크" title="후보와 선택 기준에 대한 직접 링크" translate="no">​</a></h2>
<p>첫 번째 후보는 <a href="https://gohugo.io/" target="_blank" rel="noopener noreferrer" class="">Hugo</a>를 유지하고 <a href="https://imfing.github.io/hextra/" target="_blank" rel="noopener noreferrer" class="">Hextra</a> 단일 사이트로 합치는 방식이었습니다. 기존 도구를 그대로 쓰므로 마이그레이션 비용은 가장 적지만, 문서 중심 테마라 블로그 2개의 레이아웃을 나누어 살리기에는 아쉬웠습니다.</p>
<p>Docusaurus는 독립된 문서 영역 여러 개와 블로그 여러 개를 공식 플러그인의 다중 인스턴스로 구성할 수 있습니다. 문서 노트 5개와 블로그 2개라는 저의 구조에 이 방식이 정확히 맞았습니다.</p>
<p><a href="https://starlight.astro.build/" target="_blank" rel="noopener noreferrer" class="">Astro Starlight</a>도 빠르고 깔끔한 문서 도구였지만 기본 구조는 단일 문서 사이트에 가깝고, 블로그에는 서드파티 플러그인이 필요했습니다. 결국 새 기술의 화려함보다 지금 가진 영역을 무리 없이 한 프로젝트에 담을 수 있는지를 기준으로 Docusaurus를 골랐습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="힘들었던-점">힘들었던 점<a href="https://dbalog.dev/blog/hugo-to-docusaurus-migration#%ED%9E%98%EB%93%A4%EC%97%88%EB%8D%98-%EC%A0%90" class="hash-link" aria-label="힘들었던 점에 대한 직접 링크" title="힘들었던 점에 대한 직접 링크" translate="no">​</a></h2>
<p>첫 번째는 자동 변환의 사각지대였습니다. 변환 작업이 커밋된 파일을 기준으로 진행되면서 커밋하지 않은 글 11개가 결과에서 빠졌고, 배포 후 운영에서 404로 드러났습니다. 전체 변환을 다시 돌리면 이미 손본 글까지 덮을 수 있어 변환 스크립트에 증분 모드를 추가하고 누락된 글만 복구했습니다.</p>
<p>두 번째는 구 URL 보존이었습니다. 기존 주소 1,100여 개를 새 경로로 보내는 301 map을 만들었는데, URL 인코딩된 한글 태그 주소가 긴 key가 되면서 Nginx의 <code>map_hash_bucket_size</code> 기본값을 넘겼습니다. 설정 자체는 정상이었지만 reload가 실패했고, bucket 크기를 늘린 뒤에야 기존 주소를 유지한 채 전환할 수 있었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="통합-후-모습">통합 후 모습<a href="https://dbalog.dev/blog/hugo-to-docusaurus-migration#%ED%86%B5%ED%95%A9-%ED%9B%84-%EB%AA%A8%EC%8A%B5" class="hash-link" aria-label="통합 후 모습에 대한 직접 링크" title="통합 후 모습에 대한 직접 링크" translate="no">​</a></h2>
<p>홈은 일곱 영역을 안내하는 카드로 바꿨습니다. 상단 메뉴에서 블로그와 문서 노트를 오갑니다.</p>
<p><a href="https://dbalog.dev/assets/files/dbalog-home-3c271ddfd0d0d7e16859384dac5ecff6.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="통합 후 dbalog.dev 홈" src="https://dbalog.dev/assets/images/dbalog-home-thumb-b10a2a9d1b116287ff7ca8df9a5a3cc0.png" width="480" height="276" class="img_ev3q"></a></p>
<p>문서 영역은 좌측 사이드바와 우측 목차가 있는 Docusaurus 기본 문서 레이아웃을 그대로 씁니다.</p>
<p><a href="https://dbalog.dev/assets/files/dbalog-docs-be7fcc086894b88cba645b42d7869df6.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="문서 영역 레이아웃 (PostgreSQL 노트)" src="https://dbalog.dev/assets/images/dbalog-docs-thumb-f22ca228d7dad2e57e8d61a4f6a96dcb.png" width="480" height="276" class="img_ev3q"></a></p>
<p>이제 빌드와 배포가 하나로 줄었어요. 예전 URL의 색인이 새 주소로 안정적으로 재구축되는지는 조금 더 지켜보는 중이에요.</p>]]></content>
        <category label="Docusaurus" term="Docusaurus"/>
        <category label="Hugo" term="Hugo"/>
        <category label="블로그" term="블로그"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PostgreSQL 18.5 결번]]></title>
        <id>https://dbalog.dev/blog/postgresql-18-5-skipped-release</id>
        <link href="https://dbalog.dev/blog/postgresql-18-5-skipped-release"/>
        <updated>2026-08-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[18.4 다음이 18.6입니다. 18.5는 tarball까지 만들어졌다가 회귀가 발견되어 배포 없이 폐기됐습니다. PostgreSQL 역사상 결번된 마이너 버전은 여섯 개뿐이고, 사건으로 묶으면 2008년과 2026년 두 번입니다. 릴리스가 어떤 순서로 만들어지길래 취소가 번호를 소모하는지, 그리고 18년 전에는 무슨 일이 있었는지 1차 출처로 따라가 봅니다.]]></summary>
        <content type="html"><![CDATA[<p>버전 번호가 하나 비어 있으면 신경이 쓰여요. 8월 13일에 나온 PostgreSQL 마이너 릴리스는 18.6인데, 그 앞은 18.5가 아니라 18.4입니다. 제 경우엔 패키지 목록을 보다가 "내가 하나 놓쳤나" 싶어 한참을 되짚었습니다. 결론부터 적으면 놓친 것은 없습니다. 18.5는 만들어졌지만 세상에 나오지 않았습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="공식이-말한-것은-한-줄뿐입니다">공식이 말한 것은 한 줄뿐입니다<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EA%B3%B5%EC%8B%9D%EC%9D%B4-%EB%A7%90%ED%95%9C-%EA%B2%83%EC%9D%80-%ED%95%9C-%EC%A4%84%EB%BF%90%EC%9E%85%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="공식이 말한 것은 한 줄뿐입니다에 대한 직접 링크" title="공식이 말한 것은 한 줄뿐입니다에 대한 직접 링크" translate="no">​</a></h2>
<p>사유를 언급한 곳은 두 군데인데, 표현이 미묘하게 다릅니다.</p>
<p><a href="https://www.postgresql.org/docs/release/18.6/" target="_blank" rel="noopener noreferrer" class="">18.6 릴리스 노트</a>에는 이렇게 적혀 있습니다.</p>
<blockquote>
<p>18.5 was never released, due to a regression discovered post-wrap.</p>
</blockquote>
<p><a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/" target="_blank" rel="noopener noreferrer" class="">릴리스 공지</a>는 좀 더 건조합니다.</p>
<blockquote>
<p>This release skips PostgreSQL 18 versions from PostgreSQL 18.4 to 18.6. 18.5 was not shipped due to a regression.</p>
</blockquote>
<p>공지는 "shipped"라고만 했고, 릴리스 노트는 <code>post-wrap</code>이라는 단어를 하나 더 얹었습니다. 이 단어가 사실상 전부를 설명합니다. 무엇이 wrap이고, 그 이후라는 게 왜 중요한지 알면 18.5가 사라진 경로가 그대로 보입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="마이너-릴리스는-이런-순서로-만들어집니다">마이너 릴리스는 이런 순서로 만들어집니다<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EB%A7%88%EC%9D%B4%EB%84%88-%EB%A6%B4%EB%A6%AC%EC%8A%A4%EB%8A%94-%EC%9D%B4%EB%9F%B0-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A7%91%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="마이너 릴리스는 이런 순서로 만들어집니다에 대한 직접 링크" title="마이너 릴리스는 이런 순서로 만들어집니다에 대한 직접 링크" translate="no">​</a></h2>
<p>PostgreSQL의 <a href="https://wiki.postgresql.org/wiki/Release_process" target="_blank" rel="noopener noreferrer" class="">Release process 위키</a>에 절차가 공개돼 있습니다. 요약하면 committer가 버전을 찍고, 서버에서 tarball을 말고, packager들이 먼저 써 보고, 마지막에 태그를 미는 순서입니다.</p>
<!-- -->
<p>2단계가 wrap입니다. <code>pgsql</code> 계정으로 borka.postgresql.org에 들어가 <code>mk-release-bundle</code>을 실행하면 tarball과 체크섬이 staging 디렉토리에 만들어집니다. 이 단계를 특정 서버에서만 하는 이유는 재현성 때문입니다. bison, flex, docbook 버전이 다르면 결과물이 달라지니 빌드 환경을 한 곳으로 고정해 둔 것입니다.</p>
<p>주목할 곳은 3단계와 4단계 사이입니다. 위키가 태그를 두고 분명하게 적어 뒀습니다.</p>
<blockquote>
<p>Pushing a tag is more or less irreversible, so don't do this until packager preliminary testing is over.</p>
</blockquote>
<p>태그를 밀면 되돌리기 어렵습니다. 그래서 packager들이 최소 24시간 먼저 만져 보고, 이상이 없어야 태그가 나갑니다. 18.5는 그 구간을 통과하지 못했습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="취소는-되는데-번호는-돌아오지-않습니다">취소는 되는데 번호는 돌아오지 않습니다<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EC%B7%A8%EC%86%8C%EB%8A%94-%EB%90%98%EB%8A%94%EB%8D%B0-%EB%B2%88%ED%98%B8%EB%8A%94-%EB%8F%8C%EC%95%84%EC%98%A4%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="취소는 되는데 번호는 돌아오지 않습니다에 대한 직접 링크" title="취소는 되는데 번호는 돌아오지 않습니다에 대한 직접 링크" translate="no">​</a></h2>
<p>같은 위키에 사고 처리 절차도 한 줄로 적혀 있습니다.</p>
<blockquote>
<p>In event of disaster, fix code as needed then repeat the wrap process, with or without a version number bump as seems appropriate.</p>
</blockquote>
<p>문제가 생기면 코드를 고쳐 wrap을 다시 하되, 번호를 올릴지 말지는 그때 적절히 판단하라는 뜻입니다. 규정이 아니라 재량입니다. 18.5 건에서는 올리는 쪽을 택했습니다.</p>
<p>왜 그랬을까요. <code>version_stamp.pl</code>이 이미 <code>Stamp 18.5.</code> 커밋을 남겼고 그 번호로 tarball이 만들어졌기 때문입니다. staging에 있던 물건이라 일반 사용자에게 내려간 것은 아니지만, packager들 손에는 18.5라는 이름표가 붙은 파일이 이미 가 있었습니다. 같은 번호로 내용이 다른 tarball을 한 번 더 돌리는 것보다 번호를 하나 올리는 편이 안전합니다. 태그를 밀기 전이라 릴리스 자체는 멈출 수 있었지만, 번호는 그 시점에 이미 쓰인 것입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="8월-9일까지는-185였습니다">8월 9일까지는 18.5였습니다<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#8%EC%9B%94-9%EC%9D%BC%EA%B9%8C%EC%A7%80%EB%8A%94-185%EC%98%80%EC%8A%B5%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="8월 9일까지는 18.5였습니다에 대한 직접 링크" title="8월 9일까지는 18.5였습니다에 대한 직접 링크" translate="no">​</a></h2>
<p>타임라인도 이 해석과 맞습니다. Jonathan Katz(조너선 카츠)가 pgsql-hackers에 올린 <a href="http://www.mail-archive.com/pgsql-hackers@lists.postgresql.org/msg235391.html" target="_blank" rel="noopener noreferrer" class="">8월 13일 릴리스 공지 초안</a>은 2026년 8월 9일자인데, 거기 적힌 버전은 아직 <strong>18.5</strong>입니다.</p>
<p>마이너 릴리스는 관례상 목요일에 공개하고 tarball은 그 주 초에 맙니다. 8월 13일이 목요일이니 wrap은 10일 전후, 공지 초안은 그 직전에 돌린 셈입니다. 초안이 나가고 며칠 사이에 번호가 하나 올라갔습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="결번은-여섯-개뿐입니다">결번은 여섯 개뿐입니다<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EA%B2%B0%EB%B2%88%EC%9D%80-%EC%97%AC%EC%84%AF-%EA%B0%9C%EB%BF%90%EC%9E%85%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="결번은 여섯 개뿐입니다에 대한 직접 링크" title="결번은 여섯 개뿐입니다에 대한 직접 링크" translate="no">​</a></h2>
<p>그럼 이런 일이 자주 있었을까요. 세어 봤습니다. bucardo가 <a href="https://bucardo.org/postgres_all_versions.html" target="_blank" rel="noopener noreferrer" class="">모든 버전의 릴리스 노트를 한 페이지</a>에 모아 두는데, 여기서 <code>never released</code>를 전수 검색하면 여섯 건이 나옵니다.</p>








































<table><thead><tr><th>결번</th><th>직전 릴리스</th><th>대체 릴리스</th></tr></thead><tbody><tr><td>7.4.20</td><td>7.4.19 (2008-01-07)</td><td>7.4.21 (2008-06-12)</td></tr><tr><td>8.0.16</td><td>8.0.15 (2008-01-07)</td><td>8.0.17 (2008-06-12)</td></tr><tr><td>8.1.12</td><td>8.1.11 (2008-01-07)</td><td>8.1.13 (2008-06-12)</td></tr><tr><td>8.2.8</td><td>8.2.7 (2008-03-17)</td><td>8.2.9 (2008-06-12)</td></tr><tr><td>8.3.2</td><td>8.3.1 (2008-03-17)</td><td>8.3.3 (2008-06-12)</td></tr><tr><td>18.5</td><td>18.4 (2026-05-14)</td><td>18.6 (2026-08-13)</td></tr></tbody></table>
<p>여섯 개지만 사건은 두 건입니다. 위의 다섯은 대체 릴리스 날짜가 전부 2008년 6월 12일로 같습니다. 한 번의 사고로 당시 지원하던 브랜치가 통째로 결번된 것입니다.</p>
<p>18.5는 혼자입니다. 같은 날 나온 17.11, 16.15, 15.19, 14.24는 번호가 정상입니다. 회귀가 18 브랜치에만 있었다는 뜻입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2008년에는-무슨-일이-있었나">2008년에는 무슨 일이 있었나<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#2008%EB%85%84%EC%97%90%EB%8A%94-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%B4-%EC%9E%88%EC%97%88%EB%82%98" class="hash-link" aria-label="2008년에는 무슨 일이 있었나에 대한 직접 링크" title="2008년에는 무슨 일이 있었나에 대한 직접 링크" translate="no">​</a></h2>
<p>8.3.2 릴리스 노트를 열면 <code>Release Date</code> 필드가 <code>never released</code>입니다. 그런데 본문은 멀쩡합니다. "This release contains a variety of fixes from 8.3.1"로 시작하고, <code>Migration to Version 8.3.2</code> 절도 붙어 있고, Changes 목록에 마흔 건 가까운 수정이 그대로 나열돼 있습니다. Windows에서 UTF-8 인코딩일 때 나던 크래시, <code>%r</code> 매크로의 archive 절단 지점 오산, GIN의 "too many LWLocks taken" 실패, <code>SIGTERM</code>으로 backend를 개별 종료했을 때 shared memory가 오염되던 문제가 보입니다.</p>
<p>그중 <code>%r</code> 항목은 지금 봐도 아찔합니다. warm standby 스크립트가 그 값을 믿고 WAL segment 파일을 버리면 데이터를 잃을 수 있었습니다. 이런 수정을 담은 릴리스가 문턱까지 갔다가 멈췄습니다.</p>
<p>노트에 사유는 적혀 있지 않습니다. 대신 대체 버전 노트의 첫 문장이 단서를 흘립니다.</p>





















<table><thead><tr><th>버전</th><th>첫 문장</th></tr></thead><tbody><tr><td>8.3.3</td><td>one serious and one minor bug fix <strong>over 8.3.2</strong></td></tr><tr><td>8.2.9</td><td>one serious and one minor bug fix <strong>over 8.2.8</strong></td></tr><tr><td>7.4.21</td><td>one serious bug fix <strong>over 7.4.20</strong></td></tr></tbody></table>
<p>세상에 나온 적 없는 버전을 기준점으로 삼아 "그 대비 몇 건"이라고 적었습니다. 결번본이 실재했다는 자백입니다. 그리고 여기서 말하는 "one serious"가 무엇인지는 다섯 브랜치 공통 항목으로 노트에 남아 있습니다.</p>
<blockquote>
<p>Make <code>pg_get_ruledef()</code> parenthesize negative constants (Tom Lane)</p>
<p>Before this fix, a negative constant in a view or rule might be dumped as, say, <code>-42::integer</code>, which is subtly incorrect: it should be <code>(-42)::integer</code> due to operator precedence rules. Usually this would make little difference, but it could interact with another recent patch to cause PostgreSQL to reject what had been a valid SELECT DISTINCT view query. Since this could result in pg_dump output failing to reload, it is being treated as a high-priority fix.</p>
</blockquote>
<p>view나 rule 안에 들어 있는 음수 상수를 괄호 없이 덤프하던 문제입니다. 연산자 우선순위상 <code>(-42)::integer</code>여야 하는데 <code>-42::integer</code>로 나왔습니다. 그 자체로는 대개 차이가 없지만, 당시 함께 들어간 다른 패치와 맞물리면 멀쩡하던 SELECT DISTINCT view 쿼리를 PostgreSQL이 거부했습니다.</p>
<p>여기서 심각도가 뛴 지점은 마지막 문장입니다. <strong>pg_dump 출력이 다시 적재되지 않을 수 있다</strong>는 결론이었습니다. 백업 파일이 복원되지 않는 문제는 다른 어떤 버그와도 무게가 다릅니다. 준비돼 있던 다섯 개 tarball을 버리고 이 수정을 얹어 다시 낸 이유로 충분합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="두-사건이-남긴-기록의-차이">두 사건이 남긴 기록의 차이<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EB%91%90-%EC%82%AC%EA%B1%B4%EC%9D%B4-%EB%82%A8%EA%B8%B4-%EA%B8%B0%EB%A1%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4" class="hash-link" aria-label="두 사건이 남긴 기록의 차이에 대한 직접 링크" title="두 사건이 남긴 기록의 차이에 대한 직접 링크" translate="no">​</a></h2>
<p>구조는 같습니다. wrap이 끝난 뒤 회귀가 잡혔고, 고쳐서 번호를 올려 다시 냈습니다. 달라진 것은 그 사실을 어떻게 적어 두느냐입니다.</p>
<p>18.6 노트는 결번 사유를 한 줄로 명시했습니다. 짧지만 왜 번호가 비었는지가 문서 안에 적혀 있습니다. 2008년 노트들은 <code>never released</code>라는 사실만 남기고 사유를 적지 않았습니다. 지금 그 이유를 재구성할 수 있는 것은 대체본이 "over 8.3.2"라는 표현을 쓴 덕분입니다. 의도한 기록이라기보다 흔적에 가깝습니다.</p>
<p>공통점도 있습니다. 여섯 개 전부 릴리스 노트 페이지가 지금도 살아 있습니다. 나오지 않은 버전의 문서를 지우지 않는 관행이 적어도 2008년부터 이어지고 있습니다. 덕분에 18년 전 사건을 1차 출처로 따라갈 수 있었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="검색하면-섞여-나오는-이야기-하나">검색하면 섞여 나오는 이야기 하나<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EA%B2%80%EC%83%89%ED%95%98%EB%A9%B4-%EC%84%9E%EC%97%AC-%EB%82%98%EC%98%A4%EB%8A%94-%EC%9D%B4%EC%95%BC%EA%B8%B0-%ED%95%98%EB%82%98" class="hash-link" aria-label="검색하면 섞여 나오는 이야기 하나에 대한 직접 링크" title="검색하면 섞여 나오는 이야기 하나에 대한 직접 링크" translate="no">​</a></h2>
<p>18.5 결번을 검색하면 "standby가 구버전 마이너의 WAL을 재생하다 self-deadlock에 빠진다"는 내용이 사유처럼 딸려 나옵니다. 별개 사건입니다.</p>
<p><a href="https://www.credativ.de/en/blog/postgresql-en/replication-deadlock-bug-in-current-postgres-releases-14-16/" target="_blank" rel="noopener noreferrer" class="">credativ가 정리한 그 버그</a>는 <code>MultiXactOffsetSLRU</code> deadlock이고, 2026년 5월 14일에 나온 14.23, 15.18, 16.14에서 유입됐습니다. 17과 18은 영향이 없습니다. 증상은 standby의 startup 프로세스가 <code>pg_stat_activity</code>에서 <code>LWLock/MultiXactOffsetSLRU</code> 대기로 멈추는 형태입니다.</p>
<p>같은 8월 13일 릴리스에서 함께 고쳐졌을 뿐, 18.5를 취소시킨 원인이 아닙니다. 두 이야기를 붙여 쓰면 틀린 글이 됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="지금-확인할-것">지금 확인할 것<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EC%A7%80%EA%B8%88-%ED%99%95%EC%9D%B8%ED%95%A0-%EA%B2%83" class="hash-link" aria-label="지금 확인할 것에 대한 직접 링크" title="지금 확인할 것에 대한 직접 링크" translate="no">​</a></h2>
<p>운영 관점에서 할 일은 많지 않습니다.</p>
<ol>
<li class="">18.4에서 18.6으로 바로 올립니다. 중간에 빠뜨린 버전은 없습니다</li>
<li class="">자동화 스크립트가 마이너 번호를 순차 증가로 가정하고 있다면 확인합니다. <code>18.4 + 1 = 18.5</code>를 기대하는 코드는 여기서 멈춥니다</li>
<li class="">사내 문서나 지원 버전 표에 18.5를 적어 둔 곳이 있으면 지웁니다</li>
</ol>
<p>3번은 사소해 보이지만 실제로 혼란을 만듭니다. "18.5 적용 예정"이라고 써 둔 계획서가 남아 있으면 나중에 읽는 사람이 없는 버전을 찾게 됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="남은-질문">남은 질문<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EB%82%A8%EC%9D%80-%EC%A7%88%EB%AC%B8" class="hash-link" aria-label="남은 질문에 대한 직접 링크" title="남은 질문에 대한 직접 링크" translate="no">​</a></h2>
<p>18.5를 취소시킨 회귀가 정확히 무엇이었는지는 공개되지 않았습니다. 공식 문구는 "a regression"이 전부입니다. 2008년 사건에서도 회귀를 유발한 "another recent patch"가 무엇인지는 노트에 적혀 있지 않습니다.</p>
<p>저는 이 공백이 문제라고 보지는 않아요. 세상에 나가지 않은 코드의 버그를 상세히 적는 것은 그 자체로 이상한 일이니까요. 다만 번호가 하나 비면 사람은 반드시 이유를 찾습니다. 18.6 노트가 한 줄이라도 남겨 둔 덕분에 저 같은 사람이 패키지 목록 앞에서 오래 헤매지 않았습니다. 기록은 그 정도만 해 줘도 충분히 일을 합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/postgresql-18-5-skipped-release#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.postgresql.org/docs/release/18.6/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 18.6 릴리스 노트</a> - "18.5 was never released, due to a regression discovered post-wrap"</li>
<li class=""><a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24 and 19 Beta 3 Released!</a> - PostgreSQL News, 2026-08-13</li>
<li class=""><a href="https://wiki.postgresql.org/wiki/Release_process" target="_blank" rel="noopener noreferrer" class="">Release process</a> - PostgreSQL wiki, wrap/tag 절차와 사고 처리</li>
<li class=""><a href="https://www.postgresql.org/support/versioning/" target="_blank" rel="noopener noreferrer" class="">Versioning Policy</a> - 마이너 릴리스 주기와 번호 규칙</li>
<li class=""><a href="https://www.postgresql.org/docs/8.3/release-8-3-2.html" target="_blank" rel="noopener noreferrer" class="">Release 8.3.2</a> - Release Date가 never released인 노트</li>
<li class=""><a href="https://www.postgresql.org/docs/8.3/release-8-3-3.html" target="_blank" rel="noopener noreferrer" class="">Release 8.3.3</a> - "one serious and one minor bug fix over 8.3.2"</li>
<li class=""><a href="https://bucardo.org/postgres_all_versions.html" target="_blank" rel="noopener noreferrer" class="">Postgres Release Notes - All Versions</a> - 결번 전수 조사에 사용</li>
<li class=""><a href="http://www.mail-archive.com/pgsql-hackers@lists.postgresql.org/msg235391.html" target="_blank" rel="noopener noreferrer" class="">2026-08-13 release announcement draft</a> - 8월 9일 시점 표기는 18.5</li>
<li class=""><a href="https://www.credativ.de/en/blog/postgresql-en/replication-deadlock-bug-in-current-postgres-releases-14-16/" target="_blank" rel="noopener noreferrer" class="">Replication Deadlock Bug in Current Postgres Releases 14-16</a> - credativ, 18.5 결번과는 별개 사건</li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="릴리스" term="릴리스"/>
        <category label="운영" term="운영"/>
        <category label="버전관리" term="버전관리"/>
        <category label="pg_dump" term="pg_dump"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Oracle RU 즉시 적용]]></title>
        <id>https://dbalog.dev/blog/oracle-release-update-immediate-apply</id>
        <link href="https://dbalog.dev/blog/oracle-release-update-immediate-apply"/>
        <updated>2026-08-25T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[2026년 8월 18일 오라클이 19c와 26ai 고객 전체에 분기 Release Update를 나오는 즉시 테스트하고 적용하라고 권고했습니다. 배경은 AI 모델이 취약점 발견뿐 아니라 보안 패치의 리버스 엔지니어링과 공격 경로 개발까지 빠르게 해낸다는 판단입니다. 같은 주에 나온 8월 CSPU는 943개 패치였습니다. 패치 공개가 곧 정보 공개가 되는 구조를 짚습니다.]]></summary>
        <content type="html"><![CDATA[<p>벤더가 "패치를 빨리 적용하세요"라고 말하는 건 새롭지 않아요. 그런데 그 이유가 "AI가 우리 패치를 분석해서 공격 방법을 알아낼 수 있어서"라면 성격이 다릅니다.</p>
<p>2026년 8월 18일 오라클이 <a href="https://blogs.oracle.com/database/prepare-now-apply-the-upcoming-oracle-database-release-update-immediately-upon-availability" target="_blank" rel="noopener noreferrer" class="">Prepare Now: Apply Oracle Database Release Update Immediately Upon Availability</a>를 냈습니다. 지원 중인 모든 릴리스, 19c와 Oracle AI Database 26ai를 포함해, 데이터베이스 자산 전체에 최신 분기 Release Update를 테스트하고 배포할 준비를 지금 하라는 내용입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="권고의-근거가-달라졌다">권고의 근거가 달라졌다<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#%EA%B6%8C%EA%B3%A0%EC%9D%98-%EA%B7%BC%EA%B1%B0%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EB%8B%A4" class="hash-link" aria-label="권고의 근거가 달라졌다에 대한 직접 링크" title="권고의 근거가 달라졌다에 대한 직접 링크" translate="no">​</a></h2>
<p>주목할 부분은 요구 사항이 아니라 근거입니다. 오라클이 든 이유는 frontier AI 모델이 소프트웨어 취약점을 찾고 악용하는 장벽을 크게 낮추고 있다는 것입니다. 그 모델들이 하는 일로 네 가지를 꼽았습니다. 약점 식별, 소프트웨어 변경 분석, 보안 패치의 리버스 엔지니어링, 그리고 잠재적 공격 경로 개발입니다. 속도와 규모가 전례 없다고 표현했습니다.</p>
<p>이 목록에서 세 번째가 핵심입니다. 보안 패치의 리버스 엔지니어링입니다.</p>
<p>패치는 무엇이 잘못됐는지를 알려 줍니다. 코드의 어느 줄이 어떻게 바뀌었는지 보면 그 전에 무엇이 가능했는지 역산할 수 있습니다. 이걸 patch diffing이라고 부르고, 오래된 기법입니다. 새로운 건 그 작업의 비용입니다. 숙련된 분석가가 며칠 걸리던 일을 모델이 훨씬 빨리 해내면, 패치 공개와 실제 적용 사이의 구간이 그만큼 위험해집니다.</p>
<!-- -->
<p>패치를 늦게 적용하는 쪽이 더 위험해지는 구조는 원래 있었습니다. 달라진 것은 그 위험이 커지는 속도입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="8월-cspu의-물량">8월 CSPU의 물량<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#8%EC%9B%94-cspu%EC%9D%98-%EB%AC%BC%EB%9F%89" class="hash-link" aria-label="8월 CSPU의 물량에 대한 직접 링크" title="8월 CSPU의 물량에 대한 직접 링크" translate="no">​</a></h2>
<p>같은 주에 나온 숫자가 이 권고의 배경을 보여 줍니다. <a href="https://blog.qualys.com/vulnerabilities-threat-research/2026/08/19/oracle-critical-patch-update-august-2026-security-update-review" target="_blank" rel="noopener noreferrer" class="">Qualys 분석</a>에 따르면 8월 Critical Security Patch Update는 943개 취약점을 다뤘습니다.</p>
<p>무인증 원격 악용이 가능한 것들이 제품군별로 이렇게 나왔습니다.</p>





























<table><thead><tr><th>제품군</th><th>무인증 원격 악용 가능</th></tr></thead><tbody><tr><td>Oracle Fusion Middleware</td><td>182</td></tr><tr><td>Oracle Hyperion</td><td>107</td></tr><tr><td>Oracle Commerce</td><td>47</td></tr><tr><td>Oracle E-Business Suite</td><td>27</td></tr><tr><td>Oracle Siebel CRM</td><td>21</td></tr></tbody></table>
<p>E-Business Suite에서 CVSS 9.8인 CVE-2026-60782와 CVE-2026-70926이 나왔습니다.</p>
<p>DBA 관점에서 궁금한 것은 Database 쪽 물량입니다. 943개 중 데이터베이스 제품군은 17개였습니다.</p>

























<table><thead><tr><th>구성요소</th><th>패치 수</th><th>최고 CVSS</th></tr></thead><tbody><tr><td>Oracle Database Server</td><td>6</td><td>9.6</td></tr><tr><td>Oracle Autonomous Health Framework</td><td>7</td><td>8.8</td></tr><tr><td>Oracle Essbase</td><td>4</td><td>9.8</td></tr></tbody></table>
<p>전체 943개에 비해 17개는 적어 보입니다. 하지만 Database Server의 최고 점수가 9.6이라는 게 중요합니다. 개수가 아니라 그 6개가 무엇인지가 판단 근거입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="월간-체제와-즉시-적용이-만나면">월간 체제와 즉시 적용이 만나면<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#%EC%9B%94%EA%B0%84-%EC%B2%B4%EC%A0%9C%EC%99%80-%EC%A6%89%EC%8B%9C-%EC%A0%81%EC%9A%A9%EC%9D%B4-%EB%A7%8C%EB%82%98%EB%A9%B4" class="hash-link" aria-label="월간 체제와 즉시 적용이 만나면에 대한 직접 링크" title="월간 체제와 즉시 적용이 만나면에 대한 직접 링크" translate="no">​</a></h2>
<p><a class="" href="https://dbalog.dev/blog/oracle-monthly-cspu">오라클이 분기 패치를 버리고 월간 CSPU 체제로 옮긴 이야기</a>를 다룬 적이 있습니다. 이번 권고를 그 흐름 위에 놓으면 실무 부담이 선명해집니다.</p>
<p>분기 체제에서는 1년에 네 번 패치 시기가 왔습니다. 한 번마다 테스트에 몇 주를 쓸 여유가 있었습니다. 월간 체제에서는 열두 번입니다. 여기에 "나오는 즉시 적용하라"가 붙으면, 테스트 기간을 얼마로 잡을지가 실질적인 문제가 됩니다.</p>
<p>권고와 현실 사이의 간격을 어떻게 좁힐지가 결국 각 조직의 판단입니다. 몇 가지 방향은 이렇습니다.</p>
<p>RU 종류를 구분하는 것이 먼저입니다. 오라클의 Release Update와 Release Update Revision은 성격이 다릅니다. 전체를 같은 절차로 다루면 열두 번을 감당하지 못합니다.</p>
<p>테스트 자동화의 범위를 다시 볼 필요도 있습니다. 회귀 검증을 사람이 도는 구조라면 월간 주기를 못 따라갑니다. 이 부분은 패치 정책 이전에 검증 파이프라인 문제입니다.</p>
<p>그리고 적용 순서를 노출도로 정하는 방법이 있습니다. 무인증 원격 악용이 가능한 구성요소를 앞에 두는 식입니다. 위 표에서 Fusion Middleware의 182개가 왜 먼저인지는 숫자가 설명합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai가-양쪽에서-일한다">AI가 양쪽에서 일한다<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#ai%EA%B0%80-%EC%96%91%EC%AA%BD%EC%97%90%EC%84%9C-%EC%9D%BC%ED%95%9C%EB%8B%A4" class="hash-link" aria-label="AI가 양쪽에서 일한다에 대한 직접 링크" title="AI가 양쪽에서 일한다에 대한 직접 링크" translate="no">​</a></h2>
<p><a class="" href="https://dbalog.dev/blog/anthropic-project-glasswing">Anthropic의 Project Glasswing</a>을 다룰 때는 AI가 핵심 인프라의 취약점을 찾아 주는 쪽 이야기였습니다. 이번 오라클 공지는 같은 능력의 반대쪽입니다.</p>
<p>두 사례를 나란히 놓으면 구도가 정리됩니다. 방어 측은 AI로 코드를 감사해 취약점을 미리 찾습니다. 공격 측은 AI로 패치를 분석해 이미 고쳐진 취약점을 역산합니다. 전자는 패치 이전, 후자는 패치 이후입니다. 그래서 패치 공개 시점이 양쪽의 경계선이 됩니다.</p>
<p>벤더가 이 판단을 공식 문서로 낸 것이 이번 공지의 의미라고 봅니다. "AI 때문에 패치 정책을 바꿉니다"를 제품 블로그에 적은 사례가 아직 흔하지 않습니다. 오라클이 자사 데이터베이스 고객 전체를 향해 그렇게 적었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="실무에서-확인할-것">실무에서 확인할 것<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%ED%99%95%EC%9D%B8%ED%95%A0-%EA%B2%83" class="hash-link" aria-label="실무에서 확인할 것에 대한 직접 링크" title="실무에서 확인할 것에 대한 직접 링크" translate="no">​</a></h2>
<p>Oracle Database Appliance를 쓰는 환경이라면 릴리스 19.32에 7월 Database Release Update와 19c, 26ai 클론 파일이 들어 있습니다. 계획, 테스트, 배포 일정을 지금 시작하라는 것이 공지의 요구입니다.</p>
<p>정리하면 확인 항목은 셋입니다. 현재 운영 중인 데이터베이스의 RU 수준이 어디인지, 다음 RU가 나올 때 테스트에 며칠을 쓸 계획인지, 그리고 그 며칠이 이번 공지의 논리에 비추어 감당할 만한 구간인지입니다.</p>
<p>마지막 항목이 답하기 어렵습니다. 패치 공개 후 며칠이 안전한지 아무도 숫자로 말해 주지 않으니까요. 저는 이 공지가 그 숫자가 줄어들고 있다는 신호로 읽혔어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/oracle-release-update-immediate-apply#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://blogs.oracle.com/database/prepare-now-apply-the-upcoming-oracle-database-release-update-immediately-upon-availability" target="_blank" rel="noopener noreferrer" class="">Prepare Now: Apply Oracle Database Release Update Immediately Upon Availability</a> (Oracle Database Blog, 2026-08-18)</li>
<li class=""><a href="https://blog.qualys.com/vulnerabilities-threat-research/2026/08/19/oracle-critical-patch-update-august-2026-security-update-review" target="_blank" rel="noopener noreferrer" class="">Oracle Critical Patch Update, August 2026 Security Update Review</a> (Qualys, 2026-08-19)</li>
<li class=""><a href="https://blogs.oracle.com/security/august-2026-critical-security-patch-update-released" target="_blank" rel="noopener noreferrer" class="">August 2026 Critical Security Patch Update Released</a> (Oracle Security Blog, 2026-08-18)</li>
<li class=""><a href="https://blogs.oracle.com/oda/oda-releases" target="_blank" rel="noopener noreferrer" class="">Oracle Database Appliance 릴리스 정보</a></li>
</ul>]]></content>
        <category label="Oracle" term="Oracle"/>
        <category label="보안" term="보안"/>
        <category label="패치" term="패치"/>
        <category label="CSPU" term="CSPU"/>
        <category label="AI" term="AI"/>
        <category label="취약점" term="취약점"/>
        <category label="운영" term="운영"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PG 병렬 집계 공유 해시]]></title>
        <id>https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash</id>
        <link href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash"/>
        <updated>2026-08-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[PostgreSQL의 병렬 집계는 워커가 부분 집계를 하고 리더 하나가 합치는 구조입니다. 그룹이 많으면 그 합치는 단계가 병목이 됩니다. pgEdge의 Andrei Lepikhov가 DSM 공유 해시 테이블로 이 단계를 없애 보고 균등 분포에서 4.8배를 얻었지만, skew에서 0.04배까지 무너졌습니다. 500만 그룹 쿼리로 병목을 직접 재 보고, 프로세스 모델이 어디서 값을 청구하는지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>병렬 쿼리를 켰는데 빨라지지 않는 경험, 집계 쿼리에서 특히 잦습니다. 이유가 구조에 있고, 그 구조를 바꿔 보려는 실험 결과가 나왔어요.</p>
<p>pgEdge의 Andrei Lepikhov(안드레이 레피호프)가 <a href="https://www.pgedge.com/blog/do-global-hash-tables-strike-back-in-postgresql" target="_blank" rel="noopener noreferrer" class="">Do Global Hash Tables Strike Back in PostgreSQL?</a>에서 공유 해시 테이블 기반 병렬 집계를 직접 구현하고 측정했습니다. 결론이 흥미롭습니다. 되기는 되는데, 값이 너무 비쌉니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="지금-구조-나눠서-세고-하나가-합친다">지금 구조: 나눠서 세고, 하나가 합친다<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EC%A7%80%EA%B8%88-%EA%B5%AC%EC%A1%B0-%EB%82%98%EB%88%A0%EC%84%9C-%EC%84%B8%EA%B3%A0-%ED%95%98%EB%82%98%EA%B0%80-%ED%95%A9%EC%B9%9C%EB%8B%A4" class="hash-link" aria-label="지금 구조: 나눠서 세고, 하나가 합친다에 대한 직접 링크" title="지금 구조: 나눠서 세고, 하나가 합친다에 대한 직접 링크" translate="no">​</a></h2>
<p>PostgreSQL의 병렬 집계는 두 단계입니다. 각 워커가 자기가 읽은 행을 <code>Partial HashAggregate</code>로 부분 집계합니다. 그 결과를 <code>Gather</code>가 모으고, 리더 프로세스 하나가 <code>Finalize HashAggregate</code>로 합칩니다.</p>
<p>그룹 수가 적으면 이 구조가 잘 돕니다. 워커 넷이 각각 100개 그룹의 부분 합계를 만들면, 리더는 400개 행만 합치면 됩니다.</p>
<p>그룹 수가 많으면 이야기가 달라집니다. 그룹이 500만 개면 각 워커의 부분 집계 결과도 수백만 행이고, 리더가 그걸 다 받아 다시 해시 테이블을 만들어야 합니다. 저자가 든 예시에서 스캔은 252ms인데 Finalize 단계가 전체 25초 중 18초를 썼습니다. 병렬을 끄면 13초였으니, 병렬 계획이 직렬보다 거의 두 배 느렸습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="직접-재-봤다">직접 재 봤다<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EC%A7%81%EC%A0%91-%EC%9E%AC-%EB%B4%A4%EB%8B%A4" class="hash-link" aria-label="직접 재 봤다에 대한 직접 링크" title="직접 재 봤다에 대한 직접 링크" translate="no">​</a></h2>
<p>PostgreSQL 18에서 확인했습니다. 워커를 8개까지 쓸 수 있게 두고, 500만 행 테이블 셋을 만들어 그룹 수만 달리했습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- 그룹 100개</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> agg_few </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">i </span><span class="token operator" style="color:#393A34">%</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> g</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> i </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">5000000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- 그룹 500만개 (모든 행이 각자 그룹)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> agg_many </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">g </span><span class="token keyword" style="color:#00009f">bigint</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> v </span><span class="token keyword" style="color:#00009f">bigint</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">insert</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">into</span><span class="token plain"> agg_many </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> i </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">5000000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>그룹 100개짜리는 예상대로 병렬 계획이 나왔습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Finalize GroupAggregate (actual time=851.515..855.136 rows=100.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -&gt;  Gather Merge (actual time=851.509..855.094 rows=400.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Workers Planned: 3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Workers Launched: 3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        -&gt;  Sort (actual time=837.048..837.052 rows=100.00 loops=4)</span><br></div></code></pre></div></div>
<p><code>Gather Merge</code>가 400행을 올리고 <code>Finalize</code>가 4ms 안에 끝납니다. 워커 넷이 각각 100개 그룹을 만들었으니 정확히 400행입니다.</p>
<p>그룹 500만개에서는 계획 자체가 달라졌습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">HashAggregate (actual time=3860.148..8242.441 rows=5000000.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Group Key: g</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Batches: 17  Memory Usage: 135313kB  Disk Usage: 161920kB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -&gt;  Seq Scan on agg_many (actual time=0.023..569.741 rows=5000000.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Execution Time: 8488.126 ms</span><br></div></code></pre></div></div>
<p>병렬이 아예 없습니다. planner가 워커를 하나도 쓰지 않기로 결정했습니다. 비용 모형이 이미 "이 경우 병렬 집계는 손해"라고 판단한 것입니다. 저자가 지적한 문제를 planner가 회피하는 방식으로 대응하고 있다는 뜻입니다.</p>
<p>그래서 억지로 병렬을 켜 봤습니다. <code>parallel_setup_cost</code>와 <code>parallel_tuple_cost</code>를 0으로 낮췄습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">HashAggregate (actual time=2438.858..6180.438 rows=5000000.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Batches: 17  Memory Usage: 135313kB  Disk Usage: 161936kB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  -&gt;  Gather (actual time=0.263..300.906 rows=5000000.00)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Workers Planned: 3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Workers Launched: 3</span><br></div></code></pre></div></div>
<p>여기가 이 실험에서 가장 선명한 부분입니다. <code>Gather</code>가 500만 행을 300ms에 올립니다. 읽기는 병렬로 잘 됐습니다. 그런데 그 위의 <code>HashAggregate</code>가 6,180ms를 씁니다. 스캔은 병렬인데 집계는 단일 프로세스입니다.</p>
<p>주목할 것은 <code>Partial HashAggregate</code>가 아예 붙지 않았다는 점입니다. 그룹이 너무 많아 부분 집계가 아무 이득이 없다고 보고, 워커는 스캔만 하고 집계 전부를 리더에게 넘겼습니다. 저자가 지적한 "단일 프로세스 병목"이 계획에 그대로 나타납니다.</p>
<!-- -->
<p>디스크 spill도 눈에 걸립니다. <code>work_mem</code>을 64MB로 뒀는데 17개 batch로 나뉘고 161MB를 디스크에 썼습니다. 리더 하나가 500만 그룹의 해시 테이블을 메모리에 못 담아서입니다. 워커들이 나눠 담았다면 각자 125만 그룹이니 상황이 달랐을 것입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="프로토타입-공유-해시-테이블">프로토타입: 공유 해시 테이블<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EA%B3%B5%EC%9C%A0-%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94" class="hash-link" aria-label="프로토타입: 공유 해시 테이블에 대한 직접 링크" title="프로토타입: 공유 해시 테이블에 대한 직접 링크" translate="no">​</a></h2>
<p>저자가 만든 것은 DSM(Dynamic Shared Memory)에 해시 테이블 하나를 두고, 모든 워커가 lock을 잡고 그 테이블을 갱신하는 방식입니다. 부분 집계와 Finalize 단계가 통째로 사라집니다.</p>
<p>구현 규모가 이 실험의 성격을 보여 줍니다. <code>nodeAgg.c</code> 밖에 약 800줄, planner에 약 600줄(비용 모형과 적용 여부 판단), executor 코어에 일곱 줄이 들어갔습니다. <code>nodeAgg.c</code>에서 가져와 고친 static 함수가 여덟 개입니다.</p>
<p>새로 만들어야 했던 것들이 있습니다. Parallel Hash Join의 해시 테이블 설계를 재사용할 수 없어서 전용 해시 테이블을 만들었습니다. numeric이나 text처럼 가변 타입의 상태를 다루려고 DSM 안팎으로 복사하는 계층을 별도로 짰습니다. 메모리 한도 계산은 로컬에 모았다가 일괄 반영하는 방식으로 처리했고, 집계 함수마다 공유 방식이 안전한지 표시하는 <code>aggsharedsafe</code> 플래그를 새로 넣었습니다.</p>
<p>기존 기계장치도 꽤 썼습니다. spill 처리에 SharedTuplestore, by-value 상태에 32KB 단위 DSA 할당자, 단계 동기화와 임시 파일, 그리고 대기 이벤트 다섯 개와 LWLock tranche 두 개입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="균등-분포에서는-통한다">균등 분포에서는 통한다<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EA%B7%A0%EB%93%B1-%EB%B6%84%ED%8F%AC%EC%97%90%EC%84%9C%EB%8A%94-%ED%86%B5%ED%95%9C%EB%8B%A4" class="hash-link" aria-label="균등 분포에서는 통한다에 대한 직접 링크" title="균등 분포에서는 통한다에 대한 직접 링크" translate="no">​</a></h2>
<p>측정은 GCP VM에서 했습니다. 48코어를 주로 쓰고 16코어, 14코어도 함께 봤습니다. 전부 메모리 안에서 돌았고 디스크 spill은 없었습니다.</p>
<p>그룹이 고르게 분포하고 워커가 8개일 때 결과입니다. by-value 집계는 약 4.8배, numeric 같은 by-reference 집계는 약 2.0배 빨라졌습니다. 같은 조건에서 두 배 차이가 나는 이유가 DSM 안팎으로 상태를 복사하는 비용입니다.</p>
<p>집계 함수 개수를 늘려 보면 곡선이 나옵니다. 그룹당 집계 2개면 4.64배, 12개면 5.80배로 정점을 찍고, 32개가 되면 4.40배로 내려갑니다. lock을 한 번 잡고 여러 집계를 갱신하니 개수가 늘면 lock 비용이 분산되고, 너무 늘면 critical section이 길어져 다시 나빠집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="skew에서-무너진다">skew에서 무너진다<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#skew%EC%97%90%EC%84%9C-%EB%AC%B4%EB%84%88%EC%A7%84%EB%8B%A4" class="hash-link" aria-label="skew에서 무너진다에 대한 직접 링크" title="skew에서 무너진다에 대한 직접 링크" translate="no">​</a></h2>
<p>문제는 여기입니다. 한 그룹이 전체를 얼마나 차지하는지에 따라 결과가 이렇게 변합니다.</p>





















<table><thead><tr><th>최대 그룹 비중</th><th>속도 향상</th></tr></thead><tbody><tr><td>2%</td><td>4.49배</td></tr><tr><td>5~10%</td><td>거의 동등</td></tr><tr><td>95%</td><td>0.04배</td></tr></tbody></table>
<p>95%에서 0.04배는 25배 느려진다는 뜻입니다. 이유가 명확합니다. 모든 워커가 같은 그룹의 상태를 갱신하려고 같은 lock을 두고 줄을 섭니다. 병렬이 아니라 순차가 되고, 여기에 lock 획득 비용이 얹힙니다.</p>
<p>현실 데이터에서 흔한 80대 20 분포에서는 by-value 집계가 대략 동등, by-reference는 통상적인 워커 수에서 이득이 거의 없었습니다. 실무 데이터가 대개 고르지 않다는 점을 생각하면 이 결과가 결정적입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="세-가지-장애물">세 가지 장애물<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%9E%A5%EC%95%A0%EB%AC%BC" class="hash-link" aria-label="세 가지 장애물에 대한 직접 링크" title="세 가지 장애물에 대한 직접 링크" translate="no">​</a></h2>
<p>저자는 lock 경합 자체보다 더 근본적인 문제 셋을 지목했습니다.</p>
<p>첫째, LWLock의 입도입니다. "LWLock은 이 크기의 critical section에 맞는 primitive가 아니다"라고 적었습니다. <code>count(*)</code>조차 나노초 단위 증가 연산을 하려고 마이크로초 단위 <code>LWLockAcquire()</code>를 부릅니다. 보호하려는 작업보다 보호 장치가 비쌉니다.</p>
<p>둘째, JIT 컴파일이 불가능해집니다. 지금의 비공유 경로는 <code>ExecBuildAggTrans()</code>가 만든 마이크로프로그램 하나를 JIT으로 함수 하나로 컴파일합니다. 공유 경로에서는 lock을 잡기 전에 인자를 계산하고 lock 안에서 transition 함수를 호출해야 하니, 이 통합 컴파일이 성립하지 않습니다. 병렬로 얻은 이득을 컴파일 손실로 반납하는 구조입니다.</p>
<p>셋째, lock 안에서 임의 코드가 돕니다. <code>enum_cmp_internal()</code> 같은 함수는 카탈로그를 조회하고, 그 과정에서 <code>table_open()</code>과 buffer 읽기가 일어날 수 있습니다. LWLock을 잡은 상태에서 다른 lock을 잡는 상황이 만들어집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="프로세스-모델이-청구서를-보낸다">프로세스 모델이 청구서를 보낸다<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%AA%A8%EB%8D%B8%EC%9D%B4-%EC%B2%AD%EA%B5%AC%EC%84%9C%EB%A5%BC-%EB%B3%B4%EB%82%B8%EB%8B%A4" class="hash-link" aria-label="프로세스 모델이 청구서를 보낸다에 대한 직접 링크" title="프로세스 모델이 청구서를 보낸다에 대한 직접 링크" translate="no">​</a></h2>
<p>저자의 결론 문장이 셉니다. "프로세스 모델이 공유 모델로 가는 길의 주된 장애물이다."</p>
<p>스레드 기반 엔진과 비교하면 차이가 구체적입니다. DSM 안에서는 직접 포인터를 쓸 수 없어서 <code>palloc</code>, <code>repalloc</code>, MemoryContext를 쓸 수 없습니다. by-reference 상태는 복사해 넣고 복사해 나와야 합니다. TOAST를 제자리에서 압축 해제할 수 없습니다. 집계 함수 구현이 표준적인 가변 상태 패턴을 쓸 수 없습니다.</p>
<p>정리하면 이렇습니다. "PostgreSQL은 스레드를 가진 엔진보다 공유 가변 상태에 훨씬 많은 값을 치르고, 돌려받는 것은 같다."</p>
<p><a class="" href="https://dbalog.dev/blog/pgrust-query-engine">pgrust가 Volcano 모델의 청구서를 뜯어 보여 준 이야기</a>와 같은 종류의 결론입니다. 30년 전에 정한 실행 모델이 지금 어떤 최적화를 막고 있는지 확인하는 작업입니다. <a class="" href="https://dbalog.dev/blog/postgresql-whats-missing">Momjian이 짚은 코어의 빈칸들</a>에도 같은 성격의 항목이 여럿 있었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="그럼-어디에-쓸까">그럼 어디에 쓸까<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EA%B7%B8%EB%9F%BC-%EC%96%B4%EB%94%94%EC%97%90-%EC%93%B8%EA%B9%8C" class="hash-link" aria-label="그럼 어디에 쓸까에 대한 직접 링크" title="그럼 어디에 쓸까에 대한 직접 링크" translate="no">​</a></h2>
<p>저자는 포기하지 않고 적용 범위를 좁히자고 제안합니다.</p>
<p>고정 폭 by-value 상태로 한정합니다. <code>count</code>, <code>sum(integer)</code>, <code>sum(float)</code>이 여기 들어갑니다. 이런 상태는 원자적 병합 연산 하나로 처리할 수 있으니 lock 없이 갱신할 수 있습니다. 위 세 장애물 중 첫째와 둘째가 상당히 완화됩니다.</p>
<p>그리고 MCV 통계로 skew를 미리 감지해 병리적인 경우를 피합니다. planner가 <code>pg_stats</code>의 most common values를 보고 "이 그룹 키는 한 값이 지배적이다"라고 판단하면 공유 방식을 고르지 않는 식입니다. 통계가 이미 있는 정보라서 추가 비용이 낮습니다.</p>
<p>더 유망한 쪽으로 두 곳을 꼽았습니다. SetOp 연산자는 현재 병렬화가 아예 없습니다. 그리고 단순 <code>SELECT DISTINCT</code>입니다. 둘 다 by-reference 상태의 복잡성이 걸리지 않는 영역입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="남는-생각">남는 생각<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EB%82%A8%EB%8A%94-%EC%83%9D%EA%B0%81" class="hash-link" aria-label="남는 생각에 대한 직접 링크" title="남는 생각에 대한 직접 링크" translate="no">​</a></h2>
<p>이 글의 값은 4.8배라는 숫자가 아니라 그 숫자가 왜 못 남는지에 있습니다. 균등 분포 벤치마크만 보면 도입할 이유가 충분해 보이는데, skew 하나로 25배 역행이 나옵니다. 벤치마크 조건을 고르는 일이 결과 자체보다 중요하다는 사례입니다.</p>
<p>제 실측에서 planner가 500만 그룹에 병렬을 아예 안 붙인 것도 같은 맥락으로 읽힙니다. 비용 모형이 이미 이 지점을 알고 회피하고 있습니다. 새 실행 방식을 넣는다면 planner가 그것을 언제 고를지 판단하는 부분이 구현의 절반이 됩니다. 저자가 planner에 600줄을 쓴 이유겠죠.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/postgresql-parallel-aggregate-shared-hash#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.pgedge.com/blog/do-global-hash-tables-strike-back-in-postgresql" target="_blank" rel="noopener noreferrer" class="">Do Global Hash Tables Strike Back in PostgreSQL?</a> (Andrei Lepikhov, pgEdge, 2026-08-20)</li>
<li class=""><a href="https://www.postgresql.org/docs/18/parallel-plans.html" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 병렬 계획 문서</a></li>
<li class=""><a href="https://www.postgresql.org/docs/18/xaggr.html" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 집계 함수 확장 문서</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="병렬쿼리" term="병렬쿼리"/>
        <category label="HashAggregate" term="HashAggregate"/>
        <category label="planner" term="planner"/>
        <category label="DSM" term="DSM"/>
        <category label="성능" term="성능"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[pg_shmemviz 공유 메모리]]></title>
        <id>https://dbalog.dev/blog/pg-shmemviz</id>
        <link href="https://dbalog.dev/blog/pg-shmemviz"/>
        <updated>2026-08-24T07:00:00.000Z</updated>
        <summary type="html"><![CDATA[pg_walviz를 만든 Bertrand Drouvot이 이번엔 공유 메모리를 열었습니다. pg_shmem_allocations가 이름과 크기까지만 보여 주는 자리에서, pg_shmemviz는 DWARF 디버그 정보로 C 구조체의 필드 오프셋과 컴파일러 padding, NUMA 페이지 배치까지 브라우저에서 보여 줍니다. 개발용 도구이고 운영 인스턴스에서는 쓰면 안 된다는 조건이 붙습니다.]]></summary>
        <content type="html"><![CDATA[<p>일주일 전에 <a class="" href="https://dbalog.dev/blog/pg-walviz">pg_walviz</a>로 WAL segment 내부를 들여다봤는데, 같은 저자가 이번엔 공유 메모리를 열었어요. Bertrand Drouvot(베르트랑 드루보)이 2026년 8월 20일 <a href="https://github.com/bdrouvot/pg_shmemviz" target="_blank" rel="noopener noreferrer" class="">pg_shmemviz</a> v0.1.0-beta.1을 공개했습니다.</p>
<p>두 도구의 성격이 닮았습니다. 이미 있는 뷰로도 요약은 볼 수 있는데, 정작 "그게 메모리 어디에 어떤 모양으로 놓여 있나"는 안 보인다는 문제를 같은 방식으로 풉니다. 스냅샷을 떠서 브라우저에서 바이트 단위로 걸어 다니게 하는 방식입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pg_shmem_allocations는-어디까지-보여-주나">pg_shmem_allocations는 어디까지 보여 주나<a href="https://dbalog.dev/blog/pg-shmemviz#pg_shmem_allocations%EB%8A%94-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EB%B3%B4%EC%97%AC-%EC%A3%BC%EB%82%98" class="hash-link" aria-label="pg_shmem_allocations는 어디까지 보여 주나에 대한 직접 링크" title="pg_shmem_allocations는 어디까지 보여 주나에 대한 직접 링크" translate="no">​</a></h2>
<p>먼저 기존 뷰의 한계를 실측으로 확인했습니다. PostgreSQL 18 컨테이너를 기본 설정으로 띄우고 <code>pg_shmem_allocations</code>를 조회했습니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ docker run -d --name shmemtest -e POSTGRES_PASSWORD=pw postgres:18</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ docker exec shmemtest psql -U postgres -Atc "show shared_buffers"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">128MB</span><br></div></code></pre></div></div>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">off</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> size</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> allocated_size</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pg_shmem_allocations</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token keyword" style="color:#00009f">order</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">by</span><span class="token plain"> size </span><span class="token keyword" style="color:#00009f">desc</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">limit</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">         name         |    off    |   size    | allocated_size</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">----------------------+-----------+-----------+----------------</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> Buffer Blocks        |   6785664 | 134221824 |      134221824</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> &lt;anonymous&gt;          |           |   4747776 |        4747776</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> XLOG Ctl             |     60928 |   4208200 |        4208256</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> AioHandleIOV         | 150482816 |   2850816 |        2850816</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                      | 154760064 |   2280576 |        2280576</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> AioHandle            | 148879232 |   1603584 |        1603584</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> AioHandleData        | 153333632 |   1425408 |        1425408</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> Buffer Descriptors   |   5737088 |   1048576 |        1048576</span><br></div></code></pre></div></div>
<p>전체는 75개 항목, 합계 150MB였습니다. 여기서 볼 수 있는 것과 볼 수 없는 것이 꽤 선명하게 갈립니다.</p>
<p>보이는 것은 이름, 시작 오프셋, 요청 크기, 실제 할당 크기입니다. <code>XLOG Ctl</code>이 4,208,200바이트를 요청했는데 4,208,256바이트가 할당된 것을 보면 56바이트가 정렬 때문에 붙었다는 사실까지는 읽힙니다. PostgreSQL 18에서 비동기 I/O가 들어오면서 <code>AioHandle</code>, <code>AioHandleIOV</code>, <code>AioHandleData</code> 세 항목이 합쳐 5.8MB를 차지하는 것도 확인됩니다. <a class="" href="https://dbalog.dev/blog/postgresql-18-async-io">비동기 I/O 글</a>에서 다룬 io_uring 구조가 메모리에서 이 정도 자리를 쓴다는 뜻입니다.</p>
<p>안 보이는 것이 문제입니다. 위 출력에서 이름 칸이 빈 행이 하나 있는데, 이건 아직 아무에게도 배정되지 않은 여유 공간입니다. <code>&lt;anonymous&gt;</code>는 이름 없이 잡힌 익명 할당이라 오프셋조차 안 나옵니다. 그리고 <code>XLOG Ctl</code> 안에 <code>XLogCtlData</code> 구조체의 어떤 필드가 몇 번째 바이트에 앉아 있는지, 필드 사이에 컴파일러가 끼워 넣은 padding이 몇 바이트인지는 이 뷰의 관심사가 아닙니다.</p>
<p>정리하면 이렇습니다.</p>


















































<table><thead><tr><th>알고 싶은 것</th><th>pg_shmem_allocations</th><th>pg_shmemviz</th></tr></thead><tbody><tr><td>할당 이름과 크기</td><td>지원</td><td>지원</td></tr><tr><td>요청 크기 대 실제 크기 차이</td><td>지원</td><td>지원</td></tr><tr><td>할당 사이 빈 구간의 물리적 위치</td><td>부분 (이름 없는 행)</td><td>지원</td></tr><tr><td>C 구조체의 필드별 오프셋과 값</td><td>—</td><td>지원</td></tr><tr><td>필드 사이 컴파일러 padding</td><td>—</td><td>지원</td></tr><tr><td>포인터가 가리키는 대상 영역</td><td>—</td><td>지원</td></tr><tr><td>페이지의 NUMA 노드 배치</td><td>pg_shmem_allocations_numa</td><td>지원 (시각화)</td></tr><tr><td>두 시점 사이 차이 비교</td><td>수동</td><td>지원</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="실행-중인-서버에-붙지-않고-구조체를-읽는-방법">실행 중인 서버에 붙지 않고 구조체를 읽는 방법<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%8B%A4%ED%96%89-%EC%A4%91%EC%9D%B8-%EC%84%9C%EB%B2%84%EC%97%90-%EB%B6%99%EC%A7%80-%EC%95%8A%EA%B3%A0-%EA%B5%AC%EC%A1%B0%EC%B2%B4%EB%A5%BC-%EC%9D%BD%EB%8A%94-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="실행 중인 서버에 붙지 않고 구조체를 읽는 방법에 대한 직접 링크" title="실행 중인 서버에 붙지 않고 구조체를 읽는 방법에 대한 직접 링크" translate="no">​</a></h2>
<p>여기가 이 도구에서 가장 흥미로운 부분입니다. 공유 메모리의 바이트 배열을 읽는 것 자체는 어렵지 않습니다. 어려운 건 그 바이트가 무슨 구조체의 어느 필드인지 알아내는 일입니다. 그 정보는 소스 코드에만 있고 실행 중인 서버의 메모리에는 없습니다.</p>
<p>pg_shmemviz는 이 문제를 <code>postgres</code> 실행 파일의 DWARF 디버그 정보로 해결합니다. 컴파일된 바이너리에는 각 구조체의 필드 이름, 타입, 오프셋이 DWARF 형식으로 들어 있습니다. macOS에서는 LLDB, 그 외 환경에서는 GDB를 써서 이 메타데이터를 읽습니다. 중요한 건 디버거를 실행 중인 서버에 attach하지 않는다는 점입니다. 실행 파일의 메타데이터만 읽습니다.</p>
<!-- -->
<p>복사 단계에는 lock을 걸지 않습니다. 덕분에 서버를 멈추지 않지만, 대가가 있습니다. 스냅샷 안의 필드들이 서로 다른 순간의 값일 수 있습니다. 저자도 이 점을 문서에 명시했습니다. 그러니 "이 두 카운터의 차이가 정확히 몇인가"를 따지는 용도로는 맞지 않고, 구조와 배치를 파악하는 용도입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="설치와-사용">설치와 사용<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%84%A4%EC%B9%98%EC%99%80-%EC%82%AC%EC%9A%A9" class="hash-link" aria-label="설치와 사용에 대한 직접 링크" title="설치와 사용에 대한 직접 링크" translate="no">​</a></h2>
<p>extension과 CLI 두 부분으로 되어 있습니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">cd ~/pg_shmemviz</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">make PG_CONFIG=/path/to/postgres-install/bin/pg_config</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">make PG_CONFIG=/path/to/postgres-install/bin/pg_config install</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">psql -d postgres -c 'CREATE EXTENSION pg_shmemviz'</span><br></div></code></pre></div></div>
<p>스냅샷을 뜨는 명령입니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_shmemviz/bin/pg_shmemviz capture \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --pg-config /path/to/postgres-install/bin/pg_config \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --dbname postgres \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  /path/to/new-snapshot</span><br></div></code></pre></div></div>
<p>뜬 스냅샷을 브라우저로 봅니다. 기본값은 <code>127.0.0.1:8765</code>이고 로컬 브라우저가 열립니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_shmemviz/bin/pg_shmemviz serve /path/to/new-snapshot</span><br></div></code></pre></div></div>
<p>원격 서버에서 뜬 스냅샷을 SSH 포트 포워딩으로 볼 때는 브라우저 자동 실행을 끕니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_shmemviz/bin/pg_shmemviz serve --no-open --port 8765 /path/to/new-snapshot</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="화면이-보여-주는-것">화면이 보여 주는 것<a href="https://dbalog.dev/blog/pg-shmemviz#%ED%99%94%EB%A9%B4%EC%9D%B4-%EB%B3%B4%EC%97%AC-%EC%A3%BC%EB%8A%94-%EA%B2%83" class="hash-link" aria-label="화면이 보여 주는 것에 대한 직접 링크" title="화면이 보여 주는 것에 대한 직접 링크" translate="no">​</a></h2>
<p>뷰가 여러 개인데 서로 연동됩니다. 한쪽에서 할당을 고르면 다른 쪽이 같은 지점을 따라갑니다.</p>
<p>공유 메모리 맵은 main segment와 DSM, DSA 영역을 통틀어 이름 있는 할당, padding, 미사용 구간을 늘어놓습니다. 위 실측에서 이름 칸이 비어 있던 2.2MB가 어디에 어떤 이웃과 붙어 있는지가 여기서 드러납니다.</p>
<p>Structure Fields 패널이 이 도구의 핵심입니다. 중첩된 C 구조체를 펼쳐 필드별 오프셋과 값, 컴파일러가 끼운 padding, 배열의 stride padding을 보여 줍니다. 경계를 알 수 있는 포인터는 가리키는 영역을 참조 구간으로 표시합니다. 통계, WAL, 프로세스 배열, SLRU, dynahash, DSM registry 같은 PostgreSQL 특유의 구조에는 전용 해석이 들어가 있습니다.</p>
<p>Physical Bytes 뷰는 주소, 오프셋, 값, 어느 구조체 필드에 속하는지, 어느 NUMA 노드에 놓였는지를 함께 보여 주는 바이트 창입니다. Buffer Cache 뷰는 선택 사항인데, buffer별 식별자와 database, relation, fork, block 번호, 그리고 그 buffer를 pin하고 있는 backend까지 나옵니다. <code>pg_buffercache</code>로 보던 내용을 물리적 배치 위에 겹쳐 놓은 셈입니다.</p>
<p>NUMA 노드가 여러 개인 장비에서는 페이지 배치를 그림으로 봅니다. <code>pg_shmem_allocations_numa</code> 뷰가 숫자로 알려 주던 것을 눈으로 확인하는 용도입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="스냅샷-두-개를-비교한다">스냅샷 두 개를 비교한다<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%8A%A4%EB%83%85%EC%83%B7-%EB%91%90-%EA%B0%9C%EB%A5%BC-%EB%B9%84%EA%B5%90%ED%95%9C%EB%8B%A4" class="hash-link" aria-label="스냅샷 두 개를 비교한다에 대한 직접 링크" title="스냅샷 두 개를 비교한다에 대한 직접 링크" translate="no">​</a></h2>
<p>이 기능이 실무 관점에서 가장 쓸모 있어 보입니다. 서로 다른 시점의 스냅샷 두 개를 나란히 열어 할당 단위, 필드 단위, 바이트 단위로 차이를 봅니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_shmemviz/bin/pg_shmemviz serve \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  /path/to/before-snapshot \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  /path/to/after-snapshot</span><br></div></code></pre></div></div>
<p>설정 하나를 바꿨을 때 공유 메모리가 실제로 어떻게 달라지는지 확인하는 데 쓸 수 있습니다. 위 실측에서 PostgreSQL 18의 AIO 관련 세 할당이 5.8MB를 차지했는데, <code>io_method</code>를 바꾸기 전후로 스냅샷을 떠서 비교하면 그 5.8MB의 내부 구성이 어떻게 변하는지가 필드 단위로 보일 것입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="운영-인스턴스에서는-쓰지-않는다">운영 인스턴스에서는 쓰지 않는다<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%9A%B4%EC%98%81-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%97%90%EC%84%9C%EB%8A%94-%EC%93%B0%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4" class="hash-link" aria-label="운영 인스턴스에서는 쓰지 않는다에 대한 직접 링크" title="운영 인스턴스에서는 쓰지 않는다에 대한 직접 링크" translate="no">​</a></h2>
<p>저자가 문서 앞쪽에 강하게 못 박은 부분이라 그대로 옮깁니다.</p>
<blockquote>
<p>Do not run <code>pg_shmemviz</code> on a production PostgreSQL instance.</p>
</blockquote>
<p>이유가 몇 겹입니다. 스냅샷은 공유 메모리 전체를 복사한 파일이라 크고, 그 안에 실제 데이터가 그대로 들어갑니다. buffer에 올라온 테이블 내용이 파일로 떠지는 셈이니 민감 정보가 그대로 흘러나갑니다. viewer에는 인증도, 권한 검사도, TLS도 없습니다. loopback 밖으로 내보내면 안 됩니다.</p>
<p>구조체 해석에는 캡처한 서버가 쓰던 것과 정확히 같은 <code>postgres</code> 실행 파일이 필요합니다. 빌드가 다르면 필드 오프셋이 어긋나 엉뚱한 값을 읽습니다. 검증은 PostgreSQL 20devel 기준으로 되어 있고, 어느 버전까지 되는지는 저장소 README를 봐야 합니다. 제가 위에서 실측한 PostgreSQL 18 컨테이너 이미지에는 디버그 정보가 없으니, 실제로 붙여 보려면 디버그 심볼을 켜서 직접 빌드한 인스턴스가 필요합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어디에-쓰면-좋을까">어디에 쓰면 좋을까<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%96%B4%EB%94%94%EC%97%90-%EC%93%B0%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C" class="hash-link" aria-label="어디에 쓰면 좋을까에 대한 직접 링크" title="어디에 쓰면 좋을까에 대한 직접 링크" translate="no">​</a></h2>
<p>용도가 좁습니다. 운영 진단 도구가 아니고, extension이나 코어 패치를 개발하면서 "내가 잡은 공유 메모리 구조가 실제로 어떻게 배치됐나"를 확인하는 도구입니다. 그리고 학습 자료로서의 가치가 따로 있습니다. <code>XLogCtlData</code>나 <code>PGPROC</code> 배열이 메모리에서 어떤 모양인지 소스만 읽어서 상상하던 것을 눈으로 확인하는 경험은 소스 독해 속도를 꽤 올려 줍니다.</p>
<p><a class="" href="https://dbalog.dev/blog/pgsimcity-postgres-3d">PostgreSQL 내부를 3D 도시로 걸어 본 글</a>에서 시각화 도구가 학습에 어떤 도움이 되는지 이야기했는데, pg_shmemviz는 그보다 훨씬 실무 쪽에 가깝습니다. 비유가 아니라 실제 주소와 실제 바이트를 보여 주니까요.</p>
<p>pg_walviz와 pg_shmemviz가 한 주 간격으로 나왔습니다. 저는 이 흐름이 반갑습니다. PostgreSQL 내부는 소스를 읽을 수 있는 사람에게만 열려 있었는데, 그 문턱을 낮추는 도구가 늘고 있어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/pg-shmemviz#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://bdrouvot.github.io/2026/08/20/welcome-to-pg-shmemviz-postgresql-shared-memory-visualizer/" target="_blank" rel="noopener noreferrer" class="">pg_shmemviz 공개 글</a> (Bertrand Drouvot, 2026-08-20)</li>
<li class=""><a href="https://github.com/bdrouvot/pg_shmemviz" target="_blank" rel="noopener noreferrer" class="">pg_shmemviz 저장소</a></li>
<li class=""><a href="https://www.postgresql.org/docs/current/view-pg-shmem-allocations.html" target="_blank" rel="noopener noreferrer" class="">pg_shmem_allocations 공식 문서</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="shared_buffers" term="shared_buffers"/>
        <category label="공유메모리" term="공유메모리"/>
        <category label="DWARF" term="DWARF"/>
        <category label="디버깅" term="디버깅"/>
        <category label="도구" term="도구"/>
        <category label="NUMA" term="NUMA"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Code Concise]]></title>
        <id>https://dbalog.dev/blog/claude-code-concise-output-style</id>
        <link href="https://dbalog.dev/blog/claude-code-concise-output-style"/>
        <updated>2026-08-24T06:00:00.000Z</updated>
        <summary type="html"><![CDATA[v2.1.237에 내장 Concise 출력 스타일이 들어왔습니다. 결과부터 말하고 서론과 진행 설명을 빼되 작업 깊이는 유지한다는 스타일입니다. 출력 스타일이 CLAUDE.md와 어디서 갈라지는지, 시스템 프롬프트를 건드린다는 게 프롬프트 캐시와 서브에이전트에 무슨 뜻인지, 그리고 같은 주에 들어온 사용량 한도 자동 재개까지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>Claude Code 응답에서 제일 자주 걸러내고 싶은 게 뭘까요. 저는 "먼저 파일을 확인해 보겠습니다" 같은 예고 문장이었어요. v2.1.237에 그걸 기본으로 지워 주는 출력 스타일이 들어왔습니다.</p>
<p>이름은 Concise입니다. 변경 로그 한 줄은 이렇습니다.</p>
<blockquote>
<p>Added built-in "Concise" output style: Claude leads with results, skips preamble/narration, works thoroughly</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="내장-스타일이-다섯-개가-됐다">내장 스타일이 다섯 개가 됐다<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EB%82%B4%EC%9E%A5-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%B4-%EB%8B%A4%EC%84%AF-%EA%B0%9C%EA%B0%80-%EB%90%90%EB%8B%A4" class="hash-link" aria-label="내장 스타일이 다섯 개가 됐다에 대한 직접 링크" title="내장 스타일이 다섯 개가 됐다에 대한 직접 링크" translate="no">​</a></h2>
<p>출력 스타일은 원래 Default에 Explanatory와 Learning 둘이 붙은 구성이었습니다. 지금은 Proactive와 Concise가 더해져 Default 외에 네 개입니다.</p>



































<table><thead><tr><th>스타일</th><th>하는 일</th><th>출력 길이</th></tr></thead><tbody><tr><td>Default</td><td>기본 소프트웨어 엔지니어링 프롬프트</td><td>기준</td></tr><tr><td>Proactive</td><td>즉시 실행, 관례적 판단은 묻지 않고 진행</td><td>기준</td></tr><tr><td>Concise</td><td>결과부터, 서론과 진행 설명 생략</td><td>짧음</td></tr><tr><td>Explanatory</td><td>작업 중간에 구현 선택 이유를 설명</td><td>길다</td></tr><tr><td>Learning</td><td>설명에 더해 <code>TODO(human)</code> 표시로 직접 구현을 요청</td><td>길다</td></tr></tbody></table>
<p>Concise를 두고 오해하기 쉬운 부분이 하나 있습니다. 짧게 답하라는 게 작업을 덜 하라는 뜻은 아닙니다. 공식 문서는 "doing the engineering work as thoroughly as in the Default style"이라고 적어 뒀습니다. 설명을 요청하면 그때는 길게 답합니다. 그리고 짧게 만들지 않는 예외가 정해져 있습니다. 오류 보고, 보안 경고, 파괴적 작업의 확인 문구는 내용을 온전히 유지합니다. 이 예외 목록이 있다는 게 스타일 설계에서 제일 중요한 부분이라고 봅니다. 짧게 쓰다가 위험 신호를 줄여 버리면 절약이 아니라 사고니까요.</p>
<p>Proactive는 성격이 좀 다릅니다. 톤이 아니라 행동 방침을 바꿉니다. <a class="" href="https://dbalog.dev/blog/claude-code-auto-mode">auto mode</a>보다 강한 자율 실행 지침인데, 권한 모드는 그대로 둡니다. 무엇을 물어보지 않고 실행할지는 여전히 권한 모드가 결정하고, Proactive는 "판단을 사용자에게 넘기지 말고 스스로 하라"는 쪽만 건드립니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="스타일을-바꾸는-방법이-달라졌다">스타일을 바꾸는 방법이 달라졌다<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%84-%EB%B0%94%EA%BE%B8%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%B4-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EB%8B%A4" class="hash-link" aria-label="스타일을 바꾸는 방법이 달라졌다에 대한 직접 링크" title="스타일을 바꾸는 방법이 달라졌다에 대한 직접 링크" translate="no">​</a></h2>
<p>여기서 한 번 헤맬 수 있습니다. <code>/output-style</code> 명령이 없어졌습니다. v2.1.73에서 deprecated 되고 v2.1.91에서 제거됐습니다. 지금은 두 가지 방법뿐입니다.</p>
<p>터미널에서는 <code>/config</code>를 실행해 Output style 항목에서 고릅니다. 선택 결과는 프로젝트 로컬 설정 파일에 저장됩니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">.claude/settings.local.json</span><br></div></code></pre></div></div>
<p>설정 파일을 직접 고쳐도 됩니다.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"outputStyle"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Concise"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>데스크톱 앱에서는 <code>/config</code>가 메뉴 대신 Settings 화면을 엽니다. 그래서 데스크톱에서는 위 필드를 직접 넣는 쪽이 확실합니다.</p>
<p>바꾼 뒤 바로 안 바뀐다고 당황하지 않아도 됩니다. 출력 스타일은 시스템 프롬프트의 일부이고, 시스템 프롬프트는 세션이 시작할 때 한 번 읽습니다. <code>/clear</code>를 실행하거나 새 세션을 열어야 적용됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="claudemd와-어디서-갈라지나">CLAUDE.md와 어디서 갈라지나<a href="https://dbalog.dev/blog/claude-code-concise-output-style#claudemd%EC%99%80-%EC%96%B4%EB%94%94%EC%84%9C-%EA%B0%88%EB%9D%BC%EC%A7%80%EB%82%98" class="hash-link" aria-label="CLAUDE.md와 어디서 갈라지나에 대한 직접 링크" title="CLAUDE.md와 어디서 갈라지나에 대한 직접 링크" translate="no">​</a></h2>
<p>이게 문서에서 가장 값이 나가는 대목입니다. 둘 다 "Claude가 이렇게 행동하게 만드는 장치"인데 붙는 위치가 다릅니다.</p>
<!-- -->
<p>출력 스타일은 시스템 프롬프트 끝에 붙습니다. CLAUDE.md는 시스템 프롬프트 뒤에 오는 user message로 들어갑니다. 이 차이가 실무에서 세 갈래 결과를 만듭니다.</p>
<p>첫째, 커스텀 출력 스타일은 기본 소프트웨어 엔지니어링 지침을 빼 버립니다. 변경 범위를 어떻게 잡고, 주석을 어떻게 쓰고, 작업을 어떻게 검증하라는 내장 지침 전체가 사라집니다. 그걸 유지하려면 frontmatter에 <code>keep-coding-instructions: true</code>를 넣어야 합니다. 기본값이 <code>false</code>라는 걸 모르고 커스텀 스타일을 만들면, 톤만 바꾸려던 게 코딩 행동까지 바꿔 버립니다.</p>
<p>둘째, 서브에이전트에는 적용되지 않습니다. 서브에이전트는 자기 시스템 프롬프트로 돕니다. 예외가 fork인데, <a class="" href="https://dbalog.dev/blog/claude-code-fork-session-mentions">fork는 부모의 시스템 프롬프트를 통째로 물려받기</a> 때문입니다. Concise로 세션을 돌리면서 서브에이전트에게 요약을 맡겼는데 서브에이전트 응답이 장황한 이유가 여기 있습니다.</p>
<p>셋째, 프롬프트 캐시입니다. 시스템 프롬프트가 바뀌면 캐시 접두사가 깨집니다. <a class="" href="https://dbalog.dev/blog/claude-code-maximizing-sessions">토큰 절약 가이드 글</a>에서 모델과 effort를 세션 시작 시점에 고정하라는 권고를 다뤘는데, 출력 스타일도 같은 부류의 설정입니다. 세션 도중에 바꿀 값이 아닙니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="커스텀-스타일은-파일-하나다">커스텀 스타일은 파일 하나다<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%80-%ED%8C%8C%EC%9D%BC-%ED%95%98%EB%82%98%EB%8B%A4" class="hash-link" aria-label="커스텀 스타일은 파일 하나다에 대한 직접 링크" title="커스텀 스타일은 파일 하나다에 대한 직접 링크" translate="no">​</a></h2>
<p>내장 다섯 개로 부족하면 마크다운 파일을 만듭니다. 위치는 세 곳이고, 파일명이 스타일 이름이 됩니다. frontmatter에 <code>name</code>을 적으면 그쪽이 이깁니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/.claude/output-styles/          # 사용자 전역</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">.claude/output-styles/            # 프로젝트</span><br></div></code></pre></div></div>
<p>공식 문서 예시가 성격을 잘 보여 줍니다.</p>
<div class="language-markdown codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-markdown codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token front-matter-block punctuation" style="color:#393A34">---</span><span class="token front-matter-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token front-matter-block"></span><span class="token front-matter-block front-matter yaml language-yaml key atrule" style="color:#00a4db">name</span><span class="token front-matter-block front-matter yaml language-yaml punctuation" style="color:#393A34">:</span><span class="token front-matter-block front-matter yaml language-yaml"> Diagrams first</span><br></div><div class="token-line" style="color:#393A34"><span class="token front-matter-block front-matter yaml language-yaml"></span><span class="token front-matter-block front-matter yaml language-yaml key atrule" style="color:#00a4db">description</span><span class="token front-matter-block front-matter yaml language-yaml punctuation" style="color:#393A34">:</span><span class="token front-matter-block front-matter yaml language-yaml"> Lead every explanation with a diagram</span><br></div><div class="token-line" style="color:#393A34"><span class="token front-matter-block front-matter yaml language-yaml"></span><span class="token front-matter-block front-matter yaml language-yaml key atrule" style="color:#00a4db">keep-coding-instructions</span><span class="token front-matter-block front-matter yaml language-yaml punctuation" style="color:#393A34">:</span><span class="token front-matter-block front-matter yaml language-yaml"> </span><span class="token front-matter-block front-matter yaml language-yaml boolean important" style="color:#36acaa">true</span><span class="token front-matter-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token front-matter-block"></span><span class="token front-matter-block punctuation" style="color:#393A34">---</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">When explaining code, architecture, or data flow, start with a Mermaid diagram</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">showing the structure, then explain in prose.</span><br></div></code></pre></div></div>
<p>프로젝트 스타일은 작업 디렉터리부터 저장소 루트까지의 모든 <code>.claude/output-styles/</code>에서 읽습니다. 같은 이름이 여러 층에 있으면 작업 디렉터리에 가까운 쪽이 이깁니다. 플러그인도 <code>output-styles/</code> 디렉터리로 스타일을 배포할 수 있는데, 플러그인 전용 필드인 <code>force-for-plugin: true</code>를 켜면 사용자의 <code>outputStyle</code> 설정을 덮어쓰고 강제 적용됩니다. 플러그인을 깔았는데 응답 톤이 갑자기 변했다면 이 필드를 의심해 볼 만합니다.</p>
<p>frontmatter 필드는 네 개입니다.</p>






























<table><thead><tr><th>필드</th><th>뜻</th><th>기본값</th></tr></thead><tbody><tr><td><code>name</code></td><td>스타일 이름</td><td>파일명</td></tr><tr><td><code>description</code></td><td><code>/config</code> 선택 화면에 뜨는 설명</td><td>없음</td></tr><tr><td><code>keep-coding-instructions</code></td><td>내장 엔지니어링 지침 유지</td><td><code>false</code></td></tr><tr><td><code>force-for-plugin</code></td><td>플러그인 활성 시 강제 적용</td><td><code>false</code></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="토큰은-어느-쪽으로-움직이나">토큰은 어느 쪽으로 움직이나<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%ED%86%A0%ED%81%B0%EC%9D%80-%EC%96%B4%EB%8A%90-%EC%AA%BD%EC%9C%BC%EB%A1%9C-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%82%98" class="hash-link" aria-label="토큰은 어느 쪽으로 움직이나에 대한 직접 링크" title="토큰은 어느 쪽으로 움직이나에 대한 직접 링크" translate="no">​</a></h2>
<p>방향이 둘로 갈립니다. 입력 토큰은 스타일 지침이 시스템 프롬프트에 붙는 만큼 늘어납니다. 다만 첫 요청 이후에는 프롬프트 캐시가 이 비용을 상당히 덮습니다. 출력 토큰은 스타일이 결정합니다. Explanatory와 Learning은 설계상 응답이 길어지고, Concise는 반대로 갑니다.</p>
<p>그러니 Concise의 절약 효과는 출력 쪽입니다. 응답 길이가 짧아지는 만큼 출력 토큰이 줄고, 그 짧은 응답이 다음 턴의 입력으로 다시 들어가니 대화가 길어질수록 차이가 누적됩니다. 반대로 Learning 스타일로 긴 세션을 돌리면 그 반대 방향으로 누적됩니다. 제가 지금 이 글을 쓰는 세션이 Learning 스타일인데, 확실히 응답 길이가 기본보다 깁니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="같은-주에-들어온-나머지">같은 주에 들어온 나머지<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EA%B0%99%EC%9D%80-%EC%A3%BC%EC%97%90-%EB%93%A4%EC%96%B4%EC%98%A8-%EB%82%98%EB%A8%B8%EC%A7%80" class="hash-link" aria-label="같은 주에 들어온 나머지에 대한 직접 링크" title="같은 주에 들어온 나머지에 대한 직접 링크" translate="no">​</a></h2>
<p>Concise만 있던 주가 아닙니다. v2.1.234에 실무에서 체감될 항목이 둘 붙었습니다.</p>
<p>사용량 한도 자동 재개입니다. claude.ai 사용량 한도가 초기화되면 세션을 자동으로 이어서 진행합니다. <code>/config</code>에서 켜고 끕니다. 장시간 작업을 돌려 두고 자리를 비우는 사용 방식이라면 이 항목 하나로 흐름이 달라집니다. <a class="" href="https://dbalog.dev/blog/claude-code-routines">Routines</a>처럼 사람이 안 보는 동안 도는 작업과 결이 같습니다.</p>
<p>GitLab merge request 배지도 들어왔습니다. GitLab remote가 걸린 저장소에서 <code>glab</code>으로 인증돼 있으면 <code>MR !N</code> 형태로 draft, pending, green 상태를 상태줄에 표시합니다. GitHub PR 배지만 있던 자리에 GitLab이 붙은 셈입니다.</p>
<p>이후 버전도 흐름이 이어집니다. v2.1.238에는 커스텀, 프로젝트, 플러그인 출력 스타일이 세션 도중에 기본 목소리로 되돌아가던 버그 수정이 들어갔습니다. 출력 스타일 자체를 손보는 작업이 한 주 내내 계속됐다는 뜻입니다. v2.1.239에서는 사용량 한도 안내가 세션, 주간, 월간 중 무엇이 초기화되는지 구분해 알려 주게 바뀌었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어떤-스타일로-쓸까">어떤 스타일로 쓸까<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EC%96%B4%EB%96%A4-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A1%9C-%EC%93%B8%EA%B9%8C" class="hash-link" aria-label="어떤 스타일로 쓸까에 대한 직접 링크" title="어떤 스타일로 쓸까에 대한 직접 링크" translate="no">​</a></h2>
<p>정답은 작업 성격에 달렸습니다. 제 기준으로는 이렇게 갈립니다.</p>
<p>익숙한 코드베이스에서 반복 작업을 돌릴 때는 Concise가 맞습니다. 무엇을 할지 이미 알고 있으니 예고가 필요 없습니다. 처음 보는 코드베이스를 파악하는 중이라면 Explanatory가 낫습니다. "왜 이 파일을 골랐나"가 정보이기 때문입니다. 새 기술을 배우려고 붙었다면 Learning이 값을 합니다. <code>TODO(human)</code> 표시가 남으면 직접 손을 대야 하고, 그 지점이 대개 판단이 필요한 자리입니다.</p>
<p>Proactive는 조심스럽습니다. 권한 모드를 안 건드린다지만 "묻지 말고 판단하라"는 지침 자체가 리스크입니다. 되돌리기 쉬운 작업에만 쓰는 쪽이 안전합니다.</p>
<p>그리고 이 다섯 개는 서로 배타적입니다. 한 세션에 하나만 걸립니다. 작업 성격이 바뀌면 <code>/clear</code>하고 다시 고르는 게 정석입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/claude-code-concise-output-style#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreferrer" class="">Claude Code CHANGELOG</a></li>
<li class=""><a href="https://code.claude.com/docs/en/output-styles" target="_blank" rel="noopener noreferrer" class="">Output styles 공식 문서</a></li>
<li class=""><a href="https://code.claude.com/docs/en/permission-modes" target="_blank" rel="noopener noreferrer" class="">Permission modes 공식 문서</a></li>
</ul>]]></content>
        <category label="Claude Code" term="Claude Code"/>
        <category label="output style" term="output style"/>
        <category label="AI" term="AI"/>
        <category label="생산성" term="생산성"/>
        <category label="토큰" term="토큰"/>
        <category label="프롬프트 캐시" term="프롬프트 캐시"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude 도구 API 정식 출시]]></title>
        <id>https://dbalog.dev/blog/claude-skills-files-computer-use-ga</id>
        <link href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga"/>
        <updated>2026-08-24T05:00:00.000Z</updated>
        <summary type="html"><![CDATA[2026년 8월 20일 Claude 플랫폼에서 computer use, Skills API, Files API가 정식 출시됐습니다. computer use는 turn당 여러 동작을 묶어 실행하게 바뀌고 웹 전용 browser use tool이 새로 붙었으며, Skills는 버전 관리되는 저장 객체가 됐습니다. 도구 식별자와 beta 헤더, 그리고 Skills를 Managed Agents와 혼동하기 쉬운 지점까지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>에이전트에게 "이 PDF 읽고 저 웹 포털에 대신 입력해 줘"를 시키는 데 필요한 조각이 셋인데, 그 셋이 같은 날 정식 출시됐습니다. 2026년 8월 20일입니다.</p>
<p>Anthropic이 <a href="https://claude.com/blog/computer-use-skills-api-files-api" target="_blank" rel="noopener noreferrer" class="">computer use, Skills API, Files API 세 가지의 정식 출시</a>를 발표했습니다. 각각 따로 보면 기능 추가인데, 셋을 붙여 놓으면 성격이 달라집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="세-조각이-각각-맡는-일">세 조각이 각각 맡는 일<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#%EC%84%B8-%EC%A1%B0%EA%B0%81%EC%9D%B4-%EA%B0%81%EA%B0%81-%EB%A7%A1%EB%8A%94-%EC%9D%BC" class="hash-link" aria-label="세 조각이 각각 맡는 일에 대한 직접 링크" title="세 조각이 각각 맡는 일에 대한 직접 링크" translate="no">​</a></h2>
<p>발표문이 든 예시가 구성을 잘 보여 줍니다. 보험 청구 처리 에이전트입니다. Files API에서 접수 문서를 읽고, 팀의 접수 절차를 담은 skill을 따라, browser use tool로 보험사 웹 포털에서 제출을 마치고, 확인서를 다시 파일로 저장합니다.</p>

























<table><thead><tr><th>조각</th><th>맡는 일</th></tr></thead><tbody><tr><td>Files API</td><td>에이전트가 읽고 쓰는 문서의 저장소</td></tr><tr><td>Skills API</td><td>팀의 절차를 코드와 문서로 묶어 올려 두는 곳</td></tr><tr><td>computer use</td><td>화면을 보고 클릭하고 입력하는 손</td></tr><tr><td>browser use tool</td><td>웹 애플리케이션 전용 손</td></tr></tbody></table>
<p>역할 분담이 사람의 업무 구조와 닮았습니다. 문서, 절차서, 그리고 소프트웨어를 조작하는 손입니다. 지금까지는 이 셋 중 손 쪽이 가장 불안했습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="computer-use-도구-식별자가-toolset으로-바뀌었다">computer use: 도구 식별자가 toolset으로 바뀌었다<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#computer-use-%EB%8F%84%EA%B5%AC-%EC%8B%9D%EB%B3%84%EC%9E%90%EA%B0%80-toolset%EC%9C%BC%EB%A1%9C-%EB%B0%94%EB%80%8C%EC%97%88%EB%8B%A4" class="hash-link" aria-label="computer use: 도구 식별자가 toolset으로 바뀌었다에 대한 직접 링크" title="computer use: 도구 식별자가 toolset으로 바뀌었다에 대한 직접 링크" translate="no">​</a></h2>
<p>여기가 실무에서 가장 먼저 걸리는 부분입니다. 도구 타입 문자열이 바뀌었습니다.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"computer_toolset_20260801"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"name"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"computer"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>Claude API에서는 beta 헤더가 필요하지 않습니다. 이전 버전인 <code>computer_20251124</code>는 <code>anthropic-beta: computer-20251124-api</code> 헤더가 필요했고, 지금은 구형 모델용으로 남았습니다.</p>
<p>지원 모델이 갈립니다. <code>computer_toolset_20260801</code>은 <code>claude-opus-5</code>, <code>claude-sonnet-5</code>, <code>claude-opus-4-8</code>, <code>claude-fable-5</code>, <code>claude-mythos-5</code>에서 동작합니다. Opus 4.7과 4.6, Sonnet 4.6, Opus 4.5는 이전 <code>computer_20251124</code>를 써야 합니다.</p>
<p>파라미터에서 사라진 게 하나 있습니다. 화면 너비와 높이입니다. 이전 버전은 <code>display_width_px</code> 같은 값을 받았는데, <code>computer_toolset_20260801</code>은 받지 않습니다. 좌표를 돌려주는 스크린샷에서 직접 읽습니다. 기존 코드를 옮길 때 이 두 파라미터를 그대로 두면 거부됩니다.</p>
<p>이름이 tool에서 toolset으로 바뀐 이유는 안에 든 것이 하나가 아니기 때문입니다. member tool이 17개입니다.</p>

































<table><thead><tr><th>분류</th><th>member</th></tr></thead><tbody><tr><td>화면 읽기</td><td><code>screenshot</code>, <code>zoom</code></td></tr><tr><td>클릭</td><td><code>left_click</code>, <code>right_click</code>, <code>middle_click</code>, <code>double_click</code>, <code>triple_click</code></td></tr><tr><td>포인터</td><td><code>mouse_move</code>, <code>left_click_drag</code>, <code>left_mouse_down</code>, <code>left_mouse_up</code>, <code>cursor_position</code></td></tr><tr><td>스크롤</td><td><code>scroll</code></td></tr><tr><td>키보드</td><td><code>type</code>, <code>key</code>, <code>hold_key</code></td></tr><tr><td>대기</td><td><code>wait</code></td></tr></tbody></table>
<p><code>zoom</code>이 눈에 띕니다. 지정한 영역을 원본 해상도로 다시 캡처합니다. 전체 화면 스크린샷은 축소되어 작은 글자를 놓치기 쉬운데, 그 문제를 영역 재촬영으로 풉니다. <code>hold_key</code>와 <code>wait</code>는 최대 300초까지 받습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="turn당-한-동작에서-여러-동작으로">turn당 한 동작에서 여러 동작으로<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#turn%EB%8B%B9-%ED%95%9C-%EB%8F%99%EC%9E%91%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EB%8F%99%EC%9E%91%EC%9C%BC%EB%A1%9C" class="hash-link" aria-label="turn당 한 동작에서 여러 동작으로에 대한 직접 링크" title="turn당 한 동작에서 여러 동작으로에 대한 직접 링크" translate="no">​</a></h2>
<p>베타에서 정식 출시로 오면서 가장 크게 바뀐 지점입니다. 이전에는 모델 호출 한 번에 동작 하나였습니다. 지금은 한 응답에 여러 <code>tool_use</code> 블록을 담습니다.</p>
<!-- -->
<p>로그인 폼을 채우는 작업을 생각해 보면 차이가 큽니다. 아이디 칸 클릭, 입력, 비밀번호 칸 클릭, 입력, 로그인 클릭이면 모델 호출 5회였던 것이 1회가 됩니다. 지연과 비용이 함께 줄어듭니다.</p>
<p>대신 실패 처리 규칙을 알아야 합니다. 동작은 순서대로 실행되고 첫 실패에서 멈춥니다. 실행되지 않은 나머지 동작에는 이런 결과를 돌려줘야 합니다.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tool_result"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"tool_use_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"toolu_01..."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"toolset_name"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"computer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"is_error"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"content"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Not executed: an earlier computer action in this turn failed."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p><code>toolset_name</code> 필드가 모든 결과에 들어가야 합니다. 값은 <code>"computer"</code>입니다. 예전 방식으로 <code>tool_use_id</code>와 <code>content</code>만 채우면 통과하지 않습니다.</p>
<p>한 동작씩 진행하고 싶으면 병렬 도구 사용을 끕니다.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"tool_choice"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tool"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"disable_parallel_tool_use"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>화면 상태를 매 단계 확인해야 하는 작업이라면 이쪽이 안전합니다. 화면이 예상과 달라졌는데 나머지 클릭이 그대로 나가면 엉뚱한 곳을 누릅니다.</p>
<p>browser use tool은 여기서 한 겹 더 갑니다. 픽셀 좌표만 쓰는 것보다 웹 요소를 안정적으로 겨냥하도록 페이지 구조 분석을 더했습니다. 웹 애플리케이션만 다룰 거라면 이쪽이 맞습니다.</p>
<p>의료 쪽 이야기도 붙었습니다. computer use가 BAA 아래 HIPAA 규제 대상 워크로드에 쓸 수 있게 됐습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="skills-api-skill이-저장-객체가-됐다">Skills API: skill이 저장 객체가 됐다<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#skills-api-skill%EC%9D%B4-%EC%A0%80%EC%9E%A5-%EA%B0%9D%EC%B2%B4%EA%B0%80-%EB%90%90%EB%8B%A4" class="hash-link" aria-label="Skills API: skill이 저장 객체가 됐다에 대한 직접 링크" title="Skills API: skill이 저장 객체가 됐다에 대한 직접 링크" translate="no">​</a></h2>
<p><a class="" href="https://dbalog.dev/blog/claude-code-skills">Claude Code의 Skills</a>를 다룬 적이 있는데, 그때는 로컬 디렉터리에 마크다운 파일을 두는 구조였습니다. 이제 API 층으로 올라와 저장되고 버전이 붙는 객체가 됐습니다.</p>
<p>skill은 지시문과 스크립트, 템플릿이 든 폴더입니다. 작업이 필요할 때만 Claude가 읽어 들이고, Claude의 코드 실행 sandbox 안에서 돕니다. 직접 호스팅할 서버가 없습니다.</p>
<p>엔드포인트 구성입니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">POST   /v1/skills                                    # 생성 (multipart/form-data)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GET    /v1/skills                                    # 목록</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GET    /v1/skills/{skill_id}                         # 조회</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DELETE /v1/skills/{skill_id}                         # 삭제</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">POST   /v1/skills/{skill_id}/versions                # 새 버전 업로드</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GET    /v1/skills/{skill_id}/versions                # 버전 목록</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GET    /v1/skills/{skill_id}/versions/{version}      # 버전 조회</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DELETE /v1/skills/{skill_id}/versions/{version}      # 버전 삭제</span><br></div></code></pre></div></div>
<p>버전 참조에 <code>latest</code> 리터럴을 쓸 수 있습니다. <code>latest_version_id</code> 필드가 가리키는 곳으로 해석됩니다. 다만 <code>skills-2025-10-02</code> beta 헤더를 붙인 요청은 버전을 Unix epoch 타임스탬프로 주소지정합니다. 예시가 <code>"1759178010641129"</code> 형태입니다. beta 경로와 정식 경로에서 버전 식별 방식이 다르니 여기서 한 번 헤맬 수 있습니다.</p>
<p><code>name</code> 필드의 성격이 중요합니다. 첫 업로드의 <code>SKILL.md</code> frontmatter에 있는 <code>name</code>(없으면 그 폴더 이름)에서 kebab-case slug로 정해지고, 이후 바뀌지 않습니다. 나중 업로드도 같은 값으로 해석돼야 합니다. 이 slug가 마운트된 파일의 최상위 디렉터리 이름이 되고, 내려받을 때 아카이브 파일명이 됩니다.</p>
<p>skill의 출처는 네 종류로 구분됩니다.</p>

























<table><thead><tr><th><code>source.type</code></th><th>뜻</th></tr></thead><tbody><tr><td><code>custom</code></td><td>사용자가 작성. 해당 workspace 전용</td></tr><tr><td><code>anthropic</code></td><td>Anthropic 발행. 공유되며 읽기 전용</td></tr><tr><td><code>anthropic_example</code></td><td>Anthropic 발행 예제</td></tr><tr><td><code>plugin</code></td><td>설치된 플러그인에서 해석</td></tr></tbody></table>
<p>요청에 붙이는 방법에서 혼동이 잘 생깁니다. Skills를 쓰려면 세 가지를 함께 넣습니다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">beta</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">messages</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"claude-opus-5"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    max_tokens</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">16000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    container</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"skills"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"skill_id"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"skill_01..."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"version"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"latest"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    tools</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"code_execution_20260521"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"name"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"code_execution"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    betas</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"code-execution-2025-08-25"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"skills-2025-10-02"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    messages</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p><code>container</code>에 skill을 얹고, 코드 실행 도구를 선언하고, beta 두 개를 붙입니다. skill이 sandbox 안에서 도니까 코드 실행 도구가 함께 필요합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="managed-agents와-헷갈리지-않기">Managed Agents와 헷갈리지 않기<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#managed-agents%EC%99%80-%ED%97%B7%EA%B0%88%EB%A6%AC%EC%A7%80-%EC%95%8A%EA%B8%B0" class="hash-link" aria-label="Managed Agents와 헷갈리지 않기에 대한 직접 링크" title="Managed Agents와 헷갈리지 않기에 대한 직접 링크" translate="no">​</a></h2>
<p>여기가 정말 자주 어긋나는 지점입니다. Skills는 Managed Agents가 아닙니다.</p>
<p>Managed Agents는 별개 표면입니다. <code>POST /v1/agents</code>로 에이전트 설정을 저장해 두고, 세션을 만들어 실행합니다. 세션마다 컨테이너가 workspace로 할당되고 에이전트 루프 자체를 Anthropic이 돌립니다. beta 헤더도 다릅니다. <code>managed-agents-2026-04-01</code>입니다.</p>
<p>반면 Skills로 문서를 만들게 하려면 위처럼 <code>client.beta.messages.create</code>에 <code>container</code>와 코드 실행 도구를 얹으면 끝입니다. <code>client.beta.agents</code>나 세션 API를 쓰는 게 아닙니다. Skills가 Managed Agents 안에서도 쓰이기 때문에 문서를 훑다 보면 두 경로가 섞여 보입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="files-api-한-번-올리고-id로-부른다">Files API: 한 번 올리고 ID로 부른다<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#files-api-%ED%95%9C-%EB%B2%88-%EC%98%AC%EB%A6%AC%EA%B3%A0-id%EB%A1%9C-%EB%B6%80%EB%A5%B8%EB%8B%A4" class="hash-link" aria-label="Files API: 한 번 올리고 ID로 부른다에 대한 직접 링크" title="Files API: 한 번 올리고 ID로 부른다에 대한 직접 링크" translate="no">​</a></h2>
<p>성격은 단순합니다. PDF나 스프레드시트를 한 번 올려 두고, 이후 요청에서는 다시 보내지 않고 ID로 참조합니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">POST /v1/files</span><br></div></code></pre></div></div>
<p>beta 헤더는 <code>files-api-2025-04-14</code>입니다. 업로드할 때와, 그 파일을 참조하는 <code>messages.create</code> 양쪽에 모두 붙여야 합니다. 한쪽만 붙이면 실패합니다.</p>
<p>참조할 때는 content block 타입이 파일의 MIME 타입과 맞아야 합니다.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"document"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"source"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"file_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file_01..."</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>PDF와 텍스트는 <code>document</code>, 이미지는 <code>image</code>입니다.</p>
<p>정식 출시로 오면서 늘어난 것이 셋입니다. 파일 자동 만료가 들어왔고, rate limit이 5배로 올랐고, 조직당 저장 용량이 1TB가 됐습니다. 자동 만료는 편의 기능처럼 보이지만 실무에서는 정리 부담을 덜어 줍니다. 업로드해 둔 파일을 지우는 코드를 따로 관리하지 않아도 됩니다.</p>
<p>base64로 PDF를 매번 실어 보내는 방식과 비교하면 차이가 분명합니다. base64 경로는 요청 32MB, 600페이지 제한이 걸립니다. 같은 문서를 여러 번 물어볼 거라면 매 요청마다 그 크기를 다시 전송하는 셈입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어디서-쓸-수-있나">어디서 쓸 수 있나<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#%EC%96%B4%EB%94%94%EC%84%9C-%EC%93%B8-%EC%88%98-%EC%9E%88%EB%82%98" class="hash-link" aria-label="어디서 쓸 수 있나에 대한 직접 링크" title="어디서 쓸 수 있나에 대한 직접 링크" translate="no">​</a></h2>
<p>플랫폼마다 갈립니다. 세 기능 모두 Claude 플랫폼에서 쓸 수 있습니다. Skills API와 Files API는 Microsoft Foundry에서도 접근됩니다. computer tool과 browser tool은 Google Cloud Vertex AI에 들어올 예정입니다.</p>
<p>computer use는 플랫폼별로 버전이 다릅니다. <code>computer_toolset_20260801</code> 전체 지원은 Claude API고, Claude Platform on AWS와 Bedrock, Google Cloud, Foundry는 이전 버전이 beta로 올라가 있습니다. 특정 클라우드에 묶여 있다면 도구 식별자와 모델을 같이 확인해야 합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="무엇이-실제로-달라지나">무엇이 실제로 달라지나<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#%EB%AC%B4%EC%97%87%EC%9D%B4-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EB%8B%AC%EB%9D%BC%EC%A7%80%EB%82%98" class="hash-link" aria-label="무엇이 실제로 달라지나에 대한 직접 링크" title="무엇이 실제로 달라지나에 대한 직접 링크" translate="no">​</a></h2>
<p>세 기능의 정식 출시를 하나로 묶으면, 에이전트가 API를 가진 시스템만 다루던 단계에서 벗어난다는 뜻입니다. 화면만 있는 소프트웨어, 예를 들어 사내 레거시 웹 콘솔이나 외부 기관 포털은 자동화 대상이 아니었습니다. 사람이 대신 클릭했습니다.</p>
<p>다만 <a class="" href="https://dbalog.dev/blog/claudebleed-claude-chrome-extension">ClaudeBleed 사례</a>를 다루면서 봤듯이, 브라우저를 조작하는 권한은 그 자체로 공격 표면입니다. 화면을 보고 클릭하는 에이전트에게는 세션 쿠키가 붙은 브라우저가 그대로 열려 있습니다. 웹 포털에 자동 입력을 맡기기 전에, 그 에이전트가 어떤 페이지까지 갈 수 있는지 경계를 먼저 정해 두는 편이 좋습니다.</p>
<p>turn당 여러 동작이 묶이는 변화는 그래서 양면입니다. 비용과 지연이 줄지만, 잘못된 화면 판단 하나가 다섯 번의 클릭으로 번집니다. 되돌리기 어려운 작업에서는 <code>disable_parallel_tool_use</code>를 켜는 쪽이 낫다고 봅니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/claude-skills-files-computer-use-ga#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://claude.com/blog/computer-use-skills-api-files-api" target="_blank" rel="noopener noreferrer" class="">computer use, Skills API, Files API 정식 출시 발표</a> (2026-08-20)</li>
<li class=""><a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool" target="_blank" rel="noopener noreferrer" class="">computer use tool 공식 문서</a></li>
<li class=""><a href="https://platform.claude.com/docs/en/api/skills" target="_blank" rel="noopener noreferrer" class="">Skills API 레퍼런스</a></li>
</ul>]]></content>
        <category label="Claude" term="Claude"/>
        <category label="API" term="API"/>
        <category label="computer use" term="computer use"/>
        <category label="Skills" term="Skills"/>
        <category label="에이전트" term="에이전트"/>
        <category label="AI" term="AI"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PMM ClickHouse 권한 취약점]]></title>
        <id>https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory</id>
        <link href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory"/>
        <updated>2026-08-24T04:00:00.000Z</updated>
        <summary type="html"><![CDATA[Percona Monitoring and Management 3.9.0 이하에서 Grafana의 ClickHouse 데이터소스가 SOURCES 권한을 전부 가진 기본 신원으로 연결돼 있었습니다. Viewer 권한만 있어도 임의 SQL을 넣을 수 있고, url() 함수로 AWS 메타데이터까지 이어지는 경로가 나옵니다. CVSS 8.7이고 3.9.1에서 수정됐습니다. ClickHouse의 SOURCES 권한이 실제로 무엇을 여는지 직접 확인했습니다.]]></summary>
        <content type="html"><![CDATA[<p>DB를 지키려고 붙인 모니터링 도구가 DB로 들어가는 문이 되면 곤란하겠죠. 2026년 8월 19일 Percona가 자사 모니터링 제품에서 그런 경로를 공개했습니다.</p>
<p><a href="https://www.percona.com/blog/security-advisory-privileged-clickhouse-access-through-the-grafana-data-source-in-pmm/" target="_blank" rel="noopener noreferrer" class="">Percona 보안 공지</a>의 대상은 Percona Monitoring and Management(PMM) 3.9.0 이하입니다. CVSS는 8.7, CVE 번호는 공지 시점에 아직 배정되지 않았고 배정되면 공지를 갱신한다고 적혀 있습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="문제의-모양">문제의 모양<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EB%AC%B8%EC%A0%9C%EC%9D%98-%EB%AA%A8%EC%96%91" class="hash-link" aria-label="문제의 모양에 대한 직접 링크" title="문제의 모양에 대한 직접 링크" translate="no">​</a></h2>
<p>PMM은 Grafana를 화면으로 쓰고, 쿼리 분석 데이터를 ClickHouse에 담습니다. 문제는 그 둘을 잇는 데이터소스 설정에 있었습니다.</p>
<p>두 가지가 겹칩니다. 첫째, Grafana는 로그인한 사용자가 raw data source API에 접근하는 것을 허용합니다. Viewer 역할도 포함입니다. 대시보드만 볼 수 있는 계정이 데이터소스에 직접 쿼리를 보낼 수 있다는 뜻입니다. 둘째, ClickHouse 데이터소스가 연결할 때 쓰는 기본 신원이 전역 DDL, DML, SOURCES 권한을 가지고 있었습니다.</p>
<!-- -->
<p>Grafana에서 anonymous access를 켜 두면 로그인하지 않은 사용자까지 이 경로에 닿습니다. 기본값은 꺼져 있습니다. 이 조건은 명확히 해 두는 게 좋습니다. 뒤에 나오는 전체 공격 체인은 anonymous access가 켜져 있고 AWS EC2에 올라간 배포에서 성립합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sources-권한이-실제로-무엇을-여는가">SOURCES 권한이 실제로 무엇을 여는가<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#sources-%EA%B6%8C%ED%95%9C%EC%9D%B4-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%97%AC%EB%8A%94%EA%B0%80" class="hash-link" aria-label="SOURCES 권한이 실제로 무엇을 여는가에 대한 직접 링크" title="SOURCES 권한이 실제로 무엇을 여는가에 대한 직접 링크" translate="no">​</a></h2>
<p>공지가 <code>url()</code> 함수를 언급하는데, ClickHouse를 자주 다루지 않으면 이게 왜 위험한지 감이 안 옵니다. 직접 확인해 봤습니다. 기본 설정으로 ClickHouse 컨테이너를 띄웠습니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ docker run -d --name chtest -e CLICKHOUSE_PASSWORD=pw \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    clickhouse/clickhouse-server:latest</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ docker exec chtest clickhouse-client --password pw -q "select version()"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">26.7.5.10</span><br></div></code></pre></div></div>
<p><code>default</code> 사용자가 가진 권한을 그대로 뽑아 봤습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">show</span><span class="token plain"> grants </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">GRANT SOURCES ON *.* TO default</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GRANT TABLE ENGINE ON * TO default</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GRANT CHECK, SHOW, SELECT, INSERT, ALTER, CREATE, DROP, UNDROP TABLE,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      TRUNCATE, OPTIMIZE, BACKUP, KILL QUERY, KILL TRANSACTION,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      MOVE PARTITION BETWEEN SHARDS, SYSTEM, dictGet,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      displaySecretsInShowAndSelect, INTROSPECTION, CLUSTER,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      FILE, URL, REMOTE, MONGO, REDIS, MYSQL, POSTGRES, SQLITE,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ODBC, JDBC, HDFS, S3, HIVE, AZURE, KAFKA, NATS, RABBITMQ,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      YTSAURUS, ARROW FLIGHT, SOURCES ON *.* TO default</span><br></div></code></pre></div></div>
<p>목록을 보면 성격이 드러납니다. <code>FILE</code>, <code>URL</code>, <code>REMOTE</code>, <code>S3</code>, <code>MONGO</code>, <code>MYSQL</code>, <code>POSTGRES</code>, <code>HDFS</code>가 전부 들어 있습니다. SOURCES는 이들을 묶은 상위 권한입니다.</p>
<p>이게 뜻하는 바는 ClickHouse의 쿼리가 데이터베이스 안에서 끝나지 않는다는 것입니다. <code>url()</code> 테이블 함수는 SELECT 문 안에서 HTTP 요청을 보냅니다. <code>s3()</code>는 S3 버킷을 읽습니다. <code>file()</code>은 서버 로컬 파일을 테이블처럼 읽습니다. SELECT 권한만 있는 계정이 아니라, HTTP 클라이언트를 겸하는 계정이 되는 셈입니다.</p>
<p><code>file()</code>에는 별도 울타리가 하나 더 있습니다. 임의 경로는 막습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ clickhouse-client -q "select count() from file('/etc/hostname','LineAsString')"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Code: 291. DB::Exception: File `/etc/hostname` is not inside</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">`/var/lib/clickhouse/user_files`. (DATABASE_ACCESS_DENIED)</span><br></div></code></pre></div></div>
<p><code>user_files</code> 디렉터리 밖은 거부됩니다. 다만 그 안이라면 그대로 읽힙니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ docker exec chtest sh -c 'echo secret-token-abc &gt; /var/lib/clickhouse/user_files/t.txt'</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ clickhouse-client -q "select * from file('t.txt','LineAsString')"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">secret-token-abc</span><br></div></code></pre></div></div>
<p>네트워크 쪽은 울타리가 없습니다. 권한이 실제로 통과하는지 확인하려고 닫힌 포트를 겨냥해 봤습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ clickhouse-client -q "select * from url('http://127.0.0.1:1/x','LineAsString')"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Code: 1000. DB::Exception: Connection refused. (POCO_EXCEPTION)</span><br></div></code></pre></div></div>
<p>돌아온 것이 권한 거부가 아니라 연결 거부입니다. 권한 검사를 통과해 실제로 소켓을 열려고 시도했다는 뜻입니다. 겨냥한 주소가 열려 있었다면 요청이 나갔을 것입니다. SELECT 문 하나가 DB 서버에서 출발하는 HTTP 요청이 되는 지점입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="공격-체인">공격 체인<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EA%B3%B5%EA%B2%A9-%EC%B2%B4%EC%9D%B8" class="hash-link" aria-label="공격 체인에 대한 직접 링크" title="공격 체인에 대한 직접 링크" translate="no">​</a></h2>
<p>공지가 설명하는 흐름은 네 단계입니다.</p>
<p><code>url()</code> 함수로 AWS IMDSv1에 요청을 보냅니다. 인스턴스 메타데이터 서비스의 v1은 토큰 없이 GET 하나로 응답합니다. 여기서 EC2 인스턴스에 붙은 role의 임시 자격증명을 얻습니다.</p>
<p>그 자격증명으로 S3에 접근해 Terraform state 파일을 읽습니다. state 파일에는 인프라 구성이 그대로 들어 있고, 관리자 비밀번호 같은 값이 평문으로 남아 있는 경우가 흔합니다.</p>
<p>마지막으로 그 값으로 PMM 관리자로 인증합니다. 모니터링 시스템의 관리자가 되면 감시 대상 DB의 접속 정보를 그 안에서 찾을 수 있습니다.</p>
<p>단계마다 그 자체로는 알려진 문제입니다. IMDSv1이 토큰 없이 응답하는 것도, Terraform state에 비밀이 남는 것도 오래된 이야기입니다. 이 공지가 보여 주는 건 그 알려진 약점들이 SELECT 권한 하나로 연결된다는 점입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="수정과-완화">수정과 완화<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EC%88%98%EC%A0%95%EA%B3%BC-%EC%99%84%ED%99%94" class="hash-link" aria-label="수정과 완화에 대한 직접 링크" title="수정과 완화에 대한 직접 링크" translate="no">​</a></h2>
<p>수정 버전은 PMM 3.9.1이고 2026년 8월 19일 나왔습니다. 근본 수정은 이쪽입니다.</p>
<p>바로 올릴 수 없는 환경을 위해 공지가 완화 스크립트를 함께 제공합니다. 방향은 단순합니다. 데이터소스가 쓰는 계정을 읽기 전용으로 새로 만들고, SOURCES 계열을 주지 않는 것입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">USER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">IF</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">EXISTS</span><span class="token plain"> grafana_ro</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  IDENTIFIED </span><span class="token keyword" style="color:#00009f">WITH</span><span class="token plain"> plaintext_password </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'&lt;password&gt;'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  SETTINGS readonly </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GRANT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> pmm</span><span class="token punctuation" style="color:#393A34">.</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TO</span><span class="token plain"> grafana_ro</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>readonly = 1</code>을 걸고 <code>pmm</code> 데이터베이스에 SELECT만 줍니다. SOURCES를 주지 않으면 <code>url()</code>, <code>s3()</code>, <code>mongodb()</code>, <code>remote()</code>, <code>file()</code> 호출이 막힙니다. 대시보드가 필요한 것은 SELECT뿐이니 기능은 그대로 돕니다.</p>
<p>참고로 위 <code>CREATE USER</code>를 기본 <code>default</code> 계정으로 실행하면 거부될 수 있습니다. 제 컨테이너에서는 이렇게 나왔습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Code: 497. DB::Exception: default: Not enough privileges.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">To execute this query, it's necessary to have the grant</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">CREATE USER ON grafana_ro. (ACCESS_DENIED)</span><br></div></code></pre></div></div>
<p>SOURCES는 넉넉히 주면서 사용자 관리 권한은 안 주는 기본 구성입니다. 이 스크립트를 돌리려면 ACCESS MANAGEMENT 권한이 있는 계정으로 접속해야 합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="관측-스택-계정-권한을-점검하는-자리">관측 스택 계정 권한을 점검하는 자리<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EA%B4%80%EC%B8%A1-%EC%8A%A4%ED%83%9D-%EA%B3%84%EC%A0%95-%EA%B6%8C%ED%95%9C%EC%9D%84-%EC%A0%90%EA%B2%80%ED%95%98%EB%8A%94-%EC%9E%90%EB%A6%AC" class="hash-link" aria-label="관측 스택 계정 권한을 점검하는 자리에 대한 직접 링크" title="관측 스택 계정 권한을 점검하는 자리에 대한 직접 링크" translate="no">​</a></h2>
<p>공지가 다루지 않는 부분을 하나 짚고 싶습니다. 이번 문제의 뼈대는 PMM에 국한되지 않습니다. "대시보드가 DB에 붙을 때 어떤 권한으로 붙는가"라는 질문입니다. 이 질문은 어느 관측 스택에나 유효합니다.</p>
<p>점검할 지점을 정리하면 이렇습니다.</p>

































<table><thead><tr><th>확인할 것</th><th>왜</th></tr></thead><tbody><tr><td>데이터소스 계정의 권한 목록</td><td>SELECT 외에 무엇이 붙어 있는지. ClickHouse면 SOURCES 유무</td></tr><tr><td>Grafana anonymous access 설정</td><td>켜져 있으면 비인증 사용자가 데이터소스 API에 닿는다</td></tr><tr><td>Viewer 역할의 실제 범위</td><td>대시보드 열람과 raw query 실행이 같은 등급인지</td></tr><tr><td>DB 서버에서 나가는 네트워크</td><td>DB가 외부로 HTTP를 보낼 수 있는지</td></tr><tr><td>IMDS 버전</td><td>v1이 열려 있으면 SSRF 한 방이 자격증명이 된다</td></tr><tr><td>Terraform state 저장소</td><td>평문 비밀이 남아 있는지, 접근 주체가 누구인지</td></tr></tbody></table>
<p>DB 계정 권한부터 보는 게 순서지만, 네트워크 쪽에서도 한 겹 막을 수 있습니다. DB 서버가 외부로 HTTP를 보낼 이유가 대개 없습니다. IMDSv2를 강제하는 것도 이 체인 두 번째 단계를 끊습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="남는-생각">남는 생각<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EB%82%A8%EB%8A%94-%EC%83%9D%EA%B0%81" class="hash-link" aria-label="남는 생각에 대한 직접 링크" title="남는 생각에 대한 직접 링크" translate="no">​</a></h2>
<p><a class="" href="https://dbalog.dev/blog/copy-fail-cve-2026-31431">Copy Fail</a>이나 <a class="" href="https://dbalog.dev/blog/nginx-rift-cve-2026-42945">NGINX Rift</a> 같은 사례는 코드 한 줄의 결함이었습니다. 이번 건은 성격이 다릅니다. ClickHouse가 <code>url()</code>을 제공하는 것도, Grafana가 데이터소스 API를 여는 것도 각각은 의도된 기능입니다. 두 기능이 만나는 자리에서 권한 설정이 넉넉했던 것이 문제였습니다.</p>
<p>그래서 패치만으로 끝나지 않습니다. 3.9.1로 올린 뒤에도 데이터소스 계정에 무엇이 붙어 있는지 한 번 확인해 볼 만합니다. 저는 이 공지를 읽고 나서 ClickHouse의 SOURCES 권한이 그렇게 많은 것을 묶고 있는 줄 몰랐다는 걸 알았어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/pmm-clickhouse-grafana-advisory#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.percona.com/blog/security-advisory-privileged-clickhouse-access-through-the-grafana-data-source-in-pmm/" target="_blank" rel="noopener noreferrer" class="">Percona 보안 공지</a> (2026-08-19)</li>
<li class=""><a href="https://clickhouse.com/docs/en/sql-reference/statements/grant" target="_blank" rel="noopener noreferrer" class="">ClickHouse 접근 권한 문서</a></li>
<li class=""><a href="https://clickhouse.com/docs/en/sql-reference/table-functions/url" target="_blank" rel="noopener noreferrer" class="">ClickHouse url 테이블 함수</a></li>
</ul>]]></content>
        <category label="보안" term="보안"/>
        <category label="ClickHouse" term="ClickHouse"/>
        <category label="Grafana" term="Grafana"/>
        <category label="PMM" term="PMM"/>
        <category label="Percona" term="Percona"/>
        <category label="권한" term="권한"/>
        <category label="모니터링" term="모니터링"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Workers Spectre 재현]]></title>
        <id>https://dbalog.dev/blog/cloudflare-workers-spectre-revisit</id>
        <link href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit"/>
        <updated>2026-08-24T03:00:00.000Z</updated>
        <summary type="html"><![CDATA[Cloudflare가 자사 Workers 프로덕션 환경에서 원격 Spectre 공격을 재현한 연구를 공개했습니다. WebSocket으로 받은 타임스탬프만으로 초당 12비트, 정확도 99% 이상으로 다른 테넌트의 JWT를 읽어 냈습니다. 프로세스 대신 V8 isolate로 수만 테넌트를 나누는 구조가 어떤 대가를 치르는지, 2021년 방어가 왜 이 공격을 놓쳤는지, 그리고 새로 붙인 V8 샌드박스와 MPK가 무엇을 막는지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>Spectre는 2018년 이야기라고 생각하고 있었는데, 그게 아니었어요. 멀티테넌트 서버리스에서는 아직 진행 중인 문제입니다.</p>
<p>Cloudflare가 2026년 8월 19일 <a href="https://blog.cloudflare.com/revisiting-spectre-attacks-on-workers/" target="_blank" rel="noopener noreferrer" class="">자사 Workers 환경에서 원격 Spectre 공격을 재현한 연구</a>를 공개했습니다. 에든버러 대학교와 공동 연구입니다. 결과 문장이 짧고 셉니다. 프로덕션 환경에서 초당 최대 12비트, 정확도 99%로 안정적으로 유출했습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="무엇을-어떻게-빼냈나">무엇을 어떻게 빼냈나<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#%EB%AC%B4%EC%97%87%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B9%BC%EB%83%88%EB%82%98" class="hash-link" aria-label="무엇을 어떻게 빼냈나에 대한 직접 링크" title="무엇을 어떻게 빼냈나에 대한 직접 링크" translate="no">​</a></h2>
<p>피해자 Worker 안에 JWT 토큰을 두고 비트 단위로 읽어 냈습니다. 첫 바이트가 문자 <code>e</code>, 이진값 <code>0b01100101</code>로 정확히 나왔습니다.</p>
<p>초당 12비트는 느리게 들립니다. JWT 하나가 수백 바이트라면 몇 시간이 걸립니다. 하지만 공격이 성립하는지 여부에서는 속도가 본질이 아닙니다. 정확도 99%로 안정적이라는 쪽이 문제입니다. 오류율이 높으면 재시도로 시간이 곱해지는데, 그게 아니라는 뜻입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="원격-타이머라는-전제가-무너졌다">원격 타이머라는 전제가 무너졌다<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#%EC%9B%90%EA%B2%A9-%ED%83%80%EC%9D%B4%EB%A8%B8%EB%9D%BC%EB%8A%94-%EC%A0%84%EC%A0%9C%EA%B0%80-%EB%AC%B4%EB%84%88%EC%A1%8C%EB%8B%A4" class="hash-link" aria-label="원격 타이머라는 전제가 무너졌다에 대한 직접 링크" title="원격 타이머라는 전제가 무너졌다에 대한 직접 링크" translate="no">​</a></h2>
<p>Spectre 계열 공격에는 정밀한 시간 측정이 필요합니다. 캐시에 있는 값을 읽는 시간과 없는 값을 읽는 시간의 차이가 신호이기 때문입니다. 그 차이가 나노초 단위입니다.</p>
<p>2018년 이후 브라우저와 런타임이 취한 대응 중 하나가 타이머를 무디게 만드는 것이었습니다. Cloudflare Workers도 타이머를 동결(frozen timer)했습니다. Worker 안에서 시간을 재도 값이 변하지 않게 만든 것입니다. 멀티스레딩과 공유 메모리도 껐습니다.</p>
<p>이 연구가 그 전제를 치웁니다. 외부 서버로 나가는 WebSocket 연결 하나로 충분했습니다. 그쪽에서 고해상도 타임스탬프를 받아 오면 됩니다. 몇 번의 표본만으로 중앙값 기준 밀리초 미만 해상도를 얻었습니다.</p>
<p>여기에 증폭 기법을 붙였습니다. L1 캐시의 tree-based PLRU 교체 정책을 이용해 단일 캐시 사건에서 나온 시간 차이를 임의로 키웠습니다. 나노초 차이를 밀리초 해상도로 읽을 수 있게 만드는 단계입니다.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="isolate가-프로세스가-아니라서">isolate가 프로세스가 아니라서<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#isolate%EA%B0%80-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EA%B0%80-%EC%95%84%EB%8B%88%EB%9D%BC%EC%84%9C" class="hash-link" aria-label="isolate가 프로세스가 아니라서에 대한 직접 링크" title="isolate가 프로세스가 아니라서에 대한 직접 링크" translate="no">​</a></h2>
<p>Cloudflare Workers는 수만 테넌트를 같은 OS 프로세스 안에서 돌립니다. 분리 단위가 프로세스가 아니라 V8 isolate입니다. 이 선택이 효율을 만듭니다. 프로세스를 띄우는 비용 없이 요청이 들어오면 즉시 실행되고, cold start가 사실상 사라집니다.</p>
<p>대가는 분리 강도입니다. 같은 프로세스 안이면 주소 공간이 이어져 있습니다. Worker 프로세스 안에서 임의 읽기가 한 번 성립하면 다른 테넌트의 데이터에 닿습니다.</p>
<p>공격자가 피해자와 같은 프로세스에 들어가는 방법도 단순했습니다.</p>
<div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"https://victim.example"</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p>피해자 스크립트를 호출하면 스케줄러가 바로 그 프로세스 안에 피해자 Worker 인스턴스를 띄웁니다. Durable Objects와 지속 WebSocket 연결로 isolate를 계속 살려 뒀습니다.</p>
<p>gadget은 두 개를 썼습니다. 첫 번째는 압축된 힙 포인터를 유출합니다. isolate의 힙 기준 주소를 얻는 단계입니다. 두 번째는 TypedArray의 backing store를 이용한 speculative type confusion으로, 공격자가 만든 임의의 64비트 포인터에서 값을 읽습니다. 기준 주소를 알아낸 뒤 임의 주소로 넘어가는 구성입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2021년-방어는-왜-놓쳤나">2021년 방어는 왜 놓쳤나<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#2021%EB%85%84-%EB%B0%A9%EC%96%B4%EB%8A%94-%EC%99%9C-%EB%86%93%EC%B3%A4%EB%82%98" class="hash-link" aria-label="2021년 방어는 왜 놓쳤나에 대한 직접 링크" title="2021년 방어는 왜 놓쳤나에 대한 직접 링크" translate="no">​</a></h2>
<p>Cloudflare가 2021년에 내놓은 방어가 Dynamic Process Isolation(DyPrIs)입니다. 의심스러워 보이는 스크립트를 찾아 별도 프로세스로 격리합니다. 하드웨어 성능 카운터로 분기 예측 실패 같은 신호를 감시합니다.</p>
<p>이 방어가 통하지 않은 이유가 둘입니다.</p>
<p>첫째, 시점입니다. DyPrIs는 스크립트 실행이 끝난 뒤에 격리했습니다. 그런데 이 공격은 WebSocket keep-alive로 실행을 몇 시간에서 하루까지 이어 갑니다. 격리가 적용될 시점이 오지 않습니다.</p>
<p>둘째, 지표가 희석됐습니다. DyPrIs는 분기 예측 실패 횟수를 iTLB 접근 횟수로 정규화합니다. 그런데 WebSocket 트래픽 자체가 iTLB 활동을 늘립니다. 분모가 커지니 비율이 탐지 문턱 아래로 내려갑니다.</p>
<p>방어를 우회한 방식이 취약점을 새로 찾은 게 아니라, 정상 기능인 지속 연결을 쓴 것이라는 점이 인상적입니다. 탐지 지표를 정할 때 그 지표가 정상 트래픽으로 흔들릴 수 있는지가 중요하다는 사례입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="새로-붙인-방어">새로 붙인 방어<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#%EC%83%88%EB%A1%9C-%EB%B6%99%EC%9D%B8-%EB%B0%A9%EC%96%B4" class="hash-link" aria-label="새로 붙인 방어에 대한 직접 링크" title="새로 붙인 방어에 대한 직접 링크" translate="no">​</a></h2>
<p>이후 적용한 것이 셋입니다.</p>
<p>V8 샌드박스입니다. JavaScript 힙의 상당 부분에서 원시 64비트 포인터를 제거합니다. 이 연구가 쓴 speculative type confusion gadget은 임의의 64비트 포인터를 만들어 넣는 방식이었으니, 그 재료를 없애는 접근입니다. Cloudflare는 해당 gadget을 "재사용하기 더 어렵게" 만든다고 표현했습니다. 불가능하다고 하지 않았습니다.</p>
<p>Memory Protection Keys(MPK)입니다. 2025년 9월에 배포했습니다. isolate별 힙이 하드웨어로 강제되는 접근 경계 뒤에 놓입니다. 소프트웨어 검사가 아니라 CPU가 막습니다. 같은 프로세스 안이라는 구조를 유지하면서 분리를 강화하려면 이 방향이 남습니다.</p>
<p>DyPrIs 개선입니다. 장시간 실행과 I/O 위주 워크로드를 다루도록 바꿨고, 실행이 끝난 뒤가 아니라 실행 중에 탐지합니다. 위 두 회피 경로를 각각 겨냥한 수정입니다.</p>

























<table><thead><tr><th>회피 경로</th><th>대응</th></tr></thead><tbody><tr><td>실행 종료 후 격리</td><td>실행 중 탐지로 전환</td></tr><tr><td>iTLB 정규화 희석</td><td>장시간, I/O 위주 워크로드 처리</td></tr><tr><td>임의 64비트 포인터 조립</td><td>V8 샌드박스</td></tr><tr><td>같은 프로세스 내 힙 접근</td><td>MPK 하드웨어 경계</td></tr></tbody></table>
<p>실제 악용 흔적은 지난 3년간 발견되지 않았다고 밝혔습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="남는-질문">남는 질문<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#%EB%82%A8%EB%8A%94-%EC%A7%88%EB%AC%B8" class="hash-link" aria-label="남는 질문에 대한 직접 링크" title="남는 질문에 대한 직접 링크" translate="no">​</a></h2>
<p>이 글에서 가장 오래 남는 대목은 결과 수치가 아니라 구조 쪽입니다. isolate 기반 멀티테넌시는 cold start를 없애려는 선택이었고, 그 선택이 만든 공격면을 하드웨어 기능(MPK)으로 메우는 단계에 와 있습니다. 소프트웨어 격리를 얇게 만든 대가를 CPU 기능으로 지불하는 구조입니다.</p>
<p><a class="" href="https://dbalog.dev/blog/nginx-rift-cve-2026-42945">NGINX Rift</a>처럼 코드 결함을 고치는 사례와 다른 종류의 문제입니다. 고쳐야 할 한 줄이 없습니다. 아키텍처 선택의 청구서라서, 방어도 계층을 더 쌓는 형태가 됩니다.</p>
<p>DB를 운영하는 쪽에서 읽으면 결론이 좀 밋밋합니다. 서버리스 런타임 위에 올려 둔 코드가 다루는 비밀은 언제든 같은 프로세스 이웃에게 노출될 수 있다고 가정하는 편이 낫습니다. DB 접속 자격증명을 Worker 환경 변수에 두고 오래 살려 두는 구성이라면, 자격증명 수명을 짧게 잡는 것이 이 종류의 공격에 대한 가장 현실적인 대응입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/cloudflare-workers-spectre-revisit#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://blog.cloudflare.com/revisiting-spectre-attacks-on-workers/" target="_blank" rel="noopener noreferrer" class="">A revisit of remote Spectre attacks on Cloudflare Workers</a> (2026-08-19)</li>
<li class=""><a href="https://blog.cloudflare.com/spectre-research-with-tu-graz/" target="_blank" rel="noopener noreferrer" class="">Cloudflare의 2021년 Dynamic Process Isolation 발표</a></li>
<li class=""><a href="https://v8.dev/blog/sandbox" target="_blank" rel="noopener noreferrer" class="">V8 Sandbox 설계 문서</a></li>
</ul>]]></content>
        <category label="보안" term="보안"/>
        <category label="Spectre" term="Spectre"/>
        <category label="Cloudflare" term="Cloudflare"/>
        <category label="Workers" term="Workers"/>
        <category label="V8" term="V8"/>
        <category label="사이드채널" term="사이드채널"/>
        <category label="서버리스" term="서버리스"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PostgreSQL UUID v7 PK]]></title>
        <id>https://dbalog.dev/blog/postgresql-uuid-v7-primary-key</id>
        <link href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key"/>
        <updated>2026-08-24T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[UUID v4는 값이 완전히 무작위라 B-tree leaf page를 흩어 놓습니다. 상위 비트에 타임스탬프를 담는 UUID v7은 그 문제를 없앱니다. PostgreSQL 18에 내장된 uuidv7()로 100만 행을 넣어 bigserial, v4, v7 세 가지의 index 크기와 leaf 밀도를 직접 재 봤고, 원문 수치가 그대로 재현됐습니다. 대신 생성 시각이 공개된다는 대가가 붙습니다.]]></summary>
        <content type="html"><![CDATA[<p>primary key를 UUID로 쓰기로 하면 대개 v4를 씁니다. 그런데 그 선택이 index를 얼마나 부풀리는지 숫자로 본 적은 없었어요. pgEdge의 Shaun Thomas(숀 토머스)가 <a href="https://www.pgedge.com/blog/the-time-traveler-s-primary-key" target="_blank" rel="noopener noreferrer" class="">The Time Traveler's Primary Key</a>에서 그 숫자를 냈고, PostgreSQL 18 컨테이너로 직접 재현해 봤습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="v4가-index를-흩어-놓는-방식">v4가 index를 흩어 놓는 방식<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#v4%EA%B0%80-index%EB%A5%BC-%ED%9D%A9%EC%96%B4-%EB%86%93%EB%8A%94-%EB%B0%A9%EC%8B%9D" class="hash-link" aria-label="v4가 index를 흩어 놓는 방식에 대한 직접 링크" title="v4가 index를 흩어 놓는 방식에 대한 직접 링크" translate="no">​</a></h2>
<p>B-tree는 정렬된 구조입니다. 새 키가 들어오면 그 값이 들어갈 자리를 찾아 해당 leaf page에 씁니다.</p>
<p>시퀀스로 만든 키는 항상 오른쪽 끝으로 갑니다. 마지막 page를 채우고, 차면 새 page를 붙입니다. page 하나가 거의 꽉 찬 상태로 남습니다.</p>
<p>UUID v4는 값이 완전히 무작위입니다. 원문 표현대로 "생성된 각 값이 맨 첫 행보다 앞에 정렬될 확률과 맨 마지막 행보다 뒤에 정렬될 확률이 같습니다". 그래서 삽입이 index 전체에 흩어집니다. 이미 꽉 찬 page 가운데에 값이 들어오면 page split이 일어나고, split된 두 page는 각각 절반씩만 채워집니다. 같은 개수의 키를 담는 데 page가 두 배로 필요해집니다.</p>
<!-- -->
<p>UUID v7은 상위 48비트에 밀리초 단위 Unix 타임스탬프를 최상위 비트부터 담습니다. version 정보 뒤에 12비트를 더해 밀리초 이하 정밀도까지 표현합니다. 남는 약 62비트가 무작위입니다. 결과적으로 나중에 만든 값이 앞서 만든 값보다 크게 정렬됩니다. 시퀀스와 같은 삽입 패턴이 됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="직접-재-봤다">직접 재 봤다<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#%EC%A7%81%EC%A0%91-%EC%9E%AC-%EB%B4%A4%EB%8B%A4" class="hash-link" aria-label="직접 재 봤다에 대한 직접 링크" title="직접 재 봤다에 대한 직접 링크" translate="no">​</a></h2>
<p>PostgreSQL 18에 <code>uuidv7()</code>과 <code>uuidv4()</code>가 내장되어 있습니다. 기본 설정 컨테이너로 세 가지 테이블에 100만 행씩 넣었습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> t_bigint </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id bigserial </span><span class="token keyword" style="color:#00009f">primary</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> payload </span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> t_v4 </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id uuid </span><span class="token keyword" style="color:#00009f">primary</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> payload </span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> t_v7 </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id uuid </span><span class="token keyword" style="color:#00009f">primary</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> payload </span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">insert</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">into</span><span class="token plain"> t_bigint </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'x'</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">1000000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">insert</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">into</span><span class="token plain"> t_v4 </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> uuidv4</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token string" style="color:#e3116c">'x'</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">1000000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">insert</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">into</span><span class="token plain"> t_v7 </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> uuidv7</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token string" style="color:#e3116c">'x'</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">1000000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>크기를 뽑았습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"> relname  | heap  |  idx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">----------+-------+-------</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> t_bigint | 42 MB | 21 MB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> t_v4     | 50 MB | 37 MB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> t_v7     | 50 MB | 30 MB</span><br></div></code></pre></div></div>
<p>원문이 낸 수치가 v4 index 38MB, v7 30MB였는데 거의 그대로 나왔습니다. index만 놓고 보면 v7이 v4보다 19% 작습니다.</p>
<p>이유를 직접 확인하려면 <code>pgstattuple</code>의 <code>pgstatindex()</code>를 봅니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'v4'</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> k</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> avg_leaf_density</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> leaf_fragmentation </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pgstatindex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'t_v4_pkey'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">union</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">all</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'v7'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> avg_leaf_density</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> leaf_fragmentation </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pgstatindex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'t_v7_pkey'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">union</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">all</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'bigint'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> avg_leaf_density</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> leaf_fragmentation </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pgstatindex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'t_bigint_pkey'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">   k    | avg_leaf_density | leaf_fragmentation</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">--------+------------------+--------------------</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> v4     |            72.61 |              49.86</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> v7     |            89.98 |                  0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> bigint |            90.01 |                  0</span><br></div></code></pre></div></div>
<p>여기가 이 실험에서 가장 선명한 부분입니다. v4의 leaf 밀도가 72.61%이고 단편화가 49.86%입니다. v7은 밀도 89.98%에 단편화 0입니다. bigserial의 90.01%와 사실상 같습니다. v7의 삽입 패턴이 시퀀스와 구분되지 않는다는 뜻입니다.</p>
<p>원문 수치가 71.53%와 49.89%, 89.98%와 0.00%였으니 소수점 자리까지 맞았습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="heap은-줄지-않는다">heap은 줄지 않는다<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#heap%EC%9D%80-%EC%A4%84%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4" class="hash-link" aria-label="heap은 줄지 않는다에 대한 직접 링크" title="heap은 줄지 않는다에 대한 직접 링크" translate="no">​</a></h2>
<p>표에서 놓치기 쉬운 칸이 heap입니다. <code>t_bigint</code>가 42MB, UUID 두 개는 50MB로 같습니다. UUID는 128비트, bigint는 64비트니까 행마다 8바이트가 더 붙습니다. v7으로 바꿔도 이 차이는 그대로입니다.</p>
<p>정리하면 v7이 개선하는 것은 index의 공간 효율과 삽입 시 page split이고, 값 자체의 크기는 아닙니다. bigint와 UUID 사이의 선택은 여전히 별개 문제입니다. 원문도 이 부분을 분명히 합니다. 데이터가 단일 노드에 있고, 하나의 primary 인스턴스가 키를 발급하고, 키가 외부로 돌아다니지 않는다면 <code>BIGINT GENERATED ALWAYS AS IDENTITY</code>가 최선이라고 적었습니다. 위 실측에서도 bigint index가 21MB로 v7의 70% 수준입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="타임스탬프가-공개된다">타임스탬프가 공개된다<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#%ED%83%80%EC%9E%84%EC%8A%A4%ED%83%AC%ED%94%84%EA%B0%80-%EA%B3%B5%EA%B0%9C%EB%90%9C%EB%8B%A4" class="hash-link" aria-label="타임스탬프가 공개된다에 대한 직접 링크" title="타임스탬프가 공개된다에 대한 직접 링크" translate="no">​</a></h2>
<p>v7의 대가는 성능이 아니라 정보입니다. 상위 비트가 생성 시각이라서 값에서 그대로 읽힙니다. PostgreSQL 18은 그 추출 함수를 내장으로 제공합니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> uuid_extract_timestamp</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> t_v7 </span><span class="token keyword" style="color:#00009f">limit</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">01a03337-728f-7bf2-bdf8-69d062bfa627|2026-08-24 10:01:06.959+00</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">01a03337-7290-7767-bc60-69fb35dd6c00|2026-08-24 10:01:06.96+00</span><br></div></code></pre></div></div>
<p>버전도 확인됩니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> uuid_extract_version</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> t_v7 </span><span class="token keyword" style="color:#00009f">limit</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">-- 7</span><br></div></code></pre></div></div>
<p>이게 뜻하는 바가 셋입니다. 키를 받은 쪽이 그 레코드의 생성 시각을 밀리초 단위로 압니다. 키 두 개를 비교하면 그 사이 생성 간격을 알 수 있어서 생성 속도가 측정됩니다. 그리고 값이 시간순으로 인접하니 이웃 키를 추측해 보는 것이 무작위 값보다 훨씬 그럴듯해집니다.</p>
<p>주문 번호나 사용자 ID를 URL에 노출하는 구조라면 이 세 가지가 실제 문제가 됩니다. 가입 시각, 하루 주문 건수, 경쟁사 성장 속도가 키에서 읽힙니다. 원문도 생성 시각과 삽입 속도가 비공개여야 하는 경우에는 v4가 여전히 적합하다고 적었습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="오른쪽-끝이-경합-지점이-된다">오른쪽 끝이 경합 지점이 된다<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#%EC%98%A4%EB%A5%B8%EC%AA%BD-%EB%81%9D%EC%9D%B4-%EA%B2%BD%ED%95%A9-%EC%A7%80%EC%A0%90%EC%9D%B4-%EB%90%9C%EB%8B%A4" class="hash-link" aria-label="오른쪽 끝이 경합 지점이 된다에 대한 직접 링크" title="오른쪽 끝이 경합 지점이 된다에 대한 직접 링크" translate="no">​</a></h2>
<p>또 하나 짚어 둘 만한 대가가 있습니다. v7의 삽입이 index 오른쪽 끝으로 몰린다는 것은 곧 그 page가 모든 writer의 경합 지점이 된다는 뜻입니다.</p>
<p>샤딩된 환경이나 병렬 쓰기가 많은 구성에서는 이게 실제 병목이 될 수 있습니다. v4는 흩어져서 비효율적인 대신, 쓰기를 고르게 분산합니다. 단일 노드에서 순차 삽입이 주된 패턴이라면 v7이 유리하고, writer가 많고 오른쪽 끝 경합이 이미 보이는 환경이라면 계산이 달라집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어떻게-고를까">어떻게 고를까<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B3%A0%EB%A5%BC%EA%B9%8C" class="hash-link" aria-label="어떻게 고를까에 대한 직접 링크" title="어떻게 고를까에 대한 직접 링크" translate="no">​</a></h2>
<p>세 선택지의 성격을 정리하면 이렇습니다.</p>

































<table><thead><tr><th></th><th>index 효율</th><th>분산 생성</th><th>생성 시각 비공개</th><th>크기</th></tr></thead><tbody><tr><td>bigserial / identity</td><td>가장 좋음 (21MB)</td><td>어려움</td><td>유지</td><td>8바이트</td></tr><tr><td>UUID v4</td><td>나쁨 (37MB, 단편화 50%)</td><td>지원</td><td>유지</td><td>16바이트</td></tr><tr><td>UUID v7</td><td>좋음 (30MB, 단편화 0)</td><td>지원</td><td>노출</td><td>16바이트</td></tr></tbody></table>
<p>이미 UUID v4를 쓰고 있고 분산 생성이 필요해서 그렇게 한 것이라면, v7으로 옮기는 건 대체로 이득입니다. index가 작아지고 page split이 사라지는 대신 잃는 것은 생성 시각의 비공개성뿐입니다. 그 시각이 민감하지 않다면 바꿀 이유가 충분합니다.</p>
<p>반대로 UUID를 쓰는 이유가 "ID를 추측할 수 없게 하려고"였다면 v7은 그 목적과 충돌합니다. v7의 무작위 비트가 62비트라 값 자체를 맞히기는 여전히 어렵지만, 시간순 인접성 때문에 열거 시도의 탐색 공간이 좁아집니다.</p>
<p>새로 만드는 테이블이고 단일 노드라면 bigint가 답입니다. 위 실측에서 index가 v7의 70%, heap이 84%였고 그게 전부 실제 I/O입니다. UUID를 고르는 이유는 성능이 아니라 분산 생성이나 외부 노출이니까요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/postgresql-uuid-v7-primary-key#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.pgedge.com/blog/the-time-traveler-s-primary-key" target="_blank" rel="noopener noreferrer" class="">The Time Traveler's Primary Key</a> (Shaun Thomas, pgEdge, 2026-08-21)</li>
<li class=""><a href="https://www.postgresql.org/docs/18/functions-uuid.html" target="_blank" rel="noopener noreferrer" class="">PostgreSQL UUID 함수 문서</a></li>
<li class=""><a href="https://www.rfc-editor.org/rfc/rfc9562.html" target="_blank" rel="noopener noreferrer" class="">RFC 9562 (UUID v7 규격)</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="UUID" term="UUID"/>
        <category label="index" term="index"/>
        <category label="B-tree" term="B-tree"/>
        <category label="primary key" term="primary key"/>
        <category label="성능" term="성능"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PgBouncer와 Patroni 시간대]]></title>
        <id>https://dbalog.dev/blog/pgbouncer-patroni-timezone</id>
        <link href="https://dbalog.dev/blog/pgbouncer-patroni-timezone"/>
        <updated>2026-08-24T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[Oracle에서 Patroni 클러스터로 마이그레이션한 뒤 애플리케이션 로그에 올바른 타임스탬프와 UTC 타임스탬프가 섞여 나온 사례입니다. 원인은 두 겹이었습니다. ALTER SYSTEM으로 넣은 설정이 Patroni 재시작에 살아남지 못한 것과, PgBouncer가 startup parameter를 캐시한 것입니다. 클라이언트가 넘긴 startup parameter가 서버 설정을 어떻게 이기는지 직접 확인했습니다.]]></summary>
        <content type="html"><![CDATA[<p>로그의 타임스탬프 절반은 맞고 절반은 UTC라는 신고가 들어오면 어디부터 볼까요. Percona Community에 올라온 <a href="https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/" target="_blank" rel="noopener noreferrer" class="">사례</a>의 답이 흥미로웠습니다. 원인이 PostgreSQL 안이 아니었습니다.</p>
<p>배경은 Oracle에서 Patroni로 관리하는 PostgreSQL 18 클러스터로 마이그레이션한 직후입니다. 개발자들이 "타임스탬프가 어떤 건 맞고 어떤 건 UTC로 나온다"고 알렸습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="계층이-넷이었다">계층이 넷이었다<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#%EA%B3%84%EC%B8%B5%EC%9D%B4-%EB%84%B7%EC%9D%B4%EC%97%88%EB%8B%A4" class="hash-link" aria-label="계층이 넷이었다에 대한 직접 링크" title="계층이 넷이었다에 대한 직접 링크" translate="no">​</a></h2>
<p>문제를 이해하려면 timezone 값이 어디에 저장되는지 짚어야 합니다. 이 환경에서는 네 곳이었습니다.</p>



































<table><thead><tr><th>계층</th><th>처음</th><th>문제 상황</th><th>최종</th></tr></thead><tbody><tr><td>VM (OS)</td><td>UTC</td><td>UTC</td><td>UTC</td></tr><tr><td>PostgreSQL</td><td>UTC (기본값)</td><td>Africa/Lagos</td><td>Africa/Lagos</td></tr><tr><td>Patroni 설정</td><td>없음</td><td>재시작 후 UTC</td><td>Africa/Lagos</td></tr><tr><td>PgBouncer</td><td>UTC (캐시)</td><td>UTC (낡은 캐시)</td><td>갱신됨</td></tr></tbody></table>
<p>세 번째 행과 네 번째 행이 사건의 두 단계입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1단계-alter-system이-살아남지-못한다">1단계: ALTER SYSTEM이 살아남지 못한다<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#1%EB%8B%A8%EA%B3%84-alter-system%EC%9D%B4-%EC%82%B4%EC%95%84%EB%82%A8%EC%A7%80-%EB%AA%BB%ED%95%9C%EB%8B%A4" class="hash-link" aria-label="1단계: ALTER SYSTEM이 살아남지 못한다에 대한 직접 링크" title="1단계: ALTER SYSTEM이 살아남지 못한다에 대한 직접 링크" translate="no">​</a></h2>
<p>처음 시도한 방법이 이것이었습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> SYSTEM </span><span class="token keyword" style="color:#00009f">SET</span><span class="token plain"> TIMEZONE </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Africa/Lagos'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> pg_reload_conf</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>이 명령이 어디에 쓰이는지 컨테이너로 확인했습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ psql -Atc "show timezone"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Etc/UTC</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ psql -c "alter system set timezone = 'Asia/Seoul'"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ psql -Atc "select pg_reload_conf()"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">t</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ cat $PGDATA/postgresql.auto.conf</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Do not edit this file manually!</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># It will be overwritten by the ALTER SYSTEM command.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">timezone = 'Asia/Seoul'</span><br></div></code></pre></div></div>
<p>새 세션에서는 바로 반영됩니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ psql -Atc "show timezone; select now();"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Asia/Seoul</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">2026-08-24 19:02:52.265584+09</span><br></div></code></pre></div></div>
<p>여기까지는 정상입니다. 문제는 값이 <code>postgresql.auto.conf</code>에 있다는 것입니다. Patroni는 클러스터 설정을 분산 설정 저장소에서 관리하고, 노드를 재시작할 때 자기가 가진 설정으로 구성 파일을 다시 씁니다. <code>ALTER SYSTEM</code>이 남긴 값은 그 과정에서 사라집니다. Patroni 노드가 재시작되자 timezone이 UTC로 되돌아갔습니다.</p>
<p>Patroni 환경에서 올바른 방법은 Patroni 쪽에 넣는 것입니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">patronictl edit-config</span><br></div></code></pre></div></div>
<p>여기서 설정한 값은 노드 재시작에도 유지됩니다. Patroni를 쓰는 클러스터에서 <code>ALTER SYSTEM</code>을 쓰지 않는다는 원칙이 이 사례의 첫 교훈입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2단계-pgbouncer가-기억하고-있었다">2단계: PgBouncer가 기억하고 있었다<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#2%EB%8B%A8%EA%B3%84-pgbouncer%EA%B0%80-%EA%B8%B0%EC%96%B5%ED%95%98%EA%B3%A0-%EC%9E%88%EC%97%88%EB%8B%A4" class="hash-link" aria-label="2단계: PgBouncer가 기억하고 있었다에 대한 직접 링크" title="2단계: PgBouncer가 기억하고 있었다에 대한 직접 링크" translate="no">​</a></h2>
<p>Patroni 설정을 Africa/Lagos로 바로잡은 뒤에도 일부 세션이 UTC로 나왔습니다. 여기가 진짜 원인입니다.</p>
<p>PgBouncer는 세션 파라미터를 캐시합니다. 클라이언트가 연결할 때 넘기는 startup parameter를 기억해 두고, 풀에서 서버 연결을 꺼내 줄 때 그 값을 맞춰 줍니다. 그런데 Patroni 설정 변경으로 데이터베이스의 timezone이 바뀐 경우에는 이 내부 캐시를 제대로 무효화하지 않습니다. <code>ALTER DATABASE</code>로 바꿨을 때와 경로가 다릅니다.</p>
<p>그래서 낡은 UTC 기준으로 만들어진 세션들이 풀에서 계속 재활용됐습니다. 새로 만들어진 연결은 Africa/Lagos, 재활용된 연결은 UTC입니다. 로그에 두 값이 섞인 이유입니다.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="startup-parameter가-서버-설정을-이긴다">startup parameter가 서버 설정을 이긴다<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#startup-parameter%EA%B0%80-%EC%84%9C%EB%B2%84-%EC%84%A4%EC%A0%95%EC%9D%84-%EC%9D%B4%EA%B8%B4%EB%8B%A4" class="hash-link" aria-label="startup parameter가 서버 설정을 이긴다에 대한 직접 링크" title="startup parameter가 서버 설정을 이긴다에 대한 직접 링크" translate="no">​</a></h2>
<p>캐시가 왜 이렇게 강한지 궁금해서 직접 확인했습니다. 서버 설정은 Asia/Seoul인 상태에서, 클라이언트가 연결 시점에 timezone을 지정해 봤습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ psql -Atc "show timezone"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Asia/Seoul</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ PGOPTIONS="-c timezone=UTC" psql -Atc "show timezone"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">UTC</span><br></div></code></pre></div></div>
<p>같은 서버, 같은 순간인데 값이 다릅니다. 클라이언트가 연결할 때 넘긴 값이 서버의 설정을 덮어씁니다. 이게 정상 동작입니다. 세션 단위 설정이 서버 기본값보다 우선하니까요.</p>
<p>PgBouncer의 캐시가 문제가 되는 지점이 여기입니다. 서버 설정을 고쳐도 pooler가 예전 값을 세션에 계속 넣어 주면, 서버 쪽 변경은 그 세션에 닿지 않습니다. <code>show timezone</code>으로 확인해도 클라이언트가 보는 값은 pooler가 정한 값입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어떻게-고쳤나">어떻게 고쳤나<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B3%A0%EC%B3%A4%EB%82%98" class="hash-link" aria-label="어떻게 고쳤나에 대한 직접 링크" title="어떻게 고쳤나에 대한 직접 링크" translate="no">​</a></h2>
<p>PgBouncer 파드를 재시작해 캐시를 비웠습니다. 이후 새 연결은 모두 Africa/Lagos를 반영했습니다.</p>
<p>원문이 정리한 교훈 셋입니다. Patroni에서 timezone을 바꾼 뒤에는 PgBouncer를 재시작하거나 재연결시킵니다. <code>ALTER SYSTEM SET TIMEZONE</code>이 아니라 <code>patronictl edit-config</code>를 씁니다. 그리고 Kubernetes에서는 PgBouncer를 독립 파드가 아니라 sidecar로 배치합니다.</p>
<p>세 번째 항목에 이 사례의 구조적 원인이 들어 있습니다. PgBouncer가 독립 파드로 떠 있었기 때문에 Patroni 노드 재시작과 PgBouncer 재시작이 서로 무관했습니다. sidecar였다면 파드 재시작이 둘을 함께 갈아 줬을 것입니다. 연결 풀이 DB 인스턴스보다 오래 사는 배치에서는 이런 종류의 불일치가 반복됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="진단할-때-볼-지점">진단할 때 볼 지점<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#%EC%A7%84%EB%8B%A8%ED%95%A0-%EB%95%8C-%EB%B3%BC-%EC%A7%80%EC%A0%90" class="hash-link" aria-label="진단할 때 볼 지점에 대한 직접 링크" title="진단할 때 볼 지점에 대한 직접 링크" translate="no">​</a></h2>
<p>같은 증상을 만나면 확인 순서를 이렇게 잡을 만합니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- 지금 이 세션이 보는 값과 그 출처</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setting</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> source</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sourcefile</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pg_settings </span><span class="token keyword" style="color:#00009f">where</span><span class="token plain"> name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'TimeZone'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- 접속 중인 세션별로 실제 적용된 값</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> pid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> application_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> backend_start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> pg_stat_activity </span><span class="token keyword" style="color:#00009f">where</span><span class="token plain"> backend_type </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'client backend'</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>pg_settings</code>의 <code>source</code> 컬럼이 핵심입니다. 값이 <code>configuration file</code>이면 서버 설정에서, <code>client</code>면 클라이언트가 startup parameter로 넘긴 값입니다. <code>client</code>로 나오면서 값이 기대와 다르면 pooler를 봐야 합니다.</p>
<p>pooler 쪽에서는 <code>SHOW SERVERS</code>와 <code>SHOW POOLS</code>로 서버 연결이 언제 만들어졌는지 확인합니다. 설정을 바꾼 시각보다 오래된 연결이 남아 있으면 그게 문제의 세션입니다. <code>RECONNECT</code> 명령이나 파드 재시작으로 정리합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="남는-생각">남는 생각<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#%EB%82%A8%EB%8A%94-%EC%83%9D%EA%B0%81" class="hash-link" aria-label="남는 생각에 대한 직접 링크" title="남는 생각에 대한 직접 링크" translate="no">​</a></h2>
<p>이 사례가 좋은 이유는 원인이 PostgreSQL 밖에 있었다는 점입니다. <code>show timezone</code>을 아무리 확인해도, 그 값을 정한 주체가 pooler라면 서버 설정을 보는 것으로는 안 풀립니다.</p>
<p>Oracle에서 넘어온 직후라는 배경도 한몫했다고 봅니다. Oracle에는 연결 풀을 이런 식으로 앞에 두는 관례가 덜하고, 세션 파라미터가 계층별로 덮어써지는 구조에 익숙하지 않으면 의심 대상에 pooler가 안 들어옵니다. 저도 이 글을 읽기 전까지 PgBouncer가 startup parameter를 그렇게 오래 붙들고 있는 줄은 몰랐어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/pgbouncer-patroni-timezone#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/" target="_blank" rel="noopener noreferrer" class="">The curious case of timezone inconsistencies between PgBouncer and Patroni Cluster</a> (Percona Community, 2026-08-20)</li>
<li class=""><a href="https://patroni.readthedocs.io/en/latest/dynamic_configuration.html" target="_blank" rel="noopener noreferrer" class="">Patroni 동적 설정 문서</a></li>
<li class=""><a href="https://www.pgbouncer.org/config.html" target="_blank" rel="noopener noreferrer" class="">PgBouncer 설정 문서</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="PgBouncer" term="PgBouncer"/>
        <category label="Patroni" term="Patroni"/>
        <category label="timezone" term="timezone"/>
        <category label="트러블슈팅" term="트러블슈팅"/>
        <category label="Kubernetes" term="Kubernetes"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Aurora pgvector 이진 양자화]]></title>
        <id>https://dbalog.dev/blog/aurora-pgvector-binary-quantization</id>
        <link href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization"/>
        <updated>2026-08-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[AWS가 Aurora PostgreSQL에서 pgvector index를 이진 양자화로 압축해 1억 벡터 index를 367GB에서 38GB로 줄인 사례를 냈습니다. float32를 부호 1비트로 줄이는 방식인데, 벡터 자체는 32배 줄어도 index는 10배 정도만 줄어듭니다. 왜 그런지, 재순위화가 왜 필수인지, 그리고 pgvector 0.8.6으로 직접 재 본 수치를 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>벡터 검색에서 제일 자주 부딪히는 벽이 "index가 메모리에 안 들어간다"입니다. AWS가 그 벽을 이진 양자화로 넘은 <a href="https://aws.amazon.com/blogs/database/scale-pgvector-with-binary-quantization-on-amazon-aurora-postgresql/" target="_blank" rel="noopener noreferrer" class="">사례</a>를 냈습니다. 1억 벡터 index가 367GB에서 약 38GB가 됐습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="부호만-남긴다">부호만 남긴다<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EB%B6%80%ED%98%B8%EB%A7%8C-%EB%82%A8%EA%B8%B4%EB%8B%A4" class="hash-link" aria-label="부호만 남긴다에 대한 직접 링크" title="부호만 남긴다에 대한 직접 링크" translate="no">​</a></h2>
<p>이진 양자화의 방식은 이름 그대로입니다. 각 차원의 float32 값을 0을 기준으로 잘라 1비트로 만듭니다. 양수면 1, 음수면 0입니다.</p>
<p>768차원 벡터라면 원래 768 × 4바이트 = 3,072바이트입니다. 양자화하면 768비트, 즉 96바이트입니다. 32분의 1입니다.</p>
<p>정보를 크게 버리는 방식인데 벡터 검색에서 통하는 이유가 있습니다. 고차원 임베딩에서는 각 차원의 정확한 크기보다 부호 패턴이 방향을 상당히 결정합니다. 768개 차원의 부호가 얼마나 겹치는지만 봐도 두 벡터가 비슷한지 대략 알 수 있습니다. 이 비교가 Hamming 거리입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="직접-재-봤다">직접 재 봤다<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%A7%81%EC%A0%91-%EC%9E%AC-%EB%B4%A4%EB%8B%A4" class="hash-link" aria-label="직접 재 봤다에 대한 직접 링크" title="직접 재 봤다에 대한 직접 링크" translate="no">​</a></h2>
<p>pgvector 0.8.6이 들어간 PostgreSQL 18 컨테이너로 확인했습니다. 768차원 벡터 3만 개를 넣고 두 종류 index를 만들었습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">table</span><span class="token plain"> docs </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id bigserial </span><span class="token keyword" style="color:#00009f">primary</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> embedding vector</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">insert</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">into</span><span class="token plain"> docs </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">embedding</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">select</span><span class="token plain"> array_agg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">random</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::vector</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> generate_series</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">30000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>일반 HNSW와 이진 양자화 HNSW입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">index</span><span class="token plain"> idx_full </span><span class="token keyword" style="color:#00009f">on</span><span class="token plain"> docs</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">using</span><span class="token plain"> hnsw </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">embedding vector_cosine_ops</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">m</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">16</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ef_construction</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">64</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">create</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">index</span><span class="token plain"> idx_bq </span><span class="token keyword" style="color:#00009f">on</span><span class="token plain"> docs</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">using</span><span class="token plain"> hnsw </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">binary_quantize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">embedding</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">bit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> bit_hamming_ops</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">m</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">16</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ef_construction</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">64</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>bit_hamming_ops</code>가 bit 타입에 Hamming 거리를 적용하는 연산자 클래스입니다. 크기를 비교했습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"> indexrelname |  size</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">--------------+--------</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> docs_pkey    | 672 kB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> idx_bq       | 10 MB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> idx_full     | 104 MB</span><br></div></code></pre></div></div>
<p>10.4배 차이입니다. 벡터 자체는 32배 줄었는데 index는 10배 남짓입니다. 이 격차가 이 글에서 짚고 싶은 부분입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="32배와-10배-사이">32배와 10배 사이<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#32%EB%B0%B0%EC%99%80-10%EB%B0%B0-%EC%82%AC%EC%9D%B4" class="hash-link" aria-label="32배와 10배 사이에 대한 직접 링크" title="32배와 10배 사이에 대한 직접 링크" translate="no">​</a></h2>
<p>차이의 이유는 HNSW index에 든 것이 벡터만이 아니라는 데 있습니다. HNSW는 계층 그래프입니다. 각 노드가 이웃으로 향하는 링크를 들고 있고, 그 개수를 <code>m</code> 파라미터가 정합니다. 위 실험에서는 <code>m=16</code>이었습니다.</p>
<p>링크는 양자화 대상이 아닙니다. 벡터를 96바이트로 줄여도 링크는 그대로입니다. 그래서 압축률이 벡터 크기 비율보다 낮게 나옵니다.</p>
<p>원문 수치로 확인해 봐도 같은 방향입니다. 367GB에서 38GB는 약 9.7배입니다. AWS가 "32배 압축"이라고 표현한 것은 벡터 데이터 자체의 비율이고, index 전체는 10배 안쪽입니다. 용량 계획을 세울 때 32배로 잡으면 어긋납니다.</p>
<p>원문이 준 벡터당 크기도 이 관점에서 읽힙니다. 1536차원에서 벡터당 약 680바이트, 768차원에서 약 400바이트입니다. 768비트는 96바이트니까 나머지 300바이트 정도가 그래프 구조입니다.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="재순위화가-없으면-반쪽이다">재순위화가 없으면 반쪽이다<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%9E%AC%EC%88%9C%EC%9C%84%ED%99%94%EA%B0%80-%EC%97%86%EC%9C%BC%EB%A9%B4-%EB%B0%98%EC%AA%BD%EC%9D%B4%EB%8B%A4" class="hash-link" aria-label="재순위화가 없으면 반쪽이다에 대한 직접 링크" title="재순위화가 없으면 반쪽이다에 대한 직접 링크" translate="no">​</a></h2>
<p>이진 양자화만으로 검색을 끝내면 정확도가 떨어집니다. 부호만 봤으니 당연합니다. 그래서 두 단계로 나눕니다. 양자화된 index로 후보를 넉넉히 뽑고, 그 후보들만 원본 벡터로 다시 정렬합니다.</p>
<p>원문이 제시한 쿼리 모양입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'[query_vector]'</span><span class="token plain">::vector </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> exact_distance</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> your_table</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> binary_quantize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">embedding</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">bit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">~</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              binary_quantize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'[query_vector]'</span><span class="token plain">::vector</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">bit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> candidates</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> exact_distance</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>연산자 두 개가 각각 다른 일을 합니다. <code>&lt;~&gt;</code>가 bit 타입의 Hamming 거리로 후보 200개를 고릅니다. <code>&lt;=&gt;</code>가 원본 vector의 코사인 거리로 그 200개를 다시 정렬합니다. 최종 10개는 원본 정밀도로 판단된 결과입니다.</p>
<p>이 구조에서 후보 개수가 정확도와 지연의 조절 손잡이입니다. 200개를 뽑으면 100개보다 정확하고 느립니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="수치는-어디까지-좋아졌나">수치는 어디까지 좋아졌나<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%88%98%EC%B9%98%EB%8A%94-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%A2%8B%EC%95%84%EC%A1%8C%EB%82%98" class="hash-link" aria-label="수치는 어디까지 좋아졌나에 대한 직접 링크" title="수치는 어디까지 좋아졌나에 대한 직접 링크" translate="no">​</a></h2>
<p>원문이 낸 측정치 중 눈에 띄는 것들입니다.</p>
<p>OpenAI 임베딩 500만 개(1536차원)를 <code>r8g.large</code>에서 돌린 결과입니다. 이진 양자화 HNSW가 recall 0.951에 138 QPS, p99 지연 18.9ms였습니다. 원본 정밀도 HNSW는 78 QPS에 p99 1,356ms였습니다. 지연 차이가 70배 넘게 벌어집니다.</p>
<p>LAION 1억 개(768차원)를 <code>r8g.4xlarge</code>에서 cold cache로 돌린 쪽이 더 극적입니다. 이진 양자화가 recall 0.931에 13.5 QPS, 원본 정밀도가 3.4 QPS입니다. 원문은 원본 정밀도 index가 메모리에 들어가지 않았다고 적었습니다. 이 한 줄이 사실 이 기법의 존재 이유입니다. 압축 자체가 목적이 아니라, index를 buffer cache 안에 들여놓는 것이 목적입니다.</p>
<p>index 빌드 시간도 1.1시간 대 16.1시간으로 갈렸습니다.</p>
<p>빌드 설정은 이렇게 잡았습니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">maintenance_work_mem = 96GB</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">max_parallel_maintenance_workers = 48</span><br></div></code></pre></div></div>
<p>쿼리 쪽 설정입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SET</span><span class="token plain"> hnsw</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ef_search </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">800</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SET</span><span class="token plain"> hnsw</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">iterative_scan </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> relaxed_order</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SET</span><span class="token plain"> hnsw</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">max_scan_tuples </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1400</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>hnsw.iterative_scan</code>이 여기서 역할이 있습니다. <code>ef_search</code>의 후보 한계인 1,000개를 넘겨 재순위화하려면 이 설정이 필요합니다.</p>
<p>요구 사항은 Aurora PostgreSQL 16.8 이상 또는 17.x이고 pgvector 0.8.0 이상입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="제-실측의-한계">제 실측의 한계<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%A0%9C-%EC%8B%A4%EC%B8%A1%EC%9D%98-%ED%95%9C%EA%B3%84" class="hash-link" aria-label="제 실측의 한계에 대한 직접 링크" title="제 실측의 한계에 대한 직접 링크" translate="no">​</a></h2>
<p>위에서 크기는 재 봤지만 recall은 재지 않았습니다. 이유를 밝혀 두는 게 맞습니다.</p>
<p>제가 넣은 벡터는 <code>random()-0.5</code>로 만든 균등 난수입니다. 실제 임베딩과 분포가 다릅니다. 임베딩은 의미가 비슷한 것끼리 방향이 모이는 구조인데, 균등 난수는 모든 벡터가 서로 비슷하게 멀리 있습니다. 이런 데이터에서 recall을 재면 이진 양자화의 정보 손실이 실제와 다르게 나옵니다.</p>
<p>index 크기는 데이터 분포와 무관한 구조적 값이라 그대로 쓸 수 있고, 정확도는 실제 임베딩으로 재야 의미가 있습니다. 그 부분은 원문 수치를 인용하는 쪽이 정직합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="언제-쓸까">언제 쓸까<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%96%B8%EC%A0%9C-%EC%93%B8%EA%B9%8C" class="hash-link" aria-label="언제 쓸까에 대한 직접 링크" title="언제 쓸까에 대한 직접 링크" translate="no">​</a></h2>
<p>기준이 비교적 선명합니다. index가 메모리에 들어가느냐입니다.</p>
<p>들어가는 규모라면 이진 양자화의 이득이 작습니다. 원본 정밀도 HNSW가 이미 충분히 빠르고, 재순위화 단계가 붙지 않아 쿼리도 단순합니다.</p>
<p>들어가지 않는 규모라면 계산이 완전히 달라집니다. index를 디스크에서 읽는 순간 지연이 자리수 단위로 뛰기 때문에, recall을 0.95 근처로 유지하면서 index를 10분의 1로 줄이는 거래가 압도적으로 유리해집니다. 위 LAION 사례에서 QPS가 4배 차이 난 것이 그 지점입니다.</p>
<p><a class="" href="https://dbalog.dev/blog/alloydb-mcp-server">AlloyDB가 AI 에이전트에 문을 연 이야기</a>를 다룰 때도 느낀 건데, 클라우드 3사가 벡터 쪽에서 경쟁하는 방향이 새 엔진이 아니라 PostgreSQL 위에 얹는 최적화로 모이고 있어요. 이번 것도 pgvector의 기존 기능을 조합한 결과입니다. <code>binary_quantize</code>와 <code>bit_hamming_ops</code>는 pgvector 0.8.0부터 있던 것이고, AWS가 한 일은 그것을 1억 벡터 규모에서 검증하고 설정값을 찾은 것입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고">참고<a href="https://dbalog.dev/blog/aurora-pgvector-binary-quantization#%EC%B0%B8%EA%B3%A0" class="hash-link" aria-label="참고에 대한 직접 링크" title="참고에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://aws.amazon.com/blogs/database/scale-pgvector-with-binary-quantization-on-amazon-aurora-postgresql/" target="_blank" rel="noopener noreferrer" class="">Scale pgvector with binary quantization on Amazon Aurora PostgreSQL</a> (AWS Database Blog, 2026-08-18)</li>
<li class=""><a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer" class="">pgvector 저장소</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="pgvector" term="pgvector"/>
        <category label="Aurora" term="Aurora"/>
        <category label="AWS" term="AWS"/>
        <category label="벡터검색" term="벡터검색"/>
        <category label="HNSW" term="HNSW"/>
        <category label="index" term="index"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[cgroup v2 입문]]></title>
        <id>https://dbalog.dev/blog/linux-cgroup-v2</id>
        <link href="https://dbalog.dev/blog/linux-cgroup-v2"/>
        <updated>2026-08-17T06:00:00.000Z</updated>
        <summary type="html"><![CDATA[Docker의 --memory, Kubernetes의 resources.limits, systemd의 MemoryMax는 전부 같은 곳으로 내려갑니다. 커널의 cgroup입니다. 프로세스를 무리로 묶어 CPU, 메모리, I/O를 배급하는 이 장치가 무엇인지, v1의 혼돈이 v2의 단일 트리로 정리된 사연은 무엇인지 개념부터 잡고, 64MB 제한에 100MB를 밀어 넣는 실험으로 OOM kill까지 직접 확인합니다.]]></summary>
        <content type="html"><![CDATA[<p><code>docker run --memory=512m</code>을 실행할 때, Kubernetes 매니페스트에 <code>resources.limits.memory</code>를 적을 때, systemd 유닛에 <code>MemoryMax=</code>를 넣을 때. 이 셋이 결국 같은 커널 기능 하나로 내려간다는 걸 알고 계셨나요. 컨테이너 시대의 자원 통제는 전부 <strong>cgroup</strong> 위에 서 있어요.</p>
<p><a class="" href="https://dbalog.dev/blog/systemd-units-tour">systemd 유닛 11종을 돌아본 글</a>에서 cgroup을 여러 번 스쳐 지나갔는데, 정작 cgroup 자체를 정면으로 다룬 적이 없었습니다. 이번에 채웁니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cgroup은-무엇인가">cgroup은 무엇인가<a href="https://dbalog.dev/blog/linux-cgroup-v2#cgroup%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80" class="hash-link" aria-label="cgroup은 무엇인가에 대한 직접 링크" title="cgroup은 무엇인가에 대한 직접 링크" translate="no">​</a></h2>
<p>cgroup(control group)은 프로세스들을 <strong>무리(group)로 묶고, 그 무리 단위로 자원을 통제하고 계량하는</strong> 커널 기능입니다. 하는 일은 세 가지로 요약됩니다.</p>

























<table><thead><tr><th>기능</th><th>내용</th><th>예</th></tr></thead><tbody><tr><td>제한 (limit)</td><td>무리가 쓸 수 있는 자원의 상한</td><td>메모리 64MB까지, CPU 절반까지</td></tr><tr><td>계량 (accounting)</td><td>무리가 지금까지 쓴 자원 측정</td><td>누적 CPU 시간, 현재 메모리 사용량</td></tr><tr><td>통제 (control)</td><td>무리 전체를 한 번에 조작</td><td>일시 정지(freeze), 일괄 종료</td></tr></tbody></table>
<p>핵심은 대상이 프로세스 하나가 아니라 <strong>무리</strong>라는 점입니다. <code>ulimit</code> 같은 전통 도구는 프로세스 단위라서, 프로세스가 fork하면 통제를 벗어납니다. cgroup은 자식 프로세스가 부모의 cgroup을 물려받으므로 fork로 도망칠 수 없습니다. 컨테이너가 "안에서 뭘 몇 개 띄우든 전체 512MB"라는 약속을 지킬 수 있는 이유입니다.</p>
<p>구현 형태는 파일시스템입니다. <code>/sys/fs/cgroup</code> 아래 디렉터리 하나가 cgroup 하나이고, 디렉터리를 만들면 cgroup이 생기고, 그 안의 파일에 값을 쓰면 제한이 걸립니다. 시스템 콜 API가 아니라 <code>mkdir</code>와 <code>echo</code>로 조작하는 커널 기능이라는 점이 cgroup의 성격을 잘 보여줍니다.</p>
<p>계보를 짧게 짚으면, 2006년 Google 엔지니어들이 "process containers"라는 이름으로 시작해 2008년 커널 2.6.24에 cgroup이라는 이름으로 병합됐습니다. 이후 LXC와 Docker가 이 위에 컨테이너를 지었고, systemd는 서비스 관리의 뼈대로 삼았습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="v1의-혼돈-v2의-단일-트리">v1의 혼돈, v2의 단일 트리<a href="https://dbalog.dev/blog/linux-cgroup-v2#v1%EC%9D%98-%ED%98%BC%EB%8F%88-v2%EC%9D%98-%EB%8B%A8%EC%9D%BC-%ED%8A%B8%EB%A6%AC" class="hash-link" aria-label="v1의 혼돈, v2의 단일 트리에 대한 직접 링크" title="v1의 혼돈, v2의 단일 트리에 대한 직접 링크" translate="no">​</a></h2>
<p>cgroup을 검색하면 v1과 v2 이야기가 반드시 나옵니다. 이 구분을 모르면 검색 결과의 절반이 내 시스템과 안 맞는 이유를 알 수 없습니다.</p>
<p>v1의 설계는 유연함이 지나쳤습니다. 컨트롤러(memory, cpu, blkio 등)마다 <strong>각자 독립된 트리</strong>를 가질 수 있었습니다. 한 프로세스가 memory 트리에서는 A 그룹, cpu 트리에서는 B 그룹에 속하는 것이 가능했다는 뜻입니다. 표현력은 높지만 대가가 컸습니다. 트리끼리 조합이 어긋나면 동작을 예측하기 어렵고, 컨트롤러마다 인터페이스 관례가 제각각이었으며, 권한 위임 규칙도 정리되어 있지 않았습니다.</p>
<p>v2는 2016년 커널 4.5에서 이 혼돈을 정리했습니다. 원칙은 하나입니다. <strong>트리는 하나, 모든 컨트롤러는 그 트리 위에서 동작한다</strong>(unified hierarchy).</p>
<!-- -->
<p>인터페이스도 통일됐습니다. 어느 cgroup 디렉터리든 같은 규칙의 파일들이 있습니다.</p>
<ul>
<li class=""><code>cgroup.procs</code>: 이 무리에 속한 PID 목록. 여기에 PID를 쓰면 편입</li>
<li class=""><code>cgroup.controllers</code>: 이 지점에서 쓸 수 있는 컨트롤러</li>
<li class=""><code>memory.max</code>, <code>cpu.max</code>, <code>io.max</code>, <code>pids.max</code>: 컨트롤러별 상한</li>
<li class=""><code>memory.events</code>: OOM 발생 횟수 같은 사건 카운터</li>
</ul>
<p>v2에만 있는 물건도 생겼습니다. PSI(Pressure Stall Information)입니다. <code>memory.pressure</code>, <code>cpu.pressure</code>, <code>io.pressure</code> 파일이 "이 무리의 프로세스들이 자원을 못 받아 지연된 시간의 비율"을 보여줍니다. 사용량이 아니라 <strong>부족으로 인한 고통</strong>을 직접 재는 지표라서, 사용률 그래프보다 정직할 때가 많습니다.</p>
<p>전환은 배포판 기본값으로 이미 끝난 이야기입니다. Fedora 31(2019)을 시작으로 Ubuntu 21.10, Debian 11, RHEL 9가 v2를 기본으로 켰고, Docker는 20.10부터, Kubernetes는 1.25에서 v2 지원을 GA로 올렸습니다. 지금 새로 만나는 시스템은 거의 v2라고 봐도 됩니다. 내 시스템 확인은 한 줄입니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">stat -fc %T /sys/fs/cgroup/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># cgroup2fs 면 v2, tmpfs 면 v1</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="실험-64mb-국경에-100mb를-밀어-넣으면">실험: 64MB 국경에 100MB를 밀어 넣으면<a href="https://dbalog.dev/blog/linux-cgroup-v2#%EC%8B%A4%ED%97%98-64mb-%EA%B5%AD%EA%B2%BD%EC%97%90-100mb%EB%A5%BC-%EB%B0%80%EC%96%B4-%EB%84%A3%EC%9C%BC%EB%A9%B4" class="hash-link" aria-label="실험: 64MB 국경에 100MB를 밀어 넣으면에 대한 직접 링크" title="실험: 64MB 국경에 100MB를 밀어 넣으면에 대한 직접 링크" translate="no">​</a></h2>
<p>개념은 여기까지 하고, 직접 확인해 보겠습니다. macOS라서 Docker 컨테이너(Linux VM) 안에서 실험했습니다. 컨테이너 자체가 cgroup으로 만들어지니 오히려 좋은 교보재입니다.</p>
<p>먼저 <code>--memory=64m</code>이 정말 cgroup 파일로 내려가는지 확인합니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ docker run --rm --memory=64m alpine cat /sys/fs/cgroup/memory.max</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">67108864</span><br></div></code></pre></div></div>
<p>67108864 바이트, 정확히 64MiB입니다. Docker 플래그는 커널에 도달하면 <code>memory.max</code> 파일의 숫자 하나가 됩니다. 이제 이 국경 안에서 100MB를 할당해 봅니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ docker run --rm --memory=64m alpine sh -c 'head -c 100m /dev/zero | tail'</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Killed</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 종료 코드 137 (128 + SIGKILL 9)</span><br></div></code></pre></div></div>
<p><code>tail</code>은 입력을 메모리에 쌓는 성질이 있어 간단한 메모리 포식자로 쓸 수 있습니다. 64MiB 상한을 넘는 순간 커널 OOM killer가 그 cgroup <strong>안에서</strong> 희생자를 골라 SIGKILL을 보냅니다. <a class="" href="https://dbalog.dev/blog/unix-signals-in-modern-era">시그널 글</a>에서 다룬 137이라는 종료 코드가 여기서 나옵니다.</p>
<p>죽인 흔적은 사건 카운터에 남습니다. 같은 컨테이너에서 OOM을 겪은 직후 <code>memory.events</code>를 읽으면 이렇습니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">$ cat /sys/fs/cgroup/memory.events</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">low 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">high 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">max 72</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">oom 3</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">oom_kill 1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">oom_group_kill 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ cat /sys/fs/cgroup/memory.peak</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">67108864</span><br></div></code></pre></div></div>
<p>읽는 법이 흥미롭습니다. <code>max 72</code>는 사용량이 상한선에 부딪힌 횟수입니다. 부딪힐 때마다 커널은 먼저 page 회수(reclaim)로 버텨 봅니다. 그래도 안 되면 <code>oom</code>(3회)으로 넘어가고, 최종적으로 프로세스 하나를 죽였습니다(<code>oom_kill 1</code>). <code>memory.peak</code>가 정확히 67108864, 즉 상한과 같다는 것은 국경까지 꽉 채우고 넘지 못했다는 증거입니다. 상한 초과가 아니라 상한 도달 후 처형이라는 순서가 파일에 그대로 남습니다.</p>
<p>컨테이너 없이 맨 리눅스에서 같은 실험을 하려면 systemd가 지름길입니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># 임시 scope 유닛으로 감싸서 실행, 상한 64M</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">systemd-run --scope -p MemoryMax=64M some-command</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 서비스라면 유닛 파일에</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[Service]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">MemoryMax=64M</span><br></div></code></pre></div></div>
<p>systemd 유닛은 각자 cgroup 하나씩을 차지하므로, <code>MemoryMax=</code>는 결국 그 cgroup의 <code>memory.max</code>에 값을 쓰는 것입니다. 트리 전체는 <code>systemd-cgls</code>로, 무리별 실시간 사용량은 <code>systemd-cgtop</code>으로 봅니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="운영자가-기억할-세-가지">운영자가 기억할 세 가지<a href="https://dbalog.dev/blog/linux-cgroup-v2#%EC%9A%B4%EC%98%81%EC%9E%90%EA%B0%80-%EA%B8%B0%EC%96%B5%ED%95%A0-%EC%84%B8-%EA%B0%80%EC%A7%80" class="hash-link" aria-label="운영자가 기억할 세 가지에 대한 직접 링크" title="운영자가 기억할 세 가지에 대한 직접 링크" translate="no">​</a></h2>
<p>첫째, <strong>모든 상위 도구의 제한은 cgroup 파일로 환원됩니다.</strong> Docker 플래그든, Kubernetes limits든, systemd 지시자든, 디버깅의 종착지는 <code>/sys/fs/cgroup</code> 아래 파일입니다. 제한이 이상하게 동작하면 도구의 문서보다 그 파일의 실제 값을 먼저 확인하는 쪽이 빠릅니다.</p>
<p>둘째, <strong>cgroup의 메모리 계정에는 page cache가 포함됩니다.</strong> <code>memory.max</code>는 프로세스의 anonymous 메모리만 세는 것이 아니라, 그 무리가 일으킨 파일 캐시까지 셉니다. 파일을 많이 읽는 워크로드(백업, 대량 스캔, 그리고 데이터베이스)가 "실제 사용량은 얼마 안 되는데" OOM에 몰리는 전형적인 이유입니다. 캐시는 압박이 오면 회수되므로 보통은 문제가 없지만, 회수 속도가 할당 속도를 못 따라가는 순간이 있습니다. 컨테이너 위 PostgreSQL의 메모리 제한 설정은 이 지점이 본론이라, 각도를 잡아 후속 글에서 따로 다루겠습니다.</p>
<p>셋째, <strong>v2의 사건 카운터와 PSI를 감시 지표로 쓸 수 있습니다.</strong> <code>memory.events</code>의 <code>max</code>와 <code>oom_kill</code>, 그리고 <code>memory.pressure</code>는 "제한에 얼마나 자주 부딪히고 있는가"를 알려주는 조기 경보입니다. OOM으로 죽고 나서 아는 것과, <code>max</code> 카운터가 오르는 걸 보고 미리 아는 것의 차이입니다.</p>
<p>여담 하나로 마무리할게요. 이 글을 쓰게 된 계기는 Claude Code가 <a class="" href="https://dbalog.dev/blog/claude-code-fork-session-mentions">Bash 명령에 cgroup 메모리 제한을 거는 옵션</a>을 넣었다는 소식이었어요. AI 에이전트가 실행하는 명령의 폭주를 막는 안전장치로 2008년의 커널 기능이 다시 불려 나오는 것, cgroup이 여전히 현역 국경 수비대라는 좋은 증거 아닐까요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/linux-cgroup-v2#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://docs.kernel.org/admin-guide/cgroup-v2.html" target="_blank" rel="noopener noreferrer" class="">커널 문서: Control Group v2</a></li>
<li class=""><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html" target="_blank" rel="noopener noreferrer" class="">systemd.resource-control 매뉴얼</a></li>
<li class=""><a href="https://docs.kernel.org/accounting/psi.html" target="_blank" rel="noopener noreferrer" class="">PSI - Pressure Stall Information</a></li>
</ul>]]></content>
        <category label="Linux" term="Linux"/>
        <category label="cgroup" term="cgroup"/>
        <category label="커널" term="커널"/>
        <category label="Docker" term="Docker"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="systemd" term="systemd"/>
        <category label="OOM" term="OOM"/>
        <category label="운영" term="운영"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Code 토큰 절약]]></title>
        <id>https://dbalog.dev/blog/claude-code-maximizing-sessions</id>
        <link href="https://dbalog.dev/blog/claude-code-maximizing-sessions"/>
        <updated>2026-08-17T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[Anthropic이 Claude Code 세션의 토큰을 아끼는 공식 가이드를 냈습니다. 골자는 프롬프트 캐시를 깨지 않는 습관입니다. 캐시 읽기는 입력 가격의 0.1배, 캐시 쓰기는 최대 2배라서 같은 대화도 캐시가 살아 있느냐에 따라 비용이 크게 달라집니다. /model과 /effort를 대화 중간에 바꾸면 왜 손해인지, /clear, /compact, /rewind는 각각 언제 쓰는지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>같은 작업을 시켰는데 어떤 날은 사용량이 순식간에 닳고 어떤 날은 널널했던 경험, 있으실 거예요. 저도 한도에 일찍 닿은 날마다 모델 탓을 했는데, <a href="https://claude.com/blog/maximizing-the-value-of-your-claude-code-sessions" target="_blank" rel="noopener noreferrer" class="">Anthropic이 8월 14일에 낸 공식 가이드</a>를 읽고 나니 범인은 따로 있었습니다. <strong>프롬프트 캐시를 깨는 습관</strong>입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="요금의-실체는-캐시-히트율">요금의 실체는 캐시 히트율<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EC%9A%94%EA%B8%88%EC%9D%98-%EC%8B%A4%EC%B2%B4%EB%8A%94-%EC%BA%90%EC%8B%9C-%ED%9E%88%ED%8A%B8%EC%9C%A8" class="hash-link" aria-label="요금의 실체는 캐시 히트율에 대한 직접 링크" title="요금의 실체는 캐시 히트율에 대한 직접 링크" translate="no">​</a></h2>
<p>Claude Code는 매 턴마다 지금까지의 대화 전체를 모델에 보냅니다. 대화가 길어질수록 매 요청의 입력이 커지는 구조인데, 이게 감당되는 이유가 프롬프트 캐시입니다. 직전 요청과 같은 prefix는 캐시에서 읽고, 캐시 읽기 요금은 <strong>정상 입력 가격의 0.1배</strong>입니다. 반대로 캐시에 새로 쓰는 비용은 <strong>최대 2배</strong>입니다.</p>
<p>즉 20배짜리 요금 격차가 대화 내내 작동합니다. 캐시가 살아 있으면 긴 대화도 턴당 비용이 낮게 유지되고, 캐시가 깨지면 대화 전체를 정상 가격으로 다시 보내며 다시 캐시 쓰기 할증까지 뭅니다. Claude Code의 캐시 유효 시간은 1시간입니다(API 직접 호출 기본은 5분).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="캐시를-깨는-행동들">캐시를 깨는 행동들<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EC%BA%90%EC%8B%9C%EB%A5%BC-%EA%B9%A8%EB%8A%94-%ED%96%89%EB%8F%99%EB%93%A4" class="hash-link" aria-label="캐시를 깨는 행동들에 대한 직접 링크" title="캐시를 깨는 행동들에 대한 직접 링크" translate="no">​</a></h2>
<p>가이드가 지목하는 캐시 무효화 요인은 네 가지입니다.</p>

























<table><thead><tr><th>행동</th><th>왜 캐시가 깨지나</th></tr></thead><tbody><tr><td><code>/model</code> 변경</td><td>모델마다 캐시가 따로 있다</td></tr><tr><td><code>/effort</code> 조정</td><td>추론 강도가 캐시 키의 일부다</td></tr><tr><td>fast mode 전환</td><td>요청 키가 바뀐다</td></tr><tr><td>1시간 이상 방치</td><td>캐시 만료</td></tr></tbody></table>
<p>여기서 실무 결론이 나옵니다. <strong>모델과 effort는 세션 시작 시점이나 /clear 직후에 정하고, 대화 중간에는 건드리지 않는 것</strong>입니다. 긴 대화 한가운데서 <code>/model</code>을 바꾸면 그 시점까지의 대화 전체가 새 모델 기준으로 재처리되고 재캐싱됩니다. "이 질문만 가볍게 다른 모델로" 하려던 절약이 실제로는 가장 비싼 행동이 되는 역설입니다.</p>
<p>한 시간 자리를 비울 때도 마찬가지입니다. 돌아와서 이어 쓰면 만료된 캐시를 처음부터 다시 씁니다. 가이드는 장시간 중단 전에 <code>/compact</code>를 실행하라고 권합니다. 캐시가 아직 유효할 때 요약해 두는 쪽이 싸기 때문입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="문맥-관리-명령-셋의-역할-분담">문맥 관리 명령 셋의 역할 분담<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EB%AC%B8%EB%A7%A5-%EA%B4%80%EB%A6%AC-%EB%AA%85%EB%A0%B9-%EC%85%8B%EC%9D%98-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%8B%B4" class="hash-link" aria-label="문맥 관리 명령 셋의 역할 분담에 대한 직접 링크" title="문맥 관리 명령 셋의 역할 분담에 대한 직접 링크" translate="no">​</a></h2>
<p><code>/clear</code>, <code>/compact</code>, <code>/rewind</code>가 비슷해 보여도 캐시 관점에서 역할이 다릅니다.</p>
<ul>
<li class=""><code>/clear</code>: 새 작업을 시작할 때. 이전 작업의 문맥은 다음 작업에서 전부 "돈 내고 실어 나르는 짐"이 되므로, 작업 사이에는 비우고 시작합니다</li>
<li class=""><code>/compact</code>: 문맥은 이어가야 하는데 대화가 너무 길어졌거나, 장시간 자리를 비우기 직전에</li>
<li class=""><code>/rewind</code>: 마지막 몇 턴만 무르고 싶을 때. 캐시를 무효화하지 않는 것이 장점입니다</li>
</ul>
<p>저처럼 한 세션에서 이 일 저 일 이어 하던 사람에게는 <code>/clear</code>가 제일 아픈 항목입니다. "혹시 아까 맥락이 필요할지 몰라서" 남겨 둔 문맥이 매 턴 입력 토큰으로 청구되고 있었으니까요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="입력을-줄이는-잔기술">입력을 줄이는 잔기술<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EC%9E%85%EB%A0%A5%EC%9D%84-%EC%A4%84%EC%9D%B4%EB%8A%94-%EC%9E%94%EA%B8%B0%EC%88%A0" class="hash-link" aria-label="입력을 줄이는 잔기술에 대한 직접 링크" title="입력을 줄이는 잔기술에 대한 직접 링크" translate="no">​</a></h2>
<p>캐시 다음으로 효과가 큰 항목은 문맥에 들어오는 양 자체를 줄이는 것입니다.</p>
<p>파일은 <strong>@멘션</strong>으로 첨부합니다. <code>src/main.py 읽어 봐</code>라고 쓰면 모델이 Read 도구를 호출하는 왕복이 생기는데, <code>@src/main.py</code>로 멘션하면 파일이 첫 요청에 바로 첨부됩니다. 도구 호출 한 번과 그 결과 처리가 통째로 절약됩니다.</p>
<p>시끄러운 명령 출력은 서브에이전트로 격리합니다. 30,000자를 넘는 명령 출력은 자동으로 파일로 저장되고 미리보기만 문맥에 남습니다만, 그 미만의 장황한 출력(테스트 로그, 빌드 로그)은 고스란히 문맥을 차지합니다. 로그를 뒤져야 하는 작업은 독립 문맥에서 도는 서브에이전트에 맡기면 메인 세션이 가벼워집니다.</p>
<p>자주 쓰는 명령은 CLAUDE.md에 조용한 플래그와 함께 적어 둡니다. 예를 들어 테스트를 verbose로 돌리는 습관이 있다면, CLAUDE.md에 quiet 플래그가 붙은 명령을 기록해 모델이 처음부터 조용한 버전을 실행하게 하는 식입니다.</p>
<p>가이드가 제시한 우선순위는 이렇습니다. 불필요한 파일 읽기 방지, 긴 세션 분할, 모델/effort 고정, 명령 출력 최소화 순입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="이-가이드가-나온-맥락">이 가이드가 나온 맥락<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EC%9D%B4-%EA%B0%80%EC%9D%B4%EB%93%9C%EA%B0%80-%EB%82%98%EC%98%A8-%EB%A7%A5%EB%9D%BD" class="hash-link" aria-label="이 가이드가 나온 맥락에 대한 직접 링크" title="이 가이드가 나온 맥락에 대한 직접 링크" translate="no">​</a></h2>
<p>읽다 보면 이 가이드는 절약 팁 모음이라기보다 <strong>과금 구조 설명서</strong>에 가깝습니다. 에이전트형 코딩 도구의 비용은 "몇 마디 나눴나"가 아니라 "문맥 몇 토큰을 몇 번 실어 날랐나"로 결정되는데, 사용자 대부분은 전자로 체감합니다. 그 간극에서 "왜 벌써 한도냐"는 불만이 나오고, 이 문서는 거기에 대한 공식 답변으로 보입니다.</p>
<p>도구 쪽 기본값도 같은 방향으로 움직이고 있습니다. 최근 릴리스에서 <a class="" href="https://dbalog.dev/blog/claude-code-fork-session-mentions">서브에이전트 포크가 캐시를 상속하게 된 것</a>도, 포크할 때마다 문맥을 다시 캐싱하는 비용을 없애는 변경입니다. 캐시를 아끼는 방향으로 도구가 정렬되고 있으니, 사용자 습관만 따라가면 됩니다.</p>
<p>오늘부터 바꿀 것 하나만 고르라면 저는 "작업 끝나면 /clear"를 고르겠어요. 제일 쉽고, 제일 큽니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/claude-code-maximizing-sessions#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://claude.com/blog/maximizing-the-value-of-your-claude-code-sessions" target="_blank" rel="noopener noreferrer" class="">Maximizing the value of your Claude Code sessions</a> - Claude Blog, 2026-08-14</li>
<li class=""><a href="https://docs.claude.com/en/docs/build-with-claude/prompt-caching" target="_blank" rel="noopener noreferrer" class="">Claude API 프롬프트 캐싱 문서</a></li>
</ul>]]></content>
        <category label="Claude" term="Claude"/>
        <category label="Claude Code" term="Claude Code"/>
        <category label="AI" term="AI"/>
        <category label="프롬프트 캐시" term="프롬프트 캐시"/>
        <category label="비용" term="비용"/>
        <category label="생산성" term="생산성"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Code 세션 포크와 멘션]]></title>
        <id>https://dbalog.dev/blog/claude-code-fork-session-mentions</id>
        <link href="https://dbalog.dev/blog/claude-code-fork-session-mentions"/>
        <updated>2026-08-16T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[Claude Code 2.1.232에서 서브에이전트 포크가 기본 활성화됐습니다. 포크된 에이전트는 대화 전체와 프롬프트 캐시를 물려받고, 배경 실행이 기본이 됐습니다. 프롬프트에서 @를 입력하면 다른 세션을 이름으로 불러 직접 메시지를 보낼 수 있습니다. 2.1.233의 GitLab MR 지원, cgroup 메모리 제한과 함께 무엇이 달라지는지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>터미널 두 개에 Claude Code를 띄워 놓고 한쪽 결과를 복사해서 다른 쪽에 붙여넣던 시절이 있었어요. 이번 주 릴리스로 그 복붙이 공식적으로 필요 없어졌습니다.</p>
<p><a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreferrer" class="">2.1.232 changelog</a>의 첫 두 줄이 핵심입니다. <strong>서브에이전트 포크가 기본 활성화</strong>됐고, 프롬프트에서 @를 입력하면 다른 세션을 <strong>이름으로 직접</strong> 부를 수 있습니다. 따로 보면 각각 작은 기능인데, 합치면 "세션 하나 = 작업 하나"라는 전제가 무너집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="포크-문맥을-통째로-물려받는-서브에이전트">포크: 문맥을 통째로 물려받는 서브에이전트<a href="https://dbalog.dev/blog/claude-code-fork-session-mentions#%ED%8F%AC%ED%81%AC-%EB%AC%B8%EB%A7%A5%EC%9D%84-%ED%86%B5%EC%A7%B8%EB%A1%9C-%EB%AC%BC%EB%A0%A4%EB%B0%9B%EB%8A%94-%EC%84%9C%EB%B8%8C%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8" class="hash-link" aria-label="포크: 문맥을 통째로 물려받는 서브에이전트에 대한 직접 링크" title="포크: 문맥을 통째로 물려받는 서브에이전트에 대한 직접 링크" translate="no">​</a></h2>
<p>기존 서브에이전트의 약점은 기억상실이었습니다. 새 에이전트는 빈 문맥에서 출발하니, 지금까지 대화에서 쌓인 맥락을 프롬프트에 꾹꾹 눌러 담아 전달해야 했습니다. 전달이 부실하면 엉뚱한 결과가 돌아왔습니다.</p>
<p><code>subagent_type: "fork"</code>로 뜨는 포크 에이전트는 <strong>부모의 대화 전체와 프롬프트 캐시를 상속</strong>합니다. 지금까지 읽은 파일, 내린 결정, 사용자가 준 피드백을 전부 아는 상태로 출발합니다. 캐시까지 물려받으니 그 문맥을 다시 처리하는 비용도 들지 않습니다.</p>
<!-- -->
<p>함께 바뀐 기본값이 하나 더 있습니다. 대화형 세션에서 띄우는 에이전트는 이제 <strong>배경 실행이 기본</strong>입니다. 에이전트가 도는 동안 메인 세션이 멈춰 기다리지 않고, 끝나면 알림으로 결과가 돌아옵니다. 에이전트의 도구 호출 로그가 메인 문맥을 차지하지도 않습니다. 정리하면 "무거운 조사나 검증은 포크로 떼어 배경에서 돌리고, 메인은 계속 진행한다"가 이제 설정 없이 되는 기본 동작입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="멘션-세션끼리-직접-대화">@멘션: 세션끼리 직접 대화<a href="https://dbalog.dev/blog/claude-code-fork-session-mentions#%EB%A9%98%EC%85%98-%EC%84%B8%EC%85%98%EB%81%BC%EB%A6%AC-%EC%A7%81%EC%A0%91-%EB%8C%80%ED%99%94" class="hash-link" aria-label="@멘션: 세션끼리 직접 대화에 대한 직접 링크" title="@멘션: 세션끼리 직접 대화에 대한 직접 링크" translate="no">​</a></h2>
<p>두 번째 변화는 세션 간 통신입니다. 프롬프트에 <code>@</code>를 입력하면 같은 머신에서 돌고 있는 다른 Claude 세션 목록이 뜨고, 이름을 고르면 그 세션으로 메시지가 갑니다. 내부적으로는 SendMessage 도구를 사용합니다.</p>
<p>이걸 받쳐 주는 손질도 같이 들어갔습니다.</p>
<ul>
<li class="">이름이 정확히 하나의 살아 있는 세션과 일치하면 확인 절차 없이 바로 전달됩니다</li>
<li class="">한 머신의 대화형 세션들은 고유한 이름을 유지합니다. 이미 쓰는 이름으로 시작하면 자동으로 변형된 이름을 받습니다</li>
<li class=""><code>/config</code>에 다른 세션에서 오는 메시지를 받을지(수락/보류/거부) 정하는 항목이 생겼습니다</li>
</ul>
<p>블로그 작업으로 예를 들면 이렇게 됩니다. 포스트 A를 쓰는 세션과 포스트 B를 쓰는 세션을 나란히 띄워 두고, A 세션에서 <code>@세션B 방금 정한 용어 표기를 너도 맞춰 줘</code>라고 보내면 끝입니다. 사람이 두 터미널 사이에서 전서구 노릇을 할 필요가 없습니다. 지시는 각 세션에 직접 하되, 세션끼리 맞출 것은 세션끼리 맞추게 하는 구조입니다.</p>
<p><a class="" href="https://dbalog.dev/blog/claude-opus-4-8">Opus 4.8 리뷰</a>에서 "한 사람에게 맡기던 일을 한 팀에게 맡긴다"고 썼는데, 이번 릴리스는 그 팀에게 사내 메신저를 지급한 셈입니다. <a class="" href="https://dbalog.dev/blog/claude-code-dynamic-workflows">Dynamic Workflows</a>가 한 세션 안의 수직 분업이라면, @멘션은 세션 사이의 수평 분업입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="21233-worktree의-gitlab-mr-cgroup-메모리-제한">2.1.233: worktree의 GitLab MR, cgroup 메모리 제한<a href="https://dbalog.dev/blog/claude-code-fork-session-mentions#21233-worktree%EC%9D%98-gitlab-mr-cgroup-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A0%9C%ED%95%9C" class="hash-link" aria-label="2.1.233: worktree의 GitLab MR, cgroup 메모리 제한에 대한 직접 링크" title="2.1.233: worktree의 GitLab MR, cgroup 메모리 제한에 대한 직접 링크" translate="no">​</a></h2>
<p>다음 날 나온 2.1.233에서 운영 관점의 항목 둘이 눈에 띕니다.</p>
<p><strong>GitLab 지원 확대.</strong> <code>--worktree</code> 플래그와 <code>claude agents</code> 뷰가 GitLab merge request URL을 인식합니다(MR은 <code>!N</code>으로 표시). 2.1.232에서 이미 GitLab 토큰 계열(<code>glpat-</code> 등) 시크릿 마스킹과 GitLab 플러그인 마켓플레이스 지원이 들어왔으니, 이틀 사이에 GitLab 사용자 대접이 눈에 띄게 좋아졌습니다. GitHub 전용이라 망설이던 조직에는 신호가 될 만합니다.</p>
<p><strong>Bash 도구 메모리 제한.</strong> Linux에서 <code>CLAUDE_CODE_TOOL_MEMORY_LIMIT</code> 환경변수로 Bash 명령에 cgroup 메모리 제한을 걸 수 있습니다(opt-in). 에이전트가 실행한 빌드가 폭주해 메모리를 삼키면 세션까지 같이 죽던 문제의 대응입니다. CI 러너나 공용 개발 서버에서 Claude Code를 돌리는 환경이라면 켜 둘 가치가 있습니다.</p>
<p>하나 더, 방향을 보여주는 항목이 있습니다. Opus 4.8과 그 이후 모델에서 <strong>todo/task 추적 도구(TodoWrite, TaskCreate 등)가 기본 비활성화</strong>됐습니다. 되살리려면 <code>CLAUDE_CODE_ENABLE_TODO_TOOLS=1</code>을 설정해야 합니다. 신형 모델은 할 일 목록을 도구로 관리시키지 않아도 작업을 놓치지 않는다는 판단이 깔린 변경입니다. 화면에서 todo 목록이 사라졌다면 버그가 아니라 이 변경입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="써-보고-느끼는-것">써 보고 느끼는 것<a href="https://dbalog.dev/blog/claude-code-fork-session-mentions#%EC%8D%A8-%EB%B3%B4%EA%B3%A0-%EB%8A%90%EB%81%BC%EB%8A%94-%EA%B2%83" class="hash-link" aria-label="써 보고 느끼는 것에 대한 직접 링크" title="써 보고 느끼는 것에 대한 직접 링크" translate="no">​</a></h2>
<p>이 글을 쓰는 세션도 뉴스 수집을 포크 에이전트 여섯에 나눠 맡기고, 결과를 메시지로 돌려받는 흐름으로 작업했습니다. 수집 에이전트들이 배경에서 도는 동안 메인 세션은 기존 포스트 목록을 정리했고, 끝난 에이전트부터 결과가 도착했습니다. 복붙도, 파일 경유도 없었습니다.</p>
<p>체감 요령을 남기면, 포크에는 "지금까지의 문맥이 필요한 일"을, 일반 서브에이전트에는 "문맥이 오히려 편견이 되는 일"(백지 검증, 독립 리뷰)을 맡기는 구분이 유효했습니다. 포크가 기본이 됐다고 전부 포크로 보낼 일은 아니에요. 문맥 상속은 힘이면서 동시에 선입견이니까요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/claude-code-fork-session-mentions#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreferrer" class="">Claude Code CHANGELOG (2.1.232, 2.1.233)</a> - Anthropic, 2026-08-13~14</li>
<li class=""><a href="https://docs.claude.com/en/docs/claude-code/" target="_blank" rel="noopener noreferrer" class="">Claude Code 공식 문서</a></li>
</ul>]]></content>
        <category label="Claude" term="Claude"/>
        <category label="Claude Code" term="Claude Code"/>
        <category label="AI" term="AI"/>
        <category label="서브에이전트" term="서브에이전트"/>
        <category label="멀티세션" term="멀티세션"/>
        <category label="워크플로우" term="워크플로우"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[pg_walviz WAL 시각화]]></title>
        <id>https://dbalog.dev/blog/pg-walviz</id>
        <link href="https://dbalog.dev/blog/pg-walviz"/>
        <updated>2026-08-15T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bertrand Drouvot가 WAL 세그먼트 파일을 브라우저에서 시각화하는 pg_walviz를 공개했습니다. pg_waldump가 record를 텍스트로 풀어 준다면, pg_walviz는 record가 세그먼트 안에 물리적으로 어떻게 놓이는지, 페이지 경계에서 어떻게 쪼개지는지, padding이 어디 들어가는지를 보여줍니다. WAL 학습 도구로서의 쓰임새를 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>WAL을 공부할 때 제일 답답한 건 실체가 안 보인다는 점이었어요. record가 페이지 경계에서 쪼개진다, 세그먼트 첫 페이지에는 long header가 붙는다, 이런 문장을 문서로는 읽는데 실제 바이트가 어떻게 놓이는지는 상상에 맡겨야 했거든요.</p>
<p><a href="https://bdrouvot.github.io/2026/08/13/welcome-to-pg-walviz-postgresql-wal-segment-visualizer/" target="_blank" rel="noopener noreferrer" class="">Bertrand Drouvot(베르트랑 드루보)가 8월 13일 공개한 pg_walviz</a>가 정확히 그 지점을 채웁니다. WAL 세그먼트 파일 하나를 읽어 브라우저에서 시각화하는 읽기 전용 도구입니다. 현재 버전은 v0.1.0-beta.1이고 <a href="https://github.com/bdrouvot/pg_walviz" target="_blank" rel="noopener noreferrer" class="">저장소는 GitHub</a>에 있습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="무엇을-보여주나">무엇을 보여주나<a href="https://dbalog.dev/blog/pg-walviz#%EB%AC%B4%EC%97%87%EC%9D%84-%EB%B3%B4%EC%97%AC%EC%A3%BC%EB%82%98" class="hash-link" aria-label="무엇을 보여주나에 대한 직접 링크" title="무엇을 보여주나에 대한 직접 링크" translate="no">​</a></h2>
<p>화면은 서로 동기화되는 네 개의 뷰로 구성됩니다.</p>

























<table><thead><tr><th>뷰</th><th>내용</th></tr></thead><tbody><tr><td>세그먼트 개요</td><td>히트맵. resource manager별로 record가 세그먼트 어디에 몰려 있는지</td></tr><tr><td>record 조각 목록</td><td>선택한 페이지에 걸친 record들, 페이지 경계에서 쪼개진 조각 포함</td></tr><tr><td>record 검사기</td><td>선택한 record의 헤더, 물리 배치, full-page image 정보</td></tr><tr><td>물리 바이트</td><td>색으로 구분된 원본 바이트열</td></tr></tbody></table>
<p>히트맵에서 눈에 띄는 영역을 클릭하면 그 페이지의 record 목록이 뜨고, record를 고르면 헤더 필드와 실제 바이트가 같이 하이라이트됩니다. 페이지 번호, record 번호, 파일 오프셋, LSN을 직접 입력해서 이동할 수도 있습니다.</p>
<p>pg_waldump와 겹치는 도구가 아니냐는 생각이 들 수 있는데, 역할이 다릅니다. pg_waldump는 record의 <strong>논리적 내용</strong>을 사람이 읽을 텍스트로 풀어 줍니다. 어떤 rmgr가 어떤 연산을 기록했는지 보기에 좋습니다. pg_walviz는 record가 세그먼트 안에 <strong>물리적으로 어떻게 저장되는지</strong>를 보여줍니다. 페이지 헤더, record 조각, continuation record, 정렬 padding, 블록 참조가 바이트 위에 그대로 표시됩니다. 실제로 pg_walviz는 내부적으로 pg_waldump를 사용하므로 두 도구는 상하 관계에 가깝습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="실행-방법">실행 방법<a href="https://dbalog.dev/blog/pg-walviz#%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="실행 방법에 대한 직접 링크" title="실행 방법에 대한 직접 링크" translate="no">​</a></h2>
<p>PostgreSQL 서버도, 데이터 디렉터리도 필요 없습니다. 세그먼트 파일 하나와 그 파일을 만든 서버 버전에 맞는 pg_waldump 바이너리만 있으면 됩니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_walviz/bin/pg_walviz \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --pg-waldump /usr/pgsql-18/bin/pg_waldump \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  /archive/000000010000000000000042</span><br></div></code></pre></div></div>
<p>실행하면 로컬 웹서버가 뜨고 브라우저가 열립니다. 원격 서버에서 실행할 때는 브라우저 자동 실행을 끄고 포트를 지정합니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">~/pg_walviz/bin/pg_walviz --no-open --port 8765 \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --pg-waldump /usr/pgsql-18/bin/pg_waldump \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  /archive/000000010000000000000042</span><br></div></code></pre></div></div>
<p>주의사항이 둘 있습니다. 현재 쓰기가 진행 중인 세그먼트는 열지 말라는 것, 그리고 WAL에는 실데이터가 들어 있으므로 로컬 밖으로 노출하지 말라는 것입니다. 특히 두 번째는 archive에서 세그먼트를 복사해 분석용 장비에서 여는 습관과 묶어 기억해 둘 만합니다. WAL은 INSERT된 행의 내용을 그대로 담고 있어서, 세그먼트 파일 하나가 곧 데이터 유출 경로가 됩니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어디에-쓸-만한가">어디에 쓸 만한가<a href="https://dbalog.dev/blog/pg-walviz#%EC%96%B4%EB%94%94%EC%97%90-%EC%93%B8-%EB%A7%8C%ED%95%9C%EA%B0%80" class="hash-link" aria-label="어디에 쓸 만한가에 대한 직접 링크" title="어디에 쓸 만한가에 대한 직접 링크" translate="no">​</a></h2>
<p>첫째는 학습입니다. WAL record 헤더의 <code>xl_prev</code>가 무엇인지, full-page image가 왜 checkpoint 직후에 몰리는지 같은 주제는 글로 읽는 것보다 히트맵에서 직접 확인하는 쪽이 빠릅니다. checkpoint 직후 세그먼트를 열어 보면 FPI가 차지하는 공간이 시각적으로 드러나고, <code>full_page_writes</code>가 WAL 볼륨에 미치는 영향이 감으로 잡힙니다.</p>
<p>둘째는 장애 분석의 보조 도구입니다. 예전에 <a class="" href="https://dbalog.dev/blog/postgresql-incorrect-prev-link-standby">standby가 record with incorrect prev-link를 반복하며 멈춘 사건</a>을 분석할 때는 pg_waldump 출력과 오프셋 계산을 손으로 맞춰 가며 recycled 세그먼트의 잔재를 추론했습니다. 그때 이 도구가 있었다면 문제 지점의 바이트를 바로 눈으로 확인했을 겁니다. 세그먼트 경계의 long page header(40바이트)와 첫 record의 위치 같은 것들이 화면에 그대로 보이니까요.</p>
<p>PostgreSQL 내부를 3D 도시로 만든 <a class="" href="https://dbalog.dev/blog/pgsimcity-postgres-3d">PGSimCity</a>가 buffer와 프로세스를 보여주는 조감도였다면, pg_walviz는 WAL이라는 한 지점을 현미경으로 파는 도구입니다. 아직 beta라 큰 세그먼트에서 로딩이 느리고 단일 파일 검사가 권장되는 수준이지만, 방향이 좋습니다.</p>
<p>archive에 쌓여 있는 세그먼트 하나 골라서 열어 보세요. 문서 열 페이지보다 히트맵 한 화면이 WAL 구조를 빨리 가르쳐 줄 거예요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/pg-walviz#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://bdrouvot.github.io/2026/08/13/welcome-to-pg-walviz-postgresql-wal-segment-visualizer/" target="_blank" rel="noopener noreferrer" class="">Welcome to pg_walviz - PostgreSQL WAL segment visualizer</a> - Bertrand Drouvot, 2026-08-13</li>
<li class=""><a href="https://github.com/bdrouvot/pg_walviz" target="_blank" rel="noopener noreferrer" class="">pg_walviz GitHub 저장소</a></li>
<li class=""><a href="https://www.postgresql.org/docs/current/pgwaldump.html" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 문서: pg_waldump</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="WAL" term="WAL"/>
        <category label="pg_waldump" term="pg_waldump"/>
        <category label="시각화" term="시각화"/>
        <category label="내부구조" term="내부구조"/>
        <category label="도구" term="도구"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[PostgreSQL 18.6 보안]]></title>
        <id>https://dbalog.dev/blog/postgresql-18-6-security-release</id>
        <link href="https://dbalog.dev/blog/postgresql-18-6-security-release"/>
        <updated>2026-08-14T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[8월 13일 PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24와 19 Beta 3가 동시에 나왔습니다. 보안 취약점 28건과 110건 넘는 버그를 수정한 대형 릴리스입니다. CVSS 8점대가 12건이고, 그중 psql COPY 취약점은 덤프 파일을 읽어들이는 일상 작업이 공격 경로가 됩니다. 18.5가 결번이 된 사연과 업데이트 후 별도로 실행해야 할 확인 쿼리까지 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>이번 분기 마이너 릴리스는 조용히 지나갈 수 없는 규모예요. <a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/" target="_blank" rel="noopener noreferrer" class="">8월 13일 공지</a>로 PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24가 한꺼번에 나왔는데, <strong>보안 취약점 수정이 28건</strong>입니다. 통상 마이너 릴리스의 보안 수정이 손에 꼽는 수준인 걸 생각하면 이례적인 숫자입니다.</p>
<p>버전 번호부터 눈에 걸립니다. 18 계열의 직전 버전은 18.4인데 이번이 18.6입니다. 18.5는 릴리스 준비 중 회귀(regression)가 발견되어 배포 없이 결번 처리됐습니다. 18.4에서 바로 18.6으로 올라가면 되고, 중간에 놓친 버전은 없습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cvss-8점대만-12건">CVSS 8점대만 12건<a href="https://dbalog.dev/blog/postgresql-18-6-security-release#cvss-8%EC%A0%90%EB%8C%80%EB%A7%8C-12%EA%B1%B4" class="hash-link" aria-label="CVSS 8점대만 12건에 대한 직접 링크" title="CVSS 8점대만 12건에 대한 직접 링크" translate="no">​</a></h2>
<p>28건 전체 중 CVSS 8.0 이상이 12건입니다. 성격별로 묶으면 그림이 보입니다.</p>



















































































<table><thead><tr><th>계열</th><th>대표 CVE</th><th>CVSS</th><th>내용</th></tr></thead><tbody><tr><td>클라이언트 도구</td><td>CVE-2026-6464</td><td>8.1</td><td>psql COPY FROM STDIN, 데이터 줄을 psql 명령으로 처리</td></tr><tr><td>클라이언트 도구</td><td>CVE-2026-18408</td><td>8.8</td><td>psql \unrestrict, 조작된 덤프 원본에서 임의 코드 실행</td></tr><tr><td>클라이언트 도구</td><td>CVE-2026-19385</td><td>8.8</td><td>pg_dump heap buffer overflow</td></tr><tr><td>heap overflow</td><td>CVE-2026-14664</td><td>8.8</td><td>regexp 처리, 임의 코드 실행 가능</td></tr><tr><td>heap overflow</td><td>CVE-2026-14669</td><td>8.8</td><td>to_char()</td></tr><tr><td>heap overflow</td><td>CVE-2026-14676</td><td>8.8</td><td>pg_stat_statements</td></tr><tr><td>type confusion</td><td>CVE-2026-14671</td><td>8.8</td><td>refint plan cache</td></tr><tr><td>type confusion</td><td>CVE-2026-16238</td><td>8.8</td><td>pg_restore_attribute_stats()</td></tr><tr><td>type confusion</td><td>CVE-2026-16239</td><td>8.8</td><td>cursor CLOSE + DECLARE</td></tr><tr><td>type confusion</td><td>CVE-2026-14680</td><td>8.8</td><td>"internal" 인자 처리</td></tr><tr><td>기타</td><td>CVE-2026-14662</td><td>8.8</td><td>tsvector/tsquery integer wraparound</td></tr><tr><td>기타</td><td>CVE-2026-15742</td><td>8.8</td><td>fuzzystrmatch, 임의 주소 기록</td></tr></tbody></table>
<p>이 중 DBA가 특히 무겁게 볼 것은 psql 계열입니다. 나머지는 대체로 "DB에 로그인한 공격자가 권한을 넘어서는" 유형이라 접속 통제가 1차 방어선이 되지만, psql 취약점은 방향이 반대입니다. <strong>신뢰할 수 없는 덤프나 SQL 파일을 psql로 읽어들이는 쪽이 피해자</strong>가 됩니다.</p>
<p>CVE-2026-6464는 COPY FROM STDIN 처리 중 초기 실패가 발생하면 뒤따르는 데이터 줄을 psql 명령으로 해석하는 문제입니다. 누군가 건네준 덤프 파일을 복원하는 일상 작업이 곧 공격 경로입니다. CVE-2026-18408도 같은 결로, pg_dump 출력에 포함되는 <code>\unrestrict</code> 처리를 악용하면 덤프를 만든 쪽(superuser 권한으로 조작된 서버)이 복원하는 쪽 클라이언트에서 임의 코드를 실행할 수 있습니다. 외부에서 받은 덤프를 복원할 일이 있는 조직이라면 클라이언트 패키지 업데이트를 서버만큼 서둘러야 합니다.</p>
<p>낮은 점수 중에도 운영에 직접 닿는 항목이 있습니다. CVE-2026-14663은 pgcrypto에서 비활성화된 cipher로 암복호화를 요청하면 조용히 평문으로 처리하던 문제입니다. 점수는 6.5지만 "암호화됐다고 믿었는데 평문이었다"는 유형이라 감사 관점에서는 확인해 볼 가치가 있습니다. CVE-2026-14672는 SCRAM 인증에서 <code>scram_iterations</code> 응답 차이로 계정 존재 여부가 노출되는 문제입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="업데이트만으로-끝나지-않는-세-가지">업데이트만으로 끝나지 않는 세 가지<a href="https://dbalog.dev/blog/postgresql-18-6-security-release#%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%A7%8C%EC%9C%BC%EB%A1%9C-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%84%B8-%EA%B0%80%EC%A7%80" class="hash-link" aria-label="업데이트만으로 끝나지 않는 세 가지에 대한 직접 링크" title="업데이트만으로 끝나지 않는 세 가지에 대한 직접 링크" translate="no">​</a></h2>
<p>이번 릴리스의 함정은 바이너리 교체 후에도 남는 숙제입니다. 릴리스 노트가 세 가지 후속 조치를 명시합니다.</p>
<p>첫째, parallel GIN index build를 쓴 적이 있다면 <strong>reltuples 오염</strong>을 확인합니다. PostgreSQL 14, 15, 16, 18에서 parallel worker가 초기화되지 않은 row count를 보고해 <code>pg_class.reltuples</code>가 Infinity나 NaN이 되는 버그가 있었습니다. 이 상태가 되면 autovacuum과 autoanalyze가 해당 테이블을 건너뛰고, 자연적으로는 복구되지 않습니다. GIN 인덱스를 가진 테이블을 이 쿼리로 확인합니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DISTINCT</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">oid::regclass</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">reltuples</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> pg_class t</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> pg_index i </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">oid </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">indrelid</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> pg_class ic </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">indexrelid </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ic</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">oid</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">relhasindex </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> ic</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">relam </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2742</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- 2742 = gin</span><br></div></code></pre></div></div>
<p><code>reltuples</code>가 Infinity, NaN, 혹은 말이 안 되는 값이면 해당 테이블에 <code>ANALYZE</code>를 실행합니다. vacuum이 안 도는 테이블은 wraparound 위험까지 이어지니, GIN을 쓰는 클러스터라면 이 확인을 빼먹지 않는 게 좋습니다.</p>
<p>둘째, btree_gist 인덱스의 REINDEX입니다. float4/float8 컬럼에서 NaN 처리, bit/bit varying 컬럼에서 정렬이 잘못되던 문제가 고쳐졌습니다. 해당 타입 컬럼에 btree_gist 인덱스가 있다면 REINDEX가 필요합니다.</p>
<p>셋째, 큰 ltree 값의 인덱스도 REINDEX 대상입니다. 약 14,653개 이상의 label을 가진 ltree 값에서 비교 연산이 틀려 B-tree 인덱스가 손상될 수 있었습니다. 그 정도 깊이의 ltree를 쓰는 곳은 드물겠지만, 해당된다면 REINDEX 대상입니다.</p>
<p>이 밖에 버그 수정 중에는 standby가 구버전 마이너의 WAL을 재생하다 멈추는 deadlock 수정(14~16), DEFAULT partition이 pruning에서 잘못 제외되던 문제, <code>REINDEX CONCURRENTLY</code>와 deferred unique constraint 조합 오류 같은 굵직한 것들이 포함됐습니다. 전체 목록은 <a href="https://www.postgresql.org/docs/release/" target="_blank" rel="noopener noreferrer" class="">릴리스 노트</a>에 있습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="19-beta-3와-14-eol">19 Beta 3와 14 EOL<a href="https://dbalog.dev/blog/postgresql-18-6-security-release#19-beta-3%EC%99%80-14-eol" class="hash-link" aria-label="19 Beta 3와 14 EOL에 대한 직접 링크" title="19 Beta 3와 14 EOL에 대한 직접 링크" translate="no">​</a></h2>
<p>같은 날 PostgreSQL 19 Beta 3도 나왔습니다. 눈에 띄는 변화는 <strong>GROUP BY ALL 문법이 revert</strong> 된 것입니다. Beta 1에 들어왔던 편의 문법인데 최종 릴리스 전에 빠졌습니다. 그 외 FOR PORTION OF(temporal 문법) 수정 다수, logical replication의 sequence 동기화 race condition 수정 등이 반영됐습니다. 19 신기능을 검증 중이라면 Beta 3 기준으로 다시 확인하는 게 안전합니다.</p>
<p>그리고 PostgreSQL 14의 지원 종료가 <strong>2026년 11월 12일</strong>로 예고됐습니다. 이번 14.24를 포함해 앞으로 한 번의 릴리스만 남았습니다. 아직 14로 운영 중인 클러스터는 마이그레이션 일정을 지금 세워야 합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="지금-해야-할-일">지금 해야 할 일<a href="https://dbalog.dev/blog/postgresql-18-6-security-release#%EC%A7%80%EA%B8%88-%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%9D%BC" class="hash-link" aria-label="지금 해야 할 일에 대한 직접 링크" title="지금 해야 할 일에 대한 직접 링크" translate="no">​</a></h2>
<p>우선순위로 정리하면 이렇습니다.</p>
<ol>
<li class="">서버 바이너리 업데이트 (마이너 업데이트라 dump/reload 불필요, 재시작만)</li>
<li class=""><strong>클라이언트 패키지(psql, pg_dump)도 같이 업데이트</strong> - 이번 릴리스에서는 클라이언트가 서버만큼 급합니다</li>
<li class="">GIN 인덱스 테이블의 reltuples 확인, 이상 시 ANALYZE</li>
<li class="">btree_gist(float/bit 컬럼), 깊은 ltree 인덱스 REINDEX</li>
<li class="">PostgreSQL 14 사용 중이면 업그레이드 계획 수립</li>
</ol>
<p>28건이라는 숫자에 놀랐지만, 뒤집어 보면 fuzzing과 코드 감사가 코어에 촘촘히 들어가고 있다는 뜻이기도 합니다. 저는 클라이언트 도구가 공격면이 되는 흐름이 이번 릴리스의 진짜 교훈이라고 봐요. 서버는 잘 잠가 두는데 psql은 아무 파일이나 여는 습관, 이번 기회에 같이 고치면 좋겠습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/postgresql-18-6-security-release#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24 and 19 Beta 3 Released!</a> - PostgreSQL News, 2026-08-13</li>
<li class=""><a href="https://www.postgresql.org/support/security/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL Security Information</a></li>
<li class=""><a href="https://www.postgresql.org/support/versioning/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL Versioning Policy</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="보안" term="보안"/>
        <category label="CVE" term="CVE"/>
        <category label="릴리스" term="릴리스"/>
        <category label="psql" term="psql"/>
        <category label="운영" term="운영"/>
        <category label="GIN" term="GIN"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[subtransaction 64개 한계]]></title>
        <id>https://dbalog.dev/blog/postgresql-subtransactions-overflow</id>
        <link href="https://dbalog.dev/blog/postgresql-subtransactions-overflow"/>
        <updated>2026-08-12T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[한 트랜잭션이 subtransaction을 64개 넘게 만들면 그 트랜잭션만 느려지는 게 아니라 클러스터 전체가 느려집니다. PlanetScale이 공개한 실측에서는 7,200 TPS가 160 TPS까지 떨어졌습니다. PGPROC 캐시 64개 한계와 pg_subtrans SLRU 조회 경로, hot standby 신규 접속까지 막히는 이유, 그리고 지금 우리 클러스터에서 확인할 진단 쿼리를 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>SAVEPOINT는 안전장치처럼 생겼어요. 트랜잭션 중간에 표시를 남기고, 일부만 되돌릴 수 있게 해 주는 기능이니까요. 그런데 이 안전장치를 한 트랜잭션에서 64번 넘게 쓰는 순간, 그 트랜잭션이 아니라 <strong>클러스터 전체가 절벽에서 떨어집니다</strong>.</p>
<p><a href="https://planetscale.com/blog/the-dangers-of-postgres-subtransactions" target="_blank" rel="noopener noreferrer" class="">PlanetScale이 8월 11일에 공개한 분석</a>이 이 경로를 실측으로 보여줬습니다. 동일한 INSERT/SELECT/DELETE 워크로드가 평소 약 7,200 TPS(지연 약 2ms)로 돌다가, 어느 한 트랜잭션이 subtransaction 64개를 넘긴 순간 약 160 TPS(지연 약 90ms)로 떨어졌습니다. 45배 하락입니다. 문제의 트랜잭션이 끝나자 처리량은 원래대로 돌아왔습니다.</p>
<p>이 글은 해당 분석을 따라가면서, 왜 남의 트랜잭션 하나가 내 쿼리를 느리게 만드는지 경로를 짚고, 마지막에 원문에는 없는 우리 클러스터 점검용 진단 쿼리를 붙입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="subtransaction은-어디서-생기나">subtransaction은 어디서 생기나<a href="https://dbalog.dev/blog/postgresql-subtransactions-overflow#subtransaction%EC%9D%80-%EC%96%B4%EB%94%94%EC%84%9C-%EC%83%9D%EA%B8%B0%EB%82%98" class="hash-link" aria-label="subtransaction은 어디서 생기나에 대한 직접 링크" title="subtransaction은 어디서 생기나에 대한 직접 링크" translate="no">​</a></h2>
<p>subtransaction은 최상위 트랜잭션 안에 중첩된 트랜잭션입니다. 각 subtransaction은 자기만의 transaction ID(subxid)를 받습니다. 만드는 경로는 두 가지입니다.</p>
<p>명시적 경로는 SAVEPOINT입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">BEGIN</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SAVEPOINT</span><span class="token plain"> s1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">-- subtransaction 1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> orders </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SAVEPOINT</span><span class="token plain"> s2</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">-- subtransaction 2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">UPDATE</span><span class="token plain"> stock </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">COMMIT</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>암묵적 경로가 더 위험합니다. PL/pgSQL에서 EXCEPTION 절이 있는 블록은 진입할 때마다 subtransaction을 하나 만듭니다. "혹시 실패하면 이 블록만 되돌린다"를 구현하는 방법이 내부적으로 SAVEPOINT와 같기 때문입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- 이 루프는 반복마다 subtransaction을 하나씩 만든다</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FOR</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">IN</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.</span><span class="token number" style="color:#36acaa">.70</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">LOOP</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">BEGIN</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> audit_log </span><span class="token keyword" style="color:#00009f">VALUES</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  EXCEPTION </span><span class="token keyword" style="color:#00009f">WHEN</span><span class="token plain"> unique_violation </span><span class="token keyword" style="color:#00009f">THEN</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- 무시하고 계속</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">END</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">END</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">LOOP</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- 70개 생성, 64개 한계 초과</span><br></div></code></pre></div></div>
<p>ORM도 조용히 만듭니다. Django의 <code>transaction.atomic()</code>을 중첩하면 안쪽은 SAVEPOINT가 되고, JPA/Hibernate에서 <code>REQUIRES_NEW</code>가 아닌 중첩 트랜잭션 전파, Rails의 중첩 <code>transaction</code> 블록(<code>requires_new: true</code>)도 같습니다. 애플리케이션 코드에는 SAVEPOINT라는 글자가 한 번도 안 나오는데 DB에는 수십 개씩 쌓이는 상황이 흔합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="64개-한계와-pg_subtrans">64개 한계와 pg_subtrans<a href="https://dbalog.dev/blog/postgresql-subtransactions-overflow#64%EA%B0%9C-%ED%95%9C%EA%B3%84%EC%99%80-pg_subtrans" class="hash-link" aria-label="64개 한계와 pg_subtrans에 대한 직접 링크" title="64개 한계와 pg_subtrans에 대한 직접 링크" translate="no">​</a></h2>
<p>각 backend의 PGPROC 구조체에는 subtransaction ID를 담는 캐시가 있고, 크기가 <code>PGPROC_MAX_CACHED_SUBXIDS</code>, 즉 64개입니다. 이 안에 다 들어가는 동안에는 다른 세션이 MVCC visibility를 판단할 때 공유 메모리만 보면 됩니다. 빠릅니다.</p>
<p>65개째부터 이 캐시는 <strong>overflow 상태</strong>로 표시됩니다. 이제 다른 세션들은 어떤 subxid가 어느 최상위 트랜잭션에 속하는지 공유 메모리에서 알 수 없고, 디스크 기반 구조인 <code>pg_subtrans</code>를 조회해야 합니다. <code>pg_subtrans</code>는 SLRU(simple least-recently-used) 캐시를 통해 접근하는데, 이 조회는 lightweight lock을 잡고 필요하면 디스크 I/O까지 발생시킵니다.</p>
<!-- -->
<p>핵심은 비용을 치르는 쪽이 subtransaction을 만든 트랜잭션이 아니라는 점입니다. overflow가 켜져 있는 동안 <strong>다른 모든 세션</strong>이 자기 snapshot으로 튜플 가시성을 판단할 때마다(<code>XidInMVCCSnapshot</code>) <code>pg_subtrans</code> SLRU lock을 두고 경합합니다. PlanetScale 실측에서 병목으로 지목된 지점이 정확히 이 lock입니다.</p>
<p>overflow 상태는 문제의 트랜잭션이 끝날 때까지 유지됩니다. 그리고 얼마나 오래 가는지는 그 트랜잭션이 아니라, 클러스터에서 가장 오래 돌고 있는 트랜잭션의 수명에 좌우됩니다. long-running transaction이 있으면 그만큼 고통이 길어집니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hot-standby까지-번지는-이유">hot standby까지 번지는 이유<a href="https://dbalog.dev/blog/postgresql-subtransactions-overflow#hot-standby%EA%B9%8C%EC%A7%80-%EB%B2%88%EC%A7%80%EB%8A%94-%EC%9D%B4%EC%9C%A0" class="hash-link" aria-label="hot standby까지 번지는 이유에 대한 직접 링크" title="hot standby까지 번지는 이유에 대한 직접 링크" translate="no">​</a></h2>
<p>이 문제의 두 번째 얼굴이 더 고약합니다. overflow가 발생한 시점에 WAL로 기록되는 <code>RUNNING_XACTS</code> 레코드에 "subxid overflowed" 표시가 함께 남습니다. 활성 subtransaction 목록을 다 담지 못했다는 뜻입니다.</p>
<p>새로 배포한 standby가 이 오염된 <code>RUNNING_XACTS</code>를 읽으면, 일관된 snapshot을 만들 수 없다고 판단하고 hot standby 활성화를 보류합니다. WAL은 계속 재생하는데 읽기 쿼리는 받지 않는 상태, 접속하면 이런 메시지만 돌아옵니다.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">FATAL:  the database system is not yet accepting connections</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">DETAIL:  Recovery snapshot is not yet ready for hot standby.</span><br></div></code></pre></div></div>
<p><code>pg_subtrans</code>를 primary에서 넘겨받으면 되지 않느냐는 생각이 들지만, <code>pg_subtrans</code> 변경은 WAL에 기록되지 않고, standby는 기동 시 활성 페이지를 0으로 초기화하기 때문에 로컬 데이터도 신뢰할 수 없습니다. standby가 이 상태를 벗어나는 조건은 overflow 되지 않은 새 <code>RUNNING_XACTS</code> 레코드가 도착하거나, primary의 shutdown checkpoint를 재생하거나, 누락 가능성이 있는 트랜잭션들이 전부 끝나는 것입니다.</p>
<p>운영 관점에서 시나리오를 조합하면 이렇게 됩니다. 부하가 튀어서 읽기 용량을 늘리려고 replica를 새로 붙였는데, 하필 그 부하를 만들던 워크로드가 subtransaction overflow를 일으키는 중이라면, 새 replica는 WAL만 재생하며 접속을 거부합니다. 용량을 더하려고 만든 노드가 용량이 되어 주지 않는 역설입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="지금-확인할-것들">지금 확인할 것들<a href="https://dbalog.dev/blog/postgresql-subtransactions-overflow#%EC%A7%80%EA%B8%88-%ED%99%95%EC%9D%B8%ED%95%A0-%EA%B2%83%EB%93%A4" class="hash-link" aria-label="지금 확인할 것들에 대한 직접 링크" title="지금 확인할 것들에 대한 직접 링크" translate="no">​</a></h2>
<p>여기부터는 원문 내용에 우리가 운영에서 바로 실행할 쿼리를 더해 정리합니다.</p>
<p>먼저 지금 이 순간 subtransaction을 쌓고 있는 backend가 있는지 확인합니다. PostgreSQL 14부터 <code>pg_stat_get_backend_subxact()</code>로 backend별 subxact 개수와 overflow 여부를 볼 수 있습니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> pg_stat_get_backend_pid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> pid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">subxact_count</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">subxact_overflowed</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> pg_stat_get_backend_idset</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> LATERAL pg_stat_get_backend_subxact</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> s </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">subxact_count </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">subxact_count </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>subxact_overflowed = true</code>인 행이 하나라도 보이면 그 순간 클러스터 전체가 <code>pg_subtrans</code> 조회 경로로 동작 중이라는 뜻입니다. <code>pg_stat_activity</code>와 조인하면 어떤 애플리케이션, 어떤 쿼리인지까지 특정됩니다.</p>
<p>과거에 겪었는지 의심된다면 SLRU 통계를 봅니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> blks_hit</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> blks_read</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> blks_written</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> stats_reset</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> pg_stat_slru</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'subtransaction'</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>평상시 이 카운터는 거의 움직이지 않습니다. <code>blks_hit</code>가 급증한 구간이 있었다면 overflow가 있었다는 강한 정황입니다. 그래프 도구에 이 두 컬럼을 올려 두면 사후 추적이 쉬워집니다.</p>
<p>가장 오래된 트랜잭션 나이도 함께 감시합니다. overflow의 지속 시간을 결정하는 값입니다.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">max</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> xact_start</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> oldest_tx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> pg_stat_activity</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> xact_start </span><span class="token operator" style="color:#393A34">IS</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>예방책은 소박합니다.</p>

























<table><thead><tr><th>대상</th><th>조치</th></tr></thead><tbody><tr><td>PL/pgSQL 루프 안 EXCEPTION 블록</td><td>루프 밖으로 빼거나, 예외를 안 쓰는 로직(ON CONFLICT 등)으로 전환</td></tr><tr><td>ORM 중첩 트랜잭션</td><td>중첩 <code>atomic()</code>/<code>transaction</code> 블록이 정말 필요한지 검토</td></tr><tr><td>long-running transaction</td><td><code>transaction_timeout</code>(17+), <code>idle_in_transaction_session_timeout</code> 설정</td></tr><tr><td>상시 감시</td><td><code>pg_stat_get_backend_subxact()</code> + <code>pg_stat_slru</code> 지표화</td></tr></tbody></table>
<p>코어 쪽 근본 해법으로는 <code>PGPROC_MAX_CACHED_SUBXIDS</code>를 늘려 재컴파일하는 단기 우회(공유 메모리 비용 증가)와, snapshot을 CSN(Commit Sequence Number) 기반으로 바꾸는 장기 방향이 거론됩니다. CSN 논의는 10년 넘게 이어지고 있지만 아직 본가에 병합되지 않았습니다.</p>
<p>64라는 숫자를 기억해 두면 좋겠어요. SAVEPOINT와 EXCEPTION 블록은 공짜가 아니고, 비용 청구서는 옆 세션으로 날아갑니다. 저는 이번 주에 저희 클러스터에도 위 진단 쿼리 두 개를 모니터링에 추가해 두려고 합니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/postgresql-subtransactions-overflow#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://planetscale.com/blog/the-dangers-of-postgres-subtransactions" target="_blank" rel="noopener noreferrer" class="">The Dangers of Postgres Subtransactions</a> - PlanetScale, 2026-08-11</li>
<li class=""><a href="https://www.postgresql.org/docs/current/sql-savepoint.html" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 문서: SAVEPOINT</a></li>
<li class=""><a href="https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-SLRU-VIEW" target="_blank" rel="noopener noreferrer" class="">PostgreSQL 문서: pg_stat_slru</a></li>
</ul>]]></content>
        <category label="PostgreSQL" term="PostgreSQL"/>
        <category label="subtransaction" term="subtransaction"/>
        <category label="SAVEPOINT" term="SAVEPOINT"/>
        <category label="SLRU" term="SLRU"/>
        <category label="hot standby" term="hot standby"/>
        <category label="운영" term="운영"/>
        <category label="성능" term="성능"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cloudflare Agents Week]]></title>
        <id>https://dbalog.dev/blog/cloudflare-agents-week</id>
        <link href="https://dbalog.dev/blog/cloudflare-agents-week"/>
        <updated>2026-08-09T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[Cloudflare가 8월 첫 주 닷새 동안 에이전트 인프라 발표를 몰아서 내놨습니다. 사내 시스템에 에이전트를 안전하게 붙이는 오픈소스 플랫폼 Cloudflare OS, 에이전트에게 한도가 걸린 지갑을 위임하는 Wallets, WebMCP 프리뷰와 MCPv2까지. 발표들을 관통하는 하나의 그림, 에이전트를 인터넷의 1급 시민으로 만드는 인프라 계층을 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>Cloudflare는 매년 몇 차례 "week"라는 이름으로 발표를 몰아서 내놓는데, 8월 3일부터 7일까지는 처음으로 <strong>에이전트</strong>가 주인공이었어요. <a href="https://blog.cloudflare.com/agents-week-review-august-2026/" target="_blank" rel="noopener noreferrer" class="">주간 회고 글</a> 기준으로 발표를 훑으면, 개별 제품보다 그것들이 가리키는 방향이 더 흥미롭습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cloudflare-os-에이전트를-사내-시스템에-붙이는-안전한-방법">Cloudflare OS: 에이전트를 사내 시스템에 붙이는 안전한 방법<a href="https://dbalog.dev/blog/cloudflare-agents-week#cloudflare-os-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EB%A5%BC-%EC%82%AC%EB%82%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90-%EB%B6%99%EC%9D%B4%EB%8A%94-%EC%95%88%EC%A0%84%ED%95%9C-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="Cloudflare OS: 에이전트를 사내 시스템에 붙이는 안전한 방법에 대한 직접 링크" title="Cloudflare OS: 에이전트를 사내 시스템에 붙이는 안전한 방법에 대한 직접 링크" translate="no">​</a></h2>
<p>가장 큰 화제는 <a href="https://blog.cloudflare.com/cloudflare-os/" target="_blank" rel="noopener noreferrer" class="">Cloudflare OS</a>였습니다. 이름과 달리 커널이 있는 운영체제가 아니라, <strong>사내 시스템에 에이전트를 연결하는 워크스페이스 플랫폼</strong>입니다. Apache 2.0 라이선스로 <a href="https://github.com/cloudflare/cloudflare-os" target="_blank" rel="noopener noreferrer" class="">GitHub에 공개</a>됐습니다.</p>
<p>구조의 핵심은 권한 모델입니다. 에이전트는 권한 0에서 출발합니다. 사내 시스템(위키, 티켓, DB, 배포 도구) 각각의 앞에 Gatekeeper Worker라는 관문 코드를 두고, 에이전트는 그 관문이 허용하는 범위의 작업만 요청할 수 있습니다. "에이전트에게 사내 API 키를 통째로 주면 어디까지 뒤질지 모른다"는 공포를, 시스템별로 좁게 정의된 통로로 바꾸는 설계입니다.</p>
<!-- -->
<p>인프라 회사가 사내 업무 계층까지 내려와 그걸 오픈소스로 풀었다는 점, 그리고 하필 "OS"라는 이름을 붙였다는 점 때문에 Hacker News에서 논쟁이 붙었습니다. 이름이야 어떻든, 에이전트 권한 문제를 프록시 계층으로 푸는 패턴 자체는 참고할 가치가 있습니다. 뒤에 나올 이야기지만, 이 패턴은 사내 DB에 에이전트를 붙일 때 그대로 필요해지는 물건입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wallets-에이전트에게-용돈을-준다">Wallets: 에이전트에게 용돈을 준다<a href="https://dbalog.dev/blog/cloudflare-agents-week#wallets-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EC%9A%A9%EB%8F%88%EC%9D%84-%EC%A4%80%EB%8B%A4" class="hash-link" aria-label="Wallets: 에이전트에게 용돈을 준다에 대한 직접 링크" title="Wallets: 에이전트에게 용돈을 준다에 대한 직접 링크" translate="no">​</a></h2>
<p><a href="https://blog.cloudflare.com/wallets/" target="_blank" rel="noopener noreferrer" class="">Cloudflare Wallets</a>는 문제 정의가 직관적이라 Fortune 같은 일반 매체까지 다뤘습니다. <strong>에이전트는 은행 계좌를 못 만든다</strong>는 것입니다. 에이전트가 유료 API를 호출하고 리소스를 사려면 결국 사람의 카드가 어딘가에 물려 있어야 하는데, 그 카드에는 한도도 범위도 걸 수 없습니다.</p>
<p>Wallets의 구조는 위임입니다. 사람이 보유한 Account Wallet에 스테이블코인을 담아 두고, 에이전트마다 Virtual Wallet을 만들어 지출 한도, 허용 목록, 건당 최대 금액을 걸어 위임합니다. 결제 프로토콜로는 x402를 지원합니다. 8월 4일에는 <code>cloudflare.pay</code> 핸들 예약이 열렸고, 실제 지갑 인프라는 몇 달에 걸쳐 나온다고 합니다.</p>
<p>에이전트 지출 통제를 IAM처럼 다루는 첫 대형 시도라는 점에서, 실물이 나오면 다시 볼 가치가 있습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="나머지-발표들-프로토콜-계층">나머지 발표들: 프로토콜 계층<a href="https://dbalog.dev/blog/cloudflare-agents-week#%EB%82%98%EB%A8%B8%EC%A7%80-%EB%B0%9C%ED%91%9C%EB%93%A4-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B3%84%EC%B8%B5" class="hash-link" aria-label="나머지 발표들: 프로토콜 계층에 대한 직접 링크" title="나머지 발표들: 프로토콜 계층에 대한 직접 링크" translate="no">​</a></h2>
<p>한 주의 나머지를 채운 것은 프로토콜과 도구입니다.</p>
<ul>
<li class="">WebMCP 프리뷰: 웹사이트가 자신을 MCP 서버로 노출하는 방식의 프리뷰. 에이전트가 화면을 스크래핑하는 대신 구조화된 인터페이스로 사이트와 대화합니다</li>
<li class="">MCPv2: MCP 프로토콜의 다음 버전 지원</li>
<li class="">Agent Access Model: 에이전트의 리소스 접근을 사람 사용자와 구분해 다루는 모델</li>
<li class="">Kitesurf: 에이전트 우선(agent-first) 브라우저</li>
</ul>
<p>그리고 주가 끝난 뒤에도 같은 결의 발표가 이어졌습니다. 8월 14일에는 <a href="https://blog.cloudflare.com/mcp-security-updates/" target="_blank" rel="noopener noreferrer" class="">MCP 트래픽을 식별하고 보호하는 기능</a>이 나왔습니다. 에이전트가 만드는 트래픽을 봇도 사람도 아닌 제3의 유형으로 인정하고 전용 보안 장치를 붙이기 시작한 것입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="관통하는-그림">관통하는 그림<a href="https://dbalog.dev/blog/cloudflare-agents-week#%EA%B4%80%ED%86%B5%ED%95%98%EB%8A%94-%EA%B7%B8%EB%A6%BC" class="hash-link" aria-label="관통하는 그림에 대한 직접 링크" title="관통하는 그림에 대한 직접 링크" translate="no">​</a></h2>
<p>발표를 나열하면 잡다해 보이지만, 겹쳐 놓으면 한 문장이 됩니다. <strong>에이전트를 인터넷의 1급 시민으로 만들기 위한 인프라 계층을 선점하겠다</strong>는 것입니다.</p>
<p>사람 사용자에게는 이미 계정(신원), 결제 수단, 브라우저, 접근 제어가 있습니다. Cloudflare는 그 네 가지의 에이전트 버전을 한 주에 걸쳐 내놨습니다. Agent Access Model이 신원, Wallets가 결제, Kitesurf가 브라우저, Cloudflare OS와 Gatekeeper가 접근 제어입니다. CDN 회사의 신사업 나열이 아니라 계층 하나를 통째로 짜는 그림입니다.</p>
<p>DBA 관점에서 눈여겨볼 지점을 하나 꼽자면 Gatekeeper 패턴입니다. 에이전트에게 DB 접근을 열어 달라는 요청은 이미 현실에서 오고 있습니다(<a class="" href="https://dbalog.dev/blog/alloydb-mcp-server">AlloyDB의 MCP 서버</a> 때도 같은 이야기를 했습니다). 그때 계정과 권한을 직접 주는 대신, 좁게 정의된 관문 계층을 사이에 두는 설계가 표준이 될 가능성이 큽니다. 미리 그려 두면 요청이 왔을 때 당황하지 않을 수 있어요.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/cloudflare-agents-week#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://blog.cloudflare.com/agents-week-review-august-2026/" target="_blank" rel="noopener noreferrer" class="">Agents Week 2026 회고</a> - Cloudflare Blog, 2026-08-07</li>
<li class=""><a href="https://blog.cloudflare.com/cloudflare-os/" target="_blank" rel="noopener noreferrer" class="">Cloudflare OS: an open platform for agents, apps, and work</a> - Cloudflare Blog, 2026-08-04</li>
<li class=""><a href="https://blog.cloudflare.com/wallets/" target="_blank" rel="noopener noreferrer" class="">Announcing Cloudflare Wallets</a> - Cloudflare Blog, 2026-08-04</li>
<li class=""><a href="https://blog.cloudflare.com/mcp-security-updates/" target="_blank" rel="noopener noreferrer" class="">How Cloudflare detects MCP traffic and helps secure it</a> - Cloudflare Blog, 2026-08-14</li>
</ul>]]></content>
        <category label="Cloudflare" term="Cloudflare"/>
        <category label="AI" term="AI"/>
        <category label="에이전트" term="에이전트"/>
        <category label="MCP" term="MCP"/>
        <category label="Cloudflare OS" term="Cloudflare OS"/>
        <category label="Wallets" term="Wallets"/>
        <category label="인프라" term="인프라"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[DuckDB MySQL 500GB 실험]]></title>
        <id>https://dbalog.dev/blog/duckdb-mysql-engine-500gb</id>
        <link href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb"/>
        <updated>2026-08-08T02:00:00.000Z</updated>
        <summary type="html"><![CDATA[Percona가 DuckDB를 MySQL 스토리지 엔진으로 붙이는 실험을 500GB TPC-H로 검증했습니다. InnoDB가 28시간 넘게 걸리고도 4개를 완주 못 한 22개 분석 쿼리를 DuckDB 엔진은 186초에 끝냈고, 적재는 25.5배 빨랐으며 디스크는 5배 적게 썼습니다. 극단적인 숫자 뒤에 있는 구조와, 이 실험이 MySQL DBA에게 던지는 질문을 정리합니다.]]></summary>
        <content type="html"><![CDATA[<p>MySQL에서 분석 쿼리를 억지로 버텨 본 경험이 있다면, 이 숫자들이 남 일 같지 않을 거예요. InnoDB로 28시간 넘게 돌리고도 다 못 끝낸 TPC-H 22개 쿼리를, 같은 MySQL 프로세스 안의 DuckDB 엔진이 <strong>185.6초</strong>에 끝냈습니다.</p>
<p><a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/" target="_blank" rel="noopener noreferrer" class="">Percona가 8월 7일 공개한 실험</a>입니다. DuckDB를 MySQL의 스토리지 엔진으로 붙인 실험 프로젝트를 TPC-H 스케일팩터 500, 그러니까 원본 CSV 500GB(약 30억 행) 규모에서 검증했습니다. MySQL은 스토리지 엔진을 갈아 끼울 수 있는 구조라는 걸 다들 알지만, 그 자리에 컬럼 지향 분석 엔진을 꽂는 발상을 실제 수치로 검증한 것은 드문 일입니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="숫자부터">숫자부터<a href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb#%EC%88%AB%EC%9E%90%EB%B6%80%ED%84%B0" class="hash-link" aria-label="숫자부터에 대한 직접 링크" title="숫자부터에 대한 직접 링크" translate="no">​</a></h2>
<p>테스트 환경은 80코어, 187.5GB RAM 서버 한 대입니다. 같은 데이터를 InnoDB, MySQL+DuckDB 엔진, 순수 DuckDB 세 가지로 적재하고 비교했습니다.</p>





























<table><thead><tr><th>항목</th><th>InnoDB</th><th>MySQL+DuckDB 엔진</th><th>순수 DuckDB</th></tr></thead><tbody><tr><td>적재 시간</td><td>15시간 21분</td><td>36분 5초</td><td>(동일 계열)</td></tr><tr><td>디스크 사용</td><td>673.2 GB</td><td>132.4 GB</td><td>132.4 GB</td></tr><tr><td>TPC-H 22개 쿼리</td><td>28시간+ (4개 미완주)</td><td>185.6초</td><td>152.7초</td></tr></tbody></table>
<p>쿼리별로 보면 격차가 더 생생합니다. Q1은 InnoDB 11,864초가 11.1초로, Q6는 3,539초가 1.3초로 줄었습니다. InnoDB는 쿼리당 2시간 제한을 두었는데도 4개를 완주하지 못했습니다.</p>
<p>디스크 방향도 반대입니다. 원본 CSV 500GB가 InnoDB에서는 673GB로 늘어났고, DuckDB에서는 132GB로 압축됐습니다. 행 지향 저장과 인덱스 오버헤드 대 컬럼 저장과 압축의 차이가 그대로 드러납니다.</p>
<p>정확성 검증도 곁들여져 있습니다. 22개 쿼리 중 21개가 순수 DuckDB 결과와 소수점 4자리까지 일치했습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="어떻게-동작하나">어떻게 동작하나<a href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb#%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%82%98" class="hash-link" aria-label="어떻게 동작하나에 대한 직접 링크" title="어떻게 동작하나에 대한 직접 링크" translate="no">​</a></h2>
<p>핵심은 MySQL의 스토리지 엔진 인터페이스입니다. 애플리케이션은 여전히 MySQL 프로토콜로 접속해 SQL을 실행하고, 해당 테이블의 저장과 스캔을 DuckDB가 담당합니다. 적재가 25.5배 빨랐던 비결도 여기 있는데, 엔진이 <code>LOAD DATA</code>를 행 단위 삽입으로 처리하지 않고 DuckDB의 <code>COPY</code>로 직접 전달해 배치 적재로 바꿉니다.</p>
<!-- -->
<p>시도해 보는 것도 쉽게 만들어 놨습니다. Docker 이미지 한 줄로 실행됩니다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  perconalab/ducksdb-mysql-engine:latest</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="냉정하게-볼-부분">냉정하게 볼 부분<a href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb#%EB%83%89%EC%A0%95%ED%95%98%EA%B2%8C-%EB%B3%BC-%EB%B6%80%EB%B6%84" class="hash-link" aria-label="냉정하게 볼 부분에 대한 직접 링크" title="냉정하게 볼 부분에 대한 직접 링크" translate="no">​</a></h2>
<p>Percona 스스로 못박은 제약들이 있습니다.</p>
<p>첫째, 이것은 <strong>실험 소프트웨어</strong>입니다. 운영 투입 대상이 아니라고 글에서 반복해 강조합니다. 둘째, 분석 전용입니다. 포인트 조회 같은 OLTP 패턴은 여전히 행 경로가 낫고, 이 엔진의 목적이 아닙니다. 셋째, 메모리 관리가 아직 거칩니다. 메모리 제한을 넘는 쿼리는 디스크 스필 설정을 손봐야 합니다. 넷째, 서버 한 대의 특정 워크로드만 검증한 결과입니다.</p>
<p>그리고 벤치마크 자체의 성격도 감안해야 합니다. TPC-H는 컬럼 스토어에 유리한 순수 분석 워크로드입니다. InnoDB가 28시간 걸렸다는 것은 InnoDB가 나쁘다는 뜻이 아니라, 애초에 이 일을 시키면 안 되는 엔진에 이 일을 시켰다는 뜻에 가깝습니다. 문제는 현실의 많은 조직이 정확히 그렇게 쓰고 있다는 점이고, 이 실험의 가치는 그 간극을 숫자로 보여준 데 있습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="이어지는-실험-복제로-oltp와-분석-분리">이어지는 실험: 복제로 OLTP와 분석 분리<a href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb#%EC%9D%B4%EC%96%B4%EC%A7%80%EB%8A%94-%EC%8B%A4%ED%97%98-%EB%B3%B5%EC%A0%9C%EB%A1%9C-oltp%EC%99%80-%EB%B6%84%EC%84%9D-%EB%B6%84%EB%A6%AC" class="hash-link" aria-label="이어지는 실험: 복제로 OLTP와 분석 분리에 대한 직접 링크" title="이어지는 실험: 복제로 OLTP와 분석 분리에 대한 직접 링크" translate="no">​</a></h2>
<p>Percona는 이 실험을 한 단계 더 밀고 나가는 중입니다. 8월 13일 후속 글 <a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/" target="_blank" rel="noopener noreferrer" class="">Replicating from InnoDB into a DuckDB storage engine</a>에서는 primary의 InnoDB 테이블을 replica의 DuckDB 엔진 테이블로 복제하는 구성을 다뤘습니다. 쓰기는 InnoDB가 받고, 분석은 DuckDB replica가 받는 그림입니다.</p>
<p>이 방향이 흥미로운 이유는 기존 선택지와의 비교 때문입니다. MySQL의 분석 부하를 떼어내는 전통적인 답은 별도 웨어하우스(ClickHouse, BigQuery 등)로의 CDC 파이프라인인데, 그 순간 스키마 동기화, 지연, 운영 부담이 따라옵니다. replica 한 대의 스토리지 엔진만 바꿔서 같은 효과를 얻는다면, MySQL 프로토콜과 권한 체계 안에서 문제가 끝납니다. PostgreSQL 진영에서 pg_duckdb가 겨냥하는 자리와 정확히 같은 자리입니다.</p>
<p>아직 붙일 이름은 실험이지만, 방향은 뚜렷해 보여요. 분석 엔진을 밖에 두고 데이터를 나르는 대신, 익숙한 DB 안으로 분석 엔진을 들여오는 흐름입니다. Docker 이미지가 공개되어 있으니 가벼운 데이터로 직접 실행해 보고, 결과가 재미있으면 후속으로 다루겠습니다.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://dbalog.dev/blog/duckdb-mysql-engine-500gb#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/" target="_blank" rel="noopener noreferrer" class="">The DuckDB MySQL engine at 500 GB</a> - Percona Blog, 2026-08-07</li>
<li class=""><a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/" target="_blank" rel="noopener noreferrer" class="">Replicating from InnoDB into a DuckDB storage engine</a> - Percona Blog, 2026-08-13</li>
</ul>]]></content>
        <category label="MySQL" term="MySQL"/>
        <category label="DuckDB" term="DuckDB"/>
        <category label="Percona" term="Percona"/>
        <category label="스토리지 엔진" term="스토리지 엔진"/>
        <category label="OLAP" term="OLAP"/>
        <category label="TPC-H" term="TPC-H"/>
        <category label="데이터베이스" term="데이터베이스"/>
    </entry>
</feed>