<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>imeanttobe</title>
        <link>https://velog.io/</link>
        <description>i meant to be</description>
        <lastBuildDate>Wed, 06 May 2026 13:19:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>imeanttobe</title>
            <url>https://images.velog.io/images/i_meant_to_be/profile/64741fcb-1d61-495f-9ea2-320b5179f450/1607857829283.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. imeanttobe. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/i_meant_to_be" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[스펙 주도 개발(SDD)의 실체는 뭘까?]]></title>
            <link>https://velog.io/@i_meant_to_be/What-Is-SSD</link>
            <guid>https://velog.io/@i_meant_to_be/What-Is-SSD</guid>
            <pubDate>Wed, 06 May 2026 13:19:49 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>학부 졸업을 앞둔 취업 준비생으로서 최근 업계에 먼저 나가신 분들께 자주 듣는 말이 &quot;요즘 코드는 다 AI가 쳐 준다.&quot;이다. 주변에서도 AI 주도 개발을 맛 보고 여러 프로젝트에 도입하려고 시도하시는 분들이 계신다. 무엇보다도, 여러 기업들이 이미 입사 전형에서 AI 사용 경험을 묻는 것을 볼 때, AI 주도 개발은 어떤 시도라기보다는 실체가 있는 흐름이라고 보는 게 적절해 보인다. (당장 이번 봄에만 해도 Toss와 기아의 지원서 작성 화면에서 AI 경험을 직접적으로 묻는 질문을 봤다)</p>
<p>그런데 AI 주도 개발을 해 본 적 없는 입장에서 AI와 관련한 저런 소식들을 들어 보아도 그 실체가 정확히 무엇인지 파악하기가 어려웠다. AI가 코드를 쳐 준다고 하는데 도대체 어떻게 쳐 주는지, 기존 프로젝트 컨벤션은 어떻게 가르쳐줘야 하는지, 더 나아가 코드 작성도 리뷰도 AI가 해 준다면 사람이 해야 하는 일은 무엇일지... 이런 모든 궁금증은 단편적인 소식과 블로그 게시물로는 해결하기 어려웠다.</p>
<p>그래서 이번에는 AI를 활용한 개발, 정확히는 &#39;<strong><em>스펙 주도 개발</em></strong>&#39;의 실체는 무엇인지, 어떤 과정을 통해 이루어지는지, 그리고 궁극적으로 인간은 그 변화의 흐름에서 어떤 역할을 담당하게 될지를 학습 차 정리해보고자 한다.</p>
<h1 id="정의">정의</h1>
<blockquote>
<p>스펙 주도 개발(이하 SDD)은 &#39;<strong><em>AI로 코딩하기 전에 명세(spec)을 먼저 쓰고, 그것이 인간과 AI의 공통 기준점(source of truth)이 되는 방식</em></strong>&#39;이다.</p>
</blockquote>
<p>확실하게 짚고 넘어갈 점은, 위에 적어 둔 SDD의 뜻은 <strong><em>어떤 공식적인 정의로 보기는 어렵다</em></strong>는 것이다. 단어 자체가 AI 도입에 따라 업계에서 자연스럽게 통용되기 시작한 용어이기 때문이다. 중요한 점은 전 과정에서 AI가 깊게 또는 얕게라도 빠짐없이 관여한다는 점이다. 코딩은 대부분 AI가 하고, 이젠 설계와 테스트도 돕기 시작했다. 이 점 때문에 신입 개발자 취업이 어려워졌고, &quot;AI가 개발자를 모조리 대체할 것이다&quot;라는 패닉에 가까운 전망도 어느 정도 확산되고는 있다.</p>
<p>그렇다면 인간은 이제 정말 필요하지 않을까? 현업자도 아니고 졸업도 안 한 내가 쉽사리 단정짓는 것이 이상하게 보일 수도 있다고는 생각하지만, 난 인간의 역할은 분명 여전히 남아 있고 앞으로도 그럴 것이라 전망한다.</p>
<h2 id="왜-인간이-여전히-중요한가">왜 인간이 여전히 중요한가</h2>
<p>개인적으로 인간이 방향을 잡아주는 게 왜 정의에 반드시 포함되어야 하냐고 생각하냐면, 첫 번째 이유로는 <strong><em>책임과 의사결정 권한은 여전히 인간에게 남아 있기 때문</em></strong>이다.</p>
<p>AI의 발전 속도는 눈부시다. 과거에는 오류만 잡고 방향은 판단하지 못한다는 게 정설이었다. 이제는 방향에 대한 의견도 낸다. 작업이 주어졌을 때, 제한 조건을 고려하고 프로젝트나 개발자의 성향 및 컨벤션까지 따져서 어떤 아키텍처를 선택해야 할지 제안해준다. 중요한 것은 AI는 &#39;제안&#39;만 주며, 더 나아가 AI의 제안은 최선의 선택이 아니라는 점이다. 소프트웨어 개발은 공학이고, 공학에는 트레이드오프가 반드시 따르기 때문이다. 최선의 선택 같은 허상은 없으며, 가치 판단만 있다. 따라서 AI가 정해준 방향을 따를지 말지를 결정하는 것은 대체 불가능하며 고유한 인간의 책임이다.</p>
<p>그래서 <strong><em>여전히 대부분의 개발자들은 AI를 검증의 대상</em></strong>으로 보고 있다. 널리 알려진 StackOverflow의 2025년 설문에 따르면 84%의 개발자가 AI를 사용하거나 계획 중임에도 불구하고 AI 출력의 정확성에 대해 46%의 개발자가 불신하고, 33%만이 신뢰한다고 답했다. AI의 신뢰성에 대해 대부분이 신중하게 접근하고 있다는 뜻이다. 이 지점에서 우리는 중요한 단어 2개를 구분해야 한다: 바로 &#39;방향&#39;과 &#39;오류&#39;이다. AI는 오류를 잡는 건 잘 한다. 예를 들어 우리가 학부 시절 과제할 때 자주 놓쳤던 C/C++ 포인터 오류 같은 것들 말이다. 반면, AI는 어떤 방향을 갈지 결정하진 못한다. 나는 이 뷰 모델이 그렇게 복잡하지 않아서 추상화 수준이 높은 MVI 대신 간단한 MVVM으로 작업하려고 했지만, 이걸 안 말해주면 AI는 MVI로 작업할지 모를 일이다. 인간이 개입하는 지점이 바로 여기다. AI가 이런 저런 방향으로 갈 수 있다고 읊어주면, 그 중 여기로 가자고 결정하는 게 인간인 것이다.</p>
<h2 id="향후-개발자의-역할은">향후 개발자의 역할은</h2>
<p>AI는 분명 코드를 잘 친다. 설계도 잘 한다. 그러나 여전히 의사결정 권한은 인간에게 있으며, 비즈니스적 특징이나 코드 외 범위에서 정해진 컨벤션 등 인간에게 더 가까운 정보는 직접 알려주지 않으면 모른다. 따라서 인간이 AI의 작업을 디렉팅해주지 않으면 오히려 잘못된 코드를 칠 가능성이 높다. 오히려 AI 없는 개발에 비해서 효율이 더 떨어질 가능성이 있는 것이다.</p>
<p>정리하면, SDD 시대에서 AI가 반복적 작업 등 일부 맥락에서 인간에 비해 효율적이고, 따라서 인간이 하던 작업을 일부 대체하는 것은 지극히 자연스러운 일이다. 그러나 책임 소재와 의사결정의 책임이 여전히 남아 있는 만큼, 인간의 역할이 ‘직접 모든 코드를 쓰는 사람’에서 ‘문제를 정의하고, 맥락을 제공하고, 결과를 승인하는 사람’으로 이동하고 있다는 점도 분명히 짚고 넘어가야 할 것이다.</p>
<h1 id="과정">과정</h1>
<h2 id="과거와의-차이">과거와의 차이</h2>
<p>보통 무언가를 개발한다고 하면 아래와 같은 과정을 거치곤 한다:</p>
<ul>
<li>기획에 따른 설계</li>
<li>구현</li>
<li>테스트</li>
<li>리뷰</li>
<li>병합 요청</li>
</ul>
<p>일반적으로 AI가 없던 시절에는 위 과정을 대부분 사람이 맡아 진행했다. 그러나 AI가 도입된 이후에는 다르다. 기존에 인간이 맡던 작업을 오히려 주도하거나, 아니면 인간과 함께 작업을 분담하게 되었다:</p>
<ul>
<li>기획에 따른 설계 (인간이 주로, AI가 보조)</li>
<li>구현 (AI가 주로)</li>
<li>테스트 (인간과 AI가 함께)</li>
<li>리뷰 (인간이 주로, AI가 보조)</li>
<li>병합 요청 (인간이 주로)</li>
</ul>
<p>AI가 맡는 작업들이 많아진 만큼, 우리 인간이 해야 할 일은 AI가 우리가 원하는 방향대로 작업을 끌고 갈 수 있게 명확한 방향을 제시해주고 이를 벗어나지 못하게 통제하는 것이다. 따라서 고전적인 작업 과정은 이제는 아래와 같이 변화하게 된다:</p>
<h2 id="일반적인-sdd-개발-과정">일반적인 SDD 개발 과정</h2>
<blockquote>
<p>일반적인 SDD 개발 과정은 &#39;문제 정의 &gt; 탐색 및 설계 &gt; 검토 및 승인 &gt; 구현 &gt; 기계적 검증 &gt; 결과 평가 &gt; 마무리&#39; 정도로 요약할 수 있다.</p>
</blockquote>
<p>앞에서 SDD의 정의에 대해 언급했듯, 위 과정 역시 국제 표준처럼 공식적인 것은 분명 아니다. 그러나 어느 정도 위와 비슷한 형태로 통용되고 있다는 점에서 저렇게 정리해 두었다. 또한, 이 과정은 작업의 크기나 중요도에 따라 일부 생략되거나 합쳐져 진행될 수 있다는 점도 인지해두도록 하자. 즉, 중요한 점은 대략 이런 느낌으로 작업이 진행된다는 것이며, 환경에 따라 얼마든지 유연성 있게 재구성할 여지가 충분하다는 점이다.</p>
<p>차례로 각 단계에 대해 정리해보자:</p>
<h3 id="문제-정의">문제 정의</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | (추상적인 수준에서) 작업의 목표, 제약 조건, 완료 조건을 설정</li>
<li>AI | 인간의 요구 사항에 대한 일부 수정 제안</li>
</ul>
</blockquote>
<p>작업 목표를 설정하는 것은 당연하다. 필요에 따라 제약 조건을 걸기도 한다. 회사 내에 디자인 시스템이 이미 존재하는 경우 그걸 사용하라고 강제하는 등. 완료 조건이 조금 생소할 수 있는데, CLI는 단순 채팅만 하는 웹 기반 서비스와는 다르게 파일을 수정하고 MCP 서버에 접속하는 등 할 수 있는 일과 볼 수 있는 자료가 매우 많다. 따라서 어떤 파일과 디렉터리를 수정할 수 있는지, 어떤 명령은 금지되는지, 어떤 테스트를 반드시 통과해야 하는지 등을 명시해줘야, CLI의 작업 정확도를 올릴 수 있다.</p>
<p>또한 필요하다면 이슈를 생성하거나 티켓을 발행하고 브랜치를 빼는 작업도 AI가 대신 진행하도록 할 수도 있다.</p>
<h3 id="탐색-및-설계">탐색 및 설계</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | (구체적인 수준에서) 참고할 디렉터리, 사전에 정해진 아키텍처 등을 안내</li>
<li>AI | 코드베이스 탐색, 파일 읽기, 계획서 작성, 설계 방향 설정</li>
</ul>
</blockquote>
<p>&#39;문제 정의&#39; 단계에서 구체화된 목표에 따라, AI는 필요한 코드를 읽고 분석하여 어떻게 작업할지 계획을 짠다. 이 과정에서 계획서에 해당하는 임시 Markdown 문서가 생성되기도 한다.</p>
<h3 id="검토-및-승인">검토 및 승인</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | AI의 설계를 검토하고 승인 또는 반려</li>
<li>AI | 반려 시 반려 사유에 따라 계획 수정</li>
</ul>
</blockquote>
<p>이 단계는 특히 대규모 또는 위험한 작업에서 매우 중요한데, 일반적인 채팅 기반 LLM에 비해 할 수 있는 일이 많은 CLI가 검토 없이 작업을 섣불리 시작할 경우 코드에 의도하지 않은 변경 또는 삭제가 일어날 수 있기 때문이다. 더 나아가 개발 환경이 MCP 서버 등 외부 환경과 연결되어 있을 경우, 실제로 프로덕션 또는 서비스 환경에 문제가 생길지도 모를 일이다. 따라서 &#39;문제 정의&#39; 단계에서 개발자가 의도한 방향대로 설계가 이루어졌는지 컨센서스를 한 번 더 짚고 넘어가야만, 혹시 모를 문제를 예방할 수 있다.</p>
<h3 id="구현">구현</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | (필요한 경우) 제약 사항의 추가 또는 추가 수정 요청</li>
<li>AI | 승인된 설계안에 따라 기능 구현</li>
</ul>
</blockquote>
<p>이 과정은 많은 설명이 필요하진 않다. AI가 설계안에 따라 코드를 치는 과정이다.</p>
<h3 id="기계적-검증">기계적 검증</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 또는 AI | LLM 기반 작업이 아닌 테스트, 빌드, 린팅, 포매팅 등 기계적 검증의 &#39;호출&#39;</li>
</ul>
</blockquote>
<p>전통적인 개발 과정에서도 유닛 및 통합 테스트, 빌드 테스트, 린팅 및 포매팅 검사 등은 반드시 진행했다. SDD에서도 이 과정은 대체로 유지된다. 검증 자체는 인간, CI 스크립트, CLI 등 누구라도 호출할 수 있지만, 테스트 자체는 시스템 명령어 등으로 정의되어 결정적(deterministic)으로 진행된다.</p>
<p>보통 이 단계를 거치는 이유는 AI의 자의적 판단에 맡기는 것보다는 테스트, 빌드와 린트를 결정적인 명령으로 강제하는 편이 더 신뢰할 만하기 때문이다. 만약 이 기계적 검증의 시작을 AI가 호출할 경우, 다음 과정인 &#39;결과 평가&#39;에서 반드시 AI가 의도한 대로 검증을 시도했는지 평가에 포함하는 것도 잊어서는 안 될 것이다.</p>
<h3 id="결과-평가">결과 평가</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | AI의 작업물을 여러 방면으로 평가 및 향후 작업 방향 개선</li>
<li>AI | 평가 결과에 따른 전역 규칙 등 개발 환경 수정 제안</li>
</ul>
</blockquote>
<p>SDD에서 테스트라고 하면 보통 아래의 2가지로 구분이 가능하다:</p>
<ul>
<li>코드 및 산출물 검토 | 빌드, 통합 테스트 등으로 &#39;기계적 검증&#39; 과정에서 진행함</li>
<li>에이전트에 대한 평가 | 여기서 진행</li>
</ul>
<p>위에 적힌 대로 이 과정은 코드 자체에 대한 평가가 아니라, AI 자체에 대한 평가다. AI가 개발자가 요청한 일을 잘 처리했는지 정성적, 정량적 기준을 마련하여 평가하는 것이다. 나는 처음에 LLM 기반으로 동작하는 AI의 작업을 정량적 기준으로 평가할 수 있는지가 의문이었다. 그런데 사람이 사람을 평가하는 거라고 생각해보니 사실 꽤나 자연스러운 일이더라.</p>
<p>이 과정에서는 다음과 같은 항목들을 따진다. 특히, 이 중 반복해서 사용하는 것들은 지표를 점수화하는 편이 유용하다:</p>
<ul>
<li>스킬 및 커맨드 적중률 | 의도한 스킬이 잘 트리거되었는지</li>
<li>요구사항 충족률 | 요구한 사항들이 얼마나 충족되었는지</li>
<li>금지 규칙 위반률 | 금지한 제한 사항이 얼마나 위반되었는지</li>
</ul>
<p>특히 AI의 작업은 여러 개의 스킬 또는 커맨드로 구성되는데, AI가 LLM 기반인 만큼 내가 &#39;이런 저런 스킬을 사용해서 작업해줘&#39;라고 요청해도 그렇지 않을 가능성이 있다. 이러한 LLM 특유의 변동성 및 비결정성을 이 과정을 통해 평가하는 것이다. 그리고 평가 결과 문제가 있었다면, 규칙을 수정하는 등의 방안을 통해 향후 작업 정확도를 개선할 수 있다.</p>
<p>따라서, 이 과정은 에이전트 워크플로의 적합성을 돌아보고, 반복 작업의 정확도를 높이기 위한 개선점을 찾는 과정이라는 점에서 향후 지속 가능한 SSD 유지를 위해 매우 중요한 단계이다.</p>
<h3 id="마무리">마무리</h3>
<p>위 단계에서 인간과 AI의 역할은 아래와 같다:</p>
<blockquote>
<ul>
<li>인간 | PR 다듬기 및 병합 요청</li>
<li>AI | (필요한 경우) PR 본문 작성 시 코드 변경 내용 요약</li>
</ul>
</blockquote>
<p>AI의 작업물에 큰 문제가 없다고 판단되면, 이 과정까지 발생한 임시 파일을 정리하고 GitHub에 PR을 올리는 등의 과정을 진행하면 된다. GitHub 상에서 코드 리뷰도 진행할 것이고, 모든 제반 작업이 끝나면 병합을 진행하면 된다.</p>
<h2 id="논의-사항">논의 사항</h2>
<p>다음은 이 순서를 보면서 내가 가졌던 궁금증이다. 애초에 SSD라는 컨셉 자체가 공식적이지 않고 일종의 &#39;국룰&#39;로 통하고 있는 만큼, 나의 질문과 답변 역시 공식적인 성격을 가진다기보다는 <strong><em>개인의 의견이 많이 섞여 있다는 점</em></strong>을 감안하고 보면 좋을 것이다.</p>
<h3 id="왜-이-순서가-통용되는가">왜 이 순서가 통용되는가</h3>
<p>먼저 주요 CLI가 승인 및 통제와 관련된 기능을 여럿 제공하고 있기 때문이다. Codex는 Approval modes를, Claude Code는 Permission 등을 제공하여 개발자가 승인한 작업만 실행할 수 있게 하거나, 자율적으로 작업 범위를 조절하도록 하는 등 다양한 권한을 제공하고 있다.</p>
<p>다음으로는 리스크 관리가 용이하기 때문이다. 돌아보면 알겠지만 검증 및 승인 절차를 상당히 많이 포함하고 있다. 설계 과정에서 코드를 변경하지 않는 읽기 전용 계획 수립, 구현 후의 정적 테스트, 그리고 AI의 전반적인 작업을 평가하는 과정까지, 모두 개발자의 의도와 일치하고 있는지를 반복하여 물음으로써 AI가 탈선하지 않게 확인하고 있다. 그리고 구현 이전에 반드시 읽기 전용 설계를 포함함으로써, 시간과 토큰을 소비한 구현을 애써 되돌리는 일이 없게 미리 예방하는 목적도 있다.</p>
<h3 id="소규모-작업에는-이-중-일부만-진행해도-되는가">소규모 작업에는 이 중 일부만 진행해도 되는가</h3>
<p>그렇다고 볼 수도 있지만 아닌 것 같기도 하다.</p>
<p>아무래도 그냥 버튼 디자인이나 간단한 로직 리팩터링에 7단계 과정을 다 거치기에는 좀 복잡해 보이는 것도 사실이다. 그래서 만약 조직 내에서 유연성을 좀 확보하고 싶다면, 1단계 &#39;문제 정의&#39; 과정에서 볼륨이 크지 않을 때 바로 4단계 &#39;구현&#39;으로 넘어가는 것도 가능은 하겠다. 다만 엄밀히 말하면 1단계에서 4단계로 넘어간 게 아니라, 1단계부터 3단계를 좀 축약하여 진행한 것이라고 보는 게 맞을 것이다.</p>
<p>아무튼 일은 유연하게 진행하는 게 중요하겠다.</p>
<h3 id="cli-스킬-테스트는-코드-테스트와-뭐가-다른가">CLI 스킬 테스트는 코드 테스트와 뭐가 다른가</h3>
<p>코드 테스트는 코드가 주어진 입력에 의도한 결과를 내는지, 즉 로직대로 돌아가는지 평가한다. 그러나 CLI 스킬 테스트는 아래와 같은 것들을 평가한다:</p>
<ul>
<li>이 작업에서 사용자가 의도한 스킬이 트리거되었는가</li>
<li>기대한 명령이 실행되었는가</li>
<li>금지된 변경을 실행하였는가</li>
<li>최종 결과가 초기 설계와 전역 공통 규칙을 만족하는가</li>
</ul>
<p>이런 것들은 입력대로 출력이 반드시 나올 것을 기대하는 결정적(deterministic)인 코드 테스트와 다르다. LLM 기반 AI는 같은 입력을 넣어도 답변이 다르게 나오는 비결정적(non-deterministic)인 특징을 지니기 때문이다. 비결정적인 만큼 검증과 통제를 확실하게 해야만, 부작용과 문제를 사전에 확실히 방지할 수 있다.</p>
<h3 id="과정-중-생성되는-임시-문서도-영구적으로-보존해야-하는가">과정 중 생성되는 임시 문서도 영구적으로 보존해야 하는가</h3>
<p>AI가 일련의 과정을 진행하면서 상당히 많은 Markdown 문서가 생성된다. 읽기 전용 설계 단계에서 발생하는 계획서 등이 대표적인 사례다. 그렇다면 이 문서들을 PR 또는 Git 저장소에 영구적으로 포함해야 할까?</p>
<p>상황에 따르지만 보통은 &#39;아니다&#39;로 결론을 내릴 수 있는 듯하다. 보통 영구적으로 보존하는 것은 코드 컨벤션과 같은 언제 어느 상황에서도 모든 코드베이스에 적용되는 전역 규칙들이다. 반면, 위에서 말한 임시 문서들은 특정 작업에만 종속되어 있기 때문에 전역적이라고는 보기 어렵다. 이런 지역적 문서들을 전부 다 저장소에 포함하게 되면, 변경 사항이 매우 많아져서 코드 리뷰하는 사람이 보기에 매우 불편하며, 향후 다시 재사용될 가능성도 높지 않다.</p>
<p>따라서 이 문서들은 작업 중에만 사용하고, 가능하면 PR을 준비할 때 본문 작성에 활용하는 것을 마지막으로, PR에는 포함하지 않는 게 좋다는 것이 나의 결론이다.</p>
<h3 id="github에서-코드-리뷰를-하는데-cli에서도-또-해야-하나">GitHub에서 코드 리뷰를 하는데 CLI에서도 또 해야 하나</h3>
<p>요즘 GitHub에서 작업을 하는 경우, 많은 분들이 Gemini Code Assist나 CodeRabbit과 같은 리뷰 전문 에이전트를 파트너로 활용하고 계실 것이다. 그런데 위 과정에서는 로컬 환경에서도 빡빡하게 리뷰 및 검증을 진행하고 있다. 그럼 이런 궁금증이 들 수 있다: &quot;로컬 CLI와 GitHub 양쪽에서 굳이 중복으로 리뷰를 해야 하나?&quot;</p>
<p>이에 대한 개인적인 결론은 &quot;중복 리뷰 할 만하나 필수는 아니다&quot;이다. 로컬 리뷰와 PR 리뷰는 아래와 같은 차이를 가진다:</p>
<ul>
<li>개발 직후 로컬에서 리뷰 vs. PR 올라간 후에 리뷰</li>
<li>커밋되지 않은 변경 사항 위주 리뷰 vs. 저장소, 브랜치, PR 문맥을 고려하여 리뷰</li>
<li>로컬 개발 환경 사용 vs. GitHub Actions 등 CI/CD 환경 사용</li>
</ul>
<p>차이가 아주 크다고 보기는 어렵지만 완전히 동일한 과정도 아니므로, 작업 볼륨이 크지 않다면 둘 중 하나만 진행해도 무방할 수 있다.</p>
<h3 id="이슈-및-브랜치-생성-pr-요청도-모두-ai가-할-수-있는가">이슈 및 브랜치 생성, PR 요청도 모두 AI가 할 수 있는가</h3>
<p>그럴 가능성이 높다. (물론 사용하는 CLI 서비스나 환경에 따라 다를 순 있다.) 애초에 이론적으로 코드 수정도 할 수 있고 변경 내용을 요약해 자연어로 정리할 수도 있으며 MCP 서버를 통해 외부에서 명령 수행도 되는 CLI 에이전트가 브랜치 하나 파고 PR 요청 쓰는 건 전혀 어려운 일이 아니다.</p>
<p>다만 팀 내에서 자동화를 어디까지 할지 합의하는 게 과제일 것이다. 어떤 팀은 그래도 PR 요청은 각자가 변경 내용에 책임을 질 수 있게 직접 검토하고 작성하자고 합의할 수 있는 일이고, 효율을 중시하는 팀이라면 PR 요청까지 자동화할 수 있을 것이다.</p>
<h1 id="마무리-1">마무리</h1>
<p>최근 개인적으로 절실히 느끼는 점 중 하나는 어떤 도구를 쓰더라도 그걸 왜 써야 하는지를 잘 알아야 한다는 것이다.</p>
<p>이 라이브러리를 왜 썼는지, 이 기능을 구현할 때 왜 코드를 이렇게 쳤는지, 다른 방식은 왜 선택하지 않았는지 등... 그리고 이러한 접근 - 명확한 근거에 기반하여 결정하는 것 - 은 사실 어떻게 보면 굉장히 당연하고 자연스러운 마음가짐이며 무엇보다도 프로그래밍이 아닌 다른 업무를 볼 때에도 반드시 지켜야 한다.</p>
<p>예를 들어, 내가 어떤 회사에 입사했다고 쳤을 때, 그 회사에서 그렇게 해 왔으니까 나도 그렇게 해야 겠다고 생각하는 게 아니라, 그 방식이 현 시점에서도 여전히 적절한지 그 근거를 찾으며 생각해보고, 그렇지 않다면 워크플로의 개선을 요구할 수 있다는 거다. 그리고 보통 이렇게 먼저 문제를 찾아 개선을 이루는 사람들을 &#39;<strong><em>주인 의식이 있다</em></strong>&#39;고 표현하는 것 같다. 실제로도 보통 이러한 문제들은 기존 관행을 그대로 유지하는 경우에 자주 발생하기도 하고...</p>
<p>아무튼 결론은 이유 없이 행동하지 말자는 것이다. 에이전트와 일할 때에도 왜 에이전트를 사용하면 더 효율적인지, 에이전트에게 왜 이렇게 일을 시켜야 하는지 그 이유를 알아야 그만큼 에이전트를 더 잘 다룰 수 있을 것이다.</p>
<p>&lt;손자병법&gt;에서 유래한 &#39;<strong><em>지피지기 백전불태</em></strong>&#39;라는 말이 괜히 나온 게 아닐 것이다. 잊지 말자.</p>
<h1 id="참고-문헌">참고 문헌</h1>
<ul>
<li>StackOverflow. 2025 Stack Overflow Developer Survey. <a href="https://stackoverflow.co/internal/resources/2025-stack-overflow-developer-survey-for-leaders/ai-adoption/">링크</a>.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin Dispatchers와 C++ thread 멸망전 - Android]]></title>
            <link>https://velog.io/@i_meant_to_be/kotlin-dispatchers-c-thread-deathmatch</link>
            <guid>https://velog.io/@i_meant_to_be/kotlin-dispatchers-c-thread-deathmatch</guid>
            <pubDate>Sat, 28 Feb 2026 18:33:11 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>작년 2학기에 수강했던 보안프로젝트설계 강의에서는 완전 동형 암호(FHE) 스킴을 활용하여 프로젝트를 진행하는 과제가 있었다. 나는 FHE 덧셈 연산을 활용하여, 기밀성이 보장되는 개표가 가능한 투표 앱을 개발했다. 다만, 아쉬웠던 것은 곱셈 연산은 활용해볼 기회가 없었다는 점이다. 그래서 프로젝트가 끝난 이후, 곱셈 연산을 구현해보고 이를 Android에서 사용할 수 있는 여러 병렬 처리 환경에서 실험해보기로 했다. 미루고 미루다 개강 직전에야 끝냈는데, 실험 결과를 간단하게 정리해보자.</p>
