<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>조준장 개발자 생존기</title>
    <link>https://fre2-dom.tistory.com/</link>
    <description>저평가 우량주 개발자</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 05:40:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JunJangE</managingEditor>
    <image>
      <title>조준장 개발자 생존기</title>
      <url>https://tistory1.daumcdn.net/tistory/3096830/attach/d217dd7eba234cc9b332553d14ef1de6</url>
      <link>https://fre2-dom.tistory.com</link>
    </image>
    <item>
      <title>신입 개발자의 AI 주도 개발 생존기</title>
      <link>https://fre2-dom.tistory.com/589</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc5ssd/dJMcagkcQKr/eI3Ke6JtHJCuaQUVRJCKeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc5ssd/dJMcagkcQKr/eI3Ke6JtHJCuaQUVRJCKeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc5ssd/dJMcagkcQKr/eI3Ke6JtHJCuaQUVRJCKeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc5ssd%2FdJMcagkcQKr%2FeI3Ke6JtHJCuaQUVRJCKeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;663&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1f1f1f;&quot;&gt;취업 전 다양한 프로젝트와 프리랜서 활동, 그리고 네이버 부스트캠프와 우아한테크코스를 거치며 나만의 개발 철학을 차곡차곡 쌓아왔다고 자부했다. 하지만 현업의 문을 열고 마주한 가장 큰 변화는 내가 배운 '코딩' 그 자체가 아니라, 바로 AI 주도 개발(AI-Driven Development)이라는 거대한 흐름이었다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;이번 글에서는 신입 개발자로서 AI와 함께 협업하며 느낀 당혹감과 깨달음, 그리고 우리가 앞으로 가져야 할 태도에 대해 이야기해보려 한다. 정답을 제시하기보다는, &quot;아, 이 사람은 요즘 이런 생각을 하며 개발하고 있구나&quot; 하는 마음으로 편하게 읽으면 좋을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AI, '정답지'에서 '파트너'로&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생 시절부터 ChatGPT나 뤼튼 같은 AI를 종종 활용해왔다. 하지만 당시의 나에게 AI는 '모르는 문제를 풀어주는 선생님'에 가까웠다. 학습이 목적이었던 내게 질문하자마자 정답을 툭 던져주는 방식은 오히려 독이 된다고 느꼈고, 급한 상황이 아니라면 공식 문서와 구글링이라는 정석적인 길을 고집하곤 했다.&lt;br /&gt;그러던 중, 현업에서 개발자로 활동 중인 형들에게 &quot;금전적인 여유가 된다면 Cursor AI, Claude Code와 같은 AI 도구를 적극적으로 활용해 봐라.&quot; 라는 &lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1f1f1f;&quot;&gt;조언을 듣게 되었다. &lt;/span&gt;&lt;/span&gt;실제 면접에서도 AI 활용 역량에 대한 질문을 자주 받았기에, 밑져야 본전이라는 마음으로 Cursor AI를 통해 KMP와 CMP 프로젝트를 진행해 보았다.&lt;br /&gt;결과는 놀라웠다. 성능은 확실했고, 개발 속도는 비약적으로 상승했다. 그때 깨달았다. 이제는 '얼마나 코드를 잘 짜느냐'만큼이나 'AI를 얼마나 영리하게 활용하느냐'가 개발자의 새로운 핵심 역량이 될 것임을 말이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;'백화점' 속에 '방' 하나를 만든다는 것&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리 팀에서는 AI 주도 개발 프로세스를 도입해 테스트와 디벨롭을 반복하고 있다. 하지만 직접 업무에 적용해 보니 기대만큼 매끄럽지만은 않았다. 가장 큰 장벽은 바로 '컨텍스트(Context)의 부재'였다.&lt;br /&gt;이해를 돕기 위해 비유를 하나 들어보자면, 우리가 거대한 '백화점' 안에 새로운 '방' 하나를 만든다고 가정하는 것이다.&lt;br /&gt;백화점에는 수많은 방이 있고, 각 방의 컨셉과 지어진 시기(Legacy)가 모두 다르다. AI는 비용과 효율 문제로 백화점 전체 구조를 완벽히 파악하지 못한 채, 설계자가 준 정보와 연관되어 보이는 몇 개의 방만을 참고해 새 방을 만든다. 여기서 문제는 '반드시 참고해야 할 방'과 '절대 따라 해서는 안 되는 방'을 구분하는 것은 오직 그 시스템을 꿰뚫고 있는 설계자(개발자)만의 영역이라는 점이다. 설계자의 의도와 맥락을 모르는 AI가 만든 방은 겉보기엔 멀쩡해 보일지 몰라도, 보이지 않는 곳에서 전체 구조를 뒤흔드는 치명적인 결함을 품고 있을 수 있다.&lt;br /&gt;이러한 비유는 실제 개발 현장에서도 그대로 적용된다. 단순히 코드를 생성하는 속도에 감탄하기엔, AI가 놓치는 '우리만의 맥락'이 너무나 많기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AI와 함께하며 마주한 '삐끗'의 순간들&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1f1f1f;&quot;&gt;실제로 AI와 협업하며 겪었던 몇 가지 시행착오를 공유하자고 한다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;피그마 MCP를 활용해 UI 코드를 생성할 때였다. &lt;b&gt;AI는 시각적인 요구사항은 잘 잡아냈지만, 정작 우리가 공들여 구축해둔 사내 디자인 시스템 컴포넌트나 테마 토큰을 적절히 활용하지 못했다.&lt;/b&gt; 결국 생성된 코드를 다시 우리 시스템에 맞게 일일이 리팩터링을 해야 하는 번거로움이 발생했다.&lt;br /&gt;&lt;b&gt;AI는 코딩 기술은 뛰어나지만, 우리 서비스만의 특수한 비즈니스 로직이나 도메인 지식은 알지 못한다.&lt;/b&gt; 특정 로직에서 반드시 지켜야 할 예외 처리를 생략하거나, 반대로 기획서에도 없는 기능을 '창의적'으로 덧붙여 구현해버리는 바람에 디버깅에 더 많은 시간을 쏟기도 했다.&lt;br /&gt;AI가 코드를 짜주는 시간은 짧지만, 그 코드가 안전한지 검토(Confirm)하는 시간은 결코 짧지 않다. &lt;b&gt;명확하지 않은 요구사항은 결국 '잘못된 결과물 &amp;rarr; 긴 검토 시간 &amp;rarr; 재작업'이라는 비효율의 굴레를 만든다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;에이전트 기반의 워크플로우 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 시행착오를 해결하기 위해 우리는 AI를 단순한 챗봇이 아닌, 구조적인 워크플로우 안에서 움직이는 에이전트로 활용하기 시작했다.&lt;br /&gt;우리가 정의한 워크플로우는 다음&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1f1f1f;&quot;&gt;&amp;nbsp;단계를 거친다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1f1f1f;&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;[요구사항 분석 &amp;rarr; 도메인 정의 &amp;rarr; 비즈니스 로직 개발 &amp;rarr; UI 개발 &amp;rarr; 디자인 시스템 개발]&lt;br /&gt;&lt;br /&gt;특히 특정 텍스트나 키워드가 포함될 경우 해당 업무에 특화된 서브 에이전트가 실행되도록 구조화했다.&lt;br /&gt;예를 들어 '디자인 시스템' 관련 트리거가 감지되면, 사내 컨벤션을 완벽히 숙지한 UI 전용 서브 에이전트가 호출된다. 이렇게 역할을 쪼개면 AI가 처리해야 할 정보량이 줄어들어 정확도가 비약적으로 상승한다.&lt;br /&gt;기획서 분석 단계에서 플랫폼 의존성을 제거해 본질적인 비즈니스 로직을 먼저 설계하고, 도메인 개발 시 TDD를 결합해 로직의 무결성을 검증했다. 단단한 도메인 위에 UI를 입히니 디자인 시스템과의 정합성도 훨씬 통제 가능한 범위 안으로 들어왔다.&lt;br /&gt;각 Feature별로 위 과정을 반복하며 개발자의 검수를 거친다. 에이전트가 초안을 잡고 서브 에이전트가 디테일을 채우면, 우리는 마지막 '컨펌'을 통해 전체 프로젝트 구조와의 조화를 확인한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개발자, 작성자에서 '비판적 평론가'로&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대의 개발자에게 필요한 역량은 단순히 프롬프트를 잘 입력하는 기술이 아니다. 전체 서비스의 컨텍스트를 유지하면서, AI가 캐치하지 못하는 디테일을 잡아내는 '비판적 사고'다.&lt;br /&gt;우리는 AI를 100% 신뢰하는 신봉자가 되어서는 안 된다. 오히려 깐깐한 건축 감리사처럼, 혹은 냉철한 평론가처럼 AI가 가져온 구조물과 자재를 하나하나 뜯어봐야 한다. 언젠가는 AI가 서비스 전체의 맥락을 완벽히 이해하는 날이 올지도 모른다. 하지만 지금 이 순간, 확실한 것은 개발자의 역할이 'Code Writer'에서 'Solution Reviewer'로 빠르게 이동하고 있다는 사실이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8개월 차 신입인 나의 생각이 정답은 아닐 수 있다. 시간이 지나면 이 생각 또한 달라질지도 모른다. 하지만 AI로 인해 개발 환경이 빠르게 바뀌는 지금, 한 가지 분명하게 느끼는 점은 있다.&lt;br /&gt;&lt;br /&gt;앞으로 개발자의 경쟁력은 단순히 &lt;b&gt;얼마나 많은 코드를 작성했는가&lt;/b&gt;가 아니라, &lt;b&gt;AI가 만들어낸 결과를 이해하고 검증하며 올바른 방향으로 이끌 수 있는가&lt;/b&gt;에 가까워질 것이라는 점이다.&lt;br /&gt;&lt;br /&gt;돌이켜 보면 요즘 내가 회사에서 하고 있는 일들도 조금씩 그 방향으로 이동하고 있었다. AI 에이전트가 초안을 만들고, 우리는 그 결과를 검토하고 맥락을 보완한다. 도구가 코드를 작성하는 속도는 빨라졌지만, 전체 구조와 의도를 판단하는 역할은 여전히 사람에게 남아 있다.&lt;br /&gt;&lt;br /&gt;그래서인지 요즘은 이런 생각이 든다. 나는 단순히 AI를 활용하는 개발자가 되기보다, &lt;b&gt;AI와 함께 문제를 구조화하고 결과를 검증하는 개발자&lt;/b&gt;가 되어가고 있는 과정에 있는 것 같다는 생각 말이다. 아직 배워야 할 것도 많고 시행착오도 계속될 것이다. 하지만 적어도 지금은, 빠르게 변하는 환경 속에서 &lt;b&gt;조금은 올바른 방향으로 걸어가고 있는 것 같다는 느낌&lt;/b&gt;이 든다.&lt;/p&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/589</guid>
      <comments>https://fre2-dom.tistory.com/589#entry589comment</comments>
      <pubDate>Sat, 21 Feb 2026 15:29:21 +0900</pubDate>
    </item>
    <item>
      <title>2025년 회고</title>
      <link>https://fre2-dom.tistory.com/588</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HpmUt/dJMcahC4Pkx/drxfoBrymZCkJoK2kG4edK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HpmUt/dJMcahC4Pkx/drxfoBrymZCkJoK2kG4edK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HpmUt/dJMcahC4Pkx/drxfoBrymZCkJoK2kG4edK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHpmUt%2FdJMcahC4Pkx%2FdrxfoBrymZCkJoK2kG4edK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;621&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 해는 버티는 것만으로 충분하고, 어떤 해는 달리는 것만으로 숨이 차다. 나에게 2025년은 그 두 가지가 묘하게 섞인, 그러면서도 '확장'이라는 단어가 가장 잘 어울리는 시간이었다. 안드로이드라는 익숙하고도 안전한 울타리를 넘어 플랫폼의 경계를 허무는 법을 배웠고, 결핍 속에서 설계의 본질을 마주했다. 1년이라는 긴 시간 동안 내가 남긴 선명한 궤적들을 문장으로 정리해 본다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;'행운복권' 마이그레이션&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2025년 가장 기억에 남는 사이드 프로젝트는 단연 '행운복권' 서비스의 마이그레이션이었다. 어느 날 마주한 서버의 부재는 개발자에게 마치 산소 공급이 중단된 것과 같은 위기였지만, 나는 이 결핍을 시스템의 자생력을 기르는 기회로 삼기로 했다. 서버 의존성을 과감히 걷어내고, 로컬 DB(Room)와 Alarm Manager를 통해 서비스의 심장을 다시 뛰게 만들었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 나를 구원한 것은 다름 아닌 '클린 아키텍처'였다. 계층이 엄격히 분리되어 있었기에, 핵심 로직이 담긴 도메인 레이어는 건드리지 않은 채 데이터 레이어의 구현체만 교체하는 '우아한 전환'이 가능했다. &quot;좋은 설계는 변화에 유연해야 한다&quot;는 교과서적인 문장이 내 손끝에서 실체화되는 순간, 나는 비로소 아키텍처가 선사하는 미학을 온전히 이해할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2025년 한 해 동안 555명의 새로운 사용자가 앱을 만났고, 누적 다운로드 수 또한 견고하게 쌓여갔다. 기술적인 이식은 성공적이었고, 그 진짜 성적표는 단순히 살아남는 것을 넘어 '자생하는 데이터'로 증명되었다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[비용은 제로, 수익은 온전한 자산으로]&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 고무적인 성과는 수익 구조의 혁신이었다. 서버 유지비라는 고정 지출을 0원으로 완전히 걷어내면서, 발생한 광고 수익은 온전히 서비스의 자산이 되었다. Google AdMob을 통해 2025년 한 해 동안 $42.50(US)의 수익을 창출했으며, 이를 포함한 전체 누적 수익은 약 $128.43(US)에 달했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버 비용에 대한 부담이 사라지자 서비스의 생존 기간은 무한해졌고, 안정적인 수익 모델을 유지하며 다음 단계로 나아갈 동력을 얻었다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[숫자 너머에 담긴 독립의 가치]&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2026년의 문턱에서 마주한 데이터는 더욱 놀라웠다. 1월 3일 기준, 누적 일일 신규 사용자 획득 수가 모든 국가 합산 1,223명에 달했다. 최근 28일간 MAU 43명, 기기 획득 37개라는 지표는 언뜻 작아 보일 수 있지만, 이는 서버 없이 로컬 DB와 &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;Alarm Manager&lt;/span&gt;만으로 이뤄낸 '완전한 독립'의 결과물이라는 점에서 의미가 깊다. 마케팅 비용 한 푼 없이 기술적 최적화만으로 신규 유입을 끌어내고(기기 획득 수 95% 증가) 수익을 만들어내는 구조를 완성했기 때문이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 이번 마이그레이션은 단순히 코드를 옮기는 작업을 넘어, 비즈니스의 지속 가능성을 기술로 증명해낸 시간이었습니다. &quot;서버가 없어서 안 돼&quot;라는 핑계 대신 &quot;서버 없이도 가능한 구조&quot;를 고민했던 그 밤들이, 매일 아침 찍히는 십여 명의 신규 사용자라는 숫자로 보상받는 기분이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;일상 속의 소소한 행운을 전하는 &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;9&quot;&gt;행운복권&lt;/b&gt;이 궁금하시다면 &lt;a href=&quot;https://www.google.com/search?q=https://play.google.com/store/apps/details%3Fid%3Dcom.junjange.lucky_lottery&quot; data-ved=&quot;0CAAQ_4QMahcKEwi-nd-ntvGRAxUAAAAAHQAAAAAQQg&quot; data-hveid=&quot;0&quot;&gt;구글 플레이 스토어&lt;/a&gt;에서 만나 볼 수 있다. 또한, 서버 없는 자생적인 구조를 위해 클린 아키텍처를 어떻게 적용했는지 궁금한 동료 개발자분들을 위해 &lt;a href=&quot;https://www.google.com/search?q=https://github.com/junjange/lucky-lottery-android-v2&quot;&gt;Repository&lt;/a&gt;의 문도 활짝 열어두었다. 작은 아이디어가 기술을 만나 실질적인 수익과 유저의 유입으로 이어지는 여정을 함께 살펴봐 주다면 더할 나위 없는 기쁠 것 같다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;KMP + CMP의 파도를 타다&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 개발자에게 Kotlin은 익숙하고 다정한 모국어와 같다. 하지만 2025년, 나는 이 익숙한 언어를 들고 낯선 영토로 발을 내디뎠다. 바로 Kotlin Multiplatform(KMP)과 Compose Multiplatform(CMP)이라는 도구를 통해서 말이다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[Web과 App, 경계 없는 포트폴리오의 탄생]&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 도전한 것은 나만의 색깔을 담은 웹/앱 통합 포트폴리오 개발이었다. 이전에는 웹을 만들기 위해 JavaScript의 생태계를 새로 익혀야 했지만, KMP는 그 장벽을 단숨에 무너뜨렸다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 코드베이스 위에서 비즈니스 로직을 공유하고, CMP를 통해 UI 레이어까지 통일성 있게 구축했다. 안드로이드에서 짜던 그 감각 그대로 웹 브라우저 위에 화면이 그려지는 순간, 플랫폼이라는 물리적인 제약에서 벗어난 진정한 '자유'를 느꼈다. 덕분에 생산성은 극대화되었고, 나를 표현하는 방식은 더욱 다채로워졌다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 결과 완성된 웹사이트는 &lt;a href=&quot;https://junjange.github.io/&quot;&gt;https://junjange.github.io/&lt;/a&gt; 와 같다. 이 웹사이트는 KMP와 CMP가 어떻게 웹과 앱의 경계를 허물고, 개발자에게 더 넓은 표현의 장을 제공하는지 직접 보여주는 살아있는 증거이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;KMP와 CMP를 활용한 프로젝트 구조나 구현 방식에 대해 궁금한 점이 있으시다면 언제든 &lt;a href=&quot;https://github.com/junjange/junjange.github.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Repository&lt;/a&gt;에 Issue나 Pull Request를 통해 의견을 남겨주면 좋을 것 같다. 동료 개발자분들의 피드백은 나를 더 깊게 고민하게 만드는 가장 좋은 자극제가 된다. 만약 이 프로젝트가 여러분의 멀티플랫폼 여정에 조금이라도 영감이 되었다면, 따뜻한 'Star' 한 방으로 응원해 주시면 큰 힘이 될 것 같다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[Android와 iOS, 하나의 로직으로 숨 쉬는 서비스]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;실제 서비스 개발에서도 KMP의 위력은 대단했다. 그 대표적인 결실이 바로 '링크레터(LinkLetter)'이다. Android와 iOS라는 서로 다른 두 세계를 잇기 위해, 핵심 비즈니스 로직을 담은 Shared Module을 정교하게 설계했다. 네트워크 통신, 데이터 파싱, 그리고 복잡한 도메인 로직을 단 한 번만 작성함으로써 두 플랫폼 사이의 로직 파편화를 완벽히 방지했다. 단순히 로직만 공유하는 것을 넘어 UI까지 CMP로 구성하며, iOS 앱을 개발할 때 느꼈던 생소함을 Kotlin의 익숙함으로 치환했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;플랫폼마다 다른 언어로 같은 기능을 두 번씩 구현하던 비효율의 시대는 지났다. 이제는 &quot;어떤 플랫폼인가&quot;보다 &quot;어떤 가치를 전달할 것인가&quot;에 더 집중할 수 있게 된 것이다. KMP와 CMP는 나에게 단순한 기술 스택 그 이상의 의미, 즉 '세상을 바라보는 더 넓은 시야'를 선물해 주었다. 링크레터의 탄생 과정과 기술적 구조가 궁금하다면 &lt;a href=&quot;https://github.com/junjange/linkletter-client&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Repository&lt;/a&gt;에서 확인 할 수 있다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;함께 숨 쉬며 성장하는 즐거움, 그리고 취업&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;성장이란 결코 혼자서 완성할 수 있는 것이 아님을 잘 안다. 2025년은 동료들과 끊임없이 대화하고, 지식을 나누며, 그 에너지를 실무의 성과로 치환하는 역동적인 시간이었다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[학습의 깊이가 실전의 무기가 되기까지]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;내면의 단단함을 채우는 작업은 쉼 없이 계속되었다. 특히 《코틀린 동시성 프로그래밍》을 읽으며 Coroutine의 내부 구조와 비동기 처리의 정수를 익혔다. 책장 속의 이론은 현장에서 강력한 무기가 되었다. 복잡한 비동기 로직이 얽힌 트러블 슈팅 상황에서, 원인을 정확히 짚어내고 해결하는 과정은 단순한 코딩을 넘어 '엔지니어링'의 즐거움을 깨닫게 해주었다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[커뮤니티에서 나눈 온기]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;기술은 결국 사람을 향한다는 믿음으로 커뮤니티에 몸을 던졌다. 드로이드나이츠(DroidKnights) 컨트리뷰터로 활동하며 보이지 않는 곳에서의 헌신이 생태계를 어떻게 지탱하는지 배웠고, GDG Korea Android와 유스콘(YouthCon) 등 수많은 컨퍼런스에서 만난 동료들의 눈빛은 나를 늘 깨어 있게 만들었다. 참여자에서 기여자로, 다시 학습자로 순환하는 이 과정이 나를 지치지 않게 하는 연료가 되었다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[페이타랩에서 증명한 '진짜 개발'의 시간]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;이러한 치열한 기록들은 마침내 '취업'이라는 결실로 이어졌다. 이제는 사이드 프로젝트를 넘어 &lt;b&gt;'패스오더'&lt;/b&gt;라는 실전 무대에서 비즈니스의 성장을 직접 견인하고 있다. 단순히 기획안을 코드로 옮기는 것을 넘어, 기술이 어떻게 매출과 사용자 유입의 마중물이 될 수 있을지 끊임없이 고민했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;비즈니스의 폭발적 성장을 지원하는 '선물하기 지원금' 기능을 개발했다.&lt;/b&gt; 레거시 코드의 복잡함 속에서도 신규 기능을 안정적으로 안착시켰다. 배포 후 &lt;b data-index-in-node=&quot;75&quot; data-path-to-node=&quot;5,0,0&quot;&gt;일일 평균 선물 발송 건수가 약 398% 급증&lt;/b&gt;하고, 수신자가 서비스에 안착하며 &lt;b data-index-in-node=&quot;119&quot; data-path-to-node=&quot;5,0,0&quot;&gt;신규 가입자가 약 567% 증가&lt;/b&gt;하는 드라마틱한 지표를 목도했다. 기술이 비즈니스의 폭발적 성장을 뒷받침할 때의 전율은 무엇과도 바꿀 수 없는 소중한 경험이었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;UX의 디테일로 결제의 허들을 낮춘 'OCR 및 법인카드 결제' 기능을 개발했다.&lt;/b&gt; 결제의 진입장벽을 낮추기 위해 OCR 카드 등록 프로세스를 고도화했다. 스캔 결과 확인 화면에 도달한 유저의 &lt;b&gt;등록 완료율 87%&lt;/b&gt;라는 높은 전환율을 기록하며, UX의 미세한 디테일이 비즈니스 지표에 미치는 영향을 데이터로 확인했다. 더불어 &lt;b data-index-in-node=&quot;178&quot; data-path-to-node=&quot;5,1,0&quot;&gt;법인카드 결제 기능&lt;/b&gt;을 성공적으로 추가함으로써 서비스의 결제 스펙트럼을 넓히고, B2B 유저들의 실질적인 니즈까지 충족시킬 수 있었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;팀의 생산성을 위한 '자체 MVI 아키텍처' 도입했다. &lt;/b&gt;외부 라이브러리에 의존하지 않고 우리 팀에 최적화된 MVI 구조를 직접 설계하고 적용했다. 이를 통해 코드의 유지보수성을 높였을 뿐만 아니라, 팀 내 코드 이해도를 상향 평준화하여 협업의 효율을 한 단계 끌어올리는 토대를 마련했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,3,0&quot;&gt;신뢰를 지키는 '선제적 리스크 관리'와 대응했다.&lt;/b&gt; 사용자 경험의 단절을 막기 위해 노력했다. 네트워크 에러로 결제 수단이 노출되지 않는 상황에서 '재시도 다이얼로그' 표시를 제안하고 직접 구현하여 사용자 이탈을 최소화했다. 또한, 까다로운 구글 정책 리젝 상황에서도 신속한 정책 검토와 대응으로 배포 지연을 막아내며 서비스의 연속성을 견고히 지켜냈다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2026년 다시, 선을 긋는 마음으로&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;지난해가 '확장'의 해였다면, 다가올 2026년은 그 확장의 깊이를 더하고 울림을 만드는 해로 정의하고 싶다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KMP + CMP 마스터로 거듭나겠다.&lt;/b&gt; 이제 '사용해 봤다'는 수준을 넘어, 멀티플랫폼 환경에서의 성능 최적화와 아키텍처 패턴을 나만의 방식으로 정립하려 한다. 특히 2025년 로컬 마이그레이션을 성공적으로 마친 &lt;b data-index-in-node=&quot;98&quot; data-path-to-node=&quot;6&quot;&gt;'행운복권' 앱을 KMP로 다시 한번 마이그레이션&lt;/b&gt;하여 기술적 완성도를 높이고, 이 외에도 꾸준히 운영을 이어갈 새로운 KMP 기반 서비스를 런칭할 계획이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공유의 가치를 실현하는 Speaker에 도전하겠다.&lt;/b&gt; 그동안 컨퍼런스 객석에서 누군가의 성공과 실패를 경청해 왔다면, 이제는 내가 무대 위에 서려 한다. 지난 유스콘에서 자신의 고민과 시행착오를 당당하게 펼쳐 보이던 동료이자 친구들의 모습은 나에게 말로 다 못 할 깊은 영감을 주었다. 그 뜨거웠던 자극을 발판 삼아, 2026년에는 '듣는 사람'에서 '나누는 사람'으로 거듭나려 한다. 아직은 어떤 주제로 첫 무대에 서게 될지 고민하는 단계이지만, 나의 시행착오가 누군가에게는 이정표가 될 수 있도록 진솔한 기록을 쌓아가겠다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시야의 완성을 위해 백엔드와 개발 철학을 다지겠다.&lt;/b&gt; 클라이언트를 넘어 서비스의 전체적인 흐름을 이해하기 위해 백엔드 학습을 병행하려고 한다. 필요하면 직접 API를 설계하고 구축하며 비즈니스 전체를 조망할 수 있는 'T자형 인재'로 거듭나고자 한다. 더불어 다양한 기술 서적을 통해 개발자로서의 철학을 공고히 다지는 시간도 게을리하지 않을 것이다.&lt;/p&gt;</description>
      <category>Memoir</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/588</guid>
      <comments>https://fre2-dom.tistory.com/588#entry588comment</comments>
      <pubDate>Sun, 4 Jan 2026 17:47:46 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 신입 개발자의 시선에서 본 테스트 코드</title>
      <link>https://fre2-dom.tistory.com/587</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boQahb/dJMcagxmtgV/iNhAyjKmLFzAae4N9Z5xU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boQahb/dJMcagxmtgV/iNhAyjKmLFzAae4N9Z5xU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boQahb/dJMcagxmtgV/iNhAyjKmLFzAae4N9Z5xU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboQahb%2FdJMcagxmtgV%2FiNhAyjKmLFzAae4N9Z5xU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;471&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개발자들에게 테스트 코드에 대해 물어보면, 의외로 비슷한 대답이 돌아온다.&lt;br /&gt;&amp;ldquo;우리 회사에는 테스트 코드가 없다&amp;rdquo;는 말이다.&lt;/p&gt;
