<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <author>
    <name>어썸블로그</name>
  </author>
  <id>국내의 좋은 블로그 글들을 매일 배달해줍니다.</id>
  <title>개발자 어썸블로그</title>
  <updated>2026-09-03T14:50:05+09:00</updated>
  <entry>
    <author>
      <name>김재호</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;카카오 메일로 계속 스팸메일이 옵니다.&lt;br&gt;
이메일 주소를 바꿔가면서.&lt;br&gt;
제목은 처음에는 &lt;code class="language-plaintext highlighter-rouge"&gt;(광고)&lt;/code&gt;였는데 스팸필터에 추가했더니 &lt;code class="language-plaintext highlighter-rouge"&gt;(광.고)&lt;/code&gt;로, 또 &lt;code class="language-plaintext highlighter-rouge"&gt;(광고.)&lt;/code&gt;로, 또 &lt;code class="language-plaintext highlighter-rouge"&gt;(광 .고 )&lt;/code&gt; 로.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://jeho.page/assets/img/kakao-spam.jpg" alt=""&gt;&lt;/p&gt;

&lt;p&gt;더 짜증나는 것은 이 메일들이 스팸메일함이 아닌 Inbox 로 들어와서 알림이 온다는 것.&lt;br&gt;
불필요한 알림 하나라도 줄이기 위해서 애를 쓰고있는데… 😂&lt;/p&gt;

&lt;p&gt;메일 하단의 발송자 정보에는 &lt;a href="https://namu.wiki/w/Namepr"&gt;네임피알&lt;/a&gt;이라는 이름이 적혀 있습니다.&lt;br&gt;
이 업체나 광고주에게 광고 메일 수신을 동의한 적은 당연히 없습니다.&lt;br&gt;
법인이 실제로 존재하지도 않는 유령회사인 것 같더군요.&lt;/p&gt;

&lt;p&gt;이런 식으로 메일을 보내는 업체가 얄미운 것은 당연한데, 메일 서비스를 운영하는 카카오도 아쉽습니다.&lt;br&gt;
&lt;a href="https://mail.kakao.com/policy?lang=ko"&gt;카카오메일의 광고 발송 안내&lt;/a&gt;에는 &lt;code class="language-plaintext highlighter-rouge"&gt;(광.고)&lt;/code&gt;를 쓰면 안 된다고 적혀 있습니다.&lt;br&gt;
&lt;img src="https://jeho.page/assets/img/kakao-mail-policy.png" alt=""&gt;&lt;/p&gt;

&lt;p&gt;떡하니 안 된다고 써놓은 바로 그 제목이 스팸메일함도 아닌 받은편지함까지 들어오는 겁니다.&lt;/p&gt;

&lt;p&gt;사용자가 할 수 있는 일은 스팸으로 신고하고, 바뀐 주소와 제목을 그때그때 차단 목록에 보태는 정도.&lt;br&gt;
그러면 상대는 점 하나를 더 찍습니다. 😂&lt;/p&gt;

&lt;p&gt;사용자들이 피곤해하는데 너무 방치해두는 것 같습니다. 서비스 제공자가 이 정도는 해줘야 하는 것 아닐까?&lt;br&gt;
자기 정책에 금지 사례로 박아둔 패턴 정도는 사용자가 신고하기 전에 서비스가 먼저 막아주면 좋겠습니다.&lt;/p&gt;

