<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://kmshack.kr/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kmshack.kr/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-08-21T00:11:15+09:00</updated><id>https://kmshack.kr/blog/feed.xml</id><title type="html">MINSOO KIM — Android Engineer</title><subtitle>Minsoo Kim의 Android 개발, UI/UX, 생산성과 제품 개발 기록</subtitle><author><name>Minsoo Kim</name></author><entry><title type="html">AI 시대에 개발자는 무엇을 만들어야 할까?</title><link href="https://kmshack.kr/blog/ai-era-what-developers-should-build/" rel="alternate" type="text/html" title="AI 시대에 개발자는 무엇을 만들어야 할까?" /><published>2026-08-20T00:00:00+09:00</published><updated>2026-08-20T00:00:00+09:00</updated><id>https://kmshack.kr/blog/ai-era-what-developers-should-build</id><content type="html" xml:base="https://kmshack.kr/blog/ai-era-what-developers-should-build/"><![CDATA[<p>AI에게 요구사항을 설명하면 간단한 화면과 API 초안을 몇 분 안에 확인할 수 있습니다. 테스트 코드와 문서도 함께 요청할 수 있고, 아이디어를 실제로 눌러 볼 수 있는 프로토타입으로 바꾸는 비용은 계속 낮아지고 있습니다.</p>

<p>개발자에게는 분명 좋은 변화입니다. 그러나 만들 수 있는 후보는 급증했지만, 무엇을 만들어야 하는지는 더 선명해지지 않았습니다. 기능을 빠르게 추가하는 능력만으로는 이 질문에 답할 수 없습니다.</p>

<p>이제 더 중요해진 질문은 이것입니다.</p>

<p><strong>이 제품은 누구의 어떤 일을 실제로 끝내 주는가?</strong></p>

<p><br /></p>

<blockquote>
  <p>AI 시대의 개발자는 기능을 쌓기보다, 사람이 현실에서 하려던 일을 마치게 하는 도구를 만들어야 합니다.</p>
</blockquote>

<p><br /></p>

<h2 id="핵심-요약">핵심 요약</h2>

<ul>
  <li>구현 비용이 낮아질수록 풀 가치가 있는 문제를 고르는 판단이 더 중요해집니다.</li>
  <li>좋은 제품의 단위는 기능이 아니라 사용자가 실제로 마친 일입니다.</li>
  <li>AI 기능에는 확인, 수정, 복구, 대체 수단이 함께 설계되어야 합니다.</li>
</ul>

<p><br /></p>

<h2 id="1-구현이-쉬워질수록-문제-선택이-중요해집니다">1. 구현이 쉬워질수록 문제 선택이 중요해집니다</h2>

<p>과거에는 아이디어가 있어도 구현에 드는 시간과 비용 때문에 시작하지 못하는 경우가 많았습니다. 지금은 AI의 도움으로 초기 프로토타입을 훨씬 짧은 시간 안에 만들 수 있습니다. 문제는 사용자가 필요로 하는 제품까지 자동으로 만들어지는 것은 아니라는 데 있습니다.</p>

<p>기능의 개수는 제품의 가치를 증명하지 못합니다.</p>

<ul>
  <li>알림을 요약하는 기능보다 중요한 것은 사용자가 납부일을 놓치지 않는 것입니다.</li>
  <li>버스 도착 숫자를 보여 주는 기능보다 중요한 것은 출발 시각과 승차 방향을 결정하도록 돕는 것입니다.</li>
  <li>옷을 추천하는 기능보다 중요한 것은 가지고 있는 옷만으로 아침의 선택 시간을 줄여 주는 것입니다.</li>
</ul>

<p>요약, 예측, 추천은 모두 중간 과정입니다. 사용자가 그 결과를 바탕으로 다음 행동을 하고 원래 하려던 일을 마쳐야 제품의 가치가 생깁니다.</p>

<p>그래서 제품 아이디어는 기술 이름이 아니라 사용자의 변화로 설명할 수 있어야 합니다.</p>

<blockquote>
  <p>이 제품은 <code class="language-plaintext highlighter-rouge">[어떤 사람]</code>이 <code class="language-plaintext highlighter-rouge">[특정한 상황]</code>에서 <code class="language-plaintext highlighter-rouge">[하던 일]</code>을 <code class="language-plaintext highlighter-rouge">[더 나은 방식]</code>으로 마치게 돕습니다.</p>
</blockquote>

<p>이 문장을 구체적으로 채우기 어렵다면 아직 제품보다 기술 데모에 가까울 가능성이 큽니다. AI 모델을 바꾸거나 기능을 더하기 전에 누구의 어떤 순간을 바꾸려는지부터 좁혀야 합니다.</p>

<p><br /></p>

<h2 id="2-만들-것은-사람들의-임시방편-속에-있습니다">2. 만들 것은 사람들의 임시방편 속에 있습니다</h2>

<p>좋은 문제는 회의실의 아이디어 목록보다 사람들이 이미 하고 있는 불편한 행동에서 발견되는 경우가 많습니다.</p>

<p>같은 내용을 여러 앱에 복사하고, 중요한 화면을 캡처하고, 잊지 않으려고 종이에 적고, 자동화 결과가 불안해 원문을 다시 확인하는 행동이 반복된다면 아직 끝나지 않은 일이 있다는 뜻입니다. 이런 임시방편은 풀 가치가 있는 문제라는 신호입니다.</p>

<p>예를 들어 작은 가게에서 문자로 받은 주문을 엑셀로 옮기는 상황을 생각해 볼 수 있습니다. AI로 주문 문장을 표로 바꾸는 초기 프로토타입은 비교적 빠르게 만들 수 있습니다. 그러나 실제 업무에는 빠진 상품 옵션, 중복 주문, 재고 부족, 주소 확인 같은 예외가 뒤따릅니다.</p>

<p>문장을 표로 바꾸는 데서 멈추면 입력 시간은 줄어도 검수 부담은 커질 수 있습니다. 진짜 제품은 예외를 찾고, 필요한 확인을 요청하고, 검증된 주문을 배송 시스템으로 전달하는 과정까지 이어져야 합니다.</p>

<p>초기에는 사용자가 남긴 수정 기록과 중단 지점만 살펴봐도 됩니다. 자동화를 멈추고 다시 수작업으로 돌아가는 순간이 제품이 놓친 단계를 보여 줍니다. 다음 질문을 따라가면 기능의 우선순위가 드러납니다.</p>

<ol>
  <li>이 일을 시작하기 직전에 무슨 일이 있었는가?</li>
  <li>결과를 받은 뒤 사용자는 어디로 이동하는가?</li>
  <li>잘못됐을 때 누가 어떤 비용을 감당하는가?</li>
</ol>

<p><strong>제품이 켜져 있는 3분이 아니라 사용자의 하루 안에서 제품이 맡은 역할을 봐야 합니다.</strong></p>

<p><br /></p>

<h2 id="3-ai의-답을-완료-경로로-바꿉니다">3. AI의 답을 완료 경로로 바꿉니다</h2>

<p>AI를 넣었다는 사실 자체는 제품의 목적이 될 수 없습니다. 생성형 AI는 정리되지 않은 문장, 이미지, 음성 같은 비정형 입력을 구조화할 때 유용할 수 있습니다. 반면 날짜 계산, 결제, 권한, 데이터 보존처럼 결과가 분명해야 하는 일은 규칙 기반 로직이 재현과 검증에 더 적합할 수 있습니다.</p>

<p>두 영역을 구분하면 AI는 앞에 드러나는 주인공이 아니라 사용자의 흐름을 돕는 부품이 됩니다.</p>

<p>학교에서 보낸 긴 안내문을 처리하는 서비스를 예로 들어 보겠습니다. 단순한 요약만으로는 준비물을 챙기거나 신청 기한을 지키기 어렵습니다. 실제로 일을 마치려면 다음 과정이 이어져야 합니다.</p>

<ol>
  <li>준비물, 납부일, 제출 항목을 구분합니다.</li>
  <li>날짜와 대상이 불확실하면 확인이 필요하다고 표시합니다.</li>
  <li>원문을 바로 열어 보고 잘못 추출된 내용을 고칠 수 있게 합니다.</li>
  <li>사용자가 확인한 일정만 캘린더나 알림으로 보냅니다.</li>
</ol>

<p>여기서 AI의 답변은 완성품이 아니라 다음 행동을 만들기 위한 재료입니다. 제품의 가치는 요약문이 얼마나 자연스러운지가 아니라 사용자가 준비물을 챙기고 마감일을 지켰는지로 판단해야 합니다.</p>

<p>범용 생성 기능을 하나 더 만드는 것보다 <strong>특정한 사람이 반복하는 일을 처음부터 끝까지 연결하는 작고 깊은 제품</strong>에 개인 개발자가 집중해 볼 만합니다.</p>

<p><br /></p>

<h2 id="4-실패해도-빠져나올-수-있게-만듭니다">4. 실패해도 빠져나올 수 있게 만듭니다</h2>

<p>규칙 기반 로직은 결과를 재현하고 검증하기 상대적으로 쉽습니다. 반면 AI의 결과에는 불확실성이 남습니다. 그럴듯하지만 틀린 답이 나올 수 있고, 중요한 항목을 빠뜨리거나 사용자가 기대하지 않은 방식으로 데이터를 해석할 수도 있습니다.</p>

<p>따라서 AI 기능을 만드는 일에는 성공 화면뿐 아니라 실패했을 때 빠져나오는 길을 만드는 일도 포함되어야 합니다.</p>

<ul>
  <li><strong>알아차리기:</strong> 확실하지 않은 결과를 사실처럼 보여 주지 않고, 원본과 생성 결과를 비교할 수 있게 합니다.</li>
  <li><strong>수정하고 되돌리기:</strong> 잘못 분류하거나 변경한 결과를 직접 고치고 이전 상태로 복구할 수 있게 합니다.</li>
  <li><strong>다른 경로 제공하기:</strong> 자동화가 처리하지 못한 경우 이를 명확히 알리고 수동 처리처럼 이용 가능한 방법을 보여 줍니다.</li>
  <li><strong>운영 중 관찰하기:</strong> 오류 로그, 사용자 수정 기록, 중단 지점을 확인하되 개인정보는 필요한 만큼만 수집합니다.</li>
</ul>

<p>알림 자동 분류가 납부 기한이나 예약 변경 같은 메시지를 중요하지 않다고 숨기면 사용자는 놓친 사실조차 알기 어렵습니다. 확신이 낮은 항목은 별도 목록에 남기고, 자동 분류를 끄거나 원래 보기로 돌아갈 수 있어야 합니다.</p>

<p>기술 역량의 초점도 달라집니다. 입력과 출력의 계약, 회귀 테스트, 권한 경계, 배포와 롤백을 설계해 생성 결과의 영향 범위를 통제해야 합니다. <strong>빠르게 만드는 능력과 믿고 쓸 수 있게 만드는 능력은 서로 다른 역량입니다.</strong></p>

<p><br /></p>

<h2 id="5-개인-개발자를-위한-선택-체크리스트">5. 개인 개발자를 위한 선택 체크리스트</h2>

<p>개인 개발자가 모든 사람을 위한 거대한 서비스를 만들 필요는 없습니다. 오히려 좁은 문제를 깊게 해결하는 편이 운영과 차별화에 유리합니다. 아이디어를 고를 때는 다음 다섯 가지를 확인해 볼 수 있습니다.</p>

<ol>
  <li><strong>반복되는가?</strong> 한 번의 불편보다 매일 또는 매주 되풀이되는 문제를 우선 살펴봅니다. 사용자는 지금 어떤 임시방편을 반복하고 있는가?</li>
  <li><strong>일이 실제로 끝나는가?</strong> 사용 시간보다 놓친 알림, 입력 시간, 잘못된 주문처럼 바뀐 결과를 확인할 수 있어야 합니다. 제품 이후에 완료되어야 하는 행동은 무엇인가?</li>
  <li><strong>맥락이 차이를 만드는가?</strong> 범용 챗봇만으로 안정적으로 해결하기 어려운 직업, 지역, 기기, 생활 방식의 예외가 있는가? AI 없이 규칙이나 화면 개선만으로 더 잘 풀 수 있는지도 함께 묻습니다.</li>
  <li><strong>실패에서 회복할 수 있는가?</strong> AI가 틀렸을 때 사용자가 알아차리고 되돌릴 수 있는가? 한 사람의 시간을 줄이는 대신 다른 사람에게 검수 작업을 떠넘기지는 않는가?</li>
  <li><strong>계속 운영할 수 있는가?</strong> 출시 후 결과를 관찰하고, 문의에 답하고, 잘못된 자동화를 고칠 수 있는 범위인가? 감당하기 어려운 의료, 법률, 금융 판단은 작은 팀이 섣불리 자동화해서는 안 됩니다.</li>
</ol>

<p>답이 모호하다면 프롬프트를 다듬기 전에 문제를 더 작게 나누는 편이 좋습니다. 답이 구체적이라면 AI로 여러 가설을 작게 검증하고, 실제 사용에서 확인된 흐름만 제품으로 남길 수 있습니다.</p>

<p>낮아진 제작 비용은 기능을 더 쌓는 데만 쓰지 않고, <strong>무엇을 만들지 않는 편이 나은지 확인하는 여유</strong>로도 쓸 수 있습니다.</p>

