<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dkdk_0422.log</title>
        <link>https://velog.io/</link>
        <description>오늘 하루도 화이팅</description>
        <lastBuildDate>Tue, 12 Aug 2025 11:39:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dkdk_0422.log</title>
            <url>https://velog.velcdn.com/images/dkdk_0422/profile/6580a751-66cf-49d8-b95a-a4098dacd8e7/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dkdk_0422.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dkdk_0422" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[네이버 부스트캠프 - 챌린지 회고]]></title>
            <link>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 12 Aug 2025 11:39:57 GMT</pubDate>
            <description><![CDATA[<p>벌써 챌린지를 시작한지 4주가 지났고 챌린지를 마무리하게 되었습니다.
4주라는 시간이 짧은 시간이 아닌데 휙 지나간 것 같습니다 ㅎㅎ 챌린지 과정을 진행하며 느꼈던점에 대해 적어보려고 합니다.</p>
<h3 id="들어가기-앞서">들어가기 앞서</h3>
<p>제 자신에게 <code>고생했다!!</code> 라는 말을 해주고 싶습니다.
하루 평균 4~5시간 잠자며 포기하지 않고 끝까지 도전을 마친 저 자신에게 칭찬을 해주고 싶네요.
힘들었지만 재밌었기에 끝까지 할 수 있지 않았나 생각합니다.</p>
<h3 id="4주-전의-나">4주 전의 나</h3>
<p>앞서 재밌다고 느낀 부분에서 가장 큰 점은 자신의 성장을 느낄 수 있는 부분이였습니다.
4주 전만해도 저는 <code>코더</code>라고 해도 무방할 정도로 코드를 작성할 때 설계나, 내부 cs 동작, 원리 등에 대해 깊게 생각하는 능력이 매우 부족했습니다. 또한 AI를 사용할 때도 이 문제를 해결하기 위해 &quot;어떤 코드를 수정 및 추가해야해?&quot; 등 깊게 사고하지 않고 AI에게 사고를 의존하는 질문을 많이 했습니다.</p>
<p>이런 저 자신에게 진절머리가 났고 이를 극복하기 위해 CS를 공부하고 그동안 사용하던 자료구조의 내부 구현, 사용하는 라이브러리 내부 구현 등에 대해 알고 사용하기 시작하는 습관을 가지기 시작했고 알고 사용하는 것과 모르고 사용하는 것의 엄청난 차이를 느꼈습니다.
마침 네이버 부스트캠프를 모집하고 있었고 운이 좋아 <code>챌린지</code>과정에 참여할 수 있게 되었습니다.</p>
<h2 id="챌린지를-진행하며">챌린지를 진행하며</h2>
<h3 id="챌린지-1주차">챌린지 1주차</h3>
<p><code>&quot;좋은 코드는 CS로부터 나온다&quot;</code>, <code>&quot;사용하는 것의 동작 원리를 알고 사용하자&quot;</code> 라는 마음가짐을 가지고 챌린지를 진행했고 코스 또한 CS를 집중적으로 다루어 너무 좋았습니다. 하지만 1주차에서는 주어진 미션에대해 학습과 구현의 균형을 잡는 것은 매우 어려웠고 메모리 관련 미션에서는 학습이 끝나니 오후 11시인 적도 있었습니다. 나의 학습 방법이 잘못된 것인가 라는 걱정도 있었지만 학습 + 학습한 내용을 토대로 구현 과정 자체가 재미있었습니다. 그동안 사고를 깊게하는 습관이 들여지지 않아 힘든 부분도 있었지만 이러한 습관을 잡으려 온 것이고 캠프 측에서도 좋은 말씀을 많이(&quot;나만의 학습 방법을 찾는 것이 목표&quot;, &quot;학습의 주체는 나&quot;) 해주셔서 좌절하지 않고 나아갈 수 있었습니다.</p>
<h3 id="챌린지-2주차">챌린지 2주차</h3>
<p>1주차 때 느낀 피드백을 통해 학습 + 구현에 있어 여러가지 방법을 시도하며 나에게 맞는 학습법을 찾아가는 과정이였다고 생각합니다. 또 <code>피어 컴파일링</code>, <code>피어 피드백</code> 시간의 진가를 느낄 수 있는 주 였습니다. 1주차에서는 어색하고 이런 과정 자체가 익숙하지 않았지만 2주차에서는 정말 소중한 시간이라는 것을 느낄 수 있었습니다. 피어분들의 코드를 보고 나와는 어떻게 다르게 설계하고 구현했는지, 또 그에 대한 토론을 <code>피어 피드백</code> 시간에 나눌 수 있었습니다. 피어분들의 관점, 사고방식을 접할 수 있고 이를 통해 제가 바라보지 못하는 것들을 접하고 바라볼 수 있는 의미있는 시간입니다.</p>
<h3 id="챌린지-3주차">챌린지 3주차</h3>
<p>설계 + 구현, 즉 사고하는 습관이 어느정도 들었고 설계에 많은 시간을 들이기 시작했습니다. 유지보수성과 확장성을 고려했을 때 어떤 구조로 설계해야 하며 이러한 과정을 진행하며 3주차 피어분과 테스트에 대한 토론을 심도있게 나눠볼 수 있는 기회였습니다. 좋은 테스트 코드란 무엇이고 좋은 테스트 환경을 조성하기 위해서 어떤 구조로 설계해야 하는지에 대한 서로의 생각을 나눌 수 있었고 저만의 학습 방법 또한 서서히 찾아가고 있었습니다. 피어분들과 이야기를 나누며 내가 학습한 내용을 확인하고, 학습한 내용을 바탕으로 구현하고, AI와의 퀴즈를 통해 저에게 맞는 학습 방법들을 찾아갈 수 있는 주였습니다.</p>
<h3 id="챌린지-4주차">챌린지 4주차</h3>
<p>설계하고 구현하는 것이 어느정도 익숙해지는 시간이였습니다. AI와 협업하며 AI 대답하는 코드에 대해 객체지향과 테스트를 고려했을 때 구조가 잘 설계되어있는지, 코드에 문제는 없는지, 더 효율적인 방법을 사용할 수 있는지에 대한 사고를하고 검증을 할 수 있게 되었습니다. 더 이상 사고를 AI에게 맡기지 않고 제 자신이 사고하고 AI에게는 검증과 개선을 하는 도구로 사용했습니다. 또한 새로운 내용을 학습할 때 1<del>3주차때 학습한 기본 배경들이 많이 등장했고 이러한 연결고리를 찾으며 학습하는 재미가 쏠쏠했습니다.배경 지식이 쌓이고 쌓이다 보니 연결고리를 쉽게 찾을 수 있었고 학습을 마무리하는 시간 또한 4</del>6시 사이로 1~2주차에 비해 학습과 구현의 비중을 적절히 분배할 수 있었습니다.</p>
<p>3주차부터 짝 설계가 진행되며 4주차 짝 설계를 진행하며 피어분들과 프로그램 흐름, 객체 간 의존도 흐름을 같이 설계하였습니다. 혼자 설계를 했을 때와 달리 저만의 방식을 사용하여 설계할 수 없었지만 다른 피어분들과 각자의 의견을 나누며 내가 생각하지 못한 부분을 접하고 배울 수 있었고 잊고 있던 &#39;기술 사용에 있어 익숙한 것을 사용하는 것이 아닌 각 상황을 고려하여 근거있게 사용하자&#39;라는 마음가짐을 리마인드 할 수 있어 아직까지 인상깊었던 가장 좋았던 시간이였지 않나 생각합니다.</p>
<h3 id="수료">수료</h3>
<p>주저리 주저리 각 주차별로 느낀 점을 적어보았습니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/a6a66a84-264b-47e5-8a5b-ae4307895032/image.png" alt="">
챌린지 과정에 있어 정말 모든 활동이 도움이 되었던 것 같습니다. 
항상 혼자 공부해왔던 저에게 Slack을 통해 동료분들과 소통하고 피어분들과 이야기를 나누며 서로 함께 성장하는 것 자체가 저에게 있어 정말 소중한 경험이였습니다.</p>
<p>또한 주간 피드백을 통해 나를 한 번 돌아볼 수 있는 시간 또한 주어져 많은 도움이 되었습니다. 아마 나를 돌아볼 수 있는 시간이 없었다면 내가 겪고 있는 문제에 대해 크게 인식하지 못하고 이러한 성장을 하는 데 있어 어려움이 있지 않았을까 하는 생각을 합니다.</p>
<h3 id="이제-뭐해">이제 뭐해?</h3>
<p>챌린지 과정은 저에게 있어 정말 말 그대로 <code>도전</code>그 자체였습니다.
<code>인생은 도전의 연속이다</code> 라는 말이 있습니다. 챌린지에서의 도전으로 끝내지 않고 끊임없이 도전을 할 생각입니다. 할겁니다! 챌린지 과정을 통해 진정한 <code>도전</code>의 의미를 느낄 수 있었고 앞으로의 도전을 위한 준비과정, 발판이라고 생각합니다.  4주동안 저에게 맞는 공부 방법을 어느정도 찾았고, 저 자신에 대해서도 새로운 모습을 알게되고, 무엇보다 공부하는 습관, 방법에 대해 알게되었습니다. 이러한 경험을 토대로 끊임없이 앞으로 나아가는 <code>도전</code>이라는 여행을 하려 다녀오려 합니다.</p>
<p>정말 제 인생에 있어 의미있는 4주였고 만약 부스트캠프 신청을 고민하시는 분이 있다면 &quot;일단 해봐&quot; 라는 말을 전해드리고 싶습니다. </p>
<h3 id="멤버십-입과">멤버십 입과</h3>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/b8a5f654-866e-4d45-8db5-be3a282c009d/image.png" alt="">
운이 좋게 멤버십에 입과할 수 있게 되었습니다. 챌린지에서의 도전을 발판삼아 다시 새로운 도전을 해보려 합니다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 챌린지 -  2주차 회고]]></title>
            <link>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 27 Jul 2025 08:15:11 GMT</pubDate>
            <description><![CDATA[<p>눈떠보니 부스트캠프 2주차가 지나갔습니다. 
2주차를 진행하며 느꼈던 점, 아쉬웠던 점 등 회고를 작성해보려 합니다.</p>
<h2 id="2주차에-들어서며">2주차에 들어서며</h2>
<p>2주차가 되자마자 느꼈던 점은 일단 매우 덥습니다<img src="https://velog.velcdn.com/images/dkdk_0422/post/7d6a34a9-6ffd-4dcf-9507-bd89a98e2f8d/image.jpg" alt=""></p>
<p>파워 냉방으로 틀어야한다...</p>
<p>2주차를 들어서며 달라진 점이 있다면 엉덩이가 무거워졌습니다.
원래라면 50분 공부하면 10분동안 무조건 의자에서 일어나야 했던 내가 기본 2~3시간 단위로 의자에 앉아있을 수 있게 되었습니다.</p>
<p>계속 학습 + 구현을 진행하다보니 점점 공부하는 것에 익숙해져가고 있는 것 같습니다.(칭찬해)</p>
<h2 id="1주차-피드백-반영">1주차 피드백 반영</h2>
<p><a href="https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">1주차 회고</a>에서 피드백 했던 내용으로 </p>
<ul>
<li>구현에 있어 필요한 최소한의 내용을 학습한다.</li>
<li>설계에 있어 나의 의견과 근거에 대한 설명에 대한 내용을 최대한 기술한다.</li>
<li>구현을 하다 모르는 내용이 생긴다면 그 부분에 대해 학습한다.</li>
<li>구현 과정에 있어 왜 이렇게 했고 어떤 문제점이 있었는지 학습하고 정리한다.</li>
</ul>
<p>을 기반으로 2주차를 진행해보자 마음가짐을 가졌었습니다.</p>
<p>나름? 잘 지켜진 것 같습니다. 구현할 내용은 작은 단위로 나뉘어 세분화하였고 Task 단위로 학습 + 구현으로 진행했습니다.</p>
<p>확실히 1주차에서 학습 몰빵 -&gt; 구현 방법보다는 훨씬 효율적이고 얻어가는 것이 많아 좋았습니다. 그 안에서도 아직 <code>task에서 학습의 범위를 어디까지 잡을 것인가?</code>에 대한 좋은 기준은 찾지 못했지만 1주차에서 느겼던 부족한 점을 2주차에서 실행하여 보완했다는 점에서 큰 만족감을 느끼고 있습니다. </p>
<h2 id="지속가능한-지식">지속가능한 지식</h2>
<p>특히 2주차에서는 구현에 있어 필요한 배경지식에 대해 어느정도 알고있다고 생각했습니다. 
네 착각이였습니다. 어느 정도 알고있다고 생각했고 바로 설계에 들어갔지만 알고있다고 착각한거였습니다. 
<img src="https://velog.velcdn.com/images/dkdk_0422/post/e7777a1d-b9a8-4a7c-bcb5-269537d2958c/image.png" width="400" height="200"/></p>
<p>알고 있었는데?
몰랐습니다 그냥
알고 있었었는데?
모릅니다...</p>
<p>분명히 대학시절 보았던 내용이고 한번쯤 공부했었던 기억이 나는데 기억이 나질 않습니다. 그래서 처음부터 다시 공부하고 학습정리를 기록했습니다.</p>
<p>분명 공부했던 내용이 맞는데 기억이 나질 않는 이유로 생각해보면 아마 
<code>지속가능한 지식</code>이 아니였기 때문이라는 생각이 듭니다</p>
<p>과거의 저는 학습을 <code>한 번 보고 넘기는</code> 데에 그쳤고 저만의 언어로 이해하는 시간을 가지지 않아 <code>지속불가능</code>하지 않았나 라는 생각이 듭니다.</p>
<p>결국 학습한 내용에 대해 나만의 언어로 다시 정리해주지 않으면, 나중에 잊어버리게 됩니다. 때문에 학습한 내용에 대해 나만의 언어로 소화시켰을 때 <code>지속가능한 지식</code>을 습득한다 라는 결론을 내렸습니다.</p>
<h2 id="나만의-언어로-소화시키는-법">나만의 언어로 소화시키는 법</h2>
<p>2주차를 진행하며 가장 기억에 남는 점으로 학습한 내용을 나만의 언어로 소화시키는 법을 찾았다는 것입니다.</p>
<p>2주차 피어 피드백에서는 동료분들과 학습한 내용에 대해 다양한 의견을 나누고, 설계 과정에서 겪었던 고민들에 대해서도 아주 생산적인 대화를 나눌 수 있었습니다.
특히 인상 깊었던 점은, 동료들과 나눴던 대화는 머릿속에 강하게 남아있었고, 그 내용을 나중에 다시 떠올렸을 때 정확하게 이해하고 있다는 확신이 들었다는 점입니다.</p>
<p>이 경험을 통해 딱 느꼈습니다.
단순히 머릿속으로 정리하는 것과, 직접 말로 설명해보는 것 사이에는 큰 차이가 있다는 것을요.</p>
<p>이후 저는 학습한 내용을 다시 정리할 때, 말로 설명하는 습관을 의도적으로 시도해보았습니다.
실제로 그렇게 했을 때, 내가 제대로 이해하지 못한 부분은 말이 막히는 순간으로 드러났고,
반대로 설명이 자연스럽게 나오는 부분은 저만의 언어로 소화되었다는 신호라는 걸 느꼈습니다.</p>
<p>학습 정리도 매우 중요하지만 말로 설명할 수 있을 때 비로소 제 지식이 된다는 것을 알게되었습니다.</p>
<h2 id="아쉬운점">아쉬운점</h2>
<p>너무 학습 + 구현에 중점을 두지 않았나 생각합니다.
마지막 피어 피드백 시간에 운영진분께서 들어오셔서 좋은 말씀을 해주고 가셨습니다.
다른 동료의 학습 저장소를 보고 &#39;와 잘했다&#39;가 아니라 &#39;왜 잘했을까?&#39;, &#39;나와 다른점을 뭘까&#39;에 대해 생각해 보면 더 좋을 것 같다 라는 말씀을 주셨습니다.(앞으로의 학습 방향성에 대한 좋은 말씀을 해주셔서 정말 감사합니다)</p>
<p>그래서 저의 README파일과 학습정리 파일을 되돌아보게 되었습니다.
두 파일 모두 정리정돈이 되지 않고 난잡하고, 가독성이 많이 떨어진다고 생각했습니다. readme 파일은 내 저장소를 대표하는 파일이고 첫인상을 결정하는데 난잡하고 잘 정리되어있지 않으면 &#39;읽기 싫겠다&#39; 라는 생각을 하게 되었습니다. </p>
<p>학습 + 구현도 중요하지만 내가 진행한 내용에 대해 나의 생각을 잘 정리해서 표현하는 능력 또한 매우 중요합니다.</p>
<p>그래서 3주차에는 내가 학습하고 생각했던 내용을 잘 정리하여 문서화하는 연습을 해보려 합니다. </p>
<h2 id="마무리">마무리</h2>
<p>자는 시간은 점점 늦어지고 있는데 오히려 더 재미를 느끼고 있는 것 같습니다. 회고를 작성해주신 동료분의 말씀으로 </p>
<pre><code>마라토너들이 느끼는 러닝 하이를
나는 개발을 하다가 느낀 기분</code></pre><p>정말 이게 맞는 것 같습니다.
이번 주차에서 아쉬웠던 점을 반영해 다음 회고를 작성할 때는 한층 더 성장해있는 &#39;나&#39;가 되었으면 좋겠습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 - 챌린지 1주차 회고]]></title>
            <link>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dkdk_0422/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 20 Jul 2025 00:06:09 GMT</pubDate>
            <description><![CDATA[<p>네이버 부스트캠프 챌린지 과정에 입과하게 되었습니다!!
벌써 1주차가 지났고 이에 대한 회고를 작성해볼까 합니다.</p>
<h2 id="입과-전-심정">입과 전 심정</h2>
<p>챌린지 과정에서는 매일매일 미션이 주어지고 이에대해 학습 + 구현해야 하는 과정을 진행합니다.
입과 전 저는 <strong>미션</strong>에 대해 이러한 생각을 가지고 있었습니다.</p>
<ul>
<li>미션 시간이 얼마나 걸리든 해결해주마 뒤졌다 ㅋㅋ</li>
<li>요구사항에 대한 정리도 깔끔하게하고 설계 + 구현 완벽하게 해봐야지
<img src="https://velog.velcdn.com/images/dkdk_0422/post/909e0e50-1876-4b12-b77b-25df97d0032a/image.png" alt="첫번째 레슨"></li>
</ul>
<p>&quot;저는 평소에 열정만 있으면 안 되는게 없다&quot; 라는 생각을 가진 소위 머리깨진 사람이였습니다.
챌린지 미션을 수행하며 머리가 봉합된 것 같습니다 감사합니다
안 되는 게 있을지도?...</p>
<p>라고 했지만 <code>열정만 있으면 다 된다</code> 라는 생각은 여전히 유효합니다.</p>
<h2 id="첫인상">첫인상</h2>
<p>미션을 진행하기 전 마스터님께서 미션의 진행방향에 대해 알려주셨습니다.
요약하자면 학습과 구현의 균형점을 잘 유지하고 나에게 맞는 학습 방법을 찾아가는 것이였습니다.</p>
<p>구현을 위해서는 학습이 필요합니다.
구현에 필요한 내용을 학습하다 다른 길로 새보신 적이 있으신가요?
이를 <a href="https://money-mind.tistory.com/3">야크털깎기</a>라고 합니다. 궁금하신분은 읽어보시면 재밌습니다.</p>
<p>균형점을 잘 찾고 털만 깎으면 안된다.. 아직 크게 마음에 와닿지는 않았습니다.
배경지식에 따라 학습과 구현의 균형이 미션마다 달라지지 않을가? 라고 생각하고 있었고 잘 잡을 자신이 있었습니다.</p>
<p>각자의 배경지식마다 차이가 있겠지만 미션의 요구사항은 그리 쉽지 않았고 학습을 많은 필요로 했습니다.
각 미션마다 학습을 필요로 하는 시간 또한 달랐지만 <code>학습과 구현</code>의 균형점을 맞추는 것은 매우 어려웠습니다.</p>
<h2 id="일단-여러가지-방법-다-해봐">일단 여러가지 방법 다 해봐</h2>
<p>어떤 방법이 나에게 잘맞는 학습 방법일까?
<code>모르겠습니다</code> 그래서 1주일동안 어느 날은 구현에 치중하고, 어느 날은 학습에 더 치중하고 몇 가지 방법을 시도해 봤습니다.</p>
<p>그래서 찾았냐구요? 못찾았습니다 ㅋㅋ
배경지식이 어느정도 있는 미션에는 구현에 집중하고, 배경지식이 거의 없다면 학습에 집중을 했습니다.
그러다 보니 학습 끝냈는데 밤 11시네?... 구현 언제하냐?  이런 상황이 발생했습니다.</p>
<p>학습하는데 왜 이렇게 시간이 오래걸렸냐구요?
야크털깎기를 많이했습니다...</p>
<ul>
<li>제대로 설계하고 구현하기 위해서는 개념에 대해 확실히 알고 동작원리까지 구체적으로 알아야 해</li>
<li>어? 이 내용도 알면 설계하는데 도움이 되겠는걸? -&gt; 학습해야지</li>
<li>여긴 어디지?</li>
</ul>
<p>좋은 설계와 코드는 이에대한 내용을 명확하고 구체적으로 알고 있어야 한다고 생각합니다.
이에 대한 생각은 유효하지만 정해진 시간이 있었고 그 시간 안에 <code>어떻게 해결할 것인가?</code>에는 조금 안맞지 않았나 싶습니다...</p>
<p>결국 새벽까지 했지만 구현에 대해 못끝낸 미션도 있었고 늦게 구현에 집중하다보니 쫒기는 느낌이 들고 늦게 잔 만큼 다음날의 컨디션에도 영향을 주었습니다. </p>
<h2 id="그래서-다음주에는">그래서 다음주에는?</h2>
<p>1주차를 진행하며 가장 아쉬웠던 점을 뽑아보자면 </p>
<ul>
<li>내 생각에 대한 설명과 설계, 구현 과정의 설명이 많이 부족했다.</li>
<li>학습과 구현에 있어 균형을 잘 못잡았다.</li>
</ul>
<p>두 가지가 가장 아쉬웠습니다.
그래서 방법을 바꿔보려고 합니다.</p>
<p><code>꼭 완벽해야 할까?</code> 라는 생각이 들었습니다. 애초에 자기객관화를 해보자면 그냥 객기가 아니였을까? 너 그정도 아닌데 라는 생각이 듭니다.</p>
<p>그래서 2주차에는 이러한 점을 적용해보고자 합니다!</p>
<ul>
<li>구현에 있어 필요한 최소한의 내용을 학습한다.</li>
<li>설계에 있어 나의 의견과 근거에 대한 설명에 대한 내용을 최대한 기술한다. </li>
<li>구현을 하다 모르는 내용이 생긴다면 그 부분에 대해 학습한다.</li>
<li>구현 과정에 있어 왜 이렇게 했고 어떤 문제점이 있었는지 학습하고 정리한다.</li>
</ul>
<p>가장 아쉬웠던 부분으로 나의 생각과 설계, 구현 과정에 있어 상세히 기록하지 못한 점입니다.
그래서 이러한 부분을 보완하고 나에게 맞는 학습 방법을 다시 찾아볼까 합니다.</p>
<h2 id="나에게-이런-면이">나에게 이런 면이?</h2>
<p>그동안 열심히 살아왔나? 라는 질문을 받는다면 솔직하게 말하면 <code>아니요</code>라고 말할 것 같습니다.
그럼 잘 놀며 재밌게라도 살았나? 라고 한다면 그것도 아닌 것 같습니다.</p>
<p>둘 사이의 줄을타며 조금 애매모호하게 살지 않았나 라는 생각이 듭니다.
공부에 있어 크게 욕심이 없고 이렇게 보면 휴식과 자기개발의 균형도 잘 못잡지 않았나 라는 생각이 듭니다.</p>
<p>비록 1주차였지만 엉덩이가 가벼운 제가 하루종일 앉아서 학습을 하고 있었습니다.
물론 미션을 수행해야해!! 라는 생각이 초반에 어느정도 있었겠지만 확실한건 <code>힘들지만 재밌다</code> 라는 생각이 들었던것 확실합니다.</p>
<p>내가 이렇게까지 깊게 공부해본적이 있었나? 힘들긴한데 꽤 재밌는데? 라는 생각을 하게되었습니다.
나에대해 다시 생각도 하고 자신감을 찾을 수 있었습니다.</p>
<h2 id="피어-피드백-시간">피어 피드백 시간</h2>
<p>정말 너무 좋은시간인데... 1시간이 이렇게 빠르다고?
혼자 공부해왔던 저에게 피어 피드백 시간은 매우 소중하고 즐거운 시간이였습니다.
서로 미션을 수행하며 어떤 점이 힘들었고 어떤 생각을 가지고 구현했으며 다양한 의견을 나눌 수 있었습니다.</p>
<p>피어 피드백을하며 다양한 의견을 나누며 여러가지 관점을 생각해보고 시야를 넓힐 수 있는 시간이라고 생각합니다.
가장 좋았던 점은 동료분들과 학습 방법을 공유하고 내 학습 방법과 다른 점을 찾을 수 있는 점이였습니다.
내 학습 방법의 부족한 부분을 찾을 수 있고 참고하여 적용할 수 있기 때문에 매우 의미있는 활동이라 생각합니다.</p>
<p>비록 1주차가 끝나 기존의 팀원분들과 헤어지게되어 매우 아쉽지만 모두 화이팅입니다!!</p>
<h2 id="마무리">마무리</h2>
<p>정신없이 미션을 하다보니 1주차가 지나있었다. 
말 그대로 진짜 정신없었다. </p>
<p>정신없었지만 1주라는 시간안에서 나에대해서도 새롭게 알게되고 힘들었지만 재밌다는 것(성장하는 나를 지켜보는<img src="https://velog.velcdn.com/images/dkdk_0422/post/3689d0f3-14ee-43f8-a511-cde896225ea3/image.jpg" alt="">
 나)은 사실입니다.
다음 주차에서는 아쉬웠던 부분을 보완하고 나에게 맞는 학습 방법의 실마리를 찾을 수 있으면 좋겠습니다 ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트 캠프10기  베이직 회고(Android)]]></title>
            <link>https://velog.io/@dkdk_0422/%EB%B6%80%EC%8A%A4%ED%8A%B8-%EC%BA%A0%ED%94%8410%EA%B8%B0-%EB%B2%A0%EC%9D%B4%EC%A7%81-%ED%9A%8C%EA%B3%A0Android</link>
            <guid>https://velog.io/@dkdk_0422/%EB%B6%80%EC%8A%A4%ED%8A%B8-%EC%BA%A0%ED%94%8410%EA%B8%B0-%EB%B2%A0%EC%9D%B4%EC%A7%81-%ED%9A%8C%EA%B3%A0Android</guid>
            <pubDate>Sun, 13 Jul 2025 07:09:59 GMT</pubDate>
            <description><![CDATA[<p>취업 준비 중 <a href="https://boostcamp.connect.or.kr/program_wm.html">네이버 부스트 캠프 베이직</a> 과정에 참여하게 되었습니다.</p>
<h2 id="베이직-과정">베이직 과정</h2>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/3e00e981-0018-41f7-8790-63753357e1f3/image.png" alt=""> 9기와 달리 모두에게 베이직의 기회가 열려있으며 2주동안 매일 열리는 과제를 수행합니다.</p>
<p>미션으로는 주로 요구사항에 대한 구조 설계, 코드 작성, 리서치 보고서 등 요구사항에 대해 본인의 생각을 정리하고 이를 해결해나가는 과정으로 진행되었습니다.</p>
<p>미션이라고 해서 완벽한 정답이 정해져있는 것이 아니라 각자 요구사항을 읽고 본인만의 생각을 정리하여 설계하여 구현하는 것이 중요하다고 생각합니다.</p>
<p>미션 수행 후 동료분들의 사고 과정, 트러블 슈팅, 코드 등을 확인할 수 있고 서로 피어 피드백을 통해 서로의 의견을 나눌 수 있습니다(개인적으로 가장 좋았더 점).</p>
<h2 id="첫인상">첫인상</h2>
<p>사실 큰 생각 없이 베이직 과정에 참여하게 되었는데 1일차 피어 피드백 시간에서 적절한 충격(?)을 받았습니다.
첫 미션으로 요구사항에 대해 본인의 생각을 정리하고 해결하는 문제가 나왔습니다. 어떻게 생각하느냐에 따라 정답이 다르게 나올 수 있는 문제였고 나름 문제에 대해 잘 파악하고 해결했다는 느낌을 받았습니다.</p>
<p>[피어 피드백 시간]
문제를 해결하기 위해 기존의 구조를 싹 다 갈아엎고 새로운 코드를 작성하였습니다. 다른 동료분들의 해결과정을 보다 &quot;기존 구조를 변경하지 않는 선에서 요구사항을 적용하였다&quot; 문구를 보게되었습니다. 
또 다른 여러 사고과정을 보며 &quot;저런 관점으로도 문제를 바라볼 수 있구나...&quot; 느끼게 되었습니다.</p>
<p>같은 문제에 대해서 여러가지 사고방식과 관점을 공유하며 이를 통해 나를 많이 돌아보게 되는 계기가 되었습니다.</p>
<h2 id="가장-좋았던-점">가장 좋았던 점</h2>
<h3 id="생각하고-설계하는-습관의-기반을-다짐">생각하고 설계하는 습관의 기반을 다짐</h3>
<p>저는 보통 코딩테스트 문제를 풀 때나 코딩을 할 때 주로 뇌코딩으로 해결하는 습관이 있습니다.
문제에 대해 깊게 생각하지 않고 오로지 정답을 향해 나아가는 자세로 코딩을 해왔습니다. 
하지만 이러한 자세로는 미션을 해결해나가는데 있어 어려움이 있었고 요구사항에 대해 깊이 생각하고 구조를 설계하는 문제가 나왔을 때 뇌정지(?)가 왔습니다.</p>
<p>결국 공책을 펴들고 내가 생각하는 구조를 그려가며 나의 생각을 체계적으로 그리는 습관을 들이는 계기가 되었습니다.
아직 미흡하지만 이러한 점은 문제에 대해 더 오래 생각하고 구체적으로 접근할 수 있게 했습니다. </p>
<h3 id="좋은-코드는-cs로부터-나온다">좋은 코드는 cs로부터 나온다</h3>
<p>자료구조 사용에 있어 내부적으로 어떻게 동작하고 어떻게 하면 효율적으로 사용할 수 있는지를 알고 있어야 한다라는 것을 느끼게 되었습니다. 기존에 아무생각 없이 사용하던 자료구조의 내부 구조와 원리를 뜯어볼 수 있는 기회가 있었고 이를 통해 어떤 상황에서 어떤 자료구조가 왜 사용되어야 하는지를 알고있어야 한다는 점이 매우 중요하다는 것을 느꼈습니다.</p>
<p>피어 피드백, 수료생의 관점 등 베이직 과정의 모든 것이 좋았지만 가장 기억에 남고 얻어갈 수 있었던 2가지를 적어보았습니다 ㅎㅎ</p>
<h2 id="베이직만-해도-이득">베이직만 해도 이득</h2>
<p>말 그대로 베이직 코스만 하셔도 본인에게 엄청난 도움이 된다고 생각합니다.
가볍운 마음가짐으로 시작했던 베이직 과정이 나에게 있어 코딩에 있어 임하는 자세를 바꿀 수 있는 매우 귀중한 시간이 되었습니다. 베이직 미션을 해결하고 동료들의 사고과정을 확인하는 과정에서 점점 욕심이 생기기 시작했습니다. &quot;베이직만 할게 아니라 &#39;챌린지&#39; , &#39;멤버쉽&#39;까지 무조건 하고싶다.. 라는 생각이 드실 겁니다. 좋은 동료분들과 이런 커뮤니티를 할 수 있는 좋은 기회이며 성장하는 나를 보며 점점 욕심이 생기기 시작했습니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/64a14874-b12b-4d5d-896d-85e70e0e5e23/image.png" alt="">운이 좋아 챌린지 과정에 입과할 수 있게 되었습니다.
욕심이 생긴만큼 챌린지 과정에 몰두하여 좋은 동료분들과 같이 성장해나가고자 합니다.</p>
<h2 id="마무리">마무리</h2>
<p>정리해보자면 취업 준비 중 베이직 과정에 참여하게 되었고 베이직 과정을 수행하며 정말 도움이 많이 되고 성장하는 나를 볼 수 있어 욕심이 나기 시작해서 &#39;챌린지&#39;에 몰두 하려고 합니다 ㅎㅎ
베이직 과정에서 성장할 수 있었던 점으로</p>
<ul>
<li>요구사항에 대해 구체적으로 분석하고 사고하는 습관을 키움</li>
<li>단순히 생각만하지않고 직접 적고 그려가며 나의 생각을 정리하는 습관 기반을 다짐</li>
<li>내가 부족한 부분(문제를 바라보는 관점)을 동료와의 공유로 키울 수 있음</li>
<li>지속 가능한 개발자의 의미</li>
</ul>
<p>정말 의미있는 시간이였고 기억에 남는 성장 포인트를 적어보았습니다.</p>
<p>다른 개발자 분들도 기회가 되신다면 베이직 과정만이라도 참여해보시는 것을 적극 권장합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compsoe - BottomSheetScaffold 커스텀하기]]></title>
            <link>https://velog.io/@dkdk_0422/Compsoe-BottomSheetScaffold-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/Compsoe-BottomSheetScaffold-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 10 Jul 2025 12:40:53 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 이번에는 Custom <code>BottomSheetScaffold</code> 에 대해 다뤄보려고 합니다.
Material에서 제공해주는 BottomSheetScaffold를 사용해 요구사항을 구현할 수 있었으면 좋겠지만...</p>
<p>다들 아시다시피 <code>BottomSheetScaffold</code> 는 두 가지 상태를 제공합니다.</p>
<pre><code class="language-kotlin">@ExperimentalMaterial3Api
enum class SheetValue {
    /** The sheet is not visible. */
    Hidden,

    /** The sheet is visible at full height. */
    Expanded,

    /** The sheet is partially visible. */
    PartiallyExpanded,
}</code></pre>
<p>Hideen 까지 3단계로 할 수 있겠군요.
하지만 디자인 팀에서 요구한 요구사항은 이렇습니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/d5a5707e-251a-4e8b-9074-f1217b7bb39f/image.gif" width="30%"></p>
<p>총 3단계로 이뤄져있는데요
<code>Collapsed</code>, <code>HalfExpanded</code>, <code>Expanded</code> 의 상태로 정리해보자면 다음과 같습니다.</p>
<ul>
<li>Hidden 상태는 없음</li>
<li>Collapsed 상태에서 위로 드래그 시 <code>HalfExpanded</code> 상태로 변환</li>
<li><code>HalfExpanded</code>, <code>Expanded</code> 상태에서는 LazyColumn의 스크롤이 가능하고 위의 DragHandle 영역으로 BottomSheet를 조절할 수 있다.</li>
</ul>
<p>어떻게 구현했는지 알아보기 전에 우선 Compose의 Layout 단계에 대해 간단하게 알아보고 가겠습니다.</p>
<h2 id="layout">Layout</h2>
<p>Compose에서는 3단계에 걸쳐 UI를 그립니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/4f54b6ba-e2ad-4945-9e64-fee7c62fb7f0/image.png" alt=""></p>
<p>Compose가 어떻게 UI를 그리는지 자세한 내용은 <a href="https://developer.android.com/develop/ui/compose/phases?hl=ko">Compose 공식문서</a>를 통해 알 수 있습니다.
이번 포스팅에서는 <a href="https://developer.android.com/develop/ui/compose/layouts/basics?hl=ko">Layout</a>에 대해 간단하게 설명하고 넘어가겠습니다.</p>
<p>Layout 단계에서는 UI 요소들의 크기를 측정(measure)하고 위치를 배치(place)하는 과정을 거치게 됩니다.</p>
<h3 id="layout-컴포넌트">Layout 컴포넌트</h3>
<blockquote>
<p>Layout 단계를 직접 제어할 수 있게 해주는 저수준 API</p>
</blockquote>
<p><code>Layout</code> 를 사용하여 Layout 단계를 직접 제어할 수 있습니다.</p>
<p>BottomSheet의 각 상태(Collapsed, HalfExpanded, Expanded)에 대한 정확한 y 좌표(앵커)를 계산하려면, 전체 화면의 높이와 TopBar의 높이 같은 다른 컴포저블의 측정된 크기를 알아야 합니다. Layout 컴포저블의 measure 람다는 바로 이 측정(measure) 시점에 개입하여 필요한 값들을 얻고, 이를 바탕으로 앵커를 동적으로 설정할 수 있게 해줍니다.</p>
<pre><code class="language-kotlin">@Suppress(&quot;ComposableLambdaParameterPosition&quot;, &quot;NOTHING_TO_INLINE&quot;)
@UiComposable
@Composable
inline fun Layout(
    contents: List&lt;@Composable @UiComposable () -&gt; Unit&gt;,
    modifier: Modifier = Modifier,
    measurePolicy: MultiContentMeasurePolicy
) {
    Layout(
        content = combineAsVirtualLayouts(contents),
        modifier = modifier,
        measurePolicy = remember(measurePolicy) { createMeasurePolicy(measurePolicy) }
    )
}</code></pre>
<ul>
<li>contents: 여러 개의 자식 컴포넌트들을 리스트 형태로 받습니다.</li>
<li>measurePolicy: 측정과 배치 로직을 정의하는 부분</li>
</ul>
<p>이 Layout 컴포넌트를 통해 직접 <code>Composable</code> 함수들의 크기를 측정하고 배치하여 요구사항에 맞는 화면을 만들 수 있습니다.</p>
<p>어떻게 크기를 측정하고 위치를 배치하는지 알아보겠습니다.</p>
<pre><code class="language-kotlin">@Composable
internal fun CustomBottomSheetScaffoldLayout(
    modifier: Modifier = Modifier,
    topBar: @Composable () -&gt; Unit,
    titleContent: @Composable () -&gt; Unit,
    mapContent: @Composable () -&gt; Unit,
    bottomSheet: @Composable () -&gt; Unit,
    snackbarHost: @Composable () -&gt; Unit,
    sheetOffset: () -&gt; Float,
    sheetState: CustomBottomSheetState,
) {
    Layout(
        modifier = modifier.background(Color.White),
        contents =
            listOf&lt;@Composable () -&gt; Unit&gt;(
                topBar,
                titleContent,
                mapContent,
                bottomSheet,
                snackbarHost
            )
    ) { (topBarMeasurable, titleMeasurable, mapMeasurable, sheetMeasurable, snackbarMeasurable),
        constraints -&gt;
        val layoutWidth = constraints.maxWidth
        val layoutHeight = constraints.maxHeight
        // 자식 컴포넌트들이 &quot;곡 최소 크기를 가져야 한다&quot; 라는 제약에서 해방
        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        // Measurable -&gt; Placeable 변환
        // Placeable 객체: 측정이 완료된 객체, 실제 크기 정보 보유, 배치 메서드 제공
        val topBarPlaceable = topBarMeasurable.fastMap { it.measure(looseConstraints) }
        // 측정된 topBarHeight를 구할 수 있음
        val topBarHeight = topBarPlaceable.fastMaxOfOrNull { it.height } ?: 0

        .
        .
        .

        // 최종 배치, 위에서 계산한 정보들을 x,y 좌표에 배치합니다.
        layout(layoutWidth, layoutHeight) {
            val sheetWidth = sheetPlaceable.fastMaxOfOrNull { it.width } ?: 0
            val sheetOffsetX = Integer.max(0, (layoutWidth - sheetWidth) / 2)

            mapPlaceable.fastForEach {
                it.placeRelative(0, mapY)
            }

            titlePlaceable.fastForEach {
                it.placeRelativeWithLayer(
                    x = 0,
                    y = titleY
                ) {
                    alpha = 1f - titleProgress
                }
            }

            sheetPlaceable.fastForEach {
                it.placeRelative(sheetOffsetX, currentOffset.roundToInt())
            }

            topBarPlaceable.fastForEach { it.placeRelative(0, 0) }
            .
            .
            .
        }
    }
}</code></pre>
<ul>
<li>contents에 전달한 <code>Composable</code>의 <code>List&lt;Measurable&gt;</code> 객체들을 받습니다.</li>
<li>Layout이 가질 수 있는 최소 / 최대 크기를 의미하는 <code>Constraints</code> 객체를 받습니다.</li>
<li>받은 객체들을 <code>measure</code> 함수를 통해 크기를 측정합니다.</li>
<li>측정된 객체들을 <code>placeRelative</code> 함수를 통해 x,y 좌표에 배치할 수 있습니다.</li>
</ul>
<p>이렇게 Layout 컴포넌트를 이용하여 우리가 원하는대로 크기를 지정하고 위치를 배치시킬 수 있습니다.</p>
<hr>
<h2 id="anchoreddraggablestate">AnchoredDraggableState</h2>
<blockquote>
<p>Compose에서 특정 지점들(앵커)에 스냅되는 드래그 동작을 관리하는 상태 클래스입니다.</p>
</blockquote>
<p>위에서 <code>Layout</code> 컴포넌트를 통해 원하는 모양의 컴포넌트들을 만들 수 있게 되었습니다.
이제 Layout으로 만든 컴포넌트에 BottomSheet 부분을 스크롤 시 우리가 정의한 상태에 드래그하는 동작이 필요합니다.</p>
<p>이를 구현하기 위해 <code>AnchoredDraggableState</code>를 이용할 수 있습니다.</p>
<pre><code class="language-kotlin">/**
 * State of the [anchoredDraggable] modifier. Use the constructor overload with anchors if the
 * anchors are defined in composition, or update the anchors using [updateAnchors].
 *
 * This contains necessary information about any ongoing drag or animation and provides methods to
 * change the state either immediately or by starting an animation.
 *
 * @param initialValue The initial value of the state.
 */
@Stable
class AnchoredDraggableState&lt;T&gt;(initialValue: T)</code></pre>
<ul>
<li>initialValue: 컴포저블이 처음 화면에 표시될 때의 위치(앵커)를 지정합니다.</li>
</ul>
<p>우선 각 위치를 나타내는 enum class를 작성합니다.</p>
<pre><code class="language-kotlin">enum class CustomSheetValue {
    Collapsed,
    HalfExpanded,
    FullyExpanded,
}</code></pre>
<p><code>DraggableAnchors</code> 를 통해 각 앵커의 위치를 지정할 수 있습니다. </p>
<pre><code class="language-kotlin">{ (topBarMeasurable, titleMeasurable, mapMeasurable, sheetMeasurable, snackbarMeasurable),
        constraints -&gt;
        val layoutWidth = constraints.maxWidth
        val layoutHeight = constraints.maxHeight
        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        val topBarPlaceable = topBarMeasurable.fastMap { it.measure(looseConstraints) }
        val topBarHeight = topBarPlaceable.fastMaxOfOrNull { it.height } ?: 0

        val collapsedSheetHeight = 200.dp.toPx()
        val halfExpandedSheetHeight = layoutHeight / 2f

        val newAnchors = DraggableAnchors {
            CustomSheetValue.Collapsed at layoutHeight - collapsedSheetHeight
            CustomSheetValue.HalfExpanded at layoutHeight - halfExpandedSheetHeight
            CustomSheetValue.FullyExpanded at topBarHeight.toFloat()
        }
        sheetState.updateAnchors(newAnchors)
        .
        .
        .</code></pre>
<p>AnchoredDraggableState의 상태는 CustomBottomSheetState에서 관리하도록 하였습니다.</p>
<pre><code class="language-kotlin">class CustomBottomSheetState(
    initialValue: CustomSheetValue = CustomSheetValue.Collapsed,
) {
    val currentValue: CustomSheetValue
        get() = anchoredDraggableState.currentValue

    fun requireOffset(): Float = anchoredDraggableState.requireOffset()

    internal suspend fun animateTo(
        targetValue: CustomSheetValue,
    ) {
        anchoredDraggableState.animateTo(targetValue)
    }

    fun updateAnchors(anchors: DraggableAnchors&lt;CustomSheetValue&gt;) {
        anchoredDraggableState.updateAnchors(anchors)
    }

    var anchoredDraggableState =
        AnchoredDraggableState(
            initialValue = initialValue,
        )

    companion object {
        fun Saver(): Saver&lt;CustomBottomSheetState, CustomSheetValue&gt; =
            Saver(
                save = { it.currentValue },
                restore = { savedValue -&gt;
                    CustomBottomSheetState(
                        initialValue = savedValue,
                    )
                }
            )
    }
}</code></pre>
<p>각 앵커에 대한 위치를 지정했고 스크롤 했을 시 앵커의 위치까지 시트를 올리지 않아도 바텀 시트가 올라갈 수 있어야 합니다. 이는 Modifier의 <code>anchoredDraggable</code> 를 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">fun &lt;T&gt; Modifier.anchoredDraggable(
    state: AnchoredDraggableState&lt;T&gt;,
    orientation: Orientation,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    overscrollEffect: OverscrollEffect? = null,
    flingBehavior: FlingBehavior? = null
): Modifier =
    this then
        AnchoredDraggableElement(
            state = state,
            orientation = orientation,
            enabled = enabled,
            reverseDirection = null,
            interactionSource = interactionSource,
            overscrollEffect = overscrollEffect,
            flingBehavior = flingBehavior
        )</code></pre>
<p>내부 파라미터 또한 AnchoredDraggableState를 받고 있습니다.</p>
<pre><code class="language-kotlin">.anchoredDraggable(
             state = state.anchoredDraggableState,
             orientation = orientation,
             enabled = true,
             flingBehavior = AnchoredDraggableDefaults.flingBehavior(state.anchoredDraggableState)
)</code></pre>
<p>Layout의 measure 블록 안에서 constraints.maxHeight를 통해 전체 높이를 알 수 있으므로, 바로 이 시점에서 AnchoredDraggableState가 사용할 앵커 위치를 계산하고 updateAnchors를 통해 갱신해주줄 수 있습니다.</p>
<p>만약 바텀시트의 중복 스크롤을 허용하고 싶다면
<code>NestedScrollConnection</code> 을 사용하여 좀더 커스텀하게 바텀시트의 스크롤을 다룰 수 있습니다.
NestedScrollConnection 이용 시 스크롤 전 후에 동작해야 할 함수들을 작성할 수 있습니다. 이에대해서는 추후 자세히 다뤄보도록 하겠습니다.</p>
<hr>
<h2 id="정리">정리</h2>
<h3 id="목표-3단-bottomsheet와-anchoreddraggablestate">목표: 3단 BottomSheet와 AnchoredDraggableState&quot;</h3>
<p>우리의 목표는 Collapsed, HalfExpanded, Expanded 세 지점(앵커)에 자석처럼 달라붙는 BottomSheet를 만드는 것입니다. Compose에서는 이런 동작을 AnchoredDraggableState를 통해 손쉽게 구현할 수 있습니다.</p>
<pre><code class="language-kotlin">// 1. 상태 정의
enum class CustomSheetValue { Collapsed, HalfExpanded, FullyExpanded }</code></pre>
<pre><code class="language-kotlin">// 2. State 생성
val state = remember { AnchoredDraggableState(initialValue = CustomSheetValue.Collapsed) }</code></pre>
<br>

<h3 id="문제점-앵커-위치는-어떻게-계산할까">문제점: 앵커 위치는 어떻게 계산할까?&quot;</h3>
<p>AnchoredDraggableState를 사용하려면 각 상태에 해당하는 정확한 Y축 좌표(오프셋)를 알려주어야 합니다.</p>
<p>Collapsed 위치 = 전체 화면 높이 - 200.dp</p>
<p>HalfExpanded 위치 = 전체 화면 높이 / 2</p>
<p>FullyExpanded 위치 = 상단 TopBar의 높이</p>
<p>여기서 문제가 발생합니다. 전체 화면 높이나 TopBar의 높이는 렌더링 과정에서 동적으로 결정되는 값입니다. 일반적인 컴포저블 함수 내에서는 이 값들을 미리 알 수 없습니다.</p>
<h3 id="3-해결책-layout-컴포저블로-측정-시점에-개입하기">3. 해결책: Layout 컴포저블로 측정 시점에 개입하기</h3>
<p>바로 이 문제를 해결하기 위해 저수준 API인 Layout 컴포저블을 사용합니다. Layout을 사용하면 Compose의 3단계(Composition -&gt; Layout(Measure, Place) -&gt; Drawing) 중 Layout 단계에 직접 관여할 수 있습니다.</p>
<p>Layout 컴포저블의 measurePolicy 람다 내부에서는 다음 두 가지가 가능해집니다.</p>
<p>측정(Measure): 자식 컴포저블들의 크기를 측정하고, constraints를 통해 부모 레이아웃의 최대 높이(layoutHeight) 같은 정보를 얻을 수 있습니다.</p>
<p>배치(Place): 측정된 정보를 바탕으로 자식들을 원하는 x, y 좌표에 배치합니다.</p>
<h3 id="4-합치기">4. 합치기</h3>
<p>이제 우리의 CustomBottomSheetScaffoldLayout이 어떻게 동작하는지 명확해집니다.</p>
<pre><code class="language-kotlin">Layout(...) { measurables, constraints -&gt;
    // (1) 측정 단계 (Measure)
    // layoutHeight와 topBarHeight 등 필요한 모든 크기를 측정합니다.
    val layoutHeight = constraints.maxHeight
    val topBarPlaceable = ...
    val topBarHeight = topBarPlaceable.height

    // (2) 앵커 계산 및 업데이트
    // 위에서 측정한 값들을 이용해 `AnchoredDraggableState`가 필요로 하는 앵커 위치를 계산합니다.
    val newAnchors = DraggableAnchors {
        CustomSheetValue.Collapsed at layoutHeight - collapsedSheetHeight
        CustomSheetValue.HalfExpanded at layoutHeight / 2f
        CustomSheetValue.FullyExpanded at topBarHeight.toFloat()
    }
    // 계산된 앵커를 State에 즉시 업데이트합니다!
    sheetState.updateAnchors(newAnchors)

    // (3) 배치 단계 (Place)
    // 측정된 컴포저블들을 최종 위치에 배치합니다.
    // BottomSheet의 y 좌표는 anchoredDraggableState가 관리하는 오프셋 값(`sheetState.requireOffset()`)을 사용합니다.
    layout(layoutWidth, layoutHeight) {
        mapPlaceable.placeRelative(...)
        sheetPlaceable.placeRelative(x = ..., y = sheetState.requireOffset().roundToInt())
        topBarPlaceable.placeRelative(...)
    }
}</code></pre>
<p>자세한 코드는
<a href="https://github.com/dongykung/Compose_Component/tree/main/app/src/main/java/com/dkproject/compsoe_component/custombottomsheetscaffold">https://github.com/dongykung/Compose_Component/tree/main/app/src/main/java/com/dkproject/compsoe_component/custombottomsheetscaffold</a>
을 통해 확인하실 수 있습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[kotlin - Map 이해하기]]></title>
            <link>https://velog.io/@dkdk_0422/kotlin0-Map-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/kotlin0-Map-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 03 Jul 2025 06:25:40 GMT</pubDate>
            <description><![CDATA[<h2 id="map">Map</h2>
<p><code>Map</code>은 키(Key)와 값(Value)의 쌍으로 데이터를 저장하는 자료구조입니다.
모든 데이터는 <strong>고유한 키</strong>와 그에 해당하는 <strong>값</strong>으로 이루어집니다.</p>
<p>여기서 고유한 Key란 Key는 중복될 수 없지만 Value는 중복될 수 있습니다.</p>
<p>예를 들어 보물상자가 있다고 가정해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/5bc2ae8c-acd9-45ab-8a02-2f06170a21db/image.png" alt=""></p>
<p>보물 상자의 Key는 고유합니다. 보물상자의 열쇠는 고유하지만 보물상자의 내용물은 황금덩이가 여러 개 들어있을 수 있고, 빈값이 존재할 수 도 있습니다.</p>
<p>이러한 특징을 기반으로 <code>Map</code> 의 특징을 알아보겠습니다.</p>
<ul>
<li><code>Map</code>은 내부적으로 키를 사용하여 데이터의 위치를 바로 계산하므로(해싱 등), 데이터 양이 많아져도 검색 속도가 매우 빠릅니다.</li>
<li>대부분의 <code>Map</code> 구현체(예: <code>HashMap</code>)는 데이터를 저장한 순서대로 유지하지 않습니다. 순서 유지가 필요하다면 <code>LinkedHashMap</code> 같은 특정 구현체를 사용해야 합니다(추후 설명).</li>
</ul>
<h2 id="map과-hashmap">Map과 HashMap</h2>
<p>위에서 <code>Map</code>의 개념에 대해 알아보았습니다.</p>
<h3 id="map-1">Map</h3>
<p><code>Map</code>은 키(Key)와 값(Value)을 하나의 쌍으로 묶어 저장하는 데이터 구조로 <code>interface</code> 입니다.</p>
<p>인터페이스로 주요 기능을 정의하는 설계도 역할을 합니다. 즉, “위와같은 데이터 구조를 다루기 위해서 
이런 기능들이 반드시 있어야 해!” 라고 정의할 수 있습니다.</p>
<pre><code class="language-kotlin">      주요 인터페이스 메서드
      public actual fun isEmpty(): Boolea

    public actual fun containsKey(key: K): Boolean

    public actual fun containsValue(value: @UnsafeVariance V): Boolean

    public actual operator fun get(key: K): V?</code></pre>
<p>Map 인터페이스를 구현하는 클래스로는 <code>HashMap</code> , <code>TreeMap</code> , <code>LinkedHashMap</code> 등이 있습니다.</p>
<p>인터페이스이므로 자체를 객체로 생성할 수 없습니다.</p>
<h3 id="hashmap">HashMap</h3>
<p><code>HashMap</code>은 <code>Map</code>이라는 설계도(인터페이스)에 따라 실제로 만들어진 <strong>구현체</strong> 중 하나입니다. <code>Map</code>이 정의한 기능들을 모두 가지고 있으며, 실제 데이터를 저장하고 관리하는 데 사용됩니다.</p>
<p>구현체이므로 <code>Map</code> 인터페이스를 구현하는 객체를 생성할 수 있습니다.</p>
<p><strong>특징</strong></p>
<ul>
<li><strong>해시(Hash)</strong> 기술을 사용하여 데이터를 관리하므로, 많은 양의 데이터를 추가, 삭제, 조회할 때 <strong>매우 빠른 속도</strong>를 보입니다.</li>
<li>데이터의 순서를 보장하지 않습니다. 즉, 저장한 순서와 다르게 데이터가 출력될 수 있습니다.</li>
</ul>
<h2 id="해시함수">해시함수</h2>
<p><code>HashMap</code>에 본격적으로 알아보기 전 <code>해시함수</code>에 대해 먼저 알아보겠습니다.</p>
<aside>

<p>해시함수는 <strong>임의의 크기를 가진 데이터를 고정된 크기의 값으로 매핑하는 함수</strong>입니다</p>
<p>h(k) = v
k는 키(임의 크기), v는 해시값(고정 크기)</p>
</aside>

<p>Map을 사용하는 이유로 <code>Map</code>은 내부적으로 키를 사용하여 데이터의 위치를 바로 계산하므로(해싱 등), 데이터 양이 많아져도 검색 속도가 매우 빠릅기 때문입니다.</p>
<p>어떻게 검색 속도가 빠를 수 있을가요?</p>
<p>컴퓨터는 데이터를 찾을 때, 보통 처음부터 하나씩 비교하며 찾습니다 (선형 검색). 데이터가 1억 개라면 최악의 경우 1억 번을 비교해야 합니다.</p>
<p>하지만 해시 함수를 사용하면 이 과정이 완전히 달라집니다.</p>
<ul>
<li><strong>저장할 때</strong>: &#39;어떤 데이터(Key)&#39;를 해시 함수에 넣어 &#39;고유한 숫자(해시 값)&#39;를 만듭니다. 그리고 그 숫자에 해당하는 위치에 데이터를 저장합니다.</li>
<li><strong>찾을 때</strong>: 찾고 싶은 &#39;어떤 데이터(Key)&#39;를 다시 해시 함수에 넣어 &#39;고유한 숫자(해시 값)&#39;를 얻습니다. 그리고 그 숫자에 해당하는 위치로 <strong>바로 찾아갑니다.</strong></li>
</ul>
<p>이 과정은 마치 우리가 물건을 보관함에 맡기고 받은 &#39;보관증 번호&#39;와 같습니다. 수많은 보관함 중에서 내 물건을 찾기 위해 모든 문을 열어볼 필요 없이, 보관증 번호에 해당하는 문만 열면 바로 찾을 수 있는 것과 같은 원리입니다.</p>
<h3 id="hashmap의-원리">HashMap의 원리</h3>
<pre><code class="language-kotlin">transient Node&lt;K,V&gt;[] table;  // 메인 배열!

static class Node&lt;K,V&gt; implements Map.Entry&lt;K,V&gt; {
    final int hash;
    final K key;
    V value;
    Node&lt;K,V&gt; next;  // 연결리스트를 위한 포인터
}</code></pre>
<p><code>HashMap</code>은 <strong>배열 + 연결리스트 구조로 구현되어 있습니다.</strong></p>
<p>우선 <code>HashMap</code>의 putVal 함수가 어떻게 구현되어 있는지 직접 확인해보고 예시를 들어 설명해보겠습니다.</p>
<pre><code class="language-kotlin">final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node&lt;K,V&gt;[] tab; Node&lt;K,V&gt; p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) &amp; hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node&lt;K,V&gt; e; K k;
            if (p.hash == hash &amp;&amp;
                ((k = p.key) == key || (key != null &amp;&amp; key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode&lt;K,V&gt;)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount &gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &amp;&amp;
                        ((k = e.key) == key || (key != null &amp;&amp; key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size &gt; threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }</code></pre>
<p>위에서 <code>HashMap</code>은 기본적으로 배열 + 연결리스트로 구현되어있음을 확인했습니다.</p>
<ol>
<li><strong>테이블 초기화 확인</strong></li>
</ol>
<pre><code class="language-kotlin">if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;</code></pre>
<p>HashMap이 처음 사용되거나 비어있는지 확인입니다.</p>
<p>처음 사용되거나 비어있다면 <code>tab = resize()</code> 가 호출되고 첫 번째 put 호출 시 기본 크기 16으로 배열 생성됩니다. (<code>table = new Node[16]</code>)</p>
<ol>
<li><strong>배열 인덱스 계산 및 빈 슬롯 확인</strong></li>
</ol>
<pre><code class="language-kotlin">if ((p = tab[i = (n - 1) &amp; hash]) == null) tab[i] = 
                                                                newNode(hash, key, value, null);</code></pre>
<ul>
<li><strong>(n - 1) &amp; hash</strong>: 해시값을 배열 크기로 나눈 나머지 계산 (비트 연산)</li>
<li><strong>tab[i] =</strong> 계산된 인덱스의 배열 슬롯 확인</li>
<li><strong>null인 경우</strong>: 새 노드를 직접 생성하여 저장</li>
</ul>
<table>
<thead>
<tr>
<th><strong>단계</strong></th>
<th><strong>값</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>hash</td>
<td>12345</td>
<td>key.hashCode() 결과</td>
</tr>
<tr>
<td>n</td>
<td>16</td>
<td>배열 크기</td>
</tr>
<tr>
<td>n - 1</td>
<td>15</td>
<td>비트마스크 (1111)</td>
</tr>
<tr>
<td>hash &amp; 15</td>
<td>9</td>
<td>최종 인덱스</td>
</tr>
</tbody></table>
<ol>
<li><strong>충돌 처리 - 첫 번째 노드 확인</strong></li>
</ol>
<pre><code class="language-kotlin">else {
            Node&lt;K,V&gt; e; K k;
            if (p.hash == hash &amp;&amp;
                ((k = p.key) == key || (key != null &amp;&amp; key.equals(k))))
                e = p;
                ...</code></pre>
<ul>
<li><strong>p.hash == hash</strong>: 해시값이 같은지 확인</li>
<li><strong>key.equals(k)</strong>: 실제 키가 같은지 확인</li>
<li><strong>둘 다 같으면</strong>: 기존 키를 찾음! → 값만 업데이트</li>
<li><strong>예시: 같은 키 재삽입</strong></li>
</ul>
<ol>
<li><strong>트리 노드 처리</strong></li>
</ol>
<pre><code class="language-kotlin">else if (p instanceof TreeNode)
                e = ((TreeNode&lt;K,V&gt;)p).putTreeVal(this, tab, hash, key, value);</code></pre>
<ul>
<li>충돌이 많아서 연결리스트가 트리로 변환된 상태</li>
<li><strong>putTreeVal()</strong>: 트리에 삽입/검색</li>
<li>성능: O(log n) 보장</li>
</ul>
<p>트리 보장 조건</p>
<aside>

<p>static final int TREEIFY_THRESHOLD = 8; // 8개 이상시 트리화 static final int UNTREEIFY_THRESHOLD = 6; // 6개 이하시 리스트화</p>
</aside>

<ol>
<li><strong>연결리스트 순회</strong></li>
</ol>
<pre><code class="language-kotlin">else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount &gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &amp;&amp;
                        ((k = e.key) == key || (key != null &amp;&amp; key.equals(k))))
                        break;
                    p = e;
                }
            }</code></pre>
<ul>
<li>연결리스트를 따라가며 탐색</li>
<li><strong>p.next == null</strong>: 리스트 끝에 도달</li>
<li>새 노드를 리스트 마지막에 추가</li>
<li><strong>binCount &gt;= 7</strong>: 8개 이상이면 트리로 변환</li>
<li>중간에 같은 키 발견시 즉시 중단</li>
</ul>
<ol>
<li><strong>기존 키 발견시 값 업데이트</strong></li>
</ol>
<pre><code class="language-kotlin">if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }</code></pre>
<ul>
<li>기존 키가 발견된 경우 처리</li>
</ul>
<h3 id="예시-상황">예시 상황</h3>
<p><code>HashMap</code> 이라는 아파트가 있다고 가정해보겠습니다.</p>
<p>배열(<code>Node[] table</code>) = 아파트의 각 층이라고 생각할 수 있습니다.</p>
<ul>
<li><code>HashMap</code>의 뼈대인 <code>Node</code> 배열은 아파트의 각 층에 해당합니다. 이 배열의 각 칸을 버킷(bucket)이라고 부릅니다.</li>
<li><code>HashMap</code>은 기본적으로 16층짜리 아파트로 시작합니다.</li>
</ul>
<p>즉, 처음 16층의 아파트가 있다고 생각하시면 됩니다.</p>
<p>아파트에는 당연히 엘리베이터가 있겠죠?</p>
<p><strong><code>hash(key)</code> = 엘리베이터</strong></p>
<ul>
<li>내가 가진 키(key)를 해시 함수에 넣으면, 내가 몇 층으로 가야 할지 알려줍니다. (예: &quot;너는 7층으로 가!&quot;)</li>
<li>이 영역이 바로 2번 설명의 역할을 하는 조건입니다.</li>
</ul>
<p><strong><code>Node</code> = 각 층의 집 (세대)</strong></p>
<ul>
<li><strong>충돌이 없을 때</strong>: 7층에 도착했는데 아무도 살고 있지 않다면, 701호에 입주하면 됩니다. 이 경우 버킷에는 노드가 딱 하나만 있습니다.</li>
<li><strong>충돌이 발생할 때 (연결 리스트)</strong>: 7층에 도착했는데 이미 701호에 다른 사람(Node)이 살고 있습니다. 그러면 나는 그 옆집인 <strong>702호</strong>에 입주하고, 701호 집에는 &#39;옆집에 702호가 있다&#39;는 표시(<code>next</code> 포인터)를 남깁니다. 또 다른 사람이 7층에 오면 703호에 입주하고, 702호는 703호를 가리킵니다.</li>
<li>이렇게 <strong>같은 층(버킷)에 여러 세대(노드)가 복도(연결 리스트)를 따라 쭉 늘어서는 구조</strong>가 됩니다.</li>
</ul>
<p>그렇다면 현재 7층에 2세대가 살고 있다고 가정해보겠습니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/07ed3190-774e-458c-a827-72aea2605ec2/image.png" alt=""></p>
<p>그렇다면 Samsung을 키로 가지는 Android 값은 어떻게 찾을 수 있을가요?</p>
<pre><code class="language-kotlin"> if (first.hash == hash &amp;&amp; // always check first node
                ((k = first.key) == key || (key != null &amp;&amp; key.equals(k))))</code></pre>
<p>getNode 함수에서 해쉬 함수를 비교하고 key에 대한 <code>equals</code> 함수를 실행하여 비교할 수 있습니다.</p>
<h2 id="좋은-해시함수와-나쁜-해시함수">좋은 해시함수와 나쁜 해시함수</h2>
<p>위에서 <code>HashMap</code>이 어떠한 동작원리로 이루어지는지 확인했습니다.</p>
<p>기본적으로 16층 아파트가 만들어지고 각 층마다 8세대까지 살 수 있으며 8세대 이후에는 연결리스트에서 → 트리 구조로 변경되게 됩니다. </p>
<p>그 이유로는 <code>Map</code> 을 사용하는 이유에서 찾을 수 있습니다.</p>
<p>한 층에 8세대가 입주하는 순간 &quot;이 층은 너무 복잡해졌군. 더 효율적인 구조로 리모델링해야겠다!&quot;라고 판단합니다. </p>
<ul>
<li><strong>연결 리스트</strong>에서 특정 집을 찾으려면 최악의 경우 8번을 다 확인해야 합니다 (O(n))</li>
<li><strong>트리 구조</strong>에서는 훨씬 효율적으로 탐색할 수 있어, 8개의 데이터가 있어도 약 3번의 비교만으로 원하는 값을 찾을 수 있습니다 (O(log n)).</li>
</ul>
<p>여기서 <code>해시 충돌</code> 이라는 단어가 등장합니다.</p>
<aside>

<p><strong>해시 충돌</strong>이란 <a href="https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8B%9C_%ED%95%A8%EC%88%98">해시 함수</a>가 서로 다른 두 개의 입력값에 대해 동일한 출력값을 내는 상황을 의미합니다.</p>
</aside>

<p>즉 아파트 예제에서 map에 새로운 값을 추가하였다고 가정해보겠습니다.</p>
<p>hash 함수를 실행한 결과 7층으로 가라는 통보를 받았습니다.</p>
<p>하지만 7층에는 이미 3세대나 살고 있었습니다. 즉 key에 대해 동일한 hash값이 나오는게 6개나 있다는 것을 의미합니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/7a87695a-900d-4190-8f0f-7d26c7560c73/image.png" alt=""></p>
<p>이를 해시 충돌이라고 하며 새로운 값은 704호에 입주하게 됩니다.</p>
<h3 id="그럼-해시-충돌은-나쁜가">그럼 해시 충돌은 나쁜가?</h3>
<ul>
<li><strong>해시 충돌은 어쩔 수 없습니다.</strong> 입력될 수 있는 키의 종류는 거의 무한하지만, <code>HashMap</code>의 버킷(공간)은 한정되어 있기 때문에 충돌은 필연적으로 발생합니다.</li>
<li><strong>충돌 자체가 나쁜 것은 아닙니다</strong>: <code>HashMap</code>은 충돌이 일어날 것을 이미 알고 있고, 이를 해결하기 위한 방법(연결 리스트, 트리 변환)을 미리 준비해두었습니다. 문제는 충돌이 <strong>얼마나 자주, 그리고 특정 위치에 집중되어 일어나느냐</strong>입니다.</li>
<li><strong>좋은 해시 함수 = 고른 분포</strong>: 좋은 해시 함수는 충돌이 일어나더라도 특정 버킷에 몰리지 않고, 전체 버킷에 <strong>골고루 흩어지게</strong> 만듭니다. 이로 인해 <code>HashMap</code>이 충돌을 효율적으로 관리하고 빠른 성능을 유지할 수 있습니다.</li>
</ul>
<p>즉 나쁜 해시 함수란 데이터가 특정 층에 몰리는 것을 의미하고, 좋은 해시 함수란 여러 층에 각 세대들이 고르게 분포되는 것을 의미합니다.</p>
<h2 id="해시맵의-버킷-사이즈-capacity">해시맵의 버킷 사이즈 (Capacity)</h2>
<p><code>HashMap</code>의 <strong>버킷 사이즈</strong>는 *아파트가 총 몇 층인가를 의미하며, 용량(Capacity)이라고 부릅니다.</p>
<ul>
<li><strong>초기 용량 (Initial Capacity)</strong>: <code>HashMap</code>을 처음 만들면 기본적으로 16층짜리 아파트 (버킷 16개)로 지어집니다. 물론 처음부터 더 큰 아파트를 지을 수도 있습니다.</li>
<li><strong>항상 2의 거듭제곱</strong>: <code>HashMap</code>의 층수(용량)는 항상 16, 32, 64, 128...처럼 <strong>2의 거듭제곱</strong> 형태를 유지합니다. 그 이유는 <code>(n-1) &amp; hash</code>라는 빠른 비트 연산으로 버킷 위치를 계산하기 위함입니다.</li>
<li><strong>리사이징 (Resizing)</strong>: 아파트가 일정 수준 이상으로 꽉 차면 (기본적으로 <strong>75% 이상</strong> - 이를 로드 팩터(Load Factor)라 함), <code>HashMap</code>은 더 큰 아파트를 짓습니다.<ul>
<li>예를 들어 16층 아파트에 12세대(<code>16 * 0.75</code>)가 입주하면, <code>HashMap</code>은 <strong>기존의 2배 크기인 32층짜리 아파트</strong>를 새로 짓고 모든 세대(데이터)를 그곳으로 이주시킵니다. 이 과정을 <strong>리사이징</strong>이라고 합니다.</li>
<li>리사이징은 비용이 큰 작업이지만, 이를 통해 버킷(층)의 수를 늘려서 앞으로 발생할 해시 충돌을 줄이고 다시 빠른 속도를 유지할 수 있게 해줍니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose annotations]]></title>
            <link>https://velog.io/@dkdk_0422/Compose-annotations</link>
            <guid>https://velog.io/@dkdk_0422/Compose-annotations</guid>
            <pubDate>Tue, 03 Jun 2025 12:25:56 GMT</pubDate>
            <description><![CDATA[<p>해당 내용은 <a href="https://leanpub.com/composeinternalskor">Compose Internals</a> 2장 Compose 어노테이션 내용을 공부하며 기록한 내용입니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/f7a6cdca-d1e8-482c-9e03-ddffdc87e94c/image.png" alt=""></p>
<hr>
<h2 id="21-compose-컴파일러">2.1 Compose 컴파일러</h2>
<h3 id="일반적으로-아는-컴파일-흐름">일반적으로 아는 컴파일 흐름</h3>
<pre><code class="language-kotlin">소스코드 -&gt; (프로트엔드 분석) -&gt; 중간 표현(IR) -&gt; 바이트코드 생성 -&gt; 실행</code></pre>
<p>Kotlin과 JVM 진영에서는 보통 <code>kapt</code> 를 통한 어노테이션 프로세서를 사용하는 것이 일반적입니다.</p>
<h3 id="compose는-kapt를-안쓴다">Compose는 kapt를 안쓴다</h3>
<p>Compose는 <code>kapt</code> 나 어노테이션 프로세서를 전혀 사용하지 않습니다.
대신 <code>Kotlin 컴파일러 플러그인</code> 방식으로 작동합니다.</p>
<blockquote>
<p>Compose Compiler = Kotlin 컴파일러 플러그인</p>
</blockquote>
<p>즉, Kotlin의 컴파일 과정 안쪽에 직접 들어가는 확장입니다.</p>
<ul>
<li>kapt는 컴파일 전에 별도로 실행돼야 해서 느립니다.</li>
<li>Compose Compiler는 Kotlin 컴파일의 “프로늩엔드” 단계에서 진단 제공</li>
<li>컴파일러 플러그인은 <strong>컴파일 과정에 직접 내장되어 있습니다</strong></li>
</ul>
<p>주의할 점으로 IDE 연동은 따로 처리됩니다.</p>
<ul>
<li>Compose Compiler가 컴파일러 플러그인으로 돌아가기 때문에, IntelliJ / Android Studio의 에디터에서는 별도 IDEA 플러그인이 필요합니다</li>
<li>에디터가 바로 실시간으로 Copmose 오류를 잡아주는 기능은 Compose Compiler와는 다른 경로로 구현되어 있습니다.</li>
</ul>
<h3 id="ir중간표현-단계에서-소스-코드를-마음대로-조작-가능">IR(중간표현) 단계에서 소스 코드를 마음대로 조작 가능</h3>
<blockquote>
<p>Kotlin은 소스 코드를 분석한 뒤 IR(Intermediate Representation) 이라는 중간 코드 구조로 바꿔서
컴파일 합니다.</p>
</blockquote>
<ul>
<li>IR 단계에서 소스 코드를 수정함으로써 Compose Compiler는 Compose Runtime이 요구하는 대로 <strong>Composable 함수를 마음대로 변형</strong>시킬 수 있습니다(Composer 삽입 등)</li>
<li>즉, 개발자가 작성한 Composable 함수는 컴파일러가 자동으로 구조를 변형해서 런타임에 필요한 코드로 재구성 합니다.</li>
</ul>
<h3 id="어노테이션-프로세서와-kotlin-컴파일러-플러그인-차이점">어노테이션 프로세서와 Kotlin 컴파일러 플러그인 차이점</h3>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>어노테이션 프로세서 (kapt/KSP)</strong></th>
<th><strong>Kotlin 컴파일러 플러그인</strong></th>
</tr>
</thead>
<tbody><tr>
<td>개입 위치</td>
<td>소스 코드 수준</td>
<td><strong>IR(중간 표현)</strong> 수준</td>
</tr>
<tr>
<td>목적</td>
<td>코드 생성</td>
<td></td>
</tr>
<tr>
<td>원래 작성한 코드를 건드리지 않음</td>
<td>코드 변형 및 삽입</td>
<td></td>
</tr>
<tr>
<td>기존 코드의 구조를 통째로 바꾸거나 삽입/삭제 가능</td>
<td></td>
<td></td>
</tr>
<tr>
<td>위험도</td>
<td>낮음 (새 파일만 생성)</td>
<td><strong>높음 (기존 코드까지 수정)</strong></td>
</tr>
<tr>
<td>범위</td>
<td>파일 단위</td>
<td>전체 컴파일 단위 (전역 최적화 가능)</td>
</tr>
<tr>
<td>예</td>
<td>Room, Hilt, Moshi</td>
<td>Jetpack Compose, Serialization, Parcelize</td>
</tr>
</tbody></table>
<hr>
<h2 id="22-composable">2.2 @Composable</h2>
<blockquote>
<p>Composable 함수는 실행 시 트리에 내보내지는 노드로 데이터를 매핑하는 것</p>
</blockquote>
<p>Compose Compiler와 어노테이션 프로세서의 가장 큰 차이저은, Compose의 경우 실제로 어노테잉션이 붙어있는 선언이나 표현식을 <code>변형</code>한다는 것입니다. 대부분의 어노테이션 프로세서는 표현식을 변형하는 행위 등은 할 수 없으며, 추가적이거나 동등한 선언만을 제공할 수 있습니다.</p>
<p>그렇기 때문에 Compiler는 IR 변환을 사용합니다. @Composable 어노테이션은 실제로 어노테이션이 붙은 대상의 타입을 변경하며, 컴파일러 플러그인은 프론트엔드에서 Composable 타입이 일반적인 함수들과 동일한 취급을 받지 않도록 모든 종류의 규칙을 강제하는 데 활용합니다.</p>
<p>@Composable을 통해 선언이나 표현식의 타입을 변경하는 것은 대상에게 <code>메모리</code> 를 부여하는 것을 의미합니다.
즉, <code>remember</code> 를 호출하고 <code>Composer</code> 및 <code>슬롯 테이블</code> 을 활용할 수 있는 능력을 의미합니다.</p>
<p>또한, Composable의 본문 내에서 구동된 이펙트들(effects)이 준수할 수 있는 라이프사이클을 제공합니다.
Composable 함수들은 메모리에 보존 될 수 있도록 각각의 정체성(ID 값)을 할당받고, 완성된 트리에서
위치(<code>위치 기억법</code>)가 지정됩니다.</p>
<p>즉, Composable 함수들은 노드를 composition으로 방출하고 CompositionLocals를 처리할 수 있습니다.</p>
<hr>
<h2 id="23-composablecompilerapi">2.3 @ComposableCompilerApi</h2>
<blockquote>
<p>Compose에서 컴파일러에 의해서만 사용된다는 의도를 나타내기 위해 쓰입니다.</p>
</blockquote>
<p>잠재적 사용자들에게 해당 사실을 알리고, 주의해서 사용해야 함을 알리기 위한 목적을 가집니다</p>
<hr>
<h2 id="24-internalcomposeapi">2.4 @InternalComposeApi</h2>
<blockquote>
<p>API는 외부에 공개되어 있긴 하지만, <strong>Compose 내부적으로는 계속 바뀔 수 있음</strong>을 의미합니다</p>
</blockquote>
<ul>
<li><strong>공식적으론 “써도 되지만 책임은 니가 져”라는 뜻</strong>입니다.</li>
<li>안정성 보장이 없는, <strong>내부적인 성격을 가진 API라는 의도 표시입니다</strong>.</li>
</ul>
<p>Kotlin의 internal은 컴파일러가 막아주는 진짜 접근 제한이지만, Compose 같은 <strong>라이브러리 개발자</strong> 입장에서는 다음 문제가 있습니다</p>
<ul>
<li>어떤 API는 외부에 공개해야만 하지만, <strong>아직 변경될 가능성이 높거나</strong>, 내부 동작을 위해 쓰이는 것이면, 외부 개발자에게 “조심해서 써라, 나중에 깨질 수 있다”는 경고가 필요합니다.</li>
</ul>
<hr>
<h2 id="25-disallowcomposablecalls">2.5 @DisallowComposableCalls</h2>
<blockquote>
<p>함수 내에서 Composable 함수의 호출이 발생하는 것을 방지하기 위해 사용됩니다.</p>
</blockquote>
<p>이 어노테이션은 Composable 함수를 안전하게 호출할 수 없는 Composable 함수의 인라인 람다 매개변수에서 유용하게 사용될 수 있습니다. 주로 recomposition 마다 호출되면 안 되는 람다식에 가장 적합하게 사용됩니다.</p>
<p>예시로 Compose Runtime의 일부인 <code>remember</code> 함수에서 찾아볼 수 있습니다.</p>
<pre><code class="language-kotlin">@Composable
inline fun &lt;T&gt; remember(crossinline calculation: @DisallowComposableCalls () -&gt; T): T =
    currentComposer.cache(false, calculation)</code></pre>
<p>remember는 오직 첫 composition 단계에서만 수행되며, 이후의 모든 recomposition 단계에서는 항상 이미 계산된 값을 반환합니다.</p>
<p>만약 Composable 함수 호출이 허용된다면, Composable 함수의 노드 방출 시 <code>슬롯 테이블</code> 에서 공간을 차지하고 람다가 더 이상 호출되지 않으므로 첫 composition 단계 후에 삭제됩니다.</p>
<p>때문에 remember 람다 식 안에 Composable을 호출을 하면 안되며 이를 가능케 해주는 것이 <code>@DisallowComposableCalls</code> 어노테이션 입니다.</p>
<h3 id="조건부로-호출되는-inline-람다에서-가장-적합하게-사용된다">조건부로 호출되는 inline 람다에서 가장 적합하게 사용된다</h3>
<blockquote>
<p>조건부 호출 = 람다가 실제로 항상 실행되는 것이 아니라 상황에 따라 <strong>실행될 수도, 안 될 수도</strong> 있는 경우</p>
</blockquote>
<p>inline 이점</p>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>함수 호출 비용 없음 (스택 프레임 생성 제거)</td>
</tr>
<tr>
<td><strong>람다 성능 최적화</strong></td>
<td>람다 객체 생성 없이 코드 복사</td>
</tr>
<tr>
<td><strong>상위 컨텍스트 “상속”</strong></td>
<td>인라인된 코드가 <strong>바깥의 컨텍스트에서 실행</strong>되므로 @Composable 여부도 따라감</td>
</tr>
<tr>
<td>- inline은 함수의 <strong>본문이 호출한 위치에 복사</strong>되도록 컴파일합니다(함수 호출 비용 없음)</td>
<td></td>
</tr>
<tr>
<td>- 성능 최적화뿐만 아니라 <strong>람다에서 상위 컨텍스트(Composable)를 “상속”할 수 있게 해줍니다.</strong></td>
<td></td>
</tr>
</tbody></table>
<p>상속을 하는 근거로 1장에서 본 forEach문을 볼 수 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun MyList(items: List&lt;Item&gt;) {
    items.forEach {
        Text(it.name) 
    }
}</code></pre>
<p>forEach문의 람다는 @Composable로 마킹되어 있지 않지만 Composable 함수 내에서 호출되어 Composable 함수를 호출할 수 있게 됩니다. </p>
<p>하지만 remember와 같은 다른 일부 API의 경우에는 바람직하지 않습니다.</p>
<p>remember는 최초의 composition에만 호출됩니다. 그 내부 람다는 이후에 호출되지 않습니다.
그런데 그 안에서 Composable을 호출하면 </p>
<ul>
<li>Compose Runtime은 <strong>해당 Composable을 Slot Table에 등록</strong>하려고 시도합니다.</li>
<li>이후 recomposition에서 해당 노드는 <strong>실제로 존재하지 않는데도 존재하는 것으로 간주됨</strong></li>
<li>결과: 상태 구조 붕괴, Slot 테이블 충돌, 심각한 버그</li>
</ul>
<p>🛡️ 그래서 이걸 <strong>컴파일러 수준에서 막아야 합니다</strong> → @DisallowComposableCalls</p>
<h3 id="전파성"><strong>전파성?</strong></h3>
<p>만약 remember { someFunc() }인데 someFunc() 내부에서도 또 다른 인라인 람다를 받는다면, 그 <strong>안에서도 Composable 호출이 금지</strong>됩니다.</p>
<blockquote>
<p>DisallowComposableCalls는 호출 체인을 타고 전파</p>
</blockquote>
<hr>
<h2 id="26-readonlycomposable">2.6 @ReadOnlyComposable</h2>
<blockquote>
<p>Composable 함수가 <strong>상태를 변경하지 않고</strong>, <strong>단순히 값을 읽기만</strong> 한다는 걸 컴파일러에게 알려주는 
표시(어노테이션)입니다.</p>
</blockquote>
<p>Compose Runtim은 Composable 함수가 앞선 가정을 충족하는 경우, 필요하지 않은 코드 생성을 사전에 방지합니다(Composer 주입 X).</p>
<p>Jetpack Compose의 세계에서는 <strong>모든 UI와 관련된 상태 접근</strong>도 <strong>Composable 컨텍스트 안에서만 허용</strong>됩니다.</p>
<pre><code class="language-kotlin">@ReadOnlyComposable
@Composable
fun getDarkModeStatus(): Boolean {
    return isSystemInDarkTheme()
}</code></pre>
<ul>
<li>이 함수는 <strong>어떤 UI를 생성하지 않고</strong>, 오직 <strong>시스템 상태만 읽습니다.</strong></li>
<li><strong>화면에 아무것도 그리지 않음</strong>, <strong>값만 반환함</strong></li>
</ul>
<h3 id="일반-composable-함수"><strong>일반 Composable 함수</strong></h3>
<ul>
<li>내부에서 UI를 그리면 Compose는 <strong>그룹</strong> 이라는 구조로 <strong>SlotTable에 기록</strong>합니다.</li>
<li>이 그룹은 추후에 recomposition을 위해 사용됩니다 (재시작, 이동 가능성 등 포함)</li>
</ul>
<h3 id="readonlycomposable함수"><strong>@ReadOnlyComposable함수</strong></h3>
<ul>
<li><strong>그룹 생성을 하지 않음</strong></li>
<li>SlotTable에 기록도 거의 없음</li>
<li>단순히 <strong>상태 조회나 상수 반환 같은 함수일 경우</strong>, <strong>성능 최적화</strong> 가능</li>
</ul>
<h3 id="그룹이란-무엇인가"><strong>그룹이란 무엇인가?</strong></h3>
<blockquote>
<p>Composable 함수나 블록이 호출될 때, Compose는 해당 위치와 상태를 Slot Table에</p>
<p><strong>그룹 단위로 저장</strong></p>
</blockquote>
<p>이 그룹들은:</p>
<ul>
<li><strong>재시작 가능한 그룹(restartable)</strong></li>
<li><strong>이동 가능한 그룹(movable)</strong></li>
<li><strong>스킵 가능한 그룹(skippable)</strong></li>
</ul>
<p>같은 태그를 가질 수 있습니다.</p>
<pre><code class="language-kotlin">if (condition) {
    Text(&quot;Hello&quot;)
} else {
    Text(&quot;World&quot;)
}</code></pre>
<ul>
<li>여기서 Text(&quot;Hello&quot;)와 Text(&quot;World&quot;)는 서로 다른 <strong>그룹</strong>으로 간주됩니다.</li>
<li>이유는? → condition 값에 따라 <strong>위치와 의미가 달라지기 때문입니다.</strong></li>
<li>이동 가능한 그룹은 의미론적으로 고유한 키를 가지고 있기 때문에, 각자의 부모 그룹 내에서 재정렬될 수 있습니다.</li>
</ul>
<h3 id="그러면-왜readonlycomposable이-필요한가"><strong>그러면, 왜@ReadOnlyComposable이 필요한가?</strong></h3>
<ul>
<li>이런 그룹을 생성하지 않는 함수라면:<ul>
<li>Slot Table에 기록할 필요도 없음</li>
<li>UI 노드도 방출하지 않음</li>
<li>재컴포지션 대상도 아님</li>
</ul>
</li>
</ul>
<p>Composable 함수가 composition에 쓰이지 않으면, 데이터가 교체되거나 이동되지 않으므로 아무런 가치도 제공하지 않습니다. 따라서 <code>ReadOnlyComposable</code> 어노테이션은 이와 같은 상황을 방지하는 데 활용됩니다.</p>
<table>
<thead>
<tr>
<th><strong>함수</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>isSystemInDarkTheme()</td>
<td>다크 모드 여부만 반환</td>
</tr>
<tr>
<td>LocalContext.current</td>
<td>현재 context만 조회</td>
</tr>
<tr>
<td>LocalConfiguration.current</td>
<td>디바이스 설정 정보 반환</td>
</tr>
<tr>
<td>MaterialTheme.colors</td>
<td>컬러 팔레트 정보 읽기</td>
</tr>
</tbody></table>
<hr>
<h2 id="27-stablemarker">2.7 @StableMarker</h2>
<blockquote>
<p>Compose가 <strong>불필요한 recomposition을 건너뛸 수 있도록 도움</strong></p>
</blockquote>
<p>Compose는 recomposition시 상태가 변하지 않았으면 불필요한 UI 갱신을 건너 뜁니다.
이걸 하려면, Compose는 <strong>이 값이 변했는지 아닌지를 판단할 수 있어야</strong> 합니다.</p>
<h3 id="stablemarker">@StableMarker</h3>
<blockquote>
<p>다른 어노테이션에 붙이는 어노테이션
@Stable이나 @Immutable에 붙여서 이건 안정성 관련 어노테이션이다라고 표시해주는 
메타 어노테이션입니다.</p>
</blockquote>
<pre><code class="language-kotlin">@StableMarker
annotation class Stable

자체는 기능이 없고, “이 어노테이션은 Compose가 인식할 안정성 표시야” 라고 알려주는 마크입니다.</code></pre>
<p><code>@StableMarker</code>가 붙은 어노테이션이 표시된 클래스는 다음 조건을 만족해야 합니다.</p>
<ol>
<li>equals()의 호출 결과는 동일한 두 인스턴스에 대해 항상 동일합니다</li>
<li>어노테이션이 적용된 public 프로퍼티가 바뀌면 Compose가 감지 가능해야 함(composition에 알립니다)</li>
<li>어노테이션이 적용된 모든 public 프로퍼티는 안정적(stable)이라고 간주합니다.</li>
</ol>
<h3 id="❗-컴파일러가-검사하진-않는다"><strong>❗ 컴파일러가 검사하진 않는다</strong></h3>
<ul>
<li><strong>주의:</strong> 위 사항은 컴파일러와의 <strong>약속일 뿐</strong>, 실제로는 강제 검사는 하지 않아요</li>
<li>즉, <strong>개발자가 이 어노테이션을 붙였으면</strong>, 실제로 이 타입이 안정성을 보장해야 합니다.</li>
</ul>
<h3 id="사용-상황">사용 상황</h3>
<table>
<thead>
<tr>
<th>추상 클래스나 인터페이스</th>
<th>이 타입을 구현하는 모든 클래스가 안정성을 지켜야 한다는 “약속”을 명시할 때</th>
</tr>
</thead>
<tbody><tr>
<td>내부는 가변이지만 외부에선 안정한 경우</td>
<td>내부에 캐시나 상태가 있지만, 외부에서 볼 때 항상 같은 동작을 한다면@Stable로 표시 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="28-immutable">2.8 Immutable</h2>
<blockquote>
<p>인스턴스 생성 이후에 모든 외부로 노출된 프로퍼티의 필드가 변경되지 않을 것이다라는 것을 
컴파일러와 엄격하게 약속합니다.</p>
</blockquote>
<p>이는 Kotlin의 언어 차원에서 제공하는 val 키워드 보다 더 <code>강력한 약속</code> 입니다.</p>
<h3 id="val만으로는-부족한가">val만으로는 부족한가?</h3>
<p>Kotlin의 val은 재할당은 안 될 뿐이지, 그 객체가 <strong>진짜로 불변인지</strong>는 보장하지 않습니다.</p>
<pre><code class="language-kotlin">val list = mutableListOf(1, 2, 3)
list.add(4) // 리스트 내부는 변함</code></pre>
<p>외부에서는 val list로 보이지만, 내부는 <strong>가변적입니다.</strong></p>
<h3 id="immutable이-보장하는-조건">Immutable이 보장하는 조건</h3>
<ol>
<li>모든 속성은 val이어야 함</li>
<li>사용자 정의 getter가 없어야 함 (매번 값이 달라질 수 있음)</li>
<li>모든 속성도 @Immutable 또는 <strong>원시 타입(Int, String, Boolean 등)</strong>이어야 함</li>
<li>속성 값은 객체 생성 이후 절대 변하지 않아야 함</li>
</ol>
<p>@Immutable 어노테이션은 Compose Runtime에게 이미 불변인 타입을 더 강력하게 안정적이다라는 사실을 전달하기 위해 존재합니다.</p>
<p>즉, 값이 변경되지 않기 때문에 실제로 composition에게 값 변경을 알려야 할 필요가 없고, 어쨋거나 이는 <code>@StableMarker</code>에 나열된 요구 사항을 충족하는 결과입니다.</p>
<hr>
<h2 id="29-stable">2.9 @Stable</h2>
<blockquote>
<p>값이 변할 수 있지만 바뀌면 Compose가 그걸 감지할 수 있다는 약속</p>
</blockquote>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th>@Immutable</th>
<th>@Stable</th>
</tr>
</thead>
<tbody><tr>
<td>값이 바뀌는가?</td>
<td>❌ 안 바뀜 (진짜 불변)</td>
<td>✅ 바뀔 수 있음</td>
</tr>
<tr>
<td>변경 감지 가능?</td>
<td>의미 없음 (어차피 안 바뀌니까)</td>
<td>✅ 가능해야 함</td>
</tr>
<tr>
<td>예시</td>
<td>data class(val a: Int, val b: String)</td>
<td>class Counter { var count by mutableStateOf(0) }</td>
</tr>
</tbody></table>
<p>이 어노테이션이 타입에 적용되면 해당 타입이 <code>가변적임</code>(mutable)을 의미하고(그 외의 경우는 @Immutable을 사용해야 함), @StableMarker에 의한 상속의 의미만 지니게 됩니다.</p>
<h3 id="compose가-왜-알아야-할가">Compose가 왜 알아야 할가?</h3>
<p><strong>UI를 다시 그릴 필요가 있는지</strong> 판단하기 위해서입니다. 즉 스마트 리컴포지션을 위해서 입니다.</p>
<ul>
<li>@Immutable: 어차피 안 바뀜 → recomposition 필요 없음</li>
<li>@Stable: 바뀔 수도 있음 → 바뀌면 recomposition 필요</li>
</ul>
<h3 id="stable이-필요한-대표적-상황"><strong>@Stable이 필요한 대표적 상황</strong></h3>
<pre><code class="language-kotlin">@Stable
class UserProfile {
    var name by mutableStateOf(&quot;John&quot;)
}</code></pre>
<p>이 클래스는 가변적임 (name이 바뀔 수 있음)하지만 Compose는 <code>mutableStateOf</code> 덕분에 <strong>변경을 추적 가능</strong></p>
<p>그래서 안정적(stable)이라고 간주할 수 있습니다</p>
<p>아래와 같은 경우는 붙이지 않습니다.</p>
<pre><code class="language-kotlin">class Risky {
    var name = &quot;John&quot;
}</code></pre>
<br>

<h3 id="함수에도-붙일-수-있다"><strong>함수에도 붙일 수 있다</strong></h3>
<p>클래스 외에 함수나 프로퍼티에 적용할 수 있으며 함수가 항상 동일한 입력값에 대해 동일한 결과(멱등성)를 반환한다는 사실을 컴파일러에게 알립니다. 이는 함수의 매개변수가 @Stable 또는 @Immutable으로 마킹 되어있거나, <code>기본 유형</code>(primitive 타입은 기본적으로 안정적인 타입으로 간주됨)인 경우에만 가능합니다.</p>
<pre><code class="language-kotlin">@Stable
fun computeResult(input: Int): Int {
    return input * 2
}</code></pre>
<h3 id="compose-runtime과-관련성">Compose Runtime과 관련성</h3>
<p>Composable 함수에 매개변수로 전달된 모든 타입이 <code>안정적인 타입</code>으로 마킹되면, 위치 기억법을 기반으로 이전 함수 호출과의 매개변수 값이 동일한지 비교하고, 모든 값이 동일하다면 <code>recomposition</code>을 생략합니다.</p>
<h3 id="언제-사용">언제 사용?</h3>
<p>@Stable을 사용할 수 있는 타입의 예로 public프로퍼티가 변경되지는 않지만 불변의 객체로 간주될 수 없는 경우 입니다. 예를 들어, private한 가변적인 상태(state)를 소유하고 있거나, MutableState 객체에 대해서 내부적으로 프로퍼티를 위임하고 외부에서 사용되는 형태는 불가변적인 상태인 경우입니다.</p>
<pre><code class="language-kotlin">@Stable
class Counter {
    private var _count = mutableStateOf(0) // 내부 상태는 바뀜

    val count: Int
        get() = _count.value // 외부에선 읽기 전용처럼 보임

    fun increment() {
        _count.value++
    }
}</code></pre>
<ul>
<li>내부에선 _count가 mutableStateOf라서 <strong>값이 변할 수 있습니다.</strong></li>
<li>외부에선 count만 보임 → <strong>불변처럼 보입니다.</strong></li>
<li>변경은 항상 mutableStateOf를 통해 발생 → <strong>Compose가 추적 가능합니다.</strong></li>
</ul>
<h3 id="주의점">주의점</h3>
<p>어노테이션의 의미가 충족될 것이라는 확신이 없다면 이 어노테이션을 절대 사용하면 안됩니다. 그렇지 않으면 Compose Compiler에게 잘못된 정보를 제공하게 되어 쉽게 런타임 오류가 발생할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose Internals - Composable 함수들]]></title>
            <link>https://velog.io/@dkdk_0422/Compose-Internals-Composable-%ED%95%A8%EC%88%98%EB%93%A4</link>
            <guid>https://velog.io/@dkdk_0422/Compose-Internals-Composable-%ED%95%A8%EC%88%98%EB%93%A4</guid>
            <pubDate>Mon, 02 Jun 2025 08:15:13 GMT</pubDate>
            <description><![CDATA[<p>해당 내용은 <a href="https://leanpub.com/composeinternalskor">Compose Internals</a> 1장 Composable 함수들 내용을 공부하며 기록한 내용입니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/f7a6cdca-d1e8-482c-9e03-ddffdc87e94c/image.png" alt=""></p>
<h2 id="11-composable-함수의-의미">1.1 Composable 함수의 의미</h2>
<blockquote>
<p><code>@Composable</code>은 컴파일러에게 <strong>이 함수가 Compose 트리의 일부 노드를 생성한다는 의도</strong>를 전달합니다.</p>
</blockquote>
<pre><code class="language-kotlin">@Composable
fun Greeting(name: String) {
    Text(&quot;Hello, $name!&quot;)
}</code></pre>
<p>이 함수는 &quot;Hello, $name!&quot;이라는 <strong>UI 요소를 생성</strong>하고,
이를 <strong>Compose 트리에 하나의 노드로 방출(emit)</strong>합니다.</p>
<h3 id="🌳-composable-tree란"><strong>🌳 Composable Tree란?</strong></h3>
<p>Compose는 UI를 메모리 상에서 <strong>트리(Tree)</strong> 구조로 표현합니다. 이 구조는 HTML의 DOM처럼, <strong>각 Composable 함수가 하나의 노드가 되는 계층적 구조</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/6b5ed260-bb24-4bb6-96ee-f5425db5848b/image.png" alt=""></p>
<h3 id="emit-ui를-방출-한다는-의미"><strong>Emit: UI를 방출 한다는 의미</strong></h3>
<blockquote>
<p>Composable 함수는 실행되면 UI를 <strong>리턴하는 것이 아니라</strong>, <strong>트리에 노드를 추가(emit)</strong> 합니다. 이 동작을 <strong>방출</strong> 이라고 부릅니다.</p>
</blockquote>
<p>@Composable 어노테이션을 사용함으로써, 컴파일러에게 함수가 데이터를 하나의 노드(node)로 변환하여 Composable 트리(tree)에 기재하겠다는 의도를 전달합니다.</p>
<p>즉, Composable 함수를 <code>@Composable (Input) → Unit</code> 와 같은 형태로 볼 때, 입력값은 데이터가 되고, 출력은 일반적으로 생각하는 함수의 반환 값이 아니라, 트리에 요소를 삽입하기 위해 기재된 일련의 동작(action)이라고 볼 수 있습니다.  이 동작을 함수 실행의 부수 효과(side effect)로 발생한다고 말할 수 있습니다.</p>
<p>즉, Composable 함수는 “화면을 그리는 설계도”로서, 실행 시 트리에 UI 요소를 배치하는 역할을 합니다.</p>
<p>이때 함수의 리턴값은 보통 Unit이며, <strong>UI를 그리는 부수 효과(Side Effect)</strong>가 발생합니다.</p>
<h3 id="composable-함수를-실행하는-목적">Composable 함수를 실행하는 목적</h3>
<blockquote>
<p>트리의 인메모리 표현(in-memory representation)을 만들거나 업데이트하는 것</p>
</blockquote>
<p><strong>Composable 함수는 화면을 그리기 위한 트리 구조를 메모리에서 만들거나 업데이트 하는 것이 목적</strong>입니다.</p>
<p>Jetpack Compose는 UI를 그릴 때 실제 화면(UI)을 바로 만드는 게 아니라, 메모리 안에 UI 구조(트리 형태)를 먼저 만들게 됩니다.</p>
<p><strong>Composable 함수는 읽은 데이터가 바뀌면 자동으로 다시 실행되며</strong>, 메모리상의 트리를 갱신(업데이트)합니다.</p>
<ul>
<li><strong>삽입</strong>: 새 데이터가 생기면 새로운 노드를 추가</li>
<li><strong>제거</strong>: 데이터가 사라지면 해당 노드를 삭제</li>
<li><strong>교체</strong>: 값이 바뀌면 노드를 바꿈</li>
<li><strong>위치 이동</strong>: 순서가 바뀌면 재배치</li>
</ul>
<hr>
<h2 id="12-composable-함수의-속성">1.2 Composable 함수의 속성</h2>
<blockquote>
<p>@Composable이 붙은 함수는 단지 UI를 그리는 게 아니라, <strong>Compose 런타임이 해당 함수에 대해 여러 최적화 기법을 적용할 수 있도록 설계된 특별한 타입</strong>이라는 뜻입니다.</p>
</blockquote>
<p>일반 함수와 다르게 @Composable 함수는 다음과 같은 정보를 런타임에게 제공합니다:</p>
<ul>
<li><strong>이 함수는 Compose 트리의 노드다</strong></li>
<li><strong>다시 실행(Recomposition)될 수 있다</strong></li>
<li><strong>상태를 기억하거나 읽을 수 있다</strong></li>
</ul>
<h3 id="compose-runtime-최적화">Compose Runtime 최적화</h3>
<p>Compose는 다음과 같은 최적화를 하려고 합니다:</p>
<ul>
<li><strong>병렬 Composition</strong>: 독립적인 UI는 동시에 그릴 수 있음</li>
<li><strong>스마트 Recomposition</strong>: 바뀐 부분만 다시 그림</li>
<li><strong>위치 기억법</strong>(positional memoization): UI 위치를 기준으로 상태 기억</li>
<li><strong>선택적 실행 순서 변경</strong>: 덜 중요한 UI는 나중에 그리기 등</li>
</ul>
<p>이런 최적화를 하기 위해서는 이 Composable 함수가 어디서 어떻게 호출되며, 서로 영향을 주는지 아닌지에 대한 확실한 정보(확실성)이 필요합니다.</p>
<h3 id="확실성">확실성</h3>
<p>Compose가 “이 함수는 다른 함수와 완전히 독립적이구나” 혹은 “이 함수는 저 데이터가 바뀌면 꼭 다시 실행되어야 하겠네!” 같은 걸 <strong>미리 알고 있어야</strong> 최적화할 수 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun ProfileImage() {
    // 이 함수는 독립적입니다. 이름이 바뀌어도 다시 실행 안 해도 됩니다.
}

@Composable
fun Profile(name: String) {
    Text(name) // 이름이 바뀌면 여긴 다시 실행되어야 합니다.
}</code></pre>
<p>즉, @Composable 함수는 <strong>런타임에게 약속된 계약서로 비유할 수 있습니다.</strong></p>
<p>계약을 믿고 런타임은 <strong>함수를 병렬로 돌리거나</strong>, <strong>순서를 바꾸거나</strong>, <strong>다시 실행하지 않도록 최적화</strong>할 수 있습니다.</p>
<hr>
<h2 id="13-호출-컨텍스트-calling-context">1.3 호출 컨텍스트 (Calling context)</h2>
<p>Composable 함수의 속성은 대부분 Compose Compiler에 의해 활성화 됩니다.</p>
<p><code>@Composable</code> 어노테이션이 붙은 함수는 Kotlin 컴파일러 플러그인인 Compose Compiler에 의해 변환됩니다. 이 변환 과정에서 컴파일러는 함수의 매개변수 목록 끝에 Composer 객체를 암묵적으로 추가합니다. 이 객체는 런타임에 주입되며, 모든 하위 Composable 호출로 전달되어 트리의 모든 수준에서 접근할 수 있게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/004684fa-68ea-494d-b68a-93563fd90bb2/image.png" alt=""></p>
<p>아래와 같은 Composable 함수를</p>
<pre><code class="language-kotlin">@Composable
fun NamePlate(name: String, lastname: String) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = name)
        Text(text = lastname, style = MaterialTheme.typography.titleMedium)
    }
}</code></pre>
<p>Compose Compiler가 아래와 같이 변환합니다.</p>
<pre><code class="language-kotlin">fun NamePlate(name: String, lastname: String, $composer: Composer&lt;*&gt;) {
    Column(modifier = Modifier.padding(16.dp), $composer) {
        Text(text = name, $composer)
        Text(text = lastname, style = ..., $composer)
    }
}</code></pre>
<h3 id="composer">Composer</h3>
<ul>
<li>Composer는 <strong>UI 트리를 만들고 추적하고 갱신하는 관리자</strong>입니다.</li>
<li>우리가 @Composable 함수를 호출하면, <strong>자동으로 이 Composer 객체가 전달되어</strong> 트리를 구성합니다.</li>
<li>개발자는 직접 사용할 필요 없고, <strong>컴파일러가 자동으로 처리</strong>합니다.</li>
</ul>
<p>위 코드에서 확인할 수 있듯이 <code>Composer</code>는 트리 내에서 모든 Composable 호출로 전달됩니다.</p>
<p>Compose Compiler는 Composable 함수가 오로지 다른 Composable 함수에서만 호출될 수 있게 합니다.</p>
<ul>
<li>그래야만 <strong>Composer가 항상 유효한 트리 구조</strong>를 유지할 수 있기 때문입니다.</li>
<li>이 규칙이 없다면 Compose 트리가 깨지고, <strong>재구성이 불가능해질 수 있습니다</strong>.</li>
</ul>
<blockquote>
<p>즉, <code>Composer</code>는 개발자가 작성하는 Composable 코드와 Compose Runtime 간의 중재자 역할을 합니다.</p>
</blockquote>
<p>Composable 함수는 트리에 대한 변경 사항을 전달하고, 런타임 시에 트리의 형태를 빌드하거나 업데이트하기 위해 <code>Composer</code>를 사용합니다.</p>
<hr>
<h2 id="14-멱등성-idempotent">1.4 멱등성 (Idempotent)</h2>
<blockquote>
<p><strong>특정 작업이나 연산을 여러 번 반복하더라도 결과가 처음 수행한 결과와 동일하게 유지되는 성질</strong>
<strong>Recomposition 최적화</strong>의 기반</p>
</blockquote>
<p>Composable 함수는 생성하는 노드 트리에 대해 멱등성을 가져야 합니다. 동일한 입력 매개변수를 사용하여 Composable 함수를 여러 번 다시 실행하더라도 동일한 트리가 생성되어야 합니다.</p>
<p>Compose Runtime은 recomposition과 같은 작업을 위해 이러한 멱등성이 제공하는 가정에 의존합니다.</p>
<h3 id="recomposition">Recomposition</h3>
<blockquote>
<p>입력값이 변경될 때 마다 Composable 함수를 다시 실행하여 업데이트된 정보를 방출시키고 트리를 업데이트 하는 작업. <code>Compose Runtime</code>이 역할을 수행함</p>
</blockquote>
<p>UI를 구성하는 데이터가 변경되었을 때,</p>
<ul>
<li>해당 데이터를 사용하는 Composable 함수만 <strong>다시 실행</strong>해서</li>
<li><strong>UI 트리를 업데이트</strong>하는 작업입니다.</li>
</ul>
<p>Compose는 이 과정에서 UI 전체를 다시 그리지 않고, 변경된 부분만 <strong>정확하게</strong> 다시 그립니다. 이걸 가능하게 하는 조건 중 하나가 바로 <strong>멱등성</strong>입니다.</p>
<p>Recomposition의 과정은 트리를 아래로 순회하면서 어떤 노드를 재구성 해야 하는지 확인합니다. 이 과정에서 입력값이 변경된 노드만 recomposition을 수행하고 나머지는 생략합니다. 특정 노드를 생략하는 것은 해당 노드를 대표하는 Composable 함수가 멱등성의 성질을 가질 때만 가능합니다. 그 이유는 런타임은 동일한 입력값을 제공할 경우 동일한 결과를 생성한다고 가정할 수 있기 때문입니다.</p>
<p>동일한 입력값에 대한 결과는 이미 메모리에 적재되어 있으므로 Compose는 다시 실행할 필요가 없고, 결과적으로 생략할 수 있습니다.</p>
<hr>
<h2 id="15-통제되지-않은-사이드-이펙트-방지">1.5 통제되지 않은 사이드 이펙트 방지</h2>
<h3 id="사이드-이펙트side-effect">사이드 이펙트(side effect)</h3>
<blockquote>
<p>호출된 함수의 제어를 벗어나서 발생할 수 있는 예상치 못한 모든 동작을 의미합니다.</p>
</blockquote>
<ul>
<li><strong>로컬 캐시에서 데이터 읽기</strong></li>
<li><strong>네트워크 요청</strong></li>
<li><strong>전역 변수 설정</strong></li>
<li><strong>파일 시스템 접근</strong></li>
<li><strong>SharedPreferences 수정</strong></li>
<li><strong>Toast 띄우기, Navigation 이동</strong> 등</li>
</ul>
<p>이런 작업은 <strong>입력값에만 의존하지 않고</strong>, <strong>외부 요인에 따라 달라질 수 있기 때문에</strong> 사이드 이펙트로 간주됩니다.</p>
<p>Compose Runtime은 Composable 함수가 예측 가능하도록(결정론적인) 기대하기 때문에 사이드 이펙트가 포함된 Composable 함수는 예측이 어려워지고, 결과적으로 Compose에게 좋지 않습니다.</p>
<p>이 말인 즉, 사이드 이펙트는 Compose 내에서 Compose 내에서 아무 통제를 받지 않고 여러 번 실행될 수 있습니다. Composeable 함수가 사이드 이펙트를 실행한다면 매 함수 호출 시마다 새로운 프로그램 상태를 생성할 수 있으므로, Compose Runtime에게 필수적인 <code>멱등성</code>을 따르지 않게 됩니다.</p>
<pre><code class="language-kotlin">@Composable
fun FetchData() {
    val data = fetchFromNetwork()  // 사이드 이펙트 발생
    Text(data)
}</code></pre>
<p>Composable 함수는 근본적으로 Runtime에 의해 짧은 시간 내에 여러 번 다시 실행될 수 있으며, 이로 인해 네트워크 요청이 여러 번 수행되어 제어를 벗어날 수 있습니다.</p>
<p>최악의 상황은 이러한 사이드 이펙트가 아무 조건 없이 다른 스레드에서 실행될 수 있다는 것입니다.</p>
<pre><code class="language-kotlin">Compose Runtime은 Composable 함수에 대한 실행 전략을 선택할 권한을 보유하고 있습니다.
이는 하드웨어의 멀티 코어의 이점을 활용하기 위해 recomposition을 다른 스레드로 이전시킬 수 있거나,
필요성이나 우선순위에 따라 임의의 순서로 실행할 수 있습니다.</code></pre>
<h3 id="위험한-코드의-예시">위험한 코드의 예시</h3>
<pre><code class="language-kotlin">@Composable
fun MainScreen() {
    Header()         // 외부 상태를 변경
    ProfileDetail()  // 그 외부 상태를 읽음 ← ⚠ 위험한 패턴
    EventList()
}</code></pre>
<p>Header()에서 어떤 전역 변수를 설정하고, ProfileDetail()에서 전역 변수를 사용한다고 가정하겠습니다.</p>
<p>하지만 Compose는 이 함수들을 <strong>아무 순서로나</strong> 혹은 <strong>병렬로</strong> 실행할 수 있기 때문에, 예상한 동작을 보장할 수 없습니다.</p>
<h3 id="해결방안">해결방안</h3>
<p>Composable 함수를 stateless(무상태, 상태를 보존하지 않음)하게 만들려고 노력해야 합니다.
Composable 함수는 모든 입력값은 매개변수로서 받고, 결과를 생성하기 위해 주어진 입력값만을 사용합니다.</p>
<ul>
<li>Composable 함수는 <strong>입력만 받도록</strong> 설계하세요.</li>
<li>상태나 외부 의존성 없이, 주어진 값만으로 UI를 <strong>단순하게 그리는 역할</strong>만 합니다.</li>
<li>이런 함수는 단순하고 높은 재사용성을 갖게 합니다.</li>
</ul>
<h3 id="사이드-이펙트가-필요한-순간">사이드 이펙트가 필요한 순간</h3>
<blockquote>
<p>stateful(상태유지, 상태를 보유하는) 프로그램을 개발하기 위해서는 <code>사이드 이펙트</code> 가 필요하므로, 특정 단계에서는 우리가 사이트 이펙트를 실행해야만 합니다 (주로 <strong>Composable 트리의 루트에서 빈번하게 실행</strong>됩니다).</p>
</blockquote>
<ul>
<li>네트워크 호출</li>
<li>DB 저장</li>
<li>파일 접근</li>
<li>상태 관찰 (예: LiveData, Flow 등)</li>
</ul>
<p>이를 위해 <strong>Compose는 <code>Effect Handlers</code></strong> (이펙트 핸들러)라는 안전한 도구를 제공합니다.</p>
<h3 id="effect-handlers">Effect Handlers</h3>
<blockquote>
<p>Composable 함수 내에서 아무런 제어를 받지 못하고 직접적으로 이펙트가 호출되는 것을 방지하는 역할 수행</p>
</blockquote>
<p><strong>이펙트 핸들러는 사이드 이펙트가 Composable의 라이프사이클을 인식하도록 하여</strong>, <strong>해당 라이프사이클에 의해 제한되거나 실행될 수 있게 합니다</strong>.</p>
<p>Composable 노드가 트리를 떠날 때 자동으로 이펙트를 해제하거나 취소할 수 있게 하고, 이펙트에 주어진 입력값이 변경되면 재실행시키거나, 심지어 동일한 이펙트를 recomposition 과정에서 유지시키고 한 번만 호출되게 할 수 있습니다. </p>
<h2 id="16-재시작-가능-restartable">1.6 재시작 가능 (Restartable)</h2>
<h3 id="일반함수">일반함수</h3>
<p>일반 함수는 한 번 호출되면, 호출된 위치로부터 <strong>순차적으로 실행</strong>됩니다.
다른 함수를 호출할 수는 있지만, <strong>재호출(re-run)</strong> 되지는 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/b63210da-4d7b-416f-95ac-18bf07a62ddb/image.png" alt=""></p>
<h3 id="composable-함수">Composable 함수</h3>
<p>recomposition으로 <strong>여러 번 다시 시작될 수 있습니다</strong>.
그래서 런타임은 함수가 재실행될 수 있도록 해당 함수들에 대한 <code>참조를 유지</code>합니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/132882bf-1f2f-4218-9e45-a6916e705cfa/image.png" alt=""></p>
<p>Composable 함수는 관찰하는 상태(<code>state</code>)의 변화에 기반하여 반작용적으로 재실행되도록 설계되었습니다. </p>
<p>Compose Compiler는 일부 상태(<code>state</code>)를 읽는 모든 Composable 함수를 찾아 Compose Runtime에게 재시작하는 방법을 가르치는데 필요한 코드를 생성합니다. 
상태를 읽지 않는 Composable은 재시작할 필요가 없으므로, Compose Runtime에게 해당 방법을 가르칠 이유가 없습니다.</p>
<hr>
<h2 id="1-7-빠른-실행-fast-execution">1. 7 빠른 실행 (fast execution)</h2>
<p>Composable 함수들은 UI를 구축하거나 반환하지 않습니다.</p>
<h3 id="composable에-대한-오해와-진실">Composable에 대한 오해와 진실</h3>
<ul>
<li>❌ <strong>오해</strong>: Composable 함수는 UI를 반환한다.</li>
<li>✅ <strong>진실</strong>: Composable 함수는 <strong>UI의 설명(description)</strong> 을 메모리에 담는 <strong>노드 트리를 생성</strong>합니다</li>
</ul>
<p>Composable은 단순히 인메모리 구조를 구축 및 업데이트하기 위한 데이터를 방출할 뿐입니다.
이는 Composable을 더욱 빠르게 만들며, 런타임이 아무 문제없이 해당 함수를 여러번 실행할 수 있도록 합니다.</p>
<hr>
<h2 id="18-위치-기억법-positional-memoization">1.8 위치 기억법 (Positional memoization)</h2>
<h3 id="메모이제이션memoization">메모이제이션(memoization)</h3>
<blockquote>
<p>함수가 입력값에 기반하여 결과를 캐싱하는 기법
즉, 입력값이 같으면 결과를 캐싱해서 다시 계산하지 않도록 하는 기법</p>
</blockquote>
<h3 id="순수-함수">순수 함수</h3>
<blockquote>
<p>항상 같은 입력에는 같은 출력을 주고, 외부 상태에 영향을 주지 않는 함수
메모이제이션이 가능한 함수 조건입니다.</p>
</blockquote>
<h3 id="composable의-위치-기반-기억법">Composable의 위치 기반 기억법</h3>
<p>함수 메모이제이션에서, 함수 호출은 그 이름, 타입 및 매개변수 값의 조합을 통해 식별됩니다.
Compose의 경우는 추가적인 요소가 고려됩니다.</p>
<p>Composable 함수는 소스 코드 내 호출 위치에 대한 불변의 정보를 가지고 있습니다.
Compose Runtime은 동일한 함수가 동일한 매개변수 값으로 다른 위치에서 호출될 때, 동일한 Composable 부모 트리 내에서 고유한 다른 ID를 생성합니다.</p>
<pre><code class="language-kotlin">@Composable
fun MyComposable() {
    Text(&quot;Hello&quot;) // 위치 A
    Text(&quot;Hello&quot;) // 위치 B
    Text(&quot;Hello&quot;) // 위치 C
}</code></pre>
<ul>
<li>인메모리 트리는 해당 함수들을 세 개의 다른 인스턴스로 저장하게 되고, 각각은 고유한 정체성을 가지게 됩니다.
<img src="https://velog.velcdn.com/images/dkdk_0422/post/1bde8742-ffb2-4a5a-a9ba-f86490513c0e/image.png" alt=""></li>
</ul>
<ul>
<li>여기서 Text(&quot;Hello&quot;)는 3번 호출되지만, 각 호출은 서로 다른 위치에 있기 때문에 Compose는 이것들을 <strong>세 개의 다른 UI 요소로 인식</strong>합니다.</li>
<li>즉, 동일한 Composable이라도 <strong>호출 위치가 다르면 전혀 다른 것으로 간주합니다.</strong></li>
</ul>
<h3 id="왜-위치-기반으로-기억하나요"><strong>왜 위치 기반으로 기억하나요?</strong></h3>
<p>Compose는 매 프레임마다 UI를 다시 그리는 방식이 아니라, 필요한 부분만 재구성(recomposition)합니다.</p>
<ul>
<li>이때 어떤 Composable을 <strong>다시 그릴지 판단하기 위해</strong> 위치를 기억합니다.</li>
<li>그래서 Compose는 <strong>위치 + 입력값의 조합</strong>을 기준으로 캐싱된 결과를 재사용할지 결정합니다.</li>
</ul>
<h3 id="반복문에서-문제가-생긴다-중요">반복문에서 문제가 생긴다? (중요)</h3>
<p>종종 Compose Runtime 입장에서 Composable 함수에게 고유한 정체성을 할당하는 것이 어려운 경우가 있습니다. 반복문에서 생성된 Composable의 리스트 형태 입니다.</p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            MovieOverview(movie)
        }
    }
}</code></pre>
<p>이 코드에서 <code>MovieOverview</code> 는 매번 같은 위치에서 호출되지만, 각각의 talk 요소는 리스트 내에서 다른 항목으로 간주되고, 결과적으로 트리에서는 서로 다른 노드로 구성됩니다. 이와 같은 경우에 Compose Runtime은 고유한 ID를 생성하고 여전히 서로 다른 Composable 함수를 구별할 수 있도록 호출 순서에 의존합니다.</p>
<p>이러한 방식은 리스트 끝에 새로운 요소를 추가할 때는 잘 동작합니다. 리스트 내의 기존 Composable 함수들이 여전히 같은 위치에 있기 때문입니다. (색상이 동일 = 컴포저블 재구성되지 않았음을 의미)</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/67cf1ca6-61ce-47fb-9485-5fb54352c9e5/image.png" alt=""></p>
<p>하지만, 리스트 상단이나 중간에 요소를 추가한다면 Compose Runtime은 요소 삽입이 발생하는 지점 아래의 모든 <code>MovieOverview</code> Composable 함수에 대해서 recomposition을 발생시키게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/7244b18b-a238-4a53-8a24-e05a140022dc/image.png" alt=""></p>
<p>이유는 Composable 함수들의 위치가 변경되었기 때문인데, 심지어 해당 함수들의 입력값이 변경되지 않았더라도 해당합니다. 이런 방식은 업데이트가 생략되었어야 할 Composable 함수들에게 <code>recomposition</code>이 발생했기 때문에 매우 비효율적입니다.</p>
<p>이 문제를 해결하기 위해 Compose는 <code>key</code> 라는 Composable 함수를 제공하는데, 이 함수를 이용하여 Composable 함수에게 명시적인 키 값을 지정할 수 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreenWithKey(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}</code></pre>
<p>각 MoviewOverview Composable에 대한 키 값으로 movie.id(고유한)를 사용하고 있으며, 이것은 Composable Runtime이 Composable <strong>함수의 위치에 상관없이 리스트에 속한 모든 항목의 정체성을 유지하도록 합니다</strong>.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/b3534f48-063c-450a-bee6-333c45f7c77a/image.png" alt=""></p>
<p>위치 기반 기억법은 Compose Runtime이 설계에 따라 Composable 함수를 기억할 수 있게 해 줍니다.
Compose Compiler에 의해 재시작(Restartable)이 가능하다고 추론된 모든 Composable 함수는 recomposition을 생략할 수 있어야 하며, <code>인메모리에 자동적으로 기억</code>됨을 의미합니다.</p>
<h3 id="remember와-메모이제이션">remember와 메모이제이션</h3>
<blockquote>
<p>remember는 트리의 상태를 유지하는 인메모리 구조에서 값을 메모리에 읽고 쓰는 역할을 수행하는 Composable 함수입니다.</p>
</blockquote>
<p>Compose는 매번 UI를 그릴 때마다 전체 함수를 실행하지만, <strong>기존 결과를 재활용</strong>해서 불필요한 연산을 피합니다. 이를 위해 상태를 기억하고 있어야 하고, 이때 사용되는 것이 remember입니다.</p>
<pre><code class="language-kotlin">@Composable
fun FilteredImage(path: String) {
    val filters = remember { computeFilters(path) }
    ImageWithFiltersApplied(filters)
}

