<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>IT Story</title>
    <link>https://itstory1592.tistory.com/</link>
    <description>안드로이드 개발자를 향해 달리고 있는 공대생입니다!  
Android, Kotlin, Java, Python 등 학습하고 있는 내용과 프로젝트를 주로 업로드하고 있습니다.
지적과 조언은 언제나 환영입니다! 

github : https://github.com/tmdgh1592</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 02:50:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BuNa_</managingEditor>
    <image>
      <title>IT Story</title>
      <url>https://tistory1.daumcdn.net/tistory/3146959/attach/7b3828737277425da39136f7b4bc5698</url>
      <link>https://itstory1592.tistory.com</link>
    </image>
    <item>
      <title>Velog로 블로그 이전합니다.</title>
      <link>https://itstory1592.tistory.com/133</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Tistory도 좋았지만 Velog에서 마크다운을 제공하다보니 훨씬 깔끔해보이더군요..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 포스팅은 아래에서 작성할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@buna1592&quot;&gt;https://velog.io/@buna1592&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694525015583&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;buna1592 (부나) - velog&quot; data-og-description=&quot;&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@buna1592&quot; data-og-url=&quot;https://velog.io/@buna1592&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ln70u/hyTVWO3d80/YTbpRlblk8kjpV3DSyBPHk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@buna1592&quot; data-source-url=&quot;https://velog.io/@buna1592&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ln70u/hyTVWO3d80/YTbpRlblk8kjpV3DSyBPHk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;buna1592 (부나) - velog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/133</guid>
      <comments>https://itstory1592.tistory.com/133#entry133comment</comments>
      <pubDate>Tue, 12 Sep 2023 22:25:12 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Kerdy에서 다뤄온 Retrofit 에러 처리</title>
      <link>https://itstory1592.tistory.com/132</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;julian-hochgesang-nqZv8jtwLTY-unsplash.jpg&quot; data-origin-width=&quot;5184&quot; data-origin-height=&quot;3456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buC806/btssTrv6ckE/gx3C1RQJV4rFrC8GaztuD1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buC806/btssTrv6ckE/gx3C1RQJV4rFrC8GaztuD1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buC806/btssTrv6ckE/gx3C1RQJV4rFrC8GaztuD1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuC806%2FbtssTrv6ckE%2Fgx3C1RQJV4rFrC8GaztuD1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5184&quot; height=&quot;3456&quot; data-filename=&quot;julian-hochgesang-nqZv8jtwLTY-unsplash.jpg&quot; data-origin-width=&quot;5184&quot; data-origin-height=&quot;3456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000; text-align: center;&quot; href=&quot;https://itstory1592.tistory.com/131&quot;&gt;커디(Kerdy)&lt;/a&gt;&lt;span style=&quot;text-align: center;&quot;&gt;&amp;nbsp;프로젝트에서는 서버와의 HTTP 통신을 위해 Retrofit2(이하 Retrofit), OkHttp3 라이브러리를 사용하고 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Retrofit는 OkHttp3을 보다 간편하고 직관적으로 사용할 수 있도록 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://itstory1592.tistory.com/130#comment15799753&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] OkHttp &amp;amp; Retrofit&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693746180808&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] OkHttp &amp;amp; Retrofit&quot; data-og-description=&quot;Android에서 네트워크 작업을 할 때 사용하는 대표적인 라이브러리는 아래와 같다. OkHttp Retrofit 두 라이브러리 모두 Square사에서 개발한 HTTP 통신 라이브러리이다. OkHttp는 HTTP 통신을 간편하게 할 &quot; data-og-host=&quot;itstory1592.tistory.com&quot; data-og-source-url=&quot;https://itstory1592.tistory.com/130#comment15799753&quot; data-og-url=&quot;https://itstory1592.tistory.com/130&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tnQWC/hyTPvxJVeA/kyiS4cnYpXkDV8KnbEbUD1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/gViUL/hyTPynFQPD/oHPoE6MDPS2BNCKcbOUoMK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/chcUrL/hyTPxPSi5p/fnwSqlBI9VEn4mYvpbKVS1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225&quot;&gt;&lt;a href=&quot;https://itstory1592.tistory.com/130#comment15799753&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://itstory1592.tistory.com/130#comment15799753&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tnQWC/hyTPvxJVeA/kyiS4cnYpXkDV8KnbEbUD1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/gViUL/hyTPynFQPD/oHPoE6MDPS2BNCKcbOUoMK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/chcUrL/hyTPxPSi5p/fnwSqlBI9VEn4mYvpbKVS1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225');&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;[Android] OkHttp &amp;amp; Retrofit&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android에서 네트워크 작업을 할 때 사용하는 대표적인 라이브러리는 아래와 같다. OkHttp Retrofit 두 라이브러리 모두 Square사에서 개발한 HTTP 통신 라이브러리이다. OkHttp는 HTTP 통신을 간편하게 할&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;itstory1592.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;목차&lt;/span&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;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;다양한 HTTP 통신 결과&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Kerdy가 거쳐온 에러 처리&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: left;&quot;&gt;하나하나 처리하기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;공통 top-level 함수 활용하기&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;CallAdapter 활용하기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;ApiResponse를 DataModel로 변환하기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;1. 다양한 HTTP 통신 결과&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1693748307319&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;memberService.getMember(1).enqueue(object : Callback&amp;lt;MemberApiModel&amp;gt; {
    override fun onResponse(
        call: Call&amp;lt;MemberApiModel&amp;gt;,
        response: Response&amp;lt;MemberApiModel&amp;gt;,
    ) {
        val member = response.body()
        when {
            response.isSuccessful &amp;amp;&amp;amp; member != null -&amp;gt; println(member)
            !response.isSuccessful -&amp;gt; {
                when (response.code()) {
                    401 -&amp;gt; println(&quot;401 에러 발생&quot;)
                    404 -&amp;gt; println(&quot;404 에러 발생&quot;)
                    500 -&amp;gt; println(&quot;500 에러 발생&quot;)
                    else -&amp;gt; println(&quot;알 수 없는 에러 발생&quot;)
                }
            }
        }
    }

    override fun onFailure(call: Call&amp;lt;MemberApiModel&amp;gt;, t: Throwable) {
        println(&quot;서버 통신 실패&quot;)
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Retrofit을 통해 서버와의 HTTP 통신을 거치면, 이에 대한 응답(Response) 값을 전달받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: center;&quot;&gt;이때, 통신을 비동기적으로 수행하기 위해 call.enqueue() 메서드를 호출했다고 가정해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;서버와 정상적으로 통신하여 어떠한 응답을 받았다면 어떻게 될까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;enqueue()에 전달하는 Callback의 onReponse()가 호출될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;서버 응답에는 여러 가지 상황으로 이어질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;1. &lt;/span&gt;&lt;b&gt;성공(200)&lt;/b&gt;: @GET 요청을 통해 정상 응답을 받은 경우&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center; font-family: 'Noto Sans Light';&quot;&gt;2. &lt;b&gt;토큰 만료(401)&lt;/b&gt;: 토큰이 만료되어 신원 확인이 어려운 경우&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. &lt;b&gt;잘못된 EndPoint(404)&lt;/b&gt;: 올바르지 않은 EndPoint로 요청을 보낸 경우&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;4. &lt;b&gt;서버 문제(500)&lt;/b&gt;: 서버에 문제가 있는 경우 등&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;...&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;반면, 서버에 도달할 수 없는 상황(비행기 모드, Wifi 연결 끊김 등)에서는 &lt;/span&gt;call.enqueue()&lt;span style=&quot;text-align: start;&quot;&gt;에 전달한 Callback의 &lt;/span&gt;onFailure()&lt;span style=&quot;text-align: start;&quot;&gt; 메서드가 호출된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;코루틴을 사용하는 경우라면,&amp;nbsp;&lt;/span&gt;suspend&lt;span style=&quot;text-align: start;&quot;&gt; 키워드를 활용하여 &lt;/span&gt;Call&lt;span style=&quot;text-align: start;&quot;&gt; 대신 &lt;/span&gt;Response&lt;span style=&quot;text-align: start;&quot;&gt;를 반환한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;suspend 함수는 Retrofit 내부적으로 &lt;/span&gt;enqueue()&lt;span style=&quot;text-align: start;&quot;&gt;를 호출하며, 통신이 정상적으로 이루어진 경우에만 값을 받아온다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그렇지 않은 경우 예외가 발생하므로 적절한 예외 처리가 필요하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;2. Kerdy가 거쳐온 에러 처리&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;wicliff-thadeu-77B-3XoBc4M-unsplash.jpg&quot; data-origin-width=&quot;2948&quot; data-origin-height=&quot;2543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca5Mky/btssT4N9Am0/Ire5FsmKv4lmXd6KKkAlck/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca5Mky/btssT4N9Am0/Ire5FsmKv4lmXd6KKkAlck/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca5Mky/btssT4N9Am0/Ire5FsmKv4lmXd6KKkAlck/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca5Mky%2FbtssT4N9Am0%2FIre5FsmKv4lmXd6KKkAlck%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;328&quot; height=&quot;283&quot; data-filename=&quot;wicliff-thadeu-77B-3XoBc4M-unsplash.jpg&quot; data-origin-width=&quot;2948&quot; data-origin-height=&quot;2543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;서비스 사용 도중에 앱이 강제 종료되는 것은 개발 상황이 아니라면, 사용자에게는 좋지 않은 경험을 제공하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그래서 이러한 상황을 방지하기 위해 다양한 상황에 대한 처리가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;커디 안드로이드 팀은 Retrofit 응답을 더 효율적으로 처리할 수 있을지 고민해왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; 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;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;1. 하나하나 처리하기&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;가장 간단하면서도 단순한 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;모든 요청에 대해 응답을 일일이 처리해주었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그러기 위해서 &lt;b&gt;RunCatching&lt;/b&gt;과 &lt;b&gt;Sealed 클래스&lt;/b&gt;를 활용하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693749782383&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed interface ApiResult&amp;lt;T : Any&amp;gt;

// 성공적으로 응답을 가져온 경우, 값과 헤더를 포함한다.
class ApiSuccess&amp;lt;T : Any&amp;gt;(val data: T, val header: Headers = Headers.headersOf()) : ApiResult&amp;lt;T&amp;gt;

// 서버와 통신은 하였지만 응답 코드가 200대가 아닌 경우, Response line 정보를 포함한다.
class ApiError&amp;lt;T : Any&amp;gt;(val code: Int, val message: String?) : ApiResult&amp;lt;T&amp;gt;

// 서버와 아예 통신조차 하지 못한 경우, 예외를 포함한다.
class ApiException&amp;lt;T : Any&amp;gt;(val e: Throwable) : ApiResult&amp;lt;T&amp;gt;

// 알 수 없는 오류는 기타로 분류한다.
class UnknownException&amp;lt;T : Any&amp;gt;(val e: Throwable) : ApiResult&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;각 응답 상황별로 ApiResult를 상속하는 4개의 클래스를 정의해 주었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;ApiResult의 자식 타입을 통해 ViewModel에서 상황별로 대응할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693749562566&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MemberRepository(
    private val memberService: MemberService,
) {
    suspend fun getMember(): ApiResult&amp;lt;Member&amp;gt; = runCatching {
        memberService.getMember(1)
    }.fold(
        onSuccess = { response -&amp;gt;
            val responseBody = response.body()
            when {
                // 서버에서 정상적으로 응답이 왔을 때
                response.isSuccessful &amp;amp;&amp;amp; responseBody != null -&amp;gt; Success(responseBody.toData())

                // 서버에서 응답이 왔지만, 200대 응답 코드가 아닌 경우
                else -&amp;gt; Failure(response.code(), response.message())
            }
        },
        // 200대 응답 코드가 아니거나, 서버와 통신을 실패한 경우
        onFailure = { error -&amp;gt;
            when (error) {
                // 서버와 통신을 실패한 경우
                is IOException -&amp;gt; NetworkError(error)

                // 그 외의 경우
                else -&amp;gt; UnknownError(error)
            }
        },
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;memberService.getMember()를 통해 Response를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;runCatching을 사용하였기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;만약 예외가 발생하지 않았다면 onSuccess가 호출되고, 예외가 발생하면 onFailure가 호출될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위 코드의 흐름을 크게 4가지로 정리하면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. 서버에서 정상적으로 값을 가져왔다면 &lt;b&gt;Success&lt;/b&gt;를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. 200대 응답 코드가 아니거나, body가 비었다면 &lt;b&gt;Failure&lt;/b&gt;를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. 서버와 아예 통신조차 하지 못했다면 &lt;b&gt;NetworkError&lt;/b&gt;를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;4. 그 외에 예상할 수 없는 에러는 &lt;b&gt;UnknownError&lt;/b&gt;를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693751066046&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MemberViewModel(
    private val memberRepository: MemberRepository,
) {
    suspend fun getMember() {
        val result = memberRepository.getMember()
        when (result) {
            is Success -&amp;gt; // 화면에 유저 정보를 보여준다.
            is Failure -&amp;gt; handleError(result.code, result.message) // 화면에 에러 메시지를 보여준다.
            is NetworkError -&amp;gt; // 화면에 인터넷 연결 상태를 체크하라는 메시지를 보여준다.
            is UnknownError -&amp;gt; // 화면에 관리자 문의 요청 뷰를 보여준다.
        }
    }
    
    private fun handleError(code: Int, message: String?) {
        when (code) {
            401 -&amp;gt; // 로그인 화면으로 이동한다.
            404 -&amp;gt; // 화면에 유저 정보가 없다는 메시지를 보여준다.
            500 -&amp;gt; // 화면에 관리자 문의 요청 뷰를 보여준다.
            else -&amp;gt; // 화면에 에러 메시지를 보여준다.
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center; font-family: 'Noto Sans Light';&quot;&gt;이러한 타입들을 ViewModel에서는 위와 같이 다룰 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: center; font-family: 'Noto Sans Light';&quot;&gt;각 상황에 대한 데이터들을 모두 가지고 있기 때문에 적절히 핸들링할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;2. 공통 top-level 함수 활용하기&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;하지만 이 방식에 큰 문제점을 느끼기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;보일러 플레이트가 지나치게 많이 발생한다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;어떠한 통신을 하던지 간에 runCatching부터, ApiResult 타입으로 변환해야 한다는 사실에 벌써부터 머리가 지끈거렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;모두 동일한 형식으로 변환하는 것인데 각각 따로 존재해야 할 필요가 있을까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Web_sayingNo.webp&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b06Zxz/btssSq5u5Sd/fx1h2MF2jwcfk9NbwJAjh1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b06Zxz/btssSq5u5Sd/fx1h2MF2jwcfk9NbwJAjh1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b06Zxz/btssSq5u5Sd/fx1h2MF2jwcfk9NbwJAjh1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb06Zxz%2FbtssSq5u5Sd%2Ffx1h2MF2jwcfk9NbwJAjh1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;236&quot; data-filename=&quot;Web_sayingNo.webp&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;우리가 내린 결론은 No! 였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. ApiResult 변환 로직이 변경된다면, 모든 Repository의 코드를 수정해야 한다는 점.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. 모든 서버 통신 코드에 동일한 코드가 흩뿌려져 있는 점.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위 두 가지는 개발에 피로감을 줄뿐더러, 유지보수에도 취약하다고 판단하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;따라서 공통 코드를 하나의 최상위 함수(Top-level)로 만드는 방법을 시도했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693751678827&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend inline fun &amp;lt;T : Any, reified V : Any&amp;gt; handleApi(
    execute: suspend () -&amp;gt; Response&amp;lt;T&amp;gt;,
    mapToDomain: suspend (T) -&amp;gt; V,
): ApiResult&amp;lt;V&amp;gt; = runCatching {
    execute()
}.fold(
    onSuccess = { response -&amp;gt;
        val body = response.body()
        val headers = response.headers()
        when {
            // 서버에서 정상적으로 응답이 왔을 때 (Unit 타입인 경우 고려)
            response.isSuccessful &amp;amp;&amp;amp; body == null &amp;amp;&amp;amp; V::class == Unit::class -&amp;gt; Success(
                Unit as V,
                headers,
            )

            // 서버에서 정상적으로 응답이 왔을 때 (Unit 타입이 아닌 경우 고려)
            response.isSuccessful &amp;amp;&amp;amp; body != null -&amp;gt; Success(mapToDomain(body), headers)

            // 서버에서 응답이 왔지만, 200대 응답 코드가 아닌 경우
            else -&amp;gt; Failure(code = response.code(), message = response.message())
        }
    },
    onFailure = { error -&amp;gt;
        when (error) {
            // 서버와 통신을 실패한 경우
            is HttpException -&amp;gt; Failure(code = error.code(), message = error.message())

            // 서버와 통신을 실패한 경우
            is UnknownHostException -&amp;gt; NetworkError(error)

            // 그 외의 경우
            else -&amp;gt; UnknownError(error)
        }
    },
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기존 코드를 어디에서나 동일하게 사용할 수 있도록 handleApi() 함수를 구현하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;다만, 기존 코드와 조금 다른 점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이전에는 response.body()가 null이라면 에러로 분리하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;하지만, 이제는 공통적으로 함수를 사용하기 때문에 반환 타입이 Unit인 경우를 고려해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;따라서 서버에서 정상적으로 응답을 받았으며, 응답 타입이 Unit이라면 성공으로 분류하는 로직이 추가되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;3. CallAdapter 활용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;242CB14A567544422A.jpg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zh0xm/btssTeDJyR4/WpFGbBmuDqX8oss9KRI8Sk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zh0xm/btssTeDJyR4/WpFGbBmuDqX8oss9KRI8Sk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zh0xm/btssTeDJyR4/WpFGbBmuDqX8oss9KRI8Sk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzh0xm%2FbtssTeDJyR4%2FWpFGbBmuDqX8oss9KRI8Sk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;305&quot; data-filename=&quot;242CB14A567544422A.jpg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;여기서 만족할 수 있는 수준이라면 너무나도 좋겠지만 현실은 그렇지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;아래 코드를 함께 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693752005369&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ScrappedEventService {
    @GET(&quot;scraps&quot;)
    suspend fun getScrappedEvents(): Response&amp;lt;List&amp;lt;ScrappedEventResponse&amp;gt;&amp;gt;

    @POST(&quot;scraps&quot;)
    suspend fun scrapEvent(
        @Body scrappedEventRequestBody: ScrappedEventRequestBody,
    ): Response&amp;lt;Unit&amp;gt;

    @DELETE(&quot;scraps&quot;)
    suspend fun deleteScrap(
        @Query(&quot;event-id&quot;) eventId: Long,
    ): Response&amp;lt;Unit&amp;gt;
}


class ScrappedEventRepositoryImpl(
    private val scrappedEventService: ScrappedEventService,
) : ScrappedEventRepository {

    override suspend fun getScrappedEvents(): ApiResult&amp;lt;List&amp;lt;ScrappedEvent&amp;gt;&amp;gt; {
        return handleApi(
            execute = { scrappedEventService.getScrappedEvents() },
            mapToDomain = List&amp;lt;ScrappedEventApiModel&amp;gt;::toData,
        )
    }

    override suspend fun scrapEvent(eventId: Long): ApiResult&amp;lt;Unit&amp;gt; {
        val scrappedEventRequestBody = ScrappedEventRequestBody(eventId)
        return handleApi(
            execute = { scrappedEventService.scrapEvent(scrappedEventRequestBody) },
            mapToDomain = {},
        )
    }

    override suspend fun deleteScrap(eventId: Long): ApiResult&amp;lt;Unit&amp;gt; {
        return handleApi(
            execute = { scrappedEventService.deleteScrap(eventId) },
            mapToDomain = {},
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;handleApi 함수를 통해 기존 코드를 간략하게 줄일 수는 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;하지만 위 코드를 보다시피 매번 handleApi를 호출해주어야 한다는 점은 우리를 100% 만족시킬 수 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;어떻게 하면 개선을 할 수 있을까 리서치하는 도중 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://square.github.io/retrofit/2.x/retrofit/retrofit2/CallAdapter.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CallAdapter&lt;/a&gt;라는 한 줄기 빛을 발견하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;CallAdapter는 우리가 호출하는 요청에 대해 Custom Call을 반환해 줄 수 있도록 Retrofit에서 제공해 주는 클래스이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;(CallAdapter을 학습한 내용은 포스팅으로 다룰 예정이다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;즉, 개발자가 매번 handleApi를 직접 호출하지 않고, 요청을 응답받을 때 원하는 형태로 변환하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693753310256&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KerdyCall&amp;lt;T : Any&amp;gt;(private val call: Call&amp;lt;T&amp;gt;, private val responseType: Type) :
    Call&amp;lt;ApiResult&amp;lt;T&amp;gt;&amp;gt; {
    
    override fun enqueue(callback: Callback&amp;lt;ApiResult&amp;lt;T&amp;gt;&amp;gt;) {
        call.enqueue(object : Callback&amp;lt;T&amp;gt; {
            override fun onResponse(call: Call&amp;lt;T&amp;gt;, response: Response&amp;lt;T&amp;gt;) {
                if (response.isSuccessful) {
                    val responseBody = response.body()
                    
                    return when {
                        responseType == Unit::class.java -&amp;gt; callback.onResponse(
                            this@KerdyCall,
                            Response.success(Success(Unit as T)),
                        )

                        responseBody == null -&amp;gt; callback.onResponse(
                            this@KerdyCall,
                            Response.success(
                                UnknownError(IllegalStateException(&quot;Response Body가 존재하지 않습니다.&quot;))
                            ),
                        )

                        else -&amp;gt; callback.onResponse(
                            this@KerdyCall,
                            Response.success(Success(responseBody))
                        )
                    }
                } else {
                    callback.onResponse(
                        this@KerdyCall,
                        Response.success(
                            Failure(response.code(), response.errorBody()?.string()),
                        ),
                    )
                }
            }

            override fun onFailure(call: Call&amp;lt;T&amp;gt;, error: Throwable) {
                val response = when (error) {
                    is IOException -&amp;gt; NetworkError&amp;lt;T&amp;gt;(error)
                    else -&amp;gt; UnknownError(error)
                }
                callback.onResponse(this@KerdyCall, Response.success(response))
            }
        })
    }
    
    ...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위에서 만든 KerdyCall 객체를 &lt;span style=&quot;text-align: center;&quot;&gt;CallAdapter의&amp;nbsp;&lt;/span&gt;adapt() 메서드 반환으로 제공해 주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;또 하나의 특징으로, CallAdapter의 타입 파라미터를 확인해 보면 ApiResult이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이것이 무엇을 의미하는지는 아래 코드를 보면 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693753649596&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ScrappedEventService {
    @GET(&quot;/scraps&quot;)
    suspend fun getScrappedEvents(): ApiResult&amp;lt;List&amp;lt;ScrappedEventResponse&amp;gt;&amp;gt;

    @POST(&quot;/scraps&quot;)
    suspend fun scrapEvent(
        @Body scrappedEventRequestBody: ScrappedEventRequestBody,
    ): ApiResult&amp;lt;Unit&amp;gt;

    @DELETE(&quot;/scraps&quot;)
    suspend fun deleteScrap(
        @Query(&quot;event-id&quot;) eventId: Long,
    ): ApiResult&amp;lt;Unit&amp;gt;
}


class ScrappedEventRepositoryImpl(
    private val scrappedEventService: ScrappedEventService,
) : ScrappedEventRepository {

    override suspend fun getScrappedEvents(): ApiResult&amp;lt;List&amp;lt;ScrappedEventResponse&amp;gt;&amp;gt; =
        scrappedEventService.getScrappedEvents()

    override suspend fun scrapEvent(eventId: Long): ApiResult&amp;lt;Unit&amp;gt; =
        scrappedEventService.scrapEvent(eventId)

    override suspend fun deleteScrap(eventId: Long): ApiResult&amp;lt;Unit&amp;gt; =
        scrappedEventService.deleteScrap(eventId)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;handleApi의 로직을 KerdyCall에서 처리해주고 있기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;interface의 메서드의 반환 타입을 보면 ApiResult임을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;또한, Repository에서도 handleApi를 일일이 호출해 줄 필요가 없어졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기존 방식에 비해 매우 세련된 방식이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;다만, Repository에서 Remote DTO를 그대로 반환하고 있는 점은 아직 불편하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이 문제에 대해서는 아직 고민 중에 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;+) 2023.09.11 : DTO를 Data Model로 변환하는 로직 추가&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;ApiResponse를 DataModel로 변환하기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CallAdapter를 활용하여 ApiResult로 받아오는 것까지는 좋은 시도였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다만, &lt;b&gt;ApiResult&amp;lt;ApiResponse&amp;gt; -&amp;gt; ApiResult&amp;lt;DataModel&amp;gt;&lt;/b&gt; 로 변환하는 로직이 필요하기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 보일러 플레이트 코드를 지우지 못하는 느낌이었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694421770907&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override suspend fun getEventDetail(eventId: Long): ApiResult&amp;lt;EventDetail&amp;gt; {
    val result = eventDetailService.getEventDetail(eventId)
    return when(result) {
        is Success -&amp;gt; Success(result.map { it.toData() })
        is Failure -&amp;gt; Failure
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;매번 변환 로직을 작성해주면서, 또 다시 Repository(or DataSource)가 비대해지기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이러한 공통 로직을 처리하기 위한 방법을 떠올려 보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694421988539&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class ApiResult&amp;lt;out T : Any&amp;gt; {
    fun &amp;lt;R : Any&amp;gt; map(transform: (T) -&amp;gt; R): ApiResult&amp;lt;R&amp;gt; = when (this) {
        is Success -&amp;gt; Success(transform(data))
        is Failure -&amp;gt; Failure(responseCode, message)
        is NetworkError -&amp;gt; NetworkError
        is Unexpected -&amp;gt; Unexpected(error)
    }
}

data class Success&amp;lt;T : Any&amp;gt;(val data: T) : ApiResult&amp;lt;T&amp;gt;()
data class Failure(val responseCode: Int, val message: String?) : ApiResult&amp;lt;Nothing&amp;gt;()
object NetworkError : ApiResult&amp;lt;Nothing&amp;gt;()
data class Unexpected(val error: Throwable?) : ApiResult&amp;lt;Nothing&amp;gt;()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ApiResult에 map이라는 변환 로직을 두어 공통 코드를 제거하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;성공인 경우에만 DTO &amp;lt;-&amp;gt; DataModel 사이의 mapper 함수를 전달하여 변환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694421768096&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override suspend fun getEventDetail(eventId: Long): ApiResult&amp;lt;EventDetail&amp;gt; = eventDetailService
    .getEventDetail(eventId)
    .map(EventDetailApiModel::toData)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그 결과 코드를 위와 같이 깔끔하게 만들 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아래는 실제 Kerdy에서 CallAdapter를 적용하는 방법을 문서화 해둔 링크이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694432464150&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;CallAdapter 마이그레이션&quot; data-og-description=&quot;Contribute to woowacourse-teams/2023-emmsale development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; data-og-url=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jLNXc/hyTVUpU1my/TFsWEgfqbeBL9hsZXrHz7k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/CallAdapter-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jLNXc/hyTVUpU1my/TFsWEgfqbeBL9hsZXrHz7k/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;CallAdapter 마이그레이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to woowacourse-teams/2023-emmsale development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;지금이 최선일까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ski.jpg&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p0nHu/btssPwrmT26/18A6DktQpoVhVCEUgzjwR0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p0nHu/btssPwrmT26/18A6DktQpoVhVCEUgzjwR0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p0nHu/btssPwrmT26/18A6DktQpoVhVCEUgzjwR0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp0nHu%2FbtssPwrmT26%2F18A6DktQpoVhVCEUgzjwR0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;549&quot; height=&quot;309&quot; data-filename=&quot;ski.jpg&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;지금까지 커디(Kerdy)에서 서버 통신 응답을 어떻게 다루어왔는지 알아보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기존 코드의 불편함을 인식하고 비로소 코드를 수정하는 방식이야 말로 오래 기억에 남는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;하지만, 분명 이 외에도 더 효율적이고 우아한 방법이 있을 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;지금까지 그래왔듯이 당장은 코드에 문제를 느끼지 못하지만, 늘 어느 순간이 되면 문제점을 발견하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그때마다&amp;nbsp;코드를&amp;nbsp;수정하고,&amp;nbsp;더&amp;nbsp;나은&amp;nbsp;방법을&amp;nbsp;찾아보는&amp;nbsp;것이&amp;nbsp;개발자의&amp;nbsp;삶이라고&amp;nbsp;생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/tmdgh1592&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/tmdgh1592&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693754084152&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;tmdgh1592 - Overview&quot; data-og-description=&quot;tmdgh1592 has 44 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/tmdgh1592&quot; data-og-url=&quot;https://github.com/tmdgh1592&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hrQjU/hyTPBktZm2/QbZWkd9QBixcRUKet5dKzK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460&quot;&gt;&lt;a href=&quot;https://github.com/tmdgh1592&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/tmdgh1592&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hrQjU/hyTPBktZm2/QbZWkd9QBixcRUKet5dKzK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460');&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;tmdgh1592 - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;tmdgh1592 has 44 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>CallAdapter</category>
      <category>handle api</category>
      <category>RETROFIT</category>
      <category>Retrofit 에러 처리</category>
      <category>Sealed Class</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/132</guid>
      <comments>https://itstory1592.tistory.com/132#entry132comment</comments>
      <pubDate>Mon, 4 Sep 2023 00:20:48 +0900</pubDate>
    </item>
    <item>
      <title>[우아한테크코스] 레벨3 프로젝트[커디]를 마치며..</title>
      <link>https://itstory1592.tistory.com/131</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8WnDu/btsrIavfUfH/kPHSLxr7yDap6p4hZRI9x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8WnDu/btsrIavfUfH/kPHSLxr7yDap6p4hZRI9x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8WnDu/btsrIavfUfH/kPHSLxr7yDap6p4hZRI9x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8WnDu%2FbtsrIavfUfH%2FkPHSLxr7yDap6p4hZRI9x1%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;565&quot; height=&quot;190&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;눈 깜짝할 사이에 우아한테크코스(이하 우테코) 레벨 3 교육 과정이 마무리되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6월 6일, 마지막 포스팅 날짜를 보면 거의 2달 반 동안 포스팅을 작성할 시간이 부족했음을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(이에 대한 원인은 아래에서 다룰 것이다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;969780.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbe9CR/btsrBqy5qEp/am2sK2jTkkXdsnnuZXk2XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbe9CR/btsrBqy5qEp/am2sK2jTkkXdsnnuZXk2XK/img.png&quot; data-alt=&quot;'기승전결'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbe9CR/btsrBqy5qEp/am2sK2jTkkXdsnnuZXk2XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbe9CR%2FbtsrBqy5qEp%2Fam2sK2jTkkXdsnnuZXk2XK%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;501&quot; height=&quot;279&quot; data-filename=&quot;969780.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'기승전결'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;레벨 3 교육 과정을 아래에 보이는 기승전결 그래프로 요약할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래프에서 보이는 것처럼, 문제 발생으로 우리 팀은 다른 팀에 비해 부족한 시간을 가지게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금부터 커디 프로젝트에 대해 기승전결로 풀어나갈 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;기&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;(起) : 화목한(?) 프로젝트의 시작&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.04.58.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lQCzT/btsrR41KV4f/BplMWLNfdH3P8y0NwzaMEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lQCzT/btsrR41KV4f/BplMWLNfdH3P8y0NwzaMEk/img.png&quot; data-alt=&quot;'1차 데모데이 TODO'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lQCzT/btsrR41KV4f/BplMWLNfdH3P8y0NwzaMEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlQCzT%2FbtsrR41KV4f%2FBplMWLNfdH3P8y0NwzaMEk%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;529&quot; height=&quot;157&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.04.58.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'1차 데모데이 TODO'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우테코는 레벨3 교육 과정에서 이전 단계에서 배운 지식들을 바탕으로 팀 프로젝트를 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여러 크루들은 레벨 2 교육이 종료되기 전에 아이디어를 제출하며,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;선정된 아이디어에 따라 크루들을 랜덤하게 배치하는 구조이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리 팀은 백엔드 4명, 안드로이드 3명으로 구성되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음 주제는 무신사의 제품 가격 변동을 추적하는 서비스를 제공하는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주요 &lt;b&gt;문제 해결 목표&lt;/b&gt;는 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. &lt;b&gt;무신사에서 할인 제품이 실제로 할인 제품인지 확인하기. (할인으로 표기된 제품이 실제로 할인되었는지)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. &lt;b&gt;구매를 고려하는 상품이 미래에 할인될 가능성이 있는지, 과거 가격 추이를 확인하고 싶었다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;솔직히 말하면, 아이디어는 좋았지만 무신사 대신 쿠팡이나 11번가와 같이 여러 상품 가격을 비교할 수 있는 플랫폼을 타겟으로 하고 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사용자 입장에서 여러 상품 가격을 비교하고 저렴한 제품을 찾을 수 있는 서비스가 더 관심을 끌 것이라 생각했기 때문이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만, 팀 내에서 무신사를 선호하는 멤버들은 무신사 사용자도 충분히 많을 것이라고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내가 무신사를 사용하지 않아서 다른 서비스가 더 효율적일 것이라고 착각한 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 주제 선정 과정에서도 많은 교훈을 얻을 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사람들마다 생각이 다르다는 것을 알면서도,&amp;nbsp;내 생각이 다른 사용자의 입장일 것이라고 착각하는 사고방식을 반성할 수 있었기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주제와 타겟 플랫폼이 결정되었고, 구체적인 기능도 정의되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;더불어&amp;nbsp;&lt;a href=&quot;https://presentk.notion.site/57a5fb908fae4625a42406553a454097?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;코드 컨벤션&lt;/b&gt;&lt;/a&gt;과 &lt;a href=&quot;https://github.com/woowacourse-teams/2023-emmsale/wiki/Github-%EC%BB%A8%EB%B2%A4%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Git 컨벤션&lt;/b&gt;&lt;/a&gt;도 정리하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시간이 흘러 데모데이가 되었고, 아이디어를 제시하신 분께서 발표를 도맡아 하셨다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주요 질문 중 하나는 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;가격 추이를 제공하기 위해 데이터를 어떻게 수집할 계획인가요?&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 질문을 받기 전부터 팀에서 해당 문제에&amp;nbsp;대한 해결책을 찾는 것이 큰 고민이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;무신사는 별도로 API를 제공해주지 않기 때문에, 여러 고민 끝에 &lt;b&gt;크롤링&lt;/b&gt;을 택했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.12.40.png&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOrBWr/btsrCQ4XlK8/mO1LyhRoBUYwq5Xck56zKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOrBWr/btsrCQ4XlK8/mO1LyhRoBUYwq5Xck56zKK/img.png&quot; data-alt=&quot;'무신사 상의 카테고리 데이터 개수'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOrBWr/btsrCQ4XlK8/mO1LyhRoBUYwq5Xck56zKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOrBWr%2FbtsrCQ4XlK8%2FmO1LyhRoBUYwq5Xck56zKK%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;411&quot; height=&quot;106&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.12.40.png&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'무신사 상의 카테고리 데이터 개수'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에는 모든 상품을 크롤링하려 했지만 데이터가 너무 방대했다. 상품 카테고리만 10개가 넘었고, 그중에서도 상의 제품만으로도 20만 개가 넘어갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이로 인해 특정 카테고리에서만 우선적으로 크롤링을 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.14.48.png&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR1azN/btsrH75TiR1/D6HNoONGBHW5gi0FRZ0y9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR1azN/btsrH75TiR1/D6HNoONGBHW5gi0FRZ0y9K/img.png&quot; data-alt=&quot;'무신사 상품 업데이트 카테고리'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR1azN/btsrH75TiR1/D6HNoONGBHW5gi0FRZ0y9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR1azN%2FbtsrH75TiR1%2FD6HNoONGBHW5gi0FRZ0y9K%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;286&quot; height=&quot;146&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.14.48.png&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'무신사 상품 업데이트 카테고리'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;무신사는 상품 가격 변동이나 신제품 출시 시 업데이트 카테고리에서 변경 내용을 확인할 수 있도록 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 처음에는 모든 데이터를 수집한 다음, 업데이트 카테고리에서 변경된 내용만 업데이트할 계획이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;팀 내에서 충분히 고민한 내용을 질문으로 받았기 때문에, 적절하게 잘 답변할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;승&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;(承) : 크롤링이 굴린 스노우볼..&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 크롤링에서 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;크롤링은 무단으로 데이터를 수집하는 방식이기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코치님들은 레벨 3 진행 중에 크롤링으로 인한 문제 발생 가능성을 최소화하려 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt;결론적으로, 우테코 내에서 &lt;b&gt;데이터 무단 수집을 금지&lt;/b&gt;하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://www.donga.com/news/It/article/all/20230404/118678619/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;야놀자 vs 여기어때 웹크롤링 판례&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692592832672&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[그때 그 IT] 웹크롤링 판례 (2) 야놀자와 여기어때 간 숙박정보 크롤링&quot; data-og-description=&quot;&amp;lsquo;판례&amp;rsquo;란 법원이 특정 소송에서 법을 적용하고 해석해서 내린 판단입니다. 법원은 이 판례를 유사한 종류의 사건을 재판할 때 중요한 참고자료로 활용합니다. IT 분야는 기술의 발전&amp;hellip;&quot; data-og-host=&quot;www.donga.com&quot; data-og-source-url=&quot;https://www.donga.com/news/It/article/all/20230404/118678619/1&quot; data-og-url=&quot;https://www.donga.com/news/It/article/all/20230404/118678619/1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c08Jpe/hyTFplcaKc/HCIkrPO0YeL9CRP6ceit10/img.jpg?width=640&amp;amp;height=423&amp;amp;face=0_0_640_423,https://scrap.kakaocdn.net/dn/be8Vfz/hyTIM6THFD/bS5ChXk1URP6chtIEy3OK0/img.jpg?width=640&amp;amp;height=720&amp;amp;face=245_150_348_262,https://scrap.kakaocdn.net/dn/iyvEM/hyTFpFvc86/SQcQkwz8l96DP689r0kvkk/img.jpg?width=640&amp;amp;height=423&amp;amp;face=0_0_640_423&quot;&gt;&lt;a href=&quot;https://www.donga.com/news/It/article/all/20230404/118678619/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.donga.com/news/It/article/all/20230404/118678619/1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c08Jpe/hyTFplcaKc/HCIkrPO0YeL9CRP6ceit10/img.jpg?width=640&amp;amp;height=423&amp;amp;face=0_0_640_423,https://scrap.kakaocdn.net/dn/be8Vfz/hyTIM6THFD/bS5ChXk1URP6chtIEy3OK0/img.jpg?width=640&amp;amp;height=720&amp;amp;face=245_150_348_262,https://scrap.kakaocdn.net/dn/iyvEM/hyTFpFvc86/SQcQkwz8l96DP689r0kvkk/img.jpg?width=640&amp;amp;height=423&amp;amp;face=0_0_640_423');&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;[그때 그 IT] 웹크롤링 판례 (2) 야놀자와 여기어때 간 숙박정보 크롤링&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;판례&amp;rsquo;란 법원이 특정 소송에서 법을 적용하고 해석해서 내린 판단입니다. 법원은 이 판례를 유사한 종류의 사건을 재판할 때 중요한 참고자료로 활용합니다. IT 분야는 기술의 발전&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.donga.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 크롤링에 대해 처음부터 걱정하지 않은 것은 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇기에 1차 데모데이(2주 차) 직전까지도 거의 코치님들 한 분 한 분 찾아가며 크롤링 사용이 괜찮은지 여쭙기도 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대부분의 답변은 실제 사용자가 &lt;b&gt;거의 없는 시점에서 너무 깊이 걱정하지 않아도 된다&lt;/b&gt;는 내용이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;심지어 데모데이 직후에도 별다른 이야기가 나오지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만, 3주 차에 갑자기 크롤링과 관련한 공지가 발표되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요약하자면, 데이터 수집을 무단으로 금지한다는 내용이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;솔직히 조금은 당황스러웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이미 프로젝트 방향과 기능을 거의 마무리한 시점에 갑작스러운 주제 변경을 해야 했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이미 우테코&amp;nbsp;레벨 3교육이 시작된 지 1/4이 지났던 때였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 결과, 우리 팀은 다른 팀보다 프로젝트 시작이 2주 늦어지게 되었다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;API를 제공하는 여러 서비스를 검토했지만, 쿠팡은 이미 많은 Price Tracking 서비스가 존재하고 있었고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그 외에 농수산물 경매, 도매가격 API와 같은 대안은 우리가 해결하고자 했던 문제 방향과는 거리가 멀었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;(&lt;/b&gt;&lt;b&gt;나중에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;코치님에게&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;들어보니&lt;/b&gt;&lt;b&gt;, &lt;/b&gt;&lt;b&gt;농수산물&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;데이터&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;사용에는&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;가격&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;불일치와&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;업데이트&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;지연과&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;같은&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;문제가&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;있었다고&lt;/b&gt;&lt;b&gt; 한다&lt;/b&gt;&lt;b&gt;)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;PgQBwFbhQHCHXUoeX--cFePMhHnM5EmY8EhLDmgWFssQ2VG4vKSyiJSJdf_D0Qmp8cmV_fzDl5LT61apEHcCmQ.webp&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jf953/btsrCwMxQA9/dX2kdsC7UnJiyxCXgfh9tk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jf953/btsrCwMxQA9/dX2kdsC7UnJiyxCXgfh9tk/img.webp&quot; data-alt=&quot;'당시 우리팀 모습과 분위기..'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jf953/btsrCwMxQA9/dX2kdsC7UnJiyxCXgfh9tk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJf953%2FbtsrCwMxQA9%2FdX2kdsC7UnJiyxCXgfh9tk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;230&quot; data-filename=&quot;PgQBwFbhQHCHXUoeX--cFePMhHnM5EmY8EhLDmgWFssQ2VG4vKSyiJSJdf_D0Qmp8cmV_fzDl5LT61apEHcCmQ.webp&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'당시 우리팀 모습과 분위기..'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 새로운 주제로 방향을 잡아야 했고, 팀의 분위기는 점점 바닥을 쳤다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특히 아이디어를 제출한 팀원은&lt;b&gt; &lt;/b&gt;의욕을 많이 상실하여, 거의 일주일간 대화하는 모습을 거의 본 적이 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;전&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;(轉) : &lt;/span&gt;새로운 주제 선정, 처음부터 다시 시작&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.55.48.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IgHic/btsrIbm4F8g/WmHyPDohMPK1upXCNgwKU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IgHic/btsrIbm4F8g/WmHyPDohMPK1upXCNgwKU0/img.png&quot; data-alt=&quot;'2차 데모데이 TODO'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IgHic/btsrIbm4F8g/WmHyPDohMPK1upXCNgwKU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIgHic%2FbtsrIbm4F8g%2FWmHyPDohMPK1upXCNgwKU0%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;718&quot; height=&quot;462&quot; data-filename=&quot;스크린샷 2023-08-21 오후 1.55.48.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'2차 데모데이 TODO'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1차 데모데이에서의 노력이 헛되게 되었으니, &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드 컨벤션, Git 컨벤션처럼 기획과는&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;별개의&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;내용을&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;제외하고&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;4&lt;/span&gt;주분의&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;작업을&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;2&lt;/span&gt;주&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;안에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;마쳐야&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;했다&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;팀은 이전보다 훨씬 바쁘게 움직였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우리는 다시 처음으로 돌아가 주제를 새롭게 선정했고, 개발자들에게 필요한 서비스를 만들어보기로 결정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;최종로고 1.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcki32/btsrB3xbMrj/00WctUjWJqf6D1cyQ3MHaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcki32/btsrB3xbMrj/00WctUjWJqf6D1cyQ3MHaK/img.png&quot; data-alt=&quot;'커디(Kerdy) 로고'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcki32/btsrB3xbMrj/00WctUjWJqf6D1cyQ3MHaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdcki32%2FbtsrB3xbMrj%2F00WctUjWJqf6D1cyQ3MHaK%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;200&quot; data-filename=&quot;최종로고 1.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'커디(Kerdy) 로고'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자들을 하나로 잇다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이름하여, &lt;b&gt;커디(Connect Developer)&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;이&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;서비스는&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;사용자에게&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;컨퍼런스와&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;대회&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;정보를&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;제공하며&lt;/b&gt;&lt;b&gt;, &lt;/b&gt;&lt;b&gt;개발자들이&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;함께&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;컨퍼런스에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;참여하고&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;싶을&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;때&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;다른&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;개발자와&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;함께&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;할&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;수&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;있는&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;기회를&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;제공한다&lt;/b&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;컨퍼런스 정보를 제공하는 서비스는 많지만, 함께 가기를 원하는 요청 기능을 갖춘 서비스는 없어서, 다른 서비스와 큰 차별성을 가지고 있다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커디의 &lt;b&gt;기대효과&lt;/b&gt;는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. &lt;b&gt;본인은&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;안드로이드&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;개발자이고&lt;/b&gt;&lt;b&gt;, &lt;/b&gt;&lt;b&gt;주변에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;백엔드나&lt;/b&gt;&lt;b&gt; iOS &lt;/b&gt;&lt;b&gt;개발자가&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;없을&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;때&lt;/b&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. &lt;b&gt;비전공자&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;출신이어서&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;개발자들을&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;잘&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;모르며&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;혼자&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;컨퍼런스에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;가기가&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;어려울&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;때&lt;/b&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. &lt;b&gt;같은&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;조직&lt;/b&gt;&lt;b&gt;(e.g.&lt;/b&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;b&gt;우테코&lt;/b&gt;&lt;b&gt;)&lt;/b&gt;&lt;b&gt;에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;속한&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;개발자와&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;함께&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;컨퍼런스에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;참여하고&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;싶을&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;때&lt;/b&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;과연 모르는 사람들과 함께 컨퍼런스에 가려고 할까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 처음에 크롤링만큼이나 걱정했던 점이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 기대효과를 정리하니 충분히 수요가 있는 서비스임을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;모르는 사람들과 커피챗도 하는 시대&lt;/b&gt;에서, &lt;b&gt;비슷한 관심사를 가진 개발자들과 함께 컨퍼런스에 가고 싶어 하는 사용자도 충분히 있을 것&lt;/b&gt;이라고 판단했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기대 효과 중에서도 특히 &lt;b&gt;3번 목표&lt;/b&gt;가 매력적으로 다가왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;예를&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;들어&lt;/b&gt;&lt;b&gt;, &lt;/b&gt;&lt;b&gt;우테코&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;선배&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;중에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;같은&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;분야&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;개발자와&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;함께&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;컨퍼런스&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;또는&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;대회에&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;참여하고&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;싶은&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;경우가&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;많을&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;것이라고&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;생각하며&lt;/b&gt;&lt;b&gt;, &lt;/b&gt;&lt;b&gt;팀원들도&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;동의했다&lt;/b&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4022da611b6d48eeb6896a2e42b59dff.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qmk4b/btsrErw5pOp/4PfzzDECFtxEi59cn3Q1Yk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qmk4b/btsrErw5pOp/4PfzzDECFtxEi59cn3Q1Yk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qmk4b/btsrErw5pOp/4PfzzDECFtxEi59cn3Q1Yk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQmk4b%2FbtsrErw5pOp%2F4PfzzDECFtxEi59cn3Q1Yk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;268&quot; height=&quot;268&quot; data-filename=&quot;4022da611b6d48eeb6896a2e42b59dff.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;새로운 주제와 목표 역시 Price Tracker만큼이나 마음에 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기능 명세, 와이어 프레임, 사용자 스토리 작성, CI/CD 구축, Firebase 베타 테스트 환경 구축 등 할 일이 많았지만,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;축 쳐진 처음의 분위기와 다르게, 모두 우리의 처음 동기와 설레는 마음으로 작업을 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;결&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;(結) : 우테코와 커디는 행복하게 살았다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시간이 어떻게 흘렀는지 모를 만큼, 다들&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;요구사항과 기능 개발에만 몰두했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마지막 데모데이가 다가온 시점에는, 눈을 떴을 때 제외하고는 안드로이드 스튜디오와 팀원들과 함께한 시간뿐이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다양한 기능들을 기획했으며, 검색을 제외하고는 레벨 3에서 목표한 기능 요구사항을 모두 완성해냈다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;컨퍼런스 함께 가요, 댓글/대댓글, FCM 푸시 알림, 컨퍼런스/대회 조회 및 필터링, 프로필(이름, 관심 직무, 동아리 등) 등을 5주 안에 모두 구현한 커디(Kerdy) 팀이 자랑스러웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;b5c970a9080c339715f2cc5e15deb023.jpg&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CMjdb/btsrDDExolH/qos7adiqcQXWiAVlmjfjzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CMjdb/btsrDDExolH/qos7adiqcQXWiAVlmjfjzK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CMjdb/btsrDDExolH/qos7adiqcQXWiAVlmjfjzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCMjdb%2FbtsrDDExolH%2Fqos7adiqcQXWiAVlmjfjzK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;236&quot; height=&quot;173&quot; data-filename=&quot;b5c970a9080c339715f2cc5e15deb023.jpg&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;결론적으로, &lt;b&gt;기한&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;내에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;앱을&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;성공적으로&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;&amp;nbsp;Play&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;스토어에&amp;nbsp;출시&lt;/b&gt;하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;그 순간만큼은 정말로 뿌듯하고 감격스러웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;잘한 점&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 안드로이드에서의 Domain&amp;nbsp;모델 사용 여부에 대한 고민&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트 도중 안드로이드에서 도메인 모델의 필요성에 대한 깊은 고민을 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이전 레벨의 교육에서 처음 들어본 도메인 개념을 바탕으로 미션에 도메인 모델을 추가하여 각종 비즈니스 로직을 처리하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 레벨 3에서는 서버에서 검증을 처리하거나 UI에서 간단한 작업으로 해결할 수 있는 것들이 많아지면서,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안드로이드에서 도메인 모델의 필요성에 대한 의문이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들어, 이전 미션들에서는 사용자 이름에 대해 UserName라는 도메인 모델을 만들었다면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;init {...} 초기화 블록에서 사용자명은 특정 길이를 넘지 못하거나, 특수 문자를 사용할 수 없다 등의 비즈니스에 종속적인 자료구조를 만들곤 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 레벨 3에서는 이러한 모든 검증 과정은 서버에서 처리해 주었고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;심지어 위의 예시와 같은 상황도 도메인 모델이 아닌, EditText의 maxLength를 설정해서 해결할 수도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한, 특수문자와 같은 검증 로직은 Domain 모델이 아닌 ViewModel에서 작업해도 충분하다고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다른 팀은 어떻게 했을지 궁금해서 찾아보니,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Domain 모델은 만들어 놓았지만, 결국 DTO(클라이언트 &amp;lt;-&amp;gt; 서버)와 1 : 1 관계이거나, 아무런 기능을 하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또는 어떠한 검증 로직을 정의해 두어 예외가 발생하게끔 하였지만, 아래와 같이 코드가 복잡해지거나 조금은 어색해 보이는 문제가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692598755393&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class xxxViewModel : ViewModel() {
	...

    fun changeName(name: NameUiModel) {
        ...

        runCatching {
            name.toDomain() // ..?
        }.onSuccess {
            ...
        }.onFailure {
            ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 백엔드 개발자 입장이라면, 도메인 모델이 어떠한 관계를 가지는지 충분히 고민할 필요가 있어 보였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;e.g. 컨퍼런스와 스크랩한 컨퍼런스 모델은 같은가?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;e.g. 만약 같다면 컨퍼런스가 스크랩 날짜를 가지고 있는 사실이 정말로 유지보수에 유연한가? / 자연스러운가?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;e.g. 또 다른 컨퍼런스가 추가된다면 또다시 속성이 추가되어야 하는 것 아닌가? OCP에 대해 고민해 보았는가?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 안드로이드 개발자 입장에서 다음과 같은 상황을 제외하고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;u&gt;Domain 모델이라는 개념이 과연 필요할까?&lt;/u&gt;라는 의문이 들기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 서버에서 검증 로직을 처리해주지 못하는 Firebase를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 서버 없이 로컬에서 모든 것을 처리하는 서비스이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 서버가 있어도 가벼운 로직은 클라이언트에서 한 번 더 검증해 주는 것이 나쁘다고 생각하지는 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 프로젝트 규모가 커져서(기업 규모), 개발자가 실수로 비즈니스 로직을 다르게 구현하는 상황을 방지하기 위함이라면 충분히 필요성을 느낄 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;e.g. 정해진 이름의 길이는 8자인데, A 개발자는 7자 / B개발자는 8자로 설정할 수 있는 가능성 감소&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 프로젝트 규모가 작고, View 위젯에서 간단히 설정해주기만 하면 되는 코드까지 도메인 모델을 만드는 작업이 지금 당장은 오버 엔지니어링이라고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결론적으로, 왜 도메인 모델을 적용했냐고 물었을 때 당당하게 말할 수 없을 것 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서버에서는 결국 도메인 모델을 반환해 주는 것이 아닌,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;도메인 모델들을&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;특정 UI에서 필요한 값들로 가공하고 조합하여 하나로 만들어진&amp;nbsp;&lt;/span&gt;DTO를 반환해 주기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DTO만으로 서버처럼 도메인 모델을 재구성하는 것이 애당초 불가능하기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼에도 도메인 모델을 적용했다면 다음과 같은 구조가 되었을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;b&gt;(클라이언트) &lt;/b&gt;UI &amp;lt;-&amp;gt; Domain &amp;lt;-&amp;gt; Data &amp;lt; -HTTP-&amp;gt; (서버) Domain&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결국 커디팀은 과연 안드로이드에서 &lt;b&gt;&lt;u&gt;도메인이라는 명칭을 사용하는 것이 올바를까?&lt;/u&gt; &lt;/b&gt;라는 고민을 했고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;도메인 모델이 아닌, &lt;b&gt;&lt;u&gt;데이터 모델&lt;/u&gt;&lt;/b&gt;이라는 개념을 만들어 사용하기로 결정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터 모델은 Remote(서버)와 Local(Room 등..)에서 타입이 다른 경우 통일화 시켜주기 위한 모델이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로 대중화된 개념인지는 모르겠지만, 커디 팀에서는 데이터 모델이라는 명칭을 만들어서 사용 중이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번 프로젝트를 하면서 가장 고민을 많이 하고, 안드로이드 팀원들과 수차례 토론하며 강조했던 내용이라 글이 조금 길어졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(아마 따로 포스팅을 하고 싶을 정도로..?)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 내용을 잘한 점에 추가한 이유는,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결론이 어떻게 정해졌든, 안드로이드에서의 도메인이라는 개념이 필요한가에 대해 깊게 고민해 볼 수 있었기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 문서와 함께 기술 파헤치기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우테코를 시작하면서 어떤 기술을 적용하려고 할 때, 공식 문서와 포스팅을 함께 보는 습관을 들이게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;때문에, 이번 프로젝트에서 FCM, 깃허브 로그인을 구현하면서&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원래 같았으면 빠르게 구현하기 위해 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스팅만 보고 잘 모르고 사용했던 기술들도 조금은 깊이 있게 공부할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스팅만 보고 구현하면, 지금 당장 기능 자체는 빠르게 구현할 수 있지만, 그 과정에서 어떤 일이 일어나는지 알 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇게 되면 문제가 발생했을 때 무엇이 문제일지 쉽게 추측하기가 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 4.47.45.png&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bohKQP/btsrIcmhiVf/w09XidMUJ1FngBRv5gU4xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bohKQP/btsrIcmhiVf/w09XidMUJ1FngBRv5gU4xK/img.png&quot; data-alt=&quot;'FCM의 notification vs data'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bohKQP/btsrIcmhiVf/w09XidMUJ1FngBRv5gU4xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbohKQP%2FbtsrIcmhiVf%2Fw09XidMUJ1FngBRv5gU4xK%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;696&quot; height=&quot;254&quot; data-filename=&quot;스크린샷 2023-08-21 오후 4.47.45.png&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'FCM의 notification vs data'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들어, FCM에서는 백그라운드와 포그라운드 상태, 그리고 RemoteMessage 형태에 따라 동작하는 방식이 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백그라운드에서 notification이 포함된 RemoteMessage를 전달받으면 자동으로 푸시 알림을 보여주지만,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;onMessageReceived() 메서드는 호출되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이러한 사실을 제대로 공부하지 않았다면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드 개발자분에게 아무런 형태로 데이터를 보내줘도 괜찮다고 했을 것이고, 문제 해결에 오랜 시간을 투자했을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이처럼 대충 사용하는 방법만 아는 것이 아니라,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;문서를 통해 이론적인 내용을 공부하고, 직접 코드로 구현해 보면서 여러 상황에서 어떻게 대처해야 하는지 미리 가정하는 습관을 들이는 것이 중요하다는 사실을 다시금 느낄 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;아쉬웠던 점&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;하지만 감격스러운 순간과 함께 아쉬운 부분도 분명 있었다..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;1. 코드리뷰&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 2.35.36.png&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caU7RO/btsrCQYsRq2/VLQeh4sxTJUotMqJ3TTML1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caU7RO/btsrCQYsRq2/VLQeh4sxTJUotMqJ3TTML1/img.png&quot; data-alt=&quot;'본인 PR 코드리뷰 중 일부'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caU7RO/btsrCQYsRq2/VLQeh4sxTJUotMqJ3TTML1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaU7RO%2FbtsrCQYsRq2%2FVLQeh4sxTJUotMqJ3TTML1%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;2042&quot; height=&quot;382&quot; data-filename=&quot;스크린샷 2023-08-21 오후 2.35.36.png&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'본인 PR 코드리뷰 중 일부'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;각 데모데이 초기에는 코드리뷰가 원활하게 이루어졌지만,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;아무래도 다른 팀에 비해 시간이 부족했던 탓에 이런 환경을 꾸준히 유지하지는 못했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;코드 리뷰를 하면서 내가 놓친 부분이나 개선이 필요한 부분에 대해 다른 사람의 의견을 들을 수 있던 점이 정말 좋았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;그렇기에 더더욱 포기하고 싶지 않았지만 시간은 우리를 기다려주지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;결국 안드로이드 팀원들과 상의한 결과, 각 PR을 Q/A 해보고, 기능 상에 문제가 없다면 우선은 approve 하는 방향으로 흘러갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;그리고 이후에 데모데이가 끝났을 때, 다 같이 코드를 통일화하고 개선하는 작업을 하기로 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;이런 상황에 대해 조금 더 깊이 있게 고민하고 싶어 여러 문서를 찾아보니,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;Ship - Show - Ask라는&lt;/b&gt; &lt;b&gt;PR 전략&lt;/b&gt;이 있었다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;요약하자면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;실제 현업에서는 &lt;u&gt;배포까지 시간이 부족&lt;/u&gt;하고 &lt;u&gt;PR에 피로감&lt;/u&gt;을 느끼는 사람들을 위해,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;상황에 따라 적절히 사용할 수 있는 Ship, Show, Ask 3가지 옵션을 제공하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;너무 매력적이라 자세한 내용은 레벨 4 과정에서 프로젝트 유지보수 단계에 도입하고, 그 결과를 포스팅할 예정이다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left; font-family: 'Noto Sans Light';&quot;&gt;문제점을 파악하고 개선해 나간다면 아쉬움을 조금이나마 덜어낼 수 있을 것 같다는 생각이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 조금은(?) 지저분한 코드&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에는 최대한 코드를 깔끔하게 작성하고자 노력했지만,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;배포 날짜에 쫓기다 보니 시간이 지날수록, 우선 돌아가는 쓰레기라도 만들자 라는 마인드로 임하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다고 AAC ViewModel에서 context를 참조하거나,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터바인딩의 BindingAdapter를 남용하거나,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;변수명을 a, b, c와 같이 난독화된 것처럼 작성하는 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;등의 죄악은 일절 저지르지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다만, 레벨 1, 2에서 코드 리뷰를 받던 시절처럼&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모든 함수를 깔끔하게 분리하거나,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;depth가 2를 초과하지 않게끔 하거나,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;더 좋은 메서드명을 고민하기 위해 시간을 많이 투자하지는 못했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아쉬움 때문인지 코치님께도 현업에서 이런 상황이 발생하는지 여쭤보기도 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코치님께서는 다음과 같이 말씀해 주셨다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모든 기업을 다 경험한 것은 아니지만, 예전에 일했던 부서에서 처음 코드를 봤을 때 매우 충격이었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;소위 들으면 다 알법한 기업임에도 불구하고, 당장 서비스를 배포해야 하는 상황에서 테스트 코드를 작성하거나 코드를 깔끔하게 작성하는 상황이 쉽지는 않다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오히려, 기업 입장에서는 코드를 깔끔하게 작성하고 테스트 코드를 작성하는 것보다,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서비스로 돈을 벌어주는 것이 좋은 서비스이고 좋은 프로젝트일 수 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 말을 듣고 조금은 위로가 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시간이 부족한 우리 팀의 상황과 결과가 코치님께서 경험하신 현업과 많이 비슷했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(우리만 이런게 아니구나 하는..?)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;바쁜 상황에서도 목표한 기능을 모두 구현하고, 컨벤션과 코드 형식을 모두 지키기 위해 노력한 우리 팀이 더 자랑스러워졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래도 우리는 현업이 아닌, 학습을 하는 과정이기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안도하지 않고 레벨 4 시작 무렵에 팀원들과 다 같이 코드 리팩토링을 하며 문제를 해결해 나갈 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 테스트 코드는 대체 어디?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 가장 아쉬운 점은 테스트 코드 작성이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;레벨 1, 2 교육과정에서 그렇게 강조한 테스트 코드를 전혀 작성하지 못했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-21 오후 3.00.51.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rxeD0/btsrNm9PMDG/rdjqqB13kKAvZyyaLKs30K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rxeD0/btsrNm9PMDG/rdjqqB13kKAvZyyaLKs30K/img.png&quot; data-alt=&quot;'본인이 작성한 테스트 코드 문화'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rxeD0/btsrNm9PMDG/rdjqqB13kKAvZyyaLKs30K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrxeD0%2FbtsrNm9PMDG%2FrdjqqB13kKAvZyyaLKs30K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;92&quot; data-filename=&quot;스크린샷 2023-08-21 오후 3.00.51.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'본인이 작성한 테스트 코드 문화'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음 1주 차에서 컨벤션을 정할 때, 본인이 작성한 커디(Kerdy)팀의 &lt;a href=&quot;https://presentk.notion.site/301892e7645649c99ca5e7420599b71b?pvs=4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 코드 문화&lt;/a&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트 코드는 &lt;u&gt;개발한 기능에 대한 근거&lt;/u&gt;가 된다고 떠들어 놓고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정작 아무 테스트 코드도 작성하지 못했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시간이 부족했기 때문인 것은 사실이지만,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트 코드를 작성하지 못한 것 또한 사실이기에 그 어떤 것보다 아쉬움이 컸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 안드로이드 팀은 레벨 3에서 레벨 4로 넘어가는 방학 기간 중에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;본인이 개발한 기능에 대해 테스트 코드를 작성해 오는 것을 숙제로 정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MVVM 패턴을 사용하기 때문에 ViewModel에 대한 테스트 코드를 작성해 올 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 검색 기능 등을 제외하고는 이미 만들어 놓은 기능이 충분하기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;레벨 4에서는 대규모 기능 추가보다는,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;버그를 고치거나 기존 기능을 확장하면서 &lt;u&gt;반드시 테스트 코드를 작성&lt;/u&gt;한 후에 PR을 올리는 것을 목표로 할 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트를 마무리하며..&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;방학 동안 코드 리팩토링과 테스트 코드 작성, 개인적인 성장을 위한 시간을 가질 계획이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트에서 겪은 경험과 고민을 바탕으로 더 나은 결과를 이뤄내기 위해 노력할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/우아한테크코스</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>우테코 5기</category>
      <category>우테코 레벨3 회고</category>
      <category>커디</category>
      <category>팀프로젝트</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/131</guid>
      <comments>https://itstory1592.tistory.com/131#entry131comment</comments>
      <pubDate>Mon, 21 Aug 2023 18:59:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] OkHttp &amp;amp; Retrofit</title>
      <link>https://itstory1592.tistory.com/130</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKCnf/btsiNbqqS6h/dJCNUEdeWeppazpKHG7mGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKCnf/btsiNbqqS6h/dJCNUEdeWeppazpKHG7mGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKCnf/btsiNbqqS6h/dJCNUEdeWeppazpKHG7mGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKCnf%2FbtsiNbqqS6h%2FdJCNUEdeWeppazpKHG7mGK%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;310&quot; height=&quot;310&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Android에서 네트워크 작업을 할 때 사용하는 대표적인 라이브러리는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;OkHttp&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Retrofit&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;두 라이브러리 모두 Square사에서 개발한 HTTP 통신 라이브러리이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OkHttp는 HTTP 통신을 간편하게 할 수 있는 기능을 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Retrofit은 OkHttp를 조금 더 추상화하여, 직관적이고 편리하게 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Retrofit은 OkHttp에 의존하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OkHttpClient를 Retrofit의 Builder에 전달하여 생성하는 방식을 따른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결론적으로, Retrofit이 OkHttp에 비해 가진 장점은 크게 &lt;b&gt;3가지&lt;/b&gt;라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;interface와 어노테이션을 사용하여 직관적이며, Call을 직접 만들어줄 필요가 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;String 타입으로 전달되는 JSON 형태의 body를 직접 파싱 할 필요가 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;응답에 대한 작업을 자동으로 Ui Thread에서 수행할 수 있도록 지원한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 Retrofit의 장점을 OkHttp와 비교하여 살펴보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1686054406861&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val okHttpClient = OkHttpClient.Builder() // 1.
    .addInterceptor(...)
    .build()
            
val productJson = Gson().toJson(Product(1, &quot;과자&quot;)) // 2.

// 서버에 POST 하기 위한 Request Body
// Json 형태의 String을 RequestBody로 변환
val requestBody = productJson.toRequestBody(&quot;application/json&quot;.toMediaType()) // 3.

// 요청을 위한 Request
val request = Request.Builder().run { // 4.
    url(&quot;https://localhost:8080/products&quot;)
    header(&quot;Authorization&quot;, &quot;Basic {token}&quot;)
    post(requestBody)
    build()
}

okHttpClient.newCall(request).enqueue(object : Callback { // 5.
    override fun onFailure(call: Call, e: IOException) {
        // Handle this
    }

    override fun onResponse(call: Call, response: Response) {
        val jsonObject = JSONObject(response.body?.string() ?: &quot;{}&quot;) // 6.
        runOnUiThread {
            ... // Ui Logic
        }
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OkHttp를 사용하여 서버에 POST 요청을 보내는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. OkHttpClient 인스턴스를 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. Body를 만들기 위해,  객체를 Json 형태로 변환해준다. (Gson, kotlinx-serialization 등 활용할 수 있음.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. productJson을 OkHttp에서 제공해주는 확장 함수 toRequestBody()를 통해 RequestBody 객체로 변환한다. 이때, MediaType을 지정해주어야 하는데, Content-Type을 &quot;&lt;b&gt;application/json&lt;/b&gt;&quot;으로 지정해주었다. (Json 형태로 전달할 것이기 때문.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 실질적으로 서버에 요청을 보내기 위한 request 객체를 생성한다. url, method, body를 지정해 준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 서버에 HTTP 통신을 요청한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;enqueue() 메서드를 사용하면 Background Thread에서 비동기적으로 수행된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onResponse(), onFailure() 콜백을 통해, 성공 실패에 대한 처리를 쉽게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;execute() 메서드를 사용하면 현재 Thread에서 동기적으로 이루어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 서버로부터 응답을 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OkHttp는 response.body?.string()를 통해 Json 형태(String)를 만들고 이를 객체로 변환해주어야 한다.&lt;/li&gt;
&lt;li&gt;enqueue() 메서드를 사용했다면, Background에서 응답을 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 방식은 과거 HttpUrlConnection을 통해 서버와 통신하는 방식에 비하면 정말 편리한 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 클라이언트들은 아직도 목마를 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;필자가 OkHttp를 사용하면서 느낀 불편함은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 모든 Request, Response에 대해 , Json &amp;lt;-&amp;gt; Object 변환을 해주어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 매번 Request 객체를 직접 만들어주는 것이 번거롭게 느껴지고, 가독성이 떨어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. onResponse가 Background Thread에서 이루어지기 때문에, UI 작업을 위해 매번 runOnUiThread, Handler(Looper.getMainLooper()).post { ... } 등의 코드가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 대부분의 경우, base url은 하나일 텐데 매번 full url을 지정해 주는 것이 불편하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 단점을 보완하기 위해 Retrofit이 등장하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 코드를 Retrofit으로 변환해 보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1686056964517&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val okHttpClient = OkHttpClient.Builder().run {
    addInterceptor(...)
    build()
}

private val retrofit = Retrofit.Builder()
    .baseUrl(&quot;https://localhost:8080&quot;)
    .addConverterFactory(GsonConverterFactory.create()) // GSON 컨버터를 사용하여 JSON 자동 변환
    .client(okHttpClient)
    .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, OkHttp와 다르게, Retrofit.Builder()를 통해 Retrofit 인스턴스를 생성해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 공통적으로 사용될 baseUrl을 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. converterFactory를 지정함으로써, Json &amp;lt;-&amp;gt; Object 변환을 직접 해주지 않고 라이브러리가 대신해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. OkHttp의 OkHttpClient를 지정해주어야 한다. (각종 Request, Response에 대한 Intercept 처리가 가능하다.)&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;위 코드는 공통이므로, top-level function 또는 singleton 형태로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 실행될 때 메모리에 올릴지, 사용하는 시점에 올릴지는 앱이 언제부터 서버와 통신하는지에 따라 개발자가 유연하게 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1686058087303&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ProductService {
    @GET(&quot;/products&quot;)
    fun getProducts(): Call&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit은  Call을 반환해 주는 interface를 통해, 직관적이고 간편하게 개발할 수 있도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttp에서는 okHttpClient.newCall(request) 메서드를 호출하여 call을 만들어주어야 하기 때문에, Request를 만들어주는 작업이 불편하게 느껴졌지만, Retrofit은 이런 작업을 생략해 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, Annotation(GET, POST, PATCH 등)을 지정해 주어 훨씬 직관적이다.&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;+ ) 일반적으로 HTTP 통신을 위한 interface에는 &lt;b&gt;Service&lt;/b&gt; Suffix를 붙여준다. ex) XxxService&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1686058321030&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val productService = retrofit.create(ProductService::class.java)
        
productService.getProducts().enqueue(object : Callback&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; {
    override fun onResponse(
        call: Call&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;,
        response: Response&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;
    ) {
        // Ui Thread에서 동작한다.
        val products = response.body()
        // ...
    }

    override fun onFailure(call: Call&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;, t: Throwable) {
        // ...
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retrofit.create(T::class.java)를 통해 T에 해당하는 Service 인스턴스를 생성해 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Call, Request는 자동으로 Retrofit에서 만들어주기 때문에, 개발자는 OkHttp와 동일하게 enqueue(), execute() 메서드를 호출하여 작업을 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 다른 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. enqueue에 대한 Callback 작업이 Ui Thread에서 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. response.body() 메서드 반환타입이 List&amp;lt;Product&amp;gt; 라는 점이다. 즉, 알아서 Json을 Object로 파싱 하여 개발자가 할 일이 매우 줄어든다.&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;2번이 Retrofit의 가장 큰 장점이라고 할 수 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 개발자가 Json의 Raw한 문자열을 잘못 파싱 할 수도 있는 문제를 줄여준다. (&lt;b&gt;Type-safe &lt;/b&gt;하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 직접 파싱할 필요가 없기 때문에 코드가 간결해지고, 편리함을 제공해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit과 OkHttp 라이브러리를 적절히 사용하면 성능, 유지보수를 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Retrofit이 OkHttp보다 성능이 우월하다&quot; 라기보다는 이 둘을 적절히 활용하여 성능을 높일 수 있는 것이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttp의 Interceptor를 통해, Request, Response가 직전, 직후에 작업을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Header에 Authorization 값을 지정하여 매 요청마다 Header를 지정해 줄 필요가 없는 등의 편리함이 존재한다.&lt;/p&gt;</description>
      <category>Android</category>
      <category>okhttp</category>
      <category>RETROFIT</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/130</guid>
      <comments>https://itstory1592.tistory.com/130#entry130comment</comments>
      <pubDate>Tue, 6 Jun 2023 22:52:53 +0900</pubDate>
    </item>
    <item>
      <title>[Android] BroadcastReceiver 보안 이슈</title>
      <link>https://itstory1592.tistory.com/129</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Android_logo_2019_(stacked).svg.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rh97r/btsf5ewEyxT/aHF6hjlCXD6BrKp2kMbZX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rh97r/btsf5ewEyxT/aHF6hjlCXD6BrKp2kMbZX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rh97r/btsf5ewEyxT/aHF6hjlCXD6BrKp2kMbZX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRh97r%2Fbtsf5ewEyxT%2FaHF6hjlCXD6BrKp2kMbZX0%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;467&quot; height=&quot;408&quot; data-filename=&quot;Android_logo_2019_(stacked).svg.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;BroadcastReceiver&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/components/broadcasts?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BroadcastReceiver&lt;/a&gt;는 Android 4대 컴포넌트 중 하나이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시스템에서 발생하는 &lt;b&gt;이벤트를 수신하기 위한 목적&lt;/b&gt;을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를 들어, &lt;b&gt;화면이 꺼졌다가 켜진다&lt;/b&gt;던지, &lt;b&gt;단말 부팅이 완료&lt;/b&gt;되었다던지, 또는 &lt;b&gt;개발자가 임의로 시스템에 이벤트를 전달&lt;/b&gt;하여 Receiver들에게 전달할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 이벤트를 전달하는 단위를 Android에서는 &lt;b&gt;Broadcast&lt;/b&gt;라고 부른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이름 뜻 그대로, 방송이라는 의미를 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그렇기 때문에 이러한 방송을 보고 싶어하는 어플리케이션에서는 이를 수신할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Broadcast는 크게 2가지로 구분된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;바로&lt;b&gt; 명시적 브로드캐스트 vs 암시적 브로드캐스트 &lt;/b&gt;이다.&lt;/span&gt;&lt;/p&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;명시적 브로드캐스트 (Explict Broadcast)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특정 대상(Package, BroadcastReceiver ...)를 명시적으로 지정하여 전달하는 Broadcast를 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;단 하나의 BroadcastReceiver를 대상으로 전송하는  브로드캐스트이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;명시적 브로드캐스트는 Manifest에 등록한 Receiver를 통해서도 전달받을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;E.g. Package와 Broadcast 클래스를 지정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684250981164&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val 명시적_컴포넌트 = ComponentName(
	&quot;com.example.secondexample&quot;,
    &quot;com.example.secondexample.MyBroadcastReceiver&quot;
)
val 명시적_인텐트 = 
	Intent(&quot;com.example.secondexample.ACTION_MAIN&quot;).setComponent(명시적_컴포넌트)
    
sendBroadcast(명시적_인텐트)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;-&amp;gt; com.example.secondexample의 MyBroadcastReceiver 라는 Receiver에게 전달한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;암시적 브로드캐스트 (Implict Broadcast)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수신할 수 있는 Receiver가 여러개일 수 있는 Broadcast를 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;API 26(Oreo)&lt;/b&gt; 이상부터는 &lt;b&gt;Manifest에 등록한 Receiver로는 암시적 브로드캐스트를 수신할 수 없다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그러나, 일부 암시적 브로드캐스트는 아직도 Manifest에서 수신할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대표적인 예시로 &lt;b&gt;BOOT_COMPLETED&lt;/b&gt;가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;공식문서에 따르면 앱이 부팅되었을 때, 알람 매니저를 재등록하는 등의 행위를 위해 일부는 수신가능하게끔 제한을 두지 않았다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 여전히 권장사항은 아니며, 가능한 Context의 &lt;b&gt;registerReceiver()&lt;/b&gt;를 통해 앱이 실행 중인 동안에만 수신하는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;(이유는 아래 설명한다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;E.g. Action만 지정 (해당 Action을 filtering하는 Receiver는 모두 수신 가능)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684251050896&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val myAction = &quot;com.example.secondexample.ACTION_MAIN&quot;
val intentFilter = IntentFilter(myAction)
registerReceiver(MyBroadcastReceiver(), intentFilter)

val intent = Intent(myAction)
sendBroadcast(intent)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;-&amp;gt; com.example.secondexample.ACTION_MAIN 라는 action을 필터링하는 receiver에게 모두 전달된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;-&amp;gt; 단, API 26 이상에서는 manifest에 등록한 receiver로는 수신할 수 없다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안드로이드에서 &lt;u&gt;manifest에 등록한 receiver가 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;암시적 브로드캐스트를&amp;nbsp;&lt;/span&gt;수신하지 못하도록 제한한 이유&lt;/u&gt;는 크게 2가지다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 기본적으로 Broadcast를 수신 가능한 Receiver가 있다면, 해당 어플리케이션을 실행시켜 프로세스를 메모리에 올린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 때, &lt;b&gt;한 번에 많은 앱이 메모리에&lt;/b&gt; 올라가면서 &lt;b&gt;화면 버벅임&lt;/b&gt;과 같이 사용자에게 좋지 않은 UX를 제공하는 문제가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 암시적 Broadcast를 전달하면, 이를 수신할 수 있는 모든 앱이 메시지를 전달받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그렇다면 악성 앱 또한 예외는 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;악성 앱이 Broadcast를 중간에 가로채는&amp;nbsp;&lt;b&gt;Message Hooking&lt;/b&gt;이 발생할 수 있는 문제가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;반대로&amp;nbsp; 순수한 Receiver에게 악성 Broadcast를 전달하는 &lt;b&gt;Message Facking&lt;/b&gt; 문제까지 동반된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 여타 이슈로 암시적 브로드캐스트를 manifest에 등록한 receiver로 수신하는 것이 크게 제한되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;보안 이슈&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위에서 언급했듯, Broadcast는 다른 프로세스와 통신하는 수단이면서도 보안을 취약하게 만드는 큰 원인 중 하나이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 문제를 해결하기 위해 아래와 같은 방법을 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 앱 내에서만 송수신하는 경우 &lt;b&gt;LocalBroadcastManager&lt;/b&gt; 사용을 적극 권장&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684251952706&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LocalBroadcastManager.getInstance(this).sendBroadcast(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;만약, 하나의 앱 내에서만 Broadcast를 송수신할 것이라면 LocalBroadcastManager 사용을 적극 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다른 앱에서 Broadcast를 수신할 수 있는 가능성을 아예 배제하여 고려사항을 최소화할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. &lt;b&gt;protectionLevel 속성 값을 signature&lt;/b&gt;로 설정한 임의의 &lt;b&gt;권한&lt;/b&gt;을 만들어서, Receiver나 Sender에 지정함.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1684252043106&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;permission
    android:name=&quot;SENDER_PERMISSION_NAME&quot;
    android:protectionLevel=&quot;signature&quot; /&amp;gt;

&amp;lt;uses-permission android:name=&quot;SENDER_PERMISSION_NAME&quot; /&amp;gt;

...

&amp;lt;receiver
    android:name=&quot;.MyBroadcastReceiver&quot;
    android:exported=&quot;true&quot;
    android:permission=&quot;SENDER_PERMISSION_NAME&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이렇게 설정하면, MyBroadcastReceiver는 SENDER_PERMISSION_NAME이라는 권한이 허용된 Broadcast만 수신하게 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여기서 protectionLevel을 signature로 설정하는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;signature&lt;/b&gt; 값은 &lt;b&gt;동일한&lt;/b&gt; &lt;b&gt;Key store&lt;/b&gt;를 가진 앱으로 권한을 제한한다. Key store는 앱마다 단 하나씩만 가질 수 있는 고유한 서명이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서  외부에서 명시적 Broadcast를 보낸다 하더라도, 해당 권한은 자신의 앱만 보유할 수 있으므로 최상의 보안을 가지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 외부로부터 Broadcast를 수신하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&amp;lt;receiver &amp;hellip; exported = false /&amp;gt; &lt;/b&gt;처럼 설정하면 외부로부터 Broadcast를 수신하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. &lt;b&gt;명시적 브로드캐스트(Explict Broadcast)&lt;/b&gt;를&amp;nbsp;사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>API 26</category>
      <category>broadcast</category>
      <category>Broadcast Receiver</category>
      <category>oreo</category>
      <category>보안 이슈</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/129</guid>
      <comments>https://itstory1592.tistory.com/129#entry129comment</comments>
      <pubDate>Wed, 17 May 2023 00:58:33 +0900</pubDate>
    </item>
    <item>
      <title>[Android] API 33 onBackPressed() deprecated</title>
      <link>https://itstory1592.tistory.com/128</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DRCci/btsd0SbkaDw/MtFISkMXd8iM08urE5xry1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DRCci/btsd0SbkaDw/MtFISkMXd8iM08urE5xry1/img.gif&quot; data-alt=&quot;'predictive back gesture'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DRCci/btsd0SbkaDw/MtFISkMXd8iM08urE5xry1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/DRCci/btsd0SbkaDw/MtFISkMXd8iM08urE5xry1/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;274&quot; height=&quot;531&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'predictive back gesture'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;Android API 33에서 개선된 &lt;b&gt;predictive back gesture&lt;/b&gt;이다.&lt;br&gt;이름 그대로 &lt;b&gt;예측 가능한 뒤로가기 동작&lt;/b&gt;이라는 의미이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이전 버전에서 back gesture를 지원할 때에는 단순히 화살표만 보여주었다.&lt;br&gt;그렇기 때문에 사용자들은 앱에서 &lt;b&gt;화면을 오른쪽으로 Swipe&lt;/b&gt;를 할 때에도 &lt;b&gt;실수로 앱이 닫히는 것을 예측하지 못하는 경우&lt;/b&gt;가 빈번했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이러한 이유로 Android 13에서는 &lt;u&gt;화면이 작아지는 애니메이션을 추가&lt;/u&gt;하여 사용자의 동작이 앱을 종료하고 있음을 &lt;b&gt;예측&lt;/b&gt;하도록 변경하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Android &lt;/span&gt;API 33부터 onBackPressed() 콜백이 deprecated 되었다.&lt;br&gt;&lt;b&gt;Back gesture&lt;/b&gt;는 특정 앱에서만 사용 가능한 것이 아니라, &lt;b&gt;안드로이드 시스템에서 지원하는 기능&lt;/b&gt;이다.&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;따라서, &lt;b&gt;Back gesture를 사용하면 시스템이 Back Action을 Handling&lt;/b&gt;하기 때문에, 위 GIF처럼&amp;nbsp;&lt;/span&gt;뒤로 가기 애니메이션과 함께 &lt;b&gt;앱이 작아지면서 종료&lt;/b&gt;된다.&lt;br&gt;즉, 안드로이드 시스템은 &lt;b&gt;뒤로가기 Event에 대해&lt;/b&gt; &lt;b&gt;시스템의 Handling을 따를 것인지&lt;/b&gt;, &lt;b&gt;앱에서 정의한 Handling을 따를 것&lt;/b&gt;인지 &lt;b&gt;알 수가 없다&lt;/b&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;예를 들어, 어떤 Fragment에서 WebView를 통해 설문조사 화면을 보여주고 있다고 가정해보자.&lt;br&gt;설문을 진행하다가 이전 설문 내역을 수정하고자 Back gesture를 했다면 어떻게 될까?&lt;br&gt;별도의 처리를 해주지 않았다면, 시스템 Handling을 따르기 때문에 아예 Fragment가 backstack에서 pop되어 화면이 종료될 것이다.&lt;br&gt;이런 Bad UX를 최소한으로 하기 위해, 시스템의 Back Action을 intercept하여 개발자의 의도대로 커스텀할 필요가 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 개발자가&lt;b&gt; Back Event을 intercept&lt;/b&gt;하여 여러 상황에 맞추어 직접 Handling 할 수 있도록 권장하고 있다.&lt;br&gt;안드로이드 공식 문서에 따르면, 이에 대응하기 위해&amp;nbsp;&lt;b&gt;OnBackInvokedCallback&amp;nbsp;&lt;/b&gt;또는&amp;nbsp;&lt;b&gt;OnBackPressedCallback&lt;/b&gt;을 사용하기를 권장한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;+ ) 뒤로가기 버튼을 누르던, 뒤로가기 제스쳐를 하던 동일하다. 다만 API 33 으로 업데이트되면서 사용자에게 뒤로가기에 대한 더 나은 UX를 제공해주고자, &lt;b&gt;predictive back gesture, 시스템 Back Event intercept&lt;/b&gt; 등을 지원하여 개발과 이용에 편리함을 제공하는 것 같다.&lt;br&gt;따라서 소개할 내용은 back gesture에만 한정된 내용이 아님을 밝힌다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;OnBackInvokedCallback&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;android library&lt;/b&gt;에서 지원한다.&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;API 33&lt;/b&gt;부터 사용 가능하다. (이전 버전 기기에서 실행하면 예외 발생)&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AndroidManifest의 Application 속성으로 &lt;a href=&quot;https://developer.android.com/guide/navigation/predictive-back-gesture?hl=ko#opt-predictive&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;android:enableOnBackInvokedCallback&lt;/a&gt; = {true | false} 설정함에 따라 호출 여부가 결정된다.&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Android 13에서 기본 값은 &lt;b&gt;false이며,&lt;/b&gt; &lt;b&gt;predictive back gesture&lt;/b&gt; 애니메이션 사용을 중지한다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Android 14부터 기본 값이 true이므로 따로 설정해주지 않으면 시스템의 Back gesture를 가로챈다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: left;&quot; href=&quot;https://developer.android.com/guide/navigation/predictive-back-gesture?hl=ko#opt-predictive&quot;&gt;android:enableOnBackInvokedCallback&lt;/a&gt;&amp;nbsp;속성은 당연히 API 33부터 적용되며, 그 이전 버전에서는 무시된다.&lt;/span&gt;&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;OnBackPressedCallback&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;&lt;li&gt;&lt;b&gt;androidX library&lt;/b&gt;에서 지원한다. (API 33 이전 버전도 사용 가능하다.)&lt;/li&gt;&lt;li&gt;android:enableOnBackInvokedCallback 설정 여부와 관계없이 호출된다.&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;android:enableOnBackInvokedCallback가 false이면&amp;nbsp;&lt;/span&gt;onBackPressed() 함께 호출된다.&lt;/li&gt;&lt;li&gt;Fragment에서 뒤로가기 처리를 할 때에도 사용된다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;만약 androidX를 사용하지 않는 프로젝트라면, onBackInvokedCallback을 사용할 수 있도록 API 33부터 제공한다.&lt;br&gt;중요한 것은 위 두 콜백과 관계없이,&amp;nbsp;&lt;span style=&quot;color: #0070D1;&quot;&gt;android:enableOnBackInvokedCallback&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;= {true | false}를 true로 설정하면 Back gesture를 시스템 Handling을 하지 않기 때문에 override한 onBackPressed()&lt;/span&gt;가 호출되지 않는다.&lt;br&gt;정리하자면, &lt;b&gt;시스템 Back Event를 가로채는 것은 &lt;u&gt;android:enableOnBackInvokedCallback&lt;/u&gt; 값으로 결정&lt;/b&gt;된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// In your build.gradle file:
dependencies {

// Add this in addition to your other dependencies
implementation &quot;androidx.activity:activity:1.6.0-alpha05&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;만약 AndroidX를 사용 중이고 OnBackPressedCallback을 사용할 것이라면 위 의존성을 추가해준다.&lt;br&gt;그렇지 않다면 의존성을 추가하지 않고 OnBackInvokedCallback을 사용한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;application
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;android:enableOnBackInvokedCallback=&quot;true&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;API 33을 지원한다면 위 속성을 추가해 주어야 한다.&lt;br&gt;해당 속성을 추가해주어야 &lt;u&gt;시스템의 Back Event를 가로채어 앱에서 다룰 수 있다&lt;/u&gt;.&lt;br&gt;OnBackInvokedCallback을 등록했더라도 위 속성값이 false이면, 기존 onBackPressed() 콜백이 호출된다. (disable이기 때문..)&lt;br&gt;Android 13에서는 기본값이 false이지만, Android 14부터 기본값이 true이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OnBackInvokedDispatcher&lt;/b&gt;&lt;/h3&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;if (BuildCompat.isAtLeastT()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onBackInvokedDispatcher.registerOnBackInvokedCallback(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OnBackInvokedDispatcher.PRIORITY_DEFAULT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Log.d(&quot;buna&quot;, &quot;OnBackInvokedCallback&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;API 33부터 지원하는 InvokedDispatcher는 위와 같은 코드로 등록할 수 있다.&lt;br&gt;&lt;u&gt;최소 &lt;b&gt;티라미수&lt;/b&gt; 버전 이상인 경우에만&lt;/u&gt; 콜백을 등록해야 한다.&lt;br&gt;그렇지 않으면 예외가 발생하여 앱이 종료된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;android:enableOnBackInvokedCallback를 true로 설정하고 OnBackInvokedDispatcher를 사용하면, onBackPressed()는 호출되지 않는다.&lt;br&gt;반대로 &lt;b&gt;속성값이 false라면 기존의 onBackPressed()가 호출&lt;/b&gt;될 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OnBackPressedCallback&lt;/b&gt;&lt;/h3&gt;&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        Log.d(&quot;buna&quot;, &quot;OnBackPressedCallback&quot;)
    }
})&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;AndroidX&lt;/b&gt;를 사용하고 있다면 API 33 이전이더라도 콜백을 등록할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;PressedDispatcher&lt;/b&gt;는 &lt;u&gt;android:enableOnBackInvokedCallback 속성과 무관하게 호출&lt;/u&gt;된다.&lt;br&gt;따라서 속성값이 false라도 OnBackPressedCallback(enabled)에 true를 전달하면 항상 호출된다.&lt;br&gt;OnBackInvokedDispatcher와 다르게 &lt;b&gt;기존 onBackPressed() 콜백이 함께 호출&lt;/b&gt;되므로 주의해야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또한, addCallback()의 첫 번째 인자로 &lt;b&gt;lifeCycleOwner&lt;/b&gt;를 넘겨주면 Dispatcher가 lifecycle을 알 수 있다.&lt;br&gt;따라서 lifecycle이 종료될 때, 자체적으로 &lt;b&gt;콜백을 해제&lt;/b&gt;해준다.&lt;br&gt;&lt;b&gt;메모리 누수를 방지&lt;/b&gt;하기 위해서 반드시 전달해주어야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val backPressCallback = object : OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        Log.d(&quot;buna&quot;, &quot;OnBackPressedCallback&quot;)
    }
}

...

backPressCallback.isEnabled = false // any if expression&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;경우에 따라 콜백을 비활성화해야 할 수도 있다.&lt;br&gt;그럴 때에는 callback의 isEnabled을 false로 지정하면 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Android</category>
      <category>onBackInvokedCallback</category>
      <category>onbackpressed()</category>
      <category>onbackpressed() deprecated</category>
      <category>onBackPressedCallback</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/128</guid>
      <comments>https://itstory1592.tistory.com/128#entry128comment</comments>
      <pubDate>Mon, 8 May 2023 01:33:52 +0900</pubDate>
    </item>
    <item>
      <title>[Android] PendingIntent 공식문서 파헤치기</title>
      <link>https://itstory1592.tistory.com/127</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OYypH/btsdxi7Tg4h/G1TMxIE5DIOkpw8LkTOCm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OYypH/btsdxi7Tg4h/G1TMxIE5DIOkpw8LkTOCm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OYypH/btsdxi7Tg4h/G1TMxIE5DIOkpw8LkTOCm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOYypH%2Fbtsdxi7Tg4h%2FG1TMxIE5DIOkpw8LkTOCm0%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;330&quot; height=&quot;288&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;해당 포스팅은 Android 공식문서를 읽고 정리한 내용을 바탕으로 작성하였습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://developer.android.com/reference/android/app/PendingIntent&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;PendingIntent &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b6aBVj/hyStL2oVsz/G7jrGr0Bza1xQfdMKQoJP0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot; data-og-url=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot; target=&quot;_blank&quot; data-source-url=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b6aBVj/hyStL2oVsz/G7jrGr0Bza1xQfdMKQoJP0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;PendingIntent &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;&lt;/p&gt;&lt;p class=&quot;og-host&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; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A description of an Intent and target action to perform with it. Instances of this class are created with &lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#getActivity(android.content.Context,%20int,%20android.content.Intent,%20int)&quot; target=&quot;_self&quot;&gt;&lt;span&gt;getActivity(Context, int, Intent, int)&lt;/span&gt;&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#getActivities(android.content.Context,%20int,%20android.content.Intent[],%20int)&quot; target=&quot;_self&quot;&gt;&lt;span&gt;getActivities(Context, int, Intent, int)&lt;/span&gt;&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#getBroadcast(android.content.Context,%20int,%20android.content.Intent,%20int)&quot; target=&quot;_self&quot;&gt;&lt;span&gt;getBroadcast(Context, int, Intent, int)&lt;/span&gt;&lt;/a&gt;, and&amp;nbsp;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#getService(android.content.Context,%20int,%20android.content.Intent,%20int)&quot; target=&quot;_self&quot;&gt;&lt;span&gt;getService(Context, int, Intent, int)&lt;/span&gt;&lt;/a&gt;; the returned object can be handed to other applications so that they can perform the action you described on your behalf at a later time.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;PendingIntent는 getActivity(), getActivities(), getBroadcast(), getService() 메서드를 통해 생성할 수 있다.&lt;br&gt;PendingIntent에 정의된 action(액티비티를 실행시켜라!, 서비스를 실행시켜라! 등)은 다른 애플리케이션에서 수행될 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;By giving a PendingIntent to another application, you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). As such, you should be careful about how you build the PendingIntent: almost always, for example, the base Intent you supply should have the component name explicitly set to one of your own components, to ensure it is ultimately sent there and nowhere else.&lt;/span&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;다른 애플리케이션에 PendingIntent를 전달하면, 마치 자신의 Intent인 것처럼 작업을 수행할 권한을 얻는다.&lt;br&gt;예를 들어, PendingIntent를 통해 다른 앱의 BroadcastReceiver로 intent를 전달할 수 있다.&lt;br&gt;반드시 자신의 애플리케이션이 아니어도 시스템이 유지하고 있는 PendingIntent를 다른 애플리케이션으로 전달 가능하다.&lt;br&gt;다른 앱에서 작업을 실행할 수 있는 만큼, 보안에 각별히 귀 기울여야 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call&amp;nbsp;&lt;br&gt;cancel()&lt;br&gt;&amp;nbsp;to remove it.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;PendingIntent는 시스템에서 유지하는 토큰에 대한 참조일 뿐이다. 자체적으로 무언가 할 수 있는 객체는 아니다.&lt;br&gt;중요한 것은 PendingIntent가 감싸고 있는 Intent에 정의된 여러 데이터 집합들이라는 것이다.&lt;br&gt;이 말은 즉슨, PendingIntent를 생성한 애플리케이션이 종료되어도, 시스템에서 이를 유지하기 때문에 다른 애플리케이션에서 해당 PendingIntent를 제공받아 작업을 할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;동일한 operation, action, data, categories, components, flag으로 정의된 intent를 보유한 PendingIntent는  같다.&lt;br&gt;따라서 해당 PendingIntent를 cancel() 메서드를 통해 제거할 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Because of this behavior, it is important to know when two Intents are considered to be the same for purposes of retrieving a PendingIntent. A common mistake people make is to create multiple PendingIntent objects with Intents that only vary in their &quot;extra&quot; contents, expecting to get a different PendingIntent each time. This does not happen. The parts of the Intent that are used for matching are the same ones defined by Intent.filterEquals. If you use two Intent objects that are equivalent as per Intent.filterEquals, then you will get the same PendingIntent for both of them.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val one = Intent(this, MainActivity::class.java)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.putExtra(&quot;key&quot;, &quot;Hello, One!&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)

val onePendingIntent = PendingIntent.getActivity(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this, 51, one,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PendingIntent.FLAG_IMMUTABLE
)

val other = Intent(this, MainActivity::class.java)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.putExtra(&quot;key&quot;, &quot;Hello, Other!&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)

val otherPendingIntent = PendingIntent.getActivity(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this, 51, other,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PendingIntent.FLAG_IMMUTABLE
)

onePendingIntent.cancel()
otherPendingIntent.send() // RuntimeException: Unable to start activity!&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;위 코드에서&amp;nbsp;&lt;b&gt;one&lt;/b&gt;과&amp;nbsp;&lt;b&gt;other&lt;/b&gt;&amp;nbsp;Intent는 서로 다른 객체이다.&lt;br&gt;따라서 이를 PendingIntent에 전달하면, 다른 PendingIntent를 반환받을 것 같지만&amp;nbsp;&lt;u&gt;그렇지 않다.&lt;/u&gt;&lt;br&gt;동일한 Request Code, intent.filterEquals()가 참인 Intent에 대해서는 동일한 PendingIntent를 반환받는다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가령, 위 코드처럼 putExtra()를 통해 다른 extra 데이터를 전달한다고 하더라도 intent.filterEquals()는 참을 반환한다.&lt;br&gt;뿐만 아니라, 동일한 Request Code로 PendingIntent를 생성하고 있으므로 onePendingIntent와 otherPendingIntent는 같다.&lt;br&gt;그런 이유로 otherPendingIntent.send()를 호출하면 취소된 PendingIntent를 실행시키려 하니 앱이 터지게 되는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;확실하게 이 둘이 같은 객체인지 알아보고 싶다면 아래 코드를 실행해 보라.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Log.d(&quot;buna&quot;, onePendingIntent.hashCode().toString())
Log.d(&quot;buna&quot;, otherPendingIntent.hashCode().toString())&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccMWeo/btsdsQYaqdX/sTgNl4INzuKrJfdTCqcUp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccMWeo/btsdsQYaqdX/sTgNl4INzuKrJfdTCqcUp0/img.png&quot; data-alt=&quot;'onePendingIntent와 otherPendingIntent의 hashCode'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccMWeo/btsdsQYaqdX/sTgNl4INzuKrJfdTCqcUp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccMWeo%2FbtsdsQYaqdX%2FsTgNl4INzuKrJfdTCqcUp0%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;580&quot; height=&quot;58&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'onePendingIntent와 otherPendingIntent의 hashCode'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;공교롭게도 둘은 같은 객체이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;추가로 알면 좋은 내용으로, &lt;b&gt;Request Code&lt;/b&gt;는 &lt;u&gt;PendingIntent를 구분하기 위한 고유 식별자&lt;/u&gt; 역할을 한다.&lt;br&gt;그렇기에 이 둘이 다르면, 다른 PendingIntent를 반환한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTmcWf/btsdhxSQ2Te/FMkBieey9xkDQbefZYBMs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTmcWf/btsdhxSQ2Te/FMkBieey9xkDQbefZYBMs1/img.png&quot; data-alt=&quot;'Request code가 다른 PendingIntent의 hashCode'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTmcWf/btsdhxSQ2Te/FMkBieey9xkDQbefZYBMs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTmcWf%2FbtsdhxSQ2Te%2FFMkBieey9xkDQbefZYBMs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;63&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'Request code가 다른 PendingIntent의 hashCode'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;또한, filterEquals() 메서드는 조금 생소할 수 있다.&lt;br&gt;이 메서드는 두 Intent가 같은 메서드인지 반환한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Log.d(&quot;buna&quot;, one.filterEquals(other).toString())
Log.d(&quot;buna&quot;, one.hashCode().toString())
Log.d(&quot;buna&quot;, other.hashCode().toString())&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bquqEq/btsdhAaZNQg/bYr1fRz7PZtFI7pV97k6Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bquqEq/btsdhAaZNQg/bYr1fRz7PZtFI7pV97k6Mk/img.png&quot; data-alt=&quot;'Extra data가 달라도 둘은 같은 Intent로 취급'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bquqEq/btsdhAaZNQg/bYr1fRz7PZtFI7pV97k6Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbquqEq%2FbtsdhAaZNQg%2FbYr1fRz7PZtFI7pV97k6Mk%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;627&quot; height=&quot;97&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'Extra data가 달라도 둘은 같은 Intent로 취급'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;결과를 보면 알 수 있다시피, 둘은 같은 Intent로 취급된다.&lt;br&gt;하지만 hashCode() 값이 알려주듯, 실제로는 같은 객체가 아니라는 사실에 주의해야 한다.&lt;br&gt;그저 동일한 PendingIntent를 반환할 것인지에 대한 척도로써 같은 Intent로 &lt;b&gt;취급&lt;/b&gt;한다는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8gfCt/btsdjoOSW3l/tk4Eu3ua4LkbVNZUQ6ICNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8gfCt/btsdjoOSW3l/tk4Eu3ua4LkbVNZUQ6ICNK/img.png&quot; data-alt=&quot;'Intent.filterEquals()'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8gfCt/btsdjoOSW3l/tk4Eu3ua4LkbVNZUQ6ICNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8gfCt%2FbtsdjoOSW3l%2Ftk4Eu3ua4LkbVNZUQ6ICNK%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;1990&quot; height=&quot;1076&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1076&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'Intent.filterEquals()'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;Intent.filterEquals() 메서드에 들어가 보면 비교 대상 코드가 가독성 높게 잘 작성되어 있다.&lt;br&gt;주의 깊게 살펴보아야 할 것은 Intent를 사용하면서 가장 자주 접하는 &lt;b&gt;Extra data, Flags&lt;/b&gt;은 비교 대상이 아니라는 점이다.&lt;br&gt;따라서 Notification 등을 보낼 때 Extra data만 다르게 하고 다른 PendingIntent를 반환받기 기대하면 안 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;There are two typical ways to deal with this.&lt;br&gt;If you truly need multiple distinct PendingIntent objects active at the same time (such as to use as two notifications that are both shown at the same time), then you will need to ensure there is something that is different about them to associate them with different PendingIntents. This may be any of the Intent attributes considered by Intent.filterEquals, or different request code integers supplied to getActivity(Context, int, Intent, int), getActivities(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), or getService(Context, int, Intent, int).&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;위와 동일한 내용이다.&lt;br&gt;정리하자면, PendingIntent를 비교하는 방식은 크게 2가지다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;Intent.filterEquals()&lt;/li&gt;&lt;li&gt;Request code&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;getActivity(), getActivities(), getBroadcast(), getService()에 따라서도 다른 PendingIntent를 반환한다.&lt;br&gt;또한, &lt;b&gt;&lt;u&gt;PendingIntent를 만들 때 전달하는 Flag에 따라서도 다른 PendingIntent를 반환하는 경우&lt;/u&gt;&lt;/b&gt;도 있다.&lt;br&gt;&lt;b&gt;e.g. PendingIntent.FLAG_IMMTUABLE과 PendingIntent.FLAG_MUTABLE 은 위 2가지 조건이 같은 경우에도, 다른 PendingIntent를 반환한다.&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If you only need one PendingIntent active at a time for any of the Intents you will use, then you can alternatively use the flags&amp;nbsp;FLAG_CANCEL_CURRENT&amp;nbsp;or&amp;nbsp;FLAG_UPDATE_CURRENT&amp;nbsp;to either cancel or modify whatever current PendingIntent is associated with the Intent you are supplying.&lt;br&gt;Also note that flags like&amp;nbsp;FLAG_ONE_SHOT&amp;nbsp;or&amp;nbsp;FLAG_IMMUTABLE&amp;nbsp;describe the PendingIntent instance and thus, are used to identify it. Any calls to retrieve or modify a PendingIntent created with these flags will also require these flags to be supplied in conjunction with others. E.g. To retrieve an existing PendingIntent created with FLAG_ONE_SHOT,&amp;nbsp;both&amp;nbsp;FLAG_ONE_SHOT and FLAG_NO_CREATE need to be supplied.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;br&gt;PendingIntent를 생성할 때 Flag를 적절히 적용하는 것이 중요하다.&lt;br&gt;정말 다양한 Flag가 존재하는데, 예를 들어, FLAG_CANCEL_CURRENT 또는 FLAG_UPDATE_CURRENT는 각각 기존 PendingIntent를 취소하고 새로 생성하거나, Extra 데이터를 업데이트하는 역할을 수행한다.&lt;br&gt;따라서 한 번에 하나의 PendingIntent만 active 하게 만들 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent.CanceledException&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.CanceledException&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;취소된 PendingIntent를 실행하고자 할 때 발생하는 Exception이다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent.OnFinished&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.OnFinished&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;전송이 올바르게 되었을 때 호출되는 Callback 인터페이스다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;fun&lt;/b&gt; &lt;b&gt;onSendFinished&lt;/b&gt;(PendingIntent pendingIntent, Intent intent, int resultCode, String resultData, Bundle resultExtras)&lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;pendingIntent&lt;/b&gt; : 전달된 PendingIntent&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;intent&lt;/b&gt; : PendingIntent가 감싸고 있던 Intent&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;resultCode&lt;/b&gt; : 전송에 의해 결정된 최종 결과 코드&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;resultData&lt;/b&gt; : Broadcast에 의해 수집된 마지막 ResultData&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;resultExtras&lt;/b&gt; : Broadcast에 의해 수집된 마지막 Extra 데이터&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#send()&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;PendingIntent.send()&lt;/span&gt;&lt;/a&gt; : PendingIntent의 Intent 작업을 실행할 수 있다.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#cancel()&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;PendingIntent.cancel()&lt;/span&gt;&lt;/a&gt; : 현재 활성화된 PendingIntent를 취소할 수 있다. PendingIntent를 소유한 원래 애플리케이션에서만 취소할 수 있다. 만약 취소 후에 send() 한다면 &lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent.CanceledException&quot; target=&quot;_self&quot;&gt;&lt;span&gt;PendingIntent.CanceledException&lt;/span&gt;&lt;/a&gt; 발생한다.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#getIntentSender()&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;PendingIntent.getIntentSender()&lt;/span&gt;&lt;/a&gt; : PendingIntent 발신자에 대한 정보를 가진다. PendingIntent를 생성한 애플리케이션의 package, UID 등 정보를 알 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Flags&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;PendingIntent를 생성할 때, 함께 전달하는 인자이다.&lt;br&gt;어떤 Flag를 지정하느냐에 따라 PendingIntent의 성격이 달라진다.&lt;br&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_UPDATE_CURRENT&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.FLAG_UPDATE_CURRENT&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시스템이 유지하고 있는 PendingIntent가 있다면, 해당 PendingIntent의 Extra data를 업데이트한다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_CANCEL_CURRENT&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntnet.FLAG_CANCEL_CURRENT&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시스템이 유지하고 있는 PendingIntent가 있다면 취소하고 다시 생성한다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기존 Intent의 Extra data를 업데이트하고 싶을 때 사용할 수 있다. (like PendingIntent.FLAG_UPDATE_CURRENT)&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_NO_CREATE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.FLAG_NO_CREATE&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시스템이 유지하고 있는 PendingIntent가 &lt;u&gt;없다면&lt;/u&gt;, 굳이 새롭게 생성하지 않는다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수신하는 곳에서 Intent가 null이다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_ONE_SHOT&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.FLAG_ONE_SHOT&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PendingIntent를 오직 한 번만 사용한다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;만약 동일한 PendingIntent에 대해 재호출(e.g. PendingIntent.send() 두 번 호출)하면 예외가 발생한다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_IMMUTABLE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.FLAG_IMMUTABLE&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;PendingIntent.send()&lt;/b&gt;에 intent를 전달해도 값을 채울 수 &lt;b&gt;&lt;u&gt;없다&lt;/u&gt;&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;공식문서에서는 해당 FLAG 사용을 강력히 권장한다. 그 이유는 다른 어플리케이션에서 PendingIntent의 Intent를 함부로 수정하지 못하게 함으로써 보안을 높일 수 있기 때문이다. &lt;b&gt;(신용 카드를 남에게 주고 대신 결제해주길 기대하는 상황과 같음.)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;PendingIntent 생성자(Creator)&lt;/b&gt;는 항상&lt;b&gt; PendingIntent.FLAG_UPDATE_CURRENT&lt;/b&gt;를 함께 사용하여 자체 PendingIntent를 변경할 수 있다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent#FLAG_MUTABLE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PendingIntent.FLAG_MUTABLE&lt;/a&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;PendingIntent.send()&lt;/b&gt;에 intent를 전달하여 채워지지 않은 Extra Data를 추가할 수 &lt;b&gt;&lt;u&gt;있다&lt;/u&gt;&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;API 31 이전에는 따로 설정하지 않으면 기본값이 MUTABLE이다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Notification의 &lt;b&gt;RemoteInput&lt;/b&gt;(알림에서 답장), &lt;b&gt;Bubble&lt;/b&gt;(도움말 풍선) 처럼 &lt;u&gt;사용자 입력 텍스트를 intent에 추가(= 수정)해야 하는 경우에만 사용&lt;/u&gt;해야 한다.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;For&amp;nbsp;security&amp;nbsp;reasons,&amp;nbsp;the&amp;nbsp;Intent&amp;nbsp;objects&amp;nbsp;you&amp;nbsp;supply&amp;nbsp;here&amp;nbsp;should&amp;nbsp;almost&amp;nbsp;always&amp;nbsp;be&amp;nbsp;explicit&amp;nbsp;intents,&amp;nbsp;that&amp;nbsp;is&amp;nbsp;specify&amp;nbsp;an&amp;nbsp;explicit&amp;nbsp;component&amp;nbsp;to&amp;nbsp;be&amp;nbsp;delivered&amp;nbsp;to&amp;nbsp;through&amp;nbsp;Intent.setClass&lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;보안상의 이유로, Intent.setClass() 를 통해 항상 명시적 인텐트를 지정해주어야 한다.&lt;/b&gt; (&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;자세한 이유는 보안 문제를 겪지 않아봐서 모르겠다. 추측으로는 send() 메서드는 채워지지 않은 값들을 채울 수 있기 때문에, 다른 Package와 Component를 악의적으로 채워넣어 악성 애플리케이션을 실행하는 시나리오로 이어질 수 있기 때문이라고 생각한다.)&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;+) FLAG_IMMUTABLE과 FLAG_MUTABLE은 함께 사용할 수 없다.&lt;br&gt;+) API 31+부터 둘 중 하나를 지정해주지 않으면 런타임 에러가 발생한다.&lt;br&gt;+) PendingIntent는 객체 생성과 동시에 시스템에 유지되기 시작한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;PendingIntent.send()는 일반적으로 직접 호출하지는 않는다.&lt;br&gt;해당 메서드는 PendingIntent가 감싸고 있는 Intent의 작업을 실행시키는 것인데, 바로 send() 해서 작업을 실행할 것이라면 굳이 PendingIntent로 만들 이유가 없기 때문이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그럼 해당 메서드는 누가 call() 하는가?&lt;br&gt;&lt;b&gt;Notification&lt;/b&gt;을 대표적인 예로 들 수 있다.&lt;br&gt;NotificationCompat.notify()를 호출하면 시스템은 알림을 표시하고, 사용자가 알림을 누르면 비로소 PendingIntent.send()를 호출하여 작업을 실행한다.&lt;br&gt;이 과정에서, 다른 Intent 값으로 변질되지 않도록 &lt;b&gt;FLAG_IMMUTABLE&lt;/b&gt;을 사용한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;PendingIntent 취약점에 관한 게시글&lt;/b&gt;&lt;br&gt;&lt;a href=&quot;https://support.google.com/faqs/answer/9267555?hl=ko&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;인텐트&amp;nbsp;리디렉션&amp;nbsp;취약점&amp;nbsp;해결&lt;/span&gt;&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://valsamaras.medium.com/pending-intents-a-pentesters-view-92f305960f03&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;PendingIntent 취약점에 관하여&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>flag</category>
      <category>pendingintent</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/127</guid>
      <comments>https://itstory1592.tistory.com/127#entry127comment</comments>
      <pubDate>Tue, 2 May 2023 01:44:53 +0900</pubDate>
    </item>
    <item>
      <title>[Android] RecyclerView Animation (LayoutAnimation, ItemAnimator)</title>
      <link>https://itstory1592.tistory.com/126</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rBaKV/btsda01eKr9/8QfF0kmJiv7bAXww4qnKn0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rBaKV/btsda01eKr9/8QfF0kmJiv7bAXww4qnKn0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rBaKV/btsda01eKr9/8QfF0kmJiv7bAXww4qnKn0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrBaKV%2Fbtsda01eKr9%2F8QfF0kmJiv7bAXww4qnKn0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;474&quot; height=&quot;474&quot; data-filename=&quot;unnamed.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView는 안드로이드 앱에서 많은 아이템들을 리스트로 보여줄 때 유용하게 사용되는 위젯 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView에 animation을 적용할 수 있는 방법은 크게 2가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;LayoutAnimation&lt;/li&gt;
&lt;li&gt;RecyclerView.ItemAnimatior&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;LayoutAnimation&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1682265391552&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RecyclerView extends ViewGroup implements ...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LayoutAnimation은 RecyclerView의 고유한 개념이 아닌, ViewGroup에 통용되는 속성이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RecyclerView 또한 ViewGroup을 상속하고 있으므로, LayoutAnimation 적용이 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LayoutAnimation을 적용하는 방법은 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. LayoutAnimation 속성으로 정의할 animation 집합을 xml로 작성한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682263832385&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:duration=&quot;500&quot;&amp;gt;

    &amp;lt;translate
        android:fromYDelta=&quot;20%&quot;
        android:interpolator=&quot;@android:anim/accelerate_decelerate_interpolator&quot;
        android:toYDelta=&quot;0&quot; /&amp;gt;

    &amp;lt;alpha
        android:fromAlpha=&quot;0&quot;
        android:interpolator=&quot;@android:anim/accelerate_decelerate_interpolator&quot;
        android:toAlpha=&quot;1&quot; /&amp;gt;

&amp;lt;/set&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;translate/&amp;gt;&lt;/b&gt;은 item을 이동시키는 애니메이션 효과이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fromYDelta를 20%로 설정했기 때문에 살짝 아래에서 위로 올라오는 애니메이션이 진행된다.&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;&amp;lt;alpha/&amp;gt;&lt;/b&gt;는 item의 투명도를 서서히 변화시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0부터 1로 변화하므로 위 translate 속성과 함께 사용하면, 서서히 위로 올라오면서 눈에 보이는 애니메이션이 진행될 것이다.&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;interpolator&lt;/b&gt;는 animation의 &lt;b&gt;보간&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;animation을 어떻게 동작시킬지 정해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@android:anim/accelerate_decelerate_interpolator는 애니메이션이 서서히 빨라지다가 다시 느려지는 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 다양한 interpolator가 존재하며, 적용하면 시각적으로 부드럽고 고급진 효과를 제공할 수 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 위에서 만든 animation 리소스를 속성으로 사용할 LayoutAnimation xml을 작성한다.&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;layoutAnimation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:animation=&quot;@anim/anim_fall_down&quot;
    android:animationOrder=&quot;random&quot;
    android:delay=&quot;100%&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LayoutAnimation은 일반적인 animation과 달리, &amp;lt;layoutAnimation/&amp;gt; 태그로 이루어진 xml에 속성으로 지정해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;android:animation&lt;/b&gt;에 1번 과정에서 만든 animation 리소스를 등록한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;android:animationOrder&lt;/b&gt;는 ViewGroup의 자식 View들에게 어떠한 순서로 애니메이션을 진행시킬지 정하는 속성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는&lt;b&gt; normal, reverse, random&lt;/b&gt; 3가지가 존재한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;normal&lt;/b&gt; : item에 순서대로 애니메이션을 적용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;reverse&lt;/b&gt; : item에 역순으로 애니메이션을 적용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;random&lt;/b&gt; : item에 랜덤 순서대로 애니메이션을 적용한다.&lt;/li&gt;
&lt;/ol&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;android:delay&lt;/b&gt;는 각 item이 애니메이션을 진행할 때의 시간차를 정해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연한 이야기지만, 값이 작을 수록 시간차가 적고, 값이 클수록 시간차가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. LayoutAnimation을 적용하고자 하는 ViewGroup에 android:layoutAnimation 속성으로 설정한다.&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;androidx.recyclerview.widget.RecyclerView
    android:id=&quot;@+id/movies_rv&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:layoutAnimation=&quot;@anim/layout_anim_fall_down&quot;
    app:layoutManager=&quot;androidx.recyclerview.widget.LinearLayoutManager&quot;
    tools:listitem=&quot;@layout/item_movie&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, ViewGroup에 &lt;b&gt;android:layoutAnimatoin&lt;/b&gt;을 통해 애니메이션을 적용해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세 과정만 거치면, RecyclerView.ItemAnimator에 비해 비교적 간단하게 커스텀 애니메이션을 적용할 수 있다.&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;추가로, notifyDatasetXxx() 등 adapter에게 데이터셋 변경을 알리는 함수 호출시 애니메이션이 제대로 동작하지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 &lt;u&gt;자식 View가 그려졌을 때, 애니메이션 1회 동작하도록 구현&lt;/u&gt;되어 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 현상을 해결하기 위해 &lt;b&gt;recyclerView.scheduleLayoutAnimation()&lt;/b&gt; 또는 &lt;b&gt;recyclerView.startLayoutAnimation()&lt;/b&gt;을 함께 호출해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startLayoutAnimation()은 즉시 자식 View에 대한 animation을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scheduleLayoutAnimation()은 바로 애니메이션을 자식 View에 변경이 있을 때, 비로소 animation을 진행한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RecyclerView.ItemAnimatior&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView.ItemAnimator는 RecyclerView의 3가지 주요 구성요소(Adapter, ItemAnimator, LayoutManager)중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템이 리스트에서 추가, 제거, 이동, 변경되는 애니메이션 효과를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 LayoutAnimation에 비해 구현이 어렵다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 아이템 변동에 대한 다양한 callback을 제공하기 때문에 상황에 따라 애니메이션을 다르게 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public abstract static class ItemAnimator {
...
public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
...
public class DefaultItemAnimator extends SimpleItemAnimator {


// RecyclerView.java
...
ItemAnimator mItemAnimator = new DefaultItemAnimator();
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView는 기본적으로 &lt;b&gt;DefaultItemAnimtor&lt;/b&gt; 를 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultItemAnimtor의 부모 자식 관계를 간단하게 정리하면 다음과 같다.&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;RecyclerView.ItemAnimator&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;|&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SimpleItemAnimator&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;|&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DefaultItemAnimator&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DefaultItemAnimator&lt;/b&gt;는 코드가 모두 구현되어 있기 때문에, 약간의 수정을 통해 커스터마이징할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;SimpleItemAnimator&lt;/b&gt;는 추상 클래스이므로 각 아이템 변동 상황에 따른 코드를 모두 구현해주어야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 간단하게 ItemAnimator를 RecyclerView에 어떻게 적용하는지 알아본다.&lt;/p&gt;
&lt;pre id=&quot;code_1682432778587&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
val itemAnimator = DefaultItemAnimator()
recyclerView.itemAnimator = itemAnimator&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecyclerView에는 setItemAnimator라는 setter 메서드를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 통해 간단히 Animator를 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 깊게 파고 들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;code&gt;public void setItemAnimator(@Nullable ItemAnimator animator) {
    if (mItemAnimator != null) {
        mItemAnimator.endAnimations();
        mItemAnimator.setListener(null);
    }
    mItemAnimator = animator;
    if (mItemAnimator != null) {
        mItemAnimator.setListener(mItemAnimatorListener);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 RecyclerView에 새로운 Animator를 적용한다면, RecyclerView.ItemAnimator의 endAnimations()를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 애니메이션이 모두 취소된다는 것을 고려할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DefaultItemAnimator&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1682665167597&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val itemAnimator = DefaultItemAnimator()
itemAnimator.addDuration = 500
recyclerView.setItemAnimator(itemAnimator)

.
.
.
// RecyclerView.kt
public void setAddDuration(long addDuration) {
    mAddDuration = addDuration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 각 Item의 애니메이션에 Duration을 정하고 싶다면 위와 같이 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kotlin에서는 프로퍼티로 변환되어 addDuration이라고 되어 있지만, 이는 Java에서는 명백히 setter임을 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Duration을 더해주는 것이 아니라 새롭게 초기화한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SimpleItemAnimator&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 알아본 DefaultItemAnimator는 위에서 언급했듯이 기본적으로 RecyclerView에 적용되어 있으며, 모든 구현이 되어있는 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultItemAnimator가 상속하고 있는 부모 클래스인 SimpleItemAnimator는 abstract class이기 때문에 거의 모든 콜백을 개발자가 직접 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682665623322&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomAnimator : SimpleItemAnimator() {

    // Animation 실행 대기 중인 ViewHolder 리스트
    private val animViewHolders: MutableList&amp;lt;RecyclerView.ViewHolder&amp;gt; = mutableListOf()


    override fun runPendingAnimations() {
        // Animation queue에 대기 중인 Animation을 진행하기 위해 call
    }

    override fun endAnimation(item: RecyclerView.ViewHolder) {
        // 애니메이션을 종료해야 할 때 call
        item.itemView.animate().cancel()
    }

    override fun endAnimations() {
		// 모든 애니메이션을 종료해야 할 때 call
        // ex) setItemAnimator()를 할 때 모든 애니메이션을 종료하기 위해 호출된다.
        animViewHolders.forEach { it.itemView.animate().cancel() }
    }

    override fun isRunning(): Boolean {
        // 현재 실행중인 Animation이 있는가
        return addAnimViewHolders.isNotEmpty() &amp;amp;&amp;amp; moveAnimViewHolders.isNotEmpty()
    }

    override fun animateAdd(holder: RecyclerView.ViewHolder?): Boolean {
        // notifyItemInseted() 호출시 Animation
        addAnimViewHolders.add(holder)
    }
    
    override fun animateRemove(holder: RecyclerView.ViewHolder?): Boolean {
        // notifyItemRemoved() 호출시 Animation
    }

    override fun animateMove(
        holder: RecyclerView.ViewHolder?,
        fromX: Int,
        fromY: Int,
        toX: Int,
        toY: Int,
    ): Boolean {
        // notifyItemMoved() 호출시 Animation
        animViewHolders.add(holder)
    }
    
     override fun animateChange(
        oldHolder: RecyclerView.ViewHolder?,
        newHolder: RecyclerView.ViewHolder?,
        fromLeft: Int,
        fromTop: Int,
        toLeft: Int,
        toTop: Int,
    ): Boolean {
        // notifyItemChanged() 호출시 call
    }
}&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;현재 animation 실행을 대기하고 있는 ViewHolder를 관리하기 위해 MutableList를 만들어주었다.&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;runPendingAnimations()&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ItemAnimator#animateAppearance(androidx.recyclerview.widget.RecyclerView.ViewHolder,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo)&quot;&gt;animateAppearance()&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ItemAnimator#animateChange(androidx.recyclerview.widget.RecyclerView.ViewHolder,androidx.recyclerview.widget.RecyclerView.ViewHolder,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo)&quot;&gt;animateChange(), &lt;/a&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ItemAnimator#animatePersistence(androidx.recyclerview.widget.RecyclerView.ViewHolder,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo)&quot;&gt;animatePersistence(),&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ItemAnimator#animateDisappearance(androidx.recyclerview.widget.RecyclerView.ViewHolder,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo,androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo)&quot;&gt;animateDisappearance()&lt;/a&gt; 의 반환 값에 따라 호출이 결정된다. 반환값이 true인 경우 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;runPendingAnimations() will be scheduled to be run on the next frame.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 세트가 변경되고 RecyclerView가 다시 레이아웃을 그린 후, runPendingAnimations() 메서드가 호출되어 ItemAnimator에 대한 애니메이션을 실행한다.&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;endAnimation(item: RecyclerView.ViewHolder)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 ViewHolder의 animaiton을 종료해야 할 때 호출되는 endAnimation()에서는 viewHolder가 가지고 있는 view의 애니메이션을 cancel() 해주어야 한다.&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;endAnimations()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 ViewHolder의 애니메이션을 종료해야 하는 경우 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) RecyclerView.setItemAniamtor()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때에는, 인자로 ViewHolder가 따로 주어지지 않기 때문에 별도로 관리하고 있는 ViewHolder 리스트에서 각각 종료해주어야 한다.&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;isRunning()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실행 중인 Animation이 있는지를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ViewHolder를 담고 있는 리스트가 비어있지 않다는 것은 곧 실행해야 할 Animation이 있다는 것이기 때문에 리스트가 비었는지 여부를 반환해준다.&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;animateChange(), animateRemove(), animateAdd(), animateMove()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Adapter의 notifyItemXxx() 메서드 호출시 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션 호출시, 관리를 위해 ViewHolder List에 Item을 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 notifyItemXxx() 메서드가 호출될 때 상황에 적절한 Animation을 커스터마이징하여 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1682667102605&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;holder.itemView.animate().translationY(20F)...&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;animateXxx() 메서드의 반환 타입은 Boolean이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 따르면 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;반환값은 애니메이션이 설정되었는지 여부와 다음 기회에 ItemAnimator의 runPendingAnimations() 메서드를 호출해야 하는지 여부를 나타냅니다. 이 메커니즘을 통해 ItemAnimator는 개별 애니메이션을 설정할 수 있으며, animateAdd(), animateMove(), animateRemove(), animateChange()에 대한 호출이 하나씩 들어올 때 개별 애니메이션을 설정한 다음 나중에 runPendingAnimations()를 호출할 때 애니메이션을 함께 시작할 수 있습니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;code&gt;public final void dispatchAddFinished(RecyclerView.ViewHolder item) {
    onAddFinished(item);
    dispatchAnimationFinished(item);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구현시 전체적으로 중요한 점은 Animation 시작 종료시에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;dispatchXxxStarting(),&lt;/b&gt;&lt;/span&gt;&lt;b&gt; dispatchXxxFinished()&lt;/b&gt; 을 적절히 호출해주어야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682670650897&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!isRunning) {
	dispatchAnimationsFinished()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Animation 리스너를 등록하여 모든 Animation들이 끝났는지 확인하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;dispatchAnimationsFinished()&lt;/b&gt;를 호출한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;dispatch prefix 메서드를 제대로 호출해주지 않으면 Animation이 꼬이거나, 잔상이 남는 경우가 발생할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LayoutAnimation에 비해 구현시 고려해야할 지식이 훨씬 많다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그만큼 각 상황에 대해 더 유연하게 커스터마이징 할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>DefaultItemAnimator</category>
      <category>ItemAnimator</category>
      <category>LayoutAnimation</category>
      <category>RecyclerView</category>
      <category>SimpleItemAnimator</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/126</guid>
      <comments>https://itstory1592.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 28 Apr 2023 17:33:49 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Listview vs RecyclerView</title>
      <link>https://itstory1592.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안드로이드에서 아이템을 순차적으로 나열하기 위해서 사용할 수 있는 View는 2종류가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. ListView&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. RecyclerView&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 둘은 모두 아이템을 화면에 연속적으로 보여주며, 다양한 ViewType을 정의해줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 몇가지 핵심적인 차이가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;RecyclerView와&amp;nbsp; &lt;b&gt;ListView&lt;/b&gt;의 차이점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 181px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; text-align: center; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;RecyclerView&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;b&gt;ListView&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ViewHolder&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ViewHolder 패턴을 강제한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ViewHolder 패턴이 선택 사항이다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Scroll &amp;amp; Layout&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;수직, 수평 스크롤을 지원한다.&lt;/span&gt;&lt;br /&gt;Linear, Grid, StaggeredGrid와 같이 아이템 배치를 다양하게 지원한다.&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;수직 스크롤만 지원한다.&lt;/span&gt;&lt;br /&gt;Linear(Vertical) 배치만 가능하다.&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Click Detection&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;별도의 클릭 처리 interface를 제공하지 않는다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AdapterView.OnItemClickListener 인터페이스를 제공한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Decoration&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;RecyclerView.addItemDecoration() 인자로 RecyclerView.ItemDecoration 클래스의 인스턴스를 전달하여 적용한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;android:divider, android:dividerHeight 속성을 통해 구분선을 적용한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Animation&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Item 추가 / 삭제 Animation을 적용할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Item 추가 / 삭제 Animation을 적용하기 어렵다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 25.5426%; height: 57px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Speed&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.124%; height: 57px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Item 개수가 많을 때 빠르다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Item 추가 / 삭제가 빠르다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(notifyItemRangeInserted(), notifyItemRangeChanged(), ListAdapter 등 다양한 갱신 방법 지원)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 57px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Item 개수가 적을 때 빠르다.&lt;br /&gt;(단, convertView와 ViewHolder 패턴을 적절히 사용하면 RecyclerView 속도에 준할 수 있다.)&lt;br /&gt;Item 추가 / 삭제가 느리다.&lt;br /&gt;(오직 notifyDatasetChanged() 지원)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. ViewHolder&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1681964538465&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract static class Adapter&amp;lt;VH extends ViewHolder&amp;gt; {
    ...
    
    @NonNull
    public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
        
    public abstract void onBindViewHolder(@NonNull VH holder, int position);
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;RecyclerView&lt;/b&gt;는 ListView와 다르게 &lt;u&gt;ViewHolder 패턴을 강제&lt;/u&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 RecyclerView.Adapter에서는 아래와 같은 추상 메서드 구현이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;onCreateViewHolder()는 RecyclerView를 통해 사용자에게 한 번에 보여지는 item의 2 ~ 4개를 더한만큼만 호출된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터가 10,000개, 100,000개라 할지라도 onCreateViewHolder()는 같은 화면 크기에 대해 호출 횟수가 고정되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;다시 말해, RecyclerView는 동일한 View를 재사용하고, 새로운 item이 보여질 때마다 onBindViewHolder()를 호출하여 같은 View에 데이터만 갈아끼우는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;따라서 동일한 View에 대해 매번 자식 View를 조회하는 것은 불필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;뿐만 아니라,&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;findViewById()&lt;/b&gt;는 주어진 id를 바탕으로 유효한 id인지 검증하고, 유효하다면 &lt;b&gt;findViewTraversal()&lt;/b&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;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;findViewTraversal()은 일반적인 View라면, 자신의 id와 동일한지 비교하고 종료된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그러나 ViewGroup의 경우, 모든 자식 View를 순회하기 때문이 cost가 높다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이것이 RecyclerView가 ViewHolder 패턴을 강제하는 이유이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 번 조회한 View에 대해 ViewHolder로 감싸서 관리하면, 동일한 View를 재사용할 때 매번 View를 조회할 필요가 없다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면, ListView는 ViewHolder 패턴을 강제하지는 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일례로 BaseAdapter의 최상위 interface인 &lt;b&gt;Adapter&lt;/b&gt;의 코드는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681966740326&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Adapter {
    ...
    
    View getView(int position, View convertView, ViewGroup parent);

    ...    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ListView도 RecyclerView와 동일하게 한 번 생성한 view를 재사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;이러한 view를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;convertView&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이미 view가 생성된 적이 있다면 껍데기 view에 데이터만 갈아 끼워주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681967264404&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SuppressLint(&quot;ViewHolder&quot;)
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {  
    val view = View.inflate(parent?.context, R.layout.item_movie, null)

    ...    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;getView() 메서드를 구현한  위 코드는&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;ViewHolder&lt;/b&gt;&lt;/u&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;b&gt;warning&lt;/b&gt;이 발생한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;convertView가 이미 생성되었다면 null이 아닐 것이고, 새롭게 View를 inflate 할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20f6e38b4d154210b5c7b2990cd760b1.jpg&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpkg4c/btsbAO83WB5/J8W02wGEujF9ipKqbeyJF1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpkg4c/btsbAO83WB5/J8W02wGEujF9ipKqbeyJF1/img.jpg&quot; data-alt=&quot;'ListView의 ConvertView를 두 번 초기화할 필요 없다'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpkg4c/btsbAO83WB5/J8W02wGEujF9ipKqbeyJF1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpkg4c%2FbtsbAO83WB5%2FJ8W02wGEujF9ipKqbeyJF1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;547&quot; height=&quot;333&quot; data-filename=&quot;20f6e38b4d154210b5c7b2990cd760b1.jpg&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'ListView의 ConvertView를 두 번 초기화할 필요 없다'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한, ListView의 Adapter는 onBindViewHolder()와 같이 데이터를 bind하는 메서드가 따로 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;view 생성과 data bind를 getView()라는 메서드 하나에서 처리해야 한다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681971449502&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Adapter
class MyListAdapter(...) : BaseAdapter() {
    ...
    
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        var view: View? = convertView
        val viewHolder: ViewHolder

        if (view == null) {
            val view = View.inflate(parent?.context, R.layout.item_movie, null)
            viewHolder = ViewHolder(view)
            view.tag = viewHolder
        } else {
            viewHolder = view.tag as ViewHolder
        }

        viewHolder.bind(...)
        return view
    }
    
    ...
}

// ViewHolder
private class ViewHolder(view: View) {
    private val nameTextView: TextView = view.findViewById(...)
    private val phoneNumberTextView: TextView = view.findViewById(...)
    ...

    fun bind(...) {
    	nameTextView.text = ...
    	phoneNumberTextView.text = ...
        
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;getView() 메서드가 호출될 때, convertView가 null인 경우 새롭게 View 객체를 초기화해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(만약 이미 초기화된 View라면 view의 tag에 저장한 ViewHolder를 가져오기만 하면 된다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;새롭게 초기화한 View 객체를 ViewHolder로 넘겨서 필요한 자식 View를 초기화하여 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이러한 과정을 통해 ListView도 RecyclerView처럼 findViewById()를 한 번만 호출해주도록 구현할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다만, 이러한 과정이 선택 사항이므로 개발자가 구현하는 과정에서 빠뜨릴 수도 있다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 모든 과정을 getView() 메서드에서 처리해야 하므로 가독성이 떨어질 수 있으며, 일일이 convertView의 null 체크와 ViewHolder를 직접 별도로 관리해야 한다는 점에서 많은 불편함을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. Scroll &amp;amp; Layout&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ListView&lt;/b&gt;는 &lt;b&gt;수직 스크롤&lt;/b&gt;만 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일반적인 방법으로는 수평 스크롤링을 구현할 수 없다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면, &lt;b&gt;RecyclerView&lt;/b&gt;는 Vertical,&amp;nbsp;Horizontal&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;u&gt;2가지&lt;/u&gt;를 선택할 수 있으며 각각&amp;nbsp;&lt;/span&gt;&lt;b&gt;수직, 수평 스크롤&lt;/b&gt;을 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 ListView와 다르게 item을 다양하게 배치시킬 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;RecyclerView의 LayoutManager에 따라 item 배치를 다양하게 정의할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rhd3r/btsbAsrJHJP/tSsRXmggZH6pjeDjJojcGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rhd3r/btsbAsrJHJP/tSsRXmggZH6pjeDjJojcGK/img.png&quot; data-alt=&quot;'https://blog.hexabrain.net/363'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rhd3r/btsbAsrJHJP/tSsRXmggZH6pjeDjJojcGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frhd3r%2FbtsbAsrJHJP%2FtSsRXmggZH6pjeDjJojcGK%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;593&quot; height=&quot;305&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'https://blog.hexabrain.net/363'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;LinearLayout&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;View를 일직선으로 배치한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;GridLayout&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;View를 격자 형태로 배치한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마치 TableLayout과 비슷하며 Row, Column의 수를 설정하여 원하는 형태로 보여줄 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;StaggeredGridLayout&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;GridLayout과 유사하지만, View의 높이를 불규칙하게 나타낼 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3. Click Detection&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1681973954221&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ListView(this).setOnItemClickListener { adapterView, itemView, position, id -&amp;gt; 
    val item = adapterView.getItemAtPosition(position)
    
    // logic for clicking itemView
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ListView&lt;/b&gt;는 AdapterView.OnItemClickListener 인터페이스를 구현하여 View가 클릭되었을 때에 대한 처리를 수행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 &lt;b&gt;RecyclerView&lt;/b&gt;는 별도로 Item 클릭에 대한 인터페이스를 제공하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 View.OnClickListener를 onCreateViewHolder() 메서드가 호출될 때 View에 직접 등록해주어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(만약 View 클릭에 대해 테스트 가능한 구조를 원한다면, 외부에서 Click 로직을 주입받는 방식을 택한다.)&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4. Decoration&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;RecyclerView&lt;/b&gt;는 각 Item에&amp;nbsp; 장식을 추가하기 위해 &lt;u&gt;RecyclerView.addItemDecoration()&lt;/u&gt;을 호출해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ItemDecoration은 추상 클래스이므로 원하는 형태로 커스터마이징이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;만약 단순히 구분선을 추가하고 싶다면, 이미 구현되어 있는 &lt;b&gt;DividerItemDecoration &lt;/b&gt;인스턴스를 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ListView&lt;/b&gt;는 Item에 구분선을 추가하기 위해, &lt;b&gt;android:divider &lt;/b&gt;및&amp;nbsp;&lt;b&gt;android:dividerHeight&lt;/b&gt;를 설정해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;divider는 drawable을 전달받으며, dividerHeight은 구분선의 높이를 지정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;동적으로 decoration을 추가하는 RecyclerView와 달리 xml의 속성만으로도 설정이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;5. Animation&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ListView&lt;/b&gt;는 Animation을 추가하는 것이 가능하나 매우 어렵고 무거운 작업이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 View에 대해 AnimationUtils.xxx 등을 통해 직접 등록해주어야 하며 Item의 추가/ 삭제에 대한 상황에 Animation이 동작하도록&amp;nbsp; 별도로 구현해주어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면, &lt;b&gt;RecyclerView&lt;/b&gt;는 아이템 추가 / 삭제시 비교적 쉽게 Animation 적용이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;android:layoutAnimation&lt;/b&gt;에 animation 리소스에 해당하는 xml 파일을 전달해주기만 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;참고로, &lt;b&gt;LayoutAnimation&lt;/b&gt;은 &lt;u&gt;RecyclerView에만 적용되는 개념이 아니다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LayoutAnimation은 ViewGroup의 자식 View에게 일괄적으로 animation을 적용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기본적으로 자식 View들이 모두 그려졌을 때 1회 호출되며, 재실행이 불가능하다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; + ) &lt;b&gt;notifyDatasetChanged()&lt;/b&gt;와 같이 RecyclerView의 Item들을 업데이트 하는 경우, 이미 View에 대해 animation 효과를 처리하였으므로 재실행 되지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 다음 코드로 문제를 해결할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681978260318&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;adapter.notifyDatasetChanged()
recyclerView.scheduleLayoutAnimation()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;ViewGroup의 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/view/ViewGroup#scheduleLayoutAnimation()&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;b&gt;scheduleLayoutAnimation()&lt;/b&gt;&lt;/u&gt;&lt;/a&gt; 를 호출하여, ViewGroup의 contents가 업데이트 될 때 동일한 View에 대해서도 애니메이션이 재동작하도록 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Schedules the layout animation to be played after the next layout pass of this view group. This can be used to restart the layout animation when the content of the view group changes or when the activity is paused and resumed.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>ANIMATION</category>
      <category>Click Detection</category>
      <category>Decorator</category>
      <category>Layout</category>
      <category>ListView</category>
      <category>ListView vs RecyclerView</category>
      <category>RecyclerView</category>
      <category>ViewHolder</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/125</guid>
      <comments>https://itstory1592.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 20 Apr 2023 17:25:04 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Jvm Prefix Annotation 5가지 파헤치기</title>
      <link>https://itstory1592.tistory.com/124</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3bbebda874e6003b.png&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTfTNs/btr79e5IZrA/rqkh8kHAapxRjK6KvBASP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTfTNs/btr79e5IZrA/rqkh8kHAapxRjK6KvBASP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTfTNs/btr79e5IZrA/rqkh8kHAapxRjK6KvBASP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTfTNs%2Fbtr79e5IZrA%2Frqkh8kHAapxRjK6KvBASP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;195&quot; data-filename=&quot;3bbebda874e6003b.png&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;코틀린은 자바와의 상호 운용을 중요하시하는 언어이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 JVM 상에서 두 언어의 유연함을 가지기 위해 &lt;b&gt;Jvm Prefix Annotation&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;주로 사용하는 5가지 Annotation에 대해 알아보자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JvmField&lt;/li&gt;
&lt;li&gt;JvmStatic&lt;/li&gt;
&lt;li&gt;JvmOverloads&lt;/li&gt;
&lt;li&gt;JvmName&lt;/li&gt;
&lt;li&gt;JvmSynthetic&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. JvmField&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;코틀린에는 프로퍼티라는 개념이 존재하지만, 자바에서는 필드라는 개념이 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 자동으로 getter/setter를 만들어주지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 코틀린의 프로퍼티를 자바에서 필드처럼 사용하고 싶다면 &lt;b&gt;@JvmField&lt;/b&gt; 어노테이션을 사용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680688592312&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    @JvmField
    var name: String = &quot;John Doe&quot;
}

// 자바 코드에서 호출
Person person = new Person();
person.name = &quot;Alice&quot;;
String name = person.name;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;@JvmField 어노테이션을 붙여주지 않으면, 자바에서는 getName()과 setName() 메서드를 통해 name에 접근할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. JvmStatic&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@JvmStatic&lt;/b&gt; 어노테이션은 마치 자바의 static 메서드처럼 사용할 수 있게 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1680688865212&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Calculator {
    companion object {
        @JvmStatic
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}

// 자바 코드에서 호출
int result = Calculator.add(1, 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;@JvmStatic 어노테이션을 붙여주지 않으면, Calculator.Companion.add() 형식으로 호출해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-05 오후 7.03.15.png&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFrE1C/btr8ayP7POQ/PMDICGtdiNdo13qfPLW42K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFrE1C/btr8ayP7POQ/PMDICGtdiNdo13qfPLW42K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFrE1C/btr8ayP7POQ/PMDICGtdiNdo13qfPLW42K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFrE1C%2Fbtr8ayP7POQ%2FPMDICGtdiNdo13qfPLW42K%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;1844&quot; height=&quot;1048&quot; data-filename=&quot;스크린샷 2023-04-05 오후 7.03.15.png&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;디컴파일 결과, @JvmStatic 어노테이션이 붙은 메서드를 정적으로 만들어 Companion의 add() 메서드를 참조하도록 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 외부에서는 굳이 Companion에 접근할 필요가 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. JvmOverloads&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Java에는 불행하게도 파라미터에 default값을 설정할 수가 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;반면, Kotlin은 개발자의 편리함과 유연성을 위해 파라미터의 default 값 설정을 지원한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;자바와의 상호 운용을 위해 &lt;b&gt;@JvmOverloads&lt;/b&gt; 어노테이션을 사용하면, 자바에서도 마치 default값이 있는 메서드처럼 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이는&lt;u&gt; 컴파일러가 자동으로 오버로딩&lt;/u&gt; 해주기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680689271272&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    @JvmOverloads
    fun greet(name: String = &quot;World&quot;) {
        println(&quot;Hello, $name!&quot;)
    }
}

// Kotlin에서 호출
val person = Person()
person.greet() // &quot;Hello, World!&quot; 출력
person.greet(&quot;Alice&quot;) // &quot;Hello, Alice!&quot; 출력

// 자바 코드에서 호출
Person person = new Person();
person.greet(); // &quot;Hello, World!&quot; 출력
person.greet(&quot;Alice&quot;); // &quot;Hello, Alice!&quot; 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Java에서도 Kotlin처럼 greet() 메서드를 동일한 조건으로 호출할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-05 오후 7.09.31.png&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUoYZ3/btr8jNE4hp7/uKw3aOaURF2ZfFdJwVkKa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUoYZ3/btr8jNE4hp7/uKw3aOaURF2ZfFdJwVkKa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUoYZ3/btr8jNE4hp7/uKw3aOaURF2ZfFdJwVkKa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUoYZ3%2Fbtr8jNE4hp7%2FuKw3aOaURF2ZfFdJwVkKa0%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;1718&quot; height=&quot;1080&quot; data-filename=&quot;스크린샷 2023-04-05 오후 7.09.31.png&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;디컴파일 결과 name 파라미터를 입력받는 greet(String name) 메서드가 정의되어 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 맨 아래 아무 인자도 없는 greet() 메서드가 정의되어 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;자세히 살펴보면, 중간에 synthetic method가 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;default값으로 설정한 &quot;World&quot;를 내부에서 따로 초기화하고, Person의 greet(String name) 메서드에 해당 인자를 넘겨 호출한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;내부적으로는 이러한 과정을 거치기에, Java에서 마치 default 값이 설정되어 있는 메서드처럼 호출할 수 있는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@JvmName&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@JvmName&lt;/b&gt;은 Kotlin의&lt;b&gt; getter/setter를 포함한 함수, .kt 파일명&lt;/b&gt;을 자바에서 어떻게 사용할 것인지 정의할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1680689761054&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    @JvmName(&quot;greetWithName&quot;)
    fun greet(name: String) {
        println(&quot;Hello, $name!&quot;)
    }
}

// 자바 코드에서 호출
Person person = new Person();
person.greetWithName(&quot;Alice&quot;); // &quot;Hello, Alice!&quot; 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Person 클래스의 greet() 메서드에 @JvmName 어노테이션을 설정해 주었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Java에서는 greet()이 아닌, greetWithName() 이라는 이름으로 메서드를 호출할 수 있게 변경되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680689879420&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FILE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmName(actual val name: String)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 외에도 JvmName의 Target을 살펴보면 함수, 프로퍼티의 getter/setter, File에 대해서 적용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 아래와 같은 코드도 가능하다는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680690011667&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    @get:JvmName(&quot;hisName&quot;)
    val name: String = &quot;BuNa&quot;
}

// 자바 코드에서 호출
Person person = new Person();
person.hisName();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;사용 지점 대상&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;(use-site target)으로 file과 getter의 이름을 변경해 주었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;그 결과 자바에서는 &lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;hisName이라는 이름으로&amp;nbsp;&lt;/span&gt;name 프로퍼티에 접근할 수 있게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;또한, &lt;b&gt;@file:JvmName&lt;/b&gt;은 .kt 파일의 이름을 변경할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;Kotlin의 최상위 함수(top-level function)를 Java에서 접근하는 경우에 대비해서, 컴파일러는 파일명을 클래스명으로 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: left;&quot;&gt;하지만 이런 방식이 마음에 들지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680690533693&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Test.kt
@file:JvmName(&quot;Greet&quot;)

fun hello() {
    println(&quot;hello&quot;)
}

// 자바 코드에서 호출
// @JvmName 적용 전
new TestKt().hello();

// @JvmName 적용 후
new Greet().hello();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 위와 같은 방식으로 변경할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;@file:JvmName(&quot;Greet&quot;) 어노테이션을 적용함으로써, Test.kt 파일명에 대한 클래스명을 Greet으로 변경하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;TestKt라는 클래스명보다 Greet이라는 클래스명이 더 낫다는 것은 누구나 동의할 것이라고 생각한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@JvmSynthetic&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin에는 Java와 달리, internal 접근 제한자가 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;internal 접근 제한자는 다른 모듈에서 접근하지 못하도록 제한하는 기능을 제공한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Java에는 internal 접근 제한자가 없기 때문에 다른 모듈에서의 접근을 어떻게 제한해야 할지 고민할 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;@JvmSynthetic&lt;/b&gt; 어노테이션을 사용하면 Java에서는 접근하지 못하도록 설정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680690946586&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Secret {
    @JvmSynthetic
    fun print() {
        println(&quot;This is a secret!&quot;)
    }
}

// 코틀린에서 호출
val secret = Secret()
secret.print() // &quot;This is a secret!&quot; 출력

// 자바 코드에서 호출
Secret secret = new Secret();
secret.print(); // 컴파일 에러 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 사용 사례를 보여주기 위함이지, 반드시 internal 접근제한자에 대해서만 적용할 수 있는 것이 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;필요에 따라 Java에서는 접근하지 못하도록 설정할 수&amp;nbsp;있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;해당 포스팅 내용에 오류가 있다면 댓글로 남겨주세요 :)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;github :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/tmdgh1592&quot;&gt;https://github.com/tmdgh1592&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676106517811&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;tmdgh1592 - Overview&quot; data-og-description=&quot;느리더라도 천천히..!!  . tmdgh1592 has 21 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/tmdgh1592&quot; data-og-url=&quot;https://github.com/tmdgh1592&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HBaQz/hyPMP78czM/TUoi1aa26rKKRyNQM34GjK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460&quot;&gt;&lt;a href=&quot;https://github.com/tmdgh1592&quot; data-source-url=&quot;https://github.com/tmdgh1592&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HBaQz/hyPMP78czM/TUoi1aa26rKKRyNQM34GjK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460');&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;tmdgh1592 - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;느리더라도 천천히..!!  . tmdgh1592 has 21 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/Kotlin</category>
      <category>@JvmField</category>
      <category>@JvmName</category>
      <category>@JvmOverloads</category>
      <category>@JvmStatic</category>
      <category>@JvmSynthetic</category>
      <category>Jvm Prefix annotation</category>
      <author>BuNa_</author>
      <guid isPermaLink="true">https://itstory1592.tistory.com/124</guid>
      <comments>https://itstory1592.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 5 Apr 2023 20:00:20 +0900</pubDate>
    </item>
  </channel>
</rss>