&lt;div class="section-divider"&gt;&lt;span class="ornament"&gt;✔️&lt;/span&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;함께 읽으면 좋은 글:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://jeho.page/essay/2022/05/18/email-spam.html"&gt;스팸 문자 좀 그만보내요&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://jeho.page/essay/2022/05/02/kakao-ten-years.html"&gt;완성되지 않은 회사&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://jeho.page/essay/2026/09/03/kakao-mail.html</id>
    <link href="https://jeho.page/essay/2026/09/03/kakao-mail.html"/>
    <summary type="html">카카오 메일로 계속 스팸메일이 옵니다. 이메일 주소를 바꿔가면서. 제목은 처음에는 (광고)였는데 스팸필터에 추가했더니 (광.고)로, 또 (광고.)로, 또 (광 .고 ) 로.</summary>
    <title>카카오 메일 유감</title>
    <updated>2026-09-03T14:50:05+09:00</updated>
    <dc:date>2026-09-03T14:50:05+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>Outsider</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h2&gt;웹개발 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.solidjs.com/blog/async-solid-fetch-high-block-low"&gt;Async Solid: Fetch High, Block Low&lt;/a&gt;&lt;/strong&gt; : 곧 릴리스될 Solid 2.0이 비동기 처리를 다루는 방식을 설명하는 첫 번째 글이다. 비동기 데이터를 다룰 때는 보통 상위 컴포넌트가 렌더링된 뒤 네트워크 요청이 시작된다. Solid는 컴포넌트 트리 상단에서 fetch를 시작해(Fetch High) UI를 구성하는 동안 네트워크 요청을 처리하고, 데이터가 필요한 UI까지 대기를 최대한 늦출 수 있도록 지원한다(Block Low). (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://hacks.mozilla.org/2026/08/intent-to-ship-jpeg-xl/"&gt;Intent to Ship: JPEG XL&lt;/a&gt;&lt;/strong&gt; : 2020년대 새로운 이미지 포맷으로 AVIF와 JPEG XL이 있다. Firefox는 2021년에 AVIF를 지원했고 이제 JPEG XL 출시 계획을 발표했다. JPEG XL은 프로그레시브 렌더링이 가능해 전체를 로드하지 않아도 이미지를 어느 정도 보여 줄 수 있고, 화질 손실 없이 JPEG를 압축할 수 있다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openai.com/index/asana/"&gt;Asana cleared 5 years of engineering work in 2 weeks with Codex&lt;/a&gt;&lt;/strong&gt; : 이슈 관리 플랫폼인 Asana가 오래된 테스트 도구 Enzyme을 스택에서 걷어내는 데 Codex를 사용해 2주 만에 끝냈다. 모델 및 인프라는 약 12,000달러가 들었고 기존 계획에서는 최소 5년은 걸릴 것으로 예상했다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;그 밖의 개발 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.com/UberEng/article/2093444169037762840"&gt;Running a Software Factory Efficiently at Uber Scale&lt;/a&gt;&lt;/strong&gt; : Uber에서 AI 코딩 비용을 엔지니어링으로 해결해서 사용량을 7배 늘리면서 단위 비용은 절감하고 출력 품질은 유지했다. 비용 구조를 파악하고 측정 방법을 만들 뒤에 기본 모델을 선택하고 토큰/요청을 최적화했다. 프롬프트를 캐싱하고 MCP 도구를 CLI로 이용했으며, Code Mode로 토큰 사용을 최적화하고 내부 정보를 쉽게 찾을 수 있도록 AI 컨텍스트 그래프를 개발했다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cursor.com/ko/changelog/origin-code-hosting"&gt;Origin 코드 호스팅&lt;/a&gt;&lt;/strong&gt; : 기존에 예고한 대로 Cursor가 코드 호스팅 기능인 Origin을 유료 플랜 사용자에게 베타로 순차 출시한다고 발표했다.(한국어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.modular.com/blog/mojo-open-source"&gt;Mojo🔥 is now open source!&lt;/a&gt;&lt;/strong&gt; : Python 언어와 생태계를 시스템 프로그래밍 및 메타 프로그래밍과 결합해 연구와 운영 간의 격차를 해소하는 프로그래밍 언어 Mojo가 &lt;a href="https://github.com/modular/modular/tree/main/mojo"&gt;오픈소스로 공개&lt;/a&gt;됐다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://engineering.block.xyz/blog/projects-in-buzz"&gt;Projects in Buzz&lt;/a&gt;&lt;/strong&gt; : Block에서 사람과 AI 에이전트가 함께 활동할 수 있는 채팅 앱으로 공개한 &lt;a href="https://buzz.xyz/"&gt;Buzz&lt;/a&gt;의 실험 기능인 Projects를 소개한다. 프로젝트에서 Git 저장소를 제공하고 Pull Request와 이슈를 관리할 수 있으며, 에이전트에게 이런 작업을 맡기고 어떤 활동이 있었는지 볼 수 있다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://protobuf.com/"&gt;Protobuf Explained&lt;/a&gt;&lt;/strong&gt; : Protocol Buffers를 자세히 설명하는 사이트다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://z.ai/blog/glm-5.3"&gt;GLM-5.3: Frontier Coding with Emergent Cyber Capabilities&lt;/a&gt;&lt;/strong&gt; : Z.ai에서 GLM-5.3을 출시했다. GLM-5.3은 GLM-5.2와 같은 기본 모델을 사용하지만, 사후 학습만으로 성능을 높였고 5.2보다 복잡한 코딩과 장기 작업에서 훨씬 뛰어난 성능을 보인다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://qwen.ai/blog?id=qwen3.8-flash-next"&gt;Qwen3.8-Flash-Next: A New Architecture, Towards Ultimate Cost-Efficiency&lt;/a&gt;&lt;/strong&gt; : Alibaba에서 Qwen3.8-Flash-Next 모델과 가중치를 공개했다. Qwen3.8-Flash-Next는 Qwen4에 적용될 아키텍처를 사용한 멀티모달 MoE 모델로 어텐션, 잔차, 임베딩, 최적화 부분에서 개선했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://z.ai/blog/glm-5.3-flash"&gt;GLM-5.3-Flash: Frontier Intelligence, Flash Cost&lt;/a&gt;&lt;/strong&gt; : Z.ai에서 GLM-5 시리즈 최초의 네이티브 멀티모달 모델 GLM-5.3-Flash를 공개했다. GLM-5.3-Flash는 공개 전 &lt;a href="https://techcrunch.com/2026/08/23/whos-behind-the-new-stealth-model-ox-alpha/"&gt;OpenCode와 OpenRouter에서 Ox Alpha라는 이름으로 익명 제공&lt;/a&gt;되어 피드백을 수집했고, 모든 트래픽을 중국산 AI 칩으로 처리했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-5-transcribe/"&gt;Intelligent transcription with Gemini 3.5 Transcribe&lt;/a&gt;&lt;/strong&gt; : Google에서 음성-텍스트 변환 모델인 Gemini 3.5 Transcribe를 발표했다. 실시간 스트리밍과 미리 녹음된 오디오를 처리하는 API 두 가지를 제공한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.tencent.com/tencent-releases-and-open-sources-tencent-hy4-preview/"&gt;Tencent Releases and Open-Sources Tencent Hy4 preview&lt;/a&gt;&lt;/strong&gt; : Tencent가 차세대 LLM인 Hy4 프리뷰를 공개했다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인프라 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cursor.com/ko/blog/git-at-any-scale"&gt;모든 규모의 Git&lt;/a&gt;&lt;/strong&gt; : Git은 운영하기 쉬워 보일 수 있지만, 분산형이라는 특성 때문에 대규모 운영은 쉽지 않다. Git은 서버에서는 어떻게든 운영할 수 있지만, 네트워크로 주고받을 때는 packfile을 사용해야 한다. 이전에 분산 해시 테이블로 최적화하는 JGit의 방법도 packfile의 성능 한계로 사용할 수 없었다. GitHub도 처음에는 분산 파일 시스템을 사용했지만 곧 한계에 이르렀다. 2013년에 공개한 &lt;a href="https://github.blog/engineering/infrastructure/stretching-spokes/"&gt;Spokes&lt;/a&gt;는 데이터를 NVMe 디스크에 저장하고 Git 자체를 분산하지 않은 채 복제본을 만들어 3단계 커밋으로 동기화한다. 오랫동안 잘 사용됐지만, 현재는 다시 한계에 부딪히고 있다. Cursor에서는 Spokes에서 배워서 Continuity라는 Git 스토리지 시스템을 만들었다. Continuity는 S3 호환 객체 스토리지에 WAL을 기록해 Push를 선형적으로 처리한다. Spokes처럼 로컬 디스크에 NVMe를 쓰지만, Spokes와 달리 리포지토리를 추적하거나 합의할 필요가 없고 S3를 이용해 무한히 복제할 수 있다. (한국어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.fnil.net/s3-at-twenty/"&gt;Twenty Years of S3: How It Became the Default Persistence Layer for Modern Data Infrastructure&lt;/a&gt;&lt;/strong&gt; : 오브젝트 스토리지 기반 옵저버빌리티 데이터베이스인 GreptimeDB를 만든 Dennis Zhuang이 S3의 발전 과정을 정리한 글이다. 2006년에 등장한 S3는 계속 기능을 추가해 왔다. 2020년에는 강한 일관성을 도입했고, 2023년에는 Express One Zone 지원과 Mountpoint for S3로 지연 시간 문제도 해결했다. 다만 NVMe와는 여전히 거리가 멀었다.  2024년에는 조건부 쓰기가 도입돼 원자적 직렬화를 수행할 수 있게 됐고, Cursor의 Continuity도 이를 사용한다. 20년 동안 S3가 디스크만큼 빨라진 것은 아니지만, 데이터 인프라 시스템이 S3를 source of truth로 사용할 만큼 발전했다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://valkey.io/blog/what-is-valkey-benchmark/"&gt;Simulating Real Workloads with valkey-benchmark&lt;/a&gt;&lt;/strong&gt; : Valkey는 사용자 환경에서 여러 클라이언트가 명령을 전송하는 상황을 시뮬레이션해 성능을 테스트할 수 있는 valkey-benchmark CLI를 제공한다. Valkey 자체의 속도보다 서버가 부하 상태에서 어떻게 작동하는지 확인하는 데 쓰는 도구다. (영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.cncf.io/announcements/2026/08/17/cncf-announces-kubeflows-graduation-solidifying-the-standard-for-cloud-native-ai-operations/"&gt;CNCF Announces Kubeflow’s Graduation, Solidifying a Standard for Cloud Native AI Operations&lt;/a&gt;&lt;/strong&gt; : AI와 머신러닝 운영을 위한 Kubernetes 플랫폼 Kubeflow가 CNCF 졸업 프로젝트가 됐다. (영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;보안 및 장애&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.githubstatus.com/incidents/zkxwbgr0cnmx"&gt;Incident with GitHub.com&lt;/a&gt;&lt;/strong&gt; : UTC 기준 지난 17일에 7시간 47분간 발생한 GitHub 장애 보고서다. 트래픽이 급증해 Istio 사이드카 Pod의 동시 실행 제한에 도달했지만, 사이드카를 모니터링하지 않도록 정책이 잘못 설정돼 오토스케일링이 작동하지 않았다. 트래픽을 버티지 못해 장애가 확산되면서 HAProxy 노드 4대도 제한을 초과해 지연과 오류가 발생했다. 여기에 낙관적 재시도 로직이 내부 로드 밸런서에 과부하를 걸어 상황이 더 악화됐다. HAProxy를 중지시켜 문제를 복구했다. (영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;볼만한 링크&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://newsletter.eng-leadership.com/p/good-culture-is-the-biggest-productivity"&gt;Good Culture is the Biggest Productivity Hack, Not AI&lt;/a&gt;&lt;/strong&gt; : AI로 생산성이 올라가면서 다들 AI 도구에 집중하지만 그 도구가 놓인 환경에는 관심을 기울이지 않는다. AI 도구보다 중요한 것은 훌륭한 문화다. 생산성을 높이는 가장 좋은 방법은 훌륭한 기업 문화이고, 문화가 갖춰지지 않으면 아무것도 제대로 돌아가지 않는다. 많은 회사가 생산성이 10배 올랐다고 말하면 C레벨은 FOMO를 느끼지만, AI는 오히려 훌륭한 문화를 더 가치 있게 만들므로 좋은 문화를 구축하는 데 노력해야 한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lucumr.pocoo.org/2026/8/24/anger-anxiety-agency/"&gt;Anger, Anxiety and Agency&lt;/a&gt;&lt;/strong&gt; : AI로 많은 변화가 일어나는 상황에서 혼란과 불안을 느끼는 건 당연하지만, 분노는 어딘가로 향하기 때문에 좋지 않다. 이때 분노는 아무런 도움이 안 되고, 불확실성을 호기심으로 연결하면 호기심을 넘어 흥분까지 느낄 수 있다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IT 업계 뉴스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://omarchy.org/news/2026/08/omacom-foundation-launches-with-8-million/"&gt;Omacom Foundation launches with $8 million&lt;/a&gt;&lt;/strong&gt; : Rails의 DHH가 만든 Omarchy 리눅스를 관리하기 위한 Omacom 재단이 설립됐다. 여기에는 많은 유명 회사의 CEO나 창업자들이 천만 달러를 펀딩했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openrouter.ai/blog/announcements/openrouter-is-joining-stripe/"&gt;OpenRouter is Joining Stripe&lt;/a&gt;&lt;/strong&gt; : 여러 AI 모델을 쉽게 사용할 수 있게 하는 &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt;를 결제 플랫폼인 &lt;a href="https://stripe.com/"&gt;stripe&lt;/a&gt;가 인수했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/ko/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/"&gt;AWS and DuckLabs: Building the future of analytics together&lt;/a&gt;&lt;/strong&gt; : &lt;a href="https://duckdb.org/"&gt;DuckDB&lt;/a&gt;를 개발하는 &lt;a href="https://ducklabs.com/"&gt;DuckLabs&lt;/a&gt;를 AWS가 인수하기로 했다. DuckDB는 DuckLabs에서 계속 주도하고 독립 재단하에서 계속 오픈소스로 유지될 예정이다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://techcrunch.com/2026/08/26/nvidia-closes-in-on-hugging-face-acquisition/"&gt;Nvidia closes in on Hugging Face acquisition&lt;/a&gt;&lt;/strong&gt; : NVidia가 AI 모델 플랫폼 회사인 Hugging Face를 129억 달러에 인수하기로 합의했다고 알려졌다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://techcrunch.com/2026/08/17/anthropics-annualized-revenue-surges-to-65b/"&gt;Anthropic’s annualized revenue surges to $65B&lt;/a&gt;&lt;/strong&gt; : 블룸버그가 7월 말 기준 Anthropic의 연간 환산 매출을 추정한 결과 650억 달러를 넘었다고 보도했다.(5월에는 470억 달러)(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;프로젝트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://node-modules.dev/"&gt;Node Modules Inspector&lt;/a&gt;&lt;/strong&gt; : &lt;code&gt;node_modules&lt;/code&gt;의 시각화 및 분석 사이트.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://omadium.app/"&gt;omadium&lt;/a&gt;&lt;/strong&gt; : Omarchy 리눅스를 회사의 컴플라이언스 요구사항에 맞추기 위한 프로젝트.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/tobi/walgit"&gt;walgit&lt;/a&gt;&lt;/strong&gt; : 앞의 Cursor &lt;a href="https://cursor.com/ko/blog/git-at-any-scale"&gt;Git 접근법 글&lt;/a&gt;을 보고 Shopify CEO인 Tobi가 오픈소스로 구현해 본 저장소&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dactyl.dev/"&gt;Dactyl&lt;/a&gt;&lt;/strong&gt; : Deno를 만드는 Denoland의 사람들이 Android와 iOS용 앱을 위한 바이브 코딩 플랫폼 서비스를 공개했다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://berd.xyz/"&gt;Berd&lt;/a&gt;&lt;/strong&gt; : Block이 내부에서 AI 에이전트를 단일 환경에서 쓸 수 있도록 만든 데스크톱 애플리케이션을 &lt;a href="https://github.com/block/berd"&gt;오픈소스로 공개&lt;/a&gt;했다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tapes.dev/"&gt;tapes&lt;/a&gt;&lt;/strong&gt; : 에이전트 세션을 로컬 Postgres에 기록하는 CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://fx.sh/"&gt;fx&lt;/a&gt;&lt;/strong&gt; : Vercel에서 만든 코딩 에이전트&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://esm.unpkg.com/"&gt;UNPKG ESM&lt;/a&gt;&lt;/strong&gt; : npm용 CDN인 unpkg에서 브라우저를 지원하는 ESM 모듈을 베타로 지원한다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://theolundqvist.github.io/pr-cockpit/"&gt;PR Cockpit&lt;/a&gt;&lt;/strong&gt; : GitHub Pull Request를 로컬에서 빠르게 리뷰할 수 있게 하는 macOS 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;버전 업데이트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; v4.0.0&lt;/strong&gt; : HTML 확장 라이브러리, &lt;a href="https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nestjs.com/"&gt;nest.js&lt;/a&gt; v12.0&lt;/strong&gt; : Node.js 웹 프레임워크, &lt;a href="https://trilon.io/blog/nestjs-12-is-now-available"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pnpm.js.org/"&gt;pnpm&lt;/a&gt; v12.0.0&lt;/strong&gt; : Node.js 패키지 매니저, &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; v44.0.0&lt;/strong&gt; : 크로스 플랫폼 데스크톱 애플리케이션 플랫폼, &lt;a href="https://www.electronjs.org/blog/electron-44-0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt; v1.4.0&lt;/strong&gt; : JavaScript/TypeScript 런타임, &lt;a href="https://bun.com/blog/bun-v1.4"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.crossplane.io/"&gt;Crossplane&lt;/a&gt; v2.4.0&lt;/strong&gt; : 외부 인프라 관리용 Kubernetes 애드온, &lt;a href="https://github.com/crossplane/crossplane/releases/tag/v2.4.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://go.dev/"&gt;Go&lt;/a&gt; v1.27.0&lt;/strong&gt; : 프로그래밍 언어, &lt;a href="https://go.dev/doc/go1.27"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://better-auth.com/"&gt;Better Auth&lt;/a&gt; 1.7&lt;/strong&gt; : 인증 프레임워크, &lt;a href="https://better-auth.com/blog/1-7"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://grafana.com/"&gt;Grafana&lt;/a&gt; v13.2.0&lt;/strong&gt; : 메트릭 대시보드, &lt;a href="https://grafana.com/blog/grafana-13-2-release-all-the-latest-features/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nodejs.org/en"&gt;Node.js&lt;/a&gt; v26.8.0 (Current)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v26.8.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nodejs.org/en"&gt;Node.js&lt;/a&gt; v24.20.0 (LTS)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v24.20.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://nativescript.org/"&gt;NativeScript&lt;/a&gt; v9.1.0&lt;/strong&gt; : JavaScript 모바일 프레임워크, &lt;a href="https://blog.nativescript.org/nativescript-9-1-announcement/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://celld.dev/"&gt;celld&lt;/a&gt; v0.4.0&lt;/strong&gt; : 직접 운영할 수 있는 분산 듀러블 오브젝트, &lt;a href="https://github.com/denoland/celld/releases/tag/v0.4.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zod.dev/"&gt;Zod&lt;/a&gt; v4.5&lt;/strong&gt; : TypeScript 스키마 유효성 검사 라이브러리, &lt;a href="https://zod.dev/blog/zod-4-5"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zed.dev/"&gt;Zed&lt;/a&gt; v1.17&lt;/strong&gt; : 코드 에디터, &lt;a href="https://zed.dev/releases/stable/1.17.2"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://kubernetes.io/"&gt;Kubernetes&lt;/a&gt; v1.37 Garhwal&lt;/strong&gt; : 컨테이너 오케스트레이션 도구, &lt;a href="https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com/"&gt;ollama&lt;/a&gt; v0.33&lt;/strong&gt; : 오픈 모델을 로컬에서 실행하는 도구, &lt;a href="https://github.com/ollama/ollama/releases/tag/v0.33.2"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.rspack.dev/"&gt;Rspack&lt;/a&gt; v2.2&lt;/strong&gt; : 웹 번들러, &lt;a href="https://github.com/web-infra-dev/rspack/releases/tag/v2.2.1"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.graalvm.org/"&gt;GraalVM&lt;/a&gt; v25.3&lt;/strong&gt; : 통합 가상 머신, &lt;a href="https://www.graalvm.org/release-notes/25.3/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tinybase.org/"&gt;TinyBase&lt;/a&gt; v9.6.0&lt;/strong&gt; : 로컬 우선 앱을 위한 리액티브 데이터 스토어, &lt;a href="https://tinybase.org/guides/releases/#v9-6"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://blog.outsider.ne.kr/1811</id>
    <link href="https://blog.outsider.ne.kr/1811"/>
    <summary type="html">&lt;h2&gt;웹개발 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.solidjs.com/blog/async-solid-fetch-high-block-low"&gt;Async Solid: Fetch High, Block Low&lt;/a&gt;&lt;/strong&gt; : 곧 릴리스될 Solid 2.0이 비동기 처리를 다루는 방식을 설명하는 첫 번째 글이다. 비동기 데이터를 다룰 때는 보통 상위 컴포넌트가 렌더링된 뒤 네트워크 요청이 시작된다. Solid는 컴포넌트 트리 상단에서 fetch를 시작해(Fetch High) UI를 구성하는 동안 네트워크 요청을 처리하고, 데이터가 필요한 UI까지 대기를 최대한 늦출 수 있도록 지원한다(Block Low). (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://hacks.mozilla.org/2026/08/intent-to-ship-jpeg-xl/"&gt;Intent to Ship: JPEG XL&lt;/a&gt;&lt;/strong&gt; : 2020년대 새로운 이미지 포맷으로 AVIF와 JPEG XL이 있다. Firefox는 2021년에 AVIF를 지원했고 이제 JPEG XL 출시 계획을 발표했다. JPEG XL은 프로그레시브 렌더링이 가능해 전체를 로드하지 않아도 이미지를 어느 정도 보여 줄 수 있고, 화질 손실 없이 JPEG를 압축할 수 있다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://openai.com/index/asana/"&gt;Asana cleared 5 years of engineering work in 2 weeks with Codex&lt;/a&gt;&lt;/strong&gt; : 이슈 관리 플랫폼인 Asana가 오래된 테스트 도구 Enzyme을 스택에서 걷어내는 데 Codex를 사용해 2주 만에 끝냈다. 모델 및 인프라는 약 12,000달러가 들었고 기존 계획에서는 최소 5년은 걸릴 것으로 예상했다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;그 밖의 개발 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/UberEng/article/2093444169037762840"&gt;Running a Software Factory Efficiently at Uber Scale&lt;/a&gt;&lt;/strong&gt; : Uber에서 AI 코딩 비용을 엔지니어링으로 해결해서 사용량을 7배 늘리면서 단위 비용은 절감하고 출력 품질은 유지했다. 비용 구조를 파악하고 측정 방법을 만들 뒤에 기본 모델을 선택하고 토큰/요청을 최적화했다. 프롬프트를 캐싱하고 MCP 도구를 CLI로 이용했으며, Code Mode로 토큰 사용을 최적화하고 내부 정보를 쉽게 찾을 수 있도록 AI 컨텍스트 그래프를 개발했다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://cursor.com/ko/changelog/origin-code-hosting"&gt;Origin 코드 호스팅&lt;/a&gt;&lt;/strong&gt; : 기존에 예고한 대로 Cursor가 코드 호스팅 기능인 Origin을 유료 플랜 사용자에게 베타로 순차 출시한다고 발표했다.(한국어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.modular.com/blog/mojo-open-source"&gt;Mojo🔥 is now open source!&lt;/a&gt;&lt;/strong&gt; : Python 언어와 생태계를 시스템 프로그래밍 및 메타 프로그래밍과 결합해 연구와 운영 간의 격차를 해소하는 프로그래밍 언어 Mojo가 &lt;a href="https://github.com/modular/modular/tree/main/mojo"&gt;오픈소스로 공개&lt;/a&gt;됐다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://engineering.block.xyz/blog/projects-in-buzz"&gt;Projects in Buzz&lt;/a&gt;&lt;/strong&gt; : Block에서 사람과 AI 에이전트가 함께 활동할 수 있는 채팅 앱으로 공개한 &lt;a href="https://buzz.xyz/"&gt;Buzz&lt;/a&gt;의 실험 기능인 Projects를 소개한다. 프로젝트에서 Git 저장소를 제공하고 Pull Request와 이슈를 관리할 수 있으며, 에이전트에게 이런 작업을 맡기고 어떤 활동이 있었는지 볼 수 있다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://protobuf.com/"&gt;Protobuf Explained&lt;/a&gt;&lt;/strong&gt; : Protocol Buffers를 자세히 설명하는 사이트다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://z.ai/blog/glm-5.3"&gt;GLM-5.3: Frontier Coding with Emergent Cyber Capabilities&lt;/a&gt;&lt;/strong&gt; : Z.ai에서 GLM-5.3을 출시했다. GLM-5.3은 GLM-5.2와 같은 기본 모델을 사용하지만, 사후 학습만으로 성능을 높였고 5.2보다 복잡한 코딩과 장기 작업에서 훨씬 뛰어난 성능을 보인다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://qwen.ai/blog?id=qwen3.8-flash-next"&gt;Qwen3.8-Flash-Next: A New Architecture, Towards Ultimate Cost-Efficiency&lt;/a&gt;&lt;/strong&gt; : Alibaba에서 Qwen3.8-Flash-Next 모델과 가중치를 공개했다. Qwen3.8-Flash-Next는 Qwen4에 적용될 아키텍처를 사용한 멀티모달 MoE 모델로 어텐션, 잔차, 임베딩, 최적화 부분에서 개선했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://z.ai/blog/glm-5.3-flash"&gt;GLM-5.3-Flash: Frontier Intelligence, Flash Cost&lt;/a&gt;&lt;/strong&gt; : Z.ai에서 GLM-5 시리즈 최초의 네이티브 멀티모달 모델 GLM-5.3-Flash를 공개했다. GLM-5.3-Flash는 공개 전 &lt;a href="https://techcrunch.com/2026/08/23/whos-behind-the-new-stealth-model-ox-alpha/"&gt;OpenCode와 OpenRouter에서 Ox Alpha라는 이름으로 익명 제공&lt;/a&gt;되어 피드백을 수집했고, 모든 트래픽을 중국산 AI 칩으로 처리했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-5-transcribe/"&gt;Intelligent transcription with Gemini 3.5 Transcribe&lt;/a&gt;&lt;/strong&gt; : Google에서 음성-텍스트 변환 모델인 Gemini 3.5 Transcribe를 발표했다. 실시간 스트리밍과 미리 녹음된 오디오를 처리하는 API 두 가지를 제공한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.tencent.com/tencent-releases-and-open-sources-tencent-hy4-preview/"&gt;Tencent Releases and Open-Sources Tencent Hy4 preview&lt;/a&gt;&lt;/strong&gt; : Tencent가 차세대 LLM인 Hy4 프리뷰를 공개했다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인프라 관련&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://cursor.com/ko/blog/git-at-any-scale"&gt;모든 규모의 Git&lt;/a&gt;&lt;/strong&gt; : Git은 운영하기 쉬워 보일 수 있지만, 분산형이라는 특성 때문에 대규모 운영은 쉽지 않다. Git은 서버에서는 어떻게든 운영할 수 있지만, 네트워크로 주고받을 때는 packfile을 사용해야 한다. 이전에 분산 해시 테이블로 최적화하는 JGit의 방법도 packfile의 성능 한계로 사용할 수 없었다. GitHub도 처음에는 분산 파일 시스템을 사용했지만 곧 한계에 이르렀다. 2013년에 공개한 &lt;a href="https://github.blog/engineering/infrastructure/stretching-spokes/"&gt;Spokes&lt;/a&gt;는 데이터를 NVMe 디스크에 저장하고 Git 자체를 분산하지 않은 채 복제본을 만들어 3단계 커밋으로 동기화한다. 오랫동안 잘 사용됐지만, 현재는 다시 한계에 부딪히고 있다. Cursor에서는 Spokes에서 배워서 Continuity라는 Git 스토리지 시스템을 만들었다. Continuity는 S3 호환 객체 스토리지에 WAL을 기록해 Push를 선형적으로 처리한다. Spokes처럼 로컬 디스크에 NVMe를 쓰지만, Spokes와 달리 리포지토리를 추적하거나 합의할 필요가 없고 S3를 이용해 무한히 복제할 수 있다. (한국어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://blog.fnil.net/s3-at-twenty/"&gt;Twenty Years of S3: How It Became the Default Persistence Layer for Modern Data Infrastructure&lt;/a&gt;&lt;/strong&gt; : 오브젝트 스토리지 기반 옵저버빌리티 데이터베이스인 GreptimeDB를 만든 Dennis Zhuang이 S3의 발전 과정을 정리한 글이다. 2006년에 등장한 S3는 계속 기능을 추가해 왔다. 2020년에는 강한 일관성을 도입했고, 2023년에는 Express One Zone 지원과 Mountpoint for S3로 지연 시간 문제도 해결했다. 다만 NVMe와는 여전히 거리가 멀었다.  2024년에는 조건부 쓰기가 도입돼 원자적 직렬화를 수행할 수 있게 됐고, Cursor의 Continuity도 이를 사용한다. 20년 동안 S3가 디스크만큼 빨라진 것은 아니지만, 데이터 인프라 시스템이 S3를 source of truth로 사용할 만큼 발전했다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://valkey.io/blog/what-is-valkey-benchmark/"&gt;Simulating Real Workloads with valkey-benchmark&lt;/a&gt;&lt;/strong&gt; : Valkey는 사용자 환경에서 여러 클라이언트가 명령을 전송하는 상황을 시뮬레이션해 성능을 테스트할 수 있는 valkey-benchmark CLI를 제공한다. Valkey 자체의 속도보다 서버가 부하 상태에서 어떻게 작동하는지 확인하는 데 쓰는 도구다. (영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.cncf.io/announcements/2026/08/17/cncf-announces-kubeflows-graduation-solidifying-the-standard-for-cloud-native-ai-operations/"&gt;CNCF Announces Kubeflow’s Graduation, Solidifying a Standard for Cloud Native AI Operations&lt;/a&gt;&lt;/strong&gt; : AI와 머신러닝 운영을 위한 Kubernetes 플랫폼 Kubeflow가 CNCF 졸업 프로젝트가 됐다. (영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;보안 및 장애&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.githubstatus.com/incidents/zkxwbgr0cnmx"&gt;Incident with GitHub.com&lt;/a&gt;&lt;/strong&gt; : UTC 기준 지난 17일에 7시간 47분간 발생한 GitHub 장애 보고서다. 트래픽이 급증해 Istio 사이드카 Pod의 동시 실행 제한에 도달했지만, 사이드카를 모니터링하지 않도록 정책이 잘못 설정돼 오토스케일링이 작동하지 않았다. 트래픽을 버티지 못해 장애가 확산되면서 HAProxy 노드 4대도 제한을 초과해 지연과 오류가 발생했다. 여기에 낙관적 재시도 로직이 내부 로드 밸런서에 과부하를 걸어 상황이 더 악화됐다. HAProxy를 중지시켜 문제를 복구했다. (영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;볼만한 링크&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://newsletter.eng-leadership.com/p/good-culture-is-the-biggest-productivity"&gt;Good Culture is the Biggest Productivity Hack, Not AI&lt;/a&gt;&lt;/strong&gt; : AI로 생산성이 올라가면서 다들 AI 도구에 집중하지만 그 도구가 놓인 환경에는 관심을 기울이지 않는다. AI 도구보다 중요한 것은 훌륭한 문화다. 생산성을 높이는 가장 좋은 방법은 훌륭한 기업 문화이고, 문화가 갖춰지지 않으면 아무것도 제대로 돌아가지 않는다. 많은 회사가 생산성이 10배 올랐다고 말하면 C레벨은 FOMO를 느끼지만, AI는 오히려 훌륭한 문화를 더 가치 있게 만들므로 좋은 문화를 구축하는 데 노력해야 한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://lucumr.pocoo.org/2026/8/24/anger-anxiety-agency/"&gt;Anger, Anxiety and Agency&lt;/a&gt;&lt;/strong&gt; : AI로 많은 변화가 일어나는 상황에서 혼란과 불안을 느끼는 건 당연하지만, 분노는 어딘가로 향하기 때문에 좋지 않다. 이때 분노는 아무런 도움이 안 되고, 불확실성을 호기심으로 연결하면 호기심을 넘어 흥분까지 느낄 수 있다.(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IT 업계 뉴스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://omarchy.org/news/2026/08/omacom-foundation-launches-with-8-million/"&gt;Omacom Foundation launches with $8 million&lt;/a&gt;&lt;/strong&gt; : Rails의 DHH가 만든 Omarchy 리눅스를 관리하기 위한 Omacom 재단이 설립됐다. 여기에는 많은 유명 회사의 CEO나 창업자들이 천만 달러를 펀딩했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://openrouter.ai/blog/announcements/openrouter-is-joining-stripe/"&gt;OpenRouter is Joining Stripe&lt;/a&gt;&lt;/strong&gt; : 여러 AI 모델을 쉽게 사용할 수 있게 하는 &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt;를 결제 플랫폼인 &lt;a href="https://stripe.com/"&gt;stripe&lt;/a&gt;가 인수했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://aws.amazon.com/ko/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/"&gt;AWS and DuckLabs: Building the future of analytics together&lt;/a&gt;&lt;/strong&gt; : &lt;a href="https://duckdb.org/"&gt;DuckDB&lt;/a&gt;를 개발하는 &lt;a href="https://ducklabs.com/"&gt;DuckLabs&lt;/a&gt;를 AWS가 인수하기로 했다. DuckDB는 DuckLabs에서 계속 주도하고 독립 재단하에서 계속 오픈소스로 유지될 예정이다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://techcrunch.com/2026/08/26/nvidia-closes-in-on-hugging-face-acquisition/"&gt;Nvidia closes in on Hugging Face acquisition&lt;/a&gt;&lt;/strong&gt; : NVidia가 AI 모델 플랫폼 회사인 Hugging Face를 129억 달러에 인수하기로 합의했다고 알려졌다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://techcrunch.com/2026/08/17/anthropics-annualized-revenue-surges-to-65b/"&gt;Anthropic’s annualized revenue surges to $65B&lt;/a&gt;&lt;/strong&gt; : 블룸버그가 7월 말 기준 Anthropic의 연간 환산 매출을 추정한 결과 650억 달러를 넘었다고 보도했다.(5월에는 470억 달러)(영어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;프로젝트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://node-modules.dev/"&gt;Node Modules Inspector&lt;/a&gt;&lt;/strong&gt; : &lt;code&gt;node_modules&lt;/code&gt;의 시각화 및 분석 사이트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://omadium.app/"&gt;omadium&lt;/a&gt;&lt;/strong&gt; : Omarchy 리눅스를 회사의 컴플라이언스 요구사항에 맞추기 위한 프로젝트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/tobi/walgit"&gt;walgit&lt;/a&gt;&lt;/strong&gt; : 앞의 Cursor &lt;a href="https://cursor.com/ko/blog/git-at-any-scale"&gt;Git 접근법 글&lt;/a&gt;을 보고 Shopify CEO인 Tobi가 오픈소스로 구현해 본 저장소&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dactyl.dev/"&gt;Dactyl&lt;/a&gt;&lt;/strong&gt; : Deno를 만드는 Denoland의 사람들이 Android와 iOS용 앱을 위한 바이브 코딩 플랫폼 서비스를 공개했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://berd.xyz/"&gt;Berd&lt;/a&gt;&lt;/strong&gt; : Block이 내부에서 AI 에이전트를 단일 환경에서 쓸 수 있도록 만든 데스크톱 애플리케이션을 &lt;a href="https://github.com/block/berd"&gt;오픈소스로 공개&lt;/a&gt;했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://tapes.dev/"&gt;tapes&lt;/a&gt;&lt;/strong&gt; : 에이전트 세션을 로컬 Postgres에 기록하는 CLI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://fx.sh/"&gt;fx&lt;/a&gt;&lt;/strong&gt; : Vercel에서 만든 코딩 에이전트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://esm.unpkg.com/"&gt;UNPKG ESM&lt;/a&gt;&lt;/strong&gt; : npm용 CDN인 unpkg에서 브라우저를 지원하는 ESM 모듈을 베타로 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://theolundqvist.github.io/pr-cockpit/"&gt;PR Cockpit&lt;/a&gt;&lt;/strong&gt; : GitHub Pull Request를 로컬에서 빠르게 리뷰할 수 있게 하는 macOS 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;버전 업데이트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; v4.0.0&lt;/strong&gt; : HTML 확장 라이브러리, &lt;a href="https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nestjs.com/"&gt;nest.js&lt;/a&gt; v12.0&lt;/strong&gt; : Node.js 웹 프레임워크, &lt;a href="https://trilon.io/blog/nestjs-12-is-now-available"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://pnpm.js.org/"&gt;pnpm&lt;/a&gt; v12.0.0&lt;/strong&gt; : Node.js 패키지 매니저, &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; v44.0.0&lt;/strong&gt; : 크로스 플랫폼 데스크톱 애플리케이션 플랫폼, &lt;a href="https://www.electronjs.org/blog/electron-44-0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt; v1.4.0&lt;/strong&gt; : JavaScript/TypeScript 런타임, &lt;a href="https://bun.com/blog/bun-v1.4"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.crossplane.io/"&gt;Crossplane&lt;/a&gt; v2.4.0&lt;/strong&gt; : 외부 인프라 관리용 Kubernetes 애드온, &lt;a href="https://github.com/crossplane/crossplane/releases/tag/v2.4.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://go.dev/"&gt;Go&lt;/a&gt; v1.27.0&lt;/strong&gt; : 프로그래밍 언어, &lt;a href="https://go.dev/doc/go1.27"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://better-auth.com/"&gt;Better Auth&lt;/a&gt; 1.7&lt;/strong&gt; : 인증 프레임워크, &lt;a href="https://better-auth.com/blog/1-7"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://grafana.com/"&gt;Grafana&lt;/a&gt; v13.2.0&lt;/strong&gt; : 메트릭 대시보드, &lt;a href="https://grafana.com/blog/grafana-13-2-release-all-the-latest-features/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nodejs.org/en"&gt;Node.js&lt;/a&gt; v26.8.0 (Current)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v26.8.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nodejs.org/en"&gt;Node.js&lt;/a&gt; v24.20.0 (LTS)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v24.20.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nativescript.org/"&gt;NativeScript&lt;/a&gt; v9.1.0&lt;/strong&gt; : JavaScript 모바일 프레임워크, &lt;a href="https://blog.nativescript.org/nativescript-9-1-announcement/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://celld.dev/"&gt;celld&lt;/a&gt; v0.4.0&lt;/strong&gt; : 직접 운영할 수 있는 분산 듀러블 오브젝트, &lt;a href="https://github.com/denoland/celld/releases/tag/v0.4.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://zod.dev/"&gt;Zod&lt;/a&gt; v4.5&lt;/strong&gt; : TypeScript 스키마 유효성 검사 라이브러리, &lt;a href="https://zod.dev/blog/zod-4-5"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://zed.dev/"&gt;Zed&lt;/a&gt; v1.17&lt;/strong&gt; : 코드 에디터, &lt;a href="https://zed.dev/releases/stable/1.17.2"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://kubernetes.io/"&gt;Kubernetes&lt;/a&gt; v1.37 Garhwal&lt;/strong&gt; : 컨테이너 오케스트레이션 도구, &lt;a href="https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://ollama.com/"&gt;ollama&lt;/a&gt; v0.33&lt;/strong&gt; : 오픈 모델을 로컬에서 실행하는 도구, &lt;a href="https://github.com/ollama/ollama/releases/tag/v0.33.2"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.rspack.dev/"&gt;Rspack&lt;/a&gt; v2.2&lt;/strong&gt; : 웹 번들러, &lt;a href="https://github.com/web-infra-dev/rspack/releases/tag/v2.2.1"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.graalvm.org/"&gt;GraalVM&lt;/a&gt; v25.3&lt;/strong&gt; : 통합 가상 머신, &lt;a href="https://www.graalvm.org/release-notes/25.3/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://tinybase.org/"&gt;TinyBase&lt;/a&gt; v9.6.0&lt;/strong&gt; : 로컬 우선 앱을 위한 리액티브 데이터 스토어, &lt;a href="https://tinybase.org/guides/releases/#v9-6"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</summary>
    <title>기술 뉴스 #301 : 2026-09-01</title>
    <updated>2026-09-01T01:42:45+09:00</updated>
    <dc:date>2026-09-01T01:42:45+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>향로 (기억보단 기록을)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;요즘 혼자 소리 내어 말하는 일이 많아졌다.&lt;/p&gt;
