<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>과거의 나를 통해 미래의 나를 성장시키자</title>
    <link>https://chanho-study.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 6 Jul 2026 08:24:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>빨주노초잠만보</managingEditor>
    <image>
      <title>과거의 나를 통해 미래의 나를 성장시키자</title>
      <url>https://tistory1.daumcdn.net/tistory/5041344/attach/8fd5e84d35204e5e85935fa788fa368a</url>
      <link>https://chanho-study.tistory.com</link>
    </image>
    <item>
      <title>AI 원시인의 우당탕탕 AI Agent 개발 생존기</title>
      <link>https://chanho-study.tistory.com/195</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AI 시대에 개발 블로그에 관한 고찰&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 블로그를 쓰는 이유가 무엇일까? 나는 3년 동안 기술 블로그를 운영해 왔다. 그런데 최근 들어 글을 쓰는 일이 점점 뜸해졌다. AI가 다 해주는 &quot;대 딸깍의 시대&quot;에 기술 블로그가 여전히 의미가 있을까 하는 생각이 들었기 때문이다. 이런 생각이 반복되다 보니 자연스럽게 글을 쓸 동력도 줄어들었고 달에 한두 번 정도로 글을 쓰는 횟수가 줄었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zs2Ct/dJMcahY8czG/kpvOUEbxh7cnRxBiDx87Vk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zs2Ct/dJMcahY8czG/kpvOUEbxh7cnRxBiDx87Vk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zs2Ct/dJMcahY8czG/kpvOUEbxh7cnRxBiDx87Vk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZs2Ct%2FdJMcahY8czG%2FkpvOUEbxh7cnRxBiDx87Vk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;875&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 우테코에서 향로(이동욱)님의 강연을 들을 기회가 있었다. 놀랍게도 향로님은 강연을 시작하며 나와 비슷한 고민을 하고 있었다고 말씀하셨다. 지난 10년 동안 블로그를 운영해 왔지만 AI가 많은 것을 대신해 주는 시대에 기술 블로그를 계속 쓰는 일이 어떤 의미를 가지는지 고민했다는 이야기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강연이 끝난 뒤 용기 내어 1대 1로 질문을 드렸다. &quot;저는 3년 동안 기술 블로그를 운영해 왔는데, 최근에는 AI가 다 해주는 시대에 블로그를 계속 쓰는 게 어떤 의미가 있을지 고민이 됩니다. 향로님은 이런 시대에도 블로그를 계속 쓰는 이유가 무엇인가요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향로께선 블로그를 쓰는 이유가 본인에게 가장 잘 맞는 학습 방법이었으며 오로지 자기 자신을 위해서라고 하셨다. 듣고 보니 머리가 띵해짐... 내가 블로그를 쓰는 이유 또한 향로가 말씀하신 이유와 같았다. 나 또한 블로그가 가장 나에게 잘 맞는 학습 방법이었고 올바른 내용을 작성하기 위해 더 깊이 공부하게 되는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 블로그의 타이틀이 &quot;&lt;a style=&quot;background-color: #f4f4f6; color: #1e1f21; text-align: center;&quot; href=&quot;https://chanho-study.tistory.com/&quot;&gt;과거의 나를 통해 미래의 나를 성장시키자&quot;인&lt;/a&gt; 이유 또한 훗날 성장한 미래의 나라고 해서 과거의 나보다 항상 모든 것을 잘 알지 못하기 때문에 과거의 발자취를 통해 성장하자라는 이유에서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 나는 그 답을 알고 있었지만 기억 속에서 잊고 있었다. 최근 들어 AI 의존도가 높아지다 보니 이런 일들이 종종 발생하는 것 같다. 취준 하면서 기본기가 가장 중요하단 것을 알고 있었음에도 어느 순간 머릿속엔 AI를 더 공부해야 한다, 이제 이런거 공부해서 뭐 하지? AI가 다해주는데? 란 생각에 잠겨 기본적인 지식들을 멀리하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 향로는 본인이 직접 작성하신 글들을 마크다운으로 바꿔 AI의 지식베이스로 사용하신다고 한다(캬...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 글을 쓰는 방향을 자유롭게 바꿔보려 한다. AI로 인해 이제 블로그를 보는 사람도 줄어들었으니 철저히 개인 학습을 위한 공간으로 사용해야겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그래서 AI Agent는 왜 만들게 되었는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 원래 GPT나 클로드 데스크탑 앱을 사용해 간단한 질문을 주고받거나 프로젝트에서 발생하는 문제를 해결하는 정도로만 사용해 왔다. 그러다 보니 남들이 열광하는 OpenClaw, Hermes 같은 것들이 뭔지도 모르고 사용해 볼 생각조차 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 어느 날 카카오톡 오픈채팅방에서 AI Agent 스터디 모집글이 올라왔다. 모집글에서 말하는 LangGraph, LangChain이 뭐고 워크플로우 자동화는 또 뭔지 당최 아무것도 이해하지 못했지만 아주 작은 것 하나라도 얻어가고 싶다는 생각에 지원하게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mdRML/dJMcaf73z7F/SR9gpNqT4nisppa9Xok7U0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mdRML/dJMcaf73z7F/SR9gpNqT4nisppa9Xok7U0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mdRML/dJMcaf73z7F/SR9gpNqT4nisppa9Xok7U0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmdRML%2FdJMcaf73z7F%2FSR9gpNqT4nisppa9Xok7U0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;425&quot; height=&quot;921&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스터디 시작&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y5cb6/dJMcahdOha8/OfTiKeFXAp6uF3EK0v9q41/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y5cb6/dJMcahdOha8/OfTiKeFXAp6uF3EK0v9q41/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y5cb6/dJMcahdOha8/OfTiKeFXAp6uF3EK0v9q41/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY5cb6%2FdJMcahdOha8%2FOfTiKeFXAp6uF3EK0v9q41%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;283&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에 대해 너무 무지했기 때문에 심지어는 AI 에이전트가 뭔지도 모름... 민폐가 되진 않을까 걱정이 많이 컸지만 첫 대면 스터디부터 정말 정말 많은 것들을 배울 수 있었다. 가장 크게 느낀 것은 &lt;b&gt;Personal 한 AI 에이전트는 존재하지 않는다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리나 프레임워크는 특정한 기능을 하는데 특화돼 있어 그대로 사용할 수 있지만&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;AI 에이전트는 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;아무리 잘 만들더라도 사람마다 각자의 환경과 조건, 성향등 많은 것들이 다르기 때문에, 심지어 같은 프롬프트를 넣어도 사람마다 다른 결과가 나올 수 있기 때문에 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;남들이 만든 것을 그대로 사용하는 것이 불가능하다고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지금 하고 있는 사이드 프로젝트를 내가 직접 코드를 치지 않고 유지보수 할 수 있는 AI 에이전트를 만들기로 결정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;바퀴 재발명하기&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NeWky/dJMcag6ZRWs/HxEIkv3PGiBsZEDx0xQIL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NeWky/dJMcag6ZRWs/HxEIkv3PGiBsZEDx0xQIL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NeWky/dJMcag6ZRWs/HxEIkv3PGiBsZEDx0xQIL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNeWky%2FdJMcag6ZRWs%2FHxEIkv3PGiBsZEDx0xQIL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;247&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말한 이유도 있지만 가장 큰 이유는 바퀴 재발명이었다. Oh my Claude Code 같은 이미 잘 만들어진 유명한 오픈 소스들을 사용하는 방법도 있겠지만 AI 무지성 거인인 나에겐 너무 화려한 옷이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그래서 AI 에이전트가 뭔데?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 사용하다 보면 에이전트란 말을 굉장히 많이 쓴다. sub agent, multi agent 등등 여기저기 다 붙이던데... 여기저기서 쓰니 나도 이게 처음엔 굉장히 헷갈렸다. &lt;s&gt;뭐만 하면 다 에이전트래&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그냥 쉽게 이해하려고 내가 아는 유일한 에이전트인 축구 에이전트에 비유해서 이해했다. 축구 에이전트는 선수가 경기 자체에 집중할 수 있도록 주변 일을 대신 처리해 주는 사람이다. 정해진 절차에 따라 구단과의 계약을 협상하고, 이적 가능성을 알아보고, 스폰서나 일정 같은 외부 업무를 조율한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs5qme/dJMcaiw0xBU/NEngyghV8mkUeI6TVv4ty1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs5qme/dJMcaiw0xBU/NEngyghV8mkUeI6TVv4ty1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs5qme/dJMcaiw0xBU/NEngyghV8mkUeI6TVv4ty1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs5qme%2FdJMcaiw0xBU%2FNEngyghV8mkUeI6TVv4ty1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;365&quot; height=&quot;547&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트도 비슷하다. 우리가 AI에게 목표를 주면 AI 에이전트는 그 목표를 달성하기 위해 정해놓은 규칙에 따라 스스로 필요한 단계를 나누고 도구를 사용한다. 그럼 이 에이전트를 어떻게 만들어야 할까? 나는 이미 잘 만들어진 바퀴를 벤치마킹하기로 결정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Oh My Claude Code 뜯어보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/yeachan-heo/oh-my-claudecode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Oh My Claude Code&lt;/a&gt;를 선택한 이유는 AI 에이전트 스터디를 함께 하시는 혜림님께서 Oh My Claude code의 내부 구조를 해주셔서 오픈소스 에이전트 분석에 필요한 리소스를 줄일 수 있었기 때문이다(혜림님 감사합니다!!).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oh My Claude Code는 총 19개의 전문화된 sub agent로 구성되어 있으며 각 에이전트의 역할과 작업 난이도에 따라 서로 다른 모델을 사용하는 3 Tier 구조를 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/26SbW/dJMcaftuywH/vvr7fEfozAB8sNkK4EC8E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/26SbW/dJMcaftuywH/vvr7fEfozAB8sNkK4EC8E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/26SbW/dJMcaftuywH/vvr7fEfozAB8sNkK4EC8E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F26SbW%2FdJMcaftuywH%2Fvvr7fEfozAB8sNkK4EC8E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;392&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Tier 1 : `Haiku` 기반의 경량 탐색 계층으로, 빠른 코드 탐색과 초안 작성처럼 비용이 낮고 반복이 많은 작업 담당&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Tier 2 : `Sonnet` 기반의 표준 개발 계층으로, 대부분의 실제 구현과 검증 작업과 일반적인 개발 흐름의 중심 담당&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Tier 3 : `Opus` 기반의 고성능 판단 계층으로, 높은 추론 능력과 신중한 판단이 필요한 작업 담당&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 통해 단순 탐색은 저비용 모델로 빠르게 처리하고 일반 개발은 균형 잡힌 모델로 수행하며 중요한 설계와 리뷰는 고성능 모델에 맡겨 비용 효율성과 품질을 동시에 확보하며 사용자의 요청이 들어왔을 때 OMC 오케스트레이터를 사용해 적절한 에이전트에게 라우팅한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/37GJh/dJMcacDBfkH/dvOwyHse5zwAvexBUVNcxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/37GJh/dJMcacDBfkH/dvOwyHse5zwAvexBUVNcxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/37GJh/dJMcacDBfkH/dvOwyHse5zwAvexBUVNcxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F37GJh%2FdJMcacDBfkH%2FdvOwyHse5zwAvexBUVNcxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;480&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;멀티 에이전트 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 구조에서 아이디어를 착안해 `AGENTS.md` 파일을 오케스트레이터로 사용했는데, Codex가 작업 전에 반드시 읽는 파일이라는 특성을 이용해 모든 요청이 먼저 공통 정책과 라우팅 규칙을 통과하도록 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmXshe/dJMcafAhRJG/tqxg6zM0BPJpodVKB6OnP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmXshe/dJMcafAhRJG/tqxg6zM0BPJpodVKB6OnP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmXshe/dJMcafAhRJG/tqxg6zM0BPJpodVKB6OnP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmXshe%2FdJMcafAhRJG%2Ftqxg6zM0BPJpodVKB6OnP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;289&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 요청은 오케스트레이터에 의해 탐색, 분석, 계획, 테스트, 구현, 리뷰, Git 작업 중 어디에 해당하는지 분류되고 이후 해당 전문 에이전트에게 위임된다. 이를 통해 AI가 혼자 판단해 바로 코드를 수정하는 위험을 차단하고 워크플로우를 명확하게 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 각 에이전트들에 적절한 역할을 부여하였으며 필요한 경우 Skill로 분리하여 반복되는 요구사항을 전달하였다. 예를 들어 구현을 담당하는 에이전트에겐 팀 코딩 컨벤션을, 커밋과 PR 생성을 담당하는 에이전트에겐 각각 커밋과 PR 컨벤션을 설정하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNXP3M/dJMcaf1k5Xm/1CCpYiPipWovVtlMQYYZo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNXP3M/dJMcaf1k5Xm/1CCpYiPipWovVtlMQYYZo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNXP3M/dJMcaf1k5Xm/1CCpYiPipWovVtlMQYYZo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNXP3M%2FdJMcaf1k5Xm%2F1CCpYiPipWovVtlMQYYZo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;423&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개발 워크플로우 자동화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 평소 나의 개발 워크플로우를 AI가 그대로 재현하도록 구현해 봤다. 나는 다음과 같은 순서로 작업한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Github Issue 생성 -&amp;gt; 작업 브랜치 생성 -&amp;gt; 구현 -&amp;gt; 커밋 -&amp;gt; PR 생성&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 과정을 하나의 에이전트에게 모두 맡기려고 했다. 하지만 그렇게 하면 AI가 작업 범위를 정하고 코드를 수정한 뒤 커밋과 PR까지 멋대로 만들어 버릴 수 있다. 아무리 자동화가 편해도 눈을 잠깐 감았다 떴더니 PR이 생성돼 있는 건 조금 무섭다... 그래서 각 단계를 전문 에이전트의 역할로 분리하고 중요한 작업 사이에는 내가 직접 확인하는 승인 단계를 두었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;작업 준비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 작업 순서대로 새로운 기능이나 규모가 큰 작업은 먼저 GitHub Issue를 생성하고 작업 브랜치를 만든다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;계획&amp;nbsp;수립&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 시 계획 에이전트가 먼저 코드를 분석하고 구현 계획을 세운다. 어떤 파일을 수정해야 하는지, 기존 구조에 어떤 영향을 주는지, 어떤 테스트가 필요한지를 정리한 뒤 나에게 계획을 보여주고 내가 계획을 승인해야 다음 단계로 넘어간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 코드를 구현하는 단계보다 이 단계가 가장 중요하다. 이 단계에서 사람이 직접 꼼꼼히 검수해야 이후 코드를 구현했을 때 AI가 잘못 건드리는 부분도 줄어들고 원하는 결과물이 나올 확률이 증가한다. 실제로 함께 스터디를 하는 분께서 AI가 만든 작업 계획서(?)를 검토하는데 시간을 정말 많이 쓴다고 하신다. 때문에 사용자가 정확한 입력한 프롬프트를 입력하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 AI에게 매번 내 말 잘 이해했어 ? 라고 물어보는 것은 귀찮다. 따라서 AI가 프롬프트를 분석하고 모호한 부분을 분석해 다시 재질문하게 하는 소크라테스식 질문법을 적용한 오픈소스인 ouroboros 도입도 고려했으나 역으로 피로도가 상당해질 것 같아 반려했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;  소크라테스식 질문법&lt;/b&gt;&lt;br /&gt;AI에게 정답을 곧바로 요구하는 대신 AI가 반문과 질문을 통해 사용자의 사고를 확장하고 스스로 문제를 해결하도록 돕는 프롬프팅 기술 및 대화 방법론&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트&amp;nbsp;먼저&amp;nbsp;작성하기&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획이 승인되면 테스트 코드 에이전트가 구현보다 먼저 테스트를 작성한다. 테스트를 먼저 작성한 이유는 AI에게 성공의 기준을 명확하게 알려주기 위해서다. &quot;적당히 잘 구현해 줘&quot;가 아니라 &quot;이 테스트를 통과하는 상태를 만들어 줘&quot;라고 요구하면 에이전트가 엉뚱한 방향으로 달려갈 가능성을 줄일 수 있다. 테스트 코드 에이전트가 기대하는 동작이나 기존 문제를 재현하는 테스트를 만들면 해당 내용을 구현 에이전트에게 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현과&amp;nbsp;검증&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 에이전트는 승인된 계획과 테스트를 바탕으로 실제 코드를 수정하고 끝나면 관련 테스트와 코드 검사를 실행한다. 실패하면 원인을 확인하고 다시 수정하며 정해진 검증 기준을 통과할 때까지 이 과정을 반복한다. 현재 구현의 기본 흐름은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;planner &amp;rarr; 사용자 승인 &amp;rarr; tester &amp;rarr; implementer &amp;rarr; 검증&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 중요한 점은 에이전트가 혼자서 계획하고 구현하고 스스로 잘했다고 결론 내리지 않는다는 것이다. 계획과 테스트, 구현의 책임을 분리해 각 단계의 결과가 다음 에이전트의 입력이 되도록 만들었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;LLM Wiki 자동 동기화&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM Wiki는 간단히 말해 프로젝트의 지식 베이스를 저장하는 저장소다. 프로젝트의 구조, 도메인 용어, 코딩 규칙, 테스트 전략처럼 에이전트가 작업 전에 알아야 할 정보를 한 곳에 정리함으로써 일반적인 AI가 새로운 대화를 시작하면 이전 작업의 맥락을 알지 못한다는 문제점을 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 매번 직접 위키에 직접 내용을 작성하는 것이 아닌 작업 과정에서 새로운 도메인 정책이나 &quot;반복해서 사용할 규칙&quot;이 생겼다면 `wiki-maintainer`가 이를 LLM Wiki에 반영하거나 코드와 기존 문서의 내용이 달라졌다면 실제 코드를 기준으로 문서를 함께 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 AI에게 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&quot;반복해서 사용할 규칙&quot;이라고 전달하면 AI는 반복이란 말의 기준이 무엇일까? 하며 스스로 판단하에 작업한다. 이러한 문제를 해결하기 위해 캐시 히트처럼 히트 횟수를 기록해 2회 이상 반복될 경우 등재 후보에 오르게 되고 AI는 이에 대한 PR을 작성하고 사람이&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;직접 PR 리뷰를 통해 등재 여부를 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 AI가 코드를 작성하는 것에서 끝나는 것이 아니라 작업 중 얻은 지식을 다음 작업에서도 사용할 수 있도록 축적하는 구조를 만들었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커밋과&amp;nbsp;PR&amp;nbsp;생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 커밋 에이전트는 변경된 파일을 분석한 뒤 커밋 메시지를 생성한다. 이후 PR 에이전트가 변경 내용과 테스트 결과를 바탕으로 PR 제목과 본문을 작성하고 내용을 확인한 뒤 승인해면 브랜치를 push 하고 PR을 생성한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 내가 설계한 AI 에이전트를 소개해봤다. AI Agent가 뭔지도 모르던 시절부터 에이전트를 직접 설계하며 더 나은 방식을 고민하며 다양한 개념들을 접했다. 최근에는 프롬프트를 매번 직접 작성하는 대신 AI가 목표를 달성할 때까지 작업과 검증을 반복하는 시스템을 설계하는 것을 루프 엔지니어링(Loop Engineering)&quot;이 유행한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 에이전트 역시 완전한 자동 루프는 아니지만 방향은 비슷하다. 사용자가 모든 과정을 일일이 지시하지 않아도 워크플로우가 정해진 규칙에 따라 이어지고 중요한 결정이 필요할 때만 사용자에게 승인을 요청한다. 결국 중요한 것은 좋은 프롬프트 하나를 작성하는 능력보다 AI가 어떤 순서로 일하고, 무엇을 기준으로 결과를 검증하며, 어느 순간 사람에게 판단을 넘겨야 하는지를 설계하는 일이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스 엔지니어링, 루프 엔지니어링 등등 수없이 빠르게 몰아치는 AI 파도에 휩쓸려 꼴까닥하는게 하는게 아닐까란 걱정이 들었는데 향로께서 이와 관련해 이마를 탁 치는 말씀을 해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;스프링이나 코틀린 같은 것들이 버전 업데이트 될 때마다 그걸 다 따라가지도 않는데 굳이 AI라고 다 따라갈 필요가 있을까요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말을 들으니 굳이 그 파도에 휩쓸릴 필요는 없다고 느꼈다. 컨텍스트, 하네스, 루프 엔지니어링이란 단어들도 그냥 유명한 개발자들이 만들어낸 단어일 뿐 나에게 필요 없다면 굳이 그걸 따라갈 필요가 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 얼마나 많이 알고 있느냐가 아니라 그 기술을 통해 내가 무엇을 배우고 어떤 문제를 해결했느냐일 것이다. 돌이켜보면 블로그도 AI 에이전트도 결국 같은 역할을 하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그는 과거의 내가 남긴 지식을 미래의 내가 다시 사용할 수 있게 해 주고, AI 에이전트는 내가 일하는 방식과 판단 기준을 대신 기억해 준다. 둘 다 누군가에게 보여주기 위한 결과물이라기보다 나를 더 잘 이해하고 성장시키기 위한 도구였다. AI가 코드도 글도 대신 만들어 주는 시대지만 무엇을 배우고 무엇을 남길지는 여전히 내가 결정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 모든 파도를 따라가지는 않겠지만 나에게 필요한 파도라면 직접 올라타 보고 그 과정은 글로 남겨보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만든 에이전트는 이 &lt;a href=&quot;https://github.com/chanho0908/Keepiluv-Agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;저장소&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1782312497272&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - chanho0908/Keepiluv-Agent: 저는 호모로맨스 안드레 카파시 코덱서 입니다.&quot; data-og-description=&quot;저는 호모로맨스 안드레 카파시 코덱서 입니다. Contribute to chanho0908/Keepiluv-Agent development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/chanho0908/Keepiluv-Agent&quot; data-og-url=&quot;https://github.com/chanho0908/Keepiluv-Agent&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXCW0t/dJMb8T99LnF/8yRk6TnaRtkGUXMT8JrplK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c2MWdf/dJMb8T99LnG/oToxATaOuuovxCBD42OGKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/chanho0908/Keepiluv-Agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/chanho0908/Keepiluv-Agent&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXCW0t/dJMb8T99LnF/8yRk6TnaRtkGUXMT8JrplK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c2MWdf/dJMb8T99LnG/oToxATaOuuovxCBD42OGKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - chanho0908/Keepiluv-Agent: 저는 호모로맨스 안드레 카파시 코덱서 입니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;저는 호모로맨스 안드레 카파시 코덱서 입니다. Contribute to chanho0908/Keepiluv-Agent development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 260.5px; top: 4058.86px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/195</guid>
      <comments>https://chanho-study.tistory.com/195#entry195comment</comments>
      <pubDate>Wed, 24 Jun 2026 21:30:25 +0900</pubDate>
    </item>
    <item>
      <title>MutableSharedFlow Internals</title>
      <link>https://chanho-study.tistory.com/194</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/calZmA/dJMcaa6K1xi/IO3d3uAyuK0YA6kpfy7Iok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/calZmA/dJMcaa6K1xi/IO3d3uAyuK0YA6kpfy7Iok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/calZmA/dJMcaa6K1xi/IO3d3uAyuK0YA6kpfy7Iok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcalZmA%2FdJMcaa6K1xi%2FIO3d3uAyuK0YA6kpfy7Iok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;515&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SharedFlow란 ?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`SharedFlow`는 이름 그대로 여러 구독자가 하나의 데이터 스트림을 공유할 수 있도록 설계된 `Flow`로, 방출된 값을 모든 구독자에게 전달하는(브로드 캐스팅) 방식으로 동작한다. 즉, 하나의 데이터 흐름을 여러 코루틴이 동시에 관찰할 수 있도록 설계된 Hot Flow다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SharedFlow의 특징&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`collect` 함수를 호출한 코루틴이 없어도 동작하며 명시적으로 종료시키지 않는 한 절대로 완료되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;구독자는 기본적으로 `collect` 함수가 호출된 이후에 방출된 값을 수집한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;`collect` 함수를 여러번 호출해도 하나의&amp;nbsp; 데이터 스트림이 유지된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특징을 가진 SharedFlow의 동작 방식을 시간의 흐름에 따라 시각적으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IuszA/dJMcaglpYFZ/GVCdgUBFukbhej0ZETLm90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IuszA/dJMcaglpYFZ/GVCdgUBFukbhej0ZETLm90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IuszA/dJMcaglpYFZ/GVCdgUBFukbhej0ZETLm90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIuszA%2FdJMcaglpYFZ%2FGVCdgUBFukbhej0ZETLm90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;311&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0이 방출되는 시점엔 구독자가 없지만 `SharedFlow`는 이와 상관없이 0을 정상적으로 방출한다. 이는 `SharedFlow`가 구독자 존재 여부와 관계없이 방출자가 값을 방출할 수 있는 `HotFlow`임을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 첫 번째 `collect` 함수가 호출되면 그 시점 이후에 방출된 값인 1,2,3이 수집돼 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 두 번째 `collect` 함수는 1이 방출되고 나서 호출되기 때문에 2와 3만을 수집한다. 이 처럼 SharedFlow는 수집이 시작된 이후에 방출된 값만을 수집할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 `SharedFlow` 데이터 스트림 구조를 살펴보자. `collect` 일시 중단 함수가 두 번 호출되었음에도 불구하고 `SharedFlow`는 하나의 데이터 스트림을 그대로 유지하는 것을 볼 수 있다. 이는 `SharedFlow`의 핵심적인 특징으로, 여러 구독자가 동일한 데이터 스트림을 공유한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SharedFlow 만들고 데이터 방출하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`SharedFlow` 인터페이스에는 캐시를 위한 `replayCache` 프로퍼티와 수집을 위한 `collect` 일시 중단 함수만 정의돼 있으며, 데이터를 방출하기 위한 함수는 존재하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1780454698072&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface SharedFlow&amp;lt;out T&amp;gt; : Flow&amp;lt;T&amp;gt; {
	...
    public val replayCache: List&amp;lt;T&amp;gt;
	...
    override suspend fun collect(collector: FlowCollector&amp;lt;T&amp;gt;): Nothing
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 데이터의 방출하는 역할과 수집하는 역할을 명확히 분리하기 위한 설계 때문이다. `SharedFlow`는 수집만 가능한 불변 인터페이스이며, 데이터를 방출하려면 `SharedFlow`를 확장하는 가변 인터페이스인 `MutableSharedFlow`를 사용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1780454812402&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MutableSharedFlow&amp;lt;T&amp;gt; : SharedFlow&amp;lt;T&amp;gt;, FlowCollector&amp;lt;T&amp;gt; {
	override suspend fun emit(value: T)
    public fun tryEmit(value: T): Boolean
}

