<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>에디블로그</title>
    <link>https://jessyt.tistory.com/</link>
    <description>------ 한발자국씩 성장하자 ------ 
Github: https://github.com/yongtaelim
LinkedIn: https://www.linkedin.com/in/%EC%9A%A9%ED%83%9C-%EC%9E%84-622b69218/</description>
    <language>ko</language>
    <pubDate>Fri, 21 Aug 2026 11:54:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>에디개발자</managingEditor>
    <image>
      <title>에디블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4332890/attach/c4ee475062214c7abf5dd1f127d86dd1</url>
      <link>https://jessyt.tistory.com</link>
    </image>
    <item>
      <title>[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)</title>
      <link>https://jessyt.tistory.com/515</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ltcpK/dJMcaaS6kmc/lXeFthWkXKNp7f3r8PDW90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ltcpK/dJMcaaS6kmc/lXeFthWkXKNp7f3r8PDW90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ltcpK/dJMcaaS6kmc/lXeFthWkXKNp7f3r8PDW90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FltcpK%2FdJMcaaS6kmc%2FlXeFthWkXKNp7f3r8PDW90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelPp8o%2FdJMcajoR2Ji%2FF54saEUmV2KktcBESfgBL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;892&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;[백엔드] 웹훅 연동의 함정: 중복 수신, 순서 역전, 유실 그리고 대사(reconciliation)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제사&amp;middot;외부 플랫폼 연동의 절반은 웹훅 받기예요. &quot;이벤트가 오면 처리한다&quot; &amp;mdash; 단순해 보이는데, 막상 운영하면 같은 웹훅이 두 번 와요. 취소가 승인보다 먼저 와요. 와야 할 게 안 와요. 웹훅은 친절한 알림이지 &lt;b&gt;신뢰할 수 있는 전달 채널이 아니거든요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 웹훅의 3대 불확실성(중복&amp;middot;순서 역전&amp;middot;유실), 수신 처리의 정석(빠른 ACK + 멱등 처리), 그리고 돈이 걸린 연동의 필수 장치인 대사(reconciliation)까지 짚어볼게요. 함정 시리즈 7편이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 웹훅을 믿으면 안 되는 세 가지 이유&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn7jyE/dJMcaaS6kma/JqKOsfdndrhjQiTfT5XA11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn7jyE/dJMcaaS6kma/JqKOsfdndrhjQiTfT5XA11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn7jyE/dJMcaaS6kma/JqKOsfdndrhjQiTfT5XA11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn7jyE%2FdJMcaaS6kma%2FJqKOsfdndrhjQiTfT5XA11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;웹훅의 3대 불확실성. 응답 유실 시 상대가 재전송해 중복이 오고, 재시도와 병렬 전송으로 취소가 승인보다 먼저 도착하는 순서 역전이 생기며, 상대 장애나 내 다운타임에 영영 유실될 수도 있다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중복&lt;/b&gt; &amp;mdash; 상대도 &lt;a href=&quot;https://jessyt.tistory.com/504&quot;&gt;그 비대칭&lt;/a&gt;을 똑같이 겪어요. 내가 200을 줬는데 그 응답이 유실되면, 상대는 &quot;전달 실패&quot;로 보고 재전송해요. 성실한 외부사일수록 재시도를 잘 하니까, 중복은 연동에서 늘 깔리는 기본 조건이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 역전&lt;/b&gt; &amp;mdash; &quot;승인 &amp;rarr; 취소&quot; 순서로 발생한 이벤트가 &quot;취소 &amp;rarr; 승인&quot; 순서로 도착할 수 있어요. 앞 이벤트가 실패해 재시도되는 사이 뒤 이벤트가 먼저 성공하는, &lt;a href=&quot;https://jessyt.tistory.com/455&quot;&gt;카프카에서 본 그 역학&lt;/a&gt;이에요. 발생 순서와 도착 순서는 다른 거예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유실&lt;/b&gt; &amp;mdash; 상대 장애, 내 배포 순간의 5xx, 재전송 한도 소진... 영영 안 오는 웹훅도 있어요. &quot;웹훅만 믿고 상태를 확정&quot;하면 그 구멍이 그대로 데이터 불일치가 돼요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요컨대 웹훅 수신자는 &lt;b&gt;외부 회사를 브로커로 둔 at-least-once 컨슈머&lt;/b&gt;예요. 카프카 시리즈에서 배운 규율이 그대로 적용돼요 &amp;mdash; 단지 상대 설정을 우리가 못 만질 뿐이죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 수신의 정석, 빨리 받고 비동기로 처리&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elPp8o/dJMcajoR2Ji/F54saEUmV2KktcBESfgBL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelPp8o%2FdJMcajoR2Ji%2FF54saEUmV2KktcBESfgBL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;웹훅 수신 시퀀스. 외부사의 POST를 받으면 서명을 검증하고 원문을 저장한 뒤 즉시 200을 응답하며, 처리는 워커가 비동기로 미처리 이벤트를 픽업해 이벤트 ID 기반 멱등 처리와 상태 전이 규칙으로 순서를 판정한다. 처리를 끝내고 응답하면 상대 타임아웃으로 재전송 중복이 쌓인다&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;892&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 서명 검증부터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹훅 엔드포인트는 공개 URL이에요. 아무나 가짜 &quot;결제 완료&quot;를 쏠 수 있죠. 상대가 제공하는 시크릿 기반 서명(HMAC 등)을 반드시 검증해요. 이거 없는 웹훅 수신은 보안 사고 대기예요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 저장하고 즉시 200&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 실수가 웹훅 핸들러 안에서 무거운 처리(DB 갱신 + 알림 + 후속 API)를 동기로 다 하는 거예요. 처리가 길어지면 상대 입장에선 타임아웃 &amp;rarr; 재전송 &amp;rarr; 중복 증폭의 악순환이에요. 정석은 &lt;b&gt;받자마자 원본을 저장(DB나 큐)하고 즉시 200&lt;/b&gt; &amp;mdash; &quot;잘 받았다&quot;와 &quot;잘 처리했다&quot;를 분리하는 거예요. 처리 실패는 내 쪽 재시도로 풀고요. 어디서 봤죠? &lt;a href=&quot;https://jessyt.tistory.com/473&quot;&gt;Streams&lt;/a&gt;&amp;middot;카프카 컨슈머의 ack 분리와 같은 사고방식이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ 이벤트 ID로 멱등 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대가 주는 이벤트 고유 ID(없으면 본문 해시)로 처리 이력을 남기고 이미 처리한 ID는 건너뛰어요. &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;컨슈머 멱등성&lt;/a&gt;의 그 방법들 그대로요. 단, &quot;이미 처리한 이벤트&quot;에도 &lt;b&gt;200을 응답&lt;/b&gt;해야 해요 &amp;mdash; 에러를 주면 상대가 또 재전송하니까요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;④ 순서 역전은 상태 머신으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이벤트를 도착 순서대로 적용&quot;하면 역전에 무너져요. 대신 &lt;b&gt;상태 전이 규칙으로 판정&lt;/b&gt;해요 &amp;mdash; 현재 상태와 이벤트의 조합이 유효한지 봐요. 유효하지 않으면(승인 없는 취소) 보류했다가 재평가하거나, 이벤트의 타임스탬프&amp;middot;버전으로 &quot;더 옛날 이벤트의 늦은 도착&quot;을 무시해요. &quot;취소된 주문에 승인이 늦게 와도 상태가 안 되돌아가는&quot; 게 규칙으로 보장되는 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 대사, 웹훅은 알림이고 진실은 조회로&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7BC7d/dJMcagTj0dj/CNeW8HOKhdVL4SPdDOOuOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7BC7d/dJMcagTj0dj/CNeW8HOKhdVL4SPdDOOuOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7BC7d/dJMcagTj0dj/CNeW8HOKhdVL4SPdDOOuOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7BC7d%2FdJMcagTj0dj%2FCNeW8HOKhdVL4SPdDOOuOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;대사 reconciliation 구조. 웹훅은 빠르지만 유실 가능한 실시간 경로이고, 주기 배치가 상대 조회 API로 내 상태와 비교해 보정하는 경로가 유실을 복구한다. 불일치 건수는 조기 경보 지표가 된다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;유실 문제의 근본 해법은 웹훅을 더 잘 받는 게 아니라, &lt;b&gt;웹훅 없이도 진실에 도달하는 경로&lt;/b&gt;를 따로 두는 거예요. 두 경로 구조예요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 경로&lt;/b&gt; &amp;mdash; 웹훅. 빠르게 상태를 반영하지만 유실될 수 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보정 경로&lt;/b&gt; &amp;mdash; 주기 배치가 상대의 조회 API로 직접 물어봐요. 특히 &quot;PENDING 상태로 N분 넘게 머문 건&quot;을 조회해서 확정해요. 웹훅이 안 왔어도 결국 맞는 상태로 수렴해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 대사(reconciliation)예요. 결제&amp;middot;정산처럼 돈이 걸린 연동에서는 옵션이 아니라 필수고요. 그리고 대사에서 발견되는 &lt;b&gt;불일치 건수를 지표로&lt;/b&gt; 걸어두면 &amp;mdash; 웹훅 유실이나 내 처리 버그의 조기 경보가 돼요. 평소 0이던 불일치가 늘기 시작하면 뭔가 터지고 있는 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 보내는 쪽이 될 때도 같은 예의를&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거꾸로 우리가 웹훅을 발송하는 입장이면, 받는 쪽이 위 가정을 할 수 있게 해주는 게 예의예요 &amp;mdash; 이벤트 고유 ID와 타임스탬프를 싣고, 서명을 제공하고, 실패 시 &lt;a href=&quot;https://jessyt.tistory.com/508&quot;&gt;백오프 재시도&lt;/a&gt;를 하되 한도를 두고, 놓친 이벤트를 조회할 API를 같이 열어주는 것. 그리고 발송 자체는 &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스&lt;/a&gt;로 &quot;커밋됐으면 반드시 발송&quot;을 보장하고요. 좋은 웹훅 공급자의 체크리스트가 곧 좋은 수신자의 가정 목록이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 결제 완료 처리가 두 번 됐어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 ID 멱등 처리가 없는 거예요. 처리 이력 테이블(유니크 제약)을 깔고 중복 수신에도 200을 응답해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;취소했는데 상태가 다시 승인으로 돌아갔어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늦게 도착한 옛 이벤트가 상태를 덮은 거예요. 상태 전이 규칙(최종 상태에서 역행 금지)과 이벤트 타임스탬프 비교를 넣어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹훅이 안 와서 주문이 PENDING에 멈춰 있어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유실이에요. &quot;N분 넘은 PENDING 건 조회&amp;middot;확정&quot; 대사 배치가 답이에요. 이미 있다면 그 주기&amp;middot;범위를 점검해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹훅 핸들러가 느려서 상대가 계속 재전송해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 처리 과적이에요. 저장 후 즉시 200 + 비동기 처리로 분리해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹훅 연동의 본질은 한 문장이에요 &amp;mdash; 웹훅은 빠른 알림일 뿐, 진실은 멱등한 처리와 대사가 만든다. 그래서 서명을 검증해요. 저장 후 즉시 200을 주고(처리는 비동기로) 이벤트 ID로 멱등하게 처리해요. 순서는 도착 순서가 아니라 상태 전이 규칙으로 판정해요. 그리고 유실은 대사(reconciliation)로 보정하고 그 불일치 건수를 조기 경보 지표로 걸어둬요. 결국 카프카 컨슈머의 규율을 회사 바깥 경계에 그대로 적용하는 일이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 &quot;받는 쪽&quot;의 함정이었어요. 다음은 모든 게 터진 다음에 봐야 하는 곳 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/516&quot;&gt;JVM 메모리&amp;middot;스레드 진단&lt;/a&gt;으로, 함정 시리즈를 마무리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.stripe.com/webhooks&quot;&gt;Stripe &amp;mdash; Webhooks best practices&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://webhooks.fyi/&quot;&gt;webhooks.fyi &amp;mdash; Webhook Security &amp;amp; Reliability&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>reconciliation</category>
      <category>webhook</category>
      <category>대사</category>
      <category>멱등성</category>
      <category>백엔드</category>
      <category>외부 연동</category>
      <category>웹훅</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/515</guid>
      <comments>https://jessyt.tistory.com/515#entry515comment</comments>
      <pubDate>Fri, 21 Aug 2026 07:30:29 +0900</pubDate>
    </item>
    <item>
      <title>[Java] HTTP 클라이언트 함정: 타임아웃 기본값, 커넥션 풀, keep-alive, DNS 캐싱</title>
      <link>https://jessyt.tistory.com/514</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XkIVZ/dJMcaaevQQa/uwhig0WmhzlTzJbDsxGeNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XkIVZ/dJMcaaevQQa/uwhig0WmhzlTzJbDsxGeNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XkIVZ/dJMcaaevQQa/uwhig0WmhzlTzJbDsxGeNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXkIVZ%2FdJMcaaevQQa%2Fuwhig0WmhzlTzJbDsxGeNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Java] HTTP 클라이언트 함정: 타임아웃 기본값, 커넥션 풀, keep-alive, DNS 캐싱&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Java] HTTP 클라이언트 함정: 타임아웃 기본값, 커넥션 풀, keep-alive, DNS 캐싱&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 호출 코드는 보통 &quot;그냥 되니까&quot; 기본값으로 둬요. 그리고 그 기본값들은 평소엔 멀쩡하다가 &amp;mdash; 상대가 느려진 날, 트래픽이 몰린 날, 상대가 페일오버한 날 &amp;mdash; 정확히 최악의 타이밍에 터져요. HTTP 클라이언트의 함정은 코드가 아니라 &lt;b&gt;설정하지 않은 것들&lt;/b&gt;에 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 타임아웃&amp;middot;커넥션 풀의 기본값 함정, 간헐적 Connection reset의 정체(죽은 keep-alive 커넥션), 그리고 의외의 복병인 JVM DNS 캐싱까지 하나씩 뜯어봐요. RestTemplate&amp;middot;RestClient&amp;middot;WebClient&amp;middot;Feign 어느 걸 쓰든 그 밑의 원리는 같아요. 커넥션 풀의 일반 원리는 &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;HikariCP 편&lt;/a&gt;과 짝이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 기본값 점검표, 안 만지면 이렇게 돌아요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lq6kX/dJMcaijhGiN/aqNH7RxGUTVXMgiMqUxrQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lq6kX/dJMcaijhGiN/aqNH7RxGUTVXMgiMqUxrQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lq6kX/dJMcaijhGiN/aqNH7RxGUTVXMgiMqUxrQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flq6kX%2FdJMcaijhGiN%2FaqNH7RxGUTVXMgiMqUxrQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;HTTP 클라이언트 기본값 함정. 타임아웃은 기본 무제한이라 장애 전파 통로가 되고, 커넥션 풀은 호스트당 한도가 작아 풀 대기가 생기며, 에러 응답 본문을 안 읽으면 커넥션이 재사용되지 못해 풀이 마른다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임아웃 &amp;mdash; 무제한이 기본인 경우가 많아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 RestTemplate의 기본 설정은 타임아웃이 사실상 무제한이에요. 상대가 응답을 안 주면 내 스레드가 &lt;b&gt;영원히&lt;/b&gt; 기다려요. 상대 장애 &amp;rarr; 내 스레드 고갈 &amp;rarr; 내 장애, &lt;a href=&quot;https://jessyt.tistory.com/507&quot;&gt;연쇄 장애 편&lt;/a&gt;의 그 1번 통로예요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// Spring 6+ RestClient &amp;mdash; 타임아웃 명시가 첫 줄
var factory = new JdkClientHttpRequestFactory();
factory.setReadTimeout(Duration.ofSeconds(3));   // 상대 p99 기준
// connect는 1~3초면 충분 &amp;mdash; &quot;연결&quot;이 느린 건 이미 비정상

RestClient client = RestClient.builder()
    .requestFactory(factory)
    .baseUrl(&quot;https://api.partner.com&quot;)
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connect 타임아웃(연결 수립)은 짧게 1~3초, read 타임아웃(응답 대기)은 호출처의 정상 응답 p99 기준으로요. 그리고 &lt;b&gt;호출처마다 다르게&lt;/b&gt; &amp;mdash; 내부 서비스 1초와 외부 결제사 5초를 한 클라이언트 설정으로 뭉뚱그리면 어느 쪽엔가 안 맞아요. &lt;a href=&quot;https://jessyt.tistory.com/508&quot;&gt;타임아웃 예산 편&lt;/a&gt;의 &quot;안쪽 합 &amp;lt; 바깥&quot; 규칙도 같이요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커넥션 풀 &amp;mdash; 호스트당 한도가 복병이에요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀링 클라이언트(Apache HttpClient 등)는 전체 풀 크기 말고 &lt;b&gt;호스트당(per-route) 한도&lt;/b&gt;가 따로 있어요. 기본값이 작아서(예: 5), 한 외부 API에 동시 요청이 6개를 넘으면 그때부터 풀 대기예요. &quot;상대 API는 빠른데 내 호출만 느려요&quot;의 단골 원인이에요 &amp;mdash; 상대 응답 시간이 아니라 &lt;b&gt;내 풀 대기 시간&lt;/b&gt;이었던 거죠. 주력 호출처는 per-route 한도를 트래픽에 맞게 명시해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;응답 본문 &amp;mdash; 안 읽고 버리면 커넥션이 새요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 응답이라고 본문을 안 읽고 던져버리면, 그 커넥션은 재사용 불가 상태로 남아요. 풀이 슬금슬금 말라가는 조용한 누수예요. 본문은 항상 소비하거나 명시적으로 닫는 게(라이브러리별 권장 패턴) 규율이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 간헐적 Connection reset, 죽은 커넥션 재사용&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KIeWt/dJMcaaevQP9/grqaYeACbOrVLJr8qrmXU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KIeWt/dJMcaaevQP9/grqaYeACbOrVLJr8qrmXU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KIeWt/dJMcaaevQP9/grqaYeACbOrVLJr8qrmXU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKIeWt%2FdJMcaaevQP9%2FgrqaYeACbOrVLJr8qrmXU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;죽은 keep-alive 커넥션 재사용 문제. 상대 서버나 LB가 유휴 커넥션을 조용히 끊은 뒤 내 풀이 그 커넥션을 빌려주면 Connection reset이 난다. 유휴 수명을 상대보다 짧게 잡고 유휴 커넥션을 검증 정리한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;keep-alive는 커넥션을 재사용해 핸드셰이크 비용을 아끼는 좋은 기능인데, 여기에 시간차 함정이 있어요. 상대 서버(또는 중간 로드밸런서)는 유휴 커넥션을 일정 시간(예: 60초) 뒤 &lt;b&gt;조용히&lt;/b&gt; 끊어요. 내 풀은 그걸 몰라요. 다음 요청에 그 죽은 커넥션을 빌려주면 &lt;code&gt;Connection reset&lt;/code&gt;&amp;middot;&lt;code&gt;NoHttpResponseException&lt;/code&gt;이에요. &quot;가끔, 불규칙하게, 재시도하면 되는&quot; 에러의 전형이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처방은 두 갈래예요 &amp;mdash; &lt;b&gt;내 풀의 유휴 커넥션 수명을 상대의 keep-alive보다 짧게&lt;/b&gt;(상대가 60초면 나는 30초에 폐기) 잡고, 유휴 커넥션 검증&amp;middot;정리(evict idle) 옵션을 켜요. &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;HikariCP의 max-lifetime &amp;lt; wait_timeout&lt;/a&gt;과 완전히 같은 원리예요 &amp;mdash; &quot;빌려주기 전에 살아 있는지&quot;는 모든 풀의 공통 숙제예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. DNS 캐싱, 상대는 옮겼는데 나만 옛 주소로&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zD9G0/dJMcaayOFRh/k8JUrZx28yHebHfIWgilq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zD9G0/dJMcaayOFRh/k8JUrZx28yHebHfIWgilq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zD9G0/dJMcaayOFRh/k8JUrZx28yHebHfIWgilq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzD9G0%2FdJMcaayOFRh%2Fk8JUrZx28yHebHfIWgilq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;JVM DNS 캐싱 함정. 상대가 장애로 IP를 페일오버해 DNS를 갱신했는데 내 JVM은 캐시된 옛 IP를 계속 호출해 모든 요청이 실패한다. JVM DNS TTL 설정과 커넥션 수명 제한으로 대비한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;외부사가 장애로 IP를 페일오버하고 DNS를 갱신했는데, &lt;b&gt;내 서비스만&lt;/b&gt; 계속 실패하는 경우가 있어요. JVM이 DNS 조회 결과를 캐시하고 있어서예요. 환경에 따라 TTL이 길거나 사실상 영구 캐시인 설정도 있어서, 상대가 이사 간 뒤에도 옛 주소로 계속 노크하는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지를 봐요. 첫째, JVM DNS TTL(&lt;code&gt;networkaddress.cache.ttl&lt;/code&gt;)을 확인하고 30초 정도로 명시해요. 둘째, &lt;b&gt;이미 맺어진 커넥션은 DNS와 무관하게 옛 IP에 붙어 있다&lt;/b&gt;는 것 &amp;mdash; 커넥션 수명 제한(위의 그 설정)이 있어야 새 IP로 갈아타져요. DNS는 갱신됐는데 안 풀린다면 풀의 장수 커넥션을 의심해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 마지막 함정, 클라이언트 자체를 매번 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청마다 &lt;code&gt;new RestTemplate()&lt;/code&gt;&amp;middot;새 클라이언트 인스턴스를 만드는 코드도 종종 봐요. 풀&amp;middot;타임아웃 설정이 무의미해지고(매번 새 풀), 핸드셰이크를 매번 다시 해요. 리소스 정리도 안 돼요. HTTP 클라이언트는 &lt;b&gt;설정을 담아 빈으로 한 번 만들어 재사용&lt;/b&gt;하는 객체예요. 호출처별로 설정이 다르면 호출처별 빈을 따로 두고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 장애 때 우리 서비스가 같이 죽었어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;read 타임아웃 무제한 + 스레드 고갈 조합이에요. 타임아웃 명시가 1번이고, 그 위에 &lt;a href=&quot;https://jessyt.tistory.com/507&quot;&gt;서킷 브레이커&lt;/a&gt;를 얹어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Connection reset이 간헐적으로 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;죽은 keep-alive 커넥션이에요. 유휴 수명 단축 + evict idle로 잡아요. 멱등 호출이면 1회 재시도도 같이요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상대는 빠르다는데 내 호출은 느려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;per-route 풀 대기를 의심해요. 풀 지표(대기 수)를 보면 바로 갈려요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상대 페일오버 후 우리만 복구가 늦었어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 캐시 + 장수 커넥션이에요. TTL 설정과 커넥션 수명 제한을 같이 점검해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디부터 볼까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 클라이언트의 함정은 전부 &quot;평소엔 안 보이다 상대가 이상해진 날 터지는&quot; 것들이에요. 증상으로 어디를 만질지 갈라봐요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;외부 장애에 우리까지 죽었다면&lt;/b&gt; &amp;rarr; 타임아웃을 무조건 명시해요(connect 짧게, read는 p99).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상대는 빠른데 내 호출만 느리다면&lt;/b&gt; &amp;rarr; 주력 호출처의 per-route 풀 한도를 봐요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection reset이 간헐적이라면&lt;/b&gt; &amp;rarr; 유휴 커넥션 수명을 상대 keep-alive보다 짧게.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상대 페일오버 후 우리만 못 따라갔다면&lt;/b&gt; &amp;rarr; DNS TTL과 장수 커넥션을 같이 점검해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 무엇을 만지든, 클라이언트는 빈으로 한 번 만들어 재사용하는 게 전제예요. 다음 편은 받는 쪽 입장이 되어보는 외부 연동 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/515&quot;&gt;웹훅&lt;/a&gt;의 함정이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/rest-clients.html&quot;&gt;Spring &amp;mdash; REST Clients&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://hc.apache.org/httpcomponents-client-5.5.x/index.html&quot;&gt;Apache HttpClient &amp;mdash; Connection Management&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>dns 캐시</category>
      <category>HTTP 클라이언트</category>
      <category>java</category>
      <category>Keep-Alive</category>
      <category>RestClient</category>
      <category>백엔드</category>
      <category>커넥션 풀</category>
      <category>타임아웃</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/514</guid>
      <comments>https://jessyt.tistory.com/514#entry514comment</comments>
      <pubDate>Thu, 20 Aug 2026 07:30:38 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 대형 테이블 ALTER 함정: online DDL(INSTANT&amp;middot;INPLACE&amp;middot;COPY), pt-osc와 gh-ost</title>
      <link>https://jessyt.tistory.com/513</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l4K0U/dJMcajiaZxY/He1MMAr7xK22Bu5DrKI3jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l4K0U/dJMcajiaZxY/He1MMAr7xK22Bu5DrKI3jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l4K0U/dJMcajiaZxY/He1MMAr7xK22Bu5DrKI3jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl4K0U%2FdJMcajiaZxY%2FHe1MMAr7xK22Bu5DrKI3jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] 대형 테이블 ALTER 함정: online DDL(INSTANT&amp;middot;INPLACE&amp;middot;COPY), pt-osc와 gh-ost&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[MySQL] 대형 테이블 ALTER 함정: online DDL(INSTANT&amp;middot;INPLACE&amp;middot;COPY), pt-osc와 gh-ost&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;컬럼 하나 추가했을 뿐인데 서비스가 30분 멈췄어요.&quot; 수천만 건짜리 테이블의 ALTER는 평범한 SQL이 아니라 &lt;b&gt;잠재적 장애 작업&lt;/b&gt;이에요. 같은 ALTER라도 메타데이터만 바꾸는 1초짜리가 있고 테이블 전체를 복사하며 쓰기를 막는 30분짜리가 있거든요. 어느 쪽인지 모르고 실행하는 게 이 함정의 본질이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 MySQL online DDL의 세 알고리즘(INSTANT&amp;middot;INPLACE&amp;middot;COPY), 실행 전에 안전을 강제하는 법, 그리고 COPY가 불가피할 때의 도구(pt-online-schema-change&amp;middot;gh-ost)까지 정리해요. &lt;a href=&quot;https://jessyt.tistory.com/512&quot;&gt;무중단 배포 편&lt;/a&gt;의 expand 단계에서 만나는 그 벽이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 같은 ALTER가 아니에요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N8nYr/dJMcajiaZxW/S7CreKGtX1Kk6zIFq27TH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N8nYr/dJMcajiaZxW/S7CreKGtX1Kk6zIFq27TH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N8nYr/dJMcajiaZxW/S7CreKGtX1Kk6zIFq27TH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN8nYr%2FdJMcajiaZxW%2FS7CreKGtX1Kk6zIFq27TH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;MySQL ALTER 세 알고리즘. INSTANT는 메타데이터만 바꿔 1초에 끝나고, INPLACE는 재구축하되 읽기 쓰기를 허용하며, COPY는 테이블 전체를 복사하며 쓰기를 차단해 대형 테이블에서 사실상 장애가 된다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL(InnoDB)의 ALTER는 변경 종류에 따라 전혀 다른 방식으로 돌아요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;INSTANT&lt;/b&gt; &amp;mdash; 메타데이터만 바꿔요. 데이터를 안 건드리니 테이블이 1억 건이어도 1초예요. 컬럼 추가(8.0.12+ 도입, 8.0.29+부터는 임의 위치도 가능), 기본값 변경 등이 해당돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INPLACE&lt;/b&gt; &amp;mdash; 테이블 복사 없이 작업하면서 그동안 읽기&amp;middot;쓰기를 허용해요. 재구축형(PK 추가 등)과 비재구축형이 있는데, 대표 예인 세컨더리 인덱스 추가는 비재구축이에요. 오래 걸려도 서비스는 계속 도는데, 작업 부하와 복제 지연은 발생해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COPY&lt;/b&gt; &amp;mdash; 새 테이블을 만들어 전체를 복사하고 그동안 &lt;b&gt;쓰기를 차단&lt;/b&gt;해요. 컬럼 타입 변경이 대표고 PK는 단독 DROP만 여기 걸려요(같은 문장에서 DROP+ADD를 함께 하면 INPLACE 허용). 대형 테이블에선 시간 &amp;times; 디스크 2배 &amp;times; 쓰기 정지 = 사실상 장애예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함정은 &lt;b&gt;어느 알고리즘이 적용될지가 변경 종류&amp;middot;버전에 따라 다르다&lt;/b&gt;는 거예요. &quot;지난번 ALTER는 금방 끝났는데&quot;라는 경험이 다음 ALTER의 안전을 보장하지 않아요. 그 컬럼 추가는 INSTANT였고, 이번 타입 변경은 COPY인 거죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 실행 전에 안전을 강제하는 법&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JHEbi/dJMcaayOFPK/tE5uhqvIKs3aORcpf8Afd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JHEbi/dJMcaayOFPK/tE5uhqvIKs3aORcpf8Afd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JHEbi/dJMcaayOFPK/tE5uhqvIKs3aORcpf8Afd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJHEbi%2FdJMcaayOFPK%2FtE5uhqvIKs3aORcpf8Afd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;ALTER 안전 확인법. ALGORITHM=INSTANT를 명시하면 해당 알고리즘으로 불가능한 변경일 때 실행되지 않고 에러가 나서, COPY로 돌 뻔한 위험한 변경을 실행 전에 발견할 수 있다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;제일 실용적인 습관 하나 &amp;mdash; &lt;b&gt;ALGORITHM을 명시&lt;/b&gt;하는 거예요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE orders ADD COLUMN memo VARCHAR(255),
  ALGORITHM=INSTANT;        -- INSTANT로 안 되면 실행 대신 에러!

ALTER TABLE orders ADD INDEX idx_user (user_id),
  ALGORITHM=INPLACE, LOCK=NONE;   -- 재구축은 하되 잠금은 거부&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 알고리즘으로 불가능한 변경이면 MySQL이 &lt;b&gt;실행하지 않고 에러&lt;/b&gt;를 내요. &quot;조용히 COPY로 강등돼서 테이블을 잠그는&quot; 최악을 원천 차단하는 거예요. 에러가 나면 그 변경은 위험하다는 뜻이니 전략을 다시 짜면 되고요(아래 도구로). &lt;code&gt;LOCK=NONE&lt;/code&gt;까지 같이 명시하면 &quot;DML 차단되면 실행하지 마&quot;가 강제돼요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;두 가지 더 챙겨요. 첫째, &lt;b&gt;리허설&lt;/b&gt; &amp;mdash; 스테이징에 운영급 데이터를 두고 같은 ALTER의 소요 시간을 재보는 게 정석이에요. INPLACE라도 1시간짜리 작업인지 5분짜리인지는 미리 알아야 하니까요. 둘째, &lt;b&gt;복제 지연&lt;/b&gt; &amp;mdash; 소스에서 1시간 걸린 DDL은 레플리카에서도 1시간 걸리고 그동안 복제가 밀려요. 레플리카로 읽기를 보내는 구조면 INPLACE도 &quot;읽기 최신성 장애&quot;가 될 수 있어요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 메타데이터 락, 짧지만 치명적인 복병&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSTANT&amp;middot;INPLACE라도 시작과 끝에 잠깐 &lt;b&gt;메타데이터 락(MDL)&lt;/b&gt;이 필요해요. 보통은 찰나라 문제없는데, 그 순간에 &lt;b&gt;오래 도는 트랜잭션이 그 테이블을 읽고 있으면&lt;/b&gt; ALTER가 MDL을 기다려요. 진짜 무서운 건 그다음이에요 &amp;mdash; ALTER 뒤에 줄 선 모든 쿼리가 ALTER를 기다리며 &lt;b&gt;전부 멈춰요.&lt;/b&gt; &quot;1초짜리 ALTER가 서비스를 세웠어요&quot;가 대개 이 그림이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ALTER 직전엔 &lt;code&gt;information_schema.innodb_trx&lt;/code&gt;로 장수 트랜잭션이 없는지 확인하고 &lt;code&gt;lock_wait_timeout&lt;/code&gt;을 짧게 잡아 &quot;못 잡으면 빨리 포기&quot;하게 해요. 트래픽 낮은 시간대 실행은 기본이고요. (장수 트랜잭션이 만악의 근원인 건 &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;MVCC 편&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;락 편&lt;/a&gt;에서 본 그대로예요.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. COPY가 불가피할 때, pt-osc와 gh-ost&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjzjcJ/dJMcajiaZxX/ZEeJhNId9KedUeKsyLAfM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjzjcJ/dJMcajiaZxX/ZEeJhNId9KedUeKsyLAfM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjzjcJ/dJMcajiaZxX/ZEeJhNId9KedUeKsyLAfM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjzjcJ%2FdJMcajiaZxX%2FZEeJhNId9KedUeKsyLAfM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;온라인 스키마 변경 도구 원리. 새 구조의 그림자 테이블을 만들어 청크 단위로 천천히 복사하고, 그동안의 변경을 트리거나 binlog로 따라잡은 뒤, 원자적 이름 교체로 순간 전환한다. 대가는 디스크 2배와 진행 중 추가 부하다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;타입 변경처럼 COPY를 피할 수 없는 변경은, MySQL에게 맡기는 대신 전용 도구로 &quot;직접, 천천히&quot; 해요. pt-online-schema-change(Percona)와 gh-ost(GitHub)가 표준이고 원리는 같아요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;① 새 구조의 &lt;b&gt;그림자 테이블&lt;/b&gt;을 만들고&lt;/li&gt;
&lt;li&gt;② 원본 데이터를 &lt;b&gt;청크 단위로 천천히&lt;/b&gt; 복사해요 &amp;mdash; 부하를 보며 속도를 조절하고 서비스는 원본 테이블로 정상 동작해요&lt;/li&gt;
&lt;li&gt;③ 복사하는 동안 들어온 변경도 따라잡아요 &amp;mdash; pt-osc는 트리거로, gh-ost는 binlog 구독으로(원본에 트리거 부하를 안 줘서 더 선호되는 편이에요)&lt;/li&gt;
&lt;li&gt;④ 다 따라잡으면 &lt;b&gt;원자적 RENAME&lt;/b&gt;으로 교체 &amp;mdash; 잠금은 찰나예요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대가는 분명해요 &amp;mdash; 디스크가 일시적으로 2배 필요하고 진행 내내 복사 부하가 얹혀요. 그래서 &quot;심야에, 디스크 여유 확인하고 며칠 걸려도 천천히&quot;가 기본 운용이에요. 거꾸로 말하면, 이 도구 덕분에 수억 건 테이블의 구조 변경도 무중단 루틴이 될 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬럼 추가 ALTER가 서비스를 세웠어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 갈래예요 &amp;mdash; COPY로 돈 변경이었거나(ALGORITHM 명시로 재발 방지), MDL 대기 연쇄(장수 트랜잭션 확인 + lock_wait_timeout 단축)예요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 추가 중인데 레플리카가 한참 밀려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INPLACE DDL의 복제 지연이에요. 정상 동작이니, 읽기 최신성이 중요한 시간대를 피하거나 gh-ost(레플리카 지연을 보며 스스로 감속)를 써요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pt-osc 돌리다 디스크가 가득 찼어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림자 테이블 + binlog로 디스크가 2배+ 필요해요. 시작 전 여유 공간 확인이 체크리스트 1번이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALTER를 끊었는데(KILL) 뒷정리가 필요하나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구 사용 중이었다면 그림자 테이블&amp;middot;트리거가 남아 있을 수 있어요. 도구의 cleanup 절차로 정리해요. 네이티브 DDL은 원자적이라(8.0+) 중단 시 알아서 롤백돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대형 테이블 ALTER는 &quot;조용한 COPY 강등&quot;부터 막는 데서 시작해요. ALGORITHM&amp;middot;LOCK을 명시해 위험한 변경을 실행 전에 에러로 드러내고 실행 직전엔 장수 트랜잭션이 없는지 확인해 MDL 연쇄를 끊어요. 소요 시간과 복제 지연은 스테이징 리허설로 미리 재보고 COPY를 피할 수 없으면 pt-osc&amp;middot;gh-ost로 천천히 흘려요. 스키마 변경을 expand-contract(&lt;a href=&quot;https://jessyt.tistory.com/512&quot;&gt;앞 편&lt;/a&gt;)로 쪼개는 습관과 합치면, DDL은 더 이상 도박이 아니에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 설정 한 줄 없이 쓰다 당하는 영역이에요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/514&quot;&gt;HTTP 클라이언트&lt;/a&gt;의 함정으로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html&quot;&gt;MySQL &amp;mdash; Online DDL Operations&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://github.com/github/gh-ost&quot;&gt;GitHub &amp;mdash; gh-ost&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>alter table</category>
      <category>gh-ost</category>
      <category>MYSQL</category>
      <category>online DDL</category>
      <category>pt-online-schema-change</category>
      <category>백엔드</category>
      <category>스키마 변경</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/513</guid>
      <comments>https://jessyt.tistory.com/513#entry513comment</comments>
      <pubDate>Wed, 19 Aug 2026 07:30:45 +0900</pubDate>
    </item>
    <item>
      <title>[백엔드] 무중단 배포의 함정: 구버전&amp;middot;신버전 공존, DB 스키마 변경(expand-contract), graceful shutdown</title>
      <link>https://jessyt.tistory.com/512</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bftxyW/dJMb99UaBxg/ZKiGiYz6Wrn67l5C74g1I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bftxyW/dJMb99UaBxg/ZKiGiYz6Wrn67l5C74g1I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bftxyW/dJMb99UaBxg/ZKiGiYz6Wrn67l5C74g1I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbftxyW%2FdJMb99UaBxg%2FZKiGiYz6Wrn67l5C74g1I1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[백엔드] 무중단 배포의 함정: 구버전&amp;middot;신버전 공존, DB 스키마 변경(expand-contract), graceful shutdown&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[백엔드] 무중단 배포의 함정: 구버전&amp;middot;신버전 공존, DB 스키마 변경(expand-contract), graceful shutdown&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;배포할 때만 에러가 튀어요&quot;는 우연이 아니에요. 무중단 배포(롤링&amp;middot;카나리)의 본질은 &lt;b&gt;구버전과 신버전이 동시에 트래픽을 받는 구간이 반드시 존재한다&lt;/b&gt;는 거고 그 공존 구간의 호환성을 안 챙기면 배포가 곧 장애 트리거가 돼요. 컬럼 하나 rename했을 뿐인데 구버전 인스턴스가 줄줄이 죽는 식이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공존 구간이 왜 생기는지부터, DB 스키마 변경의 정석(expand-migrate-contract), 처리 중 요청을 지키는 graceful shutdown, 그리고 배포가 만드는 부수 충격(리밸런싱&amp;middot;직렬화)까지 훑어볼게요. 함정 시리즈 4편이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 공존 구간, 모든 함정의 뿌리&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSDun6/dJMcahLrW8q/kuysj1GtiuMfAIh83GqL4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSDun6/dJMcahLrW8q/kuysj1GtiuMfAIh83GqL4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSDun6/dJMcahLrW8q/kuysj1GtiuMfAIh83GqL4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSDun6%2FdJMcahLrW8q%2Fkuysj1GtiuMfAIh83GqL4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;무중단 배포의 공존 구간. 롤링 배포 중간에는 구버전과 신버전 인스턴스가 동시에 트래픽을 받으며 같은 DB와 카프카와 캐시를 공유한다. 무중단 배포의 함정은 전부 이 공존 구간의 호환성 문제다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;롤링 배포는 인스턴스를 한두 대씩 교체해요. 중간 시점엔 v1 두 대, v2 두 대가 같이 떠서 &lt;b&gt;같은 DB&amp;middot;카프카&amp;middot;캐시를 공유하며&lt;/b&gt; 트래픽을 나눠 받아요. &quot;v2가 쓴 데이터를 v1이 읽는&quot; 순간이 반드시 있어요. 게다가 배포가 실패하면 롤백하니까, &lt;b&gt;v2가 만든 데이터를 v1이 소화할 수 있어야&lt;/b&gt; 롤백도 안전해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 무중단 배포의 제1원칙은 이거예요 &amp;mdash; &lt;b&gt;N번째 버전과 N+1번째 버전은 언제나 호환되어야 한다.&lt;/b&gt; 코드만이 아니라 스키마&amp;middot;메시지&amp;middot;캐시 포맷 전부요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. DB 스키마 변경, expand &amp;rarr; migrate &amp;rarr; contract&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdMSRh/dJMcahLrW8z/nB8MT2q2GjPY48zI90gumk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdMSRh/dJMcahLrW8z/nB8MT2q2GjPY48zI90gumk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdMSRh/dJMcahLrW8z/nB8MT2q2GjPY48zI90gumk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdMSRh%2FdJMcahLrW8z%2FnB8MT2q2GjPY48zI90gumk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;expand contract 마이그레이션. 컬럼 rename을 배포와 동시에 하면 구버전이 크래시하므로, 새 컬럼 추가, 양쪽 쓰기와 백필, 다음 배포에서 옛 컬럼 제거의 3단계로 하위호환을 유지한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 사고가 스키마 변경이에요. 컬럼 rename을 배포와 동시에 하면 &amp;mdash; 아직 살아 있는 v1이 옛 컬럼을 조회하다 &quot;Unknown column&quot;으로 죽어요. 그렇다고 배포 후에 rename하면, 롤백할 때 같은 일이 거꾸로 나고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정석은 &lt;b&gt;변경을 하위호환 단계로 쪼개는 것&lt;/b&gt;이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;① expand&lt;/b&gt; &amp;mdash; 새 컬럼을 &lt;i&gt;추가만&lt;/i&gt; 해요. 추가는 v1에도 v2에도 무해해요(아무도 안 쓰니까).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;② migrate&lt;/b&gt; &amp;mdash; v2를 배포해요. v2는 &lt;i&gt;양쪽 컬럼에 쓰고&lt;/i&gt; 새 컬럼을 읽어요. 기존 데이터는 배치로 백필하고요. 이 동안 v1이 살아 있어도, 롤백해도 안전해요 &amp;mdash; 옛 컬럼이 계속 채워지고 있으니까.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;③ contract&lt;/b&gt; &amp;mdash; 전 인스턴스가 v2로 안정된 &lt;i&gt;다음 배포&lt;/i&gt;에서 옛 컬럼(과 양쪽 쓰기 코드)을 제거해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄 규칙 &amp;mdash; &lt;b&gt;&quot;스키마는 코드보다 한 발 앞서 추가되고, 한 발 늦게 제거된다.&quot;&lt;/b&gt; NOT NULL 추가, 컬럼 타입 변경, enum 값 제거도 전부 같은 틀로 풀려요. 번거로워 보여도, 이게 &quot;배포 = 도박&quot;을 &quot;배포 = 루틴&quot;으로 바꾸는 비용이에요. 참고로 대형 테이블이면 ①의 ALTER 자체도 함정인데, 그건 다음 편(online DDL)에서 다뤄요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. graceful shutdown, 처리 중이던 요청의 운명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포는 결국 &quot;끄고 켜기&quot;예요. 끄는 순간 처리 중이던 HTTP 요청과 카프카 메시지는 어떻게 될까요? 종료 시그널(SIGTERM)에 즉사하면 그대로 증발이에요 &amp;mdash; 사용자는 에러를 받고 절반만 처리된 작업이 남아요.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;server:
  shutdown: graceful          # 새 요청 거절 + 진행 중 요청 완료 후 종료
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s   # 완료 대기 한도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 3.4부터는 graceful이 기본값이에요. 그 전 버전이면 명시가 필요해요. graceful shutdown의 순서는 &amp;mdash; 새 요청 받기를 멈추고(로드밸런서에서 빠지고) 진행 중인 요청&amp;middot;메시지 처리를 끝내요. 그다음 종료예요. 두 가지를 같이 챙겨요. 첫째, &lt;b&gt;플랫폼의 종료 유예와 맞추기&lt;/b&gt; &amp;mdash; 쿠버네티스라면 terminationGracePeriodSeconds가 앱의 대기 시간보다 길어야 하고, 트래픽 제외(readiness 끊기)가 종료보다 먼저 일어나야 해요. 둘째, &lt;b&gt;유예 안에 끝나는 작업 크기&lt;/b&gt; &amp;mdash; 한 메시지 처리가 5분짜리면 30초 유예로는 부족하죠. 긴 작업은 중단 가능하게(체크포인트) 설계하거나, 어차피 재처리된다는 전제로 &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;멱등하게&lt;/a&gt; 만들어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 배포의 부수 충격들&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIrCM/dJMb99UaBxa/GFKoSmkBK5KELiDCNqRtYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIrCM/dJMb99UaBxa/GFKoSmkBK5KELiDCNqRtYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIrCM/dJMb99UaBxa/GFKoSmkBK5KELiDCNqRtYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIrCM%2FdJMb99UaBxa%2FGFKoSmkBK5KELiDCNqRtYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;배포의 부수 충격. 즉사 종료는 처리 중 요청을 증발시키므로 graceful shutdown으로, 인스턴스 교체마다 반복되는 컨슈머 리밸런싱은 static membership으로, 캐시와 메시지의 직렬화 포맷 변경은 하위호환 규칙으로 막는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컨슈머 리밸런싱 폭풍&lt;/b&gt; &amp;mdash; 인스턴스가 N대 교체되면 컨슈머 그룹 재조정이 N번 연쇄돼요. 배포 내내 메시지 처리가 끊겼다 이어졌다 하죠. &lt;a href=&quot;https://jessyt.tistory.com/457&quot;&gt;리밸런싱 편&lt;/a&gt;의 처방 &amp;mdash; static membership + CooperativeSticky &amp;mdash; 이 정확히 배포를 위한 설정이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직렬화 호환&lt;/b&gt; &amp;mdash; 캐시에 든 객체, 카프카 메시지도 v1&amp;middot;v2가 같이 읽어요. v2가 필드를 빼거나 rename한 포맷을 쓰면 v1이 역직렬화에서 터져요. 이벤트&amp;middot;캐시 객체도 스키마와 같은 규칙이에요 &amp;mdash; 추가는 자유, 삭제&amp;middot;rename은 expand-contract. &lt;a href=&quot;https://jessyt.tistory.com/464&quot;&gt;스키마 호환성 편&lt;/a&gt;의 backward/forward가 바로 &quot;배포 순서&quot; 이야기였던 거고요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시 콜드 스타트&lt;/b&gt; &amp;mdash; 새 인스턴스는 로컬 캐시가 비어 있고 커넥션 풀도 새로 채워요. 배포 직후 잠깐 느려지는 게 이거예요. 심하면 워밍업(기동 후 트래픽 점진 유입)을 둬요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 중에만 5xx가 튀어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 &amp;mdash; ① graceful shutdown 설정과 LB 제외 타이밍 ② 스키마&amp;middot;직렬화 비호환(로그에 Unknown column&amp;middot;역직렬화 예외가 있나) ③ 새 인스턴스가 준비 전에 트래픽을 받는지(readiness 체크).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬럼 바꾸고 배포했더니 구버전이 죽었어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;expand-contract 위반이에요. 즉시 복구는 옛 컬럼 복원이고, 재발 방지는 &quot;스키마 변경은 항상 추가 먼저&quot; 규칙을 마이그레이션 리뷰에 박는 거예요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;롤백했는데도 에러가 계속 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v2가 만든 데이터(새 포맷 캐시&amp;middot;메시지)를 v1이 소화 못 하는 거예요. 캐시 비우기&amp;middot;문제 메시지 처리로 복구하고, 다음부터는 데이터 포맷도 하위호환 규칙에 포함시켜요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포할 때마다 카프카 처리가 한참 끊겨요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱 연쇄예요. static membership을 적용하고, 종료 유예가 컨슈머의 정상 탈퇴(leave group)를 기다려주는지 확인해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무중단 배포의 함정은 전부 한 문장에서 나와요 &amp;mdash; &lt;b&gt;&quot;구버전과 신버전이 같은 데이터를 공유하며 공존한다.&quot;&lt;/b&gt; 호환을 지키는 한, 배포와 롤백은 언제든 안전한 루틴이 돼요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스키마&lt;/b&gt; &amp;mdash; expand &amp;rarr; migrate &amp;rarr; contract로 항상 인접 버전과 호환되게.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지&amp;middot;캐시 포맷&lt;/b&gt; &amp;mdash; 추가는 자유, 삭제&amp;middot;rename은 같은 단계로.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;종료&lt;/b&gt; &amp;mdash; graceful하게, 트래픽 제외가 종료보다 먼저.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨슈머&lt;/b&gt; &amp;mdash; static membership으로 배포에 둔감하게.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 expand 단계에서 다시 벽을 만나요 &amp;mdash; 대형 테이블 ALTER와 &lt;a href=&quot;https://jessyt.tistory.com/513&quot;&gt;online DDL&lt;/a&gt;로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html&quot;&gt;Spring Boot &amp;mdash; Graceful Shutdown&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://martinfowler.com/bliki/ParallelChange.html&quot;&gt;Martin Fowler &amp;mdash; ParallelChange (expand-contract)&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>expand contract</category>
      <category>Graceful Shutdown</category>
      <category>롤링 배포</category>
      <category>무중단 배포</category>
      <category>백엔드</category>
      <category>스키마 변경</category>
      <category>하위호환</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/512</guid>
      <comments>https://jessyt.tistory.com/512#entry512comment</comments>
      <pubDate>Tue, 18 Aug 2026 07:30:55 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 깊은 페이징 성능 문제: OFFSET이 느려지는 이유와 커서 기반 페이징(no-offset)</title>
      <link>https://jessyt.tistory.com/511</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blvUqM/dJMcagsepPw/yPZWM0vQvlizZoiPacfGD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blvUqM/dJMcagsepPw/yPZWM0vQvlizZoiPacfGD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blvUqM/dJMcagsepPw/yPZWM0vQvlizZoiPacfGD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblvUqM%2FdJMcagsepPw%2FyPZWM0vQvlizZoiPacfGD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[DB] 깊은 페이징 성능 문제: OFFSET이 느려지는 이유와 커서 기반 페이징(no-offset)&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[DB] 깊은 페이징 성능 문제: OFFSET이 느려지는 이유와 커서 기반 페이징(no-offset)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록 API가 1페이지에선 1ms인데 뒤로 갈수록 느려지다, 관리자가 &quot;전체 내보내기&quot;로 수만 페이지를 훑는 순간 DB가 비명을 질러요. &lt;code&gt;LIMIT 20 OFFSET 100000&lt;/code&gt;의 구조적 비용이에요. OFFSET은 &quot;건너뛰기&quot;처럼 보이지만 실제로는 &lt;b&gt;다 읽고 버리기&lt;/b&gt;거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 OFFSET이 왜 선형으로 느려지는지, 커서 기반 페이징(no-offset)의 동작과 구현 디테일(유니크 커서&amp;middot;인덱스 설계), 그리고 어디까지 커서로 가고 어디는 OFFSET을 둘지 가려볼게요. 인덱스 원리는 &lt;a href=&quot;https://jessyt.tistory.com/490&quot;&gt;B+Tree 편&lt;/a&gt; 위에 서 있는 이야기예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. OFFSET은 점프가 아니라 &quot;읽고 버리기&quot;예요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/31Xv3/dJMcahSaRuf/HaYkyqXPYUOAIaVFbaWtfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/31Xv3/dJMcahSaRuf/HaYkyqXPYUOAIaVFbaWtfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/31Xv3/dJMcahSaRuf/HaYkyqXPYUOAIaVFbaWtfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F31Xv3%2FdJMcahSaRuf%2FHaYkyqXPYUOAIaVFbaWtfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;OFFSET 페이징의 비용. OFFSET 100000 LIMIT 20은 십만 이십 건을 읽어 십만 건을 버리는 구조라 뒷페이지일수록 선형으로 느려진다. DB는 OFFSET 지점으로 점프하지 못하고 앞에서부터 세면서 와야 한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;offset 기반 Querydsl 페이징 구현(이 글의 비교 대상)은 &lt;a href=&quot;https://jessyt.tistory.com/55&quot;&gt;Querydsl에 pageable을 적용하며&lt;/a&gt;(2020)에 있어요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 100000;
-- 실제 동작: 정렬 순서대로 100,020건을 만들어 100,000건을 버리고 20건 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 &quot;100,000번째&quot;가 어디인지 모르니까, 앞에서부터 세면서 와야 해요. &lt;a href=&quot;https://jessyt.tistory.com/490&quot;&gt;B+Tree&lt;/a&gt;는 &quot;값으로 찾기&quot;는 잘하지만 &quot;N번째로 가기&quot;는 못 하거든요. 그래서 OFFSET이 클수록 읽고 버리는 양이 선형으로 늘어요. 정렬&amp;middot;조인&amp;middot;조건이 끼면 버릴 100,000건을 전부 만들어놓고 버리는 셈이라 더 비싸요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;이 함정의 고약한 점은 평소엔 안 보인다는 거예요. 사용자 대부분은 1~5페이지만 보니까요. 그러다 ① 크롤러 봇이 ?page=99999를 순회하거나 ② 관리자 화면에서 전체 데이터를 페이지 루프로 내보내거나 ③ 배치가 페이징으로 전체 테이블을 훑을 때 &amp;mdash; 갑자기 슬로우 쿼리가 쏟아져요. &quot;목록 API가 가끔 수십 초&quot;의 흔한 정체예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 커서 페이징, &quot;마지막으로 본 것 다음부터&quot;&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOYiL7/dJMcagsepPf/aDSLOT2kKX6FUxZ3yTYfmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOYiL7/dJMcagsepPf/aDSLOT2kKX6FUxZ3yTYfmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOYiL7/dJMcagsepPf/aDSLOT2kKX6FUxZ3yTYfmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOYiL7%2FdJMcagsepPf%2FaDSLOT2kKX6FUxZ3yTYfmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;커서 기반 페이징. 몇 번째부터가 아니라 마지막으로 본 id 다음부터를 조건으로 걸면 인덱스로 시작점에 직행해 20건만 읽는다. 어느 페이지든 같은 속도다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;발상을 바꿔요. &quot;몇 번째부터 20개&quot;가 아니라 &lt;b&gt;&quot;내가 마지막으로 본 것 다음부터 20개&quot;&lt;/b&gt;로요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 첫 페이지
SELECT * FROM orders ORDER BY id DESC LIMIT 20;
-- 다음 페이지: 응답의 마지막 id(예: 99980)를 커서로
SELECT * FROM orders WHERE id &amp;lt; 99980 ORDER BY id DESC LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WHERE id &amp;lt; ?&lt;/code&gt;는 인덱스가 가장 잘하는 일이에요 &amp;mdash; 시작점으로 직행해서 옆으로 20개. 1페이지든 100만 번째 구간이든 &lt;b&gt;항상 같은 비용&lt;/b&gt;이에요. 응답에 마지막 항목의 커서를 실어주고(&lt;code&gt;nextCursor&lt;/code&gt;), 클라이언트가 다음 요청에 들고 오는 구조고요. 무한 스크롤&amp;middot;&quot;더보기&quot; UI와 정확히 맞는 모델이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 구현 디테일 두 가지&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJPst6/dJMcahkqPT4/I4dPmImKm2gK1TgybQsG3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJPst6/dJMcahkqPT4/I4dPmImKm2gK1TgybQsG3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJPst6/dJMcahkqPT4/I4dPmImKm2gK1TgybQsG3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJPst6%2FdJMcahkqPT4%2FI4dPmImKm2gK1TgybQsG3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;커서 페이징의 트레이드오프. 임의 페이지 점프가 안 되고, 커서 키는 유니크해야 하므로 created_at과 id 복합 커서를 쓰며, 커서 조건과 정렬이 인덱스를 타도록 설계해야 한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 커서 키는 유니크해야 해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신순(created_at) 정렬에서 &lt;code&gt;WHERE created_at &amp;lt; ?&lt;/code&gt;로만 자르면, &lt;b&gt;같은 시각에 생성된 행들&lt;/b&gt;에서 누락이나 중복이 생겨요. 경계의 동점을 가를 수 없으니까요. 그래서 유니크한 보조 키(보통 id)를 붙인 복합 커서를 써요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- (created_at, id) 복합 커서 &amp;mdash; 동점은 id로 가른다
SELECT * FROM orders
WHERE (created_at &amp;lt; :lastCreatedAt)
   OR (created_at = :lastCreatedAt AND id &amp;lt; :lastId)
ORDER BY created_at DESC, id DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 행 비교 문법 &lt;code&gt;(created_at, id) &amp;lt; (:lastCreatedAt, :lastId)&lt;/code&gt;도 문법상으로는 되는데, MySQL은 이걸 인덱스 접근 조건으로 못 써서 커서 페이징의 핵심인 인덱스 직행이 깨져요. MySQL에서는 위의 OR 형태를 유지해요. 행 비교가 인덱스를 타는 건 PostgreSQL 쪽이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 커서&amp;middot;정렬 컬럼이 인덱스를 타야 성립해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서 페이징의 빠름은 전적으로 인덱스 직행에서 와요. 조회 조건까지 포함해 &lt;code&gt;(user_id, created_at, id)&lt;/code&gt;처럼 &lt;a href=&quot;https://jessyt.tistory.com/491&quot;&gt;복합 인덱스&lt;/a&gt;를 정렬 방향과 맞춰 설계해야 해요. 인덱스 못 타는 커서 쿼리는 OFFSET보다 나을 게 없어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 커서의 대가, 그리고 절충&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;임의 페이지 점프가 없어요&lt;/b&gt; &amp;mdash; &quot;37페이지로 가기&quot;는 커서로 표현이 안 돼요. 무한 스크롤이면 아예 문제가 안 되고, 페이지 번호 UI가 꼭 필요한 화면(어드민 등)은 OFFSET을 유지하되 &lt;b&gt;최대 페이지를 제한&lt;/b&gt;(예: 200페이지)하는 절충이 현실적이에요. 그 너머는 검색&amp;middot;필터로 좁히게 유도하고요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전체 카운트도 같은 부류의 비용이에요&lt;/b&gt; &amp;mdash; &quot;총 1,234,567건 / 61,728페이지&quot; 표시를 위한 &lt;code&gt;COUNT(*)&lt;/code&gt;는 깊은 OFFSET만큼 비싸요. &quot;더보기&quot; UI는 카운트 자체를 없애주고, 꼭 필요하면 근사치&amp;middot;캐시로 풀어요. JPA &lt;code&gt;Page&lt;/code&gt;가 매번 날리는 count 쿼리가 무거운 것도 같은 이야기예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치의 전체 순회는 무조건 커서로&lt;/b&gt; &amp;mdash; 페이지 루프로 테이블을 훑는 배치는 뒤로 갈수록 느려지다 타임아웃 나요. &quot;id 기준으로 잘라서 진행&quot;(키셋 순회)이 배치의 정석이에요. 진행 위치(마지막 id)를 저장해두면 중단 후 이어하기도 공짜고요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목록 API가 뒤 페이지에서만 느려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊은 OFFSET이에요. 무한 스크롤형이면 커서로 전환하고, 페이지형이면 페이지 상한 + 검색 유도로 절충해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커서 페이징인데 항목이 빠지거나 중복돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서 키가 유니크하지 않은 거예요. (정렬키, id) 복합 커서로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;크롤러가 ?page=99999를 돌면서 DB가 힘들어해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 상한 검증(400 응답)부터 거는 게 응급처치예요. 구조적으로는 커서 전환이고요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치가 갈수록 느려지다 죽어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 루프 순회예요. 키셋(마지막 id) 기반 순회로 바꾸면 일정한 속도로 끝까지 가요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OFFSET은 점프가 아니라 &lt;b&gt;읽고 버리기&lt;/b&gt;라서 깊어질수록 선형으로 느려져요. 답은 커서(no-offset) &amp;mdash; &quot;마지막으로 본 것 다음부터&quot;를 인덱스로 직행하는 방식이고, 커서 키는 유니크하게(복합 커서), 인덱스는 정렬과 맞춰 설계해요. 무한 스크롤&amp;middot;배치 순회는 커서가 정답이고, 페이지 번호가 꼭 필요한 화면만 상한 걸린 OFFSET으로 절충해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지금 우리 목록 API에 던질 질문은 하나예요 &amp;mdash; &lt;b&gt;뒤 페이지로 갈수록 느려진다면, 그건 데이터가 많아서가 아니라 OFFSET을 쓰고 있어서가 아닐까?&lt;/b&gt; 다음 함정은 &lt;a href=&quot;https://jessyt.tistory.com/512&quot;&gt;무중단 배포&lt;/a&gt;, 배포하는 순간에만 터지는 버그예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/limit-optimization.html&quot;&gt;MySQL &amp;mdash; LIMIT Query Optimization&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://use-the-index-luke.com/no-offset&quot;&gt;Use The Index, Luke &amp;mdash; No-Offset&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>DB</category>
      <category>MYSQL</category>
      <category>no-offset</category>
      <category>offset</category>
      <category>무한 스크롤</category>
      <category>백엔드</category>
      <category>커서 페이징</category>
      <category>페이징</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/511</guid>
      <comments>https://jessyt.tistory.com/511#entry511comment</comments>
      <pubDate>Mon, 17 Aug 2026 07:30:59 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 타임존 버그 정리: LocalDateTime vs Instant, UTC 저장, 날짜 경계 함정</title>
      <link>https://jessyt.tistory.com/510</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vhTA5/dJMcaaldUXc/u35yk2AkFla2F8YNi2vjS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vhTA5/dJMcaaldUXc/u35yk2AkFla2F8YNi2vjS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vhTA5/dJMcaaldUXc/u35yk2AkFla2F8YNi2vjS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvhTA5%2FdJMcaaldUXc%2Fu35yk2AkFla2F8YNi2vjS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Java] 타임존 버그 정리: LocalDateTime vs Instant, UTC 저장, 날짜 경계 함정&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Java] 타임존 버그 정리: LocalDateTime vs Instant, UTC 저장, 날짜 경계 함정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;로컬에선 멀쩡했는데 배포하니 시간이 9시간 밀려요.&quot; 타임존 버그는 늘 이렇게 환경을 갈아탈 때, 그리고 자정 근처에서 터져요. 원인의 대부분은 하나로 모여요 &amp;mdash; &lt;b&gt;타임존 정보가 없는 시각(LocalDateTime)을 &quot;절대 시각&quot;처럼 쓴 것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 LocalDateTime이 왜 위험한지부터, &quot;UTC로 저장하고 표시할 때 변환&quot;이라는 표준 규율, 타입 선택 기준(Instant vs ZonedDateTime vs LocalDateTime), 그리고 &quot;오늘 매출&quot; 집계가 어긋나는 날짜 경계 함정까지 차례로 풀어요. 함정 시리즈 2편이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. LocalDateTime은 &quot;언제&quot;가 아니에요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PEJK8/dJMcaaS6j9f/JlDHhFh7WOZBfjl7OFALOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PEJK8/dJMcaaS6j9f/JlDHhFh7WOZBfjl7OFALOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PEJK8/dJMcaaS6j9f/JlDHhFh7WOZBfjl7OFALOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPEJK8%2FdJMcaaS6j9f%2FJlDHhFh7WOZBfjl7OFALOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;LocalDateTime의 함정. 타임존 정보가 없는 벽시계 숫자라 한국의 9시인지 UTC의 9시인지 알 수 없고, KST 로컬과 UTC 컨테이너에서 now가 9시간 다른 값을 만든다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LocalDateTime&lt;/code&gt;의 &quot;2026-07-27 09:00&quot;은 그냥 숫자 묶음이에요. 어느 타임존의 9시인지 정보가 없어요. 그래서 이 타입의 의미는 &lt;b&gt;실행 환경의 타임존에 따라 달라져요.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;LocalDateTime.now();
// Mac 로컬(KST): 한국시간 기준 지금
// 운영 컨테이너(기본 UTC): UTC 기준 지금 &amp;mdash; 한국보다 9시간 전의 &quot;숫자&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컨테이너의 기본 타임존은 UTC인 경우가 많아서 로컬(KST)에서 잘 돌던 코드가 배포 직후 9시간 어긋나요. 쿠폰 만료가 9시간 일찍 되고, 새벽 배치가 오후에 돌고요. &lt;code&gt;-Duser.timezone&lt;/code&gt;이나 TZ 환경변수로 맞추는 응급처치도 있지만 환경 설정에 의존하는 시각 코드 자체가 폭탄이에요. 근본 해법은 타입이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 규율: 절대 시각으로 저장, 표시할 때만 변환&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buqL9B/dJMcaaS6j9h/8uJPxI1WDkBqBwkot02J61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buqL9B/dJMcaaS6j9h/8uJPxI1WDkBqBwkot02J61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buqL9B/dJMcaaS6j9h/8uJPxI1WDkBqBwkot02J61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuqL9B%2FdJMcaaS6j9h%2F8uJPxI1WDkBqBwkot02J61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;시각 처리 표준 규율. 발생과 저장은 UTC 기준 절대 시각인 Instant로, API 전송은 오프셋 명시된 ISO-8601로, 사용자 타임존 변환은 표시하는 맨 끝에서만 한다. 대부분은 Instant, 지역 벽시계 스케줄은 ZonedDateTime, 타임존 무관 개념만 LocalDateTime이다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;국제 표준이자 사실상 업계 합의예요 &amp;mdash; &lt;b&gt;안에서는 UTC 절대 시각, 밖(사용자 눈앞)에서만 지역 시각.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;저장&lt;/b&gt; &amp;mdash; &lt;code&gt;Instant&lt;/code&gt;(UTC 기준 타임라인 위의 한 점)로 다루고 DB도 UTC 기준으로 통일해요. JPA&amp;amp;middot;JDBC라면 &amp;lt;code&amp;gt;hibernate.jdbc.time_zone=UTC&amp;lt;/code&amp;gt;(또는 드라이버 connectionTimeZone)를 명시해야 JVM 타임존에 끌려가지 않아요. 서버가 어디 있든, 컨테이너 타임존이 뭐든 같은 값이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송&lt;/b&gt; &amp;mdash; API는 ISO-8601에 오프셋을 명시해요: &lt;code&gt;2026-07-27T00:00:00Z&lt;/code&gt; 또는 &lt;code&gt;+09:00&lt;/code&gt;. 오프셋 없는 시각 문자열을 주고받는 순간 양쪽 해석이 갈려요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표시&lt;/b&gt; &amp;mdash; 맨 마지막에 사용자 타임존으로 변환해요: &lt;code&gt;instant.atZone(ZoneId.of(&quot;Asia/Seoul&quot;))&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 선택은 질문 하나로 갈려요 &amp;mdash; &quot;이건 &lt;b&gt;사건의 순간&lt;/b&gt;인가, &lt;b&gt;지역의 벽시계&lt;/b&gt;인가?&quot;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Instant&lt;/b&gt; &amp;mdash; 결제 시각, 로그, created_at... &quot;그 일이 일어난 순간&quot;. 백엔드 시각의 대부분이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ZonedDateTime&lt;/b&gt; &amp;mdash; &quot;매일 한국시간 09:00에 알림&quot; 같은 지역 벽시계 기반 스케줄. 타임존 규칙(DST 포함)을 알아야 하는 경우예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LocalDateTime&lt;/b&gt; &amp;mdash; 생일, 영업일처럼 애초에 타임존 개념이 없는 값에만. &quot;지금&quot;을 LocalDateTime으로 만드는 코드는 거의 항상 잘못이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. &quot;오늘 매출&quot;의 함정, 날짜 경계&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsdew7/dJMcaaS6j9j/CuPfbt0oxayy2ruIQQ7RK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsdew7/dJMcaaS6j9j/CuPfbt0oxayy2ruIQQ7RK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsdew7/dJMcaaS6j9j/CuPfbt0oxayy2ruIQQ7RK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsdew7%2FdJMcaaS6j9j%2FCuPfbt0oxayy2ruIQQ7RK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;날짜 경계 함정. 한국시간 새벽 2시 주문은 UTC로는 전날이라 UTC 기준으로 날짜를 자르면 일별 집계가 어긋난다. 저장은 UTC로 하되 날짜 경계는 비즈니스 타임존인 KST 자정으로 환산해 자른다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;UTC로 저장하면 새로운 함정이 하나 생겨요. &lt;b&gt;&quot;날짜&quot;의 경계가 타임존마다 다르다&lt;/b&gt;는 거예요. 한국시간 7/27 새벽 2시의 주문은 UTC로는 아직 7/26이에요. UTC 날짜로 &lt;code&gt;GROUP BY&lt;/code&gt;를 하면 한국 비즈니스 입장의 일별 매출이 매일 새벽 9시간어치만큼 어긋나요. &quot;어제 매출이 시스템마다 달라요&quot;의 단골 원인이죠.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// &quot;KST 7/27 하루&quot;를 UTC 범위로 환산해서 자른다
ZoneId seoul = ZoneId.of(&quot;Asia/Seoul&quot;);
Instant start = LocalDate.of(2026, 7, 27).atStartOfDay(seoul).toInstant();  // = 7/26 15:00Z
Instant end   = start.plus(1, ChronoUnit.DAYS);
// WHERE created_at &amp;gt;= :start AND created_at &amp;lt; :end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙은 분리예요 &amp;mdash; &lt;b&gt;저장은 UTC, &quot;오늘&amp;middot;이달&quot; 같은 경계는 비즈니스 타임존(KST)으로 환산해서 자르기.&lt;/b&gt; 그리고 그 기준 타임존을 코드 여기저기가 아니라 한 곳에 상수로 박아둬요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 자잘하지만 아픈 함정들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오프셋 고정의 함정&lt;/b&gt; &amp;mdash; &lt;code&gt;+09:00&lt;/code&gt;을 하드코딩하는 것과 &lt;code&gt;Asia/Seoul&lt;/code&gt;은 달라요. 지역 ID는 서머타임&amp;middot;정책 변경의 역사를 알지만 고정 오프셋은 몰라요. 한국은 지금 DST가 없지만 글로벌 사용자&amp;middot;해외 거래소 연동이 있으면 차이가 바로 드러나요. 지역은 항상 ZoneId로요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SimpleDateFormat&lt;/b&gt; &amp;mdash; 스레드 세이프하지 않아서 동시 요청에서 가끔 엉뚱한 날짜를 만들어요. &lt;code&gt;DateTimeFormatter&lt;/code&gt;(불변)로요. 레거시의 간헐 버그 단골이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON 직렬화&lt;/b&gt; &amp;mdash; Jackson이 시각을 어떤 형식&amp;middot;타임존으로 내리는지 기본값에 맡기지 말고 명시해요(&lt;code&gt;JavaTimeModule&lt;/code&gt; + ISO-8601). &lt;a href=&quot;https://jessyt.tistory.com/465&quot;&gt;Redis 직렬화 편&lt;/a&gt;에서 본 LocalDateTime 직렬화 예외도 같은 뿌리예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 컬럼 타입&lt;/b&gt; &amp;mdash; MySQL의 TIMESTAMP는 세션 타임존에 따라 변환되고 DATETIME은 안 돼요. 게다가 TIMESTAMP는 저장 범위가 2038-01-19(UTC)까지라는 한계도 있어요. 이 차이를 모르고 섞으면 마이그레이션 때 시각이 밀려요. 팀이 하나를 정해 통일하는 게 중요해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포했더니 시간이 9시간 밀려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 UTC + LocalDateTime 조합이에요. 응급으로는 TZ를 맞추고 근본적으로는 Instant 기반으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일별 집계가 시스템마다 달라요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 경계 기준이 서로 다른 거예요(UTC 자정 vs KST 자정). 집계 기준 타임존을 명시하고 환산해서 잘라요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;드물게 날짜 파싱이 이상한 값을 만들어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleDateFormat 공유를 의심해요. DateTimeFormatter로 교체해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자정 직전&amp;middot;직후에만 버그가 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 항상 경계 처리예요 &amp;mdash; &quot;오늘&quot; 계산, 만료일 비교에서 타임존이 갈리는 지점을 봐요. 테스트를 자정 경계 시각으로 고정해서(Clock 주입) 재현하는 게 빨라요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임존은 환경 설정이 아니라 타입으로 다루는 영역이에요. 사건의 시각은 Instant(UTC)로 저장하고 지역 시각으로의 변환은 화면에 보여주는 맨 끝에서만 해요. &quot;오늘&amp;middot;이달&quot; 같은 날짜 경계는 비즈니스 타임존(KST)으로 환산해서 자르고, LocalDateTime은 타임존이 애초에 없는 개념에만 남겨둬요. 여기에 시각 코드엔 Clock을 주입해두면 자정&amp;middot;경계 테스트까지 손에 들어와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 또 다른 단골 함정이에요 &amp;mdash; 10만 번째 페이지에서 죽는 쿼리, &lt;a href=&quot;https://jessyt.tistory.com/511&quot;&gt;깊은 페이징&lt;/a&gt; 이야기로 이어갈게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html&quot;&gt;Java &amp;mdash; java.time package&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/datetime.html&quot;&gt;MySQL &amp;mdash; DATETIME vs TIMESTAMP&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>Instant</category>
      <category>java</category>
      <category>KST</category>
      <category>LocalDateTime</category>
      <category>UTC</category>
      <category>날짜 처리</category>
      <category>백엔드</category>
      <category>타임존</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/510</guid>
      <comments>https://jessyt.tistory.com/510#entry510comment</comments>
      <pubDate>Fri, 14 Aug 2026 07:30:02 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정</title>
      <link>https://jessyt.tistory.com/509</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYK7Jt/dJMcabkcXCL/A3HL1dUYFsJ93PurNa2w8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYK7Jt/dJMcabkcXCL/A3HL1dUYFsJ93PurNa2w8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYK7Jt/dJMcabkcXCL/A3HL1dUYFsJ93PurNa2w8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYK7Jt%2FdJMcabkcXCL%2FA3HL1dUYFsJ93PurNa2w8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Java] 돈 계산에 double 쓰면 안 되는 이유: BigDecimal 사용법과 함정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;0.1 + 0.2 == 0.3&lt;/code&gt;이 false라는 건 다들 한 번쯤 들었는데, 이게 정산 금액 1원 차이로 돌아오면 더 이상 퀴즈가 아니에요. 돈을 다루는 백엔드에서 부동소수점은 &quot;가끔 틀리는&quot; 게 아니라 &lt;b&gt;구조적으로 10진 소수를 표현 못 하는&lt;/b&gt; 타입이고, 그래서 BigDecimal이 규율이 돼요. 그런데 BigDecimal에도 함정이 세 개 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 double이 왜 돈에 안 되는지(원리), BigDecimal의 3대 규율(생성자&amp;middot;비교&amp;middot;나눗셈), 그리고 &quot;경로 전체&quot;를 지키는 타입 설계(DB&amp;middot;JSON까지) 순서로 짚어볼게요. 함정 시리즈 1편이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. double은 0.1을 저장하지 못해요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu15VH/dJMcab5w2Ij/6ASzn5iYyyHkBCSvVAU52K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu15VH/dJMcab5w2Ij/6ASzn5iYyyHkBCSvVAU52K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu15VH/dJMcab5w2Ij/6ASzn5iYyyHkBCSvVAU52K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu15VH%2FdJMcab5w2Ij%2F6ASzn5iYyyHkBCSvVAU52K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;double 부동소수점 오차. 0.1 더하기 0.2는 0.30000000000000004가 되고 비교가 깨진다. double은 2진수라 10진 소수 0.1이 무한소수가 되어 정확히 저장되지 않는 IEEE 754의 본질적 한계다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;System.out.println(0.1 + 0.2);            // 0.30000000000000004
System.out.println(1.03 - 0.42);          // 0.6100000000000001
System.out.println(10000.0 * 0.07);       // 700.0000000000001 (수수료 7%)
System.out.println(0.1 + 0.2 == 0.3);     // false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그가 아니라 IEEE 754 부동소수점의 본질이에요. double은 값을 2진수로 저장하는데, 10진수 0.1은 2진수로는 &lt;b&gt;무한소수&lt;/b&gt;예요(1/3이 10진수로 0.333...인 것처럼요). 그래서 저장하는 순간부터 이미 근사값이고, 연산할수록 오차가 누적돼요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&quot;오차가 0.00000000000000004인데 무슨 문제야&quot;라고 생각하기 쉬운데, 돈에서는 두 경로로 사고가 돼요. 첫째, &lt;b&gt;반올림 경계&lt;/b&gt; &amp;mdash; 699.9999...원을 반올림하면 700원이지만, 700.0000...1원에 버림 정책이 걸리면 옆 시스템과 1원이 어긋나요. 둘째, &lt;b&gt;대량 집계&lt;/b&gt; &amp;mdash; 건당 티끌이 수백만 건 합산에서 원 단위 차이로 자라요. 정산 대사에서 &quot;1원 안 맞음&quot;으로 발견되는 그 종류예요. 돈은 단 1원도 &quot;대충&quot;이 안 되는 도메인이라, 근사 타입 자체가 실격이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. BigDecimal 규율 ①, 생성은 문자열로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자바의 답은 BigDecimal(10진수를 정확히 표현)인데, 첫 줄부터 함정이 있어요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;new BigDecimal(0.1);
// &amp;rarr; 0.1000000000000000055511151231257827021181583404541015625&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;double 생성자는 &lt;b&gt;이미 오염된 double 값을 그대로&lt;/b&gt; 받아요. 0.1의 근사값이 정밀하게 박제되는 거죠. BigDecimal을 썼는데도 오차가 나는 미스터리가 대개 여기서 시작돼요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;new BigDecimal(&quot;0.1&quot;);          // ✅ 문자열 생성자 &amp;mdash; 정확히 0.1
BigDecimal.valueOf(0.1);        // ✅ 내부적으로 문자열 경유 &amp;mdash; 안전&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/befFq9/dJMcacpRK25/9sCBiAQtF279E2bbO1iMl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/befFq9/dJMcacpRK25/9sCBiAQtF279E2bbO1iMl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/befFq9/dJMcacpRK25/9sCBiAQtF279E2bbO1iMl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbefFq9%2FdJMcacpRK25%2F9sCBiAQtF279E2bbO1iMl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;BigDecimal 3대 함정. double 생성자는 오염된 값을 박제하므로 문자열 생성자를 쓰고, equals는 scale까지 비교해 1.0과 1.00이 다르므로 compareTo로 비교하며, 나눗셈은 무한소수에서 예외가 나므로 scale과 RoundingMode를 명시한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 규율 ②, 비교는 compareTo로&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;new BigDecimal(&quot;1.0&quot;).equals(new BigDecimal(&quot;1.00&quot;));      // false!
new BigDecimal(&quot;1.0&quot;).compareTo(new BigDecimal(&quot;1.00&quot;));   // 0 (같음)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;equals&lt;/code&gt;는 값뿐 아니라 &lt;b&gt;scale(소수점 자릿수)까지&lt;/b&gt; 비교해요. 1.0과 1.00이 다른 객체라는 거죠. &quot;금액이 같은데 if문이 안 탄다&quot;의 단골 원인이고, HashSet&amp;middot;HashMap의 키로 쓸 때도 같은 함정이 있어요. 값 비교는 항상 &lt;code&gt;compareTo() == 0&lt;/code&gt;이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 규율 ③, 나눗셈엔 scale과 반올림을 명시&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;BigDecimal.ONE.divide(new BigDecimal(&quot;3&quot;));
// &amp;rarr; ArithmeticException: Non-terminating decimal expansion

BigDecimal.ONE.divide(new BigDecimal(&quot;3&quot;), 2, RoundingMode.HALF_UP);
// &amp;rarr; 0.33  ✅ 자릿수와 반올림 방식 명시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1/3 같은 무한소수는 &quot;어디서 끊을지&quot;를 모르면 예외예요. 나눗셈(그리고 모든 반올림 지점)에는 scale과 &lt;code&gt;RoundingMode&lt;/code&gt;를 명시해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 게 &amp;mdash; &lt;b&gt;반올림 정책은 기술 선택이 아니라 비즈니스 결정&lt;/b&gt;이에요. 수수료를 올림할지 내림할지, 분배 후 남는 1원을 누가 갖는지는 코드가 아니라 정책이 정해요. 참고로 &lt;code&gt;HALF_UP&lt;/code&gt;(사사오입)이 일상적 반올림이고, &lt;code&gt;HALF_EVEN&lt;/code&gt;(은행가 반올림)은 .5를 짝수 쪽으로 보내 대량 집계의 편향을 없애는 방식이에요. 뭐가 됐든 한 곳(공통 유틸&amp;middot;정책 문서)에 명시하고 전사가 통일하는 게 핵심이에요 &amp;mdash; 시스템마다 반올림이 다르면 그게 바로 대사 불일치예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 한 구간만 double이어도 전체가 오염돼요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vzE7a/dJMcacpRK26/YyDCo9B9oyGGFLmZLdVGk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vzE7a/dJMcacpRK26/YyDCo9B9oyGGFLmZLdVGk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vzE7a/dJMcacpRK26/YyDCo9B9oyGGFLmZLdVGk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvzE7a%2FdJMcacpRK26%2FYyDCo9B9oyGGFLmZLdVGk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;금액 타입 경로 전체 규율. 중간 계산 한 곳만 double이어도 전체가 오염되므로, 자바는 BigDecimal이나 최소 단위 정수, DB는 DECIMAL 컬럼, JSON은 문자열이나 정수로 경로 전체를 통일한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;API는 BigDecimal인데 중간 계산 한 줄이 &lt;code&gt;doubleValue()&lt;/code&gt;를 거치면, 거기서 오염되고 끝까지 전파돼요. 경로 &lt;b&gt;전체&lt;/b&gt;의 타입 규율이 필요해요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자바&lt;/b&gt; &amp;mdash; BigDecimal로 통일. 대안으로 &lt;b&gt;최소 단위 정수&lt;/b&gt;(원 단위면 long, 소수점 거래면 &quot;전&quot;&amp;middot;&quot;마이크로원&quot; 단위 정수)도 강력해요 &amp;mdash; 정수 연산은 오차 자체가 없으니까요. 오차는 없지만 범위는 별개라, 곱셈 중간값이 커지는 계산은 오버플로를 따로 챙겨야 해요. 암호화폐&amp;middot;외환처럼 소수점 깊은 도메인에서 흔한 선택이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB&lt;/b&gt; &amp;mdash; &lt;code&gt;DECIMAL(19, 4)&lt;/code&gt; 같은 고정소수점 컬럼. FLOAT&amp;middot;DOUBLE 컬럼은 금액에 금지예요. JPA는 BigDecimal &amp;harr; DECIMAL이 자연 매핑돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON&lt;/b&gt; &amp;mdash; 함정이 하나 더 있어요. &lt;b&gt;자바스크립트의 Number도 double이에요.&lt;/b&gt; 금액을 JSON 숫자로 내리면 프론트에서 같은 오차가 나요(큰 ID가 깨지는 것과 같은 원리). 금액은 문자열(&quot;12345.67&quot;)이나 최소 단위 정수로 내리는 게 안전해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BigDecimal을 쓰는데도 값이 미세하게 어긋나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어딘가에서 double 생성자나 &lt;code&gt;doubleValue()&lt;/code&gt; 경유를 의심해요. 경로 전체를 추적하면 한 줄이 나와요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;금액 비교 if문이 가끔 안 타요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;equals&lt;/code&gt;의 scale 비교예요. &lt;code&gt;compareTo() == 0&lt;/code&gt;으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;옆 시스템과 정산이 1원 안 맞아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반올림 정책(방식&amp;middot;적용 시점&amp;middot;자릿수)이 서로 다른 거예요. 어느 단계에서 몇 자리로 어떻게 반올림하는지를 양쪽이 문서로 맞춰요. 건별 반올림 vs 합산 후 반올림의 차이도 단골 원인이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프론트에서 금액이 이상하게 보여요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 숫자로 내린 금액이 JS double에서 깨진 거예요. 문자열로 내려요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돈 계산은 결국 다섯 가지 습관으로 압축돼요. 한 곳의 구멍이 경로 전체를 오염시키는 종류라, 컨벤션&amp;middot;공통 유틸로 박아두는 게 답이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;double은 쓰지 않아요&lt;/b&gt; &amp;mdash; 2진수는 10진 소수를 못 담아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BigDecimal 생성은 문자열로&lt;/b&gt; &amp;mdash; double 생성자는 오염값을 박제해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;값 비교는 compareTo&lt;/b&gt; &amp;mdash; equals는 scale까지 봐요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나눗셈&amp;middot;반올림엔 scale과 RoundingMode 명시&lt;/b&gt; &amp;mdash; 반올림 정책은 비즈니스 결정이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB DECIMAL&amp;middot;JSON 문자열까지 경로 전체 통일&lt;/b&gt; &amp;mdash; 한 구간만 double이어도 끝까지 번져요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어지는 함정은 자정에 터지는 버그예요. &lt;a href=&quot;https://jessyt.tistory.com/510&quot;&gt;타임존과 날짜 처리&lt;/a&gt;로 넘어가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/math/BigDecimal.html&quot;&gt;Java &amp;mdash; BigDecimal&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html&quot;&gt;What Every Computer Scientist Should Know About Floating-Point Arithmetic&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>BigDecimal</category>
      <category>Double</category>
      <category>java</category>
      <category>RoundingMode</category>
      <category>금액 계산</category>
      <category>백엔드</category>
      <category>부동소수점</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/509</guid>
      <comments>https://jessyt.tistory.com/509#entry509comment</comments>
      <pubDate>Thu, 13 Aug 2026 07:30:18 +0900</pubDate>
    </item>
    <item>
      <title>[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산</title>
      <link>https://jessyt.tistory.com/508</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhtr9h/dJMb997HQNp/7GtyXvKKyAKSKRmKKG6kvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhtr9h/dJMb997HQNp/7GtyXvKKyAKSKRmKKG6kvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhtr9h/dJMb997HQNp/7GtyXvKKyAKSKRmKKG6kvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhtr9h%2FdJMb997HQNp%2F7GtyXvKKyAKSKRmKKG6kvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도는 분산 시스템의 기본 회복 수단인데, 잘못 쓰면 &lt;b&gt;장애를 키우는 증폭기&lt;/b&gt;가 돼요. 과부하로 비틀거리는 서비스에 모든 호출자가 일제히 3배의 재시도 트래픽을 퍼붓는 &amp;mdash; 재시도 폭풍이에요. 그 폭풍을 막는 표준 처방(지수 백오프+지터)과 짝꿍 개념인 타임아웃 예산을 짚으며 분산 패턴 시리즈를 마무리해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 재시도 폭풍, 선의가 DDoS가 된다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqfjeR/dJMb997HQNn/MiMzUA0k8RtZxT52ufkgX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqfjeR/dJMb997HQNn/MiMzUA0k8RtZxT52ufkgX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqfjeR/dJMb997HQNn/MiMzUA0k8RtZxT52ufkgX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqfjeR%2FdJMb997HQNn%2FMiMzUA0k8RtZxT52ufkgX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;재시도 폭풍 구조. 과부하된 서비스에 모든 호출자가 실패 후 일제히 재시도하면 트래픽이 몇 배로 증폭되어 회복하려던 상대가 더 깊이 무너진다. 고정 간격 재시도는 같은 박자의 부하 파도까지 만든다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;상대가 과부하로 실패하기 시작하면, 호출자들은 &quot;일시 장애겠지&quot; 하고 재시도해요. 전원이 3회씩 재시도하면 트래픽이 순간 3~4배가 돼요. 평소 부하도 못 버티던 상대는 더 깊이 무너지고 회복이 불가능해져요. 선의의 재시도가 사실상 DDoS가 되는 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 간격 재시도는 한 술 더 떠요 &amp;mdash; 같은 순간 실패한 호출자들이 정확히 1초 후 &lt;b&gt;일제히&lt;/b&gt; 다시 때리고 또 일제히... 부하가 파도처럼 박자를 맞춰 몰려요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 처방, 백오프 + 지터 + 한도&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GXFZ4/dJMb997HQNo/fjKnGjNLmlKZ8woz9dOHQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GXFZ4/dJMb997HQNo/fjKnGjNLmlKZ8woz9dOHQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GXFZ4/dJMb997HQNo/fjKnGjNLmlKZ8woz9dOHQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGXFZ4%2FdJMb997HQNo%2FfjKnGjNLmlKZ8woz9dOHQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;재시도 표준 처방. 지수 백오프로 실패할수록 간격을 늘려 상대에게 회복 시간을 주고, 지터로 호출자마다 간격을 무작위로 흩어 파도를 분산하며, 횟수는 2-3회로 제한하고 그 이상은 서킷 브레이커가 이어받는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지수 백오프&lt;/b&gt; &amp;mdash; 1초 &amp;rarr; 2초 &amp;rarr; 4초로 점점 물러나요. 일시 장애는 첫 재시도에 잡히고 진짜 장애엔 부하를 빠르게 줄여요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지터(jitter)&lt;/b&gt; &amp;mdash; 간격에 무작위를 섞어요(예: 0~백오프값 사이 랜덤). 호출자들의 박자를 흩어 파도를 평탄하게 만들어요. &lt;a href=&quot;https://jessyt.tistory.com/470&quot;&gt;캐시 TTL jitter&lt;/a&gt;와 정확히 같은 원리예요 &amp;mdash; &quot;동시에 몰리는 것&quot;이 적이면 무작위가 약이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;횟수 제한&lt;/b&gt; &amp;mdash; 2~3회면 충분해요. 그걸로 안 되면 일시 장애가 아니라는 뜻이고 그때부턴 &lt;a href=&quot;https://jessyt.tistory.com/507&quot;&gt;서킷 브레이커&lt;/a&gt;가 이어받아 아예 차단하는 게 맞아요. 재시도(짧은 장애용)와 서킷(긴 장애용)은 역할 분담이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// Resilience4j Retry &amp;mdash; 백오프 + 지터
RetryConfig config = RetryConfig.custom()
    .maxAttempts(3)   // 최초 호출 포함 3회 = 재시도는 2회
    .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
        Duration.ofMillis(500), 2.0, 0.5))   // 0.5s부터 2배씩, &amp;plusmn;50% 지터
    .retryExceptions(IOException.class, TimeoutException.class)
    .ignoreExceptions(BusinessException.class)   // 비즈니스 실패는 재시도 무의미
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;가장 중요한 전제 &amp;mdash; &lt;b&gt;재시도는 멱등한 작업에만.&lt;/b&gt; &quot;타임아웃 = 실패&quot;가 아니라 &quot;결과 모름&quot;이라는 걸 기억하세요(&lt;a href=&quot;https://jessyt.tistory.com/504&quot;&gt;멱등 API 편&lt;/a&gt;의 그 비대칭). 처리됐는데 응답만 유실된 호출을 재시도하면 중복 실행이에요. 결제 호출의 재시도는 Idempotency-Key가 깔려 있을 때만 안전하고 그게 없다면 재시도 대신 &quot;상태 조회 후 판단&quot;이 맞아요. 그리고 비즈니스 실패(잔액 부족)는 백 번 재시도해도 똑같으니 재시도 대상에서 빼요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/458&quot;&gt;카프카 에러 분류&lt;/a&gt;의 일시적/영구적 구분이 여기서도 그대로예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 타임아웃 예산, 바깥보다 안쪽이 짧아야&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도와 한 몸인 게 타임아웃이에요. 타임아웃 없는 재시도는 &quot;언제 실패할지&quot;가 없어 성립하지 않고 타임아웃 값들이 서로 안 맞으면 또 다른 사고가 나요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA220f/dJMcaccifUh/66yzngKr0n8qnKdSofR24k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA220f/dJMcaccifUh/66yzngKr0n8qnKdSofR24k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA220f/dJMcaccifUh/66yzngKr0n8qnKdSofR24k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA220f%2FdJMcaccifUh%2F66yzngKr0n8qnKdSofR24k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;타임아웃 예산 개념. 클라이언트나 게이트웨이의 전체 타임아웃 안에 내부 호출들과 재시도 여유가 들어와야 한다. 안쪽 타임아웃이 바깥보다 길면 클라이언트가 떠난 뒤에도 서버가 일하고 중복 처리가 생긴다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;원칙은 &lt;b&gt;예산 배분&lt;/b&gt;이에요. 게이트웨이가 3초에 끊는다면, 그 안의 내부 호출들 + 재시도 시간의 합이 3초 안에 들어와야 해요. 흔한 사고는 반대예요 &amp;mdash; 안쪽 외부 API 타임아웃이 10초인데 바깥이 3초면, 클라이언트는 3초에 떠나서(어쩌면 벌써 재시도해서) 응답 받을 사람이 없는데 서버는 7초를 더 일해요. 자원 낭비에 중복 처리까지 겹치죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 규칙으로 정리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모든 외부 호출에 명시적 타임아웃&lt;/b&gt; &amp;mdash; 기본값(무한대거나 너무 긴)을 믿지 않아요. HTTP 클라이언트의 connect/read 타임아웃, &lt;a href=&quot;https://jessyt.tistory.com/465&quot;&gt;Redis&lt;/a&gt;, DB까지 전부요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안쪽 합(재시도 포함) &amp;lt; 바깥&lt;/b&gt; &amp;mdash; 재시도 1회를 계획한 호출이라면 (타임아웃 &amp;times; 2 + 백오프)가 예산 안에 들어와야 해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타임아웃은 p99 기준으로&lt;/b&gt; &amp;mdash; 평균이 아니라 정상 응답의 99퍼센타일보다 약간 크게. 너무 짧으면 멀쩡한 요청을 죽이고(자체 오탐), 너무 길면 장애 때 자원이 묶여요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 어디서 재시도할 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 설계 질문 &amp;mdash; 재시도를 &lt;b&gt;어느 층에서&lt;/b&gt; 하느냐예요. 클라이언트도, 게이트웨이도, 내 서비스도, 메시지큐 컨슈머도 재시도를 하면 곱셈이 일어나요. 3회 &amp;times; 3회 &amp;times; 3회 = 27배 증폭이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙은 &quot;재시도는 한 층에서만&quot;이에요. 보통 &lt;b&gt;실패의 의미를 가장 잘 아는 층&lt;/b&gt;이 맡아요 &amp;mdash; 외부 API 호출이면 그걸 부르는 서비스가, 메시지 처리면 &lt;a href=&quot;https://jessyt.tistory.com/458&quot;&gt;컨슈머의 재시도+DLQ&lt;/a&gt;가요. 나머지 층은 재시도 없이 실패를 그대로 전달하고 최종 사용자 층에서만 &quot;다시 시도&quot; 버튼으로 사람의 판단에 맡겨요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애가 났는데 트래픽이 오히려 몇 배로 뛰어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도 폭풍이에요. 백오프+지터를 깔고 층층이 재시도가 중첩돼 있지 않은지(곱셈) 점검해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시도 때문에 중복 처리가 생겨요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비멱등 작업의 재시도예요. Idempotency-Key(&lt;a href=&quot;https://jessyt.tistory.com/504&quot;&gt;1편&lt;/a&gt;)를 깔거나 재시도 대신 조회-후-판단으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임아웃을 줄였더니 멀쩡한 요청이 실패해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p99보다 짧게 잡은 거예요. 실측 분포를 보고 정하고 느린 의존성 자체의 개선과 병행해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 &amp;mdash; 그리고 시리즈를 닫으며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도의 공식은 &lt;b&gt;멱등 전제 + 지수 백오프 + 지터 + 2~3회 한도 + (그 너머는 서킷)&lt;/b&gt;이고 타임아웃은 &lt;b&gt;전 구간 명시 + 안쪽이 바깥보다 짧게&lt;/b&gt;예요. 그리고 재시도는 한 층에서만 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 분산 패턴 시리즈가 마무리돼요. &lt;a href=&quot;https://jessyt.tistory.com/504&quot;&gt;멱등 API&lt;/a&gt;(중복을 무해하게), &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스&lt;/a&gt;(전달을 확실하게), &lt;a href=&quot;https://jessyt.tistory.com/506&quot;&gt;사가&lt;/a&gt;(흐름의 정합성), &lt;a href=&quot;https://jessyt.tistory.com/507&quot;&gt;서킷 브레이커&lt;/a&gt;(장애 격리), 그리고 타임아웃과 재시도(회복의 규율) &amp;mdash; 다섯 조각이 서로를 전제하며 맞물려요. 그 토대는 결국 &lt;a href=&quot;https://jessyt.tistory.com/455&quot;&gt;카프카&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/465&quot;&gt;Redis&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/481&quot;&gt;JPA&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/490&quot;&gt;MySQL&lt;/a&gt; 시리즈에서 다진 한 가지 감각, &lt;b&gt;분산된 것들 사이의 틈을 의심하고 그 틈을 설계로 메우는 일&lt;/b&gt;이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글부터는 이런 큰 그림이 아니라 코드 한 줄짜리 함정들이에요. 돈 계산에 double을 쓰면 안 되는 이유부터 시작해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/&quot;&gt;AWS Builders' Library &amp;mdash; Timeouts, retries, and backoff with jitter&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://resilience4j.readme.io/docs/retry&quot;&gt;Resilience4j &amp;mdash; Retry&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/분산 &amp;amp; 운영</category>
      <category>retry storm</category>
      <category>백엔드</category>
      <category>분산시스템</category>
      <category>재시도</category>
      <category>지수 백오프</category>
      <category>지터</category>
      <category>타임아웃</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/508</guid>
      <comments>https://jessyt.tistory.com/508#entry508comment</comments>
      <pubDate>Wed, 12 Aug 2026 07:30:09 +0900</pubDate>
    </item>
    <item>
      <title>[Claude] Sonnet 5 가격 인상 취소, $2/$10 그대로예요</title>
      <link>https://jessyt.tistory.com/599</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8xyB6/dJMcacqF1ms/rVN1YGwffTN3r9P1opNGTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8xyB6/dJMcacqF1ms/rVN1YGwffTN3r9P1opNGTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8xyB6/dJMcacqF1ms/rVN1YGwffTN3r9P1opNGTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8xyB6%2FdJMcacqF1ms%2FrVN1YGwffTN3r9P1opNGTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Claude] Sonnet 5 가격 인상 취소, $2/$10 그대로예요&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Claude] Sonnet 5 가격 인상 취소, $2/$10 그대로예요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Sonnet 5의 API 가격이 지금 그대로 굳었어요. Anthropic이 8월 10일 플랫폼 릴리스 노트에서 100만 토큰당 입력 $2, 출력 $10을 표준가로 확정했어요. 9월 1일부터 $3/$15로 올리기로 예고돼 있던 50% 인상은 하지 않는다고 해요. Sonnet 5로 뭔가 돌리고 있다면 9월 청구서 걱정은 덜어도 되는 발표예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 9월 1일 50% 인상이 없던 일이 됐어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 $2/$10은 8월 31일까지만 적용되는 도입가였어요. 9월 1일부터는 100만 토큰당 입력 $3, 출력 $15가 표준가로 붙을 예정이었고, 입력&amp;middot;출력 양쪽 다 50% 인상이었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에는 &quot;the previously scheduled increase to $3/$15 per million input/output tokens on September 1, 2026 will not occur&quot;로 적혀 있어요. 인상분을 미리 반영해 9월 예산을 잡아뒀다면 그 부분을 다시 계산해야 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 직전 세대 Sonnet 4.6보다 싼 가격이 유지돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격표를 보면 Sonnet 4.6이 $3/$15, Sonnet 5가 $2/$10이에요. 신모델이 구모델보다 단가가 낮은 상태가 그대로 굳은 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가 요금도 같은 비율로 내려가요. Batch API는 절반인 $1/$5, 캐시 히트는 입력가의 10%인 $0.20이에요. 프롬프트 캐시 쓰기는 5분 $2.50, 1시간 $4예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 다만 같은 글이 토큰은 더 많이 나와요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단가만 보고 33% 절감으로 계산하면 어긋날 수 있어요. Anthropic 문서 기준으로 Claude 4.7 이후 모델은 새 tokenizer를 쓰고, 4.6 이하 모델과 비교하면 같은 텍스트가 대략 30% 더 많은 토큰으로 쪼개진다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context window 설명에서도 차이가 보여요. 같은 1M 토큰인데 Sonnet 4.6은 약 750k 단어 분량, Sonnet 5는 약 555k 단어 분량으로 표기돼 있어요. 같은 문서를 넣어도 Sonnet 5 쪽이 토큰을 더 먹는다는 뜻이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 숫자를 그대로 곱해보면 감이 와요. 단가가 4.6의 3분의 2로 내려가고 토큰 수가 1.3배로 늘면, 같은 작업의 비용은 약 87% 수준이에요. 33% 절감이 아니라 10%대 절감에 가까운 계산이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 토큰 증가율은 콘텐츠에 따라 달라진다고 문서에 적혀 있어서, 위 계산은 어디까지나 어림이에요. 같은 프롬프트로 두 모델의 usage 값을 찍어보고 판단할 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. API 요금 얘기라 구독 요금제와는 별개예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 공지는 Claude 플랫폼 릴리스 노트에 실린 API 토큰 과금 얘기예요. Pro&amp;middot;Max 같은 구독 요금제나 claude.ai 사용료와는 별개 항목이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1M context는 별도 장문 할증 없이 표준가로 계산돼요. 문서에는 90만 토큰 요청도 9천 토큰 요청과 같은 단가로 청구된다고 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 기준으로 데이터 위치를 미국으로 고정하는 inference_geo 옵션을 쓰면 모든 토큰 항목에 1.1배가 붙어요. 기본값인 global 라우팅은 표준가라, 특별히 지정하지 않았다면 위 가격 그대로예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://platform.claude.com/docs/en/release-notes/api&quot;&gt;Claude Platform 릴리스 노트 (2026-08-10)&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/about-claude/pricing&quot;&gt;Claude Pricing 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>anthropic</category>
      <category>API비용</category>
      <category>claude</category>
      <category>claudeapi</category>
      <category>LLM</category>
      <category>Sonnet5</category>
      <category>가격정책</category>
      <category>토큰</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/599</guid>
      <comments>https://jessyt.tistory.com/599#entry599comment</comments>
      <pubDate>Wed, 12 Aug 2026 07:00:11 +0900</pubDate>
    </item>
    <item>
      <title>[Sublime Text] 멀티 커서와 프로젝트 전환</title>
      <link>https://jessyt.tistory.com/600</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/viTO8/dJMcabL6NG3/AAAAAAAAAAAAAAAAAAAAAK9BbrC3NpPiMs27f4XFSQ8v_w6kO7W9CmkGH5qGDjMY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=mDsRc79%2FMYeldtxFpkiivnCZk3M%3D&quot; alt=&quot;[Sublime Text] 멀티 커서와 프로젝트 전환&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;div class=&quot;yt-hero__tag&quot;&gt;개발자 도구 &amp;gt; 에디터 &amp;amp; IDE&lt;/div&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Sublime Text] 멀티 커서와 프로젝트 전환&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;Sublime Text가 대체하는 자리는 무거운 IDE라기보다 sed&amp;middot;정규식 일괄 치환과 &quot;잡파일 편집기&quot; 쪽에 가까워요. 커서를 여러 개 세워 눈으로 확인하며 고치는 방식, 그리고 폴더 묶음을 JSON 파일 하나로 저장해 프로젝트를 갈아 끼우는 방식이 그 축입니다. 이 글은 공식 문서에 적힌 단축키&amp;middot;설정 파일 형식만 근거로 멀티 셀렉션 계열 명령과 프로젝트 파일 구성을 정리했어요.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 커서는 정규식 치환을 눈으로 대체해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 이름 12군데를 한 번에 바꾸는 작업을 떠올려 보면 방법은 대략 세 가지예요. 하나씩 손으로 고치거나, 찾아 바꾸기 정규식을 쓰거나, 커서를 12개 세워 동시에 타이핑하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식 치환의 약점은 결과를 미리 못 본다는 점이에요. 패턴이 의도보다 넓게 잡히면 엉뚱한 줄까지 바뀝니다. 그 사실은 대개 저장한 뒤에 드러나요. 멀티 커서는 선택된 위치가 화면에 전부 표시된 상태에서 편집이 시작돼요. 하나가 잘못 잡혔으면 그 자리에서 건너뛰거나 빼면 됩니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;선택 확인&lt;/th&gt;
&lt;th&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;손으로 하나씩&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;2~3군데&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정규식 찾아 바꾸기&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;td&gt;수백 군데, 패턴이 명확할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티 커서&lt;/td&gt;
&lt;td&gt;화면에서 전부 보임&lt;/td&gt;
&lt;td&gt;한 화면&amp;middot;한 파일 안의 수십 군데&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 커서 자체는 이제 다른 에디터에도 대부분 들어가 있어요. 다만 Sublime Text 쪽 명령 계열은 문서에 그대로 정리돼 있어요. 하나를 익히면 나머지가 같은 뿌리에서 갈라지는 구조라 처음 배우기엔 수월한 편입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ctrl+D 하나에서 나머지 단축키가 갈라져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 「Multiple Selection with the Keyboard」에 실린 명령들이에요. 아래 표기는 Windows/Linux 기준이며 macOS는 Ctrl 자리에 Command(⌘)가 들어갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quick Add Next &amp;mdash; Ctrl+D&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서를 단어 위에 두고 누르면 그 단어가 선택되고, 한 번 더 누르면 다음에 나오는 같은 단어가 선택에 추가돼요. 계속 누르는 만큼 커서가 늘어납니다. macOS는 ⌘+D입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Quick Skip Next &amp;mdash; Ctrl+K, Ctrl+D&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ctrl+D를 누르다 보면 바꾸면 안 되는 위치가 걸릴 때가 있어요. 그때 Ctrl+K를 누른 뒤 Ctrl+D를 누르면 현재 위치를 선택에서 빼고 그다음 것으로 넘어갑니다. macOS는 ⌘+K, ⌘+D예요. 이 명령을 모르면 잘못 걸린 순간 처음부터 다시 잡아야 해서, 사실상 Ctrl+D와 한 쌍으로 쓰이는 명령입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Find All &amp;mdash; Alt+F3&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 늘리지 않고 파일 안의 같은 단어를 전부 한 번에 선택해요. macOS는 ⌃+⌘+G입니다. 개수를 확인할 필요 없이 전부 바꾼다고 확신할 때 쓰는 명령이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Split into Lines &amp;mdash; Ctrl+Shift+L&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 줄을 블록으로 잡은 상태에서 누르면 선택 영역이 줄 단위로 쪼개지고, 각 줄 끝에 커서가 하나씩 생겨요. macOS는 ⇧+⌘+L입니다. 로그를 붙여넣고 줄마다 접두어를 붙이거나, 목록을 배열 리터럴로 바꾸는 편집이 여기서 나옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;줄 위아래로 커서 추가, 그리고 되돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단어와 무관하게 위아래 줄로 커서를 넓히려면 Windows는 Ctrl+Alt+&amp;uarr;/&amp;darr;, Linux는 Alt+Shift+&amp;uarr;/&amp;darr;, macOS는 ⌃+⇧+&amp;uarr;/&amp;darr;를 씁니다. 커서를 너무 많이 늘렸다면 Ctrl+U(macOS ⌘+U)로 선택을 한 단계씩 되돌릴 수 있어요. 전부 정리하고 커서 하나로 돌아가려면 Esc입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컬럼 선택은 모드가 아니라 마우스 조합이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세로로 네모난 블록을 잡는 이른바 컬럼 선택도 별도 모드가 아니라 멀티 셀렉션의 한 형태로 구현돼 있어요. 표 형태 텍스트에서 특정 열만 잡아 지우거나 접두어를 붙일 때 쓰입니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;th&gt;Windows / Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;블록 잡기&lt;/td&gt;
&lt;td&gt;Option + 좌클릭 드래그 (또는 가운데 버튼)&lt;/td&gt;
&lt;td&gt;Shift + 우클릭 드래그 (또는 가운데 버튼)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택 추가&lt;/td&gt;
&lt;td&gt;Command&lt;/td&gt;
&lt;td&gt;Ctrl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택 빼기&lt;/td&gt;
&lt;td&gt;Shift + Command&lt;/td&gt;
&lt;td&gt;Alt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드만으로 블록을 위아래로 늘리는 조합은 앞 절의 줄 추가 단축키와 같아요. macOS는 ⌃+⇧+&amp;uarr;/&amp;darr;, Windows는 Ctrl+Alt+&amp;uarr;/&amp;darr;, Linux는 Alt+Shift+&amp;uarr;/&amp;darr;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;탭을 여러 개 고르면 창이 알아서 갈라져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sublime Text 4.0에서 들어온 Tab Multi-Select는 같은 발상을 파일 탭에 적용한 기능이에요. 문서 설명대로 여러 탭을 동시에 선택하면 에디터 창이 자동으로 나뉘어 선택된 탭이 나란히 표시됩니다. 레이아웃을 먼저 2열로 만들고 파일을 하나씩 끌어다 놓는 순서가 필요 없다는 뜻이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ctrl/Cmd+J를 누른 뒤 Shift+&amp;larr;/&amp;rarr; &amp;mdash; 옆 탭을 선택에 추가&lt;/li&gt;
&lt;li&gt;Ctrl/Cmd+J를 누른 뒤 &amp;uarr;/&amp;larr;/&amp;rarr; &amp;mdash; 선택에서 빼기&lt;/li&gt;
&lt;li&gt;Ctrl/Cmd + 클릭 &amp;mdash; 탭을 선택에 추가하거나 뺌&lt;/li&gt;
&lt;li&gt;Alt + 클릭 &amp;mdash; 포커스된 탭만 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드바에서도 같은 조합이 동작해요. 사이드바에서 파일 두 개를 Ctrl/Cmd+클릭으로 고르면 그대로 좌우 비교 화면이 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트는 폴더 묶음을 JSON 파일 하나로 저장해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sublime Text의 프로젝트는 파일 두 개로 이뤄져요. 설정을 담은 .sublime-project와, 열어둔 파일 같은 개인 상태를 담은 .sublime-workspace입니다. 문서는 보통 프로젝트 파일만 버전 관리에 넣는다고 안내하고 있어요. workspace 쪽은 사람마다 달라지는 값이라 저장소에 올리면 충돌만 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 파일은 folders, settings, build_systems 세 덩어리로 구성돼요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;{
  &quot;folders&quot;: [
    {
      &quot;path&quot;: &quot;backend&quot;,
      &quot;name&quot;: &quot;API 서버&quot;,
      &quot;folder_exclude_patterns&quot;: [&quot;build&quot;, &quot;node_modules&quot;, &quot;.gradle&quot;],
      &quot;binary_file_patterns&quot;: [&quot;*.png&quot;, &quot;*.jar&quot;],
      &quot;index_exclude_patterns&quot;: [&quot;*.min.js&quot;]
    },
    {
      &quot;path&quot;: &quot;../shared-schema&quot;
    }
  ],
  &quot;settings&quot;: {
    &quot;tab_size&quot;: 4,
    &quot;translate_tabs_to_spaces&quot;: true
  },
  &quot;build_systems&quot;: [
    {
      &quot;name&quot;: &quot;Run tests&quot;,
      &quot;cmd&quot;: [&quot;make&quot;, &quot;test&quot;]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 실제로 체감이 갈리는 항목은 패턴 쪽이에요. folder_exclude_patterns에 걸린 폴더는 사이드바에서 사라집니다. binary_file_patterns에 걸린 파일은 검색과 탐색에서 빠져요. index_exclude_patterns는 심볼 인덱싱 대상에서 제외해요. 빌드 산출물이나 압축된 번들 파일이 검색 결과를 덮는 상황을 여기서 잘라내는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path는 상대 경로와 절대 경로를 모두 받아요. 위 예시처럼 상위 폴더의 다른 저장소를 같은 창에 끌어다 붙일 수 있어요. name으로 사이드바 표시 이름을 따로 줄 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;알아둘 제약&lt;/span&gt; settings 항목이 사용자 설정을 전부 덮어쓰지는 않아요. 문서는 프로젝트 단위로 제어할 수 있는 범위를 Editor Settings 계열로 한정하고 있습니다. 테마나 UI 관련 설정을 프로젝트 파일에 적어두고 반영되기를 기대하면 어긋나요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 전환은 Quick Switch와 subl 두 갈래예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소를 여러 개 오가는 경우 전환 방법은 두 가지입니다. 하나는 에디터 안에서 Project 메뉴의 Quick Switch Project를 여는 방식이에요. 최근 연 프로젝트 목록이 뜨고 이름 일부만 입력해 골라 들어갑니다. Windows/Linux 기본 단축키는 Ctrl+Alt+P입니다. 이 항목에 연결된 명령 이름은 prompt_select_project예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축키가 데스크톱 환경이나 다른 프로그램과 겹쳐 안 먹는 사례가 포럼에 여러 건 올라와 있어요. 그럴 때는 사용자 키맵 파일(Packages/User 안의 Default.sublime-keymap)에 원하는 조합을 직접 박으면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;[
  {
    &quot;keys&quot;: [&quot;primary+alt+o&quot;],
    &quot;command&quot;: &quot;prompt_select_project&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키맵 문법의 primary는 Windows/Linux에서 Ctrl, macOS에서 ⌘로 해석되는 예약어라 한 파일로 양쪽을 덮을 때 편해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 터미널에서 subl 명령으로 여는 방식입니다. 명령줄 도구는 macOS 기준 /Applications/Sublime Text.app/Contents/SharedSupport/bin 을 PATH에 넣으면 잡혀요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# PATH 등록 (zsh)
echo 'export PATH=&quot;/Applications/Sublime Text.app/Contents/SharedSupport/bin:$PATH&quot;' &amp;gt;&amp;gt; ~/.zprofile

# 프로젝트 파일 열기
subl --project ~/work/api.sublime-project

# 새 창으로 열기
subl -n ~/work/api.sublime-project

# 현재 창에 폴더 추가
subl -a ~/work/shared-schema

# 파일의 특정 줄&amp;middot;열로 바로 이동
subl app/main.py:120:8

# git 커밋 편집기로 지정 (창을 닫을 때까지 대기)
export EDITOR='subl -w'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--project와 -n을 셸 alias로 묶어두면 저장소별 진입점을 명령 한 줄로 만들 수 있어요. Linux는 패키지 매니저로 설치하면 /usr/bin/subl 심볼릭 링크가 자동으로 생겨요. tarball 설치는 링크를 직접 걸어야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 알아두면 헛돌지 않는 것들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;.sublime-workspace는 저장소에 올리지 말 것.&lt;/b&gt; 열어둔 파일&amp;middot;창 상태 같은 개인 값이 들어가 커밋마다 바뀌어요. .gitignore에 넣는 게 문서가 안내하는 사용 방식입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트 settings는 만능이 아니에요.&lt;/b&gt; Editor Settings 범위 밖의 항목은 프로젝트 파일에 적어도 적용되지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ctrl+Alt+P는 충돌 사례가 잦아요.&lt;/b&gt; 안 먹으면 원인부터 찾기보다 사용자 키맵에서 다른 조합으로 바꾸는 편이 빠릅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컬럼 선택 마우스 조합이 OS마다 달라요.&lt;/b&gt; macOS는 Option, Windows/Linux는 Shift+우클릭이라 두 환경을 오가면 손이 자꾸 헛나갑니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ctrl+D를 남발하면 되돌리기가 어려워요.&lt;/b&gt; Ctrl+U(선택 되돌리기)와 Ctrl+K, Ctrl+D(건너뛰기)를 같이 익혀야 커서를 잘못 늘렸을 때 복구됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유료 도구라는 점.&lt;/b&gt; 무료로 내려받아 평가할 수 있고 시간 제한이 강제되지는 않지만, 계속 쓰려면 라이선스를 사야 한다고 공식 다운로드 페이지가 명시하고 있어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이 도구가 맞는 쪽은 프로젝트를 통째로 이해해 주는 IDE를 원하는 경우라기보다, 여러 저장소와 로그&amp;middot;설정 파일을 하루에도 몇 번씩 열었다 닫는 쪽이에요. 큰 텍스트 파일을 부담 없이 열어두고 폴더 묶음은 JSON 하나로 고정해둡니다. 반복 편집은 커서 여러 개로 처리하는 흐름이 축이에요. 언어 서버나 디버거를 붙여 본격적인 개발 환경으로 만드는 방향도 가능하지만, 그건 패키지 구성이 따로 필요한 다른 이야기예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작한다면 먼저 Ctrl+D와 Ctrl+K&amp;middot;Ctrl+D 한 쌍을 손에 익힙니다. 그다음 자주 여는 저장소 하나를 Project 메뉴의 Save Project As로 .sublime-project 파일에 저장한 뒤 folder_exclude_patterns에 빌드 산출물 폴더를 넣어보는 정도면 충분해요. 현재 안정 버전은 2025년 5월 21일 배포된 Build 4200입니다. macOS&amp;middot;Windows&amp;middot;Linux를 모두 지원해요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;div class=&quot;yt-sources__head&quot;&gt;출처&lt;/div&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/docs/multiple_selection_with_the_keyboard.html&quot;&gt;Sublime Text Docs &amp;mdash; Multiple Selection with the Keyboard&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/docs/column_selection.html&quot;&gt;Sublime Text Docs &amp;mdash; Column Selection&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/docs/tab_multi-select.html&quot;&gt;Sublime Text Docs &amp;mdash; Tab Multi-Select&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/docs/projects.html&quot;&gt;Sublime Text Docs &amp;mdash; Projects&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/docs/command_line.html&quot;&gt;Sublime Text Docs &amp;mdash; Command Line Interface&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.sublimetext.com/download&quot;&gt;Sublime Text &amp;mdash; Download&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;직접 사용기가 아니라 공식 문서와 다운로드 페이지에 적힌 내용을 확인해 정리한 객관 정보 글입니다. 단축키는 배포판&amp;middot;데스크톱 환경&amp;middot;설치된 패키지에 따라 다르게 잡힐 수 있어요.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/에디터 &amp;amp; IDE</category>
      <category>SublimeText</category>
      <category>개발자도구</category>
      <category>멀티커서</category>
      <category>에디터</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/600</guid>
      <comments>https://jessyt.tistory.com/600#entry600comment</comments>
      <pubDate>Wed, 12 Aug 2026 06:00:29 +0900</pubDate>
    </item>
    <item>
      <title>[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단</title>
      <link>https://jessyt.tistory.com/507</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKGqaS/dJMcadIW2rK/bV7w7hoOhe2H39Oz9cs2i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKGqaS/dJMcadIW2rK/bV7w7hoOhe2H39Oz9cs2i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKGqaS/dJMcadIW2rK/bV7w7hoOhe2H39Oz9cs2i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKGqaS%2FdJMcadIW2rK%2FbV7w7hoOhe2H39Oz9cs2i1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3WGBo%2FdJMcabEr6Mw%2F9TBG6WsJkyBTVda4GAaUGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;578&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eH75zn/dJMcahkqWix/PL7ZnMorkNV8cHCyUHgac0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eH75zn/dJMcahkqWix/PL7ZnMorkNV8cHCyUHgac0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eH75zn/dJMcahkqWix/PL7ZnMorkNV8cHCyUHgac0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeH75zn%2FdJMcahkqWix%2FPL7ZnMorkNV8cHCyUHgac0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;578&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 결제사가 느려졌을 뿐인데 내 서비스 전체가 죽는 일이 있어요. 결제와 무관한 상품 조회까지요. 장애가 호출 그래프를 타고 번지는 연쇄 장애인데, 그걸 끊는 차단기가 서킷 브레이커예요. 이름 그대로 두꺼비집이에요 &amp;mdash; 누전이 감지되면 회로를 내려서 집 전체가 타는 걸 막는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연쇄 장애가 번지는 구조에서 시작해, 서킷의 3상태(CLOSED&amp;middot;OPEN&amp;middot;HALF_OPEN), Resilience4j 실전 설정, 그리고 도입의 진짜 본체인 fallback 설계까지 짚어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 연쇄 장애, 남의 장애가 내 장애가 되는 길&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dj0NJR/dJMcaaMi2DI/XzBkWNmBmWDJLq5JTA6VTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dj0NJR/dJMcaaMi2DI/XzBkWNmBmWDJLq5JTA6VTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dj0NJR/dJMcaaMi2DI/XzBkWNmBmWDJLq5JTA6VTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdj0NJR%2FdJMcaaMi2DI%2FXzBkWNmBmWDJLq5JTA6VTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;연쇄 장애 전파 경로. 외부 결제사의 응답 지연으로 내 호출 스레드들이 묶이고 스레드 풀과 커넥션이 고갈되어 결제와 무관한 요청까지 처리하지 못하며 서비스 전체가 마비된다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;경로는 늘 같아요. 의존 서비스가 느려짐 &amp;rarr; 내 호출 스레드가 타임아웃까지 묶임 &amp;rarr; 그런 스레드가 쌓여 풀 고갈 &amp;rarr; 무관한 요청까지 처리 불가. &lt;a href=&quot;https://jessyt.tistory.com/486&quot;&gt;OSIV의 커넥션 고갈&lt;/a&gt;이나 &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;풀 말림&lt;/a&gt;과 정확히 같은 역학이에요 &amp;mdash; 자원이 &quot;일&quot;이 아니라 &quot;대기&quot;에 소모되는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 통찰 하나 &amp;mdash; 상대가 이미 죽었다면, &lt;b&gt;계속 부르는 것 자체가 자해&lt;/b&gt;예요. 어차피 실패할 호출에 스레드와 시간을 태우고 회복 중인 상대에게 부하까지 더해요. &quot;빨리 포기하고, 잠시 안 부르기&quot;가 양쪽 모두를 살려요. 그걸 자동화한 게 서킷 브레이커예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 3상태, 차단&amp;middot;탐색&amp;middot;복구&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3WGBo/dJMcabEr6Mw/9TBG6WsJkyBTVda4GAaUGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3WGBo%2FdJMcabEr6Mw%2F9TBG6WsJkyBTVda4GAaUGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;서킷 브레이커 상태머신. CLOSED에서 실패율이나 느린 호출 비율이 임계를 넘으면 OPEN으로 전환되어 호출을 즉시 실패시키고, wait duration이 지나면 HALF_OPEN에서 시험 호출을 N건만 통과시켜 성공하면 CLOSED로 복귀하고 실패하면 OPEN으로 회귀한다&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;578&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CLOSED(정상)&lt;/b&gt; &amp;mdash; 호출을 통과시키며 최근 호출들의 실패율을 집계해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OPEN(차단)&lt;/b&gt; &amp;mdash; 실패율이 임계(예: 50%)를 넘으면 회로를 열어요. 이후 호출은 &lt;b&gt;상대를 부르지도 않고 즉시 실패&lt;/b&gt;해요. 내 스레드는 0ms에 풀려나고 상대는 부하 없이 회복할 시간을 벌어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HALF_OPEN(탐색)&lt;/b&gt; &amp;mdash; 대기 시간이 지난 뒤 기본 설정에선 자동 전이가 아니라, 그 다음 호출이 들어올 때 HALF_OPEN으로 바뀌며 소수의 시험 호출만 통과시켜요(자동 전이는 옵션). 성공하면 CLOSED 복귀, 실패하면 다시 OPEN. 복구를 자동으로 감지하는 장치예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 디테일 &amp;mdash; &lt;b&gt;느린 호출도 실패로 칠 수 있어요.&lt;/b&gt; 진짜 무서운 건 에러보다 &quot;30초 걸리는 성공&quot;이거든요(스레드를 제일 오래 묶으니까). Resilience4j는 slow call 비율로도 회로를 열 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. Resilience4j 실전 설정&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;resilience4j:
  circuitbreaker:
    instances:
      paymentApi:
        sliding-window-size: 50              # 최근 50회 기준으로 집계
        minimum-number-of-calls: 20          # 최소 20회는 모여야 판단 (오픈 오판 방지)
        failure-rate-threshold: 50           # 실패율 50% 넘으면 OPEN
        slow-call-duration-threshold: 3s     # 3초 넘으면 &quot;느린 호출&quot;
        slow-call-rate-threshold: 80         # 느린 호출 80% 넘어도 OPEN
        wait-duration-in-open-state: 10s     # 10초 차단 후 HALF_OPEN
        permitted-number-of-calls-in-half-open-state: 5&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@CircuitBreaker(name = &quot;paymentApi&quot;, fallbackMethod = &quot;payFallback&quot;)
public PaymentResult pay(PaymentRequest req) {
    return paymentClient.approve(req);
}

private PaymentResult payFallback(PaymentRequest req, Throwable t) {
    return PaymentResult.unavailable(&quot;결제가 지연되고 있어요. 잠시 후 다시 시도해주세요.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;주의 &amp;mdash; @CircuitBreaker도 &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;프록시 기반&lt;/a&gt;이에요. 자기호출에서 조용히 무시되는 그 규칙이 여기도 적용돼요. 그리고 &lt;b&gt;의존성별로 인스턴스를 분리&lt;/b&gt;해요. 결제사와 추천 API가 한 서킷을 쓰면, 추천 장애가 결제까지 차단하는 황당한 일이 생겨요. 이름(name)이 곧 격리 단위예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 진짜 일은 fallback 설계예요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/olQ9x/dJMcadIW2rH/bjPASs4WK3uWS7XCL2tpH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/olQ9x/dJMcadIW2rH/bjPASs4WK3uWS7XCL2tpH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/olQ9x/dJMcadIW2rH/bjPASs4WK3uWS7XCL2tpH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FolQ9x%2FdJMcadIW2rH%2FbjPASs4WK3uWS7XCL2tpH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;서킷 브레이커 fallback 세 갈래. 캐시된 값이나 기본값으로 대체하거나, 해당 기능만 빼고 동작하는 기능 축소를 하거나, 대체 불가한 기능은 빠르고 명확한 실패 안내를 준다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;서킷이 열렸을 때 사용자에게 뭘 줄 거냐 &amp;mdash; 이게 도입의 본체예요. 라이브러리 설정은 30분이면 끝나지만 fallback은 기능마다 비즈니스 판단이 필요해요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대체 값&lt;/b&gt; &amp;mdash; 개인화 추천이 죽으면 인기 상품 목록을, 환율 조회가 죽으면 최근 캐시값을 줘요. 사용자는 차이를 거의 못 느껴요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기능 축소(graceful degradation)&lt;/b&gt; &amp;mdash; 리뷰 서비스가 죽으면 리뷰 영역만 숨기고 상품 페이지는 정상 서빙해요. &quot;전부 아니면 전무&quot;를 거부하는 거예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 실패 안내&lt;/b&gt; &amp;mdash; 결제처럼 대체가 불가능한 기능은, 30초 멈춤 끝의 타임아웃보다 즉시 &quot;잠시 후 재시도해주세요&quot;가 압도적으로 나은 경험이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 목록을 놓고 &quot;이 의존성이 죽으면?&quot;을 하나씩 답해보는 것 &amp;mdash; 그 표가 곧 fallback 설계서예요. 답이 안 나오는 기능(결제)은 그 자체로 &quot;이 의존성은 이중화가 필요하다&quot;는 발견이고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 운영에서 챙길 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상태 전이 알림&lt;/b&gt; &amp;mdash; 서킷이 OPEN 되는 순간이 곧 장애 감지예요. 상태 변화 이벤트를 모니터링&amp;middot;알림에 연결하면 의존성 장애를 사용자보다 먼저 알아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타임아웃과 한 세트&lt;/b&gt; &amp;mdash; 서킷은 &quot;실패를 집계&quot;하는 장치라, 호출 자체의 타임아웃이 없으면 느린 호출이 실패로 안 잡혀요. 타임아웃(TimeLimiter)을 짧게 거는 게 선행이에요 &amp;mdash; 이건 다음 편 주제예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;벌크헤드&lt;/b&gt; &amp;mdash; 의존성별로 스레드&amp;middot;동시 호출 수를 격리하는 패턴이에요. 서킷이 &quot;시간축 격리&quot;라면 벌크헤드는 &quot;자원축 격리&quot; &amp;mdash; 같이 쓰면 연쇄 차단이 더 단단해져요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서킷이 너무 자주 열려요 (오탐)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;minimum-number-of-calls&lt;/code&gt;가 작아 새벽 저트래픽에 한두 건 실패로 열리는 경우가 단골이에요. 최소 호출 수를 올리고 비즈니스 예외(잔액 부족 등)는 실패 집계에서 제외(&lt;code&gt;ignore-exceptions&lt;/code&gt;)해요 &amp;mdash; 그건 상대의 장애가 아니니까요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서킷이 열렸는데 아무도 몰랐어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fallback이 너무 매끄러워서 조용히 degrade만 된 거예요. 상태 전이 알림과 fallback 발동률 메트릭을 걸어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;붙였는데 동작을 안 해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기호출(프록시)이거나 인스턴스 이름 불일치예요. actuator의 circuitbreakers 엔드포인트로 상태를 직접 확인해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이게 죽으면 뭘 보여줄까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷 브레이커는 &lt;b&gt;&quot;죽은 상대를 계속 부르는 자해&quot;를 끊는 자동 차단기&lt;/b&gt;예요. 실패율과 느린 호출 비율로 OPEN되어 즉시 실패로 내 자원을 지키고 상대에게 회복 시간을 줘요. HALF_OPEN으로 복구를 탐지해요. 그런데 설정 자체는 30분이면 끝나고 진짜 무게는 의존성별 격리와 fallback 설계에 실려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 도입의 본체는 라이브러리가 아니라 이 질문이에요 &amp;mdash; &lt;b&gt;&quot;이 의존성이 죽으면 사용자에게 뭘 보여줄까?&quot;&lt;/b&gt; 기능 목록을 놓고 하나씩 답해보면 그 표가 곧 fallback 설계서가 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷의 짝꿍인 타임아웃과 재시도 전략(재시도 폭풍, 백오프+지터)을 시리즈 마지막인 &lt;a href=&quot;https://jessyt.tistory.com/508&quot;&gt;다음 글&lt;/a&gt;에서 다뤄요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot;&gt;Resilience4j &amp;mdash; CircuitBreaker&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://martinfowler.com/bliki/CircuitBreaker.html&quot;&gt;Martin Fowler &amp;mdash; CircuitBreaker&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/분산 &amp;amp; 운영</category>
      <category>fallback</category>
      <category>Resilience4j</category>
      <category>백엔드</category>
      <category>분산시스템</category>
      <category>서킷 브레이커</category>
      <category>연쇄 장애</category>
      <category>장애 격리</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/507</guid>
      <comments>https://jessyt.tistory.com/507#entry507comment</comments>
      <pubDate>Tue, 11 Aug 2026 07:30:20 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] archive 소스로 플러그인을 zip으로 설치해요</title>
      <link>https://jessyt.tistory.com/598</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/pV4bR/dJMcadbRRDV/AAAAAAAAAAAAAAAAAAAAAHTOKVJaJWON6oJuDdYz3YfxxGVhDX5u-PS5cKgxLTIu/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=vZetqt6%2BOYC2auzYmG6L%2BcVMinw%3D&quot; alt=&quot;[Claude Code] archive 소스로 플러그인을 zip으로 설치해요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] archive 소스로 플러그인을 zip으로 설치해요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 플러그인을 zip 하나로 설치해요. 받는 머신에 git이나 npm이 없어도 됩니다. 2.1.224 changelog에 올라온 archive 플러그인 소스 얘기예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 정적 파일 서버에 zip만 올려두면 돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 플러그인 소스는 github, url, git-subdir, npm처럼 대부분 git이나 패키지 매니저를 거치는 방식이었어요. archive는 HTTPS로 zip을 받아오는 게 전부예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에는 S3 버킷, Artifactory generic 저장소, nginx 같은 정적 파일 서버나 아티팩트 저장소 어디든 올려두면 된다고 나와 있어요. marketplace.json의 플러그인 항목에 이렇게 적어요.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-plugin&quot;,
  &quot;source&quot;: {
    &quot;source&quot;: &quot;archive&quot;,
    &quot;url&quot;: &quot;https://artifacts.example.com/claude-plugins/my-plugin-2.1.0.zip&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포하는 쪽은 아티팩트 서버에 zip을 올리는 것으로 끝나고, 받는 쪽은 git 자격증명이나 npm 레지스트리 설정을 거치지 않아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. sha256을 박으면 파일이 바뀐 순간 설치를 거부해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 기준으로, source 안에 sha256 필드를 넣으면 다운로드한 파일의 다이제스트를 매번 검증해요. 64자 hex 문자열이고 대소문자는 가리지 않아요.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-plugin&quot;,
  &quot;source&quot;: {
    &quot;source&quot;: &quot;archive&quot;,
    &quot;url&quot;: &quot;https://artifacts.example.com/claude-plugins/my-plugin-2.1.0.zip&quot;,
    &quot;sha256&quot;: &quot;6bfa50e3d2e00c052b46abe51fff89346ac803e45771f76dcf6df1ab74cca5e1&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 안 맞으면 설치를 멈추고 Plugin archive integrity check failed 오류를 내요. 같은 URL에 새 zip을 덮어쓰는 운영 방식이라면 핀을 박아두는 쪽이 사고를 덜 냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 쪽에는 문서에 적힌 함정이 하나 더 있어요. sha256이 버전 역할을 하는 건 plugin.json에도 마켓플레이스 항목에도 version이 없을 때뿐이에요. version을 선언해뒀다면 zip과 다이제스트를 바꿔도 version을 안 올리는 한 사용자는 캐시된 예전 사본을 그대로 씁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 최상위 두 겹까지만 열어보고 http:// URL은 거부해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zip은 두 가지 모양을 다 받아요. .claude-plugin/ 폴더가 압축 최상단에 있어도 되고, 최상위 폴더 하나 안에 들어 있어도 돼요. 문서에는 그보다 더 깊이 들어간 플러그인은 설치가 실패한다고 적혀 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 쪽 제약도 같이 확인해두는 게 좋아요. 문서에 적힌 조건은 네 가지예요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;256 MiB를 넘는 아카이브는 거부해요&lt;/li&gt;
&lt;li&gt;http:// URL은 받지 않아요&lt;/li&gt;
&lt;li&gt;loopback&amp;middot;link-local&amp;middot;클라우드 메타데이터 호스트로 향하는 주소는 막혀요&lt;/li&gt;
&lt;li&gt;리다이렉트가 걸리면 매 홉이 같은 조건을 만족해야 다운로드가 진행돼요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 아티팩트 서버 주소를 그대로 옮겨 적었다가 마지막 항목에서 막히는 경우가 나올 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 버전이 낮으면 마켓플레이스가 통째로 안 열려요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;archive는 Claude Code v2.1.224 이상에서만 동작해요. 문제는 그 아래 버전의 반응이 서로 다르다는 점이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v2.1.120부터 v2.1.223까지는 해당 플러그인 설치만 실패하면서 &quot;This plugin uses a source type your Claude Code version does not support. Update Claude Code and try again.&quot;이라는 안내가 떠요. 더 낮은 버전에서는 archive 항목이 하나라도 들어간 마켓플레이스 파일 자체가 로드되지 않아요. 팀에 배포하는 마켓플레이스라면 다른 플러그인까지 같이 안 보이게 되는 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 더, Team&amp;middot;Enterprise 플랜의 조직 설정으로 마켓플레이스를 배포할 때는 archive와 npm 소스를 쓸 수 없어요. 이 경로에서 지원하는 건 github, url, git-subdir와 마켓플레이스 저장소 안을 가리키는 상대 경로예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://code.claude.com/docs/en/plugin-marketplaces&quot;&gt;Claude Code Docs &amp;mdash; Create and distribute a plugin marketplace&lt;/a&gt;, &lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG 2.1.224&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>ai코딩도구</category>
      <category>anthropic</category>
      <category>Archive</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>개발환경</category>
      <category>마켓플레이스</category>
      <category>플러그인</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/598</guid>
      <comments>https://jessyt.tistory.com/598#entry598comment</comments>
      <pubDate>Tue, 11 Aug 2026 07:00:46 +0900</pubDate>
    </item>
    <item>
      <title>[분산시스템] 사가(Saga) 패턴: 보상 트랜잭션, 코레오그래피 vs 오케스트레이션</title>
      <link>https://jessyt.tistory.com/506</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wRCsR/dJMcaar2LUh/lTXfcfHzaX5ItDcafQO5hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wRCsR/dJMcaar2LUh/lTXfcfHzaX5ItDcafQO5hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wRCsR/dJMcaar2LUh/lTXfcfHzaX5ItDcafQO5hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwRCsR%2FdJMcaar2LUh%2FlTXfcfHzaX5ItDcafQO5hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[분산시스템] 사가(Saga) 패턴: 보상 트랜잭션, 코레오그래피 vs 오케스트레이션&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[분산시스템] 사가(Saga) 패턴: 보상 트랜잭션, 코레오그래피 vs 오케스트레이션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문&amp;middot;결제&amp;middot;재고가 한 DB에 있을 땐 &lt;code&gt;@Transactional&lt;/code&gt; 하나로 &quot;전부 성공 아니면 전부 취소&quot;가 됐어요. 서비스를 쪼개는 순간 그 마법이 사라져요 &amp;mdash; DB가 다르니 하나의 롤백이 없거든요. 그 자리를 채우는 게 사가 패턴이에요. &quot;롤백&quot;을 &quot;보상&quot;으로 바꾸는 사고방식이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 트랜잭션 문제에서 출발해, 사가의 핵심인 보상 트랜잭션, 두 스타일(코레오그래피 vs 오케스트레이션), 그리고 따라오는 어려운 숙제(보상 불가 작업&amp;middot;중간 상태 노출)까지 짚어요. &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스&lt;/a&gt;가 &quot;이벤트를 확실히 전달&quot;하는 패턴이라면, 사가는 그 위에서 &quot;흐름 전체의 정합성&quot;을 다루는 패턴이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 하나의 롤백이 사라졌다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkyNZ5/dJMcaaFBprn/SK2TYpQAztvvh5uOAYmJdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkyNZ5/dJMcaaFBprn/SK2TYpQAztvvh5uOAYmJdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkyNZ5/dJMcaaFBprn/SK2TYpQAztvvh5uOAYmJdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkyNZ5%2FdJMcaaFBprn%2FSK2TYpQAztvvh5uOAYmJdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;분산 트랜잭션 문제. 주문, 결제, 재고가 각자의 DB를 가진 서비스로 갈라지면 재고 차감 실패 시 이미 커밋된 주문과 결제를 하나의 롤백으로 되돌릴 수 없다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;재고 차감이 실패한 시점에, 주문과 결제는 이미 각자의 DB에 커밋돼 있어요. 전통적인 답인 분산 트랜잭션(2PC)은 모든 참여자가 락을 쥔 채 서로를 기다리는 구조라 느리고 조정자가 죽으면 다 같이 멈춰요. 그래서 MSA에서는 사실상 기피 대상이고 실무 표준이 사가예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 사가, 롤백 대신 보상&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKisBU/dJMcaaFBpro/KKJNAiGhSIdXg1cvyVKKT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKisBU/dJMcaaFBpro/KKJNAiGhSIdXg1cvyVKKT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKisBU/dJMcaaFBpro/KKJNAiGhSIdXg1cvyVKKT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKisBU%2FdJMcaaFBpro%2FKKJNAiGhSIdXg1cvyVKKT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;사가 패턴 동작. 주문 생성, 결제 승인이 로컬 트랜잭션으로 각각 커밋된 뒤 재고 차감이 실패하면, 결제 취소와 주문 취소라는 보상 트랜잭션을 역순으로 실행해 전체를 되돌린다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;사가는 큰 트랜잭션을 &lt;b&gt;로컬 트랜잭션의 연쇄&lt;/b&gt;로 풀어요. 각 단계는 자기 DB에 바로 커밋하고 중간에 실패하면 &lt;b&gt;이미 커밋된 단계들의 보상 트랜잭션(undo)을 역순으로&lt;/b&gt; 실행해요. 재고 실패 &amp;rarr; 결제 취소(환불) &amp;rarr; 주문 취소, 이런 식이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 인식 전환이 있어요. 보상은 롤백이 아니에요. &lt;b&gt;&quot;없던 일로&quot;가 아니라 &quot;되돌리는 새로운 일&quot;&lt;/b&gt;이에요. 결제 취소는 환불 기록이 남는 별도 트랜잭션이고 이미 나간 알림은 &quot;취소 알림&quot;으로 덮을 수밖에 없어요. 그래서 사가의 설계 규칙 1번은 &amp;mdash; &lt;b&gt;모든 단계는 보상 가능해야 한다&lt;/b&gt;예요. 단계를 설계할 때 &quot;이걸 어떻게 되돌리지?&quot;를 같이 설계하는 거예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;보상이 안 되는 작업(이메일 발송, 외부사 확정 통보)도 현실엔 있어요. 요령은 &lt;b&gt;순서&lt;/b&gt;예요 &amp;mdash; 보상 불가능한 단계를 사가의 맨 뒤로 보내요. &quot;다 확정된 다음에야 메일을 보낸다&quot;면 보상할 일 자체가 안 생기니까요. 그래도 남는 건 &quot;예약&quot; 개념으로 풀어요. 재고를 바로 차감하는 대신 선점(예약)하고 사가 성공 시 확정&amp;middot;실패 시 해제하는 식이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 코레오그래피, 이벤트로 릴레이&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAyzk2/dJMcaar2LUg/VtHCCNOox0xVcdIPbk1Lp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAyzk2/dJMcaar2LUg/VtHCCNOox0xVcdIPbk1Lp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAyzk2/dJMcaar2LUg/VtHCCNOox0xVcdIPbk1Lp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAyzk2%2FdJMcaar2LUg%2FVtHCCNOox0xVcdIPbk1Lp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;사가 두 스타일 비교. 코레오그래피는 각 서비스가 이벤트를 구독해 반응하는 방식으로 단순한 흐름에 가볍지만 전체 흐름이 보이지 않고, 오케스트레이션은 중앙 조정자가 상태 머신으로 순서와 보상을 관리해 복잡한 사가에 유리하다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 조정자 없이, 각 서비스가 앞 단계의 이벤트를 구독해 자기 일을 해요. 주문이 &quot;주문생성됨&quot;을 발행하면 결제가 받아 처리하고 &quot;결제완료됨&quot;을 발행해요. 재고가 받아 차감하고... 실패 시엔 &quot;결제실패됨&quot; 같은 이벤트를 거꾸로 타고 보상이 연쇄돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 서로를 모르니 결합이 낮아요. 2~3단계짜리 단순한 흐름엔 가볍고 좋아요. 문제는 성장할 때예요 &amp;mdash; 단계가 늘면 &lt;b&gt;&quot;이 주문이 지금 어디까지 갔지?&quot;를 아무도 몰라요.&lt;/b&gt; 흐름이 코드 어디에도 없고 이벤트 구독 관계에 흩어져 있으니까요. 장애 추적이 &quot;이벤트 고고학&quot;이 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 오케스트레이션, 지휘자를 둔다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사가 오케스트레이터라는 중앙 조정자가 흐름을 상태 머신으로 관리해요. &quot;결제 서비스, 승인해&quot; &amp;rarr; 응답 보고 &amp;rarr; &quot;재고 서비스, 차감해&quot; &amp;rarr; 실패 응답 &amp;rarr; &quot;결제 서비스, 취소해&quot;. 명령과 보상의 순서를 조정자가 다 알아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름이 한곳에 코드로 보여요. 각 사가 인스턴스의 현재 상태를 조회할 수 있고 보상 순서가 명시적이에요. 대가는 조정자라는 컴포넌트의 구현&amp;middot;운영이고요(직접 상태 머신을 짜거나 임시 프레임워크를 써요). 기준은 단순해요 &amp;mdash; &lt;b&gt;3~4단계를 넘고 보상이 얽히기 시작하면 오케스트레이션이 결국 싸요.&lt;/b&gt; &quot;지금 어디까지 갔나&quot;를 물어볼 곳이 있다는 게 운영에서 그만큼 커요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 사가가 데려오는 숙제들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중간 상태가 보여요&lt;/b&gt; &amp;mdash; 사가 진행 중엔 &quot;주문은 됐는데 결제는 아직&quot;인 순간이 존재하고 다른 조회가 그걸 볼 수 있어요(격리성 부재). 상태 필드(PENDING&amp;rarr;CONFIRMED)로 중간 상태를 명시하고 화면&amp;middot;API가 그 상태를 해석하게 설계해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 신뢰성이 전제예요&lt;/b&gt; &amp;mdash; 단계 사이의 이벤트&amp;middot;명령이 유실되면 사가가 영원히 멈춰요. 그래서 발행은 &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스&lt;/a&gt;로, 처리는 &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;멱등 컨슈머&lt;/a&gt;로 &amp;mdash; 앞 편들이 사가의 토대예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타임아웃 처리&lt;/b&gt; &amp;mdash; 응답이 영영 안 오는 단계가 있을 수 있어요. 단계별 타임아웃을 두고 초과 시 보상으로 전환하는 규칙까지가 사가 설계예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사가가 중간에 멈춰서 안 움직여요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 유실이나 타임아웃 규칙 부재예요. 아웃박스로 전달을 보장하고 오케스트레이션이라면 &quot;N분째 같은 상태인 사가&quot; 알림을 걸어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보상이 실패하면요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보상은 &quot;반드시 결국 성공&quot;하도록 설계해요 &amp;mdash; 멱등하게 만들어 재시도를 무한히 안전하게 하고 그래도 안 되면 사람에게 에스컬레이션(수동 처리 큐)해요. 보상의 실패 처리를 또 보상으로 푸는 무한 후퇴는 없어요. 어딘가엔 &quot;재시도 + 알림&quot;이 바닥이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 흐름을 추적 못 하겠어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코레오그래피가 복잡해진 신호예요. 사가 ID를 모든 이벤트에 실어 로그로 묶고(MDC) 그래도 힘들면 오케스트레이션 전환을 검토해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보상이라는 사고방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사가는 &lt;b&gt;분산 환경에서 &quot;전부 아니면 전무&quot;를 보상 트랜잭션으로 흉내 내는 패턴&lt;/b&gt;이에요. 모든 단계를 보상 가능하게 설계하고(불가능한 건 맨 뒤로 보내거나 예약으로 풀고) 흐름이 단순하면 코레오그래피, 단계가 얽히면 오케스트레이션이에요. 그리고 사가는 혼자 못 서요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스&lt;/a&gt;가 전달을 확실하게, &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;멱등성&lt;/a&gt;이 재시도를 안전하게 받쳐줘야 비로소 굴러가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 습관으로는, 단계마다 &quot;이걸 어떻게 되돌리지&quot;의 답을 같이 적어두는 게 좋아요. 그 답이 곧 보상 트랜잭션이고 답이 안 나오는 단계는 순서를 뒤로 밀라는 신호예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 재시도와 짝꿍인 서킷 브레이커를 &lt;a href=&quot;https://jessyt.tistory.com/507&quot;&gt;다음 글&lt;/a&gt;에서 다뤄요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://microservices.io/patterns/data/saga.html&quot;&gt;microservices.io &amp;mdash; Saga&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/saga&quot;&gt;Microsoft &amp;mdash; Saga pattern&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/분산 &amp;amp; 운영</category>
      <category>MSA</category>
      <category>SAGA</category>
      <category>백엔드</category>
      <category>보상 트랜잭션</category>
      <category>분산시스템</category>
      <category>사가 패턴</category>
      <category>오케스트레이션</category>
      <category>코레오그래피</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/506</guid>
      <comments>https://jessyt.tistory.com/506#entry506comment</comments>
      <pubDate>Mon, 10 Aug 2026 07:30:31 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 다른 터미널 세션에 메시지를 보내요</title>
      <link>https://jessyt.tistory.com/596</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/bfIiyq/dJMcafAQXok/AAAAAAAAAAAAAAAAAAAAACB1sXVnUcTJsgFTrAJcFnQ4HUtER8QxCfEaPqfhC8Ls/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=cQZ8P3dOcTntjxr7tRQZHNN9xno%3D&quot; alt=&quot;[Claude Code] 다른 터미널 세션에 메시지를 보내요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 다른 터미널 세션에 메시지를 보내요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code(클로드 코드) 2.1.224부터 세션 간에 메시지를 주고받는 기능이 들어갔어요. 터미널 A에서 스키마를 바꿨을 때 그 사실을 터미널 B에 직접 옮겨 적는 대신, Claude가 옆 세션에 알려주는 구조예요. 조건만 맞으면 따로 켤 것 없이 이미 돌아가고 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. macOS&amp;middot;리눅스만 되고, 확인은 /list-agents로 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 버전은 2.1.224 이상이에요. 네이티브 윈도우는 지원 대상이 아니고, WSL 2 안의 리눅스는 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션에서 /list-agents(별칭 /peers)를 쳐보면 바로 알 수 있어요. 명령 자체를 못 알아들으면 그 세션엔 기능이 없는 거라, claude --version부터 보라고 문서에 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공자 조건도 있어요. Amazon Bedrock, Claude Platform on AWS, Google Cloud Agent Platform, Microsoft Foundry 경유로 쓰는 세션에서는 안 돼요. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC이나 DO_NOT_TRACK 같은 변수를 박아둔 경우도 기능이 꺼진 상태로 뜬다고 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. SendMessage를 직접 부르는 게 아니라 말로 시켜요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 ListAgents로 받을 세션을 찾고 SendMessage로 보내요. 사람이 두 도구를 직접 호출할 일은 없고, 프롬프트만 던지면 돼요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;다른 터미널에서 돌고 있는 세션에 마이그레이션 끝났는지 물어봐줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문구도 Claude가 알아서 써요. 세션이 답하는 이름은 /rename이나 --name으로 정해요. 안 정하면 작업 디렉터리 이름을 딴 myapp-3f 같은 이름이 자동으로 붙어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오가는 건 텍스트 한 덩이뿐이에요. 대화 기록이나 파일은 안 넘어가요. 통째로 옮기고 싶으면 메시지 말고 세션 resume을 쓰라는 게 문서 안내예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 같은 머신은 소켓, 머신 밖은 서버를 거쳐요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 기준으로 같은 머신 안에서는 세션마다 뚫어둔 소켓으로 바로 전달돼요. Anthropic 서버를 거치지 않는 경로라고 적혀 있어서, 사내 정책상 외부로 나가는 트래픽을 따지는 곳이면 이 구분이 중요해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 내 머신의 세션은 그 머신의 Remote Control 연결로 도착하고 웹 세션은 클라우드 세션으로 바로 들어간다고 해요. 두 경우 모두 Anthropic 서버를 거쳐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신 밖 세션에 먼저 말을 거는 건 2.1.224 시점엔 막혀 있었어요. 2.1.225 changelog에는 ListAgents에 뜨는 이름으로 먼저 대화를 시작할 수 있게 됐다고 적혀 있는데, 문서 페이지에는 아직 답장만 된다는 서술이 남아 있어요. 이 부분은 본인 버전을 보고 판단할 대목이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 파일시스템이 따로라 호스트 세션과 서로 못 찾는다고 해요. 같은 컨테이너 안의 두 세션끼리는 되고요. 머신 밖으로 나가는 메시지마다 승인을 받고 싶으면 isolatePeerMachines를 true로 두면 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 받는 쪽은 crossSessionInbound로 막을 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도착한 메시지를 어떻게 할지는 받는 세션이 정해요. accept는 그대로 전달, hold는 승인 대기, refuse는 그냥 버림이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 안 정했을 때는 보내는 세션과 받는 세션의 권한 모드를 같이 봐요. 둘 다 권한 프롬프트가 뜨는 세션이거나 둘 다 bypassPermissions면 그대로 전달돼요. 한쪽만 bypassPermissions면 받는 쪽에 승인 창이 떠요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 창을 5분(dialogExpiry 기본값) 동안 안 누르면 메시지가 버려진다고 문서에 나와 있어요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;다른 세션이 보낸 메시지는 내 승인을 대신하지 못해요. 권한 설정이나 CLAUDE.md를 바꿔달라는 요청도 막히고, 본문에 /compact 같은 게 적혀 있어도 평문으로만 들어와요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아예 끄려면 보내기와 받기를 따로 잠가야 해요. 조직 단위로 막을 땐 managed settings에 아래 두 줄을 같이 넣으라고 안내돼 있어요.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;permissions&quot;: {
    &quot;deny&quot;: [&quot;SendMessage&quot;, &quot;ListAgents&quot;]
  },
  &quot;crossSessionInbound&quot;: &quot;refuse&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 쪽도 알아둘 부분이 있어요. 전달된 메시지는 내가 타이핑한 프롬프트와 똑같이 usage로 잡힌다고 해요. 세션 둘이 서로 핑퐁하는 사고는 반복 발송 throttle과 대기 50개 상한으로 알아서 멈춘다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://code.claude.com/docs/en/cross-session-messaging&quot;&gt;Claude Code Docs &amp;mdash; Message your other Claude Code sessions&lt;/a&gt;, &lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG 2.1.224&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>ListAgents</category>
      <category>SendMessage</category>
      <category>개발도구</category>
      <category>기능출시</category>
      <category>터미널</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/596</guid>
      <comments>https://jessyt.tistory.com/596#entry596comment</comments>
      <pubDate>Mon, 10 Aug 2026 07:00:43 +0900</pubDate>
    </item>
    <item>
      <title>[zoxide] cd 대신 쓰는 디렉터리 점프</title>
      <link>https://jessyt.tistory.com/597</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/Ou0PH/dJMcag0JSWP/AAAAAAAAAAAAAAAAAAAAAJrZZ5FPwrBeQujBiYATfQjtwKxVEHjfVnVUa4f3aSiu/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=uJRlZwWguyO16dGRK553pp6DHsE%3D&quot; alt=&quot;[zoxide] cd 대신 쓰는 디렉터리 점프&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 터미널 &amp;amp; 환경 &amp;middot; 꿀팁&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[zoxide] cd 대신 쓰는 디렉터리 점프&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;터미널에서 같은 프로젝트 폴더를 하루에도 수십 번 오가요. cd는 그때마다 전체 경로를 요구하죠. zoxide는 방문한 디렉터리를 기록해 두고 이름 일부만 치면 그리로 보내주는 도구예요. 예전의 autojump나 z 스크립트가 하던 일을 Rust로 다시 쓴 프로젝트고요. 설치와 셸 설정, 점수를 매기는 방식, 실제로 쓰는 명령과 환경 변수, 그리고 미리 알아둘 함정까지 순서대로 정리해요.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;cd는 경로를 이미 알고 있어야 움직여요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cd의 문제는 기능이 부족한 게 아니라 사람에게 기억을 요구한다는 점이에요. ~/work/backend/services/payment-gateway 같은 경로를 매번 정확히 치거나, 탭 완성으로 한 단계씩 내려가야 해요. 상위로 올라갔다 다른 가지로 내려오는 왕복이 잦을수록 타이핑이 길어져요. 별칭을 만들어 두면 이번엔 별칭 이름이 기억나지 않는 상황이 와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 푸는 접근은 오래전부터 있었어요. rupa/z 셸 스크립트, autojump, fasd 같은 도구들이 방문 기록을 데이터베이스에 쌓아 두고 &lt;code&gt;z 키워드&lt;/code&gt; 한 번으로 점프시키는 방식을 만들었어요. zoxide는 같은 아이디어를 Rust로 다시 구현한 프로젝트예요. GitHub 스타 3만 8천 개가 넘는 인기 도구고 MIT 라이선스 오픈소스라 무료예요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;고려할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cd + 탭 완성&lt;/td&gt;
&lt;td&gt;경로를 한 단계씩 입력&lt;/td&gt;
&lt;td&gt;깊은 경로일수록 타이핑이 늘어남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alias 지정&lt;/td&gt;
&lt;td&gt;자주 가는 곳에 이름 부여&lt;/td&gt;
&lt;td&gt;폴더가 늘면 별칭도 같이 관리해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;z&amp;middot;autojump&amp;middot;fasd&lt;/td&gt;
&lt;td&gt;방문 기록 기반 점프&lt;/td&gt;
&lt;td&gt;셸 스크립트 구현, 셸별 지원 편차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zoxide&lt;/td&gt;
&lt;td&gt;방문 기록 기반 점프&lt;/td&gt;
&lt;td&gt;단일 바이너리, 주요 셸 전부 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 도구 대비 zoxide가 내세우는 차이는 두 갈래예요. 하나는 셸 스크립트가 아닌 컴파일된 바이너리라 어느 셸에서든 같은 코드가 돈다는 점이에요. 다른 하나는 지원 범위예요. bash&amp;middot;zsh&amp;middot;fish&amp;middot;PowerShell은 물론 Nushell&amp;middot;Elvish&amp;middot;Xonsh&amp;middot;Tcsh, 그리고 POSIX 셸까지 초기화 명령이 각각 준비돼 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치하고 셸 설정 파일 맨 끝에 한 줄 넣으면 끝나요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 경로는 OS별 패키지 매니저로 대부분 해결돼요. macOS는 Homebrew, Windows는 winget&amp;middot;choco&amp;middot;scoop, 리눅스는 배포판 패키지나 공식 설치 스크립트를 씁니다. Rust 툴체인이 있다면 cargo로도 받아요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# macOS
&amp;gt; brew install zoxide

# Windows
&amp;gt; winget install ajeetdsouza.zoxide

# Linux (공식 설치 스크립트)
&amp;gt; curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh

# 어느 OS든 (Rust 툴체인 필요)
&amp;gt; cargo install zoxide --locked&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치만으로는 &lt;code&gt;z&lt;/code&gt; 명령이 생기지 않아요. zoxide는 셸 함수를 만들어 주는 초기화 명령을 따로 실행해야 해요. 이 줄을 셸 설정 파일에 넣어야 매 세션에 적용돼요. 공식 문서가 반복해서 강조하는 조건이 &lt;b&gt;설정 파일의 맨 끝&lt;/b&gt;에 넣으라는 것이에요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# ~/.zshrc 맨 끝
eval &quot;$(zoxide init zsh)&quot;

# ~/.bashrc 맨 끝
eval &quot;$(zoxide init bash)&quot;

# ~/.config/fish/config.fish 맨 끝
zoxide init fish | source

# PowerShell 프로파일 (echo $profile 로 위치 확인)
Invoke-Expression (&amp;amp; { (zoxide init powershell | Out-String) })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init 명령에는 옵션이 세 개 붙어요. &lt;code&gt;--cmd&lt;/code&gt;는 명령 이름을 바꿔요. &lt;code&gt;--cmd j&lt;/code&gt;로 초기화하면 명령이 j와 ji가 돼요. &lt;code&gt;--cmd cd&lt;/code&gt;로 두면 cd 자체를 zoxide로 대체해요. &lt;code&gt;--hook&lt;/code&gt;은 점수를 올리는 시점을 정해요. 기본값은 디렉터리가 바뀔 때마다인 pwd이고 나머지 선택지는 prompt(프롬프트가 뜰 때마다)와 none(올리지 않음)이에요. &lt;code&gt;--no-cmd&lt;/code&gt;는 z와 zi를 아예 정의하지 않고 내부 함수인 __zoxide_z, __zoxide_zi만 노출해요. 자기 셸 함수로 감싸 쓰려는 경우를 위한 옵션이에요.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;순서 주의&lt;/span&gt; 환경 변수로 동작을 바꿀 생각이라면 그 export 문이 &lt;code&gt;zoxide init&lt;/code&gt; 줄보다 &lt;b&gt;앞에&lt;/b&gt; 있어야 해요. 공식 문서는 환경 변수가 init 호출 전에 설정되어 있어야 한다고 못 박고 있어요. 설정 파일 맨 끝 규칙과 함께 보면, 환경 변수 &amp;rarr; init 순서로 배치하는 셈이에요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;점수는 방문 횟수와 최근성을 곱해서 계산해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zoxide의 랭킹은 frecency, 즉 frequency(빈도)와 recency(최근성)를 합친 개념이에요. 디렉터리는 처음 방문할 때 점수 1을 받고 방문할 때마다 1씩 올라가요. 그리고 검색 시점에 마지막 방문 시각을 따져 배수를 곱해요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;마지막 방문 시점&lt;/th&gt;
&lt;th&gt;계산되는 frecency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1시간 이내&lt;/td&gt;
&lt;td&gt;점수 &amp;times; 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하루 이내&lt;/td&gt;
&lt;td&gt;점수 &amp;times; 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일주일 이내&lt;/td&gt;
&lt;td&gt;점수 &amp;divide; 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그보다 오래됨&lt;/td&gt;
&lt;td&gt;점수 &amp;divide; 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1시간 이내와 일주일이 지난 경우의 배수 차이가 8배라, 오래전에 아무리 자주 갔던 폴더라도 오늘 작업 중인 폴더를 이기기 어려운 구조예요. 작업 대상이 바뀌면 랭킹도 며칠 안에 따라오는 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스가 무한정 커지지 않게 하는 장치도 들어 있어요. 전체 점수 합이 _ZO_MAXAGE(기본 10000)를 넘으면 모든 디렉터리 점수를 일정 계수로 나눠 총합을 _ZO_MAXAGE의 90% 수준으로 낮춰요. 그 과정에서 점수가 1 아래로 떨어진 항목은 데이터베이스에서 빠져요. 문서는 이론상 최대 항목 수를 _ZO_MAXAGE의 4배로 잡되 실제로는 그보다 적다고 설명해요. 여기에 더해 파일 시스템에서 이미 사라졌고 90일이 지난 항목은 조회 과정에서 지연 정리돼요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;매칭 규칙 세 가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수만큼이나 중요한 게 어떤 경로가 후보에 오르는지예요. 규칙은 세 가지고, 이걸 모르면 왜 원하는 곳으로 안 가는지 이해가 안 되는 순간이 와요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대소문자를 구분하지 않아요.&lt;/b&gt; Payment와 payment는 같게 취급돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 키워드가 경로 안에 순서대로 있어야 해요.&lt;/b&gt; &lt;code&gt;z fo ba&lt;/code&gt;는 /foo/bar에 매칭되지만 /bar/foo에는 매칭되지 않아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마지막 키워드의 마지막 조각이 경로의 마지막 조각과 일치해야 해요.&lt;/b&gt; &lt;code&gt;z bar&lt;/code&gt;는 /foo/bar를 잡지만 /bar/foo는 잡지 않아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 규칙이 실무에서 가장 자주 걸려요. 중간 디렉터리 이름만 치면 안 잡혀요. 목적지 폴더의 이름 일부를 마지막 키워드로 줘야 해요. 반대로 이 규칙 덕분에 상위 폴더가 얻어걸리는 오작동이 줄어드는 면도 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;손에 익힐 명령은 사실상 z와 zi 둘이에요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;z &amp;mdash; 기본 점프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 쓰는 형태예요. 키워드를 하나 주면 매칭되는 디렉터리 중 frecency가 가장 높은 곳으로 이동해요. 후보가 여럿이라 헷갈릴 때는 키워드를 두 개 이상 주면 범위가 좁아져요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;z foo              # foo 에 매칭되는 최고 랭킹 디렉터리로 이동
z foo bar          # foo 와 bar 를 모두 만족하는 디렉터리로 이동
z foo /            # foo 로 시작하는 하위 디렉터리로 이동

z ~/foo            # 일반 cd 처럼도 동작
z foo/             # 상대 경로로 이동
z ..               # 한 단계 위로
z -                # 직전 디렉터리로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목할 부분은 아래쪽 네 줄이에요. z는 기록에 없는 경로를 받으면 그냥 cd처럼 동작해요. &lt;code&gt;z ..&lt;/code&gt;이나 &lt;code&gt;z -&lt;/code&gt; 같은 관용 표현도 그대로 받아요. cd를 쓰던 손버릇을 유지한 채 점프 기능만 얹는 형태예요. 초기화할 때 &lt;code&gt;--cmd cd&lt;/code&gt;로 아예 cd를 대체하는 선택지가 성립하는 이유이기도 해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zi &amp;mdash; fzf로 골라서 이동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드로 좁혀도 후보가 여러 개일 때 쓰는 명령이에요. &lt;code&gt;zi foo&lt;/code&gt;를 치면 매칭된 목록이 fzf 화면으로 떠요. 방향키나 추가 타이핑으로 하나를 골라 이동해요. 어떤 후보들이 잡히고 있는지 눈으로 확인하는 용도로도 쓰여요. fzf가 설치돼 있어야 하며 문서가 명시한 최소 지원 버전은 v0.51.0이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bash 4.4 이상&amp;middot;fish&amp;middot;zsh에서는 다른 경로도 있어요. &lt;code&gt;z foo&lt;/code&gt;까지 친 뒤 공백을 넣고 탭을 누르면 인터랙티브 완성이 떠요. 명령을 바꾸지 않고 후보를 확인하는 순서예요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zoxide query &amp;mdash; 데이터베이스 들여다보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랭킹이 예상과 다르게 나올 때 원인을 확인하는 명령이에요. 검색만 하고 이동은 하지 않아요. &lt;code&gt;--list&lt;/code&gt;는 최고 점수 하나가 아니라 매칭된 전부를 보여줘요. &lt;code&gt;--score&lt;/code&gt;는 계산된 점수를 경로와 함께 출력해요. 둘을 같이 쓰면 왜 저 폴더가 1등인지 숫자로 확인돼요. 나머지 옵션도 셋 있어요. &lt;code&gt;--all&lt;/code&gt;은 삭제된 디렉터리까지 포함하고 &lt;code&gt;--exclude&lt;/code&gt;는 특정 경로를 결과에서 빼요. &lt;code&gt;--interactive&lt;/code&gt;는 zi와 같은 fzf 선택 화면을 띄워요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# 매칭되는 항목 전부를 점수와 함께 확인
&amp;gt; zoxide query --list --score project

# 잘못 쌓인 항목 제거
&amp;gt; zoxide remove /path/to/old-project

# 특정 경로 점수를 직접 지정해서 추가
&amp;gt; zoxide add --score 100 /path/to/main-repo&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zoxide add / remove / edit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add는 디렉터리를 데이터베이스에 넣거나 점수를 올려요. 평소에는 셸 훅이 알아서 호출해요. 직접 칠 일은 &lt;code&gt;--score&lt;/code&gt; 옵션으로 초기 점수를 지정할 때 정도예요. remove는 특정 경로를 데이터베이스에서 지워요. 지운 뒤에도 계속 방문하면 다시 쌓여요. 영구적으로 제외하려면 아래 나오는 _ZO_EXCLUDE_DIRS 쪽을 봐야 해요. edit은 데이터베이스를 직접 편집하는 서브커맨드예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경 변수 여섯 개가 나머지 동작을 정해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일 형식은 따로 없고 환경 변수로 조정해요. 앞서 적었듯 전부 &lt;code&gt;zoxide init&lt;/code&gt; 줄보다 앞에 있어야 해요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_DATA_DIR&lt;/td&gt;
&lt;td&gt;데이터베이스 저장 위치. 기본값은 리눅스&amp;middot;BSD가 $XDG_DATA_HOME 또는 $HOME/.local/share, macOS가 $HOME/Library/Application Support, 윈도우가 %LOCALAPPDATA%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_ECHO&lt;/td&gt;
&lt;td&gt;1로 두면 이동 전에 매칭된 경로를 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_EXCLUDE_DIRS&lt;/td&gt;
&lt;td&gt;데이터베이스에 넣지 않을 경로를 glob 목록으로 지정. 구분자는 리눅스&amp;middot;macOS&amp;middot;BSD가 콜론, 윈도우가 세미콜론. 기본값은 $HOME&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_FZF_OPTS&lt;/td&gt;
&lt;td&gt;인터랙티브 선택 시 fzf에 넘길 옵션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_MAXAGE&lt;/td&gt;
&lt;td&gt;에이징 기준값. 기본 10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_ZO_RESOLVE_SYMLINKS&lt;/td&gt;
&lt;td&gt;1로 두면 심볼릭 링크를 실제 경로로 풀어서 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# ~/.zshrc &amp;mdash; init 보다 위에 배치
export _ZO_ECHO=1
export _ZO_EXCLUDE_DIRS=&quot;$HOME:$HOME/private/*:/tmp/*&quot;
export _ZO_RESOLVE_SYMLINKS=1
export _ZO_FZF_OPTS=&quot;--height 40% --reverse&quot;

eval &quot;$(zoxide init zsh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 손대볼 만한 건 _ZO_EXCLUDE_DIRS와 _ZO_RESOLVE_SYMLINKS 정도예요. 앞쪽은 임시 폴더나 민감한 경로가 기록에 남지 않게 막아요. 뒤쪽은 심링크로 여러 이름을 가진 폴더가 서로 다른 항목으로 중복 집계되는 상황을 정리해요. 다만 문서가 짚듯 제외 목록을 설정한 뒤에는 이미 들어가 있던 항목을 &lt;code&gt;zoxide remove&lt;/code&gt;로 따로 지워야 해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰던 z나 autojump 기록은 옮겨올 수 있어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 다른 점프 도구를 쓰고 있었다면 데이터베이스를 가져오는 서브커맨드가 있어요. &lt;code&gt;--from&lt;/code&gt;에 autojump나 z를 지정해요. z 포맷은 fasd&amp;middot;z&amp;middot;z.lua&amp;middot;zsh-z까지 커버해요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# z 계열에서 가져오기
&amp;gt; zoxide import --from z &quot;$HOME/.z&quot;

# autojump 에서 가져오기 (경로만 넘어옴)
&amp;gt; zoxide import --from autojump &quot;$HOME/.local/share/autojump/autojump.txt&quot;

# 기존 데이터베이스에 합치기
&amp;gt; zoxide import --from z &quot;$HOME/.z&quot; --merge&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 제약이 문서에 명시돼 있어요. 기본 동작은 현재 데이터베이스가 비어 있을 때만 성공해요. 이미 내용이 있으면 &lt;code&gt;--merge&lt;/code&gt;를 붙여야 해요. 나머지 하나는 autojump 쪽 제약이에요. 경로만 넘어오고 점수는 넘어오지 않아요. 매칭 알고리즘이 너무 달라 점수를 그대로 옮기지 않는다는 설명이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 함정이 몇 가지 있어요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;init 줄은 설정 파일 맨 끝&lt;/b&gt; &amp;mdash; 다른 셸 플러그인이 프롬프트 훅을 건드리는 경우가 있어서 문서가 위치를 지정하고 있어요. 중간에 넣어 두면 훅이 덮여 점수가 안 쌓이는 상황이 생겨요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 변수는 init 위에&lt;/b&gt; &amp;mdash; init 이후에 export하면 그 세션에서는 반영되지 않아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처음 며칠은 효과가 거의 없어요&lt;/b&gt; &amp;mdash; 방문 기록이 있어야 점프가 되니 초반에는 평소대로 이동해 데이터베이스를 채우는 기간이 필요해요. 급하다면 zoxide add로 자주 가는 경로를 미리 넣어두는 방법이 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마지막 조각 규칙&lt;/b&gt; &amp;mdash; 목적지 폴더 이름의 일부를 마지막 키워드로 줘야 해요. 중간 경로 이름만으로는 매칭되지 않아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;--cmd cd 는 셸을 가려요&lt;/b&gt; &amp;mdash; cd를 대체하는 옵션은 Nushell과 POSIX 셸에서는 동작하지 않는다고 문서에 적혀 있어요. cd를 바꾸면 스크립트나 다른 도구가 기대하는 동작과 어긋날 여지도 있어요. 처음에는 z를 그대로 쓰다가 나중에 판단하는 편이 무난해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fzf 버전&lt;/b&gt; &amp;mdash; zi와 인터랙티브 선택은 fzf v0.51.0 이상을 요구해요. 배포판 패키지가 오래된 버전을 주는 경우가 있어요. zi가 이상하게 동작하면 fzf 버전부터 확인하는 순서가 빨라요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 셸을 오가면 초기화도 각각&lt;/b&gt; &amp;mdash; 데이터베이스는 공유되지만 init 줄은 셸별 설정 파일에 각각 넣어야 해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 폴더를 반복해 오가는 사람에게 맞아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zoxide가 유리한 조건은 뚜렷해요. 레포지토리나 서비스 폴더가 여러 개이고 경로가 깊으며 그중 일부를 매일 반복해 오가는 경우예요. 반대로 프로젝트가 한두 개뿐이거나 이미 별칭 몇 개로 충분히 굴러가는 환경이라면 굳이 도구를 하나 더 얹을 이유는 크지 않아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 순서는 짧아요. 패키지 매니저로 설치하고 셸 설정 파일 맨 끝에 init 한 줄을 넣은 다음, 며칠 평소대로 돌아다니며 기록을 쌓는 것까지가 전부예요. 그 뒤에 &lt;code&gt;zoxide query --list --score&lt;/code&gt;로 랭킹이 어떻게 잡혔는지 한 번 보면 돼요. 기록에 남기고 싶지 않은 경로가 있으면 _ZO_EXCLUDE_DIRS를 손보고요. 기존에 z나 autojump를 쓰고 있었다면 import로 기록을 옮긴 뒤 예전 도구의 초기화 줄을 지우는 순서예요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;p class=&quot;yt-sources__head&quot; data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/ajeetdsouza/zoxide&quot;&gt;GitHub &amp;mdash; ajeetdsouza/zoxide (README&amp;middot;설치)&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/ajeetdsouza/zoxide/blob/main/man/man1/zoxide.1&quot;&gt;man zoxide(1) &amp;mdash; 환경 변수&amp;middot;에이징&amp;middot;frecency&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/ajeetdsouza/zoxide/blob/main/man/man1/zoxide-init.1&quot;&gt;man zoxide-init(1) &amp;mdash; 셸별 초기화&amp;middot;옵션&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/ajeetdsouza/zoxide/wiki/Algorithm&quot;&gt;zoxide Wiki &amp;mdash; 매칭&amp;middot;에이징 알고리즘&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 직접 사용기가 아니라 zoxide 공식 저장소의 README&amp;middot;man page&amp;middot;위키 문서를 근거로 정리한 글이에요. 명령&amp;middot;옵션&amp;middot;기본값은 작성 시점 문서 기준이며 이후 릴리스에서 달라질 수 있어요.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/터미널 &amp;amp; 환경</category>
      <category>CLI</category>
      <category>rust</category>
      <category>zoxide</category>
      <category>생산성</category>
      <category>셸</category>
      <category>터미널</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/597</guid>
      <comments>https://jessyt.tistory.com/597#entry597comment</comments>
      <pubDate>Mon, 10 Aug 2026 06:00:34 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 8월 14일부터 기본이 auto 모드예요</title>
      <link>https://jessyt.tistory.com/595</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/oEYEq/dJMcabSHAiA/AAAAAAAAAAAAAAAAAAAAAKi-X_JKwWaWJOrhWOG96KuDNoHPGuVamIs_MFYGTdGw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=PRJ%2BeF0nunLI3BWKx4EqTjWMMVU%3D&quot; alt=&quot;[Claude Code] 8월 14일부터 기본이 auto 모드예요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 8월 14일부터 기본이 auto 모드예요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code의 기본 권한 모드가 바뀌어요. Anthropic이 8월 14일부터 Pro&amp;middot;Max&amp;middot;Team 플랜 새 세션의 기본값을 auto 모드로 돌린다고 공지했어요. 매번 뜨던 승인창 대신 별도 분류기가 tool call을 검사하는 방식이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 기본값을 바꿔둔 사람에겐 전환 여부를 한 번 물어봐요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 대상은 8월 14일 이후 새로 여는 세션이에요. 이미 본인이 기본 모드를 따로 지정해뒀다면 그 설정이 그대로 유지되고, auto로 바꿀지 묻는 일회성 프롬프트가 뜰 수 있다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조직이 관리 설정(managed settings)으로 기본값을 정해둔 경우는 이번 변경과 무관하게 그대로예요. Team&amp;middot;Enterprise 관리자는 managed settings에서 permissions.disableAutoMode를 &quot;disable&quot;로 두면 조직 단위로 auto 모드를 끌 수 있다고 문서에 나와 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 승인창 대신 분류기가 tool call을 판정해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto 모드는 되돌릴 수 없거나, 파괴적이거나, 사용자 환경 밖을 향하는 행동을 분류기가 막는 구조예요. 분류기가 거절하면 Claude가 대개 다른 안전한 방법을 찾거나, 사용자에게 직접 승인을 요청한다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 permissions 룰이 없어지는 건 아니에요. deny와 ask 룰은 분류기보다 먼저 평가돼서, deny는 분류기가 보기도 전에 차단하고 ask는 auto 모드에서도 프롬프트를 띄워요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 발표 기준 위험 명령 차단률은 89%라고 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유료 테스터 1,053명을 대상으로 한 비교에서 사람이 위험한 명령을 잡아낸 비율은 13.6%, auto 모드는 89%였다는 게 Anthropic 설명이에요. 외부 레드팀 테스트에서는 하드닝 뒤 miss rate가 12%에서 7%로 내려갔다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 인젝션은 제3자인 Trajectory Labs에 의뢰한 평가에서 72개 시나리오를 10회씩 돌린 720회 공격 중 Fable 5&amp;middot;Opus 5&amp;middot;Sonnet 5의 auto 모드를 뚫은 게 하나도 없었다고 해요. 다만 이 수치는 전부 발표 자료 기준이라, 실제 체감은 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. push 앞에 확인창을 두려면 ask 룰을 박아두세요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모드 전환 자체는 CLI에서 Shift+Tab, 데스크톱 앱은 드롭다운으로 언제든 가능해요. auto 모드는 작업 중인 repo의 어느 브랜치로든 push와 PR 생성을 기본 허용해요(v2.1.211부터). production&amp;middot;release&amp;middot;gh-pages처럼 배포 대상으로 읽히는 기본 브랜치 외 브랜치는 분류기가 따로 판정해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push나 PR 전에 사람 확인을 꼭 끼우고 싶으면 settings에 ask 룰을 넣는 방법이 문서에 안내돼 있어요.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;permissions&quot;: {
    &quot;ask&quot;: [
      &quot;Bash(git push *)&quot;,
      &quot;Bash(gh pr create *)&quot;
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두면 나머지는 auto 모드로 굴러가면서 push 순간에만 프롬프트가 떠요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 사내 GitHub&amp;middot;내부 도메인은 미리 등록해야 안 막혀요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분류기가 기본으로 신뢰하는 범위는 작업 디렉터리와 현재 repo의 리모트까지예요. 회사 소스컨트롤 org에 push하거나 팀 클라우드 버킷에 쓰는 작업은 autoMode.environment에 등록하기 전까지 막힌다고 문서에 명시돼 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 형식은 regex가 아니라 자연어 문장이에요. &quot;Source control: github.example.com/acme-corp and all repos under it&quot; 같은 식으로 적어두면 분류기가 그 범위를 내부로 읽어요. 사내 레지스트리&amp;middot;CI 호스트를 쓰는 팀이라면 전환 전에 한 번 채워두는 편이 안전해요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;claude auto-mode defaults   # 기본 룰 확인
claude auto-mode config     # 사용자 설정이 반영된 최종 룰 확인
claude auto-mode reset      # 커스텀 초기화 (v2.1.212+)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;environment&amp;middot;allow&amp;middot;soft_deny&amp;middot;hard_deny 배열에 &quot;$defaults&quot; 문자열을 빼고 쓰면 해당 항목의 기본 룰이 통째로 교체돼요. force push나 데이터 유출 차단 같은 내장 룰까지 사라지니 주의가 필요한 부분이에요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://claude.com/blog/auto-mode-default-in-claude-code&quot;&gt;Anthropic &amp;mdash; Auto mode is now the default in Claude Code for Pro, Max, and Team plans&lt;/a&gt;, &lt;a href=&quot;https://code.claude.com/docs/en/auto-mode-config&quot;&gt;Claude Code Docs &amp;mdash; Configure auto mode&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI코딩</category>
      <category>anthropic</category>
      <category>AUTO모드</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>ClaudeCode설정</category>
      <category>권한관리</category>
      <category>권한모드</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/595</guid>
      <comments>https://jessyt.tistory.com/595#entry595comment</comments>
      <pubDate>Sun, 9 Aug 2026 07:00:17 +0900</pubDate>
    </item>
    <item>
      <title>[Claude] Managed Agents 세션에 달러 상한이 생겼어요</title>
      <link>https://jessyt.tistory.com/594</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/XslTv/dJMcaaGmO33/AAAAAAAAAAAAAAAAAAAAALlFHjnd6YUVwyvsnWTpQSwFvrr0A8UeWz-P66__VNeK/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=1vjY7RDRY1OvxMQGwsfJ7XPXZx4%3D&quot; alt=&quot;[Claude] Managed Agents 세션에 달러 상한이 생겼어요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude] Managed Agents 세션에 달러 상한이 생겼어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트 세션에 달러 하드 캡이 생겼어요. Anthropic이 현지시간 8월 7일 Claude Managed Agents에 세션 예산을 추가했어요. 정해둔 금액에 닿으면 세션이 새 모델 요청을 더 보내지 않고 멈춰요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 센트 단위 문자열로 상한을 걸어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 만들 때 budget 필드를 같이 넘기는 방식이에요. type은 항상 limit이에요. max_list_cost의 amount는 달러가 아니라 미국 센트를 문자열로 적어요. &quot;2500&quot;이 25달러, &quot;50&quot;이 50센트예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;25.00&quot; 같은 소수 표기는 400 에러로 거부돼요. 금액에 부동소수점 반올림이 끼지 않게 하려고 숫자가 아닌 문자열로 받는다고 문서에 나와 있어요. currency는 USD만 지원해요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;curl https://api.anthropic.com/v1/sessions \
  -H &quot;x-api-key: $ANTHROPIC_API_KEY&quot; \
  -H &quot;anthropic-version: 2023-06-01&quot; \
  -H &quot;anthropic-beta: managed-agents-2026-04-01&quot; \
  -H &quot;content-type: application/json&quot; \
  -d '{
    &quot;agent&quot;: &quot;AGENT_ID&quot;,
    &quot;environment_id&quot;: &quot;ENVIRONMENT_ID&quot;,
    &quot;budget&quot;: {
      &quot;type&quot;: &quot;limit&quot;,
      &quot;max_list_cost&quot;: {&quot;amount&quot;: &quot;2500&quot;, &quot;currency&quot;: &quot;USD&quot;}
    }
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상한은 세션을 만들 때만 붙일 수 있어요. 예산 없이 시작한 세션에 나중에 예산을 추가하는 요청은 400으로 막혀요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 상한에 닿아도 세션이 죽지는 않아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상한에 도달한 세션은 종료가 아니라 idle 상태로 멈춰요. stop_reason에는 budget_reached가 찍혀요. 대화 기록과 샌드박스는 다른 idle 세션과 똑같이 보존돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예산을 올리거나 없애면 멈춰 있던 작업이 자동으로 이어져요. 클라이언트가 따로 재개를 호출할 필요는 없다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멈춰 있는 동안에는 새 작업을 시작하는 이벤트가 400으로 거부돼요. 도구 승인&amp;middot;도구 결과&amp;middot;커스텀 도구 결과처럼 이미 진행 중이던 걸 매듭짓는 이벤트만 받아요. 인터럽트도 받기는 하지만 상한에 멈춘 세션에서는 아무 일도 하지 않는다고 문서에 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 주의할 점은 예산 제거가 일방향이라는 거예요. 한 번 null로 지운 세션에는 다시 상한을 걸 방법이 없어요. 캡을 계속 두려면 제거 대신 금액 변경을 써야 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 토큰뿐 아니라 실행 시간과 웹 검색도 계산돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상한과 비교하는 값은 세션의 list cost예요. 모델 토큰은 각 모델 공식 단가로, 웹 검색은 1,000회당 10달러, 세션이 떠 있는 시간은 시간당 0.08달러로 계속 더해져요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점도 있어요. 세션은 정가 기준 합계가 상한에 닿을 때 멈춰요. 조직에 별도 할인 계약이 있다면 그 시점의 실제 청구액은 상한에 못 미쳐요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Messages API의 task budgets와는 다른 기능이에요. 그쪽은 모델이 스스로 참고하는 토큰 단위 권고치예요. 세션 예산은 플랫폼이 강제하는 달러 하드 캡이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 상한을 한 요청만큼은 넘길 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정은 모델 요청 사이에서 이뤄져요. 요청을 보내기 전에 누적 금액을 확인해요. 이미 상한에 닿았으면 다음 요청부터 멈추는 구조예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 캡을 넘기게 만든 그 요청 자체는 끝까지 실행돼요. 50센트 상한을 건 세션이 53센트에서 멈출 수 있다고 문서에 예시가 나와 있어요. 초과 폭은 스레드당 요청 한 번으로 제한돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 정지선이 아니라 새 작업을 막는 경계로 봐야 해요. 금액은 요청 한 번 분의 여유를 두고 잡는 편이 안전해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 예약 실행에도 같은 상한이 복사돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deployment에도 같은 budget 객체를 넘길 수 있어요. 이때 상한은 deployment의 누적 지출이 아니라 거기서 시작하는 세션 하나하나에 복사돼요. 매일 도는 잡이라면 실행 한 번마다 캡이 걸리는 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이나 배포 엔드포인트를 호출할 때는 managed-agents-2026-04-01 베타 헤더가 필요해요. 메모리 스토어 엔드포인트만 agent-memory-2026-07-22를 써요. 공식 SDK는 맞는 헤더를 자동으로 붙여준다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 날 Managed Agents에는 세션 중간에 에이전트와 동급 이상인 모델에게 전략을 물어보는 advisor 설정, 추론이 도는 지역을 지정하는 inference_geo, 마운트한 GitHub 저장소 루트의 .claude/skills를 세션 시작 때 자동으로 읽어오는 기능도 함께 올라왔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://platform.claude.com/docs/en/release-notes/api&quot;&gt;Claude Platform release notes (2026년 8월 7일)&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/managed-agents/budgets&quot;&gt;Session budgets 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI에이전트</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudeapi</category>
      <category>ManagedAgents</category>
      <category>기능출시</category>
      <category>비용관리</category>
      <category>세션예산</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/594</guid>
      <comments>https://jessyt.tistory.com/594#entry594comment</comments>
      <pubDate>Sat, 8 Aug 2026 07:00:40 +0900</pubDate>
    </item>
    <item>
      <title>[분산시스템] 아웃박스 패턴: DB 커밋과 Kafka 발행을 원자적으로 묶는 법</title>
      <link>https://jessyt.tistory.com/505</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl1hve/dJMcacQSe8p/heYcWFrBZbCMAk1N4WPWsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl1hve/dJMcacQSe8p/heYcWFrBZbCMAk1N4WPWsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl1hve/dJMcacQSe8p/heYcWFrBZbCMAk1N4WPWsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl1hve%2FdJMcacQSe8p%2FheYcWFrBZbCMAk1N4WPWsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[분산시스템] 아웃박스 패턴: DB 커밋과 Kafka 발행을 원자적으로 묶는 법&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[분산시스템] 아웃박스 패턴: DB 커밋과 Kafka 발행을 원자적으로 묶는 법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;주문을 저장하고 카프카로 이벤트를 발행한다&quot; &amp;mdash; 이 흔한 두 줄에 분산 시스템의 고전 문제가 숨어 있어요. DB와 카프카는 별개 시스템이라 &lt;b&gt;하나의 트랜잭션으로 묶을 수 없거든요.&lt;/b&gt; 둘 사이 어느 틈에서든 한쪽만 성공하면 데이터가 어긋나요. 이걸 구조적으로 푸는 게 아웃박스 패턴이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중 쓰기 문제에서 시작해, 아웃박스의 동작, 릴레이 구현 두 방식(폴링 vs CDC), 그리고 따라오는 조건(컨슈머 멱등성)을 하나씩 풀어요. &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;스프링 이벤트 편&lt;/a&gt; 말미의 &quot;커밋됐으면 반드시 처리돼야 하는 메시지&quot;의 답이기도 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 이중 쓰기 문제&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi3VSZ/dJMcaaFBpnM/8WknN5O0GnVPK0g0OSQK2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi3VSZ/dJMcaaFBpnM/8WknN5O0GnVPK0g0OSQK2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi3VSZ/dJMcaaFBpnM/8WknN5O0GnVPK0g0OSQK2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi3VSZ%2FdJMcaaFBpnM%2F8WknN5O0GnVPK0g0OSQK2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;이중 쓰기 문제. DB 커밋 후 카프카 발행이 실패하면 주문은 있는데 이벤트가 없고, 발행 후 커밋이 실패하면 유령 이벤트가 나간다. 어느 순서든 두 시스템 사이 틈에서 불일치가 생긴다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;순서를 어떻게 잡아도 틈이 있어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커밋 &amp;rarr; 발행&lt;/b&gt; 순서: 커밋 직후 서버가 죽거나 발행이 실패하면, 주문은 있는데 이벤트가 없어요. 정산&amp;middot;알림&amp;middot;재고 시스템이 그 주문을 영영 몰라요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행 &amp;rarr; 커밋&lt;/b&gt; 순서: 발행은 됐는데 트랜잭션이 롤백되면, 존재하지 않는 주문의 유령 이벤트가 퍼져요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;a href=&quot;https://jessyt.tistory.com/459&quot;&gt;카프카 트랜잭션&lt;/a&gt;으로 묶으면 되지 않나?&quot;라는 생각이 들 수 있는데, 그건 카프카 토픽 사이(consume-process-produce)의 원자성이에요. &lt;b&gt;DB와 카프카&lt;/b&gt;를 가로지르는 원자성은 카프카 트랜잭션도 못 줘요. 서로 다른 시스템의 커밋을 묶는 분산 트랜잭션(2PC)은 비용&amp;middot;복잡도 때문에 현대 아키텍처에서 거의 안 쓰고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 아웃박스, 문제를 한 시스템 안으로 가져온다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발상의 전환이에요. &quot;두 시스템에 쓰기&quot;를 포기하고 &lt;b&gt;&quot;한 DB에 두 번 쓰기&quot;&lt;/b&gt;로 바꿔요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yu7NF/dJMcaiwOGwo/5NbOjKUSl541RD0bfpxeVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yu7NF/dJMcaiwOGwo/5NbOjKUSl541RD0bfpxeVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yu7NF/dJMcaiwOGwo/5NbOjKUSl541RD0bfpxeVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYu7NF%2FdJMcaiwOGwo%2F5NbOjKUSl541RD0bfpxeVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;아웃박스 패턴 구조. 주문 테이블과 outbox 테이블 INSERT를 하나의 DB 트랜잭션으로 묶어 같이 커밋되거나 같이 롤백되게 하고, 별도 릴레이가 outbox의 미발행분을 읽어 카프카로 발행한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void createOrder(OrderCommand cmd) {
    Order order = orderRepository.save(Order.from(cmd));       // ① 비즈니스 데이터
    outboxRepository.save(OutboxEvent.of(                       // ② 발행할 이벤트
        &quot;ORDER_CREATED&quot;, order.getId(), toJson(order)));
    // 둘 다 같은 DB, 같은 트랜잭션 &amp;mdash; 같이 커밋되거나 같이 롤백
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 카프카로 바로 보내는 대신, 같은 DB의 &lt;code&gt;outbox&lt;/code&gt; 테이블에 INSERT해요. 주문과 이벤트가 한 트랜잭션이니 &lt;b&gt;불일치가 구조적으로 불가능&lt;/b&gt;해져요 &amp;mdash; 롤백되면 이벤트도 같이 사라지고, 커밋되면 이벤트도 반드시 남아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 별도의 &lt;b&gt;메시지 릴레이&lt;/b&gt;가 outbox의 미발행 행을 읽어 카프카로 발행하고 발행 표시를 해요. 발행이 실패해도 행은 남아 있으니 다음 주기에 재시도돼요. &quot;커밋된 이벤트는 언젠가 반드시 발행된다&quot;가 보장되는 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 릴레이 구현, 폴링 vs CDC&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9geb/dJMcaaFBpnN/HVgkvvTb0rwwLMwPukvjkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9geb/dJMcaaFBpnN/HVgkvvTb0rwwLMwPukvjkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9geb/dJMcaaFBpnN/HVgkvvTb0rwwLMwPukvjkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9geb%2FdJMcaaFBpnN%2FHVgkvvTb0rwwLMwPukvjkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;아웃박스 릴레이 두 방식. 폴링 퍼블리셔는 주기적으로 outbox를 조회해 발행하는 단순한 방식이고, CDC는 Debezium으로 DB 변경 로그를 구독해 즉시 발행한다. 둘 다 at-least-once라 컨슈머 멱등성이 전제다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴링 퍼블리셔 &amp;mdash; 단순하게 시작&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Scheduled(fixedDelay = 1000)
@Transactional   // markPublished 변경분이 커밋되게
public void relay() {
    List&amp;lt;OutboxEvent&amp;gt; events = outboxRepository.findTop100ByPublishedFalseOrderById();
    for (OutboxEvent e : events) {
        kafkaTemplate.send(e.getTopic(), e.getAggregateId(), e.getPayload())
            .get(5, TimeUnit.SECONDS);   // 브로커 ack까지 확인하고
        e.markPublished();               // 그 다음에 발행 표시 (실패하면 표시 안 됨 = 다음 주기 재시도)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄러로 주기 조회 &amp;rarr; 발행 &amp;rarr; 표시. 인프라 추가 없이 바로 만들 수 있어요. 폴링 주기만큼의 발행 지연과 DB 폴링 부하가 비용이에요. 대부분의 규모에서는 이걸로 충분해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDC(Debezium) &amp;mdash; 규모가 커지면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 변경 로그(MySQL binlog)를 Debezium이 구독해서 outbox INSERT를 감지하는 즉시 카프카로 흘려보내요. 지연이 최소화되고 애플리케이션&amp;middot;DB에 폴링 부하가 없어요. 대신 Kafka Connect 인프라 운영이 추가돼요. &quot;지연에 민감하거나 outbox 트래픽이 큰&quot; 단계에서 넘어가는 카드예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;어느 방식이든 전달 보장은 &lt;b&gt;at-least-once&lt;/b&gt;예요. 발행 성공 후 &quot;표시&quot; 전에 릴레이가 죽으면 같은 이벤트가 재발행돼요. 그래서 아웃박스는 &lt;b&gt;컨슈머 멱등성과 한 세트&lt;/b&gt;예요 &amp;mdash; outbox 행의 고유 ID를 이벤트에 실어 보내고, 컨슈머가 그 ID로 중복을 흡수해요(&lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;컨슈머 멱등성&lt;/a&gt;에서 본 그 방법들). 멱등 컨슈머 없이 아웃박스만 깔면 &quot;유실은 막았는데 중복으로 터지는&quot; 반쪽이 돼요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 운영 디테일&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;outbox 정리&lt;/b&gt; &amp;mdash; 발행 완료 행은 계속 쌓여요. 보관 기간(예: 7일)을 두고 배치로 지워요. 미발행 행의 나이를 모니터링하면 &quot;릴레이가 멈췄다&quot;를 바로 잡아요 &amp;mdash; 이 지표 하나는 꼭 알림에 걸어두세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서&lt;/b&gt; &amp;mdash; 같은 애그리거트(같은 주문)의 이벤트 순서가 중요하면, outbox의 aggregate ID를 &lt;a href=&quot;https://jessyt.tistory.com/461&quot;&gt;카프카 메시지 키&lt;/a&gt;로 써서 같은 파티션으로 보내요. 릴레이도 ID 순서대로 발행하고요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스프링 이벤트와의 조합&lt;/b&gt; &amp;mdash; 서비스 코드를 깔끔히 유지하려면, &lt;code&gt;@TransactionalEventListener(BEFORE_COMMIT)&lt;/code&gt;에서 outbox INSERT를 하는 식으로 &quot;이벤트 발행처럼 쓰되 실제론 outbox에 쌓이는&quot; 구조도 좋아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 어디까지 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 이벤트에 아웃박스를 깔 필요는 없어요. 기준은 &lt;b&gt;&quot;커밋과 이벤트의 불일치를 감당할 수 있는가&quot;&lt;/b&gt;예요. 캐시 무효화 신호나 가벼운 알림은 유실돼도 복구 가능하니 &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;AFTER_COMMIT 이벤트&lt;/a&gt;(인메모리)로 충분해요. 정산&amp;middot;재고&amp;middot;타 시스템 연동처럼 &quot;주문이 있는데 이벤트가 없으면 사고&quot;인 흐름이 아웃박스의 자리예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주문은 있는데 이벤트가 안 나갔어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아웃박스 도입 전이면 이중 쓰기의 그 틈이에요. 도입 후라면 릴레이 중단을 의심 &amp;mdash; 미발행 행 나이 지표를 봐요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 이벤트가 두 번 와요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;at-least-once의 정상 동작이에요. 컨슈머가 이벤트 ID 기반 멱등 처리를 하는지 확인해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;outbox 테이블이 비대해져요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행 완료분 정리 배치가 없는 거예요. 보관 기간 + 정리 배치를 추가해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 시스템, 한 트랜잭션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중 쓰기 문제의 본질은 &lt;b&gt;&quot;두 시스템은 한 트랜잭션으로 못 묶는다&quot;&lt;/b&gt;예요. 아웃박스는 이걸 비틀어 풀어요 &amp;mdash; 카프카로 바로 쏘는 대신 같은 DB의 outbox 테이블에 이벤트를 같이 커밋하고, 릴레이(폴링 또는 CDC)가 그걸 카프카로 옮겨요. 그러면 유실은 구조적으로 사라지고, 남는 건 at-least-once의 중복뿐이라 &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;컨슈머 멱등성&lt;/a&gt;으로 흡수해요. 이 한 세트가 &quot;커밋됐으면 반드시 전달된다&quot;의 실무 표준이고, &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;스프링 이벤트 편&lt;/a&gt; 말미에 미뤄둔 &quot;반드시 처리돼야 하는 메시지&quot;의 답이기도 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이벤트가 여러 서비스를 가로지르는 흐름 전체의 정합성은 어떻게 지킬까요. &lt;a href=&quot;https://jessyt.tistory.com/506&quot;&gt;사가 패턴&lt;/a&gt;이 그 위에 올라타요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot;&gt;microservices.io &amp;mdash; Transactional Outbox&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html&quot;&gt;Debezium &amp;mdash; Outbox Event Router&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/분산 &amp;amp; 운영</category>
      <category>CDC</category>
      <category>Debezium</category>
      <category>Kafka</category>
      <category>outbox</category>
      <category>백엔드</category>
      <category>분산시스템</category>
      <category>아웃박스 패턴</category>
      <category>이중 쓰기</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/505</guid>
      <comments>https://jessyt.tistory.com/505#entry505comment</comments>
      <pubDate>Fri, 7 Aug 2026 07:30:40 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 2.1.223, 승인창을 속이던 명령을 막았어요</title>
      <link>https://jessyt.tistory.com/592</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/cIryCy/dJMcadbPm44/AAAAAAAAAAAAAAAAAAAAAN74z6uf9HcHjnwc1lyh0AxctcfFDbRkQvqXFkjHZyIZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=oeWYBXPzg0EEeQUVh8AOTo8FvSQ%3D&quot; alt=&quot;[Claude Code] 2.1.223, 승인창을 속이던 명령을 막았어요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 2.1.223, 승인창을 속이던 명령을 막았어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 2.1.222와 2.1.223은 새 기능보다 권한&amp;middot;격리 경계의 구멍을 메운 쪽에 무게가 실린 릴리즈예요. 두 버전이 8월 4일과 6일에 연달아 올라왔고, 공식 changelog 기준으로 Bash 권한 검사, 승인 프롬프트, 워크트리 격리, PreToolUse 훅, 워크플로 샌드박스가 각각 우회 가능했다고 적혀 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 먼저 확인할 건 버전 번호 하나예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 수정은 2.1.222와 2.1.223에 나뉘어 들어가 있어요. 둘 다 반영하려면 2.1.223 이상이면 돼요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;claude --version
claude update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog에는 위험도 등급이나 악용 사례가 따로 적혀 있지 않아요. 실제 영향 범위가 어디까지인지는 각자 쓰는 설정 기준으로 판단할 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 승인창에 명령 전체가 보이지 않았어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.223에는 명령 승인 경로와 얽힌 수정이 두 건 올라왔어요. 하나는 특정하게 조립한 명령이 권한 검사에서 자기 일부를 숨길 수 있던 문제라고 적혀 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 승인 대화상자 쪽이에요. 탭 문자나 눈에 안 보이는 유니코드로 채운 명령이 승인창에서 일부를 가릴 수 있었다고 해요. 승인 버튼을 누를 때 화면에 보이는 것과 실제로 실행되는 것이 달랐다는 뜻으로 읽혀요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비가시 문자는 터미널에서 그대로 렌더링되지 않는 종류라, 붙여넣은 명령을 눈으로만 훑는 방식으로는 걸러내기 어려운 유형이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 워크트리 격리가 원본 체크아웃까지 열려 있었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.222 항목 중에는 워크트리로 격리한 세션과 그 서브에이전트가 메인 체크아웃을 상대로 파괴적인 git 명령을 실행할 수 있던 문제가 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog에는 수정 후 격리가 모든 세션 종류에서 파일 편집과 Bash 양쪽에 적용된다고 적혀 있어요. 범위에 서브에이전트가 포함됐다는 점도 같이 명시돼 있고요. 격리를 믿고 위험한 작업을 워크트리 안에서 돌리던 사용법이라면 버전 확인이 특히 필요한 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 자동 허용 훅과 조직 정책도 우회 대상이었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 2.1.222에서 PreToolUse 자동 허용 훅이 백그라운드 에이전트 작업에서 도구 제한을 지나칠 수 있던 문제가 잡혔어요. changelog에는 요약, compaction, 이름 변경처럼 사용자가 직접 부르지 않는 내부 작업이 해당된다고 적혀 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.223에는 권한 경계와 얽힌 두 건이 더 있어요. 하나는 에이전트 정의의 bypassPermissions 모드가 조직의 bypass-permissions 비활성화 정책을 무시하던 문제, 다른 하나는 워크플로 스크립트가 동적 import()로 워크플로 샌드박스 밖의 코드를 실행할 수 있던 문제예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞쪽은 조직 관리 설정, 뒤쪽은 샌드박스 격리에 걸린 사안이에요. 관리 설정으로 권한을 통제하는 팀이라면 이번 두 버전은 기능 업데이트라기보다 정책이 실제로 지켜지는지에 대한 수정에 가까워요. 팀 배포 환경이라면 구성원 버전이 모두 2.1.223 이상인지까지가 확인 대상이고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://code.claude.com/docs/en/changelog&quot;&gt;Claude Code changelog &amp;mdash; 2.1.222, 2.1.223&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>권한관리</category>
      <category>보안패치</category>
      <category>샌드박스</category>
      <category>업데이트</category>
      <category>워크트리</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/592</guid>
      <comments>https://jessyt.tistory.com/592#entry592comment</comments>
      <pubDate>Fri, 7 Aug 2026 07:00:22 +0900</pubDate>
    </item>
    <item>
      <title>[Notion] 노트 앱이 아니라 데이터베이스입니다</title>
      <link>https://jessyt.tistory.com/593</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/N9901/dJMcahyDG5B/AAAAAAAAAAAAAAAAAAAAAH285N4pEmaHcjXXMkQFbiYTv6ycmgupG2JHTXtwZOR3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=cGOpWWTvDgWTUbltDFiLU5NI9dU%3D&quot; alt=&quot;[Notion] 노트 앱이 아니라 데이터베이스입니다&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 노트 &amp;amp; 지식관리&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Notion] 노트 앱이 아니라 데이터베이스입니다&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;Notion은 문서 작성 도구로 알려져 있지만, 실제 정체는 &quot;모든 행이 페이지인 데이터베이스&quot;를 중심에 둔 워크스페이스예요. 문서&amp;middot;위키&amp;middot;스프레드시트&amp;middot;간단한 이슈 트래커를 하나로 합치는 자리를 노리는 도구죠. 이 글에서는 데이터베이스라는 핵심 개념, 처음 세팅하는 순서, 뷰&amp;middot;속성&amp;middot;링크드 뷰 같은 주요 기능, 그리고 REST API로 자동화를 붙이는 방법까지 정리해요. 무료 플랜의 제한 같은 함정도 미리 짚습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심은 &quot;모든 행이 페이지&quot;라는 한 문장이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion 공식 문서는 데이터베이스를 &quot;페이지의 모음(collections of pages)&quot;이라고 정의해요. 스프레드시트의 행 하나는 셀 값의 묶음이지만 Notion 데이터베이스의 행 하나는 그 자체가 열리는 페이지입니다. 표에서 &quot;API 서버 마이그레이션&quot;이라는 행을 클릭하면 그 안에 회의록&amp;middot;체크리스트&amp;middot;코드 블록을 자유롭게 쓰는 문서가 나오는 식이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 기존 도구 조합과의 차이를 만들어요. 문서 도구(Google Docs 류)는 글은 잘 쓰지만 목록을 구조화해 거르기 어렵고, 스프레드시트는 구조화는 되지만 셀 안에 긴 문서를 담지 못하죠. Notion은 두 성질을 한 객체에 넣었습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;문서 도구&lt;/th&gt;
&lt;th&gt;스프레드시트&lt;/th&gt;
&lt;th&gt;Notion 데이터베이스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;긴 글 작성&lt;/td&gt;
&lt;td&gt;강함&lt;/td&gt;
&lt;td&gt;사실상 불가&lt;/td&gt;
&lt;td&gt;행 = 페이지라 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;필터&amp;middot;정렬&amp;middot;그룹&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;강함&lt;/td&gt;
&lt;td&gt;속성 기준으로 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 데이터 다른 화면&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;뷰 전환 (표&amp;middot;보드&amp;middot;캘린더 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 자동화&lt;/td&gt;
&lt;td&gt;도구별 API&lt;/td&gt;
&lt;td&gt;도구별 API&lt;/td&gt;
&lt;td&gt;공식 REST API + 웹훅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 다룬 Obsidian&amp;middot;Logseq 같은 로컬 마크다운 노트와는 방향이 달라요. 그쪽은 &quot;내 파일을 내 디스크에&quot; 두는 개인 지식관리가 중심이고, Notion은 클라우드에 두고 팀이 같이 편집하는 협업 워크스페이스가 중심입니다. 데이터가 로컬 파일로 남지 않는다는 점은 뒤의 함정 섹션에서 다시 짚어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작은 페이지 하나, 데이터베이스 하나면 충분해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가입하고 나서 처음 할 일은 생각보다 단순합니다. 아래 순서대로 하면 빈 워크스페이스에서 동작하는 데이터베이스까지 도달해요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;계정 생성&lt;/b&gt; &amp;mdash; notion.com에서 가입해요. 개인 용도라면 무료 플랜으로 블록 수 제한 없이 시작하면 돼요 (2인 이상 워크스페이스는 제한이 생깁니다, 아래 함정 참고).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지 만들기&lt;/b&gt; &amp;mdash; 사이드바에서 새 페이지를 추가해요. Notion의 모든 콘텐츠는 페이지 안의 &quot;블록&quot;으로 이루어져 있어요. 텍스트&amp;middot;제목&amp;middot;코드&amp;middot;이미지 전부 블록이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 삽입&lt;/b&gt; &amp;mdash; 페이지 본문에서 슬래시(/)를 입력하면 블록 메뉴가 떠요. 여기서 database를 검색해 표 형태 데이터베이스를 넣습니다. 새로 만들 때 &quot;Build with AI&quot;로 생성하거나 추천(Suggested) 템플릿을 고르는 선택지도 공식적으로 제공돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속성 추가&lt;/b&gt; &amp;mdash; 열 머리글의 + 버튼으로 속성을 추가해요. 날짜&amp;middot;상태&amp;middot;링크 같은 속성을 붙이면 그때부터 필터와 정렬의 기준이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행 열어보기&lt;/b&gt; &amp;mdash; 아무 행이나 클릭해 페이지로 연 다음 본문에 내용을 채워보면 &quot;행 = 페이지&quot; 구조가 바로 이해돼요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축키를 하나만 기억한다면 슬래시(/)예요. 블록 추가, 데이터베이스 삽입, 페이지 링크까지 대부분의 조작이 이 메뉴에서 시작됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 기능은 뷰&amp;middot;속성&amp;middot;링크드 뷰 세 축으로 이해하면 돼요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;뷰 &amp;mdash; 같은 데이터를 여섯 가지 화면으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 데이터베이스를 표(Table)&amp;middot;리스트(List)&amp;middot;보드(Board)&amp;middot;캘린더(Calendar)&amp;middot;갤러리(Gallery)&amp;middot;차트(Chart) 뷰로 바꿔가며 봅니다. 데이터는 하나인데 화면만 바뀌는 구조라 같은 작업 목록을 개발자는 보드(칸반)로 보고 일정 관리는 캘린더로 보는 식의 운용이 가능합니다. 이슈 트래커를 따로 도입하기 애매한 소규모 팀이 보드 뷰를 가볍게 칸반 대용으로 쓰는 이유가 여기 있어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;속성과 필터&amp;middot;정렬&amp;middot;그룹 &amp;mdash; 구조화의 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행에는 날짜&amp;middot;상태&amp;middot;링크 같은 속성(property)이 붙어요. 이 속성이 필터&amp;middot;정렬&amp;middot;그룹의 기준이 되죠. 예를 들어 상태 속성이 &quot;진행 중&quot;인 항목만 거른 뷰, 마감일 순으로 정렬한 뷰를 각각 저장해두는 식이죠. 뷰마다 필터&amp;middot;정렬 설정이 따로 저장되니 &quot;내 것만 보이는 화면&quot;과 &quot;팀 전체 화면&quot;이 같은 데이터베이스 안에서 분리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;링크드 뷰 &amp;mdash; 원본 하나, 화면 여러 곳&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서가 강조하는 기능 중 하나가 링크드 뷰(linked view)예요. 원본 데이터베이스를 복사하지 않고 다른 페이지에 그 데이터베이스를 바라보는 뷰만 심는 기능이에요. 예를 들어 OKR 데이터베이스를 프로젝트 페이지와 태스크 페이지에 각각 링크해두면 어디서 수정해도 원본 하나만 바뀝니다. 부서마다 스프레드시트 사본이 돌아다니다 어긋나는 문제를 구조적으로 막는 방식이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 &amp;mdash; 구조는 잠그고 내용만 열기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업 시에는 &quot;Can edit content&quot; 권한이 유용해요. 이 권한을 받은 사람은 페이지와 속성 값 생성&amp;middot;수정은 되지만 데이터베이스의 구조(스키마) 변경은 막혀요. 팀원이 실수로 속성을 지워 뷰가 전부 깨지는 사고를 권한 단계에서 차단하는 셈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST API와 웹훅으로 외부 자동화까지 이어져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 입장에서 Notion의 무게중심은 공식 REST API예요. 문서 기준으로 페이지&amp;middot;데이터베이스&amp;middot;사용자&amp;middot;코멘트 등 워크스페이스의 거의 모든 대상을 읽고 쓰는 API예요. 웹훅으로 실시간 이벤트 구독도 지원합니다. 인증은 세 갈래예요. 스크립트&amp;middot;CLI용 개인 액세스 토큰(PAT), 단일 워크스페이스 자동화용 내부 연결(internal connection) 토큰, 여러 워크스페이스에 배포하는 앱용 OAuth 2.0.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 패턴은 내부 연결 토큰으로 데이터베이스를 조회하는 거예요. 상태가 &quot;진행 중&quot;인 항목만 가져오는 요청은 이렇게 생겼습니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;curl -X POST 'https://api.notion.com/v1/databases/{database_id}/query' \
  -H 'Authorization: Bearer {integration_token}' \
  -H 'Notion-Version: 2022-06-28' \
  -H 'Content-Type: application/json' \
  --data '{
    &quot;filter&quot;: {
      &quot;property&quot;: &quot;Status&quot;,
      &quot;status&quot;: { &quot;equals&quot;: &quot;진행 중&quot; }
    }
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조합이면 배포 스크립트가 끝날 때 릴리즈 노트 페이지를 자동 생성하거나 모니터링 알림을 데이터베이스 행으로 적재하는 자동화가 별도 도구 없이 붙어요. 연결(connection)은 명시적으로 권한을 부여받아야 해요. 읽기&amp;middot;수정&amp;middot;삽입 능력을 각각 제어하게 설계돼 있습니다.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;API 연결 시 주의&lt;/span&gt; 내부 연결을 만들었다고 바로 데이터가 보이지 않아요. 연결에 해당 페이지&amp;middot;데이터베이스 접근 권한을 명시적으로 부여해야 API 응답에 내용이 나타납니다. &quot;만들었는데 빈 배열만 온다&quot;는 상황은 대부분 이 권한 부여를 빼먹은 경우예요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 함정들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무료 플랜의 블록 제한은 &quot;2인 이상&quot;부터 걸려요.&lt;/b&gt; 혼자 쓰면 블록 무제한이에요. 멤버가 2명 이상인 워크스페이스는 추가 가능한 블록 수가 제한됩니다. 한도에 도달하면 기존 내용 편집은 되지만 새 블록 추가가 막혀요. 팀 도입 전에 유료 전환(Plus 월 14,000원, Business 월 30,000원) 비용을 미리 계산에 넣는 게 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무료 플랜의 나머지 한도&lt;/b&gt; &amp;mdash; 파일 업로드 5MB, 페이지 히스토리 7일, 외부 게스트 10명. 스크린샷을 자주 붙이는 문서라면 5MB 제한이 먼저 걸릴 확률이 높아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 파일이 아니에요.&lt;/b&gt; Obsidian처럼 마크다운 파일이 디스크에 남는 구조가 아니라 클라우드에 데이터가 있어요. 장기 보관이 중요한 문서라면 내보내기(export)를 주기적으로 챙기는 편이 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 과설계 유혹.&lt;/b&gt; 속성과 뷰가 무한정 늘어나는 구조다 보니 정작 내용보다 구조 만들기에 시간을 쓰게 되는 패턴이 흔히 지적돼요. 처음에는 속성 2~3개로 시작해 필요할 때 늘리는 쪽이 유지가 잘 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 사람에게 맞는 도구인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Notion이 맞는 자리는 명확해요. 문서&amp;middot;작업 목록&amp;middot;위키가 여러 도구에 흩어져 있는 소규모 팀, 그리고 전용 이슈 트래커까지는 필요 없지만 칸반 보드와 문서를 한곳에서 굴리고 싶은 경우입니다. API와 웹훅이 공식 지원되니 자동화를 붙이고 싶은 개발자에게도 출발점이 낮은 편이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &quot;내 파일은 내 디스크에&quot; 원칙이 우선이거나 오프라인 환경이 잦다면, 앞서 소개한 Obsidian&amp;middot;Logseq 계열이 더 맞는 편이에요. 시작해본다면 무료 플랜에서 개인 페이지 하나에 데이터베이스 하나를 만들어 일주일쯤 작업 목록을 굴려보는 것, 그게 이 도구의 성격을 파악하는 가장 빠른 길이에요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;p class=&quot;yt-sources__head&quot; data-ke-size=&quot;size16&quot;&gt;Sources&lt;/p&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.notion.com/help/intro-to-databases&quot;&gt;Notion Help &amp;mdash; Intro to databases&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://www.notion.com/pricing&quot;&gt;Notion &amp;mdash; Pricing&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://developers.notion.com/docs/getting-started&quot;&gt;Notion Developers &amp;mdash; Getting started&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 Notion 공식 문서&amp;middot;가격 페이지&amp;middot;개발자 문서를 근거로 정리한 객관 소개 글이고 직접 사용기가 아닙니다. 가격&amp;middot;플랜 한도는 작성 시점(2026-08) 기준이에요.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/노트 &amp;amp; 지식관리</category>
      <category>notion</category>
      <category>노트앱</category>
      <category>지식관리</category>
      <category>협업툴</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/593</guid>
      <comments>https://jessyt.tistory.com/593#entry593comment</comments>
      <pubDate>Fri, 7 Aug 2026 06:00:28 +0900</pubDate>
    </item>
    <item>
      <title>[분산시스템] 멱등 API 설계: Idempotency-Key로 중복 결제 막기</title>
      <link>https://jessyt.tistory.com/504</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Az51F/dJMcaglrIqu/UuPPsz3Y9Y8yKk5qCKrSmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Az51F/dJMcaglrIqu/UuPPsz3Y9Y8yKk5qCKrSmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Az51F/dJMcaglrIqu/UuPPsz3Y9Y8yKk5qCKrSmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAz51F%2FdJMcaglrIqu%2FUuPPsz3Y9Y8yKk5qCKrSmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[분산시스템] 멱등 API 설계: Idempotency-Key로 중복 결제 막기&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi6Hjn%2FdJMcageHWfx%2FeglFRmRFwbiOybU0LpwFTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;578&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;[분산시스템] 멱등 API 설계: Idempotency-Key로 중복 결제 막기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;같은 결제가 두 번 됐어요&quot;는 백엔드에서 가장 아픈 장애예요. 그런데 범인은 버그가 아니라 &lt;b&gt;정상 동작&lt;/b&gt;일 때가 많아요 &amp;mdash; 타임아웃 난 클라이언트가 재시도한 것뿐이거든요. 중복 요청은 분산 시스템에서 막을 수 없는 날씨 같은 거라, &quot;안 오게&quot;가 아니라 &quot;와도 무해하게&quot; 만들어야 해요. 그게 멱등 API예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 요청이 왜 필연인지에서 출발해, Idempotency-Key 패턴의 동작, 동시 중복 레이스 처리, 키&amp;middot;응답 설계 디테일까지 짚어볼게요. 분산 패턴 시리즈의 문을 여는 글이자, &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;카프카 컨슈머 멱등성&lt;/a&gt;의 API 버전이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 중복 요청은 왜 필연인가&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biF56P/dJMcagTjJgB/nYlHdvkQFIwREpXIUTLav0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biF56P/dJMcagTjJgB/nYlHdvkQFIwREpXIUTLav0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biF56P/dJMcagTjJgB/nYlHdvkQFIwREpXIUTLav0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiF56P%2FdJMcagTjJgB%2FnYlHdvkQFIwREpXIUTLav0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;중복 요청 발생 구조. 서버는 결제를 정상 처리했지만 응답이 유실되어 클라이언트가 타임아웃으로 재시도하면 같은 결제가 두 번 처리된다. 재시도는 올바른 동작이라 중복 요청 자체는 막을 수 없다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이 비대칭이에요 &amp;mdash; 타임아웃이 났을 때 클라이언트는 &lt;b&gt;&quot;요청이 처리됐는지&quot; 알 수 없어요.&lt;/b&gt; 안 갔을 수도, 갔는데 응답만 유실됐을 수도 있죠. 안 갔는데 재시도 안 하면 주문이 사라지고, 갔는데 재시도하면 이중 결제예요. 재시도는 해야 하고(가용성), 그러면 중복은 옵니다. 더블클릭, 모바일 네트워크 재전송, 게이트웨이&amp;middot;메시지큐의 at-least-once까지 &amp;mdash; 출처도 다양해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결론은 서버 쪽 책임이에요. &lt;b&gt;같은 요청이 몇 번 와도 결과가 한 번과 같게&lt;/b&gt; &amp;mdash; 멱등하게 만드는 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. Idempotency-Key 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업계 표준 패턴(토스페이먼츠&amp;middot;Stripe 등 결제 API가 다 쓰는 방식)은 단순해요. 클라이언트가 작업 단위마다 고유 키를 만들어 헤더로 보내고 서버는 그 키 기준으로 &quot;한 번만&quot; 처리해요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi6Hjn/dJMcageHWfx/eglFRmRFwbiOybU0LpwFTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi6Hjn%2FdJMcageHWfx%2FeglFRmRFwbiOybU0LpwFTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Idempotency-Key 상태머신. 키 없음에서 SETNX나 유니크 제약으로 원자 선점해 IN_PROGRESS가 되고, 처리 성공 시 응답을 저장하며 COMPLETED가 된다. IN_PROGRESS 중 재시도는 409, COMPLETED 재시도는 저장된 응답 재생이고, 실패는 FAILED로 정책에 따라 재처리하며 서버가 죽으면 TTL로 회수한다&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;578&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;POST /payments
Idempotency-Key: 0f7a6c2e-...   (클라이언트가 작업 단위마다 생성한 UUID)

서버:
  키 첫 등장 &amp;rarr; 결제 처리 &amp;rarr; (키, 응답) 저장 &amp;rarr; 응답
  같은 키 재등장 &amp;rarr; 저장된 응답 그대로 반환 (재처리 X)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트 두 개예요. 첫째, &lt;b&gt;키의 단위는 &quot;작업&quot;&lt;/b&gt;이에요. 사용자가 결제 버튼을 누른 그 시도 하나가 한 키예요. 재시도엔 같은 키, 새로운 주문엔 새 키죠. 클라이언트가 &quot;결제 화면 진입 시 키 생성 &amp;rarr; 재시도 시 재사용&quot;으로 구현해요. 둘째, &lt;b&gt;저장하는 건 처리 여부만이 아니라 응답 자체&lt;/b&gt;예요. 재시도한 클라이언트도 1차와 똑같은 응답(주문번호 포함)을 받아야 흐름이 이어지니까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 함정, 동시 중복은 &quot;확인 후 처리&quot;로 못 막아요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1P1Fb/dJMcadIW2lT/Lb6hgLySrWrFqGswANYTZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1P1Fb/dJMcadIW2lT/Lb6hgLySrWrFqGswANYTZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1P1Fb/dJMcadIW2lT/Lb6hgLySrWrFqGswANYTZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1P1Fb%2FdJMcadIW2lT%2FLb6hgLySrWrFqGswANYTZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;멱등 키 동시 중복 레이스. 처리 후 키를 저장하면 동시에 도착한 두 요청이 모두 키 없음 확인을 통과해 둘 다 결제된다. 처리 전에 유니크 제약이나 Redis SETNX로 키를 원자적으로 선점해야 한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;순진한 구현 &amp;mdash; &quot;키 조회 &amp;rarr; 없으면 처리 &amp;rarr; 처리 후 키 저장&quot; &amp;mdash; 은 거의 동시에 도착한 중복(더블클릭!)에 뚫려요. 둘 다 &quot;키 없음&quot;을 통과하거든요. 익숙한 구조죠? &lt;a href=&quot;https://jessyt.tistory.com/468&quot;&gt;읽고-판단-쓰기의 틈&lt;/a&gt;, 그 레이스예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법도 익숙해요 &amp;mdash; &lt;b&gt;처리 전에 키를 원자적으로 선점&lt;/b&gt;해요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 방법 1: DB 유니크 제약 &amp;mdash; INSERT가 곧 선점
INSERT INTO idempotency_keys (idem_key, status) VALUES ('abc123', 'IN_PROGRESS');
-- 중복이면 Duplicate entry 에러 = 누군가 선점함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis로 하면 &lt;code&gt;SET key IN_PROGRESS NX EX 300&lt;/code&gt;이에요(&lt;a href=&quot;https://jessyt.tistory.com/469&quot;&gt;분산 락&lt;/a&gt;에서 본 그 SETNX). 선점에 성공한 요청만 처리하고 실패한 요청은 키의 상태를 봐요 &amp;mdash; &lt;b&gt;키는 상태 머신&lt;/b&gt;이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IN_PROGRESS&lt;/b&gt; &amp;mdash; 다른 요청이 처리 중. &quot;처리 중입니다&quot; 응답(409 Conflict)으로 잠시 후 재시도를 유도해요. 절대 같이 처리하면 안 돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COMPLETED&lt;/b&gt; &amp;mdash; 저장된 응답을 그대로 반환해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FAILED&lt;/b&gt; &amp;mdash; 정책에 따라 재처리를 허용하거나(키 해제), 같은 실패 응답을 줘요.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;IN_PROGRESS에 TTL 또는 타임아웃 복구를 꼭 둬요. 처리하던 서버가 죽으면 키가 영원히 &quot;처리 중&quot;으로 남아 그 작업이 잠겨버려요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/469&quot;&gt;분산 락의 만료 없는 락&lt;/a&gt;과 같은 사고예요. &quot;처리 중 상태가 N분을 넘으면 실패로 간주하고 해제&quot; 같은 회수 규칙까지가 한 세트예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 설계 디테일&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;키 저장소&lt;/b&gt; &amp;mdash; 결제처럼 돈이 걸리면 DB(처리와 같은 트랜잭션으로 묶을 수 있어요 &amp;mdash; 키 INSERT와 결제 저장이 함께 커밋/롤백). 가벼운 중복 방지면 Redis + TTL이 간편해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보관 기간&lt;/b&gt; &amp;mdash; 영원히 둘 필요 없어요. 클라이언트 재시도가 일어날 수 있는 윈도(보통 수 시간~수일)만 보관하고 정리해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;같은 키, 다른 본문&lt;/b&gt; &amp;mdash; 키는 같은데 금액이 다른 요청이 오면? 재사용 실수일 가능성이 높으니 422로 거부하는 게 안전해요(요청 본문 해시를 키와 함께 저장해 비교).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET&amp;middot;PUT&amp;middot;DELETE&lt;/b&gt; &amp;mdash; HTTP 의미상 원래 멱등이어야 하는 메서드들이에요. 문제는 늘 POST고 Idempotency-Key는 사실상 POST(그리고 PATCH 같은 비멱등 메서드)를 위한 장치예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 어디까지 적용하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API에 깔 필요는 없어요. 기준은 &lt;b&gt;&quot;중복 실행의 피해&quot;&lt;/b&gt;예요. 결제&amp;middot;포인트 적립&amp;middot;주문 생성&amp;middot;송금 &amp;mdash; 돈과 재고가 움직이는 쓰기엔 필수고, 조회나 &quot;덮어쓰기형&quot; 수정(set 연산이라 원래 멱등)은 불필요해요. 그리고 멱등 키는 입구의 방어고 내부 처리(카프카 컨슈머의 재처리 등)는 &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;컨슈머 멱등성&lt;/a&gt;으로 따로 &amp;mdash; 양쪽이 한 세트가 돼야 끝까지 안전해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이중 결제가 났어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 선점이 &quot;처리 후&quot;거나 아예 없는 거예요. 처리 전 원자적 선점(유니크 제약&amp;middot;SETNX)으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시도한 클라이언트가 빈 응답을 받아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 여부만 저장하고 응답을 저장 안 한 거예요. 1차 응답 본문을 키와 함께 저장해 재반환해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;처리 중&quot; 상태에서 영영 안 풀려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 서버 사망 + 회수 규칙 부재예요. IN_PROGRESS에 타임아웃을 두고 만료 시 FAILED 전환&amp;middot;해제해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 요청은 막을 수 없으니 &lt;b&gt;서버가 멱등해져야&lt;/b&gt; 해요. Idempotency-Key로 작업 단위를 식별하고, 처리 전에 키를 원자적으로 선점해요(유니크 제약&amp;middot;SETNX). 응답까지 저장해 재시도에 같은 답을 줘요. 키는 없음&amp;rarr;처리 중&amp;rarr;완료의 상태 머신이고 처리 중엔 회수 규칙이 반드시 따라붙고요. 적용 여부는 상황으로 갈라요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;돈&amp;middot;재고가 움직이는 쓰기&lt;/b&gt;(결제&amp;middot;적립&amp;middot;주문&amp;middot;송금)라면 &amp;rarr; 필수예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조회나 덮어쓰기형 수정&lt;/b&gt;(set 연산)이라면 &amp;rarr; 원래 멱등이라 불필요해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 처리(메시지 재처리)&lt;/b&gt;라면 &amp;rarr; 키가 아니라 &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;컨슈머 멱등성&lt;/a&gt;의 몫 &amp;mdash; 입구와 안쪽이 한 세트예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;DB 커밋과 메시지 발행을 어떻게 원자적으로 묶느냐&quot;는 숙제는 &lt;a href=&quot;https://jessyt.tistory.com/505&quot;&gt;아웃박스 패턴 글&lt;/a&gt;에서 이어서 풀어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/&quot;&gt;IETF &amp;mdash; The Idempotency-Key HTTP Header Field (draft)&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.stripe.com/api/idempotent_requests&quot;&gt;Stripe &amp;mdash; Idempotent Requests&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/분산 &amp;amp; 운영</category>
      <category>API 설계</category>
      <category>Idempotency-Key</category>
      <category>멱등성</category>
      <category>백엔드</category>
      <category>분산시스템</category>
      <category>재시도</category>
      <category>중복 결제</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/504</guid>
      <comments>https://jessyt.tistory.com/504#entry504comment</comments>
      <pubDate>Thu, 6 Aug 2026 07:30:58 +0900</pubDate>
    </item>
    <item>
      <title>[Claude] Opus 4.1 지원 종료, 박아둔 모델 ID는 에러예요</title>
      <link>https://jessyt.tistory.com/591</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/XQnN8/dJMcaaTMBb8/AAAAAAAAAAAAAAAAAAAAAFJDd717cWGcioiVyrZh43tHaANcBxG6d29o_Arv25If/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=WRJPrvH7pfJC7XqSUjqWByKGmqM%3D&quot; alt=&quot;[Claude] Opus 4.1 지원 종료, 박아둔 모델 ID는 에러예요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude] Opus 4.1 지원 종료, 박아둔 모델 ID는 에러예요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic이 2026년 8월 5일자로 Claude Opus 4.1(claude-opus-4-1-20250805)을 은퇴시켰어요. 이제 이 모델 ID로 보낸 API 요청은 전부 에러를 돌려줘요. 6월 5일에 공지된 지원 종료 일정 그대로예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 코드에 박힌 모델 ID부터 찾아보는 게 먼저예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은퇴한 모델은 다른 모델로 자동 대체되지 않고 요청 자체가 실패해요. 배치 작업이나 야간 잡처럼 사람이 안 보는 자리에 박혀 있으면 조용히 깨진 채로 남아 있을 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;grep -rn &quot;claude-opus-4-1-20250805&quot; .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 API 키가 이 모델을 쓰고 있었는지는 Console에서도 확인돼요. 공식 안내로는 Usage 페이지에서 Export를 눌러 받은 CSV에 키별&amp;middot;모델별 사용량이 나뉘어 나와요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 공식 대체 모델은 Opus 4.8로 적혀 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic 문서의 은퇴 표에는 권장 대체 모델이 claude-opus-4-8로 명시돼 있어요. 8월 5일 릴리스 노트 쪽은 claude-opus-5로 올리는 걸 권하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 가격표 기준으로 두 모델 다 100만 토큰당 입력 $5, 출력 $25로 같아요. context window도 1M 토큰으로 같고요. &lt;a href=&quot;https://jessyt.tistory.com/578&quot;&gt;Opus 5는 7월 24일에 나온 모델&lt;/a&gt;이고, 문서에는 복잡한 에이전트 코딩과 기업 업무용으로 소개돼 있어요. 어느 쪽이 맞는지는 쓰던 작업으로 직접 비교해볼 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 갈아탈 때 temperature 파라미터가 걸릴 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 4.7 세대부터 temperature&amp;middot;top_p&amp;middot;top_k가 폐기됐어요. 공식 문서에는 이 파라미터에 기본값이 아닌 값을 넣으면 400 에러가 난다고 적혀 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 4.1 시절에 짠 코드에는 이 값들이 그대로 남아 있을 수 있어요. 모델 ID만 바꿔 배포하면 400으로 막히니까, 세 파라미터는 빼고 프롬프트로 동작을 잡으라는 게 공식 안내예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. Bedrock과 Google Cloud는 일정이 따로예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 날짜가 적용되는 범위는 Claude API, Claude Platform on AWS, Microsoft Foundry예요. Amazon Bedrock과 Google Cloud는 파트너가 각자 은퇴 일정을 잡기 때문에 모델 상태와 날짜가 다를 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 곳으로 붙여 쓰고 있다면 각 플랫폼의 모델 표를 따로 확인하는 편이 안전해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 다음 차례는 9월 말부터 줄 서 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 Active인 모델에도 잠정 은퇴 시점이 붙어 있어요. claude-sonnet-4-5-20250929는 9월 29일 이후, claude-haiku-4-5-20251001은 10월 15일 이후, claude-opus-4-5-20251101은 11월 24일 이후로 표기돼 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이건 &quot;그보다 빠르지는 않다&quot;는 하한선이라 확정 날짜가 아니에요. 공개된 모델은 은퇴 최소 60일 전에 메일과 문서로 알린다고 하니, 그 사이에 정리하면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연구 목적으로 은퇴한 모델이 계속 필요한 경우에는 External Researcher Access Program으로 접근을 신청하는 경로가 안내돼 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://platform.claude.com/docs/en/release-notes/api&quot;&gt;Claude Platform 릴리스 노트 (2026년 8월 5일)&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/about-claude/model-deprecations&quot;&gt;Model deprecations&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudeapi</category>
      <category>LLM</category>
      <category>Opus41</category>
      <category>Opus5</category>
      <category>마이그레이션</category>
      <category>지원종료</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/591</guid>
      <comments>https://jessyt.tistory.com/591#entry591comment</comments>
      <pubDate>Thu, 6 Aug 2026 07:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 애너테이션이 안 먹을 때 트러블슈팅: 트랜잭션, 캐시, 비동기 증상별 진단 가이드</title>
      <link>https://jessyt.tistory.com/503</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQe4C/dJMcaci1eMy/KJJnnpgIyVBPWw931kLYZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQe4C/dJMcaci1eMy/KJJnnpgIyVBPWw931kLYZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQe4C/dJMcaci1eMy/KJJnnpgIyVBPWw931kLYZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQe4C%2FdJMcaci1eMy%2FKJJnnpgIyVBPWw931kLYZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] 애너테이션이 안 먹을 때 트러블슈팅: 트랜잭션, 캐시, 비동기 증상별 진단 가이드&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] 애너테이션이 안 먹을 때 트러블슈팅: 트랜잭션, 캐시, 비동기 증상별 진단 가이드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 함정들의 공통점은 &lt;b&gt;조용하다&lt;/b&gt;는 거예요. @Transactional이 무시돼도, 캐시가 안 돼도, 비동기 예외가 사라져도 에러 한 줄 없이 돌아가요. 그래서 증상에서 원인으로 가는 빠른 지도가 필요해요. 이 글은 Spring 함정 시리즈의 진단 허브예요 &amp;mdash; 증상에서 원인으로 가는 점검 순서와 한 줄 확인 도구를 한자리에 모았어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 공통 진단, 프록시 경로 5문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional&amp;middot;@Cacheable&amp;middot;@Async&amp;middot;@Retryable이 안 먹는 문제의 90%는 &lt;b&gt;같은 원인&lt;/b&gt;이에요 &amp;mdash; 프록시를 안 거친 거예요. 그래서 어느 애너테이션이든 점검 순서가 같아요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lcZmC/dJMcahkqz0E/mbOL04d1dpx0f69OkqbhKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lcZmC/dJMcahkqz0E/mbOL04d1dpx0f69OkqbhKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lcZmC/dJMcahkqz0E/mbOL04d1dpx0f69OkqbhKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlcZmC%2FdJMcahkqz0E%2FmbOL04d1dpx0f69OkqbhKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 애너테이션 미동작 공통 진단 5문. 스프링 빈인가, 자기호출인가, public인가, EnableAsync 같은 기능 활성화가 됐나, 같은 스레드인가를 순서대로 점검하면 프록시 미적용의 대부분이 잡힌다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;① 스프링 빈인가&lt;/b&gt; &amp;mdash; &lt;code&gt;new&lt;/code&gt;로 만든 객체엔 프록시가 없어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;② 자기호출인가&lt;/b&gt; &amp;mdash; &lt;code&gt;this.method()&lt;/code&gt;는 프록시를 우회해요. &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;자기호출 편&lt;/a&gt;의 그 함정이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;③ public인가&lt;/b&gt; &amp;mdash; private&amp;middot;final은 프록시가 가로챌 수 없어요. 다만 Spring 6.0부터는 클래스 프록시면 protected&amp;middot;패키지 가시성 메서드도 기본 적용돼요. 영영 안 되는 건 private(과 final)이고 인터페이스 프록시일 때만 public 제약이 남아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;④ 기능이 켜져 있나&lt;/b&gt; &amp;mdash; &lt;code&gt;@EnableAsync&lt;/code&gt;&amp;middot;&lt;code&gt;@EnableCaching&lt;/code&gt;&amp;middot;&lt;code&gt;@EnableRetry&lt;/code&gt; 누락은 조용히 무시돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;⑤ 같은 스레드인가&lt;/b&gt; &amp;mdash; 트랜잭션&amp;middot;시큐리티&amp;middot;MDC는 ThreadLocal이라 스레드를 못 넘어요. &lt;a href=&quot;https://jessyt.tistory.com/501&quot;&gt;@Async 편&lt;/a&gt; 참고예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 트랜잭션 증상들&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVECJV/dJMcahkqz0F/dXYpOcmOY4Sm9zKaZWq0rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVECJV/dJMcahkqz0F/dXYpOcmOY4Sm9zKaZWq0rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVECJV/dJMcahkqz0F/dXYpOcmOY4Sm9zKaZWq0rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVECJV%2FdJMcahkqz0F%2FdXYpOcmOY4Sm9zKaZWq0rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 함정 증상 매핑. 롤백 안 됨은 checked 예외나 프록시 미적용, 잡았는데 롤백은 rollback-only 마킹, 롤백됐는데 알림 발송은 EventListener 즉시 실행, 비동기 침묵은 예외 증발과 무한 큐, 기동 실패는 순환 참조다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외가 났는데 롤백이 안 됐어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 갈래예요. 위 5문에 걸리는 프록시 미적용이거나, &lt;b&gt;checked 예외&lt;/b&gt;(기본은 커밋!)예요. &lt;code&gt;rollbackFor&lt;/code&gt; 또는 RuntimeException 통일 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;@Transactional 편&lt;/a&gt;의 롤백 규칙이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외를 잡았는데도 롤백돼요 (UnexpectedRollbackException)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRED로 합류한 내부 트랜잭션이 rollback-only를 마킹한 거예요. 운명 분리가 필요하면 REQUIRES_NEW(커넥션 2개 주의) &amp;mdash; 역시 &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;1편&lt;/a&gt;이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;readOnly 트랜잭션인데 UPDATE가 나가요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;readOnly가 제대로 걸렸다면 Hibernate가 flush를 꺼서 더티 체킹 UPDATE는 안 나가요. 그런데도 UPDATE가 나간다면 readOnly 자체가 안 걸린 경우예요. 자기호출로 프록시를 우회했거나, 바깥의 쓰기 트랜잭션에 REQUIRED로 합류해 힌트가 무시됐거나, &lt;code&gt;saveAndFlush()&lt;/code&gt; 같은 명시적 쓰기가 있는 거죠. ①번(프록시)과 &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;전파&lt;/a&gt;부터 점검해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 이벤트&amp;middot;비동기 증상들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;롤백됐는데 알림&amp;middot;메일이 나갔어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EventListener의 즉시 실행이에요. 부수효과 리스너는 &lt;code&gt;@TransactionalEventListener(AFTER_COMMIT)&lt;/code&gt;로 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;이벤트 편&lt;/a&gt;이에요. 반대로 AFTER_COMMIT 리스너 안의 save가 증발하면 REQUIRES_NEW 누락이고요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 리스너가 아예 안 돌아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactionalEventListener는 트랜잭션 밖 발행을 기본 무시해요. 발행 지점이 정말 트랜잭션 안인지(자기호출로 빠져 있지 않은지) 확인해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 작업이 침묵해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋 중 하나예요 &amp;mdash; void 예외 증발(핸들러 등록), 기본 풀의 무한 큐 적체(풀 정의), 컨텍스트 미전파(파라미터 전달&amp;middot;TaskDecorator). 전부 &lt;a href=&quot;https://jessyt.tistory.com/501&quot;&gt;@Async 편&lt;/a&gt;에 있어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기동이 안 되거나 느려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조(설계 경고 &amp;mdash; 끊기)와 @PostConstruct의 무거운 작업(ApplicationReadyEvent로 이동)을 봐요. &lt;a href=&quot;https://jessyt.tistory.com/502&quot;&gt;순환 참조 편&lt;/a&gt;이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 추측 대신 확인, 한 줄 도구 셋&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AqpNp/dJMcaci1eMx/qGBeEzkQxf26rppsTcNJVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AqpNp/dJMcaci1eMx/qGBeEzkQxf26rppsTcNJVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AqpNp/dJMcaci1eMx/qGBeEzkQxf26rppsTcNJVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAqpNp%2FdJMcaci1eMx%2FqGBeEzkQxf26rppsTcNJVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 진단 한 줄 도구. isActualTransactionActive로 트랜잭션 여부를, 스레드 이름으로 비동기 동작을, AopUtils.isAopProxy로 프록시 적용 여부를 즉시 확인한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// ① 지금 트랜잭션 안인가?
log.info(&quot;tx active = {}&quot;,
    TransactionSynchronizationManager.isActualTransactionActive());

// ② 어느 스레드에서 도는가? (@Async 판별)
log.info(&quot;thread = {}&quot;, Thread.currentThread().getName());

// ③ 이 빈은 프록시인가?
log.info(&quot;proxy = {}, class = {}&quot;,
    AopUtils.isAopProxy(orderService), orderService.getClass());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 버그는 조용해서 추측이 길어지기 쉬워요. 이 세 줄이면 &quot;트랜잭션이 있는가 / 비동기인가 / 프록시인가&quot;가 사실로 확정돼요. 의심되는 지점에 로그 한 줄 &amp;mdash; 그게 이 시리즈에서 제일 실용적인 습관이에요. 테스트 코드에서 같은 방식으로 단언을 박아두면 회귀도 막을 수 있고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 함정을 줄이는 팀 컨벤션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진단보다 좋은 건 예방이에요. 시리즈에서 나온 규칙들을 컨벤션으로 묶으면 이래요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 예외는 RuntimeException 상속&lt;/b&gt; &amp;mdash; checked 롤백 함정 원천 차단.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 단위는 별도 빈으로&lt;/b&gt; &amp;mdash; 자기호출이 구조적으로 안 나옴.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부수효과(알림&amp;middot;외부 API)는 AFTER_COMMIT 리스너로&lt;/b&gt; &amp;mdash; 롤백 불일치 차단.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Async는 전용 풀 + 예외 핸들러 세트로만&lt;/b&gt; &amp;mdash; 기본 풀&amp;middot;침묵 금지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순환 참조 허용 설정 금지&lt;/b&gt; &amp;mdash; 경고는 경고로 받기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 함정 진단의 뼈대는 &lt;b&gt;&quot;프록시 경로 5문&quot;&lt;/b&gt;이에요 &amp;mdash; 빈인가, 자기호출인가, public인가, Enable을 켰나, 같은 스레드인가. 여기서 트랜잭션 증상은 롤백 규칙&amp;middot;전파로, 이벤트는 실행 타이밍으로, 비동기는 풀&amp;middot;예외&amp;middot;컨텍스트로 갈라져요. 무엇이든 한 줄 확인 도구로 사실부터 잡아요. 조용한 버그일수록 추측보다 측정이 답이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 막혔을 때 스스로에게 던질 질문은 결국 하나예요 &amp;mdash; &lt;b&gt;&quot;지금 이 코드, 정말 프록시를 거쳐 트랜잭션 안에서 돌고 있나?&quot;&lt;/b&gt; 로그 세 줄이면 답이 나와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;@Transactional 동작 원리&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;프록시 자기호출&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;트랜잭션 이벤트&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/501&quot;&gt;@Async&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/502&quot;&gt;순환 참조와 초기화&lt;/a&gt;를 증상 관점에서 묶은 자리예요. &lt;a href=&quot;https://jessyt.tistory.com/489&quot;&gt;JPA&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/497&quot;&gt;MySQL&lt;/a&gt; 트러블슈팅 허브와 함께 보면 백엔드 진단 3종 세트가 완성돼요. 이어지는 시리즈는 분산 시스템 패턴이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/aop/proxying.html&quot;&gt;Spring Framework &amp;mdash; Proxying Mechanisms&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction.html&quot;&gt;Spring &amp;mdash; Transaction Management&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>aop</category>
      <category>Spring</category>
      <category>Transactional</category>
      <category>백엔드</category>
      <category>진단</category>
      <category>트러블슈팅</category>
      <category>프록시</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/503</guid>
      <comments>https://jessyt.tistory.com/503#entry503comment</comments>
      <pubDate>Wed, 5 Aug 2026 07:30:05 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 2.1.221, 샌드박스가 실제 키를 못 봐요</title>
      <link>https://jessyt.tistory.com/589</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ICUiq/dJMcadXhlbg/ZUjpuuA8wJahKiGqWce1aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ICUiq/dJMcadXhlbg/ZUjpuuA8wJahKiGqWce1aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ICUiq/dJMcadXhlbg/ZUjpuuA8wJahKiGqWce1aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FICUiq%2FdJMcadXhlbg%2FZUjpuuA8wJahKiGqWce1aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Claude Code] 2.1.221, 샌드박스가 실제 키를 못 봐요&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Claude Code] 2.1.221, 샌드박스가 실제 키를 못 봐요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 2.1.221부터 샌드박스 안에서 도는 명령이 자격 증명 파일의 실제 값 대신 가짜 값을 읽게 만들 수 있어요. 자격 증명 파일용 mask 모드가 changelog에 올라왔는데 지금은 Linux와 WSL에서만 동작해요. 같은 릴리스에 권한 체크 우회 수정도 들어갔어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 명령은 통과하는데 키 실물은 안 넘어가요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 생긴 값은 자격 증명 파일 설정의 &lt;code&gt;mode: &quot;mask&quot;&lt;/code&gt;예요. 파일 접근을 아예 막는 대신, 읽히는 값을 바꿔치기하는 방식이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 설명으로는 샌드박스 안 명령이 sentinel 사본을 읽어요. 실제 값 치환은 샌드박스 프록시가 바깥으로 나가는 순간에 해줘요. 파일 전체를 사본으로 돌릴 수도 있고, extract 정규식으로 잡은 구간만 가릴 수도 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식 옵션이 있다는 게 실무에선 꽤 실용적인 부분이에요. .env 파일 하나에 키가 여러 개 들어 있어도 토큰 값 구간만 잡아 가리고 나머지 설정값은 그대로 읽히게 둘 수 있으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 설명대로라면 샌드박스 안에서 curl로 API를 호출하는 명령은 그대로 성공하고, 그 명령이 읽는 파일에는 진짜 토큰이 안 들어가요. 다만 마스킹은 샌드박스 안 명령에만 걸리는 범위라, 샌드박스를 안 거치는 읽기까지 덮어주는 건 아니에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. macOS에서는 deny로 떨어져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog에 macOS는 파일 마스킹이 deny로 폴백한다고 명시돼 있어요. 맥에서 mask를 걸어도 값이 가려진 채 읽히는 게 아니라, 그 파일에 대한 접근 자체가 막힌다는 뜻이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 차이가 꽤 커요. Linux나 WSL에서는 명령이 정상적으로 끝나는데, 같은 설정이 맥에서는 파일을 못 읽는 쪽으로 떨어지니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥으로 개발하는 사람이 많은 한국 환경에서는 당장 체감이 갈리는 지점이에요. 이 기능을 그대로 써보려면 Linux 서버나 WSL 쪽 셋업이 필요해요. 맥이면 당분간은 기존 deny 흐름을 그대로 쓴다고 보면 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. zsh 조건문에 숨은 명령이 권한 체크를 빠져나가던 게 막혔어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 릴리스에 Bash 도구 권한 체크 우회 수정이 들어갔어요. zsh에서 &lt;code&gt;[[ ]]&lt;/code&gt; 정규식 조건문 안에 명령을 숨기면 승인 없이 실행되던 경로였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 명령도 권한 프롬프트를 띄워요. Windows에서 따옴표가 들어간 경로를 PowerShell 권한 체크가 잘못 처리하던 문제도 같이 잡혔어요. 권한 우회 수정이라 버전을 올려둘 이유가 되는 릴리스예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. Focus view와 prompt-audit도 같이 들어왔어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode 확장에 Focus view가 생겼어요. 채팅 메뉴 토글로, 도구 실행 로그를 펼칠 수 있는 턴 단위 요약 뒤로 접고 실행 중인 도구는 실시간으로 표시해줘요. Ctrl+Alt+F 또는 &quot;Claude Code: Toggle Focus view&quot; 커맨드로 켜고 끌 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;claude-api 스킬에는 prompt-audit 서브커맨드가 붙었어요. 프롬프트와 도구 설명이 옛날 모델 기준으로 쓰인 패턴을 잡아주는 용도라고 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 밖에 백그라운드 세션이 작업 보존을 위해 커밋&amp;middot;푸시까지 하도록 바뀌었어요. /fork로 갈라진 세션은 원본 체크아웃 대신 자체 워크트리를 만들어요. /status는 세션 종류를 interactive인지, 아니면 attached&amp;middot;unattended 백그라운드 작업인지까지 표시해줘요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG (2.1.221)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>anthropic</category>
      <category>API키</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>WSL</category>
      <category>보안</category>
      <category>샌드박스</category>
      <category>업데이트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/589</guid>
      <comments>https://jessyt.tistory.com/589#entry589comment</comments>
      <pubDate>Wed, 5 Aug 2026 07:00:53 +0900</pubDate>
    </item>
    <item>
      <title>[Maccy] 클립보드 매니저, 무료로 충분한 이유</title>
      <link>https://jessyt.tistory.com/590</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/q79nN/dJMcai5gAgC/AAAAAAAAAAAAAAAAAAAAALKZaTOkyxckqmHfweVkKgR_1fNXXITvfps1KSHYjTov/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=2x9ZScMErQlaYaNikUN9HeTq4aA%3D&quot; alt=&quot;[Maccy] 클립보드 매니저, 무료로 충분한 이유&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 생산성 툴 &amp;middot; 소개&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Maccy] 클립보드 매니저, 무료로 충분한 이유&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;macOS 기본 클립보드는 마지막에 복사한 것 하나만 기억해요. Maccy는 이 빈자리를 채우는 오픈소스 클립보드 매니저로, 히스토리 검색&amp;middot;핀 고정&amp;middot;서식 제거 붙여넣기까지 키보드만으로 처리하게 해줘요. 이 글에서는 설치부터 단축키, 설정에서 손볼 지점, 미리 알아둘 주의점까지 순서대로 정리해요.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;macOS에는 클립보드 히스토리가 애초에 없어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows는 Win+V로 클립보드 히스토리를 기본 제공하지만 macOS에는 대응하는 기능이 없어요. Cmd+C를 두 번 하면 앞의 것은 그냥 사라져요. 커밋 해시를 복사해 두고 브랜치명을 복사하는 순간 해시가 날아가는 식이라, 개발 작업에서는 이 한 칸짜리 클립보드가 꽤 자주 발목을 잡는 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 빈자리를 채우는 방법은 크게 두 갈래예요. 하나는 Raycast나 Alfred 같은 런처에 딸린 클립보드 히스토리 기능을 쓰는 것, 다른 하나는 클립보드 전용 앱을 따로 두는 거예요. Maccy는 후자 쪽의 대표적인 선택지로, 클립보드 히스토리 하나만 하는 단일 목적 앱이에요. GitHub 스타 2만 개가 넘는 인기 프로젝트이고, MIT 라이선스 오픈소스라 무료예요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;성격&lt;/th&gt;
&lt;th&gt;고려할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS 기본 클립보드&lt;/td&gt;
&lt;td&gt;마지막 1개만 유지&lt;/td&gt;
&lt;td&gt;히스토리 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;런처 내장 (Raycast&amp;middot;Alfred)&lt;/td&gt;
&lt;td&gt;런처 기능 중 하나&lt;/td&gt;
&lt;td&gt;런처 자체를 써야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maccy&lt;/td&gt;
&lt;td&gt;클립보드 전용 단일 앱&lt;/td&gt;
&lt;td&gt;가볍고 무료, 이것만 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런처를 이미 쓰고 있다면 내장 기능으로 충분한 경우가 많아요. 반대로 런처까지는 필요 없고 클립보드 히스토리만 원한다면, 그 용도 하나에 집중한 Maccy가 자리에 맞는 도구예요. 개발사 설명대로 네이티브 macOS UI 기반이라 별도 프레임워크 없이 가볍게 도는 점도 전용 앱을 고르는 이유가 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치는 brew 한 줄, 호출은 Shift+Cmd+C예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew가 있다면 명령 한 줄로 끝나요. GitHub 릴리스 페이지에서 직접 받거나 App Store에서 설치하는 경로도 있어요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;&amp;gt; brew install maccy

# 설치 후 실행
&amp;gt; open -a Maccy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 메뉴바에 아이콘이 생겨요. 기본 단축키 Shift+Cmd+C를 누르면 어디서든 히스토리 팝업이 떠요. 팝업이 뜬 상태에서 바로 타이핑하면 검색이고 화살표나 숫자로 항목을 고른 뒤 Enter를 누르면 그 항목이 클립보드로 복사돼요. 복사가 아니라 커서 위치에 바로 붙여넣기까지 하려면 Option+Enter를 쓰거나 설정에서 자동 붙여넣기를 켜면 돼요.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;권한 안내&lt;/span&gt; 선택한 항목을 앱에 바로 붙여넣는 동작은 macOS의 손쉬운 사용(Accessibility) 권한이 있어야 작동해요. 시스템 설정의 개인정보 보호 및 보안 항목에서 Maccy를 허용해 두면 돼요. 권한 없이도 히스토리에서 복사하는 것까지는 문제없이 돼요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능은 검색&amp;middot;고정&amp;middot;서식 제거, 이 셋이 핵심이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maccy의 기능 목록은 짧아요. 공식 사이트 스스로 불필요한 기능을 뺀 미니멀 디자인을 내세우는 앱이라, 알아둘 것은 사실상 세 가지예요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타이핑 즉시 검색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팝업을 띄우고 바로 키워드를 치면 히스토리 전체에서 걸러줘요. 개발사가 강조하는 지점이 이 검색 속도인데, 히스토리가 수백 개 쌓여 있어도 키보드에서 손을 떼지 않고 원하는 항목까지 도달하는 흐름이 이 앱의 중심이에요. 마우스로 메뉴바 아이콘을 눌러 목록을 스크롤하는 방식도 되지만 단축키와 검색으로 쓰는 쪽이 설계 의도에 맞아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Option+P 핀 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰는 항목은 Option+P로 고정해 두면 히스토리가 밀려도 사라지지 않아요. 반복해서 붙여넣는 계정 ID, 자주 쓰는 명령어 조각, 이메일 서명 같은 것을 올려두는 용도예요. 스니펫 전용 앱만큼의 관리 기능은 없지만 몇 개 수준의 상용구라면 이 고정 기능으로 커버돼요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Option+Shift+Enter 서식 제거 붙여넣기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 페이지나 문서에서 복사한 텍스트를 붙여넣을 때 폰트&amp;middot;색상 서식이 따라오는 문제를 단축키 하나로 해결해요. Option+Shift+Enter로 붙여넣으면 순수 텍스트만 들어가요. 문서 도구에 코드나 웹 본문을 옮길 일이 많다면 가장 자주 쓰게 될 단축키예요. 이 밖에 Option+Delete로 항목 삭제, Cmd+,로 환경설정을 여는 정도가 기본 조작의 전부예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정에서는 무시 목록을 먼저 손보는 게 좋아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경설정에서 조정할 만한 항목은 몇 가지로 좁혀져요. 첫 번째가 특정 앱 무시 목록이에요. 비밀번호 매니저처럼 민감한 값을 복사하는 앱을 무시 대상으로 올려두면 그 앱에서 복사한 내용은 히스토리에 남지 않아요. Maccy는 비밀번호 매니저가 표시하는 기밀(concealed) 타입 복사를 걸러내는 동작도 지원하니 민감 정보를 다룬다면 이 부분을 먼저 확인하는 순서가 안전해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음으로 히스토리 보관 개수, 클립보드 확인 주기(기본 500ms), 자동 붙여넣기 여부 정도를 취향에 맞게 조정하면 돼요. 특정 복사 타입을 통째로 무시하는 설정도 있어서 이미지처럼 용량이 큰 콘텐츠를 히스토리에서 빼는 식의 조정도 가능해요. 설정 화면 자체가 단출해서 처음 설치한 날 5분 정도면 한 바퀴 다 돌아볼 만한 분량이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 주의점이 몇 가지 있어요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;macOS Sonoma(14) 이상 필요&lt;/b&gt; &amp;mdash; 그 아래 버전에서는 최신 릴리스가 돌지 않아요. 구버전 macOS라면 GitHub 릴리스에서 과거 버전을 찾아야 해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공식 사이트는 maccy.app 하나&lt;/b&gt; &amp;mdash; 개발자가 직접 사칭 사이트(maccyapp.net, maccyapp.com 등)를 주의하라고 안내하고 있어요. 다운로드는 maccy.app이나 GitHub, App Store 경로만 쓰는 게 안전해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;암호 입력 필드에서 단축키가 안 먹는 경우&lt;/b&gt; &amp;mdash; macOS 보안 특성상 암호 필드에서는 팝업 단축키가 동작하지 않을 수 있어요. README는 이 경우 Karabiner-Elements로 키를 재매핑하는 우회를 안내하고 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단축키 충돌&lt;/b&gt; &amp;mdash; Shift+Cmd+C는 다른 앱이 선점하고 있을 수 있어요. 충돌하면 환경설정에서 호출 키를 바꾸면 돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런처 내장 기능과 중복&lt;/b&gt; &amp;mdash; Raycast 등의 클립보드 히스토리를 이미 켜뒀다면 히스토리가 이중으로 쌓여요. 한쪽만 남기는 정리가 필요해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;런처 없이 클립보드만 원하는 사람에게 맞아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Maccy는 클립보드 히스토리라는 한 가지 문제를 무료로, 가볍게, 로컬에서 해결하는 도구예요. 모든 데이터가 로컬에만 저장되니 복사 내용이 외부로 나가는 걱정 없이 쓸 수 있고, 오픈소스라 코드가 공개되어 있다는 점도 클립보드처럼 민감한 데이터를 다루는 앱에서는 의미 있는 특징이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작하는 방법은 단순해요. brew install maccy로 설치하고 손쉬운 사용 권한을 허용한 뒤, 비밀번호 매니저를 무시 목록에 올리는 것까지가 초기 설정의 전부예요. 이후에는 Shift+Cmd+C 호출, Enter 복사, Option+Shift+Enter 서식 제거, Option+P 고정 네 가지만 손에 익히면 이 앱에서 배울 것은 끝나요. 런처 생태계까지 갈 생각은 없는데 한 칸짜리 기본 클립보드가 답답했다면 부담 없이 걸어볼 만한 선택지예요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;p class=&quot;yt-sources__head&quot; data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://maccy.app/&quot;&gt;Maccy 공식 사이트&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/p0deje/Maccy&quot;&gt;GitHub &amp;mdash; p0deje/Maccy (README&amp;middot;릴리스)&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://apps.apple.com/us/app/maccy/id1527619437&quot;&gt;App Store &amp;mdash; Maccy&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 직접 사용기가 아니라 공식 사이트와 GitHub 문서를 근거로 정리한 소개 글이에요. 기능&amp;middot;단축키&amp;middot;요구 사항은 작성 시점 문서 기준이며 이후 릴리스에서 달라질 수 있어요.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/생산성 툴</category>
      <category>Maccy</category>
      <category>MacOS</category>
      <category>생산성</category>
      <category>오픈소스</category>
      <category>클립보드</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/590</guid>
      <comments>https://jessyt.tistory.com/590#entry590comment</comments>
      <pubDate>Wed, 5 Aug 2026 06:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[백엔드] 기술면접 단골 주제 지도: JPA, Kafka, Redis, MySQL, Spring 질문 뼈대와 깊이 글 정리</title>
      <link>https://jessyt.tistory.com/527</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/bLrTbW/dJMcafNKuNw/AAAAAAAAAAAAAAAAAAAAAK2GKm4s0-ALE-1NY4XAdrzFarLI8GJkxkiffxqbH0tT/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1782831599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=zSox3mFZQwFLYcoYUnZAK5oJPMQ%3D&quot; alt=&quot;[백엔드] 기술면접 단골 주제 지도: JPA, Kafka, Redis, MySQL, Spring 질문 뼈대와 깊이 글 정리&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[백엔드] 기술면접 단골 주제 지도: JPA, Kafka, Redis, MySQL, Spring 질문 뼈대와 깊이 글 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 기술면접에서 반복해서 나오는 주제들을 한 장에 모았어요. 주제마다 질문이 어떻게 나오는지, 답의 뼈대가 뭔지, 그리고 깊이 있게 파려면 어느 글로 가면 되는지를 정리했어요. 답을 외우는 용도가 아니라, 어디까지 알고 있는지 점검하는 체크리스트로 쓰면 좋아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. JPA &amp;mdash; 동작 원리를 묻고, 사고 경험을 또 물어요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;영속성 컨텍스트가 뭔가요?&lt;/b&gt; &amp;mdash; 1차 캐시&amp;middot;더티 체킹&amp;middot;쓰기 지연 세 가지로 답하고, flush와 commit이 다르다는 걸 짚으면 깊이가 보여요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/481&quot;&gt;영속성 컨텍스트 동작 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N+1 문제를 겪어봤나요?&lt;/b&gt; &amp;mdash; 원인(지연 로딩 + 반복 접근)과 해법 3종(fetch join&amp;middot;@EntityGraph&amp;middot;batch size)을 상황별로 갈라 답하는 게 핵심이에요. 컬렉션 fetch join + 페이징의 함정까지 가면 가산점. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/483&quot;&gt;N+1 원인과 해결&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동시에 같은 데이터를 수정하면요?&lt;/b&gt; &amp;mdash; 낙관적 락(@Version) vs 비관적 락(SELECT FOR UPDATE)을 충돌 빈도로 고르는 기준. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;낙관적 락 vs 비관적 락&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OSIV는 켜나요 끄나요?&lt;/b&gt; &amp;mdash; 트레이드오프(편의 vs 커넥션 점유)를 아는지 보는 질문이에요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/486&quot;&gt;OSIV 켤까 끌까&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. MySQL &amp;mdash; 인덱스와 격리수준은 거의 고정 출제예요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스는 왜 빠른가요?&lt;/b&gt; &amp;mdash; B+Tree 구조, 클러스터드 vs 세컨더리, 두 번 찾기까지가 한 세트예요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/490&quot;&gt;인덱스 동작 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복합 인덱스 컬럼 순서는 어떻게 정하나요?&lt;/b&gt; &amp;mdash; leftmost prefix, 동등 앞&amp;middot;범위 뒤, 커버링 인덱스. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/491&quot;&gt;복합 인덱스 설계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;격리수준을 설명해보세요&lt;/b&gt; &amp;mdash; MySQL 기본이 REPEATABLE READ인 것, MVCC와 언두 로그, 갭 락이 팬텀을 막는 방식까지. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;격리수준과 MVCC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데드락을 겪어봤나요?&lt;/b&gt; &amp;mdash; 원인 패턴(역순 잠금&amp;middot;갭 락 경합)과 재시도 전략으로 답해요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/495&quot;&gt;데드락 원인과 해결&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;느린 쿼리는 어떻게 잡나요?&lt;/b&gt; &amp;mdash; EXPLAIN type 등급과 실측(EXPLAIN ANALYZE) 순서로. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/492&quot;&gt;실행계획 읽는 법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. Spring &amp;mdash; 프록시를 이해했는지 계속 돌려 물어요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@Transactional이 안 먹는 경우는?&lt;/b&gt; &amp;mdash; 자기호출&amp;middot;private&amp;middot;내부 호출. 답의 본질은 &quot;프록시 기반 AOP의 한계&quot;예요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;@Transactional이 안 먹는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;checked 예외에서 롤백되나요?&lt;/b&gt; &amp;mdash; 기본은 안 됨(unchecked만 롤백), rollbackFor로 바꿈. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;@Transactional 동작 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트로 알림을 보내는데 롤백되면요?&lt;/b&gt; &amp;mdash; @TransactionalEventListener(AFTER_COMMIT)과 그 안의 쓰기 함정. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;@TransactionalEventListener 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Async에서 예외가 사라지는 이유는?&lt;/b&gt; &amp;mdash; void 반환 + 별도 스레드. 스레드풀 설정과 컨텍스트 미전파까지. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/501&quot;&gt;@Async 함정 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. Kafka &amp;mdash; &quot;보장&quot;이라는 단어가 나오면 이 시리즈예요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 유실은 어떻게 막나요?&lt;/b&gt; &amp;mdash; acks=all + min.insync.replicas + 멱등 프로듀서 공식. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/460&quot;&gt;acks와 유실 방지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 처리는요?&lt;/b&gt; &amp;mdash; at-least-once 전제 + 컨슈머 멱등성 패턴 4가지. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;수동 커밋과 멱등성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 보장은 어떻게 하나요?&lt;/b&gt; &amp;mdash; &quot;파티션 안에서만&quot;이 답의 시작. 같은 키 &amp;rarr; 같은 파티션. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/461&quot;&gt;파티셔닝과 메시지 키&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리밸런싱이 자주 일어나면?&lt;/b&gt; &amp;mdash; 원인(배포&amp;middot;추방&amp;middot;증설)과 cooperative 전환&amp;middot;static membership. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/457&quot;&gt;리밸런싱 줄이는 법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;전체 지도는 &lt;a href=&quot;https://jessyt.tistory.com/526&quot;&gt;Kafka 시리즈 허브&lt;/a&gt;에서.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. Redis &amp;mdash; 캐시 장애 시나리오를 물어요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐시는 어떤 문제가 생길 수 있나요?&lt;/b&gt; &amp;mdash; 스탬피드&amp;middot;관통&amp;middot;사태 3분류로 답하면 정리돼 보여요. TTL jitter&amp;middot;뮤텍스&amp;middot;Bloom Filter가 각각의 해법. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/470&quot;&gt;캐시 스탬피드와 3대 장애&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 락은 어떻게 구현하나요?&lt;/b&gt; &amp;mdash; SETNX + 만료 + 안전 해제(Lua)가 기본, Redisson watchdog, 그리고 Redlock 논쟁(fencing token)까지 알면 상급. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/469&quot;&gt;분산 락 구현&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이트 리미팅을 설계해보세요&lt;/b&gt; &amp;mdash; 고정 윈도의 경계 누수부터 토큰 버킷까지 단계적으로. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/471&quot;&gt;레이트 리미팅 구현&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 분산 시스템 &amp;mdash; 결제 도메인이면 거의 물어봐요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중복 결제는 어떻게 막나요?&lt;/b&gt; &amp;mdash; Idempotency-Key + 원자적 선점 + 응답 재생. 타임아웃의 &quot;결과 모름&quot; 비대칭을 아는 게 핵심이에요. &amp;rarr; &lt;a href=&quot;https://jessyt.tistory.com/504&quot;&gt;멱등 API 설계&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 안 실린 주제(아웃박스, 사가, 서킷 브레이커, JVM 진단, 무중단 배포)도 이 블로그 백엔드 카테고리에 차례로 올라오고 있어요. &lt;a href=&quot;https://jessyt.tistory.com/category/%EB%B0%B1%EC%97%94%EB%93%9C&quot;&gt;백엔드 카테고리&lt;/a&gt;에서 볼 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 답은 외운 문장보다 &quot;왜&quot;와 &quot;겪어본 함정&quot;이 한 줄 섞일 때 달라져요. 각 글의 &quot;자주 만나는 문제&quot; 섹션이 그 한 줄이 돼줄 거예요.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>JPA면접</category>
      <category>Kafka면접</category>
      <category>MySQL면접</category>
      <category>Spring면접</category>
      <category>기술면접</category>
      <category>백엔드면접</category>
      <category>신입백엔드</category>
      <category>이직</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/527</guid>
      <comments>https://jessyt.tistory.com/527#entry527comment</comments>
      <pubDate>Tue, 4 Aug 2026 11:00:19 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 순환 참조 해결과 빈 초기화 함정: 생성자 주입, @Lazy, @PostConstruct 주의점</title>
      <link>https://jessyt.tistory.com/502</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR7ufq/dJMcabYNmOz/gGjHOsvLUZ5OWKTSSnPPzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR7ufq/dJMcabYNmOz/gGjHOsvLUZ5OWKTSSnPPzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR7ufq/dJMcabYNmOz/gGjHOsvLUZ5OWKTSSnPPzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR7ufq%2FdJMcabYNmOz%2FgGjHOsvLUZ5OWKTSSnPPzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] 순환 참조 해결과 빈 초기화 함정: 생성자 주입, @Lazy, @PostConstruct 주의점&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] 순환 참조 해결과 빈 초기화 함정: 생성자 주입, @Lazy, @PostConstruct 주의점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;The dependencies of some of the beans in the application context form a cycle&quot; &amp;mdash; 기동이 안 되는 그 에러예요. 스프링 부트 2.6부터 순환 참조가 기본 금지라, 옛 코드를 올리다 처음 만나는 경우가 많아요. 급한 마음에 설정으로 허용해버리기 쉬운데, 이 에러는 버그가 아니라 &lt;b&gt;설계 경고&lt;/b&gt;예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조가 왜 문제인지, 끊는 법 세 가지(구조 추출&amp;middot;이벤트&amp;middot;@Lazy), 그리고 같은 &quot;기동 시점&quot; 주제인 @PostConstruct의 함정과 ApplicationReadyEvent를 차례로 짚어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 순환 참조, 닭과 달걀&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r0Z93/dJMcabYNmOv/zQoXKqRJkzOQkqGi4drCA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r0Z93/dJMcabYNmOv/zQoXKqRJkzOQkqGi4drCA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r0Z93/dJMcabYNmOv/zQoXKqRJkzOQkqGi4drCA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr0Z93%2FdJMcabYNmOv%2FzQoXKqRJkzOQkqGi4drCA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 순환 참조. OrderService가 UserService를 필요로 하고 UserService도 OrderService를 필요로 하면 생성자 주입에서 기동이 실패한다. 스프링 부트 2.6부터 기본 금지이며 이는 설계 경고다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;A의 생성자가 B를 요구하고 B의 생성자가 A를 요구하면, 스프링은 어느 쪽도 먼저 완성할 수 없어요. 그래서 생성자 주입에서는 기동이 즉시 실패해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;필드 주입(@Autowired) 시절엔 됐는데?&quot;라는 의문이 들 수 있어요. 맞아요 &amp;mdash; 필드 주입은 일단 빈 객체를 만들고 나중에 필드를 채우니, 미완성 객체를 서로 참조시키는 식으로 &lt;b&gt;몰래&lt;/b&gt; 돌아갔어요. 부트 2.6이 기본 금지로 바꾼 건, 그 &quot;몰래&quot;가 위험해서예요. 미완성 빈이 노출되는 타이밍 버그도 있지만 &lt;b&gt;두 컴포넌트가 서로를 안다 = 책임 경계가 무너졌다&lt;/b&gt;는 신호거든요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;code&gt;spring.main.allow-circular-references=true&lt;/code&gt;로 덮고 싶은 유혹이 있는데, 그건 경고등에 테이프를 붙이는 거예요. 순환은 그대로 남아서 &amp;mdash; 리팩토링을 막고 테스트를 어렵게 해요(둘을 항상 같이 띄워야 함). 어느 날은 프록시&amp;middot;AOP와 얽혀 더 이상한 버그로 돌아오기도 해요. 게다가 이 플래그는 setter&amp;middot;필드 주입이 낀 사이클만 살려줘요. 생성자끼리의 순환은 켜도 그대로 기동 실패라, 어차피 구조를 풀어야 해요. 마이그레이션 중의 한시적 우회까지만 허용하고 끊는 게 답이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 끊는 법, 구조가 정석이에요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWqW6p/dJMcabYNmOx/8RbzWlxk7D2BkQOSE68h2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWqW6p/dJMcabYNmOx/8RbzWlxk7D2BkQOSE68h2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWqW6p/dJMcabYNmOx/8RbzWlxk7D2BkQOSE68h2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWqW6p%2FdJMcabYNmOx%2F8RbzWlxk7D2BkQOSE68h2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;순환 참조 해법. 서로 원하던 기능을 별도 컴포넌트로 추출해 의존을 한 방향으로 만드는 게 정석이고, 이벤트로 의존 방향을 제거할 수도 있으며, Lazy는 기동만 시키는 임시방편이다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 공통 책임 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 B에서 원하는 것과 B가 A에서 원하는 걸 들여다보면, 보통 둘 다 필요로 하는 제3의 책임이 숨어 있어요. 그걸 C로 추출하면 A&amp;rarr;C&amp;larr;B로 화살표가 한 방향이 돼요. 예를 들어 주문과 회원이 서로를 부르고 있다면, 실은 &quot;포인트 계산&quot;이라는 별도 도메인이 끼어 있는 경우 &amp;mdash; 그걸 PointService로 빼는 식이에요. 순환 해결이 곧 설계 개선이 되는, 가장 권장하는 길이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 이벤트로 역전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B가 A를 직접 부르는 대신, B는 이벤트를 발행하고 A가 구독해요. 의존 자체가 사라져요. &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;이벤트 편&lt;/a&gt;에서 본 그 패턴이 순환 해소 도구이기도 한 거예요. &quot;주문 완료 시 회원 등급 갱신&quot; 같은 부가 흐름이면 이게 자연스러워요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ @Lazy, 임시방편&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public OrderService(@Lazy UserService userService) {
    this.userService = userService;   // 진짜 대신 지연 프록시 주입
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한쪽에 지연 프록시를 주입해 &quot;만들 때&quot;의 순환만 피하는 거예요. 기동은 되지만 순환 구조 자체는 그대로라, 문제를 첫 호출 시점으로 미룬 것뿐이에요. 리팩토링 일정 잡기 전 응급처치까지만 써요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. @PostConstruct, 아직 완전한 세상이 아니에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기동 시점의 또 다른 함정이 초기화 메서드예요. &lt;code&gt;@PostConstruct&lt;/code&gt;는 &quot;빈 하나의 의존성 주입이 끝난 직후&quot;에 불리는데, 그 시점은 생각보다 이른 시점이에요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HTExO/dJMcabYNmOy/Q3rk2LnPH2gZwpxFP26uO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HTExO/dJMcabYNmOy/Q3rk2LnPH2gZwpxFP26uO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HTExO/dJMcabYNmOy/Q3rk2LnPH2gZwpxFP26uO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHTExO%2FdJMcabYNmOy%2FQ3rk2LnPH2gZwpxFP26uO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;PostConstruct 시점 함정. 프록시 적용 전이라 자기 자신의 Transactional이나 Async 호출이 동작하지 않을 수 있고, 무거운 작업은 기동 시간과 헬스체크에 문제를 만든다. 기동 완료 후 1회 실행은 ApplicationReadyEvent가 정답이다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자기 자신의 @Transactional&amp;middot;@Async가 안 먹을 수 있어요.&lt;/b&gt; 초기화 콜백은 프록시 적용이 끝나기 전의 원본 객체에서 실행될 수 있어요. 게다가 자기 메서드 호출은 &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;자기호출 문제&lt;/a&gt;까지 겹쳐요. &quot;초기화에서 DB 세팅을 트랜잭션으로 했는데 트랜잭션이 없네?&quot;가 이 조합이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무거운 작업은 기동을 인질로 잡아요.&lt;/b&gt; 외부 API 대기, 대량 캐시 워밍을 @PostConstruct에서 하면 기동 시간이 그만큼 늘어요. 컨테이너 환경에서는 헬스체크 타임아웃 &amp;rarr; 재시작 &amp;rarr; 또 초기화... 루프의 시작이 되기도 해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;기동이 다 끝난 뒤 한 번 실행&quot;이 필요하면 정답은 &lt;code&gt;ApplicationReadyEvent&lt;/code&gt;예요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class CacheWarmer {

    @EventListener(ApplicationReadyEvent.class)
    public void warmUp() {
        // 모든 빈&amp;middot;프록시 준비 완료 + 트래픽 받기 직전 시점
        // 트랜잭션&amp;middot;@Async 전부 정상 동작
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PostConstruct는 필드 검증&amp;middot;내부 자료구조 셋업 같은 가벼운 일까지만, &quot;스프링 기능을 쓰는 초기화&quot;는 ApplicationReadyEvent로 &amp;mdash; 이 분담이 깔끔해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부트 업그레이드 후 순환 참조 에러로 기동이 안 돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.6+ 기본 금지에 걸린 거예요. 에러 메시지에 순환 고리가 그려져 나오니, 그 고리에서 공통 책임 추출&amp;middot;이벤트 역전으로 한 곳을 끊어요. allow-circular-references는 마이그레이션 한시 우회까지만요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@PostConstruct에서 한 DB 작업이 트랜잭션 없이 돌았어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 미적용+자기호출 조합이에요. ApplicationReadyEvent로 옮기고 트랜잭션 단위는 별도 빈으로 분리해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기동이 느려졌어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 콜백들의 무거운 작업을 의심해요. 기동 필수가 아닌 워밍은 ApplicationReadyEvent + @Async로 미루면 기동이 가벼워져요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조 에러는 버그가 아니라 &lt;b&gt;설계가 보내는 경고&lt;/b&gt;예요. 그래서 덮지 말고 끊는 게 답이고, 끊는 방법은 공통 책임 추출이나 이벤트 역전이 먼저예요 &amp;mdash; @Lazy와 허용 설정은 일정을 잡기 전까지의 임시방편일 뿐이고요. 기동 시점 작업도 같은 결의 분담이 필요해요. 가벼운 내부 초기화는 @PostConstruct에 두고, 스프링 기능(트랜잭션&amp;middot;@Async)을 쓰는 본격 작업은 모든 준비가 끝난 ApplicationReadyEvent로 미뤄요. 둘 다 &quot;스프링이 다 준비되기 전 시점&quot;을 의식하는 습관에서 나와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 마지막인 다음 글에서는 지금까지의 함정 전부를 증상별로 묶는 &lt;a href=&quot;https://jessyt.tistory.com/503&quot;&gt;트러블슈팅 허브&lt;/a&gt;로 마무리해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html&quot;&gt;Spring Framework &amp;mdash; Dependency Injection&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/spring-application.html#features.spring-application.application-events-and-listeners&quot;&gt;Spring Boot &amp;mdash; Application Events&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>ApplicationReadyEvent</category>
      <category>Circular Reference</category>
      <category>PostConstruct</category>
      <category>Spring</category>
      <category>백엔드</category>
      <category>생성자 주입</category>
      <category>순환 참조</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/502</guid>
      <comments>https://jessyt.tistory.com/502#entry502comment</comments>
      <pubDate>Tue, 4 Aug 2026 07:30:31 +0900</pubDate>
    </item>
    <item>
      <title>[Cursor] Google Workspace 플러그인 6종, 에디터 안으로 들어왔어요</title>
      <link>https://jessyt.tistory.com/588</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq5l8L/dJMcadpmJXZ/l5mlyQ3J72kxF7VxvqwWX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq5l8L/dJMcadpmJXZ/l5mlyQ3J72kxF7VxvqwWX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq5l8L/dJMcadpmJXZ/l5mlyQ3J72kxF7VxvqwWX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq5l8L%2FdJMcadpmJXZ%2Fl5mlyQ3J72kxF7VxvqwWX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Cursor] Google Workspace 플러그인 6종, 에디터 안으로 들어왔어요&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Cursor] Google Workspace 플러그인 6종, 에디터 안으로 들어왔어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor가 8월 3일 Google Workspace 플러그인을 공개했어요. Gmail&amp;middot;Drive&amp;middot;Calendar&amp;middot;Docs&amp;middot;Sheets&amp;middot;Slides를 MCP로 붙여서 에디터를 안 나가고 읽고 쓰게 하는 발표예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 읽기만이 아니라 쓰기까지 열려 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 플러그인 저장소에 적힌 동작 범위는 이래요. Gmail은 메일 검색&amp;middot;읽기에 초안 작성, 라벨 지정, 메일 관리까지. Drive는 파일 검색&amp;middot;읽기에 생성과 공유, 관리까지 들어가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calendar는 캘린더 목록과 일정 검색, 미팅 생성&amp;middot;수정. Docs는 문서 읽기와 수정. Sheets는 스프레드시트 읽기와 값&amp;middot;수식 수정. Slides는 프레젠테이션 읽기와 수정이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여섯 개 다 조회에서 끝나지 않고 생성&amp;middot;수정 권한이 같이 붙고, Gmail은 발송까지 들어가요. 편의보다 이 권한 범위를 먼저 보게 되는 구성이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. changelog에 적힌 Google Chat은 마켓플레이스에 없어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 본문은 여섯 번째로 Google Chat(스페이스&amp;middot;메시지 읽기, 메시지&amp;middot;DM 발송)을 적어뒀어요. 그런데 마켓플레이스와 공식 플러그인 저장소에는 Chat이 안 보이고 대신 Slides가 올라와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 문구와 실제 배포 목록이 어긋난 상태예요. Chat 연동을 기대하고 설치하러 가면 헛걸음이 될 수 있고, 지금 붙일 수 있는 건 Slides까지 포함한 여섯 개예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 연결 대상은 Google이 돌리는 원격 MCP 서버예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 플러그인 설명에는 Google의 remote MCP server로 연결한다고 나와 있어요. Cursor가 통합을 직접 구현한 게 아니라 Google이 운영하는 MCP 서버에 붙는 플러그인을 Cursor가 배포하는 구조예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 cursor.com/marketplace에서 둘러보고, 설치는 에디터 안 Customize 페이지에서 해요. 설치 범위는 프로젝트 단위와 사용자 단위 중에 고르면 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 팀에 깔아도 인증은 각자 해야 할 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 문서에는 MCP 플러그인이면 개발자마다 MCP 제공자에 따로 인증이 필요할 수 있다고 나와 있어요. 팀 마켓플레이스에 올려둔다고 팀원 전원이 자동으로 연결되지는 않아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 마켓플레이스는 Teams 플랜에서 1개, Enterprise에서 무제한이에요. Enterprise는 관리자만 팀 마켓플레이스를 추가하고, 설치 모드도 Default Off&amp;middot;Default On&amp;middot;Required 중에서 관리자가 정해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무용 구글 계정이라면 외부 앱 연동을 조직 관리자가 막아둔 환경일 수 있어요. 메일 발송과 문서 수정이 들어간 연동이라 사내 승인 절차는 각자 환경에서 확인해봐야 하는 영역이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://cursor.com/changelog/google-workspace-plugins&quot;&gt;Cursor &amp;mdash; Cursor for Google Workspace&lt;/a&gt;, &lt;a href=&quot;https://cursor.com/docs/plugins&quot;&gt;Cursor Docs &amp;mdash; Plugins&lt;/a&gt;, &lt;a href=&quot;https://cursor.com/marketplace&quot;&gt;Cursor Marketplace&lt;/a&gt;, &lt;a href=&quot;https://github.com/cursor/plugins&quot;&gt;cursor/plugins&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Cursor로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>ai코딩도구</category>
      <category>cursor</category>
      <category>gmail</category>
      <category>GoogleWorkspace</category>
      <category>mcp</category>
      <category>구글드라이브</category>
      <category>기능출시</category>
      <category>플러그인</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/588</guid>
      <comments>https://jessyt.tistory.com/588#entry588comment</comments>
      <pubDate>Tue, 4 Aug 2026 07:00:46 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Async 함정 정리: 스레드풀 설정, 예외 증발, 트랜잭션&amp;middot;컨텍스트 미전파</title>
      <link>https://jessyt.tistory.com/501</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qOXgk/dJMcaiQ6h1y/PVF7K9JuJff7ryRGK2Ul4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qOXgk/dJMcaiQ6h1y/PVF7K9JuJff7ryRGK2Ul4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qOXgk/dJMcaiQ6h1y/PVF7K9JuJff7ryRGK2Ul4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqOXgk%2FdJMcaiQ6h1y%2FPVF7K9JuJff7ryRGK2Ul4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] @Async 함정 정리: 스레드풀 설정, 예외 증발, 트랜잭션&amp;middot;컨텍스트 미전파&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] @Async 함정 정리: 스레드풀 설정, 예외 증발, 트랜잭션&amp;middot;컨텍스트 미전파&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Async&lt;/code&gt; 한 줄이면 비동기가 되니 가볍게 쓰기 쉬운데, 이 애너테이션은 함정 밀도가 유난히 높아요. 기본 스레드풀은 운영에 부적합하고, 예외는 조용히 사라져요. 트랜잭션&amp;middot;인증&amp;middot;로그 추적은 끊겨요. 셋 다 &quot;장애 났는데 아무 데도 안 보이는&quot; 종류라 미리 알아야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async의 동작(역시 프록시예요)에서 출발해, 기본 풀의 무한 큐 함정, void 예외 증발, ThreadLocal 미전파(트랜잭션&amp;middot;SecurityContext&amp;middot;MDC)를 하나씩 헤집어볼게요. &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;앞 편&lt;/a&gt; 말미의 &quot;AFTER_COMMIT + @Async&quot; 조합을 쓰려면 이 글이 선행이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. @Async도 프록시예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 익숙한 이야기부터 &amp;mdash; @Async도 &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;프록시 기반&lt;/a&gt;이에요. 자기호출&amp;middot;private에서 조용히 동기로 실행돼요. &quot;비동기인 줄 알았는데 응답이 느려요&quot;의 1번 점검 항목이에요. &lt;code&gt;@EnableAsync&lt;/code&gt;를 빼먹어도 마찬가지로 조용히 동기예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 기본 스레드풀, 큐가 무한이라 8개에서 멈춰요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGOrwA/dJMcagTjJcp/bZR0FRU00stntamrWXCKqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGOrwA/dJMcagTjJcp/bZR0FRU00stntamrWXCKqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGOrwA/dJMcagTjJcp/bZR0FRU00stntamrWXCKqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGOrwA%2FdJMcagTjJcp%2FbZR0FRU00stntamrWXCKqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 부트 기본 Async 풀의 함정. 큐가 무제한이라 스레드 풀의 규칙상 큐부터 채워져 스레드가 코어 8개에서 멈추고, 작업이 큐에 무한정 쌓인다. 풀을 직접 정의하고 큐를 유한하게 둬야 한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 @Async용으로 &lt;code&gt;ThreadPoolTaskExecutor&lt;/code&gt;를 자동 구성해주는데, 기본값이 &lt;b&gt;core 8, 큐 무제한&lt;/b&gt;이에요. 여기에 스레드풀의 동작 규칙이 겹치면 함정이 완성돼요 &amp;mdash; 풀은 core가 다 차면 &lt;b&gt;큐부터 채우고&lt;/b&gt; 큐가 가득 차야 max까지 스레드를 늘려요. 큐가 무한이면? 영원히 안 차니 스레드는 8개 고정이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 작업이 몰리면 수만 개가 큐에서 대기해요. &quot;비동기로 보냈는데 한참 뒤에 실행돼요&quot;와 큐 메모리 증가가 그 증상이에요. 그래서 @Async를 운영에 쓰려면 풀 정의가 사실상 필수예요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Bean(name = &quot;notificationExecutor&quot;)
public Executor notificationExecutor() {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setMaxPoolSize(32);
    executor.setQueueCapacity(500);            // 유한해야 max가 의미 있다
    executor.setThreadNamePrefix(&quot;notify-&quot;);   // 스레드 덤프에서 식별
    executor.setRejectedExecutionHandler(
        new ThreadPoolExecutor.CallerRunsPolicy());  // 넘치면 호출자가 실행 (백프레셔)
    executor.initialize();
    return executor;
}

@Async(&quot;notificationExecutor&quot;)   // 풀 이름 명시
public void sendPush(Long orderId) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 게, 사용자 정의 Executor 빈이 하나라도 생기면 Boot의 기본 &lt;code&gt;applicationTaskExecutor&lt;/code&gt; 자동구성이 물러나요. 한정자 없는 &lt;code&gt;@Async&lt;/code&gt;까지 전부 이 풀로 흘러가니, 기본 풀을 유지하려면 &lt;code&gt;spring.task.execution.*&lt;/code&gt;도 같이 챙겨요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;두 가지를 챙겨요. 첫째, &lt;b&gt;큐는 유한하게 + 넘칠 때의 정책을 명시&lt;/b&gt;해요. CallerRunsPolicy는 &quot;넘치면 호출자 스레드가 직접 실행&quot;이라 자연스러운 속도 조절(백프레셔)이 되고, 유실은 없어요. 다만 executor가 shutdown 중이면 조용히 버려질 수 있고, 호출자가 요청 스레드면 그 요청의 응답이 느려지는 비용이 있어요. 둘째, &lt;b&gt;용도별로 풀을 분리&lt;/b&gt;해요. 알림용과 정산용이 한 풀을 쓰면, 느린 정산 작업이 풀을 점거해 알림까지 굶어요. @Async(&quot;풀이름&quot;)으로 격리하는 게 운영의 기본이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 예외 증발, void 비동기의 침묵&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecGO5u/dJMcagTjJcq/g2wzkwK4VxMBplTO8ZIdFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecGO5u/dJMcagTjJcq/g2wzkwK4VxMBplTO8ZIdFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecGO5u/dJMcagTjJcq/g2wzkwK4VxMBplTO8ZIdFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecGO5u%2FdJMcagTjJcq%2Fg2wzkwK4VxMBplTO8ZIdFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Async void 예외 증발. 동기 호출은 예외가 콜스택을 타고 올라오지만, 비동기는 호출자가 이미 떠나 예외 받을 사람이 없어 기본 로그만 남고 사라진다. CompletableFuture 반환이나 AsyncUncaughtExceptionHandler로 대응한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;동기 호출의 예외는 콜스택을 타고 호출자에게 가요. 그런데 @Async void 메서드의 예외는 &lt;b&gt;받을 사람이 없어요&lt;/b&gt; &amp;mdash; 호출자는 이미 자기 갈 길을 갔으니까요. 스프링이 기본 로그를 남기긴 하지만 에러 응답도 알림도 없이 조용해요. &quot;푸시가 안 나가는데 에러도 없어요&quot;의 정체예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대응은 반환 타입에 따라 둘이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결과가 필요하면 &lt;code&gt;CompletableFuture&lt;/code&gt; 반환&lt;/b&gt; &amp;mdash; 호출자가 &lt;code&gt;exceptionally()&lt;/code&gt;&amp;middot;&lt;code&gt;join()&lt;/code&gt;으로 예외를 받아 처리해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fire-and-forget이면 전역 핸들러 등록&lt;/b&gt; &amp;mdash; &lt;code&gt;AsyncConfigurer&lt;/code&gt;의 &lt;code&gt;getAsyncUncaughtExceptionHandler()&lt;/code&gt;로 공통 처리(에러 로그 + 알림 + 메트릭)를 박아요. 최소한 모니터링에는 보이게요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 컨텍스트 미전파, 새 스레드는 빈손이에요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm1g4H/dJMcaftjuC7/jO5t1m1I55WsXRTipwypKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm1g4H/dJMcaftjuC7/jO5t1m1I55WsXRTipwypKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm1g4H/dJMcaftjuC7/jO5t1m1I55WsXRTipwypKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm1g4H%2FdJMcaftjuC7%2FjO5t1m1I55WsXRTipwypKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Async 컨텍스트 미전파. 요청 스레드의 트랜잭션, SecurityContext, MDC는 전부 ThreadLocal이라 풀 스레드에 없다. 필요한 값은 파라미터로 전달하고 MDC는 TaskDecorator로 복사하며 트랜잭션은 비동기 작업 자체의 것으로 설계한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 많은 것들이 ThreadLocal에 살아요 &amp;mdash; 트랜잭션, SecurityContext(인증 정보), MDC(로그 traceId). @Async로 넘어간 작업은 &lt;b&gt;다른 스레드&lt;/b&gt;라 이게 전부 없어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션&lt;/b&gt; &amp;mdash; 호출자의 트랜잭션과 완전히 별개예요. 호출자가 롤백해도 비동기 작업은 이미 실행됐고, 같이 묶을 방법도 없어요. 비동기 작업은 자체 트랜잭션(&lt;code&gt;@Transactional&lt;/code&gt;을 비동기 메서드에)으로 설계하고, &quot;커밋 확정 후 실행&quot;이 필요하면 &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;AFTER_COMMIT&lt;/a&gt;과 조합해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SecurityContext&lt;/b&gt; &amp;mdash; 비동기 안에서 인증 정보가 null이에요. 사용자 ID가 필요하면 파라미터로 넘기는 게 명시적이고 안전해요. 자동 전파가 꼭 필요하면 &lt;code&gt;DelegatingSecurityContextAsyncTaskExecutor&lt;/code&gt;로 풀을 감싸요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MDC&lt;/b&gt; &amp;mdash; traceId가 끊겨 비동기 로그가 추적에서 빠져요. &lt;code&gt;TaskDecorator&lt;/code&gt;로 제출 시점의 MDC를 복사해주는 게 정석이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;executor.setTaskDecorator(runnable -&amp;gt; {
    Map&amp;lt;String, String&amp;gt; mdc = MDC.getCopyOfContextMap();   // 제출 시점 복사
    return () -&amp;gt; {
        if (mdc != null) MDC.setContextMap(mdc);
        try { runnable.run(); } finally { MDC.clear(); }
    };
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙은 하나예요 &amp;mdash; &lt;b&gt;암묵적 컨텍스트에 기대지 말고 필요한 값은 파라미터로.&lt;/b&gt; 자동 전파 장치들은 그게 번거로울 때의 보조예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Async가 동기로 실행돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기호출이거나 @EnableAsync 누락이에요. 스레드 이름 로그로 확인하면 1분이면 갈려요(&lt;code&gt;Thread.currentThread().getName()&lt;/code&gt;).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 작업이 한참 뒤에 실행돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 풀의 무한 큐 적체예요. 풀을 정의하고 큐를 유한하게, 풀 지표(active&amp;middot;queue size)를 모니터링에 올려요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 안에서 LazyInitializationException이 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출자의 영속성 컨텍스트가 새 스레드에 없어서예요(&lt;a href=&quot;https://jessyt.tistory.com/482&quot;&gt;지연 로딩&lt;/a&gt;의 그 예외). 엔티티를 넘기지 말고 ID나 DTO를 넘긴 뒤, 비동기 쪽에서 자체 트랜잭션으로 다시 조회해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 로그가 traceId 없이 찍혀요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDC 미전파예요. TaskDecorator를 등록해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async를 운영에 쓰려면 세 가지가 세트로 따라와야 해요. 이 셋 없이 붙인 @Async는 &quot;평소엔 되는데 장애 때 아무것도 안 보이는&quot; 코드가 돼요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;① 풀 직접 정의&lt;/b&gt; &amp;mdash; 유한 큐 + 거부 정책 + 용도별 분리. 기본 풀의 무한 큐를 피해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;② 예외 처리 명시&lt;/b&gt; &amp;mdash; CompletableFuture 반환 또는 전역 핸들러. void 예외가 증발하지 않게요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;③ 컨텍스트는 파라미터로&lt;/b&gt; &amp;mdash; 트랜잭션은 자체로, MDC는 TaskDecorator로. ThreadLocal은 스레드를 못 넘어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 의존성 주입의 발밑이에요 &amp;mdash; 순환 참조와 빈 초기화 함정을 &lt;a href=&quot;https://jessyt.tistory.com/502&quot;&gt;이어지는 글&lt;/a&gt;에서 다뤄요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html#scheduling-annotation-support-async&quot;&gt;Spring Framework &amp;mdash; @Async&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html&quot;&gt;Spring Boot &amp;mdash; Task Execution&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>async</category>
      <category>Spring</category>
      <category>taskdecorator</category>
      <category>ThreadPoolTaskExecutor</category>
      <category>백엔드</category>
      <category>비동기</category>
      <category>스레드풀</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/501</guid>
      <comments>https://jessyt.tistory.com/501#entry501comment</comments>
      <pubDate>Mon, 3 Aug 2026 07:30:51 +0900</pubDate>
    </item>
    <item>
      <title>[HTTPie] curl 대신 쓰는 CLI HTTP 클라이언트</title>
      <link>https://jessyt.tistory.com/587</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/dWwzbB/dJMcagzEEZS/AAAAAAAAAAAAAAAAAAAAAEhFjo5ZzdqDORM8_FsTfEEgQ417b4iGFj5KZNbqUokL/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1788188399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=qJINQ0rtWHt0eGUjmnoZMAaEl%2F8%3D&quot; alt=&quot;[HTTPie] curl 대신 쓰는 CLI HTTP 클라이언트&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;gt; API &amp;amp; DB&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[HTTPie] curl 대신 쓰는 CLI HTTP 클라이언트&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;HTTPie는 터미널에서 API를 때릴 때 curl 옵션을 외우지 않아도 되게 만든 도구예요. JSON 본문이 기본값이에요. 응답은 색이 입혀진 채로 나오고요. 설치부터 요청 문법, 세션 유지, 자주 걸리는 함정까지 정리했어요.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;curl을 못 읽어서 생긴 도구예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 API 하나 확인하려고 curl 명령을 쓰면, 정작 중요한 URL과 본문은 &lt;code&gt;-X&lt;/code&gt; &lt;code&gt;-H&lt;/code&gt; &lt;code&gt;-d&lt;/code&gt; 사이에 파묻혀요. JSON을 보내려면 헤더를 손으로 붙여야 해요. 따옴표도 이스케이프해야 하죠. 돌아온 응답은 한 줄로 뭉쳐 나와서 다시 &lt;code&gt;jq&lt;/code&gt;에 파이프를 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPie는 그 지점을 고친 CLI HTTP 클라이언트예요. 공식 문서는 스스로를 &quot;API 시대를 위한 명령줄 HTTP&amp;middot;API 테스트 클라이언트&quot;로 소개해요. 목표는 &quot;웹 서비스와의 CLI 상호작용을 최대한 사람 친화적으로&quot;라고 적혀 있고요. 라이선스는 BSD-3-Clause 오픈소스고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체 대상은 두 갈래예요. 하나는 curl의 가독성, 다른 하나는 GUI 클라이언트를 켰다 껐다 하는 왕복이에요. 요청 하나 확인하겠다고 앱을 띄우고 컬렉션을 뒤지는 대신, 셸 히스토리에 남는 한 줄로 끝내는 쪽에 가까워요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;하려는 일&lt;/th&gt;
&lt;th&gt;curl&lt;/th&gt;
&lt;th&gt;HTTPie&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl -X POST -H &quot;Content-Type: application/json&quot; -d '{&quot;name&quot;:&quot;John&quot;}' ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http POST ... name=John&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;헤더 추가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-H &quot;X-API-Token: 123&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X-API-Token:123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쿼리스트링&lt;/td&gt;
&lt;td&gt;URL에 직접 &lt;code&gt;?token=secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;token==secret&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;응답 보기&lt;/td&gt;
&lt;td&gt;한 줄 출력, &lt;code&gt;| jq&lt;/code&gt; 필요&lt;/td&gt;
&lt;td&gt;기본으로 포매팅&amp;middot;색상 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;직접 지정&lt;/td&gt;
&lt;td&gt;데이터 항목이 있으면 JSON 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치하고 첫 요청까지 명령 두 줄이면 돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 매니저 대부분에 올라가 있어요. macOS는 Homebrew, Windows는 Chocolatey, 리눅스는 배포판 저장소나 공식 apt 저장소를 씁니다. OS를 가리지 않는 경로로는 pip가 있어요. 문서에는 apt&amp;middot;brew&amp;middot;choco&amp;middot;pip&amp;middot;port&amp;middot;snap&amp;middot;yum이 나열돼 있어요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# macOS
brew update &amp;amp;&amp;amp; brew install httpie

# Windows
choco install httpie

# 어디서나 (Python)
python -m pip install --upgrade pip wheel
python -m pip install httpie&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치하면 &lt;code&gt;http&lt;/code&gt;와 &lt;code&gt;https&lt;/code&gt; 두 개의 실행 파일이 깔려요. 이름이 곧 기본 스킴이라서, &lt;code&gt;https example.org&lt;/code&gt;는 &lt;code&gt;https://example.org&lt;/code&gt;로 나가요. 요청 문법의 뼈대는 &lt;code&gt;http [플래그] [메서드] URL [항목...]&lt;/code&gt; 한 줄이에요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;http PUT pie.dev/put X-API-Token:123 name=John&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 생략해도 돼요. 데이터 항목이 없으면 GET, 있으면 POST로 잡히기 때문에 조회는 &lt;code&gt;http pie.dev/get&lt;/code&gt;처럼 URL만 던지면 끝나요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 개발 서버는 콜론 하나로 줄여요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트만 바꿔 가며 로컬 서버를 두드릴 일이 많은데, 여기에 단축 문법이 따로 있어요. 콜론으로 시작하면 호스트가 localhost로 채워져요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;http :3000/bar     # GET /bar &amp;rarr; Host: localhost:3000
http :/foo         # GET /foo &amp;rarr; Host: localhost
http :              # GET /   &amp;rarr; Host: localhost&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트를 안 적으면 80으로 봐요. 플러그인으로 다른 스킴을 쓸 때는 &lt;code&gt;--default-scheme&lt;/code&gt;로 기본값을 바꿔 별칭을 만들어 두는 방식도 문서에 나와 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기호 여섯 개가 요청 문법의 거의 전부예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPie 문법에서 외울 건 URL 뒤에 붙는 &quot;요청 항목&quot;의 구분 기호예요. 왼쪽 이름과 오른쪽 값 사이에 무슨 기호를 쓰느냐로 헤더인지, 본문인지, 쿼리스트링인지가 갈려요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;= 와 := &amp;mdash; 본문 필드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;name=John&lt;/code&gt;은 문자열 필드, &lt;code&gt;age:=29&lt;/code&gt;는 원시 JSON 필드예요. 숫자&amp;middot;불리언&amp;middot;배열&amp;middot;객체처럼 따옴표가 붙으면 안 되는 값은 &lt;code&gt;:=&lt;/code&gt; 쪽을 써야 해요. 데이터 항목이 하나라도 있으면 기본 직렬화가 JSON 객체라서, Content-Type을 따로 지정할 필요가 없어요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;http POST pie.dev/post name=John age:=29 admin:=false tags:='[&quot;a&quot;,&quot;b&quot;]'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;: 와 == &amp;mdash; 헤더와 쿼리 파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜론은 헤더예요. &lt;code&gt;X-API-Token:123&lt;/code&gt;처럼 씁니다. 값 없이 &lt;code&gt;Header:&lt;/code&gt;로 끝내면 기본 헤더가 지워져요. 등호 두 개는 쿼리 파라미터라서 &lt;code&gt;token==secret&lt;/code&gt;이 URL 뒤에 &lt;code&gt;?token=secret&lt;/code&gt;으로 붙어요. URL 인코딩은 HTTPie가 처리해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ 와 =@ &amp;mdash; 파일 붙이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@&lt;/code&gt;는 폼 업로드예요. &lt;code&gt;screenshot@~/Pictures/img.png&lt;/code&gt;처럼 쓰면 되는데, 이건 폼 모드에서만 동작해요. 파일 내용을 값으로 넣고 싶으면 &lt;code&gt;=@&lt;/code&gt;(텍스트)나 &lt;code&gt;:=@&lt;/code&gt;(JSON)를 쓰고요. 긴 JSON 본문을 셸에 그대로 붙여넣는 대신 파일로 빼 둘 때 쓸 만해요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# 폼&amp;middot;멀티파트 전송은 -f
http -f POST pie.dev/post name='John Smith' cv@~/files/data.xml

# 파일 내용을 값으로
http POST pie.dev/post description=@files/text.txt bookmarks:=@files/data.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 필드가 하나라도 섞이면 직렬화가 &lt;code&gt;multipart/form-data&lt;/code&gt;로 바뀐다고 문서에 명시돼 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응답에서 볼 부분을 직접 골라요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 출력은 응답 헤더와 본문이에요. 여기서 더 보거나 덜 보고 싶을 때 쓰는 플래그가 나뉘어 있어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-v&lt;/code&gt; / &lt;code&gt;--verbose&lt;/code&gt; &amp;mdash; 요청과 응답 전체를 다 출력. 무엇을 보냈는지까지 확인할 때&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h&lt;/code&gt; / &lt;code&gt;--headers&lt;/code&gt;, &lt;code&gt;-b&lt;/code&gt; / &lt;code&gt;--body&lt;/code&gt; &amp;mdash; 응답 헤더만, 본문만&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt; / &lt;code&gt;--print&lt;/code&gt; &amp;mdash; 문자 코드로 조합. H=요청 헤더, B=요청 본문, h=응답 헤더, b=응답 본문, m=응답 메타데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--pretty=all|colors|format|none&lt;/code&gt; &amp;mdash; 색상과 정렬을 켜고 끔. 파이프로 넘길 때 &lt;code&gt;none&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;http -v POST pie.dev/post name=John      # 주고받은 전체
http -p Hb pie.dev/get                   # 요청 헤더 + 응답 본문만&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 파일 하나로 로그인 상태를 들고 다녀요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 &lt;code&gt;-a user:pass&lt;/code&gt;가 기본이에요. 비밀번호를 빼고 &lt;code&gt;-a user&lt;/code&gt;까지만 적으면 프롬프트로 물어봐요. digest나 bearer 같은 방식은 &lt;code&gt;-A&lt;/code&gt; / &lt;code&gt;--auth-type&lt;/code&gt;으로 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 &lt;code&gt;--session&lt;/code&gt;을 붙이면 헤더&amp;middot;인증&amp;middot;쿠키가 이름 붙은 세션에 저장돼요. 다음 요청부터는 세션 이름만 넘기면 인증 정보를 다시 안 적어도 되고요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# 세션 만들면서 인증
http --session=user1 -a username:password pie.dev/get

# 이후 재사용
http --session=user1 pie.dev/headers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 위치는 유닉스 계열이 &lt;code&gt;~/.config/httpie/sessions/&amp;lt;host&amp;gt;/&amp;lt;name&amp;gt;.json&lt;/code&gt;, 윈도우가 &lt;code&gt;%APPDATA%\httpie\sessions\...&lt;/code&gt;예요. 이름 대신 파일 경로를 주면 익명 세션이 됩니다. 세션을 갱신하지 않고 읽기만 할 때는 &lt;code&gt;--session-read-only&lt;/code&gt;를 써요. 설정 파일은 &lt;code&gt;~/.config/httpie/config.json&lt;/code&gt;에 있어요. 위치는 &lt;code&gt;HTTPIE_CONFIG_DIR&lt;/code&gt; 환경변수로 바꿉니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내려받기와 오프라인 조립&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-d&lt;/code&gt; / &lt;code&gt;--download&lt;/code&gt;를 붙이면 wget처럼 진행 표시줄을 띄우고 파일로 저장해요. 파일명은 &lt;code&gt;-o&lt;/code&gt;로 직접 주거나, 서버의 Content-Disposition 헤더, 또는 URL과 콘텐츠 타입 조합으로 정해져요. 중간에 끊긴 다운로드는 &lt;code&gt;--continue&lt;/code&gt;로 이어받고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;code&gt;--offline&lt;/code&gt;은 요청을 보내지 않고 조립 결과만 표준출력으로 찍고 끝나요. 이 옵션이 켜지면 &lt;code&gt;--print=HB&lt;/code&gt;가 자동으로 붙어요. 서버를 건드리지 않고 &quot;이 명령이 실제로 어떤 요청이 되는가&quot;만 확인하는 용도예요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;http -dco file.zip example.org/file     # 다운로드 + 이어받기 + 파일명 지정
http --offline POST pie.dev/post name=John age:=29&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 걸리지 않을 것들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; 업로드는 폼 전용이에요. &lt;code&gt;-f&lt;/code&gt; 없이 쓰면 의도한 멀티파트가 안 나와요&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:=&lt;/code&gt;를 안 쓰면 숫자도 문자열로 나가요. &lt;code&gt;age=29&lt;/code&gt;와 &lt;code&gt;age:=29&lt;/code&gt;는 서버에서 다른 값이에요&lt;/li&gt;
&lt;li&gt;배열&amp;middot;객체 값은 셸이 먼저 해석해요. &lt;code&gt;tags:='[&quot;a&quot;,&quot;b&quot;]'&lt;/code&gt;처럼 작은따옴표로 감싸는 습관이 필요해요&lt;/li&gt;
&lt;li&gt;출력을 파이프로 넘길 때는 색상 이스케이프가 섞여요. &lt;code&gt;--pretty=none&lt;/code&gt;으로 끄는 편이 안전해요&lt;/li&gt;
&lt;li&gt;리다이렉트는 기본으로 따라가지 않아요. &lt;code&gt;-F&lt;/code&gt; / &lt;code&gt;--follow&lt;/code&gt;를 붙여야 해요. 중간 응답까지 보려면 &lt;code&gt;--all&lt;/code&gt;을 씁니다. 기본 최대 리다이렉트는 30회예요&lt;/li&gt;
&lt;li&gt;사설 인증서 환경은 &lt;code&gt;--verify=no&lt;/code&gt;로 넘기기보다 &lt;code&gt;--verify=/path/to/ca_bundle&lt;/code&gt;로 CA 번들을 지정하는 쪽이 맞아요. 클라이언트 인증서는 &lt;code&gt;--cert&lt;/code&gt;, &lt;code&gt;--cert-key&lt;/code&gt;로 붙여요&lt;/li&gt;
&lt;li&gt;pip로 설치했다면 플러그인도 &lt;code&gt;httpie cli plugins install&lt;/code&gt; 명령으로 관리해요. pip에 직접 설치하는 것과 경로가 달라요&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;설치 참고&lt;/span&gt; Homebrew&amp;middot;apt 같은 시스템 패키지로 깐 경우와 pip로 깐 경우가 섞이면 &lt;code&gt;http&lt;/code&gt; 실행 파일이 어느 쪽인지 헷갈리기 쉬워요. &lt;code&gt;which http&lt;/code&gt;로 경로를 먼저 확인하세요. 플러그인은 그 설치본 기준으로 &lt;code&gt;httpie cli plugins list&lt;/code&gt;에 잡히는지 보면 됩니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 사람에게 맞나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞는 쪽은 명확해요. 로컬 서버를 띄워 놓고 엔드포인트를 반복해서 두드리는 개발 단계, JSON 본문이 주력인 API, 그리고 명령이 셸 히스토리와 스크립트에 남아야 하는 상황이에요. &lt;code&gt;http :3000/api/users name=John&lt;/code&gt; 정도면 팀 채널에 그대로 붙여넣어도 상대가 바로 읽어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 curl을 완전히 대체하지는 않아요. CI 이미지나 서버 안에는 curl이 이미 들어 있고 HTTPie는 별도 설치가 필요하니, 배포 스크립트까지 갈아치울 이유는 크지 않아요. 컬렉션 공유나 팀 단위 API 문서화가 목적이라면 Bruno&amp;middot;Postman 같은 클라이언트 쪽 영역이고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 가볍게 할 수 있어요. 설치한 뒤 &lt;code&gt;http --offline&lt;/code&gt;으로 평소 쓰던 curl 명령 하나를 HTTPie 문법으로 옮겨 보세요. 조립된 요청이 같은지 눈으로 맞춰 보는 순서가 무난해요. 여기서 기호 여섯 개에 익숙해지면 나머지는 필요할 때 문서에서 찾아 쓰면 되는 수준이에요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;div class=&quot;yt-sources__head&quot;&gt;출처&lt;/div&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://httpie.io/docs/cli&quot;&gt;HTTPie CLI 공식 문서&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://httpie.io/docs/cli/url-shortcuts-for-localhost&quot;&gt;HTTPie Docs &amp;mdash; URL shortcuts for localhost&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://httpie.io/cli&quot;&gt;HTTPie CLI 제품 페이지 (설치 방법)&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/httpie/cli&quot;&gt;httpie/cli GitHub 저장소 (BSD-3-Clause)&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 직접 사용기가 아니라 공식 문서와 저장소를 근거로 한 객관 정리예요. 명령&amp;middot;플래그&amp;middot;경로는 작성 시점 문서 기준이에요. 버전에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/API &amp;amp; DB</category>
      <category>API</category>
      <category>CLI</category>
      <category>Curl</category>
      <category>httpie</category>
      <category>터미널</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/587</guid>
      <comments>https://jessyt.tistory.com/587#entry587comment</comments>
      <pubDate>Mon, 3 Aug 2026 06:00:29 +0900</pubDate>
    </item>
    <item>
      <title>[DeepSeek] V4 Flash 정식 공개, 코딩 에이전트 점수가 확 올랐어요</title>
      <link>https://jessyt.tistory.com/586</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QwpAh/dJMcahFh4QM/cyEFfRenKbc9RaQDVcAxbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QwpAh/dJMcahFh4QM/cyEFfRenKbc9RaQDVcAxbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QwpAh/dJMcahFh4QM/cyEFfRenKbc9RaQDVcAxbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQwpAh%2FdJMcahFh4QM%2FcyEFfRenKbc9RaQDVcAxbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[DeepSeek] V4 Flash 정식 공개, 코딩 에이전트 점수가 확 올랐어요&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[DeepSeek] V4 Flash 정식 공개, 코딩 에이전트 점수가 확 올랐어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepSeek이 7월 31일 V4 Flash를 프리뷰에서 정식 릴리즈(공개 베타)로 전환했어요. 체크포인트 이름은 DeepSeek-V4-Flash-0731이고, 코딩 에이전트와 tool use 성능을 집중적으로 올린 업데이트예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 새 모델이 아니라 같은 모델의 재훈련이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처와 파라미터 수는 프리뷰 버전과 같아요. 포스트트레이닝만 다시 한 체크포인트라서, 기존에 deepseek-v4-flash를 쓰던 코드는 호출 방법을 바꿀 게 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 릴리즈에서 Responses API 포맷 지원이 추가됐어요. 멀티스텝 작업을 시키는 에이전트 앱을 만들 때 쓰는 포맷이에요. 상위 모델인 v4-pro는 아직 미지원이고, 8월 초 지원 예정으로 알려져 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. Terminal-Bench가 61.8에서 82.7로 뛰었다고 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepSeek 발표 기준으로 Terminal-Bench 2.1 점수가 프리뷰 빌드 61.8에서 82.7로 올랐어요. 상위 모델인 V4-Pro-Preview의 72.1보다도 높은 점수예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepSWE 54.4, Toolathlon(verified) 70.3도 같이 실렸어요. Terminal-Bench 기준으로 보면 GLM-5.2의 81.0을 넘고 Claude Opus 4.8의 85.0에 가까운 수치인데, 모두 발표 자료 기준이라 실제 체감은 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 출력 100만 토큰에 $0.28이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 가격 문서 기준으로 input은 cache hit $0.0028, cache miss $0.14, output은 $0.28이에요(100만 토큰당). v4-pro 가격의 대략 3분의 1 수준이에요. context window는 1M 토큰, 최대 출력은 384K 토큰이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 알아둘 게 있어요. 공식 가격 문서 공지 기준으로 DeepSeek API에 조만간 피크/오프피크 요금제가 도입될 예정인데, 피크 시간에는 가격이 2배가 된다고 해요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;피크 시간 = 베이징 기준 9~12시, 14~18시. 한국시간으로는 10~13시, 15~19시라 한국 업무 시간과 거의 겹쳐요. 배치성 작업이라면 시간대를 피하는 쪽이 요금에 유리해요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. Claude Code에 그대로 꽂아 쓸 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepSeek API는 OpenAI 호환 포맷과 Anthropic 호환 포맷을 둘 다 제공해요. Anthropic 포맷 엔드포인트(api.deepseek.com/anthropic)를 쓰면 Claude Code 같은 도구에 코드 수정 없이 연결된다고 공식 문서에 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thinking mode, JSON output, tool call, context caching도 같이 지원해요. 에이전트 코딩 도구를 저비용 모델로 돌려보고 싶었다면 시도해볼 만한 선택지가 하나 늘어난 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://api-docs.deepseek.com/news/news250731&quot;&gt;DeepSeek &amp;mdash; DeepSeek-V4-Flash-0731 Release&lt;/a&gt;, &lt;a href=&quot;https://api-docs.deepseek.com/quick_start/pricing&quot;&gt;DeepSeek API Pricing&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, DeepSeek으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI모델</category>
      <category>API가격</category>
      <category>claudecode</category>
      <category>deepseek</category>
      <category>LLM</category>
      <category>V4Flash</category>
      <category>신모델</category>
      <category>코딩에이전트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/586</guid>
      <comments>https://jessyt.tistory.com/586#entry586comment</comments>
      <pubDate>Sat, 1 Aug 2026 07:00:53 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @TransactionalEventListener 정리: AFTER_COMMIT, 이벤트와 트랜잭션 타이밍</title>
      <link>https://jessyt.tistory.com/500</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUAAxz/dJMcaccifCZ/DAzVQBPZrHBRLHhlPU8nN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUAAxz/dJMcaccifCZ/DAzVQBPZrHBRLHhlPU8nN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUAAxz/dJMcaccifCZ/DAzVQBPZrHBRLHhlPU8nN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUAAxz%2FdJMcaccifCZ%2FDAzVQBPZrHBRLHhlPU8nN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] @TransactionalEventListener 정리: AFTER_COMMIT, 이벤트와 트랜잭션 타이밍&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] @TransactionalEventListener 정리: AFTER_COMMIT, 이벤트와 트랜잭션 타이밍&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;주문이 롤백됐는데 고객한테 주문 완료 푸시가 나갔어요.&quot; 스프링 이벤트를 쓰다 한 번씩 겪는 사고예요. 이벤트로 결합을 끊는 것까지는 좋았는데, &lt;b&gt;리스너가 언제 실행되는지&lt;/b&gt;를 안 따진 거죠. 그 타이밍을 트랜잭션에 맞춰주는 게 @TransactionalEventListener예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트로 부가 로직을 분리하는 패턴, @EventListener의 타이밍 사고, 그리고 AFTER_COMMIT의 동작과 두 가지 함정(DB 쓰기 증발&amp;middot;동기 실행)을 차례로 풀어볼게요. &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;@Transactional 동작 원리&lt;/a&gt;를 알고 보면 전부 자연스러워요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 왜 이벤트인가, 핵심에서 부가를 떼기&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XdjNg/dJMcaip2PrX/gKEslRvaKQRE5maNn9SSM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XdjNg/dJMcaip2PrX/gKEslRvaKQRE5maNn9SSM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XdjNg/dJMcaip2PrX/gKEslRvaKQRE5maNn9SSM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXdjNg%2FdJMcaip2PrX%2FgKEslRvaKQRE5maNn9SSM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 이벤트로 결합 끊기. 주문 서비스가 알림과 포인트와 통계를 직접 호출하면 부가 기능 장애가 주문 실패가 되지만, 이벤트를 발행하면 각 부가 기능이 리스너로 분리되어 주문은 구독자를 모른다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;주문 생성에 알림&amp;middot;포인트&amp;middot;통계가 따라붙는다고 직접 호출로 엮으면, 알림 장애가 주문 실패가 되고 부가 기능이 늘 때마다 주문 코드를 고쳐요. 이벤트로 바꾸면 주문은 &quot;주문 생성됨&quot;을 발행하고 끝이에요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public void create(OrderCommand cmd) {
        Order order = orderRepository.save(Order.from(cmd));
        eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림&amp;middot;포인트&amp;middot;통계는 각자 리스너로 구독해요. 발행자는 구독자를 몰라요. 여기까지가 교과서고, 문제는 다음이에요 &amp;mdash; 그 리스너, &lt;b&gt;언제&lt;/b&gt; 실행되나요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. @EventListener의 사고, 롤백됐는데 알림 발송&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PjzPG/dJMcaccifCY/GKKtCKdaPNZImJoZqQB7Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PjzPG/dJMcaccifCY/GKKtCKdaPNZImJoZqQB7Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PjzPG/dJMcaccifCY/GKKtCKdaPNZImJoZqQB7Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPjzPG%2FdJMcaccifCY%2FGKKtCKdaPNZImJoZqQB7Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;EventListener 타이밍 사고. 트랜잭션 안에서 이벤트가 발행되어 리스너가 즉시 푸시를 발송했는데 직후 재고 차감 실패로 롤백되면, DB엔 주문이 없는데 고객은 주문 완료 알림을 받는다. AFTER_COMMIT은 커밋 확정 후에만 실행돼 이를 막는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;기본 &lt;code&gt;@EventListener&lt;/code&gt;는 발행 즉시, &lt;b&gt;발행한 트랜잭션 안에서&lt;/b&gt; 실행돼요. 그래서 이 시나리오가 가능해요 &amp;mdash; 주문 저장 &amp;rarr; 이벤트 발행 &amp;rarr; 리스너가 푸시 발송 &amp;rarr; 그 직후 재고 차감 실패로 전체 롤백. DB엔 주문이 없는데 고객은 &quot;주문 완료&quot; 알림을 받은 거예요. 푸시&amp;middot;메일&amp;middot;외부 API처럼 &lt;b&gt;롤백이 안 되는 부수효과&lt;/b&gt;를 트랜잭션 확정 전에 실행한 게 사고의 본질이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법이 &lt;code&gt;@TransactionalEventListener&lt;/code&gt;예요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OrderNotificationListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        pushService.send(event.getOrderId());   // 커밋 확정 후에만 실행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AFTER_COMMIT&lt;/code&gt;(기본값)이면 커밋이 확정된 뒤에만 리스너가 돌아요. 롤백되면 아예 실행 안 되고요. &quot;DB 상태와 외부 부수효과의 일치&quot;가 구조적으로 보장돼요. 반대로 롤백 시에만 실행(AFTER_ROLLBACK)이나 커밋 직전(BEFORE_COMMIT)도 phase로 고를 수 있어요 &amp;mdash; 검증 로직은 BEFORE_COMMIT이 어울려요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. AFTER_COMMIT의 함정 ①, 그 안의 DB 쓰기는 증발해요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/07n3t/dJMcaip2PrZ/q61S6ZGV3UkMdTPabj20d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/07n3t/dJMcaip2PrZ/q61S6ZGV3UkMdTPabj20d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/07n3t/dJMcaip2PrZ/q61S6ZGV3UkMdTPabj20d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F07n3t%2FdJMcaip2PrZ%2Fq61S6ZGV3UkMdTPabj20d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;AFTER_COMMIT 리스너의 두 함정. 리스너 시점엔 트랜잭션이 이미 끝나 그 안의 DB 쓰기는 저장되지 않으므로 REQUIRES_NEW가 필요하고, 기본이 동기 실행이라 느린 리스너가 응답 시간에 더해지므로 Async를 함께 쓴다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;AFTER_COMMIT 리스너 안에서 &lt;code&gt;repository.save()&lt;/code&gt;를 했는데 저장이 안 되는 미스터리가 있어요. 이유는 이름 그대로예요 &amp;mdash; 리스너가 도는 시점엔 &lt;b&gt;트랜잭션이 이미 커밋되고 끝나 있어요.&lt;/b&gt; &quot;커밋에 참여&quot;가 아니라 &quot;커밋 후&quot;라서, 그 안의 쓰기는 합류할 트랜잭션이 없어요. 스프링은 기존 트랜잭션의 잔재 동기화 구간에서 실행하기 때문에 새 커밋도 자동으로 일어나지 않아요. 조용히 증발해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너에서 DB를 써야 하면 새 트랜잭션을 명시해요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)   // 새 트랜잭션 필수
public void onOrderCreated(OrderCreatedEvent event) {
    historyRepository.save(OrderHistory.from(event));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;앞 편&lt;/a&gt;에서 본 REQUIRES_NEW가 정확히 필요한 자리예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 함정 ②, 기본은 동기 &amp;mdash; 응답 시간에 더해져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactionalEventListener도 기본은 &lt;b&gt;같은 스레드의 동기 실행&lt;/b&gt;이에요. 커밋 직후, 응답이 나가기 전에 리스너가 돌아요. 리스너가 외부 API를 2초 기다리면 사용자 응답도 2초 늦어요. 리스너의 예외도 (커밋은 이미 됐지만) 호출 흐름에 영향을 줄 수 있고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;커밋 후 + 비동기&quot; 조합이 흔해요 &amp;mdash; &lt;code&gt;@Async&lt;/code&gt;를 같이 붙이는 거예요. 단 비동기로 넘어가는 순간 새로운 함정 세트(스레드풀&amp;middot;예외 증발&amp;middot;컨텍스트 미전파)가 열리는데, 그게 바로 다음 편 주제예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;그리고 솔직한 한계 하나 &amp;mdash; AFTER_COMMIT + @Async여도 &lt;b&gt;&quot;커밋됐는데 이벤트 처리는 유실&quot;&lt;/b&gt;이 가능해요. 커밋 직후 서버가 죽으면 리스너는 영영 안 돌아요. 푸시 하나면 감수할 수 있지만, &quot;커밋됐으면 반드시 처리돼야 하는&quot; 메시지(타 시스템 연동 등)라면 인메모리 이벤트로는 부족해요. 그 보장이 필요할 때 꺼내는 게 아웃박스 패턴이고, 분산 패턴 시리즈에서 다뤄요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;롤백됐는데 알림&amp;middot;메일이 나갔어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EventListener의 즉시 실행이에요. 부수효과 리스너는 @TransactionalEventListener(AFTER_COMMIT)로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AFTER_COMMIT 리스너에서 save가 안 돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 이미 끝난 시점이라서예요. REQUIRES_NEW를 명시해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 발행했는데 리스너가 아예 안 돌아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactionalEventListener는 &lt;b&gt;트랜잭션이 없으면 기본적으로 실행되지 않아요.&lt;/b&gt; 트랜잭션 밖에서 발행한 이벤트가 조용히 무시되는 거예요(fallbackExecution = true로 바꿀 수 있지만, 먼저 &quot;왜 트랜잭션이 없지?&quot;를 봐야 해요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;자기호출&lt;/a&gt;일 수도요).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리스너 때문에 응답이 느려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 실행이에요. @Async를 붙이되, 다음 편의 비동기 함정(예외&amp;middot;스레드풀)을 같이 챙겨요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스너를 언제 부를까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 이벤트의 모든 결정은 한 질문으로 모여요 &amp;mdash; &lt;b&gt;&quot;리스너가 언제 도는가&quot;&lt;/b&gt;. 상황에 따라 답이 갈려요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;푸시&amp;middot;메일&amp;middot;외부 API 같은 부수효과&lt;/b&gt;라면 &amp;rarr; AFTER_COMMIT. @EventListener의 즉시 실행은 롤백과 어긋나 사고가 나요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리스너 안에서 DB를 써야 한다&lt;/b&gt;면 &amp;rarr; AFTER_COMMIT + REQUIRES_NEW. 커밋 후라 합류할 트랜잭션이 없어서예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리스너가 느리다&lt;/b&gt;면 &amp;rarr; @Async 추가. 단 그 순간 비동기 함정 세트가 열려요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유실이 절대 안 되는 메시지&lt;/b&gt;라면 &amp;rarr; 인메모리 이벤트 밖, 아웃박스의 영역이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 미뤄둔 @Async의 함정들은 &lt;a href=&quot;https://jessyt.tistory.com/501&quot;&gt;바로 다음 글&lt;/a&gt;에서 정면으로 풀어요. &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;@Transactional 동작 원리&lt;/a&gt;를 깔고 보면 위 분기가 전부 자연스러워요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/event.html&quot;&gt;Spring Framework &amp;mdash; Transaction-bound Events&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events&quot;&gt;Spring &amp;mdash; Application Events&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>after_commit</category>
      <category>ApplicationEventPublisher</category>
      <category>Spring</category>
      <category>TransactionalEventListener</category>
      <category>백엔드</category>
      <category>이벤트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/500</guid>
      <comments>https://jessyt.tistory.com/500#entry500comment</comments>
      <pubDate>Fri, 31 Jul 2026 07:30:03 +0900</pubDate>
    </item>
    <item>
      <title>[Cursor] iPad 앱 출시, PR 리뷰가 통째로 들어왔어요</title>
      <link>https://jessyt.tistory.com/584</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OHQ37/dJMcahL4dDF/Hpkp9ip8FH0YPKF2fNioKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OHQ37/dJMcahL4dDF/Hpkp9ip8FH0YPKF2fNioKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OHQ37/dJMcahL4dDF/Hpkp9ip8FH0YPKF2fNioKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOHQ37%2FdJMcahL4dDF%2FHpkp9ip8FH0YPKF2fNioKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Cursor] iPad 앱 출시, PR 리뷰가 통째로 들어왔어요&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Cursor] iPad 앱 출시, PR 리뷰가 통째로 들어왔어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor가 현지시간 7월 29일 iPad 앱을 출시했어요. 6월 말 iPhone 앱이 나온 지 한 달 만의 확장이에요. 이번에는 화면이 큰 만큼 PR 리뷰 화면이 코멘트&amp;middot;체크&amp;middot;승인까지 다 들어왔고, 유료 플랜이면 바로 쓸 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 유료 플랜이면 App Store에서 바로 받아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 기준으로 Cursor for iPad는 모든 유료 플랜에서 제공돼요. 별도 추가 요금 얘기는 없고, App Store에서 앱을 받아 기존 계정으로 로그인하는 방식이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6월 29일에는 iPhone 앱이 공개 베타로 먼저 나왔어요. 당시 발표는 폰에서 코딩 에이전트를 시키고 결과를 확인하는 원격 조작 용도를 앞세웠는데, iPad 버전은 같은 앱을 큰 화면에 맞게 다시 짜면서 리뷰 쪽 기능을 특히 키웠어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 리뷰 화면이 PR 전체를 커버해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체인지로그에는 리뷰 화면이 이제 PR 전체를 커버한다고 나와 있어요. 코멘트, CI 체크, 승인까지 한 화면에서 보고, 리뷰어를 추가하거나 남은 이슈를 에이전트한테 넘겨 해결시킬 수 있다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인박스도 새로 생겼어요. 지금 진행 중인 작업, 내 확인이 필요한 것, 리뷰 중인 PR을 한 곳에 모아 보여주는 구조예요. 인박스와 풀 PR 리뷰는 iPad만이 아니라 iPhone 쪽에도 같이 들어갔어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 화면을 쪼개 리뷰와 채팅을 같이 봐요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iPad 레이아웃에서는 사이드바 채팅이 고정돼서 에이전트 여러 개가 도는 걸 동시에 지켜볼 수 있어요. 스플릿 스크린으로 한쪽에 리뷰를 열어두고 다른 쪽에서 채팅을 이어가는 배치도 되고, 파일 diff는 잘리지 않고 전체가 렌더링된다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈에 띄는 건 마크업이에요. 스크린샷을 붙인 다음 특정 지점을 탭해 코멘트를 달거나, Apple Pencil로 이미지 위에 직접 그려서 에이전트한테 넘길 수 있어요. UI 수정 지시를 말 대신 그림으로 하는 셈이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. Bitbucket과 Azure DevOps도 붙었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCM 지원이 GitHub 밖으로 넓어졌어요. Bitbucket과 Azure DevOps가 추가돼서, 회사가 GitHub를 안 쓰는 경우에도 모바일 리뷰 흐름을 태우게 됐어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 하나가 PR을 여러 개 만들었을 때 마지막 것만 열리던 제약도 풀려서, 이제 전부 열어볼 수 있다고 해요. 소속된 팀이 여러 개면 앱 안에서 팀 전환도 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 중에 PR 리뷰를 밀어두는 용도로는 발표만 보면 구성이 꽤 갖춰졌어요. 다만 태블릿에서 리뷰가 실제로 얼마나 굴러가는지는 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://cursor.com/changelog/ipad&quot;&gt;Cursor Changelog &amp;mdash; Cursor, now on iPad&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Cursor로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI코딩</category>
      <category>cursor</category>
      <category>IPAD</category>
      <category>PR리뷰</category>
      <category>기능출시</category>
      <category>모바일개발</category>
      <category>코드리뷰</category>
      <category>코딩에이전트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/584</guid>
      <comments>https://jessyt.tistory.com/584#entry584comment</comments>
      <pubDate>Fri, 31 Jul 2026 07:00:45 +0900</pubDate>
    </item>
    <item>
      <title>[Starship] 셸이 뭐든 프롬프트를 통일하는 도구</title>
      <link>https://jessyt.tistory.com/585</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/ccfsHZ/dJMcajwo0ev/AAAAAAAAAAAAAAAAAAAAAA2LIBZAYCzYRlfqhVKuK1WFi7Hgq6VNgCJMGt1fb-YS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=8gK0sKjvMQZRLTbMCecep88DzTc%3D&quot; alt=&quot;[Starship] 셸이 뭐든 프롬프트를 통일하는 도구&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;div class=&quot;yt-hero__tag&quot;&gt;개발자 도구 &amp;middot; 터미널 &amp;amp; 환경&lt;/div&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;Starship, 셸이 뭐든 프롬프트를 하나로 통일하는 도구&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;Bash에서 쓰던 프롬프트 설정을 Zsh로 옮기면 그대로 깨집니다. PowerShell에서는 또 다른 문법을 배워야 합니다. Starship은 그 셸별 프롬프트 설정을 TOML 파일 하나로 통일하는 Rust 기반 도구입니다. 무엇을 대체하는지, 설치와 초기화, 설정 파일 구조, 자주 쓰는 모듈, 그리고 미리 알아둘 함정까지 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸마다 다시 짜던 프롬프트 설정을 하나로 모읍니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 프롬프트(줄 앞에 뜨는 &lt;code&gt;user@host ~/project $&lt;/code&gt; 같은 부분)를 꾸미는 전통적인 방법은 셸마다 문법이 다릅니다. Bash는 &lt;code&gt;PS1&lt;/code&gt; 환경변수에 이스케이프 코드를 넣고, Zsh는 별도의 프롬프트 확장 문법을 쓰며, PowerShell은 &lt;code&gt;prompt&lt;/code&gt; 함수를 정의합니다. 그래서 여러 셸을 오가는 사람은 같은 모양을 만들려고 설정을 세 번 짜게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starship은 이 부분을 셸 바깥으로 빼냅니다. 프롬프트를 그리는 로직을 Starship 실행 파일이 담당합니다. 각 셸은 &quot;프롬프트 그릴 때 Starship을 호출해라&quot;라는 초기화 한 줄만 갖습니다. 실제 모양은 셸과 무관하게 &lt;code&gt;~/.config/starship.toml&lt;/code&gt; 파일 하나가 결정합니다. Bash, Zsh, Fish, PowerShell, Nushell, Elvish 등 문서가 안내하는 셸이 모두 같은 설정 파일을 공유합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;전통적 프롬프트 설정&lt;/th&gt;
&lt;th&gt;Starship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설정 위치&lt;/td&gt;
&lt;td&gt;셸마다 다른 파일&amp;middot;문법&lt;/td&gt;
&lt;td&gt;공용 &lt;code&gt;starship.toml&lt;/code&gt; 한 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;셸 이식성&lt;/td&gt;
&lt;td&gt;셸 바꾸면 다시 작성&lt;/td&gt;
&lt;td&gt;초기화 한 줄만 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git&amp;middot;언어 정보&lt;/td&gt;
&lt;td&gt;직접 스크립트 작성&lt;/td&gt;
&lt;td&gt;모듈로 기본 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구현&lt;/td&gt;
&lt;td&gt;셸 스크립트&lt;/td&gt;
&lt;td&gt;Rust 단일 실행 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치하고 셸에 붙이는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비물이 하나 있습니다. Starship의 기본 모양은 아이콘 글리프를 쓰기 때문에, 터미널에 &lt;b&gt;Nerd Font 계열 폰트를 설치하고 지정&lt;/b&gt;해야 아이콘이 깨지지 않고 보입니다. 아이콘 없이 쓰고 싶다면 뒤에서 설명할 &quot;No Nerd Fonts&quot; 프리셋으로 텍스트만 쓰도록 바꾸면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 설치 스크립트나 패키지 매니저로 합니다. macOS&amp;middot;Linux에서는 Homebrew, Windows에서는 Winget으로도 받습니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# 설치 스크립트 (macOS / Linux)
curl -sS https://starship.rs/install.sh | sh

# 또는 Homebrew
brew install starship

# Windows (Winget)
winget install starship&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치했다고 바로 적용되지는 않습니다. 셸 설정 파일 맨 끝에 초기화 명령을 추가해야 합니다. 셸마다 명령이 조금씩 다릅니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# Bash &amp;mdash; ~/.bashrc 끝에
eval &quot;$(starship init bash)&quot;

# Zsh &amp;mdash; ~/.zshrc 끝에
eval &quot;$(starship init zsh)&quot;

# Fish &amp;mdash; ~/.config/fish/config.fish 끝에
starship init fish | source&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가한 뒤 셸을 새로 열면 프롬프트가 Starship 모양으로 바뀝니다. 여기까지는 설정 파일 없이도 동작하며 이 상태가 기본값입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정은 TOML 파일 하나에서 끝납니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 모양은 &lt;code&gt;~/.config/starship.toml&lt;/code&gt;에서 조정합니다. 이 파일이 없으면 기본 설정으로 동작하고 만들어 두면 그 내용이 우선합니다. 다른 경로에 두고 싶으면 &lt;code&gt;STARSHIP_CONFIG&lt;/code&gt; 환경변수로 위치를 바꿉니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 최상단에는 프롬프트 전체에 걸리는 옵션을 둡니다. 대표적으로 &lt;code&gt;add_newline&lt;/code&gt;은 프롬프트 사이에 빈 줄을 넣을지 정하고(기본값 &lt;code&gt;true&lt;/code&gt;), &lt;code&gt;format&lt;/code&gt;은 어떤 모듈이 어떤 순서로 나올지 정의합니다. 오른쪽 정렬 영역이 필요하면 &lt;code&gt;right_format&lt;/code&gt;을 씁니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# ~/.config/starship.toml

# 에디터 자동완성용 스키마 (선택)
&quot;$schema&quot; = 'https://starship.rs/config-schema.json'

# 프롬프트 사이에 빈 줄 넣기
add_newline = true

# 프롬프트 기호를 바꾸기
[character]
success_symbol = '[➜](bold green)'
error_symbol = '[✗](bold red)'

# 특정 모듈 끄기
[package]
disabled = true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈이 프롬프트의 구성 단위입니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starship에서 프롬프트에 뜨는 정보 조각 하나하나를 &lt;b&gt;모듈&lt;/b&gt;이라고 부릅니다. 각 모듈은 &lt;code&gt;[모듈이름]&lt;/code&gt; 헤더로 개별 설정하며 필요 없으면 &lt;code&gt;disabled = true&lt;/code&gt;로 끕니다. 모듈은 상황에 맞춰 나타나는 것이 특징입니다. 예를 들어 언어 런타임 모듈은 해당 프로젝트 안에 있을 때만 버전을 보여줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 모듈&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;character&lt;/code&gt; &amp;mdash; 프롬프트 기호. 직전 명령이 성공했는지 실패했는지에 따라 색이 바뀝니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;directory&lt;/code&gt; &amp;mdash; 현재 디렉터리 경로를 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git_branch&lt;/code&gt; / &lt;code&gt;git_status&lt;/code&gt; &amp;mdash; 활성 Git 브랜치와 저장소 상태(변경&amp;middot;스테이징 등)를 기호로 보여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodejs&lt;/code&gt; &amp;middot; &lt;code&gt;python&lt;/code&gt; &amp;middot; &lt;code&gt;rust&lt;/code&gt; &amp;mdash; 각 언어 프로젝트 안에서만 런타임 버전을 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws&lt;/code&gt; &amp;mdash; 활성 AWS 프로필과 리전을 보여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프리셋으로 처음부터 안 만들어도 됩니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 처음부터 손으로 짜지 않아도 됩니다. Starship 문서에는 커뮤니티가 만든 프리셋이 12종 올라와 있습니다. 대표적으로 아이콘을 텍스트로 바꾸는 &quot;No Nerd Fonts&quot;, 세그먼트를 대괄호로 감싸는 &quot;Bracketed Segments&quot;, 언어 런타임 버전을 숨겨 컨테이너 환경에 맞춘 &quot;No Runtime Versions&quot;, 그리고 &quot;Pure Prompt&quot;&amp;middot;&quot;Tokyo Night&quot;&amp;middot;&quot;Gruvbox Rainbow&quot;&amp;middot;&quot;Catppuccin Powerline&quot; 같은 테마형 프리셋이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리셋은 명령 한 번으로 설정 파일에 내보냅니다. 아래처럼 하면 선택한 프리셋 내용이 &lt;code&gt;starship.toml&lt;/code&gt;로 저장되고 이후 그 파일을 취향대로 고쳐 쓰면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;# 프리셋을 설정 파일로 내보내기
starship preset nerd-font-symbols -o ~/.config/starship.toml

# 적용 가능한 프리셋 목록 보기
starship preset --list&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 함정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Nerd Font를 안 깔면 아이콘이 깨집니다.&lt;/b&gt; 네모나 물음표로 보이면 폰트 문제입니다. 폰트를 설치&amp;middot;지정하거나, 아이콘을 안 쓰는 프리셋으로 바꾸면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;느린 명령이 프롬프트를 늦출 수 있습니다.&lt;/b&gt; 모듈이 버전 등을 확인하려고 외부 명령을 부르는데, 이게 느리면 프롬프트가 뜨는 데 지연이 생깁니다. &lt;code&gt;command_timeout&lt;/code&gt;(명령 실행 제한 시간)과 &lt;code&gt;scan_timeout&lt;/code&gt;(파일 스캔 제한 시간)을 조정해 상한을 둘 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초기화 줄의 위치가 중요합니다.&lt;/b&gt; &lt;code&gt;starship init&lt;/code&gt; 줄은 셸 설정 파일의 끝부분에 두는 것이 안전합니다. 다른 프롬프트 도구나 &lt;code&gt;PS1&lt;/code&gt;을 건드리는 설정과 순서가 엉키면 Starship 설정이 덮일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;셸 재시작이 필요합니다.&lt;/b&gt; 초기화 줄을 추가하거나 &lt;code&gt;starship.toml&lt;/code&gt;을 고친 뒤에는 셸을 새로 열어야 반영됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;누구에게 맞나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 셸이나 여러 머신을 오가는 사람에게 이점이 큽니다. &lt;code&gt;starship.toml&lt;/code&gt; 하나만 dotfiles로 관리하면 Bash를 쓰는 서버든 Zsh를 쓰는 로컬이든 같은 프롬프트가 나오기 때문입니다. Git 브랜치&amp;middot;상태와 언어 버전을 프롬프트에서 바로 보고 싶은데 셸 스크립트를 직접 짜기는 번거로운 경우에도 맞습니다. 반대로 이미 &lt;code&gt;PS1&lt;/code&gt; 한 줄에 만족하고 아이콘 폰트를 새로 깔기 싫다면 굳이 바꿀 이유는 크지 않습니다. 시작은 설치 후 초기화 한 줄만 추가해 기본값을 써 보고, 마음에 드는 프리셋을 내보낸 뒤 모듈을 하나씩 켜고 끄며 다듬는 순서를 권합니다.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;설치&amp;middot;설정 메모&lt;/span&gt; 설치 후 반드시 셸 설정 파일에 &lt;code&gt;starship init&lt;/code&gt; 줄을 추가해야 적용됩니다. 아이콘이 깨져 보이면 Nerd Font 설치&amp;middot;지정 여부부터 확인하고 아이콘을 안 쓰려면 &quot;No Nerd Fonts&quot; 프리셋으로 내보내면 됩니다. 설정 파일 경로는 기본 &lt;code&gt;~/.config/starship.toml&lt;/code&gt;이며, 필요하면 &lt;code&gt;STARSHIP_CONFIG&lt;/code&gt; 환경변수로 다른 위치를 가리킵니다.&lt;/div&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;div class=&quot;yt-sources__head&quot;&gt;출처&lt;/div&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://starship.rs/&quot;&gt;Starship 공식 사이트 &amp;mdash; 소개&amp;middot;설치&amp;middot;지원 셸&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://starship.rs/config/&quot;&gt;Starship 문서 &amp;mdash; 설정 파일 구조&amp;middot;모듈&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://starship.rs/presets/&quot;&gt;Starship 문서 &amp;mdash; 프리셋 목록&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/starship/starship&quot;&gt;GitHub &amp;mdash; starship/starship&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;직접 사용 후기가 아니라 공식 문서를 바탕으로 정리한 객관 소개 글입니다. 버전&amp;middot;기능&amp;middot;명령은 이후 릴리즈에서 달라질 수 있으니, 실제 설정 전 공식 문서를 확인하세요.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/터미널 &amp;amp; 환경</category>
      <category>starship</category>
      <category>개발환경</category>
      <category>셸</category>
      <category>터미널</category>
      <category>프롬프트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/585</guid>
      <comments>https://jessyt.tistory.com/585#entry585comment</comments>
      <pubDate>Fri, 31 Jul 2026 06:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional이 안 먹는 이유: 프록시 자기호출, private, 내부 호출 해결</title>
      <link>https://jessyt.tistory.com/499</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TYw2f/dJMcabRWY3F/6clKfhVVZcdzklz63qO9s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TYw2f/dJMcabRWY3F/6clKfhVVZcdzklz63qO9s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TYw2f/dJMcabRWY3F/6clKfhVVZcdzklz63qO9s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTYw2f%2FdJMcabRWY3F%2F6clKfhVVZcdzklz63qO9s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] @Transactional이 안 먹는 이유: 프록시 자기호출, private, 내부 호출 해결&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] @Transactional이 안 먹는 이유: 프록시 자기호출, private, 내부 호출 해결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;@Transactional을 분명히 붙였는데 트랜잭션이 없어요.&quot; 스프링에서 가장 많이 반복되는 질문이고, 답은 거의 항상 하나예요 &amp;mdash; &lt;b&gt;프록시를 안 거쳤기 때문&lt;/b&gt;. &lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;앞 편&lt;/a&gt;에서 본 대로 @Transactional의 실체는 프록시인데, 프록시가 못 끼어드는 호출 경로들이 있거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 대표 격인 자기호출(self-invocation)의 구조와 세 가지 해법을 보고, private&amp;middot;final&amp;middot;new 객체&amp;middot;다른 스레드까지 프록시가 무력화되는 경우의 전체 지도를 그려볼게요. @Cacheable&amp;middot;@Async&amp;middot;@Retryable 등 프록시 기반 애너테이션 전부에 똑같이 적용되는 이야기예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 자기호출, 프록시가 낄 자리가 없다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biWIjI/dJMcagyYVSj/Ba4rYYkvXexcNILEvokKYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biWIjI/dJMcagyYVSj/Ba4rYYkvXexcNILEvokKYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biWIjI/dJMcagyYVSj/Ba4rYYkvXexcNILEvokKYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiWIjI%2FdJMcagyYVSj%2FBa4rYYkvXexcNILEvokKYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring 프록시 자기호출 함정. 외부 빈에서 호출하면 프록시가 트랜잭션을 시작하지만, 같은 클래스 안에서 this로 호출하면 프록시를 거치지 않아 Transactional이 있어도 그냥 메서드 호출이 된다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class OrderService {

    public void process(List&amp;lt;Order&amp;gt; orders) {
        for (Order o : orders) {
            this.saveOrder(o);      // ❌ 자기호출 &amp;mdash; 트랜잭션 안 걸림!
        }
    }

    @Transactional
    public void saveOrder(Order o) { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 &lt;code&gt;orderService.saveOrder()&lt;/code&gt;를 부르면 프록시가 가로채 트랜잭션을 열어요. 그런데 &lt;code&gt;process()&lt;/code&gt; 안에서 &lt;code&gt;this.saveOrder()&lt;/code&gt;를 부르면, 그건 &lt;b&gt;실제 객체가 자기 자신의 메서드를 직접 부르는 것&lt;/b&gt;이에요. 프록시는 바깥에 있으니 낄 자리가 없죠. 애너테이션은 장식이 되고, 트랜잭션 없이 실행돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고약한 건 에러가 안 난다는 거예요. 그냥 조용히 트랜잭션 없이 돌아요. 예외가 나도 롤백이 안 되고, 부분 저장이 일어나고 나서야 &quot;어? 트랜잭션이?&quot;가 되는 거죠. &lt;a href=&quot;https://jessyt.tistory.com/466&quot;&gt;@Cacheable의 자기호출&lt;/a&gt;에서 캐시가 조용히 무시되던 것과 완전히 같은 구조예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 해법 셋, 구조 분리가 정석&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WPSse/dJMcabRWY3o/wT0q8CKF3tDeT14XEJm9U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WPSse/dJMcabRWY3o/wT0q8CKF3tDeT14XEJm9U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WPSse/dJMcabRWY3o/wT0q8CKF3tDeT14XEJm9U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWPSse%2FdJMcabRWY3o%2FwT0q8CKF3tDeT14XEJm9U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;자기호출 해법 3가지. 트랜잭션 단위를 별도 빈으로 분리하는 게 권장이고, 자신의 프록시를 주입받아 호출하는 자기 주입은 임시방편이며, TransactionTemplate은 코드로 경계를 명시한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 빈 분리 (권장)&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderTxService txService;

    public void process(List&amp;lt;Order&amp;gt; orders) {
        for (Order o : orders) {
            txService.saveOrder(o);   // ✅ 다른 빈 = 프록시 경유
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 단위를 별도 빈으로 추출하면 호출이 자연스럽게 프록시를 거쳐요. 그리고 보통 이 분리가 설계에도 이로워요 &amp;mdash; &quot;여기부터 트랜잭션&quot;이라는 경계가 클래스 구조에 드러나니까요. 자기호출 문제의 90%는 이걸로 푸는 게 맞아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 자기 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 프록시를 주입받아 &lt;code&gt;self.saveOrder()&lt;/code&gt;로 부르는 방법이에요. 동작은 하는데, &quot;자기를 주입받는&quot; 어색함과 순환 참조 이슈가 있어서 임시방편 이상으로는 권하지 않아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ TransactionTemplate&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;private final TransactionTemplate txTemplate;

public void process(Order o) {
    txTemplate.executeWithoutResult(status -&amp;gt; {
        // 이 블록이 트랜잭션 &amp;mdash; 프록시와 무관
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애너테이션 대신 코드로 경계를 지정해요. 프록시 메커니즘과 무관하니 자기호출 문제가 원천적으로 없고, &quot;트랜잭션이 어디서 시작하고 끝나는지&quot;가 코드에 명시돼요. 루프 안에서 건별 트랜잭션을 돌리는 배치성 로직에 특히 잘 맞아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 프록시가 못 끼는 경우 전체 지도&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0dgn7/dJMcacXFajZ/fkypO1rlBCXWHNenV1QFZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0dgn7/dJMcacXFajZ/fkypO1rlBCXWHNenV1QFZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0dgn7/dJMcacXFajZ/fkypO1rlBCXWHNenV1QFZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0dgn7%2FdJMcacXFajZ%2FfkypO1rlBCXWHNenV1QFZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring 프록시가 무력화되는 경우. 자기호출, private 메서드, final 메서드와 클래스, new로 만든 객체, 다른 스레드 실행이 있으며, 의심될 땐 isActualTransactionActive로 확인한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자기호출&lt;/b&gt; &amp;mdash; 위의 그 함정이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;private 메서드&lt;/b&gt; &amp;mdash; 프록시는 상속(CGLIB)이나 인터페이스 위임으로 만드는데, private은 오버라이드도 위임도 안 돼요. 애너테이션을 붙여도 무시돼요(친절하게도 경고조차 안 떠요).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;final 메서드&amp;middot;클래스&lt;/b&gt; &amp;mdash; CGLIB이 상속으로 가로채야 하는데 final이면 오버라이드 불가예요. 단, final 메서드는 조용히 미적용이지만 final 클래스는 (인터페이스 없이 클래스 프록시 대상이면) 기동 시점에 프록시 생성 에러로 터져요. 조용히 넘어가는 쪽이 더 위험해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;new로 만든 객체&lt;/b&gt; &amp;mdash; 스프링이 만든 빈이 아니면 프록시 자체가 없어요. 직접 &lt;code&gt;new Service()&lt;/code&gt; 한 객체의 애너테이션은 전부 장식이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 스레드&lt;/b&gt; &amp;mdash; 트랜잭션은 ThreadLocal에 묶여 있어요. 트랜잭션 안에서 새 스레드(또는 &lt;code&gt;@Async&lt;/code&gt;)로 넘긴 작업은 그 트랜잭션 밖이에요. 이건 다음다음 편(@Async)에서 깊게 다뤄요.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;의심스러울 땐 추측하지 말고 확인해요 &amp;mdash; &lt;code&gt;TransactionSynchronizationManager.isActualTransactionActive()&lt;/code&gt;를 로그로 찍으면 &quot;지금 이 코드가 트랜잭션 안인가&quot;가 바로 나와요. 트랜잭션 미적용 버그는 조용해서, 이 한 줄 확인이 몇 시간을 아껴줘요. 테스트에서도 같은 방식으로 &quot;이 메서드는 트랜잭션 안에서 돈다&quot;를 단언할 수 있어요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Transactional을 붙였는데 롤백이 안 돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 &amp;mdash; ① 자기호출인가 ② private/final인가 ③ checked 예외인가(&lt;a href=&quot;https://jessyt.tistory.com/498&quot;&gt;앞 편&lt;/a&gt;의 롤백 규칙) ④ 그 코드가 정말 스프링 빈인가. isActualTransactionActive로 확인부터요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Cacheable&amp;middot;@Retryable도 안 먹어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 진단이에요. 프록시 기반 애너테이션은 전부 같은 한계를 공유해요. 하나가 안 먹는 구조면 다른 것도 안 먹어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리팩토링했더니 갑자기 트랜잭션이 사라졌어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드를 같은 클래스 안으로 옮기면서 외부 호출이 자기호출로 바뀐 경우가 단골이에요. 클래스 합치기 리팩토링 후엔 트랜잭션 경계를 꼭 다시 봐요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional이 안 먹는 미스터리의 답은 거의 항상 하나예요 &amp;mdash; &lt;b&gt;&quot;프록시를 안 거쳤다&quot;&lt;/b&gt;. 프록시가 못 끼어드는 경로를 외워두면 다섯 종류의 함정이 한 번에 풀려요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;경로&lt;/b&gt; &amp;mdash; 자기호출, private, final, new 객체, 다른 스레드.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정석 해법&lt;/b&gt; &amp;mdash; 트랜잭션 단위를 별도 빈으로 분리(또는 TransactionTemplate로 명시).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장&lt;/b&gt; &amp;mdash; 같은 한계가 @Cacheable&amp;middot;@Async&amp;middot;@Retryable 등 프록시 기반 애너테이션 전부에 적용돼요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 트랜잭션과 이벤트의 미묘한 타이밍이에요 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/500&quot;&gt;@TransactionalEventListener 편&lt;/a&gt;으로 이어져요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/aop/proxying.html&quot;&gt;Spring Framework &amp;mdash; Proxying Mechanisms&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;Spring &amp;mdash; Method Visibility and @Transactional&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>aop</category>
      <category>self-invocation</category>
      <category>Spring</category>
      <category>Transactional</category>
      <category>백엔드</category>
      <category>자기호출</category>
      <category>프록시</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/499</guid>
      <comments>https://jessyt.tistory.com/499#entry499comment</comments>
      <pubDate>Thu, 30 Jul 2026 07:30:36 +0900</pubDate>
    </item>
    <item>
      <title>[MCP] 스펙 2026-07-28 확정, 세션 ID가 사라졌어요</title>
      <link>https://jessyt.tistory.com/583</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/dhaaA2/dJMcafU5del/AAAAAAAAAAAAAAAAAAAAAAlk36J3whjTXB-8IUX7GPZ4P3hv54HTumvIViQ2chb3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=2PPijcG5s%2BR4S5%2BMT4cG%2B%2FEvdps%3D&quot; alt=&quot;[MCP] 스펙 2026-07-28 확정, 세션 ID가 사라졌어요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[MCP] 스펙 2026-07-28 확정, 세션 ID가 사라졌어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 스펙 새 버전이 7월 28일 확정됐어요. 세션을 붙잡아두던 구조가 빠졌어요. 요청 하나가 필요한 정보를 스스로 다 들고 다니는 방식이에요. 세션 식별자에 기대서 만든 서버라면 손봐야 하는 breaking change예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 세션을 잡아두던 구조가 빠졌어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 발표에는 initialize / initialized 교환과 Mcp-Session-Id 헤더가 사라졌다고 나와 있어요. 대신 프로토콜 버전, 클라이언트 정체, 클라이언트 capability가 요청마다 _meta에 실려요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;빠진 것 : initialize / initialized 교환, Mcp-Session-Id 헤더
들어온 것 : Mcp-Method, Mcp-Name 헤더 (메서드&amp;middot;툴 이름이 실림)
요청마다 _meta에 실리는 것 : 프로토콜 버전, 클라이언트 정체, 클라이언트 capability&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 자립하니까 공유 저장소 없이 로드밸런서 뒤에 서버를 여러 대 세울 수 있어요. 서버리스나 엣지에 올리기 쉬워진다는 게 이번 릴리즈의 논리예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 세션 식별자를 쓰던 서버는 마이그레이션이 필요해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tier 1 SDK 네 개(TypeScript, Python, Go, C#)가 새 스펙에 맞춰 업데이트됐어요. 깨지는 부분은 마이그레이션 노트가 따로 붙었고 Rust SDK는 베타로 지원한다고 해요. 공식 글도 세션 식별자를 쓰던 쪽에는 전환 비용이 있다고 그대로 적었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림을 열어둔 채 서버가 먼저 요청을 던지던 패턴은 MRTR(Multi Round-Trip Requests)로 대체됐어요. 확인을 한 번 받고 이어가는 동작을 stateless 연결에서도 할 수 있게 만든 장치예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;deprecation 항목은 1년 창이 있지만, stateless 전환은 새 스펙을 쓰기로 하는 순간 바로 걸려요. 실제 작업량은 서버 구조마다 다를 테니 직접 코드를 보고 판단할 부분이에요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. Roots&amp;middot;Sampling&amp;middot;Logging은 최소 12개월 뒤에 빠져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 기능이 deprecated로 내려갔어요. 대체 방법은 문서에 같이 적혀 있어요. 제거까지 최소 12개월은 둔다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 HTTP+SSE 전송도 같은 1년 창을 받았어요. 당장 멈추는 건 없지만 내년 이맘때까지는 정리 계획을 잡아둘 필요가 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 인증이 사내 IdP 배포에 그대로 붙어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가 규격이 실제 OAuth 2.0&amp;middot;OIDC 배포에 맞춰 여러 군데 조여졌어요. 클라이언트는 RFC 9207에 따라 인가 응답의 iss 파라미터를 검증해야 하고, 클라이언트 자격증명은 발급한 인가 서버에 묶여요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic Client Registration은 deprecated고, 대신 Client ID Metadata Documents를 쓰라고 나와 있어요. Entra나 Okta 같은 사내 IdP에 붙일 때 우회 코드를 끼워넣던 부분이 줄어든다는 뜻이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내망 안에서 MCP 서버를 굴리는 팀에는 stateless보다 이쪽이 더 직접적인 변화일 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. Claude 쪽 반영 시점은 &quot;곧&quot;이라고만 나와 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic은 같은 날 Claude 제품 전반에 새 스펙 지원을 순차 적용한다고 발표했어요. 커넥터 디렉토리에는 서버가 950개 넘게 올라와 있다고 해요. Tasks와 MCP Apps는 코어가 아니라 버전이 매겨진 확장으로 갈라졌고, 스펙 글에는 Enterprise Managed Authorization도 확장으로 정리됐다고 나와 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥터 쪽에 같이 실린 항목은 대화 안에 UI를 그리는 MCP Apps, 조직이 관리하는 인증, 게시한 커넥터용 옵저버빌리티 대시보드, 공개 엔드포인트 없이 사내망에 붙는 MCP tunnels(리서치 프리뷰)예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격 변동은 발표에 없어요. 롤아웃 날짜도 &quot;곧&quot;이라고만 적혀 있어서 실제 적용 시점은 제품별로 확인해야 해요. SDK 월 다운로드가 4억 건을 넘어 올해 4배가 됐다는 수치도 같이 나왔는데, 이건 발표 자료 기준이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://blog.modelcontextprotocol.io/posts/2026-07-28/&quot;&gt;Model Context Protocol &amp;mdash; The 2026-07-28 Specification&lt;/a&gt;, &lt;a href=&quot;https://claude.com/blog/bringing-mcp-2026-07-28-to-claude&quot;&gt;Anthropic &amp;mdash; Bringing MCP 2026-07-28 to Claude&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI개발</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>mcp</category>
      <category>OAuth</category>
      <category>기능출시</category>
      <category>스펙업데이트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/583</guid>
      <comments>https://jessyt.tistory.com/583#entry583comment</comments>
      <pubDate>Thu, 30 Jul 2026 07:00:46 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional 동작 원리: 전파(propagation), 롤백 규칙, REQUIRES_NEW</title>
      <link>https://jessyt.tistory.com/498</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yfa4G/dJMcadCaFFd/ggKdW6Whg1PloAfuGbbUrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yfa4G/dJMcadCaFFd/ggKdW6Whg1PloAfuGbbUrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yfa4G/dJMcadCaFFd/ggKdW6Whg1PloAfuGbbUrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYfa4G%2FdJMcadCaFFd%2FggKdW6Whg1PloAfuGbbUrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[Spring] @Transactional 동작 원리: 전파(propagation), 롤백 규칙, REQUIRES_NEW&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[Spring] @Transactional 동작 원리: 전파(propagation), 롤백 규칙, REQUIRES_NEW&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt;은 한 줄이면 트랜잭션이 되니 편한데, 그만큼 &quot;왜 이러지?&quot; 순간도 많아요. 예외를 잡았는데 롤백되고요. 예외가 났는데 커밋되고요. 내부 호출에선 아예 안 먹고요. 전부 동작 원리 &amp;mdash; 프록시&amp;middot;전파&amp;middot;롤백 규칙 &amp;mdash; 세 가지에서 나오는 일이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional이 실제로 뭘 하는지, 전파 속성(REQUIRED vs REQUIRES_NEW)과 그 유명한 UnexpectedRollbackException, checked 예외가 롤백 안 되는 규칙, readOnly까지 한 번에 짚어요. Spring 함정 시리즈의 기둥이 되는 글이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 실체는 프록시예요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB1v0t/dJMcadWywRX/vLCcrzPokOHQeKsQuHWfxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB1v0t/dJMcadWywRX/vLCcrzPokOHQeKsQuHWfxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB1v0t/dJMcadWywRX/vLCcrzPokOHQeKsQuHWfxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB1v0t%2FdJMcadWywRX%2FvLCcrzPokOHQeKsQuHWfxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring @Transactional 프록시 동작. 호출자는 스프링이 만든 프록시를 거치고, 프록시가 트랜잭션을 시작해 실제 메서드를 호출한 뒤 정상이면 커밋, 예외면 롤백한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;code&gt;@Transactional&lt;/code&gt;이 붙은 빈을 그대로 주입하지 않고, 그 빈을 감싼 &lt;b&gt;프록시&lt;/b&gt;를 주입해요. 호출이 프록시를 거치면서 ① 트랜잭션 시작(커넥션 획득) &amp;rarr; ② 실제 메서드 실행 &amp;rarr; ③ 정상이면 commit, 예외면 rollback이 일어나는 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실에서 함정 몇 개가 바로 따라 나와요. &lt;b&gt;프록시를 안 거치면 아무 일도 안 일어나요.&lt;/b&gt; 같은 클래스 안에서 &lt;code&gt;this.method()&lt;/code&gt;로 부르는 자기호출, private 메서드, final 메서드가 그 경우예요(이건 다음 편에서 정면으로 다뤄요). 오늘은 프록시를 제대로 거쳤을 때의 이야기 &amp;mdash; 전파와 롤백이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 전파, 트랜잭션 안에서 트랜잭션을 부르면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 A의 트랜잭션 메서드가 서비스 B의 트랜잭션 메서드를 불러요. B는 새 트랜잭션을 열까요, A에 합류할까요? 이걸 정하는 게 전파(propagation)예요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctJLuP/dJMcadCaFFb/6K8DFA4K6Jk7cmzxGlP71K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctJLuP/dJMcadCaFFb/6K8DFA4K6Jk7cmzxGlP71K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctJLuP/dJMcadCaFFb/6K8DFA4K6Jk7cmzxGlP71K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctJLuP%2FdJMcadCaFFb%2F6K8DFA4K6Jk7cmzxGlP71K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring 트랜잭션 전파. 기본값 REQUIRED는 기존 트랜잭션에 합류해 내부 예외 시 전체가 rollback-only로 마킹되고, REQUIRES_NEW는 독립 트랜잭션이라 내부 실패와 본 작업의 운명을 분리할 수 있다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;REQUIRED (기본값), 합류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 트랜잭션이 있으면 합류하고, 없으면 새로 만들어요. A&amp;rarr;B가 한 덩어리가 되니 &quot;둘 다 성공 아니면 둘 다 취소&quot;가 자연스럽게 보장돼요. 대부분의 경우 이게 맞는 동작이에요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;그런데 여기에 그 유명한 함정이 있어요. B에서 RuntimeException이 나면, B의 프록시가 &lt;b&gt;공유 트랜잭션에 rollback-only 마킹&lt;/b&gt;을 해요. A가 그 예외를 try-catch로 잡고 &quot;B는 실패해도 괜찮아, 난 계속할래&quot; 하고 커밋하려 하면 &amp;mdash; 이미 rollback-only라 커밋이 불가능해요. 그래서 &lt;code&gt;UnexpectedRollbackException&lt;/code&gt;이 터져요. &quot;예외를 잡았는데 왜 롤백돼요?&quot;의 정체예요. 같은 트랜잭션에 합류한 이상, 내부의 실패는 잡는다고 없던 일이 안 돼요.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REQUIRES_NEW, 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 트랜잭션을 잠시 멈추고 완전히 새 트랜잭션을 열어요. B의 커밋/롤백이 A와 독립이에요. &quot;본 작업이 실패해도 시도 이력은 남겨야 한다&quot;(이력&amp;middot;감사 로그), &quot;부가 작업 실패가 본 작업을 못 막게 한다&quot;(알림 발송 기록) 같은 운명 분리가 필요할 때 써요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의 두 가지예요. 첫째, &lt;b&gt;커넥션을 2개 점유&lt;/b&gt;해요 &amp;mdash; A 것 하나, B 것 하나. REQUIRES_NEW가 깊이 중첩되면 &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;커넥션 풀&lt;/a&gt;이 빨리 말라요. 둘째, A가 나중에 롤백돼도 B는 이미 커밋돼 있어요. 그게 의도여야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 전파(SUPPORTS, MANDATORY, NESTED 등)는 존재만 알아두면 돼요. 실무의 99%는 REQUIRED와 REQUIRES_NEW예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 롤백 규칙, checked 예외는 커밋돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의외로 많이 모르는 규칙이에요. 스프링의 기본 롤백 대상은 &lt;b&gt;RuntimeException과 Error뿐&lt;/b&gt;이에요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZXbgi/dJMcadCaFFc/Z0aOHTzpf64BHnbsVTmIE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZXbgi/dJMcadCaFFc/Z0aOHTzpf64BHnbsVTmIE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZXbgi/dJMcadCaFFc/Z0aOHTzpf64BHnbsVTmIE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZXbgi%2FdJMcadCaFFc%2FZ0aOHTzpf64BHnbsVTmIE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring 롤백 규칙. RuntimeException과 Error는 기본 롤백되지만 checked 예외는 롤백되지 않고 커밋된다. checked 예외도 롤백하려면 rollbackFor를 명시해야 한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void process() throws IOException {
    orderRepository.save(order);
    fileService.write(order);   // IOException(checked) 발생!
}
// &amp;rarr; 예외는 던져졌지만 save는 커밋된다  &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;checked 예외(IOException, 커스텀 Exception 상속 예외)가 던져져도 &lt;b&gt;커밋돼요.&lt;/b&gt; &quot;예외가 났는데 데이터는 들어가 있어요&quot;라는 기괴한 상황이 여기서 나와요. 역사적으로 &quot;checked 예외 = 호출자가 처리할 수 있는 비즈니스 상황&quot;이라는 EJB 시절 관례에서 온 규칙이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법은 둘 중 하나예요. &lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;로 명시하거나, 팀 컨벤션으로 &lt;b&gt;비즈니스 예외를 전부 RuntimeException 상속으로&lt;/b&gt; 통일하는 거예요. 후자가 애너테이션 누락 위험이 없어 더 견고해요. Spring Framework 6.2부터는 &lt;code&gt;@EnableTransactionManagement(rollbackOn = RollbackOn.ALL_EXCEPTIONS)&lt;/code&gt;로 전역 기본 자체를 바꿀 수도 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. readOnly = true, 단순 표식이 아니에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 메서드에 &lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;를 붙이는 건 문서화 이상의 효과가 있어요. JPA에서는 flush 모드가 바뀌어 &lt;a href=&quot;https://jessyt.tistory.com/481&quot;&gt;더티 체킹&lt;/a&gt;용 스냅샷 비교를 건너뛰니 메모리&amp;middot;CPU가 절약돼요. 레플리카 DB로 읽기를 라우팅하는 구성에서는 이 플래그가 라우팅 기준이 되기도 하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &quot;쓰기를 막아주는 안전장치&quot;로 과신하면 안 돼요 &amp;mdash; DB&amp;middot;드라이버에 따라 쓰기가 실제로 차단되지 않을 수 있어요. 의도 표현 + 최적화 힌트로 이해하는 게 정확해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UnexpectedRollbackException이 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRED로 합류한 내부 트랜잭션의 예외를 바깥에서 잡은 거예요. 운명을 분리하고 싶으면 내부를 REQUIRES_NEW로, 같이 죽어야 하면 잡지 말고 같이 롤백돼요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외가 났는데 데이터가 저장돼 있어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;checked 예외예요. &lt;code&gt;rollbackFor&lt;/code&gt;를 명시하거나 RuntimeException으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REQUIRES_NEW를 썼더니 커넥션 풀이 말라요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩 호출마다 커넥션이 2개씩 점유되는 구조예요. 정말 운명 분리가 필요한 지점에만 좁게 쓰고, 이벤트 기반 분리(다음다음 편의 @TransactionalEventListener)도 대안이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Transactional을 붙였는데 아예 트랜잭션이 없어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시를 안 거친 거예요 &amp;mdash; 자기호출&amp;middot;private&amp;middot;다른 스레드. 다음 편에서 정면으로 다뤄요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional의 본질은 한 문장이에요 &amp;mdash; &lt;b&gt;프록시가 시작과 커밋/롤백을 감싼다.&lt;/b&gt; 여기서 나머지가 다 따라 나와요. 전파 기본값 REQUIRED는 합류라서, 내부 예외를 try-catch로 잡아도 rollback-only에 걸려 UnexpectedRollbackException이 터져요. 운명을 떼고 싶으면 REQUIRES_NEW지만 커넥션을 2개 쓴다는 값을 치르고요. 롤백 대상은 RuntimeException뿐이라 checked 예외는 rollbackFor로 명시하거나 RuntimeException으로 통일해야 하고, 조회는 readOnly로 최적화 힌트를 줘요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시가 무력화되는 자기호출 함정은 &lt;a href=&quot;https://jessyt.tistory.com/499&quot;&gt;바로 다음 글&lt;/a&gt;에서 정면으로 다뤄요. 같은 지반 위의 JPA 트랜잭션 경계 이야기(&lt;a href=&quot;https://jessyt.tistory.com/481&quot;&gt;영속성 컨텍스트&lt;/a&gt;&amp;middot;&lt;a href=&quot;https://jessyt.tistory.com/486&quot;&gt;OSIV&lt;/a&gt;)도 함께 보면 그림이 맞물려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html&quot;&gt;Spring Framework &amp;mdash; Declarative Transaction Management&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;Spring &amp;mdash; @Transactional Settings&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/프레임워크 &amp;amp; 언어</category>
      <category>propagation</category>
      <category>requires_new</category>
      <category>rollbackFor</category>
      <category>Spring</category>
      <category>Transactional</category>
      <category>롤백</category>
      <category>백엔드</category>
      <category>전파</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/498</guid>
      <comments>https://jessyt.tistory.com/498#entry498comment</comments>
      <pubDate>Wed, 29 Jul 2026 07:30:55 +0900</pubDate>
    </item>
    <item>
      <title>[Excalidraw] 다이어그램을 손그림으로 그리는 이유</title>
      <link>https://jessyt.tistory.com/582</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/bsdlii/dJMcacxeVwE/AAAAAAAAAAAAAAAAAAAAAGLIJUjZ__LZs9DoHRj4Gx2CyB4lqMV0OJeq88LrOx4M/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pp1AxBe%2BiMWgqG6B0Vfl6s77B1o%3D&quot; alt=&quot;[Excalidraw] 다이어그램을 손그림으로 그리는 이유&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 협업 &amp;amp; 이슈&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Excalidraw] 다이어그램을 손그림으로 그리는 이유&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;아키텍처 그림 하나 그리려고 draw.io를 열었다가 정렬과 스타일만 만지다 끝나는 경우가 있어요. Excalidraw는 반대로 &quot;일부러 삐뚤빼뚤한&quot; 손그림 스타일을 기본으로 깔아둔 오픈소스 화이트보드예요. 왜 이런 선택이 개발자들에게 통했는지, 처음 여는 법부터 협업&amp;middot;내보내기&amp;middot;통합까지 순서대로 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정식 다이어그램 도구가 아니라, 회의실 화이트보드를 옮긴 도구예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Excalidraw는 MIT 라이선스 오픈소스 프로젝트예요. GitHub 스타가 12만 개를 넘고, 브라우저에서 &lt;a href=&quot;https://excalidraw.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;excalidraw.com&lt;/a&gt;을 열면 설치나 가입 없이 바로 그리기 시작하면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 컨셉은 손그림(hand-drawn) 스타일이에요. 사각형을 그려도 선이 자로 잰 듯 반듯하지 않고 살짝 흔들리게 렌더링돼요. 이게 단순한 미적 취향이 아니라 용도의 차이를 만들어요. 반듯한 다이어그램은 &quot;완성된 문서&quot;처럼 보여서 회의 중에 고치자는 말을 꺼내기가 애매한데, 손그림은 처음부터 초안처럼 보이니까 지우고 다시 그리는 데 부담이 없다는 논리예요. 회의실 화이트보드에 마커로 그리던 경험을 브라우저로 옮긴 쪽에 가까워요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 목적의 도구들과 놓고 보면 자리가 이래요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;성격&lt;/th&gt;
&lt;th&gt;어울리는 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Excalidraw&lt;/td&gt;
&lt;td&gt;손그림 화이트보드, 로컬 우선&lt;/td&gt;
&lt;td&gt;설계 논의 초안, 아키텍처 스케치, 블로그 그림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;draw.io&lt;/td&gt;
&lt;td&gt;정형 다이어그램 에디터&lt;/td&gt;
&lt;td&gt;공식 문서에 들어갈 정돈된 구성도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FigJam&lt;/td&gt;
&lt;td&gt;팀 화이트보드 (Figma 생태계)&lt;/td&gt;
&lt;td&gt;워크숍&amp;middot;브레인스토밍, 디자인 조직 협업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid&lt;/td&gt;
&lt;td&gt;텍스트로 정의하는 다이어그램&lt;/td&gt;
&lt;td&gt;코드 리뷰 대상이 되는 문서 속 도식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 대체 관계라기보다 단계가 달라요. 논의가 굳기 전의 그림은 Excalidraw로, 확정된 그림은 draw.io나 Mermaid로 옮기는 식의 조합이 흔해요. 뒤에서 다루겠지만 Mermaid 코드를 Excalidraw 도형으로 변환하는 공식 경로도 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작은 접속이 전부예요. 저장도 브라우저가 알아서 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 절차라고 할 게 거의 없어요. excalidraw.com을 열면 빈 캔버스가 나오고, 그리는 내용은 브라우저 로컬에 자동 저장돼요. 계정을 만들 필요도, 저장 버튼을 누를 필요도 없어요. 탭을 닫았다 다시 열어도 그리던 그림이 그대로 남아 있어요. PWA를 지원해서 앱처럼 설치해두면 오프라인에서도 동작해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 상단 메뉴에서 파일로 저장하면 .excalidraw 확장자가 붙는데 내용물은 그냥 JSON이에요. 도형&amp;middot;좌표&amp;middot;스타일이 전부 텍스트로 들어 있어서 git 저장소에 커밋하면 버전 관리까지 돼요. 다이어그램 원본을 리포지토리에 함께 두고 싶은 팀에게는 이 지점이 바이너리 포맷 도구와의 실질적인 차이예요.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;저장 방식 주의&lt;/span&gt; 무료 웹앱의 자동 저장은 어디까지나 브라우저 로컬 저장소예요. 브라우저 데이터를 지우거나 다른 기기에서 열면 그림이 없어요. 남겨야 할 그림은 .excalidraw 파일로 내려받아 두는 습관이 안전합니다. 클라우드 저장&amp;middot;워크스페이스는 유료인 Excalidraw+ 영역이에요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능은 단순한데, 화살표와 라이브러리가 실사용을 결정해요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그리기 도구와 단축키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구는 사각형&amp;middot;다이아몬드&amp;middot;원&amp;middot;화살표&amp;middot;선&amp;middot;자유 그리기&amp;middot;텍스트&amp;middot;지우개 정도로 단출해요. 도구마다 숫자 단축키가 배정되어 있어서 키보드로 도구를 바꿔가며 그리는 흐름이 자연스럽고, 캔버스에서 ? 키를 누르면 전체 단축키 목록이 떠요. 다크 모드도 지원해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;화살표 바인딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도식 도구로서 중요한 기능이에요. 화살표를 도형에 연결하면 도형을 옮겨도 화살표가 따라와요. 화살표에는 레이블 텍스트도 붙어요. 서비스 A에서 B로 가는 호출에 &quot;gRPC&quot; 같은 라벨을 다는 전형적인 아키텍처 그림이 그려져요. 이 바인딩이 없으면 도형을 옮길 때마다 선을 다시 그려야 하니, 화이트보드 도구와 그림판의 경계가 여기서 갈려요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셰이프 라이브러리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 도형만으로 부족하면 공개 라이브러리를 불러오면 돼요. 다른 사용자들이 만들어 공유한 도형 묶음(클라우드 서비스 아이콘, UI 요소 등)을 &lt;a href=&quot;https://libraries.excalidraw.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;libraries.excalidraw.com&lt;/a&gt;에서 골라 한 번에 추가하는 방식이에요. 자주 쓰는 도형 조합을 직접 라이브러리로 저장해 재사용할 수도 있어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실시간 협업과 종단간 암호화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Share 버튼으로 협업 링크를 만들면 여러 명이 같은 캔버스에 동시에 그려요. 이때 데이터가 종단간 암호화(end-to-end encryption)로 전송된다는 점을 공식적으로 내세워요. 링크에 포함된 키로만 내용을 푸는 구조라, 서버를 거쳐도 서버가 그림 내용을 읽지 못한다는 설명이에요. 읽기 전용 공유 링크도 따로 만들어져요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내보내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성한 그림은 PNG&amp;middot;SVG로 내보내거나 클립보드로 바로 복사돼요. 블로그나 위키에 붙일 때는 SVG가 유용한데, 손그림 스타일이 벡터 그대로 유지되어 확대해도 깨지지 않아요. PNG로 내보낼 때 Excalidraw 장면 데이터를 파일에 심는 옵션을 켜두면, 그 PNG를 나중에 다시 불러와 수정을 이어가는 것도 가능해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mermaid 코드를 손그림으로 바꾸는 공식 경로가 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 기반 다이어그램을 쓰던 사람에게 유용한 기능이에요. 앱 메뉴의 Mermaid 변환 기능에 Mermaid 문법을 붙여 넣으면 Excalidraw 도형으로 변환돼요. 변환된 결과물은 일반 도형이라 자유롭게 옮기고 고치면 돼요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;flowchart LR
    Client --&amp;gt; Gateway
    Gateway --&amp;gt; AuthService
    Gateway --&amp;gt; OrderService
    OrderService --&amp;gt; DB[(Database)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 Mermaid 코드를 붙여 넣으면 손그림 스타일의 플로차트가 생성되는 식이에요. 구조는 텍스트로 빠르게 잡고, 배치나 강조는 캔버스에서 마우스로 다듬는 조합이 가능해요. 이 변환기는 @excalidraw/mermaid-to-excalidraw라는 별도 패키지로도 공개되어 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에디터 안에서, 노트 앱 안에서, 자기 서비스 안에서 돌아가요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Excalidraw가 오픈소스 생태계에서 자리 잡은 데에는 &quot;다른 도구 안에 들어가는&quot; 형태가 큰 몫을 했어요. 공식 저장소가 소개하는 통합 경로는 크게 세 갈래예요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;VS Code 확장&lt;/b&gt; &amp;mdash; .excalidraw 파일을 에디터 안에서 바로 열고 편집해요. 다이어그램을 코드와 같은 리포지토리에 두는 워크플로우와 맞물려요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Obsidian 플러그인&lt;/b&gt; &amp;mdash; 노트 앱 Obsidian 안에서 그림을 그리고 노트에 임베드하는 커뮤니티 플러그인이 활발해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;npm 패키지&lt;/b&gt; &amp;mdash; @excalidraw/excalidraw 패키지로 React 앱에 캔버스를 통째로 임베드해요. Notion, Replit 같은 서비스들이 Excalidraw를 자사 제품에 통합해 쓰고 있다고 공식 저장소가 밝히고 있어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 서비스에 화이트보드 기능이 필요할 때, 처음부터 만드는 대신 이 패키지를 쓰는 선택지가 생기는 거예요. 기본 임베드는 이 정도 코드로 시작해요.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;npm install react react-dom @excalidraw/excalidraw

// App.jsx
import { Excalidraw } from &quot;@excalidraw/excalidraw&quot;;

export default function App() {
  return (
    &amp;lt;div style={{ height: &quot;600px&quot; }}&amp;gt;
      &amp;lt;Excalidraw /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 제약들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무료 웹앱은 캔버스 관리 기능이 없어요.&lt;/b&gt; 여러 다이어그램을 프로젝트별로 정리해두는 개념이 없고, 파일로 내려받아 직접 관리해야 해요. 여러 장면&amp;middot;클라우드 저장&amp;middot;팀 워크스페이스&amp;middot;댓글&amp;middot;프레젠테이션은 유료 Excalidraw+(사용자당 월 6달러, 14일 체험) 기능이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;손그림 스타일이 안 맞는 문서가 있어요.&lt;/b&gt; 감사 대응 문서나 고객 제출용 자료처럼 격식이 필요한 곳에서는 스타일 자체가 걸림돌이 될 수 있어요. 도형 스타일을 반듯하게 바꾸는 옵션은 있지만 그 용도라면 처음부터 정형 도구 쪽이 자연스러워요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공개 셰이프 라이브러리는 커뮤니티 제작물이에요.&lt;/b&gt; 아이콘 최신성이나 완성도가 라이브러리마다 달라요. 특정 클라우드의 최신 서비스 아이콘이 없는 경우도 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협업 링크는 링크를 아는 사람이 들어오는 구조예요.&lt;/b&gt; 암호화와 별개로, 세밀한 권한 관리가 필요한 조직 협업은 유료 워크스페이스 쪽 영역이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 없이 5분이면 판단이 끝나는 도구예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Excalidraw가 어울리는 사람은 분명한 편이에요. 설계 논의를 시작할 때 그림부터 그리는 개발자, 블로그&amp;middot;발표 자료에 넣을 아키텍처 그림이 자주 필요한 사람, 다이어그램 원본을 git으로 관리하고 싶은 팀. 반대로 조직 차원의 문서 표준이 정형 다이어그램이라면 굳이 갈아탈 이유는 적어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판단 비용이 낮다는 게 이 도구의 미덕이에요. 설치도 가입도 없으니 excalidraw.com을 열어 지금 머릿속에 있는 구조 하나를 그려보면 돼요. 화살표 바인딩과 SVG 내보내기까지 5분 정도 써보면 자기 워크플로우에 들어올 도구인지 아닌지 답이 나와요. 파일 백업 습관 하나만 챙기면 무료 범위로도 개인 용도는 충분해요.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;p class=&quot;yt-sources__head&quot; data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://excalidraw.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Excalidraw 공식 웹앱&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/excalidraw/excalidraw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub &amp;mdash; excalidraw/excalidraw&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://docs.excalidraw.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Excalidraw 개발자 문서&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://plus.excalidraw.com/pricing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Excalidraw+ 요금 안내&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 직접 사용기가 아니라 공식 문서&amp;middot;저장소&amp;middot;요금 페이지를 근거로 한 객관 정리입니다. 기능과 요금은 작성 시점(2026년 7월) 기준이며 이후 달라질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/협업 &amp;amp; 이슈</category>
      <category>excalidraw</category>
      <category>다이어그램</category>
      <category>오픈소스</category>
      <category>협업도구</category>
      <category>화이트보드</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/582</guid>
      <comments>https://jessyt.tistory.com/582#entry582comment</comments>
      <pubDate>Wed, 29 Jul 2026 06:00:40 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응</title>
      <link>https://jessyt.tistory.com/497</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p94Vx/dJMcacQSePL/nOfXTAvDxTnPMCv1gbIYZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p94Vx/dJMcacQSePL/nOfXTAvDxTnPMCv1gbIYZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p94Vx/dJMcacQSePL/nOfXTAvDxTnPMCv1gbIYZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp94Vx%2FdJMcacQSePL%2FnOfXTAvDxTnPMCv1gbIYZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;DB가 느려요&quot;는 사실 네 가지 다른 문제의 같은 증상이에요 &amp;mdash; 쿼리가 느리거나, 락에 막혔거나, 리소스가 포화됐거나, 커넥션이 말랐거나. 어느 쪽인지 가르지 않고 인덱스부터 추가하면 절반은 헛수고예요. 이 글은 그 분기를 잡아주는 MySQL 시리즈의 진단 허브예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증상별로 보는 지표와 대응을 정리하고, 깊은 내용은 시리즈의 해당 글로 연결해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 진단 분기, 증상이 원인을 가리켜요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RXpRG/dJMcacQSePK/gfFrbFJPMsFPFSscWZxQB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RXpRG/dJMcacQSePK/gfFrbFJPMsFPFSscWZxQB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RXpRG/dJMcacQSePK/gfFrbFJPMsFPFSscWZxQB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRXpRG%2FdJMcacQSePK%2FgfFrbFJPMsFPFSscWZxQB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;MySQL 느린 쿼리 진단 분기. 특정 쿼리만 항상 느리면 인덱스 문제, 평소 빠른 쿼리가 가끔 멈추면 락 대기, 전반적으로 느려지면 리소스 포화, DB는 한가한데 앱이 타임아웃이면 커넥션 풀 고갈이다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;특정 쿼리만 항상 느리다&lt;/b&gt; &amp;rarr; 인덱스&amp;middot;쿼리 문제. EXPLAIN의 영역이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;평소 빠른 쿼리가 가끔 멈춘다&lt;/b&gt; &amp;rarr; 락 대기. 누가 잡고 있는 거예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전반적으로 다 느려졌다&lt;/b&gt; &amp;rarr; 리소스 포화(버퍼 풀&amp;middot;디스크 IO) 또는 무거운 쿼리의 연쇄 영향.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB는 한가한데 앱이 타임아웃&lt;/b&gt; &amp;rarr; 커넥션 풀 고갈. DB가 아니라 풀의 문제예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;항상이냐 가끔이냐&quot;, &quot;특정이냐 전체냐&quot; 두 질문만 던져도 사분면이 갈려요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 증상: 특정 쿼리만 항상 느려요&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LeUjK/dJMcafNGueQ/keXDX9WMLL0KZmXt69ayI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LeUjK/dJMcafNGueQ/keXDX9WMLL0KZmXt69ayI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LeUjK/dJMcafNGueQ/keXDX9WMLL0KZmXt69ayI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLeUjK%2FdJMcafNGueQ%2FkeXDX9WMLL0KZmXt69ayI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;느린 쿼리 개선 루프. slow query log로 1초 이상 쿼리를 수집하고 EXPLAIN으로 원인을 분석해 인덱스와 쿼리를 수정한 뒤 EXPLAIN ANALYZE로 실측 검증한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;왕도는 루프예요. &lt;b&gt;수집 &amp;rarr; 분석 &amp;rarr; 수정 &amp;rarr; 검증.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1초 넘는 쿼리를 로그로 수집
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GLOBAL 변경은 새 세션부터 적용돼요. HikariCP처럼 커넥션을 오래 쥐는 환경에선 기존 커넥션이 옛 값을 계속 써서 &quot;설정했는데 안 잡히는&quot; 일이 생겨요(풀 재시작 또는 maxLifetime 회전 후 적용).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slow query log에 잡힌 쿼리를 &lt;a href=&quot;https://jessyt.tistory.com/492&quot;&gt;EXPLAIN&lt;/a&gt;으로 분석해요 &amp;mdash; type이 ALL인지, rows가 큰지, filesort가 뜨는지. 원인이 보이면 &lt;a href=&quot;https://jessyt.tistory.com/491&quot;&gt;인덱스 설계 원칙&lt;/a&gt;(왼쪽 접두&amp;middot;동등 앞 범위 뒤&amp;middot;커버링)이나 쿼리 수정(컬럼 가공 제거&amp;middot;형변환 정리)으로 고치고, EXPLAIN ANALYZE로 실측 검증까지 해요. 감으로 인덱스를 추가하면 &quot;고쳤는데 그대로&quot;가 반복돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM을 쓴다면 &quot;쿼리 자체는 빠른데 개수가 많은&quot; 패턴도 의심해요 &amp;mdash; 그건 MySQL이 아니라 &lt;a href=&quot;https://jessyt.tistory.com/489&quot;&gt;JPA N+1&lt;/a&gt;의 영역이에요. slow log엔 안 잡히는데 화면은 느린 경우가 그래요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 증상: 평소 빠른 쿼리가 가끔 멈춰요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 자체는 문제없는데 간헐적으로 수 초~수십 초 걸린다면, 거의 항상 락 대기예요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqt1Ok/dJMb99Uak3W/36jhuVInbF9STwO65VZoF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqt1Ok/dJMb99Uak3W/36jhuVInbF9STwO65VZoF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqt1Ok/dJMb99Uak3W/36jhuVInbF9STwO65VZoF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqt1Ok%2FdJMb99Uak3W%2F36jhuVInbF9STwO65VZoF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;지금 당장 느릴 때 보는 세 명령. SHOW PROCESSLIST로 도는 쿼리를 보고, innodb_lock_waits로 락 대기 관계를 확인하고, innodb_trx로 오래 열린 트랜잭션을 찾는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW PROCESSLIST;                       -- 지금 뭐가 돌고 있나
SELECT * FROM sys.innodb_lock_waits;    -- 누가 누구를 기다리나
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started;                   -- 오래 열린 트랜잭션&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로커는 대부분 &lt;b&gt;장수 트랜잭션&lt;/b&gt;이에요 &amp;mdash; 커밋 안 하고 열려 있는 배치, 수동 세션, 트랜잭션 안에서 외부 API를 기다리는 코드요. 장애 중이면 블로커 KILL이 가장 빠른 복구일 때가 많고, 재발 방지는 &lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;락 동작 이해&lt;/a&gt;(특히 인덱스 없는 UPDATE의 전체 잠금)와 &lt;a href=&quot;https://jessyt.tistory.com/495&quot;&gt;데드락 예방 수칙&lt;/a&gt;(순서 통일&amp;middot;짧은 트랜잭션)이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 증상: 전반적으로 다 느려졌어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 쿼리가 아니라 전체가 가라앉았다면 리소스를 봐요. 두 가지가 단골이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;버퍼 풀 부족&lt;/b&gt;이에요. InnoDB는 데이터&amp;middot;인덱스를 메모리(버퍼 풀)에 올려두고 일하는데, 데이터가 커져 버퍼 풀 적중률이 떨어지면 디스크 읽기가 늘며 전체가 느려져요. &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;(전용 서버면 메모리의 50~75%)와 적중률 지표를 확인해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;무거운 쿼리 하나의 연쇄 효과&lt;/b&gt;예요. 풀 스캔 쿼리가 버퍼 풀을 다 밀어내거나 디스크 IO를 독점하면, 멀쩡한 쿼리들까지 같이 느려져요. 이때 processlist에 그 무거운 쿼리가 보여요 &amp;mdash; &quot;전체가 느려진 시점에 뭐가 돌고 있었나&quot;가 질문이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 증상: DB는 한가한데 앱이 타임아웃 나요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB CPU도 낮고 slow log도 깨끗한데 앱에서 &quot;connection timeout&quot;이면, DB가 아니라 &lt;b&gt;커넥션 풀&lt;/b&gt;이에요. active/pending 지표를 보고, &quot;오래 점유&quot;의 3대 원인 &amp;mdash; 느린 쿼리&amp;middot;트랜잭션 안 외부 호출&amp;middot;OSIV &amp;mdash; 과 누수를 점검해요. 전부 &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;HikariCP 편&lt;/a&gt;에서 다뤘어요. 풀 사이즈를 늘리는 건 원인 분석 후의 마지막 카드예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 평소에 깔아두면 좋은 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;slow query log 상시 ON&lt;/b&gt; &amp;mdash; long_query_time 1초. 평소에 쌓여야 &quot;언제부터 느려졌나&quot;를 알 수 있어요. SET GLOBAL은 서버 재시작에 휘발되니, 상시로 둘 거면 &lt;code&gt;SET PERSIST&lt;/code&gt;나 my.cnf에 박아둬요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 지표 대시보드&lt;/b&gt; &amp;mdash; QPS, 슬로우 쿼리 수, 버퍼 풀 적중률, 락 대기 수, 커넥션 active/pending. 다섯 개면 위 사분면이 다 커버돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;릴리즈와 지표를 같이 보기&lt;/b&gt; &amp;mdash; &quot;느려진 시점 = 배포 시점&quot;인 경우가 많아요. 새 쿼리가 인덱스를 안 타는 거죠. 배포 후 슬로우 쿼리 수 확인을 루틴으로 만들어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;DB가 느려요&quot;는 &lt;b&gt;특정/전체 &amp;times; 항상/가끔&lt;/b&gt;으로 가르면 네 갈래예요. 특정+항상은 인덱스(EXPLAIN 루프), 특정+가끔은 락(블로커 추적), 전체는 리소스(버퍼 풀&amp;middot;무거운 쿼리), DB 한가+앱 타임아웃은 커넥션 풀이에요. 측정 없이 인덱스부터 만지지 않기 &amp;mdash; 이게 이 시리즈 전체를 한 줄로 줄인 말이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 네 갈래를 따라 &lt;a href=&quot;https://jessyt.tistory.com/490&quot;&gt;인덱스 동작 원리&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/491&quot;&gt;복합 인덱스 설계&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/492&quot;&gt;EXPLAIN&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;격리수준과 MVCC&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;InnoDB 락&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/495&quot;&gt;데드락&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://jessyt.tistory.com/496&quot;&gt;HikariCP&lt;/a&gt;까지 내려왔어요. 같은 이야기를 ORM 위층에서 다시 보고 싶으면 &lt;a href=&quot;https://jessyt.tistory.com/489&quot;&gt;JPA 트러블슈팅&lt;/a&gt;과 한 쌍으로 읽으면 돼요. 다음 시리즈는 프레임워크 차례예요. @Transactional이 안 먹는 이유부터, Spring의 함정들로 넘어가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/slow-query-log.html&quot;&gt;MySQL &amp;mdash; The Slow Query Log&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html&quot;&gt;MySQL &amp;mdash; InnoDB Buffer Pool&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>MYSQL</category>
      <category>processlist</category>
      <category>slow query log</category>
      <category>느린 쿼리</category>
      <category>락 대기</category>
      <category>백엔드</category>
      <category>트러블슈팅</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/497</guid>
      <comments>https://jessyt.tistory.com/497#entry497comment</comments>
      <pubDate>Tue, 28 Jul 2026 07:30:52 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] HikariCP 커넥션 풀 설정: 풀 사이즈 공식, 타임아웃, 커넥션 누수 잡기</title>
      <link>https://jessyt.tistory.com/496</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIqZZI/dJMcacDpuS4/ReJ9fXVWsQgHBiqw7Kygl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIqZZI/dJMcacDpuS4/ReJ9fXVWsQgHBiqw7Kygl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIqZZI/dJMcacDpuS4/ReJ9fXVWsQgHBiqw7Kygl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIqZZI%2FdJMcacDpuS4%2FReJ9fXVWsQgHBiqw7Kygl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] HikariCP 커넥션 풀 설정: 풀 사이즈 공식, 타임아웃, 커넥션 누수 잡기&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[MySQL] HikariCP 커넥션 풀 설정: 풀 사이즈 공식, 타임아웃, 커넥션 누수 잡기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Connection is not available, request timed out after 30000ms&quot; &amp;mdash; 트래픽이 몰리는 날 만나는 그 에러예요. 반사적으로 풀 사이즈를 늘리고 싶지만, 커넥션 풀의 세계에서는 &lt;b&gt;크게가 아니라 작게가 정답&lt;/b&gt;인 경우가 많아요. 반직관적이라 원리를 알아야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 커넥션 풀이 왜 필요한지부터, HikariCP(스프링 부트 기본) 핵심 설정, 풀 사이즈 공식과 그 반직관, 커넥션 누수 진단, 그리고 풀 부족의 진짜 원인 찾기까지 정리해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 풀이 하는 일&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XWBVA/dJMb99Nmvju/4XsOsbN5b2QCGjlAMl6FjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XWBVA/dJMb99Nmvju/4XsOsbN5b2QCGjlAMl6FjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XWBVA/dJMb99Nmvju/4XsOsbN5b2QCGjlAMl6FjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXWBVA%2FdJMb99Nmvju%2F4XsOsbN5b2QCGjlAMl6FjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;커넥션 풀 동작 구조. HikariCP가 커넥션을 미리 만들어두고 요청 스레드에 빌려주며, close는 끊기가 아니라 반납이라 커넥션이 풀로 돌아가 재사용된다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;DB 커넥션 하나를 새로 만드는 건 비싸요 &amp;mdash; TCP 연결, 인증, 세션 초기화까지 수십 ms예요. 매 쿼리마다 이걸 하면 쿼리보다 연결이 더 오래 걸려요. 그래서 풀이 커넥션을 미리 만들어두고 빌려줘요. 코드에서 &lt;code&gt;close()&lt;/code&gt;를 불러도 끊는 게 아니라 &lt;b&gt;풀에 반납&lt;/b&gt;하는 거고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 설정은 몇 개 안 돼요.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      maximum-pool-size: 20        # 최대 커넥션 수 (기본 10)
      connection-timeout: 3000     # 풀에서 못 빌리면 3초 후 에러 (기본 30초)
      max-lifetime: 1800000        # 커넥션 수명 30분 (DB wait_timeout보다 짧게)
      leak-detection-threshold: 30000  # 30초 미반납 시 누수 의심 로그&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;두 가지 기본값은 손보는 게 좋아요. &lt;code&gt;connection-timeout&lt;/code&gt; 기본 30초는 너무 길어요 &amp;mdash; 풀이 마르면 요청들이 30초씩 매달려 있다가 한꺼번에 죽어요. 3~5초로 줄여 빨리 실패하고 빨리 드러나게 해요. &lt;code&gt;max-lifetime&lt;/code&gt;은 MySQL의 &lt;code&gt;wait_timeout&lt;/code&gt;보다 확실히 짧아야 해요. DB가 먼저 끊은 죽은 커넥션을 풀이 빌려주면 &quot;Communications link failure&quot;가 간헐적으로 터져요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 풀 사이즈, 크게가 아니라 작게&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCxW0o/dJMcacDpuS1/SdhewGhvgu50UObXAK58y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCxW0o/dJMcacDpuS1/SdhewGhvgu50UObXAK58y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCxW0o/dJMcacDpuS1/SdhewGhvgu50UObXAK58y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCxW0o%2FdJMcacDpuS1%2FSdhewGhvgu50UObXAK58y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;커넥션 풀 사이징의 반직관. 풀을 100개로 늘리면 DB 코어는 그대로인데 동시 실행만 늘어 쿼리당 시간이 길어지는 악순환이고, DB 코어의 2배 안팎의 작은 풀이 회전율을 높여 같은 처리량을 처리한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;부족하니 늘리자&quot;가 왜 악수가 되냐면, DB가 &lt;b&gt;동시에&lt;/b&gt; 처리할 수 있는 양은 결국 DB 서버의 코어 수에 묶여 있거든요. 코어 16개에 커넥션 100개를 던지면, 100개 쿼리가 16개 코어를 두고 컨텍스트 스위칭하며 경쟁해요. 쿼리당 처리 시간이 길어지고 &amp;rarr; 커넥션 점유가 길어지고 &amp;rarr; 풀이 더 부족해지는 악순환이에요. DB 메모리도 커넥션당 소모되고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 공식 문서의 권장 출발점이 유명해요 &amp;mdash; &lt;b&gt;(DB 코어 수 &amp;times; 2) 안팎.&lt;/b&gt; 원형 공식은 &lt;code&gt;connections = (core_count &amp;times; 2) + effective_spindle_count&lt;/code&gt;인데, SSD에선 스핀들 항이 사실상 무의미해서 코어 &amp;times; 2만 남겨 쓰는 거예요. 16코어 DB면 풀 30 전후가 출발점인데, 이건 &lt;b&gt;DB가 받는 총량&lt;/b&gt;이에요 &amp;mdash; 앱 인스턴스들이 이 총량을 나눠 가져요(인스턴스 5대면 각 6~10이지, 인스턴스당 30이 아니에요). 실제로 작은 풀이 더 높은 처리량을 내는 벤치마크가 HikariCP 위키에 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 풀 부족 신호가 올 때 늘리기 전에 물어야 할 질문 &amp;mdash; &lt;b&gt;&quot;왜 커넥션을 오래 점유하지?&quot;&lt;/b&gt; 진범은 대부분 이 셋이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;느린 쿼리&lt;/b&gt; &amp;mdash; 인덱스 문제로 1초씩 걸리는 쿼리가 풀을 말려요. &lt;a href=&quot;https://jessyt.tistory.com/492&quot;&gt;EXPLAIN&lt;/a&gt;부터.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 안의 외부 호출&lt;/b&gt; &amp;mdash; API 응답 2초를 기다리는 동안 커넥션을 쥐고 있어요. 호출을 트랜잭션 밖으로.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OSIV&lt;/b&gt; &amp;mdash; 응답 끝까지 커넥션을 잡는 그 패턴이에요. &lt;a href=&quot;https://jessyt.tistory.com/486&quot;&gt;OSIV 편&lt;/a&gt;에서 본 대로요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 커넥션 누수, 반납 안 하는 코드 잡기&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGliYo/dJMcaa6ERJH/WkKJ0peUimiR8kzXEhSEyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGliYo/dJMcaa6ERJH/WkKJ0peUimiR8kzXEhSEyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGliYo/dJMcaa6ERJH/WkKJ0peUimiR8kzXEhSEyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGliYo%2FdJMcaa6ERJH%2FWkKJ0peUimiR8kzXEhSEyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;커넥션 누수 진단. 예외 경로의 close 누락이나 미종료 트랜잭션으로 커넥션이 반납되지 않으면 가용 커넥션이 점점 줄다 타임아웃 장애가 난다. leakDetectionThreshold를 켜면 미반납 코드의 스택트레이스가 로그에 남는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;풀 부족의 또 다른 갈래는 누수예요. 어딘가의 코드가 커넥션을 빌리고 안 돌려주는 거예요. 스프링 관리 영역에서는 드물지만, 수동 &lt;code&gt;DataSource&lt;/code&gt; 사용, 예외 경로의 자원 정리 누락, 끝나지 않는 트랜잭션에서 생겨요. 증상이 특징적이에요 &amp;mdash; &lt;b&gt;active 커넥션은 높은데 실제 도는 쿼리는 없고&lt;/b&gt;, 시간이 지날수록 가용 커넥션이 줄어들다 어느 날 장애가 나요. 재시작하면 잠시 멀쩡해지고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잡는 도구가 &lt;code&gt;leak-detection-threshold&lt;/code&gt;예요. 지정 시간(예: 30초) 이상 반납 안 된 커넥션이 있으면, &lt;b&gt;그걸 빌려간 코드의 스택트레이스&lt;/b&gt;를 로그로 찍어줘요. 범인이 로그에 그대로 나오는 거죠. 운영에서 상시 켜두기 부담스러우면 의심될 때만 켜도 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 모니터링할 지표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 Micrometer로 지표를 내보내요. 대시보드에 셋만 띄워도 충분해요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;active&lt;/b&gt; &amp;mdash; 사용 중 커넥션. max에 자주 닿으면 위 &quot;오래 점유&quot; 원인 분석부터예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pending&lt;/b&gt; &amp;mdash; 커넥션을 기다리는 스레드 수. 0이 아니면 이미 부족하다는 뜻이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;usage(점유 시간)&lt;/b&gt; &amp;mdash; 커넥션 한 번 빌림의 평균 시간. 이게 길어지는 추세면 느린 쿼리&amp;middot;트랜잭션 비대를 의심해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;DB는 한가한데 active만 높다&quot;는 OSIV&amp;middot;외부 호출 패턴, &quot;DB도 바쁘고 usage도 길다&quot;는 쿼리 튜닝 &amp;mdash; 지표 조합으로 방향이 갈려요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;connection timeout이 간헐적으로 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽 피크에 풀이 마르는 거예요. pending&amp;middot;usage 지표로 &quot;오래 점유&quot; 원인(느린 쿼리&amp;middot;외부 호출&amp;middot;OSIV)을 먼저 잡고, 그래도 부족하면 그때 사이즈를 조정해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Communications link failure가 가끔 터져요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 끊은 커넥션을 풀이 빌려준 거예요. &lt;code&gt;max-lifetime&lt;/code&gt;을 MySQL &lt;code&gt;wait_timeout&lt;/code&gt;보다 짧게(여유 있게 30초 이상 차이) 잡아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시작하면 괜찮다가 며칠 지나면 또 말라요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전형적인 누수예요. &lt;code&gt;leak-detection-threshold&lt;/code&gt;를 켜고 로그의 스택트레이스로 미반납 코드를 찾아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀의 핵심은 &lt;b&gt;&quot;작은 풀 + 빠른 회전&quot;&lt;/b&gt;이에요. 사이즈는 DB 코어 기준으로 작게 출발하고, 부족 신호가 오면 늘리기 전에 점유 시간(느린 쿼리&amp;middot;트랜잭션 안 외부 호출&amp;middot;OSIV)부터 줄여요. &lt;code&gt;connection-timeout&lt;/code&gt;은 짧게, &lt;code&gt;max-lifetime&lt;/code&gt;은 DB보다 짧게, 누수는 &lt;code&gt;leak-detection-threshold&lt;/code&gt;로 &amp;mdash; 이 네 줄이면 커넥션 장애의 대부분이 예방돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;connection timeout&quot; 알림이 왔을 때 풀 사이즈에 손대기 전에 던질 질문은 하나예요. &quot;이 커넥션들이 지금 무엇을 하느라 오래 점유되고 있나?&quot; 그 답을 못 찾은 채 늘리는 풀은 악순환의 시작이에요. 이제 마지막 편에서, 이런 증상들을 한데 모아 느린 쿼리 트러블슈팅 허브로 시리즈를 닫아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;HikariCP &amp;mdash; About Pool Sizing&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.datasource.connection-pool&quot;&gt;Spring Boot &amp;mdash; DataSource Connection Pool&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>Connection Timeout</category>
      <category>HikariCP</category>
      <category>maximum-pool-size</category>
      <category>MYSQL</category>
      <category>백엔드</category>
      <category>커넥션 누수</category>
      <category>커넥션 풀</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/496</guid>
      <comments>https://jessyt.tistory.com/496#entry496comment</comments>
      <pubDate>Mon, 27 Jul 2026 07:30:13 +0900</pubDate>
    </item>
    <item>
      <title>[Claude API] 대화 중간에 툴 바꿔도 프롬프트 캐시가 안 깨져요</title>
      <link>https://jessyt.tistory.com/580</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/beVvbX/dJMcagNdYLX/AAAAAAAAAAAAAAAAAAAAAGqB5w3YGE2khtwQQ62UNUaKsh2kvcLgz2lvGxBwTwXh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=wSK9ODvlnoOkCcXYCN8SVjUZmRg%3D&quot; alt=&quot;[Claude API] 대화 중간에 툴 바꿔도 프롬프트 캐시가 안 깨져요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude API] 대화 중간에 툴 바꿔도 프롬프트 캐시가 안 깨져요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화 중간에 툴을 바꿔도 프롬프트 캐시가 안 깨져요. Anthropic이 7월 24일 Opus 5와 함께 mid-conversation tool changes 베타를 열었기 때문이에요. 에이전트를 오래 굴리는 쪽이면 비용으로 연결되는 변화로 보여요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 지금까지는 툴 하나만 건드려도 캐시가 통째로 날아갔어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 캐시는 요청 앞부분을 tools, system, messages 순서로 해싱해요. 캐시가 맞으려면 이 앞부분이 최근 요청과 캐시 브레이크포인트까지 바이트 단위로 같아야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 tools 배열이 그 맨 앞에 있다는 점이에요. 툴 하나를 빼거나 설명 한 줄을 고치면 해시가 달라지고 그 뒤 대화 전체가 캐시를 놓쳐요. 공식 문서 설명 기준으로, 턴이 쌓인 세션일수록 다시 읽어야 할 양이 커지는 구조예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. tools는 그대로 두고 system 메시지로 켜고 꺼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 방식은 쓸 툴을 처음에 tools 배열에 전부 선언해둬요. 그다음 대화 중간에 role이 system인 메시지를 넣고, 그 안의 tool_addition과 tool_removal 블록으로 특정 툴을 그 지점부터 열거나 닫아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 안의 tool 필드는 툴을 새로 정의하는 게 아니라 이름으로 참조만 해요. MCP connector 툴은 서버 안의 툴 하나만 지목할 수도 있어요. 서버 전체를 한 덩이로 참조하는 것도 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tools에 넣어두되 처음부터 보여주고 싶지 않은 툴은 defer_loading을 켜면 돼요. 그러면 tool_addition이 꺼낼 때까지 모델 쪽에는 안 보여요.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;client = anthropic.Anthropic()

response = client.beta.messages.create(
    model=&quot;claude-opus-5&quot;,
    max_tokens=1024,
    betas=[&quot;mid-conversation-tool-changes-2026-07-01&quot;],
    # 툴 전체를 여기 선언해두고 이 배열은 이후 손대지 않음
    tools=[
        {
            &quot;name&quot;: &quot;get_weather&quot;,
            &quot;description&quot;: &quot;Get the current weather for a location.&quot;,
            &quot;input_schema&quot;: {
                &quot;type&quot;: &quot;object&quot;,
                &quot;properties&quot;: {&quot;location&quot;: {&quot;type&quot;: &quot;string&quot;}},
                &quot;required&quot;: [&quot;location&quot;],
            },
        },
    ],
    messages=[
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Say OK.&quot;},
        # 여기서부터 get_weather 회수. 앞 턴이 그대로라 캐시 프리픽스가 유지됨
        # (실제 캐시 적립은 cache_control 을 켠 요청에서)
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: [
            {&quot;type&quot;: &quot;tool_removal&quot;,
             &quot;tool&quot;: {&quot;type&quot;: &quot;tool_reference&quot;, &quot;name&quot;: &quot;get_weather&quot;}},
        ]},
    ],
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 베타 헤더가 필요하고 Sonnet 5는 목록에 없어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청에 mid-conversation-tool-changes-2026-07-01 베타 헤더를 넣어야 동작해요. 문서 기준으로 지원 모델은 Fable 5, Mythos 5, Opus 4.8, Opus 5 네 개이고 Claude API와 Amazon Bedrock, Google Cloud에서 쓸 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sonnet 5는 지원 목록에 없어요. 비용 때문에 Sonnet 계열로 에이전트를 돌리는 경우라면 아직 순서가 안 온 기능이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. system 메시지를 아무 자리에나 못 넣어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;system 메시지는 user 턴 바로 뒤, 또는 서버 툴 결과로 끝나는 assistant 턴 바로 뒤에 와야 해요. 그리고 messages 배열의 마지막이거나 바로 뒤에 assistant 턴이 와야 해요. tool_result를 담은 user 턴 뒤도 허용이라서 에이전트 루프에서는 툴 결과 바로 다음이 자리예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assistant의 tool_use 블록과 그에 답하는 tool_result 사이에는 못 넣어요. 그 밖의 위치는 400 에러예요. tools에 선언 안 된 이름을 참조해도 400이 떨어져요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;캐시는 opt-in이에요. 요청에 cache_control이 없으면 애초에 캐시가 안 만들어지니 지킬 것도 없어요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서가 드는 상황은 긴 세션 도중의 정책 변경, 툴 가용성 같은 상태 변화, 에이전트 루프 중간에 들어온 사용자 입력 전달이에요. 전환할 때마다 캐시를 새로 쓰던 비용이 빠지는 구조인데, 실제로 얼마나 줄어드는지는 툴 정의 크기와 대화 길이에 달려서 직접 재보고 판단할 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages&quot;&gt;Claude Docs &amp;mdash; Mid-conversation system messages and tool changes&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/release-notes/api&quot;&gt;Claude Platform release notes (2026-07-24)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI에이전트</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudeapi</category>
      <category>Opus5</category>
      <category>ToolUse</category>
      <category>기능출시</category>
      <category>프롬프트캐시</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/580</guid>
      <comments>https://jessyt.tistory.com/580#entry580comment</comments>
      <pubDate>Mon, 27 Jul 2026 07:00:27 +0900</pubDate>
    </item>
    <item>
      <title>[Neovim] Vim에 Lua와 LSP를 내장한 모달 에디터</title>
      <link>https://jessyt.tistory.com/581</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/mzsnx/dJMcacKOKtL/AAAAAAAAAAAAAAAAAAAAADa_mFAOCr3q-LzKGqGg-Esjvod8z8J59W0Z68z8RATJ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=0xF2Hop9S2fm8mNia%2BYPv%2BUTkGA%3D&quot; alt=&quot;[Neovim] Vim에 Lua와 LSP를 내장한 모달 에디터&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 에디터 &amp;amp; IDE&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Neovim] Vim에 Lua와 LSP를 내장한 모달 에디터&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;Neovim은 Vim에서 갈라져 나온 오픈소스 에디터예요. 편집 방식(모달 편집)은 Vim과 같지만, 설정 언어가 Lua로 바뀌고 LSP 클라이언트와 tree-sitter가 본체에 들어갔습니다. 이 글은 Vim과 뭐가 다른지, 처음 설치해서 init.lua를 어떻게 시작하는지, 미리 알아둘 함정은 뭔지 공식 문서 기준으로 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vim의 편집 방식은 그대로 두고, 확장 방식을 바꾼 프로젝트예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Neovim은 스스로를 &quot;hyperextensible Vim-based text editor&quot;라고 소개합니다. 핵심은 두 단어예요. Vim-based &amp;mdash; hjkl 이동, 노멀/인서트 모드, 텍스트 오브젝트 같은 Vim의 편집 모델과 Vimscript 플러그인이 대부분 그대로 돌아갑니다. hyperextensible &amp;mdash; 확장하는 방법이 다릅니다. Vimscript 대신 Lua로 설정과 플러그인을 쓰며 외부 프로그램이 MessagePack 기반 API로 에디터를 원격 조종하는 구조예요. VS Code 확장이나 브라우저에 Neovim이 embed되는 것도 이 API 덕분입니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Vim&lt;/th&gt;
&lt;th&gt;Neovim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설정 파일&lt;/td&gt;
&lt;td&gt;~/.vimrc&lt;/td&gt;
&lt;td&gt;~/.config/nvim/init.lua (또는 init.vim)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 언어&lt;/td&gt;
&lt;td&gt;Vimscript, Vim9script&lt;/td&gt;
&lt;td&gt;Lua + Vimscript (Vim9script 미지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LSP&lt;/td&gt;
&lt;td&gt;플러그인 필요&lt;/td&gt;
&lt;td&gt;클라이언트 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구문 분석&lt;/td&gt;
&lt;td&gt;정규식 기반 하이라이트&lt;/td&gt;
&lt;td&gt;tree-sitter 파싱 엔진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본값&lt;/td&gt;
&lt;td&gt;보수적 (직접 켜야 함)&lt;/td&gt;
&lt;td&gt;현대적 기본값이 켜져 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;터미널&lt;/td&gt;
&lt;td&gt;:terminal (8.1부터)&lt;/td&gt;
&lt;td&gt;:terminal 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;에디터는 가볍게, 지능은 언어 서버에&quot; 구도로 보면 위치가 명확해집니다. VS Code가 에디터와 확장 마켓을 한 몸으로 묶은 쪽이라면, Neovim은 터미널 안에서 필요한 부품만 조립해 IDE를 만드는 쪽이에요. 대체하는 대상은 두 가지입니다. 서버에서 쓰던 Vim, 그리고 로컬에서 무겁게 느껴지던 GUI 에디터.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 후 첫 명령은 :Tutor입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS는 &lt;code&gt;brew install neovim&lt;/code&gt;, 리눅스는 각 배포판 패키지 매니저, Windows는 winget이나 &lt;a href=&quot;https://github.com/neovim/neovim/releases&quot;&gt;GitHub 릴리즈&lt;/a&gt;로 설치합니다. 현재 안정 버전은 0.12예요. 터미널에서 &lt;code&gt;nvim&lt;/code&gt;을 치면 시작 화면이 뜹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 편집이 처음이면 &lt;code&gt;:Tutor&lt;/code&gt;부터 실행하는 게 정석입니다. 30분 분량의 대화형 튜토리얼이 내장돼 있어서 파일을 실제로 편집하며 이동&amp;middot;삭제&amp;middot;저장을 익히게 돼 있어요. 이 단계를 건너뛰고 플러그인부터 깔면 &quot;글자 하나 못 지우는 에디터&quot;라는 첫인상만 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일 위치는 Vim과 다릅니다. 홈 디렉토리의 .vimrc가 아니라 &lt;b&gt;XDG 표준 경로인 ~/.config/nvim/ 아래에 init.lua를 둡니다&lt;/b&gt;. 정확한 경로는 에디터 안에서 &lt;code&gt;:echo stdpath('config')&lt;/code&gt;로 확인해요. init.lua와 init.vim 중 하나만 쓸 수 있고 둘을 동시에 두면 안 된다는 점은 공식 문서가 명시하는 규칙입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vimrc 첫 20줄이 필요 없어요 &amp;mdash; 기본값이 이미 켜져 있습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim 설정 파일의 앞부분은 관습적으로 &lt;code&gt;syntax on&lt;/code&gt;, &lt;code&gt;set hlsearch&lt;/code&gt; 같은 &quot;켜기&quot; 명령으로 시작하는데, Neovim은 이 목록 상당수가 기본값입니다. 공식 vim_diff 문서에 나오는 것만 추려도 이 정도예요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구문 하이라이트와 파일타입 감지가 자동으로 켜져 있음&lt;/li&gt;
&lt;li&gt;검색 하이라이트(hlsearch)와 증분 검색(incsearch) 기본 활성&lt;/li&gt;
&lt;li&gt;상태줄 항상 표시(laststatus=2), 마우스 기본 활성&lt;/li&gt;
&lt;li&gt;터미널이 지원하면 트루컬러(termguicolors) 자동 활성&lt;/li&gt;
&lt;li&gt;undo 파일이 ~/.local/state/nvim/undo/에 자동 저장 &amp;mdash; 에디터를 껐다 켜도 실행 취소 이력이 남음&lt;/li&gt;
&lt;li&gt;명령어 히스토리 10000개(최대치), ripgrep이 설치돼 있으면 :grep이 ripgrep을 사용&lt;/li&gt;
&lt;li&gt;.editorconfig 파일 자동 적용, :Man으로 man 페이지 열람 내장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Neovim의 빈 설정 파일은 Vim의 빈 vimrc보다 체감이 훨씬 낫습니다. 설정을 한 줄도 안 쓰고 며칠 써보는 시작 방법이 실제로 성립해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;init.lua는 옵션, 키맵, 모듈 세 가지로 시작합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lua 설정의 기본 재료는 세 개예요. 옵션은 &lt;code&gt;vim.opt&lt;/code&gt;, 키 매핑은 &lt;code&gt;vim.keymap.set()&lt;/code&gt;, 파일 분리는 &lt;code&gt;require()&lt;/code&gt;. 공식 Lua 가이드의 문법을 그대로 쓴 최소 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;-- ~/.config/nvim/init.lua
vim.g.mapleader = ' '            -- 리더 키를 스페이스로

vim.opt.number = true            -- 줄 번호
vim.opt.tabstop = 4              -- 탭 너비
vim.opt.shiftwidth = 4           -- 들여쓰기 너비
vim.opt.wildignore = { '*.o', '__pycache__' }  -- 리스트 옵션은 Lua 테이블로

-- 키맵: (모드, 키, 동작) &amp;mdash; 동작에 Lua 함수를 바로 넣을 수 있음
vim.keymap.set('n', '&amp;lt;Leader&amp;gt;w', '&amp;lt;cmd&amp;gt;write&amp;lt;cr&amp;gt;')
vim.keymap.set('n', '&amp;lt;Leader&amp;gt;d', vim.diagnostic.open_float)

-- 설정이 커지면 lua/ 디렉토리로 분리
-- ~/.config/nvim/lua/keymaps.lua 를 만들고:
require('keymaps')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vimscript의 &lt;code&gt;set wildignore=*.o,__pycache__&lt;/code&gt; 같은 콤마 문자열이 Lua 테이블로 바뀐 게 보이실 거예요. 리스트형 옵션을 프로그래밍 언어의 자료구조로 다루게 된 것이 Lua 설정의 실질적인 장점입니다. 조건문, 반복문, 함수를 설정 파일 안에서 일반 언어처럼 쓸 수 있고, lua/ 디렉토리에 파일을 나눠 두면 &lt;code&gt;require()&lt;/code&gt;로 불러오는 모듈 구조가 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IDE 기능은 플러그인이 아니라 본체에 들어 있어요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내장 LSP 클라이언트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정의로 이동, 참조 찾기, 이름 변경, 진단 표시 같은 IDE 기능을 언어 서버 프로토콜로 처리하는 클라이언트가 본체에 있습니다. 다만 클라이언트만 내장이에요. gopls, rust-analyzer, pyright 같은 &lt;b&gt;언어 서버 자체는 언어별로 따로 설치해야 합니다&lt;/b&gt;. 이 지점이 VS Code와의 체감 차이가 가장 큰 부분입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tree-sitter 파싱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식으로 색만 입히던 기존 하이라이트와 달리, tree-sitter는 코드를 실제 구문 트리로 파싱합니다. 하이라이트 정확도가 올라가는 것 외에, &quot;함수 하나를 통째로 선택&quot; 같은 구문 단위 이동&amp;middot;선택의 기반이 돼요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;:terminal과 클라이언트-서버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 분할 창 안에서 셸을 띄우는 :terminal이 내장돼 있습니다. 0.12에서는 &lt;code&gt;:connect&lt;/code&gt;, &lt;code&gt;:detach&lt;/code&gt;, &lt;code&gt;:restart&lt;/code&gt; 같은 명령으로 에디터 프로세스와 화면을 분리해서 붙었다 떼는 클라이언트-서버 구조가 공식 기능으로 소개되고 있습니다. tmux처럼 세션을 유지한 채 다른 곳에서 다시 붙는 사용 방식이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 알아두면 좋은 함정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;학습 곡선은 에디터가 아니라 모달 편집에 있습니다.&lt;/b&gt; Vim 경험이 없다면 첫 1~2주는 생산성이 떨어지는 구간을 지나야 해요. :Tutor를 먼저 끝내는 것이 가장 싼 지름길입니다.&lt;/li&gt;
&lt;li&gt;LSP&amp;middot;자동완성&amp;middot;파일 탐색기를 갖춘 &quot;완성된 IDE&quot;까지는 플러그인 조합과 설정이 필요합니다. 처음부터 직접 쌓기 부담스러우면 kickstart.nvim 같은 커뮤니티 스타터 설정을 읽으면서 시작하는 방식이 널리 쓰여요.&lt;/li&gt;
&lt;li&gt;init.lua와 init.vim은 동시에 쓸 수 없습니다. Vim에서 넘어올 때 .vimrc를 그대로 init.vim으로 옮겨 시작한 뒤 Lua 전환은 그다음에 해도 됩니다.&lt;/li&gt;
&lt;li&gt;Vim 9의 Vim9script는 지원하지 않습니다. Vim9script로 작성된 플러그인은 돌아가지 않아요.&lt;/li&gt;
&lt;li&gt;기본 색상 테마가 Vim과 다릅니다. 예전 배색으로 돌리려면 &lt;code&gt;:colorscheme vim&lt;/code&gt;을 설정에 추가하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;터미널에서 사는 시간이 길수록 맞는 도구입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh로 서버에 붙어 작업하는 일이 잦은 사람, tmux 위에서 개발 환경을 굴리는 사람, 에디터 띄우는 데 몇 초 기다리는 게 싫은 사람에게 맞는 후보입니다. 반대로 GUI와 마우스 중심 워크플로우가 편하고 설정 파일을 만지는 것 자체가 비용으로 느껴진다면, VS Code에 Vim 키바인딩 확장을 얹는 쪽이 현실적인 절충이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 순서는 단순합니다. 설치 &amp;rarr; &lt;code&gt;:Tutor&lt;/code&gt; 완주 &amp;rarr; 빈 설정으로 며칠 사용 &amp;rarr; 불편한 것만 init.lua에 한 줄씩 추가. 남의 설정 수백 줄을 통째로 복사하는 것보다, 필요가 생길 때마다 한 줄씩 늘리는 쪽이 각 줄의 의미를 아는 설정으로 남습니다.&lt;/p&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;Note&lt;/span&gt; 설정 경로가 헷갈리면 에디터 안에서 :echo stdpath('config')를 실행하면 됩니다. macOS와 리눅스 기본값은 ~/.config/nvim이고, 이 폴더를 Git 저장소로 만들어 dotfiles로 관리하는 방식이 일반적이에요.&lt;/div&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;p class=&quot;yt-sources__head&quot; data-ke-size=&quot;size16&quot;&gt;Sources&lt;/p&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://neovim.io/&quot;&gt;Neovim 공식 사이트&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://neovim.io/doc/user/vim_diff.html&quot;&gt;공식 문서 &amp;mdash; Vim과의 차이 (vim_diff)&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://neovim.io/doc/user/lua-guide.html&quot;&gt;공식 문서 &amp;mdash; Lua 가이드&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/neovim/neovim&quot;&gt;GitHub &amp;mdash; neovim/neovim&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 직접 사용기가 아니라 공식 사이트&amp;middot;문서를 근거로 정리한 객관 소개입니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/에디터 &amp;amp; IDE</category>
      <category>Lua</category>
      <category>neovim</category>
      <category>Vim</category>
      <category>에디터</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/581</guid>
      <comments>https://jessyt.tistory.com/581#entry581comment</comments>
      <pubDate>Mon, 27 Jul 2026 06:00:27 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 2.1.219, 중첩 서브에이전트가 3단계로 풀렸어요</title>
      <link>https://jessyt.tistory.com/579</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/cBBKe3/dJMcaf1Iflv/AAAAAAAAAAAAAAAAAAAAAFtcOuBIT0e6u9errzuYaBaGX8ZOsVYepVcBrVJQE_ZK/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=JiENC%2BV0NKWIgWMV9gBehjQ4UMU%3D&quot; alt=&quot;[Claude Code] 2.1.219, 중첩 서브에이전트가 3단계로 풀렸어요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 2.1.219, 중첩 서브에이전트가 3단계로 풀렸어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 2.1.219와 2.1.220이 나왔어요. 이번 릴리즈에서 가장 큰 변화는 서브에이전트 중첩 스폰이에요. 2.1.217에서 기본 차단으로 잠갔던 항목이 두 릴리즈 만에 기본 3단계 허용으로 열렸어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 서브에이전트가 다시 서브에이전트를 부를 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 기준으로 서브에이전트의 중첩 스폰 허용 깊이 기본값이 1에서 3으로 바뀌었어요. 서브에이전트가 만든 서브에이전트가 또 서브에이전트를 만드는 데까지 열린 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.217에서 이 값이 1로 잠기면서 중첩이 아예 막혔던 걸 감안하면 짧은 주기의 방향 전환이에요. 동시 실행 상한 20개는 그대로예요. 무한정 늘어나는 쪽이 아니라 깊이만 열어준 형태로 보여요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩이 부담스러우면 예전 동작으로 되돌리는 환경 변수가 그대로 남아 있어요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;export CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1   # 중첩 스폰 끄기 (2.1.219 기본값은 3)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;headless 쪽도 같이 손봤어요. --forward-subagent-text를 켜면 depth-2 이상에서 생긴 서브에이전트도 stream-json 출력에 나타나요. 각 출력은 자기를 띄운 Agent tool_use id로 묶여요. 중첩을 실제로 쓸 거라면 로그가 먼저 보여야 하니 짝이 맞는 변경이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 워크플로우 크기에 기본 가이드라인이 생겼어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 워크플로우의 기본 크기 가이드라인이 medium으로 잡혔어요. 에이전트 15개 미만을 목표로 하라는 권고값이에요. 다른 크기나 무제한으로 바꾸려면 /config의 Dynamic workflow size에서 고르면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일에서도 지정할 수 있게 workflowSizeGuideline 키가 추가됐어요. 이 키가 설정돼 있으면 /config 화면의 해당 행은 숨겨져요. 실행 중인 워크플로우 상태줄에도 현재 기본 크기가 표시된다고 적혀 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 샌드박스가 허용 목록 밖 호스트를 묻지 않고 바로 막아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sandbox.network.strictAllowlist 설정이 추가됐어요. 샌드박스로 돌아가는 명령이 허용 목록에 없는 호스트로 나가려 할 때 승인 프롬프트 없이 바로 거부하는 옵션이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 뜨는 네트워크 승인 창을 줄이려는 목적보다는, 무인으로 도는 세션에서 나가는 트래픽을 확정적으로 잠그는 쪽에 쓰임새가 있을 것으로 보여요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 작업 폴더가 붙는 시점에 훅이 걸려요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectoryAdded 훅이 생겼어요. /add-dir로 폴더를 추가하거나 SDK의 register_repo_root 요청으로 세션 중간에 새 작업 폴더가 등록될 때 발생해요. 세션 시작 시점에만 걸 수 있던 준비 작업을 폴더가 붙는 시점으로 옮기면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 쪽 진단도 같이 붙었어요. headless stream-json 초기화 이벤트에 mcp_server_errors가 추가돼 설정 검증에서 걸러진 --mcp-config 항목이 목록으로 나오고, 터미널 실행에서는 시작 경고로 떠요. claude mcp list와 /mcp는 서버 연결 실패 시 HTTP 상태와 오류 문구를 같이 보여줘요. MCP 설정값 앞뒤에 눈에 안 보이는 공백이 섞여 있으면 경고도 나와요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 모델 쪽은 Opus 5 정리가 들어갔어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Opus 5가 Claude Code에 들어오면서 기본 Opus 모델이 됐어요. context window는 1M이고, /model 선택창에서 병합된 Opus 행이 그냥 &quot;Opus&quot;로 나오던 표기가 &quot;Opus (1M context)&quot;로 고쳐졌어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fast mode에서는 Opus 4.7이 빠졌어요. /fast는 이제 Opus 5와 Opus 4.8에만 적용돼요. Fable 모델 행에 오래된 캐시 때문에 &quot;Requires usage credits&quot;가 잘못 붙던 문제도 수정됐어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.220은 버그 수정과 안정성 개선만 담긴 릴리즈예요. 업데이트는 claude update 한 번이면 되고, 중첩 3단계 기본값이 실제 작업에 어떻게 작용하는지는 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI코딩</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>개발도구</category>
      <category>기능출시</category>
      <category>서브에이전트</category>
      <category>워크플로우</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/579</guid>
      <comments>https://jessyt.tistory.com/579#entry579comment</comments>
      <pubDate>Sun, 26 Jul 2026 07:00:12 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Opus 5] 공개, 가격 그대로 context window 1M</title>
      <link>https://jessyt.tistory.com/578</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/E0Fmw/dJMcajprhKC/AAAAAAAAAAAAAAAAAAAAAGWeJevrAYWlVyFuNDsGHv0eixHVlsJJaE8ta5Dvua4h/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GbvpmLJxHNtjEA%2BXUtWXyP6ZJv8%3D&quot; alt=&quot;[Claude Opus 5] 공개, 가격 그대로 context window 1M&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Opus 5] 공개, 가격 그대로 context window 1M&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic이 7월 24일 Claude Opus 5를 공개했어요. 값은 Opus 4.8과 같은데 context window는 1M으로 올라갔어요. Max 요금제에서는 기본 모델 자리를 가져갔고 API 쪽에는 thinking 기본값이 바뀌는 변경이 붙어 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 값은 Opus 4.8과 같고 Fable 5의 절반이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic 발표 기준으로 100만 토큰당 입력 $5, 출력 $25예요. Opus 4.8과 동일한 값이에요. 6월에 나온 Fable 5($10 / $50)로 보면 절반이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 ID는 claude-opus-5이고 Claude API와 Amazon Bedrock, Google Cloud, Microsoft Foundry에 같은 날 올라왔어요. Opus 4.8도 네 곳 모두에 그대로 남아 있어서 급하게 갈아탈 필요는 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fast mode는 research preview 단계예요. 값은 입력 $10 / 출력 $50으로 두 배이고 Claude API와 Claude Code(사용량 크레딧)에서만 돼요. Bedrock이나 Google Cloud, Microsoft Foundry 쪽은 아직 빠져 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. Max는 기본 모델이 바뀌고 Pro는 최상위가 Opus 5가 돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5는 Max에서 새 기본 모델이 되고 Pro에서는 고를 수 있는 최상위 모델이 된다고 발표에 나와 있어요. &lt;a href=&quot;https://jessyt.tistory.com/572&quot;&gt;Fable 5가 Pro 요금제 사용량 한도에서 빠지고 별도 크레딧 과금으로 바뀐&lt;/a&gt; 뒤라, Pro 구독자 입장에서는 한도 안에서 쓰는 상단이 Opus 5로 채워지는 셈이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;claude.ai와 Claude Code, Claude Cowork에서 바로 선택할 수 있어요. GitHub Copilot에도 같은 날 추가됐는데 Pro+&amp;middot;Max&amp;middot;Business&amp;middot;Enterprise 등급에서 사용량 과금으로 열린다고 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. context window는 1M이 기본이자 최대예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1M 토큰이 기본값이면서 동시에 최대값이에요. 200k 같은 작은 변형이 따로 없고 최대 출력은 128k 토큰이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지식 기준일은 2026년 5월이에요. Fable 5(2026년 1월)보다 넉 달 뒤라 최근 라이브러리 버전을 물어볼 일이 많다면 차이가 날 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prompt caching 최소 길이도 1,024 토큰에서 512 토큰으로 내려갔어요. Opus 4.8에서 너무 짧아 캐시가 안 잡히던 시스템 프롬프트는 코드 수정 없이 캐시 대상이 돼요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. thinking이 기본으로 켜져 있어서 max_tokens를 다시 봐야 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 4.8은 thinking을 adaptive로 명시해야 켜졌어요. Opus 5는 켜진 상태가 기본이고 깊이는 effort 값(low, medium, high, xhigh, max)으로 조절해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max_tokens가 thinking과 응답 텍스트를 합친 상한이라 4.8에서 thinking 없이 돌리던 요청은 값을 다시 잡으라고 공식 문서에 적혀 있어요. 기존 설정 그대로 두면 응답이 중간에 잘릴 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. thinking을 끄려면 effort를 high 이하로 내려야 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5에서는 thinking을 disabled로 두면서 effort를 xhigh나 max로 잡으면 400 에러가 나요. effort와 무관하게 끌 수 있던 Opus 4.8과 달라진 breaking change라고 릴리스 노트에 명시돼 있어요.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;model = &quot;claude-opus-4-8&quot;   # 이전
model = &quot;claude-opus-5&quot;     # 변경 후

# thinking을 끌 거라면 effort는 high 이하로
output_config = {&quot;effort&quot;: &quot;high&quot;}
thinking = {&quot;type&quot;: &quot;disabled&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 쪽 권고도 하나 있어요. &quot;마지막에 검증 단계를 넣어라&quot; 같은 지시를 예전 모델 기준으로 박아뒀다면 빼라는 안내예요. Opus 5는 시키지 않아도 자기 작업을 검증하는 쪽이라 과하게 반복된다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 적은 성능&amp;middot;동작 서술은 모두 Anthropic 발표와 공식 문서 기준이에요. 실제 체감은 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.anthropic.com/news/claude-opus-5&quot;&gt;Anthropic &amp;mdash; Introducing Claude Opus 5&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/about-claude/models/whats-new-opus-5&quot;&gt;What's new in Claude Opus 5&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI모델</category>
      <category>anthropic</category>
      <category>API</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>LLM</category>
      <category>Opus5</category>
      <category>신모델</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/578</guid>
      <comments>https://jessyt.tistory.com/578#entry578comment</comments>
      <pubDate>Sat, 25 Jul 2026 07:00:07 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략</title>
      <link>https://jessyt.tistory.com/495</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P3tbt/dJMcahkqzGc/MeB3Qif8bsJOXiUaGj1F80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P3tbt/dJMcahkqzGc/MeB3Qif8bsJOXiUaGj1F80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P3tbt/dJMcahkqzGc/MeB3Qif8bsJOXiUaGj1F80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP3tbt%2FdJMcahkqzGc%2FMeB3Qif8bsJOXiUaGj1F80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 알림에 &lt;code&gt;Deadlock found when trying to get lock&lt;/code&gt;이 뜨면 심장이 철렁하죠. 그런데 데드락은 미스터리가 아니에요 &amp;mdash; 원인 패턴이 몇 개 안 되고 MySQL이 친절하게 &quot;누가 누구와 어떻게 엉켰는지&quot; 로그까지 남겨줘요. 읽는 법만 알면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락이 생기는 구조, 실무에서 자주 보는 패턴 둘(역순 잠금&amp;middot;갭 락+INSERT), &lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt; 읽는 법, 예방 체크리스트와 재시도 전략까지 차례로 풀어요. &lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;InnoDB 락&lt;/a&gt; 편의 락 종류를 알고 보면 훨씬 잘 보여요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 데드락의 구조, 원형 대기&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpLxoO/dJMcahkqzFM/snbEEhCYPFC16VwBk27IDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpLxoO/dJMcahkqzFM/snbEEhCYPFC16VwBk27IDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpLxoO/dJMcahkqzFM/snbEEhCYPFC16VwBk27IDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpLxoO%2FdJMcahkqzFM%2FsnbEEhCYPFC16VwBk27IDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;데드락 원형 대기 구조. 트랜잭션 A가 행 1을 잡고 행 2를 기다리는데 B는 행 2를 잡고 행 1을 기다려 서로 영원히 대기한다. InnoDB가 즉시 감지해 한쪽을 강제 롤백하며 1213 에러를 던진다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;A는 행 ①을 잡고 ②를 기다려요. B는 ②를 잡고 ①을 기다려요. 누구도 양보할 수 없는 원형 대기예요. 다행히 InnoDB는 이걸 &lt;b&gt;즉시 감지&lt;/b&gt;해서 한쪽(보통 롤백 비용이 적은 쪽)을 강제 롤백시켜요. 그래서 데드락은 &quot;무한 멈춤&quot;이 아니라 &quot;한쪽의 1213 에러&quot;로 나타나요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실에서 중요한 결론이 나와요 &amp;mdash; 데드락의 기본 대응은 &lt;b&gt;재시도&lt;/b&gt;예요. 희생된 트랜잭션을 다시 실행하면 (상대는 이미 끝났으니) 대부분 성공해요. 에러를 그대로 사용자에게 던지는 게 가장 나쁜 처리예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 단골 패턴 ①, 역순 잠금&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bg876/dJMcahkqzFU/pEYyyyCki6n2AkSgUZdqx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bg876/dJMcahkqzFU/pEYyyyCki6n2AkSgUZdqx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bg876/dJMcahkqzFU/pEYyyyCki6n2AkSgUZdqx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBg876%2FdJMcahkqzFU%2FpEYyyyCki6n2AkSgUZdqx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;실무 데드락 단골 패턴. 역순 잠금은 두 로직이 반대 순서로 자원을 갱신해 생기며 잠금 순서 통일이 해법이고, 갭 락과 INSERT 조합은 같은 범위를 조회한 두 트랜잭션이 서로의 틈 INSERT를 막아 생긴다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;가장 고전적인 패턴이에요. 주문 로직은 &quot;주문 &amp;rarr; 재고&quot; 순으로 UPDATE해요. 재고 정산 배치는 &quot;재고 &amp;rarr; 주문&quot; 순으로 UPDATE하면, 둘이 겹치는 순간 서로의 두 번째 자원을 기다리며 엉켜요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법은 &lt;b&gt;잠금 순서 통일&lt;/b&gt;이에요. &quot;여러 자원을 갱신할 땐 항상 같은 순서로&quot;라는 팀 규칙을 만드는 거예요. 여러 행을 한 번에 잠글 때도 마찬가지예요 &amp;mdash; &lt;code&gt;WHERE id IN (...)&lt;/code&gt;으로 여러 행을 갱신하는 로직이 둘 있다면, 둘 다 ID 오름차순으로 잠그게 정렬해두면 역순 엉킴이 사라져요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 단골 패턴 ②, 갭 락 + INSERT&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 당황스러운 패턴이에요. &lt;b&gt;같은 행을 안 건드렸는데 데드락&lt;/b&gt;이 나요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A와 B가 둘 다 &quot;이 값이 있나&quot; 범위 조회(잠그는 읽기)를 해요. 둘 다 같은 갭에 &lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;갭 락&lt;/a&gt;을 잡아요(갭 락끼리는 공존 가능해요). 그다음 둘 다 그 틈에 INSERT를 시도하면 &amp;mdash; 각자의 INSERT가 상대의 갭 락에 막혀요. 서로가 서로를 기다리는 원형 대기 완성이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;존재 확인 후 INSERT&quot; 패턴(중복 체크 후 가입 처리 등)에서 동시 요청이 몰리면 이게 터져요. 해법은 패턴 자체를 바꾸는 거예요 &amp;mdash; 유니크 제약을 걸고 &lt;code&gt;INSERT&lt;/code&gt; 후 중복 에러를 처리하거나, upsert(&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;)로 한 문장으로 끝내요. &quot;읽고 판단하고 쓰기&quot;를 DB의 원자적 한 방으로 바꾸는, &lt;a href=&quot;https://jessyt.tistory.com/456&quot;&gt;멱등성&lt;/a&gt; 글에서 본 그 사고방식이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. SHOW ENGINE INNODB STATUS 읽는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락이 나면 MySQL이 마지막 데드락의 전말을 보관해요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS\G
-- 출력에서 &quot;LATEST DETECTED DEADLOCK&quot; 섹션을 찾는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 섹션에 이렇게 나와요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;(1) TRANSACTION&lt;/b&gt; &amp;mdash; 첫 번째 트랜잭션이 실행하던 SQL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(1) WAITING FOR THIS LOCK TO BE GRANTED&lt;/b&gt; &amp;mdash; 걔가 기다리던 락 (어느 인덱스의 어느 레코드인지)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(2) TRANSACTION / HOLDS THE LOCK(S) / WAITING FOR&lt;/b&gt; &amp;mdash; 두 번째 트랜잭션이 쥔 락과 기다린 락&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WE ROLL BACK TRANSACTION (N)&lt;/b&gt; &amp;mdash; 누가 희생됐는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽는 요령은 &quot;각자 무슨 SQL을 하다가, 어느 인덱스 레코드를 기다렸나&quot;를 맞춰보는 거예요. &lt;code&gt;lock_mode X locks gap before rec&lt;/code&gt; 같은 문구가 보이면 갭 락 패턴이고, 서로 다른 테이블을 반대로 기다리면 역순 잠금이에요. 두 SQL을 알면 코드에서 그 두 경로를 찾는 건 금방이에요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;운영 팁 &amp;mdash; 이 출력은 &quot;마지막 데드락 1건&quot;만 보관해요. 자주 나는데 패턴이 여러 개면 놓쳐요. &lt;code&gt;innodb_print_all_deadlocks = ON&lt;/code&gt;을 켜면 모든 데드락이 에러 로그에 남아서 빈도와 패턴 분석이 가능해져요. 데드락 알림이 반복되면 제일 먼저 켤 스위치예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 예방 체크리스트와 재시도&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9gVJ/dJMcahkqzF3/aPW0EO4fHB40QUGZFF5iwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9gVJ/dJMcahkqzF3/aPW0EO4fHB40QUGZFF5iwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9gVJ/dJMcahkqzF3/aPW0EO4fHB40QUGZFF5iwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9gVJ%2FdJMcahkqzF3%2FaPW0EO4fHB40QUGZFF5iwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;데드락 대응 체크리스트. 잠금 순서 통일, 트랜잭션 짧게, WHERE 컬럼 인덱스, 잠금 범위 최소화로 빈도를 줄이고, 그래도 발생하는 데드락은 재시도로 무해하게 처리한다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;잠금 순서 통일&lt;/b&gt; &amp;mdash; 역순 잠금 차단. 다중 행은 ID 정렬 후 갱신.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 짧게&lt;/b&gt; &amp;mdash; 락을 쥔 시간이 짧을수록 겹칠 확률이 줄어요. 외부 API 호출을 트랜잭션 안에 두는 게 최악이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WHERE에 인덱스&lt;/b&gt; &amp;mdash; 인덱스 없는 UPDATE의 풀 스캔 잠금은 데드락 확률을 폭증시켜요(&lt;a href=&quot;https://jessyt.tistory.com/494&quot;&gt;앞 편&lt;/a&gt;의 그 함정).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잠금 범위 최소화&lt;/b&gt; &amp;mdash; 갭 락 경합을 줄여요. 유니크 인덱스 동등 조건은 갭 락 없이 레코드 락만 잡아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 받아들일 것 하나 &amp;mdash; &lt;b&gt;데드락은 0이 안 돼요.&lt;/b&gt; 동시성 있는 시스템의 자연 현상이에요. 1213 에러는 짧은 대기 후 트랜잭션 전체를 재실행하는 재시도 로직으로 흡수해요. 스프링이면 &lt;code&gt;@Retryable&lt;/code&gt;이나 AOP로 공통화하되, 주의점은 &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;낙관적 락 재시도&lt;/a&gt;와 같아요 &amp;mdash; 이미 롤백 마킹된 트랜잭션 안에서 잡지 말고 트랜잭션 밖에서 새로 시작해야 해요. 재시도할 로직은 멱등해야 하고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 행을 안 건드렸는데 데드락이 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갭 락 + INSERT 패턴이에요. STATUS 출력에 gap 락이 보이면 확정이에요. &quot;확인 후 INSERT&quot;를 upsert&amp;middot;유니크 제약 방식으로 바꿔요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특정 배치가 돌 때마다 서비스에서 데드락이 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치와 실시간 로직의 잠금 순서가 다른 거예요. 배치의 갱신 순서를 실시간 로직과 통일하거나, 배치를 작은 청크로 쪼개 락 점유를 짧게 해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데드락이 너무 자주 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈도가 높으면 재시도로 덮을 단계가 아니에요. &lt;code&gt;innodb_print_all_deadlocks&lt;/code&gt;로 패턴을 모아 가장 잦은 한두 패턴부터 구조적으로(순서 통일&amp;middot;범위 축소) 제거해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락은 &lt;b&gt;원형 대기이고 InnoDB가 감지해 한쪽을 롤백하는 1213 에러&lt;/b&gt;로 나타나요. 원인은 대부분 역순 잠금 아니면 갭 락+INSERT이고 &lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;가 전말을 알려줘요. 줄이고 흡수하는 손은 네 가지예요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;잠금 순서 통일&lt;/b&gt; &amp;mdash; 다중 행은 ID 정렬 후 갱신해 역순 엉킴을 차단.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 짧게&lt;/b&gt; &amp;mdash; 외부 API 호출을 트랜잭션 안에 두지 않기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WHERE에 인덱스&lt;/b&gt; &amp;mdash; 풀 스캔 잠금이 데드락 확률을 폭증시키니까.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;남는 건 재시도&lt;/b&gt; &amp;mdash; &quot;제로&quot;가 아니라 &quot;드물고 무해하게&quot;가 목표예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 락의 세계였어요. 이제 무대를 옮겨, 애플리케이션과 DB 사이의 관문 &amp;mdash; HikariCP 커넥션 풀로 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks.html&quot;&gt;MySQL &amp;mdash; Deadlocks in InnoDB&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlock-detection.html&quot;&gt;MySQL &amp;mdash; Deadlock Detection&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>deadlock</category>
      <category>INNODB STATUS</category>
      <category>MYSQL</category>
      <category>데드락</category>
      <category>락 순서</category>
      <category>백엔드</category>
      <category>재시도</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/495</guid>
      <comments>https://jessyt.tistory.com/495#entry495comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:30:22 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 2.1.218, 코드 리뷰가 대화창을 안 채워요</title>
      <link>https://jessyt.tistory.com/576</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/BaIWA/dJMcahyuPhd/AAAAAAAAAAAAAAAAAAAAAAaRABe7IAxi0faqaEFFIpivMiCBXlylx5K6Lne6uIgd/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=0po7img0Kxe0uYQpdt2uhH9A8%2B0%3D&quot; alt=&quot;[Claude Code] 2.1.218, 코드 리뷰가 대화창을 안 채워요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 2.1.218, 코드 리뷰가 대화창을 안 채워요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 2.1.218부터 /code-review가 백그라운드 서브에이전트로 빠져서, 리뷰 중간 출력이 본 대화를 더 이상 채우지 않아요. 7월 22일 나온 이번 판의 중심 변경이에요. 스킬 실행 방식도 같이 바뀌어서, context: fork를 쓰는 스킬이라면 설정을 한 번 확인해야 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 리뷰는 뒤에서 돌고, 대화는 그대로 이어져요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 기준으로 /code-review는 이제 백그라운드 서브에이전트로 실행돼요. 리뷰가 뱉는 중간 출력이 본 대화에 쌓이지 않아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬래시 커맨드를 여러 개 쌓아둔 상태라면, 쌓인 커맨드들이 그대로 리뷰 대상으로 유지된다고 해요. 리뷰를 걸어둔 채로 다른 작업을 이어가는 식이 가능해 보이는 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 수정도 같이 들어갔어요. /code-review ultra가 비대화형 세션에서 조용히 로컬 리뷰로 떨어지던 문제가 잡혀서 이제는 클라우드 리뷰가 뜬다고 해요. /ultrareview에 &quot;내 auth 변경 리뷰해줘&quot; 같은 서술형 인자를 넣으면 실패하던 것도, 현재 브랜치를 리뷰하면서 그 문장을 findings에 메모로 붙이는 방식으로 바뀌었어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 권한 창이 뜨던 자리를 auto 모드 분류기가 대신 판단해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto 모드에서 dangerous-rm 검사, 백그라운드 &amp;amp; 실행 검사, 수상한 윈도우 경로 검사는 더 이상 권한 대화상자를 띄우지 않아요. 대신 auto 모드 분류기가 판정한다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto와 같이 쓰는 plan 모드도 같은 방향이에요. 정적 분석기가 읽기 전용이라고 증명하지 못한 Bash 명령마다 물어보던 절차가 빠지고, 분류기 판단으로 넘어갔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 창은 줄어드는 대신 판단 주체가 규칙에서 모델로 옮겨간 구조예요. 어느 정도까지 걸러주는지는 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. context: fork 스킬은 이제 기본이 백그라운드 실행이에요&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
name: my-skill
context: fork
background: false   # 앞에서 돌려야 하면 이렇게 빠져나와요
---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context: fork로 선언한 스킬은 changelog 기준으로 기본값이 백그라운드 실행으로 바뀌었어요. 앞단에서 돌아야 하는 스킬이면 frontmatter에 background: false를 직접 박아야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬&amp;middot;플러그인 frontmatter 불리언이 받는 값도 넓어졌어요. true/false 외에 yes&amp;middot;no&amp;middot;on&amp;middot;off&amp;middot;1&amp;middot;0을 대소문자 구분 없이 받아요. 반대로 에이전트 이름에 콜론은 못 쓰게 막혔는데, 플러그인 네임스페이스용으로 예약된 문자라서예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 대화가 날아가던 버그와 hook 신뢰 구멍이 막혔어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 화살표 키를 누르면 대화가 되돌릴 수 없이 사라지던 문제가 고쳐졌다고 해요. 편집 직후에 누르면 한 번 확인을 묻고, 에이전트 화면에서 Esc는 백그라운드로 보낸 대화로 돌아가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 쪽에서는 에이전트 frontmatter에 걸린 hook이 신뢰하지 않은 폴더에서 실행되던 통로가 막혔어요. 에이전트 파일이 놓인 폴더 자체가 workspace trust를 받아야 hook이 돌아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 쪽도 하나 있어요. 서버 연결이 실패하면 HTTP 상태와 에러 문구를 claude mcp list와 /mcp에 같이 보여줘서, 연결이 왜 안 됐는지 바로 확인돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://code.claude.com/docs/en/changelog&quot;&gt;Claude Code changelog&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>AI코딩</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>기능출시</category>
      <category>서브에이전트</category>
      <category>업데이트</category>
      <category>코드리뷰</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/576</guid>
      <comments>https://jessyt.tistory.com/576#entry576comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:00:33 +0900</pubDate>
    </item>
    <item>
      <title>[Logseq] 문서 대신 불릿으로 쓰는 로컬 노트</title>
      <link>https://jessyt.tistory.com/577</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/b580ms/dJMcabdSz1c/AAAAAAAAAAAAAAAAAAAAAJXiHP8sd7pZPv-7NqBasVSyxSdHttbnD8_bDJ4o3HbJ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=yosT9m0AUBNGDO1bDSXRvcq392Y%3D&quot; alt=&quot;[Logseq] 문서 대신 불릿으로 쓰는 로컬 노트&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;style&gt;
.yt-post,.yt-post *{box-sizing:border-box;}
.yt-post{max-width:680px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Pretendard&quot;,&quot;Noto Sans KR&quot;,sans-serif;color:#1a1a1a;line-height:1.78;font-size:16.5px;background:transparent;padding:4px;-webkit-font-smoothing:antialiased;color-scheme:light;}
.yt-post p{margin:0 0 20px 0;}
.yt-post h2{font-size:21px;font-weight:700 !important;letter-spacing:-0.01em;margin:52px 0 18px 0;line-height:1.4;color:#111111 !important;}
.yt-post h3{font-size:17px;font-weight:700 !important;margin:32px 0 12px 0;color:#111111 !important;}
.yt-post a{color:#1b6e3c;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;}
.yt-post strong{font-weight:700 !important;color:#111111 !important;}
.yt-post ul,.yt-post ol{margin:0 0 20px 0;padding-left:22px;}
.yt-post li{margin:0 0 8px 0;}

/* HERO — 텍스트 제목 블록 (그라데이션·glow·chip 제거) */
.yt-hero{margin:8px 0 40px 0;padding:0 0 26px 0;border-bottom:1px solid #e5e5e5;}
.yt-hero__tag{font-size:12.5px;letter-spacing:0.04em;color:#888888;margin:0 0 14px 0;}
.yt-hero__title{font-size:30px;font-weight:800 !important;line-height:1.28;margin:0 0 16px 0;color:#111111 !important;letter-spacing:-0.02em;}
.yt-hero__lede{font-size:17.5px;line-height:1.65;color:#444444;margin:0;}
.yt-hero__chips,.yt-hero__chip{display:none;}

/* 표 — 비교·데이터 (yt-vs·impact·indices·rank 대체) */
.yt-post table{width:100%;border-collapse:collapse;margin:24px 0 28px 0;font-size:15px;}
.yt-post th,.yt-post td{text-align:left;padding:10px 14px;border-bottom:1px solid #e5e5e5;vertical-align:top;}
.yt-post th{font-weight:700;color:#111111;border-bottom:2px solid #cccccc;}

/* 코드 — 검은배경 유지 (개발자 친화) */
.yt-code,.yt-post pre{background:#1a1a1a;color:#e8e8e8;border-radius:8px;padding:16px 18px;margin:24px 0;overflow-x:auto;font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;}
.yt-post code{font-family:ui-monospace,&quot;SF Mono&quot;,Menlo,Consolas,monospace;font-size:0.9em;background:transparent;padding:0;color:#1b6e3c;}
.yt-code code,.yt-post pre code{background:none;color:inherit;padding:0;}
.yt-code__file{display:block;font-size:11.5px;color:#888888;margin-bottom:10px;}
.yt-code__line{display:block;white-space:pre;}

/* 인용 — blockquote (큰 ❝ 제거, 좌보더 italic) */
.yt-post blockquote,.yt-pullquote{margin:28px 0;padding:4px 0 4px 22px;border-left:3px solid #1b6e3c;font-style:italic;font-size:18px;line-height:1.6;color:#333333;}
.yt-pullquote__text{font-style:italic;margin:0 0 8px 0;}
.yt-pullquote__sig{font-style:normal;font-size:13px;color:#888888;letter-spacing:0.04em;}

/* 노트 — 좌보더 1개 (yt-incident·position·checklist 대체) */
.yt-note{margin:24px 0;padding:14px 0 14px 18px;border-left:3px solid #cccccc;color:#333333;font-size:15.5px;line-height:1.7;}
.yt-note--warn{border-left-color:#b03a2e;}
.yt-note__label{display:block;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#888888;margin-bottom:6px;}

/* 푸터 — 단순 텍스트 */
.yt-coda-foot{font-size:16px;line-height:1.7;color:#333333;margin:32px 0;font-style:italic;}
.yt-coda-foot strong{font-style:normal;color:#111111;}
.yt-sources{margin:40px 0 16px 0;padding:20px 0 0 0;border-top:1px solid #e5e5e5;font-size:13.5px;color:#666666;}
.yt-sources__head{font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#999999;margin-bottom:10px;}
.yt-sources__link{display:block;color:#1b6e3c;text-decoration:none;padding:3px 0;word-break:break-all;}
.yt-disclaimer{font-size:12.5px;color:#999999;line-height:1.6;margin:12px 0;}

/* 시리즈·네비 — 절제된 텍스트 링크 */
.yt-series{font-size:13px;color:#888888;margin:0 0 28px 0;padding-bottom:14px;border-bottom:1px solid #eeeeee;}
.yt-series__label{text-transform:uppercase;letter-spacing:0.08em;font-size:11px;color:#999999;}
.yt-series__link{color:#1b6e3c;text-decoration:none;}
.yt-prev-next{margin:40px 0 0 0;padding-top:20px;border-top:1px solid #e5e5e5;font-size:14px;display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.yt-prev-next__label{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:#999999;margin-bottom:6px;}
.yt-prev-next__card{display:block;color:#1b6e3c;text-decoration:none;}
.yt-prev-next__card--next{text-align:right;}
.yt-prev-next__card--vacant{opacity:0.4;pointer-events:none;}

@media (max-width:600px){.yt-post{font-size:16px;}.yt-hero__title{font-size:24px;}.yt-hero__lede{font-size:16px;}.yt-post h2{font-size:19px;margin-top:42px;}.yt-prev-next{grid-template-columns:1fr;}.yt-prev-next__card--next{text-align:left;}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;yt-post&quot;&gt;
&lt;div class=&quot;yt-hero&quot;&gt;
&lt;p class=&quot;yt-hero__tag&quot; data-ke-size=&quot;size16&quot;&gt;개발자 도구 &amp;middot; 노트 &amp;amp; 지식관리&lt;/p&gt;
&lt;h1 class=&quot;yt-hero__title&quot;&gt;[Logseq] 문서 대신 불릿으로 쓰는 로컬 노트&lt;/h1&gt;
&lt;p class=&quot;yt-hero__lede&quot; data-ke-size=&quot;size16&quot;&gt;Logseq는 &quot;문서를 만들고 폴더에 정리하는&quot; 기존 노트 앱의 자리를 아웃라이너 방식으로 대체하는 오픈소스 도구예요. 모든 메모가 불릿(블록) 하나로 시작하고 데이터는 클라우드가 아니라 내 컴퓨터의 마크다운 파일로 남습니다. 이 글에서는 문서형 노트와 뭐가 다른지, 처음 설치해서 저널&amp;middot;백링크&amp;middot;쿼리까지 쓰는 순서, 그리고 도입 전에 알아야 할 DB 버전 전환기 이슈까지 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서를 쌓는 게 아니라 블록을 쌓는 노트예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion이나 Obsidian 같은 노트 앱은 기본 단위가 &quot;페이지(문서)&quot;예요. 글 하나를 만들어 제목을 붙인 뒤 어느 폴더에 넣을지 정하는 흐름이죠. Logseq는 기본 단위가 &lt;b&gt;블록(불릿 한 줄)&lt;/b&gt;입니다. 페이지를 먼저 만드는 게 아니라, 오늘 날짜 저널에 불릿을 하나 적는 것부터 시작해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 소개 문구는 &quot;privacy-first, open-source platform for knowledge management&quot;입니다. 여기서 privacy-first가 구호가 아닌 게, 모든 데이터가 서버가 아니라 로컬 디스크의 마크다운(또는 Org-mode) 파일로 저장돼요. 라이선스는 AGPL-3.0 오픈소스입니다. 데스크톱 앱은 무료로 GitHub 릴리즈에서 받아요. 2020년에 공개됐고 모바일 앱과 웹 버전(app.logseq.com)도 있어요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Logseq&lt;/th&gt;
&lt;th&gt;Obsidian&lt;/th&gt;
&lt;th&gt;Notion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본 단위&lt;/td&gt;
&lt;td&gt;블록(불릿)&lt;/td&gt;
&lt;td&gt;페이지(문서)&lt;/td&gt;
&lt;td&gt;페이지 + 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장 위치&lt;/td&gt;
&lt;td&gt;로컬 파일&lt;/td&gt;
&lt;td&gt;로컬 파일&lt;/td&gt;
&lt;td&gt;클라우드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 포맷&lt;/td&gt;
&lt;td&gt;Markdown, Org-mode&lt;/td&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;td&gt;자체 포맷 (내보내기 지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이선스&lt;/td&gt;
&lt;td&gt;오픈소스 (AGPL-3.0)&lt;/td&gt;
&lt;td&gt;비공개 소스&lt;/td&gt;
&lt;td&gt;비공개 소스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 로컬 마크다운 계열인 Obsidian과 갈리는 지점이 바로 이 &quot;블록 vs 문서&quot;예요. 긴 글을 완성해 쌓아두는 쪽이면 문서형이 편합니다. 회의 메모&amp;middot;TIL&amp;middot;읽은 것 기록처럼 짧은 조각이 매일 쏟아지는 쪽이면 아웃라이너가 맞아요. Logseq는 후자를 위한 도구예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작은 저널이에요, 오늘 날짜 페이지에 그냥 씁니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 &lt;a href=&quot;https://github.com/logseq/logseq/releases&quot;&gt;GitHub 릴리즈 페이지&lt;/a&gt;에서 OS에 맞는 파일을 받으면 됩니다. macOS는 dmg, Windows는 exe, Linux는 AppImage와 설치 스크립트가 제공돼요. 앱을 열면 가장 먼저 &quot;그래프(graph)&quot;를 만들라고 하는데, 그래프는 노트 전체를 담는 로컬 폴더 하나를 뜻합니다. 빈 폴더 하나를 지정하면 끝이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프를 열면 바로 보이는 화면이 &lt;b&gt;저널(Journals)&lt;/b&gt;입니다. 오늘 날짜 페이지가 자동으로 만들어져 있고, 커서가 첫 불릿에 놓여 있어요. 여기가 Logseq 워크플로우의 출발점입니다. &quot;이 메모를 어디에 저장하지?&quot;를 고민하지 않고 일단 오늘 저널에 적는 것 &amp;mdash; 분류는 나중에 링크가 대신해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 중에 &lt;code&gt;/&lt;/code&gt;를 누르면 명령 메뉴가 떠서 할 일(TODO), 날짜, 코드 블록 같은 요소를 바로 넣어요. Tab으로 들여쓰기, Shift+Tab으로 내어쓰기를 하면 블록이 트리 구조로 쌓입니다. 문서형 에디터의 제목&amp;middot;본문 위계 대신, 들여쓰기 깊이가 곧 구조가 되는 방식이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심은 링크예요 &amp;mdash; 폴더 없이 노트가 연결됩니다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 링크 [[ ]]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 중에 &lt;code&gt;[[Kafka]]&lt;/code&gt;처럼 대괄호 두 개로 감싸면 그 이름의 페이지가 자동으로 만들어지면서 링크가 걸립니다. 미리 페이지를 만들 필요가 없어요. 저널에 흩어진 메모들이 &lt;code&gt;[[Kafka]]&lt;/code&gt; 링크 하나로 한 주제에 모이는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;링크드 레퍼런스 (Linked References)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 하단에는 그 페이지를 링크한 모든 블록이 자동으로 모여서 보여요. Kafka 페이지에 직접 쓴 내용이 없어도, 여러 날짜의 저널에서 &lt;code&gt;[[Kafka]]&lt;/code&gt;를 언급한 메모가 전부 그 아래 나타납니다. 폴더 정리를 안 해도 주제별 모아보기가 되는 이유가 이 기능이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블록 참조 (( ))&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 단위가 아니라 &lt;b&gt;블록 하나를 다른 곳에 참조&lt;/b&gt;하는 것도 됩니다. 특정 불릿을 다른 페이지에 인용하면 원본이 수정될 때 참조한 쪽에도 같이 반영돼요. 문서형 노트에서 같은 내용을 복사해 여러 문서에 붙여넣던 일을 참조 하나로 대신하는 셈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모가 그대로 할 일 목록과 플래시카드가 됩니다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태스크 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 앞에 TODO 키워드를 붙이면 그 메모가 할 일이 됩니다. 클릭할 때마다 TODO &amp;rarr; DOING &amp;rarr; DONE으로 상태가 바뀌어요. 별도 투두 앱에 옮겨 적는 게 아니라, 회의 메모 중간의 &quot;이거 확인해야 함&quot; 한 줄이 그 자리에서 태스크가 되는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흩어진 블록을 조건으로 모아 오는 쿼리 기능이 있어요. 예를 들어 &quot;아직 DONE이 아닌 TODO 전부&quot; 같은 조건을 페이지에 심어두면 결과가 실시간으로 갱신됩니다. 라이브 쿼리와 화이트보드는 0.9.1 버전(2023년 3월)부터 전체 사용자에게 공개됐어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;플래시카드와 PDF 주석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록에 #card 태그를 붙이면 그 블록이 암기 카드가 되어 내장된 간격 반복(spaced repetition) 방식으로 복습 순서가 돌아옵니다. PDF를 그래프에 넣으면 앱 안에서 열어 하이라이트를 칠 수 있고 그 하이라이트가 블록으로 저장되어 노트에서 참조돼요. 논문이나 기술 문서를 읽으며 발췌하는 용도로 쓰이는 기능입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디스크에는 평범한 마크다운 폴더가 남습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Logseq가 만드는 그래프 폴더를 파인더나 터미널에서 열어보면 이런 구조예요. 저널은 날짜별 파일로, 직접 만든 페이지는 pages 폴더에 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;plaintext&quot;&gt;&lt;code&gt;my-graph/
├── journals/
│   ├── 2026_07_17.md
│   └── 2026_07_18.md
├── pages/
│   ├── Kafka.md
│   └── 독서 메모.md
└── logseq/
    └── config.edn   # 그래프별 설정 파일

# journals/2026_07_18.md 내용 예시
- TODO [[Kafka]] 컨슈머 랙 모니터링 문서 읽기
- [[독서 메모]] 3장까지 읽음
	- 핵심은 파티션 단위 순서 보장 #card&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 의미는 &lt;b&gt;탈출 비용이 없다&lt;/b&gt;는 거예요. Logseq를 그만 쓰기로 해도 마크다운 파일은 그대로 남아 다른 에디터로 열립니다. Git으로 버전 관리를 하거나 스크립트로 가공하는 것도 일반 텍스트 파일 다루듯 하면 돼요. 클라우드 노트 서비스가 종료되거나 유료 정책이 바뀔 때의 리스크가 구조적으로 없는 저장 방식입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도입 전에 알아둘 것 &amp;mdash; 지금은 DB 버전 전환기예요&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DB 버전 전환기&lt;/b&gt; &amp;mdash; 공식 저장소 기준으로 현재 파일 기반 버전과 별개로 &quot;DB 그래프&quot;를 쓰는 데이터베이스 버전이 베타로 진행 중입니다. DB 버전용 새 모바일 앱(iOS 먼저, Android 예정)과 실시간 협업(RTC) 동기화는 알파 단계예요. 지금 시작한다면 안정적인 파일 기반 버전으로 시작하되, 개발의 무게중심이 옮겨가는 시기라는 점은 알고 들어가는 게 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공식 Sync는 아직 후원자 대상 베타&lt;/b&gt; &amp;mdash; 기기 간 동기화 기능인 Logseq Sync는 스폰서&amp;middot;백커 대상 베타로 운영돼 왔어요. 일반 사용자는 iCloud Drive&amp;middot;Dropbox 같은 파일 동기화나 Git으로 폴더 자체를 맞추는 방식을 씁니다. 다만 파일 동기화 서비스 위에서 여러 기기가 동시에 쓰면 충돌 파일이 생길 여지가 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 것이 불릿이라는 호불호&lt;/b&gt; &amp;mdash; 아웃라이너 구조상 긴 산문형 문서를 쓰기에는 문서형 에디터보다 불편하다는 평가가 따라다닙니다. 블로그 초안처럼 완결된 글 위주라면 맞지 않을 가능성이 높아요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백업은 본인 몫&lt;/b&gt; &amp;mdash; 로컬 저장의 뒷면입니다. 서버 백업이 없으니 Time Machine이든 Git이든 백업 수단을 따로 두어야 해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;yt-note&quot;&gt;&lt;span class=&quot;yt-note__label&quot;&gt;설치 안내&lt;/span&gt; 데스크톱 앱은 GitHub 릴리즈에서 무료로 받습니다. 설치 없이 써 보려면 브라우저에서 app.logseq.com으로 로컬 폴더를 연결해 체험하는 방법도 있습니다. 플러그인과 테마는 앱 내 마켓플레이스에서 설치해요.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;매일 쏟아지는 짧은 기록이 많은 사람에게 맞습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Logseq가 맞는 경우는 이렇습니다. 회의 메모&amp;middot;TIL&amp;middot;트러블슈팅 기록처럼 짧은 조각이 매일 생기는 사람, 폴더 분류에 번번이 실패해 본 사람, 그리고 노트 데이터를 특정 서비스에 묶어두고 싶지 않은 사람이에요. 반대로 완결된 긴 문서 위주라면 Obsidian 같은 문서형이, 팀 공유와 데이터베이스가 필요하면 Notion 쪽이 용도에 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작하는 방법은 단순해요. 빈 폴더로 그래프 하나를 만들고, 일주일 동안 모든 메모를 저널에만 적으면서 주제가 보일 때마다 대괄호로 링크를 걸어 보는 것. 폴더 없이 링크만으로 노트가 정리되는 감각이 맞는지 확인되면, 그때 쿼리&amp;middot;플래시카드 같은 기능을 얹어 가면 됩니다.&lt;/p&gt;
&lt;div class=&quot;yt-sources&quot;&gt;
&lt;div class=&quot;yt-sources__head&quot;&gt;Sources&lt;/div&gt;
&lt;a class=&quot;yt-sources__link&quot; href=&quot;https://logseq.com/&quot;&gt;Logseq 공식 사이트&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://docs.logseq.com/&quot;&gt;Logseq 공식 문서&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://github.com/logseq/logseq&quot;&gt;Logseq GitHub 저장소 (README&amp;middot;릴리즈)&lt;/a&gt; &lt;a class=&quot;yt-sources__link&quot; href=&quot;https://blog.logseq.com/&quot;&gt;Logseq 공식 블로그&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;yt-disclaimer&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 공식 사이트&amp;middot;문서&amp;middot;저장소를 근거로 한 객관 정리로, 직접 사용기가 아닙니다. 기능과 정책은 이후 버전에서 달라질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발자 도구/노트 &amp;amp; 지식관리</category>
      <category>logseq</category>
      <category>노트앱</category>
      <category>오픈소스</category>
      <category>지식관리</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/577</guid>
      <comments>https://jessyt.tistory.com/577#entry577comment</comments>
      <pubDate>Fri, 24 Jul 2026 06:00:28 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE</title>
      <link>https://jessyt.tistory.com/494</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q2k9P/dJMcahLrGjy/nFDzraXpLM0Arj07gwkzqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q2k9P/dJMcahLrGjy/nFDzraXpLM0Arj07gwkzqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q2k9P/dJMcahLrGjy/nFDzraXpLM0Arj07gwkzqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq2k9P%2FdJMcahLrGjy%2FnFDzraXpLM0Arj07gwkzqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h1&gt;[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;행 하나만 UPDATE했는데 왜 다른 INSERT가 막히죠?&quot; InnoDB 락을 모르면 미스터리지만, 알면 당연한 동작이에요. InnoDB는 행만 잠그는 게 아니라 행 사이의 &lt;b&gt;틈(갭)&lt;/b&gt;도 잠그거든요. 이 글은 그 락의 실체를 정리해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 락&amp;middot;갭 락&amp;middot;넥스트키 락이 각각 무엇을 잠그는지, 갭 락이 팬텀을 어떻게 막는지, 공유 락과 배타 락은 어떻게 다른지, 그리고 &quot;인덱스 없는 UPDATE가 테이블을 통째로 잠그는&quot; 최악의 함정까지 짚어요. &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;격리수준&amp;middot;MVCC&lt;/a&gt; 편과 한 쌍이에요 &amp;mdash; MVCC가 &quot;안 막고 읽는&quot; 장치라면, 락은 &quot;막아야 할 때&quot;의 장치예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 잠그는 읽기와 안 잠그는 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 구분부터요. 일반 SELECT는 &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;MVCC 일관 읽기&lt;/a&gt;라 락을 안 잡아요. 락이 등장하는 건 쓰기(UPDATE&amp;middot;DELETE&amp;middot;INSERT)와 잠그는 읽기예요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM orders WHERE id = 1;                  -- 락 없음 (스냅샷 읽기)
SELECT * FROM orders WHERE id = 1 FOR UPDATE;       -- 배타 락 (X)
SELECT * FROM orders WHERE id = 1 FOR SHARE;        -- 공유 락 (S)
UPDATE orders SET status = 'PAID' WHERE id = 1;     -- 배타 락 (X)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 락(S)끼리는 같이 잡을 수 있지만(여럿이 읽기), 배타 락(X)은 어떤 락과도 같이 못 잡아요. &lt;code&gt;FOR UPDATE&lt;/code&gt;는 &quot;내가 곧 수정할 테니 아무도 건드리지 마&quot;고, JPA의 &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;비관적 락&lt;/a&gt;이 바로 이걸 쓰는 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. 락 3종, 행&amp;middot;틈&amp;middot;둘 다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNFM0h/dJMcagTjIU6/uUZdYL2QBbapVemijC5xik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNFM0h/dJMcagTjIU6/uUZdYL2QBbapVemijC5xik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNFM0h/dJMcagTjIU6/uUZdYL2QBbapVemijC5xik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNFM0h%2FdJMcagTjIU6%2FuUZdYL2QBbapVemijC5xik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;InnoDB 락 3종. 레코드 락은 존재하는 행 자체를 잠그고, 갭 락은 행 사이의 틈을 잠가 INSERT를 차단하며, 넥스트키 락은 레코드와 직전 갭을 함께 잠그는 REPEATABLE READ의 기본 잠금 단위다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레코드 락&lt;/b&gt; &amp;mdash; 존재하는 행(정확히는 인덱스 레코드)을 잠가요. &lt;code&gt;WHERE id = 20&lt;/code&gt;의 UPDATE가 잡는 그 락이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;갭 락&lt;/b&gt; &amp;mdash; 행 사이의 틈을 잠가요. id 10과 20 사이의 갭을 잠그면, 그 사이 값(11~19)의 INSERT가 막혀요. 행이 아니라 &quot;없는 자리&quot;를 잠그는 거예요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;넥스트키 락&lt;/b&gt; &amp;mdash; 레코드 락 + 직전 갭 락이에요. REPEATABLE READ에서 범위 조건 잠금의 기본 단위예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;행 하나 잠갔는데 INSERT가 막혀요&quot;의 정체가 갭 락이에요. 버그가 아니라, 다음에 볼 팬텀 방지를 위한 의도된 동작이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 갭 락이 팬텀을 막는 구조&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tuQfk/dJMcagTjIU8/K20bQsBXA8fKeAPy11KJkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tuQfk/dJMcagTjIU8/K20bQsBXA8fKeAPy11KJkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tuQfk/dJMcagTjIU8/K20bQsBXA8fKeAPy11KJkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtuQfk%2FdJMcagTjIU8%2FK20bQsBXA8fKeAPy11KJkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;갭 락의 팬텀 방지. 범위 조건 FOR UPDATE가 행들과 사이 틈까지 넥스트키 락으로 잠가, 다른 트랜잭션의 틈 INSERT가 대기하므로 같은 범위를 다시 읽어도 없던 행이 나타나지 않는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 A가 &lt;code&gt;WHERE id BETWEEN 10 AND 30 FOR UPDATE&lt;/code&gt;로 범위를 잠그면, 그 범위의 행들뿐 아니라 사이사이 틈까지 넥스트키 락으로 잠겨요. 그래서 B가 id=15를 INSERT하려 하면 A가 끝날 때까지 대기해요. A가 같은 범위를 다시 읽어도 새 행이 나타날 수 없죠 &amp;mdash; &lt;a href=&quot;https://jessyt.tistory.com/493&quot;&gt;앞 편&lt;/a&gt;에서 &quot;InnoDB는 RR에서 팬텀을 대부분 막는다&quot;고 한 그 장치예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;대가도 분명해요. 갭 락은 &quot;없는 자리&quot;까지 잠그니, 동시 INSERT가 많은 테이블에서 범위 잠금이 잦으면 INSERT들이 줄줄이 대기해요. 심지어 &lt;b&gt;조건에 맞는 행이 하나도 없어도&lt;/b&gt; 그 구간의 갭은 잠겨요 &amp;mdash; &quot;없는 걸 조회했는데 락 경합&quot;이라는 황당한 상황의 정체예요. 갭 락 경합이 심하면 잠그는 범위를 좁히거나(동등 조건+유니크 인덱스는 갭 락 없이 레코드 락만), READ COMMITTED(갭 락 거의 없음)를 검토하는 게 그 &quot;왜&quot;가 될 수 있어요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 최악의 함정, 인덱스 없는 UPDATE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 이 글에서 제일 중요한 부분이에요. &lt;b&gt;락은 인덱스 레코드 위에 걸려요.&lt;/b&gt; 그러면 조건 컬럼에 인덱스가 없으면 어떻게 될까요?&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHZBdR/dJMcaf7S68G/QTcjKRSyuPg37ciwx04Bfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHZBdR/dJMcaf7S68G/QTcjKRSyuPg37ciwx04Bfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHZBdR/dJMcaf7S68G/QTcjKRSyuPg37ciwx04Bfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHZBdR%2FdJMcaf7S68G%2FQTcjKRSyuPg37ciwx04Bfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;인덱스 없는 UPDATE의 위험. 인덱스 있는 조건은 해당 레코드만 잠그지만, 인덱스 없는 컬럼 조건은 풀 스캔하며 읽은 모든 행과 틈을 잠가 사실상 테이블 전체가 잠긴다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 없으니 풀 스캔으로 모든 행을 읽으면서 검사하는데, InnoDB는 &lt;b&gt;읽은 행을 전부 잠가요.&lt;/b&gt; 조건에 안 맞는 행도요(RR 기준). 행 1건 고치려던 UPDATE가 사실상 테이블 전체 잠금이 되고, 그 테이블의 모든 쓰기가 줄을 서요. 트래픽 있는 서비스면 이거 하나로 장애예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;UPDATE&amp;middot;DELETE의 WHERE 컬럼 인덱스는 성능 문제가 아니라 안전 문제&lt;/b&gt;예요. 어드민에서 가끔 도는 정리 쿼리, 배치의 조건 컬럼까지 전부 점검 대상이에요. &quot;느린 건 참아도 잠그는 건 못 참는다&quot;가 운영의 감각이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. INSERT의 락, 중복 키와 인텐션 락&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT도 락과 무관하지 않아요. 두 가지만 알아둬요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;유니크 키 중복 검사&lt;/b&gt;예요. 같은 유니크 값을 두 트랜잭션이 INSERT하면, 뒤의 것이 앞의 커밋/롤백까지 대기해요(중복 여부가 그때 결정되니까). 선착순 가입 같은 데서 의외의 대기가 생기는 지점이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;인텐션 락&lt;/b&gt;이라는 게 &lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;나 바로 다음에 볼 &lt;code&gt;data_locks&lt;/code&gt; 출력에 보이는데(IS&amp;middot;IX), 이건 &quot;이 테이블 어딘가에 행 락을 걸 예정&quot;이라는 표식일 뿐이에요. 행 락끼리의 경합과는 무관하니 보여도 놀랄 필요 없어요. 테이블 전체 작업(DDL 등)과의 조율용이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;06. 락을 눈으로 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 누가 뭘 잠그고 누가 기다리는지는 &lt;code&gt;performance_schema&lt;/code&gt;로 봐요.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 현재 락 대기 관계 (누가 누구를 기다리나)
SELECT * FROM sys.innodb_lock_waits;

-- 잡혀 있는 락 상세
SELECT * FROM performance_schema.data_locks;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;UPDATE가 안 끝나요&quot; 신고가 오면 이 두 개부터예요. 기다리는 쿼리와 잡고 있는 트랜잭션이 바로 보여요. 잡고 있는 쪽이 안 끝나는 장수 트랜잭션이면 그게 근본 원인이고요. 락 대기가 서로 맞물리면 데드락인데, 그건 다음 편의 주제예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;07. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행 하나 잠갔는데 INSERT가 막혀요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갭 락이에요. 범위&amp;middot;비유니크 조건의 잠금은 틈까지 잠가요. 유니크 인덱스 동등 조건으로 잠그면 레코드 락만 걸려요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UPDATE 하나에 서비스 전체 쓰기가 멈췄어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 없는 WHERE의 풀 스캔 잠금을 의심해요. &lt;code&gt;data_locks&lt;/code&gt;에서 락 범위를 확인하고 조건 컬럼에 인덱스를 추가해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락 타임아웃(Lock wait timeout exceeded)이 나요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가 오래 잡고 있는 거예요. &lt;code&gt;innodb_lock_waits&lt;/code&gt;로 블로커를 찾아요. 대부분 커밋 안 하고 열려 있는 트랜잭션(애플리케이션 버그, 수동 세션)이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB 락은 &lt;b&gt;레코드(행)&amp;middot;갭(틈)&amp;middot;넥스트키(둘 다)&lt;/b&gt; 세 단위로 나뉘고, 전부 인덱스 레코드 위에 걸려요. 갭 락이 팬텀을 막는 대신 INSERT 대기를 만들고, 인덱스 없는 UPDATE는 풀 스캔 잠금으로 테이블을 통째로 세워요. UPDATE&amp;middot;DELETE의 WHERE에는 인덱스 &amp;mdash; 이 글에서 단 하나만 챙긴다면 이 문장이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 락 대기가 한 방향이 아니라 서로 맞물리면 어떻게 될까요. 두 트랜잭션이 서로의 락을 기다리며 영원히 멈추는 그 상황, 데드락의 진단과 해결로 이어집니다. 애플리케이션 레벨의 락 선택은 &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;JPA 낙관적&amp;middot;비관적 락&lt;/a&gt; 편과 맞닿아 있고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html&quot;&gt;MySQL &amp;mdash; InnoDB Locking&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html&quot;&gt;MySQL &amp;mdash; Locks Set by Different SQL Statements&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>innodb</category>
      <category>MYSQL</category>
      <category>SELECT FOR UPDATE</category>
      <category>갭 락</category>
      <category>넥스트키 락</category>
      <category>락</category>
      <category>레코드 락</category>
      <category>백엔드</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/494</guid>
      <comments>https://jessyt.tistory.com/494#entry494comment</comments>
      <pubDate>Thu, 23 Jul 2026 07:30:36 +0900</pubDate>
    </item>
    <item>
      <title>[Claude Code] 2.1.217, 서브에이전트 폭주에 상한이 걸렸어요</title>
      <link>https://jessyt.tistory.com/575</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://blog.kakaocdn.net/dna/nltVp/dJMcabLSO7I/AAAAAAAAAAAAAAAAAAAAAKejdW-wQv3B44_RCgJfoF7PIHsbILD0kOuu_KvcbrmP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2FZsh7FTKewlQ1zo4rQfsx3G29ik%3D&quot; alt=&quot;[Claude Code] 2.1.217, 서브에이전트 폭주에 상한이 걸렸어요&quot; /&gt;&lt;/figure&gt;
&lt;h1&gt;[Claude Code] 2.1.217, 서브에이전트 폭주에 상한이 걸렸어요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 2.1.216과 2.1.217이 나왔어요. 이번 두 릴리즈에서 가장 큰 변화는 서브에이전트 실행에 걸린 안전장치예요. 동시 실행 상한이 기본 20개로 잡혔고 중첩 스폰은 기본으로 꺼졌어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 서브에이전트가 무한정 불어나지 않게 됐어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog 기준으로 메시지 하나가 백그라운드 에이전트를 무제한으로 만들 수 없도록 동시 실행 서브에이전트에 기본 20개 상한이 생겼어요. 서브에이전트가 다시 서브에이전트를 만드는 중첩 스폰은 아예 기본으로 막혔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값 모두 환경 변수로 조절 가능해요. 기본값이 부족한 대규모 에이전트 워크플로우를 굴리는 케이스라면 확인해둘 부분이에요.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;export CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=40   # 동시 실행 상한 (기본 20)
export CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=2    # 중첩 스폰 허용 깊이 (기본 차단)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 쪽 구멍도 같이 막혔어요. --max-budget-usd 상한에 도달해도 백그라운드 서브에이전트가 계속 돌던 버그가 고쳐져서 이제 상한을 넘으면 새 스폰이 거부되고 실행 중이던 백그라운드 에이전트도 멈춰요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. :heart: 치면 ❤️가 들어가는 자동완성이 생겼어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 입력창에 이모지 숏코드 자동완성이 붙었어요. :heart:를 치면 ❤️로 바뀌고 :hea까지만 쳐도 후보가 떠요. 필요 없으면 emojiCompletionEnabled 설정으로 끌 수 있어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 트랜스크립트가 조용히 사라지던 상황에 경고가 붙었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크가 가득 차서 트랜스크립트 쓰기가 실패하거나 물려받은 환경 변수 탓에 세션 저장이 꺼져 있으면 그동안은 기록이 소리 없이 날아갔어요. 2.1.217부터는 이 상황에서 경고를 띄워줘요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 누수 수정도 하나 있어요. 잘린 MCP tool 출력이 원본 전체를 세션 끝까지 메모리에 붙잡고 있던 문제가 고쳐졌어요. MCP 서버를 많이 붙여 쓰는 환경이라면 체감될 만한 수정이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 구멍도 하나 닫혔어요. 심볼릭 링크가 걸린 작업 폴더를 정규화하지 않아 백그라운드 세션이 자기 작업 폴더 밖으로 빠져나갈 수 있던 문제가 2.1.217에서 고쳐졌어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 2.1.216은 긴 세션 느려짐을 잡은 릴리즈예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 세션에서 턴이 쌓일수록 메시지 정규화 비용이 제곱으로 늘어나 몇 초씩 멈추고 resume도 느려지던 문제가 2.1.216에서 고쳐졌어요. 세션을 길게 끌고 가는 사용자에게는 이게 제일 반가운 항목일 듯해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정도 하나 추가됐어요. sandbox.filesystem.disabled를 켜면 네트워크 egress 제어는 유지하면서 파일시스템 격리만 끌 수 있어요. 샌드박스를 통째로 켜고 끄는 것만 되던 기존보다 조합이 유연해졌어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 관련 수정도 있어요. worktree 격리 서브에이전트가 git -C나 GIT_DIR 변수로 공유 checkout에 손대던 우회로가 막혔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트는 claude update 한 번이면 돼요. 상한 기본값이 실제 워크플로우에 어떤 영향을 주는지는 직접 써보고 판단할 부분이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG (공식)&lt;/a&gt;, &lt;a href=&quot;https://code.claude.com/docs/en/whats-new&quot;&gt;Claude Code Docs &amp;mdash; What's new&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 공식 발표 내용을 정리한 것이고, Anthropic으로부터 어떤 형식의 협찬도 받지 않았습니다.&lt;/p&gt;</description>
      <category>AI/정보</category>
      <category>ai코딩도구</category>
      <category>anthropic</category>
      <category>changelog</category>
      <category>claude</category>
      <category>claudecode</category>
      <category>CLI</category>
      <category>서브에이전트</category>
      <category>업데이트</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/575</guid>
      <comments>https://jessyt.tistory.com/575#entry575comment</comments>
      <pubDate>Thu, 23 Jul 2026 07:00:15 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드</title>
      <link>https://jessyt.tistory.com/493</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p3leD/dJMcaci1euz/0IQ4XsDc6zALkjs1DVFC31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p3leD/dJMcaci1euz/0IQ4XsDc6zALkjs1DVFC31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p3leD/dJMcaci1euz/0IQ4XsDc6zALkjs1DVFC31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp3leD%2FdJMcaci1euz%2F0IQ4XsDc6zALkjs1DVFC31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6Ultt%2FdJMcaar272H%2FTOuvAnmnaEIbsokh9wL6Rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;706&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리수준은 면접 단골이라 표는 다들 외우는데, 실무에서 정작 중요한 질문은 따로 있어요. &quot;MySQL 기본값(REPEATABLE READ)을 그냥 둬도 되나?&quot;, &quot;읽는 동안 왜 안 막히지?&quot;, &quot;같은 트랜잭션에서 조회했는데 왜 옛날 값이 나오지?&quot; &amp;mdash; 전부 MVCC를 알면 풀려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리수준 4단계와 이상 현상, InnoDB가 락 없이 일관 읽기를 만드는 MVCC(언두 로그), READ COMMITTED와 REPEATABLE READ의 실질 차이, 그리고 실무 선택 기준까지 차례로 풀어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;01. 격리수준이 거래하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 완전히 직렬로 돌리면 정합성은 완벽하지만 동시성이 죽어요. 격리수준은 &quot;어느 정도의 이상 현상을 허용하고 동시성을 살릴까&quot;의 단계별 거래예요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6Ultt/dJMcaar272H/TOuvAnmnaEIbsokh9wL6Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6Ultt%2FdJMcaar272H%2FTOuvAnmnaEIbsokh9wL6Rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;격리수준과 이상 현상 매트릭스. READ UNCOMMITTED는 더티 리드와 반복 불가 읽기와 팬텀이 모두 발생하고 READ COMMITTED는 더티 리드만 방지한다. MySQL 기본인 REPEATABLE READ는 반복 불가 읽기까지 방지하고 팬텀도 InnoDB에서는 대부분 방지하며, SERIALIZABLE은 전부 방지한다&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;706&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;더티 리드&lt;/b&gt; &amp;mdash; 커밋 안 된 값을 읽음. 롤백되면 &quot;없던 값&quot;을 읽은 셈이라 치명적이에요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복 불가 읽기&lt;/b&gt; &amp;mdash; 같은 행을 두 번 읽었는데 사이에 다른 커밋이 끼어 값이 달라짐.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팬텀 리드&lt;/b&gt; &amp;mdash; 같은 조건으로 두 번 조회했는데 없던 행이 나타남(INSERT가 끼어듦).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준상 REPEATABLE READ는 팬텀을 허용하지만, &lt;b&gt;InnoDB는 MVCC와 갭 락 덕분에 RR에서도 팬텀을 대부분 막아요.&lt;/b&gt; MySQL이 표준 표보다 한 칸 강한 이유고, 그 장치가 다음 주제예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. MVCC, 락 없이 읽는 장치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순진하게 구현하면 &quot;읽는 동안 쓰지 마&quot; 락이 필요해요. 그러면 읽기&amp;middot;쓰기가 서로 줄을 서서 동시성이 박살나죠. InnoDB는 다르게 풀어요 &amp;mdash; &lt;b&gt;버전을 여러 개 보관하고 트랜잭션마다 자기 시점의 버전을 읽게&lt;/b&gt; 해요. 다중 버전 동시성 제어(MVCC)예요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4VPA/dJMcaiKiIOY/tQXVpjOxTu4wu2OZcgx580/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4VPA/dJMcaiKiIOY/tQXVpjOxTu4wu2OZcgx580/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4VPA/dJMcaiKiIOY/tQXVpjOxTu4wu2OZcgx580/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4VPA%2FdJMcaiKiIOY%2FtQXVpjOxTu4wu2OZcgx580%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;InnoDB MVCC 동작. 행의 최신 값과 별개로 언두 로그에 이전 버전들이 체인으로 보관되어, 먼저 시작한 트랜잭션은 자기 시작 시점 기준 버전을 언두 체인에서 찾아 읽는다. 읽기와 쓰기가 서로 막지 않는다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;행이 수정되면 이전 값이 &lt;b&gt;언두 로그&lt;/b&gt;에 체인으로 남아요. SELECT는 &quot;내 트랜잭션이 볼 수 있는 시점&quot;의 버전을 이 체인에서 찾아 읽어요. 그래서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누가 수정 중이어도 내 SELECT는 안 기다려요(이전 버전을 읽으면 되니까).&lt;/li&gt;
&lt;li&gt;내 SELECT가 길어도 남의 쓰기를 안 막아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 &quot;MySQL은 읽기가 안 막힌다&quot;의 정체예요. 일반 SELECT는 락을 안 잡는 &lt;b&gt;일관 읽기(consistent read)&lt;/b&gt;고, 락을 잡는 읽기(&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;)는 별도예요 &amp;mdash; 그건 다음 편(락)의 주제예요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;MVCC의 숨은 비용도 알아둬요. 언두 로그는 &quot;그 버전을 볼 수도 있는 트랜잭션&quot;이 살아 있는 한 못 지워요. 그래서 &lt;b&gt;몇 시간씩 열려 있는 긴 트랜잭션 하나가 언두 로그를 무한정 키워요.&lt;/b&gt; 디스크가 차고 버전 체인이 길어져 조회도 느려져요. &quot;트랜잭션은 짧게&quot;가 매너가 아니라 성능 규칙인 이유 중 하나예요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. RC vs RR, 스냅샷을 언제 찍나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ COMMITTED와 REPEATABLE READ의 실질 차이는 한 줄이에요 &amp;mdash; &lt;b&gt;스냅샷(읽기 기준 시점)을 언제 찍느냐.&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T5v92/dJMcaci1euy/KWFR8vK4aLsExXSkCpbMFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T5v92/dJMcaci1euy/KWFR8vK4aLsExXSkCpbMFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T5v92/dJMcaci1euy/KWFR8vK4aLsExXSkCpbMFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT5v92%2FdJMcaci1euy%2FKWFR8vK4aLsExXSkCpbMFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;READ COMMITTED와 REPEATABLE READ 차이. RC는 SELECT마다 새 스냅샷을 찍어 항상 최신 커밋을 보고, MySQL 기본 RR은 첫 SELECT에 스냅샷을 고정해 트랜잭션 내내 같은 시점을 본다&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RC&lt;/b&gt; &amp;mdash; SELECT마다 새 스냅샷. 항상 &quot;방금 커밋된&quot; 최신을 봐요. 같은 트랜잭션 안에서 같은 쿼리 결과가 달라질 수 있어요(반복 불가 읽기 허용).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RR&lt;/b&gt; &amp;mdash; 트랜잭션의 첫 SELECT에서 스냅샷을 고정. 내내 같은 시점을 봐요. &quot;분명 다른 데서 커밋했는데 내 트랜잭션에선 안 보여요&quot;는 버그가 아니라 RR의 정상 동작이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 기준은 단순해요. &lt;b&gt;MySQL은 RR 기본을 그대로 쓰는 게 보통이에요.&lt;/b&gt; 생태계(복제, 잠금 동작)가 RR 기준으로 다져져 있고, 트랜잭션 내 일관성은 대부분의 비즈니스 로직에 이로워요. RC로 낮추는 건 갭 락 경합을 줄이려는 등 구체적 이유가 있을 때의 선택이에요 &amp;mdash; &quot;왜&quot;가 있어야 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 흔한 오해 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;격리수준이 있으면 갱신 분실도 막아주죠?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니요. A&amp;middot;B가 같은 값을 읽고 각자 계산해 쓰는 &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;갱신 분실&lt;/a&gt;은 RR에서도 생겨요. MVCC는 &quot;읽기의 일관성&quot;을 줄 뿐, read-modify-write 충돌은 낙관적 락(@Version)&amp;middot;비관적 락(FOR UPDATE)&amp;middot;원자적 UPDATE로 따로 막아야 해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;RR이면 같은 걸 두 번 읽으면 늘 같다면서요? 그런데 달라졌어요&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 트랜잭션이 수정한 건 보여요. 그리고 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;는 스냅샷이 아니라 &lt;b&gt;최신 커밋 값&lt;/b&gt;을 읽어요(락을 잡아야 하니까요). 일반 SELECT와 락 읽기가 다른 시점을 볼 수 있다는 건 꽤 미묘한 함정이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;SERIALIZABLE이 제일 안전하니 그걸 쓰면 되죠?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 SELECT가 공유 락을 잡게 돼서 동시성이 크게 떨어져요. 일반 서비스에서 전역 SERIALIZABLE은 사실상 안 써요. 정합성이 극도로 중요한 특정 구간만 락(FOR UPDATE)으로 좁게 보호하는 게 현실적이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 자주 만나는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 트랜잭션이 커밋했는데 조회가 안 돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RR의 스냅샷 고정 &amp;mdash; 정상이에요. 최신을 봐야 하면 트랜잭션을 새로 시작하거나, 그 로직만 락 읽기로 가져가요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언두 로그(또는 ibdata)가 계속 커져요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장수 트랜잭션을 찾아요. &lt;code&gt;information_schema.innodb_trx&lt;/code&gt;에서 오래 열린 트랜잭션을 확인하고, 배치&amp;middot;관리 쿼리가 트랜잭션을 안 닫고 있는 경우가 단골이에요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 조건 SELECT인데 두 번째에 행이 늘었어요 (RC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RC에서는 정상이에요(팬텀&amp;middot;반복불가 허용). 트랜잭션 안 일관성이 필요하면 RR로, 특정 구간만이면 락으로 풀어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RR을 그대로 쓰되, 셋만 기억하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리수준은 이상 현상과 동시성을 맞바꾸는 거래이고, MySQL 기본 RR은 첫 SELECT에 스냅샷을 고정해 트랜잭션 내 일관성을 줘요. 그걸 락 없이 가능하게 하는 게 언두 로그 기반 MVCC고요. 실무에서 들고 갈 건 세 줄이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;격리수준은&lt;/b&gt; &amp;rarr; RR 기본을 그대로. 낮출 땐 &quot;왜&quot;가 있을 때만.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션은&lt;/b&gt; &amp;rarr; 짧게. 장수 트랜잭션이 언두 로그를 키워요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;갱신 분실은&lt;/b&gt; &amp;rarr; 격리수준이 아니라 &lt;a href=&quot;https://jessyt.tistory.com/485&quot;&gt;락&lt;/a&gt;으로. MVCC는 읽기 일관성만 줘요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 &quot;락&quot;이 바로 다음 주제예요. RR의 팬텀 방지를 떠받치는 레코드 락&amp;middot;갭 락&amp;middot;넥스트키 락의 실체로 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html&quot;&gt;MySQL &amp;mdash; Transaction Isolation Levels&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html&quot;&gt;MySQL &amp;mdash; InnoDB Multi-Versioning&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/데이터 &amp;amp; DB</category>
      <category>innodb</category>
      <category>MVCC</category>
      <category>MYSQL</category>
      <category>REPEATABLE READ</category>
      <category>격리수준</category>
      <category>백엔드</category>
      <category>언두 로그</category>
      <category>팬텀 리드</category>
      <author>에디개발자</author>
      <guid isPermaLink="true">https://jessyt.tistory.com/493</guid>
      <comments>https://jessyt.tistory.com/493#entry493comment</comments>
      <pubDate>Wed, 22 Jul 2026 07:30:46 +0900</pubDate>
    </item>
  </channel>
</rss>