&lt;p&gt;샤워를 하면서 질문 하나를 정해 두고 혼자 답한다.&lt;br&gt;늦게까지 일한 날, 사람이 거의 없는 회사 앞에서 택시를 기다리는 몇 분 동안에도 누군가 내게 질문했다고 생각하고 답하기도 한다.&lt;/p&gt;
&lt;p&gt;왜 지금 이 일을 하는지, 최근 가장 고민하는 것은 무엇인지, 그때로 돌아간다면 같은 결정을 할 것인지 등등.&lt;/p&gt;
&lt;p&gt;답을 미리 적어 두지는 않는다.&lt;br&gt;충분히 정리될 때까지 기다리지도 않는다.&lt;br&gt;첫 문장부터 바로 말한다.&lt;/p&gt;
&lt;p&gt;생각을 정리할 때는 주로 글을 썼다.&lt;/p&gt;
&lt;p&gt;글을 쓰다 보면 내가 알고 있다고 생각했던 것과 실제로 설명할 수 있는 것이 다르다는 걸 자주 깨닫게 된다.&lt;br&gt;머릿속에서는 분명하다고 생각했는데, 막상 쓰려고 하면 왜 그렇게 생각하는지 설명하지 못할 때가 있다.&lt;/p&gt;
&lt;p&gt;글은 몇 번이고 고칠 수 있다.&lt;br&gt;앞 문장으로 돌아가 지울 수도 있고, 문단의 순서를 바꿀 수도 있다.&lt;br&gt;마음에 들지 않으면 모두 지우고 다시 쓸 수도 있다.&lt;br&gt;다른 사람에게 보여주기 전까지 초고는 나에게만 있다.&lt;/p&gt;
&lt;p&gt;말하기는 다르다.&lt;/p&gt;
&lt;p&gt;말에는 초고가 없다.&lt;/p&gt;
&lt;p&gt;말하고 나서야 표현이 이상했다는 걸 알 때가 있다.&lt;br&gt;그때는 이미 상대가 그 말을 들은 뒤다.&lt;br&gt;다시 설명할 수는 있지만, 처음 한 말을 없었던 일로 만들 수는 없다.&lt;/p&gt;
&lt;p&gt;사과도 정정도 그다음에 해야 한다.&lt;/p&gt;
&lt;p&gt;그래서 혼자 말할 때는 규칙을 하나 세웠다.&lt;br&gt;중간에 말이 꼬여도 다시 첫 문장으로 돌아가지 않기로.&lt;/p&gt;
&lt;p&gt;방금 한 말이 정확하지 않았다고 인정한 뒤, 내가 말하려던 뜻을 설명한다.&lt;br&gt;앞에서 한 말과 지금 하려는 말이 맞지 않으면 잘못 말한 부분부터 바로잡는다.&lt;/p&gt;
&lt;p&gt;다시 시작하면 더 매끄럽게 말할 수 있다.&lt;br&gt;하지만 내가 연습하려는 건 매끄럽게 말하는 일이 아니라, 잘못 말한 뒤에 바로잡는 일이었다.&lt;/p&gt;
&lt;p&gt;실제 대화에서는 예상하지 못한 반응도 생긴다.&lt;br&gt;상대는 내가 대수롭지 않게 말한 부분을 다시 묻거나 전혀 다른 뜻으로 받아들이기도 한다.&lt;/p&gt;
&lt;p&gt;그럴 때는 준비한 답을 그대로 이어갈 수 없다.&lt;br&gt;어디에서 다르게 이해했는지 확인한 뒤 다시 설명해야 한다.&lt;br&gt;그렇다고 누군가를 평가하거나 중요한 결정을 알리는 말까지 준비 없이 하자는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;이 연습이 말할 때만 필요한 것은 아니었다.&lt;/p&gt;
&lt;p&gt;리더로 오래 일하면서 처음부터 다시 하고 싶은 순간도 많았다.&lt;/p&gt;
&lt;p&gt;조직을 이렇게 구성하지 말았어야 했다거나, 제품의 첫 방향을 다르게 잡았어야 했다고 생각한 적이 있었다.&lt;/p&gt;
&lt;p&gt;하지만 판단이 틀렸다는 걸 알게 됐을 때는 이미 여러 사람이 그 결정에 맞춰 일하고 있었다.&lt;br&gt;그 결정에 따라 만들어진 기능도 있었고, 내가 설명한 방향대로 일을 진행한 사람도 있었다.&lt;/p&gt;
&lt;p&gt;다음에는 같은 실수를 하지 말아야겠다고 다짐하는 건 어렵지 않았다.&lt;br&gt;하지만 그 다짐으로 지금 벌어진 일이 나아지는 건 아니었다.&lt;/p&gt;
&lt;p&gt;잘못 판단한 부분을 인정하고, 부족했던 설명을 다시 하고, 바꿔야 할 것을 바꿔야 했다.&lt;/p&gt;
&lt;p&gt;돌이켜보면 시작하기 전에는 알 수 없었던 일도 많았다.&lt;br&gt;일을 시작하고 나서야 잘못 판단했다는 걸 알게 된 적도 있었다.&lt;br&gt;오래 고민한다고 해서 모든 문제를 미리 알 수 있는 것도 아니었다.&lt;/p&gt;
&lt;p&gt;혼자 말할 때 첫 문장으로 돌아가지 않는 것도 그래서다.&lt;/p&gt;
&lt;p&gt;말이 틀렸다면 바로 그다음 문장에서 고치고, 설명이 부족했다면 이어서 보탠다.&lt;br&gt;그렇게 샤워가 끝날 때까지 처음으로 돌아가지 않고 계속 말한다.&lt;/p&gt;
&lt;p&gt;일에서도 말에서도, 처음부터 틀리지 않는 능력이 있다면 좋겠지만 나한텐 그런 능력이 없었다.&lt;br&gt;그래서 틀렸다는 걸 알게 된 뒤 그다음 문장을 말하는 일이 더 중요해졌다.&lt;/p&gt;
&lt;p&gt;ps)&lt;/p&gt;
&lt;p&gt;요즘 GPT에 마이크를 켜고 생각나는 대로 말하는 것도 꽤 재미있다.&lt;br&gt;두서없이 말해도 알아서 정리해 주고, 녹음만 남겨 두는 것보다 나중에 다시 꺼내 쓰기도 편하다.&lt;br&gt;이 글도 GPT에 먼저 말한 내용을 정리해서 썼다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://jojoldu.tistory.com/884</id>
    <link href="https://jojoldu.tistory.com/884"/>
    <summary type="html">&lt;p&gt;요즘 혼자 소리 내어 말하는 일이 많아졌다.&lt;/p&gt;