<h1 id="배경">배경</h1>
<p>여기서는 내가 알고 있는 것을 다시 리뷰하고, 혹시 있을지 모를 독자 분들을 위한 설명도 겸하여 필요한 배경 지식을 간단히 짚고 넘어가겠다.</p>
<h2 id="완전-동형-암호-fhe">완전 동형 암호 (FHE)</h2>
<p>완전 동형 암호(FHE, Fully Homomorphic Encryption) 스킴이란 암호화 된 상태에서도 연산의 정확성이 보장되는 암호화 스킴을 말한다. 이해를 돕기 위해 기존의 암호화 방식과 비교하면 아래와 같은 <strong><em>엄청난</em></strong> 차이점이 있다:</p>
<blockquote>
</blockquote>
<ul>
<li>일반 스킴 | 암호화된 2개의 수끼리 연산하기 위해서는 둘 다 모두 <strong><em>반드시 복호화</em></strong>를 해야만 함</li>
<li>FHE 스킴 | 암호화된 2개의 수를 <strong><em>복호화하지 않고</em></strong> 그 상태 그대로 연산할 수 있음</li>
</ul>
<p>기존의 AES 암호화와 같은 방식에서는 암호문 상태에서 산술 연산을 직접 지원하지 않으므로, 연산을 하고 싶다면 복호화가 필요하다. 물론 모든 서비스 제공자들은 &quot;우리는 보안을 잘 지키고 있어요!&quot; 하면서 사용자를 안심시키곤 하나, 최근 쿠팡이나 통신사들 탈탈 털리는 걸 본 사람들이라면 아마 저 말을 신뢰하기는 다소 어려울 수 있다는 점을 알 것이다.</p>
<p>FHE 스킴에서는 저런 걱정을 할 필요가 없다. 복호화 자체가 필요 없기 때문이다. 물론 연산 외 과정에서는 문제가 있을 수 있으나, 일단 복호화를 1번이라도 덜 거친다는 것 자체가 보안성에 있어 엄청난 메리트인 것은 틀림이 없다.</p>
<h2 id="bfv-스킴의-연산">BFV 스킴의 연산</h2>
<p>이 프로젝트에서 사용한 FHE 스킴은 정수를 대상으로 하는 BFV 스킴이다. 서울대학교 연구진이 개발한 실수 대상 CKKS 스킴도 있기는 하나, 득표 수는 자연수로 표현되므로 BFV를 채택했다. 이 스킴은 크게 2가지 연산을 제공하는데, 덧셈과 곱셈이다. 그러나 다음과 같은 특이 사항이 있다:</p>
<blockquote>
<p>곰셈의 연산 부하가 덧셈에 비해 <strong><em>압도적으로</em></strong> 높다.</p>
</blockquote>
<p>그 이유는 BFV는 내부적으로 연산 과정에서 암호문의 원본 평문을 추측하기 어렵도록 노이즈를 도입하고 있는데, 곱셈의 노이즈 증가가 덧셈에 비해 많이 크기 때문이다. 그래서 곱셈 연산에서는 노이즈를 줄여주기 위해 키 스위칭(key switching)이라는 과정을 한 번 더 거치는데, 이 과정에서 원본에 비해 차수가 커진 결과가 나오기 때문에, 차수를 줄이는 재선형화(relinearize)가 필요하다. 또한, 연산 과정에서 노이즈도 더 많이 발생하는 만큼, 일정 수준에 도달하면 노이즈를 깎아주는 부트스트래핑(bootstrapping)도 진행해야 한다. 과정이 더 붙은 만큼, 부하가 더 많이 가는 작업인 것이다. </p>
<p>따라서, BFV 스킴을 사용함에 있어 덧셈 연산만 쓰는 서비스보다 곱셈 연산을 쓰는 서비스에 더 많은 처리 성능이 필요하게 된다.</p>
<h2 id="ndk와-jni">NDK와 JNI</h2>
<p>Android NDK(Native Development Kit)는 Android에서 C/C++ 코드를 사용해야 할 때 사용하는 개발 도구이다. C/C++ 코드를 CMake와 함께 빌드하여 앱에 네이티브로 올릴 수 있게 도와준다. 그리고 그 과정에서 JNI(Java Native Interface)를 통해 소통하게 된다. 즉, 정리하면 Android의 C/C++ 코드들은 C/C++ → JNI → Kotlin의 도식으로 이어진다는 것이다.</p>
<p>또한, 한 가지 재미있는 점은 NDK를 통해 작성된 C/C++ 코드에서도 C/C++ 수준에서의 병렬 처리를 사용할 수 있다는 듯하다. 다시 말해, <code>thread</code>와 OpenMP를 사용할 수 있다는 것이다.</p>
<h2 id="coroutines와-dispatchers">Coroutines와 Dispatchers</h2>
<p><a href="https://velog.io/@i_meant_to_be/thread-coroutines-dispatchers-kotlin">링크</a>를 참고하라.</p>
<h1 id="실험-설계">실험 설계</h1>
<h2 id="접근">접근</h2>
<p>위에서도 언급했듯이, BFV 덧셈보다 곱셈이 부하가 크다. 상당히 크다. 그래서 보통은 서버 단에서 연산을 처리하도록 하는 게 국룰이라고 들었으나, 경우에 따라 온디바이스 처리가 필요한 경우도 있을 것이다. 개인 프라이버시를 반드시 지켜야 하는 서비스라던지. 이 경우 디바이스에서 곱셈 연산을 진행해야 하는데, 부하가 큰 만큼 최대한 효율적으로 처리를 해야 할 필요가 있을 것이다.</p>
<p>이를 위해 나는 NDK를 사용하는 Android 앱에서 사용 가능한 아래의 4가지 환경에서 곱셈 연산을 병렬 처리로 실행해보고, 어떤 선택지가 가장 좋을지를 평가해보고자 한다:</p>
<blockquote>
</blockquote>
<ul>
<li>Kotlin Side <code>Dispatchers.IO</code></li>
<li>Kotlin Side <code>Dispatchers.Default</code></li>
<li>Kotlin Side <code>newFixedThreadPoolContext</code></li>
<li>C++ Side <code>thread</code></li>
</ul>
<h2 id="조건">조건</h2>
<p>모든 환경에서 동일하게 적용되는 공통 실험 조건은 아래와 같다:</p>
<blockquote>
</blockquote>
<ul>
<li>연산 횟수 | 100 번</li>
<li>측정 대상 | 곱셈 연산을 100번 반복했을 때의 실행 시간</li>
<li>측정 기기 | Galaxy S24+ (10 코어 / 슈퍼 1, 빅 2, 미들 3, 리틀 4)</li>
</ul>
<p>실험을 진행한 환경은 아래와 같다:</p>
<blockquote>
</blockquote>
<ul>
<li>C++ <code>thread</code>, 스레드 1개</li>
<li>C++ <code>thread</code>, 스레드 4개</li>
<li>C++ <code>thread</code>, 스레드 6개</li>
<li>C++ <code>thread</code>, 스레드 10개 (실제 물리 코어 수와 동일)</li>
<li>C++ <code>thread</code>, 스레드 20개</li>
<li>Kotlin <code>newFixedThreadPoolContext</code>, 스레드 10개 (실제 물리 코어 수와 동일)</li>
<li>Kotlin <code>Dispatchers.IO</code>, 스레드 10개</li>
<li>Kotlin <code>Dispatchers.IO</code>, 스레드 100개</li>
<li>Kotlin <code>Dispatchers.Default</code>, 스레드 10개</li>
<li>Kotlin <code>Dispatchers.Default</code>, 스레드 100개</li>
</ul>
<h2 id="코드">코드</h2>
<p>벤치마킹 및 실험에 사용한 코드는 아래와 같다:</p>
<ul>
<li><a href="https://github.com/i-meant-to-be/cau-spd-consensus-fe/blob/develop/app/src/main/cpp/JNIBenchmark.cpp">C++ 코드</a></li>
<li><a href="https://github.com/i-meant-to-be/cau-spd-consensus-fe/blob/develop/app/src/main/java/com/imeanttobe/consensusapp/domain/benchmark/RunBenchmarkUseCase.kt">Kotlin 코드</a></li>
</ul>
<h1 id="결과">결과</h1>
<p>실험 결과는 아래와 같다:</p>
<h2 id="각-환경별-연산-소요-시간">각 환경별 연산 소요 시간</h2>
<table>
<thead>
<tr>
<th>언어</th>
<th>구분</th>
<th>스레드 수</th>
<th>시간 (ms)</th>
</tr>
</thead>
<tbody><tr>
<td>C++</td>
<td><code>thread</code></td>
<td>1개</td>
<td>46,799</td>
</tr>
<tr>
<td>C++</td>
<td><code>thread</code></td>
<td>4개</td>
<td>17,536</td>
</tr>
<tr>
<td>C++</td>
<td><code>thread</code></td>
<td>6개</td>
<td>15,057</td>
</tr>
<tr>
<td>C++</td>
<td><code>thread</code></td>
<td>10개</td>
<td><strong>_<span style="color: red">10,864</span>_</strong></td>
</tr>
<tr>
<td>C++</td>
<td><code>thread</code></td>
<td>20개</td>
<td>11,090</td>
</tr>
<tr>
<td>Kotlin</td>
<td><code>newFixedThreadPoolContext</code></td>
<td>10개</td>
<td>11,100</td>
</tr>
<tr>
<td>Kotlin</td>
<td><code>Dispatchers.IO</code></td>
<td>10개</td>
<td>11,324</td>
</tr>
<tr>
<td>Kotlin</td>
<td><code>Dispatchers.IO</code></td>
<td>100개</td>
<td>13,017</td>
</tr>
<tr>
<td>Kotlin</td>
<td><code>Dispatchers.Default</code></td>
<td>10개</td>
<td>11,199</td>
</tr>
<tr>
<td>Kotlin</td>
<td><code>Dispatchers.Default</code></td>
<td>100개</td>
<td>11,988</td>
</tr>
</tbody></table>
<h2 id="분석">분석</h2>
<h3 id="베스트-케이스는-c-스레드-10개">베스트 케이스는 C++, 스레드 10개</h3>
<p>최대 성능은 C++, 스레드 10개 사용 환경이었다.</p>
<p>아무래도 C++ 단에서 JNI와 통신하지 않으면서 모든 연산을 처리해버렸고, 동시에 스레드 수도 실제 물리 코어 수와 동일하게 유지하여 문맥 교환 비용을 줄인 것이 최대 성능의 주 이유라고 생각해볼 수 있을 것 같다. 그러나, 현재 내 기기가 빅-리틀 아키텍처를 택하는 만큼 각 코어의 성능이 다르기 때문에, 오히려 빅 코어의 활성화를 노리는 스레드 수를 정하는 게 단순히 물리 코어 수와 같은 스레드 수를 쓰는 것보다 더 높은 성능을 낼 수 있다는 점은 감안하고 봐야 할 것이다.</p>
<h3 id="kotlin-베스트-케이스는-newfixedthreadpoolcontext">Kotlin 베스트 케이스는 <code>newFixedThreadPoolContext</code></h3>
<p>Kotlin 내에서는 스레드 풀을 아예 별도로 생성해버리는 <code>newFixedThreadPoolContext</code> 케이스가 가장 높은 성능을 냈다.</p>
<p>이는 Coroutine이 다른 작업에도 동원될 수 있는 <code>Dispatchers</code>와는 다르게, 이 경우 아예 Java의 <code>ExecutorService</code> 수준에서 별도의 스레드 풀을 생성해버리기 때문이다. 정말로 이 작업만을 위해 독점적으로 생성된 스레드 풀인 만큼, 작업 처리에 집중되어 높은 성능을 내는 것은 당연한 결과일 것이다.</p>
<h3 id="성능은-스레드-20개보다-10개가-더-우수함">성능은 스레드 20개보다 10개가 더 우수함</h3>
<p>C++ 스레드 10개와 C++ 스레드 20개 대조군에서, 스레드 10개가 훨씬 더 높은 성능을 냈다.</p>
<p>이유로는 스레드 개수가 실제 물리 코어 개수보다 많아지면서, 여러 스레드가 물리 코어를 오가느라 문맥 교환이 상당히 많이 일어났기 때문이라고 추측해볼 수 있겠다.</p>
<p>또한 동일한 이유로 Kotlin <code>Dispatchers.IO</code> 스레드 10개와 스레드 100개 대조군, Kotlin <code>Dispatchers.Default</code> 스레드 10개와 스레드 100개 대조군에서도 성능 차이가 발생했다고 볼 수 있겠다.</p>
<h3 id="처리-속도가-스레드-개수에-비례하진-않음">처리 속도가 스레드 개수에 비례하진 않음</h3>
<p>C++ 스레드 1개는 약 46초 소요, 베스트 케이스인 C++ 스레드 10개는 약 11초 소요되었다. 즉, 대충 잡아서 4.5배 정도의 성능 향상이 있었다는 것이다. 왜 스레드 개수는 10개 늘어났는데 성능은 4.5배밖에 늘지 않았을까? 그 이유는 내 스마트폰인 Galaxy S24+의 CPU가 빅-리틀 구조를 택하고 있기 때문으로 추측된다.</p>
<p>빅-리틀 구조는 성능이 매우 좋은 빅 코어 조금과 그보다는 낮은 리틀 코어 다수를 섞는 전략이다. 위에서도 말했듯, Galaxy S24+의 CPU는 슈퍼 1, 빅 2, 미들 3, 리틀 4, 총 10개의 코어로 구성되어 있다. 그리고 나는 실험 설계 시 각 코어에 동일한 횟수의 곱셈 연산을 할당했다. 즉, 빅 코어 1개와 리틀 코어 1개가 같은 수의 곱셈을 진행했다는 것이다. 당연히 빅 코어에서 작업이 먼저 끝났을 것이고, 빅 코어는 리틀 코어가 작업을 마치길 기다렸을 것이다. 이러한 작업 불균형은 사실 병렬 처리에서는 흔하게 마주치는 문제이며, 이걸 어떻게 잘 풀어낼 것인지가 병렬 처리 프로그래머의 실력을 가늠하는 척도이기도 할 것이다.</p>
<h3 id="스레드-100개에서-보이는-dispatchersio와-dispatchersdefault-성능-차이">스레드 100개에서 보이는 <code>Dispatchers.IO</code>와 <code>Dispatchers.Default</code> 성능 차이</h3>
<p>특이한 점은 Kotlin <code>Dispatchers.IO</code>, 스레드 100개에서는 약 13초가 소요되었지만, Kotlin <code>Dispatchers.Default</code>에서는 그보다 1초 적은 약 12초로 충분했다는 것이다. 왜 동일한 Kotlin Coroutines를 사용했고 스레드 개수도 같았는데 이런 차이가 생겼을까?</p>
<p>이는 두 전략의 최대 생성 가능한 스레드 개수가 다르기 때문이다. 이전 게시글에서도 설명했듯이, <code>Dispatchers.IO</code>는 <code>max(64, CPU의 코어 수)</code>개의 스레드를 생성할 수 있지만, <code>Dispatchers.Default</code>는 실제 물리 코어 개수까지만 제한되기 때문이다.</p>
<p>물론 코드에서 나는 무조건 실험 조건에서 지정해 두었던 스레드 개수만큼 스레드 생성을 명령했다. 정확히는 <code>async</code> 블록을 100개 생성하면서 Coroutine을 100개 생성하라고 명령한 것이다. 그러나 <code>Dispatchers.Default</code>는 내가 Coroutine을 100개를 생성하라고 명령해도 스레드를 알아서 내 스마트폰의 코어 개수인 10개로 제한하여 생성하는 것으로 보인다. 그러므로, 아래와 같은 차이가 발생하게 되는 것이다:</p>
<blockquote>
</blockquote>
<ul>
<li><code>Dispatchers.Default</code> | Coroutine 100개, 스레드 10개, 물리 코어 10개</li>
<li><code>Dispatchers.IO</code> | Coroutine 100개, 스레드 64개, 물리 코어 10개</li>
</ul>
<p>Coroutine만 스레드에 적절히 배분해주면 되는 <code>Dispatchers.Default</code>와는 달리, <code>Dispatchers.IO</code>는 Coroutine에 더해 스레드를 어떻게 물리 코어에 배분하면 될지 2중으로 고민을 해야 한다. 그러니 처리 속도가 1초 이상 지연되는 것은 어쩌면 당연한 일일지도 모르겠다.</p>
<p>게다가 <code>Dispatchers.IO</code>가 이렇게 많은 스레드를 생성하는 이유는 IO 작업 시 발생하는 블로킹을 최대한 피하기 위해서이다. 사실 성능이 떨어지는 것은 IO 작업이 아닌 고부하 연산을 요청한 내가 목적에 맞지 않는 작업을 요구해서인 것 같기도 하다.</p>
<h2 id="결론">결론</h2>
<h3 id="kotlin과-c은-취향-차이">Kotlin과 C++은 취향 차이</h3>
<p>물론 C++, 스레드 10개 환경에서 가장 실험 결과가 좋기는 했다. 그러나 Kotlin 환경에서도 최고 기록이 약 10초에서 11초를 웃도는 수준으로 나왔기 때문에, 사실 그렇게 비약적인 차이는 없었다고 볼 수 있다.</p>
<p>둘의 차이라고 한다면, 아무래도 Android 앱의 동작 환경이 Kotlin인 만큼, Kotlin에서 코드를 작성하는 것이 디버그나 스택 추적 등에 훨씬 용이하다는 것이겠다. 실제로 NDK를 사용하는 Android 앱 디버깅을 실행할 경우, Android 런타임 쪽 디버거와 NDK C++ 쪽 디버거 2개가 동시에 동작하기 때문에, 에뮬레이터가 아니라 내 스마트폰에서도 앱 속도가 매우 느렸다.</p>
<p>다만 아무래도 C++ 단에서 모든 것을 처리하는 것이 이론상으로도 - JNI 통신 횟수를 줄일 수 있다 - 그리고 실제 실험 결과로도 - Kotlin에 비해 약 150 ms 우수 - 조금 더 성능이 좋다. 따라서 정말로 극한의 최적화를 원한다면 C++ 단에서 병렬 처리를 진행하면 된다.</p>
<p>정리하자면, 각 환경에서는 아래와 같은 이점이 있고...</p>
<blockquote>
</blockquote>
<ul>
<li>Kotlin에서는 <strong><em>디버깅 용이성</em></strong> 확보 가능</li>
<li>C++에서는 <strong><em>작지만 확실한 성능 우위</em></strong> 확보 가능</li>
</ul>
<p>이 둘은 어느 무엇이 낫다고 확실하게 말하기 어렵기 때문에, 결국 취향 차이라는 결론을 내릴 수 있을 것 같다.</p>
<h3 id="결국-중요한-것은-cs">결국 중요한 것은 CS</h3>
<p>제일 성능이 좋았던 케이스는 C++, Kotlin을 불문하고 스레드 개수를 물리 코어 개수와 동일하게 썼을 때였다. 물리 코어 개수보다 많은 스레드를 생성할 경우 문맥 교환으로 추정되는 추가 비용으로 인해 오히려 성능이 악화되는 결과가 발생했다.</p>
<p>그리고 이러한 내용들은 사실 전부 학부 강의 중 운영체제나 멀티코어컴퓨팅 등에서 병렬 처리 다룰 때 배우는 부분들이다. Kotlin이나 C++ 등의 언어에서 고도로 추상화되어 잘 보이지 않았지만, 정작 까보니 중요한 것은 각 언어의 영향보다는 오히려 근본 CS 지식들이었다는 점을 느낄 수 있었던 실험이었다.</p>
<h1 id="트러블슈팅">트러블슈팅</h1>
<h2 id="ndk에서-c-전역-변수-초기화-시-주의점">NDK에서 C++ 전역 변수 초기화 시 주의점</h2>
<p>트러블슈팅 중 매우 중요한 지식을 하나 얻었다. NDK에서 C++ 코드 작성 시, 전역 변수는 반드시 포인터로 선언하고 초기화를 나중에 해줘야 한다는 것이었다.</p>
<h3 id="tls">TLS</h3>
<p>전역 변수 중에서도 개발자가 성능 최적화를 목적으로 각 스레드가 복사본을 가지라고 특정한 변수들은 보통 스레드 안 TLS(thread local storage)에 저장된다. 여기서 TLS란?</p>
<blockquote>
<p>TLS는 스레드 로컬 저장소(thread local storage)의 약자로, 각 스레드가 공유 자원에 접근할 때 발생하는 부하를 줄이기 위해 공유 자원의 복사본을 저장하는 공간.</p>
</blockquote>
<p>...이다. 이해를 돕기 위해 풀어 설명해보면, 예를 들어 연산을 위해 특정한 전역 변수를 여러 스레드에서 접근해야 할 수 있다. 고전적으로는, 이를 잘 통제하기 위해 <code>mutex</code> 등 동시성 통제 정책을 적용하곤 했다. 그런데 이러한 방식을 적용하면 여러 스레드가 공유 자원 접근하겠다고 줄을 서느라 오히려 작업이 지연되는 현상이 발생한다. 성능 올리겠다고 스레드 여럿 썼는데 오히려 성능이 낮아지는 괴현상이 발생하는 것이다.</p>
<p>이를 방지하기 위해 각 스레드는 TLS라는 공간을 확보해두고, 개발자는 TLS에 저장할 변수에 옵션을 지정하여 런타임이 자동으로 스레드별 복사본을 잘 관리하도록 지시할 수 있다. 그리고 저장 시에 다양한 옵션을 설정할 수 있는데, C++의 OpenMP의 경우 다음과 같은 옵션을 지원한다:</p>
<blockquote>
</blockquote>
<ul>
<li><code>private</code> — 각 스레드가 초기화되지 않은 독립 복사본을 가짐</li>
<li><code>firstprivate</code> — 부모 스레드의 현재 값으로 초기화된 독립 복사본을 가짐</li>
<li><code>lastprivate</code> — 병렬 구간이 끝난 후, 마지막 이터레이션 스레드의 값이 원본에 write-back됨</li>
<li><code>threadprivate</code> — 전역/정적 변수를 TLS화하여 병렬 구간 간에도 값이 유지됨</li>
</ul>
<p>확실하게 짚고 넘어갈 점은, 일부 옵션의 경우 복사 후에는 동기화나 갱신이 되지 않을 수 있다는 점이다. 따라서 TLS를 사용할 생각이라면, 목적과 상황에 따라 적절한 옵션을 지정해주는 것이 매우 중요하다. 그리고 그 판단은 온전히 개발자의 몫이다. 만약 특정 전역 변수를 복사하지 않고 말 그대로 &#39;공유&#39;하여 모든 스레드가 온전히 원본을 참고하도록 하고 싶다면 <code>shared</code> 옵션을 사용할 수 있다. 단, 이 경우에는 모든 스레드가 같은 변수를 공유하므로, <code>mutex</code>나 <code>atomic</code> 등 동기화가 필요할 것이다.</p>
<h3 id="ndk에서-문제가-발생한-이유">NDK에서 문제가 발생한 이유</h3>
<p>NDK에서 일반적인 C++ 전역 변수(Ciphertext)를 포인터 없이 선언했는데 왜 TLS 오류가 났을까?</p>
<p>그 이유는 Gemini의 답변에 따르면, 전역 변수가 메모리에 로드되며 자동으로 실행된 생성자 내부에서 Microsoft SEAL 라이브러리의 <code>MemoryManager</code>를 호출했기 때문이다. 이 매니저가 성능 최적화를 위해 내부적으로 TLS(OpenMP의 <code>threadprivate</code>에 해당하는 것으로 추정되는 <code>thread_local</code>) 공간 할당을 요구했다. 그러나 아직 메인 스레드의 TLS는 준비되는 과정에 있었던 것이다. 라이브러리는 이제 막 메모리에 올라가고 있는 불안정한 타이밍(<code>dlopen</code> 함수가 끝나기도 전)에 동적인 TLS 할당을 시스템에 요구했고, Android OS가 이를 처리하지 못하고 뻗어버린 것이다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>따라서, Android에서 C/C++ 코드를 작성할 때에는, 전역 객체에서는 웬만하면 무거운 초기화는 피하고, JNI 레벨에서 명시적으로 초기화 함수를 호출하여 동적으로 할당 및 초기화해주는 게 권장된다고 한다.</p>
<h2 id="병렬-처리에서-공유-자원에-접근-통제-진행-시-주의점">병렬 처리에서 공유 자원에 접근 통제 진행 시 주의점</h2>
<p>바로 위에서의 얘기와 이어지는 지점이다. 병렬 처리를 구현하면서 공유 자원에 <code>mutex</code> 등 동시성 통제 정책을 적용할 경우, 오히려 병렬 작업이 <code>mutex</code>의 임계 구역 설정으로 인해 순차 작업으로 바뀌어버리는 전혀 의도하지 않은 결과가 발생할 수 있다. 따라서, 병렬 처리를 구현하고 있다면 공유 자원에 무작정 통제 정책을 걸기 보다는 다른 방향의 해결책을 찾아보는 것이 병렬 처리로 얻는 성능적 이점을 극대화하기 좋을 것이다.</p>
<p>이번 실험에서는 공유 자원으로 사용했던 암호화 문맥(context)과 평가기(evaluator) 그리고 인코더(encoder)가 전부 병렬 처리를 위해 최적화되어 있어서 통제 정책을 걸 필요가 없었다. 더 정확히는, 암호화 문맥은 초기화 이후에는 값이 변하지 않기 때문에 읽기에 제한을 둘 필요가 없었다. 인코더와 평가기는 상태 없이 오직 로직만을 저장하고 있었기 때문에 또 동시 접근을 제한할 필요가 없었고. 이런 식으로 현명한 구현을 통해 병렬 처리의 성능을 최대로 끌어올리는 꼼수를 부릴 줄 아는 참된 개발자가 되어야 하겠다.</p>
<h1 id="링크">링크</h1>
<p><a href="https://github.com/i-meant-to-be/cau-spd-consensus-fe">GitHub 저장소</a>에서 모든 코드 확인 가능.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인생 첫 (그리고 마지막) 해커톤 후기]]></title>
            <link>https://velog.io/@i_meant_to_be/my-first-and-last-hackerthon</link>
            <guid>https://velog.io/@i_meant_to_be/my-first-and-last-hackerthon</guid>
            <pubDate>Sun, 08 Feb 2026 12:04:59 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>정말 쉽지 않았다...</p>
<p>중앙대학교를 거점으로 활동하는 것으로 보이는 두 동아리 - GDGoC와 UMC CAU - 가 공동 주최한 청룡톤을 마치고 집으로 돌아왔다.</p>
<p>나름 인상적인 경험이기는 했으나 해커톤은 여러모로 나랑은 맞지 않는 포맷이라는 것을 확실하게 느낀 이틀이었다. 물론 안 좋은 점만 있었던 것은 절대 아니었다. 밤을 새며 프로젝트를 진행하는 경험 자체가 처음이라서 신선하게 느껴졌고, 주최 측도 불편함 전혀 없는 엄청 깔끔한 진행으로 잘 도와주었으며, 함께 한 팀원들도 싸움 없이 각자 맡은 부분을 잘 처리해줘서 너무 고마웠다. 특히 어쩌다 보니 내가 제안한 아이디어가 선택이 되었는데, 도메인 지식이 나에게만 있었어서 내가 기획 등에 있어서는 리드를 조금 해야 했었다. 그래서 수상받지 못했을 때 팀원들에게 미안한 감정도 들었다. (이 글을 읽으실지는 모르겠으나) 다시 한 번 같이 밤을 샜던 팀원들께 감사했었다는 말을 드리고 싶다.</p>
<p>이렇게 깔끔한 진행과 열정적이었던 팀원들과는 별개로, 첫 - 그리고 아마 마지막일 확률이 매우 높은 - 이번 해커톤은 여러모로 좀 생각할 지점이 많은 경험이었다. 간단하게 대충 휘갈겨보자.</p>
<h1 id="본론">본론</h1>
<h2 id="해커톤-참가자는-달콤한-취침의-꿈을-꾸는가">해커톤 참가자는 달콤한 취침의 꿈을 꾸는가?</h2>
<p>우리 팀은 마감을 4시간 남긴 4시 반 쯤 API 개통을 마치고 개발도 어느 정도 정리된 후 바로 휴식을 취했다. 나 역시 바로 동방으로 내려가서 3시간 정도 쪽잠을 자고, 마감 직전 발표 자료만 조금 고쳐서 마무리를 했다. 그런데 다른 팀들은 대부분 그보다 더 늦게까지 개발을 진행한 것 같더라.</p>
<p>사실 팀에서 필수 요구사항에 더해 계획해 두었던 여러 추가 기능들이 꽤 있었다. 그리고 백엔드 고수 팀원의 도움으로 구현을 위해 필요했던 API도 다 뚫어 둔 상태였다. 그러나, 그 기능들은 결국 구현되지 못했다. 아마 생각하기에 몇 시간을 더 썼으면 살을 조금 더 붙일 수 있었을 것이다.</p>
<p>잠을 못 잘 것을 각오하고 왔다면, 그냥 잠을 안 자는 게 낫지 않았을까 하는 생각이 생기는 지점이었다. 핑계를 대자면 패턴이 망가진 관계로 최근 오전 5시에나 취침을 취하던 나로서는 해커톤 참가를 위해 9시에 기상하는 게 쉽지 않았다. 그러므로 해커톤에 참가할 생각이라면, 나처럼 아무 생각 없이 참가하는 것보다는, 정말로 각을 잡고 충분히 수면한 채로 들어가는 게 좋을 것이라 생각한다. 안 그래도 힘든데 잠도 못 자고 오면 머리가 안 돌아갈 것이다.</p>
<h2 id="프론트에-작업이-쏠리는-것에-대해">프론트에 작업이 쏠리는 것에 대해</h2>
<p>디자인도 하고 화면도 프론트가 따야 하니 할 일이 확실히 프론트에 쏠려 있었다. 근데 이건 사실 어떤 프로젝트를 해도 비슷한 것 같다. 아니면 내가 순혈 프론트 인간이라서 이렇게 느끼는 것일 수도 있고... 그렇다기엔 백엔드가 나쁘다고 생각하는 건 아니다. 지금 진행하고 있는 장기 프로젝트도 백엔드 팀원들이 가끔 일 없다고 농담을 던지곤 하는데, 사실 기술적인 사양이나 스펙보다 서비스 자체의 품질이나 기획을 신경 쓰게 되면 어쩔 수 없이 디자인과 프론트에 업무량이 몰릴 수밖에 없는 것 같다.</p>
<p>그리고 기술에만 신경 쓰면 프로젝트가 오래 못 갈 수밖에 없다는 생각도 가끔 한다. 애초에 기술만 고려하게 되면 기획이 빈약하니 사용자도 없을 확률이 높다. 그러니 그런 프로젝트들은 한 철 장사 딸깍 프로젝트로 끝나는 건 어쩌면 당연하다. 사용자 유지가 안 되니까. 반대로 사용자 경험 위주로 접근하게 되면, 사용자가 직접 접촉하는 UI와 프론트에 조금 더 초점이 갈 수밖에 없다. 이 둘 사이 황밸을 유지하는 게 정말 어려운 것 같다.</p>
<h2 id="개발자는-백-몰빵-그리고-프는-웹-몰빵">개발자는 백 몰빵 그리고 프는 웹 몰빵</h2>
<p>모인 사람들 대부분이 소프트웨어학부라 그런지 듣기로는 백엔드 지망이 프론트 지망보다 많았다고 한다. 나랑 같이 작업한 프론트 팀원도 1지망이 백엔드였는데 인원이 너무 많아 프론트로 강제 전직한 케이스. 그러다 보니 &quot;내가 이 분께 React에 대한 짧은 지식을 알려드린다 해도 이 분께서 여기서 얻어갈 게 무엇이 있으실까?&quot;라는 생각을 하지 않을 수 없었다. 어차피 향후 취업 백엔드로 하실 텐데 지금 이 시간이 취업에 어떤 도움이 될까 하는... 뭐 근데 이건 지나친 오지랖일 수도 있겠다는 생각도 있기는 하다.</p>
<p>한편, 여기에 더해 프론트는 대부분 웹 몰빵이었다. 평소에도 주변을 보면 백 하는 사람이 제일 많고, 프론트는 애초에 적은데 웹 하는 사람이 대부분, Android나 iOS 하는 사람은 눈 씻고 봐도 찾아 보기가 어렵다. 그리고 무엇보다도 해커톤에서는 웹이 정말 최적의 폼 팩터라는 생각도 들었다. 일단 모바일 앱은 무겁다. 메모리 관리나 병렬 처리(해커톤에서?) 등 신경 쓸 부분도 웹보다 더 많다. 무엇보다도 웹의 가장 큰 장점은, 애초에 마켓 심사 때문에 배포 자체가 불가능한 모바일과는 달리 웹은 편하게, 빠르게, 비용 없이 배포가 가능하다는 것이다. 접근성도 좋다. 어떤 디바이스 - 모바일이던 랩탑이던 - 에서도 접근이 쉽다. 그러니 그 짧은 시간에 개발 - 배포 - 사용자 유도까지 다 하려면 웹 말고는 사실상 선택지가 없는 셈이다. 아니, 이 정도 장점이면 웹을 안 하는 게 사실 바보처럼 느껴지기도 한다. </p>
<p>아무튼... 그래서 Android가 메인이고 React는 다소 가볍게 만지고 있던 나로서는 울며 겨자 먹기로 웹을 하기는 했는데, 애초에 정성을 다해 웹 개발 경험을 쌓은 것도 아니었기에 한계가 있었다. 게다가 바로 아래에서도 말하겠지만, React 경험이 적었기 때문에 재사용 가능한 디자인 시스템 라이브러리에 대해 알고 있는 게 없었다. 설상가상으로 팀원 한 분은 백 지망인데 프론트로 떨어진 분이라서 내가 리드를 해야 했으니, 여러모로 어려운 경험이었다고 말할 수 있겠다.</p>
<h2 id="해커톤은-실력보다는-전략이다">해커톤은 실력보다는 전략이다</h2>
<p>제일 중요한 부분 같다.</p>
<p>내가 React로 진행하고 있는 프로젝트는 디자이너가 있다. 디자인을 다 해주시기 때문에, 시중에 풀려 있는 Material 같은 디자인 시스템을 사용하지 않는다. 그러다 보니 기존 UI 컴포넌트를 재사용해서 구현하는 것보다는 새로운 컴포넌트를 아예 처음부터 짜는 케이스가 비중이 훨씬 더 높다. 그러나 해커톤 가서 웹 프로젝트 결과물을 보니까 뭔가 시각적으로 비슷한 느낌을 주는 서비스들이 굉장히 많더라. 그래서 옛 기억을 되짚어보니 어떤 디자인 시스템들 같은 경우는 React에서도 사용이 가능하다고 봤던 것이 떠올랐다. Android의 Material 3처럼 말이다. (검색해보니 실제로 MUI라는 이름으로 이미 존재하는 듯하다.) 아마 몇몇 팀은 그러한 디자인 시스템을 사용해서 신속하게 유려한 디자인을 가진 서비스를 개발할 수 있지 않았나 싶다.</p>
<p>결론적으로는 나처럼 맨 땅에 헤딩하며 개발할 시간이 없다는 것이다. 최대한 기존에 만들어진 시스템을 재사용하고, 기존 프로젝트에서 재사용 가능한 로직이 있다면 코드를 적극 재활용하며, 개발 시간을 단축해주는 효율적인 도구와 프레임워크/라이브러리를 잘 찾아내야 하는 것 같다. 단순히 AI에게 모든 걸 맡김으로써 시간을 날먹하라는 것보다는, 그 위 차원에서 시간을 줄일 수 있는 방법을 찾아야만 높은 수준을 달성할 수 있을 것 같다.</p>
<h2 id="데우스-엑스-인공지능">데우스 엑스 인공지능</h2>
<p>인공지능이 컴퓨터 프로그램 개발 과정에서 엄청난 영향력을 행사하고 있다. 그렇기 때문에 자원이 한정된 해커톤에서는 인공지능을 여기저기에 적극 도입하고 잘 활용해야 수준 높은 결과물을 낼 수 있는 것 같다. 괜히 나에게 이 해커톤 추천해주신 분께서 이런 말을 하신 게 아님을 오늘 깨닫게 됐다.</p>
<p>일단 인공지능은 모두가 지금까지 잘 해왔듯 단순 코드 작성에 적극 활용 가능하다. 디자인 할 시간 없고 반복적인 코드 일일이 칠 시간 없다. 다 LLM에게 맡기자. 그리고 기능에도 인공지능을 적극 붙여야 할 것 같다. 오늘 1등한 팀은 AI로 인터넷 전화를 걸어서 응급실 뺑뺑이를 해결한다는 솔루션을 냈다. 대단한 아이디어였다.</p>
<p>디자인 시스템 라이브러리와 마찬가지로, 어떤 분야에서 어떤 인공지능 모델 또는 서비스가 우수한지 미리 알아두는 것도 중요한 지점일 것 같다. 오늘 발표 후 교수님들께서 피드백을 주시면서 특정 서비스는 대학원 내부에서는 평가가 안 좋다는 말씀을 해주시더라. 반대로 특정 도메인에 적절한 인공지능 서비스도 있을 것이다. 이런 것들을 평소의 프로젝트 경험이나 리서칭으로 파악해두고, 해커톤에 나가서 개발에 적극 활용한다면 또 한 번 시간을 크게 줄일 수 있지 않을까.</p>
<h1 id="결론">결론</h1>
<p>시험 기간에 공부 대신 잠을 택하는 나로서는 이렇게 밤 새서 개발하는 건 전혀 상성이 안 맞는 것 같다. 한 번쯤 경험해보는 것은 좋겠으나 잠을 못 자는 건 너무나 큰 고통이었다는 것을 또 다시 깨달았다.</p>
<p>후기는 대충 남겼으니 이제 못 잔 잠을 마저 채우러 가야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스레드, 스레드 풀, Coroutines와 Dispatchers - Kotlin]]></title>
            <link>https://velog.io/@i_meant_to_be/thread-coroutines-dispatchers-kotlin</link>
            <guid>https://velog.io/@i_meant_to_be/thread-coroutines-dispatchers-kotlin</guid>
            <pubDate>Wed, 28 Jan 2026 12:33:04 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>멀티코어 프로세서가 당연해진 시대에서 멀티코어를 활용하지 않는다는 것은 죄악이다. 죄를 지은 자를 회사에서 채용하지 않는 것은 당연한 일이고... 그러므로 현대 개발자라면 당연히 멀티코어를 사용하고 작업을 병렬로 처리하거나 비동기로 빼는 방법을 잘 알고 적용할 수 있어야 할 것이다.</p>