@Composable
fun ImageWithFiltersApplied(filters: List&lt;Filter&gt;) {
        TODO()
}</code></pre>
<ul>
<li>remember는 컴포저블 함수 내부에서 호출된 정확한 위치를 기준으로 값을 기억합니다.</li>
<li>computeFilters(path)의 결과는 메모리에 <strong>해당 위치와 연결된 슬롯</strong>에 저장됩니다.</li>
<li>다음번 recomposition에서도 <strong>해당 위치에 도달하면</strong> 캐싱된 값이 그대로 사용됩니다.</li>
<li>이때 path 값이 바뀌지 않았다면 재계산은 생략됩니다.</li>
</ul>
<p>Compose는 내부적으로 <strong>Slot Table</strong>이라는 구조를 이용합니다. 쉽게 말해:</p>
<ul>
<li>각 Composable 함수의 호출 위치는 슬롯(slot)이라는 단위로 기록됩니다.</li>
<li>이 슬롯은 함수 호출 순서와 위치에 기반해 고유하게 관리됩니다.</li>
<li>remember는 해당 슬롯에 값을 저장하고 다음에 동일한 위치에 도달하면 이를 <strong>조회</strong>합니다.</li>
</ul>
<h3 id="remember의-작동-범위는-로컬context-scoped이다"><strong>remember의 작동 범위는 로컬(context-scoped)이다</strong></h3>
<p>Compose에서 메모이제이션은 애플리케이션 전체에 걸쳐서 적용되지 않습니다.
무언가가 메모리에 기록될 때, 메모이제이션을 호출하는 Composable의 컨텍스트 내에서만 수행됩니다.</p>
<ul>
<li>remember의 값은 해당 Composable 함수 호출 컨텍스트 내에서만 유효합니다.</li>
<li>전역적으로 캐싱되지 않으며, 재사용 범위도 <strong>슬롯 테이블 내의 위치</strong>로 한정됩니다.</li>
<li>이 말은: <strong>항상 remember는 그 위치의 값만 기억한다는 것을 의미합니다.</strong></li>
</ul>
<hr>
<h2 id="19-suspend-함수와의-유사성">1.9 Suspend 함수와의 유사성</h2>
<h3 id="suspend-함수란">suspend 함수란</h3>
<p>suspend 함수는 <strong>비동기 처리 또는 중단 가능한 작업</strong>을 Kotlin 코루틴에서 다루기 위해 사용됩니다.</p>
<ul>
<li>호출 컨텍스트(Continuation)를 <strong>컴파일러가 자동으로 추가</strong>합니다.</li>
<li>Continuation<T>은 현재 작업을 멈췄다가, 나중에 <strong>다시 이어서 실행할 수 있도록</strong> 해줍니다.</li>
<li>즉, 중단(suspension)과 재개(resumption)를 관리합니다.</li>
</ul>
<pre><code class="language-kotlin">suspend fun publishTweet(tweet: Tweet): Post</code></pre>
<p>위의 코드는 Kotlin 컴파일러에 의해 아래와 같이 변경됩니다.</p>
<pre><code class="language-kotlin">fun publishTweet(tweet: Tweet, continuation: Continuation&lt;Post&gt;): Any</code></pre>
<p> Continuation은 <strong>콜백처럼 작동하는 객체</strong>이며, 실행이 어디까지 왔고 어떤 데이터를 가지고 있는지를 기억합니다.</p>
<p>즉, 다양한 중단점에서 실행을 일시 중단하고 재개하는 데 필요한 모든 정보를 담고 있습니다.</p>
<h3 id="composable-함수-1">Composable 함수</h3>
<p>재시작(restartable)이 가능하고, 상태(state) 등으로부터 반응할 수 있도록 만듭니다.</p>
<ul>
<li>실행을 <strong>중단(suspend)하는 것과는 다르게</strong>, <strong>재시작(restart)</strong> 가능한 구조입니다.</li>
<li>Compose 컴파일러는 @Composable 함수에 <strong>숨겨진 Slot Table과 Remember 관리 컨텍스트</strong>를 자동으로 추가합니다.</li>
<li>이 구조는 <strong>UI 트리를 인메모리에 저장</strong>하고, 상태가 바뀔 때 필요한 부분만 다시 실행(recompose)할 수 있도록 합니다.</li>
</ul>
<h3 id="왜-compose는-suspend를-쓰지-않았을까"><strong>왜 Compose는 suspend를 쓰지 않았을까?</strong></h3>
<ul>
<li><strong>suspend는 실행 흐름 제어에 집중된 개념</strong>이므로, UI 상태 재조합과는 맞지 않음</li>
<li>Compose는 <strong>UI 트리 상태를 메모리에 저장하고 최적화</strong>하는 데 초점을 맞춤</li>
<li><strong>슬롯 테이블, remember 시스템, recomposition 컨트롤 등 Compose 전용 런타임</strong>이 필요</li>
<li>suspend를 사용하면 오히려 Compose가 원하는 <strong>재시작 기반 구조</strong>를 표현하기 어렵고, 런타임 오버헤드가 발생할 수 있음</li>
</ul>
<hr>
<h2 id="110-composable-함수의-색깔">1.10 Composable 함수의 색깔</h2>
<h3 id="함수-컬러링-개념">함수 컬러링 개념</h3>
<p>이 개념은 <strong>동기(synchronous) 함수</strong>와 <strong>비동기(asynchronous) 함수</strong>가 서로 완전히 <strong>다른 특성을 가지며, 쉽게 섞이지 않는다는 사실</strong>에서 나왔습니다.</p>
<p>Bob Nystrom은 동기 함수는 파란색, 비동기 함수는 빨간색처럼 생각할 수 있다고 했습니다. </p>
<ul>
<li>suspend 함수는 <strong>다른 suspend 함수</strong>에서만 호출 가능</li>
<li>@Composable 함수도 <strong>다른 @Composable 함수</strong>에서만 호출 가능</li>
</ul>
<p>즉, <strong>함수의 색이 다르면 섞을 수 없고</strong>, 중간에 통합 지점(entry point)이 필요하다는 의미입니다.</p>
<h3 id="kotlin의-suspend함수와-채색-함수"><strong>Kotlin의 suspend함수와 채색 함수</strong></h3>
<pre><code class="language-kotlin">suspend fun fetchUser(): User</code></pre>
<ul>
<li>이 함수는 시간이 걸릴 수 있는 네트워크 작업 등을 처리할 수 있습니다.</li>
<li>하지만 이 함수는 <strong>다른 suspend 함수에서만</strong> 호출할 수 있습니다.</li>
<li>일반 함수에서 이걸 쓰고 싶다면 <strong>launch나 runBlocking 같은 통합점</strong>이 필요합니다.</li>
</ul>
<p>이 제한 덕분에 Kotlin은<strong>비동기 실행을 안전하고 명확하게 제어할 수 있습니다.</strong></p>
<h3 id="jetpack-compose의-composable함수도-채색-함수"><strong>Jetpack Compose의 @Composable함수도 채색 함수</strong></h3>
<pre><code class="language-kotlin">@Composable
fun Greeting(name: String) {
    Text(&quot;Hello, $name&quot;)
}</code></pre>
<ul>
<li>Composable 함수는 프로그램 로직을 작성하기 위해 설계된 것이 아니라 <strong><code>노드 트리의 변경사항을 기술하기 위한 것</code></strong>입니다.</li>
<li>마찬가지로, 이 함수는 <strong>다른 @Composable 함수에서만</strong> 호출할 수 있습니다.</li>
<li>일반 함수에서 호출하려면 <strong>setContent { ... } 같은 통합 진입점</strong>이 필요합니다.</li>
</ul>
<h3 id="inline-함수">inline 함수</h3>
<blockquote>
<p>inline 함수란 <strong>함수를 호출할 때, 실제 코드가 그 자리에 복사되는 함수</strong>입니다.</p>
</blockquote>
<pre><code class="language-kotlin">inline fun doSomething(action: () -&gt; Unit) {
    println(&quot;Start&quot;)
    action()
    println(&quot;End&quot;)
}
</code></pre>
<pre><code class="language-kotlin">컴파일러가 doSomething을 호출하면 
doSomething {
    println(&quot;Doing work&quot;)
}