<p><br /></p>

<h2 id="사람이-안심하고-일을-마치게-하는-것">사람이 안심하고 일을 마치게 하는 것</h2>

<p>AI 시대에 개발자가 만들어야 할 것은 반드시 AI 제품일 필요가 없습니다. 누군가의 반복 작업을 줄이는 작은 자동화일 수도 있고, 중요한 정보를 놓치지 않게 돕는 알림일 수도 있으며, 복잡한 절차에서 다음 선택을 분명하게 보여 주는 화면일 수도 있습니다.</p>

<p>중요한 것은 기능 이후에 일어나는 변화입니다. 사용자가 시간을 되찾고, 불안해서 다시 확인하는 횟수가 줄고, 문제가 생겨도 이전 상태로 돌아올 수 있어야 합니다.</p>

<p>코드는 앞으로 더 쉽게 만들어질 것입니다. <strong>AI가 결과물을 만들수록 개발자는 그 결과가 안전하게 행동으로 이어지는 경로를 설계해야 합니다.</strong></p>]]></content><author><name>Minsoo Kim</name></author><category term="AI" /><category term="개발자" /><category term="제품 개발" /><summary type="html"><![CDATA[구현 비용이 낮아진 AI 시대에 개발자가 어떤 문제를 고르고 무엇을 만들어야 하는지, 맥락과 완결성, 신뢰의 관점에서 살펴봅니다.]]></summary></entry><entry><title type="html">AI 시대, 당신의 통장 잔고가 그대로인 진짜 이유</title><link href="https://kmshack.kr/blog/ai-leverage/" rel="alternate" type="text/html" title="AI 시대, 당신의 통장 잔고가 그대로인 진짜 이유" /><published>2026-07-31T00:00:00+09:00</published><updated>2026-07-31T00:00:00+09:00</updated><id>https://kmshack.kr/blog/ai-leverage</id><content type="html" xml:base="https://kmshack.kr/blog/ai-leverage/"><![CDATA[<p>AI가 비즈니스의 모든 것을 바꿀 것처럼 이야기됩니다. 자료를 정리하고, 문서를 만들고, 프로그램을 작성하는 속도는 이미 눈에 띄게 빨라졌습니다. 예전에는 하루가 걸리던 일도 이제 몇 시간이면 끝낼 수 있습니다.</p>

<p>그런데 이상한 점이 있습니다. <strong>처리하는 일은 많아졌는데 매출은 기대만큼 늘어나지 않습니다.</strong> 생산성이 높아졌다면 사업의 결과도 함께 좋아져야 할 것 같지만, 현실은 꼭 그렇지 않습니다.</p>

<p>사업가 알렉스 호르모지가 이야기한 레버리지의 관점에서 보면 이유는 단순합니다. AI는 일을 빠르게 만들어 주지만, 어떤 일을 해야 하는지까지 결정해 주지는 않기 때문입니다.</p>

<p><br />
<img src="/blog/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" alt="AI로 빨라진 업무가 병목을 지나 사업 성과로 연결되는 모습" class="center-image" />
<br /></p>

<blockquote>
  <p>AI가 속도를 높여도 사업의 병목이 그대로라면 결과는 달라지지 않습니다.</p>
</blockquote>

<p><br /></p>

<h2 id="핵심-요약">핵심 요약</h2>

<ul>
  <li>AI는 일을 빠르게 처리하지만, 어떤 일을 해야 하는지 결정하지는 못합니다.</li>
  <li>매출을 높이려면 도구보다 먼저 판매와 서비스 제공 구조를 점검해야 합니다.</li>
  <li>가장 강력한 레버리지는 중요하지 않은 일을 멈추고 하나의 병목에 집중하는 의사결정입니다.</li>
</ul>

<p><br /></p>

<h2 id="1-생산성이-높아졌는데-왜-매출은-그대로일까">1. 생산성이 높아졌는데 왜 매출은 그대로일까?</h2>

<p>레버리지란 <strong>적은 노력으로 더 큰 결과를 만들어 내는 힘</strong>을 뜻합니다. 그런 의미에서 AI는 분명 강력한 레버리지입니다. 반복 업무를 줄이고, 자료 조사와 초안 작성에 필요한 시간을 크게 단축해 줍니다.</p>

<p>문제는 남는 시간을 어디에 사용하느냐입니다.</p>

<p>업무 처리 능력이 늘어나면 예전에는 시간과 인력이 부족해 포기했던 일까지 다시 시작하게 됩니다. 매출에 직접적인 영향을 주지 않는 보고서를 더 만들거나, 고객이 요청하지 않은 기능을 추가하고, 중요하지 않은 회의를 더 자주 여는 식입니다.</p>

<p>중요하지 않은 일을 아무리 빠르게 처리해도 사업의 결과는 달라지지 않습니다. 오히려 해야 할 일이 많아졌다는 착각만 만들 수 있습니다.</p>

<p><strong>AI는 속도를 높여 주는 도구이지, 방향을 정해 주는 도구가 아닙니다.</strong> 잘못된 방향으로 달리고 있다면 더 빠른 속도는 목적지에서 더 멀어지게 만들 뿐입니다.</p>

<p><br /></p>

<h2 id="2-기술-없이도-수익을-높이는-구조의-힘">2. 기술 없이도 수익을 높이는 구조의 힘</h2>

<p>사업의 효율을 높이기 위해 반드시 새로운 기술이 필요한 것은 아닙니다. 때로는 판매 구조나 서비스를 제공하는 방식만 바꿔도 훨씬 큰 레버리지가 만들어집니다.</p>

<h3 id="11에서-그룹-방식으로-바꾸기">1:1에서 그룹 방식으로 바꾸기</h3>

<p>한 시간 동안 한 명을 상담하던 사람이 같은 시간에 10명을 상담할 수 있다면, 단순 계산만으로 제공 효율은 10배 가까이 높아질 수 있습니다. 새로운 프로그램을 개발하지 않아도 서비스 구조를 바꾸는 것만으로 시간의 한계를 줄일 수 있습니다.</p>

<h3 id="비동기-방식-도입하기">비동기 방식 도입하기</h3>

<p>모든 상담을 정해진 시간에 실시간으로 진행할 필요는 없습니다. 고객이 질문을 남기고 담당자가 가능한 시간에 답변하는 방식을 도입하면 일정의 제약이 줄어듭니다. 고객은 기다리는 시간을 줄이고, 담당자는 비슷한 질문을 모아 더 효율적으로 처리할 수 있습니다.</p>

<h3 id="영업-전에-충분한-정보-제공하기">영업 전에 충분한 정보 제공하기</h3>

<p>가격, 서비스 방식, 자주 묻는 질문을 상담 전에 충분히 전달하면 영업 과정도 달라집니다. 고객은 자신에게 필요한 서비스인지 미리 판단할 수 있고, 영업 담당자는 구매 의사가 높은 고객과 더 깊은 대화를 나눌 수 있습니다.</p>

<p>이처럼 레버리지는 새로운 도구를 도입하는 것뿐만 아니라 <strong>기존의 일을 어떤 구조로 제공하느냐</strong>에 따라 달라집니다.</p>

<p><br /></p>

<h2 id="3-가장-강력한-레버리지는-의사결정입니다">3. 가장 강력한 레버리지는 의사결정입니다</h2>

<p>자동화보다 더 큰 효과를 만드는 것은 <strong>중요하지 않은 일을 하지 않기로 결정하는 것</strong>입니다.</p>

<p>잘못된 결정을 내린 상태에서 AI를 사용하면 실수도 더 빠르게 확대됩니다. 필요 없는 기능을 더 빨리 만들고, 효과 없는 광고 문구를 더 많이 생산하고, 읽히지 않는 보고서를 더 정교하게 작성하게 됩니다.</p>

<p>사업가에게 필요한 능력은 더 많은 일을 벌이는 것이 아닙니다. 지금 하지 않아도 되는 일을 제거하고, 가장 중요한 문제 하나를 선택하는 능력입니다.</p>

<p>한 번의 올바른 판단은 여러 사람의 업무 방향을 바꾸고 수백 시간의 낭비를 막아 줍니다. 반대로 잘못된 우선순위는 아무리 좋은 도구를 사용해도 만회하기 어렵습니다.</p>

<p><br /></p>

<h2 id="4-우리-사업의-진짜-병목을-찾는-방법">4. 우리 사업의 진짜 병목을 찾는 방법</h2>

<p>사업을 성장시키려면 현재 가장 막혀 있는 지점, 즉 <strong>병목(Bottleneck)</strong>을 먼저 찾아야 합니다. 사업의 흐름은 크게 세 단계로 나눌 수 있습니다.</p>

<ul>
  <li><strong>수요 창출:</strong> 상품을 알리고 잠재 고객을 유입시키는 단계</li>
  <li><strong>전환:</strong> 관심을 보인 고객이 실제 구매를 결정하는 단계</li>
  <li><strong>제공:</strong> 구매한 고객에게 기대한 결과를 전달하는 단계</li>
</ul>

<p>어느 단계가 병목인지에 따라 해결해야 할 문제도 달라집니다.</p>

<ul>
  <li>신규 고객 유입이 부족하다면 메시지와 유입 채널을 점검해야 합니다.</li>
  <li>문의는 많지만 구매로 이어지지 않는다면 제안, 가격, 신뢰 요소를 개선해야 합니다.</li>
  <li>구매는 발생하지만 환불과 불만이 많다면 서비스 품질과 제공 과정을 살펴봐야 합니다.</li>
</ul>

<p>신규 고객 유입이 문제인데 AI로 내부 보고서 시스템만 개선한다면 매출은 늘어나지 않습니다. 문의는 충분한데 구매가 일어나지 않는다면 자동화보다 제안을 먼저 개선해야 합니다.</p>

<p>새로운 AI 도구를 도입하기 전에 다음 질문부터 확인해 보는 것이 좋습니다.</p>

<ol>
  <li>지금 사업의 성장을 가장 크게 막고 있는 숫자는 무엇인가?</li>
  <li>그 숫자를 개선하면 실제 매출이나 이익이 달라지는가?</li>
  <li>최근 데이터와 고객의 반응이 같은 문제를 가리키고 있는가?</li>
  <li>이 문제와 관계없는 업무 중 당장 멈출 수 있는 것은 무엇인가?</li>
</ol>

<p>이 질문에 답할 수 있다면 AI를 어디에 사용해야 할지도 자연스럽게 분명해집니다.</p>

<p><br /></p>

<h2 id="결론-ai는-도구이고-본질은-판단력입니다">결론: AI는 도구이고, 본질은 판단력입니다</h2>

<p>모든 사업의 시간, 사람, 자본은 한정되어 있습니다. 사업가의 실력은 이 자원을 얼마나 많은 일에 나누느냐가 아니라, 가장 큰 결과를 만들 <strong>단 하나의 문제에 얼마나 집중하느냐</strong>에서 드러납니다.</p>

<p>AI는 그 문제를 해결하는 훌륭한 가속 장치가 될 수 있습니다. 하지만 무엇이 문제인지 찾아내는 일까지 대신해 주지는 않습니다.</p>

<p>AI를 더 많이 사용하는 방법을 고민하기 전에 먼저 물어봐야 합니다.</p>

<p><strong>지금 우리 사업의 성장을 막고 있는 단 하나의 병목은 무엇인가?</strong></p>

<p>그 답을 찾는 순간, AI는 유행하는 도구가 아니라 실제 성과를 만드는 레버리지가 됩니다.</p>

<p><br /></p>

<h2 id="자주-묻는-질문">자주 묻는 질문</h2>

<h3 id="ai를-도입하면-매출이-자동으로-늘어나나요">AI를 도입하면 매출이 자동으로 늘어나나요?</h3>

<p>아닙니다. AI는 업무 속도를 높이는 도구일 뿐입니다. 매출을 막고 있는 병목과 우선순위를 먼저 해결해야 AI의 생산성이 실제 성과로 이어집니다.</p>

<h3 id="ai보다-먼저-확인해야-할-것은-무엇인가요">AI보다 먼저 확인해야 할 것은 무엇인가요?</h3>

<p>수요 창출, 전환, 제공 중 어느 단계가 사업 성장을 막고 있는지 확인해야 합니다. 병목이 신규 고객 유입이라면 내부 자동화보다 메시지와 유입 채널을 먼저 개선해야 합니다.</p>

<h3 id="기술-없이도-레버리지를-높일-수-있나요">기술 없이도 레버리지를 높일 수 있나요?</h3>