<p>이처럼 현대 개발자에게는 매우 중요한 병렬 처리 개념을 다시 짚어보는 것을 목적으로 하는 이 글에서는, 병렬 처리에서 가장 기초적인 개념인 스레드와 스레드 풀, 그리고 최근 Android의 기반이 되는 Kotlin에서 병렬 처리를 위해 제공하는 유틸리티인 Coroutines 그리고 Dispatchers에 대해 살펴볼 것이다.</p>
<hr>
<h1 id="스레드">스레드</h1>
<h2 id="정의">정의</h2>
<blockquote>
<p>스레드란 프로세스의 2가지 특징인 자원 소유권과 스케줄링/수행 중 후자에 해당하는 단위이다.</p>
</blockquote>
<p>학부 전공 교재에는 위처럼 정의를 내리고 있다. 다소 어렵게 느껴질 수 있다. 그러나, 프로세스가 코드나 전역 변수 등 공유 자원(자원 소유권)에 대한 소유권을 가지고, 프로세스에 속한 스레드들이 해당 공유 자원을 사용하면서 작업을 처리(스케줄링/수행)하는 것을 생각해보면 비교적 이해하기 편할 것이다. 쉽게 말하면, <strong>프로세스 내에서 실행되는 가장 작은 단위의 작업 흐름</strong>인 것이다.</p>
<p>어쨌든 핵심은 프로세스는 하나의 작업을 더 빨리 끝내기 위해 여러 개의 스레드를 두고 이들이 동시에 작업을 병렬로 처리하도록 한다는 것이다. 이처럼 하나의 프로세스 내에서 수행되는 여러 개의 스레드가 동시에 존재하는 경우를 멀티스레딩(multithreading)이라고 한다. 물론 단일 프로세스에 단일 스레드만 존재하는 경우도 가능하기는 하지만, 앞에서 말한 대로 일꾼이 10명 있는데 굳이 1명에게만 일을 시킬 이유는 없다. 따라서 이 글에서는 멀티스레딩 환경을 전제하고 내용을 전개하도록 하겠다.</p>
<h2 id="장점">장점</h2>
<p>스레드가 아무 이유 없이 등장하지는 않았을 것이다. 따라서 장점이 있을 것인데, 그 내용은 다음과 같다:</p>
<ul>
<li>프로세스 생성보다 스레드 생성이 더 가벼움</li>
<li>프로세스 종료보다 스레드 종료가 더 가벼움</li>
<li>프로세스 간 교환보다 스레드 간 교환이 더 가벼움</li>
<li>IPC(Inter-process communication)보다 스레드 간 공유 메모리를 통한 통신 비용이 더 저렴함</li>
</ul>
<p>특히 주목해야 할 것은 3번째 &quot;프로세스 간 교환보다 스레드 간 교환이 더 가벼움&quot;이다. 이게 중요한 이유는 공평한 병렬 처리를 보장하기 위해서 보통의 운영체제는 프로세스 또는 스레드를 계속 바꾸어가며 작업을 실행하기 때문이다. 프로세스 A와 B가 있다고 가정해보자. CPU는 A의 작업을 먼저 처리하고 있었다. 이 때 계속 기다리던 B가 나타나서 &quot;왜 계속 A의 것만 처리해줘? 내 작업은 언제 처리해 줄 거야?&quot;라고 항의한다. CPU는 알았다고 하면서 A의 작업을 잠시 치워 두고 B의 작업을 처리하게 될 것이다. 이를 익숙한 용어로는 문맥 교환(context switching)이라고 한다. 현대 CPU에서는 문맥 교환이 자주 발생하는 만큼, 성능을 올리려면 그 비용을 최대한 줄여야 한다. 그리고 이 목적 달성에 적합한 수단이 바로 스레드인 것이다.</p>
<h2 id="종류">종류</h2>
<p>스레드에는 크게 2가지 종류가 있다. 사용자 수준 스레드(ULT, User-Level Thread)와 커널 수준 스레드(KLT, Kernel-Level Thread)이다.</p>
<h3 id="사용자-수준-스레드-ult">사용자 수준 스레드 (ULT)</h3>
<p>사용자 수준 스레드(이하 ULT)는 스레드와 관련한 모든 작업이 모두 커널 위 응용 수준에서 실행되는 것을 말한다. 여기서 말하는 모든 작업이란 스레드 생성, 유지, 교환 및 파괴 등 스레드의 유지/관리에 관한 모든 일들을 의미한다.</p>
<h3 id="커널-수준-스레드-klt">커널 수준 스레드 (KLT)</h3>
<p>커널 수준 스레드(이하 KLT)는 스레드와 관련한 모든 작업이 모두 커널에서 실행되는 것을 말한다. 커널은 아무래도 프로세서를 직접 다루기 때문에, 사용자는 응용에서 스레드 관련 작업을 처리하지 않으며 대신 운영체제의 API를 호출할 뿐이다.</p>
<h3 id="비교">비교</h3>
<p>ULT와 KLT는 각각의 장단점을 가지고 특성도 다소간 다르다. 이 부분을 집중적으로 살펴보자.</p>
<h4 id="실제-병렬-처리가-이루어지는지에-관해">실제 병렬 처리가 이루어지는지에 관해</h4>
<p>ULT는 병렬 처리가 이루어지지 않을 수도 있으나 KLT는 멀티스레드를 사용하고 있다면 무조건 병렬 처리임이 보장된다. 왜냐하면 ULT는 응용 위에서 스레드를 나누어 둔 것이기 때문이다. 커널은 응용이 스레드를 나눈 사실조차 모를 것이며, 만약 커널이 여러 스레드를 가진 프로세스를 하나의 처리기에 할당한다면, 스레드가 있기는 해도 사실상 단일 프로세서에서의 처리이기 때문에 진정한 병렬 처리라고 볼 수 없을 것이다.</p>
<h4 id="커널-모드와-사용자-모드-간-전환-비용에-관해">커널 모드와 사용자 모드 간 전환 비용에 관해</h4>
<p>ULT는 온전히 응용 위에서만 동작하기 때문에 커널 모드로의 전환이 필요하지 않다. 반면에 KLT는 문맥 교환 등 일부 작업을 위해 커널 모드로 전환해야 하고, 이 과정에서 비용이 발생할 가능성이 있다.</p>
<h4 id="스케줄링-전략에-관해">스케줄링 전략에 관해</h4>
<p>ULT는 프로그램의 특성에 맞는 스케줄링 전략을 취할 수 있다. 선입선출이 필요한 프로그램이 있을 수 있고, 라운드 로빈 방식이 필요한 프로그램이 있을 수 있다. ULT를 사용하는 경우 이처럼 프로그램의 특징에 맞는 최적화된 스케줄링 기법을 채택할 수 있다. 그러나 KLT는 아무래도 저수준인 만큼 사용자가 개입할 여지가 적다.</p>
<h4 id="호환성에-관해">호환성에 관해</h4>
<p>ULT는 응용에서 동작하는 만큼 운영체제와 독립적이다. 코드 하나 짜 두면 어떤 운영체제에서도 손쉽게 실행할 수 있다. 반면에 KLT는 운영체제의 API를 직접 건드려야 하므로, 운영체제마다 병렬 처리 코드를 따로 작성해주어야 한다. 생각만 해도 귀찮다.</p>
<h3 id="ult와-klt의-결합">ULT와 KLT의 결합</h3>
<p>위에서 살펴본 것처럼 ULT와 KLT는 각각의 고유한 장단점을 지닌다. 그래서 일부 똑똑한 선배 개발자들은 이 둘을 합쳐서, 둘의 장점을 최대한으로 활용할 수 있게 개선하고자 시도했다. 그 대표적인 사례가 바로 Kotlin의 Dispatchers이다. 이 부분은 뒤에서 살펴볼 것이므로 여기서는 설명은 생략하고, 아무튼 잘 섞어두면 양쪽의 장점을 최대한 취할 수 있다는 점만 알고 가면 되겠다.</p>
<h2 id="무게">무게</h2>
<p>스레드나 프로세스에 대해 공부하다 보면 가끔 &#39;경량 스레드(lightweight thread)&#39; 등의 표현을 접해볼 수 있다. 그러나 나는 지금까지 &#39;경량&#39;이라는 표현의 의미를 직관적으로만 파악해왔지, 왜 경량이라고 부르는지에 대해서 명확한 이유를 찾아본 적이 없었다는 점을 깨달았다. 그래서 스레드에서 무게가 구체적으로 무엇을 의미하는지, 그리고 경량 스레드와 중량 스레드를 구분하는 기준은 무엇인지에 대해 찾아보았다.</p>
<h3 id="분류">분류</h3>
<p>스레드를 무게에 따라 구분한다면, 크게 아래와 같이 중량 스레드(heavyweight thread)와 경량 스레드(lightweight thread)로 구분할 수 있다:</p>
<ul>
<li><strong>중량 스레드 (heavyweight thread)</strong> | 무거운 스레드를 의미하며, 보통 KLT에 대응됨.</li>
<li><strong>경량 스레드 (lightweight thread)</strong> | 가벼운 스레드를 의미하며, 보통 ULT에 대응됨.</li>
</ul>
<h3 id="비교-1">비교</h3>
<p>스레드를 가벼운 것과 무거운 것으로 나눌 수 있다면, 왜 무거운지에 대해서도 설명이 가능할 것이다. 그러나 KLT와 ULT에 대한 비교는 이미 위에서 자세히 진행했기 때문에, 여기서는 간단하게만 설명하고 넘어가도록 하겠다:</p>
<h4 id="메모리-오버헤드">메모리 오버헤드</h4>
<p>사실 경량과 중량의 가장 큰 차이는 단순하게도 스택의 크기로 설명할 수 있다. 이러한 관점에서 KLT와 ULT를 비교해보면, KLT는 생성 시 메모리의 일부 공간을 직접 할당받게 될 뿐더러 그 크기도 보통 1 MB 정도로 큰 편이지만, ULT는 보통 힙에서 관리되는 경우가 많고 크기도 KLT에 비해서는 작다. 요약하면 ULT가 KLT에 비해 메모리 크기도 작으며 유지/관리도 편하다는 것이다.</p>
<h4 id="문맥-교환-비용">문맥 교환 비용</h4>
<p>KLT는 문맥 교환 시 커널 모드로 전환도 필요하고 하드웨어 수준에서 메모리와 레지스터 및 캐시 교환이 이루어져야 하지만, ULT는 이 모든 것이 이미 할당된 프로세스의 메모리 공간 안에서 이루어지므로 상대적으로 비용이 낮다.</p>
<h4 id="io-처리-시">I/O 처리 시</h4>
<p>KLT는 메모리나 보조기억장치 등에 I/O 요청 시 그대로 블록되어 아무 작업도 수행하지 않는다. ULT는 조금 갈리는데, 전통적인 ULT의 경우에는 여전히 KLT와는 완전히 분리되어 있기 때문에, 프로세스 내부에서 스레드를 나누었다고 하더라도 그 스레드를 가진 프로세스가 I/O 요청을 수행할 경우 어쩔 수 없이 프로세스 전체가 블록되는 것은 동일하다. 반면, Kotlin Coroutines와 같은 현대적인 ULT의 경우, 중단된 작업을 스냅샷의 형태로 따로 빼 두고 그 자리에 다른 작업을 대신 올리기 때문에, 비는 시간 없이 처리기를 최대한으로 활용할 수 있다.</p>
<hr>
<h1 id="스레드-풀">스레드 풀</h1>
<h2 id="정의-1">정의</h2>
<blockquote>
<p>스레드 풀은 재사용 가능한 스레드 여러 개의 집합을 의미한다.</p>
</blockquote>
<p>이 정의는 전에 들었던 멀티코어컴퓨팅 강의 자료에서 가져왔다. 스레드 풀은 여러 스레드로 구성되어 있어서, 사용자가 작업을 스레드에 던져 주면 스레드 풀이 가용한 스레드에 해당 작업을 맡기는 방식이다.</p>
<p>표면적으로 보면 &#39;단순히 스레드 여러 개 모아둔 거에 불과한데 그렇게 큰 성능 향상이 있을까?&#39;라고 생각하기 쉽다. 그러나 우리는 스레드 풀의 스레드가 <strong><em>재사용 가능</em></strong>하다는 점에 주목해야 한다. 스레드를 생성하고 파괴하는 데에는 비용이 필요하다. 그러나 스레드 풀은 한 번 생성해두면 (일반적으로는) 다음 작업을 위해 즉시 파괴되지 않고 유휴(idle) 상태로 남는다. 즉, 스레드 생성과 파괴에 드는 비용이 크게 절약되는 것이다.</p>
<h2 id="특징">특징</h2>
<h3 id="여러-스레드를-동시에-관리">여러 스레드를 동시에 관리</h3>
<p>스레드 풀은 여러 개의 스레드를 동시에 관리한다. 또한 스레드 풀은 보통 스케줄링 등 여러 스레드를 유지하고 작업을 처리하는 데 필요한 일들을 대신 처리해준다. Java의 <code>ExecutorService</code>가 대표적이다. 따라서 사용자 입장에서는 스레드 관리에 필요한 고민을 할 필요가 없다. 작업만 던져주면 된다. 왜? 유지/관리는 유틸리티가 알아서 처리해주니까.</p>
<h3 id="최대-스레드-개수를-제한할-수-있음">최대 스레드 개수를 제한할 수 있음</h3>
<p>단순히 여러 스레드를 무한히 생성할 경우 종국에는 메모리 공간이 꽉 차서 메모리 오버플로우가 발생할 수 있다. 이를 방지하기 위해, 개발자는 필요에 따라 최대로 생성 가능한 스레드 수를 제한할 수 있다.</p>
<h3 id="여러-작업을-큐에서-관리">여러 작업을 큐에서 관리</h3>
<p>만약 스레드 풀의 모든 스레드가 전부 작업하고 있을 경우, 스레드 풀은 새로 제출된 작업을 작업 큐에 담아둔다. 이후 작업을 마친 스레드 하나가 유휴 상태로 전환되면, 큐 관리 정책에 따라 처리해야 할 작업을 선별하여 꺼내서 가용한 스레드에 하나씩 배분해준다.</p>
<h2 id="장점과-단점">장점과 단점</h2>
<p>정리하면 스레드 풀은 아래와 같은 장점을 가진다:</p>
<ul>
<li>스레드 풀 생성 시 여러 스레드를 만들어 두고 계속 재사용하므로, 스레드 생성과 파괴에 드는 비용이 적다.</li>
<li>스레드 유지/관리를 대신 해 주기 때문에, 사용자의 책임이 확실히 줄어든다.</li>
</ul>
<p>그러나 동시에 아래와 같은 단점도 지닌다:</p>
<ul>
<li>만약 스레드 풀의 모든 스레드에 이어 작업 큐까지 꽉 차 버리면, 새로 들어오는 작업을 어떻게 처리할 지에 관한 정책을 수립해야 한다.</li>
<li>스레드 풀을 사용한다고 하더라도, 여전히 I/O 작업 요청 시 스레드가 중지(blocking)되는 것은 막을 수 없다. 다시 말해, 스레드 생성 비용은 아껴줄 수 있어도, 스레드가 노는 시간까지 줄여주지는 못한다는 것이다.</li>
</ul>
<hr>
<h1 id="coroutines">Coroutines</h1>
<h2 id="정의-2">정의</h2>
<blockquote>
<p>Coroutines are lightweight alternatives to threads.</p>
</blockquote>
<p>Coroutines는 Android 개발에 주로 사용되는 언어 Kotlin에서 지원하는 병렬 프로그래밍 도구이다. 공식 문서에 다양한 설명과 정의가 혼재하고 있으나, 지금까지 글에서 스레드에 대해 주로 다루어왔기에, 스레드와 밀접한 정의를 하나 가져왔다. 그렇다면 이제 각 표현을 자세히 살피며 해체 분석을 해 보자.</p>
<h3 id="lightweight">&quot;lightweight&quot;</h3>
<p>이 표현은 &#39;경량&#39;으로 번역이 가능하겠다. 위에서 설명한 대로 경량 스레드, 즉 ULT를 의미하는 것으로 볼 수 있다. 다시 말해, Coroutines는 ULT에 기반한 동시성(concurrency) 처리를 지원해주는 도구라는 의미이다.</p>
<h3 id="alternatives-to-threads">&quot;alternatives to threads&quot;</h3>
<p>이 표현은 &quot;스레드에 대한 대안&quot;으로 번역이 가능하겠다. 가벼운 의미에서는 맞는 말이라고 본다. Coroutines를 사용하게 된다면, 기존에 주로 사용하던 Java의 <code>Thread</code>나 <code>Runnable</code>을 직접 다룰 필요가 없어지기 때문이다. 이러한 (상대적) 저수준 도구를 직접 사용하는 대신, 어느 정도 추상화가 되어 있는 Coroutines를 사용하는 게 편할 것이라는 의미에서 대안이라는 단어를 사용한 것 같다.</p>
<p>그러나 조금 더 깊게 들어가보면 사실 &#39;스레드 - 엄밀히는 여기서 말하는 스레드란 KLT일 것이다 - 에 대한 대안&#39;보다는 &#39;스레드와 함께 사용할 수 있다&#39;는 표현이 더 정확하다고 볼 수 있다. 왜냐하면 ULT인 Coroutines는 내부적으로 KLT를 사용하기 때문이다. 그래서 Coroutines를 더 정확히 표현하자면 &quot;KLT를 더 효율적으로 사용할 수 있게 해주는 사용자 수준(user-level)의 스레드 관리 도구&quot;로 소개할 수 있을 거라고도 생각이 든다.</p>
<h2 id="특징-1">특징</h2>
<h3 id="스레드의-형태를-취함">스레드의 형태를 취함</h3>
<p>Coroutines는 내부적으로 <code>Continuation</code>라고 하는 일종의 문맥 데이터를 가지고 있다. 또한, Coroutine에서 다른 Coroutine으로 전환할 경우 이 <code>Continuation</code> 문맥을 저장해 두고 추후 복구에 활용한다는 점에서, 각 스레드마다 지역 변수 등을 저장하는 별도 공간이 마련되어 있는 KLT와 유사한 방식으로 동작한다고 볼 수 있다.</p>
<h3 id="비선점형-스레드-전략을-취함">비선점형 스레드 전략을 취함</h3>
<p>스레드와 프로세스 전략에는 크게 선점형 전략과 비선점형 전략이 있다:</p>
<ul>
<li>선점형 (preemptive) | 프로세스나 스레드가 현재 프로세서에 올라가 있는 작업을 강제로 멈추고 바꿀 수 있는 경우.</li>
<li>비선점형 (non-preemptive) | 프로세스나 스레드 스스로가 중단을 요청하기 전까지는 외부에서 작업을 멈출 수 없는 경우.</li>
</ul>
<p>Coroutines는 비선점형 전략을 취하기 때문에, 외부 Coroutines에서는 지금 동작하고 있는 Coroutines를 억지로 멈출 수 없다. 이는 어쨌든 문맥 교환의 횟수를 줄여주기 때문에, 그 비용을 조금이나마 줄여줄 수 있다는 점에서 효율적이다.</p>
<h3 id="klt-위에서-동작하는-ult의-형태임">KLT 위에서 동작하는 ULT의 형태임</h3>
<p>Coroutines 공식 문서에 따르면, Coroutines는 운영체제에 의해 관리되는 스레드 위에서 동작한다고 설명되어 있다. 즉, 위에서 말했던 KLT와 ULT의 혼합형인 것이다. Coroutines라는 ULT는 기본적으로 응용 수준의 스케줄러와 디스패처에 의해 관리되며, 이 스케줄러와 디스패처는 필요에 따라 ULT에 해당하는 Coroutine을 실제 물리 스레드인 KLT에 할당하는 식이다. 따라서, KLT와 ULT의 장점을 모두 취하는 형태라고 볼 수 있다.</p>
<h3 id="작업이-중지block되지-않고-연기suspend됨">작업이 중지(block)되지 않고 연기(suspend)됨</h3>
<p>위에서 설명했듯 KLT는 I/O 요청 시 그대로 작업이 끝나기 전까지 중지한다. 즉, 그 대기 시간 동안에는 아무 일도 하지 않는다는 것이다. 반면에 Coroutines 스케줄러는 특정 Coroutine이 I/O 등의 사유로 중지될 경우, 해당 Coroutine의 진행 상황을 저장한다. 그리고 기존 Coroutine을 그것이 실행되던 KLT에서 즉시 내리고, 작업을 기다리고 있는 다른 Coroutine을 대신 올린다. 즉, 하드웨어를 비는 시간 없이 최대한 사용할 수 있다는 것이다. 그래서 Coroutines 위에서 돌아가는 함수는 <code>suspend</code>라는 키워드를 붙여야 한다.</p>
<p>조금 더 부연 설명을 하자면, Coroutines이 중지될 때 그 작업 맥락을 <code>Continuation</code>의 형태로 저장하게 된다. <code>Continuation</code>은 중단 후 다음에 실행할 코드 위치와 현재 지역 변수의 상태를 담고 있는 자료구조인데, <code>suspend</code> 함수 컴파일 시에 함수 파라미터 맨 뒤에 이 <code>Continuation</code> 객체가 덧붙여지게 된다. 이처럼 현재 상태를 <code>Continuation</code>의 형태로 <code>suspend</code> 함수에 전달해준다는 점에서 이러한 방식을 CPS(Continuation-Passing Style)라고 부른다.</p>
<h3 id="동시성-제공-병렬성은-조건이-충족된다면-제공">동시성 제공, 병렬성은 조건이 충족된다면 제공</h3>
<p>이 부분을 설명하기 위해서는 동시성과 병렬성의 차이에 관해 알아야 할 필요가 있다. 찾아보니까 면접 단골 질문 주제라는 것 같더라. 간단히 설명하면 아래와 같다:</p>
<ul>
<li>동시성 (concurrency) | 논리적으로 여러 작업이 동시에 처리되는 것처럼 보이게 하는 것.</li>
<li>병렬성 (parallelism) | 여러 작업을 물리 프로세서에 실제로 나누어 동시에(simultaneously) 처리하는 것.</li>
</ul>
<p>Coroutines는 기본적으로 하나의 KLT에 여러 개의 Coroutine를 대체하면서 올릴 수 있게 되어 있다. 아까 설명한 것처럼 <code>suspend</code>된 작업을 기다리는 것보다는, 그 시간에 놀고 있는 작업을 올려주는 게 효율적이기 때문이다. 그래서 논리적으로 동시에 처리되는 것처럼 보이는 동시성을 만족하게 되는 것이다.</p>
<p>반면 병렬성의 경우에는 동작 환경이 싱글코어라면 달성할 수 없지만, 멀티코어라면 자연스럽게 달성이 된다. 그래서 조건이 충족되어야 한다고 언급한 것이다.</p>
<hr>
<h1 id="dispatchers">Dispatchers</h1>
<h2 id="배경">배경</h2>
<p>지금까지의 설명으로 스레드가 무엇이고, 스레드 풀은 무엇이며, Kotlin의 Coroutines가 어떻게 스레드 대신 사용 가능한 효율적인 동시성 구현 도구를 제공하는지에 대해 설명했다. 그런데 단 한 가지, 빼먹은 설명이 있다. 다시 한 번 내가 부연한 정의를 살펴보자:</p>
<blockquote>
<p>&quot;Coroutines는 KLT를 <strong>더 효율적으로 사용할 수 있게</strong> 해주는 사용자 수준(user-level)의 스레드 관리 도구&quot;</p>
</blockquote>
<p>여기서 주목할 표현은 &#39;더 효율적으로 사용할 수 있게&#39;이다. 방법론이 빠져 있지 않은가? 어떻게 효율적으로 관리할지에 대한 답이 없다. 그 답이 바로 Dispatchers이다.</p>
<h2 id="정의-3">정의</h2>
<blockquote>
<p>The coroutine context includes a coroutine dispatcher that determines what thread or threads the corresponding coroutine uses for its execution.</p>
</blockquote>
<h3 id="the-coroutine-context-includes-a-coroutine-dispatcher">&quot;The coroutine context includes a coroutine dispatcher&quot;</h3>
<p>정의를 살펴보면, 가장 먼저 &quot;Coroutine 문맥은 Dispatcher를 포함한다&quot;고 설명하고 있다. 이 말은 아래 코드를 보면 쉽게 이해가 가능하다:</p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    launch(Dispatchers.IO) { // &lt;- 여기가 중요하다
        println(&quot;IO 작업 시작: ${Thread.currentThread().name}&quot;)
        delay(1000L)
        println(&quot;IO 작업 완료: ${Thread.currentThread().name}&quot;)
    }
}</code></pre>
<p>Kotlin에서는 Coroutine 사용 시에 필요한 경우 Dispatchers를 명시해줄 수 있다. Dispatchers에는 여러 종류가 있는데, 상황에 맞는 Dispatcher를 적절한 Coroutine 문맥에 지정해 작업을 처리할 수 있다는 말이다.</p>
<h3 id="a-coroutine-dispatcher-that-determines-what-thread-or-threads-the-corresponding-coroutine-uses-for-its-execution">&quot;a coroutine dispatcher that determines what thread or threads the corresponding coroutine uses for its execution.&quot;</h3>
<p>다음으로는, &quot;Coroutine dispatcher는 각각의 Coroutine이 자신의 수행을 위해 어떤 스레드 혹은 스레드들을 사용할지 결정한다&quot;고 설명하는 부분이다. 즉, Dispatchers는 스레드처럼 명확히 실재하는 어떤 개념이 아니라, 그저 스레드 풀을 관리하고 작업을 효율적으로 수행하기 위한 일종의 전략을 의미한다는 것이 이 표현에서 잘 드러나 있다.</p>
<h2 id="동작-방식">동작 방식</h2>
<h3 id="mn-모델을-취함">M:N 모델을 취함</h3>
<p>Dispatchers는 Kotlin의 Coroutines(ULT)를 실제 스레드(KLT)에 배분하는 스케줄러 역할을 담당한다. 이 때 다수의 ULT를 다수의 KLT에 배분하는 구조가 성립하므로, 스레드 모델 중에서는 M:N 모델에 해당한다.</p>
<h3 id="모든-dispatchers가-스레드-풀-공유">모든 Dispatchers가 스레드 풀 공유</h3>
<p>기본적으로 모든 Dispatchers는 동일한 스레드 풀을 공유한다. 즉, <code>Dispatchers.Default</code>와 <code>Dispatchers.IO</code>가 완전히 동일한 스레드를 서로 차례를 바꾸어가며 쓸 수 있다는 말이다.</p>
<h3 id="경우에-따라-문맥-전환을-생략할-수-있음">경우에 따라 문맥 전환을 생략할 수 있음</h3>
<p>이는 바로 위의 특징에 의해 발생하는 장점이다.</p>
<p>예를 들어, <code>Dispatchers.Default</code>에서 연산을 진행하다가 파일에 있는 값이 필요해서 <code>Dispatchers.IO</code>로 전환했다고 가정해보자. 이 상황에서는 <code>Dispatchers.Default</code>에서 돌던 작업을 <code>Dispatchers.IO</code>로 문맥 전환해줘야 할 것이다. 그러나 이 경우에서는 작업의 내용과 내부 Coroutine 문맥은 Dispatcher를 바꾸기 전이나 후나 완전히 동일하다. 그러니까 그냥 이 작업을 실행하던 스레드의 라벨만 <code>Dispatchers.Default</code>에서 <code>Dispatchers.IO</code>로 바꿔주면 되는 것이다. 그 외의 다른 문맥 전환에 필요한 작업은 전혀 필요하지 않다.</p>
<p>이러한 원리로 Coroutine은 신속하고 효율적인 문맥 전환을 달성할 수 있게 되는 것이다.</p>
<h2 id="종류-1">종류</h2>
<p>Dispatchers에는 다양한 형태가 있는데, 그 목록은 다음과 같다:</p>
<ul>
<li><code>Dispatchers.Default</code></li>
<li><code>Dispatchers.IO</code></li>
<li><code>Dispatchers.Main</code></li>
<li><code>Dispatchers.Unconfined</code></li>
</ul>
<h3 id="dispatchersdefault"><code>Dispatchers.Default</code></h3>
<p>기본값으로 사용되는 Dispatcher이다. Coroutine 생성 시 별도의 Dispatcher가 명시되지 않으면 얘를 사용하게 된다. 백그라운드에서 실행되며, 공식 문서의 설명으로는 연산 집약적인 작업에 적절하다고 한다. 기본적으로, 이 Dispatcher에서 사용 가능한 최대 스레드 개수는 보통 CPU의 코어 개수와 동일하며, 최소값은 2이다.</p>
<h3 id="dispatchersio"><code>Dispatchers.IO</code></h3>
<p>I/O 작업에 사용되는 Dispatcher이다. <code>Dispatchers.Default</code>와 마찬가지로 백그라운드에서 실행되며, 블로킹(blocking)을 동반하는 I/O 집약적인 작업에 적절하다. 필요에 따라 생성되는 스레드를 사용하기는 하나, 64개 또는 CPU의 총 코어 수 중 큰 값으로 제한된다.</p>
<h3 id="dispatchersmain"><code>Dispatchers.Main</code></h3>
<p>UI 렌더링에 사용되는 Dispatcher이다. Android에서는 화면을 그리는 작업이 이 스레드에서 실행된다. 다르게 말하면, 만약 오래 걸리거나 블로킹(blocking)이 동반되는 작업을 <code>Dispatchers.Main</code>에서 실행한다면, 화면이 멈춘다는 말이다. 그러므로 반드시 이러한 작업들은 백그라운드에서 실행되는 Dispatcher에 맡기도록 하자.</p>
<h3 id="dispatchersunconfined"><code>Dispatchers.Unconfined</code></h3>
<p>&#39;confine&#39;이라는 단어는 Oxford 사전에 따르면 &#39;to limit an activity, person, or problem in some way&#39;를 의미한다. 즉, (특정한 무언가에) 국한한다는 뜻이다. 이 뜻에 따르면, <code>Dispatchers.Unconfined</code>는 특정 스레드에 국한되지 않고 어느 스레드에서든 실행이 가능하도록 하는 Dispatcher를 의미한다.</p>
<h4 id="동작-방식-1">동작 방식</h4>
<p>이 Dispatcher는 다음과 같이 동작한다:</p>
<ul>
<li>최초 실행 시에는 이 Coroutine을 실행한 현재 호출 프레임에서 즉시 실행된다.</li>
<li>만약 <code>delay</code> 등의 함수로 인해 중단이 발생할 경우, 해당 중단 함수가 작업을 마친 스레드 - 다시 말해 중단된 함수를 깨워준 스레드 - 에서 계속 작업을 이어간다.</li>
<li>중첩된 <code>Dispatchers.Unconfined</code> 호출은 스택 오버플로우를 막기 위해 내부 큐(event loop)에 작업을 넣어 실행한다.</li>
</ul>
<h4 id="사용-이유">사용 이유</h4>
<p>일반적인 비동기 작업 호출 시에는, 비동기 작업 처리를 위한 스레드를 생성하거나 할당해준 후에야 해당 작업을 수행하기 시작한다. 그러나 <code>Dispatchers.Unconfined</code>가 지정된 작업에 한해서는 별도 스레드 생성 없이 즉시 작업을 시작한다. 다시 말하면 일반적인 경우보다도 더 신속하고 빠르게 작업 처리가 가능할 &#39;가능성&#39;이 있다는 뜻이다.</p>
<h4 id="문제">문제</h4>
<p>그러나 이 전략의 가장 큰 문제점은 스택 오버플로우가 발생할 수 있다는 점이다. 예를 들어, 아래와 같은 코드가 있다고 치자:</p>
<pre><code class="language-kotlin">launch(Dispatchers.Unconfined) { // &lt;- 1번
    launch(Dispatchers.Unconfined) { // &lt;- 2번
        launch(Dispatchers.Unconfined) { // &lt;- 3번
            // ...
        }
    }
}</code></pre>
<p>일단 기본적으로 현재 이 비동기 작업을 호출한 문맥이 존재할 것이다. 그리고 이 문맥에서 사용 가능한 메모리 공간에는 하드웨어나 Kotlin 정책 상의 제약으로 인해 한계(보통 1 MB)가 있을 것이다. 자, 그렇다면 이 코드가 어떻게 스택 오버플로우를 일으키는지 순서대로 따라가보자:</p>
<ul>
<li>1번 <code>launch</code> 함수가 실행 요청됨.</li>
<li>Dispatcher가 <code>Dispatchers.Unconfined</code>이므로 별도 문맥 전환 없이 현재 문맥에서 호출.</li>
<li>현재 문맥의 스택 프레임에 1번 <code>launch</code> 함수에 필요한 스택 프레임 생성.</li>
<li>2번 <code>launch</code> 함수와 3번 <code>launch</code> 함수에서도 동일한 일이 발생함.</li>
<li>계속 반복되면서 현재 스택 프레임에 계속해서 다른 스택 프레임이 쌓임.</li>
<li>이러다가 제한된 크기를 넘어서면 스택 오버플로우 발생...</li>
</ul>
<p>따라서 Kotlin은 이러한 문제를 예방하기 위해, 중첩된 <code>Dispatchers.Unconfined</code> 호출을 확인했을 경우 해당 작업들을 곧바로 실행하는 게 아니라 큐에 넣어둔 채로 보관함으로써 스택 오버플로우를 예방한다. 이 방식을 트램펄린(trampoline) 기법이라고 부른다던데, 이에 대한 Gemini의 대략적인 설명은 아래와 같다:</p>
<ul>
<li>가장 바깥쪽 Unconfined 코루틴이 실행될 때, Thread-Local에 &quot;나 지금 Unconfined 실행 중이야&quot;라고 깃발을 꽂습니다.</li>
<li>내부에서 또 launch(Unconfined)가 호출되면, 깃발을 확인합니다.</li>
<li>&quot;어? 이미 실행 중이네? 재귀 호출하면 위험해.&quot; -&gt; 즉시 실행하지 않고, 작업을 내부 큐(Queue)에 넣고 함수를 바로 리턴해버립니다. (스택 해제)</li>
<li>바깥쪽 코루틴이 자기 할 일을 다 마치면, 큐를 확인해서 쌓여있던 내부 작업들을 하나씩 꺼내 실행합니다.</li>
</ul>
<h4 id="다른-방법-coroutinestartundispatched">다른 방법: <code>CoroutineStart.UNDISPATCHED</code></h4>
<p>비슷하게, 호출 즉시 현재 스택 프레임에서 작업을 시작할 수 있는 방법으로는 <code>CoroutineStart.UNDISPATCHED</code> 옵션이 있다. 사용 방법은 아래와 같다:</p>
<pre><code class="language-kotlin">launch(
    context = Dispatchers.Default,
    start = CoroutineStart.UNDISPATCHED
) {
    // 작업 진행...
}</code></pre>
<p>이 경우 첫 실행은 <code>Dispatchers.Unconfined</code>와 동일하게 비동기 함수를 호출한 스택 프레임에서 즉시 실행한다. 다음 중단점을 만나기 전까지는. 차이점이 있다면 중단이 풀려도 아무 스레드에서나 동작하는 <code>Dispatchers.Unconfined</code>와는 달리, <code>CoroutineStart.UNDISPATCHED</code>는 중단이 풀린 후에는 지정된 Dispatcher 정책에 따라 동작한다는 점이다. 따라서 빠른 실행 후에는 어느 정도 여유 있게 처리해도 되는 UI 초기화 등에 이 옵션을 사용할 수 있다.</p>
<h2 id="사용자-지정-dispatcher-사용">사용자 지정 Dispatcher 사용</h2>
<p>Dispatchers에서는 범용적으로 사용 가능한 위의 4가지 - 사실상 사용하기 어려운 <code>Dispatchers.Unconfined</code>를 뺀다면 3가지이긴 하지만 - 옵션을 제공한다. 그러나 경우에 따라 위의 기본 옵션 말고 사용자가 원하는 Dispatcher 정책을 수립하여 사용하고 싶을 수도 있다. 예를 들어, 동형 암호(FHE)의 곱셈 연산처럼 매우 큰 부하의 연산이 오랜 시간 지속되는 경우에는 <code>Dispatchers.Default</code>로는 해결이 어렵다. 만약 FHE 곱셈 연산을 진행하던 중에 금방 끝나는 JSON 파싱 요청이 <code>Dispatchers.IO</code>로 들어온다고 해도, 모든 Dispatchers가 하나의 스레드 풀을 공유하는 Dispatchers의 특성상 모든 스레드를 이미 다 <code>Dispatchers.Default</code>에서 사용하고 있기 때문에 JSON 파싱을 요청받은 <code>Dispatchers.IO</code>에 돌아갈 스레드가 없기 때문이다. 기아(starvation) 현상의 발생이다.</p>
<p>따라서 Dispatchers는 이를 위해 아래의 3가지 수단을 제공한다:</p>
<ul>
<li>단일 스레드 격리 | <code>newSingleThreadContext(&quot;identifier&quot;)</code> 함수 사용</li>
<li>스레드 풀 격리 | <code>newFixedThreadPoolContext(THREAD_NUMBER, &quot;identifier&quot;)</code> 함수 사용</li>
<li>기존 Dispatchers의 스레드 풀 재사용 | <code>Dispatchers.IO.limitedParallelism(THREAD_NUMBER)</code> 함수 사용</li>
</ul>
<p>필요에 따라 3가지 함수 중 하나를 사용하면 된다. 그러나, 각각의 스레드의 생성 및 종료가 관리되는 스레드 풀과는 달리, <code>newSingleThreadContext</code>를 통해 생성한 단일 스레드는 작업 종료 후 반드시 명시적으로 해제해 주어야 함을 기억하자. 또한, 마지막 방법인 기존 Dispatchers의 스레드 풀을 재사용하는 전략은 새로운 스레드를 만들지 않고 기존 스레드를 재활용한다는 점에서 경제적일 수는 있으나, 여전히 Dispatchers의 한계 이상의 작업을 요구하는 경우 대처하기 어려울 수 있음을 알고 있어야 할 것이다. (물론 그 정도로 부하가 큰 작업을 Android 단말기에서 실행할 일이 실질적으로 있을지는 잘 모르겠다.)</p>
<hr>
<h1 id="결론">결론</h1>
<p>이 글을 통해 나는 병렬 처리의 최소 단위인 스레드와 스레드 풀에 대해 알아보았으며, 이 개념을 Kotlin에서 구현해 낸 Coroutines와 이를 유지/관리하기 위한 전략인 Dispatchers가 어떻게 동작하는지에 대해 간략히 알아보았다. 장장 6시간 정도 이 글에 시간을 쏟았으니, 아마 이 부분 관련해서는 까먹을 일은 없을 것 같다.</p>
<p>뿌듯한 하루다. 굿.</p>
<hr>
<h1 id="참고-문헌">참고 문헌</h1>
<ul>
<li>William Stallings. (2015). <em>『운영체제: 내부구조 및 설계 원리』</em> (제8판) (조유근 감수, 전광일 등 13인 역). 프로텍미디어.</li>
<li>Kotlin. (2025). <em>『Coroutines』</em>. Kotlin 공식 문서. <a href="https://kotlinlang.org/docs/coroutines-overview.html">https://kotlinlang.org/docs/coroutines-overview.html</a>.</li>
<li>Kotlin. (2025). <em>『Coroutines basics』</em>. Kotlin 공식 문서. <a href="https://kotlinlang.org/docs/coroutines-basics.html">https://kotlinlang.org/docs/coroutines-basics.html</a>.</li>
<li>Kotlin. (2025). <em>『Coroutine context and dispatchers』</em>. Kotlin 공식 문서. <a href="https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html">https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html</a>.</li>
<li>Kotlin. (2025). <em>『CoroutineDispatcher』</em>. Kotlin 공식 문서. <a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/">https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/</a>.</li>
<li>Wikipedia. (2026). <em>『Parallel computing』</em>. Wikipedia. <a href="https://en.wikipedia.org/wiki/Parallel_computing">https://en.wikipedia.org/wiki/Parallel_computing</a>.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android 16 KB 페이징 정책과 C/C++ 빌드 - Android]]></title>
            <link>https://velog.io/@i_meant_to_be/Android-16-KB-Paging-And-Building-C-Binaries</link>
            <guid>https://velog.io/@i_meant_to_be/Android-16-KB-Paging-And-Building-C-Binaries</guid>
            <pubDate>Sun, 23 Nov 2025 13:10:00 GMT</pubDate>
            <description><![CDATA[<h1 id="배경">배경</h1>
<p>현재 C++ 바이너리가 포함되는 Android 앱을 하나 개발하고 있는데, 빌드 후에 갑자기 평소에 보이지 않던 경고가 발생했다. 그 경고의 원인을 따라 가보니 다음 링크를 찾을 수 있었다: <a href="https://developer.android.com/guide/practices/page-sizes">Support 16 KB page sizes - Android Developers</a></p>
<p>이 공지에서는 <strong><em>2025년 11월 1일부터 Play 스토어에 제출되는 64 비트 디바이스 대상 모든 앱은 반드시 16 KB 페이지 크기를 지원해야 한다</em></strong>고 안내하고 있다. 대충 간단히 읽어보니 원래는 4 KB가 최적의 크기였으나, 현재는 디바이스의 평균 성능이 올라가면서 16 KB로 변경하게 되었다는 것 같더라. 근데 페이징 관련하여 운영체제 강의에서 배웠던 내용들이 기억이 잘 안 나더라. 그래서 리마인드차 간단하게 관련 게시글을 작성해보고자 한다.</p>
<h1 id="페이징">페이징</h1>
<h2 id="정의">정의</h2>
<p>페이징(paging)은 메모리를 잘게 쪼개는 것을 말한다. 조금 더 전공적(?)으로는 <strong><em>프로세스의 논리 메모리를 같은 크기의 페이지(page)로 나누고, 이를 물리 메모리의 프레임(frame)에 매핑하여 연속되지 않은 물리 메모리를 연속된 공간처럼 사용할 수 있게 하는 메모리 관리 기법</em></strong>이다.</p>
<h2 id="목적">목적</h2>
<p>메모리 파편화를 방지하고, 메모리를 효율적으로 사용하기 위해서이다. 페이징을 하지 않았을 때 가능한 한 가지 상황을 가정해보자:</p>
<ul>
<li>○○○○○○○○○○ 내 메모리는 총 10 KB이다. </li>
<li>●●●●●●○○○○ 앱 A를 실행시켰고 얘는 메모리를 6 KB 먹는다. </li>
<li>●●●●●●◆◆○○ 앱 B를 실행시켰고 얘는 메모리를 2 KB 먹는다. </li>
<li>○○○○○○◆◆○○ 앱 A를 종료했다. </li>
<li>■■■■○○◆◆○○ 앱 C를 실행시켰고 얘는 메모리를 4 KB 먹는다. </li>
</ul>
<p>이 상황에서 메모리를 4 KB 먹는 앱 D를 실행시키려고 하면, 가능할까? 답은 <strong><em>불가능</em></strong>이다. 왜냐하면 칸의 개수만 따지면 4 KB 남긴 했지만, 공간이 파편화되어 있기 때문에 <strong><em>&#39;연속적인&#39;</em></strong> 4 KB를 할당하는 것은 불가능하기 때문이다. 페이징 정책이 없는 OS는 부팅 후 시간이 지남에 따라 새 프로세스가 실행되고 종료되는 과정을 반복하면서 파편화 정도가 더욱 더 심각해진다. 종국에는 아무 프로세스도 실행할 수 없는 상태에 이르게 된다.</p>
<p>이러한 메모리 파편화를 해결하기 위한 방법이 바로 페이징 정책이다.</p>
<h2 id="구성-요소">구성 요소</h2>
<p>페이징에 대해 논하기 위해서는 프레임(frame), 페이지(page) 그리고 페이지 테이블(page table)에 대해 알아봐야 한다.</p>
<h3 id="페이지-page">페이지 (page)</h3>
<p>프로세스의 논리 주소 공간을 고정된 크기로 나눈 블록을 의미한다.</p>
<h3 id="프레임-frame">프레임 (frame)</h3>
<p>물리 메모리를 페이지와 같은 단위로 나눈 것을 의미한다.</p>
<h3 id="페이지-테이블-page-table">페이지 테이블 (page table)</h3>
<p>논리 메모리인 페이지를 물리 메모리의 프레임과 매핑하는 표이다. 이게 핵심이다.</p>
<h2 id="동작-방식">동작 방식</h2>
<p>앞의 상황에서 앱 D를 실행하지 못했던 이유는 페이징을 하지 않는 OS에서는 반드시 메모리가 연속적으로 할당되어야 하기 때문이다. 그러나 페이징 정책을 사용하는 OS에서는 반드시 연속적으로 메모리를 할당할 필요가 없다. 분리되어 있어도 된다.</p>
<table>
<thead>
<tr>
<th>인덱스</th>
<th>프로세스 A의 메모리 (페이지)</th>
<th>페이지 테이블</th>
<th>물리 메모리 공간 (프레임)</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>0번 페이지</td>
<td>0번 페이지-4번 프레임</td>
<td>0번 프레임 <em>(비어 있음)</em></td>
</tr>
<tr>
<td>1</td>
<td>1번 페이지</td>
<td>1번 페이지-3번 프레임</td>
<td>1번 프레임 (2번 페이지 저장)</td>
</tr>
<tr>
<td>2</td>
<td>2번 페이지</td>
<td>2번 페이지-1번 프레임</td>
<td>2번 프레임 <em>(비어 있음)</em></td>
</tr>
<tr>
<td>3</td>
<td>-</td>
<td>-</td>
<td>3번 프레임 (1번 페이지 저장)</td>
</tr>
<tr>
<td>4</td>
<td>-</td>
<td>-</td>
<td>4번 프레임 (0번 페이지 저장)</td>
</tr>
</tbody></table>
<p>위 표처럼 페이지 테이블은 각 페이지와 실제 프레임 번호를 매핑함으로써, 논리 메모리가 분할된 채로 물리 메모리에 저장될 수 있게 해준다. 이 경우에서는 메모리 2칸을 요구하는 프로세스를 비어 있는 0번 프레임과 2번 프레임에 나누어 배치할 수 있으므로, 메모리 파편화를 어느 정도는 방지하고 더 효율적으로 메모리를 사용할 수 있게 된다.</p>
<h2 id="단점">단점</h2>
<p>물론 이 방식도 아래와 같은 단점이 있다:</p>
<ul>
<li>별도의 페이징 테이블을 보관할 수 있는 <strong><em>저장 공간이 필요</em></strong>하다.</li>
<li>메모리를 참고하기 위해선 베이스 레지스터에 인덱스만 더하면 되는 페이징 없는 OS와는 달리, 페이징을 활용하는 OS는 <strong><em>페이지 테이블을 참조하는 주소 변환 과정이 추가</em></strong>되기 때문에 상대적으로는 조금 느릴 것이다.</li>
</ul>
<p>그럼에도 불구하고 심각한 메모리 파편화로 인해 메모리를 아예 사용하지 못하는 상황은 유발하지 않는다는 점에서, 페이징 정책으로 얻는 장점이 단점에 비하면 압도적으로 크다고 볼 수 있을 것이다.</p>
<h1 id="android의-페이징-정책">Android의 페이징 정책</h1>
<h2 id="근거">근거</h2>
<p>위에 안내한 링크를 읽어보면, Android는 페이징 단위를 기존 4 KB에서 16 KB로 증가시켰다. 이유가 뭘까?</p>
<h3 id="페이지-테이블-크기-감소">페이지 테이블 크기 감소</h3>
<p>나 어릴 적에는 4 GB 하면 많은 메모리였지만, 지금은 16 GB 메모리를 가지는 디바이스도 많이 출시되고 있다. 만약 4 KB 페이징 정책을 유지한다면, 4 GB 디바이스에서는 1백만 개의 테이블 엔트리를 관리하면 된다. 그러나 16 GB 메모리에서는 그 4배인 4백만 개의 테이블 엔트리를 관리해야 한다. 테이블도 결국 저장 공간을 필요로 하는 만큼, 메모리가 커지고 있는데 페이징 단위는 그대로라면, OS는 페이지 테이블 관리를 위해 저장 공간도 더 많이 써야 할 것이다.</p>
<h3 id="메모리-접근-횟수-및-레이턴시-감소">메모리 접근 횟수 및 레이턴시 감소</h3>
<p>테이블도 일종의 공유 자원인 만큼 (적어도 쓰기 작업에서는) 원자적인 접근이 보장되어야 할 것이다. 그러나, 테이블 크기가 크다면, 그만큼 접근 요청도 매우 많을 것이고, 이는 곧 병목 현상의 발생을 의미한다. 테이블 크기를 줄이게 되면 접근 요청도 그만큼 줄어들고, 접근에 필요한 시간(레이턴시)도 크게 줄어들 것이다.</p>
<h3 id="tlb-효율-증가">TLB 효율 증가</h3>
<p>보통 CPU는 페이지를 프레임으로 바꾸기 위해, 논리적인 페이지 테이블이 아니라 TLB라고 불리는 오직 페이징만을 위해 존재하는 물리적인 메모리를 사용한다. 페이징 단위를 4 KB에서 16 KB로 증가시킨다는 것은, 곧 TLB의 테이블 엔트리 1개의 효율이 4배 올라간다는 말과 같다.</p>
<p>따라서, 16 KB 로 페이징 단위를 증가시키는 것은 최근 하드웨어의 발전에 발 맞추려는 Google의 적절한 조치라고 볼 수 있겠다.</p>
<h2 id="개발에-미치는-영향">개발에 미치는 영향</h2>
<h3 id="일반적인-경우">일반적인 경우</h3>
<p>만약 당신이 오직 Kotlin과 Java로만 프로젝트를 개발하고 있다면, 이런 변경 사항에 신경 쓸 필요 없다. Kotlin/Java 쪽은 이미 알아서 16 KB 페이징에 맞추어 처리하는 것 같다. 그러나 C/C++은 얘기가 다르다.</p>
<p>C/C++는 다른 고급 언어와는 다르게, 메모리 공간에 직접 접근이 가능하다. 그래서 C/C++에서는 베이스 레지스터에 인덱스를 더하는 등 메모리 주소에 대한 직접적인 연산이 필요한 경우가 있다. 그런데, 만약 C/C++ 코드는 4 KB를 페이징 단위로 알고 있는데, 실제 동작 환경에서는 16 KB를 페이징 단위로 쓰고 있다면 문제가 생긴다.</p>
<ul>
<li>C/C++ 코드는 4 KB를 페이징 단위라고 알고 있어서, 코드와 데이터를 4 KB 단위로 잘라 두었다.</li>
<li>Android는 16 KB 단위로 메모리를 관리한다.</li>
<li>앱 실행 시, C/C++ 바이너리가 &quot;내 코드랑 데이터 중 1번째 덩어리는 0 KB 지점에, 2번째 덩어리는 4 KB 지점에, ..., 이런 식으로 올려줘&quot;라고 요청한다. </li>
<li>그러나, Android에게는 메모리 페이지의 시작 지점은 16 KB의 배수(0 KB, 16 KB, 32 KB, ...)여야만 한다.</li>
<li>Android에게는 2번째 페이지의 시작 주소인 4 KB는 16 KB의 배수가 아니므로, 적절하지 않은 메모리 할당이다.</li>
<li>따라서 요청을 거부하고 오류를 뱉는다.</li>
</ul>
<p>이런 과정에 의해 오류가 발생하게 된다. 따라서, C/C++을 Android 프로젝트에 포함하고 있다면, 이 바이너리가 16 KB 단위로 정렬되도록 수정해주어야 한다.</p>
<p>다행히도, <a href="https://developer.android.com/guide/practices/page-sizes#build">위에서 소개한 안내</a>에 따르면, <strong><em>NDK r27 버전과 Android Gradle Plugin 8.5.1 이후부터는 , C/C++를 코드에 포함하고 있어도 알아서 16 KB 단위로 코드를 정렬</em></strong>해 준다. 따라서 일반적인 경우에는 딱히 뭘 건들 필요가 없다.</p>
<h3 id="나는">나는...?</h3>
<p>근데 이 글을 왜 썼겠는가? 자동으로 변환해주고 있다는 안내에도 불구하고, 내 코드는 4 KB 단위로 정렬되는 문제가 발생했기 때문이다.</p>
<p>나는 Microsoft에서 개발한 오픈 소스 동형 암호화 라이브러리인 <a href="https://github.com/microsoft/SEAL">SEAL</a>을 내 Android 프로젝트에 포함했다. 그리고 위 안내의 내용처럼 16 KB 단위로 컴파일될 것을 기대했으나 그렇지 않았다. 아마 SEAL 코드 내부에 무언가 4 KB 페이징을 강제하는 옵션이 있을 것이다. 그러나 나는 그걸 섣불리 찾아 수정할 수 없었는데...</p>
<ul>
<li>SEAL 코드를 이해하지 않은 채로 무턱대고 수정한다면, 뭔가 의도하지 않은 문제가 생길 수 있다.</li>
<li>추후에 SEAL 라이브러리를 업데이트할 경우, 단순히 소스 코드를 통째로 바꾸는 게 아니라 변경한 부분을 일일이 새로운 버전에도 반영해주어야 한다.</li>
</ul>
<p>그래서, 나는 그냥 코드 내부에 존재할지도 모르는 4 KB 페이징 설정을 강제로 16 KB로 덮어씌우는 방법을 택하기로 했다.</p>
<h1 id="cc-컴파일에서의-변경-사항">C/C++ 컴파일에서의 변경 사항</h1>
<p>보통 Android 프로젝트에서 C/C++을 사용하고 있다면, NDK와 CMake를 통해 컴파일을 할 것이다. 이 때, 가장 최상위에 위치한 CMakeLists.txt에 아래 코드를 적어주면 된다:</p>
<pre><code class="language-cmake"># CMake 버전 특정
cmake_minimum_required(VERSION 3.18)

# 16 KB 호환성 강제 설정
if(ANDROID_ABI STREQUAL &quot;arm64-v8a&quot; OR ANDROID_ABI STREQUAL &quot;x86_64&quot;)
    target_link_options(project_name PRIVATE &quot;-Wl,-z,max-page-size=16384&quot;)
endif()</code></pre>
<p>코드를 상세하게 파 보자:</p>
<h2 id="cmake-버전-특정">CMake 버전 특정</h2>
<p>아래에서 사용할 <code>target_link_options</code> 명령어는 <a href="https://cmake.org/cmake/help/latest/command/target_link_options.html">CMake 3.18 이상에서만 지원</a>된다. 그래서 반드시 최소 버전을 명시해주어야 한다.</p>
<h2 id="abi-확인">ABI 확인</h2>
<p><code>ANDROID_ABI STREQUAL &quot;arm64-v8a&quot; OR ANDROID_ABI STREQUAL &quot;x86_64&quot;</code> 코드는 64 비트 ABI가 타겟일 경우에만 실행되도록 조건을 걸어주는 역할을 한다.</p>
<h2 id="링커-옵션-설정">링커 옵션 설정</h2>
<p>각 옵션을 상세히 살펴보자:</p>
<ul>
<li><code>target_link_options()</code>은 링킹 단계에 옵션을 주겠다는 말이다.</li>
<li><code>PRIVATE</code>는 이 라이브러리를 가져다 쓰는 다른 프로젝트에는 이 변경 사항을 적용하지 않겠다는 말이다.</li>
<li><code>-Wl</code>은 이어지는 옵션을 컴파일러가 아니라 링커에게 전달하라는 말이다.</li>
<li><code>-z,</code>는 링커에게 콤마 뒤에 키워드 옵션을 주겠다는 말이다.</li>
<li><code>max-page-size=16384</code>에서 16,384 = 16 * 1,024이며, 이는 곧 16 KB를 의미한다.</li>
</ul>
<h1 id="결론">결론</h1>
<p>물론 위에서 명시한 대로 AGP와 NDK를 모두 최신 버전으로 업데이트해 쓰면 굳이 이 문제를 겪을 일은 없을 수도 있다. 다만, 나의 경우처럼 외부에서 가져온 라이브러리가 강제로 4 KB 단위로 잘려 있을 수도 있고, 또는 나도 모르게 페이징 시 문제가 발생할지도 모르는 일이다.</p>
<p>따라서 방어적인 관점에서는 위 옵션을 CMake에 전달하여, 혹시나 모를 상황을 방지하는 것도 나쁘지 않을 수 있겠다는 생각이 든다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[효율적인 Android CI 구축 w/ GitHub Actions - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/Efficient-CI-with-GitHub-Actions-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/Efficient-CI-with-GitHub-Actions-Jetpack-Compose</guid>
            <pubDate>Mon, 04 Aug 2025 08:56:46 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기">들어가기</h1>
<h2 id="배경">배경</h2>
<p>보통 프로젝트를 위해 GitHub 저장소를 파면 가장 먼저 하는 것들 중 하나가 (언어나 프레임워크 상관 없이) CI 스크립트를 올리는 것이다. 그런데, React 등 웹 기반 FE 프레임워크랑은 다르게 Android는 CI 돌릴 때 시간이 너무 오래 걸린다. 특히나, Android의 경우 Android 내부 바이너리에 있는 기능을 사용할 경우, 에뮬레이터까지 돌려줘야 해서 훨씬 더 비용이 많이 들게 된다.</p>
<p>따라서, 개발자의 정신 건강과 효율적이고 신속한 개발 템포를 위해서라면, CI 스크립트를 효율적으로 짤 필요가 있다. 이를 위해 나는 GitHub Actions에서 기본으로 제공하는 &#39;actions/cache&#39;를 사용하기로 했다.</p>
<h2 id="actionscache에-대해">&#39;actions/cache&#39;에 대해</h2>
<h3 id="동작-방식">동작 방식</h3>
<p>해당 공식 문서에 나온 동작 방식을 요약하면 아래와 같다:</p>
<ul>
<li>캐시 키를 하나 지정한다.</li>
<li>GitHub Actions 스크립트에서 지정한 캐시 키를 호출한다.</li>
<li>캐시 상태에 따라...<ul>
<li>Hit일 경우, GitHub의 캐시 저장소에 있던 캐시를 불러온다.</li>
<li>Fail일 경우, 일단 남은 작업을 진행하고, 작업 후 Post 단계에서 현재 러너에 저장된 파일을 GitHub 캐시 저장소에 올린다.</li>
</ul>
</li>
</ul>
<h2 id="어떻게-효율성을-달성하는가">어떻게 효율성을 달성하는가?</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>보통 Android CI 스크립트에서 AVD를 돌린다면 아래와 같은 준비 절차가 필요하다:</p>
<ol>
<li>Android 시스템 이미지를 다운받는다.</li>
<li>전체 시스템 이미지를 불러온다.</li>
<li>VM을 초기화한다.</li>
<li>부팅을 진행한다. 이 과정에서 부팅에 필요한 스크립트와 프로세스가 전부 올라와야 한다.</li>
<li>홈 스크린이 뜨고 시스템이 안정될 때까지 기다린다.</li>
</ol>
<p>일단 1번부터 오래 걸린다. 시스템 이미지가 기본 GB 단위이기 때문이다. 여기에 더해, 안 그래도 다운로드에도 시간이 오래 걸리는데, 부팅도 콜드 부팅이다. 부팅도 당연히 오래 걸릴 수밖에 없다. 오래 걸리는 일이 2개니까 당연히 느리고, 만약 CI가 자주 실행되는 환경이라면 이로 인해 잡아먹하는 시간이 점점 많이 늘어나게 될 것이다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>&#39;actions/cache&#39;는 이러한 시간 낭비를 해결할 수 있게 해 준다. <strong><em>시간이 많이 걸리거나 용량이 커서 다운로드에 오랜 시간이 필요한 파일</em></strong>을 캐싱하여, 2번째 실행부터는 해당 준비 과정을 최대한 생략할 수 있기 때문이다. &#39;actions/cache&#39;를 통해 우리는 다음 2가지 파일을 캐싱할 수 있다:</p>
<ul>
<li>시스템 이미지 → 다운로드 시간 절약</li>
<li>OS 스냅샷 → 부팅 시간 절약</li>
</ul>
<p>먼저, 시스템 이미지를 캐시 저장소에서 바로 가져오면 되므로 다운로드 시간이 크게 줄어든다.</p>
<p>중요한 건 다음인데, AVD는 2번째 실행부터는 스냅샷을 불러와 사용하기 때문에, 콜드 부팅을 할 필요가 없어 부팅 시간도 크게 줄어든다. AVD의 스냅샷은 <strong><em>특정 시점에서 CPU, GPU, RAM, 저장소와 실행되는 중이었던 프로세스나 앱의 상태를 캡쳐해 저장해 둔 것</em></strong>을 의미한다. 예를 들어, 내가 오후 3시 20분에 AVD의 상태를 스냅샷으로 떠 두었다면, 다음부터 부팅할 때에는 오후 3시 20분의 상태가 그대로 복원되는 거다. 단순히 특정 시점의 상태를 그대로 복원하면 가상 머신을 그대로 사용할 수 있으므로, 부팅과 애플리케이션이 시작되기를 기다릴 필요가 없다. 부팅에 필요한 시간이 크게 줄어들게 되는 것이다.</p>
<h1 id="구현">구현</h1>
<h2 id="gradle-wrapperjar-업로드">gradle-wrapper.jar 업로드</h2>
<p>본격적으로 CI 스크립트를 확인하기 전에 먼저 해야 할 일이 있다. .gitignore 파일에서 gradle-wrapper.jar 파일을 제외하는 것이다. 보통은 .jar 파일은 바이너리라서 GitHub 저장소에 올리지 않는데, 이 파일만은 빌드 및 테스트에 사용하는 Gradle을 돌리기 위해 반드시 필요하다. 따라서, 저장소에서 <code>*.jar</code>로 모든 .jar 파일의 커밋을 막고 있다면, gradle-wrapper.jar만 예외로 넣어주자.</p>
<p>이 파일이 있어야, GitHub Actions 러너가 Gradle을 돌릴 수 있다. </p>
<h2 id="ci-스크립트">CI 스크립트</h2>
<h3 id="요약">요약</h3>
<p>내가 작성한 CI 스크립트는 아래 순서대로 진행된다. 이 중, 중요하지 않은 부분은 생략하고 언급할 내용이 있는 부분만 설명을 남겨두겠다.</p>
<ul>
<li>GitHub 저장소의 코드 불러오기</li>
<li>Java 준비</li>
<li>Gradle 준비</li>
<li>gradlew에 실행 권한 부여</li>
<li>KVM 활성화 </li>
<li>캐시 확인</li>
<li>가상 머신 스냅샷 생성</li>
<li>계측 테스트(instrumental test) 진행</li>
<li>린팅, 단위 테스트, 빌드 진행</li>
<li>(선택) 린팅 결과물을 아티팩트로 업로드</li>
<li>(선택) 빌드 결과물인 .apk 파일을 아티팩트로 업로드</li>
</ul>
<h3 id="kvm-활성화">KVM 활성화</h3>
<pre><code class="language-yaml">- name: Enable KVM
  run: |
    echo &#39;KERNEL==&quot;kvm&quot;, GROUP=&quot;kvm&quot;, MODE=&quot;0666&quot;&#39; | sudo tee /etc/udev/rules.d/99-kvm.rules
    sudo udevadm control --reload-rules
    sudo udevadm trigger</code></pre>
<p>KVM은 Kernel-based Virtual Machine의 약자이다. 이 옵션은 하드웨어의 가상화 지원 기술(Inter의 VT-x 등)을 활성화하기 때문에, 가상 머신 성능 향상에 큰 도움을 준다. 이 부분도 결국은 테스트 시간을 줄일 수 있게 도와주므로, 집어넣도록 하자.</p>
<h3 id="캐시-확인-및-스냅샷-준비">캐시 확인 및 스냅샷 준비</h3>
<pre><code class="language-yaml">- name: AVD cache
  uses: actions/cache@v4
  id: avd-cache # Specify a unique GitHub Actions ID for the cache
  with:
    path: |
      ~/.android/avd/*
      ~/.android/adb*
    key: avd-33

- name: Create AVD and generate snapshot for caching
  if: steps.avd-cache.outputs.cache-hit != &#39;true&#39;
  uses: reactivecircus/android-emulator-runner@v2
  with:
    # 생략...</code></pre>
<p>가장 중요한 부분이다. 이상의 2개 단계 중 1번째인 &#39;AVD cache&#39; 단계는 <code>avd-33</code>이라는 키를 가진 캐시가 GitHub 캐시 저장소에 존재하는지 여부를 확인한다. (<code>avd-cache</code>는 캐시를 식별하기 위한 키가 아니라, 다음 단계에서 캐싱 성공 여부를 확인하기 위해 사용하는 GitHub Actions 전용 변수다. 헷갈리지 말자.) 이 부분에 따라 이후 스크립트가 어떤 일을 하는지가 조금 달라진다. 자세히 알아보자:</p>
<h4 id="캐시-히트-성공-시">캐시 히트 성공 시</h4>
<ul>
<li>단계 &#39;AVD cache&#39;에서 <code>avd-33</code> 캐시가 있는지를 확인한다.</li>
<li>히트 시, 저장된 캐시를 그대로 불러온다.</li>
<li>단계 &#39;Create AVD and generate snapshot for caching&#39;에서 <code>steps.avd-cache.outputs.cache-hit</code>가 <code>true</code>인지를 확인한다.</li>
<li>캐시가 히트되어 캐싱된 파일을 불러왔으므로 값은 <code>true</code>, 전체 조건식은 <code>false</code>이다. 따라서 이 단계를 생략한다.</li>
</ul>
<p>요약하면, 캐싱 성공 시 캐시 저장소에서 시스템 이미지와 스냅샷을 가져온다. 그리고 다음 단계인 스냅샷 준비 단계 자체를 그냥 생략해버린다. 그러므로 빠른 시간 내에 CI를 위한 준비를 마칠 수 있는 것이다.</p>
<h4 id="캐시-히트-실패-시">캐시 히트 실패 시</h4>
<ul>
<li>단계 &#39;AVD cache&#39;에서 <code>avd-33</code> 캐시가 있는지를 확인한다.</li>
<li>실패 시, 다음 단계로 넘어간다.</li>
<li>단계 &#39;Create AVD and generate snapshot for caching&#39;에서 <code>steps.avd-cache.outputs.cache-hit</code>가 <code>true</code>인지를 확인한다.</li>
<li>캐시 히트가 실패하였으므로 값은 <code>false</code>, 전체 조건식은 <code>true</code>이다. 따라서 이 단계를 진행하여, 시스템 이미지로부터 부팅 후 스냅샷을 생성한다.</li>
<li>남은 단계를 마저 진행한다.</li>
<li>CI 스크립트 가장 마지막의 <code>Post AVD cache</code> 단계에서, 지정된 <code>path</code>에 있는 파일들을 캐시 키 <code>avd-33</code>과 함께 캐시 저장소로 업로드한다.</li>
</ul>
<p>요약하면, 캐싱 실패 시에는 일단 Android 시스템을 콜드 부팅하고 스냅샷을 만들어 테스트를 진행한다. 그리고, 오직 테스트를 실패했을 때에만 캐시 저장소에 캐시를 업로드한다. 이 때 업로드할 파일을 <code>path</code>에 지정해줄 수 있다. 이 CI 스크립트에서는 AVD 관련 파일과 스냅샷이 담긴 &#39; <del>/.android/avd/*&#39; 그리고 Android 디버그 툴이 담긴 &#39;</del>/.android/adb*&#39;를 캐시 저장소에 업로드하하게 된다.</p>
<p>이렇게 캐싱 여부에 따라 적절한 스냅샷을 준비한 후, 준비된 스냅샷 하에서 Android 바이너리 기능이 필요한 계측 테스트(instrumented test) - src/androidTest에 존재하는 테스트 - 를 진행하게 된다. </p>
<h3 id="adv가-필요-없는-나머지-작업-진행">ADV가 필요 없는 나머지 작업 진행</h3>
<pre><code class="language-yaml">- name: Run unit tests, lint check and build
  run: ./gradlew --no-daemon --stacktrace --continue test lintDebug assembleDebug</code></pre>
<p>마지막으로 Android 바이너리가 필요하지 않은 나머지 작업을 AVD 없이 진행한다. 이 과정에서는 아래 작업들이 진행된다:</p>
<ul>
<li>Android 바이너리가 필요 없는 단위 테스트 (src/test에 위치)</li>
<li>린팅 오류 확인</li>
<li>빌드</li>
</ul>
<h3 id="전체-스크립트">전체 스크립트</h3>
<p>이 과정을 통해 완성된 스크립트는 아래와 같다:</p>
<pre><code class="language-yaml">name: Android CI

on:
  push:
    branches:
      - develop
  pull_request:
    branches:
      - develop

jobs:
  build_and_test:
    name: build_and_test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          java-version: &#39;21&#39;
          distribution: &#39;temurin&#39;

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Enable KVM
        run: |
          echo &#39;KERNEL==&quot;kvm&quot;, GROUP=&quot;kvm&quot;, MODE=&quot;0666&quot;&#39; | sudo tee /etc/udev/rules.d/99-kvm.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger

      - name: AVD cache
        uses: actions/cache@v4
        id: avd-cache # Specify a unique GitHub Actions ID for the cache
        with:
          path: |
            ~/.android/avd/*
            ~/.android/adb*
          key: avd-33

      - name: Create AVD and generate snapshot for caching
        if: steps.avd-cache.outputs.cache-hit != &#39;true&#39;
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          arch: x86_64
          force-avd-creation: false
          emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
          disable-animations: false
          script: echo &quot;Generated AVD snapshot for caching.&quot;

      - name: Run instrumented tests from snapshot
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          arch: x86_64
          force-avd-creation: false
          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
          script: ./gradlew --no-daemon --stacktrace --continue connectedAndroidTest

      - name: Run unit tests, lint check and build
        run: ./gradlew --no-daemon --stacktrace --continue test lintDebug assembleDebug

      - name: Upload lint report artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lint-report
          path: app/build/reports/lint-results-debug.html</code></pre>
<h1 id="결론">결론</h1>
<p>나는 &#39;actions/cache&#39;를 사용한 GitHub Actions CI 스크립트를 통해 다음 2가지에 필요한 시간을 크게 절약할 수 있었다:</p>
<ul>
<li>시스템 이미지 다운로드</li>
<li>AVD 콜드 부팅</li>
</ul>
<p>내가 이 CI 스크립트를 적용한 저장소는 현재로서는 혼자 개발하기 때문에 그렇게 크게 체감되진 않겠지만, 만약 여러 사람이 개발하는 저장소라면 시간 절약 효과를 톡톡히 볼 수 있을 것이라 생각한다.</p>
<h1 id="참고-문헌">참고 문헌</h1>
<ul>
<li><a href="https://developer.android.com/studio/run/emulator-snapshots?hl=ko">Android Snapshot - Android Developers</a></li>
<li><a href="https://github.com/actions/cache">actions/cache - GitHub Actions</a></li>
<li><a href="https://github.com/ReactiveCircus/android-emulator-runner?tab=readme-ov-file#usage--examples">ReactiveCircus/android-emulator-runner - GitHub Actions</a></li>
<li><a href="https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching">Dependency caching reference - GitHub Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[액세스/리프레시 토큰 관리 도구 구현 - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/How-To-Manage-Tokens-On-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/How-To-Manage-Tokens-On-Jetpack-Compose</guid>
            <pubDate>Sat, 02 Aug 2025 18:29:02 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기">들어가기</h1>
<h2 id="배경">배경</h2>
<p>현재 나는 React 기반 웹 앱을 Android에서 동작하는 Jetpack Compose 앱으로도 서비스할 수 있게 준비하고 있다. 이 과정에서, 기존 웹 앱에서 사용하던 Google OAuth를 Compose에서도 쓰기 위해, 액세스 토큰과 리프레시 토큰을 저장해야 할 필요가 생겼다. 근데 관련한 지식이 나에겐 없어서, AI 딸깍해서 코드만 취하기에는 코드 자체가 이해가 어렵더라. 그래서 배경 지식을 먼저 확실히 배우고 구현에 들어가기로 했다. 이 게시물은 그 배경 지식들을 나의 이해를 돕기 위해 정리한 글이다.</p>
<h2 id="oauth와-액세스-토큰-리프레시-토큰">OAuth와 액세스 토큰, 리프레시 토큰</h2>
<p>쉽게 말하면, Google ID와 PW를 제3자 서비스에 노출하지 않고 Google이 사용자를 대신 인증해주기 위해 사용하는 것으로 보인다. 아무래도 신뢰하기 어려운 제3자에게 (개인의 거의 모든 정보가 담긴 Google 계정의) ID와 PW를 넘겨주는 건 좀 그러니까.</p>
<p>나는 OAuth를 처음 접해봐서, Gemini에게 간단하게 과정을 설명해달라고 요구했다. 그 내용을 간단히 옮겨 보겠다:</p>
<h3 id="토큰-발급-과정">토큰 발급 과정</h3>
<h4 id="접근-요청">접근 요청</h4>
<p>메모 앱이 내 Google 캘린더 정보를 보고 싶어한다. &quot;Google 계정으로 로그인&quot; 버튼을 눌러, OAuth를 시작한다.</p>
<h4 id="google로-이동">Google로 이동</h4>
<p>메모 앱은 인증을 위해 나를 Google로 보낸다.</p>
<h4 id="인증-및-동의">인증 및 동의</h4>
<p>나는 Google에 로그인을 하고, Google은 내게 메모 앱에 캘린더 접근 권한을 줄 것이냐고 묻는다. 나는 동의 버튼을 누르게 된다.</p>
<h4 id="권한-코드-발급-및-서명">권한 코드 발급 및 서명(?)</h4>
<p>나의 동의를 확인하면, Google은 메모 앱에 권한 코드(authorization code)라는 것을 보낸다. 이건 일종의 임시 확인증 같은 것으로, 아직 최종 승인을 받은 게 아니다. 왜냐하면 Google은 지금 실행되고 있는 메모 앱이 가짜가 아닌지 확인할 수 없기 때문이다. 따라서 메모 앱은 자신이 진짜 메모 앱이라는 걸 증명할 필요가 있다.</p>
<p>개발을 하면서 NAVER 지도 API 서비스 같은 걸 사용하다 보면, API 키 같은 걸 제공하고는 한다. 이 API 키는 각 개인에게 고유하게 지급되기 때문에, 개인을 식별할 수 있는 도구가 된다. Google OAuth를 사용하기 위해 Google Cloud Platform에서도 이 API 키에 해당하는 걸 발급을 해 주는데, 앱 개발 시에 이걸 반드시 집어넣게 되어 있다.  이 API 키를 권한 코드에 붙여서 Google에게 &quot;나 진짜 메모 앱이고 그 증거로 이 API 키를 보낼게. 승인해줘!&quot;라고 말하게 되는 것이다.</p>
<p>서명(?)이라고 쓴 이유는 내부적으로 진짜 서명이 이루어지는지, 아니면 서명은 아니지만 이와 유사한 작업인지, 지금 알고 있는 내용으로는 확실히 판단할 수 없어서이다. 그래서 모호하게 써 놨다.</p>
<h4 id="토큰-수령">토큰 수령</h4>
<p>앱의 API 키를 확인한 Google은 &quot;얘는 진짜 메모 앱이구나.&quot;라는 것을 알게 되고, 적합한 요청이므로 액세스 토큰과 리프레시 토큰을 제공한다. 최종 승인이다. 이제부터 메모 앱은 이 토큰을 사용해 Google의 캘린더 데이터에 접근할 수 있다.</p>
<h3 id="두-토큰의-역할">두 토큰의 역할</h3>
<p>그럼 2개 토큰은 어떤 일을 할까? 뭐하러 토큰을 2개나 발급해주는 걸까? 이는 보안 위협 때문이다. 만약 토큰 1개만 주고 &quot;이거 평생 써먹어라&quot; 하면 보안에 취약해진다. 한 번만 탈취하면 그 이후부터는 계속 사용 가능하기 때문이다. 그래서 데이터에 접근할 수 있는 토큰에는 제한을 두어야 한다. 이게 액세스 토큰이다.</p>
<blockquote>
<h4 id="액세스-토큰">액세스 토큰</h4>
<p>액세스 토큰은 데이터에 직접 접근(access)할 수 있다. 다만, 제한 시간이 있다. Gemini에 따르면, 보통 3,600초 정도란다. 이건 뭐 서비스나 OAuth 공급자 정책에 따라 다르겠지만, 1시간 정도니까, 공격을 당한다고 해도 그나마 평~생 털릴 걱정은 없다. 그런데 만약 액세스 토큰이 만료되면? 사용자도 1시간 뒤에는 서비스에 접근할 수 없을 것이다. 그래서 주기적으로 재발급을 해 줘야 하는데, 재발급을 위해 필요한 게 리프레시 토큰이다.</p>
</blockquote>
<blockquote>
<h4 id="리프레시-토큰">리프레시 토큰</h4>
<p>리프레시 토큰은 액세스 토큰을 갱신(refresh)해준다. 액세스 토큰과는 달리 데이터에 접근이 불가능하다.</p>
</blockquote>
<p>이처럼, 역할이 분리된 두 가지 토큰을 사용하면, 공격에 어느 정도 대비하면서도 안전하게 인증을 받고 데이터에 접근할 수 있다.</p>
<h2 id="웹과-앱에서의-oauth-차이점">웹과 앱에서의 OAuth 차이점</h2>
<p>이 지점에서 질문이 생긴다. 내가 지금 토큰 관리자를 구현하려는 이유는, 암호화된 토큰을 안전하게 저장하기 위해서이다. 그러나, 웹 환경에서는 암호화? 그런 거 하지 않는다. 특히나 액세스 토큰 같은 경우는, 웹에서는 암호화가 전혀 안 된 세션 스토리지에 저장해서 사용하는 경우가 잦다. 나는 왜 앱 환경에서는 암호화를 하면서, 웹에서는 그렇지 않은지가 궁금해서 이 부분을 LLM들에게 물어봤다.</p>
<h3 id="모바일-환경에서의-주요-보안-위협-물리적-탈취-및-공격">모바일 환경에서의 주요 보안 위협: 물리적 탈취 및 공격</h3>
<p>모바일 환경에서 주요한 보안 위협은 파일 자체에 대한 물리적인 접근이다. 가장 흔한 시나리오로는 그냥 디바이스 자체를 훔치거나 도난당하는 경우가 있겠다. 이에 더해, 탈옥이나 루팅 등으로 내부 저장소에 더 쉽게 접근할 수 있게 되는 경우도 있다. 따라서, 이런 물리적 위협이 발생하여 파일에 접근이 가능해지는 불상사가 생겨도, 파일을 읽는 것 자체가 소용이 없게 토큰을 암호화하는 게 모바일에서는 적합한 전략인 것이다.</p>
<h3 id="웹-환경에서의-주요-보안-위협-xss-공격">웹 환경에서의 주요 보안 위협: XSS 공격</h3>
<p>XSS 공격(Cross-Site Scripting)은 웹 사이트에 악성 스크립트를 사용하여 공격하는 것을 말한다. 즉, 정상 실행되어야 할 스크립트를 다른 악성 스크립트로 교체하여 그게 대신 실행되게 하는 것이다. XSS 공격이 주된 위협이라면, 토큰의 암호화는 아무런 의미가 없다. Samsung Knox처럼 보안을 위한 별도 물리 프로세서나 컨테이너가 마련되어 있는 모바일 디바이스와는 다르게, 웹 브라우저는 그런 거 없기 때문이다.</p>
<p>왜 웹 환경에서 암호화가 의미가 없을까? 이를 이해하기 위해, 토큰을 암호화해서 저장했다고 가정해보자. 이 토큰을 사용하려면 언젠가는 복호화를 해야 하고, 그 복호화 키는 자바스크립트가 접근할 수 있는 어딘가(메모리, 변수 등)에 있어야 할 것이다. 그럼 공격자 스크립트는 암호화된 토큰과 그 복호화 키를 함께 훔쳐가서 자신의 서버에서 복호화하면 그만이다. 애초에 암호화해서 얻을 수 있는 실익이 크지 않은 것이다.</p>
<p>따라서 웹 브라우저에서는 토큰을 암호화하는 것보다는, XSS 공격을 막는 것을 가장 중요하게 본다. 만약 XSS 공격으로 인해 액세스 토큰이 탈취되어도, 공격의 영향이 크지 않게 재설정 시간도 매우 짧게 설정한다. 그리고 이 액세스 토큰을 재발급할 수 있는 <strong><em>가장 중요하고 꼭 지켜야 하는</em></strong> 리프레시 토큰은, 모바일 환경에서처럼 격리된 공간인 쿠키에 저장한다. 쿠키는 <code>HttpOnly</code> 쿠키라고, JS가 아예 접근조차 할 수 없는 공간에 분리되어 저장되고, 다음 보안 옵션도 설정하여 최대한 안전성을 확보한다:</p>
<ul>
<li><code>Secure</code> 옵션은 HTTPS 프로토콜에서만 쿠키가 전송되도록 강제한다.</li>
<li><code>SameSite</code> 옵션은 CSRF(Cross-Site Request Forgery) 공격을 방어하기 위한 옵션이다. 쿠키가 다른 도메인을 가진 사이트에 보내지는 것을 막는다.</li>
</ul>
<h3 id="정리">정리</h3>
<p>정리하면, 모바일과 웹 환경은 서로 많이 다르기 때문에, 각 환경의 고유 특성과 가장 위협적인 공격에 맞추어 다르게 대응하게 되는 것이다.</p>
<h1 id="구현">구현</h1>
<h2 id="과정">과정</h2>
<p>자, 이제 왜 토큰을 암호화해야 하는지를 알았으니 구현을 해도 되겠다. 먼저, 무엇이 필요한지를 식별해야 한다:</p>
<blockquote>
<p>나는 Google에게 받은 액세스 토큰과 리프레시 토큰을, 암호화한 후, 기기에 저장해야 한다.</p>
</blockquote>
<p>즉, 나는 다음과 같은 도구를 구현해야만 하는 것이다:</p>
<ul>
<li>평문 토큰을 암/복호화하는 유틸</li>
<li>암호화된 토큰을 R/W할 수 있는 저장소</li>
</ul>
<p>찬찬히 진행해보자.</p>
<h2 id="암복호화-유틸-구현">암/복호화 유틸 구현</h2>
<p>순서상 이게 먼저 되어야 한다. 그래야 토큰 저장소에서 이걸로 암/복호화를 할 테니까.</p>
<h4 id="암호화-정책">암호화 정책</h4>
<p>먼저 암호화 정책을 정해야 한다. 이것저것 고려해보고 Gemini와도 얘기해본 결과, 아래 정책을 사용하기로 했다:</p>
<ul>
<li>암호화 알고리즘 AES</li>
<li>비트 수 256</li>
<li>블록 모드 GCM (갈루아/카운터 모드)</li>
<li>패딩 없음</li>
</ul>
<p>대충 학교에서는 블록 암호인 AES의 성능 향상을 위해 블록 단위로 병렬 처리하는 방법이 여러 가지가 있는데, 그 중 가장 좋은 방법이 카운터 방법이라고 배웠다. 병렬 처리와 사전 연산이 모두 가능하기 때문이다. 갈루아/카운터 모드는 카운터 모드에 무결성과 인증을 위한 대책까지 포함한 방법이라고 한다. 아무튼 현재로서는 제일 좋다는 말인 것 같다. 추가로, 어쨌든 카운터 모드를 사용하기 때문에 IV(initialization vector)가 필요하다. 이것도 염두에 두어야 한다.</p>
<h4 id="토큰-저장을-위한-구조-정의">토큰 저장을 위한 구조 정의</h4>
<p>다음으로는 토큰을 기기에 어떻게 저장할지 그 구조를 정해야 한다. Gemini는 토큰을 바이트 스트림으로 저장할 거기 때문에, 두 토큰을 하나로 접합(concat)하여 관리하기 편하게 하는 방안을 추천했다. 그래서 구현은 아래와 같이 진행했다:</p>
<pre><code class="language-kotlin">@Serializable
data class TokenBundle(
    val accessToken: String,
    val refreshToken: String,
)
</code></pre>
<p>먼저, <code>@Serializable</code> 어노테이션을 달아서 JSON으로의 직렬화/역직렬화가 가능하게 하였다. 왜냐하면, 액세스 토큰과 리프레시 토큰을 JSON 문자열로 변환하고 이를 바이트 스트림으로 바꿀 것이기 때문이다. 이에 대응하여, Proto 자료형도 아래와 같이 정의했다:</p>
<pre><code class="language-proto">syntax = &quot;proto3&quot;;

option java_package = &quot;com.my.app&quot;;
option java_multiple_files = true;

message EncryptedTokens {
  bytes encrypted_token_bundle = 1;
  bytes initialization_vector = 2;
}</code></pre>
<p>직렬화되어 JSON 문자열이 된 토큰 번들은 바이트 스트림으로 바뀌어 <code>encrypted_token_bundle</code>이라는 이름으로 Proto DataStore에 저장된다. 동일하게, 암호 생성에 필요한 IV가 저장될 공간도 미리 준비해둔다.</p>
<h4 id="암호화-관리자-cryptomanager-구현">암호화 관리자 <code>CryptoManager</code> 구현</h4>
<p>다음으로는 실제 암/복호화를 담당할 유틸을 구현하자. 가장 먼저, 테스트 환경을 고려해서 가짜 암호화 관리자를 구현할 것에 대비하여, 아래와 같이 인터페이스를 미리 선언해둔다:</p>
<pre><code class="language-kotlin">interface CryptoManager {
    fun encrypt(plainText: String): Pair&lt;ByteArray, ByteArray&gt;
    fun decrypt(encryptedText: ByteArray, iv: ByteArray): String
}</code></pre>
<p>그리고 이 인터페이스를 바탕으로 실제 구현체를 짜 보자:</p>
<pre><code class="language-kotlin">class CryptoManagerImpl @Inject constructor() : CryptoManager {
    // 구현이 들어갈 자리
}</code></pre>
<p>먼저 위와 같이 <code>CryptoManager</code> 인터페이스를 상속하도록 하고, Hilt가 어떻게 얘를 만들어야 할지 알 수 있도록 <code>@Inject constructor</code>를 달아준다.</p>
<pre><code class="language-kotlin">companion object {
  // Define the constants for the encryption algorithm and key alias
  private const val KEY_ALIAS = &quot;debate_timer_oauth_key&quot;
  private const val ANDROID_KEYSTORE = &quot;AndroidKeyStore&quot;
  private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
  private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
  private const val PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
  private const val KEY_SIZE = 256
  private const val TRANSFORMATION = &quot;AES/GCM/NoPadding&quot;
}</code></pre>
<p>다음으로는 아까 정해둔 암호화 정책과 관련한 값들을 클래스 컴패니언 객체로 생성한다.</p>
<pre><code class="language-kotlin">/**
* Android Keystore에 저장된 키를 가져오거나, 없다면 새로 생성합니다.
*/
private fun getOrCreateKey(): SecretKey {
    // 1. Try to get the key
    val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry

    // 2. If key exists, return it
    if (existingKey != null) {
        return existingKey.secretKey
    }

    // 3. Else, prepare key generation spec
    val paramsBuilder = KeyGenParameterSpec.Builder(
        KEY_ALIAS,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
    paramsBuilder.apply {
        setBlockModes(BLOCK_MODE)
        setEncryptionPaddings(PADDING)
        setKeySize(KEY_SIZE)
    }

    // 4. Generate key with the spec
    val keyGenerator = KeyGenerator.getInstance(ALGORITHM, ANDROID_KEYSTORE)
    keyGenerator.init(paramsBuilder.build())
    return keyGenerator.generateKey()
}</code></pre>
<p>다음으로는 가장 중요한 키를 Android Keystore에서 가져오는 함수다. 키가 없을 경우에는 생성하고, 키가 있다면 가져오도록 구현한다. 또한, 아까 전에 선언해 두었던 암호화 정책 관련 상수들을 사용해서, 정책이 반영되어 키가 생성되도록 하자.</p>
<pre><code class="language-kotlin">override fun encrypt(plainText: String): Pair&lt;ByteArray, ByteArray&gt; {
  // 1. Load cipher instance
  val cipher = Cipher.getInstance(TRANSFORMATION)

  // 2. Init cipher with encryption mode
  cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())

  // 3. Return encrypted text and IV
  return Pair(cipher.doFinal(plainText.toByteArray()), cipher.iv)
}

override fun decrypt(encryptedText: ByteArray, iv: ByteArray): String {
  // 1. Load cipher instance and prepare variables
  val cipher = Cipher.getInstance(TRANSFORMATION)
  val spec = GCMParameterSpec(128, iv)

  // 2. Init cipher with decryption mode
  cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)

  // 3. Return decrypted text
  return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}</code></pre>
<p>마지막으로, 방금 만들어 둔 키 생성 함수로 암/복호화하는 함수를 구현한다. Java의 <code>Cipher</code> 객체를 사용하여 구현하면 된다. 또한, 이 함수들은 인터페이스에서 반드시 구현하도록 지정해 둔 함수이므로, <code>override</code>를 붙이는 것도 까먹지 말자.</p>
<p>이렇게 하면 암호화 유틸은 완성이다. 이제 얘를 써먹어서 토큰을 R/W하는 저장소만 구현하면 된다.</p>
<h2 id="토큰-저장소-구현">토큰 저장소 구현</h2>
<h4 id="protobuf-직렬화역직렬화-코드-구현">Protobuf 직렬화/역직렬화 코드 구현</h4>
<pre><code class="language-kotlin">const val FILE_NAME = &quot;encrypted_tokens.pb&quot;

object TokenSerializer : Serializer&lt;EncryptedTokens&gt; {
    override val defaultValue: EncryptedTokens = EncryptedTokens.getDefaultInstance()

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

    override suspend fun writeTo(t: EncryptedTokens, output: OutputStream) {
        t.writeTo(output)
    }
}

val Context.tokenDataStore: DataStore&lt;EncryptedTokens&gt; by dataStore(
    fileName = FILE_NAME,
    serializer = TokenSerializer
)</code></pre>
<p>먼저 Protobuf를 사용하는 만큼 직렬화/역직렬화 코드를 만들고, <code>DataStore</code> 객체를 Android <code>Context</code>에 붙여줘야 한다. Proto DataStore 그 자체에 관해서는 다른 게시글에서도 다룬 바 있을 테니 설명은 생략한다.</p>
<h4 id="저장소-인터페이스-구현">저장소 인터페이스 구현</h4>
<p>마찬가지로 테스트 용이성을 위해 인터페이스를 먼저 선언하고, 그걸 구현하는 식으로 저장소를 만들 예정이다. 인터페이스는 간단하다:</p>
<pre><code class="language-kotlin">interface TokenRepo {
    suspend fun getTokens(): Result&lt;TokenBundle&gt;
    suspend fun saveTokens(bundle: TokenBundle): Result&lt;Boolean&gt;
}</code></pre>
<h4 id="저장소-구현체-구현">저장소 구현체 구현</h4>
<p>인터페이스를 정의했으니, 이제 각각 토큰을 읽고 쓰는 함수를 구현해야 한다. 가장 먼저, Hilt가 적절하게 필요한 것들을 주입해줄 수 있게 클래스 서명을 다음과 같이 정의한다:</p>
<pre><code class="language-kotlin">class TokenRepoImpl @Inject constructor(
    @ApplicationContext context: Context,
    private val cryptoManager: CryptoManager
) :
    TokenRepo {
    private val tokenDataStore: DataStore&lt;EncryptedTokens&gt; = context.tokenDataStore

    // 그 외 다른 코드
}</code></pre>
<p>이렇게 해줌으로써 Hilt가 무엇을 넣어서 <code>TokenRepoImpl</code>을 만들어야 할지, 어느 범위의 <code>Context</code>를 넣어줘야 할지를 알 수 있게 되었다. 또한, 이 <code>Context</code>를 사용해서 아까 붙여준 Proto DataStore에도 접근이 가능하게 되었다. 이제 이것들을 바탕으로 읽기/쓰기 함수를 구현하면 된다.</p>
<pre><code class="language-kotlin">private suspend fun getBundle(): TokenBundle? {
    try {
        val encryptedTokens = tokenDataStore.data.first()
        val ciphertext = encryptedTokens.encryptedTokenBundle.toByteArray()
        val iv = encryptedTokens.initializationVector.toByteArray()
        val decryptedTokens = cryptoManager.decrypt(ciphertext, iv)

        return Gson().fromJson(decryptedTokens, TokenBundle::class.java)
    } catch (_: NoSuchElementException) {
        return null
    } catch (e: Exception) {
        throw e
    }
}

override suspend fun getTokens(): Result&lt;TokenBundle&gt; {
    try {
        val bundle = getBundle()

        return if (bundle != null) {
            Result.success(bundle)
        } else {
            Result.failure(NoSuchElementException())
        }
    } catch (e: Exception) {
        return Result.failure(e)
    }
}</code></pre>
<p>먼저 토큰 읽기 함수다. 대충 순서는 아래와 같다:</p>
<ul>
<li>Proto DataStore에서 암호화된 토큰 번들과 IV를 불러옴</li>
<li>아까 만들어 둔 암호화 유틸로 복호화</li>
<li>성공 시 토큰 번들 반환</li>
</ul>
<p>굳이 눈 여겨볼 점이 있다면 결과를 <code>Result&lt;T&gt;</code>로 감쌌다는 점이겠다. 얘도 결국 저장소이기 때문에 일종의 데이터 입출력에 해당해서, UI를 처리할 때 비동기적으로, 처리 상태에 따라 다른 내용을 보여주어야 한다. 이를 대비하기 위해 미리 <code>Result&lt;T&gt;</code>로 감싸서, 추후 조건부 렌더링이 편하게 정리해두었다.</p>
<pre><code class="language-kotlin">override suspend fun saveTokens(bundle: TokenBundle): Result&lt;Boolean&gt; {
    try {
        tokenDataStore.updateData { current -&gt;
            val serializedBundle = Gson().toJson(bundle)
            val encryptionOutput = cryptoManager.encrypt(serializedBundle)

            current.toBuilder()
                .setEncryptedTokenBundle(encryptionOutput.first.toByteString())
                .setInitializationVector(encryptionOutput.second.toByteString())
                .build()
        }

        return Result.success(true)
    } catch (e: Exception) {
        return Result.failure(e)
    }
}</code></pre>
<p>다음으로는 토큰 쓰기 함수다. 암호화를 암호화 도구에 요청하면, 암호화 결과물과 암호화에 쓴 IV를 돌려줄 것이다. 이 두 가지를 Proto DataStore에 저장하면 된다. 마찬가지로 처리에 성공하면 <code>Result&lt;Boolean&gt;</code>을 반환하여 추후 비동기 조건부 렌더링이 편하게 만들어 두었다.</p>
<h1 id="결론">결론</h1>
<p>웹 환경과는 다르게, 모바일 환경에서는 토큰 암호화와 관리에 신경을 많이 써 주어야 한다. 이를 위해, 나는 다음 2가지 도구를 구현했다:</p>
<ul>
<li>토큰 암/복호화 도구</li>
<li>암호화한 토큰과 IV를 저장하는 Proto DataStore 저장소</li>
</ul>
<p>웹 환경에선 그냥 헤더에 액세스 토큰 붙이고 관리도 세션 저장소에서 딸깍 하면 되는데 얘는 손이 왜 이렇게 많이 가는지 모르겠다. Android 바이너리를 이용해야만 테스트가 가능하기 때문에, 자원도 많이 먹는다. 여러모로 웹이 가볍다는 부분을 깨닫는 지점이다.</p>
<p>그래도 난 Android가 좋다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hilt와 함께 ViewModel 사용하기 - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/Hilt-ViewModel-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/Hilt-ViewModel-Jetpack-Compose</guid>
            <pubDate>Sun, 29 Sep 2024 13:03:14 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>더 정확히 말하자면, 디자인 패턴 중 MVVM(Model-View-ViewModel) 패턴을 구현하는 방법이다. Hilt를 끼지 않고 ViewModel을 쓸 수 있는 방법도 있는 것으로 알고 있으나, Hilt를 쓰는 게 생산성이나 정신건강 측면에서 이로울 것이다.</p>
<p>그리고 이 게시글은 <a href="https://velog.io/@i_meant_to_be/KSP-Hilt-Jetpack-Compose">KSP로 Hilt 적용하기 - Jetpack Compose</a> 게시글에서의 요구사항이 충족되지 않으면 적용할 수 없다. 프로젝트에 아직 Hilt를 적용하지 않았다면, Hilt부터 적용하고 나서 이 게시글을 보도록 하자.</p>
<h2 id="구현">구현</h2>
<h3 id="프로젝트-구조">프로젝트 구조</h3>
<p>프로젝트는 아래와 같은 구조를 가진다:</p>
<blockquote>
</blockquote>
<p>/프로젝트 루트/view/SampleView.kt
/프로젝트 루트/viewmodel/SampleViewModel.kt</p>
<p>&#39;SampleView.kt&#39; 파일은 구체적인 UI 구현이 담긴 페이지이고, &#39;SampleViewModel.kt&#39; 파일은 &#39;SampleView&#39;에 표시될 데이터를 관리하는 뷰 모델이다. 이런 식으로 각각의 페이지마다 뷰 모델을 하나씩 만들어주면 된다.</p>
<h3 id="viewmodel-작성">ViewModel 작성</h3>
<p>아래와 같이 ViewModel을 구현하자:</p>
<pre><code class="language-kotlin">@HiltViewModel
class SampleViewModel @Inject constructor(
    // 필요한 모델 추가
) : ViewModel() {
    // 변수
    private val _sampleText = mutableStateOf&lt;String&gt;(&quot;Sample text&quot;)

    // Getter
    val sampleText: State&lt;String&gt; = _sampleText

    // Setter
    fun setSampleText(newValue: String) {
        _sampleText.value = newValue
    }
}</code></pre>
<h4 id="hiltviewmodel">@HiltViewModel</h4>
<p>이 어노테이션은 Hilt에게 내가 이 뷰 모델을 Hilt를 통해 사용할 거라고 알려주는 역할을 한다. 추측컨대, 뷰 모델을 여러 개 만들면 문제가 생길 수 있으니 이 어노테이션을 지정해서 뷰 모델이 여러 번 요청되어도 싱글톤으로 생성된 단 하나의 뷰 모델만 전달하도록 제한하는 역할을 하는 게 아닌가 싶다.</p>
<h4 id="inject-constructor">@Inject constructor()</h4>
<p>필요하다면, 데이터베이스 등 접근해야 하는 모델을 이 생성자에 넣어주면 된다. Android의 로컬 데이터베이스 서비스인 Room을 예시로 들 수 있겠다. 만약 Room을 사용한다면, 아래처럼 생성자가 구현될 것이다:</p>
<pre><code class="language-kotlin">class SampleViewModel @Inject constructor(
    private val UserRepository: userRepository
) : ViewModel() {
    // ...
}</code></pre>
<p>위 코드에서 주의해서 볼 부분은, 뷰 모델을 통해 누군가가 외부 모델에 마음대로 접근하는 것을 막기 위해, 모델에 <code>private</code> 키워드를 달아 주었다는 점이다.</p>
<p>한편, 우리는 모델의 데이터를 안전히 관리하기 위해 뷰 모델을 사용하는 것인 만큼, UI에 표시될 데이터도 엄격하게 선언하여 관리할 필요가 있다. 위 코드를 보면 3가지 요소가 있는데, Raw 변수, Getter와 Setter가 그것이다.</p>
<h4 id="변수">변수</h4>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">private val _변수 이름 = mutableStateOf&lt;변수 타입&gt;(변수 값)</code></pre>
<p>변수는 위처럼 선언하는데, <code>private</code> 접근 제한자를 앞에 달아서 뷰 모델 외부에서 무단으로 변수 값에 접근할 수 없도록 막아준다. 그리고 <code>mutableStateOf()</code> 함수를 통해 상태(State)로 선언하여, 값이 바뀌었을 때 컴포저블이 추적하여 UI를 수정할 수 있게 한다.</p>
<p>추가로, <code>Int</code>나 <code>Float</code>와 같은 자료형은 <code>mutableIntStateOf()</code>, <code>mutableFloatStateOf()</code>와 같은 자체적인 상태 함수도 있으니 참고 바람.</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">var _변수 이름 by mutableStateOf(변수 값)
    private set</code></pre>
<p>위 방법도 가능하다. 이 코드는 변수 선언과 Getter를 합친 것이다. Setter의 경우에는 <code>private</code> 접근 제한자를 달아, 역시 뷰 모델 외부에서 마음대로 접근할 수 없도록 막았다. 그래서 만약 Setter가 필요하다면, 아래 적혀있는 대로 별도의 Setter 선언이 필요하다. </p>
<h4 id="getter">Getter</h4>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">val 변수 이름: State&lt;변수 타입&gt; = _변수 이름</code></pre>
<p>Getter는 위처럼 선언한다. 처음에, 우리는 <code>mutableStateOf()</code> 함수를 통해 <code>MutableState</code> 타입으로 변수를 선언했었다. 하지만 이 타입은 Mutable하기에 값의 변경이 자유자재로 가능하다. 따라서 변수를 <code>MutableState</code>가 아닌 값의 읽기만 가능한 <code>State</code>로 선언하여, 뷰 모델 외부에서는 값을 읽을 수만 있도록 제한한다.</p>
<h4 id="setter">Setter</h4>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">fun setSampleText(newValue: String) {
    _sampleText.value = newValue
}</code></pre>
<p>Setter는 위처럼 선언한다. 너무 뻔해서 더 할 얘기는 없는 코드.</p>
<h3 id="view-작성">View 작성</h3>
<p>본인 입맛과 프로젝트의 설계에 맞게 아래와 같이 View를 구현하자:</p>
<pre><code class="language-kotlin">@Composable
fun SampleView(
    viewModel: SampleViewModel = hiltViewModel() // ViewModel 주입
) {
    Text(text = viewModel.sampleText.value) // ViewModel의 데이터인 &#39;sampleText&#39;를 가져다 쓰기
}</code></pre>
<p>코드의 3번째 줄 <code>viewModel: SampleViewModel = hiltViewModel()</code>을 보자. 이 코드를 간단히 설명하면, Hilt에게 <strong>&quot;Hilt야, 내가 <code>hiltViewModel()</code>을 호출했으니 너는 내가 <code>viewModel: SampleViewModel</code> 코드를 통해 지정한 <code>SampleViewModel</code>을 의존성 주입을 통해 <code>SampleView</code>에 가져다줘!&quot;</strong>라고 하는 것과 똑같다.</p>
<p><code>hiltViewModel()</code> 함수를 뷰의 매개변수에 사용하면, Hilt는 알아서 <code>@HiltViewModel</code> 어노테이션이 달린 뷰 모델을 개발자가 요청한 뷰에 주입해주는 것이다.</p>
<p>필요한 데이터는 <code>viewModel.필요한 변수 및 메소드</code>의 형식으로 접근하여 사용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[KSP로 Hilt 적용하기 - Jetpack Compose ]]></title>
            <link>https://velog.io/@i_meant_to_be/KSP-Hilt-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/KSP-Hilt-Jetpack-Compose</guid>
            <pubDate>Fri, 13 Sep 2024 00:17:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/i_meant_to_be/post/2caf8a86-cc0c-4ec5-8339-264da084b796/image.png" alt=""></p>