요래 됩니다
println(&quot;Start&quot;)
println(&quot;Doing work&quot;)
println(&quot;End&quot;)</code></pre>
<h3 id="composable-함수-안에서-foreach-같은-일반-함수를-사용할-수-있는-이유">Composable <strong>함수 안에서 forEach 같은 일반 함수를 사용할 수 있는 이유?</strong></h3>
<pre><code class="language-kotlin">@Composable
fun SpeakerList(speakers: List&lt;Speaker&gt;) {
    Column {
        speakers.forEach {
            Speaker(it) // 어떻게 이게 되는지?
        }
    }
}</code></pre>
<p>forEach는 사실 inline 함수이기 때문에, 이 안에서 Speaker(it) 같은 Composable 호출을 허용할 수 있습니다.</p>
<p>인라인 덕분에 람다 함수 안의 Composable 호출이<strong>SpeakerList 함수 본문에 복사되어 들어가기 때문에Composable → Composable 관계가 유지됩니다.</strong></p>
<h3 id="함수-컬러링은-정말-문제일까"><strong>함수 컬러링은 정말 문제일까?</strong></h3>
<ul>
<li><strong>Composable과 일반 함수, 혹은 suspend 함수 사이를 넘나드는 작업</strong>이 번거롭고 헷갈릴 수 있습니다</li>
<li>하지만 Compose나 Coroutine은 이를 통합 지점으로 명확히 나눔으로써 <strong>개발자와 컴파일러 모두에게 안전성과 명확성</strong>을 제공합니다.</li>
</ul>
<hr>
<h2 id="111composable-함수-타입">1.11Composable 함수 타입</h2>
<p>@Composable 어노테이션은 컴파일 시점에 함수의 타입을 효과적으로 변경합니다.
함수의 구문(syntax)적 관점에서 Composable 함수의 타입은 <code>@Composable (T) → A</code> 입니다.</p>
<p>여기서 A는 Unit일 수도 있고, 함수가 값을 반환하는 경우(예를 들어 remember) 다른 타입일 수도 있습니다.
일반적인 람다를 선언하는 것처럼 Composable 람다를 선언할 수 있습니다.</p>
<pre><code class="language-kotlin"> val textComposable: @Composable (String) -&gt; Unit = {
   Text(
     text = it,
     style = MaterialTheme.typography.subtitle1
   )
 }

 @Composable
 fun NamePlate(name: String, lastname: String) {
   Column(modifier = Modifier.padding(16.dp)) {
     Text(
       text = name,
       style = MaterialTheme.typography.h6
     )
     textComposable(lastname)
   }
 }</code></pre>