public fun &amp;lt;T&amp;gt; MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`MutableSharedFlow`는 `emit` 일시 중단 함수와 `tryEmit` 함수를 통해 데이터를 방출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`MutableSharedFlow` 객체를 생성하고 이를 변수에 할당한 후 데이터를 방출하고 처리하는 예제를 작성해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1780455059292&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow: MutableSharedFlow&amp;lt;String&amp;gt; = MutableSharedFlow()

  sharedFlow.emit(&quot;collect 전에 방출되는 값&quot;)

  launch {
    sharedFlow.collect { value -&amp;gt;
      println(&quot;$value 처리&quot;)
    }
  }

  delay(1000L) // 1000밀리초간 대기
  sharedFlow.emit(&quot;collect 후에 방출되는 값&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 순서대로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;`sharedFlow`에 대해 `emit(&quot;collect 전에 방출되는 값&quot;)`을 호출해 데이터를 방출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;`launch` 코루틴을 생성해 `sharedFlow`에서 방출된 값을 수집해 출력하도록 한다.&lt;/li&gt;
&lt;li&gt;`delay(1000L)`을 호출해 1000밀리 초 동안 대기한다.&lt;/li&gt;
&lt;li&gt;`sharedFlow.emit(&quot;collect 후에 방출되는 값&quot;)`을 호출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`sharedFlow`에 대해 `collect`를 호출한 시점 이후에 방출된 값만 `FlowCollector` 에 의해 처리되기 때문에 코드를 실행하면 다음과 같은 결과가 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1780455406063&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과 
Collect 후에 방출되는 값 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과에서 알 수 있듯이 collect 전에 방출되는 값은 수집되지 않고 collect 후에 방출되는 값만 `FlowCollector`에 의해 처리된다. 이처럼 `SharedFlow`에 대해 `collect` 함수를 호출하면 기본적으로 호출 이후에 방출된 값만 수집된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;replayCache&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`collect` 일시 중단 함수를 호출하기 이전에 방출된 값을 수집하기 위해 `SharedFlow`는 `replayCache`라는 프로퍼티를 제공하는데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;`replayCache`에는 `SharedFlow`에서 방출된 데이터가 저장된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780456108718&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface SharedFlow&amp;lt;out T&amp;gt; : Flow&amp;lt;T&amp;gt; {
    public val replayCache: List&amp;lt;T&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`replayCache`에 데이터가 저장된 이후에 `collect` 일시 중단 함수가 호출되면 이곳에 저장된 값들이 `FlowCollect`에 전달되어 `collect` 일시 중단 함수가 호출되기 전의 데이터를 처리할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`replayCache`의 크기는 `MutableSharedFlow`생성 시 `replay` 매개변수를 설정하면 된다. 예를 들어 `collect` 일시 중단 함수가 호출됐을 때 가장 최근에 방출된 데이터 한 개를 전달받고 싶다면 `replay = 1`을 입력하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780456438790&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val sharedFlow: MutableSharedFlow&amp;lt;Int&amp;gt; = MutableSharedFlow(replay = 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`replayCache`의 동작을 확인하기 위해 다음 코드를 살펴보자&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. replay = 1&lt;/h4&gt;
&lt;pre id=&quot;code_1780458991082&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow: MutableSharedFlow&amp;lt;Int&amp;gt; = MutableSharedFlow(replay = 1)

  launch {
    sharedFlow.emit(0) // 시작하자마자 0 방출 - replayCache: [0]
    delay(1000L)
    sharedFlow.emit(1) // 1000밀리초 후에 1 방출 - replayCache: [1]
    delay(1000L)
    sharedFlow.emit(2) // 2000밀리초 후에 2 방출 - replayCache: [2]
    delay(1000L)
    sharedFlow.emit(3) // 3000밀리초 후에 3 방출 - replayCache: [3]
  }

  launch {
    delay(500L)
    sharedFlow.collect { value -&amp;gt; // 500밀리초에 첫 collect 호출
      println(&quot;첫 번째 collect를 통해 수집된 값: $value&quot;)
    }
  }

  launch {
    delay(1500L)
    sharedFlow.collect { value -&amp;gt; // 1500밀리초에 둘째 collect 호출
      println(&quot;두 번째 collect를 통해 수집된 값: $value&quot;)
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;동작한다&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`MutablesharedFlow`의 `replayCache`의 크기가 1로 설정&lt;/li&gt;
&lt;li&gt;1000밀리 초 간격으로 0, 1, 2, 3을 순차적으로 방출&lt;/li&gt;
&lt;li&gt;500밀리 초 시점과 1500밀리 초 시점에 `MutableSharedFlow`에 대해 `collect` 일시 중단 함수 호출&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계를 하나씩 시각적으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q8Yx0/dJMcacQQfLs/Zxn1QLY6yIHXsRJaXDkCSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q8Yx0/dJMcacQQfLs/Zxn1QLY6yIHXsRJaXDkCSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q8Yx0/dJMcacQQfLs/Zxn1QLY6yIHXsRJaXDkCSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ8Yx0%2FdJMcacQQfLs%2FZxn1QLY6yIHXsRJaXDkCSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;715&quot; height=&quot;153&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`MutableSharedFlow`에서 각 값이 방출될 때마다 해당 값이 `replayCache`에 저장된다. 예제에서는 `replay`가 1로 설정돼 있으므로 캐시에는 항상 최근에 방출된 값 하나만 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 `[0]`이 방출되면 `replayCache`에는 `[0]`이 저장되고, 이후 1, 2, 3이 차례로 방출되면서 캐시는 각각 `[1] -&amp;gt; [2] -&amp;gt; [3]`이 저장되어 다음과 같이 출력된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780457218203&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
첫 번째 collect를 통해 수집된 값: 0
첫 번째 collect를 통해 수집된 값: 1
두 번째 collect를 통해 수집된 값: 1
첫 번째 collect를 통해 수집된 값: 2
두 번째 collect를 통해 수집된 값: 2
첫 번째 collect를 통해 수집된 값: 3
두 번째 collect를 통해 수집된 값: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 캐시가 모두 차면 가장 최근에 방출된 값이 저장되기 때문에 가장 최근에 방출된 값과 캐시에 저장된 값이 같아진다. 다음으로 `replay`를 2로 설정해 보자&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. replay = 2&lt;/h4&gt;
&lt;pre id=&quot;code_1780457275638&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow: MutableSharedFlow&amp;lt;Int&amp;gt; = MutableSharedFlow(replay = 2)

  launch {
    sharedFlow.emit(0) // 시작하자마자 0 방출 - replayCache: [0]
    delay(1000L)
    sharedFlow.emit(1) // 1000밀리초 후에 1 방출 - replayCache: [0, 1]
    delay(1000L)
    sharedFlow.emit(2) // 2000밀리초 후에 2 방출 - replayCache: [1, 2]
    delay(1000L)
    sharedFlow.emit(3) // 3000밀리초 후에 3 방출 - replayCache: [2, 3]
  }

  launch {
    delay(500L)
    sharedFlow.collect { value -&amp;gt; // 500밀리초에 첫 collect 호출
      println(&quot;첫 번째 collect를 통해 수집된 값: $value&quot;)
    }
  }

  launch {
    delay(1500L)
    sharedFlow.collect { value -&amp;gt; // 1500밀리초에 둘째 collect 호출
      println(&quot;두 번째 collect를 통해 수집된 값: $value&quot;)
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`replay=2`로 설정했으므로 `replayCache`에 저장될 수 있는 최대 원소의 수는 두 개가 되어 다음과 같이 첫 번째 collect와 두 번째 collect 모두 0, 1, 2, 3 전부를 수집하는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1780457676130&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
첫 번째 collect를 통해 수집된 값: 0
첫 번째 collect를 통해 수집된 값: 1
두 번째 collect를 통해 수집된 값: 0
두 번째 collect를 통해 수집된 값: 1
첫 번째 collect를 통해 수집된 값: 2
두 번째 collect를 통해 수집된 값: 2
첫 번째 collect를 통해 수집된 값: 3
두 번째 collect를 통해 수집된 값: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 타임라인을 시각화하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvsNBO/dJMcaip0OpC/wJmdwslmvykyPdOhIsKBOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvsNBO/dJMcaip0OpC/wJmdwslmvykyPdOhIsKBOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvsNBO/dJMcaip0OpC/wJmdwslmvykyPdOhIsKBOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvsNBO%2FdJMcaip0OpC%2FwJmdwslmvykyPdOhIsKBOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;636&quot; height=&quot;131&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replayCache에 어떤 값이 저장됐는지 직접 확인하고 싶다면 `sharedFlow`의 `replayCache`를 출력해 보 면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780458390026&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow: MutableSharedFlow&amp;lt;Int&amp;gt; = MutableSharedFlow(replay = 2)

  sharedFlow.emit(0)
  println(sharedFlow.replayCache) // [0] 출력
  delay(1000L)
  sharedFlow.emit(1)
  println(sharedFlow.replayCache) // [0, 1] 출력
  delay(1000L)
  sharedFlow.emit(2)
  println(sharedFlow.replayCache) // [1, 2] 출력
  delay(1000L)
  sharedFlow.emit(3)
  println(sharedFlow.replayCache) // [2, 3] 출력
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;완료되지 않는 SharedFlow&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 만든 코드들을 실행하면 프로세스가 종료되지 않고 계속 실행 상태에 머문다. 이는 `SharedFlow`가 완료되지 않고 데이터를 무한히 방출하는 `Flow`이기 때문이다. `SharedFlow`의 수집을 중단하고 싶다면 `SharedFlow` 자체를 종료하는 것이 아니라 수집 중인 코루틴을 취소해야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1780458568998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow: MutableSharedFlow&amp;lt;String&amp;gt; = MutableSharedFlow()

  val collectJob = launch {
    sharedFlow.collect { value -&amp;gt;
      println(&quot;MutableSharedFlow에서 방출된 값: $value&quot;)
    }
  }

  delay(1000L)
  collectJob.cancel() // launch 코루틴 취소
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;extraBufferCapacity&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`MutableSharedFlow`의 버퍼 크기는 `replay`와 `extraBufferCapacity`을 더해 결정된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780460755133&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public fun &amp;lt;T&amp;gt; MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
){
	val bufferCapacity0 = replay + extraBufferCapacity
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음과 같이 `replay`를 0으로, `extraBufferCapacity`를 1로 두고 코드를 실행해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1780460994100&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val startTime = System.currentTimeMillis()

  // 총 버퍼의 크기 1
  val sharedFlow = MutableSharedFlow&amp;lt;String&amp;gt;(
    replay = 0,
    extraBufferCapacity = 1
  )

  launch {
    sharedFlow.collect { value -&amp;gt;
      delay(1000L) // 처리에 1000밀리초 걸림
      println(&quot;[${getElapsedTime(startTime)}] $value 처리 완료&quot;)
    }
  }

  yield()

  println(&quot;[${getElapsedTime(startTime)}] 데이터1 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터1&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터1 방출 완료&quot;)

  println(&quot;[${getElapsedTime(startTime)}] 데이터2 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터2&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터2 방출 완료&quot;)

  println(&quot;[${getElapsedTime(startTime)}] 데이터3 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터3&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터3 방출 완료&quot;)
}

fun getElapsedTime(startTime: Long): String =
  &quot;지난 시간: ${System.currentTimeMillis() - startTime}밀리초&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 데이터2는 버퍼에 저장되기 때문에 데이터 1의 처리 완료를 기다리지 않고 방출 완료된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780462272136&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
[지난 시간: 0밀리초] 데이터1 방출 시작
[지난 시간: 0밀리초] 데이터1 방출 완료
[지난 시간: 0밀리초] 데이터2 방출 시작
[지난 시간: 0밀리초] 데이터2 방출 완료 // 데이터 1의 처리 완료를 기다리지 않고 방출 완료됨
[지난 시간: 0밀리초] 데이터3 방출 시작
[지난 시간: 1000밀리초] 데이터1 처리 완료
[지난 시간: 1000밀리초] 데이터3 방출 완료
[지난 시간: 2000밀리초] 데이터2 처리 완료
[지난 시간: 3000밀리초] 데이터3 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 `replay=1, extraBufferCapacity=1`로 두고 코드를 실행하면 버퍼의 크기가 2가 되기 때문에 모든 데이터가 지연없이 방출되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1780462506187&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
[지난 시간: 6밀리초] 데이터1 방출 시작
[지난 시간: 11밀리초] 데이터1 방출 완료
[지난 시간: 11밀리초] 데이터2 방출 시작
[지난 시간: 11밀리초] 데이터2 방출 완료
[지난 시간: 11밀리초] 데이터3 방출 시작
[지난 시간: 14밀리초] 데이터3 방출 완료
[지난 시간: 1018밀리초] 데이터1 처리 완료
[지난 시간: 2025밀리초] 데이터2 처리 완료
[지난 시간: 3028밀리초] 데이터3 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`replay`와 `extraBufferCapacity`는 둘 다 `SharedFlow`의 버퍼 공간에 영향을 주지만 그 목적이 다르다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 132px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8449%;&quot;&gt;대상&lt;/td&gt;
&lt;td style=&quot;width: 41.124%;&quot;&gt;replay&lt;/td&gt;
&lt;td style=&quot;width: 44.031%;&quot;&gt;extraBufferCapacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.8449%; height: 20px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 20px;&quot;&gt;이전 값을 전달하기 위한 캐시&lt;/td&gt;
&lt;td style=&quot;width: 44.031%; height: 20px;&quot;&gt;소비 속도가 느린 구독자를 위한 버퍼 공간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.8449%; height: 20px;&quot;&gt;대상&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;새 구독자&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.031%; height: 20px;&quot;&gt;기존 구독자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 14.8449%; height: 20px;&quot;&gt;저장되는 값&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 20px;&quot;&gt;구독 전에 방출된 최근 값&lt;/td&gt;
&lt;td style=&quot;width: 44.031%; height: 20px;&quot;&gt;구독자가 처리하지 못한 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 14.8449%; height: 18px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 18px;&quot;&gt;`replay = 1`이면 새 구독자는 최근 값 1개를 즉시 받음&lt;/td&gt;
&lt;td style=&quot;width: 44.031%; height: 18px;&quot;&gt;`extraBufferCapacity = 1`이면 수집자가 바빠도 값 1개를 추가로 저장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 남은 것은 방출자가 버퍼에 값을 저장할 수 없는 상황에서 어떤 일이 일어나는지이다. `MutableSharedFlow`는 값을 방출하기 위해 `emit`과 `tryEmit` 함수를 제공한다. 두 함수 모두 값을 방출한다는 목적은 같지만 버퍼가&amp;nbsp;가득&amp;nbsp;찼거나&amp;nbsp;구독자가&amp;nbsp;값을&amp;nbsp;처리할&amp;nbsp;준비가&amp;nbsp;되지&amp;nbsp;않은&amp;nbsp;상황에서&amp;nbsp;동작&amp;nbsp;방식이&amp;nbsp;다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;emit vs tryEmit&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`emit`은 &lt;b&gt;일시 중단 함수&lt;/b&gt;로, 구독자가 있고 버퍼에 더 이상 값을 저장할 공간이 없다면 값을 방출할 수 있는 상태가 될 때까지 호출한 코루틴을 일시 중단하며 이후 구독자가 이전 값을 처리하거나 버퍼에 공간이 생기면 다시 재개되어 값을 방출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자가 없는 상황에서 `emit` 함수가 어떻게 동작하는지 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1780464055173&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val startTime = System.currentTimeMillis()

  val sharedFlow = MutableSharedFlow&amp;lt;String&amp;gt;(replay = 0, extraBufferCapacity = 0)

  println(&quot;[${getElapsedTime(startTime)}] 데이터1 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터1&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터1 방출 완료&quot;)

  println(&quot;[${getElapsedTime(startTime)}] 데이터2 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터2&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터2 방출 완료&quot;)

  println(&quot;[${getElapsedTime(startTime)}] 데이터3 방출 시작&quot;)
  sharedFlow.emit(&quot;데이터3&quot;)
  println(&quot;[${getElapsedTime(startTime)}] 데이터3 방출 완료&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 구독자를 위한 `replay` 캐시도 없고, 느린 구독자를 위한 `extraBufferCapacity` 공간도 없으며 `collect`를 호출하는 구독자도 없다. 하지만 실행해 보면 `emit`은 중단되지 않고 바로 완료된다.&lt;/p&gt;
&lt;pre id=&quot;code_1780464124257&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
[지난 시간: 0밀리초] 데이터1 방출 시작
[지난 시간: 1밀리초] 데이터1 방출 완료
[지난 시간: 1밀리초] 데이터2 방출 시작
[지난 시간: 1밀리초] 데이터2 방출 완료
[지난 시간: 1밀리초] 데이터3 방출 시작
[지난 시간: 1밀리초] 데이터3 방출 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`emit`은 기다릴 대상이 없으므로 바로 끝나지만 값을 받을 구독자도 없고 저장 공간도 없기 때문에 데이터는 유실된다. 그렇다면 구독자가 있는 상황에서는 어떨까? 구독자가 값을 처리하는 속도보다 방출 속도가 빠르면 `emit`은 계속 즉시 완료될 수 있을까? 이를 이해하려면 `emit`과 `tryEmit`의 차이를 살펴봐야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;tryEmit&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1780464558739&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MutableSharedFlow&amp;lt;T&amp;gt; : SharedFlow&amp;lt;T&amp;gt;, FlowCollector&amp;lt;T&amp;gt; {
	public fun tryEmit(value: T): Boolean
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`tryEmit`은 일반 함수로 값을 즉시 방출하고 성공 여부를 `Boolean` 으로 반환한다. 값을 바로 방출할 수 있으면 `true`, 버퍼가 가득 차 있어 즉시 방출할 수 없으면 `false`를 반환한다. `tryEmit`이 `true`를 반환하는 대표적인 경우는 다음 두 가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구독자가 없는 경우&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;구독자가 있고 버퍼에 여유 공간이 있는 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;구독자가 없는 경우에는 값을 기다릴 대상이 없기 때문에 `tryEmit`은 성공한다. 다만 `replay`가 설정돼 있지 않다면 이 값은 저장되지 않고 사라진다. 즉, `true`가 반환됐다고 해서 반드시 어떤 구독자가 그 값을 수집했다는 의미는 아니다. 다음 예제를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780532881617&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow = MutableSharedFlow&amp;lt;String&amp;gt;(
    extraBufferCapacity = 1
  )

  println(&quot;구독자가 생기기 전 tryEmit: ${sharedFlow.tryEmit(&quot;이벤트&quot;)}&quot;)

  launch {
    sharedFlow.collect { value -&amp;gt;
      delay(1000L)
      println(&quot;$value 처리 완료&quot;)
    }
  }

  yield()

  println(&quot;구독자가 생긴 후 첫 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터1&quot;)}&quot;)
  delay(100L)

  println(&quot;구독자가 생긴 후 두 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터2&quot;)}&quot;)
  delay(100L)

  println(&quot;구독자가 생긴 후 세 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터3&quot;)}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같이 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`extraBufferCapacity`를 1로 설정한 `SharedFlow`생성&lt;/li&gt;
&lt;li&gt;구독자가 생기기 전에 `tryEmit`을 통해 데이터를 방출해 그 결과를 출력&lt;/li&gt;
&lt;li&gt;각 데이터를 처리하는 데 1000밀리초가 걸리는 구독자를 하나 생성하고 100밀리초 간격으로 `tryEmit`을 통해 데이터를 방출&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1780532964870&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
구독자가 생기기 전 tryEmit: true
구독자가 생긴 후 첫 번째 tryEmit: true
구독자가 생긴 후 두 번째 tryEmit: true
구독자가 생긴 후 세 번째 tryEmit: false
데이터1 처리 완료
데이터2 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구독자가 생기기 전에 호출한 `tryEmit`: 구독자가 없으므로 성공&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;구독자가 생긴 뒤 첫 번째 `tryEmit` : 구독자에게 곧바로 전달될 수 있으므로 성공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;두 번째 `tryEmit`: 구독자가 아직 첫 번째 값을 처리 중이지만 `extraBufferCapacity`가 1이므로 버퍼에 저장되어 성공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;세 번째 `tryEmit` : 호출하는 시점에는 버퍼에 이미 `데이터2`가 들어 있으므로 더 이상 값을 저장할 공간이 없어 즉시 실패하고 `false`를 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 상황에서 `emit`을 사용했다면 세 번째 값은 실패하지 않고 버퍼에 공간이 생길 때까지 코루틴이 일시 중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 `emit`과 `tryEmit`을 구분하는 핵심이다. 따라서 반드시 방출되어야 하는 값이라면 `emit`을, 값의 유실을 감수할 수 있다면 `tryEmit`을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 주의할 점은 버퍼를 설정하지 않은 `MutableSharedFlow`에서의 `tryEmit` 동작이다. 기본 설정의 `MutableSharedFlow`는 `replay = 0`, `extraBufferCapacity = 0`이므로 구독자가 있는 상태에서는 값을 임시로 저장할 공간이 없다. 따라서 `tryEmit` 함수를 사용할 때는 반드시 버퍼를 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼를 설정하지 않은 상태에서 구독자가 있는 `MutableSharedFlow`에 `tryEmit` 함수를 호출하면 항상 방출에 실패한다. 이를&amp;nbsp;확인하기&amp;nbsp;위해&amp;nbsp;다음&amp;nbsp;코드를&amp;nbsp;살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1780533857320&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
  val sharedFlow = MutableSharedFlow&amp;lt;String&amp;gt;()

  launch {
    sharedFlow.collect { value -&amp;gt;
      delay(1000L) // 처리에 1000밀리초 걸림
      println(&quot;$value 처리 완료&quot;)
    }
  }

  yield()

  println(&quot;구독자가 생긴 후 첫 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터1&quot;)}&quot;)
  delay(100L)
  println(&quot;구독자가 생긴 후 두 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터2&quot;)}&quot;)
  delay(100L)
  println(&quot;구독자가 생긴 후 세 번째 tryEmit: ${sharedFlow.tryEmit(&quot;데이터3&quot;)}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버퍼가 설정되지 않아 버퍼에 원소를 전달할 수 없어 방출이 실패해 모든 `tryEmit` 함수가 false를 반환하는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1780533998098&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
구독자가 생긴 후 첫 번째 tryEmit: false
구독자가 생긴 후 두 번째 tryEmit: false
구독자가 생긴 후 세 번째 tryEmit: false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000219882132&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center;&quot;&gt;코틀린 코루틴 리액티브 프로그래밍: Flow와 Channel&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt;ㄴㅇㄴㅇ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt;ㅇㄴㄷreplayCache&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 101px; top: 240.141px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <category>KOTLIN</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/194</guid>
      <comments>https://chanho-study.tistory.com/194#entry194comment</comments>
      <pubDate>Sun, 14 Jun 2026 11:58:30 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin Koog에 대해 알아보자</title>
      <link>https://chanho-study.tistory.com/193</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 5월 26일 오전 06_54_26.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJiO1u/dJMb997zWkS/Fa9Q8qYzmyfRQedAMMnVw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJiO1u/dJMb997zWkS/Fa9Q8qYzmyfRQedAMMnVw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJiO1u/dJMb997zWkS/Fa9Q8qYzmyfRQedAMMnVw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJiO1u%2FdJMb997zWkS%2FFa9Q8qYzmyfRQedAMMnVw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;485&quot; data-filename=&quot;ChatGPT Image 2026년 5월 26일 오전 06_54_26.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. Koog이란 무엇인가 ?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog는 JetBrains가 공개한 Kotlin 기반 AI Agent 프레임워크다. KotlinConf 2025에서 소개되었고 Python이나 JavaScript 중심으로 발전해 온 Agent 프레임워크 생태계 안에서 Kotlin/JVM 개발자도 자신이 익숙한 언어와 도구로 Agent를 만들 수 있게 하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에서는 Koog를 &lt;b&gt;JVM 생태계를 위해 설계된 오픈소스 AI Agent 프레임워크&lt;/b&gt;라고 설명한다. Kotlin 개발자에게는 타입 안정적인 Kotlin DSL을 제공하고, Java 개발자에게는 fluent builder 스타일 API를 제공한다. Kotlin Multiplatform을 활용하면 JVM뿐 아니라 JS, WasmJS, Android, iOS 타겟에서도 Agent를 배포할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog은 다음 문제를 해결하려는 프레임워크다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;LLM 호출 코드를 단순 API 래퍼 수준에서 끝내지 않고 Agent 실행 흐름으로 구조화한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Agent가 사용할 도구, 프롬프트, 전략, 메모리, 추적 기능을 Kotlin 코드 안에서 관리한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JVM 백엔드, Android, Kotlin Multiplatform 프로젝트에서 AI Agent를 자연스럽게 통합한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 왜 Kotlin에 Agent 프레임워크가 필요한가 ?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI Agent 생태계는 LangChain, LangGraph처럼 Python 중심으로 빠르게 성장했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JavaScript 진영에도 LangChain.js 같은 선택지가 있다. 하지만 Kotlin 개발자 입장에서는 기존 프레임워크를 쓰려면 언어의 경계를 넘나들어야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 Kotlin 백엔드에서 Agent 기능을 넣고 싶을 때 선택지는 대략 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Python Agent 서버를 따로 만들고 Kotlin 서비스에서 HTTP로 호출한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;LLM API를 직접 호출하며 Agent 루프, 도구 호출, 히스토리 관리를 직접 구현한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Java 생태계의 AI 라이브러리를 Kotlin에서 감싸서 사용한다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;셋 다 가능하지만, Kotlin스럽지는 않다. 특히 Agent는 단순히 &amp;ldquo;LLM에게 질문하고 답 받기&amp;rdquo;가 아니라 여러 단계의 작업을 실행하고, 도구를 호출하고, 실패를 복구하고, 실행 과정을 관찰해야 한다. 이 흐름이 애플리케이션 핵심 로직 안으로 들어올수록 타입 안정성, 테스트 가능성, 관측 가능성이 중요해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이러한 환경속에서 Koog은 AI Agent를 Kotlin/JVM 구성 요소로 다룰 수 있게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. Koog의 핵심 구성 요소&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.1 Agent&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog에서 Agent는 사용자의 입력을 받아 LLM과 상호작용하고 필요하면 도구를 호출한 뒤 최종 응답을 반환하는 실행 단위다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 기본적인 Agent는 미리 정의된 전략을 사용한다. 입력 문자열을 LLM에 전달하고, LLM이 도구 호출을 요청하면 Koog가 해당 도구를 실행한다. 도구 실행 결과는 다시 LLM에 전달되고, 더 이상 도구 호출이 필요 없을 때 최종 문자열 응답을 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val apiKey = System.getenv(&quot;OPENAI_API_KEY&quot;)
        ?: error(&quot;The API key is not set.&quot;)

    val agent = AIAgent(
        promptExecutor = simpleOpenAIExecutor(apiKey),
        llmModel = OpenAIModels.Chat.GPT4o
    )

    val result = agent.run(&quot;Hello! How can you help me?&quot;)
    println(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`promptExecutor`는 실제 LLM Provider와 통신하는 역할을 한다.&amp;nbsp; Koog는 &lt;a href=&quot;https://docs.koog.ai/llm-providers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LLM providers&lt;/a&gt; 문서에서 OpenAI, Anthropic, Google, DeepSeek, OpenRouter, Amazon Bedrock, Mistral, Alibaba, Ollama 등을 지원한다고 설명한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`llmModel`은 사용할 모델을 의미한다. 예제에서는 OpenAI의 GPT-4o 모델을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`agent.run()`은 Agent에게 작업을 실행시키는 부분이다. 단순한 질문이라면 바로 응답을 반환하지만 도구가 연결되어 있다면 중간에 도구를 호출하고 그 결과를 바탕으로 다시 응답을 생성할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.2 Tools&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;LLM은 파일을 읽거나, 데이터베이스를 조회하거나, 외부 API를 호출하거나, 사용자에게 질문하는 일은 직접 할 수 없다. 이러한 작업을 가능하게 만드는 것이 Tool이다. Koog에서는 Kotlin 함수에 `@Tool`과 설명을 붙여 Agent가 사용할 수 있는 도구로 등록할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이&amp;nbsp;내용은&amp;nbsp;&lt;a href=&quot;https://docs.koog.ai/tools-overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tools&amp;nbsp;overview&amp;nbsp;문서&lt;/a&gt;와&amp;nbsp;&lt;a href=&quot;https://docs.koog.ai/agents/basic-agents/#adjust-agent-iterations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Basic&amp;nbsp;agents&amp;nbsp;문서의&amp;nbsp;Tool&amp;nbsp;예시&lt;/a&gt;에서&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Tool
@LLMDescription(&quot;Ask the user a question by sending it to stdout and return the answer from stdin&quot;)
fun askUser(
    @LLMDescription(&quot;Question from the agent&quot;)
    question: String
): String {
    println(question)
    return readln()
}

val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(System.getenv(&quot;OPENAI_API_KEY&quot;)),
    llmModel = OpenAIModels.Chat.GPT4o,
    toolRegistry = ToolRegistry {
        tool(::askUser)
    }
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비유하자면 Tool은 Agent의 손과 발이다. 데이터베이스 조회, 외부 API 호출, 파일 처리 같은 작업을 Tool로 제공하면 LLM은 상황에 맞춰 어떤 도구를 호출할지 판단할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 여기서 조심해야 할 점도 있다. Tool은 Agent에게 권한을 주는 행위다. 읽기만 가능한 도구와 실제로 데이터를 변경하는 도구는 위험도가 다르다. 따라서 Tool을 설계할 때는 항상 권한 범위를 고려해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.3 Strategy&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Agent가&amp;nbsp;단순한&amp;nbsp;질문에&amp;nbsp;답하는&amp;nbsp;수준을&amp;nbsp;넘어&amp;nbsp;복잡한&amp;nbsp;작업을&amp;nbsp;수행하려면 구체적인 실행&amp;nbsp;흐름이&amp;nbsp;필요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog에서는 이 실행 흐름을 `Strategy`로 다룬다. &lt;a href=&quot;https://docs.koog.ai/agents/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에서는 여러 형태의 Agent 전략을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/agents/basic-agents/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Basic agent&lt;/a&gt;: 대부분의 간단한 사용 사례에 맞는 기본 실행 흐름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/agents/functional-agents/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Functional agent&lt;/a&gt;: Kotlin/Java 코드의 함수형 흐름으로 Agent 단계를 구성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/agents/graph-based-agents/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Graph-based agent&lt;/a&gt;: 노드와 엣지로 Agent 워크플로우를 정의&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/agents/planner-agents/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Planner agent&lt;/a&gt;: 목표 상태를 향해 계획을 세우고 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 흥미로운 부분은 Graph-based agent다. Agent 흐름을 그래프로 표현하면 각 단계를 명확히 나누고, 조건 분기와 재시도, 검증 루프를 모델링하기 좋아진다. 또한 실행 상태를 특정 지점에 저장하고 복구하는 persistence 기능과 잘 맞는다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.4 Memory, Compression, Persistence&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Agent가&amp;nbsp;한&amp;nbsp;번&amp;nbsp;답하고&amp;nbsp;끝나는&amp;nbsp;챗봇이라면&amp;nbsp;메모리의&amp;nbsp;중요성이&amp;nbsp;크지&amp;nbsp;않을&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;하지만&amp;nbsp;여러&amp;nbsp;단계를&amp;nbsp;거치는&amp;nbsp;작업이라면&amp;nbsp;이야기가&amp;nbsp;달라진다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;Agent가&amp;nbsp;PR을&amp;nbsp;분석하다가&amp;nbsp;중간에&amp;nbsp;실패했다고&amp;nbsp;생각해&amp;nbsp;보자.&amp;nbsp;처음부터&amp;nbsp;다시&amp;nbsp;모든&amp;nbsp;파일을&amp;nbsp;읽고,&amp;nbsp;다시&amp;nbsp;분석하고,&amp;nbsp;다시&amp;nbsp;판단해야&amp;nbsp;한다면&amp;nbsp;비용도&amp;nbsp;크고&amp;nbsp;사용자&amp;nbsp;경험도&amp;nbsp;좋지&amp;nbsp;않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog는 이런 문제를 해결하기 위해 다음 기능을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/history-compression/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;History compression&lt;/a&gt;: 긴&amp;nbsp;대화나&amp;nbsp;작업&amp;nbsp;기록을&amp;nbsp;압축해&amp;nbsp;토큰&amp;nbsp;사용량을&amp;nbsp;줄이는&amp;nbsp;기능이다.&amp;nbsp;Agent가&amp;nbsp;오래&amp;nbsp;실행될수록&amp;nbsp;대화&amp;nbsp;히스토리가&amp;nbsp;길어지는데,&amp;nbsp;이를&amp;nbsp;그대로&amp;nbsp;모델에&amp;nbsp;전달하면&amp;nbsp;비용이&amp;nbsp;커지고&amp;nbsp;컨텍스트&amp;nbsp;한계에&amp;nbsp;도달할&amp;nbsp;수&amp;nbsp;있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/features/chat-memory/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chat memory&lt;/a&gt; / &lt;a href=&quot;https://docs.koog.ai/features/long-term-memory/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;long-term memory&lt;/a&gt;: 세션 간 지식 유지나 검색 기반 응답을 지원한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/features/agent-persistence/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Agent persistence&lt;/a&gt;: 실행 상태를 저장해 장애 발생 후 특정 지점부터 복구한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.5 Observability&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;Agent 시스템은 일반 코드보다 디버깅이 어렵다. 같은 입력이어도 모델 응답이 달라질 수 있고, 도구 호출 순서도 달라질 수 있다. 그래서 실제 운영 환경에서는 &amp;ldquo;Agent가 왜 그렇게 행동했는가&amp;rdquo;를 추적하는 능력이 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 위해 Koog은 `tracing`과 `OpenTelemetry` 기능을 제공한다.&amp;nbsp; `OpenTelemetry`는 무슨 일이 일어났는지 추적하기 위한 관측 도구다. 하나의 사용자 요청이 시스템 안에서 지나간 전체 경로를 Trace라고 하고, 그 안에서 나뉘는 개별 실행 단계를 Span이라고 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 Agent가 사용자의 요청을 받고, LLM을 호출하고, Tool을 실행한 뒤 다시 응답을 생성했다면 이 전체 흐름은 하나의 Trace가 된다. 그리고 AIAgent 실행, LLM 호출, Tool 호출, 최종 응답 생성 같은 단계는 각각 Span으로 기록될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog는&amp;nbsp;&lt;a href=&quot;https://docs.koog.ai/features/open-telemetry/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenTelemetry&amp;nbsp;support&amp;nbsp;문서&lt;/a&gt;에서&amp;nbsp;Agent&amp;nbsp;실행,&amp;nbsp;노드&amp;nbsp;실행,&amp;nbsp;LLM&amp;nbsp;호출,&amp;nbsp;Tool&amp;nbsp;호출을&amp;nbsp;span으로&amp;nbsp;추적할&amp;nbsp;수&amp;nbsp;있다고&amp;nbsp;설명한다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;Agent가&amp;nbsp;어떤&amp;nbsp;LLM&amp;nbsp;호출을&amp;nbsp;했는지,&amp;nbsp;어떤&amp;nbsp;도구를&amp;nbsp;호출했는지,&amp;nbsp;어느&amp;nbsp;단계에서&amp;nbsp;시간이&amp;nbsp;오래&amp;nbsp;걸렸는지,&amp;nbsp;토큰과&amp;nbsp;비용이&amp;nbsp;얼마나&amp;nbsp;발생했는지&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. MCP와 A2A 지원&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Koog 문서에서 눈에 띄는 부분 중 하나는 &lt;a href=&quot;https://docs.koog.ai/model-context-protocol/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MCP(Model Context Protocol)&lt;/a&gt;와 &lt;a href=&quot;https://docs.koog.ai/a2a-protocol-overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;A2A(Agent2Agent) Protocol&lt;/a&gt; 지원이다. &lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MCP(Model Context Protocol) : Agent가 외부 도구와 컨텍스트에 접근하기 위한 프로토콜&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MCP는 Agent가 외부 도구와 컨텍스트에 접근하는 표준화된 방식으로 자리 잡고 있다. Koog에서 MCP 도구를 직접 Agent에 연결할 수 있기 때문에 MCP 생태계의 도구들을 활용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;A2A(Agent2Agent) : Agent 간 통신을 위한 프로토콜&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://discuss.pytorch.kr/t/koog-jetbrains-kotlin-ai/7013&quot;&gt;PyTorchKR: Koog, JetBrains의 새로운 Kotlin 기반 AI 에이전트 프레임워크&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://blog.jetbrains.com/ai/2025/05/meet-koog-empowering-kotlin-developers-to-build-ai-agents/&quot;&gt;JetBrains Blog: Meet Koog&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://blog.jetbrains.com/ai/2026/03/koog-comes-to-java/&quot;&gt;JetBrains Blog: Koog Comes to Java&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/&quot;&gt;Koog 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://docs.koog.ai/quickstart/&quot;&gt;Koog Quickstart&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/JetBrains/koog&quot;&gt;Koog GitHub Repository&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 125px; top: 23px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <category>KOTLIN</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/193</guid>
      <comments>https://chanho-study.tistory.com/193#entry193comment</comments>
      <pubDate>Mon, 25 May 2026 09:58:01 +0900</pubDate>
    </item>
    <item>
      <title>Compose SlotTable Internals</title>
      <link>https://chanho-study.tistory.com/192</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzCbJH/dJMcacpmKFv/Nek7Qf7LWkDpL9VxH3ro41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzCbJH/dJMcacpmKFv/Nek7Qf7LWkDpL9VxH3ro41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzCbJH/dJMcacpmKFv/Nek7Qf7LWkDpL9VxH3ro41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzCbJH%2FdJMcacpmKFv%2FNek7Qf7LWkDpL9VxH3ro41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;446&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SlotTable이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Jetpack Compose의 UI 트리를 저장하기 위한 자료구조&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 이 정의를 접하면 두 가지 의문이 생긴다. UI 트리가 무엇인지? 그리고 왜 이걸 별도로 저장해야 하는지다. 이에 관해 하나씩 알아보자&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose 코드를 작성하면 `Compose Compiler`는 이 코드를 해석해 결과적으로 다음과 같은 계층 구조를 만들어낸다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271772&quot; class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Column {
    Text(&quot;안녕&quot;)
    Row {
        Text(&quot;페토&quot;)
    }
}

// UI Tree
Column
├── Text(&quot;안녕&quot;)
└── Row
    └── Text(&quot;페토&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose는 상태가 바뀌면 해당 컴포저블을 다시 실행해 화면을 업데이트하며 이것을 리컴포지션(Recomposition)이라 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271773&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;var count by remember { mutableStateOf(0) }
Text(&quot;$count&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만약 위 코드에서 count가 바뀔 때마다 전체 UI를 처음부터 다시 그리면 어떻게 될까? 버튼 하나의 텍스트를 바꾸려고 화면 전체를 재렌더링한다면? 이는 불필요한 연산이 폭발적으로 늘어난다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose는 이러한 문제를 해결하기 위해 이전 실행 결과를 어딘가에 저장해두고 다음 리컴포지션 때 &quot;무엇이 바뀌었는지&quot;만 비교하는데, 그 저장소가 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SlotTable&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. SlotTable이 기억하는 것&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SlotTable은 세 가지를 기억한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;첫 번째, UI 트리 구조를 기억한다. 예를 들어 `Column` 안에 `Text`와 `Row`가 있고, `Row` 안에 `Icon`과 `Text`가 있다는 계층 관계 전체를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두 번째, `remember` API를 사용해 저장한 값을 기억한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;세 번째, 식별 정보를 기억한다. 각 컴포저블의 순서, 키, 속성 등 리컴포지션 시 동일한 컴포저블을 추적하기 위한 정보가 이에 해당한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한 가지 주목할 점은 각 컴포저블의 호출 순서를 저장한다는 점인데 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;위치 기억법(Positional Memoization)&lt;/b&gt;&lt;/span&gt;이라 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 위치 기억법(Positional Memoization)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 메모이제이션(Memoization)이란 함수가 동일한 입력에 대해 결과를 캐싱해 두고 같은 입력이 들어오면 다시 계산하지 않고 캐싱된 결과를 반환하는 기법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose는 메모이제이션에 소스 코드에서의 호출 위치를 식별자로 사용한다. 이러한 방식덕에 동일한 함수라도 다른 위치에서 호출되면 Compose Runtime은 이를 다른 노드로 취급한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271773&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun MyComposable() {
    Text(&quot;Hello&quot;) // id 1
    Text(&quot;Hello&quot;) // id 2
    Text(&quot;Hello&quot;) // id 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Text 컴포저블은 모두 동일한 함수에 동일한 입력이지만 호출 위치가 다르기 때문에 트리에서 각각 고유한 노드로 저장되며 이때 사용되는 핵심 메커니즘이 바로 `SlotTable`이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose Runtime은 Composable 함수의 정보를 `SlotTable`에 저장하며 호출 위치를 키로 사용한다. 자세한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://chanho-study.tistory.com/189&quot;&gt;이전글&lt;/a&gt;을 참고바란다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 저장 구조&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1777179271774&quot; class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Column {
    Text(&quot;안녕&quot;)
    Row {
        Text(&quot;페토&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시 코드를 토대로 `SlotTable`에 UI 트리가 저장되는 원리에 대해 이해해 보자. Composable들이 `SlotTable`에 실제로 저장되는 형태를 시각화하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 그림처럼 저장되는 이유는 `SlotTable`의 내부에 두 개의 배열이 존재하기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271774&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;internal class SlotTable : CompositionData, Iterable&amp;lt;CompositionGroup&amp;gt; {
    /**
     * 그룹 정보를 저장하는 배열
     */
    var groups = IntArray(0)
        private set

    /**
     * 각 그룹에 대한 슬롯 데이터를 저장하는 배열
     */
    var slots = Array&amp;lt;Any?&amp;gt;(0) { null }
        private set
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;`Groups: IntArray`&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;UI의 설계도에 해당하며 컴포저블 하나에 대한 메타데이터가 다음과 같이 연속된 5개의 정수로 표현된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1777179271774&quot; class=&quot;coq&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 그룹 레이아웃
//  0             | 1             | 2             | 3             | 4             |
//  Key           | Group info    | Parent anchor | Size          | Data anchor   |
private const val Group_Fields_Size = 5&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;`slots: Array&amp;lt;Any?&amp;gt;`&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 실제 데이터 저장소로 `remember`로 기억한 값, 상태(state) 객체, 람다, UI Node(LayoutNode) 등 컴포저블이 생성한 실제 데이터와 객체를 보관한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`groups` 배열은 Compose Runtime이 Slots 배열을 어떻게 해석해야 하는지 알려주는 역할을 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;일반적으로 객체에 필요한 상태를 캡슐화해 클래스를 단위로 저장하는 것과는 다르게 배열 안에 직접 펼쳐서 저장한 형태인 점이 특이한데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 설계는 객체 지향적인 특징보다 성능을 우선시하는 데이터 지향 설계(data-oriented design) 기법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조는 객체 할당을 완전히 배제하고 비트 연산을 수행하는 확장 함수를 통해 접근하므로 속도가 매우 빠르다. 예를 들어 특정 그룹이 UI 노드를 나타내는지 확인하려면 다음과 같이 비트 연산을 활용해 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271775&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private const val NodeBit_Mask = 0b0100_0000_0000_0000__0000_0000_0000_0000
private inline fun IntArray.isNode(address: Int) =
    this[address * Group_Fields_Size + GroupInfo_Offset] and NodeBit_Mask != 0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이러한 방식으로 Compose는 런타임에 수천 개의 그룹을 순회하더라고 GC 부담 없이 처리할 수 있다. 그룹 하나의 구성 요소를 대략적으로 표현하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271775&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;groups = [
  ...,
  1823,   // [0] Key        
  0b0100, // [1] Group info
  2,      // [2] Parent anchor
  1,      // [3] Size       
  3,      // [4] Data anchor
  ...
]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`Key` : 컴파일러가 호출 위치마다 부여한 고유 번호로 위치 메모이제이션을 위한 키&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;`Group info` : 나는 노드인가, 특별한 속성이 있는가? 같은 플래그 비트들&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;`Parent anchor` : 부모 컴포저블이 groups 배열 몇 번째에 있는지&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(e.g.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;내 부모는 groups 배열 2번 위치에 있다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;`Size` :&lt;span&gt;&amp;nbsp;&lt;/span&gt;내 자식 그룹의 개수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;`Data anchor` : Slots 배열 내에서 그룹의 슬롯 데이터가 시작되는 위치(e.g. 내가 `remember`로 저장한 값은 slots[3]에 있다)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1777179271776&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun SlotTableDemo(modifier: Modifier = Modifier) {
	val message = remember { &quot;정페토&quot; }
	Column(
        modifier = modifier
    ) {
        Text(text = &quot;안녕&quot;)
        Text(text = &quot;Hello&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`SlotTable`에 대한 정보는 컴파일 타임에 접근하여 출력할 수 없기 때문에 리플렉션을 사용해 런타임에 `SlotTable`의 정보를 구한 뒤 출력해 보면 실제로 다음과 같은 데이터가 나온다. 사용한 코드는 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/chanho0908/SlotTableTest&quot;&gt;저장소&lt;/a&gt;에서 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271776&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;========================================
SlotTable Structure
========================================
D  Total groups: 64
Groups array size: 320
========================================

--- 그룹 0 ---
Key          : 100
Group info   : 1
Parent anchor: -1
Size         : 64
Data anchor  : 0

--- 그룹 1 ---
Key          : -96456358
Group info   : 1
Parent anchor: 0
Size         : 63
Data anchor  : 1

... 

--- 그룹 5 ---
Key          : 125
Group info   : 1073741825
Parent anchor: 3
Size         : 59
Data anchor  : 5&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그룹 0의 `Parent anchor`가 -1이라는 것은 부모가 없다는 뜻이다. 즉 가장 루트에 있는 컴포저블을 의미하며 `Size`는 64개로 총 그룹 수와 동일하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그룹 5의 `Group Info`인 1073741825를 이진수로 변환한 뒤 `NodeBit_Mask`와 AND 연산을 수행하면 결과가 0이 아니므로 그룹 5에 저장된 데이터는 Node임을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271776&quot; class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Group5의 Group Info
1073741825 = 01000000_00000000_00000000_00000001

// SlotTable의 NodeBit_Mask
1073741824 = 01000000_00000000_00000000_00000000

AND 연산 (두 비트가 모두 1일 때만 1)
  01000000_00000000_00000000_00000001
&amp;amp; 01000000_00000000_00000000_00000000
= 01000000_00000000_00000000_00000000
= 1073741824&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`Data anchor`는 해당 그룹의 슬롯 데이터가 시작되는 위치를 가리킨다. 그룹 5의 경우 `slots[5]`부터 시작하며 `LayoutNode`와 `remember`값 등 여러 데이터가 순서대로 저장된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;실제로 Slot 배열 정보를 출력해 본 결과 `slots[5]`부터 `LayoutNode`가 들어있고 `remember`에 저장한 변수는 `slots[15]`에 있는 것을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SlotTable Read/Write&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞서 `SlotTable` 내부에 `groups`와 `slots` 배열이 있다는 걸 살펴봤다. 그런데 두 배열 모두 private set으로 선언되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271777&quot; class=&quot;haxe&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;var groups = IntArray(0)
    private set
var slots = Array&amp;lt;Any?&amp;gt;(0) { null }
    private set&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 Compose Runtime은 SlotTable을 어떻게 읽고 쓰는 걸까? 여기서 등장하는 것이 SlotReader와 SlotWriter다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SlotReader&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`SlotReader`는 `SlotTable`을 읽기 위한 커서다. Compose가 리컴포지션을 수행할 때 이전 상태를 확인하기 위해 사용하며 속도를 위해 배열에 대한 직접 참조를 보유하는 경량 객체다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271777&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;internal class SlotReader(internal val table: SlotTable) {
    private val groups: IntArray = table.groups
    private var slots: Array&amp;lt;Any?&amp;gt; = table.slots
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Reader는&amp;nbsp;여러&amp;nbsp;개가&amp;nbsp;동시에&amp;nbsp;열려&amp;nbsp;있어도&amp;nbsp;된다.&amp;nbsp;읽기만&amp;nbsp;하기&amp;nbsp;때문에&amp;nbsp;데이터가&amp;nbsp;변경될&amp;nbsp;위험이&amp;nbsp;없어&amp;nbsp;동시&amp;nbsp;접근이&amp;nbsp;안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SlotWriter&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`SlotWriter`는 `SlotTable`을 읽기 쓰기 할 때 사용하는 커서다. 최초 컴포지션이나 변경사항을 적용할 때 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271777&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;internal class SlotWriter(internal val table: SlotTable) {
    private var groups: IntArray = table.groups
    private var slots: Array&amp;lt;Any?&amp;gt; = table.slots
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한 번에 하나의 Writer만 활성화할 수 있으며, Writer가 활성화된 동안에는 Reader도 사용할 수 없다. 이러한 제약을 통해 트랜잭션 안전성(transactional safety)을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;잠금 메커니즘&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SlotTable은 openReader()와 openWriter() 메서드를 통해 접근을 직접 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271778&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun openReader(): SlotReader {
    if (writer) error(&quot;Cannot read while a writer is pending&quot;)
    readers++
    return SlotReader(table = this)
}

fun openWriter(): SlotWriter {
    runtimeCheck(!writer) { &quot;Cannot start a writer when another writer is pending&quot; }
    runtimeCheck(readers &amp;lt;= 0) { &quot;Cannot start a writer when a reader is pending&quot; }
    writer = true
    return SlotWriter(table = this)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Writer가 열려 있으면 Reader를 열 수 없고, Reader가 열려 있으면 Writer를 열 수 없다. 이 단순한 잠금 메커니즘 덕분에 `SlotTable`의 멀티 스레드 환경에서 데이터 정합성을 유지할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;성능의&amp;nbsp;비결:&amp;nbsp;갭&amp;nbsp;버퍼(Gap&amp;nbsp;Buffer)&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`SlotWriter`의 성능은 갭 버퍼(Gap Buffer) 덕분이다. Gap Buffer는 원래 텍스트 에디터에서 고안된 자료구조로 배열에서 삽입과 삭제를 효율적으로 수행하기 위해 설계되었다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gap Buffer&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;텍스트 에디터에서 고안한 자료구조로 문서에 글자를 입력하면 커서 위치에 글자가 삽입되는데 만약 이를 배열로 구현한다면 중간에 글자를 삽입할 때마다 뒷자리에 있는 모든 글자를 한 칸씩 밀어야 한다. 만약 100만 글자짜리 문서의 맨 앞에 글자를 추가하면 100만번 복사가 일어나는 셈이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`Gap Buffer`는 이를 해결하고자 배열 내부에 커서 위치에 맞춰 빈 공간을 추가한다. 다음 그림처럼 `Gap Buffer`는 커서 위치에 빈 공간을 두고 삽입이 발생하면 그 공간을 채우는 방식으로 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Compose의 Gap Buffer 활용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`SlotWriter`는 `groups`와 `slots` 두 배열 각각에 Gap Buffer를 적용한다. Gap의 현재 위치는 두 쌍의 변수로 관리된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271778&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private const val Group_Fields_Size = 5
private var groupGapStart: Int = table.groupsSize
private var groupGapLen: Int = groups.size / Group_Fields_Size - table.groupsSize

private var slotsGapStart: Int = table.slotsSize
private var slotsGapLen: Int = slots.size - table.slotsSize&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`groupGapStart`는 Gap이 시작하는 위치, `groupGapLen`은 Gap의 크기다. 새 그룹을 삽입하면 `groupGapStart` 위치에 데이터를 쓰고 `groupGapLen`을 줄인다. 배열을 밀어낼 필요가 없으므로 O(1)이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최초&amp;nbsp;컴포지션이&amp;nbsp;빠른&amp;nbsp;이유&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최초 컴포지션에서는 컴포저블이 항상 순차적으로 실행된다. 하향식으로 순차적으로 실행되기 때문에 Gap이 항상 다음 삽입 위치 바로 앞에 있다. 때문에 Gap을 이동시킬 필요가 없으므로 삽입 비용이 사실상 0에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777179271779&quot; class=&quot;arcade&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Column {        // 1번째 실행
    Text(&quot;안녕&quot;) // 2번째 실행
    Row {        // 3번째 실행
        Text(&quot;페토&quot;) // 4번째 실행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gap 이동 비용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제는 Gap이 있는 위치가 아닌 다른 위치에 삽입을 해야 할 때다. Gap을 목표 위치까지 먼저 이동 시키고 그 경로에 있는 모든 요소가 복사해야 하는데,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그룹이 1,000개인 컴포지션에서 500번째 위치에 삽입하면 500번의 복사가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이는 컴포지션이 커질수록 이 비용이 선형적으로 증가하는 구조적 한계가 있다. 참고로 이 역할은&lt;span&gt; `&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;moveGroupGapTo()` 함수가 담당한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이것이 최근 `SlotTable`이 `Gap Buffer`에서 `LinkBuffer`로 교체된 이유다. 드래그 앤 드롭, 조건부 렌더링처럼 트리 중간을 자주 변경하는 동적 UI에서는 Gap 이동 비용이 계속 누적된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose는 이 문제를 해결하기 위해 연결 리스트 기반의 `LinkBuffer`를 도입했으며 이에 대한 자세한 내용은 다음 글에서 다루겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오늘은 `SlotTable`의 내부 구조에 대해 알아봤다. 그 동안 내가 생각하던 구현 방식과는 다른 새로운 관점들이 많이 보였다. 특히 객체 지향의 이점을 버리고 원시타입 그대로 오직 &quot;5개의 그룹&quot;이라는 규칙을 통해 원시 타입을 그대로 사용함으로써 객체 생성 비용과 GC 오버헤드 같은 문제들을 최적화할 수 있다는 것도 처음 배우게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Jetpack Compose라는 도구를 사용해 단순히 화면을 그리는 것만이 아닌 새로운 설계 관점의 철학을 배울 수 있어 매우 값진 시간이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 119.5px; top: 23px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Compose Gap Buffer</category>
      <category>Compose SlotTable</category>
      <category>Gap Buffer</category>
      <category>SlotTable</category>
      <category>슬롯 테이블</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/192</guid>
      <comments>https://chanho-study.tistory.com/192#entry192comment</comments>
      <pubDate>Sun, 26 Apr 2026 13:55:14 +0900</pubDate>
    </item>
    <item>
      <title>2026 상반기 회고 : 번아웃</title>
      <link>https://chanho-study.tistory.com/190</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;올해 들어 첫 회고글을 언제 작성할지 참 고민이 많았다. 사실 회고글을 쓸 만큼 커다란 이벤트나 깨달음이 있어야 쓰게 되는데 너무나도 차가운 취업 시장으로 인해 겪은 여러 번의 좌절로 인해 글을 쓸 마음이 잘 들지 않았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;인생 첫 번아웃&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;작년 12월부터 시작해 어느덧 5개월 차에 접어든 취준으로 인해 정신적으로, 신체적으로 너무나 힘든 시간을 보내고 있다. 개발을 시작한 이례로 5년 만에 처음 번아웃이 온 것 같은데, 요즘 들어 무기력증을 너무나 크게 느끼고 있기 때문이다. 나는 아무것도 하지 않고 휴식을 취할 때 오히려 불안함을 느낀다. 밤을 새워 코딩할 때 즐겁고 새로운 문제를 마주할 때 희열을 느낀다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우테코가 끝나자마자 Yapp을 한 것도 단 한순간이라도 쉬면 안 된다고 생각했기 때문이다. 하지만 Yapp도 기수가 끝났고, 프로젝트가 여전히 진행 중이긴 하지만 특별히 피처를 개발하지 않기 때문에 많은 시간을 쏟지 않고 있다. 함께 개발한 페어 또한 취준으로 매우 바쁜 상황이라 코드 리뷰를 받기까지 몇 주의 시간이 걸려 별도의 리뷰 없이 셀프 머지를 하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;혹자는 그러면 취업 준비를 하면 되지 않냐고 말할 수 있다. 백번 맞는 말이다. 하지만 면접 준비도 세네 달 동안은 열심히 준비할 수 있었지만, 다섯 달째가 되니 너무 지치고 힘들더라...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그렇다 보니 하루하루가 너무 괴롭고 특히 밤이 너무 힘들었다. 밤을 새워 공부하거나 작업해야 마음이 편한데 아무것도 할 게 없으니 너무 괴롭고, 잠을 자려해도 잠이 오지 않았다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;현실을 도피하고 싶어서인지 점점 술에 의존하게 되어 술을 마시는 날이 많아지고 스트레스를 먹는 걸로 푸는 습관이 생겨 체중도 3킬로나 늘어나게 되었다. 흔히 말하는 취준 스트레스였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;1396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAxe9X/dJMcabYd99N/hBKROAQ6tXIQFwjOhNS0k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAxe9X/dJMcabYd99N/hBKROAQ6tXIQFwjOhNS0k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAxe9X/dJMcabYd99N/hBKROAQ6tXIQFwjOhNS0k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAxe9X%2FdJMcabYd99N%2FhBKROAQ6tXIQFwjOhNS0k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;623&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;1396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 살다가는 건강을 다 버릴 것 같아 평소 좋아하던 풋살을 다시 시작했고 새로운 취미로 러닝을 시작했다. 우테코를 시작하기 전엔 하루에 풋살을 두 탕 뛰거나 일주일에 최소 4번은 할 정도로 좋아했지만 우테코를 하면서 딱 한 번밖에 못했다. 그 한 번마저 다리를 다쳐 3주간 절름발이 신세를 졌다... 그렇다 보니 이전과는 다르게 체력이 쓰레기라고 생각할 정도로 너무너무 떨어져 있더라...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;한창 했을 땐 플랩 레벨도 아마 5까진 갔어서 나름 나쁘지 않은 실력이라고 생각했는데 요즘엔 두 단계나 떨어졌다. 예전 같았으면 어떻게든 레벨을 올리려고 이 악물고 했을 텐데 요즘엔 그냥 안 다치고 하는게 최고다. 괜히 다쳐서 운동도 못하게 되면 무기력증이 아니라 우울증이 걸릴 수준이니...&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;취준회고&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;지금까지 약 30개의 기업에 지원했고 5번의 서류 합격을 했다. 이 과정에서 노션 이력서부터 시작해 8번의 이력서 수정이 있었고 각 버전마다 이전 면접에서 느낀 것들을 토대로 업그레이드 시켜왔다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;1684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opNzm/dJMcaiiJjB1/sYJGV6vwfSoHpOTc2tAzBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opNzm/dJMcaiiJjB1/sYJGV6vwfSoHpOTc2tAzBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opNzm/dJMcaiiJjB1/sYJGV6vwfSoHpOTc2tAzBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopNzm%2FdJMcaiiJjB1%2FsYJGV6vwfSoHpOTc2tAzBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;485&quot; height=&quot;745&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;1684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;첫 면접을 보면서 개발자로서 내가 가진 기본기가 굉장히 부족한 것을 느꼈다. 면접 질문 중 &quot;코루틴은 어떻게 Context Switching을 최적화했나요?&quot;라는 질문을 받았는데, 이 질문을 통해 나는 단순히 코루틴은 경량 스레드다라는 것만 알았지 왜 경량 스레드인지는 알지 못했고 고민해 볼 생각조차도 못했다는 것을 깨달았다. 이후 Computer Science에 대한 공부를 시작했고 OS의 매력에 빠져들어 학부 시절엔 눈길도 주지 않았을 공룡책을 직접 찾아보기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;두 번째 면접은 화상으로 진행했는데 회사에 안드로이드 개발자가 모두 퇴사해 혼자 개발을 진행해야 했다. 특히 면접에서 어떤 기준을 가지고 나를 평가하는지 모를 질문들만 받아 내겐 맞지 않는 회사라고 생각했고 당연히 떨어지게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이때쯤부터 점점 마음이 조급해져 눈을 낮추고 아무 회사나 가자고 생각했다. 제이슨이 말씀해 주신 나만의 기준에 부합하는 회사를 찾고 싶었지만 현실은 그렇게 녹록지 않았다. 그래서 세 번째 면접을 본 회사는 나의 기준에 부합하지 않았거니와 면접 경험이 굉장히 별로였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;안드로이드 리드분의 면접 태도는 이 사람과 일하면 큰일 날 것 같은데 싶은 느낌을 줄 정도였다. 회사에선 Compose를 사용할 계획도 없으며 연봉도 3천만 원으로 매우 낮았지만 그럼에도 불구하고 취업이 간절했기에 최선을 다해 면접에 임했다. 결과적으로 2차 임원진(컬처핏) 면접까지 갔으나 떨어지게 되었다. 여담으로 몇 달 뒤에 이 회사에서 같은 공고가 올라왔지만 지원하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;네 번째 면접은 지난 5번의 면접 중 가장 아쉽고 후회되는 면접이었다. 면접을 통해 이 회사에서 일하고 싶다고 생각들 정도였으니. 그러나 역설적이게도 이 면접이 가장 최악의 컨디션으로 봤던 면접이었다. 진행 중인 프로젝트에서 내 태스크가 밀려 배포 일정이 밀리게 되어 면접 당일 새벽 4시까지 작업하느라 2시간 밖에 잠을 못 잤다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;면접을 가는 길에 너무 피곤해서 편의점에서 핫식스를 한 캔 원샷 때리고 면접에 들어갔으나 면접 과정에서 내가 생각하기에도 답변을 너무 못했다. 취업을 위해 프로젝트를 하는 건데 프로젝트를 하느라 면접 준비를 못하다니 정말 바보 같은 짓이었다... 감자 스터디 친구들 한태 이 이야기를 해주니 온갖 조롱이 돌아왔다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나도 알아 안다고... 흑흑 ,,,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;마지막 면접은 지금까지의 내 개발 가치관을 완전히 흔들었다. AI에 관한 질문을 많이 받았는데 그동안의 면접에선 한 번도 받아보지 못한 질문이었다. 이미 회사에선 개발자가 코드를 작성하지 않고 AI가 작성한다는데 그동안의 내 개발 가치관과는 정반대였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나는 순수한 내 실력 자체가 중요하다고 생각해 최근엔 AI를 지양해 왔다. 하지만 회사에서 더 이상 개발자가 코드를 작성하지 않는다는 말을 듣고 굉장히 큰 충격을 받았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 최근에는 반대로 프로젝트를 진행하며 내가 직접 코드를 작성하는 것을 지양하고 AI가 내가 원하는 대로 결과물을 만들어낼 수 있도록 만드는 연습을 하고 있는데 이게 요즘에 유행하는 하네스 엔지니어링이라더라.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ojxuS/dJMcaaLNolw/2YOlUYKMXn9K8dEwjBgCzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ojxuS/dJMcaaLNolw/2YOlUYKMXn9K8dEwjBgCzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ojxuS/dJMcaaLNolw/2YOlUYKMXn9K8dEwjBgCzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FojxuS%2FdJMcaaLNolw%2F2YOlUYKMXn9K8dEwjBgCzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;515&quot; height=&quot;363&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러면서 나만을 위한 부하 직원들을 만들게 되었다. SkyDoves님의 DoveLetter MCP를 활용해 코드 리뷰를 해주는 리뷰어, 실제 기능을 구현하는 직원, Macrobenchmark 같은 다양한 성능 측정 도구들을 활용해 성능을 분석해 주는 직원, 테스트 코드를 전문적으로 작성하는 직원, 작업 결과물을 토대로 PR을 만들어주는 직원 등등을 구성해 나만의 작은 팀을 꾸렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아직 말을 잘 안 듣는 신입 직원들이라 가르칠게 많지만 나름대로 AI를 활용하는 법을 깨우치고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVNmmt/dJMcaiwfOOb/kP0nnncZNUq5RIzFvUv0f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVNmmt/dJMcaiwfOOb/kP0nnncZNUq5RIzFvUv0f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVNmmt/dJMcaiwfOOb/kP0nnncZNUq5RIzFvUv0f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVNmmt%2FdJMcaiwfOOb%2FkP0nnncZNUq5RIzFvUv0f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;636&quot; height=&quot;281&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이후 현재 개발 시장의 AI 트렌드를 파악하기 위해 두 번의 AI 컨퍼런스를 참여했다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;선배 개발자분과 정말 많은 이야기를 나누며 깨달은 것이 하나 있다. 개발자는 더 이상 무언가를 만드는 사람이 아니다. 만드는 것은 AI의 역할이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그동안 내가 개발을 좋아하는 이유는 무언가를 만들고 문제를 해결해 나가는 과정에 있었다. 문제를 해결하기 위해 밤새 머리를 싸매고 고민하며 해결할 수 없는 문제를 해결했을 때 느껴지는 희열감, 그리고 이 작고 큰 경험들이 쌓여 성장해 나가는 나 자신을 보며 나의 목표, 나의 삶 그 자체인 개발을 너무나 사랑했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 이제는 무언가를 만드는 것도, 그 과정에서 문제를 해결하는 것도 AI는 비교할 수 없을 만큼 빠르게 수행해 낸다. 그렇다면 나는 무엇인가? 개발자는 이제 무엇을 하는 사람인가? 아니면 애초부터 내가 생각하고 꿈꾸던 개발자를 잘못 생각하고 있던 것일까?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;다시 일어나기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;이런 고민들로 인한 미래에 대한 불안감과 취준 스트레스로 점점 무기력증이 극에 달해갔을 무렵 우테코 6기 선배 올리브의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #0070d1; text-align: start;&quot; href=&quot;https://thdbs523.tistory.com/456&quot;&gt;글&lt;/a&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;을 보게 되었다. 올리브의 회고글을 읽고 정말 많은 깨달음을 얻고 다시 일어설 수 있게 되었다. 그중 가장 나의 마음을 울린 글이다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이대로 가다가는 장기적으로 건강한 취준을 할 수 없겠다는 생각이 들어, 의식적으로 나를 위한 하루를 보내기 시작했다. 하루동안은 노트북을 펼치지 않고, 일찍 일어나 장을 보러 나갔다. 스스로를 위해 좋아하는 음식을 정성스레 만들었고, 방을 깨끗이 청소하며 복잡한 마음도 다잡으려 노력했다. 천천히 다이어리를 쓰면서 내 감정을 들여다봤다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;취준에 몰입하다 보면 시간적/금전적/심리적 여유가 줄어들어서, 좋아하는 취미나 여가를 온전히 즐기지 못할 때가 많다. 그러다 보면 '취업만 하면 하고 싶은 걸 마음껏 할 수 있겠지'라는 생각을 하게 되고, 자연스레 행복을 미래로 계속 유예하게 된다. 하지만 미래에도 또 다른 어려움이 있을 것이고, 그렇게 미래만 바라보다 보면 지금의 나를 잃어버릴 것 같았다. 의식적으로 나를 돌보는 하루를 보내며, 현재를 온전히 받아들이고 즐기는 사람이 되려 했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;이 글에서 전하는 이야기는 나의 이야기와 너무나 같았다. 나 또한 취준을 떠나 정상적인 생활을 할 수 없어지고 있는 지경이었으니,,, 나는 재작년부터 시작해 올해로 2년이 넘는 시간 동안 안드로이드 개발을 공부해 왔다. 매번 부트캠프를 들을 때마다 도저히 알바와는 병행할 수 없는 스케줄로 인해 금전적인 압박을&amp;nbsp; 받아왔고 이는 심리적인 압박으로도 이어져왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;사실 취업이 급한 가장 큰 이유도 금전적인 압박 때문이었다. 그러다 보니 취업을 하면 이 금적적인 압박에서도 벗어나고 심리적으로도 편안한 삶을 살 수 있을 것이라 생각했다. 하지만 올리브의 글을 읽고 나니 이 또한 막연한 미래에 대한 잘못된 환상이란 것을 깨닫게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;또한 지금의 나는 나를 위한 삶을 살고 있지 않다는 것을 깨달았다. 늘 나를 벼랑 끝에 있는 사람이라고 되새김하며 열심히 하지 않으면 안 된다고, 당장 일어나 앞으로 달리라고 스스로를 채찍질하기만 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;그래서 잠시 온전히 나를 위한 삶을 살아보기로 했다. 하루는 아침 러닝 저녁 풋살을 했다. 덕분에 종아리와 체력이 탈탈 털렸지만 오랜만에 하루를 운동으로 채우니 너무 즐거웠다. 나.. 생각보다 아직 죽지 않았을지도?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o2Y8L/dJMcag6kg3n/3YIkxFaAyZKCEQKZdmk7R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o2Y8L/dJMcag6kg3n/3YIkxFaAyZKCEQKZdmk7R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o2Y8L/dJMcag6kg3n/3YIkxFaAyZKCEQKZdmk7R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo2Y8L%2FdJMcag6kg3n%2F3YIkxFaAyZKCEQKZdmk7R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;310&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;또 하루는 평소 가고 싶었던 라멘집을 갔다. 평소 유튜브에서 즐겨보던 라멘 괴인 웅성과 카라미의 영상에서 자주 본 연남동의 소바하우스 멘야준이라는 가게였다. 인생 첫 쇼유라멘이었는데 기대했던 것만큼 너무너무 맛있는 한 끼였다. 영상에서만 보던 멘야준 사장님도 실제로 보니 연예인을 보는듯한 기분이었다.&lt;/span&gt;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;location&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;a href=&quot;https://map.daum.net/?latlng=126.92258810853387,37.56000543985191&amp;amp;q=%EC%86%8C%EB%B0%94%ED%95%98%EC%9A%B0%EC%8A%A4%20%EB%A9%98%EC%95%BC%EC%A4%80&amp;amp;itemId=212961126&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt; &lt;span class=&quot;location-info&quot;&gt; &lt;span class=&quot;location-name&quot;&gt;소바하우스 멘야준&lt;/span&gt; &lt;span class=&quot;location-address&quot;&gt;서울 마포구 월드컵북로6길 84&lt;/span&gt; &lt;/span&gt; &lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2026-04-22-23-11-31 001.jpeg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buWYQS/dJMcagSNryW/6quNdB0ALs8EqcyQySSjik/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buWYQS/dJMcagSNryW/6quNdB0ALs8EqcyQySSjik/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buWYQS/dJMcagSNryW/6quNdB0ALs8EqcyQySSjik/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuWYQS%2FdJMcagSNryW%2F6quNdB0ALs8EqcyQySSjik%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;665&quot; data-filename=&quot;KakaoTalk_Photo_2026-04-22-23-11-31 001.jpeg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저녁엔 하쿠텐이라는 가게를 가고 싶었지만 다이어트 중인 관계로 다음을 기약했다. 식사 후엔 홍대의 서브 컬처 굿즈샵들을 구경하며 돌아다녔다. 그런데 이게 무슨 일인가? 무려 MAPPA 팝업 스토어를 하는 것이 아닌가???!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mzJI2/dJMcajaRZiK/j0mmyOUqCw07ivFAeBAkLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mzJI2/dJMcajaRZiK/j0mmyOUqCw07ivFAeBAkLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mzJI2/dJMcajaRZiK/j0mmyOUqCw07ivFAeBAkLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmzJI2%2FdJMcajaRZiK%2Fj0mmyOUqCw07ivFAeBAkLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;432&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 온전히 나를 위한 날을 보내고 나니 한츰 머리가 맑아지고 앞으로의 계획을 세우게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년에&amp;nbsp;이루고&amp;nbsp;싶은&amp;nbsp;것들&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;좋은 면접관 되기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뜬금없이 면접관이라니? 나는 IT 연합 동아리 Yapp에서 28기 운영진 및 Android 파트 리드를 맡게 되었다. 때문에 이번 기수 채용을 위한 서류 검토 및 면접을 하게 되었는데(방구석 숨 고르기 청년인 내가 동아리에선 면접관?) 지원자로 참여했을 때 보다 몇 백배는 더 부담이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 지원자일 땐 나만을 대표하면 되지만 면접관이 된 이상 어쩌면 면접을 보는 그 순간만큼은 내가 동아리를 대표한다고 생각하기 때문이다. 내가 회사 면접에서 면접관의 불성실한 태도로 인해 일하고 싶지 않은 회사로 생각했던 것처럼 내 행동에 따라 면접관이 이 동아리를 오고 싶지 않을 수도 있기 때문이다. 따라서 지난 8번의 동아리 면접 경험을 바탕으로 면접자에게 좋은 경험이 될 수 있도록 많이 고민해 봐야겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 연합 동아리 면접 중 메시업을 봤을 때 가장 편안하고 재밌었기 때문에 이때의 경험을 살려보려 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컨퍼런스 개최&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 우테코 선배이신 산군께서 AI를 메인 주제로 컨퍼런스를 주최하셨고 감사하게도 운영진으로 참여하게 되었다. AI에 관심 있는 사람들이 모여 서로의 경험과 인사이트를 나눌 수 있는 오프라인 밋업으로 SYNC (Show Your New C-Worker) 라고 이름 지었다. 구성원은 나, 산군, 코비, 악어, 베르로 모두 우테코 모바일 안드로이드를 수료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇하나 없이 처음부터 맨땅에 헤딩하며 빌드업하고 있기 때문에 너무 재미있고 설레면서도 한편으론 과연 사람들이 관심 있어할지, 제대로 행사를 개최할 수 있을지 걱정이 많지만 열심히 준비해서 많은 사람들에게 좋은 경험을 주는 것이 목표다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GDG 행사에 Write 권한 요청하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번&amp;nbsp;Read&amp;nbsp;Only로&amp;nbsp;참여하던&amp;nbsp;GDG&amp;nbsp;행사에&amp;nbsp;Write&amp;nbsp;권한을&amp;nbsp;요청드려볼&amp;nbsp;생각이다.&amp;nbsp;커피&amp;nbsp;심부름,&amp;nbsp;뒷정리,&amp;nbsp;쓰레기&amp;nbsp;청소 같은&amp;nbsp;사소한&amp;nbsp;일도&amp;nbsp;좋다.&amp;nbsp;어떤&amp;nbsp;일도&amp;nbsp;하는&amp;nbsp;Agent가&amp;nbsp;되어&amp;nbsp;용기 내볼&amp;nbsp;생각이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Only인 코틀린의 val 키워드처럼 그동안은 내가 기여할 부분은 없다고 생각했지만, deepCopy()를 사용하면 기존 운영진 분들의 리스트에 영향을 주지 않고 내가 낄 수 있지 않겠는가 하하하&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;나 가꾸기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;건강한 신체에 건강한 정신이 깃든다&quot;는 말처럼 건강한 신체를 유지해야 취준이라는 언제 끝날지 모르는 긴 터널을 지치지 않고 달려 나갈 수 있다. 운동은 목표가 있어야 열심히 하게 되므로 목표를 정해 보면 좋을 것 같은데, 러닝 목표는 하프 마라톤 정도로 잡으면 좋을 것 같다. 진짜 마라톤을 나간다는 건 아니고 21km 달리는 걸 목표로 잡아봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;술...도 줄이긴 해야하는데 이건 진짜 여간 쉽지 않은 것 같다. 다만 지금처럼 현실을 도피하기 위한 도피처로 삼는 일은 절대 금지해야겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;취업&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0keGg/dJMcajaR0HS/Ktva3uKiosYVh3sfQK5RV0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0keGg/dJMcajaR0HS/Ktva3uKiosYVh3sfQK5RV0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0keGg/dJMcajaR0HS/Ktva3uKiosYVh3sfQK5RV0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0keGg%2FdJMcajaR0HS%2FKtva3uKiosYVh3sfQK5RV0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;255&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 200.5px; top: 1480.25px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/190</guid>
      <comments>https://chanho-study.tistory.com/190#entry190comment</comments>
      <pubDate>Thu, 23 Apr 2026 12:31:48 +0900</pubDate>
    </item>
    <item>
      <title>Compose Internals : 1. Composable 함수들(Composable functions)</title>
      <link>https://chanho-study.tistory.com/189</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpFRQu/dJMcagSMgzu/H49khWIJufXzLNWlOzBHf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpFRQu/dJMcagSMgzu/H49khWIJufXzLNWlOzBHf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpFRQu/dJMcagSMgzu/H49khWIJufXzLNWlOzBHf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpFRQu%2FdJMcagSMgzu%2FH49khWIJufXzLNWlOzBHf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;827&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Jetpack Compose를 사용해 UI를 그릴 때 우린 당연한 것처럼 @Composable을 사용해 왔다. 하지만 @Composable 어노테이션이 정확히 어떤 원리로 화면을 그리며, 일반 함수와는 어떤 차이가 있는지 알지 못한다. 이 글에선 @Composable 어노테이션이 함수에 어떤 특성을 부여하는지, Compose Runtime이 이 특성을 어떻게 활용하는지를 하나씩 살펴볼 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Composable 함수의 의미&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Composable 함수는 실행 시 자신의 정보를 메모리 속 트리에 노드로 기록하는데 이를 Compose 관용어로 &quot;방출(emit)&quot;이라고 한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776668168369&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello, $name!&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 함수가 실행되면 &quot;Hello, $name!&quot; 정보를 담은 노드가 트리에 추가되고 Compose Runtime이 트리를 해석해 실제 UI를 만든다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Composable 함수를 표현식으로 나타내면 다음과 같다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776669122265&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable (Input) -&amp;gt; Unit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;입력은 함수의 인자, 출력은 트리에 노드를 삽입하는 동작이며 &lt;/span&gt;이는 Composition 처리 과정 중에 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable&amp;nbsp;함수는&amp;nbsp;상태가&amp;nbsp;변경될&amp;nbsp;때마다&amp;nbsp;다시&amp;nbsp;실행되어&amp;nbsp;메모리&amp;nbsp;구조를&amp;nbsp;항상&amp;nbsp;최신&amp;nbsp;상태로&amp;nbsp;유지한다.&amp;nbsp;따라서&amp;nbsp;트리를&amp;nbsp;최신&amp;nbsp;상태로&amp;nbsp;유지하기&amp;nbsp;위해&amp;nbsp;노드를&amp;nbsp;추가하거나&amp;nbsp;제거,&amp;nbsp;교체한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Composable 함수의 속성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose Runtime은 Composable 함수가 사전에 정의된 특성을 준수하도록 가정하기 때문에 병렬적인 Composition, 우선순위에 따른 임의의 Composition 정렬, 스마트 Recomposition , 또는 위치 기억법(positional memoization) 등과 같은 다양한 런타임 최적화 기법을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;호출 컨텍스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable 함수는 반드시 다른 Composable 함수 안에서만 호출할 수 있다. 일반 함수에서 Composable을 호출하면 컴파일 에러가 발생하는데 왜 이런 제약이 존재할까? &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 이해하려면 Compose Compiler가 뒤에서 어떤 일을 하는지 알아야 한다. Compose Compiler는 컴파일 시점에 모든 Composable 함수의 매개변수 목록 끝에 Composer와 $changed라는 매개변수를 암묵적으로 추가한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776745369880&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun Greeting(name: String) {
    var count by remember { mutableStateOf(0) }
    Text(
        text = &quot;Hello $name!&quot;,
    )
}

public static final void Greeting(
	@NotNull String name, 
	@Nullable Composer $composer, 
	int $changed
)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Composer&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable 함수와 Compose Runtime 사이의 중재자 역할을 한다. Composable 함수는 Composer를 통해 트리에 노드를 추가하거나 업데이트하며, Composer는 트리의 모든 Composable 호출로 하향 전달되기 때문에 트리 어느 깊이에서든 접근할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;$changed&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Recomposition 최적화를 위한 비트마스크로 각 비트가 매개변수 하나의 변경 여부를 나타낸다. Compose Runtime은 Recomposition 시점에 이 값을 보고 매개변수가 변경되지 않았다면 해당 Composable을 건너뛴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조는 suspend 함수와 비슷한데, suspend 함수도 컴파일러가 Continuation이라는 매개변수를 함수의 마지막 인자로 암묵적으로 추가하며 Composable의 Composer와 같은 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;멱등성(Idempotent)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable 함수는 동일한 입력에 대해 항상 동일한 트리를 생성해야 하며 이를 멱등성이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose Runtime은 상태가 변경되면 Composable 함수를 다시 실행해 트리를 업데이트하는데, 이 과정을 Recomposition이라 한다. Recomposition은 트리를 순회하며 입력값이 변경된 노드만 다시 실행하고 나머지는 건너뛴다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이때, 특정 노드를 건너뛸 수 있는 이유가 바로 멱등성 덕분이다. 입력값이 동일하다면 어차피 같은 결과가 나올 것이 보장되기 때문에 이미 메모리에 저장된 결과를 그대로 재사용한다. 앞서 살펴본 $changed가 바로 이 판단을 실제로 수행하는 메커니즘이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776732925939&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello, $name!&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;name이&amp;nbsp;변경되지&amp;nbsp;않았다면&amp;nbsp;Compose&amp;nbsp;Runtime은&amp;nbsp;Greeting을&amp;nbsp;다시&amp;nbsp;실행하지&amp;nbsp;않는다.&amp;nbsp;동일한&amp;nbsp;입력이면&amp;nbsp;동일한&amp;nbsp;트리가&amp;nbsp;나온다는&amp;nbsp;것이&amp;nbsp;보장되기&amp;nbsp;때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반대로 멱등성이 깨지면 어떻게 될까? &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Compose&amp;nbsp;Runtime은&lt;/span&gt; &quot;입력이 같으면 결과도 같다&quot;라고 가정하고 건너뛰었는데 실제로는 다른 결과가 나와버려 UI와 실제 상태와 달라지는 버그로 이어진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776732944654&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 멱등성이 깨지는 예시
var count = 0

@Composable
fun Counter() {
    count++  // 외부 상태를 직접 변경
    Text(text = &quot;Count: $count&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 코드는 Counter가 몇 번 실행되느냐에 따라 결과가 달라진다. Recomposition이 발생할 때마다 count가 증가하기 때문에 동일한 입력임에도 다른 UI가 그려지기 때문에 Composable 함수 안에서 외부 상태를 직접 읽거나 쓰는 것을 피해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;통제되지 않은 사이드 이펙트 방지&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사이드 이펙트란, 호출된 함수의 제어를 벗어나 발생할 수 있는 예상치 못한 모든 동작을 의미하며 네트워크 요청, 데이터베이스 트랜잭션, 전역 변수 변경 등이 해당한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable&amp;nbsp;함수는&amp;nbsp;여러&amp;nbsp;번,&amp;nbsp;임의의&amp;nbsp;순서로,&amp;nbsp;심지어&amp;nbsp;병렬로&amp;nbsp;실행될&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;이러한&amp;nbsp;특성&amp;nbsp;때문에&amp;nbsp;Composable&amp;nbsp;함수&amp;nbsp;안에서&amp;nbsp;직접&amp;nbsp;사이드&amp;nbsp;이펙트를&amp;nbsp;실행하면&amp;nbsp;통제할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;상황이&amp;nbsp;발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776740505087&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun EventsFeed(networkService: EventsNetworkService) {
    val events = networkService.loadAllEvents() // 네트워크 요청

    LazyColumn {
        items(events) { event -&amp;gt;
            Text(text = event.name)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 코드는 Recomposition이 발생할 때마다 네트워크 요청이 실행된다. Composable 함수는 Compose Runtime에 의해 짧은 시간 내에 여러 번 다시 실행될 수 있으며, 개발자가 제어할 수 없기 때문에 네트워크 요청이 의도치 않게 반복 호출된다. 더 큰 문제는 실행 순서인데 아래 코드를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776740595149&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MainScreen() {
    Header()
    ProfileDetail()
    EventList()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Header, ProfileDetail, EventList는 Compose Compiler에 의해 어떤 순서로든, 심지어 병렬로 실행될 수 있기 때문에 Header에서 외부 변수를 업데이트하고 ProfileDetail에서 그 변수를 읽는 식의 로직은 절대 작성해선 안 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;때문에 우리는 모든 Composable 함수를 Stateless 하게 만들려고 노력해야 하며&amp;nbsp;이를&amp;nbsp;위해&amp;nbsp;Composable&amp;nbsp;함수는&amp;nbsp;모든&amp;nbsp;입력값을&amp;nbsp;매개변수로&amp;nbsp;전달받아&amp;nbsp;외부&amp;nbsp;상태에&amp;nbsp;의존하지&amp;nbsp;않도록&amp;nbsp;설계해야&amp;nbsp;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러나 모든 상황에서 Stateless 한 Composable 함수를 만드는 것은 불가능하다. 이를 해결하기 위해 Jetpack Compose는 LaunchedEffect, SideEffect, DisposableEffect 같은 사이드 이펙트 핸들러를 제공한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사이드 이펙트 핸들러는 사이드 이펙트가 Composable의 라이프사이클을 인식하도록 하여 안전하고 통제된 환경에서 실행될 수 있게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;재시작 가능(Restartable)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일반&amp;nbsp;함수는&amp;nbsp;호출되면&amp;nbsp;콜&amp;nbsp;스택에서&amp;nbsp;한&amp;nbsp;번&amp;nbsp;실행되고&amp;nbsp;끝이다.&amp;nbsp;하지만&amp;nbsp;Composable&amp;nbsp;함수는&amp;nbsp;다르다.&amp;nbsp;Compose&amp;nbsp;Runtime은&amp;nbsp;Composable&amp;nbsp;함수가&amp;nbsp;관찰하는&amp;nbsp;상태(State)가&amp;nbsp;변경될&amp;nbsp;때마다&amp;nbsp;해당&amp;nbsp;함수를&amp;nbsp;다시&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;참조를&amp;nbsp;유지한다.&amp;nbsp;이를&amp;nbsp;재시작&amp;nbsp;가능(Restartable)하다고&amp;nbsp;한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose&amp;nbsp;Compiler는&amp;nbsp;상태를&amp;nbsp;읽는&amp;nbsp;Composable&amp;nbsp;함수를&amp;nbsp;감지해&amp;nbsp;Compose&amp;nbsp;Runtime에게&amp;nbsp;재시작하는&amp;nbsp;방법을&amp;nbsp;알려주는&amp;nbsp;코드를&amp;nbsp;자동으로&amp;nbsp;생성한다.&amp;nbsp;반대로&amp;nbsp;상태를&amp;nbsp;읽지&amp;nbsp;않는&amp;nbsp;Composable은&amp;nbsp;재시작할&amp;nbsp;필요가&amp;nbsp;없으므로&amp;nbsp;해당&amp;nbsp;코드가&amp;nbsp;생성되지&amp;nbsp;않는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776741819996&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text(text = &quot;Count: $count&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;count가 변경되면 Compose Runtime은 Counter를 다시 실행한다. 이때 Recomposition은 변경된 상태를 읽는 Composable만 대상으로 하기 때문에 count를 읽지 않는 다른 Composable은 재실행되지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776741933960&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static final void Greeting(
    @NotNull String name,
    @Nullable Composer $composer,
    int $changed
) {
    $composer = $composer.startRestartGroup(1442732377);
    ...
    Object it$iv$iv = $composer.rememberedValue();
    ...
    ScopeUpdateScope var10000 = $composer.endRestartGroup();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 코드를 디컴파일해보면 다음과 같이 변환되는데, &lt;b&gt;startRestartGroup&lt;/b&gt;과 &lt;b&gt;endRestartGroup&lt;/b&gt;이 바로 이 재시작 메커니즘을 구현하는 코드다. startRestartGroup은 Composable의 재시작 범위를 시작하고, endRestartGroup은 범위를 끝내며 상태가 변경됐을 때 이 Composable을 어떻게 재시작할지 Compose Runtime에 등록한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;rememberedValue()는 remember { } 블록의 실제 구현이다. remember는 내부적으로 Composer가 관리하는 슬롯 테이블에서 이전에 저장된 값을 읽어온다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, $composer.rememberedValue()는 이전 Composition에서 저장해 둔 count의 상태를 슬롯 테이블에서 꺼내오기 때문에 Recomposition이 발생해도 remember 블록 안의 값이 초기화되지 않고 유지되는 이유가 여기에 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;위치 기억법(Positional Memoization)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메모이제이션(Memoization)이란 함수가 동일한 입력에 대해 결과를 캐싱해 두고 같은 입력이 들어오면 다시 계산하지 않고 캐싱된 결과를 반환하는 기법이다. Compose는 이 개념을 Composable 함수에 적용하는데, 한 가지 요소가 추가된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; Composable 함수는 소스 코드에서의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;호출 위치가 식별자로 사용&lt;/b&gt;&lt;/span&gt;된다. 즉, 동일한 함수라도 다른 위치에서 호출되면 Compose Runtime은 이를 다른 노드로 취급한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776744702756&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MyComposable() {
    Text(&quot;Hello&quot;) // id 1
    Text(&quot;Hello&quot;) // id 2
    Text(&quot;Hello&quot;) // id 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;세 Text는 동일한 함수에 동일한 입력이지만 호출 위치가 다르기 때문에 트리에서 각각 고유한 노드로 저장된다. 이&amp;nbsp;메커니즘이&amp;nbsp;동작하는&amp;nbsp;핵심이&amp;nbsp;바로&amp;nbsp;앞서&amp;nbsp;살펴본&amp;nbsp;슬롯&amp;nbsp;테이블이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose&amp;nbsp;Runtime은&amp;nbsp;Composable&amp;nbsp;함수의&amp;nbsp;정보를&amp;nbsp;슬롯&amp;nbsp;테이블에&amp;nbsp;저장하며,&amp;nbsp;호출&amp;nbsp;위치를&amp;nbsp;키로&amp;nbsp;사용해&amp;nbsp;이전에&amp;nbsp;저장된&amp;nbsp;값을&amp;nbsp;읽고&amp;nbsp;쓴다.&amp;nbsp;remember도&amp;nbsp;이&amp;nbsp;슬롯&amp;nbsp;테이블을&amp;nbsp;활용해&amp;nbsp;값을&amp;nbsp;캐싱한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FZ2XX/dJMcaaruB2D/BQaIceIHevRLuJddFx2tq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FZ2XX/dJMcaaruB2D/BQaIceIHevRLuJddFx2tq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FZ2XX/dJMcaaruB2D/BQaIceIHevRLuJddFx2tq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFZ2XX%2FdJMcaaruB2D%2FBQaIceIHevRLuJddFx2tq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;362&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1776744966642&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun FilteredImage(path: String) {
    // 계산 비용이 왕 큰 함수
    val filters = remember { computeFilters(path) } // 슬롯 테이블에 캐싱
    ImageWithFiltersApplied(filters)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable 함수 내에서 발생하는 비용이 큰 계산 결과를 캐싱하고 싶다고 가정해 보자. 이와 관련해 Compose Runtime은 remember 함수를 제공한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시 코드에선 이미지 필터라는 연산 결과를 사전 계산하고 캐싱하기 위해 remember를 사용했다. 캐싱된 값을 검색하기 위한 키 값은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;소스 코드의 호출 위치와 함수의 입력값&lt;/b&gt;&lt;/span&gt;을(위의 예시 코드에서는 path라는 파일 경로가 사용) 기반으로 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리스트처럼 반복문에서 Composable이 생성되는 경우엔 어떨까? &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776746303692&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun TalksScreen(talks: List&amp;lt;Talk&amp;gt;) {
    Column {
        for (talk in talks) {
            Talk(talk)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시 코드의 경우 Talk는 매번 같은 위치에서 호출되기 때문에 Compose Runtime은 &lt;b&gt;호출 순서를 기준으로 각 노드를 구별&lt;/b&gt;한다. 이 경우 리스트 끝에 항목을 추가하는 경우엔 문제가 없지만 중간이나 앞에 항목을 추가하면 Compose Runtime이 요소 삽입이 발생하는 지점 아래의 모든 Talk Composable함수에 대해서 Recomposition을 발생시킨다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이는 Composable 함수들의 위치가 변경되었기 때문인데, 이는 해당 함수들의 입력값이 변경되지 않았더라도 해당한다. 마치 Recyclerview의 notifyDataSetChanged()를 사용할 때와 유사한데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이를 해결하기 위해 Compose는 key를 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776746439873&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun TalksScreen(talks: List&amp;lt;Talk&amp;gt;) {
    Column {
        for (talk in talks) {
            key(talk.id) {
                Talk(talk)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;key를&amp;nbsp;사용하면&amp;nbsp;Compose&amp;nbsp;Runtime이&amp;nbsp;호출&amp;nbsp;위치가&amp;nbsp;아닌&amp;nbsp;명시적으로&amp;nbsp;지정한&amp;nbsp;키&amp;nbsp;값으로&amp;nbsp;노드를&amp;nbsp;식별하기&amp;nbsp;때문에&amp;nbsp;리스트&amp;nbsp;중간에&amp;nbsp;항목이&amp;nbsp;추가되더라도&amp;nbsp;기존&amp;nbsp;노드의&amp;nbsp;정체성이&amp;nbsp;유지되어&amp;nbsp;불필요한&amp;nbsp;Recomposition이&amp;nbsp;발생하지&amp;nbsp;않는다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;함수&amp;nbsp;컬러링&amp;nbsp;(Function&amp;nbsp;Coloring)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable&amp;nbsp;함수는&amp;nbsp;일반&amp;nbsp;함수에서&amp;nbsp;호출할&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;반드시&amp;nbsp;setContent&amp;nbsp;{&amp;nbsp;}&amp;nbsp;같은&amp;nbsp;통합점을&amp;nbsp;거쳐야&amp;nbsp;한다.&amp;nbsp;이처럼&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;두&amp;nbsp;범주의&amp;nbsp;함수가&amp;nbsp;섞이지&amp;nbsp;못하는&amp;nbsp;현상을&amp;nbsp;함수&amp;nbsp;컬러링(Function&amp;nbsp;Coloring)이라고&amp;nbsp;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;함수&amp;nbsp;컬러링은&amp;nbsp;Bob&amp;nbsp;Nystrom이&amp;nbsp;2015년&amp;nbsp;작성한&amp;nbsp;&quot;&lt;a href=&quot;https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;What&amp;nbsp;color&amp;nbsp;is&amp;nbsp;your&amp;nbsp;function?&lt;/a&gt;&quot;이라는 글에서 소개된 개념으로 동기 함수에서 비동기 함수를 호출할 수 없는 문제를 두 함수가 서로 다른 색깔을 가진다고 표현한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin의 suspend 함수도 마찬가지로 다른 suspend 함수에서만 호출할 수 있기 때문에 채색된 함수로 간주된다. @Composable도 동일한 맥락이다. 일반 함수에서 Composable을 호출하려면 통합점이 필요하고 그 이후로는 완전히 Composable로만 이루어진 콜 스택을 갖게 된다. 그런데 아래 코드는 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776751702192&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun SpeakerList(speakers: List&amp;lt;Speaker&amp;gt;) {
    Column {
        speakers.forEach {
            Speaker(it) // 일반 함수(forEach 람다) 안에서 Composable 호출
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;forEach는&amp;nbsp;일반&amp;nbsp;함수인데&amp;nbsp;그&amp;nbsp;안에서&amp;nbsp;Composable인&amp;nbsp;Speaker를&amp;nbsp;호출하고&amp;nbsp;있다.&amp;nbsp;컴파일&amp;nbsp;에러가&amp;nbsp;날&amp;nbsp;것&amp;nbsp;같지만&amp;nbsp;문제없이&amp;nbsp;동작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이유는 forEach가 inline 함수이기 때문으로, 컴파일 시점에 람다 코드가 호출부에 그대로 인라이닝 되기 때문에 실제로는 SpeakerList 본문 안에서 Speaker가 호출되는 것과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Composable&amp;nbsp;함수&amp;nbsp;타입&amp;nbsp;(Composable&amp;nbsp;Function&amp;nbsp;Types)&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@Composable 어노테이션은 컴파일 타임에 함수의 타입을 변경한다. Composable 함수의 타입은 다음과 같으며&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;일반 람다처럼 변수에 저장하거나 다른 함수에 전달할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776751938725&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable (Input) -&amp;gt; Unit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또한 Composable 함수는 @Composable Scope.() -&amp;gt; Unit 형태의 타입을 가질 수 있는데, 특정 Composable 안에서만 사용할 수 있는 범위를 지정할 때 활용된다. Box의 content 파라미터가 대표적인 예다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776751971076&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;inline fun Box(
    content: @Composable BoxScope.() -&amp;gt; Unit
) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 114px; top: 5970.12px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;BoxScope가&amp;nbsp;수신&amp;nbsp;객체로&amp;nbsp;지정되어&amp;nbsp;있기&amp;nbsp;때문에&amp;nbsp;content&amp;nbsp;람다&amp;nbsp;안에서만&amp;nbsp;BoxScope의&amp;nbsp;함수를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;Compose에서&amp;nbsp;Modifier&amp;nbsp;관련&amp;nbsp;함수들이&amp;nbsp;특정&amp;nbsp;스코프&amp;nbsp;안에서만&amp;nbsp;동작하는&amp;nbsp;이유가&amp;nbsp;바로&amp;nbsp;이&amp;nbsp;타입&amp;nbsp;시스템&amp;nbsp;덕분이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지금까지&amp;nbsp;@Composable&amp;nbsp;어노테이션이&amp;nbsp;함수에&amp;nbsp;어떤&amp;nbsp;특성을&amp;nbsp;부여하는지,&amp;nbsp;그리고&amp;nbsp;Compose&amp;nbsp;Runtime이&amp;nbsp;이를&amp;nbsp;어떻게&amp;nbsp;활용하는지&amp;nbsp;살펴봤다.&amp;nbsp;정리하면&amp;nbsp;다음과&amp;nbsp;같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;호출 컨텍스트 : Compose Compiler가 Composer와 $changed를 암묵적으로 추가하고 Composable은 반드시 다른 Composable 안에서만 호출할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;멱등성 : 동일한 입력에 대해 항상 동일한 트리를 생성해야 하며, 이를 통해 Recomposition 시 불필요한 재실행을 건너뛸 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사이드 이펙트 방지 : Composable은 여러 번, 임의의 순서로 실행될 수 있기 때문에 직접적인 사이드 이펙트는 이펙트 핸들러를 통해 안전하게 처리해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;재시작 가능 : 관찰하는 상태가 변경되면 Compose Runtime이 해당 Composable을 다시 실행한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위치 기억법 : 호출 위치를 식별자로 사용해 Composable의 정체성을 유지하고 슬롯 테이블을 통해 값을 캐싱한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;함수 컬러링 : Composable은 채색된 함수로 통합점 이후 완전히 Composable로만 이루어진 콜 스택을 갖는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/189</guid>
      <comments>https://chanho-study.tistory.com/189#entry189comment</comments>
      <pubDate>Tue, 21 Apr 2026 14:58:01 +0900</pubDate>
    </item>
    <item>
      <title>ART(Android Runtime)와 Baseline Profile 기초</title>
      <link>https://chanho-study.tistory.com/188</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 3월 18일 오후 04_45_42.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RKGiQ/dJMcahReybm/TCzUyttqskHa6zErz4HyX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RKGiQ/dJMcahReybm/TCzUyttqskHa6zErz4HyX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RKGiQ/dJMcahReybm/TCzUyttqskHa6zErz4HyX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRKGiQ%2FdJMcahReybm%2FTCzUyttqskHa6zErz4HyX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;446&quot; data-filename=&quot;ChatGPT Image 2026년 3월 18일 오후 04_45_42.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서론&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 설치한 앱은 실행하면서 코드를 그때그때 컴파일하기 때문에 처음 몇 번은 느리고 쓸수록 빨라진다. Baseline Profile을 사용하면 중요한 코드를 첫 실행 전에 미리 번역해 두므로 설치 직후부터 빠른 성능을 낼 수 있는데, Baseline Profile의 원리를 이해하기 위해 기초적인 개념부터 시작해 하나씩 이해해 보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;안드로이드 앱이 실행되기까지의 과정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;컴파일이란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컴파일이란 사람이 이해하는 고수준 언어(e.g kotlin, java...)를 컴퓨터가 이해할 수 있는 0과 1로 이루어진 저수준 언어(기계어)로 번역하는 과정을 말한다. 코틀린으로 작성한 코드가 스마트폰에서 실행되려면 총 3단계를 거친다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1단계 : Kotlinc Compile&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbsuUr/dJMcaibxAUd/LekLD97PUucopOBIupkOK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbsuUr/dJMcaibxAUd/LekLD97PUucopOBIupkOK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbsuUr/dJMcaibxAUd/LekLD97PUucopOBIupkOK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbsuUr%2FdJMcaibxAUd%2FLekLD97PUucopOBIupkOK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;614&quot; height=&quot;60&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자가 작성한 코틀린 코드(.kt) 파일은 컴퓨터가 바로 이해할 수 없기 때문에 JVM이 실행할 수 있는 형태인 JVM 바이트코드(.class)로 만들어야 하며 이 역할을 하는 것이 바로 Kotlin Compiler이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;바이트코드는 JVM이 읽는 중간 언어이며 완전한 기계어 형태는 아니다. 바이트코드라는 이름은 JVM 명령어의 opcode가 1바이트 단위로 표현되는 데서 왔다. 다만 실제 instruction 전체 길이는 opcode 뒤에 붙는 operand 유무에 따라 더 길어질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/63sks/dJMcabwGlQt/ct1iOH6ZVDDcqvFIe7K351/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/63sks/dJMcabwGlQt/ct1iOH6ZVDDcqvFIe7K351/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/63sks/dJMcabwGlQt/ct1iOH6ZVDDcqvFIe7K351/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F63sks%2FdJMcabwGlQt%2Fct1iOH6ZVDDcqvFIe7K351%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;86&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;IDE의 기능을 활용하면 다음과 같이 Kotlin Bytecode를 확인할 수 있다. Kotlin Compiler의 컴파일 과정은 다른 글에서 자세히 다뤄보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2432&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clpEim/dJMcaiWQCYE/z4AQ6qKdLRLE0LZOyb3HJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clpEim/dJMcaiWQCYE/z4AQ6qKdLRLE0LZOyb3HJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clpEim/dJMcaiWQCYE/z4AQ6qKdLRLE0LZOyb3HJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclpEim%2FdJMcaiWQCYE%2Fz4AQ6qKdLRLE0LZOyb3HJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;252&quot; data-origin-width=&quot;2432&quot; data-origin-height=&quot;1076&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2단계 : R8 Compile&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GCA3U/dJMcabKglOe/49u2CK5UFqQS5Zl74LwiGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GCA3U/dJMcabKglOe/49u2CK5UFqQS5Zl74LwiGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GCA3U/dJMcabKglOe/49u2CK5UFqQS5Zl74LwiGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGCA3U%2FdJMcabKglOe%2F49u2CK5UFqQS5Zl74LwiGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;71&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1단계에서 생성된 .class 바이트코드는 안드로이드 기기에서 그대로 실행되지 않기 때문에 DEX 형식으로 변환되어야 한다. 이 과정은 기본적으로 D8이 담당하며 릴리즈 빌드에서는 R8이 코드 축소&amp;middot;난독화&amp;middot;최적화를 수행하면서 DEX 생성 과정까지 함께 담당한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;릴리즈 빌드에서는 필요에 따라 isMinifyEnabled = true로 R8의 축소&amp;middot;난독화&amp;middot;최적화를 적용하여 AAB(Android App Bundle) 형태로 배포되며 Google Play는 이를 바탕으로 기기별 최적화 APK를 생성해 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N2BIp/dJMcacCnTlL/8oM3J8k76YMAUusQpbLNJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N2BIp/dJMcacCnTlL/8oM3J8k76YMAUusQpbLNJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N2BIp/dJMcacCnTlL/8oM3J8k76YMAUusQpbLNJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN2BIp%2FdJMcacCnTlL%2F8oM3J8k76YMAUusQpbLNJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;265&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AAB는&amp;nbsp;설치&amp;nbsp;가능한&amp;nbsp;형식이&amp;nbsp;아닌&amp;nbsp;배포용&amp;nbsp;포맷으로,&amp;nbsp;모듈식&amp;nbsp;구조를&amp;nbsp;사용해&amp;nbsp;다른&amp;nbsp;구성에&amp;nbsp;대한&amp;nbsp;리소스와&amp;nbsp;코드를&amp;nbsp;별개의&amp;nbsp;번들로&amp;nbsp;분리한다.&amp;nbsp;Google&amp;nbsp;Play는&amp;nbsp;이&amp;nbsp;모듈식&amp;nbsp;구조를&amp;nbsp;사용하여&amp;nbsp;다운로드&amp;nbsp;시점에&amp;nbsp;사용자&amp;nbsp;기기에&amp;nbsp;최적화된&amp;nbsp;APK를&amp;nbsp;생성하여&amp;nbsp;앱&amp;nbsp;크기를&amp;nbsp;줄인다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3단계 : AOT / JIT&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/saPJa/dJMcagLyKBS/RpbkKlHk696clAqUn98Xb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/saPJa/dJMcagLyKBS/RpbkKlHk696clAqUn98Xb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/saPJa/dJMcagLyKBS/RpbkKlHk696clAqUn98Xb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsaPJa%2FdJMcagLyKBS%2FRpbkKlHk696clAqUn98Xb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;70&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Google &lt;/span&gt;Play Store에서 앱을 설치하면 3단계가 시작된다. 앱 설치 및 실행 이후에는 ART(Android Runtime)가 DEX 코드를 해석하거나 필요에 따라 JIT/AOT 컴파일을 통해 기기에서 효율적으로 실행되도록 최적화한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드&amp;nbsp;앱&amp;nbsp;실행&amp;nbsp;방식의&amp;nbsp;변화&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드의&amp;nbsp;앱&amp;nbsp;실행&amp;nbsp;방식은&amp;nbsp;버전이&amp;nbsp;올라가면서&amp;nbsp;계속&amp;nbsp;발전해&amp;nbsp;왔다.&amp;nbsp;초기에는&amp;nbsp;코드를&amp;nbsp;실행할&amp;nbsp;때마다&amp;nbsp;한&amp;nbsp;줄씩&amp;nbsp;해석하며&amp;nbsp;실행하는&amp;nbsp;방식에&amp;nbsp;가까웠고,&amp;nbsp;이후에는&amp;nbsp;자주&amp;nbsp;실행되는&amp;nbsp;코드를&amp;nbsp;미리&amp;nbsp;번역해&amp;nbsp;재사용하는&amp;nbsp;방식,&amp;nbsp;더&amp;nbsp;나아가&amp;nbsp;설치&amp;nbsp;시점이나&amp;nbsp;사용자&amp;nbsp;사용&amp;nbsp;패턴을&amp;nbsp;바탕으로&amp;nbsp;중요한&amp;nbsp;코드를&amp;nbsp;선별적으로&amp;nbsp;최적화하는&amp;nbsp;방식으로&amp;nbsp;발전했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Android 1.0(2008)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인터프리터 방식으로 앱을 실행할 때마다 코드를 한 줄 한 줄 읽고 번역하면서 실행했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;b&gt;Android&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;2.2 Froyo(2010)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;br /&gt;&lt;u&gt;JIT(Just-In-Time) 컴파일러 도입&lt;/u&gt;&lt;br /&gt;JIT는 앱이 실행되는 도중 자주 호출되는 코드를 감지해 그 부분을 기계어로 컴파일한 뒤 메모리에 저장해 재사용하는 방식이다. 덕분에 이전보다 실행 성능은 크게 좋아졌지만 앱 실행 중에 컴파일 작업이 함께 일어나기 때문에 그만큼 추가적인 런타임 오버헤드가 존재했다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;Android&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;4.4 Kitkat(2013)&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;u&gt;ART(Android Runtime) 실험적으로 도입&lt;/u&gt;&lt;br /&gt;ART는 기존 Dalvik과 달리 AOT(Ahead-Of-Time) 컴파일 방식을 사용했는데, 앱을 설치할 때 중요한 코드를 미리 기계어로 변환해 두고 실행 시에는 이미 번역된 결과를 사용하는 방식이다. 이 방식은 앱 실행 성능을 높이는 데 유리했지만 설치 시점의 부담이 커진다는 특징이 있었다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;&lt;b&gt;&lt;b&gt;Android&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;5.0 Lollipop (2014)&lt;/b&gt;&lt;br /&gt;&lt;u&gt;Dalvik이 완전히 사라지고 ART가 기본 런타임으로 대체&lt;/u&gt;&lt;br /&gt;이로 인해 앱 실행 속도와 전반적인 반응성은 개선되었지만 앱 설치 시 전체 코드를 미리 컴파일해야 했기 때문에 설치 시간이 길어지고 저장 공간 사용량도 늘어나는 문제가 있었다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;&lt;b&gt;Android&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;7.0 Nougat(2016)&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;u&gt;JIT와 AOT를 함께 사용하는 하이브리드 방식 도입&lt;/u&gt;&lt;br /&gt;앱은 우선 빠르게 설치되고 실행되며 실행 중에는 어떤 코드가 자주 사용되는지 프로파일링 한다. 이후 자주&amp;nbsp;실행되는&amp;nbsp;코드는&amp;nbsp;기기가&amp;nbsp;유휴&amp;nbsp;상태이거나&amp;nbsp;충전&amp;nbsp;중일&amp;nbsp;때&amp;nbsp;AOT로&amp;nbsp;추가&amp;nbsp;최적화하고,&amp;nbsp;그렇지&amp;nbsp;않은&amp;nbsp;코드는&amp;nbsp;JIT에&amp;nbsp;맡긴다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;&lt;b&gt;&lt;b&gt;Android&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;9.0 Pie(2018)&lt;/b&gt;&lt;br /&gt;&lt;u&gt;Cloud Profile 도입&lt;/u&gt;&lt;br /&gt;Google Play는 많은 사용자의 실행 패턴을 바탕으로 프로파일 데이터를 집계하고 이를 앱 설치 시 최적화에 활용할 수 있도록 했다. 덕분에 사용자는 앱을 처음 설치한 직후에도 다른 사용자들의 실제 사용 데이터를 기반으로 어느 정도 최적화된 상태에서 앱을 시작할 수 있게 되었다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Cloud Profile은 분명 유용한 방식이지만 충분한 프로파일 데이터가 수집되지 않은 상황에서는 한계가 있다. 예를 들어 앱을 처음 출시했거나 사용자 수가 적은 초기 단계라면 실제 사용자 실행 데이터를 기반으로 한 최적화 효과를 충분히 기대하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 경우 앱을 처음 설치하거나 업데이트한 직후에는 중요한 코드 경로가 아직 충분히 AOT 최적화되지 않은 상태일 수 있다. Android는 과거처럼 설치 시점에 앱 전체를 AOT 컴파일하지 않고 런타임 실행 정보와 프로파일을 활용해 점진적으로 최적화를 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 초기 실행에서는 ART가 DEX 바이트코드를 해석(interpreter)하며 실행하고 실행 중 자주 호출되는 코드는 JIT 컴파일을 통해 점차 최적화된다. 이후 기기가 적절한 조건에 있을 때 누적된 프로파일 정보를 바탕으로 추가적인 AOT 최적화가 수행되어 다음 실행부터는 더 나은 성능을 낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 이런 방식은 결국 사용자가 첫 실행이나 초기 몇 번의 실행에서 상대적으로 느린 성능을 경험할 수 있다는 한계가 있다. 이 문제를 완화하기 위해 사용하는 것이 바로 Baseline Profile이다. Baseline Profile을 앱에 포함하면 개발자가 미리 지정한 Critical User Journey에 대해 AOT 최적화를 더 일찍 적용할 수 있어 첫 실행부터 더 빠른 시작 속도와 부드러운 동작을 제공할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Baseline Profile&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Baseline Profile은 앱의 중요한 코드 경로를 프로파일로 제공해 앱 설치 후 ART가 해당 경로를 AOT 최적화할 수 있게 해 주는 기술이다. 쉽게&amp;nbsp;말해&amp;nbsp;앱이&amp;nbsp;처음&amp;nbsp;실행될&amp;nbsp;때&amp;nbsp;더&amp;nbsp;빠르게&amp;nbsp;동작하도록&amp;nbsp;미리&amp;nbsp;컴파일해야&amp;nbsp;할&amp;nbsp;코드&amp;nbsp;목록을&amp;nbsp;만들어&amp;nbsp;플레이스토어에&amp;nbsp;배포한다.&amp;nbsp;이를&amp;nbsp;기반으로&amp;nbsp;Cloud&amp;nbsp;Profile&amp;nbsp;수집&amp;nbsp;전이라도,&amp;nbsp;심지어&amp;nbsp;앱을&amp;nbsp;처음&amp;nbsp;실행하는&amp;nbsp;사용자에게도&amp;nbsp;첫&amp;nbsp;실행부터&amp;nbsp;빠른&amp;nbsp;성능을&amp;nbsp;보장할&amp;nbsp;수&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5SN2h/dJMcafePUr3/pjW3h1f5GFqvWiZUKHkVHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5SN2h/dJMcafePUr3/pjW3h1f5GFqvWiZUKHkVHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5SN2h/dJMcafePUr3/pjW3h1f5GFqvWiZUKHkVHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5SN2h%2FdJMcafePUr3%2FpjW3h1f5GFqvWiZUKHkVHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;218&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 프로파일 기반 최적화(PGO, Profile Guided Optimization)이라 하며 Jank 현상을 감소시키고 전반적인 성능 개선을 통해 사용자에게 더 나은 경험을 제공할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Baseline Profile을 사용하기 위해선 Baseline Profile Generator를 사용해 baselineprofile 모듈을 생성해 준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oaA59/dJMcabKhiOd/J8ZvjbMFGChG4s0UhK1yt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oaA59/dJMcabKhiOd/J8ZvjbMFGChG4s0UhK1yt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oaA59/dJMcabKhiOd/J8ZvjbMFGChG4s0UhK1yt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoaA59%2FdJMcabKhiOd%2FJ8ZvjbMFGChG4s0UhK1yt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;480&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모듈을 생성하면 두 개의 클래스가 자동으로 만들어진다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;BaselineProfileGenerator&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1773744357740&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

    @get:Rule
    val baselineProfileRule = BaselineProfileRule()

    @Test
    fun generate() = baselineProfileRule.collect(
        packageName = &quot;패키지네임&quot;,
    ) {
        // defines the app's critical user journey
        pressHome()
        startActivityAndWait()
        
		// 1. Wait until the content is asynchronously loaded
        waitForAsyncContent()
        // 2. Scroll the feed content
        scrollSnackListJourney()
        // 3. Navigate to detail screen
        goToSnackDetailJourney()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱이 처음 실행될 때 더 빠르게 동작하도록 미리 컴파일해 두면 좋은 코드 목록을 만드는 클래스다. 개발자가 사용자의 앱을 사용하는 흐름을 시나리오 형태로 작성하면 그 흐름을 따라 앱을 실행하면서 Baseline Profile을 생성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 만들어진 프로파일은 baseline-prof.txt 파일로 저장되고 이후 앱 설치 시 AOT 컴파일 최적화에 활용된다. Baseline Profile 생성은 앱의 대표적인 User Journey를 기반으로 작성해야 하는데 공식 코드랩에는 아래처럼 사용자 흐름을 함수로 나눠 작성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;waitForAsyncContent() - 앱 시작&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱이 켜지자마자 바로 측정을 시작지 않고 목록과 실제 콘텐츠가 화면에 나타날 때까지 기다리는 역할을 한다. 네트워크 호출 같은 비동기로 데이터를 불러오는 화면에서 사용자가 실제로 볼 수 있는 상태까지 도달했는지를 확인하는 코드다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773744513154&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
private fun SnackCollectionList(
    snackCollections: List&amp;lt;SnackCollection&amp;gt;,
    filters: List&amp;lt;Filter&amp;gt;,
    onSnackClick: (Long) -&amp;gt; Unit,
    modifier: Modifier = Modifier
) {
    var filtersVisible by rememberSaveable { mutableStateOf(false) }
    Box(modifier) {
        LazyColumn(
            modifier = Modifier.testTag(&quot;snack_list&quot;),
        ) { ... // }
    }
}

fun MacrobenchmarkScope.waitForAsyncContent() {
    device.wait(Until.hasObject(By.res(&quot;snack_list&quot;)), 5_000)
    val contentList = device.findObject(By.res(&quot;snack_list&quot;))
    contentList.wait(Until.hasObject(By.res(&quot;snack_collection&quot;)), 5_000)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;scrollSnackListJourney() - 스낵 목록 스크롤&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자가 목록 스크롤을 재현하며 스크롤 중 자주 실행되는 UI 코드와 렌더링 관련 경로가 프로파일에 포함된 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773744605920&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun MacrobenchmarkScope.scrollSnackListJourney() {
    val snackList = device.findObject(By.res(&quot;snack_list&quot;))
    snackList.setGestureMargin(device.displayWidth / 5)
    snackList.fling(Direction.DOWN)
    device.waitForIdle()
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;goToSnackDetailJourney() - 스낵 상세 화면 이동&lt;br /&gt;&lt;/b&gt;스낵&amp;nbsp;목록의&amp;nbsp;아이템&amp;nbsp;하나를&amp;nbsp;눌러&amp;nbsp;상세&amp;nbsp;화면으로&amp;nbsp;이동한다.&amp;nbsp;단순히&amp;nbsp;클릭만&amp;nbsp;하는&amp;nbsp;것이&amp;nbsp;아니라&amp;nbsp;목록&amp;nbsp;화면이&amp;nbsp;사라질&amp;nbsp;때까지&amp;nbsp;기다리고&amp;nbsp;화면&amp;nbsp;전환이&amp;nbsp;실제로&amp;nbsp;완료되었는지&amp;nbsp;확인한다.&amp;nbsp;이&amp;nbsp;과정에서&amp;nbsp;상세&amp;nbsp;화면&amp;nbsp;진입&amp;nbsp;과정에서&amp;nbsp;쓰이는&amp;nbsp;코드도&amp;nbsp;Baseline&amp;nbsp;Profile에&amp;nbsp;반영된다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773744665137&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun MacrobenchmarkScope.goToSnackDetailJourney() {
    val snackList = device.findObject(By.res(&quot;snack_list&quot;))
    val snacks = snackList.findObjects(By.res(&quot;snack_item&quot;))
    val index = (iteration ?: 0) % snacks.size
    snacks[index].click()
    device.wait(Until.gone(By.res(&quot;snack_list&quot;)), 5_000)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;StartupBenchmarks&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Jetpack Macrobenchmark를 사용해 Baseline Profile이 실제로 앱 시작 성능을 얼마나 개선하는지 측정하는 클래스다. 쉽게 말해 BaselineProfileGenerator가 프로파일을 생성하는 단계이면 StartupBenchmarks는 그 프로파일이 실제로 앱 시작 성능을 얼마나 개선했는지 측정하는 단계다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773743093664&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmarks {

    @get:Rule
    val rule = MacrobenchmarkRule()

    @Test
    fun startupCompilationNone() =
        benchmark(CompilationMode.None())

    @Test
    fun startupCompilationBaselineProfiles() =
        benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
    
    private fun benchmark(compilationMode: CompilationMode) {
        rule.measureRepeated(
            packageName = &quot;패키지명&quot;,
            metrics = listOf(StartupTimingMetric()),
            compilationMode = compilationMode,
            startupMode = StartupMode.COLD,
            iterations = 10,
            setupBlock = {
                pressHome()
            },
            measureBlock = {
                startActivityAndWait()
            }
    	)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;CompilationMode&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞서 설명한 것처럼 APK 파일들은 DEX 파일들의 집합이다. 이 DEX 파일들이 언제 어떤 방식으로 컴파일(AOT or JIT) 되는지 알 수 없다. 이러한 환경 속에서 CompilationMode는 AOT나 JIT 컴파일 방식을 지정해 주는 파라미터다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9535%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;None&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 사전 컴파일 없이 측정&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Baseline Profile 미적용 상태를 시뮬레이션&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9535%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Partial&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Baseline Profile을 기반으로 AOT 컴파일&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 실제 배포 환경과 가장 유사한 상태&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.9535%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Full&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- AOT만 사용해 컴파일&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 실제 기기에서는 거의 발생하지 않아 벤치마크 참고용으로만 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Partial에는 Baseline Profile 사용 여부를 제어하는 BaselineProfileMode 옵션이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Require&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 81.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Baseline Profile이 &lt;b&gt;반드시&lt;/b&gt; 존재해야 하며 없으면 테스트가 실패&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Profile이 정상적으로 포함됐는지 검증할 때 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;UseIfAvailable&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 81.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Baseline Profile이 있으면 사용하고 없으면 그냥 실행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 프로파일 유무와 관계없이 측정을 이어가고 싶을 때 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Disable&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 81.0465%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&amp;nbsp;&lt;/span&gt;Baseline Profile이 있어도 &lt;b&gt;무시&lt;/b&gt;하고 JIT 방식으로만 실행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Profile 미적용 상태를 명시적으로 재현할 때 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Metrics&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;metrics는 measureRepeated에서 &lt;b&gt;무엇을 측정할지&lt;/b&gt; 지정하는 파라미터다. 리스트 형태로 여러 개를 동시에 측정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;u&gt;&lt;b&gt;StartupTimingMetric&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱 시작 시간을 측정하는 Metric으로 내부적으로 두 가지 시간을 측정한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;timeToInitialDisplay (TTID)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Choreographer가 UI 렌더링을 시도하며 앱 시작 후 첫 프레임이 화면에 그려질 때까지 걸린 시간을 측정한다.&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt; Choreographer&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하드웨어가 주기적으로 발생시키는 Vsync 신호 수신해 UI 업데이트, 애니메이션등 UI 렌더링 타이밍을 조율하는 클래스&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt; VSync(Vertical Synchronization) 신호&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;디스플레이 하드웨어가 화면을 위에서 아래로 한 줄씩 그리며 한 프레임을 다 그리면 다음 프레임 준비를 위해 발생시키는 타이밍 신호다. VSync가 없으면 GPU가 아무 타이밍에나 화면에 새 프레임을 밀어 넣기 때문에 디스플레이가 한 프레임을 그리는 도중에 GPU가 다음 프레임을 덮어써버리는 Tearing 현상이 발생한다&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;timeToFullDisplay(TTFD)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱 시작 시점부터 측정되며 첫 번째 프레임 렌더링 완료 이후 화면에 필요한 각종 데이터가 UI에 로딩되어 사용자가 앱을 온전히 사용할 수 있기까지의 시점을 의미한다. 단, 안드로이드 시스템은 이 시점을 정확히 알 수 없기 때문에 개발자가 직접 reportFullyDrawn()을 호출해야 측정된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            MyApp(
                onContentReady = {
                    // 콘텐츠가 완전히 로드된 시점에 호출
                    reportFullyDrawn()
                }
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;reportFullyDrawn()을 호출하지 않으면 TTID만 측정되고 TTFD는 결과에 포함되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;StartupTimingMetric 외에도 다양한 Metric이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 24.3023%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;FrameTimingMetric&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 75.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프레임 렌더링 시간 측정. Jank 발생 여부 확인에 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 24.3023%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TraceSectionMetric&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 75.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특정 커스텀 트레이스 구간의 실행 시간 측정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 24.3023%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MemoryUsageMetric&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 75.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱의 메모리 사용량 측정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 24.3023%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PowerMetric&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 75.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배터리 소모량 측정 (API 29 이상, 실제 기기 필요)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;u&gt;&lt;b&gt;StartupMode&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qAq1G/dJMcagx0ctu/KkrcYuPLpM3W8m3ljKB7Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qAq1G/dJMcagx0ctu/KkrcYuPLpM3W8m3ljKB7Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qAq1G/dJMcagx0ctu/KkrcYuPLpM3W8m3ljKB7Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqAq1G%2FdJMcagx0ctu%2FKkrcYuPLpM3W8m3ljKB7Y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;544&quot; height=&quot;241&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;startupMode는 앱을 &lt;b&gt;어떤 상태에서 시작할지&lt;/b&gt; 지정하는 파라미터다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;COLD&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 앱 프로세스를 완전히 종료한 후 시작&lt;br /&gt;- 가장 오래 걸리며 &lt;b&gt;최악의 시나리오&lt;/b&gt;를 측정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WARM&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 앱이 백그라운드에 있다가 다시 포그라운드로 올라오는 상태&lt;br /&gt;- 프로세스는 살아있지만 Activity는 재생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HOT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 앱과 Activity 모두 메모리에 남아있는 상태에서 재개&lt;br /&gt;- 가장 빠르며 &lt;b&gt;최선의 시나리오&lt;/b&gt;를 측정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Baseline Profile은 주로 앱을 처음 실행하는 사용자의 경험을 개선하는 것이 목적이기 때문에 StartupBenchmarks에서는 &lt;b&gt;COLD&lt;/b&gt; 모드로 측정하는 것이 일반적이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지금까지&amp;nbsp;Android&amp;nbsp;앱의&amp;nbsp;컴파일&amp;nbsp;과정부터&amp;nbsp;시작해&amp;nbsp;Baseline&amp;nbsp;Profile이&amp;nbsp;무엇인지,&amp;nbsp;어떻게&amp;nbsp;생성하고&amp;nbsp;측정하는지까지&amp;nbsp;전체적인&amp;nbsp;흐름을&amp;nbsp;살펴봤다.&amp;nbsp;정리하자면&amp;nbsp;Android는&amp;nbsp;Dalvik의&amp;nbsp;인터프리터&amp;nbsp;방식에서&amp;nbsp;출발해&amp;nbsp;JIT,&amp;nbsp;AOT를&amp;nbsp;거쳐&amp;nbsp;현재의&amp;nbsp;하이브리드&amp;nbsp;방식으로&amp;nbsp;발전해&amp;nbsp;왔다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 과정에서 Cloud Profile과 Baseline Profile이 등장했고 Baseline Profile은 개발자가 직접 Critical User Journey를 정의해 첫 실행부터 빠른 성능을 보장할 수 있다는 점에서 강력한 도구다. 앱 시작 속도 개선, Jank 감소, 사용자 이탈률 감소로 이어질 수 있기 때문에 프로덕션 앱이라면 적용을 적극적으로 고려해 볼 만하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음 글에선 실제 프로젝트에서 적용하는 방법에 대해 소개하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Reference&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://www.youtube.com/watch?v=E6PoIYC9aH4&amp;amp;t=2037s&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[DroidKnights 2025] 송상윤 - Benchmark와 BaselineProfile을 사용해 LazyColumn 스크롤 성능을 75% 개선하기까지의 여정&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://velog.io/@squart300kg/MacroBenchmark-Baselin-Profile%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EC%97%AC%EC%A0%95#5-%EC%B0%B8%EA%B3%A0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MacroBenchmark &amp;amp; Baselin Profile을 사용한 성능 개선 여정 - 1편&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developers-kr.googleblog.com/2022/03/improving-app-performance-with-baseline.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Baseline Profile&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://codelabs.developers.google.com/android-baseline-profiles-improve?hl=ko#0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Baseline Profile Codelab&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://namu.wiki/w/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%20%EB%9F%B0%ED%83%80%EC%9E%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;안드로이드 런타임 나무위키&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Android_Runtime&quot;&gt;Android Runtime wikipedia&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/performance/baselineprofiles/overview#measuring-improvements&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android Baseline Profiles Overview&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 131.5px; top: 3144px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/188</guid>
      <comments>https://chanho-study.tistory.com/188#entry188comment</comments>
      <pubDate>Mon, 16 Mar 2026 16:12:02 +0900</pubDate>
    </item>
    <item>
      <title>Android Bitmap과 메모리 최적화</title>
      <link>https://chanho-study.tistory.com/187</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;픽셀&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pyPl4/dJMb99SVy2v/iFWfXmqphRizKVgZ9cAsH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pyPl4/dJMb99SVy2v/iFWfXmqphRizKVgZ9cAsH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pyPl4/dJMb99SVy2v/iFWfXmqphRizKVgZ9cAsH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpyPl4%2FdJMb99SVy2v%2FiFWfXmqphRizKVgZ9cAsH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;319&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;픽셀(Pixel)은 Picture Element의 줄임말로, 디지털 이미지를 구성하는 최소 단위다. 각 픽셀은 빨강(R), 초록(G), 파랑(B), 투명도(A, Alpha) 네 가지 채널로 표현되며 각 채널은 0~255 범위의 값을 1byte로 저장한다. 따라서 &lt;b&gt;ARGB_8888&lt;/b&gt; 포맷 기준으로 픽셀 하나는 4byte를 차지한다. ARGB_8888이 포맷에 대해서는 추후 자세히 다루겠다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;색상&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #ee2323; font-family: 'Noto Serif KR';&quot;&gt;Red&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #009a87; font-family: 'Noto Serif KR';&quot;&gt;Green&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;Blue&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Alpha&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;빨강색&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;파란색&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;흰색&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;255&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;투명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;비트맵&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비트맵(Bitmap)은 이 픽셀들이 압축 없이 메모리에 펼쳐진 형태다. JPEG, PNG 같은 이미지 파일은 압축된 상태로 저장되지만 화면에 렌더링 하기 위해서는 반드시 Bitmap으로 디코딩해야 한다. 때문에 3MB짜리 JPEG가 메모리상에서 수십 MB로 불어날 수 있으며, 메모리 사용량은 아래 공식으로 산출된다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;  &lt;/span&gt;메모리 사용량 공식&lt;/b&gt;&lt;br /&gt;Bitmap Memory = 가로(px) &amp;times; 세로(px) &amp;times; 픽셀당 바이트수&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;ContentResolver와&amp;nbsp;BitmapFactory로&amp;nbsp;Bitmap&amp;nbsp;생성하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Android에서 Uri로부터 이미지를 디코딩하는 가장 기본적인 방법은 ContentResolver로 InputStream을 열고 BitmapFactory.decodeStream()에 넘기는 것이다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771688853872&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun uriToBitmap(imageUri: Uri): Bitmap? {
    return contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        BitmapFactory.decodeStream(inputStream)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;InputStream&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;InputStream은 데이터를 &lt;b&gt;바이트 스트림 단위&lt;/b&gt;로 순차적으로 읽기 위한 파이프라인으로 openInputStream()을 통해 Uri가 가리키는 파일의 원본 바이트에 접근할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771729299359&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun uriToBitmap(imageUri: Uri): Bitmap {
    return contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        val bytes = inputStream.readBytes()

        Log.d(&quot;StreamTest&quot;, &quot;총 바이트: ${bytes.size}&quot;)
        Log.d(&quot;StreamTest&quot;, &quot;파일 헤더: ${bytes.joinToString(&quot; &quot;) { &quot;%02X&quot;.format(it) }}&quot;)

	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;바이트 스트림을 실제로 로그를 통해 확인해 본 결과 다음과 같이 16진수 값이 출력된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n05e4/dJMcadOvZiC/ffyTZg4aLtl7pwfjRdWA3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n05e4/dJMcadOvZiC/ffyTZg4aLtl7pwfjRdWA3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n05e4/dJMcadOvZiC/ffyTZg4aLtl7pwfjRdWA3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn05e4%2FdJMcadOvZiC%2FffyTZg4aLtl7pwfjRdWA3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;72&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;이를 간단하게 해석해보면 가장 앞에 있는 3byte(FF D8 FF)는&amp;nbsp; JPEG 파일의 시작을 알리는 SOI(Start of Image) 마커다. 이를 통해 나의 휴대폰으로 찍은 사진은 JPEG 형태로 저장되는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8mhRQ/dJMcaadfYga/veX7wZ4EbK4z6QWWgJ6M1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8mhRQ/dJMcaadfYga/veX7wZ4EbK4z6QWWgJ6M1k/img.png&quot; data-alt=&quot;출처 : https://www.file-recovery.com/jpg-signature-format.htm&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8mhRQ/dJMcaadfYga/veX7wZ4EbK4z6QWWgJ6M1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8mhRQ%2FdJMcaadfYga%2FveX7wZ4EbK4z6QWWgJ6M1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;739&quot; height=&quot;190&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://www.file-recovery.com/jpg-signature-format.htm&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1771729102338&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun uriToBitmap(imageUri: Uri): Bitmap? {
    return contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        BitmapFactory.decodeStream(inputStream)
    }?.also { bitmap -&amp;gt;
        Log.d(&quot;MemoryTest&quot;, &quot;allocationByteCount: ${bitmap.allocationByteCount / 1024}KB&quot;)
        Log.d(&quot;MemoryTest&quot;, &quot;size: ${bitmap.width} x ${bitmap.height}&quot;)
        Log.d(&quot;MemoryTest&quot;, &quot;config: ${bitmap.config}&quot;)
    }
}

// allocationByteCount: 48768KB
// size: 4080 x 3060
// config : ARGB_8888&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;또한 갤럭시 S25 기준으로 촬영한 이미지를 &lt;b&gt;allocationByteCount&lt;/b&gt;를 사용해 디코딩된 이미지의 크기를 측정해 본 결과 &lt;b&gt;48768KB&lt;/b&gt;가 측정되며 크기는 &lt;b&gt;4080 x 3060 가&lt;/b&gt; 측정되었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;config로 출력된&amp;nbsp;ARGB_8888은 Android Bitmap의 기본 픽셀 포맷으로 픽셀 하나의 색(ARGB)을 각각 8bit(1byte)씩 총 4 * 8 = 32bit(4byte)로 표현한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 채널이 8bit이므로 2^8에 해당하는 0 ~ 255 범위의 색을 표현할 수 있으며 4개의 채널(ARGB)을 사용하므로 도합&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;256*256*256=4,294,967,296개의 풍부한 색을 표현할 수 있지만 그 만큼 메모리를 많이 사용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞서 살펴본 Bitmap 메모리 사용량 공식을 기반으로 이번 테스트 결과를 실제로 계산해 보면&amp;nbsp;실제로 로그에서 측정된 값과 거의 근사한 수치가 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4080 &amp;times; 3060 &amp;times; 4byte (ARGB_8888) = 49,939,200 byte&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;로그로 측정된 값은 48,768KB로 계산값과 1KB 차이에 불과하며 메모리 정렬(alignment) 등 내부 처리 방식에 따라 약간의 편차가 생길 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Bitmap.Config&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Bitmap.Config는 Android Bitmap 클래스 내부에 정의된 열거형(enum)으로 픽셀 데이터를 메모리에 저장하는 방식을 결정한다. Config에 따라 픽셀당 바이트 수가 달라지므로 메모리 사용량과 색상 표현 범위에 직접적인 영향을 미친다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;1506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK3BRd/dJMcacB8eYP/bYJqRhe2nHzvUOiV0Dknj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK3BRd/dJMcacB8eYP/bYJqRhe2nHzvUOiV0Dknj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK3BRd/dJMcacB8eYP/bYJqRhe2nHzvUOiV0Dknj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK3BRd%2FdJMcacB8eYP%2FbYJqRhe2nHzvUOiV0Dknj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;443&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 143px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Config&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;픽셀당 크기&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#ALPHA_8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALPHA_8&lt;/span&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Alpha 채널만 저장하여 RGB 색상 정보를 표현 불가&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;- 마스크 처리나 그림자 효과처럼 투명도만 필요한 경우에 활용&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_4444&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;R&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;G&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;_4444&lt;/span&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- ARGB &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;각 채널을 4bit로 표현해 픽셀당 총 4 *4 = 16bit(2byte)로 표현&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;색상 표현력이 낮아 다양한 색을 표현할 수 없어 API Level 29부터 Deprecated&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_8888&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;A&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;R&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;G&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;_8888&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- ARGB 각 채널을 8bit로 표현해 픽셀당 8 * 4 = 32bit(4byte)로 표현&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;색상 품질과 표현력이 가장 뛰어나 대부분의 이미지 처리에서 사용&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Android Bitmap의 기본 Config&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#HARDWARE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HARDWARE&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;픽셀 데이터를 CPU 메모리가 아닌 GPU 메모리에 저장&lt;/span&gt;&lt;br /&gt;- 화&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;면 렌더링 성능이 향상되지만 &lt;br /&gt;- 픽셀 데이터를 CPU에서 직접 읽거나 쓸 수 없어 수정 등의 작업 불가능&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#RGBA_1010102&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;R&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;G&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A_1010102&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- R, G, B 채널을 10bit,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; Alpha 채널을 2bit&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;로 (10 * 3) + 2 = 32bit(4byte) 표현&lt;/span&gt;&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ARGB_8888보다 색상 표현력이 더 우수&lt;br /&gt;- HDR 콘텐츠 처리에 적합&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#RGBA_F16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;R&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;G&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A_F16&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;각 채널을 16bit 부동소수점으로 저장해 &amp;nbsp;16 * 4 = 64bit(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;8byte&lt;/span&gt;)로 표현&amp;nbsp;&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;광색역이미지나 HDR 처리처럼 높은 정밀도가 요구되는 작업에 활용&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;메모리 사용량이 ARGB_8888의 두 배&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config#RGB_565&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;R&lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;G&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;B&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;_565&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.7287%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2byte&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.0542%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 알파 채널이 없어 투명도를 표현 불가&lt;br /&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;R에 5bit, G에 6bit, B에 5bit로 5 + 6 + 5 = 16byte(2byte)로 표현&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;- 투명도가 필요 없는 이미지(e.g. 배경 이미지, 썸네일)에 적합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ARGB_8888 대비 메모리를 절반으로 줄일 수 있지만 그 만큼 색상 표현 범위가 좁음&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HDR 콘텐츠&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;High Dynamic Range의 약자로, 아주 어두운 부분과 아주 밝은 부분을 동시에 더 자세하게 표현하는 기술&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;e.g.&amp;nbsp; 햇빛이 강하게 비치는 하늘, 야경 사진, 역광&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;inPreferredConfig&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Bitmap의&amp;nbsp;픽셀&amp;nbsp;포맷은&amp;nbsp;BitmapFactory.Options의&amp;nbsp;inPreferredConfig를&amp;nbsp;사용해&amp;nbsp;직접&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;실제로&amp;nbsp;각&amp;nbsp;Config별로&amp;nbsp;메모리&amp;nbsp;사용량을&amp;nbsp;측정해본&amp;nbsp;결과는&amp;nbsp;다음과&amp;nbsp;같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771738854545&quot; class=&quot;reasonml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun uriToBitmap(imageUri: Uri): Bitmap? {
    val options = BitmapFactory.Options().apply {
        inPreferredConfig = Bitmap.Config.RGB_565
    }

    return contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        BitmapFactory.decodeStream(inputStream, null, options)
    }?.also { bitmap -&amp;gt;
        Log.d(&quot;MemoryTest&quot;, &quot;[RGB_565]&quot;)
        Log.d(&quot;MemoryTest&quot;, &quot;allocationByteCount: ${bitmap.allocationByteCount / 1024}KB&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOdQ3Z/dJMcafeAUeL/hSKqbM7Nf8WAMlRYYKsX80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOdQ3Z/dJMcafeAUeL/hSKqbM7Nf8WAMlRYYKsX80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOdQ3Z/dJMcafeAUeL/hSKqbM7Nf8WAMlRYYKsX80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOdQ3Z%2FdJMcafeAUeL%2FhSKqbM7Nf8WAMlRYYKsX80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;344&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;측정 결과ARGB_8888과 RGBA_1010102는 모두 48,768KB로 동일하게 측정되었다. 두 포맷 모두 픽셀당 4byte를 사용하기 때문에 메모리 사용량은 같지만 RGBA_1010102는 RGB 채널에 각각 10bit를 할당해 더 넓은 색상 범위를 표현할 수 있다는 점에서 차이가 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;RGBA_F16은 97,537KB로 ARGB_8888의 약 두 배에 해당했으며, RGB_565는 24,384KB로 ARGB_8888의 절반 수준이였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;지금까지 사용한 방식은 가장 단순한 디코딩 방법이지만 치명적인 문제가 있다. decodeStream은 이미지를 원본 해상도 그대로 디코딩하기 때문에 여러 장의 사진을 동시에 처리할 경우 OOM(Out Of Memory)나 이미지 로딩 지연 문제가 발생할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;또한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RGB_565를 사용할 경우 메모리가 줄어드는 이점이 있지만 원본 이미지의 알파를 표현할 수 없기 때문에 원본 품질 유지가 중요할 경우 사용할 수 없다는 제약이 있다. 때문에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RGB_565외에도 비트맵의 메모리를 최적화할 수 있는 방법에 대해서 알아보겠다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;inSampleSize&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미지를 디코딩할 때 원본 해상도보다 축소된 크기로 디코딩 할 수 있도록 샘플링 비율을 지정하는 옵션이다. 예를 들어 inSampleSize = 2로 설정하면 가로, 세로 각각 절반 크기로 디코딩되어 메모리 사용량이 원본의 4분의1 수준으로 줄어든다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 위해 먼저 inJustDecodeBounds = true를 설정한다. &lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;inJustDecodeBounds는&lt;/span&gt; 실제 픽셀 데이터를 메모리에 올리지 않고 이미지의 원본 크기(width, height)만 먼저 읽어오는 기능이다. 그 후 읽어온 원본 크기와 목표 크기를 바탕으로 적절한 inSampleSize를 계산한 뒤 실제 디코딩을 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771740961767&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; private fun uriToBitmap(imageUri: Uri): Bitmap {
    // 1단계: 실제 디코딩 없이 이미지 크기만 먼저 읽기
    val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
    contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        BitmapFactory.decodeStream(inputStream, null, bounds)
    }

    Log.d(&quot;MemoryTest&quot;, &quot;[After-Sample] 원본 크기: ${bounds.outWidth} x ${bounds.outHeight}&quot;)

    // 2단계: sampleSize 계산 후 실제 디코딩
    val options = BitmapFactory.Options().apply {
        inSampleSize = calculateInSampleSize(bounds, 1920, 1080)
    }

    return contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
        BitmapFactory.decodeStream(inputStream, null, options)
    }?.also { bitmap -&amp;gt;
        Log.d(
            &quot;MemoryTest&quot;,
            &quot;[After-Sample] allocationByteCount: ${bitmap.allocationByteCount / 1024}KB&quot;
        )
        Log.d(&quot;MemoryTest&quot;, &quot;[After-Sample] 실제 디코딩 크기: ${bitmap.width} x ${bitmap.height}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;calculateInSampleSize는 &lt;a href=&quot;https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 가이드하는 함수로 원본 크기와 목표 크기를 입력받아 적절한 inSampleSize를 계산하는 함수다. inSampleSize는 반드시 2의 거듭제곱(1, 2, 4, 8 ...)으로 지정해야 하며 이를 위해 sampleSize를 2씩 곱해가며 축소된 크기가 목표 크기 이상을 유지하는 최대값을 구한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771746827619&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height &amp;gt; reqHeight || width &amp;gt; reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize &amp;gt;= reqHeight &amp;amp;&amp;amp; halfWidth / inSampleSize &amp;gt;= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식을 적용한 결과 4080 x 3060의 원본 이미지가 목표 해상도 1920 x 1080에 가깝게 축소되어 디코딩되므로 앞서 측정한 48,768KB에 비해 메모리 사용량을 크게 줄일 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhJLc6/dJMcafZUqHY/3tcX0cLOaJvpHKtEBxknNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhJLc6/dJMcafZUqHY/3tcX0cLOaJvpHKtEBxknNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhJLc6/dJMcafZUqHY/3tcX0cLOaJvpHKtEBxknNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhJLc6%2FdJMcafZUqHY%2F3tcX0cLOaJvpHKtEBxknNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;141&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제로 측정해본 결과 원본 크기는 4080 &amp;times; 3060에서 inSampleSize = 2가 적용되어 실제 디코딩 크기는 2040 &amp;times; 1530으로 가로, 세로 각각 절반으로 축소되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 결과 메모리 사용량은 12,192KB로 에서 절반으로 줄어들었으며, 아무 옵션도 적용하지 않은 상태인 48,768KB 대비 약 1/4 수준으로 감소했다. 이는 앞서 설명한 메모리 공식으로도 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2040 &amp;times; 1530 &amp;times; 4byte(ARGB_8888) = 12,192KB&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지금까지 픽셀과 비트맵의 개념부터 시작해 Android에서 Bitmap이 메모리에 올라가는 과정과이를 최적화하는 방법까지 살펴봤다. 핵심을 정리하면 이렇다. 우리가 다루는 이미지 파일은 압축된 형태로 저장되어 있지만 화면에 렌더링하기 위해선 반드시 Bitmap으로 디코딩해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서 메모리 사용량은 파일 크기가 아닌 가로 &amp;times; 세로 &amp;times; 픽셀당 바이트 수로 결정되며, 아무런 최적화 없이 디코딩할 경우 고해상도 이미지 하나가 수십 MB의 메모리를 점유할 수 있다. 이를 해결하기 위해 inJustDecodeBounds로 실제 디코딩 없이 원본 크기를 먼저 읽어온 뒤, inSampleSize를 활용해 목표 해상도에 맞게 축소 디코딩하는 방식을 적용했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;갤럭시 S25 기준 실측 결과 48,768KB였던 메모리 사용량이 12,192KB로 약 75% 감소했다. 다만 inSampleSize는 2의 거듭제곱 단위로만 축소되는 특성상 목표 해상도에 정확히 맞출 수 없다는 제약이 있다. 환경에 따라 이미지의 특성과 요구사항에 맞게 조합하는 것이 중요하다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;참조&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/graphics/Bitmap.Config&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bitmap.Config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://android.googlesource.com/platform/frameworks/base.git/+/7f9f99ea11051614a7727dfb9f9578b518e76e3c/graphics/java/android/graphics/Bitmap.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android Bitmap Source Code&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://android.googlesource.com/platform/frameworks/base/+/master/graphics/java/android/graphics/BitmapFactory.java&quot;&gt;Android BitmapFactory Source Code&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://www.file-recovery.com/jpg-signature-format.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPG Signature Format&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 67.5px; top: 1849.95px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/187</guid>
      <comments>https://chanho-study.tistory.com/187#entry187comment</comments>
      <pubDate>Sun, 22 Feb 2026 12:10:20 +0900</pubDate>
    </item>
    <item>
      <title>Android ExifInterface를 활용해 촬영한 사진이 회전하는 문제 해결하기</title>
      <link>https://chanho-study.tistory.com/186</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Exif 태그를 이용한 사진 수정.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dybYR6/dJMcagYJ2eQ/6KIG7ZR1IYEi5RlIBAAEMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dybYR6/dJMcagYJ2eQ/6KIG7ZR1IYEi5RlIBAAEMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dybYR6/dJMcagYJ2eQ/6KIG7ZR1IYEi5RlIBAAEMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdybYR6%2FdJMcagYJ2eQ%2F6KIG7ZR1IYEi5RlIBAAEMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;797&quot; height=&quot;531&quot; data-filename=&quot;Exif 태그를 이용한 사진 수정.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 개발 중인 앱에서 사진을 촬영하고 서버에 업로드하는 과정에서 사진이 회전되는 문제가 발생했다. 함께 개발 중인 페어께서 이 문제를 발견하셨고 &quot;회전 메타 데이터 활용해서 사진 안 돌아가게 수정해 주세요&quot;라는 요구 사항을 전달받았다. 이와 관련된 내용을 조사하던 중 Exif 태그라는 것의 존재를 알게 되었고 이를 활용해 문제를 해결한 방법을 소개하고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;134-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1733&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zgnnS/dJMcajgOGHB/837owuOQrGKO159wKx5CY0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zgnnS/dJMcajgOGHB/837owuOQrGKO159wKx5CY0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zgnnS/dJMcajgOGHB/837owuOQrGKO159wKx5CY0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/zgnnS/dJMcajgOGHB/837owuOQrGKO159wKx5CY0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;750&quot; data-filename=&quot;134-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1733&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;기존 코드&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;촬영된 이미지를 서버에 업로드하기 위해 Uri 형태의 이미지를 ByteArray로 변환하는 방식을 사용했다. UI Layer에서 사용의 편의를 위해 Context의 확장함수로 선언했으며 이 함수는 Android 시스템이 제공하는 Uri를 실제 이미지 데이터로 읽어 들인 뒤 ByteArray로 압축 변환하여 반환한다. 동작 과정은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771030546723&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun Context.uriToByteArray(imageUri: Uri): ByteArray?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Uri는 단순한 경로 정보이기 때문에 바로 이미지 데이터에 접근할 수 없다. ContentResolver를 사용해 InputStream을 열어 실제 파일 데이터를 읽는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771030946792&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;contentResolver.openInputStream(imageUri)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;InputStream으로 읽어온 이미지를 Bitmap 객체로 변환한다. 즉, 파일 형태의 이미지를 Bitmap(메모리 상의 이미지 객체) 로 변환하는 과정이다. 이 단계에서 이미지 픽셀 데이터가 메모리에 로드된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771030990043&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val bitmap = BitmapFactory.decodeStream(inputStream)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Bitmap을 JPEG 형식으로 압축 품질 90%로 설정하고 최종적으로 서버 전송이 가능한 ByteArray 형태로 변환한다. bitmap.recycle()은 Bitmap이 사용 중인 픽셀 메모리를 즉시 해제하여 메모리 사용량을 줄이는 메서드이다. 호출 이후에는 Bitmap을 다시 사용할 수 없으며 접근 시 예외가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771031067830&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ByteArrayOutputStream().use { outputStream -&amp;gt;
    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
    bitmap.recycle()
    outputStream.toByteArray()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uDec9/dJMcaiCeKft/1KaiT5GFwN6rPzaKGo2oa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uDec9/dJMcaiCeKft/1KaiT5GFwN6rPzaKGo2oa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uDec9/dJMcaiCeKft/1KaiT5GFwN6rPzaKGo2oa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuDec9%2FdJMcaiCeKft%2F1KaiT5GFwN6rPzaKGo2oa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;539&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전체 코드는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771030475939&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun Context.uriToByteArray(imageUri: Uri): ByteArray? {
    return try {
        contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
            val bitmap = BitmapFactory.decodeStream(inputStream) ?: return null

            ByteArrayOutputStream().use { outputStream -&amp;gt;
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
                bitmap.recycle()
                outputStream.toByteArray()
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Exif란?&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Exif는 &lt;b&gt;Exchangeable Image File Format&lt;/b&gt;의 줄임말로 사진 파일 안에 함께 저장되는 메타데이터(metadata) 이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 이 메타데이터에는 카메라로 촬영한 사진의 촬영 날짜나 시간, 카메라 기종 및 설정 값, GPS 위치 정보, 그리고 회전 정보(Orientation)등 다양한 정보를 가진다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;1252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yv3zZ/dJMcagxEVWN/o2KVZTxrGV3EQYHymHqaGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yv3zZ/dJMcagxEVWN/o2KVZTxrGV3EQYHymHqaGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yv3zZ/dJMcagxEVWN/o2KVZTxrGV3EQYHymHqaGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyv3zZ%2FdJMcagxEVWN%2Fo2KVZTxrGV3EQYHymHqaGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;573&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;1252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 중 특히 이번 문제와 관련된 것은 &lt;b&gt;Orientation&lt;/b&gt; 태그다. 스마트폰으로 세로 방향 사진을 찍으면 카메라 센서가 실제로는 가로 기준으로 저장된다는 점 때문에 Exif에 회전 정보만 기록하고 실제 이미지는 회전하지 않는다는 점이 문제의 원인이었다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Android에서 Exif 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드에선 &lt;a href=&quot;https://developer.android.com/jetpack/androidx/releases/exifinterface&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ExifInterface&lt;/a&gt;를 사용해 Exif 정보를 읽고 쓸 수 있으며 JPEG,&amp;nbsp;PNG,&amp;nbsp;HEIF,&amp;nbsp;DNG&amp;nbsp;등&amp;nbsp;주요&amp;nbsp;이미지&amp;nbsp;형식의&amp;nbsp;Exif&amp;nbsp;데이터&amp;nbsp;읽기를&amp;nbsp;지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771032462259&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Exifinterface &amp;nbsp;|&amp;nbsp; Jetpack &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Exifinterface 이미지 파일 EXIF(데이터) 태그를 읽고 씁니다. 최근 업데이트 안정화 버전 출시 후보 버전 베타 버&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/jetpack/androidx/releases/exifinterface&quot; data-og-url=&quot;https://developer.android.com/jetpack/androidx/releases/exifinterface?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/IuNYf/dJMb8RRN6mm/3nr2EUggvPYOrtBOEgcWzk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/jetpack/androidx/releases/exifinterface&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/jetpack/androidx/releases/exifinterface&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/IuNYf/dJMb8RRN6mm/3nr2EUggvPYOrtBOEgcWzk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Exifinterface &amp;nbsp;|&amp;nbsp; Jetpack &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Exifinterface 이미지 파일 EXIF(데이터) 태그를 읽고 씁니다. 최근 업데이트 안정화 버전 출시 후보 버전 베타 버&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ExifInterface를 사용하기 위해선 모듈 수준의 build.gradle.kts에 의존성을 추가해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771032362709&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[versions]
exifinterface = &quot;1.4.2&quot;

[libraries]
androidx-exifinterface = { group = &quot;androidx.exifinterface&quot;, name = &quot;exifinterface&quot;, version.ref = &quot;exifinterface&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 170px; top: 1767.02px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Orientation 값 읽기 &lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이전과 동일하게 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ContentResolver를 사용해 InputStream을 열어 실제 파일 데이터를 읽고, 이 데이터를 ExifInterface 객체에 전달해 다양한 속성을 가져올 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사진의 회전 정보는 ExifInterface의 &lt;b&gt;TAG_ORIENTATION&lt;/b&gt;을 통해 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771033043440&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun orientation(imageUri: Uri): Int =
	contentResolver.openInputStream(imageUri)?.use { exifStream -&amp;gt;
		ExifInterface(exifStream).getAttributeInt(
		ExifInterface.TAG_ORIENTATION,
		ExifInterface.ORIENTATION_NORMAL,
	)
} ?: ExifInterface.ORIENTATION_NORMAL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 얻을 수 있는 Orientation 값은 다음과 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ORIENTATION_NORMAL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회전 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ORIENTATION_ROTATE_90&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;90도 회전&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ORIENTATION_ROTATE_180&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;180도 회전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ORIENTATION_ROTATE_270&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;270도 회전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이미지 회전&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Exif를 통해 이미지의 Orientation 값을 확인했다면 해당 값만큼 실제 이미지를 회전시켜 주어야 한다. Exif는 단순히 &amp;ldquo;이 이미지는 90도 회전된 상태다&amp;rdquo;라는 메타 정보만 제공할 뿐, 실제 픽셀 데이터 자체를 회전시키지는 않기 때문이다. 따라서 업로드 전에 Bitmap을 직접 회전하여 픽셀 자체를 올바른 방향으로 보정하는 과정이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771033648394&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val rotatedBitmap =
    when (orientation) {
        ExifInterface.ORIENTATION_ROTATE_90 -&amp;gt; rotate(bitmap, 90f)
        ExifInterface.ORIENTATION_ROTATE_180 -&amp;gt; rotate(bitmap, 180f)
        ExifInterface.ORIENTATION_ROTATE_270 -&amp;gt; rotate(bitmap, 270f)
        else -&amp;gt; bitmap
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미지를 회전시킬 때는 Matrix라는 객체를 사용하는데, Matrix는 안드로이드 그래픽 시스템에서 제공하는 2차원 좌표 변환을 담당하는 클래스다. Matrix는 이동, 회전, 확대/축소, 뒤집기, 기울이기와 같은 같은 변환을 지원한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회전은&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;Matrix.postRotate()를&amp;nbsp;통해&amp;nbsp;각도를&amp;nbsp;적용한&amp;nbsp;뒤,&amp;nbsp;Bitmap.createBitmap()으로&amp;nbsp;변환된&amp;nbsp;Bitmap을&amp;nbsp;새로&amp;nbsp;생성하는&amp;nbsp;방식으로&amp;nbsp;처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771034328264&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun rotate(image: Bitmap, degree: Float): Bitmap {
    val matrix = Matrix().apply { postRotate(degree) }
    return Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전체 코드는 다음과 같다. 이미지를 변환에 대한 전반적인 책임을 가진 객체와 ExifInterface를 사용해 실제 이미지를 회전하는 객체를 분리하여 객체의 역할과 책임을 분리하는데 중점을 두었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771034919696&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ImageGenerator(
    private val contentResolver: ContentResolver,
    private val rotator: Rotator,
) {
    /**
     * 주어진 [Uri]로부터 이미지를 읽어 JPEG 형식의 [ByteArray]로 변환한다.
     *
     * 내부 동작 과정:
     * 1. [android.content.ContentResolver.openInputStream]으로 InputStream을 연다.
     * 2. [android.graphics.BitmapFactory.decodeStream]으로 Bitmap 디코딩
     * 3. JPEG(품질 90) 압축 후 ByteArray 반환
     *
     * 실패 케이스:
     * - InputStream 열기 실패
     * - 디코딩 실패 (손상 이미지 등)
     * - 압축 실패
     *
     * @param imageUri 변환할 이미지 Uri (content:// 또는 file://)
     * @return 변환 성공 시 JPEG 바이트 배열, 실패 시 null
     */
    fun uriToByteArray(imageUri: Uri): ByteArray? =
        try {
            val orientation: Int = rotator.orientation(imageUri)
            val bitmap: Bitmap = bitmap(contentResolver, imageUri)
            val rotatedBitmap =
                when (orientation) {
                    ExifInterface.ORIENTATION_ROTATE_90 -&amp;gt; rotator.rotate(bitmap, 90f)
                    ExifInterface.ORIENTATION_ROTATE_180 -&amp;gt; rotator.rotate(bitmap, 180f)
                    ExifInterface.ORIENTATION_ROTATE_270 -&amp;gt; rotator.rotate(bitmap, 270f)
                    else -&amp;gt; bitmap
                }
            if (rotatedBitmap !== bitmap) bitmap.recycle()
            byteArray(rotatedBitmap)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }

    /**
     * [Uri] 로부터 실제 [Bitmap] 을 디코딩한다.
     *
     * 새로운 InputStream을 열어 [BitmapFactory.decodeStream] 으로 변환한다.
     */
    private fun bitmap(
        contentResolver: ContentResolver,
        imageUri: Uri,
    ): Bitmap =
        contentResolver.openInputStream(imageUri)?.use { inputStream -&amp;gt;
            BitmapFactory.decodeStream(inputStream)
        } ?: throw IllegalArgumentException(IMAGE_DECODE_ERROR_MESSAGE.format(imageUri))

    /**
     * [Bitmap] 을 JPEG 형식(품질 90)으로 압축하여 [ByteArray] 로 변환한다.
     *
     * 압축 완료 후 메모리 절약을 위해 내부에서 [Bitmap.recycle] 을 호출한다.
     * 따라서 호출 이후 전달한 Bitmap은 재사용하면 안 된다.
     *
     * @param bitmap 압축 대상 Bitmap
     * @return JPEG 바이트 배열
     */
    private fun byteArray(bitmap: Bitmap): ByteArray =
        ByteArrayOutputStream().use { outputStream -&amp;gt;
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
            bitmap.recycle()
            outputStream.toByteArray()
        }

    companion object {
        private const val IMAGE_DECODE_ERROR_MESSAGE = &quot;Failed to open or decode image: %s&quot;
    }
}

class Rotator(
    private val contentResolver: ContentResolver,
) {
    /**
     * 이미지의 EXIF 메타데이터에서 Orientation 값을 읽는다.
     *
     * - NORMAL &amp;rarr; 회전 없음
     * - ROTATE_90 / 180 / 270 &amp;rarr; 해당 각도만큼 시계 방향 회전 필요
     *
     * 내부적으로 새로운 InputStream을 열어 [ExifInterface] 로 분석한다.
     *
     * @param imageUri 대상 이미지 Uri
     * @return EXIF orientation 값 (기본값: ORIENTATION_NORMAL)
     */
    fun orientation(imageUri: Uri): Int =
        contentResolver.openInputStream(imageUri)?.use { exifStream -&amp;gt;
            ExifInterface(exifStream).getAttributeInt(
                ExifInterface.TAG_ORIENTATION,
                ExifInterface.ORIENTATION_NORMAL,
            )
        } ?: ExifInterface.ORIENTATION_NORMAL

    /**
     * 주어진 [Bitmap] 을 지정한 각도만큼 회전한 새로운 Bitmap을 생성한다.
     *
     * 원본 Bitmap은 수정되지 않고,
     * 새로운 Bitmap 인스턴스가 반환된다.
     *
     * @param image 회전 대상 Bitmap
     * @param degree 시계 방향 회전 각도
     * @return 회전된 새 Bitmap
     */
    fun rotate(
        image: Bitmap,
        degree: Float,
    ): Bitmap {
        val matrix = Matrix().apply { postRotate(degree) }
        return Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 결과 이미지가 촬영한 상태로 잘 돌아가 있는것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Hl0K/dJMcadt8eDG/LOFFc6KZ4jVMKONSqJcJVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Hl0K/dJMcadt8eDG/LOFFc6KZ4jVMKONSqJcJVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Hl0K/dJMcadt8eDG/LOFFc6KZ4jVMKONSqJcJVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Hl0K%2FdJMcadt8eDG%2FLOFFc6KZ4jVMKONSqJcJVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;281&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Android</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/186</guid>
      <comments>https://chanho-study.tistory.com/186#entry186comment</comments>
      <pubDate>Sat, 14 Feb 2026 10:50:34 +0900</pubDate>
    </item>
    <item>
      <title>Compose 디자인 시스템 설계하기</title>
      <link>https://chanho-study.tistory.com/185</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xqrfp/dJMcabwdcUB/TtXbmMopdVrg3b5rnq2y2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xqrfp/dJMcabwdcUB/TtXbmMopdVrg3b5rnq2y2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xqrfp/dJMcabwdcUB/TtXbmMopdVrg3b5rnq2y2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXqrfp%2FdJMcabwdcUB%2FTtXbmMopdVrg3b5rnq2y2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;478&quot; height=&quot;478&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;디자이너와 협업 시 가장 중요한 것은 디자이너가 의도한 디자인을 정확하게 구현하는 것이다. 특정 텍스트가 어떤 폰트를 사용해야 하는지, 글자 크기는 얼마인지, 자간과 행간은 어떻게 적용되어야 하는지까지 세세하게 정해져 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;같은 스타일의 텍스트를 구현하면서도 매번 폰트나 크기, 행간이나 자간을 다시 설정해야 하고 화면이 많아질수록 이러한 작업은 반복되어 개발 생산성이 점점 저해된다. 이러한 불편함을 줄이기 위해 디자이너가 설계한 디자인을 기준으로 재사용할 수 있는 시스템을 만드는 것이 바로 디자인 시스템이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;디자인 시스템을 도입하면 디자이너의 요구사항을 일관된 방식으로 반영할 수 있고 이미 정의된 시스템을 활용해 보다 안정적으로 화면을 구현할 수 있다. 오늘은 실제 프로젝트에서 사용한 디자인 시스템을 기반으로 디자인 시스템을 설계하는 법에 대해 소개하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;색상 시스템 정의&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;디자이너가 요구한 색상을 Compose의 theme/Colors 패키지에 정의해준다. 다음은 우리 팀에서 사용하는 색상의 일부다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qfIaM/dJMcah4g6Ui/mDIYR3gVWxNb1AyXmeutz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qfIaM/dJMcah4g6Ui/mDIYR3gVWxNb1AyXmeutz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qfIaM/dJMcah4g6Ui/mDIYR3gVWxNb1AyXmeutz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqfIaM%2FdJMcah4g6Ui%2FmDIYR3gVWxNb1AyXmeutz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;299&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1769082468772&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.twix.designsystem.theme

import androidx.compose.ui.graphics.Color

object GrayColor {
    val C050 = Color(0xFFF4F4F4)
    val C100 = Color(0xFFECECEC)
    val C200 = Color(0xFFC6C6C6)
    val C300 = Color(0xFF858585)
    val C400 = Color(0xFF464646)
    val C500 = Color(0xFF171717)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;색상 값을 화면마다 직접 사용하는 대신 디자이너가 정의한 이름으로 정의된 색상을 사용함으로써 디자인 변경에 유연하게 대응할 수 있다. 만약 특정 회색 계열의 색상이 수정되더라도 해당 색상 객체만 변경하면 모든 화면에 동일하게 반영된다. 또한 디자이너와의 커뮤니케이션에서도 &amp;ldquo;이 화면에는 Gray 500을 사용했다&amp;rdquo;처럼 같은 용어를 사용할 수 있어 협업이 훨씬 수월하다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Typography 디자인 시스템 설계&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;텍스트는 제목, 본문, 설명 등 다양한 역할을 가지며 화면마다 반복적으로 사용된다. 그만큼 일관된 규칙 없이 관리하기 어려운 요소다. Typography 디자인 시스템의 핵심은 텍스트를 값이 아닌 역할 기준으로 정의하는 것이다. 다음은 우리 앱의 Typography 가이드 중 일부다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/10w1R/dJMcabwdcA7/4ZeF5lfmTskYH0c1odiPvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/10w1R/dJMcabwdcA7/4ZeF5lfmTskYH0c1odiPvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/10w1R/dJMcabwdcA7/4ZeF5lfmTskYH0c1odiPvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F10w1R%2FdJMcabwdcA7%2F4ZeF5lfmTskYH0c1odiPvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;280&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 일관된 형태로 사용할 수 있도록 enum 형태로 정의한다. 이 enum은 텍스트의 크기나 굵기와 같은 구체적인 값이 아니라 해당 텍스트가 어떤 역할을 가지는지를 나타내며 디자이너가 정의한 변수명을 그대로 사용함으로써 수월한 의사소통을 가능하게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img id=&quot;image-hover-icon&quot; style=&quot;position: absolute; width: 25px; height: 25px; cursor: pointer; left: 204px; top: 50.1875px; display: none; z-index: 10000; opacity: 0.7;&quot; src=&quot;chrome-extension://pbhpcbdjngblklnibanbkgkogjmbjeoe/src/public/images/128px.png&quot; /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769083260254&quot; class=&quot;crystal&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;enum class AppTextStyle {
    H1, H2, H3, H4,
    T1, T2, T3,
    B1, B2, B3, B4,
    C1, C2
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;AppTypography&amp;nbsp;설계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;텍스트 스타일을 역할 기준으로 정의했다면 이제 각 역할에 실제로 어떤 스타일을 적용할지 정리해야 한다. 이를 위해 모든 텍스트 스타일을 하나로 묶은 AppTypography를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769084066236&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Immutable
data class AppTypography(
    val h1: TextStyle,
    val h2: TextStyle,
    val h3: TextStyle,
    val h4: TextStyle,
    val t1: TextStyle,
    val t2: TextStyle,
    val t3: TextStyle,
    val b1: TextStyle,
    val b2: TextStyle,
    val b3: TextStyle,
    val b4: TextStyle,
    val c1: TextStyle,
    val c2: TextStyle,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AppTypography는 앱에서 사용하는 모든 텍스트 스타일을 한 곳에서 관리하기 위한 데이터 클래스다. 제목, 본문, 캡션 등 각 역할에 대응하는 TextStyle을 속성으로 가지고 있으며 이를 통해 타이포그래피 규칙을 일관되게 유지할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또한 텍스트 스타일과 관련된 모든 설정이 한 곳에 모여 있기 때문에 디자인 변경이 발생했을 때 특정 화면을 직접 수정할 필요 없이 AppTypography만 수정하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Font&amp;nbsp;Family&amp;nbsp;정의&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음으로 앱 전반에서 사용할 폰트를 정의해주어야 한다. theme/Type 패키지에 Font Family를 정의함으로써 타이포그래피에서 사용할 폰트를 한 곳에서 관리할 수 있도록 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769083610217&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.twix.designsystem.theme

val LaundryGothicFamily =
    FontFamily(
        Font(R.font.laundry_gothic_regular, FontWeight.Normal),
        Font(R.font.laundry_gothic_bold, FontWeight.Bold),
    )

val NanumSquareNeoFamily =
    FontFamily(
        Font(R.font.nanum_square_neo_light, FontWeight.Light),
        Font(R.font.nanum_square_neo_regular, FontWeight.Normal),
        Font(R.font.nanum_square_neo_bold, FontWeight.Bold),
        Font(R.font.nanum_square_neo_extra_bold, FontWeight.ExtraBold),
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이처럼 Font Family를 미리 정의해두면 이후 타이포그래피 스타일에서 어떤 폰트를 사용할지 일관되게 관리할 수 있다. 또한 폰트 변경이 필요해질 경우에도 한 곳만 수정하면 전체 화면에 적용할 수 있다는 장점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;디자인&amp;nbsp;스펙을&amp;nbsp;그대로&amp;nbsp;코드로&amp;nbsp;구현&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekyG0U/dJMcaioyZ7s/AzL89kNHdJ96RyKv1qHOa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekyG0U/dJMcaioyZ7s/AzL89kNHdJ96RyKv1qHOa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekyG0U/dJMcaioyZ7s/AzL89kNHdJ96RyKv1qHOa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekyG0U%2FdJMcaioyZ7s%2FAzL89kNHdJ96RyKv1qHOa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;715&quot; height=&quot;286&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 텍스트 스타일은 디자이너가 정의한 디자인 스펙을 기준으로 작성했다. 폰트 종류, 굵기, 글자 크기뿐만 아니라 행간(line height)과 자간(letter spacing)까지 모두 정의했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769084520899&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun provideAppTypography(): AppTypography =
    AppTypography(
        h1 =
            TextStyle(
                fontFamily = NanumSquareNeoFamily,
                fontWeight = FontWeight.Bold,
                fontSize = 28.sp,
                lineHeight = lineHeightPercent(28f, 140f),
                letterSpacing = letterSpacingPercent(28f, -1f),
            ),
        h2 =
            TextStyle(
                fontFamily = NanumSquareNeoFamily,
                fontWeight = FontWeight.Normal,
                fontSize = 24.sp,
                lineHeight = lineHeightPercent(24f, 140f),
                letterSpacing = letterSpacingPercent(24f, -1f),
            ),
	//...            
	)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;행간과 자간은 디자이너가 퍼센트 단위로 전달해주는 경우가 많아 이를 그대로 반영하기 위해 퍼센트 기반으로 계산하는 함수로 분리했다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 방식의 장점은 디자인 스펙을 직관적으로 코드로 확인할 수 있다는 점이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769084558701&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 행간 계산하는 메서드
 * */
private fun lineHeightPercent(
    fontSizeSp: Float,
    percent: Float,
): TextUnit = (fontSizeSp * (percent / 100f)).sp

/**
 * 자간 계산하는 메서드
 * */
private fun letterSpacingPercent(
    fontSizeSp: Float,
    percent: Float,
): TextUnit = (fontSizeSp * (percent / 100f)).sp&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;AppTextStyle과 AppTypography 연결&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞서 정의한 AppTextStyle enum은 텍스트의 역할만 표현한다. 실제 TextStyle과 연결하기 위해 AppTextStyle을 TextStyle로 변환하는 매핑 함수를 추가했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769084808055&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun AppTextStyle.toTextStyle(typography: AppTypography): TextStyle =
    when (this) {
        AppTextStyle.H1 -&amp;gt; typography.h1
        AppTextStyle.H2 -&amp;gt; typography.h2
        AppTextStyle.H3 -&amp;gt; typography.h3
        AppTextStyle.H4 -&amp;gt; typography.h4
        AppTextStyle.T1 -&amp;gt; typography.t1
        AppTextStyle.T2 -&amp;gt; typography.t2
        AppTextStyle.T3 -&amp;gt; typography.t3
        AppTextStyle.B1 -&amp;gt; typography.b1
        AppTextStyle.B2 -&amp;gt; typography.b2
        AppTextStyle.B3 -&amp;gt; typography.b3
        AppTextStyle.B4 -&amp;gt; typography.b4
        AppTextStyle.C1 -&amp;gt; typography.c1
        AppTextStyle.C2 -&amp;gt; typography.c2
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 매핑을 통해 UI에서는 AppTextStyle만 사용하면 되고 실제 스타일 값은 디자인 시스템 내부에서 자동으로 결정되어 결과적으로 화면 구현 코드에서 디자인과 관련된 세부 설정을 분리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;AppTypography를 전역으로 제공&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; AppTypography를 앱 전반에서 사용할 수 있도록 CompositionLocal을 사용해 전역으로 제공했다. 그리고&amp;nbsp;Theme에서&amp;nbsp;AppTypography를&amp;nbsp;한&amp;nbsp;번만&amp;nbsp;생성해&amp;nbsp;모든&amp;nbsp;Composable에&amp;nbsp;전달했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769084899935&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val LocalAppTypography =
    staticCompositionLocalOf&amp;lt;AppTypography&amp;gt; {
        error(&quot;AppTypography가 제공되지 않음&quot;)
    }

@Composable
fun TwixTheme(content: @Composable () -&amp;gt; Unit) {
    val typography = remember { provideAppTypography() }

    CompositionLocalProvider(
        LocalAppTypography provides typography,
    ) {
        content()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조를 사용하면 어떤 Composable에서도 동일한 타이포그래피 규칙을 사용할 수 있으며 디자인 시스템이 앱 전반에 자연스럽게 적용된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;공용 Text Composable 구현&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막으로 프로젝트 전역에서 앞서 설정한 디자인을 적용하는 Text Composable을 적용한다. 이를 통해 텍스트가 필요한 화면에서 원하는 style과 색상만 전달하면 앞서 구축한 디자인 시스템을 통해 일관된 디자인을 적용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769318449881&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun AppText(
    text: String,
    style: AppTextStyle,
    color: Color,
    modifier: Modifier = Modifier,
    textAlign: TextAlign? = null,
    maxLines: Int = Int.MAX_VALUE,
    overflow: TextOverflow = TextOverflow.Clip,
) {
    val typo = LocalAppTypography.current
    val baseStyle = style.toTextStyle(typo)

    CompositionLocalProvider(LocalDensity provides Density(LocalDensity.current.density, fontScale = 1f)) {
        Text(
            modifier = modifier,
            text = text,
            style = baseStyle,
            color = color,
            textAlign = textAlign,
            maxLines = maxLines,
            overflow = overflow,
        )
    }
}

// 사용하는 곳 에서...
AppText(
 text = &quot;페토&quot;,
 style = AppTextStyle.T3,
 color = GrayColor.C400,
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;참조&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/YAPP-Github/Twix-Android/tree/develop/core/design-system/src/main/java/com/twix/designsystem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Twix&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Compose Typography</category>
      <category>디자인 시스템</category>
      <category>안드로이드 디자인 시스템</category>
      <category>컴포즈 디자인 시스템</category>
      <author>빨주노초잠만보</author>
      <guid isPermaLink="true">https://chanho-study.tistory.com/185</guid>
      <comments>https://chanho-study.tistory.com/185#entry185comment</comments>
      <pubDate>Thu, 22 Jan 2026 21:36:17 +0900</pubDate>
    </item>
  </channel>
</rss>