<p>가능합니다. 그룹 상담, 비동기 응답, 상담 전 정보 제공처럼 서비스 구조를 바꾸는 것만으로도 같은 시간에 더 많은 고객에게 가치를 제공할 수 있습니다.</p>]]></content><author><name>Minsoo Kim</name></author><category term="AI" /><category term="비즈니스" /><category term="생산성" /><summary type="html"><![CDATA[AI로 생산성이 높아져도 매출이 늘지 않는 이유와 의사결정, 사업 구조, 병목을 통해 진짜 레버리지를 찾는 방법을 정리합니다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kmshack.kr/blog/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" /><media:content medium="image" url="https://kmshack.kr/blog/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AppUpdateManager로 인앱 업데이트 처리하기</title><link href="https://kmshack.kr/blog/AppUpdateManager/" rel="alternate" type="text/html" title="AppUpdateManager로 인앱 업데이트 처리하기" /><published>2019-06-24T00:00:00+09:00</published><updated>2019-06-24T00:00:00+09:00</updated><id>https://kmshack.kr/blog/AppUpdateManager</id><content type="html" xml:base="https://kmshack.kr/blog/AppUpdateManager/"><![CDATA[<p>앱 업데이트를 안내하기 위해 자체 버전 확인 로직과 Play 스토어 이동 화면을 구현하는 경우가 많다. <code class="language-plaintext highlighter-rouge">AppUpdateManager</code>를 사용하면 사용자가 앱을 벗어나지 않고 업데이트를 진행하는 인앱 업데이트 흐름을 구성할 수 있다. 즉시 업데이트와 유연한 업데이트의 차이와 기본 구현 방법을 살펴본다.</p>

<p><br /></p>

<h3 id="문제점">문제점</h3>
<p>기존 방식에서는 사용자가 Play 스토어로 이동해 업데이트 버튼을 누르고, 설치가 끝난 뒤 앱으로 돌아와야 한다. 업데이트 시간이 길면 흐름에서 이탈할 수 있고, 단계적 배포 중에는 계정이나 기기에 새 버전이 아직 제공되지 않을 수도 있다.</p>

<p><br /></p>

<h2 id="appupdatemanager">AppUpdateManager</h2>
<p>Google은 이런 업데이트 흐름을 개선하기 위해 Play Core 라이브러리에 <code class="language-plaintext highlighter-rouge">AppUpdateManager</code>를 추가했다. 업데이트 가능 여부를 확인하고 Play 스토어로 이동하지 않은 채 인앱 업데이트를 진행할 수 있다.</p>

<h3 id="dependency">dependency</h3>
<p>build.gradle에 아래와 같이 종속성을 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>implementation 'com.google.android.play:core:1.6.1'
</code></pre></div></div>

<p><br /></p>

<h2 id="업데이트-체크하기">업데이트 체크하기</h2>
<p>AppUpdateManager를 통해 <code class="language-plaintext highlighter-rouge">appUpdateInfo</code>로 현재 업데이트 상태를 가져올 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">appUpdateManager</span> <span class="o">=</span> <span class="nc">AppUpdateManagerFactory</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="k">this</span><span class="o">)</span>
  
<span class="n">launch</span><span class="o">{</span>
  <span class="n">val</span> <span class="n">appUpdateInfo</span> <span class="o">=</span> <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">appUpdateInfo</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
  
  <span class="n">when</span><span class="o">(</span><span class="n">appUpdateInfo</span><span class="o">.</span><span class="na">updateAvailability</span><span class="o">()){</span>
      <span class="nc">UpdateAvailability</span><span class="o">.</span><span class="na">UPDATE_AVAILABLE</span> <span class="o">-&gt;{</span>
         <span class="c1">//업데이트 가능한 상태</span>
      <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><br /></p>

<p><code class="language-plaintext highlighter-rouge">appUpdateInfo</code>를 가져오는 콜백 구조는 <code class="language-plaintext highlighter-rouge">suspendCoroutine</code>을 이용해 suspend 함수로 감쌀 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nc">Task</span><span class="o">&lt;</span><span class="nc">AppUpdateInfo</span><span class="o">&gt;.</span><span class="na">await</span><span class="o">():</span> <span class="nc">AppUpdateInfo</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">suspendCoroutine</span> <span class="o">{</span> <span class="n">continuation</span> <span class="o">-&gt;</span>
        <span class="n">addOnCompleteListener</span> <span class="o">{</span> <span class="n">result</span> <span class="o">-&gt;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">isSuccessful</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">continuation</span><span class="o">.</span><span class="na">resume</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">result</span><span class="o">)</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">continuation</span><span class="o">.</span><span class="na">resumeWithException</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">exception</span><span class="o">)</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h3 id="updateavailability-4가지-상태">UpdateAvailability 4가지 상태</h3>
<ul>
  <li>DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS : AppUpdateType.IMMEDIATE 타입을 통해 업데이트를 수행중인 경우</li>
  <li>UNKNOWN: 알수 없음</li>
  <li>UPDATE_AVAILABLE: 현재 최신 버전이 아니며 업데이트가 필요한 경우</li>
  <li>UPDATE_NOT_AVAILABLE: 현재 최신 버전이며 업데이트가 필요 하지 않은 경우</li>
</ul>

<p><br /></p>

<h2 id="업데이트-수행하기">업데이트 수행하기</h2>
<p>업데이트 가능 여부를 확인했다면 <code class="language-plaintext highlighter-rouge">AppUpdateManager.startUpdateFlowForResult()</code>로 업데이트를 시작한다. 두 가지 업데이트 유형 중 상황에 맞는 방식을 선택할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">appUpdateManager</span><span class="o">.</span><span class="na">startUpdateFlowForResult</span><span class="o">(</span>
    <span class="n">appUpdateInfo</span><span class="o">,</span>
    <span class="nc">AppUpdateType</span><span class="o">.</span><span class="na">FLEXIBLE</span> <span class="o">,</span> <span class="c1">// or AppUpdateType.IMMEDIATE</span>
    <span class="n">activity</span><span class="o">,</span>
    <span class="no">REQUEST_CODE_UPDATE</span><span class="o">)</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="즉시-업데이트">즉시 업데이트</h2>
<p><code class="language-plaintext highlighter-rouge">AppUpdateType.IMMEDIATE</code> 타입을 사용하면 별도의 업데이트 UI를 표시하게 되며 사용자가 업데이트전 다른 작업을 하지 못하도록 블락시키게 됩니다. 강제 업데이트 해야 할 경우 적절합니다. 앱이 업데이트 되면 자동으로 애플리케이션이 자동으로 재시작됩니다. 업데이트가 완료 되는 경우 onActivityForResult()를 통해 결과를 확인할 수 있습니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2019-06-24-AppUpdateManager/immediate.jpg" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>업데이트 중에는 항상 업데이트 UI가 표시되도록 <code class="language-plaintext highlighter-rouge">UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS</code> 상태를 처리하면 된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">override</span> <span class="n">fun</span> <span class="nf">onResume</span><span class="o">()</span> <span class="o">{</span>
    <span class="kd">super</span><span class="o">.</span><span class="na">onResume</span><span class="o">()</span>
    <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">appUpdateInfo</span>
        <span class="o">.</span><span class="na">addOnSuccessListener</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">updateAvailability</span><span class="o">()</span> <span class="o">==</span> <span class="nc">UpdateAvailability</span><span class="o">.</span><span class="na">DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">startUpdateFlowForResult</span><span class="o">(</span>
                    <span class="n">it</span><span class="o">,</span>
                    <span class="nc">AppUpdateType</span><span class="o">.</span><span class="na">IMMEDIATE</span><span class="o">,</span>
                    <span class="n">activity</span><span class="o">,</span>
                    <span class="no">REQUEST_CODE_UPDATE</span><span class="o">)</span>
            <span class="o">}</span>
        <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><br /></p>

<h2 id="자연스러운-업데이트">자연스러운 업데이트</h2>
<p><code class="language-plaintext highlighter-rouge">AppUpdateType.FLEXIBLE</code>을 사용하면 사용자가 앱을 계속 이용하는 동안 업데이트 파일을 백그라운드에서 내려받을 수 있다. 다운로드가 끝나면 별도의 UI로 완료 사실을 알리고 설치를 진행한다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2019-06-24-AppUpdateManager/flexible.jpg" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p>다운로드 진행 상태는 <code class="language-plaintext highlighter-rouge">InstallStateUpdatedListener</code>로 받을 수 있다. 다운로드가 끝나면 Snackbar처럼 방해가 적은 UI로 설치를 안내하고, <code class="language-plaintext highlighter-rouge">appUpdateManager.completeUpdate()</code>를 호출해 설치를 진행한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">listener</span> <span class="o">=</span> <span class="nc">InstallStateUpdatedListener</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">installStatus</span><span class="o">()</span> <span class="o">==</span> <span class="nc">InstallStatus</span><span class="o">.</span><span class="na">DOWNLOADED</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Snackbar</span><span class="o">.</span><span class="na">make</span><span class="o">(</span><span class="n">coordinator_layout</span><span class="o">,</span> <span class="s">"업데이트 버전 다운로드 완료"</span><span class="o">,</span> <span class="nc">Snackbar</span><span class="o">.</span><span class="na">LENGTH_INDEFINITE</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setAction</span><span class="o">(</span><span class="s">"설치/재시작"</span><span class="o">,</span> <span class="nc">View</span><span class="o">.</span><span class="na">OnClickListener</span> <span class="o">{</span>
                    <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">completeUpdate</span><span class="o">()</span>
                <span class="o">}).</span><span class="na">show</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">appUpdateManager</span><span class="o">.</span><span class="na">registerListener</span><span class="o">(</span><span class="n">listener</span><span class="o">)</span>
</code></pre></div></div>

<p><br /></p>

<p>새 버전이 있어도 특정 기기나 계정에는 아직 업데이트가 제공되지 않을 수 있다. 해당 업데이트 유형을 실제로 사용할 수 있는지 <code class="language-plaintext highlighter-rouge">appUpdateInfo.isUpdateTypeAllowed()</code>로 확인해야 한다.</p>

<p><br /></p>

<p>참고:<br />
<a href="https://developer.android.com/guide/app-bundle/in-app-updates">https://developer.android.com/guide/app-bundle/in-app-updates</a><br />
<a href="https://proandroiddev.com/theres-a-new-update-available-75a2c5bda76e">https://proandroiddev.com/theres-a-new-update-available-75a2c5bda76e</a></p>]]></content><author><name>Minsoo Kim</name></author><category term="Play 스토어" /><category term="안드로이드" /><summary type="html"><![CDATA[앱 업데이트를 안내하기 위해 자체 버전 확인 로직과 Play 스토어 이동 화면을 구현하는 경우가 많다. AppUpdateManager를 사용하면 사용자가 앱을 벗어나지 않고 업데이트를 진행하는 인앱 업데이트 흐름을 구성할 수 있다. 즉시 업데이트와 유연한 업데이트의 차이와 기본 구현 방법을 살펴본다.]]></summary></entry><entry><title type="html">WorkManager로 정기적인 백그라운드 작업 수행하기</title><link href="https://kmshack.kr/blog/work_manager/" rel="alternate" type="text/html" title="WorkManager로 정기적인 백그라운드 작업 수행하기" /><published>2019-03-18T00:00:00+09:00</published><updated>2019-03-18T00:00:00+09:00</updated><id>https://kmshack.kr/blog/work_manager</id><content type="html" xml:base="https://kmshack.kr/blog/work_manager/"><![CDATA[<p>Android O부터 백그라운드 서비스와 암시적 브로드캐스트에는 여러 제약이 적용된다. 즉시 실행할 필요는 없지만 완료가 보장되어야 하는 작업을 <code class="language-plaintext highlighter-rouge">WorkManager</code>로 예약하고, 주기적으로 실행하는 방법을 살펴본다.</p>

<p><br /></p>

<p>WorkManager는 Android Jetpack의 일부로 1.0.0버전으로 얼마전 공개되었습니다. Google은 이미 <code class="language-plaintext highlighter-rouge">JobScheduler</code>, <code class="language-plaintext highlighter-rouge">Firebase JobDispatcher</code>와 같은 백그라운드 작업을 위한 라이브러리를 수차례 공개하였습니다. 또한 Evernote의 <code class="language-plaintext highlighter-rouge">Android Job</code>이 있습니다. WorkManager는 이미 공개된 라이브러리보다 많은 장점이 있습니다.</p>

<ul>
  <li>이전 버전과의 호환성(API14이상 모두 지원)</li>
  <li>Google Play Service에 대한 의존성이 없음</li>
  <li>체인 기반의 작업 관리</li>
  <li>작업 상태 쿼리가능</li>
</ul>

<p><br /></p>
<h2 id="이제-프로젝트에-적용해봅시다">이제 프로젝트에 적용해봅시다!</h2>

<p>항상 그렇듯 build.gradle에 종속성을 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>allprojects {
    repositories {
        google()
        jcenter()
    }
}
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies {
    def work_version = 1.0.0

    // (Java only)
    implementation "android.arch.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "android.arch.work:work-runtime-ktx:$work_version"

}
</code></pre></div></div>

<p><br />
백그라운드에서 일부 작업을 실행하는 예제를 보도록하겠습니다. 현재 좌표를 하루에 두번씩 서버로 전송하는 예제이며, 몇 가지 제약조건이 추가됩니다. 기기가 Wi-Fi에 연결되어 있고 저장 용량이 부족하지 않은 경우에만 작동합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nf">LocationWorker</span><span class="o">(</span><span class="nl">context:</span> <span class="nc">Context</span><span class="o">,</span> <span class="nl">workerParams:</span> <span class="nc">WorkerParameters</span><span class="o">)</span> 
    <span class="o">:</span> <span class="nc">Worker</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">workerParams</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="o">...</span>
    
    <span class="n">override</span> <span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">{</span>
        
        <span class="n">val</span> <span class="n">latitude</span> <span class="o">=</span> <span class="n">inputData</span><span class="o">.</span><span class="na">getDouble</span><span class="o">(</span><span class="no">KEY_LATITUDE</span><span class="o">,</span> <span class="mf">40.1903484</span><span class="o">)</span>
        <span class="n">val</span> <span class="n">longitude</span> <span class="o">=</span> <span class="n">inputData</span><span class="o">.</span><span class="na">getDouble</span><span class="o">(</span><span class="no">KEY_LONGITUDE</span><span class="o">,</span> <span class="mf">44.5148367</span><span class="o">)</span>
        
        <span class="n">val</span> <span class="n">sendDataService</span> <span class="o">=</span> <span class="nc">SendDataService</span><span class="o">.</span><span class="na">getInstance</span><span class="o">()</span>
        <span class="n">sendDataService</span><span class="o">.</span><span class="na">sendLocation</span><span class="o">(</span><span class="n">latitude</span><span class="o">,</span> <span class="n">longitude</span><span class="o">)</span>
            <span class="o">.</span><span class="na">addSuccessCallback</span> <span class="o">{</span>
            
            <span class="o">}</span>
            <span class="o">.</span><span class="na">addFailureCallback</span> <span class="o">{</span>
            
            <span class="o">}</span>

        <span class="k">return</span> <span class="nc">Result</span><span class="o">.</span><span class="na">success</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
Worker가 생성되고, 주기적으로 실행되는 작업 공간의 큐에 추가됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">createConstraints</span><span class="o">()</span> <span class="o">=</span> <span class="nc">Constraints</span><span class="o">.</span><span class="na">Builder</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">setRequiredNetworkType</span><span class="o">(</span><span class="nc">NetworkType</span><span class="o">.</span><span class="na">UNMETERED</span><span class="o">)</span>  <span class="c1">//와이파이 연결된 경우</span>
                        <span class="c1">// 다른값(NOT_REQUIRED, CONNECTED, NOT_ROAMING, METERED)</span>
                        <span class="o">.</span><span class="na">setRequiresBatteryNotLow</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>                 <span class="c1">// 배터리가 부족하지 않는 경우</span>
                        <span class="o">.</span><span class="na">setRequiresStorageNotLow</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>                 <span class="c1">// 저장소가 부족하지 않는 경우</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">()</span>

<span class="n">fun</span> <span class="nf">createWorkRequest</span><span class="o">(</span><span class="nl">data:</span> <span class="nc">Data</span><span class="o">)</span> <span class="o">=</span> <span class="nc">PeriodicWorkRequestBuilder</span><span class="o">&lt;</span><span class="nc">LocationWorker</span><span class="o">&gt;(</span><span class="mi">12</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">HOURS</span><span class="o">)</span><span class="c1">// 12시간 반복</span>
                <span class="o">.</span><span class="na">setInputData</span><span class="o">(</span><span class="n">data</span><span class="o">)</span> <span class="c1">// 입력 데이터                                                  </span>
                <span class="o">.</span><span class="na">setConstraints</span><span class="o">(</span><span class="n">createConstraints</span><span class="o">())</span> <span class="c1">// 작업을 재시도 할경우에 대한 정책</span>
                <span class="o">.</span><span class="na">setBackoffCriteria</span><span class="o">(</span><span class="nc">BackoffPolicy</span><span class="o">.</span><span class="na">LINEAR</span><span class="o">,</span> 
                                <span class="nc">PeriodicWorkRequest</span><span class="o">.</span><span class="na">MIN_BACKOFF_MILLIS</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MILLISECONDS</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">()</span>

<span class="n">fun</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 입력 데이터를 설정합니다. Bundle와 동일합니다.</span>
    <span class="n">val</span> <span class="n">work</span> <span class="o">=</span> <span class="n">createWorkRequest</span><span class="o">(</span><span class="nc">Data</span><span class="o">.</span><span class="na">EMPTY</span><span class="o">)</span>
    
    <span class="cm">/* 작업을 큐에 넣을때 동일한 작업인 경우에 대한 정책을 지정할 수 있습니다. 
    ExistingPeriodicWorkPolicy.KEEP은 동일한 작업을 큐에 넣게되며, 
    ExistingPeriodicWorkPolicy.REPLACE인 경우 작업이 대체됩니다. */</span>
    <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">().</span><span class="na">enqueueUniquePeriodicWork</span><span class="o">(</span><span class="s">"Smart work"</span><span class="o">,</span> <span class="nc">ExistingPeriodicWorkPolicy</span><span class="o">.</span><span class="na">KEEP</span><span class="o">,</span> <span class="n">work</span><span class="o">)</span>
    
    <span class="c1">// 작업의 상태를 LiveData를 통해 관찰하게 됩니다. </span>
    <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">().</span><span class="na">getWorkInfoByIdLiveData</span><span class="o">(</span><span class="n">work</span><span class="o">.</span><span class="na">id</span><span class="o">)</span>
        <span class="o">.</span><span class="na">observe</span><span class="o">(</span><span class="n">lifecycleOwner</span><span class="o">,</span> <span class="nc">Observer</span> <span class="o">{</span> <span class="n">workInfo</span> <span class="o">-&gt;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">workInfo</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">workInfo</span><span class="o">.</span><span class="na">state</span> <span class="o">==</span> <span class="nc">WorkInfo</span><span class="o">.</span><span class="na">State</span><span class="o">.</span><span class="na">SUCCEEDED</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// 작업 완료</span>
            <span class="o">}</span>
        <span class="o">})</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
앞서 WorkManager의 장점으로 소개 했던 체인 기반의 작업에 대해서도 알아 보겠습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">chainWorks</span><span class="o">(</span><span class="nl">filter1:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">filter2:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">compress:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">upload:</span> <span class="nc">Work</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">()</span>
    <span class="c1">// Worker를 동시에 병렬로 실행합니다.</span>
    <span class="o">.</span><span class="na">beginWith</span><span class="o">(</span><span class="n">listOf</span><span class="o">(</span><span class="n">filter1</span><span class="o">,</span> <span class="n">filter2</span><span class="o">))</span>
    <span class="c1">// 이전 beginWith의 모든 작업이 끝난 경우 실행됩니다.</span>
    <span class="o">.</span><span class="na">then</span><span class="o">(</span><span class="n">compress</span><span class="o">)</span>
    <span class="c1">//compress작업이 완료 된경우 upload가 실행됩니다.</span>
    <span class="o">.</span><span class="na">then</span><span class="o">(</span><span class="n">upload</span><span class="o">)</span>
    <span class="c1">//enqueue()를 호출해야 이 모든 작업이 실행됩니다.</span>
    <span class="o">.</span><span class="na">enqueue</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2019-03-18-work_manager/worker_manager.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br />
<code class="language-plaintext highlighter-rouge">Worker</code>는 우리가 원했던 작업구현 방식입니다. doWork() 메서드에서 필요한 작업을 구현하면 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">WorkRequest</code>는 Worker의 arguments(입력된 데이터)와 constraints(네트워크 연결) 작업을 당담합니다.</p>

<p><code class="language-plaintext highlighter-rouge">WorkManager</code>는 WorkRequest를 큐에 담고 작업을 시작합니다. 이 작업을 스케쥴링 하기 위해 <code class="language-plaintext highlighter-rouge">Room 데이터베이스</code>에 저장 하는 가장 좋은 방법을 사용합니다. 이 작업 결과는 LiveData를 통해 전송됩니다.</p>

<p><br /></p>
<h2 id="결론">결론</h2>
<p>WorkManager는 앱이 종료되거나 기기가 재시작되어도 실행되며, 비동기 작업을  구현하는 가장간단하면서 효과적인 솔루션입니다. 또한 제약사항을 통해 앱의 소비전력도 줄일 수 있습니다.</p>

<p><br /></p>

<p>참고:<br />
<a href="https://medium.com/@RobertLevonyan/android-workmanager-manage-periodic-tasks-c13fa7744ebd">https://medium.com/@RobertLevonyan/android-workmanager-manage-periodic-tasks-c13fa7744ebd</a>
<a href="https://developer.android.com/topic/libraries/architecture/workmanager">https://developer.android.com/topic/libraries/architecture/workmanager</a></p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><category term="서비스" /><summary type="html"><![CDATA[Android O부터 백그라운드 서비스와 암시적 브로드캐스트에는 여러 제약이 적용된다. 즉시 실행할 필요는 없지만 완료가 보장되어야 하는 작업을 WorkManager로 예약하고, 주기적으로 실행하는 방법을 살펴본다.]]></summary></entry><entry><title type="html">ViewPager2 주요 변경점 살펴보기</title><link href="https://kmshack.kr/blog/ViewPager2/" rel="alternate" type="text/html" title="ViewPager2 주요 변경점 살펴보기" /><published>2019-02-18T00:00:00+09:00</published><updated>2019-02-18T00:00:00+09:00</updated><id>https://kmshack.kr/blog/ViewPager2</id><content type="html" xml:base="https://kmshack.kr/blog/ViewPager2/"><![CDATA[<p><a href="https://android-developers.googleblog.com/2011/08/horizontal-view-swiping-with-viewpager.html">2011년에 출시된 ViewPager</a>의 후속 버전인 <a href="https://developer.android.com/jetpack/androidx/releases/viewpager2#1.0.0-alpha01">ViewPager2</a>가 2019년 알파 버전으로 공개되었다. RecyclerView 기반 어댑터, 세로 방향 페이징과 RTL 지원 등 주요 변경점을 살펴본다.</p>

<p>ViewPager는 널리 사용되었지만 Fragment 중심의 어댑터 구조, RTL과 세로 방향 페이징 지원 등에서 아쉬움이 있었다. ViewPager2는 이런 요구를 반영해 구조와 기능을 개선했다.</p>

<p><br /></p>

<p>이제 다음 기능을 공식적으로 지원한다.</p>
<h2 id="viewpager2">ViewPager2</h2>

<p>프로젝트는 AndroidX로 구성하고 minSdkVersion 14이상을 지원해야 합니다.
build.gradle에 아래 라이브러리를 추가합니다.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha01'
</code></pre></div></div>

<p><br />
RecyclerView에 익숙하다면 ViewPager2를 구성하는것은 매우 익숙합니다. RecyclerView.Adapter를 상속받아 어댑터를 생성합니다. 물론 ViewHolder도 필요합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nf">AppPagerAdapter</span><span class="o">(</span><span class="kd">private</span> <span class="n">val</span> <span class="nl">apps:</span> <span class="nc">Array</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;)</span> <span class="o">:</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">Adapter</span><span class="o">&lt;</span><span class="nc">AppViewHolder</span><span class="o">&gt;()</span> <span class="o">{</span>
    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onCreateViewHolder</span><span class="o">(</span><span class="nl">parent:</span> <span class="nc">ViewGroup</span><span class="o">,</span> <span class="nl">viewType:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">AppViewHolder</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">AppViewHolder</span><span class="o">(</span><span class="nc">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">parent</span><span class="o">.</span><span class="na">context</span><span class="o">).</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">app_pager_item</span><span class="o">,</span> <span class="n">parent</span><span class="o">,</span> <span class="kc">false</span><span class="o">))</span>
    <span class="o">}</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onBindViewHolder</span><span class="o">(</span><span class="nl">holder:</span> <span class="nc">AppViewHolder</span><span class="o">,</span> <span class="nl">position:</span> <span class="nc">Int</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">txtApp</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">apps</span><span class="o">[</span><span class="n">position</span><span class="o">]</span>
    <span class="o">}</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">getItemCount</span><span class="o">()</span> <span class="o">=</span> <span class="n">apps</span><span class="o">.</span><span class="na">size</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nf">AppViewHolder</span><span class="o">(</span><span class="nl">view:</span> <span class="nc">View</span><span class="o">)</span> <span class="o">:</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span><span class="o">(</span><span class="n">view</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">val</span> <span class="nl">txtApp:</span> <span class="nc">TextView</span> <span class="o">=</span> <span class="n">view</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">txt_app</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
마지막으로 RecyclerView와 마찬가지로 ViewPager2의 Adapter를 설정한다. LayoutManager를 직접 지정할 필요는 없으며, <code class="language-plaintext highlighter-rouge">ORIENTATION_VERTICAL</code>로 세로 방향 페이징을 설정할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ViewPager2</span><span class="o">.</span><span class="na">orientation</span> <span class="o">=</span> <span class="nc">ViewPager2</span><span class="o">.</span><span class="na">RIENTATION_VERTICAL</span>
</code></pre></div></div>

<p><br />
ViewPager2는 아직 알파 버전인 단계로 버그나 문제점은 보여집니다. 하지만 ViewPager의 View 재사용성 문제를 RecyclerView의 ViewHolder패턴을 그대로 사용하면서 해결함과 동시에 RecyclerView에 익숙한 개발자들에게 별도의 학습없이 적용할 수 있게 솔루션을 제공하였습니다. 또한 orientation기능 도입으로 가로 스크롤뿐만 세로스크롤도 지원하게 되었습니다. 또한 RTL도 지원합니다!</p>

<p>추가 기능외 전 버전의 notifyDataSetChanged()가 실행되지 않는 문제를 해결하였습니다. getItemPosition에서 POSITION_NONE을 강제로 반환하도록 하여 문제를 회피했던 경험 모두 있으실것 같은데 이제 해결되었습니다.</p>

<p><br /></p>

<h3 id="알려진-버그-100-알파-01기준">알려진 버그 (1.0.0 알파 01기준)</h3>
<p>-ClipToPadding 미지원<br />
-TabLayout 미지원<br />
-pageWidth 미지원(강제 가로/세로 100%)<br />
-currentItem 설정시 이전 페이지 보이는 현상</p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><category term="뷰페이저" /><summary type="html"><![CDATA[2011년에 출시된 ViewPager의 후속 버전인 ViewPager2가 2019년 알파 버전으로 공개되었다. RecyclerView 기반 어댑터, 세로 방향 페이징과 RTL 지원 등 주요 변경점을 살펴본다.]]></summary></entry><entry><title type="html">Kotlin Coroutines 예외 처리 패턴</title><link href="https://kmshack.kr/blog/kotlin_coroutine_pattern/" rel="alternate" type="text/html" title="Kotlin Coroutines 예외 처리 패턴" /><published>2018-12-11T00:00:00+09:00</published><updated>2018-12-11T00:00:00+09:00</updated><id>https://kmshack.kr/blog/kotlin_coroutine_pattern</id><content type="html" xml:base="https://kmshack.kr/blog/kotlin_coroutine_pattern/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">async</code>에서 발생한 예외는 <code class="language-plaintext highlighter-rouge">await()</code>만 <code class="language-plaintext highlighter-rouge">try/catch</code>로 감싼다고 항상 처리되는 것은 아니다. 부모와 자식 Job 사이의 전파 규칙을 이해하고 <code class="language-plaintext highlighter-rouge">coroutineScope</code> 또는 <code class="language-plaintext highlighter-rouge">SupervisorJob</code>을 선택하는 방법을 예제로 살펴본다.</p>

<h2 id="async-호출을-감싸서-처리할-때-coroutinescope-또는-supervisorjob-사용하기">async 호출을 감싸서 처리할 때 coroutineScope 또는 SupervisorJob 사용하기</h2>

<p>async 블록이 예외를 throw하는 경우 try/catch블록으로 감싸는 것만으로 모든 예외를 처리할 수 있다는 것에 신뢰하면 안됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="nl">job:</span> <span class="nc">Job</span> <span class="o">=</span> <span class="nc">Job</span><span class="o">()</span>
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>   <span class="c1">// (1)</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>                               <span class="c1">// (2)</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예제에서 doWork() 함수는 처리되지 않는 예외를 throw할 수 있는 새로운 코루틴(1)을 시작합니다. try/catch 블록(2)으로 doWork() 함수를 감싸게 되면 충돌이 발생합니다.</p>

<p>이는 자식 Job이 실패 하는 경우 부모도 즉각적으로 실패 해버리기때문에 발생하는 문제입니다.</p>

<p>충돌을 피할 수 있는 한 가지의 방법은 <code class="language-plaintext highlighter-rouge">SupervisorJob</code>을 사용하는 것입니다.</p>

<p>자식의 실패또는 취소로 인해 Job이 실패 되지 않으며 이로 인해 다른 자녀들에게 영향을 주지않게 됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               <span class="c1">// (1)</span>
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>참고: 이 기능은 SupervisorJob으로 코루틴 범위에서 비동기를 명시적으로 실행하는 경우에만 작동합니다. async가 부모 코루틴(1)의 범위에서 시작되었기 때문에 아래 코드는 여전히 크래시가 납니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">async</span> <span class="o">{</span>                                         <span class="c1">// (1)</span>
            <span class="c1">// may throw Exception </span>
        <span class="o">}.</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>크래시를 피하는 또 다른 방법은 <code class="language-plaintext highlighter-rouge">coroutineScope</code>(1)을 사용하여 async를 감싸서 사용하는 방법이 있습니다. 이로 인해 async 내부에서 예외가 발생하면 외부 범위를 건드리지 않고 이 범위에서 작성된 다른 모든 coroutine만 취소 됩니다.(2)</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">coroutineScope</span> <span class="o">{</span>     <span class="c1">// (1)</span>
    <span class="n">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>                       <span class="c1">// (2)</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="루트-코루틴은-main-디스패처를-선호하자">루트 코루틴은 Main 디스패처를 선호하자.</h2>

<p>백그라운드 코루틴에서 백그라운드 작업을 수행하고 UI업데이트를 해야 하는 경우 Main 디스패처에서 실행해야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span>          <span class="c1">// (1)</span>
<span class="n">fun</span> <span class="nf">login</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span> <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span> <span class="o">}</span>  <span class="c1">// (2)  </span>
    <span class="n">networkClient</span><span class="o">.</span><span class="na">login</span><span class="o">(...)</span>
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span> <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span> <span class="o">}</span>  <span class="c1">// (2)</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예에서 기본 디스패처(1)에서 코루틴을 실행합니다. 이 접근 방식을 사용하면 UI를 터치 할 때 마다 Main 디스패처로 컨텍스트를 전환해야 합니다.(2)</p>

<p>이런 경우 Main 디스패처로 코루틴을 실행 하는 것이 코드가 훨씬 단순해지며 컨텍스트 전환이 더 명확해집니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span>
<span class="n">fun</span> <span class="nf">login</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>    
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span> <span class="n">networkClient</span><span class="o">.</span><span class="na">login</span><span class="o">(...)</span> <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="불필요한-asyncawait-사용을-피하자">불필요한 async/await 사용을 피하자.</h2>

<p>async 함수를 사용하고 즉시 await하는 경우라면 코드를 당장 걷어내야합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">data</span> <span class="o">=</span> <span class="n">async</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* code */</span> <span class="o">}.</span><span class="na">await</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>만약 코루틴 컨텍스트를 바꾸고 즉시 중단하고 싶다면 withContext를 사용하는 것이 훨씬 좋은 방법이 될 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">data</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* code */</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>퍼포먼스 측면으로는 큰 문제는 아니지만( 심지어 async가 작업을 수행하기 위해 새로운 코루틴을 생성해도) 의미론적으로 async 하다는 것은 백그라운드에서 여러 개의 코루틴을 시작한 뒤 기다리고 있음을 의미하기 때문에 적절하지 않습니다.</p>

<p><br /></p>

<h2 id="scope-job을-취소하지-말자">scope Job을 취소하지 말자.</h2>

<p>코루틴을 취소해야 하는 경우 스코프 작업을 취소하면 안됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">WorkManager</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">doWork1</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span>
    <span class="o">}</span>
    <span class="n">fun</span> <span class="nf">doWork2</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span>
    <span class="o">}</span>
    <span class="n">fun</span> <span class="nf">cancelAllWork</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">job</span><span class="o">.</span><span class="na">cancel</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">workManager</span> <span class="o">=</span> <span class="nc">WorkManager</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork2</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">cancelAllWork</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span> <span class="c1">// (1)</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 코드의 문제는 작업을 취소할 때 완료 상태로 만들어 버리는 것에 있습니다. 이미 완료된 작업 범위에서 실행된 코루틴은 재 실행되지 않습니다. (1)</p>

<p>특정 범위의 모든 코루틴을 취소하려면 <code class="language-plaintext highlighter-rouge">cancelChildren</code> 함수를 사용할 수 있다. 개별 작업도 취소할 수 있다.(2)</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">WorkManager</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">doWork1</span><span class="o">():</span> <span class="nc">Job</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span> <span class="c1">// (2)</span>
    <span class="n">fun</span> <span class="nf">doWork2</span><span class="o">():</span> <span class="nc">Job</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span> <span class="c1">// (2)</span>
    <span class="n">fun</span> <span class="nf">cancelAllWork</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">coroutineContext</span><span class="o">.</span><span class="na">cancelChildren</span><span class="o">()</span>         <span class="c1">// (1)                             </span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">workManager</span> <span class="o">=</span> <span class="nc">WorkManager</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork2</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">cancelAllWork</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="분명하지-않은-디스패처는-suspend-함수로-작성하지-말자">분명하지 않은 디스패처는 suspend 함수로 작성하지 말자.</h2>

<p>명백한 코루틴 디스패처에 대한 실행인 경우 suspend 함수로 작업하지 않아야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nf">login</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span>  
        <span class="n">someBlockingCall</span><span class="o">()</span> 
    <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예제는 로그인하는 기능으로 Main 디스패처가 아닌 곳에서 실행하면 크래시를 발생하는 suspend 함수 입니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>     <span class="c1">// (1) no crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span>  <span class="c1">// (2) cause crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CalledFromWrongThreadException</code> 생성된 원래 스레드에서만 해당 뷰를 제어해야 합니다.</p>

<p>suspend 함수는 어떠한 코루틴 디스패처에 대해 실행할 수 있도록 디자인되어야합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nf">login</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span>  
        <span class="n">someBlockingCall</span><span class="o">()</span> 
    <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 모든 디스패처에서 로그인 기능을 실행할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>     <span class="c1">// (1) no crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span>  <span class="c1">// (2) no crash ether</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="globalscope-사용을-피하자">GlobalScope 사용을 피하자.</h2>

<p>만일 안드로이드 애플리케이션에서 <code class="language-plaintext highlighter-rouge">GlobalScope</code>를 사용하고 있다면 당장 사용을 중단해야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">GlobalScope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="c1">// code</span>
<span class="o">}</span>
</code></pre></div></div>