<p>또한 Composable 함수는 @Composable Scope.() → A와 같은 형태의 타입을 가질 수 있는데, 이는 특정 Composable로만 정보 범위를 지정하는 데 자주 사용됩니다.</p>
<pre><code class="language-kotlin">inline fun Box(
   ...,
   content: @Composable BoxScope.() -&gt; Unit
) {
   // ...
   Layout(
     content = { BoxScopeInstance.content() },
     measurePolicy = measurePolicy,
     modifier = modifier
   )
}</code></pre>
<p>@Composable 어노테이션은 런 타임 시 Composable 함수의 유효성을 검사하고 사용하는 방법을 변경하는데, 이것이 바로 Composable 함수가 Kotlin의 표준 함수와 다른 타입으로 간주되는 이유입니다.</p>
<h3 id="왜-이렇게-타입이-다를까"><strong>왜 이렇게 타입이 다를까?</strong></h3>
<ol>
<li>@Composable 함수는 <strong>특별한 호출 환경</strong>을 필요로 합니다. (remember, recomposition, SlotTable 등)</li>
<li>그래서 컴파일러는 @Composable을 보고 특수한 호출 트랜스폼(transform)을 적용합니다.</li>
<li>따라서 @Composable () -&gt; Unit은 () -&gt; Unit과 <strong>다른 함수 타입</strong>입니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin - 더 이상 Stack을 사용하지 마세요]]></title>
            <link>https://velog.io/@dkdk_0422/Kotlin-%EB%8D%94-%EC%9D%B4%EC%83%81-Stack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@dkdk_0422/Kotlin-%EB%8D%94-%EC%9D%B4%EC%83%81-Stack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94</guid>
            <pubDate>Wed, 21 May 2025 10:09:40 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요~ 