<h3 id="개요">개요</h3>
<p>Hilt는 매번 적용할 때마다 너무 복잡해서 화가 난다. 그래서 Kotlin 2.0으로의 전환 절차를 정리할 겸 하여 Hilt까지 같이 정리했다.</p>
<p>사실 Hilt의 KSP 대응은 완전히 끝난 건 아닌데, 2.4 후반부 버전부터 알파 느낌으로 KSP 사용이 가능하다고 한다. 보통은 KAPT를 많이 쓰는 것 같던데, KSP가 컴파일 시 KAPT에 비해 성능이 잘 나오는 것으로 알고 있다.</p>
<p><a href="https://dagger.dev/dev-guide/ksp.html">이 링크</a>를 통해, Hilt에서 KSP를 사용하기 위한 요구 조건을 확인할 수 있다. 참고 바람.</p>
<h3 id="절차">절차</h3>
<h4 id="패키지-버전-카탈로그-갱신">패키지 버전 카탈로그 갱신</h4>
<p>먼저, 패키지 버전 카탈로그(libs.versions.toml)에 필요한 플러그인과 라이브러리의 버전을 명시해주어야 한다. 그 전에, 본인의 Kotlin 버전에 맞는 KSP 버전을 확인할 필요가 있다. <a href="https://github.com/google/ksp/releases">다음 링크</a>에 가서 확인해보도록 하자. 비교적 최근에 업데이트된 Kotlin 버전과 그에 대응되는 KSP 버전은 아래와 같다:</p>
<ul>
<li>Kotlin 2.0.10 - KSP 1.0.24</li>
<li>Kotlin 2.0.21 - KSP 1.0.26</li>
</ul>
<p>적절한 KSP 버전을 확인했다면, 아래와 같이 패키지 버전 카탈로그를 수정하자:</p>
<p>(참고로, 플러그인과 라이브러리의 버전까지 정확하게 따라할 필요는 없다. 이 글은 2024년 11월 7일에 갱신된 것으로, 이후 업데이트에 따라 얼마든지 버전이 달라질 수 있음을 미리 알린다.)</p>
<blockquote>
</blockquote>
<pre><code>[versions]
# Plugins
ksp = &quot;2.0.10-1.0.24&quot;
# Libraries
navigation = &quot;2.8.3&quot;
hilt = &quot;2.52&quot;
androidxHilt = &quot;1.2.0&quot;</code></pre><blockquote>
</blockquote>
<pre><code>[libraries]
# Hilt
androidx-navigation = { group = &quot;androidx.navigation&quot;, name = &quot;navigation-compose&quot;, version.ref = &quot;nav&quot; }
androidx-hilt-navigation-compose = { group = &quot;androidx.hilt&quot;, name = &quot;hilt-navigation-compose&quot;, version.ref = &quot;androidxHilt&quot; }
dagger-hilt-android = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android&quot;, version.ref = &quot;hilt&quot; }
dagger-hilt-android-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android-compiler&quot;, version.ref = &quot;hilt&quot;}
dagger-hilt-android-gradle-plugin = { module = &quot;com.google.dagger:hilt-android-gradle-plugin&quot;, version.ref = &quot;hilt&quot; }</code></pre><blockquote>
</blockquote>
<pre><code>[plugins]
kotlin-ksp = { id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;ksp&quot; }
dagger-hilt-android = { id = &quot;com.google.dagger.hilt.android&quot;, version.ref = &quot;hilt&quot;}</code></pre><h4 id="프로젝트-수준-buildgradlekts-갱신">프로젝트 수준 build.gradle.kts 갱신</h4>
<p>다음으로, 프로젝트 수준 build.gradle.kts(/build.gradle.kts)를 아래와 같이 수정한다:</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    // KSP 및 Hilt 플러그인 추가
    alias(libs.plugins.kotlin.ksp) apply false
    alias(libs.plugins.dagger.hilt.android) apply false
}
// Hilt Android Gradle Plugin 적용
buildscript {
    dependencies {
        classpath(libs.dagger.hilt.android.gradle.plugin)
    }
}</code></pre>
<h4 id="모듈-수준-buildgradlekts-갱신">모듈 수준 build.gradle.kts 갱신</h4>
<p>마지막으로, 모듈 수준 build.gradle.kts(/app/build.gradle.kts)를 아래와 같이 수정한다. Hilt 컴파일러만 <code>ksp</code>로 선언하는 부분에 유의하자:</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">plugins {
    // KSP와 Hilt 플러그인 추가
    alias(libs.plugins.kotlin.ksp)
    alias(libs.plugins.dagger.hilt.android)
}
dependencies {
    // 관련 플러그인들 추가
    implementation(libs.dagger.hilt.android)
    ksp(libs.dagger.hilt.android.compiler)
    implementation(libs.androidx.navigation.compose)
    implementation(libs.androidx.hilt.navigation.compose)
}</code></pre>
<p>수정이 끝났으면, Gradle sync를 한 번 진행해준다.</p>
<h4 id="소스에-hilt-관련-변경-사항-반영">소스에 Hilt 관련 변경 사항 반영</h4>
<p>가장 먼저, MainActivity.kt에 앱의 이름으로 클래스를 하나 만들고, <code>@HiltAndroidApp</code> 어노테이션을 달아 준다. 아래 코드를 참고:</p>
<pre><code class="language-kotlin">@HiltAndroidApp
class MyApp : Application() // import android.app.Application</code></pre>
<p>두 번째로, 앱의 진입점인 <code>MainActivity</code> 클래스에 <code>@AndroidEntryPoint</code> 어노테이션을 달아 준다. 아래 코드를 참고:</p>
<pre><code class="language-kotlin">@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MobileAppDevTheme {
                // 애플리케이션 구현...
            }
        }
    }
}</code></pre>
<p>마지막으로, AndroidManifest.xml 파일의 <code>application</code> 블럭 아래에 <code>android:name</code> 항목을 추가한 후, 값으로 MainActivity.kt에 맨 처음 새로 추가했던 클래스 이름을 넣어준다. </p>
<p>그리고 반드시 클래스 이름 앞에 온점(.)을 적어두어야 한다. 예를 들어, <code>@HiltAndroidApp</code> 어노테이션이 붙은 클래스의 이름이 <code>MyApp</code>이라면, AndroidManifest.xml에는 <code>android:name=&quot;.MyApp&quot;</code>으로 작성해야 한다.</p>
<p>아래를 참고:</p>
<pre><code>&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;

    &lt;application
        android:name=&quot;.MyApp&quot;
    &lt;/application&gt;