<p>GlobalScope는 전체 애플리케이션 수명 동안에 작동하고, 취소되지 않는 최상위 수준의 동시 처리를 시작하는데 사용됩니다.</p>

<p>별로도 정의한 <code class="language-plaintext highlighter-rouge">CoroutineScope</code>를 사용해야하며 async를 사용하거나 GlobalScope 인스턴스에서 실행하는 것이 좋습니다.</p>

<p>안드로이드에서 코루틴은 Activity, Fragment, View 또는 ViewModel의 수명주기로 쉽게 범위를 지정할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MainActivity</span> <span class="o">:</span> <span class="nc">AppCompatActivity</span><span class="o">(),</span> <span class="nc">CoroutineScope</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>

    <span class="n">override</span> <span class="n">val</span> <span class="nl">coroutineContext:</span> <span class="nc">CoroutineContext</span>
        <span class="nf">get</span><span class="o">()</span> <span class="o">=</span> <span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Main</span> <span class="o">+</span> <span class="n">job</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onDestroy</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onDestroy</span><span class="o">()</span>
        <span class="n">coroutineContext</span><span class="o">.</span><span class="na">cancelChildren</span><span class="o">()</span>
    <span class="o">}</span>
    
    <span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">launch</span> <span class="o">{</span>
        <span class="c1">// code</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