최근 코딩테스트 공부를 다시 시작하며 Stack에 대해 공부하고 있던 도중 Java 공식문서에서 스택이 필요할 때 <code>ArrayDeque</code> 를 구현체로 한 <code>Deque</code> 인터페이스를 사용할 것을 권고하고 있는 내용을 확인했습니다.</p>
<p>이에 대해 자세히 알아보겠습니다.</p>
<h2 id="java와-kotlin">Java와 Kotlin</h2>
<p>Java와 Kotlin은 상호호환성을 가집니다. 때문에 Java에서 제공하는 다양한 클래스, 예를 들어 ArrayList, HashMap, Stack 같은 자료구조를 Kotlin에서도 그대로 사용할 수 있습니다.</p>
<p>Java와 Kotlin이 상호호환성을 가지는 근거로 Kotlin의 컬렉션 프레임워크나 .javaClass를 통해 확인할 수 있습니다.</p>
<pre><code class="language-kotlin">public actual interface MutableList&lt;E&gt; : List&lt;E&gt;, MutableCollection&lt;E&gt;

@SinceKotlin(&quot;1.1&quot;)
@kotlin.internal.InlineOnly
public inline fun &lt;T&gt; mutableListOf(): MutableList&lt;T&gt; = ArrayList()

@SinceKotlin(&quot;1.1&quot;) public actual typealias ArrayList&lt;E&gt; = java.util.ArrayList&lt;E&gt;</code></pre>
<p>흔히 사용하는 <code>MutableList</code> 를 예시로 들어보겠습니다.
인터페이스는 Kotlin에서 만들어두고 실제 자료형은 java.util.<code>ArrayList</code>인 것을 확인할 수 있습니다.</p>
<blockquote>
<p>즉, interface만 Kotlin에 만들어두고 실제 자료형은 Java의 자료형을 사용하는 방식</p>
</blockquote>
<p>이거.. 내부 구현체를 확인하려면 Java코드를 까봐야 하네...? 왜 이런 방법을 선택했을가요?</p>
<ul>
<li>자바의 표쥰 컬렉션을 이용하면 호환성이 높습니다.</li>
<li>자바와 코틀린 간에 호출이 일어날 때 서로 변환할 필요가 없습니다.</li>
</ul>
<p>앞서 Java와 Kotlin은 상호호환성을 가진다고 했습니다.</p>
<p>범용적인 자바 표준을 이용해 호환성을 높이고, 자바와 코틀린 간에 서로 문제없이 호출하는 상호운용성을 위해 코틀린이 택한 방식입니다.</p>
<hr>
<h2 id="stack">Stack</h2>
<p>그럼 왜 Stack을 사용하지 말라는 걸까요?
<a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/Stack.java">Stack</a> 소스 코드를 통해 확인할 수 있습니다.</p>
<pre><code class="language-java">public class Stack&lt;E&gt; extends Vector&lt;E&gt; {
    public Stack() {
    }

    public E push(E item) {
        addElement(item);
        return item;
    }

    public synchronized E pop() {
       ...
        removeElementAt(len - 1);
        return obj;
    }

    public synchronized E peek() {
       ...
       return elementAt(len - 1);
    }

    public synchronized int search(Object o) {
        ...
    }
}</code></pre>
<p>여기서 확인할 수 있는 점으로 </p>
<ol>
<li>Vector를 상속받고 있습니다.</li>
<li>pop, peek, search에 synchronized를 사용하고 있습니다.</li>
</ol>
<p>그럼 Vector의 코드를 확인해보겠습니다.</p>
<pre><code class="language-java">public class Vector&lt;E&gt;
    extends AbstractList&lt;E&gt;
    implements List&lt;E&gt;, RandomAccess, Cloneable, java.io.Serializable
{
  public synchronized void copyInto(Object[] anArray) {
        System.arraycopy(elementData, 0, anArray, 0, elementCount);
    }

   public synchronized int capacity() {
        return elementData.length;
    }
    ...
    public synchronized void addElement(E obj) {
        modCount++;
        add(obj, elementData, elementCount);
    }

}</code></pre>
<p>Vector의 함수들 또한 앞에 synchronized 키워드가 사용되었습니다.
그럼 synchronized는 뭐하는 녀석일까요?</p>
<h3 id="synchronized">synchronized</h3>
<blockquote>
<p>synchronized는 자바에서 멀티스레드 환경에서 동시 접근을 막기 위한 동기화 키워드입니다.</p>
</blockquote>
<p>즉, 메서드나 블록에 이 키워드가 붙으면, 한 번에 오직 한 번에 하나의 스레드만 접근할 수 있게 <code>락</code> (Lock)을 겁니다.
해당 메서드를 여러 스레드가 동시에 호출하려고 할 때 하나의 차례로만 실행됩니다.</p>
<p>왜 synchronized를 붙였을가요?</p>
<p>Stack은 1995년에 설계된 Java 1.0의 자료구조 입니다.</p>
<p>당시 멀티스레드를 안전하게 다룰 방법이 제한적이였고, 당시 컴퓨터는 대부분 싱글코어 CPU였기 때문에 성능 저하가 치명적으로 드러나지 않았을 거라고 생각합니다. </p>
<p>정리하자면</p>
<ul>
<li>락을 걸기 때문에, 오버헤드가 발생합니다.</li>
<li>스레드 충돌이 없는데도 락을 잡고 해제함 -&gt; 낭비로 이어집니다.</li>
<li>코딩 테스트에서는 대부분 단일 스레드로 작동하기 때문에 락은 불필요한 비용입니다.</li>
</ul>
<hr>
<h2 id="arraydeque">ArrayDeque</h2>
<p>ArrayDeque는 Stack의 문제를 해결했을가요?</p>
<pre><code class="language-java">public class ArrayDeque&lt;E&gt; extends AbstractCollection&lt;E&gt;
                           implements Deque&lt;E&gt;, Cloneable, Serializable
{
    private int newCapacity(int needed, int jump) {...}

     public ArrayDeque() {
         // 초기 용량, 후 grow 함수로 더블링
        elements = new Object[16 + 1];
    }
    static final &lt;E&gt; E elementAt(Object[] es, int i) {...}
    public void addFirst(E e) {...}
    public void addLast(E e) {...}
    ...
    public E removeLast() {...}
    public void push(E e) {
        addFirst(e);
    }
    public E peek() {
        return peekFirst();
    }
    ...

}</code></pre>
<p>ArrayDeque는 Deque 인터페이스를 구현한 클래스입니다.</p>
<h3 id="deque란">Deque란?</h3>
<p>&quot;Double Ended Queue&quot;의 줄임말로 양쪽 끝에서 삽입과 삭제가 가능한 <code>큐</code>입니다.
ArrayDeque는 Deque의 모든 메서드를 배열 기반으로 구현합니다.</p>
<pre><code class="language-java">public interface Deque&lt;E&gt; extends Queue&lt;E&gt;, SequencedCollection&lt;E&gt; {
    void addFirst(E e);
    void addLast(E e);
    E removeFirst();
    E removeLast();
    void push(E e);
    E pop();
    ...
}</code></pre>
<p>ArrayDeque는 순환 배열 기반으로 구현된 양방향 큐(Deque)입니다.
양쪽에서 삽입 / 삭제가 가능하므로 한쪽 끝에서 삽입하고 삭제를 하면 <code>스택</code> 처럼 사용할 수 있습니다.
즉, <code>스택</code>, <code>큐</code> 모두로 유연하게 사용 가능</p>
<p>Stack과 달리 synchronized를 사용하지도 않습니다.
때문에 락을 잡지 않고 불필요한 성능 손실 또한 없습니다.</p>
<p>때문에 Stack을 사용해야 한다면 ArrayDeque가 가장 추천되는 자료구조 입니다.</p>
<h3 id="그럼-linkedlist는">그럼 LinkedList는?</h3>
<pre><code class="language-java">public class LinkedList&lt;E&gt;
    extends AbstractSequentialList&lt;E&gt;
    implements List&lt;E&gt;, Deque&lt;E&gt;, Cloneable, java.io.Serializable
{
}</code></pre>
<p>LinkedList 또한 Deque 인터페이스를 구현하고 있고 Stack과 Queue 용도로 사용할 수 있습니다.
기능적으로 유사하지만, 내부 구조와 성능 측면에서 차이가 있습니다.</p>
<p>ArrayDeque는 배열 기반 / LinkedList는 이중 연결 리스트 기반</p>
<p>ArrayDeque는 배열 기반이기 때문에 데이터가 메모리에 연속으로 저장됩니다. 
또한 배열 확장(더블링) 비용보다 LinkedList의 새 노드를 새로 생성하는 비용이 더 많이 들어가게 됩니다.
데이터 접근 또한 배열은 O(1)만에 가능하지만 LinkedList는 각 노드를 순회해야 합니다.</p>
<p>때문에 LinkedList 보다 ArrayDeque가 선호된다고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode 5 - Longest Palindromic Substring]]></title>
            <link>https://velog.io/@dkdk_0422/LeetCode-5-Longest-Palindromic-Substring</link>
            <guid>https://velog.io/@dkdk_0422/LeetCode-5-Longest-Palindromic-Substring</guid>
            <pubDate>Fri, 16 May 2025 08:06:03 GMT</pubDate>
            <description><![CDATA[<p><a href="https://leetcode.com/problems/longest-palindromic-substring/description/">LeetCode 5 - Longest Palindromic Substring</a></p>
<h3 id="해결방안">해결방안</h3>
<ol>
<li>len &lt; 2 면 이미 팰린드롬이기에 바로 s 반환</li>
<li>팰린드롬 문자열의 길이는 홀수, 짝수 둘 다 모두 가능함</li>
<li>팰린드롬은 확인 방법 = 중앙 값으로부터 l, r 을 설정해 점점 멀어지며 같은 문자열인지 확인</li>
<li>0부터 len 까지 반복문 → i 가 중앙점이 되며 이 중앙점으로 부터 팰린드롬 검사</li>
<li>새로 구한 팰린드롬 최대 길이 인덱스를 저장</li>
</ol>
<pre><code class="language-kotlin">class Solution {
    var left = 0
    var maxLen = 0
    fun longestPalindrome(s: String): String {
        val len = s.length
        if (len &lt; 2) return s

        for(i in 0 until len ) {
            extendPalindrome(s, i, i)
            extendPalindrome(s, i, i + 1)
        }
        return s.substring(left, left + maxLen)
    }

    private fun extendPalindrome(s: String, j: Int, k: Int) {
        var l = j
        var r = k
        while(l &gt;= 0 &amp;&amp; r &lt; s.length &amp;&amp; s[l] == s[r]) {
            l--
            r++
        }
        if(maxLen &lt; r - l -1) {
            left = l + 1
            maxLen = r - l - 1

            println(&quot;r = $r   l = $l   r - l - 1 = ${r - l - 1}   left = $left    maxLen = ${r - l - 1}&quot;)
        }
    }
}

fun main() {
    val c = Solution()
    println(c.longestPalindrome(&quot;babad&quot;))
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[leetcode 42 - 빗물 트래핑(Kotlin)]]></title>
            <link>https://velog.io/@dkdk_0422/leetcode-%EB%B9%97%EB%AC%BC-%ED%8A%B8%EB%9E%98%ED%95%91Kotlin</link>
            <guid>https://velog.io/@dkdk_0422/leetcode-%EB%B9%97%EB%AC%BC-%ED%8A%B8%EB%9E%98%ED%95%91Kotlin</guid>
            <pubDate>Fri, 16 May 2025 07:57:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://leetcode.com/problems/trapping-rain-water/description/">LeetCode.42 - 빗물 트래킹</a></p>
<h2 id="해결-방안">해결 방안</h2>
<h3 id="투포인터">투포인터</h3>
<ul>
<li>투 포인터를 지정</li>
<li>왼쪽 가장 큰벽 / 오른쪽 가장 큰벽 변수 지정</li>
<li>가장 큰벽 사이즈 - 현재 벽 사이즈 =  빈공간</li>
</ul>
<p>두개의 포인터가 이동하며 for문을 돌며 현재 인덱스의 벽 크기가 가장 큰 벽 사이즈 보다 작다면 빈공간을 의미</p>
<pre><code class="language-kotlin">class Solution {
    fun trap(height: IntArray): Int {
        var answer = 0
        var left = 0
        var right = height.size - 1
        var leftMax = height[left] // 왼쪽 제일 큰 벽
        var rightMax = height[right] // 오른쪽 제일 큰 벽

        while(left != right) {
            leftMax = leftMax.coerceAtLeast(height[left]) // 최댓값 구하기
            rightMax = rightMax.coerceAtLeast(height[right]) // 최댓값 구하기

            if(leftMax &lt;= rightMax) {
                answer += leftMax - height[left]
                left ++
            } else {
                answer += rightMax - height[right]
                right --
            }
        }
        return answer
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 컬렉션 프레임워크 빅오]]></title>
            <link>https://velog.io/@dkdk_0422/%EC%9E%90%EB%B0%94-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%B9%85%EC%98%A4</link>
            <guid>https://velog.io/@dkdk_0422/%EC%9E%90%EB%B0%94-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%B9%85%EC%98%A4</guid>
            <pubDate>Tue, 13 May 2025 08:20:26 GMT</pubDate>
            <description><![CDATA[<h2 id="리스트-시간-복잡도">리스트 시간 복잡도</h2>
<table>
<thead>
<tr>
<th>연산</th>
<th>ArrayList</th>
<th>LinkedList</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 끝에 삽입</td>
<td>O(1) 가끔(더블링) O(n)</td>
<td>O(1)</td>
</tr>
<tr>
<td>인덱스 중간에 삽입</td>
<td>O(n)</td>
<td>탐색 O(n), 삽입 O(1)</td>
</tr>
<tr>
<td>인덱스 끝에서 삭제</td>
<td>O(1)</td>
<td>O(1)</td>
</tr>
<tr>
<td>인덱스 중간에서 삭제</td>
<td>O(n)</td>
<td>탐색 O(n), 삭제 O(1)</td>
</tr>
<tr>
<td>조회</td>
<td>O(1)</td>
<td>O(n)</td>
</tr>
</tbody></table>
<h3 id="arraylist">ArrayList</h3>
<p>ArrayList의 인덱스 끝에 삽입하는 경우 O(1)이지만 더블링이 일어나는 경우 <strong>O(n)</strong>이 소요된다. 하지만 분할 상환 분석에 따른 시간 복잡도는 <strong>O(1)</strong>이다.</p>
<p>인덱스 중간에 삽입하는 시간 복잡도는 <strong>O(n)</strong>이다. 신규 엘리먼트를 포함하여 전체를 새로운 공간에 복사해야 하기 때문에 O(n)이 소요된다. </p>
<p>인덱스 끝에서 삭제는 <strong>O(1)</strong>만에 가능하지만 중간에서 삭제를 하려면 해당 엘리먼트를 제외한 나머지는 다시 복사해야 해서 <strong>O(n)</strong>이 소요된다.</p>
<p>배열의 가장 큰 장점은 어느 위치에 있든 인덱스를 지정하면 <strong>O(1)</strong>만에 조회가 가능하다는 점이다.</p>
<h3 id="linkedlist">LinkedList</h3>
<p>인덱스 끝에 삽입이 O(1)에 바로 가능하지만 인덱스 중간에 삽입하려면 해당 위치까지 거슬러 내려가야 한다. 그래서 탐색에 <strong>O(n)</strong>이 필요하다. 하지만 삽입 자체는 간단히 노드를 연결해주기만 하면 되기 때문에 <strong>O(1)</strong>이다. 컴퓨터과학에서 이 과정을 모두 합쳐 연결 리스트에서 중간에 삽입하는 연산을 <strong>O(n)</strong>으로 지칭한다.</p>
<p>삭제 또한 마찬가지이다.</p>
<p>바로 해당 위치를 찾을 수 있는 배열과 달리 연결 리스트는 매번 탐색해야 하므로 조회 시 <strong>O(n)</strong>이다. 조회가 잦다면 연결 리스트 구현인 LinkedList를 사용하는 것은 지양한다.</p>
<hr>
<h2 id="실제-속도-비교">실제 속도 비교</h2>
<p>어느 상황에 ArrayList를 사용하고 LinkedList를 사용해야 할까? </p>
<h3 id="데이터-삽입">데이터 삽입</h3>
<p>ArrayList, LinkedList는 엘리먼터를 삽입하는 방식이 O(1)이 걸린다. 그러나 ArrayList는 배열 크기를 미리 지정하지 않는 경우 가끔씩 더블링이 일어나 O(n)이 되기 때문에 훨씬 더 불리할 것 같다. 그럼 LinkedList가 더 빠르지 않을까?</p>
<pre><code class="language-java">// ArrayList&lt;Integer&gt; 1억개 삽입
List&lt;Integer&gt; arrayList = new ArrayList&lt;&gt;();
for(int i = 0; i &lt; 100000000; i++)
    arrayList.add(1);

// LinkedList&lt;Integer&gt; 1억개 삽입
List&lt;Integer&gt; linkedList = new LinkedList&lt;&gt;();
    for(int i = 0; i &lt; 100000000; i++)
        linkedList.add(1);</code></pre>
<ul>
<li>ArrayList<Integer> 1억 개 삽입 : 2265ms</li>
<li>LinkedList<Integer> 1억 개 삽입 : 17231ms</li>
</ul>
<p>?? 같은 O(1)이고 더블링 까지 고려했을 때 ArrayList가 더 느려야 하는거 아닌가? 어덯게 8배 더 빠를 수 있을가</p>
<aside>

<p>LinkedList의 경우 메모리를 할당해야 하는 등의 훨씬 더 비싼 작업이 수행되기 때문</p>
</aside>

<p>LinkedList의 경우 별도의 Node를 선언하고 연결하는 작업이 진행된다. 여기서 Node 선언처럼 객체를 생성하는 작업은 매우 비싼 작업이기 때문에 같은 시간복잡도라도 훨씬 더 오래 걸린다.</p>
<h3 id="데이터-중간-삭제">데이터 중간 삭제</h3>
<p>인덱스 중간 즈음에 엘리먼터를 추가한다면 ArrayList나 LinkedList는 모두 동일한 시간 복잡도로 O(n)이다. 그렇다면 1억 개 중 100만 번째 인덱스를 100개 삭제하는 실행 속도는 서로 비슷할가?</p>
<pre><code class="language-java">// ArrayList&lt;Integer&gt; 1000000번째 인덱스 100개 삭제
for(int i = 0; i &lt; 100; i++)
    arrayList.remove(1000000);

// LinkedList&lt;Integer&gt; 1000000번째 인덱스 100개 삭제
for(int i = 0; i &lt; 100; i++)
    linkedList.remove(1000000);</code></pre>
<ul>
<li>ArrayList<Integer> : 9912ms</li>
<li>LinkedList<Integer> : 556ms</li>
</ul>
<p>동일한 시간 복잡도임에도 불구하고 20배 가깝게 차이가 난다.</p>
<p>새로운 공간에 나머지 엘리먼트를 모두 복사하는 ArrayList는 꽤 비싼 작업을 O(n)으로 실행해야 하는 데 반해 LinkedList는 상대적으로 부담이 적은 탐색에만 O(n)이 소요되기 때문이며, 삭제 자체는 해당 노드만 제거하면 되므로 O(1)에 가능하다.</p>
<p>정리하자면 비슷한 시간 복잡도라도 ArrayList는 인덱스 끝에서의 삽입과 조회가 빠르고, LinkedList는 인덱스 중간에서의 삽입과 삭제가 빠르다.</p>
<hr>
<h2 id="맵-시간-복잡도">맵 시간 복잡도</h2>
<p>HashMap은 추가, 삭제, 조회 모두 O(1)에 가능하다.</p>
<p>LinkedHashMap은 입력 순서를 보장한다. HashMap과 동일한 O(1)이지만 행여 시간 복잡도가 동일해도 실제 속도에서 차이가 날가 걱정할 수 있다. 아무래도 연결 리스트를 추가로 활용하는 특성상 추가 속도는 LinkedHashMap이 살짝 더 느리지만 30%내외로 우려할 정도는 아니다. </p>
<p>LinkedHashMap이 오히려 조회 속도는 살짝 더 빠르며, 이 역시도 10% 내외로 거의 차이가 나지 않기 때문에 성능이 동일한 자료형으로 봐도 무방함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lifecycle-Aware하게 이벤트 처리하기]]></title>
            <link>https://velog.io/@dkdk_0422/Lifecycle-Aware%ED%95%98%EA%B2%8C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/Lifecycle-Aware%ED%95%98%EA%B2%8C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 24 Apr 2025 11:16:35 GMT</pubDate>
            <description><![CDATA[<p>다들 개발하시며 이벤트 처리를 어떤 방법으로 사용하시고 계신가요?</p>
<p>저는 아래의 7가지 방법에서 4번 방법을 사용하다 최근 <code>repeatOnLifecycle</code> 개념에 대해 알게되어 5번 방법을 사용하고 있었습니다. 7가지 방법에 대해 궁금하시다면 <a href="https://medium.com/@gun0912?source=post_page---byline--31bb183a88ce---------------------------------------">Ted Park</a> 님의 블로그를 확인해 보시기 바랍니다.</p>
<p><a href="https://medium.com/prnd/mvvm%EC%9D%98-viewmodel%EC%97%90%EC%84%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-6%EA%B0%80%EC%A7%80-31bb183a88ce">MVVM의 ViewModel에서 이벤트를 처리하는 방법 7가지</a></p>
<p>내가 최근 사용한 이벤트 처리</p>
<pre><code class="language-kotlin">// XML
 lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.eventFlow.collect { event -&gt; handleEvent(event) }
            }
        }

// Compose
 val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            uiEvent.collect { event -&gt;
                    handleEvent(event)
            }
        }
    }</code></pre>
