<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>시배's Android</title>
    <link>https://si8ae.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 28 Jun 2026 06:19:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>si8ae</managingEditor>
    <item>
      <title>[RevenueCat] DroidKnights 2025 컨퍼런스 후기</title>
      <link>https://si8ae.tistory.com/157</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dshgLj/btsOM98QIEt/zGmIm0L3LKaOYIkCBESwWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dshgLj/btsOM98QIEt/zGmIm0L3LKaOYIkCBESwWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dshgLj/btsOM98QIEt/zGmIm0L3LKaOYIkCBESwWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdshgLj%2FbtsOM98QIEt%2FzGmIm0L3LKaOYIkCBESwWK%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;622&quot; height=&quot;370&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;지난주, &lt;b&gt;대한민국 최대 안드로이드 개발자 행사인 드로이드나이츠 2025&lt;/b&gt;에 다녀왔습니다.&lt;br /&gt;주니어부터 시니어까지 모든 안드로이드 개발자들이 모여 지식을 나누고 공감하는 뜻깊은 자리였는데요,&lt;br /&gt;저 역시 다양한 세션을 들으며 많은 인사이트를 얻을 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;299&quot; data-start=&quot;281&quot; data-ke-size=&quot;size26&quot;&gt;✨ 내가 들은 세션 &amp;amp; 소감&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9gNZ1/btsOMVpwjxn/cqlWfPK4KRJMYWgxI3hwoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9gNZ1/btsOMVpwjxn/cqlWfPK4KRJMYWgxI3hwoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9gNZ1/btsOMVpwjxn/cqlWfPK4KRJMYWgxI3hwoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9gNZ1%2FbtsOMVpwjxn%2FcqlWfPK4KRJMYWgxI3hwoK%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;502&quot; height=&quot;586&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;343&quot; data-start=&quot;301&quot; data-ke-size=&quot;size23&quot;&gt;✅ ReadMoreTextView : 텍스트 &amp;lsquo;더보기&amp;rsquo; 기능 구현하기&lt;/h3&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;기존 라이브러리의 약간의 깜빡임 문제를 개선한 새로운 구현이 인상적이었습니다.&lt;br /&gt;특히 &lt;b&gt;RTL(LTR 포함)&lt;/b&gt; 까지 고려해서 작성된 점이 정말 신기했고, 발표자의 세심함이 느껴졌습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;530&quot; data-start=&quot;457&quot; data-ke-size=&quot;size23&quot;&gt;✅ Benchmark와 BaselineProfile을 사용해 LazyColumn 스크롤 성능을 75% 개선하기까지의 여정&lt;/h3&gt;
&lt;p data-end=&quot;651&quot; data-start=&quot;531&quot; data-ke-size=&quot;size16&quot;&gt;Benchmark를 활용해 &lt;b&gt;스크롤 성능을 수치로 측정&lt;/b&gt;하고 이를 개선하는 과정이 흥미로웠습니다.&lt;br /&gt;다만, 발표 제목과 달리 &lt;b&gt;BaselineProfile 활용 사례는 부족&lt;/b&gt;해서 조금은 아쉬움이 남았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;701&quot; data-start=&quot;658&quot; data-ke-size=&quot;size23&quot;&gt;✅ 전방위 자동화 시대, CI/CD를 넘어 PM과 QA까지 확장하기&lt;/h3&gt;
&lt;p data-end=&quot;803&quot; data-start=&quot;702&quot; data-ke-size=&quot;size16&quot;&gt;현재 회사에서는 이미 잘 구축된 CI/CD 덕분에 편하게 일하고 있지만,&lt;br /&gt;자동화가 PM과 QA까지 확장되는 사례를 보며 앞으로 더 고민해야 할 부분이라는 생각이 들었습니다.  &lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;840&quot; data-start=&quot;810&quot; data-ke-size=&quot;size23&quot;&gt;✅ Android에서 실현 가능한 모든 AI&lt;/h3&gt;
&lt;p data-end=&quot;954&quot; data-start=&quot;841&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;On-device AI&lt;/b&gt;를 가능하게 하기 위해 nano 모델을 별도로 제공한다는 점이 놀라웠습니다.&lt;br /&gt;특히 &lt;b&gt;Gemini를 활용한 국내 사례&lt;/b&gt;로 우리 회사가 소개된 부분은 반갑고 신기했습니다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;984&quot; data-start=&quot;961&quot; data-ke-size=&quot;size23&quot;&gt;✅ 당신의 클린아키텍처는 틀렸다&lt;/h3&gt;
&lt;p data-end=&quot;1066&quot; data-start=&quot;985&quot; data-ke-size=&quot;size16&quot;&gt;발표자의 의견에 대부분 공감하며 들을 수 있었던 세션이었습니다.&lt;br /&gt;기존에 생각하던 클린 아키텍처의 관점을 다시 돌아보게 되는 계기가 되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;1119&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size23&quot;&gt;✅ 나도 edgeToEdge 적용하기 싫어. 근데 누군가는 해야 하잖아?&lt;/h3&gt;
&lt;p data-end=&quot;1231&quot; data-start=&quot;1120&quot; data-ke-size=&quot;size16&quot;&gt;현재 회사에서도 &lt;b&gt;edgeToEdge 적용 시 dialog 관련 이슈&lt;/b&gt;로 골머리를 앓고 있었는데,&lt;br /&gt;이 발표 덕분에 실마리를 찾고 회사 이슈를 해결할 수 있었습니다. 정말 유익한 세션이었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1260&quot; data-start=&quot;1238&quot; data-ke-size=&quot;size26&quot;&gt;  RevenueCat 부스 후기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1050&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZqOFp/btsOOLyW7AB/kAKaWYi2GN6KEUKc9FkmZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZqOFp/btsOOLyW7AB/kAKaWYi2GN6KEUKc9FkmZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZqOFp/btsOOLyW7AB/kAKaWYi2GN6KEUKc9FkmZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZqOFp%2FbtsOOLyW7AB%2FkAKaWYi2GN6KEUKc9FkmZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;423&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1050&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opyPm/btsON5Zewj8/NCbrPKfkzkoKVY9of90nB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opyPm/btsON5Zewj8/NCbrPKfkzkoKVY9of90nB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opyPm/btsON5Zewj8/NCbrPKfkzkoKVY9of90nB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopyPm%2FbtsON5Zewj8%2FNCbrPKfkzkoKVY9of90nB1%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;361&quot; height=&quot;469&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1420&quot; data-start=&quot;1262&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;RevenueCat 부스는 귀여운 양말을 나눠주며 관람객들의 발걸음을 사로잡았습니다.&lt;br /&gt;부스에서 QR코드를 스캔하면 추첨 이벤트에 참여할 수 있었고,&lt;br /&gt;&lt;b&gt;skydoves 님의 Manifest Android Interview 책과 키보드&lt;/b&gt;를 경품으로 받을 기회도 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;1572&quot; data-start=&quot;1422&quot; data-ke-size=&quot;size16&quot;&gt;책 내용을 살펴보니 &lt;b&gt;예제 코드와 설명이 디테일하고 색상 강조도 잘 되어 있어 가독성&lt;/b&gt;이 뛰어났습니다.&lt;br /&gt;가격이 꽤 있는 편이라 회사에서 구매를 검토했지만, 이름이 Interview 라 눈치가 보여 쉽지 않을 것 같고,&lt;br /&gt;개인적으로 조만간 구매할 생각입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1598&quot; data-start=&quot;1579&quot; data-ke-size=&quot;size26&quot;&gt;  RevenueCat 소개&lt;/h2&gt;
&lt;figure id=&quot;og_1750679442043&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;In-App Subscriptions Made Easy &amp;ndash; RevenueCat&quot; data-og-description=&quot;The world&amp;rsquo;s best apps use RevenueCat to power in-app purchases, manage customer data, and grow revenue across iOS, Android, and the web.&quot; data-og-host=&quot;www.revenuecat.com&quot; data-og-source-url=&quot;https://www.revenuecat.com/&quot; data-og-url=&quot;https://www.revenuecat.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bdDqJO/hyZcnHOMZY/gYf4K7GhNb0YUKwCTvBoV0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.revenuecat.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.revenuecat.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bdDqJO/hyZcnHOMZY/gYf4K7GhNb0YUKwCTvBoV0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;In-App Subscriptions Made Easy &amp;ndash; RevenueCat&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The world&amp;rsquo;s best apps use RevenueCat to power in-app purchases, manage customer data, and grow revenue across iOS, Android, and the web.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.revenuecat.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;1720&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;RevenueCat은 &lt;b&gt;개발자들이 더 많은 수익을 올릴 수 있도록 돕는 서비스&lt;/b&gt;입니다.&lt;br /&gt;인앱 구매, 구독, 페이월 기능을 손쉽게 구현하고, 고객 데이터를 관리하며 수익화 전략을 최적화할 수 있도록 지원합니다.&lt;/p&gt;
&lt;p data-end=&quot;1731&quot; data-start=&quot;1722&quot; data-ke-size=&quot;size16&quot;&gt;  주요 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1912&quot; data-start=&quot;1732&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1762&quot; data-start=&quot;1732&quot;&gt;연간 &lt;b&gt;11조 원 이상의 인앱 결제&lt;/b&gt;를 처리&lt;/li&gt;
&lt;li data-end=&quot;1788&quot; data-start=&quot;1763&quot;&gt;손쉬운 &lt;b&gt;페이월 커스터마이징 및 게시&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1830&quot; data-start=&quot;1789&quot;&gt;&lt;b&gt;A/B 테스트&lt;/b&gt;를 통한 수익 극대화 및 사용자층 맞춤 전략 최적화&lt;/li&gt;
&lt;li data-end=&quot;1881&quot; data-start=&quot;1831&quot;&gt;&lt;b&gt;서버 드리븐 UI&lt;/b&gt; 기반 A/B 테스트, LTV 보고서, 다양한 수익화 기능 제공&lt;/li&gt;
&lt;li data-end=&quot;1912&quot; data-start=&quot;1882&quot;&gt;&lt;b&gt;비개발 직군도 사용 가능한 간편한 관리 UI&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2001&quot; data-start=&quot;1914&quot; data-ke-size=&quot;size16&quot;&gt;RevenueCat 부스를 통해 서비스 철학과 다양한 기능을 직접 들을 수 있었고,&lt;br /&gt;인앱 결제 및 구독 관리의 최신 트렌드를 이해하는 좋은 기회였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;2014&quot; data-start=&quot;2008&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-end=&quot;2107&quot; data-start=&quot;2016&quot; data-ke-size=&quot;size16&quot;&gt;드로이드나이츠 2025는 다양한 인사이트와 영감을 얻을 수 있는 값진 자리였습니다.&lt;br /&gt;세션과 부스 모두 알차게 즐길 수 있었고, 내년 행사도 벌써부터 기대됩니다!&lt;/p&gt;</description>
      <category>Android/Android</category>
      <category>droidknights</category>
      <category>droidknights 2025</category>
      <category>revenuecat</category>
      <category>skydoves</category>
      <category>드로이드나이츠</category>
      <category>드로이드나이츠 2025</category>
      <category>레비뉴 캣</category>
      <category>인앱 결제</category>
      <category>페이월</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/157</guid>
      <comments>https://si8ae.tistory.com/157#entry157comment</comments>
      <pubDate>Mon, 23 Jun 2025 20:52:03 +0900</pubDate>
    </item>
    <item>
      <title>Android | Server Driven UI 구현하기 with Custom KSerializer</title>
      <link>https://si8ae.tistory.com/156</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Server-Driven UI&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Server-Driven UI는 서버에서 UI를 정의하고 클라이언트에서 이를 동적으로 렌더링하는 방식으로, 서버에서 전달되는 JSON 형식을 파싱하여 적절한 UI를 생성하는 것이 핵심입니다. 이번 글에서는 Kotlin Serialization의 &lt;/span&gt;&lt;span&gt;&lt;b&gt;KSerializer&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 사용하여 JSON 형식을 커스텀 파싱하는 방법에 대해 다룹니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;왜 KSerializer가 필요한가?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로 JSON 데이터를 Kotlin 데이터 클래스로 매핑할 때는 Kotlin Serialization에서 제공하는 기본적인 매핑 기능을 사용할 수 있습니다. 하지만 아래와 같이 &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;이 JSON 키로 사용되고, 데이터가 해당 키의 값으로 내려오는 구조라면, 이를 효과적으로 처리하기 위해 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Custom KSerializer&lt;/b&gt;&lt;/span&gt;&lt;span&gt;가 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Json 데이터 예시 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1733988440766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;Row&quot; : {},
    &quot;Column&quot; : {},
    &quot;Text&quot; : {},
    &quot;Image&quot; : {},
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 데이터를 파싱하려면 &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;과 이를 기반으로 한 UI 컴포넌트 매핑이 필요합니다. 이를 위해 Kotlin의 &lt;/span&gt;&lt;span&gt;KSerializer&lt;/span&gt;&lt;span&gt;를 사용해 맞춤형 디코딩 로직을 구현할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;목표 Kotlin 구조&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1733988553311&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Serializable
sealed interface UiComponentResponse