참고: <a href="https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e">https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e</a></p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><category term="코틀린" /><category term="코루틴" /><summary type="html"><![CDATA[async에서 발생한 예외는 await()만 try/catch로 감싼다고 항상 처리되는 것은 아니다. 부모와 자식 Job 사이의 전파 규칙을 이해하고 coroutineScope 또는 SupervisorJob을 선택하는 방법을 예제로 살펴본다.]]></summary></entry><entry><title type="html">MotionLayout으로 코드 없이 전환 효과 만들기</title><link href="https://kmshack.kr/blog/motionlayout/" rel="alternate" type="text/html" title="MotionLayout으로 코드 없이 전환 효과 만들기" /><published>2018-10-12T00:00:00+09:00</published><updated>2018-10-12T00:00:00+09:00</updated><id>https://kmshack.kr/blog/motionlayout</id><content type="html" xml:base="https://kmshack.kr/blog/motionlayout/"><![CDATA[<p>ConstraintSet을 사용하면 두 레이아웃 사이의 크기와 위치 변화를 애니메이션으로 표현할 수 있다. 이 아이디어를 확장한 <code class="language-plaintext highlighter-rouge">MotionLayout</code>으로 복잡한 화면 전환을 XML에 선언하는 방법을 살펴본다.</p>

<p>아직 ConstraintSet을 사용해보지 않으셨다면 아래 <a href="https://www.youtube.com/watch?v=OHcfs6rStRo">Sean McQuillan @objcode</a>의 영상을 보시기 권해드립니다.</p>

<iframe width="98%" height="400" src="https://www.youtube.com/embed/OHcfs6rStRo?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe>

<p>위의 영상에서 볼수 있듯 ConstraintLayout과 TransitionManager를 통해 애니메이션을 만드는 쉬운 방법을 제공합니다. 이런 기본적인 아이디어를 바탕으로 MotionLayout은 개념을 훨씬 확장하게 됩니다.</p>

<p><br /></p>
<h1 id="motionlayout">MotionLayout?</h1>

<p>MotionLayout은 레이아웃 전환과 복잡한 모션을 쉽게 제어하기 위해 만들어졌다. 버튼이나 타이틀처럼 사용자와 상호 작용하는 UI 요소의 위치와 크기를 입력에 따라 바꿔야 할 때 특히 유용하다. 단순한 애니메이션이라면 기존 Android 프레임워크가 제공하는 다음 방법도 사용할 수 있다.</p>

<ul>
  <li>Animated Vector Drawable</li>
  <li>Property Animation</li>
  <li>LayoutTransition</li>
  <li>TransitionManager</li>
  <li>CoordinatorLayout</li>
</ul>

<p><br />
MotionLayout은 기존 애니메이션 방식과는 전혀다릅니다. 이름에서도 알 수 있듯 레이아웃이며 이에따라 요소들을 배치할 수 있습니다. 실제로 ConstraintLayout을 상속한 하위클래스로 풍부한 레이아웃 기능을 기반으로합니다.</p>

