<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>준비된 개발자</title>
    <link>https://readystory.tistory.com/</link>
    <description>Share to learn,
Learn to share.</description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 02:51:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Ready Kim</managingEditor>
    <image>
      <title>준비된 개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/2870784/attach/8f14ac2f9fe94f5eac1ab71c35985ed4</url>
      <link>https://readystory.tistory.com</link>
    </image>
    <item>
      <title>좋은 팀 문화 만들기에 대한 고찰(feat. 당근마켓)</title>
      <link>https://readystory.tistory.com/222</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 제가 현재 근무중인 당근마켓에서 새로운 팀빌딩을 하게 되었어요.&lt;br /&gt;새로운 동료들과 함께 팀을 만들어 나가야 하는 상황에서, 어떻게 하면 좋은 팀을 만들 수 있을까에 대해 고민하고 공유하는 시간을 가졌는데 그 과정이 좋았어서 블로그에도 기록차 남겨봅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;각자 할 일을 잘 하면 될 것 같은데&amp;hellip; 팀워크가 중요한가요?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQSGb8/btrSSKXaoPX/U4iEg6I6qTKvUbbBkwTRa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQSGb8/btrSSKXaoPX/U4iEg6I6qTKvUbbBkwTRa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQSGb8/btrSSKXaoPX/U4iEg6I6qTKvUbbBkwTRa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQSGb8%2FbtrSSKXaoPX%2FU4iEg6I6qTKvUbbBkwTRa1%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;541&quot; height=&quot;405&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 회사든 간에 혼자서 한 부서를 담당하는 회사는 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당근마켓 또한 목적 조직으로 팀을 구성하고 있고, 팀에는 다양한 역할과 전문성을 가진 팀원이 모여 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;아래와 같은 팀이 최고의 팀이 될 수 있을 거라 기대하는 사람은 아무도 없을거에요.&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;/li&gt;
&lt;li&gt;우선순위 없이 모든 걸 다 잘하고 싶어하는 팀&lt;/li&gt;
&lt;li&gt;독창적인 철학 없이 효율만을 강조하는 팀&lt;/li&gt;
&lt;li&gt;갈등 상황을 기회라 보지 않고 없애야 할 골치거리라고 인식하는 팀&lt;/li&gt;
&lt;li&gt;내가 할 일과 네가 할 일을 칼 같이 구분하는 데 열을 올리는 팀&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;팀워크는 단순히 &amp;ldquo;으쌰으쌰&amp;rdquo; 하는 팀원들끼리의 인간적인 화합이나 동맹을 의미하지 않아요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀워크를 구축하는 비결은 서로 &lt;b&gt;모순되는 가치를 포용&lt;/b&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;/li&gt;
&lt;li&gt;고도의 화합을 추구하면서도 구성원들을 서로 느슨하게 연결하는 것&lt;/li&gt;
&lt;li&gt;개인의 개성을 중시하면서도 팀과의 조화를 강조하는 것&lt;/li&gt;
&lt;li&gt;책임감을 부여하면서도 자율성을 용인하는 것&lt;/li&gt;
&lt;li&gt;솔직한 비판을 추구하면서도 타인에 대한 지지를 권장하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상충되는 두 가치 중 하나를 선택하고 밀어붙이는 것이 바람직한 리더십이라고 여기는 리더들에겐 매우 어렵게 느껴지겠지만, 위대한 팀은 바로 그 어려운 걸 해냈을 때 만들어져요.&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;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;팀워크는 팀의 성과를 만들어내요.&lt;/b&gt;&lt;br /&gt;좋은 팀워크를 갖춘 팀에서는 팀원 서로서로가 믿음을 가지고 서로에게 잘 맞는 일을 맡아요. 모든 일을 자신이 다 해야만 깔끔하게 해낼 수 있다는 생각을 버리고, 느리게 오는 팀원이 있다면 앞서간 팀원이 도와줄 수도 있고 어려움을 가진 팀원이 있다면 빠르게 일을 해내는 팀원이 함께 해줄 수 있는 상호보완적 관계가 잘 형성되어 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀워크는 동기부여가 돼요.&lt;/b&gt;&lt;br /&gt;자신의 팀에서 팀워크가 잘 맞다는 신호는 모두가 자신의 일을 열심히 하는 상황이에요. 각자의 팀원이 자신에게 잘 맞고 잘 해낼 수 있는 일을 적절하게 분배하여 모두 다 자신에게 맞는 일을 열심히 할 수 있어요. 이 상황에서 모두가 서로를 보면서 더 동기부여가 되고 더 잘하고 싶은 마음이 생기게 돼요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀워크가 잘 맞는 팀에서 일하면 재미있어요.&lt;/b&gt;&lt;br /&gt;팀워크가 잘 맞는 팀들은 대부분 서로가 친하고 편하게 일을 진행 할 수 있어요. 하지만 조금이라도 팀워크가 어긋나는 팀은 서로가 앙숙관계가 되거나 잘 맞지 않는 경우가 대부분이에요. 서로에게 믿음과 재미를 줄 수 있는 팀이 된다면 최고의 효율을 낼 수 있어요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 팀이 되는 것은 당연히 어려워요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최고의 회사라 불리는 빅테크 기업의 문화의 제도를 그대로 베껴온다고 해서 우리도 최고의 회사가 되는 것은 아니에요. 알리바바 마윈은 &amp;ldquo;모방만 하는 기업은 망한다&amp;rdquo; 라고 했어요.&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;건강한 몸매를 만들기 위해 결심한 사람은 다음과 같이 행동해요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피트니스 센터 또는 필라테스에 등록한다.&lt;/li&gt;
&lt;li&gt;식단을 조절하며 운동한다.&lt;/li&gt;
&lt;li&gt;변화를 관찰한다. (눈바디, 인바디 등)&lt;/li&gt;
&lt;li&gt;2번과 3번을 반복한다.&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;/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;b&gt;좋은 팀워크를 갖춘 팀을 만든다는 것은 급진적으로 이루어지지 않아요.&lt;/b&gt; 그리고 그 과정은 때로는 고통스럽고 때로는 무의미하게 느껴질 수 있어요. 하지만 식단과 운동이 힘들다고 해서 중간에 포기하면 좋은 몸매를 가질 수 없는 것처럼, 귀찮고 고통스럽고 무의미해 보인다 할지라도 포기하지 않고 꾸준히 노력하는 것이 중요해요. 이 과정이 결국에는 좋은 팀을 만들어낼 것이라는 믿음으로 서로 의지하며 노력해 나가야 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홀푸드의 창립자 중 하나인 존 매키는 개인의 창의력과 잠재력을 끌어내는 기업의 관계에 대해 다음과 같이 말했어요.&lt;/p&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 Serif KR'; color: #666666;&quot;&gt;팀 단위로 일하면 친밀함과 신뢰가 생기며 관계도 자연스러워집니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #666666;&quot;&gt;인간은 수십만 년 동안 작은 규모의 집단과 부족을 기반으로 진화해왔죠. 팀의 일원이 되면 공동의 노력이 더 인정받고, 더 창의적이고 더 나은 성과를 내기 위해 서로 격려하므로 더 깊은 성취감을 느끼게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #666666;&quot;&gt;잘 만들어진 팀은 협업을 하지 않았더라면 잠자고 있었을 잠재적 힘을 발휘하게 합니다.&amp;nbsp;즉 부분의 합보다 전체가 훨씬 더 강력한 시너지 효과를 내는 거죠. 서로 나누고 협동하는 팀 문화는 인간의 근본적인 본성을 충족시켜줄 뿐 아니라 뛰어난 성과를 만드는 데에도 결정적인 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 팀워크를 갖춘 팀이 되기 위해서는 어떤 노력을 해야하나요?&lt;/h2&gt;
&lt;p 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;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;ldquo;행복한 가정은 모두 비슷하다. 하지만 불행한 가정은 저마다 다른 모습으로 불행하다.&amp;rdquo;&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 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;/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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;집념을 공유해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 팀원들은 자신이 하는 일과 회사에 대해 열정적인 신념, 목표 등을 서로 공유해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 팀원들은 자신이 팀에서 매우 특별한 존재이며, &lt;b&gt;서비스를 더 나은 방향으로 만들 사명을 가지고 있다고 믿어야 해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 틀에 박힌 형태의 팀과 팀원들은 자신들이 하는 일을 그저 얼른 마쳐야 할 업무로만 봐요.&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;h3 data-ke-size=&quot;size23&quot;&gt;능력보다 조화를 우선해요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;훌륭한 팀은 팀이 목표를 달성하는 데 필요한 개인의 장점을 굉장히 중요하게 생각해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 팀에서는 팀원 개개인의 동기와 가치, 성격 등이 적절하게 융화될 수 있는 독창적인 방법을 개발해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 팀 문화에 잘 어울리고 팀을 발전시킬 수 있는 팀원을 채용해요. 그런 자질이 있는 사람에게는 팀에 들어오라고 권하지만, 그렇지 않은 사람에겐 관심을 두지 않아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조화가 아닌 능력을 우선시 하는 팀은 팀원을 뽑을 때 주로 과거의 경력이나 기술적인 측면만을 기준으로 선발해요.&lt;/p&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;  당근마켓이 컬처핏 면접을 중요하게 여기고, 각 직무별 면접관들도 직무적인 역량만 보고 채용하지 않는 것도 조화로운 팀을 만드는 데 큰 기여를 해주고 있다고 믿어요.&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;/aside&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 팀은 팀의 성공에 큰 영향을 미치는 중요한 분야를 집중 공략해요.&lt;/b&gt;&lt;br /&gt;팀이 맡고 있는 분야에 어마어마한 시간을 쏟아 헌신하며, 그 외의 산만한 요소를 피하기 위해 어떤 노력도 마다하지 않아요. &lt;b&gt;불필요한 절차와 간섭도 배제해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 팀은 기업의 서비스를 초월하여 더 성장하고 발전할 새로운 기회를 창의적으로 탐구해요. 그리고 그걸 잘하기 위해 시간과 자원, 자율성 등을 제공 받을 수 있도록 접근방식을 개발해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇지 못하는 팀은 우선순위의 폭이 매우 넓어서 집중해야 할 분야가 지나치게 광범위해요. 따라서 덜 중요한 일에 주의력이 분산 되곤 해요.&lt;/p&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;  저서 [구글 소프트웨어 엔지니어는 이렇게 일한다] 에서도 중요한 일과 급하게 해야하는 일에 대해 밸런스를 잘 잡아야 한다고 강조하고 있어요. 저서에서는 중요한 일은 급하지 않고, 급한 일은 중요하지 않다고 말하고 있어요. 급한 일만 해내다가는 정작 중요한 일을 못할 수 있기 때문에 팀의 내부적, 외부적 요인에 의해 치고 들어오는 일에 대해 적절하게 잘 쳐내는 것 또한 좋은 팀을 만들어 나가는 데 중요한 일이라고 생각해요.&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;/aside&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 팀은 강하면서도 부드러워요. 명확한 목표를 정해놓고 측정 가능한 결과를 내기 위해 강도 높게 노력해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이런 팀은 &lt;b&gt;약점을 솔직하게 인정하고, 능력이 부족한 팀원에게는 조치를 취해요.&lt;/b&gt; 동시에 이들은 협동, 신뢰, 성실함 등의 분위기를 조성하기 위해 더 부드러운 방식을 사용해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 그렇지 못한 팀은 약점을 드러내지 않으려 하고, 능력이 부족한 팀원을 비난하고 방치해요.&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;b&gt;팀의 실패는 온전히 팀의 실패지 개인의 실패가 아니에요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;불편함을 편안하게 받아들여요&lt;/h3&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;b&gt;도전과 어려움에 대해 일을 착수 하기까지는 할지 말지에 대해서 엄청나게 많은 충돌을 피하지 않고 논쟁하지만, 결정이 된 이후에는 팀의 결정이 곧 내 결정이라는 마음으로 최선을 다해 인정하고 따라요. 그리고 결과에 대해서도 회피하지 않아요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;예를 들어, &lt;code&gt;거 봐 내가 이거 안될거라 했지?&lt;/code&gt;, &lt;code&gt;내가 저번에 OOO 해야 한다고 했잖아&lt;/code&gt; 등 팀의 결정이 좋지 못한 결과를 가져왔을 때 팀원으로써 책임을 나눠 갖지 않고 회피하는 것은 좋지 못해요. 이 경우에는 왜 본인이 팀을 더 설득하지 못했을지, 이번 실패에 대해 어떤 것을 확실히 배우게 됐는지 등을 통해 더 좋은 팀이 될 수 있도록 함께 노력해야 해요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 못한 팀은 갈등과 충돌을 피하거나 실패의 조짐으로 받아들여요. 그리고 긁어 부스럼이 싫고, 실패가 두려워 큰 도전과 어려움을 감수하는 것을 기피해요.&lt;/p&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;  당근마켓의 문화 중 가장 뜨거운 감자라고 한다면 단연코 **&amp;ldquo;신뢰와 충돌&amp;rdquo;** 일거에요. 당근마켓에 있는 모든 동료들은 이 가치가 얼마나 훌륭한 가치인지, 그러면서 동시에 정말 어려운 가치라는 것을 알고 있을거에요. 전사적으로 이 가치를 잘 지켜나가는 것은 쉽지 않은 일이겠지만, 나로부터 시작된다는 믿음으로 팀에서부터 잘 지켜나간다면 결국 전사적으로 잘 지켜나가는 문화가 될 것이라 믿어요.&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;/aside&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이런 팀은 되지 않도록 해야 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 팀이 되기 위한 노력을 하는 것도 중요하지만, 되고 싶지 않은 모습을 설정하는 것도 중요해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우리가 지양해야 하는 팀에 대해 설정해봤어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일을 전문적으로 해내기만 하면 되는 직업으로 보는 것은 지양해요.&lt;/b&gt;&lt;br /&gt;예를 들어, &lt;code&gt;개발자가 개발만 잘하면 되지.&lt;/code&gt; , &lt;code&gt;디자이너가 디자인만 잘하면 되지.&lt;/code&gt; 등이 여기에 해당해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀원의 가치를 개인의 경력이나 능력으로만 평가한다.&lt;/b&gt;&lt;br /&gt;예를 들어, &lt;code&gt;OOO 은 주니어라서 이런 부분이 부족할거야.&lt;/code&gt;, &lt;code&gt;OOO 은 구글에서 온 사람이니까 믿고 따르자.&lt;/code&gt; 등이 여기에 해당해요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한꺼번에 많은 목표를 추구하지 않아요. 우선 순위가 많을수록 좋다고 믿는 것은 지양해요.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀원들 간의 갈등이나 불편한 상황은 피하려고 한다.&lt;/b&gt;&lt;br /&gt;예를 들어, &lt;code&gt;좋은 게 좋은 거지&amp;hellip; 아쉬운 점 피드백 주면 상처 받을 수도 있으니까 괜히 일을 만들지 말자&lt;/code&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;골든 타임&lt;/code&gt;은 외상을 입었을 때, 내외과 치료를 받아 죽음에 이르는 것을 방지할 가능성이 가장 큰 시간대를 뜻해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업에서 신뢰도 마찬가지에요. 팀 업무를 하며 고통 받는 팀원이 있을 때 팀이 해당 팀원의 고통을 방치하게 된다면, 해당 팀원은 갈수록 퍼포먼스(기여도)가 떨어지게 될 것이고 결국 서로간에 신뢰를 잃게 될 것이에요. 이는 단순히 개인과 개인의 문제가 아니며 결국 팀 전체의 손해로 이어지게 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/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;h2 data-ke-size=&quot;size26&quot;&gt;우리 팀은 어떤 팀이 되고 싶은지 얘기해야 해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세계의 많은 훌륭한 팀 문화를 갖고 있는 기업들이 모두 같은 팀 문화를 갖고 있지 않아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업마다, 팀마다 특징과 성격이 모두 달라요. 물론 많은 부분에서 공통점이 있겠지만 분명하게 차이점도 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;code&gt;넷플릭스&lt;/code&gt;는 자사의 팀 체제를 가장 잘 비유할 수 있는 대상으로 &lt;b&gt;&amp;lsquo;프로 스포츠 팀&amp;rsquo;&lt;/b&gt;을 꼽아요. 개인이 기업 내에서 당면한 도전 과제를 해결하는 데 필요한 능력을 발휘하지 못하면, 언제든 다른 구성원으로 대체된다는 점에서 그렇게 말한다고 해요. 가족은 자녀를 해고하지 않기 때문에 가족의 일원이라고는 하지 않는다고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 &lt;code&gt;자포스&lt;/code&gt;는 가족 같은 분위기를 조성하기 위해 많은 노력을 기울이며 실제로 &lt;b&gt;&amp;lsquo;가족&amp;rsquo;&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;&lt;b&gt;우리가 어떤 팀이 되고 싶은 지를 설정하는 것은 좋은 팀 문화를 만드는 것에 있어 반드시 선행되어야 하는 굉장히 중요한 일이에요.&lt;/b&gt; 따라서 팀의 문화를 만들어나가기 전에는 반드시 팀원 간에 솔직한 소통을 충분히 할 필요가 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 팀도 실패할 수 있어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;훌륭한 팀은 있을 수 있지만, 완벽하고 완전한 팀은 있을 수 없어요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 혁신적인 팀 체제를 갖춘 기업이라 할지라도 오랜 세월 건재하지 못하고 경쟁자들의 도전에 굴복하거나 스스로 불러온 상처에 쓰러지곤 하는 것이 기업의 역사에요.&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;여러 가지 외부 요인 역시 기업의 성공에 걸림돌이 될 수 있어요. 예를 들어 신기술이 기존의 기업을 추월할 수 있죠. 증강현실이 영화와 TV 에서 우월한 입지를 차지하게 되면, 넷플릭스의 비즈니스 모델은 쇠락할지도 몰라요. 현시점에서 우리는 새로운 기술이 넷플릭스를 더 진화하게 할지, 아니면 넷플릭스를 침식시킬지 알 수 없죠. 예전에 넷플릭스가 기존의 미디어 업체들을 침식시켰듯 새로운 강자가 부상할 수도 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이처럼 좋은 팀을 만드는 것이 반드시 시장에서 당근마켓이 더 성공하는 것을 보장해주지는 못할 수 있어요. 하지만 가능성은 높일 수 있다고 믿어요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 마주하는 당근마켓은 단순히 앱 서비스 일 수 있지만, 그 뒤에는 당근마켓이라는 서비스를 잘 만들고 유저에게 &amp;ldquo;당근스러운&amp;rdquo; 가치를 제공하기 위해 고민하고 노력하는 당근마켓의 문화가 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 보이스나 마켓 리뷰 등을 보면 당근마켓을 사랑해주시고 응원해주시는 유저분들이 굉장히 많다는 것을 느낄 수 있어요. &lt;b&gt;많은 분들의 기대와 사랑을 받는 서비스를 만드는 만큼 보다 더 좋은 서비스를 잘 만들기 위해서는 서비스를 만드는 우리부터 먼저 좋은 팀으로 일할 수 있었으면 좋겠고, 완벽한 서비스와 완전한 팀을 만들 수는 없더라도 불가능 하다고 포기해버리는 것이 아니라 어떻게든 해내기 위해 최선을 다해 노력하는 우리가 되었으면 좋겠어요.&lt;/b&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;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbK0qA/btrSOyXfTWt/kkTDV5GwPzkviX30pQXLd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbK0qA/btrSOyXfTWt/kkTDV5GwPzkviX30pQXLd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbK0qA/btrSOyXfTWt/kkTDV5GwPzkviX30pQXLd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbK0qA%2FbtrSOyXfTWt%2FkkTDV5GwPzkviX30pQXLd0%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;400&quot; height=&quot;397&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Insight</category>
      <category>당근마켓 팀문화</category>
      <category>원팀</category>
      <category>좋은 팀 문화 만들기</category>
      <category>좋은팀</category>
      <category>좋은팀 만들기</category>
      <category>팀빌딩</category>
      <category>팀워크</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/222</guid>
      <comments>https://readystory.tistory.com/222#entry222comment</comments>
      <pubDate>Mon, 5 Dec 2022 01:04:07 +0900</pubDate>
    </item>
    <item>
      <title>성장을 갈망하는 사람들이 가지면 좋은 습관에 대한 고찰</title>
      <link>https://readystory.tistory.com/221</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;IT 기업에 종사하는 사람들, 그중에서도 MZ 세대의 눈에 띄는 특징을 꼽으라고 한다면 단연코 &quot;성장에 목마른 사람들&quot;이라고 할 수 있을 것 같습니다. 개발자, 디자이너, PM 등 직군에 상관없이 각자의 분야에서 업무를 하면서 본인이 성장하고 있는지 점검하는 것은 굉장히 중요한 관심사일 것입니다.&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일 잘하는 개발자 / 개발 잘하는 개발자&lt;/li&gt;
&lt;li&gt;빠르게 개발하는 개발자 / 안정적이게 개발하는 개발자&lt;/li&gt;
&lt;li&gt;다양한 분야의 개발 지식을 두루 갖춘 개발자 / 한 분야에 엄청 깊은 지식을 갖고 있는 개발자&lt;/li&gt;
&lt;li&gt;커뮤니케이션은 잘 하는데, 개발은 평범하게 하는 개발자 / 개발은 엄청 잘하는데, 커뮤니케이션이 아쉬운 개발자&lt;/li&gt;
&lt;li&gt;회사의 성장을 개인의 성장보다 우선으로 하는 개발자 / 개인의 성장을 회사의 성장보다 우선으로 하는 개발자&lt;/li&gt;
&lt;li&gt;같이 일 하고 싶은 개발자 / 같이 일 하고 싶지 않은 개발자&lt;/li&gt;
&lt;/ul&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;b&gt; &quot;메타인지를 통해 스스로를 점검하기&quot;&lt;/b&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;예를 들자면, 좋은 몸매를 갖기 위해 헬스장을 등록하여 운동을 다닌다고 해서 몸이 무조건 좋아지는 것이 아닙니다. PT 를 받아보신 분들은 알겠지만, 제일 처음에는 먼저 인바디를 통해 현재 내 몸 상태가 어떠한지 검사하여 내 몸의 균형 상태가 어떤지에 따라 그에 맞게 집중해야 하는 운동과 식단을 제시해줍니다.&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;우리의 성장도 마찬가지입니다. 무턱대고 좋은 개발자가 되고 싶다고 마음 먹고서 열심히 일을 한다고 해서 좋은 개발자가 되지 않습니다. 현재 나는 어떤 사람이고, 어떤 능력이 좋고 나쁜지를 파악하고 있어야 강점은 부각하고, 약점은 보완하는 식으로 성장해나갈 수가 있습니다. 그리고 내가 어떤 유형의 개발자를 목표로 함에 따라 키워나가야 하는 능력이 완전히 달라지기도 합니다. 누군가에게는 한 기업의 CTO 로써 한 기업의 전반적인 기술을 총괄하는 사람이 되고 싶은 사람이 있는가 하면, 누군가는 자신의 기술 분야에서 스페셜리스트가 되는 것이 목표일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 역량은 우수하지만 협업 능력이 다소 부족하신 분은 주구장창 개발만 공부할 게 아니라, 협업과 관련해서 소프트 스킬을 익히고 키울 수 있도록 노력해야 합니다. 반대로 커뮤니케이션 능력은 우수하지만 개발 역량이 부족하신 분들은 내가 어떤 부분에서 역량이 부족한지 점검하고 부족하다 생각하는 부분을 집중해서 성장시켜야 합니다. 이는 정량적인 평가가 아닌, 정성적인 평가이기 때문에 스스로를 판단하면서 점검하는 것도 중요하지만 동료들의 도움을 받는 것도 좋습니다. 동료, 또는 리더에게 내가 현재 갖고 있는 강점과 약점에 대해 피드백을 요청하고 내가 더 나아가기 위해 어떤 것을 챙겨야 할지 듣는 것도 좋은 방법이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 스스로 점검이 어려우신 분들이 있다면 &lt;a href=&quot;https://github.com/jorgef/engineeringladders&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;engineeringladders&lt;/a&gt; 라는 도구를 활용해보시는 것을 추천합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/umZpV/btrMTFphUrT/jOu89Qebf93BjfBl1mFg8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/umZpV/btrMTFphUrT/jOu89Qebf93BjfBl1mFg8K/img.png&quot; data-alt=&quot;Engineering ladders&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/umZpV/btrMTFphUrT/jOu89Qebf93BjfBl1mFg8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FumZpV%2FbtrMTFphUrT%2FjOu89Qebf93BjfBl1mFg8K%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;479&quot; height=&quot;388&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Engineering ladders&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로 업무를 하다 보면 다음과 같은 질문을 많이 받습니다.&lt;/p&gt;
&lt;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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&quot;이 기능 개발하는 데 &lt;span style=&quot;color: #ee2323;&quot;&gt;일정&lt;/span&gt;은 얼마나 잡으면 될까요?&quot;&lt;/b&gt;&lt;/i&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;/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;한 가지 팁이 있다면 내가 생각했을 때 이 정도 기간이면 되겠다고 생각되는 일정보다는 약간 더 여유 있게 잡으시는 것이 좋습니다. 보통 일정을 물어보는 동료들은 정말 얼마나 걸릴지 모르기 때문에 물어보는 경우가 많을 것이라서, 내가 7일 걸린다고 한 작업을 10일이 걸려서 완성하면 일을 느리게 처리한 것처럼 보이지만, 반대로 10일 걸린다고 한 작업을 7일 안에 해내면 일을 빨리 처리한 사람이 되는 것이 현실이기 때문에 과도하지 않은 선에서는 보통 일정을 조금 더 여유 있게 잡으시는 것을 추천드립니다. 이때 오해하면 안 되는 것은 일정을 여유 있게 잡았다고 해서 업무도 여유 있게 하라는 말이 아닙니다. 그렇게 되면 높은 확률로 작업은 더 느려지게 되고 팀 전체적인 속도가 저하되어서 빠르게 업무를 해내는 스타트업에서는 자신 때문에 일이 진행이 잘 안 되는 블로커가 되기 쉽습니다. 커뮤니케이션 단계에서는 일정을 보수적으로 산정하되, 작업은 최선을 다해 진행하실 것이 가장 좋습니다.&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;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성장을 추구하는 많은 사람들이 가지면 좋은 습관에 대한 저의 고찰을 작성해봤습니다. 제가 강조하고 싶은 것은 무식하게 일 하지 않는 것입니다. 우리가 아무런 목표와 지식도 없이 헬스장에 가서 내 마음대로 운동하고 집으로 돌아온다면 1년을 넘게 운동을 해도 좋은 몸을 갖기 어려울 것입니다. 정확하게 내가 키우고 싶은 포인트가 무엇인지를 정하고 집중해서 훈련하세요. 그렇게 하나씩 집중해서 키워나간다면 우리는 어떤 회사도, 동료도 원할 수밖에 없는 육각형 인재가 될 수 있을 것이고 그렇게 되면 인력 시장에서의 가치는 엄청날 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 제가 현재 속해있는 당근마켓에서 면접관으로 면접에 임하면 지원자분께 항상 이런 질문을 드립니다.&lt;/p&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;size18&quot;&gt;&lt;i&gt;&lt;b&gt;&quot;지원자 분께서 생각하는 &lt;span style=&quot;color: #006dd7;&quot;&gt;좋은 개발자&lt;/span&gt;는 어떤 개발자인가요?&quot;&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;&quot;지원자 님은 &lt;span style=&quot;color: #009a87;&quot;&gt;어떤 환경&lt;/span&gt;에서 가장 높은 퍼포먼스를 발휘할 수 있나요?&quot;&lt;/b&gt;&lt;/i&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글은 &lt;a href=&quot;https://event-us.kr/ted/event/45142&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;주니어 개발자 컨퍼런스&lt;/a&gt; 에서 제가 발표한 내용을 기반으로 작성하였습니다.&lt;/p&gt;</description>
      <category>Insight</category>
      <category>개발자 성장</category>
      <category>개발자 습관</category>
      <category>개발자 컨퍼런스</category>
      <category>개인의 성장</category>
      <category>메타인지</category>
      <category>성장</category>
      <category>신입 개발자 성장</category>
      <category>좋은 습관</category>
      <category>주니어 개발자</category>
      <category>주니어 개발자 컨퍼런스</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/221</guid>
      <comments>https://readystory.tistory.com/221#entry221comment</comments>
      <pubDate>Sat, 24 Sep 2022 15:31:23 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 딥링크는 이걸로 해결! DeepLinkDispatch 알아보기</title>
      <link>https://readystory.tistory.com/218</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발자로 실무를 하다보면 딥링크를 다룰 일이 굉장히 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발자와 협업을 할 때 사용하기도 하고, Push 알림에 targetUri 로 설정하기도 하는 등 활용도가 높은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 딥링크를 처리하는 라이브러리는 다양하게 있습니다만 그중에서 오늘 소개할 라이브러리는 에어비앤비에서 개발하고 오픈소스로 공개한 &lt;a href=&quot;https://github.com/airbnb/DeepLinkDispatch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DeepLinkDispatch&lt;/a&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Activity 에 &lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 어노테이션과 함께 URI 를 등록하면, DeepLinkDispatch 가 요청이 들어왔을 때 적절하게 이동시켜 줍니다. 이때 URI 에 함께 들어온 파라미터 또한 파싱 해주기 때문에 다양한 상황에 대해 유연하게 활용할 수도 있습니다.&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;예제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 예제를 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1655532818970&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DeepLink(&quot;example://readystory.com/deepLink/{id}&quot;)
class SampleActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
      val parameters: Bundle = intent.getExtras()
      val id = parameters.getString(&quot;id&quot;)
      // Do something with id
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 굉장히 간단하게 적용된 것을 확인할 수 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 어노테이션에 딥링크 URL 를 설정해주면, 요청이 들어왔을 때 해당 액티비티를 띄워줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이때, 위 예제에서 {id} 로 표기된 path parameter 나, 쿼리 파라미터가 있을 경우에는 Activity 의 Intent 에 key-value 형식으로 값이 파싱되어 들어있으니 필요에 따라 꺼내 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, &lt;b&gt;DeepLink.IS_DEEP_LINK&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;위 예제에는 나오지 않았지만 하나의 액티비티에 다수의 딥링크를 적용할 수 있습니다. 따라서 여러 링크를 연결하고 싶은 경우에는 &lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink({&quot;A Link&quot;, &quot;B Link&quot;})&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;b&gt;함수에 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 어노테이션은 Activity 클래스에만 적용할 수 있는 것이 아닙니다. 함수에도 적용할 수 있는데요. 이때 조건은 Java 는 &lt;b&gt;public static&lt;/b&gt; 으로 선언된 함수이어야 하고, Kotlin 은 &lt;b&gt;object&lt;/b&gt; 내에서 &lt;b&gt;@JvmStatic&lt;/b&gt; 어노테이션을 적용한 함수에만 적용 가능합니다. &lt;i&gt;(주의할 점은, companion object 내 함수에는 적용이 불가합니다.)&lt;/i&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;아쉽게도 아닙니다. &lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 어노테이션을 함수에 적용할 때는 함수의 리턴 타입에 대해 몇 가지 제한사항이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 먼저 &lt;b&gt;Intent&lt;/b&gt; 를 반환하는 케이스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intent 를 리턴하는 함수에 &lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 를 적용한다는 것은 특정 딥링크를 통해 유입 됐을 시에 어떤 액티비티를 실행할 지에 대해서 어떤 Intent 를 통해 화면을 띄울지에 대해 커스텀 가능하다는 것을 의미합니다.&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;만약 딥링크를 통해 실행시킨 Activity 에 대해 백스택 관리도 하고자 할 경우에는 &lt;span style=&quot;color: #009a87;&quot;&gt;@DeepLink&lt;/span&gt; 를 적용한 함수의 리턴 타입을 &lt;b&gt;TaskStackBuilder&lt;/b&gt; 로 선언하시면 되겠습니다. DeepLinkDispatch 라이브러리는 이때 반환된 TaskStackBuilder 의 마지막 Intent 를 사용하여 액티비티를 실행하기 때문에, 딥링크에 의해 호출되는 액티비티의 스택에 어떤 Intent 를 어떤 순서로 쌓을지 커스텀할 수 있습니다.&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;아래는 Java 로 작성된 예시코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1655536303571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DeepLink(&quot;http://readystory.com/deepLink/{id}/{name}&quot;)
public static TaskStackBuilder intentForTaskStackBuilderMethods(Context context) {
  Intent detailsIntent =  new Intent(context, SecondActivity.class).setAction(ACTION_DEEP_LINK_COMPLEX);
  Intent parentIntent =  new Intent(context, MainActivity.class).setAction(ACTION_DEEP_LINK_COMPLEX);
  TaskStackBuilder  taskStackBuilder = TaskStackBuilder.create(context);
  taskStackBuilder.addNextIntent(parentIntent);
  taskStackBuilder.addNextIntent(detailsIntent);
  return taskStackBuilder;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 딥링크로 인해 위 함수가 호출된 경우에는 MainActivity 위에 SecondActivity 를 쌓았기 때문에, SecondActivity 가 처음에 보여졌다가 스택에서 제거 된다면 MainActivity 가 보여질 것입니다.&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;만약 상황에 따라 Intent 를 반환해야할수도 있고 TaskStackBuilder 를 반환해야할수도 있는 상황이 있다면 어떻게 해야할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 DeepLinkDispatch 라이브러리에서 제공해주는 &lt;b&gt;DeepLinkMethodResult&lt;/b&gt; 타입을 반환하도록 함수를 선언해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepLinkMethodResult 에는 Intent 와 TaskStackBuilder 를 각각 담을 수 있으며, 이때 null 이 아닌 객체를 통해 실행합니다. (만약 둘 다 null 이 아닐 경우에는 TaskStackBuilder 의 우선순위가 더 높습니다.)&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;Custom Annotations&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 내에 딥링크로 연결해주는 화면이 많아질수록, 반복되는 문자열이 많이 있을 수 있습니다. 예를들어 https:// 등과 같은 스킴이 될 수도 있지만 ready:// 와 같이 앱마다 정의하고 있는 커스텀 스킴, 또는 readystory.com 처럼 호스트 이름 등이 반복될 수 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 경우에 보다 편하게 적용할 수 있도록 DeepLinkDispatch 라이브러리에서는 Custom Annotation 방식을 제공해주고 있습니다. Custom Annotation 을 활용한다면 공통의 prefix 를 자동으로 제공할 수 있습니다.&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;pre id=&quot;code_1655537851798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DeepLinkSpec(prefix = { &quot;app://readystory&quot; })
@Retention(RetentionPolicy.RUNTIME)
public @interface AppDeepLink {
  String[] value();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 선언한 경우에는 이제 액티비티에서 아래와 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// &quot;app://readystory/view_users&quot;
@AppDeepLink({ &quot;/view_users&quot; })
class CustomPrefixesActivity : AppCompatActivity() {
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 http(s) 형태의 웹 스킴에 대한 prefix 를 제공하기 위한 @WebDeepLink 같은 어노테이션을 만들더라도 다른 커스텀 어노테이션과 함께 사용 가능하니 적극적으로 사용한다면 앱 개발시에 상당히 유용하게 활용 가능합니다.&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;프로젝트에 새롭게 딥링크를 적용해야 하는 니즈가 있으신 분들은 DeepLinkDispatch 라이브러리를 도입해보는 것을 추천합니다.&lt;/p&gt;</description>
      <category>Android/Library</category>
      <category>Android</category>
      <category>Android AppLink</category>
      <category>Android DeepLink</category>
      <category>Android DeepLink Library</category>
      <category>Android DeepLinkDispatch</category>
      <category>Android DefferedDeepLink</category>
      <category>Android Scheme</category>
      <category>DeepLinkDispatch</category>
      <category>안드로이드 딥링크</category>
      <category>안드로이드 딥링크 라이브러리</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/218</guid>
      <comments>https://readystory.tistory.com/218#entry218comment</comments>
      <pubDate>Sat, 18 Jun 2022 16:44:29 +0900</pubDate>
    </item>
    <item>
      <title>네이버를 떠나 당근마켓에 합류한 이야기</title>
      <link>https://readystory.tistory.com/217</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대학교를 졸업하기 전, 핵데이라는 해커톤을 통해 전환형 인턴 -&amp;gt; 정규직 전환의 이야기를 남긴 지 어느덧 2~3년 정도가 흘렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/99&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2019.10.02 - [Life] - 네이버 2차(최종) 면접 + 정직원 전환 후기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1651041497563&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;네이버 2차(최종) 면접 + 정직원 전환 후기&quot; data-og-description=&quot;후기에 앞서.. 면접이 너무 어려웠고, 면접 후에 결과가 나오기까지 머릿속으로 '내가 왜 그렇게 대답했지' 라거나, '아.. 이건 이렇게 말했어야 하는데..' 싶은 것들이 너무 많았지만 어쨌든 감사&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/99&quot; data-og-url=&quot;https://readystory.tistory.com/99&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OIrme/hyOcuKztHy/VkTlIHVOKTkPUENAPHgdT0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b0KfdI/hyObnM3Png/LxKekKtkT6p33hzo7kS8Mk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bCV4Ss/hyObr2YXfO/Zo0apM32SAERCrduj9SVrk/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/99&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/99&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OIrme/hyOcuKztHy/VkTlIHVOKTkPUENAPHgdT0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b0KfdI/hyObnM3Png/LxKekKtkT6p33hzo7kS8Mk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bCV4Ss/hyObr2YXfO/Zo0apM32SAERCrduj9SVrk/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022');&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;네이버 2차(최종) 면접 + 정직원 전환 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;후기에 앞서.. 면접이 너무 어려웠고, 면접 후에 결과가 나오기까지 머릿속으로 '내가 왜 그렇게 대답했지' 라거나, '아.. 이건 이렇게 말했어야 하는데..' 싶은 것들이 너무 많았지만 어쨌든 감사&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 큰 생각 없이, 당시의 나의 느낀점과 경험을 기록하고자 작성한 것이었는데 생각 외로 많은 분들이 읽어 주셨고 1년 정도는 내 블로그에서 가장 많은 조회수를 차지하는 포스팅으로 자리 잡았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 나의 첫 이직을 기념하며(?) 어떤 마음으로 이직을 준비하게 됐고, 그 과정이 어땠는지, 그리고 최종적으로 합류한 당근마켓에서 약 4개월 간 경험한 이야기들을 해보고자 한다.&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;왜 네이버를 2년도 안 다니고 나온거야?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 네이버가 안 좋은 회사여서 나온 것은 절대 아니다. 나는 여전히 내 주변에 개발자로 취업을 준비하는 지인이나 후배들이 있다면 주저 없이 네이버 카카오를 첫 회사로 추천한다. (당근에 와서 더욱 그걸 느꼈다.) 다만 내 욕심을 채울 수 있는 업무 환경은 아니란 판단이 섰을 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버에서 내가 있던 팀의 리더님과 내 사수님도 다 너무 좋은 분이셨고, 덕분에 인격적으로나 실력적으로나 많은 부분을 배울 수 있었고 신입인데도 불구하고 나를 믿어 주시고 다양한 경험을 해볼 수 있게 도와주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과적으로 동아리 활동은, 코로나로 인해 많이 다운되어 있던 에너지를 확실히 끌어올려 줬고 처음에는 대학생 위주의 동아리라 생각했는데 생각보다 현업에서 일하고 있는 주니어 개발자들도 꽤 있었어서 자극도 많이 받았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 동아리 하는 친구들을 보면서 보다 젊고 에너지 넘치는 회사에서 일한다면 더 재밌게 일할 수 있지 않을까? 라는 생각을 하게 되었고 급발진해서 바로 이직 준비에 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼 왜 당근마켓이야?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이직을 준비하면서 여러 개의 회사를 지원했다. 지원한 회사를 선정함에 있어서 나름의 기준이 있었는데, 시리즈 C 이상의 투자 유치 또는 유니콘 이상의 기업이었다. 아무래도 네이버라는 대한민국에서 가장 큰 IT 회사에서 커리어를 시작했다 보니 엄청 큰 규모의 회사에서 갑자기 너무 작은 스타트업으로 업무 환경이 바뀌어버리면 적응하기가 힘들 것 같기도 했고, 아직 경력이 짧다 보니 현실적으로 갑자기 큰 책임과 역할을 요구하는 환경보다는 어느 정도 안정권에 들어선 환경에서 내가 가장 높은 역량을 발휘할 수 있을 것 같았다.&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조 기업가치) 이상이면서, 각 회사의 서비스 중 내가 흥미를 가질 수 있는 서비스를 운영하는 회사로 추려서 지원했었고 감사하게도 채용 프로세스가 너무 느려서 스스로 채용 포기한 회사를 제외하고서는 모든 회사에 합격했다.&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;두 회사 모두 면접 경험이 정말 좋았고, 해당 회사에 재직 중인 지인을 통해 얘기를 들어봐도 둘 다 너무 좋은 회사라는 인상을 받을 수 있었다.(직원이 남에게 자기 회사를 칭찬하기란 쉽지 않기에)&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;당근마켓은 어때?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기준으로 당근마켓에서 근무한 지 4개월 정도 됐는데, 거짓말 안 하고 이직하길 잘했다는 생각을 100번도 넘게 했다. 그만큼 회사 분위기, 문화, 내게 주어지는 역할 등이 만족스럽다. &lt;span&gt;컬처핏 면접의 영향인지, 그냥 원래 스타트업에 이런 분들이 많은 건지는 모르겠지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;네이버에서와 가장 큰 차이점이 있다면 밝은 성격의 소유자가 정말 많고 다들 따뜻하고 친절하면서 각자 맡은 일에 오너십이 강한 분들이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저에게 따뜻한 가치를 제공하고자 하는 회사의 방향성에 맞게, 내부 문화도 따뜻한 환경을 만들어 나가려고 하는 노력들이 많이 보인다. 그래서 그런가 업무를 하면서 서로 감정이 상하는 경우가 잘 없고 우리가 보다 더 성장하기 위해 의견 충돌하는 것을 어려워하지 않는다. 회사 내부에서는 &quot;신뢰와 충돌&quot;이라고 부르는데, 얼마 전에는 우리가 신뢰와 충돌을 잘하기 위해 어떻게 해야 하고, 잘 동작하지 않았다면 왜 그랬는지에 대해 토론하는 자리도 있었다. 물론 100 이면 100 모든 것이 좋기만 하고 훌륭하기만 한 것은 아니지만 대체로 많은 부분에서 업무적으로나 업무 외적으로 만족하고 있어서 앞으로 당근에서의 경험이 더욱 기대된다.&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;최근 회사가 굉장히 공격적으로 채용을 하고 있는데, 내가 입사할 당시인 4개월 전만 해도 직원 수가 200명 정도였는데 4개월 만에 50% 늘어나 지금은 300명 규모가 됐다. 아무래도 300명 규모가 됐을 때 많은 스타트업이 성장통을 겪는다곤 하는데, 당근마켓도 예외는 아닌 것 같다. 최근 들어 조직구조 개편과 효율적인 업무를 위한 고민(회의가 너무 많음) 등을 내부적으로 진행하면서 이 성장통을 잘 이겨내기 위해 노력하고는 있다. 낙천적일 수도 있지만 나는 이런 과도기를 겪을 수 있는 것도 정말 좋은 경험이라 생각하고 있어서 스트레스를 받기보다는 어떻게 하면 잘 이겨낼 수 있을까 고민하면서 한 편으로는 기대하고 있다.&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;당근마켓에서 안드로이드 개발자는 사업 과제를 해내는 목적 조직(중고거래팀, 동네생활팀 등)과 안드로이드 관련해서 한 팀을 이루는 기능 조직(안드로이드 챕터)에 속해서 일을 하게 된다. 안드로이드 챕터가 어떤 문화에서 일하고, 어떻게 협업하는지에 대한 자세한 내용을 여기에 다 서술하는 것은 맥락을 벗어나는 일 같아서 작년 말에 진행했던 채용 설명회의 유튜브 링크를 첨부한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/yHlGT1DVofE&quot;&gt;https://youtu.be/yHlGT1DVofE&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=yHlGT1DVofE&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/d9ehR8/hyOcCIFvtV/dPAh8npyGkS2QrIBTbBWiK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/yHlGT1DVofE&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&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://gdg.community.dev/events/details/google-gdg-korea-android-presents-aac-viewmodel-semina-eobsi-jinhaenghaneun-aac-viewmodel-iyagi-jayuroun-toroneuro-jinhaenghabnida/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GDG 행사에 패널로 나가는 경험&lt;/a&gt;도 할 수 있었는데 거기서 평소에는 발표를 보기만 하던 입장에서 직접 참여자로 행사에 함께 할 수 있는 경험을 한 것도 좋은 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 MBTI 는 ENFJ 인데 특히 파워 J 여서 인생에 대한 계획도 꽤나 구체적으로 세워뒀다. 그래서 안드로이드 개발 측면에서도 큰 그림을 그리고 있는데 그 계획을 실현하기 위해서는 절대적으로 안드로이드 챕터에 좋은 동료들이 많이 필요하다. 그래서 회사뿐만 아니라 나를 위해서 동료가 필요하기 때문에 적극적으로 홍보를 하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 많은 우수한 분들을 모셔서 서비스적으로도, 기술적으로도 재미있게 일을 할 수 있으면 좋겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 내 이직 후기를 기록하려 한 것인데 최근 채용을 적극적으로 하고 있어서 홍보 차원에서 &lt;a href=&quot;https://team.daangn.com/jobs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;채용 페이지&lt;/a&gt;도 남깁니다..ㅎㅎ&lt;/p&gt;</description>
      <category>etc</category>
      <category>네이버 안드로이드</category>
      <category>네이버 이직</category>
      <category>네이버 퇴사</category>
      <category>당근마켓 면접</category>
      <category>당근마켓 안드로이드</category>
      <category>당근마켓 안드로이드 채용</category>
      <category>당근마켓 이직</category>
      <category>당근마켓 입사</category>
      <category>안드로이드 개발자 이직</category>
      <category>안드로이드 개발자 이직 후기</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/217</guid>
      <comments>https://readystory.tistory.com/217#entry217comment</comments>
      <pubDate>Wed, 27 Apr 2022 17:50:18 +0900</pubDate>
    </item>
    <item>
      <title>[Android] ListAdapter, AsyncListDiffer, DiffUtil 제대로 알고 쓰기</title>
      <link>https://readystory.tistory.com/216</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱을 개발하다 보면 높은 확률로 사용하게 되는 것이 RecyclerView 입니다. 옛날에는 RecyclerView 가 ListView 의 대안으로, 리스트 형태의 뷰에서만 사용해야 하는 느낌이었다면 요즘은 꼭 리스트 형태의 뷰가 아니더라도 굉장히 다양한 용도로 활용하고 있는 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 살펴볼 내용은 RecyclerView 의 Adapter 를 구현할 때, RecyclerView.Adapter 를 상속받아 구현하는 방법보다 더 빠르다고 알려진 ListAdapter 를 상속하는 방식에 대해 자세하게 알아볼 예정입니다. 이 글을 읽고 난다면, ListAdapter 와 DiffUtil 의 내부 동작 원리를 더 잘 이해하게 될 것이고 운이 좋다면 면접 자리에서도 관련된 질문에 대해 자신 있게 답변할 수 있을 것입니다.&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;ListAdapter -&amp;gt; AsyncListDiffer -&amp;gt; DiffUtil&lt;/b&gt; 순으로 살펴볼 예정입니다. 이는 ListAdapter 가 AsyncListDiffer 를 사용하고 있고, AsyncListDiffer 가 내부적으로 DiffUtil.ItemCallback 을 사용하고 있기 때문입니다.&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;ListAdapter&amp;lt;T, VH&amp;gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ListAdapter 입니다. 많은 안드로이드 SDK 가 그러하듯 자바로 구현되어 있고, 2 가지 생성자를 제공하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1650709151827&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class ListAdapter&amp;lt;T, VH extends RecyclerView.ViewHolder&amp;gt;
        extends RecyclerView.Adapter&amp;lt;VH&amp;gt; {
    final AsyncListDiffer&amp;lt;T&amp;gt; mDiffer;

    protected ListAdapter(@NonNull DiffUtil.ItemCallback&amp;lt;T&amp;gt; diffCallback) {
        mDiffer = new AsyncListDiffer&amp;lt;&amp;gt;(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder&amp;lt;&amp;gt;(diffCallback).build());
        mDiffer.addListListener(mListener);
    }

    protected ListAdapter(@NonNull AsyncDifferConfig&amp;lt;T&amp;gt; config) {
        mDiffer = new AsyncListDiffer&amp;lt;&amp;gt;(new AdapterListUpdateCallback(this), config);
        mDiffer.addListListener(mListener);
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 많은 분들이 DiffUtil.ItemCallback 객체를 넘기는 생성자를 많이 사용하시고, 그 아래에 선언된 AsyncDifferConfig 객체를 넘기는 방식은 사용하지 않았거나 있는지 조차 몰랐을 것 같은데요. AsyncDifferConfig 는 내부적으로 main thread executor 와 background thread executor 를 별도로 설정하고 싶은 경우에 등록할 수 있게 해주는 API 를 제공하는 객체입니다. 특별히 메인 쓰레드나 작업 쓰레드 환경을 지정하고자 하는 게 아니라면 기존에 많이 사용하던 방식인 DiffUtil.ItemCallback 을 생성자로 넘기는 방식으로 생성하면 되겠습니다.&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;ListAdapter 는 AsyncListDiffer 를 프로퍼티로 가지며, 실제 ItemList 객체 관리나 DiffUtil 을 통한 Item 변경사항 확인 및 로직 처리 등은 AsyncListDiffer 내에서 이루어집니다.&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;ListAdapter 클래스 안에는 아래와 같은 함수들이 정의되어 있습니다. (Java)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;public void submitList(@Nullable List&amp;lt;T&amp;gt; list)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변화 감지 대상이면서, 화면에 표시될 아이템 리스트를 제출(등록)합니다.&lt;/li&gt;
&lt;li&gt;만약 이미 아이템 리스트가 화면에 보여지고 있었다면, diff 체크가 백그라운드 쓰레드에서 수행되어, Adapter.notifyItem() 이벤트 처리 함수가 메인 쓰레드에서 호출됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;public void submitList(@Nullable List&amp;lt;T&amp;gt; list, @Nullable final Runnable commitCallback)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 submitList() 와 같은 역할을 하는 함수입니다만, 차이점이 있다면 onCurrentListChanged() 함수가 호출될 때 실행되는 콜백을 넘길 수 있습니다.&lt;/li&gt;
&lt;li&gt;만약 submitList 함수를 통해 넘긴 List 객체가 이전에 넘겼던 객체와 동일하다면, 어댑터에 아무런 변화는 없겠지만 그럼에도 commitCallback 함수는 호출됩니다.&lt;br /&gt;따라서, 헷갈리면 안되는 것은 commitCallback 콜백이 호출됐다 해서 무조건 Item List 에 변화가 생긴 것은 아니란 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;protected T getItem(int position)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n 번째 아이템을 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;public int getItemCount()&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이템 갯수를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;public List&amp;lt;T&amp;gt; getCurrentList()&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 아이템 리스트를 반환합니다. 이때, 반환된 리스트는 읽기 전용(Unmodifiable) 객체입니다.&lt;/li&gt;
&lt;li&gt;등록한 리스트가 없거나, submitList(null) 을 통해 null 을 등록한 상황에서도 getCurrentList() 의 리턴 값은 non-null 이기 때문에 emptyList 가 반환됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;public void onCurrentListChanged(List&amp;lt;T&amp;gt; previousList, List&amp;lt;T&amp;gt; currentList)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;currentList 가 업데이트될 때마다 호출됩니다.&lt;/li&gt;
&lt;li&gt;previousList, currentList 모두 non-null 이므로, 캐싱된 값이 없는 경우에는 null 이 아닌 empty list 가 인자 값으로 제공됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 ListAdapter 는 크게 구현부가 복잡하거나 이해하기 어려운 함수가 없어서 러닝 커브가 높지 않다 생각하는데요. (AsyncListDiffer 를 가져다 쓰세요! 하면 어려우니 쉽게 활용하라고 만든 클래스일 테니 당연할지도)&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;저는 처음에 ListAdapter 를 학습했을 때 헷갈렸던 부분은 한 번 submitList 를 통해 아이템을 등록해 놓으면 백그라운드 쓰레드에서 계속해서 아이템을 관찰하고 있다가, 아이템에 변경이 생겼을 경우에 자동으로 notify 를 해준다고 오해했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;하지만 DiffUtil.ItemCallback 의 함수를 통한 아이템 변경 감지는 submitList() 가 호출됐을 때 수행되기 때문에, 만약 item 이 변경된 이후에 리스트를 업데이트하고자 한다면 다시 한번 submitList() 를 호출해줘야 한다는 것을 명심해야 합니다.&lt;/b&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;b&gt;AsyncListDiffer&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서도 언급했지만 ListAdapter 의 실제 핵심 로직은 바로 이 AsyncListDiffer 내부에 구현되어 있다고 볼 수 있습니다. 그리고 우리가 ListAdapter 의 생성자를 통해 넘긴 DiffUtil.ItemCallback 객체도 AsyncListDiffer 에서 전달받아 사용합니다.&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;AsyncListDiffer 의 공개된 API 는 대부분 ListAdapter 의 함수와 동일하고, 많은 함수를 제공하고 있진 않기 때문에 핵심 로직을 담고 있는 submitList() 에 대해서만 살펴보도록 하겠습니다. (사실 이것만 알면 됩니다.)&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;AsyncListDiffer.submitList() 가 호출되면, 이전에 저장하고 있던 리스트를 previousList 에 따로 보관하고 새로운 리스트로 currentList 를 업데이트 합니다. 이때 아이템이 없었는데 새로 생겼거나, 반대로 아이템이 있었는데 없어진 경우 내부적으로 콜백리스너(ListUpdateListener)를 통해 RecyclerView.Adapter 의 notifyItemRangeInserted(), notifyItemRangeRemoved() 함수가 호출됩니다.&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;그리고 백그라운드 쓰레드에서 DiffUtil.calculateDiff() 함수가 호출되는데, 이때 이 함수의 파라미터로 드디어 우리가 ListAdapter 의 생성자로 넘긴 DiffUtil.ItemCallback 가 활용됩니다. (그대로 사용되는 건 아니고, 한 번 감싸진 형태로 사용됩니다.)&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;문제는 DiffUtil.calculateDiff() 함수가 내부적으로 꽤나 복잡합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;O(N^2) 의 시간 복잡성을 갖는 알고리즘으로 더해지거나(added), 옮겨지거나(moved), 지워진(removed) 아이템이 있는지 검출하는데, 이때 snake sort(정확하진 않음) 라 불리는 정렬 알고리즘을 통해 비교하기 용이하게 합니다.(원본을 건드리진 않음)&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;그 이후에 계속해서 내부적으로 아이템들을 평가하며 비교해나가기 시작하는데, 이때 DiffUtil.ItemCallback 에서 정의했던 areItemsTheSame(), areContentsTheSame() 이 사용됩니다.&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;DiffUtil.ItemCallback&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncListDiffer 내에서 DiffUtil.ItemCallback 을 사용하기까지 과정이 많이 있지만 핵심만 말씀드리자면, &lt;span style=&quot;color: #009a87;&quot;&gt;areItemsTheSame()&lt;/span&gt; 함수가 먼저 실행이 되고 해당 함수의 결과로 &lt;span style=&quot;color: #009a87;&quot;&gt;true&lt;/span&gt; 가 반환됐을 경우에만 &lt;span style=&quot;color: #006dd7;&quot;&gt;areContentsTheSame()&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;color: #009a87;&quot;&gt;areItemsTheSame()&lt;/span&gt; 에는 일반적으로 id 처럼 아이템을 식별할 수 있는 유니크한 값을 비교하고, &lt;span style=&quot;color: #006dd7;&quot;&gt;areContentsTheSame()&lt;/span&gt; 에는 아이템의 내부 정보가 모두 동일한지 비교합니다. 그래서 &lt;span style=&quot;color: #006dd7;&quot;&gt;areContentsTheSame()&lt;/span&gt; 에서는 보통 equals() 함수를 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;주의할 점은, 코틀린으로 개발하는 많은 분들이 아이템을 정의할 때 data class 로 정의하는데 이때 data class 는 별도로 재정의하지 않는다면 자동으로 equals() 함수를 재정의 한다는 것을 인지해야 합니다. 그리고 코틀린에서는 == 오퍼레이터가 equals() 함수를 의미한다는 것을 알아야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;그래야 areContentsTheSame() 함수에서 itemA == itemB 등과 같이 사용했을 때 함수의 원래 의도대로 사용할 수가 있습니다.&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;그리고 DiffUtil.ItemCallback 추상 클래스에는 필수는 아니지만 재정의 할 수 있는 함수를 하나 더 제공하는데, 바로 &lt;span style=&quot;color: #8a3db6;&quot;&gt;getChangePayload()&lt;/span&gt; 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 사용되는 함수는 아니긴 하지만, &lt;span style=&quot;color: #8a3db6;&quot;&gt;getChangePayload()&lt;/span&gt; 는 &lt;span style=&quot;color: #009a87;&quot;&gt;getItemsTheSame()&lt;/span&gt; 함수는 true 를 리턴하는데 &lt;span style=&quot;color: #006dd7;&quot;&gt;getContentsTheSame()&lt;/span&gt; 은 false 를 리턴한 경우에 호출됩니다.&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;ItemAnimator 를 사용하는 경우에 &lt;span style=&quot;color: #8a3db6;&quot;&gt;getChangePayload()&lt;/span&gt; 를 통해 반환되는 리턴 값으로 적절한 애니메이션을 동작하게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt; getChangePayload()&lt;/span&gt; 에서 반환되는 리턴 값은 &lt;b&gt;RecyclerView.Adapter 의 onBindViewHolder(VH holder, int position, List&amp;lt;Object&amp;gt; payload)&lt;/b&gt; 의 인자 값으로 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제를 동반한 포스팅이 보다 더 이해를 도왔을 수 있겠지만, 예제까지 첨부하면 포스팅이 너무 길어질 것으로 판단되어 첨가하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 글을 보는 것 만으로 다 이해하긴 어렵다 생각하기 때문에(다 담아내기도 어렵고), 보다 자세한 내용에 관심 있으신 분들은 안드로이드 스튜디오를 켜서 내부 구현까지도 직접 찾아 들어가 주석과 구현 코드까지도 살펴보시길 권장합니다.&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android AsnyListDiffer</category>
      <category>android diffutil</category>
      <category>android diffutil callback</category>
      <category>android listadapter</category>
      <category>android recyclerview</category>
      <category>android recyclerview background</category>
      <category>android recyclerview diffutil</category>
      <category>android recyclerview diffutil.ItemCallback</category>
      <category>android recyclerview listadapter</category>
      <category>android recyclerview viewholder</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/216</guid>
      <comments>https://readystory.tistory.com/216#entry216comment</comments>
      <pubDate>Sat, 23 Apr 2022 23:03:20 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Why DataStore? (부제 : Good-bye SharedPreferences)</title>
      <link>https://readystory.tistory.com/215</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발을 하다보면, 간단한 데이터에 대해 로컬에 저장하고 사용하고자 하는 니즈를 자주 마주하게 됩니다. 그럴 때마다 그동안에는 SharedPreferences 라는 라이브러리를 사용해왔는데요. SharedPreferences 는 key-value 의 형식의 데이터로 다뤄지며, xml 파일로 로컬 저장소에 저장됩니다.&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;얼마전, Jetpack DataStore 가 1.0.0 stable 버전이 정식 릴리즈 되었는데요. Android Developer 유튜브 채널에서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DataStore 를 소개하는 영상에서는 진행자가 아래와 같은 말로 소개를 했습니다.&lt;/span&gt;&lt;/p&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;size18&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&quot;It aims to replace SharedPreferences&quot;&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;즉, DataStore 라이브러리는 SharedPreferences 를 대체하기 위한 목적으로 만들어졌다고 볼 수 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 SharedPreferences 에 어떤 문제점이 있어서 DataStore 라는 별도의 라이브러리를 만들었을까요? 우리는 DataStore 를 사용하기에 앞서 이 부분을 먼저 짚고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOWoda/btrvE5KzYn8/vKJLBDnbCIsSR0Y3qS4sl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOWoda/btrvE5KzYn8/vKJLBDnbCIsSR0Y3qS4sl1/img.png&quot; data-alt=&quot;Developer 사이트에서도 Datastore 사용을 권장하고 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOWoda/btrvE5KzYn8/vKJLBDnbCIsSR0Y3qS4sl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOWoda%2FbtrvE5KzYn8%2FvKJLBDnbCIsSR0Y3qS4sl1%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;1766&quot; height=&quot;366&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Developer 사이트에서도 Datastore 사용을 권장하고 있습니다.&lt;/figcaption&gt;
&lt;/figure&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;b&gt;SharedPreferences 의 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 비동기(Asynchronous) API 를 제한적으로 지원&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 SharedPreferences 에서는 읽기(Read)에 대해 기존에 값을 읽어오는 것은 동기(sync) API 만을 제공하고, 값에 변화가 있을 때마다 비동기적으로 값을 가져오는 방법으로는 오직 OnSharedPreferenceChangeListener 를 통해서만 콜백을 받을 수 있게 지원하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 쓰기(Write)에 대해서는 Editor 를 통해 put 하고, commit() 이 아닌 apply() 라는 함수를 통해 비동기로 write 할 수 있게 제공하고 있었는데요. 이 apply() 도 사실 즉시 비동기 호출을 하는게 아니라 내부 코드를 보면 pending 시켜두었다가 서비스나 액티비티가 Start/Stop 되는 시점에 백그라운드에서 동작하게 하는데, 이때 실행되는 fsync() 라는 native 함수가 사실상 main thread 를 Block 하기 때문에 자칫 잘못하면 ANR 까지도 이어질 수 있다고 합니다.&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;2. Runtime Exception 에 취약함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedPreferences 는 기본적으로 Exception 에 대한 에러 핸들링을 제공하고 있지 않습니다. 따라서 SharedPreferences 때문에 발생하는 Exception 을 다루기에 어려움이 있습니다. 심지어 위에서 언급한대로 apply() 통해 pending 된 작업을 처리하다가 에러가 발생할 경우에는 해당 예외를 잡을 방법이 없이 크래쉬를 맞이할 수 밖에 없습니다.&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;3. UI Thread 에 안전하지 않음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/SharedPreferencesImpl.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SharedPreferenceImpl 내부 코드&lt;/a&gt;를 보면, Editor 를 통한 commit() 함수에서는 별도의 쓰레드가 아닌 호출된 Thread 에서 바로 File Write 를 하고 있습니다. 이는 파일에 쓰는 데이터가 많지 않으면 언뜻 보기에 문제 없어 보일 수 있지만 저사양 기기에서나, 데이터 양이 많아지면 Main Thread 를 오랫동안 Block 하면서 유저에게 버벅이는 경험을 주거나 ANR 까지도 이어질 수 있습니다.&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;DataStore&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 DataStore 에 대해 알아보겠습니다. DataStore 의 주요 특징으로는 아래와 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;코루틴&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;및&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt; Flow&lt;/b&gt; &lt;/span&gt;를&lt;span&gt; &lt;/span&gt;사용하여&lt;span&gt; &lt;/span&gt;비동기적이고&lt;span&gt;, &lt;/span&gt;일관된&lt;span&gt; &lt;/span&gt;트랜잭션&lt;span&gt; &lt;/span&gt;방식으로&lt;span&gt; &lt;/span&gt;데이터&lt;span&gt; &lt;/span&gt;저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소규모&lt;span&gt; &lt;/span&gt;단순&lt;span&gt; &lt;/span&gt;데이터에&lt;span&gt; &lt;/span&gt;적합한&lt;span&gt; &lt;/span&gt;솔루션&lt;/b&gt;으로&lt;span&gt;, &lt;/span&gt;복잡한&lt;span&gt; &lt;/span&gt;데이터나&lt;span&gt; &lt;/span&gt;참조&lt;span&gt; &lt;/span&gt;무결성&lt;span&gt; &lt;/span&gt;등을&lt;span&gt; &lt;/span&gt;필요로&lt;span&gt; &lt;/span&gt;할&lt;span&gt; &lt;/span&gt;때는&lt;span&gt; &lt;/span&gt;여전히&lt;span&gt; Room &lt;/span&gt;을&lt;span&gt; &lt;/span&gt;사용하는&lt;span&gt; &lt;/span&gt;것이&lt;span&gt; &lt;/span&gt;더&lt;span&gt; &lt;/span&gt;적합&lt;/li&gt;
&lt;li&gt;key-value &lt;span&gt;방식의&lt;/span&gt; &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;Preferences DataStore &lt;/span&gt;&lt;/b&gt;&lt;span&gt;와&lt;/span&gt; Protocol buffer &lt;span&gt;를&lt;/span&gt; &lt;span&gt;사용한&lt;/span&gt; &lt;span&gt;타입이&lt;/span&gt; &lt;span&gt;지정된&lt;/span&gt; &lt;span&gt;객체를&lt;/span&gt; &lt;span&gt;저장할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있는&lt;/span&gt; &lt;span&gt;방식인&lt;/span&gt; &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Proto DataStore&lt;/span&gt;&lt;/b&gt; &lt;span&gt;솔루션을&lt;/span&gt; &lt;span&gt;제공&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 SharedPreferences 와 비교한다면 아래와 같이 정리할 수 있습니다.&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;blob&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b08RYB/btrvH0BBFSr/hf5lntq4Yf5fSyhRkaCoi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b08RYB/btrvH0BBFSr/hf5lntq4Yf5fSyhRkaCoi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b08RYB/btrvH0BBFSr/hf5lntq4Yf5fSyhRkaCoi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb08RYB%2FbtrvH0BBFSr%2Fhf5lntq4Yf5fSyhRkaCoi0%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;640&quot; height=&quot;550&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataStore 는 코루틴 Flow 를 통해서만 제공되기 때문에 기본적으로는 비동기 API 만을 제공합니다. 하지만 first() 등을 활용한다면 동기로도 활용할 수 있습니다. 그리고 CorruptionHandler 이나 Flow 의 확장함수 중 catch() 등을 통해서 에러 핸들링을 잘 지원해주고 있고, 모든 무거운 작업은 Dispatchers.IO 에서 작업되기 때문에 UI Thread 에서 얼마든지 사용하더라도 안전하다 할 수 있습니다.&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;SharedPreferences 에서 DataStore 로 데이터 마이그레이션 하는 방법도 공식적으로 지원하고 있기 때문에 기존에 SharedPreferences 를 사용하고 있던 프로젝트에서도 보다 편하고 안전한 방법으로 마이그레이션을 할 수 있습니다.&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;Preferences Datastore&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 소개할 것은 Preferences DataStore 입니다. Preferences DataStore 는 이름에서 힌트를 얻을 수 있듯이, key-value 방식으로 데이터를 읽고 쓰는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 DataStore 를 다루기 위해서는 DataStore&amp;lt;T&amp;gt; 타입의 객체를 활용해야 하는데, 이때 Preferences DataStore 를 생성하는 방식은 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;한 가지 방식은 preferencesDataStore() 라는 Delegate 함수이고, 다른 하나는 PreferenceDataStoreFactory.create() 팩토리 함수입니다. 사실 preferencesDataStore() 함수 내부적으로 PreferenceDataStoreFactory.create() 함수를 사용하고 있기 때문에 팩토리 통해서 객체를 생성한다고 아시면 되겠습니다.&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;개인적으로 Hilt 나 Dagger 등 의존성 주입 프레임워크를 사용한다면 직접 Factory 통해서 생성하는 것을 추천하고, 그렇지 않다면 File 의 top level 에서 Delegate 함수 통해 생성 후 싱글톤으로 관리하는 방식을 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646927984932&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Delegate 방식
private val Context.dataStore: DataStore&amp;lt;Preferences&amp;gt; by preferencesDataStore(name = DATASTORE_NAME)

// Factory 방식
PreferenceDataStoreFactory.create(
    corruptionHandler = corruptionHandler,
    migrations = produceMigrations(applicationContext),
    scope = scope
) {
    applicationContext.preferencesDataStoreFile(name)
}&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;DataStore&amp;lt;Preferences&amp;gt; 객체를 생성했다면, 이번에는 간단하게 read &amp;amp; write 를 해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 예제 코드는 간단하게 이름과 나이를 갖고 있는 User 객체를 정의해서 사용해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646928134389&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User(
    val name: String? = &quot;&quot;,
    val age: Int? = 0
)&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;먼저 Key-Value 중 Key 를 정의해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646928198108&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val NAME_KEY = stringPreferencesKey(&quot;name&quot;)
val AGE_KEY = intPreferencesKey(&quot;age&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key 를 사용하는 것에서부터 SharedPreferences 와 큰 차이점이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedPreferences 에서는 Key 의 타입이 String 이었던 것과는 달리, DataStore 에서는 Preferences.Key&amp;lt;T&amp;gt; 타입이어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위와 같이 DataStore 패키지 안에 정의되어 있는 XXXPreferencesKey(String) 함수를 통해 원하는 타입에 맞는 키로 변환해주어야 합니다.&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;이렇게 구현했을 때의 장점은 Key 에 타입이 설정됨으로써 value 가 읽기/쓰기 과정에서 다른 타입으로 다뤄질 경우 컴파일 에러를 통해 개발자가 사전에 문제를 알 수 있게 합니다.&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;이제 Key 가 준비되었으니, 먼저 읽기를 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646928913363&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val userPreferencesFlow: Flow&amp;lt;User&amp;gt; = context.dataStore.data
    .catch { exception -&amp;gt;
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }
    .map { preferences -&amp;gt;
        val name: String = preferences[NAME_KEY] ?: &quot;&quot;
        val age: Int = preferences[AGE_KEY] ?: 0
        User(name, age)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시면 DataStore&amp;lt;Preferences&amp;gt; 타입의 dataStore 변수의 data 프로퍼티를 통해 데이터를 읽어올 수 있고, catch() 를 통해 에러를 핸들링 하고, map() 을 통해 원하는 객체로 변환도 할 수 있습니다. 이외에도 Flow 와 관련된 다양한 함수를 통해 풍부하게 활용 가능하다고 할 수 있습니다.&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;참고로 개발을 하다보면, 캐싱된 값을 사용하거나 스냅샷을 활용하고자 하는 니즈가 있을 수 있습니다. 주의할 것은 이때 StateFlow 로 감싸기 보다는 first() 통해서 스냅샷을 뜨기를 권장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646929258132&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Don&amp;rsquo;t
suspend fun fetchCachedPrefs(scope: CoroutineScope): StateFlow&amp;lt;Preferences&amp;gt; = dataStore.data.stateIn(scope)

// Do
suspend fun fetchInitialPreferences(): Preferences = dataStore.data.first().toPreferences()&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;이번에는 쓰기(Write)를 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 쓰기는 edit() 함수를 통해 할 수 있습니다. 내부적으로 구현을 살펴보면 Immutable 한 Preferences 객체를 MutablePreferences 로 변환해서 write 하는데요. 이때 주의할 점은 데이터 쓰기는 edit() 을 통해 하기만을 권장합니다. 개발자가 임의로 toMutablePreferences() 로 변환해서 put 한다 해도 반영이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, edit() 함수는 suspend function 이기 때문에 CoroutineScope 내에서나 suspend function 내에서 호출되어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646929501890&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun updateUserName(name: String) {
    context.dataStore.edit { preferences -&amp;gt;
        preferences[NAME_KEY] = name
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 NAME_KEY 가 Key&amp;lt;String&amp;gt; 타입으로 선언되어있기 때문에 name 자리에 다른 타입(예를들어 Int)의 데이터가 들어올 경우에는 컴파일 에러가 발생합니다.&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;DataStore 의 Read, Modify, Write 작업은 모두 Atomic 하게 수행되기 때문에, 데이터 일관성을 보장하고 경쟁 상태(race condition)을 예방합니다.&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;Proto DataStore&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 구조화된 데이터 타입(Typed Object)으로 데이터를 읽고 쓸 수 있는 Proto DataStore 를 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proto DataStore 를 사용하기 위해서는 Protocol Buffer 를 알아야 하는데, 잘 모르시는 분들은 &lt;a href=&quot;https://developers.google.com/protocol-buffers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문서&lt;/a&gt;를 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, main/proto 디렉토리에 *.proto 파일을 만들고 스키마를 정의합니다. (Enum 도 정의 가능합니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1646929949233&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// user_prefs.proto
syntax = &quot;proto3&quot;;

option java_package = &quot;com.reddy.datastoresample&quot;;

message UserData {
  string name = 1;
  int32 age = 2;
}&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;a href=&quot;https://github.com/google/protobuf-gradle-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;protobuf-gradle-plugin&lt;/a&gt; 을 사용하면 컴파일 단계에서 위에서 정의한 proto 파일의 스키마에 따라 클래스를 생성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 해당 클래스로 직렬화/역직렬화 할 수 있도록 Serializer 인터페이스 구현체를 정의해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646930384074&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object UserDataSerializer : Serializer&amp;lt;UserData&amp;gt; {
    override val defaultValue: UserData = UserData.newBuilder()
        .setName(&quot;reddy&quot;)
        .setAge(29)
        .build()

    @Suppress(&quot;BlockingMethodInNonBlockingContext&quot;)
    override suspend fun readFrom(input: InputStream): UserData {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException(&quot;Cannot read proto.&quot;, exception)
        }
    }

    @Suppress(&quot;BlockingMethodInNonBlockingContext&quot;)
    override suspend fun writeTo(t: UserData, output: OutputStream) = t.writeTo(output)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 주의할 점은, protobuf 플러그인에서 생성된 객체(위 예에서는 UserData)는 생성자가 private 으로 정의되어 있고 불변 객체이므로 값을 변경하기 위해서는 newBuilder() 를 호출하여 빌더를 통해서 새로운 객체로 만들어주는 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646930597988&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val Context.userDataStore: DataStore&amp;lt;UserData&amp;gt; by dataStore(
    fileName = DATASTORE_NAME,
    serializer = UserPreferencesSerializer,
    produceMigrations = { context -&amp;gt;
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = PREFERENCES_NAME
            ) { sharedPrefs: SharedPreferencesView, currentData: UserData -&amp;gt;
                val name = sharedPrefs.getString(NAME_KEY, &quot;&quot;)
                val age = sharedPrefs.getInt(AGE_KEY, 0)
                if (!name.isNullOrBlank() &amp;amp;&amp;amp; age &amp;gt; 0) {
                    currentData.toBuilder().setName(name).setAge(age).build()
                } else {
                    currentData
                }
            }
        )
    }
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proto DataStore 는 DataStore&amp;lt;UserData&amp;gt; 타입으로 선언할 수 있으며, 위임(Delegate) 함수인 dataStore() 통해서 생성할 수 있습니다. 이때 Preferences DataStore 와는 다르게 serializer 를 등록해줘야 합니다.&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;그리고 위 예시에서는 produceMigrations 파라미터를 통해 SharedPreferences 에서 DataStore 로 데이터 마이그레이션 하는 예시입니다. DataStore 는 위와 같이 produceMigrations 파라미터에 List&amp;lt;DataMigration&amp;lt;T&amp;gt;&amp;gt; 타입의 값을 넘기면서 다양한 형태의 마이그레이션 객체를 넘길 수 있는데요. DataStore 패키지에서는 대표적으로 SharedPreferencesMigration 을 제공하고 있는데, 이 객체는 SharedPreferences 에서 DataStore 로 마이그레이션을 방법을 제공합니다.&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;이제 Proto DataStore 의 데이터를 읽고 쓰는 예시를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 read &amp;amp; write 를 한 번에 작성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1646930973055&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserPreferencesRepository(private val userPreferencesStore: DataStore&amp;lt;UserPreferences&amp;gt;) {
    val userPreferencesFlow: Flow&amp;lt;UserPreferences&amp;gt; = userPreferencesStore.data
        .catch { exception -&amp;gt;
            // error handling
        }

    // Don't
    suspend fun fetchCachedPrefs(scope: CoroutineScope): StateFlow&amp;lt;UserPreferences&amp;gt; =
        userPreferencesStore.data.stateIn(scope)

    // Do
    suspend fun fetchInitialPreference() = userPreferencesStore.data.first()

    // edit() 을 사용하던 Preferences DataStore 와는 달리 updateData() 를 사용.
    suspend fun updateAge(age: Int) {
        userPreferencesStore.updateData { currentPreferences -&amp;gt;
            currentPreferences.toBuilder().setAge(30).build()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Preferences DataStore 에서 보신 것과 크게 사용성이 다르지 않아 어렵지 않게 느껴지실텐데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈여겨 볼 점이 있다면 쓰기(Write) 할 때 edit() 함수를 호출해야 했던 Preferences DataStore 와는 달리 updateData() 라는 이름의 함수를 사용해야 한다는 점입니다.&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;이렇게 해서 DataStore 의 등장 배경과 SharedPreferences 와 DataStore 의 비교, 그리고 DataStore 의 간단한 예시까지 살펴봤는데요. SQLite 가 Room 의 등장 이후 역사속으로 사라져가는 것처럼 SharedPreferences 도 점차 DataStore 가 안정화 됨에 따라 사라지지 않을까 싶습니다.&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>android datastore</category>
      <category>android datastore example</category>
      <category>android preferences datastore</category>
      <category>android proto datastore</category>
      <category>android protobuf</category>
      <category>android protocol buffer</category>
      <category>android sharedpreferences</category>
      <category>sharedpreferences datastore</category>
      <category>sharedpreferences datastore migration</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/215</guid>
      <comments>https://readystory.tistory.com/215#entry215comment</comments>
      <pubDate>Fri, 11 Mar 2022 01:57:01 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Jetpack Compose 를 도입하기 전에 알아야할 6가지</title>
      <link>https://readystory.tistory.com/211</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose&quot;&gt;Jetpack Compose&lt;/a&gt;는 구글에서 개발한, 모던한 안드로이드 개발을 위한 UI Toolkit 입니다. 컴포즈는 기존의 View System 대비 보다 적은 코드로 네이티브 UI 를 구축할 수 있게 해줍니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;최근 개발 생태계에서는 선언형 프레임워크가 주목받고 있습니다. React, SwiftUI, Flutter, Jetpack Compose 등 다양한 플랫폼에서 마크업이 아닌 선언형으로 UI 를 구축하는 방식에 대해 채택하고 있는 것이 그 증거입니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;안드로이드는 태생부터 xml 기반의 레이아웃 구성을 기반으로 UI 를 구축해왔기 때문에 새로운 패러다임인 Jetpack Compose 로 전환하는 것이 꽤나 큰 변화인데요. 만약 Jetpack Compose 로의 마이그레이션을 검토하고 계시다면, 혹은 컴포즈에 대해 학습을 준비하고 계시다면 몇 가지 알아둬야 할 사항들이 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;1. 실제 제품에 적용 가능할까?&lt;/h3&gt;
&lt;p&gt;사실 2021년 초까지만 해도 Jetpack Compose 에 대한 사람들의 평가는 &amp;quot;실무에 적용하기엔... 몇 년은 걸릴 것 같다.&amp;quot; 였습니다. 당시에는 아직 alpha 버전이기도 했고, 안정성 문제나 관련 레퍼런스 문서의 부재 등 복잡한 앱 서비스를 제공하고 있는 기업에서 도입하기에는 다소 부담스러운 것이 사실이었습니다.&lt;/p&gt;
&lt;p&gt;그러나 2021년 8월에 1.0.0 스테이블 버전이 정식 출시 되면서, 공식적으로는 프로덕션에 적용 가능한 수준으로 준비를 마쳤습니다. 그러나 여전히 experimental API 도 많이 있고, 컴포즈와 관련된 개발론이나 설계 방식 등에 대한 정보가 부족하기에 규모가 큰 프로젝트를 마이그레이션 하기에는 여전히 부담스럽습니다. 그러나 신규 프로젝트라면 개발 기간 등을 잘 고려했을 때 충분히 컴포즈를 도입할만 하다 생각이 들고, 실제로 그런 팀들을 심심치 않게 볼 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;2. Java 와 사용 가능할까?&lt;/h3&gt;
&lt;p&gt;Jetpack Compose 는 Kotlin only 입니다. 최근에는 많은 기업과 많은 앱들이 코틀린을 주력 언어로 작성되고 관리되고 있습니다만, 많은 프로젝트에서 여전히 자바를 혼용해서 사용하고 있을텐데요. 안타깝게도 컴포즈는 코루틴이나 &lt;code&gt;@Composable&lt;/code&gt; 어노테이션이 오직 코틀린 컴파일러에 의해서만 해석되고 동작하기 때문에 자바에서는 사용할 수 없습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;3. 어떤 것들이 준비 되어야 할까?&lt;/h3&gt;
&lt;p&gt;Jetpack Compose 를 사용하기 위해서는 아래의 환경을 최소로 요구합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Min SDK Version&lt;ul&gt;
&lt;li&gt;Android 5.0 (API 21)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Android Studio Arctic Fox(2020.3.1)&lt;/li&gt;
&lt;li&gt;Android X&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 필수는 아니지만 컴포즈는 Material Design 에 대해서도 커스텀 구현이 제공되고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;4. 퍼포먼스나 빌드 속도, 앱 용량 등 영향이 있을까?&lt;/h3&gt;
&lt;p&gt;구글은 Jetpack Compose 를 발표하면서, 기존의 View System 보다 Compose 가 더 성능도 좋고 시간도 단축할 수 있다고 했습니다. 그렇다면 정말 그런지, 아래 지표를 통해 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;깃헙에 공개 되어 있는 안드로이드 프로젝트 중 &lt;a href=&quot;https://github.com/chrisbanes/tivi/&quot;&gt;tivi&lt;/a&gt; 와 &lt;a href=&quot;https://github.com/android/sunflower/tree/compose_recyclerview&quot;&gt;Sunflower&lt;/a&gt; 를 통해 테스트를 해볼텐데요. tivi 프로젝트는 only view, only compose 에 대해서, Sunflower 프로젝트는 only view, view + compose 에 대해서 각각 빌드 시간과 앱 용량을 비교해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;294&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKTnLo/btrjlPMR0wN/DgXyd2Lf3K5PeyY1S8JFok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKTnLo/btrjlPMR0wN/DgXyd2Lf3K5PeyY1S8JFok/img.png&quot; data-alt=&quot;빌드 속도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKTnLo/btrjlPMR0wN/DgXyd2Lf3K5PeyY1S8JFok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKTnLo%2FbtrjlPMR0wN%2FDgXyd2Lf3K5PeyY1S8JFok%2Fimg.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;294&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드 속도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;316&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6cgr/btrjlQEZWoz/2PyRRgkFnpyyACTVs5w3Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6cgr/btrjlQEZWoz/2PyRRgkFnpyyACTVs5w3Tk/img.png&quot; data-alt=&quot;앱 용량&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6cgr/btrjlQEZWoz/2PyRRgkFnpyyACTVs5w3Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6cgr%2FbtrjlQEZWoz%2F2PyRRgkFnpyyACTVs5w3Tk%2Fimg.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;316&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱 용량&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 결과를 보면 아시겠지만 오직 기존의 View System 만 적용한 케이스보다는 Compose 만 적용한 케이스가 빌드 속도나 앱 용량 측면에서 더 좋은 결과를 가져왔고요. 반면에 기존의 View System 과 Compose 를 혼용한 경우에는 반대로 기존의 View System 보다 부족한 결과를 보이고 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 앱 용량이 중요한 프로젝트의 경우에는 Compose 로 마이그레이션 할 때 유의할 필요가 있겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;5. 기존 프로젝트에서 사용 가능할까?&lt;/h3&gt;
&lt;p&gt;앞서 살짝 언급하기도 했지만, Jetpack Compose 는 기존의 View System 과 완벽하게 상호 운용이 가능하도록 만들어졌습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;quot;Jetpack Compose is designed to work with the established view-based UI approach.&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어쩌면 당연할 수 있겠지만 기존의 View system 과 컴포즈를 함께 사용할 때는 아래의 케이스가 있겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존의 View System UI 에 Compose element 추가하기&lt;/li&gt;
&lt;li&gt;기존의 View System UI 를 Composable function 에 추가하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위와 같은 상호 운용이 가능하기 때문에 자바에서 코틀린으로 전환하던 것처럼 각자의 프로젝트에서 마이그레이션을 목표로 할 때 점진적으로 조금씩 바꿔나가면 되겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;6. 기존에 많이 사용되던 라이브러리와 함께 사용 가능할까?&lt;/h3&gt;
&lt;p&gt;Compose 는 통합을 염두에 두고 제작되었기 때문에, 모든 Jetpack 라이브러리와 함께 사용이 가능합니다.&lt;br&gt;뿐만 아니라 안드로이드 개발에 자주 사용되는 주요 라이브러리들 또한 컴포즈와 함께 사용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지 라이브러리 : Glide, Coil 등&lt;/li&gt;
&lt;li&gt;의존성 주입 : Dagger, Hilt 등&lt;/li&gt;
&lt;li&gt;비동기 처리 : Kotlin Coroutines, Flow 등&lt;/li&gt;
&lt;li&gt;네트워크 : Retrofit, Ktor 등&lt;/li&gt;
&lt;li&gt;애니메이션 : Lottie 등&lt;br&gt;

&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;글의 서두에서 언급한 것처럼 현재 클라이언트 개발에서 UI 를 개발하는 방식에 대해 선언형 UI 가 당분간은 대세로 자리잡을 것 같습니다. 구글에서도 컴포즈에 힘을 많이 싣고 있고, 최근에 안드로이드와 관련된 리포트나 블로그 발행글만 봐도 컴포즈를 주제로 다룬 것들이 대다수일 정도로 굉장히 주목할만한 기술이란 생각이 듭니다.&lt;br&gt;&lt;br&gt;&lt;br&gt;만약 사이드 프로젝트나 소규모의 신규 프로젝트를 기획하고 있으시다면 한 번 쯤은 컴포즈를 통해 개발을 하는 것에 대해 검토해보시길 추천합니다.&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>Android</category>
      <category>Android Compose</category>
      <category>Android Compose Migration</category>
      <category>Android Compose 도입</category>
      <category>Android Compose 마이그레이션</category>
      <category>Android Jetpack</category>
      <category>Android Jetpack Compose</category>
      <category>Android Jetpack Compose Migartion</category>
      <category>Android 컴포즈</category>
      <category>안드로이드 컴포즈 도입</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/211</guid>
      <comments>https://readystory.tistory.com/211#entry211comment</comments>
      <pubDate>Sat, 30 Oct 2021 01:28:17 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Room + Hilt 사용할 때 초기 데이터 설정하기(pre-populate)</title>
      <link>https://readystory.tistory.com/210</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Room 을 사용하다보면 초기 데이터를 넣어야 하는 경우가 있을 겁니다.&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;Developer 사이트에 의하면 데이터베이스에 미리 값을 채우는 방식으로 assets 과 File system 을 사용하는 방법을 제시하고 있는데요. &lt;a href=&quot;https://developer.android.com/training/data-storage/room/prepopulate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(참고 링크)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경우에는 Room 이 자동으로 테이블을 생성해주는 쿼리 부분을 직접 입력해줘야 합니다. 하지만 테이블 생성은 온전히 프레임워크에 맡긴채 데이터만 넣고 싶은 경우도 있는데요. 이번 포스트에서는 코드를 통해 초기 데이터를 설정하는 방법에 대해 알아보겠습니다.&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;Room 사전 준비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본문에 앞서 예제 작성을 위해 필요한 RoomDatabase 클래스와 Dao, Entity 객체에 대해 먼저 정의하도록 하겠습니다.&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 id=&quot;code_1634304061338&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Database(
    entities = [AlbumEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun albumDao(): AlbumDao
}

@Dao
interface AlbumDao {

    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun addAlbum(item: AlbumEntity)

    @Query(&quot;SELECT * FROM AlbumEntity&quot;)
    suspend fun getAllAlbums(): List&amp;lt;AlbumEntity&amp;gt;
}

@Entity
data class AlbumEntity(
    @PrimaryKey(autoGenerate = true) val id: Int,
    val name: String
) {
    companion object {
        const val DEFAULT_ALBUM_ID = 1
        val DEFAULT_ALBUM = AlbumEntity(DEFAULT_ALBUM_ID, &quot;기본 앨범&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Hilt 를 사용하시는 분이라면 아래와 같이 Module 을 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1634308162139&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@InstallIn(SingletonComponent::class)
@Module
object DBModule {

    @Singleton
    @Provides
    fun provideAppDatabase(
        @ApplicationContext context: Context
    ): AppDatabase = Room
            .databaseBuilder(context, AppDatabase::class.java, &quot;kim_ready.db&quot;)
            .build()

    @Singleton
    @Provides
    fun provideAlbumDao(appDatabase: AppDatabase): AlbumDao = appDatabase.albumDao()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RoomDatabase.Callback&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 DBModule 오브젝트의 정의를 보면, Room.databaseBuilder() 를 통해 우리가 정의한 AppDataBase 객체를 생성해주고 있는 것을 확인할 수 있는데요. 바로 여기 이 Builder 의 addCallback() 메소드를 통해서 우리는 초기 데이터 세팅을 할 수 있습니다.&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;RoomDatabase.Callback 의 정의는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1634308424714&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Callback for {@link RoomDatabase}.
 */
public abstract static class Callback {

    /**
     * Called when the database is created for the first time. This is called after all the
     * tables are created.
     *
     * @param db The database.
     */
    public void onCreate(@NonNull SupportSQLiteDatabase db) {
    }

    /**
     * Called when the database has been opened.
     *
     * @param db The database.
     */
    public void onOpen(@NonNull SupportSQLiteDatabase db) {
    }
    
    /**
     * Called after the database was destructively migrated
     *
     * @param db The database.
     */
    public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db){
    }
}&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;주석을 보시면 아시겠지만, 이 콜백 클래스는 Room Database 를 생성할 때에 각각의 상황에 맞춰 콜백 함수를 전달 받고자 할 때 사용됩니다. 우리는 디비가 생성되는 시점에 초기데이터를 넣을 것이기 때문에 위 3가지 함수 중 onCrate() 함수만을 재정의할 것입니다.&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;Sample code&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 예제 코드를 작성할 때에는 Hilt 의 모듈에서 AppDatabase 의 인스턴스를 생성해주고 있었는데요. 우리는 이번에 AppDatabase 에서 생성되는 AppDatabase 의 DAO 객체를 통해 아이템을 insert 할 것이기 때문에 싱글톤 객체를 Hilt 에 위임하지 않고 직접 정의하는 것으로 수정하도록 하겠습니다.&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;수정된 AppDatabase&amp;nbsp; 의 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1634308655441&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Database(
    entities = [AlbumEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun albumDao(): AlbumDao

    companion object {
        fun getInstance(context: Context): AppDatabase = Room
            .databaseBuilder(context, AppDatabase::class.java, &quot;kim_ready.db&quot;)
            .addCallback(object : Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)

                    Executors.newSingleThreadExecutor().execute {
                        runBlocking {
                            getInstance(context).albumDao().addAlbum(AlbumEntity.DEFAULT_ALBUM)
                        }
                    }
                }
            })
            .build()
    }
}&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;이렇게 작성해준 다음 Hilt 의 DBModule 은 아래와 같이 수정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1634308709060&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@InstallIn(SingletonComponent::class)
@Module
object DBModule {

    @Singleton
    @Provides
    fun provideAppDatabase(
        @ApplicationContext context: Context
    ): AppDatabase = AppDatabase.getInstance(context)

    @Singleton
    @Provides
    fun provideAlbumDao(appDatabase: AppDatabase): AlbumDao = appDatabase.albumDao()
}&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;이렇게 수정 했다면, 앱 실행시 최초에 디비가 생성되는 때에 우리가 정의한 DEFAULT_ALBUM 객체의 데이터가 AlbumEntity 테이블에 정상적으로 잘 셋팅 된 것을 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>Android</category>
      <category>android hilt room</category>
      <category>android room hilt</category>
      <category>android room initial data</category>
      <category>android room initial row</category>
      <category>android room insert initial data</category>
      <category>android room populate</category>
      <category>android room prepopulate</category>
      <category>android room 초기 값</category>
      <category>android room 초기 데이터</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/210</guid>
      <comments>https://readystory.tistory.com/210#entry210comment</comments>
      <pubDate>Fri, 15 Oct 2021 23:40:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Room 을 통해 List 등 다양한 타입 데이터 저장하기(feat. Moshi)</title>
      <link>https://readystory.tistory.com/209</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Room 은 SQLite 에 대한 추상화 레이어를 제공하여 개발자가 보다 편리하게 로컬 데이터베이스에 접근할 수 있게 해주는 Jetpack 라이브러리입니다.&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;또한 Room 은 rxjava 나 paging 라이브러리 등과 연동이 가능한 익스텐션 모듈도 있기 때문에 기존 프로젝트에 해당 라이브러리를 사용하고 있었다면 보다 편리하게 연동할 수도 있습니다.&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;이번 포스팅에서는 Room 에 대한 기본 사용법이 아닌, 다양한 커스텀 클래스나 리스트 등 자료구조 데이터를 어떻게 저장하고 가져올 수 있을지에 대해 다뤄보도록 하겠습니다.&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;미리 그 방법을 말씀드리자면, 데이터를 JSON 형태의 문자열로 Serialization(직렬화) 하여 저장하고, 꺼낼 때는 다시 해당 JSON 형태의 문자열을 원하는 형태의 클래스나 자료구조로 변환하도록 할 건데요. 이를 위해서는 직렬화 라이브러리를 사용하면 편리하기 때문에 okhttp, retrofit 등을 개발한 square 사의 Moshi 라는 라이브러리를 사용하도록 하겠습니다. (Moshi 에 대한 가이드는 &lt;a href=&quot;https://github.com/square/moshi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고하세요.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gson, jackson, kotlinx.serialization 등 다른 직렬화 라이브러리를 사용하시더라도 방법의 차이만 있을 뿐 원리는 같으니 본인의 프로젝트에서 사용하는 다른 직렬화 라이브러리가 있다면 해당 라이브러리로도 충분히 적용 가능합니다.&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;예제 설정하기&lt;/b&gt;&lt;/h3&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 id=&quot;code_1631547399888&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@JsonClass(generateAdapter = true)
data class ChampionInfo (
    @field:Json(name = &quot;id&quot;) @PrimaryKey val id: String = &quot;&quot;,
    @field:Json(name = &quot;name&quot;) val name: String? = null,
    @field:Json(name = &quot;image&quot;) val image: Image? = null,
    @field:Json(name = &quot;tags&quot;) val tags: List&amp;lt;String&amp;gt;? = null,
    @field:Json(name = &quot;skins&quot;) val skins: List&amp;lt;Skin&amp;gt;? = null
)

@JsonClass(generateAdapter = true)
data class Image(
    @field:Json(name = &quot;filaName&quot;) val fileName: String
)

@JsonClass(generateAdapter = true)
data class Skin(
    @field:Json(name = &quot;num&quot;) val num: Int,
    @field:Json(name = &quot;name&quot;) val name: String
)&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;위 ChampionInfo 클래스는 하나의 Entity 로써, 하나의 테이블입니다. 그리고 그 안에 name, image, tags, skins 등 다양한 필드가 정의되어 있는데요. 커스텀 클래스인 Image 와 리스트 형태의 List&amp;lt;String&amp;gt;, 그리고 리스트 형태의 커스텀 클래스인 List&amp;lt;Skin&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;참고로 @Entity 어노테이션과 @PrimaryKey 어노테이션은 Room 에서 제공하는 어노테이션이고, @JsonClass 와 @field:Json 어노테이션은 Moshi 에서 제공하는 어노테이션입니다.&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;직렬화 / 역직렬화 활용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리는 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, fileName 으로 &quot;A.png&quot; 라는 값을 갖고 있는 Image 객체에 대해 아래와 같이 Json 문자열로 변환 후 그대로 로컬 디비에 ChampionInfo 엔티티의 image 필드에 저장하는 것입니다. 그리고 List 형태의 데이터 또한 마찬가지로 JsonArray 형태의 문자열로 변환 후 해당 문자열을 그대로 필드에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot; colspan=&quot;5&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;ChampionInfo&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;image&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;tags&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;skins&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;100&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;Akali&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;{&quot;fileName&quot; : &quot;A.png&quot;}&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;[&quot;&lt;span style=&quot;color: #000000;&quot;&gt;Ninja&lt;/span&gt;&quot;, &quot;&lt;span style=&quot;color: #000000;&quot;&gt;Assassin&lt;/span&gt;&quot;]&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;[{&quot;num&quot;: 0, &quot;name&quot;: &quot;default&quot;}, {&quot;num&quot;: 1, &quot;name&quot;: &quot;tinger Akali&quot;}]&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;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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@ProvidedTypeConverter, @TypeConverter 활용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room 에서 제공하는 &lt;b&gt;@ProvidedTypeConverter&lt;/b&gt; 를 사용하여 타입 컨버터를 정의하고 &lt;b&gt;@TypeConverter&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;이때, 직렬화 - 역직렬화에 Moshi 에서 생성해준 Adapter 를 사용하는데요. 각자 편의에 맞게 gson, jackson 등 다른 직렬화 라이브러리를 여기서 적용하시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1631546101659&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ProvidedTypeConverter
class ImageTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): Image? {
        val adapter: JsonAdapter&amp;lt;Image&amp;gt; = moshi.adapter(Image::class.java)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: Image): String {
        val adapter: JsonAdapter&amp;lt;Image&amp;gt; = moshi.adapter(Image::class.java)
        return adapter.toJson(type)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Image 클래스를 직렬화/역직렬화 하는 컨버터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631546920154&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ProvidedTypeConverter
class StringListTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): List&amp;lt;String&amp;gt;? {
        val listType = Types.newParameterizedType(List::class.java, String::class.java)
        val adapter: JsonAdapter&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = moshi.adapter(listType)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: List&amp;lt;String&amp;gt;): String {
        val listType = Types.newParameterizedType(List::class.java, String::class.java)
        val adapter: JsonAdapter&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = moshi.adapter(listType)
        return adapter.toJson(type)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;List&amp;lt;String&amp;gt; 형태의 데이터를 직렬화/역직렬화 하는 컨버터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631546966810&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ProvidedTypeConverter
class SkinListTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): List&amp;lt;Skin&amp;gt;? {
        val listType = Types.newParameterizedType(List::class.java, Skin::class.java)
        val adapter: JsonAdapter&amp;lt;List&amp;lt;Skin&amp;gt;&amp;gt; = moshi.adapter(listType)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: List&amp;lt;Skin&amp;gt;): String {
        val listType = Types.newParameterizedType(List::class.java, Skin::class.java)
        val adapter: JsonAdapter&amp;lt;List&amp;lt;Skin&amp;gt;&amp;gt; = moshi.adapter(listType)
        return adapter.toJson(type)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;List&amp;lt;Skin&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;위와 같이 컨버터들을 모두 준비했다면, 이제 아래와 같이 본인이 정의한 RoomDataBase 구현 객체를 생성하는 단계에서 빌더에 컨버터를 등록해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631547410547&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory()) // 코틀린에서 JsonAdapter 를 생성하기 위한 팩토리 객체
        .build()
        
val appDatabase = Room
        .databaseBuilder(context, AppDatabase::class.java, &quot;YOUR_DB_NAME&quot;) // AppDatabase::class.java 대신 본인이 정의한 RoomDatabase 의 하위 클래스를 설정해주면 됩니다.
        .addTypeConverter(ImageTypeConverter(moshi))  // image 를 위한 컨버터
        .addTypeConverter(StringListTypeConverter(moshi))  // List&amp;lt;String&amp;gt; 을 위한 컨버터
        .addTypeConverter(SkinListTypeConverter(moshi))  // List&amp;lt;Skin&amp;gt; 을 위한 컨버터
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제를 실제로 사용하여 Hilt 와 함께 적용한 샘플 프로젝트가 궁금하다면 제가 작성한 &lt;a href=&quot;https://github.com/KimReady/LOL-Champs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;샘플 프로젝트의 Github&lt;/a&gt; 을 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되었다면 저장소에 Star&lt;span style=&quot;color: #8b949e;&quot;&gt;⭐&lt;/span&gt; 도 꾹 눌러주시면 감사하겠습니다 :)&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>android room</category>
      <category>android room array</category>
      <category>android room converter</category>
      <category>android room custom class</category>
      <category>android room entity</category>
      <category>android room field</category>
      <category>android room list</category>
      <category>android room list converter</category>
      <category>android room moshi</category>
      <category>android room typeconverter</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/209</guid>
      <comments>https://readystory.tistory.com/209#entry209comment</comments>
      <pubDate>Tue, 14 Sep 2021 00:35:41 +0900</pubDate>
    </item>
    <item>
      <title>[Android] DataBinding + StateFlow + Sealed class 예제</title>
      <link>https://readystory.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Android Studio Arctic fox 버전부터 AAC DataBinding 에 Kotlin Coroutines 에서 제공하는 StateFlow 를 결합할 수 있게 되었습니다. 이로 인해 코틀린 코루틴을 사용하여 개발하던 개발자는 이제 LiveData 에 대한 의존성 없이도 데이터바인딩을 사용하는 것이 가능해졌습니다.&lt;br /&gt;&lt;br /&gt;LiveData 와 StateFlow 에 대한 비교에 대해서는 제가 이전에 작성한 글을 참고하시면 되겠습니다.&lt;br /&gt;&lt;a href=&quot;https://readystory.tistory.com/207&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[Android] LiveData VS StateFlow, 왜 StateFlow 를 써야 할까?&lt;/span&gt;&lt;/a&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;b&gt;StateFlow&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateFlow 는 굉장히 심플한 컨셉을 갖고 있습니다. 말 그대로 하나의 상태 값을 반드시 들고 있는 인터페이스라고 보시면 되는데요. 실제 내부 구현도 아래와 같이 간단하게 구현되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface StateFlow&amp;lt;out T&amp;gt; : SharedFlow&amp;lt;T&amp;gt; { 
    /** * The current value of this state flow. */
    public val value: T 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;SharedFlow 에 대해서는 추후 이벤트 처리와 관련하여 별도 포스팅으로 다루도록 하겠습니다.&lt;br /&gt;위 StateFlow 는 기본적으로 read-only 이기 때문에 값을 수정하기 위해서는 MutableStateFlow 로 선언하여 사용하면 되겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class CounterModel {
    private val _counter = MutableStateFlow(0) // private mutable state flow
    val counter = _counter.asStateFlow() // publicly exposed as read-only state flow 
    
    fun inc() {
        _counter.value++
    }
}&lt;/code&gt;&lt;/pre&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;DataBinding&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AAC DataBinding 은 레이아웃과 데이터 사이의 다리 역할을 하는 프레임워크입니다. 데이터바인딩은 개발자가 작성해야 하는 보일러 플레이트 코드를 확연히 줄여주고, 개발자로 하여금 데이터를 처리하는 로직에만 집중할 수 있게 해 줘 생산성을 향상해줍니다.&lt;br /&gt;&lt;br /&gt;데이터바인딩은 lifecycle 을 알고 있기 때문에 UI 가 화면에 보이는 경우에만 트리거 됩니다.&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;DataBinding With StateFlow&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 StateFlow 와 DataBinding 을 함께 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class ViewModel() {
    val username: StateFlow&amp;lt;String&amp;gt;
} 


//code in xml
&amp;lt;TextView 
    android:id=&quot;@+id/name&quot;
    android:text=&quot;@{viewmodel.username}&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;보시다시피 LiveData 를 사용했을 때와 큰 차이점은 없어보입니다.(Two-way 바인딩도 마찬가지입니다.)&lt;br /&gt;따라서 레이아웃에서 어떻게 처리하냐 보다는 StateFlow 를 어떻게 구성하는지 잘 아는 것이 관건인 것 같습니다.&lt;br /&gt;&lt;br /&gt;관련해서는 StateFlow 가 결국 코루틴 플로우의 하위 타입이기 때문에 Flow 와 관련하여 제공되는 다양한 API 를 활용하면 좋습니다. &lt;a href=&quot;https://kotlinlang.org/docs/flow.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;(참고)&lt;/span&gt;&lt;/a&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;b&gt;sealed class 와 함께 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 조금 심화된 버전으로 sealed 클래스를 활용하여 하나의 StateFlow 객체를 통해 여러 가지 상태의 데이터를 처리하는 예제에 대해 살펴보겠습니다.&lt;br /&gt;&lt;br /&gt;예제는 제가 리그오브레전드 API 를 통해 작성한 샘플 앱을 기반으로 가져왔습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;sealed class UiState {
    object Loading: UiState()
    data class Success&amp;lt;T&amp;gt;(val data: T): UiState()
    data class Error(val error: Throwable?): UiState() 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UiState 클래스는 Loading, Success, Error 3가지 상태가 있습니다. &lt;br /&gt;&lt;br /&gt;다음은 ViewModel 클래스에서 위에서 정의한 UiState 타입을 갖는 StateFlow 를 선언하겠습니다. Repository 의 세부 구현 사항은 포스트에 포함하지 않고, 전체 코드가 궁금하신 분은 &lt;a href=&quot;https://github.com/KimReady/LOL-Champs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Github 링크&lt;/span&gt;&lt;/a&gt;를 참고해주시면 되겠습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MainViewModel constructor(
    private val mainRepository: MainRepository 
): ViewModel() {

    val uiState: StateFlow&amp;lt;UiState&amp;gt; = mainRepository.getAllChampions() 
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000L),
            initialValue = UiState.Loading
        )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 예제 코드를 보면 stateIn 이라는 함수를 통해 flow builder 를 StateFlow 로 변환해주었고 uiState 의 값을 초기값으로 Loading 값을 설정해준 다음, 가져오는 데이터의 결과에 따라 UiState.Success 나 UiState.Error 값으로 변환해주도록 하였습니다.&lt;br /&gt;&lt;br /&gt;그런 다음 바인딩 어댑터를 아래와 같이 선언하고 레이아웃에 연결하여 주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@BindingAdapter(&quot;show&quot;)
fun ProgressBar.bindShow(uiState: UiState) {
    visibility = if (uiState is UiState.Loading) View.VISIBLE else View.GONE
}

@BindingAdapter(&quot;toast&quot;)
fun View.bindToast(uiState: UiState) {
    if (uiState is UiState.Error) {
        uiState.error?.message?.let { errorMessage -&amp;gt;
            Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() 
        }
    }
}

@BindingAdapter(&quot;championItems&quot;)
fun RecyclerView.bindChampionItems(uiState: UiState) {
    val boundAdapter = this.adapter
    if (boundAdapter is ChampionAdapter &amp;amp;&amp;amp; uiState is UiState.Success&amp;lt;*&amp;gt;) {
        boundAdapter.submitList(uiState.data as List&amp;lt;Champion&amp;gt;)
    }
}&lt;/code&gt;&lt;/pre&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-origin-width=&quot;296&quot; data-origin-height=&quot;592&quot; width=&quot;480&quot; height=&quot;960&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dnh0s/btreDk50WVo/PzxktWyhKAZGkRrOJ71L10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dnh0s/btreDk50WVo/PzxktWyhKAZGkRrOJ71L10/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dnh0s/btreDk50WVo/PzxktWyhKAZGkRrOJ71L10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDnh0s%2FbtreDk50WVo%2FPzxktWyhKAZGkRrOJ71L10%2Fimg.png&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;592&quot; width=&quot;480&quot; height=&quot;960&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드가 궁금하신 분은 &lt;a href=&quot;https://github.com/KimReady/LOL-Champs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Github 저장소&lt;/span&gt;&lt;/a&gt;를 보시면 되겠습니다.&lt;br /&gt;도움이 되었다면 저장소에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;STAR&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;span style=&quot;color: #8b949e;&quot;&gt;⭐&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #8b949e;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 꾹 눌러주세요!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;감사합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>android coroutine databinding</category>
      <category>android databinding</category>
      <category>android databinding example</category>
      <category>android databinding sealed class</category>
      <category>android lol api</category>
      <category>Android MVVM</category>
      <category>android retrofit coroutine</category>
      <category>android stateflow</category>
      <category>android stateflow databinding</category>
      <category>lol api</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/208</guid>
      <comments>https://readystory.tistory.com/208#entry208comment</comments>
      <pubDate>Fri, 10 Sep 2021 14:47:54 +0900</pubDate>
    </item>
    <item>
      <title>[Android] LiveData VS StateFlow, 왜 StateFlow 를 써야할까?</title>
      <link>https://readystory.tistory.com/207</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LiveData 는 Lifecycle 라이브러리 중 하나로, 안드로이드 공통의 라이프사이클과 관련된 문제를 해결할 수 있게 해 주면서 앱 개발시 보다 더 유지보수하기 쉽게, 테스트하기 쉽게 만들어주는 라이브러리입니다.&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;LiveData 는 옵저버 패턴을 활용하여 구현되었으며, 관찰 가능한 일반 클래스인 ObservableXXX 클래스와는 달리 LiveData 는 생명주기의 변화를 인식합니다. 즉, Activity, Fragment, Service 등 안드로이드 컴포넌트의 생명 주기 인식을 통해 Active 상태에 있는 컴포넌트에서만 업데이트합니다.&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;LiveData 를 사용했을 때 장점은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Activity 와 Fragment 는 LiveData 객체를 안전하게 관찰할 수 있고, 생명 주기가 끝나는 즉시 관찰을 멈추기 때문에 누수를 걱정하지 않아도 됩니다.&lt;/li&gt;
&lt;li&gt;LiveData 는 옵저버 패턴을 따르기에 LiveData 는 관찰 대상인 데이터가 변경될 때 Observer 객체에 알립니다. 그리고 이러한 Observer 객체를 통해 UI 를 업데이트한다면 개발자가 직접 업데이트할 필요가 없게 됩니다.&lt;/li&gt;
&lt;li&gt;Activity 가 Back Stack 에 있을 때를 비롯하여 Observer 의 생명 주기가 비활성 상태에 있으면 Observer 는 어떤 LiveData 이벤트도 받지 않아 비정상 종료가 발생하지 않게 됩니다.&lt;/li&gt;
&lt;li&gt;생명 주기가 비활성화(Inactive)되었다가 다시 활성화(Active)될 때 최신 데이터를 수신합니다.&lt;/li&gt;
&lt;li&gt;Room 이나 Retrofit 라이브러리 등과 호환되어 함께 사용하기 좋습니다.&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;위 내용을 봤을 때, LiveData 는 단순히 옵저버 패턴을 통해 데이터의 변화를 관찰하고 UI 를 업데이트하기에 용이하다는 것을 넘어 Activity 나 Fragment 등의 생명주기에 따른 관리도 잘해주는 라이브러리라는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇기에 꼭 MVVM 디자인 패턴이 아니더라도 데이터를 관리하는데에 지금까지도 굉장히 많이 사용되고 있으며, 주로 함께 사용하는 라이브러리로는 AAC DataBinding, AAC ViewModel 등이 있습니다.&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;언뜻 보기에는 LiveData 는 흠잡을 데 없이 좋은 것 같습니다.&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;그렇다면 이번에는 아키텍처 관점에서 LiveData 를 한 번 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mx2J8/btrBtdaYzFE/XihV6fSNTrtSUKdfC6yFHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mx2J8/btrBtdaYzFE/XihV6fSNTrtSUKdfC6yFHk/img.png&quot; data-alt=&quot;Android 관점에서의 Clean Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mx2J8/btrBtdaYzFE/XihV6fSNTrtSUKdfC6yFHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMx2J8%2FbtrBtdaYzFE%2FXihV6fSNTrtSUKdfC6yFHk%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;890&quot; height=&quot;800&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Android 관점에서의 Clean Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 클린 아키텍처를 안드로이드 개발 관점에서 재구성하여 그림으로 표현한 것입니다. 위 그림은 클린 아키텍쳐에 대한 수많은 재해석 중 하나로, 안드로이드 개발의 다양한 관점에서 클린 아키텍처를 정의한 케이스에 대해 보다 더 알고 싶다면 &lt;a href=&quot;https://proandroiddev.com/multiple-ways-of-defining-clean-architecture-layers-bbb70afa5d4a&quot;&gt;여기&lt;/a&gt;를 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처에서 계층 간의 의존성은 한쪽으로만 발생하여야 합니다. 예컨대, Presentation 계층에서는 Domain 계층을 알지만, 그 반대인 Domain 계층은 Presentation 계층을 알면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;한 가지 알아두면 좋은 점은 안드로이드 개발에 자주 사용되는 디자인 패턴인 MVVM 패턴이나 MVP 패턴 등은 Presentation Layer 에 포함되는 패턴입니다. 따라서 MVVM 과 클린 아키텍처는 양자택일의 문제가 아니라 함께 적용이 가능한 부분인 것을 알아두면 좋을 것 같습니다.&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;이번 포스팅에서는 클린 아키텍처에 대한 내용을 주로 다루기보다는, LiveData 와 StateFlow 에 대해 중점적으로 살펴볼 것이니 아키텍처와 관련한 자세한 설명은 생략하고 간단하게만 살펴보도록 하겠습니다.&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;Presentation Layer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면과 입력에 대한 처리 등 UI 와 관련된 부분을 담당합니다. Activity, Fragment, View, ViewModel 등을 포함합니다.&lt;br /&gt;여기에서 Activity 와 Fragment 는 View 의 역할을 하는, 다시 말해 데이터를 소유하는 것이 아니라 데이터를 표시하기만 하는 역할을 하므로 LiveData 인스턴스를 보유해서는 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;LiveData 객체는 주로 AAC ViewModel 에서 관리하게 되는데, 이 또한 Presentation Layer 에 속해 있으니 크게 문제가 될 것은 없어 보입니다.&lt;br /&gt;그리고 Presentation Layer 는 Domain Layer 에 대한 의존성을 가지고 있습니다.&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;Data Layer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain Layer 에 대한 의존성을 갖습니다. Domain 계층의 Repository 인터페이스를 포함하고 있으며, 이에 대한 구현을 Data Layer 에서 하게 됩니다. 그리고 데이터베이스(Local)와 서버(Remote)와의 통신도 Data 계층에서 이루어집니다. 또한 필요하다면 Mapper 클래스를 두어 Data Layer 의 모델을 Domain 계층의 모델로 변환해주는 역할도 이 계층에서 하게 됩니다.&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;Data Layer 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 LiveData 는 비동기 데이터 스트림을 처리하도록 설계되지 않았습니다.&lt;/b&gt; LiveData transfromation 과 MediatorLiveData 등을 통해 이를 처리하게 할 수는 있겠지만, 모든 LiveData 의 관찰은 오직 Main Thread 에서만 진행되기 때문에 한계점을 갖고 있습니다.&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;안드로이드의 교과서인 디벨로퍼 사이트에서도 이에 대해 명시하고 있으며, Repository 에서는 LiveData 를 사용하지 않도록 권장하면서 동시에 Kotlin Flow 를 사용하도록 권장하고 있습니다. (&lt;a href=&quot;https://developer.android.com/topic/libraries/architecture/livedata#livedata-in-architecture&quot;&gt;참고&lt;/a&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;Domain Layer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션의 비즈니스 로직에서 필요한 UseCase 와 Model 을 포함하고 있습니다. UseCase 는 각 개별 기능 또는 비즈니스 논리 단위이며, Presentation Layer, Data Layer 계층에 대한 의존성을 가지지 않고 독립적으로 분리되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Domain 계층은 안드로이드에 의존성을 가지지 않은 순수 Java 및 Kotlin 코드로만 구성합니다.&lt;/b&gt;&lt;/span&gt; 여기에는 Repository 인터페이스도 포함되어 있습니다.&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;LiveData 의 문제는 여기서도 발생합니다. 우리는 너무나도 자연스럽게 이 Domain Layer 에서 LiveData 를 사용하곤 하는데, 만약 계층 별로 멀티 모듈로 프로젝트를 구성하고 있다면 단지 LiveData 만을 위해서 안드로이드 의존성을 가지도록 해야할 수도 있게 됩니다. 그리고 LiveData 는 안드로이드 SDK 에 포함되어 있다보니 단위 테스트를 할 때도 이를 위한 별도의 테스트 지원 모듈을 의존해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;요약을 하자면 클린 아키텍처 관점에서 LiveData 의 문제점은 아래와 같겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LiveData 는 UI 에 밀접하게 연관되어 있기 때문에 Data Layer 에서 비동기 방식으로 데이터를 처리하기에 자연스러운 방법이 없다.&lt;/li&gt;
&lt;li&gt;LiveData 는 안드로이드 플랫폼에 속해 있기 때문에 순수 Java / Kotlin 을 사용해야 하는 Domain Layer 에서 사용하기에 적합하지 않다.&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;안드로이드 개발 언어로 코틀린이 자리 잡기 전까지는 위와 같은 문제점을 안고 있으면서도 안드로이드 진영에서는 별다른 선택권이 없었습니다. 그러나 코틀린 코루틴이 발전하면서, Flow 가 등장하게 되었고 많은 안드로이드 커뮤니티에서는 이 Flow 를 이용해서 LiveData 를 대체할 수 있을지에 대한 기대가 생기기 시작했습니다.&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;그러나 Flow 를 통해 LiveData 를 대체하는 것은 쉬운 일이 아니었습니다.&lt;br /&gt;그 이유는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flow 는 스스로 안드로이드 생명주기에 대해 알지 못합니다. 그래서 라이프사이클에 따른 중지나 재개가 어렵습니다.&lt;/li&gt;
&lt;li&gt;Flow 는 상태가 없어 값이 할당된 것인지, 현재 값은 무엇인지 알기가 어렵습니다.&lt;/li&gt;
&lt;li&gt;Flow 는 Cold Stream 방식으로, 연속해서 계속 들어오는 데이터를 처리할 수 없으며 collect 되었을 때만 생성되고 값을 반환합니다. 만약, 하나의 flow builder 에 대해 다수의 collector 가 있다면 collector 하나마다 하나씩 데이터를 호출하기 때문에 업스트림 로직이 비싼 비용을 요구하는 DB 접근이나 서버 통신 등이라면 여러 번 리소스 요청을 하게 될 수 있습니다.&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;이를 위해 kotlin 1.41 버전에 Stable API 로 등장한 것이 바로 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;code&gt;StateFlow&lt;/code&gt; &lt;/b&gt;&lt;/span&gt;와 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;code&gt;SharedFlow&lt;/code&gt; &lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 StateFlow 특징과 장점까지만 살펴보고, 다음 기회에 SharedFlow 와 함께 다양한 예제를 통해 활용 방법까지 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StateFlow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateFlow 는 현재 상태와 새로운 상태 업데이트를 collector 에 내보내는 Observable 한 State holder flow 입니다. 그리고 LiveData 와 마찬가지로 &lt;b&gt;&lt;code&gt;value&lt;/code&gt; &lt;/b&gt;프로퍼티를 통해서 현재 상태 값을 읽을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;StateFlow 는 SharedFlow 의 한 종류이며, LiveData 에 가장 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징으로는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StateFlow 는 항상 값을 가지고 있고, 오직 한 가지 값을 가집니다.&lt;/li&gt;
&lt;li&gt;StateFlow 는 여러 개의 collector 를 지원합니다. 이는 flow 가 공유된다는 의미이며 앞서 설명했던 flow 의 단점(3)과는 다르게 업스트림이 collector 마다 중복으로 처리되지 않습니다.&lt;/li&gt;
&lt;li&gt;StateFlow 는 collector 수에 관계없이 항상 구독하고 있는 것의 최신 값을 받습니다.&lt;/li&gt;
&lt;li&gt;StateFlow 는 flow builder 를 사용하여 빌드된 flow 가 cold stream 이었던 것과 달리, hot stream 입니다. 따라서 collector 에서 수집하더라도 생산자 코드가 트리거 되지 않고, 일반 flow 는 마지막 값의 개념이 없었던 것과 달리 StateFlow 는 마지막 값의 개념이 있으며 생성하자마자 활성화됩니다.&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;StateFlow 와 LiveData 는 둘 다 관찰 가능한 데이터 홀더 클래스이며, 앱 아키텍쳐에서 사용할 때 비슷한 패턴을 따릅니다. 즉, MVVM 에서 LiveData 사용되는 자리에 StateFlow 로 대체할 수 있습니다. 그리고 Android Studio Arctic fox 버전부터는 AAC Databinding 에도 StateFlow 가 호환됩니다.&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;그러나 StateFlow 와 LiveData 는 다음과 같이 다르게 작동합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StateFlow 의 경우 초기 상태를 생성자에 전달해야 하지만, LiveData 의 경우는 전달하지 않아도 됩니다.&lt;/li&gt;
&lt;li&gt;View 가 &lt;code&gt;STOPPED&lt;/code&gt; 상태가 되면 LiveData.observe() 는 Observer 를 자동으로 등록 취소하는 반면, StateFlow 는 자동으로 collect 를 중지하지 않습니다. 만약 동일한 동작을 실행하려면 &lt;code&gt;Lifecycle.repeatOnLifecycle&lt;/code&gt; 블록에서 흐름을 수집해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제를 통해 간단한 사용방법도 알아보도록 하겠습니다.&lt;br /&gt;예제는 ViewModel 생성 단계에서 Loading 상태로 설정을 해주고, 이어서 비동기 처리를 통해 어떤 결과 값을 받아오면 업데이트해주는 코드를 작성해보겠습니다.&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;먼저 LiveData 를 사용한 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class MyViewModel {
    private val _myUiState = MutableLiveData&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt;(Result.Loading)
    val myUiState: LiveData&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt; = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 동일한 처리를 StateFlow 를 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;class MyViewModel {
    private val _myUiState = MutableStateFlow&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt;(Result.Loading)
    val myUiState: StateFlow&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt; = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네. 사실 LiveData 가 StateFlow 로만 변경됐을 뿐 그다지 차이는 없어 보입니다. 이번에는 조금 다르게 구현하여 비교해보도록 하겠습니다.&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;먼저 LiveData 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;class MyViewModel(...) : ViewModel() {
    val result: LiveData&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt; = liveData {
        emit(Result.Loading)
        emit(repository.fetchItem())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 위 코드에서 작성된 liveData {} 는 코루틴 빌더입니다.&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;이어서 StateFlow 방식도 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class MyViewModel(...) : ViewModel() {
    val result: StateFlow&amp;lt;Result&amp;lt;UiState&amp;gt;&amp;gt; = flow {
        emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
        initialValue = Result.Loading
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 살펴봤던 예제에 비해 이번 예제에서는 LiveData 와 코드가 사뭇 다릅니다. 눈여겨볼 것은 flow builder 에&amp;nbsp; &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;stateIn()&lt;/span&gt;&lt;/b&gt; 함수를 사용하여 Flow 를 StateFlow 객체로 변환해준 것입니다. 그리고 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;stateIn()&lt;/b&gt;&lt;/span&gt; 함수 내부에는 파라미터로 scope, started, initialValue 가 있는데 각각 요구하는 값은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/b&gt; : 공유가 시작되는 Coroutine Scope.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;started&lt;/code&gt;&lt;/b&gt; : 공유가 시작 및 중지되는 시기를 제어하는 전략을 설정하는 파라미터.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Lazily&lt;/code&gt;&lt;/b&gt; : 첫 번째 subscriber 가 나타나면 시작하고, scope 가 취소되면 중지.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;b&gt;Eagerly&lt;/b&gt;&lt;/code&gt; : 즉시 시작되며, scope 가 취소되면 중지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;WhileSubscribed&lt;/code&gt; &lt;/b&gt;: collector 가 없을 때 upstream flow 를 취소. 앱이 백그라운드로 전환되면 취소하게 하는 등의 전략 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;initialValue&lt;/code&gt; &lt;/b&gt;: StateFlow 의 초기 값.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다소 긴 내용이었지만, 우리는 LiveData 가 클린 아키텍처 관점에서 갖고 있는 한계점과 이를 대체하기 위한 StateFlow 의 개념과 특징을 비교하며 살펴보았습니다.&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;마지막으로, LiveData 를 StateFlow 로 사용했을 때 얻게 되는 이점을 정리해보도록 하겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 플랫폼에 종속적이었던 LiveData 와는 달리, StateFlow 는 순수 kotlin 라이브러리이기 때문에 Domain Layer 에서 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;코루틴을 통해 Work Thread 에서도 비용이 많이 드는 데이터 스트림을 처리할 수 있기 때문에 Data Layer 에서 LiveData 를 사용하는 것보다 향상된 성능으로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;StateFlow 는 zip, flatMapMerge 등 다양한 Flow API 를 사용할 수 있기 때문에 LiveData 보다 풍부하게 활용할 수 있습니다.&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;StateFlow 와 SharedFlow 가 등장하고, AAC DataBinding 에 StateFlow 가 지원 가능하게 되면서 장기적으로 LiveData 는 deprecated 되는 것이 아니냐는 소문이 돌고 있는데요. 위와 같은 장점들을 고려해봤을 때 앞으로 계속해서 StateFlow 를 위한 API 가 개발이 되고 관련 라이브러리가 많이 등장하게 된다면 충분히 가능성 있는 일이라는 생각이 듭니다.&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;</description>
      <category>Android/Jetpack</category>
      <category>android clean architecture</category>
      <category>android clean architecture livedata</category>
      <category>android clean architecture stateflow</category>
      <category>android coroutine stateflow</category>
      <category>Android LiveData</category>
      <category>android livedata vs stateflow</category>
      <category>android stateflow</category>
      <category>android stateflow databinding</category>
      <category>kotlin sharedflow</category>
      <category>kotlin stateflow</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/207</guid>
      <comments>https://readystory.tistory.com/207#entry207comment</comments>
      <pubDate>Thu, 26 Aug 2021 03:17:33 +0900</pubDate>
    </item>
    <item>
      <title>코틀린과 재귀호출 이야기② - 메모이제이션(Memoization) 기법</title>
      <link>https://readystory.tistory.com/206</link>
      <description>&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;b&gt;1. 37 + 19 는 얼마일까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답을 구하셨나요? 네. 답은 56 입니다. 아마 다들 암산으로 계산 후에 결과를 도출해 냈을 겁니다.&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;b&gt;2. 37 + 19 는 얼마일까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 질문은 1번과 동일합니다. 1번을 계산하신 분들이라면 2번 질문을 듣고 아마 곧바로 56을 대답할 수 있으실 겁니다. 하지만 2번의 답을 말할 때는 1번처럼 암산으로 계산하지 않으셨을 거예요. 왜냐하면 바로 직전에 이미 계산해둔 값을 기억하고 있었기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서 살펴볼 메모이제이션 기법이란 바로 이러한 방식을 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;즉, 과거에 계산한 식을 불필요하게 다시 계산할 필요 없이 기억하고 있다가 필요할 때 재활용하는 것이지요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 이펙트가 없는 함수(또는 연산)는 입력이 같다면 몇 번을 호출하고 계산하여도 동일한 결과가 나옵니다. 우리는 저장된 값을 사용함으로써 다시 계산하는 것을 피하고 실행을 빠르게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;주의사항으로는, 메모이제이션은 부작용(Side Effect) 없는 순수함수에서만 사용되어야 한다는 것입니다.&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;color: #000000;&quot;&gt;&lt;b&gt;다이나믹 프로그래밍(DP, Dynamic Programming)&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 메모이제이션을 2가지 방법으로 구현해볼 예정입니다. 하나는 Groovy 언어의 라이브러리가 제공해주는 솔루션과 유사하게 만들고, 다른 하나는 코틀린 델리게이션을 이용합니다.&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;첫 번째 문제는 &quot;피보나치 수열&quot; 입니다. 피보나치 수열은 프로그래밍 예제에서 매우 많이 사용됩니다. 그 이유 중 하나는 이 문제가 매우 단순하면서 이해하기 좋고, 문제의 솔루션에만 집중할 수 있기 때문입니다. 이번에 예제로 사용한 이유도 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 코틀린으로 피보나치 수를 찾는 단순한 재귀 함수를 구현해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629099653798&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.system.measureTimeMillis

fun fibonacci(n: Int): Long = when (n) {
  0, 1 -&amp;gt; 1L
  else -&amp;gt; fibonacci(n - 1) + fibonacci(n - 2)
}

fun main() {
  println(measureTimeMillis { fibonacci(40) }) // 약 500 밀리초
  println(measureTimeMillis { fibonacci(45) }) // 약 5.5 초
}&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;fibonacci() 함수는 주어진 값이 2보다 작을 경우 1을 리턴하고, 나머지 경우 2개의 재귀 함수 호출의 결과를 연산해서 결과를 리턴합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 fibonacci(4) 를 호출하면 fibonacci(3) 과 fibonacci(2) 가 실행됩니다. 그리고 fibonacci(3) 이 실행될 때 다시 fibonacci(2) 를 호출하게 됩니다. n 이 작을 경우에는 실행이 빠르지만, n 이 증가함에 따라 실행 시간이 기하급수적으로 증가합니다.&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;위 예제에서도 보시다시피 n 이 40인 경우에 제 PC 기준으로 약 0.5초가 소요된 것에 비해 n 이 45 인 경우에는 약 11배 정도인 5.5 초가 걸렸습니다.&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;관련하여 먼저 Groovy 방식의 메모이제이션 기법을 살펴보겠습니다.&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;Groovy 방식의 메모이제이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리에겐 연산 결과를 저장할 수 있는 방법이 몇 가지 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 만들고 필드나 프로퍼티에 데이터를 캐시하여 클래스의 함수가 그 캐시를 사용하도록 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방법을 사용하려면 클래스를 만들어야 하므로 이미 클래스를 만들고 있는 경우라면 선택해도 좋지만, 우리는 지금 단독 함수를 다루고 있기 때문에 클래스를 만들지 않는 방법으로 살펴보겠습니다.&lt;/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;Groovy 언어에서 메모이제이션은 라이브러리 내부에 구현되어 있습니다. Groovy 에서는 아무 람다 표현 식에서나 memoize() 함수를 호출할 수 있고, memoize() 함수는 저장된 람다를 리턴합니다. 코틀린에서 이와 유사한 접근으로 적용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1629100121660&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T, R&amp;gt; ((T) -&amp;gt; R).memoize(): ((T) -&amp;gt; R) {
  val original = this
  val cache = mutableMapOf&amp;lt;T, R&amp;gt;()
  return { n: T -&amp;gt; cache.getOrPut(n) { original(n) } }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 본론으로 와서, 첫 번째 라인에서 우리는 memoize() 메소드를 제네릭 람다 표현식에 주입(Inject) 했습니다. 제네릭 람다 표현식은 T 타입의 파라미터를 받고, R 타입을 리턴합니다. memoize() 함수의 리턴 타입은 memoize() 가 주입된 메소드의 타입과 같은 타입의 람다 표현식입니다. 다시 말하자면 함수에서 memoize() 를 호출한 결과는 함수의 동일한 시그니처를 가진 함수입니다.&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;memoize() 함수에서 우리는 this 를 로컬 변수 original 에 할당해서 오리지날 함수의 레퍼런스를 저장할 수 있습니다. 그리고 비어있는 cache 를 초기화합니다. 마지막으로 T 타입의 파라미터를 받고, R 타입의 결과를 리턴하는 람다를 리턴합니다. 그리고 리턴된 함수는 결과가 존재하는지 보기 위해 캐시를 확인하고 캐시에 결과가 없다면 연산을 해서 결과를 만들고 리턴하기 전에 저장합니다. 이미 결과가 존재한다면 연산을 건너뛰고 저장된 값을 리턴합니다.&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;memoize() 함수를 사용해서 피보나치수의 연산을 수행해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629100593016&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lateinit var fib: (Int) -&amp;gt; (Long)
fun main() {
  fib = { n: Int -&amp;gt;
    when (n) {
      0, 1 -&amp;gt; 1L
      else -&amp;gt; fib(n - 1) + fib(n - 2)
    }
  }.memoize()

  println(measureTimeMillis { fib(40) })  // 약 0 밀리초
  println(measureTimeMillis { fib(45) })  // 약 0 밀리초
  println(measureTimeMillis { fib(500) })  // 약 1 밀리초
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 충격적입니다. 앞서 메모이제이션을 적용하지 않았던 방식에서는 4초 이상 걸리던 연산이 1 밀리초 미만에서 끝나버리고, 입력이 500인 실행은 1 밀리초 밖에 안 걸렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 작성한 memoize() 함수는 파라미터 하나만 사용하는 모든 함수에서 사용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방식에는 단점이 하나 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;이 방식을 사용하기 위해서는 fib 를 먼저 정의하고, 그 후에 fib 에 람다 표현식을 할당해야 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 lateinit var 으로 지연 초기화를 할 수밖에 없었습니다. 만약 var 로 선언하면서 동시에 초기화를 하려 한다면 fib(n - 1), fib(n - 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;그렇다면 선언과 동시에 초기화를 할 수는 없을까요? 이를 해결하기 위한 방법으로 코틀린은 델리게이트를 가지고 있습니다. 델리게이션을 사용하면 솔루션이 어떻게 바뀔지 살펴보도록 하겠습니다.&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;&lt;a href=&quot;https://readystory.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는 걸까?&lt;/a&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;b&gt;델리게이트를 이용한 메모이제이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 섹션에서 fib = { ... }.memoize() 코드는 변수 fib 를 메모이즈된 람다 표현식으로 변경했습니다. 하지만 코틀린에는 프로퍼티와 지역변수의 접근을 인터셉트할 수 있는 기능이 있습니다. 우리는 델리게이트를 이용해서 프로퍼티와 지역변수에 접근할 수 있습니다.&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;이제는 fib 에 var 대신 val 을 사용할 예정입니다. 우리는 fib 에 할당을 단 한 번만 할 것입니다. 그렇기 때문에 델리게이트에 setValue() 는 필요 없고, getValue() 메소드만 있으면 됩니다.&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;pre id=&quot;code_1629101687098&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.reflect.*

class Memoize&amp;lt;T, R&amp;gt;(val func: (T) -&amp;gt; R) {
  private val cache = mutableMapOf&amp;lt;T, R&amp;gt;()
  operator fun getValue(thisRef: Any?, property: KProperty&amp;lt;*&amp;gt;) = { n: T -&amp;gt;
    cache.getOrPut(n) { func(n) }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 델리게이트는 내부적으로 캐시를 가지고 있고, 오리지날 함수는 func 프로퍼티로 가지고 있습니다. 그리고 getValue() 함수가 값이 캐시에 없을 경우 오리지날 함수를 실행시키는 람다 표현식을 리턴합니다.&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;델리게이션을 활용한 솔루션과 Groovy 방식의 솔루션은 유사한 컨셉을 사용합니다. 하지만 이번 솔루션엔 이전 솔루션에 비해서 fib 함수가 아주 다르게 적용되었습니다. fib 함수를 만들 때 델리게이션을 적용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1629101877391&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val fib: (Int) -&amp;gt; Long by Memoize {n: Int -&amp;gt;
  when (n) {
    0, 1 -&amp;gt; 1L
    else -&amp;gt; fib(n - 1) + fib(n - 2)
  }
}&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;b&gt;이제는 Groovy 방식과는 다르게, 선언과 초기화를 동시에 할 수 있게 됩니다. 그 이유는 내부적으로 우리가 fib 변수에 직접 접근하는 게 아니고 위임을 통해서 접근하기 때문입니다.&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;이전 버전만큼의 성능이 좋은지 확인하기 위해서 이번 버전의 fib() 도 실행해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629102290983&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    println(measureTimeMillis { fib(40) })  // 약 0 밀리초
    println(measureTimeMillis { fib(45) })  // 약 0 밀리초
    println(measureTimeMillis { fib(500) })  // 약 1 밀리초
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과. Groovy 방식의 메모이제이션 솔루션과 비슷한 결과를 얻는 것을 확인했습니다.&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;지금까지 메모이제이션을 생성하는 두 가지 방법을 보았습니다. 둘 다 비슷한 방식으로 구현이 가능하지만, Groovy 방식의 함수 호출 솔루션에 비해서 델리게이션 솔루션이 더 깔끔하고 적은 노력으로 메모이제이션을 적용할 수 있었습니다.&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;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 크기가 큰 문제에서는 재귀를 사용할 수 없습니다. 스택 오버플로우 에러가 발생할 가능성이 있기 때문입니다. 코틀린은 tailrec 을 제공해서 재귀 호출을 사용하는 코드에 꼬리 재귀 최적화를 제공해줍니다. 코틀린이 내부적으로 재귀를 반복으로 바꿔주어 개발자는 스택 오버플로우를 걱정할 필요 없이 재귀의 힘을 사용할 수 있습니다.&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;그리고 코틀린이 메모이제이션 함수를 직접 제공해주진 않지만, 연산 결과를 기억하거나 캐싱하는 기능을 만들 수 있습니다. 이런 접근방법을 사용해 다이나믹 프로그래밍이라고 불리는 알고리즘 기법으로 프로그램의 연산 시간을 크게 줄일 수 있습니다. Groovy 방식으로 함수 레벨에서 메모이제이션을 구하는 방법과 델리게이트 기반으로 메모이제이션을 구현하는 방법도 확인했습니다.&lt;/p&gt;</description>
      <category>Kotlin</category>
      <category>kotlin dp</category>
      <category>kotlin dynamic programming</category>
      <category>kotlin memoization</category>
      <category>kotlin recursion</category>
      <category>kotlin recursive function</category>
      <category>코틀린 다이나믹 프로그래밍</category>
      <category>코틀린 메모이제이션</category>
      <category>코틀린 재귀</category>
      <category>코틀린 재귀함수</category>
      <category>코틀린 재귀호출</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/206</guid>
      <comments>https://readystory.tistory.com/206#entry206comment</comments>
      <pubDate>Mon, 16 Aug 2021 18:10:14 +0900</pubDate>
    </item>
    <item>
      <title>코틀린과 재귀호출 이야기① - tailrec 을 활용한 꼬리호출 최적화(Tail Call Optimization)</title>
      <link>https://readystory.tistory.com/205</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 하위 문제의 솔루션을 사용해서 문제에 대한 솔루션을 공식화할 수 있다면 재귀(recursion)로 구현할 수 있습니다. 재귀는 분명 매력적이긴 하지만, 조금만 복잡해져도 코드를 파악하기가 쉽지 않습니다.&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;재귀로 큰 문제를 해결할 때는 런타임 스택 오버플로우에 빠져서 효율성이 떨어질 수 있습니다. 그래서 개발자는 이러한 문제에 빠지지 않기 위해 꼬리 호출 최적화(tail call optimization) 이라고 불리는 테크닉을 이용할 수 있습니다. 거기에 데이터를 저장하는 알고리즘을 사용하면 성능은 더욱 향상되는데요. 아쉽게도 코틀린은 빌트인 메모이제이션(memoization) 을 지원해주지는 않지만 코틀린의 풍부한 문법을 활용한다면 표현력이 강한 메모이제이션 기능을 쉽게 만들 수 있습니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재귀의 강점과 위험성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀를 사용하면 우리는 분할정복 기법(divide and conquer)을 사용할 수 있습니다. 분할정복 기법이란, 문제를 해결할 때 문제를 작게 쪼개서 각 부분의 솔루션을 구현한 후 각 결과를 다시 합쳐서 해결하는 기법을 말합니다.&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;pre id=&quot;code_1629089420117&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun quickSort(numbers: List&amp;lt;Int&amp;gt;): List&amp;lt;Int&amp;gt; =
  if (numbers.isEmpty()) {
    numbers
  } else {
    val pivot = numbers.first()
    val tail = numbers.drop()
    val lessOrEqual = tail.filter { e -&amp;gt; e &amp;lt;= pivot }
    val larger = tail.filter { e -&amp;gt; e &amp;gt; pivot }
    quickSort(lessOrEqual) + pivot + quickSort(larger)
  }

fun main() {
  println(quickSort(listOf(12, 5, 15, 12, 8, 19)))  // [5, 8, 12, 12, 15, 19]
}&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;quickSort() 함수는 주어진 입력을 두 파트로 분리하고 두 파트를 각각 정렬합니다. 그리고 마지막으로 두 솔루션을 합쳐서 전체 솔루션을 만듭니다. 코틀린에서는 일반적인 재귀를 쉽게 지원합니다. 하지만 일반적인 재귀함수에는 리턴 타입이 필요합니다. 그리고 일반적인 재귀함수를 사용할 때는 타입 추론을 사용할 수 없습니다.&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;/p&gt;
&lt;pre id=&quot;code_1629089804427&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.math.BigInteger

fun factorialRec(n: Int): BigInteger =
  if (n &amp;lt;= 0) 1.toBigInteger() else n.toBigInteger() * factorialRec(n - 1)
  
fun main() {
  println(factorialRec(5))  // 120
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;factorialRec() 함수는 0보다 작거나 같은 값을 받으면 1을 리턴하고, 0보다 큰 값을 받으면 주어진 입력을 스스로 재귀적으로 호출하여 그 결과를 곱해서 리턴합니다.&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;pre id=&quot;code_1629089908707&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.math.BigInteger

fun factorialIterative(n: Int): BigInteger =
  (1..n).fold(BigInteger(&quot;1&quot;)) { product, e -&amp;gt; product * e.toBigInteger() }
  
fun main() {
  println(factorialIterative(5))  // 120
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fold() 함수는 인자로 람다와 함께 초기값을 받는다는 점만 제외하면 reduce() 함수와 비슷한 함수입니다. 사람마다 차이가 있겠지만 일반적으로 개발자들이 코드를 파악하고 받아들이기에는 재귀 솔루션이 좀 더 좋을 것입니다.&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;예를 들어보겠습니다. 위에서 작성한 factorialIterative() 와 factorialRec() 함수에 아주 큰 값을 처리하도록 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629090177155&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  println(factorialIterative(50000))  // OK
  println(factorialRec(50000))  // Runtime Error : StackOverflow !
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀를 이용한 솔루션이 이해하기에는 더 쉬울 수 있어도, 문제 풀이에는 적절하지 않습니다. 그래서 성능을 최우선으로 하는 알고리즘 문제 풀이 등에서는 재귀는 웬만하면 사용하지 않으려 하기도 합니다.&lt;/p&gt;
&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;이제 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;꼬리호출 최적화(tail call optimization)&lt;/b&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;b&gt;꼬리호출 최적화(Tail Call Optimization)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 작성한 코드는 프로시저가 되고 생성된 바이트 코드는 결국 실행이 된다는 사실을 기억해봅시다. factorialIterative() 함수는 반복을 사용한 프로시저입니다. 그리고 반복을 사용하는 프로세스로 컴파일되고 실행될 것입니다.&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;이와 유사하게 factorialRec() 는 재귀 프로시저고 재귀 프로세스로 컴파일되고 실행될 것입니다. 하지만 여기서 반전이 있는데요. [컴퓨터 프로그램의 구조와 해석] 책에 의하면 실제로는 재귀 프로시저는 반복 프로세스로 컴파일될 수 있다는 것입니다. 이 말이 사실이라면 우리는 코드를 이해하기 쉬운 재귀로 작성하고, 런타임 시에는 보다 효율적인 반복으로 행동하게 할 수 있을 것입니다.&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;그리고 이 것이 가능하게 해주기 위해 코틀린은 tailrec 키워드를 제공해주고 있습니다. tailrec 을 사용한다면 우리는 코틀린 컴파일러에게 해당 함수를 반복을 사용한 프로시저로 처리하게 지시할 수 있습니다.&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;그렇다면 이제 factorialRec() 함수를 다시 작성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629090579498&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.math.BigInteger

tailrec fun factorialRec(n: Int): BigInteger =
  if (n &amp;lt;= 0) 1.toBigInteger() else n.toBigInteger() * factorialRec(n - 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 작성했던 factorialRec() 함수 맨 앞에 tailrec 이라는 키워드를 붙여줬습니다. 이러면 된 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않나 봅니다. 실행해봤더니 정상 동작하지 않습니다. 자세히 살펴보니 코틀린은 우리에게 &quot;a function is marked as tial-recursive but no tial calls are found&quot; 라고 경고를 해주고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;우리가 기억해야 할 것은 코틀린의 재귀를 반복으로 최적화하는 것은 호출이 마지막 위치일 경우에만 가능하다는 것입니다.&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;factorialRec() 함수의 n.toBigInteger() * factorialRec(n - 1) 코드를 보면 마치 factorialRec() 호출이 마지막에 실행된다고 착각할 수 있습니다. 하지만 함수가 리턴하기 전 수행하는 연산은 곱셈 연산입니다. 왜냐하면 곱셈 연산이 factorialRec() 함수 호출이 완료될 때까지 기다렸다가 해당 결과 값으로 곱셈을 하기 때문이지요. 그래서 각 재귀 호출들의 스택 사이즈가 커지는 것이기도 합니다.&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;Tail Call 이란, 재귀 호출이 진짜로 함수의 마지막 연산인 호출을 의미합니다. 이번에는 함수 이름을 factorial() 로 수정하고, 재귀 호출이 함수의 마지막 연산이 되도록 유의하여 코드를 다시 작성해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1629090989369&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.math.BigInteger

tailrec fun factorial(n: Int, result: BigInteger = 1.toBigInteger()): BigInteger =
  if (n &amp;lt;= 0) result else factorial(n - 1, result * n.toBigInteger())
  
fun main() {
  println(factorial(5))  // 120
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행해보면 예상했던 결과가 나오고, 이제 아무런 경고도 나오지 않습니다. 이런 점을 봤을 때 코틀린 컴파일러가 뒤에서 조용히 최적화를 해서 반복을 사용한 프로시저로 함수를 변경했다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 앞서 factorialRec() 함수로는 런타임 스택 오버플로우를 일으켰던 50000 이란 숫자를 다시 입력해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629091377052&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  println(factorial(50000)) // OK
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 매우 길어서 본문에는 첨부하지 않았지만, 이번에는 스택 오버플로우가 발생하지 않고 정상적으로 결과를 출력하는 것을 확인할 수 있습니다. 이는 tailrec 키워드를 통한 수정이 최적화를 이끌어 냈다는 증거로 볼 수 있겠습니다.&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;하지만 조금 찝찝할 수 있습니다. 아무리 결과를 직접 눈으로 확인했다지만, 컴파일된 결과를 직접 본 것도 아닌데 결과만으로 최적화됐다고 하다니.. 믿지 못할 수도 있습니다. 그래서 factorialRec() 함수와 tailrec 키워드를 적용한 factorial() 함수의 바이트 코드를 비교해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1629092031193&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final static factorialRec(I)Ljava/math/BigInteger;
   // ...
   L9
    LINENUMBER 34 L9
    ILOAD 0
    ICONST_1
    ISUB
    INVOKESTATIC MainKt.factorialRec (I)Ljava/math/BigInteger;
    ASTORE 2
   // ...

public final static factorial(ILjava/math/BigInteger;)Ljava/math/BigInteger;
   // ...
   L1
    LINENUMBER 37 L1
    ILOAD 0
    IFGT L2
    ALOAD 1
    GOTO L3
   // ...
   L10
    ASTORE 1
    ISTORE 0
    GOTO L0
   // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 factorialRec() 의 바이트 코드에서 factorialRec() 를 재귀적으로 호출하기 위해서 INVOKEVIRTUAL 명령어가 사용되었습니다. 그리고 위 코드에선 생략됐지만 그 후 BigInteger 의 multiply() 메소드가 호출됩니다. 이는 재귀 프로시저가 재귀 프로세스로 컴파일됐다는 것을 보여줍니다.&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;반면에 tailrec 키워드를 적용한 factorial() 의 바이트코드는 INVOKEVIRTUAL 재귀 호출이 전혀 없습니다. 대신 IFGT 를 호출하고, goto 로 함수의 다른 부분으로 점프를 합니다. 이는 재귀 프로시저가 반복을 이용하는 프로세스로 컴파일됐다는 증거로 충분합니다.&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;정리를 하자면 tailrec 최적화는 재귀가 꼬리호출일 때만 동작합니다. tialrec 을 사용하기 위해서 우리는 factorialRec() 를 재귀 호출이 마지막에 오도록 factorial() 함수로 재작성 했습니다. 그래서 재귀가 마지막에 나오게 되었고, 꼬리 재귀로 평가되었습니다. 만약 재귀가 복잡하다면 tailrec 을 사용하는 것이 쉽지 않을 수도 있고 심지어 불가능 할 수도 있습니다.&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;</description>
      <category>Kotlin</category>
      <category>kotlin memoization</category>
      <category>kotlin recursion</category>
      <category>kotlin recursive</category>
      <category>kotlin recursive function</category>
      <category>kotlin tailrec</category>
      <category>kotlin tailrec function</category>
      <category>코틀린 tailrec</category>
      <category>코틀린 재귀</category>
      <category>코틀린 재귀함수</category>
      <category>코틀린 재귀호출</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/205</guid>
      <comments>https://readystory.tistory.com/205#entry205comment</comments>
      <pubDate>Mon, 16 Aug 2021 14:40:24 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Kotlin Delegation 이해하기③ - 변수와 프로퍼티 델리게이션</title>
      <link>https://readystory.tistory.com/204</link>
      <description>&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://readystory.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kotlin Delegation 이해하기① - 왜 상속보다 추천되는 걸까?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1627386069119&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?&quot; data-og-description=&quot;상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/202&quot; data-og-url=&quot;https://readystory.tistory.com/202&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHoCpB/hyK1mjmAYj/Na6nBv10j30gSHHpz6WF8k/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/BnnEA/hyK2IkNj6k/OdHu3AhaJnkWRo9P7HsFcK/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/Lz42k/hyK2H0ufVu/fDCLmHxsq8v7NkUH4Zk5MK/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/202&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHoCpB/hyK1mjmAYj/Na6nBv10j30gSHHpz6WF8k/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/BnnEA/hyK2IkNj6k/OdHu3AhaJnkWRo9P7HsFcK/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/Lz42k/hyK2H0ufVu/fDCLmHxsq8v7NkUH4Zk5MK/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/203&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1627386084958&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kotlin] Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기&quot; data-og-description=&quot;2021.07.25 - [Kotlin] - [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? 상속과 델리게이션(위임) 모두 객체지향 프로..&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/203&quot; data-og-url=&quot;https://readystory.tistory.com/203&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/L86v8/hyK1hvzfgm/OlggCWzQELJn8h5S7fzFWK/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/DrADl/hyK2JYisBN/nubULMkjEdQJZjYb3Ohpzk/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/c3iFK1/hyK2zBnNtf/DlHBK5EqZx73mChFbwPaRK/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/203&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/203&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/L86v8/hyK1hvzfgm/OlggCWzQELJn8h5S7fzFWK/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/DrADl/hyK2JYisBN/nubULMkjEdQJZjYb3Ohpzk/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/c3iFK1/hyK2zBnNtf/DlHBK5EqZx73mChFbwPaRK/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kotlin] Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2021.07.25 - [Kotlin] - [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? 상속과 델리게이션(위임) 모두 객체지향 프로..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Kotlin Delegation 이해하기 시리즈 3편으로 마지막 포스트가 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 예제에서 우리는 클래스 수준의 델리게이션에 집중했습니다. 코틀린은 클래스 수준의 델리게이션뿐만 아니라 객체의 속성(property)과 지역 변수에 접근하기 위한 델리게이션 역시 설정하고, 사용할 수 있습니다.&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;속성이나 지역 변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출합니다. 이와 유사하게 속성이나 변수를 설정할 때 코틀린은 setValue() 함수를 호출합니다. 객체의 델리게이션을 위의 두 메소드와 함께 제공함으로써 우리는 객체의 속성과 지역변수를 읽고 쓰는 요청을 가로챌 수 있습니다.&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;변수 델리게이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 지역 변수의 읽기와 쓰기에 대한 접근을 모두 가로챌 수 있습니다. 그리고 리턴되는 것을 변경할 수 있고 데이터를 언제, 어디에 저장하는지도 변경할 수 있습니다. 이런 기능을 묘사하기 위해서 String 변수에 대한 접근을 가로채는 커스텀 델리게이션을 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 유저의 댓글을 받는 어플리케이션을 만든다고 가정해 보겠습니다. 유저가 입력하는 텍스트는 다른 유저들에게 보여야 하기 때문에 비속어가 없어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 예의 없는 단어인 &quot;stupid&quot; 를 필터링하는 델리게이션을 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627386386262&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val comment = &quot;this is stupid&quot;
println(comment)  // this is stupid&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;우리의 목표는 &quot;stupid&quot; 라는 단어를 변경하여 문장이 출력되었을 때 무례하지 않게 만드는 것입니다. 이를 위해서 PoliteString 이라는 클래스를 만들고, 서론에서 언급한 setValue() 와 getValue() 를 갖도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627386514625&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.reflect.KProperty
class PoliteString(var content: String) {  
  operator fun setValue(thisRef: Any?, property: KProperty&amp;lt;*&amp;gt;, value: String) {
    content = value
  }
  
  operator fun getValue(thisRef: Any?, property: KProperty&amp;lt;*&amp;gt;) = content.replace(&quot;stupid&quot;, &quot;s*****&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PoliteString 클래스는 델리게이션으로만 동작하도록 되어있습니다. 코틀린에서는 인터페이스를 구현할 필요도 없고 관습적인 코드도 없이 단지 메소드만 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;델리게이션이 가변(mutable)인 속성이나 변수를 타깃으로 한다면 위 코드처럼 setValue() 도 구현해줘야 하고, 불변이라면 getValue() 만 선언해주면 되겠습니다.&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;다시 예제로 돌아와, PoliteString 클래스는 content 라는 뮤터블한 속성을 받습니다. getValue() 함수에서 문제가 되는 단어인 &quot;stupid&quot; 를 정리하고 문자열을 반환해 줍니다. 각각의 함수들은 operator 표기로 작성되어 &quot;=&quot; 기호를 통해 사용할 수 있게 됩니다. operator 에 대해 생소하신 분들은 &lt;a href=&quot;https://kotlinlang.org/docs/operator-overloading.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코틀린 공식 문서&lt;/a&gt;를 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 PoliteString 을 통해 테스트해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627386825397&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  var comment: String by PoliteString(&quot;nice message&quot;)
  println(comment)  // nice message
  comment = &quot;this is stupid&quot;
  println(comment)  // this is s*****
}&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;보시다시피 comment 변수를 선언하면서 PoliteString 클래스로 델리게이션을 사용하였고 그 결과 문자열에 &quot;stupid&quot; 가 포함될 경우 &quot;s*****&quot; 로 대체되어 가져올 수 있도록 적용되었습니다. 만약 by 키워드 뒤에 클래스의 생성자를 사용하고 싶지 않다면 해당 델리게이션 인스턴스를 리턴해주는 함수를 사용할 수도 있습니다.&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;속성 델리게이션(Property Delegation)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 사용한 접근법으로 우리는 지역변수뿐만 아니라 객체의 속성에도 델리게이션 접근을 할 수 있습니다. 속성을 정의할 때 값을 할당하는 게 아니라 by 를 사용하고 그 뒤에 델리게이션을 위치하면 됩니다. 다시 말하자면 델리게이션은 getValue() 를 구현하거나 getValue() 와 setValue() 를 모두 사용하는 속성이라면 어떤 객체에서든 사용 가능합니다.&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;코틀린 스탠다드 라이브러리의 디자인을 보면 Map 과 MutableMap 은 델리게이션을 사용할 수 있습니다. Map 은 val 속성, MutableMap 은 var 속성으로 사용할 수 있습니다. 왜냐하면 Map 은 getValue() 메소드도 가지고 있고, MutableMap 은 getValue() 와 setValue() 메소드 모두 가지고 있기 때문입니다.&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;일단 우리는 데이터 소스로 사용될 MutableMap 에 comment 의 값을 저장하기 위해서 PoliteString 의 변형을 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627394935129&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PoliteString(val dataSource: MutableMap&amp;lt;String, Any&amp;gt;) {
  operator fun getValue(thisRef: Any?, property: KProperty&amp;lt;*&amp;gt;) =
    (dataSource[property.name] as? String)?.replace(&quot;stupid&quot;, &quot;s*****&quot;) ?: &quot;&quot;
    
  operator fun setValue(thisRef: Any?, property: KProperty&amp;lt;*&amp;gt;, value: String) {
    dataSource[property.name] = value
  }
}&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;PoliteString 을 수정하여서 String 파라미터를 받는 대신에 MutableMap&amp;lt;String, Any&amp;gt; 를 받아서 comment 의 값을 저장합니다. getValue() 메소드 안에서 속성의 이름을 key 로 사용해서 map 에 있는 값을 리턴해줍니다. 그리고 값이 존재한다면 쉽게 String 으로 캐스팅하고, 아까와 마찬가지로 &quot;stupid&quot; 라는 텍스트는 &quot;s*****&quot; 으로 대체해줍니다. setValue() 에서는 값을 map 에 저장하기만 합니다.&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;이제 PostComment 클래스를 만들어 보겠습니다. PostComment 클래스는 블로그 게시물의 comment 를 나타냅니다. PostComment 의 프로퍼티는 로컬 필드에 저장하는 대신 map 에 get/set 동작으로 위임합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627395198741&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PostComment(dataSource: MutableMap&amp;lt;String, Any&amp;gt;) {
  val title: String by dataSource
  var likes: Int by dataSource
  val comment: String by PoliteString(dataSource)
  
  override fun toString() = &quot;Title: $title Likes: $likes Comment: $comment&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프라이머리 생성자가 MutableMap&amp;lt;String, Any&amp;gt; 타입의 기존 데이터 dataSource 를 파라미터로 받습니다. dataSource 는 이 클래스의 속성을 위임받아서 처리해 주는 델리게이션입니다.&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;title 은 읽기 전용 프로퍼티이고, likes 는 Int 타입에 읽기-쓰기 가능한 프로퍼티입니다. 그리고 이 둘은 모두 dataSource 에 델리게이션 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 comment 프로퍼티는 PoliteString 에 위임됩니다. PoliteString 은 동일한 dataSource 에서 데이터가 저장되고 검색됩니다. 물론 PoliteString 또한 동일한 dataSource 에서 데이터가 저장되고 검색됩니다.&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;comment 속성의 읽기와 쓰기는 각각 PoliteString 델리게이션의 getValue() 와 setValue() 을 호출합니다. 이 위임을 통해서 comment 값을 dataSource 에서 읽거나 dataSource 에 저장합니다.&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;pre id=&quot;code_1627395679868&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val data = mutableMapOf(
        &quot;title&quot; to &quot;Using Delegation&quot;,
        &quot;likes&quot; to 2,
        &quot;comment&quot; to &quot;Keep it simple, stupid&quot;)

val post = PostComment(data)
post.likes++
println(post)  // Title: Using Delegation Likes: 3 Comment: Keep it simple, s*****&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 직접 델리게이션을 만드는 방법을 확인해봤습니다.&lt;/p&gt;
&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;Lazy Delegation&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축 평가란 지금까지 진행한 식의 평가가 결과를 도출하기에 충분할 경우 식의 실행을 건너뛰는 것을 말합니다. 대부분의 프로그래밍 언어는 이 기능을 지원하고, 코틀린의 Lazy Delegation 은 이러한 접근의 영역을 넓혀줍니다. 함께 살펴보겠습니다.&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;pre id=&quot;code_1627395931703&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun getTemperature(city: String): Double {
  println(&quot;fetch from webservic for $city&quot;)
  return 30.0
}&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;이 기상 함수 getTemperature() 는 웹 서비스에 원격 접속을 해야 하기 때문에 약간의 시간이 걸리고, 웹 서비스를 사용할 때 사용요금을 내야 한다고 가정해보겠습니다. 따라서 가능하다면 호출을 안 하는 게 이득입니다. 단축 평가는 자연스럽게 호출을 피하게 해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627396162492&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val showTemperature = false
val city = &quot;Seoul&quot;
if (showTemperature &amp;amp;&amp;amp; getTemperature(city) &amp;gt; 20) {
  println(&quot;Warm&quot;)
} else {
  println(&quot;Nothing to report&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;showTemperature 변수의 값은 false 입니다. 단축 평가식 덕분에 getTemperature() 메소드는 생략되어 호출되지 않습니다. 하지만 이 코드를 약간 리팩토링 하면 효율성이 떨어져 버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627396258192&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val showTemperature = false
val city = &quot;Seoul&quot;
val temperature = getTemperature(city)

if (showTemperature &amp;amp;&amp;amp; temperature &amp;gt; 20) {
  println(&quot;Warm&quot;)
} else {
  println(&quot;Nothing to report&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 getTemperature() 의 결과를 지역 변수에 저장했습니다. 그리고 if 문을 사용해서 결과를 평가했습니다. 하지만 이런 변화 때문에 오버헤드가 발생했습니다. 그리고 단축 평가 때문에 temperature 변수는 사용되지도 않았습니다.&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;개발자들은 Boolean 식의 단축 평가에 아주 익숙합니다. 다만 코틀린은 이런 Boolean 식 외에도 실행을 스킵할 수 있는 방법을 지원합니다. 개발자는 직접 컴파일러에게 식의 결과가 정말로 필요하기 전까지는 식을 실행하지 않도록 지연 연산을 요청할 수 있습니다. 식의 결과가 필요하지 않으면 식 전체를 스킵해 버립니다.&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;이젠 이전 코드를 Lazy Delegation 을 사용하도록 수정해보겠습니다. 그리고 이때 Lazy Delegation 클래스를 직접 사용하는 대신 편리하게 lazy 라는 wrapper 함수를 사용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627396460074&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val showTemperature = false
val city = &quot;Seoul&quot;
val temperature by lazy { getTemperature(city) }

if (showTemperature &amp;amp;&amp;amp; temperature &amp;gt; 20) {
  println(&quot;Warm&quot;)
} else {
  println(&quot;Nothing to report&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 temperature 를 by 키워드를 사용해서 델리게이션 속성으로 변경했습니다. lazy 함수는 연산을 실행할 수 있는 람다 표현식을 인자(argument)로 받습니다. 그리고 요청 즉시 실행하지 않고, 필요한 순간에만 실행합니다.&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;즉, lazy 뒤에 오는 람다 표현식의 연산은 변수의 값이 필요할 때만 수행됩니다. 변수의 값이 필요하기 전까지는 실행이 연기되고, 영원히 실행되지 않을 가능성이 있습니다. 이번 예제에서는 영원히 실행되지 않습니다.&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;위 예제에서는 우리가 showTemperature 변수의 값을 true 로 변경했을 때 getTemperature() 가 실행됩니다. &lt;b&gt;이는 temperature 가 정의되는 시점에 실행되는 게 아니라 showTemperature 의 평가 이후에 &quot;temperature &amp;gt; 20&quot; 의 평가가 필요한 시점에 실행됩니다.&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;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;그리고 중요한 점은 일단 람다 표현식이 실행되면 델리게이션은 결과를 저장하고 있다가 미래에 요청이 있으면 저장된 값을 알려줍니다. 람다 표현식이 다시 실행되는 게 아닙니다.&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;lazy 함수는 기본적으로 람다 표현식의 실행과 동기화됩니다. 그래서 하나의 스레드만 실행됩니다. 만약에 멀티 쓰레드에서 코드를 동시에 실행하는 게 안전한 경우나 안드로이드 UI 쓰레드처럼 싱글 쓰레드만 쓸 수 있는 경우라면 enum 타입인 LazyThreadSafetyMode 인자 값을 lazy 함수로 전달해서 다른 종류의 동기화 옵션을 선택할 수 있습니다.&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;재밌게도 게으름(Lazy)이 보다 효율적인 코드를 만들어 냈는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 델리게이션에 대해서 1~3편을 통해 알아봤고, 이 외에도 코틀린 스탠다드 라이브러리에 빌트인 되어 있는 observable() 델리게이션 함수나 vetoable() 함수도 추가로 알아보시면 좋을 것 같습니다.&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;</description>
      <category>Kotlin</category>
      <category>kotlin by</category>
      <category>kotlin by lazy</category>
      <category>kotlin delegation</category>
      <category>kotlin lazy</category>
      <category>kotlin lazy delegation</category>
      <category>kotlin property delegation</category>
      <category>코틀린 by lazy</category>
      <category>코틀린 lazy</category>
      <category>코틀린 델리게이션</category>
      <category>코틀린 위임</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/204</guid>
      <comments>https://readystory.tistory.com/204#entry204comment</comments>
      <pubDate>Tue, 27 Jul 2021 23:44:07 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기</title>
      <link>https://readystory.tistory.com/203</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.25 - [Kotlin] - [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1627301766848&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?&quot; data-og-description=&quot;상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/202&quot; data-og-url=&quot;https://readystory.tistory.com/202&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boXZJF/hyKZ2FFTBj/E4kSVSk5IHR5Ekk7L9SIck/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bECvc7/hyK1fXHeP3/kJayf0RaStoODoVFR8rvjk/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bThLeg/hyKZ2lomLA/PUHz7b9mcYj2kZuIMQscP1/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/202&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boXZJF/hyKZ2FFTBj/E4kSVSk5IHR5Ekk7L9SIck/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bECvc7/hyK1fXHeP3/kJayf0RaStoODoVFR8rvjk/img.jpg?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bThLeg/hyKZ2lomLA/PUHz7b9mcYj2kZuIMQscP1/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;이번 글에서는 생성자의 파라미터와 프로퍼티에 위임하는 방법에 대해 자세하게 알아보겠습니다. 지난 글이 Delegation 에 대한 개념적인 내용이었다면 이번 글은 좀 더 코틀린 문법에 대해 포커스를 맞출 예정입니다.&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;파라미터에 위임하기&lt;/b&gt;&lt;/h3&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 id=&quot;code_1627303062863&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Worker {
  fun work()
  fun takeVacation()
}

open class JavaProgrammer : Worker {
  override fun work() = println(&quot;code with Java&quot;)
  override fun takeVacation() = println(&quot;code at the beach&quot;)
}

class PythonProgrammer : Worker {
  override fun work() = println(&quot;code with Python&quot;)
  override fun takeVacation() = println(&quot;code at the home&quot;)
}

class Manager : Worker by JavaProgrammer()&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;Manager 클래스를 보시면 Worker by JavaProgrammer() 란 코드를 작성 했었습니다. 즉, Manager 의 인스턴스가 명시적으로 생성된 JavaProgrammer() 의 인스턴스로 위임(Delegation) 합니다.&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;&lt;b&gt;첫 째&lt;/b&gt;, Manager 클래스의 인스턴스는 오직 JavaProgrammer 의 인스턴스에게만 요청할 수 있습니다. 다른 종류의 Worker 인터페이스를 구현한 클래스에게 요청이 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘 째&lt;/b&gt;, Manager 의 인스턴스는 델리게이션에 접근할 수 없습니다. 이는 Manager 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다는 의미입니다.&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;pre id=&quot;code_1627303302189&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Manager(val staff: Worker) : Worker by staff {
  fun meeting() = println(&quot;organizing meeting with ${staff.javaClass.simpleName}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manager 클래스의 생성자는 staff 라는 파라미터를 받습니다. staff 는 val 로 정의했기 때문에 프로퍼티가 됩니다. 만약 val 이 제거된다면 staff 는 클래스의 프로퍼티가 아니고 그냥 파라미터로 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 val 이 사용되든 안되든 상관없이 Manager 클래스는 staff 파라미터를 델리게이션으로 사용합니다.&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;staff 는 Manager 클래스의 프로퍼티기 때문에 meeting() 함수에서 접근할 수 있습니다. 그리고 이렇게 구현하면 Manager 클래스가 더이상 JavaProgrammer 에 묶이지 않고 생성자 파라미터를 통해 Worker 인터페이스를 구현하는 어떠한 객체도 받을 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manager 인스턴스 두 개를 생성해서 이런 동작들을 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627303563142&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val javaManager = Manager(JavaProgrammer())
val pythonManager = Manager(PythonProgrammer())
javaManager.work()  // code with Java
javaManager.meeting()  // organizing meeting with JavaProgrammer
pythonManager.work()  // code with python
pythonManager.meeting()  // organizing meeting with PythonProgrammer&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;위 코드에서 보시다시피 이제 Manager 클래스는 어느 하나의 Worker 인터페이스 구현체에 묶이지 않고, 유연하게 사용 가능해졌음을 확인할 수 있습니다. 두 개의 Manager 인스턴스 모두 work() 함수가 호출되면 코틀린이 자동으로 연결된 델리게이션으로 요청을 전달합니다. 그리고 Manager 클래스에 정의된 meeting() 함수가 호출되면 Manager 인스턴스의 속성이 staff 를 이용해서 함수를 수행합니다.&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;함수 충돌 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 함수를 위한 wrapper 를 만듭니다. 예를 들어 Manager 클래스에 work(), takeVacation() 함수를 구현하지 않아도 Manager 클래스를 정의할 때 델리게이션을 사용했기 때문에 컴파일러가 자동으로 Manager 클래스에 work(), takeVacation() 함수를 구현하여 호출되면 델리게이션 인스턴스에 위임하게 됩니다.&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;i&gt;그렇다면 만약 사용하는 클래스와 델리게이션 클래스에 동일한 이름과 시그니처가 있는 함수가 있다면 어떻게 될까요?&lt;/i&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;이전 예제에서 Worker 인터페이스는 takeVacation() 함수를 가지고 있고, Manager 클래스는 해당 함수를 델리게이션인 Worker 에게 위임했습니다. 하지만 이대로 사용했다간 우리는 Manager 의 휴가를 구현하고 싶은데도 델리게이션 하고 있는 Worker 에게 휴가처리도 위임해버리게 됩니다. 이는 의도하지 않은 설계를 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야 합니다. 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않습니다. 앞서 보았듯, Manager 는 Worker 인터페이스를 구현하지만 work() 나 takeVacation() 메소드를 실제로 구현하여 제공하지 않습니다.&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;델리게이션 클래스의 모든 인터페이스를 위해서 코틀린 컴파일러가 Wrapper 를 만듭니다. 하지만 델리게이션 클래스가 이미 인터페이스의 함수를 구현한 상태에서 델리게이션을 이용하는 클래스에서 다시 함수를 구현하려고 하는 경우에는 override 키워드를 사용해야 합니다. 그렇게 하면 클래스에서 구현한 메소드에 우선 순위가 생기고, 컴파일러는 Wrapper 함수를 생성지 않습니다.&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;이를 확인하기 위해 Manager 클래스에 takeVacation() 함수를 구현해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627305640930&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Manager(val staff: Worker) : Worker by staff {
  override fun takeVacation() = println(&quot;manager vacation&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;override 키워드를 사용했기 때문에 코드를 읽는 사람들은 해당 함수가 어쩌다보니 델리게이션의 함수와 같은 이름으로 생성된게 아니라 인터페이스의 함수를 구현했다는 사실을 명확하게 알 수 있습니다. 이것을 보고 코틀린 컴파일러는 takeVacation() 함수의 Wrapper 를 생성하지 않고 work() 함수의 Wrapper 만을 생성할 것입니다.&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;이번 버전 Manager 클래스의 인스턴스에서 인터페이스의 함수를 사용해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627305758734&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val manager = Manager(JavaProgrammer())
manager.work()  // code with Java
manager.takeVacation()  // manager vacation&lt;/code&gt;&lt;/pre&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;b&gt;Delegation 의 주의사항&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 우리가 만든 예제에서 Manager 는 JavaProgrammer 의 인스턴스에게 델리게이션을 요청했습니다. 하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;Manager 의 참조는 JavaProgrammer 의 참조에 할당할 수 없습니다.&lt;/span&gt; 이 말의 뜻은 Manager 는 JavaProgrammer 를 사용할 수 있지만 Manager 를 JavaProgrammer 로 사용할 수는 없다는 이야기입니다.&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;다시 말해 Manager 는 JavaProgrammer 를 가지고 있는 것이지, JavaProgrammer 의 한 종류가 아니라는 뜻입니다. 앞서 말한 바와 같이 델리게이션은 상속과 다르게 대체될 가능성이 없는 재사용성을 제공해 줍니다.&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;/p&gt;
&lt;pre id=&quot;code_1627306112422&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val manager = Manager(JavaProgrammer())
val coder: JavaProgrammer = manager  // Error : type mismatch
val employee: Worker = manager  // OK&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;color: #009a87;&quot;&gt;&lt;b&gt;Manager 는 JavaProgrammer 의 자식은 아니기에 대체될 수 없지만, Worker 로 취급될 수는 있습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 아까 staff 를 Manager 의 생성자로 전달할 때 val 을 사용했습니다. 하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;이 때 val 을 var 로 변경하면 몇 가지 이슈가 발생합니다.&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_1627306451760&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val manager = Manager(JavaProgrammer())

println(&quot;Staff is ${manager.staff.javaClass.simpleName}&quot;)  // Staff is JavaProgrammer
manager.work()  // code with Java

manager.staff = PythonProgrammer()
println(&quot;Staff is ${manager.staff.javaClass.simpleName}&quot;)  // Staff is PythonProgrammer
manager.work()  // code with Java&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;Manager 의 생성자는 staff 란 이름의 델리게이션을 가변(mutable)으로 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 당시에 JavaProgrammer() 인스턴스를 주입하고서 이후에 PythonProgrammer() 인스턴스로 staff 를 변경해주었음에도 manager.work() 의 결과는 여전히 처음에 설정했던 &quot;code with Java&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;이는 코틀린 컴파일러가 어떻게 델리게이션을 처리하는지 보다 자세히 알 필요가 있습니다. Manager 클래스를 정의할 때 사용하였던 Worker by staff 델리게이션에서 staff 는 프로퍼티가 아니라 파라미터입니다. 마치, Manager 생성자에 staff 파라미터에 val 이나 var 을 선언하지 않은 것과 같습니다.&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;문제는 이 뿐만이 아닙니다. staff 를 PythonProgrammer 인스턴스로 변경했을 때 원래 사용하던 JavaProgrammer 의 인스턴스엔 더 이상 접근할 수 없어졌음에도 불구하고 델리게이션이 JavaProgrammer 인스턴스를 사용중이기 때문에 가비지 콜렉터가 수집해 가지 않아 메모리릭이 발생할 수 있습니다.&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;이런 여러 이유로 생성자 파라미터를 통해 델리게이션을 사용할 경우에는 반드시 파라미터로 선언하거나, val 로 선언하여 사용하시길 권장합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kotlin</category>
      <category>kotlin by</category>
      <category>kotlin by example</category>
      <category>kotlin delegation</category>
      <category>kotlin parameter delegation</category>
      <category>kotlin property delegation</category>
      <category>코틀린 델리게이션</category>
      <category>코틀린 델리게이션 클래스</category>
      <category>코틀린 위임</category>
      <category>코틀린 파라미터 델리게이션</category>
      <category>코틀린 프로퍼티 델리게이션</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/203</guid>
      <comments>https://readystory.tistory.com/203#entry203comment</comments>
      <pubDate>Mon, 26 Jul 2021 22:43:32 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는 걸까?</title>
      <link>https://readystory.tistory.com/202</link>
      <description>&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;/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;i&gt;&amp;lt;GoF의 디자인 패턴&amp;gt;&lt;/i&gt; 같은 책이나 &lt;i&gt;&amp;lt;이펙티브 자바&amp;gt;&lt;/i&gt; 등 굉장히 유명한 저서에서는 상속(is-a)보다는 델리게이션(has-a)을 사용할 것을 강력하게 추천하고 있습니다. 아마 자바로 개발하신 분들이라면 델리게이션보다는 상속을 통한 재사용을 많이 보시고, 사용하셨을 텐데요. 이는 자바가 상속에 대해서는 많은 지원을 해줬지만 델리게이션에 대해서는 지원이 약하기 때문입니다. 코틀린은 자바와 달리 델리게이션을 위한 기능들을 문법적으로 제공합니다.&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;상속 대신 델리게이션을 써야 하는 상황&lt;/b&gt;&lt;/h3&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;만약 &quot;개는 동물이다&quot; 와 같이 포함 관계(is-a)에 있는 다른 클래스로 대체할 때는 상속을 사용하는게 맞습니다. 하지만 Manager 가 Worker 를 가지고 있고 Worker 에게 일을 넘기는 것처럼, 오직 다른 객체의 구현을 재사용 하는 경우라면 델리게이션을 사용하는게 좋습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 자바에서는 델리게이션이 적절한 설계일 때 많은 코드를 중복 작성해야 했습니다. 이는 언어적인 차원에서 델리게이션을 강력하게 지원해주지 않아 어쩔 수 없었는데요. 코틀린은 델리게이션을 사용할 때 더 좋은 선언적인 접근 방식을 사용합니다. 따라서 개발자는 컴파일러에게 편리하게 의도를 전달하고, 컴파일러는 필요한 코드를 생성하여 실행시킵니다.&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;델리게이션(Delegation)을 사용한 디자인&lt;/b&gt;&lt;/h3&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;예제는 어느 한 소프트웨어 기업의 프로젝트를 시뮬레이션 해보도록 하겠습니다. 예제에서는 일을 할 작업자(Worker) 와 그들을 관리하는 관리자(Manager)가 필요합니다. Worker 중에서도 각각의 언어에 특화된 JavaProgrammer 와 PythonProgrammer 두 개의 클래스를 구현해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627144423905&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Worker {
  fun work()
  fun takeVacation()
}

class JavaProgrammer : Worker {
  override fun work() = println(&quot;code with Java&quot;)
  override fun takeVacation() = println(&quot;code at the beach&quot;)
}

class PythonProgrammer : Worker {
  override fun work() = println(&quot;code with Python&quot;)
  override fun takeVacation() = println(&quot;code at the home&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 회사에는 팀을 관리하기 위한 소프트웨어 개발 매니저가 필요합니다. 해당 클래스를 만들어보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627144470974&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Manager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manager 클래스에는 아직 아무런 정의가 되어 있지 않습니다. 이제 이 Manager 클래스에 로직을 넣기 위해 디자인을 해야 하는데 먼저 델리게이션이 아닌, 상속을 사용해 디자인해보도록 하겠습니다.&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;회사의 입장에서는 Programmer 도, Manager 도 모두 Worker 입니다. 회사는 프로젝트를 진행하기 위해 관리자에게 의존할 것이고, 관리자는 프로그래머에게 일을 시킬 것입니다. 가장 단순한 형태로 만들어보자면, Manager 가 work() 를 호출하기 위해서는 Manager 도 Worker 인터페이스를 구현하여 실행하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 방법 중 하나가 상속인 것이고, Java 에서는 주로 상속을 사용했습니다. Manager 를 JavaProgrammer 로부터 상속 받으면 Manager 클래스에서 구현을 다시 작성할 필요가 없게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627144782920&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;open class JavaProgrammer : Worker {
  override fun work() = println(&quot;code with Java&quot;)
  override fun takeVacation() = println(&quot;code at the beach&quot;)
}

class Manager : JavaProgrammer()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속을 위해 JavaProgrammer 클래스를 open class 로 변경해주고, Manager 클래스가 이를 상속하도록 변경했습니다.&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;그러면 이제 Manager 인스턴스에서 work() 를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627144847586&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val manager = Manager()
manager.work()	// code with Java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 &quot;code with Java&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;하지만 이 디자인에는 문제점이 존재합니다. Manager 클래스는 JavaProgrammer 가 아님에도 불구하고 해당 클래스에 갇혀버리게 됩니다. 이제 Manager 에서는 PythonProgrammer 클래스가 제공하는 구현을 사용할 수 없습니다. 즉, JavaProgrammer 만을 위한 Manager 가 되어버렸습니다.&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;우리는 Manager 가 JavaProgrammer 나 특정 언어의 개발자라고 설정한 적이 없습니다. 하지만 상속이 그렇게 만들어버렸죠. 위 코드처럼 구현할 경우 아래와 같은 코드가 정상 동작하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627145034702&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val coder: JavaProgrammer = manager&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;이는 명백하게 의도된 디자인이 아니지만, 막을 방도가 없습니다. 원래 우리가 의도했던 바는 JavaProgrammer 뿐만 아니라 작업을 맡길 수 있는 모든 Worker 객체에게 Manager 가 의존하는 것입니다. 그래서 우리는 Manager 의 인스턴스가 모든 종류의 Worker 인스턴스에게 일을 위임(Delegation)하게 만들길 원합니다.&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;Java 같은 언어는 상속을 위한 문법을 가지고 있어도, 델리게이션을 위한 문법은 없습니다. 아마 다른 객체를 참조할 수 있겠지만, 언어 차원에서 그런 디자인을 구현하는 부담을 모두 개발자에게 미뤄버립니다.&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;비록 코틀린이지만 잠시 동안 Java 에서 사용 가능한 기능만을 사용하여 변경해 보도록 하겠습니다. 아래의 코드는 Java 에서 Manager 가 Worker 에게 델리게이션을 사용하는 방식을 코틀린 코드로 나타낸 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1627145292504&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Manager(val worker: Worker) {
  fun work() = worker.work()
  fun takeVacation() = worker.work()  // 매니저가 쉬어도 작업자는 일을 해야하는...
}

fun main() {
  val manager = Manager(JavaProgrammer())
  manager.work()	// code with Java
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manager 인스턴스를 만든 후 JavaProgrammer 인스턴스를 생성자로 전달했습니다. 이런 디자인이 상속을 이용하는 것보다 좋은 점은 Manager 가 JavaProgrammer 클래스에 강하게 묶이지 않아 언제든지 Manager 의 생성자에 PythonProgrammer 인스턴스 등 Worker 인터페이스를 구현하는 어떤 인스턴스라도 넘길 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 다르게 말하면, Manager 의 인스턴스는 Worker 인터페이스를 구현하는 클래스의 인스턴스에게 위임할 수 있다는 뜻입니다. 하지만 이런 디자인은 규모가 커짐에 따라 코드가 쉽게 장황해지고, 소프트웨어 디자인의 기본사항 몇 가지도 어길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manager 클래스 안에 구현된 메소드들은 그저 Manager 인스턴스가 참조로 가지고 있는 Worker 의 인스턴스를 호출하는 기능만 가지고 있습니다. 만약 Worker 인터페이스에 더 많은 메소드가 있었다면 Manager 에는 더 많은 호출 코드가 들어가야 했을 것입니다. 모든 호출 코드는 호출할 메소드명도 그렇고 거의 비슷합니다. 이는 &lt;i&gt;&amp;lt;실용주의 프로그래머&amp;gt; &lt;/i&gt;저서에서 설명하는&amp;nbsp;&lt;b&gt;DRY(Don't Repeat Yourself, 반복하지 말 것) 원칙 &lt;/b&gt;을 위반합니다. 뿐만 아니라 SOLID 원칙 중 확장에는 열려있어야 하고 변경에는 닫혀 있어야 한다는 &lt;b&gt;개방-폐쇄 원칙(OCP, Open-Closed Principle)&lt;/b&gt; 도 지키지 못하게 됩니다. 다시 말해, 클래스를 확장하기 위해 클래스를 변경하면 안 된다는 것인데 만약 Worker 인터페이스에 deploy() 메소드를 추가한다면 Manager 클래스에서도 해당 메소드를 위임하는 호출을 하기 위해 메소드를 추가해야만 함으로 OCP 를 위반하게 됩니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;by 키워드를 사용한 델리게이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 예제에서 Manager 클래스에 Worker 인터페이스로 요청을 위임하는 델리게이션을 구현했습니다. Manager 의 바디는 중복된 메소드 호출과 DRY, OCP 원칙 위반으로 다소 지저분해졌었습니다.&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;Java 에서는 저런 코딩만이 유일한 방법이지만 코틀린에서는 개발자가 직접 손대지 않고도 컴파일러에게 코드를 요청할 수 있습니다. 그래서 Manager 는 보스답게(?) 번거로운 작업 없이 일을 맡길 수 있습니다.&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;pre id=&quot;code_1627146389054&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Manager() : Worker by JavaProgrammer()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드의 Manager 는 어떤 메소드도 따로 작성해주지 않았습니다. Manager 는 JavaProgrammer 를 이용해 Worker 인터페이스를 구현하고 있습니다. 코틀린 컴파일러는 Worker 에 속하는 Manager 클래스의 메소드를 바이트 코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 클래스의 인스턴스로 호출을 요청합니다. 다시 말하자면 위 예제의 by 키워드가 컴파일 시간에 이전 예제에서 우리가 시간을 들여서 수동으로 구현했던 델리게이션을 대신 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;클래스 정의 수준에서 사용되는 코틀린의 by 키워드의 왼쪽에는 인터페이스가, 오른쪽엔 해당 인터페이스를 구현한 클래스가 필요합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 잘 동작하는지 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627146698104&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val manager = Manager()
manager.work()  // code with Java&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 째&lt;/b&gt;, Manager 클래스는 Worker 인터페이스를 구현하긴 하지만 JavaProgrammer 클래스를 상속받지 않습니다. 상속을 이용한 구현에서 우리는 Manager 의 인스턴스를 JavaProgrammer 타입의 변수에 저장할 수 있었습니다. 하지만 이제 그런 상황은 더 이상 발생하지 않으며, 아래와 같은 상황에서 오류가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1627146997124&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val coder: JavaProgrammer = manager  // Error: type mismatch&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;b&gt;둘 째&lt;/b&gt;, 상속을 사용한 솔루션에서 work() 같은 메소드를 위임하기 위해 그대로 호출하는 보일러플레이트 코드가 Manager 클래스에서는 작성되지 않았습니다. 대신 by 뒤에 오는 베이스 클래스로 요청을 넘겼을 뿐입니다. 사실상 우리가 manager.work() 를 호출할 때, 우리는 Manager 클래스의 보이지 않는 메소드인 work() 를 호출하는 격입니다. 이 함수는 코틀린 컴파일러에 의해서 합성되었고 델리게이션에게 호출을 요청합니다. 그래서 위 케이스에서는 클래스 선언 시 주어진 JavaProgrammer 의 인스턴스에게 요청하게 됩니다.&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;다음 포스트에서는 이런 형태의 델리게이션이 갖는 제약사항과 그 해결책, 그리고 코틀린에서 다양한 형태로 지원되는 델리게이션에 대해 더 알아보겠습니다.&lt;/p&gt;</description>
      <category>Kotlin</category>
      <category>kotlin by</category>
      <category>kotlin by lazy</category>
      <category>kotlin delegation</category>
      <category>kotlin delegation 장점</category>
      <category>kotlin delegation 활용</category>
      <category>객체지향 설계</category>
      <category>코틀린</category>
      <category>코틀린 by</category>
      <category>코틀린 델리게이션</category>
      <category>코틀린 위임</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/202</guid>
      <comments>https://readystory.tistory.com/202#entry202comment</comments>
      <pubDate>Sun, 25 Jul 2021 02:30:54 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 한 방에 정리하는 코틀린 제네릭(kotlin generic) - in, out, where, reified</title>
      <link>https://readystory.tistory.com/201</link>
      <description>&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;자세한 내용은 &lt;a href=&quot;https://kotlinlang.org/docs/generics.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코틀린 공식 문서&lt;/a&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;b&gt;제네릭(generic) : 파라미터 타입의 가변성과 제약사항&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성하다 보면 다양한 타입에 동일한 로직을 적용하기 위해 코드 재사용을 과도하게 하려는 경우가 있는데요. 이를 테면 파라미터를 전부 Any 로 받는다거나.. 등 이런 경우에는 타입 안정성을 저하시킬 수가 있습니다. 제네릭은 이런 이슈에 적절한 균형을 맞춰줍니다. 제네릭을 사용하면 다양한 타입에서 사용 가능한 코드를 작성할 수 있습니다!&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;자바에서는 기본적으로 제네릭은 타입 불변성을 강요했습니다. 제네릭 함수가 파라미터 타입 T 를 받는다면 T 의 부모 클래스나 자식 클래스를 사용하는 것이 불가능했습니다. 즉, 타입이 정확히 일치해야만 했습니다. 이것이 나쁜 것은 아닙니다. 개인적으로 코드에 제약은 크게 걸수록 안정성은 높아진다고 생각합니다.&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;pre id=&quot;code_1626700224649&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;open class Fruit
class Apple : Fruit()
class Banana : Fruit()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 클래스들을 선언하겠습니다. Fruit 이라는 부모 클래스를 상속하는 Apple, Banana 클래스를 준비합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626700378173&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun receiveFruits(fruits: Array&amp;lt;Fruit&amp;gt;) {
    println(&quot;Number of fruits: ${fruits.size}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 위와 같이 Fruit 클래스를 제네릭 타입으로 선언된 배열(Array)을 파라미터로 받는 receiveFruits() 함수를 작성합니다.&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;위 함수의 파라미터로는 Array&amp;lt;Apple&amp;gt; or Array&amp;lt;Banana&amp;gt; 객체를 전달할 수 없습니다. (이는 자바에서도 마찬가지 입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 상속이란 대체 가능성을 의미하기 때문에 자식 클래스의 인스턴스는 부모 클래스의 인스턴스를 인자로 하는 모든 메소드에 전달할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 위 경우에는 Fruit 을 상속하는 Apple 이나 Banana 배열을 넘길 수 없었을까요?&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 id=&quot;code_1626701840063&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val fruits: Array&amp;lt;Apple&amp;gt; = arrayOf(Apple())
    receiveFruits(fruits)
}

fun receiveFruits(fruits: Array&amp;lt;Fruit&amp;gt;) {
	fruits[0] = Banana() // 문제가 될 수 있음!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, Array&amp;lt;Apple&amp;gt; 객체를 Array&amp;lt;Fruit&amp;gt; 을 인자로 받는 함수에 인자로 전달될 수 잇다면 위와 같이 receiveFruits() 함수가 Banana 객체를 해당 파라미터에 담게될 때 문제가 발생합니다. Banana 는 Apple 처럼 취급될 수 없기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 체크를 통해 fruits 의 요소 타입이 Banana 일 경우에만 변경 가능하도록 구현을 할 수도 있겠지만 이런 방식은 SOLID 원칙 중 리스코프 치환 원칙에 위배됩니다.&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;코틀린은 Apple 이 Fruit 을 상속 받았더라도 Array&amp;lt;Apple&amp;gt; 를 Array&amp;lt;Fruit&amp;gt; 으로 취급해서 전달하는 것을 막아서 제네릭을 타입 안정적으로 만들었습니다. 그럼 아래와 같은 경우에는 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626702357487&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun receiveFruits(fruits: List&amp;lt;Fruit&amp;gt;) {
    println(&quot;Number of fruits: ${fruits.size}&quot;)
}

fun main() {
    val fruits: List&amp;lt;Apple&amp;gt; = listOf(Apple(), Apple())
    receiveFruits(fruits)   // Number of fruits: 2
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경우에는 정상적으로 Number of fruits: 2 라는 문자열이 출력됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단지 Array 를 List 로 바꿨을 뿐인데 왜 List 는 동작하는걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array&amp;lt;T&amp;gt; 는 가변(mutable) 이지만 List&amp;lt;T&amp;gt; 는 불변(immutable) 입니다. 개발자는 Array 의 아이템은 변경할 수 있지만 List 의 아이템은 변경할 수 없습니다. 그렇다면 컴파일러는 어떻게 저 차이점을 알고 알려주는 걸까요?&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;Array&amp;lt;T&amp;gt; 는 class Array&amp;lt;T&amp;gt; 로 정의되어 있고, List&amp;lt;T&amp;gt; 는 interface List&amp;lt;out E&amp;gt; 로 정의되어 있습니다. 가장 큰 차이는 List 제네릭 타입에 사용된 out 입니다. 이에 대해 자세히 살펴보겠습니다.&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;&amp;lt;out T&amp;gt; 으로 공변성(covariance) 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔은 우리가 코틀린에게 타입 안정성을 희생하지 않고 약간의 제약을 풀어달라고 요청해야할 때도 있습니다. 다시 말하면 코틀린 컴파일러가 공변성을 허용해서 제네릭 베이스 타입이 요구되는 곳에 제네릭 파생 타입이 허용되도록 하길 원하는 것인데요. 이럴 때 타입 프로젝션(type projections)이 필요합니다.&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;pre id=&quot;code_1626703609955&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

fun main() {
    val fruitsBasket1 = Array&amp;lt;Fruit&amp;gt;(3) { _ -&amp;gt; Fruit() }
    val fruitsBasket2 = Array&amp;lt;Fruit&amp;gt;(3) { _ -&amp;gt; Fruit() }
    copyFromTo(fruitsBasket1, fruitsBasket2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copyFromTo() 함수는 from 배열의 객체를 순회하면서 to 배열로 값을 넣어주는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 전달 받은 두 배열의 크기는 동일하다고 가정하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현 할 경우에는 별 문제 없이 동작합니다. 왜냐하면 copyFromTo() 함수의 파라미터로 Fruit 타입의 배열로 정의했고, 그에 맞춰 넘겨줬기 때문입니다.&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;하지만 아래와 같이 Fruit 이 아닌 Apple 객체의 배열을 넘겨준다면 컴파일 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626703803500&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

fun main() {
    val fruitsBasket1 = Array&amp;lt;Apple&amp;gt;(3) { _ -&amp;gt; Apple() }
    val fruitsBasket2 = Array&amp;lt;Fruit&amp;gt;(3) { _ -&amp;gt; Fruit() }
    copyFromTo(fruitsBasket1, fruitsBasket2) // type mismatch
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 Array&amp;lt;Fruit&amp;gt; 자리에 Array&amp;lt;Apple&amp;gt; 을 전달하지 못하도록 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 케이스에서는 from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Array&amp;lt;T&amp;gt; 의 T 에 Fruit 클래스나 Fruit 클래스의 하위 클래스가 전달되더라도 아무런 위험이 없습니다. 이런 것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 이야기합니다.&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;Fruit 의 자식 클래스들을 전달 가능하게 해보겠습니다. copyFromTo() 함수의 from 파라미터를 Array&amp;lt;out Fruit&amp;gt; 타입으로 수정해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626704019724&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;out Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

fun main() {
    val fruitsBasket1 = Array&amp;lt;Apple&amp;gt;(3) { _ -&amp;gt; Apple() }
    val fruitsBasket2 = Array&amp;lt;Fruit&amp;gt;(3) { _ -&amp;gt; Fruit() }
    copyFromTo(fruitsBasket1, fruitsBasket2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 코틀린은 from 레퍼런스에 data 가 새로 들어가게 하는 메소드 호출이 없다는 사실을 확인하고 메소드 시그니처가 호출되는 것을 확인하여 이를 검증합니다.&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;이때 Array&amp;lt;out Fruit&amp;gt; 으로 선언된 from 파라미터는 읽기만 가능할 뿐 내용을 변경하거나 추가하려 할 경우에는 컴파일 에러가 발생하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626704259894&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;out Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        from[i] = Fruit() // compile error !
    }
}&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;Array&amp;lt;T&amp;gt; 클래스는 T 타입의 객체를 읽고, 쓰는 메소드 모두를 가지고 있습니다. 하지만 out 키워드를 통해 공변성을 사용하기 위해서는 우리가 코틀린 컴파일러에게 주어진 Array&amp;lt;T&amp;gt; 파라미터에서 어떤 값도 추가하거나 변경하지 않겠다는 약속을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 제네릭 클래스를 사용하는 관점에서 공변성을 이용하는 걸 use-site variance 또는 type projection 이라고 부릅니다. 이와 달리 제네릭 타입을 사용할 때가 아닌 선언할 때 공변성을 사용한다고 지정하는 것을 declation-site variance 라고 부릅니다. 이에 대한 예제로는 처음에 살펴 봤던 List&amp;lt;out T&amp;gt; 로 되어있는 List 인터페이스에서 찾아볼 수 있습니다.&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;List&amp;lt;out T&amp;gt; 로 declation-site variance 가 정의 되어 있기 때문에 receiveFruits() 함수를 정의할 때 파라미터에 List&amp;lt;out Fruit&amp;gt; 형태로 선언하지 않고도 List&amp;lt;Apple&amp;gt; 을 receiveFruits() 함수에 전달할 수 있었던 것입니다.&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;이를 다르게 말하자면, List&amp;lt;out T&amp;gt; 는 코틀린에게 receiveFruits() 를 비롯해 이와 유사한 모든 함수들에게서 List&amp;lt;T&amp;gt; 에 변경이나 추가가 없다는 것을 보장해줍니다. &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;즉, out 키워드를 사용하여 공변성을 사용할 경우에는 읽기만(read-only) 사용 가능하다고 볼 수 있는 것이죠.&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;read-only 가 있다면 반대로 쓰기 전용(write-only) 도 있겠죠? 반대의 경우인 반공변성(contravariance) 에 대해 알아보겠습니다.&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;&amp;lt;in T&amp;gt; 으로 반공변성(contravariance) 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1626704818353&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;out Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copyFromTo() 메소드를 다시 보도록 하겠습니다. T 가 Fruit 타입이거나 Fruit 의 하위 클래스라면 아무 Array&amp;lt;T&amp;gt; 로부터 객체를 복사하는 것이 적절합니다. 공변성은 코틀린이 from 파라미터를 유연하도록 하는 게 안전하다는 사실을 알려줍니다.&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;그렇다면 이번에는 to 파라미터를 살펴보겠습니다. to 파라미터의 타입은 변경 불가능한 Array&amp;lt;Fruit&amp;gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;to 파라미터에 Array&amp;lt;Fruit&amp;gt; 을 전달한다면 아무런 문제도 없습니다. 근데 Fruit collection이나 Fruit 기반의 하위 클래스 collection 에 Fruit 이나 Fruit 의 부모 클래스를 전달하고 싶을 때는 어떨까요?&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;letter-spacing: 0px;&quot;&gt;to 파라미터의 타입을 Array&amp;lt;Any&amp;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;letter-spacing: 0px;&quot;&gt;반공변성을 사용하지 않고 Array&amp;lt;Any&amp;gt; 를 to 자리에 넣으면 어떻게 되는지 확인해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1626705451963&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;out Fruit&amp;gt;, to: Array&amp;lt;Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

fun main() {
    val fruitsBasket1 = Array&amp;lt;Apple&amp;gt;(3) { _ -&amp;gt; Apple() }
    val fruitsBasket2 = Array&amp;lt;Any&amp;gt;(3) { _ -&amp;gt; Any() }
    copyFromTo(fruitsBasket1, fruitsBasket2) // Error! type mismatch
}&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;우리는 코틀린에게 다시 진정하라고 요청할 수 있습니다. 이번에는 to 파라미터에 원래 요청된 타입이나 그 타입의 조상 타입이 가능하게 하는 권한(반공변성)을 요청할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626705570580&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun copyFromTo(from: Array&amp;lt;out Fruit&amp;gt;, to: Array&amp;lt;in Fruit&amp;gt;) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

fun main() {
    val fruitsBasket1 = Array&amp;lt;Apple&amp;gt;(3) { _ -&amp;gt; Apple() }
    val fruitsBasket2 = Array&amp;lt;Any&amp;gt;(3) { _ -&amp;gt; Any() }
    copyFromTo(fruitsBasket1, fruitsBasket2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array&amp;lt;Fruit&amp;gt; 이었던 두 번째 파라미터를 Array&amp;lt;in Fruit&amp;gt; 으로 변경했습니다. in 키워드는 함수가 파라미터에 값을 설정할 수 있게 만들고, 값을 읽을 수 없게 만듭니다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;읽기 전용&lt;/span&gt;은 안에 들어있는 값을 빼서 읽어야 하니까 &lt;span style=&quot;color: #009a87;&quot;&gt;out&lt;/span&gt;, &lt;span style=&quot;color: #006dd7;&quot;&gt;쓰기 전용&lt;/span&gt;은 새로운 값을 집어 넣어야 하니까 &lt;span style=&quot;color: #006dd7;&quot;&gt;in&lt;/span&gt; 으로 외웁시다!&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;/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;where 를 사용한 파라미터 타입 제한&lt;/b&gt;&lt;/h3&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;pre id=&quot;code_1626705947819&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; useAndClose(input: T) {
    input.close()  // ERROR: unresolved reference: close
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 컴파일 에러를 발생시킵니다. 왜냐하면 T 타입에 close() 라는 함수가 있다는 것이 보장되어 있지 않기 때문입니다. 하지만 우리는 코틀린에게 인터페이스를 통해서 close() 함수가 있는 타입만 들어올 수 있도록 제약을 걸 수 있습니다. 예를 들어보자면 AutoCloseable 인터페이스가 있습니다.&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;pre id=&quot;code_1626706054904&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T: AutoCloseable&amp;gt; useAndClose(input: T) {
    input.close()  // OK
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 useAndClose() 함수는 모든 타입이 아닌, AutoCloseable 인터페이스를 구현한 클래스만이 파라미터로 전달 가능하게 됐습니다.&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;이처럼 하나의 제약조건을 넣기 위해서 파라미터 타입 뒤에 콜론(:)을 넣은 후 제약조건을 정의하면 됩니다. 하지만 여러 개의 제약 조건을 넣을 땐 이런 방식으론 불가능합니다. 이럴 때는 where 를 사용해야 합니다.&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;파라미터 타입이 AutoCloseable 을 만족하는 것에 추가로 Appendable 을 제약조건으로 더해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626706329977&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; useAndClose(input: T) where T: AutoCloseable, T: Appendable {
    input.append(&quot;there&quot;)
    input.close()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 메소드 정의 끝부분에 where 절을 쓰고 콤마(,)로 구분해서 제약 조건을 나열합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 우리는 전달받은 파라미터의 close() 함수와 append() 함수를 사용할 수 있게 됩니다.&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;스타 프로젝션(star projection)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린의 제네릭과 Java 의 제네릭의 차이점은 선언처 가변성(declaration-site variance) 뿐만이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 는 개발자가 raw 타입을 직접 만들 수 있습니다. 예를 들어 ArrayList 를 제네릭 사용하지 않고 raw type(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;하지만 raw type 을 직접 만드는 것은 일반적으로 타입 안정성이 없고 가급적 하지 말아야 할 일입니다. 그리고 Java 에서는 함수가 모든 타입의 제네릭 객체를 받아서 읽기 전용으로 사용할 수 있도록 만들기 위해 와일드 카드 타입(?)을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;파라미터 타입을 정의하는 스타 프로젝션(star projection) &amp;lt;*&amp;gt; 은 제네릭 읽기전용 타입과 raw 타입을 위한 코틀린의 기능입니다.&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;/p&gt;
&lt;pre id=&quot;code_1626706885028&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun printValues(values: Array&amp;lt;*&amp;gt;) {
    for (value in values) {
        println(value)
    }
    values[0] = values[1] // ERROR
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;printValues() 함수는 Array&amp;lt;*&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;만약 파라미터를 Array&amp;lt;T&amp;gt; 로 작성했다면 위에서 Error 가 발생한 values[0] = values[1] 코드도 컴파일이 됐을 것입니다. 이 경우 콜렉션을 반복하는 도중 콜렉션을 변경할 가능성에 노출되기 때문에 예기치 못한 오류가 발생할 가능성이 생겨버럽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타 프로젝션은 이런 부주의한 오류로부터 우리를 보호해줍니다. 여기서 사용된 스타 프로젝션&amp;lt;*&amp;gt; 은 out T 와 동일하지만 더 간결하게 작성할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타 프로젝션을 &amp;lt;in T&amp;gt; 방식으로 대체한다면 &amp;lt;in Nothing&amp;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;b&gt;구체화된 타입 파라미터 (Reified Type Parameters)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 에서 제네릭을 사용할 때 Class&amp;lt;T&amp;gt; 를 함수 파라미터로 전달해야 하는 코드를 작성하신 적 있으신가요? 주로 리플렉션을 사용할 때 Class&amp;lt;T&amp;gt; 형태의 타입을 사용해보신 적 있으실 겁니다. 하지만 함수의 파라미터로 Class&amp;lt;T&amp;gt; 를 넘기는 코드는 code smell 로 볼 수 있는데요. 보통 제네릭 함수에서 특정 타입이 필요하지만 자바의 타입 이레이저 때문에 타입 정보를 잃어버릴 경우 필수적으로 따라옵니다.&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;코틀린은 reified 키워드를 통한 구체화된 타입 파라미터(Reified Type Parameter)를 이용해서 code smell 을 제거했습니다.&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;구체화를 확실히 이해하기 위해서는 일단 좀 장황하고 지저분한 코드를 사용해봐야 할 것 같습니다. 앞서 사용했던 Fruit, Apple, Banana 클래스를 활용하여 아래와 같이 함수를 작성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626710757618&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; findFirst(fruits: List&amp;lt;Fruit&amp;gt;, ofClass: Class&amp;lt;T&amp;gt;): T {
    val selected = fruits.filter { fruit -&amp;gt; ofClass.isInstance(fruit) }
    if (selected.isEmpty()) {
        throw RuntimeException(&quot;Not found&quot;)
    }
    return ofClass.cast(selected[0])
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트 코드로 컴파일 되면서 파라미터 타입 T 가 지워지기 때문에 함수 안에서 T 를 fruit is T 나 selected[0] as T 처럼 연산자와 함께 사용할 수 없습니다. 해결 방법으로는 Java 와 코틀린 모두 위 예제와 같이 우리가 원하는 객체의 타입을 파라미터로 던져야합니다.&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;위 예제에서는 ofClass: Class&amp;lt;T&amp;gt; 의 방법을 사용했습니다. 그리고 코드에서 우리는 ofClass 를 타입 체크와 타입 캐스팅을 위해서 사용했습니다. 그 결과 코드가 장황하고 지저분한 느낌이 있습니다. 이런 접근은 함수가 호출될 때마다 런타임 타입 정보를 추가적인 인자로 전달해야만 하기 때문에 함수를 함수를 호출하는 쪽과 받아주는 쪽 모두에게 나쁜 코드를 만들게 됩니다.&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;다행히도 코틀린에는 reified 타입 파라미터라는 훨씬 좋은 기능이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린도 자바와 마찬가지로 타입 이레이저의 한계를 다뤄야 하기 때문에 실행 시간에 파라미터 타입은 사용할 수 없습니다. 하지만 코틀린은 파라미터 타입이 reified 라고 마크되어 있고 함수가 inline 으로 선언되었다면 우리가 파라미터 타입을 사용할 수 있도록 권한을 줍니다. inline 의 장점에 대해서는 추후 별도로 다루도록 하고, 지금은 간단하게 인라인 함수란 컴파일 시점에서 확정되므로 함수 호출 시 오버헤드는 없는 함수 정도로만 알면 되겠습니다.&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;이제 위 findFirst 함수를 리팩토링 해보겠습니다!&lt;/p&gt;
&lt;pre id=&quot;code_1626711173645&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;inline fun &amp;lt;reified T&amp;gt; findFirst(fruits: List&amp;lt;Fruit&amp;gt;): T {
    val selected = fruits.filter { fruit -&amp;gt; fruit is T }
    if (selected.isEmpty()) {
        throw RuntimeException(&quot;Not found&quot;)
    }
    return selected[0] as T
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터 타입 T를 reified 로 선언하고 Class&amp;lt;T&amp;gt; 파라미터를 제거했습니다. 그리고 이제는 함수 안에서 T 를 타입 체크와 캐스팅용으로 사용 가능합니다.&amp;nbsp;&lt;b&gt;함수가 inline 으로 선언되어 있기 때문에 함수의 바디가 함수 호출하는 부분에서 확장됩니다. 그래서 코드가 확장될 때 타입 T 는 컴파일 시간에 확인되는 실제 타입으로 대체됩니다. 참고로 reified 키워드는 inline 함수에서만 사용 가능합니다.&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;이 덕에 함수의 가독성을 높이는 것 이외에도 reified 타입 파라미터를 사용하는 함수를 호출하는 사용자 입장에서도 장점이 있습니다. reified 타입 파라미터는 함수에 추가적인 클래스 정보를 전달하지 않도록 만들어주고, 코드에서 캐스팅을 안전하게 하는 데 도움을 주고 컴파일 시간 안정성을 확보한 채로 리턴 타입을 커스터마이징 할 수 있게 해줍니다.&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;대표적으로 예시로는 코틀린 스탠다드 라이브러리에 있는 listOf&amp;lt;T&amp;gt;() 와 mutableListOf&amp;lt;T&amp;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;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 완전 새로운 수준의 타입 안정성을 추구합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭 함수와 클래스를 사용할 때 개발자의 니즈를 충족시켜 주기 위해서 파라미터 타입을 조정하여 타입 안정성과 유연성을 제공해줍니다. 게다가 reified 타입 파라미터는 컴파일 타임에 타입 안정성을 강화해서 코드의 오류를 제거해줍니다.&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;</description>
      <category>Kotlin</category>
      <category>kotlin</category>
      <category>kotlin generic</category>
      <category>kotlin generic in</category>
      <category>kotlin generic out</category>
      <category>kotlin generic reified</category>
      <category>kotlin generic where</category>
      <category>kotlin reified</category>
      <category>kotlin where</category>
      <category>코틀린 제네릭</category>
      <category>코틀린 제네릭 reified</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/201</guid>
      <comments>https://readystory.tistory.com/201#entry201comment</comments>
      <pubDate>Tue, 20 Jul 2021 01:29:33 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 의외로 놓치기 쉬운 when, 제대로 알아보기</title>
      <link>https://readystory.tistory.com/200</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에는 JAVA 의 switch 문이 없습니다. 대신 when 이 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 자바로 개발하시다가 코틀린을 새롭게 공부하시는 분들은 놓치기 쉬운 특징들이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when 은 표현식으로 사용할 때와 명령문으로 사용할 때 각각 다른 특징을 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 어쩌면 기본적인 문법에 해당할 수 있는 when 이라는 문법에 대해 면밀히 살펴보겠습니다.&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;표현식(expression)으로서의 when&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;표현식(expression)으로서의 when&lt;/span&gt;&lt;/b&gt; 입니다. 코틀린의 가장 큰 특징이라면 함수, if, when 등 자바에서는 명령문으로 사용 되던 문법들이 표현식으로 제공되어 변수에 저장하거나 함수 파라미터, 또는 반환 값에 사용될 수 있다는 것인데요.&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;조건은 평점 3점을 주거나, 장바구니에 담고 2점을 준 고객입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626445983772&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun isPromotionTarget(addToCart: Boolean, rates: Int): Boolean {
    if (rates &amp;lt; 2) { return false }
    if (rates &amp;gt; 3) { return false }
    if (rates == 3) { return true }
    return addToCart &amp;amp;&amp;amp; rates == 2
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when 은 if-else 문이나 표현식을 간단한 형태로 바꿀 수 있습니다. 그럼 when 을 사용하여 리팩토링 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626446195359&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun isPromotionTarget(addToCart: Boolean, rates: Int) = when {
    rates &amp;lt; 2 -&amp;gt; false
    rates &amp;gt; 3 -&amp;gt; false
    rates == 3 -&amp;gt; true
    else -&amp;gt; addToCart &amp;amp;&amp;amp; rates == 2
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 if-else 를 사용했던 버전에서는 리턴 타입을 지정해주고, 블록{}을 이용하여 메소드 바디를 만들었습니다. 그리고 when 을 사용한 이번 버전에서는 리턴타입을 타입 추론을 이용하게 하고 단일 표현식 함수 문법을 사용했습니다.&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;바로 여기서 사용된 when 이 표현식으로써 사용된 것입니다. 함수에 의해서 리턴되는 값은 when 안의 하나의 표현식에서 나온 값입니다. 눈에 띄는 변화로는 if 문을 사용할 때 일일이 나열되었던 return 이라는 키워드가 사라졌습니다. (코틀린에서는 if 문도 표현식이라서 if-else if 로 구성한다면 return 키워드를 사용하지 않을 수 있긴 합니다.)&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;물론 두 버전의 isPromotionTarget() 함수는 동일한 입력으로 동일한 결과를 만들어냅니다. 하지만 when 을 사용한 버전은 비교적(?) 더 나은 가독성과 유지보수성을 제공합니다. 이처럼 when 은 일반적으로 if 에 비해서 간결합니다.&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;* P.S. 여기서 함정은 위 예시는 사실 if 도 when 도 사용하지 않고 간단히 (addToCart &amp;amp;&amp;amp; rates == 2 || rates == 3) 조건식으로 해결할 수 있습니다. 예시를 위해 위처럼 구성한 것이니 when 의 특징에 초점을 두시면 되겠습니다 :)&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;size18&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;코틀린 컴파일러는 when 이 표현식으로 사용 될 때 else 부분이 존재하는지, 표현식이 가능한 모든 입력에 대해 값을 생성하는지 검증합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, when 을 표현식으로 사용했는데 else 가 없거나 처리할 수 없는 입력 케이스가 있다면 컴파일러는 오류를 발생시킵니다.&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;위 예제에서는 when 을 사용할 때 인자(argument) 를 받지 않았는데요. 이번에는 when 에 인자를 전달하는 예시를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626446938808&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun whatToDo(dayOfWeek: Any) = when (dayOfWeek) {
    &quot;Saturday&quot;, &quot;Sunday&quot; -&amp;gt; &quot;Relax&quot;
    in listOf(&quot;Monday&quot;, &quot;Tuesday&quot;, &quot;Wednesday&quot;, &quot;Thursday&quot;) -&amp;gt; &quot;Work hard&quot;
    in 2..4 -&amp;gt; &quot;Work hard&quot;
    &quot;Friday&quot; -&amp;gt; &quot;Party&quot;
    is String -&amp;gt; &quot;What?&quot;
    else -&amp;gt; &quot;No Clue&quot;
}
println(whatToDo(&quot;Sunday&quot;)) // Relax
println(whatToDo(&quot;Wednesday&quot;)) // Work hard
println(whatToDo(3)) // Work hard
println(whatToDo(&quot;Friday&quot;)) // Party
println(whatToDo(&quot;Munday&quot;)) // What?
println(whatToDo(8)) // No Clue&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;이번 예제에서는 when 에 dayOfWeek 이라는 변수를 넘겼습니다. when 안의 모든 조건이 논리 연산자로 이루어진 표현식이었던 이전 예제와는 다르게 이번 예제에서는 예제 안의 조건들이 혼합되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 한 눈에 살펴봐도&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when 안의 첫 번째 라인에는 전달 받은 값이 콤마로 구분된 두 개의 값 중 하나에 해당하는지 확인합니다. 이는 자바에서는 제공하지 않는 문법이기 때문에 눈여겨 보실 필요가 있겠습니다.&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;다음 두 라인(in listOf(...), in 2..4) 은 전달 받은 파라미터가 리스트 안(또는 범위)에 속하는지를 각각 확인합니다.&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;다음은 dayOfWeek 값이 &quot;Friday&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;다음은 전달 받은 값에 대해 타입 체크를 하는 부분입니다. is 라는 키워드는 스마트 캐스팅 기능을 제공하며, 관련해서 다룬 &lt;a href=&quot;https://readystory.tistory.com/184?category=815287&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;포스팅&lt;/a&gt;이 있으니 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 else 는 기본 선택지를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;else 는 위에 설정된 모든 조건에 해당하지 않을 경우에 해당하며, 컴파일러는 else 가 있는지를 강력하게 확인하고, else 가 마지막이 아닌 부분에 오는 것은 허용하지 않습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when 의 각 조건 케이스에 뒤에는 블록{} 이 올 수도 있습니다. 그리고 블록 안의 마지막 표현식이 해당 조건의 결과로 리턴됩니다. 하지만 가독성 측면으로 봤을 때 블록을 이용하지 않는 것이 좋기 때문에 복잡한 로직이 필요하다면 리팩토링을 통해서 함수나 메소드로 분리하고 -&amp;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;b&gt;명령문으로써의 when&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 이상의 값에 기반해서 다른 동작을 원한다면 when 을 표현식이 아니라 명령문으로 사용하면 됩니다. 이전 예제를 변경하여 스트링을 리턴하지 말고 출력해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626448010455&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun printWhatToDo(dayOfWeek: Any) {
    when (dayOfWeek) {
        &quot;Saturday&quot;, &quot;Sunday&quot; -&amp;gt; println(&quot;Relax&quot;)
        in listOf(&quot;Monday&quot;, &quot;Tuesday&quot;, &quot;Wednesday&quot;, &quot;Thursday&quot;) -&amp;gt; println(&quot;Work hard&quot;)
        in 2..4 -&amp;gt; println(&quot;Work hard&quot;)
        &quot;Friday&quot; -&amp;gt; println(&quot;Party&quot;)
        is String -&amp;gt; println(&quot;What?&quot;)
    }
}
printWhatToDo(&quot;Sunday&quot;) // Relax
printWhatToDo(&quot;Wednesday&quot;) // Work hard
printWhatToDo(3) // Work hard
printWhatToDo(&quot;Friday&quot;) // Party
printWhatToDo(&quot;Munday&quot;) // What?
printWhatToDo(8) //&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;위 예제에서 printWhatToDo() 함수의 리턴 타입은 Unit 입니다. 즉, 함수는 아무것도 리턴하지 않고 when 안에서 각각의 조건별로 프린트를 합니다. 앞서 살펴본 예제와 차이점이 있다면 else 문이 제거 되었습니다.&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;when 을 표현식으로 사용할 때는 else 가 없을 경우 컴파일러가 에러 처리를 했었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;코틀린에서 when 이 명령문으로 사용될 때는 else 가 없어도 상관 없습니다.&lt;/b&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;b&gt;when 과 변수 스코프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제들에서는 when 명령문 혹은 표현식의 외부에서 변수를 전달 받아 조건 확인을 위해 사용되었습니다. 그러나 꼭 그래야만 하는 것은 아닙니다. &lt;b&gt;변수가 when 의 스코프에서만 사용된다면 when 으로 변수의 스코프에 제약을 주는 것이 변수가 새어나가서 다르게 사용되는 것을 방지할 수 있고 코드를 관리하기 쉽게 만들어줍니다.&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;다음은 when 을 이용해서 시스템의 코어 숫자를 세는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626451710499&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun systemInfo(): String {
    val numberOfCores = Runtime.getRuntime().availableProcessors()
    return when (numberOfCores) {
        1 -&amp;gt; &quot;1 core, packing this one to the museum&quot;
        in 2..16 -&amp;gt; &quot;You hav $numberOfCores cores&quot;
        else -&amp;gt; &quot;$numberOfCores cores!, I want your machine&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemInf() 함수는 코어의 숫자를 리턴합니다. 위 예제에서 보시다시피 numberOfCores 라는 변수를 when 외부에서 선언하여 when 에 전달하고&amp;nbsp; 있는데요. 이를 다시 작성하여 노이즈를 줄이고 numberOfCores 변수의 스코프를 when 으로 제한해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1626451892549&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun systemInfo(): String =
    when (val numberOfCores = Runtime.getRuntime().availableProcessors()) {
        1 -&amp;gt; &quot;1 core, packing this one to the museum&quot;
        in 2..16 -&amp;gt; &quot;You hav $numberOfCores cores&quot;
        else -&amp;gt; &quot;$numberOfCores cores!, I want your machine&quot;
    }&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;첫 번째, when 의 결과를 바로 리턴하여 바깥쪽 블럭{} 과 return 키워드를 제거해 덜 복잡한 짧은 코드를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 째로 numberOfCores 는 when 의 결과를 얻을 때만 사용 가능하고, 이후의 연산에서는 사용이 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 변수의 스코프를 제한하는 것은 when 에서의 경우 뿐만 아니라 대체로 좋은 디자인입니다.&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;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 다른 프로그래밍 언어(JAVA, C, C++ 등)에 어느정도 지식이 있는 채로 코틀린을 처음 학습 하시는 분들은 when 이 switch 에 대치되는 문법으로만 알고 넘어가기가 쉬워 몇 가지 디테일한 특성들을 놓치기가 쉬운데요. 코틀린은 객체지향 언어 뿐만 아니라, 함수 지향, 스크립트, DSL 지원 등 정말 많은 관점에서 많은 기능들을 제공하고 있기 때문에 기본기를 다지실 때 놓치고 있는 부분은 없나 잘 확인하면서 학습하는 것이 중요해 보입니다.&lt;/p&gt;</description>
      <category>Kotlin</category>
      <category>kotlin if else when</category>
      <category>kotlin java switch</category>
      <category>kotlin switch</category>
      <category>kotlin when</category>
      <category>kotlin when expression</category>
      <category>kotlin when java switch</category>
      <category>코틀린</category>
      <category>코틀린 if else when</category>
      <category>코틀린 switch</category>
      <category>코틀린 when</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/200</guid>
      <comments>https://readystory.tistory.com/200#entry200comment</comments>
      <pubDate>Sat, 17 Jul 2021 01:18:31 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 의외로 잘 모르는 Fragment 의 Lifecycle</title>
      <link>https://readystory.tistory.com/199</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;많은 앱들이 여러가지 이유로 single activity application 을 지향하고 있습니다. 따라서 Fragment 로 UI 를 구성하는 경우가 굉장히 많은데요. 이때 많은 개발자들이 Activity 의 Lifecycle 에 대해서는 잘 알고 있지만 Fragment 의 생명주기에 대해서는 정확하게 모르는 경우가 많습니다. 따라서 이번 포스팅에서는 여러가지 실험을 통해&amp;nbsp;Fragment 의 Lifecycle 에 대해 알아보도록 하겠습니다.&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;Fragment Lifecycle&lt;/b&gt;&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;1004&quot; width=&quot;640&quot; height=&quot;783&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDyVCU/btq9CtTEtoA/kpOuUqYRAw8aVmbyKT7jpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDyVCU/btq9CtTEtoA/kpOuUqYRAw8aVmbyKT7jpk/img.png&quot; data-alt=&quot;Fragment Lifecycle&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDyVCU/btq9CtTEtoA/kpOuUqYRAw8aVmbyKT7jpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDyVCU%2Fbtq9CtTEtoA%2FkpOuUqYRAw8aVmbyKT7jpk%2Fimg.png&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;1004&quot; width=&quot;640&quot; height=&quot;783&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Fragment Lifecycle&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액티비티의 생명주기에 따른 콜백함수와 비교해봤을 때 생성 시에는 onViewCreated() - onViewStateRestored() 가 추가로 있고, 파괴(?) 시에는 onSaveInstanceState() - onDestroyView() 가 추가된 것을 확인할 수 있습니다.&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;액티비티도 마찬가지지만 기본적으로 Lifecycle 은 위에서 아래 방향으로 진행됩니다. 이를테면 Fragment 가 백스택에 최상단으로 올라왔을 경우에는 생명주기가 &lt;span style=&quot;color: #009a87;&quot;&gt;CREATED - STARTED - RESUMED&lt;/span&gt; 순으로 진행되고, 반대로 백스택에서 pop 됐을 경우에는 &lt;span style=&quot;color: #009a87;&quot;&gt;RESUMED - STARTED - CREATED - DESTROYED&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;그림을 잘 보시면 Fragment Lifecycle 과 View Lifecycle 이 상이한 것을 볼 수 있는데요. Fragment 의 Lifecycle 이 변화되는 순간 Fragment Callback 함수를 호출하게 되고, 해당 콜백 함수가 종료되는 시점에 View 의 Lifecycle 에 이벤트를 전달하게 됩니다.&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;디벨로퍼 사이트의 내용을 기반으로 Fragment 와 View 의 각 Lifecycle 에 대해 간단하게 정리해보도록 하겠습니다.&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;onCreate()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Fragment 만 CREATED 가 된 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 FragmentManager 에 add 됐을 때 도달하며 onCreate() 콜백함수를 호출합니다. 주의할 점은 onCreate() 이전에 onAttach() 가 먼저 호출된다는 것입니다. 생각보다 onAttach() 가 먼저 호출된다는 걸 잊어버리기 쉽기 때문에 신경써서 꼭 기억해둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이 시점에는 아직 Fragment View 가 생성되지 않았기 때문에 Fragment 의 View 와 관련된 작업을 두기에 적절하지 않습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onCreate() 콜백 시점에는 Bundle 타입으로 savedInstanceState 파라미터가 함께 제공되는데, 이는 onSaveInstanceState() 콜백 함수에 의에 저장된 Bundle 값입니다. 여기서 또 알아야할 부분은 savedInstanceState 파라미터는 &lt;b&gt;프래그먼트가 처음 생성 됐을 때만 null&lt;/b&gt; 로 넘어오며, onSaveInstanceState() 함수를 재정의하지 않았더라도 그 이후 재생성부터는 non-null 값으로 넘어옵니다.&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;onCreateView(), onViewCreated()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onCreate() 이후에는 onCreateView() 와 onViewCreated() 콜백함수가 이어서 호출됩니다. onCreateView() 의 반환값으로 정상적인 Fragment View 객체를 제공했을 때만 Fragment View 의 Lifecycle 이 생성됩니다.&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;onCreateView() 를 재정의 하여 Fragment View 를 직접 생성하고 inflate 할 수 있지만, LayoutId 를 받는 Fragment 의 생성자를 사용하여 해당 리소스 아이디 값을 통해 onCreateView() 재정의 없이도 Fragment View 를 생성할 수도 있습니다.&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;onCreateView() 를 통해 반환된 View 객체는 onViewCreated() 의 파라미터로 전달되는데, 이 시점부터는 Fragment View 의 Lifecycle 이 INITIALIZED 상태로 업데이트 됐기 때문에 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;View 의 초기값을 설정해주거나 LiveData 옵저빙, RecyclerView 또는 ViewPager2 에 사용될 Adapter 세팅 등은 onViewCreated() 에서 해주는 것이 적절하겠습니다.&lt;/span&gt;&lt;/b&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;b&gt;onViewStateRestored()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onViewStateRestored() 콜백 함수는 아마 생소하신 분들이 많을 것 같은데요.&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;onViewStateRestored() 함수는 저장해둔 모든 state 값이 Fragment 의 View 계층구조에 복원 됐을 때 호출됩니다. 따라서 여기서부터는 체크박스 위젯이 현재 체크 되어있는지 등 각 뷰의 상태값을 체크할 수 있습니다.&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;View lifecycle owner 는 이때 INITIALIZED 상태에서 CREATED 상태로 변경됐음을 알립니다.&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;onStart()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 가 사용자에게 보여질 수 있을 때 호출됩니다. 이는 주로 Fragment 가 attach 되어있는 Activity 의 onStart() 시점과 유사합니다. 이 시점부터는 Fragment 의 child FragmentManager 통해 FragmentTransaction 을 안전하게 수행할 수 있습니다.&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;역시나 Fragment 의 Lifecycle 이 STARTED 로 이동한 후에 Fragment View 의 Lifecycle 또한 STARTED 로 변환됩니다.&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;onResume()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 가 보이는 상태에서 모든 Animator 와 Transition 효과가 종료되고, 프래그먼트가 사용자와 상호작용할 수 있을 때 onResume() 콜백이 호출됩니다. onStart() 와 마찬가지로 주로 Activity 의 onResume() 시점과 유사합니다.&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;Resumed 상태가 됐다는 것은 사용자가 프래그먼트와 상호작용 하기에 적절한 상태가 됐다고 했는데, 이는 반대로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;onResume() 이 호출되지 않은 시점에서는 입력을 시도하거나 포커스를 설정하는 등의 작업을 임의로 하면 안된다는 것을 의미&lt;/b&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;b&gt;onPause()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Fragment 를 떠나기 시작했지만 Fragment 는 여전히 visible 일 때 onPause() 가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;여기서 눈 여겨 볼 점은 Fragment 와 View 의 Lifecycle 이 PAUSED 가 아닌 STARTED 가 된다는 점입니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀히 따지면 &lt;a href=&quot;https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lifecycle&lt;/a&gt; 에 PAUSE 와 STOP 에 해당하는 상태가 없습니다.&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;onStop()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 가 더이상 화면에 보여지지 않게 되면 Fragment 와 View 의 Lifecycle 은 CREATED 상태가 되고, onStop() 콜백 함수가 호출되게 됩니다. 이 상태는 부모 액티비티나 프래그먼트가 중단됐을 때 뿐만 아니라, 부모 액티비티나 프래그먼트의 상태가 저장될 때도 호출됩니다.&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;Fragment 의 onStop() 의 경우 주의해야할 점이 있는데, &lt;b&gt;API 28 버전을 기점으로 onSaveInstanceState() 함수와 onStop() 함수 호출 순서가 달라졌습니다.&lt;/b&gt; 아래 사진에서 보시다시피 API 28 버전부터 onStop() 이 onSaveInstanceState() 함수보다 먼저 호출됨으로써 onStop() 이 FragmentTransaction 을 안전하게 수행할 수 있는 마지막 지점이 되었습니다.&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-origin-width=&quot;966&quot; data-origin-height=&quot;318&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4Zkm/btq9DwbxrgQ/Il287fhextuJbiCRZtZde1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4Zkm/btq9DwbxrgQ/Il287fhextuJbiCRZtZde1/img.png&quot; data-alt=&quot;API 28 버전부터 onStop() 과 onSaveInstanceState() 의 순서가 바뀌었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4Zkm/btq9DwbxrgQ/Il287fhextuJbiCRZtZde1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4Zkm%2Fbtq9DwbxrgQ%2FIl287fhextuJbiCRZtZde1%2Fimg.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;318&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;API 28 버전부터 onStop() 과 onSaveInstanceState() 의 순서가 바뀌었다.&lt;/figcaption&gt;
&lt;/figure&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;b&gt;onDestroyView()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 exit animation 과 transition 이 완료되고, Fragment 가 화면으로부터 벗어났을 경우 Fragment View 의 Lifecycle 은 DESTROYED 가 되고 onDestroy() 가 호출됩니다.&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;이 시점부터는 getViewLifecycleOwnerLiveData() 의 리턴값으로 null 이 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;해당 시점에서는 가비지 컬렉터에 의해 수거될 수 있도록 Fragment View 에 대한 모든 참조가 제거되어야 합니다.&lt;/b&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;b&gt;onDestroy()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 가 제거되거나 FragmentManager 가 destroy 됐을 경우, 프래그먼트의 Lifecycle 은 DESTROYED 상태가 되고, onDestroy() 콜백 함수가 호출됩니다. 해당 지점은 Fragment Lifecycle 의 끝을 알립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 onAttach() 가 onCreate() 이전에 호출 됐던 것처럼 onDetach() 또한 onDestroy() 이후에 호출됩니다.&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;실제로 어떻게 호출되는지 직접 찍어보자!&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백문이 불여일견.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 의 Lifecycle 이 실제로 어떻게 찍히는지 직접 샘플을 통해 살펴보도록 하겠습니다.&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;case 1. FragmentTransaction 을 통해 Fragment 위에 Fragment 를 add 하는 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;425&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7pph0/btq9AIX8WJO/fTFejN9yx4zZ7Kb0KVUxUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7pph0/btq9AIX8WJO/fTFejN9yx4zZ7Kb0KVUxUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7pph0/btq9AIX8WJO/fTFejN9yx4zZ7Kb0KVUxUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7pph0%2Fbtq9AIX8WJO%2FfTFejN9yx4zZ7Kb0KVUxUK%2Fimg.png&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;425&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lifecycle 에 따른 callback 은 위에서 살펴본 순서대로 호출되는 것을 확인할 수 있는데, FragmentTransaction 을 통해 add 하는 경우에는 아래에 깔린 FirstFragment 의 Lifecycle 은 여전히 RESUMED 상태입니다.&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;case 2. FragmentTransaction 을 통해 Fragment 를 replace 하는 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;548&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nUK4s/btq9yNS8YVJ/06EdlfhuuhvgPNg8PyXCdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nUK4s/btq9yNS8YVJ/06EdlfhuuhvgPNg8PyXCdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nUK4s/btq9yNS8YVJ/06EdlfhuuhvgPNg8PyXCdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnUK4s%2Fbtq9yNS8YVJ%2F06EdlfhuuhvgPNg8PyXCdK%2Fimg.png&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;548&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 FragmentTransaction 을 통해 replace 하는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 보듯 FirstFragment 의 Lifecycle 이 onPause() - onStop() 이 호출된 이후에 SecondFragment 가 attach 되고, SecondFragment 의 onStart() 가 호출되고서야 FirstFragment 가 Destroy 되고, 그제서야 SecondFragment 의 onResume() 이 호출됩니다.&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;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;case 3. FragmentTransaction 을 통해 Fragment 를 replace 하면서 addToBackStack() 하는 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;499&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhQ1n0/btq9Eh6i97O/BcJURGOaokOcKB5Ch9tnnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhQ1n0/btq9Eh6i97O/BcJURGOaokOcKB5Ch9tnnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhQ1n0/btq9Eh6i97O/BcJURGOaokOcKB5Ch9tnnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhQ1n0%2Fbtq9Eh6i97O%2FBcJURGOaokOcKB5Ch9tnnk%2Fimg.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;499&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 FragmentTransaction 을 통해 replace 하면서 addBackStack() 까지 실행해주는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 보듯 이 경우에는 backstack 에 Fragment 가 남아있기 때문에 case 2 와 비교했을 때 SecondFragment 의 onStart() 이후에 FirstFragment 에서 onDestroyView() 까지만 호출되고, 그 이후에 onDestroy() 와 onDetach() 는 호출되지 않습니다.&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;이런 디테일이 훗날 예상치 못한 버그를 막을 수 있습니다.&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;case 4. HOME 버튼을 통해 화면을 빠져나간 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;297&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc6if4/btq9yL8YdbR/Q0yx8KW9jlQg51lBV6ipzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc6if4/btq9yL8YdbR/Q0yx8KW9jlQg51lBV6ipzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc6if4/btq9yL8YdbR/Q0yx8KW9jlQg51lBV6ipzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc6if4%2Fbtq9yL8YdbR%2FQ0yx8KW9jlQg51lBV6ipzk%2Fimg.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;297&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Fragment 가 화면에 보여진 이후 홈 버튼을 통해 화면을 벗어났을 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 보시다시피 onResume() 이후에 홈 버튼을 누르면 onPause() - onStop() - onSaveInstanceState() 까지만 호출되고, onDestroyView() 부터 그 이후의 라이프 사이클 콜백은 호출되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이때 알아두셔야 하는 것은 onStop() 상태에 있는 동안 메모리 부족 상황 등의 경우, 시스템에 의해 Destroy 될 수 있습니다.&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;그리고 위 사진은 API 29 레벨에서 테스트 했기 때문에 onStop() 콜백이 onSaveInstanceState() 이전에 호출 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/fragment-lifecycle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; 에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>Android Fragment</category>
      <category>Android Fragment Lifecycle</category>
      <category>Android Fragment Lifecycle Example</category>
      <category>Android View Lifecycle</category>
      <category>Android ViewLifecycleOwner</category>
      <category>Deep dive Fragment Lifecycle</category>
      <category>Fragment Lifecycle</category>
      <category>Fragment Lifecycle 제대로 알기</category>
      <category>View Lifecycle</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/199</guid>
      <comments>https://readystory.tistory.com/199#entry199comment</comments>
      <pubDate>Thu, 15 Jul 2021 00:05:20 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 사용자의 활동 상태(걷기, 자전거, 자동차 등)를 알려주는 Activity Recognition Transition API</title>
      <link>https://readystory.tistory.com/198</link>
      <description>&lt;h3&gt;&lt;b&gt;Activity Recognition Transition API ?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;처음에 이 API 에 대한 소개를 봤을 때만 해도, Activity 가 흔히 안드로이드의 4대 컴포넌트 중 하나인 액티비티인 줄 알고 해석을 했었는데 알고보니 위 Activity 는 정말 단어 그대로 사용자의 &quot;활동&quot; 입니다. 그러니 헷갈리지 않게 잘 분별하도록 합시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;옛날에는 스마트폰 사용자가 현재 걷고 있는 중인지, 달리는 중인지, 차를 탔는지 등에 대해 알기 위해서 따로 제공해주는 API 가 없어 개발자가 일일히 위치 센서와 자이로 센서 등을 활용하여 나름의 계산식을 세워 구해야만 했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 지금은 안드로이드에서 Activity Recognition Transition API 를 제공해주고 있기 때문에, 특별한 이유가 따로 있지 않은 이상 손쉽게 유저의 활동 상태를 알 수 있게 됐습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;환경 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;먼저 Activity Recognition Transition API 를 사용하기 위해서는 2가지 작업이 준비되어야 합니다. 하나는 Google 의 play-services-location 라이브러리에 대해 의존성을 갖는 것이고, 나머지 하나는 AndroidManifest.xml 파일에 권한을 선언해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. build.gradle (app 수준)&lt;/p&gt;
&lt;pre id=&quot;code_1616678579156&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'com.google.android.gms:play-services-location:17.1.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. AndroidManifest.xml&lt;/p&gt;
&lt;pre id=&quot;code_1616678852149&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
         package=&quot;com.example.myapp&quot;&amp;gt;
  
  &amp;lt;!-- API 버전 28 이하 --&amp;gt;
  &amp;lt;uses-permission android:name=&quot;com.google.android.gms.permission.ACTIVITY_RECOGNITION&quot; /&amp;gt;
  &amp;lt;!-- API 버전 29 이상 --&amp;gt;
  &amp;lt;uses-permission android:name=&quot;android.permission.ACTIVITY_RECOGNITION&quot; /&amp;gt;

  ...
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보시다시피 Android API 29 버전을 기준으로 선언해줘야 하는 권한이 다르기 때문에 반드시 둘 다 선언해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 API 버전 29 이상에서 선언해준 ACTIVITY_RECOGNITION 권한은 Runtime Permission 이기 때문에 액티비티 구현부에 런타임 권한을 요청하는 코드를 추가해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;ActivityTransitionRequest 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;ActivityTransitionRequest 객체는 유저의 활동이 변경 됐을 때 통지되는 객체입니다. 이를 생성하기 위해서는 ActivityTransition 객체를 담고 있는 리스트 객체를 선언하고, 수신하고자 하는 활동에 대해 추가해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ActivityTransition 객체는 Builder 객체를 통해 생성하며, DetectedActivity 클래스에 정의된 상수(IN_VEHICLE, ON_BICYCLE, ON_FOOT, STILL, WALKING, RUNNING 등)를 통해 type 을 설정하고 ActivityTransition 클래스에 정의된 상수(&lt;span&gt;ACTIVITY_TRANSITION_ENTER&lt;/span&gt;, &lt;span&gt;ACTIVITY_TRANSITION_EXIT &lt;/span&gt;등) 을 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ActivityTransition 에 대해 보다 자세한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://developers.google.com/android/reference/com/google/android/gms/location/ActivityTransition&quot;&gt;여기&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;를 참고하세요.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예시는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1616683004684&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val transitions: List&amp;lt;ActivityTransition&amp;gt; = 
        listOf(
            ActivityTransition.Builder()
                .setActivityType(DetectedActivity.WALKING)	// 걷기
                .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
                .build(),
            ActivityTransition.Builder()
                .setActivityType(DetectedActivity.WALKING)
                .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
                .build(),
            ActivityTransition.Builder()
                .setActivityType(DetectedActivity.STILL)	// 움직임 없음
                .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
                .build(),
            ActivityTransition.Builder()
                .setActivityType(DetectedActivity.STILL)
                .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
                .build()
        )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 ActivityTransitionRequest 객체를 생성할 수 있습니다..&lt;/p&gt;
&lt;pre id=&quot;code_1616683254073&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val request = ActivityTransitionRequest(transitions)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;PendingIntent 활용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;ActivityTransitionRequest 객체를 생성했다면 이제 PendingIntent 를 생성해줍니다. PendingIntent 를 생성하기 위한 다양한 방법이 있지만 이번 예제에서는 BroadcastReceiver 를 등록한 상황을 가정해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1616686357838&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val intent = Intent(TRANSITIONS_RECEIVER_ACTION)  // 자신만의 action 값을 정의합니다
val pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 request 와 pendingIntent 가 준비 되었으니 등록해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1616686486634&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ActivityRecognition.getClient(this)
    .requestActivityTransitionUpdates(request, pendingIntent)
    .addOnSuccessListener {
        Log.d(LOG_TAG, &quot;Transitions API was successfully registered&quot;)
    }.addOnFailureListener { e -&amp;gt;
        Log.d(LOG_TAG, &quot;Transitions Api could not be registered : $e&quot;)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;등록하는 시점에 대해 주의할 점은 API 29 버전 이상에 대해서는 런타임 권한 요청이 승인된 경우에 등록하도록 신경써줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;등록하는 것과 반대로 해지하는 것도 비슷합니다. 주로 onStop() 콜백에서 해지해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1616686813841&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ActivityRecognition.getClient(this)
    .removeActivityTransitionUpdates(pendingIntent)
    .addOnSuccessListener {
        Log.d(LOG_TAG, &quot;Transitions successfully unregistered.&quot;)
    }
    .addOnFailureListener { e -&amp;gt;
        Log.d(LOG_TAG, &quot;Transitions could not be unregistered : $e&quot;)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 BroadcastReceiver 와 ActivityRecognition 을 등록했다면, BroadcastReceiver 의 onReceive() 에 다음과 같이 작성하여 Transition 이벤트를 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1616686997902&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun onReceive(context: Context?, intent: Intent?) {
    // 앞서 intent 에 설정해둔 action 과 일치하는지 검사
    if (intent?.action != TRANSITIONS_RECEIVER_ACTION) {
        return
    }

    if (ActivityTransitionResult.hasResult(intent)) {
        val result: ActivityTransitionResult = ActivityTransitionResult.extractResult(intent) ?: return

        for (event in result.transitionEvents) {
            val message = &quot;Transition : ${if(event.activityType == DetectedActivity.WALKING) &quot;WALKING&quot; else &quot;STILL&quot;} (${event.transitionType})&quot;
            Log.d(LOG_TAG, message)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 동작은 에뮬레이터로 확인하기에는 어려움이 있기 때문에 실제 디바이스에서 테스트 할 것을 권장합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에 대한 예제는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/activity-recognition-transition-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>Android</category>
      <category>android activity recognition transition api</category>
      <category>android art api</category>
      <category>android recognition</category>
      <category>android transition api</category>
      <category>안드로이드 걷기 인식</category>
      <category>안드로이드 달리기 인식</category>
      <category>안드로이드 움직임 탐지</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/198</guid>
      <comments>https://readystory.tistory.com/198#entry198comment</comments>
      <pubDate>Fri, 26 Mar 2021 00:48:03 +0900</pubDate>
    </item>
    <item>
      <title>[Android] DataBinding &amp;quot;unresolved reference BR&amp;quot; 해결하기</title>
      <link>https://readystory.tistory.com/197</link>
      <description>&lt;h3&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;안드로이드 데이터 바인딩을 사용하다보면 바인딩 관련해서 컴파일 단계에서 생성해주는 다양한 클래스들 때문에 예상치 못한 오류들을 많이 만나게 됩니다.&lt;/p&gt;
&lt;p&gt;그 중 하나가 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&quot;unresolved reference BR&quot;&lt;/span&gt;&lt;/b&gt; 컴파일 오류인데, 구글링에 마땅히 잘 정리된 해결 방법이 없어 정리하려 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;해결 방법 1&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;app 수준의 build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1604674684085&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apply plugin: 'kotlin-kapt'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코틀린 프로젝트인 경우 위처럼 'kotlin-kapt' 플러그인을 추가해줘야합니다. 그래들 플러그인 버전 3.1 버전 이하를 사용하신다면 com.android.databinding:compiler:x.y.z 에 대한 디펜던시도 추가해줘야 하지만, 3.2 버전 이상의 그래들 플러그인을 사용하고 계신다면 내장되어 있기 때문에 'kotlin-kapt' 만 추가해주면 됩니다.&lt;/p&gt;
&lt;p&gt;자바 프로젝트의 경우 추가해주지 않아도 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;해결 방법 2&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;사실 해결 방법 1에 대해서는 구글링하면 쉽게 찾아 볼 수 있지만, 제 경우에는 'kotlin-kapt' 플러그인을 추가해도 해결되지 않았었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;제 경우에는 안드로이드 스튜디오에서 BR 에 대해 자동 import 를 해주지 않아서 해결되지 않았었는데요.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;직접 import 문을 넣어주면 해결됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604675489270&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import androidx.databinding.library.baseAdapters.BR&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Resolution</category>
      <category>android unresolved reference BR</category>
      <category>android unresolved reference BR 해결</category>
      <category>unresolved reference BR</category>
      <category>unresolved reference BR 해결</category>
      <category>데이터바인딩 unresolved reference BR</category>
      <category>안드로이드 unresolved reference BR</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/197</guid>
      <comments>https://readystory.tistory.com/197#entry197comment</comments>
      <pubDate>Sat, 7 Nov 2020 00:11:56 +0900</pubDate>
    </item>
    <item>
      <title>[Android] MVVM 패턴에서 이벤트 처리하기</title>
      <link>https://readystory.tistory.com/196</link>
      <description>&lt;h3&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;MVVM 아키텍처는 마이크로소프트에서 제안한 패턴으로, &lt;b&gt;Model-View-ViewModel&lt;/b&gt; 의 약자를 딴 아키텍처입니다.&lt;/p&gt;
&lt;p&gt;Model 은 데이터와 비즈니스 로직을 포함하고, View 는 UI 를 나타내며 ViewModel 은 Model 과 View 사이에서 상호작용을 담당합니다.&lt;/p&gt;
&lt;p&gt;그러면 MVVM 패턴에서 이벤트 처리는 어디서 할까요? 바로 &lt;b&gt;ViewModel&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드에서 MVVM 패턴으로 구현하기 위해서는 Jetpack 에서 제공하는 데이터 바인딩을 사용해야 하는데요. 지금부터 AAC ViewModel 과 LiveData, 그리고 DataBinding 을 사용하여 이벤트 처리를 어떻게 다룰 수 있는지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래 설명에서 사용되는 예제는 &lt;a href=&quot;https://github.com/droidknights/DroidKnights2020_App&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;드로이드 나이츠 2020&lt;/a&gt; 에서 참고하여 작성한 코드입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Event 클래스 정의하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;MVVM 에서 이벤트를 처리할 때 가장 고민되는 부분은 &quot;Context 에 대한 의존이 필요한 경우에 어떻게 처리해야 하나?&quot;입니다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;디벨로퍼 사이트에 따르면 Jetpack 에서 제공하는 AAC ViewModel 에서는 Context 이나 View 에 대한 의존을 갖고 있지 말라고 권장하고 있기 때문인데요.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 Event 처리를 담당할 Event 클래스를 정의해보겠습니다. 이번에 정의하는 Event 클래스는 LiveData 에서 사용할 예정입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1600615755502&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Event&amp;lt;out T&amp;gt;(private val content: T) {

    private var hasBeenHandled = false

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Event 클래스에서 선언된 hasBeenHandled 변수는 &lt;b&gt;하나의 이벤트 처리에 대해 한 번만 처리하기 위해서 사용됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;DataBinding + &lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AAC ViewModel + &lt;/b&gt;LiveData&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;이제 Event 클래스를 사용하여 예제를 작성해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;버튼을 누르면 EditText 에서 작성된 텍스트가 다음 액티비티에서 표시되도록 하는 샘플 앱을 작성해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MainViewModel&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600616562215&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainViewModel : ViewModel() {
    private val _openEvent = MutableLiveData&amp;lt;Event&amp;lt;String&amp;gt;&amp;gt;()
    val openEvent: LiveData&amp;lt;Event&amp;lt;String&amp;gt;&amp;gt; get() = _openEvent

    val sampleText: MutableLiveData&amp;lt;String&amp;gt; = MutableLiveData()

    fun onClickEvent(text: String) {
        _openEvent.value = Event(text)
    }
}

inline fun &amp;lt;T&amp;gt; LiveData&amp;lt;Event&amp;lt;T&amp;gt;&amp;gt;.eventObserve(
    owner: LifecycleOwner,
    crossinline onChanged: (T) -&amp;gt; Unit
): Observer&amp;lt;Event&amp;lt;T&amp;gt;&amp;gt; {
    val wrappedObserver = Observer&amp;lt;Event&amp;lt;T&amp;gt;&amp;gt; { t -&amp;gt;
        t.getContentIfNotHandled()?.let {
            onChanged.invoke(it)
        }
    }
    observe(owner, wrappedObserver)
    return wrappedObserver
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;LiveData 를 수신 객체로 하는 eventObserve() 확장 함수를 정의해줍니다. 확장 함수를 정의해서 사용하는 이유는 Event 클래스에서 정의한 getContentIfNotHandled() 함수를 통해서 하나의 이벤트 당 한 번의 처리를 하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러면 이제 이 MainViewModel 을 사용해서 UI 를 작성해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600617344265&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;layout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&amp;gt;

    &amp;lt;data&amp;gt;
        &amp;lt;variable
            name=&quot;vm&quot;
            type=&quot;com.ready.blog.samples.MainViewModel&quot; /&amp;gt;
    &amp;lt;/data&amp;gt;

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        tools:context=&quot;.MainActivity&quot;&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edit_text&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;@={vm.sampleText}&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/event_btn&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;SEND&quot;
            android:onClick=&quot;@{() -&amp;gt; vm.onClickEvent(vm.sampleText)}&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/edit_text&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;
&amp;lt;/layout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보시면 two-way binding 을 사용하여 EditText 와 vm.sampleText 를 연결해주었고, Button 을 클릭하면 MainViewModel 에서 정의한 onClickEvent() 함수에 해당 텍스트를 넘기면서 호출하는 방식으로 UI 를 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MainActivity&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600617432550&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DataBindingUtil.setContentView&amp;lt;ActivityMainBinding&amp;gt;(this, R.layout.activity_main).apply {
            lifecycleOwner = this@MainActivity
            vm = viewModel
        }

        initObserve()
    }

    private fun initObserve() {
        viewModel.openEvent.eventObserve(this) { sampleText -&amp;gt;
            val intent = Intent(this, NextActivity::class.java)
            intent.putExtra(&quot;sample&quot;, sampleText)
            startActivity(intent)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Activity 코드를 살펴보시면, DataBinding을 통해 activity_main.xml 을 연결해주고 난 뒤 initObserve() 함수를 호출하여 Event 객체로 들어온 Text 를 인텐트에 담아 NextActivity 를 실행하는 코드입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_next.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600617999969&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.NextActivity&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/sample_text&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;NextActivity&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600618015038&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class NextActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_next)

        sample_text.text = intent.getStringExtra(&quot;sample&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;편의상 NextActivity 는 DataBinding 을 사용하지 않았습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;실행 화면&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;device-2020-09-21-010954.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtBVT/btqI6NqlC4a/TClcrLvhqKkLMwmzNYCEvK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtBVT/btqI6NqlC4a/TClcrLvhqKkLMwmzNYCEvK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtBVT/btqI6NqlC4a/TClcrLvhqKkLMwmzNYCEvK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/QtBVT/btqI6NqlC4a/TClcrLvhqKkLMwmzNYCEvK/img.gif&quot; data-filename=&quot;device-2020-09-21-010954.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;위 예제에서 사용된 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/mvvm-event-handling&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; 에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android databinding onClickEvent</category>
      <category>android event</category>
      <category>android handle event</category>
      <category>android mvvm event</category>
      <category>android viewmodel onClickEvent</category>
      <category>안드로이드 MVVM 이벤트</category>
      <category>안드로이드 MVVM 이벤트 처리</category>
      <category>안드로이드 MVVM 패턴 이벤트</category>
      <category>안드로이드 데이터바인딩 이벤트 처리</category>
      <category>안드로이드 이벤트 처리</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/196</guid>
      <comments>https://readystory.tistory.com/196#entry196comment</comments>
      <pubDate>Mon, 21 Sep 2020 01:16:14 +0900</pubDate>
    </item>
    <item>
      <title>[구조 패턴] 데코레이터 패턴(Decorator Pattern) 이해 및 예제</title>
      <link>https://readystory.tistory.com/195</link>
      <description>&lt;p&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;데코레이터 패턴(Decorator Pattern)은 Flyweight 패턴, Adapter 패턴, Bridge 패턴처럼 구조 패턴 중 하나로, 런타임에서 객체의 기능을 수정하는데 사용되는 패턴입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;구조 패턴(Structural Pattern)이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;구조 패턴이란 작은 클래스들을 상속과 합성을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴입니다.&lt;/p&gt;
&lt;p&gt;이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 양 사용할 수 있습니다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것입니다. 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일 단계에서가 아닌 런타임 단계에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;데코레이터 패턴 이해 및 예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;데코레이터 패턴은 상속(Inheritance)과 합성(Composition)을 사용하여 객체에 동적으로 책임을 추가할 수 있게 합니다. 이 방법은 서브 클래스를 생성하는 것보다 유연한 방법을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 다양한 종류의 자동차를 구현한다고 가정해보겠습니다. Car 라는 인터페이스를 정의하고 이를 구현하는 Basic Car 를 두고, 이를 확장하여 구현하는 Sports Car, Luxury Car 를 정의해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그림으로 나타내면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkKEX1/btqI7wBF8Ub/gmd80zqVY2EdK7krMQ3nzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkKEX1/btqI7wBF8Ub/gmd80zqVY2EdK7krMQ3nzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkKEX1/btqI7wBF8Ub/gmd80zqVY2EdK7krMQ3nzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkKEX1%2FbtqI7wBF8Ub%2Fgmd80zqVY2EdK7krMQ3nzk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그런데 만약 런타임 단계에서 Sports Car의 특징과 Luxury Car의 특징을 모두 갖고 있는 Car 를 얻고 싶다면 어떻게 해야할까요? 일반적으로 둘의 기능을 갖는 또다른 서브 클래스를 정의할 수도 있겠습니다만, 데코레이터 패턴을 사용하여 해결해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Car.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593209173&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Car {
	public void assemble();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;BasicCar.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593237408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BasicCar implements Car {

	@Override
	public void assemble() {
		System.out.print(&quot;Basic Car.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;CarDecorator.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593262683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CarDecorator implements Car {

	protected Car car;
	
	public CarDecorator(Car c){
		this.car=c;
	}
	
	@Override
	public void assemble() {
		this.car.assemble();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;SportsCar.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593285927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SportsCar extends CarDecorator {

	public SportsCar(Car c) {
		super(c);
	}

	@Override
	public void assemble(){
		super.assemble();
		System.out.print(&quot; Adding features of Sports Car.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;LuxuryCar.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593308306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LuxuryCar extends CarDecorator {

	public LuxuryCar(Car c) {
		super(c);
	}
	
	@Override
	public void assemble(){
		super.assemble();
		System.out.print(&quot; Adding features of Luxury Car.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 모든 클래스가 준비되었으니 테스트 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;DecoratorPatternTest.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600593386254&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DecoratorPatternTest {

	public static void main(String[] args) {
		Car sportsCar = new SportsCar(new BasicCar());
		sportsCar.assemble();
		System.out.println(&quot;\n*****&quot;);
		
		Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar()));
		sportsLuxuryCar.assemble();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1600593659663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Basic Car. Adding features of Sports Car.
*****
Basic Car. Adding features of Luxury Car. Adding features of Sports Car.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보시다시피 CarDecorator 를 사용하여 SportsCar와 LuxuryCar 의 기능을 합성할 수 있게 됐습니다. 이렇게 작성할 경우 클래스의 종류가 많아질 수록 큰 효과를 발휘할 수 있는데요. 런타임에서 다양한 기능을 조합하여 기능을 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;중요 포인트&lt;/b&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;/li&gt;
&lt;li&gt;단점이 있다면, 다수의 데코레이터 객체를 생성하고 사용해야 한다는 것입니다.&lt;/li&gt;
&lt;li&gt;JDK 에서 FileReader, BufferedReader 등 IO 클래스에 사용되는 패턴입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>JAVA/Design Pattern</category>
      <category>Decorator Pattern</category>
      <category>decorator pattern example</category>
      <category>decorator pattern java</category>
      <category>java decorator pattern</category>
      <category>데코레이터 패턴</category>
      <category>데코레이터 패턴 예제</category>
      <category>데코레이터 패턴 코드</category>
      <category>자바 데코레이터 패턴</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/195</guid>
      <comments>https://readystory.tistory.com/195#entry195comment</comments>
      <pubDate>Sun, 20 Sep 2020 18:32:44 +0900</pubDate>
    </item>
    <item>
      <title>[구조 패턴] 브릿지 패턴(Bridge Pattern) 이해 및 예제</title>
      <link>https://readystory.tistory.com/194</link>
      <description>&lt;p&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;브릿지 패턴(Bridge Pattern)은 Flyweight 패턴, Adapter 패턴, Decorator 패턴처럼 구조 패턴 중 하나로, 두 인터페이스에 계층 구조(Hierarchy)를 가지고 있을 때 인터페이스를 구현(implements)으로부터 분리하고 클라이언트 프로그램으로부터 구현 세부사항을 숨기기 위해 사용되는 패턴입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;구조 패턴(Structural Pattern)이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;구조 패턴이란 작은 클래스들을 상속과 합성을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴입니다.&lt;/p&gt;
&lt;p&gt;이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 양 사용할 수 있습니다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것입니다. 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일 단계에서가 아닌 런타임 단계에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;브릿지 패턴 이해 및 예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;디자인 패턴의 교과서인 GoF에서는 브릿지 패턴에 대해 다음과 같이 정의하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;추상화(abstraction)를 구현으로부터 분리하여 각각 독립적으로 변화할 수 있도록 하는 패턴&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예제를 통해 이해를 돕도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 구조의 인터페이스를 구현하는 프로그램을 만든다고 가정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nrQF0/btqIkuSK1sh/5pakKpU8Qk7aFi9S8aJsqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nrQF0/btqIkuSK1sh/5pakKpU8Qk7aFi9S8aJsqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nrQF0/btqIkuSK1sh/5pakKpU8Qk7aFi9S8aJsqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnrQF0%2FbtqIkuSK1sh%2F5pakKpU8Qk7aFi9S8aJsqK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;보시다시피 Shape 과 Color 라는 2가지 인터페이스가 있습니다.&lt;/p&gt;
&lt;p&gt;이제 브릿지 패턴을 사용하여 두 인터페이스의 compoisition 을 구성해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Color.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662369317&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Color {

	public void applyColor();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Shape.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662386714&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class Shape {
	//Composition
	protected Color color;
	
	//constructor with implementor as input argument
	public Shape(Color c){
		this.color=c;
	}
	
	abstract public void applyColor();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Shape 클래스가 Color 인터페이스를 소유하고 있고, applyColor() 메소드는 abstract 로 선언하여 하위 클래스에게 구현을 위임합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 이 Shape 추상 클래스를 상속하여 구체화 하는 Triangle 클래스와 Pentagon 클래스를 정의해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Triangle.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662515256&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Triangle extends Shape {

	public Triangle(Color c) {
		super(c);
	}

	@Override
	public void applyColor() {
		System.out.print(&quot;Triangle filled with color &quot;);
		color.applyColor();
	} 

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Pentagon.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662566183&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Pentagon extends Shape {

	public Pentagon(Color c) {
		super(c);
	}

	@Override
	public void applyColor() {
		System.out.print(&quot;Pentagon filled with color &quot;);
		color.applyColor();
	} 

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 각 Shape 클래스가 소유할 Color 인터페이스의 구현 객체를 정의해보겠습니다. 컬러는 RedColor 와 GreenColor 로 선언하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;RedColor.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662650271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RedColor implements Color{

	public void applyColor(){
		System.out.println(&quot;red.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;GreenColor.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662663324&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class GreenColor implements Color{

	public void applyColor(){
		System.out.println(&quot;green.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 위에서 정의한 클래스들을 사용하여 테스트 해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;BridgePatternTest.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662715119&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BridgePatternTest {

	public static void main(String[] args) {
		Shape tri = new Triangle(new RedColor());
		tri.applyColor();
		
		Shape pent = new Pentagon(new GreenColor());
		pent.applyColor();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599662839875&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Triangle filled with color red.
Pentagon filled with color green.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;브릿지 디자인 패턴은 추상화(abstraction)와 구현(implement)이 독립적으로 다른 계층 구조를 가질 수 있고, 클라이언트 어플리케이션으로부터 구현을 숨기고 싶을 때 사용될 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;위 예제의 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-JVM/tree/java/bridge-pattern&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; 에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>JAVA/Design Pattern</category>
      <category>bridge design pattern</category>
      <category>bridge pattern</category>
      <category>bridge pattern example</category>
      <category>bridge pattern java example</category>
      <category>java bridge pattern</category>
      <category>브릿지 디자인 패턴</category>
      <category>브릿지 패턴</category>
      <category>브릿지 패턴 예제</category>
      <category>자바 브릿지 패턴</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/194</guid>
      <comments>https://readystory.tistory.com/194#entry194comment</comments>
      <pubDate>Thu, 10 Sep 2020 00:03:06 +0900</pubDate>
    </item>
    <item>
      <title>[구조 패턴] 퍼사드 패턴(Facade Pattern) 이해 및 예제</title>
      <link>https://readystory.tistory.com/193</link>
      <description>&lt;p&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;퍼사드 패턴(Facade Pattern)은 Flyweight 패턴, Adapter 패턴, Decorator 패턴처럼 구조 패턴 중 하나로, 클라이언트가 쉽게 시스템과 상호작용 할 수 있도록 도와주는 패턴입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;구조 패턴(Structural Pattern)이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;구조 패턴이란 작은 클래스들을 상속과 합성을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴입니다.&lt;/p&gt;
&lt;p&gt;이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 양 사용할 수 있습니다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것입니다. 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일 단계에서가 아닌 런타임 단계에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;퍼사드 패턴 이해 및 예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;디자인 패턴의 교과서인 GoF에서는 퍼사드 패턴에 대해 다음과 같이 정의하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;퍼사드 패턴은 서브시스템을 더 쉽게 사용할 수 있도록 higher-level 인터페이스를 정의하고, 제공합니다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이해를 돕기 위해 예시를 들어보겠습니다.&lt;/p&gt;
&lt;p&gt;MySql 과 Oracle 데이터베이스를 사용하는 인터페이스가 있고, 이를 이용해 HTML 리포트 또는 PDF 리포트를 생성해야 한다고 가정해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;우리는 이 인터페이스들을 서로 조합하여 리포트를 만들어야 합니다. 지금은 database connection 방법 2가지, generate report 방법 2가지의 인터페이스를 가지고서 조합을 해야 하지만 만약 경우의 수가 더 복잡해질 경우 일반적인 방법으로는 관리하기가 어려워질 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래서 우리는 퍼사드 패턴을 적용하여 wrapper 인터페이스를 제공하여 클라이언트가 보다 쉽게 관리할 수 있도록 도와보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MySqlHelper.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599480004965&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Connection;

public class MySqlHelper {
	
	public static Connection getMySqlDBConnection(){
		// 실제 커넥션을 리턴해야 하지만, 예제이기에 null 을 리턴하겠습니다.
		return null;
	}
	
	public void generateMySqlPDFReport(String tableName, Connection con){
		// get data from table and generate pdf report
	}
	
	public void generateMySqlHTMLReport(String tableName, Connection con){
		// get data from table and generate pdf report
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;OracleHelper.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599480055844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Connection;

public class OracleHelper {

	public static Connection getOracleDBConnection(){
		// 실제 커넥션을 리턴해야 하지만, 예제이기에 null 을 리턴하겠습니다.
		return null;
	}
	
	public void generateOraclePDFReport(String tableName, Connection con){
		// get data from table and generate pdf report
	}
	
	public void generateOracleHTMLReport(String tableName, Connection con){
		// get data from table and generate pdf report
	}
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 퍼사드 패턴 인터페이스를 생성해보겠습니다. 타입 안정성을 위해서 디비 타입과 리포트 타입에 대해서는 Enum을 사용하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;HelperFacade.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599480187610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Connection;

public class HelperFacade {

	public static void generateReport(DBTypes dbType, ReportTypes reportType, String tableName){
		Connection con = null;
		switch (dbType){
		case MYSQL: 
			con = MySqlHelper.getMySqlDBConnection();
			MySqlHelper mySqlHelper = new MySqlHelper();
			switch(reportType){
			case HTML:
				mySqlHelper.generateMySqlHTMLReport(tableName, con);
				break;
			case PDF:
				mySqlHelper.generateMySqlPDFReport(tableName, con);
				break;
			}
			break;
		case ORACLE: 
			con = OracleHelper.getOracleDBConnection();
			OracleHelper oracleHelper = new OracleHelper();
			switch(reportType){
			case HTML:
				oracleHelper.generateOracleHTMLReport(tableName, con);
				break;
			case PDF:
				oracleHelper.generateOraclePDFReport(tableName, con);
				break;
			}
			break;
		}
		
	}
	
	public static enum DBTypes{
		MYSQL,ORACLE;
	}
	
	public static enum ReportTypes{
		HTML,PDF;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드가 모두 준비되었다면 이제 메인 함수를 통해 테스트해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;FacadePatternTest.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599480385301&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Connection;

import com.journaldev.design.facade.HelperFacade;
import com.journaldev.design.facade.MySqlHelper;
import com.journaldev.design.facade.OracleHelper;

public class FacadePatternTest {

	public static void main(String[] args) {
		String tableName=&quot;Employee&quot;;
		
		//generating MySql HTML report and Oracle PDF report using Facade
		HelperFacade.generateReport(HelperFacade.DBTypes.MYSQL, HelperFacade.ReportTypes.HTML, tableName);
		HelperFacade.generateReport(HelperFacade.DBTypes.ORACLE, HelperFacade.ReportTypes.PDF, tableName);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보시다시피 퍼사드 패턴을 사용하게 되면 클라이언트 사이드에서 무겁게 로직을 작성할 필요 없이 쉽고 깔끔하게 리포트를 생성할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;퍼사드 패턴의 중요 포인트!&lt;/b&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;/li&gt;
&lt;li&gt;퍼사드 패턴은 특정 기능에 대해 인터페이스의 수가 확장되고(위 예제로 치면 디비 종류나 리포트 종류가 늘어난다는 등), 시스템이 복잡해질 수 있는 상황에서 사용하기 적합합니다.&lt;/li&gt;
&lt;li&gt;퍼사드 패턴은 비슷한 작업을 해야하는 다양한 인터페이스들 중 하나의 인터페이스를 클라이언트에 제공해야 할 때 적용하는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;팩토리 패턴과 종종 함께 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA/Design Pattern</category>
      <category>facade pattern example</category>
      <category>facade pattern 예제</category>
      <category>JAVA design pattern</category>
      <category>java facade pattern</category>
      <category>자바 구조 패턴</category>
      <category>자바 디자인 패턴</category>
      <category>자바 퍼사드 패턴</category>
      <category>퍼사드 패턴</category>
      <category>퍼사드 패턴 예제</category>
      <category>퍼사드 패턴이란</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/193</guid>
      <comments>https://readystory.tistory.com/193#entry193comment</comments>
      <pubDate>Mon, 7 Sep 2020 21:19:51 +0900</pubDate>
    </item>
    <item>
      <title>Good Bye PowerMock, 더욱 강력해져서 돌아왔다. Mockito!</title>
      <link>https://readystory.tistory.com/190</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://github.com/mockito/mockito&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mockito&lt;/a&gt; 는 Java 에서 테스트 코드를 작성하는데 자주 사용되는 테스트 프레임워크입니다.&lt;/p&gt;
&lt;p&gt;하지만 기존에 Mockito 는 치명적인 단점이 있었는데요. 그건 바로 static 메소드나 필드에 대해 mocking 을 지원하지 않는다는 것이었습니다. 그래서 static 에 대해서는 테스트를 포기하거나, 대안으로 &lt;a href=&quot;https://github.com/powermock/powermock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PowerMock&lt;/a&gt; 를 사용해야 했었는데요.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Mockito 도 3.4.0 버전부터 드디어 static 을 지원하게 되면서, 이제 Mockito 만을 사용해서도 강력한 테스트가 가능하게 됐습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용 방법도 크게 어렵지 않은데요. 간단한 예제와 함께 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;간단하게 Calculator 클래스를 만들어서 static 메소드 하나와 인스턴스 메소드 하나를 선언해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1599058733739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Calculator {
    public static int add(int x, int y) {
        return x + y;
    }

    public int multiply(int x, int y) {
        return x * y;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 테스트 코드를 작성해볼텐데요. 기존의 Mockito 를 통해 아래와 같이 작성한다면 테스트가 실패하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1599058891416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.*;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;


public class CalculatorTest {
    @Test
    public void testStaticAdd() {
        // given
        when(Calculator.add(anyInt(), anyInt())).thenReturn(100);

        // when
        int sum = Calculator.add(10, 20);

        // then
        assertEquals(100, sum);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;원래라면 when().thenReturn() 을 통해서 특정 메소드에 대해 리턴 값을 지정할 수 있는데요. 위와 같이 static 메소드에 대해서는 해당 Mocking 이 불가능 했었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만, 이제 Mockito 를 사용해서도 static method 에 대해 Mocking 이 가능하게 됐습니다. 방법은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1599059147262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.*;
import org.mockito.MockedStatic;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;


public class CalculatorTest {
    private Calculator calculator;
    private static MockedStatic&amp;lt;Calculator&amp;gt; mockedCalculator;

    @BeforeClass
    public static void beforeClass() {
        mockedCalculator = mockStatic(Calculator.class);
    }

    @AfterClass
    public static void afterClass() {
        mockedCalculator.close();
    }

    @Before
    public void setUp() {
        calculator = new Calculator();
    }

    @Test
    public void testStaticAdd() {
        // given
        when(Calculator.add(anyInt(), anyInt())).thenReturn(100);

        // when
        int sum = Calculator.add(10, 20);

        // then
        assertEquals(100, sum);
    }

    @Test
    public void testMultiply() {
        // when
        int result = calculator.multiply(2, 5);

        // then
        assertEquals(10, result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 MockedStatic 객체를 선언합니다. 그리고 제네릭 타입으로 내가 Mocking 하고자 하는 클래스(위 예제에서는 Calculator)를 선언하고서, @BeforeClass 어노테이션을 붙인 메소드를 정의합니다. 이때, 주의할 점은 static 메소드로 선언해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;선언이 되었다면, @BeforeClass 가 붙은 메소드에서 MockedStatic 객체에 mockStatic() 메소드를 사용하여 mocking 을 해줍니다. 이 과정이 완료되었다면, 이후로는 위 예제의 testStaticAdd() 함수 내에서 사용한 것처럼 해당 객체의 static method에 대해서는 when().thenReturn() 을 통해 mocking 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;최근 버전에서는 static 이외에도 &lt;a href=&quot;https://javadoc.io/static/org.mockito/mockito-core/3.5.9/org/mockito/Mockito.html#mocked_construction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mockConstruction()&lt;/a&gt; 을 제공하고 있는데요. 생성자 내에서 특정 객체에 대해 생성해야 하는데 해당 객체에 대해서도 Mocking 해야하는 경우에 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기존에 Mockito 를 사용하고 계시던 분들도 최근에 배포된 버전을 참고하셔서 보다 강력하게 테스트 코드를 작성하시면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;위 예제의 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-JVM/tree/java/mockito&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; 에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>JAVA</category>
      <category>java static mock</category>
      <category>java static test</category>
      <category>mock static</category>
      <category>Mockito</category>
      <category>mockito static</category>
      <category>powermock</category>
      <category>static mock</category>
      <category>static test</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/190</guid>
      <comments>https://readystory.tistory.com/190#entry190comment</comments>
      <pubDate>Thu, 3 Sep 2020 00:20:09 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 권한(Permission) 개념 잡기</title>
      <link>https://readystory.tistory.com/188</link>
      <description>&lt;p&gt;IT 업계가 발전함에 따라 함께 주목 받고 민감하게 다루어지는 것이 개인 정보, 데이터 등인데요. 안드로이드 뿐만 아니라 iOS도 마찬가지로, 스마트폰 유저의 프라이버시를 보호하기 위해 권한에 대한 정책을 강화하고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드 앱은 유저의 민감한 정보에 접근하거나 카메라나 인터넷 등 시스템 기능을 다루기 위해서는 반드시 권한을 요청해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드에서는 보안을 위해 앱마다 권한을 별도로 관리하고 있기 때문에 어떤 한 유저가 A 라는 앱에 대해 카메라 사용 권한을 허용했더라도, B 앱에서 권한을 거부한다면 B 앱에서는 카메라 기능을 사용할 수 없게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 권한에 대해 개념만 잡고, 실질적인 권한 요청 코드(런타임 요청)에 대해서는 별도 포스팅으로 소개하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Manifest 에서 권한 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;권한 설정에 대하여 가장 기본적인 방법은 매니페스트 파일에 선언하는 것입니다. (&lt;a href=&quot;https://readystory.tistory.com/187&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글 - 매니페스트 개념 잡기&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;예를 들어, 앱에서 인터넷에 대해 사용하는 것을 허용하기 위해서는 매니페스트 파일 내에 &amp;lt;uses-permission&amp;gt; 을 통해 다음과 같이 선언하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1596884488347&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
          package=&quot;com.example.snazzyapp&quot;&amp;gt;

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

    &amp;lt;application ...&amp;gt;
        ...
    &amp;lt;/application&amp;gt;
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 유저의 프라이버시나 디바이스 기능 중 민감하지 않은 normal 권한이라면 매니페스트에 선언하는 것만으로도 시스템이 자동으로 권한을 승인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 유저의 민감 정보나 SMS 전송 등과 같이 민감한 디바이스 기능에 대한 권한에 대해서는 시스템이 자동으로 권한을 부여해주지 않고 명시적으로 유저의 승인을 받아야만 권한이 부여됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드는 권한을 이렇게 권한에 대해 normal permission 과 dangerous permission 으로 구분한다는 것을 기억하시면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;위험 권한(Dangerous Permission) 요청하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;앞서 위험 권한은 시스템에서 자동으로 승인해주지 않고, 유저의 승인이 있어야만 권한을 받을 수 있다고 말씀 드렸습니다. 여기서 주의할 부분이 있는데요. 안드로이드 6.0 (API 23 버전) 이전/이후로 그 방식이 나뉩니다. (안드로이드 파편화 문제가 어서 해결 되었으면 하는 간절함이...)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;앱 설치시에 권한 요청(Install-time request) - API 23 버전 미만&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;예전부터 안드로이드 폰을 사용해오신 유저분이라면 아마 기억하실 수도 있는데요. 안드로이드 롤리팝 버전까지만 해도 앱 실행 단계가 아닌 설치 단계에서 앱에서 사용되는 권한을 미리 고지 받아 승인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 방식은 각 권한에 대해 개별적으로 승인을 하는 것이 아니라 일괄적으로 요청되고 승인 되었기 때문에, 일부 권한은 승인하되 일부 권한에 대해 거절하고 싶더라도 그것이 불가능 했었습니다. 심지어 권한 승인을 거절할 경우 앱 설치조차 할 수 없었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;앱 실행 단계에서 권한 요청(Runtime request) - API 23 버전 이상&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;스마트폰 기기가 API Level 23 버전 이상이고, 앱의 targetSdkVersion 이 23 버전 이상일 경우 유저는 앱 설치 단계에서 권한에 대한 고지를 받지 않습니다. 앱을 설치한 후에서야 권한에 대해 승인 요청을 받게 되는데요. 이때 앞서 23 버전 미만과는 다르게, 권한에 대해 일괄 승인 받지 않고 개별 승인을 받으며 권한을 거부하더라도 해당 기능만 사용하지 않는 기능에 대해서는 앱을 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 개발자는 권한을 거절 당했더라도 앱이 크래쉬가 발생하지 않도록 신경 써서 앱을 개발해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;요청하는 권한이 앱에 필수적인 기능일 경우(예를 들어 카메라 필터 앱이 카메라 권한을 요청하는 경우)에는 해당 권한이 없으면 정상적인 기능 제공이 불가능하니, 해당 권한의 중요성에 대해 권한 요청 전에 별도의 화면이나 다이얼로그를 통해 유저에게 권한의 중요성과 해당 권한이 거절 당했을 경우에 앱을 정상적으로 사용할 수 없다는 것을 고지하는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;하드웨어 기능에 대한 권한&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;앞서 말씀드린 것과 같이 권한은 유저의 프라이버시 뿐만 아니라 하드웨어의 특정 기능을 사용하기 위해서도 승인 받을 필요가 있는데요. 세상에는 많은 안드로이드 기반의 기기가 존재하고, 그중에는 앱이 필요로 하는 하드웨어 기능이 없는 기기가 있을 수 있습니다. 예를 들어 블루투스 기반의 앱을 만들었는데, 블루투스를 지원하지 않는 스마트폰이 있을 수 있는거죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 경우에는 매니페스트 파일에서 &amp;lt;uses-feature&amp;gt; 속성을 통해 어느정도 해소할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1596888491710&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;uses-feature android:name=&quot;android.hardware.camera&quot; android:required=&quot;false&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;매니페스트 파일에서 위와 같이 선언할 수 있는데요. 만약 &lt;b&gt;android:required=&quot;true&quot;&lt;/b&gt; 로 설정할 경우 camera 가 없는 기기에서는 플레이 스토어를 통해 해당 앱을 설치할 수 없게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 예시와 같이 &lt;b&gt;android:required=&quot;false&quot;&lt;/b&gt; 로 설정할 경우 카메라가 없더라도 설치는 할 수 있는데요. 이 경우에는 권한 요청 전에 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;PackageManager.hasSystemFeature()&lt;/b&gt;&lt;/span&gt; 함수를 통해 시스템 기능을 지원하는지 확인 후에 권한을 요청할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;권한 자동 조정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;안드로이드는 1년에 최소 1개 이상의 버전을 출시하면서 많은 변화가 누적되어 왔는데요. 만약에 이전 버전에 없던 권한이 새로운 버전에서 추가된다면 어떻게 대응해야 할까요? 다행스럽게도 이 경우에는 시스템이 자동으로 권한을 조정해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어, 외부 저장소 접근에 대해서 안드로이드 API 18 버전까지는 별도로 권한이 필요 없다가 19버전부터는 READ_EXTERNAL_STORAGE 권한이 생겨 해당 권한이 있어야만 저장소에 대한 접근이 가능해졌습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Android 는 targetSdkVersion 에 따라 앱에 권한이 필요한지 확인하기 때문에, 앱의 targetSdkVersion 이 권한이 추가된 sdk version 보다 낮을 경우 시스템이 자동으로 앱에 권한을 추가해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android permission</category>
      <category>android permission request</category>
      <category>android runtime permission</category>
      <category>안드로이드 manifest permission</category>
      <category>안드로이드 권한</category>
      <category>안드로이드 권한 개념</category>
      <category>안드로이드 권한 정리</category>
      <category>안드로이드 런타임 퍼미션</category>
      <category>안드로이드 퍼미션</category>
      <category>안드로이드 퍼미션 개념</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/188</guid>
      <comments>https://readystory.tistory.com/188#entry188comment</comments>
      <pubDate>Sat, 8 Aug 2020 21:23:54 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Manifest 개념 잡기</title>
      <link>https://readystory.tistory.com/187</link>
      <description>&lt;p&gt;안드로이드 프로젝트에는 반드시 포함되어야 하는 파일이 있습니다. 그건 바로 AndroidManifest.xml 파일입니다. Manifest 파일은 프로젝트의 Source Set의 루트&lt;span style=&quot;color: #333333;&quot;&gt;(별도의 설정을 하지 않았다면 src/main)&lt;/span&gt;에 위치해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매니페스트 파일은 앱에 대한 필수적인 정보를 안드로이드 빌드 툴과 Android OS, 그리고 구글 플레이에 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매니페스트 파일에는 많은 정보를 담을 수 있지만 그중에서도 특히 선언되어야 하는 정보가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱의 패키지 이름&lt;/li&gt;
&lt;li&gt;앱에서 사용되는 컴포넌트(액티비티, 서비스, 브로드캐스트 리시버, 컨텐트 프로바이더)&lt;/li&gt;
&lt;li&gt;권한(Permission)&lt;/li&gt;
&lt;li&gt;앱에서 요구하는 하드웨어와 소프트웨어 특징&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;지금부터 위 4가지에 대해 하나하나 자세히 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Package name and Application ID&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;매니페스트 파일의 Root element(&amp;lt;manifest&amp;gt;&amp;lt;/manifest&amp;gt;) 에는 해당 앱의 패키지 네임이 반드시 기재되어야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 &quot;com.ready.example&quot; 이라는 패키지 네임의 manifest 파일을 생성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1596536995928&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.ready.example&quot;
    android:versionCode=&quot;1&quot;
    android:versionName=&quot;1.0&quot; &amp;gt;
    ...
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그렇다면 manifest 의 패키지 네임은 언제 어디서 사용될까요?&lt;/p&gt;
&lt;p&gt;이는 우리가 앱을 빌드하여 APK 를 추출하는 과정에서 Android Build Tool 에 의해 다음 2가지 목적으로 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App Resource 에 접근하는데 사용되는 R 클래스의 네임스페이스로 적용됩니다.&lt;br /&gt;위 예에서는 com.ready.example.R 클래스가 생성됩니다.&lt;/li&gt;
&lt;li&gt;매니페스트 파일 내에서 선언된 상대경로에 적용됩니다.&lt;br /&gt;예를 들어 &amp;lt;activity android:name=&quot;.MainActivity&amp;gt; 라고 선언했다면 이는 &quot;com.ready.example.MainActivity&quot; 를 가리키게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;매니페스트의 package 속성으로 선언된 패키지 경로는 프로젝트의 base package name 과 매칭 되어야 합니다. 만약 프로젝트 내 sub-package 를 갖고 있더라도 R 클래스는 sub-package 의 경로가 아닌 매니페스트 내 package 속성에 따라 생성됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 package 속성이 불변인 것은 아닙니다. 안드로이드 빌드 툴은 APK 를 추출할 때 위와 같은 작업을 처리한 후에 package 속성의 값을 build.gradle 에 선언된 applicationId 로 교체해 버립니다. 이는 구글 플레이 스토어에 등록된 각 앱들을 구분하기 위해 package 값을 유니크하게 해야할 필요가 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 경우에 Manifest 파일에 선언된 package 이름과 build.gradle 에 선언된 applicationId 값이 같다면 문제가 없지만, 다를 경우에는 상황에 따라 충돌이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;App Components&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;아시겠지만 안드로이드에는 4대 컴포넌트(&lt;span style=&quot;color: #333333;&quot;&gt;Activity, Service, Broadcast Receiver, Content Provider&lt;/span&gt;)라 불리는 녀석들이 있습니다.&lt;/p&gt;
&lt;p&gt;이 컴포넌트들을 앱에서 사용한다면 매니페스트 파일에 등록해줘야 하는데요. Manifest 파일 내에서 각각 다음 태그를 통해 선언할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;activity&amp;gt; : Activity&lt;/li&gt;
&lt;li&gt;&amp;lt;service&amp;gt; : Service&lt;/li&gt;
&lt;li&gt;&amp;lt;receiver&amp;gt; : Broadcast Receiver&lt;/li&gt;
&lt;li&gt;&amp;lt;provider&amp;gt; : Content Provider&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;만약 컴포넌트의 서브 클래스를 구현하여 사용하면서 매니페스트 파일에 선언해주지 않는다면, 시스템은 해당 컴포넌트에 대해 실행할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4대 컴포넌트들은 각각 인텐트에 의해 활성화됩니다. 여기서 인텐트란 메세지 객체로, 어떤 행동을 수행할지에 대한 명령이나 작업에 필요한 데이터를 포함합니다. 안드로이드 개발하면서 인텐트는 굉장히 중요한 역할을 하는 녀석이기 때문에 추후에 따로 다루는 포스트를 작성하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아무튼, 앱이 인텐트를 시스템에 발행하면 시스템은 각 앱의 매니페스트에 선언된 intent-filter 에 기초하여 처리할 수 있는 인텐트를 컴포넌트를 찾게 됩니다. 만약 여러 개의 앱이 인텐트를 다룰 수 있다면, 사용자가 해당 인텐트를 어떤 앱에게 넘길지 선택할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 매니페스트에 선언된 컴포넌트들과 &amp;lt;application&amp;gt; 은 유저에게 보여줄 수 있는 icon 과 label 속성을 갖고 있습니다. 그리고 xml 로 구성하다보면 트리 구조기 때문에 부모-자식 관계가 생기는데요. 이때 자식 element에 icon 과 label 이 설정되어있지 않다면 부모에 설정된 값이 기본 값으로 설정되게 됩니다. 그렇기 때문에 각 컴포넌트마다 설정하지 않고 앱 전체에 기본 값을 설정하려면 &amp;lt;application&amp;gt; 에 설정하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Permissions&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;매니페스트 하면 빼놓을 수 없는 역할이 권한 설정인데요. 이 권한이라는 것은 안드로이드 진영에서 민감하게 다루고 있는 관심사 중 하나입니다. 안드로이드 앱은 민감한 유저 정보나 카메라나 인터넷 등 특정 시스템 기능을 사용할 때 반드시 권한을 요청해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 SMS 전송에 대한 권한을 요청하기 위해서는 Manifest 파일에 다음과 같이 설정하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596556990175&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;manifest ... &amp;gt;
    &amp;lt;uses-permission android:name=&quot;android.permission.SEND_SMS&quot;/&amp;gt;
    ...
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 Android 6.0 버전(API Level 23) 이상에서는 몇몇 권한에 대해서 사용자에게 동적으로 요청하여 승인이나 거절을 받아야 하는데요. 이것과는 별개로 앱에서 필요로 하는 모든 권한에는 Manifest 파일에 &amp;lt;uses-permission&amp;gt; 요소를 설정해줘야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;권한 관련해서도 다룰 내용이 매우 많지만 오늘의 주제는 매니페스트 파일이기 때문에 권한과 관련된 내용은 별도 포스팅에서 자세히 다루도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Device Compatibility&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Manifest 파일에는 앱이 필요로 하는 하드웨어나 소프트웨어 특징을 명시할 수 있습니다. 예를 들어, 카메라 앱을 만들 경우에 카메라가 필수로 있어야 하니 카메라가 있는 기기에서만 Play Store 에서 해당 앱이 다운로드 될 수 있도록 명시하는 것입니다. 아마 태블릿을 이용하시는 분이라면 플레이 스토어에서 특정 앱을 설치하려 할 때 &quot;다운로드 받을 수 없는 기기입니다.&quot; 라고 문구 뜬 걸 볼 수 있었을 겁니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매니페스트 파일에 &amp;lt;uses-feature&amp;gt; 태그를 사용하면 명시할 수 있습니다. 예를 들어 compass sensor 가 있는 기기에서만 다운로드 가능하게 하기 위해서는 아래와 같이 매니페스트 파일을 작성하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596558856366&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;manifest ... &amp;gt;
    &amp;lt;uses-feature android:name=&quot;android.hardware.sensor.compass&quot;
                  android:required=&quot;true&quot; /&amp;gt;
    ...
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또 매니페스트 파일에는 &amp;lt;uses-sdk&amp;gt; 태그를 통해 요구되는 sdk 버전을 명시할 수도 있는데요. 하지만, 안드로이드 스튜디오로 프로젝트를 구성한 경우에는 build.gradle 에 선언된 minSdkVersion 이 manifest 의 &amp;lt;uses-sdk&amp;gt; 속성을 재정의하기 때문에 min sdk 설정에 대해서는 매니페스트 파일이 아닌 build.gradle 에 선언하는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>Android Manifest</category>
      <category>Manifest</category>
      <category>Manifest file</category>
      <category>manifest 개념</category>
      <category>manifest 특징</category>
      <category>매니페스트</category>
      <category>매니페스트 개념</category>
      <category>매니페스트란</category>
      <category>안드로이드</category>
      <category>안드로이드 매니페스트</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/187</guid>
      <comments>https://readystory.tistory.com/187#entry187comment</comments>
      <pubDate>Wed, 5 Aug 2020 01:37:35 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Fragment에서 Back Press 처리하기(with. OnBackPressedDispatcher)</title>
      <link>https://readystory.tistory.com/186</link>
      <description>&lt;p&gt;Android App을 개발하다 보면 화면을 구성할 때 하나의 액티비티에 다수의 프래그먼트를 사용해서 구성하는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나 안드로이드의 Fragment에는 Activity의 onBackPressed()와 같은 콜백 메소드가 없기 때문에 별도의 리스너를 만들어 액티비티에서 back press 이벤트가 발생했을 시 해당 프래그먼트에서 구현한 콜백 함수를 호출하는 형식으로 구현해주는 방법 등 야매스러운(?) 방법들을 사용했어야만 했습니다. 하지만 이 방식은 프래그먼트의 생명주기를 잘 관리하면서 사용해야 한다는 단점이 있었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 정녕 방법이 없는 걸까요? 아닙니다. 다행스럽게도 Androidx 패키지에서 이에 대한 대책이 나왔습니다. 바로 &lt;b&gt;OnBackPressedDispatcher()&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;OnBackPressedDispatcher()&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Fragment 에서는 OnBackPressedCallback 객체를 생성하여 액티비티에 addCallback() 해주면 되는데요.&lt;/p&gt;
&lt;p&gt;예제를 통해 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;fragment_sample.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1589294865332&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;FrameLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.SampleFragment&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/sample_text&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:gravity=&quot;center&quot;
        android:text=&quot;back press event not yet.&quot; /&amp;gt;

&amp;lt;/FrameLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;SampleFragment.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1589294669650&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SampleFragment : Fragment() {
    private lateinit var callback: OnBackPressedCallback
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_sample, container, false)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                sample_text.text = &quot;occur back pressed event!!&quot;
            }
        }
        requireActivity().onBackPressedDispatcher.addCallback(this, callback)
    }

    override fun onDetach() {
        super.onDetach()
        callback.remove()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;간단합니다! OnBackPressedCallback 객체를 선언하여 onAttach() 에서 BackPressedDsipatcher에 등록해주고, onDetach()에서 제거해주었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 callBack이 잘 동작하는지 확인하기 위해 간단하게 텍스트뷰 하나 두고서 Back Press 이벤트 발생했을 시에 텍스트뷰의 텍스트를 &quot;occur back pressed event!!&quot;로 바꿔보도록 코드를 작성해봤습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Fragment에서 Back Press를 처리하기 위해서는 &lt;b&gt;2가지&lt;/b&gt;만 신경 써주시면 되는데요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하나는 &lt;b&gt;OnBackPressedCallback&lt;/b&gt; 추상 클래스의&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt; handleOnBackPressed()&lt;/b&gt; &lt;/span&gt;를 재정의하여 해당 메소드 내에 내가 처리하고자 하는 로직을 넣어주는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 이 콜백 객체를 이제 액티비티의 BackPressedDispatcher에 등록해줘야 하는데요. 그 부분이 바로 가장 아래에 있는&lt;b&gt; requireActivity().onBackPressedDispatcher.addCallback()&lt;/b&gt; 입니다. 여기서 천 번째 파라미터는 LifecycleOwner 타입을 필요로 하기 때문에 일반적으로 프래그먼트 객체를 넘겨주면 되고, 두 번째 파라미터에 앞서 만든 콜백 객체를 넘겨주면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러면 이제 액티비티에서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;OnBackPress() 이벤트 발생 시 BackPressedDispatcher에 등록된 리스너들 중 생명주기의 상태가 Alive 상태의 콜백 리스너들만 실행&lt;/b&gt;&lt;/span&gt;하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이때 주의할 점은, 만약 액티비티의 onBackPress() 함수를 Override 하여 super.onBackPressed()를 호출하지 않도록 한다면 BackPressedDispatcher 또한 동작하지 않기 때문에 유의해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;device-2020-05-13-000402.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QxOPF/btqD7Xqxwy1/QVaZ6aaxA0bJaYB1KaC4bK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QxOPF/btqD7Xqxwy1/QVaZ6aaxA0bJaYB1KaC4bK/img.gif&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QxOPF/btqD7Xqxwy1/QVaZ6aaxA0bJaYB1KaC4bK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/QxOPF/btqD7Xqxwy1/QVaZ6aaxA0bJaYB1KaC4bK/img.gif&quot; data-filename=&quot;device-2020-05-13-000402.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제에 대한 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/fragment-backpress&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android fragment back</category>
      <category>android fragment back event</category>
      <category>android fragment back press</category>
      <category>android fragment backpress callback</category>
      <category>android fragment backpress event</category>
      <category>android fragment backpress listener</category>
      <category>android fragment OnBackPressDispatcher</category>
      <category>android fragment onbackpressed</category>
      <category>android fragment 뒤로가기</category>
      <category>안드로이드 프래그먼트 뒤로가기</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/186</guid>
      <comments>https://readystory.tistory.com/186#entry186comment</comments>
      <pubDate>Wed, 13 May 2020 00:10:11 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 안드로이드의 Touch Event 는 어떻게 전달 될까? (with. Touch Intercept)</title>
      <link>https://readystory.tistory.com/185</link>
      <description>&lt;p&gt;사용자와의 상호작용(Interaction)을 처리하는 것은 모바일 프로그래밍에서 굉장히 중요합니다.&lt;/p&gt;
&lt;p&gt;안드로이드 애플리케이션은 기본적으로 Activity를 통해 화면을 구성하며, 사용자는 화면을 터치함으로써 애플리케이션에 다양한 이벤트를 전달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;많은 분들이 안드로이드 앱 개발을 하면서 onTouchEvent()나 OnClickListener#onClick() 등을 사용하며 터치와 클릭에 대한 처리를 하셨을 텐데요. 하지만 안드로이드 플랫폼에서 터치 이벤트를 내부적으로 어떻게 처리하는지에 대해서는 모르는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇기 때문에 종종 복잡한 레이아웃 구성이나, 터치 이벤트를 가로채야 하는 등 일반적인 상황에서 약간만 벗어나는 구현을 해야 할 때 헤매기 십상인데요. 이번 포스팅에서는 안드로이드가 어떤 단계를 거쳐서 터치 이벤트를 전달하고 처리하는지에 대해 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Touch Event 전달 순서&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;안드로이드에서 View에 대한 터치 이벤트가 발생했을 때 이에 대하여 어디서부터 어디로, 어떤 과정을 통해 이벤트가 전달이 될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 예제를 통해 쉽게 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;356&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnlUmW/btqDZNJ28hY/ik0uKkZI7dSdouFU2r5XOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnlUmW/btqDZNJ28hY/ik0uKkZI7dSdouFU2r5XOk/img.png&quot; data-alt=&quot;그림 1. Touch Event 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnlUmW/btqDZNJ28hY/ik0uKkZI7dSdouFU2r5XOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnlUmW%2FbtqDZNJ28hY%2Fik0uKkZI7dSdouFU2r5XOk%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;356&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. Touch Event 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림과 같이 레이아웃이 구성되어있다고 가정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Touch Event가 발생하면, 모든 이벤트 알림의 시작점은 액티비티로부터 시작됩니다.&lt;/p&gt;
&lt;p&gt;Activity에서 시작된 이벤트 알림이 &lt;b&gt;Activity -&amp;gt; ViewGroup A -&amp;gt; ViewGroup B -&amp;gt; View&lt;/b&gt; 순으로 전달하게 됩니다.&lt;/p&gt;
&lt;p&gt;즉, Top down 방식인 거죠.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 이벤트를 받은 View에서 Touch Event에 대한 처리를 수행하겠다고 하면, 이번에는 반대로 &lt;b&gt;View -&amp;gt; ViewGroup B -&amp;gt; ViewGroup A -&amp;gt; Activity&lt;/b&gt; 순으로 이벤트 처리를 전달하게 됩니다.&lt;b&gt; 한 마디로 액티비티에서 시작해 액티비티로 끝나게 되는 것이죠.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;701&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rl2IO/btqDZMYG58q/kVUdKKKIupSM91RZ8krsRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rl2IO/btqDZMYG58q/kVUdKKKIupSM91RZ8krsRK/img.png&quot; data-alt=&quot;그림 2. Touch Event 전달 경로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rl2IO/btqDZMYG58q/kVUdKKKIupSM91RZ8krsRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRl2IO%2FbtqDZMYG58q%2FkVUdKKKIupSM91RZ8krsRK%2Fimg.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;701&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. Touch Event 전달 경로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;[그림 2]를 살펴보시면 이해가 더 명확하게 될 텐데요.&lt;/p&gt;
&lt;p&gt;Activity의 dispatchTouchEvent() 함수를 통해 자식 뷰인 ViewGroup A에게 이벤트 알림이 가고, ViewGroup A에서 onInterceptTouchEvent()를 먼저 실행하여 intercept 여부를 확인한 다음에 false일 경우에만 다시 자식 뷰인 ViewGroup B에게 알림을 전달하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;만약 onInterceptTouchEvent() 함수를 Override 하여 true를 리턴하게 구현한다면, 해당 뷰그룹에서 더 이상 자식 뷰에게 터치 이벤트를 전달하지 않게 됩니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 하나 주의할 점은 &lt;b&gt;onInterceptTouchEvent() 함수는 ViewGroup에 정의된 함수&lt;/b&gt;이기 때문에, 액티비티와 View에는 정의되어 있지 않습니다. 그렇다고 해서 인터셉트가 아주 불가능한 것은 아닌데요. 액티비티에서는 dispatchTouchEvent()를 Override 하여 인터셉트에 대한 처리를 구현할 수 있습니다.&lt;span style=&quot;color: #ee2323;&quot;&gt; (View는 불가능)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 [그림 2]의 맨 밑에 View를 보면 &lt;b&gt;OnTouchListener가 있을 경우와 없을 경우&lt;/b&gt;가 있는데요. 만약 View에 TouchListener를 등록해 놓았다면 리스너의 onTouch() 함수가 처리될 것이고, TouchListener가 별도로 등록되어있지 않다면 View 클래스에 기본적으로 구현되어있는 onTouchEvent()가 호출됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;터치 이벤트가 내려올 때 onInterceptTouchEvent()의 결과에 따라 더 이상 아래로 전달되지 않았던 것처럼, 반대로 올라갈때는 onTouchEvent()의 결과가 true면 더이상 전달하지 않게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;백문이 불여일코! 예제를 통해 위에서 설명한 과정이 제대로 동작하는지 테스트해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 위 그림에서 ViewGroup 의 역할을 할 CustomLayout과, View 의 역할을 할 CustomView을 작성해보겠습니다. CustomView는 TextView를 상속하게 구현했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 터치 이벤트 전달과 관련된 함수들의 순서를 파악하기 위해 각각의 함수들을 재정의하여 로그를 찍어보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;CustomLayout.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588953661698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomLayout(context: Context) : FrameLayout(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called dispatchTouchEvent() in CustomLayout&quot;)
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called onInterceptTouchEvent() in CustomLayout&quot;)
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called onTouchEvent() in CustomLayout&quot;)
        return super.onTouchEvent(event)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;CustomView.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588953686515&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomView(context: Context) : AppCompatTextView(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called dispatchTouchEvent() in CustomView&quot;)
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called onTouchEvent() in CustomView&quot;)
        return super.onTouchEvent(event)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 액티비티에 위에서 만든 것들을 추가해주겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MainActivity.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588953817529&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val customLayout = CustomLayout(this)
        addContentView(customLayout, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))

        val customView = CustomView(this)
        customView.text = &quot;Hello, Ready Story!&quot;
        customLayout.addView(customView)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자, 이제 실행해서 로그가 어떻게 찍히는지 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxbD7n/btqD0jhhM9r/vYS1Bd5G5qk4AvO8uIIzI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxbD7n/btqD0jhhM9r/vYS1Bd5G5qk4AvO8uIIzI1/img.png&quot; data-alt=&quot;그림 3. 예제 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxbD7n/btqD0jhhM9r/vYS1Bd5G5qk4AvO8uIIzI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxbD7n%2FbtqD0jhhM9r%2FvYS1Bd5G5qk4AvO8uIIzI1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. 예제 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;앞서 설명한 것과 같이 부모 뷰인 CustomLayout의 dispatchTouchEvent() -&amp;gt; onInterceptTouchEvent() 순으로 호출되고 이어서 자식 뷰인 CustomView의 dispatchTouchEvent()가 호출됩니다. 그리고 별도의 TouchListener를 등록하지 않았기 때문에 CustomView의 onTouchEvent()가 호출되고 이어서 CustomLayout의 onTouchEvent()가 호출됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 CustomLayout의 onInterceptTouchEvent()의 결과를 true로 하여 CustomView로 터치 이벤트가 전달되지 않도록 해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;CustomLayout.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588954196523&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomLayout(context: Context) : FrameLayout(context) {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called dispatchTouchEvent() in CustomLayout&quot;)
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called onInterceptTouchEvent() in CustomLayout&quot;)
        return true // true 값 반환시 자식에게 터치 이벤트 전달 X
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d(&quot;TouchEventTest&quot;, &quot;called onTouchEvent() in CustomLayout&quot;)
        return super.onTouchEvent(event)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2KCWG/btqD1pnR2DY/SEZYRnd1pK52QMOoS3gLMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2KCWG/btqD1pnR2DY/SEZYRnd1pK52QMOoS3gLMk/img.png&quot; data-alt=&quot;그림 4. 터치 인터셉트 결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2KCWG/btqD1pnR2DY/SEZYRnd1pK52QMOoS3gLMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2KCWG%2FbtqD1pnR2DY%2FSEZYRnd1pK52QMOoS3gLMk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 4. 터치 인터셉트 결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[그림 4]의 결과와 같이 CustomLayout에서 CustomView로 터치 이벤트를 전달하지 않고, CustomLayout의 onTouchEvent()가 곧바로 호출되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제에 대한 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/touch-event-delivery&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>Android Custom Touch Event</category>
      <category>Android dispatchTouchEvent</category>
      <category>Android onInterceptTouchEvent</category>
      <category>Android onTouchEvent</category>
      <category>Android Touch Event Delivery</category>
      <category>Android Touch Listener</category>
      <category>Android TouchEvent</category>
      <category>Android TouchEvent Intercept</category>
      <category>Android TouchEvent 과정</category>
      <category>안드로이드 터치 이벤트</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/185</guid>
      <comments>https://readystory.tistory.com/185#entry185comment</comments>
      <pubDate>Sat, 9 May 2020 01:15:08 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 코틀린의 스마트한 타입 캐스팅(with. is)</title>
      <link>https://readystory.tistory.com/184</link>
      <description>&lt;p&gt;아시다시피 자바와 코틀린은 객체지향 언어(Object Oriented Programming Language)입니다.&lt;/p&gt;
&lt;p&gt;그리고 객체 지향 프로그래밍 언어의 특징 중 하나인 다형성으로 인해 부모 객체에 자식 클래스의 인스턴스를 주입하는 등 &lt;span style=&quot;color: #333333;&quot;&gt;조건에만 맞는다면&amp;nbsp;&lt;/span&gt;꼭 인스턴스와 같은 객체 타입이 아니더라도 대입할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러다 보면 필요에 따라 Type Casting(형 변환)을 해야 하는 경우가 있는데요. 기존에 자바에서는 &lt;b&gt;instanceof&lt;/b&gt; 키워드를 통해 안전하게 형 변환 가능한지 검사한 후에 변환하고자 하는 대상 앞에 () 괄호를 명시해주어 형 변환을 할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;그렇다면 코틀린에서는 어떻게 형 변환을 할 수 있을까요?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;대표적으로 &lt;span style=&quot;color: #006dd7;&quot;&gt;is&lt;/span&gt; 와&lt;span style=&quot;color: #f89009;&quot;&gt; as&lt;/span&gt; 가 있습니다만, 이번 포스팅에서는 스마트 캐스팅 기능을 지원하는 &lt;span style=&quot;color: #006dd7;&quot;&gt;is&lt;/span&gt; 에 대해 알아보도록 하겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;스마트 캐스팅 -&lt;span style=&quot;color: #006dd7;&quot;&gt; is&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;우선 &lt;span style=&quot;color: #ee2323;&quot;&gt;코틀린에는 자바의 instanceof 가 없습니다.&lt;/span&gt; 대신 &lt;b&gt;is&lt;/b&gt; 라는 녀석이 있는데요. 독특하게도 이 녀석은 instanceof 이상의 역할을 해줍니다. 사용 방식은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;b&gt;[타입 체크할 변수] &lt;span style=&quot;color: #006dd7;&quot;&gt;is&lt;/span&gt; [확인하고자 하는 타입]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코틀린의 is는 단순히 타입을 체크하는 역할을 넘어 결과로 true를 리턴할 경우 해당 타입으로 자동 캐스팅을 해줍니다. 즉, 캐스팅을 위한 변수를 하나 더 만들지 않아도 되는데요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예제를 통해 자바의 instanceof 를 사용했을 때와 비교해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;java code&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588667119634&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class JavaA {
    int a = 5;
}

class JavaB extends JavaA {
    int b = 10;
}

public class Main {
    public static void main(String[] args) {
        JavaA obj = new JavaB();
        if (obj instanceof JavaB) {
            JavaB obj2 = (JavaB) obj;  // 타입 캐스팅을 위한 별도의 변수 생성
            System.out.println(obj2.b);  // 10
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 자바 코드입니다. JavaA 라는 클래스와 그를 상속하는 JavaB 클래스를 정의하여 JavaA 타입의 객체에 들어있는 JavaB 인스턴스를 타입 체크 후에 자식 클래스인 JavaB 타입으로 형 변환하는 예제입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;obj 변수는 JavaB 생성자를 통해 생성한 인스턴스이기 때문에 &quot;obj instanceof JavaB&quot; 의 결과로 true를 리턴하게 되고, 따라서 obj2 라는 JavaB 타입의 객체에 대입할 수 있게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 같은 예제를 코틀린의 is 를 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;kotlin code&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588667318795&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;open class KotlinA {
    val a = 5
}

class KotlinB : KotlinA() {
    val b = 10
}

fun main() {
    val obj: KotlinA = KotlinB()
    if (obj is KotlinB) {  // smart casting
        println(obj.b)  // 10
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코틀린 코드입니다. 전반적으로 자바 코드와 유사해 보이는 듯 하지만 한 가지 특이한 부분이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;바로 KotlinB 타입으로 타입 캐스팅을 해주는 코드도 없을뿐더러 그를 저장하기 위한 변수도 따로 만들지 않았음에도 KotlinA 타입으로 선언한 obj 변수에서 곧바로 KotlinB 클래스에 정의된 b 변수를 호출하는 부분입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 캐스팅을 스마트 캐스팅이라고 하는데요. 비록 obj 가 처음에 선언할 때는 KotlinA 타입으로 선언되었지만, is 키워드를 통해서 KotlinB 타입이라는 것이 확인된 시점부터는 코틀린이 자동으로 검사 대상이었던 obj 변수의 타입을 KotlinB 타입으로 캐스팅하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보통 어떤 변수의 타입을 체크하는 목적에는 해당 타입으로의 캐스팅을 하기 위해서인 경우가 많기 때문에 이 스마트 캐스팅은 개발자가 좀 더 간결한 코드 작성을 할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 하나 응용을 해보자면 타입이 아닌지 체크하는 문법은 is 앞에 느낌표를 붙여주면 됩니다. -&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; !is&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;그리고 재밌는 기능은&lt;b&gt; if 문 안에서 !is 를 사용하게 되면 else 문 안에서 스마트 캐스팅이 적용되게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1588685038495&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (obj !is String) {  // same as !(obj is String)
    print(&quot;Not a String&quot;)
} else {
    print(obj.length)  // obj is String
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서처럼 if 문에서 obj 의 타입이 String 이 아니라는 조건에서 false가 리턴된다면 String Type 이라는 결론이 도출되어 obj의 타입이 String으로 스마트 캐스팅 되게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한 가지 더 특징이 있는데요. &amp;amp;&amp;amp; 와 || 등 논리 연산자를 통해 여러 조건이 함께 쓰일 경우에 왼쪽에서 오른쪽 순으로 검사가 일어나기 때문에 만약 is 나 !is 를 이용하여 왼쪽에 조건식을 걸어둘 경우 이어서 검사 되는 오른쪽 조건식에서 스마트 캐스팅이 적용된 채로 검사하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1588685509665&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// x is automatically cast to string on the right-hand side of `||`
if (x !is String || x.length == 0) return

// x is automatically cast to string on the right-hand side of `&amp;amp;&amp;amp;`
if (x is String &amp;amp;&amp;amp; x.length &amp;gt; 0) {
    print(x.length) // x is automatically cast to String
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나 스마트 캐스팅은 항상 적용되는 것이 아닙니다. 컴파일러가 타입 체크와 사용에 대해 보장할 수 없는 상황에서는 적용되지 않는데요. 관련해서 몇 가지 규칙과 주의사항이 있습니다. val 과 var 에 따라 조금씩 다른데요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;val &lt;span style=&quot;color: #000000;&quot;&gt;local variable(지역 변수)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt; -&amp;nbsp; delegate properties(ex - by lazy 등) 의 경우를 제외하고 항상 적용됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; local variable&lt;/span&gt;&lt;/b&gt; - 체크와 사용 사이에 수정되지 않고, 람다에서 변수가 수정되지 않으며, delegate properties가 아닐 경우에만 적용됨&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;val&lt;/span&gt; properties(멤버 변수)&lt;/b&gt;&lt;/span&gt; - 같은 모듈 내에서 선언됐을 경우에는 적용되지만, open properties나 custom getter의 경우에는 적용되지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;var&lt;/span&gt; properties&lt;/b&gt; - 어떠한 경우에도 스마트 캐스팅이 적용되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;제네릭(Generic)과 is&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;코틀린에서 제네릭을 사용하게 되면 컴파일 타임에서 각 연산에 대해 Type Safety 를 보장하게 됩니다만, 런타임에서는 제네릭 타입에 대한 정보를 갖고 있지 않습니다. 예를 들어 위에서 선언했던 KotlinA 타입을 갖는 List&amp;lt;KotlinA&amp;gt; 라는 컬렉션을 선언했을 때 이는 컴파일 단계에서는 리스트에 들어가고 나오는 타입에 대해 KotlinA 타입이 맞는지 검사해주지만, 런타임 단계에서는 KotlinA 타입에 대한 정보는 지우고 그저 List&amp;lt;*&amp;gt; 로 취급할 뿐입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 &lt;span style=&quot;color: #ee2323;&quot;&gt;런타임 단계에서 obj is List&amp;lt;KotlinA&amp;gt;와 같이 특정 타입 파라미터에 대한 타입 검사하는 것이 불가능합니다&lt;/span&gt;만, obj is List&amp;lt;*&amp;gt; 와 같이 별표를 사용하거나 ArrayList나 LinkedList 등 구체적인 구현 객체에 대한 타입 검사는 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1588687946738&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (something is List&amp;lt;*&amp;gt;) {
    something.forEach { println(it) } // The items are typed as `Any?`
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1588687940443&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun handleStrings(list: List&amp;lt;String&amp;gt;) {
    if (list is ArrayList) {
        // `list` is smart-cast to `ArrayList&amp;lt;String&amp;gt;`
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제에서 작성된 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-JVM/tree/kotlin/type-cast&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>Kotlin</category>
      <category>kotlin</category>
      <category>kotlin casting</category>
      <category>kotlin generic is</category>
      <category>kotlin is</category>
      <category>kotlin smart cast</category>
      <category>kotlin type casting</category>
      <category>kotlin 스마트 캐스팅</category>
      <category>kotlin 형 변환</category>
      <category>코틀린 타입 캐스팅</category>
      <category>코틀린 형 변환</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/184</guid>
      <comments>https://readystory.tistory.com/184#entry184comment</comments>
      <pubDate>Tue, 5 May 2020 23:24:37 +0900</pubDate>
    </item>
    <item>
      <title>[Android] parent View 범위 밖에서 child View 그리는 방법 - clipChildren</title>
      <link>https://readystory.tistory.com/183</link>
      <description>&lt;p&gt;Android에서 View는 Tree 구조로 이루어져 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;layout 을 구성할 때 xml 형태로 ViewGroup과 View를 적절히 배치하여 구성하게 되는데, 이렇게 정의된 xml 파일을 Activity의 setContentView나 LayoutInflater를 통하여 렌더링 하게 됩니다. 그리고 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;안드로이드는 렌더링 할 때 Tree의 각 노드들에 대해 Pre-Order 방식으로 방문합니다.&lt;/b&gt;&lt;/span&gt; 즉, Parent View부터 Child View 순으로 방문하게 되는 것이지요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇기 때문에 Android 에서 layout 을 그려낼 때에는 Parent View가 Child View보다 먼저 그려지게 됩니다.&lt;/p&gt;
&lt;p&gt;Android에서 View를 Draw 하는 과정에 대한 자세한 설명은 &lt;a href=&quot;https://developer.android.com/guide/topics/ui/how-android-draws&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;developer 사이트&lt;/a&gt;를 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 한 가지 알아야 할 특징은, 안드로이드 프레임워크는 기본적으로 Parent View의 범위를 벗어나는 영역에서의 Child View에 대해서 invalid(유효하지 않은)로 판단하여 해당 부분을 그려내지 않는다는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이해를 돕기 위해 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Parent View 하나와 Child View를 각각 하나씩 만들어 Child View의 사이즈를 Parent View의 사이즈보다 크게 설정하여 어떻게 렌더링 되는지 확인해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588599336789&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;FrameLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;100dp&quot;
        android:background=&quot;@color/colorPrimary&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;&amp;gt;

        &amp;lt;TextView
            android:layout_width=&quot;150dp&quot;
            android:layout_height=&quot;150dp&quot;
            android:layout_gravity=&quot;center&quot;
            android:background=&quot;@color/colorAccent&quot;
            android:text=&quot;child&quot;
            android:textColor=&quot;@android:color/white&quot;
            android:gravity=&quot;center&quot;/&amp;gt;
    &amp;lt;/FrameLayout&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보시다시피 부모 뷰인 FrameLayout의 높이는 100dp로 설정하고, 자식 뷰의 높이를 150dp로 설정하여 부모의 높이보다 자식의 뷰가 더 크게 그려지는지 확인해보도록 하겠습니다. 위에서 설명한 대로라면 자식 뷰는 부모 뷰의 영역 밖에서는 그려질 수 없기 때문에 부모 뷰보다 더 크게 그려지면 안 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xt5yk/btqDRMqOYKe/MShZhxLuY4m6zyDB12OYo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xt5yk/btqDRMqOYKe/MShZhxLuY4m6zyDB12OYo1/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xt5yk/btqDRMqOYKe/MShZhxLuY4m6zyDB12OYo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxt5yk%2FbtqDRMqOYKe%2FMShZhxLuY4m6zyDB12OYo1%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;보시다시피 child의 높이가 parent 보다 더 크게 설정됐음에도 불구하고 parent 영역 바깥에서 더 그려지지가 않습니다. 비단 이 특징은 사이즈뿐만 아니라 margin으로 인해 영역을 벗어나도 마찬가지로 적용되는데요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588599973304&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;FrameLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;100dp&quot;
        android:background=&quot;@color/colorPrimary&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;&amp;gt;

        &amp;lt;TextView
            android:layout_width=&quot;100dp&quot;
            android:layout_height=&quot;100dp&quot;
            android:layout_gravity=&quot;center&quot;
            android:background=&quot;@color/colorAccent&quot;
            android:layout_marginTop=&quot;20dp&quot;
            android:text=&quot;child&quot;
            android:textColor=&quot;@android:color/white&quot;
            android:gravity=&quot;center&quot;/&amp;gt;
    &amp;lt;/FrameLayout&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번에는 Child View의 사이즈는 Parent View와 똑같이 100dp로 설정하고서 Child View에 margin Top을 20dp 만큼 주어서 자식 뷰가 부모 뷰의 영역 밑으로 범위를 벗어나도록 정의한 후 결과를 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A22Op/btqDRNi0DlD/XRT5uo7kkYuOMYTSNR7Yr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A22Op/btqDRNi0DlD/XRT5uo7kkYuOMYTSNR7Yr0/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A22Op/btqDRNi0DlD/XRT5uo7kkYuOMYTSNR7Yr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA22Op%2FbtqDRNi0DlD%2FXRT5uo7kkYuOMYTSNR7Yr0%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;뿐만 아니라 View의 등장 및 퇴장 시 애니메이션을 사용하는 경우가 많은데 이럴 때에도 적용되는 특징이기 때문에 잘 알아둘 필요가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 이 특징을 적용하고 싶지 않을 때는 어떻게 하면 될까요?&lt;/p&gt;
&lt;p&gt;바로 &lt;b&gt;clipChildren이라는&lt;/b&gt; 옵션을 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;clipChildren 옵션은 ViewGroup에 있는 속성(attributes)으로, 자식 뷰가 그릴 수 있는 범위를 해당 ViewGroup 영역으로 설정해주는 것입니다. 기본 값은 true이며, Child View의 드로잉 범위 제한을 해제하기 위해서는 clipChildren 속성 값을 &lt;span style=&quot;color: #006dd7;&quot;&gt;false&lt;/span&gt;로 설정해줘야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 위 예에서 한 번 적용해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두 번째 예제에서 Child View가 제대로 그려지려면 어디에 clipChildren=false을 적용해야 할까요? 아마 많은 분들이 바로 직속 상위 뷰인 FrameLayout에 설정해야 할 것으로 예상하실 텐데요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사실 원하는 결과를 얻기 위해서는 그보다 상위 뷰인 ConstraintLayout 에다가 적용해야 합니다. 왜냐하면 FrameLayout의 영역을 확장해주는 것이 곧 TextView의 제한 범위를 확장하는 결과를 불러오기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588601327824&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:clipChildren=&quot;false&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;FrameLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;100dp&quot;
        android:background=&quot;@color/colorPrimary&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;&amp;gt;

        &amp;lt;TextView
            android:layout_width=&quot;100dp&quot;
            android:layout_height=&quot;100dp&quot;
            android:layout_gravity=&quot;center&quot;
            android:background=&quot;@color/colorAccent&quot;
            android:layout_marginTop=&quot;20dp&quot;
            android:text=&quot;child&quot;
            android:textColor=&quot;@android:color/white&quot;
            android:gravity=&quot;center&quot;/&amp;gt;
    &amp;lt;/FrameLayout&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과를 바로 확인해보겠습니다!&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNSiIz/btqDURj2w4P/kIXfB2A5Jz3qHGHZAwkkW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNSiIz/btqDURj2w4P/kIXfB2A5Jz3qHGHZAwkkW0/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNSiIz/btqDURj2w4P/kIXfB2A5Jz3qHGHZAwkkW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNSiIz%2FbtqDURj2w4P%2FkIXfB2A5Jz3qHGHZAwkkW0%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;제대로 적용된 결과를 확인했습니다!&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 이 속성을 xml 수준이 아닌 코드 수준에서 적용하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p&gt;ViewGroup#setClipChildren(boolean enabled) 메소드를 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 특정 뷰의 모든 조상 뷰를 방문하면서 setClipChildren(false)를 적용하는 재귀 함수를 소개해드리고, 포스팅 마무리하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588601759057&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void setAllParentsClip(View v) {
    while (v.getParent() != null &amp;amp;&amp;amp; v.getParent() instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) v.getParent();
        viewGroup.setClipChildren(false);
        viewGroup.setClipToPadding(false);
        v = viewGroup;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;kotlin&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588601722092&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun View.setAllParentsClip() {
    var parent = parent
    while (parent is ViewGroup) {
        parent.clipChildren = false
        parent.clipToPadding = false
        parent = parent.parent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 살펴본 예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/android/clipChildren&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android animation 잘림</category>
      <category>android clipchildren</category>
      <category>android minus margin</category>
      <category>android out of parent view</category>
      <category>android view rendering</category>
      <category>android view 잘림</category>
      <category>Android ViewGroup</category>
      <category>viewgroup clipchildren</category>
      <category>안드로이드 뷰 잘림</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/183</guid>
      <comments>https://readystory.tistory.com/183#entry183comment</comments>
      <pubDate>Mon, 4 May 2020 23:22:17 +0900</pubDate>
    </item>
    <item>
      <title>[Android] WebView에서 local file 접근하기(feat. Android Assets)</title>
      <link>https://readystory.tistory.com/182</link>
      <description>&lt;p&gt;안드로이드에서 WebView를 사용하면 별도의 서버에서 호스팅 되고 있는 웹 컨텐츠를 보여줄 수도 있고, 별도의 웹 서버를 두지 않고 local static file(html, css, js)만을 사용하여 웹 페이지를 제공할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 WebView에서 local file에 대해서는 어떻게 접근할 수 있을까요? 한 가지 방법만 있는 것은 아닙니다만 오늘 소개드릴 방법은 assets 폴더에 넣고 접근하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Assets&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Assets에 대해 간단하게 설명을 드리자면, 단어 뜻 그대로&lt;b&gt; &quot;자산&quot;&lt;/b&gt;입니다. 프로젝트가 패키징 할 때 함께 assets 디렉토리에 위치한 파일들을 포함하여 런타임에서 접근할 수 있게 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Assets은 res 디렉토리가 아닌 assets/ 이라는 디렉토리에서 관리되는데, &lt;span style=&quot;color: #333333;&quot;&gt;프로젝트 생성시 기본적으로 생성되는 디렉토리가 아니기 때문에 따로&lt;/span&gt; 생성해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Assets은 종종 res/raw 디렉토리와 비교되곤 합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;res/raw 와의 결정적인 차이는 res/raw 아래 존재하는 파일들은 R 클래스에 의해 id 값이 관리되기 때문에 컴파일 타임에서 파일 존재에 대한 검사가 발생하게 되고, 디렉토리를 다양하게 구성하여 관리할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;반면에 Assets 방식은 R 클래스를 통해 리소스가 관리되지 않기 때문에 컴파일이 아닌 런타임에서 검사가 발생하기 때문에 좀 더 유연하게 파일에 대한 접근을 할 수 있습니다. 그리고 file path에 의한 접근이 가능하기 때문에 assets/ 디렉토리 하에 다양한 디렉토리를 두어 리소스 관리의 편의성을 높일 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;WebView with Assets&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;본론으로 돌아와서 웹뷰에서 local file에 접근하는 수단으로 Assets을 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 html과 js 파일을 준비해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;sample.html&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588166397378&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Ready Story&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;h1 id=&quot;sample-title&quot;&amp;gt;Sample for the injected local js file.&amp;lt;/h1&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;sample.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;sample.js&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588166414659&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(function() {
    document.getElementById(&quot;sample-title&quot;).style.fontStyle = &quot;italic&quot;;
}());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정말 간단한 파일입니다. 여기서 눈여겨 볼 것은 sample.html 내에서 sample.js를 호출할 때 상대 경로로 호출한다는 부분입니다. 그렇기 때문에 sample.html과 sample.js를 같은 경로에 둬야 합니다.&lt;/p&gt;
&lt;p&gt;(꼭 그래야만 하는 것은 아닙니다. WebViewClient의 shouldInterceptRequest()을 이용하여 호출을 가로챈 다음 적절한 데이터를 주입해주는 방식도 있습니다만, 관련해서는 다른 포스팅에서 다루도록 하겠습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 assets 폴더를 만들어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;주의해야 하는 것은 res/ 폴더 아래 만드는 것이 아니라 동등한 위치에 만들어줘야 한다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqjMSB/btqDOHoTKAG/ifAk8YWoTqajKd97e2Vf80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqjMSB/btqDOHoTKAG/ifAk8YWoTqajKd97e2Vf80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqjMSB/btqDOHoTKAG/ifAk8YWoTqajKd97e2Vf80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqjMSB%2FbtqDOHoTKAG%2FifAk8YWoTqajKd97e2Vf80%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 assets/ 아래위에서 만든 파일들을 넣어줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 Activity 안에 WebView가 가득 차도록 layout을 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588166960561&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;WebView
        android:id=&quot;@+id/web_view&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 Activity에서 WebView에 local file에 대한 url을 연결해줍니다.&lt;/p&gt;
&lt;p&gt;WebView 내에서 JavaScript 파일을 사용하기 때문에 &lt;i&gt;&lt;b&gt;WebView#getSettings()#setJavaScriptEnabled()&lt;/b&gt;&lt;/i&gt; 를 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MainActivity.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1588167097521&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        web_view.settings.javaScriptEnabled = true
        web_view.loadUrl(&quot;file:///android_asset/sample.html&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 보시다시피 &lt;b&gt;assets/&lt;/b&gt; 디렉토리에 대한 url은 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&quot;file:///android_asset/&quot;&lt;/span&gt;&lt;/b&gt; 을 baseUrl로 두고서 접근하시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VY8VR/btqDQcIbqJF/ImWkH7hXgNQv9l1cModyX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VY8VR/btqDQcIbqJF/ImWkH7hXgNQv9l1cModyX1/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VY8VR/btqDQcIbqJF/ImWkH7hXgNQv9l1cModyX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVY8VR%2FbtqDQcIbqJF%2FImWkH7hXgNQv9l1cModyX1%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;결과 화면처럼 텍스트에 기울임꼴(Italic)이 잘 적용되었다면 정상적으로 적용된 것이구요. 만약에 기울임꼴이 적용되지 않았다면 js 파일이 제대로 로드되지 않은 것이니 파일 경로나 오타는 없는지 확인하시기 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;전체 예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/webview-static-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android assets raw</category>
      <category>android assets res</category>
      <category>Android WebView</category>
      <category>android webview assets</category>
      <category>android webview javascript</category>
      <category>Android WebView local file</category>
      <category>android webview local html</category>
      <category>android webview local js</category>
      <category>android webview local js file</category>
      <category>android webview local static file</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/182</guid>
      <comments>https://readystory.tistory.com/182#entry182comment</comments>
      <pubDate>Wed, 29 Apr 2020 22:37:53 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 웹뷰를 풍부하게 활용할 수 있도록 해주는 WebViewClient</title>
      <link>https://readystory.tistory.com/181</link>
      <description>&lt;p&gt;안드로이드 웹뷰에는 WebViewClient와 WebChromeClient가 있습니다. 처음에는 이 두 클라이언트 클래스가 헷갈리는데요. 하나하나 차근차근 정리하면서 각각의 역할을 구분한다면 그냥 웹뷰만 사용했을 때보다 훨씬 풍부한 활용이 가능하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 먼저 WebViewClient 에 대해 간략하게 소개해드리도록 하고, 추후 활용하는 부분에 대해서도 계속해서 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Methods&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;WebViewClient의 역할은 간단하게 말해서 notification 입니다. 우리는 WebViewClient를 통해 웹뷰에서 일어나는 요청, 상태, 에러 등 다양한 상황에서의 콜백을 조작할 수 있습니다. 다양한 메소드를 제공하고 있습니다만 대표적으로 사용되는 몇 가지 메소드만 살펴보도록 하겠습니다. 전체 메소드에 대해서는 &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;developer 사이트&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;onPageStarted(view: WebView?, url: String?, favicon: Bitmap?)&lt;/b&gt;&lt;br /&gt;- page loading을 시작했을 때 호출되는 콜백 메소드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onPageFinished(view: WebView?, url: String?)&lt;/b&gt;&lt;br /&gt;- page loading을 끝냈을 때 호출되는 콜백 메소드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onLoadResource(view: WebView?, url: String?)&lt;/b&gt;&lt;br /&gt;- 파라미터로 넘어온 url 에 의해 특정 리소스를 load 할 때 호출되는 콜백 메소드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?)&lt;/b&gt;&lt;br /&gt;- request 에 대해 에러가 발생했을 때 호출되는 콜백 메소드. error 변수에 에러에 대한 정보가 담겨져있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;shouldInterceptRequest(view: WebView?, request: WebResourceRequest?)&lt;/b&gt;&lt;br /&gt;- resource request를 가로채서 응답을 내리기 전에 호출되는 메소드&lt;br /&gt;- 이 메소드를 활용하여 특정 요청에 대한 필터링 및 응답 값 커스텀 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?)&lt;/b&gt;&lt;br /&gt;- 현재 웹뷰에 로드될 URL에 대한 컨트롤을 할 수 있는 메소드&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇듯 WebViewClient를 사용하면 다양한 콜백 메소드를 활용할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;WebViewClient는 WebView의 setWebViewClient 메소드를 통해 등록해주면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;위에서 알아본 메소드들이 잘 호출되는지, 언제 호출되는지 알아보기 위해서 간단한 예제를 작성해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;WebViewClient를 상속하여 각 메소드들을 재정의하는 클래스를 하나 만들고, 이를 웹뷰에 등록한 다음 제 블로그 사이트를 로드할 때 각 메소드 호출부에서 기록되는 값들을 텍스트뷰에 기록해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 WebViewClient를 상속하는 클래스를 작성하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587909467306&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomWebViewClient(val writeLog: (String) -&amp;gt; Unit) : WebViewClient() {
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        writeLog(&quot;onPageStarted : ${url}\n&quot;)
        super.onPageStarted(view, url, favicon)
    }

    override fun onPageFinished(view: WebView?, url: String?) {
        writeLog(&quot;onPageFinished : ${url}\n&quot;)
        super.onPageFinished(view, url)
    }

    override fun onLoadResource(view: WebView?, url: String?) {
        writeLog(&quot;onLoadResource : ${url}\n&quot;)
        super.onLoadResource(view, url)
    }

    @TargetApi(Build.VERSION_CODES.M)
    override fun onReceivedError(
        view: WebView?,
        request: WebResourceRequest?,
        error: WebResourceError?
    ) {
        writeLog(&quot;onReceivedError : ${error?.description.toString()}\n&quot;)
        super.onReceivedError(view, request, error)
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        writeLog(&quot;shouldInterceptRequest : ${request?.url.toString()}\n&quot;)
        return super.shouldInterceptRequest(view, request)
    }

    @TargetApi(Build.VERSION_CODES.N)
    override fun shouldOverrideUrlLoading(
        view: WebView?,
        request: WebResourceRequest?
    ): Boolean {
        writeLog(&quot;shouldOverrideUrlLoading : ${request?.url.toString()}\n&quot;)
        return super.shouldOverrideUrlLoading(view, request)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 짚고 넘어갈 부분은 먼저 CustomWebViewClient 의 생성자에 정의된 writeLog 프로퍼티 입니다. 이는 코틀린의 특징 중 하나인 &quot;kotlin functions are first-class citizen.&quot; 이라는 것을 이용한 것으로, 잠시 후 액티비티 코드에서 작성될 텍스트뷰에 로깅하는 함수를 받아오는데 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 아래 3개의 함수에서는 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;@RequiresApi&lt;/span&gt;&lt;/b&gt; 어노테이션을 볼 수 있는데요. 이는 어노테이션이 붙은 각 함수들이 일정 버전 이상에서 지원하는 버전이기 때문에 요구되는 API 버전을 명시해준 것입니다. 안드로이드는 파편화 현상이 심해서 사용하는 클래스나 메소드가 내 프로젝트의 버전에 적용 가능한지 항상 잘 확인해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 레이아웃을 작성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587909799212&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;WebView
        android:id=&quot;@+id/web_view&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/log_text&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/log_text&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;100dp&quot;
        android:background=&quot;#ccc&quot;
        android:scrollbars=&quot;vertical&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;


&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;많은 로그가 기록될 것이기 때문에 TextView에 스크롤바를 설정해주었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 액티비티 클래스를 작성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587909847937&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.os.Bundle
import android.os.Handler
import android.text.method.ScrollingMovementMethod
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    private val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        web_view.run {
            webViewClient = CustomWebViewClient(::writeLog)
            loadUrl(&quot;https://readykim.com/&quot;)
        }
        log_text.movementMethod = ScrollingMovementMethod()
    }

    private fun writeLog(log: String) {
        handler.post {
            log_text.append(log)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 살펴볼 건 CustomWebViewClient의 생성자에 앞서 말했던 일급 객체로써 writeLog 라는 함수를 넣어줬다는 것과 writeLog 함수 내에 Handler를 두어 UI Thread에서 로깅할 수 있도록 처리한 부분입니다. 그리고 TextView에 스크롤을 하기 위해 movementMethod를 설정해주었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결과 화면은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d48wIA/btqDGhkr4xZ/xaPy2jmF8e4k0BGX2xzSqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d48wIA/btqDGhkr4xZ/xaPy2jmF8e4k0BGX2xzSqk/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d48wIA/btqDGhkr4xZ/xaPy2jmF8e4k0BGX2xzSqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd48wIA%2FbtqDGhkr4xZ%2FxaPy2jmF8e4k0BGX2xzSqk%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아래 로그가 찍힌 것을 보시면 액티비티에서 호출해준 loadUrl() 메소드에서 설정한 제 블로그 URL 이 나와있고, 이어서 제 블로그 HTML 코드에서 호출하는 각종 css 파일과 js 파일들에 대한 Request들도 잘 찍히고 있습니다. 테스트 해보실 때 존재하지 않는 URL을 호출해보시면 onReceivedError() 메소드도 잘 호출되는 것을 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제에 작성된 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/webview-client&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android webview callback</category>
      <category>android webview customwebviewclient</category>
      <category>android webview error listener</category>
      <category>Android WebView listener</category>
      <category>android webview pageFinished</category>
      <category>android webview request intercept</category>
      <category>android webview should IntercepteRequest</category>
      <category>android webview shouldoverridingurl</category>
      <category>Android WebViewClient</category>
      <category>WebView Listener</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/181</guid>
      <comments>https://readystory.tistory.com/181#entry181comment</comments>
      <pubDate>Sun, 26 Apr 2020 23:23:50 +0900</pubDate>
    </item>
    <item>
      <title>[Android] WebView Scroll 다루기 - page Up/Down, OverScrollMode</title>
      <link>https://readystory.tistory.com/180</link>
      <description>&lt;p&gt;안드로이드에서 WebView를 사용하다보면 스크롤에 대해 다룰 일이 많습니다. 이는 대다수의 웹 페이지가 스크롤을 필요로 하고 사용하고 있기 때문인데요. 스크롤은 단순한 기능을 하는 녀석이기 때문에 간단한 방법으로 조작할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하나하나 예제와 함께 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;WebView Scroll 위 아래로 움직이기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;웹뷰에 대한 스크롤을 코드상에서 컨트롤할 수 있습니다.&lt;/p&gt;
&lt;p&gt;WebView에서 제공하는&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; pageUp(boolean top) / pageDown(boolean bottom)&lt;/b&gt;&lt;/span&gt; 메소드를 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;pageUp() 을 예로 설명하자면, 파라미터에 false 값을 넣으면 페이지 단위로 스크롤하게 되고 true 값을 넣게되면 가장 상단으로 스크롤하게 됩니다. pageDown() 의 경우 반대로 동작한다고 보면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;WebView OverScroll Mode&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Android WebView에는 OverScroll이란 게 있습니다. 이는 웹뷰를 가장 상단이나 가장 하단으로 쭉 스크롤하게 되면 끝 부분에 부딪히면서 더이상 스크롤하지 못한다는 것을 알려주는 파랑색 표시를 뜻합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이에 대해서는 WebView의 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;setOverScrollMode(int mode)&lt;/b&gt;&lt;/span&gt; 메소드를 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;모드는 다음 3가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OVER_SCROLL_ALWAYS&lt;/b&gt; : 항상 표시(웹뷰에 스크롤이 없어도 표시)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OVER_SCROLL_IF_CONTENT_SCROLLS&lt;/b&gt; : 웹뷰(웹뷰에 스크롤 있을 때만 표시)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OVER_SCROLL_NEVER&lt;/b&gt; : 항상 표시하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;백문이 불여일코! 직접 예제를 작성해보면서 이해를 돕도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 예제는 제 블로그를 호출하는 웹뷰를 띄워놓고 위에서 살펴본 메소드들에 대한 버튼을 두어 테스트해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;예제 앱의 화면은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RsIIs/btqDGIBI2tk/0SRmYsL4VrPLX9k9PQFbMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RsIIs/btqDGIBI2tk/0SRmYsL4VrPLX9k9PQFbMK/img.png&quot; data-alt=&quot;예제 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RsIIs/btqDGIBI2tk/0SRmYsL4VrPLX9k9PQFbMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRsIIs%2FbtqDGIBI2tk%2F0SRmYsL4VrPLX9k9PQFbMK%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;예제 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 레이아웃을 설정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1587828543171&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;WebView
        android:id=&quot;@+id/web_view&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/page_up&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/move_top&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;move to Top&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/page_down&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/move_bottom&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/page_up&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;Page Up&quot;
        app:layout_constraintStart_toEndOf=&quot;@id/move_top&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/page_down&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/move_bottom&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;Move to Bottom&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/page_down&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/always_mode&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/page_down&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;Page Down&quot;
        app:layout_constraintStart_toEndOf=&quot;@id/move_bottom&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/never_mode&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/always_mode&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;Always&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/if_contents_mode&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/if_contents_mode&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;If Contents&quot;
        app:layout_constraintStart_toEndOf=&quot;@id/always_mode&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/never_mode&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/never_mode&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;Never&quot;
        app:layout_constraintStart_toEndOf=&quot;@id/if_contents_mode&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로는 액티비티에서 웹뷰에 블로그를 로드하고, 각 버튼에 동작을 지정해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;MainActivity.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1587828647738&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        web_view.loadUrl(&quot;https://readystory.tistory.com&quot;)

        move_top.setOnClickListener {
            web_view.pageUp(true)
        }

        move_bottom.setOnClickListener {
            web_view.pageDown(true)
        }

        page_up.setOnClickListener {
            web_view.pageUp(false)
        }

        page_down.setOnClickListener {
            web_view.pageDown(false)
        }

        always_mode.setOnClickListener {
            web_view.overScrollMode = WebView.OVER_SCROLL_ALWAYS
        }

        if_contents_mode.setOnClickListener {
            web_view.overScrollMode = WebView.OVER_SCROLL_IF_CONTENT_SCROLLS
        }

        never_mode.setOnClickListener {
            web_view.overScrollMode = WebView.OVER_SCROLL_NEVER
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제의 전체 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/webview-pageupdown-and-overscroll&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Basic</category>
      <category>android webview move to bottom</category>
      <category>android webview move to top</category>
      <category>Android WebView overscrollmode</category>
      <category>Android WebView page down</category>
      <category>Android WebView page up</category>
      <category>Android WebView Scroll bottom</category>
      <category>Android WebView Scroll Down</category>
      <category>Android WebView Scroll Top</category>
      <category>Android WebView Scroll Up</category>
      <category>안드로이드 웹뷰</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/180</guid>
      <comments>https://readystory.tistory.com/180#entry180comment</comments>
      <pubDate>Sun, 26 Apr 2020 00:36:05 +0900</pubDate>
    </item>
    <item>
      <title>[Android] LiveData 유연하게 사용하기 - Transformations.map, switchMap</title>
      <link>https://readystory.tistory.com/179</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LiveData 는 ViewModel 클래스와 DataBinding 등과 자주 쓰이는 클래스입니다. 처음 접하시는 분들은 대게 LiveData 와 MutableLiveData 만 사용하실텐데요.&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;개발을 하다보면 하나의 데이터가 바뀔 때마다 다른 여러 데이터들도 함께 바껴야하는 상황을 많이 만나게 됩니다. 또 Room이나 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;이럴 때 사용하는 것이 바로 &lt;b&gt;Transformations 의 map 과 switchMap&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;&lt;span style=&quot;color: #333333;&quot;&gt;Transformations 메소드를 사용하면 LiveData와 마찬가지로 Observer의 lifecycle에 안전하게 데이터를 전달할 수 있습니다. 그리고 지연 평가(Lazy Estimation)로 동작하기 때문에 원천이 되는 객체의 변화가 일어나지 않는다면 동작하지 않습니다.&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;b&gt;Transformations.map&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformations.map 메소드는 다은과 같이 구현되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587476659402&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @MainThread
    @NonNull
    public static &amp;lt;X, Y&amp;gt; LiveData&amp;lt;Y&amp;gt; map(
            @NonNull LiveData&amp;lt;X&amp;gt; source,
            @NonNull final Function&amp;lt;X, Y&amp;gt; mapFunction) {
        final MediatorLiveData&amp;lt;Y&amp;gt; result = new MediatorLiveData&amp;lt;&amp;gt;();
        result.addSource(source, new Observer&amp;lt;X&amp;gt;() {
            @Override
            public void onChanged(@Nullable X x) {
                result.setValue(mapFunction.apply(x));
            }
        });
        return result;
    }&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;위 코드는 java 코드입니다. 보시면 static 메소드이기 때문에 Transformations 인스턴스 없이 클래스 참조로 바로 사용가능합니다. Main Thread 에서 실행되며, 파라미터에 null 값을 넣어서는 안됩니다.&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;파라미터에 대해서도 가볍게 설명하자면 첫 번째 파라미터인 source는 결과물로 만들어진 LiveData의 원천 역할을 합니다. 그리고 두 번째 파라미터인 func 함수의 결과로 인해 어떤 LiveData 가 리턴될지 결정됩니다.&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;내부적으로 MediatorLiveData가 사용되고 있습니다만, MediatorLiveData에 대해서는 별도로 다루도록 하겠습니다.&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;Trnasformations.map 의 핵심은 값의 변형입니다. Kotlin Collections의 map 함수나 RxJava의 map 함수를 아시는 분은 보다 쉽게 이해할 수 있을텐데요. source 객체의 값을 Observing 하면서 그 값이 바뀔때마다 새로 만들어진 LiveData의 값도 연쇄적으로 바뀌게 됩니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Transformations.switchMap&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformations.switchMap 은 map 메소드와 비슷합니다만 차이점이 있다면 두 번째 파라미터로 들어오는 함수형 인터페이스 내 메소드의 리턴 타입이 값이 아닌 LiveData 타입이라는 것입니다. 이는 Room 등 LiveData를 지원하는 다른 라이브러리들과 함께 사용하기에 좋습니다.&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;pre id=&quot;code_1587477100728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @MainThread
    @NonNull
    public static &amp;lt;X, Y&amp;gt; LiveData&amp;lt;Y&amp;gt; switchMap(
            @NonNull LiveData&amp;lt;X&amp;gt; source,
            @NonNull final Function&amp;lt;X, LiveData&amp;lt;Y&amp;gt;&amp;gt; switchMapFunction) {
        final MediatorLiveData&amp;lt;Y&amp;gt; result = new MediatorLiveData&amp;lt;&amp;gt;();
        result.addSource(source, new Observer&amp;lt;X&amp;gt;() {
            LiveData&amp;lt;Y&amp;gt; mSource;

            @Override
            public void onChanged(@Nullable X x) {
                LiveData&amp;lt;Y&amp;gt; newLiveData = switchMapFunction.apply(x);
                if (mSource == newLiveData) {
                    return;
                }
                if (mSource != null) {
                    result.removeSource(mSource);
                }
                mSource = newLiveData;
                if (mSource != null) {
                    result.addSource(mSource, new Observer&amp;lt;Y&amp;gt;() {
                        @Override
                        public void onChanged(@Nullable Y y) {
                            result.setValue(y);
                        }
                    });
                }
            }
        });
        return result;
    }&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;map 메소드보다는 다소 코드가 길지만 null 체크가 늘었고, 앞서 말씀드린 것처럼 결과물로 값이 아닌 LiveData를 만들어 리턴하기 때문에 그에대한 코드가 조금 더 늘어났을 뿐 사실상 map 메소드와 로직은 유사합니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예제 코드 - 원/정사각형 넓이 구하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제는 반지름(또는 한 변의 길이)를 입력하면 그에따른 원 넓이와 정사각형의 넓이를 화면 중앙에 출력하는 코드를 작성해보겠습니다. 아직 데이터바인딩에 대한 포스트를 작성하진 않았기 때문에 이번 예제 코드에서 데이터바인딩을 사용하진 않았습니다만, ViewModel-LiveData-DataBinding 은 함께 사용했을 때 굉장히 좋은 시너지를 발휘합니다.&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;먼저 레이아웃 코드를 작성합니다. (activity_main.xml)&lt;/p&gt;
&lt;pre id=&quot;code_1587478366276&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;EditText
        android:id=&quot;@+id/edit_text&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;40dp&quot;
        android:layout_margin=&quot;10dp&quot;
        android:hint=&quot;반지름(한 변의 길이)를 입력하세요.&quot;
        android:inputType=&quot;numberDecimal&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/ok_button&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/ok_button&quot;
        android:layout_width=&quot;40dp&quot;
        android:layout_height=&quot;40dp&quot;
        android:layout_margin=&quot;10dp&quot;
        android:background=&quot;@drawable/button_selector&quot;
        android:gravity=&quot;center&quot;
        android:text=&quot;OK&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/circle_area_text&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;원 넓이 : &quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/square_area_text&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;정사각형 넓이 : &quot;
        android:layout_marginTop=&quot;20dp&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/circle_area_text&quot; /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EditText에 길이를 입력하면 그에 따른 원 넓이와 정사각형 넓이를 화면 중앙에 출력할 수 있도록 레이아웃을 구성합니다.&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;다음으로는 ViewModel 클래스를 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587478477428&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainViewModel : ViewModel() {
    private val widthText: MutableLiveData&amp;lt;Int&amp;gt; = MutableLiveData()
    val areaOfSquare: LiveData&amp;lt;Int&amp;gt; = Transformations.map(widthText) { it * it }
    val areaOfCircle: LiveData&amp;lt;Double&amp;gt; = Transformations.switchMap(widthText) { width -&amp;gt;
        getAreaOfCircle(width)
    }

    fun updateText(newWidth: Int) {
        widthText.value = newWidth
    }

    private fun getAreaOfCircle(width: Int): LiveData&amp;lt;Double&amp;gt; {
        val liveData: MutableLiveData&amp;lt;Double&amp;gt; = MutableLiveData().apply {
            value = width * width * PI
        }
        return liveData
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의깊게 보실 부분은 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;areaOfSquare&lt;/b&gt; &lt;/span&gt;와 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;areaOfCircle&lt;/b&gt; &lt;/span&gt;입니다. 보시면 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;areaOfSquare&lt;/b&gt;&lt;/span&gt;는 Transformations.map 메소드를 사용하기 때문에 LiveData 객체가 아닌 값을 리턴하는 람다식을 두 번째 파라미터로 넘겨줍니다. 그리고 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;areaOfCircle&lt;/span&gt; &lt;/b&gt;는 LiveData 객체를 리턴하는 함수가 두 번째 파라미터에 와야하기 때문에 getAreaOfCircle() 이라는 함수를 만들어 활용했습니다.&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;마지막으로 MainActivity 클래스를 작성하면 끝!&lt;/p&gt;
&lt;pre id=&quot;code_1587478510219&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                            .get(MainViewModel::class.java)

        ok_button.setOnClickListener {
            viewModel.updateText(edit_text.text.toString().toInt())
        }

        viewModel.areaOfCircle.observe(this, Observer&amp;lt;Double&amp;gt; { area -&amp;gt;
            circle_area_text.text = &quot;원 넓이 : $area&quot;
        })

        viewModel.areaOfSquare.observe(this, Observer&amp;lt;Int&amp;gt; { area -&amp;gt;
            square_area_text.text = &quot;정사각형 넓이 : $area&quot;
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성을 완료했다면 실행해봅시다. 잘 동작하는 것을 확인할 수 있습니다.&lt;/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;device-2020-04-21-232939.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eAlCzs/btqDzDAyASd/2ftzO8mAZP7hEEMDd01mHK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eAlCzs/btqDzDAyASd/2ftzO8mAZP7hEEMDd01mHK/img.gif&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eAlCzs/btqDzDAyASd/2ftzO8mAZP7hEEMDd01mHK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/eAlCzs/btqDzDAyASd/2ftzO8mAZP7hEEMDd01mHK/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;320&quot; height=&quot;640&quot; data-filename=&quot;device-2020-04-21-232939.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformations를 사용하면 그냥 LiveData 만 사용했을 때보다 유연하게 개발할 수 있습니다. 다음 포스팅에서는 앞서 내부 구현코드에서 등장했던 MediatorLiveData를 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>Android LiveData</category>
      <category>android livedata map</category>
      <category>android livedata switchmap</category>
      <category>android mediatorLiveData</category>
      <category>android room livedata</category>
      <category>android Transformations</category>
      <category>android Transformations.map</category>
      <category>android Transformations.switchMap</category>
      <category>android viewmodel livedata</category>
      <category>LiveData</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/179</guid>
      <comments>https://readystory.tistory.com/179#entry179comment</comments>
      <pubDate>Tue, 21 Apr 2020 23:37:02 +0900</pubDate>
    </item>
    <item>
      <title>Kotlinx.Serialization + Retrofit 사용하기</title>
      <link>https://readystory.tistory.com/178</link>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 글에서 Kotlinx.Serialization을 알아봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://readystory.tistory.com/177&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Kotlin/Library] - Kotlinx Serialization - 코틀린에 가장 적합한 Json Converter&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1587138290665&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Kotlinx Serialization - 코틀린에 가장 적합한 Json Converter&quot; data-og-description=&quot;아마 REST API를 구현해보신 분이라면 JSON 데이터를 다뤄보신 경험이 있으실 겁니다. 개발을 하다보면 JSON 이나 XML 등을 다뤄야하는 경우가 자주 발생하는데요. 기본적으로 JSON 이나 XML 은 JVM 언어에 정의되..&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/177&quot; data-og-url=&quot;https://readystory.tistory.com/177&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/omsvD/hyFHEbOqbf/frn86Do1LBdsgKrPSyc9Lk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bSDHTA/hyFHPdjNRj/vyMWqFRr3fUV6lOkyil6s0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bEFqgY/hyFIXANyoy/qrtxGnkHYo6IJL697iHx1k/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=378_177_623_444&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/177&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/177&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/omsvD/hyFHEbOqbf/frn86Do1LBdsgKrPSyc9Lk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bSDHTA/hyFHPdjNRj/vyMWqFRr3fUV6lOkyil6s0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bEFqgY/hyFIXANyoy/qrtxGnkHYo6IJL697iHx1k/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=378_177_623_444');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Kotlinx Serialization - 코틀린에 가장 적합한 Json Converter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;아마 REST API를 구현해보신 분이라면 JSON 데이터를 다뤄보신 경험이 있으실 겁니다. 개발을 하다보면 JSON 이나 XML 등을 다뤄야하는 경우가 자주 발생하는데요. 기본적으로 JSON 이나 XML 은 JVM 언어에 정의되..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;아마 코틀린 언어를 가장 많이 사용하는 분야는 안드로이드일 것이라 생각하는데요.&lt;/p&gt;
&lt;p&gt;직렬화 라이브러리는 단독으로 사용되기 보다는 Retrofit 등의 라이브러리와 함께 사용하여 JSON 데이터를 주고 받는 형태에 쓰이는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 kotlin serialization 과 Retrofit 을 함께 사용하는 방법에 대해 실습해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;참고로 레트로핏 사용법에 대해서는 기본적으로 안다는 가정하에, 자세하게 설명하지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;사전 작업&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;build.gradle 에 Serialization, Retrofit 에 대한 디펜던시를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587139161657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    // kotlin serialization
    implementation &quot;org.jetbrains.kotlin:kotlin-stdlib-jdk8&quot;
    implementation &quot;org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0&quot; // JVM dependency

    // retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
    implementation(&quot;com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.5.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마지막 줄에 있는 converter가 바로 레트로핏과 kotlin serialization 을 연동시켜 주는 모듈입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;실습에 앞서 JSON 데이터를 요청하면 응답해주는 서버가 필요하겠죠?&lt;/p&gt;
&lt;p&gt;이번 실습에서는 제가 자주 애용하는 fake server인 &lt;a href=&quot;https://jsonplaceholder.typicode.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JSONPlaceholder&lt;/a&gt; 를 통해 실습해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;https://jsonplaceholder.typicode.com/albums/{id}로 요청하게 되면 다음과 같은 형태의 JSON 데이터를 응답해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587138767356&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// https://jsonplaceholder.typicode.com/albums/3

{
  &quot;userId&quot;: 1,
  &quot;id&quot;: 3,
  &quot;title&quot;: &quot;omnis laborum odio&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 우리는 이에 맞게 코틀린 data class를 작성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587138816853&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Serializable
data class Album (
    val userId: Int,
    val id: Int,
    val title: String,
    val comment: String = &quot;default comment&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;앞서 보여드렸던 Json 데이터와는 달리, 코틀린에서 정의한 Album 클래스에는 commnet 라는 값이 있는 것도 보이실겁니다. 이는 Kotlin Serialization의 장점인 default value 도 정상적으로 사용 가능한가 테스트하기 위해서 추가해주었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 레트로핏에 사용될 인터페이스를 정의하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587139033057&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ReadyService {

    @GET(&quot;albums/{id}&quot;)
    fun getAlbum(@Path(&quot;id&quot;) id: Int): Call&amp;lt;Album&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 레트로핏 객체를 선언해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587139220931&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(&quot;https://jsonplaceholder.typicode.com/&quot;)
        .addConverterFactory(Json.asConverterFactory(MediaType.parse(&quot;application/json&quot;)!!))
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Json.asConverterFactory() 함수는 Json 클래스에 정의된 함수가 아니라, 코틀린 익스텐션 함수를 통해 구현된 함수입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자, 이제 모든 준비가 되었으니 통신을 해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587139339984&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(&quot;https://jsonplaceholder.typicode.com/&quot;)
        .addConverterFactory(Json.asConverterFactory(MediaType.parse(&quot;application/json&quot;)!!))
        .build()

    val service = retrofit.create(ReadyService::class.java)

    val call = service.getAlbum(3)
    val response = call.execute()

    if (response.isSuccessful) {
        val album: Album? = response.body()
        println(album)
    } else {
        println(&quot;Failed to load data.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1587139518664&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Album(userId=1, id=3, title=omnis laborum odio, comment=default comment)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실행해보시면 id가 3인 Album 객체가 정상적으로 출력될 것입니다.&lt;/p&gt;
&lt;p&gt;그리고 응답 값에는 없던 comment 필드에도 디폴트 값으로 설정해준 &quot;default comment&quot; 값이 제대로 들어가있을 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;위 예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-JVM/tree/kotlin/kotlinx-serialization-with-retrofit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Kotlin/Library</category>
      <category>kotlin retrofit</category>
      <category>kotlin retrofit converter</category>
      <category>kotlin retrofit serialization</category>
      <category>Kotlin Serialization</category>
      <category>kotlin serialization retrofit</category>
      <category>kotlinx serialization</category>
      <category>코틀린 레트로핏</category>
      <category>코틀린 직렬화</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/178</guid>
      <comments>https://readystory.tistory.com/178#entry178comment</comments>
      <pubDate>Sat, 18 Apr 2020 01:13:21 +0900</pubDate>
    </item>
    <item>
      <title>Kotlinx Serialization - 코틀린에 가장 적합한 Json Converter</title>
      <link>https://readystory.tistory.com/177</link>
      <description>&lt;p&gt;아마 REST API를 구현해보신 분이라면 JSON 데이터를 다뤄보신 경험이 있으실 겁니다.&lt;/p&gt;
&lt;p&gt;개발을 하다보면 JSON 이나 XML 등을 다뤄야하는 경우가 자주 발생하는데요.&lt;/p&gt;
&lt;p&gt;기본적으로 JSON 이나 XML 은 JVM 언어에 정의되어 있는 타입이 아니기 때문에 별도의 변환 작업과 파싱 작업을 해주어야 합니다. 이 과정에서 행해지는 변환 작업을 직렬화(Serialization)/역직렬화(Deserialization) 라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대표적인 Converter Library 로는 &lt;b&gt;Gson, Jackson, Moshi&lt;/b&gt; 등이 있습니다. 그리고 이는 모두 Java 언어로 개발되어 있습니다. Java로 구현되어 있다해서 코틀린에서 사용하지 못하는건 아니기 때문에 코틀린에서도 역시 이 라이브러리를 사용하고 계신 분들이 굉장히 많은데요. 그러나 완벽하게 호환된다고 하기에는 약간 아쉬운 부분들이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;어떤 부분이 아쉬운지 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;코틀린의 data class&lt;/b&gt;&lt;/span&gt; 를 예로 들어보겠습니다.&lt;/p&gt;
&lt;p&gt;먼저, User 라는 이름의 data class 를 정의해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587046652263&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User (
    val name: String,
    val email: String,
    val age: Int = 20
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아시다시피 코틀린의 data class 는 default value가 설정 가능하여, User 객체를 생성할 때 생성자에 age에 대한 값을 생략할 수 있고, 생략할 경우에 age 변수에는 기본 값으로 설정한 20이 설정됩니다. (자바에는 이런 문법이 없습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 User 클래스를 두고 Gson과 Kotlinx.Serialization 의 결과를 비교해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Gson&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;먼저 &lt;a href=&quot;https://github.com/google/gson&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gson&lt;/a&gt;을 통해 변환해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587047229102&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val jsonString = &quot;&quot;&quot;
            {
                &quot;name&quot; : &quot;Ready Kim&quot;,
                &quot;email&quot; : &quot;ready.kim@gmail.com&quot;
            }
        &quot;&quot;&quot;.trimIndent()

    val user = Gson().fromJson(jsonString, User::class.java)

    println(user)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;간단하게 JSON을 구성해서 &quot;name&quot; 과 &quot;email&quot; 필드만 지정해주고서 User 클래스로 변환하도록 코드를 작성해봤습니다.&lt;/p&gt;
&lt;p&gt;위 코드를 실행하면 결과는 다음과 같이 나옵니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1587055658602&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User(name=Ready Kim, email=ready.kim@gmail.com, age=0)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;우리는 분명히 위에서 User 클래스를 작성할 때 age 프로퍼티의 default value로 20을 설정해줬습니다만, 변환된 결과를 보시면 제대로 설정이 안 된것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉, Gson은 코틀린의 default value 문법을 무시해버립니다. primitive 타입의 필드에 대해서는 0, refernce 타입의 필드에 대해서는 null 값을 기본으로 합니다. 주의할 점은 gson 컨버터는 코틀린의 null-safety 를 준수하지 않기 때문에 이에 대한 고려를 하지 않으면 Crash를 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 코틀린에서 Gson, Jackson, Moshi 등을 사용할 때는 각 필드를 nullable 하게 설정해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Kotlinx.Serialization&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;이번에는 코틀린에서 공식적으로 제공하고 있는 &lt;a href=&quot;https://github.com/Kotlin/kotlinx.serialization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kotlinx.Serialization&lt;/a&gt; 라이브러리를 사용해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;일단 Kotlin Serialization 은 코틀린 언어를 만든 JetBrains 사에서 만들었으며, 다른 컨버터 언어들과는 다르게 Reflection을 사용하지 않고 개발한 라이브러리입니다. 리플렉션을 사용하지 않았기 때문에 성능상에서도 다른 컨버터 라이브러리에 비해 경쟁력을 갖습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Kotlinx.Serialization의 사용법은 굉장히 간단합니다.&lt;/p&gt;
&lt;p&gt;그냥 변환하고자 하는 클래스에 &lt;b&gt;@Serializable&lt;/b&gt; 어노테이션만 추가해주면 됩니다. 이 어노테이션을 부착하고 나면 해당 클래스의 Companion Object의 serializer() 함수를 사용할 수 있게 됩니다. 그리고 이 함수가 반환하는 KSerializer 타입의 객체를 직렬화 작업에 사용하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;백문이 불여일코! 한 번 코드를 작성해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Kotlin Serialization을 사용하기 위해서는 디펜던시를 추가해줘야 하는데, 이 부분은 &lt;a href=&quot;https://github.com/Kotlin/kotlinx.serialization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Document&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;준비가 되었다면 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1587054610227&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Serializable
data class User (
    val name: String,
    val email: String,
    val age: Int = 20
)

fun main() {
    val jsonString = &quot;&quot;&quot;
            {
                &quot;name&quot; : &quot;Ready Kim&quot;,
                &quot;email&quot; : &quot;ready.kim@gmail.com&quot;
            }
        &quot;&quot;&quot;.trimIndent()

    val user = Json.parse(User.serializer(), jsonString)

    println(user)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;말씀드린것처럼 Kotlinx Serialization은 리플렉션을 사용하지 않기 때문에 User::class.java 타입의 파라미터를 사용하지 않았습니다.&lt;/p&gt;
&lt;p&gt;실행 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1587055675565&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User(name=Ready Kim, email=ready.kim@gmail.com, age=20)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자! 이번에는 age에 20이 성공적으로 설정된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;글의 서론에서 언급한 것처럼 이런 컨버터 라이브러리는 REST API를 사용할 때 자주 필요로 하게 됩니다. 따라서, HTTP 통신에 사용되는 대표적인 라이브러리인 Retrofit 과 함께 사용하는 경우가 많은데요. 세계적인 네임드 개발자 Jake Wharton 형님이 Retrofit 과 호환되도록 Converter를 추가해주셨습니다. &lt;a href=&quot;https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문서&lt;/a&gt;를 참고하셔서 Retrofit 객체를 생성하실 때 Converter Factory 를 설정해주시면 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;레트로핏과도 함께 사용할 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;추가로, 직렬화의 대상이 되는 클래스에 &lt;b&gt;@Optional&lt;/b&gt;, &lt;b&gt;@Transient&lt;/b&gt; 등의 어노테이션을 사용하면 보다 풍부하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 작성된 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-JVM/tree/kotlin/kotlinx-serialization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;참고&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@jurajkunier/kotlinx-json-vs-gson-4ba24a21bd73&quot;&gt;https://medium.com/@jurajkunier/kotlinx-json-vs-gson-4ba24a21bd73&lt;/a&gt;&lt;/p&gt;</description>
      <category>Kotlin/Library</category>
      <category>kotlin data class serialization</category>
      <category>kotlin deserialization</category>
      <category>kotlin gson</category>
      <category>kotlin jackson</category>
      <category>kotlin moshi</category>
      <category>kotlin retrofit</category>
      <category>kotlin retrofit converter</category>
      <category>Kotlin Serialization</category>
      <category>코틀린 역직렬화</category>
      <category>코틀린 직렬화</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/177</guid>
      <comments>https://readystory.tistory.com/177#entry177comment</comments>
      <pubDate>Fri, 17 Apr 2020 01:49:26 +0900</pubDate>
    </item>
    <item>
      <title>[Android] AAC ViewModel 을 생성하는 6가지 방법 - ViewModelProvider</title>
      <link>https://readystory.tistory.com/176</link>
      <description>&lt;p&gt;이 글은 이전 포스팅(&lt;a href=&quot;https://readystory.tistory.com/173&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 화면 회전해도 데이터 유지하기 - AAC ViewModel&lt;/a&gt;)에 이어지는 글입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModel 클래스를 상속하여 정의한 클래스는 개발자가 직접 생성자를 통하여서 인스턴스를 생성할 수 없고, ViewModelProvider.Factory 인터페이스를 필요로 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅을 통하여서 안드로이드는 어떻게 ViewModel 을 관리하고, 생성하는지 알아보고 필요에 따라 어떤 방식으로 뷰모델 객체를 생성해야 하는지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;ViewModelStoreOwner / ViewModelStore&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;ViewModel 은 ViewModelStore 라는 객체에서 관리를 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;ViewModelStore 클래스는 내부적으로 HashMap&amp;lt;String, ViewModel&amp;gt; 를 두어 ViewModel 을 관리합니다.&lt;/p&gt;
&lt;p&gt;그러면 이 ViewModelStore 객체는 누가 어떻게 만들고 관리할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그건 바로 &lt;b&gt;ViewModelStoreOwner&lt;/b&gt; 라는 녀석이 합니다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ViewModelStoreOwner 는 다음과 같이 생긴 인터페이스이고, FragmentActivity 의 부모격인 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;ComponentActivity&lt;/span&gt;&lt;/b&gt;와 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Fragment&lt;/b&gt;&lt;/span&gt; 클래스가 이를 구현(Implement) 하고 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1586947259243&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Java Code
public interface ViewModelStoreOwner {

    @NonNull
    ViewModelStore getViewModelStore();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;액티비티와 프래그먼트가 바로 이 ViewModelStoreOwner 를 구현하고 있기 때문에, 우리는 ViewModel 객체를 생성할 때 액티비티나 프래그먼트를 필요로 하고, 어떤 Owner 를 통해 생성하냐에 따라 ViewModel 의 Scope 가 결정됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 액티비티나 프래그먼트만 있으면 ViewModel 인스턴스를 생성할 수 있을까요?&lt;/p&gt;
&lt;p&gt;아니겠죠. 앞서 말씀 드린것처럼 ViewModel 을 생성할 때는 실질적으로 ViewModel 인스턴스를 생성하는 역할을 하는 팩토리를 필요로 합니다. (팩토리 패턴이 낯선 분들은 &lt;a href=&quot;https://readystory.tistory.com/117&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고하세요)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModelProvider 안에 정의되어 있는 팩토리는 다음과 같이 정의되어 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1586947681530&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Java Code
public class ViewModelProvider {

    public interface Factory {

        @NonNull
        &amp;lt;T extends ViewModel&amp;gt; T create(@NonNull Class&amp;lt;T&amp;gt; modelClass);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 ViewModel 인스턴스를 생성하기 위한 재료들은 전부 확인 했으니, 실습을 통해 직접 생성해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;&lt;s&gt;ViewModelProviders&lt;/s&gt; -&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;Deprecated&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;정상적인 실습을 하기에 앞서, 많은 블로그들과 심지어 구글 공식 Developer 사이트에서도 아직까지 예제로 사용되고 있는 ViewModelProviders 에 대해 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;s&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ViewModelProviders.of(this).get(AnyViewModel::class.java)&lt;/span&gt;&lt;/s&gt; --&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;deprecated !!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModelProviders 클래스는 Deprecated 되었습니다. &quot;Deprecated 되었다&quot; 라는 말은 해당 클래스(또는 메소드)를 더이상 개발하지 않는 것을 뜻하며 특정 버전 이후로 호환되지 않을 수 있기 때문에 더이상 사용하지 않아야한다는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대체로 deprecated 되는 기능들에 대해서는 대안이 함께 제시되는데요. 이 경우에는 ViewModelProviders 대신에 ViewModelProvider 를 사용하도록 권장하고 있습니다. 따라서 우리는 ViewModelProvider 를 통해 ViewModel 객체를 생성하는 방법에 대해서만 다루도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;1. 파라미터가 없는 ViewModel - &lt;span style=&quot;color: #006dd7;&quot;&gt;Lifecycle Extensions&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;이 방법은 가장 편한 방법 중 하나입니다. androidx.lifecycle의 lifecycle-extensions 모듈을 가져와 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;먼저, module 수준의 build.gradle 에 다음과 같이 디펜던시를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586948874018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    // ...
    implementation &quot;androidx.lifecycle:lifecycle-extensions:2.2.0&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 예제에 사용할 ViewModel 클래스를 정의하겠습니다. 파라미터와 멤버 변수, 함수를 갖고있지 않은 심플한 ViewModel 클래스입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586948788799&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 파라미터가 없는 ViewModel class
class NoParamViewModel : ViewModel()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 이 뷰모델 클래스를 통하여 액티비티에서 객체를 생성해주겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586948970237&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private lateinit var noParamViewModel: NoParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* use ViewModelProvider's constructor provided from lifecycle-extensions package */
        noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보시다시피 ViewModelProvider 생성자에 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;this&lt;/span&gt;&lt;/b&gt; 를 포함해주고, get() 메소드 내에 생성하고자 하는 뷰모델 클래스 타입을 넣어주면 됩니다.&lt;/p&gt;
&lt;p&gt;이때 this 는 ViewModelStoreOwner 타입이기 때문에 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;액티비티&lt;/span&gt;&lt;/b&gt;나 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;프래그먼트&lt;/span&gt;&lt;/b&gt;를 넣어주시면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;2. 파라미터가 없는 ViewModel - &lt;span style=&quot;color: #006dd7;&quot;&gt;ViewModelProvider.NewInstanceFactory&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;이번에 살펴볼 방법은 &lt;b&gt;NewInstanceFactory&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;이는 안드로이드가 기본적으로 제공해주는 팩토리 클래스이며, ViewModelProvider.Factory 인터페이스를 구현하고 있습니다. 따라서 ViewModel 클래스가 파라미터를 필요로 하지 않거나, 특별히 팩토리를 커스텀 할 필요가 없는 상황에서는 1번 방법을 사용하거나, 2번 방법을 사용하면 되겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2번 방법은 1번에서 추가해준 lifecycle-extenstions 모듈을 추가하지 않아도 사용가능한 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586949557205&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private lateinit var noParamViewModel: NoParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(NoParamViewModel::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;3. &lt;b&gt;파라미터가 없는 ViewModel -&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;ViewModelProvider.Factory&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;이번에는 직접 &lt;b&gt;ViewModelProvider.Factory&lt;/b&gt; 인터페이스를 구현하여 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;이 방법의 장점은 하나의 팩토리로 다양한 ViewModel 클래스를 관리할 수도 있고, 원치 않는 상황에 대해서 컨트롤 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586950424947&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class NoParamViewModelFactory : ViewModelProvider.Factory {
    override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
        return if (modelClass.isAssignableFrom(NoParamViewModel::class.java)) {
            NoParamViewModel() as T
        } else {
            throw IllegalArgumentException()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 NoParamViewModel 클래스가 아니면 &lt;span style=&quot;color: #ee2323;&quot;&gt;IllegalArgumentException&lt;/span&gt; 을 던지도록 구현되어 있습니다. 이는 어디까지나 개발자의 마음대로 구현하면 되는 부분이며, 어떤 타입의 클래스가 전달되더라도 인스턴스를 생성하도록 구현할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;4. 파라미터가 있는 ViewModel - &lt;span style=&quot;color: #006dd7;&quot;&gt;ViewModelProvider.Factory&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;3번 방법의 연장선상에서, &lt;b&gt;ViewModelProvider.Factory&lt;/b&gt; 를 구현하면 파라미터를 소유하고 있는 ViewModel 객체의 인스턴스를 생성할 수 있습니다. 직접 구현한 Factory 클래스에 파라미터를 넘겨주어 create() 내에서 인스턴스를 생성할 때 활용하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 이번에는 파라미터가 있는 ViewModel 을 정의하고 그에 대한 객체를 생성하는 예제를 작성해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;ViewModel&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1586950976478&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HasParamViewModel(val param: String) : ViewModel()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;ViewModelFactory&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1586950992882&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HasParamViewModelFactory(private val param: String) : ViewModelProvider.Factory {
    override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
        return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
            HasParamViewModel(param) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Activity (or Fragment)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1586951058380&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private lateinit var hasParamViewModel: HasParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sampleParam = &quot;Ready Story&quot;

        hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
            .get(HasParamViewModel::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3번과 4번 예제에서는 각각 파라미터 유무에 따라 별도의 팩토리 클래스를 구현하였지만, 꼭 그럴 필요 없이 하나의 팩토리 클래스로 두 가지 상황에 대한 처리를 한꺼번에 할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;5. 파라미터가 없는 AndroidViewModel - &lt;span style=&quot;color: #006dd7;&quot;&gt;AndroidViewModelFactory&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;이번에는 조금 다른 ViewModel 을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;사실 developer 사이트에 의하면 ViewModel 클래스에서 Context 객체를 소유하거나 접근하는 것에 있어서 권장하지 않고 있습니다. 하지만 정말 불가피하게 필요한 경우가 있을 수 있는데요. ViewModel 에서 Context 를 사용해야할 필요성이 있을 때는 &lt;b&gt;AndroidViewModel&lt;/b&gt; 클래스를 사용하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 안드로이드에서는 이러한 AndroidViewModel 객체에 대한 생성을 위해 ViewModelProvider.&lt;span&gt;AndroidViewModelFactory&lt;/span&gt; 라는 별도의 팩토리를 제공합니다. 예제를 통해 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 AndroidViewModel 을 상속하는 ViewModel 클래스를 정의해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586951605885&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보시다시피 AndroidViewModel 은 Application 객체를 필요로 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 AndroidViewModelFactory 를 이용하여 뷰모델 객체를 생성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586951701439&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private lateinit var noParamAndroidViewModel: NoParamAndroidViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
            .get(NoParamAndroidViewModel::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AndroidViewModelFactory 내부 코드를 살펴보면 2번에서 살펴본 NewInstanceFactory 를 상속한 코드라는 걸 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;6. 파라미터가 있는 AndroidViewModel&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;드디어 마지막입니다. &lt;b&gt;파라미터가 있는&lt;/b&gt; &lt;b&gt;AndroidViewModel&lt;/b&gt; 객체를 생성하는 방법인데요.&lt;/p&gt;
&lt;p&gt;사실 4번의 방법으로도 가능합니다만, 이번에는 5번에서 살펴본 AndroidViewModelFactory 와 유사한 방식으로 커스텀 팩토리를 구현해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 파라미터가 있는 AndroidViewModel 클래스를 준비합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586951956431&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 Custom Factory를 구현해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586952054097&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HasParamAndroidViewModelFactory(private val application: Application, private val param: String)
    : ViewModelProvider.NewInstanceFactory() {

    override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
        if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
            try {
                return modelClass.getConstructor(Application::class.java, String::class.java)
                    .newInstance(application, param)
            } catch (e: NoSuchMethodException) {
                throw RuntimeException(&quot;Cannot create an instance of $modelClass&quot;, e)
            } catch (e: IllegalAccessException) {
                throw RuntimeException(&quot;Cannot create an instance of $modelClass&quot;, e)
            } catch (e: InstantiationException) {
                throw RuntimeException(&quot;Cannot create an instance of $modelClass&quot;, e)
            } catch (e: InvocationTargetException) {
                throw RuntimeException(&quot;Cannot create an instance of $modelClass&quot;, e)
            }
        }
        return super.create(modelClass)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;제법 복잡해보이지만, catch 문을 많이 분기했을 뿐이지 실상은 복잡하지 않습니다.&lt;/p&gt;
&lt;p&gt;위 코드에서는 ViewModelProvider.NewInstanceFactory 클래스를 상속하여 구현했지만, ViewModelProvider.Factory 인터페이스를 구현하여도 무방합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;긴 글이었습니다. 어떤 방법으로든 각자의 상황에 맞게 사용하시면 되고, 이번 포스팅에서 다루지는 않았지만 android-ktx / fragment-ktx 모듈을 사용하면 보다 편리하게 뷰모델 인스턴스를 생성할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위에서 작성된 모든 예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/viewmodel-provider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>AAC ViewModel</category>
      <category>Android ViewModel</category>
      <category>Android ViewModel Factory</category>
      <category>Android ViewModelProvider</category>
      <category>AndroidViewModel</category>
      <category>ViewModel Factory</category>
      <category>ViewModelProvider</category>
      <category>ViewModelProvider.Factory</category>
      <category>ViewModelProvider.NewInstanceFactory</category>
      <category>파라미터 있는 ViewModel</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/176</guid>
      <comments>https://readystory.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 16 Apr 2020 08:00:02 +0900</pubDate>
    </item>
    <item>
      <title>[Git] Local branch, Remote branch 이름 바꾸기</title>
      <link>https://readystory.tistory.com/175</link>
      <description>&lt;p&gt;프로젝트의 크기가 커지거나, Git flow, Github flow 등의 브랜치 전략을 지키다보면 브랜치 이름을 수정해야하는 소요가 종종 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 브랜치 이름을 바꾸는 방법에 대해 다뤄보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두 방법 모두 간단합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Local Branch(로컬 브랜치) 이름 변경 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 변경하고자 하는 브랜치로 checkout 을 해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586935922234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git checkout &amp;lt;old_name_branch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 git branch 명령어에 -m 옵션을 주어 이름을 변경해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586935984063&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git branch -m &amp;lt;new_name_branch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 하면 기존에 있던 브랜치가 새로 지정한 브랜치 이름으로 변경된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Remote Branch(원격 브랜치) 이름 변경 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Github 이나 Gitlab 등 원격 저장소에 이미 생성되어 있는 브랜치의 이름을 변경하는 방법은 위 방법과는 조금 다릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저, push 하고자 하는 branch 에 checkout 하신 후에 새로운 이름의 브랜치로 push 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586936131820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push origin -u &amp;lt;new_name_branch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 명령을 실행한 후에 원격 저장소를 확인해보시면 기존에 있던 저장소의 이름이 바뀐게 아니라 새로운 브랜치가 하나 늘어난걸 확인할 수 있는데요. 이제 필요없는 예전 이름의 브랜치를 --delete 옵션을 통해 삭제해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586936231497&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push origin --delete &amp;lt;old_name_branch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러면 결과적으로 old name 브랜치가 new name 브랜치로 이름이 변경된 효과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <category>git branch</category>
      <category>git branch -m</category>
      <category>git branch rename</category>
      <category>git local granch rename</category>
      <category>git remote branch rename</category>
      <category>깃 브랜치 이름 변경</category>
      <category>로컬 브랜치 이름 변경</category>
      <category>브랜치 이름 변경</category>
      <category>원격 브랜치 이름 변경</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/175</guid>
      <comments>https://readystory.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 15 Apr 2020 16:38:58 +0900</pubDate>
    </item>
    <item>
      <title>This version of Android Studio cannot open this project, please retry with Android Studio x.y or newer. 문제 해결하기</title>
      <link>https://readystory.tistory.com/174</link>
      <description>&lt;p&gt;협업을 하거나 오픈 소스를 가져오는 등 내가 직접 프로젝트를 생성한 것이 아니라 다른 사람(혹은 컴퓨터)이 만든 프로젝트를 내 안드로이드 스튜디오로 가져와서 실행하려고 할 때 다음과 같은 에러를 띄우면서 빌드에 실패하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;This version of Android Studio cannot open this project, please retry with Android Studio x.y or newer.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이는 프로젝트에 설정된 build-tool(gradle) 버전과 내 안드로이드 스튜디오의 버전이 맞지 않아&amp;nbsp; 발생하는 에러인데요. 해결 방법은 간단합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 문제가 발생하는 프로젝트로 가셔서 &lt;b&gt;프로젝트 수준의 build.gradle&lt;/b&gt; 파일로 가서 프로젝트에 설정된 gradle 버전을 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586932439859&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    classpath 'com.android.tools.build:gradle:3.6.1'  // 3.6.1 버전
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 3.6.1 자리에 적혀 있는 숫자가 바로 gradle 버전입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 내 안드로이드 스튜디오의 버전을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Mac 기준으로 좌측 상단의&lt;b&gt; [Android Studio] - [About Android Studio]&lt;/b&gt; 를 누르시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1aUOq/btqDp1PbGDi/GKk5kVAbnBQp6ncYtbRhgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1aUOq/btqDp1PbGDi/GKk5kVAbnBQp6ncYtbRhgk/img.png&quot; data-alt=&quot;Mac 기준&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1aUOq/btqDp1PbGDi/GKk5kVAbnBQp6ncYtbRhgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1aUOq%2FbtqDp1PbGDi%2FGKk5kVAbnBQp6ncYtbRhgk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Mac 기준&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Windows 기준으로는&lt;b&gt; [Help] - [About]&lt;/b&gt; 을 누르시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러면 아래와 같이 창이 뜨고, 버전을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;361&quot; data-filename=&quot;blob&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J7SoW/btqDtzqop4G/jiyrPmZLjMmCwlV5rDAzEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J7SoW/btqDtzqop4G/jiyrPmZLjMmCwlV5rDAzEK/img.png&quot; data-alt=&quot;예시 - 3.5.3 버전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J7SoW/btqDtzqop4G/jiyrPmZLjMmCwlV5rDAzEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ7SoW%2FbtqDtzqop4G%2FjiyrPmZLjMmCwlV5rDAzEK%2Fimg.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;361&quot; data-filename=&quot;blob&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 - 3.5.3 버전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 문제를 명확하게 발견했습니다!&lt;/p&gt;
&lt;p&gt;프로젝트에 설정된 gradle 버전은 3.6.1 버전이었는데, 안드로이드 스튜디오의 버전은 3.5.3 버전이었기 때문에 발생하는 문제였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 이를 해결하기 위해서는 다음 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;둘 중 하나의 방법&lt;/span&gt;&lt;/b&gt;을 택하면 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;안드로이드 스튜디오의 버전은 그대로 두고, 프로젝트에 설정된 gradle 버전을 낮춘다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트의 설정된 gradle 버전은 그대로 두고, 안드로이드 스튜디오의 버전을 업그레이드 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1번은 앞서 살펴봤던 프로젝트 수준의 build.gradle 로 가셔서 버전 부분만 수정하면 되구요.&lt;/p&gt;
&lt;p&gt;2번은 Mac 기준으로 &lt;b&gt;[Android Studio] - [check for updates]&lt;/b&gt;, Windows 기준으로 &lt;b&gt;[Help] - [check for updates] &lt;/b&gt;를 클릭하시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;어떤 방법을 선택하던지 무작정 편한 방법으로 하시기 보다는 협업하는 동료들이 있다면 협의 후에, 여러 컴퓨터에서 개발해야 하는 경우에는 다른 프로젝트와의 호환성을 고려하셔서 대응하시는 것을 추천합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Resolution</category>
      <category>android studio gradle version</category>
      <category>android studio version</category>
      <category>android studio version update</category>
      <category>gradle version</category>
      <category>gradle version update</category>
      <category>please retry with Android Studio x.y or newer.</category>
      <category>This version of Android Studio cannot open this project</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/174</guid>
      <comments>https://readystory.tistory.com/174#entry174comment</comments>
      <pubDate>Wed, 15 Apr 2020 15:50:52 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 화면 회전해도 데이터 유지하기 - AAC ViewModel</title>
      <link>https://readystory.tistory.com/173</link>
      <description>&lt;p&gt;안드로이드는 액티비티나 프래그먼트를 관리할 때 Lifecycle 을 토대로 관리합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnxOpO/btqDsOA57LU/y5nXiDNHJefaxyH57wa5X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnxOpO/btqDsOA57LU/y5nXiDNHJefaxyH57wa5X1/img.png&quot; data-alt=&quot;그림 1. 액티비티 라이프 사이클&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnxOpO/btqDsOA57LU/y5nXiDNHJefaxyH57wa5X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnxOpO%2FbtqDsOA57LU%2Fy5nXiDNHJefaxyH57wa5X1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. 액티비티 라이프 사이클&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;우리는 이러한 라이프사이클을 직접 관리하는 것이 아니라 안드로이드 시스템이 각 라이프사이클에 맞춰서 호출해주는 콜백 함수(onCreate() 등)을 Override 하여 때에 맞는 로직을 추가할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드를 시작한 지 얼마 안되신 분들이 많이 겪는 문제 중 하나가 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;세로 모드에서 가로 모드 전환시 데이터가 초기화 되거나 날아가는 현상&lt;/span&gt;&lt;/b&gt;입니다. 앞서 라이프사이클에 대해 말씀드린 이유는 바로 이 문제의 원인이 라이프사이클에 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드의 액티비티는 세로 모드/가로 모드 전환시 라이프사이클 중 &lt;span style=&quot;color: #ee2323;&quot;&gt;onDestory()&lt;/span&gt;가 호출된 다음 &lt;b&gt;onCreate()&lt;/b&gt;가 다시 호출됩니다. &lt;b&gt;onCreate()&lt;/b&gt;가 다시 호출될 때 xml layout이 다시 Inflate 되기 때문에 뭔가 작업 중이던 데이터가 초기화되게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이에 대한 해결책으로 onSaveStateInstance()와 ViewModel 클래스가 있습니다만, 오늘은 그 중 ViewModel에 대해서만 다루도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;AAC ViewModel&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;ViewModel이면 그냥 ViewModel 이지, 왜 자꾸 AAC ViewModel 이라고 언급할까요? 그건 오늘 다루는 ViewModel 클래스가 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;MVVM 아키텍쳐에서 말하는 ViewModel 과는 전혀 관련이 없기 때문&lt;/span&gt;&lt;/b&gt;에 확실히 구분하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 &lt;b&gt;AAC&lt;/b&gt;는 뭘까요?&lt;/p&gt;
&lt;p&gt;AAC는 &lt;a href=&quot;https://developer.android.com/topic/libraries/architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Android Architecture Component&lt;/b&gt;&lt;/a&gt;의 약자로, &lt;a href=&quot;https://developer.android.com/jetpack?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jetpack 라이브러리&lt;/a&gt; 중 &quot;아키텍처&quot; 부분에 해당합니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 편의상 ViewModel 이라고 부르겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModel 클래스는 액티비티와 프래그먼트의 데이터를 관리하는 데 사용됩니다. 그리고 액티비티(또는 프래그먼트)와 어플리케이션 내 다른 클래스들과의 커뮤니케이션 하는데 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModel 클래스는 액티비티나 프래그먼트의 Scope와 함께 생성되고, 이 Scope가 살아있는 동안에 유지됩니다. 다르게 말하면, 액티비티나 프래그먼트가 Screen Rotation 등의 이유로 Destroy 상태가 되더라도 ViewModel은 Destroy 되지 않는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OfJFY/btqDoTcLGMG/YtPnr6HtW8jbsNQQeV2zKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OfJFY/btqDoTcLGMG/YtPnr6HtW8jbsNQQeV2zKK/img.png&quot; data-alt=&quot;그림 2. AAC ViewModel 의 Scope&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OfJFY/btqDoTcLGMG/YtPnr6HtW8jbsNQQeV2zKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOfJFY%2FbtqDoTcLGMG%2FYtPnr6HtW8jbsNQQeV2zKK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. AAC ViewModel 의 Scope&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ViewModel 클래스는 주로 LiveData와 Data Binding과 함께 사용됩니다.&lt;br /&gt;(&lt;a href=&quot;https://readystory.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] AAC ViewModel과 찰떡 궁합! LiveData 이해하기&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;주의하실 점은 ViewModel은 오직 UI에 대한 데이터만을 관리하는 데에만 책임을 가져야 합니다. 그렇기 때문에 ViewModel은 View에 대한 접근을 하거나 액티비티나 프래그먼트에 대한 참조를 갖고 있으면 안됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;간단한 예제를 통해 코드와 함께 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;예제는 화면 중앙에 TextView를 두고, EditText를 통해 값을 변경한 다음에 화면을 가로/세로 전환했을 때 데이터가 유지되는지 &lt;span style=&quot;color: #333333;&quot;&gt;ViewModel 사용한 버전과 사용하지 않은 버전을 비교해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;319&quot; data-origin-height=&quot;658&quot; width=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMkaR2/btqDsbXG8r4/5SNPfLMkloaJog9U4OWYKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMkaR2/btqDsbXG8r4/5SNPfLMkloaJog9U4OWYKk/img.png&quot; data-alt=&quot;예제 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMkaR2/btqDsbXG8r4/5SNPfLMkloaJog9U4OWYKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMkaR2%2FbtqDsbXG8r4%2F5SNPfLMkloaJog9U4OWYKk%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;319&quot; data-origin-height=&quot;658&quot; width=&quot;360&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;예제 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;공통&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;먼저 버전에 상관 없이 공통으로 사용되는 layout을 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1586882454292&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&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;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;EditText
        android:id=&quot;@+id/edit_text&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;40dp&quot;
        android:inputType=&quot;text&quot;
        android:layout_margin=&quot;10dp&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toStartOf=&quot;@id/ok_button&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/ok_button&quot;
        android:layout_width=&quot;40dp&quot;
        android:layout_height=&quot;40dp&quot;
        android:gravity=&quot;center&quot;
        android:text=&quot;OK&quot;
        android:layout_margin=&quot;10dp&quot;
        android:background=&quot;@drawable/button_selector&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/sample_text&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;@string/hello_text&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ViewModel 사용하지 않은 버전&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;ViewModel을 사용하지 않은 버전의 액티비티 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586882560755&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* ViewModel 사용하지 않은 버전 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ok_button.setOnClickListener {
            sample_text.text = edit_text.text
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기까지 작성하시고 앱을 실행해보시면 EditText에 텍스트를 입력 후 버튼을 누르면 중앙에 있는 TextView가 변경되지만, 화면을 전환하면 다시 초기값으로 돌아가는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ViewModel을 사용한 버전&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;ViewModel을 정의하는 방법은 androidx.lifecycle 패키지의 ViewModel 추상 클래스를 상속하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;만약 어플리케이션과 생명주기를 함께하는 싱글톤 ViewModel을 선언하고 싶으시다면 AndroidViewModel을 상속하시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저, ViewModel과 LiveData를 사용하기 위해서는 모듈 수준의 build.gradle 파일에 androidx.lifecycle 패키지에 대한 dependency 를 설정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586883683231&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    // ...
    implementation &quot;androidx.lifecycle:lifecycle-extensions:2.2.0&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 ViewModel 클래스를 다음과 같이 정의해줍니다.&lt;/p&gt;
&lt;p&gt;원활한 예제를 위해 LiveData를 간단하게 사용하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586882995060&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainViewModel : ViewModel() {
    private var inputText: MutableLiveData&amp;lt;String&amp;gt; = MutableLiveData()

    fun getInputText() = inputText

    fun updateText(newText: String) {
        inputText.value = newText
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 액티비티 코드를 작성해줍니다.&lt;/p&gt;
&lt;p&gt;한 가지 알아야 하는 것은 &lt;b&gt;우리는 ViewModel 클래스를 선언할 때 직접 생성자를 사용하여 인스턴스를 생성할 수 없습니다.&lt;/b&gt; 안드로이드에게 생성에 대하여 위임해야 합니다. 따라서 우리는 ViewModelProvider 를 통해 뷰모델 객체를 생성해야 하며, 관련해서 다양한 방법들이 있지만 자세한 내용은 별도의 포스팅에서 다루도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1586883064988&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* ViewModel 사용한 버전 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewModel.getInputText().observe(this, Observer&amp;lt;String&amp;gt; { newStr -&amp;gt;
            sample_text.text = newStr
        })

        ok_button.setOnClickListener {
            viewModel.updateText(edit_text.text.toString())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 주의깊게 보실 부분은 ViewModelProvider(this) 의 this 입니다.&lt;/p&gt;
&lt;p&gt;이는 ViewModel 객체의 ViewModelStoreOwner를 넘겨주는 것이며, 여기서 어떤 액티비티나 프래그먼트를 설정해주느냐에 따라 ViewModel의 생명주기가 결정됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 작성하신 다음 실행해보시면 중앙에 있는 TextView 값을 변경한 후에 화면을 전환 하더라도 값이 그대로 유지가 되어있는 것을 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;onCleared()&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;ViewModel을 더이상 사용하지 않거나, 뷰모델이 관찰하고 있는 데이터를 초기화해야할 때는 onCleared()를 호출하시면 됩니다. 이를 잘 활용하면 메모리 누수(Memory Leak)을 예방할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;AAC ViewModel은 단독으로 쓰일때보다 LiveData와 DataBinding 등과 함께 사용될 때 진가를 발휘합니다. 이번 포스팅에서 간단하게 ViewModel을 살펴보았으니 추후 Data Binding과 함께 MVVM 아키텍쳐 포스트를 할 때 다시 다루도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예제 코드는 &lt;a href=&quot;https://github.com/KimReady/Blog-Sample-Android/tree/post/aac-viewmodel&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 저장소&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Jetpack</category>
      <category>AAC ViewModel</category>
      <category>Android Architecture Component</category>
      <category>Android Jetpack</category>
      <category>Android ViewModel</category>
      <category>Android ViewModel Example</category>
      <category>Android ViewModelProvider</category>
      <category>ViewModel</category>
      <category>ViewModel onCleared</category>
      <category>안드로이드 뷰모델</category>
      <category>안드로이드 뷰모델 예제</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/173</guid>
      <comments>https://readystory.tistory.com/173#entry173comment</comments>
      <pubDate>Wed, 15 Apr 2020 02:11:04 +0900</pubDate>
    </item>
    <item>
      <title>개발자 블로그 플랫폼에 대한 고찰(with. Medium, Tistory, Github.io)</title>
      <link>https://readystory.tistory.com/172</link>
      <description>&lt;p&gt;&lt;i&gt;4월 10일(금) ~ 12일(일).&lt;/i&gt; 3일에 걸쳐 블로그를 개편했다.&lt;/p&gt;
&lt;p&gt;사실 개편이래봐야 플랫폼을 옮긴 것도 아니고(진지하게 고민했지만), 그냥 스킨 바꾸고 css 이것 저것 수정하면서 내가 원하는 형태로 커스텀한 정도이지만 꽤 유익한 경험을 했기 때문에 글로 기록을 남긴다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 경험을 통해 기록하고자 하는 바는 3가지이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 플랫폼 변경에 대한 고민&lt;/h4&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Medium?&lt;/span&gt; &lt;span style=&quot;color: #009a87;&quot;&gt;Tistory?&lt;/span&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt;Github.io?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;사실 올 해 들어 블로그와 관련해서 가장 많이 했던 고민 중 하나이다.&lt;/p&gt;
&lt;p&gt;우선 내가 돈을 내고 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Medium&lt;/span&gt;&lt;/b&gt; 정기 구독을 하고 있기도 하고, 많은 국내외 유명 개발자들이 미디엄에서 블로그 활동을 하고 있기 때문에 미디엄이 매력적으로 느껴졌다. 특히 Android Weekly, Kotlin Weekly 등의 구독 서비스를 통해 받아보는 칼럼의 대다수는 미디엄 링크이기도 하다. 하지만 다음 이유로 미디엄으로 가는 것은 포기했다.&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;무료로 보는 것에 한계가 있다.&lt;/b&gt;&lt;br /&gt;-&amp;gt; 그렇기에 내 글을 읽을 수 있는 독자의 범위가 좁아질 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI 커스텀이 불가능하다.&lt;/b&gt;&lt;br /&gt;-&amp;gt; 나는 디자인 감각이 거의 없다시피 한 사람이지만 그래도 취향이라는게 있다..ㅋ&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아직 한국에서는 멤버쉽도 지원하지 않는 등 완전히 정착하지는 못한 듯 하다.&lt;/b&gt;&lt;br /&gt;-&amp;gt; 멤버쉽이 지원되면 다시 고민해 볼 듯..&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그럼에도 너무 깔끔한 디자인에, 나름 좋은 UX를 가지고 있다 생각하여 독자로써는 매우매우 잘 이용하고 있는 플랫폼이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Github.io&lt;/span&gt;&lt;/b&gt;에 대해 얘기해보자면 앞서 미디엄에서 말한 단점을 모두 커버하고 있는 플랫폼이다. 무료이고 커스텀이 가능하며 한국에서도 많은 개발자들이 블로그로 사용하고 있다. 게다가 &lt;b&gt;마크다운으로 작성되기 때문에 블로그 글을 쓸 때마다 내 저장소에 잔디가 심어져 블로그 활동이 곧 깃헙 활동으로 이어지는 이점이 있다.&lt;/b&gt; 그러나 내게 있어 마크다운으로 블로그 포스팅을 하는 것은 생산성이 좀 떨어졌다. 마크다운으로 작성하는 것이 크게 불편하진 않았으나 왠지 모르게 속도가 느렸다. 그리고 어드민 페이지가 없어 통계 등 블로그 관리하는 데 있어 다른 플랫폼에 비해 아쉬운 부분들이 조금 있었다.&lt;/p&gt;
&lt;p&gt;뭐랄까.. 개인 블로그를 별도로 제작하여 호스팅하는 느낌..?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 보시다시피 최종적으로 결정한 플랫폼, &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;Tistory&lt;/span&gt;&lt;/b&gt; 이다. 티스토리는 인터넷 상에 배포되어 있는 스킨을 빠르고 다양하게 적용해볼 수 있다는 점에서 강점을 갖지만 결국엔 내 입맛대로 변경하다보면 손길이 적게 가는 건 아니다.(스킨을 수정할 때는 라이센스를 잘 확인해야 한다.) 그러나 앞서 말했던 다른 플랫폼들이 갖고 있는 단점들을 거의 다 커버하고 있다고 생각한다. &lt;b&gt;무료에 커스텀 가능하고, 대중적이며 구글 검색에 노출도 잘 되고 심지어 마크다운도 호환된다.&lt;/b&gt; 그래서 일단은 티스토리에 남아있기로 결정했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 스킨 변경&lt;/h4&gt;
&lt;p&gt;티스토리에 남아있기로 한 나는 스킨을 당장 변경했다. 이전에 쓰던 스킨을 약 5개월 정도 사용했었는데, javascript 쪽이 너무 무겁게 작성되어 있어서 내 블로그의 평균 렌더링 시간이 평균 5초 정도로 분석됐다. &lt;span style=&quot;color: #ee2323;&quot;&gt;특히나 렌더링 시간에 민감한 대한민국 국민에게 이 정도의 퍼포먼스는 곧바로 방문자 이탈로 이어질 수 있다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;처음에는 내가 직접 코드를 살펴보고 수정하면서 렌더링 속도를 높여볼까 했지만 스크립트의 가독성이 현저하게 떨어져 이걸 유지보수 하느니 차라리 내가 다시 코드를 짜거나 다른 스킨으로 바꾸는게 낫겠다고 판단했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고서 이리저리 좋은 스킨 없나 하고 넷상을 돌아다니면서 유료 스킨도 살펴보고 옛날 스킨도 살펴보다가 하나의 스킨을 골랐다. 그리고 재배포가 안될뿐 수정은 가능하다는 것을 확인하고 바로 적용하였는데, 일단 아무 생각 없이 변경하고 나니 손 댈 곳이 너~~무 많았다. 아마 개발자용으로 제작된 스킨이 아니다 보니 개발자 감성(?)에 맞는 디자인 보다는 일반 산문 형식의 블로그에 특화된 듯한 숨은 설정들이 많았어서 css 파일과 javascript 파일을 뜯어내며 내 입맛대로 엄청난 수정을 가했다. 거의 반은 갈아 엎은 정도..ㅎ&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;처음에는 css만 깔짝깔짝 바꾸다가 아예 없던 기능을 추가할 일이 있어 블로그를 시작하고 반 년만에 처음으로 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;티스토리 스킨 가이드&lt;/a&gt;라는 것을 찾아 살펴봤다. 웃기지만 티스토리 스킨 가이드는 github.io 로 되어있다. 스킨 가이드를 보며 댓글 관리나 카테고리 리스트 등에 대한 작업을 추가하고, 프로필 등을 수정했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;어쨌든 스킨을 변경하고 나니 렌더링 속도가 눈에 띄게 향상됐다. 평균 렌더링 속도가 1초 정도가 나오면서 일반 사용자들이 불편함을 느끼지 않을 정도가 되었다. 작업하면서는 이럴 시간에 글 3개를 더 쓰겠다 싶었지만 막상 변경하고 나니 바꾸길 잘했다 싶다.&lt;/p&gt;
&lt;p&gt;&lt;i&gt;마치 레거시 프로젝트를 리팩토링한 듯한 기분..?&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. code 테마 선정&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;개발자 블로그에 있어 가장 중요한 요소 중 하나는 예시 코드가 담겨있는 부분의 테마라고 생각한다.&lt;/b&gt; 어떤 테마를 선택하냐에 따라 블로그의 분위기와 무게감, 가독성 등이 좌우된다. 나에게 있어 어떤 테마를 선택하느냐는 블로그 스킨만큼이나 중요했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;나는 이상하게 안드로이드 스튜디오나 인텔리 제이 등 IDE를 사용할 때 무조건 다크 테마로 어두운 배경에서 코드를 작성하는데 블로그 글 만큼은 꼭 밝은 배경에 포스팅 하고 싶었다. 앞서 말했듯 나는 평상시에도 Medium 블로그를 굉장히 많이 보는 편인데, 그래서인지 github-gist 테마가 굉장히 깔끔하고 예쁘다는 생각을 자주했다. 그래서 이걸 내 블로그에도 적용할 수는 없을까? 하다가 hilightjs에서 github-gist 테마도 지원하는 것을 확인하고 Medium 블로그를 벤치마킹 하여 최대한 비슷하게 만들어 이렇게나마 Medium 으로 가지 않은 아쉬움(?)을 달랬다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p&gt;사실 주로 안드로이드 코드만 보던 중에 html, css, javascript 를 오랜만에 보게 돼서 재밌기도 했다. 그리고 확실히 프론트엔드 쪽 공부도 조만간 해야겠다는 생각이 들기도 했고..ㅎ 개선점이 보이면 미루지 않고 빨리 행동해야 한다는 생각을 갖고 블로그 개편을 실시했는데 일단 만족하고 있고, 겉만 번지르르한 것이 아니라 알맹이도 잘 채워넣는 블로그를 만들어 나가야겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;그리고 Medium 은 내년이나 내후년쯤부터 영어 블로그를 만들어 활동할 때 이용하려고 계획하고 있다.&lt;/b&gt;(아직은 계획만..)&lt;/p&gt;
&lt;p&gt;끝!&lt;/p&gt;</description>
      <category>Life</category>
      <category>github.io</category>
      <category>medium blog</category>
      <category>개발자 블로그</category>
      <category>깃헙</category>
      <category>미디엄</category>
      <category>블로그 개편</category>
      <category>블로그 스킨</category>
      <category>블로그 플랫폼</category>
      <category>티스토리</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/172</guid>
      <comments>https://readystory.tistory.com/172#entry172comment</comments>
      <pubDate>Mon, 13 Apr 2020 11:17:12 +0900</pubDate>
    </item>
    <item>
      <title>2020 - 신입 개발자의 1분기 회고 및 2분기 목표 설정</title>
      <link>https://readystory.tistory.com/171</link>
      <description>&lt;p&gt;어느덧 신입사원으로 입사한 지 1분기가 지나갔다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1월은 팀이 진행하는 프로젝트 분석 및 회사 적응, 2월은 신입 연수, 3월은 코로나로 인한 재택 근무로 사실상 회사에 있던 시간보다 회사에 없던 시간이 더 많았던 1분기였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코딩을 많이 하게 될 것이라는 기대와는 달리 프로덕션 코딩 이외의 것에 훨씬 시간을 많이 쏟게 되는 기간이었는데, 이를테면 테스트 코드 작성, 젠킨스와 도커를 활용한 배포 및 운영 등에 시간을 많이 할애했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;특히 Android Instrument Test 를 젠킨스 서버에 올려서 테스트 자동화를 진행해야 하는데, 서버가 VM 서버다 보니 하드웨어 가속화를 지원하지 않아 &lt;b&gt;x86 CPU의 에뮬레이터&lt;/b&gt;를 사용하지 못하고 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;x86보다 10배 이상 느린&lt;/span&gt; ARM CPU&lt;/b&gt; &lt;b&gt;에뮬레이터&lt;/b&gt;를 사용해야만 했고 여기서 발생하는 많은 문제들을 해결하는 데 굉장히 많은 삽질을 했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결과적으로는 열심히 삽질 한 덕분에 수월했더라면 몰랐을 부분까지도 깊이 있게 공부할 수 있었고, 그동안 안드로이드 스튜디오에서 자동으로 해주어 편하게 할 수 있었던 부분들을 직접 커맨드 라인으로 실행해보면서 몰랐던 프로세스들까지도 세세하게 바라보게 되었다. 특히 에뮬레이터와 ADB 부분을 많이 봤다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1분기는 일정도 바빴고, 앞서 말한 삽질(?) 덕분에 블로그 활동이나 자기계발에 많이 힘쓰지 못했는데 2분기는 조금 더 여유를 갖고 개발 서적도 좀 읽고 블로그도 열심히 하고 회사 일도 열심히 해야겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;목표는 항상 구체적인 것이 좋더랬지. 이렇게 글을 쓰는 이유도 말이나 생각만 하는 것이 아니라 기록을 남김으로써 목표를 분명히 하고 실천하고자 하기 위함이니까.. 현실 가능한 수준에서 목표를 적어보겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2분기 목표&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;1. 도서 [도커/쿠버네티스를 활용한 컨테이너 개발 실전 입문] 읽고 리뷰&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;2. 도서 [실용주의 프로그래머] 읽고 리뷰&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;3. &lt;a href=&quot;https://github.com/android/architecture-samples&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android Architecture 프로젝트&lt;/a&gt;를 통한 MVVM 소개 포스팅(시리즈로 진행 예정)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;4. 개인 프로젝트 완성(안드로이드 앱)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;5. 헬스장 등록하고 열심히 운동&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이건 여담이지만 작년 9월쯤부터 블로그를 열심히 쓰기 시작한 것 같은데 신기하게도 매 월 방문자 수가 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;우상향 그래프&lt;/span&gt;&lt;/b&gt;를 그리면서 증가한다. 아직 내로라 하는 블로거 분들에 비하면 미약하지만 꾸준하게 성장한다는 점과 누군가에게 도움이 되는 포스팅을 하겠다는 마음 하나로 힘을 내어 힘 닿는 데까지 블로깅을 할 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;P.S. 얼마전부터 1일 1커밋도 시작했다. 개인 프로젝트와 &lt;a href=&quot;https://github.com/KimReady/Leetcode-JVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;릿코드&lt;/a&gt;를 매일 조금씩 진행하면서 하루에 한 번이라도 꼭 코드를 작성하려고 한다. 2분기 회고 및 3분기 목표 설정 때까지 부디 빠짐없이 해내고 있기를 바랄 뿐이다..^^&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;fighting.jpg&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWYcyP/btqDcyyFTpl/84jLQRLfM3TuyVYWqH9530/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWYcyP/btqDcyyFTpl/84jLQRLfM3TuyVYWqH9530/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWYcyP/btqDcyyFTpl/84jLQRLfM3TuyVYWqH9530/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWYcyP%2FbtqDcyyFTpl%2F84jLQRLfM3TuyVYWqH9530%2Fimg.jpg&quot; data-filename=&quot;fighting.jpg&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;338&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Life</category>
      <category>1분기 회고</category>
      <category>2분기 목표</category>
      <category>개발자 목표</category>
      <category>개발자 회고</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/171</guid>
      <comments>https://readystory.tistory.com/171#entry171comment</comments>
      <pubDate>Sat, 4 Apr 2020 20:03:10 +0900</pubDate>
    </item>
    <item>
      <title>[Github] Key is invalid. You must supply a key in OpenSSH public key format. 문제 해결하기</title>
      <link>https://readystory.tistory.com/170</link>
      <description>&lt;p&gt;깃헙 저장소에 Push 가 되거나 PR 이 될 때마다 Jenkins(혹은 Travis 등)에서 빌드를 자동화 하고, 테스트를 자동화 하기 위해서는 깃헙에 해당 서버의 Deploy key 를 등록해줘야 하는데요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 과정은 굉장히 간단합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.github.com/v3/guides/managing-deploy-keys/#setup-2&quot;&gt;Github API 공식 가이드&lt;/a&gt;를 보고 하면 되는데, 이대로 적용하려고 보니 아래와 같은 에러 메세지를 띄우면서 실패했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-04-01 오후 12.35.18.png&quot; data-origin-width=&quot;2068&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdBYoM/btqC5oxaMIf/vUXGUiDdaRRqDvGVrlEDy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdBYoM/btqC5oxaMIf/vUXGUiDdaRRqDvGVrlEDy1/img.png&quot; data-alt=&quot;Deploy Key 등록 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdBYoM/btqC5oxaMIf/vUXGUiDdaRRqDvGVrlEDy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdBYoM%2FbtqC5oxaMIf%2FvUXGUiDdaRRqDvGVrlEDy1%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-04-01 오후 12.35.18.png&quot; data-origin-width=&quot;2068&quot; data-origin-height=&quot;694&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Deploy Key 등록 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;문서에서 하라는대로 했는데, 이게 왜 뜨는 걸까..? 하고 차근차근 처음부터 해봤더니 제가 다음과 같은 실수를 했더군요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보통 &lt;code&gt;ssh-keygen&lt;/code&gt; 명령으로 키를 생성하고 나면, id_rsa.pub 파일이 생성이 되는데 이 파일을 읽을 때 주의할 점은 &lt;b&gt;&lt;code&gt;.pub&lt;/code&gt; 를 반드시 포함하여 상태에서 파일을 읽거나 열어야 한다는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;왜냐하면 .pub 파일은 public key 이고, .pub 가 안붙은 파일은 private key 이기 때문이죠.&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;(public key는 github에, private key는 jenkins에 등록해줘야 합니다)&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;저는 아래와 같이 읽어온 값으로 Deploy Key 를 등록하려 했었는데&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;cat ~/.ssh/id_rsa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 읽으면 위 사진과 같은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&quot;Key is invalid. You must supply a key in OpenSSH public key format.&quot;&lt;/span&gt; 문제가 발생하게 되구요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정상 등록 하기 위해서는 .pub 를 붙여서 읽은 값을 등록해줘야 합니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 정상 등록된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-04-01 오후 12.48.54.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bai35w/btqC8IuoH28/9VvHo2RlIkKM8QaTnm0Ex0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bai35w/btqC8IuoH28/9VvHo2RlIkKM8QaTnm0Ex0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bai35w/btqC8IuoH28/9VvHo2RlIkKM8QaTnm0Ex0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbai35w%2FbtqC8IuoH28%2F9VvHo2RlIkKM8QaTnm0Ex0%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-04-01 오후 12.48.54.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;390&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <category>github deploy key</category>
      <category>github Key is invalid.</category>
      <category>jenkins github deploy key</category>
      <category>jenkins github key</category>
      <category>Key is invalid.</category>
      <category>Key is invalid. You must supply a key in OpenSSH public key format.</category>
      <category>젠킨스 깃헙 키</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/170</guid>
      <comments>https://readystory.tistory.com/170#entry170comment</comments>
      <pubDate>Wed, 1 Apr 2020 12:51:14 +0900</pubDate>
    </item>
    <item>
      <title>[Leetcode] 4번 - Median of Two Sorted Arrays (Java, Kotlin)</title>
      <link>https://readystory.tistory.com/169</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. &lt;span&gt;Median of Two Sorted Arrays (Hard)&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;There are two sorted arrays&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nums1&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nums2&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;of size m and n respectively.&lt;/p&gt;
&lt;p&gt;Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You may assume&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nums1&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nums2&lt;/b&gt;&amp;nbsp;cannot be both empty.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example 1:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;nums1 = [1, 3]&lt;/p&gt;
&lt;p&gt;nums2 = [2]&lt;/p&gt;
&lt;p&gt;The median is 2.0&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example 2:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;nums1 = [1, 2]&lt;/p&gt;
&lt;p&gt;nums2 = [3, 4]&lt;/p&gt;
&lt;p&gt;The median is (2 + 3)/2 = 2.5&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Java Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232934369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int fullLength = nums1.length + nums2.length;
        int midCount = 2 - (fullLength % 2);
        int cnt = fullLength &amp;gt;&amp;gt; 1;
        double mid = 0;

        int i = 0;
        int j = 0;

        while (cnt &amp;gt;= 0) {
            int tmpMid;

            if (i &amp;lt; nums1.length &amp;amp;&amp;amp; j &amp;lt; nums2.length) {
                tmpMid = (nums1[i] &amp;gt; nums2[j]) ? nums2[j++] : nums1[i++];
            } else {
                tmpMid = (i &amp;gt;= nums1.length) ? nums2[j++] : nums1[i++];
            }

            if (cnt &amp;lt; midCount) {
                mid += tmpMid;
            }
            cnt--;
        }

        return mid / midCount;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Kotlin Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232949953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
        val fullLength = nums1.size + nums2.size
        val midCount = 2 - fullLength % 2
        var cnt = fullLength shr 1

        var mid = 0.0
        var i = 0
        var j = 0

        while (cnt &amp;gt;= 0) {
            val tmpMid: Int =
                if (i &amp;lt; nums1.size &amp;amp;&amp;amp; j &amp;lt; nums2.size) {
                    if (nums1[i] &amp;gt; nums2[j]) nums2[j++] else nums1[i++]
                } else {
                    if (i &amp;gt;= nums1.size) nums2[j++] else nums1[i++]
                }

            if (cnt &amp;lt; midCount) {
                mid += tmpMid.toDouble()
            }
            cnt--
        }
        return mid / midCount
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 외에도 다양한 문제들의 해답 코드를 &lt;a href=&quot;https://github.com/KimReady/Leetcode-JVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙 저장소&lt;/a&gt;에서 확인할 수 있습니다. &lt;b&gt;(Java, Kotlin)&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>leetcode</category>
      <category>leetcode soultion</category>
      <category>Median of Two Sorted Arrays</category>
      <category>Median of Two Sorted Arrays java</category>
      <category>Median of Two Sorted Arrays kotlin</category>
      <category>릿코드 4번</category>
      <category>릿코드 4번 java</category>
      <category>릿코드 4번 kotlin</category>
      <category>릿코드 4번 정답</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/169</guid>
      <comments>https://readystory.tistory.com/169#entry169comment</comments>
      <pubDate>Sun, 29 Mar 2020 18:29:52 +0900</pubDate>
    </item>
    <item>
      <title>[Leetcode] 3번 - Longest Substring Without Repeating Characters (Java, Kotlin)</title>
      <link>https://readystory.tistory.com/168</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Longest Substring Without Repeating Characters (Medium)&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Given a string, find the length of the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;longest substring&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;without repeating characters.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example 1:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Input: &lt;/b&gt;&lt;span&gt;&quot;abcabcbb&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Output: &lt;/b&gt;&lt;span&gt;3 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;b&gt;Explanation:&lt;/b&gt;&lt;/span&gt; The answer is &quot;abc&quot;, with the length of 3.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example 2:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Input: &lt;/b&gt;&lt;span&gt;&quot;bbbbb&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Output: &lt;/b&gt;&lt;span&gt;1&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;Explanation: &lt;/b&gt;T&lt;/span&gt;he answer is &quot;b&quot;, with the length of 1.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example 3:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Input: &lt;/b&gt;&lt;span&gt;&quot;pwwkew&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Output: &lt;/b&gt;&lt;span&gt;3&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;Explanation: &lt;/b&gt;&lt;/span&gt;The answer is &quot;wke&quot;, with the length of 3.&lt;/p&gt;
&lt;p&gt;Note that the answer must be a &lt;b&gt;substring&lt;/b&gt;, &quot;pwke&quot; is a &lt;i&gt;subsequence&lt;/i&gt; and not a substring.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span&gt;Java Solution&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232587478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Set set = new HashSet();
        int n = s.length();
        int begin = 0, end = 0;
        int ans = 0;

        while(end &amp;lt; n) {
            if(set.contains(s.charAt(end))) {
                set.remove(s.charAt(begin++));
            } else {
                set.add(s.charAt(end++));
                ans = Math.max(ans, end - begin);
            }
        }
        return ans;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span&gt;Kotlin Solution&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232602170&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    fun lengthOfLongestSubstring(s: String): Int {
        val set = hashSetOf&amp;lt;Char&amp;gt;()
        val n = s.length
        var begin = 0
        var end = 0
        var answer = 0

        while (end &amp;lt; n) {
            if (s[end] in set) {
                set.remove(s[begin++])
            } else {
                set.add(s[end++])
                answer = Integer.max(answer, end - begin)
            }
        }
        return answer
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 외에도 다양한 문제들의 해답 코드를 &lt;a href=&quot;https://github.com/KimReady/Leetcode-JVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙 저장소&lt;/a&gt;에서 확인할 수 있습니다. &lt;b&gt;(Java, Kotlin)&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>leetcode</category>
      <category>leetcode 3 kotlin</category>
      <category>leetcode 3번</category>
      <category>leetcode solution</category>
      <category>Longest Substring Without Repeating Characters</category>
      <category>Longest Substring Without Repeating Characters java</category>
      <category>Longest Substring Without Repeating Characters kotlin</category>
      <category>릿코드</category>
      <category>릿코드 3번 java</category>
      <category>릿코드 3번 kotlin</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/168</guid>
      <comments>https://readystory.tistory.com/168#entry168comment</comments>
      <pubDate>Sat, 28 Mar 2020 18:24:17 +0900</pubDate>
    </item>
    <item>
      <title>[Leetcode] 2번 - Add Two Numbers (Java, Kotlin)</title>
      <link>https://readystory.tistory.com/167</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Add Two Numbers (Medium)&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You are given two&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;non-empty&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;linked lists representing two non-negative integers. The digits are stored in&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;reverse order&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You may assume the two numbers do not contain any leading zero, except the number 0 itself.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Input:&lt;/b&gt; (2 -&amp;gt; 4 -&amp;gt; 3) + (5 -&amp;gt; 6 -&amp;gt; 4)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Output:&lt;/b&gt; 7 -&amp;gt; 0 -&amp;gt; 8&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Explanation:&lt;/b&gt; 342 + 465 = 807.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Java Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232324776&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode answer = new ListNode(l1.val);
        ListNode temp = answer;

        while(l1.next != null) {
            temp.next = new ListNode(l1.next.val);
            l1 = l1.next;
            temp = temp.next;
        }
        
        int p = 0;
        temp = answer;
        while(l2 != null || p == 1) {
            int sum = l2 != null ? temp.val + l2.val : temp.val;
            p = sum &amp;gt;= 10 ? 1 : 0;
            temp.val = sum % 10;

            if(p == 1) {
                if(temp.next != null) {
                    temp.next.val += p;
                } else {
                    temp.next = new ListNode(p);
                }
            }
            l2 = l2 != null ? l2.next : null;
            temp.next = temp.next == null ? (l2 != null ? new ListNode(0) : null) : temp.next;
            temp = temp.next;
        }

        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Kotlin Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232348998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Example:
 * var li = ListNode(5)
 * var v = li.`val`
 * Definition for singly-linked list.
 * class ListNode(var `val`: Int) {
 *     var next: ListNode? = null
 * }
 */

class Solution {
    fun addTwoNumbers(l1: ListNode?, l2: ListNode?): ListNode? {
        var tmp1: ListNode? = l1
        var tmp2: ListNode? = l2
        var carry: Int = 0

        val ans = ListNode(-1)
        var tmpAns = ans

        while(tmp1 != null || tmp2 != null || carry &amp;gt; 0) {
            var cal = 0
            tmp1?.let {
                cal += it.`val`
                tmp1 = it.next
            }
            tmp2?.let {
                cal += it.`val`
                tmp2 = it.next
            }
            
            cal += carry
            if(cal &amp;gt;= 10) {
                carry = 1
                cal %= 10
            } else {
                carry = 0
            }

            tmpAns.next = ListNode(cal)
            tmpAns = tmpAns.next!!
        }


        return ans.next
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 외에도 다양한 문제들의 해답 코드를 &lt;a href=&quot;https://github.com/KimReady/Leetcode-JVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙 저장소&lt;/a&gt;에서 확인할 수 있습니다. &lt;b&gt;(Java, Kotlin)&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>leetcode</category>
      <category>leetcode add two numbers</category>
      <category>leetcode add two numbers java</category>
      <category>leetcode add two numbers kotlin</category>
      <category>leetcode solution</category>
      <category>릿코드 2번</category>
      <category>릿코드 2번 자바</category>
      <category>릿코드 2번 코틀린</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/167</guid>
      <comments>https://readystory.tistory.com/167#entry167comment</comments>
      <pubDate>Fri, 27 Mar 2020 23:20:14 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode] 1번 - Two Sum (Java, Kotlin)</title>
      <link>https://readystory.tistory.com/166</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Two Sum (easy)&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Given an array of integers, return&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;indices&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;of the two numbers such that they add up to a specific target.&lt;/p&gt;
&lt;p&gt;You may assume that each input would have&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;exactly&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;one solution, and you may not use the&lt;span&gt;&amp;nbsp;&lt;/span&gt;same&lt;span&gt;&amp;nbsp;&lt;/span&gt;element twice.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Example:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;Given nums = [2, 7, 11, 15],&lt;/p&gt;
&lt;p&gt;target = 9,&lt;/p&gt;
&lt;p&gt;Because nums[&lt;b&gt;0&lt;/b&gt;] + nums[&lt;b&gt;1&lt;/b&gt;] = 2 + 7 = 9,&lt;/p&gt;
&lt;p&gt;return [&lt;b&gt;0&lt;/b&gt;, &lt;b&gt;1&lt;/b&gt;].&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Java Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232064435&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map&amp;lt;Integer, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; nums.length; i++) {
            if (map.containsKey(nums[i])) {
                return new int[] {map.get(nums[i]), i};
            }
            map.put(target - nums[i], i);
        }
        return new int[]{};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Kotlin Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1585232080103&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    fun twoSum(nums: IntArray, target: Int): IntArray {
        val visited: MutableMap&amp;lt;Int, Int&amp;gt; = mutableMapOf()

        nums.forEachIndexed { i, num -&amp;gt;
            if (visited.containsKey(num)) {
                return@twoSum intArrayOf(visited[num]!!, i)
            }
            visited[target - num] = i
        }

        return intArrayOf()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 외에도 다양한 문제들의 해답 코드를 &lt;a href=&quot;https://github.com/KimReady/Leetcode-JVM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙 저장소&lt;/a&gt;에서 확인할 수 있습니다. &lt;b&gt;(Java, Kotlin)&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>leetcode</category>
      <category>leetcode two sum</category>
      <category>two sum java</category>
      <category>two sum kotlin</category>
      <category>릿코드</category>
      <category>릿코드 1번</category>
      <category>릿코드 two sum</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/166</guid>
      <comments>https://readystory.tistory.com/166#entry166comment</comments>
      <pubDate>Thu, 26 Mar 2020 23:16:23 +0900</pubDate>
    </item>
    <item>
      <title>RxJava 리액티브 프로그래밍 책 후기</title>
      <link>https://readystory.tistory.com/164</link>
      <description>&lt;table class=&quot;tt-plugin-interpark&quot; style=&quot;background: #fff; border: 1px solid #e0e0e0; width: 408px;&quot; border=&quot;0&quot; width=&quot;100%&quot; cellspacing=&quot;0&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 10px 0 10px 10px;&quot; width=&quot;70&quot;&gt;&lt;a href=&quot;http://book.interpark.com/blog/integration/product/itemDetail.rdo?prdNo=303812308&amp;amp;refererType=8303&amp;amp;bookblockname=bpmain_in&amp;amp;booklinkname=wg_search_60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&amp;amp;key=60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img style=&quot;border: 0 none;&quot; src=&quot;http://bimage.interpark.com/goods_image/2/3/0/8/303812308h.jpg&quot; width=&quot;66&quot; height=&quot;90&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 10px 10px 10px 15px;&quot;&gt;
&lt;dl style=&quot;margin: 0; padding: 2px 0 0 0; list-style: none; font: 11px dotum, sans-serif; letter-spacing: -1px; color: #777;&quot;&gt;
&lt;dt style=&quot;padding: 0; margin: 0;&quot;&gt;&lt;a style=&quot;font-size: 12px; color: #444 !important; font-weight: bold; text-decoration: none !important;&quot; href=&quot;http://book.interpark.com/blog/integration/product/itemDetail.rdo?prdNo=303812308&amp;amp;refererType=8303&amp;amp;bookblockname=bpmain_in&amp;amp;booklinkname=wg_search_60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&amp;amp;key=60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RxJava 리액티브 프로그래밍&lt;/a&gt;&lt;/dt&gt;
&lt;dd style=&quot;padding: 7px 0 0 0; margin: 0; color: #a0a0a0;&quot;&gt;국내도서&lt;/dd&gt;
&lt;dd style=&quot;padding: 17px 0 0 0; margin: 0;&quot;&gt;저자 : 스다 토모유키 / 이승룡역&lt;/dd&gt;
&lt;dd style=&quot;padding: 4px 0 0 0; margin: 0;&quot;&gt;출판 : 길벗 &lt;span style=&quot;letter-spacing: normal;&quot;&gt;2019.04.10&lt;/span&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;a style=&quot;float: right; width: 44px; height: 11px; background: url('//t1.daumcdn.net/tistory_admin/static/images/icon_ipark_detail.gif') no-repeat; overflow: hidden; display: block; text-indent: -1000em;&quot; href=&quot;http://book.interpark.com/blog/integration/product/itemDetail.rdo?prdNo=303812308&amp;amp;refererType=8303&amp;amp;bookblockname=bpmain_in&amp;amp;booklinkname=wg_search_60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&amp;amp;key=60A4953D4AF6B452E4B39ABAE304F011097546653C951F369CCB576F43A6DA54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;상세보기&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RxJava, RxKotlin, RxJS, RxAndroid 등 Rx는 다양한 언어로 제공되고 있는 프로그래밍 패러다임이다. Rx 가 리액티브 프로그래밍의 결이라는 것은 알고 있었지만 왜 Rx 인지에 대해서는 알지 못했는데 이 책에서는 서두에 그 이유를 서술한다. Rx는 Reactive eXtensions 의 약자이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Rx 진영의 간략한 역사와 함께 어떤 특징들을 갖고 있으며 RxJava를 학습하기 위해서는 어떤 개념을 알아야 하는지도 간단하게 설명해주기 때문에 &lt;b&gt;입문자가 보기에는 더없이 좋은 책&lt;/b&gt;이라 생각이 든다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;이 책의 장점은 상세한 마블 다이어그램에 있다.&lt;/b&gt; 물론 이는 RxJava의 document에도 어느 정도 잘 되어있지만, 이 책은 공식문서보다도 더 최신화되고 때로는 더 자세한 다이어그램이 포함되어 있기 때문에 이해하는 데 큰 도움이 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러나 장점만 있는 것은 아니다. &lt;b&gt;책이 단조롭다는 단점이 있다.&lt;/b&gt; RxJava에 포함되어 있는 클래스나 함수 등에 대해서 설명하고 있기 때문에 응용 면에서 예제가 많이 부족하고 단조롭다. 따라서 이 책을 통해서 어떤 함수가 있고, 어떤 기능을 하는지는 파악할 수 있지만 이를 활용해서 어떤 작업들을 할 수 있을 지에 대해서는 추가적으로 다른 책을 보거나 예제를 직접 찾아보는 수 밖에 없다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;총평을 하자면,&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt; RxJava를 처음 접하는 사람에게는 좋은 책&lt;/b&gt;&lt;/span&gt;이지만 RxJava를 어느정도 알고 있는 사람이 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;좀 더 깊은 학습과 경험을 위해 구매하기에는 맞지 않는 책&lt;/b&gt;&lt;/span&gt;인 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;참고로 현재 RxJava의 stable 버전이 3.x 버전으로 올라왔다.&lt;/p&gt;
&lt;p&gt;하지만 이 책은 2.x 버전에 대해 다루고 있기 때문에 최신 버전의 RxJava3 를 배우고자 하는 이에게는 적합하지 않을 수 있다.&lt;/p&gt;</description>
      <category>Book</category>
      <category>Rx 책 추천</category>
      <category>RxJava</category>
      <category>RxJava 리액티브 프로그래밍</category>
      <category>RxJava 입문</category>
      <category>RxJava 책</category>
      <category>RxJava 책 추천</category>
      <category>RxJava 초보</category>
      <category>리액티브 프로그래밍 책 추천</category>
      <author>Ready Kim</author>
      <guid isPermaLink="true">https://readystory.tistory.com/164</guid>
      <comments>https://readystory.tistory.com/164#entry164comment</comments>
      <pubDate>Sat, 21 Mar 2020 17:32:38 +0900</pubDate>
    </item>
  </channel>
</rss>