&lt;p&gt;샤워를 하면서 질문 하나를 정해 두고 혼자 답한다.&lt;br&gt;늦게까지 일한 날, 사람이 거의 없는 회사 앞에서 택시를 기다리는 몇 분 동안에도 누군가 내게 질문했다고 생각하고 답하기도 한다.&lt;/p&gt;
&lt;p&gt;왜 지금 이 일을 하는지, 최근 가장 고민하는 것은 무엇인지, 그때로 돌아간다면 같은 결정을 할 것인지 등등.&lt;/p&gt;
&lt;p&gt;답을 미리 적어 두지는 않는다.&lt;br&gt;충분히 정리될 때까지 기다리지도 않는다.&lt;br&gt;첫 문장부터 바로 말한다.&lt;/p&gt;
&lt;p&gt;생각을 정리할 때는 주로 글을 썼다.&lt;/p&gt;
&lt;p&gt;글을 쓰다 보면 내가 알고 있다고 생각했던 것과 실제로 설명할 수 있는 것이 다르다는 걸 자주 깨닫게 된다.&lt;br&gt;머릿속에서는 분명하다고 생각했는데, 막상 쓰려고 하면 왜 그렇게 생각하는지 설명하지 못할 때가 있다.&lt;/p&gt;
&lt;p&gt;글은 몇 번이고 고칠 수 있다.&lt;br&gt;앞 문장으로 돌아가 지울 수도 있고, 문단의 순서를 바꿀 수도 있다.&lt;br&gt;마음에 들지 않으면 모두 지우고 다시 쓸 수도 있다.&lt;br&gt;다른 사람에게 보여주기 전까지 초고는 나에게만 있다.&lt;/p&gt;
&lt;p&gt;말하기는 다르다.&lt;/p&gt;
&lt;p&gt;말에는 초고가 없다.&lt;/p&gt;
&lt;p&gt;말하고 나서야 표현이 이상했다는 걸 알 때가 있다.&lt;br&gt;그때는 이미 상대가 그 말을 들은 뒤다.&lt;br&gt;다시 설명할 수는 있지만, 처음 한 말을 없었던 일로 만들 수는 없다.&lt;/p&gt;
&lt;p&gt;사과도 정정도 그다음에 해야 한다.&lt;/p&gt;
&lt;p&gt;그래서 혼자 말할 때는 규칙을 하나 세웠다.&lt;br&gt;중간에 말이 꼬여도 다시 첫 문장으로 돌아가지 않기로.&lt;/p&gt;
&lt;p&gt;방금 한 말이 정확하지 않았다고 인정한 뒤, 내가 말하려던 뜻을 설명한다.&lt;br&gt;앞에서 한 말과 지금 하려는 말이 맞지 않으면 잘못 말한 부분부터 바로잡는다.&lt;/p&gt;
&lt;p&gt;다시 시작하면 더 매끄럽게 말할 수 있다.&lt;br&gt;하지만 내가 연습하려는 건 매끄럽게 말하는 일이 아니라, 잘못 말한 뒤에 바로잡는 일이었다.&lt;/p&gt;
&lt;p&gt;실제 대화에서는 예상하지 못한 반응도 생긴다.&lt;br&gt;상대는 내가 대수롭지 않게 말한 부분을 다시 묻거나 전혀 다른 뜻으로 받아들이기도 한다.&lt;/p&gt;
&lt;p&gt;그럴 때는 준비한 답을 그대로 이어갈 수 없다.&lt;br&gt;어디에서 다르게 이해했는지 확인한 뒤 다시 설명해야 한다.&lt;br&gt;그렇다고 누군가를 평가하거나 중요한 결정을 알리는 말까지 준비 없이 하자는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;이 연습이 말할 때만 필요한 것은 아니었다.&lt;/p&gt;
&lt;p&gt;리더로 오래 일하면서 처음부터 다시 하고 싶은 순간도 많았다.&lt;/p&gt;
&lt;p&gt;조직을 이렇게 구성하지 말았어야 했다거나, 제품의 첫 방향을 다르게 잡았어야 했다고 생각한 적이 있었다.&lt;/p&gt;
&lt;p&gt;하지만 판단이 틀렸다는 걸 알게 됐을 때는 이미 여러 사람이 그 결정에 맞춰 일하고 있었다.&lt;br&gt;그 결정에 따라 만들어진 기능도 있었고, 내가 설명한 방향대로 일을 진행한 사람도 있었다.&lt;/p&gt;
&lt;p&gt;다음에는 같은 실수를 하지 말아야겠다고 다짐하는 건 어렵지 않았다.&lt;br&gt;하지만 그 다짐으로 지금 벌어진 일이 나아지는 건 아니었다.&lt;/p&gt;
&lt;p&gt;잘못 판단한 부분을 인정하고, 부족했던 설명을 다시 하고, 바꿔야 할 것을 바꿔야 했다.&lt;/p&gt;
&lt;p&gt;돌이켜보면 시작하기 전에는 알 수 없었던 일도 많았다.&lt;br&gt;일을 시작하고 나서야 잘못 판단했다는 걸 알게 된 적도 있었다.&lt;br&gt;오래 고민한다고 해서 모든 문제를 미리 알 수 있는 것도 아니었다.&lt;/p&gt;
&lt;p&gt;혼자 말할 때 첫 문장으로 돌아가지 않는 것도 그래서다.&lt;/p&gt;
&lt;p&gt;말이 틀렸다면 바로 그다음 문장에서 고치고, 설명이 부족했다면 이어서 보탠다.&lt;br&gt;그렇게 샤워가 끝날 때까지 처음으로 돌아가지 않고 계속 말한다.&lt;/p&gt;
&lt;p&gt;일에서도 말에서도, 처음부터 틀리지 않는 능력이 있다면 좋겠지만 나한텐 그런 능력이 없었다.&lt;br&gt;그래서 틀렸다는 걸 알게 된 뒤 그다음 문장을 말하는 일이 더 중요해졌다.&lt;/p&gt;
&lt;p&gt;ps)&lt;/p&gt;
&lt;p&gt;요즘 GPT에 마이크를 켜고 생각나는 대로 말하는 것도 꽤 재미있다.&lt;br&gt;두서없이 말해도 알아서 정리해 주고, 녹음만 남겨 두는 것보다 나중에 다시 꺼내 쓰기도 편하다.&lt;br&gt;이 글도 GPT에 먼저 말한 내용을 정리해서 썼다.&lt;/p&gt;</summary>
    <title>틀려도 다시 말하지 않기</title>
    <updated>2026-08-30T22:51:57+09:00</updated>
    <dc:date>2026-08-30T22:51:57+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>GREEN.1229</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;br&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SE-0544 — noncopyable 타입 deinit에서의 mutation과 consumption에 대해 정리&lt;/b&gt;&lt;/span&gt;해보겠습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOCJvk%2FdJMcagfGIHE%2Fxpi1k8fwKTGejkRcaFqE60%2Fimg.jpg" onerror="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';" loading="lazy" width="400" height="400" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Intro&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style="background: #f2f8ee; border-radius: 10px; padding: 16px 20px; margin: 16px 0;"&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Proposal:&lt;/b&gt; SE-0544&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Author:&lt;/b&gt; Joe Groff&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Review Manager:&lt;/b&gt; John McCall&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Status:&lt;/b&gt; Accepted&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Implementation:&lt;/b&gt; swiftlang/swift#90836&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;noncopyable 타입은 &lt;code&gt;deinit&lt;/code&gt;을 정의해서 생명주기가 끝날 때 소유하고 있던 리소스를 정리할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;하지만 이 제안 이전에는 &lt;code&gt;deinit&lt;/code&gt; 본문 안에서 &lt;code&gt;self&lt;/code&gt;가 immutable하고 오직 borrow만 가능&lt;/b&gt;했어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안은&lt;b&gt; &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;의 필드를 mutate하거나 consume할 수 있게 허용하되, value resurrection 문제를 피하기 위해 &lt;code&gt;self&lt;/code&gt; 전체를 하나의 값으로서 mutate하거나 consume하는 건 여전히 막아두자고 제안&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Motivation&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;많은 noncopyable 타입 구현체는 리소스를 소유한 다른 noncopyable 값들로 구성돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 구성 요소들이 전체 값의 정리 과정에서 어떻게 consume되는지 제어하고 싶은 건 자연스러운 요청일겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="nimrod" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct File: ~Copyable {
  consuming func close() {...}
}