&lt;p data-end=&quot;311&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;이유는 제각각이다.&lt;br /&gt;필요성을 느끼지 못해서라는 사람도 있고, 바쁘다 보니 작성할 여유가 없었다는 사람도 있다.&lt;br /&gt;솔직히 말하자면, 나 역시 한동안은 후자에 가까웠다.&lt;/p&gt;
&lt;p data-end=&quot;499&quot; data-start=&quot;313&quot; data-ke-size=&quot;size16&quot;&gt;입사 초기에 팀에는 안드로이드 개발자 한 분만이 &lt;b&gt;중요하다고 판단되는 로직&lt;/b&gt;에 한해서 테스트 코드를 작성해두고 있었다. 다만 내가 담당했던 Feature 영역에서는 Repository, UseCase, ViewModel 등 중 어느 것도 &amp;ldquo;반드시 테스트가 필요하다&amp;rdquo;고 느껴지지 않았다. 그 당시의 나는 그렇게 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;765&quot; data-start=&quot;501&quot; data-ke-size=&quot;size16&quot;&gt;생각이 바뀌게 된 계기는, 기존에 작성된 코드를 기반으로 새로운 기능을 구현하게 되었을 때였다.&lt;br /&gt;Feature 하나를 완성하기 위해서는 과거 로직의 의도와 흐름을 먼저 이해해야 했다. 문제는 그 과정이 생각보다 훨씬 오래 걸렸다는 점이다. 관련된 Context를 하나씩 따라가다 보니, 객체와 함수 내부에는 복잡한 계산 로직이 얽혀 있었고, 이를 설명해주는 문서나 주석도 충분하지 않았다. 결국 디버깅을 반복하며 직접 흐름을 추적하는 것이 가장 빠른 방법이 되어버렸다.&lt;/p&gt;
&lt;p data-end=&quot;937&quot; data-start=&quot;767&quot; data-ke-size=&quot;size16&quot;&gt;그때 처음으로 이런 생각이 들었다.&lt;br /&gt;&lt;b&gt;&amp;ldquo;테스트 코드가 있었다면, 이 객체가 어떤 입력을 받아 어떤 결과를 기대하는지 훨씬 빨리 알 수 있었을 텐데.&amp;rdquo;&lt;/b&gt;&lt;br /&gt;성공 케이스와 실패 케이스, 그리고 예외 상황이 테스트로 정리돼 있었다면, 코드를 &amp;lsquo;이해하기 위해&amp;rsquo; 쓰는 시간은 지금보다 훨씬 줄었을 것이다.&lt;/p&gt;
&lt;p data-end=&quot;1081&quot; data-start=&quot;939&quot; data-ke-size=&quot;size16&quot;&gt;코드를 이해한 뒤 기능 구현을 마치고 QA 단계에 들어갔다.&lt;br /&gt;요구사항을 충실히 반영해 개발을 마쳤다고 생각했지만, QA 과정에서 내가 미처 고려하지 못했던 예외 케이스들이 하나둘씩 드러나기 시작했다. 요구사항의 경계에 놓인 애매한 상황도 있었고, 일부 동작은 애초에 명확하게 정의되지 않은 상태였다는 사실도 그때서야 알게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;1249&quot; data-start=&quot;1083&quot; data-ke-size=&quot;size16&quot;&gt;해당 Feature를 잘 알고 있을 것이라 생각했던 개발자나 유관자들에게 물어봤지만, 명확한 답을 얻기는 어려웠다. 문서로 남아 있지 않은 영역이다 보니, 각자의 기억과 해석이 조금씩 달랐다. 결국 CTO님께 직접 이전 요구사항을 여쭤보며 맥락을 다시 정리했고, 그 과정을 문서로 남기기 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;1311&quot; data-start=&quot;1251&quot; data-ke-size=&quot;size16&quot;&gt;요구사항이 정리되고 나니, 다음에 해야 할 일도 자연스럽게 보였다.&lt;/p&gt;
&lt;p data-end=&quot;1311&quot; data-start=&quot;1251&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;이건 테스트로 남겨야겠다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1508&quot; data-start=&quot;1313&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 나는 기능 구현이 끝난 뒤 테스트 코드를 작성했다.&lt;br /&gt;단순히 정상 동작만 검증하는 테스트가 아니라, QA 과정에서 드러났던 예외 상황과 애매한 경계 조건들을 하나씩 코드로 고정했다. 이 테스트들은 단지 품질을 보장하기 위한 도구가 아니라, &lt;b&gt;이 Feature가 어떤 전제를 가지고 설계되었는지를 가장 정확하게 설명해주는 문서&lt;/b&gt;가 되었다.&lt;/p&gt;
&lt;p data-end=&quot;1629&quot; data-start=&quot;1510&quot; data-ke-size=&quot;size16&quot;&gt;이 경험 이후로, 테스트 코드는 더 이상 &amp;ldquo;여유가 있을 때 작성하는 것&amp;rdquo;이 아니게 되었다.&lt;br /&gt;테스트는 미래의 나를 위한 기록이자, 언젠가 이 코드를 마주할 누군가를 위한 최소한의 배려라는 생각이 들었기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;1782&quot; data-start=&quot;1631&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 비즈니스 로직처럼 여러 입력과 조건을 통해 결과가 도출되는 코드에 대해서는 테스트 작성을 기본으로 가져가려 한다. 또한 Hotfix로 배포되는 수정 사항은, 우리가 개발이나 QA 과정에서 놓쳤던 부분이라는 의미이기도 하기에, 반드시 테스트 코드로 남길 계획이다.&lt;/p&gt;
&lt;p data-end=&quot;1866&quot; data-start=&quot;1784&quot; data-ke-size=&quot;size16&quot;&gt;이 기준을 실제로 실천해보고, 분명한 효과가 나타난다면 개인의 습관에 그치지 않고 팀의 규칙으로 이어질 수 있도록 적극적으로 공유해볼 생각이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5-2&quot; data-message-id=&quot;e1fc788c-8353-4855-ba8f-5796c91d9402&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2065&quot; data-start=&quot;2020&quot; data-ke-size=&quot;size16&quot;&gt;신입 개발자로서 테스트 코드를 바라보는 나의 시선은, 그렇게 조금씩 바뀌고 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/587</guid>
      <comments>https://fre2-dom.tistory.com/587#entry587comment</comments>
      <pubDate>Sun, 28 Dec 2025 16:55:11 +0900</pubDate>
    </item>
    <item>
      <title>프로세스 실종 사건 - Activity 스택이 사라졌다.</title>
      <link>https://fre2-dom.tistory.com/582</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRN7zQ/btsQ8WFaIhu/Klc7VGphBhxSWQWXmoj0Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRN7zQ/btsQ8WFaIhu/Klc7VGphBhxSWQWXmoj0Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRN7zQ/btsQ8WFaIhu/Klc7VGphBhxSWQWXmoj0Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRN7zQ%2FbtsQ8WFaIhu%2FKlc7VGphBhxSWQWXmoj0Vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사라진 결제의 행방을 추적하라&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사건은 이러하다.&lt;br /&gt;그날도 평소처럼 평화로운 오후였다.&lt;br /&gt;사용자는 결제 화면에서 신중하게 결제 수단을 고르고, 한껏 기대에 부풀어 &amp;lsquo;결제하기&amp;rsquo; 버튼을 눌렀다.&lt;br /&gt;이제 곧 결제가 완료되고, 상품권이 발송될 터였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 그때였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은행 앱으로 연결된 순간, 우리의 앱은 잠시 무대 뒤로 퇴장했다.&lt;br /&gt;잠시면 돌아올 줄 알았다. 하지만 그것은&lt;b&gt;&lt;span&gt; 그의 마지막 기억&lt;/span&gt;&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 은행 앱에서 오랜 시간 머물렀다.&lt;br /&gt;카드를 선택하고, 비밀번호를 입력하고, 그러나 마지막 순간에 결제를 취소했다.&lt;br /&gt;그리고 돌아왔을 때, 앱은 여전히 그 자리에 있었다.&lt;/p&gt;