<p>하지만 SharedFlow를 통해 Event 처리 시 문제가 발생할 수 있다는 사실을 Ted Park님의 글을 보고 알게되었습니다.</p>
<p>만약 사용자가 리스트에서 item 하나를 클릭했고 서버로부터 상태를 체크하고 화면을 보여주는 로직이 있다고 가정했을 때 아직 서버로부터 상태 체크가 끝나지 않았는데 사용자가 Home 버튼을 눌러 앱이 백그라운드로 내려갔다면 상세 화면을 실행하는 이벤트를 emit 해도 onStop 상태에 있기 때문에 이벤트를 받지 못합니다.</p>
<p>그럼 SharedFlow의 reply 쓰면 되는거 아닌가? 할 수 있지만</p>
<p>reply를 설정하게 되면 Configuration Changer가 일어났을 때 이전 이벤트를 받게되어 의도하지 않은 동작을 일으키게 됩니다. (화면 전환 → 토스테 메시지 나옴)</p>
<p>이를 방지하기 위해 <strong><code>EventFlow</code></strong> 를 직접 작성하여 reply로 이전 이벤트들을 가지고 있다가 새로운 구독자가 생겼을 때 
<code>consumed</code> (소비) 소비되지 않은 이벤트라면 값을 emit 하고 이미 소비된 이벤트라면 값을 방출하지 않습니다.</p>
<p>EventFlow에 대한 자세한 내용은 </p>
<p>위의 Ted Park 님의 포스팅에서 자세한 내용을 확인하실 수 있습니다.</p>
<pre><code class="language-kotlin">interface EventFlow&lt;out T&gt; : Flow&lt;T&gt; {
    companion object {
        const val DEFAULT_REPLAY: Int = 3
    }
}

interface MutableEventFlow&lt;T&gt; : EventFlow&lt;T&gt;, FlowCollector&lt;T&gt;

@Suppress(&quot;FunctionName&quot;)
fun &lt;T&gt; MutableEventFlow(
    replay: Int = EventFlow.DEFAULT_REPLAY
): MutableEventFlow&lt;T&gt; = EventFlowImpl(replay)

fun &lt;T&gt; MutableEventFlow&lt;T&gt;.asEventFlow(): EventFlow&lt;T&gt; = ReadOnlyEventFlow(this)

private class ReadOnlyEventFlow&lt;T&gt;(flow: EventFlow&lt;T&gt;) : EventFlow&lt;T&gt; by flow

private class EventFlowImpl&lt;T&gt;(
    replay: Int
) : MutableEventFlow&lt;T&gt; {

    private val flow: MutableSharedFlow&lt;EventFlowSlot&lt;T&gt;&gt; = MutableSharedFlow(replay = replay)

    @InternalCoroutinesApi
    override suspend fun collect(collector: FlowCollector&lt;T&gt;) = flow
        .collect { slot -&gt;
            if (!slot.markConsumed()) {
                collector.emit(slot.value)
            }
        }

    override suspend fun emit(value: T) {
        flow.emit(EventFlowSlot(value))
    }
}

private class EventFlowSlot&lt;T&gt;(val value: T) {

    private val consumed: AtomicBoolean = AtomicBoolean(false)

    fun markConsumed(): Boolean = consumed.getAndSet(true)
}</code></pre>
<h2 id="channel을-이용한-이벤트-처리">Channel을 이용한 이벤트 처리</h2>
<p><code>EventFlow</code> 에 감탄하고 있던 도중 Ted Park님의 블로그에서 </p>
<p><a href="https://medium.com/prnd/viewmodel%EC%97%90%EC%84%9C-%EB%8D%94%EC%9D%B4%EC%83%81-eventflow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94-3974e8ddffed">ViewModel에서 더이상 EventFlow를 사용하지 마세요</a></p>
<p>이러한 내용의 글을 읽게 되었습니다. 또 감탄…</p>
<h3 id="뷰모델에서-이벤트를-전파할때-필요한-조건">뷰모델에서 이벤트를 전파할때 필요한 조건</h3>
<ol>
<li>구독자가 없을 때 이벤트가 발생했어도, 다시 구독자가 생기면 해당 이벤트가 전달되어야 합니다. 위에서 든 예시로 사용자가 이벤트를 받기 전 홈버튼을 누르고 다시 돌아왔을 때 해당 이벤트를 전달 받을 수 있어야 합니다.</li>
<li>이벤트가 소비되면(처리) 다시 처리되지 않아야 합니다. Configufation Change가 일어나도 이전에 소비된 이벤트는 더 이상 받을 필요가 없습니다.</li>
</ol>
<p>이러한 규칙을 <code>EventFlow</code> 도 잘 지키지만 <code>Channel</code> 또한 이 요건을 다 충족합니다.</p>
<p>channel은 비동기 데이터 스트림이라고 생각하시면 됩니다.</p>
<p>자세한 내용은 <a href="https://medium.com/prnd/viewmodel%EC%97%90%EC%84%9C-%EB%8D%94%EC%9D%B4%EC%83%81-eventflow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94-3974e8ddffed">위 블로그 링크</a>를 참고해주세요.</p>
<p>Channel 사용시 직접 EventFlow를 구현하지 않아도 우리가 원하는 이벤트 처리가 가능합니다.</p>
<p>Channel의 <strong><code>*receiveAsFlow</code></strong> 로* Flow로 만들 수 있기 때문에 뷰에서는 바뀔 코드가 없고 뷰모델의 코드만 수정하면 됩니다.</p>
<pre><code class="language-kotlin">  // viewModel
  private val _uiEvent = Channel&lt;SignUpUiEvent&gt;()
  val uiEvent = _uiEvent.receiveAsFlow()</code></pre>
<p>Ted Park님이 제공해주신 샘플코드에서 </p>
<pre><code class="language-kotlin">fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -&gt; Unit) {
    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
    }
}</code></pre>
<p>LifecycleOwner의 확장함수를 작성해서 계속해서 lifecycleScope.launch 안에 repeatOnLifecycle을 사용하지 않도록 사용하신 모습을 볼 수 있습니다. 중복 코드를 없애고 액티비티에서도 간편하게 사용할 수 있습니다.</p>
<pre><code class="language-kotlin"> // Activity
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        repeatOnStarted {
            viewModel.eventFlow.collect { event -&gt; handleEvent(event) }
        }
    }</code></pre>
<h3 id="compose-에서는">Compose 에서는?</h3>
<p>Compose에서도 항상 LaunchedEffect를 작성하고 lifecycleOwner.lifecycle.repeatOnLifecycle 을 계속 작성해야 하는 번거로움이 있다고 생각해 확장 함수를 작성해봤습니다.</p>
<pre><code class="language-kotlin">@Composable
fun &lt;T&gt; Flow&lt;T&gt;.collectOnStarted(
    action: suspend (T) -&gt; Unit
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(this, lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            Log.d(&quot;GuestDetailViewModel&quot;, &quot;collectOnStarted&quot;)
            collect { action(it) }
        }
    }
}</code></pre>
<p><code>EventFlow</code> , <code>Channel.receiveAsFlow()</code> 모두 Flow 타입을 만족하기 때문에 Flow<T>에 대한 확장 함수를 작성했습니다.</p>
<p>lifecycleOwner값을 가져오고 repeatOnLifecycle을 사용해 Lifecycle의 상태가 STARTED 상태일 때만 이벤트를 수집하고 ONSTOP 상태일 때는 수집되지 않게 합니다.</p>
<p>확장 함수를 사용한 뷰 코드입니다.</p>
<pre><code class="language-kotlin">@Composable
fun MyScreen(
    viewModel: TestViewModel = hiltViewModel()
) {
    viewModel.uiEvent.collectOnStarted { event-&gt;
        when(event) {...}
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin Channel 기본 개념 잡기]]></title>
            <link>https://velog.io/@dkdk_0422/Kotlin-Channel-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-%EC%9E%A1%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/Kotlin-Channel-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-%EC%9E%A1%EA%B8%B0</guid>
            <pubDate>Thu, 24 Apr 2025 08:14:22 GMT</pubDate>
            <description><![CDATA[<p>Kotlin <code>Channel</code> 의 기본 개념에 대해 알아보려고 합니다~</p>
<h1 id="channel">Channel</h1>
<blockquote>
<p>Kotlin Channel은 코루틴 간 통신을 위한 <strong>동시성 프리미티브로</strong>, 데이터를 안전하게 전송하고 공유할 수 있는 메커니즘을 제공합니다.</p>
</blockquote>
<p>즉 비동기 데이터 스트림이라고 생각할 수 있습니다.</p>
<p>한쪽에서 데이터를 보내고(send), 다른 쪽에서는 데이터를 받는(receive) <strong><code>파이프라인</code></strong> 처럼 작동합니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/2a754f3a-d3b7-42ec-a352-0223b37f3e3d/image.png" alt=""></p>
<p>위처럼 한 코루틴은 생산자로서 데이터를 생성하여 채널에 보내고, 다른 코루틴은 소비자로서 채널에서 데이터를 받습니다. <strong><code>생성자 - 소비자</code></strong> 패턴을 쉽게 구현할 수 있습니다.</p>
<h2 id="기본-사용법">기본 사용법</h2>
<p>자세한 개념에 대해 알아보기 전 기본 사용 방법에 대해 알아보겠습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val channel = Channel&lt;Int&gt;()
    launch {
        for (x in 1..5) { // 1부터 5까지 반복문
            println(&quot;send ${x * x}&quot;)
            channel.send(x * x) // send 시 채널에 데이터 전송
        }
    }
    // receive 시 채널로부터 데이터 받음
    repeat(5) { println(&quot;Receive = ${channel.receive()}&quot;) }
    channel.close()
    println(&quot;Done!&quot;)
}</code></pre>
<ul>
<li>send 시 채널에 데이터를 전송합니다.</li>
<li>receive 시 채널로부터 데이터를 받을 수 있습니다.</li>
<li>close() 시 채널을 닫습니다. 닫힌 후에는 더 이상 데이터를 보낼 수 없지만, 이미 채널에 있는 데이터는 여전히 수신할 수 있습니다.</li>
</ul>
<pre><code class="language-kotlin">val channel = Channel&lt;Int&gt;()
launch {
    for (x in 1..5) channel.send(x * x)
    channel.close() // we&#39;re done sending
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println(&quot;Done!&quot;)</code></pre>
<p>위처럼 작성 시 채널이 닫힐 때까지 모든 값을 수신하는 코드가 됩니다.</p>
<h2 id="channel-구현부">Channel 구현부</h2>
<pre><code class="language-kotlin">public interface SendChannel&lt;in E&gt; 
public interface ReceiveChannel&lt;out E&gt;
public interface Channel&lt;E&gt; : SendChannel&lt;E&gt;, ReceiveChannel&lt;E&gt; </code></pre>
<p>채널은 <strong><code>SendChannel</code></strong> , <strong><code>ReceiveChannel</code></strong>  인터페이스가 존재하고 <strong><code>Channel</code></strong> 인터페이스가 이 두개의 인터페이스를 따르고 있습니다.</p>
<p>SendChannel과 ReceiveChannel에 대해선 나중에 알아보고 우선 채널에 대해 자세히 알아보겠습니다.</p>
<pre><code class="language-kotlin">public interface Channel&lt;E&gt; : SendChannel&lt;E&gt;, ReceiveChannel&lt;E&gt; {
    public companion object Factory {
        public const val UNLIMITED: Int = Int.MAX_VALUE
        public const val RENDEZVOUS: Int = 0
        public const val CONFLATED: Int = -1
        public const val BUFFERED: Int = -2
        internal const val OPTIONAL_CHANNEL = -3
        public const val DEFAULT_BUFFER_PROPERTY_NAME: String = &quot;kotlinx.coroutines.channels.defaultBuffer&quot;
        internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME,
            64, 1, UNLIMITED - 1
        )
    }
}

public fun &lt;E&gt; Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -&gt; Unit)? = null
): Channel&lt;E&gt; </code></pre>
<p>우선 Channel 인스턴스를 생성할 때 전달할 수 있는 파라미터에 대해 알아보겠습니다.</p>
<h3 id="capacity채널의-타입">capacity(채널의 타입)</h3>
<aside>

<blockquote>
<p><strong>채널의 버퍼 용량을 결정합니다.</strong></p>
</blockquote>
</aside>

<blockquote>
<p><code>capacity</code> 는 <strong>백프레셔 처리할 방법을 정합니다.</strong></p>
</blockquote>
<aside>

<p>백프레셔란 생산자가 소비자보다 빠르게 데이터를 발행하는 상황에 있어 Channel은 여러 버퍼링 전략을 통해 해결할 수 있도록 하는 전략을 말합니다..</p>
</aside>

<p>capacity로 여러 값들을 설정할 수 있는데 이는 위의 <code>Channel</code> 인터페이스에 정의된 Factory에서 확인할 수 있습니다. 각 채널 용량으로 사용할 수 있는 미리 정의된 상수들입니다.</p>
<ul>
<li><strong>RENDEZVOUS</strong><ul>
<li>버퍼가 없는 채널. 송신자와 수신자가 만날 때(rendezvous)만 데이터가 전송됩니다.</li>
<li><code>send</code> 호출은 상대편에서 <code>receive</code> 를 호출할 때 까지 <strong>일시 중단</strong>됩니다(그 반대도 마찬가지). 즉, sendScope에서 <code>send</code> 후 receive가 불려야 다음 코드블럭으로 넘어갈 수 있습니다.</li>
<li>생산자와 소비자가 동시에 작동해야 할 때, 각 항목이 즉시 처리되어야 할 때 적합합니다.</li>
</ul>
</li>
<li><strong>UNLIMITED</strong><ul>
<li>제한 없는 버퍼를 가진 채널. <code>send</code> 호출은 절대 일시 중단되지 않습니다.</li>
<li>메모리 사용량에 주의해야 합니다.</li>
<li>소비자가 모든 항목을 처리해야 하모 메모리가 충분할 때 적합합니다.</li>
</ul>
</li>
<li><strong>CONFLATED</strong><ul>
<li>병합 채널. 새 요소가 이전 요소를 덮어씁니다.</li>
<li>버퍼 크기는 1이며, 가장 최근 값만 유지됩니다.</li>
<li>최신 값만 중요하고 중간 값은 무시해도 될 때 사용합니다.</li>
</ul>
</li>
<li><strong>BUFFERED</strong><ul>
<li>기본값(64)을 버퍼 크기로 사용하는 채널입니다.</li>
</ul>
</li>
</ul>
<h3 id="onbufferoverflow-정책">onBufferOverflow 정책</h3>
<aside>

<blockquote>
<p>버퍼가 가득 찼을 때의 동작을 지정합니다.</p>
</blockquote>
</aside>

<p>이는 <strong><code>BufferOverFlow</code></strong> Enum Class로 이뤄져 있습니다.</p>
<pre><code class="language-kotlin">public enum class BufferOverflow {
    SUSPEND,
    DROP_OLDEST,
    DROP_LATEST
}</code></pre>
<p>각 정책에 대해 알아보겠습니다~ </p>
<ul>
<li><strong>SUSPEND</strong><ul>
<li>버퍼가 가득 차면 <code>send</code> 작업이 일시 중단됩니다. 버퍼에 공간이 생길 때까지 송신자는 대기합니다.</li>
<li>백프레셔를 제공하여 생산자가 소비자보다 빠르게 생성하지 못하도록 합니다.</li>
<li>모든 항목이 처리되어야 하고 백프레셔가 필요할 때 적합합니다.</li>
</ul>
</li>
<li><strong>DROP_OLDEST</strong><ul>
<li>버퍼가 가득 차면 가장 오래된 요소(버퍼의 맨 앞 요소)가 삭제되고 새 요소가 추가됩니다.</li>
<li>즉, 큐의 맨 앞 요소를 버리고 새 요소를 뒤에 추가합니다.</li>
<li>처리 속도가 중요하고 오래된 데이터를 포기(최신 데이터가 중요함)할 수 있는 경우 유용합니다.</li>
</ul>
</li>
<li><strong>DROP_LATEST</strong><ul>
<li>버퍼가 가득 차면 가장 최근에 추가된 요소(버퍼의 맨 뒤 요소)가 삭제되고 새 요소가 추가됩니다.</li>
<li>즉, 큐의 맨 뒤 용소를 버리고 새 요소를 추가합니다.</li>
<li>가장 최근 데이터가 아닌 이전 데이터를 유지하는 것이 중요할 때 유용합니다.</li>
</ul>
</li>
</ul>
<h3 id="onundeliveredelement">onUndeliveredElement</h3>
<aside>

<blockquote>
<p>전달되지 않은 요소를 처리하는 콜백 함수</p>
</blockquote>
</aside>