@Serializable(with = UiComponentResponseWrapperSerializer::class)
data class UiComponentResponseWrapper(
    val type: String,
    val component: UiComponentResponse,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UiComponentResponse&lt;span&gt;는&lt;/span&gt; &lt;span&gt;다양한&lt;/span&gt; UI &lt;span&gt;컴포넌트를&lt;/span&gt; &lt;span&gt;나타내는&lt;/span&gt; sealed interface&lt;span&gt;이며&lt;/span&gt;, &lt;span&gt;이를&lt;/span&gt; &lt;span&gt;감싸는&lt;/span&gt; UiComponentResponseWrapper&lt;span&gt;는&lt;/span&gt; JSON &lt;span&gt;데이터를&lt;/span&gt; type&lt;span&gt;과&lt;/span&gt; component&lt;span&gt;로&lt;/span&gt; &lt;span&gt;구분하여&lt;/span&gt; &lt;span&gt;파싱할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있도록&lt;/span&gt; &lt;span&gt;설계되었습니다&lt;/span&gt;.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Custom KSerializer 구현하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;KSerializer&lt;/span&gt;&lt;span&gt;는 Kotlin Serialization에서 커스텀 직렬화/역직렬화 로직을 구현하는 핵심 도구입니다. 아래는 &lt;/span&gt;&lt;span&gt;UiComponentResponseWrapper&lt;/span&gt;&lt;span&gt;를 처리하기 위한 Custom KSerializer의 구현입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1733988638393&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object UiComponentResponseWrapperSerializer : KSerializer&amp;lt;UiComponentResponseWrapper&amp;gt; {
    override val descriptor: SerialDescriptor =
        buildClassSerialDescriptor(&quot;UiComponentResponseWrapper&quot;) {
            element(&quot;type&quot;, String.serializer().descriptor)
            element(&quot;component&quot;, buildClassSerialDescriptor(&quot;UiComponentUnion&quot;))
        }

    override fun deserialize(decoder: Decoder): UiComponentResponseWrapper {
        val jsonObject = (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonObject
            ?: throw SerializationException(&quot;Expected JSON object&quot;)

        val entry = jsonObject.entries.firstOrNull()
            ?: throw SerializationException(&quot;Empty JSON object&quot;)

        val type = entry.key

        val element = when (type) {
            &quot;Text&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;TextResponse&amp;gt;(entry.value)
            &quot;Column&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;ColumnResponse&amp;gt;(entry.value)
            &quot;Row&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;RowResponse&amp;gt;(entry.value)
            &quot;Box&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;BoxResponse&amp;gt;(entry.value)
            &quot;Image&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;ImageResponse&amp;gt;(entry.value)
            &quot;HorizontalGrid&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;HorizontalGridResponse&amp;gt;(entry.value)
            &quot;VerticalGrid&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;VerticalGridResponse&amp;gt;(entry.value)
            &quot;Icon&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;IconResponse&amp;gt;(entry.value)
            &quot;Spacer&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;SpacerResponse&amp;gt;(entry.value)
            &quot;List&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;ListResponse&amp;gt;(entry.value)
            &quot;Divider&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;DividerResponse&amp;gt;(entry.value)
            &quot;LazyColumn&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;LazyColumnResponse&amp;gt;(entry.value)
            &quot;LazyRow&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;LazyRowResponse&amp;gt;(entry.value)
            &quot;TopAppBar&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;TopAppBarResponse&amp;gt;(entry.value)
            &quot;HorizontalPager&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;HorizontalPagerResponse&amp;gt;(entry.value)
            &quot;VerticalPager&quot; -&amp;gt; Json.decodeFromJsonElement&amp;lt;VerticalPagerResponse&amp;gt;(entry.value)
            else -&amp;gt; throw SerializationException(&quot;Unknown component type: $type&quot;)
        }

        return UiComponentResponseWrapper(type, element)
    }

    override fun serialize(encoder: Encoder, value: UiComponentResponseWrapper) {
        val jsonObject = buildJsonObject {
            put(value.type, Json.encodeToJsonElement(value.component))
        }
        (encoder as? JsonEncoder)?.encodeJsonElement(jsonObject)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-pm-slice=&quot;1 5 []&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;주요 구현 포인트&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;true&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Descriptor 정의&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;UiComponentResponseWrapper&lt;/span&gt;&lt;span&gt;의 구조를 정의하여 JSON 직렬화/역직렬화 시 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;은 JSON 키, &lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;는 값으로 매핑됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;deserialize&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;JSON 데이터를 디코딩하여 첫 번째 엔트리(&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;)에 따라 적절한 데이터 클래스를 생성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;when&lt;/span&gt;&lt;span&gt; 문을 통해 타입별 매핑을 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;serialize&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Kotlin 객체를 JSON으로 변환할 때 &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;과 &lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;를 포함하는 JSON 객체를 생성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;예시&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Realtime Database를 활용하여, 저의 사이드 프로젝트인 &lt;b&gt;Cherish 앱&lt;/b&gt;의 메인 화면을 JSON 형태로 서버에서 내려받아 Server-Driven UI로 구현해 보았습니다.&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2024-12-124.38.19-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1ejLM/btsLhuUKRFA/STH6RLsr1VZAoSmrnclth0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1ejLM/btsLhuUKRFA/STH6RLsr1VZAoSmrnclth0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1ejLM/btsLhuUKRFA/STH6RLsr1VZAoSmrnclth0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b1ejLM/btsLhuUKRFA/STH6RLsr1VZAoSmrnclth0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;772&quot; height=&quot;435&quot; data-filename=&quot;2024-12-124.38.19-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/koreatlwls/server-driven-ui&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/koreatlwls/server-driven-ui&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Android</category>
      <category>custom json parsing</category>
      <category>custom serializable</category>
      <category>json parsing</category>
      <category>Kotlin</category>
      <category>kotlinx</category>
      <category>SERIALIZABLE</category>
      <category>Serialization</category>
      <category>server</category>
      <category>server driven</category>
      <category>server driven ui</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/156</guid>
      <comments>https://si8ae.tistory.com/156#entry156comment</comments>
      <pubDate>Thu, 12 Dec 2024 16:42:40 +0900</pubDate>
    </item>
    <item>
      <title>Android | Firebase Realtime Database를 더 쉽게 사용하기</title>
      <link>https://si8ae.tistory.com/155</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Firebase Realtime Database를 더 쉽게 사용하기: flowList() 함수로 오픈소스 기여 이야기&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1732495409624&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - skydoves/firebase-android-ktx:   Kotlin &amp;amp; Compose-friendly Firebase extensions designed to help you focus on your bu&quot; data-og-description=&quot;  Kotlin &amp;amp; Compose-friendly Firebase extensions designed to help you focus on your business logic. - skydoves/firebase-android-ktx&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/skydoves/firebase-android-ktx&quot; data-og-url=&quot;https://github.com/skydoves/firebase-android-ktx&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwlEjd/hyXDd7YdeV/fWN42KqZuPG17v3EKkzmZk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bp3rdK/hyXDmqjArk/qL896WHz1J96EfSbi0vks1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/skydoves/firebase-android-ktx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/skydoves/firebase-android-ktx&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwlEjd/hyXDd7YdeV/fWN42KqZuPG17v3EKkzmZk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bp3rdK/hyXDmqjArk/qL896WHz1J96EfSbi0vks1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - skydoves/firebase-android-ktx:   Kotlin &amp;amp; Compose-friendly Firebase extensions designed to help you focus on your bu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Kotlin &amp;amp; Compose-friendly Firebase extensions designed to help you focus on your business logic. - skydoves/firebase-android-ktx&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요&lt;span&gt;! &lt;/span&gt;이번&lt;span&gt; &lt;/span&gt;글에서는&lt;span&gt; &lt;/span&gt;제가&lt;span&gt; &lt;/span&gt;최근에&lt;b&gt;&lt;span&gt; firebase-android-ktx &lt;/span&gt;&lt;/b&gt;오픈소스&lt;span&gt; &lt;/span&gt;프로젝트에&lt;span&gt; &lt;/span&gt;기여한&lt;span&gt; &lt;/span&gt;경험을&lt;span&gt; &lt;/span&gt;공유하고자&lt;span&gt; &lt;/span&gt;합니다&lt;span&gt;. &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;글에서는&lt;span&gt; &lt;/span&gt;제가&lt;span&gt; &lt;/span&gt;추가한&lt;span&gt; flowList() &lt;/span&gt;함수의&lt;span&gt; &lt;/span&gt;동작&lt;span&gt; &lt;/span&gt;원리&lt;span&gt;, &lt;/span&gt;작성&lt;span&gt; &lt;/span&gt;배경&lt;span&gt;, &lt;/span&gt;그리고&lt;span&gt; &lt;/span&gt;오픈소스에&lt;span&gt; &lt;/span&gt;기여하면서&lt;span&gt; &lt;/span&gt;느낀&lt;span&gt; &lt;/span&gt;점을&lt;span&gt; &lt;/span&gt;다뤄보겠습니다&lt;span&gt;.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 기여한 내용: flowList() 함수&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Realtime Database를 사용하다 보면 리스트 형식으로 데이터를 저장하고 이를 읽어오는 경우가 많습니다. 하지만 기존의 API는 데이터 변화를 비동기적으로 감지하기 어렵고, 이를 간편하게 처리하기 위한 유틸리티 함수가 부족했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이러한 필요성을 느끼고, 데이터를 &lt;b&gt;Flow&amp;lt;Result&amp;lt;List&amp;lt;T?&amp;gt;&amp;gt;&amp;gt;&lt;/b&gt; 타입으로 반환하는 &lt;b&gt;flowList()&lt;/b&gt; 함수를 추가했습니다. 이 함수는 데이터를 실시간으로 구독할 수 있고, Result를 통해 성공과 실패를 명확히 구분합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;zwj;  추가한 코드: flowList()&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1732494770598&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public fun &amp;lt;T : Any&amp;gt; DatabaseReference.flowList(
  path: (DataSnapshot) -&amp;gt; DataSnapshot,
  decodeProvider: (String) -&amp;gt; T
): Flow&amp;lt;Result&amp;lt;List&amp;lt;T?&amp;gt;&amp;gt;&amp;gt; = callbackFlow {
  val listener = object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
      val children = path.invoke(snapshot).children
      val result = mutableListOf&amp;lt;T?&amp;gt;()
      for (data in children) {
        result.add(data.serializedValue(decodeProvider))
      }
      trySend(Result.success(result))
    }

    override fun onCancelled(error: DatabaseError) {
      trySend(Result.failure(error.toException()))
    }
  }
  addValueEventListener(listener)
  awaitClose { removeEventListener(listener) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 설명&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;callbackFlow를 사용한 Flow 생성&lt;/b&gt;&lt;br /&gt;이 함수는 callbackFlow를 활용해 Firebase의 ValueEventListener 이벤트를 Flow로 변환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;path를 통한 데이터 경로 설정&lt;/b&gt;&lt;br /&gt;path는 DataSnapshot에서 필요한 하위 데이터를 선택할 수 있도록 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;decodeProvider를 통한 데이터 디코딩&lt;/b&gt;&lt;br /&gt;각 데이터 항목을 디코딩하는 방법을 decodeProvider로 정의할 수 있어 다양한 타입의 데이터를 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Result를 활용한 상태 관리&lt;/b&gt;&lt;br /&gt;데이터를 성공적으로 가져오거나 실패 시 예외를 반환하는 구조로 안정성과 가독성을 높였습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  동기: 왜 이 기능이 필요했는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 제가 진행하던 &lt;b&gt;Server-Driven UI 프로젝트&lt;/b&gt;에서 필요성을 느껴 기여하게 되었습니다. 해당 프로젝트는 UI와 데이터를 서버에서 받아 동적으로 렌더링해야 했고, 리스트 데이터를 실시간으로 구독해야 했습니다. 기존 API로는 비효율적이라 이를 더 간결하게 처리할 수 있는 유틸리티 함수가 필요했죠.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✨ 결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기여는 Firebase Realtime Database를 더 쉽게 사용할 수 있도록 돕기 위한 작은 발걸음이었지만, 저에게는 큰 의미가 있었습니다. 앞으로도 다양한 오픈소스 프로젝트에 기여하며 개발자로서 성장해 나가고 싶습니다.&lt;/p&gt;</description>
      <category>Android/Android</category>
      <category>Android</category>
      <category>Firebase</category>
      <category>firebase ktx</category>
      <category>firebase realtime database</category>
      <category>flow</category>
      <category>Kotlin</category>
      <category>RDB</category>
      <category>Realtime Database</category>
      <category>skydoves</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/155</guid>
      <comments>https://si8ae.tistory.com/155#entry155comment</comments>
      <pubDate>Mon, 25 Nov 2024 09:45:33 +0900</pubDate>
    </item>
    <item>
      <title>Android | Build Optimization (2)</title>
      <link>https://si8ae.tistory.com/154</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Gradle 옵션을 활용한 Build Optimization&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 Android 프로젝트에서 빌드 시간이 느려지는 것은 큰 생산성 저하를 야기할 수 있습니다. 이 문제를 해결하기 위해, Gradle은 다양한 빌드 최적화 옵션을 제공하고 있으며, 이를 적절히 활용하면 빌드 시간을 크게 단축할 수 있습니다. 이번 포스트에서는 Gradle 옵션을 통해 실제로 빌드 시간을 얼마나 줄일 수 있는지 그 과정을 공유하고, 각 옵션에 대해 설명하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 환경에서의 빌드 시간&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 아무 옵션도 적용하지 않은 기본 프로젝트 환경에서의 빌드 시간을 측정했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Clean Build -&amp;gt; Build APK&lt;/b&gt;&lt;br /&gt;결과: &lt;b&gt;1분 52초&lt;/b&gt;&lt;br /&gt;처음에는 기본적인 clean 빌드를 수행했으며, 상당히 긴 시간이 소요되는 것을 확인했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Gradle 최적화 옵션을 하나씩 적용해 보며, 빌드 시간을 얼마나 줄일 수 있는지 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;278&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhsgxi/btsJSIPrISq/FqDih3SzDX8KQtBb3UH7cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhsgxi/btsJSIPrISq/FqDih3SzDX8KQtBb3UH7cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhsgxi/btsJSIPrISq/FqDih3SzDX8KQtBb3UH7cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhsgxi%2FbtsJSIPrISq%2FFqDih3SzDX8KQtBb3UH7cK%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;278&quot; height=&quot;58&quot; data-origin-width=&quot;278&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;org.gradle.parallel=true 옵션&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.gradle.parallel=true 옵션을 활성화하면 Gradle은 가능한 경우 여러 작업을 병렬로 실행합니다. 특히 멀티모듈 프로젝트에서 각 모듈의 독립적인 작업들이 병렬로 수행되면서 빌드 시간이 단축될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;br /&gt;프로젝트의 gradle.properties 파일에 다음을 추가합니다:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1727861124120&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.gradle.parallel=true&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빌드 결과&lt;/b&gt;&lt;br /&gt;결과: &lt;b&gt;1분 26초&lt;/b&gt;&lt;br /&gt;빌드 시간이 약 26초 단축되었습니다. 이는 Gradle이 여러 모듈을 동시에 빌드하면서 빌드 과정의 효율성을 높였기 때문입니다. 이 때, &lt;b&gt;Gradle Configuration&lt;/b&gt; 시간은 약 11초가 소요되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYXwjy/btsJSQGws0E/nGWT1J7ysmDclIzUBqKk20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYXwjy/btsJSQGws0E/nGWT1J7ysmDclIzUBqKk20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYXwjy/btsJSQGws0E/nGWT1J7ysmDclIzUBqKk20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYXwjy%2FbtsJSQGws0E%2FnGWT1J7ysmDclIzUBqKk20%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;284&quot; height=&quot;46&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OffWL/btsJS7nNE2M/x0v78XCkKkQcch2eNMkCUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OffWL/btsJS7nNE2M/x0v78XCkKkQcch2eNMkCUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OffWL/btsJS7nNE2M/x0v78XCkKkQcch2eNMkCUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOffWL%2FbtsJS7nNE2M%2Fx0v78XCkKkQcch2eNMkCUk%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;906&quot; height=&quot;129&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;org.gradle.unsafe.configuration-cache=true 옵션&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.gradle.unsafe.configuration-cache=true 옵션은 Gradle 빌드의 구성 단계(configuration phase)를 캐시하여, 동일한 설정의 빌드를 다시 할 때 빌드 구성을 재활용할 수 있게 합니다. 이를 통해 구성 단계에 소요되는 시간을 절감할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;br /&gt;프로젝트의 gradle.properties 파일에 다음을 추가합니다:&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;org.gradle.parallel=true
org.gradle.unsafe.configuration-cache=true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빌드 결과&lt;/b&gt;&lt;br /&gt;결과: &lt;b&gt;1분 20초&lt;/b&gt;&lt;br /&gt;빌드 시간이 더 줄었으며, 구성 단계 시간도 7초로 줄어들었습니다. &lt;b&gt;Configuration Cache&lt;/b&gt; 덕분에, 동일한 빌드를 여러 번 반복할 때 빌드 구성을 캐시에서 재사용함으로써 불필요한 설정 작업이 생략됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EvUVv/btsJUl6cP7n/sDNzcdjnOBITZcAIv1kajK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EvUVv/btsJUl6cP7n/sDNzcdjnOBITZcAIv1kajK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EvUVv/btsJUl6cP7n/sDNzcdjnOBITZcAIv1kajK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEvUVv%2FbtsJUl6cP7n%2FsDNzcdjnOBITZcAIv1kajK%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;270&quot; height=&quot;76&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N5dcM/btsJUN9bl0L/usimSj2KXvJy1pOrGZl5Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N5dcM/btsJUN9bl0L/usimSj2KXvJy1pOrGZl5Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N5dcM/btsJUN9bl0L/usimSj2KXvJy1pOrGZl5Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN5dcM%2FbtsJUN9bl0L%2FusimSj2KXvJy1pOrGZl5Z0%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;1796&quot; height=&quot;240&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;br /&gt;&lt;b&gt;org.gradle.caching=true 옵션과 로컬 빌드 캐시&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.gradle.caching=true 옵션은 작업 결과물(output)을 캐싱하여, 동일한 입력에 대해 동일한 출력이 요구될 때 캐시된 결과를 재사용하도록 합니다. 특히 코드 변경이 없는 상태에서 동일한 작업을 다시 실행할 때 큰 효과를 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 로컬 캐시를 활용하면, 이전 빌드 결과를 로컬 디스크에 저장하여 더 빠른 빌드가 가능합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;br /&gt;프로젝트의 gradle.properties 파일에 다음을 추가합니다:&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;org.gradle.parallel=true
org.gradle.unsafe.configuration-cache=true
org.gradle.caching=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;프로젝트의 setting.gradle.properties 파일에 다음을 추가합니다:&lt;/p&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1727861703760&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildCache {
    local {
        isEnabled = true
        directory = File(rootDir, &quot;build-cache&quot;)
        removeUnusedEntriesAfterDays = 30
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빌드 결과&lt;/b&gt;&lt;br /&gt;결과: &lt;b&gt;20초&lt;/b&gt;&lt;br /&gt;로컬 캐싱 덕분에 빌드 시간이 20초로 크게 단축되었습니다. 이 옵션은 특히 clean 빌드 이후 변경이 거의 없는 상태에서 빌드를 반복할 때 매우 유용합니다. 빌드가 완료되면 Gradle은 빌드 결과를 캐시에 저장하고, 이후 동일한 작업에 대해 캐시된 결과를 재사용함으로써 빌드 시간이 비약적으로 단축됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsijHQ/btsJTRR9DL3/y020BuDgkXveGX8xATI9ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsijHQ/btsJTRR9DL3/y020BuDgkXveGX8xATI9ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsijHQ/btsJTRR9DL3/y020BuDgkXveGX8xATI9ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsijHQ%2FbtsJTRR9DL3%2Fy020BuDgkXveGX8xATI9ek%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;200&quot; height=&quot;52&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;52&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>android build</category>
      <category>android build optimization</category>
      <category>Android Studio</category>
      <category>build optimization</category>
      <category>gradle</category>
      <category>gradle cache</category>
      <category>Kotlin</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/154</guid>
      <comments>https://si8ae.tistory.com/154#entry154comment</comments>
      <pubDate>Fri, 4 Oct 2024 19:36:11 +0900</pubDate>
    </item>
    <item>
      <title>Android | Build Optimization (1)</title>
      <link>https://si8ae.tistory.com/153</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Build Optimization&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 안드로이드 프로젝트에서 빌드 시간이 느려지는 것은 개발 생산성을 크게 저해하는 문제 중 하나입니다. 이를 해결하기 위한 방법으로 &lt;b&gt;multi modularization&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;size16&quot;&gt;다수의 모듈이 특정 구현체 모듈을 참조할 경우, 해당 구현체가 변경될 때마다 참조하는 모든 모듈이 다시 빌드되는 문제가 발생합니다. 이러한 상황에서는 모듈 분리만으로는 빌드 최적화가 부족하며, 참조 모듈이 불필요하게 빌드되지 않도록 하는 추가적인 전략이 필요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Dependency Inversion을 활용한 해결책&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위한 방법 중 하나는 impl(implementation) 모듈과 api 모듈을 분리하는 방식입니다. 이 구조에서는 impl 모듈이 오직 app 모듈에서만 참조되며, 나머지 모듈은 api 모듈을 참조하도록 합니다. 이를 통해 api 모듈을 참조하는 모듈들은 impl 모듈의 변경사항에 영향을 받지 않으며, 추가적인 빌드가 발생하지 않도록 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 공식 문서에서도 이 방식을 Dependency Inversion이라는 개념으로 소개하고 있습니다. Dependency Inversion은 구현체(Concrete Implementation)와 추상화(Abstraction)를 분리하여 모듈 간 의존성을 줄이는 방법입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEswnd/btsJTHa3vxN/JZoK43YtyAsKclAvWfcpu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEswnd/btsJTHa3vxN/JZoK43YtyAsKclAvWfcpu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEswnd/btsJTHa3vxN/JZoK43YtyAsKclAvWfcpu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEswnd%2FbtsJTHa3vxN%2FJZoK43YtyAsKclAvWfcpu1%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;617&quot; height=&quot;255&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1727855307762&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;일반적인 모듈화 패턴 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 일반적인 모듈화 패턴 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 모든 프로젝트에 맞는 하나의 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/topic/modularization/patterns#dependency_inversion&quot; data-og-url=&quot;https://developer.android.com/topic/modularization/patterns?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mSIGP/hyXaC8GQTL/7C8PTGK0WZrZaKAVKFNNVK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/DJ9H9/hyXaBIS9td/YnzHD5bvpxAQdbsf3dymKK/img.png?width=1443&amp;amp;height=589&amp;amp;face=0_0_1443_589,https://scrap.kakaocdn.net/dn/4Ndiu/hyXaG4wAio/kfS8BysJCcoJjpDzxKKxmk/img.png?width=1241&amp;amp;height=467&amp;amp;face=0_0_1241_467&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/modularization/patterns#dependency_inversion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/topic/modularization/patterns#dependency_inversion&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mSIGP/hyXaC8GQTL/7C8PTGK0WZrZaKAVKFNNVK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/DJ9H9/hyXaBIS9td/YnzHD5bvpxAQdbsf3dymKK/img.png?width=1443&amp;amp;height=589&amp;amp;face=0_0_1443_589,https://scrap.kakaocdn.net/dn/4Ndiu/hyXaG4wAio/kfS8BysJCcoJjpDzxKKxmk/img.png?width=1241&amp;amp;height=467&amp;amp;face=0_0_1241_467');&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;일반적인 모듈화 패턴 &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를 통해 번역되었습니다. 일반적인 모듈화 패턴 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 모든 프로젝트에 맞는 하나의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 통해, 각 모듈은 구체적인 구현에 의존하지 않고, 추상화된 API에만 의존하게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예시: 데이터베이스 모듈&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 어떤 기능 모듈이 데이터베이스에 의존한다고 가정해 봅시다. 이때 해당 기능 모듈은 구체적으로 어떤 데이터베이스를 사용하는지에 대해 알 필요가 없습니다. 로컬 Room 데이터베이스든, 원격 Firestore든 상관없이 데이터만 저장하고 불러오는 역할을 할 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 모듈은 데이터베이스 API를 제공하는 추상화된 모듈에 의존하며, 구체적인 데이터베이스 구현은 별도의 구현체 모듈에서 처리됩니다. 이때 구현체 모듈 역시 추상화 모듈에 의존하여, 추상화된 API 규약을 따르게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;의존성 주입(Dependency Injection) 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 의문이 생길 수 있습니다. &quot;기능 모듈은 어떻게 구현체 모듈과 연결되는가?&quot; 그 해답은 의존성 주입(Dependency Injection)입니다. 기능 모듈은 필요한 데이터베이스 인스턴스를 직접 생성하지 않고, 외부에서 주입받습니다. 일반적으로는 app 모듈에서 이 역할을 수행하며, Hilt 같은 DI 프레임워크를 사용하여 구현체와 API를 연결할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API와 구현체를 분리함으로써 얻을 수 있는 이점은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;교체 가능성(Interchangeability)&lt;/b&gt;: 동일한 API를 기반으로 여러 구현체를 개발할 수 있으며, 이를 상황에 따라 자유롭게 교체할 수 있습니다. 예를 들어, 테스트 환경에서는 Mock 구현을 사용하고, 실제 배포 환경에서는 실제 구현을 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디커플링(Decoupling)&lt;/b&gt;: 모듈들이 특정 구현체에 의존하지 않기 때문에, 예를 들어 Room에서 Firestore로 데이터베이스를 변경하더라도, 변경 사항은 구현체 모듈에만 영향을 미칩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성(Testability)&lt;/b&gt;: 구현체를 분리함으로써 API 규약을 기준으로 테스트를 작성할 수 있으며, 다양한 시나리오를 테스트하기 위한 Mock 구현을 쉽게 활용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌드 성능 향상(Build Performance)&lt;/b&gt;: API와 구현체를 분리하면, 구현체 모듈의 변경이 API 모듈에 의존하는 다른 모듈에 영향을 주지 않습니다. 결과적으로 불필요한 빌드가 줄어들어 빌드 시간이 단축됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실제 성능 개선 사례&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 위 방식으로 data 모듈을 분리하고 증분 빌드 시간을 측정해본 결과, 약 &lt;b&gt;20초&lt;/b&gt;에서 &lt;b&gt;10초&lt;/b&gt; 가량의 유의미한 성능 차이를 확인할 수 있었습니다. 이러한 최적화는 대규모 프로젝트에서 개발 속도를 크게 향상시키며, 더 나은 개발 경험을 제공합니다.&lt;/p&gt;</description>
      <category>Android/Android</category>
      <category>Android</category>
      <category>Android Studio</category>
      <category>API</category>
      <category>build</category>
      <category>Dependency</category>
      <category>gradle</category>
      <category>Impl</category>
      <category>inversion</category>
      <category>Kotlin</category>
      <category>multi modularization</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/153</guid>
      <comments>https://si8ae.tistory.com/153#entry153comment</comments>
      <pubDate>Wed, 2 Oct 2024 17:02:41 +0900</pubDate>
    </item>
    <item>
      <title>Hi Jack Mocker | 개선기 (5) feat.SharedFlow</title>
      <link>https://si8ae.tistory.com/152</link>
      <description>&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Hi Jack Mocker란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1721910672792&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&quot; data-og-description=&quot;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nu5PF/hyWCMR8vp4/ydQGAl7pbjk6p4NNv9Fag1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nu5PF/hyWCMR8vp4/ydQGAl7pbjk6p4NNv9Fag1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hi Jack Mocker&lt;/b&gt;는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;소개&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답을 가로채고 수정한 후 다시 전달하는 과정에서 흥미로운 문제에 직면했고, 이를 해결하는 과정에서 코틀린의 Channel에서 SharedFlow로 전환하게 되었습니다. 이 포스트에서는 그 과정과 학습한 내용을 공유하고자 합니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;초기 구현 : Channel 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 다음과 같은 방식으로 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721911276068&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
    var response = chain.proceed(chain.request())

    if (hjmDataStore.getHjmMode()) {
        interceptorManager.sendWithInterceptorChannel(response)

        startHjmActivityIfNeeded()
        response = interceptorManager.receiveWithResultChannel()
    }

    return@runBlocking response
}
 suspend fun receiveWithResultChannel(): Response {
    if (resultChannel.isClosedForReceive) {
        resultChannel = Channel(UNLIMITED)
    }

    return resultChannel.receive()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구현에서는 Channel을 사용하여 HjmActivity에서 수정된 응답을 다시 Interceptor로 전달했습니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #333333; 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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 접근 방식은 단일 요청에 대해서는 잘 작동했지만, 여러 개의 동시 요청을 처리할 때 문제가 발생했습니다. Channel의 특성상 첫 번째로 도착한 응답이 첫 번째로 대기 중인 Interceptor에 전달되는 문제가 있었습니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결책 : SharedFlow 도입&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 SharedFlow를 도입했습니다. SharedFlow는 여러 소비자가 동시에 값을 관찰할 수 있어, 우리의 사용 사례에 더 적합했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721911379036&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
    var response = chain.proceed(chain.request())

    if (hjmDataStore.getHjmMode()) {
        val uuid = UUID.randomUUID().toString()
        interceptorManager.sendEventAtInterceptorEvent(uuid, response)

        startHjmActivityIfNeeded()

        response = interceptorManager.resultEvent.filter { it.first == uuid }.first().second
    }

    return@runBlocking response
}

class InterceptorManager {
    val isHjmActivityRunning = AtomicBoolean(false)

    private val _interceptorEvent = MutableSharedFlow&amp;lt;Pair&amp;lt;String, Response&amp;gt;&amp;gt;(replay = 1)
    val interceptorEvent = _interceptorEvent.asSharedFlow()

    private val _resultEvent = MutableSharedFlow&amp;lt;Pair&amp;lt;String, Response&amp;gt;&amp;gt;()
    val resultEvent = _resultEvent.asSharedFlow()

    suspend fun sendEventAtInterceptorEvent(uuid: String, response: Response) {
        _interceptorEvent.emit(Pair(uuid, response))
    }

    suspend fun sendEventAtResultEvent(uuid: String, response: Response) {
        _resultEvent.emit(Pair(uuid, response))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Channel과 SharedFlow의 주요 차이점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;1.&amp;nbsp;데이터&amp;nbsp;공유&amp;nbsp;방식:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel:&amp;nbsp;point-to-point&amp;nbsp;통신&amp;nbsp;방식으로,&amp;nbsp;하나의&amp;nbsp;sender와&amp;nbsp;하나의&amp;nbsp;receiver가&amp;nbsp;연결되어&amp;nbsp;데이터를&amp;nbsp;주고받습니다.&amp;nbsp;즉,&amp;nbsp;각각의&amp;nbsp;collector는&amp;nbsp;독립적인&amp;nbsp;데이터&amp;nbsp;스트림을&amp;nbsp;받습니다.&lt;br /&gt;SharedFlow:&amp;nbsp;broadcast&amp;nbsp;통신&amp;nbsp;방식으로,&amp;nbsp;하나의&amp;nbsp;sender가&amp;nbsp;여러&amp;nbsp;receiver에게&amp;nbsp;동일한&amp;nbsp;데이터를&amp;nbsp;전송합니다.&amp;nbsp;즉,&amp;nbsp;모든&amp;nbsp;collector가&amp;nbsp;동일한&amp;nbsp;데이터&amp;nbsp;스트림을&amp;nbsp;공유합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp;데이터&amp;nbsp;버퍼링:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel: 버퍼 크기를 지정할 수 있으며, 버퍼가 가득 차면 sender는 receiver가 데이터를 가져갈 때까지 대기합니다. (blocking)&lt;/li&gt;
&lt;li&gt;SharedFlow:&amp;nbsp;replay,&amp;nbsp;extraBufferCapacity&amp;nbsp;설정을&amp;nbsp;통해&amp;nbsp;버퍼링&amp;nbsp;동작을&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;기본적으로&amp;nbsp;최신&amp;nbsp;데이터만&amp;nbsp;유지하며,&amp;nbsp;이전&amp;nbsp;데이터는&amp;nbsp;버려집니다.&amp;nbsp;(non-blocking)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp;사용&amp;nbsp;용도:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel: 주로 coroutine 간의 직접적인 통신에 사용됩니다. 예를 들어, producer coroutine에서 생성한 데이터를 consumer coroutine으로 전달하는 데 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;SharedFlow:&amp;nbsp;여러&amp;nbsp;coroutine&amp;nbsp;또는&amp;nbsp;컴포넌트가&amp;nbsp;동일한&amp;nbsp;데이터를&amp;nbsp;관찰해야&amp;nbsp;하는&amp;nbsp;경우에&amp;nbsp;유용합니다.&amp;nbsp;예를&amp;nbsp;들어,&amp;nbsp;앱의&amp;nbsp;상태&amp;nbsp;변경을&amp;nbsp;여러&amp;nbsp;화면에서&amp;nbsp;관찰해야&amp;nbsp;하는&amp;nbsp;경우에&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.&amp;nbsp;Cold&amp;nbsp;vs.&amp;nbsp;Hot:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel:&amp;nbsp;cold&amp;nbsp;stream으로,&amp;nbsp;collector가&amp;nbsp;collect를&amp;nbsp;호출할&amp;nbsp;때마다&amp;nbsp;새로운&amp;nbsp;스트림이&amp;nbsp;생성됩니다.&lt;/li&gt;
&lt;li&gt;SharedFlow:&amp;nbsp;hot&amp;nbsp;stream으로,&amp;nbsp;collector의&amp;nbsp;존재&amp;nbsp;여부와&amp;nbsp;관계없이&amp;nbsp;데이터를&amp;nbsp;방출합니다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android/Hi Jack Mocker</category>
      <category>api mocking</category>
      <category>channel</category>
      <category>hi jack mocker</category>
      <category>Kotlin</category>
      <category>kotlin channel</category>
      <category>kotlin sharedflow</category>
      <category>Mocking</category>
      <category>okhttp3</category>
      <category>retrofit2</category>
      <category>SharedFlow</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/152</guid>
      <comments>https://si8ae.tistory.com/152#entry152comment</comments>
      <pubDate>Thu, 25 Jul 2024 21:49:49 +0900</pubDate>
    </item>
    <item>
      <title>Compose | Pager OOM(Out of Memory) issue</title>
      <link>https://si8ae.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 진행 중인 홈노크타운 프로젝트에서 Hi-Jack-Mocker 라이브러리를 연동하는 과정에서 OOM 문제를 겪었습니다. 이 문제를 해결하기 위해 여러 시도를 해보았고, 그 과정을 공유하고자 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3476&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djSoSo/btsIElOYULX/Pccfbgnp7QKsQbHTiATwEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djSoSo/btsIElOYULX/Pccfbgnp7QKsQbHTiATwEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djSoSo/btsIElOYULX/Pccfbgnp7QKsQbHTiATwEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjSoSo%2FbtsIElOYULX%2FPccfbgnp7QKsQbHTiATwEK%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;3476&quot; height=&quot;242&quot; data-origin-width=&quot;3476&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1721361814674&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&quot; data-og-description=&quot;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&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;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Hi Jack Mocker&lt;/b&gt;는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제의 시작 : 무한 스크롤 배너&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhNxTN/btsIFVA51zx/GuoHFZynOTZIZYelL0PC60/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhNxTN/btsIFVA51zx/GuoHFZynOTZIZYelL0PC60/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhNxTN/btsIFVA51zx/GuoHFZynOTZIZYelL0PC60/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bhNxTN/btsIFVA51zx/GuoHFZynOTZIZYelL0PC60/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;267&quot; height=&quot;1333&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;홈노크타운 프로젝트 상단 배너&lt;/b&gt;: 프로젝트 상단에 사용자가 무한 스크롤할 수 있는 롤링 배너가 존재합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PagerState 설정&lt;/b&gt;: 이 배너는 무한 스크롤 기능을 위해 PagerState의 pageCount를 Int.MAX_VALUE로 설정했습니다.&lt;/li&gt;
&lt;/ul&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;b&gt;Compose 버전 차이&lt;/b&gt;: 홈노크타운 프로젝트는 비교적 낮은 버전의 Compose를 사용하고 있었습니다. 하지만 HiJackMocker는 최신 Compose 버전을 사용하고 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 추가&lt;/b&gt;: 개발 도중 HiJackMocker를 devImplementation에 추가한 후, OOM 문제가 발생하기 시작했습니다.&lt;/li&gt;
&lt;/ul&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;b&gt;LeakCanary 사용&lt;/b&gt;: 메모리 누수를 의심해 LeakCanary를 추가하여 살펴보았지만, 문제점을 발견하지 못했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인 추적&lt;/b&gt;: 우연히 배너가 스와이핑되지 않아 배너를 제거하고 확인해보니 OOM이 발생하지 않았습니다. 이를 통해 배너와 관련된 문제가 있다는 것을 알게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 과정&amp;nbsp;&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;b&gt;Issue Tracker 확인&lt;/b&gt;: Compose의 Issue Tracker를 확인해보니 유사한 문제가 보고되어 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정 사항 확인&lt;/b&gt;: 해당 문제의 수정 사항을 보니, maxScrollOffset을 Int.MAX_VALUE에서 Float.MAX_VALUE로 변경한 것이었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1721361949004&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;Google Issue Tracker&quot; data-og-description=&quot;&quot; data-og-host=&quot;issuetracker.google.com&quot; data-og-source-url=&quot;https://issuetracker.google.com/issues/311414925&quot; data-og-url=&quot;https://issuetracker.google.com/issues/311414925&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://issuetracker.google.com/issues/311414925&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://issuetracker.google.com/issues/311414925&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;Google Issue Tracker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;issuetracker.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;PagerState의 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. maxScrollOffset 계산&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721362141315&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal fun PagerLayoutInfo.calculateNewMaxScrollOffset(pageCount: Int): Long {
    val pageSizeWithSpacing = pageSpacing + pageSize
    val maxScrollPossible =
        (pageCount.toLong()) * pageSizeWithSpacing + beforeContentPadding + afterContentPadding
    // ... 나머지 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 pageCount가 매우 크면 maxScrollPossible이 Int.MAX_VALUE를 쉽게 초과할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 스크롤 계산&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721362594807&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private var maxScrollOffset: Int = Int.MAX_VALUE
	private set

private fun performScroll(delta: Float): Float {
    val currentScrollPosition = currentAbsoluteScrollOffset()
    // ... 중간 코드 생략
    val updatedScrollPosition = (currentScrollPosition + decimalAccumulationInt)
    val coercedScrollPosition = updatedScrollPosition.coerceIn(minScrollOffset, maxScrollOffset)
    // ... 나머지 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 updatedScrollPosition과 coercedScrollPosition이 Int.MAX_VALUE를 초과하는 경우, 이전에는 오버플로우나 부정확한 계산이 발생했을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pager는 일반적으로 가상화 기법을 사용하여 화면에 보이는 항목만 메모리에 유지합니다.&lt;/li&gt;
&lt;li&gt;scrollPosition이 잘못 계산되면, 시스템이 실제로 필요한 것보다 훨씬 많은 페이지를 메모리에 로드하려 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 리소스 누수&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721362774151&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal fun applyMeasureResult(
    result: PagerMeasureResult,
    visibleItemsStayedTheSame: Boolean = false
) {
    // ... 코드 생략
    pagerLayoutInfoState.value = result
    // ... 나머지 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘못된 scrollPosition으로 인해 applyMeasureResult가 부정확한 결과를 적용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이로 인해 더 이상 필요하지 않은 페이지 객체들이 메모리에서 해제되지 않을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 무한 루프 가능성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721363438141&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun notifyPrefetch(delta: Float, info: PagerLayoutInfo) {
    // ... 코드 생략
    val indexToPrefetch = // 계산 로직
    if (indexToPrefetch in 0 until pageCount) {
        // 프리페치 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;indexToPrefetch 계산 시 매우 큰 값이 사용될 수 있으며, 이는 과도한 메모리 사용으로 이어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분들에서 Int.MAX_VALUE를 초과하는 값들이 사용될 때 오버플로우, 부정확한 계산, 또는 예상치 못한 동작이 발생할 수 있었고, 이는 결과적으로 과도한 메모리 사용이나 OOM을 유발할 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scrollPosition 계산 오류로 인해 notifyPrefetch가 계속해서 새로운 페이지를 프리페치하려 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이는 지속적인 메모리 할당으로 이어질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분들에서&lt;b&gt; Int.MAX_VALUE&lt;/b&gt;를 초과하는 값들이 사용될 때 &lt;b&gt;오버플로우, 부정확한 계산, 또는 예상치 못한 동작&lt;/b&gt;이 발생할 수 있었고, 이는 결과적으로 과도한 메모리 사용이나 &lt;b&gt;OOM&lt;/b&gt;을 유발할 수 있었습니다. &lt;b&gt;Float.MAX_VALUE&lt;/b&gt;로 변경함으로써 OOM이 발생하지 않게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;향후 계획&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정화 버전 기다리기: 현재 이 수정 사항은 1.7.0-alpha01 버전에만 적용되어 있습니다. 아직 stable 버전으로 올라오지 않았기 때문에, 안정화 버전을 기다려야 할 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 경험을 통해 최신 라이브러리를 사용하는 프로젝트와의 의존성 추가 시 발생할 수 있는 문제를 해결하는 과정을 배웠습니다.&lt;/b&gt; 특히, 메모리 관련 문제는 작은 설정 하나로도 큰 영향을 미칠 수 있다는 점을 다시 한번 깨닫게 되었습니다. 앞으로도 꾸준한 모니터링과 최신 정보 확인을 통해 더 안정적인 서비스를 제공할 수 있도록 노력해야겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Compose</category>
      <category>Android Compose</category>
      <category>compose</category>
      <category>compose multiplatform</category>
      <category>HorizontalPager</category>
      <category>JetPack Compose</category>
      <category>kotlin compose</category>
      <category>OOM</category>
      <category>out of memory</category>
      <category>pager</category>
      <category>VerticalPager</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/151</guid>
      <comments>https://si8ae.tistory.com/151#entry151comment</comments>
      <pubDate>Fri, 19 Jul 2024 19:56:30 +0900</pubDate>
    </item>
    <item>
      <title>Hi Jack Mocker | 개선기 (4) feat.out of order</title>
      <link>https://si8ae.tistory.com/150</link>
      <description>&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Hi Jack Mocker란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1720853030161&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&quot; data-og-description=&quot;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/y6z5w/hyWzEMrZYy/keADKRmLPDujraQnGEAF8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/y6z5w/hyWzEMrZYy/keADKRmLPDujraQnGEAF8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hi Jack Mocker&lt;/b&gt;는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Channel의 동시 네트워크 요청 처리 순서 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 여러 네트워크 요청을 처리하는 데 어려움이 있었습니다. 처음에는 Kotlin의 &lt;b&gt;채널을&lt;/b&gt; 사용하여 모킹된 네트워크 응답을 인터셉터에 전달했습니다. 하지만 &lt;b&gt;채널은&lt;/b&gt; 여러 인터셉터 간에 메시지를 공정하게 &lt;b&gt;분배하기&lt;/b&gt; 때문에 응답이 순서대로 전달되지 않는 문제가 발생했습니다. 이로 인해 &lt;b&gt;A 데이터를&lt;/b&gt; 받아야 하는 요청이 &lt;b&gt;B 데이터를 받는&lt;/b&gt; 등의 예기치 않은 문제가 발생했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720853538113&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;future.complete(interceptorManager.receiveWithResultChannel())

suspend fun receiveWithResultChannel(): Response {
	if (resultChannel.isClosedForReceive) {
		resultChannel = Channel(UNLIMITED)
    }

    return resultChannel.receive()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에 보다시피 아무런 조건 없이 receive()를 하다 보니 문제가 발생했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 다수의 구독자가 있는 경우에 적합한 Kotlin의 &lt;b&gt;SharedFlow&lt;/b&gt;로 전환했습니다. 추가로 &lt;b&gt;UUID&lt;/b&gt;를 도입하여 각 인터셉터가 &lt;b&gt;올바른&lt;/b&gt; 응답을 받을 수 있도록 했습니다. 구현 방법은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 요청에 대해 UUID 생성&lt;/b&gt;: 각 요청-응답 쌍을 고유하게 식별할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UUID와 함께 이벤트 전송&lt;/b&gt;: 인터셉터가 이벤트를 UUID와 함께 전송합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UUID로 응답 필터링&lt;/b&gt;: 응답을 받을 때 UUID로 필터링하여 올바른 응답을 받도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720853623229&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
    var response = chain.proceed(chain.request())

    if (hjmDataStore.getHjmMode()) {
        val uuid = UUID.randomUUID().toString()
        interceptorManager.sendEventAtInterceptorEvent(uuid, response)

        startHjmActivityIfNeeded()

        response = interceptorManager.resultEvent.filter { it.first == uuid }.first().second
    }

    return@runBlocking response
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 각 요청이 적절한 응답을 받도록 보장할 수 있었습니다.&lt;/p&gt;</description>
      <category>Android/Hi Jack Mocker</category>
      <category>Android</category>
      <category>api mocking</category>
      <category>droidknights</category>
      <category>Kotlin</category>
      <category>Mocking</category>
      <category>network mocking</category>
      <category>okHttp</category>
      <category>Retrofit</category>
      <category>드로이드나이츠</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/150</guid>
      <comments>https://si8ae.tistory.com/150#entry150comment</comments>
      <pubDate>Sat, 13 Jul 2024 19:55:54 +0900</pubDate>
    </item>
    <item>
      <title>Hi Jack Mocker | 개선기 (3) feat.channel data loss</title>
      <link>https://si8ae.tistory.com/149</link>
      <description>&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Hi Jack Mocker란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1720622350444&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&quot; data-og-description=&quot;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/plqAg/hyWzuoWko3/Rqz00tkauMb5lldy8B6LW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/plqAg/hyWzuoWko3/Rqz00tkauMb5lldy8B6LW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hi Jack Mocker&lt;/b&gt;는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Channel의 send와 consumeEach 동시 호출 시 데이터 유실 문제&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1720601874484&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private var interceptorChannel = Channel&amp;lt;Response&amp;gt;(UNLIMITED)
private var resultChannel = Channel&amp;lt;Response&amp;gt;(UNLIMITED)
val isHjmActivityRunning = AtomicBoolean(false)

@OptIn(DelicateCoroutinesApi::class)
suspend fun sendWithInterceptorChannel(response: Response) {
    if (interceptorChannel.isClosedForSend) {
        interceptorChannel = Channel(UNLIMITED)
    }
    interceptorChannel.send(response)
}

@OptIn(DelicateCoroutinesApi::class)
suspend fun consumeEachInterceptorChannel(action: (Response) -&amp;gt; Unit) {
    if (interceptorChannel.isClosedForReceive) {
        interceptorChannel = Channel(UNLIMITED)
    }

    interceptorChannel.consumeEach {
        action(it)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hi Jack Mocker 라이브러리는 OkHttp3를 통해 네트워크 응답을 가로채어 이를 모킹하고 UI에 전달합니다. 하지만 간헐적으로 네트워크 요청이 &lt;b&gt;3개가 send 되었지만 1~2개만 receive 되는 문제&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;size16&quot;&gt;초기에는 send와 receive 과정에서 &lt;b&gt;동시성 문제&lt;/b&gt;라고 생각하여 &lt;b&gt;mutex&lt;/b&gt;를 추가하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720601954196&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OptIn(DelicateCoroutinesApi::class)
suspend fun sendWithInterceptorChannel(response: Response) {
    mutex.withLock {
        if (interceptorChannel.isClosedForSend) {
            interceptorChannel = Channel(UNLIMITED)
        }

        interceptorChannel.send(response)
    }
}

@OptIn(DelicateCoroutinesApi::class)
suspend fun receiveAllWithInterceptorChannel(action: (Response) -&amp;gt; Unit) {
    mutex.withLock {
        if (interceptorChannel.isClosedForReceive) {
            interceptorChannel = Channel(UNLIMITED)
        }

        for (response in interceptorChannel) {
            action(response)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 문제를 해결하는 것처럼 보였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 원인은 &lt;b&gt;이벤트 순서 문제&lt;/b&gt;였으며, &lt;b&gt;액티비티 종료&lt;/b&gt;가 &lt;b&gt;send와 receive 이벤트 이후에&lt;/b&gt; 호출되어 데이터가 손실되는 것이었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모킹된 응답을 UI로 전송.&lt;/li&gt;
&lt;li&gt;액티비티 종료 요청.&lt;/li&gt;
&lt;li&gt;새로운 네트워크 요청 발생.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기&lt;/b&gt; 특성상 이벤트가 &lt;b&gt;1, 2, 3&lt;/b&gt; 순서대로 처리되지 않고 &lt;b&gt;1, 3, 2&lt;/b&gt; 순서로 처리될 수 있었기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BoCQA/btsIveg0xbP/EqZac4L0SRrGC4IgoVmGtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BoCQA/btsIveg0xbP/EqZac4L0SRrGC4IgoVmGtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BoCQA/btsIveg0xbP/EqZac4L0SRrGC4IgoVmGtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBoCQA%2FbtsIveg0xbP%2FEqZac4L0SRrGC4IgoVmGtk%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;237&quot; height=&quot;2400&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 1, 3, 2 순서로 처리가되어 HjmActivity에서는 &lt;b&gt;빈 리스트&lt;/b&gt;를 보여주는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 모킹된 데이터를 전송한 후, onFinishEvent라는 SharedFlow를 사용하여 true를 방출하고 이를 통해 액티비티를 종료했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 ViewModel에서 인터셉터를 통해 새로운 데이터를 수신할 경우, onFinishEvent에 false를 방출하여 액티비티 종료를 막았습니다. 이때 300밀리초의 &lt;b&gt;debounce&lt;/b&gt;를 추가하여 새로운 이벤트가 없고 true가 collect 될때만 액티비티가 종료되도록 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720682971580&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LaunchedEffect(Unit) {
    hjmViewModel.onFinishEvent
        .distinctUntilChanged()
        .debounce(300)
        .collectLatest { isFinish -&amp;gt;
            if (isFinish) {
                interceptorManager.isHjmActivityRunning.set(false)
                finish()
            }
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Hi Jack Mocker</category>
      <category>Android</category>
      <category>api mocking</category>
      <category>debounce</category>
      <category>droidknights</category>
      <category>hi jack mocker</category>
      <category>hjm</category>
      <category>Mocking</category>
      <category>network mocking</category>
      <category>드로이드나이츠</category>
      <category>오픈 소스</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/149</guid>
      <comments>https://si8ae.tistory.com/149#entry149comment</comments>
      <pubDate>Sat, 13 Jul 2024 19:00:49 +0900</pubDate>
    </item>
    <item>
      <title>Hi Jack Mocker | 개선기 (2) feat.JSON 변환 이슈</title>
      <link>https://si8ae.tistory.com/148</link>
      <description>&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Hi Jack Mocker란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1720597931684&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&quot; data-og-description=&quot;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/plqAg/hyWzuoWko3/Rqz00tkauMb5lldy8B6LW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/koreatlwls/Hi-Jack-Mocker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/plqAg/hyWzuoWko3/Rqz00tkauMb5lldy8B6LW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hi Jack Mocker&lt;/b&gt;는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SingleItem 클래스의 value 타입을 Any로 변경하여 JSON 변환 문제 해결하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HiJackMocker 라이브러리의 SingleItem 클래스에서 value의 타입을 String에서 Any로 변경한 이유와 그로 인해 해결한 문제들에 대해 다루겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기존의 SingleItem 클래스와 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 SingleItem 클래스는 다음과 같이 value를 String 타입으로 한정하고 있었습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1720598490016&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class SingleItem(
    override val key: String,
    val value: String
) : JsonItem&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;b&gt;SingleItem&lt;/b&gt; 클래스는 &lt;b&gt;JSON&lt;/b&gt; 데이터를 &lt;b&gt;String&lt;/b&gt; 값으로만 표현할 수 있어서, 다양한 JSON 데이터 타입을 처리하는 데 한계가 있었습니다. 실제 JSON 객체에서는 String 외에도 &lt;b&gt;Int&lt;/b&gt;, &lt;b&gt;Double&lt;/b&gt;, &lt;b&gt;Boolean&lt;/b&gt; 등 다양한 데이터 타입이 존재할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 SingleItem을 사용하여 UI에 JSON 데이터를 표시하고, &lt;b&gt;Mocking&lt;/b&gt; 후 &lt;b&gt;JSON 객체로 다시 변환&lt;/b&gt;하는 과정에서 여러 &lt;b&gt;문제가&lt;/b&gt; 발생했습니다. 예를 들어, &lt;b&gt;Int, Double, Boolean과 같은 기본형 타입이 String으로만&lt;/b&gt; 처리되다 보니, Mocking이 완료된 이후 JSON 객체로 변환할 때 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제로 인해 &lt;b&gt;Moshi&lt;/b&gt;나 &lt;b&gt;Gson&lt;/b&gt;과 같은 J&lt;b&gt;SON 변환 라이브러리에서 에러가&lt;/b&gt; 발생할 수 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SingleItem 클래스의 개선&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하기 위해 SingleItem 클래스의 value 타입을 Any로 변경했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720598575112&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class SingleItem(
    override val key: String,
    val value: Any
) : JsonItem&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Any 타입으로 변경함으로써 String뿐만 아니라 Boolean, Int, Float, Double 등 &lt;b&gt;다양한&lt;/b&gt; JSON 데이터 타입을 지원할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;SingleItem&lt;/b&gt;은 JSON의 모든 기본형 타입을 처리할 수 있으며, &lt;b&gt;Mocking 완료 후에도 JSON 객체를 정확하게 재생성할&lt;/b&gt; 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;UI에서 SingleItem의 value를 표시하는 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value의 타입이 Any가 되면서, UI를 그릴 때 value의 실제 타입에 따라 다른 방법으로 표시하도록 개선했습니다. 예를 들어, Boolean, Int, Float, Double 등의 타입에 따라 UI를 다르게 표현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720598650192&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;is JsonItem.SingleItem -&amp;gt; {
            when (item.value) {
                is Boolean -&amp;gt; {
                    BooleanValue(
                        value = item.value,
                        onValueChange = { onValueChange(it) }
                    )
                }

                is Int, is Long, is Float, is Double -&amp;gt; {
                    NumberValue(
                        value = item.value,
                        onValueChange = { onValueChange(it) }
                    )
                }

                else -&amp;gt; {
                    TextValue(
                        value = item.value.toString(),
                        onValueChange = { onValueChange(it) }
                    )
                }
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 BooleanValue, NumberValue, TextValue는 각 타입에 맞는 UI 컴포넌트를 보여줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1720598714824&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NonNull public JSONObject put(@NonNull String name, @Nullable Object value) throws JSONException {
        if (value == null) {
            nameValuePairs.remove(name);
            return this;
        }
        if (value instanceof Number) {
            // deviate from the original by checking all Numbers, not just floats &amp;amp; doubles
            JSON.checkDouble(((Number) value).doubleValue());
        }
        nameValuePairs.put(checkName(name), value);
        return this;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSONObject의 put 함수는 &lt;b&gt;Object&lt;/b&gt; 타입의 값을 허용하기 때문에, SingleItem 클래스의 value를 String에서 &lt;b&gt;Any&lt;/b&gt;로 변경하면 JSON 변환 과정에서 발생할 수 있는 문제를 해결할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Hi Jack Mocker</category>
      <category>Android</category>
      <category>api mocking</category>
      <category>droidknights</category>
      <category>hijackmocker</category>
      <category>hjm</category>
      <category>Interceptor</category>
      <category>network mocking</category>
      <category>okHttp</category>
      <category>Retrofit</category>
      <category>드로이드나이츠</category>
      <author>si8ae</author>
      <guid isPermaLink="true">https://si8ae.tistory.com/148</guid>
      <comments>https://si8ae.tistory.com/148#entry148comment</comments>
      <pubDate>Fri, 12 Jul 2024 18:09:42 +0900</pubDate>
    </item>
  </channel>
</rss>