&lt;p data-end=&quot;486&quot; data-start=&quot;363&quot; data-ke-size=&quot;size16&quot;&gt;하지만 무언가 달랐다.&lt;br /&gt;화면은 익숙했지만, &lt;b&gt;기억은 사라져 있었다.&lt;/b&gt;&lt;br /&gt;결제 금액도, 선택한 선물 정보도, 입력했던 메시지도 모두 흔적이 없었다.&lt;br /&gt;마치 새로 설치한 앱처럼, &lt;b&gt;모든 것이 처음부터였다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;551&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;그의 화면에는 아무것도 남지 않았다.&lt;br /&gt;결제는 하지도 못했고, 지금껏 입력해온 모든 정보까지 잃어버렸다.&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;700&quot; data-ke-size=&quot;size16&quot;&gt;나는 한동안 멍하니 로그를 바라봤다.&lt;br /&gt;&amp;lsquo;분명 아무 문제 없었는데... 도대체 왜?&amp;rsquo;&lt;br /&gt;그 순간, 나는 확신했다.&lt;br /&gt;이건 단순한 &amp;lsquo;버그&amp;rsquo;가 아니다.&lt;br /&gt;&lt;b&gt;프로세스 실종 사건&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;첫 번째 수사 - 프로세스는 왜 사라졌는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사건 현장은 너무도 깨끗했다.&lt;br /&gt;마치 누군가 일부러 흔적을 지운 듯했다.&lt;br /&gt;로그조차 남지 않은 화면, 앱은 조용히 사라졌고, 스택에는 그 어떤 기록도 없었다.&lt;/p&gt;
&lt;p data-end=&quot;399&quot; data-start=&quot;278&quot; data-ke-size=&quot;size16&quot;&gt;나는 잠시 숨을 고르고, 눈앞의 정적을 바라봤다.&lt;br /&gt;&lt;b&gt;&amp;ldquo;이건 단순한 크래시가 아니다.&amp;rdquo;&lt;/b&gt;&lt;br /&gt;이상할 만큼 조용했다.&lt;br /&gt;예외도, 로그캣도, 트레이스도 남지 않았다.&lt;br /&gt;마치 앱이 스스로 조용히 퇴장한 듯했다.&lt;/p&gt;
&lt;p data-end=&quot;474&quot; data-start=&quot;401&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;혹시... 프로세스가 메모리에서 제거된 건 아닐까?&amp;rdquo;&lt;br /&gt;직감이었다.&lt;br /&gt;그 순간, 머릿속에서 수많은 가능성이 스쳐 지나갔다.&lt;/p&gt;
&lt;p data-end=&quot;696&quot; data-start=&quot;562&quot; data-ke-size=&quot;size16&quot;&gt;앱이 메모리에서 제거되는 이유는 다양하다.&lt;br /&gt;너무 오래 다른 앱(은행 앱)에 머물렀거나,&lt;br /&gt;백그라운드에서 시스템이 메모리를 회수했거나,&lt;br /&gt;혹은 내부 어딘가에서 &lt;b&gt;메모리릭&lt;/b&gt;이 누적되어&lt;br /&gt;스스로를 조금씩 갉아먹은 것일 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;740&quot; data-start=&quot;698&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이건 어디까지나 추측이었다.&lt;br /&gt;&lt;b&gt;우리에게 필요한 건, 증거다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-end=&quot;740&quot; data-start=&quot;698&quot; data-ke-size=&quot;size16&quot;&gt;나는 재현에 돌입했다.&lt;br /&gt;긴 체류, 화면 전환, 메모리 압박.&lt;br /&gt;은행 앱으로 넘어갔다가 되돌아오기,&lt;br /&gt;다른 앱을 여러 번 실행하고 복귀하기,&lt;br /&gt;시스템 자원을 극한까지 몰아붙이기.&lt;/p&gt;
&lt;p data-end=&quot;893&quot; data-start=&quot;857&quot; data-ke-size=&quot;size16&quot;&gt;그리고, 몇 차례의 시도 끝에 마침내 그 순간이 찾아왔다.&lt;/p&gt;
&lt;p data-end=&quot;948&quot; data-start=&quot;766&quot; data-ke-size=&quot;size16&quot;&gt;앱 프로세스가 메모리에서 제거되는 순간이었다.&lt;br /&gt;화면은 고요했지만, 내부에서는 모든 Activity 스택이 사라졌다.&lt;br /&gt;ViewModel은 더 이상 살아 있지 않았고, Repository는 초기화되었으며,&lt;br /&gt;마지막 남은 데이터는 한 줌의 메모리 흔적만 남기고 증발했다.&lt;/p&gt;
&lt;p data-end=&quot;1003&quot; data-start=&quot;950&quot; data-ke-size=&quot;size16&quot;&gt;그제야 사건의 첫 퍼즐이 맞춰졌다.&lt;br /&gt;&lt;b&gt;범인은 바로, 시스템의 &amp;lsquo;프로세스 킬&amp;rsquo;이었다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;두 번째 수사 - 복구되지 않은 기억의 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 사라졌다는 사실은 분명했다.&lt;br /&gt;하지만 사건은 거기서 끝나지 않았다.&lt;br /&gt;죽음은 언제나 복구의 시작이어야 한다.&lt;/p&gt;
&lt;p data-end=&quot;635&quot; data-start=&quot;551&quot; data-ke-size=&quot;size16&quot;&gt;앱이 다시 실행되고, 사용자가 다시 돌아왔다면 무언가 남아 있어야 했다.&lt;br /&gt;결제 정보든, 상품권 정보든, 최소한 &amp;ldquo;여기까지 했었다&amp;rdquo;는 흔적이라도.&lt;/p&gt;
&lt;p data-end=&quot;748&quot; data-start=&quot;637&quot; data-ke-size=&quot;size16&quot;&gt;그러나 돌아온 화면은 텅 비어 있었다.&lt;br /&gt;모든 데이터는 사라지고, 앱은 마치 처음 보는 사람처럼 나를 맞이했다.&lt;/p&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;나는 복구되지 않은 이유를 차분히 정리했다.&lt;br /&gt;&lt;b&gt;상태는 저장되지 않았고, 데이터는 기억되지 않았으며, 복원할 단서조차 존재하지 않았다.&lt;/b&gt;&lt;br /&gt;프로세스가 죽는 건 시스템의 일이라 해도, 그 이후의 복구는 개발자의 책임이었다.&lt;/p&gt;
&lt;p data-end=&quot;1134&quot; data-start=&quot;1090&quot; data-ke-size=&quot;size16&quot;&gt;그 공백을 메우지 못한다면, 사용자는 매번 처음부터 다시 시작해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1134&quot; data-start=&quot;1090&quot; data-ke-size=&quot;size16&quot;&gt;앱에게 있어 &lt;b&gt;죽음은 피할 수 없지만, 망각은 막을 수 있다.&lt;/b&gt;&lt;br /&gt;이제 사건의 본질은 드러났다.&lt;br /&gt;&lt;b&gt;범인은 시스템이었고, 방조자는 복구 로직의 부재였다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1354&quot; data-start=&quot;1235&quot; data-ke-size=&quot;size16&quot;&gt;다음 수사에서는, 이 사라진 기억을 되살리기 위한 &lt;br /&gt;&lt;b&gt;SavedStateHandle&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Repository 캐싱&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Activity Bundle 저장&lt;/b&gt;에 대해서 본격적으로 알아보자.&lt;/p&gt;
&lt;h2 data-end=&quot;202&quot; data-start=&quot;170&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;세 번째 수사 - 사라진 기억을 되살리는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;299&quot; data-start=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;사건의 전모는 드러났다.&lt;br /&gt;프로세스는 시스템에 의해 사라졌고,&lt;br /&gt;복구는 준비되지 않았다.&lt;br /&gt;이제 남은 건 단 하나, &lt;b&gt;사라진 기억을 되살리는 기술&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;299&quot; data-start=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;나는 먼저, 살아남은 기록들을 검토했다.&lt;br /&gt;로그의 틈새에서 희미하게 남은 흔적들.&lt;br /&gt;onSaveInstanceState()가 호출되지 않은 순간,&lt;br /&gt;메모리에서 증발한 ViewModel의 상태.&lt;/p&gt;
&lt;p data-end=&quot;483&quot; data-start=&quot;469&quot; data-ke-size=&quot;size16&quot;&gt;결국 결론은 하나였다.&lt;/p&gt;
&lt;blockquote data-end=&quot;518&quot; data-start=&quot;484&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;518&quot; data-start=&quot;486&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;죽음을 막을 수 없다면, 복구를 설계해야 한다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;596&quot; data-start=&quot;520&quot; data-ke-size=&quot;size16&quot;&gt;복구는 기적이 아니다.&lt;br /&gt;정확한 구조, 명확한 책임 분리,&lt;br /&gt;그리고 데이터를 &lt;b&gt;어디에, 어떻게 남겨둘 것인가&lt;/b&gt;에 달려 있었다.&lt;/p&gt;
&lt;h3 data-end=&quot;588&quot; data-start=&quot;545&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. onSavedStateHandle &amp;mdash; 마지막 유언을 남기는 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;784&quot; data-start=&quot;687&quot; data-ke-size=&quot;size16&quot;&gt;ViewModel이 살아 있는 동안, UI 상태를 실시간으로 저장해둘 수 있다.&lt;br /&gt;프로세스가 사라지더라도, 시스템이 저장한 상태는 다음 실행 시 그대로 복원된다.&lt;/p&gt;
&lt;p data-end=&quot;838&quot; data-start=&quot;747&quot; data-ke-size=&quot;size16&quot;&gt;SavedStateHandle은 앱의 &lt;b&gt;단기 기억&lt;/b&gt;이다.&lt;br /&gt;잠시 백그라운드로 나갔다 돌아와도, 사용자가 마지막으로 머물던 위치와 입력값을 기억한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760447080841&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PaymentViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    var inputText = savedStateHandle.getStateFlow(&quot;inputText&quot;, &quot;&quot;)
    
    fun onTextChanged(value: String) {
        savedStateHandle[&quot;inputText&quot;] = value
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1097&quot; data-start=&quot;1047&quot; data-ke-size=&quot;size16&quot;&gt;비록 짧은 기억이지만, 이것만으로도 사용자는 &amp;ldquo;앱이 나를 기억하고 있다&amp;rdquo;는 신뢰를 느낀다.&lt;/p&gt;
&lt;h3 data-end=&quot;1142&quot; data-start=&quot;1104&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Repository 캐싱 - 데이터의 은신처를 만들어라&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1256&quot; data-start=&quot;1144&quot; data-ke-size=&quot;size16&quot;&gt;하지만 모든 상태를 메모리에만 의존할 순 없다.&lt;br /&gt;ViewModel은 죽으면 기억도 함께 사라진다.&lt;br /&gt;따라서 &lt;b&gt;Repository&lt;/b&gt; 레이어에서 중요한 데이터는 미리 &lt;b&gt;캐싱&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1335&quot; data-start=&quot;1258&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Room&lt;/b&gt;, &lt;b&gt;SharedPreferences&lt;/b&gt;, &lt;b&gt;DataStore&lt;/b&gt;&lt;br /&gt;이름은 달라도 역할은 같다.&lt;br /&gt;데이터를 잠시 숨겨두는 은신처다.&lt;/p&gt;
&lt;p data-end=&quot;1415&quot; data-start=&quot;1337&quot; data-ke-size=&quot;size16&quot;&gt;앱이 다시 살아날 때, Repository는 &amp;ldquo;이전에 봤던 기억&amp;rdquo;을 복원시킨다.&lt;br /&gt;사용자는 아무 일 없었던 것처럼 결제를 이어간다.&lt;/p&gt;
&lt;h3 data-end=&quot;1455&quot; data-start=&quot;1422&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Activity Bundle - 마지막 보호막&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 복구 시나리오의 최후 보루는 Activity의 &lt;b&gt;Bundle&lt;/b&gt;이다.&lt;br /&gt;onSaveInstanceState()에 꼭 필요한 데이터를 담아두면,&lt;br /&gt;Activity가 재생성될 때 자동으로 복원된다.&lt;/p&gt;
&lt;p data-end=&quot;1692&quot; data-start=&quot;1648&quot; data-ke-size=&quot;size16&quot;&gt;이건 일종의 보험이다.&lt;br /&gt;&lt;b&gt;&amp;ldquo;혹시 모를 사고에 대비해 남겨두는 복구 파일.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760447053793&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun onSaveInstanceState(outState: Bundle) {
    outState.putString(&quot;paymentId&quot;, paymentId)
    super.onSaveInstanceState(outState)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1818&quot; data-start=&quot;1780&quot; data-ke-size=&quot;size16&quot;&gt;짧은 코드 한 줄이지만,&lt;br /&gt;그 한 줄이 &lt;b&gt;사용자 경험 전체를 지켜낸다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;수사 종결 보고서 - 프로세스의 죽음이 남긴 것&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2064&quot; data-start=&quot;1949&quot; data-ke-size=&quot;size16&quot;&gt;나는 이 세 가지 방법을 종합해 결론을 내렸다.&lt;br /&gt;&lt;b&gt;복구의 기술은 곧 신뢰의 기술이다.&lt;/b&gt;&lt;br /&gt;앱은 언제든 죽을 수 있다.&lt;br /&gt;하지만 사용자는 그 죽음을 알아차려서는 안 된다.&lt;br /&gt;자연스럽게 이어지는 경험, 그것이 복구의 완성이다.&lt;/p&gt;
&lt;p data-end=&quot;810&quot; data-start=&quot;734&quot; data-ke-size=&quot;size16&quot;&gt;그날의 사건 이후, 나는 로그창이 아니라 &lt;b&gt;사용자의 여정&lt;/b&gt;을 보기 시작했다.&lt;br /&gt;이제 더 이상 프로세스의 죽음은 두렵지 않다.&lt;/p&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;812&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면, 나는 그 죽음을 &lt;b&gt;디자인할 줄 알게 되었기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;ldquo;&lt;b&gt;프로세스의 실종을 막을 수는 없다.&lt;/b&gt;&lt;br /&gt;하지만 사용자가 그 실종을 느끼지 않게 할 수는 있다.&lt;br /&gt;그것이 바로,&amp;nbsp;&lt;b&gt;개발자가 만들 수 있는 최고의 경험이다&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;.&lt;/span&gt;&lt;/b&gt;&amp;rdquo;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 개발탐정 코난의 사건노트 중에서 -&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/582</guid>
      <comments>https://fre2-dom.tistory.com/582#entry582comment</comments>
      <pubDate>Fri, 19 Sep 2025 19:21:06 +0900</pubDate>
    </item>
    <item>
      <title>Jetpack Compose 환경에서 ExoPlayer 최적화</title>
      <link>https://fre2-dom.tistory.com/581</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIKgnx/btsPTysWEPh/mdqOxm5kmOKZkUbnnmvkK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIKgnx/btsPTysWEPh/mdqOxm5kmOKZkUbnnmvkK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIKgnx/btsPTysWEPh/mdqOxm5kmOKZkUbnnmvkK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIKgnx%2FbtsPTysWEPh%2FmdqOxm5kmOKZkUbnnmvkK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Jetpack Compose로 동영상 UI를 만들기 시작했을 때는 단순히 화면에 영상만 잘 나오면 된다고 생각했다. 그러나 구현을 조금씩 구체화할수록, 예상하지 못했던 문제들이 하나둘씩 고개를 들기 시작했다.&lt;br&gt;특히 요즘 유행하는 세로형 숏폼 콘텐츠 앱을 만들다 보면 이 어려움을 누구나 실감할 것이다. 사용자는 화면을 끊김 없이 스크롤하고 싶어 하고, 개발자는 각 화면마다 ExoPlayer를 안정적으로 관리해야 한다. 동시에 메모리 누수 없이 플레이어를 재활용하는 과제까지 따라붙는다.&lt;br&gt;결국 세 가지 조건이 충돌한다.&lt;br&gt;스크롤은 매끄러워야 하고, 영상은 자연스럽게 재생돼야 하며, 앱은 절대 크래시가 나면 안 된다. 문제는 이 세 가지가 서로 얽혀 있다는 점이다. 하나를 잡으면 다른 하나가 삐걱거리는 식으로 개발자를 끊임없이 시험한다.&lt;br&gt;“이 복잡한 퍼즐을 어떻게 풀어야 할까?”&lt;br&gt;이번 글은 Jetpack Compose와 ExoPlayer 경험이 있는 개발자에게 특히 유용하다.&lt;br&gt;단순한 구현 방법이나 코드 스니펫을 넘어, &lt;b&gt;컴퓨터 과학 원리와 소프트웨어 설계 지식을 실제 프로젝트에 적용해 문제를 해결하는 과정&lt;/b&gt;을 중심으로 다룬다.&lt;br&gt;메모리 관리, 객체 재사용, 리소스 최적화 같은 주제를 CS적 관점에서 접근하며, 실제 성능 개선과 안정적인 사용자 경험을 동시에 달성한 전략을 공유하고자 한다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ExoPlayer 구현은 쉬웠다. 진짜 문제는 그다음이었다. &lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;프로젝트는 Jetpack Compose 기반으로 구성되어 있었고, 이 안에서 .m3u8 형식의 HLS 스트리밍 영상을 재생해야 했다.&lt;br&gt;Media3의 ExoPlayer는 안드로이드에서 가장 널리 사용되는 미디어 플레이어이며, Compose 환경에서도 비교적 수월하게 적용할 수 있다는 점에서 자연스러운 선택이었다. 실제로 첫 구현까지는 큰 어려움 없이 진행됐다.&lt;br&gt;하지만 문제는 그 이후였다.&lt;br&gt;실제 화면 구성을 얹고, 사용자 상호작용이 개입되기 시작하면서 예상치 못한 여러 가지 복잡한 이슈들이 모습을 드러냈다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;페이지마다 ExoPlayer를 생성했을 때 드러난 문제&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;우리는 세로로 스크롤되는 VerticalPager 안에 동영상을 배치했다.&lt;br&gt;페이지 단위로 콘텐츠가 구성되어 있었기에, 처음에는 각 페이지마다 ExoPlayer 인스턴스를 별도로 생성하는 방식이 가장 자연스러워 보였다.&lt;br&gt;그러나 곧 한계가 드러났다.&lt;br&gt;스크롤이 발생할 때마다 새로운 인스턴스를 만들고 초기화하다 보니, 영상은 매번 끊기고 페이지 전환 시 렌더링도 매끄럽지 않았다. 더 큰 문제는 메모리 사용량이 예상보다 빠르게 늘어났다는 점이었다.&lt;br&gt;결국 이 구조는 ‘돌아가긴 하지만’, 성능과 사용자 경험 모두에 뚜렷한 불편을 남겼다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. AndroidView와 Recomposition에서 리소스가 정리되지 않는 문제&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;Compose에서 'PlayerView'와 같은 기존 Android View를 사용하려면 'AndroidView'를 활용하게 된다.&lt;br&gt;그런데 Compose의 Recomposition 과정에서 AndroidView 내부 뷰나 연결된 리소스가 예상대로 해제되지 않는 경우가 종종 발생한다.&lt;br&gt;예를 들어, 화면 전환이나 상태 변경으로 구성 요소가 다시 그려질 때, 기존 ExoPlayer 인스턴스나 PlayerView가 메모리에 남아 있는 사례가 발견됐다.&lt;br&gt;이 구조를 그대로 유지하면, 화면을 몇 번 스크롤한 뒤 메모리 사용량이 급격히 늘어나고, GC(Garbage Collection)가 개입하지 않으면 OOM(Out of Memory) 상황으로 이어질 수 있다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 실제로 OOM까지&amp;nbsp;이어졌다.&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;이 상황이 반복되면서 일부 디바이스에서는 앱이 강제 종료되거나, 메모리 부족으로 인해 재생 중 오류가 발생하는 현상이 나타났다.&lt;br&gt;특히 단말 성능이 낮거나, 장시간 여러 영상을 탐색한 경우 문제는 더 빈번하게 발생했다.&lt;br&gt;이는 단순한 버벅임이나 끊김을 넘어, 영상 재생이라는 핵심 기능 자체를 위협하는 심각한 이슈였다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;하나의 ExoPlayer를 공유하는 구조로&amp;nbsp;전환&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;이 문제를 해결하기 위해, ExoPlayer 인스턴스를 싱글톤 형태로 관리하는 구조를 도입했다.&lt;br&gt;Jetpack Compose 환경에 맞춰 공유 가능한 뷰 모델이나 상태 관리 구조와 연결하고, 'PlayerView'는 필요에 따라 특정 영상과 연결하거나 해제하는 방식으로 전환했다.&lt;br&gt;이 구조로 전환하면서 다음과 같은 이점을 얻을 수 있었다.&lt;br&gt;&lt;br&gt;- 동시에 재생 가능한 영상 수를 자연스럽게 제한할 수 있었다.&lt;br&gt;- 매번 새로운 플레이어를 생성하고 해제하는 과정에서 발생하던 GC 오버헤드가 크게 줄었다.&lt;br&gt;- 불필요한 Recomposition에 의한 리소스 누수도 함께 해결되었다.&lt;br&gt;결과적으로, 메모리 사용량이 안정화되었고, 장시간 사용하거나 스크롤이 잦은 환경에서도 영상 재생 품질을 유지할 수 있었다.&lt;br&gt;하지만 하나의 플레이어만 사용하는 구조가 항상 최선은 아니었다.&lt;br&gt;영상 플레이어를 어떻게 관리할지에 대한 근본적인 고민이 필요했고, 우리는 이후 ‘단일 플레이어 구조’와 ‘다중 플레이어 구조’ 각각의 장단점을 비교 분석하는 과정에 들어갔다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단일 vs 다중 ExoPlayer, 어떤 구조가&amp;nbsp;좋을까?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;ExoPlayer 최적화를 고민할 때 가장 먼저 마주한 질문은 “모든 영상에 하나의 플레이어를 쓸 것인가, 아니면 영상마다 별도의 플레이어를 만들 것인가?”였다.&lt;br&gt;각 방식에는 분명한 장단점이 존재했다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;하나의 ExoPlayer를 재사용하는&amp;nbsp;경우&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;메모리를 절약하며 효율적으로 리소스를 관리하는 전략이었다.&lt;br&gt;&lt;b&gt;장점&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;- 메모리 사용량이 크게 줄었다.&lt;br&gt;- 스크롤이 부드럽고 가벼워졌다.&lt;br&gt;- 플레이어 생성 및 초기화에 드는 시간과 리소스가 감소했다.&lt;br&gt;&lt;b&gt;단점&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;- 영상 간 전환 시 플레이어 상태를 다시 맞춰야 하므로 약간의 딜레이가 발생할 수 있었다.&lt;br&gt;- 재생 위치, 볼륨 등 세부 상태를 별도로 관리해야 한다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;각 영상마다 ExoPlayer를 생성하는&amp;nbsp;경우&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;각 영상에 독립적인 플레이어를 할당하는 방식으로, 즉시 전환에 유리하다.&lt;br&gt;&lt;b&gt;장점&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;- 각 영상이 독립적으로 재생되며 서로 간섭하지 않는다.&lt;br&gt;- 영상 전환 시 별도의 초기화 없이 즉시 재생할 수 있다.&lt;br&gt;&lt;b&gt;단점&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;- 플레이어 수가 많아질수록 메모리 사용량이 급증한다.&lt;br&gt;- CPU 사용량도 증가해 앱 성능 저하 위험이 커진다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3개의 ExoPlayer를 돌려 쓰는 ‘트리플’&amp;nbsp;전략&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;우리는 앞서 살펴본 두 방식의 장점을 적절히 결합해, &lt;b&gt;‘3개의 ExoPlayer 인스턴스를 미리 생성하고 순환 재사용하는’ 전략&lt;/b&gt;을 선택했다.&lt;/p&gt;&lt;pre class=&quot;gcode&quot; data-code-block-lang=&quot;kotlin&quot; data-code-block-mode=&quot;2&quot;&gt;&lt;code&gt;val exoPlayerPair = remember {
    Triple(
        ExoPlayer.Builder(context).build(),
        ExoPlayer.Builder(context).build(),
        ExoPlayer.Builder(context).build()
    )
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;Compose의 'remember'를 활용해 최초 한 번만 인스턴스를 생성하고, 이후에는 계속 재사용하도록 구현했다.&lt;br&gt;각 플레이어는 다음과 같이 역할을 분담했다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- 이전 화면 영상 담당&lt;/b&gt;&lt;br&gt;&lt;b&gt;- 현재 화면 영상 담당&lt;/b&gt;&lt;br&gt;&lt;b&gt;- 다음 화면 영상 담당&lt;/b&gt;&lt;br&gt;현재 페이지 인덱스에 따라 적절한 플레이어를 선택하여 사용하고, 나머지 플레이어는 일시 정지시켜 리소스를 효율적으로 관리했다.&lt;/p&gt;&lt;pre class=&quot;maxima&quot; data-code-block-lang=&quot;kotlin&quot; data-code-block-mode=&quot;2&quot;&gt;&lt;code&gt;VerticalPager(
    modifier = Modifier.fillMaxSize(),
    state = pagerState
) { page -&amp;gt;

    val exoPlayer = when (page % 3) {
        0 -&amp;gt; exoPlayerPair.first
        1 -&amp;gt; exoPlayerPair.second
        else -&amp;gt; exoPlayerPair.third
    }

    Box(modifier = Modifier.fillMaxSize()) {
        VideoPlayer(
            exoPlayer = exoPlayer,
            uri = videoUri[page]
        )
        // 추가 UI 구성 요소
    }
}

when (pagerState.currentPage % 3) {
    0 -&amp;gt; {
        exoPlayerPair.first.play()
        exoPlayerPair.second.pause()
        exoPlayerPair.third.pause()
    }

    1 -&amp;gt; {
        exoPlayerPair.first.pause()
        exoPlayerPair.second.play()
        exoPlayerPair.third.pause()
    }

    2 -&amp;gt; {
        exoPlayerPair.first.pause()
        exoPlayerPair.second.pause()
        exoPlayerPair.third.play()
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;또한 Compose의 'DisposableEffect'를 활용해 화면에서 사라질 때 플레이어 리소스를 깔끔하게 해제했다.&lt;br&gt;영상 URI가 변경되면 'setMediaSource()'로 새 영상을 설정하고, 미리 'prepare()'를 호출하여 다음 재생을 준비했다. 덕분에 영상 전환이 빠르고 부드럽게 이루어질 수 있었다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Compose의 DisposableEffect 활용&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;'DisposableEffect'는 Composable의 생명 주기에 맞춰 리소스를 관리하는 데 핵심적인 역할을 했다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Composable이 사라질 때 플레이어 인스턴스&amp;nbsp;해제&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;'exoPlayerTriple'과 같이 전역적으로 관리되는 플레이어 인스턴스는, 해당 Composable이 화면에서 완전히 사라질 때 'release()'되어야 한다.&lt;br&gt;이를 통해 불필요한 리소스 누수를 방지할 수 있었다.&lt;/p&gt;&lt;pre class=&quot;stylus&quot; data-code-block-lang=&quot;kotlin&quot; data-code-block-mode=&quot;2&quot;&gt;&lt;code&gt;DisposableEffect(Unit) {
    onDispose {
        exoPlayerPair.first.release()
        exoPlayerPair.second.release()
        exoPlayerPair.third.release()
    }
}

&lt;/code&gt;&lt;/pre&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. URI 변경 시 미디어 소스 및 플레이어 상태&amp;nbsp;관리&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;개별 플레이어가 새로운 영상을 재생해야 할 때, 'DisposableEffect'의 'key'를 'uri'로 설정하면, 'uri'가 변경될 때마다 플레이어를 새로 설정하고, Composable이 재구성되거나 사라질 때 플레이어 상태를 깔끔하게 정리할 수 있다.&lt;/p&gt;&lt;pre class=&quot;reasonml&quot; data-code-block-lang=&quot;kotlin&quot; data-code-block-mode=&quot;2&quot;&gt;&lt;code&gt;DisposableEffect(uri) {
    val dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory()
    val source = HlsMediaSource.Factory(dataSourceFactory)
        .createMediaSource(MediaItem.fromUri(uri))
    exoPlayer.apply {
        setMediaSource(source)
        prepare()
        playWhenReady = true
        videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
        repeatMode = Player.REPEAT_MODE_ONE
    }

    onDispose {
        exoPlayer.stop()
        exoPlayer.clearMediaItems()
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;이 접근 덕분에 메모리 누수 걱정 없이, 부드러운 스크롤과 안정적인 영상 재생을 동시에 달성할 수 있었다. 실제 프로파일러 결과에서도 CPU와 메모리 사용량이 눈에 띄게 개선되었다.&lt;br&gt;&lt;br&gt;&lt;b&gt;- CPU 사용량&lt;/b&gt;: 평균 42%에서 18%로 감소, 약 57% 개선&lt;br&gt;&lt;b&gt;- 메모리 사용량&lt;/b&gt;: 다중 인스턴스 사용 시보다 현저히 감소&lt;br&gt;무엇보다 사용자 경험에서 큰 차이를 만들었다.&lt;br&gt;스크롤을 아무리 빠르게 넘겨도 영상이 끊기거나 버벅이는 현상이 없었고, 재생은 매우 부드럽게 이루어졌다.&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EpOMa/btsPS2Bw5ih/HIdeMLDyVTTGoIS0kMaekK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EpOMa/btsPS2Bw5ih/HIdeMLDyVTTGoIS0kMaekK/img.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;564&quot; style=&quot;width: 51.111%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EpOMa/btsPS2Bw5ih/HIdeMLDyVTTGoIS0kMaekK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEpOMa%2FbtsPS2Bw5ih%2FHIdeMLDyVTTGoIS0kMaekK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu0JKU/btsPVbQZR5E/zXeql5tpbmUo7y9HVMcfCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu0JKU/btsPVbQZR5E/zXeql5tpbmUo7y9HVMcfCk/img.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;453&quot; style=&quot;width: 47.7262%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu0JKU/btsPVbQZR5E/zXeql5tpbmUo7y9HVMcfCk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu0JKU%2FbtsPVbQZR5E%2FzXeql5tpbmUo7y9HVMcfCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;최적화 전/ 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;ExoPlayer는 미디어 재생을 위한 강력한 도구이지만, 그 잠재력을 온전히 활용하고 안정적인 서비스를 제공하기 위해서는 깊이 있는 소프트웨어 설계 지식이 필요하다는 점을 이번 경험을 통해 다시 한번 깨달았다.&lt;br&gt;특히 Jetpack Compose와 같은 현대적인 선언형 UI 프레임워크 환경에서는, UI의 생명 주기와 리소스 관리가 더욱 복잡해진다. 따라서 검증된 설계 패턴과 원칙을 이해하고 적용하는 것이 필수적이다.&lt;br&gt;예를 들어, OOM 문제를 해결하기 위해 &lt;b&gt;싱글톤 패턴&lt;/b&gt;을 활용해 플레이어 인스턴스를 관리하거나, &lt;b&gt;객체 풀(Object Pool)&lt;/b&gt; 개념을 적용해 3개의 ExoPlayer를 순환 재사용한 전략이 이에 해당한다.&lt;br&gt;이처럼 기본적인 컴퓨터 과학 지식과 소프트웨어 공학 원칙을 바탕으로 문제를 접근하는 것이 매우 중요했다.&lt;br&gt;이번 최적화 과정을 통해 배운 가장 중요한 교훈은, 아무리 강력한 도구라도 올바른 설계와 지속적인 개선이 없으면 오히려 독이 될 수 있다는 점이다.&lt;br&gt;단순한 기능 구현을 넘어, 성능과 사용자 만족도 사이의 균형을 유지하기 위해 &lt;b&gt;지속적인 모니터링과 점진적 최적화&lt;/b&gt;가 반드시 필요함을 다시 한번 강조하고 싶다.&lt;/p&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/581</guid>
      <comments>https://fre2-dom.tistory.com/581#entry581comment</comments>
      <pubDate>Fri, 15 Aug 2025 12:29:56 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고 (feat. 우아한테크코스)</title>
      <link>https://fre2-dom.tistory.com/574</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZIrl/btsL4Jla8ns/glTiQnUFfC3VXDKHLPHyiK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZIrl/btsL4Jla8ns/glTiQnUFfC3VXDKHLPHyiK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZIrl/btsL4Jla8ns/glTiQnUFfC3VXDKHLPHyiK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZIrl%2FbtsL4Jla8ns%2FglTiQnUFfC3VXDKHLPHyiK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;405&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;우아한테크코스로 시작해서.. 우아한테크코스로 끝나다..&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년은 개발자로서 큰 변화를 경험한 한 해였다. 특히, 우아한테크코스 6기에 참여하면서 단순한 기술 학습을 넘어 협업, 문제 해결, 그리고 성장하는 방법을 배울 수 있었다. 이번 회고에서는 한 해 동안 겪었던 주요 경험과 배운 점을 정리하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;우아한테크코스를 통해 가장 크게 달라진 점은 문제를 해결하는 과정에서 구조와 원칙을 고민하며 코드를 바라보게 되었다는 점이다.&quot;&lt;/b&gt; 이전에는 기능을 빠르게 구현하고 결과를 내는 데에만 집중했다면, 이제는 &amp;ldquo;이 구조가 정말 최선일까?&amp;rdquo;, &amp;ldquo;왜 이렇게 동작할까?&amp;rdquo; 같은 질문을 스스로에게 던지며 개발에 임하게 되었다. 단순히 정답을 외우는 방식이 아니라, 눈앞의 문제를 해결하기 위해 설계와 구조를 고민하고 적용하는 과정을 반복하면서, 책에서 봤던 디자인 패턴이나 원칙들이 실제 문제 해결에 도움이 되기 시작했다. 마치 밥을 꼭꼭 씹어 먹듯 코드의 의미와 역할을 되새기며 작성하다 보니, 어느 순간 반복적으로 마주치는 구조나 상황 속에서 &amp;ldquo;이게 바로 전략 패턴이었네?&amp;rdquo;, &amp;ldquo;이게 상태 패턴이네?&amp;rdquo;라는 인식이 자연스럽게 떠올랐다. 단순히 개념을 외워두었다가 꺼내 쓰는 것이 아니라, 문제를 해결하려다 보니 그 패턴이 필요해졌고, 그러다 보니 자연스럽게 원칙과 구조가 몸에 익어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;안드로이드 개발을 통해 기술의 발전을 실감했다.&quot;&lt;/b&gt; 처음에는 남들이 사용하는 최신 기술이나 유행하는 기술들을 무작정 적용하려 했지만, 스스로 그 근본적인 이유가 부족하다는 것을 깨달았다. 이 점은 네이버 부스트캠프에서 많은 고민을 하면서 알게 되었고, 그 후 우아한테크코스에서 더 깊이 생각하게 되었다. Android 프레임워크와 Kotlin을 활용해 MVC에서 MVP, MVVM 구조로 프로젝트를 리팩터링하면서, 각 상황에 맞는 적합한 아키텍처를 선택하는 것의 중요성을 알게 되었다. 특히, 리스트 목록을 구현할 때 ListView 대신 RecyclerView를 사용하는 이유를 알게 되었고, 이는 단순히 &amp;lsquo;좋은 기술&amp;rsquo;을 사용하는 것 이상의 의미가 있다는 점을 깨달았다. 기술 선택은 언제나 그 선택에 대한 명확한 근거와 맥락을 필요로 한다는 교훈을 얻었다. 결국 기술은 단지 도구일 뿐, 그 도구를 언제, 어떻게 사용할지가 진정한 실력의 차이를 만든다는 것을 실감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;코드 리뷰는 단순히 오류를 지적받는 과정이 아닌, 개발자 간 관점을 나누고 성장하는 대화의 장이었다.&quot;&lt;/b&gt; 초기에는 리뷰의 중요성을 몰라 상대방의 리뷰를 받아들이기 급했다. 하지만 리뷰를 통해 &amp;lsquo;왜 이 방식이 더 적합한지&amp;rsquo;를 스스로 설명하고 설득하는 연습을 하면서 논리적 사고와 표현 능력을 함께 키울 수 있었다. 특히 내가 남긴 리뷰가 동료에게 도움이 되는 방식으로 전달되도록 고민하며, 단순한 지적이 아닌 대안을 제시하는 방식으로 피드백을 남기게 되었다. 단순히 &amp;ldquo;이 부분이 잘못되었습니다&amp;rdquo;가 아니라 &amp;ldquo;이 방식은 특정 조건에서 예상치 못한 동작이 발생할 수 있으니, 이런 구조는 어떨까요?&amp;rdquo;라고 말하는 습관이 생겼다. 이러한 경험은 단지 코드 품질을 높이는 데 그치지 않고, 팀 내에서 신뢰받는 개발자로서의 책임감을 기르는 데 큰 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;Android와 Kotlin 모두 테스트가 가능해졌다.&quot;&lt;/b&gt; 이전부터 테스트 코드를 작성해보고 싶다는 욕구가 있었는데, 이번 기회에 제대로 배우게 되었다. 우아한테크코스는 테스트 코드가 빠지면 섭섭한 교육인 것 같다. 테스트 코드를 작성하면서 가장 크게 느낀 점은, 단지 오류를 잡는 도구가 아니라, 내 코드가 올바르게 설계되었는지를 확인하는 거울이라는 것이었다. 특히 ViewModel이나 UseCase 레벨의 테스트를 작성하면서, 코드가 외부 의존성과 어떻게 분리되어야 하는지를 자연스럽게 학습할 수 있었다. 또, 테스트를 작성하면 이후 리팩토링이나 기능 확장 시에도 안정감을 가지고 작업할 수 있다는 점에서, 테스트는 단순한 품질 확보 수단을 넘어 개발자의 자신감을 키우는 역할을 한다는 것을 체감했다. 앞으로 어떤 프로젝트를 하더라도 테스트 코드를 우선순위에 둘 수밖에 없는 이유를 스스로 납득하게 된 시간이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;동아리원에서 심사위원까지..&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;올해, 나는 한국대학생IT경영학회 큐시즘에서 동아리 심사위원을 맡게 되었다.&quot;&lt;/b&gt; 이 경험은 예전에 내가 GDG on Campus에서 해커톤을 기획하며 심사위원을 섭외했던 때와 비교되는 큰 변화였다. 당시에는 심사위원을 구하는 것이 쉽지 않았고, 많은 노력이 필요했지만, 올해는 내가 직접 심사위원 역할을 맡게 되어 조금은 벅차기도 했다. 큐시즘은 기획자, 디자이너, 개발자가 한 팀을 이루는 방식으로 진행되었고, 학생들의 실력이 매우 뛰어나 직장인들이 포함된 동아리와 비교해도 손색이 없을 정도였다. 심사위원으로 처음 맡게 되었을 때는 내가 이 역할을 잘할 수 있을지 의문이 들었지만, 그동안 쌓아온 개발 경험을 바탕으로 최선을 다해 평가하고 피드백을 제공했다. 이 경험을 통해, 기술과 협업 능력을 갖춘 참가자들의 실력을 직접 느낄 수 있었고, 심사 과정에서 내가 배운 점도 많았다. 결국, 이번 경험은 내 자신에게 큰 도전이었고, 더 나아가 내가 가진 지식과 경험을 공유하는 기회로서 값진 시간이 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;앞으로의 계획..&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 부족하다고 느낀 CS 분야를 안드로이드 개발과 연관지어 학습할 것이다.&lt;br /&gt;그동안 안드로이드 폰 사용자만 이용할 수 있었던 서비스의 한계를 KMP 기술을 활용하여 해결할 것이다.&lt;br /&gt;꾸준히 진행할 서비스를 새롭게 개발할 것이다.&lt;/p&gt;
&lt;p data-end=&quot;406&quot; data-start=&quot;273&quot; data-ke-size=&quot;size16&quot;&gt;2024년은 나에게 정말 많은 성장을 안겨준 해였다. 우아한테크코스를 통해 개발자로서의 기본적인 태도를 정립하고, 다양한 프로젝트 경험을 통해 실력을 키울 수 있었다. 앞으로도 계속해서 배우고 도전하며, 더 나은 개발자로 성장해 나가겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;2268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbaMAX/btsL4Is28PL/yC7spGZP7PBEVb7YJiTgvk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbaMAX/btsL4Is28PL/yC7spGZP7PBEVb7YJiTgvk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbaMAX/btsL4Is28PL/yC7spGZP7PBEVb7YJiTgvk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbaMAX%2FbtsL4Is28PL%2FyC7spGZP7PBEVb7YJiTgvk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;2268&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;2268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Memoir</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/574</guid>
      <comments>https://fre2-dom.tistory.com/574#entry574comment</comments>
      <pubDate>Fri, 22 Nov 2024 20:54:29 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드에서 일회성 이벤트 처리, 어떻게 할&amp;nbsp;것인가?</title>
      <link>https://fre2-dom.tistory.com/573</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBpd1/btsPVY47vwf/ZI0VQ0CvpNhArwULr7H5P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBpd1/btsPVY47vwf/ZI0VQ0CvpNhArwULr7H5P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBpd1/btsPVY47vwf/ZI0VQ0CvpNhArwULr7H5P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBpd1%2FbtsPVY47vwf%2FZI0VQ0CvpNhArwULr7H5P1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1118&quot; height=&quot;745&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;안드로이드 개발을 하다 보면, UI에서 단 한 번만 실행되어야 하는 이벤트를 처리해야 하는 상황이 종종 발생한다.&lt;br /&gt;예를 들어, 사용자에게 &lt;b&gt;Toast 메시지를 한 번만 띄우거나&lt;/b&gt;, 특정 조건이 충족되었을 때 &lt;b&gt;화면 이동을 한 번만 수행&lt;/b&gt;해야 하는 경우가 그렇다.&lt;/p&gt;
&lt;p data-end=&quot;307&quot; data-start=&quot;213&quot; data-ke-size=&quot;size14&quot;&gt;하지만 안드로이드의 생명주기(Lifecycle) 특성과 ViewModel, LiveData의 동작 방식 때문에, 이 단순해 보이는 요구조차 쉽게 구현하기 어렵다.&lt;/p&gt;
&lt;p data-end=&quot;412&quot; data-start=&quot;309&quot; data-ke-size=&quot;size14&quot;&gt;이번 글에서는 안드로이드에서 &lt;b&gt;일회성 이벤트를 처리하는 다양한 방법&lt;/b&gt;을 살펴보고, 각각의 장단점을 정리하려고 한다.&lt;br /&gt;또한 어떤 상황에서 어떤 방법이 적절한지 함께 고민해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. LiveData&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7TF3F/btsKkn4WeVa/YA8RYHxZ11A7tC9MKg05EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7TF3F/btsKkn4WeVa/YA8RYHxZ11A7tC9MKg05EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7TF3F/btsKkn4WeVa/YA8RYHxZ11A7tC9MKg05EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7TF3F%2FbtsKkn4WeVa%2FYA8RYHxZ11A7tC9MKg05EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;136&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;일반적으로 LiveData는 데이터가 변경될 때, 활성화된 옵저버에게만 업데이트를 전달한다.&lt;br /&gt;하지만 옵저버가 비활성 상태였다가 다시 활성 상태로 전환되면, 마지막으로 활성 상태였을 때의 값이 다시 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;즉, LiveData는 옵저버가 비활성에서 활성으로 전환될 때 &lt;b&gt;마지막 값을 재전달&lt;/b&gt;하여 최신 상태로 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그러나 이 특성 때문에 &lt;b&gt;일회성 이벤트를 처리할 때 문제가 발생&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생 시나리오&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. MainActivity에서 Toast를 띄우라는 UI 이벤트가 발생한다.&lt;br /&gt;2. 이후 DetailActivity로 이동했다가 다시 MainActivity로 돌아온다.&lt;br /&gt;3. LiveData를 Observe하고 있던 옵저버가 비활성 상태에서 다시 활성 상태로 전환되며 관찰을 재개한다.&lt;br /&gt;4. 이때 이전에 발생했던 Toast 이벤트가 다시 전달되어, &lt;b&gt;의도하지 않게 Toast가 중복으로 표시&lt;/b&gt;되는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 문제를 해결하기 위해 고안된 것이 &lt;b&gt;SingleLiveEvent&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. SingleLiveEvent&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJMiYs/btsKk09uPwc/sgfIyqQKKuavOAoILdZ7Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJMiYs/btsKk09uPwc/sgfIyqQKKuavOAoILdZ7Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJMiYs/btsKk09uPwc/sgfIyqQKKuavOAoILdZ7Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJMiYs%2FbtsKk09uPwc%2FsgfIyqQKKuavOAoILdZ7Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;136&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;SingleLiveEvent는 &lt;b&gt;단발성 이벤트를 한 번만 전달하고 소비&lt;/b&gt;할 수 있도록 설계된 LiveData 기반의 이벤트 래퍼(Event Wrapper)이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;open class Event&amp;lt;out T&amp;gt;(private val content: T) {

    var hasBeenHandled = false
        private set

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    fun peekContent(): T = content
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;실제로 이 이벤트 래퍼 개념은 &lt;b&gt;LiveData 공식 문서&lt;/b&gt;에서도 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150&quot;&gt;추천 자료&lt;/a&gt;를 통해 권장되는 방법으로 소개되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 SingleLiveEvent는 LiveData 위에서 구현되기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;코루틴 환경과 자연스럽게 연동하기에는 다소 제한적&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Channel&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Kotlin Coroutines의 &lt;b&gt;Channel&lt;/b&gt;을 활용하면, 단일 소비자 패턴을 기반으로 &lt;b&gt;코루틴 환경에서도 안전하게 단발성 이벤트를 전달&lt;/b&gt;할 수 있다.&lt;br /&gt;Channel은 이벤트를 보내는 쪽(send)과 받는 쪽(receive)을 비동기적으로 연결하며, SingleLiveEvent가 제공하던 단발성 이벤트 처리 기능을 &lt;b&gt;그대로 유지하면서 코루틴과 자연스럽게 통합&lt;/b&gt;할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-end=&quot;478&quot; data-start=&quot;390&quot; data-ke-size=&quot;size14&quot;&gt;즉, SingleLiveEvent로 처리하던 단발성 이벤트를 &lt;b&gt;코루틴 환경에서도 안전하고 효율적으로 구현&lt;/b&gt;할 수 있는 방법이 바로 Channel이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이전 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// sealed interface
sealed interface Toast {
    data object ShowToast
    data object ShowXXX
    data object ShowYYY
}

// ViewModel
private val _showToastEvent: MutableLiveData&amp;lt;Event&amp;lt;Toast&amp;gt;&amp;gt; = MutableLiveData(null)
val showToastEvent: LiveData&amp;lt;Event&amp;lt;Toast&amp;gt;&amp;gt; get() = _showToastEvent

// UI
viewModel.showToastEvent.observeEvent(this) { toastEvent -&amp;gt;
    when (toastEvent) {
        is Event.ShowToast -&amp;gt; // TODO
        is Event.ShowXXX -&amp;gt; // TODO
        is Event.ShowYYY -&amp;gt; // TODO
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이후 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// ViewModel
private val _showToastEvent = Channel&amp;lt;Effect&amp;gt;(Channel.BUFFERED)
val showToastEvent = _showToastEvent.receiveAsFlow()

// UI
lifecycleScope.launch {
    viewModel.showToastEvent.collect { toastEvent -&amp;gt;
        when (toastEvent) {
            is Event.ShowToast -&amp;gt; // TODO
            is Event.ShowXXX -&amp;gt; // TODO
            is Event.ShowYYY -&amp;gt; // TODO
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;기존의 SingleLiveEvent를 &lt;b&gt;Channel&lt;/b&gt;로 변경하고, observe 대신 collect 방식으로 처리하면 된다.&lt;br /&gt;이제 UI에서는 하나의 showToastEvent를 collect하여, 이벤트 유형에 맞게 Toast를 간단히 표시할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;254&quot; data-start=&quot;217&quot; data-ke-size=&quot;size14&quot;&gt;하지만 Channel만 사용하는 경우에도 주의할 점이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생 시나리오&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. ViewModel에서 서버와 통신하며 위치 데이터를 주기적으로 emit한다.&lt;br /&gt;2. UI에서는 위치 데이터 변화를 감지하고, 변경될 때마다 화면을 다시 그린다.&lt;br /&gt;3. 이때 사용자가 홈 버튼을 눌러 앱을 백그라운드로 전환하면, UI는 보이지 않지만 위치 데이터를 계속 감지하고 화면을 다시 그리는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;즉, 사용자가 UI를 보고 있지 않을 때도 데이터를 계속 관찰하기 때문에 메모리 누수가 발생하게 된다.&lt;/p&gt;
&lt;h4 data-end=&quot;82&quot; data-start=&quot;47&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방안: repeatOnLifecycle() 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;안드로이드에서는 &lt;b&gt;repeatOnLifecycle()&lt;/b&gt;&amp;nbsp;함수를 사용하여 이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;386&quot; data-start=&quot;149&quot; data-ke-size=&quot;size14&quot;&gt;'repeatOnLifecycle()'은 Lifecycle 상태에 맞춰 코루틴을 자동으로 관리해주는 기능을 제공한다.&lt;br /&gt;지정된 Lifecycle.State(보통 STARTED나 RESUMED)에 도달하면 코루틴을 실행하고, 해당 상태에서 벗어나면 자동으로 중단된다.&lt;br /&gt;이 덕분에 개발자는 코루틴의 시작과 중지를 일일이 관리할 필요 없이, UI가 활성화된 동안만 안전하게 이벤트를 수집할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이후 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;// UI
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.showToastEvent.collect { toastEvent -&amp;gt;
            when (toastEvent) {
                is Event.ShowToast -&amp;gt; // TODO
                is Event.ShowXXX -&amp;gt; // TODO
                 is Event.ShowYYY -&amp;gt; // TODO
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그러나 Channel은 여러 구독자에게 동일한 이벤트를 전달할 수 없다는 한계가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. SharedFlow&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이때 &lt;b&gt;SharedFlow&lt;/b&gt;를 활용하면, 코루틴 기반의 Flow를 통해 &lt;b&gt;여러 구독자에게 데이터를 동시에 전달&lt;/b&gt;할 수 있다.&lt;br /&gt;SharedFlow는 브로드캐스트 방식으로 동작하기 때문에, 여러 구독자가 동일한 데이터를 받아 처리할 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;377&quot; data-start=&quot;270&quot; data-ke-size=&quot;size14&quot;&gt;즉, SharedFlow를 사용하면 &lt;b&gt;복수 구독자에게 데이터를 브로드캐스트 방식으로 전달&lt;/b&gt;할 수 있으며, &lt;b&gt;라이프사이클에 의존하지 않는 이벤트 처리&lt;/b&gt;가 가능하다는 장점이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이후 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ViewModel
private val _showToastEvent = MutableSharedFlow&amp;lt;Toast&amp;gt;()
val showToastEvent = _showToastEvent.asSharedFlow()

// UI
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.showToastEvent.collect { toastEvent -&amp;gt;
            when (toastEvent) {
                is Event.ShowToast -&amp;gt; // TODO
                is Event.ShowXXX -&amp;gt; // TODO
                is Event.ShowYYY -&amp;gt; // TODO
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;기존의 &lt;b&gt;Channel&lt;/b&gt; 대신 &lt;b&gt;SharedFlow&lt;/b&gt;로 변경하면 된다.&lt;/p&gt;
&lt;p data-end=&quot;135&quot; data-start=&quot;95&quot; data-ke-size=&quot;size14&quot;&gt;하지만 SharedFlow만 사용하는 경우에도 주의할 점이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생 시나리오&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;목록에서 특정 아이템을 선택하고, 서버 응답에 따라 상세 화면으로 이동하는 로직을 가정해보자.&lt;br /&gt;&lt;br /&gt;1. 사용자가 목록에서 아이템을 선택한다.&lt;br /&gt;2. 서버에서 해당 아이템 상태를 확인하기 전에 홈 버튼을 눌러 앱을 백그라운드로 전환한다.&lt;br /&gt;3. 이때, 상세 화면으로 이동하라는 이벤트를 emit하더라도, UI는 이미 onStop() 상태이므로 이벤트를 수신하지 못한다.&lt;br /&gt;4. 결국 앱으로 돌아왔을 때, 상세 화면으로 이동하지 못하게 된다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;425&quot; data-start=&quot;370&quot; data-ke-size=&quot;size14&quot;&gt;즉, &lt;b&gt;이벤트를 관찰하고 있는 구독자가 없는 상태라면 해당 이벤트는 유실될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;425&quot; data-start=&quot;370&quot; data-ke-size=&quot;size14&quot;&gt;그래서 등장한 것이 EventFlow다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. EventFlow&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;EventFlow&lt;/b&gt;는 이벤트가 발생하면 이를 내부적으로 캐시하고, &lt;b&gt;해당 이벤트가 이미 소비(consumed)되었는지 여부에 따라 새로운 구독자에게 전달&lt;/b&gt;할지를 결정하는 구조이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;250&quot; data-start=&quot;174&quot; data-ke-size=&quot;size14&quot;&gt;즉, EventFlow는 &lt;b&gt;소비되지 않은 이벤트를 보관&lt;/b&gt;했다가, 구독자가 이를 수신할 수 있을 때 전달하는 방식으로 동작한다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;250&quot; data-start=&quot;174&quot; data-ke-size=&quot;size14&quot;&gt;하지만 EventFlow만 사용하는 경우에도 한계가 있다.&lt;/p&gt;
&lt;h4 data-end=&quot;98&quot; data-start=&quot;84&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생 시나리오&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;98&quot; data-start=&quot;84&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;/b&gt;이벤트 객체가 있고, 이를 AFragment와 BFragment에서 collect하고 있다고 가정해보자.&lt;br /&gt;&lt;br /&gt;1. 이벤트가 emit되면, 근소한 시간 차이로 AFragment에서 먼저 수신되고 소비(consumed)된다.&lt;br /&gt;2. 이후 BFragment에서 이벤트를 collect하려고 하지만, 이벤트는 이미 소비되었기 때문에 전달되지 않는다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;392&quot; data-start=&quot;311&quot; data-ke-size=&quot;size14&quot;&gt;즉, EventFlow를 사용할 경우, &lt;b&gt;여러 구독자에게 데이터를 동시에 전달할 수 있는 SharedFlow의 장점이 사라지게 된다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;73&quot; data-start=&quot;47&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. EventFlow + HashMap&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;191&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EventFlow + HashMap&lt;/b&gt;은 이벤트가 발생하면 이를 내부적으로 캐시하고, &lt;b&gt;이벤트가 소비(consumed)되었는지 여부에 따라 새로 구독하는 옵저버에게 전달할지&lt;/b&gt;를 결정하는 구조이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;265&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;즉, 소비되지 않은 이벤트를 보관하고 있다가, &lt;b&gt;새로운 옵저버가 구독할 때 해당 이벤트를 전달&lt;/b&gt;하는 방식으로 동작한다.&lt;/p&gt;
&lt;h4 data-end=&quot;61&quot; data-start=&quot;46&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;HashMap의 역할&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;140&quot; data-start=&quot;63&quot; data-ke-size=&quot;size14&quot;&gt;EventFlow + HashMap 구조에서 &lt;b&gt;HashMap&lt;/b&gt;은 각 이벤트와 이를 소비하는 옵저버의 상태를 관리하는 데 사용된다.&lt;/p&gt;
&lt;p data-end=&quot;140&quot; data-start=&quot;63&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;키(Key) &lt;/b&gt;: 현재 collect 중인 옵저버의 이름과 해당 슬롯의 toString() 값을 결합하여 생성된다. 이를 통해 각 옵저버를 &lt;b&gt;고유하게 식별&lt;/b&gt;할 수 있으며, 어떤 옵저버가 어떤 이벤트를 수신할 수 있는지를 명확히 알 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;값(Value) &lt;/b&gt;: 이벤트와 동일한 값을 가지는 새로운 이벤트 객체가 저장된다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;403&quot; data-start=&quot;346&quot; data-ke-size=&quot;size14&quot;&gt;이 구조 덕분에, &lt;b&gt;새로운 옵저버가 구독할 때 이전에 발생한 이벤트도 적절히 전달&lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;정리하면&lt;/b&gt;&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;LiveData&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 구독자가 활성화될 때 마지막 값 재전달&lt;br /&gt;- 단발성 이벤트 처리에는 적합하지 않음&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SingleLiveEvent&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 이벤트를 한 번만 전송 가능&lt;br /&gt;- 코루틴 환경과 자연스럽게 연동하기에는 다소 제한적&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Channel&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 단일 소비자에게 효율적 전달&lt;br /&gt;- 여러 소비자에게 이벤트 전달 불가&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SharedFlow&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 여러 소비자에게 브로드캐스트 방식 전달 가능&lt;br /&gt;- 구독자가 없으면 이벤트 유실 가능&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;EventFlow&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 소비되지 않은 이벤트 캐시 후 새로운 옵저버에 전달&lt;br /&gt;- 복수 소비자 환경에서는 한계 존재&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;EventFlow + HashMap&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 캐시된 이벤트를 새로운 옵저버에게 안전하게 전달&lt;br /&gt;- 복수 소비자 환경에서도 이벤트 관리 가능&lt;br /&gt;- 다만, 구현이 다소 복잡함&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;결론적으로&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이벤트 처리는 보통 한 곳에서 이루어지므로, 코드가 간결하고 이해하기 쉬운 &lt;b&gt;Channel&lt;/b&gt;을 사용하는 것이 적합하다. 다만, 특별한 요구 사항이 있거나 복수 소비자를 지원해야 하는 경우에는 &lt;b&gt;EventFlow + HashMap&lt;/b&gt;을 사용하는 것이 더 적절하다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;참고문헌&lt;/b&gt;&lt;/h1&gt;
&lt;figure id=&quot;og_1755326022265&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;UI 이벤트 &amp;nbsp;|&amp;nbsp; App architecture &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. UI 이벤트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. UI 이벤트는 UI 레이어에서 UI 또는 ViewModel로 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/topic/architecture/ui-layer/events&quot; data-og-url=&quot;https://developer.android.com/topic/architecture/ui-layer/events?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNcBL9/hyZyi7xq7F/pCKLiblaq7HdOoXZWKUw7K/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/fLtDQ/hyZymPDU4l/6DjKVCF7r8L7QsStMcUZM1/img.png?width=2127&amp;amp;height=1260&amp;amp;face=0_0_2127_1260&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/architecture/ui-layer/events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/topic/architecture/ui-layer/events&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNcBL9/hyZyi7xq7F/pCKLiblaq7HdOoXZWKUw7K/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/fLtDQ/hyZymPDU4l/6DjKVCF7r8L7QsStMcUZM1/img.png?width=2127&amp;amp;height=1260&amp;amp;face=0_0_2127_1260');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UI 이벤트 &amp;nbsp;|&amp;nbsp; App architecture &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. UI 이벤트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. UI 이벤트는 UI 레이어에서 UI 또는 ViewModel로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1755326022479&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;LiveData 개요 &amp;nbsp;|&amp;nbsp; App architecture &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/topic/libraries/architecture/livedata&quot; data-og-url=&quot;https://developer.android.com/topic/libraries/architecture/livedata?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mI8RE/hyZyjFnaFw/tZtPmjk6eKulvvIMFtkEEk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/libraries/architecture/livedata&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/topic/libraries/architecture/livedata&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mI8RE/hyZyjFnaFw/tZtPmjk6eKulvvIMFtkEEk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;LiveData 개요 &amp;nbsp;|&amp;nbsp; App architecture &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1755326023616&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)&quot; data-og-description=&quot;2021 Update: This guidance is deprecated in favor of the official guidelines.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150&quot; data-og-url=&quot;https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2021 Update: This guidance is deprecated in favor of the official guidelines.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1755326029893&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;ViewModels and LiveData: Patterns + AntiPatterns&quot; data-og-description=&quot;A collection of patterns and recommendations that we&amp;rsquo;ve been collecting since we released the first alpha version of the Architecture Components.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54&quot; data-og-url=&quot;https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Gdy3M/hyZyn8RXNS/tQjI7eGN6qBzaMffUT6Qn1/img.png?width=803&amp;amp;height=230&amp;amp;face=0_0_803_230&quot;&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Gdy3M/hyZyn8RXNS/tQjI7eGN6qBzaMffUT6Qn1/img.png?width=803&amp;amp;height=230&amp;amp;face=0_0_803_230');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ViewModels and LiveData: Patterns + AntiPatterns&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A collection of patterns and recommendations that we&amp;rsquo;ve been collecting since we released the first alpha version of the Architecture Components.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1755326065235&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;MVVM의 ViewModel에서 이벤트를 처리하는 방법 7가지&quot; data-og-description=&quot;ViewModel의 이벤트 처리를 어떻게 하고 계신가요? 헤이딜러에서 LiveData -&amp;gt; SingleLiveData -&amp;gt; SharedFlow -&amp;gt; EventFlow -&amp;gt; Channal로 이벤트 처리 방법을 변화 하기까지 과정을 소개합니다&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce&quot; data-og-url=&quot;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&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oXYjs/hyZyrpSwQQ/2UqhrLDKj58VY2Z4K4YcK0/img.png?width=1146&amp;amp;height=573&amp;amp;face=0_0_1146_573&quot;&gt;&lt;a href=&quot;https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oXYjs/hyZyrpSwQQ/2UqhrLDKj58VY2Z4K4YcK0/img.png?width=1146&amp;amp;height=573&amp;amp;face=0_0_1146_573');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MVVM의 ViewModel에서 이벤트를 처리하는 방법 7가지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ViewModel의 이벤트 처리를 어떻게 하고 계신가요? 헤이딜러에서 LiveData -&amp;gt; SingleLiveData -&amp;gt; SharedFlow -&amp;gt; EventFlow -&amp;gt; Channal로 이벤트 처리 방법을 변화 하기까지 과정을 소개합니다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/573</guid>
      <comments>https://fre2-dom.tistory.com/573#entry573comment</comments>
      <pubDate>Sun, 27 Oct 2024 17:40:36 +0900</pubDate>
    </item>
    <item>
      <title>너는 왜 inline Composable이야?</title>
      <link>https://fre2-dom.tistory.com/572</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 8월 16일 오후 08_19_24.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyiUrK/btsPUJHD42F/dLzOpaQiShXBWH5GB6pZr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyiUrK/btsPUJHD42F/dLzOpaQiShXBWH5GB6pZr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyiUrK/btsPUJHD42F/dLzOpaQiShXBWH5GB6pZr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyiUrK%2FbtsPUJHD42F%2FdLzOpaQiShXBWH5GB6pZr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;ChatGPT Image 2025년 8월 16일 오후 08_19_24.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Compose를 사용하다 보면, 어떤 Composable은 일반 함수로, 또 어떤 Composable은 inline 함수로 정의된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;예를 들어 Box 컴포저블도 다음과 같이 두 가지 형태로 제공된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Box(modifier: Modifier) {
    Layout(measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -&amp;gt; Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 외에도 Column과Row도 inline 함수이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 저 Box 컴포저블은 inline 함수일까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;inline을 사용하는 이유는 일반 함수에서와 동일하다. &lt;b&gt;자주 호출되는 함수&lt;/b&gt;거나 &lt;b&gt;복잡한 람다식&lt;/b&gt;이 포함된 경우 성능 최적화를 위해 inline을 사용한다.&lt;/p&gt;
&lt;figure id=&quot;og_1729559476443&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kotlin] 얼렁뚱땅 inline 탐험일지  &quot; data-og-description=&quot;탐험 개요고차함수를 활용하기 위해 컬렉션 함수 내부 코드를 보던 중 inline을 발견!inline이 뭐지 코드 줄을 안으로 뭐 하는건가???.. 일단 코틀린에서 제공하는 고차함수 API에서 쓰이는 것으로 확&quot; data-og-host=&quot;fre2-dom.tistory.com&quot; data-og-source-url=&quot;https://fre2-dom.tistory.com/561&quot; data-og-url=&quot;https://fre2-dom.tistory.com/561&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MDS16/hyXlLKLaVK/RKpK0kFzBCtG4iUzj592P1/img.png?width=800&amp;amp;height=201&amp;amp;face=0_0_800_201,https://scrap.kakaocdn.net/dn/F5FCE/hyXlUnp0gL/IXiorebxtATztbGi4vu14k/img.png?width=800&amp;amp;height=201&amp;amp;face=0_0_800_201,https://scrap.kakaocdn.net/dn/drN6X6/hyXlSXrnIT/1Krvx1xAnBkTjzK3orfxZ1/img.jpg?width=1280&amp;amp;height=1280&amp;amp;face=0_0_1280_1280&quot;&gt;&lt;a href=&quot;https://fre2-dom.tistory.com/561&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://fre2-dom.tistory.com/561&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MDS16/hyXlLKLaVK/RKpK0kFzBCtG4iUzj592P1/img.png?width=800&amp;amp;height=201&amp;amp;face=0_0_800_201,https://scrap.kakaocdn.net/dn/F5FCE/hyXlUnp0gL/IXiorebxtATztbGi4vu14k/img.png?width=800&amp;amp;height=201&amp;amp;face=0_0_800_201,https://scrap.kakaocdn.net/dn/drN6X6/hyXlSXrnIT/1Krvx1xAnBkTjzK3orfxZ1/img.jpg?width=1280&amp;amp;height=1280&amp;amp;face=0_0_1280_1280');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kotlin] 얼렁뚱땅 inline 탐험일지  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;탐험 개요고차함수를 활용하기 위해 컬렉션 함수 내부 코드를 보던 중 inline을 발견!inline이 뭐지 코드 줄을 안으로 뭐 하는건가???.. 일단 코틀린에서 제공하는 고차함수 API에서 쓰이는 것으로 확&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fre2-dom.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Box 컴포저블에서도 동일한 이유로 inline 키워드를 적용한다. 특히, 이 함수 내부에서 사용하는 BoxScope는 Box 내부에서만 동작하는 레이아웃 관련 API를 제공하는데, inline 키워드를 사용하면 &lt;b&gt;BoxScope 내부에서 정의된 람다&lt;/b&gt;가 매번 객체로 생성되는 것을 방지할 수 있다. 이렇게 하면 &lt;b&gt;함수 호출 비용&lt;/b&gt;과 &lt;b&gt;메모리 사용량&lt;/b&gt;을 줄여 성능을 더욱 최적화할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  즉, 컴포저블에서 inline을 사용하는 이유는 성능 최적화뿐만 아니라, BoxScope와 같은 UI 스코프가 자주 호출되는 상황에서 불필요한 람다 객체 생성을 피하고, 코드가 효율적으로 실행되도록 보장하기 위함이다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;inline을 사용하기 적합한 컴포저블&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// inline Comopsable
@Composable
inline fun CustomButton(
    modifier: Modifier = Modifier,
    shape: RoundedCornerShape = RoundedCornerShape(8.dp),
    buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
    buttonTitle: String,
    enabled: Boolean = true,
    crossinline onClick: () -&amp;gt; Unit,
) {
    Button(
        modifier = modifier.fillMaxWidth(),
        onClick = { onClick() },
        shape = shape,
        colors = buttonColors,
        enabled = enabled,
    ) {
        Text(
            text = buttonTitle,
            fontSize = 14.sp,
            fontWeight = FontWeight.W500,
            modifier = Modifier.padding(vertical = 15.dp),
        )
    }
}

// 복잡한 람다
val complexLambda: () -&amp;gt; Unit = {
    // 많은 로직이 포함된 경우
    for (i in 1..1000) {
        println(&quot;복잡복잡 $i&quot;)
    }
}

// Composable 호출
@Composable
fun ExampleUsage() {
 CustomButton(
        modifier = Modifier.padding(horizontal = 32.dp),
        shape = RoundedCornerShape(100.dp),
        buttonColors = ButtonDefaults.buttonColors(containerColor = Blue50),
        buttonTitle = stringResource(R.string.sign_up_button),
        onClick = complexLambda,
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위와 같은 예시에서 inline 키워드는 버튼 클릭 처리에 사용되는 람다 객체 생성을 방지하며, 성능을 최적화할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  컴포저블 함수가 자주 호출되거나 복잡한 람다식을 포함할 때 이를 적절히 사용함으로써 성능과 메모리 효율성을 높일 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;crossinline을 아주 간단하게 알아보면...&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;crossinline은 inline 함수의 매개변수로 사용되는 람다 표현식에서 사용할 수 있는 키워드이다. 이 키워드를 사용하면 해당 람다에서 return 문을 사용할 수 없게 된다. (&lt;b&gt;비지역 반환(Non-Local Return)&lt;/b&gt;이라고도 한다) 이는 inline 함수가 중첩된 함수에서 반환될 때, 호출 스택에 영향을 미쳐 예기치 않은 동작을 방지하기 위한 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  즉, crossinline을 사용하면 해당 람다가 다른 함수의 반환과 상관없이 독립적으로 실행된다.&lt;/blockquote&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/572</guid>
      <comments>https://fre2-dom.tistory.com/572#entry572comment</comments>
      <pubDate>Tue, 22 Oct 2024 10:12:44 +0900</pubDate>
    </item>
    <item>
      <title>Data Binding 프로퍼티 실종 사건 수사 일지</title>
      <link>https://fre2-dom.tistory.com/571</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxd4hG/btsPVCuahYu/Oe1cgoTKKGifxZyohcVWS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxd4hG/btsPVCuahYu/Oe1cgoTKKGifxZyohcVWS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxd4hG/btsPVCuahYu/Oe1cgoTKKGifxZyohcVWS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxd4hG%2FbtsPVCuahYu%2FOe1cgoTKKGifxZyohcVWS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1142&quot; height=&quot;761&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사건의 발단  ️&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;백그라운드 스레드에서 Room DB를 통해 Reservation(예약 정보)이라는 데이터를 받아오고, 해당 데이터를 UI에 렌더링하기 위해 데이터 바인딩을 사용했다. 이 과정에서 BindingAdapter를 활용하여 UI를 업데이트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그러나 10번에 3번 꼴로 데이터 바인딩 프로퍼티가 null인 경우가 발생하여 앱이 크래시되는 문제가 있었다. 이러한 문제는 바인딩 프로퍼티가 누락되어 발생한 것으로 보인다. (아마 7번은 운 좋게 데이터 바인딩이 성공된 것으로 보인다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// presenter 코드
override fun loadReservation(id: Long) {
    thread {
        reservationRepository.findReservation(id).onSuccess { reservation -&amp;gt;
            showReservation(reservation, &quot;선릉 극장&quot;)
        }.onFailure { e -&amp;gt;
            when (e) {
                is NoSuchElementException -&amp;gt; {
                    view.showToastMessage(e)
                    view.navigateBackToPrevious()
                }

                else -&amp;gt; {
                    view.showToastMessage(e)
                    view.navigateBackToPrevious()
                }
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// view(Activity) 코드
override fun showReservation(
    reservation: Reservation,
    theaterName: String,
) {
    runOnUiThread {
        binding.reservation = reservation
        binding.theaterName = theaterName
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 코드는 Presenter에서 백그라운드 스레드를 열어 Room DB에서 데이터를 가져온 후, 해당 데이터를 View에 전달하여 메인 스레드에서 데이터 바인딩하는 과정을 보여준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@BindingAdapter(&quot;count&quot;, &quot;seats&quot;, &quot;theaterName&quot;)
fun TextView.showReservation(
    count: Int,
    seats: List&amp;lt;Seat&amp;gt;,
    theaterName: String,
) {
    this.text =
        this.context.getString(R.string.reserve_count, count, seats.toSeatString(), theaterName)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;데이터 바인딩이 정상적으로 이루어졌다면 XML에서 BindingAdapter를 통해 뷰를 렌더링하게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;범인 찾기  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;범인을 찾기 위해 검증해야 할 두 가지가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. 운이 좋게 백그라운드 스레드에서 가져온 데이터가 메인 스레드로 전달되어 뷰가 렌더링되었는가?&lt;br /&gt;2. BindingAdapter의 파라미터를 nullable하게 선언하여 null 값이 바인딩 되면서 앱이 크래시 나지 않는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;나는 이 두 가지 검증을 통해 문제를 해결하려고 노력했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;운이 좋게 백그라운드 스레드에서 가져온 데이터가 메인 스레드로 전달되어 뷰가 렌더링되었는가?  &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;첫 번째 검증에서는 다양한 코드를 구현하면서 백그라운드 스레드에서 데이터 바인딩을 통한 UI 업데이트가 잘 이루어졌음을 확인했다. 일반적으로 데이터 바인딩은 메인 스레드에서만 실행되어야 한다고 생각했지만, 백그라운드 스레드에서도 UI가 잘 업데이트되는 것을 발견하고, 이에 대한 공식 문서를 확인하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/libraries/data-binding/generated-binding#background_thread&quot;&gt;공식 문서&lt;/a&gt;에 따르면 데이터 모델이 컬렉션이 아닌 경우에만 백그라운드 스레드에서 변경할 수 있다고 한다. 따라서 데이터 모델이 컬렉션이 아닌 경우에는 백그라운드 스레드에서 데이터 바인딩을 통해 UI를 업데이트할 수 있다는 결론을 내릴 수 있다. 이러한 점을 고려하면 스레드 문제는 아니었음을 알 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;BindingAdapter의 파라미터를 nullable하게 선언하여 null 값이 바인딩 되면서 앱이 크래시 나지 않는가?  &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;두 번째 검증에서는 놀랍게도 view에 null 값이 들어오지도 않았고 크래시도 나지 않았다. null 값이 들어와서 null 값으로 바인딩이 되는 것이 정상적인 실행 결과라고 생각했는데 아니었다. 아무리 생각해도 이상해서 Thread에서 1초의 시간을 두어 UI가 업데이트되도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그런데 다음과 같은 현상이 발생했다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/446658833&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bmtpJ1/hyV2ymhVuO/hbYX1EZV4ytG4eko5KSJt1/img.jpg?width=368&amp;amp;height=720&amp;amp;face=0_0_368_720,https://scrap.kakaocdn.net/dn/dHbz1z/hyV2vpyyOH/LDN5C7TlCeOFzVKDgNcvk0/img.jpg?width=368&amp;amp;height=720&amp;amp;face=0_0_368_720&quot; data-video-width=&quot;368&quot; data-video-height=&quot;720&quot; data-video-origin-width=&quot;368&quot; data-video-origin-height=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/446658833?service=daum_tistory&quot; width=&quot;368&quot; height=&quot;720&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 영상을 보면, 데이터가 null 값으로 들어왔다가 시간이 지나면서 데이터가 제대로 들어오는 것을 확인할 수 있다. 아마도 메인 스레드에서는 데이터가 들어오지 않았기 때문에 데이터 바인딩을 null 값으로 설정한 후, 시간이 지나면 백그라운드 스레드에서 DB에서 받은 데이터를 데이터 바인딩해준 것으로 보인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다른 수사관에게 조언을 구하다.  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;나는 레아에게 이게 무슨 상황인지 조언을 구했다. 레아는 데이터 바인딩이 실행되어 구현되는 코드를 보면 도움이 될 것이라고 해주셨고, 나는 데이터 바인딩이 실행되는 내부 코드를 확인해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내부 코드를 보면 다음 이미지와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YYxGD/btsHmWBM67N/lHkUIjSx5087y60DiBisC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YYxGD/btsHmWBM67N/lHkUIjSx5087y60DiBisC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YYxGD/btsHmWBM67N/lHkUIjSx5087y60DiBisC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYYxGD%2FbtsHmWBM67N%2FlHkUIjSx5087y60DiBisC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1538&quot; height=&quot;744&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 이미지를 보면 Reservation과 theaterName이 nullable한 것을 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;범인 검거  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그렇다. 데이터 바인딩 프로퍼티 스스로가 범인이었던 것이다. (사실 내가 범인..)&lt;br /&gt;데이터 바인딩 프로퍼티는 nullable하므로, nullable한 것을 고려하여 bindingAdapter를 사용할 때는 해당 파라미터를 nullable하게 선언하여 사용할 필요가 있다. 이렇게 하면 null이 들어왔을 때 예외가 발생하는 상황을 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;앞으로는 이를 유념하여 bindingAdapter를 사용할 것 같다.&lt;/p&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/571</guid>
      <comments>https://fre2-dom.tistory.com/571#entry571comment</comments>
      <pubDate>Sun, 12 May 2024 15:14:34 +0900</pubDate>
    </item>
    <item>
      <title>얼렁뚱땅 Kotlin inline 탐험일지  </title>
      <link>https://fre2-dom.tistory.com/561</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AVr2W/btsPWtqdFAS/PprYtVgCzIwH5qIspRQBfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AVr2W/btsPWtqdFAS/PprYtVgCzIwH5qIspRQBfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AVr2W/btsPWtqdFAS/PprYtVgCzIwH5qIspRQBfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAVr2W%2FbtsPWtqdFAS%2FPprYtVgCzIwH5qIspRQBfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;797&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;탐험 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;고차함수를 활용하기 위해 컬렉션 함수 내부 코드를 보던 중 inline을 발견!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2288&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0HZAU/btsHpxahXdU/cmoB5OlxhdwKfTarMXD8M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0HZAU/btsHpxahXdU/cmoB5OlxhdwKfTarMXD8M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0HZAU/btsHpxahXdU/cmoB5OlxhdwKfTarMXD8M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0HZAU%2FbtsHpxahXdU%2FcmoB5OlxhdwKfTarMXD8M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2288&quot; height=&quot;576&quot; data-origin-width=&quot;2288&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;inline이 뭐지 코드 줄을 안으로 뭐 하는건가???.. 일단 코틀린에서 제공하는 고차함수 API에서 쓰이는 것으로 확인되니 장점이 있겠지??&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그래서 inline이 뭔데?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/inline-functions.html&quot; target=&quot;_self&quot;&gt;&lt;span&gt;코틀린 공식문서&lt;/span&gt;&lt;/a&gt;를 보면 다음과 같이 설명한다.&lt;br /&gt;&quot;고차함수를 사용하면, 부가적인 메모리 할당으로 인해 메모리 효율이 안 좋아지고, 함수 호출로 인한 런타임 오버헤드가 발생하게된다. 그러나 람다식을 inline으로 처리하면 이러한 종류의 오버헤드를 제거할 수 있다.&quot;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;런타임 오버헤드가 뭔데?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;런타임 오버헤드는 프로그램이 실행되는 동안 추가적으로 발생하는 비용이나 부담을 의미한다. 이는 프로그램이 실행되는 동안 발생하는 여러 가지 작업들에 대한 처리 시간이나 자원 소비 등을 포함한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아무튼..&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;고차함수를 사용하면 메모리 효율이 안좋아지고 런타임 오버헤드가 발생하는데, 이것을 inline 키워드를 통해 해결할 수 있다는 것이다.&lt;br /&gt;코드를 보면서 이야기해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;고차함수&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun function(
    n: Int,
    action: () -&amp;gt; Unit,
): Int {
    action()
    return n
}

fun main() {
    val result =
        function(10) {
            println(&quot;고차함수&quot;)
        }
    println(result)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 코드를 보게되면 function이라는 고차함수가 있고, 이 고차함수는 정수형 데이터와 람다식 총 2개의 파라미터가 있다.&lt;br /&gt;위 고차함수는 action인 람다식을 실행시키고 n을 반환한다. 얼추 코드를 이해했다면 자바 코드로 디컴파일을 해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고차함수 디컴파일&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;public final class ApplicationKt {
   public static final int function(int n, @NotNull Function0 action) {
      Intrinsics.checkNotNullParameter(action, &quot;action&quot;);
      action.invoke();
      return n;
   }

   public static final void main() {
      int result = function(10, (Function0)null.INSTANCE);
      System.out.println(result);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;코드를 보게되면 action에 람다식을 전달해주도록 구현한 부분이 새로운 객체를 생성하여 넘겨주고, 넘긴 객체를 통해 함수 호출을 하도록 구현되어 있는 것을 확인할 수 있다.&lt;br /&gt;이는 무의미하게 새로운 객체를 매번 생성하게 된다. 결국 많은 메모리를 차지하게 되고, 내부적으로 연쇄적인 함수 호출을 하게 되어 오버헤드가 발생하여 성능이 떨어질 수 있다.&lt;br /&gt;위 탐험을 통해 고차함수의 단점을 이해할 수 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;inline 고차함수&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;inline fun function(
    n: Int,
    action: () -&amp;gt; Unit,
): Int {
    action()
    return n
}

fun main() {
    val result =
        function(10) {
            println(&quot;고차함수&quot;)
        }
    println(result)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그렇다면 inline 키워드를 붙인 후 디컴파일을 해보자!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;inline 고차함수 디컴파일&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;public final class ApplicationKt {
   public static final int function(int n, @NotNull Function0 action) {
      int $i$f$function = 0;
      Intrinsics.checkNotNullParameter(action, &quot;action&quot;);
      action.invoke();
      return n;
   }

   public static final void main() {
      int n$iv = 10;
      int $i$f$function = false;
      int var3 = false;
      String var4 = &quot;고차함수&quot;;
      System.out.println(var4);
      System.out.println(n$iv);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;inline 키워드만 붙였을 뿐인데, 위와 같이 컴파일되는 형태가 달라진다.&lt;br /&gt;action()을 호출하는 부분에 람다식 내부의 코드가 그대로 복사된 것을 확인할 수 있다. 컴파일되는 바이트코드 양은 더 늘어나겠지만, 객체를 생성하거나 함수를 또 호출하는 등 비효율적인 행동은 하지 않는다.&lt;br /&gt;이러한 이유로 인라인 함수는 일반 함수보다 성능이 좋다. 인라인 함수를 사용하게 되면 코드는 객체를 항상 새로 만드는것이 아니라 해당 함수의 내용을 호출한 함수에 넣는 방식으로 컴파일 코드를 작성하게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그렇다면 inline 함수에서 받은 람다식을 다른 함수로 전달하면 어떻게 될까??&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;1570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blXAE9/btsHn41lO6z/onLoqlMU1ddCTfnuTy26t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blXAE9/btsHn41lO6z/onLoqlMU1ddCTfnuTy26t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blXAE9/btsHn41lO6z/onLoqlMU1ddCTfnuTy26t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblXAE9%2FbtsHn41lO6z%2FonLoqlMU1ddCTfnuTy26t1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;562&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;1570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;컴파일 에러가 발생한다.&lt;br /&gt;내부적으로 코드를 복사하는 개념이기 때문에, 인자로 전달받은 람다식은 다른 함수로 전달되거나 참조될 수 없다.&lt;br /&gt;그런데.. 또.. 이러한 경우에 사용할 수 있는 키워드를 코틀린에서는 제공해준다..&lt;br /&gt;바로.. noinline이다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;noinline은 뭔데?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/inline-functions.html#noinline&quot; target=&quot;_self&quot;&gt;&lt;span&gt;코틀린 공식문서&lt;/span&gt;&lt;/a&gt;를 보면 noinline을 다음과 같이 설명한다.&lt;br /&gt;&quot;인라인 함수에 전달된 모든 람다를 인라인으로 처리하지 않으려면 함수 매개 변수 중 일부에 noinline 키워드를 붙이면 된다.&quot;&lt;br /&gt;위 코드를 보면 actionB는 functionB에서 사용된다. 우리는 actionB를 전달하고싶기 때문에 noinline 키워드를 붙여보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;noinline&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4e8vG/btsHoueqkEw/iFQSon4VJb3qCxpHPMgGo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4e8vG/btsHoueqkEw/iFQSon4VJb3qCxpHPMgGo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4e8vG/btsHoueqkEw/iFQSon4VJb3qCxpHPMgGo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4e8vG%2FbtsHoueqkEw%2FiFQSon4VJb3qCxpHPMgGo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1130&quot; height=&quot;804&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;noinline을 붙이면 위와 같이 컴파일 에러가 사라지는 것을 확인할 수 있다.&lt;br /&gt;전달받은 함수들 중 일부는 다른 함수로 넘겨줘야할 때와 같이, 모든 인자를 inline 처리하고 싶지 않을 때가 있을 것이다. 이럴 때 사용하는 키워드가 바로 noinline 이다. inline 에서 제외시킬 인자 앞에 noinline 키워드를 붙이면, 그 순간 이후로 해당 인자는 다른 함수로 전달할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그러면 모든 함수에 inline을 붙이면 좋은 것인가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그것은 아니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일반 함수에서 inline&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 함수 호출의 경우에는 이미 JVM에서 강력하게 인라이닝을 지원해준다.&lt;/li&gt;
&lt;li&gt;JVM은 코드 실행을 분석해서 가장 유리한 방향으로 인라이닝 해준다.&lt;/li&gt;
&lt;li&gt;즉, 일반 함수에서 inline을 붙이면 코드 중복이 생기게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고차 함수에서 inline&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 코드를 갖고 있는 고차 함수를 inline 처리하면 바이트코드의 양이 훨씬 많아지게 된다.&lt;/li&gt;
&lt;li&gt;이 경우 성능이 오히려 악화될 수도 있다.&lt;/li&gt;
&lt;li&gt;다른 글들을 보니 inline 처리는 1~3줄, 1~5줄 정도의 길이를 권장하고 있다고 하는데 출처를 알 수 없다.&lt;/li&gt;
&lt;li&gt;공식문서에서도 큰 함수에는 inline을 사용하지 말라고만 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트를 해보았다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위에 대한 해답을 찾기 위해 인텔리제이의 Profier를 통해 inline을 사용한 경우와 사용하지 않은 경우를 CPU와 메모리 사용량을 확인하며 테스트 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식문서에서&amp;nbsp;말하는&amp;nbsp;큰&amp;nbsp;함수는&amp;nbsp;로직이&amp;nbsp;복잡한&amp;nbsp;함수 &lt;br /&gt;작은&amp;nbsp;함수는&amp;nbsp;로직이&amp;nbsp;간단한&amp;nbsp;함수&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;위 가정을 토대로 테스트를 진행했다.&lt;br /&gt;나는 간단한 로직일 수록 inline을 사용한 것이 더 효율적이고, 로직이 복잡할 수록 inline을 사용하지 않은 것이 더 효율적으로 나올 것을 예상했다.&lt;br /&gt;그러나 결과는 매번 Inline을 사용했을 떄가 효율적인 것으로 나왔다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;내가 테스트를 잘 못했을 수도 있지만 나의 결론은 다음과 같다.&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 테스트를 잘 못했을 수도 있다.&lt;/li&gt;
&lt;li&gt;생각보다 로직이 복잡하다는 의미는 내가 한번도 보지 못한 코드일 수도 있겠다.&lt;/li&gt;
&lt;li&gt;큰 함수란 기준은 사람마다 다르기 때문에, 개발한 사람이 큰 함수라고 하면 큰 함수일 수 있다.&lt;/li&gt;
&lt;li&gt;잘은 모르지만, 그냥 본인만에 기준을 잡고 inline을 사용하면 되지 않을까?&lt;/li&gt;
&lt;li&gt;나는 고차함수의 호출 횟수와 크기에 따라서 inline을 사용할 지 정할 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;탐험 일지&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;inline을 사용한다고 무조건 성능이 좋아지지는 않을 수 있다.&lt;/li&gt;
&lt;li&gt;inline은 고차 함수와 함께 적절하게 사용하는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 전달이 아닌 경우라면 JVM이 인라이닝을 해줄테니 우리는 하지말자.&lt;/li&gt;
&lt;li&gt;함수 전달 시에는 inline을 명시해 주는 게 좋다.&lt;/li&gt;
&lt;li&gt;빈번하게 사용되거나, 호출 오버헤드를 줄이기 위해 사용하는 게 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;noinline은 파라미터로 제3의 함수에 전달할 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;휴.. 이제 inline을 다 탐험했다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;는 뻥이고.. 공부하던 중 crossinline, reified, Inline properties 키워드를 발견해버렸다.. 이건 다음에 알아보도록 하자.&lt;/p&gt;</description>
      <category>Develop/Kotlin</category>
      <author>JunJangE</author>
      <guid isPermaLink="true">https://fre2-dom.tistory.com/561</guid>
      <comments>https://fre2-dom.tistory.com/561#entry561comment</comments>
      <pubDate>Sat, 6 Apr 2024 20:36:31 +0900</pubDate>
    </item>
  </channel>
</rss>