struct Buffer: ~Copyable {
  borrowing func flush(to file: borrowing File) {...}
  consuming func release() {...}
}

struct BufferedFile: ~Copyable {
  let file: File
  let buffer: Buffer
  
  deinit {
    // Flush then close the buffer
    buffer.flush(to: file)
    buffer.release()
    // Then close the file
    file.close()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;비슷한 맥락에서, deinit이 정리 과정의 일부로 mutating 메서드에 담아둔 코드를 쓰고 싶을 수도 있어요.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Proposed Solution&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;의 필드를 mutate하고 consume할 수 있도록 허용하자고 제안합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Detailed Design&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;"resurrection"과 의도치 않은 재귀 피하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;noncopyable 타입의 &lt;code&gt;deinit&lt;/code&gt;은 값의 소유권을 가진 컨텍스트 중에서도 독특한 존재예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;다른 소유 컨텍스트라면 값을 암묵적으로 &lt;code&gt;deinit&lt;/code&gt;을 호출해서 파괴하겠지만, &lt;code&gt;deinit&lt;/code&gt; 자신은 당연히 그럴 수 없죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;은 오직 값을 구성하는 저장 프로퍼티나 채워진 enum case만 파괴합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;만약 &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;를 consuming이나 mutating 연산에 통째로 넘길 수 있다면, 그 callee의 생명주기가 끝나는 시점에 다시 &lt;code&gt;deinit&lt;/code&gt;이 호출될 테니 값이 callee 안에서 "부활"&lt;/b&gt;하는 셈이 돼요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;이러면 실수로 무한 루프를 만들기 쉬워집니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct Foo: ~Copyable {
  deinit {
    self.foo()
  }

  consuming func foo() {
    // oops, implicitly calls back into \`deinit\`
  }
}

struct Bar: ~Copyable {
  deinit {
    self.bar()
  }

  mutating func bar() {
    // oops, implicitly calls \`deinit\` on the old value of \`self\`
    // before reassigning it
    self = Bar()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안은 이런 문제를 피하기 위해 &lt;b&gt;&lt;code&gt;self&lt;/code&gt;를 하나의 값으로서 mutate하거나 consume하는 걸 금지&lt;/b&gt;해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;대부분의 경우 이 정도 제약은 받아들일 만하다고 봅니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;다만 이 제약의 한 가지 결과로, &lt;code&gt;deinit&lt;/code&gt;은 같은 타입의 &lt;code&gt;mutating&lt;/code&gt;이나 &lt;code&gt;consuming&lt;/code&gt; 메서드를 호출할 수 없어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그래도 &lt;code&gt;deinit&lt;/code&gt;은 필드를 다루는 &lt;code&gt;static&lt;/code&gt; 메서드를 통해 다른 메서드와 로직을 공유할 수는 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct Resource: ~Copyable {
  var resourceID: Int
  
  // Shared logic for releasing the underlying resource by ID.
  // The release operation may surface error conditions, but these can be
  // ignored in normal use.
  private static func release(resourceID: Int) throws {...}
  
  // Consuming method that releases the resource, surfacing errors to be
  // handled
  consuming func release() throws {
    try Self.release(resourceID: self.resourceID)
    discard self
  }
  
  // Deinit that implicitly closes the resource, swallowing errors
  deinit {
    do {
      try Self.release(resourceID: self.resourceID)
    } catch {
      // Ignore the error
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;또한 &lt;code&gt;deinit&lt;/code&gt;이 값 전체의 소유권을 넘기고 싶은 상황도 있을 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;예를 들어 값을 정리하는 데 시간이 오래 걸린다면, &lt;code&gt;deinit&lt;/code&gt; 도중 바로 정리하기보다는 죽어가는 값을 나중에 정리하도록 큐에 넣어두는 게 나을 수 있죠. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;은 항상 타입의 원본 선언 안에 정의되기 때문에, 언제나 &lt;code&gt;struct&lt;/code&gt;의 레이아웃과 memberwise 이니셜라이저에 접근할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;그래서 self의 필드들을 memberwise 이니셜라이저에 넘겨서 값을 명시적으로 "부활"시킬 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;let deferredCleanupValues: ConcurrentQueue

struct DeferredCleanup: ~Copyable {
  var resource1: Resource1
  var resource2: Resource2

  deinit {
    // Instead of cleaning up this value's resources immediately, push an
    // equivalent value into the queue to be cleaned up later
    let newSelf = Self(resource1: self.resource1, resource2: self.resource2)
    deferredCleanupValues.push(newSelf)
  }

  consuming func runTimeConsumingCleanup() async { ... }
}

func runDeferredCleanups() async {
  while let value = deferredCleanupValues.pop() {
    await value.runTimeConsumingCleanup()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 밖의 제약&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt; 도중 클로저 안에서 &lt;code&gt;self&lt;/code&gt;를 캡처하는 건 여전히 허용되지 않아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;부분적으로 consume된 self의 정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 리턴하는 시점에 &lt;code&gt;self&lt;/code&gt;의 어떤 구성 요소가 아직 consume되지 않았다면, 남은 구성 요소들은 암묵적으로 파괴돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여기엔 noncopyable 구성 요소의 &lt;code&gt;deinit&lt;/code&gt;을 실행하는 것도 포함됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Alternatives Considered&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;self&lt;/code&gt;를 전체 값으로서 mutate하거나 consume하는 제약이 실제로 너무 부담스럽다고 판명되면, &lt;b&gt;이 제약을 완화하는 방향도 열려 있어요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;resurrection 위험을 완화할 다른 방법들도 있습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;resurrect self 연산 도입하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안의 &lt;code&gt;deinit&lt;/code&gt;들은 필드로부터 memberwise-initialize해서 &lt;code&gt;self&lt;/code&gt;를 수동으로 "부활"시킬 수 있지만, 이건 장황해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이게 흔한 패턴이 되면 이를 위한 축약 문법을 도입할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이건 &lt;code&gt;discard self&lt;/code&gt;의 반대로 볼 수 있는데, &lt;code&gt;discard self&lt;/code&gt;가 &lt;code&gt;self&lt;/code&gt;의 암묵적 &lt;code&gt;deinit&lt;/code&gt;을 비활성화하고 &lt;code&gt;self&lt;/code&gt;를 전체 값으로 더 이상 쓸 수 없게 만드는 거라면, 이건 반대로 &lt;code&gt;self&lt;/code&gt;의 암묵적 &lt;code&gt;deinit&lt;/code&gt;을 다시 활성화해서 전체 값으로 다시 다룰 수 있게 하는 셈이니까요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;"deinit-safe" 메서드에 어노테이션 달기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 전체 값에 적용할 수 있는 연산을, 어떤 방식으로든 "deinit-safe"임을 명시적으로 표시한 메서드로만 제한할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그렇게 표시된 &lt;code&gt;consuming&lt;/code&gt; 메서드는 반드시 &lt;code&gt;discard self&lt;/code&gt;를 하도록 요구하고, 표시된 &lt;code&gt;mutating&lt;/code&gt; 메서드는 &lt;code&gt;self&lt;/code&gt;를 완전히 재할당하는 걸 막는 식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;deinit이 지역적으로 정의된 메서드만 호출하도록 제한하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;명시적인 어노테이션 대신, &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;를 mutate하거나 consume할 수 있는 방법을, &lt;code&gt;deinit&lt;/code&gt;과 함께 원본 타입 정의 안에 정의된 메서드나 같은 모듈 안의 메서드로만 제한할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그러면 파일/모듈 단위 분석으로 &lt;code&gt;deinit&lt;/code&gt;에서 호출되는 메서드가 잠재적으로 다시 &lt;code&gt;deinit&lt;/code&gt;을 호출하는 지점을 감지할 수 있을 거예요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;mutation 이후 self의 명시적 consumption이나 discard 요구하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;첫 번째 pitch 스레드에서 ellie20은 "값이 mutate된 이후엔 어떤 의미에서 다른 값이다"라고 지적하면서, &lt;code&gt;self&lt;/code&gt;를 전체 값으로 mutating한 이후에는 그 뒤의 값을 명시적으로 &lt;code&gt;discard&lt;/code&gt;하거나 &lt;code&gt;consume&lt;/code&gt;하도록 요구해야 한다고 주장했어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 시점의 값은 더 이상 원래 &lt;code&gt;deinit&lt;/code&gt; 대상이던 값이 아니기 때문이에요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct A: ~Copyable {
  deinit {
    self.replace()
    // explicitly indicate that the replaced \`self\` is discarded, rather than
    // deinit-ed
    discard self
  }

  mutating func replace() { ... }
}

struct B: ~Copyable {
  deinit {
    self.replace()
    // explicitly indicate that the replaced \`self\` is consumed, causing deinit
    // to run again on the new value
    _ = consume self
  }

  mutating func replace() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Conclusion&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그동안 noncopyable 타입의 &lt;code&gt;deinit&lt;/code&gt;은 &lt;code&gt;self&lt;/code&gt;를 오직 borrow만 할 수 있어서, 내부 필드를 정리하는 로직을 &lt;code&gt;mutating&lt;/code&gt;/&lt;code&gt;consuming&lt;/code&gt; 메서드로 재사용하기 어려웠어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안 덕분에 필드 단위의 mutate/consume은 자유로워지면서도, &lt;code&gt;self&lt;/code&gt; 전체를 다루는 건 여전히 막아서 "부활"과 무한 재귀라는 위험한 함정은 피했다는 점이 인상적이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;static&lt;/code&gt; 메서드로 로직을 공유하거나, &lt;b&gt;memberwise 이니셜라이저로 명시적으로 부활시키는 우회로도 함께 마련해둔 균형 잡힌 제안&lt;/b&gt; 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;References&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure id="og_1788151029441" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="object" data-og-title="swift-evolution/proposals/0544-mutate-or-consume-in-deinit.md at main · swiftlang/swift-evolution" data-og-description="This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution" data-og-host="github.com" data-og-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" data-og-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" data-og-image="https://scrap.kakaocdn.net/dn/bU7qH5/dJMb9eT7Fnf/b4Dcl9ZmfEi18xsZot9Gdk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cRQvmK/dJMb8XSotOw/w5xdqWnezbH1ky3m1L7pa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600"&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" target="_blank" rel="noopener" data-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/bU7qH5/dJMb9eT7Fnf/b4Dcl9ZmfEi18xsZot9Gdk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cRQvmK/dJMb8XSotOw/w5xdqWnezbH1ky3m1L7pa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;swift-evolution/proposals/0544-mutate-or-consume-in-deinit.md at main · swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://green1229.tistory.com/635</id>
    <link href="https://green1229.tistory.com/635"/>
    <summary type="html">&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SE-0544 &amp;mdash; noncopyable 타입 deinit에서의 mutation과 consumption에 대해 정리&lt;/b&gt;&lt;/span&gt;해보겠습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/OCJvk/dJMcagfGIHE/xpi1k8fwKTGejkRcaFqE60/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOCJvk%2FdJMcagfGIHE%2Fxpi1k8fwKTGejkRcaFqE60%2Fimg.jpg" onerror="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';" loading="lazy" width="400" height="400" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Intro&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style="background: #f2f8ee; border-radius: 10px; padding: 16px 20px; margin: 16px 0;"&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Proposal:&lt;/b&gt; SE-0544&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Author:&lt;/b&gt; Joe Groff&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Review Manager:&lt;/b&gt; John McCall&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Status:&lt;/b&gt; Accepted&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Implementation:&lt;/b&gt; swiftlang/swift#90836&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;noncopyable 타입은 &lt;code&gt;deinit&lt;/code&gt;을 정의해서 생명주기가 끝날 때 소유하고 있던 리소스를 정리할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;하지만 이 제안 이전에는 &lt;code&gt;deinit&lt;/code&gt; 본문 안에서 &lt;code&gt;self&lt;/code&gt;가 immutable하고 오직 borrow만 가능&lt;/b&gt;했어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안은&lt;b&gt; &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;의 필드를 mutate하거나 consume할 수 있게 허용하되, value resurrection 문제를 피하기 위해 &lt;code&gt;self&lt;/code&gt; 전체를 하나의 값으로서 mutate하거나 consume하는 건 여전히 막아두자고 제안&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Motivation&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;많은 noncopyable 타입 구현체는 리소스를 소유한 다른 noncopyable 값들로 구성돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 구성 요소들이 전체 값의 정리 과정에서 어떻게 consume되는지 제어하고 싶은 건 자연스러운 요청일겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="nimrod" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct File: ~Copyable {
  consuming func close() {...}
}

struct Buffer: ~Copyable {
  borrowing func flush(to file: borrowing File) {...}
  consuming func release() {...}
}

struct BufferedFile: ~Copyable {
  let file: File
  let buffer: Buffer
  
  deinit {
    // Flush then close the buffer
    buffer.flush(to: file)
    buffer.release()
    // Then close the file
    file.close()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;비슷한 맥락에서, deinit이 정리 과정의 일부로 mutating 메서드에 담아둔 코드를 쓰고 싶을 수도 있어요.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Proposed Solution&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;의 필드를 mutate하고 consume할 수 있도록 허용하자고 제안합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Detailed Design&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;"resurrection"과 의도치 않은 재귀 피하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;noncopyable 타입의 &lt;code&gt;deinit&lt;/code&gt;은 값의 소유권을 가진 컨텍스트 중에서도 독특한 존재예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;다른 소유 컨텍스트라면 값을 암묵적으로 &lt;code&gt;deinit&lt;/code&gt;을 호출해서 파괴하겠지만, &lt;code&gt;deinit&lt;/code&gt; 자신은 당연히 그럴 수 없죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;은 오직 값을 구성하는 저장 프로퍼티나 채워진 enum case만 파괴합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;만약 &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;를 consuming이나 mutating 연산에 통째로 넘길 수 있다면, 그 callee의 생명주기가 끝나는 시점에 다시 &lt;code&gt;deinit&lt;/code&gt;이 호출될 테니 값이 callee 안에서 "부활"&lt;/b&gt;하는 셈이 돼요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;이러면 실수로 무한 루프를 만들기 쉬워집니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct Foo: ~Copyable {
  deinit {
    self.foo()
  }

  consuming func foo() {
    // oops, implicitly calls back into \`deinit\`
  }
}

struct Bar: ~Copyable {
  deinit {
    self.bar()
  }

  mutating func bar() {
    // oops, implicitly calls \`deinit\` on the old value of \`self\`
    // before reassigning it
    self = Bar()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안은 이런 문제를 피하기 위해 &lt;b&gt;&lt;code&gt;self&lt;/code&gt;를 하나의 값으로서 mutate하거나 consume하는 걸 금지&lt;/b&gt;해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;대부분의 경우 이 정도 제약은 받아들일 만하다고 봅니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;다만 이 제약의 한 가지 결과로, &lt;code&gt;deinit&lt;/code&gt;은 같은 타입의 &lt;code&gt;mutating&lt;/code&gt;이나 &lt;code&gt;consuming&lt;/code&gt; 메서드를 호출할 수 없어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그래도 &lt;code&gt;deinit&lt;/code&gt;은 필드를 다루는 &lt;code&gt;static&lt;/code&gt; 메서드를 통해 다른 메서드와 로직을 공유할 수는 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct Resource: ~Copyable {
  var resourceID: Int
  
  // Shared logic for releasing the underlying resource by ID.
  // The release operation may surface error conditions, but these can be
  // ignored in normal use.
  private static func release(resourceID: Int) throws {...}
  
  // Consuming method that releases the resource, surfacing errors to be
  // handled
  consuming func release() throws {
    try Self.release(resourceID: self.resourceID)
    discard self
  }
  
  // Deinit that implicitly closes the resource, swallowing errors
  deinit {
    do {
      try Self.release(resourceID: self.resourceID)
    } catch {
      // Ignore the error
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;또한 &lt;code&gt;deinit&lt;/code&gt;이 값 전체의 소유권을 넘기고 싶은 상황도 있을 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;예를 들어 값을 정리하는 데 시간이 오래 걸린다면, &lt;code&gt;deinit&lt;/code&gt; 도중 바로 정리하기보다는 죽어가는 값을 나중에 정리하도록 큐에 넣어두는 게 나을 수 있죠. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;은 항상 타입의 원본 선언 안에 정의되기 때문에, 언제나 &lt;code&gt;struct&lt;/code&gt;의 레이아웃과 memberwise 이니셜라이저에 접근할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;그래서 self의 필드들을 memberwise 이니셜라이저에 넘겨서 값을 명시적으로 "부활"시킬 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;let deferredCleanupValues: ConcurrentQueue

struct DeferredCleanup: ~Copyable {
  var resource1: Resource1
  var resource2: Resource2

  deinit {
    // Instead of cleaning up this value's resources immediately, push an
    // equivalent value into the queue to be cleaned up later
    let newSelf = Self(resource1: self.resource1, resource2: self.resource2)
    deferredCleanupValues.push(newSelf)
  }

  consuming func runTimeConsumingCleanup() async { ... }
}

func runDeferredCleanups() async {
  while let value = deferredCleanupValues.pop() {
    await value.runTimeConsumingCleanup()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 밖의 제약&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt; 도중 클로저 안에서 &lt;code&gt;self&lt;/code&gt;를 캡처하는 건 여전히 허용되지 않아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;부분적으로 consume된 self의 정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 리턴하는 시점에 &lt;code&gt;self&lt;/code&gt;의 어떤 구성 요소가 아직 consume되지 않았다면, 남은 구성 요소들은 암묵적으로 파괴돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여기엔 noncopyable 구성 요소의 &lt;code&gt;deinit&lt;/code&gt;을 실행하는 것도 포함됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Alternatives Considered&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;self&lt;/code&gt;를 전체 값으로서 mutate하거나 consume하는 제약이 실제로 너무 부담스럽다고 판명되면, &lt;b&gt;이 제약을 완화하는 방향도 열려 있어요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;resurrection 위험을 완화할 다른 방법들도 있습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;resurrect self 연산 도입하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안의 &lt;code&gt;deinit&lt;/code&gt;들은 필드로부터 memberwise-initialize해서 &lt;code&gt;self&lt;/code&gt;를 수동으로 "부활"시킬 수 있지만, 이건 장황해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이게 흔한 패턴이 되면 이를 위한 축약 문법을 도입할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이건 &lt;code&gt;discard self&lt;/code&gt;의 반대로 볼 수 있는데, &lt;code&gt;discard self&lt;/code&gt;가 &lt;code&gt;self&lt;/code&gt;의 암묵적 &lt;code&gt;deinit&lt;/code&gt;을 비활성화하고 &lt;code&gt;self&lt;/code&gt;를 전체 값으로 더 이상 쓸 수 없게 만드는 거라면, 이건 반대로 &lt;code&gt;self&lt;/code&gt;의 암묵적 &lt;code&gt;deinit&lt;/code&gt;을 다시 활성화해서 전체 값으로 다시 다룰 수 있게 하는 셈이니까요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;"deinit-safe" 메서드에 어노테이션 달기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;deinit&lt;/code&gt;이 전체 값에 적용할 수 있는 연산을, 어떤 방식으로든 "deinit-safe"임을 명시적으로 표시한 메서드로만 제한할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그렇게 표시된 &lt;code&gt;consuming&lt;/code&gt; 메서드는 반드시 &lt;code&gt;discard self&lt;/code&gt;를 하도록 요구하고, 표시된 &lt;code&gt;mutating&lt;/code&gt; 메서드는 &lt;code&gt;self&lt;/code&gt;를 완전히 재할당하는 걸 막는 식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;deinit이 지역적으로 정의된 메서드만 호출하도록 제한하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;명시적인 어노테이션 대신, &lt;code&gt;deinit&lt;/code&gt;이 &lt;code&gt;self&lt;/code&gt;를 mutate하거나 consume할 수 있는 방법을, &lt;code&gt;deinit&lt;/code&gt;과 함께 원본 타입 정의 안에 정의된 메서드나 같은 모듈 안의 메서드로만 제한할 수도 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그러면 파일/모듈 단위 분석으로 &lt;code&gt;deinit&lt;/code&gt;에서 호출되는 메서드가 잠재적으로 다시 &lt;code&gt;deinit&lt;/code&gt;을 호출하는 지점을 감지할 수 있을 거예요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;mutation 이후 self의 명시적 consumption이나 discard 요구하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;첫 번째 pitch 스레드에서 ellie20은 "값이 mutate된 이후엔 어떤 의미에서 다른 값이다"라고 지적하면서, &lt;code&gt;self&lt;/code&gt;를 전체 값으로 mutating한 이후에는 그 뒤의 값을 명시적으로 &lt;code&gt;discard&lt;/code&gt;하거나 &lt;code&gt;consume&lt;/code&gt;하도록 요구해야 한다고 주장했어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그 시점의 값은 더 이상 원래 &lt;code&gt;deinit&lt;/code&gt; 대상이던 값이 아니기 때문이에요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct A: ~Copyable {
  deinit {
    self.replace()
    // explicitly indicate that the replaced \`self\` is discarded, rather than
    // deinit-ed
    discard self
  }

  mutating func replace() { ... }
}

struct B: ~Copyable {
  deinit {
    self.replace()
    // explicitly indicate that the replaced \`self\` is consumed, causing deinit
    // to run again on the new value
    _ = consume self
  }

  mutating func replace() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Conclusion&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그동안 noncopyable 타입의 &lt;code&gt;deinit&lt;/code&gt;은 &lt;code&gt;self&lt;/code&gt;를 오직 borrow만 할 수 있어서, 내부 필드를 정리하는 로직을 &lt;code&gt;mutating&lt;/code&gt;/&lt;code&gt;consuming&lt;/code&gt; 메서드로 재사용하기 어려웠어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 제안 덕분에 필드 단위의 mutate/consume은 자유로워지면서도, &lt;code&gt;self&lt;/code&gt; 전체를 다루는 건 여전히 막아서 "부활"과 무한 재귀라는 위험한 함정은 피했다는 점이 인상적이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;static&lt;/code&gt; 메서드로 로직을 공유하거나, &lt;b&gt;memberwise 이니셜라이저로 명시적으로 부활시키는 우회로도 함께 마련해둔 균형 잡힌 제안&lt;/b&gt; 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;References&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure id="og_1788151029441" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="object" data-og-title="swift-evolution/proposals/0544-mutate-or-consume-in-deinit.md at main &amp;middot; swiftlang/swift-evolution" data-og-description="This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution" data-og-host="github.com" data-og-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" data-og-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" data-og-image="https://scrap.kakaocdn.net/dn/bU7qH5/dJMb9eT7Fnf/b4Dcl9ZmfEi18xsZot9Gdk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cRQvmK/dJMb8XSotOw/w5xdqWnezbH1ky3m1L7pa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600"&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md" target="_blank" rel="noopener" data-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0544-mutate-or-consume-in-deinit.md"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/bU7qH5/dJMb9eT7Fnf/b4Dcl9ZmfEi18xsZot9Gdk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cRQvmK/dJMb8XSotOw/w5xdqWnezbH1ky3m1L7pa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;swift-evolution/proposals/0544-mutate-or-consume-in-deinit.md at main &amp;middot; swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</summary>
    <title>[SE-0544] Mutation and consumption in non-Copyable type deinits</title>
    <updated>2026-08-31T13:37:48+09:00</updated>
    <dc:date>2026-08-31T13:37:48+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>admin@wonkooklee.com (Wonkook Lee)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;AI로 결과물을 먼저 만든 뒤 재귀적 질문과 자기 설명으로 빈틈을 채우는 ‘생성 후 이해’ 방식의 탑다운 학습을 실무와 글쓰기 경험으로 풀어냅니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.wonkooklee.com/blog/20260830_01/?utm_source=rss&amp;amp;utm_medium=referral&amp;amp;utm_campaign=feed-blog&amp;amp;utm_content=cta"&gt;전문 보기 →&lt;/a&gt;&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://blog.wonkooklee.com/blog/20260830_01/?utm_source=rss&amp;utm_medium=referral&amp;utm_campaign=feed-blog&amp;utm_content=title</id>
    <link href="https://blog.wonkooklee.com/blog/20260830_01/?utm_source=rss&amp;utm_medium=referral&amp;utm_campaign=feed-blog&amp;utm_content=title"/>
    <summary type="html">AI로 결과물을 먼저 만든 뒤 재귀적 질문과 자기 설명으로 빈틈을 채우는 ‘생성 후 이해’ 방식의 탑다운 학습을 실무와 글쓰기 경험으로 풀어냅니다.</summary>
    <title>AI 시대의 탑다운 학습과 생성 후 이해</title>
    <updated>2026-08-30T09:00:00+09:00</updated>
    <dc:date>2026-08-30T09:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>망나니개발자</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="color: #f15f5f;"&gt;1.&lt;span&gt;&lt;span&gt; 도커 이미지 빌드 최적화를 위한 Layered Jar &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style5"&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span&gt;&lt;b&gt;[ 기존 Fat JAR 방식의 문제와 Layered Jar의 등장 ]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;기존 Fat JAR 방식의 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;Layered Jar란 스프링 부트 애플리케이션을 위한 Docker 이미지를 만들 수 있도록 도와주는 도구이다. 표준 jar 스펙은 jar 내부에 jar 파일을 가질 수 없다. 하지만 애플리케이션을 개발하다 보면 많은 외부의 프레임워크와 라이브러리를 사용하게 되어 다른 Jar가 필요해진다. 따라서 스프링 부트는 jar 내부에 다른 jar를 가지면서 java -jar로 실행할 수 있는 "fat jar" 형식을 예전부터 지원해 왔다. 이러한 fat jar 내부 구성은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1120" data-origin-height="720"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png" data-phocus="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK1bok%2FdJMcaal9Xal%2FB1wEombeASshwS4cVgOl0K%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="720" data-origin-width="1120" data-origin-height="720"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이러한 fat jar를 도커 이미지로 만드는 것은 어렵지 않다. 하지만 fat jar 그대로 도커 이미지로 복사해 사용하면 비효율적인 부분이 많다. 대개 변경되는 것은 사용자 코드인데, 상대적으로 변경이 적은 라이브러리/프레임워크 파일들까지 포함된 단일 jar 파일을 매번 새로운 도커 이미지로 만들어야 하기 때문이다. 따라서 사용자 애플리케이션 클래스와 라이브러리 모음을 분리시켜서, 변경된 사용자 애플리케이션 부분만 Docker 레이어에 갈아 끼우고 나머지는 캐시에서 사용하는 구조를 고안하게 되었다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;Layered Jar 방식의 등장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;앞서 설명하였듯 fat jar 형식은 jar 표준 기술이 아닌 스프링 부트만의 자체적인 포맷이다. 따라서 스프링 부트는 2.3부터 이 jar 내부에 레이어 인덱스 파일을 추가하는 기능을 넣었다. 여러 가지 변천사가 있었지만 지금은 jar 내부 구조를 fat jar와 동일하게 두되, BOOT-INF/layers.idx 라는 인덱스 파일 하나만 추가한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 파일은 우리가 직접 만드는 것은 아니고, Spring Boot Gradle 플러그인의 bootJar 태스크가 만든다. 스프링 부트 2.3 에서는 layered { enabled = true } 로 직접 활성화를 해야 했지만, 2.4 부터는 기본 활성화되어 아무 설정 없이도 기본 4개 레이어로 layers.idx 가 생성되고 MANIFEST 에 Spring-Boot-Layers-Index 가 선언된다. BootJar 태스크는 이 설정을 해석해 BOOT-INF/layers.idx 를 생성한다. 이 파일에는 레이어 목록과 각 레이어에 속하는 경로가 적혀 있고, 목록의 순서가 곧 이미지에 쌓을 순서다. 기본 레이어는 다음과 같다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;dependencies: 외부 프레임워크/라이브러리 의존성 모음&lt;/li&gt;
&lt;li&gt;spring-boot-loader: org/springframework/boot/loader 아래의 모든 코드&lt;/li&gt;
&lt;li&gt;snapshot-dependencies: 버전에 SNAPSHOT 이 붙은 의존성&lt;/li&gt;
&lt;li&gt;application: 사용자 애플리케이션 클래스와 리소스 및 프로젝트 의존성 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1120" data-origin-height="720"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png" data-phocus="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEgsqw%2FdJMcaaGEqra%2FzorcURedatK6wG0vNFgYOk%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="720" data-origin-width="1120" data-origin-height="720"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 구성은 애플리케이션 빌드 시의 변경 빈도에 따라 구분되었다. 라이브러리 코드는 변경이 적으니 레이어를 분리시켜 캐시에서 재사용하게 했다. 반면 애플리케이션 코드는 빌드마다 바뀔 가능성이 크다. 따라서 기존에는 매번 하나의 app.jar를 도커 이미지에 푸시했다면, 이제는 변경이 있는 애플리케이션 부분만 푸시하면 되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1120" data-origin-height="740"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png" data-phocus="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpuEJV%2FdJMcaal9Xm5%2FKrRacOcxTz5Odu3Eqekmj0%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="740" data-origin-width="1120" data-origin-height="740"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ jarmode에 대하여 ]&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;jarmode의 등장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;스프링 부트가 나눈 레이어를 도커 이미지로 만들려면 결국 dockerfile을 써야 한다. 그런데 Docker의 규칙과 스프링 부트의 jar 방식 사이에 호환되지 않는 부분이 있다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;dockerfile에서 이미지 레이어를 만드는 단위는 COPY 명령 한 줄이다. COPY 를 네 번 쓰면 레이어가 네 개 생긴다. 따라서 레이어를 넷으로 나누려면, 이미지를 빌드하는 시점에 디스크 위에 각 4계층의 최상위 폴더가 네 개 있어야 한다. 그래서 각 폴더를 한 번씩 COPY 하는 것이다.&lt;/p&gt;
&lt;pre class="dockerfile"&gt;&lt;code&gt;dependencies/           &amp;lt;- COPY 한 번 = 이미지 레이어 하나
spring-boot-loader/     &amp;lt;- COPY 한 번
snapshot-dependencies/  &amp;lt;- COPY 한 번
application/            &amp;lt;- COPY 한 번
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그런데 jar는 내부적으로 fat jar 구조이므로, 그냥 jar 압축을 풀면 도커 이미지 레이어와 호환되지 않는 폴더 구조가 나온다.&lt;/p&gt;
&lt;pre class="crystal"&gt;&lt;code&gt;org/springframework/boot/loader/...
BOOT-INF/classes/...
BOOT-INF/lib/library1.jar
BOOT-INF/lib/lib3-SNAPSHOT.jar
BOOT-INF/layers.idx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;따라서 파일 이름만 봐서는 해당 디렉토리의 어떤 파일이 각각 dependencies 이고, 어떤 것이 snapshot-dependencies 인지 구분할 수 없고, 인덱스를 읽어야 비로소 알 수 있다. 또한 로더 클래스(org/springframework/boot/loader/...)는 jar 스펙상 반드시 jar 루트에 있어야 java -jar 로 실행되는데, 도커 이미지에서는 루트로 존재하지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1360" data-origin-height="660"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJlmw0%2FdJMcacdhtNU%2FP4WrFsW2oYYKYa8TK5oKfK%2Fimg.png" onerror="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';" loading="lazy" width="1360" height="660" data-origin-width="1360" data-origin-height="660"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 문제를 해결하려면 우리는 layers.idx 를 읽고, 파일을 레이어별로 분류하고, 최상위 폴더로 옮기고, 로더 클래스는 따로 챙겨야 한다. 이는 결국 unzip 과 mv 의 조합일 뿐이지만 매번 직접 짜기 번거롭다. 따라서 스프링 부트는 이 일을 jar 기능인 jarmode로 제공하게 되었다. &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;jarmode는 jar를 실행할 때 주는 시스템 프로퍼티로, 부트스트랩 코드가 애플리케이션 구동 대신 완전히 다른 일을 하게 한다. 예를 들면 레이어를 추출하는 일이다. 이 기능은 spring-boot-jarmode-tools jar가 담당하는데, 기본으로 함께 포함된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;jarmode의 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;다음과 같이 jarmode를 속성으로 주고 실행할 수 있다. 그러면 다음과 같이 실행 가능한 명령어들을 알려준다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;mangkyu@mangkyu-5b21 % java -Djarmode=tools -jar my-app.jar
Usage:
  java -Djarmode=tools -jar my-app.jar

Available commands:
  extract      Extract the contents from the jar
  list-layers  List layers from the jar that can be extracted
  help         Help about any command&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 다음과 같이 필요한 명령어를 수행할 수 있다. 예를 들어 레이어 목록을 조회하면 다음과 같은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;mangkyu@mangkyu-5b21 % java -Djarmode=tools -jar my-app.jar list-layers
dependencies
spring-boot-loader
snapshot-dependencies
application&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;jar 파일을 각 레이어로 압축 해제해서 복사하는 dockerfile을 작성할 수 있는 것이다. 다음은 멀티 스테이지 dockerfile로, builder 스테이지가 나중에 필요한 폴더를 추출한다. 그리고 각 COPY 명령은 앞에서 나열한 레이어와 하나씩 대응된다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;# Perform the extraction in a separate builder container
FROM bellsoft/liberica-openjre-debian:25-cds AS builder
WORKDIR /builder

# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar

# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} application.jar

# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

# This is the runtime container
FROM bellsoft/liberica-openjre-debian:25-cds
WORKDIR /application

# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./

# Start the application jar - this is not the uber jar used by the builder
ENTRYPOINT ["java", "-jar", "application.jar"]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고로 과거에는 jarmode=layertools로 제공이 되었는데, 해당 모드는 Boot 3.3 에서 deprecated 되고 4.1.0(2026-06-10 GA)에서 제거되었다. &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;다만 tools는 layertools의 이름만 바꾼 것이 아니라, 추출 결과의 구조 자체가 다르다. layertools는 실제로 mv 명령어로 재배치만 한 것이므로, 각 레이어 안에 BOOT-INF/lib/, BOOT-INF/classes/ 같은 원래 경로를 그대로 유지했다. 그래서 COPY로 겹쳐 쌓으면 원래 fat jar 구조가 복원되었고, 내부에 jar 파일이 없기 때문에 JarLauncher를 직접 지목해 실행해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1440" data-origin-height="880"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDwVHR%2FdJMcadQJTp5%2FPkb16VhN1saiYSJ4fnlEp0%2Fimg.png" onerror="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';" loading="lazy" width="1440" height="880" data-origin-width="1440" data-origin-height="880"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;반면 tools는 의존성을 lib/ 아래로 평탄화하고, application 레이어에는 클래스 파일 대신 새로 만든 얇은 jar를 넣는다. 이 얇은 jar에는 애플리케이션 코드와 lib/를 가리키는 참조만 들어 있어서, 합쳐진 결과를 평범한 java -jar로 실행할 수 있다. 레이어로 나뉘지 않고 앱이름/앱이름.jar와 앱이름/lib/ 한 벌만 만들어지도록 한 것은, 이 레이아웃이 CDS와 AOT 캐시에 유리하기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1440" data-origin-height="880"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png" data-phocus="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FezMQVa%2FdJMb991Zixp%2FneopyNvLJ4eNwKr1ZRy1ok%2Fimg.png" onerror="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';" loading="lazy" width="1440" height="880" data-origin-width="1440" data-origin-height="880"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;--layer 옵션을 주면 추출 결과를 레이어별 폴더로 나눠 담을 수 있다. 또한 --launcher 옵션도 존재한다. 이 옵션을 주고 사용하면 이의 layertools와 완전히 동일한 구조로 추출이 가능하다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ Layered Jar의 동작 방식 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;전반적인 Layered Jar의 동작 방식을 빌드 시점부터 따라가보자.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 Spring Boot 2.4부터 레이어링이 기본으로 켜져 있어서, 계층을 커스터마이징 할 것이 아니라면 build.gradle.kts 에 layered { } 를 선언하는 등의 추가적인 작업이 필요 없다. BootJar 태스크가 기본 네 개 레이어를 알아서 만든다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그 결과 산출물 jar 안에 BOOT-INF/layers.idx 가 들어간다. 인덱스 파일의 위치는 매니페스트의 Spring-Boot-Layers-Index 에 적히고, jarmode 는 이 값을 보고 인덱스를 찾는다. 앞에서 본 것처럼 jar의 파일 구조 자체는 평범한 fat jar 그대로이고, 늘어난 것은 이 인덱스 파일 하나뿐이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이미지를 빌드할 때는 builder 스테이지가 jarmode=tools extract --layers --launcher 로 jar를 푼다. --layers 가 인덱스를 읽어 레이어별 폴더로 나누고, --launcher 가 엔트리를 원본 경로 그대로 꺼낸다. 그다음 폴더마다 COPY 를 한 줄씩 쓴다. COPY 한 줄이 이미지 레이어 하나이므로 네 줄이면 레이어가 넷이 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그렇게 이미지 레이어가 만들어진다. 아래일수록 덜 바뀌는 것이 깔리고, 맨 위 application 만 배포마다 새로 만들어진다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;dependencies 172.3MB — 버전을 올리기 전까지 그대로다&lt;/li&gt;
&lt;li&gt;spring-boot-loader 0.6MB — 부트 버전을 올릴 때만 바뀐다&lt;/li&gt;
&lt;li&gt;snapshot-dependencies 0MB — 스냅샷 의존성을 쓰지 않아 비어 있다&lt;/li&gt;
&lt;li&gt;application 1.8MB — 배포마다 새로 생성된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;프로젝트 의존성, 즉 멀티모듈 산출물은 기본적으로 application 레이어에 들어간다. 그래서 배포마다 새로 올라가는 것은 이 1.8MB뿐이고, 172.3MB짜리 dependencies 는 캐시에서 그대로 재사용된다. fat jar를 통째로 넣던 방식과 갈리는 지점이 여기다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1120" data-origin-height="800"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png" data-phocus="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnZh5c%2FdJMcacj1BRI%2FKNV3O5ugu5Yjk5Qg0hm7ak%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="800" data-origin-width="1120" data-origin-height="800"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고 자료&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;a href="https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1"&gt;https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3"&gt;https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-boot/docs/3.1.3/reference/html/container-images.html"&gt;https://docs.spring.io/spring-boot/docs/3.1.3/reference/html/container-images.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://mangkyu.tistory.com/474</id>
    <link href="https://mangkyu.tistory.com/474"/>
    <summary type="html">&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="color: #f15f5f;"&gt;1.&lt;span&gt;&lt;span&gt; 도커 이미지 빌드 최적화를 위한 Layered Jar &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style5" /&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span&gt;&lt;b&gt;[ 기존 Fat JAR 방식의 문제와 Layered Jar의 등장 ]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;기존 Fat JAR 방식의 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;Layered Jar란 스프링 부트 애플리케이션을 위한 Docker 이미지를 만들 수 있도록 도와주는 도구이다. 표준 jar 스펙은 jar 내부에 jar 파일을 가질 수 없다. 하지만 애플리케이션을 개발하다 보면 많은 외부의 프레임워크와 라이브러리를 사용하게 되어 다른 Jar가 필요해진다. 따라서 스프링 부트는 jar 내부에 다른 jar를 가지면서 java -jar로 실행할 수 있는 "fat jar" 형식을 예전부터 지원해 왔다. 이러한 fat jar 내부 구성은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1120" data-origin-height="720"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png" data-phocus="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/K1bok/dJMcaal9Xal/B1wEombeASshwS4cVgOl0K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK1bok%2FdJMcaal9Xal%2FB1wEombeASshwS4cVgOl0K%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="720" data-origin-width="1120" data-origin-height="720"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이러한 fat jar를 도커 이미지로 만드는 것은 어렵지 않다. 하지만 fat jar 그대로 도커 이미지로 복사해 사용하면 비효율적인 부분이 많다. 대개 변경되는 것은 사용자 코드인데, 상대적으로 변경이 적은 라이브러리/프레임워크 파일들까지 포함된 단일 jar 파일을 매번 새로운 도커 이미지로 만들어야 하기 때문이다. 따라서 사용자 애플리케이션 클래스와 라이브러리 모음을 분리시켜서, 변경된 사용자 애플리케이션 부분만 Docker 레이어에 갈아 끼우고 나머지는 캐시에서 사용하는 구조를 고안하게 되었다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;Layered Jar 방식의 등장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;앞서 설명하였듯 fat jar 형식은 jar 표준 기술이 아닌 스프링 부트만의 자체적인 포맷이다. 따라서 스프링 부트는 2.3부터 이 jar 내부에 레이어 인덱스 파일을 추가하는 기능을 넣었다. 여러 가지 변천사가 있었지만 지금은 jar 내부 구조를 fat jar와 동일하게 두되, BOOT-INF/layers.idx 라는 인덱스 파일 하나만 추가한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 파일은 우리가 직접 만드는 것은 아니고, Spring Boot Gradle 플러그인의 bootJar 태스크가 만든다. 스프링 부트 2.3 에서는 layered { enabled = true } 로 직접 활성화를 해야 했지만, 2.4 부터는 기본 활성화되어 아무 설정 없이도 기본 4개 레이어로 layers.idx 가 생성되고 MANIFEST 에 Spring-Boot-Layers-Index 가 선언된다. BootJar 태스크는 이 설정을 해석해 BOOT-INF/layers.idx 를 생성한다. 이 파일에는 레이어 목록과 각 레이어에 속하는 경로가 적혀 있고, 목록의 순서가 곧 이미지에 쌓을 순서다. 기본 레이어는 다음과 같다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;dependencies: 외부 프레임워크/라이브러리 의존성 모음&lt;/li&gt;
&lt;li&gt;spring-boot-loader: org/springframework/boot/loader 아래의 모든 코드&lt;/li&gt;
&lt;li&gt;snapshot-dependencies: 버전에 SNAPSHOT 이 붙은 의존성&lt;/li&gt;
&lt;li&gt;application: 사용자 애플리케이션 클래스와 리소스 및 프로젝트 의존성 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1120" data-origin-height="720"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png" data-phocus="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bEgsqw/dJMcaaGEqra/zorcURedatK6wG0vNFgYOk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEgsqw%2FdJMcaaGEqra%2FzorcURedatK6wG0vNFgYOk%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="720" data-origin-width="1120" data-origin-height="720"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 구성은 애플리케이션 빌드 시의 변경 빈도에 따라 구분되었다. 라이브러리 코드는 변경이 적으니 레이어를 분리시켜 캐시에서 재사용하게 했다. 반면 애플리케이션 코드는 빌드마다 바뀔 가능성이 크다. 따라서 기존에는 매번 하나의 app.jar를 도커 이미지에 푸시했다면, 이제는 변경이 있는 애플리케이션 부분만 푸시하면 되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1120" data-origin-height="740"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png" data-phocus="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/puEJV/dJMcaal9Xm5/KrRacOcxTz5Odu3Eqekmj0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpuEJV%2FdJMcaal9Xm5%2FKrRacOcxTz5Odu3Eqekmj0%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="740" data-origin-width="1120" data-origin-height="740"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ jarmode에 대하여 ]&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;jarmode의 등장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;스프링 부트가 나눈 레이어를 도커 이미지로 만들려면 결국 dockerfile을 써야 한다. 그런데 Docker의 규칙과 스프링 부트의 jar 방식 사이에 호환되지 않는 부분이 있다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;dockerfile에서 이미지 레이어를 만드는 단위는 COPY 명령 한 줄이다. COPY 를 네 번 쓰면 레이어가 네 개 생긴다. 따라서 레이어를 넷으로 나누려면, 이미지를 빌드하는 시점에 디스크 위에 각 4계층의 최상위 폴더가 네 개 있어야 한다. 그래서 각 폴더를 한 번씩 COPY 하는 것이다.&lt;/p&gt;
&lt;pre class="dockerfile"&gt;&lt;code&gt;dependencies/           &amp;lt;- COPY 한 번 = 이미지 레이어 하나
spring-boot-loader/     &amp;lt;- COPY 한 번
snapshot-dependencies/  &amp;lt;- COPY 한 번
application/            &amp;lt;- COPY 한 번
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그런데 jar는 내부적으로 fat jar 구조이므로, 그냥 jar 압축을 풀면 도커 이미지 레이어와 호환되지 않는 폴더 구조가 나온다.&lt;/p&gt;
&lt;pre class="crystal"&gt;&lt;code&gt;org/springframework/boot/loader/...
BOOT-INF/classes/...
BOOT-INF/lib/library1.jar
BOOT-INF/lib/lib3-SNAPSHOT.jar
BOOT-INF/layers.idx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;따라서 파일 이름만 봐서는 해당 디렉토리의 어떤 파일이 각각 dependencies 이고, 어떤 것이 snapshot-dependencies 인지 구분할 수 없고, 인덱스를 읽어야 비로소 알 수 있다. 또한 로더 클래스(org/springframework/boot/loader/...)는 jar 스펙상 반드시 jar 루트에 있어야 java -jar 로 실행되는데, 도커 이미지에서는 루트로 존재하지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1360" data-origin-height="660"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bJlmw0/dJMcacdhtNU/P4WrFsW2oYYKYa8TK5oKfK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJlmw0%2FdJMcacdhtNU%2FP4WrFsW2oYYKYa8TK5oKfK%2Fimg.png" onerror="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';" loading="lazy" width="1360" height="660" data-origin-width="1360" data-origin-height="660"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 문제를 해결하려면 우리는 layers.idx 를 읽고, 파일을 레이어별로 분류하고, 최상위 폴더로 옮기고, 로더 클래스는 따로 챙겨야 한다. 이는 결국 unzip 과 mv 의 조합일 뿐이지만 매번 직접 짜기 번거롭다.&amp;nbsp;따라서 스프링 부트는 이 일을 jar 기능인 jarmode로 제공하게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;jarmode는 jar를 실행할 때 주는 시스템 프로퍼티로, 부트스트랩 코드가 애플리케이션 구동 대신 완전히 다른 일을 하게 한다. 예를 들면 레이어를 추출하는 일이다. 이 기능은 spring-boot-jarmode-tools jar가 담당하는데, 기본으로 함께 포함된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;b&gt;jarmode의 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;다음과 같이 jarmode를 속성으로 주고 실행할 수 있다. 그러면 다음과 같이 실행 가능한 명령어들을 알려준다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;mangkyu@mangkyu-5b21 % java -Djarmode=tools -jar my-app.jar
Usage:
  java -Djarmode=tools -jar my-app.jar

Available commands:
  extract      Extract the contents from the jar
  list-layers  List layers from the jar that can be extracted
  help         Help about any command&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 다음과 같이 필요한 명령어를 수행할 수 있다. 예를 들어 레이어 목록을 조회하면 다음과 같은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;mangkyu@mangkyu-5b21 % java -Djarmode=tools -jar my-app.jar list-layers
dependencies
spring-boot-loader
snapshot-dependencies
application&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;jar 파일을 각 레이어로 압축 해제해서 복사하는 dockerfile을 작성할 수 있는 것이다. 다음은 멀티 스테이지 dockerfile로, builder 스테이지가 나중에 필요한 폴더를 추출한다. 그리고 각 COPY 명령은 앞에서 나열한 레이어와 하나씩 대응된다.&lt;/p&gt;
&lt;pre class="bash" data-ke-language="bash"&gt;&lt;code&gt;# Perform the extraction in a separate builder container
FROM bellsoft/liberica-openjre-debian:25-cds AS builder
WORKDIR /builder

# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar

# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} application.jar

# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

# This is the runtime container
FROM bellsoft/liberica-openjre-debian:25-cds
WORKDIR /application

# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./

# Start the application jar - this is not the uber jar used by the builder
ENTRYPOINT ["java", "-jar", "application.jar"]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고로 과거에는 jarmode=layertools로 제공이 되었는데, 해당 모드는 Boot 3.3 에서 deprecated 되고 4.1.0(2026-06-10 GA)에서 제거되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;다만 tools는 layertools의 이름만 바꾼 것이 아니라, 추출 결과의 구조 자체가 다르다. layertools는 실제로 mv 명령어로 재배치만 한 것이므로, 각 레이어 안에 BOOT-INF/lib/, BOOT-INF/classes/ 같은 원래 경로를 그대로 유지했다. 그래서 COPY로 겹쳐 쌓으면 원래 fat jar 구조가 복원되었고, 내부에 jar 파일이 없기 때문에 JarLauncher를 직접 지목해 실행해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1440" data-origin-height="880"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bDwVHR/dJMcadQJTp5/Pkb16VhN1saiYSJ4fnlEp0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDwVHR%2FdJMcadQJTp5%2FPkb16VhN1saiYSJ4fnlEp0%2Fimg.png" onerror="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';" loading="lazy" width="1440" height="880" data-origin-width="1440" data-origin-height="880"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;반면 tools는 의존성을 lib/ 아래로 평탄화하고, application 레이어에는 클래스 파일 대신 새로 만든 얇은 jar를 넣는다. 이 얇은 jar에는 애플리케이션 코드와 lib/를 가리키는 참조만 들어 있어서, 합쳐진 결과를 평범한 java -jar로 실행할 수 있다. 레이어로 나뉘지 않고 앱이름/앱이름.jar와 앱이름/lib/ 한 벌만 만들어지도록 한 것은, 이 레이아웃이 CDS와 AOT 캐시에 유리하기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1440" data-origin-height="880"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png" data-phocus="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/ezMQVa/dJMb991Zixp/neopyNvLJ4eNwKr1ZRy1ok/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FezMQVa%2FdJMb991Zixp%2FneopyNvLJ4eNwKr1ZRy1ok%2Fimg.png" onerror="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';" loading="lazy" width="1440" height="880" data-origin-width="1440" data-origin-height="880"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;--layer 옵션을 주면 추출 결과를 레이어별 폴더로 나눠 담을 수 있다. 또한 --launcher 옵션도 존재한다. 이 옵션을 주고 사용하면 이의 layertools와 완전히 동일한 구조로 추출이 가능하다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ Layered Jar의 동작 방식 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;전반적인 Layered Jar의 동작 방식을 빌드 시점부터 따라가보자.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 Spring Boot 2.4부터 레이어링이 기본으로 켜져 있어서, 계층을 커스터마이징 할 것이 아니라면 build.gradle.kts 에 layered { } 를 선언하는 등의 추가적인 작업이 필요 없다. BootJar 태스크가 기본 네 개 레이어를 알아서 만든다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그 결과 산출물 jar 안에 BOOT-INF/layers.idx 가 들어간다. 인덱스 파일의 위치는 매니페스트의 Spring-Boot-Layers-Index 에 적히고, jarmode 는 이 값을 보고 인덱스를 찾는다. 앞에서 본 것처럼 jar의 파일 구조 자체는 평범한 fat jar 그대로이고, 늘어난 것은 이 인덱스 파일 하나뿐이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이미지를 빌드할 때는 builder 스테이지가 jarmode=tools extract --layers --launcher 로 jar를 푼다. --layers 가 인덱스를 읽어 레이어별 폴더로 나누고, --launcher 가 엔트리를 원본 경로 그대로 꺼낸다. 그다음 폴더마다 COPY 를 한 줄씩 쓴다. COPY 한 줄이 이미지 레이어 하나이므로 네 줄이면 레이어가 넷이 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그렇게 이미지 레이어가 만들어진다. 아래일수록 덜 바뀌는 것이 깔리고, 맨 위 application 만 배포마다 새로 만들어진다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;dependencies 172.3MB &amp;mdash; 버전을 올리기 전까지 그대로다&lt;/li&gt;
&lt;li&gt;spring-boot-loader 0.6MB &amp;mdash; 부트 버전을 올릴 때만 바뀐다&lt;/li&gt;
&lt;li&gt;snapshot-dependencies 0MB &amp;mdash; 스냅샷 의존성을 쓰지 않아 비어 있다&lt;/li&gt;
&lt;li&gt;application 1.8MB &amp;mdash; 배포마다 새로 생성된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;프로젝트 의존성, 즉 멀티모듈 산출물은 기본적으로 application 레이어에 들어간다. 그래서 배포마다 새로 올라가는 것은 이 1.8MB뿐이고, 172.3MB짜리 dependencies 는 캐시에서 그대로 재사용된다. fat jar를 통째로 넣던 방식과 갈리는 지점이 여기다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1120" data-origin-height="800"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png" data-phocus="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/nZh5c/dJMcacj1BRI/KNV3O5ugu5Yjk5Qg0hm7ak/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnZh5c%2FdJMcacj1BRI%2FKNV3O5ugu5Yjk5Qg0hm7ak%2Fimg.png" onerror="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';" loading="lazy" width="1120" height="800" data-origin-width="1120" data-origin-height="800"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고 자료&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;a href="https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1"&gt;https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3"&gt;https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-boot/docs/3.1.3/reference/html/container-images.html"&gt;https://docs.spring.io/spring-boot/docs/3.1.3/reference/html/container-images.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>[SpringBoot] 도커 이미지 빌드 최적화를 위한 Layered Jar</title>
    <updated>2026-09-01T10:00:48+09:00</updated>
    <dc:date>2026-09-01T10:00:48+09:00</dc:date>
  </entry>
  <dc:date>2026-09-03T14:50:05+09:00</dc:date>
</feed>
