<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>짜이한</title>
    <link>https://jja2han.tistory.com/</link>
    <description>안녕하세요  </description>
    <language>ko</language>
    <pubDate>Wed, 13 May 2026 21:28:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>재한</managingEditor>
    <image>
      <title>짜이한</title>
      <url>https://tistory1.daumcdn.net/tistory/5344822/attach/9e872310fcc846dab4cc54896b0c2ebc</url>
      <link>https://jja2han.tistory.com</link>
    </image>
    <item>
      <title>2025년 회고 - 개발자 커리어를 시작하다</title>
      <link>https://jja2han.tistory.com/517</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 글을 정말 오랜만에 쓰는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 개입되지 않는 글을 쓴다는게 이렇게 어색할 줄은 몰랐네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법도 틀리고, 두서도 없는 엉망진창인 글이겠지만 그게 또 사람의 매력이 아닐까 하고 글을 적어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나의 2025년은 어땠는가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;취업을 하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업에 대한 글을 그 당시 설레과 기뻤던 감정으로 썼던 기억이 있습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768629646332&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[공채후기] 2025 SOOP Android 신입 공채 합격 후기&quot; data-og-description=&quot;저는 Android 모바일 개발 직무에 지원했던 지원자입니다.2024년에 Android 개발 직무 지원 공고가 뜨지 않았고, 23년과 25년에 뜬것 보면 매년 채용 공고가 열리는 것 같지는 않다고 느꼈습니다.요즘 &quot; data-og-host=&quot;jja2han.tistory.com&quot; data-og-source-url=&quot;https://jja2han.tistory.com/513&quot; data-og-url=&quot;https://jja2han.tistory.com/513&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Aj9rt/dJMb82MwFSR/r0bmS0Hewho9V7zPS53SX0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bBMHCU/dJMb9fry5FI/ZJ64lZFNaNXvVGrsdakNZ0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/FKfWl/dJMb9fZoXz3/ow35Gp4KGVik5XIOtkAR2K/img.png?width=1280&amp;amp;height=878&amp;amp;face=0_0_1280_878&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/513&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jja2han.tistory.com/513&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Aj9rt/dJMb82MwFSR/r0bmS0Hewho9V7zPS53SX0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bBMHCU/dJMb9fry5FI/ZJ64lZFNaNXvVGrsdakNZ0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/FKfWl/dJMb9fZoXz3/ow35Gp4KGVik5XIOtkAR2K/img.png?width=1280&amp;amp;height=878&amp;amp;face=0_0_1280_878');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[공채후기] 2025 SOOP Android 신입 공채 합격 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;저는 Android 모바일 개발 직무에 지원했던 지원자입니다.2024년에 Android 개발 직무 지원 공고가 뜨지 않았고, 23년과 25년에 뜬것 보면 매년 채용 공고가 열리는 것 같지는 않다고 느꼈습니다.요즘&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jja2han.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;막연하게 개발자가 되고싶다!&lt;/b&gt;를 꿈꿨던 시절부터&amp;nbsp; Android 개발자를 꿈꿨고, 회사가 독자적인 서비스를 운영하는 그런 기업에 가고 싶었던 저에게는 굉장히 큰 행운이었다고 생각하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 어렸을때부터 친숙하고, 아직까지도 자주 사용하고, 애정이 있는 그런 기업에 들어가기란 쉽지는 않다고 생각하고 정말 운이 좋다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지인들의 말과 여러 포스팅만으로도 들었던 개발자의 삶은 제가 상상했던 개발자의 모습과 굉장히 일치했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지도 판교로 첫 출근하는 길은 생생하고 잊지 못할 경험이 될 것 같네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HcNHr/dJMcajuahmF/y3OLKxvC3utY4VpEh9HM6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HcNHr/dJMcajuahmF/y3OLKxvC3utY4VpEh9HM6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HcNHr/dJMcajuahmF/y3OLKxvC3utY4VpEh9HM6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHcNHr%2FdJMcajuahmF%2Fy3OLKxvC3utY4VpEh9HM6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;347&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사를 하고는 정말 한동안 바쁘게 지냈던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무인도에 던져져도 쉽게 적응할 수 있을것 같았던 저를 다시 되돌아보게 되는 그런 기간이었네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취준생의 시선에서 바라봤던 개발의 업무 프로세스, 협업의 자세, 구현 등 모든 것이 다시 재조립되는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 현직자와 취준생이 바라보는 시야가 다름을 느꼈고, 그 동안 제가 좁은 시선을 가지고 개발에 임했다는 사실을 깨달았을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 배울것이 많다는 설렘과 내가 했던 것을 회사에서 녹여낼 수 있을지에 대한 조급함이 공존한 복잡 미묘한 감정을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 또 인간은 적응의 동물이라고, 정신없이 작업을 쳐내고 시간이 지나다 보니 서서히 어떠한 개발자가 되는 게 좋은지에 대한 방향성이 조금은 잡혔다고 생각이 드는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 제가 개발해보고 싶었던 SOOP의 라이브 파트를 담당하게 되었고, 더 나은 서비스를 제공하기 위해 여러 노력을 하고 있는 2년차(?) 개발자가 되었네요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마라톤과 런닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막연하게 취업을 하기 전부터 마라톤에 나가고 싶다는 생각이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 2025년 완전히 런닝이 떠오르게 되었고, 마라톤도 당연히 떠오르게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연스레 주변에서도 런닝과 마라톤의 흥미가 있던 사람들이 생겼고 그 유행에 자연스레 편승하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인생 첫 마라톤으로 하프 마라톤을 나가게 되었는데요, 완주하게 되어서 정말 기뻤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하고 있는 일과 다른 성취감을 느낄 수 있어서 좋았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;3057&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ6vPB/dJMcagYxz4D/Y6WlCncVfS0aIN3rmy54M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ6vPB/dJMcagYxz4D/Y6WlCncVfS0aIN3rmy54M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ6vPB/dJMcagYxz4D/Y6WlCncVfS0aIN3rmy54M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ6vPB%2FdJMcagYxz4D%2FY6WlCncVfS0aIN3rmy54M0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;507&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;3057&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년에도 마라톤에 참가해서, 좋은 기록과 건강하고 재밌게 뛰고 싶네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;군대 가기 전까지는 방학마다 알바를 한 돈으로 해외여행을 다녔던 저는 여행을 정말 좋아했지만, 취업 준비 과정에서 이러한 욕구를 꾹 누르고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 해외여행 2번과 국내 여행 1번 다녀왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마음이 맞는 사람들과 낯선 공간의 문화와 장소를 경험하는 것은 정말 도움이 많이 되고, 즐거운 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckp6G9/dJMcadOeVgj/zJ9HkadK9fIsCYPsFTZbLK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckp6G9/dJMcadOeVgj/zJ9HkadK9fIsCYPsFTZbLK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-17-15-47-27.jpeg&quot; width=&quot;274&quot; height=&quot;365&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckp6G9/dJMcadOeVgj/zJ9HkadK9fIsCYPsFTZbLK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckp6G9%2FdJMcadOeVgj%2FzJ9HkadK9fIsCYPsFTZbLK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9RqUc/dJMcai25Y42/UBIY6EZ6kqhkTCT1YGSFN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9RqUc/dJMcai25Y42/UBIY6EZ6kqhkTCT1YGSFN0/img.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2026-01-17-15-47-43.jpeg&quot; width=&quot;416&quot; height=&quot;555&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9RqUc/dJMcai25Y42/UBIY6EZ6kqhkTCT1YGSFN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9RqUc%2FdJMcai25Y42%2FUBIY6EZ6kqhkTCT1YGSFN0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽은 나트랑, 오른쪽은 마쓰야마&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년에는 여럿이 아닌 혼자서 여행하는 경험도 해보고 싶은 것이 개인적인 목표입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년의 목표&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이러한 엔지니어로 성장하고 싶다.&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;팀의 생산성에 기여할 수 있는 개발자&lt;/li&gt;
&lt;li&gt;회사 서비스를 깊숙하게 이해하는 개발자&lt;/li&gt;
&lt;li&gt;AI에 휘둘리지 않고, AI를 도구로서 사용하는 개발자&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 부족한 엔지니어이기 때문에 회사 서비스에 한해서는 스페셜리스트가, 개발의 측면에서는 Android 뿐만 아니라 AI, 자동화 툴을 다룰 수 있는 제너럴 리스트가 되는 것이 목표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 목표가 개발자가 아닌 엔지이너라고 적은 것이기도 하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더욱 꼼꼼해지기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 그렇게 꼼꼼한 성격은 아니지만, 일에 대해서는 꼼꼼하다고 생각하고 자부하는 편이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 2025년 성과와 목표를 되돌아보니 정말 이렇게 밖에 할 수 없었을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 촘촘하고, 꼼꼼하게 설정할 수 없었을까라는 아쉬움이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 목표와 성과를 스스로 명확하게 작성하는 것도 중요하지만, 내가 했던 작업을 수치화, 추상화하는 능력도 중요하다는 점을 배울 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 더더욱 내가 하는 작업이 서비스에 어떠한 영향을 끼치고, 사용자들의 반응을 살피는 것을 생각하고, 고민해보려고 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;기술적인 발전 이루기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2025년을 강타한 KMP나, 현재 대부분 기업에서 사용하는 Andoird Compose, 스트리밍 기업인 만큼 ExoPlayer와 미디어 파이프라인을 공부하는 것이 목표가 되겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;블로그 글 포스팅 자주 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 정보 검색을 하는 방법이 기술 블로그 -&amp;gt; AI로 전환되었지만, 그래도 기술 블로그의 가치는 여전하다고 생각을 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 취업을 한 이후로, 글감이 없다는 핑계로 포스팅을 하지 않았었습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서는 문서화를 많이 하고 있지만, 내가 운영하는 개인 기술 블로그에 남길 수 있는 글을 많이 남기지 못한 것이 정말 아쉬웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 과정에서 배웠던 내용이나, 생각 등을 주기적으로 정리할 수 있으면 좋을 것 같네요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2025년의 아쉬웠던 점은 훌훌 털어버리고, 새롭고 설렘이 가득한 2026년이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;더 괜찮은 사람이 되도록 노력해보겠습니다!&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;234&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4n823/dJMcafeh1LR/cQqHwuTWaiNFzOxqucXJIK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4n823/dJMcafeh1LR/cQqHwuTWaiNFzOxqucXJIK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4n823/dJMcafeh1LR/cQqHwuTWaiNFzOxqucXJIK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4n823%2FdJMcafeh1LR%2FcQqHwuTWaiNFzOxqucXJIK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;600&quot; data-origin-width=&quot;234&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Experience/후기(코딩테스트,프로그램,프로젝트)</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/517</guid>
      <comments>https://jja2han.tistory.com/517#entry517comment</comments>
      <pubDate>Sat, 17 Jan 2026 15:50:35 +0900</pubDate>
    </item>
    <item>
      <title>복잡한 레이아웃을 아름답게 해결할 수 있는 방법(feat: SubComposeLayout)</title>
      <link>https://jja2han.tistory.com/516</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 글에는 Compose의 Layout에 대해서 다뤄봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 SubComposeLayout을 다루기 위해서 먼저 Layout을 다뤄본 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 SubComposeLayout을 이해하기 위해선 Compose의 Layout 과정을 숙지하고 있어야 합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1750572454334&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] - Jetpack Compose Layout&quot; data-og-description=&quot;취업 이후 첫 글인 것 같네요.개발 실력이 모자라 배우느라 바쁘기도 했지만, 개발 외적으로 회사의 프로세스를 적응하고 이해하는데 많은 시간을 쏟고 있는 요즘입니다. 최근엔 서서히 적응을 &quot; data-og-host=&quot;jja2han.tistory.com&quot; data-og-source-url=&quot;https://jja2han.tistory.com/514&quot; data-og-url=&quot;https://jja2han.tistory.com/514&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bDThfT/hyY7ZodJdX/iLoKUWlQ7qQJWpXL9uN1ik/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bePq8J/hyZcomdog5/sVIEDOc62vVk5UywS04p10/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/cla5KD/hyZbqLbIVn/KwhCui8bcQez8D8PPXQzy0/img.png?width=1340&amp;amp;height=898&amp;amp;face=0_0_1340_898&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/514&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jja2han.tistory.com/514&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bDThfT/hyY7ZodJdX/iLoKUWlQ7qQJWpXL9uN1ik/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bePq8J/hyZcomdog5/sVIEDOc62vVk5UywS04p10/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/cla5KD/hyZbqLbIVn/KwhCui8bcQez8D8PPXQzy0/img.png?width=1340&amp;amp;height=898&amp;amp;face=0_0_1340_898');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] - Jetpack Compose Layout&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;취업 이후 첫 글인 것 같네요.개발 실력이 모자라 배우느라 바쁘기도 했지만, 개발 외적으로 회사의 프로세스를 적응하고 이해하는데 많은 시간을 쏟고 있는 요즘입니다. 최근엔 서서히 적응을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jja2han.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 레이아웃은 기본 제공 컴포저블(Row, Box, Column)이나 Layout 함수만으로 충분히 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 하위 레이아웃의 크기나 상태에 따라 상위 레이아웃이 다시 하위 레이아웃을 구성해야 하는 복잡한 상황에선 한계가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 등장한 개념이 바로 &lt;b&gt;SubComposeLayout&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout과 SubComposeLayout의 차이를 살펴보기 전에 Layout에 대해 간단히 짚고 넘어가 보시죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout은 Compose UI 렌더링 과정(Composition -&amp;gt; Layout -&amp;gt; Draw)의 한 단계로,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJxIGv/btsOLASZrXV/cox6dOlXYo2aU1pi1dGBa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJxIGv/btsOLASZrXV/cox6dOlXYo2aU1pi1dGBa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJxIGv/btsOLASZrXV/cox6dOlXYo2aU1pi1dGBa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJxIGv%2FbtsOLASZrXV%2Fcox6dOlXYo2aU1pi1dGBa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;560&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;각 UI 요소의 크기를 측정하고 위치를 배포하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 디테일하게 파고 들어가자면 Layout 함수는 이 과정에서 하위 레이아웃의 크기를 먼저 측정한 뒤, 상위 레이아웃이 배치 결정을 내립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 하위 레이아웃의 크기나 상위 레이아웃의 제약조건에 따라 하위 레이아웃을 다시 그려야 하는 경우는 기존 Layout만으로는 한계가 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SubComposeLayout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SubComposeLayout은 Layout과 유사하지만, Measure 단계에서 별도의 Composition을 실행할 수 있는 Layout입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzUVO7/btsOMeV5VQp/76OrsoJOl1Dw6uzUvtDg7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzUVO7/btsOMeV5VQp/76OrsoJOl1Dw6uzUvtDg7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzUVO7/btsOMeV5VQp/76OrsoJOl1Dw6uzUvtDg7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzUVO7%2FbtsOMeV5VQp%2F76OrsoJOl1Dw6uzUvtDg7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1282&quot; height=&quot;508&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composition -&amp;gt; Layout -&amp;gt; Draw의 과정에서 별도의 Composition 단계가 끼어들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Layout에서는 Single Composition이지만, SubComposeLayout에서는 Layout의 Measure 단계에서 필요한 만큼 하위 레이아웃을 동적으로 구성할 수 있고, 상위 레이아웃의 제약조건이나 측정 결과에 따라 하위 레이아웃을 유연하게 바꿀 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 같은 컨텐츠를 여러 번 subcompose하여 각각 다른 조건으로 측정할 수 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이를 통해 일반 Layout에선 불가능한 측정 -&amp;gt; 분석 -&amp;gt; 재측정 패턴을 구현할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 SubcomposeLayout을 사용하는 특별한 경우를 살펴보죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BoxWithConstraints&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ndSVB/btsOLEgGEzt/on915kH7oykQnCPfpd9C0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ndSVB/btsOLEgGEzt/on915kH7oykQnCPfpd9C0k/img.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;968&quot; data-is-animation=&quot;false&quot; width=&quot;550&quot; height=&quot;345&quot; style=&quot;width: 61.5507%; margin-right: 10px;&quot; data-widthpercent=&quot;62.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ndSVB/btsOLEgGEzt/on915kH7oykQnCPfpd9C0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FndSVB%2FbtsOLEgGEzt%2Fon915kH7oykQnCPfpd9C0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1544&quot; height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIOOqF/btsOL4THnAz/CAA4J4B92edaj86BcprEjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIOOqF/btsOL4THnAz/CAA4J4B92edaj86BcprEjK/img.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;1126&quot; data-is-animation=&quot;false&quot; width=&quot;435&quot; height=&quot;450&quot; style=&quot;width: 37.2865%;&quot; data-widthpercent=&quot;37.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIOOqF/btsOL4THnAz/CAA4J4B92edaj86BcprEjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIOOqF%2FbtsOL4THnAz%2FCAA4J4B92edaj86BcprEjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1088&quot; height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;content&lt;/b&gt; 블럭은 &lt;b&gt;BoxWithConstraintScope&lt;/b&gt;를 이용하는데, 해당 스코프에선 제약사항과 최소, 최대 너비와 높이를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, BoxWithConstraints는 부모의 (최대/최소 크기) 정보를 자식 Composable에 제공하고, 자식의 UI를 동적으로 바꿀 수 있게 해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 SubcomposeLayout을 사용해 measure 단계에서 자식의 composition을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모의 제약조건이 결정된 뒤 자식의 composition이 구성되기에 자식은 부모 크기 정보를 바탕으로 UI를 그릴 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGHGgS/btsOLluHC3w/q0VhXgpkqup2zpN8c0Png0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGHGgS/btsOLluHC3w/q0VhXgpkqup2zpN8c0Png0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGHGgS/btsOLluHC3w/q0VhXgpkqup2zpN8c0Png0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGHGgS%2FbtsOLluHC3w%2Fq0VhXgpkqup2zpN8c0Png0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;421&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BoxWithConstraints가 배치될 공간의 constraints를 파악합니다.&lt;/li&gt;
&lt;li&gt;부모의 (최대/최소 정보)를 subcomposition에서 자식 composable에 전달&lt;/li&gt;
&lt;li&gt;자식 composable은 maxHeight 값을 읽어, 조건에 따른 composition을 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 요구사항은 Layout 함수로는 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 자식 트리가 이미 composition 된 이후 measure가 가능하기 때문에, 부모 크기에 따라 자식을 변경할 수 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계를 간단하게 요약하면 아래와 같은 그림이 되겠네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;1176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8RRxR/btsOL9m17ib/EI2Psw3IIinSOm9lTsnWo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8RRxR/btsOL9m17ib/EI2Psw3IIinSOm9lTsnWo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8RRxR/btsOL9m17ib/EI2Psw3IIinSOm9lTsnWo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8RRxR%2FbtsOL9m17ib%2FEI2Psw3IIinSOm9lTsnWo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;230&quot; height=&quot;397&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;1176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LazyRow&amp;nbsp; - 아이템 높이 통일하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 요구사항이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df1MlM/btsOL22J2dY/XlIrmw3ke9KeN8RrZ35iw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df1MlM/btsOL22J2dY/XlIrmw3ke9KeN8RrZ35iw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df1MlM/btsOL22J2dY/XlIrmw3ke9KeN8RrZ35iw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf1MlM%2FbtsOL22J2dY%2FXlIrmw3ke9KeN8RrZ35iw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;606&quot; height=&quot;284&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 높이로 합의가 되면 가장 이상적이지만,&lt;br /&gt;만약 합의가 되지 않는다면 우리는 &quot;&lt;b&gt;실제로 그리기 전까지 각 아이템의 정확한 높이 중 가장 큰 높이&quot;&lt;/b&gt;를 알아내야 하고,&lt;br /&gt;이 값을 기반으로 모든 아이템의 높이를 맞춰주는 커스텀 레이아웃을 구현해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyColumn/LazyRow 등 Compose의 기본 Lazy 리스트는 아이템의 실제 높이를 미리 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Layout 함수로 이를 구현하려고 하면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 아이템을 한 번 측정해서 최대 높이를 구함&lt;/li&gt;
&lt;li&gt;그 최대 높이로 모든 아이템을 다시 측정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 Compose에서는 같은 Measurable을 두 번 측정할 수 없으므로 불가능합니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이럴 경우 SubComposeLayout은 효과적인 해결책이 될 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정 단계에서 자식 composable을 여러 번 composition 하고 측정할 수 있기 때문입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 아이템을 한 번씩 측정해 개별 높이를 구함.&lt;/li&gt;
&lt;li&gt;그중 최대 높이를 찾음&lt;/li&gt;
&lt;li&gt;모든 아이템의 높이를 최대 높이로 재측정&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CXlQ9/btsOMpQDsNp/Ebt78btbHQVgW1nhLTSRyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CXlQ9/btsOMpQDsNp/Ebt78btbHQVgW1nhLTSRyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CXlQ9/btsOMpQDsNp/Ebt78btbHQVgW1nhLTSRyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCXlQ9%2FbtsOMpQDsNp%2FEbt78btbHQVgW1nhLTSRyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;461&quot; height=&quot;456&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 아이템들의 높이를 가장 큰 높이로 맞추기 위해선 각 아이템의 높이 중 최대 높이를 구해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, LazyRow의 아이템의 높이를 maxHeight로 맞춰주고 layout 단계에서 배치를 해주면 끝입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⛔️한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요하게 많은 subcompositon을 남발하면 &lt;b&gt;오버헤드가 발생&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 Layout 단계에서 &lt;b&gt;추가로, composition 단계&lt;/b&gt;가 생기다 보니 당연한 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 공식문서에서도,&lt;b&gt; SubcomposeLayout을 언제 사용해야 하고, 필요하지 않은 상황이 무엇&lt;/b&gt;인지&lt;b&gt; 분명하게 이해&lt;/b&gt;하는 것이 매우 중요하다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; 사용해야 하는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;자식의&amp;nbsp; composition이 다른 자식의 measure 결과에 의존하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BoxWithConstraints&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모의 제약 조건과 그 값에 따라 자식 컴포저블의 구조를 다르게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LazyColumn / LazyRow
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 필요한 아이템만 동적으로 compose 해야 합니다.&lt;/li&gt;
&lt;li&gt;이를 위해 SubComposeLayout을 사용해서 measure 단계에서 필요한 자식 composable을 Subcomposition&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;특정 자식 컴포저블의 measure 결과에 따라 다른 자식 composable의 표시 여부를 결정하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 자식의 높이가 일정 값 이상일 때만 두 번째 자식을 보여주거나, 다른 UI를 구성할 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 공식문서는 여기까지 얘기를 하고 있지만, 저는 하나의 경우가 더 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;자식의 measure가 다른 자식의 measure 결과에 따라 달라지는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우가 2-measure 문제를 해결하는 핵심 사용 사례입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 아이템을 가장 큰/작은 아이템 크기로 통일&lt;/li&gt;
&lt;li&gt;한 아이템의 크기를 기준으로 다른 아이템들 크기 조정&lt;/li&gt;
&lt;li&gt;특정 기준에 따른 동적 크기 조정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Layout에서는 single pass measure가 원칙이기 때문에,&amp;nbsp; 2-measure 패턴은 SubComposeLayout이 해결책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❌사용하면 안 되는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;한 자식 컴포저블의 측정 결과가 다른 컴포저블의 크기를 정하는 데 필요한 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 두 개의 Text중 더 긴 텍스트에 맞춰 둘 다 같은 높이로 만들고 싶은 요구사항이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkVW4M/btsONdcrMdi/uoea1emWWzzmHELDNaSGk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkVW4M/btsONdcrMdi/uoea1emWWzzmHELDNaSGk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkVW4M/btsONdcrMdi/uoea1emWWzzmHELDNaSGk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkVW4M%2FbtsONdcrMdi%2Fuoea1emWWzzmHELDNaSGk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;505&quot; height=&quot;318&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단순한 요구사항의 경우 SubComposeLayout이 아닌 일반적인 Layout을 사용해서 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에 측정 결과가 composition(UI 구성)이 아닌 measure에 영향을 끼친다면 굳이 SubComposeLayout을 사용하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 SubComposeLayout을 도입할 땐 여러 가지를 고려해야 한다고 생각합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일반적인 Layout으로 정말 불가능한가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식의 composition 자체가 다른 자식의 measure 결과에 의존을 하는지 혹은 single pass measure로 가능한지 고려해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성능 트레이드오프를 감수할 만한 가치가 있는가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 고려해야 할 사항이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 저하가 우려되지만, 사용자 경험상 꼭 필요한 기능인지, 다른 대안적 해결책은 정말 없는지 고려해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복잡도 대비 효과가 충분한가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SubComposeLayout을 사용함으로써 구현이 복잡해지고, 유지 보수 측면에서 좋지 않은지 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SubComposeLayout은 일반 Layout의 Single Pass Measure 제약을 우회하여, 복잡한 레이아웃 요구사항을 해결할 수 있게 해주는 강력한 도구이지만, 사용해야 하는 경우와 아닌 경우에 대해서 인지하고 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 단순히 더 &lt;b&gt;유연해 보인다&lt;/b&gt;거나 더 &lt;b&gt;고급 기술 같다&lt;/b&gt;는 이유로 남용해서는 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://foso.github.io/Jetpack-Compose-Playground/ui/layout/subcomposelayout/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://foso.github.io/Jetpack-Compose-Playground/ui/layout/subcomposelayout/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=l6rAoph5UgI&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=l6rAoph5UgI&lt;/a&gt;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>jetpack compose</category>
      <category>subcomposelayout</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/516</guid>
      <comments>https://jja2han.tistory.com/516#entry516comment</comments>
      <pubDate>Mon, 23 Jun 2025 02:29:30 +0900</pubDate>
    </item>
    <item>
      <title>Navigation 코드를 줄여보세요 (feat: Compose Destinations)</title>
      <link>https://jja2han.tistory.com/515</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose navigation을 활용해서 프로젝트를 진행 중인데요, 생각보다 보일러 플레이트 코드가 많이 발생하더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 어떻게 구조를 바꿀 수 있을까 하다가 KSP를 활용해서 &lt;b&gt;&lt;span style=&quot;background-color: oklch(0.99 0.004 106.471); color: oklch(0.304 0.04 213.681); text-align: left;&quot;&gt;Navigation&lt;/span&gt;&lt;/b&gt; 코드를 자동화하는 라이브러리를 찾게 되었고, 적용해 봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compose Destinations&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.99 0.004 106.471); color: oklch(0.304 0.04 213.681); text-align: left;&quot;&gt;Compose Destinations&lt;/span&gt;는 Jetpack Compose를 위한 Type-Safe Navigation을 제공하는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라이브러리에서 강조하는 점은 Compose Navigation의 복잡함과 보일러플레이트 코드를 크게 줄여준다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 Stable 한 버전은 1.9.50으로, 23년 10월 자 라이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749638353166&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Release 1.9.50 No longer beta!   No more accompanist-navigation!   &amp;middot; raamcosta/compose-destinations&quot; data-og-description=&quot;Removed accompanist navigation since animations were imported to official compose navigation!   Because of this, some minor changes had to be made: DestinationStyleAnimated -&amp;gt; DestinationStyle.Ani...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&quot; data-og-url=&quot;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bQSPA3/hyY8StPENM/TANIIRKhw5lx7qK2Mnj6C1/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234,https://scrap.kakaocdn.net/dn/bl8ha1/hyY8YgwGgD/Fl4THjvNnYjEpd39MCpc60/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234&quot;&gt;&lt;a href=&quot;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bQSPA3/hyY8StPENM/TANIIRKhw5lx7qK2Mnj6C1/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234,https://scrap.kakaocdn.net/dn/bl8ha1/hyY8YgwGgD/Fl4THjvNnYjEpd39MCpc60/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Release 1.9.50 No longer beta!   No more accompanist-navigation!   &amp;middot; raamcosta/compose-destinations&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Removed accompanist navigation since animations were imported to official compose navigation!   Because of this, some minor changes had to be made: DestinationStyleAnimated -&amp;gt; DestinationStyle.Ani...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 릴리즈가 5월 3일인데, 꽤 활발하게 운영되고 있는 라이브러리인 것 같다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(따끈따끈하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소개하고 있는 가장 큰 특징은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Destination 어노테이션의 navArgs에 대응되는 클래스를 선언하면 Type Safe한 네비게이션 인자를 지원한다.&lt;/li&gt;
&lt;li&gt;다양한 어노테이션을 통해 그래프를 쉽게 정의할 수 있다. (+ 중첩 그래프)&lt;/li&gt;
&lt;li&gt;화면 간 이동 후 결과가 Type-Safe하다!&lt;/li&gt;
&lt;li&gt;SavedStateHandle / BackStackEntry Type Safe 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;viewModel에서 네비게이션 인자를 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애니메이션, 바텀시트, 딥링크 등 쉽게 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공식 Compose Navigation의 모든 기능을 사용할 수 있다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트에 적용하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle &lt;b&gt;[project]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749638716173&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
   alias(libs.plugins.ksp) apply false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle &lt;b&gt;[app]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749638760061&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
   alias(libs.plugins.ksp)
}

dependencies {
    implementation(libs.compose.destinations.core)
    ksp(libs.compose.destinations.ksp)
    implementation(libs.compose.destinations.bottom.sheet)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 version은 프로젝트에서 사용되는 &lt;b&gt;compose&lt;/b&gt;의 버전에 따라 바뀝니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beI4FT/btsOv48bOdB/f9otFKM6FYG0X3kttFdbv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beI4FT/btsOv48bOdB/f9otFKM6FYG0X3kttFdbv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beI4FT/btsOv48bOdB/f9otFKM6FYG0X3kttFdbv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeI4FT%2FbtsOv48bOdB%2Ff9otFKM6FYG0X3kttFdbv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;625&quot; height=&quot;186&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose의 버전은 compose-bom 버전을 통해서 확인할 수 있는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749638855929&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;BOM과 라이브러리 버전 매핑 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. BOM과 라이브러리 버전 매핑 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 그룹 출시 노트 열의 URL은 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&quot; data-og-url=&quot;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;BOM과 라이브러리 버전 매핑 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. BOM과 라이브러리 버전 매핑 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 그룹 출시 노트 열의 URL은&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 페이지에서 자신의 프로젝트 compose-bom 버전 -&amp;gt; compose 버전을 확인해서 version을 선택하면 될 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;네비게이션 그래프 정의&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;RootGraph&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749639059076&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NavHostGraph
annotation class RootGraph&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 어노테이션을 붙이면 navigation graph를 형성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 nested navigation이라면 해당 어노테이션을 활용해서 그래프 클래스를 선언해 주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 중첩 네비게이션 구조라면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uks86/btsOwFNyEMe/Bm2Zus8qmRu7WlenVDGiRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uks86/btsOwFNyEMe/Bm2Zus8qmRu7WlenVDGiRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uks86/btsOwFNyEMe/Bm2Zus8qmRu7WlenVDGiRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUks86%2FbtsOwFNyEMe%2FBm2Zus8qmRu7WlenVDGiRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;279&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 활용해서 customGraph 어노테이션을 만들어 주시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749639484966&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NavGraph&amp;lt;RootGraph&amp;gt;(start = true)
annotation class HomeGraph&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomeGraph는 RootGraph 타입을 가지는 다른 그래프 중 시작 그래프라는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아니라면,&lt;/p&gt;
&lt;pre id=&quot;code_1749639522818&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NavGraph&amp;lt;RootGraph&amp;gt;
annotation class TestGraph&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들기 전, sample 코드의 화면 구성은 Home -&amp;gt; Detail 간단한 구성입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Home은 첫 번째 진입점이라는 표시를 해주기 위해서 &lt;span style=&quot;background-color: oklch(0.99 0.004 106.471); color: oklch(0.304 0.04 213.681); text-align: left;&quot;&gt;start 값을 true로 설정하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749639698697&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Destination&amp;lt;RootGraph&amp;gt;(start = true)
fun HomeScreen()

@Destination&amp;lt;RootGraph&amp;gt;
fun DetailScreen()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 NavGraph 코드를 살펴본 것처럼 &lt;span style=&quot;background-color: oklch(0.99 0.004 106.471); color: oklch(0.304 0.04 213.681); text-align: left;&quot;&gt;start의 기본값이 false이기 때문에 생략해도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Destinations는 NavHost가 아닌 DestinationNavHost를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/53o33/btsOxCCS1SL/evvw5qs0gKRfmMZGJ8ERkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/53o33/btsOxCCS1SL/evvw5qs0gKRfmMZGJ8ERkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/53o33/btsOxCCS1SL/evvw5qs0gKRfmMZGJ8ERkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F53o33%2FbtsOxCCS1SL%2Fevvw5qs0gKRfmMZGJ8ERkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;250&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주된 파라미터는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;navGraph : 시작 네비게이션 그래프&lt;/li&gt;
&lt;li&gt;start : 런타임에 시작 목적지&lt;/li&gt;
&lt;li&gt;defaultTransitions : 화면 전환 애니메이션 style&lt;/li&gt;
&lt;li&gt;dependenciesContainerBuilder : 의존성 주입을 위한 ContainerBuilder&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749640054955&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DestinationsNavHost(navGraph = NavGraphs.root)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NavGraphs.root는 프로젝트를 빌드하면 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEcjV/btsOwFz6OQm/G8CpZ4PWv3YRJy1TgqEnZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEcjV/btsOwFz6OQm/G8CpZ4PWv3YRJy1TgqEnZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEcjV/btsOwFz6OQm/G8CpZ4PWv3YRJy1TgqEnZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEcjV%2FbtsOwFz6OQm%2FG8CpZ4PWv3YRJy1TgqEnZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;344&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 kdoc를 활용해서 제가 어노테이션을 생성할 수 있을 줄 알았는데 빌드 후 생성된 NavGraphs 코드를 타고 들어가면 생성되네요!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;화면 전환과 인자 전달&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Detail 화면에선 owner와 repositoryName이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Destinations에서는 &lt;b&gt;navArgs를 정의 후, 넘겨주면 됩니다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1749640313182&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class DetailArgs(
    val owner: String,
    val repositoryName: String,
)
@Destination&amp;lt;RootGraph&amp;gt;(navArgs = DetailArgs::class)
@Composable
fun DetailRoute()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 간단한 DetailArgs를 생성 후, @Destination 어노테이션 navArgs에 정의해 주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 코드를 빌드하면 다음과 같은 코드가 생성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749640409922&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Generated from [DetailRoute]
 */
public data object DetailRouteDestination : BaseRoute(), TypedDestinationSpec&amp;lt;DetailArgs&amp;gt; {

    override fun invoke(navArgs: DetailArgs): Direction = with(navArgs) {
        invoke(owner, repositoryName)
    }

    public operator fun invoke(
        owner: String,
        repositoryName: String,
    ): Direction {
        return Direction(
            route = &quot;$baseRoute&quot; +
                    &quot;/${stringNavType.serializeValue(&quot;owner&quot;, owner)}&quot; +
                    &quot;/${stringNavType.serializeValue(&quot;repositoryName&quot;, repositoryName)}&quot;
        )
    }

    override val baseRoute: String = &quot;detail_route&quot;

    override val route: String = &quot;$baseRoute/{owner}/{repositoryName}&quot;

    override val arguments: List&amp;lt;NamedNavArgument&amp;gt;
        get() = listOf(
            navArgument(&quot;owner&quot;) {
                type = stringNavType
            },
            navArgument(&quot;repositoryName&quot;) {
                type = stringNavType
            }
        )

    @Composable
    override fun DestinationScope&amp;lt;DetailArgs&amp;gt;.Content() {
        DetailRoute()
    }

    override fun argsFrom(bundle: Bundle?): DetailArgs {
        return DetailArgs(
            owner = stringNavType.safeGet(bundle, &quot;owner&quot;)
                ?: throw RuntimeException(&quot;'owner' argument is mandatory, but was not present!&quot;),
            repositoryName = stringNavType.safeGet(bundle, &quot;repositoryName&quot;)
                ?: throw RuntimeException(&quot;'repositoryName' argument is mandatory, but was not present!&quot;),
        )
    }

    override fun argsFrom(savedStateHandle: SavedStateHandle): DetailArgs {
        return DetailArgs(
            owner = stringNavType.get(savedStateHandle, &quot;owner&quot;)
                ?: throw RuntimeException(&quot;'owner' argument is mandatory, but was not present!&quot;),
            repositoryName = stringNavType.get(savedStateHandle, &quot;repositoryName&quot;)
                ?: throw RuntimeException(&quot;'repositoryName' argument is mandatory, but was not present!&quot;),
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내부적으로 전달한 클래스를 분해해서 type-safe 한 인자 전달을 지원하고 있네요!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 navArgs를 정의 안 했다면 아래와 같이 간단한 코드가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 생성된 &lt;b&gt;DetailRouteDestination&lt;/b&gt;을 활용해서 화면 전환 코드를 작성해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749640799597&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;destinationsNavigator.navigate(
    DetailRouteDestination(
        DetailArgs(
            owner = owner,
            repositoryName = repositoryName
        )
    )
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 사용되는 navigator의 타입은 DestinationsNavigator인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Destinations 내부 코드에선 NavHostController -&amp;gt; DestinationsNavController로 변환해 주는 확장함수를 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lLpDf/btsOxjp1bAC/VD4mEW7qM0SAE9kPKZsYOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lLpDf/btsOxjp1bAC/VD4mEW7qM0SAE9kPKZsYOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lLpDf/btsOxjp1bAC/VD4mEW7qM0SAE9kPKZsYOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlLpDf%2FbtsOxjp1bAC%2FVD4mEW7qM0SAE9kPKZsYOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;561&quot; height=&quot;100&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3i20x/btsOwrB2vVA/h5pkAgsI5nRZ61lekKgdZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3i20x/btsOwrB2vVA/h5pkAgsI5nRZ61lekKgdZ1/img.png&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;762&quot; data-is-animation=&quot;false&quot; width=&quot;548&quot; height=&quot;610&quot; style=&quot;width: 35.7054%; margin-right: 10px;&quot; data-widthpercent=&quot;36.13&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3i20x/btsOwrB2vVA/h5pkAgsI5nRZ61lekKgdZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3i20x%2FbtsOwrB2vVA%2Fh5pkAgsI5nRZ61lekKgdZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0HXkx/btsOxtsmXEm/66PkqKVRzD3klFH3yla4I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0HXkx/btsOxtsmXEm/66PkqKVRzD3klFH3yla4I1/img.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;591&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.1318%;&quot; data-widthpercent=&quot;63.87&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0HXkx/btsOxtsmXEm/66PkqKVRzD3klFH3yla4I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0HXkx%2FbtsOxtsmXEm%2F66PkqKVRzD3klFH3yla4I1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;938&quot; height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DestinationScope&lt;/b&gt;의 구현체인 Impl에서 &lt;b&gt;navController&lt;/b&gt;를 &lt;b&gt;DestinationNavigtor&lt;/b&gt;로 바꿔주고 있음을 확인할 수 있네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 복잡한 화면을 예로 들어 설명&lt;/p&gt;
&lt;pre id=&quot;code_1749641453984&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  navGraph
├──   HomeBaseGraph
│   ├── HomeFirstScreen
│   ├── HomeSecondScreen
│   └──   MenuGraph
│
├──   SearchGraph
│   ├── SearchScreen
│   └── SearchSecondScreen
│
├──   MyScreen
│
├──   MenuGraph
│   ├── MenuScreen
│   ├──   KoreanFoodGraph
│   │   ├── KimchiScreen
│   │   └── RamenScreen
│   │
│   ├──   AmericanFoodGraph
│   │   ├── HambugerScreen
│   │   └── ...
│   │
│   └──   MoreFoodGraph
│       └── ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;b&gt;Compose Navigation&lt;/b&gt;을 사용했다면 어떻게 구현되었을까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 Navigation과 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;Route&lt;/b&gt;와 &lt;b&gt;Graph&lt;/b&gt;를 정의해 줘야겠죠?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Compose Navigation&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1749641515515&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Serializable
sealed interface Route {

    @Serializable
    data object HomeBase : Route

    @Serializable
    data object HomeFirst : Route

    @Serializable
    data object HomeSecond : Route

    @Serializable
    data object SearchBase : Route

    @Serializable
    data object Search : Route

    @Serializable
    data class Menu(
        val type: FoodType,
        val foodTitle: String,
    ) : Route

    sealed interface Food : Route {
        val foodDetailType: FoodDetailType

        @Serializable
        data class KoreaFood(
            override val foodDetailType: FoodDetailType
        ) : FoodRoute

        @Serializable
        data class AmericanFood(
            override val foodDetailType: FoodDetailType
        ) : FoodRoute
    }

    @Serializable
    data object My : Route
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후, Food에 대한 Type이 추가될 때마다 FoodRoute를 상속받는 data class가 추가될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;HomeGraph&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749641676312&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun NavGraphBuilder.homeGraph(
    navController: NavHostController,
) {
    navigation&amp;lt;Route.HomeBase&amp;gt;(startDestination = Route.HomeFirst) {
        composable&amp;lt;Route.HomeFirst&amp;gt; {
            HomeFirstScreen()
        }
        composable&amp;lt;Route.HomeSecond&amp;gt; {
            HomeSecondScreen()
        }
        MoreGraph()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomeGraph를 구현함에 있어서 발생되는 코드는 크게 적지 않다고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 구현은 최대한 담백한 구현으로 navArgs, animation, 화면 이동에 대한 코드를 적용하지 않은 스켈레톤 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 HomeGraph보다 MoreGraph에서 개발자가 작성할 코드의 영역이 훨씬 길고 장황하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;MenuGraph&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749641784524&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun NavGraphBuilder.MenuGraph(
    navController: NavHostController,
) {
    composable&amp;lt;Route.Menu&amp;gt; {
        MenuScreen()
    }
    KoreanFoodGraph()
    AmericanFoodGraph()
    ...
    MoreFoodGraph()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 음식이 추가될 때마다 MenuGraph가 비대해질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 음식의 타입이 추가될 때마다 우리는 추가적인 FoodGraph를 또 생성해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 애플리케이션에서는 nested navigation을 사용하는 복잡한 구조를 채택하고 있고, 이를 구현하기 위해 많은 양의 코드가 발생된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 형식으로 구현되는 보일러 플레이트 코드는 작성자로 하여금 피곤을 느끼게 하고, 읽는 사람으로 하여금 가독성을 저해하는 원인이 되기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Compose Destinations를 적용해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업은 간단합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;@NavGraph를 사용해 그래프를 정의&lt;/li&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;실제 destination인 Screen에 @Destination 추가&lt;/li&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;(만약 전달할 argument가 있다면 argumentClass 정의)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1749642247335&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.ramcosta.composedestinations.annotation.NavGraph
import com.ramcosta.composedestinations.annotation.RootGraph

@NavGraph&amp;lt;RootGraph&amp;gt;(start = true)
annotation class HomeGraph(val start: Boolean = false)

@NavGraph&amp;lt;HomeGraph&amp;gt;
annotation class HomeSubGraph(val start: Boolean = false)

@NavGraph&amp;lt;RootGraph&amp;gt;
annotation class SearchGraph(val start: Boolean = false)

@NavGraph&amp;lt;RootGraph&amp;gt;
annotation class MyGraph(val start: Boolean = false)

@NavGraph&amp;lt;HomeSubGraph&amp;gt;
annotation class MenuGraph(val start : Boolean = false)

@NavGraph&amp;lt;MenuGraph&amp;gt;
annotation class KoreanFoodGraph(val start : Boolean = false)

@NavGraph&amp;lt;MenuGraph&amp;gt;
annotation class AmericanFoodGraph(val start : Boolean = false)

...

//MoreFoodGraph&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세웠던 내비게이션 구조대로 그래프를 정의해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 특징은 RootGraph 타입과 아닌 것이 있는데요, RootGraph 타입은 Top-Level-Destination이라고 생각하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서 &lt;b&gt;start가 true인 HomeGraph가 Top-Level-Destination의 기본 시작점&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomeSub는 HomeGraph를 타입으로 가지는데, 해당 그래프의 하위 그래프가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스크린의 @Destination을 붙여주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1749642475474&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
@Destination&amp;lt;HomeGraph&amp;gt;(start=true)
fun HomeFirstScreen()

@Composable
@Destination&amp;lt;HomeGraph&amp;gt;
fun HomeSecondScreen()

@Composable
@Destination&amp;lt;SearchGraph&amp;gt;(start=true)
fun SearchScreen()

@Composable
@Destination&amp;lt;MyGraph&amp;gt;(start=true)
fun MyScreen()
// MyScreen은 one-depth라 MyGraph가 아닌 RootGraph를 타입으로 가져도 되지만
// 확장 가능성을 고려해 graph로 선언했습니다.

@Composable
@Destination&amp;lt;MenuGraph&amp;gt;(start = true)
fun MenuScreen()

@Composable
@Destination&amp;lt;KoreanFoodGraph&amp;gt;(start = true)
fun KimchiScreen()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쭉쭉 붙여주면 됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 화면 전환 간 인자가 필요하다면 위에서 구현한 것처럼 NavArgs를 구현하고 파라미터에 정의해 주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 준비를 마쳤다면, 그동안 작성했던 Navigation 코드를 전부 지워주세요(NavHost 포함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Compose Destination에서 사용하는 navHost를 적용해 주시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749642571248&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DestinationsNavHost(
    modifier = Modifier
    navController = navController,
    navGraph = NavGraphs.root,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 정의했던 NavGraphs.root는 위 어노테이션에서 RootGraph를 타입으로 가진 Graph 중 start가 true인 어노테이션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose Destination은 Compose Navigation과 다르게 화면 전환 애니메이션이 굉장히 밋밋한 편입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tFVMy/btsOx5YQ1q6/3gFbUyFLXzEc7zqtEyc07K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tFVMy/btsOx5YQ1q6/3gFbUyFLXzEc7zqtEyc07K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tFVMy/btsOx5YQ1q6/3gFbUyFLXzEc7zqtEyc07K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtFVMy%2FbtsOx5YQ1q6%2F3gFbUyFLXzEc7zqtEyc07K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;206&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DestinationNavHost의 파라미터인 defaultTransitions를 커스텀해서 구현해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8TWKd/btsOvuzkxwJ/BKM5EKjCGRKxKCPbAzrG10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8TWKd/btsOvuzkxwJ/BKM5EKjCGRKxKCPbAzrG10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8TWKd/btsOvuzkxwJ/BKM5EKjCGRKxKCPbAzrG10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8TWKd%2FbtsOvuzkxwJ%2FBKM5EKjCGRKxKCPbAzrG10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;200&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #4d4d4c; text-align: right;&quot;&gt;&lt;span&gt;Compose Destinations를 적용해 보니, 네비게이션 관련 코드가 훨씬 간결해지고, 타입 세이프하게 인자를 전달할 수 있어 실수도 줄일 수 있었습니다. 특히 복잡한 네비게이션 구조를 관리할 때 반복적인 코드 작성에서 오는 피로감이 많이 줄어든 점이 인상적이었습니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #4d4d4c; text-align: right;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #4d4d4c; text-align: right;&quot;&gt;Jetpack Compose로 앱을 개발하면서 네비게이션 구조가 복잡해질수록 Compose Destinations의 진가가 드러납니다. 타입 세이프티, 자동 코드 생성, 쉬운 그래프 정의 등 다양한 장점을 직접 경험해 보시길 추천합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file&quot;&gt;https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749642718989&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose navigation with no boilerpl&quot; data-og-description=&quot;Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate. - raamcosta/compose-destinations&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file&quot; data-og-url=&quot;https://github.com/raamcosta/compose-destinations&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/csDEEb/hyY45uPo6y/QQ3OkDIdtyXX6XjPjiHqgk/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234,https://scrap.kakaocdn.net/dn/FjQLz/hyY4b24ljR/iBu4qkLRWHWfgGZG9l50I0/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234&quot;&gt;&lt;a href=&quot;https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/csDEEb/hyY45uPo6y/QQ3OkDIdtyXX6XjPjiHqgk/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234,https://scrap.kakaocdn.net/dn/FjQLz/hyY4b24ljR/iBu4qkLRWHWfgGZG9l50I0/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_144_1060_234');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose navigation with no boilerpl&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate. - raamcosta/compose-destinations&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/&quot;&gt;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749642725452&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Defining Navigation Graphs | Compose Destinations&quot; data-og-description=&quot;Defining navigation graphs&quot; data-og-host=&quot;composedestinations.rafaelcosta.xyz&quot; data-og-source-url=&quot;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/&quot; data-og-url=&quot;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmGytH/hyY75GUzlp/q1pNQrL3DZhmgOAzns0R4k/img.png?width=1012&amp;amp;height=1568&amp;amp;face=0_0_1012_1568&quot;&gt;&lt;a href=&quot;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmGytH/hyY75GUzlp/q1pNQrL3DZhmgOAzns0R4k/img.png?width=1012&amp;amp;height=1568&amp;amp;face=0_0_1012_1568');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Defining Navigation Graphs | Compose Destinations&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Defining navigation graphs&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;composedestinations.rafaelcosta.xyz&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>compose destinations</category>
      <category>jetpack compose</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/515</guid>
      <comments>https://jja2han.tistory.com/515#entry515comment</comments>
      <pubDate>Wed, 11 Jun 2025 21:06:33 +0900</pubDate>
    </item>
    <item>
      <title>Compose Layout</title>
      <link>https://jja2han.tistory.com/514</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;취업 이후 첫 글인 것 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 실력이 모자라 배우느라 바쁘기도 했지만, 개발 외적으로 회사의 프로세스를 적응하고 이해하는데 많은 시간을 쏟고 있는 요즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근엔 서서히 적응을 하고 있고, 마인드맵 프로젝트부터 관심이 있었던 Custom Layout을 Jetpack Compose에선 어떻게 구현하고 있을지 궁금했고, 그에 관한 내용을 적어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose Layout을 설명하기 전 CustomLayout을 만들기 위해서 View(Xml)에선 어떻게 하고 있었을까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;과거(Xml)&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1747470393701&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CustomLayout(context: Context) : ViewGroup(context) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 자식 뷰를 측정
        var maxWidth = 0
        var maxHeight = 0
        children.forEach { child -&amp;gt;
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            maxWidth = maxOf(maxWidth, child.measuredWidth)
            maxHeight = maxOf(maxHeight, child.measuredHeight)
        }
        setMeasuredDimension(
            resolveSize(maxWidth, widthMeasureSpec),
            resolveSize(maxHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // 자식 뷰 배치
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            child.layout(left, top, right, bottom)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 XML로 CustomLayout을 구현하기 위해선 View 또는 ViewGroup을 상속받아서 구현했으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정과 배치를 위해 &lt;b&gt;onMeasure()&lt;/b&gt;와 &lt;b&gt;onLayout()&lt;/b&gt;을 오버라이드 하여 크기를 측정하고 배치했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기까지는 Compose의 Layout과 큰 차이점은 없다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Compose와 XML의 차이점으로 생기는 &lt;b&gt;미리 보기 기능 지원 X&lt;/b&gt;, X&lt;b&gt;ML 레이아웃과 Kotlin 로직이 분리&lt;/b&gt;가 단점으로 다가왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(사실 Compose를 경험해보지 않았다면 생각하지도 못할 단점 이긴 합니다)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Compose에선 어떻게 CustomLayout을 만들고 있을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Compose&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose의 Layout 시스템은 아래 3가지를 목표로 하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkkKUs/btsN0KQfolU/8NLfW7b7rtQHkn7pEiYZwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkkKUs/btsN0KQfolU/8NLfW7b7rtQHkn7pEiYZwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkkKUs/btsN0KQfolU/8NLfW7b7rtQHkn7pEiYZwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkkKUs%2FbtsN0KQfolU%2F8NLfW7b7rtQHkn7pEiYZwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;170&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Easy : 커스텀 레이아웃을 쉽게&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Powerful : 레이아웃 시스템이 효과적으로 작동해 성능의 우수성을 통해&lt;/li&gt;
&lt;li&gt;Performant : 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Compose phase&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEeR2S/btsN1R1Cpps/jGO3KKOEpwnTQCzjbNCq11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEeR2S/btsN1R1Cpps/jGO3KKOEpwnTQCzjbNCq11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEeR2S/btsN1R1Cpps/jGO3KKOEpwnTQCzjbNCq11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEeR2S%2FbtsN1R1Cpps%2FjGO3KKOEpwnTQCzjbNCq11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;83&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 크게 Composition, Layout, Drawing 3단계로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계는 간단하게 설명하자면,&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Composition&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Composable을 실행해 UI 트리를 만듭니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 구현한 Composable 함수를 읽어 아래와 같이 UI 트리를 만듭니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2dsph/btsN2MrLbvF/5TWn00tKvXXrFVS2JURiu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2dsph/btsN2MrLbvF/5TWn00tKvXXrFVS2JURiu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2dsph/btsN2MrLbvF/5TWn00tKvXXrFVS2JURiu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2dsph%2FbtsN2MrLbvF%2F5TWn00tKvXXrFVS2JURiu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;354&quot; height=&quot;236&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Layout&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Composition 단계에서 생성된 UI 트리의 각 요소를 측정하여 배치하는 역할을 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 자식 노드의 너비와 높이를 결정하고 x와 y좌표를 알아내는 단계입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Drawing&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Composable 요소를 렌더링, 즉 그리는 단계입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3단계를 간단하게 요약하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOJxfi/btsN1cFqv8A/eFeXfkZXVadBajB1rb6Rvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOJxfi/btsN1cFqv8A/eFeXfkZXVadBajB1rb6Rvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOJxfi/btsN1cFqv8A/eFeXfkZXVadBajB1rb6Rvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOJxfi%2FbtsN1cFqv8A%2FeFeXfkZXVadBajB1rb6Rvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;655&quot; height=&quot;112&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composition 단계에선 &lt;b&gt;무엇을 표시할 지 정의&lt;/b&gt;하고 있으며, Layout 단계에선 &lt;b&gt;어디에 배치할 지 계산&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 Drawing 단계에선 &lt;b&gt;어떻게 화면에 렌더링&lt;/b&gt;할지 실행한다고 요약할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomLayout을 만들기 위해선 위 3단계 중 &lt;b&gt;Layout 단계에 집중할 필요성이 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Layout Phase&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃 단계에선 자식의 크기를 측정하고 어디에 배치할지 계산하는 단계입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clVCJT/btsN10RNulB/K7oN7aMxNkgqV8DKf856O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clVCJT/btsN10RNulB/K7oN7aMxNkgqV8DKf856O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clVCJT/btsN10RNulB/K7oN7aMxNkgqV8DKf856O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclVCJT%2FbtsN10RNulB%2FK7oN7aMxNkgqV8DKf856O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;309&quot; height=&quot;185&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 레이아웃 단계는 &lt;b&gt;Measurement(측정)&lt;/b&gt;, &lt;b&gt;Placement(배치)&lt;/b&gt;로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구현한 CustomLayout의 onMeasure, onLayout과 유사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 해당 단계를 명시적으로 분리했지만, Compose는 Layout 단계로 합쳤고 각각의 Scope만 명시해주고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CYAw5/btsN2qoWFuP/Ek21GmMdZIlyiWdUUkoQRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CYAw5/btsN2qoWFuP/Ek21GmMdZIlyiWdUUkoQRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CYAw5/btsN2qoWFuP/Ek21GmMdZIlyiWdUUkoQRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCYAw5%2FbtsN2qoWFuP%2FEk21GmMdZIlyiWdUUkoQRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;209&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI트리에선 &lt;b&gt;자식의 크기를 측정 -&amp;gt; 자신의 사이즈를 결정 -&amp;gt; 자식을 배치하는 3단계&lt;/b&gt;가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9UyDU/btsN10qHN3c/qFoKkmTZ8vdzl50jpragC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9UyDU/btsN10qHN3c/qFoKkmTZ8vdzl50jpragC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9UyDU/btsN10qHN3c/qFoKkmTZ8vdzl50jpragC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9UyDU%2FbtsN10qHN3c%2FqFoKkmTZ8vdzl50jpragC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;217&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식이 더 이상 없는 리프 노드인 &lt;span&gt;Image&lt;/span&gt;, &lt;span&gt;Text&lt;/span&gt; 등은&lt;b&gt; 자신의 콘텐츠에 따라 직접 측정 후 크기를 결정&lt;/b&gt;하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Row&lt;/span&gt;, &lt;span&gt;Column&lt;/span&gt;처럼 자식을 가지는 Composable은 &lt;span&gt;&lt;b&gt;모든 자식의 크기가 결정된 이후&lt;/b&gt;&lt;/span&gt;에야 자신의 크기를 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 어떤 자식의 크기를 다른 자식의 크기에 따라 결정해야 하는 복잡한 상황에서는,&lt;br /&gt;일반적인 Compose의 흐름만으로는 한계가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이러한 상황에서 활용할 수 있는 도구가 바로 SubComposeLayout입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 개념은 조금 더 깊이 있는 주제이므로, 다음 글에서 자세히 다뤄보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 잘 구현되어 있는 Layout Phase지만, 복잡한 레이아웃을 만들 경우 한계가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomLayout을 구현하기 위해선 &lt;b&gt;각 단계에서 어떤 작업을 하는지 이해할 필요성&lt;/b&gt;이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Compose Layout&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlz7W2/btsN2te9QPS/tHKDdiPkCvNA47kmZOxiE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlz7W2/btsN2te9QPS/tHKDdiPkCvNA47kmZOxiE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlz7W2/btsN2te9QPS/tHKDdiPkCvNA47kmZOxiE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdlz7W2%2FbtsN2te9QPS%2FtHKDdiPkCvNA47kmZOxiE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;399&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서 제공하는 Layout 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Row, Column과 같은 모든 상위 레벨 레이아웃은 해당 Composable을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 함수를 통해 CustomLayout을 구현하는 스켈레톤 코드는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFvKFV/btsN02DjlJi/LZy7fost4hfO72YUqZeM0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFvKFV/btsN02DjlJi/LZy7fost4hfO72YUqZeM0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFvKFV/btsN02DjlJi/LZy7fost4hfO72YUqZeM0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFvKFV%2FbtsN02DjlJi%2FLZy7fost4hfO72YUqZeM0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;109&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 파라미터인 MeasurePolicy를 후행 람다로 제공하여 MeasurePolicy를 구현하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ukL7U/btsN2bTi2aa/SdbkixxAct2IeevaTxc1Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ukL7U/btsN2bTi2aa/SdbkixxAct2IeevaTxc1Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ukL7U/btsN2bTi2aa/SdbkixxAct2IeevaTxc1Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FukL7U%2FbtsN2bTi2aa%2FSdbkixxAct2IeevaTxc1Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;139&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MeasureScope의 measure은 measurables, constraints를 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;627&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FEDE9/btsN2GFg3lo/8fDycCv0MyygKqw0LzQ6kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FEDE9/btsN2GFg3lo/8fDycCv0MyygKqw0LzQ6kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FEDE9/btsN2GFg3lo/8fDycCv0MyygKqw0LzQ6kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFEDE9%2FbtsN2GFg3lo%2F8fDycCv0MyygKqw0LzQ6kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;441&quot; height=&quot;311&quot; data-origin-width=&quot;627&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constraints : &lt;b&gt;레이아웃에 크기를 알려주는 클래스로, 레이아웃 높이와 너비의 최댓값과 최솟값을 모델링&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max와 min 값의 조절을 통해 크기의 제약이 없는 레이아웃을 만들거나, 정확한 값을 조절해 원하는 레이아웃의 크기를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sJgi8/btsN1htuJ8U/H0Tduq1q9SF1WhrGxMjjXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sJgi8/btsN1htuJ8U/H0Tduq1q9SF1WhrGxMjjXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sJgi8/btsN1htuJ8U/H0Tduq1q9SF1WhrGxMjjXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsJgi8%2FbtsN1htuJ8U%2FH0Tduq1q9SF1WhrGxMjjXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;508&quot; height=&quot;162&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 Docs에 대한 설명을 해석하자면, &lt;b&gt;측정 가능한 Composable 요소&lt;/b&gt;를 의미한다고 적혀있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout의 단계는 &lt;b&gt;Measure -&amp;gt; Place&lt;/b&gt;로 구성되어 있기에 아직 배치되지 않았지만, 측정 가능한 Composable을 의미한다는 뜻으로 해석할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 measure 함수의 결괏값으로 Placeable 객체를 반환하고, 배치가 가능하게됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHvWKf/btsN0O6ar1T/7vgEnC02FBYI60uMbKiiOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHvWKf/btsN0O6ar1T/7vgEnC02FBYI60uMbKiiOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHvWKf/btsN0O6ar1T/7vgEnC02FBYI60uMbKiiOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHvWKf%2FbtsN0O6ar1T%2F7vgEnC02FBYI60uMbKiiOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;289&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Placeable&lt;/span&gt;은 &lt;b&gt;Measure&lt;/b&gt;이 완료된 결괏값으로, &lt;b&gt;부모는 이 &lt;span&gt;Placeable&lt;/span&gt;을 &lt;span&gt;place&lt;/span&gt; 함수를 통해 원하는 위치에 배치&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Docs에서는 &lt;/span&gt;&lt;b&gt;&quot;A Placeable should never be stored between measure calls&quot;&lt;/b&gt;&lt;span&gt;라고 명시하고 있는데요,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;&lt;span&gt;Placeable&lt;/span&gt;을 변수에 저장하지 말라는 의미&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 상태 변화가 발생할 때마다 레이아웃을 다시 측정하며, 이 과정에서 &lt;span&gt;measure &amp;rarr; place&lt;/span&gt; 사이클이 반복됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;span&gt;Placeable&lt;/span&gt;은 측정 시점에서만 유효한 객체이며, &lt;span&gt;measure&lt;/span&gt;가 다시 호출되면 새로운 측정 결과가 생성되기 때문에 이전에 저장해 둔 &lt;span&gt;Placeable&lt;/span&gt;은 더 이상 유효하지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 &lt;/span&gt;&lt;span&gt;Placeable&lt;/span&gt;&lt;span&gt;은 &lt;b&gt;측정 시점에만 유효한 값이고, 저장할 필요가 없습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HS8ie/btsN2HjTQ16/7KAfDX8rikkKBsti0Vtk5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HS8ie/btsN2HjTQ16/7KAfDX8rikkKBsti0Vtk5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HS8ie/btsN2HjTQ16/7KAfDX8rikkKBsti0Vtk5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHS8ie%2FbtsN2HjTQ16%2F7KAfDX8rikkKBsti0Vtk5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;572&quot; height=&quot;330&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout은 Measure -&amp;gt; Place 단계로 진행되는데요, 코드적으로도 명시적으로 분리하고 있진 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편의상으로 &lt;b&gt;MeasureScope&lt;/b&gt;와 &lt;b&gt;PlaceScope&lt;/b&gt;로 구분하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MeasureScope&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAKNOI/btsN2r2IX1b/bt6XUhuYHjwl5YYYHH6rhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAKNOI/btsN2r2IX1b/bt6XUhuYHjwl5YYYHH6rhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAKNOI/btsN2r2IX1b/bt6XUhuYHjwl5YYYHH6rhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAKNOI%2FbtsN2r2IX1b%2Fbt6XUhuYHjwl5YYYHH6rhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;119&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MeasureScope는 컴포넌트의 크기를 측정하는 역할을 하며, PlaceScope는 측정 완료된 Placeable을 배치하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;measure의 결괏값은 Placeable이기 때문에 measurable을 List&amp;lt;Measurable&amp;gt; -&amp;gt; List&amp;lt;Placeable&amp;gt;로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스켈레톤 코드기 때문에 &lt;b&gt;일반적인 제약조건을 설정했지만, 요구사항에 맞는 constraints를 설정&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 첫 번째 자식은 원래 크기대로, 나머지는 50% 크기로 축소해야 하는 요구사항이 있다면 아래와 같이 작성할 수 있을 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1747551770349&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Layout(
    modifier = modifier,
    content = content
) { measurables, constraints -&amp;gt;
    val placeables = measurables.mapIndexed { index, measurable -&amp;gt;
        if (index == 0) {
            measurable.measure(constraints)
        } else {
            val halfConstraints = Constraints(
                minWidth = constraints.minWidth / 2,
                maxWidth = constraints.maxWidth / 2,
                minHeight = constraints.minHeight / 2,
                maxHeight = constraints.maxHeight / 2
            )
            measurable.measure(halfConstraints)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PlaceScope&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2b8TU/btsN00FBZiu/GdExOUKlORO4rQeyrKGSjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2b8TU/btsN00FBZiu/GdExOUKlORO4rQeyrKGSjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2b8TU/btsN00FBZiu/GdExOUKlORO4rQeyrKGSjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2b8TU%2FbtsN00FBZiu%2FGdExOUKlORO4rQeyrKGSjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;188&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MeasureScoep의 layout 함수는 width, height, placementBlock을 파라미터로 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;layout 함수의 전달할 값은 MeasureScope에서 계산한 placeable의 width와 height의 결괏값이 될 수도 있고, 고정 값을 줄 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 가장 큰 자식의 크기를 기준으로 Layout 크기를 결정할 경우 아래와 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1747552597420&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Layout(
    modifier = modifier,
    content = content
) { measurables, constraints -&amp;gt;
    val placeables = measurables.map { it.measure(constraints) }

    val maxWidth = placeables.maxOfOrNull { it.width } ?: 0
    val maxHeight = placeables.maxOfOrNull { it.height } ?: 0

    layout(maxWidth, maxHeight) {
        placeables.forEach {
            it.place(0, 0)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 예로는 자식들을 한 줄로 배치하고 총 너비와 자식의 최대 높이로 크기를 결정하는 요구사항의 경우 아래와 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1747552801900&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Layout(
    modifier = modifier,
    content = content
) { measurables, constraints -&amp;gt;
    val placeables = measurables.map { it.measure(constraints) }

    val totalWidth = placeables.sumOf { it.width }
    val maxHeight = placeables.maxOfOrNull { it.height } ?: 0

    layout(totalWidth, maxHeight) {
        var xPosition = 0
        placeables.forEach {
            it.place(xPosition, 0)
            xPosition += it.width
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout Phase에서 Measure -&amp;gt; Place 단계에서 어떠한 작업을 해야 할지 명확하게 파악했다면&amp;nbsp; 복잡한 레이아웃도 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 FlowLayout을 Layout 함수를 통해 구현했는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slot 뿐만 아니라 horizontalPadding, verticalPadding을 받아서 실제 동작과 유사하게 구현해 봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1747553044974&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun FlowRowLayout(
    modifier: Modifier = Modifier,
    horizontalPadding: Dp = 5.dp,
    verticalPadding: Dp = 5.dp,
    content: @Composable () -&amp;gt; Unit,
) {
    Layout(
        modifier = modifier,
        content = content,
    ) { measurables, constraints -&amp;gt;
        val placeables =
            measurables.map { measurable -&amp;gt;
                measurable.measure(constraints)
            }
        var yPosition = 0
        var xPosition = 0
        var totalHeight =
            if (placeables.isNotEmpty()) {
                verticalPadding.value.toInt() * 2 + placeables.first().height
            } else {
                verticalPadding.value.toInt() * 2
            }

        placeables.forEach { placeable -&amp;gt;
            if (xPosition + placeable.width &amp;gt;= constraints.maxWidth) {
                xPosition = 0
                yPosition += placeable.height
                totalHeight += placeable.height + verticalPadding.value.toInt()
            }
            xPosition += placeable.width
        }
        xPosition = 0
        yPosition = verticalPadding.value.toInt()
        layout(constraints.maxWidth, totalHeight) {
            for (placeable in placeables) {
                if (xPosition + placeable.width &amp;gt;= constraints.maxWidth) {
                    xPosition = 0
                    yPosition += placeable.height + verticalPadding.value.toInt()
                }
                placeable.placeRelative(x = xPosition, y = yPosition)
                xPosition += placeable.width + horizontalPadding.value.toInt()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YE2WH/btsN1BdPKlx/buHDJlrPsGlFt35Vhwsry0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YE2WH/btsN1BdPKlx/buHDJlrPsGlFt35Vhwsry0/img.png&quot; data-alt=&quot;Layout을 통해 구현한 CustomFlowLayout&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YE2WH/btsN1BdPKlx/buHDJlrPsGlFt35Vhwsry0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYE2WH%2FbtsN1BdPKlx%2FbuHDJlrPsGlFt35Vhwsry0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;126&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Layout을 통해 구현한 CustomFlowLayout&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIJ4qM/btsN2Hc7HGk/9Fg8nSwJOpuPIAryvuppMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIJ4qM/btsN2Hc7HGk/9Fg8nSwJOpuPIAryvuppMk/img.png&quot; data-alt=&quot;실제 FlowLayout&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIJ4qM/btsN2Hc7HGk/9Fg8nSwJOpuPIAryvuppMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIJ4qM%2FbtsN2Hc7HGk%2F9Fg8nSwJOpuPIAryvuppMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;119&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 FlowLayout&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Jetpack Compose에서 Custom Layout을 구현하는 과정을 기존 View 시스템과 비교하며 정리해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 기반 ViewGroup에서의 onMeasure, onLayout 오버라이드 방식과 달리, Compose에서는 Layout 함수와 MeasurePolicy를 통해 훨씬 더 직관적이고 유연하게 커스텀 레이아웃을 만들 수 있다는 점이 인상적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 직접 FLowLayout을 구현해보면서, Compose의 레이아웃 시스템이 얼마나 강력하면서도 유연하게 동작하는지 경험할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에선 SubComposeLayout과 같은 고급 주제도 다뤄보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=zMKMwh9gZuI&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=zMKMwh9gZuI&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignLeft&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=zMKMwh9gZuI&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/1iydo/hyYU8EyAes/zIxBSkKVIN9uJwN3qYS1Ek/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1008_136_1152_600,https://scrap.kakaocdn.net/dn/eRWtr/hyYU2RSF8N/YRgXeUcuBwXKOllP77hyk0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1008_136_1152_600&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Deep dive into Jetpack Compose layouts&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/zMKMwh9gZuI&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747553371228&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Compose Deep Dive &amp;mdash; 2.Layout&quot; data-og-description=&quot;Jetpack Compose Layout의 동작 원리를 Deep Dive into Jetpack Compose Layout(Android Dev Summit21) 영상과 함께 살펴봅니다.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&quot; data-og-url=&quot;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jLIgE/hyYWVqH0sQ/tWkyNLuCg74XKgvLBx6GAK/img.jpg?width=1200&amp;amp;height=800&amp;amp;face=0_0_1200_800&quot;&gt;&lt;a href=&quot;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/hongbeomi-dev/compose-deep-dive-2-layout-204262dae5ae&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jLIgE/hyYWVqH0sQ/tWkyNLuCg74XKgvLBx6GAK/img.jpg?width=1200&amp;amp;height=800&amp;amp;face=0_0_1200_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose Deep Dive &amp;mdash; 2.Layout&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose Layout의 동작 원리를 Deep Dive into Jetpack Compose Layout(Android Dev Summit21) 영상과 함께 살펴봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>customlayout</category>
      <category>jetpack compose</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/514</guid>
      <comments>https://jja2han.tistory.com/514#entry514comment</comments>
      <pubDate>Sun, 18 May 2025 16:30:18 +0900</pubDate>
    </item>
    <item>
      <title>[공채후기] 2025 SOOP Android 신입 공채 합격 후기</title>
      <link>https://jja2han.tistory.com/513</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brAtPd/btsMxCLZgkz/LrkK8GWn7pMd0CFigqhZz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brAtPd/btsMxCLZgkz/LrkK8GWn7pMd0CFigqhZz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brAtPd/btsMxCLZgkz/LrkK8GWn7pMd0CFigqhZz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrAtPd%2FbtsMxCLZgkz%2FLrkK8GWn7pMd0CFigqhZz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1122&quot; height=&quot;335&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Android 모바일 개발 직무에 지원했던 지원자입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년에 Android 개발 직무 지원 공고가 뜨지 않았고, 23년과 25년에 뜬것 보면 매년 채용 공고가 열리는 것 같지는 않다고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 채용 시장이 그렇고 특히 Android 개발 직무에 대한 채용이 정말 얼어있는 와중에 소중한 공고였던것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채용 프로세스 일정은 짧고 단순한게 매력적이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnZBZo/btsMwx5Eu4e/7FU4AuT50ZNlWyT4yPVoS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnZBZo/btsMwx5Eu4e/7FU4AuT50ZNlWyT4yPVoS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnZBZo/btsMwx5Eu4e/7FU4AuT50ZNlWyT4yPVoS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnZBZo%2FbtsMwx5Eu4e%2F7FU4AuT50ZNlWyT4yPVoS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;928&quot; height=&quot;306&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5단계로 이루어진 채용 프로세스였고, 각 프로세스마다의 기간이 짧아서 지원자입장에서 좋았다고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서류전형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 전형은 자기소개서로, 다른 기업에 비해 굉장히 친절하다는 느낌을 많이 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기업에 대한 인식? 성격의 장단점 등 문항의 제목을 들을 때부터 힘들었던 다른 기업에 비해 지원자를 많이 배려한다는 느낌을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 문항은 다음과 같은데요,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지원한 직무와 관련하여 본인만이 가지고 있는 차별화된 경쟁력은 무엇이며, 그렇게 생각하는 이유를 기술해 주세요(700)&lt;/li&gt;
&lt;li&gt;본인의 이야기를 형식의 제한 없이 자유롭게 기술해주세요.(1500)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;문항이 두 개인 점도 좋았고, 글자수도 그렇게 크지 않았었습니다. 평소에 관심이 있던 기업이라서 그런지 자소서 내용이 술술 잘 써졌던 것도 컸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;사실 자소서를 쓰면서 제가 가장 고민했던 점은 &lt;b&gt;내가 어떤 사람으로 보이는 것이 유리할까? 와 같이 나의 컨셉을&amp;nbsp;정하는 것이었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;b&gt;나는 이런 걸 잘한다, 이런 것에 강점이 있다는 것을 적어야 하는 자소서 문항들이었기에 내가 어떤 개발자인지, 평소에 어떤 가치관을 가지고 있는지를 자소서에 녹여내는 것에 많은 시간을 사용했습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;b&gt;모든 자소서가 그렇듯 면접을 염두에 두고 작성해야 하는 만큼, 내가 면접에서 어떤 모습을 어필할 수 있을지 고민하면서 쓰는 것이 개인적으로는 좋다고 생각이 들었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에는 만든 이력서와 포트폴리오를 작성했고, &lt;b&gt;블라인드 채용이라는 점이 조금 독특했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만든 이력서와 포트폴리오에 대한 개인정보를 지우는 게 쉽지가 않았던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 결과는 빠른 시일 내에 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N19Ax/btsMyT0lhxG/DdAAMZkClg5zTKFDIKNkY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N19Ax/btsMyT0lhxG/DdAAMZkClg5zTKFDIKNkY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N19Ax/btsMyT0lhxG/DdAAMZkClg5zTKFDIKNkY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN19Ax%2FbtsMyT0lhxG%2FDdAAMZkClg5zTKFDIKNkY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;795&quot; height=&quot;347&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;b&gt;1차 면접&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Android 개발 직무는 사전 과제 전형이 있었습니다. 다른 전형은 과제가 아닌 코딩 테스트가 있던 걸로 아는데, 자세하게는 모르겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Android 개발 직무의 경우는 요구사항에 대한 간단한 앱을 만드는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;기한은 제법 넉넉하게 줬고, 특이사항으로는 특정 기술 스택을 사용하면 가산점이 부여되는 방식이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;과제 자체는 그렇게 크게 어렵지 않았고, 개인적으로 느끼기에는 코드 한 줄 한 줄의 의도(?)가 중요하다고 느꼈습니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직무 면접에서 작성한 과제 코드에 대한 질문이 제법 들어왔었기 때문에 그 부분을 유의하시면 좋을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 사실 이렇게 디테일하게 코드 질문이 들어올 줄 몰라서 크게 당황했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 과제와 함께 1차 면접이 진행되는데, 제가 느끼기엔 인성보다는 직무에 가까운 면접이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 맞춰서 면접관님들도 직무 관련으로 배정받았다는 느낌을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제로 제가 진행했던 면접도 면접관님(5) : 지원자(3)로 진행되는 다대다 면접이었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 면접 경험도 없었고, 특히 다대다 면접은 전혀 경험해보지 못했기 때문에 새로운 경험이었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문의 유형은 자소서 기반도 있었고, 다대다 면접이기에 공통 질문과 그 외에 개인적인 질문도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;면접 분위기도 너무 좋았고, 압박보다는 지원자에 대한 생각과 의도를 궁금해한다는 느낌을 많이 받았었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 질문으로는 1분 자기소개서나 지원 동기 마지막으로 하고 싶은 말, 뽑아야 하는 이유? 이 정도였던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다대다 면접이다 보니 답변을 길게 장황하게 하기보다는 짧고 간결하고 핵심만 전달하는 것이 중요하겠구나..라고 면접 끝나고 느꼈던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접이 끝나고 별 기대를 하지 않았고, 일주일 뒤에 결과가 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 채용 프로세스가 시원시원하네요ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PCOPy/btsMyHTg8fy/6nInjMAJtB3QioMDSUqTV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PCOPy/btsMyHTg8fy/6nInjMAJtB3QioMDSUqTV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PCOPy/btsMyHTg8fy/6nInjMAJtB3QioMDSUqTV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPCOPy%2FbtsMyHTg8fy%2F6nInjMAJtB3QioMDSUqTV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;395&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Play 전형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOOP 채용 프로세스의 가장 큰 특이점은 Play 전형이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 SOOP 서비스를 많이 이용해 왔다고 자부할 수 있고, 지원 목표가 1차 면접 합격하고, Play 전형 경험만 해보자라고 할 정도로 기대를 많이 했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2차 면접과 같이 진행되는데, 해당 전형을 잘 보고 못 보고의 결과는 중요하지 않고, 네가 일하게 될 서비스를 경험만 해봐!라는 의도로 진행되는 단계였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1050&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWifoh/btsMzr3uTyN/LgObDufKSoVvvpkbtkYnS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWifoh/btsMzr3uTyN/LgObDufKSoVvvpkbtkYnS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWifoh/btsMzr3uTyN/LgObDufKSoVvvpkbtkYnS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWifoh%2FbtsMzr3uTyN%2FLgObDufKSoVvvpkbtkYnS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;500&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1050&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Play 전형 전날에 방송을 준비하면서 프릭샷이라는 도구를 사용해 봤는데, 정말 편리하더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 느낀 점도 2차 면접에 물어볼 수 있을 것 같아서 그런 점도 조금 생각해 보면서 방송을 준비했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHK1Ci/btsMysvdrwg/6wKJUMQSoK4LyZCyFT3ap1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHK1Ci/btsMysvdrwg/6wKJUMQSoK4LyZCyFT3ap1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHK1Ci/btsMysvdrwg/6wKJUMQSoK4LyZCyFT3ap1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHK1Ci%2FbtsMysvdrwg%2F6wKJUMQSoK4LyZCyFT3ap1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;732&quot; height=&quot;502&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 방송화면인데, 지인이나 주변 친구들, 그리고 SOOP 인사 담당 관계자분들까지 방송에 들어와 주셨습니다 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최고 인원은 32명까지 갔었는데, 재밌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 부담 없이 하고 싶은 거 하시면서 Play 전형 진행해 주시면 될 것 같아요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 처음에 큰 부담이 있었는데, 방송을 진행하면서 즐겼던 것 같아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2차 면접(최종)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Play 전형 이틀 뒤에 2차 면접이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 채용 후기를 참고하면서 준비했는데, 생각보다 SOOP에 대한 채용 후기 글이 없어서 취업 커뮤니티나 블로그 후기 글을 바탕으로 준비를 했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차는 직무였기에, 2차는 인성이지 않을까?라는 생각으로 직무보다는 인성을 위주로 준비를 했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 직무를 준비하면서 자소서 기반과 프로젝트, 포트폴리오 기반은 완벽하게 숙지를 한 상태였기에, 면접 경험이 없는 인성을 위주로 준비했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 유튜브(면접왕 이형, 강민혁)를 참고하면서 인성 면접을 준비했고, 예상 질문과 답변을 머릿속으로 생각하면서 갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주변 사람들은 그런 유튜브도 좋은 참고가 되지만, &lt;b&gt;인성 질문은 정답이 없기에 솔직하게 진심 있게 답변하면 좋은 결과가 있을 거다!라는&lt;/b&gt; 말을 가장 많이 들었던 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 면접은 회사 내부 사무실에서 진행되었고, 정말 떨렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접관님들은 2분, 지원자는 3분&amp;nbsp; 2:3으로 면접이 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 면접관님들과 거리가 가까웠고, 보시는 서류가 지원자 눈에도 보였기 때문에 더 긴장되었던 것 같습니다.&lt;br /&gt;(서류에 많은 것을 작성하셨었거든요 ㅋㅋㅋ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 최종 면접이다 보니까 더 긴장이 많이 되었고,&lt;b&gt; 직무 관련보다는 더 추상적인 개념을 많이 다뤘던 것 같습니다&lt;/b&gt;.(이건 직무마다 다를 것 같아요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 직무면접보다 더 어려웠고, 잘 본지 못 본 지 감이 안 왔달까..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자소서 기반, 공통 질문, 개인 질문과 같이 1차와 유사하게 진행되었고, 공통 질문으로는 자기소개서와 지원동기, SOOP의 장단점, 궁금한 점, 마지막으로 할 말 이런 식의 질문을 받았던 것 같아요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정말 솔직하게 제 생각을 얘기했던것 같고, 모른다면 모른다고 대답했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외는 면접 진행하면서 즉발적으로 생겨난 질문들이었기 때문에 아마 큰 도움이 안 될 것 같다는 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접을 진행하면서 느낀 점은 &lt;b&gt;해당 서비스에 대한 이해도가 정말 중요하다고 느꼈습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다대다 면접이고, &lt;b&gt;여러 면접을 진행하는 면접관님들인 만큼 답변의 길이가 길면 안 된다는 것을 또 깨달았습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 그렇고 답변의 길이가 길면 길수록 답변에 대한 핵심 포인트가 옅어지기 때문에 그러한 부분을 가장 깊이 고민하면서 면접을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;면접은 대화를 나누는 자리다! 너를 어떤 사람인지 표현해라!라고 생각했기에&lt;/b&gt; 기계처럼 외운 답변을 쏟기보단, 내 생각을 절더라도 천천히 얘기하려고 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 면접 자체가 정말 예상할 수 없는 질문이 많았기에 준비한 답변을 할 수가 없었습니다. ㅋㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 면접을 보고 일주일 뒤에 결과가 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV1J7W/btsMxmCBFLa/WpMKMzMBB2r5NDB2be9MjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV1J7W/btsMxmCBFLa/WpMKMzMBB2r5NDB2be9MjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV1J7W/btsMxmCBFLa/WpMKMzMBB2r5NDB2be9MjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV1J7W%2FbtsMxmCBFLa%2FWpMKMzMBB2r5NDB2be9MjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;755&quot; height=&quot;428&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 운이 좋게도 합격 통보를 받았습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 채용 시장도 좋지 않고, 특히 모바일 개발 직무에 대해서는 더욱 얼어붙어있다고 생각이 들었는데 원하는 기업, 원하는 직무에 갈 수 있는 것이 큰 행운이었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧다면 짧고, 길다면 긴 취준 기간이었지만, 큰 목표를 향해 천천히 노력하면 다들 좋은 결과가 있을 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(저도 그랬으니까요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 합격도 처음이었고, 면접 경험도 처음이었지만 채용 프로세스를 진행하면서 많은 공부가 되었고 자극을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 결과가 좋다면 더욱 좋겠지만, 채용 프로세스 내에서는 배운다는 마음가짐으로 멘탈을 유지한 것도 큰 도움이 된 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 블로그는 아마 현업에서 제가 부족한 부분을 다루지 않을까..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 부족하다고 생각했지만, 저도 합격한 만큼 다들 좋은 결과 있기를 바라겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Experience/후기(코딩테스트,프로그램,프로젝트)</category>
      <category>2025</category>
      <category>Android 개발</category>
      <category>soop</category>
      <category>신입 채용</category>
      <category>취업 후기</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/513</guid>
      <comments>https://jja2han.tistory.com/513#entry513comment</comments>
      <pubDate>Thu, 27 Feb 2025 00:12:36 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] - CoroutineExceptionHandler(Chapter8-2)</title>
      <link>https://jja2han.tistory.com/512</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/510&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] - 예외 전파 제한(Chapter 8-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 구조화된 동시성(Chapter7)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/505&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] async와 Deferred(Chapter5)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/504&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/503&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 코루틴이 어떻게 예외를 전파하고, 또 그 예외 전파를 어떻게 방지할 수 있는지에 대해 적어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 이렇게 전파된 에러를 어떻게 처리하는지, 그리고 코루틴은 &lt;b&gt;어떠한 에러 처리 방법&lt;/b&gt;을 제공하는지 적어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoroutineExceptionHandler를 사용한 예외 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 코루틴들에 공통적인 예외 처리기를 설정해야 할 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 이를 위해 CoroutineContext 구성 요소로 &lt;b&gt;CoroutineExceptionHandler&lt;/b&gt;라는 예외 처리기를 지원하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineExceptionHandler 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1737007945739&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public inline fun CoroutineExceptionHandler(crossinline handler: 
(CoroutineContext, Throwable) -&amp;gt; Unit): CoroutineExceptionHandler { /* compiled code */ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler 함수는 예외를 처리하는 람다식 &lt;b&gt;handler&lt;/b&gt;를 매개변수로 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler는 &lt;b&gt;CoroutineContext&lt;/b&gt;와 &lt;b&gt;Throwable&lt;/b&gt; 타입의 매개변수를 갖는 &lt;b&gt;람다식으로&lt;/b&gt;, 이 람다식에 &lt;b&gt;예외 발생 시 어떠한 동작을 수행할지 입력합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 CoroutineExceptionHandler 객체입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737008039433&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable-&amp;gt;
	println(&quot;[예외 발생] ${throwable}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineExceptionHandler 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 CoroutineExceptionHandler 객체는 CoroutineContext 구성요소로 포함될 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737008172742&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
        println(&quot;[예외 발생] $throwable&quot;)
    }
    CoroutineScope(exceptionHandler).launch(CoroutineName(&quot;Coroutine1&quot;)) {
        throw Exception(&quot;Coroutine1에 예외 발생!&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 함수가 호출되면 Job 객체가 새로 생성되므로, &lt;b&gt;구조화가 깨진다&lt;/b&gt;는 사실은 이제 다들 알고 계실거라 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 코루틴1은&lt;b&gt; CoroutineScope의 구성요소를 상속받기 때문에 선언된 exceptionHandler도 상속받습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 구조화하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sqH0G/btsLQBe28Hr/etSABTGp2vEguBZX9UzUI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sqH0G/btsLQBe28Hr/etSABTGp2vEguBZX9UzUI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sqH0G/btsLQBe28Hr/etSABTGp2vEguBZX9UzUI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsqH0G%2FbtsLQBe28Hr%2FetSABTGp2vEguBZX9UzUI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;358&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 exceptionHandler에 의해 예외가 처리돼 예외 정보가 출력됨을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJGdBw/btsLPu2glea/mmVoaJHkDc6AWDH7eR7VI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJGdBw/btsLPu2glea/mmVoaJHkDc6AWDH7eR7VI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJGdBw/btsLPu2glea/mmVoaJHkDc6AWDH7eR7VI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJGdBw%2FbtsLPu2glea%2FmmVoaJHkDc6AWDH7eR7VI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;671&quot; height=&quot;117&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 궁금증이 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exceptionHandler는 상속받아서 코루틴1과 코루틴스코프 객체 모두 설정돼 있는데, &lt;b&gt;어디에서 예외가 처리된 것일까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;처리되지 않은 예외만 처리하는 CoroutineExceptionHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CoroutineExceptionHandler 객체는 처리되지 않은 예외만 처리합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외가 전파되었다면 그 예외는 처리된 것으로 처리합니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴이 부모 코루틴으로 예외를 전파할 경우 자식 코루틴에 설정된 handler는 예외가 처리된 것으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기 때문에 동작하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 궁금증에 대답은 &lt;b&gt;코루틴 스코프&lt;/b&gt;라고 할 수 있겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737008683602&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
        println(&quot;[예외 발생] $throwable&quot;)
    }
    launch(CoroutineName(&quot;Coroutine1&quot;)+exceptionHandler) {
        throw Exception(&quot;Coroutine1에 예외 발생!&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 구조화가 깨지지 않은 채, runBlocking의 자식 코루틴으로 코루틴1이 생성되고, 핸들러는 코루틴1에만 설정됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmR77N/btsLPvtlE2W/g1JcVGEJ0epcKgCB5vfwL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmR77N/btsLPvtlE2W/g1JcVGEJ0epcKgCB5vfwL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmR77N/btsLPvtlE2W/g1JcVGEJ0epcKgCB5vfwL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmR77N%2FbtsLPvtlE2W%2Fg1JcVGEJ0epcKgCB5vfwL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;298&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 1에서 예외가 발생하므로 설정된 핸들러가 예외를 처리할 것처럼 보이지만, 실제 실행 결과는 에러가 처리되지 않고 종료됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSDQfd/btsLQRolNPi/a4x1knIgtIsxfG32mdANA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSDQfd/btsLQRolNPi/a4x1knIgtIsxfG32mdANA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSDQfd/btsLQRolNPi/a4x1knIgtIsxfG32mdANA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSDQfd%2FbtsLQRolNPi%2Fa4x1knIgtIsxfG32mdANA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1628&quot; height=&quot;254&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 코루틴1이 runBlocking 코루틴으로 예외를 전파했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 예외가 전파 == 예외 처리로 생각하기 때문에 핸들러는 이미 처리된 예외라고 생각해서 동작하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 계층 상 여러 CoroutineExceptionHandler가 설정돼 있더라도 마지막으로 예외를 전파받는 위치에 설정된 객체만 예외를 처리하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler가 동작하도록 만들기 위해서는 CoroutineExceptionHandler가 &lt;b&gt;설정된 위치를 오류가 처리되는 위치로 만들어야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineExceptionHandler가 예외를 처리하도록 만들기&lt;/h4&gt;
&lt;pre id=&quot;code_1737009122806&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
        println(&quot;[예외 발생] $throwable&quot;)
    }
    CoroutineScope(exceptionHandler).launch(CoroutineName(&quot;Coroutine1&quot;)) {
        throw Exception(&quot;Coroutine1에 예외 발생!&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 CoroutineExceptionHandler가 동작할 수 있었던 이유는 예외가 &lt;b&gt;마지막으로 처리되는 위치&lt;/b&gt;인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope가 CoroutineExceptionHandler 설정돼 있었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Job과 CoroutineExceptionHandler 함께 설정하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler가 예외를 처리하게 하는 가장 간단한 방법은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CoroutineExceptionHandler 객체를 루트 Job과 함께 설정하는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Job()을 호출하면 구조화를 끊고, 새로운 루트 Job을 만들 수 있으므로 이를 사용하면 CoroutineExceptionHandler 객체가 설정되는 위치를 마지막으로 예외를 전파받는 위치로 만들 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SupervisorJob과 CoroutineExceptionHandler 함께 사용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SupervisorJob 객체는 예외를 전파받지 않는 특수한 Job 객체입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 SupervisorJob과 CoroutineExceptionHandler를 함께 사용하면 어떻게 동작될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외를 전파 받지 않으니 Handler가 동작하지 않을까요?&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정답은 예외가 처리된다입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob 객체는 예외를 전파 받지 않을 뿐, &lt;b&gt;예외 발생에 대한 정보는 자식 코루틴으로부터 전달받습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보를 바탕으로 Handler 객체가 예외를 처리합니다&lt;/p&gt;
&lt;pre id=&quot;code_1737009480066&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
        println(&quot;[예외 발생] $throwable&quot;)
    }
    val supervisedScope = CoroutineScope(SupervisorJob() + exceptionHandler)
    supervisedScope.apply {
        launch(CoroutineName(&quot;Coroutine1&quot;)) {
            throw Exception(&quot;Coroutine1에 예외 발생!&quot;)
        }
        launch(CoroutineName(&quot;Coroutine2&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob 객체와 CoroutineExceptionHandler가 설정된 스코프 객체 아래에서 예외가 발생하면 SupervisorJob 객체에는 에러가 전파되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 예외에 대한 정보는 전달받으므로, 에러를 처리하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5PH5q/btsLQOrHqfq/kQ2qHwhpNJXoK1PXAeUN4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5PH5q/btsLQOrHqfq/kQ2qHwhpNJXoK1PXAeUN4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5PH5q/btsLQOrHqfq/kQ2qHwhpNJXoK1PXAeUN4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5PH5q%2FbtsLQOrHqfq%2FkQ2qHwhpNJXoK1PXAeUN4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;381&quot; height=&quot;283&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVaWhI/btsLOUf7PYl/kWTqkoKmQ8dKclR181vAs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVaWhI/btsLOUf7PYl/kWTqkoKmQ8dKclR181vAs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVaWhI/btsLOUf7PYl/kWTqkoKmQ8dKclR181vAs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVaWhI%2FbtsLOUf7PYl%2FkWTqkoKmQ8dKclR181vAs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;200&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineExceptionHandler는 예외 전파를 제한하지 않는다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler는 예외가 마지막으로 처리되는 위치에서 예외를 처리할 뿐, 예외 전파를 제한하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(저도 try-catch처럼 예외 전파를 제한한다고 생각했습니다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737010158984&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt;
        println(&quot;[예외 발생] $throwable&quot;)
    }
    launch(CoroutineName(&quot;Coroutine1&quot;)+exceptionHandler) {
        throw Exception(&quot;Coroutine1에 예외 발생!&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 핸들러가 설정된 코루틴1에 예외가 발생합니다. 하지만 코루틴1에서 발생한 예외는 runBlocking으로 전파되고, 종료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, CoroutineExceptionHandler는 예외 전파를 제한하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dioXt4/btsLP5t722m/epita2YzjTqZVX97EuZSjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dioXt4/btsLP5t722m/epita2YzjTqZVX97EuZSjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dioXt4/btsLP5t722m/epita2YzjTqZVX97EuZSjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdioXt4%2FbtsLP5t722m%2Fepita2YzjTqZVX97EuZSjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1642&quot; height=&quot;248&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;try catch 문을 사용한 예외 처리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;try catch 문을 사용해 코루틴 예외 처리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴에서 예외가 발생했을 때 코틀린에서 try-catch 문을 통해 예외를 처리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1737010462706&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        try {
            throw Exception(&quot;Coroutine1에 예외가 발생했습니다!&quot;)
        } catch (e: Exception) {
            println(e.message)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)){
        delay(100L)
        println(&quot;Coroutine2 실행 완료&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴1에서 예외가 발생하지만, try-catch 문을 통해 처리되고 있기 때문에 runBlocking 코루틴으로 예외가 전파되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드 실행 결과를 보면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgDRQj/btsLQrRgBhi/Mgn5rk3Hh6uMD7kbiMckHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgDRQj/btsLQrRgBhi/Mgn5rk3Hh6uMD7kbiMckHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgDRQj/btsLQrRgBhi/Mgn5rk3Hh6uMD7kbiMckHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgDRQj%2FbtsLQrRgBhi%2FMgn5rk3Hh6uMD7kbiMckHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;173&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 메시지가 출력되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 흔히들 try-catch 문을 코루틴 빌더 함수에 사용하는 실수를 하는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 코루틴에서 발생한 예외가 잡히지 않는데요, 왜 그런지 코드를 통해 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737010611668&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    try {
        launch(CoroutineName(&quot;Coroutine1&quot;)) {
            throw Exception(&quot;Coroutine1에 예외가 발생했습니다!&quot;)
        }
    } catch (e: Exception) {
        println(e.message)
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;Coroutine2 실행 완료&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전의 코드와 위 코드의 &lt;b&gt;다른 점은 launch 함수가 try-catch 내부에 있느냐, 밖에 있느냐입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;launch는 코루틴을 생성하는 데 사용되는 함수일 뿐이므로 람다식의 실행을 처리하진 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;람다식의 실행은 코투린이 스레드로 분배되는 시점에 일어나기 때문에 try-catch 딴에서는 처리할 대상이 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드를 실행해 보면 예외가 전파되며, 프로세스가 비정상 종료되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GoRX8/btsLQGtSAw7/h2kYDSYL3LtCn4WUjnGqA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GoRX8/btsLQGtSAw7/h2kYDSYL3LtCn4WUjnGqA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GoRX8/btsLQGtSAw7/h2kYDSYL3LtCn4WUjnGqA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGoRX8%2FbtsLQGtSAw7%2Fh2kYDSYL3LtCn4WUjnGqA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;100&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 중요한 점은 코루틴 빌더 함수에 try-catch 문이 아닌, 람다식 내부에서 try-catch를 사용해 에러를 처리해야 한다는 점입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async의 예외 처리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;async의 예외 노출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async는 다른 코루틴 빌더 함수와 다르게 &lt;b&gt;결괏값을 Deferred 객체로 감싸고 await 호출 시점에 결과값을 노출합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특성 때문에 코루틴 실행 중 예외가 발생해 &lt;b&gt;결괏값이 없다면 await 호출 시 예외가 노출됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737011292951&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    supervisorScope {
        val deferred = async(CoroutineName(&quot;Coroutine1&quot;)) {
            throw Exception(&quot;Coroutine1에 예외가 발생했습니다&quot;)
        }
        try {
            deferred.await()
        } catch (e: Exception) {
            println(&quot;[노출된 예외] ${e.message}&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 supervisorScope를 사용해 예외 전파를 방지하며, 내부에서 코루틴1이 async 함수에 의해 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴1에서 예외가 발생하므로 deferred에 대해 await 함수를 호출하면 예외가 외부로 노출되는데 이 처리를 위해 &lt;b&gt;try-catch 문으로 await 호출부를 감쌉니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과 에러가 try-catch에 의해 처리되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vl7po/btsLQEJBRh0/YlwDpt9f1PG2OoxYA8Nr1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vl7po/btsLQEJBRh0/YlwDpt9f1PG2OoxYA8Nr1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vl7po/btsLQEJBRh0/YlwDpt9f1PG2OoxYA8Nr1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvl7po%2FbtsLQEJBRh0%2FYlwDpt9f1PG2OoxYA8Nr1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;134&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 async 코루틴 빌더를 호출해 만들어진 코루틴에서 예외가 발생할 경우 await 호출부에서 예외를 처리해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async 함수 사용 시 많이 하는 실수 중 하나는 await 호출부에서만 예외를 처리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더 함수로 예외가 발생하면 코루틴으로 예외를 전파하는데, 이를 적절하게 처리해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737013419685&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    async(CoroutineName(&quot;Coroutine1&quot;)) {
        throw Exception(&quot;Coroutine1에 예외가 발생했습니다&quot;)
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name} 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 runBlocking의 자식 코루틴으로 코루틴1과 2가 만들어지며 async를 사용해 만들어진 코루틴1에서 예외가 발생됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Cwie/btsLOEEJlhU/d67TjpqsyEiMOx9tMybOn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Cwie/btsLOEEJlhU/d67TjpqsyEiMOx9tMybOn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Cwie/btsLOEEJlhU/d67TjpqsyEiMOx9tMybOn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Cwie%2FbtsLOEEJlhU%2Fd67TjpqsyEiMOx9tMybOn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;82&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await 호출부가 없음에도 예외 로그가 나오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 코루틴1에서 발생한 예외가 부모 코루틴으로 전파돼 부모 코루틴을 취소시키기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 근본적인 이유인 예외 전파를 방지해야 하는데요, 이를 위해 supervisorScope를 사용해 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737013568011&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    supervisorScope {
        async(CoroutineName(&quot;Coroutine1&quot;)) {
            throw Exception(&quot;Coroutine1에 예외가 발생했습니다&quot;)
        }
        launch(CoroutineName(&quot;Coroutine2&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name} 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w6OrH/btsLQmCyIIM/EEUxCxcvqRsCcJajUpCiW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w6OrH/btsLQmCyIIM/EEUxCxcvqRsCcJajUpCiW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w6OrH/btsLQmCyIIM/EEUxCxcvqRsCcJajUpCiW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw6OrH%2FbtsLQmCyIIM%2FEEUxCxcvqRsCcJajUpCiW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;554&quot; height=&quot;139&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실행결과를 보면 코루틴1이 예외를 전파하지 않아 코루틴2가 정상 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이처럼 async 코루틴 빌더를 사용할 때는 전파되는 예외와 await 호출 시 노출되는 예외를 모두 처리해줘야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전파되지 않는 예외&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전파되지 않는 CancellationException&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 CancellationException 예외가 발생해도 부모 코루틴으로 전파되지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737013749557&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;(CoroutineName(&quot;runBlocking 코루틴&quot;)) {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine2&quot;)) {
            throw CancellationException()
        }
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    delay(100L)
    println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 코루틴1에서 예외를 처리하는 핸들러를 설정하지 않았음에도 runBlocking 코루틴과 코루틴1이 정상 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UHrci/btsLQNNdlwG/ytstInRoL5g9ZLekXc5hnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UHrci/btsLQNNdlwG/ytstInRoL5g9ZLekXc5hnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UHrci/btsLQNNdlwG/ytstInRoL5g9ZLekXc5hnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUHrci%2FbtsLQNNdlwG%2FytstInRoL5g9ZLekXc5hnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;210&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이는 CancellationException의 특징 때문인데요, 해당 에러는 코루틴만 취소 시키고 전파되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VlrO1/btsLP1k8JFB/TXLz6BaHkkrQR5Gt37CPF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VlrO1/btsLP1k8JFB/TXLz6BaHkkrQR5Gt37CPF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VlrO1/btsLP1k8JFB/TXLz6BaHkkrQR5Gt37CPF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVlrO1%2FbtsLP1k8JFB%2FTXLz6BaHkkrQR5Gt37CPF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;273&quot; height=&quot;393&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코루틴 취소시 사용되는 JobCancellationException&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CancellationException은&lt;b&gt; 코루틴의 취소&lt;/b&gt;에 사용되는 특별한 예외이기 때문에 부모 코루틴으로 전파하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체에 cancel 함수를 호출하면 CancellationException의 서브 클래스인 JobCancellationException을 발생시켜 코루틴을 취소시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737014042918&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;{
    val job = launch {
        delay(1000L)
    }
    job.invokeOnCompletion { e-&amp;gt;
        println(e)
    }
    job.cancel()
}
/*
결과
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled;
job=&quot;coroutine#2&quot;:StandaloneCoroutine{Cancelled}@32e6e9c3
/*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초간 지속되는 Job을 만들고 invokeOnCompletion 함수를 통해 job에 발생한 예외를 출력하는 콜백을 등록하고, cancel 함수 호출을 통해 어떤 예외인지 살펴보는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실행 결과를 보면 JobCancellationException이 발생해 코루틴이 취소되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이처럼 CancellationException은 특정 코루틴만 취소하는 데 사용됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;withTimeOut 사용해 실행 시간 제한하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 제한 시간을 두고 작업을 실행할 수 있도록 만드는 withTimeOut 함수를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2728&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qU3Bw/btsLQNNez5a/4XPO1a5O7naelYRByzfIV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qU3Bw/btsLQNNez5a/4XPO1a5O7naelYRByzfIV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qU3Bw/btsLQNNez5a/4XPO1a5O7naelYRByzfIV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqU3Bw%2FbtsLQNNez5a%2F4XPO1a5O7naelYRByzfIV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;190&quot; data-origin-width=&quot;2728&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;withTimeOut 함수는 매개변수로 실행 제한 시간과 실행돼야 할 작업을 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 시간에 작업이 완료되지 않으면 &lt;b&gt;TimeOutCancellationException을 발생시키는데, 이는 위에서 언급됐던 CancellationException의 서브 클래스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 예외가 전파되지 않고, 예외가 발생한 코루틴만 취소시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737014504650&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;(CoroutineName(&quot;Parent Coroutine&quot;)){
    launch(CoroutineName(&quot;Child Coroutine&quot;)){
        withTimeout(1000L){
            delay(2000L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    delay(2000L)
    println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 자식 코루틴의 실행 시간을 1초로 제한시키고, 2초가 소요되는 작업을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 에러는 발생하고, withTimeOut은 TimeOutCancellationException을 발생시켜 자식 코루틴을 취소시키지만 예외는 전파되지 않고, 부모 코루틴이 정상적으로 실행된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;withTimeOut 함수는 실행 시간이 제한돼야 할 필요가 있는 다양한 작업에 사용되며, 대표적으로 네트워크 호출 실행 시간제한이 있다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실행 시간 초과 시 취소가 아닌 결과가 반환돼야 하는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 취소가 아닌 null을 반환하는 withTimeOutOrNull 함수를 사용하면 처리할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737014705412&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;(CoroutineName(&quot;Parent Coroutine&quot;)){
    launch(CoroutineName(&quot;Child Coroutine&quot;)){
        val result = withTimeoutOrNull(1000L){
            delay(2000L)
            return@withTimeoutOrNull
        }
        println(result)
    }
}
// 결과 null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션은 다양한 예외 상황에 노출되며, 예외를 적절히 처리해 애플리케이션의 안정성을 확보할 수 있다.&lt;/li&gt;
&lt;li&gt;코루틴은 비동기 작업을 실행할 때 사용되기 때문에 애플리케이션의 안정성을 위해 예외 처리가 필수적이다.&lt;/li&gt;
&lt;li&gt;코루틴에서 발생한 예외는 부모 코루틴으로 전파되며, 적절히 처리되지 않으면 최상위 루트 코루틴까지 전파된다.&lt;/li&gt;
&lt;li&gt;예외를 전파받은 코루틴이 취소되면 해당 코루틴의 모든 자식 코루틴에 취소가 전파되고, 취소된다.&lt;/li&gt;
&lt;li&gt;새로운 루트 Job 객체를 통해 코루틴의 구조화를 깨 코루틴의 예외 전파를 제한할 수 있다.&lt;/li&gt;
&lt;li&gt;SupervisorJob 객체는 예외를 전파받지 않는 특수한 Job 객체이며, 사용해 예외 전파를 제한할 수 있다.&lt;/li&gt;
&lt;li&gt;SupervisorJob 객체는 예외를 전파받지 않지만, 예외 정보는 전달받는다.&lt;/li&gt;
&lt;li&gt;예외가 전파되거나 예외 정보가 전달된 경우 해당 코루틴에서 예외가 처리된 것으로 본다.&lt;/li&gt;
&lt;li&gt;CoroutineExceptionHandler 객체는 이미 처리된 예외에 대해서 동작하지 않는다. 즉 마지막으로 예외가 전파되는 위치에 설정되지 않으면 동작하지 않는다.&lt;/li&gt;
&lt;li&gt;CoroutineExceptionHandler는 예외 전파를 제한하지 않는다.&lt;/li&gt;
&lt;li&gt;코루틴 빌더 함수에 대한 try-catch 문은 코루틴이 실행될 때 발생하는 예외를 잡지 못한다.&lt;/li&gt;
&lt;li&gt;CancellationException은 다른 예외와 다르게 부모 코루틴으로 전파되지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737015305579&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bEwyDr/hyX4rKOP2A/eQCFTOuNLuyPxt4RPvBTc0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/iHUkN/hyX4rxhDCr/dYxQamYzafx9vJszkwQwwK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/Ksvm3/hyX4kEVBDZ/lLaC48T3hq8kNIKNOPeq7k/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bEwyDr/hyX4rKOP2A/eQCFTOuNLuyPxt4RPvBTc0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/iHUkN/hyX4rxhDCr/dYxQamYzafx9vJszkwQwwK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/Ksvm3/hyX4kEVBDZ/lLaC48T3hq8kNIKNOPeq7k/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>코루틴</category>
      <category>코루틴의 정석</category>
      <category>코틀린</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/512</guid>
      <comments>https://jja2han.tistory.com/512#entry512comment</comments>
      <pubDate>Thu, 16 Jan 2025 17:15:25 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] - 누적합(1차원, 2차원)</title>
      <link>https://jja2han.tistory.com/511</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;누적합이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적합(Prefix Sum)은 배열이나 리스트에서 특정 구간의 합을 빠르게 계산하기 위해 사용하는 알고리즘 기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 입력 배열의 첫 번째 원소부터 특정 위치까지의 합을 저장한 배열을 말하며, 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 배열의 원소들을 더한 중간 결과를 미리 저장함으로써, 반복 계산을 최소화하고 효율성을 높입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 그림을 살펴보면 이해가 쉬울 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c13Zb1/btsLP6lVOEF/1m2kmiSvmyVQSHc1nDrMgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c13Zb1/btsLP6lVOEF/1m2kmiSvmyVQSHc1nDrMgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c13Zb1/btsLP6lVOEF/1m2kmiSvmyVQSHc1nDrMgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc13Zb1%2FbtsLP6lVOEF%2F1m2kmiSvmyVQSHc1nDrMgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;437&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;759&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력으로 [2,4,6,8,10] 이라는 배열이 주어진다면, 누적합은 위와 같이 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 누적합을 사용해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;누적합을 사용하는 이유&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;효율적인 구간 합 계산&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 배열에서 특정 구간의 합을 구하려면, 해당 구간의 원소를 하나씩 더해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 시간복잡도는 구간의 길이인 O(n)이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 누적합 배열을 사용하면 구간의 합을 O(1)로 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적합은 특정 위치까지의 합을 저장하기 때문에 누적합의 성질을 이용하면 쉽게 구할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgi370/btsLPwrSmJg/vGTzxtETxy830pJ0bkEK8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgi370/btsLPwrSmJg/vGTzxtETxy830pJ0bkEK8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgi370/btsLPwrSmJg/vGTzxtETxy830pJ0bkEK8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgi370%2FbtsLPwrSmJg%2FvGTzxtETxy830pJ0bkEK8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;278&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;585&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 우리가 2번째~4번째의 구간 합을 알고 싶다면 두 가지 접근 방법으로 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, 누적합 배열만을 이용해서 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A [4](4번까지의 합 정보가 저장됨) - A [1](1번까지의 합 정보가 저장됨)을 연산하면 2~4번까지의 구간합이 구해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로, 누적합 배열과 원본 숫자 배열을 이용해서 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A [4](4번까지의 합 정보가 저장됨) - A [2](2번까지의 합 정보가 저장됨) + N [2](2번 숫자)을 연산하면 구간합이 구해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복 계산 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에서 여러 번 구간 합을 계산해야 하는 경우, 매번 원소를 하나씩 더하면 중복 계산이 발생하기 마련입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적합은 한 번의 전처리로 이러한 중복 계산을 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;꿀팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통의 누적합은 이전 누적합의 정보와 현재 숫자의 정보를 더해서 배열을 구현합니다.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 배열의 시작은 0번 index이기 때문에, 이러한 연산 처리에 굉장히 까다롭습니다.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 배열의 시작지점을 0번이 아닌 1번으로 시작시켜서 연산 처리에 편리함을 더하는 테크닉도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre id=&quot;code_1737002848016&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) throws IOException {
    br = new BufferedReader(new InputStreamReader(System.in));
    st = new StringTokenizer(br.readLine());
    n = Integer.parseInt(st.nextToken());
    int[] num = new int[n + 1]; // 숫자 배열 1번 인덱스를 시작지점으로 하기 때문에 크기를 1 증가시킴
    int[] sum = new int[n + 1]; // 누적합 배열
    st = new StringTokenizer(br.readLine());
    for (int i = 1; i &amp;lt;= n; i++) { // 배열의 시작 인덱스를 1로 변경
        num[i] = Integer.parseInt(st.nextToken());
        sum[i] = sum[i - 1] + num[i];
    }
    st = new StringTokenizer(br.readLine());
    int start = Integer.parseInt(st.nextToken()); // 구하려는 누적합 시작 구간
    int end = Integer.parseInt(st.nextToken()); // 구하려는 누적합 끝 구간
    int answer1 = sum[end] - sum[start - 1];
    int answer2 = sum[end] - sum[start] + num[start];
    System.out.println(answer1 + &quot; == &quot; + answer2 + &quot; &quot; + (answer2 == answer1));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Buisp/btsLOo2J4LR/TO6luop806okuaZrmQp3j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Buisp/btsLOo2J4LR/TO6luop806okuaZrmQp3j1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Buisp/btsLOo2J4LR/TO6luop806okuaZrmQp3j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBuisp%2FbtsLOo2J4LR%2FTO6luop806okuaZrmQp3j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;304&quot; height=&quot;194&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서는 1차원 누적합에 대해서 다뤘다면 지금부터는 2차원 누적합에 대해 다뤄보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차원 누적합은 1차원 누적합보다 복잡하지만, 규칙을 발견하다면 쉽게 응용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw2T7U%2FbtsLOuBS4L4%2FjqDwllhdFdaWCnjnCE3ol0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;388&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 4*4 2차원 배열이 있다고 생각해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2,2) ~ (4,4)까지의 합을 구하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4+6+8+11+13+15+12+14+16 = 99가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손으로, 눈으로 하면 정말 쉽지만 코드적으로 풀어내려면 머리가 조금 띵해지실 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 그려보면서 구하면 조금 쉽게 접근할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8z9JQ/btsLOuop2cc/dubCIRvk2GwTmsD8vcrQ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8z9JQ/btsLOuop2cc/dubCIRvk2GwTmsD8vcrQ4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8z9JQ/btsLOuop2cc/dubCIRvk2GwTmsD8vcrQ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8z9JQ%2FbtsLOuop2cc%2FdubCIRvk2GwTmsD8vcrQ4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;346&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 1x1부터 4x4까지의 이차원 누적합을 저장한 배열입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 (1,1) ~ (2,2)의 누적합을 구하는 과정을 예로 들어 설명하면 이해하기 쉬울 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금부터는 원본 배열을 건들지 않고, 누적합만을 이용해서 구해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2,2의 누적합은&amp;nbsp; 다음과 같이 구할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot; width=&quot;358&quot; height=&quot;388&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;744&quot; data-origin-width=&quot;686&quot; style=&quot;width: 47.4617%; margin-right: 10px;&quot; data-widthpercent=&quot;48.02&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w2T7U/btsLOuBS4L4/jqDwllhdFdaWCnjnCE3ol0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw2T7U%2FbtsLOuBS4L4%2FjqDwllhdFdaWCnjnCE3ol0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2l08L/btsLPvs70oL/IigaTBKn28Ll0kGBneyBD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2l08L/btsLPvs70oL/IigaTBKn28Ll0kGBneyBD0/img.png&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;520&quot; data-is-animation=&quot;false&quot; width=&quot;351&quot; height=&quot;352&quot; data-widthpercent=&quot;51.98&quot; style=&quot;width: 51.3755%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2l08L/btsLPvs70oL/IigaTBKn28Ll0kGBneyBD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2l08L%2FbtsLPvs70oL%2FIigaTBKn28Ll0kGBneyBD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;519&quot; height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간 영역의 누적합과, 파란 영역의 누적합&amp;nbsp; 그리고 4를 더한 값에 중복된 1을 빼주면 10이라는 결과가 나오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 2차원의 누적합에선 중복이 발생할 수밖에 없습니다. 왜냐하면 왼쪽에서 접근하고, 위쪽에서 접근하는데 두 방향은 중복된 원소를 가질 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 식으로 바꿔볼까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S[2][2] = S[1][2]([1,1] ~ [1,2] 합이 저장됨) + S[2][1]([1,1] ~ [2,1] 합이 저장됨) + A[2][2](원래 숫자) - A[1][1](중복 숫자)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 영역을 확장해서 생각해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;571&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RiVXW/btsLO8EZ5p6/JkQELZrKPyhtjEBYjS5mX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RiVXW/btsLO8EZ5p6/JkQELZrKPyhtjEBYjS5mX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RiVXW/btsLO8EZ5p6/JkQELZrKPyhtjEBYjS5mX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRiVXW%2FbtsLO8EZ5p6%2FJkQELZrKPyhtjEBYjS5mX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;310&quot; height=&quot;311&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;571&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1,1) ~ (3,3)의 구간합인 54를 구하기 위해서는 표시된 영역들과 (3,3) 위치에 있는 숫자가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간색 영역과 파란색 영역을 더하는 과정에서 보라색의 영역이 중복이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중복을 제거해 줘야 정확한 누적합을 구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리가 원하는 (i, j)로 공식화해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR28Uf/btsLOqTSh13/y05zQuCEvgN9QkEcecRgp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR28Uf/btsLOqTSh13/y05zQuCEvgN9QkEcecRgp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR28Uf/btsLOqTSh13/y05zQuCEvgN9QkEcecRgp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR28Uf%2FbtsLOqTSh13%2Fy05zQuCEvgN9QkEcecRgp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;419&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737004677912&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) throws IOException {
    br = new BufferedReader(new InputStreamReader(System.in));
    st = new StringTokenizer(br.readLine());
    n = Integer.parseInt(st.nextToken());
    m = Integer.parseInt(st.nextToken());
    int[][] num = new int[n + 1][m + 1];
    int[][] sum = new int[n + 1][m + 1];
    for (int i = 1; i &amp;lt;= n; i++) {
        st = new StringTokenizer(br.readLine());
        for (int j = 1; j &amp;lt;= m; j++) {
            num[i][j] = Integer.parseInt(st.nextToken());
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + num[i][j];
        }
    }
    for (int i = 1; i &amp;lt;= n; i++) {
        for (int j = 1; j &amp;lt;= m; j++) {
            System.out.print(sum[i][j] + &quot; &quot;);
        }
        System.out.println();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPnF1u/btsLP2D08gg/jFbL4JhrnBG3m6PNLBPKKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPnF1u/btsLP2D08gg/jFbL4JhrnBG3m6PNLBPKKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPnF1u/btsLP2D08gg/jFbL4JhrnBG3m6PNLBPKKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPnF1u%2FbtsLP2D08gg%2FjFbL4JhrnBG3m6PNLBPKKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;438&quot; height=&quot;392&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;꿀팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 문제에서 특정 크기만큼 구간을 잘라서 구간합을 구하는 문제도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 (2,2) ~ (4,4)를 구한다던지와 같은 문제가 있는데 그럴 경우는 다음과 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737004844969&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = k; i &amp;lt;= n; i++) {
    for (int j = k; j &amp;lt;= m; j++) {
        cnt = Math.min(
        	cnt, 
       		sum[i][j] - (sum[i - k][j] + sum[i][j - k] - sum[i - k][j - k])
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 좌표가 (4,4)이고 크기가 3인 사각형이라면&amp;nbsp; (2,2) ~ (4,4)의 구간합을 구하는 구현식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적합은 간단한 아이디어지만, 전처리, 중복 계산 방지와 같은 효율성을 크게 개선할 수 있는 강력한 기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차원 누적합은 간단하고, 금방 떠올릴 수 있지만 2차원 누적합은 한번 풀어보고 익혀서 푸는 것이 코딩테스트나, 다른 응용에서 쉽게 떠올릴 수 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Algorithm</category>
      <category>누적합</category>
      <category>알고리즘</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/511</guid>
      <comments>https://jja2han.tistory.com/511#entry511comment</comments>
      <pubDate>Thu, 16 Jan 2025 14:24:57 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] - 예외 전파 제한(Chapter 8-1)</title>
      <link>https://jja2han.tistory.com/510</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/509&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 구조화된 동시성(Chapter7)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/505&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] async와 Deferred(Chapter5)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/504&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/503&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 사용하고, 개발하는 애플리케이션은 여러 상황에서 에러에 노출됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dynY3x/btsLLDLRRLd/0mNwKUBAfNOJ2hhKzMmX6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dynY3x/btsLLDLRRLd/0mNwKUBAfNOJ2hhKzMmX6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dynY3x/btsLLDLRRLd/0mNwKUBAfNOJ2hhKzMmX6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdynY3x%2FbtsLLDLRRLd%2F0mNwKUBAfNOJ2hhKzMmX6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;269&quot; height=&quot;269&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생했을 때 예외가 적절히 처리되지 않으면 예측하지 못한 방향으로 동작하거나 비정상 종료될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 안정적인 애플리케이션을 위해선 예외를 적절하게 처리하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 맥락에서 비동기 작업을 수행하는 코루틴의 예외 처리 또한 중요한데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 네트워크 요청이나 데이터 베이스 작업 같은 입출력 작업과 같이 중요한 작업에서 쓰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이러한 작업은 예측할 수 없는 예외가 발생할 가능성이 높아 코루틴에 대한 적절한 예외 처리는 안정적인 애플리케이션을 만드는 데 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 예외를 안전하게 처리할 수 있도록 만드는 여러 장치를 갖고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생했을 때 코루틴이 어떻게 동작하는지, 그리고 어떻게 처리하는지 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴의 예외 전파&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코루틴에서 예외가 전파되는 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 챕터에서 코루틴의 취소 전파에 대해서 다뤘습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 취소는 부모 코루틴 -&amp;gt; 자식 코루틴과 같이 위에서 아래 방향으로 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 비슷하게 코루틴에서의 예외는 자식 -&amp;gt; 부모로 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생한 코루틴은 취소되고 부모 코루틴으로 전파됩니다. 만약 그 부모 코루틴도 적절한 대체가 없다면 다시 상위 코루틴으로 전파되는데, 이것이 반복된다면 최상위 코루틴, 즉 루트 코루틴까지 예외가 전파될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 전파는 굉장히 위험합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 루트 코루틴이 취소된다면, 모든 코루틴에게 취소가 전파되는 최악의 상황이 나옵니다.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 구조화된 코루틴이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AkZsh/btsLKJ6Y9KQ/sk5AC9W7qbU7b4ok9f78Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AkZsh/btsLKJ6Y9KQ/sk5AC9W7qbU7b4ok9f78Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AkZsh/btsLKJ6Y9KQ/sk5AC9W7qbU7b4ok9f78Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAkZsh%2FbtsLKJ6Y9KQ%2Fsk5AC9W7qbU7b4ok9f78Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;345&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 코루틴 5에서 예외가 발생하면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dE9rxY/btsLLuajkP8/c3PlQOBc3qv3kf5hk1JTB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dE9rxY/btsLLuajkP8/c3PlQOBc3qv3kf5hk1JTB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dE9rxY/btsLLuajkP8/c3PlQOBc3qv3kf5hk1JTB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdE9rxY%2FbtsLLuajkP8%2Fc3PlQOBc3qv3kf5hk1JTB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;363&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외는 부모로 전파되기 때문에 Coroutine2 -&amp;gt; Coroutine1으로 예외가 전파됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 그전에 예외가 적절하게 대처되면 좋겠지만, 만약 적절히 처리되지 않는다면 Coroutine1은 취소됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VJX5W/btsLKC0Yq7y/iJZ2BtN0uG2DmvIS4uxkG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VJX5W/btsLKC0Yq7y/iJZ2BtN0uG2DmvIS4uxkG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VJX5W/btsLKC0Yq7y/iJZ2BtN0uG2DmvIS4uxkG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVJX5W%2FbtsLKC0Yq7y%2FiJZ2BtN0uG2DmvIS4uxkG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;614&quot; height=&quot;316&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴이 취소되면 모든 자식 코루틴에게 취소가 전파되는 특성 때문에 모든 코루틴에 취소가 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 코루틴의 예외 전파를 제대로 막지 못하면 구조화된 코루틴이 모두 취소될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통해 알아봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1736757090141&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            throw Exception(&quot;예외 발생!&quot;)
        }
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runBlocking 코루틴은 Coroutine1,2를 자식 코루틴으로 갖고, Coroutine1은 Coroutine3을 자식 코루틴으로 갖습니다.&lt;br /&gt;Coroutine3에서 예외가 발생했고, 코드의 실행 결과는 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xym2B/btsLK61VOAP/BeNVNbaMxW7oEdPQKABFxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xym2B/btsLK61VOAP/BeNVNbaMxW7oEdPQKABFxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xym2B/btsLK61VOAP/BeNVNbaMxW7oEdPQKABFxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXym2B%2FbtsLK61VOAP%2FBeNVNbaMxW7oEdPQKABFxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;112&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 출력문 없이 예외가 발생했다는 로그만 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Coroutine3에서 발생한 예외가 모든 코루틴을 취소시킨 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 구조화는 큰 작업을 연관된 작은 작업으로 나누는 방식으로 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 작은 작업에서 발생한 예외로 인해 큰 작업이 취소된다면 안정적인 애플리케이션은 아닐 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외 전파 제한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Job 객체를 사용한 예외 전파 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Job 객체를 사용해 예외 전파 제한하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 예외 전파를 제한하기 위한 방법은 코루틴의 구조화를 깨는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 챕터에서 코루틴의 구조화를 깨는 두 가지 방법을 다뤄봤었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Job 생성 후 전달&lt;/li&gt;
&lt;li&gt;CoroutineScope 생성 후 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이코루틴은 자신의 부모 코루틴으로만 예외를 전파하는 특성을 가지고 있으므로, 구조화를 깬다면 예외가 전파되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방법은 새로운 Job 객체를 만들어 구조화를 깨는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736757631758&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Parent Coroutine&quot;)) {
        launch(CoroutineName(&quot;Coroutine1&quot;) + Job()) {
            launch(CoroutineName(&quot;Coroutine3&quot;)) {
                throw  Exception(&quot;예외 발생!&quot;)
            }
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 Coroutine1은 새로운 Job 객체를 부모 Job으로 설정함으로써 Parent Coroutine 과의 구조화를 깨고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Coroutine3이 예외를 발생시켜도, Coroutine1은 예외가 전파되고, Parent Coroutine은 예외가 전파되진 않습니다. 대신 새롭게 만들어진 Job 객체에 예외를 전파합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V6FAE/btsLKGvBKXB/UJTvQkXcy2v1npSEWr0Hw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V6FAE/btsLKGvBKXB/UJTvQkXcy2v1npSEWr0Hw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V6FAE/btsLKGvBKXB/UJTvQkXcy2v1npSEWr0Hw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV6FAE%2FbtsLKGvBKXB%2FUJTvQkXcy2v1npSEWr0Hw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;362&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parent Coroutine에는 예외가 전파되지 않아 Coroutine2는 정상적으로 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bizUqc/btsLK6gzeVX/dt3tljputeyTipOvvbYd8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bizUqc/btsLK6gzeVX/dt3tljputeyTipOvvbYd8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bizUqc/btsLK6gzeVX/dt3tljputeyTipOvvbYd8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbizUqc%2FbtsLK6gzeVX%2Fdt3tljputeyTipOvvbYd8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;113&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Job 객체를 사용한 예외 전파 제한의 한계&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체를 생성해 코루틴의 구조화를 깨는 것은 예외뿐만 아니라 취소 전파도 제한시키는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 작업의 구조화가 깨진다면 큰 작업에 취소가 요청되더라도 작은 작업은 취소되지 않으며 이는 비동기 작업을 불안정하게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통해 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736758337710&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val parentJob = launch(CoroutineName(&quot;Parent Coroutine&quot;)) {
        launch(CoroutineName(&quot;Coroutine1&quot;) + Job()) {
            launch(CoroutineName(&quot;Coroutine3&quot;)) {
                delay(100L)
                println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
            }
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    delay(20L)
    parentJob.cancel()
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coroutine1은 더 이상 Parent Coroutine의 자식 코루틴이 아니기 때문에 취소 전파가 제한됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드를 실행해 보면&amp;nbsp;Parent Coroutine이 취소됐음에도 Coroutine1과 Coroutin3은 정상 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TpVHV/btsLKqGuY89/mBNZqkNb629H1KFf6K375K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TpVHV/btsLKqGuY89/mBNZqkNb629H1KFf6K375K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TpVHV/btsLKqGuY89/mBNZqkNb629H1KFf6K375K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTpVHV%2FbtsLKqGuY89%2FmBNZqkNb629H1KFf6K375K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;194&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 흐름에서는 Parent Coroutine이 취소된다면 Coroutine1과 Coroutine3도 함께 취소 돼야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 예외 전파 방지를 위해 새로운 Job 객체를 사용해 구조화가 깨져 버려 두 코루틴은 정상 실행 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구조화를 깨지 않으면서 예외 전파를 제한할 수 있는 방법이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 이것을 가능하게 하기 위해 &lt;b&gt;SupervisorJob&lt;/b&gt; 객체를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SupervisorJob 객체를 사용한 예외 전파 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SupervisorJob 객체를 사용해 예외 전파 제한하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmOkAE/btsLM51lIDS/DdITECeBRreT03NjPJEPbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmOkAE/btsLM51lIDS/DdITECeBRreT03NjPJEPbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmOkAE/btsLM51lIDS/DdITECeBRreT03NjPJEPbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmOkAE%2FbtsLM51lIDS%2FDdITECeBRreT03NjPJEPbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;791&quot; height=&quot;46&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;자식 코루틴으로부터 예외를 전파받지 않는 특수한 Job 객체로 하나의 자식 코루틴에서 발생한 예외가 다른 자식 코루틴에게 영향을 미치지 못하도록 만드는 데 사용된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Job 객체는 예외가 발생하면 예외를 전파받아 취소되지만 SupervisorJob 객체를 취소되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob 객체도, Parent 인자 없이 사용하면 루트 Job으로 만들 수 있으며 parent 인자를 넘기면 부모 Job이 있는 SupervisjorJob 객체를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통해 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736759157146&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val supervisorJob = SupervisorJob()
    launch(CoroutineName(&quot;Coroutine1&quot;) + supervisorJob) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            throw Exception(&quot;예외 발생!!&quot;)

        }
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    launch(CoroutineName(&quot;Coroutine2&quot;) + supervisorJob) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 1의 부모 Job을 supervisorJob으로 전달한 코드입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxdqBy/btsLM0mzAYs/nNfX8wpMLWBvvlRqxuoEhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxdqBy/btsLM0mzAYs/nNfX8wpMLWBvvlRqxuoEhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxdqBy/btsLM0mzAYs/nNfX8wpMLWBvvlRqxuoEhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxdqBy%2FbtsLM0mzAYs%2FnNfX8wpMLWBvvlRqxuoEhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;416&quot; height=&quot;271&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 코루틴 3의 에러가 코루틴 1로 전파돼 코루틴 1을 취소시키지만, 코루틴 1은 supervisorJob으로 예외를 전파하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드를 실행해 보면 supervisorJob의 다른 자식 코루틴인 Coroutine2 코루틴이 정상 실행되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SnfW0/btsLMuhz5i6/b2BPYNXZpC7wQUAeSQC3r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SnfW0/btsLMuhz5i6/b2BPYNXZpC7wQUAeSQC3r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SnfW0/btsLMuhz5i6/b2BPYNXZpC7wQUAeSQC3r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSnfW0%2FbtsLMuhz5i6%2Fb2BPYNXZpC7wQUAeSQC3r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;122&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob 객체는 자식 코루틴의 예외를 전파받지 않는 특성을 가지고 있습니다.&lt;br /&gt;하지만 위 구조화의 그림처럼 SupervisorJob 객체는 runBlocking이 호출돼 만들어진 Job 객체와의 구조화를 깨는 점이 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코루틴의 구조화를 깨지 않고 SupervisorJob 사용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화를 깨지 않고 SupervisorJob을 사용하기 위해서는 인자로 부모 Job 객체를 넘기면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736842281993&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
    launch(CoroutineName(&quot;Coroutine1&quot;) + supervisorJob) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            throw Exception(&quot;예외 발생!!&quot;)

        }
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    launch(CoroutineName(&quot;Coroutine2&quot;) + supervisorJob) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    supervisorJob.complete()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob()을 통해 생성된 Job 객체는 Job()을 통해 생성된 객체와 같이 자동으로 완료 처리가 되지 않기 때문에 명시적 완료 처리인 complete를 호출해 완료 처리를 해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 supervisorJob의 인자로 부모 Job을 넘겨주면, runBlocking 코루틴과의 구조화를 깨지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YO1ny/btsLN9CKtlH/uG5PYnX9npKKYqaJWahdhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YO1ny/btsLN9CKtlH/uG5PYnX9npKKYqaJWahdhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YO1ny/btsLN9CKtlH/uG5PYnX9npKKYqaJWahdhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYO1ny%2FbtsLN9CKtlH%2FuG5PYnX9npKKYqaJWahdhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;305&quot; height=&quot;264&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SupervisorJob을 CoroutineScope와 함께 사용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope의 CoroutineContext에 SupervisorJob 객체가 설정된다면 CoroutineScope의 자식 코루틴에서 발생하는 예외가 다른 자식 코루틴으로 전파되지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736842611023&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val coroutineScope = CoroutineScope(SupervisorJob())
    coroutineScope.apply {
        launch(CoroutineName(&quot;Coroutine1&quot;)) {
            launch(CoroutineName(&quot;Coroutine3&quot;)) {
                throw Exception(&quot;예외 발생!!&quot;)

            }
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 CoroutineScope 생성 함수의 SupervisorJob()을 인자로 넘겨 CoroutineScope 객체가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새로운 CoroutineScope의 생성으로 runBlocking 코루틴과의 구조화는 깨지게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf9Yee/btsLMqfck0y/tat7dU54t7fV9vB2gcI8s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf9Yee/btsLMqfck0y/tat7dU54t7fV9vB2gcI8s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf9Yee/btsLMqfck0y/tat7dU54t7fV9vB2gcI8s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf9Yee%2FbtsLMqfck0y%2Ftat7dU54t7fV9vB2gcI8s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;465&quot; height=&quot;293&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행해 보면 예외가 발생했음에도 코루틴 2가 정상 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM3TYK/btsLMKdlYZd/2iSrMvo5KueSK9AVP0o5Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM3TYK/btsLMKdlYZd/2iSrMvo5KueSK9AVP0o5Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM3TYK/btsLMKdlYZd/2iSrMvo5KueSK9AVP0o5Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM3TYK%2FbtsLMKdlYZd%2F2iSrMvo5KueSK9AVP0o5Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;92&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SupervisorJob을 사용할 때 흔히 하는 실수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 전파 방지를 위해 코루틴 빌더 함수의 context 인자에 SupervisorJob()을 넘겨 생성되는 코루틴의 하위에 자식 코루틴을 생성하는 것은 큰 문제를 내포하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736842861418&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Parent Coroutine&quot;)+ SupervisorJob()) {
        launch(CoroutineName(&quot;Coroutine1&quot;)) {
            launch(CoroutineName(&quot;Coroutine3&quot;)) {
                throw  Exception(&quot;예외 발생!&quot;)
            }
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 문제가 없어 보이지만, launch 함수는 context 인자에 Job 객체가 입력될 경우 해당 Job 객체를 부모로 하는 새로운 Job 객체를 새로 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, launch 함수에 SupervisorJob()을 인자로 넘기면 SupervisorJob()을 통해 만들어지는 새로운 Job 객체가 만들어져 다음과 같은 구조가 형성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/46G5H/btsLOtA0Rl9/2vgkKEEmgMbCIUBaQ2Jne0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/46G5H/btsLOtA0Rl9/2vgkKEEmgMbCIUBaQ2Jne0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/46G5H/btsLOtA0Rl9/2vgkKEEmgMbCIUBaQ2Jne0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F46G5H%2FbtsLOtA0Rl9%2F2vgkKEEmgMbCIUBaQ2Jne0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;326&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이런 구조에서 코루틴 3에 예외가 발생하면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wUvaa/btsLL8r8Sah/XKTz9bFyqjkKQJV5AdzY6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wUvaa/btsLL8r8Sah/XKTz9bFyqjkKQJV5AdzY6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wUvaa/btsLL8r8Sah/XKTz9bFyqjkKQJV5AdzY6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwUvaa%2FbtsLL8r8Sah%2FXKTz9bFyqjkKQJV5AdzY6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;318&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 3에서 발생한 예외가 코루틴 1을 통해 부모 코루틴까지 전파돼 취소되며, 자식 코루틴인 코루틴 2도 취소됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 코루틴의 예외가 SupervisorJob 객체로 전파되지는 않지만 이는 아무런 쓸모가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 SupervisorJob 객체는 예외 전파를 방지하는 강력한 도구이지만, 잘못 사용하면 기능을 제대로 수행하지 못할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Job 계층 구조를 정확하게 파악하고, 어떤 위치에 있어야 하는지 숙지한 후 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;supervisorScope를 사용한 예외 전파 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 전파의 세 번째 방법은 supervisorScope 함수를 사용하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 함수는 SupervisorJob 객체를 가진 CoroutineScope 객체를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 내부 구현으로 CoroutineScope에 SupervisorJob를 인자로 넘겨 복잡한 설정 없이 구조화를 깨지 않고 예외 전파를 제한하는 CoroutineScope를 생성할 수 있습니다. 또한 명시적 완료 호출 없이 자식 코루틴이 모두 실행 완료되면 자동으로 완료 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736843709110&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    supervisorScope {
        launch(CoroutineName(&quot;Coroutine1&quot;)) {
            launch(CoroutineName(&quot;Coroutine3&quot;)) {
                throw Exception(&quot;예외 발생!&quot;)
            }
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine2&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 runBlocking 함수에 의해 루트 Job 객체가 생성되고, 자식 코루틴으로 SupervisorJob 객체를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 아래에 코루틴 1, 코루틴 2, 코루틴 3을 자식 코루틴으로 가집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HrNA4/btsLNVrlRwg/7g93GAvkwnQTGkffgjvPD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HrNA4/btsLNVrlRwg/7g93GAvkwnQTGkffgjvPD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HrNA4/btsLNVrlRwg/7g93GAvkwnQTGkffgjvPD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHrNA4%2FbtsLNVrlRwg%2F7g93GAvkwnQTGkffgjvPD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;372&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 만약 코루틴 3에서 예외가 발생한다면 코루틴 1까지만 예외가 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드 실행 결과 코루틴 2가 정상적으로 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b18veQ/btsLOplaOUp/s99m631kQADOWhvKWHgvjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b18veQ/btsLOplaOUp/s99m631kQADOWhvKWHgvjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b18veQ/btsLOplaOUp/s99m631kQADOWhvKWHgvjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb18veQ%2FbtsLOplaOUp%2Fs99m631kQADOWhvKWHgvjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;140&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 코루틴의 예외 전파를 제한하는 세 가지 방법을 알아봤습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Job 생성&lt;/li&gt;
&lt;li&gt;SupervisorJob&lt;/li&gt;
&lt;li&gt;SupervisorScope&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 장점이 있음로 상황에 맞게 고민하고 사용하는 것이 정답이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 예외 전파를 제한 하는 방법에 대해 알아봤다면, 다음 글에서는 예외를 처리하는 방법을 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736844266868&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eskXY4/hyX0mjJq4r/tp70DPWLzCsaBdnawo8Xuk/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/j72Wl/hyX0lLSMxV/OY4T4FgEpGTnAtDVLPg64k/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/4s2DS/hyX0v8No3M/OAYbc4FF9hJdYD0BTtKzVk/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eskXY4/hyX0mjJq4r/tp70DPWLzCsaBdnawo8Xuk/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/j72Wl/hyX0lLSMxV/OY4T4FgEpGTnAtDVLPg64k/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/4s2DS/hyX0v8No3M/OAYbc4FF9hJdYD0BTtKzVk/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>코루틴의 정석</category>
      <category>코틀린</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/510</guid>
      <comments>https://jja2han.tistory.com/510#entry510comment</comments>
      <pubDate>Tue, 14 Jan 2025 17:44:33 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] 구조화된 동시성(Chapter7)</title>
      <link>https://jja2han.tistory.com/509</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/505&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] async와 Deferred(Chapter5)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/504&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/503&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조화된 동시성의 원칙&lt;/b&gt;이란 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적이고 예측 가능할 수 있게 만드는 원칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 부모-자식 관계로 구조화함으로써 보다 안전하게 관리되고 제어될 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코루틴을 부모-자식 관계로 구조화하는 방법은 간단하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 코루틴을 만드는 코루틴 빌더의 람다식 속에서 새로운&lt;b&gt; 코루틴 빌더를 호출&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1732537788891&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    launch { 
        // 부모 코루틴
        launch { 
            // 자식 코루틴
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 구조화하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btt9Vo/btsKW5ux8Q7/kTPcgW5e7BQhXHTj5gheO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btt9Vo/btsKW5ux8Q7/kTPcgW5e7BQhXHTj5gheO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btt9Vo/btsKW5ux8Q7/kTPcgW5e7BQhXHTj5gheO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtt9Vo%2FbtsKW5ux8Q7%2FkTPcgW5e7BQhXHTj5gheO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;337&quot; height=&quot;431&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 코루틴은 다음과 같은 여러 특징을 갖는다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;부모 코루틴의 실행 환경이 자식 코루틴에게 상속&lt;/li&gt;
&lt;li&gt;작업을 제어하는 데 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부모 코루틴이 취소되면 자식 코루틴도 취소된다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부모 코루틴은 자식 코루틴이 완료될 때까지 대기한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CoroutineScope를 사용해 코루틴이 실행되는 범위를 제한할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 환경 상속&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;부모 코루틴의 실행 환경 상속&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 코루틴이 자식 코루틴을 생성하면 부모 코루틴의 CoroutineContext가 &lt;b&gt;자식 코루틴에게 전달&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1732538246543&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val coroutineContext = newSingleThreadContext(&quot;MyThread&quot;) + CoroutineName(&quot;CustomCoroutine&quot;)
    launch(coroutineContext) {
        println(&quot;[${Thread.currentThread().name}] 부모 코루틴 실행&quot;)
        launch {
            println(&quot;[${Thread.currentThread().name}] 자식 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYFhZL/btsKUHPRLSV/unZ6XEO5O4KKtRvIO4fkok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYFhZL/btsKUHPRLSV/unZ6XEO5O4KKtRvIO4fkok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYFhZL/btsKUHPRLSV/unZ6XEO5O4KKtRvIO4fkok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYFhZL%2FbtsKUHPRLSV%2FunZ6XEO5O4KKtRvIO4fkok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;141&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 코루틴은 coroutineContext에 설정된 대로 MyThread 스레드를 사용해 실행되고 코루틴 이름이 설정된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴은 coroutineContext를 설정하지 않았지만, 부모 코루틴과 이름이 같은 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 이유는 부모 코루틴의 실행 환경을 담는 CoroutineContext 객체가 자식 코루틴에게 상속되기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 환경 덮어쓰기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 부모 코루틴의 모든 실행 환경이 &lt;b&gt;항상 자식 코루틴에게 상속되지는 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴 빌더에게&lt;b&gt; 새로운 실행 환경이 전달&lt;/b&gt;되면 CoroutineContext 구성 요소들은 덮어씌워진다.&lt;/p&gt;
&lt;pre id=&quot;code_1732538512981&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val coroutineContext = newSingleThreadContext(&quot;MyThread&quot;) + CoroutineName(&quot;ParentCoroutine&quot;)
    launch(coroutineContext) {
        println(&quot;[${Thread.currentThread().name}] 부모 코루틴 실행&quot;)
        launch(CoroutineName(&quot;ChildCoroutine&quot;)) {
            println(&quot;[${Thread.currentThread().name}] 자식 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ECh52/btsKVUt3pRD/ngkb1SGg00HbpkZgpstlKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ECh52/btsKVUt3pRD/ngkb1SGg00HbpkZgpstlKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ECh52/btsKVUt3pRD/ngkb1SGg00HbpkZgpstlKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FECh52%2FbtsKVUt3pRD%2Fngkb1SGg00HbpkZgpstlKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;169&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴 빌더에 context 인자로 전달된 CoroutineContext는 부모 코루틴에게 전달받은 CoroutineContext을 덮어 씌운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모와 자식 코루틴의 CoroutineContext 객체에 CoroutineName이 중복으로 포함돼 있다면 자식 코루틴 빌더의 CoroutineName이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 자식 코루틴 빌더에 새로운 객체를 전달함으로써 부모 코루틴으로부터 전달된 CoroutineContext를 재정의 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 Job 객체는 상속되지 않고 코루틴 빌더 함수가 호출되면 새롭게 생성되는 것을 주의해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상속되지 않는 Job&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 다뤘듯이 모든 코루틴 빌더 함수는 호출 때마다 Job 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 제어에 Job 객체가 필요한데 Job 객체를 부모 코루틴으로부터 상속받게 되면 코루틴의 제어가 어려워지기 때문에, 코루틴들은 서로 다른 Job을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732538858727&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val runBlockingJob = coroutineContext[Job]
    launch {
        val launchJob = coroutineContext[Job]
        if (runBlockingJob == launchJob) {
            println(&quot;job is same&quot;)
        } else {
            println(&quot;job is not same&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xvv6I/btsKVQE8KVW/QEKHQku6gtNRELkiudqliK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xvv6I/btsKVQE8KVW/QEKHQku6gtNRELkiudqliK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xvv6I/btsKVQE8KVW/QEKHQku6gtNRELkiudqliK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXvv6I%2FbtsKVQE8KVW%2FQEKHQku6gtNRELkiudqliK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;190&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실행 결과를 보면 runBlocking과 launch 블록의 Job 객체가 다른데, 이는 실행 환경을 상속받았음에도 서로 다른 Job 객체를 가진다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 부모 코루틴의 Job과 자식 코루틴의 Job이 아무 관계가 없는 것은 아니다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;이 두 관계는 코루틴을 구조화하는 데 사용된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조화에 사용되는 Job&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ben4Xb/btsKUi3zq59/0hdrCKX5QKLWp2ZKQ1ZZu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ben4Xb/btsKUi3zq59/0hdrCKX5QKLWp2ZKQ1ZZu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ben4Xb/btsKUi3zq59/0hdrCKX5QKLWp2ZKQ1ZZu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fben4Xb%2FbtsKUi3zq59%2F0hdrCKX5QKLWp2ZKQ1ZZu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;744&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 빌더가 호출되면 Job 객체는 새롭게 생성되지만 위 그림과 같이 내부 정의된 프로퍼티를 통해&lt;b&gt; 부모 &amp;lt;-&amp;gt;자식 양방향 참조를&lt;/b&gt; 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티는 &lt;b&gt;부모 프로퍼티와 자식 프로퍼티&lt;/b&gt;로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 하나의 부모 코루틴만을 가질 수 있기 때문에 &lt;b&gt;Job 객체를 가리키는 프로퍼티의 타입은 Job?&lt;/b&gt;이다.&lt;br /&gt;부모를 가지는데, 최상위 부모&lt;b&gt;(루트 코루틴)&lt;/b&gt;는 부모가 없을 수 있기 때문에 nullable 타입이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 코루틴은 하위에 &lt;b&gt;여러 자식&lt;/b&gt;을 가질 수 있기 때문에, child 프로퍼티의 타입은 &lt;b&gt;Sequence&amp;lt;Job&amp;gt;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용을 표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 55px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;Job 프로퍼티&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;Type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;parent&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;Job?&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;부모 코루틴은 최대 한개이며, 없을 수도 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;children&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Sequence&amp;lt;Job&amp;gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;코루틴은 복수의 자식 코루틴을 가질 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴의 구조화와 작업 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 구조화는 하나의 큰 비동기 작업을 작은 비동기 작업으로 나눌 때 일어난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHBkod/btsKV9qZvtP/vPKM1jajtBUpTda7c7w7UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHBkod/btsKV9qZvtP/vPKM1jajtBUpTda7c7w7UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHBkod/btsKV9qZvtP/vPKM1jajtBUpTda7c7w7UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHBkod%2FbtsKV9qZvtP%2FvPKM1jajtBUpTda7c7w7UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;561&quot; height=&quot;936&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시라기보단, 다음과 같이 비동기 작업도 작은 비동기 작업으로 분할할 수 있다는 느낌만 감을 잡으면 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 작업을 코루틴으로 바꾸면, 여러 서버로부터&amp;nbsp;데이터를 다운로드하는 코루틴과 데이터를 변환하는 코루틴을 합친 코루틴이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코루틴을 구조화하는 가장 중요한 이유는 코루틴을 안전하게 관리하고 제어하기 위함이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 코루틴은 다음과 같은 특성이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코루틴으로&lt;b&gt; 취소가 요청되면서 자식 코루틴으로 전파&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;부모 코루틴은 &lt;b&gt;모든 자식 코루틴이 완료돼야 완료&lt;/b&gt;될 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;취소의 전파&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 자식 코루틴으로 취소를 전파하는 특성을 갖기 때문에 &lt;b&gt;특정 코루틴이 취소되면 하위의 코루틴이 취소&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wtk6W/btsKW9KwhIA/fkkeRwGCkquueXc748z8SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wtk6W/btsKW9KwhIA/fkkeRwGCkquueXc748z8SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wtk6W/btsKW9KwhIA/fkkeRwGCkquueXc748z8SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWtk6W%2FbtsKW9KwhIA%2FfkkeRwGCkquueXc748z8SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;800&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 1에 취소가 요청되면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 코루틴은 자식 코루틴으로 취소를 전파하는 특성이 있기 때문에, 코루틴 2, 3, 4에 취소가 요청되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 코루틴은 자식에게 취소를 전파할 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmglcX/btsKWNAN4Kr/51jiPAxk5yDlb92yIvyOQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmglcX/btsKWNAN4Kr/51jiPAxk5yDlb92yIvyOQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmglcX/btsKWNAN4Kr/51jiPAxk5yDlb92yIvyOQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmglcX%2FbtsKWNAN4Kr%2F51jiPAxk5yDlb92yIvyOQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;322&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 2에 취소가 요청된다면 5와 6, 3일 경우 7, 4일 경우 8,9에 취소가 전파된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 특정 코루틴에 취소가 요청되면 취소는 자식 코루틴 방향으로만 전파되며, 부모 코루틴으로는 취소가 전파되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732541194246&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val startTime = System.currentTimeMillis()
    val parentJob = launch(Dispatchers.IO) {
        val dbResultsDeferred = listOf(&quot;DB1&quot;, &quot;DB2&quot;, &quot;DB3&quot;).map {
            async {
                delay(1000L)
                println(&quot;[${getElapsedTime(startTime)}] ${it}으로부터 데이터를 가져오는데 성공했습니다.&quot;)
                &quot;[$it data]&quot;
            }
        }
        val result = dbResultsDeferred.awaitAll()
        println(result)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 3개의 데이터베이스로부터 데이터를 가져와 병합하는 코루틴이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czkYor/btsKVQZs47R/VSUQnvbjVJAk9TORExaUOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czkYor/btsKVQZs47R/VSUQnvbjVJAk9TORExaUOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czkYor/btsKVQZs47R/VSUQnvbjVJAk9TORExaUOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczkYor%2FbtsKVQZs47R%2FVSUQnvbjVJAk9TORExaUOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;138&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 작업 중간에 부모 코루틴이 취소됐다고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 자식 코루틴의 작업은 더 이상 진행할 필요가 없고, &lt;b&gt;진행할 경우 자원을 낭비하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732541364265&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val startTime = System.currentTimeMillis()
    val parentJob = launch(Dispatchers.IO) {
        val dbResultsDeferred = listOf(&quot;DB1&quot;, &quot;DB2&quot;, &quot;DB3&quot;).map {
            async {
                delay(1000L)
                println(&quot;[${getElapsedTime(startTime)}] ${it}으로부터 데이터를 가져오는데 성공했습니다.&quot;)
                &quot;[$it data]&quot;
            }
        }
        val result = dbResultsDeferred.awaitAll()
        println(result)
    }
    println(&quot;[${getElapsedTime(startTime)}]&quot;)
    parentJob.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r6ss5/btsKWbPS24p/1QDOmpUKcEFCE3bqkRhkXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r6ss5/btsKWbPS24p/1QDOmpUKcEFCE3bqkRhkXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r6ss5/btsKWbPS24p/1QDOmpUKcEFCE3bqkRhkXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr6ss5%2FbtsKWbPS24p%2F1QDOmpUKcEFCE3bqkRhkXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;170&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1000 ms가 되기 전에, cancel 함수를 통해 부모 코루틴이 중단되고, 자식 코루틴도 모두 취소되기 때문에 작업이 진행되지 않은 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 코루틴은 자식 코루틴이 완료돼야 완료될 수 있지만, 자식 코루틴의 실행 완료를 기다리기 전까지 Job의 상태는 실행 완료 중이라는 상태를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 완료는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isActive : true&lt;/li&gt;
&lt;li&gt;isCancelled : false&lt;/li&gt;
&lt;li&gt;isCompleted : false&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 값을 가지는데, 이는 &lt;b&gt;실행 중 상태와 동일하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 실행 완료와 실행 중 상태는 구분하기 어려워서, 구분 없이 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoroutineScope 사용해 코루틴 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineSocpe 객체는 자신의 범위 내에서 생성된 코루틴들에게 실행 환경을 제공하고, 실행 범위를 관리하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope를 생성하는 방법은 2가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;인터페이스 구현을 통한 생성&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tBQdh/btsKXJLJr3r/BujMzn9Nvck4R44JzlIdP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tBQdh/btsKXJLJr3r/BujMzn9Nvck4R44JzlIdP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tBQdh/btsKXJLJr3r/BujMzn9Nvck4R44JzlIdP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtBQdh%2FbtsKXJLJr3r%2FBujMzn9Nvck4R44JzlIdP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;270&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 인터페이스는 다음과 같이 선언돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 실행 환경인 CoroutineContext를 가진 단순한 인터페이스로 이 인터페이스를 구현한 구체적인 클래스를 사용하면 CoroutineScope 객체를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수를 통한 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CoroutineScope&lt;span&gt; 객체를 생성하는 또 다른 방법은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CoroutineScope&lt;span&gt; 함수를 사용하는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CdEix/btsKXJ51siF/tUuvIzdoY8xEvLxr80HDZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CdEix/btsKXJ51siF/tUuvIzdoY8xEvLxr80HDZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CdEix/btsKXJ51siF/tUuvIzdoY8xEvLxr80HDZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCdEix%2FbtsKXJ51siF%2FtUuvIzdoY8xEvLxr80HDZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;730&quot; height=&quot;90&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 함수는 CoroutineScope를&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 인자로 입력받아 CoroutineScope&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 객체를 생성하며, Job 객체가 없을 경우 &lt;b&gt;새로운 Job 객체를 생성한다.&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;따라서 CoroutineScope(Dispatchers.IO)를 호출하면 Dispatchers.IO와 새로운 Job 객체로 구성된 CoroutineContext를 가진 CoroutineScope 객체를 생성할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736426791341&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val coroutineScope = CoroutineScope(Dispatchers.IO)
    coroutineScope.launch {
        delay(100L)
        println(&quot;${Thread.currentThread().name}] 코루틴 실행 완료&quot;)
    }
    Thread.sleep(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 coroutineScope 변수는 CoroutineScope(Dispatchers.IO)가 호출돼 만들어진 CoroutineScope 객체를 가리킨다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드를 실행해 보면 launch 코루틴이 Dispatchers.IO에 의해 백그라운드 스레드로 보내져 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 두 가지 방법으로 CoroutineScope 객체를 만들 수 있는데, 이 과정에서 CoroutineScope 내부에서 실행되는 코루틴이 &lt;b&gt;CoroutineScope로부터 CoroutineContext를 제공받는다는 사실이 중요하다&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;그러면 CoroutineScope는 어떻게 코루틴에게 CoroutineContext를 제공할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코루틴에게 실행 환경을 제공하는 CoroutineScope&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 살펴보기에 앞서, launch 코루틴 빌더 함수가 어떻게 선언돼 있는지 알아봐야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dre4E6/btsLJCLV7kK/mR1NXzFAAunbOS0k7CfBD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dre4E6/btsLJCLV7kK/mR1NXzFAAunbOS0k7CfBD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dre4E6/btsLJCLV7kK/mR1NXzFAAunbOS0k7CfBD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdre4E6%2FbtsLJCLV7kK%2FmR1NXzFAAunbOS0k7CfBD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;556&quot; height=&quot;526&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 launch 코루틴 빌더 함수는 CoroutineScope의 확장 함수로 선언돼 있으며, launch 함수가 호출되면 다음 과정을 통해 CoroutineScope 객체로부터 실행 환경을 제공받아 코루틴의 실행 환경을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;수신 객체인 CoroutineScope로부터 CoroutineContext 객체를 제공받는다.&lt;/li&gt;
&lt;li&gt;제공받은 CoroutineContext 객체에 launch 함수의 context 인자로 넘어온 CoroutineContext를 더한다.&lt;/li&gt;
&lt;li&gt;생성된 CoroutineContext에 코루틴 빌더 함수가 호출돼 새로 생성되는 Job을 더한다. 이때 CoroutineContext를 통해 전달되는 Job 객체는 새로 생성되는 Job 객체의 부모 Job 객체가 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1736428115413&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val newScope = CoroutineScope(CoroutineName(&quot;MyCoroutine&quot;)+Dispatchers.IO)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;143&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAwGM0/btsLIB1DmQl/kbmcLpkXXtRgyCKwqUlUB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAwGM0/btsLIB1DmQl/kbmcLpkXXtRgyCKwqUlUB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAwGM0/btsLIB1DmQl/kbmcLpkXXtRgyCKwqUlUB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAwGM0%2FbtsLIB1DmQl%2FkbmcLpkXXtRgyCKwqUlUB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;143&quot; height=&quot;265&quot; data-origin-width=&quot;143&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 newScope의 CoroutineContext 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 선언된 CoroutineName과 Dispatchers.IO와 생성된 newScope의 Job로 구성되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1736428150650&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val newScope = CoroutineScope(CoroutineName(&quot;MyCoroutine&quot;)+Dispatchers.IO)
newScope.launch(CoroutineName(&quot;LaunchCoroutine&quot;)) {
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;newScope의 launch 함수를 통해 생성되는 CoroutineContext 객체는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;165&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chsBxx/btsLJjsoTPZ/SWnhUalyak97dksRsfK8z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chsBxx/btsLJjsoTPZ/SWnhUalyak97dksRsfK8z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chsBxx/btsLJjsoTPZ/SWnhUalyak97dksRsfK8z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchsBxx%2FbtsLJjsoTPZ%2FSWnhUalyak97dksRsfK8z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;165&quot; height=&quot;267&quot; data-origin-width=&quot;165&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴은 부모 코루틴의 CoroutineContext 객체를 상속받는데, 그 과정에서 CoroutineName이 넘어왔으므로, 덮어씌워진다.&lt;/p&gt;
&lt;pre id=&quot;code_1736428205200&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val newScope = CoroutineScope(CoroutineName(&quot;MyCoroutine&quot;)+Dispatchers.IO)
newScope.launch(CoroutineName(&quot;LaunchCoroutine&quot;)) {
	val launchJob = this.coroutineContext[Job]
	val newScopeJob = newScope.coroutineContext[Job]
   ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch 코루틴 빌더 함수는 새로운 Job을 생성하고, 이 Job은 반환된 CoroutineContext의 Job을 부모로 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;171&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3TjbN/btsLJTfDtJh/mXKdX39cu6ROaWYZEsGbPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3TjbN/btsLJTfDtJh/mXKdX39cu6ROaWYZEsGbPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3TjbN/btsLJTfDtJh/mXKdX39cu6ROaWYZEsGbPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3TjbN%2FbtsLJTfDtJh%2FmXKdX39cu6ROaWYZEsGbPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;171&quot; height=&quot;281&quot; data-origin-width=&quot;171&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1736428260277&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val newScope = CoroutineScope(CoroutineName(&quot;MyCoroutine&quot;)+Dispatchers.IO)
    newScope.launch(CoroutineName(&quot;LaunchCoroutine&quot;)) {
        println(this.coroutineContext[CoroutineName])
        println(this.coroutineContext[CoroutineDispatcher])
        val launchJob = this.coroutineContext[Job]
        val newScopeJob = newScope.coroutineContext[Job]
        println(&quot;launchJob?.parent == newScopeJob &amp;gt;&amp;gt; ${launchJob?.parent == newScopeJob}&quot;)
    }
    Thread.sleep(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 확인하고 싶은 내용은 newScope의 coroutineContext 객체의 이름과 Dispatcher 그리고 Job들의 부모-자식 관계이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o0eYx/btsLIDdNk5P/94GQxVGsneaacwNkxQ1elK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o0eYx/btsLIDdNk5P/94GQxVGsneaacwNkxQ1elK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o0eYx/btsLIDdNk5P/94GQxVGsneaacwNkxQ1elK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0eYx%2FbtsLIDdNk5P%2F94GQxVGsneaacwNkxQ1elK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;142&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 부모 코루틴이 자식 코루틴으로 실행 환경을 상속하는 방식과 완전히 동일하다.&lt;br /&gt;동일한 이유는 실&lt;b&gt;제로 코루틴이 부모 코루틴의 CoroutineContext 객체를 가진 CoroutineScope 객체로부터 실행 환경을 상속받기 때문&lt;/b&gt;&lt;b&gt;이다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;launch, runBlocking, async 같은 코루틴 벌다 함수의 람다식에서 this.~ 를 통해 코루틴의 실행 환경에 접근할 수 있는 이유는 CoroutineScope가 수신 객체로 제공됐기 때문이다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgmyw2/btsLIC0hMBy/bTcjAiscOWLSG9KiXC06M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgmyw2/btsLIC0hMBy/bTcjAiscOWLSG9KiXC06M0/img.png&quot; data-alt=&quot;this는 생략 가능하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgmyw2/btsLIC0hMBy/bTcjAiscOWLSG9KiXC06M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdgmyw2%2FbtsLIC0hMBy%2FbTcjAiscOWLSG9KiXC06M0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;205&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;this는 생략 가능하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;CoroutineScope에 속한 코루틴의 범위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch, async와 같이 코루틴 빌더의 람다식은 &lt;b&gt;CoroutineScope 객체를 수신 객체&lt;/b&gt;로 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 객체는 기본적으로 특정 범위의 코루틴을 제어하는 역할을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736428759051&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 runBlocking 람다식 내에서, Coroutine1, Coroutine2라는 2개의 자식 코루틴을 실행하고 있으며,&lt;br /&gt;Coroutine1은 Coroutine3, Coroutine4를 자식 코루틴으로 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 각 CoroutineScope 객체의 범위를 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runBlocking의 수신 객체로 제공되는 CoroutineScope의 범위는 전체이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNgjT8/btsLIdUp8Ua/e5KYWHTiY7mh1TyoNnavJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNgjT8/btsLIdUp8Ua/e5KYWHTiY7mh1TyoNnavJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNgjT8/btsLIdUp8Ua/e5KYWHTiY7mh1TyoNnavJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNgjT8%2FbtsLIdUp8Ua%2Fe5KYWHTiY7mh1TyoNnavJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;542&quot; height=&quot;278&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CorotuineScope1의 범위는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3lkiN/btsLIfrdzub/jkTWEv3PzLm5KSUOsIk7hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3lkiN/btsLIfrdzub/jkTWEv3PzLm5KSUOsIk7hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3lkiN/btsLIfrdzub/jkTWEv3PzLm5KSUOsIk7hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3lkiN%2FbtsLIfrdzub%2FjkTWEv3PzLm5KSUOsIk7hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;173&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 코루틴 빌더 람다식에서 수신 객체로 제공되는 CoroutineScope 객체는 코루틴 빌더로 생성되는 코루틴과 람다식 내에서 CoroutineScope 객체를 사용해 실행되는 모든 코루틴을 포함한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;CoroutineScope를 새로 생성해 기존 CoroutineScope 범위 벗어나기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 다룬 코드들에서 모든 코루틴들은 runBlocking 람다식의 CoroutineScope 객체의 범위에 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 벗어나기 위해서는 어떻게 해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 새로운 CoroutineScope 객체를 생성하고, 이 객체를 사용해 코루틴을 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1736429203801&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        CoroutineScope(Dispatchers.IO).launch(CoroutineName(&quot;Corotuine4&quot;)) {
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zcK3W/btsLIOTWPOb/ZdQ4FAc6uF3jxn31sKSpr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zcK3W/btsLIOTWPOb/ZdQ4FAc6uF3jxn31sKSpr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zcK3W/btsLIOTWPOb/ZdQ4FAc6uF3jxn31sKSpr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzcK3W%2FbtsLIOTWPOb%2FZdQ4FAc6uF3jxn31sKSpr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;114&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 새로운 CoroutineScope 객체를 생성하면 기존 CoroutineScope 객체의 범위를 벗어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 가능한 이유는 CoroutineScope가 호출되면 생성되는 새로운 Job 객체에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 Job 객체를 사용해 구조화되는데, CoroutineScope 함수를 사용해 새로운 객체를 생성하면 기존 계층 구조를 따르지 않게 되는 새로운 Job 객체가 생성되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt9jae/btsLIM2UIS8/d8VOYxfiVe1tWzlUkAjHDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt9jae/btsLIM2UIS8/d8VOYxfiVe1tWzlUkAjHDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt9jae/btsLIM2UIS8/d8VOYxfiVe1tWzlUkAjHDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt9jae%2FbtsLIM2UIS8%2Fd8VOYxfiVe1tWzlUkAjHDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;603&quot; height=&quot;322&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 새로운 계층 구조가 만들어지면서&amp;nbsp;Coroutine4는 runBlocking 코루틴과 아무 관련이 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 이렇게 코루틴의 구조화를 깨는 것은 비동기 작업을 안전하지 않게 만들기 때문에 지양해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineScope 취소하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 인터페이스는 &lt;b&gt;확장 함수로 cancel 함수를 지원&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 객체의 범위에 속한 모든 코루틴을 취소하는 함수로 &lt;b&gt;범위 내에서 실행 중인 모든 코루틴에 취소가 요청&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1736429864769&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        this.cancel()
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 Coroutine1 코루틴의 CoroutineScope 객체에 취소 요청을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 범위에 속한 #1, #3, #4는 실행 도중 취소되며, #2 코루틴만 끝까지 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pFoyG/btsLHyEzwHE/kDIiKGL5BolKBtAKZnWVTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pFoyG/btsLHyEzwHE/kDIiKGL5BolKBtAKZnWVTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pFoyG/btsLHyEzwHE/kDIiKGL5BolKBtAKZnWVTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpFoyG%2FbtsLHyEzwHE%2FkDIiKGL5BolKBtAKZnWVTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;138&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJQsLQ/btsLJcUkGid/Kglzm0J8s8ExoQJcVIghaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJQsLQ/btsLJcUkGid/Kglzm0J8s8ExoQJcVIghaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJQsLQ/btsLJcUkGid/Kglzm0J8s8ExoQJcVIghaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJQsLQ%2FbtsLJcUkGid%2FKglzm0J8s8ExoQJcVIghaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;88&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancel 함수의 내부에는 Job 객체에 접근한 후 cancel 함수를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#1 코루틴의 Job이 취소되면 자식 코루틴인 #3, #4의 Job에게 취소가 전파돼 모든 자식 코루틴들이 취소된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ppdV9/btsLJmimwVr/ZYmb1JaP5vlo0oO2wZenOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ppdV9/btsLJmimwVr/ZYmb1JaP5vlo0oO2wZenOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ppdV9/btsLJmimwVr/ZYmb1JaP5vlo0oO2wZenOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FppdV9%2FbtsLJmimwVr%2FZYmb1JaP5vlo0oO2wZenOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;597&quot; height=&quot;284&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조화와 Job&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 구조화의 중심에는 &lt;b&gt;Job 객체&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 취소 요청을 하면 CoroutineScope 객체 내의 Job 객체에 취소 요청을 보내는 것처럼 실제 동작은 Job 객체를 조작하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;runBlocking과 루트 Job&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;runBlokcing 함수를 호출해 코루틴이 생성될 경우 부모 Job이 없는 Job 객체를 생성한다.&lt;/b&gt;&lt;br /&gt;이러한 Job(부모 Job이 없는 경우) &lt;b&gt;루트 Job&lt;/b&gt;이라고 하며, 구조화 시작점의 역할을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736668813271&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; { // 루트 Job 생성
	println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 구조는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b838er/btsLJW58g6U/zwMwPrxh3h8twNGjJtb1Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b838er/btsLJW58g6U/zwMwPrxh3h8twNGjJtb1Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b838er/btsLJW58g6U/zwMwPrxh3h8twNGjJtb1Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb838er%2FbtsLJW58g6U%2FzwMwPrxh3h8twNGjJtb1Fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;178&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 자식 코루틴이 있는 경우의 구조를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736668971874&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }

    }
    launch(CoroutineName(&quot;Coroutine2&quot;)) {
        launch(CoroutineName(&quot;Coroutine5&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runBlocking 루트 코루틴 아래에 , 내부 launch 함수를 통해 #1, #2가 실행되고, #1 내부에서 #3, #4 , #2 내부에서는 #5가 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lAWAx/btsLJVlMcgv/ISoyW9TpghjC1cYKes3ZAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lAWAx/btsLJVlMcgv/ISoyW9TpghjC1cYKes3ZAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lAWAx/btsLJVlMcgv/ISoyW9TpghjC1cYKes3ZAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlAWAx%2FbtsLJVlMcgv%2FISoyW9TpghjC1cYKes3ZAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;281&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Job 구조화 깨기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CoroutineScope 사용해 구조화 깨기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope 객체는 Job 객체를 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coroutine 객체가 생성되면 새로운 루트 Job이 생성되며, &lt;b&gt;이를 사용해 코루틴의 구조화를 깰 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736669349249&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val newScope = CoroutineScope(Dispatchers.IO)
    newScope.launch(CoroutineName(&quot;Coroutine1&quot;)) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    newScope.launch(CoroutineName(&quot;Coroutine2&quot;)) {
        launch(CoroutineName(&quot;Coroutine5&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 runBlocking 함수를 통해 루트 Job이 생성되지만, 새로운 루트 Job을 가진 newScope가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 newScope의 launch 함수를 통해 자식 코루틴을 실행하게 된다.&lt;br /&gt;해당 코드를 구조화하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEopp8/btsLK4B4LM3/qyYXIEQ73uB56p7xNijn70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEopp8/btsLK4B4LM3/qyYXIEQ73uB56p7xNijn70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEopp8/btsLK4B4LM3/qyYXIEQ73uB56p7xNijn70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEopp8%2FbtsLK4B4LM3%2FqyYXIEQ73uB56p7xNijn70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;277&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 자식 코루틴들이 newScope 하위에서 실행되기 때문에 runBlocking 코루틴은 자식 코루틴이 없고, 코드 실행 결과 아무런 결과 없이 프로세스가 종료되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNWysc/btsLKHAspMC/C6tXbbfWvp9eaa8RfOLki0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNWysc/btsLKHAspMC/C6tXbbfWvp9eaa8RfOLki0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNWysc/btsLKHAspMC/C6tXbbfWvp9eaa8RfOLki0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNWysc%2FbtsLKHAspMC%2FC6tXbbfWvp9eaa8RfOLki0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;71&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 newScope로 인해 구조화가 깨졌기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runBlocking 코루틴은 자식 코루틴들의 완료를 기다리는 상황이지만, newScope는 runBlocking 코루틴의 자식 코루틴이 아니기 때문에 다른 코루틴의 완료를 기다리지 않고 메인 스레드 사용을 종료해 프로세스가 종료되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 다른 코루틴이 실행 완료될 때까지 기다리는 임시 코드 delay를 추가하면 결과가 정상적으로 출력되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ze0RB/btsLJLp2wBL/VowDHdssbXkgKLybKS33M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ze0RB/btsLJLp2wBL/VowDHdssbXkgKLybKS33M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ze0RB/btsLJLp2wBL/VowDHdssbXkgKLybKS33M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fze0RB%2FbtsLJLp2wBL%2FVowDHdssbXkgKLybKS33M0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;138&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;하지만 구조화가 깨진 코루틴이 실행 완료되는 것은 프로그래밍적으로 코드가 불안정해지기 때문에 지양해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Job 사용해 구조화 깨기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 구조화를 깨기 위한 또 다른 방법은 &lt;b&gt;Job을 생산하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope를 생성하면 Job이 자동적으로 생성되고, 이를 이용해 코루틴의 구조화를 깨는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Job 생산만으로도 가능하다는 얘기이다.&lt;/p&gt;
&lt;pre id=&quot;code_1736669788497&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val newRootJob = Job()
    launch(CoroutineName(&quot;Coroutine1&quot;)+newRootJob) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)+newRootJob) {
        launch(CoroutineName(&quot;Coroutine5&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job()을 통해 새로운 루트 Job을 생성하고, 이를 CoroutineContext 인자로 넘기면 #1, #2의 공통 부모 Job이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqHEM0/btsLJUtGfZ8/K9c2GBDXCb4EDYckvatsJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqHEM0/btsLJUtGfZ8/K9c2GBDXCb4EDYckvatsJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqHEM0/btsLJUtGfZ8/K9c2GBDXCb4EDYckvatsJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqHEM0%2FbtsLJUtGfZ8%2FK9c2GBDXCb4EDYckvatsJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;325&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도, newRootJob을 취소 요청하면, 자식 Job 전체에게 취소가 전파된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 특정 Job이 취소되기를 원치 않는다면 newRootJob을 넘기는 것처럼&amp;nbsp; 새로운 Job 객체를 인자로 넘겨주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1736670030144&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val newRootJob = Job()
    launch(CoroutineName(&quot;Coroutine1&quot;)+newRootJob) {
        launch(CoroutineName(&quot;Coroutine3&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        launch(CoroutineName(&quot;Coroutine4&quot;)) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    launch(CoroutineName(&quot;Coroutine2&quot;)+newRootJob) {
        launch(CoroutineName(&quot;Coroutine5&quot;)+Job()) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
    delay(50L)
    newRootJob.cancel()
    delay(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Coroutine5는 newRootJob과 계층 구조가 끊어지기 때문에 정상적으로 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct1DZa/btsLMce4Ave/DqEvCBp4Ecq0OvB83TYKo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct1DZa/btsLMce4Ave/DqEvCBp4Ecq0OvB83TYKo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct1DZa/btsLMce4Ave/DqEvCBp4Ecq0OvB83TYKo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct1DZa%2FbtsLMce4Ave%2FDqEvCBp4Ecq0OvB83TYKo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;182&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;하지만 만약 Coroutine5가 생성되기 전, 부모 코루틴인 Coroutine2가 취소된다면 Coroutine5는 당연히 실행될 수 없다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccOBtI/btsLLCed4b7/yvXWhUH5hUqo3LgBxJm1GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccOBtI/btsLLCed4b7/yvXWhUH5hUqo3LgBxJm1GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccOBtI/btsLLCed4b7/yvXWhUH5hUqo3LgBxJm1GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccOBtI%2FbtsLLCed4b7%2FyvXWhUH5hUqo3LgBxJm1GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;269&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성된 Job은 자동으로 실행 완료되지 않는다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch 함수를 통해 생성된 Job 객체는 모든 자식 코루틴들이 실행 완료되면 자동으로 실행 완료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Job 생성 함수(&lt;b&gt;Job()&lt;/b&gt;)를 통해 생성된 Job 객체는 &lt;b&gt;자식 코루틴들이 모두 실행 완료되더라도 자동으로 실행 완료 되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 명시적으로 완료 함수인 &lt;b&gt;complete&lt;/b&gt; 호출이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1736670402198&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        val coroutine1Job = coroutineContext[Job]
        val newJob = Job(parent = coroutine1Job)
        launch(CoroutineName(&quot;Coroutine2&quot;)+newJob) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 구조화가 깨지지 않은 코루틴 구조를 형성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;하지만 해당 코드는 종료되지 않는 오류 코드이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 코루틴의 실행 완료가 처리되지 않고 있는데, 이는 Job을 통해 생성된 &lt;b&gt;newJob이 자동으로 실행 완료 처리되지 않기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 특징 상 자식 코루틴들이 모두 완료되지 않으면 부모 코루틴도 실행 완료될 수 없기 때문에 &lt;b&gt;무작정 대기하게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQPlf7/btsLL9o7Ov3/FiVyeV9mJmbC99E9hmPLx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQPlf7/btsLL9o7Ov3/FiVyeV9mJmbC99E9hmPLx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQPlf7/btsLL9o7Ov3/FiVyeV9mJmbC99E9hmPLx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQPlf7%2FbtsLL9o7Ov3%2FFiVyeV9mJmbC99E9hmPLx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;518&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해선 complete 호출을 통해 명시적으로 Job의 상태를 실행 완료로 만들어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736670625041&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    launch(CoroutineName(&quot;Coroutine1&quot;)) {
        val coroutine1Job = coroutineContext[Job]
        val newJob = Job(parent = coroutine1Job)
        launch(CoroutineName(&quot;Coroutine2&quot;)+newJob) {
            delay(100L)
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        newJob.complete()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;complete 함수를 통해 newJob은 실행 완료 중 상태로 바뀌며 자식 코루틴 실행이 완료되면 자동으로 실행 완료 중 상태로 바뀌어 부모 코루틴인 Coroutine1과 runBlocking 코루틴 모두 실행 완료 상태로 변하고 프로세스가 정상적으로 종료된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OSbda/btsLJExRC0E/Uvvjp8eegoc0epfIprnOxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OSbda/btsLJExRC0E/Uvvjp8eegoc0epfIprnOxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OSbda/btsLJExRC0E/Uvvjp8eegoc0epfIprnOxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOSbda%2FbtsLJExRC0E%2FUvvjp8eegoc0epfIprnOxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;265&quot; height=&quot;403&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구조화된 동시성의 원칙이란 &lt;b&gt;비동기 작업을 구조화&lt;/b&gt;함으로써 비동기 프로그래밍을 보다 &lt;b&gt;안정적이고 예측할&lt;/b&gt; 수 있게 만드는 원칙이다.&lt;/li&gt;
&lt;li&gt;코루틴은 구조화된 동시성의 원칙을 통해 코루틴을 &lt;b&gt;부모-자식&lt;/b&gt; 관계로 구조화해 비동기 프로그래밍의 안정성을 부여한다.&lt;/li&gt;
&lt;li&gt;부모 코루틴은 자식 코루틴에게 &lt;b&gt;실행 환경을 상속한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;코루틴 빌더 함수에 전달된 CoroutineContext 객체를 통해 실행 환경을 변경할 수 있다.&lt;/li&gt;
&lt;li&gt;코루틴 빌더가 호출될 때마다 새로운 Job 객체가 생성된다.&lt;/li&gt;
&lt;li&gt;Job 객체는 부모 코루틴의 Job 객체를 참조하며, 부모가 있을 수도, 없을 수도 있기 때문에 해당 타입은 nullable이다.&lt;/li&gt;
&lt;li&gt;Job 객체는 자식 Job 객체들을 참조할 수 있는 children 프로퍼티가 있다.&lt;/li&gt;
&lt;li&gt;부모 코루틴은 자식 코루틴이 완료될 때까지 완료되지 않는다. 자식 코루틴이 실행 중이라면 부모 코루틴은 실행 완료 중 상태를 가진다.&lt;/li&gt;
&lt;li&gt;부모 코루틴의 취소는 자식에게 전파되지만, 자식 코루틴의 취소는 부모 코루틴으로 전파되지 않는다.&lt;/li&gt;
&lt;li&gt;코루틴의 구조화를 깨기 위해선 CoroutineScope 객체를 생성하거나 Job 생성 함수를 호출하는 방법이 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736673453559&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bSBIkb/hyX0zJzS0V/9JzCNicpXQHtkyWznBLr61/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/sg4iY/hyX0t3CTX6/MH4hzZQnUO0vQPdxOjM9xk/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/Ow9Co/hyX0swTnXS/RjUKyCyj1GMhdYNhhQBew0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bSBIkb/hyX0zJzS0V/9JzCNicpXQHtkyWznBLr61/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/sg4iY/hyX0t3CTX6/MH4hzZQnUO0vQPdxOjM9xk/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/Ow9Co/hyX0swTnXS/RjUKyCyj1GMhdYNhhQBew0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>코루틴의 정석</category>
      <category>코틀린</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/509</guid>
      <comments>https://jja2han.tistory.com/509#entry509comment</comments>
      <pubDate>Sun, 12 Jan 2025 18:18:19 +0900</pubDate>
    </item>
    <item>
      <title>[Design Pattern] 팩토리 메서드 패턴</title>
      <link>https://jja2han.tistory.com/508</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;생성패턴&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드 패턴은 생성 패턴에 속해 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;기존 코드의 유연성과 재사용을 증가시키는 다양한 객체 생성 메커니즘들을 제공&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brKqn2/btsLDL40A8D/RU7DoWkd3B2bVorvgmv3RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brKqn2/btsLDL40A8D/RU7DoWkd3B2bVorvgmv3RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brKqn2/btsLDL40A8D/RU7DoWkd3B2bVorvgmv3RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrKqn2%2FbtsLDL40A8D%2FRU7DoWkd3B2bVorvgmv3RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;198&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;물류 관리 앱을 개발하는 상황을 예로 들어보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 버전에서는 &lt;b&gt;&lt;code&gt;트럭(Truck)&lt;/code&gt;&lt;/b&gt; 운송만 지원하며, 대부분의 코드는 &lt;b&gt;&lt;code&gt;Truck&lt;/code&gt;&lt;/b&gt; 클래스에 의존합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이후 해상 물류 기능이 요구되면서 &lt;b&gt;&lt;code&gt;Ship&lt;/code&gt;&lt;/b&gt; 클래스를 추가해야 하지만, 기존 코드가 &lt;b&gt;&lt;code&gt;Truck&lt;/code&gt;&lt;/b&gt; 클래스와 강하게 결합되어 있어 수정이 어렵습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;새로운 운송이 추가될 때마다 조건문과 코드 수정이 반복되어 코드가 복잡해지고 유지보수가 어려워집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 문제점을 해결하기 위해 &lt;b&gt;팩토리 메서드를&lt;/b&gt; 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;의도&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;팩토리 메서드 패턴은 부모 클래스에서 객체를 생성할 수 있는 인터페이스를 제공하면서, 자식 클래스가 생성되는 객체의 구체적인 유형을 결정할 수 있도록 하는 생성 패턴입니다. 이 패턴은 객체 생성 로직을 캡슐화하여 코드의 유연성과 확장성을 높이는 데 사용됩니다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;동작 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드 패턴은 &lt;b&gt;new 연산자를 사용한 직접적인 객체 생성&lt;/b&gt; 대신, 팩토리 메서드를 호출하여 객체를 생성합니다. &lt;br /&gt;객체는 여전히 new 연산자로 생성되지만, 이&lt;b&gt; 호출은 팩토리 메서드 내부&lt;/b&gt;에서만 이루어집니다. 이를 통해 &lt;b&gt;클라이언트 코드&lt;/b&gt;와 &lt;b&gt;객체 생성 로직 간&lt;/b&gt;의 &lt;b&gt;결합도&lt;/b&gt;를 낮출 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhXrCm/btsLEJrRTG8/YncqBDxKsJ8J4zkbfu0zE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhXrCm/btsLEJrRTG8/YncqBDxKsJ8J4zkbfu0zE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhXrCm/btsLEJrRTG8/YncqBDxKsJ8J4zkbfu0zE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhXrCm%2FbtsLEJrRTG8%2FYncqBDxKsJ8J4zkbfu0zE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;890&quot; height=&quot;324&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;(상속 관계에서는 자식 클래스는 오버라이딩을 통해 메서드를 변경할 수 있고, createTransport 메서드가 반환하는 객체의 클래스를 변경했다)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Factory 추상 클래스는 객체 생성을 위한 팩토리 메서드(createTransport)를 선언합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;LoadFactory는 Truck 객체를 반환하고, SeaFactory는 Ship 객체를 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트는 팩토리를 통해 운송 수단을 생성하며, 객체의 구체적인 클래스에 대해 알 필요가 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 변경은 처음에는 무의미해 보일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;생성자 호출을 단순히 프로그램의 한 부분에서 다른 부분으로 옮긴 것 처럼 보일 수 있기 때문입니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 이 변경 덕분에 자식 클래스에서 팩토리 메서드를 오버라이딩 하여 생성되는 &lt;b&gt;객체의 유형을 유연하게 변경&lt;/b&gt;할 수 있습니다. 이는 코드의 수정 없이 새로운 제품 유형을 추가할 수 있는 확장성을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다만 몇 가지 제한 사항이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드가 반환할 수 있는 객체는 &lt;b&gt;반드시 공통 기초 클래스나 공통 인터페이스를 구현&lt;/b&gt;해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어, &lt;b&gt;모든 제품(Truck, Ship)&lt;/b&gt;은 &lt;code&gt;Transport&lt;/code&gt; 라는 공통 인터페이스를 구현해야 하며, 팩토리 기초 클래스의 &lt;code&gt;createTransport&lt;/code&gt; 메서드도 이 인터페이스를 반환 타입으로 선언해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vynt9/btsLEGu7JiZ/m5dhfDVnfvdBrPQIvyfS1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vynt9/btsLEGu7JiZ/m5dhfDVnfvdBrPQIvyfS1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vynt9/btsLEGu7JiZ/m5dhfDVnfvdBrPQIvyfS1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvynt9%2FbtsLEGu7JiZ%2Fm5dhfDVnfvdBrPQIvyfS1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;356&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어 Truck과 Ship 클래스 모두 &lt;b&gt;Transport 인터페이스를 구현&lt;/b&gt;해야 하며, 이 인터페이스는 &lt;code&gt;deliver&lt;/code&gt; 메서드를 선언합니다.&lt;br /&gt;하지만 각 클래스는 이 메서드의&lt;b&gt; 구체적인 동작 방식을 다르게&lt;/b&gt; 구현합니다. &lt;b&gt;트럭은 육로로, 선박은 해상으로 물건을 배달&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;LoadFactory&lt;/b&gt; 클래스에 포함된 팩토리 메서드(createTransport)는 &lt;b&gt;Truck을 반환&lt;/b&gt;하는 반면, &lt;br /&gt;&lt;b&gt;SeaFactory&lt;/b&gt; 클래스에 포함된 팩토리 메서드는 &lt;b&gt;Ship을 반환&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트 코드의 역할&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트 코드는 팩토리 메서드를 호출하여 객체를 생성하지만, 구체적으로는 어떤 객체가 반환되는지는 알 필요가 없습니다. 모든 객체는 공통 인터페이스(Transport)에 정의된 메서드만 사용할 수 있습니다. 예를 들어 클라이언트가 deliver 메서드를 호출하면 트럭은 육로 배달을, 선박은 해상 배달을 수행하지만, 클라이언트는 이 동작의 세부 구현에 대해 몰라도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;구조&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI27ub/btsLFP5JKuI/wVitrbY5WrcSKmmzsP6OXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI27ub/btsLFP5JKuI/wVitrbY5WrcSKmmzsP6OXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI27ub/btsLFP5JKuI/wVitrbY5WrcSKmmzsP6OXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI27ub%2FbtsLFP5JKuI%2FwVitrbY5WrcSKmmzsP6OXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;412&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Product(공통 인터페이스)&lt;/span&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;모든 제품이 구현해야 할 공통 메서드를 정의합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Ex) Transport 인터페이스는 deliver 메서드를 선언합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ConcreteProduct(구체적인 제품)&lt;/span&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;공통 인터페이스를 구현하는 구체적인 클래스들입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Ex) Truck 클래스는 육로 배달을, Ship 클래스는 해상 배달을 구현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Creator(추상 팩토리 클래스)&lt;/span&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체를 반환하는 팩토리 메서드를 선언합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드의 반환 타입은 공통 인터페이스와 일치해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ConcreteCreator(구체적인 팩토리 클래스)&lt;/span&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;추상 팩토리 메서드를 오버라이딩 하여 특정 제품(Truck, Ship)을 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;LoadFactory는 트럭 객체를, SeaFactory는 선박 객체를 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;적용&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드 패턴은 다음과 같은 경우에 적합합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드가 함께 작동해야 하는 객체의 정확한 유형이나 의존성을 미리 알 수 없을 때.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체 생성 로직을 사용하는 코드와 분리하고 싶을 때&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;라이브러리나 프레임워크 사용자가 내부 컴포넌트를 확장할 수 있도록 설계할 때.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 객체를 재사용하거나 캐싱하여 리소스를 절약하고 싶을 때.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;장점&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;유연한 확장성&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 코드를 수정하지 않고 새로운 객체 생성 방식을 준수할 수 있다.(OCP)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체 생성 책임을 하위 클래스에 위임하여 코드의 재사용성과 확장성을 높일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결합도 감소&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체 생성 로직과 사용 로직을 분리하여 Creator와 Product 간의 강한 결합을 막는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인터페이스를 통해 객체를 생성하므로 구현체에 대한 의존도를 줄이고 유지보수가 용이하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;단일 책임 원칙 준수&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체 생성 로직이 별도의 팩토리 클래스에 집중되므로 각 클래스가 하나의 책임만 가지게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드 일관성 및 관리 용이성&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;객체 생성을 한 곳에서 관리할 수 있어 코드의 가독성과 일관성이 향상된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;단점&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드 복잡성 증가&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 설계와 구현이 복잡해질 수 있으며, 단순한 객체 생성에는 오히려 과한 설계가 될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클래스 수 증가&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;새로운 유형이 추가될 때마다 해당 제품을 생성하는 팩토리 클래스를 추가해야 하므로 클래스 수가 급격히 증가할 수 있으며, 이는 클래스 구조의 복잡을 야기한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 설계 비용&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;패턴을 적용하기 위해 인터페이스와 추상 클래스를 설계해야 하므로 초기 개발 단계에서 시간이 더 소요될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;정리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;팩토리 메서드 패턴은 &lt;b&gt;객체 생성 로직&lt;/b&gt;을 &lt;b&gt;캡슐화&lt;/b&gt;하여 &lt;b&gt;유지 보수성&lt;/b&gt;과 &lt;b&gt;확정성&lt;/b&gt;을 높이는 데 유용합니다. 특히, 다양한 유형의 객체를 유연하게 추가할 수 있어 변화하는 &lt;b&gt;요구사항에 대응하기 적합&lt;/b&gt;합니다. 하지만 필요 이상으로 &lt;b&gt;복잡한 설계&lt;/b&gt;를 초래할 가능성이 있으므로 적절한 상황에서 사용하는것이 중요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;참고&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1736151419170&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;팩토리 메서드 패턴&quot; data-og-description=&quot;/ 디자인 패턴들 / 생성 패턴 팩토리 메서드 패턴 다음 이름으로도 불립니다: 가상 생성자,&amp;nbsp;Factory Method 의도 팩토리 메서드는 부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지&quot; data-og-host=&quot;refactoring.guru&quot; data-og-source-url=&quot;https://refactoring.guru/ko/design-patterns/factory-method&quot; data-og-url=&quot;https://refactoring.guru/ko/design-patterns/factory-method&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/r3vth/hyXWxyU5kt/3sbSgKntvAsXeHV2xkE5i1/img.png?width=640&amp;amp;height=400&amp;amp;face=0_0_640_400,https://scrap.kakaocdn.net/dn/UNUHh/hyXWxezSJn/qRNDewfROv4szZVg8PWwuk/img.png?width=660&amp;amp;height=380&amp;amp;face=0_0_660_380&quot;&gt;&lt;a href=&quot;https://refactoring.guru/ko/design-patterns/factory-method&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://refactoring.guru/ko/design-patterns/factory-method&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/r3vth/hyXWxyU5kt/3sbSgKntvAsXeHV2xkE5i1/img.png?width=640&amp;amp;height=400&amp;amp;face=0_0_640_400,https://scrap.kakaocdn.net/dn/UNUHh/hyXWxezSJn/qRNDewfROv4szZVg8PWwuk/img.png?width=660&amp;amp;height=380&amp;amp;face=0_0_660_380');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;팩토리 메서드 패턴&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;/ 디자인 패턴들 / 생성 패턴 팩토리 메서드 패턴 다음 이름으로도 불립니다: 가상 생성자,&amp;nbsp;Factory Method 의도 팩토리 메서드는 부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;refactoring.guru&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Science/Design Pattern</category>
      <category>디자인 패턴</category>
      <category>생성 패턴</category>
      <category>팩토리 메서드 패턴</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/508</guid>
      <comments>https://jja2han.tistory.com/508#entry508comment</comments>
      <pubDate>Mon, 6 Jan 2025 17:21:40 +0900</pubDate>
    </item>
    <item>
      <title>SSAFY 11기 수료 후기</title>
      <link>https://jja2han.tistory.com/507</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;23년도에 네이버 부스트캠프에서는 좋은 동료들과 저 자신의 &quot;성장&quot;을 위해 달렸다면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;24년도에 싸피에서 저 자신에 대한 &quot;증명&quot;을 위해 달렸던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;싸피 지원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싸피 지원 단계에서는 크게 준비를 하지 않았던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 부스트캠프,&amp;nbsp; 우테코 프리코스, 학업을 모두 병행하다 보니 싸피의 우선순위는 후순위로 생각했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 와서 생각해 보면 동시에 여러 개를 잡으려고 하다 보니, 온전하게 하나에 집중하지 못한 것이 마음에 걸리네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트는 프로젝트 준비 때문에 준비를 하지 못했고, 언어 이슈로 한문제를 풀지 못했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 난이도가 쉬웠던 탓에 탈락을 예상했지만, 합격을 해서 조금 놀랐었습니다..ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(입과 후 동기들에게 물어보니 대부분 2문제를 풀었더라고요)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 면접 준비보다는 다른 활동에 집중하면서 시간을 보냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 면접을 보러 가는 당일 기차에서 PT 면접을 준비하는 조금 안일한 자세로 면접에 임하게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인생에서 2번째 면접 경험이었는데 깔끔하게 답을 했다고 생각했지만 코딩테스트와 특정 질문에서 제가 너무 완강한 태도를 보여서 떨어지게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 운이 좋게 추가합격의 연락이 와서 싸피에 합격할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MvJIK/btsLDu9twSO/8p53rVukSwOw4VkiDOL7rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MvJIK/btsLDu9twSO/8p53rVukSwOw4VkiDOL7rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MvJIK/btsLDu9twSO/8p53rVukSwOw4VkiDOL7rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMvJIK%2FbtsLDu9twSO%2F8p53rVukSwOw4VkiDOL7rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;506&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부족한 부분을 채웠던 1학기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1학기의 주된 활동은 스타트캠프와 관통 프로젝트이며, 그 외에는 커리큘럼에 맞는 수업과 과제가 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 1학기를 제가 부족하다고 생각했던 부분을 채우면서 보냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;알고리즘&lt;/li&gt;
&lt;li&gt;면접 경험&lt;/li&gt;
&lt;li&gt;CS&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분을 채우기 위해서 반에서 스터디에 참여하고, 매일 공부한 것을 서로 인증하는 반 내의 이벤트(?)에 참여해서 열심히 보냈던 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmJkr1/btsLEcNTMEz/Rt6lYkEkM3aJKLqHtmGTnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmJkr1/btsLEcNTMEz/Rt6lYkEkM3aJKLqHtmGTnK/img.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;986&quot; data-is-animation=&quot;false&quot; style=&quot;width: 40.5367%; margin-right: 10px;&quot; data-widthpercent=&quot;41.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmJkr1/btsLEcNTMEz/Rt6lYkEkM3aJKLqHtmGTnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmJkr1%2FbtsLEcNTMEz%2FRt6lYkEkM3aJKLqHtmGTnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1546&quot; height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca7k8I/btsLEpfjpTi/1dHUaSdO1lHfWCHKUbaA2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca7k8I/btsLEpfjpTi/1dHUaSdO1lHfWCHKUbaA2K/img.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;792&quot; data-is-animation=&quot;false&quot; style=&quot;width: 58.3005%;&quot; data-widthpercent=&quot;58.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca7k8I/btsLEpfjpTi/1dHUaSdO1lHfWCHKUbaA2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca7k8I%2FbtsLEpfjpTi%2F1dHUaSdO1lHfWCHKUbaA2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1786&quot; height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 공부를 하면서, 같은 목표에 도전하는 사람들이 있다면 적극적으로 서로 이용하는 것이 좋다고 생각하는 주의라 열심히 다른 사람을 이용했던 것 같습니다. (물론 저도 다른 사람에게 이용당했습니다 ㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS54fp/btsLDWxJtzD/IiA7jSgjNjmDm2Tiegxbik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS54fp/btsLDWxJtzD/IiA7jSgjNjmDm2Tiegxbik/img.png&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;1012&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.7453%; margin-right: 10px;&quot; data-widthpercent=&quot;50.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS54fp/btsLDWxJtzD/IiA7jSgjNjmDm2Tiegxbik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS54fp%2FbtsLDWxJtzD%2FIiA7jSgjNjmDm2Tiegxbik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2348&quot; height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQjtH1/btsLDMPE9wr/tDF7eVMKB1Xr2KOVkwyLB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQjtH1/btsLDMPE9wr/tDF7eVMKB1Xr2KOVkwyLB1/img.png&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;1008&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.0919%;&quot; data-widthpercent=&quot;49.67&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQjtH1/btsLDMPE9wr/tDF7eVMKB1Xr2KOVkwyLB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQjtH1%2FbtsLDMPE9wr%2FtDF7eVMKB1Xr2KOVkwyLB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2308&quot; height=&quot;1008&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 풀고 싶었던 문제만 풀어서 올렸던 알고리즘의 정상화도 시도했습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 유형의 쉬운 문제를 풀어보면서 유형을 익히고, 같은 유형의 어려운 문제를 풀면서 유형을 익혔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일 년 동안 약 150문제를 풀었고 티어는 골드 2에서 플레 4까지 올라갔네요&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QraUq/btsLE2qoRgG/KDbR2Xk2Tg7Tal3wHoqSF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QraUq/btsLE2qoRgG/KDbR2Xk2Tg7Tal3wHoqSF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QraUq/btsLE2qoRgG/KDbR2Xk2Tg7Tal3wHoqSF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQraUq%2FbtsLE2qoRgG%2FKDbR2Xk2Tg7Tal3wHoqSF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;828&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 그래프와 구현에만 치중되었던 도형이 디피, 그리디, 자료구조 등 제가 약하다고 생각했던 유형까지 뻗어 나간 것을 보면서 정상화가 어느 정도 성공했다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관통 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1학기의 마지막은 관통 프로젝트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 모바일 반이라, 자유 주제로 프로제트를 진행하면 되는데, 다른 전공반은 아마 주제가 고정이었던 걸로 들었습니다.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 기간은 명시적으로 1주일이고, 그 전부터 시작해도 상관은 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아마 2명이서 진행하는거라 마음 맞는 사람과 하면 됩니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 1학기 커리큘럼에서 백엔드를 경험하고 백엔드 쪽도 공부해보고 싶어서 안드로이드와 백엔드 모두 참여했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1736070042599&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jaehan4707/ninemova: TMDB를 이용한 영화 정보 제공 및 AI 기반 영화 추천 어플리케이션&quot; data-og-description=&quot;TMDB를 이용한 영화 정보 제공 및 AI 기반 영화 추천 어플리케이션. Contribute to jaehan4707/ninemova development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jaehan4707/ninemova&quot; data-og-url=&quot;https://github.com/jaehan4707/ninemova&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btoBdF/hyXWC7JVYg/CdLy07hVnba93DTl8TF5LK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bOnSrE/hyXWq7mg4v/keCatTLTgFmYyfflOsyEwk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jaehan4707/ninemova&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jaehan4707/ninemova&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btoBdF/hyXWC7JVYg/CdLy07hVnba93DTl8TF5LK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bOnSrE/hyXWq7mg4v/keCatTLTgFmYyfflOsyEwk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jaehan4707/ninemova: TMDB를 이용한 영화 정보 제공 및 AI 기반 영화 추천 어플리케이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TMDB를 이용한 영화 정보 제공 및 AI 기반 영화 추천 어플리케이션. Contribute to jaehan4707/ninemova development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1주일이라는 짧은 기간 동안 계획했던 기능을 구현하느라 힘든 시간을 보냈던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 결과는 1등 수상!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고생을 보답받은것 같아서 기분이 좋았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkrtCz/btsLDb3ozhf/5yQvtkGiy2AqXq456atMY1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkrtCz/btsLDb3ozhf/5yQvtkGiy2AqXq456atMY1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkrtCz/btsLDb3ozhf/5yQvtkGiy2AqXq456atMY1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkrtCz%2FbtsLDb3ozhf%2F5yQvtkGiy2AqXq456atMY1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;580&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 1학기를 마치면, 잡페어가 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시기부터는 슬슬 원서를 써봐도 되지 않을까라는 생각이 들었던 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 생각해보면 1학기부터 원서를 조금 더 적극적으로 써도 되지 않았을까입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12기나 그다음 기수를 하시는 분들은 싸이 활동도 좋지만, 조금 더 적극적으로 취업활동을 병행해도 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2학기보다 1학기가 훨씬 여유로워서 2학기는 정말 바쁘거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트의 연속이던 2학기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q7XaW/btsLEe5VNU5/z3fFz7kCy8SGfd8etbU7SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q7XaW/btsLEe5VNU5/z3fFz7kCy8SGfd8etbU7SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q7XaW/btsLEe5VNU5/z3fFz7kCy8SGfd8etbU7SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq7XaW%2FbtsLEe5VNU5%2Fz3fFz7kCy8SGfd8etbU7SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;743&quot; height=&quot;442&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2학기부터는 정말~ 바쁩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 프로젝트를 3개 하는 것은 알고 있었지만, 이렇게 바쁠 줄은 몰랐었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속해서 올라오는 기업 공고와 자소서 지옥, 프로젝트 지옥 등 2학기는 정말 여유 없이 할 일들을 쳐냈던 기간이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0kqEG/btsLCKZb1hk/kvRLh19A6u7MkCJEgvd9yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0kqEG/btsLCKZb1hk/kvRLh19A6u7MkCJEgvd9yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0kqEG/btsLCKZb1hk/kvRLh19A6u7MkCJEgvd9yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0kqEG%2FbtsLCKZb1hk%2FkvRLh19A6u7MkCJEgvd9yK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;405&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2학기는 기본적으로 공통 -&amp;gt; 특화 -&amp;gt; 자율 3개의 프로젝트로 이어지면서 세 프로젝트 모두 각각의 특징이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공통 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저와 잘 맞는 팀원을 찾는 것에서부터 하고 싶은 도메인과 여러 제약사항이 있기에 프로젝트의 시작인 팀 빌딩에서 정말 힘들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 팀원들과 얘기를 하면서 주제를 선정하는 개발적인 부분과 지라와 노션 등 프로젝트 일정을 관리하는 비개발적인 부분 모두를 팀 내에서 처리해야 했기에 신경 써야 할 것이 정말 많았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싸피 내에서도 명확한 가이드라인을 주기보다는 간접적인 도움을 주는 정도라 적응하기 쉽지 않았던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 가장 힘들었던 것은 아이디어 회의였습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcmQPF/btsLDVS7Y4h/LtoiRwI4bNtlGHaY6hD73k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcmQPF/btsLDVS7Y4h/LtoiRwI4bNtlGHaY6hD73k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcmQPF/btsLDVS7Y4h/LtoiRwI4bNtlGHaY6hD73k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcmQPF%2FbtsLDVS7Y4h%2FLtoiRwI4bNtlGHaY6hD73k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1988&quot; height=&quot;852&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두가 만족할 만한 아이디어를 찾는 과정은 정말 험난했던 것 같습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 부분이 마음에 들면, 하나에서 꺼려지게 되는 무한 굴레였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개발보다 아이디어 선정에서 많은 팀들이 시간과 노력을 할애했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 하면 해당 프로젝트의 당위성을 쉽게 다른 사람이 납득할 수 있을까에 대해서 많은 고민을 했고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 문해력을 타깃으로 한 서비스를 기획하게 되었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미 기존에 존재하는 서비스를 의식하지 말자&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 프로젝트의 아이디어 회의마다 가장 중요하게 생각했던 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 서비스라면, 우리만의 차이점을 부각해서 기존 서비스와 차별성을 두는 것이 싸피 프로젝트 성공의 핵심이었던 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnm2rb/btsLDVlh3Ud/R3wYbxOLXVHUxKsKxSD4fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnm2rb/btsLDVlh3Ud/R3wYbxOLXVHUxKsKxSD4fK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnm2rb/btsLDVlh3Ud/R3wYbxOLXVHUxKsKxSD4fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnm2rb%2FbtsLDVlh3Ud%2FR3wYbxOLXVHUxKsKxSD4fK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1580&quot; height=&quot;1162&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 문해력을 이용한 서비스로 방향을 정했고, 시장 조사를 통해 기존 어플과 어떤 차이점을 둘 수 있을까에 대해 많은 고민을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저희는 10대를 타깃으로 하는 만큼 디자인과 게임형 어플리케이션이라는 차이점을 뒀고, 공통 프로젝트 최우수상을 수상할 수 있었습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1736071223971&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jaehan4707/Mallang: 문해력 증진 어플리케이션&quot; data-og-description=&quot;문해력 증진 어플리케이션. Contribute to jaehan4707/Mallang development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jaehan4707/Mallang&quot; data-og-url=&quot;https://github.com/jaehan4707/Mallang&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXSPKb/hyXWnbKyhC/kP6Umtn7WinSVWCPmJTEek/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c4Idlp/hyXWvU9sDl/x9HN97cIHs1XsvWe7y7kT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jaehan4707/Mallang&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jaehan4707/Mallang&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXSPKb/hyXWnbKyhC/kP6Umtn7WinSVWCPmJTEek/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/c4Idlp/hyXWvU9sDl/x9HN97cIHs1XsvWe7y7kT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jaehan4707/Mallang: 문해력 증진 어플리케이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;문해력 증진 어플리케이션. Contribute to jaehan4707/Mallang development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 다 잘 만든 프로젝트였지만 수상에 제가 작성한 테스트 코드와 발표가 큰 영향을 끼쳤던 것 같습니다ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특화 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특화프로젝트는 여러 가지 도메인 중 하나를 선택해서 프로젝트를 진행하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인공지능(영상/음성)&lt;/li&gt;
&lt;li&gt;빅데이터(분산/추천)&lt;/li&gt;
&lt;li&gt;블록체인(P2P거래/NFT)&lt;/li&gt;
&lt;li&gt;모빌리티(스마트홈/자율주행)&lt;/li&gt;
&lt;li&gt;메타버스 게임&lt;/li&gt;
&lt;li&gt;핀테크&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 여기서 블록체인 NFT를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 전국에서 블록체인을 선택한 팀은 6팀이었고, NFT를 선택한 팀은 저희가 유일했습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 선택의 무게를 온전히 저희 팀이 감당했었습니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 팀들에게 NFT를 설명하기 위해 저희는 프로젝트 단계에서부터 블록체인을 학습했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 NFT를 넣기 위해 많은 고민을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 프로젝트 + 블록체인이 아닌 반드시 블록체인이 필요한 프로젝트를 선정하기 위해 많은 아이디어 회의를 거쳤고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 인생네컷과 NFT를 결합한 서비스를 기획하게 되었습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1736072066909&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jaehan4707/SemoNemo: 나만의 프레임/촬영 제작 어플리케이션&quot; data-og-description=&quot;나만의 프레임/촬영 제작 어플리케이션. Contribute to jaehan4707/SemoNemo development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jaehan4707/SemoNemo&quot; data-og-url=&quot;https://github.com/jaehan4707/SemoNemo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKcikQ/hyXWy5oEy1/b9Tl7K1BI2ItpGY3v0W6t1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m217E/hyXWuaQWO7/CSrYZET0y1r5vMtwoMf5Wk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jaehan4707/SemoNemo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jaehan4707/SemoNemo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKcikQ/hyXWy5oEy1/b9Tl7K1BI2ItpGY3v0W6t1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m217E/hyXWuaQWO7/CSrYZET0y1r5vMtwoMf5Wk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jaehan4707/SemoNemo: 나만의 프레임/촬영 제작 어플리케이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;나만의 프레임/촬영 제작 어플리케이션. Contribute to jaehan4707/SemoNemo development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 안드로이드 파트 중에서 블록체인을 다루기 위한 메타마스크 공식 문서를 읽고 프로젝트에 적용하기 위해 많은 노력을 했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대다수의 코드가 자바 스크립트로 작성되어서 이를 안드로이드 환경에서 돌아가기 위한 코틀린 코드로 변경하고 앱 간의 전환을 매끄럽게 하는 것이 저의 역할이었습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 속도 개선을 위해 싸피에서 제공하는 블록체인 네트워크가 아닌 독자적인 블록체인 네트워크를 개발했고, 저희만의 코인을 개발해 최대한 블록체인을 깊이 있게 학습하고 고민했다는 내용을 발표와 프로젝트에 녹여냈고 그 결과 프로젝트 2등을 수상할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자율 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자율 프로젝트는 말 그대로 프로젝트의 주제가 자율입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제 선택부터 모든 것이 팀의 선택으로 이어졌고, 앞선 두 프로젝트에서 수상을 위해 열심히 노력했다면 이번 프로젝트에서는 &quot;해보고 싶었던 것을 하자&quot;라는 마인드로 프로젝트를 임했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기간에서는 학습을 위한 인프런 강의와 책을 지원받아서 저는 안드로이드 파트가 아닌 백엔드 파트로 프로젝트에 참여하게 되었고, 어떻게 하면 성능을 개선할 수 있을까 와 효율적인 코드 설계를 위한 많은 고민을 하는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 코드를 작성하면서 성능 테스트를 해봤고, 테스트 코드를 작성하면서 코드가 의도대로 동작하는지와 같은 부분에 많은 신경을 썼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 저는 유저들의 실시간 랭킹을 계산하는 기능을 담당했고, 일반적인 데이터베이스 테이블과 쿼리를 사용한 랭킹 계산에서 Redis의 Sorted set을 사용한 랭킹 계산으로 성능을 개선하는 과정을 거쳤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpGrH0/btsLEbBqx2K/tnkdALGugQ9IbGydTzIPwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpGrH0/btsLEbBqx2K/tnkdALGugQ9IbGydTzIPwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpGrH0/btsLEbBqx2K/tnkdALGugQ9IbGydTzIPwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpGrH0%2FbtsLEbBqx2K%2FtnkdALGugQ9IbGydTzIPwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;710&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 확장 가능성을 고려해, 랭킹의 추상화를 적용해 효율적인 설계를 위해 노력했던 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxiwnB/btsLDHU1WKQ/KkUWD2LXDHrlmI43fnudxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxiwnB/btsLDHU1WKQ/KkUWD2LXDHrlmI43fnudxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxiwnB/btsLDHU1WKQ/KkUWD2LXDHrlmI43fnudxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxiwnB%2FbtsLDHU1WKQ%2FKkUWD2LXDHrlmI43fnudxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1136&quot; height=&quot;1164&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자가 모두 해보고 싶었던 영역을 담당해 프로젝트를 진행했고, 그 결과 규모가 작은 프로젝트였지만 모두 만족할 수 있었던 프로젝트였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1736072455263&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jaehan4707/RunIt: 랭킹 기반 런닝 동기부여 어플리케이션&quot; data-og-description=&quot;랭킹 기반 런닝 동기부여 어플리케이션. Contribute to jaehan4707/RunIt development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jaehan4707/RunIt&quot; data-og-url=&quot;https://github.com/jaehan4707/RunIt&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gK5A9/hyXWtiIEot/pl1PwAhaL9sgC4xyhhmSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Kqi1B/hyXWnbKV2P/LB4vr088rXWCsFEOdYRJv1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jaehan4707/RunIt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jaehan4707/RunIt&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gK5A9/hyXWtiIEot/pl1PwAhaL9sgC4xyhhmSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Kqi1B/hyXWnbKV2P/LB4vr088rXWCsFEOdYRJv1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jaehan4707/RunIt: 랭킹 기반 런닝 동기부여 어플리케이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;랭킹 기반 런닝 동기부여 어플리케이션. Contribute to jaehan4707/RunIt development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1년의 끝 싸피 수료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 1년 동안 싸피로 활동을 하면서 많은 것을 경험한 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2학기부터는 정말 시간이 빨리 흘러 벌써 끝이 난 건가? 할 정도로 바쁘게 보냈던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 사람들을 만나서 함께 성장할 수 있는 시간이었고, 같은 목표를 향해 공부할 수 있는 여러 사람들이 있던 것이 큰 원동력이 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LYthQ/btsLEb2uwKF/hb0gKxFuR4C9ATvD42YdX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LYthQ/btsLEb2uwKF/hb0gKxFuR4C9ATvD42YdX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LYthQ/btsLEb2uwKF/hb0gKxFuR4C9ATvD42YdX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLYthQ%2FbtsLEb2uwKF%2Fhb0gKxFuR4C9ATvD42YdX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1050&quot; height=&quot;1400&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Experience/후기(코딩테스트,프로그램,프로젝트)</category>
      <category>11기</category>
      <category>SSAFY</category>
      <category>수료후기</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/507</guid>
      <comments>https://jja2han.tistory.com/507#entry507comment</comments>
      <pubDate>Sun, 5 Jan 2025 19:32:53 +0900</pubDate>
    </item>
    <item>
      <title>[Design Pattern] - 디자인 패턴이란?</title>
      <link>https://jja2han.tistory.com/506</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;디자인 패턴&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;디자인 패턴은 개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 모범 사례다. &lt;br /&gt;디자인 패턴은 객체 지향 4대 특성(캡슐화,상속,추상화,다형성)과 설계 원칙(SOLID)을 기반으로 구현되어 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴은 알고리즘과 자주 혼동된다&lt;/b&gt;. 왜냐하면 두 개념 모두 알려진 문제에 대한 일반적인 해결책을 설명하고 있기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;알고리즘은 어떤 목표를 달성하기 위해 따라야 할 명확한 일련의 절차를 정의&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패턴은 해결책에 대한 더 상위 수준의 설명&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘은 요리법에 비유할 수 있지만 패턴은 요리법이 아닌 &lt;b&gt;청사진에 더 가깝다.&lt;/b&gt; 알고리즘과 요리법 둘 다 목표를 달성하기 위한 명확한 단계들이 제시되어 있지만, 청사진은 결과와 기능들을 제시하나 구현 단계 및 순서는 사용자가 결정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인 패턴의 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;재사용성 : 반복적인 문제에 대한 일반적인 해결책을 제공하므로, 이를 재사용하여 유사한 상황에서 코드를 더 쉽게 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;가독성 : 일정한 구조로 정리하고 명확하게 작성하여 개발자가 코드를 이해하고 유지 보수하기 쉽게 만든다.&lt;/li&gt;
&lt;li&gt;유지 보수성 : 코드를 쉽게 모듈화 할 수 있으며, 변경이 필요한 경우 해당 모듈만 수정하여 유지보수가 쉬워진다.&lt;/li&gt;
&lt;li&gt;확장성 : 새로운 기능을 추가하거나 변경할 때 디자인 패턴을 활용하여 기존 코드를 변경하지 않고도 새로운 기능을 통합할 수 있다.&lt;/li&gt;
&lt;li&gt;안정성과 신뢰성 : 수많은 사람들이 인정한 모범 사례로 검증된 설루션을 제공한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인 패턴의 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴은 소프트웨어 설계에 있어 중요한 도구이지만, 그 사용에는 몇 가지 한계와 비판이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 과도한 사용의 위험&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디자인 패턴을 필요 이상으로 사용하면, 불필요하게 복잡해질 수 있다. 이는 코드의 가독성과 유지 보수성을 저하시킬 수 있음.&lt;/li&gt;
&lt;li&gt;모든 설계 문제에 디자인 패턴을 적용하려는 시도는 때때로 문제를 더 복잡하게 만들 수 있다. 패턴은 특정ㅌ 문제에 대한 해결책이지만, 모든 상황에 적합한 것은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 유연성과 성능 간의 트레이드오프&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일부 디자인 패턴, 특히 구조화 패턴은 추가적인 추상화 레이어를 도입한다. 이는 유연성을 향상시키지만, 동시에 시스템 성능에 부정적인 영향을 줄 수 있다.&lt;/li&gt;
&lt;li&gt;특정 패턴, 예를 들어 싱글턴이나 플라이웨이트는 성능을 최적화하는데 도움이 될 수 있지만, 잘못 사용될 경우 성능 저하를 초래할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 비판적 접근의 중요성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디자인 패턴은 은탄환이 아니며, 모든 설계 문제에 대한 해결책이 될 수는 없다. 각 패턴의 적용은 해당 문제의 맥락과 요구사항을 고려해 신중하게 이루어져야 한다.&lt;/li&gt;
&lt;li&gt;소프트웨어 개발 분야는 지속적으로 변화하고 있기에 개발자는 지속적으로 학습하고 , 새로운 패턴을 탐색하며, 기존의 패턴을 재평가할 필요가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인 패턴의 종류&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csIgal/btsLCnV5Dzf/5gfcPbCQtceYubvygdoc00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csIgal/btsLCnV5Dzf/5gfcPbCQtceYubvygdoc00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csIgal/btsLCnV5Dzf/5gfcPbCQtceYubvygdoc00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsIgal%2FbtsLCnV5Dzf%2F5gfcPbCQtceYubvygdoc00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1076&quot; height=&quot;772&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴은 크게 &lt;b&gt;생성(Creational)&lt;/b&gt;, &lt;b&gt;구조(Structural)&lt;/b&gt;, 그리고 &lt;b&gt;행위(Behavioral)&lt;/b&gt; 패턴으로 분류된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;생성 패턴&lt;/b&gt; : 기존의 코드의 재활용과 유연성을 증가시키는 객체 생성 매커니즘들을 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조 패턴&lt;/b&gt; : 구조를 유연하고 효율저그올 유지하면서 객체와 클래스를 더 큰 구조로 조합하는 방법을 설명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행동 패턴&lt;/b&gt;은 객체 간의 효과적인 의사소통과 책임 할당을 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성 패턴(Creational Pattern)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;싱글톤(Singleton) : 하나의 클래스 인스턴스를 전역에서 접근 가능하게 하면서 해당 인스턴스가 한 번만 생성되도록 보장하는 패턴&lt;/li&gt;
&lt;li&gt;팩토리 메서드 (Factory Method) : 객체를 생성하기 위한 인터페이스를 정의하고, 서브 클래스에서 어떤 클래스의 인스턴스를 생성할지 결정하는 패턴&lt;/li&gt;
&lt;li&gt;추상 팩토리(Abstract Factory) : 관련된 객체들의 집합을 생성하는 인터페이스를 제공하며, 구체적인 팩토리 클래스를 통해 객체 생성을 추상화하는 패턴&lt;/li&gt;
&lt;li&gt;빌더(Builder) : 복잡한 객체의 생성 과정을 단순화하고, 객체를 단계적으로 생성하며 구성하는 패턴&lt;/li&gt;
&lt;li&gt;프로토타입(Prototype) : 객체를 복제하여 새로운 객체를 생성하는 패턴으로 , 기존 객체를 템플릿으로 사용하는 패턴이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조 패턴(Structural Pattern)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;어뎁터(Adapter) : 인터페이스 호환성을 제공하지 않는 클래스를 사용하기 위해 래퍼(Wrapper)를 제공하는 패턴&lt;/li&gt;
&lt;li&gt;브릿지(Bridge) : 추사오하와 구현을 분리하여 두 가지를 독립저그올 확장할 수 있는 패턴&lt;/li&gt;
&lt;li&gt;컴포지트(Composite) : 개별 객체와 복합 객체를 동일하게 다루어, 트리 구조의 객체를 구성하는 패턴&lt;/li&gt;
&lt;li&gt;데코레이터(Decorator) : 객체에 동적으로 새로운 기능을 추가하여 객체를 확장할 수 있는 패턴&lt;/li&gt;
&lt;li&gt;퍼사드(Facade) : 서브 시스템을 더 쉽게 사용할 수 있도록 단순한 인터페이스를 제공하는 패턴&lt;/li&gt;
&lt;li&gt;플라이웨이트 패턴(Flyweight) : 공유 가능한 객체를 통해 메모리 사용을 최적화하는 패턴&lt;/li&gt;
&lt;li&gt;프락시 패턴(Proxy) : 다른 객체에 대한 대리자(Proxy)를 제공하여 접근 제어, 지연 로딩 등을 구현하는 패턴&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;행위 패턴(Behavioral Pattern)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;옵저버(Observer) : 객체 간의 일대다 종속 관계를 정의하여 한 객체의 상태 변경이 다른 객체들에게 알려지도록 한다.&lt;/li&gt;
&lt;li&gt;전략(Strategy) : 알고리즘을 정의하고, 실행 중에 선택할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;커맨드(Command) : 요청을 객체로 캡슐화하여 요청을 매개변수화 하고, 요청을 큐에 저장하거나 로깅하고 실행을 지연시킨다.&lt;/li&gt;
&lt;li&gt;상태(State) : 객체의 상태를 캡슐화하고, 상태 전환을 관리한다.&lt;/li&gt;
&lt;li&gt;책임 연쇄(Chain Of responsibility) : 요청을 보내는 객체와 이를 처리하는 객체를 분리하여, 다양한 처리자 중 하나가 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;방문자(Visitor) : 객체 구조를 순회하면서 다양한 연산을 수행할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;인터프리터(Interpreter) : 언어나 문법에 대한 해석기를 제공하여, 주어진 언어로 표현된 문제를 해결하는 패턴이다.&lt;/li&gt;
&lt;li&gt;메멘토(Memento) : 객체의 내부 상태를 저장하고 복원할 수 있는 기능을 제공하는 패턴이다.&lt;/li&gt;
&lt;li&gt;템플릿 메서드 패턴(Templater Method) : 알고리즘의 구조를 정의하면서 하위 클래스에서 각 단계의 구현을 제공하는 디자인 패턴&lt;/li&gt;
&lt;li&gt;이터레이터(Iterator) : 컬렉션 내의 요소들에 접근하는 방법을 표준화하여 컬렉션의 내부 구조에 독립적으로 접근할 수 있는 패턴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고&lt;/p&gt;
&lt;figure id=&quot;og_1735632177912&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OOP] 디자인 패턴(Design Pattern)이란? - 장점 및 종류&quot; data-og-description=&quot;디자인 패턴(Design Pattern)이란? 디자인 패턴은 개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하&quot; data-og-host=&quot;ittrue.tistory.com&quot; data-og-source-url=&quot;https://ittrue.tistory.com/550&quot; data-og-url=&quot;https://ittrue.tistory.com/550&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3IpIa/hyXSrFTDyC/vZ7kZF6QTU9GKwyxx7Uy50/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IRo3h/hyXWpGqoqr/nD9okt8p3QKelfNArymZG1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KUwop/hyXWtvhOJ7/wmhJjA9uxtxYFhTzZhDYe0/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360&quot;&gt;&lt;a href=&quot;https://ittrue.tistory.com/550&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ittrue.tistory.com/550&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3IpIa/hyXSrFTDyC/vZ7kZF6QTU9GKwyxx7Uy50/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IRo3h/hyXWpGqoqr/nD9okt8p3QKelfNArymZG1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KUwop/hyXWtvhOJ7/wmhJjA9uxtxYFhTzZhDYe0/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OOP] 디자인 패턴(Design Pattern)이란? - 장점 및 종류&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴(Design Pattern)이란? 디자인 패턴은 개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ittrue.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1735632179516&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;디자인 패턴들&quot; data-og-description=&quot;&quot; data-og-host=&quot;refactoring.guru&quot; data-og-source-url=&quot;https://refactoring.guru/ko/design-patterns&quot; data-og-url=&quot;https://refactoring.guru/ko/design-patterns&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Sr8H2/hyXWs4eCdy/CSWqartK4vPiTpBoYtShU1/img.png?width=960&amp;amp;height=245&amp;amp;face=0_0_960_245,https://scrap.kakaocdn.net/dn/cK9aAs/hyXWxxHYSB/JM8okEd3wpW8hPgkjSxIck/img.png?width=835&amp;amp;height=203&amp;amp;face=0_0_835_203&quot;&gt;&lt;a href=&quot;https://refactoring.guru/ko/design-patterns&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://refactoring.guru/ko/design-patterns&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Sr8H2/hyXWs4eCdy/CSWqartK4vPiTpBoYtShU1/img.png?width=960&amp;amp;height=245&amp;amp;face=0_0_960_245,https://scrap.kakaocdn.net/dn/cK9aAs/hyXWxxHYSB/JM8okEd3wpW8hPgkjSxIck/img.png?width=835&amp;amp;height=203&amp;amp;face=0_0_835_203');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴들&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;refactoring.guru&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1735632180286&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[Design pattern] 많이 쓰는  14가지 핵심 GoF 디자인 패턴의 종류&quot; data-og-description=&quot;디자인 패턴을 활용하면 단지 코드만 &amp;lsquo;재사용&amp;rsquo;하는 것이 아니라, 더 큰 그림을 그리기 위한 디자인도 재사용할 수 있습니다. 우리가 일상적으로 접하는 문제 중 상당수는 다른 많은 이들이 접&quot; data-og-host=&quot;www.hanbit.co.kr&quot; data-og-source-url=&quot;https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823&quot; data-og-url=&quot;https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RLuPF/hyXWvNpKtc/K50yaVfLvZ2SnA9FLxKoQ0/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080,https://scrap.kakaocdn.net/dn/dFHdVa/hyXSzRtJWn/nH0lZpZzC1EcKmvRx2H6rK/img.png?width=800&amp;amp;height=572&amp;amp;face=0_0_800_572,https://scrap.kakaocdn.net/dn/bzwiJe/hyXWvUbBeA/Ba88wkmk9RSYeLI9nNsaV0/img.png?width=777&amp;amp;height=574&amp;amp;face=0_0_777_574&quot;&gt;&lt;a href=&quot;https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RLuPF/hyXWvNpKtc/K50yaVfLvZ2SnA9FLxKoQ0/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080,https://scrap.kakaocdn.net/dn/dFHdVa/hyXSzRtJWn/nH0lZpZzC1EcKmvRx2H6rK/img.png?width=800&amp;amp;height=572&amp;amp;face=0_0_800_572,https://scrap.kakaocdn.net/dn/bzwiJe/hyXWvUbBeA/Ba88wkmk9RSYeLI9nNsaV0/img.png?width=777&amp;amp;height=574&amp;amp;face=0_0_777_574');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Design pattern] 많이 쓰는 14가지 핵심 GoF 디자인 패턴의 종류&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴을 활용하면 단지 코드만 &amp;lsquo;재사용&amp;rsquo;하는 것이 아니라, 더 큰 그림을 그리기 위한 디자인도 재사용할 수 있습니다. 우리가 일상적으로 접하는 문제 중 상당수는 다른 많은 이들이 접&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.hanbit.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1735632182211&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;디자인 패턴(Design Pattern) 총정리: 23가지 디자인 패턴 정의, 종류, 장단점&quot; data-og-description=&quot;디자인 패턴은 프로그램을 개발하는 과정에서 빈번하게 발생하는 문제를 정리해서 상황에 따라 간편하게 적용할 수 있게 정리한 것입니다. 패턴을 익히고 적용함으로써, 초보 개발자도 광범위&quot; data-og-host=&quot;oobwrite.com&quot; data-og-source-url=&quot;https://oobwrite.com/entry/디자인-패턴Design-Pattern-총정리-23가지-디자인-패턴-정의-종류-장단점&quot; data-og-url=&quot;https://oobwrite.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4Design-Pattern-%EC%B4%9D%EC%A0%95%EB%A6%AC-23%EA%B0%80%EC%A7%80-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EC%9D%98-%EC%A2%85%EB%A5%98-%EC%9E%A5%EB%8B%A8%EC%A0%90&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/J8bcW/hyXSCm8ntS/8cvfabmhxwzkYOEJiD6KK1/img.gif?width=570&amp;amp;height=437&amp;amp;face=0_0_570_437,https://scrap.kakaocdn.net/dn/cYy5vP/hyXSpuxdKX/xyEXKiIiFwjf6IT8C6SHn0/img.gif?width=570&amp;amp;height=437&amp;amp;face=0_0_570_437&quot;&gt;&lt;a href=&quot;https://oobwrite.com/entry/디자인-패턴Design-Pattern-총정리-23가지-디자인-패턴-정의-종류-장단점&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://oobwrite.com/entry/디자인-패턴Design-Pattern-총정리-23가지-디자인-패턴-정의-종류-장단점&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/J8bcW/hyXSCm8ntS/8cvfabmhxwzkYOEJiD6KK1/img.gif?width=570&amp;amp;height=437&amp;amp;face=0_0_570_437,https://scrap.kakaocdn.net/dn/cYy5vP/hyXSpuxdKX/xyEXKiIiFwjf6IT8C6SHn0/img.gif?width=570&amp;amp;height=437&amp;amp;face=0_0_570_437');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴(Design Pattern) 총정리: 23가지 디자인 패턴 정의, 종류, 장단점&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴은 프로그램을 개발하는 과정에서 빈번하게 발생하는 문제를 정리해서 상황에 따라 간편하게 적용할 수 있게 정리한 것입니다. 패턴을 익히고 적용함으로써, 초보 개발자도 광범위&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;oobwrite.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Computer Science/Design Pattern</category>
      <category>디자인 패턴</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/506</guid>
      <comments>https://jja2han.tistory.com/506#entry506comment</comments>
      <pubDate>Tue, 31 Dec 2024 17:07:38 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] async와 Deferred(Chapter5)</title>
      <link>https://jja2han.tistory.com/505</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot; href=&quot;https://jja2han.tistory.com/504&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/503&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch 코루틴 빌더를 통해 생성되는 &lt;b&gt;코루틴은 기본적으로 작업의 결과를 반환하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴으로부터 결과를 수신해야 하는 상황이 생길 것이다.&amp;nbsp;&lt;br /&gt;예를 들어 네트워크 통신 이후, 응답을 받아 처리해야 할 경우 네트워크 통신을 실행하는 코루틴으로부터 결과를 수신받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 비동기 작업으로부터 결과를 수신해야 하는 경우를 위해 &lt;b&gt;async 코루틴 빌더&lt;/b&gt;를 통해 코루틴으로부터 &lt;b&gt;결괏값을 수신&lt;/b&gt;받을 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch 함수와 다르게 async 함수는&amp;nbsp; &lt;b&gt;결과값이 있는 코루틴 객체 Deferred&lt;/b&gt;가 반환되며 해당 객체를 통해 결괏값을 수신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;async&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async를 사용해 Deffered 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch와 async는 코루틴 빌더로, &lt;b&gt;매우 비슷한 동작 구조&lt;/b&gt;를 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LoZfs/btsKUjz5uvq/uJnLp80KNJbmRQqqAeyxE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LoZfs/btsKUjz5uvq/uJnLp80KNJbmRQqqAeyxE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LoZfs/btsKUjz5uvq/uJnLp80KNJbmRQqqAeyxE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLoZfs%2FbtsKUjz5uvq%2FuJnLp80KNJbmRQqqAeyxE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1168&quot; height=&quot;518&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async 선언부와 launch 선언부&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;CoroutineDispatcher&lt;/b&gt;를 설정할&amp;nbsp; 수 있다.&lt;/li&gt;
&lt;li&gt;start 인자로 지연시작을 설정해 코루틴이 지연시작되도록 할 수 있다.&lt;/li&gt;
&lt;li&gt;코루틴에서 실행할 코드를 작성하는 block 람다식을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차이점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;launch는 결과값을 직접 반환할 수 없다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;async는 코루틴이 결괏값을 직접 반환할&amp;nbsp; 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async는 코루틴에서 결과값을 담아 반환하기 위해 Deferrred &amp;lt;T&amp;gt; 타입의 객체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deferred는 Job과 같이 코루틴을 추상화한 객체이지만, 추가적인 확장으로 결괏값을 감싸는 기능이 있고, 해당 결괏값의 타입은 제네릭 타입인 T로 표현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;await&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deferred 객체는 결과값 &lt;b&gt;수신의 대기&lt;/b&gt;를 위해 await 함수를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join 함수와 유사하게 해당 &lt;b&gt;코루틴이 실행 완료될 때까지 일시 중단&lt;/b&gt;하며, 실행 완료되면 &lt;b&gt;결괏값을 반환하고 호출부의 코루틴을 재개&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1732347949128&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val networkDeferred : Deferred&amp;lt;String&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async &quot;Network Response&quot;
    }
    val result  = networkDeferred.await()
    println(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDPDzI/btsKS2GvQ3W/9lxY2K3kRXiaxFD1wIa170/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDPDzI/btsKS2GvQ3W/9lxY2K3kRXiaxFD1wIa170/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDPDzI/btsKS2GvQ3W/9lxY2K3kRXiaxFD1wIa170/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDPDzI%2FbtsKS2GvQ3W%2F9lxY2K3kRXiaxFD1wIa170%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;220&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 다음과 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhK7u3/btsKTYQtoVK/aCVPOkAdJv5MekAWw5daKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhK7u3/btsKTYQtoVK/aCVPOkAdJv5MekAWw5daKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhK7u3/btsKTYQtoVK/aCVPOkAdJv5MekAWw5daKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhK7u3%2FbtsKTYQtoVK%2FaCVPOkAdJv5MekAWw5daKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;682&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await 함수를 호출하면 networkDeferred 코루틴이 완료될 때까지 runBlocking 코루틴이 일시 중단된다.&lt;br /&gt;이후 코루틴으로부터 네트워크 응답이 반환되면 runBlocking 코루틴이 재개되며, result 변수에 결과가 할당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Deffered&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 코루틴 빌더는 Job 객체를 생성한다는 것은 앞장에서 다룬 내용이다.&lt;br /&gt;하지만 async 코루틴 빌더는 Job 객체가 아닌 Deferred 객체를 생성해 반환한다.&lt;br /&gt;이러면 앞장에서 다룬 내용과 틀리지 않나? 라는 생각이 들 수 있는데, Deferred도 Job 객체이다.&lt;br /&gt;조금 더 자세히 말하면&lt;b&gt;Deferred 객체는 Job 객체의 특수한 형태로 몇 가지 기능이 추가된, 서브 타입이다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKi13P/btsKUxSKE4P/Ke4CtXnbFOKFkWFggtotVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKi13P/btsKUxSKE4P/Ke4CtXnbFOKFkWFggtotVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKi13P/btsKUxSKE4P/Ke4CtXnbFOKFkWFggtotVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKi13P%2FbtsKUxSKE4P%2FKe4CtXnbFOKFkWFggtotVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1460&quot; height=&quot;550&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체의 서브타입이기 때문에, Job 객체의 &lt;b&gt;모든 함수와 프로퍼티에 접근이 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) join, cancel, isActive 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복수의 코루틴으로부터 결과값 수신하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;await를 사용해 복수의 코루틴 결괏값 수신&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;야구장의 티켓을 여러 사이트에서 구매할 수 있다고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 경우 각 사이트에 등록된 구매자를 조회한 후 병합해, 모든 구매자를 확인해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 로드할 사이트가 2개 있으므로 각 사이트의 서버로부터 등록된 구매자들의 데이터를 가져와 병합한 코드는 아래와 같이 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732349153306&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val aDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;이지은&quot;, &quot;김민정&quot;)
    }
    val buyPeopleA = aDeferred.await()
    val bDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;유재석&quot;, &quot;유지민&quot;)
    }
    val buyPeopleB = bDeferred.await()
    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록 : ${listOf(buyPeopleA,buyPeopleB)}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgKFKe/btsKTWSGZw4/cdqareBS9dwvlVLVTzZPg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgKFKe/btsKTWSGZw4/cdqareBS9dwvlVLVTzZPg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgKFKe/btsKTWSGZw4/cdqareBS9dwvlVLVTzZPg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgKFKe%2FbtsKTWSGZw4%2FcdqareBS9dwvlVLVTzZPg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;102&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 다음과 같은 과정으로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;startTime 시작 시간 기록&lt;/li&gt;
&lt;li&gt;aDeferred를 통해 A 구매자 데이터 로드&lt;/li&gt;
&lt;li&gt;A의 서버로부터 결과가 수신될 때까지 대기&lt;/li&gt;
&lt;li&gt;bDeferred를 통해 B 구매자 데이터 로드&lt;/li&gt;
&lt;li&gt;B의 서버로부터 결과가 수신될 때까지 대기&lt;/li&gt;
&lt;li&gt;모든 작업을 마친 후, 걸린 시간과 참여자 목록을 병합해 출력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 정상적으로 나온 것에 주목하지 말고, &lt;b&gt;시간이 2초 걸렸다는 것에 주목을 해야 한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 데이터 로드와 B 데이터 로드는 &lt;b&gt;순차적인 작업이 아니므로, 동시에 진행되는 것이 효율적인 작업&lt;/b&gt;일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;해당 코드는 동시에 진행되는 것이 아니라, 순차적으로 진행&lt;/b&gt;되기 때문에 2초가 걸렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXgDvv/btsKSQ67QOv/cckIjzPsFKDuHDZZFSd88K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXgDvv/btsKSQ67QOv/cckIjzPsFKDuHDZZFSd88K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXgDvv/btsKSQ67QOv/cckIjzPsFKDuHDZZFSd88K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXgDvv%2FbtsKSQ67QOv%2FcckIjzPsFKDuHDZZFSd88K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1618&quot; height=&quot;816&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 순차적으로 진행이 되었을까에 집중해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 &lt;b&gt;await를 호출해 결괏값이 반환될 때까지 코루틴을 일시 중단&lt;/b&gt;시켰기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 A의 결과를 로드할 때까지 대기가 B 작업 요청보다 먼저 실행되었기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A 요청 -&amp;gt; A 대기 -&amp;gt; B요청 -&amp;gt; B대기의 작업순서&lt;/b&gt;가 결정되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 두 작업을 동시에 처리할 수 있는 방법은 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 말했듯이 &lt;b&gt;await의 호출을 뒤로 보내면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732350016118&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val aDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;이지은&quot;, &quot;김민정&quot;)
    }
    val bDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;유재석&quot;, &quot;유지민&quot;)
    }
    val buyPeopleA = aDeferred.await()
    val buyPeopleB = bDeferred.await()
    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록 : ${listOf(buyPeopleA,buyPeopleB)}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VITLD/btsKTHVKxL9/alipr1DmDKQMH2YXwoKNG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VITLD/btsKTHVKxL9/alipr1DmDKQMH2YXwoKNG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VITLD/btsKTHVKxL9/alipr1DmDKQMH2YXwoKNG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVITLD%2FbtsKTHVKxL9%2Falipr1DmDKQMH2YXwoKNG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;248&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 aDeferred.await가 호출되기 전에 bDeferred 코루틴이 실행되므로 두 코루틴이 동시에 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xx1Kp/btsKS7Of9bQ/x6a6uCKOC2KLMMFAcRmxsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xx1Kp/btsKS7Of9bQ/x6a6uCKOC2KLMMFAcRmxsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xx1Kp/btsKS7Of9bQ/x6a6uCKOC2KLMMFAcRmxsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXx1Kp%2FbtsKS7Of9bQ%2Fx6a6uCKOC2KLMMFAcRmxsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;1022&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;aDeferred.await()를 호출하면 코루틴이 일시 중단된다.&lt;/li&gt;
&lt;li&gt;A 작업의 결과를 반환받으면 코루틴이 다시 시작되고, bDeferred.await가 호출되고, 다시 일시 중단 된다.&lt;/li&gt;
&lt;li&gt;B 작업의 결과를 반환받으면 코루틴은 다시 시작되고, 결과를 병합한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 aDeferred와 bDeferred 코루틴이 &lt;b&gt;동시에 실행&lt;/b&gt;되기 때문에 결과를 수신할 때까지 1초 정도만 소요됨을 확인할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 코루틴이 동시에 실행될 수 있도록 만드는 것이 코루틴 성능 측면에서 매우 중요하다.&lt;br /&gt;위 예시는 단편적인 예시로 delay를 1초로 걸었지만, 만약 1분, 5분과 같은 걸리는 &lt;b&gt;작업에선 매우 치명적인 결함이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;awaitAll을 사용한 결괏값 수신&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 N개의 사이트가 있다고 가정할 때, 결과값 수신 대기를 위해서 &lt;b&gt;N개의 await를 작성하기에는 정말 비효율적&lt;/b&gt;일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;join도 joinAll이 있듯이, await도 awaitAll이라는 복수의 결괏값을 수신하기 위한 함수를 제공&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8Jyo/btsKTyrjBZg/MOWCIKU4kLyvvJ50Mrm7l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8Jyo/btsKTyrjBZg/MOWCIKU4kLyvvJ50Mrm7l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8Jyo/btsKTyrjBZg/MOWCIKU4kLyvvJ50Mrm7l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8Jyo%2FbtsKTyrjBZg%2FMOWCIKU4kLyvvJ50Mrm7l1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1220&quot; height=&quot;130&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;awaitAll 함수는 가변인자로 Deferred &amp;lt;T&amp;gt; 타입의 객체를 받아, 해당 객체의 결과가 수신될 때까지 호출부의 코루틴을 일시 중단하고, 결괏값이 수신되면 List로 만들어 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732351251254&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val aDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;이지은&quot;, &quot;김민정&quot;)
    }
    val bDeferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;유재석&quot;, &quot;유지민&quot;)
    }
    val result = awaitAll(aDeferred,bDeferred)
    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록 : $result}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCvC9Z/btsKTrsfAcT/ZVKyboLfpVgIh7ATKvwZZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCvC9Z/btsKTrsfAcT/ZVKyboLfpVgIh7ATKvwZZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCvC9Z/btsKTrsfAcT/ZVKyboLfpVgIh7ATKvwZZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCvC9Z%2FbtsKTrsfAcT%2FZVKyboLfpVgIh7ATKvwZZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;998&quot; height=&quot;218&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 아래와 같이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3ORLr/btsKUgwWj6B/k2eVq7cvR95XWBxVzJ1nn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3ORLr/btsKUgwWj6B/k2eVq7cvR95XWBxVzJ1nn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3ORLr/btsKUgwWj6B/k2eVq7cvR95XWBxVzJ1nn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3ORLr%2FbtsKUgwWj6B%2Fk2eVq7cvR95XWBxVzJ1nn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1650&quot; height=&quot;1004&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;runBlocking 코루틴에서 awaitAll 함수가 호출되면, aDeferred, bDeferred 코투린들의 실행이 모두 완료될 때까지 runBlocking 코루틴을 일시 중단한다.&lt;/li&gt;
&lt;li&gt;두 코루틴의 실행이 완료되면 결과가 반환되고, 중단된 코루틴이 재개된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WithContext&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리에서 제공되는 &lt;b&gt;withContext 함수를 사용하면 async-await 작업을 대체할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NR0Zw/btsKVwlbomM/6iS2ak0Dxt1CgL8UmxYiR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NR0Zw/btsKVwlbomM/6iS2ak0Dxt1CgL8UmxYiR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NR0Zw/btsKVwlbomM/6iS2ak0Dxt1CgL8UmxYiR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNR0Zw%2FbtsKVwlbomM%2F6iS2ak0Dxt1CgL8UmxYiR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;180&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;withContext가 호출되면 함수의 인자로 설정된 CoroutineContext 객체를 사용해 block 람다식을 실행하고, 완료되면 결과를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 async-await를 대체할 수 있을까?&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;withContext 함수를 호출한 코루틴은 인자로 받은 CoroutineContext 객체를 사용해 block 람다식을 실행한다.&lt;/li&gt;
&lt;li&gt;block 람다식을 모두 실행하면 다시 기존의 CoroutineContext 객체를 사용해 코루틴이 재개된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동작은 &lt;b&gt;async-await를 연속적으로 실행했을 때와 매우 비슷하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 async-await를 활용한 코드를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1732352389822&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val deferred: Deferred&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async listOf(&quot;이지은&quot;, &quot;김민정&quot;)
    }
    val result = deferred.await()
    println(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhYMjE/btsKT6HMRR2/0rmlBOZLKGCjaQpWa8Lwu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhYMjE/btsKT6HMRR2/0rmlBOZLKGCjaQpWa8Lwu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhYMjE/btsKT6HMRR2/0rmlBOZLKGCjaQpWa8Lwu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhYMjE%2FbtsKT6HMRR2%2F0rmlBOZLKGCjaQpWa8Lwu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;200&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 async 함수를 호출해 Deferred 객체를 만들고, await 함수를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 async -&amp;gt; await의 구조는 결괏값 수신을 대기하는 코드이고, 이를 withContext로 대체할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1732352518607&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val result = withContext(Dispatchers.IO){
        delay(1000L)
        listOf(&quot;이지은&quot;,&quot;김민정&quot;)
    }
    println(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dv0K3I/btsKTrFMwuu/lM89A97Y7wcCxKJ7Ngk750/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dv0K3I/btsKTrFMwuu/lM89A97Y7wcCxKJ7Ngk750/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dv0K3I/btsKTrFMwuu/lM89A97Y7wcCxKJ7Ngk750/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdv0K3I%2FbtsKTrFMwuu%2FlM89A97Y7wcCxKJ7Ngk750%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;178&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async-await 쌍이 withContext 함수로 대체되면 중간에 &lt;b&gt;Deferred 객체가 생성되는 부분이 없어지고, 결과가 바로 반환된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async-await에 비해 정말 깔끔한 코드이지만,&lt;b&gt;&lt;u&gt; 특정 상황에서 의도치 않게 동작할 수 있다&lt;/u&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;withContext의 동작 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 함수(withContext, async-await)는 겉보기에 비슷하게 동작하지만, &lt;b&gt;내부적으로는 다르게 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async-await는 &lt;b&gt;새로운 코루틴을 생성해 작업을 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;withContext는 실&lt;b&gt;행 중이던 코루틴을 유지&lt;/b&gt;한 채 코루틴의 &lt;b&gt;실행 환경만 변경해 작업을 처리&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732436288245&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    println(&quot;[${Thread.currentThread().name}] runBlocking 블록 실행&quot;)
    withContext(Dispatchers.IO){
        println(&quot;[${Thread.currentThread().name}] withContext 블록 실행&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S28fI/btsKUC0G1Qi/c7fZVFko6KaLGj6fAGssB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S28fI/btsKUC0G1Qi/c7fZVFko6KaLGj6fAGssB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S28fI/btsKUC0G1Qi/c7fZVFko6KaLGj6fAGssB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS28fI%2FbtsKUC0G1Qi%2Fc7fZVFko6KaLGj6fAGssB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;113&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실행 결과를 보면 &lt;b&gt;runBlocking&lt;/b&gt; 함수의 block 람다식을 실행하는 &lt;b&gt;스레드&lt;/b&gt;와 &lt;b&gt;withContext&lt;/b&gt; 함수의 block 람다식을 실행하는 &lt;b&gt;스레드는 다르지만&lt;/b&gt;, &lt;b&gt;코루틴은 같은 것&lt;/b&gt;을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;withContext 함수는 새로운 코루틴을 만드는 대신 기존의 코루틴에서 CoroutineContext 객체만 바꿔서 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 동작 방식을 조금 더 자세히 알아보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;withContext 함수가 호출되면 실행 중인 코루틴의 실행 환경이 withContext 함수의 context 인자 값으로 변경돼 실행되며, 이를 컨&lt;b&gt;텍스트 스위칭&lt;/b&gt;이라고 부른다.&lt;/li&gt;
&lt;li&gt;만약 context 인자로 CoroutineDispatcher&amp;nbsp; 객체가 넘어온다면 코루틴은 해당 객체를 사용해 실행된다.&lt;/li&gt;
&lt;li&gt;따라서 위 코드에서 withContext(Dispatcher.sIO)가 호출되면 해당 코루틴은 다시 Dispatchers.IO의 작업 대기열로 이도한 후, Dispatchers.IO가 사용할 수 있는 스레드 중 하나로 보내져 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PO1SP/btsKTESwNzG/RT213Nw38XJUvuQKZ92SYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PO1SP/btsKTESwNzG/RT213Nw38XJUvuQKZ92SYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PO1SP/btsKTESwNzG/RT213Nw38XJUvuQKZ92SYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPO1SP%2FbtsKTESwNzG%2FRT213Nw38XJUvuQKZ92SYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;370&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 withContext 함수는 함수의&lt;b&gt; block 람다식이 실행되는 동안 코루틴의 실행 환경을 변경&lt;/b&gt;시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async-await 방식의 내부 동작을 코드를 통해 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1732437536925&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    println(&quot;[${Thread.currentThread().name}] runBlocking 블록 실행&quot;)
    async(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] async 블록 실행&quot;)
    }.await()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xXWTX/btsKVxYWcTB/WYD9KxQDyOiKWcp5cTJNsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xXWTX/btsKVxYWcTB/WYD9KxQDyOiKWcp5cTJNsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xXWTX/btsKVxYWcTB/WYD9KxQDyOiKWcp5cTJNsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxXWTX%2FbtsKVxYWcTB%2FWYD9KxQDyOiKWcp5cTJNsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;99&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실해 결과를 보면 async 블록을 실행하는 &lt;b&gt;코루틴은&lt;/b&gt; runBlocking &lt;b&gt;코루틴과&lt;/b&gt; &lt;b&gt;다른&lt;/b&gt; 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 async-await를 사용하면 &lt;b&gt;새로운 코루틴을 만들지만 await 함수가 호출돼 순차 처리가 돼 동기적으로 실행되는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJhOAi/btsKUfSze8M/crI9MXHIwfkkilZgdYcbwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJhOAi/btsKUfSze8M/crI9MXHIwfkkilZgdYcbwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJhOAi/btsKUfSze8M/crI9MXHIwfkkilZgdYcbwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJhOAi%2FbtsKUfSze8M%2FcrI9MXHIwfkkilZgdYcbwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1012&quot; height=&quot;442&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;withContext를 호출하면 코루틴이 유지된 채로 코루틴을 실행하는 스레드만 변경되기 때문에 동기적으로 실행되는 것이고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async-await를 사용하면 새로운 코루틴을 만들지만 await를 통해 순차 처리가 돼 동기적으로 실행되는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 새로운 코루틴을 만드는 것과 만들지 않는 것에 대한 차이로 발생하는 주의점이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복수의 독립적인 작업이 병렬적으로 실행돼야 하는 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;withContext 함수는 새로운 코루틴을 만들지 않기 때문에 하나의 코루틴에서 withContext 함수가 여러 번 호출되면 순차적으로 실행된다. 즉, &lt;b&gt;복수의 독립적인 작업에서 성능 문제가 발생할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732438593016&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val str1 = withContext(Dispatchers.IO) {
        delay(1000L)
        &quot;My Name is&quot;
    }
    val str2 = withContext(Dispatchers.IO) {
        delay(1000L)
        &quot;jaehan&quot;
    }
    println(
        &quot;[${getElapsedTime(startTime)}] $str1 $str2&quot;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vJzmp/btsKVc1UgV9/cnpABGAFvutErXtVFk7hDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vJzmp/btsKVc1UgV9/cnpABGAFvutErXtVFk7hDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vJzmp/btsKVc1UgV9/cnpABGAFvutErXtVFk7hDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvJzmp%2FbtsKVc1UgV9%2FcnpABGAFvutErXtVFk7hDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;106&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 1초간 대기 후 문자열을 반환하는 2개의 작업을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 작업은 withContext를 통해 백그라운드 스레드에서 &lt;u&gt;&lt;b&gt;병렬적으로 실행&lt;/b&gt;&lt;/u&gt;되는 것처럼 보이지만 &lt;b&gt;실제로는 순차적으로 실행&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;코드의 실행 결과로 2초가 소요됨&lt;/b&gt;을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIP2Ql/btsKVDkqM9r/3WUQRV9Bo0SmaKuefZWtO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIP2Ql/btsKVDkqM9r/3WUQRV9Bo0SmaKuefZWtO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIP2Ql/btsKVDkqM9r/3WUQRV9Bo0SmaKuefZWtO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIP2Ql%2FbtsKVDkqM9r%2F3WUQRV9Bo0SmaKuefZWtO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;459&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 &lt;b&gt;runBlocking 코루틴 하나만 생성&lt;/b&gt;된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 메인 스레드에서 실행되는데, &lt;b&gt;withContext를 사용하면 코루틴을 유지한 채 실행 스레드&lt;/b&gt;만 변경된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 각 withContext 블록의 코드를 실행하는데 1초가 걸리지만 순차적으로 처리돼 2초의 시간이 걸리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이는 withContext 함수가 새로운 코루틴을 생성하지 않기 때문에 생기는 문제이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 새로운 코루틴을 만드는 async-await를 통해 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732439416743&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val str1 = async(Dispatchers.IO) {
        delay(1000L)
        &quot;My Name is&quot;
    }
    val str2 = async(Dispatchers.IO) {
        delay(1000L)
        &quot;jaehan&quot;
    }

    val result = awaitAll(str1, str2)
    println(&quot;[${getElapsedTime(startTime)}] $result&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHbcq3/btsKVaQwfd8/9FfZwjc2zKMCBw1k32jJV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHbcq3/btsKVaQwfd8/9FfZwjc2zKMCBw1k32jJV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHbcq3/btsKVaQwfd8/9FfZwjc2zKMCBw1k32jJV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHbcq3%2FbtsKVaQwfd8%2F9FfZwjc2zKMCBw1k32jJV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;103&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 두 작업 모두 실행 뒤 &lt;b&gt;awaitAll&lt;/b&gt; 함수가 호출됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 2개의 코루틴이 &lt;b&gt;병렬적으로 실행&lt;/b&gt;돼 코드를 실행하는 데 &lt;b&gt;1초가 소요됨&lt;/b&gt;을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;705&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oyiKw/btsKU7TGZlE/vZGHK3fFIP3sPQlaXUrsN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oyiKw/btsKU7TGZlE/vZGHK3fFIP3sPQlaXUrsN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oyiKw/btsKU7TGZlE/vZGHK3fFIP3sPQlaXUrsN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoyiKw%2FbtsKU7TGZlE%2FvZGHK3fFIP3sPQlaXUrsN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;917&quot; height=&quot;705&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;705&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;withContext 함수를 사용한 코드가 깔끔해 보이긴 하지만, &lt;b&gt;잘못 사용할 경우 코루틴을 동기적으로 실행하도록 만들어, 실행 시간이 배로 증가할 수 있다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async 함수를 사용해 코루틴을 실행하면 코루틴의 결과를 감싸는 Deferred 객체를 반환받는다.&lt;/li&gt;
&lt;li&gt;Deferred는 Job의 서브타입으로, 결괏값을 반환하는 기능이 추가된 객체이다.&lt;/li&gt;
&lt;li&gt;Deferred는 await 함수를 통해 결과값을 반환할 때까지 코루틴을 일시 중단시킬 수 있다.&lt;/li&gt;
&lt;li&gt;awaitAll 함수를 사용해 복수의 Deferred 코루틴의 결과값을 수신할 수 있다.&lt;/li&gt;
&lt;li&gt;withContext 함수는 async-await를 대체할 수 있다.&lt;/li&gt;
&lt;li&gt;withContext는 코루틴을 새로 생성하지 않는다.&lt;br /&gt;코루틴의 실행 환경을 변경해 코루틴을 실행하므로, 이를 활용해 코루틴이 실행되는 스레드를 변경할 수 있다.&lt;/li&gt;
&lt;li&gt;withContext로 인해 실행 환경이 변경돼 실행되는 코루틴은 작업을 모두 실행하면 다시 이전의 실행 환경으로 돌아온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732440921599&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cdt9PA/hyXC95m9nm/dg41iGFqPRfBh83w7wllGK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/q8MHg/hyXDfdsdny/LDTbBfMJ3RMUCXBJkgmAh1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/boHmvH/hyXDmDFE3F/rjlDEaUy1NhJcukvgqu0Ek/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cdt9PA/hyXC95m9nm/dg41iGFqPRfBh83w7wllGK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/q8MHg/hyXDfdsdny/LDTbBfMJ3RMUCXBJkgmAh1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/boHmvH/hyXDmDFE3F/rjlDEaUy1NhJcukvgqu0Ek/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>Kotlin</category>
      <category>코루틴</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/505</guid>
      <comments>https://jja2han.tistory.com/505#entry505comment</comments>
      <pubDate>Sun, 24 Nov 2024 18:35:36 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-2)</title>
      <link>https://jja2han.tistory.com/504</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 글&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot; href=&quot;https://jja2han.tistory.com/503&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴의 취소 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cancel&lt;/b&gt; 함수나, &lt;b&gt;cancelAndJoin&lt;/b&gt; 함수는 코루틴을 즉시 취소하는 것이 아닌 취소 요청을 보내는 함수이다. 취소 요청을 받은 코루틴이 요청을 확인하는 시점에 비로소 취소가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 코루틴이 취소 요청을 확인하지 않는다면 영원히 취소되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 코루틴들은 언제 취소를 확인할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴이 &lt;b&gt;취소를 확인하는 시점은 일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점&lt;/b&gt;이며, &lt;br /&gt;이 시점들이 없다면 코루틴은 취소되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch(Dispatchers.Default) {
          while(true){
            println(&quot;작업 중!&quot;)
        }
    }
    delay(1000L)
    job1.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rB7UZ/btsKTVmfdIn/8bzu2uYPLkQLKzzQxzMPiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rB7UZ/btsKTVmfdIn/8bzu2uYPLkQLKzzQxzMPiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rB7UZ/btsKTVmfdIn/8bzu2uYPLkQLKzzQxzMPiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrB7UZ%2FbtsKTVmfdIn%2F8bzu2uYPLkQLKzzQxzMPiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1402&quot; height=&quot;516&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초 후 cancel 요청을 했지만, 코루틴이 취소되지 않고, 계속해서 while문을 실행시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소되지 않는 이유는 &lt;b&gt;코루틴의 취소를 확인하지 못했기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드로는 while문을 벗어날 수 없고, while 문 내부에도 일시 중단 지점이 없기 때문에 취소를 확인할 시점이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드가 취소되도록 만드는 방법은 세 가지 방법이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;dealy&lt;/li&gt;
&lt;li&gt;yield&lt;/li&gt;
&lt;li&gt;CoroutineScope.isActive&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;delay를 사용한 취소 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delay는 suspend 함수로 선언돼 특정 시간만큼 호출부의 코루틴을 일시 중단하게 만든다.&lt;br /&gt;코루틴은 일시 중단되는 시점에 코루틴의 취소를 확인하기 때문에 while문 내부에 delay를 사용하면 일시 중단 후 취소를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        while (true){
            println(&quot;작업 중!&quot;)
            delay(1L)
        }
    }
    delay(10L)
    job1.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VpZXx/btsKU8ZbqDs/EacXwWl0ptTuh2Co9GY3C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VpZXx/btsKU8ZbqDs/EacXwWl0ptTuh2Co9GY3C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VpZXx/btsKU8ZbqDs/EacXwWl0ptTuh2Co9GY3C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVpZXx%2FbtsKU8ZbqDs%2FEacXwWl0ptTuh2Co9GY3C0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;256&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 실행 결과를 보면 10ms 이후에 프로세스가 종료됨을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 이 방법은 while문이 반복될 때마다 작업을 일시 중단 시키고 있고, 이는 성능저하의 원인이 될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;yield를 사용한 취소 확인&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;yield 함수는 자신이 사용하던 스레드를 양보한다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드 사용을 양보한다는 것은 스레드 사용을 중단한다는 뜻&lt;/b&gt;으로 yield를 호출한 코루틴이 일시 중단되며 이 시점에 취소됐는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        while (true){
            println(&quot;작업 중!&quot;)
            yield()
        }
    }
    delay(10L)
    job1.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5ivZ9/btsKTDTnHnH/AAXlcjxc41TTMg5qDywtkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5ivZ9/btsKTDTnHnH/AAXlcjxc41TTMg5qDywtkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5ivZ9/btsKTDTnHnH/AAXlcjxc41TTMg5qDywtkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5ivZ9%2FbtsKTDTnHnH%2FAAXlcjxc41TTMg5qDywtkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1416&quot; height=&quot;728&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 근본적으로 while 문 내부에서 일시 중단시키는 과정은 반복되고, 이러한 작업은 성능 저하로 이어진다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineScope.isActive를 사용한 취소 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CorotuineScope&lt;/b&gt;는 코루틴이 활성화됐는지 확인할 수 있는 &lt;b&gt;Boolean 타입의 프로퍼티인 isActive&lt;/b&gt;를 제공한다. &lt;br /&gt;코루틴에 취소가 요청되면 isActive 프로퍼티의 값은 false로 바뀌며, while 문의 인자로 isActive를 넘기면 코루틴이 취소 요청이 되면 while문을 취소시킬 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        while (this.isActive) {
            println(&quot;작업 중!&quot;)
            yield()
        }
    }
    delay(10L)
    job1.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy3MDD/btsKVvT4Cy9/McBlcZNwbmpgodwi5YLkIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy3MDD/btsKVvT4Cy9/McBlcZNwbmpgodwi5YLkIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy3MDD/btsKVvT4Cy9/McBlcZNwbmpgodwi5YLkIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy3MDD%2FbtsKVvT4Cy9%2FMcBlcZNwbmpgodwi5YLkIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1416&quot; height=&quot;728&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 사용하면 코루틴이 잠시 멈추지도 않고 스레드 사용을 양보하지도 않으면서 계속 작업을 할 수 있어 효율적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴의 상태와 Job의 상태 변수&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/emRMCH/btsKS80FU8t/h8dmdQSunYn66xVtTdYxK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/emRMCH/btsKS80FU8t/h8dmdQSunYn66xVtTdYxK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/emRMCH/btsKS80FU8t/h8dmdQSunYn66xVtTdYxK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FemRMCH%2FbtsKS80FU8t%2Fh8dmdQSunYn66xVtTdYxK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1362&quot; height=&quot;558&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 위 그림과 같이 6가지 상태를 가질 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성 : 코루틴 빌더를 통해 코루틴을 생성하면 코루틴은 기본적으로 생성 상태에 놓이며, &lt;b&gt;자동으로 실행 중 상태로 넘어간다&lt;/b&gt;. &lt;br /&gt;만약 생성 상태의 코루틴이 실행 중 상태로 자동으로 변경되지 않도록 만들고 싶다면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 코루틴을 만들면 된다.&lt;/li&gt;
&lt;li&gt;실행 중 : 지연 코루틴이 아닌 코루틴을 만들면 자동으로 실행 중 상태로 바뀐다.&lt;br /&gt;코루틴이 실제로 실행 중일 때뿐만 아니라 실행된 후에 일시 중단된 때도 실행 중 상태로 본다.&lt;/li&gt;
&lt;li&gt;실행 완료 : 코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어간다.&lt;/li&gt;
&lt;li&gt;취소 중 : Job.cancel(), cancelAndJoin을 통해 코루틴에 취소가 요청됐을 경우 취소 중 상태로 넘어가며, 이는 아직 취소된 상태가 아니어서 코루틴은 계속해서 실행된다.&lt;/li&gt;
&lt;li&gt;취소 완료 : 코루틴의 취소 확인 시점에 취소가 확인된 경우 취소 완료 상태가 된다.&lt;br /&gt;이 상태에서는 더 이상 실행되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체는 코루틴이 어떤 상태에 있는지 나타내는 상태 변수들을 외부로 공개한다.&lt;br /&gt;상태 변수는 &lt;b&gt;isActive, isCancelled, isCompleted&lt;/b&gt;의 세가지이며, 각 변수는 모두 boolean 타입이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isActive : 코루틴이 활성화 돼 있는지의 여부, 코루틴이 활성화돼 있으면 true를 반환하고, 활성화돼 있지 않으면 false를 반환한다. 활성화돼 있다는 것은 코루틴이 실행된 후 취소가 요청되거나 실행이 완료되지 않은 상태라는 의미이다. 따라서 취소가 요청되거나 실행이 완료된 코루틴은 활성화되지 않은 것으로 본다.&lt;/li&gt;
&lt;li&gt;isCancelled : 코루틴이 취소 요청됐는지의 여부, 요청되기만 하면 true가 반환되므로 true이더라도 즉시 취소되는 것은 아니다.&lt;/li&gt;
&lt;li&gt;isCompleted : 코루틴 실행이 완료됐는지의 여부, 실행 중일 경우 false를 반환하고, 실행 완료, 취소 완료일 경우 true를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성 상태의 코루틴&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pfeHG/btsKVtvfmx8/vaFmkO70GpBMzHnqW9vkKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pfeHG/btsKVtvfmx8/vaFmkO70GpBMzHnqW9vkKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pfeHG/btsKVtvfmx8/vaFmkO70GpBMzHnqW9vkKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpfeHG%2FbtsKVtvfmx8%2FvaFmkO70GpBMzHnqW9vkKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1386&quot; height=&quot;464&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;**코루틴이 생성만 되고 실행되지 않은 상태**&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 상태의 코루틴을 만들기 위해서는 지연 시작이 적용된 코루틴을 생성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch(start = CoroutineStart.LAZY) {
        delay(1000L)
    }
    printJobState(job1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pJBEm/btsKUfEI5iO/RzXgBFis5VnMrCzKwvjKE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pJBEm/btsKUfEI5iO/RzXgBFis5VnMrCzKwvjKE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pJBEm/btsKUfEI5iO/RzXgBFis5VnMrCzKwvjKE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpJBEm%2FbtsKUfEI5iO%2FRzXgBFis5VnMrCzKwvjKE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1038&quot; height=&quot;366&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드의 실행 결과를 보면 생성된 후 실행 X, 취소 요청 X, 실행 완료 X &amp;rarr; 모두 false가 반환된다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 중 상태의 코루틴&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2iXj4/btsKVqyxIcr/LsNKVA9XeVbnFAnamZDVt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2iXj4/btsKVqyxIcr/LsNKVA9XeVbnFAnamZDVt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2iXj4/btsKVqyxIcr/LsNKVA9XeVbnFAnamZDVt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2iXj4%2FbtsKVqyxIcr%2FLsNKVA9XeVbnFAnamZDVt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;458&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 빌더로 코루틴을 생성하면 &lt;b&gt;CoroutineDispatcher에&lt;/b&gt; 의해 스레드로 보내져 실행되고, 이때의 코루틴의 상태를 &lt;b&gt;실행 중&lt;/b&gt; 상태라 부른다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000L)
    }
    printJobState(job1)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTea7A/btsKT4pDpkY/MYCD1wYFrYukrz2grdJu0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTea7A/btsKT4pDpkY/MYCD1wYFrYukrz2grdJu0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTea7A/btsKT4pDpkY/MYCD1wYFrYukrz2grdJu0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTea7A%2FbtsKT4pDpkY%2FMYCD1wYFrYukrz2grdJu0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;288&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 후 취소 요청 X, 실행 완료 X &amp;rarr; isActive만 true&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 완료 상태의 코루틴&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZmvT/btsKUiuH0br/Eo8syBO14HZKhm0qCk2AsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZmvT/btsKUiuH0br/Eo8syBO14HZKhm0qCk2AsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZmvT/btsKUiuH0br/Eo8syBO14HZKhm0qCk2AsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZmvT%2FbtsKUiuH0br%2FEo8syBO14HZKhm0qCk2AsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1374&quot; height=&quot;454&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초간 실행되는 코루틴을 생성하고 3초 대기후 Job의 상태를 출력해 보는 코드를 작성해 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000L)
    }
    delay(3000L)
    printJobState(job1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6BTL5/btsKVbIqFEj/Krhd5wMPEzq9p0iVqSJ7hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6BTL5/btsKVbIqFEj/Krhd5wMPEzq9p0iVqSJ7hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6BTL5/btsKVbIqFEj/Krhd5wMPEzq9p0iVqSJ7hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6BTL5%2FbtsKVbIqFEj%2FKrhd5wMPEzq9p0iVqSJ7hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1302&quot; height=&quot;266&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3초가 지난 시점이라, 코루틴이 실행 완료되고 isCompleted가 true 인 것을 확인할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;취소 중인 코루틴&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pnzlW/btsKT3EeIWr/KEuDgwuLakpRtQi84kWXw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pnzlW/btsKT3EeIWr/KEuDgwuLakpRtQi84kWXw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pnzlW/btsKT3EeIWr/KEuDgwuLakpRtQi84kWXw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpnzlW%2FbtsKT3EeIWr%2FKEuDgwuLakpRtQi84kWXw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1370&quot; height=&quot;472&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소가 요청됐으나 취소되지 않은 상태인 취소 중 코루틴의 상태를 하기 위해선 코루틴의 취소를 요청해야 한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val job: Job = launch {
        while (true){

        }
    }
    job.cancel()
    printJobState(job)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 코루틴이 취소를 확인할 수 있는 시점이 없어서 실제로 코루틴이 취소 요청만 보낼 뿐이지, 실제로 취소되지는 않으므로, 취소 중인 상태에 머물고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ABZY5/btsKVvzLrDh/KZHHh7GlfHOwZkm0veCeIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ABZY5/btsKVvzLrDh/KZHHh7GlfHOwZkm0veCeIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ABZY5/btsKVvzLrDh/KZHHh7GlfHOwZkm0veCeIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FABZY5%2FbtsKVvzLrDh%2FKZHHh7GlfHOwZkm0veCeIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1396&quot; height=&quot;296&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 결과는 isCompleted가 아닌, isCancelled의 상태가 유지된다.&lt;br /&gt;&lt;b&gt;중요한 점은 취소가 요청되고, 실제로 코드가 실행 중이더라도 코루틴이 활성화된 상태로 보지 않아 isActive는 false가 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;취소 완료된 코루틴&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Nqxu/btsKTD6UEMw/sQR5hjBEBoobpXmS7c6wh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Nqxu/btsKTD6UEMw/sQR5hjBEBoobpXmS7c6wh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Nqxu/btsKTD6UEMw/sQR5hjBEBoobpXmS7c6wh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Nqxu%2FbtsKTD6UEMw%2FsQR5hjBEBoobpXmS7c6wh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1382&quot; height=&quot;416&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 취소가 요청되고 취소 요청아 확인되는 시점에 취소가 완료된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val job: Job = launch {
        delay(5000L)
    }
    job.cancelAndJoin()
    printJobState(job)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 launche 함수를 통해 5초간 지속되는 코루틴을 생성한 후, 코루틴이 취소될 수 있도록 cancelAndJoin 함수를 호출한다.&lt;br /&gt;5초가 지난 후, 일시 중단 상태가 되고, 코루틴의 취소 요청을 확인했으므로, 코루틴의 상태는 취소 완료가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcG9Tv/btsKUlLm6AI/BhtEAVm3DcZqb7orr0cKr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcG9Tv/btsKUlLm6AI/BhtEAVm3DcZqb7orr0cKr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcG9Tv/btsKUlLm6AI/BhtEAVm3DcZqb7orr0cKr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcG9Tv%2FbtsKUlLm6AI%2FBhtEAVm3DcZqb7orr0cKr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1382&quot; height=&quot;316&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코루틴 상태와 Job 상태&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;코루틴 상태&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isActive&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isCancelled&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isCompleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;생성&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;실행 중&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;실행 완료&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;취소 중&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;취소 완료&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;요약&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;runBlocking 함수와 launche 함수는 코루틴을 만들기 위한 코루틴 빌더 함수이다.&lt;/li&gt;
&lt;li&gt;launch 함수를 호출하면 Job 객체가 만들어져 반환되며, Job 객체는 코루틴의 상태를 추적하고 제어하는 데 사용된다.&lt;/li&gt;
&lt;li&gt;Job 객체의 Join 함수를 호출하면 함수를 호출한 코루틴이 Job 객체의 실행이 완료될 때까지 일시 중단된다.&lt;/li&gt;
&lt;li&gt;joinAll 함수를 사용해 복수의 코루틴이 실행 완료될 때까지 대기할 수 있다.&lt;/li&gt;
&lt;li&gt;Job 객체의 cancel 함수를 사용해 코루틴에 취소를 요청할 수 있다.&lt;/li&gt;
&lt;li&gt;cancel 함수가 호출되면 코루틴이 곧바로 취소되는 것이 아니라 취소 플래그의 상태만 바뀌는 것이다.&lt;/li&gt;
&lt;li&gt;코루틴에 취소를 요청한 후 취소가 완료될 때까지 대기하고 나서 다음 코드를 실행하고 싶다면 cancel 대신 cancelAndJoin 함수를 사용하면 된다.&lt;/li&gt;
&lt;li&gt;delay, yield 함수나 isActive 프로퍼티 등을 사용해 코루틴이 취소를 확인할 수 있도록 만들 수 있다.&lt;/li&gt;
&lt;li&gt;코루틴은 생성, 실행 중, 실행 완료 중, 취소 중, 취소 완료 상태를 가진다.&lt;/li&gt;
&lt;li&gt;Job 객체는 isActive, isCancelled, isCompleted 프로퍼티를 통해 코루틴의 상태를 나타낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732440948019&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cdt9PA/hyXC95m9nm/dg41iGFqPRfBh83w7wllGK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/q8MHg/hyXDfdsdny/LDTbBfMJ3RMUCXBJkgmAh1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/boHmvH/hyXDmDFE3F/rjlDEaUy1NhJcukvgqu0Ek/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cdt9PA/hyXC95m9nm/dg41iGFqPRfBh83w7wllGK/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/q8MHg/hyXDfdsdny/LDTbBfMJ3RMUCXBJkgmAh1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/boHmvH/hyXDmDFE3F/rjlDEaUy1NhJcukvgqu0Ek/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>Kotlin</category>
      <category>코틀린</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/504</guid>
      <comments>https://jja2han.tistory.com/504#entry504comment</comments>
      <pubDate>Sat, 23 Nov 2024 14:12:05 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] 코루틴 빌더와 Job(Chapter4-1)</title>
      <link>https://jja2han.tistory.com/503</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #666666; font-size: 16px; background-color: #fcfcfc;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/502&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] CoroutineDispatcher(Chapter3)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴 빌더&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴을 생성하는 데 사용하는 함수, 빌더 함수를 호출하면 새로운 코루틴이 생성된다.&lt;/li&gt;
&lt;li&gt;ex): runBlocking, launch&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 코루틴 빌더 함수는 코루틴을 만들고 코루틴을 추상화한 Job 객체를 생성한다. &lt;br /&gt;launch 함수 또한 코루틴 빌더이므로, 다음과 같이 launch 함수를 호출하면 코루틴이 만들어지고, Job 객체가 생성돼 반환된다. &lt;br /&gt;반환된 Job 객체는 코루틴의 상태를 추적하고 제어하는 데 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;join을 사용한 코루틴 순차 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 간에 순차 처리가 필요한 경우는 어떤 경우일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예로는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터베이스 작업을 &lt;b&gt;순차적으로 처리&lt;/b&gt;해야 하는 경우&lt;/li&gt;
&lt;li&gt;캐싱된 토큰 값이 업데이트된 이후에 네트워크 요청을 해야 하는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우는 각 작업을 하는 코루틴이 &lt;b&gt;각자에 순서에 맞게 순차적으로 처리&lt;/b&gt;돼야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체는 순차 처리가 필요한 상황을 위해 &lt;b&gt;join&lt;/b&gt; 함수를 제공한다.&lt;br /&gt;join 함수의 역할은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 A, 작업 B가 있을 때, 작업 A &amp;rarr; 작업 B로 진행되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 작업B에 대해, &amp;ldquo;작업 A가 끝나면 시작해&amp;rdquo;와 같은 명령을 줄 수 있는 함수가 join 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문적으로 풀어서 설명하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;join 함수는 먼저 처리돼야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만든다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 순차 처리가 안될 경우의 상황이다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Updating token...&quot;)
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] Token updated!&quot;)
    }

    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Network calling!&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 의도는 토큰을 업데이트하고, 업데이트된 코드를 바탕으로 네트워크 작업을 요청하는 것이지만, &lt;b&gt;실제 결과는 그렇지 않다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xQehV/btsKQzqr8dB/u480E68BhLQiPAgBat6n4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xQehV/btsKQzqr8dB/u480E68BhLQiPAgBat6n4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xQehV/btsKQzqr8dB/u480E68BhLQiPAgBat6n4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxQehV%2FbtsKQzqr8dB%2Fu480E68BhLQiPAgBat6n4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;151&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 토큰 업데이트 시작 이후 토큰 업데이트가 끝나기 전에 네트워크 요청을 하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdA58g/btsKSC6YjuX/go0YISagUKdcmsyAtajpNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdA58g/btsKSC6YjuX/go0YISagUKdcmsyAtajpNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdA58g/btsKSC6YjuX/go0YISagUKdcmsyAtajpNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdA58g%2FbtsKSC6YjuX%2Fgo0YISagUKdcmsyAtajpNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;306&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 도식화한 그림이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;runBlocking 코루틴은 메인 스레드에서 실행되는 코루틴으로 runBlocking 코루틴에서 launch 함수를 호출해 updateTokenJob을 생성하고 Dispatchers.IO에 해당 코루틴을 실행 요청한다.&lt;/li&gt;
&lt;li&gt;Dispatchers.IO는 worker-1 스레드에 해당 코루틴을 할당해 실행시킨다.&lt;/li&gt;
&lt;li&gt;runBlocking 코루틴은 launch 함수를 한 번 더 호출해 neworkCallJob을 생성하고, Dispatchers.IO에 실행 요청 &amp;rarr; worker-3 스레드에 networkCallJob을 보내 실행시킴.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 잘못된 코드로, &lt;b&gt;토큰 요청과 네트워크 요청 작업이 병렬로 동시에 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 토큰 요청이 완료된 이후에 networkCallJob이 실행돼야 한다. 코루틴, &lt;br /&gt;job 객체는 이러한 문제 해결을 위해 순차 처리할 수 있는 join 함수를 제공한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;join 함수 사용 해 순차 처리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 방법은 간단한데, JobA 코루틴이 완료된 후에 JobB 코루틴이 실행돼야 한다면 JobB 코루틴이 실행되기 전에 &lt;br /&gt;JobA 코루틴에 join 함수를 호출하면 된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Updating token...&quot;)
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] Token updated!&quot;)
    }
    updateTokenJob.join()
    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Network calling!&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DVpRE/btsKR8efXZN/e2ZonJ1tLQxEDl58fj4DQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DVpRE/btsKR8efXZN/e2ZonJ1tLQxEDl58fj4DQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DVpRE/btsKR8efXZN/e2ZonJ1tLQxEDl58fj4DQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDVpRE%2FbtsKR8efXZN%2Fe2ZonJ1tLQxEDl58fj4DQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;117&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 객체의 join 함수를 호출하면 join의 대상이 된 코루틴의 작업이 완료될 때까지 join을 호출한 코루틴이 일시 중단된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;updaetTokenJob.join이 호출되면, runBlocking 코루틴은 updateTokenJob 코루틴이 완료될 때까지 일시 중단된다.&lt;/li&gt;
&lt;li&gt;이후 updateTokenJob 내부의 코드가 모두 실행되면 runBlocking 코루틴이 재개돼 networkCallJob을 실행한다.&lt;/li&gt;
&lt;li&gt;토큰 업데이트 완료 &amp;rarr; 네트워크 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZK3Cd/btsKQBIxFUE/KSfVPg7XMPcwYTTycbtrk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZK3Cd/btsKQBIxFUE/KSfVPg7XMPcwYTTycbtrk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZK3Cd/btsKQBIxFUE/KSfVPg7XMPcwYTTycbtrk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZK3Cd%2FbtsKQBIxFUE%2FKSfVPg7XMPcwYTTycbtrk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;281&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 다음과 같은 과정으로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;join 함수는 join을 호출한 코루틴만 일시 중단한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;join 함수는 join 함수를 호출한 코루틴을 제외하고 이미 실행중인 다른 코루틴을 일시 중단하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Updating token...&quot;)
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] Token updated!&quot;)
    }
    val independentJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Independent job...&quot;)
    }
    updateTokenJob.join()
    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] Network calling!&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/80tRG/btsKQF5gjYH/4rfGhN5dC1sc7E3s5dXtk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/80tRG/btsKQF5gjYH/4rfGhN5dC1sc7E3s5dXtk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/80tRG/btsKQF5gjYH/4rfGhN5dC1sc7E3s5dXtk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F80tRG%2FbtsKQF5gjYH%2F4rfGhN5dC1sc7E3s5dXtk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;164&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 동작 결과는 토큰 업데이트 요청 &amp;rarr; 독립 작업 시작 &amp;rarr; 토큰 업데이트 완료 &amp;rarr; 네트워크 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 updateTokenJob.join()이 호출되더라도 updateTokenJob이 끝날 때까지 independentJob은 기다리지 않고 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLTds1/btsKQq8jFZ6/ZF3uUURJiXHRLB8kPvxRIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLTds1/btsKQq8jFZ6/ZF3uUURJiXHRLB8kPvxRIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLTds1/btsKQq8jFZ6/ZF3uUURJiXHRLB8kPvxRIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLTds1%2FbtsKQq8jFZ6%2FZF3uUURJiXHRLB8kPvxRIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;671&quot; height=&quot;383&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;runBlocking 코루틴은 updateTokenJob.join을 호출하기 전에 independentJob을 실행한다.&lt;/li&gt;
&lt;li&gt;join 함수를 호출한 코루틴은 runBlocking 코루틴이고, join의 대상이 된 코루틴은 updateTokenJob이므로, runBlocking 코루틴만 updateTokenJob이 완료될 때까지 중단된다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;다른 스레드에서 이미 실행중인 independentJob은 일시중단에 영향을 받지 않는 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;joinAll을 사용한 코루틴 순차 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join을 통해서 하나의 코루틴을 일시중단 할 수 있는데, 여러 개의 코루틴의 실행이 모두 끝날 때까지 &lt;b&gt;일시 중단&lt;/b&gt;시키는 방법은 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그러기 위해서 코루틴은 joinAll 함수를 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;joinAll 함수&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lQmsq/btsKQyk4YpM/KBNCSSGt2kswnNkOUWISj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lQmsq/btsKQyk4YpM/KBNCSSGt2kswnNkOUWISj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lQmsq/btsKQyk4YpM/KBNCSSGt2kswnNkOUWISj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlQmsq%2FbtsKQyk4YpM%2FKBNCSSGt2kswnNkOUWISj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;213&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 동작은 간단한데, 다음과 같이 가변 인자로 Job 타입의 객체를 받은 후 Job 객체에 대해 모두 join 함수를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 joinAll의 대상이 된 코루틴들의 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 이미지 2개를 변환한 후 변환된 이미지를 서버에 올려야 하는 상황의 코드이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] job1 is finished&quot;)
    }
    val job2 = launch(Dispatchers.Default) {
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] job2 is finished&quot;)
    }
    joinAll(job1, job2)
    val uploadImageJob: Job = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] image 1,2 upload&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SigBA/btsKSGH7B9I/xhHmIKu9E5ykYYo8O0eHYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SigBA/btsKSGH7B9I/xhHmIKu9E5ykYYo8O0eHYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SigBA/btsKSGH7B9I/xhHmIKu9E5ykYYo8O0eHYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSigBA%2FbtsKSGH7B9I%2FxhHmIKu9E5ykYYo8O0eHYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;124&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 진행을 도식화하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HgK0g/btsKQwnfiaA/rPI1UKt15CMvRtxuN6C2xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HgK0g/btsKQwnfiaA/rPI1UKt15CMvRtxuN6C2xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HgK0g/btsKQwnfiaA/rPI1UKt15CMvRtxuN6C2xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHgK0g%2FbtsKQwnfiaA%2FrPI1UKt15CMvRtxuN6C2xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;360&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 설명하면,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Job1과 Job2는 Dispatchers.Default에 의해 공유 스레드풀의 스레드인 worker-1, worker-2에 각각 할당돼 처리된다.&lt;/li&gt;
&lt;li&gt;joinAll을 통해 Job1, Job2가 완료될 때까지 runBlocking 코루틴(메인 스레드에 속함)은 일시 중단된다.&lt;/li&gt;
&lt;li&gt;Job1, Job2가 모두 완료되면 runBlocking 코루틴이 재개돼 updateImageJob을 실행 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoroutineStart.LAZY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 빌더 함수를 통해 생성하면, 바로 실행이 된다. 만약 코루틴을 생성만 하고 나중에 실행하고 싶을 경우 어떻게 할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 생성된 코루틴을 지연 시작할 수 있는 기능을 제공하고 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main() = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        delay(1000L)
        println(&quot;[${Thread.currentThread().name}] job1 is finished&quot;)
    }
    val lazyJob : Job = launch(start = CoroutineStart.LAZY) {
        println(&quot;[${Thread.currentThread().name}] 지연 실행&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통의 코루틴은 launch 함수를 호출하고, 가용 가능한 스레드가 있다면 곧바로 실행된다. 따라서 Job1은 바로 실행되고, 지연시작을 설정한 lazyZob은 실행되지 않을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbhuYM/btsKSFbyZOi/dQFOmMG5fMCkg6upchjoSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbhuYM/btsKSFbyZOi/dQFOmMG5fMCkg6upchjoSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbhuYM/btsKSFbyZOi/dQFOmMG5fMCkg6upchjoSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbhuYM%2FbtsKSFbyZOi%2FdQFOmMG5fMCkg6upchjoSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;242&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 지연 코루틴은 명시적으로 실행을 요청하지 않으면 실행되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 코루틴을 실행하기 위해서는 Job 객체의 start 함수를 명시적으로 호출해야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val startTime = System.currentTimeMillis()
    val job1 = launch(Dispatchers.Default) {
        println(&quot;[${getElapsedTime(startTime)}] job1 is finished&quot;)
    }
    val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
        println(&quot;[${getElapsedTime(startTime)}] 지연 실행&quot;)
    }
    delay(1000L)
    lazyJob.start()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EIPFq/btsKRQSpKTL/yySli4JUHKu6TzstUGv4l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EIPFq/btsKRQSpKTL/yySli4JUHKu6TzstUGv4l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EIPFq/btsKRQSpKTL/yySli4JUHKu6TzstUGv4l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEIPFq%2FbtsKRQSpKTL%2FyySli4JUHKu6TzstUGv4l0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;169&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job1은 launch 블록을 통해 바로 시작되고, lazyJob은 delay를 지난 후, 명시적인 start 호출을 통해 코루틴이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코루틴 취소하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 취소해야 하는 이유가 뭘까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드를 생성하는 비용은 상당히 비싸다. 따라서 재사용 가능하게 스레드를 활용하게 만드는 방법으로 발전했고, 코루틴은 스레드 안에서 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코루틴이 실행될 필요가 없는데, 계속해서 실행된다면 그만큼 사용 가능한 스레드의 수에서 손해를 보게 되고, 이는 성능 저하로 이어질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이러한 문제를 해결하기 위해 Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cancel&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val startTime = System.currentTimeMillis()
    val job1 = launch(Dispatchers.Default) {
        repeat(5) {
            delay(1000L)
            println(&quot;[${getElapsedTime(startTime)}] 반복횟수 $it&quot;)
        }
    }
    delay(3100L)
    job1.cancel()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brCCjT/btsKQJfFnAP/NdqiI6keMIXW2h1ij3MDb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brCCjT/btsKQJfFnAP/NdqiI6keMIXW2h1ij3MDb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brCCjT/btsKQJfFnAP/NdqiI6keMIXW2h1ij3MDb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrCCjT%2FbtsKQJfFnAP%2FNdqiI6keMIXW2h1ij3MDb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;86&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 1초 대기 후 반복 횟수를 출력하는 작업을 5번 반복하는 코루틴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 3.1초가 지난 후, cancel을 통해서 코루틴이 취소됨을 확인할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cancelAndJoin&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancel 함수를 통해 코루틴을 취소하고, 다른 작업을 실행하면 해당 작업은 코루틴이 취소되기 전에 실행될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말이 조금 어려운데, 코드를 통해 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val startTime = System.currentTimeMillis()
    val job1 = launch(Dispatchers.Default) {
        repeat(5) {
            delay(1000L)
            println(&quot;[${getElapsedTime(startTime)}] 반복횟수 $it&quot;)
        }
    }
    delay(3100L)
    job1.cancel()
    executeJob2()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job1이 끝나고, 이어서 Job2를 실행시키려고 하는 코드이다. 위 코드는 문맥상으로 문제가 없어 보이지만, 순차성 관점에서 중요한 문제점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Job 객체에 cancel을 호출하면 코루틴은 즉시 취소되는 것이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 보충설명하면, 취소 요청을 보내는 것이다. 따라서 cancel 함수를 사용하면 cancel의 대상이 된 Job 객체는 곧바로 취소되는 것이 아니라 미래의 어느 시점에 취소된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Job1이 취소되고, Job2를 실행하려는 함수가 실행되는 것을 보장할 수 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job을 취소하고 바로 함수의 실행을 보장하기 위해선 Job 객체의 cancelAndJoin 함수를 사용하면 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancelAndJoin 함수는 대상이 된 코루틴의 취소가 완료될 때까지 코루틴을 일시 중단시킨다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;fun main(): Unit = runBlocking {
    val startTime = System.currentTimeMillis()
    val job1 = launch(Dispatchers.Default) {
        repeat(5) {
            delay(1000L)
            println(&quot;[${getElapsedTime(startTime)}] 반복횟수 $it&quot;)
        }
    }
    delay(3100L)
    job1.cancelAndJoin()
    executeJob2()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 job1이 취소 완료될 때까지 runBlocking 코루틴이 일시 중단되고, Job2 실행 함수의 호출을 보장할 수 있게 된다.&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>Kotlin</category>
      <category>코루틴</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/503</guid>
      <comments>https://jja2han.tistory.com/503#entry503comment</comments>
      <pubDate>Thu, 21 Nov 2024 21:51:11 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] CoroutineDispatcher(Chapter3)</title>
      <link>https://jja2han.tistory.com/502</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineDispatcher란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스패처는 dispatch와 -er의 합성어로 dispatch의 보내다는 뜻에 -er이 붙어 무언가를 보내는 주체라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;CoroutineDispatcher는 코루틴을 보내는 주체&lt;/b&gt;라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineDispatcher 객체는 스레드로 코루틴을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 &lt;b&gt;일시 중단이 가능한 작업&lt;/b&gt;이기 때문에 스레드가 있어야 실행될 수 있으며, CoroutineDispatcher는 코루틴을 스레드로 보내 실행시키는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineDispatcher는 코루틴을 스레드로 보내는 데 사용할 수 있는 스레드나 스레드풀을 가지며, 코루틴을 실행 요청한 스레드에서 코루틴이 실행되도록 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoroutineDispatcher의 내부 동작&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlZZyy/btsKQyj4i4K/XIj6StAEenxFASafVPPhjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlZZyy/btsKQyj4i4K/XIj6StAEenxFASafVPPhjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlZZyy/btsKQyj4i4K/XIj6StAEenxFASafVPPhjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlZZyy%2FbtsKQyj4i4K%2FXIj6StAEenxFASafVPPhjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;698&quot; height=&quot;419&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineDispatcher 객체는 실행돼야 하는 작업을 저장하는 &lt;b&gt;작업 대기열&lt;/b&gt;을 가지며, CoroutineDispatcher 객체가 사용할 수 있는 스레드풀에는 thread-1, Thread-2라는 2개의 스레드가 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 CoroutineDispatcher 객체에 Coroutine2 코루틴의 실행이 요청되면 CoroutineDispatcher 객체는 &lt;b&gt;실행 요청받은 코루틴을 작업 대기열&lt;/b&gt;에 적재한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/betfDo/btsKQC7x0Fk/XafrNkISHJgtGN8VFShlg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/betfDo/btsKQC7x0Fk/XafrNkISHJgtGN8VFShlg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/betfDo/btsKQC7x0Fk/XafrNkISHJgtGN8VFShlg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbetfDo%2FbtsKQC7x0Fk%2FXafrNkISHJgtGN8VFShlg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;268&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음, CoroutineDispatcher 객체는 자신이 사용할 수 있는 &lt;b&gt;스레드가 있는지 확인&lt;/b&gt;하고, 해당 스레드로 코루틴을 보내 실행시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLqejG/btsKQ9KPBAK/3GHPBtnrDLyvOSJGdVodpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLqejG/btsKQ9KPBAK/3GHPBtnrDLyvOSJGdVodpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLqejG/btsKQ9KPBAK/3GHPBtnrDLyvOSJGdVodpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLqejG%2FbtsKQ9KPBAK%2F3GHPBtnrDLyvOSJGdVodpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;321&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 사용할 수 있는 스레드를 코루틴이 모두 점유하고 있는 경우는 어떻게 동작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드풀과 동일하게, 사용가능한 스레드를 확인하고 없다면 작업대기열에 대기할 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNnhu9/btsKP9SeOYj/NmVTKsbh0IK9VaJyMM9Dv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNnhu9/btsKP9SeOYj/NmVTKsbh0IK9VaJyMM9Dv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNnhu9/btsKP9SeOYj/NmVTKsbh0IK9VaJyMM9Dv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNnhu9%2FbtsKP9SeOYj%2FNmVTKsbh0IK9VaJyMM9Dv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;281&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 코루틴이 스레드로 보내지는 시점은 스레드풀의 스레드 중 하나가 자유로워졌을 때이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1YTfo/btsKO7HPdyW/JBxJZbfVa8QisP6Kzr2Nv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1YTfo/btsKO7HPdyW/JBxJZbfVa8QisP6Kzr2Nv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1YTfo/btsKO7HPdyW/JBxJZbfVa8QisP6Kzr2Nv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1YTfo%2FbtsKO7HPdyW%2FJBxJZbfVa8QisP6Kzr2Nv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;268&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 CoroutineDispatcher 객체는 자신에게 실행 요청된 코루틴을 우선 대기열에 적재한 후 사용할 수 있는 스레드가 생기면 스레드로 보내는 방식으로 시작한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CoroutineDispatcher의 역할&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;CoroutineDispatcher는 코루틴의 실행을 관리하는 주체로 자신에게 실행 요청된 코루틴을
작업 대기열에 적재하고, 자신이 사용할 수 잇는 스레드가 새로운 작업을 실행할 수 있는 상태라면
스레드로 코루틴을 보내 실행될 수 있게 만드는 역할을 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제한된 디스패처와 무제한 디스패처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineDispatcher에는 두 가지 종류가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;제한된 디스패처 : 사용할 수 있는 스레드나 스레드풀이 제한된 디스패처&lt;/li&gt;
&lt;li&gt;무제한 디스패처 : 사용할 수 있는 스레드나 스레드풀이 제한되지 않은 디스패처&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부모 코루틴과 자식 코루틴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 구조화를 제공해 코루틴 내부에서 새로운 코루틴을 실행할 수 있다. 이때 바깥쪽의 코루틴틴을 부모 코루틴이라 하고, 내부에서 생성되는 새로운 코루틴을 자식 코루틴이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화는 코루틴을 계층 관계로 만드는 것뿐만 아니라 부모 코루틴의 실행 환경을 자식 코루틴에 전달하는 데도 사용된다. &lt;br /&gt;&lt;b&gt;만약 자식 코루틴에 Coroutine Dispatcher 객체가 설정되지 않았으면 부모 코루틴의 CourtineDispatcher 객체를 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 정의된 CoroutineDispatcher&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 개발자가 직접 CoroutineDispatcher 객체를 생성하는 문제의 방지를 위해 미리 정의된 CoroutineDispatcher의 목록을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://Dispatchers.IO&quot;&gt;Dispatchers.IO&lt;/a&gt; : 네트워크 요청이나 파일 입출력 등의 입출력(I/0) 작업을 위한 CoroutineDispatcher&lt;/li&gt;
&lt;li&gt;Dispatchers.Default : CPU를 많이 사용하는 연산 작업을 위한 CoroutineDispatcher&lt;/li&gt;
&lt;li&gt;Dispatchers.Main : 메인 스레드를 사용하기 위한 CoroutineDispatcher&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;http://Dispatchers.IO&quot;&gt;Dispatchers.IO&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 스레드 프로그래밍이 가장 많이 사용되는 작업은 &lt;b&gt;입출력(I/O) 작업&lt;/b&gt;이다. 애플리케이션에서는 &lt;b&gt;네트워크 통신&lt;/b&gt;을 위해 HTTP 요청을 하거나&lt;b&gt; DB 작업&lt;/b&gt; 같은 입출력 작업 여러 개를 동시에 수행하므로 이런 요청을 동시에 수행하기 위해서는 많은 스레드가 필요하다. &lt;br /&gt;이를 위해 코루틴 라이브러리에서는 입출력 작업을 위해 미리 정의된 Dispatchers.IO를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dispatcher.IO는 입출력 작업을 위해 사용되는 CoroutineDispatcher 객체이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대로 사용할 수 있는 스레드의 수는 Kotlin(1.7.2)버전을 기준으로 사용 가능한 프로세서의 수와 64중 큰 값&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;{
	launch(Dispatchers.IO){
		
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Disptachers.Default&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대용량 데이터를 처리&lt;/b&gt;해야 하는 작업처럼 &lt;b&gt;CPU 연산&lt;/b&gt;이 필요한 작업(CPU 바운드)이 있다. &lt;br /&gt;이럴 때 사용하는 CoroutineDispatcher가 Dispatchers.Default이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt;{
	launch(Dispatchers.Default){
		
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;입출력 작업 vs 바운드 작업&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주된 차이점은 작업이 실행됐을 때 &lt;b&gt;스레드를 지속적으로 사용하는 지 여부&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 입출력 작업은(네트워크 요청, DB조회 요청) 등을 실행한 후 결과를 반환받을 때까지 스레드를 사용하지 않는다. &lt;br /&gt;반면에 CPU 바운드 작업은 작업을 하는 동안 스레드를 지속적으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 CPU 바운드 작업과 입출력 작업에서의 &lt;b&gt;효율성 차이&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입출력 작업을 코투린을 사용해 실행하면, 입출력 작업 실행 후 스레드가 쉬지 않고 다른 입출력 작업을 수행하는 반면, &lt;br /&gt;CPU 바운드 작업은 코루틴을 사용해 실행하더라도 스레드가 지속적으로 사용되기때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공유 스레드풀을 사용하는 IO와 Default&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 라이브러리는 스레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 &lt;b&gt;공유 스레드풀을 제공&lt;/b&gt;한다. &lt;br /&gt;이 공유 스레드풀은 스레드를 무제한으로 생성할 수 있으며, 관련 API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 스레드풀을 시각화하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WN1TG/btsKPFxmUS6/vGGNoguUQTKPXRBJti2js1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WN1TG/btsKPFxmUS6/vGGNoguUQTKPXRBJti2js1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WN1TG/btsKPFxmUS6/vGGNoguUQTKPXRBJti2js1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWN1TG%2FbtsKPFxmUS6%2FvGGNoguUQTKPXRBJti2js1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;516&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dispatchers.Main&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatchers.Main은 코루틴 라이브러리만 추가하면 사용할 수 있도록 설계된 Dispatchers.IO나 Dispatchers.Default와 다르게 &lt;br /&gt;&lt;b&gt;일반적으로 UI가 있는 애플리케이션에서 메인 스레드의 사용을 위해 사용되는 특별한 CoroutineDispatcher객체이다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CoroutineDispatcher 객체는 코루틴을 스레드로 보내 실행하는 객체이다. 코루틴을 작업 대기열에 적재한 후 사용이 가능한 스레드로 보내 실행한다.&lt;/li&gt;
&lt;li&gt;자식 코루틴은 기본적으로 부모 코루틴의 CoroutineDispatcher 객체를 상속받아 사용한다.&lt;/li&gt;
&lt;li&gt;코루틴 라이브러리는 미리 정의된 CoroutineDispatcher 객체인 &lt;a href=&quot;http://Dispatchers.IO&quot;&gt;Dispatchers.IO&lt;/a&gt;, Dispatchers.Default, Dispatchers.Main을 제공한다.&lt;/li&gt;
&lt;li&gt;Dispatchers.IO는 입출력 작업을 위한 객체로, 네트워크 요청이나 DB 조회, 파일 I/O 등에 사용된다.&lt;/li&gt;
&lt;li&gt;Dispatchers.Default는 CPU 바운드 작업을 위한 객체로 대용량 데이터 처리 등을 하는 데 사용된다.&lt;/li&gt;
&lt;li&gt;Dispatchers.IO와 Dispatchers.Default는 코루틴 라이브러리에서 제공하는 공유 스레드풀을 사용한다.&lt;/li&gt;
&lt;li&gt;Dispatchers.Main은 일반적으로 UI가 있는 애플리케이션에서 UI를 업데이트하는 데 사용된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732109677664&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boWs4x/hyXDgJEkrp/hFAApTgJDGkoVIgcCm8Su0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/W2TQr/hyXzQsjPlV/faefbT6fsJdVZV9dQjPW41/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/fCuA8/hyXDlxqmr8/FQFgvvgkyuFixHpZsiNkF0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boWs4x/hyXDgJEkrp/hFAApTgJDGkoVIgcCm8Su0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/W2TQr/hyXzQsjPlV/faefbT6fsJdVZV9dQjPW41/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/fCuA8/hyXDlxqmr8/FQFgvvgkyuFixHpZsiNkF0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>Kotlin</category>
      <category>코루틴</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/502</guid>
      <comments>https://jja2han.tistory.com/502#entry502comment</comments>
      <pubDate>Wed, 20 Nov 2024 22:36:06 +0900</pubDate>
    </item>
    <item>
      <title>[코루틴의 정석] 스레드 기반 작업의 한계와 코루틴의 등장(Chapter1)</title>
      <link>https://jja2han.tistory.com/501</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일 스레드 애플리케이션의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드는 하나의 작업을 수행할 때 다른 작업을 동시에 수행하지 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드 또한 예외가 아니어서 메인 스레드에서 실행하는 작업이 처리되는 동안 다른 작업 수행 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&amp;gt; 메인 스레드 하나만 사용하는 애플리케이션은 하나의 작업이 오래 걸리면 다른 작업을 전혀 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&amp;gt; 이에 따른 응답성에 문제가 생길 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AODca/btsKNvVfFfI/g9Q8VM8EmKptL7GSdgJO71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AODca/btsKNvVfFfI/g9Q8VM8EmKptL7GSdgJO71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AODca/btsKNvVfFfI/g9Q8VM8EmKptL7GSdgJO71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAODca%2FbtsKNvVfFfI%2Fg9Q8VM8EmKptL7GSdgJO71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;236&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴대폰에서 애플리케이션을 메인 스레드만 사용해 만 들 경 우, 메인 스레드에서 UI 상호작용을 처리하는 작업이 반복된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 네트워크 요청을 하고 응답을 대기하거나 복잡한 연산 작업을 수행하는 오래 걸리는 작업을 하고 있다면, 애플리케이션은 UI를 그리는 작업을 멈추고, 사용자 입력 또한 제대로 전달받지 못하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이는 안드로이드 휴대폰이 버벅이는 주요 이유가 될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W5qrD/btsKMDNrYvZ/bTAAENxBy1blkFfF7Y40Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W5qrD/btsKMDNrYvZ/bTAAENxBy1blkFfF7Y40Zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W5qrD/btsKMDNrYvZ/bTAAENxBy1blkFfF7Y40Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW5qrD%2FbtsKMDNrYvZ%2FbTAAENxBy1blkFfF7Y40Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;698&quot; height=&quot;277&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버도 동일한 예가 있는데, 클라 &amp;rarr; 서버로 오래 걸리는 작업 요청이 들어올 경우 단일 스레드만 사용할 경우 응답속도가 늦어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;순차적인 처리는 당연하게도 응답속도와 처리속도의 지연을 수반한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 스레드만 사용해 작업하면 해야 할 작업이 &lt;b&gt;다른 작업에 의해 방해&lt;/b&gt;받거나 작업 속도가 느려질 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 스레드 프로그래밍을 통한 단일 스레드의 한계 극복&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한것처럼 단일 스레드의 단점은 해야 할 작업이 다른 작업에 의해 방해받는다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점은 멀티 스레드 프로그래밍을 통해 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/COdI1/btsKNxyCy7R/NmPmuLxpVxGkyEFhhNrqD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/COdI1/btsKNxyCy7R/NmPmuLxpVxGkyEFhhNrqD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/COdI1/btsKNxyCy7R/NmPmuLxpVxGkyEFhhNrqD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCOdI1%2FbtsKNxyCy7R%2FNmPmuLxpVxGkyEFhhNrqD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;342&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인스레드가 할 작업을 별도 스레드가 처리할 수 있도록 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 메인 스레드에 오래 걸리는 작업이 요청됐을 때, 오래 걸리는 작업을 백그라운드 스레드에서 처리하도록 만들면 메인 스레드는 UI 작업에 집중할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버도 마찬가지로, 데이터베이스 3개를 여러 백그라운드 스레드에서 조회하고, 결과를 병합해서 반환하면 응답속도를 빠르게 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드, 스레드풀을 사용한 멀티 스레드 프로그래밍&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한계를 극복하기 위해 다양한 방식으로 멀티 스레드 프로그래밍은 변화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코투린 이전에는, 스레드와 스레드풀을 활용했고, 스레드를 직접 다루는 가장 간단한 방법은 &lt;b&gt;Thread&lt;/b&gt; 클래스이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thread 클래스와 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통상적으로 오래 걸리는 작업일 경우 별도 스레드에서 실행되도록 Thread 클래스를 상속하는 클래스를 만들어서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자 스레드와 데몬 스레드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM 프로세스는 일반적으로 메인 스레드의 작업이 종료되면 종료된다.&lt;/li&gt;
&lt;li&gt;메인 스레드는 사용자 스레드이고, 사용자 스레드가 모두 종료되면 프로세스가 종료된다.&lt;/li&gt;
&lt;li&gt;JVM은 스레드를 사용자 스레드와 데몬 스래드로 구분한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 스레드는 우선도가 높은 스레드이고, 데몬 스레드는 우선도가 낮은 스레드이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JVM이 종료되는 시점은 우선도가 높은 사용자 스레드가 모두 종료될 때이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Thread 클래스를 상속해서 만든 Thread 객체는 default 사용자 스레드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;한계 1&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread 클래스를 상속한 클래스를 인스턴스화해 실행할 때마다 매번 새로운 스레드가 생성된다. 스레드는 생성 비용이 비싸기 때문에 매번 새로운 스레드를 생성하는 것은 성능적으로 좋지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;한계2&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드 생성과 관리에 대한 책임이 개발자에게 있다. 따라서 프로그램의 복잡성이 증가하며, 실수로 인해 오류나 메모리 누수(Memory Leak)을 발생시킬 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 한계 덕분에, 한 번 생산한 스레드를 재사용할 수 있어야 하고, 관리할 수 있는 시스템이 있어야 한다. 이러한 역할을 Executor 프레임워크가 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Executor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;등장 배경&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 스레드를 &lt;b&gt;직접 관리하는 문제를 해결&lt;/b&gt;하고 생성된 스레드의 &lt;b&gt;재사용성&lt;/b&gt;을 높이기 위해 등장&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쓰레드 풀&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Executor은 스레드를 생성하고 관리하는데, &lt;b&gt;쓰레드 풀&lt;/b&gt;이라는 개념을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드풀은 스레드와 풀의 합성어인데, 스레드의 집합이라는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰레드 풀을 관리하고 사용자로부터 요청받은 작업을 각 스레드에 할당하는 시스템을 더한 것이 Executor 프레임워크이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 처리를 위해 스레드 풀을 미리 생성하고, 작업을 요청받으면 &lt;b&gt;쉬고 있는 스레드에 작업을 분배&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;각 스레드가 작업을 끝내더라도 스레드를 종료하지 않고 다음 작업이 들어오면&lt;b&gt; 재사용&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 프레임워크가 주는 장점은 스레드에 대한 생성, 관리, 작은 분배와 같은 디테일한 책임을 프레임워크가 담당하게 되므로, 개발자가 더 이상 신경 쓰지 않아도 된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 단순하게, 스레드 풀에 할당할 수 있는 스레드의 개수와 작업을 설정해 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Executor 프레임워크에서 사용자가 사용할 수 있는 함수는 크게 2가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드풀을 생성하고 생성된 스레드풀을 관리하는 객체 반환&lt;/li&gt;
&lt;li&gt;스레드풀을 관리하는 객체에 작업을 제출&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val executorService : ExecutorService = Executors.newFixedThreadPool(2)
// 두개의 스레드를 가진 스레드풀을 생성하고, 관리하는 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submit 함수를 통해 스레드풀에 작업을 제출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;fun main() {
  val startTime = System.currentTimeMillis()
  val executorService: ExecutorService = Executors.newFixedThreadPool(2)

  // 작업1
  executorService.submit {
      println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
      작업1 시작!&quot;)
      Thread.sleep(1000L)
      println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
      작업1 완료!&quot;)
  }
  // 작업2
  executorService.submit {
      println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
      작업2 시작!&quot;)
      Thread.sleep(1000L)
      println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
      작업2 완료!&quot;)
  }

  executorService.shutdown()
}

fun getElapsedTime(startTime: Long): String = 
	&quot;${System.currentTimeMillis() - startTime}ms&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbNv82/btsKOypU9du/y5Bx3SAWqD5n3OpkPIdQVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbNv82/btsKOypU9du/y5Bx3SAWqD5n3OpkPIdQVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbNv82/btsKOypU9du/y5Bx3SAWqD5n3OpkPIdQVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbNv82%2FbtsKOypU9du%2Fy5Bx3SAWqD5n3OpkPIdQVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;294&quot; height=&quot;95&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;executorService, 즉 스레드 풀의 스레드 최대 개수가 2개이므로, 작업 1과 작업 2는 &lt;b&gt;병렬적으로 수행&lt;/b&gt;되어 , 동시에 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 만약 스레드 풀의 최대개수보다 많은 작업이 동시에 할당된다면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도, 스레드 풀의 작업이 끝나고 가용 가능한 스레드가 생기면 작업이 진행될 것이다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;fun main() {
    val startTime = System.currentTimeMillis()
    val executorService: ExecutorService = Executors.newFixedThreadPool(2)

    // 작업1
    executorService.submit {
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업1 시작!&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업1 완료!&quot;)
    }
    // 작업2
    executorService.submit {
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업2 시작!&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업2 완료!&quot;)
    }

    // 작업3
    executorService.submit {
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업3 시작!&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 
        작업3 완료!&quot;)
    }

    executorService.shutdown()
}

fun getElapsedTime(startTime: Long): String = 
	&quot;${System.currentTimeMillis() - startTime}ms&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPP5VH/btsKN5ocdbq/YzjobBxIHtdqJCB090iCZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPP5VH/btsKN5ocdbq/YzjobBxIHtdqJCB090iCZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPP5VH/btsKN5ocdbq/YzjobBxIHtdqJCB090iCZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPP5VH%2FbtsKN5ocdbq%2FYzjobBxIHtdqJCB090iCZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;296&quot; height=&quot;138&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 1과 작업 2는 동시에 실행되지만, 작업 3은 작업 1, 2가 완료된 후에 실행되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 동작하는 이유는 위에서 예측했던 것처럼 2개의 스레드가 이미 작업을 처리 중이기 때문에 &lt;b&gt;가용가능한 스레드가 없어서 그렇다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내부 구조와 동작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEl2x/btsKN5BI5UN/lyRDaTyEYSy88GkzWU89nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEl2x/btsKN5BI5UN/lyRDaTyEYSy88GkzWU89nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEl2x/btsKN5BI5UN/lyRDaTyEYSy88GkzWU89nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEl2x%2FbtsKN5BI5UN%2FlyRDaTyEYSy88GkzWU89nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;427&quot; height=&quot;338&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 부분으로 나뉘는데&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;할당받은 작업을 적재하는 &lt;b&gt;작업 대기열(Blocking Queue)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;작업을 수행하는 스레드의 집합인 &lt;b&gt;스레드 풀&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Executor Service 객체는 사용자로부터 요청받은 작업을 대기열에 적재한 후 쉬고 있는 스레드에 할당하는 역할을 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의의와 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크에게 책임을 맡기고, 개발자가 스레드를 관리하지 않는다는 점에서 혁신적인 프레임워크라고 할 수 있다. (단순하게 스레드 풀의 개수를 지정하고, submit으로 작업을 제출만 하면 된다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 대표적인 문제점이 있는데, 그것이 바로 &lt;b&gt;스레드 블로킹&lt;/b&gt;이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스레드 블로킹&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 아무것도 하지 못하고 사용될 수 없는 상태&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드는 비싼 자원이기 때문에, 사용될 수 없는 상태에 놓이는 것이 반복되면 애플리케이션의 성능이 떨어지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 상황이, 제출한 작업에서 결과를 전달받을 때까지 무한 대기하는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future 객체와 get을 통해서 Executor 객체는 결괏값이 &lt;b&gt;반환될 때까지 스레드를 블로킹&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;이는 성능상 심각한 문제를 초래할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 블로킹 함수가 Thread의 Sleep이다. 일정 시간 대기해야 할 때 매우 유용한 함수이지만, 스레드를 블로킹시킨다는 단점이 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val startTime = System.currentTimeMillis()
    println(&quot;[${getElapsedTime(startTime)}] 메인 스레드 시작!&quot;)
    Thread.sleep(1000L)
    println(&quot;[${getElapsedTime(startTime)}] 메인 스레드 완료!&quot;)
}

fun getElapsedTime(startTime: Long): String = 
	&quot;${System.currentTimeMillis() - startTime}ms&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lkhJl/btsKMD0YATk/CFFMnu2tEMdvYmlkvw1Pn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lkhJl/btsKMD0YATk/CFFMnu2tEMdvYmlkvw1Pn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lkhJl/btsKMD0YATk/CFFMnu2tEMdvYmlkvw1Pn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlkhJl%2FbtsKMD0YATk%2FCFFMnu2tEMdvYmlkvw1Pn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;233&quot; height=&quot;62&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 멀티 스레드 프로그래밍의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드는 생성 비용과 작업을 전환하는 비용이 비싸다. 만약 스레드가 아무 작업을 하지 못하고 기다릴 경우 컴퓨터의 자원이 낭비된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oomQd/btsKOKX3m9V/dpdpw3zj2RNHNEkIUJcgHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oomQd/btsKOKX3m9V/dpdpw3zj2RNHNEkIUJcgHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oomQd/btsKOKX3m9V/dpdpw3zj2RNHNEkIUJcgHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoomQd%2FbtsKOKX3m9V%2Fdpdpw3zj2RNHNEkIUJcgHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;265&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 Thread0이, 작업 1 수행 중에 작업을 마저 수행하려면 Thread-1 스레드에서 실행되는 작업 2의 결과물이 필요한 상황이다. 정말 많은 상황인데, 네트워크 요청을 한 후에 응답을 기다려야 하거나, 복잡한 연산의 결괏값을 반환받을때까지 대기하는 상황에서 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황에서 Thread0은 Thread1의 작업이 완료될 때 까지 &lt;b&gt;아무것도 하지 못하고 대기&lt;/b&gt;한다. 그 시간만큼 Thread0은 사용할 수 없게 되고, 스레드의 비용을 생각하면 매우 치명적인 영향이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피할 수 있는 방법은 다양하지만(콜백, 체이닝 함수) 작업이 많아지고, 작업 간의 종속성이 생긴다는 점에서 그 자체로 한계가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 코루틴은 어떻게? 이러한 한계를 극복할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 &lt;b&gt;작업 단위 코루틴을&lt;/b&gt; 통해 스레드 블로킹 문제를 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 단위 코루틴은 작업 실행 도중 일시 중단할 수 있는 작업 단위이다. 코루틴은 작업이 일시 중단되면 더 이상 스레드 사용이 필요하지 않으므로 스레드의 사용 권한을 양보하며, 양보된 스레드는 다른 작업을 실행하는 데 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴이 &lt;b&gt;경량화된 스레드&lt;/b&gt;라고 불리는 이유가 여기에 있다. 개발자가 코루틴을 만들어 코루틴 스케줄러에 넘기면 코루틴 스케줄러는 자신이 사용할 수 있는 스레드나 스레드 풀에 해당 코루틴을 분배해 작업을 수행한다. 스레드를 사용하던 중에 필요 없게 되면 해당 스레드를 다른 코루틴이 쓸 수 있게 양보할 수 있어서 스레드 블로킹이 일어나지 않게 되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9dpA0/btsKNV7evdA/rAkXkK767W20OHK0In99bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9dpA0/btsKNV7evdA/rAkXkK767W20OHK0In99bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9dpA0/btsKNV7evdA/rAkXkK767W20OHK0In99bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9dpA0%2FbtsKNV7evdA%2FrAkXkK767W20OHK0In99bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;308&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 멀티프로그래밍은 Thread0이 작업 3을 수행하기 위해서는 작업 1이 끝나고 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업1이 끝날 때까지 Thread0을 점유하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코루틴은 자신이 스레드를 사용하지 않을 때 스레드 사용 권한을 반납한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 사용 권한을 반납하면 해당 스레드에서는 다른 코루틴이 실행될 수 있고, 작업 1을 일시 중단하고, 작업3이 실행된다. 이후 작업2가 완료되면 다시 Thread0이 작업1을 할당받아 작업을 시작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 단위로서의 코루틴이 스레드를 사용하지 않을 때, &lt;b&gt;사용 권한을 양보&lt;/b&gt;하는 방식으로 &lt;b&gt;스레드 사용을 최적화&lt;/b&gt;하고 스레드가&lt;b&gt; 블로킹&lt;/b&gt;되는 상황을 &lt;b&gt;방지&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스레드에 비해 생성과 전환에 비용이 적게 들기 때문에, 경제적&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;요약&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM 상에서 실행되는 코틀린 애플리케이션은 실행 시 메인 스레드를 생성하고 메인 스레드를 사용해 코드를 실행한다.&lt;/li&gt;
&lt;li&gt;단일 스레드 애플리케이션은 한 번에 하나의 작업만 수행할 수 있으며, 복잡한 작업이나 네트워크 요청 등이 있으면 응답성이 떨어질 수 있다.&lt;/li&gt;
&lt;li&gt;멀티 스레드 프로그래밍을 사용하면 여러 작업을 동시에 실행할 수 있어서 단일 스레드 프로그래밍의 문제를 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;직접 Thread 클래스를 상속해 스레드를 생성하고 관리할 수 있으나, 생성된 스레드의 재사용이 어려워 리소스의 낭비를 일으킨다.&lt;/li&gt;
&lt;li&gt;Executor 프레임워크는 스레드 풀을 사용해 스레드의 생성과 관리를 최적화하고 스레드 재사용을 용이하게 했다.&lt;/li&gt;
&lt;li&gt;Executor 프레임워크를 비롯한 기존의 멀티 스레드 프로그래밍 방식들은 스레드 블로킹 문제를 근본적으로 해결할 수 없다.&lt;/li&gt;
&lt;li&gt;스레드 블로킹은 스레드가 작업을 기다리면서 리소스를 소비하지만 아무 일도 하지 않는 상태를 말한다.&lt;/li&gt;
&lt;li&gt;코루틴은 스레드 블로킹 문제 해결을 위해 등장했다. 코루틴은 필요할 때 스레드 사용 권한을 양보하고 일시 중단하며, 다른 작업이 스레드를 사용할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;일시 중단 후 재개된 코루틴은 재개 시점에 사용할 수 있는 스레드에 할당돼 실행된다.&lt;/li&gt;
&lt;li&gt;코루틴은 스레드와 비교해 생성과 전환 비용이 적게 들고 스레드에 자유롭게 뗐다 붙였다 할 수 있어 경량화된 스레드라고 불린다.&lt;/li&gt;
&lt;li&gt;코루틴을 사용하면 스레드 블로킹 없이 비동기적으로 작업을 처리할 수 있으며, 이를 통해 애플리케이션의 응답성을 크게 향상할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://product.kyobobook.co.kr/detail/S000212376884&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731942920158&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코틀린 코루틴의 정석 | 조세영 - 교보문고&quot; data-og-description=&quot;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0PQ7S/hyXzUHBGfa/m8RA8rZMVOFykZ3b8jNED0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/bmgpuH/hyXzSDaFRA/kl6EA9gFbswIFAQGPVF2K1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/cj9h1y/hyXzV7IBON/wyquqzfdk1vSNnOT29ZlV0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000212376884&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0PQ7S/hyXzUHBGfa/m8RA8rZMVOFykZ3b8jNED0/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/bmgpuH/hyXzSDaFRA/kl6EA9gFbswIFAQGPVF2K1/img.jpg?width=458&amp;amp;height=573&amp;amp;face=0_0_458_573,https://scrap.kakaocdn.net/dn/cj9h1y/hyXzV7IBON/wyquqzfdk1vSNnOT29ZlV0/img.jpg?width=814&amp;amp;height=3959&amp;amp;face=0_0_814_3959');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 조세영 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 코루틴의 정석 | 많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 이해하기 쉬운 설명을 통해 누구나 쉽게 이해할 수 있도록 설명한다. 안드로이드, 스프링 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>coroutine</category>
      <category>Kotlin</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/501</guid>
      <comments>https://jja2han.tistory.com/501#entry501comment</comments>
      <pubDate>Tue, 19 Nov 2024 00:28:39 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - Interceptor를 이용한 Retrofit 에러 핸들링</title>
      <link>https://jja2han.tistory.com/500</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 프로젝트를 진행할 때마다 네트워크 응답 처리에 대한 구조를 고민하곤 합니다.&amp;nbsp;&lt;br /&gt;어떻게 하면 직관적인 코드로 깔끔하게 처리할지, 다른 사람이 이해하기 쉬울까를 고민하다 보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 프로젝트때마다 다른 에러 핸들링 코드가 만들어지는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 조금 새롭게 Interceptor를 활용한 에러 핸들링 코드를 구현했고, 그에 대한 내용을 작성할까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Interceptor&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YHvXl/btsKcOgw4Yx/ZVylBopH2xB1LPHxKYM5V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHvXl/btsKcOgw4Yx/ZVylBopH2xB1LPHxKYM5V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHvXl/btsKcOgw4Yx/ZVylBopH2xB1LPHxKYM5V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYHvXl%2FbtsKcOgw4Yx%2FZVylBopH2xB1LPHxKYM5V0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;496&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interceptor의 역할은 다양하게 있는데, 주된 역할은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로깅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit을 사용한다면 반드시 들어봤거나, 사용해봤을 것이라 생각합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #a9b7c6;&quot;&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 네트워크 통신에 대한 로그를 확인하기 위해 Interceptor를 추가할 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요청 가로 채기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 요청 가로채기를 위해 Interceptor를 사용하는 경우는 한 가지가 있는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 요청 헤더에 AccessToken을 넣어줄 때 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729332308626&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AccessTokenInterceptor
    @Inject
    constructor(
        private val tokenDataSource: TokenDataSource,
    ) : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val originalRequest = chain.request()
            val requestBuilder = originalRequest.newBuilder()
            val accessToken: String =
                runBlocking {
                    val token: String =
                        tokenDataSource.getAccessToken() ?: run {
                            &quot;&quot;
                        }
                    token
                }
            val request =
                requestBuilder
                    .header(
                        &quot;Authorization&quot;,
                        accessToken,
                    ).build()
            return chain.proceed(request)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Interceptor는 요청을 가로채어, 추가적인 행동을 취할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;응답 가로채기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 글의 목적인 응답 가로채기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답을 가로채어, 내 입맛대로 응답 데이터나 상태를 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api의 리턴 타입을 설정할 때 성공과 실패에 대한 클래스 객체를 둘 다 열어 두는 것보단 하나의 타입으로 고정시키는 것이 처리를 하는데 용이하다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 에러일 경우는 처리해야 할 작업이 꽤 명확하다고 생각했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;에러 메시지 출력&lt;/li&gt;
&lt;li&gt;API 재요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Api의 리턴 타입을 성공과 실패를 담는 상위의 클래스보단, API 통신이 성공할 경우 핸들링하는 것이 깔끔하다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서버와의 공통된 객체인 BaseResponse를 구현하고, API&amp;nbsp; 각 메서드의 반환타입으로 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729332592657&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @GET(&quot;api/user/me&quot;)
    suspend fun loadMyInfo(): BaseResponse&amp;lt;User&amp;gt;

data class BaseResponse&amp;lt;T&amp;gt;(
    val code: String,
    val data: T?,
    val message: String,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이럴 경우 서버에서 BaseResponse가 성공만을 담당한다면 에러가 발생할 경우 어떻게 처리할 것이냐가 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 통신이 실패할 경우 반환되는 타입은 BaseResponse와는 다르기 때문이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729332705682&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ErrorResponse(
    val errorCode: String,
    val message: String,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 에러가 발생할 경우 응답을 가로채 ErrorResponse의 객체로 바꿔주고, 해당 데이터를 내려보내줘야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환타입이 다른데? 어떻게 내려보내줘야 할까에 대해서 고민을 했고, Exception으로 내려줘서 runCatching에서 처리하면 좋지 않을까?&lt;br /&gt;그러면 BaseResponse와도 충돌이 나지 않고, 깔끔하게 처리할 수 있겠다 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Custom Error인 ApiException과, 사용자 인증 오류인 RefreshTokenExpiredException을 구현해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Custom Exception&lt;/h4&gt;
&lt;pre id=&quot;code_1729332906864&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ApiException(
    override val message: String? = null,
    override val cause: Throwable? = null,
    val error: ErrorResponse,
) : IOException(message, cause)

class RefreshTokenExpiredException(
    val error: ErrorResponse,
    override val message: String? = null,
    override val cause: Throwable? = null,
) : IOException(message)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoException을 상속받으면서, 추가로 서버에서 보내주는 Error 타입에 객체를 파라미터로 갖게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이러한 네트워크 오류 처리가 부족해서 IOException으로 처리를 해버렸는데, 잘못된 부분이라고 생각이 듭니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ErrorHandlingInterceptor&lt;/h4&gt;
&lt;pre id=&quot;code_1729332847828&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ErrorHandlingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        try {
            val response = chain.proceed(request)
            if (response.isSuccessful) return response
            val errorBody = response.body?.string() ?: return response
            val errorResponse = Gson().fromJson(errorBody, ErrorResponse::class.java)
            errorResponse?.let {
                if (it.errorCode == &quot;AU004&quot;) {
                    throw RefreshTokenExpiredException(it)
                }
            }
            throw ApiException(error = errorResponse)
        } catch (e: Throwable) {
            when (e) {
                is ApiException -&amp;gt; throw e
                is RefreshTokenExpiredException -&amp;gt; throw e
                is IOException -&amp;gt; throw IOException(e)

                else -&amp;gt; throw e
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 가로채기에서 Interceptor가 하는 일은 꽤 명확한데요,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response의 상태코드가 200, 즉 성공했다면 그대로 응답을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실패했다면, Response의 body를 구현한 ErrorResponse의 객체로 직렬화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 errorCode가 서버에서 지정한 코드(사용자 인증 오류) 일 경우 RefreshTokenExpiredException을 던져줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니라면 통상적인 서버 오류이기에 ApiException에 담아서 던집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ApiResponse&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Api 통신의 결과를 핸들링해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 API의 응답 객체를 만들어 성공과 실패를 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패 안에서는 확장에 용이하게 에러를 구분했지만, 크게 사용하는 일은 없었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729334012922&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class ApiResponse&amp;lt;out D&amp;gt; {
    data class Success&amp;lt;out D&amp;gt;(
        val data: D,
    ) : ApiResponse&amp;lt;D&amp;gt;()

    sealed class Error(
        open val errorCode: String = &quot;&quot;,
        open val errorMessage: String = &quot;&quot;,
    ) : ApiResponse&amp;lt;Nothing&amp;gt;() {
        data class ServerError(
            override val errorCode: String,
            override val errorMessage: String,
        ) : Error(errorCode, errorMessage)

        data class TokenError(
            override val errorCode: String,
            override val errorMessage: String,
        ) : Error(errorCode, errorMessage)

        data class NetworkError(
            override val errorMessage: String,
        ) : Error(errorMessage = errorMessage)

        data class UnknownError(
            override val errorMessage: String,
        ) : Error(errorMessage = errorMessage)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공일 경우는 반환타입인 data만을, 실패일 경우는 에서 메시지와 에러 코드를 가지는 클래스를 에러 클래스를 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;emitApiResponse&lt;/h4&gt;
&lt;pre id=&quot;code_1729333959216&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun &amp;lt;T&amp;gt; emitApiResponse(
    apiResponse: suspend () -&amp;gt; BaseResponse&amp;lt;T&amp;gt;,
    default: T,
): ApiResponse&amp;lt;T&amp;gt; =
    runCatching {
        apiResponse()
    }.fold(
        onSuccess = { result -&amp;gt;
            ApiResponse.Success(data = result.data ?: default)
        },
        onFailure = { e -&amp;gt;
            when (e) {
                is ApiException -&amp;gt;
                    ApiResponse.Error.ServerError(
                        errorCode = e.error.errorCode,
                        errorMessage = e.error.message,
                    )

                is RefreshTokenExpiredException -&amp;gt; {
                    ApiResponse.Error.TokenError(
                        errorMessage = e.error.message,
                        errorCode = e.error.errorCode,
                    )
                }

                is IOException -&amp;gt;
                    ApiResponse.Error.NetworkError(
                        errorMessage = e.message ?: &quot;&quot;,
                    )

                else -&amp;gt;
                    ApiResponse.Error.UnknownError(
                        errorMessage = e.message ?: &quot;&quot;,
                    )
            }
        },
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api의 response를 핸들링하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fold를 통해서 성공과 실패 블록을 구분하고,&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공한 경우는 ApiResponse.Success에 data를 담아서 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 서버 통신에서 성공했지만, body가 null인 경우도 있어서 data 타입을 nullable로 지정하는 것보단 default 객체를 파라미터로 받아서 null 일경우 default 객체를 반환하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패한 경우는 에러 타입에 따라 구분해서 반환해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용법은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729334656148&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override suspend fun publishNft(request: PublishNftRequest): Flow&amp;lt;ApiResponse&amp;lt;Nft&amp;gt;&amp;gt; =
    flow {
        val response =
            emitApiResponse(apiResponse = { api.publishNft(request) }, default = Nft())
        emit(response)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;emitApiResponse의 반환타입이 ApiResponse이기에 해당 반환값을 emit 해주면 끝입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 추후 반환값을 전부 사용하지 않는 통신이 발생했고, 변환하는 작업이 필요해서,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729334730083&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override suspend fun getUserNft(userId: Long): Flow&amp;lt;ApiResponse&amp;lt;List&amp;lt;MyFrame&amp;gt;&amp;gt;&amp;gt; =
    flow {
        val response =
            emitApiResponse(
                apiResponse = { api.getUserNft(userId) },
                default = GetMyFrameResponse(),
            )
        when (response) {
            is ApiResponse.Error -&amp;gt; emit(response)
            is ApiResponse.Success -&amp;gt; emit(ApiResponse.Success(response.data.content))
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 와서 생각해 보면 mapper까지 넘겨줘서, mapper로 변환해서 깔끔하게 처리하는 것이 좋았다고 생각이 듭니다..ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>interceptor</category>
      <category>에러 처리</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/500</guid>
      <comments>https://jja2han.tistory.com/500#entry500comment</comments>
      <pubDate>Sat, 19 Oct 2024 19:47:24 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - Authenticator를 활용해 JwtToken 갱신하기</title>
      <link>https://jja2han.tistory.com/499</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹개발이나, 클라이언트에서 사용자 인증을 위해서 JwtToken을 도입하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서도 JwtToken을 기존에 처리했었는데, 이번 프로젝트에서 &lt;b&gt;Authenticator를&lt;/b&gt; 활용해서 처리해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 대한 내용을 포스팅할까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Authenticator&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허락되지 않은 사용자, 인증되지 않은 사용자일 경우 HTTP 상태코드 중 &lt;b&gt;401&lt;/b&gt;을 받게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blhJvi/btsJ3jhhkCQ/7X4ABMNPNKiqAfw1FKxqO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blhJvi/btsJ3jhhkCQ/7X4ABMNPNKiqAfw1FKxqO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blhJvi/btsJ3jhhkCQ/7X4ABMNPNKiqAfw1FKxqO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblhJvi%2FbtsJ3jhhkCQ%2F7X4ABMNPNKiqAfw1FKxqO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;289&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 401 에러를 받을 경우 갱신 요청을 보내는 로직을 작성했다면, 네트워크 통신 단계에서 &lt;b&gt;401에러&lt;/b&gt;를 검증하고 작성한 로직을 실행시킬 수 있는 Authenticator에 존재를 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 이 전에 몰랐던 것이 말이 안 된다..라고 생각이 들긴 합니다)&lt;/p&gt;
&lt;pre id=&quot;code_1728721465196&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class JwtAuthenticator
    @Inject
    constructor(
        private val tokenDataSource: TokenDataSource,
        private val authApi: AuthApi,
    ) : Authenticator {
        override fun authenticate(
            route: Route?,
            response: Response,
        ): Request? {
            val request = response.request
            if (request.header(&quot;Authorization&quot;).isNullOrEmpty()) {
                return null
            }
            val refreshToken =
                runBlocking {
                    tokenDataSource.getRefreshToken()
                }
            return try {
                val newJwtToken: JwtToken? =
                    runBlocking {
                        val result =
                            authApi.getNewToken(
                                refreshToken ?: &quot;&quot;,
                            )
                        result.data
                    }
                if (newJwtToken == null) return null
                runBlocking { tokenDataSource.saveJwtToken(newJwtToken) }

                request
                    .newBuilder()
                    .removeHeader(&quot;Authorization&quot;)
                    .addHeader(&quot;Authorization&quot;, newJwtToken.accessToken)
                    .build()
            } catch (e: Throwable) {
                when (e) {
                    is ApiException -&amp;gt; throw e
                    is RefreshTokenExpiredException -&amp;gt; throw e
                    is IOException -&amp;gt; throw IOException(e)

                    else -&amp;gt; throw e
                }
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Authenticator를 401 에러가 발생할 경우 실행되기 때문에, 그에 맞는 동작을 구현하려고 노력했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TokenDataSource에 있는 JwtToken을 활용해서 서버로 토큰 갱신을 처리하는 로직인데요,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음으로 request header에 AccessToken이 없다면 넘어갑니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Header에 AccessToken 정보가 있을 경우에는 저장된 refreshToken을 가져옵니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 과정에서 RunBlocking이 사용된 이유는 동기적으로 진행되어야 하기 때문에 토큰을 가져오는 로직을 진행한 후&lt;br /&gt;아래 코드가 실행되도록 의도했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 가져온 JwtToken의 RefreshToken을 바탕으로 서버에 갱신 요청을 하고, 해당 토큰을 저장한 후 기존의 헤더를 지우고, 새로운 헤더를 만들어 AccessToken을 담아냅니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구현한 Authenticator를 Retrofit에 추가해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ii1io/btsJ4M3oMl8/IEXNLsCyGrf3S3sl8qvcyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ii1io/btsJ4M3oMl8/IEXNLsCyGrf3S3sl8qvcyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ii1io/btsJ4M3oMl8/IEXNLsCyGrf3S3sl8qvcyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIi1io%2FbtsJ4M3oMl8%2FIEXNLsCyGrf3S3sl8qvcyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1392&quot; height=&quot;678&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 사이클 형성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 코드를 빌드하면 오류가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p4vJK/btsJ4hQeaiY/Sl9SFtvERvuNThCbebRJKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p4vJK/btsJ4hQeaiY/Sl9SFtvERvuNThCbebRJKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4vJK/btsJ4hQeaiY/Sl9SFtvERvuNThCbebRJKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp4vJK%2FbtsJ4hQeaiY%2FSl9SFtvERvuNThCbebRJKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;258&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 내용은 정말 길지만, 간단하게 요약하면 의존성 주입에 대한 사이클이 발생했다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구현한 JwtAuthenticator는 AuthApi를 주입받고 있는데, AuthApi를 구현하기 위해 사용될 Retrofit에서 다시 OkHttpClient를 주입받고 있기 때문에 의존성 사이클이 발생한 것이 원인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JwtAuthenticator -&amp;gt; AuthApi -&amp;gt; OkHttpClient -&amp;gt; JwtAuthenticator라는 사이클을 형성하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 서버와의 통신에서 AccessToken을 필요로 하지 않는 API가 존재하기 때문에 해당 부분을 덜어낼 새로운 OkHttpClient 객체를 만들어 분리를 해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 AuthAPI를 사용할 OkHttpClient객체와 사용하지 않을 OkHttpClient를 분리해서 해당 OkHttpClient에는 Authenticator를 주입하지 않게 하는 것이 해결방법이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKoF4v/btsJ4ooaRAR/3FVX1P4RHLyH2RkMoFbsck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKoF4v/btsJ4ooaRAR/3FVX1P4RHLyH2RkMoFbsck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKoF4v/btsJ4ooaRAR/3FVX1P4RHLyH2RkMoFbsck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKoF4v%2FbtsJ4ooaRAR%2F3FVX1P4RHLyH2RkMoFbsck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;436&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설계를 통해 JwtToken을 갱신하는 Authenticator와 AuthApi를 분리할 수 있었고, 의존성 사이클을 해소할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Retrofit이 OkHttpClient 객체를 구분할 수 있어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIE3Ah/btsJ4tXcwAb/bqvbwecR9EqX6vEPrU9m2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIE3Ah/btsJ4tXcwAb/bqvbwecR9EqX6vEPrU9m2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIE3Ah/btsJ4tXcwAb/bqvbwecR9EqX6vEPrU9m2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIE3Ah%2FbtsJ4tXcwAb%2FbqvbwecR9EqX6vEPrU9m2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;330&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 @Qualifer를 이용해&amp;nbsp; AuthApi를 사용하는 객체와 아닌 객체를 분리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 OkHttpClient는 다음과 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diyGMQ/btsJ4JyQ5P4/qr99BTgCwzEOiqh6QBBIpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diyGMQ/btsJ4JyQ5P4/qr99BTgCwzEOiqh6QBBIpk/img.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;766&quot; data-is-animation=&quot;false&quot; style=&quot;width: 42.8343%; margin-right: 10px;&quot; data-widthpercent=&quot;43.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diyGMQ/btsJ4JyQ5P4/qr99BTgCwzEOiqh6QBBIpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiyGMQ%2FbtsJ4JyQ5P4%2Fqr99BTgCwzEOiqh6QBBIpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1402&quot; height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzLTj0/btsJ24xUZsy/JpuroqtuWCpa77FuAI4Dak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzLTj0/btsJ24xUZsy/JpuroqtuWCpa77FuAI4Dak/img.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;626&quot; data-is-animation=&quot;false&quot; style=&quot;width: 56.0029%;&quot; data-widthpercent=&quot;56.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzLTj0/btsJ24xUZsy/JpuroqtuWCpa77FuAI4Dak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzLTj0%2FbtsJ24xUZsy%2FJpuroqtuWCpa77FuAI4Dak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1498&quot; height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 설계도대로, BaseClient는 JwtAuthenticator를 주입하고, AuthClient는 주입하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 ApiModule에는&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcRrvA/btsJ4KYOxqx/GJWW652kQvkrrs5m3uk3k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcRrvA/btsJ4KYOxqx/GJWW652kQvkrrs5m3uk3k0/img.png&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;240&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.5771%; margin-right: 10px;&quot; data-widthpercent=&quot;51.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcRrvA/btsJ4KYOxqx/GJWW652kQvkrrs5m3uk3k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcRrvA%2FbtsJ4KYOxqx%2FGJWW652kQvkrrs5m3uk3k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpEoHF/btsJ3RkmXJo/xczm8f5xvz7D6N2XGwehKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpEoHF/btsJ3RkmXJo/xczm8f5xvz7D6N2XGwehKk/img.png&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;238&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.2601%;&quot; data-widthpercent=&quot;48.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpEoHF/btsJ3RkmXJo/xczm8f5xvz7D6N2XGwehKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpEoHF%2FbtsJ3RkmXJo%2Fxczm8f5xvz7D6N2XGwehKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;880&quot; height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthApi에는 AuthClient를, 다른 서버 통신에는 BaseClient를 주입하면 해결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 Authenticator라는 존재를 알게 되었고 활용해 봤습니다. 하지만 조사해 본 결과 401 에러 외에도 407 에러를 받을 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authenticator가 실행된다고 하더라고요. 그리고 이번처럼 서버와 인증되지 않은 사용자일 경우 401 에러를 주지 않을 경우는 해당 코드의 도입을 고려해봐야 할 것 같다는 생각이 들었습니다.. 그래서 몇몇의 코드는 Authenticator가 아닌 Interceptor에서 처리를 하는 경우도 있는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;figure id=&quot;og_1728723276522&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;안드로이드 만료 토큰 갱신 / 요청 api에 토큰 삽입 자동화 시스템 개발기 - 3 Interceptor에서 Token 삽&quot; data-og-description=&quot;이번 편은 Retrofit Interface에서 Token Parameter를 없앤 방법에 대해서 이야기할 것이다. 먼저, 사내 인증 시스템에서는 토큰을 이중으로 발급 받아야하는 상황이라고 적었었다. 라마인드 하자면, 아래&quot; data-og-host=&quot;modelmaker.tistory.com&quot; data-og-source-url=&quot;https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%A7%8C%EB%A3%8C-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0-%EC%9A%94%EC%B2%AD-api%EC%97%90-%ED%86%A0%ED%81%B0-%EC%82%BD%EC%9E%85-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3-Interceptor%EC%97%90%EC%84%9C-Token-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%ED%8E%B8-Android-Retrofit-Auto-Insert-Token-Auto-Refresh-Token&quot; data-og-url=&quot;https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%A7%8C%EB%A3%8C-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0-%EC%9A%94%EC%B2%AD-api%EC%97%90-%ED%86%A0%ED%81%B0-%EC%82%BD%EC%9E%85-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3-Interceptor%EC%97%90%EC%84%9C-Token-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%ED%8E%B8-Android-Retrofit-Auto-Insert-Token-Auto-Refresh-Token&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dqQZiX/hyXhP6Y8la/7DugARGpgJpaUDkA2OnAK1/img.png?width=800&amp;amp;height=733&amp;amp;face=0_0_800_733,https://scrap.kakaocdn.net/dn/c1SplF/hyXeaycrj9/R1daRpdflfKkBN880Hg5f0/img.png?width=800&amp;amp;height=733&amp;amp;face=0_0_800_733,https://scrap.kakaocdn.net/dn/ciL1zl/hyXd7IeVcx/byzJ4eOGB1ksBZkxwOZK2k/img.png?width=1431&amp;amp;height=1022&amp;amp;face=0_0_1431_1022&quot;&gt;&lt;a href=&quot;https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%A7%8C%EB%A3%8C-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0-%EC%9A%94%EC%B2%AD-api%EC%97%90-%ED%86%A0%ED%81%B0-%EC%82%BD%EC%9E%85-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3-Interceptor%EC%97%90%EC%84%9C-Token-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%ED%8E%B8-Android-Retrofit-Auto-Insert-Token-Auto-Refresh-Token&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%A7%8C%EB%A3%8C-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0-%EC%9A%94%EC%B2%AD-api%EC%97%90-%ED%86%A0%ED%81%B0-%EC%82%BD%EC%9E%85-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3-Interceptor%EC%97%90%EC%84%9C-Token-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%ED%8E%B8-Android-Retrofit-Auto-Insert-Token-Auto-Refresh-Token&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dqQZiX/hyXhP6Y8la/7DugARGpgJpaUDkA2OnAK1/img.png?width=800&amp;amp;height=733&amp;amp;face=0_0_800_733,https://scrap.kakaocdn.net/dn/c1SplF/hyXeaycrj9/R1daRpdflfKkBN880Hg5f0/img.png?width=800&amp;amp;height=733&amp;amp;face=0_0_800_733,https://scrap.kakaocdn.net/dn/ciL1zl/hyXd7IeVcx/byzJ4eOGB1ksBZkxwOZK2k/img.png?width=1431&amp;amp;height=1022&amp;amp;face=0_0_1431_1022');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 만료 토큰 갱신 / 요청 api에 토큰 삽입 자동화 시스템 개발기 - 3 Interceptor에서 Token 삽&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 편은 Retrofit Interface에서 Token Parameter를 없앤 방법에 대해서 이야기할 것이다. 먼저, 사내 인증 시스템에서는 토큰을 이중으로 발급 받아야하는 상황이라고 적었었다. 라마인드 하자면, 아래&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;modelmaker.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1728723334534&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Retrofit2를 사용해 JWT 토큰 인증하기 -2&quot; data-og-description=&quot;레트로핏은 안드로이드에서 서버와 REST API 통신을 위해 주로 사용되는 라이브러리이다. OkHttp를 기반으로 동작하며 높은 성능과 뛰어난 가독성, 쉬운 유지보수 등의 이유로 가장 많이 사용되는 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@trasalby/Retrofit2%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-1&quot; data-og-url=&quot;https://velog.io/@trasalby/Retrofit2를-사용해-JWT-토큰-인증하기-1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8yza3/hyXhUAsq4M/Q38cK6uGc1XLKOopY8mpdk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@trasalby/Retrofit2%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@trasalby/Retrofit2%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8yza3/hyXhUAsq4M/Q38cK6uGc1XLKOopY8mpdk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Retrofit2를 사용해 JWT 토큰 인증하기 -2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;레트로핏은 안드로이드에서 서버와 REST API 통신을 위해 주로 사용되는 라이브러리이다. OkHttp를 기반으로 동작하며 높은 성능과 뛰어난 가독성, 쉬운 유지보수 등의 이유로 가장 많이 사용되는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Authenticator</category>
      <category>jwttoken</category>
      <category>RETROFIT</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/499</guid>
      <comments>https://jja2han.tistory.com/499#entry499comment</comments>
      <pubDate>Sat, 12 Oct 2024 17:54:01 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - Test 코드를 작성해보자(Compose UI Test)</title>
      <link>https://jja2han.tistory.com/498</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 마무리 될 즈음 당연하게 거쳐야 햘 과정이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것은 바로 기능들을 검증하는것이겠지요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문적인 용어로는 &lt;b&gt;QA(Quality Assuarance)&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안의 프로젝트에서는 QA 과정이 없거나, 아주 간단하게 진행했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvOINL/btsJEpO89R7/la25zABITIazgVJn5wkVfk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvOINL/btsJEpO89R7/la25zABITIazgVJn5wkVfk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvOINL/btsJEpO89R7/la25zABITIazgVJn5wkVfk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvOINL%2FbtsJEpO89R7%2Fla25zABITIazgVJn5wkVfk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;274&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 개발과 문서화를 하기에도 시간이 벅차기 때문인데요,&lt;br /&gt;하지만 좋은 프로젝트는 유저가 사용하기 이전에 서비스가 안전한지, 개발자의 의도대로 동작하는 지 검증하는것이 당연히 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4LvKu/btsJD7gV2rg/6tb98qcjkFFFzPjiuk4at1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4LvKu/btsJD7gV2rg/6tb98qcjkFFFzPjiuk4at1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4LvKu/btsJD7gV2rg/6tb98qcjkFFFzPjiuk4at1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4LvKu%2FbtsJD7gV2rg%2F6tb98qcjkFFFzPjiuk4at1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;437&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 진행했던 프로젝트에서 테스트 배포 후 받았던 피드백 내용들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 외적으로 개발단계에서 생각치 못한 문제들이 많이 발생하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpN5HI/btsJE9SrBkv/DSMcVwjduvSImSCRR0fpoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpN5HI/btsJE9SrBkv/DSMcVwjduvSImSCRR0fpoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpN5HI/btsJE9SrBkv/DSMcVwjduvSImSCRR0fpoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpN5HI%2FbtsJE9SrBkv%2FDSMcVwjduvSImSCRR0fpoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;199&quot; height=&quot;339&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 위 화면처럼 말도 안되는 오류가 생기기도 하구요..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트 마무리 단계에서 저희는 QA를 도입해보기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;1736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRjCcG/btsJD63p1FI/bwhuAUKbqcFagRPS65J3jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRjCcG/btsJD63p1FI/bwhuAUKbqcFagRPS65J3jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRjCcG/btsJD63p1FI/bwhuAUKbqcFagRPS65J3jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRjCcG%2FbtsJD63p1FI%2FbwhuAUKbqcFagRPS65J3jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;610&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;1736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 화면으로 나누고, 검증할 내용과 선행조건을 입력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증해야할 기능과 화면을 적고 직접 앱을 사용하면서 진행하다가 문득, 손으로 하는것이 다른사람들에게 와닿을까..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 검증하는것이 도움이 될까라는 궁금증이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코드로 검증을 하면 좋지 않을까..? 더 나아가 자동화도 할 수 있으면 좋을것 같다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Test Code&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직무역량이나 우대사항에서 Test Code 작성 경험이라는 말을 보면 가슴이 답답해지고 막막해졌던 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 취준생의 프로젝트에서 Test Code를 작성하고 도입하는것이 배보다 배꼽이 더 크다라고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 (살짝) 그렇게 생각하지만, 한번 작성해보면 좋지 않을까? 싶어서 QA와 Test Code를 접목해서 도입해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 제가 생각한 Test Code를 작성하면 좋은점은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작성한 코드가 의도대로 작동하는지 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;리팩토링 시 동일한 동작을 하는지 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;테스트 코드를 통해서 내가 작성한 코드의 의도와 결과를 다른 사람이 확인하기 편하다.&lt;br /&gt;즉 문서화의 역할을 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test Code의 종류는 3가지가 있는데, 제가 작성한 테스트 코드는 Unit Test와 UI Test입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unit Test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unit Test는 사전적으로 단위테스트라고도 불리며, 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 설명이 생소할 수 있는데, 모듈의 의미는 하나의 기능 or 함수로 이해하면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예로는 로그인 과정에서 유효성 검증입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각하고 작성한 Unit Test의 Flow는 &quot;어떤 기능이 실행되면 어떤 결과가 나와야 한다! 그리고 그것을 검증한다&quot;로 이해했고, 그렇게 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UI Test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI Test는 실제 사용자들이 사용하는 화면에 대한 테스트를 하면서 의도대로 서비스가 동작하는지 검증하는 테스트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI Test의 경우 화면가 직접적으로 연관이 되어있기 때문에 화면(UI)이 변경될 경우 영향을 받기 때문에 유지 보수 비용이 큰 작업이긴 합니다. 하지만 반드시 필요한 작업이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로젝트에서 내가 작성한 테스트 코드를 살펴봐야하는데요,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byqtGP/btsJFwzSmDT/V6EKEbIQy1J6qBEldIEfqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byqtGP/btsJFwzSmDT/V6EKEbIQy1J6qBEldIEfqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byqtGP/btsJFwzSmDT/V6EKEbIQy1J6qBEldIEfqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyqtGP%2FbtsJFwzSmDT%2FV6EKEbIQy1J6qBEldIEfqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2014&quot; height=&quot;608&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 QA를 바탕으로 테스트 코드를 살펴보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Android Studio에서 사용할 수 있는 테스트코드 라이브러리는 여러개 있지만 저는 Junit4와 Compose UI Test를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Junit4는 한글 사용이 까다로워서 5를 사용하고 싶었지만, 적용하는 과정이 너무 복잡하고 대부분의 코드가 Junit4로 구성되어 있기에 저도 버전 4로 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose UI Test는 제가 XML이 아닌 Compose를 사용했기 때문에 사용해야했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ww7NH/btsJD8UsRVR/zvvKxnkcpf9hTWnCJTTq8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ww7NH/btsJD8UsRVR/zvvKxnkcpf9hTWnCJTTq8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ww7NH/btsJD8UsRVR/zvvKxnkcpf9hTWnCJTTq8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fww7NH%2FbtsJD8UsRVR%2FzvvKxnkcpf9hTWnCJTTq8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;409&quot; height=&quot;524&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 Unit 테스트 코드가 있는 패키지 구조입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data : 상황별로 쓰일 Data가 있고, 검증하기 위해 사용됩니다.&lt;/li&gt;
&lt;li&gt;xxViewModelTest : ViewModel에서 사용될 로직을 테스트하는 코드들입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726573960262&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object NickNameTestData {
    const val DUPLICATED_NICKNAME = &quot;짜이한&quot;
    const val NO_DUPLICATED_NICKNAME = &quot;짜이한한한&quot;
    const val TOO_SHORT_NICKNAME = &quot;한&quot;
    const val TOO_LONG_NICKNAME = &quot;가나다라마바사아자카타파하라&quot;
    const val INCLUDE_SPACE_NICKNAME = &quot;짜이한 &quot;
    const val INCLUDE_NUMBER_NICKNAME = &quot;짜이한1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입에서 닉네임을 입력하는 로직을 검증하는데 사용될 데이터로, 각 오류에 해당되는 예시 데이터들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복된 닉네임이거나, 길이가 길거나 짥거나, 공백이 있거나 숫자가 있는 등 정규식에서 사용될 데이터라고 생각하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 Data들은 로직을 검증하기 위해 사용될 데이터로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는&amp;nbsp; Given - When - Then으로 구조화하는것이 보편적입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Given : 테스트 실행을 준비하는 단계로, 테스트에 필요한 데이터를 세팅하는 단계입니다.&lt;/li&gt;
&lt;li&gt;when : 테스트를 실행하는 단계로, 검증하고 싶은 기능을 실제로 수행하는 단계입니다.&lt;/li&gt;
&lt;li&gt;then : 테스트 결과를 검증하는 단계로, 예상 결과 실제 결과를 비교하는 단계입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Unit Test 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조를 대입해보면 아래와 같은 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726575625619&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun `닉네임이_중복되었다면_UiState를_Error로_업데이트_한다`() = runTest {
    // Given: 중복된 닉네임을 입력하고, 저장소에서 중복된 닉네임으로 에러를 반환하도록 설정
    val nickName = DUPLICATED_NICKNAME
    coEvery { userRepository.checkNickName(nickName) } returns flowOf(
        ApiResponse.Error(
            errorMessage = ErrorMessage.DUPLICATED_NICKNAME
        )
    )
    viewModel.nicknameState.updateNickname(nickName)

    // When: 닉네임 중복 검사를 실행
    viewModel.checkNickName()

    // Then: UiState가 에러 상태로 업데이트되었는지 확인
    assertEquals(
        NickNameUiState.Error(errorMessage = ErrorMessage.DUPLICATED_NICKNAME),
        viewModel.uiState.value
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 구조별로 하나하나 뜯어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Given&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unit Test의 검증은 실세 서버와의 통신을 동작하는 것이 아닌 제가 작성한 메서드를 검증하는것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 검증하고 싶은 내용이 중복된 닉네임을 중복검사했을 경우, 원하는 결과를 얻을 수 있는지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 테스트에서 사용될 nickName을 중복된 닉네임으로 설정하고, coEvery를 통해 해당 함수의 반환값을 설정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복된 닉네임 -&amp;gt; 오류를 반환해야하므로, 저는 Error 타입을 반환하게했고, 메시지는 중복된 닉네임이 발생할 경우로 설정해줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;When&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 동작을 수행하는 단계로, 저는 checkNickName()을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Then&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 검증하는 단계로 저는 ViewModel에서 사용되는 UiState를 비교했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류일 경우 저는 viewModel의 uiState가 errorMessage가 DUPLICATED_NICKNAME인지 검증했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chfOwY/btsJD7gWRcU/zP1kkq8USLeIAmo5VtqTn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chfOwY/btsJD7gWRcU/zP1kkq8USLeIAmo5VtqTn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chfOwY/btsJD7gWRcU/zP1kkq8USLeIAmo5VtqTn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchfOwY%2FbtsJD7gWRcU%2FzP1kkq8USLeIAmo5VtqTn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;127&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 PASS입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 검증하고자 하는 값과 viewModel에서의 동작 값이 같음을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;1132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOt4Va/btsJDhx3QMt/0fTEgOHPEQTjCqxmC7e4BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOt4Va/btsJDhx3QMt/0fTEgOHPEQTjCqxmC7e4BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOt4Va/btsJDhx3QMt/0fTEgOHPEQTjCqxmC7e4BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOt4Va%2FbtsJDhx3QMt%2F0fTEgOHPEQTjCqxmC7e4BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;570&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;1132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 위와 같은 테스트 코드를 작성했고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnd2He/btsJEHvmQAo/GSQ0r2DFgbIttOMzgWIGJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnd2He/btsJEHvmQAo/GSQ0r2DFgbIttOMzgWIGJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnd2He/btsJEHvmQAo/GSQ0r2DFgbIttOMzgWIGJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnd2He%2FbtsJEHvmQAo%2FGSQ0r2DFgbIttOMzgWIGJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;130&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 커버리지 결과를 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI Test 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI Test도 Unit Test와 동일하게 given - when - then 구조를 가져갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Compose UI Test를 도입했는데요, 보편적인 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726576888575&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `function_A`() {
    	// given : 데이터 설정, 결과값 지정
        composeTestRule.setContent {
        	AScreen()
        }
        // when : 동작 수행(버튼 클릭 등)
        composeTestRule.onNodeWithTag(&quot;clear_button&quot;).performClick()
        // then : 결과 검증
        assertEquals(
            testData,
            AScreen.data,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@get: Rule은 Junit에서 테스트 환경을 설정하는 어노테이션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Compose 테스트 환경을 설정하기 위해 createComposeRule을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;function에서는 composTestRule.setContent를 통해서 화면을 Compose 환경에 렌더링하고, 수행할 동작을 수행 후 결과를 검증하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 제가 닉네임을 입력하는 화면에서 작성한 테스트 코드를 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAe3SC/btsJEFEhAZL/dxM715CzqJCntYQCR489Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAe3SC/btsJEFEhAZL/dxM715CzqJCntYQCR489Xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAe3SC/btsJEFEhAZL/dxM715CzqJCntYQCR489Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAe3SC%2FbtsJEFEhAZL%2FdxM715CzqJCntYQCR489Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;364&quot; height=&quot;477&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 x버튼을 누를 경우 텍스트 필드에 있는 텍스트 값들이 모두 지워는것을 원하고, 이를 검증해볼것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 x 버튼을 클릭을 해야하는데요, 테스트 환경에서 x 버튼에 접근하기 위해서 x버튼을 인식할 수 있는 인식표를 넣어줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 xml에서도 contentDescription을 입력하라고 권유되는 문구를 보셨을텐데요, 이를 이용해서 x 버튼에 이름을 달아주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지 방식을 사용할 수 있는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 contentDecription을 통해서 지정해주는것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726578062858&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Image(
    modifier = Modifier.size(200.dp),
    painter = painterResource(id = resId),
    contentDescription = &quot;user_img&quot;,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 해당 요소를 찾기 위해서는 테스트 코드에서 아래 코드를 통해서 요소를 인식할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726578092050&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;composeTestRule.onNodeWithContentDescription(&quot;user_img&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째는 Modifier에 testTag를 달아주는것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726578031909&quot; class=&quot;lisp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;IconButton(
    modifier = Modifier.testTag(&quot;clear_button&quot;),
    onClick = onClearPressed
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 해당 요소를 찾기 위해서 테스트 코드에서 아래 코드를 통해서 요소를 인식할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726578161850&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;composeTestRule.onNodeWithTag(&quot;clear_button&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성된 테스트 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726578264608&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun `전체_지우기_버튼을_누르면_닉네임은_지워져야한다`() {
    // Given: 닉네임이 이미 설정되어 있는 상태
    val nickName = NO_DUPLICATED_NICKNAME
    composeTestRule.setContent {
        NicknameScreen(viewModel = viewModel)
    }
    viewModel.nicknameState.updateNickname(nickName)

    // When: 전체 지우기 버튼을 클릭했을 때
    composeTestRule.onNodeWithTag(&quot;clear_button&quot;).performClick()

    // Then: 닉네임이 빈 문자열로 초기화되어야 함
    assertEquals(
        viewModel.nicknameState.nickname,
        &quot;&quot;,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 당연하게 PASS가 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6BAK1/btsJEPtjEc3/qEBuSefddYnKNPJ6oHYPU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6BAK1/btsJEPtjEc3/qEBuSefddYnKNPJ6oHYPU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6BAK1/btsJEPtjEc3/qEBuSefddYnKNPJ6oHYPU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6BAK1%2FbtsJEPtjEc3%2FqEBuSefddYnKNPJ6oHYPU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;198&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 버튼을 클릭 후 상호작용도 확인할 수 있고, 특정 요소가 화면에 출력되는지, 출력되는 아이템의 개수등을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력되는 데이터 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1726578924722&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun `불러온_학습_퀴즈_데이터들은_화면에_표시된다`() = runTest {
    val userId = dataStoreRepository.getUserId() ?: 0L
    coEvery { savedStateHandle.get&amp;lt;Long&amp;gt;(&quot;studyId&quot;) } returns -1L
    coEvery { studyRepository.getStudyQuiz(userId) } returns flowOf(
        ApiResponse.Success(studyQuiz)
    )
    viewModel = StudyViewModel(savedStateHandle, studyRepository, dataStoreRepository)
    composeTestRule.setContent {
        HandleStudyUi(studyState = viewModel.studyState.value, studyViewModel = viewModel)
    }
    composeTestRule.onNodeWithTag(&quot;study_quiz_title&quot;)
        .assertTextEquals(studyQuiz.quizTitle)
    composeTestRule.onNodeWithTag(&quot;study_quiz_script&quot;)
        .assertTextEquals(studyQuiz.quizScript)
    composeTestRule.onNodeWithTag(&quot;study_quiz_list&quot;).onChildren()
        .assertCountEquals(studyQuiz.wordList.size)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 불러온 퀴즈 데이터들의 개수가 화면에 출력되는 개수가 일치하는지 확인하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onChildren()의 assetCountEquals를 통해 숫자가 일치하는지 검증할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 메시지 출력 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1726579056634&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
fun `보기를_선택하지_않고_제출할경우_에러메시지가_출력된다`() = runTest {
    val userId = dataStoreRepository.getUserId() ?: 0L
    coEvery { savedStateHandle.get&amp;lt;Long&amp;gt;(&quot;studyId&quot;) } returns -1L
    coEvery { studyRepository.getStudyQuiz(userId) } returns flowOf(
        ApiResponse.Success(studyQuiz)
    )
    viewModel = StudyViewModel(savedStateHandle, studyRepository, dataStoreRepository)
    composeTestRule.setContent {
        HandleStudyUi(studyState = viewModel.studyState.value, studyViewModel = viewModel)
    }
    composeTestRule.onNodeWithTag(&quot;study_submit_button&quot;).performClick()
    composeTestRule.onNodeWithTag(&quot;error_snack_bar&quot;).assertIsDisplayed()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기를 선택하지 않고, 제출 버튼을 누를 경우 스낵바가 화면에 출력되는지를 확인하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assertIsDisplayed를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAQTFI/btsJDppgpAu/wUlLbs1mTgwK03ksaSArt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAQTFI/btsJDppgpAu/wUlLbs1mTgwK03ksaSArt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAQTFI/btsJDppgpAu/wUlLbs1mTgwK03ksaSArt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAQTFI%2FbtsJDppgpAu%2FwUlLbs1mTgwK03ksaSArt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1450&quot; height=&quot;914&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 UI Test Code의 실행 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성해본 경험은 처음이었는데요, TDD가 아닌 개발이 마무리 된 후 테스트 코드를 도입해서 작성하는데 더 수월하지 않았나 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 테스트 코드를 작성하고, 검증하면서 개발을 하면 아마 엄청 많이 바껴서 더 복잡하지 않았을까...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성하면서 느낀점은 많은데, 내가 진행하는 규모에서 도입하는것이 과연 이득일지 아직은 고민이 되는것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 프로젝트에서는 TDD를 도입해서 커버리지 범위도 넓게 하고, 전문적으로 작성해보고 싶은 마음이 드는것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;figure id=&quot;og_1726641658182&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] Jetpack Compose 테스트코드 어떻게 짜는거예요?&quot; data-og-description=&quot;UI 테스트 좋아하세요?&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@lyh990517/Android-Jetpack-Compose-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A7%9C%EB%8A%94%EA%B1%B0%EC%98%88%EC%9A%94&quot; data-og-url=&quot;https://velog.io/@lyh990517/Android-Jetpack-Compose-테스트코드-어떻게-짜는거예요&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/j7XaC/hyW2RFkzEY/o1kChrTdBu5ZNZxrzElRDK/img.png?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/ctGHNO/hyW2ZDlqNU/mD6ZzBdKRpd4xp84Kt7eM1/img.png?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/ddpbfb/hyW22mzdyP/8jKhmye6z8CVmKxDTJdFd1/img.jpg?width=1152&amp;amp;height=1504&amp;amp;face=408_662_835_1129&quot;&gt;&lt;a href=&quot;https://velog.io/@lyh990517/Android-Jetpack-Compose-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A7%9C%EB%8A%94%EA%B1%B0%EC%98%88%EC%9A%94&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@lyh990517/Android-Jetpack-Compose-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A7%9C%EB%8A%94%EA%B1%B0%EC%98%88%EC%9A%94&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/j7XaC/hyW2RFkzEY/o1kChrTdBu5ZNZxrzElRDK/img.png?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/ctGHNO/hyW2ZDlqNU/mD6ZzBdKRpd4xp84Kt7eM1/img.png?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/ddpbfb/hyW22mzdyP/8jKhmye6z8CVmKxDTJdFd1/img.jpg?width=1152&amp;amp;height=1504&amp;amp;face=408_662_835_1129');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] Jetpack Compose 테스트코드 어떻게 짜는거예요?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;UI 테스트 좋아하세요?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1726579946082&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Compose 레이아웃 테스트 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 레이아웃 테스트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 UI를 테스트하여 Compose 코&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/ui/compose/testing?hl=ko&quot; data-og-url=&quot;https://developer.android.com/develop/ui/compose/testing?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpmNtG/hyW6yjIWHI/m1zHE7VV2KoSPDgFHKkPhK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/testing?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/ui/compose/testing?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpmNtG/hyW6yjIWHI/m1zHE7VV2KoSPDgFHKkPhK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose 레이아웃 테스트 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 레이아웃 테스트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 UI를 테스트하여 Compose 코&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>compose ui test</category>
      <category>junit4</category>
      <category>ui test</category>
      <category>unit test</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/498</guid>
      <comments>https://jja2han.tistory.com/498#entry498comment</comments>
      <pubDate>Tue, 17 Sep 2024 22:33:27 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - 외부 라이브러리 없이 동영상 편집 구현하기</title>
      <link>https://jja2han.tistory.com/497</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;해당 프로젝트 관련 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/496&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] - 동영상에 대한 썸네일 리스트 반환하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/494&quot;&gt;[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/493&quot;&gt;[Android] - 레이어 분리 [멀티 모듈 적용기(1)]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/492&quot;&gt;[Android] 커스텀 컨트롤러 구현 및 동영상 재생 관리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/491&quot;&gt;[Android] 비디오 타임라인 이벤트 처리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot;&gt;[Android] - Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 글에서는 동영상을 선택할 경우 편집기 UI에 썸네일 리스트를 반환하는 글을 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 동영상을 어떻게 라이브러리 없이 편집했는지에 대한 글을 작성할까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MediaExtractor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상 편집을 구현하기 위해서 저는 MediaExtractor와 MediaMuxer를 사용했는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선적으로 MediaExtractor에 대해서 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaExtractor&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/android/media/MediaExtractor&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724496350531&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MediaExtractor &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/media/MediaExtractor&quot; data-og-url=&quot;https://developer.android.com/reference/android/media/MediaExtractor&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7FGnD/hyWV1GphnZ/uJMtgCOBKjlF1mFdluAdKk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaExtractor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/reference/android/media/MediaExtractor&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7FGnD/hyWV1GphnZ/uJMtgCOBKjlF1mFdluAdKk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MediaExtractor &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaExtractor는 안드로이드 프레임워크에서 멀티미디어 파일의 트랙(비디오, 오디오) 데이터를 추출할 수 있도록 도와주는 클래스입니다. 주로 비디오 또는 오디오 파일을 디코딩하거나, 재생 전 데이터를 처리할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;MediaExtractor&lt;/span&gt;를 사용하여 파일에서 특정 트랙(오디오 트랙)을 선택하고, 해당 트랙에서 데이터 샘플을 추출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상을 편집하기 위해선 동영상의 데이터를 추출하는 것이 필요한데요, 해당 작업을 MediaExtractor를 통해서 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상의 여러 정보를 가져올 수 있지만 주로 오디오와 비디오에 대한 트랙을 추출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오 트랙은 미디어 파일에서 소리를 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 트랙은 영상 데이터를 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaExtractor를 통해서 영상에 대해 필요한 정보를 추출하고 MediaMuxer에 전달해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 작업이 아래 코드를 통해서 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724497368508&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val outputFilePath = video.absolutePath
val extractor = MediaExtractor() // extractor 생성
extractor.setDataSource(file.path) // 추출할 파일 지정
val muxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) //muxer 초기 설정(경로, 타입)
val trackIndexMap = IntArray(extractor.trackCount) 
for (i in trackIndexMap.indices) {
    val format = extractor.getTrackFormat(i) // format 추출
    val mime = format.getString(MediaFormat.KEY_MIME) // 트랙의 유형을 식별해야 함.
    val trackIndex = muxer.addTrack(format) // muxer에 track를 추가함
    trackIndexMap[i] = trackIndex // 추가한 track의 인덱스를 저장
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 와서 아쉬운 점은 필요없는 트랙까지 모두 추가해주고 있는 모습이 보이는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터링을 걸어서 mime 타입이 video, audio에 대한 트랙 정보만 muxer에 전달하는 것이 좋았던 거 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 수정할 수 있을것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724497568091&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (i in 0 until extractor.trackCount) {
    val format = extractor.getTrackFormat(i)
    val mime = format.getString(MediaFormat.KEY_MIME)
    if (mime != null &amp;amp;&amp;amp; mime.startsWith(&quot;audio/&quot;) || mime.startsWith(&quot;video/&quot;)) {
        val trackIndex = muxer.addTrack(format)
        trackIndexMap[i] = trackIndex
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 작업을 통해서 진행도는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MediaExtractor에 데이터를 추출할 영상을 설정&lt;/li&gt;
&lt;li&gt;Muxer로 생성할 데이터의 타입(Mp4)과 저장될 경로를 설정&lt;/li&gt;
&lt;li&gt;영상 파일에 대해 오디오 트랙과 영상 트랙을 추출함.&lt;/li&gt;
&lt;li&gt;Muxer에 트랙을 추가함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MediaMuxer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaMuxer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/android/media/MediaMuxer&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724498961056&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MediaMuxer &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/media/MediaMuxer&quot; data-og-url=&quot;https://developer.android.com/reference/android/media/MediaMuxer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MEMGq/hyWVThjgKS/nv3sixm6ckEhSMVaSFv67K/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaMuxer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/reference/android/media/MediaMuxer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MEMGq/hyWVThjgKS/nv3sixm6ckEhSMVaSFv67K/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MediaMuxer &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaMuxer는 여러 개의 트랙을 합쳐 하나의 파일로 결합해 새로운 미디어 파일을 생성하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 MediaExtractor를 통해 비디오 파일에서 추출한 트랙을 하나의 파일로 결합해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상에 대한 디코딩과 인코딩은 MediaCodec이 담당하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MediaCodec&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;MediaCodec&lt;/span&gt;은 미디어 데이터의 인코딩 및 디코딩을 수행하는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오와 오디오 스트림의 압축 및 압축 해제, 즉 데이터의 변환을 처리해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaCodec&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/android/media/MediaCodec&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724499313306&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MediaCodec &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/media/MediaCodec&quot; data-og-url=&quot;https://developer.android.com/reference/android/media/MediaCodec&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iyXRU/hyWV2egjLa/QwheqZhDbh2qvooBRX0EYK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/media/MediaCodec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/reference/android/media/MediaCodec&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iyXRU/hyWV2egjLa/QwheqZhDbh2qvooBRX0EYK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MediaCodec &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaCodec과 MediaMuxer의 관계는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MediaCodec을 통해서 비디오를 인코딩 혹은 디코딩 후 처리된 데이터를 결합해서 MediaMuxer를 통해서 하나의 파일로 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 제가 작성한 코드는 인코딩과 디코딩을 처리하기보단 입력받은 시간에 대해 동영상 파일을 추출해서 저장하는 방식에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 인코딩과 디코딩을 처리할 필요가 없다고 생각해서 다음과 같이 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724499692900&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;muxer.start()
    val buffer = ByteBuffer.allocate(1024 * 1024)
    val bufferInfo = MediaCodec.BufferInfo()
    for (i in trackIndexMap.keys) {
        extractor.selectTrack(i)
        if (start != 0L) {
            extractor.seekTo(start * 1000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        }
        while (true) {
            bufferInfo.size = extractor.readSampleData(buffer, 0)
            bufferInfo.presentationTimeUs = extractor.sampleTime
            if (bufferInfo.size &amp;lt; 0) {
                break
            }
            if (bufferInfo.presentationTimeUs &amp;gt; end * 1000L) {
                break
            }
            bufferInfo.flags = extractor.sampleFlags
            muxer.writeSampleData(trackIndexMap[i]!!, buffer, bufferInfo)
            extractor.advance()
        }
	    extractor.unselectTrack(i)
    }
    extractor.release()
    muxer.stop()
    muxer.release()
    return video
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724499760530&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;muxer.start()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;muxer를 시작해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업을 통해 나는 이제 muxer의 등록된 트랙에 대해 데이터를 기록할 준비가 되었다는 설정을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;start를 해주지 않으면 아래 writeSampleData를 호출하더라도 데이터가 기록되지 않기에 필요한 작업입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724499849086&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val buffer = ByteBuffer.allocate(1024 * 1024)
val bufferInfo = MediaCodec.BufferInfo()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 읽을 임시 buffer와 데이터를 저장할 bufferInfo 객체를 생성해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bufferInfo 객체는 후 muxer의 데이터를 기록하기 위해 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 muxer의 등록된 트랙에 대해 작업을 수행해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724500194062&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (i in trackIndexMap.keys) {
    extractor.selectTrack(i)
    if (start != 0L) {
        extractor.seekTo(start * 1000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
    }
    while (true) {
        bufferInfo.size = extractor.readSampleData(buffer, 0)
        bufferInfo.presentationTimeUs = extractor.sampleTime
        if (bufferInfo.size &amp;lt; 0) {
            break
        }
        if (bufferInfo.presentationTimeUs &amp;gt; end * 1000L) {
            break
        }
        bufferInfo.flags = extractor.sampleFlags
        muxer.writeSampleData(trackIndexMap[i]!!, buffer, bufferInfo)
        extractor.advance()
    }
    extractor.unselectTrack(i)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 추가한 트랙 index에 대해 트랙을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 start와 end를 통해 동영상 편집을 해야 하기 때문에 start가 0이 아니라면 seekTo 메서드를 통해 동영상의 프레임을 뒤로 당겨줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 작업은 간단합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문을 통해 동영상이 끝나지 않는 시점까지 계산해서 데이터를 저장해 주는 작업인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;extractor를 통해서 데이터를 읽어오고 해당 데이터를 bufferInfo에 저장하고, 해당 값을 muxer에 저장한 후 extractor.advance 호출을 통해 데이터의 시점을 이동해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 과정에서 데이터를 읽을 수 없어 bufferInfo의 사이즈가 0이 되거나, 시간대가 편집이 끝나는 시점을 초과한다면 종료해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 트랙의 선택을 해제해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 트랙에 대해 작업을 완료했다면 리소소를 해제해줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724500534013&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extractor.release()
muxer.stop()
muxer.release()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 끝났기 때문에 extractor와 muxer를 반납하고 , 종료한 뒤 파일을 완성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignLeft&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/449050779&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/e9W6i/hyWShxonIb/aEHYj5ukPYK2N8UFtdWhV0/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920,https://scrap.kakaocdn.net/dn/bkkrDQ/hyWV6AXQb4/b6WM0NlALEBN2D8ihH44h0/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920&quot; data-video-width=&quot;500&quot; data-video-height=&quot;1084&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;1864&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;'짜이한'에서 업로드한 동영상&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/449050779?service=daum_tistory&quot; width=&quot;500&quot; height=&quot;1084&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편집 구간을 설정한 후 영상을 확인해보면 편집된 영상임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 작성하면서 영상 처리에 대한 흐름을 완벽하게 숙지하지 않고 코드를 작성해서 불필요한 트랙의 추가와 최적화가 전혀 처리되지 않아서 실수 투성이인 코드인 것 같습니다 ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답인 코드가 아니라 참고만 해주세요!&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>동영상 편집</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/497</guid>
      <comments>https://jja2han.tistory.com/497#entry497comment</comments>
      <pubDate>Sat, 24 Aug 2024 21:02:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - 동영상에 대한 썸네일 리스트 반환하기</title>
      <link>https://jja2han.tistory.com/496</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;해당 프로젝트 관련 글&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/494&quot;&gt;[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/493&quot;&gt;[Android] - 레이어 분리 [멀티 모듈 적용기(1)]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/492&quot;&gt;[Android] 커스텀 컨트롤러 구현 및 동영상 재생 관리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/491&quot;&gt;[Android] 비디오 타임라인 이벤트 처리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot;&gt;[Android] - Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상을 직접 편집할 수 있고, 지도에 올리는 애플리케이션을 개발하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편집 기능을 구현하기 위해 편집기 UI를 구현했고, 요구사항에 맞게 편집기 UI 안에 썸네일을 리스트형태로 보여줘야 하고,&lt;br /&gt;영상에 대한 썸네일을 구하는 코드를 작성했고, 그에 대한 글을 작성할까 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MediaMetadataRetriver&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상의 이미지를 가져오기 위해선 동영상의 데이터에 대해 접근할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서는 MediaMetadataRetriver를 통해서 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLLBqx/btsI1dVyOY3/Jcbr8YXB9JWKgKBOBUHHdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLLBqx/btsI1dVyOY3/Jcbr8YXB9JWKgKBOBUHHdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLLBqx/btsI1dVyOY3/Jcbr8YXB9JWKgKBOBUHHdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLLBqx%2FbtsI1dVyOY3%2FJcbr8YXB9JWKgKBOBUHHdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;287&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 영어실력으로 해석해보자면, 인풋으로 들어온 media file에 대해서 meta dat를 뽑아낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input media를 넣어주기 위해선 &lt;code&gt;setDataSource&lt;/code&gt;라는 함수를 이용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SetDataSource&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DB45r/btsI03Tb3fl/dIJ1zeoMcPNMTWCKB8yw81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DB45r/btsI03Tb3fl/dIJ1zeoMcPNMTWCKB8yw81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DB45r/btsI03Tb3fl/dIJ1zeoMcPNMTWCKB8yw81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDB45r%2FbtsI03Tb3fl%2FdIJ1zeoMcPNMTWCKB8yw81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;413&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;setDataSource&lt;/code&gt;는 위와같이 여러 가지 형태로 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 동영상 파일에 대해 path를 계산하는 것보단 동영상 파일의 Uri와 context를 제공하는 것이 간편해서 이용했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;context를 전달하는것은 좋지 않다고 생각해서 동영상 파일의 path를 계산해서 사용하는 방향을 선택했습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(저는 썸네일에 대한 작업을 처리하는 클래스를 따로 분리해서 사용했기에 context를 전달하는 것은 적절하지 않다고 생각했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 MediaMetadataRetriver에 동영상을 넣어줬다면 뽑아낼 수 있는 정보는 정말 다양합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상에 대한 정보를 뽑아내기 위해서는 아래 코드를 사용해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;extractMetadata&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val retriever = MediaMetadataRetriver() // MediaMetadataRetriver 객체 생성
retriver.setDataSource(videoPath) // 동영상 파일 넣어주기
val info = retriver.extractMetadata(원하는 데이터)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 의하면 뽑아낼 수 있는 데이터는 다양합니다. 데이터에 대한 확인은 공식문서에서 constants에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 동영상의 썸네일을 구간 별로 뽑아내야했기에, 동영상의 길이만을 가지고 왔습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상의 길이에 대응해 고정된 썸네일의 개수를 리스트 형태로 반환하는 것이 이번 작업의 목표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 뽑아온 durationStr의 반환타입은 String?이기에 다음과 같은 코드를 통해서 동영상의 길이를 더블체크 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val duration = durationStr?.toLongOrNull() ?: return emptyList()
val interval = duration / thumbnailCount&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toLongOrNull을 통해 Long 또는 Null을 반환하게 해서 Null 일경우 썸네일이 들어있지 않은 리스트를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 duration이 Null이 아닐 경우 제가 정한 썸네일 개수를 나눠서 구간을 끊어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;썸네일의 개수를 10개이고, 동영상이 100초로 가정할 경우 10,20,30, ~~ 100초에 해당하는 구간에 대해 썸네일을 추출해 줍니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val thumbnails = mutableListOf&amp;lt;VideoThumbnail&amp;gt;()
for (i in 0 until thumbnailCount) {
    val timeUs = (i * interval) * 1000L
    val bitmap = retriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST)
    val scaledBitmap = bitmap?.let {
        Bitmap.createScaledBitmap(it, width / 10, it.height, false)
    }
    if (scaledBitmap != null) {
        thumbnails.add(VideoThumbnail(thumbnails.size, scaledBitmap))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은&lt;br /&gt;MediaMetadataRetriver를 통해 받아온 &lt;b&gt;duration의 단위는 ms이기 때문에 단위 변환을 해줘야 한다는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 변환을 하지 않으면 구한 구간이 아무 의미 없는 정보가 되어버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;썸네일을 가져오기 위해선 &lt;code&gt;getFrameAtTime&lt;/code&gt; 함수를 이용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getFrameAtTime&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2678&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muJjC/btsI1xGi9MU/eHkaLoSeIQxonP12owIxtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muJjC/btsI1xGi9MU/eHkaLoSeIQxonP12owIxtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muJjC/btsI1xGi9MU/eHkaLoSeIQxonP12owIxtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuJjC%2FbtsI1xGi9MU%2FeHkaLoSeIQxonP12owIxtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;791&quot; height=&quot;332&quot; data-origin-width=&quot;2678&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대다수의 함수에서 파라미터로 &lt;b&gt;option&lt;/b&gt;을 입력받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getFrameAtTime에서 활용되는 option은 4개가 있는데요, 짧은 영어실력으로 번역을 해보면 다음과 같이 해석됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTION_CLOSEST&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;입력받은 시간(키 프레임일 필요는 없음)에 대해 가장 가까운 시간대 혹은 그에 대응되는 프레임을 반환한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTION_CLOSEST_SYNC&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;입력받은 시간에 대해 가장 가까운 시간대 혹은 그에 대응되는 키 프레임을 반환한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTION_NEXT_SYNC&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;입력받은 시간 이후에 나오는 첫 번째 키 프레임을 반환한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OPTION_PREVIOUS_SYNC&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;입력받은 시간 이전에 위치한 가장 가까운 키 프레임을 반환한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OPTION_NEXT_SYNC와 OPTION_PREVIOUS_SYNC&lt;/b&gt;는 정확한 시간에 썸네일을 뽑아내기에는 적합하지 않은 옵션이라고 생각해, &lt;b&gt;OPTION_CLOSEST, OPTION_CLOSEST_SYNC&lt;/b&gt;를 직접 비교해서 확인해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 동영상 파일은 같은 파일을 사용했고, 동영상 파일와 편집 시작시간, 편집 끝나는 시간에 대해 로그를 찍었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt; OPTION_CLOSEST&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/25J9c/btsI0HiPlro/mTh0Y1FWtXVQdQpCMco1xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/25J9c/btsI0HiPlro/mTh0Y1FWtXVQdQpCMco1xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/25J9c/btsI0HiPlro/mTh0Y1FWtXVQdQpCMco1xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F25J9c%2FbtsI0HiPlro%2FmTh0Y1FWtXVQdQpCMco1xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;170&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OPTION_CLOSEST_SYNC&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvnsmV/btsI0LZxCBF/ZfB50PpDM9vQ89r5BtipCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvnsmV/btsI0LZxCBF/ZfB50PpDM9vQ89r5BtipCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvnsmV/btsI0LZxCBF/ZfB50PpDM9vQ89r5BtipCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvnsmV%2FbtsI0LZxCBF%2FZfB50PpDM9vQ89r5BtipCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;202&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수치가 아닌 영상으로 확인해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 영상이 OPTION_CLOSET을 사용한 편집이고, 두 번째 영상이 OPTION_CLOSET_SYNC를 사용한 편집입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignLeft&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/448735766&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/SbBjR/hyWOjm2biW/z68jHQnkEpECyydjy3plp0/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920,https://scrap.kakaocdn.net/dn/frV0c/hyWKxNXpj2/fphQ48QifFWKBXza0U8XQ1/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920&quot; data-video-width=&quot;860&quot; data-video-height=&quot;1864&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;1864&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;'짜이한'에서 업로드한 동영상&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/448735766?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;1864&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/448735767&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bQwwKc/hyWKAxcV29/iWx8LfKHi2ogp2FQnDOIp1/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920,https://scrap.kakaocdn.net/dn/cfkImT/hyWKIaWifb/RTMs3flNEtDje4qht6Vmk1/img.jpg?width=886&amp;amp;height=1920&amp;amp;face=0_0_886_1920&quot; data-video-width=&quot;860&quot; data-video-height=&quot;1864&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;1864&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;'짜이한'에서 업로드한 동영상&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/448735767?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;1864&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 1시간 영상에 대해서 &lt;b&gt;2초 차이가 났고&lt;/b&gt;, 로그로 실감하기보단 휴대폰을 실제로 보면서 느끼는 체감이 훨씬 컸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력되는 썸네일의 이미지도 육안으로 동일한 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;썸네일 크기 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 이렇게 뽑은 썸네일들을 내가 원하는 편집기 UI에 크기를 맞춰줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 썸네일을 추출하는 함수에서 편집기 UI에 대응되는 width 값을 제공해야 했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val widthPixels = binding.recyclerViewVideoThumbnail.measuredWidth
uploadVideoViewModel.getThumbnails(width = widthPixels, path = file.path)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로길이는 layout의 meauredWidth를 이용해서 측정할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;썸네일은 bitmap으로 반환되었고, 크기를 변경해야 했기 때문에 &lt;code&gt;createdaScaledBitmap&lt;/code&gt;을 사용해서 크기를 변경시켜 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val scaledBitmap =
    bitmap?.let {
        Bitmap.createScaledBitmap(it, width / 10, it.height, false)
    }
if (scaledBitmap != null) {
    thumbnails.add(VideoThumbnail(thumbnails.size, scaledBitmap))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후에 retriever를 release 해줍니다. 하지 않으면 memory leak으로 이어질 수 있기 때문에 관련 리소스를 해제하여 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 선택한 동영상에 대해서 썸네일을 추출하는 과정을 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다(?) 간단한 코드로 동영상에 대한 썸네일을 추출할 수 있어서 라이브러리에 대한 극찬을 계속했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음글은 영상 편집과정에 대해 포스팅할까 합니다.&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>mediadataretriver</category>
      <category>썸네일 추출</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/496</guid>
      <comments>https://jja2han.tistory.com/496#entry496comment</comments>
      <pubDate>Sat, 10 Aug 2024 18:15:43 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] - Maven Spring 자동 배포 파이프 라인 구축(Github Actions)</title>
      <link>https://jja2han.tistory.com/495</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MiniMoment 프로젝트를 진행하면서 얕은 범위로 서버까지 개발을 진행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 개발을 진행하니, CI/CD에 관심이 자연스레 관심이 생겨서 Docker를 이용해 자동 배포 파이프라인을 구축해보고 싶었습니다.&lt;br /&gt;저는 Jenkins가 아닌 Github Actions를 이용해서 파이프라인을 구축했고, 느낀 점을 적어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도커 파일 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 파일은 여러 블로그에서 사용되는 코드를 참고해서 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target 폴더 아래에 mimo-*.jar 이라는 jar 파일을 app.jar이라는 이름의 파일로 복사해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721920003386&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk
COPY target/mimo-*.jar app.jar
ENTRYPOINT [ &quot;java&quot;, &quot;-jar&quot;,&quot;app.jar&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자료의 부족&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 배포 파이프라인을 적용하기 위해 많은 글을 찾아봤는데, 대부분의 글이 Gradle 관련 프로젝트였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저희 프로젝트는 Maven..이었기 때문에 전체적인 흐름을 파악하는 것으로 만족해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle과 Maven의 차이는 여러 가지가 있겠지만, 현재 상황에서 느껴지는 차이점은 jar 파일을 만들어내는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle은./gradlew ~ 를 이용해서 build를 하고 jar 파일을 만들어냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven은 mvn install이란 명령어를 통해 build를 하고 jar 파일을 만들어냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yaml 파일에서 step을 정하는 과정에서 두 부분의 차이가 있었고, 이를 반영했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 이용한 Github Actions를 사용하기 위해선 여러 가지 변수들이 필요하고, 이를 yaml 파일에 노출하는 것은 위험하기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secrets 변수로 관리해 줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변수 관리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;APPLICATION_PROPERTIES : 서버 포트나, 디비 경로와 같은 민감한 정보들&lt;/li&gt;
&lt;li&gt;DOCKER_USER_NAME : 도커에 로그인하기 위한 유저 네임&lt;/li&gt;
&lt;li&gt;DOCKER_USER_PASSWORD : 도커에 로그인 하기 위한 비밀번호&lt;/li&gt;
&lt;li&gt;DOCKER_REPO_NAME : 도커 레포지토리 이름&lt;/li&gt;
&lt;li&gt;DOCKER_CONTAINER_NAME : 도커 컨테이너 이름&lt;/li&gt;
&lt;li&gt;EC2_HOST : EC2 Host 주소&lt;/li&gt;
&lt;li&gt;EC2_SSH_PRIVATE_KEY : pem 확장자인 EC2 키입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Yaml 파일 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1721920893569&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  push:
    branches: [ &quot;develop&quot; ]
  pull_request:
    branches: [ &quot;develop&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액션을 실행할 트리거입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 develop에 푸시되거나, pr이 생긴 경우 실행하도록 트리거를 설정해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721920956810&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jobs:
  build:
    runs-on: ubuntu-latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jobs는 해당 작업에 대한 이름이고, runs-on은 작업이 실행될 환경이고, 우분투 최신버전으로 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721921143565&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; - name: Checkout repository
    uses: actions/checkout@v3

  - name: Set up JDK 19
    uses: actions/setup-java@v3
    with:
      java-version: '19'
      distribution: 'temurin'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checkout&amp;nbsp; Repository는 깃헙 레조피토리 폴더 구조를 그대로 파일 시스템으로 들고 오는 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인에서는 반드시 필요한 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set up JDK 19에서는 말 그대로 JDK 버전을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721921321463&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Set up application.properites
    run: echo &quot;${{ secrets.APPLICATION_PROPERTIES }}&quot; &amp;gt; ./src/main/resources/application.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties나 yaml 파일은 민감한 정보가 많아서 github에 푸시하지 않는 ignore에 추가하지만, 실제 배포 소스 코드에는 포함이 되어야 하기 때문에 secrets 변수로 관리해서 필요한 경로에 위치시켜 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721921398742&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Set up Maven
    uses: actions/setup-java@v3
    with:
      java-version: '19'
      distribution: 'temurin'
      cache: 'maven'

  - name: Clean with Maven
    run: mvn clean

  - name: Install with Maven
    run: mvn install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Gradle 환경이 아니고, Maven 기반 프로젝트이기에, Maven을 세팅하고, jar 파일을 만들기 위해 mvn을 clean -&amp;gt; install 하는 과정을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721921465156&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Build the Docker image
    run: |
      docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{secrets.DOCKER_PASSWORD}}
      docker build -t ${{secrets.DOCKER_REPO }}  
      docker push ${{ secrets.DOCKER_REPO }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 이미지를 build 하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 허브에 로그인을 하고, 도커 파일에 정의된 경로에 jar 파일을 통해 이미지를 빌드하고, 도커 레포지토리에 푸시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721921513865&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Deploy to EC2 Server
    uses: appleboy/ssh-action@master
    with: 
      host: ${{ secrets.EC2_HOST}}
      username : ec2-user
      key : ${{secrets.EC2_SSH_PRIVATE_KEY}}
      script: |
        CONTAINER_ID=$(sudo docker ps -q --filter &quot;publish=80-8080&quot;)
        if [ ! -z &quot;$CONTAINER_ID&quot; ]; then
          sudo docker stop $CONTAINER_ID
          sudo docker rm $CONTAINER_ID
        fi
        sudo docker pull ${{secrets.DOCKER_REPO }}
        sudo docker run -- name ${{ secrets.DOCKER_CONTAINER_NAME }} -d -p 8080:8080
        sudo docker logs ${{secrets.DOCKER_CONTAINER_NAME}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버에 배포하는 단계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;host와 username을 지정하고, key를 입력해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행할 명령어는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행 중인 컨테이너의 ID를 가져온 빈다.&lt;/li&gt;
&lt;li&gt;만약 컨테이너 ID가 존재한다면(실행 중이라면) 해당 컨테이너를 멈추고, 컨테이너를 삭제합니다.&lt;/li&gt;
&lt;li&gt;도커 레포지토리에 pull을 하고 Image를 받아옵니다.&lt;/li&gt;
&lt;li&gt;도커 이미지를 바탕으로 컨테이너를 실행합니다.&lt;/li&gt;
&lt;li&gt;로그를 찍어줍니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해 보니 d 옵션을 줬는데 로그 찍는 명령어를 적어놨고, 포트 번호를 하드 코딩하는 등 미흡한 점이 많네요.&lt;br /&gt;코드가 아닌 흐름을 참고해 주세요. ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 yaml 파일&lt;/p&gt;
&lt;pre id=&quot;code_1721920190470&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Docker Image CI

on:
  push:
    branches: [ &quot;develop&quot; ]
  pull_request:
    branches: [ &quot;develop&quot; ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up JDK 19
        uses: actions/setup-java@v3
        with:
          java-version: '19'
          distribution: 'temurin'

      - name: Set up application.properites
        run: echo &quot;${{ secrets.APPLICATION_PROPERTIES }}&quot; &amp;gt; ./src/main/resources/application.properties

      - name: Set up Maven
        uses: actions/setup-java@v3
        with:
          java-version: '19'
          distribution: 'temurin'
          cache: 'maven'

      - name: Clean with Maven
        run: mvn clean

      - name: Install with Maven
        run: mvn install

      - name: Build the Docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{secrets.DOCKER_PASSWORD}}
          docker build -t ${{secrets.DOCKER_REPO }}  
          docker push ${{ secrets.DOCKER_REPO }} 
          
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with: 
          host: ${{ secrets.EC2_HOST}}
          username : ec2-user
          key : ${{secrets.EC2_SSH_PRIVATE_KEY}}
          script: |
            CONTAINER_ID=$(sudo docker ps -q --filter &quot;publish=80-8080&quot;)
            if [ ! -z &quot;$CONTAINER_ID&quot; ]; then
              sudo docker stop $CONTAINER_ID
              sudo docker rm $CONTAINER_ID
            fi
            sudo docker pull ${{secrets.DOCKER_REPO }}
            sudo docker run -- name ${{ secrets.DOCKER_CONTAINER_NAME }} -d -p 8080:8080
            sudo docker logs ${{secrets.DOCKER_CONTAINER_NAME}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나름 잘 작성했다고 생각했고, 액션이 돌아가는 과정을 기다리는 과정에서 오류가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOz4G/btsINs7n9rr/vFu2n8kLojvCJvw6kkxr10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOz4G/btsINs7n9rr/vFu2n8kLojvCJvw6kkxr10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOz4G/btsINs7n9rr/vFu2n8kLojvCJvw6kkxr10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOz4G%2FbtsINs7n9rr%2FvFu2n8kLojvCJvw6kkxr10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;111&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 한 결과 쉘에 기록이나 로그 파일에 기록이 남을 수 있어 이에 대한 경고를 하는 것으로 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 블로그의 방법을 참고해서,&amp;nbsp; echo로 패스워드를 출력하고, 해당 정보를 파이프로 로그인 시 전달해 줬습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPswx7/btsINMq4H0l/n4wOcYk9EkGBoUxt0brU51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPswx7/btsINMq4H0l/n4wOcYk9EkGBoUxt0brU51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPswx7/btsINMq4H0l/n4wOcYk9EkGBoUxt0brU51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPswx7%2FbtsINMq4H0l%2Fn4wOcYk9EkGBoUxt0brU51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;62&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 결과 액션이 정상적으로 동작했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NoTxX/btsINxnjD93/ldEmZ0ypTP6Z6brICqu8bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NoTxX/btsINxnjD93/ldEmZ0ypTP6Z6brICqu8bk/img.png&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;700&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.041%; margin-right: 10px;&quot; data-widthpercent=&quot;32.42&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NoTxX/btsINxnjD93/ldEmZ0ypTP6Z6brICqu8bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNoTxX%2FbtsINxnjD93%2FldEmZ0ypTP6Z6brICqu8bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Krp7o/btsIN4ZcB4O/enKVaZ73Urd3Fk56iZkEK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Krp7o/btsIN4ZcB4O/enKVaZ73Urd3Fk56iZkEK1/img.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;191&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;67.58&quot; data-filename=&quot;blob&quot; style=&quot;width: 66.7962%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Krp7o/btsIN4ZcB4O/enKVaZ73Urd3Fk56iZkEK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKrp7o%2FbtsIN4ZcB4O%2FenKVaZ73Urd3Fk56iZkEK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공했지만 아쉬운 점은 build라는 job 하나로 모든 단계를 진행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 build의 과정은 Jdk와 Maven을 세팅하는 과정까지라고 생각하고, 더 개선해 볼 여지가 있다 생각했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Rs5n/btsIN4ZcDff/GIAWrue56R6g3jIpGVGD71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Rs5n/btsIN4ZcDff/GIAWrue56R6g3jIpGVGD71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Rs5n/btsIN4ZcDff/GIAWrue56R6g3jIpGVGD71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Rs5n%2FbtsIN4ZcDff%2FGIAWrue56R6g3jIpGVGD71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1061&quot; height=&quot;74&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 job을 나누고, 코드를 분배했는데, 액션이 실패한 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 메시지로 추정한 결과 checkout repository라는 작업이 없어서 코드를 가져오지 못했다?라고 이해했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나눠진 작업에서 필요한 의존관계를 살펴보고 코드를 다시 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeRc5v/btsIMhr9Fxp/6R1XKP17NfB3iADgodCKkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeRc5v/btsIMhr9Fxp/6R1XKP17NfB3iADgodCKkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeRc5v/btsIMhr9Fxp/6R1XKP17NfB3iADgodCKkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeRc5v%2FbtsIMhr9Fxp%2F6R1XKP17NfB3iADgodCKkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;196&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 크게 job을 두 개로 분리해서 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job 사이의 의존관계는 needs라는 명령어를 통해서 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721922588320&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: MiniMoment Deploy

on:
  push:
    branches: [ &quot;develop&quot; ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up JDK 19
        uses: actions/setup-java@v3
        with:
          java-version: '19'
          distribution: 'temurin'

      - name: Set up application.properties
        run: echo &quot;${{ secrets.APPLICATION_PROPERTIES }}&quot; &amp;gt; ./src/main/resources/application.properties

      - name: Set up Maven
        uses: actions/setup-java@v3
        with:
          java-version: '19'
          distribution: 'temurin'
          cache: 'maven'
          
      - name: Clean with Maven
        run: mvn clean

      - name: Install with Maven
        run: mvn install

      - name: Log in to Docker Hub
        run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: Build the Docker image
        run: docker build -t ${{ secrets.DOCKER_REPO }} .
        
      - name: Push the Docker image
        run: docker push ${{ secrets.DOCKER_REPO }}
        
  deploy-ec2-server:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with: 
          host: ${{ secrets.EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          script: |
            CONTAINER_ID=$(sudo docker ps -q --filter &quot;publish=80-8080&quot;)
            if [ ! -z &quot;$CONTAINER_ID&quot; ]; then
              sudo docker stop $CONTAINER_ID
              sudo docker rm $CONTAINER_ID
            fi
            IMAGE_ID=$(sudo docker images -q ${{ secrets.DOCKER_REPO}})
            if [! -z &quot;$IMAGE_ID&quot; ]; then
              sudo docker rmi $IMAGE_ID
            fi
            sudo docker pull ${{ secrets.DOCKER_REPO }}
            sudo docker run --name ${{ secrets.DOCKER_CONTAINER_NAME }} -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}
            sudo docker logs ${{ secrets.DOCKER_CONTAINER_NAME }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직까지도 build에서 도커 관련 작업을 하는 것이 맘에 안 들었습니다. 개선할 필요성도 느꼈고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 분리한 단계에 대한 작업은 다음과 같이 구성할 예정이었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;build
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JDK 세팅&lt;/li&gt;
&lt;li&gt;application.properties 작성&lt;/li&gt;
&lt;li&gt;Maven 세팅 및 빌드&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;docker-login
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도커 로그인&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;build-and-push-docker-image&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도커 이미지 빌드&lt;/li&gt;
&lt;li&gt;도커 이미지 푸시&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;deploy-ec2-server
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행 중인 컨테이너 지우기&lt;/li&gt;
&lt;li&gt;컨테이너에 사용된 도커 이미지 지우기&lt;/li&gt;
&lt;li&gt;도커 레포지토리 pull&lt;/li&gt;
&lt;li&gt;도커 빌드&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p6NZU/btsIN2f3Iei/t718PM0RXfnfKpEVzyFc3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p6NZU/btsIN2f3Iei/t718PM0RXfnfKpEVzyFc3k/img.png&quot; data-origin-width=&quot;331&quot; data-origin-height=&quot;142&quot; data-is-animation=&quot;false&quot; style=&quot;width: 65.4952%; margin-right: 10px;&quot; data-widthpercent=&quot;66.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p6NZU/btsIN2f3Iei/t718PM0RXfnfKpEVzyFc3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp6NZU%2FbtsIN2f3Iei%2Ft718PM0RXfnfKpEVzyFc3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;331&quot; height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JVXeJ/btsINqaII9n/YXtnGDUf4vZiPrhFK9cs41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JVXeJ/btsINqaII9n/YXtnGDUf4vZiPrhFK9cs41/img.png&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;734&quot; data-is-animation=&quot;false&quot; style=&quot;width: 33.342%;&quot; data-widthpercent=&quot;33.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JVXeJ/btsINqaII9n/YXtnGDUf4vZiPrhFK9cs41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJVXeJ%2FbtsINqaII9n%2FYXtnGDUf4vZiPrhFK9cs41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;871&quot; height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 분리한 결과 build-and-push-docker-image에서 오류가 발생했고, 오류 원인은 도커 인증이 되지 않았다 이런 의미라고 이해했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각한 플로우로는 도커 인증을 한 후 적용이 되는 줄 알았지만, 각 작업이 독립적으로, 병렬적으로 진행된다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 job마다 checkout Repository라는 코드가 붙어있나?라는 생각을 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 docker-login과 build-and-push-docker-image 작업을 같이 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과!!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLn7gZ/btsIMQHGFTb/EMQ4pr6GJLCDDXS19p2zrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLn7gZ/btsIMQHGFTb/EMQ4pr6GJLCDDXS19p2zrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLn7gZ/btsIMQHGFTb/EMQ4pr6GJLCDDXS19p2zrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLn7gZ%2FbtsIMQHGFTb%2FEMQ4pr6GJLCDDXS19p2zrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;901&quot; height=&quot;92&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공했습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번외로 만난 오류가 build와 docker 작업 사이에서 jar 파일을 찾을 수 없어서 생긴 오류도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했던 것과 같이 각 작업별로 디렉터리나 파일 구조가 연결되지 않기에 jar 파일을 빌드해도 찾을 수 없다는 오류가 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HeOIq/btsINZ4LFG9/0kclqMIVIC0k8f3ReTBtOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HeOIq/btsINZ4LFG9/0kclqMIVIC0k8f3ReTBtOK/img.png&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;327&quot; data-is-animation=&quot;false&quot; style=&quot;width: 38.768%; margin-right: 10px;&quot; data-widthpercent=&quot;39.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HeOIq/btsINZ4LFG9/0kclqMIVIC0k8f3ReTBtOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHeOIq%2FbtsINZ4LFG9%2F0kclqMIVIC0k8f3ReTBtOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;436&quot; height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S6EEJ/btsIOUPatqx/OJJ9GXJPCNlL8hjh8ejKfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S6EEJ/btsIOUPatqx/OJJ9GXJPCNlL8hjh8ejKfK/img.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;182&quot; data-is-animation=&quot;false&quot; style=&quot;width: 60.0692%;&quot; data-widthpercent=&quot;60.78&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S6EEJ/btsIOUPatqx/OJJ9GXJPCNlL8hjh8ejKfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS6EEJ%2FbtsIOUPatqx%2FOJJ9GXJPCNlL8hjh8ejKfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽이 build 파일 디렉토리고, 오른쪽이 docker관련 작업 단계에서의 디렉터리인데, 디렉터리가 비어있는 것을 확인했고,&lt;br /&gt;이를 해결하기 위해&amp;nbsp; build에서 jar 파일을 업로드해서, 다른 job에서 다운로드하는 형태로 jar 파일을 보관했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgRbCu/btsIOCgVkmQ/ngw2DkQklSIaO4D2YAN1ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgRbCu/btsIOCgVkmQ/ngw2DkQklSIaO4D2YAN1ok/img.png&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;116&quot; data-is-animation=&quot;false&quot; style=&quot;width: 41.4694%; margin-right: 10px;&quot; data-widthpercent=&quot;41.96&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgRbCu/btsIOCgVkmQ/ngw2DkQklSIaO4D2YAN1ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgRbCu%2FbtsIOCgVkmQ%2Fngw2DkQklSIaO4D2YAN1ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;295&quot; height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJeJ7z/btsIOGp1KTR/OwMPbIklq3PSLxPhDkdHSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJeJ7z/btsIOGp1KTR/OwMPbIklq3PSLxPhDkdHSk/img.png&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;83&quot; data-is-animation=&quot;false&quot; style=&quot;width: 57.3678%;&quot; data-widthpercent=&quot;58.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJeJ7z/btsIOGp1KTR/OwMPbIklq3PSLxPhDkdHSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJeJ7z%2FbtsIOGp1KTR%2FOwMPbIklq3PSLxPhDkdHSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;292&quot; height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Volume&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트 내부 경로에서 동영상의 썸네일과 동영상 mp4 파일을 관리하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 배포가 진행될 때마다 Container를 삭제하고, 빌드하는 작업이 진행되었고, 해당 파일들이 소실되는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대응으로는 S3 서버를 도입할까 했지만, 앱 배포가 급선무라 S3가 아닌 방식으로 해결할 수 있는 수단이 필요했고, 그 수단으로 Docker Volume을 적용했고, 이를 CI/CD에 추가적으로 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 원래 Docker Volume과 같은 내용들은 Docker-Compose로 관리하는 것이 맞습니다. ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws에 docker volume을 생성하고, 경로를 설정하고, CI/CD를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 코드는 아래에 첨부하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721924845475&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run --name ${{ secrets.DOCKER_CONTAINER_NAME }} -d -p 8080:8080 -v ${{ secrets.MIMO_VIDEO_VOLUME }} -v ${{ secrets.MIMO_THUMBNAIL_VOLUME }} ${{ secrets.DOCKER_REPO }}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Skils/Server</category>
      <category>ci/cd</category>
      <category>Docker</category>
      <category>github Actions</category>
      <category>maven</category>
      <category>server</category>
      <category>자동 배포</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/495</guid>
      <comments>https://jja2han.tistory.com/495#entry495comment</comments>
      <pubDate>Fri, 26 Jul 2024 01:30:24 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)]</title>
      <link>https://jja2han.tistory.com/494</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot;&gt;[Android] Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/491&quot;&gt;[Android] 비디오 타임라인 이벤트 처리&lt;/a&gt;&lt;/p&gt;
&lt;a href=&quot;https://jja2han.tistory.com/493&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] - 레이어 분리 [멀티 모듈 적용기(1)]&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 멀티 모듈 적용을 위해서 의존성 방향이 정리되지 않은 Domain, Data, Presentation의 계층을 완전하게 분리했습니다.&lt;br /&gt;이번 글에서는 분리한 계층들을 하나의 App 모듈에서 여러 개의 모듈로 분리해 보는 과정과 느꼈던 점에 대해서 적어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전에 모듈에 대해서 알아볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모듈이란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈은 소스 파일 및 빌드 설정으로 구성된 모음이며, 이를 통해 프로젝트를 별개의 기능 단위로 분할할 수 있습니다.&lt;/li&gt;
&lt;li&gt;프로젝트에는 하나 이상의 모듈이 포함될 수 있습니다.&lt;/li&gt;
&lt;li&gt;하나의 모듈이 다른 모듈을 종속적으로 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;모듈의 대표적인 예로는 자동으로 생성되는 app 모듈이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 하나의 모듈을 귀찮게 여러 개로 분리하는 과정을 겪을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 모듈의 장점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관심사 분리가 가능하다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존의 단일 모듈 방식에서는 실수로 의존성 규칙을 위반할 수 있지만, 멀티 모듈 방식을 사용하면 build.gradle 파일에서 의존성을 추가하지 않는다면 다른 모듈의 코드를 사용할 수 없고, 자연스레 의존성 규칙을 쉽게 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 클린아키텍처에서의 Domain 모듈은 다른 모듈을 종속적으로 사용하고 있지 않는 것이 바람직합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Data, Presentation의 코드를 사용하는 것이 옳지 않는데, 멀티 모듈 구조를 사용할 경우 분리가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;빌드 시간 감소를 기대할 수 있다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 부분에 대해서는 크게 와닿은 부분은 없습니다. 현업자도 아니고, 보통의 신입 개발자분들이 하는 프로젝트에서 이러한 빌드 감소를 체감할만한 규모로 개발하기가 쉽지 않다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현업에서는 서비스가 큰 프로젝트에서는 100개의 모듈을 사용하는 프로젝트도 있다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 규모에서 하나의 모듈을 계속해서 빌드하는 시간보다는 변경된 모듈만 빌드해서 빌드 시간을 감소하는 것에 체감이 매우 크다고 합니다. 하지만 너무 많은 모듈이 있을 경우 관심사 분리를 잘하더라도, 복잡해지는 것은 어쩔 수 없는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 재사용성이 높아진다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 클린 아키텍처로 모듈을 분리할 경우는 코드의 재사용성이 크게 높아지는 건가?라는 생각이 들곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최근 안드로이드에서는 클린 아키텍처의 정의를 조금 다르게 하고, 기능 별로 모듈 분리를 하는 추세라 그러한 구조인 경우 코드 재사용성이 상당히 높을 것이라고 생각합니다. 해당 모듈에 대한 의존성만 추가해서 사용하면 되기 때문에 반복되는 코드를 확실히 줄일 수 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEZV2/btsIyfAkpxc/50E9ZIauyOdxSykg9ti2E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEZV2/btsIyfAkpxc/50E9ZIauyOdxSykg9ti2E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEZV2/btsIyfAkpxc/50E9ZIauyOdxSykg9ti2E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEZV2%2FbtsIyfAkpxc%2F50E9ZIauyOdxSykg9ti2E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;266&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 클린 아키텍처는 의존성 방향이 UI -&amp;gt; Domain -&amp;gt; Data로 클린아키텍처와 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 NowInAndroid에 가면 이 구조를 적용해 기능별로 모듈을 적용한 레포지토리가 있고, 그것을 참고해서 하는 것도 좋은 방법이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 모듈 단위 테스트를 진행할 수 있다는 장점이 있지만, 저는 테스트 코드를 작성하지 않아서 느끼지 못했습니다ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 스튜디오에서 모듈을 생성할 경우 File -&amp;gt; New Moudle 탭으로 이동해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pm7C9/btsIwU4ZY9F/RmsEEb6nzgKkAIs883JFDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pm7C9/btsIwU4ZY9F/RmsEEb6nzgKkAIs883JFDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pm7C9/btsIwU4ZY9F/RmsEEb6nzgKkAIs883JFDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpm7C9%2FbtsIwU4ZY9F%2FRmsEEb6nzgKkAIs883JFDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;189&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목해야 할 점은 만들 수 있는 모듈의 템플릿이 굉장히 많다는 점입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v1Z4r/btsIyu5idBF/X85KfAlEz4BQ0nduBwsoe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v1Z4r/btsIyu5idBF/X85KfAlEz4BQ0nduBwsoe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v1Z4r/btsIyu5idBF/X85KfAlEz4BQ0nduBwsoe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv1Z4r%2FbtsIyu5idBF%2FX85KfAlEz4BQ0nduBwsoe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;527&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주로 사용되는 템플릿은 Phone &amp;amp; Tablet, Android Library, Java or Kotlin Library입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 템플릿에 대응되는 모듈은 다음과 각각 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Phone &amp;amp; Tablet -&amp;gt; App 모듈&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Android Library -&amp;gt; Data, Presentation&lt;/li&gt;
&lt;li&gt;Java or Kotlin Library -&amp;gt; Domain&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈화의 핵심은 각 모듈에게 필요한 라이브러리만을 사용하게 하는 것이고, LiveData보다 Flow 사용이 선호되는 이유도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain Layer에서 LiveData를 이용해 버리면 Android 의존성을 갖기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금부터는 클린 아키텍처의 설계에 따라 의존 방향을 잘 생각해서 모듈 분리를 해볼 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presentation, Data 모듈은 Domain에 의존&lt;/li&gt;
&lt;li&gt;App 모듈은 3개 모듈 모두 의존 (의존성 주입 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 분리를 하기 전에 알아야 할 것이 또 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Version Catalog라는 친구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Version Catalog&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 안드로이드 프로젝트를 새로 시작하면 build.gradle의 문법이 바뀐 것을 확인할 수 있는데, 그것이 Version Catalog 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 공식 문서에 따르면 버전 카탈로그를 사용하면 확장 가능한 방식으로 종속 항목 및 플러그인을 추가하고 유지할 수 있다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 살펴보면 더 쉬운데요,&lt;/p&gt;
&lt;pre id=&quot;code_1720794070159&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;com.google.dagger:hilt-android:2.44&quot;)
implementation(libs.hilt.android)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 방식보다 뭐가 더 좋길래 바뀐 것일까요? 그리고 멀티 모듈을 적용한다면 Version Catalog가 각광받을까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 파일로 여러 프로젝트 및 모듈의 버전 관리를 통합할 수 있습니다.&lt;/li&gt;
&lt;li&gt;특히 멀티 모듈을 적용함에 있어 같은 의존성을 모듈마다 추가해 주는 경우가 있는데, 이 경우 하나의 파일에서 버전을 쉽게 관리할 수 있습니다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예를 들어 핵심 라이브러리의 버전이 바뀔 경우 모듈마다 이를 적용해줘야 하는데, Version Catalog를 적용하면 매우 쉽게 관리할 수 있는 장점이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;함께 사용되는 의존성들을 bundle로 묶어 가독성 좋게 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;IDE 상에서 Catalog마다 자동 완성 기능 등 편리한 요소를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S2zU1/btsIyccxMFJ/usVNI6OyeXxk3YWK1q4ypK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S2zU1/btsIyccxMFJ/usVNI6OyeXxk3YWK1q4ypK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S2zU1/btsIyccxMFJ/usVNI6OyeXxk3YWK1q4ypK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS2zU1%2FbtsIyccxMFJ%2FusVNI6OyeXxk3YWK1q4ypK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;489&quot; height=&quot;135&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Version Catalog를 적용하기 위해선 gradle 폴더 아래에 toml 파일을 생성해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b80kIp/btsIy1O4WMd/OA5CqVJaSveUabVqcGH99k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b80kIp/btsIy1O4WMd/OA5CqVJaSveUabVqcGH99k/img.png&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;85&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;74.07&quot; style=&quot;width: 73.2072%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b80kIp/btsIy1O4WMd/OA5CqVJaSveUabVqcGH99k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb80kIp%2FbtsIy1O4WMd%2FOA5CqVJaSveUabVqcGH99k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;206&quot; height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v3GM3/btsIyeVJqeq/EZiJkjkRYZ5eX2OT2FjPUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v3GM3/btsIyeVJqeq/EZiJkjkRYZ5eX2OT2FjPUk/img.png&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;264&quot; data-is-animation=&quot;false&quot; style=&quot;width: 25.63%;&quot; data-widthpercent=&quot;25.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v3GM3/btsIyeVJqeq/EZiJkjkRYZ5eX2OT2FjPUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv3GM3%2FbtsIyeVJqeq%2FEZiJkjkRYZ5eX2OT2FjPUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;224&quot; height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toml 파일은 다음과 같이 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;versions : 라이브러리 버전에 대한 변수명과 같은 값을 정의&lt;/li&gt;
&lt;li&gt;libraries : 라이브러리 정의&lt;/li&gt;
&lt;li&gt;plugins : 플러그인 정의&lt;/li&gt;
&lt;li&gt;bundles : 연관 있는 라이브러리끼리 묶어 그룹핑&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 version catalog가 아닌 이전 방식을 택하고 있다면 NowInAndroid의 libs.version.toml 파일을 참고해 변경하시는 것을 추천합니다. 해당 파일은 compose를 기반으로 설명하고 있지만, 보시면 조금 감이 오실 거라 생각 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만든 toml 파일을 gradle에 적용해 보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwanjP/btsIydCvYHd/K6W63b1sImBofVsZX3fhB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwanjP/btsIydCvYHd/K6W63b1sImBofVsZX3fhB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwanjP/btsIydCvYHd/K6W63b1sImBofVsZX3fhB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwanjP%2FbtsIydCvYHd%2FK6W63b1sImBofVsZX3fhB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;179&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 Data Moudle에 해당되는데, bundle로 여러 라이브러리를 한꺼번에 그리고 명시적으로 표현해 깔끔해진 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 gradle 버전이 7.4 이하라면 toml 파일의 경로를 명시해줘야 하는데, settings.gradle.kts 파일에 다음과 같은 코드를 추가해 주세요&lt;/p&gt;
&lt;pre id=&quot;code_1720794580136&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enableFeaturePreview(&quot;VERSION_CATALOGS&quot;)

dependencyResolutionManagement {
    versionCatalogs {
        create(&quot;fileName&quot;) {
            from(files(&quot;fileName.version.toml&quot;))
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 구조에서 version catalog로 마이그레이션 하는 과정에서 굉장히 많은 오류가 있을 것이라고 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 오류가 중복된 라이브러리가 포함되어 생긴 오류였고, 그러한 경우 라이브러리에 대한 주입을 잘 확인하시는 것을 추천드립니다 ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈을 적용하고, version catalog를 도입해 보면서 느꼈던 점은 해당 방식이 편하다! 꼭 해보자!라는 감정보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 설계 방식을 변경하는 것이 매우 매우 힘들고, 어렵다라는 것을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 모듈에서 여러 계층의 코드를 깔끔하게 분리를 하고, 또 그 분리된 패키지들을 모듈로 적용하는 것에 꼬박 하루를 넘게 사용한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직도 클린 아키텍처 구조를 반드시 따르고, 멀티 모듈을 적용해야 하냐?라고 하면 선뜻 대답하기 힘든 것 같습니다.(제 수준에서는요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현업에서도 멀티 모듈을 적용하는 것이 당연하고, 이번 경험을 통해 코드를 수정하는 작업이 굉장히 힘든 것을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 안드로이드의 클린 아키텍처이냐,&amp;nbsp; 로버트 마틴의 클린 아키텍처이냐에 대한 의견은 사실 자기가 편한 대로 하는 것이 정답이 아닐까..라는 생각을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 이미 진행 중인 프로젝트에서 클린 아키텍처를 적용하는데 있어서 분리하기 힘들다고 판단된 것이 안드로이드 클린 아키텍쳐여서 클린 아키텍쳐를 선택한 감이 없지 않아 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 클린 아키텍처는 Feature 중심 모듈화이고, Feature 별 사용되는 drawable이 분리돼있는 것을 보고 경악을 했습니다ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 제가 공부를 한 개인적인 의견이기 때문에 정답이 아닙니다. 그 점 참고해 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@purpledev/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brunch.co.kr/@purpledev/43&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720795020283&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;안드로이드 멀티모듈 도입기&quot; data-og-description=&quot;온보딩 프로젝트를 통한 안드로이드 멀티모듈 도입기 | 안녕하세요 서비스 개발팀 Android 신입 개발자 Stephen 입니다. Intro. 저는 현재 안드로이드 개발을 할 때 멀티 모듈 방식을 사용하고 있으며&quot; data-og-host=&quot;brunch.co.kr&quot; data-og-source-url=&quot;https://brunch.co.kr/@purpledev/43&quot; data-og-url=&quot;https://brunch.co.kr/@purpledev/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8KlvX/hyWzqgbKoR/E6zWk8cJJZAzYwTYPoB6yk/img.png?width=733&amp;amp;height=472&amp;amp;face=0_0_733_472,https://scrap.kakaocdn.net/dn/br9oJs/hyWzqtJ1hh/NZAi6cm0PeiQesQXKphoUk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/cFs7NJ/hyWzubPyQb/aSG68rsNcTpKh3kd2lIYD0/img.jpg?width=1280&amp;amp;height=826&amp;amp;face=0_0_1280_826&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@purpledev/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://brunch.co.kr/@purpledev/43&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8KlvX/hyWzqgbKoR/E6zWk8cJJZAzYwTYPoB6yk/img.png?width=733&amp;amp;height=472&amp;amp;face=0_0_733_472,https://scrap.kakaocdn.net/dn/br9oJs/hyWzqtJ1hh/NZAi6cm0PeiQesQXKphoUk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/cFs7NJ/hyWzubPyQb/aSG68rsNcTpKh3kd2lIYD0/img.jpg?width=1280&amp;amp;height=826&amp;amp;face=0_0_1280_826');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 멀티모듈 도입기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;온보딩 프로젝트를 통한 안드로이드 멀티모듈 도입기 | 안녕하세요 서비스 개발팀 Android 신입 개발자 Stephen 입니다. Intro. 저는 현재 안드로이드 개발을 할 때 멀티 모듈 방식을 사용하고 있으며&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;brunch.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://everyday-develop-myself.tistory.com/309&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://everyday-develop-myself.tistory.com/309&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720795027913&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (1)&quot; data-og-description=&quot;도대체 이놈의 클린 아키텍처가 뭐길래 이렇게 저를 괴롭히는지 모르겠습니다. 클린 아키텍처를 처음 접하고, 이것이 뭔지 이해하기 까지도 시간이 많이 걸렸습니다. 그럼에도 아직 잘 모르는 &quot; data-og-host=&quot;everyday-develop-myself.tistory.com&quot; data-og-source-url=&quot;https://everyday-develop-myself.tistory.com/309&quot; data-og-url=&quot;https://everyday-develop-myself.tistory.com/309&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EyQ4x/hyWzuJEm4E/fHrN13K9L5Z6ZrtH28G5m0/img.jpg?width=772&amp;amp;height=567&amp;amp;face=0_0_772_567,https://scrap.kakaocdn.net/dn/b0sk9D/hyWzxzDGSA/0tvi4KUKeiM5H0Sc9Lro51/img.jpg?width=772&amp;amp;height=567&amp;amp;face=0_0_772_567,https://scrap.kakaocdn.net/dn/ldAzh/hyWzyL4YKi/gbSgOKJl8USUvubqY4Ak51/img.png?width=1993&amp;amp;height=1444&amp;amp;face=0_0_1993_1444&quot;&gt;&lt;a href=&quot;https://everyday-develop-myself.tistory.com/309&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://everyday-develop-myself.tistory.com/309&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EyQ4x/hyWzuJEm4E/fHrN13K9L5Z6ZrtH28G5m0/img.jpg?width=772&amp;amp;height=567&amp;amp;face=0_0_772_567,https://scrap.kakaocdn.net/dn/b0sk9D/hyWzxzDGSA/0tvi4KUKeiM5H0Sc9Lro51/img.jpg?width=772&amp;amp;height=567&amp;amp;face=0_0_772_567,https://scrap.kakaocdn.net/dn/ldAzh/hyWzyL4YKi/gbSgOKJl8USUvubqY4Ak51/img.png?width=1993&amp;amp;height=1444&amp;amp;face=0_0_1993_1444');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도대체 이놈의 클린 아키텍처가 뭐길래 이렇게 저를 괴롭히는지 모르겠습니다. 클린 아키텍처를 처음 접하고, 이것이 뭔지 이해하기 까지도 시간이 많이 걸렸습니다. 그럼에도 아직 잘 모르는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;everyday-develop-myself.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90?category=538292&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90?category=538292&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720795035986&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[안드로이드] 클린 아키텍처 - (1) 개념&quot; data-og-description=&quot;오늘의 주제는 로버튼 마틴 형님의 &amp;quot;클린 아키텍처&amp;quot;라는 개념이다. 많은 기업들이 해당 아키텍처 구조를 사용하고 있으며, 비즈니스 로직 덩어리가 클수록 장점이 부각되는 구조이다. 하지만 기&quot; data-og-host=&quot;work2type.tistory.com&quot; data-og-source-url=&quot;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90?category=538292&quot; data-og-url=&quot;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WbBwO/hyWzEFx8T4/jhUcMvWMddN3pbgciOTEG1/img.png?width=800&amp;amp;height=587&amp;amp;face=0_0_800_587,https://scrap.kakaocdn.net/dn/d76HCe/hyWzsLQ8OU/zM6CJayBTFWok6DDqHkT91/img.png?width=800&amp;amp;height=587&amp;amp;face=0_0_800_587,https://scrap.kakaocdn.net/dn/iHeKW/hyWzsSBzSw/aKTPLyk94jAlTYo76jSOYk/img.png?width=750&amp;amp;height=551&amp;amp;face=0_0_750_551&quot;&gt;&lt;a href=&quot;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90?category=538292&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://work2type.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-%EA%B0%9C%EB%85%90?category=538292&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WbBwO/hyWzEFx8T4/jhUcMvWMddN3pbgciOTEG1/img.png?width=800&amp;amp;height=587&amp;amp;face=0_0_800_587,https://scrap.kakaocdn.net/dn/d76HCe/hyWzsLQ8OU/zM6CJayBTFWok6DDqHkT91/img.png?width=800&amp;amp;height=587&amp;amp;face=0_0_800_587,https://scrap.kakaocdn.net/dn/iHeKW/hyWzsSBzSw/aKTPLyk94jAlTYo76jSOYk/img.png?width=750&amp;amp;height=551&amp;amp;face=0_0_750_551');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[안드로이드] 클린 아키텍처 - (1) 개념&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오늘의 주제는 로버튼 마틴 형님의 &quot;클린 아키텍처&quot;라는 개념이다. 많은 기업들이 해당 아키텍처 구조를 사용하고 있으며, 비즈니스 로직 덩어리가 클수록 장점이 부각되는 구조이다. 하지만 기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;work2type.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@ilil1/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@ilil1/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720795045394&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;안드로이드에서 클린 아키텍처란?&quot; data-og-description=&quot;1. 로버트 마틴(Robert C. Martin) 클린 아키텍쳐 vs Google의 앱 아키텍쳐 2. 클린 아키텍처란? 3. 실무 프로덕트에 적용된 클린아키텍처&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@ilil1/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80&quot; data-og-url=&quot;https://velog.io/@ilil1/안드로이드에서-클린-아키텍처란&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lV9YL/hyWzo3Ne9L/4MN53noUfT4PRvptI74yQ0/img.png?width=1000&amp;amp;height=417&amp;amp;face=0_0_1000_417,https://scrap.kakaocdn.net/dn/bfLvT5/hyWzBB2P4S/dobT5HyRvGg7QuivXp2gB1/img.png?width=1000&amp;amp;height=417&amp;amp;face=0_0_1000_417,https://scrap.kakaocdn.net/dn/cW4LtE/hyWzskLD6e/3WRruC72wkWo3eer52QmPk/img.jpg?width=5184&amp;amp;height=3888&amp;amp;face=0_0_5184_3888&quot;&gt;&lt;a href=&quot;https://velog.io/@ilil1/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@ilil1/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lV9YL/hyWzo3Ne9L/4MN53noUfT4PRvptI74yQ0/img.png?width=1000&amp;amp;height=417&amp;amp;face=0_0_1000_417,https://scrap.kakaocdn.net/dn/bfLvT5/hyWzBB2P4S/dobT5HyRvGg7QuivXp2gB1/img.png?width=1000&amp;amp;height=417&amp;amp;face=0_0_1000_417,https://scrap.kakaocdn.net/dn/cW4LtE/hyWzskLD6e/3WRruC72wkWo3eer52QmPk/img.jpg?width=5184&amp;amp;height=3888&amp;amp;face=0_0_5184_3888');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드에서 클린 아키텍처란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 로버트 마틴(Robert C. Martin) 클린 아키텍쳐 vs Google의 앱 아키텍쳐 2. 클린 아키텍처란? 3. 실무 프로덕트에 적용된 클린아키텍처&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@purpledev/46&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brunch.co.kr/@purpledev/46&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720795058390&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Version Catalog 도입을 위한 온보딩&quot; data-og-description=&quot;gradle 버전 관리를 위한 Version Catalog 도입 | 안녕하세요. 서비스 개발팀 Android 개발자 두루라고 합니다. 최근 프로젝트에 도입하려고 하는 gradle 버전 관리를 위한 Version Catalog&amp;nbsp;온보딩 내용에 대해&quot; data-og-host=&quot;brunch.co.kr&quot; data-og-source-url=&quot;https://brunch.co.kr/@purpledev/46&quot; data-og-url=&quot;https://brunch.co.kr/@purpledev/46&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kddLD/hyWzBPy9Y2/6LOQGYwgx2inpuyg1caAg1/img.png?width=1280&amp;amp;height=347&amp;amp;face=0_0_1280_347,https://scrap.kakaocdn.net/dn/bcfO3Z/hyWzC8MwLr/jkkuj8TJ3iQAS1qSAcWYS1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/molZi/hyWzCAU6pE/zqgda2dA7MBbMHKzwkDInK/img.png?width=1280&amp;amp;height=884&amp;amp;face=0_0_1280_884&quot;&gt;&lt;a href=&quot;https://brunch.co.kr/@purpledev/46&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://brunch.co.kr/@purpledev/46&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kddLD/hyWzBPy9Y2/6LOQGYwgx2inpuyg1caAg1/img.png?width=1280&amp;amp;height=347&amp;amp;face=0_0_1280_347,https://scrap.kakaocdn.net/dn/bcfO3Z/hyWzC8MwLr/jkkuj8TJ3iQAS1qSAcWYS1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/molZi/hyWzCAU6pE/zqgda2dA7MBbMHKzwkDInK/img.png?width=1280&amp;amp;height=884&amp;amp;face=0_0_1280_884');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Version Catalog 도입을 위한 온보딩&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;gradle 버전 관리를 위한 Version Catalog 도입 | 안녕하세요. 서비스 개발팀 Android 개발자 두루라고 합니다. 최근 프로젝트에 도입하려고 하는 gradle 버전 관리를 위한 Version Catalog&amp;nbsp;온보딩 내용에 대해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;brunch.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>version catalog</category>
      <category>멀티 모듈</category>
      <category>클린 아키텍쳐</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/494</guid>
      <comments>https://jja2han.tistory.com/494#entry494comment</comments>
      <pubDate>Fri, 12 Jul 2024 23:38:56 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - 레이어 분리 [멀티 모듈 적용기(1)]</title>
      <link>https://jja2han.tistory.com/493</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot;&gt;[Android] Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/491&quot;&gt;[Android] 비디오 타임라인 이벤트 처리&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 프로젝트의 코드를 보거나, 이력서를 보게 되면 클린 아키텍처 설계 경험, 혹은 멀티 모듈이라는 말이 등장하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈을 얘기하기 전 클란 아키텍쳐에 대해 먼저 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분이 알고 있는 클린 아키텍쳐는 로버트 마틴의 클린 아키텍처일 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbHblL/btsIpJaNjHn/3Mxt792K1iuK1Kk8ELC6JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbHblL/btsIpJaNjHn/3Mxt792K1iuK1Kk8ELC6JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbHblL/btsIpJaNjHn/3Mxt792K1iuK1Kk8ELC6JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbHblL%2FbtsIpJaNjHn%2F3Mxt792K1iuK1Kk8ELC6JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;478&quot; height=&quot;351&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처를 읽지 않아도, 적용해 본 적이 없어도 클린 코드와 더불어 한번쯤은 들어봤을 법한 단어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처에서의 핵심은 의존성의 방향이 단방향이라는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 빗대어 얘기하자면 내부의 원은 외부의 원에 대해 알지 못하는 규칙을 클린 아키텍처에서는 강조하고 있고, 이를 의존성 규칙을 지켜야 한다고 표현하곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처의 각 요소(Entity, UseCase, Controller, GateWays, Presenter)등에 대해 저보다 자세하게, 효과적으로 다룬 글들은 많다고 생각하기 때문에 설명은 생략하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 클린 아키텍처에서 강조하는 의존성 규칙이 왜 중요한 것일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개발을 하는 누구나 아키텍쳐 설계의 중요성을 알고, 클래스 간의 결합도와 의존도를 신경 쓰면서 개발해야 하는 것은 알고 있다고 하지만, 지키기가 굉장히 힘들 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 의존성 규칙을 지켜 관심사를 분리한다면 좋은 점이 무엇인지 모른 채, &quot;클린 아키텍처가 좋대, 요즘 유명하대&quot;라는 생각을 시작으로 계층을 분리하고, 관심사를 분리하곤 합니다. (저도 그래왔습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 규모 있는 프로젝트를 진행한 경험이 없고, 고차원의 테스트 코드를 작성한 경험이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 제가 생각하는 의존성 규칙을 지켜 관심사를 분리할 경우의 이점은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;협업할 때 효과적이다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사를 분리할 경우 팀원들에게 작업을 분배하기 쉽고, 팀원들이 독립적인 작업을 진행하므로, 깃에서 충돌이 발생할 가능성이 줄어듭니다. 실제 겪은 경험으로 관심사를 분리하지 않고, 팀원들과 작업을 진행하다 각자가 작업한 코드가 서로 영향을 발생시켰고, 이를 해결하기 위해 굉장히 많은 고생을 했었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드를 이해할 때 효과적이다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 관심사 별로 분리되어 있어, 해당 코드가 무엇을 말하고자 하는지 단번에 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람별로 코드를 작성하는 스타일은 천차만별이고, 이러한 간격을 최대한 줄이고자 컨벤션과 코드 리뷰를 도입한다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그래도 다른 사람의 코드를 이해하는 일은 여전히 어렵다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 프로젝트를 진행하면서 팀원의 작업을 이어서 진행한 경험이 있는데, 기능의 맥락을 파악하지 못했고, 코드의 범위를 파악하는 것이 어려워서 많은 도움을 요청했던 경험이 있습니다. 하지만 관심사가 분리된다면, 이러한 비용을 줄일 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행하고 있는 프로젝트에서 클린 아키텍처를 적용하기 위해 많은 고생을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성의 방향을 무시한 채 덕지덕지 작성했던 코드들과 반창고를 붙였던 부분들을 모두 도려내야 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DbJu8/btsIp9tzteu/OyWuj8gzqnCK7ckZpzlGYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DbJu8/btsIp9tzteu/OyWuj8gzqnCK7ckZpzlGYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DbJu8/btsIp9tzteu/OyWuj8gzqnCK7ckZpzlGYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDbJu8%2FbtsIp9tzteu%2FOyWuj8gzqnCK7ckZpzlGYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;384&quot; height=&quot;268&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 현재 프로젝트 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지로 계층을 분리했지만, 계층 간의 의존성 방향이 엉망진창이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 하면서 신경 썼던 부분은 의존성 방향에 맞게 각 레이어가 동작을 하는가, 해당 레이어에서 불필요한 라이브러리는 사용되지 않았는가를 확인하는 것이었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Parcelable -&amp;gt; Serializable&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx9soB/btsIpvRHNuG/tH3zGWL9NQDeEvuJoHWJhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx9soB/btsIpvRHNuG/tH3zGWL9NQDeEvuJoHWJhK/img.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;1182&quot; data-is-animation=&quot;false&quot; width=&quot;291&quot; height=&quot;500&quot; style=&quot;width: 46.05%; margin-right: 10px;&quot; data-widthpercent=&quot;46.59&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx9soB/btsIpvRHNuG/tH3zGWL9NQDeEvuJoHWJhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx9soB%2FbtsIpvRHNuG%2FtH3zGWL9NQDeEvuJoHWJhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;1182&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRoF3i/btsIrwOQRWK/xJIMnOlhYMMaAavcI7zurk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRoF3i/btsIrwOQRWK/xJIMnOlhYMMaAavcI7zurk/img.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1202&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.7872%;&quot; data-widthpercent=&quot;53.41&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRoF3i/btsIrwOQRWK/xJIMnOlhYMMaAavcI7zurk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRoF3i%2FbtsIrwOQRWK%2FxJIMnOlhYMMaAavcI7zurk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Domain 계층에서의 Post입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain 계층에서 최대한 Kotlin &amp;amp; Java 라이브러리만 사용하는 것이 맞다고 생각했기에 해당 부분을 바꿔줘야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android의 Parcelable이 아닌 Kotlin의 Serializable을 사용해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 직렬화하는 이유는 여러 가지가 있지만, intent에 Int, String과 같은 인자를 전달하는 것이 아닌 객체 그 자체를 전달하기 위해서 Android에서 직렬화를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parcelable은 intent나 네비게이션으로 인자를 전달할 때 객체 그 자체를 전달할 수 있도록 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 argType이 객체가 될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;// 네비게이션 그래프
&amp;lt;dialog
    &amp;lt;argument
        android:name=&quot;postList&quot;
        app:argType=&quot;com.mimo.android.domain.model.PostData[]&quot;/&amp;gt;

    &amp;lt;argument
        android:name=&quot;clusterPostList&quot;
        app:argType=&quot;com.mimo.android.domain.model.PostData[]&quot;/&amp;gt;

    &amp;lt;argument
        android:name=&quot;address&quot;
        app:argType=&quot;string&quot;/&amp;gt;
&amp;lt;/dialog&amp;gt;

// intent로 전달할 때
bundleOf(
    &quot;postList&quot; to postList.toTypedArray(),
    &quot;clusterPostList&quot; to clusterPostList.toTypedArray(),
    &quot;address&quot; to address,
),&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Serializable은 객체를 String으로 직렬화해서 전달하고, 수신 측에서는 역직렬화를 통해서 객체로 다시 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 네비게이션 그래프
&amp;lt;dialog
    &amp;lt;argument
        android:name=&quot;postList&quot;
        app:argType=&quot;string&quot;/&amp;gt;

    &amp;lt;argument
        android:name=&quot;clusterPostList&quot;
        app:argType=&quot;string&quot;/&amp;gt;

    &amp;lt;argument
        android:name=&quot;address&quot;
        app:argType=&quot;string&quot;/&amp;gt;
&amp;lt;/dialog&amp;gt;

// intent로 전달할 때
val data = (mapViewModel.postState.value as UiState.Success&amp;lt;List&amp;lt;Post&amp;gt;&amp;gt;).data
val postList = Json.encodeToString(data)
intent.putExtra(&quot;postList&quot;,postList)

// intent로 전달 받을 때
val postIndex = intent.getIntExtra(&quot;postIndex&quot;, -1)
val postList: List&amp;lt;Post&amp;gt; = intent.getStringExtra(&quot;postList&quot;)?.let {
    Json.decodeFromString(it)
} ?: emptyList()&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MarkerData 수정하기&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package com.mimo.android.domain.model

import android.os.Parcelable
import com.mimo.android.data.model.response.MarkerResponse
import com.naver.maps.geometry.LatLng
import com.naver.maps.map.clustering.ClusteringKey
import kotlinx.parcelize.Parcelize

@Parcelize
data class MarkerData(
    val id: Int,
    val latitude: Double,
    val longitude: Double,
    val postId: Int,
) : ClusteringKey, Parcelable {
    override fun getPosition(): LatLng = LatLng(latitude, longitude)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain Layer에서 MarkerData가 네이버 map과 의존하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했던 것처럼 Domain Layer는 Kotlin &amp;amp; Java 라이브러리만 사용하고 싶었기 때문에 변경해줘야 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjjEI7/btsIpXfPOh1/qhe7UiPLbX5MnQGMQrdxR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjjEI7/btsIpXfPOh1/qhe7UiPLbX5MnQGMQrdxR1/img.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot; style=&quot;width: 58.2103%; margin-right: 10px;&quot; data-widthpercent=&quot;58.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjjEI7/btsIpXfPOh1/qhe7UiPLbX5MnQGMQrdxR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjjEI7%2FbtsIpXfPOh1%2Fqhe7UiPLbX5MnQGMQrdxR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEfAvO/btsIqdJuAEq/W7xmKMOKW3XkSvuccu0nok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEfAvO/btsIqdJuAEq/W7xmKMOKW3XkSvuccu0nok/img.png&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;1354&quot; data-is-animation=&quot;false&quot; style=&quot;width: 40.6269%;&quot; data-widthpercent=&quot;41.1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEfAvO/btsIqdJuAEq/W7xmKMOKW3XkSvuccu0nok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEfAvO%2FbtsIqdJuAEq%2FW7xmKMOKW3XkSvuccu0nok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1290&quot; height=&quot;1354&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain에서는 순수한 정보인 MarkerData를 사용하고, UI에서 domain의 MarkerData를 기반으로 ClusteringKey를 상속받는 MapMarkerData로 변환하는 함수를 통해 UI에서 사용하도록 했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음글은 분리한 레이어를 바탕으로 멀티 모듈을 적용해 볼 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발이 어느 정도 진행된 단계에서 레이어를 분리하는 과정에서 작성했던 코드들의 비참함을 확인할 수 있는 시간이었던 것 같습니다.&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>레이어 분리</category>
      <category>멀티 모듈</category>
      <category>클린 아키텍쳐</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/493</guid>
      <comments>https://jja2han.tistory.com/493#entry493comment</comments>
      <pubDate>Sun, 7 Jul 2024 19:30:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 커스텀 컨트롤러 구현 및 동영상 재생 관리</title>
      <link>https://jja2han.tistory.com/492</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 비디오 타임라인 이벤트 처리&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 비디오 타임라인에 대해 시간대를 제공하고, 동영상 시점을 변화시켰습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번글에서는 갤러리 앱처럼 편집한 구간에 대해 재생할 수 있는 컨트롤러를 적용해 볼 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447652407&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/csMtdk/hyWrQrSK3B/kc5UdAP5kVIBib2K8pyKRk/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/c41u0H/hyWoN4DPEG/ekX7gGQZFFIeOSeowv1AUK/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;785&quot; data-video-height=&quot;1920&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447652407?service=daum_tistory&quot; width=&quot;785&quot; height=&quot;1920&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능 요구사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클립의 구간을 벗어날 수 없다.&lt;/li&gt;
&lt;li&gt;재생을 하는 도중 타임라인이 변경될 경우 동영상 재생을 정지한다.&lt;/li&gt;
&lt;li&gt;동영상이 재생하는 동안 타임라인도 같이 이동한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해선 동영상의 재생을 관리하는 컨트롤러가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 ExoPlayer에서 컨트롤러를 제공하고, 이를 사용하기 위해선 다음과 같은 코드 설정이 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719149296625&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app:use_controller=&quot;true&quot; //XML PlayerView&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rO2IF/btsH88hkrWC/g8lfuwEbAFFhnrDmVQET4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rO2IF/btsH88hkrWC/g8lfuwEbAFFhnrDmVQET4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rO2IF/btsH88hkrWC/g8lfuwEbAFFhnrDmVQET4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrO2IF%2FbtsH88hkrWC%2Fg8lfuwEbAFFhnrDmVQET4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;484&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 컨트롤러가 Exoplayer의 기본적인 컨트롤러 UI입니다.&lt;br /&gt;편집된 동영상의 재생을 관리하기 위해선 UI가 과도하게 구현되었다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재생과 정지 버튼만 있으면 되기 때문이죠. 따라서 커스텀 컨트롤러를 구현해보기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 컨트롤러 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExoPlayer의 컨트롤러를 커스텀하는 방법은 공식문서에 친절하게 나와있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719149442028&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ExoPlayer를 사용한 미디어 스트리밍 &amp;nbsp;|&amp;nbsp; Android media &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 Codelab에서는 Android YouTube 앱에서 실행되는 오픈소스 미디어 플레이어인 ExoPlayer를 사용하여 미디어 플레이어를 빌드하여 오디오 및 가변 품질 동영상 스트림을 렌더링합니다. Codelab에서는 라&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6&quot; data-og-url=&quot;https://developer.android.com/codelabs/exoplayer-intro?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mG3XG/hyWoMEFhWW/MJBq8517yt5W7kZkaPWTRk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/Ns6pP/hyWoMdzYiH/dUczAsR9zojSyCKXIC2s91/img.png?width=1370&amp;amp;height=2534&amp;amp;face=0_0_1370_2534&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mG3XG/hyWoMEFhWW/MJBq8517yt5W7kZkaPWTRk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/Ns6pP/hyWoMdzYiH/dUczAsR9zojSyCKXIC2s91/img.png?width=1370&amp;amp;height=2534&amp;amp;face=0_0_1370_2534');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ExoPlayer를 사용한 미디어 스트리밍 &amp;nbsp;|&amp;nbsp; Android media &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 Codelab에서는 Android YouTube 앱에서 실행되는 오픈소스 미디어 플레이어인 ExoPlayer를 사용하여 미디어 플레이어를 빌드하여 오디오 및 가변 품질 동영상 스트림을 렌더링합니다. Codelab에서는 라&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 커스텀할 컨트롤러의 UI를 구현해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 컨트롤러 UI 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 간단하게 재생버튼과 동영상의 현재 시간/최대 시간을 표현하는 UI를 만들어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExoPlayer에서 커스텀 컨트롤러를 구현하기 위한 편의성을 굉장히 많이 제공해주기 때문에 정말 간단합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719149518448&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;LinearLayout
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_gravity=&quot;center|bottom&quot;
        android:gravity=&quot;center&quot;
        android:orientation=&quot;horizontal&quot;
        android:background=&quot;@drawable/rectangle_fill_light_purple_radius_40&quot;
        android:layout_marginBottom=&quot;5dp&quot;
        android:paddingEnd=&quot;5dp&quot;
        &amp;gt;

        &amp;lt;ImageButton
            android:id=&quot;@id/exo_play_pause&quot;
            style=&quot;@style/ExoMediaButton.Play&quot;
            android:layout_width=&quot;20dp&quot;
            android:layout_height=&quot;20dp&quot;
            android:scaleType=&quot;fitCenter&quot;
            app:tint=&quot;@color/black&quot; /&amp;gt;

        &amp;lt;TextView
            android:id=&quot;@id/exo_position&quot;
            style=&quot;@style/Typography.Body03.Medium&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:textColor=&quot;@color/black&quot;/&amp;gt;

        &amp;lt;TextView
            android:id=&quot;@+id/tv_divide_time&quot;
            style=&quot;@style/Typography.Body03.Medium&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;/&quot;
            android:textColor=&quot;@color/black&quot; /&amp;gt;

        &amp;lt;TextView
            android:id=&quot;@id/exo_duration&quot;
            style=&quot;@style/Typography.Body03.Medium&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:textColor=&quot;@color/black&quot;/&amp;gt;
    &amp;lt;/LinearLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 과정에서 특이했던 점은 &lt;b&gt;컴포넌트에 대한 id와 style만 지정해주면 제 기능을 한다는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 시간을 의미하는 exo_postion&lt;/li&gt;
&lt;li&gt;동영상 길이를 의미하는 exo_duration&lt;/li&gt;
&lt;li&gt;버튼을 눌렀을때 정지, 누르지 않을 경우 재생을 나타내는 버튼은 exo_player_pause&lt;/li&gt;
&lt;li&gt;버튼 이미지를 의미하는 @style/ExoMediaButton.play&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 id와 style을 지정하면 각각의 역할을 하고, 값이 매핑됩니다.&lt;br /&gt;정말 신기합니다.(위 TextView에 style은 제가 설정한 font이므로 무시하셔도 됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 속성도 굉장히 많이 제공되고 있고, 필요에 따라 컨트롤러를 커스텀할 수 있는 것은 ExoPlayer의 장점인 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t05Kv/btsH8wDhgJm/oFJaKV1HhabQbYfhlaBXM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t05Kv/btsH8wDhgJm/oFJaKV1HhabQbYfhlaBXM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t05Kv/btsH8wDhgJm/oFJaKV1HhabQbYfhlaBXM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft05Kv%2FbtsH8wDhgJm%2FoFJaKV1HhabQbYfhlaBXM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;401&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExoPlayer에서 제공되는 것들중 일부입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 이 UI로 컨트롤러를 쓸거야~라고 UI를 지정해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719149812669&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app:controller_layout_id=&quot;@layout/custom_player_controller&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIAdXW/btsH8UKpOe9/Fy8i44OuUJoCRoLOZft361/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIAdXW/btsH8UKpOe9/Fy8i44OuUJoCRoLOZft361/img.png&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;388&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.8652%; margin-right: 10px;&quot; data-widthpercent=&quot;53.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIAdXW/btsH8UKpOe9/Fy8i44OuUJoCRoLOZft361/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIAdXW%2FbtsH8UKpOe9%2FFy8i44OuUJoCRoLOZft361%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c91PwP/btsH94rCG3N/yakWaTAlTU6Anl8WJmZzv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c91PwP/btsH94rCG3N/yakWaTAlTU6Anl8WJmZzv0/img.png&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;524&quot; data-is-animation=&quot;false&quot; style=&quot;width: 45.972%;&quot; data-widthpercent=&quot;46.51&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c91PwP/btsH94rCG3N/yakWaTAlTU6Anl8WJmZzv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc91PwP%2FbtsH94rCG3N%2FyakWaTAlTU6Anl8WJmZzv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러가 적용됨을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 재생버튼과 정지버튼을 스위치처럼 사용했는데 만약 따로 두고 싶다면 play_pause가 아닌 play, pause를 따로 배치하면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재생 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 해결해야 할 점은 어떻게 재생을 함과 동시에 타임라인을 이동시킬 수 있는가입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 2가지 방법을 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Slider의 value를 증가시켜, ExoPlayer의 시점을 옮긴다.&lt;/li&gt;
&lt;li&gt;ExoPlayer를 재생시키고, 재생 시점을 Slider의 Value와 일치시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 동영상 재생 관리는 ExoPlayer가 하는 것이 맞기 때문에 1번이 아닌 2번을 택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 방법을 선택하면 Slider의 변화 요인에 따라 ExoPlayer가 이동하고, Slider의 변화요인은 다양하기에 오류가 생긴다면 특정하기 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExoPlayer 재생 상태 관찰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExoPlayer의 재생 상태를 계속해서 관찰해야 했기에, 다음과 같은 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719150198764&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;exoPlayer.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        if (isPlaying) {
            startTracking()
        } else {
            stopTracking()
        }
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수는 exoPlayer의 상태를 관찰하는 함수로, isPlaying이라는 Boolean 값을 통해 재생 중인지, 아닌지를 판단해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상태에 따라 타임라인을 이동하는 함수와 그 이동을 중지하는 함수를 구현해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719150283149&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun startTracking() {
    trackingJob = lifecycleScope.launch {
        while (true) {
            trackingVideo()
            delay(100)
        }
    }
}

private fun trackingVideo() {
    player?.let { player -&amp;gt;
        val currentPosition = player.currentPosition
        val duration = player.duration
        if (duration &amp;gt; 0) {
            val positionRatio = currentPosition.toFloat() / duration * 100
            binding.sliderVideoTime.value = positionRatio
        }
    }
}

private fun stopTracking() {
    trackingJob?.cancel()
    trackingJob = null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상이 재생되는 동안은 계속해서 트래킹을 지시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래킹의 작업은 동영상의 길이대비 현재 재생시점을 영상 타임라인의 value로 일치화하는 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 동영상 재생이 정지된다면 트래킹 작업을 취소하고, null로 해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 동영상이 재생되면 동영상 재생의 시점을 들고 와서 전체 길이에 대한 포지션을 계산하고 Slider의 Value와 일치화한다고 이해해 주시면 됩니다. [ 동영상 재생 -&amp;gt; 재생 시점 변화 -&amp;gt; 슬라이더 적용 ]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447652895&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/2CLSv/hyWoARMupD/igYJqfkllzLLkckzCrDA1K/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/mOcjf/hyWrWFD3zx/myUWakR2fUOdzdnb2YsXk0/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;785&quot; data-video-height=&quot;1920&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447652895?service=daum_tistory&quot; width=&quot;785&quot; height=&quot;1920&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임라인 Value 겹침 문제 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현을 하고 보니 오류가 발생했다.&lt;br /&gt;기존 작성했던 터치로 인한 타임 라인의 이동과 동영상 재생의 이벤트 처리가 겹쳐서 재생이 되지 않았습니다.&lt;br /&gt;기존 코드는 타임 라인을 터치해서 value가 변화할 때마다 해당 값으로 exoPlayer의 포지션을 옮겨줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 동영상이 재생됨에 있어서 타임라인의 value가 변화하니 동영상이 제자리 재생되는 현상이 발생한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 동영상 재생 -&amp;gt; 슬라이더 값 변화 -&amp;gt; 동영상 시점 변화의 사이클이 무한 반복이라 동영상이 재생이 되지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 슬라이더의 Value 변화를 구별할 수 있는 flag값이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상 재생으로 인한 슬라이더의 변화와 유저 터치로 인한 슬라이더 변화를 구별하기 위한 flag값이 필요했는데,&lt;br /&gt;감사하게도 Slider의 onChangedListener에 fromUser라는 flag가 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZMLoj/btsH972YpsS/H3XyikODrRkLXWpn5WYJqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZMLoj/btsH972YpsS/H3XyikODrRkLXWpn5WYJqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZMLoj/btsH972YpsS/H3XyikODrRkLXWpn5WYJqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZMLoj%2FbtsH972YpsS%2FH3XyikODrRkLXWpn5WYJqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;571&quot; height=&quot;273&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fromUser -&amp;gt; True라면 유저에 의해 변화한 값이고, false라면 유저가 아닌 다른 요인에 의해 변화한 값입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 fromUser를 기준으로 슬라이더 값 변화를 처리해 봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719151156645&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoTime.addOnChangeListener { slider, value, fromUser -&amp;gt;
        val videoLength = player?.duration ?: 0
        val newPosition = (videoLength * value / 100).toLong()
        val minRangeValue = sliderVideoThumbnail.values[0]
        val maxRangeValue = sliderVideoThumbnail.values[1]
        if (fromUser) {
            player?.run {
                seekTo(newPosition)
                pause()
            }
            if (value &amp;lt; minRangeValue) {
                slider.value = minRangeValue
                sliderVideoThumbnail.values = mutableListOf(value, maxRangeValue)
            } else if (value &amp;gt; maxRangeValue) {
                slider.value = maxRangeValue
                sliderVideoThumbnail.values = mutableListOf(minRangeValue, value)
            }
        } else {
            if (value &amp;lt; minRangeValue) {
                slider.value = minRangeValue
            } else if (value &amp;gt;= maxRangeValue) {
                player?.run {
                    val newPosition =
                        (videoLength * sliderVideoThumbnail.values[0] / 100).toLong()
                    seekTo(newPosition)
                    pause()
                }
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447652964&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bqn9m3/hyWrXEydJv/FhL1zOzT5TgGVDxvBODAvK/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/ZdUZa/hyWoJOHV02/O4pOzNWaPquMo9KErDR0C0/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;785&quot; data-video-height=&quot;1920&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447652964?service=daum_tistory&quot; width=&quot;785&quot; height=&quot;1920&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상이 재생됨에 따라 타임라인이 이동하는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 문제가 발견되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동영상 클립 범위 넘어감&lt;/h3&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447653016&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ACMlo/hyWrO8FRTo/uA75mxR6fPpZ7WSGWGK3g1/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/e28ZI/hyWoM5JgFC/EN4dDJtjxYxUmkTk7mPKbk/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;785&quot; data-video-height=&quot;1920&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447653016?service=daum_tistory&quot; width=&quot;785&quot; height=&quot;1920&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상의 최소 재생 범위에 대해 코드가 적용되지 않아서 생긴 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서 fromUser로 인한 분기 처리 코드에서 범위를 제한하는 코드가 빠졌던 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재생에 대해 범위를 넘어가는 경우는 타임라인의 Handle이 오른쪽 클립을 추월하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 부분을 중점으로 코드를 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719150939059&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (value &amp;lt; minRangeValue) {
      slider.value = minRangeValue
  } else if (value &amp;gt;= maxRangeValue) {
      player?.run {
          val newPosition =
              (videoLength * sliderVideoThumbnail.values[0] / 100).toLong()
          seekTo(newPosition)
          pause()
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위가 넘어간다면 동영상의 포지션을 왼쪽 클립에 맞춰주고, 동영상 재생을 중지했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구현 결과입니다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447653067&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/hPHY1/hyWoFZQAfM/nHDKN3RovXlKtXEFTElvFK/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/gWKdU/hyWoMq9IQq/X4UrymMaNY5p1wLTjPMhK1/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;785&quot; data-video-height=&quot;1920&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447653067?service=daum_tistory&quot; width=&quot;785&quot; height=&quot;1920&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Custom Controller</category>
      <category>ExoPlayer</category>
      <category>Kotlin</category>
      <category>동영상 재생 관리</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/492</guid>
      <comments>https://jja2han.tistory.com/492#entry492comment</comments>
      <pubDate>Sun, 23 Jun 2024 21:41:07 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 비디오 타임라인 이벤트 처리</title>
      <link>https://jja2han.tistory.com/491</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] - Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/490&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 영상 편집 UI 구현&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전글에선 영상 편집 UI를 구현했다면 이번 글은 타임라인에 대한 이벤트 처리에 대해 포스팅입니다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447650688&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/e94LL/hyWrQFpBr7/Tsvr9w0NGrRkfaPrw1WxQ0/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/bPKMmm/hyWoAYx688/2jO4Lh0WslbE21HzKmrv20/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;800&quot; data-video-height=&quot;1957&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447650688?service=daum_tistory&quot; width=&quot;800&quot; height=&quot;1957&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 화면은 삼성 갤러리 동영상 편집화면이고, 기능은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1️⃣타임 라인 조절 시 동영상의 시점이 변한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2️⃣왼쪽과 오른쪽 클립 이동시 해당 시점으로 동영상 시점이 변한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3️⃣동영상의 재생 범위는 왼쪽 클립과 오른쪽 클립 사이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4️⃣타임 라인 클릭 시 영상의 시간대가 나와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5️⃣재생버튼과 현재 동영상 시간/동영상 길이를 표시해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 구현하기 위해선 앞서 구현했던 동영상 클립(Range Slider), 타임라인(Slider)와 ExoPlayer 사이의 연관관계를 통해 재생을 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계에 대한 도식화는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eb5RQB/btsH8xhTfGq/epTnrejQKkOIKkDE6n2CBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eb5RQB/btsH8xhTfGq/epTnrejQKkOIKkDE6n2CBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eb5RQB/btsH8xhTfGq/epTnrejQKkOIKkDE6n2CBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feb5RQB%2FbtsH8xhTfGq%2FepTnrejQKkOIKkDE6n2CBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;271&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 저번에 구현했던 onChangedListener를 활용해 슬라이더의 value에 따른 동영상 시점을 계산해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동영상 시점 계산하기&lt;/h2&gt;
&lt;pre id=&quot;code_1719145527729&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoTime.addOnChangeListener { slider, value, fromUser -&amp;gt;
   val videoLength = player?.duration ?: 0
   val newPosition = (videoLength * value / 100).toLong()
   // 아래 코드 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;videoLength는 동영상의 전체 길이를 나타냅니다.&lt;br /&gt;exoPlayer에 MediaItem으로 동영상을 등록했기 때문에 추출할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이 길이에 대해 슬라이더의 Value를 이용해 몇 퍼센트 지점인지를 구해야 합니다.&lt;/li&gt;
&lt;li&gt;Long 타입으로 캐스팅한 이유는 ExoPlayer의 SeekTo 메서드는 포지션을 Long 타입의 파라미터로 받기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 슬라이더 지점에 대해 해당 구간이 몇초인지 라벨링을 사용자에게 제공해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글에서도 setLabelFormatter를 통해 라벨링을 커스텀하게 변환할 수 있다고 얘기했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아래와 같은 함수를 통해서 Value에 대한 타임라인을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타임라인 구현하기&lt;/h2&gt;
&lt;pre id=&quot;code_1719145998781&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun Long.converterTimeLine(value: Long): String {
    val posSeconds = (value / 1000) % 60
    val posMinutes = (value / (1000 * 60)) % 60
    return String.format(&quot;%02d:%02d&quot;, posMinutes, posSeconds)
}
sliderVideoTime.setLabelFormatter {
    newPosition.converterTimeLine(newPosition)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm66mj/btsH94LUbWL/2OwcxaIV4a2FjtukzMjAxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm66mj/btsH94LUbWL/2OwcxaIV4a2FjtukzMjAxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm66mj/btsH94LUbWL/2OwcxaIV4a2FjtukzMjAxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm66mj%2FbtsH94LUbWL%2F2OwcxaIV4a2FjtukzMjAxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;390&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 타임라인이 이동할 때마다 동영상의 시점을 변화해주는것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExoPlayer의 SeekTo를 통해 구현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0AFuq/btsH9VIjShS/zSkVXVOKzeGe4xeEdzPis0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0AFuq/btsH9VIjShS/zSkVXVOKzeGe4xeEdzPis0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0AFuq/btsH9VIjShS/zSkVXVOKzeGe4xeEdzPis0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0AFuq%2FbtsH9VIjShS%2FzSkVXVOKzeGe4xeEdzPis0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;192&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Long값인 positionMs값으로 동영상 시점을 이동하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 구했던 newPositon은 동영상 전체 길이에 대한 시간대를 의미합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719146248595&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;player.seekTo(newPosition)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;seekTo를 통해 동영상 시점을 이동해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 결과물은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447651143&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/DwdCw/hyWoJOG7Er/EHwyekAKlcErKBfsoqsKfK/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/2HTgv/hyWrQSWFey/cCX8chtSw5wkosfzAthHrk/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;300&quot; data-video-height=&quot;734&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447651143?service=daum_tistory&quot; width=&quot;300&quot; height=&quot;734&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>ExoPlayer</category>
      <category>동영상 타임라인</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/491</guid>
      <comments>https://jja2han.tistory.com/491#entry491comment</comments>
      <pubDate>Sun, 23 Jun 2024 21:39:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 영상 편집 UI 구현</title>
      <link>https://jja2han.tistory.com/490</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/486&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] - Retrofit으로 에러 메시지 처리하기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상을 다루는 프로젝트를 진행함에 있어서 영상 편집을 한번 시도해보고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 Android 기본 어플인 갤러리의 UI를 많이 참고했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YcZ4F/btsH8SThG7d/GbcxMsKtzZy1THGWKviF1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YcZ4F/btsH8SThG7d/GbcxMsKtzZy1THGWKviF1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YcZ4F/btsH8SThG7d/GbcxMsKtzZy1THGWKviF1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYcZ4F%2FbtsH8SThG7d%2FGbcxMsKtzZy1THGWKviF1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;138&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시중에 많은 동영상 편집 어플은 있지만, 실제로 블로그에 다룬 사람들은 많이 없었기에, 적절한 UI 컴포넌트를 찾는데 정말 오래 걸린 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후보는 여러 가지가 있는데요,&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SeekBar ❌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/widget/SeekBar&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/reference/android/widget/SeekBar&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719141381414&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SeekBar &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/widget/SeekBar&quot; data-og-url=&quot;https://developer.android.com/reference/android/widget/SeekBar&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CPKgI/hyWoJun3oh/CGKMMa79AERFkCtB8olqy1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/widget/SeekBar&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/reference/android/widget/SeekBar&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CPKgI/hyWoJun3oh/CGKMMa79AERFkCtB8olqy1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SeekBar &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Material에서는 SeekBar가 아닌 Slider로 사용되기 때문에 후보에서 제외했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slider ❌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m3.material.io/components/sliders/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://m3.material.io/components/sliders/overview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719141519855&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Sliders &amp;ndash; Material Design 3&quot; data-og-description=&quot;Sliders allow users to make selections from a range of values.&quot; data-og-host=&quot;m3.material.io&quot; data-og-source-url=&quot;https://m3.material.io/components/sliders/overview&quot; data-og-url=&quot;https://m3.material.io/components/sliders/overview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fYvaz/hyWrKLWxBT/KVNcJGjZGQK8Dg7C3qVUY0/img.png?width=512&amp;amp;height=256&amp;amp;face=0_0_512_256&quot;&gt;&lt;a href=&quot;https://m3.material.io/components/sliders/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m3.material.io/components/sliders/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fYvaz/hyWrKLWxBT/KVNcJGjZGQK8Dg7C3qVUY0/img.png?width=512&amp;amp;height=256&amp;amp;face=0_0_512_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Sliders &amp;ndash; Material Design 3&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Sliders allow users to make selections from a range of values.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;m3.material.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 양방향 스크롤을 구현하기 위해 Slider를 두 개 배치해서, 각각 왼쪽과 오른쪽을 담당하려고 했었습니다.&lt;br /&gt;UI 특성상 왼쪽과 오른쪽의 위치 역전이 발생하면 안 되고, xml에서 width 값을 모두 match_parent로 지정할 경우&lt;br /&gt;한쪽의 Slider 터치 이벤트가 묻혀버리기 때문에 양방향 스크롤을 구현하기 위해서는 Slider는 적합하지 않다고 판단했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RangeSlider ✅&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUqlDs/btsIaE7gDsS/ll7jPP6Xpu4wMNTYWUIKSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUqlDs/btsIaE7gDsS/ll7jPP6Xpu4wMNTYWUIKSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUqlDs/btsIaE7gDsS/ll7jPP6Xpu4wMNTYWUIKSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUqlDs%2FbtsIaE7gDsS%2Fll7jPP6Xpu4wMNTYWUIKSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1232&quot; height=&quot;346&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider에서 파생된 컴포넌트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider의 추가적인 구현을 통해 양방향 스크롤이 가능한 컴포넌트였고, 왼쪽과 오른쪽의 역전을 방지하는 것이 내부 구현되어 있기 때문에 적합하다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 타임라인 기능을 구현하기 위해서는 영상에 대한 시간 표시가 필요했는데, 내부적으로 라벨링에 대한 기능도 제공하기 때문에 더욱 적절했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 RandeSlider 컴포넌트를 하나씩 뜯어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RangeSlider&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OUxDC/btsH9Hcr8XK/IlKGSF5KGELU75Fj2KJK9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OUxDC/btsH9Hcr8XK/IlKGSF5KGELU75Fj2KJK9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OUxDC/btsH9Hcr8XK/IlKGSF5KGELU75Fj2KJK9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOUxDC%2FbtsH9Hcr8XK%2FIlKGSF5KGELU75Fj2KJK9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1256&quot; height=&quot;706&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ Value&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider의 현재 지점을 뜻한다. Slider는 0.0~100.0까지의 범위를 가지고 있는데, 상대적인 위치가 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RangeSlider는 Slider와 다르게 2가지 value를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fromValue[최소값], toValue[최대값]으로 구성되어 있는데, 쉽게 생각하면 from이 왼쪽, to가 오른쪽이라고 이해하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Value는 위 그림처럼 숫자로 표현되는 것이 일반적이지만, UI 쓰임에 맞게 변형이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Float값인 Value에 대해 더 의미 있는 라벨링을 해주고 싶다면 setLabelFormatter를 통해 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Value에 대해 영상의 타임라인을 표시해야 했기에 아래와 같은 구현을 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1719141901326&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoTime.setLabelFormatter { value-&amp;gt;
   converterValueToTimeLine(value) // 반환타입은 반드시 string으로!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 반드시 반환타입이 string으로 지정해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 함수를 사용하지 않는 내부 구현이라면 다음과 같이 쓸 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719141987581&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoTime.setLabelFormatter { value -&amp;gt;
   val line = &quot;현재 값은 ${value*100}원입니다.&quot;
   line
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 labelFormatter를 통해 float인 value를 원하는 문자열로 변환해서 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 6️⃣ Indicator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴대폰을 사용할 경우 슬라이더의 대해 이동할 수 있는 정해진 구간이 있는 UI를 경험해 본 적 있을 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유로운 Slider 이동이 아닌 고정적으로 단계를 정해주고 싶을 때, 그 단계가 Indicator입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider의 step size를 지정하면 step size만큼의 indicator가 증가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적인 스크롤이 아닌 뚝뚝 끊긴 느낌의 UI를 제공하는데 내가 원했던 스크롤은 끊기지 않고 부드러운 스크롤이기 때문에 적용하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ 5️⃣ Active/InActivte&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번과 5번은 각각 활성/비활성화된 영역을 의미합니다.&lt;br /&gt;슬라이더가 지나간 부분은 활성화이고, 지나가지 못한 부분은 비활성화 영역이다.&lt;br /&gt;Slider의 속성에서 ActiveColor와 InActiveColor를 통해서 UI 차이점을 두곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNvVyj/btsH8ikYdEW/Bb4MtqUkvR3pmCKIG9tj2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNvVyj/btsH8ikYdEW/Bb4MtqUkvR3pmCKIG9tj2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNvVyj/btsH8ikYdEW/Bb4MtqUkvR3pmCKIG9tj2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNvVyj%2FbtsH8ikYdEW%2FBb4MtqUkvR3pmCKIG9tj2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1354&quot; height=&quot;230&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삼성 갤러리앱의 UI입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클립이 지나간 부분과 지나가지 않은 부분의 영역 처리를 통해 영상의 길이를 확실하게 사용자에게 보여주고 있는 것을 알 수 있습니다.&lt;br /&gt;이렇게 RangeSlider의 활성/비활성화 영역 구분은 반드시 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ Handle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명칭은 Handle이지만 코드에서는 Thumb라고 불립니다. 슬라이더를 움직이게 하는 막대기인데, 갤러리앱에서 클립 같이 생긴 것이 Thumb역할을 합니다. 실제 구현에서는 Thumb의 Custom Drawable을 입혀 그려내는 것처럼 Custom이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타임라인 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;갤러리앱을 잘 살펴보면 양 끝의 클립을 조절해 움직일 수 있는 부분과 가운데 동영상 타임라인을 조절할 수 있는 막대기가 있는 것을 확인할 수 있습니다.&lt;br /&gt;영상 편집앱에 대한 레퍼런스와 UI를 참고할 만한 코드가 없어서 굉장히 고민을 많이 했습니다.&lt;br /&gt;움직일 수 있는 Handle은 양 클립 가운데 타임라인이기에 Slider를 2개 사용하는 것이라고 판단했습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 저는 RangeSlider와 Slider를 겹쳐서 구현하기로 했습니다.&lt;br /&gt;RangeSlider : 동영상 클립&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider : 동영상 재생 타임라인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도식화하면 아래와 같은데요,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0CJi7/btsH8jquHUv/7cKMsIva5UwiVvbw7kdl00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0CJi7/btsH8jquHUv/7cKMsIva5UwiVvbw7kdl00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0CJi7/btsH8jquHUv/7cKMsIva5UwiVvbw7kdl00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0CJi7%2FbtsH8jquHUv%2F7cKMsIva5UwiVvbw7kdl00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;500&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 할 경우 예상되는 문제점은 클릭 이벤트가 겹친다는 것이었습니다. &lt;br /&gt;즉 반드시 하나의 터치 이벤트는 소실될 수밖에 없었고, 동적으로 Slider의 너비를 바꿀 수 없기 때문에 반드시 xml 단계에서 크기를 할당해야 했습니다. 이렇게 영역이 겹치지만, 영역에 따른 개별적인 이벤트 처리가 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개별적인 이벤트 처리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동영상 클립 스크롤로 썸네일 영역을 활성/비활성화해야 합니다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이는 위에 언급했던 Activity Color와 InActivity Color를 통해 해결할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동영상 타임 라인의 Handle이 동영상 클립의 Handle 범위를 벗어나면 안 됩니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;갤러리앱을 사용해 보면, 클립을 이동시키지 않는다면 타임 라인의 Handle은 클립의 범위를 절대로 벗어날 수 없습니다.&lt;/li&gt;
&lt;li&gt;즉 동영상 타임 라인의 Handle 범위는 동영상 클립의 최솟값과 최댓값 사이에서 움직여야 합니다.&lt;/li&gt;
&lt;li&gt;따라서 왼쪽 클립이 이동할 경우 타임 라인의 Handle도 같이 움직여야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번은 xml 속성 영역으로 해결할 수 있지만, 2번 같은 경우는 코드로 구현해야 하고, 처리가 어려웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slider의 값을 처리하기 위해선 Listener가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text도 onTextChagned가 있듯이 Slider도 다음과 같은 리스너가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719142964353&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract void onValueChange (Slider slider, 
                float value, 
                boolean fromUser)
slider.addOnChangeListenr{ slider, value, fromUser -&amp;gt;	
		//이벤트 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파라미터는 다음과 같은 의미입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;slider : 슬라이더 객체&lt;/li&gt;
&lt;li&gt;value : 슬라이더의 현재 값&lt;/li&gt;
&lt;li&gt;fromUser : 변수명으로 추측해 보면 user에 의해 변화한 것인지, 아닌지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 fromUser의 존재 이유를 몰랐었는데 구현을 하면서 존재 이유를 명확하게 깨달을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영상 썸네일 스크롤 이벤트 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상&amp;nbsp; 썸네일 스크롤의 이벤트 처리는 다음과 같이 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719143160547&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoThumbnail.addOnChangedListener { _, value, _ -&amp;gt; //slider, value, fromUser
   sliderVideoTime.value = value
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상 클립 조절 시 영상 타임 라인의 Handle도 따라 이동해야 하기 때문에 다음과 같은 코드를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slider와 fromUser는 사용하지 않기 때문에 백틱 처리해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영상 타임라인 이벤트 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 영상&amp;nbsp; 타임라인 이벤트 처리입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719143299277&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sliderVideoTime.addOnChangeListener { slider, value, _ -&amp;gt;
   val minRangeValue = sliderVideoThumbnail.values[0]
   val maxRangeValue = sliderVideoThumbnail.values[1]
	 if (value &amp;lt; minRangeValue) {
          slider.value = minRangeValue
          sliderVideoThumbnail.values = mutableListOf(value, maxRangeValue)
   } else if (value &amp;gt; maxRangeValue) {
          slider.value = maxRangeValue
          sliderVideoThumbnail.values = mutableListOf(minRangeValue, value)
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임라인의 경우 클립의 범위를 벗어나면 안 되는 것이 첫 번째 지켜야 할 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 썸네일의 최솟값과 최댓값을 가져와서 타임라인의 값이 변화할 때마다 비교해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RangeSlider의 value는 [fromValue,toValue]로 지정할 수 있는데, 이를 통해&amp;nbsp; 만약 타임라인의 값이 범위를 벗어난다면 범위를 바꿔줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 결과물입니다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/447649914&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/tQ7ud/hyWoDnpPH0/H07m6ZeOHuW7vXv2AFkngK/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920,https://scrap.kakaocdn.net/dn/CDGOV/hyWoARK8ZP/ZiTKnNuD04ES3XW3vpMxF1/img.jpg?width=785&amp;amp;height=1920&amp;amp;face=0_0_785_1920&quot; data-video-width=&quot;800&quot; data-video-height=&quot;1957&quot; data-video-origin-width=&quot;785&quot; data-video-origin-height=&quot;1920&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/447649914?service=daum_tistory&quot; width=&quot;800&quot; height=&quot;1957&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 디테일한 점을 신경 쓰느라 오래 걸렸던 것 같습니다.&lt;br /&gt;타임라인의 범위가 벗어날 경우 고정시켜야 할지, 범위를 늘려야 할지 고민하다가 제가 사용한다면 이게 더 편할 것 같다~로 느낀 방향으로 구현하기는 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현을 하고 보니 Slider의 Track 크기가 알맞지 않아 활성화된 영역 UI가 굉장히 불편하기는 한데, 이래서 동영상 편집 어플이 다 검은색으로 UI를 구현하는 건가?라는 의심이 들긴 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m3.material.io/components/sliders/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://m3.material.io/components/sliders/overview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719147463322&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Sliders &amp;ndash; Material Design 3&quot; data-og-description=&quot;Sliders allow users to make selections from a range of values.&quot; data-og-host=&quot;m3.material.io&quot; data-og-source-url=&quot;https://m3.material.io/components/sliders/overview&quot; data-og-url=&quot;https://m3.material.io/components/sliders/overview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fYvaz/hyWrKLWxBT/KVNcJGjZGQK8Dg7C3qVUY0/img.png?width=512&amp;amp;height=256&amp;amp;face=0_0_512_256&quot;&gt;&lt;a href=&quot;https://m3.material.io/components/sliders/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m3.material.io/components/sliders/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fYvaz/hyWrKLWxBT/KVNcJGjZGQK8Dg7C3qVUY0/img.png?width=512&amp;amp;height=256&amp;amp;face=0_0_512_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Sliders &amp;ndash; Material Design 3&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Sliders allow users to make selections from a range of values.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;m3.material.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719147475197&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;material-components-android/docs/components/Slider.md at master &amp;middot; material-components/material-components-android&quot; data-og-description=&quot;Modular and customizable Material Design UI components for Android - material-components/material-components-android&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&quot; data-og-url=&quot;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tQQo3/hyWoPVHHTf/eAhKfMis6KCnpCQJkBNVq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tQQo3/hyWoPVHHTf/eAhKfMis6KCnpCQJkBNVq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;material-components-android/docs/components/Slider.md at master &amp;middot; material-components/material-components-android&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Modular and customizable Material Design UI components for Android - material-components/material-components-android&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>rangeslider</category>
      <category>slider</category>
      <category>영상 편집 ui</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/490</guid>
      <comments>https://jja2han.tistory.com/490#entry490comment</comments>
      <pubDate>Sun, 23 Jun 2024 21:06:01 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Flow를 이용한 새로고침 구현 및 개선</title>
      <link>https://jja2han.tistory.com/489</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;안드로이드.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6iMij/btsHERHfwIj/PbAZIrnm4T8yA2kx9eaU2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6iMij/btsHERHfwIj/PbAZIrnm4T8yA2kx9eaU2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6iMij/btsHERHfwIj/PbAZIrnm4T8yA2kx9eaU2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6iMij%2FbtsHERHfwIj%2FPbAZIrnm4T8yA2kx9eaU2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-filename=&quot;안드로이드.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 실시간 데이터의 변환을 감지해야 하는 화면이 여럿 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예를 들어 커뮤니티와 댓글창과 같이 다른 유저의 행동으로 인해 서버의 데이터가 변한 경우, 이를 다른 유저의 화면에도 반영해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통상적으로 MVVM 패턴에서 프래그먼트나 액티비티의 뷰모델이 생성될 경우 init 블록이나 view가 생성될 당시 서버의 데이터를 로드해서 UI에 반영하게 되는 로직이 일반적이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버로의 데이터 호출은 딱 한번 일어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;어떻게?&lt;/b&gt; 다른 앱은 댓글과 게시글이 달릴 경우 다른 유저가 화면을 이동시키지 않아도(프래그먼트나 뷰모델이 재생산되지 않아도) &lt;b&gt;실시간으로 반영할 수 있는지 궁금했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 간단하게 도식화하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Qgx1/btsHDVXUAhg/lnfuU9alfsQrZWCZ9agwL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Qgx1/btsHDVXUAhg/lnfuU9alfsQrZWCZ9agwL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Qgx1/btsHDVXUAhg/lnfuU9alfsQrZWCZ9agwL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Qgx1%2FbtsHDVXUAhg%2FlnfuU9alfsQrZWCZ9agwL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;413&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;뷰모델이 초기 생산되면 서버로 데이터를 요청합니다.&lt;/li&gt;
&lt;li&gt;서버가 요청에 대한 결과로 데이터를 뷰모델에 반환합니다.&lt;/li&gt;
&lt;li&gt;뷰모델의 데이터를 UI에 반영합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 뷰모델 &amp;lt;-&amp;gt; 서버의 과정에서 레포지토리가 위치하지만, 현재 글에서는 뷰모델의 동작을 다루기 위해 생략했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRTUuC/btsHFLTNeXh/aS0HW06IiLtkN4TcjQQDn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRTUuC/btsHFLTNeXh/aS0HW06IiLtkN4TcjQQDn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRTUuC/btsHFLTNeXh/aS0HW06IiLtkN4TcjQQDn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRTUuC%2FbtsHFLTNeXh%2FaS0HW06IiLtkN4TcjQQDn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;213&quot; height=&quot;464&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉 위와 같은 화면에서 사용자 A와 사용자 B가 있다고 가정할 때, B가 영화 일기를 작성할 경우 A의 화면에도 반영되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 B가 다른 사용자들에게 나 추가했어~~라고 전달하는 구조는 옳지 않다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 저는 사용자들의 화면에서 주기적으로 새로고침 즉 데이터를 로드하는 행위가 필요하다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하기 위해 thread를 이용한 while 문으로 처리할 수 있지만, viewModel에서 flow를 사용하고 있기에, 저는 thread가 아닌 flow로 이를 구현해 보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2개의 함수&lt;/b&gt;를 구현했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫 번째로는 입력된 주기를 바탕으로 flow를 생산하는 함수입니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 주기마다 울리는 알람이라고 생각하시면 편합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716876521471&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun createTickerFlow(interval: Long): Flow&amp;lt;Unit&amp;gt; = flow {
    while (true) {
        delay(interval)
        emit(Unit)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;두 번째 함수는 생명주기동안 주기마다 행위를 실행하는 새로고침 함수입니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 주기와, 생명주기, 행위를 입력받습니다&lt;/p&gt;
&lt;pre id=&quot;code_1716876578983&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun runTickerFlow(
    interval: Long, // 새로고침 주기
    scope: CoroutineScope, // 생명 주기
    action: () -&amp;gt; Unit, // 행위
) {
    val tickerFlow = createTickerFlow(interval)
    scope.launch {
        tickerFlow.collect {
            action.invoke()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createTickerFlow를 통해 주기적으로 flow를 생산하는 알람 형태의 tickerFlow를 생산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생명주기동안 해당 flow가 생산될 때마다, 행위로 소비해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뷰모델 내에서 함수를 정의하지 않는 이유는 여러 화면에서 새로고침이 필요하기 때문에 따로 분리해 줬습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰모델에서는 다음과 같이 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716876746580&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// viewModel init 블록
runTikcerFlow{
    interval = 1000L,
    scope = viewModelScope,
    action = { loadComments(viewModelScope)},
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;커뮤니티-실시간-_online-video-cutter.com_.gif&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btX2fd/btsHEV3PJUr/BXqSn0e87sWcwh5V6xecr0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btX2fd/btsHEV3PJUr/BXqSn0e87sWcwh5V6xecr0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btX2fd/btsHEV3PJUr/BXqSn0e87sWcwh5V6xecr0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/btX2fd/btsHEV3PJUr/BXqSn0e87sWcwh5V6xecr0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;523&quot; data-filename=&quot;커뮤니티-실시간-_online-video-cutter.com_.gif&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 원한 새로고침은 생명주기동안 새로고침을 불러오는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 로그를 찍어보니 화면이 destroy가 돼도, 계속해서 로그가 찍히는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqjHPN/btsHEPvRNnZ/jebZoVKVfpo39EEIOKV471/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqjHPN/btsHEPvRNnZ/jebZoVKVfpo39EEIOKV471/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqjHPN/btsHEPvRNnZ/jebZoVKVfpo39EEIOKV471/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqjHPN%2FbtsHEPvRNnZ%2FjebZoVKVfpo39EEIOKV471%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;191&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 의도치 않은 상황이었고, 화면을 벗어나도 새로고침 행위가 반복되기 때문에 이는 &lt;b&gt;메모리 누수&lt;/b&gt;로 이어지는 큰 버그였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프래그먼트가 onDetach 돼도, viewModel의 onCleared가 호출되지 않았고, 이는 내비게이션 호스트에 의해 뷰모델이 관리되었기 때문이라고 판단 내렸습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 새로고침을 반복하는 생명주기를 변경해야 했기에, UI가 살아있을 동안만 새로고침을 하면 되기 때문에 다음과 같이 수정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 viewModel의 init 블록에서 데이터를 로드하는 것을 fragment의 onViewCreated에서 호출해 줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716877801942&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun loadData() {
    runTickerFlow(
        interval = 1000L,
        scope = lifecycleScope,
        action = { communityViewModel.loadComments(lifecycleScope) },
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티의 게시글들은 stateFlow로 관리하기 때문에 변화값이 없다면, 업데이트가 되지 않았기 때문에 효과적으로 관리할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yk6bh/btsHErCaReh/nOvSbix55LfAJZ8Tlx9cI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yk6bh/btsHErCaReh/nOvSbix55LfAJZ8Tlx9cI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yk6bh/btsHErCaReh/nOvSbix55LfAJZ8Tlx9cI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyk6bh%2FbtsHErCaReh%2FnOvSbix55LfAJZ8Tlx9cI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;206&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 결과 UI가 없어지면, 새로고침도 멈추는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침을 구현해 보면서, 현업에서는 어떻게 새로고침을 구현하는지 궁금해지긴 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토이프로젝트라서 코드가 깔끔하진 않았지만, 그래도 새로고침을 구현하는 경험을 할 수 있었고, 개선까지 할 수 있어서 만족했던 프로젝트였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;figure id=&quot;og_1716878186054&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Android Kotlin Flow를 사용하여 자동으로 데이터를 refresh하는 전략&quot; data-og-description=&quot;Making timers lifecycle-aware타이머의 수명주기를 인식하도록 만들기아티클 : https://bladecoder.medium.com/strategies-for-automatically-refreshing-data-on-android-usin&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@woga1999/Android-Kotlin-Flow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-refresh%ED%95%98%EB%8A%94-%EC%A0%84%EB%9E%B5&quot; data-og-url=&quot;https://velog.io/@woga1999/Android-Kotlin-Flow를-사용하여-자동으로-데이터를-refresh하는-전략&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eKucY/hyV9Qt49Z2/X3xFXUAcvaJd9opt36H4w0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@woga1999/Android-Kotlin-Flow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-refresh%ED%95%98%EB%8A%94-%EC%A0%84%EB%9E%B5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@woga1999/Android-Kotlin-Flow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-refresh%ED%95%98%EB%8A%94-%EC%A0%84%EB%9E%B5&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eKucY/hyV9Qt49Z2/X3xFXUAcvaJd9opt36H4w0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android Kotlin Flow를 사용하여 자동으로 데이터를 refresh하는 전략&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Making timers lifecycle-aware타이머의 수명주기를 인식하도록 만들기아티클 : https://bladecoder.medium.com/strategies-for-automatically-refreshing-data-on-android-usin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Flow</category>
      <category>새로고침</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/489</guid>
      <comments>https://jja2han.tistory.com/489#entry489comment</comments>
      <pubDate>Tue, 28 May 2024 15:37:21 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - Youtube API 적용해보기</title>
      <link>https://jja2han.tistory.com/488</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;안드로이드.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zh0KA/btsHDjps6li/Xy52ipMpJJsaoIO6qt49ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zh0KA/btsHDjps6li/Xy52ipMpJJsaoIO6qt49ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zh0KA/btsHDjps6li/Xy52ipMpJJsaoIO6qt49ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzh0KA%2FbtsHDjps6li%2FXy52ipMpJJsaoIO6qt49ZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-filename=&quot;안드로이드.png&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 프로젝트를 하면서 TMDB API를 이용해 영화 관련 앱을 만들고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차별화된 기능으로 영화 이름에 대한 트레일러 영상을 제공하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TMDB API는 공공 API로 영화에 대한 세부 정보를 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.themoviedb.org/reference/intro/getting-started&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716719060224&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Getting Started&quot; data-og-description=&quot;Welcome to version 3 of The Movie Database (TMDB) API. This is where you will find the definitive list of currently available methods for our movie, tv, actor and image API.&quot; data-og-host=&quot;developer.themoviedb.org&quot; data-og-source-url=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot; data-og-url=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Getting Started&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Welcome to version 3 of The Movie Database (TMDB) API. This is where you will find the definitive list of currently available methods for our movie, tv, actor and image API.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.themoviedb.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안타깝게도, 영화에 대한 세부 정보는 제공하지만, 영화 트레일러와 같은 영상 링크는 제공해주지 않더라고요,&lt;br /&gt;간단한 트레일러까지 제공해 주면 좋지 않을까 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트레일러를 제공해주기 위한 매개체로는 가장 대중적인 Youtube로 정했고, Youtube API가 있지 않을까 해서 자료를 서칭 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처음으로 찾아본 API는 &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;YouTube Android Player API였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/android/player?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/youtube/android/player?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716719337524&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;YouTube Android Player API
   &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;Android 애플리케이션에 동영상 재생 기능을 추가하세요.&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/youtube/android/player?hl=ko&quot; data-og-url=&quot;https://developers.google.com/youtube/android/player?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/android/player?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/youtube/android/player?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;YouTube Android Player API &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android 애플리케이션에 동영상 재생 기능을 추가하세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제 지원이 중지되었다고 하더라고요...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다른 것을 찾아보기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 찾은 것은&amp;nbsp; Youtube Data API였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/v3/getting-started?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/youtube/v3/getting-started?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716719449440&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;YouTube Data API 개요 &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. YouTube Data API 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 소개 이 문서는 YouTube와 상호작용하&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/youtube/v3/getting-started?hl=ko&quot; data-og-url=&quot;https://developers.google.com/youtube/v3/getting-started?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/v3/getting-started?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/youtube/v3/getting-started?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;YouTube Data API 개요 &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. YouTube Data API 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 소개 이 문서는 YouTube와 상호작용하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 API를 사용하기 위해서는 google cloud에 앱을 등록해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://console.cloud.google.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716719585473&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google 클라우드 플랫폼&quot; data-og-description=&quot;로그인 Google 클라우드 플랫폼으로 이동&quot; data-og-host=&quot;accounts.google.com&quot; data-og-source-url=&quot;https://console.cloud.google.com/&quot; data-og-url=&quot;https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fconsole.cloud.google.com%2F&amp;amp;followup=https%3A%2F%2Fconsole.cloud.google.com%2F&amp;amp;ifkv=AaSxoQyHAMLELzIqrQI1zRvZC5RI4O4GlBYO2sj9E71kmeJ3BzR544l5ddaEATBJLs_6Q5p8tqui&amp;amp;osid=1&amp;amp;passive=1209600&amp;amp;service=cloudconsole&amp;amp;flowName=WebLiteSignIn&amp;amp;flowEntry=ServiceLogin&amp;amp;dsh=S-519387038%3A1716719584049221&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://console.cloud.google.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google 클라우드 플랫폼&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;로그인 Google 클라우드 플랫폼으로 이동&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;accounts.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록하는 방법은 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c84svG/btsHDjiGeZy/nO9Bolppo4HkokiiSAqsK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c84svG/btsHDjiGeZy/nO9Bolppo4HkokiiSAqsK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c84svG/btsHDjiGeZy/nO9Bolppo4HkokiiSAqsK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc84svG%2FbtsHDjiGeZy%2FnO9Bolppo4HkokiiSAqsK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1472&quot; height=&quot;666&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트를 눌러줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMpoE1/btsHDzFFnZB/6QQ1zN9i0tcxckHqrByP5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMpoE1/btsHDzFFnZB/6QQ1zN9i0tcxckHqrByP5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMpoE1/btsHDzFFnZB/6QQ1zN9i0tcxckHqrByP5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMpoE1%2FbtsHDzFFnZB%2F6QQ1zN9i0tcxckHqrByP5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;430&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 이름을 입력하고 만들어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2432&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDdgk/btsHBrvZJv4/24mnPkEPKklX7VZfsRlco0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDdgk/btsHBrvZJv4/24mnPkEPKklX7VZfsRlco0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDdgk/btsHBrvZJv4/24mnPkEPKklX7VZfsRlco0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDdgk%2FbtsHBrvZJv4%2F24mnPkEPKklX7VZfsRlco0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2432&quot; height=&quot;1170&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2432&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 및&amp;nbsp; 서비스를 눌러주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CvZ1z/btsHCzGgxKn/baJf8OOb8LEnWIJj2iQBCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CvZ1z/btsHCzGgxKn/baJf8OOb8LEnWIJj2iQBCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CvZ1z/btsHCzGgxKn/baJf8OOb8LEnWIJj2iQBCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCvZ1z%2FbtsHCzGgxKn%2FbaJf8OOb8LEnWIJj2iQBCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;794&quot; height=&quot;110&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누르면 페이지가 나오는데 API 및 서비스 사용 설정을 눌러주세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEgrxT/btsHDWUQMib/LeHb5Z4IlK65DarHhdTKK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEgrxT/btsHDWUQMib/LeHb5Z4IlK65DarHhdTKK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEgrxT/btsHDWUQMib/LeHb5Z4IlK65DarHhdTKK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEgrxT%2FbtsHDWUQMib%2FLeHb5Z4IlK65DarHhdTKK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;456&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Youtube API를 검색해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzt3X/btsHBWWx5zx/BCQyWzrlUDAYsWvvqwEnek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzt3X/btsHBWWx5zx/BCQyWzrlUDAYsWvvqwEnek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzt3X/btsHBWWx5zx/BCQyWzrlUDAYsWvvqwEnek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frzt3X%2FbtsHBWWx5zx%2FBCQyWzrlUDAYsWvvqwEnek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1358&quot; height=&quot;514&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 처음 들어가시면 관리가 아닌 사용이라고 적혀있을 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용을 누른 후 사용자 인증 정보로 들어가 주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQLOxF/btsHDfN7Bn9/YEP2JB1BMtzPprLHAlDTJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQLOxF/btsHDfN7Bn9/YEP2JB1BMtzPprLHAlDTJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQLOxF/btsHDfN7Bn9/YEP2JB1BMtzPprLHAlDTJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQLOxF%2FbtsHDfN7Bn9%2FYEP2JB1BMtzPprLHAlDTJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1130&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czkESg/btsHBNrZOvO/Ezfsvn5yc0aPj7ySUP8uE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czkESg/btsHBNrZOvO/Ezfsvn5yc0aPj7ySUP8uE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czkESg/btsHBNrZOvO/Ezfsvn5yc0aPj7ySUP8uE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczkESg%2FbtsHBNrZOvO%2FEzfsvn5yc0aPj7ySUP8uE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;874&quot; height=&quot;554&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 키를 만들어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TbQk3/btsHDeodfhK/LgFFvA9wNtKbTJz2rSJmWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TbQk3/btsHDeodfhK/LgFFvA9wNtKbTJz2rSJmWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TbQk3/btsHDeodfhK/LgFFvA9wNtKbTJz2rSJmWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTbQk3%2FbtsHDeodfhK%2FLgFFvA9wNtKbTJz2rSJmWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;920&quot; height=&quot;234&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API키를 기억해 주세요. 나중에 요청 보낼 때 써야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 사용하기 전에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Youtube Player View가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/PierfrancescoSoffritti/android-youtube-player&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/PierfrancescoSoffritti/android-youtube-player&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716720567061&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - PierfrancescoSoffritti/android-youtube-player: YouTube Player library for Android and Chromecast, stable and customizab&quot; data-og-description=&quot;YouTube Player library for Android and Chromecast, stable and customizable. - PierfrancescoSoffritti/android-youtube-player&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/PierfrancescoSoffritti/android-youtube-player&quot; data-og-url=&quot;https://github.com/PierfrancescoSoffritti/android-youtube-player&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dBYTdD/hyV90QGiY9/tl7GLwp4AZUtSSEctJc5c0/img.png?width=1924&amp;amp;height=939&amp;amp;face=0_0_1924_939&quot;&gt;&lt;a href=&quot;https://github.com/PierfrancescoSoffritti/android-youtube-player&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/PierfrancescoSoffritti/android-youtube-player&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dBYTdD/hyV90QGiY9/tl7GLwp4AZUtSSEctJc5c0/img.png?width=1924&amp;amp;height=939&amp;amp;face=0_0_1924_939');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - PierfrancescoSoffritti/android-youtube-player: YouTube Player library for Android and Chromecast, stable and customizab&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;YouTube Player library for Android and Chromecast, stable and customizable. - PierfrancescoSoffritti/android-youtube-player&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 해당 라이브러리를 통해서 Youtube Player View를 구현했고, 동영상 재생을 관리해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5sntz/btsHDg7kqOZ/WPKO4cw0HfSHRaaOJz6iVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5sntz/btsHDg7kqOZ/WPKO4cw0HfSHRaaOJz6iVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5sntz/btsHDg7kqOZ/WPKO4cw0HfSHRaaOJz6iVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5sntz%2FbtsHDg7kqOZ%2FWPKO4cw0HfSHRaaOJz6iVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1538&quot; height=&quot;412&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상을 재생할 뷰를 만들어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 뷰는 유튜브 UI처럼 생긴 뷰를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716720758879&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;YouTubePlayerView youTubePlayerView = findViewById(R.id.youtube_player_view);
getLifecycle().addObserver(youTubePlayerView);

youTubePlayerView.addYouTubePlayerListener(new AbstractYouTubePlayerListener() {
  @Override
  public void onReady(@NonNull YouTubePlayer youTubePlayer) {
    String videoId = &quot;S0Q4gqBUs7c&quot;;
    youTubePlayer.loadVideo(videoId, 0);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공된 문서를 보면, videoId만 넣어주면, 자동으로 유튜브가 연결되는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 영화 이름에 대한 트레일러 영상이 필요했기에, Youtube Data API에서 search를 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716720893723&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Search: list &amp;nbsp;|&amp;nbsp; YouTube Data API &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Search: list 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. API 요청에 지정된 쿼리 매개변수와 일치하는&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&quot; data-og-url=&quot;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/youtube/v3/docs/search/list?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Search: list &amp;nbsp;|&amp;nbsp; YouTube Data API &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Search: list 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. API 요청에 지정된 쿼리 매개변수와 일치하는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostMan처럼 임시로 사용해 볼 수 있는 기회가 있어서, 데이터를 요청하고, 받는 코드를 작성하기 조금 수월했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 엄청난 response 데이터 형식에 조금 놀랐지만요,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;1352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs2NTt/btsHDytenp3/gchDrezJ5Kak66P191XVg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs2NTt/btsHDytenp3/gchDrezJ5Kak66P191XVg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs2NTt/btsHDytenp3/gchDrezJ5Kak66P191XVg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs2NTt%2FbtsHDytenp3%2FgchDrezJ5Kak66P191XVg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;517&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;1352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 query 매개변수에 대한 정의도 해주고,, 친절했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 매개변수에 따르면 part는 snippet으로 설정하고, 영화 이름&amp;nbsp; 즉 검색어를 q라는 매개변수에 담아서 보내줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 바탕으로, Retrofit 호출문은 다음과 같이 구성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716721202626&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GET(&quot;search&quot;)
suspend fun getMovieTrailer(
    @Query(&quot;key&quot;) apiKey: String = BuildConfig.YOUTUBE_API_KEY,
    @Query(&quot;part&quot;) part: String = &quot;snippet&quot;,
    @Query(&quot;q&quot;) movieName: String,
): Response&amp;lt;YoutubeSearchResponse&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값은 정말~정말 복잡하므로, data from json을 통해서 자동으로 만들어주세요...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 하나하나 입력하다가 계속 형식이 틀렸던 기억이 있네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository code입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDhzP/btsHCVPQ6qR/zQiRXcDkgzU4NnCKYMGw9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDhzP/btsHCVPQ6qR/zQiRXcDkgzU4NnCKYMGw9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDhzP/btsHCVPQ6qR/zQiRXcDkgzU4NnCKYMGw9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDhzP%2FbtsHCVPQ6qR%2FzQiRXcDkgzU4NnCKYMGw9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1768&quot; height=&quot;578&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 영상 하나를 원했기 때문에 가장 위에 있는 영상의 videoId만 가져왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 vidoeId가 int가 아닌 string 값이라는 점입니다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 int로 하고, 코드를 돌렸는데 계속 터져서 한참 찾았었습니다 ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 트레일러 영상 id에 대한 요청을 viewModel에서 ui로 전달해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDnIk/btsHDHXUNB0/2C6Pan4MkFQAp6hJlrXBl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDnIk/btsHDHXUNB0/2C6Pan4MkFQAp6hJlrXBl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDnIk/btsHDHXUNB0/2C6Pan4MkFQAp6hJlrXBl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDnIk%2FbtsHDHXUNB0%2F2C6Pan4MkFQAp6hJlrXBl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1928&quot; height=&quot;904&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel은 movieTitle에 대한 요청 쿼리를 날리고, vidoeId를 반환받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &quot;&quot;이 아닐 경우는 영상이 존재한 경우이기 때문에 uiState의 videoId값을 업데이트해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상이 존재하지 않는다면, 영상 검색 실패에 대한 UI 처리를 위해 event 값을 emit 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따른 UI에서는 다음과 같은 작업을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;videoId가 있을 경우, 해당 아이디로 youtube player를 재생한다.&lt;/li&gt;
&lt;li&gt;없을 경우 snackbar를 띄운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vpcMs/btsHCvKKOxW/J7yZW4swwjuH8wAWv0BCnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vpcMs/btsHCvKKOxW/J7yZW4swwjuH8wAWv0BCnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vpcMs/btsHCvKKOxW/J7yZW4swwjuH8wAWv0BCnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvpcMs%2FbtsHCvKKOxW%2FJ7yZW4swwjuH8wAWv0BCnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;646&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;videoId가 업데이트되었다면, Youtube Player에 비디오 id에 대한 영상을 load 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 자세한 사용법이 알고 싶다면, 위 첨부된 링크에서 본인의 앱 시나리오에 맞게 사용하면 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저처럼 단일 영상을 다루고 싶다면, 해당 코드로도 충분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문적인 트레일러 영상이 나오는 영화도 있고, 안 나오는 영화도 있더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 검색어에 대한 영상중 첫 영상을 들고 오기 때문에 그런 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;영화 검색.gif&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3XP6/btsHDV2IphO/CPrBWMOO9zDZGJcOZeqir0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3XP6/btsHDV2IphO/CPrBWMOO9zDZGJcOZeqir0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3XP6/btsHDV2IphO/CPrBWMOO9zDZGJcOZeqir0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/t3XP6/btsHDV2IphO/CPrBWMOO9zDZGJcOZeqir0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;724&quot; data-filename=&quot;영화 검색.gif&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Youtube Data API 사용법에 대해 다룬 글이 많이 없어서 적용하는데 꽤 애를 먹었던 것 같습니다 ㅎㅎ.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>youtube API</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/488</guid>
      <comments>https://jja2han.tistory.com/488#entry488comment</comments>
      <pubDate>Sun, 26 May 2024 20:16:30 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1781] - 컵라면(Kotlin)[골드2]</title>
      <link>https://jja2han.tistory.com/487</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clmgBh/btsHl15onO9/kxAeDZ4R7asRbHnK7BIB3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clmgBh/btsHl15onO9/kxAeDZ4R7asRbHnK7BIB3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clmgBh/btsHl15onO9/kxAeDZ4R7asRbHnK7BIB3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclmgBh%2FbtsHl15onO9%2FkxAeDZ4R7asRbHnK7BIB3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1781&quot;&gt;https://www.acmicpc.net/problem/1781&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 문제를 보고 난이도에 비해 정말 쉽다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의실 배정과 유사하게 우선순위큐를 사용해서, deadLine이 짧은 순, 가치가 무거운 순으로 정렬한 뒤, 시간에 맞게 빼면 된다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 문제에 대한 테스트케이스는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dk64bJ/btsHov4gaoj/YOobdcr6JtrxRoRtRMu6r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dk64bJ/btsHov4gaoj/YOobdcr6JtrxRoRtRMu6r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dk64bJ/btsHov4gaoj/YOobdcr6JtrxRoRtRMu6r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdk64bJ%2FbtsHov4gaoj%2FYOobdcr6JtrxRoRtRMu6r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1142&quot; height=&quot;360&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업들을 우리가 목표로 하는 최대의 가치의 순서로 작업할 수 있도록 정렬해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고려해야할 점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데드라인이 적은 순서대로 정렬을 한다.&lt;/li&gt;
&lt;li&gt;가치가 많은 순으로 정렬을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건을 고려해서 정렬을 하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WBjbF/btsHmVbQaNn/j0ypO5q80QARQiRXquEOr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WBjbF/btsHmVbQaNn/j0ypO5q80QARQiRXquEOr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WBjbF/btsHmVbQaNn/j0ypO5q80QARQiRXquEOr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWBjbF%2FbtsHmVbQaNn%2Fj0ypO5q80QARQiRXquEOr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1025&quot; height=&quot;358&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데드라인 별로 작업을 선택해서 답을 구하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 함정이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 풀이했던 방식은 데드라인이 아니라, 그때 풀어야 했던 문제라고 생각했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 1초에는 데드라인이 1인 문제를, 2초에는 데드라인이 2인 문제를 풀어야 한다고 생각했지만, 데드라인의 개념은 사실상&amp;nbsp; 그게 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n 초까지만 완료하면 되는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 1초라고 해서 반드시 1초에 문제를 푸는 것이 아닌 1~n 초까지의 문제를 풀 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예시케이스로 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AGBf1/btsHnvqdLn4/yNZOva74Mhe9tS3tQekyS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AGBf1/btsHnvqdLn4/yNZOva74Mhe9tS3tQekyS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AGBf1/btsHnvqdLn4/yNZOva74Mhe9tS3tQekyS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAGBf1%2FbtsHnvqdLn4%2FyNZOva74Mhe9tS3tQekyS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;342&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 입력에서 내 코드는 1일 차에 14, 2일 차에 10, 3일 차에 8, 4일차에 18, 5일차에 5를 선택해, 가치의 총합은 55이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 3일차에 5번을 선택하는 것보단 6번을 선택하고, 4일 차에 7번을 선택하는 것이 더 가치의 총합은 높다(이때 총합은 59이다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 뒤의 문제까지 고려해서 담아야 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로직을 생각하는데 조금 오래 걸렸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n의 최댓값은 200,000이기 때문에 N^2의 알고리즘으론 문제를 풀 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 한 번의 탐색으로 문제를 해결해야 했고, 매번 문제를 선택하기 전에 뒤 문제를 탐색할만한 시간적 여유가 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 선택하기전 뒤 문제를 탐색하는 로직은 적절하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 근본적으로 다가가면, 5번 문제가 선택되지 않는 이유는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3초~n 초까지의 문제 중 선택할 수 있는 문제는 4번부터 9번까지의 문제일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5번 문제의 가치는 8이고, 6번 문제의 가치는 18이다. 즉 3초일 때 6번을 담는 것이 이론적으로 맞고, 이를 프로그램적으로 구현하기가 굉장히 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 문제를 선택하고, 선택하지 않는 이분법적 경우의 수를 통해서 완전탐색으로 문제를 해결할 수도 있지만, n이 너무 크기 때문에 이 방법은 적절하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 나는 선택한 문제를 임시적으로 저장할 우선순위큐를 하나 더 두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위큐에는 문제들의 가치가 들어가고, 오름차순으로 정렬된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 우선순위큐를 하나씩 꺼내서 해당 시간에 풀 수 있는 문제인지를 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 시간에 풀 수 없는 문제라면 문제를 선택하지 않는 것이 아닌, 앞에서 풀었던 문제들의 가치와 비교해서, 더 유망한 문제라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 풀었던 문제가 아닌 현재 탐색하고 있는 문제를 푸는 것이 유망한 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 보면 편할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1715505572823&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var time = 1
val task = PriorityQueue&amp;lt;Int&amp;gt;()
while (!pq.isEmpty()) {
    val now = pq.poll()
    if (time &amp;lt;= now.deadLine) {
        task.add(now.weight)
        time += 1
    } else {
        if(task.peek()&amp;lt;now.weight){
            task.poll()
            task.add(now.weight)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task는 선택한 문제들이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;time을 기준으로 deadLine이 더 길다면 당연하게 문제를 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 문제를 풀었다면 문제를 푸는 데 걸리는 시간은 1 초기 때문에 시간을 증가시켜 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 만약 현재 시간보다 데드라인이 더 짧은 문제가 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 앞에서 풀었을 때 더 유망한 선택지가 될 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 풀었던 문제들의 집합인 task에서 가장 적은 가치인 문제와 비교해서, 만약 가치가 크다면 문제를 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 우선순위큐를 통해서 문제를 선택할 때 뒤 문제까지 탐색하지 않아도 되는 효과를 볼 수 있었던 좋은 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연료 채우기와 정말 비슷한 문제라고 생각하는데 이 문제도 좋으니 풀어보면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1826&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1826&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1715505803805&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0

    data class Task(val deadLine: Int, val weight: Int)

    private val pq = PriorityQueue&amp;lt;Task&amp;gt;(compareBy({ it.deadLine }, { -it.weight }))

    fun run() {
        input()
        print(solution())
    }

    private fun input() {
        n = br.readLine().toInt()
        repeat(n) {
            val (d, w) = br.readLine().split(&quot; &quot;).map { it.toInt() }
            pq.add(Task(d, w))
        }
    }

    private fun solution() : Int{
        var time = 1
        val task = PriorityQueue&amp;lt;Int&amp;gt;()
        while (!pq.isEmpty()) {
            val now = pq.poll()
            if (time &amp;lt;= now.deadLine) {
                task.add(now.weight)
                time += 1
            } else {
                if(task.peek()&amp;lt;now.weight){
                    task.poll()
                    task.add(now.weight)
                }
            }
        }
        var answer = 0
        while(task.isNotEmpty()){
            answer+=task.poll()
        }
        return answer
    }
}

fun main() {
    Solution().run()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <category>우선순위큐</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/487</guid>
      <comments>https://jja2han.tistory.com/487#entry487comment</comments>
      <pubDate>Sun, 12 May 2024 18:24:42 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - Retrofit으로 에러 메시지 처리하기</title>
      <link>https://jja2han.tistory.com/486</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doCHtX/btsG5SValwq/QNIXvdspkU9h9gFAg9OnVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doCHtX/btsG5SValwq/QNIXvdspkU9h9gFAg9OnVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doCHtX/btsG5SValwq/QNIXvdspkU9h9gFAg9OnVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoCHtX%2FbtsG5SValwq%2FQNIXvdspkU9h9gFAg9OnVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/485&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] DataSource 적용 및 분리&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 서버와 안드로이드를 동시에 개발하고 있는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 안드로이드 개발만 경험해 본 상황에서 가볍게 서버의 코드를 작성하고 있는 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에 서버 개발자분들에게 무리하게 API를 요구했던 제 자신을 반성하게 되는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번글은 오류 처리에 관한 글인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 하면서 api를 처음 연결한 상황에서 에러 처리를 명확하게 짚고 넘어가고 싶어서 초반부에 코드를 굉장히 많이 수정했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 제가 고려했던 점은 일괄된 에러처리를 경험해 보자였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버개발자와 협업이었다면, 조금 더 수월하게 진행할 수 있었지만, 혼자 했기에 실수할만한 부분을 빠르게 해결했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 서버의 응답은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 성공 시&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2fHxT/btsG7pdyEYn/xkUKGgCzdfkAwXO2MMdYa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2fHxT/btsG7pdyEYn/xkUKGgCzdfkAwXO2MMdYa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2fHxT/btsG7pdyEYn/xkUKGgCzdfkAwXO2MMdYa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2fHxT%2FbtsG7pdyEYn%2FxkUKGgCzdfkAwXO2MMdYa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;799&quot; height=&quot;145&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 실패 시&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2hI7j%2FbtsG9fgtrxi%2FUWLKgek5oHNchgumXyKlu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;788&quot; height=&quot;185&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공과 실패에 대해서 첫 번째 분기점으로 200이냐 아니냐였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그를 위해 서버에서는 CustomError를 만들어서 클라이언트에게 보내줘야 하는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드를 하고 있는 저에겐 조금 어려운 일이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 예로 들면, 네이버 자동로그인에 성공했다면 유저의 정보와 AccessToken, RefreshToken을 모두 Dto에 담아서 서버에 전송합니다. 서버는 전달받은 Dto를 바탕으로 Mysql에 저장합니다. 하지만 여기서 만약 하나의 정보라도 빠진다면 저장이 되면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 그렇게 시나리오를 계획했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이러한 에러 처리를 안드로이드에서 하는 것보단 서버에서 하는 것이 맞다고 느꼈고, 커스텀 에러를 만들어서 서버에서 핸들링해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CustomErrorCode&lt;/h2&gt;
&lt;pre id=&quot;code_1714654098639&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum CustomErrorCode {
    INVALID_DATA_FORMAT(ErrorMessage.INVALID_DATA_FORMAT, HttpStatus.BAD_REQUEST, 400);
    private final String message;
    private final HttpStatus httpStatus;

    private final int statusCode;

    CustomErrorCode(String message, HttpStatus httpStatus, int statusCode) {
        this.message = message;
        this.httpStatus = httpStatus;
        this.statusCode = statusCode;
    }

    public String getMessage() {
        return message;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    public int getStatusCode() {
        return statusCode;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enum class로 CustomErrorCode에 대해서 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomErrorcode는 message와, httpStatus, statusCode를 생성자로 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 제가 생각했던 시나리오에 맞춰서 ErrorMessage와 httpStatus, statusCode를 작성해서 커스텀 에러 코드를 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 또 다른 에러를 추가해야 할 상황이 생긴다면&lt;/p&gt;
&lt;pre id=&quot;code_1714654226208&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INVALID_VIDEO_ID(ErrorMessage.INVLIAD_VIDEO_ID,HttpStatus.BAD_REQUEST,400);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 추가할 계획입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CustomException&lt;/h2&gt;
&lt;pre id=&quot;code_1714654279726&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomException extends RuntimeException {

    private final CustomErrorCode customErrorCode;
    private final String errorMessage;

    public CustomException(CustomErrorCode customErrorCode) {
        this.customErrorCode = customErrorCode;
        this.errorMessage = customErrorCode.getMessage();
    }

    public CustomErrorCode getCustomErrorCode() {
        return customErrorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 제가 생각한 시나리오의 에러가 발생한다면 던져야 할 Exception class가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이를 CustomException으로 정했으며, 위에서 구현한 customErrorCode와 errorMessage의 정보를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CustomExceptionHandler&lt;/h2&gt;
&lt;pre id=&quot;code_1714654375051&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
@Slf4j
public class CustomExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity&amp;lt;ApiErrorResult&amp;lt;?&amp;gt;&amp;gt; handleException(CustomException e, HttpServletRequest request) {
        log.error(&quot;errorCode : {}, errorMessage : {}, url : {}&quot;, e.getCustomErrorCode(), e.getErrorMessage(), request.getRequestURL());
        ApiErrorResult&amp;lt;?&amp;gt; errorResult = new ApiErrorResult&amp;lt;&amp;gt;(e.getCustomErrorCode().getStatusCode(), e.getErrorMessage());
        return ResponseEntity.status(e.getCustomErrorCode().getHttpStatus()).body(errorResult);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 만약 서버 로직에서 customException이 발생되고, 이를 위 handler에서 처리하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우는 ResponseEntity에다가 body로 담아서 보냈습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2hI7j/btsG9fgtrxi/UWLKgek5oHNchgumXyKlu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2hI7j%2FbtsG9fgtrxi%2FUWLKgek5oHNchgumXyKlu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;788&quot; height=&quot;185&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음과 같은 body와 httpStatus라는 결과가 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이를 Android에서 처리해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 로직 말고는 간단하게 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SignUpApi&lt;/h3&gt;
&lt;pre id=&quot;code_1714654937381&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @POST(&quot;signUp&quot;)
    suspend fun signUp(
        @Body userRequest: UserRequest,
    ): Response&amp;lt;UserSignUpResponse&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 코드에 맞는 UserSignUpResponse라는 구현체를 만들어서 서버와 통신을 진행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserSignUpResponse&lt;/h3&gt;
&lt;pre id=&quot;code_1714654967770&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class UserSignUpResponse(
    val data: Boolean? = null,
    val statusCode: String? = null,
    val message: String? = null,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 오는 코드는 data만을 담는 200Ok, statusCode와 message를 담는 나머지 에러들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대응하기 위해 nullable로 변수를 열어두고, 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserRepositoryImpl&lt;/h3&gt;
&lt;pre id=&quot;code_1714654919137&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; override fun signUp(userRequest: UserRequest): Flow&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; = flow {
        runCatching {
            val response = userRemoteDataSource.signUp(userRequest)
            Timber.d(&quot;mini-moment $response ${response.body()}&quot;)
            response
        }.onSuccess { response -&amp;gt;
            response.body()?.let { body -&amp;gt;
                body.data?.let { flag -&amp;gt;
                    localDataSource.saveAccessToken(userRequest.accessToken)
                    localDataSource.saveRefreshToken(userRequest.refreshToken)
                    emit(ApiResponse.Success(data = flag))
                } ?: run {
                    emit(
                        ApiResponse.Error(
                            errorCode = body.statusCode?.toInt() ?: 0,
                            errorMessage = body.message ?: &quot;&quot;,
                        ),
                    )
                }
            }
        }.onFailure { throwable -&amp;gt; // 다른 예외가 발생한 경우 -&amp;gt; 서버가 닫힌 경우
            emit(ApiResponse.Error(errorMessage = throwable.message ?: &quot;&quot;))
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository에서 회원가입에 대한 처리로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통상적인 에러 처리와 다르게 저는 실패와 성공 모두 바디를 담아서 보냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 점이 다르냐?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통상적인 서버 통신은 실패일 경우 body를 비워서 보냅니다, 반대로 성공한 경우는 body에 요청 데이터를 담습니다.&lt;/li&gt;
&lt;li&gt;하지만 저는 실패와 성공 모두 body를 담아서 보냈습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 모두 body를 담아서 보냈는가에 대해서 설명해 드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 실패 시 에러 처리를 클라이언트에서 statusCode에 따라서 하면 정말 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 회원가입 API에 대한 에러코드로 400이 온다면? 데이터 형식이 잘못되었다~~라고 처리를 하면 되는 것처럼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 더욱 복잡한 API와 서버 로직이 있을 경우, 이러한 예외 처리를 Android에서 한다면 매우 복잡할 것이라고 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(제가 서버 개발을 같이 진행하면서 느꼈던 점입니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서버에서 오류가 발생했다면 어떤 오류가 발생했는지 body에 담아서 클라이언트에서 body의 내용을 그대로 전달만 하는 구조로 만들기 위해 성공 실패 모두 body의 값이 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 요청에 대해 성공할 경우 다음과 같은 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7V5gs/btsG6XBBlm0/wclM5Ocf1cxf4RAcXUxl0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7V5gs/btsG6XBBlm0/wclM5Ocf1cxf4RAcXUxl0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7V5gs/btsG6XBBlm0/wclM5Ocf1cxf4RAcXUxl0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7V5gs%2FbtsG6XBBlm0%2FwclM5Ocf1cxf4RAcXUxl0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1056&quot; height=&quot;253&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선&amp;nbsp; OkHttpClient Log입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입이라는 post 요청에 대해 body로 data 값이 true라는 결괏값을 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 postman의 결과와 일치한 걸 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FnwVG/btsG7z1AhpT/02RBqmaE15x2BteUUeybZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FnwVG/btsG7z1AhpT/02RBqmaE15x2BteUUeybZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FnwVG/btsG7z1AhpT/02RBqmaE15x2BteUUeybZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFnwVG%2FbtsG7z1AhpT%2F02RBqmaE15x2BteUUeybZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;843&quot; height=&quot;134&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서도 body의 data값이 있기 때문에 Success로 처리함을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 실패 처리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패 처리에 대한 코드는 간단하게 회원가입의 정보중 하나를 null로 만들고 요청을 보냈습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nelbg/btsG8bTsFGw/vmBtKkrUxfJVBdrGSF4Cd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nelbg/btsG8bTsFGw/vmBtKkrUxfJVBdrGSF4Cd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nelbg/btsG8bTsFGw/vmBtKkrUxfJVBdrGSF4Cd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnelbg%2FbtsG8bTsFGw%2FvmBtKkrUxfJVBdrGSF4Cd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;534&quot; height=&quot;215&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttpClientLog에는 제가 의도란 대로 실패 처리와 body가 정확하게 들어온 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 부분에서 결과와 다르게 동작했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bagv8r/btsG8mtRArl/PoXkPqdKiL6XlXQsOPcKF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bagv8r/btsG8mtRArl/PoXkPqdKiL6XlXQsOPcKF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bagv8r/btsG8mtRArl/PoXkPqdKiL6XlXQsOPcKF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbagv8r%2FbtsG8mtRArl%2FPoXkPqdKiL6XlXQsOPcKF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;84&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;body가 null로 찍혔습니다. 분명히 log에는 body가 들어온 것을 확인할 수 있었고, PostMan으로 실행한 결과 body가 있었기 때문에 정말 당황스러웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 자세하게 살펴보기 위해 디버그를 해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLav7I/btsG52wEiCO/Sc1S27CCnXeTpEqtkha8NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLav7I/btsG52wEiCO/Sc1S27CCnXeTpEqtkha8NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLav7I/btsG52wEiCO/Sc1S27CCnXeTpEqtkha8NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLav7I%2FbtsG52wEiCO%2FSc1S27CCnXeTpEqtkha8NK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;977&quot; height=&quot;172&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response의 속성 중 body는 null로 찍혀있고, errorBody를 찾을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmNzXA/btsG8ZLJ9Fg/1OmfR6ZO405FaK1Baad3V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmNzXA/btsG8ZLJ9Fg/1OmfR6ZO405FaK1Baad3V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmNzXA/btsG8ZLJ9Fg/1OmfR6ZO405FaK1Baad3V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmNzXA%2FbtsG8ZLJ9Fg%2F1OmfR6ZO405FaK1Baad3V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;876&quot; height=&quot;198&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;errorBody안에는 많은 속성이 있지만, 실제로 body로 들어와야 할 데이터들이 담겨있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retrofit을 사용하면서 code가 200이 아니라면 원래 들어와야 할 body가 errorBody로 간다는 것을 처음 알게 된 것 같습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;errorBody의 asResponseBody를 변환해줘야 합니다. 왜냐하면 저 형태 그대로는 사용하지 못하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ErrorResponse라는 새로운 클래스를 만들어줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714656572110&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ErrorResponse(
    val statusCode: Int? = null,
    val message: String? = null,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 errorBody의 저 부분을 ErrorResponse로 변환해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 retrofit에서 사용했던 Gson 라이브러리를 통해서 해당 데이터를 ErrorResponse로 변환시켰습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714656641749&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val errorData = Gson().fromJson(result.errorBody()?.string(), ErrorResponse::class.java)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 response의 body가 null인 경우는 에러이기 때문에 다음과 같이 코드를 작성해 봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714656815691&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; runCatching {
            val response = userRemoteDataSource.signUp(userRequest)
            Timber.d(&quot;mini-moment response : $response&quot;)
            Timber.d(&quot;mini-moment response body : ${response.body()}&quot;)
            response
        }.onSuccess { response -&amp;gt;
            val errorBody =
                Gson().fromJson(response.errorBody()?.string(), ErrorResponse::class.java)
            Timber.d(&quot;mini-moment errorBody : $errorBody$&quot;)
            response.body()?.let { body -&amp;gt;
                body.data?.let { flag -&amp;gt;
                    localDataSource.saveAccessToken(userRequest.accessToken)
                    localDataSource.saveRefreshToken(userRequest.refreshToken)
                    emit(ApiResponse.Success(data = flag))
                } ?: run {
                    emit(
                        ApiResponse.Error(
                            errorCode = body.statusCode?.toInt() ?: 0,
                            errorMessage = body.message ?: &quot;&quot;,
                        ),
                    )
                }
            } ?: run {
                emit(
                    ApiResponse.Error(
                        errorCode = errorBody.statusCode ?: 0,
                        errorMessage = errorBody.message ?: &quot;&quot;,
                    ),
                )
            }
        }.onFailure { throwable -&amp;gt; // 다른 예외가 발생한 경우 -&amp;gt; 서버가 닫힌 경우,서버 url이 잘못된 경우
            emit(ApiResponse.Error(errorMessage = throwable.message ?: &quot;&quot;))
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOuMIr/btsG9AShfuD/3cKYLCUFAOq9NYilPuHxzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOuMIr/btsG9AShfuD/3cKYLCUFAOq9NYilPuHxzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOuMIr/btsG9AShfuD/3cKYLCUFAOq9NYilPuHxzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOuMIr%2FbtsG9AShfuD%2F3cKYLCUFAOq9NYilPuHxzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;149&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;149&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공적으로 에러가 난 경우의 body를 꺼내서 Error 객체에 담을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 최적화입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 Api 호출의 결과를 핸들링하기 위해서 위와 같은 로직이 반복될게 자명했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와의 통신도 일괄적인 형태로 진행될 것이기 때문에 분기 처리의 조건은 다음과 같았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;body가 null이 아닐 경우 성공, null일 경우 에러&lt;/li&gt;
&lt;li&gt;body의 data가 null이 아닐 경우 성공, null일 경우 에러&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 조건을 일반화해서 함수로 처리할 수 없을까를 고민했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 필요한 정보는 response 객체와 errorBody입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response의 타입을 T로 일반화시켜서 모든 타입에 대해서 대응하고자 하는 것이 목적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;errorBody는 있는 경우 에러 처리를 위해 사용될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 정보를 바탕으로 다음과 같은 코드를 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714657496043&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun &amp;lt;T&amp;gt; apiHandler(
    apiResponse: suspend () -&amp;gt; Response&amp;lt;T&amp;gt;,
): ApiResponse&amp;lt;T&amp;gt; {
    runCatching {
        val response = apiResponse.invoke()
        val errorData =
            Gson().fromJson(response.errorBody()?.string(), ErrorResponse::class.java)
        Timber.d(&quot;mini-moment errorData : $errorData&quot;)
        if (response.isSuccessful) {
            response.body()?.let { body -&amp;gt;
                return ApiResponse.Success(body)
            }
        } else {
            return ApiResponse.Error(
                errorCode = errorData.statusCode ?: 0,
                errorMessage = errorData.message ?: &quot;&quot;,
            )
        }
    }.onFailure {
        return ApiResponse.Error(errorMessage = it.message ?: &quot;&quot;)
    }
    return ApiResponse.Failure
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JOjHW/btsG53oNBSs/6WiTNV72yUQoR9x6Bafnr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JOjHW/btsG53oNBSs/6WiTNV72yUQoR9x6Bafnr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JOjHW/btsG53oNBSs/6WiTNV72yUQoR9x6Bafnr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJOjHW%2FbtsG53oNBSs%2F6WiTNV72yUQoR9x6Bafnr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2990&quot; height=&quot;145&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log를 자세히 보시면 errorData가 null이고, 현재 Response의 타입의 ErrorBody를 ErrorResponse로 바꿀 수가 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민을 하다가 apiHandler에 errorData를 response와 같이 전달했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714657199542&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun &amp;lt;T&amp;gt; apiHandler(
    apiResponse: suspend () -&amp;gt; Pair&amp;lt;Response&amp;lt;T&amp;gt;, ErrorResponse?&amp;gt;,
): ApiResponse&amp;lt;T&amp;gt; {
    runCatching {
        val action = apiResponse.invoke()
        val response = action.first
        if (response.isSuccessful) {
            response.body()?.let { body -&amp;gt;
                return ApiResponse.Success(body)
            }
        } else {
            return ApiResponse.Error(
                errorCode = action.second?.statusCode ?: 0,
                errorMessage = action.second?.message ?: &quot;&quot;,
            )
        }
    }.onFailure {
        return ApiResponse.Error(errorMessage = it.message ?: &quot;&quot;)
    }
    return ApiResponse.Failure
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1isnB/btsG9eaPMq6/71BMsRD0tLK4zyk1oZ6Wk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1isnB/btsG9eaPMq6/71BMsRD0tLK4zyk1oZ6Wk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1isnB/btsG9eaPMq6/71BMsRD0tLK4zyk1oZ6Wk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1isnB%2FbtsG9eaPMq6%2F71BMsRD0tLK4zyk1oZ6Wk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;94&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밖에서 타입이 명시된 상황에서 ErrorResponse로의 변환은 가능했고, body가 있다면 성공, 없다면 errorBody의 code와 message를 Error로 담아서 return 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runCatching 밖의 블록은 네트워크 통신이 들어가기 전 실패한 것으로 서버 url 주소가 잘못된 경우가 대표적인 예입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 UserRepositoryImpl&amp;nbsp; signUp 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714657810199&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override fun signUp(userRequest: UserRequest): Flow&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; = flow {
        val response = apiHandler {
            val result = userRemoteDataSource.signUp(userRequest)
            val errorData = Gson().fromJson(result.errorBody()?.string(), ErrorResponse::class.java)
            Pair(result, errorData)
        }
        when (response) {
            is ApiResponse.Success -&amp;gt; {
                localDataSource.saveAccessToken(userRequest.accessToken)
                localDataSource.saveRefreshToken(userRequest.refreshToken)
                emit(ApiResponse.Success(data = response.data.data ?: false))
            }

            is ApiResponse.Error -&amp;gt; {
                emit(
                    ApiResponse.Error(
                        errorCode = response.errorCode,
                        errorMessage = response.errorMessage,
                    ),
                )
            }

            else -&amp;gt; {}
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apiHandler를 통해서 복잡한 runCatching 로직을 함수로 처리하고, response의 타입에 따라 핸들링하는 직관적인 코드가 탄생했습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성하고 보니 읽기도 편하고, 로직도 간단해 보여서 정말 만족했던 코드였던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200이 아닌 경우 body값이 errorBody로 넘어가는 등,, 몰랐던 개념을 많이 배운 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 에러 처리에 대해 깔끔하게 한 것 같아서 뿌듯하네요 ㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>apihandler</category>
      <category>RETROFIT</category>
      <category>spring</category>
      <category>에러 핸들링</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/486</guid>
      <comments>https://jja2han.tistory.com/486#entry486comment</comments>
      <pubDate>Thu, 2 May 2024 22:54:38 +0900</pubDate>
    </item>
    <item>
      <title>[Android] DataSource 적용 및 분리</title>
      <link>https://jja2han.tistory.com/485</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8in1/btsG36r1KQh/H2EZd6bVFHYWQ4kWC8JAb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8in1/btsG36r1KQh/H2EZd6bVFHYWQ4kWC8JAb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8in1/btsG36r1KQh/H2EZd6bVFHYWQ4kWC8JAb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8in1%2FbtsG36r1KQh%2FH2EZd6bVFHYWQ4kWC8JAb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/484&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 로그인 프로필 가져오기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 PR에서 흥미로운 리뷰를 받았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XqyJx/btsG6e3dJoH/YPEURU9BpJcg0ky2KefEMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XqyJx/btsG6e3dJoH/YPEURU9BpJcg0ky2KefEMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XqyJx/btsG6e3dJoH/YPEURU9BpJcg0ky2KefEMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXqyJx%2FbtsG6e3dJoH%2FYPEURU9BpJcg0ky2KefEMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;603&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 레포지토리 위에 하나의 계층을 두어 DataSource를 적용해 본 적이 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위 계층을 하나 두면 오히려 복잡하지 않을까라는 생각으로 학습을 한 뒤 코드로 적용해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1FYVr/btsG7oj3jov/8yLMMPP35TZzI3koS6oru1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1FYVr/btsG7oj3jov/8yLMMPP35TZzI3koS6oru1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1FYVr/btsG7oj3jov/8yLMMPP35TZzI3koS6oru1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1FYVr%2FbtsG7oj3jov%2F8yLMMPP35TZzI3koS6oru1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;436&quot; height=&quot;392&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 안드로이드에서 권장하는 DataLayer의 방식은 DataSource -&amp;gt; Respository의 흐름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 굳이? Data Sources까지 만들어야 하는 이유가 뭘까에 대해서 궁금했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이는 제가 Repository 패턴에 대해서 잘못 해석을 하고 개발을 했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository는 직접적으로 데이터를 작성하고, 수정하는 형태가 아닌 데이터를 UI layer에 제공하고, UI에서 사용하려는 데이터로 변환하는 mapper 작업을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data를 직접적으로 작성, 수정, 삭제하는 CRUD 작업은 레포지토리가 아닌 DataSource 계층에서 해야 할 일이라는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 API를 호출한 결과 DTO와 UI에서 사용하는 데이터의 형식은 다를 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714573432685&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array&amp;lt;ArticleApiModel&amp;gt;,
    val comments: Array&amp;lt;CommentApiModel&amp;gt;,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 데이터를 UI에서 모두 사용하지 않는다면 굳이 해당 데이터를 UI로 전달해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안되는 것은 아니지만 필요 이상으로 많은 정보를 전달할 필요도 없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714573502511&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI에서 필요로 한 데이터를 바탕으로 클래스를 재정의하고, DataSource에서 내려받은 데이터를 변환하는 작업 즉 mapper를&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리에서 담당하고, 이를 viewModel로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 레포지토리에서 받아온 데이터에 대해 성공과 실패를 처리하기도 하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 UI에서 사용할 데이터로 변환하는 mapper 작업은 부가적인 클래스를 만들어야 하기 때문에 클래스 수가 많아지는 단점이 있지만, 실제 사용하려는 데이터의 형태와 다를 경우 Model을 분리하는 것을 권장한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 그렇듯 저도 Local DataSource와 Remote DataSource로 분리를 해 데이터의 출처를 명확하게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local DataSource는 room, sharedPreference, dataStore 등 안드로이드 내부 저장소를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 remote는 서버와 Network 통신을 통해 내려받은 데이터의 출처를 의미합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LocalDataSource&lt;/h2&gt;
&lt;pre id=&quot;code_1714574232696&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface LocalDataSource {
    suspend fun getAccessToken(): String
    suspend fun getRefreshToken(): String
    suspend fun saveAccessToken(accessToken: String)
    suspend fun saveRefreshToken(refreshToken: String)
    suspend fun deleteAccessToken()
    suspend fun deleteRefreshToken()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 DataStore를 이용해 토큰을 저장, 삭제, 조회하기 때문에 LocalDataSource를 다음과 같이 정의했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714574281133&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class LocalDataSourceImpl @Inject constructor(
    private val dataStore: DataStore&amp;lt;Preferences&amp;gt;,
) : LocalDataSource {
    override suspend fun getAccessToken(): String = dataStore.data.map { preferences -&amp;gt;
        preferences[ACCESS_TOKEN_KEY] ?: &quot;&quot;
    }.first()

    override suspend fun getRefreshToken(): String = dataStore.data.map { preferences -&amp;gt;
        preferences[REFRESH_TOKEN_KEY] ?: &quot;&quot;
    }.first()

    override suspend fun saveAccessToken(accessToken: String) {
        dataStore.edit { prefs -&amp;gt;
            prefs[ACCESS_TOKEN_KEY] = accessToken
        }
    }

    override suspend fun saveRefreshToken(refreshToken: String) {
        dataStore.edit { prefs -&amp;gt;
            prefs[REFRESH_TOKEN_KEY] = refreshToken
        }
    }

    override suspend fun deleteAccessToken() {
        dataStore.edit { prefs -&amp;gt;
            prefs.remove(ACCESS_TOKEN_KEY)
        }
    }

    override suspend fun deleteRefreshToken() {
        dataStore.edit { prefs -&amp;gt;
            prefs.remove(ACCESS_TOKEN_KEY)
        }
    }

    companion object {
        val ACCESS_TOKEN_KEY = stringPreferencesKey(BuildConfig.ACCESS_TOKEN_KEY)
        val REFRESH_TOKEN_KEY = stringPreferencesKey(BuildConfig.REFRESH_TOKEN_KEY)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했듯이 DataSource의 책임은 데이터의 CRUD 이기 때문에 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 해당 데이터를 사용할 Repository에 데이터를 내려주기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDataSource와 의존하고 있는 Repository는 다음과 같이 구현해 봤습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DataStoreRepositoryImpl&lt;/h3&gt;
&lt;pre id=&quot;code_1714574395692&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DataStoreRepositoryImpl @Inject constructor(
    private val localDataSource: LocalDataSource,
) : DataStoreRepository {
    override suspend fun getUserToken(): Flow&amp;lt;ApiResponse&amp;lt;LoginResponse&amp;gt;&amp;gt; = flow {
        val accessToken = localDataSource.getAccessToken()
        val refreshToken = localDataSource.getRefreshToken()
        if (accessToken.isBlank().not() &amp;amp;&amp;amp; refreshToken.isBlank().not()) {
            emit(
                ApiResponse.Success(
                    LoginResponse(
                        accessToken = accessToken,
                        refreshToken = refreshToken,
                    ),
                ),
            )
        } else if (accessToken.isBlank()) {
            emit(
                ApiResponse.Error(errorMessage = ErrorMessage.NO_ACCESS_TOKEN_MESSAGE),
            )
        } else {
            emit(
                ApiResponse.Error(errorMessage = ErrorMessage.NO_REFRESH_TOKEN_MESSAGE),
            )
        }
    }

    override suspend fun saveAccessToken(accessToken: String) {
        localDataSource.saveAccessToken(accessToken)
    }

    override suspend fun saveRefreshToken(refreshToken: String) {
        localDataSource.saveRefreshToken(refreshToken)
    }

    override suspend fun deleteAccessToken() {
        localDataSource.deleteAccessToken()
    }

    override suspend fun deleteRefreshToken() {
        localDataSource.deleteRefreshToken()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource로부터 얻어온 토큰을 통해 viewModel에게 전달한 결괏값을 핸들링하고, 사용될 정보만을 담는 객체로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에선 토큰이 있는지 없는지만을 검사하기 때문에 isBlank.not을 통해서 비어있는지 검사하고, 그에 따른 결과를 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel의 코드는 변하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실질적인 비즈니스 로직은 변화하지 않았기 때문에 Repository에서 내려주는 결괏값에 따라 UI를 업데이트합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RemoteDataSource&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 UserRepository가 api와 의존해서 서버 호출에 대한 데이터를 내려받고, 그 과정에서 에러 핸들링과 매퍼를 담당했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선된 구조에서는 UserRepository가 api와 의존하는 것이 아닌 RemoteDataSource와 의존하고, 거기서 데이터를 내려받고, 핸들리오가 매퍼를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserRemoteDataSource&lt;/h3&gt;
&lt;pre id=&quot;code_1714574850433&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserRemoteDataSource {
    suspend fun signUp(userRequest: UserRequest): Response&amp;lt;UserSignUpResponse&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserRemoteDataSourceImpl&lt;/h3&gt;
&lt;pre id=&quot;code_1714574891862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserRemoteDataSourceImpl @Inject constructor(
    private val userApi: UserApi,
) : UserRemoteDataSource {
    override suspend fun signUp(userRequest: UserRequest): Response&amp;lt;UserSignUpResponse&amp;gt; {
        return userApi.signUp(userRequest)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RemoteDataSource가 서버와 직접적으로 맞닿아 통신을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserRepositoryImpl&lt;/h3&gt;
&lt;pre id=&quot;code_1714575158218&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserRepositoryImpl @Inject constructor(
    private val userRemoteDataSource: UserRemoteDataSource,
    private val localDataSource: LocalDataSource,
) : UserRepository {
    override fun signUp(userRequest: UserRequest): Flow&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; = flow {
        runCatching {
            userRemoteDataSource.signUp(userRequest)
        }.onSuccess { response -&amp;gt;
            response.body()?.let {
                it.data?.let {
                    localDataSource.saveAccessToken(userRequest.accessToken)
                    localDataSource.saveRefreshToken(userRequest.refreshToken)
                    emit(ApiResponse.Success(data = it))
                } ?: run {
                    emit(
                        ApiResponse.Error(
                            errorCode = it.statusCode ?: ErrorCode.NONE,
                            errorMessage = it.message ?: &quot;&quot;,
                        ),
                    )
                }
            }
        }.onFailure { // 다른 예외가 발생한 경우 -&amp;gt; 서버가 닫힌 경우
            emit(ApiResponse.Error(errorMessage = it.message ?: &quot;&quot;))
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserRepository는 LocalDataSource와 RemoteDataSource 모두 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 요청에 대한 결과를 위한 Remote, 성공했다면 Local에 저장하기 위한 LocalDataSource에 접근해야 하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 코드는 UserRepository가 다른 레포지토리(DataStoreRepository)와 의존하는 구조였는데, DataSource라는 상위 계층을 의존하는 방향으로 변경됐고, 꽤나 직관적인 구조를 따르고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGtMH/btsG4VcCmpl/4YX2WV6nbHXEWZzEIMyPB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGtMH/btsG4VcCmpl/4YX2WV6nbHXEWZzEIMyPB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGtMH/btsG4VcCmpl/4YX2WV6nbHXEWZzEIMyPB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGtMH%2FbtsG4VcCmpl%2F4YX2WV6nbHXEWZzEIMyPB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;502&quot; height=&quot;551&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI에서 직접적으로 DataSource 클래스를 직접 참조하지 못하도록 중간 계층인 Repository를 이용하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repository 클래스는 DataSource와 UI 사이에 위치해, mapper와 결과에 대한 에러 핸들링을 담당합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 그림에서 혼동이 오는 곳이 있는데, LoginViewModel이 DataStoreRepository를 의존하는 것이 아닌&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;User Repository가 RemoteDataSource와 LocalDataSource에 의존하고 있는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;과거에는 레포지토리가 다른 레포지토리에 의존하지 않고, 데이터의 출처가 하나라면 레포지토리가 DataSource의 역할을 대신해도 되지 않을까?라는 생각으로 두 계층을 하나로 묶어서 사용했었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 UserRepository처럼 데이터의 출처가 여러 곳이라면 위 방법은 조금 위험하며,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의존하는 Repository가 변경된다면, 변경되어야 할 코드가 굉장히 많아지고, 분리하는 것이 안전한 방식이라는 것을 느낀 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 domain Layer 까지 생각한다면 더더욱 그렇게 해야하구요,,&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Data layer</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/485</guid>
      <comments>https://jja2han.tistory.com/485#entry485comment</comments>
      <pubDate>Thu, 2 May 2024 00:05:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 네이버 로그인 프로필 가져오기</title>
      <link>https://jja2han.tistory.com/484</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL6mjq/btsG3AGXrdq/8RHki9WmYkjgQgDY7iGkhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL6mjq/btsG3AGXrdq/8RHki9WmYkjgQgDY7iGkhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL6mjq/btsG3AGXrdq/8RHki9WmYkjgQgDY7iGkhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL6mjq%2FbtsG3AGXrdq%2F8RHki9WmYkjgQgDY7iGkhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/483&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 자동 로그인 with DataStore(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 포스팅에서는 DataStore를 이용하면서 자동로그인을 구현해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정에서 매번 DataStore를 사용하는 곳에서 DataStore를 주입해줘야 하는 상황이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 추후 개발에서 API와 Repository가 많이 생기기 때문에 개발 초반부에 Hilt를 적용하는 것이 생산성에 도움이 될 것이라고 판단하고 적용해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 코드는 Application에서 선언한 DataStore를 사용했지만, Hilt를 통해서 주입해 줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714570541273&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@InstallIn(SingletonComponent::class)
@Module
object DataStoreModule {

    @Singleton
    @Provides
    fun providesDataStore(@ApplicationContext appContext: Context): DataStore&amp;lt;Preferences&amp;gt; {
        return PreferenceDataStoreFactory.create {
            appContext.preferencesDataStoreFile(BuildConfig.DATASTORE_NAME)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 로그인을 한 후 유저의 정보를 저장하는 API를 구현해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 작성한 서버 API의 명세서는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lrDNq/btsG339T9ZD/BCmEtQo5UFTMpHDNAwGe8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lrDNq/btsG339T9ZD/BCmEtQo5UFTMpHDNAwGe8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lrDNq/btsG339T9ZD/BCmEtQo5UFTMpHDNAwGe8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlrDNq%2FbtsG339T9ZD%2FBCmEtQo5UFTMpHDNAwGe8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1438&quot; height=&quot;450&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 서버에 요청할 Request Data를 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714570643084&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class UserRequest(
    val userName: String = &quot;&quot;,
    val userContact: String = &quot;&quot;,
    val accessToken: String = &quot;&quot;,
    val refreshToken: String = &quot;&quot;,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id값은 디비에서 Auto-Increment로 설정했기 때문에 필요 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 서버의 UserDto와 변수명이 일치해야 합니다..(아니라면 실패합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 데이터와 요청도 정상적이었는데, 오타 때문에 실패해서 한참 헤맸습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Android에서 레트로핏으로 호출할 SignUpApi를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성하기 전 Retrofit을 주입하기 위해 NetworkModule을 구현해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714570977270&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        val apiKey = BASE_URL
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
            .baseUrl(apiKey)
            .client(okHttpClient)
            .build()
    }

    @Singleton
    @Provides
    fun provideOkHttpClient() = OkHttpClient.Builder().run {
        addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        connectTimeout(120, TimeUnit.SECONDS)
        readTimeout(120, TimeUnit.SECONDS)
        writeTimeout(120, TimeUnit.SECONDS)
        build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SignUpApi입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714570828698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@POST(&quot;signUp&quot;)
suspend fun signUp(
    @Body userRequest: UserRequest,
): Response&amp;lt;Boolean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 성공했다면 True, 실패했다면 False를 반환하기 때문에 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Module&lt;/p&gt;
&lt;pre id=&quot;code_1714571144180&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Provides
    @Singleton
    fun provideUserApi(
        retrofit: Retrofit,
    ): UserApi = retrofit.create()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7irG7/btsG51JOcxr/Q3Dx013tn8KKuv9fUKVLlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7irG7/btsG51JOcxr/Q3Dx013tn8KKuv9fUKVLlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7irG7/btsG51JOcxr/Q3Dx013tn8KKuv9fUKVLlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7irG7%2FbtsG51JOcxr%2FQ3Dx013tn8KKuv9fUKVLlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1418&quot; height=&quot;134&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레트로핏의 확장함수인 create를 통해서 타입만 명시해 준다면 UserApi::class.java 코드를 작성해주시지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Data Layer의 설계입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOvtjB/btsG3Ne2HMP/OHywKcdKqcxm4AhFscmKSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOvtjB/btsG3Ne2HMP/OHywKcdKqcxm4AhFscmKSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOvtjB/btsG3Ne2HMP/OHywKcdKqcxm4AhFscmKSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOvtjB%2FbtsG3Ne2HMP%2FOHywKcdKqcxm4AhFscmKSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;598&quot; height=&quot;416&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리 패턴을 이용해 Respository가 api를 호출하고 그에 대한 데이터를 viewModel에 전달하는 구조로 개발을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserRepository&lt;/h2&gt;
&lt;pre id=&quot;code_1714571475876&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserRepository {

    fun signUp(userRequest: UserRequest): Flow&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserRepositoryImpl&lt;/h2&gt;
&lt;pre id=&quot;code_1714571493650&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserRepositoryImpl @Inject constructor(private val api: UserApi) : UserRepository {
    override fun signUp(userRequest: UserRequest): Flow&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; = flow {
        val response = api.signUp(userRequest)
        runCatching {
            response
        }.onSuccess {
            if (it.isSuccessful) {
                emit(ApiResponse.Success(data = true))
            } else {
                emit(ApiResponse.Failure)
            }
        }.onFailure {
            emit(
                ApiResponse.Error(
                    errorCode = response.code(),
                    errorMessage = it.message ?: &quot;&quot;,
                ),
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api의 반환타입은 response입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-catch를 통해 예외 처리를 하는 것도 방법이지만, 저는 runCatching을 통해서 예외 처리를 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714571592458&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public inline fun &amp;lt;T, R&amp;gt; T.runCatching(block: T.() -&amp;gt; R): Result&amp;lt;R&amp;gt; {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runCatching도 내부에서 try - catch를 진행하고, onSuccess와 onFailure이라는 조금 더 직관적인 코드를 제시해 주기 때문에 더 선호합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel에서 API를 호출하기 전 Request Data의 값들을 가져와야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전에 저번에 작성했던 NaverLoginManager에서 로그인을 성공했다면 유저의 정보를 가져오는 코드를 구현해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714572096319&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private val profileCallback = object : NidProfileCallback&amp;lt;NidProfileResponse&amp;gt; {
    override fun onSuccess(response: NidProfileResponse) {
        _loginResult.value = ApiResponse.Success(
            LoginResponse(
                userName = response.profile?.name ?: &quot;&quot;,
                userContact = response.profile?.mobile ?: &quot;&quot;,
                accessToken = NaverIdLoginSDK.getAccessToken() ?: &quot;&quot;,
                refreshToken = NaverIdLoginSDK.getRefreshToken() ?: &quot;&quot;,
            ),
        )
    }

    override fun onFailure(httpStatus: Int, message: String) {
        _loginResult.value = ApiResponse.Error(
            errorCode = ErrorCode.FAILED_LOGIN,
            errorMessage = message,
        )
    }

    override fun onError(errorCode: Int, message: String) {
        onFailure(errorCode, message)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;profileCallback은 네이버 로그인에서 제공해 주는 콜백으로, 로그인을 성공했다면 유저의 정보를 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 해당&amp;nbsp; 콜백은 로그인에 성공했다면 호출해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714572096320&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private val oauthLoginCallback = object : OAuthLoginCallback {
    override fun onSuccess() {
        NidOAuthLogin().callProfileApi(profileCallback)
    }

    override fun onFailure(httpStatus: Int, message: String) {
        _loginResult.value = ApiResponse.Error(
            errorCode = ErrorCode.FAILED_LOGIN,
            errorMessage = message,
        )
    }

    override fun onError(errorCode: Int, message: String) {
        onFailure(errorCode, message)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginViewModel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 viewModel에서 signUp을 호출해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714571762014&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun observerLoginResponse() {
        viewModelScope.launch {
            NaverLoginManager.loginResult.collectLatest { loginResponse -&amp;gt;
                when (loginResponse) {
                    is ApiResponse.Success -&amp;gt; {
                        userSignUp(loginResponse.data)
                    }

                    is ApiResponse.Error -&amp;gt; {
                        _event.emit(
                            LoginEvent.Error(
                                errorCode = loginResponse.errorCode,
                                errorMessage = loginResponse.errorMessage,
                            ),
                        )
                    }

                    is ApiResponse.Failure -&amp;gt; {}
                }
            }
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 로그인의 성공했다면 내부의 signUp 함수를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714571792474&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private suspend fun userSignUp(loginResponse: LoginResponse) {
    userRepository.signUp(
        UserRequest(
            userName = loginResponse.userName ?: &quot;&quot;,
            userContact = loginResponse.userContact ?: &quot;&quot;,
            accessToken = loginResponse.accessToken ?: &quot;&quot;,
            refreshToken = loginResponse.refreshToken ?: &quot;&quot;,
        ),
    ).collectLatest { signUpResponse -&amp;gt;
        when (signUpResponse) {
            is ApiResponse.Success -&amp;gt; _event.emit(LoginEvent.Success)
            is ApiResponse.Failure -&amp;gt; {}
            is ApiResponse.Error -&amp;gt; _event.emit(
                LoginEvent.Error(
                    errorCode = signUpResponse.errorCode,
                    errorMessage = signUpResponse.errorMessage,
                ),
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 디비에 정보를 성공적으로 저장했다면 LoginEvent를 Success로, 실패했다면 Error를 View에 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>naver 로그인</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/484</guid>
      <comments>https://jja2han.tistory.com/484#entry484comment</comments>
      <pubDate>Wed, 1 May 2024 23:04:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 자동 로그인 with DataStore(Kotlin)</title>
      <link>https://jja2han.tistory.com/483</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKWiIa/btsG0FuhAHw/S9W0NJEu49ifo7hfZ4opwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKWiIa/btsG0FuhAHw/S9W0NJEu49ifo7hfZ4opwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKWiIa/btsG0FuhAHw/S9W0NJEu49ifo7hfZ4opwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKWiIa%2FbtsG0FuhAHw%2FS9W0NJEu49ifo7hfZ4opwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 관련 글&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jja2han.tistory.com/482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android] 네이버 간편 로그인(Kotlin)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플을 사용하다 보면 어플의 로고가 뜨고 메인화면으로 넘어가는 구조를 보신 적이 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; Android에서는 &lt;code&gt;SplashActivity&lt;/code&gt;로 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pPoOq/btsG1eJZWDK/1gxJkEf25ujSkO3a0sNiYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pPoOq/btsG1eJZWDK/1gxJkEf25ujSkO3a0sNiYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pPoOq/btsG1eJZWDK/1gxJkEf25ujSkO3a0sNiYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpPoOq%2FbtsG1eJZWDK%2F1gxJkEf25ujSkO3a0sNiYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;488&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 화면 진입점인 Splash Activity에서 자동 로그인을 요청하고, 실패했다면 Login 화면으로, 성공했다면 Main 화면으로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 그럼 궁금증은 &lt;b&gt;자동 로그인을 요청하는 곳은 어디이며, 어떠한 로직으로 결과를 반환하는가?입니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 remote 요청과 local 요청이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;remote 요청 : 서버 요청으로 인한 결과 반환&lt;/li&gt;
&lt;li&gt;local 요청 : Room, DataStore, sharedPreference 등 Android 내부 요청으로 인한 결과 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청하는 수단은 우선 서버는 제외했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 유저의 액세스토큰과 리프래시 토큰의 유무를 검사하는 작업이 굳이 서버를 통해야 하나라는 생각이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 있었다면 액세스토큰이 만료된 경우 리프래시 토큰을 통해 액세스 토큰을 갱신하고, 이를 업데이트해야 했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드와 서버 동시에 진행하고 있어서 Oauth까지 처리하기엔 무리라고 판단했습니다..ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Local에서 간단하게 토큰 정보를 저장하기에 Room을 사용하는 것은 적절하지 않다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 &lt;code&gt;단순한 토큰의 정보를 저장하기 때문&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 요청하는 수단은 &lt;code&gt;DataStore&lt;/code&gt;와 &lt;code&gt;sharedPreference&lt;/code&gt; 중 고민했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SharedPreference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sharedPreference는 초창기 기술로 가벼운 데이터를 &lt;code&gt;key-value쌍&lt;/code&gt;으로 저장하는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 기술에 비해 처음 나온 기술이기에 다른 기술에 비해 많은 한계가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 &lt;b&gt; UI Thread에 안전하지 않다입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 파일에서 쓰고 읽기 때문에 &lt;code&gt;IO Thread&lt;/code&gt;에서 작업을 해야 하는데, 이를 &lt;code&gt;Main Thread&lt;/code&gt;에서 진행할 경우 &lt;code&gt;ANR&lt;/code&gt; 오류가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로, &lt;b&gt;RunTime Exception에 취약하다입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 Excepion에 대한 &lt;code&gt;에러 핸들링을 제공하지 않기&lt;/code&gt; 때문에, 에러를 처리하기에 어려움이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Android 공식문서에서도 DataStore를 권장한다고 나와있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUTdDr/btsGZOk2uDY/iaR1BILRkCtS2IQ579yriK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUTdDr/btsGZOk2uDY/iaR1BILRkCtS2IQ579yriK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUTdDr/btsGZOk2uDY/iaR1BILRkCtS2IQ579yriK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUTdDr%2FbtsGZOk2uDY%2FiaR1BILRkCtS2IQ579yriK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;903&quot; height=&quot;320&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DataStore&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;코루틴&lt;/code&gt; 및 &lt;code&gt;Flow&lt;/code&gt;를 지원하기에 이에 대한 이점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 작업이 가능하다.&lt;/li&gt;
&lt;li&gt;데이터를 저장하거나, 읽는 무거운 작업은 Dispatchers.IO에서 작업되기 때문에 UI Thread에서 사용해도 안 접합니다.&lt;/li&gt;
&lt;li&gt;그 외에도 Flow의 확장함수인 Catch와 코루틴을 사용하기 때문에 Coroutine exceptions handler를 통해 에러에 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 비교만 하고, 설정과 사용법에 대해서는 다른 좋은 글이 많기 때문에 생략하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 자동 로그인을 요청하고 결과를 반환하기 위해 Local 저장소중 DataStore를 선택했는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataStore는 같은 프로세스에서 인스턴스를 2개 이상 만들지 않는 것을 권장하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Hilt를 사용한다면 ApplicationContext를 통해서 주입할 수 있지만, 아직 Hilt를 적용하지 않았기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 상위인 Application에서 DataStore를 선언했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 싱글톤 형태로 DataStore를 유지할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val Context.dataStore
    : DataStore&amp;lt;Preferences&amp;gt; by preferencesDataStore(name = BuildConfig.DATASTORE_NAME)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각한 시나리오는 최초 로그인 시 데이터스토어에 액세스토큰과 리프래시 토큰을 저장한 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 로그인 시 데이터 스토어를 훑으면서 토큰이 있는지 검사하고, 자동 로그인을 처리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그를 위해 최초 로그인 시 호출되는 콜백에 결과를 추적할 필요가 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun observerLoginResponse() {
        viewModelScope.launch {
            NaverLoginManager.loginResult.collectLatest { loginResponse -&amp;gt;
                when (loginResponse) {
                    is ApiResponse.Success -&amp;gt; {
                        dataStoreRepository.saveAccessToken(loginResponse.data)
                        _event.emit(LoginEvent.Success)
                    }

                    is ApiResponse.Error -&amp;gt; {
                        _event.emit(
                            LoginEvent.Error(
                                errorCode = loginResponse.errorCode,
                                errorMessage = loginResponse.errorMessage,
                            ),
                        )
                    }

                    is ApiResponse.Failure -&amp;gt; {}
                }
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 코드에서 성공 시 dataStoreRepository에 saveAccessToken을 reponse의 data를 담아서 요청했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response의 data는 액세스토큰과 refresh토큰이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DataStoreRepository&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 액세스 토큰을 저장하는 saveAccessToken입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun saveAccessToken(loginSuccess: LoginResponse) {
    dataStore.edit { prefs -&amp;gt;
        prefs[ACCESS_TOKEN_KEY] = loginSuccess.accessToken
        prefs[REFRESH_TOKEN_KEY] = loginSuccess.refreshToken
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sharedPreference와 다르게 비동기 작업을 지원해 주기 때문에 suspend 키워드는 반드시 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 key-value 쌍에 맞게 액세스토큰과 리프래시 토큰을 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 키의 맞는 액세스토큰과 리프래시 토큰이 있을 경우 저는 자동 로그인을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그를 위해 Splash Activity에서 토큰을 검사해야 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그를 위해 getAccessToken이라는 함수를 살펴보면,&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;fun getAccessToken(): Flow&amp;lt;ApiResponse&amp;lt;LoginResponse&amp;gt;&amp;gt; = dataStore.data.map { prefs -&amp;gt;
    if (prefs[ACCESS_TOKEN_KEY] == null) {
        ApiResponse.Failure
    } else {
        ApiResponse.Success(
            LoginResponse(
                accessToken = prefs[ACCESS_TOKEN_KEY] ?: &quot;&quot;,
                refreshToken = prefs[REFRESH_TOKEN_KEY] ?: &quot;&quot;,
            ),
        )
    }
}.catch { exception -&amp;gt;
    ApiResponse.Error(
        errorMessage = exception.message ?: &quot;&quot;,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환타입을 Flow 형태로 LoginResponse를 감싸는 ApiResponse로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataStore의 data를 훑으면서, 저장된 액세스 토큰이 있는지 검사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 액세스 토큰이 없다면 실패를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;있다면 액세스토큰과 리프래시 토큰을 담아서 성공을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataStore의 장점인 flow를 사용하기 때문에 내부 과정에서 에러가 발생할 경우 catch를 통해서 다양한 에러에 대응할 수 있고, 에러 메시지를 Error에 담아서 반환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 와서 생각해 보면 null 일 경우 error를 던져서 catch 블록에서 잡는 코드가 더 깔끔할 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Splash ViewModel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplashViewModel은 accessToken에 대한 결과를 Collect 합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun getUserPreferences() {
    viewModelScope.launch {
        dataStoreRepository.getAccessToken().collectLatest { loginResponse -&amp;gt;
            when (loginResponse) {
                is ApiResponse.Success -&amp;gt; _event.emit(LoginEvent.Success)
                is ApiResponse.Error -&amp;gt; _event.emit(
                    LoginEvent.Error(
                        errorCode = loginResponse.errorCode,
                        errorMessage = loginResponse.errorMessage,
                    ),
                )

                is ApiResponse.Failure -&amp;gt; {
                    _event.emit(LoginEvent.Error())
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액세스 토큰이 있다면 성공에 대한 이벤트를, 실패했다면 실패를 반환하고, 이에 대한 이벤트 처리를 View에서 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Splash Activity&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;private fun collectUserPreferences() {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.event.collectLatest { loginEvent -&amp;gt;
                when (loginEvent) {
                    is LoginEvent.Success -&amp;gt; {
                        startActivity(Intent(this@SplashActivity, MainActivity::class.java))
                    }

                    is LoginEvent.Error -&amp;gt; {
                        startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 성공할 경우 MainActivity로 이동하고, 실패했다면 Login을 진행하기 위해 LoginActivity로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보니 LoginResponse를 Splash에서 활용하고 있는데, 옳지 못한 구조인 것 같네요,, ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 리팩토링 때 분리를 해서 진행해야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginResponse의 값이 변경될 예정이기 때문에 분리는 필수적인 것 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt를 적용하기 전 DataStore를 이용해 자동로그인을 구현해 봤는데요, 매번 viewModel의 dataStoreRepository와 dataStore를 주입해줘야 하는 번거로움이 있어서 불편한 것 같습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글은 Hilt를 적용한 후 유저의 정보를 DB에 저장하는 글을 작성해 볼 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler&quot;&gt;https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1714405381339&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Coroutine exceptions handling | Kotlin&quot; data-og-description=&quot; &quot; data-og-host=&quot;kotlinlang.org&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler&quot; data-og-url=&quot;https://kotlinlang.org/docs/exception-handling.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b9mB4r/hyVZqf3vQZ/4Z1AbJKEOhVksQvzd0YPEK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b9mB4r/hyVZqf3vQZ/4Z1AbJKEOhVksQvzd0YPEK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Coroutine exceptions handling | Kotlin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/215&quot;&gt;https://readystory.tistory.com/215&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1714405393060&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] Why DataStore? (부제 : Good-bye SharedPreferences)&quot; data-og-description=&quot;개요 안드로이드 개발을 하다보면, 간단한 데이터에 대해 로컬에 저장하고 사용하고자 하는 니즈를 자주 마주하게 됩니다. 그럴 때마다 그동안에는 SharedPreferences 라는 라이브러리를 사용해왔는&quot; data-og-host=&quot;readystory.tistory.com&quot; data-og-source-url=&quot;https://readystory.tistory.com/215&quot; data-og-url=&quot;https://readystory.tistory.com/215&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yHNbb/hyVZfMnlDt/NdJ4qcYVKpCBEFwaExfO81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/62Qyy/hyVVB4Ce1K/R6rd6W3W3pQUzvNkPLQ2ck/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bQi4sr/hyVZl6S40N/PcZhbfeSSHLxbtYO44mM40/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022&quot;&gt;&lt;a href=&quot;https://readystory.tistory.com/215&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://readystory.tistory.com/215&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yHNbb/hyVZfMnlDt/NdJ4qcYVKpCBEFwaExfO81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/62Qyy/hyVVB4Ce1K/R6rd6W3W3pQUzvNkPLQ2ck/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bQi4sr/hyVZl6S40N/PcZhbfeSSHLxbtYO44mM40/img.jpg?width=819&amp;amp;height=1022&amp;amp;face=0_0_819_1022');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] Why DataStore? (부제 : Good-bye SharedPreferences)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 안드로이드 개발을 하다보면, 간단한 데이터에 대해 로컬에 저장하고 사용하고자 하는 니즈를 자주 마주하게 됩니다. 그럴 때마다 그동안에는 SharedPreferences 라는 라이브러리를 사용해왔는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;readystory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Datastore</category>
      <category>Kotlin</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/483</guid>
      <comments>https://jja2han.tistory.com/483#entry483comment</comments>
      <pubDate>Tue, 30 Apr 2024 00:43:38 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 네이버 간편 로그인(Kotlin)</title>
      <link>https://jja2han.tistory.com/482</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdKgQE/btsGZX25Lku/JErguHkYK8zqx3PrmxoHB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdKgQE/btsGZX25Lku/JErguHkYK8zqx3PrmxoHB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdKgQE/btsGZX25Lku/JErguHkYK8zqx3PrmxoHB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdKgQE%2FbtsGZX25Lku%2FJErguHkYK8zqx3PrmxoHB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 네이버 지도 API를 사용하기 때문에 로그인도 네이버 간편 로그인을 사용하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 설정했던 과정과 작성했던 코드와 그에 대한 설명을 적어볼까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 등록하는 글은 다른 블로그에도 많더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Gradle 설정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8wTDh/btsGZ06DZun/3qaNHjd4tT46BzMKPWmiK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8wTDh/btsGZ06DZun/3qaNHjd4tT46BzMKPWmiK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8wTDh/btsGZ06DZun/3qaNHjd4tT46BzMKPWmiK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8wTDh%2FbtsGZ06DZun%2F3qaNHjd4tT46BzMKPWmiK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;162&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 arr 파일을 다운로드하고 gradle에 추가하는 방법을 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드한 파일을 project 탭으로 전환시켜서 lib 폴더에 복사해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z0p5F/btsG0dkvNdd/caI8iCZLeFyrXtK86g3ekK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z0p5F/btsG0dkvNdd/caI8iCZLeFyrXtK86g3ekK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z0p5F/btsG0dkvNdd/caI8iCZLeFyrXtK86g3ekK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ0p5F%2FbtsG0dkvNdd%2FcaI8iCZLeFyrXtK86g3ekK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;556&quot; height=&quot;126&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 arr 파일을 dependency에 추가해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714396807756&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies{
	implementation(files(&quot;libs/oauth-5.9.1.aar&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 사용하는 라이브러리를 추가해주셔야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 안 쓸 거 같은 라이브러리를 제외하다가, gradle 충돌이 많이 발생해서 제공했던 라이브러리를 겹치는 라이브러리 빼고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 추가해 줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714396908517&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;com.airbnb.android:lottie:3.1.0&quot;)
implementation(&quot;org.jetbrains.kotlin:kotlin-stdlib:1.6.21&quot;)
implementation(&quot;org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9&quot;)
implementation(&quot;androidx.appcompat:appcompat:1.3.1&quot;)
implementation(&quot;androidx.legacy:legacy-support-core-utils:1.0.0&quot;)
implementation(&quot;androidx.browser:browser:1.4.0&quot;)
implementation(&quot;androidx.constraintlayout:constraintlayout:1.1.3&quot;)
implementation(&quot;androidx.security:security-crypto:1.1.0-alpha06&quot;)
implementation(&quot;androidx.core:core-ktx:1.3.0&quot;)
implementation(&quot;androidx.fragment:fragment-ktx:1.3.6&quot;)
implementation(&quot;androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0&quot;)
implementation(&quot;com.squareup.retrofit2:retrofit:2.9.0&quot;)
implementation(&quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;)
implementation(&quot;com.squareup.moshi:moshi-kotlin:1.11.0&quot;)
implementation(&quot;com.squareup.okhttp3:logging-interceptor:4.2.1&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 gradle 설정은 모두 끝났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 UI 상에서 버튼을 Naver Login에 맞게 설정해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714396981377&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;com.navercorp.nid.oauth.view.NidOAuthLoginButton
    android:id=&quot;@+id/btn_naver_login&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;50dp&quot;
    app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
    app:layout_constraintEnd_toEndOf=&quot;parent&quot;
    app:layout_constraintStart_toStartOf=&quot;parent&quot;
    app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 사진을 넣을 필요 없이 네이버에서 권장하는 버튼의 모양으로 사용가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vAwbc/btsG0vrQsog/Ippdk6YRkufZH6FUVaWyu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vAwbc/btsG0vrQsog/Ippdk6YRkufZH6FUVaWyu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vAwbc/btsG0vrQsog/Ippdk6YRkufZH6FUVaWyu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvAwbc%2FbtsG0vrQsog%2FIppdk6YRkufZH6FUVaWyu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;114&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NaverIdLoginSdk 초기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 애플리케이션에서 초기화해 줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714397088312&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MimoApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        Timber.plant(Timber.DebugTree())
        NaverIdLoginSDK.initialize(
            this,
            BuildConfig.CLIENT_ID,
            BuildConfig.CLIENT_SECRET,
            &quot;사용자&quot;,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 로그인 sdk를 초기화하는데 client_id, client_secret, 사용자 이름이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 정보는 네이버 앱을 등록하는 과정에서 정보를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저는 해당 정보가 git에 올라가는 것을 방지하기 위해 local.properites에 저장했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714397220544&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NAVER_CLIENT_ID=&quot;CleintID~~~~&quot;
NAVER_CLIENT_SECRET=&quot;ClientSecret~~~&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 로컬 프로퍼티에 저장한 정보를 사용하기 위해선 app 모듈의 gradle에서 추가적인 작업이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 다음과 같은 작업이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;buildConfig 설정&lt;/h2&gt;
&lt;pre id=&quot;code_1714397267479&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildFeatures {  
    buildConfig = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714397300769&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val properties = Properties()
properties.load(project.rootProject.file(&quot;local.properties&quot;).inputStream())
val clientId = properties[&quot;NAVER_CLIENT_ID&quot;] ?: &quot;&quot;
val clientSecret = properties[&quot;NAVER_CLIENT_SECRET&quot;] ?: &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 앱 모듈 gradle에서 안드로이드 아래에 작성해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 프로퍼티의 파일을 들고 와서 선언한 변수에 매핑시켜 주는 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이렇게 하면 중요한 정보를 숨기면서 사용할 수 있습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매핑한 정보를 buildConfig에 넣어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714397403205&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;defaultConfig {
    applicationId = &quot;com.mimo.android&quot;
    minSdk = 26
    targetSdk = 34
    versionCode = 1
    versionName = &quot;1.0&quot;
    buildConfigField(&quot;String&quot;, &quot;CLIENT_ID&quot;, &quot;$clientId&quot;)
    buildConfigField(&quot;String&quot;, &quot;CLIENT_SECRET&quot;, &quot;$clientSecret&quot;)
    testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 그래 들을 동기화하면 buildConfig 파일에 다음과 같은 정보가 추가됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qNwSg/btsG3m8koW1/i4xC8FXQt4uwYRMk7f6qE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qNwSg/btsG3m8koW1/i4xC8FXQt4uwYRMk7f6qE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qNwSg/btsG3m8koW1/i4xC8FXQt4uwYRMk7f6qE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqNwSg%2FbtsG3m8koW1%2Fi4xC8FXQt4uwYRMk7f6qE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;418&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 로그인을 실행하기 위해서 다음 코드를 실행시켜줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714397620243&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NaverIdLoginSDK.authenticate()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 파라미터로는 context와 launcer or callback을 사용하는데 저는 callback을 사용했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백은 3가지의 함수를 오버라이딩 해줘야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;성공했을 경우&lt;/li&gt;
&lt;li&gt;실패했을 경우&lt;/li&gt;
&lt;li&gt;에러가 발생한 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자가 개발한 시나리오에 맞게 성공 처리 혹은 에러 처리를 해주시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처음 네이버 로그인을 구현하기 위한 콜백과 그에 대한 이벤트 처리를 Activity와 ViewModel에서 작성하고 보니 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;두 클래스의 볼륨이 커진다는 느낌이 들었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 설계한 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmtCwR/btsGZLhydb1/6HqKruPHu821ShwpoBhWF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmtCwR/btsGZLhydb1/6HqKruPHu821ShwpoBhWF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmtCwR/btsGZLhydb1/6HqKruPHu821ShwpoBhWF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmtCwR%2FbtsGZLhydb1%2F6HqKruPHu821ShwpoBhWF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;410&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;777&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Activity와 viewModel 사이에 LoginManager를 통해서 ViewModel은 로그인에 대한 결과를 반환받는 구조입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 hilt가 적용 전이라 구조에 대해서 많기 고민해 봤는데, 우선적으로 Object로 구현하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(생각해 보니 클래스로 구현하는 것이 메모리상으로 훨씬 좋을 것 같습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Activity에서 LoginManager login 함수 호출&lt;/h2&gt;
&lt;pre id=&quot;code_1714398267866&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;with(binding) {
    btnNaverLogin.setOnClickListener {
        NaverLoginManager.login(this@LoginActivity)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 로그인을 진행하기 위해서 context를 파라미터로 받기 때문에 context를 파라미터로 전달했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginManger를 설명하기 전 API 통신에 대한 응답을 효과적으로 처리하기 위해 &lt;b&gt;ApiResponse&lt;/b&gt;를 커스텀해서 구현해 봤습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ApiResponse&lt;/h4&gt;
&lt;pre id=&quot;code_1714398464333&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class ApiResponse&amp;lt;out T : Any?&amp;gt; {
    data class Success&amp;lt;out T : Any?&amp;gt;(
        val data: T,
    ) : ApiResponse&amp;lt;T&amp;gt;()

    data class Error(
        val errorCode: Int = 0,
        val errorMessage: String = &quot;&quot;,
    ) : ApiResponse&amp;lt;Nothing&amp;gt;()

    data object Failure : ApiResponse&amp;lt;Nothing&amp;gt;()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 통신은 크게 성공(200), 실패, 에러로 나눴고, 그에 따른 객체를 제네릭 타입으로 구현해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 클라이언트에서 필요한 정보를 ApiResponse로 감싸면, 일관적인 API 통신 처리를 할 수 있을 것 같아서 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 대한 응답으론 저는 accessToken과 refreshToken을 이용하기 때문에 다음과 같이 LoginResponse를 구현해 봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714398589949&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class LoginResponse(
    val accessToken: String = &quot;&quot;,
    val refreshToken: String = &quot;&quot;,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NaverLoginManager&lt;/h2&gt;
&lt;pre id=&quot;code_1714398328377&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object NaverLoginManager {

    private val _loginResult = MutableStateFlow&amp;lt;ApiResponse&amp;lt;LoginResponse&amp;gt;&amp;gt;(ApiResponse.Failure)
    val loginResult: StateFlow&amp;lt;ApiResponse&amp;lt;LoginResponse&amp;gt;&amp;gt; = _loginResult

    private val oauthLoginCallback = object : OAuthLoginCallback {
        override fun onSuccess() {
            _loginResult.value = ApiResponse.Success(
                LoginResponse(
                    accessToken = NaverIdLoginSDK.getAccessToken() ?: &quot;&quot;,
                    refreshToken = NaverIdLoginSDK.getRefreshToken() ?: &quot;&quot;,
                ),
            )
        }

        override fun onFailure(httpStatus: Int, message: String) {
            _loginResult.value = ApiResponse.Error(
                errorCode = httpStatus,
                errorMessage = message,
            )
        }

        override fun onError(errorCode: Int, message: String) {
            onFailure(errorCode, message)
        }
    }

    fun login(context: Context) {
        NaverIdLoginSDK.authenticate(context, oauthLoginCallback)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loginResult는 viewModel에서 collect 하기 위해서 &lt;b&gt;LoginResponse를 ApiResponse가 감싸는 형태인 stateFlow&lt;/b&gt;로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stateFlow는 초기값 설정이 필요하기 때문에 Failure로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Activity에서 로그인이 호출되고, 익숙한 네이버 로그인 화면이 나오고 로그인에 대한 결과 처리를 callback에서 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공할 경우는 액세스토큰과 리프래시 토큰을 담고 있는 LoginResponse를 감싼 Success로 값을 설정하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패할 경우 errorCode와 message를 보유한 Error로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 loginResult를 viewModel에서 collect 하고, 그에 따른 viewEvent를 view에 전달해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginEvent&lt;/p&gt;
&lt;pre id=&quot;code_1714398895159&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed interface LoginEvent {
    data object Success : LoginEvent
    data class Error(
        val errorCode: Int = 0,
        val errorMessage: String = &quot;&quot;,
    ) : LoginEvent
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginEvent는 성공과 실패가 있고, 실패할 경우 사용자에게 전달하기 위해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;errorCode와 errorMessage를&lt;span&gt; 가진 Error가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;지금 와서 보니 viewEvent도 제네릭타입으로 설정해서 재사용할 수 있다면 훨씬 좋았을 것 같습니다..&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;LoginViewModel&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1714399041823&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val _event = MutableSharedFlow&amp;lt;LoginEvent&amp;gt;()
val event: SharedFlow&amp;lt;LoginEvent&amp;gt; = _event
private fun observerLoginResponse() {
    viewModelScope.launch {
        NaverLoginManager.loginResult.collectLatest { loginResponse -&amp;gt;
            when (loginResponse) {
                is ApiResponse.Success -&amp;gt; {
                    _event.emit(LoginEvent.Success)
                }

                is ApiResponse.Error -&amp;gt; {
                    _event.emit(
                        LoginEvent.Error(
                            errorCode = loginResponse.errorCode,
                            errorMessage = loginResponse.errorMessage,
                        ),
                    )
                }

                is ApiResponse.Failure -&amp;gt; {}
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel은 LoginManager의 loginResult 값을 collect 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 collect 될 경우 그에 따른 분기 처리를 해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패할 경우 viewEvent로 Error를 emit, 성공할 경우는 Success를 emit 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LoginActiviy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Activity에서는 전달받은 event에 따라서 UI를 업데이트합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714399139357&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun collectLoginEvent() {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            loginViewModel.event.collectLatest { loginEvent -&amp;gt;
                when (loginEvent) {
                    is LoginEvent.Success -&amp;gt; {
                        val intent = Intent(this@LoginActivity, MainActivity::class.java)
                        startActivity(intent)
                    }

                    is LoginEvent.Error -&amp;gt; {
                        showMessage(loginEvent.errorMessage)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flow는 생명주기를 관리해줘야 하는 점에서 repeatOnLifeCycle을 추가해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 성공할 경우 MainActivity로 이동하고, 실패할 경우 toastMessage를 출력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음글은 DataStore를 이용한 자동로그인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대한 각자의 책임에 맞게 분리를 해보려고 했는데 네이버 로그인을 처음 사용해 봐서 조금 어려웠던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 글이 Activity에서 처리하는 내용이 많아서 다른 방식으로 처리해 봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.naver.com/docs/login/android/android.md#android&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.naver.com/docs/login/android/android.md#android&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1714396545891&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Android - LOGIN&quot; data-og-description=&quot;Android Android용 네아로SDK는 서드파티 애플리케이션에서 네이버 로그인이 제공하는 로그인, 로그아웃, 토큰 관리 등의 기능을 쉽게 구현할 수 있게 합니다. Github을 참고해 주세요. 문의사항이 있&quot; data-og-host=&quot;developers.naver.com&quot; data-og-source-url=&quot;https://developers.naver.com/docs/login/android/android.md#android&quot; data-og-url=&quot;https://developers.naver.com/docs/login/android/android.md#android&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developers.naver.com/docs/login/android/android.md#android&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.naver.com/docs/login/android/android.md#android&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android - LOGIN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android Android용 네아로SDK는 서드파티 애플리케이션에서 네이버 로그인이 제공하는 로그인, 로그아웃, 토큰 관리 등의 기능을 쉽게 구현할 수 있게 합니다. Github을 참고해 주세요. 문의사항이 있&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppeper.github.io/kotlin/kotlin-generic/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ppeper.github.io/kotlin/kotlin-generic/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1714399473220&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Kotlin 제네릭의 in, out 키워드?&quot; data-og-description=&quot;안드로이드와 코틀린을 공부 하면서 out, in 키워드를 많이 봤지만 의미를 정확히 알지 못하였던 개념에 대해서 차근차근 알아가보려고 한다  &quot; data-og-host=&quot;ppeper.github.io&quot; data-og-source-url=&quot;https://ppeper.github.io/kotlin/kotlin-generic/&quot; data-og-url=&quot;https://ppeper.github.io/kotlin/kotlin-generic/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dMPBVX/hyVVCJecYX/honEMbykLvFlKawfKMXwak/img.png?width=1271&amp;amp;height=712&amp;amp;face=0_0_1271_712,https://scrap.kakaocdn.net/dn/g2ZYO/hyVVEtvvh5/BJc6lzHen8by7uNKRq6Kp0/img.png?width=954&amp;amp;height=532&amp;amp;face=0_0_954_532,https://scrap.kakaocdn.net/dn/bY47mh/hyVVDVECFS/6mDz4WKNhLRAN64LyfDgCk/img.png?width=538&amp;amp;height=295&amp;amp;face=0_0_538_295&quot;&gt;&lt;a href=&quot;https://ppeper.github.io/kotlin/kotlin-generic/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ppeper.github.io/kotlin/kotlin-generic/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dMPBVX/hyVVCJecYX/honEMbykLvFlKawfKMXwak/img.png?width=1271&amp;amp;height=712&amp;amp;face=0_0_1271_712,https://scrap.kakaocdn.net/dn/g2ZYO/hyVVEtvvh5/BJc6lzHen8by7uNKRq6Kp0/img.png?width=954&amp;amp;height=532&amp;amp;face=0_0_954_532,https://scrap.kakaocdn.net/dn/bY47mh/hyVVDVECFS/6mDz4WKNhLRAN64LyfDgCk/img.png?width=538&amp;amp;height=295&amp;amp;face=0_0_538_295');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin 제네릭의 in, out 키워드?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드와 코틀린을 공부 하면서 out, in 키워드를 많이 봤지만 의미를 정확히 알지 못하였던 개념에 대해서 차근차근 알아가보려고 한다  &lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ppeper.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>Kotlin</category>
      <category>네이버 간편 로그인</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/482</guid>
      <comments>https://jja2han.tistory.com/482#entry482comment</comments>
      <pubDate>Mon, 29 Apr 2024 23:11:10 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1285] - 동전 뒤집기(Kotlin)[골드1]</title>
      <link>https://jja2han.tistory.com/481</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciDzBj/btsGWttrYnf/OSPTYuWnzOGFxb5643UOxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciDzBj/btsGWttrYnf/OSPTYuWnzOGFxb5643UOxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciDzBj/btsGWttrYnf/OSPTYuWnzOGFxb5643UOxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciDzBj%2FbtsGWttrYnf%2FOSPTYuWnzOGFxb5643UOxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1285&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1285&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1714057346198&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1285번: 동전 뒤집기&quot; data-og-description=&quot;첫째 줄에 20이하의 자연수 N이 주어진다. 둘째 줄부터 N줄에 걸쳐 N개씩 동전들의 초기 상태가 주어진다. 각 줄에는 한 행에 놓인 N개의 동전의 상태가 왼쪽부터 차례대로 주어지는데, 앞면이 위&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1285&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1285&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/00ooe/hyVVHWV6BD/RLEJhIkYnFLBeZ4sgac0O0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/ddteGn/hyVVzEADlq/pfs9zXw1mKbQ53arJp6JUk/img.png?width=464&amp;amp;height=466&amp;amp;face=0_0_464_466&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1285&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/00ooe/hyVVHWV6BD/RLEJhIkYnFLBeZ4sgac0O0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/ddteGn/hyVVzEADlq/pfs9zXw1mKbQ53arJp6JUk/img.png?width=464&amp;amp;height=466&amp;amp;face=0_0_464_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1285번: 동전 뒤집기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 20이하의 자연수 N이 주어진다. 둘째 줄부터 N줄에 걸쳐 N개씩 동전들의 초기 상태가 주어진다. 각 줄에는 한 행에 놓인 N개의 동전의 상태가 왼쪽부터 차례대로 주어지는데, 앞면이 위&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동전에 대해 행과 열을 뒤집는 순서가 중요한 문제라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 선택할 수 있는 경우의 수는 뒤집느냐, 뒤집지 않느냐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 n의 최대 크기가 20이기에 최대 행에 대해 2^20, 열에 대해 2^20이 발생하기 때문에 최적화가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업의 순서가 중요한 점과 브루트포스로 n을 돌릴 경우 시간초과가 당연한 사실에서 저번에 풀었던 외판원 문제가 떠올랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외판원 문제도 도시를 방문하느냐, 안 하느냐에 대한 2가지 경우의 수가 존재하고, 해당 문제도 동전을 선택하느냐, 하지 않느냐에 대한 2가지 경우의 수가 존재하기 때문에 비슷하게 풀면 될 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 그렇다면 비트마스킹을 어떻게 이용할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통상적으로 방문에 대한 정보를 따로 배열을 만들어서 기록하는것이 메모리적으로 손해이기 때문에 비트마스킹을 통해서 방문 정보를 저장하는 풀이가 보통적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 나는 행을 뒤집는 정보를 비트마스킹으로 저장하고, 열을 뒤집는 정보를 비트마스킹으로 저장했다.&lt;/p&gt;
&lt;pre id=&quot;code_1714057977065&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for(i in 0 until n){
	val next = 1 shl i
    if(rowVisited and next==0){
    	//동전을 뒤집다는 정보를 저장하고 재귀 호출
        function(rowVisited or next , colVisited)
    }
    function(rowVisited, colVisited) //행 동전 뒤집지 않기
    if(colVisited and next == 0){
    	function(rowVisited,colVisited or next)
    }
    function(rowVisited,colVisited)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 재귀 호출이 무려 4번이 일어났다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트마스킹을 통해서 최적화를 한 목적은 방문 정보를 저장하기 위함이지, 연산의 수를 줄여주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 연산의 수를 줄여야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 동전을 뒤집어서 앞면을 최소로 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 풀이에 연산이 많은 것은 행과 열을 동시에 신경썼기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 행을 뒤집기 -&amp;gt;&amp;nbsp; 2번행 뒤집기 -&amp;gt; 1번 열을 뒤집기 Vs 1번 행 뒤집기 -&amp;gt; 1번 열 뒤집기 -&amp;gt; 2번 행 뒤집기를 비교해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 순서마다의 동전의 앞면 수는 다르지만, 최종적으로 같은 모양을 그리는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 각 동전에 대해 뒤집는 수가 같기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 행에 대해서 모든 경우의 수로 뒤집어 보고, 열에 대해서 검사해 보는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열에 대해서 검사를 할 때는 열의 앞면 동전의 수를 기준으로 절반보다 많다면 뒤집는 것이 결과에 유리할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 우리는 최소의 동전을 앞면으로 만들어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 행을 뒤집는 정보를 먼저 계산하고, 열을 뒤집는 경우에 대해서 검사해 보면 문제는 해결된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1714058538204&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.math.*

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private lateinit var graph: Array&amp;lt;BooleanArray&amp;gt;
    private var frontCoin = Int.MAX_VALUE
    fun run() {
        input()
        solve()
        print(frontCoin)
    }

    private fun input() {
        n = br.readLine().toInt()
        graph = Array(n) { BooleanArray(n) }
        for (i in 0 until n) {
            val line = br.readLine()
            for (j in 0 until n) {
                if (line[j] == 'H') {
                    graph[i][j] = false
                } else {
                    graph[i][j] = true
                }
            }
        }
    }

    private fun solve() {
        flipCoin(0, 0)
    }

    private fun flipCoin(now: Int, rowVisited: Int) {
        if (now == n) {
            frontCoin = min(frontCoin, checkFrontCoin(rowVisited))
            return
        }
        val next = 1 shl now
        flipCoin(now + 1, rowVisited or next)
        flipCoin(now + 1, rowVisited)
    }

    private fun checkFrontCoin(rowVisited: Int): Int {
        var sum = 0
        for (j in 0 until n) {
            var front = 0
            for (k in 0 until n) {
                val next = 1 shl k
                if (rowVisited and next != 0) { //뒤집어야 행인 경우
                    if (!graph[k][j]) {
                        front += 1
                    }
                    continue
                }
                if (graph[k][j]) {
                    front += 1
                }
            }
            sum += min(front, n - front)
        }
        return sum
    }
}

fun main() {
    Solution().run()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/481</guid>
      <comments>https://jja2han.tistory.com/481#entry481comment</comments>
      <pubDate>Fri, 26 Apr 2024 00:23:58 +0900</pubDate>
    </item>
    <item>
      <title>[백준 13397] - 구간 나누기2(Kotlin)[골드4]</title>
      <link>https://jja2han.tistory.com/480</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beZUYO/btsGTGNg9oW/OOvGjgzxvGzkGMZcGA4QD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beZUYO/btsGTGNg9oW/OOvGjgzxvGzkGMZcGA4QD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beZUYO/btsGTGNg9oW/OOvGjgzxvGzkGMZcGA4QD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeZUYO%2FbtsGTGNg9oW%2FOOvGjgzxvGzkGMZcGA4QD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13397&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13397&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713944070741&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;13397번: 구간 나누기 2&quot; data-og-description=&quot;첫째 줄에 배열의 크기 N과 M이 주어진다. (1 &amp;le; N &amp;le; 5,000, 1 &amp;le; M &amp;le; N) 둘째 줄에 배열에 들어있는 수가 순서대로 주어진다. 배열에 들어있는 수는 1보다 크거나 같고, 10,000보다 작거나 같은 자연수&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/13397&quot; data-og-url=&quot;https://www.acmicpc.net/problem/13397&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GpYyQ/hyVVApKtAv/kolCykZPvJeFA6keayc5Qk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13397&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/13397&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GpYyQ/hyVVApKtAv/kolCykZPvJeFA6keayc5Qk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;13397번: 구간 나누기 2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 배열의 크기 N과 M이 주어진다. (1 &amp;le; N &amp;le; 5,000, 1 &amp;le; M &amp;le; N) 둘째 줄에 배열에 들어있는 수가 순서대로 주어진다. 배열에 들어있는 수는 1보다 크거나 같고, 10,000보다 작거나 같은 자연수&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 요구하는 바를 이해하기 정말 어려웠던 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구간의 점수의 최댓값의 최솟값이라니,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력을 살펴보면 배열의 크기가 1부터 5000이고, 구간의 개수가 M개 이하이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에 대해서&amp;nbsp; M개 이하의 구간으로 분리하고, 각 구간에서 구간의 점수를 구하고, 구간의 점수중 최댓값이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서 최솟값을 구해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로만 적어봐도 경우의 수가 굉장히 많은 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5000에서 구간의 개수를 정하는 것 + 정확한 구간을 정하는 것 + 구간의 점수 구하기 등등만 해도 일반적인 문제가 아닌 최적화 문제라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디피와 그리디보단 이분탐색에 가까워 보였고, 탐색의 시작점과 끝점, 결정조건에 대해서 고민해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나무 자르기, 랜선 자르기 등 구하고자 하는 길이에 대해서 조건을 만족하는지를 결정하는 문제가 대부분의 이분탐색 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제도 구하려고 하는 답인 구간의 점수의 최댓값의 최솟값에 대해서 특정 값(mid)이 만족하는지 결정하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 구간을 어떻게 나눠야 하는지 감이 안 잡혀서 답을 보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 간과했던 사실은 [7]이라는 구간이 존재한다면 구간의 점수는 0이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 정보를 바탕으로 구간을 어떻게 나누는지 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 정한 이분탐색의 결정문제는 mid값보다 큰 구간의 점수를 가지는 구간이 있느냐, 없느냐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 케이스로 살펴보자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 5 4 6 2 1 3 7의 배열이 있고, 구간의 최대 개수는 3이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색은 다음과 같이 진행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초기 left와 right의 값은 0과 배열의 최댓값인 7이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 left와 right 값의 의미는 구간이 만들 수 있는 점수의 최솟값과 최댓값을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;left와 right 합의 절반인 mid를 구하고 mid에 대해서 mid를 최대로 구간을 나눌 수 있는지를 검사한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 구간을 나눌 수 있다면 더 작은 mid값에 대해서 검사를 하기 위해 right = mid -1 오른쪽의 범위를 좁힌다.&lt;/li&gt;
&lt;li&gt;구간을 나눌 수 없다면 더 큰 mid 값에 대해서 검사를 하기 위해&amp;nbsp; left = mid +1, 왼쪽의 범위를 넓힌다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 구간을 나눌 수 없다는 뜻은 mid가 최대값인 구간을 만드는데 필요한 구간의 수가 M개 초과인 경우이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;경계지점까지 이분탐색을 진행한 후(left와 right의 범위가 역전되었을 때, 그때의 경계포인트에서 반환할 지점을 정해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정문제는 다음과 같이 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 탐색하면서 최댓값과 최소값을 계속해서 업데이트하고, 최대값과 최솟값의 차이가 mid 값보다 커진다면 구간의 개수를 추가해주고, 최대값과 최솟값을 다시 갱신한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H56bG/btsGV9URI5g/Uhq8PdwvePL0XTiB1X9W31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H56bG/btsGV9URI5g/Uhq8PdwvePL0XTiB1X9W31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H56bG/btsGV9URI5g/Uhq8PdwvePL0XTiB1X9W31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH56bG%2FbtsGV9URI5g%2FUhq8PdwvePL0XTiB1X9W31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;317&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색과 예제를 통해서 과정을 함께 해보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;1073&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ID38N/btsGSUetAh7/pgS492llI0oHSTLjM5DtA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ID38N/btsGSUetAh7/pgS492llI0oHSTLjM5DtA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ID38N/btsGSUetAh7/pgS492llI0oHSTLjM5DtA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FID38N%2FbtsGSUetAh7%2FpgS492llI0oHSTLjM5DtA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;364&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;1073&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 left와 right가 엇갈리는 지점에서 반환해야 할 값은 left이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇다 할 방법이 있다기보단 테스트케이스에 대해서 내가 만든 이분탐색을 진행하고 테스트케이스의 답을 통해서 left와 right를 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713972048915&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.math.*

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var m = 0
    private lateinit var graph: IntArray
    fun run() {
        input()
        print(solve())
    }

    private fun input() {
        br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
            n = this[0]
            m = this[1]
        }
        graph = br.readLine().split(&quot; &quot;).map { it.toInt() }.toIntArray()
    }

    private fun solve(): Int {
        var left = 0 // 구간의 점수 최대가 0이 될 수 있음.
        var right = graph.max()

        while (left &amp;lt;= right) {
            val mid = (left + right) / 2
            if (countGroup(mid)) { //mid가 최대인 구간을 만들 수 있니?
                right = mid - 1
            } else {
                left = mid + 1
            }
        }
        return left
    }

    private fun countGroup(maxSum: Int): Boolean {
        var minGroup = Int.MAX_VALUE
        var maxGroup = Int.MIN_VALUE
        var groupCnt = 1
        graph.forEach { num -&amp;gt;
            minGroup = min(minGroup, num)
            maxGroup = max(maxGroup, num)

            if (maxGroup - minGroup &amp;gt; maxSum) { //최대와 최소의 차이가 maxSum보다 클 경우 구간을 자름.
                groupCnt++
                minGroup = num
                maxGroup = num
            }
        }
        return groupCnt &amp;lt;= m
    }
}

fun main() {
    Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/480</guid>
      <comments>https://jja2han.tistory.com/480#entry480comment</comments>
      <pubDate>Thu, 25 Apr 2024 00:21:35 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - BaseActivity, BaseFragment .. BaseX에 대한 고찰</title>
      <link>https://jja2han.tistory.com/479</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Android 개발을 시작한 지 얼마 지나지 않았을 때 BaseActivity, BaseFragment를 생성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Activity와 Fragment가 이를 상속받는 코드의 형태를 봤었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 당시는 더 복잡하고, 보는 사람으로 하여금 혼동을 주는 코드라고 생각했지만, BaseActivity와 Fragment를 상속받음으로써 해결되는 다양한 장점들이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppCompatActivity, Fragment가 아닌 BaseActivity, BaseFragment를 상속받는 것은&lt;b&gt; 중복된 코드의 발생을 줄일 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 얼마나 코드의 중복이 발생하길래 복잡한 구조(BaseActivity와 BaseFragment)를 택하는걸까라고 생각하실 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713711072875&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TestActivity : AppCompatActivity {
    private lateinit var binding : ActivityTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_Test)
        binding.lifecycleOwner = this
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1713711407272&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TestFragment : Fragment {

    private var _binding: FragmentTestBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        _binding =
            DataBindingUtil.inflate(layoutInflater, R.layout.fragment_test, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예로, Activity와 Fragment에서 필연적으로 ViewBinding 혹은 DataBinding을 사용할 경우&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;binding 객체를 inflate하고, 해제하는 작업이 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실상 코드 양으로 보면 그렇게 길지 않은 작업이라고 생각할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Android의 특성상 여러개의 뷰를 구현하고, 매번 Activity와 Fragment에서 binding객체를 inflate 하고 해제하는 작업은 생각보다 귀찮고, 부담스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 반복적인 코드의 발생을 줄이기 위해서 BaseActivity와 BaseFragment를 Activity와 Fragment가 상속받는 구조를 많이 사용하고 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;BaseActivity&lt;/h2&gt;
&lt;pre id=&quot;code_1713711637260&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class BaseActivity&amp;lt;T : ViewDataBinding&amp;gt;(private val layoutResId: Int) :
    AppCompatActivity() {

    private lateinit var binding: T

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, layoutResId)
        binding.lifecycleOwner = this
        initView()
    }

    abstract fun initView()

    fun showMessage(message: String) {
        Snackbar.make(this.binding.root, message, Snackbar.LENGTH_LONG).show()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 제네릭 타입을 이용해서 ViewDataBinding에 대응하는 binding 객체를 inflate하고, lifecycleOwner를 할당해 줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 외에도 Activity에서 사용할 ToastMessage를 미리 구현해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713711948614&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : BaseActivity&amp;lt;ActivityMainBinding&amp;gt;(R.layout.activity_main) {
    override fun initView() {
        //UI 관련 초기화 작업
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 액티비티는 구현한 BaseActivity를 상속받으면서, binding 객체에 대한 코드를 줄일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;initView를 통해서 onCreate에서 해야 할 작업들을 명시해 주면 코드가 굉장히 깔끔해짐을 알 수 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;BaseFragment&lt;/h2&gt;
&lt;pre id=&quot;code_1713712029794&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class BaseFragment&amp;lt;T : ViewDataBinding&amp;gt;(private val layoutResId: Int) : Fragment() {
    private var _binding: T? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        _binding = DataBindingUtil.inflate(inflater, layoutResId, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        return binding.root
    }

    override fun onViewCreated(
        view: View,
        savedInstanceState: Bundle?,
    ) {
        super.onViewCreated(view, savedInstanceState)
        initView()
    }

    abstract fun initView()

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    fun setupBackStack() {
        findNavController().popBackStack()
    }

    fun showMessage(message: String) {
        Snackbar.make(this.requireView(), message, Snackbar.LENGTH_LONG).show()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment에서도 binding 객체를 할당하고, 해제하는 작업을 정의하고, 그 외에 뒤로 가기, 토스트메시지등을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 상속받는 Fragment의 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713712156041&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainFragment : BaseFragment&amp;lt;FragmentMainBinding&amp;gt;(R.layout.fragment_main) {

    override fun initView() {
		//UI 관련 초기화 작업
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 Fragment 코드 양이 줄어듬을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 프로젝트의 구현에서는 BaseViewModel 구현을 통해서 Network의 Connection 상태를 감지하는 코드를 작성한 적도 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BaseViewModel&lt;/h2&gt;
&lt;pre id=&quot;code_1713712390716&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@HiltViewModel
open class BaseViewModel
@Inject
constructor(
    private val logoutEventRepository: LogoutEventRepository,
    private val networkManager: NetworkManager,
) : ViewModel() {

    private val _isConnected = MutableStateFlow(false)
    val isConnected: StateFlow&amp;lt;Boolean&amp;gt; = _isConnected

    init {
        networkManager.registerNetworkCallback()
        observerNetworkConnection()
    }

    private fun observerNetworkConnection() {
        viewModelScope.launch {
            networkManager.isConnected.collectLatest { isConnected -&amp;gt;
                _isConnected.update { isConnected }
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        networkManager.unRegisterNetworkCallback()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 BaseActivity, BaseFragment, BaseViewModel를 통해서 다음과 같은 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;액티비티, 프래그먼트, 뷰모델에서 공통적으로 사용하는 코드의 양을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;특정 생명주기에 대응해서 공통적으로 사용하는 코드의 양을 줄일 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 BaseActivity, BaseFragment, BaseViewModel의 사용은 독이 될 때가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이런 문제점은 상속 관계를 구축할 때 많이 겪는 문제점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 액티비티, 프래그먼트, 뷰모델에서 사용되는 함수들을 BaseX에 추가할 때마다, 크기가 방대해진다는 점입니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점점 BaseX의 기능이 많아짐에 따라, 당연하게도 변화에 취약한 환경을 가질 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이를 위해 우리는 클래스를 분리하는 작업을 선호합니다)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. BaseX의 기능을 모두 사용하지 않는 특정 컴포넌트가 생길 수 있습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모든 액비비티, 프래그먼트, 뷰모델은 각자의 BaseX를 상속받기 때문에 어떻게 보면 매끄럽지 못한 상속관계라고 할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. BaseX를 구현한 사람 외에는 알기 어렵습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 경험담으로, BaseActivity를 사용하는 과정에서 logIn, logOut을 위한 토큰의 검사를 하는 코드가 있었고, 해당 코드를 위해 다른 팀원께서 BaseActivity의 코드를 추가했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 다른 액티비티의 구현을 위해서 BaseActivity를 상속했고, 추가된 오버라이딩 함수에 의해 뇌정지가 왔었고, 물어봤었습니다..ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 BaseX의 코드가 변경됨에 따라 당사자가 아니면 흐름을 알기 매우 어렵다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 편리하고, 코드의 양을 줄이는 BaseX의 사용이지만, 상속관계를 잘못 이용함으로써 꽤 많은 단점이 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점을 해결하기 위한 대안은 특정 액티비티나 프래그먼트에서 사용되는 함수들은 확장함수를 통해서 해결할 수 도 있고요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상속의 장점이자, 단점이라고 생각하고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;BaseX를 사용할 때, 최대한 범용적인 함수의 사용 목적으로 사용하고, 최대한 BaseX의 크기를 줄이는 것이 &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;올바른 BaseX의 사용이 아닐까 생각합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/479</guid>
      <comments>https://jja2han.tistory.com/479#entry479comment</comments>
      <pubDate>Mon, 22 Apr 2024 00:32:49 +0900</pubDate>
    </item>
    <item>
      <title>[백준 16236] - 아기 상어(Kotlin)[골드3]</title>
      <link>https://jja2han.tistory.com/478</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNZ4Zj/btsGOHYWU6z/JdqvpfZOEpYLjJh0Y4Es7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNZ4Zj/btsGOHYWU6z/JdqvpfZOEpYLjJh0Y4Es7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNZ4Zj/btsGOHYWU6z/JdqvpfZOEpYLjJh0Y4Es7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNZ4Zj%2FbtsGOHYWU6z%2FJdqvpfZOEpYLjJh0Y4Es7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/16236&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/16236&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713707424258&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;16236번: 아기 상어&quot; data-og-description=&quot;N&amp;times;N 크기의 공간에 물고기 M마리와 아기 상어 1마리가 있다. 공간은 1&amp;times;1 크기의 정사각형 칸으로 나누어져 있다. 한 칸에는 물고기가 최대 1마리 존재한다. 아기 상어와 물고기는 모두 크기를 가&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/16236&quot; data-og-url=&quot;https://www.acmicpc.net/problem/16236&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bgg898/hyVS6b3SHu/lIr56UKaBYECMXEaSAfC6k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/16236&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/16236&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bgg898/hyVS6b3SHu/lIr56UKaBYECMXEaSAfC6k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;16236번: 아기 상어&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;N&amp;times;N 크기의 공간에 물고기 M마리와 아기 상어 1마리가 있다. 공간은 1&amp;times;1 크기의 정사각형 칸으로 나누어져 있다. 한 칸에는 물고기가 최대 1마리 존재한다. 아기 상어와 물고기는 모두 크기를 가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 이해하는데 시간을 굉장히 많이 쓴 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상어가 이동하는 조건과, 상어가 물고기를 먹는 조건, 그리고 상어를 이동시키는 방법 모두 중요했던 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 내가 풀이를 위해 구현한 자료구조는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1713707549328&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Shark(val x: Int, val y: Int, val size: Int = 2, var eat: Int = 0, var time: Int = 0)
data class Fish(val x: Int, val y: Int, val distance: Int = 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Shark는 아기상어를 뜻하며, 좌표와 크기, 먹은 물고기 수, 이동한 시간을 파라미터로 가진다.&lt;/li&gt;
&lt;li&gt;Fish는 물고리를 뜻하며, 좌표와 거리를 파라미터로 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 두 데이터 클래스를 이용해서 좌표를 탐색하고, 물고기를 먹고, 상어를 이동시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상어의 이동을 결정하는 조건은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;더 이상 먹을 수 있는 물고기가 없다면 중지&lt;/li&gt;
&lt;li&gt;먹을 수 있는 물고기가 1마리라면 해당 칸으로 이동&lt;/li&gt;
&lt;li&gt;먹을 수 있는 물고기가 여러마리라면, 거리가 가장 가까운 물고기를 먹는다.&lt;/li&gt;
&lt;li&gt;거리가 같은 물고기가 여러개라면, 가장 위, 왼쪽의 물고기를 먹는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 헷갈린 포인트는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상어의 이동방향은 상하좌우인데, 거리가 다른 물고기가 있을까였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 처음에 이해한 문제는 상어를 이동시키면서, 현재 방향에서 먹을 수 있는 물고기를 찾는 문제라고 이해했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 상어의 현재 위치에서 이동할 수 있는 모든 곳을 탐색하면서 먹을 수 있는 물고기를 찾는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 나는 물고기의 대한 정보를 인접리스트로 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Fish[1][1]은 크기가 1인 물고기중 1번 인덱스에 해당되는 물고기의 정보이다.&lt;/p&gt;
&lt;pre id=&quot;code_1713707924066&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (i in 1..6) {
    if (i &amp;gt;= now.size) {
        break
    }
    for (j in 0..&amp;lt;fishes[i].size) {
        eatFish.add(fishes[i][j])
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 현재 상어의 정보를 바탕으로 먹을 수 있는 물고기의 수를 계산했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현한 이유는 상어가 움직이면서, 먹을 수 있는 물고기를 판단하는것은 불필요하다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 과정을 통해서 현재 상어가 먹을 수 있는 물고기가 있는지, 없는지를 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 먹을 수 있는 상어가 없다면 그때가 종료 포인트인것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 방법의 문제점은 먹을 수 있는 물고기를 판단만하지, 거리를 계산하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 먹을 수 있는 물고기에 대해서 BFS를 통해 거리를 계산했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713708055645&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val realFish = PriorityQueue&amp;lt;Fish&amp;gt;(compareBy({ it.distance }, { it.x }, { it.y }))
    while (eatFish.size != 0) {
        val fish = eatFish.removeFirst()
        val eatTime = canEating(now, fish)
        if (eatTime != Int.MAX_VALUE) {
            realFish.add(fish.copy(distance = eatTime))
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 상어가 먹어야할 물고기의 우선순위는 거리가 가깝고, 위, 왼쪽에 있기 때문에 우선순위큐를 다음과 같이 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fish&lt;/b&gt;에 대해서 현재 상어의 정보(위치, 크기)를 바탕으로 먹을 수 있는지를 판단했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713708165931&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun canEating(shark: Shark, fish: Fish): Int {
    val q = arrayListOf&amp;lt;Shark&amp;gt;()
    q.add(shark)
    var result = Int.MAX_VALUE
    val visit = Array(n) { BooleanArray(n) }
    while (!q.isEmpty()) {
        val now = q.removeFirst()
        if (now.x == fish.x &amp;amp;&amp;amp; now.y == fish.y) {
            result = Math.min(result, now.time)
        }
        if (visit[now.x][now.y]) {
            continue
        }
        visit[now.x][now.y] = true
        for (d in 0..&amp;lt;4) {
            val mx = now.x + dx[d]
            val my = now.y + dy[d]
            if (checkRange(mx, my).not() || graph[mx][my] &amp;gt; now.size || visit[mx][my]) {
                continue
            }
            q.add(now.copy(x = mx, y = my, time = now.time + 1))
        }
    }
    return result
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canEating은 기본적인 BFS이고, 반환값으로 물고기까지의 거리를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 realFish가 현재 상어가 먹을 수 있는 물고기의 후보군들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 먹을 수 있는 상어가 없다면 종료포인트이고, 먹을 수 있는 상어가 있다면 우선순위큐의 Top에 해당하는 물고기일것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1713708242253&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val fish = realFish.poll()
fishes[graph[fish.x][fish.y]].remove(fish.copy(distance = 0))
graph[fish.x][fish.y] = 0
now.eat += 1
t += fish.distance
if (now.eat == now.size) {
    q.add(now.copy(x = fish.x, y = fish.y, eat = 0, size = now.size + 1))
} else {
    q.add(now.copy(x = fish.x, y = fish.y, eat = now.eat, time = 0))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fish를 먹었다면 fishes의 배열에서 물고기를 지워주고, 전체 그래프에서 물고기의 위치를 초기화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 상어가 물고기를 크기만큼 먹었다면 상어의 크기를 증가시켰고, 아니라면 먹은 숫자를 그대로 q에 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eghbbu/btsGMG1EoWj/GKwaoljL6rTbqoavTPc1Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eghbbu/btsGMG1EoWj/GKwaoljL6rTbqoavTPc1Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eghbbu/btsGMG1EoWj/GKwaoljL6rTbqoavTPc1Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feghbbu%2FbtsGMG1EoWj%2FGKwaoljL6rTbqoavTPc1Hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1168&quot; height=&quot;80&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 결과는 2152ms였고, 생각보다 너무 느렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 먹을 수 있는 물고기의 후보군들에 대해서 BFS를 매번 했기 때문이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 줄일수있기 위해서는 한번의 탐색으로 먹을 수 있는 물고기의 후보군을 계산하고, 가장 유망한 물고기를 결정시켜줘야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 canEating의 변화를 통해 해결할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1713708426024&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun canEating(shark: Shark): Fish? {
    val q = arrayListOf&amp;lt;Shark&amp;gt;()
    q.add(shark.copy(time = 0))
    val fish = PriorityQueue&amp;lt;Fish&amp;gt;(compareBy({ it.distance }, { it.x }, { it.y }))
    val visit = Array(n) { BooleanArray(n) }
    visit[shark.x][shark.y] = true
    while (!q.isEmpty()) {
        val now = q.removeFirst()
        for (d in 0..&amp;lt;4) {
            val mx = now.x + dx[d]
            val my = now.y + dy[d]
            if (checkRange(mx, my).not() || graph[mx][my] &amp;gt; now.size || visit[mx][my]) {
                continue
            }
            if (graph[mx][my] != 0 &amp;amp;&amp;amp; graph[mx][my] &amp;lt; now.size) { //0이 아니고 사이즈가 작을때 먹을 수 있음
                fish.add(Fish(x = mx, y = my, distance = now.time + 1))
            }
            visit[mx][my] = true
            q.add(now.copy(x = mx, y = my, time = now.time + 1))
        }
    }
    if (fish.size == 0) {
        return null
    } else {
        val resultFish = fish.poll()
        graph[resultFish.x][resultFish.y] = 0
        return resultFish
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번의 BFS로 현재 상어가 갈 수 있는 모든곳을 탐색했고, 먹을 수 있는 물고기를 fish에 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fish는 우선순위큐로 거리&amp;gt;X좌표&amp;gt;Y좌표 순으로 우선순위를 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먹을 수 있는 물고기는 fish에 저장하고, 최종적인 fish의 결과에 따라 없다면 null, 있다면 top을 반환하고, 해당 위치를 0으로 초기화 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713708540081&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val result = canEating(now)
if (result != null) {
    now.eat += 1
    if (now.eat == now.size) {
        q.add(
            now.copy(
                x = result.x,
                y = result.y,
                size = now.size + 1,
                eat = 0,
                time = now.time + result.distance
            )
        )
    } else {
        q.add(
            now.copy(
                x = result.x,
                y = result.y,
                eat = now.eat,
                time = now.time + result.distance
            )
        )
    }
} else {
    return now.time
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 result값이 null이라면 종료포인트이고, 아니라면 물고기를 먹었다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물고기를 먹었을 때, 먹은 수가 현재 상어의 크기와 일치하다면, 상어의 크기를 증가시켜주고, 아니라면 그대로 상어의 위치를 변환하고, 시간을 증가시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM7Fpx/btsGQlukROP/0wtr0b0gAi7a76Q5O1k00K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM7Fpx/btsGQlukROP/0wtr0b0gAi7a76Q5O1k00K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM7Fpx/btsGQlukROP/0wtr0b0gAi7a76Q5O1k00K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM7Fpx%2FbtsGQlukROP%2F0wtr0b0gAi7a76Q5O1k00K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;966&quot; height=&quot;82&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 코드로 시간을 많이 단축시켰다 ㅎㅎ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞힌 사람들의 코드를 보니 시간이 굉장히 빨랐다.ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 BFS의 로직을 최적화 하신것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713708733820&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.PriorityQueue

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private lateinit var graph: Array&amp;lt;IntArray&amp;gt;
    private lateinit var start: Shark
    private val dx = arrayOf(0, -1, 1, 0)
    private val dy = arrayOf(-1, 0, 0, 1)

    data class Shark(val x: Int, val y: Int, val size: Int = 2, var eat: Int = 0, var time: Int = 0)
    data class Fish(val x: Int, val y: Int, val distance: Int = 0)

    fun run() {
        input()
        print(solve())
    }

    private fun input() {
        n = br.readLine().toInt()
        graph = Array(n) { IntArray(n) }
        for (i in 0..&amp;lt;n) {
            val line = br.readLine().split(&quot; &quot;).map { it.toInt() }
            for (j in 0..&amp;lt;n) {
                graph[i][j] = line[j]
                if (graph[i][j] == 9) {
                    start = Shark(i, j)
                    graph[i][j] = 0
                }
            }
        }
    }

    private fun solve(): Int {
        val q = arrayListOf&amp;lt;Shark&amp;gt;()
        q.add(start)
        while (!q.isEmpty()) { // 아기 상어
            val now = q.removeFirst()
            val result = canEating(now)
            if (result != null) {
                now.eat += 1
                if (now.eat == now.size) {
                    q.add(
                        now.copy(
                            x = result.x,
                            y = result.y,
                            size = now.size + 1,
                            eat = 0,
                            time = now.time + result.distance
                        )
                    )
                } else {
                    q.add(
                        now.copy(
                            x = result.x,
                            y = result.y,
                            eat = now.eat,
                            time = now.time + result.distance
                        )
                    )
                }
            } else {
                return now.time
            }
        }
        return 0
    }

    private fun canEating(shark: Shark): Fish? {
        val q = arrayListOf&amp;lt;Shark&amp;gt;()
        q.add(shark.copy(time = 0))
        val fish = PriorityQueue&amp;lt;Fish&amp;gt;(compareBy({ it.distance }, { it.x }, { it.y }))
        val visit = Array(n) { BooleanArray(n) }
        visit[shark.x][shark.y] = true
        while (!q.isEmpty()) {
            val now = q.removeFirst()
            for (d in 0..&amp;lt;4) {
                val mx = now.x + dx[d]
                val my = now.y + dy[d]
                if (checkRange(mx, my).not() || graph[mx][my] &amp;gt; now.size || visit[mx][my]) {
                    continue
                }
                if (graph[mx][my] != 0 &amp;amp;&amp;amp; graph[mx][my] &amp;lt; now.size) { //0이 아니고 사이즈가 작을때 먹을 수 있음
                    fish.add(Fish(x = mx, y = my, distance = now.time + 1))
                }
                visit[mx][my] = true
                q.add(now.copy(x = mx, y = my, time = now.time + 1))
            }
        }
        if (fish.size == 0) {
            return null
        } else {
            val resultFish = fish.poll()
            graph[resultFish.x][resultFish.y] = 0
            return resultFish
        }
    }

    private fun checkRange(x: Int, y: Int) = x in 0 until n &amp;amp;&amp;amp; y in 0 until n
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/478</guid>
      <comments>https://jja2han.tistory.com/478#entry478comment</comments>
      <pubDate>Sun, 21 Apr 2024 23:13:20 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1766] - 문제집(Kotlin)[골드2]</title>
      <link>https://jja2han.tistory.com/477</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MIpzA/btsGOJPCPgC/ln0cvAbK2hS9Rhhs1exaFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MIpzA/btsGOJPCPgC/ln0cvAbK2hS9Rhhs1exaFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MIpzA/btsGOJPCPgC/ln0cvAbK2hS9Rhhs1exaFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMIpzA%2FbtsGOJPCPgC%2Fln0cvAbK2hS9Rhhs1exaFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1766&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1766&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713593185341&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1766번: 문제집&quot; data-og-description=&quot;첫째 줄에 문제의 수 N(1 &amp;le; N &amp;le; 32,000)과 먼저 푸는 것이 좋은 문제에 대한 정보의 개수 M(1&amp;nbsp;&amp;le; M &amp;le; 100,000)이 주어진다. 둘째 줄부터 M개의 줄에 걸쳐 두 정수의 순서쌍 A,B가 빈칸을 사이에 두고 주&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1766&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1766&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7cnva/hyVSXTbTSm/fMcYHbANorA8f0ga3OU0mK/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1766&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1766&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7cnva/hyVSXTbTSm/fMcYHbANorA8f0ga3OU0mK/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1766번: 문제집&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 문제의 수 N(1 &amp;le; N &amp;le; 32,000)과 먼저 푸는 것이 좋은 문제에 대한 정보의 개수 M(1&amp;nbsp;&amp;le; M &amp;le; 100,000)이 주어진다. 둘째 줄부터 M개의 줄에 걸쳐 두 정수의 순서쌍 A,B가 빈칸을 사이에 두고 주&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 조건을 고려하면서 어떻게 풀지 고민을 하면 위상정렬과 굉장히 유사하다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위상정렬에 정의는 순서가 있는 작업을 차례로 진행할 때, 순서를 결정하는 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 대놓고 문서를 풀 순서를 정하는 키워드가 있기도하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제에서 문제를 정하는 조건은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;N개의 문제는 모두 풀어야 한다.&lt;/li&gt;
&lt;li&gt;먼저 푸는 것이 좋은 문제가 있다면 반드시 먼저 풀어야 한다.&lt;/li&gt;
&lt;li&gt;가능한 쉬운 문제부터 풀어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 조건과 3번 조건이 문제에서 중요한 조건이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 조건을 통해서 우리는 문제 간의 부모 자식과 같이 관계가 존재하고, 해당 관계가 상위 문제가 풀려야 하위 문제를 풀 수 있음을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 3번 조건을 통해서 같은 우선순위의 문제라도, 반드시 숫자가 적은(난이도가 쉬운) 문제를 풀어야 함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 조건을 바탕으로 나는 우선순위큐를 사용한 위상정렬로 문제를 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 위상정렬의 기본 문제들은 stack이나 queue를 사용하는 문제가 많았지만, 해당 문제들은 답이 여러 개인 문제가 많았지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 쉬운 문제부터 풀어야 하는 조건 때문에 답이 명확하게 하나였고, 그를 우선순위큐를 통해서 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;inDegree 결정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력은 A B로 주어지는데 A가 B보다 먼저 풀어야 하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 B는 A를 풀어야 풀 수 있고, 자신보다 선행되어야 할 문제들의 개수를 inEdge로 더해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계가 아닐 정점 간의 메모리 할당은 불필요하기 때문에 graph는 인접리스트로 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713593658959&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;repeat(m) {
    br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
        graph[this[0]].add(this[1])
        inEdge[this[1]]++
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저 풀어야 할 문제 정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압도적으로 먼저 풀어야 할 문제는 자신보다 먼저 풀어야 할 문제가 없는 문제들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들은 inEdge의 값이 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 inEdge 값이 0인 정점들을 큐에 넣어줘야 했고, 우선순위큐를 통해서 난이도 별로 정렬할 수 있게 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713593821943&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val q: PriorityQueue&amp;lt;Int&amp;gt; = PriorityQueue()
for (i in 1..n) {
    if (inEdge[i] == 0) {
        q.add(i)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 순서 정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue에 들어있는 원소들은 순서가 정해져 있는 문제들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;now에 대해서 now와 연결된 문제들을 순회하면서 다음을 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;now를 풀었기 때문에 now와 연결된 정점들의 inEdge를 감소시켜 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inEdge가 0인 정점들은 이제 풀 수 있는 문제들이기에 q에 넣어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1713593929159&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (!q.isEmpty()) { //q에 들어간 원소들은 inEdge가 0인 놈들
    val now = q.poll()
    sb.append(now).append(&quot; &quot;)
    for (i in 0 until graph[now].size) {
        val connectVertex = graph[now][i]
        inEdge[connectVertex]--
        if (inEdge[connectVertex] == 0) { //이제 연결된 간선이 없을 경우
            q.add(connectVertex)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713594127851&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.lang.StringBuilder
import java.util.PriorityQueue


class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var m = 0
    private lateinit var graph: Array&amp;lt;ArrayList&amp;lt;Int&amp;gt;&amp;gt;
    private lateinit var inEdge: IntArray
    private val sb = StringBuilder()

    fun run() {
        input()
        solve()
        println(sb.toString())
    }

    private fun input() {
        br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
            n = this[0]
            m = this[1]
        }
        graph = Array(n + 1) { arrayListOf() }
        inEdge = IntArray(n + 1)
        repeat(m) {
            br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
                graph[this[0]].add(this[1])
                inEdge[this[1]]++
            }
        }
    }

    private fun solve() {
        val q: PriorityQueue&amp;lt;Int&amp;gt; = PriorityQueue()
        for (i in 1..n) {
            if (inEdge[i] == 0) {
                q.add(i)
            }
        }

        while (!q.isEmpty()) { //q에 들어간 원소들은 inEdge가 0인 놈들
            val now = q.poll()
            sb.append(now).append(&quot; &quot;)
            for (i in 0 until graph[now].size) {
                val connectVertex = graph[now][i]
                inEdge[connectVertex]--
                if (inEdge[connectVertex] == 0) { //이제 연결된 간선이 없을 경우
                    q.add(connectVertex)
                }
            }
        }
    }
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/477</guid>
      <comments>https://jja2han.tistory.com/477#entry477comment</comments>
      <pubDate>Sat, 20 Apr 2024 15:23:17 +0900</pubDate>
    </item>
    <item>
      <title>[백준 2352] - 반도체 설계(Kotlin)[골드2]</title>
      <link>https://jja2han.tistory.com/476</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH8fKr/btsGB1EWUdc/2bOsY03EC5UmiaMvLYYxTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH8fKr/btsGB1EWUdc/2bOsY03EC5UmiaMvLYYxTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH8fKr/btsGB1EWUdc/2bOsY03EC5UmiaMvLYYxTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH8fKr%2FbtsGB1EWUdc%2F2bOsY03EC5UmiaMvLYYxTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2352&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2352&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713185906702&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2352번: 반도체 설계&quot; data-og-description=&quot;첫째 줄에 정수 n(1 &amp;le; n &amp;le; 40,000)이 주어진다. 다음 줄에는 차례로 1번 포트와 연결되어야 하는 포트 번호, 2번 포트와 연결되어야 하는 포트 번호, &amp;hellip;, n번 포트와 연결되어야 하는 포트 번호가 주&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2352&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2352&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fKJZd/hyVPWgcgXL/FCk5KzBDvHCHPNsPhXv5p0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2352&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2352&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fKJZd/hyVPWgcgXL/FCk5KzBDvHCHPNsPhXv5p0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2352번: 반도체 설계&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 정수 n(1 &amp;le; n &amp;le; 40,000)이 주어진다. 다음 줄에는 차례로 1번 포트와 연결되어야 하는 포트 번호, 2번 포트와 연결되어야 하는 포트 번호, &amp;hellip;, n번 포트와 연결되어야 하는 포트 번호가 주&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백준 문제를 자주 풀면 접해봤을 유형의 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제의 유형은 LIS로 최장 증가 부분 수열의 길이를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 반도체의 선이 꼬이지 않게 가장 길게 연결하기 위해선 뒤에 등장하는 번호가 앞에 등장했던 번호들보다 커지면 안 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 항상 앞에 등장했던 번호들보다 뒤에 등장하는 번호들이 작아짐이 보장되는 가장 긴 구간을 구해야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 조금 더 난이도가 길어진다면, 최장 부분 수열의 정확한 구간을 구하는 문제로 발전하지만, 해당 문제는 단순하게 구간의 길이를 구하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIS 문제에선 이분탐색 알고리즘을 통해 숫자가 들어갈 수 있는 위치를 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 전반적인 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;수열의 맨 뒤 원소보다 큰 원소는 그대로 구간에 붙여준다.&lt;/li&gt;
&lt;li&gt;하지만 수열의 맨 뒤 원소보다 작은 원소가 등장한다면, 해당 원소가 들어갈 수 있는 위치를 찾아줘야 한다.&lt;br /&gt;(여기서 이분탐색을 통해서 위치를 찾아준다)&lt;/li&gt;
&lt;li&gt;n까지 반복하고, 수열의 크기가 가장 길게 증가하는 구간의 길이가 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v4yoR/btsGDI5weJf/o5LwkKvPJStpTNvThBiiN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v4yoR/btsGDI5weJf/o5LwkKvPJStpTNvThBiiN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v4yoR/btsGDI5weJf/o5LwkKvPJStpTNvThBiiN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv4yoR%2FbtsGDI5weJf%2Fo5LwkKvPJStpTNvThBiiN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;382&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수열[mid]의 값과 내가 넣고자 하는 값(number)을 비교하면서, 범위 조절은 다음과 같은 규칙을 따른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수열[mid] &amp;gt;&amp;nbsp; number : right를 mid-1로 조정해서, 탐색하려는 구간을 감소시킨다.&lt;/li&gt;
&lt;li&gt;수열[mid] &amp;lt; number : left를 mid+1로 조정해서, 탐색하려는 구간을 증가시킨다.&lt;/li&gt;
&lt;li&gt;수열[mid] == number : 인 경우는 존재하지 않는다. (같은 원소가 없는 것이 보장되기 때문이다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 left를 반환해야 할지, right를 반환해야 할지 고민이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 통해서 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPa2H/btsGFCiXmtg/jZF2y7ez983w5Ks6fscEo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPa2H/btsGFCiXmtg/jZF2y7ez983w5Ks6fscEo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPa2H/btsGFCiXmtg/jZF2y7ez983w5Ks6fscEo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPa2H%2FbtsGFCiXmtg%2FjZF2y7ez983w5Ks6fscEo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;566&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색이 끝나고 최종적인 R과 L의 위치는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11이 들어가야 할 위치는 R이 아닌 L임을 알 수 있고, 따라서 left를 반환하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713187182802&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private lateinit var graph: IntArray
    private val result = arrayListOf&amp;lt;Int&amp;gt;()
    fun run() {
        input()
        solve()
        print(result.size)
    }

    private fun input() {
        n = br.readLine().toInt()
        graph = br.readLine().split(&quot; &quot;).map { it.toInt() }.toIntArray()
    }

    private fun solve() {
        result.add(graph.first())
        for (i in 1 until n) {
            if (result.last() &amp;lt; graph[i]) { //새로 들어오는 원소가 더 클 경우
                result.add(graph[i])
            } else { //더 작을 경우
                val index = binarySearch(graph[i])
                result[index] = graph[i]
            }
        }
    }

    private fun binarySearch(num: Int): Int { //num이 들어갈 수 있는 공간을 찾아야 함.
        var left = 0
        var right = result.size - 1
        while (left &amp;lt;= right) {
            val mid = (left + right) / 2
            if (result[mid] &amp;gt; num) { //더 클 경우
                right = mid - 1
            } else { //작거나 같을 경우
                left = mid + 1
            }
        }
        return left
    }

}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <category>이분탐색</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/476</guid>
      <comments>https://jja2han.tistory.com/476#entry476comment</comments>
      <pubDate>Mon, 15 Apr 2024 22:20:47 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1300] - k번째 수(Kotlin)[골드1]</title>
      <link>https://jja2han.tistory.com/475</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8BgLO/btsGCnUn4xm/LoZW0YdCkb3uKK9fk90Ckk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8BgLO/btsGCnUn4xm/LoZW0YdCkb3uKK9fk90Ckk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8BgLO/btsGCnUn4xm/LoZW0YdCkb3uKK9fk90Ckk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8BgLO%2FbtsGCnUn4xm%2FLoZW0YdCkb3uKK9fk90Ckk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1300&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1300&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713106771196&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1300번: K번째 수&quot; data-og-description=&quot;세준이는 크기가 N&amp;times;N인 배열 A를 만들었다. 배열에 들어있는 수&amp;nbsp;A[i][j] = i&amp;times;j 이다. 이 수를 일차원 배열 B에 넣으면 B의 크기는 N&amp;times;N이 된다. B를 오름차순 정렬했을 때, B[k]를&amp;nbsp;구해보자. 배열 A와 B&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1300&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1300&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gHTsI/hyVPJneBU0/ivpBJ9MQodQI7JvpKKSCMk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1300&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1300&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gHTsI/hyVPJneBU0/ivpBJ9MQodQI7JvpKKSCMk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1300번: K번째 수&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;세준이는 크기가 N&amp;times;N인 배열 A를 만들었다. 배열에 들어있는 수&amp;nbsp;A[i][j] = i&amp;times;j 이다. 이 수를 일차원 배열 B에 넣으면 B의 크기는 N&amp;times;N이 된다. B를 오름차순 정렬했을 때, B[k]를&amp;nbsp;구해보자. 배열 A와 B&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NXN의 배열을 일차원으로 압축시켜서 k번째 index의 수를 찾는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 NXN 배열을 만들어서 값을 i*j 값으로 넣어주기엔 N의 최댓값이 10^5승이므로 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실제 배열을 만들어주지 않고, 우리는 k번째 index의 수를 찾아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 사용할 수 있는 정보는 배열의 원소들이 i*j의 형태로 계산된다는점과 A의 행렬은 NXN라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 행렬의 자세한 정보는 알지 못하지만, 최솟값은 1이고, 최댓값은 NXN이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단은 N을 4라고 가정하고, A와 B 행렬을 그려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A행렬&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PUBEO/btsGC38XmtA/2oiQWIqmQ9QURTpwYk6KV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PUBEO/btsGC38XmtA/2oiQWIqmQ9QURTpwYk6KV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PUBEO/btsGC38XmtA/2oiQWIqmQ9QURTpwYk6KV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPUBEO%2FbtsGC38XmtA%2F2oiQWIqmQ9QURTpwYk6KV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;382&quot; height=&quot;389&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B행렬&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWWRaI/btsGBhgicYg/jPFKz4ei2txWRvLNnJV6Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWWRaI/btsGBhgicYg/jPFKz4ei2txWRvLNnJV6Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWWRaI/btsGBhgicYg/jPFKz4ei2txWRvLNnJV6Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWWRaI%2FbtsGBhgicYg%2FjPFKz4ei2txWRvLNnJV6Lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;93&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 B행렬에서 k번째 index의 수를 찾아야 하고, 나는 이분탐색을 이용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 B 행렬을 그려보니 이분탐색으로 찾을 수 있을 것이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색을 구현하기 범위가 무엇을 의미하는지, 어떻게 조절할 것인지가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이분탐색에서 사용하는 start와 end 변수를 다음과 같이 설정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start : A 행렬의 최소 숫자인 1&lt;/li&gt;
&lt;li&gt;end : A 행렬의 최대 숫자인 n*n&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 n이 4이고, k가 11일 경우 결괏값은 8이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqlYoA/btsGEhsaaF0/IcUKIRMyyI0QljKbshxIc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqlYoA/btsGEhsaaF0/IcUKIRMyyI0QljKbshxIc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqlYoA/btsGEhsaaF0/IcUKIRMyyI0QljKbshxIc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqlYoA%2FbtsGEhsaaF0%2FIcUKIRMyyI0QljKbshxIc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;116&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 B [k]이 X라면 X보다 작은 숫자의 개수는 k-1개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 정보를 이용해서 이분탐색의 구현을 다음과 같이 설정했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;mid는 start와 end 합의 절반값이며, 인덱스가 아닌 숫자의 값을 의미한다.&lt;/li&gt;
&lt;li&gt;mid 값을 통해서 mid 값보다 작거나 같은 숫자의 개수를 센다. (해당 값을 cnt라고 하자)&lt;/li&gt;
&lt;li&gt;cnt가 만약 k보다 작다면, mid값이 B [k]의 값보다 앞에 있다는 것을 의미한다.&lt;br /&gt;따라서 left의 값을 mid+1로 조정한다.&lt;/li&gt;
&lt;li&gt;cnt가 만약 k보다 같거나 크다면, mid 값이 B[k]의 값 혹은 그 뒤에 있다는 것을 의미한다.&lt;br /&gt;따라서 right의 값을 mid-1로 조정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 mid보다 같거나 작은 숫자의 개수를 어떻게 행렬의 구현 없이 카운팅 할 수 있는지에 대해 먼저 얘기할까 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4X4 행렬에서 8을 찾고자 할 때 다음과 같은 영역이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FOqQs/btsGB92VQ7t/TJn1tMQSxUguHQ8KCmAoyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FOqQs/btsGB92VQ7t/TJn1tMQSxUguHQ8KCmAoyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FOqQs/btsGB92VQ7t/TJn1tMQSxUguHQ8KCmAoyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFOqQs%2FbtsGB92VQ7t%2FTJn1tMQSxUguHQ8KCmAoyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;549&quot; height=&quot;544&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행에 해당하는 열의 원소들은 i*j 값이라는 단순한 규칙을 따른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 각 행에 대응해 찾고자 하는 원소 x의 위치를 구하고, 1부터 n까지 적용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8의 원소인 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 행에 대해서 작거나 같은 수의 개수가 최대 8개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 행에 대해서 작거나 같은 수의 개수가 최대 4개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 행에 대해서 작거나 같은 수의 개수가 최대 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번 행에 대해서 작거나 같은 수의 개수가 최대 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 행 I에 대해서 찾고자 하는 원소 X의 위치는 X / I 임을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 A 행렬은 NXN이기 때문에 위치가 N을 넘어간다면, 실제 더해질 수 있는 수는 N게임을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1713108872950&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun checkIndex(number: Long): Long {
    var sum = 0L
    for (i in 1..n) {
        var rem = number / i
        if (rem &amp;gt;= n) {
            rem = n
        }
        sum += rem
    }
    return sum
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이분탐색의 결과로 left를 반환할지, right를 반환해야 할지 고민이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 left와 right 값을 둘 다 반환해 보고, 테스트케이스의 정답과 일치하는 부분을 반환했다,, ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 추측하기엔, lower bound와 upper bound의 개념을 이용해야 하는 것 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lower bound : 찾고자 하는 값 이상의 값이 처음으로 등장하는 인덱스&lt;/li&gt;
&lt;li&gt;upper bound :&amp;nbsp; 찾고자 하는 값 초과의 값이 처음으로 등장하는 인덱스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k값에 대해 mid보다 작거나 같은 경우의 수가 여러 개일 가능성이 있기 때문에 찾고자 하는 값 이상의 값이 처음으로 등장하는 인덱스를 구하는 lower bound를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713109703221&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0L
    private var k = 0L
    fun run() {
        input()
        print(solve())
    }

    private fun input() {
        n = br.readLine().toLong()
        k = br.readLine().toLong()
    }

    private fun solve(): Long {
        var left = 1L
        var right = n * n
        while (left &amp;lt;= right) {
            val mid = (left + right) / 2
            if (checkIndex(mid) &amp;gt;= k) {
                right = mid - 1
            } else {
                left = mid + 1
            }
        }
        return left
    }

    private fun checkIndex(number: Long): Long {
        var sum = 0L
        for (i in 1..n) {
            var rem = number / i
            if (rem &amp;gt;= n) {
                rem = n
            }
            sum += rem
        }
        return sum
    }

}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <category>이분탐색</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/475</guid>
      <comments>https://jja2han.tistory.com/475#entry475comment</comments>
      <pubDate>Mon, 15 Apr 2024 01:00:10 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1939] - 중량제한(Kotlin)[골드3]</title>
      <link>https://jja2han.tistory.com/474</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZvKfV/btsGCbMVc3F/BTFdRUirXx61W6WmkEObqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZvKfV/btsGCbMVc3F/BTFdRUirXx61W6WmkEObqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZvKfV/btsGCbMVc3F/BTFdRUirXx61W6WmkEObqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZvKfV%2FbtsGCbMVc3F%2FBTFdRUirXx61W6WmkEObqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1939&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1939&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713020555280&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1939번: 중량제한&quot; data-og-description=&quot;첫째 줄에 N, M(1 &amp;le; M &amp;le; 100,000)이 주어진다. 다음 M개의 줄에는 다리에 대한 정보를 나타내는 세 정수 A, B(1 &amp;le; A, B &amp;le; N), C(1 &amp;le; C &amp;le; 1,000,000,000)가 주어진다. 이는 A번 섬과 B번 섬 사이에 중량제한이&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1939&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1939&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BRyLZ/hyVMUjmnwd/SgEmvwSgUG52kqqfzYt1z0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1939&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1939&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BRyLZ/hyVMUjmnwd/SgEmvwSgUG52kqqfzYt1z0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1939번: 중량제한&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 N, M(1 &amp;le; M &amp;le; 100,000)이 주어진다. 다음 M개의 줄에는 다리에 대한 정보를 나타내는 세 정수 A, B(1 &amp;le; A, B &amp;le; N), C(1 &amp;le; C &amp;le; 1,000,000,000)가 주어진다. 이는 A번 섬과 B번 섬 사이에 중량제한이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발지부터 도착지점까지 옮길 수 있는 중량의 최댓값을 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제를 풀 수 있는 방법은 여러가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프림과 크루스칼을 통해서 start -&amp;gt; end까지의 최소 비용을 계산하는 방법도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색을 통해서 중량을 설정하고, 옮길 수 있는지에 대한 여부를 통해 mid 값을 조절하는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이분탐색을 통해서 해당 문제를 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;간선 저장하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 풀이에는 n*n 배열의 간선의 가중치를 저장했지만, 메모리 초과를 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 초과를 피하기 위해서 인접행렬을 사용해 간선의 정보를 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접행렬에 원소에는 정점과 가중치가 필요로 했기에, 추가적으로 데이터 클래스를 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713021119487&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Edge(val vertex: Int, val weight: Long)
private lateinit var graph: Array&amp;lt;ArrayList&amp;lt;Edge&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;graph [i]의 Edge는 정점 i에서 Edge의 vertex까지의 가중치가 weight를 의미한다.&lt;/p&gt;
&lt;pre id=&quot;code_1713021234637&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;repeat(m) {
    br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
        graph[this[0]].add(Edge(this[1], this[2].toLong()))
        graph[this[1]].add(Edge(this[0], this[2].toLong()))
        maxWeight = max(maxWeight, this[2].toLong())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력과정에서 양방향 그래프이기 때문에 다음과 같이 넣어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이분탐색의 최댓값을 설정하기 위해 maxWeight를 계속 구해줘서 뒤 이분탐색을 실행할 때, 최적화를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이분탐색으로 거리 조절하기&lt;/h4&gt;
&lt;pre id=&quot;code_1713020772324&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun solve(): Long {
    var left = 1L
    var right = maxWeight
    while (left &amp;lt;= right) {
        val mid: Long = (left + right) / 2
        //운반할 수 있는지 없는지 찾아야 함
        if (isTransfer(mid)) { //배달가능
            left = mid + 1
        } else { // 배달 불가능
            right = mid - 1
        }
    }
    return right
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 출발지에서 도착지점까지 옮길 수 있는 중량을 찾기 위해서 나는 이분탐색을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간의 여유가 있다면 각 엣지마다의 무게를 전체 리스트로 사용해서 검사할 수 있지만, 해당 방법은 시간이 오래 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 중량을 선택하는 과정을 logN 알고리즘인 이분탐색으로 결정하는 것을 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while문이 종료된다면, 중량에 대한 탐색이 끝났다는 의미이고, 그때의 right값을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 right는 운반할 수 있는 최대의 중량을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;옮길 수 있는지 체크하기&lt;/h4&gt;
&lt;pre id=&quot;code_1713020892307&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun isTransfer(weight: Long): Boolean {
    val q = arrayListOf&amp;lt;Int&amp;gt;()
    val visit = BooleanArray(n + 1)
    visit[startFactory] = true
    q.add(startFactory)
    while (q.isNotEmpty()) {
        val now = q.removeFirst()
        for (i in 0 until graph[now].size) {
            val nowVertex = graph[now][i]
            if (nowVertex.weight &amp;lt; weight || visit[nowVertex.vertex]) { //비용보다 비싸다면 no
                continue
            }
            if(nowVertex.vertex==endFactory){
                return true
            }
            visit[nowVertex.vertex] = true
            q.add(nowVertex.vertex)
        }
    }
    return false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발지부터 도착지까지 옮길 수 있는지 체크하는 것은 BFS를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옮길 수 있는 조건은 최대 중량인 weight를 통해서 해당 간선의 가중치가 weight 이하인 정점들만 큐에 넣어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 도착할 수 있는지, 아닌지만 확인하면 되기 때문에 도착지점을 발견한다면 true를 반환하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1713021353455&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.math.max
import kotlin.math.min

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var m = 0
    private lateinit var graph: Array&amp;lt;ArrayList&amp;lt;Edge&amp;gt;&amp;gt;
    private var maxWeight = 0L
    private var minWeight = 1000000000L
    private var startFactory = 0
    private var endFactory = 0
    fun run() {
        input()
        print(solve())
    }

    private fun input() {
        br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
            n = this[0]
            m = this[1]
        }
        graph = Array(n + 1) { ArrayList&amp;lt;Edge&amp;gt;() }
        repeat(m) {
            br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
                graph[this[0]].add(Edge(this[1], this[2].toLong()))
                graph[this[1]].add(Edge(this[0], this[2].toLong()))
                maxWeight = max(maxWeight, this[2].toLong())
            }
        }
        br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
            startFactory = this[0]
            endFactory = this[1]
        }
    }

    private fun solve(): Long {
        var left = 1L
        var right = maxWeight
        while (left &amp;lt;= right) {
            val mid: Long = (left + right) / 2
            //운반할 수 있는지 없는지 찾아야 함
            if (isTransfer(mid)) { //배달가능
                left = mid + 1
            } else { // 배달 불가능
                right = mid - 1
            }
        }
        return right
    }

    private fun isTransfer(weight: Long): Boolean {
        val q = arrayListOf&amp;lt;Int&amp;gt;()
        val visit = BooleanArray(n + 1)
        visit[startFactory] = true
        q.add(startFactory)
        while (q.isNotEmpty()) {
            val now = q.removeFirst()
            for (i in 0 until graph[now].size) {
                val nowVertex = graph[now][i]
                if (nowVertex.weight &amp;lt; weight || visit[nowVertex.vertex]) { //비용보다 비싸다면 no
                    continue
                }
                if(nowVertex.vertex==endFactory){
                    return true
                }
                visit[nowVertex.vertex] = true
                q.add(nowVertex.vertex)
            }
        }
        return false
    }

    data class Edge(val vertex: Int, val weight: Long)
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/474</guid>
      <comments>https://jja2han.tistory.com/474#entry474comment</comments>
      <pubDate>Sun, 14 Apr 2024 00:17:18 +0900</pubDate>
    </item>
    <item>
      <title>[백준 1029] - 그림 교환(Kotlin)[골드1]</title>
      <link>https://jja2han.tistory.com/473</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pky0b/btsGwy2DHPL/lbIQSj6fPJ55ixytGMvzi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pky0b/btsGwy2DHPL/lbIQSj6fPJ55ixytGMvzi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pky0b/btsGwy2DHPL/lbIQSj6fPJ55ixytGMvzi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPky0b%2FbtsGwy2DHPL%2FlbIQSj6fPJ55ixytGMvzi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1029&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1029&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712751727721&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1029번: 그림 교환&quot; data-og-description=&quot;첫째 줄에 예술가의 수 N이 주어진다. N은 2보다 크거나 같고, 15보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에는 N개의 수가 주어진다. i번째 줄의 j번째&amp;nbsp;수는 j번 예술가가 i번 예술가에&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1029&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1029&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cFGW3d/hyVMOCIXnR/XsrdidvnEQa6dAZkhEFQMk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1029&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1029&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cFGW3d/hyVMOCIXnR/XsrdidvnEQa6dAZkhEFQMk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1029번: 그림 교환&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 예술가의 수 N이 주어진다. N은 2보다 크거나 같고, 15보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에는 N개의 수가 주어진다. i번째 줄의 j번째&amp;nbsp;수는 j번 예술가가 i번 예술가에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 보고 감이 안 잡혀서 알고리즘 분류를 보고 풀 수 있었던 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트마스킹이 왜 필요한가에 대해서 생각을 하다가, 저번에 풀었던 외판원 순회 문제가 떠올랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2098&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2098&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712751833501&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2098번: 외판원 순회&quot; data-og-description=&quot;첫째 줄에 도시의 수 N이 주어진다. (2 &amp;le; N &amp;le; 16) 다음 N개의 줄에는 비용 행렬이 주어진다. 각 행렬의 성분은 1,000,000 이하의 양의 정수이며, 갈 수 없는 경우는 0이 주어진다. W[i][j]는 도시 i에서 j&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2098&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2098&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/okbUW/hyVMW1Qedy/QLX7sQbyy7Do3eXB6QKUE0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2098&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2098&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/okbUW/hyVMW1Qedy/QLX7sQbyy7Do3eXB6QKUE0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2098번: 외판원 순회&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 도시의 수 N이 주어진다. (2 &amp;le; N &amp;le; 16) 다음 N개의 줄에는 비용 행렬이 주어진다. 각 행렬의 성분은 1,000,000 이하의 양의 정수이며, 갈 수 없는 경우는 0이 주어진다. W[i][j]는 도시 i에서 j&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제와 비슷한 문제인것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 n의 범위가 2부터 15까지라서 완전탐색을 해서 모든 경우의 수를 계산하려고 했지만, 현재 작품을 산 사람과, 다음에 작품을 산 사람들을 모두 고려해야 하는 순열을 구해야 했기 때문에 최대 15! 경우의 수가 나왔고, 시간초과를 피할 수 없다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 방법으로는 백트래킹도 고려를 해봤는데, 가지치기에 대한 조건이 현재 사람에 대해 더이상 그림을 살 수 있는 사람이 없을 경우가 조건일 것 같았다. 그러할 경우 더 이상의 탐색은 무의미하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제에서의 그림을 살 수 있는 조건은 크거나 같은 가격이기 때문에 모든 사람의 매입가가 같다면 가지치기에 대한 효율이 그리 높지 않을것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 탐색을 더 깊게 하지 않기 위해선 이전의 정보가 있다면 그것을 그대로 반환하는 메모이제이션이 좋다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 DP 테이블을 어떻게 세우는지가 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처음으로 고려해야할 것은 현재까지 몇 명이 그림을 샀는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내가 탐색하려는 단계에서의 그림을 산 사람이 누구인지 알아야, 중복된 거래가 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 그림을 산 사람에 대한 정보를 비트마스킹을 통해서 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 비슷하게 위에서 언급했던 문제도, TSP 문제에서 현재까지 방문했던 도시들을 비트마스킹을 통해서 방문처리를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트마스킹에 대한 처리는 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 사람이 샀다면 1&amp;lt;&amp;lt;1, 두 번째 사람이 샀다면 1&amp;lt;&amp;lt;2, n번째 사람이 샀다면 1&amp;lt;&amp;lt;n과 같이 연산을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 사람이 샀다면 해당 방문의 값은 1일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 첫 번째 사람과 두 번째 사람 모두 샀다면 방문의 값은 11 -&amp;gt; 3일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 우리는 누가 샀는지에 대한 정보를 비트마스킹을 통해서 1로 채울 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람의 수는 최대 15명이기 때문에 메모리상으로 굉장히 넉넉하다. 최대 2^15이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 방문을 처리했다면, 어떠한 사람이 그림을 샀는지에 대한 처리를 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 것처럼 첫 번째 사람이 그림을 사면 1, 두 번째 사람이 그림을 산다면 2, n번째 사람이 산다면 2^n일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보를 현재 방문정보와 and 연산을 해서 1이 아니라면 해당 사람은 그림을 사지 않았다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내 방문 정보가 3이라면 11이다. 만약 세 번째 사람이 그림을 샀는지에 대한 검사를 한다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11과 100 (2&amp;lt;&amp;lt;3) and 연산을 한다고 하면 답은 0이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 그림을 샀는지에 대한 처리를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 고려해야 할 것은 마지막으로 산 가격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 그림을 사야 하는 조건이 그림을 산 가격보다 크거나 같아야 하기 때문에 해당 정보는 반드시 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 2개의 정보만으로 DP 테이블을 구성하기에는 조금 부족했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 마지막 가격만을 가지고, 현재 그림을 산 사람에 대한 정보를 판단하기에는 무리가 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 가격으로 그림을 살 수 있는지를 판단할 수 있지만, 어떤 사람이 그림을 사야 하는지를 구해내기엔 부족하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&amp;nbsp; (현재 그림을 산 사람들), (현재 그림을 들고 있는 사람), (마지막으로 그림이 거래된 가격) 3개의 정보로 DP 테이블을 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DP 테이블의 값은 그림을 산 사람의 수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 DP 테이블을 세웠다면, 다음은 완전탐색과 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 메모이제이 션만 섞어준다면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1712753350393&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.math.max

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private lateinit var ary: Array&amp;lt;IntArray&amp;gt;
    private lateinit var dp: Array&amp;lt;Array&amp;lt;IntArray&amp;gt;&amp;gt;
    fun run() {
        input()
        print(solve(1,0,1,0))
    }

    private fun input() {
        n = br.readLine().toInt()
        ary = Array(n) { IntArray(n) }
        for (i in 0 until n) {
            val line = br.readLine()
            for (j in 0 until n) {
                ary[i][j] = line[j].toString().toInt()
            }
        }
        dp = Array(1 shl n) { Array(n) { IntArray(10) } }
    }

    private fun solve(visited: Int, curPeople: Int, depth: Int, lastPrice: Int): Int {

        if (visited == 1 shr n - 1) { //다 방문했을 경우
            return 0
        }

        if (dp[visited][curPeople][lastPrice] != 0) {
            return dp[visited][curPeople][lastPrice]
        }

        dp[visited][curPeople][lastPrice] = depth

        for (i in 0 until n) {
            val next = 1 shl i
            val nowPrice = ary[curPeople][i]
            if (visited and next != 0) { //이미 산 사람이라면
                continue
            }
            if (lastPrice &amp;gt; nowPrice) { //마지막에 산 가격보다 크다면
                continue
            }
            dp[visited][curPeople][lastPrice] = max(dp[visited][curPeople][lastPrice],solve(visited or next, i, depth + 1, nowPrice))
        }
        return dp[visited][curPeople][lastPrice]
    }
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/473</guid>
      <comments>https://jja2han.tistory.com/473#entry473comment</comments>
      <pubDate>Wed, 10 Apr 2024 21:50:52 +0900</pubDate>
    </item>
    <item>
      <title>[Android] - 네트워크 연결 관리하기(ConnectivityManager)</title>
      <link>https://jja2han.tistory.com/472</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yrsBk/btsGtjSpPLA/5qgZn3eGDrJRpW7Km6VSkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yrsBk/btsGtjSpPLA/5qgZn3eGDrJRpW7Km6VSkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yrsBk/btsGtjSpPLA/5qgZn3eGDrJRpW7Km6VSkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyrsBk%2FbtsGtjSpPLA%2F5qgZn3eGDrJRpW7Km6VSkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 Network 연결 상태에 따라 API 동작 및 화면을 갱신해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 방법이 있지만, 저는 공식문서에 나와있는 ConnectivityManager를 통해서 해결했습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1712589082619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네트워크 상태 읽기 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 네트워크 상태 읽기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android에서는 앱이 연결의 동적 변&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*1sjssa6*_up*MQ..*_ga*MzQ1NzYyOTI0LjE3MTI0OTE2ODg.*_ga_6HH9YJMN9M*MTcxMjU3OTk2Mi4zLjAuMTcxMjU3OTk2Mi4wLjAuMA..#kotlin&quot; data-og-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bJfd44/hyVMVODtxe/x4Za6xXQRGgk5hEs8WdH3k/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/B7MUt/hyVJVJvkFS/307U3RqV9EUSywqDJKLGg0/img.png?width=1209&amp;amp;height=358&amp;amp;face=0_0_1209_358,https://scrap.kakaocdn.net/dn/yvoNE/hyVMVujsDy/RAHkIKYyHZlyHkpe2Zi3C0/img.png?width=1208&amp;amp;height=334&amp;amp;face=0_0_1208_334&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*1sjssa6*_up*MQ..*_ga*MzQ1NzYyOTI0LjE3MTI0OTE2ODg.*_ga_6HH9YJMN9M*MTcxMjU3OTk2Mi4zLjAuMTcxMjU3OTk2Mi4wLjAuMA..#kotlin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*1sjssa6*_up*MQ..*_ga*MzQ1NzYyOTI0LjE3MTI0OTE2ODg.*_ga_6HH9YJMN9M*MTcxMjU3OTk2Mi4zLjAuMTcxMjU3OTk2Mi4wLjAuMA..#kotlin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bJfd44/hyVMVODtxe/x4Za6xXQRGgk5hEs8WdH3k/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/B7MUt/hyVJVJvkFS/307U3RqV9EUSywqDJKLGg0/img.png?width=1209&amp;amp;height=358&amp;amp;face=0_0_1209_358,https://scrap.kakaocdn.net/dn/yvoNE/hyVMVujsDy/RAHkIKYyHZlyHkpe2Zi3C0/img.png?width=1208&amp;amp;height=334&amp;amp;face=0_0_1208_334');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 상태 읽기 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 네트워크 상태 읽기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android에서는 앱이 연결의 동적 변&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명하기 전 네트워크 사용을 위해선 안드로이드 매니페스트에 다음 권한을 포함해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712589140111&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&amp;gt;
&amp;lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 권한은 음악, 갤러리 접근과 달리 일반권한이기 때문에 런타임에 요청할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 어플이 설치됨에 따라 자동으로 권한이 추가됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가되는 권한은 다음과 같은 일을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;android.permission.INTERNET : 애플리케이션에서 네트워크 소켓을 열기 위해 필요함&lt;/li&gt;
&lt;li&gt;android.permission.ACCESS_NETWORK_STATE : 애플리케이션에서 네트워크에 관한 정보에 접근하기 위해 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 앱의 네트워크 연결의 변경사항을 추적하기 위해 ConnectivityManager를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConnectivityManager는 앱에 시스템의 연결 상태를 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ConnectivityManager의 인스턴스를 가져오기 위해선 context가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Hilt 모듈을 이용했기 때문에 다음과 같은 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712589523925&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
@InstallIn(SingletonComponent::class)
object ConnectivityManagerModule {

    @Singleton
    @Provides
    fun provideConnectivityManager(@ApplicationContext context: Context): ConnectivityManager {
        return context.getSystemService(ConnectivityManager::class.java)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 실행되는 동안 네트워크 연결 상태를 감지하기 위해서 ApplicationContext를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 이벤트를 추적하기 위해선 NetworkCallback 클래스가 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712589992961&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network : Network) {
        Log.e(TAG, &quot;The default network is now: &quot; + network)
    }

    override fun onLost(network : Network) {
        Log.e(TAG, &quot;The application no longer has a default network. The last default network was &quot; + network)
    }

    override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
        Log.e(TAG, &quot;The default network changed capabilities: &quot; + networkCapabilities)
    }

    override fun onLinkPropertiesChanged(network : Network, linkProperties : LinkProperties) {
        Log.e(TAG, &quot;The default network changed link properties: &quot; + linkProperties)
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각이 의미하는 바는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onAvailable : 네트워크가 연결되어 이용이 가능할 때&lt;/li&gt;
&lt;li&gt;onLost : 네트워크 연결이 끊겨, 이용이 불가능할 때&lt;/li&gt;
&lt;li&gt;onCapabilitiesChanged : 네트워크 기능이 변경되었을 때&lt;/li&gt;
&lt;li&gt;onLinkPropertiesChanged : 기본 네트워크의 링크 속성이 변경되었을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용을 바탕으로 Network의 연결을 감지하는 NetworkManager 클래스를 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712590380498&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class NetworkManager @Inject constructor(private val connectivityManager: ConnectivityManager) {

    private val _isConnected = MutableStateFlow(false)
    val isConnected: StateFlow&amp;lt;Boolean&amp;gt; = _isConnected

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            _isConnected.value = true
        }

        override fun onLost(network: Network) {
            _isConnected.value = false
        }
    }

    fun registerNetworkCallback() {
        connectivityManager.registerDefaultNetworkCallback(networkCallback)
    }

    fun unRegisterNetworkCallback() {
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 네트워크의 연결을 지속적으로 관찰해야 했기에, stateFlow로 선언해서, 네트워크 연결의 변화가 생길 경우 value를 업데이트해 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unRegisterNetworkCallback 함수는 더 이상 콜백이 필요 없을 경우 호출하며, callback을 해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetworkManager를 어떤 식으로 사용해야 할지 고민을 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Activity나 Fragment가 네트워크 연결의 상태를 알아야 했고, 적절한 위치에 NetworkManager를 위치시켜서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isConnecetd의 값에 따라 UI를 변경시켜야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 viewModel의 NetworkManager를 주입해서, viewModel에서 네트워크 연결을 관찰했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712590924888&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@HiltViewModel
class testViewModel @Inject constructor(private val networkManager: NetworkManager) : ViewModel(){
	
    init{
    	networkManager.registerNetworkCallback()
    }
    
    private val _isConnected = MutableStateFlow(false)
    val isConnected: StateFlow&amp;lt;Boolean&amp;gt; = _isConnected

    private fun observerNetworkConnection() {
        viewModelScope.launch {
            networkManager.isConnected.collectLatest { isConnected -&amp;gt;
                _isConnected.update { isConnected }
            }
        }
    }
    
    override fun onCleared(){
    	super.onCleared()
        networkManager.unRegisterCallback()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel의 생명주기에 따라 callback을 등록하고, 해제하는 작업을 포함시켰습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이렇게 viewModel의 isConnected값을 통해서 Fragment나 Activity에서 UI를 변경하거나, API 접근을 제한해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 네트워크 연결이 끊겼을 때의 화면 변경을 위해 UI를 구현해야 했고, 저는 Compose를 이용한 Dialog를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서는 CircularProgressIndicator를 통해서 간단하게 로딩 화면을 구현할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712591311502&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun LoadingDialogScreen() {

    val screenWidth = LocalConfiguration.current.screenWidthDp.dp
    val screenHeight = LocalConfiguration.current.screenHeightDp.dp
    val dialogWidth = screenWidth * 0.8f
    Dialog(onDismissRequest = {}) {
        Column(
            modifier = Modifier
                .width(dialogWidth)
                .height(screenHeight),
            verticalArrangement = Arrangement.Center,
        ) {
            Row(
                modifier = Modifier
                    .align(Alignment.CenterHorizontally),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                CircularProgressIndicator(
                    modifier = Modifier.size(100.dp),
                    color = Color.LightGray,
                    strokeWidth = 10.dp,
                )
            }
            Spacer(modifier = Modifier.height(30.dp))
            Row(
                modifier = Modifier.align(Alignment.CenterHorizontally),
            ) {
                Text(
                    text = stringResource(id = R.string.disconnected_network_message),
                    style = MaterialTheme.typography.displaySmall,
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML도 유사하게 XMl을 생성하고, DialogFragment를 생성해서 navigate를 구현하면 쉽게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현한 Dialog를 Composable 함수에서 호출하기 위해선 네트워크 연결 상태를 알아야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712591399653&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val isConnected by boardListViewModel.isConnected.collectAsStateWithLifecycle()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isConnected가 변경될 때마다, Composable 함수를 다시 실행하기 위해 collectAsStateWithLifeCycle()을 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712591527127&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (isConnected.not()) {
    LoadingDialogScreen()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결이 끊겼을 경우 LoadingDialog를 호출하는 것으로 해결했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;와이파이연결검증.gif&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYYxwl/btsGraPvgl9/fO8bcKX0s7mLtG1cliVq11/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYYxwl/btsGraPvgl9/fO8bcKX0s7mLtG1cliVq11/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYYxwl/btsGraPvgl9/fO8bcKX0s7mLtG1cliVq11/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dYYxwl/btsGraPvgl9/fO8bcKX0s7mLtG1cliVq11/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;750&quot; data-filename=&quot;와이파이연결검증.gif&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결을 감지하기 위해서 broadcast receiver를 사용해야 하는 줄 알았는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 편리한 ConnectivityManager가 있는 것을 알게 된 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 특성상 네트워크의 유형별 다양한 처리보단 상태 정보만 필요했기에, 간단하게 구현했는데 공식문서를 다시 차근차근 읽어봐야 할 것 같습니다.. ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;figure id=&quot;og_1712591858358&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;연결 상태 및 연결 측정 모니터링 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 연결 상태 및 연결 측정 모니터링 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ConnectivityManager는 기&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/training/monitoring-device-state/connectivity-status-type?hl=ko&amp;amp;_gl=1*wce7bp*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot; data-og-url=&quot;https://developer.android.com/training/monitoring-device-state/connectivity-status-type?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/czqbEm/hyVMLrHwxW/2UxqYAwbQULx4XyYdfX9kK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/training/monitoring-device-state/connectivity-status-type?hl=ko&amp;amp;_gl=1*wce7bp*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/training/monitoring-device-state/connectivity-status-type?hl=ko&amp;amp;_gl=1*wce7bp*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/czqbEm/hyVMLrHwxW/2UxqYAwbQULx4XyYdfX9kK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;연결 상태 및 연결 측정 모니터링 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 연결 상태 및 연결 측정 모니터링 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ConnectivityManager는 기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1712591868137&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네트워크 상태 읽기 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 네트워크 상태 읽기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android에서는 앱이 연결의 동적 변&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*gc6utv*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot; data-og-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qJEES/hyVMOhFedc/eN4FSFYQFt4qoZtzikMFk1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/n9KQL/hyVJSeUm0r/X20U6BAESzBc09fWVeAcrk/img.png?width=1209&amp;amp;height=358&amp;amp;face=0_0_1209_358,https://scrap.kakaocdn.net/dn/isISZ/hyVJ6jWlZv/23s54aWwbdFKu3g2F6vujk/img.png?width=1208&amp;amp;height=334&amp;amp;face=0_0_1208_334&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*gc6utv*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko&amp;amp;_gl=1*gc6utv*_up*MQ..*_ga*MjEzNDM4NzE4NS4xNzEyNTg5NzUw*_ga_6HH9YJMN9M*MTcxMjU4OTc1MC4xLjAuMTcxMjU4OTc1MC4wLjAuMA..&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qJEES/hyVMOhFedc/eN4FSFYQFt4qoZtzikMFk1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/n9KQL/hyVJSeUm0r/X20U6BAESzBc09fWVeAcrk/img.png?width=1209&amp;amp;height=358&amp;amp;face=0_0_1209_358,https://scrap.kakaocdn.net/dn/isISZ/hyVJ6jWlZv/23s54aWwbdFKu3g2F6vujk/img.png?width=1208&amp;amp;height=334&amp;amp;face=0_0_1208_334');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 상태 읽기 &amp;nbsp;|&amp;nbsp; Connectivity &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 네트워크 상태 읽기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android에서는 앱이 연결의 동적 변&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1712592269970&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] Jetpack Compose + Flow로 네트워크 연결 상태 처리를 해보자.&quot; data-og-description=&quot;저는 안드로이드 관련된 Medium 포스팅을 메일로 전달받고 있습니다. 최근 제목과 같이 Jetpack Compose와 Flow를 활용하여 네트워크 연결 상태 처리를 하는 포스팅을 보고 예제를 간단하게 작성하여 &quot; data-og-host=&quot;doitddo.tistory.com&quot; data-og-source-url=&quot;https://doitddo.tistory.com/126&quot; data-og-url=&quot;https://doitddo.tistory.com/126&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hJeeZ/hyVMPgzlop/MOIwZCpt42KyK4aAfb48o0/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473,https://scrap.kakaocdn.net/dn/Fhqox/hyVMOhFfUx/fumAN0cWfENhgetsjfLDN0/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473,https://scrap.kakaocdn.net/dn/bti5wS/hyVMPARnmC/3IS2HwRsahKp3vNxsf7xZk/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473&quot;&gt;&lt;a href=&quot;https://doitddo.tistory.com/126&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://doitddo.tistory.com/126&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hJeeZ/hyVMPgzlop/MOIwZCpt42KyK4aAfb48o0/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473,https://scrap.kakaocdn.net/dn/Fhqox/hyVMOhFfUx/fumAN0cWfENhgetsjfLDN0/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473,https://scrap.kakaocdn.net/dn/bti5wS/hyVMPARnmC/3IS2HwRsahKp3vNxsf7xZk/img.png?width=320&amp;amp;height=473&amp;amp;face=0_0_320_473');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] Jetpack Compose + Flow로 네트워크 연결 상태 처리를 해보자.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;저는 안드로이드 관련된 Medium 포스팅을 메일로 전달받고 있습니다. 최근 제목과 같이 Jetpack Compose와 Flow를 활용하여 네트워크 연결 상태 처리를 하는 포스팅을 보고 예제를 간단하게 작성하여&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;doitddo.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Skils/Android</category>
      <category>Android</category>
      <category>ConnectivityManager</category>
      <category>network</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/472</guid>
      <comments>https://jja2han.tistory.com/472#entry472comment</comments>
      <pubDate>Tue, 9 Apr 2024 01:04:44 +0900</pubDate>
    </item>
    <item>
      <title>[백준 2616] - 소형기관차(Kotlin)[골드3]</title>
      <link>https://jja2han.tistory.com/471</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3Madp/btsGqOkji3P/FtiEyhh8jAC9XU2LWqJZp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3Madp/btsGqOkji3P/FtiEyhh8jAC9XU2LWqJZp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3Madp/btsGqOkji3P/FtiEyhh8jAC9XU2LWqJZp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3Madp%2FbtsGqOkji3P%2FFtiEyhh8jAC9XU2LWqJZp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2616&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2616&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712414054836&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2616번: 소형기관차&quot; data-og-description=&quot;첫째 줄에 기관차가 끌고 가던 객차의 수가 입력된다. 그 수는 50,000 이하이다. 둘째 줄에는 기관차가 끌고 가던 객차에 타고 있는 손님의 수가 1번 객차부터 차례로 입력된다. 한 객차에 타고 있&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2616&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2616&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yMper/hyVMQziqB9/WamHacliKuuddEP7MZSKyk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2616&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yMper/hyVMQziqB9/WamHacliKuuddEP7MZSKyk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2616번: 소형기관차&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 기관차가 끌고 가던 객차의 수가 입력된다. 그 수는 50,000 이하이다. 둘째 줄에는 기관차가 끌고 가던 객차에 타고 있는 손님의 수가 1번 객차부터 차례로 입력된다. 한 객차에 타고 있&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 읽고 누적합을 구해서 소형차를 배정하는 문제인 것 같았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpfSVU/btsGrU5b0z4/Dn7PMRNxdWbXcOvMTn8syK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpfSVU/btsGrU5b0z4/Dn7PMRNxdWbXcOvMTn8syK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpfSVU/btsGrU5b0z4/Dn7PMRNxdWbXcOvMTn8syK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpfSVU%2FbtsGrU5b0z4%2FDn7PMRNxdWbXcOvMTn8syK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;321&quot; data-origin-width=&quot;733&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 주어진 테스트케이스에 대해서 먼저 누적합에 대한 그림을 그려보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음으로 접근한 방법은 정해진 구간까지의 최대 누적합을 구하는 방법으로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 50까지 접근했을때의 최댓값은 40과 50을 태운 90이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 10까지 접근했을때의 최댓값은 40과 50을 태운 90 vs (35+40), (50+10)중 후자의 값이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구간별로 배정할 수 있는 최대의 값을 구했다.&lt;/p&gt;
&lt;pre id=&quot;code_1712414631605&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun getAccumulateSum() {
    for (i in 0 until k) {
        sum[k - 1] += ary[i]
    }
    for (i in k until n) {
        sum[i] = sum[i - 1] - ary[i - k] + ary[i]
    }
    dp[k - 1] = sum[k - 1]
    for (i in k until n) {
        dp[i] = max(dp[i - 1], dp[i - k] + sum[i])
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 주어진 테스트케이스에 대해선 정답이 나왔지만, 소형차가 3대라는 점과, 연속적으로 뽑지 않을 경우도 있기 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정테스트케이스에 대해서&amp;nbsp; 통과하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 소형차 3대에 대한 정보와 차를 배정했는지에 대한 여부를 판단해야 했기에, DP 배열을 2차원으로 접근하는 것이 맞다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 정의한 1차원 DP 배열은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;dp[i] = index i까지 k개의 칸을 고려했을 때의 최대 누적합&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 2차원 DP 배열은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;dp[i][j] = i번째 소형차를 j칸까지 검사했을 때의 최대 합&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7Nchz/btsGspqlxnH/a9YxKmialbOQlwKQul9xY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7Nchz/btsGspqlxnH/a9YxKmialbOQlwKQul9xY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7Nchz/btsGspqlxnH/a9YxKmialbOQlwKQul9xY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7Nchz%2FbtsGspqlxnH%2Fa9YxKmialbOQlwKQul9xY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;482&quot; height=&quot;434&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 소형차에 대해서 최댓값은 (45,60)의 구간을 담는 105가 최댓값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 두 번째 소형차의 구간은 첫번째 소형차의 정보를 이용해서 계산해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 소형차의 최댓값은 첫 번째에서 (40,50)을 담은 90과 (45,60)을 담은 105를 더한 195가 최대일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 세 번째 소형차의 구간은 첫번째, 두번째 소형차의 정보를 이용해서 계산해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번째 소형차의 최댓값은 (35,40) + (50,10) + (45,60)인 240이 최댓값일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 구할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배낭 문제와 유사하게 접근하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 60을 마지막으로 소형차를 배정할 때, 2번째 소형차에서 60을 담지 않는 구간인 30까지의 최댓값을 이용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;60을 담기 위해선 45,60을 이전 소형차가 배정받으면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 소형차의 배정칸 이전 구간을 확인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 dp 점화식은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;dp[i][j] = maxOf(dp[i-1][j], dp[i][j-1], dp[i-1][j-소형차칸] + j칸 까지의 누적합&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dp[i-1][j]와 dp[i][j-1]이 추가된 이유는 해당 칸을 배정받지 않았을 때를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 조건은 해당 칸을 배정받았을 때를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1712416069048&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var k = 0
    private lateinit var ary: IntArray
    private lateinit var sum: IntArray
    private lateinit var dp: Array&amp;lt;IntArray&amp;gt;

    fun run() {
        input()
        getAccumulateSum()
        solve()
        print(dp[3][n])
    }

    private fun input() {
        n = br.readLine().toInt()
        ary = br.readLine().split(&quot; &quot;).map { it.toInt() }.toIntArray()
        sum = IntArray(n + 1)
        dp = Array(4) { IntArray(n + 1) }
        k = br.readLine().toInt()
    }

    private fun solve() {
        for (i in 1..3) {
            for (j in k..n) {
                dp[i][j] = maxOf(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - k] + sum[j - 1])
            }
        }
    }

    private fun getAccumulateSum() {
        for (i in 0 until k) {
            sum[k - 1] += ary[i]
        }
        for (i in k until n) {
            sum[i] = sum[i - 1] - ary[i - k] + ary[i]
        }
    }
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/471</guid>
      <comments>https://jja2han.tistory.com/471#entry471comment</comments>
      <pubDate>Sun, 7 Apr 2024 00:09:14 +0900</pubDate>
    </item>
    <item>
      <title>[백준 2482] - 색상환(Kotlin)[골드3]</title>
      <link>https://jja2han.tistory.com/470</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw3dI4/btsGlRJmuCO/jjxx9NRdQxpZIHnWVdHng0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw3dI4/btsGlRJmuCO/jjxx9NRdQxpZIHnWVdHng0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw3dI4/btsGlRJmuCO/jjxx9NRdQxpZIHnWVdHng0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw3dI4%2FbtsGlRJmuCO%2Fjjxx9NRdQxpZIHnWVdHng0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2482&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2482&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712240105091&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2482번: 색상환&quot; data-og-description=&quot;첫째 줄에 N색상환에서 어떤 인접한 두 색도 동시에 선택하지 않고 K개의 색을 고를 수 있는 경우의 수를 1,000,000,003 (10억 3) 으로 나눈 나머지를 출력한다.&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2482&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2482&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bPehQW/hyVJ3fm5Pe/xphjLBHfleoxlF5PTsJ1ok/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2482&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bPehQW/hyVJ3fm5Pe/xphjLBHfleoxlF5PTsJ1ok/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2482번: 색상환&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 N색상환에서 어떤 인접한 두 색도 동시에 선택하지 않고 K개의 색을 고를 수 있는 경우의 수를 1,000,000,003 (10억 3) 으로 나눈 나머지를 출력한다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 선택한 색에 따라 다음 선택이 영향이 받는 문제였고, 배낭 문제와 굉장히 유사하게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배낭 문제와 같이 물건을 담으면서, 해당 물건을 담을 경우와 담지 않을 경우를 나눠서 계산하는 것처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제도 색깔을 선택할 경우와 선택하지 않을 경우를 나눠서 계산하는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 그림을 그려가면서 가장 먼저 발견한 조건은 k가 n의 절반보다 크다면 경우의 수는 0이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 문제에서도 주어졌지만, 문제를 읽을 당시엔 이해를 하지 못했고, 실제로 그림을 그려가면서 이해할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D5n4M/btsGnZsmZzt/5689K3fwkk6IRkkakOX1x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D5n4M/btsGnZsmZzt/5689K3fwkk6IRkkakOX1x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D5n4M/btsGnZsmZzt/5689K3fwkk6IRkkakOX1x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD5n4M%2FbtsGnZsmZzt%2F5689K3fwkk6IRkkakOX1x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;230&quot; height=&quot;205&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림과 같이 색이 4개가 있을 때, k가 2일 경우는&amp;nbsp; (1,3) (2,4)를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 k가 3개라면? 선택할 수 있는 경우의 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 n개의 색 중에서 인접한 두 색을 고르지 않는 조건을 만족하면서 k개의 색을 고르는 경우는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 적어도 k는 n의 절반보다 크거나 같아야 함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 색을 선택해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgk8vX/btsGmzg3dtG/jw094gelfs6eSZqdtkZly0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgk8vX/btsGmzg3dtG/jw094gelfs6eSZqdtkZly0/img.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;534&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.0467%; margin-right: 10px;&quot; data-widthpercent=&quot;52.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgk8vX/btsGmzg3dtG/jw094gelfs6eSZqdtkZly0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcgk8vX%2FbtsGmzg3dtG%2Fjw094gelfs6eSZqdtkZly0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nw2zD/btsGl1ZeE4R/5mMALxOkpROaxFpWubyIf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nw2zD/btsGl1ZeE4R/5mMALxOkpROaxFpWubyIf0/img.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;551&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.7905%;&quot; data-widthpercent=&quot;47.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nw2zD/btsGl1ZeE4R/5mMALxOkpROaxFpWubyIf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnw2zD%2FbtsGl1ZeE4R%2F5mMALxOkpROaxFpWubyIf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;551&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5를 선택했다면 선택할 수 있는 색깔의 범위는 3까지이다. 여기서 우리는 k-1개를 뽑아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 5(1) + 1~3(k-1)를 합쳐 k개를 만들어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 5를 선택하지 않았다면 4번까지 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리는 1~4번의 범위에서 k개를 뽑아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배낭과 유사하게 접근하는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 dp 점화식은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;dp[i][j] = dp[i-1][j] + dp[i-2][j-1]&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문의 범위에 대해서도 많은 고민을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 위에서 n이라는 색깔의 범위 내에서 인접한 두 색을 선택하지 않으려고 할 때 n/2를 뽑을 수 없다는 것을 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 그림에서도 6이라는 색을 선택할 때&amp;nbsp; 최대 3개까지만 선택할 수 있음을 알 고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 선택하려는 색(i)에 따라 j의 범위는 i/2가 최대임을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 base case인 경우는 n개의 색 중에서 1개를 고르는 경우는 n개이기 때문에 해당 케이스는 따로 계산해 줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1712242760764&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var k = 0
    private lateinit var dp: Array&amp;lt;IntArray&amp;gt;
    private val div = 1000000003

    fun run() {
        input()
        if (k &amp;gt; n / 2) {
            print(0)
            return
        }
        solve()
        print(dp[n][k])
    }

    private fun input() {
        n = br.readLine().toInt()
        k = br.readLine().toInt()
        dp = Array(n + 1) { IntArray(n + 1) }
    }

    private fun solve() {
        for (i in 0..n) {
            dp[i][1] = i
        }
        for (i in 1..n) {
            for (j in 2..i / 2) {
                dp[i][j] = (dp[i - 2][j - 1] + dp[i - 1][j]) % div
            }
        }
    }
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <category>색상환</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/470</guid>
      <comments>https://jja2han.tistory.com/470#entry470comment</comments>
      <pubDate>Fri, 5 Apr 2024 00:01:18 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] - Contract</title>
      <link>https://jja2han.tistory.com/469</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsjIJW/btsGlvFfEmI/B7cdGCWtpdlgMuNtSrzppk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsjIJW/btsGlvFfEmI/B7cdGCWtpdlgMuNtSrzppk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsjIJW/btsGlvFfEmI/B7cdGCWtpdlgMuNtSrzppk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsjIJW%2FbtsGlvFfEmI%2FB7cdGCWtpdlgMuNtSrzppk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;168&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Contract&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.3.60 버전부터 사용되었으며, 컴파일러가 이해할 수 있게 명시적으로 자신의 동작을 설명할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 이해할 수 있게 라는 말이 조금은 헷갈릴 여지가 있다고 생각한다. 코드를 통해 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 구현하려고 할 때의 시나리오를 생각해 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@JvmInline
value class Password(val password: String)

@JvmInline
value class Id(val id: String)

fun validate(id: Id?, password: Password?): Boolean {
    return id != null &amp;amp;&amp;amp; password != null
}
fun signUp(id:Id?, password: Password?){
    if(validate(id,password)){
        println(&quot;Id : ${id.id} Password: ${password.password}&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 이런 경험이 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어? 난 분명히 함수를 통해서 nullcheck를 해줬는데 왜 밑에서 스마트캐스트가 되지 않았지?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BFDsz/btsGmAMz56N/PvwP0WfKJ6HSuikj1Po3R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BFDsz/btsGmAMz56N/PvwP0WfKJ6HSuikj1Po3R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BFDsz/btsGmAMz56N/PvwP0WfKJ6HSuikj1Po3R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBFDsz%2FbtsGmAMz56N%2FPvwP0WfKJ6HSuikj1Po3R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;260&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드도 id와 password의 nullcheck를 함수를 통해서 했지만, 이후 id와 password가 스마트캐스트 되지 않아 중복으로 널 체크를 해야 하는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 코드를 작성하면서 validate의 반환값이 true면 id와 password가 null이 아니란 것을 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컴파일러는 알지 못하기 때문에 스마트캐스트가 되지 않아, 널 체크를 또 해줘야 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 간단한 해결방법으로 타입체크를 직접 하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kquna/btsGlWP6I7U/Uk63Wckw3JAPKVDe6MmX31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kquna/btsGlWP6I7U/Uk63Wckw3JAPKVDe6MmX31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kquna/btsGlWP6I7U/Uk63Wckw3JAPKVDe6MmX31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKquna%2FbtsGlWP6I7U%2FUk63Wckw3JAPKVDe6MmX31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1022&quot; height=&quot;242&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방법은 컴파일타임에서 컴파일러가 if문의 내용에 따라 id와 password가 null이 아니라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 직접적인 타입 체크는&amp;nbsp;비즈니스 로직이 변경됨에 따라, 기능이 추가됨에 따라 함수로 분리를 하는 것이 훨씬 효율적이고, 가독성이 좋은 코드이기에 이렇게 해결하는 방법은 추천하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 컴파일러에게 내가 원하는 결괏값이 전달되지 않아서 생긴 문제를 해결하기 위해 등장한 것이 contract이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@OptIn(ExperimentalContracts::class)
fun validate(id: Id?, password: Password?): Boolean {
    contract {
        returns(true) implies (id != null &amp;amp;&amp;amp; password != null)
    }
    return id != null &amp;amp;&amp;amp; password != null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contract는 아직 실험적인 기능이기 때문에 &lt;b&gt;@OptIn(ExperimentalContracts::class)&lt;/b&gt; 어노테이션을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contract 블록은 다음과 같이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id, password 모두 null이 아니라면 true가 반환될 것이라고 컴파일러에게 전달하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcOxLB/btsGmB5MCw1/CtuyaFNIbVkVwTcvoV0db0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcOxLB/btsGmB5MCw1/CtuyaFNIbVkVwTcvoV0db0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcOxLB/btsGmB5MCw1/CtuyaFNIbVkVwTcvoV0db0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcOxLB%2FbtsGmB5MCw1%2FCtuyaFNIbVkVwTcvoV0db0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1150&quot; height=&quot;272&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contract 블록을 통해 컴파일러가 validate의 반환값이 true일 경우 id와 password는 null이 아님을 알고 있기 때문에 smart cast가 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;callsInPlace&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun callsInPlaceTest(action: () -&amp;gt; Unit) {
    action()
}

fun callsInPlace() {
    val number: Int
    callsInPlaceTest {
        number = sum(5)
    }
    println(number)
}

fun sum(time: Int): Int {
    var sum = 0
    repeat(time){
        sum++
    }
    return sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 보면 전혀 어색하지 않아 보이지만, 컴파일타임에서 두 가지 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RBRtb/btsGmBLtt9r/QVsk7XVGQPjMc2vv1cPfn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RBRtb/btsGmBLtt9r/QVsk7XVGQPjMc2vv1cPfn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RBRtb/btsGmBLtt9r/QVsk7XVGQPjMc2vv1cPfn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRBRtb%2FbtsGmBLtt9r%2FQVsk7XVGQPjMc2vv1cPfn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;188&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Captured values initialization is forbidden due to possible reassignment&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number의 재할당 가능성 때문에 오류가 발생한다라는 문구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성한 우리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;callsInPlaceTest&lt;/b&gt;의 파라미터인 action이 딱 한번 실행되어서, number의 값을 할당하는 것을 알고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 컴파일러 입장에선 람다함수인 action이 몇 번 호출되는지 알 수 없다는 뜻이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기에 val로 선언된 number가 여러 번 할당될 우려가 있기 때문에 컴파일타임에서 var로 바꾸라고 권장하는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckxpwr/btsGmcSHp57/DQHRc5J2rWLJMs4w2G4kB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckxpwr/btsGmcSHp57/DQHRc5J2rWLJMs4w2G4kB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckxpwr/btsGmcSHp57/DQHRc5J2rWLJMs4w2G4kB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckxpwr%2FbtsGmcSHp57%2FDQHRc5J2rWLJMs4w2G4kB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;204&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Variable 'number' must be initialized&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number가 초기화되지 않았다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 오류 역시 람다식의 action이 몇 번 호출되는지 알 수 없기 때문에 number의 할당 여부를 알지 못해서 생기는 오류이다. &lt;br /&gt;따라서 초기화하라고 권장하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 컴파일타임에서 람다 &lt;b&gt;함수의 호출 횟수를 명시적으로 컴파일러에게 알려주기 위해&lt;/b&gt; 사용하는 것이 &lt;b&gt;callsInPlace&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다식의 호출 횟수를 정하는 유형은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UNKNOWN - 컴파일러의 기본 값&lt;/li&gt;
&lt;li&gt;AT_LEAST_ONCE : 최소 1번 이상 실행&lt;/li&gt;
&lt;li&gt;AT_MOST_ONCE : 최대 1번 실행 (0 or 1)&lt;/li&gt;
&lt;li&gt;EXACTLY_ONCE : 정확히 1번 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 코드를 통해 컴파일러에게 람다 함수는 한 번만 호출될 거야!라고 명시해 줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@OptIn(ExperimentalContracts::class)
fun callsInPlaceTest(action: () -&amp;gt; Unit) {
    contract {
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
    }
    action()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contract는 아직까지 실험적인 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러에게 명시적으로 결과를 가르쳐주는 contract를 통해 nullCheck를 중복으로 했던 코드, 스마트캐스트 때문에 스파게티코드로 작성했던 부분도 개선할 수 있을 것이란 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직 실험적인 기능이라서 프로젝트에 적용하는 것이 조금은 위험하지 않을 것이란 생각이 들지만, 현재 코틀린의 버전이 1.9까지 올라갔기 때문에 걱정하지 않아도 될 것 같다.&lt;/p&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oguzhanaslann.medium.com/exploring-the-power-of-kotlin-contracts-for-better-code-quality-80bb279d7d2d&quot;&gt;https://oguzhanaslann.medium.com/exploring-the-power-of-kotlin-contracts-for-better-code-quality-80bb279d7d2d&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pspdfkit.com/blog/2018/kotlin-contracts/&quot;&gt;https://pspdfkit.com/blog/2018/kotlin-contracts/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/harrythegreat/kotlin-contracts-문법-쉽게-배워보기-9ffdc399aa75&quot;&gt;https://medium.com/harrythegreat/kotlin-contracts-문법-쉽게-배워보기-9ffdc399aa75&lt;/a&gt;&lt;/p&gt;</description>
      <category>Skils/Kotlin</category>
      <category>contract</category>
      <category>Kotlin</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/469</guid>
      <comments>https://jja2han.tistory.com/469#entry469comment</comments>
      <pubDate>Thu, 4 Apr 2024 01:53:10 +0900</pubDate>
    </item>
    <item>
      <title>[백준 2252] - 줄 세우기(Kotlin)[골드3]</title>
      <link>https://jja2han.tistory.com/468</link>
      <description>&lt;div style=&quot;display: none;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blWxVt/btsGkpK99mY/CybMks0aHaiGKfWADCzKp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blWxVt/btsGkpK99mY/CybMks0aHaiGKfWADCzKp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blWxVt/btsGkpK99mY/CybMks0aHaiGKfWADCzKp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblWxVt%2FbtsGkpK99mY%2FCybMks0aHaiGKfWADCzKp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2252&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2252&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712066829833&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2252번: 줄 세우기&quot; data-og-description=&quot;첫째 줄에 N(1 &amp;le; N &amp;le; 32,000), M(1 &amp;le; M &amp;le; 100,000)이 주어진다. M은 키를 비교한 회수이다. 다음 M개의 줄에는 키를 비교한 두 학생의 번호 A, B가 주어진다. 이는 학생 A가 학생 B의 앞에 서야 한다는 의&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2252&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2252&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cpZliQ/hyVGNq3YBE/OPzM9QWwy9vc2hbc5xXz4k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2252&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cpZliQ/hyVGNq3YBE/OPzM9QWwy9vc2hbc5xXz4k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2252번: 줄 세우기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 N(1 &amp;le; N &amp;le; 32,000), M(1 &amp;le; M &amp;le; 100,000)이 주어진다. M은 키를 비교한 회수이다. 다음 M개의 줄에는 키를 비교한 두 학생의 번호 A, B가 주어진다. 이는 학생 A가 학생 B의 앞에 서야 한다는 의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 분류가 위상정렬인 줄 모르고 풀었다. 어쩌다 보니.. 위상정렬처럼 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 입력단계에서 A와 B를 구분하는 과정에서 B로는 들어왔지만 A로 한 번도 들어오지 못한 녀석이&amp;nbsp;&lt;br /&gt;맨 뒤에 서야한다는것은 알았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ML1a/btsGksA2Ulq/XLCb86khgXa4zJD0AkYFSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ML1a/btsGksA2Ulq/XLCb86khgXa4zJD0AkYFSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ML1a/btsGksA2Ulq/XLCb86khgXa4zJD0AkYFSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ML1a%2FbtsGksA2Ulq%2FXLCb86khgXa4zJD0AkYFSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;284&quot; height=&quot;336&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력에 대해서 도식화를 해보면 오른쪽과 같은 관계도가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 통해서 3가지 분류를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;선택을 받지 못한 사람 (1,2)&lt;/li&gt;
&lt;li&gt;선택을 하지 못한 사람(4)&lt;/li&gt;
&lt;li&gt;선택을 하고, 받은 사람(3)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 선택을 받지 못한 사람이 가장 앞에 위치할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 조건을 보면 선택을 받지 못한 사람들 중에서의 우열은 구분할 필요가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서야 할 위치가 결정되는 조건은 본인을 선택한 사람이 이미 줄을 선 경우였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 들어오는 간선의 개수를 계산했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 인접리스트의 형태로&amp;nbsp; A-&amp;gt;B의 형태로 저장했다.&lt;/p&gt;
&lt;pre id=&quot;code_1712067442979&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
    graph[this[0]].add(this[1]) //this[0] -&amp;gt; this[1]
    edges[this[1]]++ //this[1]로 들어오는 간선의 개수 증가
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 우선순위가 가장 높은(들어오는 간선의 개수가 0인) 정점을 우선 출력해야 했기 때문에 Queue에 담았다.&lt;/p&gt;
&lt;pre id=&quot;code_1712067532294&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val dq = ArrayDeque&amp;lt;Int&amp;gt;()
for(i in 1 ..  n){
    if(edges[i]==0){
        dq.add(i)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayDeque를 사용한 이유는 Queue가 Java 기반이기도 하고, Java Queue에 비해서 사용법이 다르지도 않고, 성능도 차이가 없어서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직은 간단하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;dq를 돌면서 해당 원소가 뻗고 있는 정점에 대한 간선을 검사하고 , 간선을 하나 빼준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜냐하면 dq를 돌면서 하나씩 줄을 세우기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;간선의 개수가 0이라면 dq의 맨 앞에 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 앞에 넣어주는 이유는 간단하다. 예를 들어 1 2와 3 4가 입력으로 받았을때, dq에는 1과 3이 들어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1과 3중 하나에 대해서 뻗어가는 정점의 간선을 검사하고, 2의 간선의 개수가 0이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 dq에 2가 들어가야하는데, 이미 q에는 3이 들어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 1 -&amp;gt; 2의 순서를 유지하기 위해선 2가 3보다 앞에 위치해야 했고, 따라서 dq에 맨 앞에 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 앞에 넣지 않아서 1-&amp;gt;3-&amp;gt;2-&amp;gt;4가 출력되었다. 따라서 dq에 넣는 순서를 보장해줘야 했고, 맨 앞에 넣어주니 해결되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1712068092099&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlin.text.StringBuilder

class Solution {

    private val br = System.`in`.bufferedReader()
    private var n = 0
    private var m = 0
    private lateinit var graph: Array&amp;lt;ArrayList&amp;lt;Int&amp;gt;&amp;gt;
    private lateinit var edges: IntArray
    private val sb = StringBuilder()
    fun run() {
        input()
        solve()
        print(sb.toString())
    }

    private fun input() {
        br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
            n = this[0]
            m = this[1]
        }
        graph = Array(n + 1) { ArrayList() }
        edges = IntArray(n + 1) // 들어오는 간선의 개수
        repeat(m) {
            br.readLine().split(&quot; &quot;).map { it.toInt() }.apply {
                graph[this[0]].add(this[1]) // this[0] -&amp;gt; this[1]
                edges[this[1]]++ // this[1]로 들어오는 간선을 저장
            }
        }
    }

    private fun solve() {
        val dq = ArrayDeque&amp;lt;Int&amp;gt;()
        for(i in 1 ..  n){
            if(edges[i]==0){
                dq.add(i)
            }
        }
        while (!s.isEmpty()) {
            val now = dq.removeFirst()
            sb.append(now).append(&quot; &quot;)
            for (i in 0 until graph[now].size) {
                edges[graph[now][i]]--
                if(edges[graph[now][i]]==0){
                    dq.addFirst(graph[now][i])
                }
            }
        }
    }
}

fun main() {
    val solution = Solution().run()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CodingTest/Baekjoon</category>
      <category>Kotlin</category>
      <category>백준</category>
      <category>위상정렬</category>
      <author>재한</author>
      <guid isPermaLink="true">https://jja2han.tistory.com/468</guid>
      <comments>https://jja2han.tistory.com/468#entry468comment</comments>
      <pubDate>Tue, 2 Apr 2024 23:29:52 +0900</pubDate>
    </item>
  </channel>
</rss>