&lt;/manifest&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 2.0으로 전환하기 - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/Migrate-To-Kotlin-2-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/Migrate-To-Kotlin-2-Jetpack-Compose</guid>
            <pubDate>Thu, 12 Sep 2024 04:09:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/i_meant_to_be/post/52f2abf1-339f-4959-9048-cda6e33ac252/image.png" alt=""></p>
<h3 id="개요">개요</h3>
<p>Jetpack Compose 개발 시 Kotlin 2.0 - 정확히는 2.0.10 - 으로 전환하는 방법을 적어둔다. 생각보다 사소하게 할 게 많아서 까먹지 않게 정리해둔다.</p>
<p>2.0.0이 아니라 2.0.10을 기준으로 글을 쓰는 이유는 Kotlin + Compose의 2.0.0 버전에 치명적인 오류가 있기 때문이다.</p>
<p>다음 원문을 참고 바람:</p>
<blockquote>
</blockquote>
<p>It is strongly recommended that you update your multiplatform Compose app created with Kotlin 2.0.0 to version 2.0.10 or later. The Compose compiler 2.0.0 has an issue where it sometimes incorrectly infers the stability of types in multiplatform projects with non-JVM targets, <strong>which can lead to unnecessary (or even endless) recompositions.</strong></p>
<h3 id="절차">절차</h3>
<p><strong>.gitignore 파일 수정</strong>
Kotlin 2.0 이상은 빌드 과정에서 생성되는 파일이 프로젝트 루트의 .kotlin 디렉토리에 저장된다. 따라서 .gitignore 파일에 다음 한 줄을 추가해야 한다:</p>
<blockquote>
</blockquote>
<p><code>.kotlin/</code></p>
<p><strong>패키지 버전 카탈로그의 Kotlin 버전 수정</strong>
패키지 버전 카탈로그(libs.versions.toml)의 Kotlin 버전을 아래와 같이 2.0.10으로 바꾼다:</p>
<blockquote>
</blockquote>
<pre><code>[versions]
...
kotlin = &quot;2.0.10&quot;
...</code></pre><p><strong>패키지 버전 카탈로그에 Jetpack Compose 컴파일러 추가</strong>
패키지 버전 카탈로그의 plugins 목록에 아래와 같이 Jetpack Compose 컴파일러를 추가한다:</p>
<blockquote>
</blockquote>
<pre><code>[plugins]
android-application = { id = &quot;com.android.application&quot;, version.ref = &quot;agp&quot; }
kotlin-android = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }
compose-compiler = { id = &quot;org.jetbrains.kotlin.plugin.compose&quot;, version.ref = &quot;kotlin&quot; } // 추가해야 할 항목
...</code></pre><p><strong>build.gradle.kts에 Jetpack Compose 컴파일러 추가</strong>
프로젝트 및 모듈 수준의 build.gradle.kts 파일의 plugins 절 안에 아래와 같이 Jetpack Compose 컴파일러를 추가한다:</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.compose.compiler) // 추가해야 할 항목
    ... 
}</code></pre>
<p><strong>Kotlin 컴파일러 옵션 수정</strong>
모듈 수준의 build.gradle.kts의 kotlinOptions 절을 지우고, kotlin - compilerOptions로 대체한다. 이 때, 기존에 설정되어 있던 JvmTarget에 맞추어 수정해야 한다. </p>
<p>참고로 이걸 추가한 후에, 한 번 Gradle sync를 진행해 주는 것을 추천한다.</p>
<p>아래를 참고:</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">android {
    // 수정 전
    kotlinOptions {
        jvmTarget = &quot;1.8&quot;
    }
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">android {
    // 수정 후
    kotlin {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_1_8) // 기존의 jvmTarget과 버전을 맞춰주자
        }
    }
}</code></pre>
<p><strong>Jetpack Compose 컴파일러 옵션 수정</strong>
모듈 수준의 build.gradle.kts의 composeOptions 절을 지우고, 파일 루트에 composeCompiler 절을 추가한 후에, 필요에 따라 옵션을 지정한다.</p>
<p>참고로 Jetpack Compose 컴파일러에 적용 가능한 옵션은 <a href="https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compiler.html#compose-compiler-options-dsl">다음 링크</a>를 참고하여 확인해볼 수 있다.</p>
<p>아래를 참고:</p>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">// 수정 전
android {
    composeOptions {
        kotlinCompilerExtensionVersion = &quot;1.5.1&quot;
    }
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-kotlin">// 수정 후
android {
    ...
}
composeCompiler {
    enableStrongSkippingMode = true
    includeSourceInformation = true
}</code></pre>
<h3 id="참고-링크">참고 링크</h3>
<ul>
<li><a href="https://medium.com/@l2hyunwoo/kotlin-2-0%EC%9C%BC%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%98%EA%B8%B0-1742f294df51">Kotlin 2.0으로 마이그레이션하기 - HyunWoo Lee</a></li>
<li><a href="https://medium.com/mobile-innovation-network/migrate-to-kotlin-2-0-with-6-easy-steps-b41a6bf68184">Migrate to Kotlin 2.0 with 6 Easy Steps - Mayur Waghmare</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[동적 계획법: 연속된 행렬의 곱셈 (CMM)]]></title>
            <link>https://velog.io/@i_meant_to_be/Algorithm-04</link>
            <guid>https://velog.io/@i_meant_to_be/Algorithm-04</guid>
            <pubDate>Mon, 10 Apr 2023 16:38:39 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>여러 행렬을 연속해서 곱할 때, 가능한 순서는 굉장히 많다. 예를 들어, A, B, C, D, 총 4개의 행렬을 곱하는 경우, 다음의 조합이 가능하다:</p>