<p>다음과 같은 상황에서 호출됩니다.</p>
<ul>
<li><code>send</code> 작업이 취소되었을 때</li>
<li>채널이 닫힌 후 남아있는 요소가 있을 때</li>
<li>요소 처리 중 예외가 발생했을 때</li>
</ul>
<p>기본적으로 null이기 때문에 전달되지 않은 요소에 대한 특별한 처리는 이뤄지지 않습니다.</p>
<p>이 콜백은 전달되지 않은 요소의 리소스를 정리하는 데 유용합니다.</p>
<p>채널이 I/O fㅣ소스나 네트워크 연결과 같은 자원을 포함한 객체를 전송하는 경우 이러한 리소스를 적절히 해제할 수 있습니다.</p>
<h2 id="언제-사용할까">언제 사용할까?</h2>
<p><code>Channel</code> 이 무슨 역활을 하는지, 파라미터에 어떤 것이 있고 동작이 어떻게 달라지는 지 가볍게 알아봤습니다.</p>
<p>그럼 이러한 특성을 고려했을 때 Channel은 언제, 어떤 상황에 사용했을 때 유용할까요?</p>
<ul>
<li>코루틴 간의 통신이 필요할 때</li>
<li>백프레셔 관리가 필요할 때</li>
<li>비동기 작업 간 흐름 제어</li>
</ul>
<p>등 이 있지만 안드로이드에서 <strong><code>이벤트 처리</code></strong> 에 매우 유용하다고 생각합니다.</p>
<p>이벤트 처리의 특성으로 </p>
<ol>
<li>이벤트를 받아줄 수 있는 구독자가 생길 때 까지 없어지지 않음</li>
<li>이벤트가 처리되면 해당 이벤트는 사라짐</li>
</ol>
<p>이 있습니다. 자세한 내용으로는 (<a href="https://medium.com/prnd/viewmodel%EC%97%90%EC%84%9C-%EB%8D%94%EC%9D%B4%EC%83%81-eventflow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94-3974e8ddffed">https://medium.com/prnd/viewmodel에서-더이상-eventflow를-사용하지-마세요-3974e8ddffed</a> 의 블로그를 참조하시면 이벤트 처리에 대한 매우 좋은 내용을 보실 수 있습니다.</p>
<p>이 때 <strong>BUFFERED을</strong> 사용한 채널을 사용하면 네트워크 도중 홈버튼을 눌렀을 때 이벤트가 도착해서 이를 처리할 수 있고 처리되었다면 사라지기 때문에 Configuration Change가 일어나도 같은 이벤트가 발생되지 않습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android WindowSize로 반응형 / 적응형 UI 만들기]]></title>
            <link>https://velog.io/@dkdk_0422/Android-WindowSize%EB%A1%9C-%EB%B0%98%EC%9D%91%ED%98%95-%EC%A0%81%EC%9D%91%ED%98%95-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/Android-WindowSize%EB%A1%9C-%EB%B0%98%EC%9D%91%ED%98%95-%EC%A0%81%EC%9D%91%ED%98%95-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 15 Apr 2025 12:35:28 GMT</pubDate>
            <description><![CDATA[<p>오늘은 <code>WindowSize</code> 란 것을 이용하여 기기마다 기기의 화면 크기에 따라 다른 UI를 제공하는 방법을 알아보겠습니다.</p>
<h1 id="windowsizeclass">WindowSizeClass</h1>
<aside>

<p>WindowSizeClass는 앱이 실행되는 창의 가용 공간을 너비와 높이 기준으로 세 가지 크기 범주로 분류합니다.</p>
</aside>

<ul>
<li>Compact: 좁은 화면 (예: 대부분의 스마트폰)</li>
<li>Medium: 중간 크기 화면 (예: 일부 태블릿, 폴더블 디바이스의 내부 화면)</li>
<li>Expanded: 넓은 화면 (예: 대부분의 태블릿, 데스크톱 환경)</li>
</ul>
<h2 id="분류-기준">분류 기준</h2>
<h3 id="너비">너비</h3>
<ul>
<li><strong>Compact</strong>: 너비 &lt; <code>600dp</code></li>
<li><strong>Medium</strong>: <code>600dp</code> ≤ 너비 &lt; <code>840dp</code></li>
<li><strong>Expanded</strong>: 너비 ≥ <code>840dp</code></li>
</ul>
<h3 id="높이">높이</h3>
<ul>
<li>Compact: 높이 &lt; <code>480dp</code></li>
<li><strong>Medium</strong>: <code>480dp</code> ≤ 높이 &lt; <code>900dp</code></li>
<li>Expanded: 높이 ≥ <code>900dp</code></li>
</ul>
<hr>
<h2 id="왜-분류해야-하는데">왜 분류해야 하는데?</h2>
<p><code>WindowSizeClass</code> 를 사용하여 분류하면 앱이 다양한 화면 크기와 형태에 적용할 수 있는 반응형 UI를 구현할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/216b9b97-ee28-4ff8-ab97-48b70b6f4eeb/image.png" alt=""></p>
<p>이런 식으로 말이죠.</p>
<p>유저 입장에서는 훨씬 친화적이겠죠?</p>
<p>만약 테블릿에서도 스마트폰 UI를 사용하게 된다면 엄청 불편할 겁니다…</p>
<p>즉, <code>WindowSizeClass</code> 를 활용하여 사용자에게 일관되고 최적화된 경험을 제공할 수 있습니다.</p>
<p>그럼 바로 Compose에서 어떻게 사용할 수 있는지 보겠습니다.</p>
<hr>
<h2 id="활용">활용</h2>
<pre><code class="language-kotlin">androidx-windowSize = {group = &quot;androidx.compose.material3&quot;, name = &quot;material3-window-size-class&quot;}</code></pre>
<p>우선 의존성을 추가해줘야 <code>WindowSizeClass</code> 를 사용할 수 있습니다.</p>
<p>그리고 위 사진처럼 기기의 가로 화면에 따라 다른 UI를 제공해보겠습니다.</p>
<p>우선 기기의 사이즈정보를 가져올 수 있는 <code>WindowSizeClass</code> 를 가져오겠습니다.</p>
<pre><code class="language-kotlin">val windowSize = calculateWindowSizeClass(this)

@ExperimentalMaterial3WindowSizeClassApi
@Composable
fun calculateWindowSizeClass(activity: Activity): WindowSizeClass {
    LocalConfiguration.current
    val density = LocalDensity.current
    val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
    val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
    return WindowSizeClass.calculateFromSize(size)
}</code></pre>
<p><code>WindowSizeClass</code> 에서 제공하는 <code>calculateWindowSizeClass</code> 를 사용하여 정보를 가져올 수 있습니다.</p>
<p>WindowSizeClass가 어떻게 작성되었을까요?</p>
<pre><code class="language-kotlin">@Immutable
class WindowSizeClass private constructor(
    val widthSizeClass: WindowWidthSizeClass,
    val heightSizeClass: WindowHeightSizeClass
) {
    value class WindowWidthSizeClass private constructor(private val value: Int) :
    Comparable&lt;WindowWidthSizeClass&gt; {
        companion object {
        val Compact = WindowWidthSizeClass(0)
        val Medium = WindowWidthSizeClass(1)
        val Expanded = WindowWidthSizeClass(2)
    }

    value class WindowHeightSizeClass private constructor(private val value: Int) :
    Comparable&lt;WindowHeightSizeClass&gt; {
         companion object {
        val Compact = WindowHeightSizeClass(0)
        val Medium = WindowHeightSizeClass(1)
        val Expanded = WindowHeightSizeClass(2)
    }
}</code></pre>
<p><code>WindowSizeClass</code> 안에 value class로 Width와 Height 사이즈 클래스가 있고 그 안에 값에 따라 나뉘는 <code>Compact</code> , <code>Medium</code> , <code>Expanded</code> 로 나뉘고 있는 모습을 확인할 수 있습니다.</p>
<p>바로 이를 활용하여 기기 사이즈마다 다른 UI를 제공할 수 있습니다.</p>
<pre><code class="language-kotlin">when (windowSize) {
        WindowWidthSizeClass.Compact -&gt; {
            Scaffold(
                bottomBar = { BottomNavigationBar(navController = navController) }
            ) { innerPadding -&gt;
                Box(modifier = Modifier.padding(innerPadding))
                content(innerPadding)
            }
        }
        WindowWidthSizeClass.Medium -&gt; {
            Scaffold { innerPadding -&gt;
                Row(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
                    NavigationRailContent(navController = navController)
                    Box(modifier = Modifier.fillMaxSize().padding(start = 16.dp)) {
                        content(PaddingValues())
                    }
                }
            }
        }
        WindowWidthSizeClass.Expanded -&gt; {
            PermanentNavigationDrawer(
                drawerContent = {
                    PermanentDrawerSheet(
                        modifier = Modifier.width(240.dp),
                        drawerContainerColor = MaterialTheme.colorScheme.inverseOnSurface
                    ) { // PermanentDrawerSheet Scope
                        NavigationDrawerContent(navController = navController)
                    }
                }
            ) {
                Box(modifier = Modifier.fillMaxSize()) {
                    content(PaddingValues())
                }
            }
        }
    }</code></pre>
<p>이런식으로 저는 <code>WindowWidthSizeClass</code> 의 값에 따라 다른 UI를 제공했습니다.</p>
<ul>
<li><p><strong><code>Compact</code></strong></p>
<p>  가장 기본적인 스마트폰 형태이기 때문에 바텀 네비게이션 바를 설정했습니다.</p>
</li>
<li><p><strong><code>Medium</code></strong></p>
<p>  일부 태블릿, 폴더블 휴대폰이기 때문에 <code>NavigationRail</code> 을 사용했습니다.</p>
</li>
<li><p><strong><code>Expanded</code></strong></p>
<p>  태블릿, 윈도우기 때문에 <code>PermanentNavigationDrawer</code> 를 사용했습니다.</p>
</li>
</ul>
<p><a href="https://m3.material.io/components/navigation-rail/overview"><strong>NavigationRail</strong></a>과 <a href="https://developer.android.com/develop/ui/compose/layouts/adaptive/list-detail?hl=ko"><strong>Drawer</strong></a>에 대한 자세한 내용은 링크에 들어가시면 자세한 내용을 확인하실 수 있습니다.</p>
<hr>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://m3.material.io/components/navigation-rail/overview">https://m3.material.io/components/navigation-rail/overview</a></p>
<p><a href="https://developer.android.com/develop/ui/compose/layouts/adaptive/list-detail?hl=ko">https://developer.android.com/develop/ui/compose/layouts/adaptive/list-detail?hl=ko</a></p>
<p><a href="https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes">https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CoroutineContext에 관하여]]></title>
            <link>https://velog.io/@dkdk_0422/CoroutineContext%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@dkdk_0422/CoroutineContext%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sat, 12 Apr 2025 11:07:49 GMT</pubDate>
            <description><![CDATA[<p>해당 내용은 <a href="https://product.kyobobook.co.kr/detail/S000212376884?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=googleSearch&amp;gad_source=1">코틀린 코루틴의 정석</a> 6 장을 토대로 공부한 내용입니다.</p>
<p>그동안 코루틴을 <code>launch</code> 나 <code>async</code> 로 생성하면서 우리는 context 인자에 <code>CoroutineDispatcher</code> 나 <code>CoroutineName</code> 을 전달해 주었다.</p>
<p>전달해 줌으로서 코루틴의 이름을 설정하고 어느 스레드에 보낼지를 결정할 수 있었다.</p>
<p>그렇다면 <strong><code>CoroutineContext</code></strong> 가 뭐길래 이를 가능하게 해주는걸까?</p>
<h1 id="coroutinecontext">CoroutineContext</h1>
<aside>

<p>코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스</p>
</aside>

<p>즉, 코루틴의 실행 환경, 정보가 담겨있는 인터페이스다.</p>
<h2 id="coroutinecontext-구성요소">CoroutineContext 구성요소</h2>
<p><code>CoroutineDispatcher</code> , <code>CoroutineName</code> , <code>Job</code> , <code>exceptionHandler</code> 로 구성된다.</p>
<ul>
<li><code>CoroutineName</code>: 코루틴의 이름을 설정한다</li>
<li><code>CoroutineDispatcher</code>: 코루틴을 스레드에 할당해 실행한다.</li>
<li><code>Job</code>: 코루틴의 추상체로 코루틴을 조작하는 데 사용된다 (추적 및 제어)</li>
<li><code>CoroutineExceptionHandler</code>: 코루틴에서 발생한 예외를 처리한다</li>
</ul>
<hr>
<h2 id="coroutinecontext-구성하기">CoroutineContext 구성하기</h2>
<p>CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리한다</p>
<table>
<thead>
<tr>
<th>키</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>CoroutineName 키</td>
<td>CoroutineName 객체</td>
</tr>
<tr>
<td>CoroutineDispatcher 키</td>
<td>CoroutineDispatcher 객체</td>
</tr>
<tr>
<td>Job 키</td>
<td>Job 객체</td>
</tr>
<tr>
<td>CoroutineExceptionHandler 키</td>
<td>CoroutineExceptionHandler 객체</td>
</tr>
</tbody></table>
<p>각 구성요소는 고유한 키를 가지며, 키에 대한 <strong><code>중복된 값은 허용되지 않습니다</code></strong> .</p>
<p>따라서 각 개체마다 한개씩만 가질 수 있습니다.</p>
<p>여기서 주의할 점은 <code>키-값</code> 을 사용한다고 해서 [key] = 객체 로 접근하는 방식이 아닌 <code>더하기 연산자(+)</code> 를 사용해 CoroutineContext 객체를 구성합니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    launch(context = CoroutineName(&quot;DK&quot;) + Dispatchers.IO) { 
        // 비동기 작업 실행
    }
}</code></pre>
<p>바로 이런식으로 사용합니다.</p>
<p>설정되지 않은 <code>Job</code> , <code>CoroutineExceptionHandler</code> 구성 요소는 설정되지 않은 상태로 유지 됩니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val myContext = CoroutineName(&quot;MyCoroutine&quot;) + Dispatchers.IO
    launch(context = myContext) {
                // 비동기 코드...
    }
}</code></pre>
<p>이런식으로도 사용이 가능합니다.</p>
<h3 id="coroutinecontext-구성-요소-덮어씌우기">CoroutineContext 구성 요소 덮어씌우기</h3>
<p>만약 <code>CoroutineContext</code> 객체에 같은 구성 요소가 둘 이상 더해진다면 나중에 추가된 구성 요소가 이전의 값을 덮어 씌우게 됩니다. 예시를 통해 확인해보겠습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val myContext = CoroutineName(&quot;OldCoroutine&quot;) + Dispatchers.IO
    launch(context = myContext + CoroutineName(&quot;NewCoroutine&quot;)) {
        println(&quot;Coroutine Name : ${coroutineContext[CoroutineName]}&quot;)
    }
}
Coroutine Name : CoroutineName(NewCoroutine)</code></pre>
<p><code>launch</code> 에 CoroutineName을 새 인자로 추가되었기 때문에 원래 있던 이름을 덮어 씌우게 됩니다.</p>
<p>즉, 나중에 들어온 값이 덮어씌웁니다!!.</p>
<h3 id="coroutinecontext에-job-생성해-추가하기">CoroutineContext에 Job 생성해 추가하기</h3>
<p><code>Job</code> 객체는 기본적으로 launch나 runBlocking 같은 코루틴 빌더 함수를 통해 자동으로 생성되지만 <code>Job()</code> 을 호출해 생성할 수도 있습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = Job()
    val coroutineContext = CoroutineName(&quot;coroutine1&quot;) + Dispatchers.Default + job
}</code></pre>
<p>하지만 <code>Job</code> 객체를 직접 생성해 추가하면 <code>코루틴의 구조화</code> 가 깨지게 됩니다.</p>
<p>때문에 새로운 Job 객체를 생성해 CoroutineContext에 추가하는 것은 <code>주의 필요</code>  합니다.</p>
<p>이에 대해서는 추후 <strong><code>구조화된 동시성</code></strong> 에서 다루겠습니다.</p>
<hr>
<h2 id="coroutinecontext-구성-요소에-접근하기">CoroutineContext 구성 요소에 접근하기</h2>
<p>CoroutineContext를 설정했으니 이번에는 각 구성 요소에 어떻게 접근하는지 알아보겠습니다.</p>
<p>각 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요합니다.</p>
<p>우선 각 구성 요소의 키를 얻는 방법부터 알아보겠습니다.</p>
<h3 id="coroutinecontext-구성-요소의-키">CoroutineContext 구성 요소의 키</h3>
<p>구성 요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들 수 있는데 일반적으로 CoroutineContext 구성 요소는 자신의 내부에 키를 싱글톤 객체로 구현합니다.</p>
<p>직접 확인해볼가요?</p>
<pre><code class="language-kotlin">public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key&lt;CoroutineName&gt;
    ...

public interface Job : CoroutineContext.Element {
  /**
   * Key for [Job] instance in the coroutine context.
  */
  public companion object Key : CoroutineContext.Key&lt;Job&gt;</code></pre>
<p>Key를 <strong><code>companion object</code></strong> 로 설정했습니다.</p>
<p><code>Job</code> 또한 마찬가지로 인터페이스 내부에 Key가 동반 객체로 선언된 것을 볼 수 있습니다.</p>
<p>이는 <code>Dispatcher</code> , <code>ExceptionHandler</code> 또한 마찬가지 입니다.</p>
<h3 id="키를-사용해-coroutinecontext-구성-요소에-접근하기">키를 사용해 CoroutineContext 구성 요소에 접근하기</h3>
<p>이번에는 해당 키를 사용해 CoroutineContext 구성 요소에 접근해보겠습니다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking&lt;Unit&gt; {
    val myCoroutineContext = CoroutineName(&quot;MyCoroutine&quot;) + Dispatchers.IO
    val myCoroutineName = myCoroutineContext[CoroutineName.Key]
    val myCoroutineDispatcher = myCoroutineContext[CoroutineDispatcher.Key]
    println(&quot;CoroutineName : $myCoroutineName\nCoroutineDispatcher : $myCoroutineDispatcher&quot;)
}
// CoroutineName : CoroutineName(MyCoroutine)
// CoroutineDispatcher : Dispatchers.IO</code></pre>
<p>이렇게 <code>CoroutineName</code> 과 <code>Dispatcher</code> 을 가져와 봤습니다.</p>
<p>이런 경우 <code>.Key</code> 를 붙이지 않아도 동일한 결과를 얻을 수 있습니다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking&lt;Unit&gt; {
    val myCoroutineContext = CoroutineName(&quot;MyCoroutine&quot;) + Dispatchers.IO
    val myCoroutineName = myCoroutineContext[CoroutineName]
    val myCoroutineDispatcher = myCoroutineContext[CoroutineDispatcher]
    println(&quot;CoroutineName : $myCoroutineName\nCoroutineDispatcher : $myCoroutineDispatcher&quot;)
}
// CoroutineName : CoroutineName(MyCoroutine)
// CoroutineDispatcher : Dispatchers.IO</code></pre>
<p>키가 들어갈 자리에 CoroutineName을 사용하면 자동으로 CoroutineName.Key를 사용해 연산을 처리하기 때문에 이러한 결과를 얻을 수 있습니다.</p>
<h3 id="구성-요소의-key-프로퍼티를-사용해-구성-요소에-접근하기">구성 요소의 key 프로퍼티를 사용해 구성 요소에 접근하기</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO
    val coroutineContext = coroutineName + dispatcher
    println(coroutineContext[coroutineName.key])
    println(coroutineContext[dispatcher.key])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO</code></pre>
<p>이런 식으로 구성요소들의 key 프로퍼티를 통해서도 구성 요소에 접근할 수 있습니다.</p>
<p>여기서 중요한점은 구성 요소의 key 프로퍼티는 동반 객체로 선언된 Key와 동일한 객체를 가리킵니다.</p>
<p>예를 들어 <code>CoroutineName.Key</code>와 <code>coroutineName.key</code>는 서로 같은 객체를 참조합니다.</p>
<p>코드를 통해 확인해보겠습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO

    println(coroutineName.key === CoroutineName.Key)
}
// true</code></pre>
<p>이를 통해 서로 동일한 객체를 가리키는 것을 알 수 있습니다.</p>
<hr>
<h2 id="coroutinecontext-구성-요소-제거하기">CoroutineContext 구성 요소 제거하기</h2>
<p>위 내용에서 구성요소를 <code>플러스(+)</code> 연산자를 통해 구성 요소를 추가했씁니다.</p>
<p>이번에는 구성 요소를 제거하는 방법에 대해 알아보겠습니다.</p>
<h3 id="minuskey-사용해-구성-요소-제거하기">minusKey 사용해 구성 요소 제거하기</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO
    val myCoroutineContext = coroutineName + dispatcher

    //myCoroutineContext의 CoroutineName 제거해보기
    val deletedCoroutineContext = myCoroutineContext.minusKey(CoroutineName.Key)
    println(deletedCoroutineContext[CoroutineName])
    println(deletedCoroutineContext[CoroutineDispatcher])
}
// null
// Dispatchers.IO</code></pre>
<p>위 코드의 경우 myCoroutineContext에서 CoroutineName만 제거돼 반환되며, 반환된 CoroutineContext는 deletedCoroutineContext에 할당됩니다.</p>
<p>따라서 deletedCoroutineContext는 <code>CoroutineDispatcher</code> 만 출력되게 됩니다.</p>
<p><code>CoroutineName</code> 은 <code>minusKey</code> 로 삭제했기 때문에 null이 출력된 모습을 볼 수 있습니다.</p>
<h3 id="minuskey-함수-사용-시-주의할-점">minusKey 함수 사용 시 주의할 점</h3>
<p><code>minusKey</code> 함수 사용 시 주의할 점은 minusKey를 호출한 CoroutineContext 객체는 그대로 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다는 점입니다.</p>
<p>바로 확인해볼가요?</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO
    val myCoroutineContext = coroutineName + dispatcher

    //myCoroutineContext의 CoroutineName 제거해보기
    val deletedCoroutineContext = myCoroutineContext.minusKey(CoroutineName.Key)
    println(myCoroutineContext[CoroutineName])
    println(myCoroutineContext[CoroutineDispatcher])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO</code></pre>
<p>결과를 보시면 <code>minusKey</code> 가 호출된 myCoroutineContext는 구성 요소가 제거되지 않은 모습을 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[okhttp Authenticator가 발생하는 과정]]></title>
            <link>https://velog.io/@dkdk_0422/okhttp-Authenticator%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@dkdk_0422/okhttp-Authenticator%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 11 Apr 2025 12:23:16 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dkdk_0422/okhttp%EC%8B%AC%ED%99%94-%EB%82%B4%EB%B6%80-Intercepter-github-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0">이전 포스팅에서</a> okhttp가 가지고 있는 <code>interceptor</code> 에 대해 알아보았습니다.</p>
<p>그 중  <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">RetryAndFollowUpInterceptor</a> 가 서버로부터 401 response를 받을 시 <code>OkHttpClient</code> 의 <code>Authenticator</code> 의 <code>authenticate</code> 를 호출한다고 언급했습니다.</p>
<p>이번 포스팅에서는 어떻게 <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">RetryAndFollowUpInterceptor</a> 이 친구가 이렇게 동작할 수 있는지에 대해 알아보자 합니다.</p>
<p><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">RetryAndFollowUpInterceptor</a> 코드를 보시면서 보시는 걸 추천드립니다!**</p>
<h2 id="retryandfollowupinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">RetryAndFollowUpInterceptor</a></h2>
<pre><code class="language-kotlin">            while (true) {
73      try {
74        response = realChain.proceed(request)
75        newRoutePlanner = true
76      } catch (e: IOException) {
77        // An attempt to communicate with a server failed. The request may have been sent.
78         if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
79          throw e.withSuppressed(recoveredFailures)
80        } else {
81          recoveredFailures += e
82        }
83        newRoutePlanner = false
84        continue
85     }
        }</code></pre>
<p>우선, 최초의 <strong><code>요청</code></strong>(request)에 대해 realChain.proceed(request)를 호출하여 서버에 요청을 보내고 <strong><code>응답</code></strong>(response)을 받습니다.</p>
<p><strong>응답 후 <code>followUpRequest</code> 함수 호출(227번째 line)</strong></p>
<pre><code class="language-kotlin"> private fun followUpRequest(
    userResponse: Response,
    exchange: Exchange?,
    call: RealCall,
  ): Request? {
    val responseCode = userResponse.code
    when (responseCode) {
      .
            .
      HTTP_UNAUTHORIZED -&gt; return client.authenticator.authenticate(route, userResponse).also {
        if (it != null) {
          call.eventListener.retryDecision(call, true, &quot;Received HTTP_UNAUTHORIZED (401) and authenticate request&quot;)
        } else {
          call.eventListener.retryDecision(call, false, &quot;Received HTTP_UNAUTHORIZED (401) without authenticate request&quot;)
        }
      }</code></pre>
<p>바로 이 코드에서 <code>HTTP_UNAUTHORIZED</code> 일 때 </p>
<pre><code class="language-kotlin">return client.authenticator.authenticate(route, userResponse)</code></pre>
<p>클라이언트의 <code>authenticator</code> 의 <code>authenticate</code> 를 호출하는 것을 확인할 수 있습니다.</p>
<p>여깃었구나…ㅠㅠ</p>
<p>자 그럼 authenicate가 무엇을 반환할까요?</p>
<p>저같은 경우 서버로부터 refreshToken을 전달해 accessToken을 새로 받고 다시 요청을 보냈습니다.</p>
<pre><code class="language-kotlin">  response.request.newBuilder()
                    .header(&quot;Authorization&quot;, &quot;Bearer ${tokenData.accessToken}&quot;)
                    .build()
  // return 타입은 Request? 임</code></pre>
<p>이렇게 말이죠.</p>
<p>다시 <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">RetryAndFollowUpInterceptor</a>  코드로 돌아가보겠습니다.</p>
<pre><code class="language-kotlin"> val followUp = followUpRequest(response, exchange, call)
  if (followUp == null) {
          if (exchange != null &amp;&amp; exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }
response.body.closeQuietly()
request = followUp
priorResponse = response</code></pre>
<p>authenticate가 null을 반환하지 않고 <code>request</code> 를 반환하니 <code>if(followUp == null)</code> 코드는 패스합니다.</p>
<p>그 후 response.body.closeQuietly()로 이전 응답의 리소스를 정리한 후</p>
<p>request 변수에 새 요청(followUp)을 할당합니다.</p>
<p>priorResponse에 이전 응답을 저장합니다.</p>
<p>while 루프가 다음 반복으로 진입합니다.</p>
<p>while 루프의 반복에서 새로운 request(즉, 인증 정보가 업데이트된 요청)를 가지고 다시 <strong><code>realChain.proceed(request)</code></strong>가 호출됩니다.</p>
<p>이와 같이 while 루프는 새 요청이 계속 생성(즉, followUp이 계속 반환)되면 반복되며, 최종적으로 재시도할 필요가 없을 때 followUp이 null이 되어 최종 응답을 반환합니다.</p>
<p>여기서 또 궁금한게 생겼습니다.</p>
<h3 id="priorresponse에-왜-이전-응답을-저장해주지">priorResponse에 왜 이전 응답을 저장해주지?</h3>
<p>리다이렉션이나 인증 실패와 같이 follow-up 요청이 발생할 때 이전에 받은 응답을 새로운 요청과 연결함으로써, <strong>최종 응답에 이력(response chain)을 포함시키기 위함</strong>입니다. </p>
<p>이 이력을 통해 최종 결과가 도출되기까지 어떤 중간 응답이 있었는지를 추적할 수 있으며, 이는 <strong><code>디버깅</code>이나</strong> <strong><code>로깅</code></strong>에 매우 유용합니다</p>
<p>실제로 <code>Authenticator</code> 에서 이를 사용했단 사실!!</p>
<pre><code class="language-kotlin">class TokenAuthenticator @Inject constructor(
    private val tokenManager: TokenManager,
    private val authService: AuthService,
): Authenticator  {
    override fun authenticate(route: Route?, response: Response): Request? {
        // 무한 루프 방지
        if (responseCount(response) &gt;= 2) return null
        return runBlocking {
           ...
        }
    }

    private fun responseCount(response: Response): Int {
        var count = 1
        var r = response.priorResponse
        while (r != null) {
            count++
            r = r.priorResponse
        }
        return count
    }
}</code></pre>
<p><code>responseCount</code> 함수에서 응답객체의 response. <code>priorResponse</code> 를 사용하여 무한 요청이 가지 않게 막은 모습을 볼 수 있습니다.</p>
<p>이 과정이 일어나는 도식화로 마무리 하겠습니다! </p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/efece36e-4e44-42f4-bc65-ca23001449dc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🤔okhttp(심화) 내부 Intercepter github 코드 분석해보기]]></title>
            <link>https://velog.io/@dkdk_0422/okhttp%EC%8B%AC%ED%99%94-%EB%82%B4%EB%B6%80-Intercepter-github-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dkdk_0422/okhttp%EC%8B%AC%ED%99%94-%EB%82%B4%EB%B6%80-Intercepter-github-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 11 Apr 2025 08:30:58 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dkdk_0422/Temp-Title">이전에 okhttp의 개념과 애플리케이션에서 사용할 수 있는 네트워크 인터셉터와 애플리케이션 인터셉터에 대해 공부했다.</a></p>
<p>공부하며 서버에서 401을 응답하면 <code>OkhttpClient</code> 의 <code>Authenticator</code> 가 자동으로 실행되고 재요청을 보낼 수 있다.</p>
<p>분명 <strong>G</strong>리는 기술인데…</p>
<p>어떻게 이게 가능한거지? 궁금해서 <strong><a href="https://github.com/square/okhttp">okhttp의 github</a></strong>에 들어가게 되었다.</p>
<p>우선 어떤 인터셉터들이 있는지 보겠습니다.</p>
<h2 id="인터셉터의-종류">인터셉터의 종류</h2>
<p>어떤 인터셉터들이 있는지 확인하기 위해서 <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealCall.kt#L182">RealCall</a> 파일에 들어갔습니다.</p>
<pre><code class="language-kotlin">    val interceptors = mutableListOf&lt;Interceptor&gt;()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)</code></pre>
<p>오… 우선 인터셉터들을 <code>mutableListOf</code>  로 관리를 하고 있구만…</p>
<p>이제 어떤 인터셉터들이 있는지 확인해 보겠습니다.</p>
<h3 id="clientinterceptors">client.interceptors</h3>
<blockquote>
<p>클라이언트(Android)에서 설정한 <strong><code>애플리케이션 인터셉터</code></strong> 들이 해당됩니다.</p>
</blockquote>
<h3 id="retryandfollowupinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt"><strong>RetryAndFollowUpInterceptor</strong></a></h3>
<pre><code class="language-kotlin"> // * This interceptor recovers from failures and follows redirects as necessary. It may throw an
 // * [IOException] if the call was canceled.</code></pre>
<blockquote>
<p>요청 실패 및 리다이렉트 처리를 담당합니다.</p>
</blockquote>
<p>이녀석이 바로 서버에서 401을 내려주면 <strong><code>Authenticator</code></strong> 를 실행시키고 재요청을 할 수 있게 해주는 아주 고마운 녀석입니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/a4a32224-06de-48cf-857f-646fc9a76c69/image.png" alt=""></p>
<p>이 친구가 어떻게 401을 받으면 <strong><code>Authenticator</code></strong> 를 실행시키고 재요청을 보내는지는 다음 포스팅에서 다루도록 하겠습니다.</p>
<hr>
<h3 id="bridgeinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/BridgeInterceptor.kt#L34"><strong>BridgeInterceptor</strong></a></h3>
<pre><code class="language-kotlin">/**
 * Bridges from application code to network code. First it builds a network request from a user
 * request. Then it proceeds to call the network. Finally it builds a user response from the network
 * response.
 */</code></pre>
<aside>

<blockquote>
<p>앱 코드에서 네트워크 코드로 가는 <strong><code>다리 역할</code></strong> 을 합니다.</p>
</blockquote>
</aside>

<p> 쉽게 말하면 OkHttp 내부에서 “사용자 요청 ↔ 실제 HTTP 요청”을 연결하는 중간처리기 역활을 합니다.</p>
<p><strong>요청(Request) 표준화</strong></p>
<ul>
<li>User-Agent, Accept-Encoding, Content-Type, Content-Length 등을 자동으로 추가</li>
<li>Gzip 압축 요청 여부도 여기서 설정됩니다.</li>
</ul>
<p><strong>요청 바디 처리</strong></p>
<ul>
<li>POST, PUT 등의 요청에 Body가 있는 경우, Content-Length 혹은 Transfer-Encoding: chunked를 자동 설정합니다.</li>
</ul>
<p><strong>응답(Response) 변환</strong></p>
<ul>
<li>네트워크로부터 받은 응답(Response)을 사용자 응답 형태로 변환합니다.</li>
</ul>
<hr>
<h3 id="cacheinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/CacheInterceptor.kt#L44"><strong>CacheInterceptor</strong></a></h3>
<pre><code class="language-kotlin">/** Serves requests from the cache and writes responses to the cache. */</code></pre>
<blockquote>
<p>HTTP 및 HTTPS 응답을 파일 시스템에 저장하여 이후 재사용할 수 있도록 하는 역할을 합니다.</p>
</blockquote>
<p>캐시를 사용하기 위해 <code>OkHttpClient</code> 를 설정할 때 <code>Cache</code> 를 설정해줘야 합니다.</p>
<pre><code class="language-kotlin">val cacheSize = 50L * 1024L * 1024L // 50 MiB
val cache = Cache(File(application.cacheDir, &quot;http_cache&quot;), cacheSize)

val client = OkHttpClient.Builder()
    .cache(cache)
    .build()</code></pre>
<p>이렇게 설정하면 OkHttp는 서버의 Cache-Control 헤더를 기반으로 응답을 캐시합니다. 서버가 해당 헤더를 제공하지 않는 경우, 클라이언트 측에서 인터셉터를 사용하여 캐시 동작을 제어할 수 있습니다.  </p>
<p>즉, 네트워크에서 받은 응답이 캐시 가능하면 이를 저장하여 이후 동일한 요청 시 재사용할 수 있도록 합니다.</p>
<hr>
<h3 id="connectinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectInterceptor.kt#L28"><strong>ConnectInterceptor</strong></a></h3>
<pre><code class="language-kotlin"> * Opens a connection to the target server and proceeds to the next interceptor. The network might
 * be used for the returned response, or to validate a cached response with a conditional GET.</code></pre>
<blockquote>
<p><strong>서버와 실제 소켓 연결(TCP/HTTPS)</strong> 을 설정합니다</p>
</blockquote>
<p>즉, 실제로 서버와 연결을 수행하는 친구입니다.</p>
<p>이 친구가 없으면 서버와 연결이 안됩니다.</p>
<hr>
<h3 id="clientnetworkinterceptors네트워크-인터셉터">client.networkInterceptors(네트워크 인터셉터)</h3>
<blockquote>
<p>클라이언트에서 추가하는 네트워크 수준의 인터셉터(<strong>웹 소켓이 아닌경우</strong>)</p>
</blockquote>
<hr>
<h3 id="callserverinterceptor"><a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/CallServerInterceptor.kt#L28"><strong>CallServerInterceptor</strong></a></h3>
<p>/** This is the last interceptor in the chain. It makes a network call to the server. */</p>
<blockquote>
<p>최종적으로 서버와 통신하여 요청을 보내고 응답을 받는 역활을 합니다.</p>
</blockquote>
<p> CallServerInterceptor를 거쳐 <strong>진짜 HTTP 통신이 시작</strong>됩니다.</p>
<pre><code>1.    writeRequestHeaders: 요청 헤더 전송
2.    writeTo: 요청 바디 전송 (POST, PUT 등)
3.    finishRequest: 요청 종료
4.    readResponseHeaders: 응답 헤더 수신
5.    openResponseBody: 응답 바디 수신
6.    응답 객체 Response 빌드 후 반환</code></pre><hr>
<h2 id="chainproceedrequest">chain.proceed(request)</h2>
<p>위에서 <code>Intercepter</code> 들의 종류와 역활에 대해 알아봤습니다.</p>
<p>그러면 어떻게 이런 형태로 동작하게 되는걸까요?</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/c33ce727-1e2c-4377-8df8-608fd3d29a2e/image.png" alt=""></p>
<p>이러한 역활을 수행할 수 있게 해주<strong>는 <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RealInterceptorChain.kt#L36"><code>RealInterceptorChain</code></a></strong> 에 대해 알아보겠습니다.</p>
<pre><code class="language-kotlin">class RealInterceptorChain(
  internal val call: RealCall,
  private val interceptors: List&lt;Interceptor&gt;,
  private val index: Int,
  internal val exchange: Exchange?,
  internal val request: Request,
  internal val connectTimeoutMillis: Int,
  internal val readTimeoutMillis: Int,
  internal val writeTimeoutMillis: Int,
) : Interceptor.Chain {

}

//RealCall.kt
val chain = RealInterceptorChain(...)
      val interceptors = mutableListOf&lt;Interceptor&gt;()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)
 try {
      val response = chain.proceed(originalRequest)
      if (isCanceled()) {
        response.closeQuietly()
        throw IOException(&quot;Canceled&quot;)
      }
      return response
    } catch (e: IOException) {
      calledNoMoreExchanges = true
      throw noMoreExchanges(e) as Throwable
    } finally {
      if (!calledNoMoreExchanges) {
        noMoreExchanges(null)
      }
    }</code></pre>