<h4 id="property-animation--transitionmanager--coordinatorlayout--motionlayout">Property Animation + TransitionManager + CoordinatorLayout = MotionLayout</h4>

<p><br />
두 레이아웃 사이의 전환에 있어 최상위 레이아웃 뿐만 아니라 하위 레이아웃 속성에 애니메이션을 적용할 수 있습니다. 또한 CoordinatorLayout과 같이 애니메이션의 포지셔닝을 마음대로 조절가능합니다.  터치 핸들링과 키프레임을 지원함으로 자신의 필요에 맞게 쉽게 전환 효과를 정의 할 수 있습니다.</p>

<p>MotionLayout은 전혀 코드가 필요하지 않고 xml로 완벽하게 작동할 수 있게 합니다. 이렇게 코드와 디커플링됨에 따라 Android Studio에서 xml을 보여주는 훌륭한 그래픽 도구를 제공할 수 있을 것으로 보입니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-12-motionlayout/1.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>마지막으로 MotionLayout을 지원하는 ConstraintLayout 2.0에서는 API 레벨 14(ICS) 부터 서포트 라이브러리를 통해 사용할 수 있습니다. 이는 현재 안드로이드 기기의 99.8%를 지원하는 수준입니다.</p>

<p><br /> 
<strong>MotionLayout 프로젝트에 추가</strong><br />
Gradle file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies {
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha2'
}
</code></pre></div></div>

<p><br />
<strong>MotionLayout 사용</strong><br />
Layout file:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span> <span class="err">...</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<p><br /></p>
<h1 id="motionscene">MotionScene</h1>
<p>MotionLayout은 일반적인 레이아웃과는 달리 <code class="language-plaintext highlighter-rouge">res/xml</code> 디렉토리에 저장되는 별도의 <code class="language-plaintext highlighter-rouge">xml</code>파일인 <code class="language-plaintext highlighter-rouge">MotionScene</code>에 속성을 정의합니다. 여기에는 애니메이션을 지정하는 필요한 Transition 처리, 키 프레임, 터치 처리등의 요소들이 포함됩니다.</p>

<p><br /> <img src="/blog/images/2018-10-12-motionlayout/2.png" alt="" class="center-image" /> <br /></p>

<p><br /></p>
<h2 id="기존-constraintset을-이용한-방식을-motionlayout에-적용">기존 ConstraintSet을 이용한 방식을 MotionLayout에 적용</h2>

<p>첫 번째 ConstraintSet에는 위젯을 화면 왼쪽에, 두 번째 ConstraintSet에는 오른쪽에 배치해 좌우로 전환하는 예제를 살펴보겠다. <code class="language-plaintext highlighter-rouge">TransitionManager</code>를 사용하면 이 변화를 애니메이션으로 표현할 수 있다.</p>

<p><br /></p>

<ul>
  <li>화면 왼쪽에 위젯 배치</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.ConstraintLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_marginStart=</span><span class="s">"8dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span>
        <span class="na">app:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintStart_toStartOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.ConstraintLayout&gt;</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-12-motionlayout/4.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br /></p>
<ul>
  <li>화면 오른쪽에 위젯 배치</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.ConstraintLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_marginEnd=</span><span class="s">"8dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span>
        <span class="na">app:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintEnd_toEndOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.ConstraintLayout&gt;</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-12-motionlayout/5.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br />
ConstraintLayout을 사용하면 두 개의 레이아웃을 ConstraintSet을 이용하여 서로 전환할 수 있습니다. TransitionManager를 사용하는 경우 전환 애니메이션으로 표시됩니다. <strong>이 방식의 문제는 전환이 시작되면 중도에 중단할 수 없다는 것에 있습니다. 전환시 특정 지점으로 이동하도록 지원하지 않기 때문에 사용자와의 상호작용할 수 없습니다.</strong></p>

<p><br />
MotionLayout은 이런 모든 문제를 해결 할 수 있습니다. 이를 위해 기존의 2개의 레이아웃을 이용하여 MotionLayout을 통해 자동으로 초기화 하는 방식으로 바꿔보겠습니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">android:id=</span><span class="s">"@+id/motionLayout"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span>
    <span class="na">app:layoutDescription=</span><span class="s">"@xml/scene_01"</span>
    <span class="na">tools:showPaths=</span><span class="s">"true"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.motion.MotionLayout&gt;</span>
</code></pre></div></div>
<p><br /></p>

<p>레이아웃 <code class="language-plaintext highlighter-rouge">inflate</code>시 MotionScene은 <code class="language-plaintext highlighter-rouge">@xml/scene_01</code>파일을 참조하게 됩니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;MotionScene</span>
    <span class="na">xmlns:motion=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;Transition</span>
        <span class="na">motion:constraintSetStart=</span><span class="s">"@layout/motion_01_cl_start"</span>
        <span class="na">motion:constraintSetEnd=</span><span class="s">"@layout/motion_01_cl_end"</span>
        <span class="na">motion:duration=</span><span class="s">"1000"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;OnSwipe</span>
            <span class="na">motion:touchAnchorId=</span><span class="s">"@+id/button"</span>
            <span class="na">motion:touchAnchorSide=</span><span class="s">"right"</span>
            <span class="na">motion:dragDirection=</span><span class="s">"dragRight"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/Transition&gt;</span>

<span class="nt">&lt;/MotionScene&gt;</span>
</code></pre></div></div>

<p><br />
<code class="language-plaintext highlighter-rouge">scene_01</code>은 전환에 사용할 ConstraintSet 시작(<code class="language-plaintext highlighter-rouge">motion_01_cl_start</code>)/종료(<code class="language-plaintext highlighter-rouge">motion_01_cl_end</code>) 레이아웃을 지정할 수 있습니다. 또한 핵심인 <code class="language-plaintext highlighter-rouge">onSwipe</code>는 전환에 있어 핸들러를 지정했음을 주목해야합니다.</p>

<p><br /></p>
<h3 id="onswipe">OnSwipe</h3>
<p><code class="language-plaintext highlighter-rouge">OnSwipe</code> 핸들러는 사용자의 액션이 일치되는 경우 전환을 시작합니다.</p>