<blockquote>
</blockquote>
<p>(AB)(CD)
((AB)C)D
(A(BC))D
A((BC)D)
A(B(CD))</p>
<p>그리고 각 순서에 따라 필요한 곱셈 연산의 수도 크게 차이가 난다. 아래를 참고하자:</p>
<blockquote>
</blockquote>
<p>A = 20 * 2, B = 2 * 30, C = 30 * 12, D = 12 * 8 크기일 때,<br>
(AB)(CD) = 3k 회의 연산 필요
((AB)C)D = 8k 회의 연산 필요
(A(BC))D = 1k 회의 연산 필요
A((BC)D) = 10k 회의 연산 필요
A(B(CD)) = 3k 회의 연산 필요</p>
<p>따라서 주어진 배열들의 크기를 고려하여, 최대한 적은 연산 횟수를 갖는 곱셈 순서를 찾아내야 한다. 다만 지금은 DP를 활용하는 게 더 중요하므로, 곱셈 순서는 고려하지 않고 최소 연산 횟수를 계산하는 데 중점을 두고자 한다.</p>
<br>

<hr>
<h2 id="접근">접근</h2>
<p>동적 계획법은 보통 아래의 절차로 진행되는 경우가 많다:</p>
<blockquote>
</blockquote>
<ol>
<li>문제에서 재귀적 특성을 찾아 수학적으로 정의하기</li>
<li>코드로 변환하여 작은 문제부터 큰 문제까지 풀기</li>
</ol>
<p>그런데 사실 1번 과정이 제일 어려운 경우가 많다. 수학적이지 않아 보이는 문제나 상황을 수학적으로 변환해야 하기 때문이다. 그럴 때에는, 비교적 작은 n을 입력으로 하여 간단히 계산을 해 보고, 그 결과에서 규칙을 찾아보는 게 더 좋은 접근일 수도 있다.</p>
<blockquote>
</blockquote>
<p><strong>&gt; 0. 비교적 작은 문제를 직접 풀어 규칙 추측하기</strong></p>
<ol>
<li>문제에서 재귀적 특성을 찾아 수학적으로 정의하기</li>
<li>코드로 변환하여 작은 문제부터 큰 문제까지 풀기</li>
</ol>
<h3 id="의사-코드">의사 코드?</h3>
<h4 id="곱하는-행렬-사이의-거리가-1인-경우">곱하는 행렬 사이의 거리가 1인 경우:</h4>
<p>A0부터 A2까지 3개의 배열이 있을 때, A0 * A1은 가능한 순서의 수가 1개밖에 없으므로 DP 적용하지 않고 바로 연산한다.</p>
<h4 id="곱하는-행렬-사이의-거리가-2-이상인-경우">곱하는 행렬 사이의 거리가 2 이상인 경우:</h4>
<p>위에서 계산한 값을 바탕으로 연산한다.</p>
<p>A0부터 A2까지 3개의 배열이 있을 때, (A0 * A1) * A2와 A0 * (A1 * A2)의 2가지 경우의 수가 있다. 이 때 위에서 계산한 (A0 * A1)과 (A1 * A2)의 연산 수에, (A0 * A1)을 계산해 나온 행렬과 A2를 곱하는 데 필요한 연산 수까지 추가로 더해주어야 한다.</p>
<p>예를 들어, A0 = 5 * 2, A1 = 2 * 3, A2 = 3 * 4라고 하자. (A0 * A1)에는 5 * 2 * 3 = 30회의 연산이 필요하다. 그리고 계산 결과는 5 * 3의 행렬이다. 우리는 이 행렬에 3 * 4 크기의 행렬을 또 곱해야 한다. 따라서 우리는 5 * 3 * 4 = 60회의 연산을 추가로 실행해야 한다. 최종적으로는 30회 + 60회 = 90회의 연산이 필요하다는 말.</p>
<br>

<hr>
<h2 id="python-코드">Python 코드</h2>
<h3 id="동적-계획법">동적 계획법</h3>
<pre><code class="language-py">import copy

def cmm_dp(arr: list) -&gt; int:
    length = len(arr) - 1
    matrix = [copy.deepcopy([0] * length) for _ in range(length)]
    step = 1

    while step &lt; length:
        for n in range(step, length):
            if (step == 1):
                matrix[n][n - 1] = arr[n - 1] * arr[n] * arr[n + 1]
            else:
                x = n
                y = n - step

                p = matrix[x - 1][y] + arr[y] * arr[x] * arr[x + 1]
                q = matrix[x][y + 1] + arr[y] * arr[y + 1] * arr[x + 1]
                matrix[x][y] = min(p, q)
        step += 1

    return matrix[-1][0]</code></pre>
<h4 id="행렬의-크기-나타내기">행렬의 크기 나타내기</h4>
<p>나는 1차원 배열에 행렬 크기를 저장했다.</p>
<p>배열 An의 크기를 d0 * d1이라고 했을 때, A0 = d0 * d1, A1 = d1 * d2, ..., 이런 식으로 규칙을 이룰 것이다. 이 때, 굳이 d1을 두 번 저장할 필요가 없다고 생각해서 그냥 1차원 배열에 d0, d1, ..., 이런 식으로 저장했다.</p>
<p><img src="https://velog.velcdn.com/images/i_meant_to_be/post/bce4130b-cfcd-481f-8ff1-7d2af37bab17/image.png" alt=""></p>
<p>표에 적으면 이런 식으로 나오는데, 예를 들어 배열 A0의 크기는 5 * 2이고, 배열 A1의 크기는 2 * 3이고, ..., 배열 A5의 크기는 7 * 8이고... 이런 식으로 이해하면 된다.</p>
<br>

<hr>
<h2 id="시간-복잡도">시간 복잡도</h2>
<h3 id="무차별-대입">무차별 대입</h3>
<p>행렬 A1, A2, ..., An이 존재할 때, 이 행렬들을 곱할 때 가능한 순서의 개수를 Tn이라고 하자. 이 때, A1 * (A2 * ... * An)으로 행렬을 나누면, (A2 * ... * An)에서 가능한 순서의 개수는 Tn-1이다. 또한, (A1 * ... * An-1) * An으로 행렬을 나눠도 가능한 순서의 개수는 Tn-1이다. 따라서, Tn &gt;= Tn-1 + Tn-1 = 2Tn-1이다.</p>
<p>한편, 행렬이 두 개 있을 때 가능한 순서의 개수는 오직 1개밖에 없다. 따라서 T2 = 1이다.</p>
<p>이상의 두 가지 조건을 바탕으로 계산하면, Tn &gt;= 2^n-2이라는 부등식이 도출된다. 따라서 연속된 행렬의 곱셈에서 연산의 수가 가장 적은 순서를 무차별 대입법으로 찾아내는 경우에 대해서, O(2^n)의 시간 복잡도가 필요하다.</p>
<h3 id="동적-계획법-1">동적 계획법</h3>
<p>자고 일어나서 정리하도록 하자...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동적 계획법: 이항 계수]]></title>
            <link>https://velog.io/@i_meant_to_be/Algorithm-03</link>
            <guid>https://velog.io/@i_meant_to_be/Algorithm-03</guid>
            <pubDate>Mon, 10 Apr 2023 07:32:47 GMT</pubDate>
            <description><![CDATA[<h2 id="접근">접근</h2>
<p>분할 정복은 큰 문제를 작은 문제로 나눌 수 있는 경우에 적용하면 좋고, 동적 계획법(Dynamic programming)은 작은 문제의 답을 큰 문제 해결에 재활용할 수 있는 경우에 적용하면 좋다. 그래서 동적 계획법을 상향식(Bottom-up) 프로그래밍이라고 부르기도 한다.</p>
<p>그 대표적인 예시가 이항 계수 계산이다. (n, k)의 이항 계수는 (n - 1, k - 1) + (n - 1, k)의 합이기 때문에, 작은 문제부터 계산해서 큰 문제의 답을 구하기에는 최적화된 문제다.</p>
<p>(단, 이 글에서는 n &gt;= k인 경우만 따지도록 한다.)</p>
<br>

<hr>
<h2 id="python-코드">Python 코드</h2>
<h3 id="재귀-recursion">재귀 (Recursion)</h3>
<pre><code class="language-py">def binomial_coefficient_recursion(n: int, k: int) -&gt; int:
    if (n == k) or (k == 0): return 1
    else:
        left = binomial_coefficient_recursion(n - 1, k - 1)
        right = binomial_coefficient_recursion(n - 1, k)
        return left + right</code></pre>
<p>쉽다. 이항 계수의 계산식을 그대로 코드로 변환하면 된다. 그러나 재귀 특성상 아마 시간이 굉장히 오래 걸릴 것이다.</p>
<h3 id="동적-계획법-dynamic-programming">동적 계획법 (Dynamic programming)</h3>
<pre><code class="language-py">import copy

def binomial_coefficient_dp(n: int, k: int) -&gt; int:
    if (n == k) or (k == 0): return 1
    else:
        matrix = [copy.deepcopy([0] * (n + 1)) for _ in range(k + 1)]
        x = 0
        y = 0

        while True:
            to_next_line = False

            # 값 채우기
            if (x == y):
                matrix[x][y] = 1
                to_next_line = True
            elif (x == 0):
                matrix[x][y] = 1
            elif (x == k):
                matrix[x][y] = matrix[x - 1][y - 1] + matrix[x][y - 1]
                to_next_line = True
            else: 
                matrix[x][y] = matrix[x - 1][y - 1] + matrix[x][y - 1]

            # 이동
            if (x == k) and (y == n):
                break
            elif (to_next_line):
                x = 0
                y += 1
            else:
                x += 1

        return matrix[k][n]</code></pre>
<p>주요 포인트가 몇 개 있다:</p>
<h4 id="깊은-복사-copy-패키지의-deepcopy-함수">깊은 복사: <code>copy</code> 패키지의 <code>deepcopy()</code> 함수</h4>
<p>이항 계수 계산을 위해 행렬을 생성하면서, 깊은 복사를 제공하는 <code>copy</code> 패키지의 <code>deepcopy()</code> 함수를 사용했다.</p>
<p>Python의 리스트 자료형은 <code>copy()</code> 메소드를 제공하긴 하는데, 이건 얕은 복사라서 이번 문제에서는 사용할 수 없다. <code>copy()</code> 메소드를 쓸 경우, 첫 번째 세로줄의 내용을 바꾸면 건들지 않은 두 번째, 세 번째, ...등 모든 세로줄의 내용도 동일하게 바뀌는 문제가 발생한다.</p>
<h4 id="메모리-공간-줄이기">메모리 공간 줄이기</h4>
<p>동작을 1) 행렬 칸의 값을 계산하고 2) 다음 좌표를 계산하는 두 단계로 구성했다.</p>
<p>사실 단순하게 하려면, 그냥 n * n 크기의 정사각형 배열을 생성하고 x와 y가 같을 때 다음 줄로 넘어가는 식으로 구현해도 된다. 근데 그러면 1) 답을 구할 때 필요하지 않은 더미 데이터에 대해서도 계산이 진행되어야 하고 2) 계산된 더미 데이터가 행렬에서 차지하는 공간으로 인해 메모리 공간도 조금 더 많이 쓰게 된다.</p>
<p>소모를 좀 줄이기 위해서 행렬 값 계산과 다음 좌표 계산을 조건문으로 세세하게 구현해 효율성을 높였다.</p>
<br>