<p>우선 Interceptor.Chain interface를 사용하고 있는 모습입니다.</p>
<p><code>RealCall</code> 파일에서는 chain을 <code>RealInterceptorChain(...)</code> 으로 생성해서 <code>chain.proceed</code> 을 실행한 response를 반환하고 있습니다.</p>
<p>어떻게 <code>RealInterceptorChain</code> 이 동작하는지 한번 알아보겠습니다.</p>
<p><code>RealInterceptorChain</code> 은 Interceptor.Chain 인터페이스를 사용하고 있기 때문에 </p>
<p> override fun proceed(request: Request): Response 를 꼭 <code>override</code> 해야 합니다.</p>
<p>실제로 <code>RealCall</code> 파일에서 .proceed를 호출하는 구현부가 이곳이기도 하구요.</p>
<p>어떻게 구현했는지 보겠습니다.</p>
<pre><code class="language-kotlin">private var calls: Int = 0

class RealInterceptorChain(
  internal val call: RealCall,
  private val interceptors: List&lt;Interceptor&gt;,
  private val index: Int,
  internal val exchange: Exchange?,
  internal val request: Request,
  internal val connectTimeoutMillis: Int,
  internal val readTimeoutMillis: Int,
  internal val writeTimeoutMillis: Int,
) : Interceptor.Chain {
  private var calls: Int = 0

  internal fun copy(
    index: Int = this.index,
    exchange: Exchange? = this.exchange,
    request: Request = this.request,
    connectTimeoutMillis: Int = this.connectTimeoutMillis,
    readTimeoutMillis: Int = this.readTimeoutMillis,
    writeTimeoutMillis: Int = this.writeTimeoutMillis,
  ) = RealInterceptorChain(
    call,
    interceptors,
    index,
    exchange,
    request,
    connectTimeoutMillis,
    readTimeoutMillis,
    writeTimeoutMillis,
  )
@Throws(IOException::class)
  override fun proceed(request: Request): Response {
    check(index &lt; interceptors.size)

    calls++

    if (exchange != null) {
      check(exchange.finder.routePlanner.sameHostAndPort(request.url)) {
        &quot;network interceptor ${interceptors[index - 1]} must retain the same host and port&quot;
      }
      check(calls == 1) {
        &quot;network interceptor ${interceptors[index - 1]} must call proceed() exactly once&quot;
      }
    }

    // Call the next interceptor in the chain.
    val next = copy(index = index + 1, request = request)
    val interceptor = interceptors[index]

    @Suppress(&quot;USELESS_ELVIS&quot;)
    val response =
      interceptor.intercept(next) ?: throw NullPointerException(
        &quot;interceptor $interceptor returned null&quot;,
      )

    if (exchange != null) {
      check(index + 1 &gt;= interceptors.size || next.calls == 1) {
        &quot;network interceptor $interceptor must call proceed() exactly once&quot;
      }
    }

    return response
  }
 }</code></pre>
<p>우선 각 변수, 함수들에 대해 알아보겠습니다.</p>
<ul>
<li><p><strong>exchange: Exchange?</strong></p>
<ul>
<li>네트워크 교환(실제 소켓이나 HTTP 스트림 등)을 나타내며, 네트워크 인터셉터 단계에서 사용됩니다.
만약 네트워크 인터셉터 외의 단계라면 null일 수 있습니다.</li>
</ul>
</li>
<li><p><strong>calls</strong></p>
<ul>
<li>전체 HTTP 요청/응답 사이클을 대표하는 RealCall 인스턴스로, 호출과 관련된 전반적 정보를 포함합니다</li>
</ul>
</li>
<li><p><strong>index: Int</strong></p>
<ul>
<li>현재 체인에서 실행할 인터셉터의 인덱스입니다.
이 값이 증가하면서 인터셉터들이 순차적으로 호출됩니다.</li>
</ul>
</li>
</ul>
<ul>
<li><p><strong>request: Request</strong></p>
<ul>
<li>현재 처리 중인 HTTP 요청 객체입니다.</li>
</ul>
</li>
</ul>
<br>

<p>이제 <code>proceed</code> 함수가 어떻게 동작하는지 보겠습니다.</p>
<ol>
<li><p><strong>체인 인덱스 검증</strong></p>
<pre><code class="language-kotlin"> check(index &lt; interceptors.size)</code></pre>
<ul>
<li>현재 index가 전체 인터셉터 리스트의 크기보다 작은지 확인합니다</li>
<li>만약 index가 인터셉터의 개수를 초과했다면 논리 오류가 발생하게 됩니다</li>
</ul>
</li>
<li><p><strong>호출 카운트 증가</strong></p>
<pre><code class="language-kotlin"> calls++</code></pre>
<ul>
<li>현재 인터셉터가 proceed()를 호출한 횟수를 기록합니다.</li>
<li>네트워크 인터셉터는 반드시 한 번만 proceed()를 호출해야 하므로 이를 검사할 기반이 됩니다.</li>
</ul>
</li>
<li><p><strong>네트워크 인터셉터에 대한 추가 검증 (exchange가 있을 경우)</strong></p>
<pre><code class="language-kotlin"> if (exchange != null) {
   check(exchange.finder.routePlanner.sameHostAndPort(request.url)) {
     &quot;network interceptor ${interceptors[index - 1]} must retain the same host and port&quot;
   }
   check(calls == 1) {
     &quot;network interceptor ${interceptors[index - 1]} must call proceed() exactly once&quot;
   }
 }</code></pre>
<ul>
<li>현재 요청(request)이 이전에 사용한 라우트 플래너의 호스트와 포트와 동일한지 확인합니다.</li>
<li>네트워크 인터셉터는 요청의 목적지가 변경되면 안 됩니다.</li>
<li>현재 인터셉터가 proceed()를 <strong>한 번만 호출</strong>했는지 확인합니다.</li>
</ul>
</li>
<li><p><strong>다음 체인 준비</strong></p>
<pre><code class="language-kotlin"> val next = copy(index = index + 1, request = request)</code></pre>
<ul>
<li><p>현재 체인 상태를 복사하면서 인덱스를 1 증가시킵니다.</p>
</li>
<li><p>새 객체(next)는 다음 인터셉터가 처리할 체인 정보를 담고 있습니다</p>
</li>
<li><p>여기서 copy를 하는 이유는  <strong>불변성(immutability) 및 상태 보호를 위해서</strong> 입니다<strong>.</strong></p>
<p>각 인터셉터가 호출될 때마다 원본 체인의 상태(예를 들어, 현재 인덱스, 요청 정보, 타임아웃 값 등)를 그대로 유지하면서 새로운 변경된 값을 적용해야 합니다.
이를 위해 copy()를 통해 새로운 체인 객체를 생성하면, 이전 체인의 상태에 영향을 주지 않고 안전하게 상태를 업데이트할 수 있습니다. 즉, 한 인터셉터에서 발생한 변경이 다른 인터셉터에 부정적인 영향을 주지 않게 됩니다.</p>
</li>
</ul>
</li>
<li><p><strong>인터셉터 선택</strong></p>
<pre><code class="language-kotlin"> val interceptor = interceptors[index]</code></pre>
<ul>
<li>현재 인덱스에 해당하는 인터셉터를 선택합니다.</li>
</ul>
</li>
<li><p><strong>인터셉터(index +1)  실행</strong></p>
<pre><code class="language-kotlin"> @Suppress(&quot;USELESS_ELVIS&quot;)
 val response =
   interceptor.intercept(next) ?: throw NullPointerException(
     &quot;interceptor $interceptor returned null&quot;,
   )</code></pre>
<ul>
<li>선택된 인터셉터의 intercept() 메서드를 호출하면서, 새 체인(next)을 전달합니다.</li>
<li>인터셉터의 결과로 Response 객체를 받아옵니다.</li>
</ul>
</li>
<li><p><strong>네트워크 인터셉터에 대한 후처리 검증 (exchange가 있을 경우)</strong></p>
<pre><code class="language-kotlin"> if (exchange != null) {
   check(index + 1 &gt;= interceptors.size || next.calls == 1) {
     &quot;network interceptor $interceptor must call proceed() exactly once&quot;
   }
 }</code></pre>
<ul>
<li>만약 현재가 네트워크 인터셉터 관련 체인이라면, 다음 체인(next)에서 proceed()가 정확히 한 번 호출되었는지 다시 확인합니다.</li>
</ul>
</li>
<li><p><strong>응답 반환</strong></p>
<pre><code class="language-kotlin"> return response</code></pre>
<ul>
<li>최종적으로 응답 객체를 반환하며, 이는 체인을 거쳐 최종적으로 서버에서 받은 응답일 수도 있고, 중간 인터셉터에서 변환된 응답일 수도 있습니다.</li>
</ul>
</li>
</ol>
<p>자세히 다루진 않았지만 <code>RealInterceptorChain</code> 파일에서 </p>
<p><code>withConnectTimeout</code>, <code>withReadTimeout</code>, <code>withWriteTimeout</code> 메서드를 통해 타임아웃을 조정할 수 있습니다. </p>
<p>또한 네트워크 인터셉터에서는 이 Timeout을 변경할 수 없도록 제한합니다.</p>
<p>공부를 하면서 왜 이름을 <code>Chain</code> 이라고 네이밍을 한 지 알게되었습니다.</p>
<p>잘못된 정보가 있다면 알려주시면 감사하겠습니다!!🙏</p>
<p>네트워킹이 이루어지는 과정을 도식화한 그림으로 마무리 하겠습니다. 읽어주셔서 감사합니다!</p>
<br>

<p><strong>요청</strong>
<img src="https://velog.velcdn.com/images/dkdk_0422/post/fd471cad-64ec-40d4-970c-2b206c76d9e2/image.png" alt="">
** 응답**
<img src="https://velog.velcdn.com/images/dkdk_0422/post/4e031b2a-1cea-4388-bae7-cc7086341bf2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Okhttp3 Network Intercepter | Application Intercepter]]></title>
            <link>https://velog.io/@dkdk_0422/Temp-Title</link>
            <guid>https://velog.io/@dkdk_0422/Temp-Title</guid>
            <pubDate>Fri, 11 Apr 2025 06:03:09 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크-인터셉터와-애플리케이션-인터셉터">네트워크 인터셉터와 애플리케이션 인터셉터</h1>
<p>이전의 <a href="https://velog.io/@dkdk_0422/okhttp3%EA%B0%9C%EB%85%90-%EB%B0%8F-Intercepter%EC%99%80-Authenticator%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D">OkHttp3 개념 및 Intercepter와 Authenticator</a> 에서 Interceptor와 Authenticator를 이용해 <code>accesstoken</code> , <code>refreshtoken</code> 을 이용한 인증 방법에 대해 공부했습니다.</p>
<p>해당 내용을 스터디 시간에 발표를 했고 스터디원 분이 질문을 주셨습니다.</p>
<p>“<code>Authenticator</code> 가 다시 <code>Request</code> 를 반환하는데 다시 네트워크 요청을 보내게 되어 Interceptor 에서 다시 헤더에 토큰을 추가할 텐데 이쪽에서 헤더를 다시 붙이는 이유가 있나요?</p>
<p>저도 오래되어 기억이 잘 나지 않지만 해당 도식화 순서가 맞는지??”</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/56618ecd-b8c8-48a4-8504-5c73af84ddde/image.png" alt=""></p>
<pre><code class="language-kotlin">   override fun authenticate(route: Route?, response: Response): Request? {
        // 무한 루프 방지
        if (responseCount(response) &gt;= 2) return null
        return runBlocking {
            runCatching {
                val refreshToken = tokenManager.getRefreshToken() ?: throw IllegalStateException(&quot;Refresh token is null&quot;)

                val tokenResponse = authService.refreshToken(refreshToken)
                if (tokenResponse.code != 200 &amp;&amp; tokenResponse.data == null) {
                    throw IllegalStateException(&quot;refreshtokenRequest 발급 실패&quot;)
                }

                val tokenData = tokenResponse.data
                    ?: throw IllegalStateException(&quot;Token data is null&quot;)

                tokenManager.saveTokens(tokenData.accessToken, tokenData.refreshToken)

                response.request.newBuilder()
                    .header(&quot;Authorization&quot;, &quot;Bearer ${tokenData.accessToken}&quot;)
                    .build()
            }.getOrElse {
                tokenManager.clear()
                null
            }
        }
    }

    OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .addInterceptor(interceptor)
            .authenticator(authenticator)
            .build()</code></pre>
<p>저 또한 그렇게 알고 있었지만 <code>interceptor</code>에 로그를 찍어봤지만 <code>authenticate</code> 가 호출된 후 기대했던 예상과는 달리 <code>interceptor</code> 에 대한 로그는 찍히지 않았습니다.</p>
<h2 id="문제">문제</h2>
<ul>
<li><code>authenticate</code> 호출 → 재요청 → <code>interceptor</code> 의 로그가 찍혀야 하는데 안찍힘</li>
<li><code>authenticate</code> 에 헤더에 새 토큰을 집어넣는 코드를 지우니 <code>401</code> 에러를 그대로 가져옴</li>
</ul>
<h2 id="해결과정">해결과정</h2>
<p>답은 역시…<a href="https://square.github.io/okhttp/features/interceptors/">공식 문서</a>에 있었습니다</p>
<p>인터셉터는 <strong><code>애플리케이션 인터셉터</code></strong> 와 <strong><code>네트워크 인터셉터</code></strong> 로 분류됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/8ca857b7-17af-47ae-bb2b-6a61d0289bec/image.png" alt=""></p>
<p>우선 간단하게 말하면 <strong><code>애플리케이션 인터셉터</code>  클라이언트가 최초 요청을 보낼 때 한 번 호출됩니다.</strong></p>
<p><strong><code>네트워크 인터셉터</code> 는 실제 네트워크로 요청이 전송되기 직전과 응답이 도착한 직후마다 호출됩니다.</strong></p>
<p>아 이러니… 내가 작성한 인터셉터에 로그가 안찍히지.. 했습니다.</p>
<p>위에서 제가 작성한  <code>.addInterceptor(interceptor)</code>  는 애플리케이션 인터셉터였기 때문입니다.</p>
<p>두 인터셉터에 대해서 알아보겠습니다.</p>
<p>그렇다면 <code>Authenticator</code> 는 누구에게 401을 받고 누구에게 request를 다시 보내는 걸가요?</p>
<p>자세하기 알아보기 전에 두 인터셉터의 차</p>
<h3 id="네트워크-인터셉터">네트워크 인터셉터</h3>
<ul>
<li>보안 검사, 캐시 무효화, 압축 조작, 인증 후 재요청 등 “실제 네트워크 요청” 직전에 쓰임</li>
<li>서버로 나가는 최종 헤더 확인</li>
<li>응답 압축 해제</li>
<li><strong>진짜 네트워크 요청”에 대한 마지막 가공 또는 확인 용도</strong></li>
</ul>
<h3 id="application-interceptor">Application Interceptor</h3>
<ul>
<li>클라이언트가 최초 요청을 보낼 때 한 번 호출됩니다.</li>
<li>요청, 응답을 <strong>앱 수준에서 가공</strong></li>
<li>캐시 응답에도 동작</li>
<li>인증 재시도 시 <strong>재호출 ❌</strong></li>
</ul>
<p><strong>사용 예시:</strong></p>
<ul>
<li>모든 요청에 공통 헤더 추가 (Authorization 등)</li>
<li>Query 파라미터 추가</li>
<li>Logging</li>
<li>API Key 추가</li>
</ul>
<hr>
<h3 id="network-interceptor">Network Interceptor</h3>
<ul>
<li><strong>네트워크 계층</strong>에서 작동</li>
<li><strong>진짜 전송 직전</strong>에 요청을 가로채거나 응답을 수정</li>
<li>인증 재시도 시 <strong>재호출 ✅</strong></li>
<li>압축 해제 등 <strong>실제 전송 직전 작업</strong> 가능</li>
</ul>
<p><strong>사용 예시:</strong></p>
<ul>
<li>최종 헤더 검증</li>
<li>토큰 교체, 압축 조작</li>
<li>요청 시간 측정</li>
<li>Retry 대상 확인</li>
</ul>
<hr>
<h2 id="🔍-차이점-비교-테이블">🔍 차이점 비교 테이블</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Application Interceptor</th>
<th>Network Interceptor</th>
</tr>
</thead>
<tbody><tr>
<td>위치</td>
<td>앱 ↔️ OkHttp</td>
<td>OkHttp ↔️ 서버</td>
</tr>
<tr>
<td>호출 시점</td>
<td>최초 요청 1번만</td>
<td>인증 재시도 시에도 호출</td>
</tr>
<tr>
<td>캐시 응답 작동 여부</td>
<td>✅ 작동</td>
<td>❌ 작동 안 함</td>
</tr>
<tr>
<td>재요청에 다시 호출되는가?</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>압축, 보안, 인증 확인</td>
<td>❌ 불가</td>
<td>✅ 가능</td>
</tr>
<tr>
<td>용도</td>
<td>공통 헤더, 로그</td>
<td>보안, 인증, 요청 감시</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론">결론</h2>
<p>서버에서 <code>401</code> 을 받게되면 <code>Autenticator</code> 가 실행되고 여기서 반환된 <code>request</code> 는 내부적으로 <code>chain.proceed(request)</code> 을 통해 인터셉터들을 타고 가서(네트워크 인터셉터가 있다면 탐) 다시 서버에 요청을 보내게 됨.</p>
<p>좀 더 깊이 알고싶어 내부적으로 어떻게 동작하는지 궁금하여 <a href="https://github.com/square/okhttp/blob/0a8129ffeff105c5f2b54baabeb254cad5d84508/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt">okhttp의 github</a> 를 통해 내부적인 인터셉터들의 존재 유무를 알게되었고 다음 포스팅에 이에 대해 다뤄보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[async와 Deferred(withContext)]]></title>
            <link>https://velog.io/@dkdk_0422/async%EC%99%80-DeferredwithContext</link>
            <guid>https://velog.io/@dkdk_0422/async%EC%99%80-DeferredwithContext</guid>
            <pubDate>Thu, 10 Apr 2025 08:15:33 GMT</pubDate>
            <description><![CDATA[<p>해당 내용은<a href="https://product.kyobobook.co.kr/detail/S000212376884?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=googleSearch&amp;gad_source=1">코틀린 코루틴의 정석</a>6장 내용을 공부하며 정리한 내용입니다.</p>
<p>기존의 <code>launch</code> 코루틴 빌더는 결과를 반환하지 않습니다.</p>
<p>그렇다면 결과를 수신해야 할 때는 어떤 방법을 사용할가요?</p>
<p><code>async</code> 코루틴 빌더를 통해 코루틴을 생성하면 생성한 코루틴으로부터 결괏값을 수신받을 수 있습니다.</p>
<p><code>launch</code> 함수 사용시 결괏값이 없는 <code>Job</code> 객체를 반환했지만 async 함수를 사용하면 결괏값이 있는 코루틴 객체인 <code>Deferred</code> 객체가 반환됩니다.</p>
<p>이 <code>Deferred</code> 객체를 통해 코루틴으로부터 결괏값을 수신할 수 있습니다.</p>
<h2 id="async-사용해-결괏값-수신하기">async 사용해 결괏값 수신하기</h2>
<h3 id="async-사용해-deferred-만들기">async 사용해 Deferred 만들기</h3>
<pre><code class="language-kotlin">public fun &lt;T&gt; CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&gt; T
): Deferred&lt;T&gt;</code></pre>
<p><code>async</code> 함수 또한 launch와 마찬가지로 context를 통해 Dispatcher를 설정할 수 있고 start 인자로 <code>LAZY</code> 를 설정해 코루틴 지연을 설정할 수 있습니다. block 은 코루틴에서 실행할 코드를 작성하는 람다식 입니다.</p>
<blockquote>
<p><code>Deferred</code> 는 Job과 같이 코루틴을 추상화한 객체이지만 코루틴으로 부터 생성된 결괏값을 감싸는 기능을 추가로 가짐</p>
</blockquote>
<p>바로 Deferred<String>을 반환하는 객체를 만들어 보겠습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
   val message: Deferred&lt;String&gt; = async {
       delay(1000L)
       &quot;Hello World!!&quot;
   }
    println(message.await())
}
// Hello World!!</code></pre>
<p>명시적으로 타입 Deferred<String>을 붙여주지 않아도 됩니다.</p>
<h3 id="await를-사용한-결괏값-수신">await를 사용한 결괏값 수신</h3>
<blockquote>
<p><code>Deferred</code> 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체</p>
</blockquote>
<p>Deferred 객체는 결괏값 수신의 대기를 위해 <code>await</code> 함수를 제공한다.</p>
<p><code>await</code> 함수는 await의 대상이 된 Deferred 코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단하며, Deferred 코루틴이 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개한다.</p>
<p>바로 예제를 통해 알아보겠습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
   val message: Deferred&lt;String&gt; = async {
       delay(1000L)
       return@async &quot;Hello World!!&quot;
   }
    println(&quot;메시지 언제오나??&quot;)
    val result: String = message.await()
    println(result)
}
메시지 언제오나??
Hello World!!</code></pre>
<p>여기서 주의깊게 봐야할 것은 <code>await</code> 이다.</p>
<p>앞서 Deferred 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 객체라고 배웠다.</p>
<p>그래서 message의 타입은 async 빌더를 사용했기 때문에 <code>Deferred&lt;String&gt;</code> 이 되는 것이다.</p>
<p>이때 <code>message.await</code> 을 해버리는 순간 result의 type은 <code>String</code> 이 된다.</p>
<p>즉 결괏값이 반환되는 시점이기 때문에 String객체의 데이터(결괏값)를 얻게되는 것이다.</p>
<hr>
<h2 id="deferred는-특수한-형태의-job">Deferred는 특수한 형태의 Job</h2>
<blockquote>
<p><code>Deferred</code> 객체는 결괏값을 가지는 Job으로 비동기 작업의 결과를 미래에 받아올 수 있는 Promise 객체</p>
</blockquote>
<pre><code class="language-kotlin">public interface Deferred&lt;out T&gt; : Job </code></pre>
<p>위 코드를 보면 <code>Deferred</code> 객체는 <code>Job</code> 인터페이스의 서브타입으로 선언되어 있다.</p>
<p>즉, 결괏값 수신을 위해 Job 객체에서 <strong>몇 가지 기능이 추가</strong>됐을 뿐, 여전히 Job 객체의 일종이다. </p>
<p>그러므로 Job과 마찬가지로 <code>join()</code>, <code>cancel()</code>, <code>isActive</code> , <code>isCancelled</code>, <code>isCompleted</code>  등 프로퍼티들을 사용할 수 있다.</p>
<p>정리하자면 Deferred 객체는 결괏값을 반환받는 기능이 추가된 Job 객체이며,</p>
<p>Job 객체의 모든 함수와 변수를 사용할 수 있습니다.</p>
<hr>
<h2 id="복수의-코루틴으로부터-결괏값-수신하기">복수의 코루틴으로부터 결괏값 수신하기</h2>
<p>개발을 하다 보면 여러 비동기 작업으로부터 결괏값을 반환받아 병합해야 하는 경우가 자주 생긴다.</p>
<p>이때 우리는 복수의 코루틴을 생성하고 결괏값을 취합해야 한다.</p>
<h3 id="await를-사용해-복수의-코루틴으로부터-결괏값-수신">await를 사용해 복수의 코루틴으로부터 결괏값 수신</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val stringList = (1..10).map { value -&gt;
        val result = async {
            delay(1000L)
            return@async &quot;num is $value&quot;
        }.await()
        result
    }
    stringList.forEach { print(&quot;$it &quot;) }
    println(getElapsedTime(startTime))
}
num is 1 num is 2 num is 3 num is 4 num is 5
num is 6 num is 7 num is 8 num is 9 num is 10
지난 시간 : 10077ms</code></pre>
<p>여기서 주의깊게 봐야할 점은 지난 시간이 10초가 걸렸다는 것이다.</p>
<p>위 코드는 아주 비효율적인 코드이다.</p>
<p>1부터 10까지 반복문을 돌면서 1일 때 1초쉬고 2일때 2초쉬고… 이런식으로 10초가 흘러간다.</p>
<p>우리가 원하는 것은 1초안에 작업이 끝나는 것이다, 어떻게 할 수 있을가?</p>
<h3 id="awaitall을-사용한-결괏값-수신">awaitAll을 사용한 결괏값 수신</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val stringList: List&lt;Deferred&lt;String&gt;&gt; = (1..10).map { value -&gt;
        val result: Deferred&lt;String&gt; = async {
            delay(1000L)
            return@async &quot;num is $value&quot;
        }
        result
    }
    println(stringList.awaitAll())
    println(getElapsedTime(startTime))
}
[num is 1, num is 2, num is 3, num is 4, num is 5, num is 6, num is 7, num is 8, num is 9, num is 10]
지난 시간 : 1040ms</code></pre>
<p>우리가 의도한대로 1초만에 작업이 완료된 것을 확인할 수 있다.</p>
<p>이를 위해 코루틴 라이브러리는 복수의 <code>Deferred</code> 객체로부터 결괏값을 수신하기 위한 <code>awaitAll</code> 함수를 제공합니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val participantDeferred1: Deferred&lt;Array&lt;String&gt;&gt; = async {
        delay(1000L)
        arrayOf(&quot;길동&quot;, &quot;옥지&quot;)
    }
    val participantDeferred2: Deferred&lt;Array&lt;String&gt;&gt; = async {
        delay(1000L)
        arrayOf(&quot;옥순&quot;, &quot;빵빵&quot;)
    }
    val result = awaitAll(participantDeferred1, participantDeferred2)
    println(result)
    println(getElapsedTime(startTime))
}</code></pre>
<p>이런식으로도 가능하다</p>
<p>즉, <code>awaitAll</code> 함수에 값을 집어넣거나, <code>Collection&lt;Deferred&lt;T&gt;&gt;.awaitAll()</code> 컬렉션의 awaitAll을 사용할 수 있다.</p>
<hr>
<h2 id="withcontext">withContext</h2>
<aside>

<blockquote>
<p><strong>일시적으로 CoroutineContext를 전환하는 suspend 함수</strong></p>
</blockquote>
</aside>

<pre><code class="language-kotlin">public suspend fun &lt;T&gt; withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -&gt; T
): T </code></pre>
<h3 id="withcontext로-async---await-대체하기">withContext로 async - await 대체하기</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val networkDeferred: Deferred&lt;String&gt; = async(Dispatchers.IO) { 
        delay(1000L)
        return@async &quot;Dummy Response&quot;
    }
    val result = networkDeferred.await()
}</code></pre>
<p>해당 코드를 <code>withContext</code> 로 대체해 보자</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val result: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext &quot;Dummy Response&quot;
    }
    println(result)
}</code></pre>
<h3 id="withcontext의-동작-방식">withContext의 동작 방식</h3>
<blockquote>
<p><code>async - await</code> 는 새로운 코루틴을 생성해 작업을 처리하지만 <code>withContext</code> 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경만 변경해 작업을 처리한다</p>
</blockquote>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    println(&quot;[${Thread.currentThread().name} runBlocking]&quot;)
    withContext(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name} withContext]&quot;)
    }
}
[main @coroutine#1 runBlocking]
[DefaultDispatcher-worker-2 @coroutine#1 withContext]</code></pre>
<p>출력에서 같은 <code>coroutine</code> 에서 실행된 것을 확인할 수 있다.</p>
<p>즉, 실행되는 스레드는 다르지만 코루틴은 coroutine#1으로 같은 것을 볼 수 있다.</p>
<p><code>withContext</code> 함수는 새로운 코루틴을 만드는 대신 기존의 코루틴에서 <code>CoroutineContext</code> 객체만 바꿔서 실행된다. 여기서 Dispatcher를 IO로 바꾸었기 때문에 백그라운드 스레드에서 실행되었다.</p>
<p>동작원리에 대해서 알아보겠습니다.</p>
<p>처음 메인 스레드에 <code>coroutine#1</code> 이 있습니다. <code>withContext(Dispatchers.IO)</code>  로 인해 IO 작업 대기열에 <code>coroutine#1</code> 이 옮겨지고 백그라운드 스레드에서 실행되게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dkdk_0422/post/f431113e-b3c1-4b97-a913-3f55a771218c/image.png" alt=""></p>
<p>이렇게 메인에서 실행 중인 코루틴의 실행 환경이 <code>withContext</code> 의 context 인자 값으로 변경돼 실행되며,</p>
<p>이를 <code>컨텍스트 스위칭</code> (Context Switching)이라고 부릅니다.</p>
<p>이처럼 <code>withContext</code> 함수는 함수의 block 람다식이 실행되는 동안 코루틴의 실행 환경을 변경시킵니다.</p>
<h3 id="withcontext-사용-시-주의점">withContext 사용 시 주의점</h3>
<p>복수의 독립적인 작업이 병렬로 실행돼야 하는 상황에 <code>withContext</code> 를 사용할 경우 <strong>성능에 문제를 일으킬 수</strong> 있습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val hello = withContext(Dispatchers.IO) {
        delay(1000L)
        &quot;hello&quot;
    }
    val dongkyung = withContext(Dispatchers.IO) {
        delay(1000L)
        &quot;dongkyung&quot;
    }
    val result = &quot;$hello $dongkyung&quot;
    println(result)
    println(getElapsedTime(startTime))
}
hello dongkyung
지난 시간 : 2052ms</code></pre>
<p>해당 코드를 보면 1초만에 할 수 있는 작업을 <code>withContext</code> 를 사용해 순차적으로 처리돼 2초가 걸렸다.</p>
<p>이를 해결하기 위해선 <code>withContext</code> 를 지우고 코루틴을 생성하는 <code>async - await</code> 쌍으로 대체해야 한다.</p>
<p>즉, <code>withContext</code> 함수 사용시 코드가 깔끔해 보이는 효과를 내지만 잘못 사용하게 되면 코루틴을 동기적으로 실행하도록 만들어 코드 실행 시간이 배 이상으로 증가하여 효율성이 떨어지게 된다.</p>
<blockquote>
<p><code>withContext</code> 는 새로운 코루틴을 만들지 않는다!!</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>