<p><strong>주요 속성</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">touchAnchorId</code> 추적해야 할 대상 (<code class="language-plaintext highlighter-rouge">@+id/button</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">touchAnchorSide</code> 손가락을 추적해야하는 물체의 측면 (<code class="language-plaintext highlighter-rouge">right/left/top/bottom</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">dragDirection</code> 추적중인 동작의 방향 (<code class="language-plaintext highlighter-rouge">dragRight/dragLeft/dragUp/dragDown</code>)</li>
</ul>

<p><br /></p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-12-motionlayout/3.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>이제 코드 한 줄 없이 사용자의 스와이프 이벤트를 통해 네모박스가 자연스럽게 애니메이션되면서 끝에서 끝으로 이동 합니다.</p>

<p><br /></p>
<h2 id="motionlayout-자체로-motionscene-구현">MotionLayout 자체로 MotionScene 구현</h2>
<p>위의 예제 ConstraintSet을 사용하여 2개의 레이아웃으로 전환하였으나 이번에는 MotionLayout으로만 사용하여 빠르게 레이아웃을 수성하도록 해보겠습니다.</p>

<p>MotionLayout은 <code class="language-plaintext highlighter-rouge">res/xml</code> 디렉토리에 있는 MotionScene 파일에서 ConstraintSet을 직접 설정하는 기능을 지원합니다. 이는 ConstraintSet을 사용하기 위한 다양한 레이아웃 생성을 하지 않아도 되는 장점이 있습니다. 이는 코드와 별도로 동작하기 때문에 나중에 Android Studio에서 모션레이아웃 뷰어를 지원할 가능성이 높습니다.</p>

<p><br /></p>
<h3 id="motionlayout의-속성">MotionLayout의 속성</h3>
<ul>
  <li>alpha</li>
  <li>visibility</li>
  <li>elevation</li>
  <li>rotation, rotation[X/Y]</li>
  <li>translation[X/ Y/Z]</li>
  <li>scaleX/Y</li>
</ul>

<p><br />
첫 번째 예제에서 사용한 ConstraintLayout 대신 MotionLayout으로 레이아웃을 다시 구성했다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span>
    <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:id=</span><span class="s">"@+id/motionLayout"</span>
    <span class="na">app:layoutDescription=</span><span class="s">"@xml/scene_02"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.motion.MotionLayout&gt;</span>
</code></pre></div></div>

<p>첫 번째 예제에서는 <code class="language-plaintext highlighter-rouge">constraintSetStart</code>와 <code class="language-plaintext highlighter-rouge">constraintSetEnd</code>에 <code class="language-plaintext highlighter-rouge">@layout</code>을 지정했지만, 여기서는 MotionScene 안에 정의한 ConstraintSet의 <code class="language-plaintext highlighter-rouge">@id</code>를 지정한다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;MotionScene</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:motion=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;Transition</span>
        <span class="na">motion:constraintSetStart=</span><span class="s">"@+id/start"</span>
        <span class="na">motion:constraintSetEnd=</span><span class="s">"@+id/end"</span>
        <span class="na">motion:duration=</span><span class="s">"1000"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;OnSwipe</span>
            <span class="na">motion:touchAnchorId=</span><span class="s">"@+id/button"</span>
            <span class="na">motion:touchAnchorSide=</span><span class="s">"right"</span>
            <span class="na">motion:dragDirection=</span><span class="s">"dragRight"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/Transition&gt;</span>

    <span class="nt">&lt;ConstraintSet</span> <span class="na">android:id=</span><span class="s">"@+id/start"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Constraint</span>
            <span class="na">android:id=</span><span class="s">"@+id/button"</span>
            <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_marginStart=</span><span class="s">"8dp"</span>
            <span class="na">motion:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintStart_toStartOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/ConstraintSet&gt;</span>

    <span class="nt">&lt;ConstraintSet</span> <span class="na">android:id=</span><span class="s">"@+id/end"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Constraint</span>
            <span class="na">android:id=</span><span class="s">"@+id/button"</span>
            <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_marginEnd=</span><span class="s">"8dp"</span>
            <span class="na">motion:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintEnd_toEndOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/ConstraintSet&gt;</span>

<span class="nt">&lt;/MotionScene&gt;</span>
</code></pre></div></div>

<p>기존의 위젯은 Constraint로 모두 대체합니다. 속성 변경을 원하는 위젯만 Constraint를 지정해야 적용가능 합니다. 만약 10개의 위젯이 있는 레이아웃일 경우 2개만 애니메이션 처리하고 싶은 경우 MotionScene의 ConstraintSet은 해당 위젯에 대한 Constraint를 지정하면 됩니다.</p>

<p><br /></p>
<h3 id="motionlayout-속성">MotionLayout 속성</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">app:layoutDescription="reference"</code> 예제에서 본것 처럼 MotionScene xml파일을 지정해야 합니다.</li>
  <li><code class="language-plaintext highlighter-rouge">app:applyMotionScene="boolean"</code> MotionScene을 적용할지 여부를 지정합니다.</li>
  <li><code class="language-plaintext highlighter-rouge">app:showPaths="boolean"</code> 동작 경로를 표시합니다. (기본값= false). 릴리즈 빌드시 끄는것을 잊지마세요.</li>
  <li><code class="language-plaintext highlighter-rouge">app:progress="float"</code> 특정 위치의 진행 장면으로 이동합니다. (0~1)</li>
  <li><code class="language-plaintext highlighter-rouge">app:currentState="reference"</code> 레이아웃 초기화시 특정 ConstraintSet을 강제로 실행합니다.</li>
</ul>

<p><br />
이제 한 줄의 코드 없이 사용자 이벤트를 통한 상호작용과 부드러운 애니메이션을 처리를 할 수 있습니다.<br />
위의 모든 예제 코드는 <a href="https://github.com/googlesamples/android-ConstraintLayoutExamples">GitHub</a>를 통해 보실 수 있습니다.</p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><summary type="html"><![CDATA[ConstraintSet을 사용하면 두 레이아웃 사이의 크기와 위치 변화를 애니메이션으로 표현할 수 있다. 이 아이디어를 확장한 MotionLayout으로 복잡한 화면 전환을 XML에 선언하는 방법을 살펴본다.]]></summary></entry><entry><title type="html">Android에서 키보드 전환을 자연스럽게 처리하기</title><link href="https://kmshack.kr/blog/smoothkeyboard/" rel="alternate" type="text/html" title="Android에서 키보드 전환을 자연스럽게 처리하기" /><published>2018-10-09T00:00:00+09:00</published><updated>2018-10-09T00:00:00+09:00</updated><id>https://kmshack.kr/blog/smoothkeyboard</id><content type="html" xml:base="https://kmshack.kr/blog/smoothkeyboard/"><![CDATA[<p><a href="https://play.google.com/apps/testing/me.saket.dank">Dank</a> 앱은 키보드가 나타나고 사라질 때 콘텐츠 크기와 스크롤을 자연스럽게 조절한다. 기본 동작에서 생기는 갑작스러운 레이아웃 변화를 줄이고 부드럽게 전환하는 원리를 살펴본다.</p>

<p>소프트키보드가 표시되면 안드로이드는 콘텐츠의 크기를 즉각 변경합니다. (<code class="language-plaintext highlighter-rouge">windowSoftInputMode</code>를 설정하지 않은경우) 이로 인해 레이아웃에 부자연스러운 변화가 생깁니다. 아쉽게도 플랫폼이나 대다수의 앱이 이를 처리하지 않기 때문에 사용자들은 정상적으로 작동한다는것으로 착각하고 있습니다.</p>

<p>역시 글로만 봐서 이해하기 힘들기때문에 차이점을 명확히 알아보기 위해 Google Keep과 Dank 앱을 영상으로 비교 해보겠습니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-09-smooth_keyboard/1.gif" alt="" class="center-image" /> Google Keep</td>
    </tr>
  </tbody>
</table>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-09-smooth_keyboard/2.gif" alt="" class="center-image" /> Dank</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p>화면을 비교하면 Dank 앱은 키보드가 나타날 때 콘텐츠 크기와 스크롤 위치를 부드럽게 조절한다. 반면 당시 Google Keep은 키보드 전환에 맞춘 별도의 애니메이션이 없어 콘텐츠 영역이 갑자기 바뀌어 보인다.</p>

<p>좀 더 부드러운 화면으로 비교를 원한다면 영상으로 확인해보세요.( <a href="/blog/images/2018-10-09-smooth_keyboard/keep_keyboard_720p.mp4">Google Keep</a>, <a href="/blog/images/2018-10-09-smooth_keyboard/dank_keyboard_720p.mp4">Dank</a>)</p>

<p><br /></p>

<p>눈치 빠른 분들은 Dank앱이 처리하는 작은 트릭을 발견하실 수 있습니다. 이 작은 트릭은 키보드가 표시되면 Activity의 전체 화면의 크기가 조정된다는 것에 있습니다. 이는 Activity를 구성하는 View 계층의 최상단 레이아웃(<code class="language-plaintext highlighter-rouge">DecorView</code>)에 아무런 영향을 받지 않고 처리 했다는 사실을 알 수 있습니다. 실제로 뷰의 크기가 저장되는 레이아웃ID는 콘텐츠 레이아웃(<code class="language-plaintext highlighter-rouge">android.R.id.content</code>)입니다. 이것은 <code class="language-plaintext highlighter-rouge">DecorView</code>내의 콘텐츠 레이아웃을 제어할 수 있다는 것을 의미합니다.</p>

<p>Activity를 구성하는 View Tree는 일반적으로 다음과 같습니다.</p>

<p><strong>DecorView</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">-</span> <span class="nc">LinearLayout</span>
<span class="o">--</span> <span class="nc">FrameLayout</span>  <span class="o">&lt;-</span> <span class="n">android</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">content</span>
<span class="o">---</span> <span class="nc">LinearLayout</span>
<span class="o">----</span> <span class="nc">Activity</span> <span class="n">content</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-09-smooth_keyboard/3.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>리사이즈 되는 부분을 제어하기 위해 콘텐츠 레이아웃(android.R.id.content)의 크기가 변경되는 것을 감지하는 유틸 클래스를 만들었습니다. View의 크기가 변경되면 콘텐츠의 전체 높이에서 변경된 높이만큼 변경되도록 애니메이션처리 합니다.</p>

<p><br /></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">decorView</span> <span class="o">=</span> <span class="n">activity</span><span class="o">.</span><span class="na">window</span><span class="o">.</span><span class="na">decorView</span>

<span class="n">decorView</span><span class="o">.</span><span class="na">viewTreeObserver</span><span class="o">.</span><span class="na">addOnPreDrawListener</span> <span class="o">{</span> 
  <span class="n">val</span> <span class="n">contentHeight</span> <span class="o">=</span> <span class="n">contentViewFrame</span><span class="o">.</span><span class="na">height</span>
  <span class="n">val</span> <span class="n">sizeChanged</span> <span class="o">=</span> <span class="n">contentHeight</span> <span class="o">!=</span> <span class="n">previousHeight</span>

  <span class="nf">if</span> <span class="o">(</span><span class="n">sizeChanged</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">animateSizeChange</span><span class="o">(</span><span class="n">from</span> <span class="o">=</span> <span class="n">previousHeight</span><span class="o">,</span> <span class="n">to</span> <span class="o">=</span> <span class="n">contentHeight</span><span class="o">)</span>
  <span class="o">}</span>

  <span class="n">previousHeight</span> <span class="o">=</span> <span class="n">contentHeight</span>
<span class="o">}</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/blog/images/2018-10-09-smooth_keyboard/4.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">animateSizeChange</span><span class="o">(</span><span class="nl">from:</span> <span class="nc">Int</span><span class="o">,</span> <span class="nl">to:</span> <span class="nc">Int</span><span class="o">)</span> <span class="o">{</span>
  <span class="c1">// Immediately snap back to the original size.</span>
  <span class="n">contentView</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">from</span><span class="o">)</span>
  
  <span class="nc">ObjectAnimator</span><span class="o">.</span><span class="na">ofInt</span><span class="o">(</span><span class="n">from</span><span class="o">,</span> <span class="n">to</span><span class="o">)</span>
    <span class="o">.</span><span class="na">addUpdateListener</span> <span class="o">{</span> 
      <span class="n">val</span> <span class="n">h</span> <span class="o">=</span> <span class="n">it</span><span class="o">.</span><span class="na">animatedValue</span> <span class="n">as</span> <span class="nc">Int</span>
      <span class="n">contentView</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">h</span><span class="o">)</span> 
    <span class="o">}</span>
    <span class="o">.</span><span class="na">start</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이와 관련된 모든 코드는 <a href="https://github.com/saket/FluidKeyboardResize">여기</a>를 통해 확인하세요.</p>

<p><br /></p>

<p>콘텐츠 뷰의 크기에 따라 애니메이션처리를 하여 높이를 변경하는 것은 손이 많이 가는일이지만 앱의 품질을 높이는 큰역할을 합니다. 다르게 구현할 수 있는 방법도 많고, 이것이 최상의 솔루션은 아니지만 여러달 테스트하면서 문제점을 발견하지는 못하였습니다.</p>

<p><br />
<br />
참고: <a href="https://saket.me/smoothly-reacting-to-keyboard/">https://saket.me/smoothly-reacting-to-keyboard/</a></p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><summary type="html"><![CDATA[Dank 앱은 키보드가 나타나고 사라질 때 콘텐츠 크기와 스크롤을 자연스럽게 조절한다. 기본 동작에서 생기는 갑작스러운 레이아웃 변화를 줄이고 부드럽게 전환하는 원리를 살펴본다.]]></summary></entry><entry><title type="html">ProcessLifecycleOwner로 앱의 Background/Foreground 상태 감지하기</title><link href="https://kmshack.kr/blog/ProcessLifecycleOwner%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%B1-Background,-Foreground-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC/" rel="alternate" type="text/html" title="ProcessLifecycleOwner로 앱의 Background/Foreground 상태 감지하기" /><published>2018-06-13T00:00:00+09:00</published><updated>2018-06-13T00:00:00+09:00</updated><id>https://kmshack.kr/blog/ProcessLifecycleOwner%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%20%EC%95%B1%20Background,%20Foreground%20%EC%9D%B4%EB%B2%A4%ED%8A%B8%20%EC%B2%98%EB%A6%AC</id><content type="html" xml:base="https://kmshack.kr/blog/ProcessLifecycleOwner%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%B1-Background,-Foreground-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC/"><![CDATA[<p>Android 앱 전체가 Background 또는 Foreground 상태로 전환되는 시점을 감지해야 할 때가 있다. 각 Activity의 Lifecycle을 따로 추적하는 대신 <code class="language-plaintext highlighter-rouge">ProcessLifecycleOwner</code>를 사용해 애플리케이션 수준의 상태 변화를 관찰하는 방법을 살펴본다.</p>

<p>Architecture Components를 아직 사용하지 않고 있다면 build.gradle에 아래와 같이 라이브러리를 추가 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies{
  kapt "android.arch.lifecycle:compiler:1.1.1"
  implementation "android.arch.lifecycle:extensions:1.1.1"
}
</code></pre></div></div>

<p>Activity(or Fragment)의 Lifecycle 상태를 모니터링할 때 사용하는 LifecycleObserver 인터페이스를 동일하게 하나 구현합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AppLifecycleObserver</span> <span class="o">:</span> <span class="nc">LifecycleObserver</span> <span class="o">{</span>

    <span class="nd">@OnLifecycleEvent</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">Event</span><span class="o">.</span><span class="na">ON_START</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">onForeground</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">//foreground</span>
    <span class="o">}</span>

    <span class="nd">@OnLifecycleEvent</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">Event</span><span class="o">.</span><span class="na">ON_STOP</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">onBackground</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">//background</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>구현된 인터페이스를 Activity가 아닌 Application의 onCreate()에 ProcessLifecycleOwner를 이용하여 Observer하도록 합니다. ProcessLifecycleOwner는 내부적으로 Activity의 개수및 Lifecycle 상태를 모니터링 하는 역할을 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AppApplication</span> <span class="o">:</span> <span class="nc">Application</span><span class="o">()</span> <span class="o">{</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onCreate</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">()</span>

        <span class="nc">ProcessLifecycleOwner</span><span class="o">.</span><span class="na">get</span><span class="o">().</span><span class="na">lifecycle</span>
            <span class="o">.</span><span class="na">addObserver</span><span class="o">(</span><span class="nc">AppLifecycleObserver</span><span class="o">())</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ON_START</code>는 앱이 Foreground로 진입했음을, <code class="language-plaintext highlighter-rouge">ON_STOP</code>은 Background로 전환되었음을 의미한다. ProcessLifecycleOwner는 여러 Activity의 Lifecycle을 종합해 이벤트를 전달하므로 밀리초 단위의 즉각적인 상태 감지가 필요한 용도에는 적합하지 않을 수 있다.
ProcessLifecycleOwner에 대한 더 많은 정보는 <a href="https://developer.android.com/reference/android/arch/lifecycle/ProcessLifecycleOwner">안드로이드 개발문서</a>를 참고 해주세요.</p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><category term="아키텍처" /><summary type="html"><![CDATA[Android 앱 전체가 Background 또는 Foreground 상태로 전환되는 시점을 감지해야 할 때가 있다. 각 Activity의 Lifecycle을 따로 추적하는 대신 ProcessLifecycleOwner를 사용해 애플리케이션 수준의 상태 변화를 관찰하는 방법을 살펴본다.]]></summary></entry><entry><title type="html">Kotlin Coroutines로 Retrofit2 요청 동시에 처리하기</title><link href="https://kmshack.kr/blog/Kotlin-Coroutines-Retrofit2-+-Coroutines-%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC/" rel="alternate" type="text/html" title="Kotlin Coroutines로 Retrofit2 요청 동시에 처리하기" /><published>2018-05-12T00:00:00+09:00</published><updated>2018-05-12T00:00:00+09:00</updated><id>https://kmshack.kr/blog/Kotlin%20Coroutines%20%E2%80%93%20Retrofit2%20+%20Coroutines%20%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC</id><content type="html" xml:base="https://kmshack.kr/blog/Kotlin-Coroutines-Retrofit2-+-Coroutines-%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC/"><![CDATA[<p>Android의 UI 스레드에서 네트워크 요청, 데이터베이스 쿼리나 무거운 연산을 수행하면 화면이 멈추고 ANR이 발생할 수 있다. Kotlin Coroutines를 사용해 여러 Retrofit2 요청을 동시에 처리하고 결과를 조합하는 방법을 살펴본다.</p>

<p><br /></p>
<h2 id="우리는-이런-문제를-해결-하기-위해-아래와-같은-몇-가지-방식을-사용합니다">우리는 이런 문제를 해결 하기 위해 아래와 같은 몇 가지 방식을 사용합니다.</h2>
<ol>
  <li>AsyncTask – 순서대로 짧은 연산 실행, 동시에 수행할 수 있음</li>
  <li>Executors – 고정된 스레드 풀과 동시에 작업 실행</li>
  <li>Intent Service – 순서대로 긴 연산을 실행, 큐잉 처리함</li>
  <li>RxJava – 가장 인기 있으며, 안드로이드 프레임워크에서 지원하지 않음</li>
  <li>Raw Threads</li>
</ol>

<p>위의 방식은 훌륭하지만 올바르게 사용하기 위해선 미묘한 차이를 보입니다. 또, 디버깅과 취소처리가 간단하지 않습니다.</p>

<p><br /></p>
<h2 id="kotlin-coroutines">Kotlin Coroutines</h2>

<p>코틀린 코루틴는 순차적으로 일어나는 일을 비동기 처리하기 위한 방법입니다. 코루틴을 생성하는 것은 스레드를 만드는 것에 비해 비용이 적게듭니다. 그 이유는</p>
<blockquote>
  <p>코루틴은 컴파일러의 코드 변환으로 구현되므로 VM이나 OS의 별도 지원이 필요하지 않다. suspend 함수도 이 변환을 통해 동작한다.</p>
</blockquote>

<p>코루틴은 여전히 실험단계에 있으며, 이는 API가 앞으로 변화될 가능성을 의미합니다. 하지만 JetBrains은 이전 버전과 같은 호환성을 제공할것 이라고 약속했습니다. (<a href="https://stackoverflow.com/questions/46240236/can-experimental-kotlin-coroutines-be-used-in-production">여기를 통해 확인 해보세요.</a>)</p>

<p><br /></p>
<h2 id="code">Code</h2>

<p>코루틴을 사용하기 위해 함수를 suspend로 표시해야 모든 정상기능을 사용할 수 있습니다. 이러한 기능을 사용하기 위해 실행및 비동기화(Launch &amp; Async)하는 코루틴 빌더가 필요합니다.</p>

<p><br /></p>
<h2 id="launch--async">Launch &amp; Async</h2>

<ul>
  <li>Launch – 현재 스레드를 차단하지 않고 새로운 코루틴을 실행하고 코루틴을 제거하는데 사용할 수 있는 작업으로 코루틴에 대한 참조를 반환합니다.</li>
  <li>Async – 새로운 코루틴을 실행하고 지연후 결과를 반환합니다.</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">actual</span> <span class="n">fun</span> <span class="nf">launch</span><span class="o">(</span>
    <span class="nl">context:</span> <span class="nc">CoroutineContext</span> <span class="o">=</span> <span class="nc">DefaultDispatcher</span><span class="o">,</span>
    <span class="nl">start:</span> <span class="nc">CoroutineStart</span> <span class="o">=</span> <span class="nc">CoroutineStart</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">,</span>
    <span class="nl">parent:</span> <span class="nc">Job</span><span class="o">?</span> <span class="o">=</span> <span class="kc">null</span><span class="o">,</span>
    <span class="nl">block:</span> <span class="n">suspend</span> <span class="nc">CoroutineScope</span><span class="o">.()</span> <span class="o">-&gt;</span> <span class="nc">Unit</span>
<span class="o">):</span> <span class="nc">Job</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">newContext</span> <span class="o">=</span> <span class="n">newCoroutineContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">parent</span><span class="o">)</span>
    <span class="n">val</span> <span class="n">coroutine</span> <span class="o">=</span> <span class="k">if</span> <span class="o">(</span><span class="n">start</span><span class="o">.</span><span class="na">isLazy</span><span class="o">)</span>
        <span class="nc">LazyStandaloneCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span> <span class="k">else</span>
        <span class="nc">StandaloneCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">active</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="n">coroutine</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">coroutine</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span>
    <span class="k">return</span> <span class="n">coroutine</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="n">actual</span> <span class="n">fun</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">async</span><span class="o">(</span>
    <span class="nl">context:</span> <span class="nc">CoroutineContext</span> <span class="o">=</span> <span class="nc">DefaultDispatcher</span><span class="o">,</span>
    <span class="nl">start:</span> <span class="nc">CoroutineStart</span> <span class="o">=</span> <span class="nc">CoroutineStart</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">,</span>
    <span class="nl">parent:</span> <span class="nc">Job</span><span class="o">?</span> <span class="o">=</span> <span class="kc">null</span><span class="o">,</span>
    <span class="nl">block:</span> <span class="n">suspend</span> <span class="nc">CoroutineScope</span><span class="o">.()</span> <span class="o">-&gt;</span> <span class="no">T</span>