<hr>
<h2 id="시간-복잡도">시간 복잡도</h2>
<p>n = 5, k = 3일 때, 연산 횟수는 아래 표와 같다:
<img src="https://velog.velcdn.com/images/i_meant_to_be/post/3bc5728b-08a8-4015-9440-597005de85f3/image.png" alt=""></p>
<p>요악하면 아래 두 개를 더한 값이 연산 횟수다:</p>
<ul>
<li>1부터 k + 1을 더한 값</li>
<li>n - k에 k + 1을 곱한 값</li>
</ul>
<p>W(n, k) = (k = 1)(k + 2)/2 + (n + k)(k + 1)이고, 전개하면 k의 제곱과 nk 항이 존재한다. n이 k보다 크거나 같기 때문에 k의 제곱은 무시되고, W(n, k) = O(nk)이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[분할 정복: 퀵 정렬]]></title>
            <link>https://velog.io/@i_meant_to_be/Algorithm-02</link>
            <guid>https://velog.io/@i_meant_to_be/Algorithm-02</guid>
            <pubDate>Tue, 04 Apr 2023 16:03:15 GMT</pubDate>
            <description><![CDATA[<h2 id="접근">접근</h2>
<p>&quot;[문제 분할 - 분할된 문제 해결 - 부분해 병합]의 3단계로 이루어지는 분할 정복에서, 병합 과정을 빼고 정렬할 수 있을까?&quot; 라는 물음으로부터 만들어진 알고리즘이란다.</p>
<p>병합 정렬에서는 배열을 병합하는 과정에 비교적 많은 코드가 필요했는데, 퀵 정렬에서는 병합 과정이 없어진 만큼 그 부하가 분할 과정에 집중된다.</p>
<p>대충 의사 코드는 아래와 같다:</p>
<pre><code>퀵 정렬 (배열, 시작 인덱스, 끝 인덱스) {
    만약 시작 인덱스 &lt; 끝 인덱스일 경우 {
        피봇 인덱스를 선언, 0으로 초기화
        피봇을 기준으로 피봇보다 작은 배열, 피봇보다 큰 배열로 분할
        퀵 정렬 (배열, 시작 인덱스, 피봇 인덱스 - 1)
        퀵 정렬 (배열, 피봇 인덱스 + 1, 끝 인덱스)
    }
}

퀵 분배 (배열, 시작 인덱스, 끝 인덱스) -&gt; 피봇 인덱스 반환 {
    배열의 시작 인덱스를 피봇으로 설정
    현재 탐색 인덱스를 시작 인덱스 + 1로 설정
    다음 교체 인덱스를 시작 인덱스 + 1로 설정

    // 첫 번째는 피봇이라서 두 번째부터 탐색을 진행함
    배열의 두 번째 인덱스부터 끝까지 반복 {
            만약 배열의 현재 탐색 인덱스에 위치한 값 &lt; 피봇이면 {
                현재 탐색 인덱스와 다음 교체 인덱스끼리 값을 변경
                다음 교체 인덱스에 1을 더함
            }
        현재 탐색 인덱스에 1을 더함
    }
    배열이 나누어졌으면, 피봇보다 작은 숫자들 뒤에 피봇을 재배치
    피봇 인덱스를 반환
}</code></pre><br>

<hr>
<h2 id="rust-코드">Rust 코드</h2>
<p>나중에 시간이 나면 추가해보도록 하겠다. 나는 시간 빌 게이츠가 아니기에...</p>
<br>

<hr>
<h2 id="python-코드">Python 코드</h2>
<h3 id="재귀-recursion">재귀 (Recursion)</h3>
<h4 id="정렬-함수">정렬 함수</h4>
<pre><code class="language-py">def quicksort(arr: list, low: int, high: int):
    if (low &lt; high):
        pivot_index = quicksort_partition(arr, low, high)
        quicksort(arr, low, pivot_index)
        quicksort(arr, pivot_index + 1, high)</code></pre>
<h4 id="분할-함수">분할 함수</h4>
<pre><code class="language-py">def quicksort_partition(arr: list, low: int, high: int) -&gt; int:
    def __swap(arr: list, a: int, b: int):
        temp = arr[a]
        arr[a] = arr[b]
        arr[b] = temp

    pivot = arr[low]
    current_index = low + 1
    to_fill_index = low + 1

    while (current_index &lt; high):
        if (arr[current_index] &lt; pivot):
            __swap(arr, current_index, to_fill_index)
            to_fill_index += 1
        current_index += 1

    arr.insert(to_fill_index - 1, arr.pop(low))
    return to_fill_index - 1</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[분할 정복: 병합 정렬]]></title>
            <link>https://velog.io/@i_meant_to_be/Algorithm-01</link>
            <guid>https://velog.io/@i_meant_to_be/Algorithm-01</guid>
            <pubDate>Sun, 02 Apr 2023 13:18:03 GMT</pubDate>
            <description><![CDATA[<h2 id="접근">접근</h2>
<p>분할 정복의 접근 방식은 아래와 같다:</p>
<ul>
<li>하나의 문제를 하나 이상의 그보다 작은 크기의 문제로 분할한다.</li>
<li>분할된 각각의 작은 문제를 해결한다.</li>
<li>(필요할 경우) 각각의 작은 문제에서 도출된 답을 합친다.</li>
</ul>
<p>병합 정렬에 이상의 접근 방식을 적용하면:</p>
<ul>
<li>입력된 배열을 2개의 부분 배열로 분할한다.</li>
<li>양 쪽의 배열을 각각 정렬한다.</li>
<li>정렬된 양 배열을 합친다.</li>
</ul>
<p>이를 바탕으로 간단하게 의사 코드를 작성하면:</p>
<pre><code>병합 정렬(배열) {
    만약 배열 길이가 2보다 작을 경우 {
        (길이가 1인 배열은 사실상 정렬된 상태이므로) 입력된 배열을 그대로 반환
    } 그렇지 않을 경우 {
        좌측 배열에 0부터 mid까지를 복사
        우측 배열에 mid + 1부터 배열의 끝까지를 복사

        병합 정렬(좌측 배열)
        병합 정렬(우측 배열)

        좌측 배열과 우측 배열을 합친 결과물을 반환
    }
}</code></pre><br>

<hr>
<h2 id="rust-코드">Rust 코드</h2>
<h3 id="재귀-recursion">재귀 (Recursion)</h3>
<h4 id="정렬-함수">정렬 함수</h4>
<pre><code class="language-rust">fn mergesort(vec: &amp;Vec&lt;i32&gt;) -&gt; Vec&lt;i32&gt; {
    if vec.len() &lt; 2 {
        // 배열 크기가 1이면, 그대로 반환
        // 입력은 &amp;Vec&lt;i32&gt;로 받았는데 반환형이 참조 변수가 아니므로,
        // to_vec() 메소드를 통해 &amp;Vec&lt;i32&gt;를 Vec&lt;i32&gt;로 변환해서 반환한다
        vec.to_vec()
    } else {
        // 배열 크기가 2 이상일 경우
        // 배열을 2개로 나누고 각각 정렬 후 병합해서 반환
        let mid = vec.len() / 2;

        let left = mergesort(&amp;vec[0..mid].to_vec());
        let right = mergesort(&amp;vec[mid..].to_vec());

        merge(&amp;left, &amp;right)
    }
}</code></pre>
<h4 id="병합-함수">병합 함수</h4>
<pre><code class="language-rust">fn merge(left: &amp;Vec&lt;i32&gt;, right: &amp;Vec&lt;i32&gt;) -&gt; Vec&lt;i32&gt; {
    let mut temp: Vec&lt;i32&gt; = Vec::new();

    let mut left_counter = 0;
    let mut right_counter = 0;

    // 배열의 첫 번째 원소를 비교해가며, 작은 숫자부터 반환할 벡터에 집어넣음
    while left_counter &lt; left.len() &amp;&amp; right_counter &lt; right.len() {
        if left[left_counter] &lt; right[right_counter] {
            temp.push(left[left_counter]);
            left_counter += 1;
        } else {
            temp.push(right[right_counter]);
            right_counter += 1;
        }
    }

    // 좌측 배열에만 숫자가 남아있을 경우
    if left_counter &lt; left.len() {
        while left_counter &lt; left.len() {
            temp.push(left[left_counter]);
            left_counter += 1;
        }
    }

    // 우측 배열에만 숫자가 남아있을 경우
    if right_counter &lt; right.len() {
        while right_counter &lt; right.len() {
            temp.push(right[right_counter]);
            right_counter += 1;
        }
    }

    // 병합된 배열을 반환
    temp
}</code></pre>
<h4 id="여담">여담</h4>
<ul>
<li>코드 출처는 <a href="https://mohitkarekar.com/posts/2020/merge-sort-in-rust/">여기</a>. 혼자 힘으로 해 보려고 했는데 Rust에 익숙하지 않아서 좀 무리였다. 거의 바꾼 건 없다. <code>+=</code> 연산자 정도?</li>
<li>처음에는 배열을 쓰려고 했는데, 배열은 첫 번째 원소를 Pop하기에는 부적절한 자료형이라는 걸 알고 벡터로 계획을 바꿨다. 근데 벡터도 후방에서의 Pop만 지원하고 전방에서는 불가능해서, 다음에는 데크로 바꿨다. 코드를 대충 쓰고 데크의 첫 번째 원소를 반환하는 <code>first()</code> 메소드를 쓰려고 보니, 반환형이 <code>Option</code>이더라. 아직 <code>Option</code>을 다룰 줄 몰라서(예외 처리?를 할 줄 몰라서) 최종적으로 여기서 막혔다. 그래서 구글링으로 위 링크를 참고하게 됨...</li>
<li>교수님은 재귀 말고도 DP 등의 다른 방법으로도 구현이 가능하다고 하셨었다. 다른 테크닉으로도 병합 정렬을 구현할 수 있게 되면, 나중에 이 글에 코드를 추가해야 겠다.</li>
<li>Kotlin 배웠을 때 처럼, 일단 구글 보면서 몸으로 부딪히면 그 과정에서 익히는 게 분명 있을 거다. 배열의 슬라이싱이 그렇다. 처음은 항상 어려운 게 맞으니까 부족한 것 같아도 기 죽지 말자. 중간고사 보고 러스트 튜토리얼 조금 더 파 봐야겠다...</li>
</ul>
<br>

<hr>
<h2 id="python-코드">Python 코드</h2>
<h3 id="재귀-recursion-1">재귀 (Recursion)</h3>
<h4 id="정렬-함수-1">정렬 함수</h4>
<pre><code class="language-py">def mergesort_sort(arr: list) -&gt; list:
    if (len(arr) &lt; 2):
        return arr
    else:
        mid = len(arr) // 2
        left = mergesort_sort(arr[0:mid])
        right = mergesort_sort(arr[mid:])
        return mergesort_merge(left, right)</code></pre>
<h4 id="병합-함수-1">병합 함수</h4>
<pre><code class="language-py">def mergesort_merge(arr1: list, arr2: list):
    temp = []

    while (len(arr1) and len(arr2)):
        if (arr1[0] &lt; arr2[0]): temp.append(arr1.pop(0))
        else: temp.append(arr2.pop(0))

    if (len(arr1)): temp.extend(arr1)
    if (len(arr2)): temp.extend(arr2)
    return temp

if __name__ == &quot;__main__&quot;:
    arr = [15, 22, 13, 27, 12, 10, 20, 25]
    print(mergesort_sort(arr))</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[분할 정복: 이진 탐색]]></title>
            <link>https://velog.io/@i_meant_to_be/Algorithm-00</link>
            <guid>https://velog.io/@i_meant_to_be/Algorithm-00</guid>
            <pubDate>Sun, 02 Apr 2023 08:34:19 GMT</pubDate>
            <description><![CDATA[<h2 id="접근">접근</h2>
<p>분할 정복의 접근 방식은 아래와 같다:</p>
<ul>
<li>하나의 문제를 하나 이상의 그보다 작은 크기의 문제로 분할한다.</li>
<li>분할된 각각의 작은 문제를 해결한다.</li>
<li>(필요할 경우) 각각의 작은 문제에서 도출된 답을 합친다.</li>
</ul>
<p>이를 이진 탐색에 적용해보면:</p>
<ul>
<li>입력된 배열을 2개로 분할한다.</li>
<li>분할된 배열에서 탐색을 진행하고, 탐색에 실패하면 다시 배열을 2개로 분할한다.</li>
</ul>
<p>이진 탐색에서는 분할된 2개의 배열 중 1개의 배열만 취하므로, 해답 도출을 위해 답을 합치는 절차는 생략해도 된다.</p>
<br>

<hr>
<h2 id="rust-코드">Rust 코드</h2>
<h3 id="반복-iteration">반복 (Iteration)</h3>
<pre><code class="language-rust">fn bs_iteration(arr: &amp;[i32], target: &amp;i32) -&gt; Option&lt;usize&gt; {
    let length = arr.len();

    let mut mid = length / 2;
    let mut low = 0;
    let mut high = length - 1;
       let mut current = arr[mid];

    while low &lt;= high { match current.cmp(&amp;target) {
            // target과 current가 같을 경우
            std::cmp::Ordering::Equal =&gt; return Some(mid),

            // current가 target 좌측에 있을 경우 (low &lt; current &lt; target &lt; high)
            std::cmp::Ordering::Less =&gt; low = mid + 1,

            // current가 target 우측에 있을 경우 (low &lt; target &lt; current &lt; high)
            std::cmp::Ordering::Greater =&gt; high = mid - 1, 
        }

        mid = (low + high) / 2;
        current = arr[mid];
    }

    // 탐색에 실패한 경우
    return None;
}</code></pre>
<br>

<h4 id="주목할-만한-코드">주목할 만한 코드</h4>
<pre><code class="language-rust">let mut high = length - 1;</code></pre>
<ul>
<li>배열의 인덱스는 0부터 시작하지만 length는 길이를 그대로 반환하기 때문에, high에서 1을 빼 주었다.</li>
</ul>
<br>

<pre><code class="language-rust">match current.cmp(&amp;target) {
    std::cmp::Ordering::Equal =&gt; return Some(mid),
    std::cmp::Ordering::Less =&gt; low = mid + 1,
    std::cmp::Ordering::Greater =&gt; high = mid - 1,
}</code></pre>
<ul>
<li><code>cmp()</code> 함수는 기준이 함수를 부르는 변수인 것으로 보인다. 이 코드에서 Less의 의미는 함수를 부른 <code>current</code>가 <code>&amp;target</code>에 비해 작은 경우를 의미하는 듯함.</li>
<li>이게 Rusty한 접근이라고 어디서 봤다. 이유는 잘 모르겠음.</li>
</ul>
<br>

<pre><code class="language-rust">fn binary_search() -&gt; Option&lt;usize&gt; {
    // ...
    return None;
}</code></pre>
<ul>
<li>Rust 공식 문서는 <code>Option</code>에 대해 아래와 같이 설명하고 있다:<blockquote>
<p>Type <code>Option</code> represents an optional value: every <code>Option</code> is either <code>Some</code> and contains a value, or <code>None</code>, and does not. <code>Option</code> types are very common in Rust code, as they have a number of uses:<br>(생략)...<br><strong>- Return value for otherwise reporting simple errors, where <code>None</code> is returned on error</strong></p>
</blockquote>
</li>
</ul>
<br> 

<hr>
<h3 id="재귀-recursion">재귀 (Recursion)</h3>
<pre><code class="language-rust">fn bs_recursion(arr: &amp;[i32], target: i32, low: usize, high: usize) -&gt; Option&lt;usize&gt; {
    let mid = (low + high) / 2;
    let current = arr[mid];

    if low &gt; high {
        return None;
    } else {
        match current.cmp(&amp;target) {
            std::cmp::Ordering::Equal =&gt; return Some(mid),
            std::cmp::Ordering::Less =&gt; bn_recursion(&amp;arr, target, mid + 1, high), 
            std::cmp::Ordering::Greater =&gt; bn_recursion(&amp;arr, target, low, mid - 1)
        }
    }
}</code></pre>
<br>

<h4 id="주목할-만한-코드-1">주목할 만한 코드</h4>
<ul>
<li>주목할 만한 코드라기보다는... <code>std::cmp::Ordering</code>에서 제공하는 항목이 <code>Less</code>, <code>Greater</code>, <code>Equal</code> 이렇게 셋 뿐이다. 이상이나 이하에 대한 표현이 없다는 말. 원래는 조건문을 쓰지 않고 <code>low.cmp(&amp;high)</code>로 &#39;Rusty&#39;하게 코드를 짜 보려고 했는데, <code>Ordering</code>으로는 이상과 이하를 표현할 수 없다는 걸 알고 다시 조건문으로 돌아왔다. </li>
</ul>
<br>

<hr>
<h2 id="python-코드">Python 코드</h2>
<h3 id="반복-iteration-1">반복 (Iteration)</h3>
<pre><code class="language-py">def binary_search_iteration(arr: list, target: int) -&gt; int:
    low = 0
    high = len(arr)
    mid = (low + high) // 2

    if (low &gt; high): return -1
    else:
        while (True):
            if (target == arr[mid]): return mid
            elif (target &lt; arr[mid]): high = mid - 1
            else: low = mid + 1
            mid = (low + high) // 2
</code></pre>
<h3 id="재귀-recursion-1">재귀 (Recursion)</h3>
<pre><code class="language-py">def binary_search_recursion(arr: list, target: int, low: int, high: int) -&gt; int:
    mid = (low + high) // 2

    if (low &gt; high): return -1
    else:
        if (target == arr[mid]): return mid
        elif (target &lt; arr[mid]): return binary_search_recursion(arr, target, low, mid - 1)
        else: return binary_search_recursion(arr, target, mid + 1, high)</code></pre>
<h4 id="여담">여담</h4>
<p>파이썬은 진짜 쉽다... Rust 붙잡고 있다간 내 성적이 날아갈 거 같아서 일단 Python으로 달리고, 시간 날 때나 좀 공부해보자. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[01. 기초 - Rust]]></title>
            <link>https://velog.io/@i_meant_to_be/01-Rust</link>
            <guid>https://velog.io/@i_meant_to_be/01-Rust</guid>
            <pubDate>Tue, 14 Feb 2023 17:04:33 GMT</pubDate>
            <description><![CDATA[<h2 id="변수와-상수">변수와 상수</h2>
<h3 id="일반-변수">일반 변수</h3>
<pre><code class="language-rust">let mut val: i32 = 1239;</code></pre>
<p>변수 할당은 <code>let</code>으로 가능하고, <code>mut</code>로 가변 여부를 지정할 수 있다.</p>
<p>또한 변수 이름 뒤에 콜론과 함께 자료형을 입력하여 변수의 자료형을 명시적으로 지정해줄 수 있다. 다만 대부분의 상황에서 Rust 컴파일러가 알아서 자료형을 추측하기 때문에 보통은 생략하고, 문자열을 파싱하는 등 다양한 자료형이 올 수 있는 애매한 상황에서만 명시적으로 지정해줘도 된다.</p>
<h3 id="상수">상수</h3>
<pre><code class="language-rust">const THIS_IS_CONSTANT_VALUE: f64 = 1929.2323;
const THIS_IS_CONSTANT_VALUE: f64 = 3.14 * 23123;</code></pre>
<p>상수는 <code>const</code>로 가능하고, Rust에서는 대문자 + 밑줄(_)을 활용하여 상수 이름을 정한다. 그리고 일반 변수와 다르게 반드시 상수의 자료형을 명시적으로 정의해주어야 한다! 또한 상수는 런타임에서 연산이 불가능하다. 굳이 상수에 식을 써야겠다면, 사칙연산 같은 가장 기초적인 연산만 사용할 수 있다는 듯.</p>
<h3 id="shadowing">Shadowing</h3>
<pre><code class="language-rust">let a = 6;
let a = 7;</code></pre>
<p>저번 글에서 간단히 짚고 넘어갔기 때문에 뭐... 굳이 더 설명할 필요는 없을 것 같다.</p>
<br>

<hr>
<h2 id="자료형">자료형</h2>
<p>Rust의 자료형에는 단일 값을 받는 Scalar 자료형과 여러 값을 하나로 묶는 Compound 자료형이 있다고 한다. Scalar에는 정수, 소수(부동소수점), 부울, 문자가 존재하고, Compound에는 튜플과 배열이 있다.</p>
<h3 id="정수">정수</h3>
<pre><code class="language-rust">// 정수의 다양한 표현
let dec = 1000;
let hex = 0xff;
let oct = 0o77;
let bin = 0b11;

// 언더바 사용
let easy_to_read = 1_024_768; // 1,024,768</code></pre>
<p>8, 16, 32, 64, 128 비트의 Signed, Unsigned 정수형을 제공한다. 앞에 부호 여부를 쓰고 그 뒤에 비트 수를 붙여주면 된다. <code>u32</code>, <code>i32</code>, ..., 이런 식으로. 추가로 <code>isize</code>, <code>usize</code>의 경우 시스템의 최대 메모리 공간에 맞추는 듯하다. 32 비트 시스템의 경우는 32 비트로, 64 비트 시스템의 경우는 64 비트로. 추가로 <code>byte</code>라는 자료형도 있던데, 이거는 아스키 코드 말하는 것 같다.</p>
<p>당연히 진법도 적용 가능하다. 그리고 신기한 점으로 보통 사람들이 숫자를 쓸 때 읽기 쉽게 쉼표를 중간에 붙이곤 하는데(1,024,768처럼), Rust에서는 쉼표의 역할을 하는 밑줄을 제공해준다. 근데 실제로 자주 쓰진 않을 듯.</p>
<pre><code class="language-rust">let mut a = b&#39;l&#39;;
a = a.</code></pre>
<p>그리고 정수 연산 중 발생하는 예외 처리를 위한 다양한 함수가 있다고 한다. <code>wrapping_add</code>나 <code>overflow_add</code> 이런 것들... <code>wrapping_add</code>를 예로 들면 자료형이 받아낼 수 있는 최대값 이상이 연산될 경우, 그 결과값에 2의 보수법을 적용한 값을 결과로 반환한다고 한다. 다만 이거는 오류를 내지 않기 위한 수단일 뿐, 실제 프로그램 돌아갈 땐 의도되지 않은 결과가 발생할 게 뻔하므로 예외 처리를 빡세게 할 수 있도록 하자.</p>
<h3 id="소수">소수</h3>
<pre><code class="language-rust">let a = 3.14; // f64 </code></pre>
<p><code>f32</code>, <code>f64</code>의 두 가지 자료형을 제공한다고 한다. 타입 지정 안 하고 소수를 쓸 경우, 기본값은 <code>f64</code>.</p>
<p>또한 얘는 Signed밖에 없다.</p>
<h3 id="숫자-자료형의-연산">숫자 자료형의 연산</h3>
<p>더하기, 빼기, 나누기, 곱하기, 나머지 연산 다 지원한다.</p>
<h3 id="부울">부울</h3>
<p><code>true</code> 그리고 <code>false</code>.</p>
<p>이건 뭐 코드 좀 만져본 사람이면 다 인정하는 거라.</p>
<h3 id="문자-char">문자 (<code>char</code>)</h3>
<pre><code class="language-rust">let character = &#39;a&#39;;</code></pre>
<p>4 바이트의 크기를 갖는 유니코드 형식 문자다. 이러한 특징으로 인해 한국어나 이모티콘까지 모두 <code>char</code>에 해당한다.</p>
<h3 id="튜플">튜플</h3>
<pre><code class="language-rust">// 선언
let tup = (1, 3.1415, &quot;Alpha&quot;);

// 튜플 내 값에 접근
println!(&quot;{}&quot;, tup.0);</code></pre>
<p>Python에서 보던 그거다. 위 코드처럼 소괄호에 쉼표를 더해 선언할 수 있고, 각자 다른 자료형끼리도 하나의 튜플로 묶을 수 있다.</p>
<p>튜플 내부 값에 대한 접근은 <code>tuple.n</code>의 형태로 한다. 예를 들어 <code>tuple</code>의 10 번째 값을 확인하고 싶다면 <code>tuple.9</code>라고 써 주면 된다. (인덱스가 0부터 시작하기 때문에 9로 표기)</p>
<pre><code class="language-rust">// 튜플 값의 수정
let mut a = (1, 3, 4);
println!(&quot;{}&quot;, a.0);

a.0 = 3;
println!(&quot;{}&quot;, a.0);</code></pre>
<p>다만 값의 수정이 불가능한 Python과 달리, Rust에서는 <code>mut</code>를 붙여 선언할 경우 값의 수정이 가능한 것으로 보인다. 위 코드는 컴파일 잘 되고 값도 바뀐 값으로 출력된다.</p>
<pre><code class="language-rust">// 튜플 값의 분해
let tup = (1, 2, 3);
let (a, b, c) = tup</code></pre>
<p>Rust의 튜플은 분해도 가능하다. 위 코드를 참고하자. 예시로 <code>a</code>가 <code>tup.0</code>에 대응된다.</p>
<pre><code class="language-rust">// Unit 튜플
let tup = ();</code></pre>
<p>비어 있는 튜플은 특별히 <code>Unit</code>이라는 이름으로 불린단다. Kotlin에서 자주 보던 그거. 아마 C 계열의 <code>void</code>나 Kotlin의 <code>Unit</code>에 대응되는 요소 같다.</p>
<h3 id="배열">배열</h3>
<pre><code class="language-rust">// 선언
let arr = [1, 2, 3, 4, 5];

// 배열 내 값에 접근
println!(&quot;{}&quot;, arr[0]);</code></pre>
<p>배열은 C의 배열과 거의 유사하다. 선언 형태도 똑같다.</p>
<p>튜플과 달리 단일 자료형의 값으로만 구성할 수 있고 대괄호와 인덱스를 통해 내부 값에 접근한다. 또한 길이가 고정되어 있다! 길이가 가변적인 Python의 리스트와는 다르다. Vector 자료형도 내부 라이브러리에 존재하기는 한다는데 아직은 안 보여줬고 나도 찾아보지는 않았다.</p>
<pre><code class="language-rust">// 배열 자료형 명시하기
let integer_arr: [i32; 5] = [1, 2, 3, 4, 5];

// 하나의 값으로 배열 전체 초기화하기
let same_arr:[i32; 5] = [128; 5];</code></pre>
<p><code>let</code>으로 배열을 선언할 때 자료형을 명시하고 싶다면, <code>[자료형; 길이]</code>로 써 주자.</p>
<br>

<hr>
<h2 id="함수">함수</h2>
<h3 id="statement와-expression">Statement와 Expression</h3>
<blockquote>
</blockquote>
<ul>
<li><strong>Statement</strong> 특정 동작을 수행하지만 반환 값 없음</li>
<li><strong>Expression</strong> 특정 값으로 평가되어 반환될 수 있음</li>
</ul>
<p>Statement의 예시로 <code>let</code>, <code>fn</code> 등이 있고, Expression은 뒤에 세미콜론이 붙지 않는다. 세미콜론이 있는 경우 이는 Statement라고 한다.</p>
<pre><code class="language-rust">{
    let x = 3;
    x + 1
}</code></pre>
<p>이상의 코드는 세미콜론이 붙지 않으므로 Expression이다. 대충 예상은 하겠지만 4를 반환한다.</p>
<h3 id="함수-1">함수</h3>
<pre><code class="language-rust">// 함수 예시
fn add_on_pie(value: f64) -&gt; f64 {
    value + 3.14
}

// 함수
fn 함수_이름(매개변수: 매개변수 자료형) -&gt; 반환 자료형 {
    함수 바디
}</code></pre>
<p>함수는 위 코드와 같이 선언한다. Kotlin의 함수 형태와 거의 유사하다.</p>
<pre><code class="language-rust">// Expression을 활용한 함수 반환
// 1. return과 세미콜론 다 붙이기: 컴파일 됨
fn some_func() -&gt; u32 {
    return 120;
}

// 2. 세미콜론만 빼기: 컴파일 됨
fn some_func() -&gt; u32 {
    return 120
}

// 3. return과 세미콜론 다 빼기: 컴파일 됨
fn some_func() -&gt; u32 {
    120
}</code></pre>
<p>위의 Expression을 활용하면 이런 다양한 반환문을 사용할 수 있다. 이게 가능한 이유는 Rust의 모든 함수는 암시적으로 함수 몸체의 가장 마지막 Expression을 반환하도록 구성되어 있기 때문이라고 한다. 신기한 부분.</p>
<br>

<hr>
<h2 id="흐름-제어">흐름 제어</h2>
<h3 id="주석">주석</h3>
<p><code>//</code>로 쓰면 된다. <code>/* */</code>도 된다.</p>
<p><code>///</code>은 컴파일러에서 쓰지 말라고 한다.</p>
<h3 id="반복">반복</h3>
<pre><code class="language-rust">// loop
loop {
    break; // 루프 깨기
}

// while
while condition {

}

// for
for item in array {

}</code></pre>
<p><code>loop</code>, <code>while</code>, <code>for</code>이 있다. 그리고 셋 모두 <code>break</code>와 <code>continue</code>를 지원한다.</p>
<p><code>loop</code>의 경우 단순한 무한 반복이다.</p>
<p><code>while</code>의 경우 위 코드의 <code>condition</code>에 해당하는 값이 <code>true</code>일 경우에만 반복한다. 재밌는 점 하나로 <code>condition</code> Expression을 소괄호에 묶어줄 필요가 없다. C는 소괄호에 안 묶어주면 오류 내는데. 이건 편리한 부분 같다.</p>
<p><code>for</code>의 경우 Python에서 많이 본 형태대로 사용 가능하다.</p>
<pre><code class="language-rust">// 평범한 탈출
loop {
    break;
}

// 값을 반환하며 탈출
let a = loop {
    break 300;
}</code></pre>
<p>break 뒤에 특정한 값을 붙여 줄 경우 루프를 깨면서 그 값을 반환할 수 있다. 이상의 코드에서 <code>a</code>의 값은 300이 된다.</p>
<pre><code class="language-rust">// 루프에 레이블 붙이기
&#39;loop_1: loop {
    &#39;loop_2: loop {
        // body
    }
} </code></pre>
<p>루프에 이름도 붙여줄 수 있다. 작은 따옴표 뒤에 이름을 적어주고 루프를 선언해주면 된다. 이거는 보통 중첩 루프문에서 특정 루프를 깨고 싶을 때 사용하는 것 같다.</p>
<br>

<hr>
<h2 id="잡담">잡담</h2>
<p>대충 코드 끄적이면서 몇 가지 특징을 알게 되었다. 간단히 정리해보자.</p>
<pre><code class="language-rust">let tup = (1, 2, 3);
println!(&quot;{} {} {}&quot;, tup.0, tup.1, tup.2); // &quot;1 2 3&quot; 출력됨</code></pre>
<p>먼저, <code>String</code>에 복잡한 표현식을 쓰고 싶을 때는 중괄호를 적극 이용하자. C 언어에서 문자열에 변수 넣을 때 썼던 방법과 유사하지만, 여기서는 순서만 잘 지켜주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/i_meant_to_be/post/1809551b-39a2-4bb1-971b-4b7661ef2186/image.jpg" alt="신박한 오류"></p>
<p>다음으로 Rust에는 후위표기식이 없단다. 후위표기식을 코드에 넣어 봤더니 &quot;우리 집에는 그런 메뉴 없어요~&quot;란다. 난 세상에 이런 기능 없다고 컴파일러가 말하는 경우는 처음 봤다. 재미있는 언어다.</p>
<p>마지막으로 Rust에서 Kotlin의 <code>IntRange</code> 같은 게 존재한다. <code>1..4</code> 이런 식으로 쓰는데, Kotlin과는 다르게 마지막 값은 포함하지 않는다. 알아둬야 할 듯.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[00. 첫 걸음 - Rust]]></title>
            <link>https://velog.io/@i_meant_to_be/00-Rust</link>
            <guid>https://velog.io/@i_meant_to_be/00-Rust</guid>
            <pubDate>Mon, 13 Feb 2023 12:53:42 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>퇴직(?) 후 할 게 없어서 개강까지 남은 시간동안 잠깐 Rust를 찍먹해보기로 했다. 공식 문서를 참고하면서 배운 내용을 간단히 정리하고자 쓰는 글이다.</p>
<br>

<hr>
<h2 id="설치">설치</h2>
<p>Rust 공식 홈페이지에서 작업 환경에 맞는 설치 파일을 다운로드하여 설치한다. 설치 경로를 변경할 수 있는데, Windows 기준으로 환경 변수에서 <strong>&quot;RUSTUP_HOME&quot;</strong>과 <strong>&quot;CARGO_HOME&quot;</strong> 변수를 지정해주면, 설치 프로그램에서 해당 환경 변수를 인식하여 자동으로 설치 경로를 변경해준다.</p>
<p>추가로 나는 VS Code를 쓸 예정인데, Rust에서 공식적으로 지원하는 코드 분석 확장이 있다. </p>
<br>

<hr>
<h2 id="hello-world">Hello, world!</h2>
<p>모든 언어의 시작은 당연히 Hello, world!와 함께.</p>
<pre><code class="language-rust">fn main() {
    println!(&quot;Hello, world!&quot;);
}
</code></pre>
<p>일단 C/C++ 계열 언어와 생김새가 상당히 비슷하다. 프로그램의 진입점으로 <code>main</code> 함수를 쓰는 것도 그렇고, 모든 라인의 끝마다 세미콜론이 붙는 것도 그렇다.</p>
<p>프로젝트가 아닌 단일 소스의 컴파일은 <code>rustc source.rs</code> 형식의 명령어로 하는 듯하다. 해당 명령어로 컴파일해서 .exe 파일을 생성하고 그걸 실행하는 식.</p>
<br>

<hr>
<h2 id="cargo-사용">Cargo 사용</h2>
<p>종속성 등 프로젝트 관리 전반에 쓰는 도구인 듯하다. 목적만 봐도 거의 사실상 반 필수로 사용될 것 같은 친구다.</p>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<p>프로젝트 생성은 <code>cargo new project_name</code> 명령어로 진행한다. 프로젝트 생성 시 프로젝트 디렉토리에서 아래 두 파일을 발견할 수 있다:</p>
<blockquote>
</blockquote>
<ul>
<li>/Cargo.toml</li>
<li>/src/main.rs</li>
</ul>
<p>Cargo.toml 파일은 프로젝트 설정과  패키지 종속성을 정의하는 파일 같고, /src 디렉토리 내에서 코드를 짜면 되는 듯하다.</p>
<h3 id="프로젝트-빌드">프로젝트 빌드</h3>
<p>프로젝트 빌드는 프로젝트의 루트 디렉토리(대충 추측컨대 Cargo.toml이 존재하는 디렉토리)에서 <code>cargo build</code> 명령어를 사용한다. 다만 이 명령어는 위의 <code>rustc source.rs</code>의 경우처럼 소스를 컴파일하지만 실행은 하지 않는다. 대신 <code>cargo run</code>으로 컴파일 후 별도 명령어 없이 즉시 생성된 프로그램을 돌려볼 수 있다.</p>
<p>재밌는 점 하나, 기존 버전에서 별도의 코드 변경 없이 <code>cargo run</code> 명령어를 실행할 경우 Powershell에 컴파일 메시지가 뜨지 않는다. Git 마냥 소스 변경 내용을 Cargo에서 확인하고, 변경 없을 경우 컴파일 생략 후 바로 실행한다는 듯. 좀 쩐다. 이게 채-신 언어인가?</p>
<p>추가로 릴리즈 목적의 빌드를 위해서는 <code>cargo build --release</code>를 쓰면 된다. 릴리즈 빌드 시에는 컴파일에 시간을 더 쓰는 대신 최적화 과정이 붙어 프로그램의 실행 속도를 개선한다고 한다.</p>
<h3 id="프로젝트-오류-확인">프로젝트 오류 확인</h3>
<p><code>cargo check</code> 명령어도 있는데, 이건 .exe 파일을 생성하지는 않고 컴파일이 가능한지 여부만 확인한다. 개발하면서 특정 시점에서 코드가 오류를 일으키는지 확인하고 싶을 때가 있을 거다. 그때마다 프로젝트를 컴파일하고 빌드하면 시간이 오래 걸리니, 컴파일은 생략하고 컴파일 가능 여부만 판단함으로써 불필요한 자원 소모를 방지하고자 하는 데 그 목적이 있다.</p>
<p>이건 여담이지만, Rust 문서 원문에서 이 명령어를 설명해 둔 부분을 잘 이해하지 못했다. 이게 컴파일만 하는 건지, 아니면 컴파일 후 .exe 파일까지 뽑아주는 건지, 잘 와닿지 않아서 구글링을 좀 해 봤다:</p>
<blockquote>
</blockquote>
<ol>
<li>컴파일: 사용자가 작성한 코드를 컴퓨터가 이해할 수 있는 언어로 번역하는 일</li>
<li>빌드: 컴파일된 코드를 실제 실행할 수 있는 상태로 만드는 일</li>
<li>배포: 빌드가 완성된 실행 가능한 파일을 사용자가 접근할 수 있는 환경에 배치시키는 일</li>
<li>혹은 컴파일을 포함해 war, jar 등의 실행 가능한 파일을 뽑아내기까지의 과정을 빌드한다고도 함.</li>
</ol>
<p>출처는 <a href="https://itholic.github.io/qa-compile-build-deploy/">여기</a>다. 근데 생각해보니까 우리 학교 CS 커리큘럼에 이런 걸 가르치는 과목이 있을까? 난 배운 적 없는데. 혹시 소프트웨어공학에서 이런 거 배우나? 음... 잘 모르겠다.</p>
<br>

<hr>
<h2 id="문법-맛보기">문법 맛보기</h2>
<h3 id="표준-라이브러리">표준 라이브러리</h3>
<pre><code class="language-rust">use std::io;</code></pre>
<p>와 같이 쓰면 불러올 수 있다. 눈길이 가는 부분은 세미콜론. C와는 좀 다르게 라이브러리를 불러오는 구문에도 세미콜론을 넣는다.</p>
<p>그리고 표준 라이브러리 중 모든 프로그램에 포함되는 가장 기초적인 몇 가지 요소들이 있는데, 이들을 Prelude라고 부른다고 한다. 아마 뭐 정수와 같은 가장 기본적인 자료형들이겠지...?</p>
<h3 id="변수">변수</h3>
<pre><code class="language-rust">let mut guess: String = String::new();</code></pre>
<p>하나하나 분해해보자.</p>
<blockquote>
</blockquote>
<p><code>let</code> 변수를 선언한다는 의미
<code>mut</code> 변수의 값을 바꿀 수 있도록 한다는 의미
<code>=</code> 좌변에 우변의 값을 &#39;bind&#39;한다는 의미</p>
<p><code>mut</code> 없이 <code>let</code>으로만 변수를 선언할 경우, 초기화 후에는 값을 변경할 수 없게 된다고 한다. </p>
<h3 id="입력-참조와-예외-처리">입력, 참조와 예외 처리</h3>
<pre><code class="language-rust">io::stdin()
    .read_line(&amp;mut guess)
    .expect(&quot;Failed to read line&quot;)</code></pre>
<p>입력 자체는 <code>io::stdin().read_line(buf)</code> 메소드로 하는 것 같다. 추가로 <code>::</code>는 Python, Java, Kotlin 등에서 쓰는 <code>Class.method()</code>의 그것이 아닐까 싶다.</p>
<p>다음으로, <code>&amp;</code> 연산자가 눈에 띈다. C에서 익숙하게 보던 그거다. <code>&amp;</code> 연산자는 참조 연산자로 특정 값을 메모리 공간에 여러 번 복사하지 않고도 코드 전반에서 편하게 공유할 수 있게 해 주는 기능이다. 그리고 Rust의 핵심이라고도 한다.</p>
<p>다만 이 단계에서는 자세한 건 생략하고, 참조 역시 변수 선언과 마찬가지로 <code>mut</code> 여부를 결정할 수 있으며 <code>mut</code> 키워드 없이는 기본적으로 불변이다, 라는 정도만 알고 있으면 된다고 한다.</p>
<p>추가로 <code>read_line(buf)</code> 메소드는 <code>Result</code> 객체를 반환한다고 한다. 대충 추측해보건데 파일을 읽어오는 작업을 시도한 후 이 작업의 성공 여부를 반환하는 것 같다. <code>Result</code> 클래스(열거형같기는 하지만)에는 <code>expect(msg)</code>라는 메소드가 붙어 있는데, <code>read_line(buf)</code> 메소드 실행에 실패했을 때 <code>msg</code>에 있는 메시지를 반환하며 프로그램을 종료한다고 한다.</p>
<p>또 재밌는 점, <code>Result</code> 객체를 어떤 형식으로든 사용하지 않으면 컴파일러에서 경고를 보낸다고 한다. 아마 느슨한 예외 처리를 경고하기 위한 조치가 아닐까 싶다.</p>
<h3 id="출력">출력</h3>
<pre><code class="language-rust">println!(&quot;You guessed: {guess}&quot;);</code></pre>
<p>일단 아까부터 왜 출력 함수에 느낌표가 자꾸 붙는지 모르겠다. 생각보다 굉장히 신경이 쓰인다. 별도의 설명이 없는데 나중에 알려주겠지...?</p>
<p>그건 그렇다 치고, 출력 자체는 저렇게 하는데 아마 변수를 <code>String</code>에 직접 넣기 위해서는 중괄호를 활용하면 되는 것 같다.</p>
<h3 id="외부-크레이트-사용">외부 크레이트 사용</h3>
<p>Rust는 패키지, 라이브러리를 Crate라고 부른다고 한다. 그리고 무작위 함수를 사용하기 위해 <code>rand</code>라는 크레이트를 불러 올 필요가 있다.</p>
<p>처음에 봤던 Cargo.toml 파일의 [depencencies] 부분을 수정해주면 된다:</p>
<pre><code>[dependencies]
rand = &quot;0.8.5&quot;</code></pre><p>그리고 <code>cargo build</code>하면, 지정한 크레이트가 불러와진다. 그리고 이렇게 불러오는 모든 크레이트들은 crates.io에서도 확인할 수 있다고 한다. 아마 Dart의 pub.dev와 유사한 역할인 것 같다.</p>
<p>또한 Cargo는 개발자가 별도로 지시하지 않는 한 크레이트의 버전을 함부로 올리지 않는다고 한다. 아마 버전 업 되면서 발생할 수 있는 호환성 문제를 방지하기 위함인 듯하다.</p>
<p>심지어 처음에 0.8.5로 설정한 <code>rand</code> 크레이트 버전을 0.9.0으로 올리고 다시 빌드할 경우 Cargo가 빌드를 거부한다.</p>
<h3 id="match-구문과-shadowing"><code>match</code> 구문과 Shadowing</h3>
<pre><code class="language-rust">let mut guess: String = String::new();

io::stdin()
    .read_line(&amp;mut guess)
    .expect(&quot;Failed to read line&quot;);

let guess: u32 = guess.trim().parse().expect(&quot;Please type a number!&quot;);

match guess.cmp($secret_number) {
    Ordering::Less =&gt; println!(&quot;Too small!&quot;),
    Ordering::Greater =&gt; println!(&quot;Too big!&quot;),
    Ordering::Equal =&gt; println!(&quot;You win!&quot;)
}</code></pre>
<h4 id="match-구문"><code>match</code> 구문</h4>
<p><code>guess</code>와 <code>secret_number</code>의 값 비교를 위해 사용한 <code>match</code> 구문이다. 이게 정확히 어떤 유형의 구문인지는 잘 모르겠지만, 아무튼 대충 봐서는 Kotlin의 <code>when (value) {}</code> 절과 비슷해 보인다.</p>
<p>그리고 한 줄 코드를 중괄호 없이 쓸 수 있게 해 주는 것도 좋다. 사실 이거는 비교적 최근 언어라고 하면 대부분 다 지원해주지 않나 싶긴 하다.</p>
<h4 id="shadowing">Shadowing</h4>
<p>그리고 처음에 <code>String</code>으로 선언했던 <code>guess</code> 변수를 <code>u32</code>로 재할당했다. 이렇게 기존에 선언된 변수에 다른 값을 할당하는 것을 Shadowing이라고 부른다고 한다.</p>
<p>다만 여기서 궁금한 점이 하나 생긴다. 지금은 가변하게 설정해 둔 변수에 값을 재할당하는 상황이다. 만약 처음에 불변하게 A 자료형으로 설정한 값에 새로운 B 자료형 값을 할당하는 게 가능할까? 그래서 무작위 정수를 정하는 코드를 아래와 같이 바꿔보았다:</p>
<pre><code class="language-rust">// Before
let secret_number = rand::thread_rng().gen_range(1..=100);

// After 1: 같은 i32 자료형으로 변경
let secret_number = rand::thread_rng().gen_range(1..=100);
let secret_number = 81;

// After 2: 처음은 i32, 다음은 String
let secret_number = rand::thread_rng().gen_range(1..=100);
let secret_number = 81;
let secret_number = &quot;Alpha&quot;;</code></pre>
<p>일단 두 개의 After 코드 모두 컴파일은 된다. 근데 경고를 주긴 한다. 각각의 실행 결과는 아래와 같다:</p>
<ul>
<li>After 1의 경우에는 <code>secret_number</code>가 81로 고정된다. 값도 정상적으로 바뀌는 것 같다.</li>
<li>After 2의 경우에도 <code>secret_number</code>를 출력하면 &quot;Alpha&quot;라고 잘만 뜬다.</li>
</ul>
<p>그럼 대체 어떤 점에서 <code>mut</code>가 없으면 불변한 변수라고 말한 걸까?</p>
<pre><code class="language-rust">// After 3: let을 빼고 재할당
let secret_number = rand::thread_rng().gen_range(1..=100);
secret_number = 32</code></pre>
<p><code>let</code>을 빼고 변수를 같은 자료형으로 재할당하면 컴파일이 안 되고 오류가 뜬다. <code>secret_number</code>를 가변하게 설정해 보라는 조언까지 같이 준다. 이렇게 세 코드를 보니 대충 어떤 의미에서 <code>mut</code>이 없는 변수가 불변하다는지 이해가 간다.</p>
<h3 id="반복">반복</h3>
<pre><code class="language-rust">loop {
    // To do
    break;
}</code></pre>
<p>별 거 없다. 그냥 중괄호 안에 대충 코드 때려넣으면 무한 반복이란다. 반복 깨는 것도 익히 알려진 <code>break</code> 쓰면 된다.</p>
<h3 id="잘못된-입력-대처하기">잘못된 입력 대처하기</h3>
<pre><code class="language-rust">// Before
let guess: u32 = guess.trim().parse().expect(&quot;Please type a number!&quot;);

// After
let guess: u32 = match guess.trim().parse() {
    Ok(num) =&gt; num,
    Err(_) =&gt; continue
};</code></pre>
<p>공식 문서에서 적어 둔 대로 코드를 바꿔보았다. 숫자가 아닌 값을 입력할 경우 프로그램을 처음부터 다시 시작하도록 하는 코드다.</p>
<p>근데 분명 <code>guess</code>의 자료형이 <code>u32</code>로 명시적으로 지정되어 있는데, 어떻게 <code>continue</code>라는 코드가 들어갈 수 있는지가 이해가 안 간다. <code>match</code> 문을 잘 모르는 상태에서 대충 보면, <code>Ok(num)</code>일 경우 <code>num</code>을 반환하고 아닐 경우 <code>continue</code>를 반환한다는 것처럼 읽히는데, 확실하게 알 수 있는 건 <code>continue</code>는 <code>u32</code>가 절대 아니다. 뭐 나중에 공식 문서에서 더 자세히 알려주겠지... 일단은 넘기자.</p>
<br>

<hr>
<h2 id="결론">결론</h2>
<p>Rust의 존재를 알게 된 계기가 소마 당시 멘토님 덕분이었는데, 그 분이 <em>&#39;얘는 목표가 런타임에서 오류를 가능한 한 제거하고 프로그램을 돌린다는 거다. 개발자 책임을 최대한 더는 거다. 그래서 타입 체크도 빡세고 다른 보편적 언어랑은 좀 느낌이 다르다...&#39;</em>라는 뉘앙스로 말씀하셨었다.</p>
<p>간단하게 튜토리얼만 해 보았는데도 그 말이 정말 맞다는 게 느껴졌다. 오류 및 경고 메시지가 굉장히 친절하고 어떻게 코드를 바꾸어야 할지도 제안해 준다. 타입 체크는 빡세겠지만, 퍼즐 푸는 느낌도 들어서 나름의 재미도 있다.</p>
<p>그리고 이거 쓰면서 느낀 또 다른 깨달음 하나는, 확실히 여러 언어를 배워 놓으면 다른 언어를 시작하기가 훨씬 더 편해진다는 점이었다. 이번에 잠깐 프론트엔드 개발하면서 Kotlin을 처음 만져봤는데, <code>val value: Int</code>처럼 변수 뒤에 콜론을 붙이고 자료형(함수의 경우는 반환형)을 쓰는 부분이나,</p>
<pre><code class="language-kotlin">return when(intValue) {
    1 -&gt; // To do
    2 -&gt; // To do
    3 -&gt; // To do
    else -&gt; // To do
}</code></pre>
<p>처럼 C 언어에서는 볼 수 없는 다양한 조건문과 스위칭 문의 형태들이 다른 일부 언어에서도 조금씩 공유되고 있기 때문이다.</p>
<p>배움의 소소한 재미를 느낄 수 있었던 3시간이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proto Datastore와 Hilt 같이 사용하기 - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/Proto-With-Hilt-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/Proto-With-Hilt-Jetpack-Compose</guid>
            <pubDate>Thu, 12 Jan 2023 05:02:54 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Hilt로 의존성 주입을 적용한 ViewModel에서 Proto Datastore(이하 Proto)를 클래스 생성자로 불러오려고 하니 많은 오류가 발생했다. 그 오류들을 일일히 기억하지도 못하고 기록도 안 했지만, 결과적으로는 돌아가는 코드를 작성했고 그걸 공유하고자 한다.</p>
<p>본 게시글은 블로그의 Jetpack Compose 시리즈에 올라온 코드를 재사용하기 때문에, 만약 이걸 읽는 누군가가 있다면 이전 글들을 같이 보는 것을 추천한다.</p>
<p>그리고 이건 개인적인 한탄이지만... 매번 구글링하고 StackOverflow 찾아보면서 문제를 어찌저찌 해결하기는 하면서도, 오류의 근본적인 원인과 Compose의 딥한 부분까지는 완전히 이해하지는 못하는 게 너무 아쉽다. 그래도 이렇게 무지성으로 부딪히면서 검색해보고 코드 짜보고 하면 내부 구조나 동작 방식에 대해 파편적으로나마 이해할 수 있게 되겠지, 하는 조금 긍정적인 생각은 있다.
<br></p>
<hr>
<h2 id="to-do">To-do</h2>
<p>Hilt + ViewModel + Proto의 트리오 앙상블을 위해서는 몇 가지 코드 수정이 조금 필요하다.</p>
<blockquote>
</blockquote>
<ul>
<li><code>Context.dataStore</code>의 선언 위치 변경</li>
<li>Repository 생성자 및 코드 일부 수정</li>
</ul>
<br>

<hr>
<h2 id="구현">구현</h2>
<h3 id="contextdatastore의-선언-위치-변경"><code>Context.dataStore</code>의 선언 위치 변경</h3>
<p>로컬 데이터 저장소에 해당하는 <code>dataStore</code>, 원래는 MainActivity.kt의 최상단에 선언해 주었었다. 그걸 그대로 Repository를 선언한 코드에 옮겨주면 된다.</p>
<p>나의 경우는 Proto를 쓸 때 디렉토리 구조를 보통 아래와 같이 하는데, 이 예시에서는 <code>SampleRepository.kt</code>로 옮겨주면 된다:</p>
<blockquote>
</blockquote>
<ul>
<li>/data<ul>
<li><strong><span style="color: #30A8F2">/SampleRepository.kt (여기로)</span></strong></li>
<li>/SampleSerializer.kt</li>
</ul>
</li>
<li>/viewmodels</li>
<li>/views</li>
<li><span style="color: #30A8F2"><strong>/MainActivity.kt (여기에서)</strong></span></li>
</ul>
<pre><code class="language-kotlin">package com.imeanttobe.compose.data

import ...

private val Context.dataStore: DataStore&lt;Sample&gt; by dataStore(
    fileName = &quot;sample.pb&quot;,
    serializer = SampleSerializer
)</code></pre>
<p>여기서 중요한 건 선언 위치는 무조건 코드 파일 최상단이어야 한다는 점이다. <code>dataStore</code>를 싱글톤으로 사용하기 위함이다. 처음에는 <code>dataStore</code>을 Repository 클래스 안에서만 쓰니까, 클래스 안에서 선언해줘도 되겠지, 하고 생각했다. 그러다가 &quot;There are multiple DataStores active for the same file: (후략)&quot;하는 예외 상황을 만나게 되었고 뒤늦게 싱글톤으로 쓰는 방법을 찾게 되었다.</p>
<p>이상의 이슈는 <a href="https://patrick-dev.tistory.com/m/25">patrick-dev 님의 블로그</a>를 참고해서 해결했다. 감사합니다 <del>_</del>
<br></p>
<h3 id="repository-생성자-및-코드-일부-수정">Repository 생성자 및 코드 일부 수정</h3>
<p>먼저 생성자다. Hilt가 의존 관계를 알 수 있도록 ViewModel에서의 경우와 같이 생성자에 <code>@Inject constructor</code> 어노테이션을 달아주어야 한다. 그리고 기존에 생성자로 넘겨주었던 <code>dataStore</code>는 이제 같은 파일의 최상단에 선언해 주었기 때문에 지워도 된다. 대신 <code>dataStore</code>를 사용하려면 <code>android.content.Context</code>가 필요하기 때문에 얘를 생성자에 넣어주어야 한다.</p>
<p>이상의 수정 사항을 반영한 생성자는 아래와 같다:</p>
<pre><code class="language-kotlin">// Before
class SampleRepository (
    private val dataStore: DataStore&lt;Sample&gt;
)

// After
class SampleRepository @Inject constructor(
    @ApplicationContext private val context: Context
)</code></pre>
<p><code>@ApplicationContext</code>는 Hilt를 사용하면서 애플리케이션 또는 액티비티의 <code>Context</code>를 참고해야 할 경우에 사용하는 어노테이션이라고 한다. 이것만 뺀 상태로 앱을 돌려봤는데 실행이 안 되더라. 꼭 넣어주도록 하자.</p>
<p>다음으로는 <code>Flow</code> 타입을 통해 ViewModel에 데이터를 넘겨주는 <code>val sampleData: Flow&lt;Sample&gt;</code>을 조금 수정해야 한다. <code>dataStore</code>의 선언 위치가 바뀌었기 때문에 이에 따른 대응 차원의 수정이다.</p>
<pre><code class="language-kotlin">// Before
class SampleRepository (
    private val dataStore: DataStore&lt;Sample&gt;
) {
    val flow: Flow&lt;SampleData&gt; = dataStore.data
    // 이하 생략
}

// After
class SampleRepository @Inject constructor(
    @ApplicationContext private val context: Context
) {
    val flow: Flow&lt;SampleData&gt; = context.dataStore.data
    // 이하 생략
}</code></pre>
<p>여담으로 여기서 <code>Sample</code>과 <code>SampleData</code>, 두 자료형의 의미에 대해 궁금한 경우가 있을 수 있다. <code>Sample</code>은 .proto 파일에서 <code>message</code>에 정의해 준 이름이고, <code>SampleData</code>는 <code>Sample</code>을 가공해서 사용하기 위해 별도로 만든 클래스다. 날 것의 데이터를 가공한 후 담는데 쓰는 참치 캔... 정도로 이해하면 될 것 같다.
<br></p>
<hr>
<h2 id="결론">결론</h2>
<p>에 뭐 휘황찬란하게 적을 건 딱히 없다. 잘 돌아간다.</p>
<p>조만간 SQLite를 쓰는 Room까지 적용해볼 것 같다. 이러다 완전히 안드로이드 개발자가 되어버리는 건 아닐지 모르겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proto Datastore 활용하기 - Jetpack Compose]]></title>
            <link>https://velog.io/@i_meant_to_be/Proto-Datastore-Jetpack-Compose</link>
            <guid>https://velog.io/@i_meant_to_be/Proto-Datastore-Jetpack-Compose</guid>
            <pubDate>Sun, 08 Jan 2023 07:23:13 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>안드로이드 환경에서 앱 설정 값을 기억해야 한다거나... 하여 로컬 저장소가 필요할 때가 있다. 그때 쓰는 게 <span style="color: #30A8F2"><strong>Preference Datastore</strong></span> 또는 <span style="color: #30A8F2"><strong>Proto Datastore</strong></span>인데, 나는 얼리어답터 + 남들 안 쓰는 거 쓰는 사람이라 <span style="color: #30A8F2"><strong>Proto Datastore</strong></span>(이하 Proto)을 써 보기로 했다.</p>
<p>Proto의 장점으로는,</p>
<blockquote>
</blockquote>
<ul>
<li>단순히 Key-Value가 아니라 멤버 변수의 자료형이 구체적으로 정의된 클래스 형태로 데이터 저장 가능</li>
<li>Kotlin 외에도 C++, C#, Java, Python 등 다양한 환경에서 사용 가능</li>
</ul>
<p>뭐 대충 이 정도가 있단다. 나머지는 잘 기억 안 남. 아무튼 Kotlin + Compose 환경에서 ViewModel과 함께 Proto를 찍먹해보도록 하자.
<br></p>
<hr>
<h2 id="to-do">To-do</h2>
<p>Compose에서 Proto를 쓰기 위해서 해야 할 일은 아래와 같다:</p>
<blockquote>
</blockquote>
<ul>
<li>build.gradle 설정</li>
<li>.proto 파일 작성</li>
<li>Serializer 작성</li>
<li>Repository 작성</li>
<li>ViewModel에 DataStore 적용하기</li>
<li>MainActivity에 반영</li>
</ul>
<br>

<hr>
<h2 id="구현">구현</h2>
<h3 id="buildgradle-설정">build.gradle 설정</h3>
<p>모듈 수준 build.gradle 파일을 열고 아래 내용들을 추가하자:</p>
<pre><code class="language-gradle">plugins {
    // 주요 내용 외 생략
    id &quot;com.android.protobuf&quot; version &quot;0.9.1&quot;
}</code></pre>
<pre><code class="language-gradle">android {
    kotlinOptions {
        // 주요 내용 외 생략
        freeCompilerArgs = [
                &quot;-Xjvm-default=all&quot;
        ]  
    }
}</code></pre>
<pre><code class="language-gradle">dependencies {
    // 주요 내용 외 생략
    implementation &quot;androidx.datastore:datastore:1.0.0&quot;
    implementation &quot;com.google.protobuf:protobuf-javalite:3.21.12&quot;
}</code></pre>
<pre><code class="language-gradle">protobuf {
    protoc {
        artifact = &quot;com.google.protobuf:protoc:3.11.0&quot;
    }

    generateProtoTasks {
        all().each { task -&gt;
            task.builtins {
                java {
                    option &#39;lite&#39;
                }
            }
        }
    }
}</code></pre>
<p>이렇게 네 가지 수정 사항을 반영해주고 Gradle Sync를 진행해주면 된다.
<br></p>
<h3 id="proto-파일-작성">.proto 파일 작성</h3>
<p>다음으로 .proto 파일을 작성해서 DataStore에서 다룰 자료형을 정의해주어야 한다. Proto는 다양한 언어 환경에서 사용할 수 있기 때문에, JSON처럼 특정 언어에 구애받지 않는 별도의 공통된 형식으로 파일을 작성한다. </p>
<p>.proto 파일은 <span style="color: #30A8F2"><strong>/app/src/main</strong></span> 경로 안에 /proto 디렉토리를 새로 생성하고 그 안에서 작성한다. Android Studio에서 해당 디렉토리를 찾을 수 없는 분들은 아래 사진을 참고해서 Project 탭의 보기 모드를 &#39;Project&#39;로 바꿔주자. 그럼 경로를 찾을 수 있을 거다:
<img src="https://velog.velcdn.com/images/i_meant_to_be/post/a9ed1ccb-d6d8-4324-a67b-91f4afd9ac95/image.jpg" alt="사진 - Project 보기 방식"></p>
<p>/proto 디렉토리를 생성했으면 적절한 이름의 .proto 파일을 생성하고 편집기에서 연다. 그리고 아래와 같이 내용을 작성해준다:</p>
<pre><code class="language-proto">syntax = &quot;proto3&quot;;

option java_package = &quot;com.imeanttobe.compose&quot;;
option java_multiple_files = true;

message Sample {
  int32 counter = 1;
}</code></pre>
<ul>
<li><code>syntax</code> 항목은 사용할 Protobuf 버전을 지정하는 부분이다. proto2도 있긴 한데 최신 버전인 proto3을 쓰도록 하자.</li>
<li><code>java_package</code> 항목에는 개발 중인 앱의 전체 패키지 이름을 써 주면  된다. (e.g. com.example.app)</li>
<li><code>java_multiple_files</code> 항목은 최상위 수준인 클래스, enum에 해당하는 자바 클래스, enum 파일 등을 별도의 파일로 분리할 지를 결정하는 항목이다. 자세한 건 아래 링크의 &#39;옵션&#39; 부분을 참고하기 바란다. 레퍼런스에서는 다들 <code>true</code>로 지정해서 쓰더라.</li>
<li><code>message</code> 항목은 클래스랑 비슷한 개념이다. <code>message &lt;이름&gt;</code> 코드를 통해 자료형을 선언하고, 그 안에 적절한 멤버 변수를 정의해준다. </li>
<li>각 멤버 변수에 붙은 1, 2, ...에 해당하는 값은 해당 멤버 변수에 부여된 고유 값이라고 한다.</li>
</ul>
<p>자료형 등 그 외 자세한 내용은 <a href="https://developers.google.com/protocol-buffers/docs/proto3">Protobuf 3 공식 문서</a>를 참고하도록 하자. 그래서 이런저런 것들을 고려한 후 앱에 적절한 형태의 자료형을 정의했다면, <span style="color: #30A8F2"><strong>프로젝트를 다시 빌드</strong></span>해주어야 한다. 그래야 .proto 파일에서 정의한 자료형이 Java 클래스로 빌드되고, 이를 활용해 Android Studio 내에서 DataStore를 구현할 수 있게 된다. 빌드된 Java 클래스는 아래 사진처럼 편하게 Import하여 사용할 수 있게 된다:
<img src="https://velog.velcdn.com/images/i_meant_to_be/post/24bc983d-9deb-443d-a8ac-08c373dee1c5/image.jpg" alt="사진 - 자동 완성"></p>
<p>프로젝트를 다시 빌드했는데 생성한 자료형의 Import가 불가능할 경우, Android Studio를 껐다가 다시 키고 빌드를 다시 진행하자. 그러면 보통 해결되더라.
<br></p>
<h3 id="serializer-작성">Serializer 작성</h3>
<p>Proto는 JSON처럼 직렬화된 데이터를 쓰기 때문에, 이를 활용하려면 직렬화/역직렬화 과정을 거쳐야 한다. 앱 패키지 내에서 적절한 경로에 Serializer.kt를 생성하고, 아래와 같이 내용을 작성해주도록 하자:</p>
<pre><code class="language-kotlin">object SampleSerializer : Serializer&lt;Sample&gt; {
    override val defaultValue: Sample
        get() = Sample.getDefaultInstance()

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

    override suspend fun writeTo(t: Sample, output: OutputStream) {
        t.writeTo(output)
    }
}</code></pre>
<p>이 코드는 수정하지 않고 그대로 써도 무방한 듯하다. 필요한 경우, Proto에서 정의한 자료형 이름에 따라 Serializer 이름도 맞춰주면 된다. 그리고 ViewModel을 싱글톤으로 쓰는 것과 유사한 이유로 DataStore도 단일 객체로 사용하기 때문에, Serializer 역시 <code>object</code>로 선언한다.
<br></p>
<h3 id="repository-작성">Repository 작성</h3>
<p>다음으로 Repository를 작성해야 한다. ViewModel은 Repository에 딸린 <code>Flow</code>를 읽거나 메소드를 호출하는 식으로 간접적으로 데이터를 읽고 쓸 수 있으며, 직접적인 데이터 조작은 지금 작성하는 Repository에서 전담하게 된다. 다음 코드를 보자:</p>
<pre><code class="language-kotlin">class SampleRepository(private val sampleDataStore: DataStore&lt;Sample&gt;) {
    val flow: Flow&lt;Sample&gt; = sampleDataStore.data

    suspend fun increaseCounter() {
        sampleDataStore.updateData { sample -&gt;
            sample
                .toBuilder()
                .setCounter(sample.counter + 1)
                .build()
        }
    }
    suspend fun decreaseCounter() {
        sampleDataStore.updateData { sample -&gt;
            sample
                .toBuilder()
                .setCounter(sample.counter - 1)
                .build()
        }
    }
}</code></pre>
<p>Repository는 생성자 매개 변수로 <code>DataStore&lt;T&gt;</code>를 받는다. <code>DataStore&lt;T&gt;</code>는 .proto에서 정의한 데이터를 직접 읽고 쓸 수 있는 메소드를 제공해주는 클래스다. 이걸 매개 변수로 받아 Repository에서 데이터 조작을 진행하며, 이후에 MainActivity.kt에서 선언해 줄 예정이다.</p>
<h4 id="읽기">읽기</h4>
<p>읽기는 <code>flow</code> 변수로 한다. 간단하게 매개 변수로 가져온 <code>sampleDataStore.data</code>로 받아올 수 있다.</p>
<h4 id="쓰기-및-수정">쓰기 및 수정</h4>
<p>쓰기 및 수정은 아래 형태의 함수로 한다:</p>
<pre><code class="language-kotlin">suspend fun setData() {
    dataStore.updateData { message -&gt;
        message
            .toBuilder()
            // To do
            .build()    
    }
}</code></pre>
<p>Proto는 .proto에서 지정한 멤버 변수별로 Getter와 Setter를 제공해준다. 예를 들어, .proto 파일에서 정의한 특정 <code>sample</code>에 <code>int32</code>로 정의된 <code>intValue</code>라는 이름의 멤버 변수가 있다고 가정해보자. Proto는 프로젝트의 재빌드 단계에서 .proto 파일을 Java로 컴파일하고, <code>sample.intValue</code>라는 이름의 Getter와 <code>sample.setIntValue()</code>라는 이름의 Setter를 자동으로 생성하게 된다. 우리는 <code>// To do</code> 부분에서 제공받은 Getter와 Setter를 적절히 조합하여 데이터를 조작하면 된다. 말이 어려울 수 있는데, 아래 예시를 보면 무슨 말인지 이해가 갈 거다:</p>
<pre><code class="language-kotlin">class SampleRepository(private val sampleDataStore: DataStore&lt;Sample&gt;) {
    // 읽기
    val flow: Flow&lt;Sample&gt; = sampleDataStore.data

    // 쓰기 및 수정
    suspend fun increaseCounter() {
        sampleDataStore.updateData { sample -&gt;
            sample
                .toBuilder()
                .setCounter(sample.counter + 1)
                .build()
        }
    }
    suspend fun decreaseCounter() {
        sampleDataStore.updateData { sample -&gt;
            sample
                .toBuilder()
                .setCounter(sample.counter - 1)
                .build()
        }
    }
}</code></pre>
<br>

<h3 id="viewmodel에-repository-적용하기">ViewModel에 Repository 적용하기</h3>
<p>Repository를 구현했으니, 이걸 활용할 ViewModel에 Repository를 생성자 매개 변수로 넣어주고, Repository에서 구현한 데이터 조작 함수를 실행하도록 코드를 작성해야 한다. 아래 코드를 보자:</p>
<pre><code class="language-kotlin">class MainViewModel(private val sampleRepository: SampleRepository) : ViewModel() {
    val flow: Flow&lt;Sample&gt; = sampleRepository.flow

    fun increaseCounter() {
        viewModelScope.launch { sampleRepository.increaseCounter() }
    }
    fun decreaseCounter() {
        viewModelScope.launch { sampleRepository.decreaseCounter() }
    }
}</code></pre>
<p>생성자 매개 변수에 <code>private val sampleRepository: SampleRepository</code>을 추가해 Repository를 끌어다 쓸 수 있게 했다. 그리고 <code>increaseCounter()</code>와 <code>decreaseCounter()</code>를 구현해 Repository의 데이터 조작 함수를 활용할 수 있게 했다. 주목할 점으로, Repository의 데이터 조작 함수는 모두 <code>suspend fun</code>이다. 따라서 ViewModel에서 이를 끌어다 쓸 때는 <code>viewModelScope.launch {}</code>로 Coroutine 스코프를 걸어주어야 한다.</p>
<p>그리고 당연하지만 ViewModelFactory에도 Repository를 매개 변수로 넣어주어야 한다. 까먹지 말자.</p>
<pre><code class="language-kotlin">class MainViewModelFactory(private val sampleRepository: SampleRepository) : ViewModelProvider.Factory {
    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            @Suppress(&quot;UNCHECKED_CAST&quot;)
            return MainViewModel(sampleRepository) as T
        }
        throw IllegalArgumentException(&quot;Unknown ViewModel class&quot;)
    }
}</code></pre>
<br>

<h3 id="mainactivity에-반영">MainActivity에 반영</h3>
<p>가장 먼저 MainActivity.kt의 최상단에 아래 코드를 추가한다:</p>
<pre><code class="language-kotlin">private val Context.sampleDataStore: DataStore&lt;Sample&gt; by dataStore(
    fileName = &quot;sample.pb&quot;,
    serializer = SampleSerializer
)</code></pre>
<p>얘는 이후 ViewModel에 붙은 Repository의 매개 변수인 <code>DataStore&lt;T&gt;</code>에 들어갈 변수다.</p>
<ul>
<li><code>fileName</code> 매개 변수는 로컬에 저장될 Protobuf 파일의 이름이다. .pb 확장자로 끝내는 게 관례인 듯 하며, 직접 <code>String</code>으로 박아줘도 되고 <code>private const val</code>로 선언해서 박아도 된다.</li>
<li><code>serializer</code> 매개 변수는 Serializer다. 아까 구현해 둔 Serializer를 넣어주면 된다.</li>
</ul>
<p>그리고 <code>MainActivity</code> 클래스의 <code>setContent {}</code> 함수 안에 ViewModel을 추가해준다:</p>
<pre><code class="language-kotlin">private val Context.sampleDataStore: DataStore&lt;Sample&gt; by dataStore(
    fileName = &quot;sample.pb&quot;,
    serializer = SampleSerializer
)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel = ViewModelProvider(
                this,
                MainViewModelFactory(SampleRepository(sampleDataStore))
            )[MainViewModel::class.java]

            // 여기에 UI 코드 구현
        }
    }
}</code></pre>
<p>이 코드로 ViewModel을 생성하면 인스턴스 개수를 1개로 유지할 수 있는 듯하다. 여기까지 했으면 끝이다. UI를 짜면서 필요한 Composable에 ViewModel을 매개 변수로 넘겨주면 된다. Composable에서는 아래 코드처럼 ViewModel을 활용해 데이터를 읽고 쓸 수 있다:</p>
<pre><code class="language-kotlin">@Composable
fun MainView(viewModel: MainViewModel) {
    // 읽기
    val data: Sample = viewModel.flow.collectAsState(initial = Sample.getDefaultInstance()).value

    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp)
    ) {
        Text(
            // 위에서 선언한 data 변수를 통해 counter 값을 가져오는 코드
            text = data.counter.toString(),
            style = MaterialTheme.typography.titleMedium
        )
        // onClick 매개 변수를 통해 viewModel에 딸린 데이터 조작 메소드를 호출하는 코드 
        Button(onClick = { viewModel.increaseCounter() }) {
            Icon(
                imageVector = Icons.Default.KeyboardArrowUp,
                contentDescription = &quot;Increase counter&quot;
            )
        }
        // onClick 매개 변수를 통해 viewModel에 딸린 데이터 조작 메소드를 호출하는 코드
        Button(onClick = { viewModel.decreaseCounter() }) {
            Icon(
                imageVector = Icons.Default.KeyboardArrowDown,
                contentDescription = &quot;Decrease counter&quot;
            )
        }
    }
}</code></pre>
<br>

<hr>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/i_meant_to_be/post/75d5284b-94b6-43bb-8fb5-1dbc15c7799d/image.jpg" alt="사진 - 스크린샷"></p>
<p>잘 돌아간다. 카운터를 조작하고 앱을 껐다가 다시 켜도 끄기 전 값이 그대로 잘 남아있는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android 빌드 오류: 패키지 이름 수정 - Flutter]]></title>
            <link>https://velog.io/@i_meant_to_be/Android-build-error-Flutter</link>
            <guid>https://velog.io/@i_meant_to_be/Android-build-error-Flutter</guid>
            <pubDate>Sun, 16 Oct 2022 08:24:26 GMT</pubDate>
            <description><![CDATA[<h5 id="이-글은-다크-모드에-최적화되어-있다-강조-색이-노란색-계열이라-라이트-모드로-보면-눈뽕당할-확률이-크다-참고-바란다">이 글은 다크 모드에 최적화되어 있다. 강조 색이 노란색 계열이라 라이트 모드로 보면 눈뽕당할 확률이 크다. 참고 바란다.</h5>
<hr>
<h2 id="개요">개요</h2>
<p>귀찮아서 맨날 Windows에서만 디버깅을 돌리다가, 어제 일부 기능이 모바일에서 실제로 동작하는지 확인해보기 위해 내 휴대폰에서 디버깅을 시도했다. 그리고 날 반갑게 맞이한 건 빌드 오류. 분명 한 달 전에는 잘 돌아갔는데 그 사이에 무슨 일이 있었는지 갑자기 앱이 실행되지 않았다.</p>
<p>대충 구글링을 해 본 결과, 이런 상황이 발생한 이유는 얼마 전 <span style="color: #FFB10A"><strong>애플리케이션 패키지 이름을 수정</strong></span>한 것과 관련이 있는 듯했다. 나는 Android 앱을 개발해본 적이 없어서, 어떤 파일에서 어느 부분을 수정해야 하는지를 몰랐다. 그래서 관련 내용을 찾아 몇 가지 요소를 수정했고 오류를 해결할 수 있었다.</p>
<hr>
<h2 id="오류에-대해">오류에 대해</h2>
<h3 id="로그-내용">로그 (내용)</h3>
<p>나의 경우는 아래와 같은 오류가 발생했다:</p>
<pre><code>E/AndroidRuntime(29600): FATAL EXCEPTION: main
E/AndroidRuntime(29600): Process: com.charity.soonaapp, PID: 29600
E/AndroidRuntime(29600): java.lang.RuntimeException: Unable to instantiate application $com.package.name package com.package.name:
                         java.lang.ClassNotFoundException: Didn&#39;t find class &quot;$com.package.name&quot; {너무 길어 생략}
E/AndroidRuntime(29600):     at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1573)
E/AndroidRuntime(29600):     at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1502)
E/AndroidRuntime(29600):     at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7537)
E/AndroidRuntime(29600):     at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)
E/AndroidRuntime(29600):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2397)
E/AndroidRuntime(29600):     at android.os.Handler.dispatchMessage(Handler.java:106)
E/AndroidRuntime(29600):     at android.os.Looper.loopOnce(Looper.java:226)
E/AndroidRuntime(29600):     at android.os.Looper.loop(Looper.java:313)
E/AndroidRuntime(29600):     at android.app.ActivityThread.main(ActivityThread.java:8741)
E/AndroidRuntime(29600):     at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime(29600):     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
E/AndroidRuntime(29600):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)
E/AndroidRuntime(29600): Caused by: java.lang.ClassNotFoundException: {위와 같은 내용이라 생략}
E/AndroidRuntime(29600):     at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
E/AndroidRuntime(29600):     at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
E/AndroidRuntime(29600):     at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
E/AndroidRuntime(29600):     at android.app.AppComponentFactory.instantiateApplication(AppComponentFactory.java:76)
E/AndroidRuntime(29600):     at androidx.core.app.CoreComponentFactory.instantiateApplication(CoreComponentFactory.java:52)
E/AndroidRuntime(29600):     at android.app.Instrumentation.newApplication(Instrumentation.java:1232)
E/AndroidRuntime(29600):     at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1565)
E/AndroidRuntime(29600):     ... 11 more</code></pre><p>중요한 문구를 찾아보자면 <span style="color: #FFB10A"><strong><code>java.lang.RuntimeException: Unable to instantiate application com.package.name</code></strong></span>이 되겠다.</p>
<h3 id="원인">원인</h3>
<p>나는 오류가 발생한 원인을 <span style="color: #FFB10A"><strong>AndroidManifest.xml</strong></span> 파일로 추정하고 있다. 수정 전 파일 내용을 보자:</p>
<pre><code class="language-xml">&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.package.name&quot;&gt;
   &lt;application
        android:label=&quot;내가 만든 앱&quot;
        android:name=&quot;$com.package.name&quot;
        android:icon=&quot;@mipmap/ic_launcher&quot;&gt;
         &lt;!--(이하 생략)--&gt;</code></pre>
<p><code>android:name</code> 항목을 보면 <code>$com.package.name</code>로 설정되어 있다. <span style="color: #FFB10A"><strong>이걸 건들면 안 됐었다. 그게 원인이 아닐까 추측한다.</strong></span></p>
<p>원래 해당 항목의 기본값은 <code>${applicationName}</code>인데, 난 이걸 패키지 이름으로 착각하고 <code>$com.package.name</code>으로 그냥 수정해버렸다. 심지어 특정한 변수를 지정하는 의미로 추정되는 <code>$</code> 기호조차 빼지 않고 그냥 패키지 이름을 그대로 박아버렸다. 안타까운 일이다. 핑계를 대자면, 패키지 이름을 어떻게 바꿔야 하는지 알려주는 사람이 없었다. 그래서 일이 터지고 난 뒤에야 정확한 수정 절차를 알아봤고, 오류를 고칠 수 있었다.</p>
<p>결론적으로, 어떤 요소를 어떻게 수정해야 하는지를 이제부터 간단하게 설명해보겠다.</p>
<hr>
<h2 id="해결-방법-패키지-이름을-잘-수정하는-법">해결 방법: 패키지 이름을 잘 수정하는 법</h2>
<p>패키지 이름을 바꾸기 위해 수정해야 할 요소로는 아래의 4가지가 있다:</p>
<ul>
<li><span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/build.gradle</span> 파일 내용 수정하기</strong></li>
<li><span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/AndroidManifest.xml</span> 파일 내용 수정하기</strong></li>
<li><span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/</span> 이하 폴더의 디렉토리 이름 수정하기</strong></li>
<li><span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/</span> 이하 폴더에 존재하는 <span style="color: #FFB10A">MainActivity.kt</span> 파일 내용 수정하기</strong></li>
</ul>
<p>차례차례 확인해보자.
<br></p>
<h3 id="buildgradle-수정">build.gradle 수정</h3>
<p>이 파일의 주소는 다음과 같다: <span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/build.gradle</strong></span>
파일을 열고 <code>applicationId</code> 항목을 아래와 같이 수정하도록 하자:</p>
<pre><code class="language-gradle">android {
    defalutConfig {
        applicationId &quot;{내가 원하는 패키지 이름}&quot;

        // 이외 불필요한 내용 생략 
    }
}</code></pre>
<br>

<h3 id="androidmanifestxml-수정">AndroidManifest.xml 수정</h3>
<p>이 파일의 주소는 다음과 같다: <span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/AndroidManifest.xml</strong></span>
파일을 열고 <code>package</code> 항목을 아래와 같이 수정하도록 하자:</p>
<pre><code class="language-xml">&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;{내가 원하는 패키지 이름}&quot;&gt;

    &lt;!--(이하 불필요한 내용 생략)--&gt;
&lt;/manifest&gt;
</code></pre>
<br>

<h3 id="프로젝트-디렉토리-이름-수정">프로젝트 디렉토리 이름 수정</h3>
<p>먼저 <span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/</strong></span> 경로로 이동하자. 만약 여러분이 프로젝트를 생성할 때 별도의 다른 옵션을 지정하지 않았다면, 다음과 같은 디렉토리 구조가 존재할 것이다:</p>
<blockquote>
<p><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/<span style="color: #FFB10A">com/example/{프로젝트 이름}</span></strong></p>
</blockquote>
<p>색상을 칠해둔 부분이 수정해야 할 부분이다. 여러분이 원하는 패키지 이름이 <span style="color: #FFB10A"><strong>&quot;com.myapp.package&quot;</strong></span>이라고 가정해보자. 그렇다면 디렉토리 구조는 아래와 같아야 한다:</p>
<blockquote>
<p><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/<span style="color: #FFB10A">com/myapp/package</span></strong></p>
</blockquote>
<p>그리고 중요한 사항으로, 경로의 가장 끝에 위치하는 폴더에는 바로 다음에 수정할 MainActivity.kt 파일이 존재해야 한다. 위 예시 기준으로는 package 폴더가 되겠다. 아무튼 이렇게 수정하면 된다.
<br></p>
<h3 id="mainactivitykt-수정">MainActivity.kt 수정</h3>
<p>경우에 따라 파일 이름이 MainActivity.java인 경우도 있다고는 하는데, 아마 구버전일 확률이 높을 것 같다.
어쨌든 이 파일의 주소는 다음과 같다: <span style="color: #FFB10A"><strong>{프로젝트 디렉토리}/android/app/src/main/kotlin/com/myapp/package</span></strong>
파일을 열고 <code>package</code> 항목을 아래와 같이 수정하도록 하자:</p>
<pre><code class="language-kt">package {내가 원하는 패키지 이름}

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}</code></pre>
<hr>
<h2 id="끝">끝</h2>
<p>뭐, 이런 과정을 거쳐 오류를 수정하고 패키지 이름을 원활하게 바꿀 수 있었다.</p>
<p>그리고 내가 겪었던 몇 가지 빌드 오류가 더 있다. build.gradle 파일에서 설정해야 하는 Android 컴파일 SDK 버전이나 최소 SDK 버전과 같은 것들인데, 보통 이런 오류는 외부 패키지로 인해 발생하는 경우가 많은 것으로 생각된다. 무엇보다도 이 오류 내용에 대해서는 Flutter의 디버그 콘솔에서 아주 친절하게 잘 알려주므로 여기에서는 언급하지 않겠다. (대충 귀찮다는 말)</p>
<p>그으럼 20000</p>
]]></description>
        </item>
    </channel>
</rss>