<span class="o">):</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">newContext</span> <span class="o">=</span> <span class="n">newCoroutineContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">parent</span><span class="o">)</span>
    <span class="n">val</span> <span class="n">coroutine</span> <span class="o">=</span> <span class="k">if</span> <span class="o">(</span><span class="n">start</span><span class="o">.</span><span class="na">isLazy</span><span class="o">)</span>
        <span class="nc">LazyDeferredCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span> <span class="k">else</span>
        <span class="nc">DeferredCoroutine</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">active</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="n">coroutine</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">coroutine</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span>
    <span class="k">return</span> <span class="n">coroutine</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>
<ul>
  <li>CoroutineContext – 코루틴 빌더가 실행되는 스레드를 정의합니다.</li>
  <li>DefaultDispatcher – CommonPool을 사용합니다. 개발자는 실행해야하는 스레드를 지정할 수 있는 모든 권한을가집니다. 다른 옵션은 UI 스레드에서 실행되는 UI입니다.</li>
  <li>CoroutineStart – 시작 방법 정의
    <ul>
      <li>DEFAULT - 즉시 실행을 시작</li>
      <li>LAZY - 코루틴을 느리게 시작</li>
      <li>ATOMIC - 자동으로 최적화된 방법으로 시작</li>
      <li>UNDISPATCHED - 분산 처리방법으로 시작</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<h3 id="예제">예제</h3>

<p><strong>연속적으로 처리하는 작업</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">private</span> <span class="n">lateinit</span> <span class="kt">var</span> <span class="nl">job1:</span> <span class="nc">Job</span>
    <span class="n">suspend</span> <span class="n">fun</span> <span class="nf">getThingsDone</span><span class="o">(</span><span class="nl">myThings:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
        <span class="c1">// Heavy computation going on here :D</span>
        <span class="k">return</span> <span class="n">myThings</span> <span class="o">+</span> <span class="mi">99</span>
    <span class="o">}</span>

    <span class="n">suspend</span> <span class="n">fun</span> <span class="nf">getThingsDoneAgain</span><span class="o">(</span><span class="nl">myThings:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
        <span class="c1">// Heavy computation going on here :D</span>
        <span class="k">return</span> <span class="n">myThings</span> <span class="o">+</span> <span class="mi">50</span>
    <span class="o">}</span>
    
    <span class="n">fun</span> <span class="nf">toDoWorkSerially</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">job1</span> <span class="o">=</span> <span class="n">launch</span><span class="o">(</span><span class="no">UI</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">work1</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDone</span><span class="o">(</span><span class="mi">23</span><span class="o">)</span> <span class="o">}</span>
                <span class="n">val</span> <span class="n">work2</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDoneAgain</span><span class="o">(</span><span class="mi">55</span><span class="o">)</span> <span class="o">}</span>
                
                <span class="n">val</span> <span class="n">result1</span> <span class="o">=</span> <span class="n">work1</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="n">tvResult1</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result1</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
                
                <span class="n">val</span> <span class="n">result2</span> <span class="o">=</span> <span class="n">work2</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="n">tvResult2</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result2</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
            
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">exception</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">()</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>UI (Main Thread)는 Context를 사용하여 새로운  코루틴 Launch를 만듭니다. 이 과정에서 백그라운드에서 무거운 작업을 수행하고 UI 스레드에서 두 개의 다른 코루틴을 실행합니다. Async는 새로운 코루틴을 생성하고 지연된것을 리턴하여 기다릴 수 있게 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">work1</code>과 <code class="language-plaintext highlighter-rouge">work2</code>는 순서대로 실행된다. <code class="language-plaintext highlighter-rouge">result1</code>은 <code class="language-plaintext highlighter-rouge">work1</code>이 끝날 때까지 기다리고, 이어서 <code class="language-plaintext highlighter-rouge">result2</code>는 <code class="language-plaintext highlighter-rouge">work2</code>가 끝날 때까지 기다린다. 두 작업은 <code class="language-plaintext highlighter-rouge">CommonPool</code>을 Context로 사용하므로 백그라운드 스레드에서 실행되며, 예외는 <code class="language-plaintext highlighter-rouge">try/catch</code> 블록에서 처리한다.</p>

<p><br />
<strong>동시에 처리 하는 작업</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="n">lateinit</span> <span class="kt">var</span> <span class="nl">job2:</span> <span class="nc">Job</span>

<span class="n">fun</span> <span class="nf">toDoWorkConcurrent</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">job2</span> <span class="o">=</span> <span class="n">launch</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">val</span> <span class="n">work1</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDone</span><span class="o">(</span><span class="mi">43</span><span class="o">)</span> <span class="o">}</span>
            <span class="n">val</span> <span class="n">work2</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDoneAgain</span><span class="o">(</span><span class="mi">123</span><span class="o">)</span> <span class="o">}</span>

            <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">computeResult</span><span class="o">(</span><span class="n">work1</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">work2</span><span class="o">.</span><span class="na">await</span><span class="o">())</span>

            <span class="n">withContext</span><span class="o">(</span><span class="no">UI</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">tvResult1</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
            <span class="o">}</span>

        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">exception</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">()</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="n">fun</span> <span class="nf">computeResult</span><span class="o">(</span><span class="nl">await:</span> <span class="nc">Int</span><span class="o">,</span> <span class="nl">await1:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">await</span> <span class="o">+</span> <span class="n">await1</span>
<span class="o">}</span>
    
</code></pre></div></div>

<p>두 작업의 결과는 <code class="language-plaintext highlighter-rouge">work1</code>과 <code class="language-plaintext highlighter-rouge">work2</code>가 모두 끝난 뒤 사용할 수 있다. <code class="language-plaintext highlighter-rouge">await()</code>를 호출하면 각 Deferred의 완료를 기다린다.</p>

<p>코루틴 취소는 Job에서 cancel()을 통해 간단히 호출할 수 있습니다.</p>

<p><br /></p>
<h2 id="retrofit2--coroutines">Retrofit2 + Coroutines</h2>
<p>코루틴을 Retrofit2와 함께 사용하기 위해서는 Retrofit2 Kotlin Coroutines Adapter 라이브러리를 이용하여 Service 메서드의 반환 유형으로 Deferred 타입을 사용할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">LoginRepository</span> <span class="nd">@Inject</span> <span class="n">constructor</span><span class="o">(</span><span class="kd">private</span> <span class="n">val</span> <span class="nl">apiService:</span> <span class="nc">APIService</span><span class="o">,</span> <span class="kd">private</span> <span class="n">val</span> <span class="nl">sharedPreferences:</span> <span class="nc">SharedPreferences</span><span class="o">)</span> <span class="o">{</span>

    <span class="n">fun</span> <span class="nf">getOTPForNumber</span><span class="o">(</span><span class="nl">phoneNumber:</span> <span class="nc">String</span><span class="o">):</span> <span class="nc">LiveData</span><span class="o">&lt;</span><span class="nc">OTPResult</span><span class="o">&gt;</span> <span class="o">{</span>
    
        <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="nc">MutableLiveData</span><span class="o">&lt;</span><span class="nc">OTPResult</span><span class="o">&gt;()</span>
        <span class="n">launch</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">request</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getOTPForNumber</span><span class="o">(</span><span class="n">phoneNumber</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">response</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">isSuccessful</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Success</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">message</span><span class="o">()))</span>
                <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                    <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Error</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">code</span><span class="o">(),</span> <span class="n">response</span><span class="o">.</span><span class="na">message</span><span class="o">()))</span>
                <span class="o">}</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Exception</span><span class="o">(</span><span class="n">exception</span><span class="o">))</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">loginResult</span>
    <span class="o">}</span>
    
   <span class="n">fun</span> <span class="nf">getMyThings</span><span class="o">(</span><span class="nl">date:</span> <span class="nc">String</span><span class="o">,</span> <span class="nl">meal:</span> <span class="nc">Triple</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">,</span> <span class="nc">Boolean</span><span class="o">&gt;):</span> <span class="nc">LiveData</span><span class="o">&lt;</span><span class="nc">DispatchesResult</span><span class="o">&gt;</span> <span class="o">{</span>

        <span class="n">val</span> <span class="n">dispatchResult</span> <span class="o">=</span> <span class="nc">MutableLiveData</span><span class="o">&lt;</span><span class="nc">DispatchesResult</span><span class="o">&gt;()</span>
        <span class="n">launch</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">task1</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getDispatches</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">date</span><span class="o">,</span> <span class="n">meal</span><span class="o">.</span><span class="na">first</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">task2</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getDispatches</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">date</span><span class="o">,</span> <span class="n">meal</span><span class="o">.</span><span class="na">second</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">task3</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">checkAppDetails</span><span class="o">()</span>
                <span class="n">checkResult</span><span class="o">(</span><span class="n">task1</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">task2</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">task3</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">dispatchResult</span><span class="o">)</span>

            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">dispatchResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">DispatchesResult</span><span class="o">.</span><span class="na">Exception</span><span class="o">(</span><span class="n">exception</span><span class="o">))</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">dispatchResult</span>
    <span class="o">}</span>

     <span class="kd">private</span> <span class="n">fun</span> <span class="nf">checkResult</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">task1</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="n">task2</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">task3</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="n">task3</span><span class="o">.</span><span class="na">body</span><span class="o">()?.</span><span class="na">code</span> <span class="o">==</span> <span class="mi">1000</span><span class="o">))</span> <span class="o">{</span>
        
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>apiService 인터페이스가 기존 Call대신 Deferred를 반환하기 때문에 이를 통해 이전에 다룬 코루틴을 그대로 사용할 수 있습니다.</p>

<p><br /></p>
<h2 id="결론">결론</h2>
<p>코루틴을 사용하면 코드를 쉽게 읽을 수 있는 비동기 코드 작성이 가능하며, 오류및 유지 보수를 줄일 수 있습니다. 또한 코드의 경량화는 말할것도 없습니다.</p>]]></content><author><name>Minsoo Kim</name></author><category term="안드로이드" /><category term="코틀린" /><summary type="html"><![CDATA[Android의 UI 스레드에서 네트워크 요청, 데이터베이스 쿼리나 무거운 연산을 수행하면 화면이 멈추고 ANR이 발생할 수 있다. Kotlin Coroutines를 사용해 여러 Retrofit2 요청을 동시에 처리하고 결과를 조합하는 방법을 살펴본다.]]></summary></entry></feed>