<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Renovatio.log</title>
        <link>https://velog.io/</link>
        <description>안했으면 빨리 백준하나 풀고자.</description>
        <lastBuildDate>Sat, 24 Aug 2024 09:59:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Renovatio.log</title>
            <url>https://velog.velcdn.com/images/renovatio_hyuns/profile/d1f4a661-5b79-40b4-a47d-1444665c07b5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Renovatio.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/renovatio_hyuns" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[안드로이드 내부저장소, 뭘 써야할까?]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%82%B4%EB%B6%80%EC%A0%80%EC%9E%A5%EC%86%8C-%EB%AD%98-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%82%B4%EB%B6%80%EC%A0%80%EC%9E%A5%EC%86%8C-%EB%AD%98-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 24 Aug 2024 09:59:27 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/bdd30590-31c5-4caa-a3c6-b5242369c729/image.png" alt=""></p>
<p>양심고백으로 글을 시작해보겠습니다. 저는 지금까지 &quot;복잡한 데이터 = Room, 키-값 단순 데이터 = SharedPreference&quot; 라는 1차원적인 사고방식을 가지고 개발해왔습니다. 그러나 Pixionary를 개발하던 중 의구심이 생기게 되었습니다. *<em>&#39;단순한 형식의 데이터 형식이지만, 그 양이 많아진다면 SharedPrefernece가 빠를까, Room이 빠를까?&#39; *</em></p>
<p>따라서 이번기회에 다양한 안드로이드 내부저장소에 대해 알게되었고 이들을 직접 벤치마킹 해봄으로써 어떤 장단점이 있으며 Pixionary에서는 어떤 내부저장소를 사용하는것이 바람직한지 파악해보고자 합니다.</p>
<h2 id="실험-규칙">실험 규칙</h2>
<ul>
<li>monotonic time을 측정가능한 measureTime 메서드 활용하여 로직 수행시간 측정</li>
<li>10번 단독실행의 평균치를 데이터로써 사용</li>
<li>모든 IO 로직은 Dispatcher.IO 코루틴 디스패처에서 수행</li>
<li>랜덤 생성한 3만개 Float형 value와 그에 해당하는 String타입 key만을 활용</li>
<li>정렬이나 초기화 등 부가 로직을 제외한 database load, IO 작업만을 캡슐화하여 시간측정</li>
<li>코루틴 스코프는 runBlocking을 사용</li>
</ul>
<blockquote>
<p><strong>RunBlocking을 사용하는 이유</strong>
일반적인 coroutineScope 사용시 suspend 함수를 이용한 비동기 작업을 수행하게 됩니다. 이는 스레드에서 해당 작업도중 다른 작업이 들어왔을 때 일시정지하고 자원을 양보할 수 있다는 뜻이므로 매서드 수행시간을 측정할 때 오차범위가 커지게 됩니다. 
따라서 주어진 작업이 완료될 동안 스레드를 블로킹 하여 작업이 끝까지 한번에 진행될 수 있도록 하는 runBlocking을 활용합니다.
실제 서비스 개발에서는 옳지 못한 방법이 맞으나 실험인점을 감안하여 이러한 방식으로 진행합니다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d770acd7-b570-4a3c-9a7c-87794113d70e/image.png" alt="">
CoroutineScope 사용시 오차범위가 50~60ms 정도까지 생기는 반면 RunBlocking 사용시 오차범위가 20ms정도로 비교적 안정적인 모습을 볼 수 있습니다.</p>
<h1 id="room">Room</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/01dfc357-ab2a-482b-9e26-406900084b07/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Library Size : 138.1 KB
Memory Usage : 2.4 MB
Delete Time : 7.6 ms
Store Time : 323.9 ms
Load Time : 45.3ms</p>
<p>기본적으로 안드로이드는 SQLite DBMS를 사용하게 되는데, 이에 대한 추상 레이어를 제공해주어 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 해주는 라이브러리 입니다.
가장 대중적으로 활용되고 구글에서도 공식적으로 권장하고 있는 라이브러리인 만큼 특별한 단점을 찾아보기 힘든 육각형 라이브러리입니다. </p>
<h1 id="realm">Realm</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/cb038363-9776-4127-93fb-c594b403bbab/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Library Size : 4MB
Memory Usage : 2.63 MB
Delete Time : 7.2 ms
Store Time : 437.6 ms
Load Time : 6.3ms</p>
<p>Room을 통해 엑세스하는 SQLite는 모바일 특화 관계형 데이터베이스였다면 Realm은 모바일에 특화된 NoSQL 데이터베이스 입니다. Swift, Objective-C, Java, Kotlin 등 다양한 SDK를 제공하여 통합성이 좋으며 상당히 인지도가 있기 때문에 관련 자료도 많고 견고한 라이브러리입니다.
NoSQL 특성상 복잡하지 않은 데이터들을 저장하고 읽는데 상당히 빠른 속도를 기대해볼 수 있습니다.</p>
<h1 id="objectbox">ObjectBox</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/467a9ce4-ed36-4b81-9e2c-ae9a657bbeee/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Library Size : 886 KB
Memory Usage : 2.84 MB
Delete Time : 3.25 ms
Store Time : 78.48 ms
Load Time : 33.36 ms</p>
<p>객체 지향 데이터베이스로, Realm과 같은 NoSQL 기반의 데이터베이스입니다. 객체를 직접 저장하고 쿼리할 수 있어 ORM의 필요성을 줄입니다. ObjectBox는 데이터베이스와 상호작용하는 데 있어 SQL쿼리가 아닌, 메서드 호출로 작업을 수행합니다.
모바일 최적화된 NoSQL 라이브러리로 안드로이드에서 사용시 상당히 좋은 퍼포먼스를 보여줍니다.</p>
<h1 id="sharedpreferences">SharedPreferences</h1>
<blockquote>
</blockquote>
<p><strong>OOM으로 인해 3000개 데이터로 실험 후 10배 한 수치 적용</strong>
Library Size : - (내장 라이브러리)
Memory Usage : 177 KB
Delete Time : 1.2 ms
Store Time : 5.6 s
Load Time : 9.4 ms</p>
<p>key - value 형식으로 데이터를 저장하기 위한 간편한 경량의 데이터베이스(?) 입니다. 데이터베이스에 (?)를 붙인 이유는 데이터베이스라고 부르기엔 너무 보잘것 없는 녀석이기 때문입니다. 이녀석은 그냥 xml파일로 데이터를 저장합니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e789118a-a4f3-47d0-8f7f-2158563b545f/image.png" alt=""></p>
<p>실제로 파일 따서 들어가면 요렇게 데이터를 저장해두고 읽어오고 있습니다.</p>
<p>이번 실험을 통해 새로운 사실을 알게 되었습니다. 30000개 데이터를 저장하려고 했을 때 아래 그림과 같이 Java 영역의 heap이 폭발하면서 OOM 에러와 함께 앱이 죽어버렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/3130d99a-187e-4c70-a3f8-830d0989b46d/image.png" alt=""></p>
<p><a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/app/ContextImpl.java">관련 코드</a>를 통해 작동방식을 설명해준 <a href="https://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences/19148351#19148351">스택오버플로우 글</a>을 찾았는데, 요약하면 SharedPreferences의 경우 앱의 모든 컴포넌트들이 동일한 SharedPreferences 인스턴스를 사용하도록 하기 위하여 getSharedPreferences로 인스턴스를 생성하는 동시에 Hashmap에 모든 데이터를 들고있다는 내용입니다. 따라서 3만개 데이터를 계속 Hashmap에 들고 있으려고 하다보니 OOM이 나고 앱이 죽게 된 것입니다.</p>
<pre><code class="language-kotlin">@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        // 요부분
        final ArrayMap&lt;File, SharedPreferencesImpl&gt; cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion &gt;= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        &amp;&amp; !getSystemService(UserManager.class)
                            .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException(&quot;SharedPreferences in credential encrypted &quot;
                                + &quot;storage are not available until after user (id &quot;
                                + UserHandle.myUserId() + &quot;) is unlocked&quot;);
                }
            }
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }</code></pre>
<p>위 코드가 SharedPreferences 객체를 얻어오는 매서드의 구현부인데, 실제로 ArrayMap이라는 HashMap에 모든 데이터를 로드하여 읽어오도록 구현된 것을 볼 수 있습니다.</p>
<p><strong>즉, SharedPreferences는 데이터베이스의 개념이 아니라 heap에서 들고있는 데이터이므로 매우 작은 데이터만을 저장하는것이 바람직합니다.</strong></p>
<p>위의 내용으로 미루어보면 실험결과가 납득이 됩니다. 인메모리의 HashMap에서 데이터를 바로 참조하고 삭제하므로 읽기와 삭제가 매우 빠르며 데이터 쓰기의 경우 File 에 쓰게 되기 때문에 속도가 매우 느린것을 볼 수 있습니다.</p>
<h1 id="proto-datastore">Proto DataStore</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b6583201-9382-4874-a78c-d4fd23308e0e/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Library Size : 568 KB
Memory Usage : 881 KB
Delete Time : 92.1 ms
Store Time : 1.42 s
Load Time : 0.64 ms / 4.3 ms</p>
<p>기존의 오래된 SharedPreferences라이브러리의 단점을 보완하여 Jetpack에서 추가된 <strong>DataStore</strong>는 서로 다른 두 가지 구현, 즉 타입 객체를 저장하는 <strong>Proto Datastore</strong>(<a href="https://protobuf.dev/">프로토콜 버퍼</a>로 지원됨) 와 키-값 쌍을 저장하는 <strong>Preferences Datastore</strong>를 제공합니다.</p>
<blockquote>
<p><strong>프로토콜 버퍼란?</strong>
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.
구글에서 개발하였고 xml과 유사한 데이터 저장형식이지만 더 가볍고 빠르고 간단합니다. 기존의 SharedPreferences와 다르게 프로토콜 버퍼에 저장하기 위해서 스키마를 정의해야 하지만 성능이 더 좋다고 하니 한번 실험해보자구요.</p>
</blockquote>
<p>Proto Datastore의 특징으로 키-값 형태로 데이터를 저장하는것이 아니라 타입객체로써 저장하게 됩니다. 따라서 데이터를 읽을 때 Flow를 통해 모든 데이터를 통으로 반환하게 되므로 비동기적으로 데이터를 옵저빙할 수 있습니다.</p>
<p>확실히 SharedPreferences에 비해 속도가 매우 빨라졌음을 확인할 수 있었습니다. 또한 코루틴과 Flow를 기본적으로 사용하기 때문에 추후 비동기 프로그래밍을 적용하기 더욱 수월해졌습니다.</p>
<p><strong>Load Time 이 두 가지 존재하는 이유는 protoDataStore에서 데이터를 읽는 방식이 라이브러리에서 빌드한 엔티티 클래스와 Flow로 반환하기 때문입니다. 처음 시간은 Flow로 반환되는 시간이며 두번째 시간은 반환된 데이터를 모든 테스트에서 공통적으로 사용하고 있는 데이터클래스로 가공하는데 걸린 시간입니다.</strong></p>
<p>따라서 protoDataStore를 기존의 Room이나 여타 데이터베이스와 동일한 인터페이스로 사용한다면 두번째 시간인 4.3ms가 걸리는 것입니다. 그럼에도 불구하고 굉장히 빠른 속도로 데이터를 읽어오는것을 알 수 있습니다.</p>
<h1 id="file-json">File (JSON)</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/0db4d476-6088-4ce9-a2f1-5d5f05545b79/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Library Size : 0 KB
Memory Usage : 1.02 MB
Delete Time : 167.8 ms
Store Time : 87.6 ms
Load Time : 112.1 ms</p>
<p><del>엥?? 갑자기 파일이요?</del>
열심히 구현하고 비교를 해보다가 갑자기 근본적인 궁금증이 생겼습니다. 어차피 key-value 형식으로 데이터를 저장하는거고, 심지어 데이터의 업데이트도 일어나지 않는 정적 데이터면 굳이 데이터베이스를 사용할 필요가 있을까? 어차피 데이터베이스도 파일에 저장하고 관리해주는 라이브러리인데, 라이브러리를 거치는것보다 그냥 직접 파일에 저장하고 읽어오면 더 빠르지 않을까?</p>
<p>그래서 JSONObject로 실험해봤습니다. 예상보다 나쁘지 않은 결과가 나왔습니다. 그러나 delete, 혹은 update를 할 때 마다 모든 JSON Object를 불러와서 삭제, 수정하고 다시 저장하는 행위를 반복해야하다보니 이러한 연산이 자주 일어난다면 상당한 부하가 걸릴 것입니다.
단순히 정적 데이터를 저장, 읽어오는 용도로만 사용한다면 충분히 사용할 만한 성능이 나오는것을 확인하였습니다.</p>
<p>다만 Realm이나 ObjectBox와 같이 NoSQL 기반 데이터베이스가 읽기나 쓰기속도적인 측면에서 각각의 뚜렷한 장점을 가지고 있기에 굳이 File을 사용할 일은 없다고 결론지으면 될것 같습니다.</p>
<p>Realm의 경우 JSON형식을 바이너리로 표현한 BSON형식으로 데이터를 저장하기 때문에 읽기 속도적인 측면에서 더 빠른게 아닌가 추측해봅니다.</p>
<blockquote>
</blockquote>
<p>ObjectBox는 쓰기가 압도적으로 빠르고 Realm은 쓰기가 상당히 느린데 JSON을 BSON으로 바꾸는 과정이 오버헤드가 걸리나? 와 같은 궁금증들이 계속 생기는데, 자꾸 두더지처럼 파고들어가면 끝도없고 정작 중요한 기능개발을 못할거같아서 뇌피셜로만 남기고 본 포스팅의 목적인 <strong>성능 벤치마킹</strong>에 좀 집중을 해볼게요</p>
<h1 id="결론">결론</h1>
<p>Pixionary에서 저장할 데이터의 특징은 아래와 같았습니다.</p>
<ul>
<li>값의 업데이트가 일어나지 않는 고정 데이터</li>
<li>초기화 단계에서 한 번 저장한 이후로는 읽기 / 삭제 연산만 발생</li>
<li>초기화 단계가 굉장히 오래걸리며 읽기연산은 자주 발생함</li>
</ul>
<h3 id="메모리-사용량">메모리 사용량</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/43757b3b-af66-4cf3-9fc3-b6b8e0c23c3d/image.png" alt=""></p>
<p>결과를 보면 서드파티 라이브러리들이 (Room, Realm, ObjectBox) 메모리(저장공간)를 비교적 더 많이 사용하고 있음을 알 수 있었습니다. </p>
<p>사실 Pixionary에서는 딥러닝 모델을 온디바이스에서 사용하기 때문에 이미 앱의 용량이 크게 되어버려서 라이브러리 사이즈가 몇메가바이트 정도 더 먹는다고 큰 차이가 보이진 않습니다. 따라서 이번 벤치마킹에 있어서 특별하게 고려되어야 할 사항은 아닙니다.</p>
<h3 id="write">Write</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/050c45d9-dcd4-49f9-88f5-b5d450a96ca6/image.png" alt="">
아래의 차트들에서도 보이겠지만, 성능 면에서는 서드파티 라이브러리들이 비교적 좋은 면을 보이고 있습니다.
RDB냐 NoSQL이냐에 따라 특별한 차이가 보이지는 않습니다만 ObjectBox가 write operation에서는 압도적인 성능을 보였습니다.
SharedPreferences의 경우 처참한 수준의 성능이었으며 작동방식으로 인해 애초에 대용량 데이터로 사용자체가 불가능했습니다. Jetpack에서 추가되어 SharedPreferences를 대체하고 있는 DataStore를 보면 상당히 성능적으로 개선된 모습을 확인할 수 있었습니다.
또한 File을 바로 쓰는 경우 당연하게도 가장 빠른 속도를 보였습니다.</p>
<p>그러나 write operation에 대한 지표 역시 이번 벤치마킹에서는 큰 부분을 차지하지 못합니다.
write의 경우 Pixionary앱을 맨 처음 실행시킬 때 딱 한 번 수행되며, 이 작업은 약 30분~1시간정도 소요될 것으로 예상하기 때문에 1초 2초 더 걸린다고 아무런 체감이 되지 않기 때문입니다.
애초에 유저 상호작용과 관계없이 백그라운드 스레드로 돌릴 작업에 해당되기에 그냥 성능이 저렇구나 정도로 확인만 해보았습니다.</p>
<h3 id="delete">Delete</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/dcad5a4f-bdfe-4f49-9c6b-2c656efae204/image.png" alt=""></p>
<p>특정 key를 가지는 아이템 하나를 찾아 지우는 연산의 경우 ObjectBox가 역시 가장 빠른 속도를 보였으며 key-value 데이터 저장방식인 SharedPreferences가 압도적으로 빠른 속도를 보였습니다.
ProtoDataStore와 File 방식의 경우 구조적인 한계로 인해 처참한 결과가 나오게 되었는데, 데이터베이스의 경우엔 쿼리를 날려 특정 요소를 찾고 지우는것이 가능하지만, 이 둘의 경우엔 쿼리가 존재하지 않기에 데이터를 전부 불러와서 찾고, 지운 후 다시 저장하는 방식을 사용했기에 그렇습니다.</p>
<h3 id="read">Read</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/defa8fc5-2fd1-44d3-9245-1879bb746ea1/image.png" alt=""></p>
<p>사실상 가장 중요한 지표입니다. Pixionary의 주요 서비스는 검색기능이므로 한번 검색버튼을 눌렀을 때 최대한 빠른 응답속도를 내도록 해야 합니다. 
지금까진 NoSQL 데이터베이스중 ObjectBox가 좋은 성능을 냈다면 Read operation한정 Realm 라이브러리가 압도적으로 좋은 성능을 내고 있습니다. 
또한 ProtoDataStore가 1ms도 안되는 속도로 가장 빠른 속도를 내고 있으며 최적화가 되지 않은 4ms정도의 속도 조차 Realm보다 빠릅니다.</p>
<h3 id="선택의-시간">선택의 시간</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/240c1a05-7b3b-46dd-809a-ab18a0588a99/image.png" alt=""></p>
<p>사실 직접 벤치마킹을 해보기 전에 다른 <a href="https://notes.devlabs.bg/realm-objectbox-or-room-which-one-is-for-you-3a552234fd6e">외국인분이 쓰신 글</a>을 보고 &#39;읽기속도가 가장 빠른 NoSQL 라이브러리인 Realm을 써봐야되겠다&#39; 라고 생각하고 있었습니다. 그러나 us 단위의 속도로 데이터를 가져오는 ProtoDataStore를 보고 생각이 바뀌었습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/4b47f6af-3bca-49d1-a438-7989dfa49094/image.png" alt=""></p>
<p>그러나 ProtoDataStore에는 몇가지 한계점이 존재합니다.</p>
<ul>
<li>읽기는 압도적으로 빠르지만 쓰기, 삭제, 업데이트는 비교적 많이 느림</li>
<li>작성한 스크립트에 의해 생성된 클래스로 객체를 반환하기 때문에 Domain layer에서 사용하는 model로 바로 활용할 수 없음 (후가공을 해야함)</li>
</ul>
<p>하지만 어차피 쓰기작업은 백그라운드로 돌릴거라 상관없고, 업데이트는 일어나지 않는 데이터이므로 삭제 작업만 어떻게 최적화 하면 충분히 선택할만한 데이터 저장소입니다. 또한 지금은 실험 프로젝트이므로 후가공 과정이 필요하다고 여겨졌지만, 실제 서비스 개발을 할 때 어차피 검색과정에서 유사도를 비교하고, 정렬하는 후가공 과정이 필요합니다. 따라서 두번째 한계점은 다른 라이브러리를 사용하더라도 마찬가지로 적용될 것입니다.</p>
<p>이러한 이유들로 인해 ProtoDataStore가 가지는 한계점을 충분히 극복할 수 있다 판단하였고, 압도적으로 빠른 읽기속도의 이점을 얻기위해 ProtoDataStore를 활용해보기로 결정하였습니다.</p>
<h3 id="추가실험-반전">추가실험 (반전)</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/bb74e209-243e-43dd-82e4-d7b0972d8aa6/image.png" alt="">
Pixionary 앱 요구사항 중 딥러닝 모델 추론 결과로 나오는 [12, 512] 사이즈의 float vector를 저장해야 하는 조건이 있습니다. 단일 Float 데이터로만 실험했던 위 결과에 플러스 해서 List&lt;Float&gt; 으로 이루어진 데이터로 실험해본 결과 읽기와 쓰기 속도에서 뚜렷한 차이점이 드러났습니다.
Room의 경우에는 List타입을 지원하지 않기 때문에 TypeConverter를 통해 String으로 변환 후 저장해야만 했고, 그렇기에 거의 사용할 수 없는 수준의 성능을 보이게 되었습니다. 그러나 Realm이나 ObjectBox같은 NoSQL 데이터베이스들은 RealmList나 FloatArray처럼 자체적으로 리스트 타입을 지원하기 때문에 훨씬 더 큰 차이를 보였습니다.
쓰기 속도에서 ObjectBox가 Realm보다 훨씬 빠른것은 아마도 FloatArray라는 원시타입 배열을 객체로써 저장할 수 있도록 지원하기 때문일 것입니다. 
ProtoDataStore의 경우 repeated 키워드로 스키마를 정의하여 배열을 활용할 수 있기 때문에 성능에 큰 기복없이 역시 준수한 성능을 보이는 것으로 확인했습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/271fe492-8410-46d0-96d2-260ba25fffd4/image.png" alt="">
삭제 시간에서는 데이터베이스 들간의 큰 차이가 없었지만 protoDataStore의 구조적 한계로 인해 삭제할 아이템의 인덱스를 탐색하는 시간이 많이 소요될 뿐더러 리스트의 형태로 저장하는 것이 아니라 repeated 키워드를 통해 각각의 Float 데이터로 따로 따로 저장하여 하나로 묶기 때문에 일일이 개별적으로 삭제하는데 너무 많은 시간이 소요되는 단점이 생겨버렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b99cbb36-75a2-4d95-908f-12fcbe4070fe/image.png" alt=""></p>
<blockquote>
<p><strong>다시 결론</strong>
Pixionary에서는 모델 출력결과를 FloatArray로 사용하고 있기 때문에 이를 지원할 뿐더러 속도도 가장 빠르고 원시타입 배열로 인해 메모리도 절약할 수 있는 <strong>ObjectBox</strong>를 사용하지 않을 이유가 없는것 같습니다. ProtoDataStore의 유일한 장점은 읽기속도가 가장 빠르다는 점인데, 사실상 ObjectBox와 10ms정도밖에 차이가 나지 않습니다.
대량의 List 형식의 데이터를 저장하고 한번에 불러오는 작업이 필요하다면 NoSQL라이브러리를 고려해보세요. <del>(ObjectBox가 Realm보다 전반적으로 성능이 좋다고 생각되는건 저만의 생각인가요)</del></p>
</blockquote>
<h1 id="회고">회고</h1>
<p>벤치마킹이라는 작업을 처음해보았는데, 최대한 오차가 적은 환경에서 실험하려 했었고, 왜 이러한 성능이 나타나는지, 그리고 각각 어떤 상황에 적합한 라이브러리일지 분석하려 해보았습니다. RDB기반 ORM인 Room은 전반적으로 특별한 성능을 보이는 지표가 없었지만, 복잡한 쿼리가 필요하거나 다양한 관계를 맺을 필요가 있는 데이터들의 경우엔 Room을 사용하는것이 바람직 할 것입니다. (위 차트의 수치만을 맹신해선 안된다는 뜻입니다.)
같은 NoSQL 기반 라이브러리임에도 Realm과 ObjectBox는 각각의 뚜렷한 장점을 가지고 있었으며 서비스 성격에 맞게 선택해볼만한 가치가 있었습니다.</p>
<p>이번 벤치마킹에서 SharedPreferences가 사실상 모든 데이터를 hashmap형태로 인메모리에 올려서 사용하고 있다는 것을 처음 알게 되었고, 지금까지 SharedPreferences를 데이터베이스처럼 잘못 알고 사용하고 있었다는것을 배우게 되었습니다. 앞으론 이름에 걸맞게 환경설정과 관련된 자질구레한 데이터만 최소한으로 저장하도록 해야겠습니다.</p>
<p>사실 프로젝트 생성하고 실험환경 코드템플릿 만들고 라이브러리마다 각각 브랜치파서 구현하고 하느라 조금 귀찮은 마음도 생겼는데, 예상외로 ProtoDataStore라는 보물을 찾게 되었고 그 과정에서 배운점도 많아 굉장히 보람찬 시간이었습니다. </p>
<p>앞으로 어떻게든 시간여유를 내서 이러한 벤치마킹을 더 많이 시도해보고 더 많이 생각해보는 시간을 가져봐야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[kotlin으로 구현하는 BPE 알고리즘]]></title>
            <link>https://velog.io/@renovatio_hyuns/kotlin%EC%9C%BC%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-BPE-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@renovatio_hyuns/kotlin%EC%9C%BC%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-BPE-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 04 Jul 2024 04:55:40 GMT</pubDate>
            <description><![CDATA[<h1 id="tokenization-이란">Tokenization 이란?</h1>
<p>딥러닝의 자연어 처리 분야에서는 문자열 입력을 기반으로 추론을 하게 됩니다. 따라서 모든 딥러닝 추론이 그렇듯 문자열 입력 역시 vector화 시켜서 모델에 입력해야 하고 일종의 byte sequence인 문자열을 일련의 알고리즘에 따라 vector로 변환하는 과정을 Tokenize라고 합니다.</p>
<p>문자열 자체가 Byte Sequence이므로 이를 그대로 character 마다 vector로 취급하여 입력하고 학습시키면 되지 않나 라는 생각을 할 수 있습니다. 그러나 문장과 단어는 character들의 순서와 배치에 따라 각각 다른의미를 가지며 같은 bag 이라는 단어일 지라도 &#39;가방&#39;, &#39;비방하다&#39;, 버리다&#39; 등과 같이 다른 의미를 가지는 동음이의어도 존재합니다. 따라서 character 들만 가지고서는 단어와 문장이 가지는 의미를 표현하는것이 불가능하기에 word tokenization이나 subword tokenization을 통해 의미를 가지는 character들의 모음을 기반으로 tokenization을 수행합니다.</p>
<h2 id="oov">OOV</h2>
<p>OOV란 Out-Of-Vocabulary 의 약자로 구성된 사전에 없는 단어가 입력으로 들어온 경우를 의미합니다. word tokenization 을 통해 단어들을 기반으로 사전을 구성했다고 가정해봅시다.
Time is an illusion. Lunchtime double so!
라는 문장이 있다면 사전은 [&quot;Time&quot;, &quot;is&quot;, &quot;an&quot;, &quot;illustion&quot;, &quot;Lunchtime&quot;, &quot;double&quot;, &quot;so&quot;] 가 되는 것이고 이들을 벡터화 시켜 모델에 입력하면 되겠죠? 가장 단편적인 예로는 one-hot encoding이 있겠네요.
Time : [1, 0, 0, 0, 0, 0, 0]
is : [0, 1, 0, 0, 0, 0, 0]
an : [0, 0, 1, 0, 0, 0, 0]
물론 이와 같은 방식은 비효율적이므로 실제로는 word2vec과 같은 모델을 사용하지만, 어찌되었든 이런 방식으로 사전을 구성하고 벡터화 시킨다면, 딥러닝 모델에 의미를 가지는 단어를 전달할 수 있습니다.</p>
<p>이 word tokenization이 가지는 문제는 두가지가 있습니다. </p>
<ul>
<li>사전의 크기가 지나치게 커지며</li>
<li>이세상의 모든 단어를 커버할 수 없다면 OOV가 발생한다.</li>
</ul>
<p>입니다. 
영어만 해도 무수히 많은 단어와 고유명사가 존재하는데, 10만개의 단어가 있다면 매 단어마다 10만차원의 vector로 표현을 해야하고, 이는 모델의 파라미터 증가와 비효율적인 연산 오버헤드로 이어집니다. (물론 위의 worst case인 경우를 생각했을 때 입니다.)
또한 신조어나 잘 사용하지 않는 고유명사같은 단어가 입력으로 들어온다면 사전이 모두 커버할 수 없으므로 OOV 문제가 발생하기 쉽습니다. 심지어 첫 번째 문제인 사전의 크기를 줄이면 줄일수록 OOV문제는 더 커지게 됩니다.</p>
<h2 id="subword-tokenization">Subword Tokenization</h2>
<p>위의 문제를 해결하기 위한 방법이 Subword Tokenization입니다.
우리가 주목해야할 것은 <strong>&quot;사전을 구성하는 tokenization 단위를 줄이면 줄일수록 사전의 크기가 줄어든다&quot;</strong> 입니다. Word 단위가 아닌 Character 단위로 사전을 구성한다 해봅시다.
알파벳은 26개이므로 각종 따옴표나 특수문자를 포함한다 해도 100개가 안되는 사이즈의 사전이 완성됩니다.
즉 word tokenization은 단어의 의미를 잘 나타내는 사전을 구성할 수 있지만 그 사이즈가 너무 커서 비효율적이며, character tokenization은 사전의 크기는 매우 작지만 그만큼 각 token 들이 가지는 의미가 희석되어 제대로된 vector화가 불가능하다는 특징이 있습니다.</p>
<p>따라서 이 둘 사이에서 타협점을 찾아 word들을 한번 더 쪼개서 의미를 가진 subword기반으로 tokenizaiton을 하면, 사전의 사이즈를 줄이면서도 OOV 문제또한 해결되지 않냐는 생각에서 나온것이 Subword Tokenization입니다.</p>
<blockquote>
<p>OOV 문제가 해결되는 이유?
<strong>word Tokenization</strong> : [low, lower, newest, wide, widest]
<strong>subword Tokenization</strong> : [l, o, w, e, r, n, s, t, i, d, es, est, lo, low, ne, new, newest, wi, wid, widest]
위와 같이 사전이 구성되었을 때 lowest라는 단어가 들어오면 word Tokenization에서는 OOV가 발생하지만, subword Tokenization에서는 low + est로 조합할 수 있으므로 OOV가 발생하지 않습니다.
말도 안되는 lownrst 라는 단어가 들어온다 해도 low + n + r + s + t 로 표현할 수 있습니다.</p>
</blockquote>
<h1 id="bpe-알고리즘">BPE 알고리즘</h1>
<p>Subword Tokenization에는 WordPiece, SentencePiece, Unigram 등 여러가지 알고리즘이 존재하지만 본 포스팅에서는 Pixionary 어플리케이션에서 사용하고 있는 BPE 알고리즘에 대해 다루겠습니다.</p>
<p>BPE (Byte Pair Encoding) 이란 Subword Tokenization 기법중 하나로 초기에 분할된 사전에서 시작하여 <strong>빈도계산과 병합을 반복</strong>하며 원하는 vocab size 만큼 사전을 구성하는 기법입니다. 알고리즘의 동작순서는 아래와 같습니다.</p>
<ol>
<li>주어진 문장들을 띄어쓰기 기반으로 분리시킵니다. (pre-tokenize)</li>
<li>pre-tokenized 된 단어들의 집합을 등장빈도에 맞춰 dictionary를 구성합니다.</li>
<li>dictionary의 각 단어들을 character 단위로 분할시키고 이들을 vocab에 추가합니다.</li>
<li>dictionary에서 분할된 character들을 2개씩 짝지어 merge 하고, merge 된 subword 중 그 빈도가 가장 높은 subword를 vocab에 추가합니다. 또한 dictionary의 character들을 vocab에 추가된 pair가 반영되도록 업데이트합니다.</li>
<li>사전에 설정한 vocab size가 될 때 까지 3번과 4번 과정을 반복합니다.</li>
</ol>
<p>위의 알고리즘 설명만 보고서는 아마 와닿지 않을 것입니다. 예제를 한번 살펴보죠.
아래와 같은 문장집합이 주어집니다.</p>
<pre><code class="language-text">// 문장 : 빈도수

low lower : 2
low new newest : 4
widest new : 3
lower new widest : 5</code></pre>
<ol>
<li>주어진 문장들을 띄어쓰기 기반으로 분리시킵니다.</li>
</ol>
<pre><code>low, lower, new, newest, widest</code></pre><ol start="2">
<li>위 과정에서 pre-tokenized된 단어들의 집합을 가지고 등장빈도에 맞춰 dictionary를 구성합니다.</li>
</ol>
<pre><code class="language-json">{
    (low, 6),
    (lower, 7),
    (new, 12),
    (newest, 4),
    (widest, 8)
}</code></pre>
<ol start="3">
<li>dictionary의 각 단어들을 character 단위로 분할시키고 이들을 vocab에 추가합니다.<pre><code>dictionary
{
 (&quot;l&quot; &quot;o&quot; &quot;w&quot;, 6),
 (&quot;l&quot; &quot;o&quot; &quot;w&quot; &quot;e&quot; &quot;r&quot;, 7),
 (&quot;n&quot; &quot;e&quot; &quot;w&quot;, 12),
 (&quot;n&quot; &quot;e&quot; &quot;w&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 4),
 (&quot;w&quot; &quot;i&quot; &quot;d&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 8)
}
</code></pre></li>
</ol>
<p>vocab
{&quot;d&quot; &quot;e&quot; &quot;i&quot; &quot;l&quot; &quot;n&quot; &quot;o&quot; &quot;r&quot; &quot;s&quot; &quot;t&quot; &quot;w&quot;}</p>
<pre><code>
4. dictionary에서 분할된 character들을 2개씩 짝지어 merge 하고, merge 된 subword 중 그 빈도가 가장 높은 subword를 알아내야 합니다.</code></pre><p>dictionary - pair 구성
{
    (&quot;lo&quot; &quot;ow&quot;, 6),
    (&quot;lo&quot; &quot;ow&quot; &quot;we&quot; &quot;er&quot;, 7),
    (&quot;ne&quot; &quot;ew&quot;, 12),
    (&quot;ne&quot; &quot;ew&quot; &quot;we&quot; &quot;es&quot; &quot;st&quot;, 4),
    (&quot;wi&quot; &quot;id&quot; &quot;de&quot; &quot;es&quot; &quot;st&quot;, 8)
}</p>
<p>dictionary - 빈도 수 계산
{
    &quot;lo&quot; : 13
    &quot;ow&quot; : 13
    &quot;we&quot; : 11
    &quot;er&quot; : 7
    &quot;ne&quot; : 16
    &quot;ew&quot; : 16
    &quot;es&quot; : 12
    &quot;st&quot; : 12
    &quot;wi&quot; : 8
    &quot;id&quot; : 8 
    &quot;de&quot; : 8
}</p>
<pre><code>
ne와 ew가 16으로 빈도수가 가장 높으므로 알파벳 순서가 빠른 ew를 vocab에 추가하고 dictionary를 업데이트하면 아래와 같습니다.</code></pre><p>dictionary
{
    (&quot;l&quot; &quot;o&quot; &quot;w&quot;, 6),
    (&quot;l&quot; &quot;o&quot; &quot;w&quot; &quot;e&quot; &quot;r&quot;, 7),
    (&quot;n&quot; &quot;ew&quot;, 12),
    (&quot;n&quot; &quot;ew&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 4),
    (&quot;w&quot; &quot;i&quot; &quot;d&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 8)
}</p>
<p>vocab
{&quot;d&quot; &quot;e&quot; &quot;i&quot; &quot;l&quot; &quot;n&quot; &quot;o&quot; &quot;r&quot; &quot;s&quot; &quot;t&quot; &quot;w&quot; &quot;ew&quot;}</p>
<pre><code>
3번과 4번 과정을 통해 dictionary가 pair 하나를 기준으로 업데이트 되었으며 vocab의 사이즈가 10에서 11로 1만큼 증가했습니다.
만약 사전설정된 vocab size가 12라서 위의 과정을 한번 더 진행해야한다고 해봅시다.
</code></pre><p>dictionary - pair 구성
{
    (&quot;lo&quot; &quot;ow&quot;, 6),
    (&quot;lo&quot; &quot;ow&quot; &quot;we&quot; &quot;er&quot;, 7),
    (&quot;new&quot;, 12),
    (&quot;new&quot; &quot;ewe&quot; &quot;es&quot; &quot;st&quot;, 4),
    (&quot;wi&quot; &quot;id&quot; &quot;de&quot; &quot;es&quot; &quot;st&quot;, 8)
}</p>
<p>dictionary - 빈도 수 계산
{
    &quot;lo&quot; : 13
    &quot;ow&quot; : 13
    &quot;we&quot; : 11
    &quot;er&quot; : 7
    &quot;new&quot; : 16
    &quot;ewe&quot; : 4
    &quot;es&quot; : 12
    &quot;st&quot; : 12
    &quot;wi&quot; : 8
    &quot;id&quot; : 8 
    &quot;de&quot; : 8
}</p>
<pre><code>
이렇게 되면 new가 16번으로 빈도가 가장 높으므로 아래와 같이 dictionary와 vocab이 업데이트 되겠네요.
</code></pre><p>dictionary
{
    (&quot;l&quot; &quot;o&quot; &quot;w&quot;, 6),
    (&quot;l&quot; &quot;o&quot; &quot;w&quot; &quot;e&quot; &quot;r&quot;, 7),
    (&quot;new&quot;, 12),
    (&quot;new&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 4),
    (&quot;w&quot; &quot;i&quot; &quot;d&quot; &quot;e&quot; &quot;s&quot; &quot;t&quot;, 8)
}</p>
<p>vocab
{&quot;d&quot; &quot;e&quot; &quot;i&quot; &quot;l&quot; &quot;n&quot; &quot;o&quot; &quot;r&quot; &quot;s&quot; &quot;t&quot; &quot;w&quot; &quot;ew&quot; &quot;new&quot;}</p>
<pre><code>
이렇게 되면 목표로 하는 12 개의 vocab size가 채워졌으므로 BPE 알고리즘이 끝나게 됩니다. 이후엔 vocab의 각 subword들에게 1번부터 12번까지의 ID값을 붙여 NLP 모델에 입력시켜 추론하면 되겠죠? 
단적인 예시를 들어보면 위 vocab 기준으로 &quot;d ew e i new&quot; 라는 문장이 들어왔다면 [1, 11, 2, 3, 12]의 벡터값을 모델에 입력하여 추론결과를 뽑으면 되는 것입니다.

만약 dei나 lon 과 같이 vocab에 없는 단어가 들어온다면 OOV가 나는게 아니냐 하는 의문이 들 수 있습니다. OOV를 처리하는 방법은 OOV에 해당하는 인덱스를 따로 두거나 무시하거나 여러 방법이 있겠지만 **Pixionary에서는 &quot;dei&quot; -&gt; &quot;d ei&quot;, &quot;lon&quot; -&gt; &quot;l o n&quot; 과 같이 vocab에서 조합가능한 pair들을 찾아 강제로 띄어쓰기가 적용된 문자열로 치환하는 방법을 사용하고 있습니다. (사전에 정의된 매우 큰 사이즈의 vocab을 사용하므로 OOV를 처리하지 못하는 경우는 없습니다.)**

# Kotlin으로 구현하는 BPE
&gt; 들어가기에 앞서, 코드가 다소 길기 때문에 모든 코드에 대해 상세하게 설명을 하기보단 각각의 매서드가 무슨 역할을 하고 어떻게 동작하도록 구현이 된 것인지에 대해 설명하는 포스팅입니다. 코드 전문이 궁금하다면 아래의 깃허브에서 확인해주세요
(본 포스팅에서는 그림 위주로 설명하려고 하니 깃허브에서 코드와 함께 보는것을 추천합니다.)
https://github.com/hyuns66/Pixionary/blob/main/app/src/main/java/com/example/findmyphoto/SimpleTokenizer.kt

![](https://velog.velcdn.com/images/renovatio_hyuns/post/322d63a8-06f8-4b83-8327-0d91d2b16306/image.png)
이제 BPE 알고리즘이 어떤식으로 동작하여 주어진 문장을 토큰화 하는지 알았습니다.
그런데, 실제 어플리케이션이 동작할 때에는 매번 수많은 문장 데이터셋을 가지고 빈도를 구할 수 없기 때문에 사전에 대용량 말뭉치를 이용해 미리 계산을 해놓은 pair 쌍 집합과 빈도를 사용하여 Tokenization을 수행합니다.

빈도가 높은 순으로 정렬되어 있으므로 for문을 돌면서 merge가능한 pair를 탐색하고 찾아지는 순서대로 각 토큰들을 subword단위로 merge하고 tokenize하게 됩니다.

이것 말곤 모든 과정이 동일합니다. 이제 kotlin 코드로 어떻게 구현을 하고 있는지 살펴보겠습니다.


## Tokenizer 초기화
Tokenizer 클래스에서 사용하넌 전역변수들은 아래와 같이 초기화가 되고 있습니다.

```kotlin
var encoder : Map&lt;String, Int&gt;
var bpeRanks : Map&lt;Pair&lt;String, String&gt;, Int&gt;
val byteEncoder: Map&lt;Int, Char&gt; = bytesToUnicode()  // (유니코드 정수, 문자)
val byteDecoder: Map&lt;Char, Int&gt; = byteEncoder.entries.associate { (k, v) -&gt; v to k }    // (문자, 유니코드 정수)

init{
    val merges = loadMerges()
    val vocab = loadVocab(merges)
    encoder = vocab.zip(0 until vocab.size).toMap()
    bpeRanks = merges.zip(0 until merges.size).toMap()
}</code></pre><p>눈여겨 보아야할 것은 encoder와 bpeRanks인데, 각각의 역할은 아래와 같습니다.</p>
<ul>
<li>bpeRanks : 조합 가능한 character들의 Pair와 그 우선순위(빈도)를 가지고 있는 Map</li>
<li>encoder : bpeRanks에 의해 조합된 String Sequence들을 token화 하기 위한 String-Int 형식의 Map </li>
</ul>
<p>즉 bpeRanks 객체를 가지고 가능한 모든 character들을 merge하여 String들의 Sequence로 만들고, 이러한 String들을 encoder를 활용하여 Int형 token들의 Sequence로 반환하게 되는 것입니다.</p>
<p><strong>bpeRanks를 만드는데 사용되는 merges는 아래 코드를 통해 가져오게 됩니다.</strong></p>
<pre><code class="language-kotlin">fun loadMerges(): List&lt;Pair&lt;String, String&gt;&gt; {
    // 안드로이드에서 파일 접근 시 Context를 사용하여 파일 경로를 얻습니다.
    // 예제에서는 경로를 직접 지정합니다. 실제 사용 시에는 적절한 파일 경로를 설정해야 합니다.
    val fileInputStream = ApplicationClass.getContext().resources.openRawResource(bpeVocabFilePath)

    fileInputStream.use { inputStream -&gt;
        val merges = inputStream.reader(Charset.defaultCharset()).readText().split(&#39;\n&#39;)
        return merges.drop(1).take(49152 - 256 - 2).map {   // it : &quot;i n&quot;, &quot;t h&quot;, &quot;a n&quot;
        val parts = it.split(&quot; &quot;)       // &quot;i n&quot; -&gt; [&quot;i&quot;, &quot;n&quot;]
        parts[0] to parts[1]        // [&quot;i&quot;, &quot;n&quot;] -&gt; Pair(&quot;i&quot;, &quot;n&quot;)
        }
    }
}</code></pre>
<p>bpeVocabFilePath라는 &#39;대형말뭉치 사전에 의해 미리 정의된 vocab set&#39;을 가져옵니다.
해당 파일 안에는 
i n
t h
a n
과 같이 줄바꿈 문자를 기준으로 merge가능한 character set들이 정의되어 있습니다. 따라서 이러한 파일을 읽어 일련의 과정을 거친 후 (&quot;i&quot;, &quot;n&quot;) 과 같은 Pair 객체들의 배열로 반환하고 있는 매서드입니다.</p>
<p>주목해야할 부분은 .take 확장함수에서 사용하는 세 개의 숫자인데,</p>
<ul>
<li>49152 : 내가 원하는 사전의 사이즈입니다. (사전 정의된 vocab set의 사이즈가 너무 크기 때문에 메모리나 속도적인 측면에서 전부다 사용할 수 없기에 이런식으로 제한을 걸게 됩니다.)</li>
<li>256 : &#39;A&#39; &#39;B&#39; 와 같은 기본 character들과 각종 특수문자, 숫자 등을 위한 유니코드들의 수 입니다.</li>
<li>2 : &quot;&lt;|startoftext|&gt;&quot;, &quot;&lt;|endoftext|&gt;&quot; 토큰을 가지고 문장의 시작과 끝을 표현하게 되는데 이들을 의미합니다.</li>
</ul>
<p><strong>encoder를 만드는데 사용되는 vocab은 아래 코드를 통해 가져오게 됩니다.</strong></p>
<pre><code class="language-kotlin">fun loadVocab(merges : List&lt;Pair&lt;String, String&gt;&gt;) : List&lt;String&gt;{
    var vocab : List&lt;String&gt; = bytesToUnicode().values.toList().map{ it.toString() }
    vocab = vocab + vocab.map { &quot;$it&lt;/w&gt;&quot; }
    vocab = vocab + merges.map {
      it.first + it.second
    }
    vocab = vocab + listOf(&quot;&lt;|startoftext|&gt;&quot;, &quot;&lt;|endoftext|&gt;&quot;)
    return vocab
}</code></pre>
<p>위 merges는 merge 가능한 character들의 Pair를 구했다면 이러한 merges를 인자로 받아 merge과정을 거친 String들의 리스트를 반환하게 됩니다. List&lt;String&gt;.zip 매서드를 통해 각각의 merge완료된 토큰들에 인덱스를 부여하게 되고, 이러한 인덱스들이 최종적으로 벡터화되기 위한 각 파라미터 값으로 들어가게 됩니다.</p>
<h2 id="tokenization-매서드-동작원리">Tokenization 매서드 동작원리</h2>
<p>실제로 문장을 입력받아 Tensor의 형태로 tokenization 하는데 사용되는 매서드는 총 3개가 유기적으로 작동하게 됩니다.</p>
<ul>
<li>tokenize(text : String, n_text : Int) : 입력받은 문장의 앞뒤에 시작토큰과 끝토큰을 추가하고 문장의 최대 길이를 맞추기 위해 slicing 하거나 zero-padding을 수행</li>
<li>encode(text : String) : 정규식 패턴을 통해 주어진 문장을 토큰 단위로 분해하여 각 토큰들을 bpe매서드에 전달. bpe 알고리즘을 통해 토큰들을 다시한번 merge하며 재가공 과정을 거치고, 그 결과를 encoder를 통해 Int형 배열(Tensor 형태)로 반환</li>
<li>bpe(token : String) : 파라미터로 주어진 토큰을 character 단위로 전부 쪼갠 후 bpe 알고리즘을 수행하여 encoder에서 변환 가능한 적절한 문자열로 가공</li>
</ul>
<blockquote>
<p>encode 매서드에서 주어진 문장을 토큰단위로 분해한다는 말 뜻이 무엇일까요?
여기서 사용하는 정규식은 아래와 같습니다.
**&quot;&lt;|startoftext|&gt;|&lt;|endoftext|&gt;|&#39;s|&#39;t|&#39;re|&#39;ve|&#39;m|&#39;ll|&#39;d|\p{L}+|\p{N}|[^\s\p{L}\p{N}]+&quot; **</p>
</blockquote>
<p>요약하자면</p>
<ul>
<li>&#39;s, &#39;t나 문장의 시작과 끝을 알리는 특수한 토큰</li>
<li>여러개의 유니코드 문자로 이루어진 sequence (하나의 단어)</li>
<li>하나의 유니코드 단위 숫자 (123 은 1 2 3 세 개의 숫자로 간주)</li>
<li>공백문자를 제외한 단어나 숫자 이외의 문자 (이모티콘이나 알 수 없는 문자 등)</li>
</ul>
<p>위 조건에 부합하는 요소들을 문장에서 찾아 토큰으로 취급하고 각각 bpe 알고리즘을 적용하겠다는 뜻입니다.</p>
<p>&quot;Hello, world! 123 😊&quot; -&gt; [&quot;Hello&quot;, &quot;,&quot;, &quot;world&quot;, &quot;!&quot;, &quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;😊&quot;] 와 같이 말이죠.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5b6d5135-39af-4118-b2a8-17109f16ab47/image.png" alt=""></p>
<p>전체적인 동작과정은 위 그림과 같습니다. 자세한 부분은 아래에서 코드와 함께 보면서 설명드릴게요</p>
<h3 id="tokenizetext-string-n_text-int--76">Tokenize(text: String, n_text: Int = 76)</h3>
<pre><code class="language-kotlin">fun tokenize(text: String, n_text: Int = 76): Pair&lt;IntArray, Int&gt; {
        val sot = this.encoder[&quot;&lt;|startoftext|&gt;&quot;]!!
        val eot = this.encoder[&quot;&lt;|endoftext|&gt;&quot;]!!
        var tokens = encode(whitespaceClean(text.trim()))
        // 문장 시작, 끝  토큰 추가
        tokens = listOf(sot) + tokens.slice(
            0..min(n_text - 1, tokens.lastIndex)
        ) + listOf(eot)
        return Pair((tokens + List(n_text + 1 - tokens.size) { 0 }).toIntArray(), tokens.size)    // 남은 공간 0패딩
    }</code></pre>
<p>가장먼저 진입하게 되는 매서드 입니다. 입력 문자열을 text 파라미터로 받아 문장의 시작과 끝을 알리는 토큰 (startoftext, endoftext)들을 추가합니다.
이후 encode 메서드를 통과하여 문자열이 최종적으로 변환된 Tensor를 얻고 고정길이를 맞추기 위해 slicing하거나 zero-padding 하여 최종 결과로써 반환합니다.</p>
<h3 id="encodetext--string">encode(text : String)</h3>
<pre><code class="language-kotlin">fun encode(text : String) : List&lt;Int&gt;{
        val bpeTokens = mutableListOf&lt;Int&gt;()

        pattern.findAll(text).forEach {
            val tokenBytes = it.value.toByteArray(StandardCharsets.UTF_8)
            val token = tokenBytes.map { byte -&gt; byteEncoder[byte.toInt()] ?: &quot;&quot; }.joinToString(&quot;&quot;)
            val bpeTokenized = bpe(token).split(&#39; &#39;)
            bpeTokens.addAll(bpeTokenized.mapNotNull { encoder[it] })
            Log.d(&quot;word_bpe&quot;, bpeTokens.toString())
        }
        return bpeTokens
    }</code></pre>
<p>두번째로 진입하게 되는 매서드 입니다. 주어진 문자열을 정규식에 의하여 토큰단위로 분할합니다. 위 그림에서 보면 이 부분에 해당되겠네요</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/28b9f8f1-00e2-4eca-9393-5256a537d3c5/image.png" alt=""></p>
<p>따라서 토큰단위로 분할하여 bpe 알고리즘 수행 후 나온 결과 문자열을 encoder에 대입하여 Tensor(bpeTokens)로 변환하여 리턴하는 매서드입니다.</p>
<pre><code class="language-kotlin">val tokenBytes = it.value.toByteArray(StandardCharsets.UTF_8)
val token = tokenBytes.map { byte -&gt; byteEncoder[byte.toInt()] ?: &quot;&quot; }.joinToString(&quot;&quot;)</code></pre>
<p>위 코드는 경우 알 수 없는 언어나 문자를 처리할 수 없는 경우를 대비하기 위하여 유니코드 정수나 문자로 치환하는 과정이므로 일반적인 영어를 사용한다면 신경쓰지 않아도 되는 코드입니다.</p>
<h3 id="bpetoken--string">bpe(token : String)</h3>
<pre><code class="language-kotlin">fun bpe(token : String) : String{
    // bpe가 필요없는 토큰인 경우 바로 리턴
    if (cache.containsKey(token)) {
        return cache[token]!!
    }
    // &#39;king&#39; -&gt; [&#39;k&#39;, &#39;i&#39;, &#39;n&#39;, &#39;g&lt;/w&gt;&#39;]
    var word = mutableListOf&lt;String&gt;()
    for ((index, text) in token.withIndex()){
        var w = text.toString()
        if (index == token.length-1){
            w += &quot;&lt;/w&gt;&quot;
        }
        word.add(w)
    }
    // [&#39;k&#39;, &#39;i&#39;, &#39;n&#39;, &#39;g&lt;/w&gt;&#39;] -&gt; {(&#39;k&#39;, &#39;i&#39;), (&#39;i&#39;, &#39;n&#39;), (&#39;n&#39;, &#39;g&lt;/w&gt;&#39;)}
    var pairs = getPairs(word)
    if (pairs.isEmpty()) {
        return token + &quot;&lt;/w&gt;&quot;
    }
    while(true){
        val bigram = pairs.minByOrNull{ pair -&gt; bpeRanks[pair] ?: Int.MAX_VALUE}
        if (bigram !in bpeRanks || bigram == null) {
            break
        }
        val first = bigram.first
        val second = bigram.second
        val new_word = mutableListOf&lt;String&gt;()
        var i = 0
        Log.d(&quot;word&quot;, word.toString())
        // [&#39;k&#39;, &#39;i&#39;, &#39;n&#39;, &#39;g&lt;/w&gt;&#39;] -&gt; [&#39;k&#39;, &#39;in&#39;, &#39;g&lt;/w&gt;&#39;] : merge 가능한 쌍 merge 해서 새로운 리스트 생성
        while(i &lt; word.size){
            try{
                for(j in i..word.size){   // catch문에서 적절한 처리하기위해 마지막까지 first 못찾는 경우 일부러 outOfBoundsException 일으킴
                    if (word[j] == first){      // i 이후로 first에 해당하는 글자 찾으면 인덱스 j에 반환
                        new_word += word.slice(i..&lt; j)  // first 이전의 글자들은 그대로 복사
                        i = j       // first 이후부터 재탐색
                        break
                    }
                }
            } catch (e : IndexOutOfBoundsException){    // first에 해당하는 글자 못찾으면 전부 복사하고 break
                new_word += word.slice(i..word.lastIndex)
                break
            }
            if (word[i] == first &amp;&amp; i &lt; word.lastIndex &amp;&amp; word[i+1] == second){ // first 뒤에 second가 붙어있으면 하나로 merge
                new_word.add(first+second)
                i += 2
            } else {
                new_word.add(word[i])
                i += 1
            }
        }
        Log.d(&quot;word_new&quot;, new_word.toString())
        word = new_word
        // word가 하나의 단어가 되면 break 아니면 pairs 갱신 후 반복
        if (word.size == 1){
            break
        } else {
            pairs = getPairs(word)
        }
    }
    // [&#39;k&#39;, &#39;in&#39;, &#39;g&lt;/w&gt;&#39;] -&gt; &quot;k in g&lt;\w&gt;&quot;
    val result_word = word.joinToString(&quot; &quot;)
    cache[token] = result_word
    Log.d(&quot;word_result&quot;, result_word)
    return result_word
}</code></pre>
<p>가장 핵심적인 bpe 알고리즘을 구현한 매서드이며 위 그림에서 파란색 네모를 친 부분에 해당됩니다.
즉 주어진 토큰에 대하여 bpe를 수행하고, encoder가 인코딩할 수 있는 문자열으로 가공하는 역할을 합니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/7a98f472-3c6e-49ba-b2e5-b17b361c0da5/image.png" alt=""></p>
<p>bpe()매서드는 분할된 모든 토큰에 대해 각각 bpe를 수행하여 최종적인 형태로 문자열을 가공하고, encode() 매서드는 공백문자 단위로 슬라이싱하여 encoder에 대입하고, Tensor를 구성하는 것을 담당하고 있습니다. 마지막으로 zero-padding은 tokenize() 매서드가 마무리하여 최종적으로 리턴하게 되는 것이죠.</p>
<p>그럼 위 예제에서 주어진 hello 라는 토큰이 bpe에서 어떻게 변화되는지 그 과정을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/25339e85-a11b-494d-a576-d6238a830d99/image.png" alt=""></p>
<p>먼저 주어진  토큰을 위의 빨간색 네모부분과 같은 형태로 사전 가공한 상태에서 알고리즘이 시작됩니다.</p>
<p>여기서 괄호안의 쌍은 각각 merge를 시도할 문자열 쌍을 의미합니다.
이제 bpeRanks를 보면서 실제로 merge가 가능한 쌍을 찾아 merge를 해나가면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ba047dbf-63b7-434f-93f0-17aaef67e168/image.png" alt=""></p>
<p>merge 가능한 쌍이 4개나 나왔지만 숫자가 가장 작은 쪽이 우선순위가 높기에 h와 e가 merge하여 새로운 merge 가능 쌍 리스트를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/3685eca1-61bc-4b5a-b5c9-50b7e289661c/image.png" alt=""></p>
<p>역시 마찬가지로 merge 가능한 쌍중 우선순위가 가장 높은 l과 l을 merge하여 새로운 리스트를 생성합니다.
이 과정에서 주의해야할 점은 o&lt;\w&gt;와 o는 다른 문자로 취급한다는 점입니다. &lt;\w&gt; 문자는 토큰의 마지막을 알리는 문자이므로 이를 고려하여 bpeRanks를 보시면 될것 같아요.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/8a03c704-da07-4b98-bccb-dffab8866017/image.png" alt=""></p>
<p>마지막으로 merge 가능한 쌍까지 merge하고나면 더이상 합칠 수 있는 쌍이 존재하지 않게 되고 bpe 알고리즘이 끝나게 됩니다. </p>
<p>분명히 완벽한 하나의 단어가 만들어지지 않았음에도 더이상 merge 가능한 쌍이 사전에 존재하지 않기 때문에 bpe가 끝났습니다. 따라서 입력 문장은 hello로 들어왔지만 최종적으로 가공된 문장은 &quot;he llo&quot; 로 가공되어 나가는 것입니다.</p>
<p>이제 encoder를 다시 볼까요</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d11e6132-081c-4fb9-8936-e6fed0ca0def/image.png" alt=""></p>
<p>&quot;hello&quot;라는 토큰은 encoder에 존재하지 않기때문에 변환 불가능한 토큰이었습니다.
그러나 최종적으로 가공된 &quot;he llo&lt;\w&gt;&quot; 라는 토큰은 [1, 7] 이라는 벡터로써 나타낼 수 있다는 것을 알 수 있습니다. </p>
<p>각 토큰들에 대해 bpe를 수행하고 인코딩한 결과가 아래와 같다면</p>
<ul>
<li>&lt;|startoftext|&gt; : [49150]</li>
<li>hello : [1, 7]</li>
<li>world : [23, 42]</li>
<li>! : [893]</li>
<li>1 : [5]</li>
<li>2 : [6]</li>
<li>3 : [7]</li>
<li>😊 : [923]</li>
<li>&lt;|endoftext|&gt; : [49151]</li>
</ul>
<p>입력 문자열에 대해 최종적으로 인코딩 된 tensor는 다음과 같을 것입니다.
[49150, 1, 7, 23, 42, 893, 5, 6, 7, 923, 49151, 0, 0, ,,,, 0]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hilt 들고 MVVM 정복] 4. Repository, 그리고 DIP]]></title>
            <link>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-4.-Repository-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DIP</link>
            <guid>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-4.-Repository-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DIP</guid>
            <pubDate>Sun, 24 Mar 2024 09:09:06 GMT</pubDate>
            <description><![CDATA[<h1 id="domain-layer-정리">Domain layer 정리</h1>
<p>지금까지 1~3편을 통해 아무런 아키텍처도 적용되지 않은 앱에 MVVM을 한스푼 넣어줬는데요, 먼저 UI를 두 가지 클래스로 분리하여 역할을 나눴고 (UI layer), UI가 동작하기 위해 필요한 복잡한 비즈니스 로직을 ViewModel로부터 분리하여 UseCase로 만들었습니다. (DomainLayer)</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/88764dbe-d0e5-4a15-8f69-478581bea4d7/image.png" alt=""></p>
<p><a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI">3편</a>의 글을 다 읽고오신 분이라면 UseCase를 설명하면서 Repository에 대해 &#39;Data를 가져오기 위한 로직의 구현부 혹은 인터페이스&#39; 라고 설명했던것을 기억하실 텐데요, 이번 포스팅에서는 이 부분에 대해 다뤄보려고 합니다.</p>
<p>자, 위 다이어그램에서 UI layer는 <a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel">2편</a>에서 다뤘고, Domain layer는 <a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI">3편</a>에서 다뤘습니다. 이제 거대한 Data layer가 남아있네요. Domain layer에 대해 정리해보면 </p>
<blockquote>
<p><strong>UseCase</strong>의 비즈니스 로직이 <strong>추상화된 Repository</strong>의 매서드를 통해 <strong>Data layer에 있는 구현부</strong>에 접근하여 데이터를 서버로부터 가져오고 이를 <strong>Model에 있는 표현식</strong>으로 만들어 <strong>ViewModel에 전달</strong>한다! 입니다.</p>
</blockquote>
<h1 id="repository-패턴">Repository 패턴</h1>
<p>지금까지 과정에서 UseCase를 통해 ViewModel에서 UI State를 셋팅하기 위한 비즈니스 로직을 완벽하게 분리했습니다. 그렇다면 비즈니스 로직에서 네트워크 통신이나 내장 DB 데이터 입출력 과정이 필요하다면 어떻게 해야 할까요? 당연히 UseCase 안에 모든 과정을 구현해도 되겠지만 이렇게 하면 생기는 가장 큰 문제점이 있습니다. UseCase의 역할이 너무 많아질 뿐더러 위 클린아키텍처 구조의 Data layer의 존재가 지워집니다.</p>
<p>자 그럼 Repository 패턴에 앞서 본질적인 의문을 먼저 해소하고 가야겠죠?</p>
<h2 id="data-layer는-왜-존재하는가-feat-soc">Data layer는 왜 존재하는가 feat. SoC</h2>
<p>Data layer 뿐만 아니라 클린아키텍처 구조에서 왜 data, domain, ui 세 개의 layer로 나누는 것도 모자라 각 layer들조차 여러개의 컴포넌트로 쪼개놓았을까요?
그것은 바로 소프트웨어 엔지니어링의 기본 원칙중 하나인 <strong>관심사의 분리 (Separation of Concerns, SoC)</strong> 때문입니다.</p>
<p>SoC는 프로그램을 잘 정의된 구분되는 기능적 단위로 나누어 각 단위가 자신의 관심사에만 집중하도록 하는 설계 원칙입니다. 이 원칙은 코드의 유지보수성, 확장성 및 가독성을 향상시키는 데 도움이 되며 주요 특징을 아래와 같이 정리할 수 있습니다.</p>
<ol>
<li><p><strong>모듈성 향상</strong>
각 부분이 특정 기능이나 책임에 집중함으로써, 코드의 모듈성이 향상됩니다. 이는 팀 내에서 작업의 분배를 용이하게 하고, 개발 과정을 보다 효율적으로 만듭니다.</p>
</li>
<li><p><strong>재사용성 증가</strong>
관심사가 잘 분리된 코드는 다른 부분이나 프로젝트에서 재사용하기 더 쉽습니다. 이는 재사용 가능한 코드 라이브러리나 모듈을 만드는 데 기여할 수 있습니다.</p>
</li>
<li><p><strong>유지보수성 개선</strong>
코드의 특정 부분을 변경해야 할 때, 관심사의 분리는 그 변경이 다른 부분에 미치는 영향을 최소화합니다. 이는 유지보수 작업을 보다 예측 가능하고 관리하기 쉽게 만듭니다.</p>
</li>
<li><p><strong>테스트 용이성</strong>
각 부분이 독립적으로 존재하므로, 단위 테스트와 같은 테스트 작업이 더 쉽고 효율적으로 이루어질 수 있습니다. 모의 객체나 가짜 구현을 사용하여 각 부분을 독립적으로 테스트할 수 있습니다.</p>
</li>
<li><p><strong>가독성 및 이해도 향상</strong>
코드가 잘 구조화되고 각 부분이 명확한 책임을 가짐으로써, 코드의 가독성이 향상되고 다른 개발자가 코드를 이해하기 쉬워집니다.</p>
</li>
<li><p><strong>확장성</strong>
애플리케이션의 특정 부분만 변경하거나 확장해야 할 때, SoC는 이러한 변경이나 확장을 보다 용이하게 합니다. 각 부분이 독립적으로 존재하기 때문에, 새로운 기능이나 요구 사항을 쉽게 통합할 수 있습니다.</p>
</li>
</ol>
<h2 id="다시-돌아와서">다시 돌아와서...!</h2>
<p>이제 의문이 해소 되셨나요? 즉 클린 아키텍처 구조도에서 나눠놓은 각 layer들과 layer안의 모듈들은 SoC 원칙에 따라 각각의 역할을 수행하기 위해 모듈화된 단위이고 우리는 이에 맞춰 개발을 진행해야 하기에 MVVM 아키텍처를 공부하고 있는 것입니다. 생각해보면 각각이 무슨 역할을 하는지, 그리고 실제로 역할이 잘 분리되어 있다는 것 또한 알 수 있습니다.</p>
<ul>
<li>View : UI를 그리고 유저와 상호작용하며 입력을 받는다</li>
<li>ViewModel : 유저입력을 받아 UI를 그리기 위한 정보를 hold 하고 업데이트 한다.</li>
<li>UseCase : ViewModel이 정보를 업데이트하기 위한 비즈니스 로직을 담당한다.</li>
</ul>
<p>와 같은 내용들 말입니다.</p>
<p>자 그럼 UseCase는 말그대로 비즈니스 로직만을 담당해야 하므로 데이터 입출력 작업에 대해서는 알지 못해야 합니다. 데이터 관련된 내용은 data layer가 담당을 하고 있기에 UseCase는 이 Data layer의 어느 구현부에 접근하여 데이터를 가져와야 하는데 우리는 이를 Repository 패턴을 활용하여 구현합니다.</p>
<p>Repository 패턴에 대해 구구절절 설명하기 전에 먼저 어떻게 구현하는지 다이어그램을 한번 그려볼게요.  ... 짠!</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a71ababf-3901-4785-9fb8-916a5a15302e/image.png" alt=""></p>
<p>공식문서의 Repository 패턴 그림을 가져와 제가 조금 수정했습니다. <del>감쪽같죠?</del>
가장 위의 View에서 유저입력을 받아 가장 아래쪽의 data source로부터 data를 가져오기까지 어떤 의존관계를 맺고있는지에 대한 다이어그램입니다. 쭉쭉쭉 내려와서 UseCase쪽에서 추상화된 Repository를 접근하고 있고 이 추상화 계층을 구현한 local / remote 구현체가 실제 데이터를 꺼내오고 있는 모양입니다.</p>
<p>추상화? 이게 왜 필요하고, 굳이 remote, local의 구현부를 따로 만드는 이유가 무엇인지 아래 내용을 통해 차차 알아봅시다.</p>
<h2 id="다시-repository-패턴이란">다시, Repository 패턴이란?</h2>
<p>레포지토리 패턴은 애플리케이션의 비즈니스 로직과 데이터 액세스 레이어 사이의 분리를 촉진하는 설계 패턴입니다. 이 패턴의 주요 목적은 데이터 소스와의 상호 작용을 추상화하여 데이터 액세스 로직을 캡슐화하는 것입니다. 레포지토리 패턴을 사용하면 UseCase는 데이터베이스나 외부 서비스와 같은 데이터 소스의 세부 구현 사항에 대해 알지 않고도 데이터를 조작할 수 있습니다.</p>
<p>레포지토리 패턴은 대체로 다음과 같은 구성 요소로 이루어집니다:</p>
<ul>
<li><p><strong>엔티티(Entity)</strong>: 데이터베이스 테이블을 대표하는 도메인 엔티티입니다.</p>
</li>
<li><p><strong>API Service</strong> : 원격 데이터소스에 접근하기 위한 메서드를 구현하여 제공합니다.</p>
</li>
<li><p><strong>레포지토리 인터페이스(Repository Interface)</strong>: 데이터 액세스 로직에 대한 추상화를 제공합니다. 이 인터페이스는 데이터를 조회, 추가, 수정, 삭제하는 메소드를 정의합니다.</p>
</li>
<li><p><strong>레포지토리 구현 클래스(Repository Implementation Class)</strong>: 레포지토리 인터페이스를 구현하는 클래스입니다. 이 클래스는 실제 데이터 소스와의 상호 작용을 담당합니다.</p>
</li>
</ul>
<p>즉 위 다이어그램에서 Repository는 레포지토리 인터페이스에 해당하며 데이터의 출처(local/remote)와 상관없이 동일한 인터페이스로 데이터에 접근할 수 있도록 해주는 추상화 계층인 것입니다.</p>
<h1 id="di는-과연-만능인가">DI는 과연 만능인가</h1>
<p>UseCase에서 바로 data source 구현부를 주입받아 사용하면 되는데 왜 굳이 추상화 계층을 통해 접근하게 구현해야 할까요?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/2ca540e1-7042-415d-be91-d174961b7499/image.png" alt="">
<del>제가 이런 그림에 대한 견문은 없지만 소프트웨어 추상화에 대해선 설명해줄 수 있습니다 ㅎㅎ</del></p>
<p>이전에 <a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI">3편</a>에서 DI를 통해 의존성을 주입함으로써 결합도를 어느정도 낮추었고 의존성 변경에 대해 어느정도 안정적으로 대응할 수 있도록 했습니다.</p>
<p>그런데 이런생각을 하셨다면 아주 잘 이해한 것입니다. <strong>&#39;아무리 DI를 통해 의존성을 외부에서 주입해준다 해도, 해당 객체를 클래스 내에서 사용하는건 동일한데, 주입받는 객체에 변경점이 생기면 클래스 내부에서도 변경이 일어날 수 있는거 아닌가?&#39;</strong></p>
<p>즉 의존성을 가지는 클래스를 직접 생성하는 것에서 주입받는것으로 바꾸었기 때문에 &#39;직접 의존&#39;에서 &#39;간접 의존&#39; 으로 바뀐 것일 뿐 실제로 해당 클래스의 구현에 의존하고 있다는 사실은 바뀌지 않습니다.</p>
<p>구현에 의존한다는 말이 무슨뜻인지 나타내기 위해 시나리오를 한번 들어볼게요.</p>
<ul>
<li>로그인 기능을 담당하는 LoginUseCase를 만들어 로그인을 수행하도록 했습니다.</li>
<li>로그인을 위해 네트워크에 접근하여 인가를 받는 AuthService 구현체를 통해 네트워크 로직에 접근하고 있습니다.</li>
<li>서버팀에서 갑자기 신기술을 도입하여 보안이 강화된 버전의 API를 릴리즈 하여 안드로이드에서도 앱을 업데이트하며 새로운 AuthTokenService 구현체를 추가하였습니다.</li>
<li>기존의 LoginUseCase에서 사용하던 AuthService를 새로 릴리즈된 AuthTokenService로 바꾸면서 UseCase의 전반적인 로직이 수정되었습니다.<pre><code class="language-kotlin">class AuthService(){    // &lt;- 기존 사용하던 ApiService
  fun validate() : Boolean {
      // ...
  }
}
</code></pre>
</li>
</ul>
<p>class AuthTokenService(){    // &lt;- 새로 기능이 탑재되어 강화된 ApiService
    fun validateToken() : String {    // &lt;- 매서드 동작방식 변경
        // ...
    }
}</p>
<p>class LoginUseCase(val childClass : AuthService){    // &lt;- 주입받는 클래스타입과
    fun login() : Boolean {    // &lt;- 사용하던 매서드 모두 수정이 일어나야 함
        return childClass.create()
    }
}</p>
<pre><code>
&gt; 자, API Service 구현체가 추가되고 매서드 동작방식이 바뀜에 따라 UseCase까지 그 영향이 전파되었습니다. 만약 AuthService를 사용하는 UseCase가 Login 뿐만 아니라 10개, 100개 더 있었다면 너무 끔찍하겠죠?

# 의존 역전 원칙 (DIP)
이를 해결하기 위한 방법이 객체지향 개발방법론에서 항상 이야기하는 의존역전 원칙 (Dependency Inversion Principle, DIP) 을 지키는 것입니다. 

의존성 역전 원칙은 다음과 같이 정의될 수 있습니다

&gt; - 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항은 추상화에 의존해야 한다.
- 쉽게말해 **&#39;구현체 말고 추상화에 의존해라&#39;**

![](https://velog.velcdn.com/images/renovatio_hyuns/post/1f39a2f0-40c5-4335-8c82-4b678efab0ea/image.png)

추상화? 는 interface를 말하는거 같은데 어떻게 빈 껍데기에 의존을 할 수 있을까요? 그리고 왜 그렇게 하라는 걸까요?

먼저 DI를 할 때 추상화 interface를 상속받은 구현체를 주입해줌으로써 interface에 대한 의존을 해결할 수 있습니다.

그리고 그 이유는 위에서 이야기 했던것처럼 두 모듈간 의존이 맺어진 관계에서 한 모듈의 변경점이 본인에게 의존하고 있는 다른 모듈에게도 전파되는 문제점을 해결하기 위함입니다. 
즉 서로 다른 모듈에 직접 접근하는 것이 아니라 가운데 허브(추상화 계층)를 하나 두고 허브를 통해 접근하도록 구현하라는 것입니다.

해외여행 갈 때 나에게 만능 어댑터가 하나 있다면 해당 나라의 정격전압이 220V 인지 110V인지 알 필요 없이 그냥 만능 어댑터에 냅다 꽂으면 되는것과 같은 원리입니다.

A 클래스가 B 클래스에 의존성을 가지고 있을 때 B 클래스를 직접 참조하지 않고 B 클래스의 추상화 버전만 참조한다면 B의 실제 구현부가 어떻게 바뀌었든 참조 관계에는 영향을 받지 않게 된다는 것입니다.

![](https://velog.velcdn.com/images/renovatio_hyuns/post/be6c0bce-3adc-4340-baa9-a6ea9b6939e4/image.png)

시각화를 한번 해보았는데요, UseCase에서 DB관련 작업을 하기위해 필요한 매서드는 모두 Interface에 정의되어있습니다. 따라서 내부 구현사항이 어떻게 바뀌었든 UseCase는 알 필요 없고 Interface를 통해 메소드를 호출만 하면 됩니다.
만약 내부 구현사항은 RepositoryImpl 이라는 구현체에서 직접 구현이 되어 있는데 버전이 올라가면서 구현부가 바뀐다면 동일한 Repository를 상속한 다른 구현체를 새로 만들어 주입해주면 됩니다. 
초록색으로 표시된 의존성 주입의 경우 DI를 담당하는 클래스가 어떤 구현체를 주입해줄지 결정하기 때문에 UseCase입장에서는 Repository의 구현체에 대해 아무것도 몰라도 역할을 잘 수행할 수 있겠죠?

정리해보면
&gt; UseCase에서 DB나 네트워크 작업을 할 경우 비즈니스 로직과 데이터엑세스에 대한 역할을 분리하기 위해 레포지토리 패턴을 사용합니다.
레포지토리 패턴은 각 데이터 엑세스에 필요한 **ApiService, Entity** 와 이를 이용하여 접근 로직을 구현한 **RepositoryImpl, UseCase**가 데이터엑세스 로직에 접근하기 위한 매서드를 정의해둔 **Repository 추상화 계층** 네 가지로 구성됩니다.
그리고 위 다이어그램과 같은 의존성 구조를 통하여 DIP를 달성하고 Repository 패턴을 구현할 수 있습니다.

# 구현 방법
```kotlin
class SignupUseCase {

    suspend operator fun invoke(username, password){
        // TODO : 실제 회원가입 요청 API 호출
    }

    suspend fun checkId(id : String){
        // TODO : 아이디 중복체크 API 호출
    }

    suspend fun checkNickname(name : String){
        // TODO : 닉네임 중복체크 API 호출
    }

    // 비즈니스 로직
}
</code></pre><p>위 코드처럼 UseCase에서 데이터 엑세스 로직이 필요한 경우가 생겼다고 가정해 봅시다.
위에서 설명한 내용 대로라면 UseCase는 Repository interface에 의존해야 하므로 아래와 같이 구성해볼 수 있습니다.</p>
<pre><code class="language-kotlin">interface UserRepository {
    suspend fun requestCheckId(id) : ApiResult&lt;Boolean&gt;
    suspend fun requestCheckNickname(name) : ApiResult&lt;Boolean&gt;
    suspend fun requestSignUp(username, password) : ApiResult&lt;Unit&gt;
}

class SignupUseCase(private val repository: UserRepository) {

    suspend operator fun invoke(username, password) : ApiResult&lt;Unit&gt;{
        return repository.requestSignUp(username, password)
    }

    suspend fun checkId(id : String) : ApiResult&lt;Boolean&gt;{
        return repository.requestCheckId(id)
    }

    suspend fun checkNickname(name : String) : ApiResult&lt;Boolean&gt;{
        return repository.requestCheckNickname(name)
    }

    // 비즈니스 로직
}
</code></pre>
<p>UseCase가 interface에 의존하도록 구성을 해봤고 UseCase가 하는건 그저 interface의 매서드를 호출해서 그대로 반환하는거밖에 없습니다. (만약 데이터엑세스 후에 추가적인 데이터 가공이나 전처리가 필요하다면 적절한 비즈니스 로직을 추가해줄 수 는 있겠죠.)</p>
<p>이제 위에서 설명한 대로라면 DI를 담당하는 부분에서 Repository 구현체를 직접 주입해주는 과정이 남아있습니다. DI를 담당하는 부분은 어디일까요? ViewModelFactory입니다.</p>
<pre><code class="language-kotlin">class JoinViewModel(signupUsecase : SignupUseCase) : ViewModel() {

    // ...
    // ViewModel 로직
    // ...

    // 뷰모델 의존성 주입을 위한 Factory
    // 여기서 ViewModel에 관련된 모든 의존성 주입이 일어남
    companion object {

        @Suppress(&quot;UNCHECKED_CAST&quot;)
        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            override fun &lt;T : ViewModel&gt; create(
                modelClass: Class&lt;T&gt;,
            ): T {
                // 원래 이 코드였는데 Repository 의존성이 추가되었으므로 변경점이 생깁니다.
                // val signupUseCase = SignupUseCase()

                val userDataSource = BeeringApplication.retrofit.create(UserApi::class.java)
                val signupUseCase = SignupUseCase(UserRepositoryImpl(userDataSource))

                return JoinViewModel(
                    signupUseCase
                ) as T
            }
        }
    }
}</code></pre>
<p><a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI#viewmodelfactory">3편</a>에서 다뤘던 내용과 유일한 차이점은 UseCase에 의존성을 추가로 주입해주기 위해 data source를 생성하는 부분이 추가되었다는 점입니다. userDataSource가 뭔지는 레트로핏을 사용해보셨다면 아실거라 믿습니다.</p>
<p>어? UseCase가 의존하고 있는 클래스는 UserRepository interface인데 실제로 ViewModelFactory에서 주입해주고 있는 클래스는 UserRepositoryImpl 클래스네요?
이 구현체 클래스는 어떻게 생겼을까요?</p>
<pre><code class="language-kotlin">class UserRepositoryImpl(
    private val userApi: UserApi
) : UserRepository {
    override suspend fun requestCheckId(id: String): ApiResult&lt;Boolean&gt; {
        val response = userApi.checkUserId(id)

        if (response.isSuccessful) {
            response.body()?.let { it -&gt;
                return ApiResult.Success(it.result)
            }
        }
        return ApiResult.Fail(response.code(), response.message())

    }

    override suspend fun requestCheckNickname(name: String): ApiResult&lt;Boolean&gt; {
        val response = userApi.checkNickname(name)

        if (response.isSuccessful) {
            response.body()?.let { it -&gt;
                return ApiResult.Success(it)
            }
        }
        return ApiResult.Fail(response.code(), response.message())
    }

    override suspend fun requestSignUp(username: String,
                                     password: String): ApiResult&lt;Unit&gt; {
        val apiRequest = JoinRequest(username, password)
        val response = userApi.signUp(apiRequest)

        if (response.isSuccessful) {
            response.body()?.let { it -&gt;
                return ApiResult.Success(it)
            }
        }
        return ApiResult.Fail(response.code(), response.message())
    }
</code></pre>
<p>뭐 대충 이렇게 생겼습니다. 별거 없지 않나요? 그냥 Retrofit API 객체 받아서 통신하고 response 결과 따라서 성공, 실패 객체로 패키징해서 반환하는 뭐 그런 로직입니다.</p>
<p>중요한건 클래스가 UserRepository를 상속하고 있는 부분입니다. 그렇기 때문에 UserRepository에 선언되어 있는 모든 매서드를 구현해주어야 하고 이 클래스가 구현부가 되는것이죠.</p>
<p>뭐 레포지토리 패턴이니 DI니 DIP니 어려운 말을 주구장창 늘어놓긴 했는데 위 개념들에 대해 이해하고나서 각각의 객체가 무슨역할을 하는지 뜯어보니 막상 역할의 분리가 확실하게 일어나서 별로 안어렵지 않나요?</p>
<ul>
<li>UseCase는 ViewModel에서 데이터 관리에 필요한 비즈니스 로직을 담당합니다.</li>
<li>Repository는 UseCase가 데이터엑세스 시 필요한 매서드들을 추상화하여 UseCase가 해당 로직에 접근할 수 있는 방법을 제공합니다.</li>
<li>RepositoryImpl은 Repository를 상속하여 직접 네트워크에 연결하여 데이터를 가져오고, 적절히 가공하여 반환하는 역할을 합니다.</li>
<li>ViewModel Factory는 모든 의존성 주입을 담당하여 RepositoryImpl을 생성하고 이를 UseCase에 주입해주며, 이렇게 생성한 UseCase를 다시 ViewModel에 주입하여 ViewModel을 생성할 수 있도록 합니다.</li>
</ul>
<p>이것이 CleanArchitecture가 강조하는 장점이라고 볼 수 있습니다.</p>
<h1 id="이쯤에서-architecture-구조도-복습">이쯤에서 Architecture 구조도 복습</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/09e5dbfd-d98c-43fe-91bc-4c6beba91dea/image.png" alt="">
네.. 또 등장했습니다 ㅎㅎ
이 그림을 처음 마주했을 때 겁나 어려워 보이고 뭐 프로젝트 하나 만드는데 이렇게 많은 컴포넌트가 필요해 싶기도 하고 눈에 들어오지도 않았을 겁니다.
그런데 이제 다 아는내용 아닌가요?</p>
<ul>
<li><strong>ViewModel</strong>과 <strong>View</strong> 컴포넌트는 UI화면을 UI 데이터와 UI 로직의 구성요소로 분리하였습니다.</li>
<li><strong>UseCase</strong>는 ViewModel에서 필요한 비즈니스 로직을 제공하고 있습니다.</li>
<li><strong>Model</strong>은 DomainLayer와 UILayer 등에서 사용할 수 있는 데이터 표현식들을 의미합니다. (쉽게말하면 그냥 코틀린의 data class들입니다.)</li>
<li><strong>Repository</strong>는 UseCase가 데이터에 엑세스 할 수 있는 방법을 제공하고 있습니다.</li>
<li><strong>RepositoryImpl</strong>은 Repository의 매서드가 어떻게 동작해야하는지 그 동작 방식을 구현하고 있습니다.</li>
<li><strong>API</strong>는 서버와 통신하기위해 만든 RetrofitInterface를 말합니다.</li>
<li><strong>DataSource</strong>는 Retrofit 라이브러리를 활용하여 RetrofitInterface를 빌드한 객체를 말합니다. RepositoryImpl에서 이 객체를 활용하여 서버와 통신하게 되죠.</li>
</ul>
<p>이렇게 각자의 역할에 맞게 컴포넌트를 분리하여 설계하는 방식을 CleanArchitecture라고 부르며 우리는 이를 ViewModel을 활용하면서 MVVM 아키텍처로 설계하였습니다.</p>
<blockquote>
<p>Entity, Mapper에 대해 설명을 생략했는데 이는 내부 저장소 접근에 관련된 컴포넌트들입니다. Entity는 RoomDB라이브러리를 사용했다면 아실것이고, 이 라이브러리를 통해 가져온 데이터를 Model의 표현식으로 변환해주는 객체가 Mapper입니다. 
네트워크 통신과 크게 다른 부분이 없으므로 자세한 코드 설명은 생략하도록 하겠습니다.</p>
</blockquote>
<p>자 지금까지의 여정에 걸쳐서 CleanArchitecture가 뭔지 어느정도 알게 되었습니다.
그런데 의문점이 하나 남았죠? 제목은 Hilt 들고 MVVM 정복하기 인데 왜 CleanArchitecture만 지금까지 주구장창 설명했을까? 입니다 :D</p>
<p>그 이유는 Hilt 라이브러리는 MVVM과 CleanArchitecture를 구성하는데 있어 도움을 주는 라이브러리일 뿐 필수적인 라이브러리가 아니기 때문에 이 부분을 빼고 모든 것을 직접 구현하는데 초점을 맞춰 진행해보았기 때문입니다.</p>
<p>다음편부터는 Hilt라이브러리가 무엇이고, 이를 어떻게 사용하며, 사용하면 뭐가 달라지고 편해지는지 에 대한 내용을 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hilt 들고 MVVM 정복] 3. UseCase, 그리고 DI]]></title>
            <link>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI</link>
            <guid>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-3.-UseCase-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DI</guid>
            <pubDate>Wed, 14 Feb 2024 04:00:17 GMT</pubDate>
            <description><![CDATA[<h1 id="비즈니스-로직은-어떻게-해야-할까">비즈니스 로직은 어떻게 해야 할까</h1>
<p><a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel">2편</a>에서 화면 UI는 세 가지 구성요소로 나눌 수 있고 이 들중 UI 라이프사이클에 영향받지 않아야 하는 구성요소를 ViewModel로 분리해야 한다고 했습니다.
따라서 Observer나 StateFlow 자료구조를 활용하여 화면 상태정보 데이터를 ViewModel에서 관리하고 View에서는 이를 관찰하여 적절한 UI 로직만 취해주도록 해주었습니다.</p>
<p>그렇다면 비즈니스 로직은 어떻게 처리하면 될까요? 데이터의 경우 라이프사이클에 의해 유실되지 않도록 특별히 ViewModel에서 가지고 있도록 해준 것이지만 사실 로직은 라이프사이클에 크게 영향을 받지 않습니다. 따라서 View에 구현하나 ViewModel에 구현하나 동작에서의 차이는 없을것입니다. 그러나 우리는 단순히 성능개선만을 위해 MVVM 구조를 설계하는게 아니라 유지보수성과 가독성, 그리고 역할의 분리를 위한 Clean Architecture를 달성하기 위해 MVVM 구조를 설계하고 있는거잖아요? </p>
<p>따라서 UseCase라는 개념을 도입하여 View는 오로지 UI 관련로직만을 수행하고 그에 필요한 비즈니스 로직은 UseCase에 구현하여 ViewModel이 수행하도록 할 수 있습니다.</p>
<h1 id="usecase란">UseCase란?</h1>
<p>다시한번 Clean Architecture 다이어그램을 볼까요
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/02245eae-2f3a-47e7-8668-996e3ebd060d/image.png" alt=""></p>
<p>UI layer는 2편에서 다뤘는데 이 때 다뤘던 ViewModel을에 Domain layer의 Usecase가 연결되어 있습니다. 이 때 화살표에 의존성 주입이라 되어 있는데 의존성 주입은 아래에서 다시 언급하도록 하겠습니다.
UseCase가 뭐냐면 UI에서 요청하게 되는 각각의 비즈니스 로직에 대해 가장 1차원적인 시각으로 캡슐화한 개념입니다.</p>
<p>회원가입 화면이기에 예를 들어보면</p>
<ul>
<li>유저가 아이디 중복확인 버튼을 눌러서 중복되는 아이디인지 확인한다.</li>
<li>비밀번호 확인을 입력했을 때 일치하는지 확인한다.</li>
<li>가입하기 버튼을 눌러서 입력한 정보를 바탕으로 회원가입을 수행한다.</li>
</ul>
<p>와 같은 내용입니다.
위 내용들을 수행하기 위해 네트워크에 연결되고, 각종 클래스를 생성하여 유저 정보를 생성하고, 비밀번호를 제대로 입력했는지 정규식으로 검증하는 등의 비즈니스 로직이 복합적으로 수행됩니다.
그러나 유저가 기대하는 행동은 아이디 중복확인, 가입 요청 등의 UI에 노출되어 있는 단순한 행동들입니다. </p>
<p>따라서 View에서는 이러한 행동을 수행하라는 요청만 ViewModel에게 보내고 구체적인 비즈니스 로직은 UseCase안에서 수행되며 이를 ViewModel이 제어하고 있는 것입니다.</p>
<h2 id="굳이-usecase를-통해-캡슐화-해야하는-이유">굳이 UseCase를 통해 캡슐화 해야하는 이유?</h2>
<p>비즈니스 로직을 분리하다보면 코드 몇 줄로도 충분히 커버할 수 있는 경우도 있을 뿐더러 이들을 매번 UseCase로 분리하여 요청하게 되면 코드양이 늘어나고 비효율적인 것처럼 느껴질 수 도 있습니다.</p>
<p>실제로 <a href="https://developer.android.com/jetpack/guide/domain-layer?hl=ko">Domain layer 관련한 공식문서</a>에서도 이를 선택사항이라고 안내하고 있습니다... 만은 제 개인적인 생각으로 매우 간단한 비즈니스 로직일지라도 UseCase를 꼭 통해서 수행되게 만드는 것이 옳은 방법일 것 같습니다.
그에 대한 근거는 아래와 같습니다.</p>
<h3 id="viewmodel이-비약적으로-커지는-것을-방지">ViewModel이 비약적으로 커지는 것을 방지</h3>
<p>ViewModel이 모든 비즈니스 로직을 구현하고 있다면 코드량이 늘어나는것 뿐만 아니라 의존도 역시 커지게 됩니다. DataSource나 네트워크에 연결하기 위한 모든 Repository들에 의존성을 가지게 되고 해당 로직을 구현하기 위한 코드역시 많이 작성해야 할 것입니다. (지금까지의 내용에서 Data layer에 대해 다루진 않았지만 Repository는 Data를 가져오기 위한 로직의 구현부 혹은 인터페이스 정도로 이해하면 됩니다.)</p>
<p>만약 데이터를 가져오기 위한 특정 비즈니스 로직이 변경되었고, 이러한 로직을 쓰는 ViewModel이 여러개가 있다면 일일이 찾아가서 바꿔주어야 하는데 상상만해도 너무 귀찮습니다
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/82600e54-a978-4378-96eb-2174ef3dbdde/image.png" alt=""></p>
<p>따라서 이러한 로직들을 UseCase들로 묶어서 캡슐화한다면 <strong>ViewModel은 Repository의 구현부, 혹은 세부적인 비즈니스 로직 등에 대해 상세하게 알 필요 없이 UseCase 함수 호출만으로 작업을 수행할 수 있으므로 의존도가 낮아지게 됩니다.</strong>
(비즈니스 로직에 필요한 모든 Repository에 대해 의존할 필요 없이 사용하는 UseCase들에 대한 의존성만 가지게 됨.)</p>
<p>로직의 변경이 있더라도 모든 ViewModel을 찾아다니며 고칠 필요 없이 UseCase만 변경하면 되니 유지보수성 역시 좋아지겠죠.</p>
<h3 id="비즈니스-로직의-분리">비즈니스 로직의 분리</h3>
<pre><code class="language-kotlin">fun validateNameUsecase(nickname : String) : Boolean{
    val containsValidCharacters = nickname.matches(Regex(&quot;[a-zA-Zㄱ-ㅎ가-힣0-9]+&quot;))
    return containsValidCharacters
}</code></pre>
<p>위 메서드는 회원가입 과정에서 입력한 닉네임이 형식을 제대로 지켰는지 여부를 정규식으로 검사해서 반환하는 UseCase 예시입니다.
이렇게 간단한 로직들임에도 굳이 UseCase를 통해 호출해야 할까요?</p>
<p>그렇습니다. ViewModel은 어디까지나 UI layer에 속하며 UI state 정보를 관리하며 사용자입력을 처리하는데 의의가 있습니다. 따라서 UI 상태정보를 업데이트하기 위한 최소한의 행동단위를 수행할 뿐 자세한 비즈니스 로직의 모든 내용을 알 필요가 없습니다.</p>
<p>또한 UseCase를 사용할 경우 메서드명으로 어떤 역할을 하는지가 명확히 드러나기 때문에 추후 코드를 수정해야할 일이 생겼을 때 빠르게 찾아서 고칠 수 있습니다.
(예를들어 닉네임 작명 규칙이 변경됐을 경우 ViewModel을 일일이 뒤적거리지 않고도 UseCase를 바로 찾아서 수정할 수 있겠죠.)</p>
<h3 id="단일책임-원칙">단일책임 원칙</h3>
<p>위에서 설명한 모든 내용들은 곧 객체지향 방법론과 연결됩니다. 그중 핵심적인 내용을 뽑아보자면 아래와 같습니다.</p>
<ul>
<li><p><strong>단일 책임</strong>: 클래스가 변경되어야 하는 이유는 하나여야 합니다. 클래스가 여러 책임을 가지면 하나의 변경으로 인해 다른 부분에 영향을 주거나 변경이 필요한 부분이 더 많아질 수 있습니다.</p>
</li>
<li><p><strong>높은 응집도</strong>: 클래스의 메서드와 속성은 밀접하게 관련되어야 하며, 하나의 목적을 위해 함께 묶여 있어야 합니다.</p>
</li>
<li><p><strong>낮은 결합도</strong>: 클래스 간의 의존성이 낮아야 합니다. 한 클래스의 변경이 다른 클래스에 영향을 미치지 않아야 합니다.</p>
</li>
</ul>
<p>UseCase의 사용은 위 세가지 원칙을 완벽하게 만족시킵니다. </p>
<p>ViewModel의 역할은 &quot;UI State를 관리하는 것&quot; 이므로 비즈니스 로직에 대해 모두 알 필요 없습니다. <strong>(높은 응집도)</strong> 또한 ViewModel이 모든 Repository들에 의존하는것 대신에 UseCase에 대해서만 의존하고, UseCase가 각각의 비즈니스 로직에 필요한 Repository에 의존하게 됨으로써 클래스간 의존성이 낮아집니다. (낮은 결합도) 마지막으로 해당 UI에 필요한 모든 비즈니스 로직에 대한 책임을 ViewModel이 떠안는 대신, 각각의 UseCase가 필요한 비즈니스 로직만을 구현하고 있다면 비즈니스 로직에 대한 유집수성과 테스트성이 용이해집니다. <strong>(단일 책임)</strong></p>
<h3 id="위와-같은-이유들로">위와 같은 이유들로</h3>
<p>ViewModel에서 비즈니스 로직을 다룰 때에는 UseCase를 통해 다루고 정확한 구현부는 UseCase에 숨겨두는 편이 좋습니다. <strong>매우 간단한 로직일 지라도 해당 로직이 한군데에서만 쓰인다는 보장은 없기 때문</strong>에 추후 유지보수성을 생각해서라도 모두 UseCase로 캡슐화를 해두는 편이 장기적인 관점에서 좋은 개발 방법이라고 생각합니다.</p>
<h1 id="구현은-어떻게">구현은 어떻게?</h1>
<p>세부적인 비즈니스 로직을 UseCase로 빼놓고 ViewModel에서 이를 호출하는식으로 구현하면 되기 때문에 간단합니다.</p>
<pre><code class="language-kotlin">class SignupUseCase {

    suspend operator fun invoke(){
        // TODO : 실제 회원가입 요청 API 호출
    }

    suspend fun checkId(id : String){
        // TODO : 아이디 중복체크 API 호출
    }

    suspend fun checkNickname(name : String){
        // TODO : 닉네임 중복체크 API 호출
    }

    fun validatePw(pw : String) : Boolean{
        // 비밀번호가 영문자를 포함하는지 확인
        val containsEnglishChars = pw.matches(Regex(&quot;.*[a-zA-Z].*&quot;))
        // 비밀번호가 특수문자를 포함하는지 확인
        val containsSpecialChars = pw.matches(Regex(&quot;.*[!@#\\\$%].*&quot;))
        // 비밀번호가 숫자를 포함하는지 확인
        val containsNumbers = pw.matches(Regex(&quot;.*[0-9].*&quot;))
        // 비밀번호의 길이가 8자에서 20자 사이인지 확인
        val isLengthValid = pw.length in 8..20

        val isValid = containsEnglishChars &amp;&amp; containsSpecialChars &amp;&amp; containsNumbers &amp;&amp; isLengthValid

        return isValid
    }

    fun validateName(nickname : String) : NameValidations{
        val containsValidCharacters = nickname.matches(Regex(&quot;[a-zA-Zㄱ-ㅎ가-힣0-9]+&quot;))
        val isLengthValid = nickname.length in 1..10

        val isValid = containsValidCharacters &amp;&amp; isLengthValid
        return NameValidations(containsValidCharacters, isLengthValid, isValid)
    }
}</code></pre>
<p>시리즈에서 아직 data layer를 다루지 않았기 때문에 API에 연결하는 매서드는 TODO로 남겨놓았고 data layer에 연결되지 않는 로직만 구현해놓은 모습입니다. data layer 부분 매서드는 어떻게 해야할지 대충 상상을 해보면 아래와 같은 모습일겁니다.</p>
<pre><code class="language-kotlin">// 필요한 Repository에 대해서만 의존
class SignupUseCase {

    private lateinit var joinRepository : JoinRepository

    init{
        joinRepository = JoinRepository()
    }

    suspend operator fun invoke(userInfo : UserInfo){
        // TODO : 실제 회원가입 요청 API 호출
        joinRepository.requestSignin(userInfo)
    }
}</code></pre>
<p><del>물론 저게 정답은 아니고 DIP 및 IoC 등에 대해 알아야할 내용이 많으니 나중에 다루자구요</del></p>
<p>자 이제 UseCase를 구현하여 비즈니스로직을 기존의 Activity에서 분리해 냈으므로 이를 ViewModel에서 호출해야겠죠?
저번 포스팅에서 ViewModel을 대충 구현해보긴 했는데 여러가지 UI state 관련 로직이 추가된 ViewModel을 가져와봅시다.</p>
<pre><code class="language-kotlin">class JoinViewModel : ViewModel() {
    // ...
    private val _userId = MutableLiveData&lt;String&gt;()
    val userId: LiveData&lt;String&gt; = _userId
    private val _password = MutableLiveData&lt;String&gt;()
    val password: LiveData&lt;String&gt; = _password
    private val _name = MutableLiveData&lt;String&gt;()
    val name: LiveData&lt;String&gt; = _name
    // ...

    init{
        // ...
        _userId.value = &quot;&quot;
        _password.value = &quot;&quot;
        _name.value = &quot;&quot;
        // ...
    }

    fun setUserId(id : String){
        _userId.value = id
        _idCheck.value = DuplicationCheck.PROCEEDING
    }

    fun setPassword(pw : String){
        _password.value = pw
        _pwValidation.value = signUp.validatePw(pw)
    }

    // ...
}</code></pre>
<p>자 이렇게 회원가입에 관련된 UI State를 관리하는 ViewModel이 있다면 여기에 비밀번호를 양식에 맞게 입력했는지 검증하는 비즈니스 로직이 수행되야 한다고 가정해봅시다. 해당 로직은 SignupUseCase의 validatePw에 구현되어 있기 때문에 ViewModel에서는 해당 UseCase를 사용해서 비즈니스로직에 접근해야 합니다.</p>
<pre><code class="language-kotlin">class JoinViewModel : ViewModel() {
    // ...
    private val _userId = MutableLiveData&lt;String&gt;()
    val userId: LiveData&lt;String&gt; = _userId
    private val _password = MutableLiveData&lt;String&gt;()
    val password: LiveData&lt;String&gt; = _password
    // ...
    val signupUsecase = SignupUseCase()    // 새로 추가된 부분

    init{
        // ...
        _userId.value = &quot;&quot;
        _password.value = &quot;&quot;
        _name.value = &quot;&quot;
        // ...
    }

    fun setUserId(id : String){
        _userId.value = id
        _idCheck.value = DuplicationCheck.PROCEEDING
    }

    fun setPassword(pw : String){
        _password.value = pw
        _pwValidation.value = signUp.validatePw(pw)
    }

    // ...

    // 새로 추가된 부분
    fun validatePw(){
        val isValid = signupUsecase.validatePw(password.value)
        if (isValid){
            // 회원가입 버튼 활성화
        } else {
            // 회원가입 버튼 비활성화
        }
    }
}


// 구현된 UseCase

class SignupUseCase {

    suspend operator fun invoke(){
        // TODO : 실제 회원가입 요청 API 호출
    }

    // ...

    fun validatePw(pw : String) : Boolean{
        // 비밀번호가 영문자를 포함하는지 확인
        val containsEnglishChars = pw.matches(Regex(&quot;.*[a-zA-Z].*&quot;))
        // 비밀번호가 특수문자를 포함하는지 확인
        val containsSpecialChars = pw.matches(Regex(&quot;.*[!@#\\\$%].*&quot;))
        // 비밀번호가 숫자를 포함하는지 확인
        val containsNumbers = pw.matches(Regex(&quot;.*[0-9].*&quot;))
        // 비밀번호의 길이가 8자에서 20자 사이인지 확인
        val isLengthValid = pw.length in 8..20

        val isValid = containsEnglishChars &amp;&amp; containsSpecialChars &amp;&amp; containsNumbers &amp;&amp; isLengthValid

        return isValid
    }

    // ...
}</code></pre>
<p>한눈에 보기 쉽게 구현된 UseCase와 전체적인 ViewModel의 형태를 코드로 나타내었는데, ViewModel이 UseCase를 어떻게 사용하는지 간결하게 정리해보면 아래와 같습니다.</p>
<pre><code class="language-kotlin">class JoinViewModel : ViewModel() {
    val signupUsecase = SignupUseCase()

    fun validatePw(){
        val isValid = signupUsecase.validatePw(password.value)
        if (isValid){
            // 회원가입 버튼 활성화
        } else {
            // 회원가입 버튼 비활성화
        }
    }
}</code></pre>
<p>위 코드에서는 UseCase 객체를 ViewModel이 직접 생성하여 사용하고 있습니다. 
결론부터 말하자면 두 ViewModel과 UseCase 두 클래스간 강한 결합도가 생겼기 때문에 좋지 않은 방법입니다.</p>
<h1 id="강한-결합도란">강한 결합도란?</h1>
<ol>
<li><p><strong>직접적인 의존성</strong> : 클래스 A가 클래스 B를 직접 참조하고 있을 때, 클래스 A와 클래스 B는 강한 결합도를 가집니다. 예를 들어, 클래스 A가 클래스 B의 메서드를 호출하고 있다면, 클래스 A는 클래스 B에 강하게 의존하고 있습니다.</p>
</li>
<li><p><strong>상속</strong> : 서브클래스가 수퍼클래스에 강하게 의존하는 경우가 있습니다. 서브클래스가 수퍼클래스의 내부 구현에 의존하여 수정하기 어렵게 만들 수 있습니다.</p>
</li>
<li><p><strong>인터페이스 구현</strong> : 클래스가 인터페이스를 구현하고 있는 경우, 해당 클래스는 해당 인터페이스에 강하게 의존하게 됩니다. 이 경우, 인터페이스의 변경이 해당 클래스에 영향을 줄 수 있습니다.</p>
</li>
<li><p><strong>전역 상태 또는 싱글톤</strong> : 클래스가 전역 변수나 싱글톤 인스턴스에 의존하는 경우, 해당 클래스는 전역 상태나 싱글톤 인스턴스에 강하게 결합됩니다. 이는 유지보수와 테스트를 어렵게 만들 수 있습니다.</p>
</li>
<li><p><strong>하드 코딩된 값</strong> : 클래스가 하드 코딩된 값에 의존하는 경우, 해당 클래스는 그 값에 강하게 결합됩니다. 이는 유지보수성을 저하시킬 수 있습니다.</p>
</li>
</ol>
<p>위와 같은 예시에 해당하는 경우 두 클래스나 인스턴스간에 <strong>강하게 결합</strong>되어 있다고 말합니다. </p>
<pre><code class="language-kotlin">class JoinViewModel : ViewModel() {
    val signupUsecase = SignupUseCase()

    fun validatePw(){
        val isValid = signupUsecase.validatePw(password.value)
        if (isValid){
            // 회원가입 버튼 활성화
        } else {
            // 회원가입 버튼 비활성화
        }
    }
}</code></pre>
<p>위 코드의 경우 1번 직접적인 의존성을 갖는 경우에 해당하는데, 만약 JoinViewModel에서 더이상 SignupUseCase를 사용하지 않고, validatePw 메서드가 하는 역할을 다른 UseCase가 담당하게 되었다면  SignupUseCase를 생성하는 ViewModel들을 모두 찾아다녀야 하기 때문에 유지보수성이 안좋아지게 됩니다. </p>
<h1 id="의존성을-주입해보자">의존성을 주입해보자</h1>
<p>클래스를 직접 생성하는 행위는 해당 클레스에 강하게 의존한다는 의미이므로 결합도가 강해지게 됩니다. 그렇다면 직접 클래스를 생성하는게 아니라 외부에서 파라미터로 받아와 사용하게 되면 결합도가 느슨해지지 않을까요?
주입받은 UseCase를 통해 validatePw라는 비즈니스 로직을 사용하기만 하면 될 뿐 어떤 UseCase가 담당하는지에 대해 알 필요가 없게 되니까요.
즉 변화가 적은 비즈니스 로직에 영향을 미치지 않으면서 관련된 코드의 변경사항에 대해 강건해지게 되는 것입니다.</p>
<pre><code class="language-kotlin">// 이렇게 말이죠

class JoinViewModel(signupUsecase : SignupUseCase) : ViewModel() {

    fun validatePw(){
        val isValid = signupUsecase.validatePw(password.value)
        if (isValid){
            // 회원가입 버튼 활성화
        } else {
            // 회원가입 버튼 비활성화
        }
    }
}</code></pre>
<p>이제는 JoinViewModel이 직접 클래스를 생성하지 않고 외부에서 JoinViewModel을 생성할 때 파라미터로 SignUpUseCase를 넘겨주게 됩니다. 따라서 JoinViewModel 입장에서는 해당 UseCase에 대한 의존도가 낮아졌다고 볼 수 있겠죠. <strong>이를 외부에서 의존성을 주입해준다 하여 Dependency Injection 줄여서 DI 라 부릅니다.</strong></p>
<p><em><strong>어...? 근데 어차피 외부에서 주입을 해줘야 한다면 아래 코드처럼 어차피 또 SignUpUseCase를 생성해야 하고 그러면 외부 클래스에서 강한 결합도가 생기는거 아닌가요? 그리고 해당 클래스 인스턴스를 주입해서 사용한다 해도 변화가 생겼을 때 영향을 받는건 별 차이 없는거 아닌가요?</strong></em></p>
<pre><code class="language-kotlin">class JoinActivity() : AppCompatActivity(){
    override fun onCreate(){
        val subClass = SubClass()    // 어차피 외부에서 또 생성해야 함 (다른곳에서 강한 결합 발생)
        val mainClass = MainClass(subClass)    // 의존성 주입을 받아도 SubClass에 변화가 생기면 MainClass 내부에도 영향이 전파됨
    }
}</code></pre>
<p>아래를 봐주세요~</p>
<blockquote>
<p><strong>DI 를 통해 강한 결합도 문제를 해결할 수 있나요?</strong>
그건 아닙니다. DI는 강한 결합도 문제를 해결하기 위한 수단 중 하나일 뿐 완전히 해결하지 못하고 어느정도 완화시킨다 정도로 생각해주세요. 이러한 결합도와 의존성 문제를 해결하기 위한 방법들 중에는 DI 외에도 의존 역전 (DIP), 제어의 역전(IoC) 와 같은 방법들이 존재하며 이 포스팅의 궁극적 주제인 Hilt 라이브러리와 연관됩니다.
따라서 이번 포스팅에서는 의존성 주입 (DI) 까지만 다루고 ViewModel에 어떻게 의존성을 주입할 수 있는지에 대한 내용까지만 이야기 해 보겠습니다.</p>
</blockquote>
<h1 id="viewmodel과-di">ViewModel과 DI</h1>
<p>자 이제 의존성을 주입하면서 ViewModel을 생성하는 방법을 한번 살펴보겠습니다. 그 전에 저번 <a href="https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel">ViewModel 포스팅</a>에서 ViewModel을 생성하는 방법에 대해 한번 다뤘기 때문에 저번 포스팅을 한번 살펴보고 와주세요.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/1df299b6-0cc2-405c-a3df-0658091b3886/image.png" alt="">
이 사진이 기억 나시나요?
저번 포스팅에서는 첫번째 ViewModelStoreOwner 만 인자로 받는 constructor를 사용했지만 이번엔 아래에 있는 constructor를 사용할 겁니다. Factory라는것을 추가로 받고있네요!</p>
<p>ViewModelStore가 뭐고 ViewModelStoreOwner는 뭐고 Factory는 뭐고 죄다 처음보는것들 투성입니다. 이쯤에서 ViewModel의 생성 및 참조 사이클을 다이어그램으로 살펴볼게요</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/fbfff0c0-2a7b-4cef-a70d-1fc135aa51a8/image.png" alt=""></p>
<p>가장 아래 인간모양이 Activity 혹은 UI 라고 생각해봅시다. UI에서 일어난 상호작용을 처리하기 위해서는 ViewModel을 호출해야 하는데 그 과정에서 다음과 같은 일이 일어납니다.</p>
<ol>
<li>UI에서 ViewModel 요청</li>
<li>ViewModelProvider가 Factory를 통해 ViewModel 생성 </li>
<li>생성된 ViewModel은 ViewModelStore에 저장됨</li>
<li>ViewModelProvider가 해당 UI와 연관된 ViewModelStoreOwner를 통해 ViewModelStore에 접근하여 ViewModel 객체 반환</li>
</ol>
<p>ViewModel 하나 생성하고 불러오는데 굉장히 많은 클래스가 엮여있습니다. </p>
<p>ViewModelStore는 생성된 ViewModel들을 Hashmap으로 관리하고 있는 클래스입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/55f81fd5-3e15-4550-9f6e-eac0ee649af6/image.png" alt=""></p>
<p>ViewModelStoreOwner는 ViewModelStore에 접근하기 위한 인터페이스이며 ComponentActivity가 ViewModelStoreOwner를 구현하고 있습니다.
(AppCompatActivity -&gt; FragmentActivity -&gt; ComponentActivity 순으로 상속하고 있습니다.)</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b19be49c-f4ed-4f48-867c-5d992ef47bed/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/397cfc70-56c2-407e-9032-e49720056f94/image.png" alt=""></p>
<p>여기에 다 담을 수는 없지만 ComponentActivity의 코드를 살펴보면 ViewModelStore를 선언해서 구현하는 부분과 ViewModelStoreOwner 인터페이스를 구현하여 해당 ViewModelStore에 접근할 수 있도록 구현해놓은 부분이 존재합니다. (바로 ViewModelStore에 접근하지 않고 ViewModelStoreOwner라는 인터페이스를 통해 접근하게 해놓은 것 역시 강한 결합도를 해결하기 위한 의존 역전의 원칙이 관여되어 있습니다.)</p>
<p>뭐 복잡한 내용이 많았는데 정리해보면 ComponentActivity 내부적으로 ViewModelStore에서 ViewModel 객체들을 관리하고 있고 이를 불러오거나 생성하기 위해서는 ViewModelStoreOwner나 Factory인스턴스를 사용해야 한다는 것입니다.</p>
<h2 id="viewmodelfactory">ViewModelFactory?</h2>
<p>자 이제 Factory에 대해 다뤄볼게요 Factory 객체는 ViewModelProvider 안에 interface로 존재하고 </p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/6968e283-44a1-4537-9971-2d96f5cc4561/image.png" alt=""></p>
<p>위에서 보여드렸던 ViewModelProvider 클래스의 constructor에서 Factory를 파라미터로 넣어 객체를 생성하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/50fa56f3-96ac-40fd-a574-ba9632da33eb/image.png" alt=""></p>
<p>빨간 네모박스 영역이 두 constructor간의 유일한 차이점입니다.
첫번째 constructor의 경우 defaultFactor를 자동으로 주입해주고 두번째 constructor의 경우 파라미터로 받은 factory를 주입해주고 있습니다.</p>
<p>defaultFactor의 경우 내부적으로 구현되어 있는 Factory인데 아무런 파라미터도 없는 ViewModel을 생성하는 Factory입니다.</p>
<p>그런데 우리의 ViewModel은 UseCase가 파라미터로 들어가있죠? 그러니까 defaultFactory를 사용하는 것이 아니라 custom Factory를 구현해서 두번째 constructor를 사용해서 주입해주면 됩니다.</p>
<p>Factory 객체는 interface 이므로 우리가 직접 구현해주면 되겠죠.</p>
<h2 id="custom-factory-구현">Custom Factory 구현</h2>
<p>바로 코드부터 보시죠</p>
<pre><code class="language-kotlin">class JoinViewModel(signupUsecase : SignupUseCase) : ViewModel() {

    // ...

    fun validatePw(){
        val isValid = signupUsecase.validatePw(password.value)
        if (isValid){
            // 회원가입 버튼 활성화
        } else {
            // 회원가입 버튼 비활성화
        }
    }

    // ...

    // 뷰모델 의존성 주입을 위한 Factory
    companion object {

        @Suppress(&quot;UNCHECKED_CAST&quot;)
        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            override fun &lt;T : ViewModel&gt; create(
                modelClass: Class&lt;T&gt;,
            ): T {
                val signupUseCase = SignupUseCase()

                return JoinViewModel(
                    signupUseCase
                ) as T
            }
        }
    }
}</code></pre>
<p>JoinViewModelFactory 와 같은 이름을 가지는 클래스로 따로 구현해도 되지만 그렇게 하면 파일이 너무 많아지기 때문에 해당 ViewModel의 companion object안에 변수로 구현해두는 편이 가독성이 좋습니다.</p>
<p>아까전에 위에서 Factory의 코드를 보여드렸는데 create()라는 하나의 메서드만 존재하는 interface였습니다. 따라서 Factory 객체를 생성해서 create() 메서드를 override 해서 구현해준 모습입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b30611b5-d00f-4742-9b72-0dc5c8e9f4c6/image.png" alt=""></p>
<p>위 코드가 defaultFactory 의 구현부인데 create() 메서드를 보시면 modelClass.newInstance() 를 통해 생성자 없는 기본 인스턴스를 생성하여 반환하고 있는것을 알 수 있습니다. 클래스에 매개변수가 있는 경우 newInstance()를 이용하여 생성할 수 없기 때문에 </p>
<pre><code class="language-kotlin">val signupUseCase = SignupUseCase()

return JoinViewModel(signupUseCase) as T</code></pre>
<p>직접 이렇게 UseCase에 대한 의존성을 주입해서 객체를 생성해주도록 Factory를 직접 구현해주어야 한다는 점 이해되셨나요?</p>
<h2 id="custom-factory-활용해서-viewmodel-생성하기">Custom Factory 활용해서 ViewModel 생성하기</h2>
<p>그렇다면 companion object에 구현해둔 Custom Factory 인스턴스를 어떻게 활용해서 ViewModel을 생성해야 할까요?</p>
<pre><code class="language-kotlin">private val joinViewModel : JoinViewModel by lazy{
    // Factory를 파라미터로 주입해준 모습
    ViewModelProvider(this, JoinViewModel.Factory).get(JoinViewModel::class.java)
}</code></pre>
<p>지금까지의 내용을 모두 이해했다면 바로 아실 수 있다고 생각합니다.
constructor가 두가지 있었죠? viewModelStoreOwner만 주입해주는 버전과 Factory까지 함께 주입해주는 버전.</p>
<p>ViewModel에 의존성주입이 필요없는 경우 첫 번째 버전을 사용하면 되지만 ViewModel에 의존성주입이 필요한 경우 Factory를 직접 구현해서 두 번째 constructor 버전을 사용해주시면 됩니다.</p>
<pre><code class="language-kotlin">private val joinViewModel : JoinViewModel by viewModels { JoinViewModel.Factory }</code></pre>
<p>이렇게 표현도 가능합니다.</p>
<h1 id="정리해보면">정리해보면</h1>
<ul>
<li>ViewModel은 항상 ViewModelProvider.Factory를 통해 인스턴스화 된다.</li>
<li>ViewModelProvider는 두가지 버전의 constructor가 있는데 하나는 defaultFactory를 사용하는 버전이고 하나는 customFactory를 사용하는 버전이다.</li>
<li>ViewModel은 ViewModelStore에서 HashMap 형태로 관리된다.</li>
<li>UI 에서는 ViewModelStoreOwner라는 인터페이스를 통해 ViewModelStore에 접근하여 ViewModel 인스턴스를 얻어온다.</li>
<li>비즈니스 로직은 캡슐화하여 UseCase로 분리하는 것이 유지보수성 및 테스트하기에 용이하다.</li>
<li>UseCase는 ViewModel에서 직접 생성하는것보다 외부에서 주입받는 편이 결합도를 낮추기에 좋다 (DI)</li>
<li>그러기 위해서는 UseCase를 생성해서 파라미터로 주입해주도록 직접 ViewModelFactory 인스턴스를 구현해주어야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hilt 들고 MVVM 정복] 2. ViewModel]]></title>
            <link>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel</link>
            <guid>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel</guid>
            <pubDate>Wed, 31 Jan 2024 18:22:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d0a9aba3-6d76-41ae-880e-7c1e9f4ccfa5/image.png" alt=""></p>
<h1 id="안드로이드-viewmodel-클래스란">안드로이드 ViewModel 클래스란?</h1>
<p>MVVM 아키텍처를 구현하기 위해 Android AAC에서는 ViewModel이라는 클래스를 제공하고 있습니다. 이를 활용하면 매우 쉽게 MVVM 아키텍처를 안드로이드에서 구현할 수 있습니다. 
명백히 MVVM 아키텍처의 ViewModel과 Android AAC가 제공하는 ViewModel 클래스가 완벽히 동일하다고 볼 수 는 없지만 MVVM 아키텍처의 ViewModel(추상적 개념)을 구현하기 위해 Android AAC의 ViewModel 클래스(구체적 개념)을 활용한다고 생각하면 됩니다.</p>
<p>그럼 ViewModel 클래스를 무작정 사용하기 전에 얘가 뭔지부터 알고 가야겠죠.</p>
<blockquote>
<p><a href="https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko">공식문서</a>에서는 ViewModel을 <a href="https://developer.android.com/topic/architecture/ui-layer/stateholders?hl=ko">비즈니스 로직 또는 화면 수준 상태 홀더</a>로 정의하고 있습니다.
<del>아... 영어로 된 어려운 공식문서를 한글로 번역해 놓으니까 이 한 줄로된 정의로써는 더욱더 무슨말인지 알아듣기 힘듭니다.</del></p>
</blockquote>
<p>쉽게 말해 안드로이드 UI의 상태 구성요소는 크게 3 가지로 구성됩니다.</p>
<ul>
<li><strong>비즈니스 로직</strong> : 앱이 동작하기 위해 필요한 데이터(눈에 보이지 않는 추상적인 데이터)를 처리하기 위한 내부적인 로직</li>
<li><strong>화면 UI 상태</strong> : 화면에 표시해야 하는 항목. 즉 화면에 눈으로 보여야 할 정적인 데이터</li>
<li><strong>UI 로직</strong> : 화면에 UI 상태를 표시하는 방법. 즉 위에서 기술한 화면 UI 상태 데이터를 눈에 보이도록 화면에 띄우기 위한 로직 (<code>Toast.show()</code> 메서드 같은 로직)</li>
</ul>
<p>위 세 가지 개념은 이번 포스팅에서 지속적으로 언급할 예정이니 눈에 콕 박아 잘 기억해두세요.</p>
<p>예시를 들어볼까요?
카카오톡 메시지를 보내고 있다고 가정합시다. 입력창에 메시지를 치고 엔터를 누르면 해당 메시지가 EditText 뷰에 입력되다가 엔터를 누르는 순간 위에 보이는 채팅창으로 올라가면서 전송됩니다.</p>
<ul>
<li>엔터를 눌렀을 때 실제 서버와 통신하여 EditText 에 있는 문자열을 전송하는 역할을 하는 것이  <strong>비즈니스 로직</strong>입니다.</li>
<li>엔터를 누르기 전에 EditText에 올려져 있는 문자열이 <strong>화면 UI 상태</strong> 입니다.</li>
<li>엔터를 눌렀을 때 EditText가 비워지고 위쪽의 채팅창에 내가 보낸 메시지가 한줄 올라가는 과정을 구현한 로직이 <strong>UI 로직</strong>입니다.</li>
</ul>
<p>아무런 앱 아키텍처를 사용하지 않고 액티비티 클래스에 모든 로직을 때려박아 구현했을 땐 눈치채지 못했지만 이 3가지 구성요소의 삼박자가 맞아 떨어져 앱이 동작하고 있던 것입니다.</p>
<h1 id="이제-clean-architecture에-대해-대충-알아봅시다">이제 Clean Architecture에 대해 대충 알아봅시다</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/8d79976e-1d46-40fe-a6b7-8097456c58c5/image.png" alt="">
MVVM을 알아가기에도 벅찬데 갑자기 무슨 클린아키텍처냐!!!!</p>
<p>안드로이드의 MVVM은 클린아키텍처와 매우 밀접합니다. 음... 클린 아키텍처는 <code>안드로이드 프로젝트를 설계할 때 어떻게 설계하면 깔끔하게 설계할 수 있을까?</code> 에서 탄생한 매우 추상적인 개념이고, MVVM은 <code>어떻게 하면 클린 아키텍처를 깔끔하게 구현할 수 있을까?</code> 에서 탄생한 적당히 추상적인 개념이라고 생각해주세요.</p>
<p><del>클린 아키텍처를 달성하기 위한 MVVM 아키텍처를 설계하기 위한 ViewModel클래스</del></p>
<p>자자 MVVM으로 구현한 클린아키텍처는 요래 생겼습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/70762eee-013b-4cba-a94c-5a0fe456a657/image.png" alt="">
아악 시작부터 머리가 으깨질것 같고 그만하고싶습니다.
그러니 지금단계에선 왼쪽은 치워두고 작고 소중한 노란색 UI Layer 부분에만 집중해주세요 ^^<del>~</del></p>
<p>말 그대로 화면을 표시하고 상호작용을 하기 위한 UI layer는 ViewModel과 View 두 가지 구성요소로 이루어지고 있음을 알 수 있습니다.</p>
<p>왜 두개로 나눌까요? 그 이유는 바로 <em><strong>위에서 기술한 3가지 안드로이드 UI의 상태 구성요소들이 각각 UI 수명주기와 종속되는지 여부가 다르기 떄문입니다.</strong></em></p>
<p>UI 수명주기란 액티비티의 onCreate, onPause, onDestroy 같은 수명주기를 의미하는데 가장 쉽게 onCreate() 메서드에 모든 로직을 때려박아 구현했다고 생각해봅시다. 이럴 경우 만약 화면 회전과 같은 이벤트가 일어나 액티비티가 다시 onCreate()부터 실행될 경우 비즈니스 로직과 화면 UI 상태가 모두 초기화 되고 처음부터 다시 실행되게 됩니다.</p>
<p><strong>즉, 카카오톡에 메시지를 입력하다가 가로모드로 바꾸게 되면 onCreate() 메서드가 다시 실행되면서 입력했던 메시지들이 전부 사라지고 다시 입력해야하는 일이 생기는 것입니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/3b7b7338-0ebe-458a-b1d7-39253ad1b7bc/image.png" alt=""></p>
<p>따라서 UI layer에서는 UI 수명주기와 무관해야 하는 구성요소와 UI 수명주기에 종속되어야 하는 구성요소를 분리해야할 필요가 있으며 이를 View와 ViewModel이 담당하게 되는 것입니다.</p>
<p>자. 그럼 다시 처음에 공식문서가 정의한 말을 곱씹어볼까요?</p>
<blockquote>
<p>ViewModel은 _<strong>비즈니스 로직 또는 화면 수준 상태 홀더</strong>_이다.</p>
</blockquote>
<h2 id="정리해봅시다">정리해봅시다.</h2>
<p>MVVM 아키텍처를 사용할 때 클린아키텍처의 UI layer를 구현하기 위해서는 UI 수명주기와 무관해야하는 <strong>비즈니스로직, 화면 UI 상태</strong> 들과 UI 수명주기에 종속되는 <strong>UI 로직</strong>을 분리해야할 필요가 있으며 <strong>비즈니스 로직, 화면 UI 상태</strong>들은 ViewModel에 넣어주고 오직 <strong>UI 로직</strong>만을 View에서 구현해주면 되는것입니다.</p>
<p>즉 ViewModel 클래스에서는 화면에 표시해야할 데이터를 모두 가지고 있으며 비즈니스 로직 수행을 담당하고, Activity 클래스 (View에 해당)에서는 UI의 수명주기에 따라 적절한 UI 로직만을 수행하며 ViewModel 클래스가 갖고있는 데이터들을 UI 로직에 의해 화면에 표시해주기만 하면 되는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a2e72c13-9797-4189-aeb7-3d703719c9cc/image.png" alt=""></p>
<p>위의 카카오톡 전송 예시를 가져와보면 이렇게 구현하면 되겠네요!!!
이렇게 되면 View가 재시작 된다 해도 ViewModel 안의 입력 메시지 정보는 유효하기 때문에 &#39;메시지 내용을 화면에 표시하기 위한 로직&#39;을 통해 입력했던 텍스트를 그대로 표시함으로써 UI의 상태를 유지할 수 있습니다.</p>
<h1 id="화면-ui-상태를-분리해보자">화면 UI 상태를 분리해보자</h1>
<p>자 그럼 첫번째 포스팅에서 소개해드렸던 회원가입 화면에서 화면 UI 상태를 나타내는 부분을 ViewModel 클래스를 사용하여 분리해 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/f7167bd9-6be9-417a-bc3b-59edcd666ea7/image.png" alt=""></p>
<p>이렇게 생긴 화면에서 ID를 입력하는 부분을 한번 살펴볼게요</p>
<pre><code class="language-kotlin">class JoinActivity : AppcompatActivity(){
    var userId: String = &quot;&quot;    // 전역변수에 Id 정보 저장

    override fun onCreate(savedInstanceState: Bundle?){

      // ...

      // Id 정보 EditTextChangedListener
      binding.userIdEdit.addTextChangedListener(object : TextWatcher {
          override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

          // 값 변경 시 실행되는 함수
          override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

              // 입력값 담기
              userId = userIdEdit.text.toString()
          }
          // 값 유무에 따른 활성화 여부
          if (userId.isNotEmpty()) {

                // 버튼 활성화 로직
         } else {
                 // 버튼 비활성화 로직
         }
}</code></pre>
<p>pseudo code가 대충 저렇게 생겼는데, 보시면 userId 변수에 저장되는 값은 <strong>화면 UI 상태</strong>이고, 해당 값 유무에 따른 버튼활성화 및 비활성화 로직은 <strong>UI 로직</strong>에 해당합니다.
(네트워크 통신이나 validation 처리같은 비즈니스 로직은 일단 제외시켰습니다. 비즈니스로직은 ViewModel에 그대로 넣어도 되고 선택적으로 나중에 Usecase같은 Domain layer에서 구현해도 됩니다.)</p>
<p>자 그럼 ViewModel 클래스를 만들어서 userId 변수를 가지고 있도록 하고 Activity 코드에서는 ViewModel의 userId를 셋팅하거나 userId를 참조하여 화면을 업데이트 할 수 있도록 리팩토링 해보겠습니다.</p>
<h2 id="생산자-소비자-패턴">생산자-소비자 패턴</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/c3786d08-6875-43d2-8d1a-147402e6a93d/image.png" alt=""></p>
<p>생산자-소비자 패턴은 작업목록 (큐) 를 가운데 두고 <strong>작업을 생산해 내는 주체</strong>와 <strong>처리하는 주체</strong>로 분리시켜 설계하는 패턴입니다. 생산자는 소비자가 몇개가 어떻게 동작하는지 관심없고 그저 작업을 생산하여 큐에 쌓아두기만 하면 됩니다. 소비자 역시 생산자에 대해 알아야 할 것은 없으며 그저 큐의 작업내용이 변경되거나 혹은 큐의 데이터가 필요한 경우 작업큐를 참조하여 처리하기만 하면 됩니다.</p>
<p>View - ViewModel간의 관계는 기본적으로 생산자-소비자 패턴을 따릅니다.
ViewModel이 데이터를 생산하여 들고있으면 View는 필요할 경우 ViewModel이 들고 있는 데이터를 참조하여 UI를 업데이트 합니다. <em><strong>View가 하는 역할은 UI를 적절히 업데이트 하고 유저의 입력을 받아 ViewModel이 데이터를 생산할 수 있도록 알려주기만 합니다. ViewModel은 View의 요청에 따라 데이터를 생산하기만 합니다.</strong></em></p>
<p>안드로이드에서 View - ViewModel 간의 생산자-소비자 패턴을 구현하기 위한 자료구조에는 크게 LiveData와 StateFlow가 있습니다.
이들에 대해 자세히 탐구하려면 이 시리즈의 논지를 조금 벗어날것 같아 간략히 소개만 하고 넘어가도록 하겠습니다.</p>
<h3 id="livedata">LiveData</h3>
<p>LiveData는 Android AAC 에서 제공하는 자료구조이며 안드로이드 플랫폼에 종속적이기 때문에 라이프사이클 변화에 따라 유기적으로 동작하도록 설계되었습니다. 따라서  옵저버 패턴을 통해 데이터의 변화를 관찰하고 UI 를 업데이트하기에 용이할 뿐더러 라이프사이클에 의한 메모리 누수또한 고려하지 않아도 됩니다.</p>
<h3 id="stateflow">StateFlow</h3>
<p>StateFlow는 Kotlin 수준에서 제공하는 Flow API 중의 하나이며 LiveData와 마찬가지로 value 속성을 통해 현재 상태 값을 읽을 수 있습니다. 또한 다양한 코루틴 스코프를 통해 비동기 처리가 가능하지만 Kotlin 수준에서 제공되는 API이므로 자체적으로 안드로이드 라이프사이클을 알지 못한다는 단점이 있습니다. (물론 이를 극복할 수 있는 방법 또한 존재하긴 합니다.)</p>
<hr>
<p>기존에는 LiveData를 사용하였지만 안드로이드 OS에 종속적이기 때문에 유닛테스트가 용이하지 않고, UI layer 이외의 Domain laer에서 사용하기 부적합하며 메인스레드에서 동작하는 단점들 때문에 StateFlow를 사용하고 있는 추세입니다. 그러나 본 포스팅에서는 처음 MVVM 입문을 다루고 있는 만큼 보다 쉽게 사용할 수 있는 LiveData를 활용하도록 하겠습니다.
(StateFlow를 사용하려면 Flow API 에 대해 또 알아가야 하므로 러닝커브가 더 높아져요...)</p>
<blockquote>
<p><strong>여담</strong>
지금단계에서 저도 StateFlow가 익숙하지 않아 그런점도 없지않아 있긴 합니다 😁 그러나 개인적으로 조금 찾아보고 공부해본 결과 Domain layer가 아닌 UI layer에서는 굳이 StateFlow 없이 LiveData를 활용하여도 충분히 얻는 이점이 많다고 생각합니다. 물론 메인스레드에서 무겁게 동작하는 단점이 존재하긴 하지만, 안드로이드 OS 친화적인 자료구조이므로 메모리 누수 걱정없이 라이프사이클에 따라 잘 동작하며 Observable 객체를 통해 보다 직관적으로 Observer 패턴을 구현할 수 있기 때문입니다.
그러나 StateFlow로 많이 넘어가고 있는 추세이고 LiveData의 단점을 StateFlow가 잘 보완하고 있는것도 사실이기 때문에 더 공부해보고 적용하여 포스팅해볼 생각입니다.</p>
</blockquote>
<h1 id="viewmodel을-써보자">ViewModel을 써보자</h1>
<pre><code class="language-kotlin">class JoinViewModel : ViewModel() {
    private val _userId = MutableLiveData&lt;String&gt;()
    val userId: LiveData&lt;String&gt; = _userId

    fun setUserId(id : String){
        _userId.value = id
    }
}</code></pre>
<p>JoinActivity에서 아이디 정보를 저장하는 전역변수 userId 를 ViewModel로 옮겨서 LiveData 형식으로 선언하였습니다.</p>
<p>setUserId 매서드는 userId 값을 셋팅하는 함수인데, Activity에서 EditText 입력 이벤트가 일어날 때 마다 해당 함수를 호출하여 뷰모델 안의 userId 값을 셋팅하도록 할 수 있습니다. (혹은 x 버튼을 누를 경우 <code>setUserId(&quot;&quot;)</code> 와 같이 호출하여 userId 정보를 비울 수 도 있겠죠.)</p>
<p>주목해봐야 할 점은 MutableLiveData와 LiveData가 함께 사용되었다는 점입니다.
LiveData 자체로는 값을 수정할 수 없는 읽기전용 자료구조이므로 이를 수정할 수 있도록 <code>setValue()</code>, <code>postValue()</code> 메서드를 제공하는 MutableLiveData를 함께 사용하였습니다.</p>
<p>그 이유는 userId가 외부에 의해 변경될 수 없도록 하기 위함입니다. 즉 생산자-소비자 패턴을 구현하기 위함인데, 이렇게 하면 ViewModel은 MutableLiveData를 수정하여 데이터를 생산하기만 하고, 외부의 액티비티나 프래그먼트는 LiveData 를 관찰하면서 데이터를 처리하기만 하도록 구현할 수 있습니다.</p>
<pre><code class="language-kotlin">class JoinActivity : AppcompatActivity(){

    // JoinViewModel을 생성
    private val joinViewModel : JoinViewModel by lazy{
        ViewModelProvider(this).get(JoinViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?){

      // ...
        // userId를 관찰하여 UI 업데이트하는 옵저버
        joinViewModel.userId.observe(this, Observer { userId -&gt;
            if (userId.isNotEmpty()) {

                // 버튼 활성화 로직

         } else {

                 // 버튼 비활성화 로직

         }
     }

        // Id 정보 EditTextChangedListener
        binding.userIdEdit.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            // 값 변경 시 실행되는 함수
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

                // 입력값 담기
                // userId = userIdEdit.text.toString()
                joinViewModel.setUserId(userIdEdit.text.toString())

            }
        }
      }
}</code></pre>
<p>자 이제 Activity가 ViewModel의 데이터를 관찰할 수 있도록 바뀌었습니다.</p>
<h2 id="viewmodel-생성하기">ViewModel 생성하기</h2>
<p>ViewModel은 Android의 아키텍처 컴포넌트로서, 액티비티 또는 프래그먼트와 생명주기를 공유합니다. 따라서 뷰모델 호출은 액티비티 생명주기 메서드 안에서 일어나야 하므로 lazy 블럭을 통해 액티비티가 생성된 이후 필요할 때 뷰모델을 불러올 수 있도록 구현해야 합니다.</p>
<blockquote>
<p>만약 <code>val joinViewModel = JoinViewModel()</code> 과 같이 액티비티에서 직접 생성해서 사용한다면 액티비티가 파괴된 후에 불특정한 시점에 ViewModel이 가비지컬렉터에 의해 파괴될 것입니다. 따라서 ViewModel과 액티비티간의 생명주기가 공유되지 않게 되겠죠. 따라서 ViewModelProvider라는 특수한 객체를 통해 생성해야 합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/6bca6980-c49f-48f1-83b3-69edff1afa5e/image.png" alt=""></p>
<p>open class인 ViewModelProvider의 constructor는 이렇게 구성되어 있는데요, 지금 우리가 사용할 것은 위에서 ViewModelStoreOwner 하나만을 매개변수로 받는 constructor입니다.</p>
<pre><code class="language-kotlin">// JoinViewModel을 생성
    private val joinViewModel : JoinViewModel by lazy{
        ViewModelProvider(this).get(JoinViewModel::class.java)
    }</code></pre>
<p>위 부분이 JoinViewModel을 생성하는 부분인데, ViewModelProvider를 통해 생성하도록 위임 하였습니다. (by 키워드) 또한 lazy 를 통해 액티비티가 완전히 onCreate 된 다음 뷰모델이 호출될 때 호출될 수 있도록 하였습니다. 그 이유는 ViewModelProvider의 인자로 context인 this가 들어가기 때문에 액티비티가 온전히 생성된 다음 호출되어야 하기 때문입니다.</p>
<pre><code class="language-kotlin">private val joinViewModel : JoinViewModel by viewModels()</code></pre>
<p>위 코드도 내부적으로 같은 역할을 하도록 구현되어 있습니다.</p>
<h2 id="viewmodel-안의-ui-상태-변경하기">ViewModel 안의 UI 상태 변경하기</h2>
<pre><code class="language-kotlin">// Id 정보 EditTextChangedListener
binding.userIdEdit.addTextChangedListener(object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

    // 값 변경 시 실행되는 함수
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

    // 입력값 담기
    // userId = userIdEdit.text.toString()
    joinViewModel.setUserId(userIdEdit.text.toString())

    }
}
</code></pre>
<p>위의 입력값 담기 부분 코드를 보면 보면 userId 전역변수에 바로 값을 저장했던 부분이 ViewModel 내의 메서드를 통해 ViewModel이 직접 값을 셋팅할 수 있도록 변경했습니다.
<em><strong>View 역할을 하는 액티비티는 ViewModel의 비즈니스 로직을 호출할 뿐, UI 상태 데이터에 대해 어떠한 작업도 수행하지 않습니다.</strong></em></p>
<h2 id="observer를-통해-livedata-관찰하기">Observer를 통해 LiveData 관찰하기</h2>
<pre><code class="language-kotlin">// userId를 관찰하여 UI 업데이트하는 옵저버
joinViewModel.userId.observe(this, Observer { userId -&gt;
    if (userId.isNotEmpty()) {

        // 버튼 활성화 로직

    } else {

        // 버튼 비활성화 로직

}</code></pre>
<p>위 코드가 ViewModel 안의 userId LiveData를 관찰하여 UI 로직을 수행하는 코드입니다.
위에서 계속 설명했듯이 <em><strong>View에서는 오직 UI 로직만을 수행해야 합니다.</strong></em>
LiveData 는 observe 메서드를 통해 관찰될 수 있으며 observe 메서드에 인자로 들어가는 Observer 객체에서 해당 관찰 데이터에 대한 UI 로직을 구현할 수 있습니다.
Observer 객체는 onChanged() 단 하나의 메서드만 가지고 있으므로 람다식으로 표현한 모습입니다.</p>
<blockquote>
<p>코드 가독성과 이해를 위해 매우 간단한 예제로 다루어 봤지만 이런 방식을 통해 화면 UI 상태를 ViewModel로 분리해내고 View에서 이를 관찰해 화면을 업데이트 하도록 할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Hilt 들고 MVVM 정복] 1. Design Pattern]]></title>
            <link>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-1.-Design-Pattern</link>
            <guid>https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-1.-Design-Pattern</guid>
            <pubDate>Wed, 10 Jan 2024 06:38:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/998d7405-09c3-4aba-8294-d93573a0c254/image.png" alt=""></p>
<h1 id="intro">Intro.</h1>
<p>안녕하세요 이번에 제가 Beering 이라는 프로젝트 팀에 중도합류하게 되어 리팩토링을 진행하게 되었습니다. 처음부터 개발하는 것이 아니라 기존에 개발하던 팀에 합류하여 리팩토링을 진행하는것은 저도 처음 해보는 경험이기 때문에 그 과정을 기록해보고자 이번 시리즈를 기획하게 되었습니다.</p>
<blockquote>
<p>이 시리즈의 의의
사실 예전에 프로젝트를 했을 때 Hilt와 MVVM을 사용하긴 했었습니다만, 그냥 새로운 기술을 써보는것 자체에 의의를 두다보니 내가 왜 이 기술을 쓰고 있는지에 대해 무지한 상태로 무지성 개발을 했던것 같습니다.
그래서 이번기회에 리팩토링을 차근차근 진행하면서 어떤 점이 실제로 개선되는지, 내가 사용하는 기술이 왜 필요한건지 정리해보려고 합니다.</p>
</blockquote>
<h1 id="시리즈에서-리팩토링으로-다룰-코드">시리즈에서 리팩토링으로 다룰 코드</h1>
<p>Beering의 회원가입 로직을 MVVM 아키텍처에 맞게 리팩토링할 예정입니다.
현재는 디자인 패턴이 적용되어 있지 않고 Activity에 대부분의 로직이 몰려있는 상태인데 ViewModel과 LiveData 를 시작으로 해서 차근차근 MVVM에 부합하는 아키텍처로 바꿔가는 과정을 다루겠습니다. 
<em><strong>물론 이론적인 내용위주로 풀어나갈 거에요</strong></em></p>
<p>현재 회원가입 로직을 보면 액티비티에 모든 로직이 몰려있는 상태입니다.</p>
<pre><code class="language-kotlin">class JoinActivity: AppCompatActivity() {
    // Validation을 위한 각종 전역변수들
    // ...
    // ...

    // 회원가입을 위한 모든 로직을 onCreate에 구현
    override fun onCreate(savedInstanceState: Bundle?) {
        // 각종 버튼들에 대한 onClick Listener
        binding.checkboxTerm1On.setOnClickListener {
            binding.checkboxTerm1On.visibility = View.INVISIBLE
            binding.checkboxTerm1Off.visibility = View.VISIBLE
            checkbox1Bool = true
            val serviceAgreement = agreementList.find {it.name == &quot;SERVICE&quot;}
            serviceAgreement?.isAgreed = true
            validJoin()
        }
        // ...
        // ...


        // 각종 Edit Text들에 대한 Change Listener
        passwordEdit.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
                // 어쩌구 저쩌구
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                // 저쩌구 어쩌구
            }
        // ...
        // ...    

    }

    // 비밀번호 유효성 검사 (각종 Validation check를 위한 메서드들)
    fun validatePassword(password: String): Boolean {
        // 비밀번호가 영문자를 포함하는지 확인
        val containsEnglishChars = password.matches(Regex(&quot;.*[a-zA-Z].*&quot;))
        if (containsEnglishChars) {
            binding.conditionEng.setTextColor(
                ContextCompat.getColor(
                    this@JoinActivity,
                    R.color.beering_black
                )
            )
            binding.check1.setImageResource(R.drawable.ic_check_dark)
        } else {
            binding.conditionEng.setTextColor(
                ContextCompat.getColor(
                    this@JoinActivity,
                    R.color.gray01
                )
            )
            binding.check1.setImageResource(R.drawable.ic_check_light)
        }
}
</code></pre>
<p>이제 저 덩어리 코드를 MVVM 아키텍처로 차츰차츰 바꿔나가볼건데 그전에 먼저 안드로이드에서 접할 수 있는 디자인패턴들에 대해 얘기해보고 시작할까 합니다.</p>
<h1 id="design-pattern-in-android">Design Pattern in Android</h1>
<blockquote>
<p>디자인 패턴이란 객체지향을 &#39;잘&#39; 설계하기 위해 재사용성을 올리고, 유지보수성을 향상시키며 설계의도를 명확하게 하기 위한 일종의 소프트웨어 아키텍처 컨벤션 이라고 볼 수 있습니다.</p>
</blockquote>
<p>디자인 패턴이라는 개념 자체가 너무 광범위하고 실제로 그 종류도 수십가지에 달하기 때문에 모든 디자인패턴을 다 공부할 필요는 없다고 생각합니다.
그러나 안드로이드 개발자라면 적어도 안드로이드에서 사용하는 디자인패턴들은 알고있어야겠죠?</p>
<p>안드로이드에서는 크게 3가지 패턴들을 사용하고 있으며 등장 시간순으로 나열해보면 아래와 같습니다.</p>
<ul>
<li>MVC 패턴</li>
<li>MVP 패턴</li>
<li>MVVM 패턴</li>
</ul>
<h1 id="mvc-패턴">MVC 패턴</h1>
<p>안드로이드에서 가장 먼저 접하게 되는 디자인 패턴으로 아래와 같은 구조를 하고 있습니다.
핵심은 Model 부분을 분리하여 데이터를 Model에서 컨트롤하고 뷰에서는 이를 요청하여 받아 사용한다는 점입니다. 대표적으로 스프링 프레임워크에서 많이 사용하는 아키텍처입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d5b6f93f-ad43-4168-a573-33afd4e5edae/image.png" alt=""></p>
<p><strong>M</strong>odel <strong>V</strong>iew <strong>C</strong>ontroller 의 약자로 각각 아래와 같은 역할을 맡습니다.</p>
<ul>
<li><strong>Model</strong></li>
</ul>
<p>데이터를 가지며 애플리케이션에서 사용되는 데이터를 보관하고 처리합니다.
View 또는 Control에 묶이지 않아 재사용 가능하며 View와 Controller는 Model을 통해 데이터를 참조해야 합니다.</p>
<ul>
<li><strong>View</strong></li>
</ul>
<p>사용자에게 보일 화면을 표현합니다.
앱 및 UI와의 상호작용에서 컨트롤러와 통신하며 유저가 어떤 입력(Action)을 하든 View는 무엇을 해야 할지 몰라야 합니다. (오직 화면 그 자체만 표현)</p>
<ul>
<li><strong>Controller</strong></li>
</ul>
<p>사용자로부터 입력을 받고 이 입력을 모델에 전달하거나 View 업데이트를 하게 됩니다.
모델의 데이터 변화에 따라 뷰를 적절히 선택하고 업데이트 시킵니다.</p>
<p>Controller가 Activity나 Fragment정도가 되고 View가 xml파일 혹은 Compose 파일 정도가 되겠네요. 그런데 안드로이드를 개발하다보면 이 둘이 강하게 결합되어 있음을 느낄 수 있습니다.
View와 Controller를 보통 바인딩시켜서 Controller가 View를 직접 제어하는 형태로 개발을 하다보니 둘의 역할을 정확하게 구분짓는 것이 모호해지죠.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/645c1d64-55ac-408c-9f63-a16f5ecd0325/image.png" alt="">
(사진 출처 : <a href="https://velog.io/@jojo_devstory/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8C%A8%ED%84%B4-MVC%EA%B0%80-%EB%AD%98%EA%B9%8C">https://velog.io/@jojo_devstory/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8C%A8%ED%84%B4-MVC%EA%B0%80-%EB%AD%98%EA%B9%8C</a>)</p>
<p>이렇게 Control과 View를 묶는 것이 조금 더 이해하기 쉬워보이기도 합니다.
액티비티에서 UI를 처리하는 로직 외에 데이터를 가공하거나 서버에 요청하는 등의 작업을 Model로 분리시키기만 하면 MVC 아키텍처의 형태로 만들 수 있겠네요.</p>
<p>Model을 분리하여 단위테스트가 가능해지며, 가장 간단한 형태이고 대부분의 로직이 Activity에 집중되어 개발속도가 빠르다는 장점이 있지만 컨트롤러에 많은 로직이 쌓이게 되고 View가 Model을 직/간접적으로 참조하고 있어 의존성이 생긴다는 단점이 있습니다.</p>
<h1 id="mvp-패턴">MVP 패턴</h1>
<p>MVC 패턴에서 생긴 단점을 어느정도 해결하고자 나온 아키텍처이며 <strong>M</strong>odel <strong>V</strong>iew <strong>P</strong>resenter의 약자입니다. 안드로이드에서는 View와 Controller가 강하게 결합되어 있다는 특징이 있다고 했습니다. 따라서 이들을 하나로 합쳐 View라고 칭하며 MVC의 Controller가 담당했던 역할을 Presenter라는 부분에서 담당합니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/91b00d87-aef5-4afe-9298-66fefa54408c/image.png" alt=""></p>
<p>MVC 아키텍처와 얼추 비슷하게 생겼지만 View와 Model간의 의존성이 제거된 형태라고 볼 수 있습니다.
Presenter가 View와 Model 사이를 중개하고 있기 때문입니다.
중요한 점은 Presenter 자체는 interface로써 어떠한 로직도 구현되어 있지 않습니다. View에서 이를 상속하여 구현해주어야 합니다. (의존성이 생기는 단점이 존재한다는 의미가 되기도 합니다.)</p>
<p>이와 같이 MVP로 넘어오면서 Model과 View사이의 의존성은 사라졌지만 결국 Presenter와 View 사이에 1:1 의존성이 또 생겨버리는 단점이 생깁니다. 또한 MVC에서는 Controller에 로직이 몰리기 때문에 비대해질 수 있는단점이 있다고 했는데 MVP 역시 Presenter의 구현체가 많아질 수록 비대해지는 것은 마찬가지입니다.</p>
<h1 id="mvvm-패턴">MVVM 패턴</h1>
<p>이번 시리즈에서 다룰 MVVM 패턴입니다. <strong>M</strong>odel <strong>V</strong>iew <strong>V</strong>iew<strong>M</strong>odel의 약자이며 각각 맡는 역할은 아래와 같습니다.</p>
<ul>
<li>Model
데이터베이스, 네트워크 호출, 파일 시스템, 외부 API와 같은 데이터 소스와 상호 작용하여 데이터를 가져오거나 저장하는 역할을 합니다.
애플리케이션의 핵심 데이터를 정의하고 유지하는데 사용됩니다. 비즈니스 로직과 데이터 저장 및 처리에 관련된 작업을 처리합니다.</li>
</ul>
<ul>
<li><p>View
UI의 시각적 요소를 표시합니다.
화면의 레이아웃, 디자인, 사용자 인터랙션을 담당합니다.
사용자 입력을 받고, ViewModel로부터 데이터를 표시하며, 사용자와 애플리케이션의 상호 작용을 담당합니다.</p>
</li>
<li><p>ViewModel
뷰모델은 뷰에 표시할 데이터를 준비하고, 들고있습니다.
뷰에서 발생한 이벤트 또는 사용자 입력을 처리하여 데이터를 변경합니다.
뷰에 표시할 데이터를 모델로부터 가져와 포맷팅하거나 가공하여 뷰에 알리고, 뷰에서 발생한 변경 사항을 모델에 반영합니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/041ecca2-6d83-4a8e-b3e2-060de15514db/image.png" alt=""></p>
<p>대충보면 MVP랑 비슷하게 생긴것 같습니다. 그런데 차이점은 MVP에서 Presenter가 View를 직접 변경하는것과 달리 ViewModel은 View를 직접 변경하지 않는 다는 점입니다. 점선으로 표시된 Notify State라는 표시는 직접 View에 관여하는 것이 아니라 데이터나 상태의 변경만 알리면 View가 알아서 UI를 업데이트 하는 것을 의미합니다.</p>
<p>안드로이드에서는 이를 ViewModel 클래스와 observable 상태를 통해 구현합니다.</p>
<p>요약해보자면 </p>
<ul>
<li>ViewModel이 어플리케이션의 비즈니스로직에 필요한 모든 데이터를 들고 있고 View는 ViewModel의 데이터들을 관찰하고 있다가 변화가 일어나면 해당 데이터에 맞게 UI를 업데이트 합니다. </li>
<li>또한 UserAction이 View를 통해 들어오면 View는 ViewModel에게 데이터의 업데이트를 요청하고 ViewModel은 내부의 비즈니스 로직에 따라 적절히 데이터를 업데이트 하게 됩니다.</li>
<li>그러면 다시 변경된 데이터에 의해 View가 업데이트 됩니다.</li>
<li>만약 ViewModel에서 데이터를 서버나 DataSource에 요청해야 한다면 Model을 통해 해당 데이터를 셋팅합니다.</li>
<li>Model은 애플리케이션에서 사용되는 데이터 형식입니다. 
(예를들면 Data class)</li>
</ul>
<h1 id="요약해보면">요약해보면</h1>
<p>MVC 패턴을 통해 Model을 분리하여 데이터를 가공하여 처리하고, 표현하는 비즈니스 로직을 따로 관리하기 시작하였습니다. </p>
<p>그러나 안드로이드의 특성상 View와 Controller의 경계가 모호하였고 결국 이 둘의 개념은 MVP 패턴이 등장하면서 View라는 개념으로 합쳐집니다. 대신 Presenter라는 새로운 개념이 등장하여 Model과 View의 의존성을 완전히 분리시키며 둘 사이의 징검다리 역할로써 동작하게 됩니다.</p>
<p>그러나 MVP 역시 Presenter가 직접 Model과 View를 제어하다보니 Presenter에 과도한 비즈니스 로직이 집중되는 문제점이 있었고 MVVM 패턴이 등장하여 ViewModel을 View가 관찰하여 스스로 업데이트 하도록 함으로써 해결하게 됩니다.</p>
<p>최종적으로 MVVM아키텍처로 설계함으로써 ViewModel은 데이터를 요청 및 가공하여 들고있고, Model은 데이터를 표현하고, View는 UI를 업데이트 하는 로직을 담당하여 적절한 역할의 분배가 이루어지게 됩니다.</p>
<blockquote>
<p>안드로이드에서 여러가지 디자인 패턴이 등장한 배경과 각각의 장단점에 대해 알아보았는데, 더 자세한 구현방법에 대한 내용은 차차 포스팅 해나가도록 하겠습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[삽질일기] MySQL 설치]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%82%BD%EC%A7%88%EC%9D%BC%EA%B8%B0-MySQL-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%82%BD%EC%A7%88%EC%9D%BC%EA%B8%B0-MySQL-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sat, 16 Dec 2023 13:10:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d1ea1ff7-eb16-42fb-ac86-7d48e1ccb7fd/image.png" alt=""></p>
<blockquote>
<p>MySQL을 설치하기 위한 삽질과정과 알게된점을 정리한 글입니다.
아! SAMSUNG 노트북을 쓰기 때문에 Windows 환경입니다. Mac이나 Linux 환경은 저도 잘 모릅니다 ^~^</p>
</blockquote>
<h1 id="사건의-발단">사건의 발단</h1>
<p>서버 공부를 한번 해보려고 배달의 민족 ERD를 짠다음 SQL export 해서 실제 DB와 서버를 구축해보려고 했다.</p>
<blockquote>
<p><strong>여담</strong>
이게 내가 짠 ERD이다. <em><strong>(나 라는 존재가 세상에 나와 가장 처음 창조한 ERD)</strong></em> 
<a href="https://www.erdcloud.com/d/pjFpLeTQTso4tf2E7">https://www.erdcloud.com/d/pjFpLeTQTso4tf2E7</a>
안드로이드 개발하다가 서버 디비 설계를 하다보니 &#39;어... 이런건 안드쪽에서 구조 만든다음에 넘겨주면 더 효율적이지 않나...?&#39; 라는 생각이 꽤 자주 들었다.
그래서 이부분은 프론트나 클라이언트에서 만들어주게 구현하면 되겠지 하는 부분이 많다보니 듬성듬성 허술해보이는 부분도 있고 한데.. 실제론 이런 고민이 있을 때 ERD에 아예 반영을 안하는게 맞는건지 잘 모르겠다.</p>
</blockquote>
<p>여튼 무지성 ERD 짜고 막상 SQL 코드로 추출하고나니 이걸 DB로 구축하려면 MySQL이 필요했고 설치해보려고 했다.</p>
<h1 id="mysql-설치-과정">MySQL 설치 과정</h1>
<p><a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a>
위 사이트로 들어가면 커뮤니티버전 MySQL을 설치할 수 있는데 </p>
<ul>
<li>MSI installer 버전</li>
<li>ZIP Archive 버전</li>
</ul>
<p>두 가지가 있다. MSI installer는 설치 프로그램을 통해서 GUI로 MySQL을 설치가능한 버전이고 ZIP Archive 버전은 직접 zip파일을 압축해제해서 초기설정과 경로셋팅 등을 직접 해주어야 하는 버전이다.</p>
<p>당연히 쉬워보이는 MSI installer 먼저 시도해봤지만 알 수 없는 무수한 에러로 인해 약 2시간의 사투 끝에 포기했다.</p>
<p>이전에 MySQL 설치했다가 지웠었는데 그게 깔끔하게 안지워졌는지 자꾸 이전버전이랑 꼬이면서 잘 안됐다.</p>
<p>그래서 ZIP Archive로 갈아타서 <em><strong>&#39;개발자답게&#39;</strong></em> 직접 cmd에서 설치를 진행했다. </p>
<p>일단 뭐 대충 다운로드 받아서 원하는 경로에 압축해제하면 된다 </p>
<p>그리고 환경변수 설정을 해주어야 하는데 무지성 구글링은 건강에 좋지 않으므로 <em><strong>&#39;개발자답게&#39;</strong></em> <a href="https://dev.mysql.com/doc/mysql-installation-excerpt/8.2/en/environment-variables.html">공식문서</a>를 통해서 한번 확인을 해보자.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/81bec312-fb99-4516-a51f-b3e0a01b6cab/image.png" alt=""></p>
<p>오....뭐가 많은데 환경변수라는게 애초에 필수적인게 아니라 선택적인 영역이므로 딱 한가지만 보면 된다.
PATH 환경변수는 shell에서 mysql 명령어를 사용할 수 있게 해주기 때문에 <em><strong>&#39;개발자답게&#39;</strong></em> cmd로 mysql 설치를 할거면 이건 꼭 필수로 등록해주자.</p>
<p>환경변수 설정하는법은 뭐 구글링 해보면 많이 나오니까 PASS</p>
<p>그리고 구글링하다보면 MYSQL_HOME 이라는 환경변수도 설정하는 경우가 있는데 얘는 my.cnf 파일의 위치를 지정해주는 변수이다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/d6418465-a326-4208-91c2-fc542afa391e/image.png" alt=""></p>
<p>my.cnf는 MySQL 서버의 각종 default 설정을 담당하는 파일인데 얘가 없어도 DB자체는 올라가기 때문에 일단 연습하는 단계에서는 굳이 안건드려도 된다. </p>
<p>만약에 MYSQL_HOME 환경변수 설정을 했다면?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a1790479-bdcb-4ed3-848f-377a41a13281/image.png" alt=""></p>
<pre><code>mysql --help </code></pre><p>쳤을 때 위 사진처럼 나오는데 읽어보면 Default 옵션들이 아래 경로에 있는 my.cnf나 my.ini를 순차적으로 탐색해서 찾은 파일에 의해 설정된다고 나온다. 가장 우측에 보이는 D드라이브 경로가 내가 MYSQL_HOME으로 설정한 경로. 
<del>(뭐 연습단계에선 안중요하니깐 넘어가요)</del></p>
<h1 id="windows에서-mysql-실행">Windows에서 MySQL 실행</h1>
<p>이제 우리는 편리한 MSI installer를 버려두고 ZIP Archive를 사용했기 때문에 cmd 코드 몇줄 치는 벌을 받아야 한다. </p>
<pre><code>mysqld --initialize
mysqld --install
net start mysql
</code></pre><p>까지 하면 MySQL 서버 초기화하고 실행시키는것까지 된다.
명령어에 대해 궁금해서 조사한 내용 바탕으로 설명을 좀 덧붙였다.</p>
<h3 id="mysqld">mysqld</h3>
<blockquote>
<p>mysqld, also known as MySQL Server, is a single multithreaded program that does most of the work in a MySQL installation. It does not spawn additional processes. MySQL Server manages access to the MySQL data directory that contains databases and tables. The data directory is also the default location for other information such as log files and status files.</p>
</blockquote>
<p>시스템 프로그래밍 관련해서 공부를 좀 하다보면 콘솔창에서 치는 이 명령어도 결국에는 하나의 응용프로그램인것을 알 수 있는데 mysqld라는 응용프로그램은 MySQL 서버에서 멀티스레딩으로 동작하는 데몬프로세스이다. (mysql뒤에 붙은 d가 Daemon이라는 뜻)</p>
<p>데몬은 유닉스(Unix) 운영체제에서 부팅 시 자동으로 켜져, 백그라운드에서 계속 실행되는 프로세스.</p>
<p>따라서 MySQL 서버가 실질적으로 동작하는 백그라운드 프로그램이라고 보면 된다.</p>
<p>그럼 mysql 명령어는 뭐고 mysqld랑 뭐가 다르냐?
<em><strong>mysql명령어는 클라이언트 프로그램을 실행하여 포그라운드에서 사용자와 상호작용하기 위한 명령어이다.</strong></em>
정리해보면</p>
<ul>
<li><code>mysqld</code>는 백그라운드에서 동작하는 실질적인 MySQL 서버 데몬 프로세스이기 때문에 서버의 구동, 초기화, 클라이언트 요청과 실제 데이터베이스 작업 처리를 담당한다. </li>
<li><code>mysql</code>은 포그라운드에서 동작하는 명령어이고 MySQL 서버에 연결하여 데이터베이스 관리, 쿼리 실행, 데이터 검색, 수정, 삭제 등과 같은 데이터베이스 작업을 요청하는 인터페이스를 제공한다.</li>
</ul>
<h3 id="mysqld---initialize"><code>mysqld --initialize</code></h3>
<blockquote>
<p>Information managed by the MySQL server is stored under a directory known as the data directory. The following list briefly describes the items typically found in the data directory, with cross references for additional information:
After MySQL is installed, the data directory must be initialized, including the tables in the mysql system schema:</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/bcb77b4e-14d8-42be-a856-994d386ef9f1/image.png" alt=""></p>
<p>&#39;mysqld --initialize&#39; 명령어는 각종 초기화 과정을 통해 data 디렉토리를 생성한다.
data 디렉토리는 스키마 정보와 실제 데이터들을 저장하는 디렉토리이기 때문에 말그대로 data = database 라고 보면 된다. (1대1 대응)
데이터베이스 안에 여러 테이블이 있고 각 테이블에 데이터가 저장되는데 이 모든 정보가 data 디렉토리 안에 들어가는 셈.</p>
<h3 id="mysqld---install"><code>mysqld --install</code></h3>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/server-options.html#option_mysqld_install"><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/4c4c4d4c-9844-4ee3-80ac-d16cf453b7b0/image.png" alt=""></a></p>
<p>mysql 관련 파일을 가져와서 압축해제까지 했지만 아직 Windows에서 사용될 준비가 되지 않았습니다. 
Windows운영체제에서는 서비스라는 개념으로 백그라운드 프로세스를 관리하는데 &#39;Windows 키&#39; -&gt; &#39;서비스&#39; 라는 것을 검색해서 열어보면 아래 사진과 같이 네트워크나 보안 등등 각종 백그라운드 프로그램이 실행되고 있는 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5266d4f6-dc2d-422f-9821-de1a0ab8bfdd/image.png" alt="">
따라서 방금 전에 설치하고 초기화를 마친 MySQL을 Windows의 서비스 형태로 만들어주는 과정이 필요한데 이게 mysqld --install 명령어입니다.
--install 옵션뒤에 서비스명을 붙일 수 있지만 안붙여도 자동으로 MySQL 로 초기화 됩니다.</p>
<h3 id="net-start-mysql"><code>net start mysql</code></h3>
<p><code>net start</code> 명령어는 Window 서비스를 실행시키는 명령어입니다.
위에서 --install 옵션 통해서 MySQL을 서비스형태로 만들어줬죠? 이걸 실행시키는 겁니다.  만약 <code>mysqld --install araralla</code> 와 같이 옵션으로 서비스명을 지정해줬다 -&gt; <code>net start araralla</code> 가 되는겁니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/8288280e-fd7e-448b-8d93-7f9adc0980a0/image.png" alt=""></p>
<p>만약 위와 같이 <code>Can&#39;t connect to MySQL server on &#39;localhost&#39; (10061)</code> 에러가 나오면 MySQL 서비스가 실행되지 않아서 접속을 못하는 거니 겁먹지않고 net start mysql 해주시면 해결됩니다.</p>
<h1 id="🚨문제발생🚨">🚨문제발생🚨</h1>
<p>자~~ 이제 DB를 백그라운드로 돌리는거까지 했으니까 직접 mysql에 들어가서 스키마 만들고 디비작업 이것저것 해줘야겠죠?</p>
<pre><code>mysqld -u root -p</code></pre><p>명령어로 입장해봅시다.</p>
<p>띠롱?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/f14dd741-dedf-4fbd-92fa-eb8c5d2dc93e/image.png" alt=""></p>
<p>난 아무것도 한게 없는데 뭔 password를 치라고 나옵니다. 그래서 그냥 아무것도 안치고 Enter 누르니까 <code>using password : NO</code> 가 나옵니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ab80f8c8-5ef6-4ffe-96e4-86348496791f/image.png" alt=""></p>
<p>아 뭔가 비밀번호를 설정해야 하나? 싶어서 원하는 비밀번호 누르면 <code>using password : YES</code> 에러가 나옵니다.ㅋㅋㅋ <del>(뭐 어쩌라고 아잇윽엑윽)</del></p>
<p>여기서 시간을 진짜 많이 까먹었는데 구글링 해서 얻은 정보는 아래와 같습니다.</p>
<ul>
<li>비밀번호가 틀렸으니 mysql에 접속해서 비밀번호를 변경해라.</li>
<li><blockquote>
<p>아니 접속 자체를 못하는데 어케 변경하라는거야 -&gt; 기각</p>
</blockquote>
</li>
<li>재접속 해라 -&gt; 응 해도 안돼 -&gt; 기각</li>
<li><code>mysqld --skip-grant</code> 명령어를 통해 인증없이 mysql에 진입해서 비밀번호 바꿔라 -&gt; 정책이 바뀌었는지 제 환경 문제인지 이제 먹히지 않더군요. -&gt; 기각</li>
</ul>
<p>진짜 여러가지 시도를 많이 해봤는데 계속 <code>using password : NO</code> 와 <code>using passwordd : YES</code> 만 반복되니 정신이 나가버릴 뻔 했습니다. 
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/43047f0d-74c4-414f-9dbe-5fa1a23e9699/image.png" alt=""></p>
<p><del>2시간동안 YES 와 NO를 왕복하다 정신이 나가버린 내 모습</del></p>
<h1 id="해결방법">해결방법</h1>
<p>심기일전 해서 구글링에 의존하지 말고 공식문서로 한번 돌파해봅시다.</p>
<p>일단 제가 생각했을 때 이 에러가 나타난 원인은 아래와 같습니다.</p>
<blockquote>
<p>mysql을 초기화할 때 제멋대로 비밀번호가 생성되었는데 내가 그걸 모른다.</p>
</blockquote>
<p><em><strong>이거 아니면 말이 안되는게 비밀번호가 없는데 비밀번호를 치라고 나오고 안치면 안들여보내주는거는 프로그램이 잘못된거죠 ㅋㅋ 이 문제인건 확실한거 같습니다.
아니 근데 왜 지멋대로 생성해놓고 안알려주냐고!!</strong></em></p>
<p>그렇다면 이 제멋대로 생성된 비밀번호를 찾던지 아니면 비정상적인 방법을 통해 억지로 mysql에 접속하여 강제로 비밀번호를 변경하던지 와 같은 방법으로 해결할 수 있을것 같았습니다.</p>
<p>일단 비정상적인 방법은 나쁘기 때문에 쓰지 않기로 하고 시작단계부터 한번 탐색해봅시다.</p>
<p>mysql 초기화 단계에서 만약 실제로 비밀번호가 생성되는거라면 초기화 하는 명령어에 뭔가 실마리가 있지 않겠습니까?
<a href="https://dev.mysql.com/doc/refman/8.0/en/data-directory-initialization.html#data-directory-initialization-overview">https://dev.mysql.com/doc/refman/8.0/en/data-directory-initialization.html#data-directory-initialization-overview</a></p>
<p>그래서 <code>mysqld --initialize</code> 할 때 무슨일이 일어나는지 알아보고자 공식문서를 들고왔습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/dd48c894-ab1e-4b3c-9c8d-c4d1ba6ac54e/image.png" alt=""></p>
<p>어...? <code>--initialize</code> 를 하면 랜덤으로 비밀번호를 생성하고 <code>--initialize-insecure</code> 옵션을 사용하면 비밀번호를 생성하지 않고 바로 접속할 수 있다고 바로 써 있네요...?</p>
<p>저의 가설이 맞았습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/f6a6c405-fe3f-4a1b-b9da-2ce970efed07/image.png" alt=""></p>
<pre><code>net stop mysql
mysqld --remove</code></pre><p>로 기존의 MySQL 서비스를 중지한 다음 삭제합니다.
추가로 initialize를 통해 생긴 data 디렉토리도 지워주셔야 합니다. </p>
<p>그리고 다시</p>
<pre><code>mysqld --initialize-insecure
mysqld --install
net start mysql</code></pre><p>을 통해 데이터베이스를 실행시켜주면 </p>
<p><img src="blob:https://velog.io/08e8164d-b0a1-4486-b99d-b02ea931f740" alt=""></p>
<p>짜잔~~ 이렇게 비밀번호 없이 mysql에 접속가능합니다.
물론 이 방법은 비밀번호 생성없이 DB를 만들었기 때문에 보안상 위험한 상태입니다. 반드시 접속후 바로 비밀번호를 설정해주세요.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/73705396-52cd-4518-8aed-b55ce3b01f34/image.png" alt=""></p>
<p>위 그림처럼 두 개의 쿼리를 날려서 비밀번호를 설정해줄 수 있습니다.</p>
<h2 id="--initialize-옵션-사용법"><code>--initialize</code> 옵션 사용법</h2>
<p>추가적으로 --initialize 옵션을 사용했을 때 랜덤생성된 비밀번호는 어디서 알 수 있는지 궁금해서 찾아봤습니다. </p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/48c19089-823f-49a2-b4f2-b67684eb8ffa/image.png" alt=""></p>
<p>공식문서 보니까 Error Log로 Warning 도메인에 띄워주는거 같네요 
그럼 이 Error Log는 어디서 보냐!</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5c30eb75-8f48-42fe-b10a-4ecb8f88f8d0/image.png" alt=""></p>
<p><code>--initialize --console</code> 옵션을 주면 콘솔창에 저렇게 임시 비밀번호를 찍어줍니다.
 --console 옵션을 까먹고 안줬다면 걱정마세요</p>
<p>data 디렉토리 안에 (노트북 이름).err 파일에 로그가 기록되기 때문에 찾아보시면 알 수 있습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/80c5086b-e4bd-4a24-a673-5f3e0a13bfb7/image.png" alt=""></p>
<p>이상으로 저는 mysql 설치 및 세팅에 대해 깨달음을 어느정도 얻게 되었습니다. 안녕히계세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드에서 이미지를 제대로 다뤄보자 feat.BitmapFactory]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%A4%EB%A4%84%EB%B3%B4%EC%9E%90-feat.BitmapFactory</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%A4%EB%A4%84%EB%B3%B4%EC%9E%90-feat.BitmapFactory</guid>
            <pubDate>Tue, 12 Dec 2023 19:31:22 GMT</pubDate>
            <description><![CDATA[<p>졸업 프로젝트를 진행하면서 멀티모달 모델을 안드로이드 기기에서 돌려야 했습니다. 일단 제가 쓰는 모델은 vision 모델 하나와 nlp 모델 하나 이렇게 두 가지인데 vision모델을 사용하기 위해서는 이미지 전처리 과정을 거쳐야겠죠? 
사실 파이썬에서는 numpy, torch 쓰면 코드 몇줄 찍찍에 뚝!딱! 끝나는 과정인데 </p>
<blockquote>
<p><em><strong>&quot;이 이미지 전처리 과정을 코틀린, 자바 언어를 사용하는 안드로이드 OS 도메인에서 어떻게 재현하지?&quot;</strong></em> </p>
</blockquote>
<p>가 이번 포스팅을 통해 해결해야 할 과제가 되겠습니다</p>
<h1 id="이미지-불러오기">이미지 불러오기</h1>
<p>먼저 이미지 전처리를 하려면 이미지를 불러와야겠죠?? <em><strong>&quot;파이썬에서는&quot;</strong></em> 코드 한줄 찎 쓰면 PIL로 이미지를 가져올 수 있지만 안드로이드에서는 조금 복잡한 과정을 거쳐야 합니다. 
일단 BitmapFactor라는 유용한 클래스를 활용해서 이미지 파일을 비트맵으로 불러올 수 있는데 한번 해보겠습니다.</p>
<pre><code class="language-kotlin">for (imgID in imgList){
            val bitmap = BitmapFactory.decodeResource(this.resources, imgID)
            bitmapList.add(bitmap)
        }</code></pre>
<p>imgList에는 이미지파일에 대한 location 정보가 담겨있는데 원래는 갤러리에서 이미지를 불러와서 구현해야 하지만 현재 데모 작성중이므로 Resources 에 직접 이미지 파일을 저장해서 불러오도록 하겠습니다.
BitmapFactor의 decodeResource 매서드를 통해 리소스이미지를 비트맵으로 불러올 수 있습니다.
갤러리에서 불러오려면 적절히 로직을 취해서</p>
<ul>
<li>decodeFile(String pathName)</li>
<li>decodeFileDescriptor(FileDescriptor fd)</li>
<li>decodeStream(InputStream is)</li>
</ul>
<p>와 같은 매서드를 활용하시면 되겠습니다.
결과를 볼까요?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/37b2d7e4-2f1b-426f-85ae-448840ead0ae/image.png" alt=""></p>
<p>왼쪽이 파이썬으로 불러온 PIL 이미지 사이즈이고 오른쪽이 BitmapFactory를 활용해서 불러온 이미지 사이즈입니다.
이상하게 동일한 이미지를 불러왔는데 각각 사이즈가 다르게 출력이 되네요. 자세히 살펴보면 안드로이드에서 불러온 이미지가 파이썬PIL이미지보다 약 3배가량 큰 사이즈를 가지고 있는 걸을 발견할 수 있습니다.</p>
<p>이를 통해 우리가 간과하고 있는 사실을 하나 유추해볼 수 있습니다. 안드로이드 개발자라면 모두 알고있어야 하는 사실중 하나로, 안드로이드 휴대폰들은 기종마다 제각각인 화면 비율과 해상도를 가지고 있다는것! 
따라서 다른 휴대폰으로 찍은 사진들은 모두 다른 해상도를 가질텐데<del>(물론 화면 해상도와 전혀 관계없는 카메라 성능차이랍니다)</del> 어느 휴대폰이든 항상 꽉찬 화면에 표시가 되게 되죠</p>
<p>그 이유는 BitmapFactory에서 비트맵파일로 디코딩할 때 기본적으로 화면 해상도를 고려하여 DP단위로 재구성하기 때문입니다.
일반적인 상황에선 문제가 없겠지만 이미지 전처리 과정에 이런 Rescaling 과정이 포함되어버리면 전처리 프로세스가 깨져 제대로된 결과를 얻을 수 없게 됩니다. </p>
<h1 id="원본해상도의-이미지-가져오기">원본해상도의 이미지 가져오기</h1>
<p>BitmapFactory가 자동으로 이미지를 Rescaling해서 변환시켜주는거라면 원본 해상도의 비트맵을 얻기 위한 방법도 BitmapFactory에서 찾을 수 있을거 같으니 <a href="https://developer.android.com/reference/android/graphics/BitmapFactory">공식문서</a>를 한번 구경해봅시다
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/4ace1e99-7512-4814-95eb-0e1794ec5873/image.png" alt="">
아까 사용했던 decodeResource 함수에 두 가지 버전이 존재하는군요!
기본적으로는 BitmapFactory.Options가 null이 들어가 기본값으로 적용되고 있었는데 이를 직접 커스텀 해줄 수 있는것 같습니다.
그럼 이 Options를 어떻게 잘 조작하면 Rescaling과정을 건너뛸 수 있지 않을까요?
다시 위 <a href="https://developer.android.com/reference/android/graphics/BitmapFactory.Options#inScaled">Options에 대한 공식문서</a>로 들어가보면 아래와 같은 내용을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/9ff8dc85-639c-490c-bfb6-6ca2ad5da530/image.png" alt="">
일단 inDensity, targetDensity에 대한 내용도 위 문서에 있으니 읽어보면 inDensity는 이미지에 대한 해상도이고 targetDensity는 이미지가 그려질 기기 화면에 대한 해상도입니다. 따라서 위 inScaled 속성값이 기본적으로 true로 되어있기 때문에 이미지 크기가 화면 해상도인 targetDensity에 맞춰 재조정되어 비트맵으로 반환되는 것입니다.
따라서 inScaled 변수를 false로 주면 Rescaling 과정이 생략되겠죠?</p>
<pre><code class="language-kotlin">val bmpFactoryOption = BitmapFactory.Options()
bmpFactoryOption.inScaled = false
for (imgID in imgList){
    val bitmap = BitmapFactory.decodeResource(this.resources, imgID, bmpFactoryOption)
    bitmapList.add(bitmap)
}</code></pre>
<p>짜잔 이렇게 BitmapFactory.Options()를 직접 커스텀 해주면 아래와 같이 원본 사이즈의 이미지 비트맵이 반환되는것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/1a7f4778-65d8-4334-a5b3-9f4896e786bb/image.png" alt=""></p>
<p>오예~! 이제 원본 이미지를 가져왔으니 지지고 볶아서 전처리 과정을 구현해주면 되겠죠? 이미지를 전처리 하는 과정은 다음 포스팅에서 다룰 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jetpack Compose] Compose의 상태 변화 감지 feat.리컴포지션]]></title>
            <link>https://velog.io/@renovatio_hyuns/Jetpack-Compose-Compose%EC%9D%98-%EC%83%81%ED%83%9C</link>
            <guid>https://velog.io/@renovatio_hyuns/Jetpack-Compose-Compose%EC%9D%98-%EC%83%81%ED%83%9C</guid>
            <pubDate>Mon, 04 Sep 2023 05:50:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 <a href="https://developer.android.com/jetpack/compose?hl=ko">Android developers&#39; Jetpack Compose 공식문서</a>를 참고하여 작성되었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/1264abd7-6d1b-4c1a-9db6-4af0fb7d8402/image.png" alt=""></p>
<h1 id="컴포지션이란">컴포지션이란?</h1>
<p>Jetpack Compose를 공부하다보면 컴포지션, 리컴포지션 이라는 용어가 나옵니다. Composition은 사전적 의미로 &#39;구성&#39; 이라고 하는데 쉽게말해 @Composable 어노테이션을 활용하여 작성한 함수가 실제 UI로 구성되는 과정을 의미합니다.</p>
<p>즉 맨 처음 UI로 초기화 될 때는 composition, 클릭이나 다른 이벤트가 일어나 UI의 상태가 바뀌어 재구성 될 때는 recomposition이라 부르는 것입니다.</p>
<h2 id="그렇다면-recomposition은-언제-일어날까요">그렇다면 Recomposition은 언제 일어날까요?</h2>
<p>클릭하거나 데이터를 제출하여 어떠한 이벤트가 발생했을 때 UI에 변화가 필요할 수 있습니다.</p>
<p><img src="https://developer.android.com/codelabs/jetpack-compose-basics/img/783e161e8bb1b2d5.gif" alt=""></p>
<p>위와 같이 버튼을 눌렀을 때 상세보기 페이지가 펼쳐지고, 버튼의 상태가 바뀌거나 하는 것과 같이 UI가 변경되어야 할 필요성이 있는데 이를 Recomposition이라 부릅니다.
처음 뷰가 초기화 되는 과정을 Composition이라 부른다고 했는데 데이터의 변화를 관찰하여 Composition이 다시 일어나는 과정을 Recomposition이라 부르는 것입니다.</p>
<p>그렇다면 Recomposition이 일어나기 위해서는 데이터의 변화를 관찰하여 뷰가 새로 그려지는 시점을 정의하는 트리거 역할을 무언가가 있어야 합니다.</p>
<p>&#39;데이터의 변화를 관찰하여 뷰가 새로 그려지는 시점을 정의&#39; 한다는 것은 공식문서에 의한 표현으로 &#39;상태(State)가 변경되는 시점&#39; 을 의미합니다. 
상태(State)가 변경되는 시점을 관찰하기 위해서는</p>
<ul>
<li>MutableStateOf</li>
<li>remember</li>
<li>LiveData</li>
</ul>
<p>등의 클래스와 메서드를 활용할 수 있으며 아래에서 실습을 통해 알아보도록 하겠습니다.</p>
<h1 id="상태변화란">상태변화란?</h1>
<pre><code class="language-kotlin">@Composable
private fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colors.background
    ) {
        Greeting(&quot;Android&quot;)
    }
}

@Composable
private fun Greeting(name: String) {
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = &quot;Hello, &quot;)
                Text(text = name)
            }
            ElevatedButton (
                onClick = {
                // TODO
                }
            ) {
                Text(text = &quot;Show more&quot;,
                    color = MaterialTheme.colors.primary)
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
    Compose_practiceTheme {
        MyApp()
        MyApp()
    }
}</code></pre>
<p>위와 같은 코드를 통해 아래의 View를 만들었습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/db4a913e-bdcd-4418-a563-29cd01dd8711/image.png" alt=""></p>
<p>이제 Show more 버튼을 눌러서 UI가 업데이트 되는 recomposition을 구현해야 하는데요 요구사항은 아래와 같습니다.</p>
<ul>
<li>버튼을 눌렀을 때 텍스트가 Show more / Show less 로 서로 바뀌기</li>
<li>버튼을 눌렀을 때 Hello, Android 페이지가 펼쳐지고 접히기</li>
</ul>
<p>위 기능을 구현하기 위해서는 각 페이지가 현재 펼쳐진 상태인지를 나타내는 flag변수 하나가 필요합니다.
또한 flag 변수의 상태에 의해 버튼의 onClick 동작을 구현해주어야 합니다.</p>
<p>그래서 위의 Greeting 컴포넌트를 </p>
<pre><code class="language-kotlin">@Composable
private fun Greeting(name: String) {
    var expanded = false    // 펼쳐져 있는 지에 대한 상태변수
    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = &quot;Hello, &quot;)
                Text(text = name)
            }
            ElevatedButton(
                onClick = {
                    expanded = !expanded
                    Log.d(&quot;is_expannded&quot;, expanded.toString())
                }
            ) {
                Text(text = if (expanded) &quot;Show less&quot; else &quot;Show more&quot;,
                    color = MaterialTheme.colorScheme.primary)
            }
        }
    }
}</code></pre>
<p>와 같이 수정하였습니다. </p>
<p>Greeting이라는 컴포넌트가 펼쳐져있는 지를 나타내는 상태변수 expanded를 만들었고 expanded의 상태에 따라 ElevatedButton의 text가 바뀌고 Column 부분의 패딩이 동적으로 바뀌게 하여 펼쳐지는 효과를 주었습니다.</p>
<p>이론상 완벽하게 구현한것 같지만 이 뷰는 <em>*<em>우리의 생각대로 동작하지 않습니다. *</em></em></p>
<p>상태에 대한 변수를 지정해주긴 했지만, 상태 변화를 감지할 수 없기 때문에 아무짝에도 쓸모없는 변수가 되었기 때문입니다.</p>
<p>즉 상태에 대한 변수를 선언하기 위해서는 일반적인 변수가 아닌 상태 변화를 감지할 수 있는 타입의 변수가 필요하고 이를 <code>MutableStateOf&lt;T&gt;</code> 타입의 변수로 선언가능합니다.</p>
<h1 id="상태-변화-감지하기">상태 변화 감지하기</h1>
<p>상태 변화를 감지해서 recomposition을 일으키기 위해서는 MutableStateOf 변수를 사용하면 됩니다.</p>
<p><a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#mutableStateOf(kotlin.Any,androidx.compose.runtime.SnapshotMutationPolicy)"><code>mutableStateOf()</code></a> 메서드는 관찰 가능한 <a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState"><code>StateOf&lt;T&gt;</code></a> 객체를 생성하는데 이는 런타임시 Compose에서 관찰 가능한 객체로써 구현되어 있습니다.
즉 MVVM 패턴에서 LiveData를 생성하고 이를 Observer에서 관찰하는 것과 동일한 효과라고 생각하시면 될것 같습니다.</p>
<p>위 코드에서 일반타입의 변수 expanded를 MutableStateOf로 바꾸기 위해 아래와 같이 선언하게 된다면</p>
<pre><code class="language-kotlin">val expanded = mutableStateOf(false) // error</code></pre>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/432d89bc-4b8b-4534-b398-65bce41d008c/image.png" alt=""></p>
<p>아마도 이런 오류를 마주치게 될 텐데요 위의 오류메시지와 설명을 통해 알 수 있는 정보로</p>
<ul>
<li>mutableStateOf() 함수는 remember라는 키워드와 함께 사용해야 한다.</li>
<li>mutableStateOf로 생성된 MutableState객체의 타입은 T (제너릭)으로 어떤 타입이든 들어갈 수 있다.</li>
<li>mutableStateOf로 생성된 MutableState객체는 (변수명).value로 값에 접근이 가능하다</li>
</ul>
<p>정도가 있겠네요.</p>
<p>가장 첫번째 에러메시지에 해당하는 remember 키워드와 함께 사용해야 한다는 것은 무슨 의미일까요?</p>
<h2 id="recomposition-process">recomposition process</h2>
<p>일단 remember에 대해 다루기 전에 recomposition이 어떤 과정을 통해 이루어지는지 알아볼 필요성이 있습니다.
상태의 변화가 일어날 때 마다 모든 UI가 업데이트 되는 것은 비효율적이며 UX에 있어서도 좋지않은 경험을 줄 것입니다.</p>
<p>따라서 recomposition이 일어날 때에는 필요한 Composable만 업데이트가 되도록 해야합니다.</p>
<p>먼저 Composable의 lifecycle을 보면 아래와 같습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/c3e020ca-5120-47df-aa3f-e8fd5cf15bbf/image.png" alt=""></p>
<p>딱 3가지로 분류되는데 </p>
<ul>
<li>composition 진입</li>
<li>recomposition 반복</li>
<li>composition 종료</li>
</ul>
<p>이것이 Activity나 Fragment에 비해 매우 간단한 Composable의 생명주기입니다.</p>
<p>그렇다면 Composition에 진입할 때에는 무슨 일이 일어날까요?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/c96951f8-4d99-45d5-8b6e-a833ed3af954/image.png" alt=""></p>
<p>NewsFeed를 구성하는 화면을 띄운다고 가정해봅시다. 최종적으로 Newfeed가 Screen에 띄워져야 하고 NewFeed를 구성하는 각각의 작은 Composable단위인 StoryWidget들이 있습니다.</p>
<p>각각의 Composable 단위인 StoryWidget에는 MutableState와 같이 상태나 정보를 담은 데이터들이 있습니다.
즉 Composition이 일어나면서 전체 뷰 (NewsFeed)가 초기화되며 세부 단위인 각각의 작은 Composable들의 데이터가 초기화되게 됩니다.</p>
<p>그렇다면 Recomposition이 일어날 땐 어떻게 될까요?
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5186a8dc-4e43-4671-b8bc-ea9e3b1bba51/image.png" alt=""></p>
<p>가장 아래쪽의 작은 초록색 부분의 데이터가 변경되어 상태변화를 감지하게 되면 해당 데이터가 존재하는 Composable만 업데이트가 되도록 Compose Compiler에서 추적하여 계층적으로 업데이트 할 수 있습니다.</p>
<p>즉 어떤 상태 변수가 어떤 Composable 안에 있는지 컴파일러가 추적가능함으로써 상태변화에 의해 변경되어야만 하는 필요한 부분만 업데이트 될 수 있는 것입니다.</p>
<h2 id="remember의-의미">remember의 의미</h2>
<p>만약에 <code>val expanded = mutableStateOf(false)</code> 와 같은 코드를 사용해서 에러가 나지 않고 제대로 빌드되었다고 해봅시다. 
그러면 expanded 변수의 값이 변화될 때를 감지하여 해당 expanded 변수가 있는 Composable에 recomposition이 일어날 것입니다.</p>
<pre><code class="language-kotlin">@Composable
private fun Greeting(name: String) {
    var expanded = mutableStateOf(false)
    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = &quot;Hello, &quot;)
                Text(text = name)
            }
            ElevatedButton(
                onClick = {
                    expanded.value = !expanded.value
                    Log.d(&quot;is_expannded&quot;, expanded.value.toString())
                }
            ) {
                Text(text = if (expanded.value) &quot;Show less&quot; else &quot;Show more&quot;,
                    color = MaterialTheme.colorScheme.primary)
            }
        }
    }
}
</code></pre>
<p>즉 상태변화를 일으킨 버튼(ElevatedButton)이 속해있는 위의 Greeting 컴포넌트가 recomposition이 일어나면서 다시 처음부터 빌드가 될겁니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a878f712-9114-424f-80a3-409e5f846727/image.png" alt=""></p>
<p>위 그림처럼 Composition은 데이터와 뷰를 모두 초기화 하는 과정이고 Recomposition역시 데이터의 변경을 감지하여 Composable의 모든 초기화 과정이 다시 일어나게 됩니다.</p>
<p>다시 위의 코드를 보면 버튼을 눌러서 expanded 변수가 false -&gt; true로 토글되어 Recomposition이 일어나게 됩니다. 그러나 Composable이 다시 초기화가 되는 과정에서 expanded 변수 역시 다시 false로 초기화가 되버리기 때문에 상태를 저장할 수 없는 상태변수인 아이러니한 상황이 벌어지게 됩니다.</p>
<p>그래서 remember 키워드를 사용해야 합니다.</p>
<pre><code class="language-kotlin">@Composable
private fun Greeting(name: String) {
    var expanded = remember {mutableStateOf(false)}
    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = &quot;Hello, &quot;)
                Text(text = name)
            }
            ElevatedButton(
                onClick = {
                    expanded.value = !expanded.value
                    Log.d(&quot;is_expannded&quot;, expanded.value.toString())
                }
            ) {
                Text(text = if (expanded.value) &quot;Show less&quot; else &quot;Show more&quot;,
                    color = MaterialTheme.colorScheme.primary)
            }
        }
    }
}</code></pre>
<p>expanded 변수를 remember로 감싸 선언해주기만 하면 끝입니다.
remember가 하는 일은 recomposition에 의해 Composable의 초기화가 다시 수행될 때 remember 코드블럭 안의 내용은 실행되지 않도록 해줍니다.
즉 composition일 때에는 모든 코드가 동작하고, recomposition일 때에는 remember 안쪽의 코드를 제외하고 동작하게 함으로써 초기에만 데이터를 초기화 시키고 이후부터는 값을 유지할 수 있도록 하는 것입니다.</p>
<p>이제 제대로된 코드가 동작하겠네요!</p>
<p>정리해보면 아래와 같습니다.</p>
<blockquote>
</blockquote>
<ul>
<li>Composable의 라이프사이클은 가장 처음 초기화를 수행하는 Commposition과 업데이트가 일어나는 Recomposition으로 구성된다.</li>
<li>Recomposition이 일어나기 위해서는 MutableStateOf() 메서드를 통해 MutableState 객체를 만들어 상태를 관찰할 수 있도록 해야한다.</li>
<li>데이터가 매번 초기화되지 않고 recomposition이 일어날 때 값을 유지하기 위해서는 remember 키워드로 감싸줘야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jetpack Compose] View와 Compose 함께 사용하기]]></title>
            <link>https://velog.io/@renovatio_hyuns/Jetpack-Compose-View%EC%99%80-Compose-%ED%95%A8%EA%BB%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@renovatio_hyuns/Jetpack-Compose-View%EC%99%80-Compose-%ED%95%A8%EA%BB%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 31 Aug 2023 05:39:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 <a href="https://developer.android.com/jetpack/compose?hl=ko">Android developers&#39; Jetpack Compose 공식문서</a>를 참고하여 작성되었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/50d6a3c4-dea0-4669-a618-825db2eb469d/image.png" alt=""></p>
<h1 id="compose를-기존-프로젝트에-적용할-수는-없을까">Compose를 기존 프로젝트에 적용할 수는 없을까</h1>
<p>만약 이미 View xml을 활용해서 프로젝트를 진행중에 있다면 Compose를 도입할 수 없는것일까요?
Jetpack Compose는 뷰 상호운영성을 고려하여 설계되었으며 뷰 시스템과 공존할 수 있다고 개발진들이 강조하고 있습니다. 즉 이미 View 시스템으로 구축한 서비스를 Compose로 점진적으로 Migration할 수 있다는 뜻이며 만약 Compose가 익숙하지 않다면 View와 Compose를 함께 사용하여 프로젝트를 구성할 수 도 있다는 뜻입니다.</p>
<p>예를들면 기존의 기능은 View 시스템을 유지하는 대신 새로생기는 기능은 Compose로 개발할 수 있겠습니다.
또한 전체화면을 Compose로 빌드하지 않아도 한 화면 내에 View와 Compose가 공존하는 것도 가능합니다.</p>
<p>이번에는 View xml로 작성되어 있는 특정 화면을 Compose로 이전하며 어떻게 두 가지 시스템이 공존할 수 있는지 알아보도록 하겠습니다.</p>
<h1 id="초기상태-셋팅">초기상태 셋팅</h1>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/0176a55f-8d54-4730-90a2-b785823132dc/image.png" width="50%" height="50%">

<p>위와 같은 화면이 xml파일로 작성되어 있습니다. 이를 Compose로 점진적으로 migration하면서 어떻게 View와 Compose 시스템이 한 화면에 공존하게 되는지 알아보겠습니다.</p>
<p>먼저 프로젝트 안에서 Compose를 사용하려면 모듈 단위의 gradle 파일에 </p>
<pre><code>buildFeatures {
        //...
        compose true
    }

dependencies {
    def composeBom = platform(&#39;androidx.compose:compose-bom:2023.08.00&#39;)
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation &quot;androidx.compose.runtime:runtime&quot;
    implementation &quot;androidx.compose.ui:ui&quot;
    implementation &quot;androidx.compose.foundation:foundation&quot;
    implementation &quot;androidx.compose.foundation:foundation-layout&quot;
    implementation &quot;androidx.compose.material:material&quot;
    implementation &quot;androidx.compose.runtime:runtime-livedata&quot;
    implementation &quot;androidx.compose.ui:ui-tooling-preview&quot;
    debugImplementation &quot;androidx.compose.ui:ui-tooling&quot;
}</code></pre><p>관련 의존성들을 추가해주어야 합니다.
<code>androidx.compose:compose-bom:2023.08.00</code> 의 라이브러리는 <a href="https://developer.android.com/jetpack/compose/bom/bom?hl=ko">compose-bom</a> (bil of material)이라 하여 compose 관련한 라이브러리들의 모음입니다. 따라서 특정 버전의 compose를 쓰고싶으면 bom 으로 implement한 다음 특정 라이브러리만 다른 버전을 사용하고 싶을 때 개별적으로 implement 하여 버전관리가 가능합니다.</p>
<h1 id="특정-레이아웃을-compose로-대체">특정 레이아웃을 Compose로 대체</h1>
<pre><code class="language-xml">&lt;androidx.core.widget.NestedScrollView
            android:id=&quot;@+id/plant_detail_scrollview&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:clipToPadding=&quot;false&quot;
            android:paddingBottom=&quot;@dimen/fab_bottom_padding&quot;
            app:layout_behavior=&quot;@string/appbar_scrolling_view_behavior&quot;&gt;

            &lt;androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;match_parent&quot;
                android:layout_margin=&quot;@dimen/margin_normal&quot;&gt;

                &lt;TextView
                    android:id=&quot;@+id/plant_detail_name&quot;
                    .../&gt;

                &lt;TextView
                    android:id=&quot;@+id/plant_watering_header&quot;
                    .../&gt;

                &lt;TextView
                    android:id=&quot;@+id/plant_watering&quot;
                    .../&gt;

                &lt;TextView
                    android:id=&quot;@+id/plant_description&quot;
                    .... /&gt;

            &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;

        &lt;/androidx.core.widget.NestedScrollView&gt;</code></pre>
<p>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ffedc166-6ddc-490a-8c40-8d21a6ac72f2/image.png" width=50%/>
</p>

<p>위와 같이 정보를 텍스트로 표현하여 스크롤 가능하게 구현한 레이아웃을 Compose로 대체하려면 Layout 파일에서 View부분을 그리는 코드를 없애고 ComposeView를 넣어야 합니다.</p>
<pre><code class="language-xml">&lt;androidx.core.widget.NestedScrollView
            android:id=&quot;@+id/plant_detail_scrollview&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:clipToPadding=&quot;false&quot;
            android:paddingBottom=&quot;@dimen/fab_bottom_padding&quot;
            app:layout_behavior=&quot;@string/appbar_scrolling_view_behavior&quot;&gt;

    &lt;androidx.compose.ui.platform.ComposeView
        android:id=&quot;@+id/compose_view&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;/&gt;

&lt;/androidx.core.widget.NestedScrollView&gt;</code></pre>
<p>layout에 ComposeView를 넣었다면 @Coposable 객체를 생성하여 ComposableView에 setContent를 해줄 수 있습니다.
setContent안의 문법은 일반 Jetpack Compose를 활용할 때와 동일합니다.</p>
<pre><code class="language-kotlin">@Composable
fun PlantDetailDescription() {
    Surface {
        Text(&quot;Hello Compose&quot;)
    }
}

// plant_detail_fragment
val binding = DataBindingUtil.inflate&lt;FragmentPlantDetailBinding&gt;(
            inflater,
            R.layout.fragment_plant_detail,
            container,
            false
        ).apply {
composeView.setContent {
                // You&#39;re in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }</code></pre>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a6a207e6-4da7-4856-8989-05632b5bf70a/image.png" alt=""></p>
<p>ScrollView 안쪽의 레이아웃이 날아가고 ComposeView로 채워진 결과를 볼 수 있습니다. </p>
<p>그림으로 정리해보면 아래와 같은 형식인데
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/2094bf01-57d2-4b2a-89f9-bfd22e8c2060/image.png" alt=""></p>
<p><strong><em>순수 Compose만으로 구현할 때에는</em></strong>
layout xml 파일 자체를 작성할 필요가 없고 오로지 activity 코드 안에 setContent로 Compose UI를 채워넣으면 됩니다.</p>
<p><strong><em>만약 layout xml과 Compose를 함께 사용하고 싶다면</em></strong>
xml 파일 안에 ComposeView 컴포넌트를 추가하고 뷰바인딩을 진행할 때 해당 ComposeView에 setContent를 걸어주면 됩니다..</p>
<h1 id="동일한-layout의-형태로-완성하기">동일한 layout의 형태로 완성하기</h1>
<p>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/3c825124-c9aa-41fd-bc7a-edb79979b436/image.png" width=50%/>
</p>

<p>제목에 해당하는 TextView는 아래와 같이 동일한 형태의 composable UI로 치환할 수 있습니다.</p>
<pre><code class="language-kotlin">// layout xml
&lt;TextView
    android:id=&quot;@+id/plant_detail_name&quot;
    android:layout_width=&quot;0dp&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_marginStart=&quot;@dimen/margin_small&quot;
    android:layout_marginEnd=&quot;@dimen/margin_small&quot;
    android:gravity=&quot;center_horizontal&quot;
    android:text=&quot;@{viewModel.plant.name}&quot;
    android:textAppearance=&quot;?attr/textAppearanceHeadline5&quot; 
    app:layout_constraintEnd_toEndOf=&quot;parent&quot;
    app:layout_constraintStart_toStartOf=&quot;parent&quot;
    app:layout_constraintTop_toTopOf=&quot;parent&quot;
    tools:text=&quot;Apple&quot; /&gt;

// kotlin jetpack compose
@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName(&quot;Apple&quot;)
    }
}</code></pre>
<ul>
<li>textAppearance와 비슷한 형태를 가지는 머티리얼 테마의 typography.h5를 style 로 주었습니다.</li>
<li>marginStart와 marginEnd를 paddingHorizontal로 치환하였습니다.</li>
<li>constraintLayout의 종속성으로 정의한 텍스트뷰의 위치를 Alignment와 wrapContentWidth를 활용하여 동일하게 가운데정렬되도록 코딩하였습니다.</li>
<li>마지막으로 적절한 text가 Composable UI에 입력될 수 있도록 name:String을 변수로 받아 text로 삽입할 수 있도록 함수화 하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e7bc9b12-5187-4869-b83f-a83a2ab5804d/image.png" alt="">
이번엔 제목 아래의 물주는 기준에 대한 내용을 Compose로 작성해보겠습니다.</p>
<pre><code class="language-kotlin">// layout xml
&lt;TextView
     android:id=&quot;@+id/plant_watering_header&quot;
     android:layout_width=&quot;0dp&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:layout_marginStart=&quot;@dimen/margin_small&quot;
     android:layout_marginTop=&quot;@dimen/margin_normal&quot;
     android:layout_marginEnd=&quot;@dimen/margin_small&quot;
     android:gravity=&quot;center_horizontal&quot;
     android:text=&quot;@string/watering_needs_prefix&quot;
     android:textColor=&quot;?attr/colorAccent&quot;
     android:textStyle=&quot;bold&quot;
     app:layout_constraintEnd_toEndOf=&quot;parent&quot;
     app:layout_constraintStart_toStartOf=&quot;parent&quot;
     app:layout_constraintTop_toBottomOf=&quot;@id/plant_detail_name&quot; /&gt;

 &lt;TextView
     android:id=&quot;@+id/plant_watering&quot;
     android:layout_width=&quot;0dp&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:layout_marginStart=&quot;@dimen/margin_small&quot;
     android:layout_marginEnd=&quot;@dimen/margin_small&quot;
     android:gravity=&quot;center_horizontal&quot;
     app:layout_constraintEnd_toEndOf=&quot;parent&quot;
     app:layout_constraintStart_toStartOf=&quot;parent&quot;
     app:layout_constraintTop_toBottomOf=&quot;@id/plant_watering_header&quot;
     app:wateringText=&quot;@{viewModel.plant.wateringInterval}&quot;
     tools:text=&quot;every 7 days&quot;/&gt;


// kotlin jetpack compose
@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview(showBackground=true)
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}
</code></pre>
<p>뭔가 코드가 갑자기 긴게 나와서 당황스러울 순 있지만 뜯어보면 별거없습니다.</p>
<ul>
<li>Column으로 Text 2개를 위아래로 붙인 형태의 Composable UI입니다.</li>
<li>위쪽 Text에는 fontWeight로 bold형태의 적절한 폰트모양을 지정해주었고 color또한 지정해주었습니다.</li>
<li>각각의 Text에 위아래 패딩을 주어 xml layout을 margin과 같이 동작하도록 하였고 Alignment를 활용하여 레이아웃을 가운데정렬 해주었습니다.</li>
</ul>
<p>이런식으로 View안에 ComposeView를 넣고 해당 Composable UI를 구성하여 ComposeView에 setContent 하는 방식으로 View와 Compose를 동시에 사용할 수 있습니다.</p>
<h2 id="compose에서-지원하지-않는-view-대체">Compose에서 지원하지 않는 View 대체</h2>
<p>Compose는 현재 시점에서 어느정도 안정화된 버전이 배포되었지만, 아직까지 완전하게 View를 대체하지는 못하고 있습니다.
그렇다면 Compose에서 자체적으로 지원하지 않는 View를 그리기 위해서는 어떻게 해야할까요?</p>
<p>예를들어 아래와 같이 상세설명 부분의 경우 html로 이루어져 있다고 합시다.</p>
<p>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/cc0938e6-465a-4700-9f48-7c4bfdff3c12/image.png" width=50%/>
</p>

<pre><code class="language-xml">&lt;TextView
    android:id=&quot;@+id/plant_description&quot;
    style=&quot;?android:attr/textAppearanceMedium&quot;
    android:layout_width=&quot;0dp&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_marginStart=&quot;@dimen/margin_small&quot;
    android:layout_marginTop=&quot;@dimen/margin_small&quot;
    android:layout_marginEnd=&quot;@dimen/margin_small&quot;
    android:minHeight=&quot;@dimen/plant_description_min_height&quot;
    app:layout_constraintEnd_toEndOf=&quot;parent&quot;
    app:layout_constraintStart_toStartOf=&quot;parent&quot;
    app:layout_constraintTop_toBottomOf=&quot;@id/plant_watering&quot;
    app:renderHtml=&quot;@{viewModel.plant.description}&quot;
    tools:text=&quot;Details about the plant&quot; /&gt;</code></pre>
<p>위 코드처럼 renderHtml로 Html형식의 텍스트를 렌더링할 수 도 있고 혹은 TextView에 Html.fromHtml() 과 같은 함수로 Html을 String으로 렌더링할 수 있습니다.
그러나 Compose의 Text는 이러한 기능을 자체적으로 지원하고 있지 않다면 어떻게 해야 할까요?</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/99e0887c-f634-49da-baf3-9e6c94fc73d6/image.png" alt=""></p>
<p><del>(구글에서 기능 만들어줄 때 까지 벅벅 긁으면서 기다린다.)</del></p>
<p>그럴 때는 <a href="https://developer.android.com/jetpack/compose/migrate/interoperability-apis/views-in-compose?hl=ko">AndroidView</a>를 활용하여 구현할 수 있습니다. </p>
<pre><code class="language-kotlin">@Composable
@UiComposable
fun &lt;T : View&gt; AndroidView(
    factory: (Context) -&gt; T,
    modifier: Modifier = Modifier,
    update: (T) -&gt; Unit = NoOpUpdate
): Unit</code></pre>
<p>위와 같이 생긴 API가 AndroidView입니다. 주요 구성요소 세 가지가 있는데</p>
<ul>
<li>factory : view가 compose UI로 처음 렌더링 될 때 실행됩니다.</li>
<li>modifier : 일반적인 Compose UI에 사용되는 Modifier입니다.</li>
<li>update : 데이터가 변경되거나 하여 compose UI가 변경될 때 마다 실행됩니다.</li>
</ul>
<p>각각 위와 같은 역할을 하며 그에 맞게 함수를 정의해주어야 합니다.</p>
<pre><code class="language-kotlin">@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context -&gt;
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview(showBackground = true)
@Composable
private fun PlantDescriptionPreview() {
    Surface {
        PlantDescription(&quot;HTML&lt;br&gt;&lt;br&gt;description&quot;)
    }
}</code></pre>
<p>위 PlantDescription 컴포넌트를 보면 </p>
<ul>
<li>htmlDescription 변수에 HtmlCompat.fromHtml함수를 활용하여 주어진 문자열을 Html코드로 취급하여 Spanned 객체로 반환합니다.</li>
<li>factory를 구현하여 AndroidView가 초기화될 때 TextView를 생성하고 LinkMovementMethod를 활용하여 하이퍼링크를 클릭할 수 있도록 초기설정해줍니다.</li>
<li>update를 구현하여 TextView의 텍스트가 html에 의해 변환된 htmlDescription Spanned 객체로 렌더링 될 수 있도록 합니다.</li>
</ul>
<p><strong>Spanned란?</strong>
TextView의 Text에 String이 아니라 Spanned라는 객체가 들어가서 물음표가 머릿속에서 뜨신 분들이 있을것 같습니다.
쉽게말해 Spanned는 문자열의 subsequence에 하이퍼링크를 걸거나, 배경 혹은 글자색을 바꾸는 등의 꾸미는 작업이 가능한 문자열 객체를 의미합니다. 
자세한 내용은 <a href="https://developer.android.com/guide/topics/text/spans?hl=ko">Span 관련한 공식문서</a>를 첨부해놓겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/568fc93f-5b1a-409b-901f-4fa2e84f80e3/image.png" alt=""></p>
<p>위 코드를 Preview해보면 실제로 Html의 <code>&lt;br&gt;</code> 태그가 적용된 형태의 TextView가 생성된 것을 알 수 있습니다.</p>
<blockquote>
<p>정리하면 AndroidView란 Compose를 활용할 때 Compose가 자체적으로 지원하지 않는 기존의 뷰를 구현하기 위한 저수준의 통합 모듈이며 이를 구현하기 위하여</p>
</blockquote>
<ul>
<li>modifier : Compose UI 관련 꾸밈 속성</li>
<li>factory : 어떤 속성을 가진 어떤 View를 생성할지 선언 및 초기화</li>
<li>update : View에 어떤 데이터를 렌더링, 업데이트할 것인지 구현 <br>
위 세 가지를 구현하시면 됩니다.</li>
</ul>
<p>위에서 만든 모듈에 샘플 데이터 대신 실제 ViewModel에 있는 상세설명 데이터를 넣게 되면</p>
<pre><code class="language-kotlin">@Composable
fun PlantDetailContent(plant: Plant) {
    Surface{
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant(&quot;id&quot;, &quot;Apple&quot;, &quot;description&quot;, 3, 30, &quot;&quot;)
    MaterialTheme {
        PlantDetailContent(plant)
    }
}</code></pre>
<p>위와 같이 최종적으로 ComposeView에 렌더링 되는 모듈을 PlantDetailContent로 만들 수 있을 것입니다.
해당 모듈은 Plant라는 객체를 입력받아 각 객체에 있는 name, wateringInterval, description 정보들을 우리가 위에서 만들었던 모듈을 활용하여 하나의 ComposeUI로 만들어 줄 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a8995e48-7b5b-4b07-bc60-ab21dac6ac2b/image.png" alt=""></p>
<p>이제 프래그먼트에서 뷰모델을 연결하고, 뷰모델의 데이터를 아래와 같이 주입해주면</p>
<pre><code class="language-kotlin">private val plantDetailViewModel: PlantDetailViewModel by viewModels {
    InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
}

composeView.setContent {
    // You&#39;re in Compose world!
    MaterialTheme {
        PlantDetailDescription(plantDetailViewModel)
    }
}

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM&#39;s LiveData&lt;Plant&gt; field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}
</code></pre>
<p>와 같이 프래그먼트에서 코드를 작성할 수 있고
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/41604639-8d23-462f-ba7b-c01331555203/image.png" alt=""></p>
<p>위 사진과 같이 html 문법에 따라 하이퍼링크도 잘 동작하도록 구현된걸 확인할 수 있습니다.</p>
<p>이러한 과정을 통해 기존의 View System을 Compose로 이전할 수 있고 개발할 때에도 동시에 두 개의 UI 시스템을 구축할 수 있으므로 적절히 조합하여 사용하면 좋을것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 스레드와 병행성]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-%EB%B3%91%ED%96%89%EC%84%B1</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-%EB%B3%91%ED%96%89%EC%84%B1</guid>
            <pubDate>Sun, 27 Aug 2023 17:11:14 GMT</pubDate>
            <description><![CDATA[<h1 id="프로세스와-스레드">프로세스와 스레드?</h1>
<p>사용자가 컴퓨터를 사용하면서 하나의 작업만 수행하지는 않을 것입니다. 아니 사용자가 작업을 수행하지 않더라도 컴퓨터 백그라운드에서 돌아가는 응용프로그램들이 다수 있을 것입니다. 따라서 컴퓨터에서는 여러 응용프로그램이 병렬적으로 실행되어야 하며 운영체제는 이를 지원하기 위해 멀티 프로세싱 기술을 사용하여 여러 응용프로그램들이 동시에 실행되는것처럼 보이도록 작동합니다. 이 때 각 응용프로그램들의 실행단위를 <strong>프로세스</strong> 라고 부릅니다.</p>
<p>그렇다면 하나의 프로세스 즉 각각의 응용프로그램 역시 동시에 여러 일을 수행해야 하는 경우가 생기지 않을까요?
웹브라우저 크롬을 예로 생각해보면 화면을 그리는 동시에 네트워크와 통신하고, 노래를 듣고있다면 웹서핑과 동시에 노래또한 재생해야 할 것입니다.
따라서 프로세스 안에서도 병렬적으로 수행해야 하는 일이 생기고 이러한 프로세스에 속한 작업 수행 단위를 <strong>스레드</strong>라 부릅니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/a808c9ba-6cbf-4962-a989-0eebd0d3b4da/image.png" alt=""></p>
<p>멀티 프로세싱에서 운영체제는 여러 프로세스들을 CPU에 번갈아 할당하며 (Context Switching) 동시에 여러 응용프로그램이 실행되는 것처럼 보이도록 한다고 했습니다.
하지만 프로세스 내에 더 작은 실행단위인 스레드가 존재하기 때문에 더 나아가 각각의 스레드가 CPU이용의 기본 단위가 됩니다.</p>
<p>즉 각각의 스레드마다 고유한 레지스터, 스택, 프로그램 카운터를 가지며 CPU를 점유하게 됩니다.</p>
<h2 id="멀티스레딩의-장점">멀티스레딩의 장점</h2>
<p>멀티 스레드를 사용하게 되었을 때 장점은 아래와 같습니다.</p>
<ul>
<li><em><strong>응답성</strong></em> : 병렬적으로 작업이 실행되기 때문에 응답이 오래걸리는 작업을 수행할 때에도 사용자는 마냥 응답을 기다리는 것이 아니라 다른 작업을 수행할 수 있습니다.</li>
<li><em><strong>자원 공유</strong></em> : 멀티 프로세스 문제의 경우 자원을 공유하기 위해 고융메모리를 할당하거나 메시지 전달 기법을 사용하는 등의 작업을 수행해야 했지만 멀티 스레드의 경우 속해있는 프로세스의 메모리와 자원을 공유하기 때문에 자원공유에 있어 편리하고 효율이 좋습니다.</li>
<li><em><strong>경제성</strong></em> : 스레드 생성은 프로세스 생성보다 시간과 자원이 절약됩니다. 이는 스레드가 속해있는 프로세스의 메모리를 공유하기 때문임과 연관있습니다.</li>
<li><em><strong>확장성</strong></em> : 각각의 스레드는 다른 처리기에서도 병렬로 수행될 수 있기 때문에 다중 처리기 구조에서 이점이 더욱 더 증가됩니다.</li>
</ul>
<h2 id="멀티스레드-프로그래밍의-과제">멀티스레드 프로그래밍의 과제</h2>
<p>멀티코어 시스템에서 멀티스레딩 프로그래밍을 하기위해서는 몇가지 어려운 사항이 있고 이를 고려하여 설계해야 합니다.</p>
<ul>
<li><em><strong>태스크 인식</strong></em> : 프로세스의 진행과정에 따라 병행가능한 태스크의 성격이 다릅니다. 따라서 개별 코어에서 병렬실행될 수 있는 태스크를 나누는 과정이 필요합니다.</li>
<li><em><strong>균형</strong></em> : 전체 프로세스 작업에 균등한 기여도를 가지도록 태스크를 나누는 작업이 필요합니다. 만약 하나의 기여도가 적은 태스크가 먼저 끝나서 코어가 놀고 있다면 비효율적인 멀티스레딩이 될 것입니다.</li>
<li><em><strong>데이터 분리</strong></em> : 태스크가 접근하여 조작하는 데이터들은 개별코어에서 사용할 수 있도록 나누어져야 합니다.</li>
<li><em><strong>데이터 종속성</strong></em> : 태스크가 접근하는 데이터에 대해서는 둘 이상의 태스크 사이에 종속성이 없어야 합니다.</li>
<li><em><strong>시험 및 디버깅</strong></em> : 다중코어에서 스레드가 병렬적으로 실행되고 있으면 단일 스레드 응용프로그램을 디버깅하는 것보다 훨씬 어려울 수 있습니다. 특정 스레드에 대해 break point를 잡아서 디버깅하고자 해도 다른 스레드가 얼마나 진행되었는지 알 수 없기 때문입니다.</li>
</ul>
<h2 id="amdahls-rule">Amdahl&#39;s Rule</h2>
<blockquote>
<p>코어는 무조건 많을수록 좋은가?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/7599ca7e-80f2-417e-8d12-7a0f68c1b477/image.png" alt=""></p>
<p>일반적으로 코어가 N개이면 N개의 스레드가 병렬적으로 돌아가기 때문에 처리속도가 N배 빨라진다고 생각할 수 있습니다.
그러나 프로세스의 동작과정에서 필수적으로 Serial하게 동작해야하는 구성요소가 있을 수 있고 이런 경우 멀티스레딩의 이점을 살릴 수 없기에 무조건적으로 N배 빨라진다고 볼 수 없습니다.</p>
<ul>
<li>코어가 N개 있고</li>
<li>Serial하게 실행되어야 하는 순차 실행 구성요소가 S% 차지한다고 할 때 </li>
</ul>
<p>실행 속도의 증가량(speedup)은 
$$
speedup \leq \frac{1}{S+\frac{(1-S)}{N}}
$$</p>
<p>와 같이 수식적으로 정의된다는 법칙입니다.
즉 코어의 갯수(N)이 무한대로 증가해도 결국 순차실행 구성요소가 차지하는 비율에 따라 $\frac{1}{S}$ 로 수렴하기 때문에 <em><strong>암달의 저주</strong></em> 라고도 불립니다.</p>
<h1 id="멀티스레딩-모델">멀티스레딩 모델</h1>
<p>스레드에는 두가지 종류가 있습니다.</p>
<ul>
<li>유저 스레드 : 사용자 수준에서 제공되는 스레드로써 커널의 지원없이 커널 위에서 관리됩니다. 자바에서 프로그래밍적으로 사용할 수 있는 스레드를 예시로 들 수 있겠습니다.</li>
<li>커널 스레드 : 운영체제에 의해 직접 관리되고 지원되는 스레드로써 CPU에 직접적으로 할당되는 스레드입니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/3e44e97b-8081-49d9-9a8a-55e668dfb0a9/image.png" alt=""></p>
<p>위 그림과 같이 유저 스레드와 커널 스레드는 각각 다른 영역에서 동작하게 되는데, 실제로 컴퓨터에서 프로세스 내의 스레드를 돌리려면 유저스레드와 커널스레드간의 연관 관계가 궁극적으로 존재해야 하며 이러한 연관 관계를 정의하는 방법이 여러가지 존재합니다.</p>
<h2 id="다대일-모델">다대일 모델</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/41459645-6485-43eb-9a36-5ec58572f1d8/image.png" alt=""></p>
<p>많은 유저 스레드를 하나의 커널 스레드로 사상합니다. 이렇게 하면 사용자 공간의 스레드 라이브러리에 의해 유저스레드만 관리하면 되기 때문에 구현이 편하고 효율적이라 할 수 있습니다. 그러나 멀티코어 시스템이 대부분의 컴퓨터 시스템에서 표준이 되고 이러한 다대일 모델은 멀티코어의 이점을 살릴 수 없기 때문에 현재에는 거의 존재하지 않는 형태의 모델입니다.</p>
<h2 id="일대일-모델">일대일 모델</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/6cd651df-e4d7-48f3-9f77-46da2fdd7b61/image.png" alt=""></p>
<p>각 사용자 스레드를 각각 하나의 커널 스레드로 사상합니다. 따라서 다대일 모델보다 많은 병렬성을 제공하여 여러 스레드가 동시에 실행될 수 있지만, 사용자 스레드를 만드려면 그에 사상되는 커널 스레드 또한 만들어야 하므로 매우 많은 수의 커널 스레드가 시스템 성능에 부담을 줄 수 있다는 단점이 있습니다.</p>
<h2 id="다대다-모델">다대다 모델</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/21e8b36f-55df-4148-9b60-38e3653c6f2f/image.png" alt=""></p>
<p>위 두 모델의 장단점을 살펴보면, 다대일 모델은 많은 수의 유저스레드를 생성할 수 있지만 완벽한 병렬성을 획득할 수 없고, 일대일 모델은 더 많은 병행 실행을 제공하지만 많은 수의 유저 스레드들을 생성할 수 없다는 단점이 있습니다.</p>
<p>따라서 다대다 모델은 여러개의 유저 스레드들을 그보다 적거나 같은 수의 커널스레드에 사상할 수 있도록 합니다. 이러한 모델은 융통성있게 구현한 모델이긴 하지만 실제로 구현하는데 어려움이 많고 현대에 처리코어 수가 늘어남에 따라 커널스레드 수를 제한하는 것이 큰 의미가 없어져 일대일 모델로 구현하는 경우가 많아졌습니다.</p>
<h1 id="암묵적-스레딩-implicit-threading">암묵적 스레딩 (Implicit Threading)</h1>
<p>동시에 병렬적으로 동작하는 응용프로그램을 작성하는 것은 곧 멀티 코어 시스템에서 멀티 스레딩을 디자인하는 작업과 동일합니다. 이는 개발자들에게 어려운 과제이기 때문에 스레드의 생성과 관리 책임을 개발자로부터 컴파일러와 런타임 라이브러리에게 넘겨줌으로써 극복할 수 있습니다.</p>
<p>이러한 방법을 <strong>암묵적 스레딩</strong>이라 부르며 암묵적 스레딩을 이용하여 멀티코어 응용프로그램을 설계할 수 있는 몇가지 접근법이 존재합니다.</p>
<h2 id="스레드-풀">스레드 풀</h2>
<p>스레드는 필요한 시기에 생성되어 작업이 끝나면 파괴됩니다. 만약 매번 호출이 일어날 때 마다 스레드를 생성하게 된다면 분명 프로세스를 생성하는것보단 매우 효율적일지 몰라도 호출이 빈번하다면 스레드를 생성하는데 드는 시간과 비용이 늘어나 응용프로그램의 성능이 저하될 수 있습니다.
따라서 프로세스를 시작할 때 아예 일정한 다수의 스레드를 생성해두고 응용프로그램이 요청을 받으면 스레드 풀에 해당 요청을 전달합니다. 
 풀에 사용가능한 스레드가 있으면 즉시 요청을 수행하고 결과를 반환하며 만약 사용가능한 스레드가 없다면 사용 가능한 스레드가 생길 때 까지 작업이 대기됩니다.</p>
<p> 이렇게 한다면 </p>
<ul>
<li>매번 스레드를 생서하는것 보다 속도가 빠르고</li>
<li>스레드 개수에 제한을 둠으로써 많은 수의 스레드를 병렬처리할 수 있는 시스템에 도움이 되며</li>
<li>태스크로부터 생성하는 방법을 분리하여 태스크 실행을 다르게 할 수 있다는 </li>
</ul>
<p>장점이 있습니다.</p>
<h2 id="fork-join">Fork Join</h2>
<p>부모스레드가 하나 이상의 자식 스레드를 생성 (Fork)한 다음 자식 스레드가 종료되면 join하고 그 시점부터 자식 스레드의 결과를 확인하고 결합할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e35637a4-d1e0-4315-b32d-f1676e6afe93/image.png" alt=""></p>
<blockquote>
<p>Java에서 fork-join 하는 pseudo code는 아래와 같습니다.</p>
</blockquote>
<pre><code class="language-java">Task(problem)
    if proble is small enough
        solve the proble directly
    else
        subtask1 = fork(new Task(subset of problem))
        subtask2 = fork(new Task(subset of problem))

        result1 = join(subtask1)
        result2 = join(subtask2)

        return combined results</code></pre>
<h2 id="openmp">openMP</h2>
<p><a href="https://www.openmp.org/">https://www.openmp.org/</a>
C/C++로 작서된, 공유 메모리 환경에서 병렬 프로그래밍을 할 수 있도록 도움을 주는 API입니다.
openMP는 병렬 영역을 선언하여 해당 병렬 영역에 컴파일러 디렉티브를 삽입할 수 있습니다. openMP가 <code>#pragma omp parallel</code> 을 만나게 되면 해당 블럭 안에 있는 연산을 디렉티브에 따라 생성된 스레드 들에 분배합니다. </p>
<pre><code class="language-C">#inlcude &lt;stdio.h&gt;
#include &lt;omp.h&gt;

#define SIZE 10000000

int a[SIZE], b[SIZE], c[SIZE]

int main(int argc, char *argv[])
{
    int i;
    for(i=0; i &lt; SIZE; i++){
        a[i] = b[i] = i;
    }

    #pragma omp parallel for
    for (i=0; i&lt;SIZE; i++){
        c[i] = a[i] + b[i];
    }

    return 0;
}</code></pre>
<p>와 같이 openMP를 사용하면 <code>#pragma omp parallel</code> 로 병렬 영역을 선언하여 오래 걸리는 연산을 빠르게 수행할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jetpack Compose] Compose란 무엇일까]]></title>
            <link>https://velog.io/@renovatio_hyuns/Jetpack-Compose-Compose%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@renovatio_hyuns/Jetpack-Compose-Compose%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Tue, 22 Aug 2023 16:39:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 <a href="https://developer.android.com/jetpack/compose?hl=ko">Android developers&#39; Jetpack Compose 공식문서</a>를 참고하여 작성되었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ae7274a4-3273-4ff2-a75c-e27b13a05eec/image.png" alt=""></p>
<h1 id="jetpack-compose">Jetpack? Compose?</h1>
<p>먼저 Compose에 대해 논하기 전에 Jetpack에 대해 알아야 합니다. 우리가 안드로이드 개발을 한다는 것은 보편적으로 Android OS 내에서 동작하는 응용프로그램을 개발하는 행위를 의미합니다. 응용프로그램을 만들기 위해 우리가 모든것을 다 코딩할 수 는 없겠죠? 마치 소고기를 먹기위해 소를 키우는일부터 시작하지 않고 마트에 가서 고기를 사오는 것처럼 안드로이드에서도 응용프로그램을 개발하기 위해 유용한 툴과 라이브러리들을 제공합니다.</p>
<p>구세대에는 이를 _<strong>Support Library</strong>_라고 불렀으며 현세대에 좀 더 편리하고 빠른 강력한 도구들로 리뉴얼한 라이브러리 모음을 _<strong>Jetpack</strong>_이라 부르는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e57fc170-fc4c-40a8-a6f0-dc9e9f4e79a6/image.png" alt=""></p>
<p>Android Jetpack은 androidx.* 형태로 패키징되어 제공됩니다. 또한 android support library는 28.0.0 버전 이후로 더이상 업데이트 되지 않으므로 새로운 기능을 개발할 때에는 androidx 패키지를 활용하여 개발해야 할 것입니다.</p>
<pre><code class="language-kotlin">// com.android.support.~~~ 로 시작하는 패키지들이 support library입니다.
dependencies {
        implementation &quot;com.android.support:support-v4:$libraryVersion&quot;
        implementation &quot;com.android.support:appcompat-v7:$libraryVersion&quot;
        implementation &quot;com.android.support:design:$libraryVersion&quot;
}

// androidx.~~~ 로 시작하는 패키지들이 jetpack library입니다.
// 이전의 support library들은 androidx.legacy에 포함되기 때문에 모두 사용가능합니다.
dependencies {
        implementation &#39;androidx.legacy:legacy-support-v4:1.0.0&#39;
        implementation &#39;androidx.appcompat:appcompat:1.0.0&#39;
        implementation &#39;com.google.android.material:material:1.0.0&#39;
        implementation &#39;androidx.recyclerview:recyclerview:1.0.0&#39;</code></pre>
<h2 id="jetpack의-구성요소">Jetpack의 구성요소</h2>
<p>Jetpack라이브러리는 통합적으로 안드로이드 응용프로그램을 개발하기 위한 도구들이 지원되므로 매우 많은 라이브러리가 포함되기 때문에 이곳에서 전부 설명할 수 없습니다.
그래도 가장 많이 사용되고 대표적인 것들을 몇가지 뽑아보자면 아래와 같습니다.</p>
<ul>
<li><p><strong>LiveData</strong>: 데이터의 변화를 관찰하고 알림을 받을 수 있는 라이브 데이터 객체를 제공합니다. 이를 통해 UI와 데이터 사이의 연결을 쉽게 관리할 수 있습니다.</p>
</li>
<li><p><strong>ViewModel</strong>: UI 관련 데이터를 관리하고, 구성 변경과 같은 수명 주기 이벤트에 대해 데이터를 보존할 수 있도록 돕습니다.</p>
</li>
<li><p><strong>Room</strong>: SQLite 데이터베이스를 사용하여 로컬 데이터를 저장하고 관리하는 데 도움을 주는 라이브러리입니다.</p>
</li>
<li><p><strong>Navigation</strong>: 앱 내 탐색을 관리하고 사용자 이동 경로를 정의하는 데 도움을 줍니다.</p>
</li>
<li><p><strong>Paging</strong>: 대용량 데이터 집합을 로드하고 표시하는 데 사용되는 라이브러리입니다.</p>
</li>
<li><p><strong>WorkManager</strong>: 비동기 작업을 관리하고 예약하는 데 사용되며, 예를 들어 백그라운드에서 데이터 동기화나 작업 예약에 유용합니다.</p>
</li>
<li><p><strong>Data Binding</strong>: UI 컴포넌트와 데이터 모델을 연결하여 코드 중복을 줄이고 앱의 유지 보수성을 향상시키는 데 도움을 줍니다.</p>
</li>
</ul>
<p>LiveData ViewModel Room같은 것들은 이미 개발할 때 종종 사용해오던 것들입니다. 나도 모르게 Jetpack library들을 자연스럽게 사용하고 있었던것 같은데 최근들어 Jetpack에서 새로운 문제를 개선하기 위한 업데이트가 일어나고 있습니다.</p>
<p>그것은 바로 UI 렌더링 방식의 변경점인데요, Jetpack Compose가 바로 그 주인공입니다.</p>
<h2 id="compose란-무엇일까">Compose란 무엇일까</h2>
<p>Jetpack에서 UI작업을 개선하기 위하여 나온 라이브러리가 Compose입니다.</p>
<p>기존의 UI 렌더링 작업은 xml 파일로 구조를 정의하고 코틀린 코드에서 xml 파일을 바인딩시켜 하나하나 기능을 구현하는 방식을 사용했습니다. 즉, UI 디자인과 코드가 분리된 형태였으며 UI 의 구조가 복잡해질 수록 코드량이 늘어나고 가독성이 떨어지는 단점이 존재했습니다.</p>
<p>Compose를 사용한다면 코드량이 훨씬 줄어들 뿐더러 UI적인 요소를 모두 kotlin코드로 통합할 수 있습니다. 또한 머티리얼디자인을 유연하게 구현할 수 있으며 애니메이션을 쉽고 빠르게 적용할 수 있는 장점이 있다고 하는데, 공식문서에 의하면 Compose의 특징은 아래와 같습니다.</p>
<ul>
<li><p><strong>선언적 UI</strong> : Compose는 선언적인 방식으로 UI를 정의하기 때문에, 어떻게 보일지를 명시적으로 정의하기 쉽습니다. UI를 kotlin으로 설명만 하면 compose에서 알아서 처리하므로 UI를 직관적이고 빠르게 개발하고 변경할 수 있습니다.</p>
</li>
<li><p><strong>재사용성</strong> : Compose에서는 컴포넌트를 재사용하기 쉽게 만들 수 있습니다. 작은 컴포넌트를 조합하여 복잡한 UI를 구축할 수 있습니다.</p>
</li>
<li><p><strong>코드 길이 감소</strong> : Compose는 코드의 길이를 줄여주고, 개발자가 더 적은 타이핑을 할 수 있도록 합니다.</p>
</li>
</ul>
<h1 id="compose-찍먹-해보기">Compose 찍먹 해보기</h1>
<p>본격적으로 컨텐츠를 시작하기 전에 간단하게 Compose가 어떤식으로 사용되는지 체험만 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5065ea1d-f0f7-4afe-b8a2-93054234d093/image.png" alt=""></p>
<p>Compose에 맞춰 기본 틀이 구성되어 있는 Empty Compose Activity로 프로젝트를 생성합니다.
(글 작성일 기준 Material3 디자인이 Preview로 풀려있는데 머티리얼 디자인의 트렌드도 변화하고 있는것 같습니다. 조만간 한번 다뤄봐야겠네요 😉)</p>
<p>위 방식으로 프로젝트 생성하면 아래와 같은 코드를 만들어줍니다.</p>
<pre><code class="language-kotlin">class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Compose_practiceTheme {
                // A surface container using the &#39;background&#39; color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting(&quot;Android&quot;)
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello $name!&quot;)
}</code></pre>
<p>setContent함수는 UI를 그리는 함수인것 같고 Surface는 background를 정의하는 컨테이너라고 주석으로 작성되어 있네요. Surface함수 안에 Greeting 함수를 보면 @Composable어노테이션이 달린 것을 볼 수 있습니다.
@Composable 어노테이션을 사용하면 Compose에서 UI를 그리는데 활용하는 함수로써 동작하게 됩니다.</p>
<p>SetContent안에 있는 Compose_practiceTheme이 무엇인지 찾아보니 
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b57b2334-b201-492b-b917-c1389aa3a91d/image.png" alt="">
프로젝트 구조가 왼쪽과 같이 되어있고 기본적으로 ui/theme 디렉토리가 생성되어 있었습니다.
프로젝트명이 Compose_practice 이므로 커스텀 테마가 자동으로 생성되어 있는걸 확인할 수 있었어요. 이외에도 color나 shape에 대해 각종 값들이 정의되어 있는데 이전에 xml방식에서는 res 폴더 안에 xml로 정의되었던 것들이 코틀린 코드로 정의되어 사용된다는 차이점이 있는것 같습니다!</p>
<p>무엇보다 가장 신기했던건 </p>
<pre><code class="language-kotlin">@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Compose_practiceTheme {
        Greeting(&quot;kingkingasdf 아랄랄랄ㄹ라&quot;)
    }
}</code></pre>
<p>처럼 @Preview 어노테이션을 달고 화면구성함수를 짜게 되면 Run configuration에서 preview로 화면을 빌드할 수 있는데 코드를 변경할 때 마다 실시간으로 휴대폰화면에 변화가 반영되는것이 너무 좋았습니다.</p>
<p>실시간으로 Preview로 화면을 보면서 UI 구성을 빠르게 할 수 있다는게 정말 큰 장점으로 다가왔어요. (xml로 했을 때는 화면 렌더링이 느려서 속터졌던 적이 한두번이 아니었던거 같은데;;)</p>
<p>이제부터 Preview를 기준으로 레이아웃을 구성해보겠습니다.</p>
<h2 id="레이아웃-구성">레이아웃 구성</h2>
<pre><code class="language-kotlin">data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
    Text(text = msg.author)
    Text(text = msg.body)
}


@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Compose_practiceTheme {
        MessageCard(
            msg = Message(&quot;Colleague&quot;, &quot;Hey, take a look at Jetpack Compose, it&#39;s great!&quot;)
        )
    }
}</code></pre>
<p>MessageCard함수로 Text 뷰를 2개 띄운 예제입니다. 기존 xml에서는 기본적인 구조가 정의되기 때문에 자동으로 TextView가 서로 겹치지 않고 바로옆에 붙어서 출력됐지만 Compose에서는 구조가 정의되지 않아서 Text 뷰가 겹쳐 나오게 됩니다.!</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/7dc90227-193f-4d4e-a1cd-37879042b2c1/image.png" alt=""></p>
<p>따라서 Compose에서 레이아웃을 구성하기 위해서는 구성요소간 구조를 Column과 Row로 정의해줄 필요가 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun MessageCard(msg: Message) {
    Column{
        Text(text = msg.author)
        Text(text = msg.body)
    }
}</code></pre>
<p>Coposable 부분을 Column으로 감싸주기만 하면 세로로 Text 뷰가 정렬되어 나오게 됩니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/18fa73cf-ab07-4d49-a612-f41dc27a0f2a/image.png" alt=""></p>
<h2 id="modifier">Modifier</h2>
<p>Column과 Row를 사용하여 조금 더 복잡한 레이아웃을 만들어보겠습니다.</p>
<pre><code class="language-kotlin">@Composable
fun MessageCard(msg: Message) {
    Row{
        Image(
            painter = painterResource(R.drawable.ic_launcher_background),
            contentDescription = &quot;Contact profile picture&quot;,
        )

        Column{
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/265c3012-7170-4fd7-acb3-6704579613d1/image.png" alt=""></p>
<p>Row와 Column을 계층적으로 쌓아 마치 LinearLayout 2 개를 중첩시킨 것과 같은 레이아웃을 만들 수 있습니다.</p>
<p>이제 Text나 Image를 좀 더 커스텀할 수 있는 방법을 알아보겠습니다.
이미지의 크기나 모양을 바꾸고싶다면 <a href="https://developer.android.com/jetpack/compose/modifiers?hl=ko">Modifier</a>라는 수정자 클래스를 사용해야 합니다.</p>
<pre><code class="language-kotlin">@Composable
fun MessageCard(msg: Message) {
    Row{
        Image(
            painter = painterResource(R.drawable.darong),
            contentDescription = &quot;Contact profile picture&quot;,
            modifier = Modifier
                // Set image size to 40 dp
                .size(40.dp)
                // Clip image to be shaped as a circle
                .clip(CircleShape)
        )

        // Add a horizontal space between the image and the column
        Spacer(modifier = Modifier.width(8.dp))

        Column{
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/28425c46-1897-4f26-b520-66436b42441d/image.png" alt=""></p>
<p>Modifier를 사용하면 동작을 정의하거나 모양을 바꾸는 등 다양한 일을 할 수 있는데요 이는 마치 xml디자인을 할 때 <code>&lt;ImageView&gt;</code> 태그 안에 width height padding 과 같은 다양한 속성을 지정해주는 작업과 유사합니다.</p>
<p>Modifier에는 더욱 더 많은 기능이 있는데</p>
<ul>
<li>애니메이션</li>
<li>클릭이벤트</li>
<li>정렬</li>
<li>패딩</li>
<li>그래픽 그리기</li>
</ul>
<p>등의 기능이 있으므로 <a href="https://developer.android.com/jetpack/compose/modifiers-list?hl=ko">공식문서</a>를 참고하여 다양한 기능을 구성하는데 활용할 수 있습니다.</p>
<p>Spacer의 경우 공간을 띄워주는 역할을 합니다.</p>
<p>이상으로 Compose가 무엇이고 어떻게 동작하는지 간단하게 알아보았습니다. 더 디테일하고 다양한 기능을 가지는 레이아웃을 그리려면 Modifier나 다양한 함수와 클래스에 대해 더 알아야 하므로 앞으로 천천히 하나씩 포스팅 해나갈 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최장 증가 수열 (LIS)]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%B5%9C%EC%9E%A5-%EC%A6%9D%EA%B0%80-%EC%88%98%EC%97%B4-LIS</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%B5%9C%EC%9E%A5-%EC%A6%9D%EA%B0%80-%EC%88%98%EC%97%B4-LIS</guid>
            <pubDate>Mon, 21 Aug 2023 12:11:32 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황-예시">문제 상황 예시</h1>
<p>먼저 이 알고리즘을 알게된 문제부터 소개하겠습니다.
<a href="https://www.acmicpc.net/problem/2631">https://www.acmicpc.net/problem/2631</a>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/772f9aec-4fbe-470b-952a-a66fb09cb7c5/image.png" alt="">
이렇게 생긴 문제인데, DP 문제로 분류되어 있어서 메모이제이션에 집중을 하고 생각해 봤는데, 진짜진짜 도저히 생각이 안나서 힌트만 얻고자 조금의 검색을 해본 결과, 최장 증가 수열이라는 것에 대해 알게 되었습니다.</p>
<h1 id="최장-증가-수열이란">최장 증가 수열이란</h1>
<p>이 최장 증가 수열 (Longest Increasing Subsequence 줄여서 LIS) 이란 주어진 배열 내에서 부분집합을 뽑았을 때, 가능한 모든 오름차순으로 배열되어 있는 부분집합 중에서 길이가 가장 긴 부분집합을 의미합니다.
즉 위 문제에서 주어진 [3, 7, 5, 2, 6, 1, 4] 배열을 기준으로 보면</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/4f33fbb9-353e-4711-a61f-65b00edf5a49/image.png" alt=""></p>
<p>위 사진과 같이 오름차순 수열을 이루는 부분집합들을 여러개 뽑을 수 있습니다.
그중 가장 길이가 긴 <strong>최장 증가 수열</strong>은 [3, 5, 6] 이 되겠네요.</p>
<p>이 최장 증가수열이라는 것을 알게되면 뒤 문제를 어떻게 해석할 수 있을까요?
결국에는 주어진 배열을 가장 적은 이동횟수로 정렬하는 문제이기 때문에, 이미 정렬되어 있는 부분만 빼고 나머지를 건드려서 정렬시켜주면 됩니다. 즉 &#39;가장 잘 정렬되어 있는 상태를 유지하면서 나머지 부분을 지금 상태에 맞게 맞추어주기만 하면&#39; 된다는 겁니다. 
그러려면 가장 길게 정렬되어 있는 최장 증가 수열을 구하고 그 외의 원소를 정렬하면 되는데, 이 문제는 정렬하는데 필요한 이동횟수를 구하는 것이므로 최장 증가 수열의 길이만큼을 총 배열에서 빼면 됩니다. 나머지 원소를 한번씩 움직여서 순서를 맞춰주면 되니까요.
즉 최장 증가 수열의 길이를 구하는 문제와 완벽하게 똑같은 문제입니다.
<a href="https://www.acmicpc.net/problem/11053">https://www.acmicpc.net/problem/11053</a></p>
<h1 id="lis의-길이를-구해봅시다">LIS의 길이를 구해봅시다</h1>
<p>그렇다면 최장 증가 수열(LIS)의 길이는 어떻게 구할 수 있을까요?</p>
<ul>
<li>Dynamic Programming : $O(N^2)$</li>
<li>Binary Search : $O(n \log n)$</li>
</ul>
<p>두 가지 방법이 존재하는데 위의 예제 배열으로 두 가지 방법을 살펴보겠습니다. (코드는 파이썬 코드입니다.)</p>
<h2 id="dp를-이용한-방법">DP를 이용한 방법</h2>
<pre><code class="language-python">N = int(sys.stdin.readline())
children = [3, 7, 5, 2, 6, 1, 4]
dp = [1] * N
for i, child in enumerate(children):
    for j in range(0, i):
        if children[j] &lt; child and dp[i] &lt; dp[j] + 1:
            dp[i] = dp[j] + 1

print(children)        # [3, 7, 5, 2, 6, 1, 4]
print(dp)            # [1, 2, 2, 1, 3, 1, 2]
print(max(dp))        # 4</code></pre>
<p>dp 배열에는 해당 인덱스까지 오름차순으로 정렬될 수 있는 가장 긴 길이를 memoization합니다.</p>
<ul>
<li>최장 증가 수열 부분집합을 찾지 못해도 본인 자신의 부분집합은 존재하므로 dp배열을 1로 초기화합니다.</li>
<li>for문으로 주어진 children 배열을 처음부터 끝까지 돌면서</li>
<li>각각의 인덱스에 대해 이전의 모든 값들을 순서대로 검사합니다.</li>
<li>만약 현재 인덱스 (i)의 값보다 더 작은 이전의 인덱스(j) 값을 찾았다면 현재 위치 (i)의 증가 수열 길이보다 이전 인덱스 (j)로부터 부분집합을 만들었을 때 증가 수열 길이가 더 크다면 dp를 업데이트합니다.</li>
<li>이렇게 하면 최종적으로 dp에는 각 인덱스 까지 부분집합을 구성했을 때 가능한 증가 수열 중 가장 긴 길이가 저장됩니다.</li>
<li>dp배열의 max값을 출력합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/1ed35fe2-657d-4ceb-8e3c-0ac76cc49a1a/image.png" alt="">
dp 배열이 어떻게 나오는지 시각화 해보면 위 그림과 같습니다.
각각 인덱스에 해당하는 인수를 포함하는 부분집합 중 가장 긴 증가수열의 길이가 dp에 저장되는 방식으로 구현하고 dp에서 가장 긴 길이가 최장 증가 수열의 길이가 되는 아이디어입니다.</p>
<p>for문이 2중으로 구성되므로 $O(N^2)$의 시간복잡도를 가집니다.</p>
<h2 id="이분탐색을-이용한-방법">이분탐색을 이용한 방법</h2>
<pre><code class="language-python">def binary_search(array, start, end, tar):
    while start &lt;= end:
        mid = (start + end) // 2
        if array[mid] &lt; tar:
            start = mid + 1
        elif array[mid] &gt; tar:
            end = mid
            if start == end:
                break
        else:
            break
    return mid

N = int(sys.stdin.readline())
children = [3, 7, 5, 2, 6, 1, 4]
lis = [0] * N
length = 0
for i, child in enumerate(children):
    if length == 0:
        lis[0] = child
        length += 1
    else:
        idx = binary_search(lis, 0, length, child)
        lis[idx] = child
        if idx == length:
            length += 1
print(lis)        # [1, 4, 6, 0, 0, 0, 0]
print(length)    # 3</code></pre>
<p>LIS의 길이를 구하는데에는 이분탐색을 사용할 수 도 있습니다.
lis배열을 추가로 생성하여 증가수열을 기록해 나가도록 합니다. 이 때 주어진 children 배열의 모든 원소를 순차적으로 탐색하며 lis 배열을 업데이트하게 됩니다.
최종적으로 완성된 lis배열의 길이가 최장 증가 수열이 길이가 되는 원리이며 이 때 증가수열을 기록해 나가는 과정을 최적화하기 위해 이분탐색을 사용하게 됩니다.</p>
<p>lis배열을 업데이트 해나가는 과정은 아래와 같습니다.</p>
<ul>
<li>children에서 원소를 하나 꺼냅니다.</li>
<li>꺼낸 원소가 lis 배열에 오름차순에 따라 들어갈 수 있는 위치를 이분탐색으로 찾습니다.</li>
<li>만약 꺼낸 원소가 가장 큰 수라면 lis 배열의 마지막에 추가합니다.</li>
<li>그렇지 않다면 꺼낸 원소보다 바로 다음으로 큰 lis 배열 내의 수를 대체합니다.</li>
<li>children의 모든 원소를 꺼낼때 까지 반복합니다.</li>
</ul>
<p>주어진 children = [3, 7, 5, 2, 6, 1, 4] 배열을 가지고 lis 배열이 생성되는 과정을 그림으로 그려보면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/69aefdf2-7c9e-45e8-b03f-ed1777d1233e/image.png" alt=""></p>
<p>알고 넘어가야할 점은 이 알고리즘은 정확한 lis 배열을 구하기 위한 방법이 아닌 lis의 길이를 구하기 위한 알고리즘 이라는 점입니다. 실제로 children 배열에서 구할 수 있는 최장 증가 수열은 [3, 5, 6] 이지만 위 알고리즘의 결과로 나온 lis 배열은 최종적으로 [1, 4, 6]인 것을 알 수 있습니다.</p>
<p>lis 배열을  업데이트하는 과정 즉 3, 7, 5 ... 들의 각 원소를 뽑아 lis 배열을 업데이트하는 각각의 스텝이 의미하는 바는 <strong>해당 원소의 위치에서 최장증가수열의 길이가 업데이트가 일어나는지를 체크하는 과정</strong>입니다. 즉 실제 최장증가수열 [3, 5, 6]에서 가장 마지막 원소는 6이므로 위 알고리즘의 진행과정에서도 6일 때 마지막 업데이트(lis배열의 가장 뒤에 원소가 추가되는 사건)가 일어나고 종료되는 것입니다.</p>
<p>그 과정에서 업데이트가 일어나지 않는 경우에는 lis 배열 중간의 값들이 계속해서 대체되기 때문에 정확한 최장증가수열을 구할 수 없습니다.</p>
<h1 id="정확한-lis배열을-구하는-방법">정확한 LIS배열을 구하는 방법</h1>
<p>정확한 최장증가수열 부분집합을 구하려면 추가적인 memoization을 거쳐야 합니다.
간단히 설명을 위해 DP 구현코드를 다시 살펴보겠습니다.</p>
<pre><code class="language-python">N = int(sys.stdin.readline())
children = [3, 7, 5, 2, 6, 1, 4]
dp = [1] * N
for i, child in enumerate(children):
    for j in range(0, i):
        if children[j] &lt; child and dp[i] &lt; dp[j] + 1:
            dp[i] = dp[j] + 1

print(children)        # [3, 7, 5, 2, 6, 1, 4]
print(dp)            # [1, 2, 2, 1, 3, 1, 2]
print(max(dp))        # 4</code></pre>
<p>위 코드에서 dp 배열에는 각각의 원소가 증가수열의 마지막 원소인 경우 해당 증가수열의 최대 길이를 나타내고 있습니다.
이 때 증가수열의 길이 뿐만 아니라 이전의 숫자가 어떤 숫자인지 추가적으로 기록해두면 연쇄적으로 역추적하여 정확한 LIS 배열을 구할 수 있습니다.</p>
<pre><code class="language-python">N = int(sys.stdin.readline())
children = [3, 7, 5, 2, 6, 1, 4]
prev_ids = [-1] * N
dp = [1] * N
for i, child in enumerate(children):
    for j in range(0, i):
        if children[j] &lt; child and dp[i] &lt; dp[j] + 1:
            dp[i] = dp[j] + 1
            prev_ids[i] = j

print(children)        # [3, 7, 5, 2, 6, 1, 4]
print(dp)            # [1, 2, 2, 1, 3, 1, 2]
print(prev_ids)        # [-1, 0, 0, -1, 2, -1, 5]

cur_idx = max(range(len(dp)), key=lambda i: dp[i])    # 최장증가수열의 가장마지막 숫자에 해당하는 children의 인덱스
real_lis = list()
while True:
    real_lis.append(children[cur_idx])
    cur_idx = prev_ids[cur_idx]
    if cur_idx &lt; 0:
        break
print(real_lis)        # [6, 5, 3]

print(max(dp))        # 4</code></pre>
<p>lis의 길이만 구했을때와 달라진 점은 prev_ids 배열이 추가로 활용된다는 점입니다.
prev_ids배열에는 dp값이 업데이트 될 때 마다 해당 숫자로 넘어오기 이전의 인덱스를 저장하여 해당 부분집합을 이룰 때 어떤 숫자를 포함하고 있었는지 추적할 수 있도록 합니다.</p>
<p>최종적으로 람다식을 활용하여 가장 긴 길이의 증가수열을 이루는 인덱스를 구하고, while문에서 prev_ids 배열을 통해 해당 증가수열을 이루는 구성요소들을 연쇄적으로 역추적하여 real_lis 배열에 저장합니다.</p>
<p>결과적으로 real_lis 배열에는 최장증가수열에 해당하는 부분집합이 저장되게 되는데, 역추적과정을 거쳤으므로 내림차순 순으로 정렬되어 저장이 되게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 메인메모리]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A9%94%EC%9D%B8%EB%A9%94%EB%AA%A8%EB%A6%AC</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A9%94%EC%9D%B8%EB%A9%94%EB%AA%A8%EB%A6%AC</guid>
            <pubDate>Sun, 20 Aug 2023 10:58:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://www.yes24.com/Product/Goods/89496122">운영체제 공룡책</a>을 읽고 정리한 내용입니다.</p>
</blockquote>
<h1 id="하드웨어에-대하여">하드웨어에 대하여</h1>
<p>CPU 스케줄링을 통해 성능향상을 이끌어내려면 많은 프로세스를 메모리에 유지하여 메모리를 공유하도록 해야합니다. 메인 메모리는 CPU가 직접 접근할 수 있는 유일한 범용 저장장치 이므로 모든 실행중인 프로세스는 메인메모리에 적재되어야 합니다.</p>
<p aligh="center">
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5345820c-37a6-4ce8-943f-908be9c46237/image.png" width="50%" align="center">
</p>

<p>위 사진처럼 메인 메모리에 여러 프로세스가 적재되는데 각 프로세스는 본인에게 해당하는 메모리에만 접근가능해야 하므로 <strong>기준 레지스터와 상한레지스터</strong> 주소를 정의하여 본인의 메모리가 아닌 외부의 주소를 접근할 경우 치명적인 오류로 간주하여 trap을 발생시킵니다.</p>
<h2 id="logical-vs-physical-주소공간">Logical vs Physical 주소공간</h2>
<p>일반적으로 프로그래밍을 할 때 로우레벨 하드웨어코딩이 아닌 이상 정확히 어떤 레지스터에 값을 저장할지 하드코딩하지 않습니다. 그저 변수에 값을 할당하는 식으로 코딩을 하면 컴파일 시에 배치 가능한 레지스터 주소 중 랜덤으로 저장되게 됩니다.
즉 CPU가 생성하는 주소를 <strong>논리주소 (Logical address)</strong>, 메모리에 실제로 적재되는 주소를 <strong>물리주소 (Physical address)</strong> 라 부릅니다.</p>
<p>따라서 프로그램이 실행되면 가상주소를 실제 물리주소로 변환하는 작업이 필요한데, 이는 <strong>메모리 관리장치 (MMU)</strong> 에 의해 실행됩니다.</p>
<p aligh="center">
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/fc58ef0e-a0ad-41a4-97a8-faa2bef813dc/image.png" width="80%">
</p>

<p>위 그림과 같이 논리주소가 346번일 때 MMU에서는 재배치 가능한 레지스터 주소를 참고하여 실제 물리주소로 매핑시키게 됩니다. 이 때 중요한 점은, 물리주소에는 상한레지스터가 있으므로 이를 벗어나지 않도록 적절히 물리주소로 변환하는 것이 핵심입니다.</p>
<h2 id="동적-적재-dynamic-loading">동적 적재 (Dynamic loading)</h2>
<p>프로세스를 실행할 때 모든 프로그래밍 데이터를 메인메모리에 적재해야 한다면 문제가 생깁니다. 예를들어 내 메인메모리가 8GB이라면 21GB 크기의 게임을 실행할 수 없을 것입니다. 
메모리 공간을 좀 더 효율적으로 이용하기 위하여 각 루틴이 실제 호출되기 전까지는 재배치 (MMU에서 논리주소를 물리주소로 연결하여 적재하는 행위) 가능한 상태로 디스크에서 대기하고 있다가 해당 루틴이 호출된다면 메인메모리에 적재되어 테이블에 변화를 기록해두고 루틴이 종료되면 다시 CPU제어가 중단되었던 루틴으로 복귀합니다.
이러한 기법을 <strong>동적 적재</strong>라 부르는데 오류 처리 루틴과 같이 간혹 실행되면서 실행할 코드가 많은 경우에 유용합니다.</p>
<h2 id="동적-연결-dynamic-linking">동적 연결 (Dynamic linking)</h2>
<p>흔히 C언어로 파일을 작성하면 object 파일로 컴파일 하고나서, 해당 object 파일을 실행가능한 binary 파일로 빌드하게 됩니다.
이 때 object 파일을 binary파일로 빌드하는 과정을 linking이라 부릅니다.
프로그램이 실행될 때 사용자 프로그램에 연결되어 유용하게 사용가능한 프로그램의 일부를 시스템 라이브러리 라고 부릅니다. 이러한 시스템 라이브러리가 binary 실행파일에 결합되어 메모리에 한번에 적재되는 형식을 <strong>정적 연결 (static linking)</strong> 이라 부릅니다. 이러한 방식으로 실행을 하게되면 메인메모리가 낭비되고 실행 파일의 크기또한 커지게 됩니다.
따라서 프로세스 실행도중 라이브러리가 필요한 시점에서 <strong>연결 (linking)</strong> 되어 사용한 후 다시 원래코드의 흐름으로 돌아오는 방식을 <strong>동적연결 (Dynamic linking)</strong> 이라 부릅니다. 이렇게 하기 위해서는 해당 라이브러리 파일을 따로 정해진 위치에 저장해두고 해당 라이브러리의 루틴을 참조하려면 loader가 정해진 위치에서 필요한 프로그램을 메모리에 적재하면 됩니다. 이러한 라이브러리를 <strong>동적연결 라이브러리 (DLL)</strong> 라고 부릅니다.</p>
<blockquote>
<p>흔히 규모가 큰 응용프로그램을 다운받아보면 .lib과 .dll파일 같은것이 보일텐데, .lib은 정적 연결 라이브러리, .dll은 동적 연결 라이브러리 파일입니다.</p>
</blockquote>
<p aligh="center">
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/c949c60f-60f3-4859-bfbd-ab0e6e546165/image.png" width="50%">
</p>


<p>내용을 정리해보면 위 그림과 같은데, 프로그래밍된 파일을 컴파일러가 object file로 컴파일 하고, linker가 실행가능한 이진 파일로 변환한 다음 loader는 해당 파일을 실제 메인메모리에 적재하여 실행하게 됩니다.
그 과정에서 외부 라이브러리를 DLL (Dynamically linked libraries) 파일로 만들어 런타임 내에서 참조할 수 있도록 하는 기법을 동적 연결 (Dynamic linking), 모든 프로그램 데이터를 한번에 메모리에 올리지 않고 런타임 내에서 루틴이 필요해졌을 때 메모리에 적재하는 기법을 동적 적재 (Dynamic loading)이라 부릅니다.</p>
<h1 id="메모리-할당">메모리 할당</h1>
<p>  메인 메모리에 여러 프로세스를 적재하려면 가장 기본적으로 연속적인 메모리 할당 기법을 생각할 수 있습니다. 가장 위에 있는 그림처럼 기준레지스터와 상한레지스터를 기준으로 메모리에 차곡차곡 적재하는것을 말합니다. </p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/dc72864d-4f3b-43c8-9196-e9a5b0074283/image.png" alt=""></p>
<p>이러한 논리 프로세스대로 기준레지스터와 상한레지스터 사이의 메모리 주소에 맞게 적재하며 외부의 메모리에 접근하지 못하도록 trap 오류를 일으켜 보안을 유지합니다.</p>
<p>이러한 연속적인 메모리 할당 기법을 사용했을 때 몇가지 문제점이 생기게 되는데 아래에서 살펴보겠습니다.</p>
<h2 id="가변-파티션과--hole">가변 파티션과  hole</h2>
<p>가장 단순하게 프로세스를 메모리에 적재하기 위하여, 메모리의 각 부분들을 사용 중인 부분과 사용 가능한 부분으로 나누어 사용 가능한 부분에만 적재될 수 있도록 하는 가변 파티션 기법을 사용할 수 있습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/fd3c2ee4-650d-4187-8381-4221278efedf/image.png" alt=""></p>
<ul>
<li>처음에는 low 메모리부터 프로세스를 차곡차곡 적재하게 되므로 1번째 상태와 같이 적재될 것입니다. </li>
<li>이후 프로세스 8이 종료되면서 해당 메모리가 빈 공간이 되고, 다른 프로세스가 적재될 수 있게 됩니다.</li>
<li>9번 프로세스가 빈 공간에 적재되었는데 비교적 작은 프로세스여서 메모리가 남게 되고</li>
<li>5번 프로세스도 종료되면서 메모리에 연속적이지 않은 빈 메모리 공간들이 생기게 됩니다.</li>
<li>이러한 빈 공간들을 hole이라 부르는데 이러한 과정으로 메모리에는 다양한 크기의 hole 들이 산재하게 됩니다.</li>
</ul>
<p>연속적인 메모리 할당기법에 의해 프로세스를 적재하기 위해서는 프로세스 보다 더 큰 크기를 가지는 hole이 필요한데 분포되어 있는 hole중 어떤 곳에 프로세스를 적재할지 정하는 알고리즘이 3 가지가 있습니다.</p>
<ul>
<li>최초 적합 (First-fit) : 가용 공간들을 linked-list로 구현하여 충분히 큰 hole을 찾았을 때 검색을 바로 종료하고 프로세스를 적재합니다. 일반적으로 가장 빠른 방법입니다.</li>
<li>최적 적합 (Best-fit) : 가용 공간들을 최소힙으로 구현하여 hole중 가장 작은것부터 탐색합니다. 해당 프로세스가 적재가능한 hole 중 가장 작은 사이즈이 hole에 적재되기 때문에 메모리공간을 효율적으로 사용하게 됩니다.</li>
<li>최악 적합 (Worst-fit) : 가용 공간들을 최대힙으로 구현하여 hole중 가장 큰 것부터 탐색합니다. 프로세스가 가장 큰 hole부터 적재되기 때문에 남는 가용 공간들을 더 크게 가져갈 수 있습니다.</li>
</ul>
<h2 id="단편화-fragmentation">단편화 (Fragmentation)</h2>
<p>특히 최초적합과 최적 적합에서 잘 일어나는 현상인데, 프로세스들이 적재되고 제거되는 일이 반복되다보면 가용공간들이 점점 작아지고 파편화되어 흩어지게 됩니다. 
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e0637ea0-0297-4d3b-a393-a6cdce7f2aff/image.png" alt=""></p>
<p>프로세스들이 위와 같이 메모리를 차지하고 있다면, 분명 남은 메모리공간이 있음에도, 파편화 되어있기 때문에 추가적으로 프로세스를 적재할 수 없는 상황이 발생합니다.
이를 해결하기 위해 페이징 (Paging)이란 기법을 사용하게 되는데 그 이전에 개념적으로 segmentation에 대해 알아보도록 하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/f97b1976-fc7a-43cb-939c-c6def4fdc19e/image.png" alt=""></p>
<p>위 그림처럼 왼쪽의 거대한 응용프로그램이 있다면 응용프로그램을 통째로 메모리에 배치하는 것이 아니라 응용프로그램의 각 기능을 담당하는 부분부분별로 나누어서 따로따로 메모리에 적재하는 기법입니다. 이렇게 되면 메인메모리가 파편화가 되었더라도, 응용프로그램을 분리시켜 적재하여 어느정도 극복이 가능하게 됩니다.
흔히 C언어 개발하다가 보면 Segmentation fault라는 에러를 자주 마주치게 되는데, 위 그림에서 알 수 있듯이 각 segment들이 특정 메모리 공간을 차지하고 있기 때문에 허용되지 않은 메모리에 접근하는 경우 일어나는 오류라고 이해할 수 있겠습니다.</p>
<h1 id="페이징-paging">페이징 (Paging)</h1>
<p>프로세스의 물리주소공간이 연속적이지 않아도 쪼개어 적재할 수 있는 기법을 paging기법이라 부릅니다. 페이징은 메인메모리의 단편화문제를 해결할 수 있으므로 대부분의 운영체제에서 사용하고 있습니다.</p>
<h2 id="페이징을-사용한-메모리-접근방법">페이징을 사용한 메모리 접근방법</h2>
<p>먼저 물리 메모리공간을 <strong>프레임</strong>이라 불리는 균일한 크기의 블록들로 나눕니다. 이후 논리적 메모리공간은 <strong>페이지</strong>라 불리는 같은 크기의 블록들로 나눕니다.
CPU에서 참조하는 주소는 모두 페이지 번호(p)와 페이지 오프셋(d) 두 부분으로 이루어집니다. 그리고 페이지 번호는 실제 프레임 위치로 매핑되며 오프셋을 더해 정확한 메모리 주소에 접근합니다. 
즉 CPU에서 나오는 페이지 번호와 오프셋으로 이루어진 논리주소를 물리주소로 변환하기 위해 MMU에서 일어나는 과정은 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/dea28110-bdb5-430d-8068-ec5c56a2d1b3/image.png" alt=""></p>
<ul>
<li>페이지 번호 p를 추출하여 페이지 테이블의 인덱스로 이동한다.</li>
<li>페이지 테이블에서 해당 프레임 번호 f를 추출한다.</li>
<li>f 프레임으로 이동하여 d위치에 있는 물리 주소를 참조한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/7a8dffdd-76fb-4f49-8cdd-e6b6ff69efa8/image.png" alt="">
이런식으로 논리주소에서는 물리주소를 전~~혀 신경쓰지 않고 페이지 단위로 주소를 접근하고싶어 합니다. 그저 이 페이지 번호를 인덱스 삼아 page table을 참조할 뿐입니다.
그럼 운영체제에서는 프로세스가 메모리에 적재될 때 page table에 비어있는 frame을 할당해주기만 하면 되는것입니다. 애초에 메모리를 균등한 프레임으로 나누어 할당해주기 때문에 외부 파편화를 완전 방지할 수 있습니다.
<strong>*하지만 한가지 문제점이 또 생기는데, *</strong> 만약에 page3이 아주 조금의 메모리만 필요하다면, 프레임 내부적으로 남는 메모리 공간이 생길 것입니다. 이를 <strong>내부 파편화</strong>라고 합니다.</p>
<h2 id="효율적인-페이징-접근">효율적인 페이징 접근</h2>
<p>현대 CPU에서는 상당히 큰 사이즈의 페이지 테이블을 사용하므로 빠른 속도의 레지스터를 사용하기엔 부적절합니다. (레지스터는 작은 사이즈에 적합) 따라서 페이지 테이블을 메인메모리에 저장하고 페이지 테이블 기준 레지스터 (PTBR)로 하여금 페이지 테이블을 가리키도록 합니다.
이렇게 하면 context changing 속도가 빨라지지지만 메모리에 직접 엑세스하는 시간이 느려질 수 있습니다.
이를 보완하기 위해 TLB(translation look-aside buffers)라 부르는 특수 소형 하드웨어 캐시를 사용하여 빠르게 검색하여 메모리에 바로 접근할 수 있도로 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 프로세스]]></title>
            <link>https://velog.io/@renovatio_hyuns/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4</link>
            <guid>https://velog.io/@renovatio_hyuns/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4</guid>
            <pubDate>Mon, 14 Aug 2023 12:18:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://www.yes24.com/Product/Goods/89496122">운영체제 공룡책</a>을 읽고 정리한 내용입니다.</p>
</blockquote>
<h1 id="프로세스란">프로세스란?</h1>
<p>운영체제는 응용프로그램이 동작할 수 있도록 자원을 관리하고 환경을 제공해줍니다. 즉 CPU활동을 관리한다고 볼 수 있는데 이러한 CPU활동을 무엇이라 부를까요?<br>응용프로그램들을 모두 메인메모리에 올릴 수 없으므로 하드디스크에 대부분의 실행프로그램들이 저장됩니다. 이후에 프로그램을 실행할 때 하드디스크에서 해당 프로그램을 불러와 메인메모리에 올려 동작할 수 있도록 준비시키고, 운영체제는 이를 실행시킵니다. <strong>이러한 작업 (메인메모리에 올라와서 동작할 준비가 되어있거나, 혹은 동작하고 있는 상태) 들을 프로세스</strong>라고 부릅니다.</p>
<p>프로세스에는 5가지 상태가 있습니다.</p>
<ul>
<li>new : 프로세스가 생성중인 상태</li>
<li>running : 명령어들이 실행되고 있는 상태</li>
<li>waiting : 프로세스가 어떤 이벤트가 일어나기를 기다리는 상태</li>
<li>ready : 프로세스가 처리기에 할당되기를 기다리는 상태</li>
<li>terminated : 프로세스의 실행이 종료된 상태</li>
</ul>
<p>CPU의 한 코어에서는 하나의 프로세스만이 <strong>실행 (running)</strong> 될 수 있지만 다수의 프로세스가 <strong>준비완료(ready)나 대기상태(waiting)</strong>로 있을 수 는 있습니다. 즉 8코어CPU를 가지고 8개의 프로세스를 실제로 동시에 실행할 수 있으며 1개의 코어로는 프로세스 스케줄링을 통해 여러개의 준비된 프로세스들을 번갈아가며 실행하며 다중 프로세스를 실행하는것과 같은 효과를 낼 수 있습니다.</p>
<h2 id="프로세스-스케줄링">프로세스 스케줄링</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/8aa007af-13ab-47f1-8515-29e870d95c54/image.png" alt="">
프로세스가 시스템에 들어가면 준비 큐에 들어가서 CPU 코어에서 실행되기를 기다립니다. 프로세스에 CPU코어가 할당되면 일시적인 할당시간동안 동작하게 되는데 종료되지 않고 인터럽트 되거나 특정이벤트가 발생되는 것을 기다려야 하는 경우가 있습니다. 해당 프로세스들은 대기 큐에 들어가서 이벤트를 기다리게 됩니다.
위 그림에서 보이는것처럼 프로세스들은 준비큐에서 기다리다가 CPU 코어를 할당받아 잠시동안 동작하고 다시 준비큐로 들어오는 과정을 종료될때까지 반복하게 되는데 그 중간과정에서 인터럽트나 입출력등의 이벤트가 필요한 경우 대기 큐에서 해당 이벤트를 대기하다가 다시 준비큐로 들어오며 다중 프로세스가 CPU 단일코어에서 동작하게 되는 모습입니다.</p>
<h2 id="cpu-스케줄링">CPU 스케줄링</h2>
<p>준비큐에 있는 프로세스에 CPU 코어를 할당하는 CPU 스케줄러가 존재합니다. 당연히 어떤 프로세스를 CPU에 올릴것인지에 대한 프로세스 스케줄링이 필요한 것처럼 멀티코어 CPU의 경우 해당 프로세스를 어떤 CPU코어에 할당할 것인지에 대한 CPU 스케줄링 또한 필요한 것입니다.
추가로 가용 메모리 공간을 확보해야 하는경우 <strong>스와핑</strong>이라는 기법을 사용할 수 있는데 메모리에서 프로세스를 제거하여 다중 프로그래밍 정도를 감소시키는 방법입니다. 메모리에서 디스크로 프로세스를 옮기면서 상태를 저장하여 이후에 다시 복원할 수 있는데 임시방편으로 가용메모리를 확보하는 경우 사용됩니다.</p>
<h3 id="안드로이드에서의-프로세스">안드로이드에서의 프로세스</h3>
<p>모바일 환경은 자원이 제한되어있습니다. 따라서 모바일 운영체제는 제한된 시스템 자원을 회수하기위해 기존 프로세스를 강제로 종료해야할 수 있습니다. 이를 위해 Android에서는 프로세스의 중요도 계층으 식별하여 중요하지 않은 프로세스부터 강제로 종료하여 중요한 프로세스를 위한 자원을 확보하게 됩니다.</p>
<ul>
<li>foreground process : 사용자가 현재 상호작용하고 있는 응용 프로그램을 나타내며 화면에 나타나 있는 현재 프로세스</li>
<li>visible process : 전경에서 직접 볼 수 없지만 전경 프로세스가 참조하는 활동을 수행하는 프로세스</li>
<li>service process : 백그라운드 프로세스와 유사하지만 사용자가 인지할 수 있는 활동을 수행하는 프로세스 (음악 스트리밍)</li>
<li>background process : 활동을 수행하고 있지만 사용자가 인식하지 못하는 프로세스 (백그라운드 게임 데이터 다운로드)</li>
<li>empty process : 응용 프로그램과 관련된 활성 구성요소가 없는 프로세스</li>
</ul>
<h1 id="프로세스간-통신">프로세스간 통신</h1>
<p>서로 영향을 주고받는 협력적인 프로세스들이 있다면 통신을 통해 데이터를 주고받을 수 있어야 합니다. 따라서 프로세스간 통신 (interprocess communication, IPC)은 공유메모리나 메시지전달 두가지 방식으로 구현됩니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5009ca7f-a8c2-4247-a3b6-9eacf83062ad/image.png" alt="">
(a) : 공유메모리 방식으로 두 프로세스가 함께 접근가능한 공유메모리 영역을 할당하여 데이터를 읽고 쓸 수 있도록 합니다. 메모리 영역을 쓰거나 참조만 하기 때문에 운영체제의 개입이 없습니다.
(b) : 메시지 전달 방식으로 운영체제가 제공해주는 통신수단인 메시지큐를 사용하여 통신하는 방식입니다. 이 방식은 프로세스의 통신간에 운영체제가 개입하게 됩니다.</p>
<h2 id="공유메모리-방식">공유메모리 방식</h2>
<p>생산자-소비자 문제의 경우 하나의 프로세스는 정보를 생산만 하고, 하나의 프로세스는 생산된 정보를 소비하기만 하는 구조입니다. 이런 경우 공유메모리를 통해 구현할 수 있는데, 하나의 프로세스는 공유메모리에 쓰기만 하고 하나의 프로세스는 공유메모리를 읽기만 하면 되기 때문입니다.
<strong>만약 공유메모리가 유한버퍼여서 꽉 찼다면 어떻게 될까요?</strong>
메모리가 꽉 찼다면 생산자 프로세스는 메모리에 자리가 날 때 까지 wait 해야할 것이고, 만약 메모리가 비어있다면 소비자 프로세스가 메모리에 데이터가 들어올 때 까지 wait 해야할 것입니다.
이러한 공유메모리 방식은 단점이 존재합니다. 공유메모리에 접근하고 조작하는 코드가 응용프로그램을 개발하는 과정에서 작성되어야 하는데, 만약 공유메모리에 접근해야하는 프로세스가 너무 많거나 추후 추가되거나 하면 그 과정이 복잡해지고 불편해질 수 있습니다. 따라서 운영체제에서 프로세스간 통신수단을 제공해주어 동일한 주소공간을 공유하지 않고도 프로세스들이 통신을 하고 동기화될 수 있는 방법이 아래에서 설명할 메시지 전달 방식입니다.</p>
<h2 id="메시지-전달-방식">메시지 전달 방식</h2>
<p>메시지 전달방식은 두 가지 연산을 필수적으로 제공합니다.</p>
<ul>
<li>send</li>
<li>receive</li>
</ul>
<p>보내고 받기. 되게 간단명료합니다. 두 프로세스간 통신을 원하면 그들 사이에 통신연결이 되어있어야 하며 서로 메시지를 보내고 받아야 합니다. 통신을 원하는 프로세스끼리 서로 가리킬 방법이 있어야 하는데 송수신자의 이름을 명시하여 send(P, message), receive(Q, message) 형식으로 P나 Q 프로세스로부터 메시지를 송수신하도록 구현할 수 있습니다. 이를 직접 통신이라 부르는데, 굳이 직접 통신하지 않아도 간접적으로 통신하는 방법 또한 존재합니다. 간접통신에서 메시지들은 메일박스 또는 포트 라고 부르는 곳으로 전송되는데 A라는 포트가 있다고 하면 send(A, message), receive(A, message)와 같이 특정 포트에 데이터를 송수신하도록 구현합니다.</p>
<h1 id="cpu-스케줄링-1">CPU 스케줄링</h1>
<p>위에서 설명했듯이 CPU는 매우 바쁜 상태를 유지하며 시스템의 모든 처리코어가 다중프로세스를 처리하도록 합니다. 프로세스 실행은 I/O 버스트와 CPU버스트를 왔다갔다 하면서 실행되는데 CPU 버스트의 실행시간 분포는 CPU 스케줄링을 하는데 중요하게 작용합니다. 
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ec4273d4-17a0-46ed-b8c4-513788ce8dcf/image.png" alt=""></p>
<h2 id="선점-및-비선점-스케줄링">선점 및 비선점 스케줄링</h2>
<p>CPU 스케줄링 결정은 네 가지 상황에서 발생합니다.</p>
<ul>
<li>한 프로세스가 실행상태에서 대기상태로 전환될 때</li>
<li>프로세스가 실행상태에서 준비 완료 상태로 전환될 때</li>
<li>프로세스가 대기 상태에서 준비 완료 상태로 전환될 때</li>
<li>프로세스가 종료될 때</li>
</ul>
<p>항상 1번과 4번의 경우에서만 스케줄링을 하게된다면, 프로세스가 알아서 종료되거나 대기상태로 돌아올때 까지 기다리기만 하면 됩니다. 이를 <strong>비선점 스케줄링</strong>이라 합니다. 2번과 3번의 경우도 발생한다면, 강제로 프로세스를 교체시키거나 가만히 놔두는 선택지가 존재합니다. 이러한 방식을 <strong>선점 스케줄링</strong>이라 합니다. 대부분의 운영체제는 효율을 위해 선점스케줄링방식으로 동작하지만 경쟁조건을 일으킬 수 있는 문제점이 있습니다.</p>
<h3 id="디스패처">디스패처</h3>
<p>CPU 스케줄링을 할 때 CPU에서 동작하는 프로세스를 교체하는 행위를 context switching이라 합니다. 이러한 context switching 기능을 제공해주는 모듈을 <strong>디스패처</strong>라 부릅니다.</p>
<h2 id="스케줄링-알고리즘">스케줄링 알고리즘</h2>
<p> CPU 스케줄링 알고리즘을 비교하기 위해 여러가지 다양한 기준이 있습니다. </p>
<ul>
<li>CPU이용률</li>
<li>처리량</li>
<li>총 처리시간</li>
<li>대기 시간</li>
<li>응답 시간</li>
</ul>
<p>보통 CPU이용률과 처리량을 극대화하고 총 처리시간, 대기 시ㅏㄴ, 응답 시간을 최소화하도록 동작합니다.</p>
<h3 id="선입선처리-스케줄링-fcfs">선입선처리 스케줄링 (FCFS)</h3>
<p>이 알고리즘은 선입선출 큐로 관리하게 되는데 프로세스가 준비 큐에 진입하면 프로세스 제어블록 (PCB)을 큐의 끝에 연결합니다. 들어온 순서대로  프로세스가 진행되기 때문에 CPU 버스트 시간의 차이가 클 수록 평균 대기시간의 편차가 커질 수 있습니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/15fcce27-696e-44ac-a317-153771eff346/image.png" alt="">
Gantt 차트를 그려보면 위와 같은데 같은 프로세스가 순서만 반대로 들어온 경우입니다. 위의 케이스에서는 대기시간이 0 + 24 + 27 초로 긴 편이고 아래의 경우에는 대기시간이 0 + 3 + 6초로 매우 짧은 편입니다.
또한 이러한 방식은 도착순서대로 프로세스가 실행되고 끝날 때 까지 내려놓지 않기 때문에 비선점형 스케줄링으로 볼 수 있습니다.</p>
<p>위의 케이스에서 볼 수 있듯이 CPU 버스트가 짧은 프로세스를 먼저 실행하면 더 대기시간을 짧게 가져갈 수 있는데 들어온 순서대로 함에 따라 긴 프로세스가 나머지 짧은 프로세스를 방해하는 현상을 <strong>convoy effect</strong>라고 합니다.</p>
<h3 id="최단작업-우선-스케줄링-sjf">최단작업 우선 스케줄링 (SJF)</h3>
<p>프로세스의 CPU 버스트가 짧은 순서로 스케줄링을 진행하고, CPU버스트가 모두 같다면 FCFS를 적요하는 스케줄링 방법입니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/da1acbbb-868d-4a36-a130-c3c48a5d53cf/image.png" alt="">
이렇게 하면 보통 평균대기시간이 짧아진, 위와같이 최적화된 스케줄링이 가능합니다.
이러한 방식은 비선점형 스케줄링으로 구현할 수 도 있지만, 현재 진행중인 프로세스의 남은 CPU 버스트보다 더 짧은 프로세스가 들어오게 된다면 교체해버리는 선점형 스케줄링 방식으로도 구현가능합니다.
<em><strong>그러나 CPU 버스트의 정확한 길이를 알 방법이 없기 때문에 이전의 버스트들의 길이를 지수평균한 값으로 근사시켜 다음 버스트의 길이를 예측하는 식으로 우선순위를 정하게 됩니다.</strong></em></p>
<h3 id="라운드-로빈-스케줄링-rr">라운드 로빈 스케줄링 (RR)</h3>
<p>10~100ms정도의 작은 단위시간을 할당해서 해당 단위시간동안만 프로세스가 동작하도록 하는 스케줄링 방법입니다.</p>
<ul>
<li>단위시간보다 긴 프로세스면 중간에 interrupt가 걸립니다.</li>
<li>단위시간보다 짧은 프로세스면 자연적으로 종료됩니다.</li>
</ul>
<p>FCFS처럼 선입선출큐로 구현을 하는데 어차피 단위시간동안만 동작하게 되므로 CPU 버스트와 무관하게 균일한 평균대기시간을 가지게 됩니다. 단위시간이 너무 짧다면 context switching이 너무 빈번하게 일어나므로 CPU버스트에 손해를 봅니다. (dispatch latency가 증가한다는 것과 같은 말입니다.) 따라서 너무 길지도 너무 짧지도 않은 적당한 단위시간을 정하는 것이 중요합니다. 
이 방식은 자명하게도 선점 스케줄링입니다.</p>
<h3 id="우선순위-스케줄링">우선순위 스케줄링</h3>
<p>SJF나 FCFS 방식의 스케줄링은 각각 나름대로의 우선순위를 정해서 우선순위에 의해 CPU를 배정하는 방식이었습니다. 이러한 방식을 통틀어 우선순위 스케줄링이라 부르는데 (반대로 라운드 로빈은 우선순위 스케줄링이 아닙니다.) 이들에는 특별한 문제점이 있습니다.</p>
<blockquote>
<p><strong>무한 봉쇄 (indefinite blocking) 혹은 기아 상태 (starvation)</strong>이라 부르는데 실행준비는 되어 있으나 우선순위가 낮은 프로세스들이 계속해서 우선순위가 밀려 CPU를 얻지 못하고 무한히 대기하는 상태를 의미합니다.</p>
</blockquote>
<p>이러한 문제를 해결하기 위한 방법으로 노화 (aging)가 있습니다. 시스템에서 대기하는 프로세스들이 탈출할 수 있도록 우선순위를 점점 증가시키는 것입니다. </p>
<h3 id="다단계-큐-스케줄링-mlq">다단계 큐 스케줄링 (MLQ)</h3>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/b8984b1f-4293-4175-af4f-6f6f801897f7/image.png" alt="">
우선 순위에 따라 준비 큐를 여러개 두는 방식입니다. 하나의 준비 큐에 우선순위 순서대로 들어가는 것이 아니라, 우선순위에 따라 각각 다른 준비 큐로 들어가 우선 순위가 높은 순서대로 준비 큐를 비우는 방식입니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/7e6f2c60-c580-4917-91ec-8e131acff001/image.png" alt="">
안드로이드 OS와 같은 경우 각각의 프로세스 역할에 따라 자명하게 구분된 우선순위가 존재하는데, 이러한 프로세스들을 우선순위가 높은 (real-time과 같은 프로세스)것부터 먼저 전부 실행시키는 것입니다.</p>
<h3 id="다단계-피드백-큐-스케줄링-mlfq">다단계 피드백 큐 스케줄링 (MLFQ)</h3>
<p>MLQ 방식에서는 우선순위가 낮은 프로세스들이 실행되지 못할 가능성이 있습니다. 따라서 aging과 같이 기아문제를 해결할 수단이 필요합니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/fe4cd18e-6de4-4891-88ff-e446df2f247a/image.png" alt="">
낮은 우선순위에서 너무 오래 대기하는 프로세스는 높은 우선순위의 준비 큐로 이동할 수 있고, 우선순위가 높은 프로세스가 CPU 버스트를 너무 오래 사용하면 낮은 우선순위 큐로 이동하게 됩니다.
가장 일반적인 스케줄링 알고리즘이며 특정 시스템에 부합하도록 설계가능하지만, 가장 복잡한 알고리즘이기도 합니다.</p>
<h1 id="다중-처리기-스케줄링">다중 처리기 스케줄링</h1>
<ul>
<li>다중 코어 CPU</li>
<li>다중 스레드 코어</li>
<li>NUMA 시스템</li>
<li>이기종 다중 처리 </li>
</ul>
<p>와 같은 환경에서는 여러 스레드가 병렬로 실행되면서 <strong>부하 공유</strong>가 가능해지며 아래 그림과 같이 두 가지의 전략이 가능합니다.
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/e8bb3080-5b6f-4a19-9527-71dc49a38a3d/image.png" alt="">
(a) 옵션을 선택하면 공유 준비 큐에 경쟁조건이 생길 수 있으므로 서로 다른 프로세서가 동일한 스레드를 스케줄하지 않도록 보장해야 합니다.
(b) 옵션을 선택하면 경쟁조건과 같은 성능문제를 겪지 않지만 코어마다 부하가 편차가 생길 수 있으므로 비효율적이게 될 수 도 있습니다. 따라서 이를 해결하기 위해 균형 알고리즘을 사용하여 부하를 균등하게 분산시킬 수 있습니다.
보통 SMP를 지원하는 운영체제에서는 (b) 옵션의 형태로 스케줄링 하게 됩니다.</p>
<h2 id="부하-균등화">부하 균등화</h2>
<p>위에서 (b)의 방식을 사용할 때 부하를 균등하게 하는 알고리즘에 대해 언급했었습니다.
하나의 처리기에만 큰 부하가 걸리게 되면 나머지 처리기들이 쉬게 되므로 부하 균등화는 SMP 시스템의 모든 처리기들에 부하를 균등하게 배분합니다.
특정 태스크가 주기적으로 각 처리기의 부하를 검사하고 부하가 높은 처리기에서 쉬고 있는 처리기로 push migration 시키거나, 쉬고있는 처리기가 바쁜 처리기로부터 pull migration 하는 방식으로 일어납니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 운영체제 구조]]></title>
            <link>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B3%B5%EB%A3%A1%EC%B1%85-Part.1</link>
            <guid>https://velog.io/@renovatio_hyuns/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B3%B5%EB%A3%A1%EC%B1%85-Part.1</guid>
            <pubDate>Sun, 06 Aug 2023 13:19:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://www.yes24.com/Product/Goods/89496122">운영체제 공룡책</a>을 읽고 정리한 내용입니다.</p>
</blockquote>
<h1 id="운영체제란">운영체제란?</h1>
<p>운영체제는 컴퓨터가 동작할 수 있는 환경을 제공해주는 SW 라고 볼 수 있습니다. 컴퓨터 자체는 물리적인 HW이기 때문에 운영체제라는 SW를 탑재해야만 다양한 응용프로그램들이 동작할 수 있습니다.</p>
<h2 id="그럼-컴퓨터는-뭔가요">그럼 컴퓨터는 뭔가요</h2>
<p>말 그대로 compute + er 계산기 인데, 정보를 처리하기 위한 장치입니다.</p>
<ul>
<li>정보의 최소단위를 Binary digit(Bit) 으로 정의하고 1과 0으로 표현하기로 약속</li>
<li>이를 물리적으로 표현하기 위해 전기신호를 on / off 하여 표현 (트랜지스터)</li>
<li>트랜지스터들의 집합으로 논리게이트를 만들고 (NOT, OR, AND … )</li>
<li>논리게이트 들의 집합합으로 논리회로를 구성 (IC, LSI, VLSI, ULSI, SoC …)</li>
<li>고밀도 집적회로 (SoC)가 곧 CPU, GPU 등의 역할을 하게되면서 컴퓨터가 발전</li>
</ul>
<h2 id="컴퓨터의-시초-튜링머신">컴퓨터의 시초 튜링머신</h2>
<p>현대 컴퓨터의 구조는 모두 앨런 튜링이 설계한 튜링머신을 기반으로 하고 있기 때문에 튜링머신의 구조에 대해 먼저 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/6bf9762c-4b17-4508-ae41-4b52b385a87e/image.png" alt=""></p>
<p>튜링머신은 세 가지로 구성되어 있습니다.</p>
<ul>
<li>테이프 : 무한히 늘어날 수 있는 종이테이프이며 각 셀마다 기호가 적혀있음</li>
<li>헤드 : 좌우로 이동할 수 있는 제어장치이며 현재 기계의 상태가 기록되어 있음</li>
<li>행동표 : 기계의 상태와 읽은 기호에 따라 행동양식이 정의되어 있고, 헤드는 이 표에 따라 동작을 수행</li>
</ul>
<p>즉 행동표에 정의 되어있는 행동 양식에 따라 헤드가 동작하며, 테이프에 어떠한 정보를 읽고 쓰는 동작을 하는 기계가 튜링머신입니다.</p>
<p>현대 컴퓨터와 비교해보면 테이프는 메모리, 테이프의 정보를 읽고 쓰는 헤드는 메모리 입출력 장치 혹은 프로세서, 동작을 정의하는 작동규칙표는 곧 CPU 혹은 응용프로그램과 유사한 것을 알 수 있습니다.</p>
<p>이 튜링머신 (응용프로그램)이 여러개 모이면 universal turing machine이 되는데, 이러한 집합을 운영체제에 비유할 수 있습니다.</p>
<h2 id="운영체제-작동방식">운영체제 작동방식</h2>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/41ee83d8-65da-4b65-bbf6-323e0b26dbd2/image.png" alt=""></p>
<p>컴퓨터 하드웨어 안에 운영체제가 필수로 들어가고, 시스템프그래밍을 통해 운영체제와 통신하여 컴퓨터의 하드웨어가 동작할 수 있는 응용프로그램을 만들 수 있습니다.</p>
<p>사용자는 이 응용프로그램을 사용하려 하게되고 요청을 받아 운영체제에서 서비스를 제공해주게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/eee6acd1-0e5a-4bdd-ad7a-17e18cce87f1/image.png" alt=""></p>
<p>현대에는 다중코어를 가진 CPU와 다양한 물리적 디바이스 컨트롤러들이 버스를 통해 메모리 (RAM)에 연결되어 있습니다.  이러한 형태의 컴퓨터 시스템을 OS 가 제어해주게 됩니다.</p>
<p>컴퓨터를 처음 작동시키면 가장먼저 벌어지는 일이 ROM(Read-Only-Memory)에 운영체제를 로딩하는 일이며 이것을 <strong>Bootstrapping</strong>이라 부릅니다.</p>
<h1 id="컴퓨터-시스템의-구성">컴퓨터 시스템의 구성</h1>
<p>컴퓨터 시스템은 <strong>하드웨어, 운영체제, 응용프로그램</strong>으로 구성됩니다.</p>
<p><strong>하드웨어</strong>는 중앙처리장치(CPU), 메모리 입출력 (I/O) 장치로 구성되고 기본 연산자원을 제공하는데 응용프로그램에서 사용자의 계산문제를 해결하기 위해 이들 자원이 어떻게 사용될지를 정의한다.</p>
<p><strong>운영체제</strong>는 그 자체로는 유능한 기능을 수행하지 못하지만, 다른 프로그램이 유용한 작업을 할 수 있는 환경을 제공합니다. </p>
<p>운영체제에는 항상 실행중인 <strong>커널</strong>, 응용프로그램 개발을 쉽게 하고 기능을 제공하는 <strong>미들웨어 프레임워크</strong>, 시스템을 관리하는데 도움이 되는 <strong>시스템 프로그램</strong>이 포함됩니다.</p>
<p>현대의 범용 컴퓨터 시스템은 하나 이상의 CPU와 공통버스를 통해 연결된 여러 장치 컨트롤러로 구성되는데 운영체제안에는 각 장치 컨트롤러에 대한 장치 드라이버가 있습니다. </p>
<p>따라서 현대에 새로운 컴퓨터나 모바일 휴대폰장치를 개발할 때 대부분 운영체제에 붙어있는 디바이스 드라이버 및 컨트롤러를 개발하는 작업이 수행됩니다.</p>
<h2 id="저장장치-구조">저장장치 구조</h2>
<p><strong>폰 노이만 구조 시스템</strong>에 의해 실행되는 전형적인 명령-실행 사이클은 먼저 메모리로부터 명령을 인출하고, 그 명령을 명령레지스터에 저장합니다. 따라서 CPU는 메모리에서만 명령을 적재할 수 있으므로 프로그램을 먼저 메모리에 적재해야 합니다. </p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/00d3c657-5368-403f-b780-722993dbdb60/image.png" alt=""></p>
<p>컴퓨터는 프로그램 대부분을 메인 메모리라 불리는 <strong>RAM</strong>에서 가져오는데, 이는 휘발성 이므로 전원이 꺼지면 내용이 손실됩니다. 비휘발성 메모리에는 운영체제를 적재하는 <strong>부트스트랩</strong>, 읽기전용 메모리 <strong>EEPROM</strong>, 비휘발성 저장장치인 <strong>펌웨어</strong> 등이 있으며, 모든 형태의 메모리는 주소를 의미하는 바이트의 배열을 제공합니다.</p>
<p>EEPROM이나 펌웨어 등은 메모리의 변경이 용이하지 않기에, 프로그램은 메인메모리 RAM에 올려야 합니다. </p>
<ul>
<li>그러나 RAM은 모든 프로그램과 데이터를 저장하기엔 크기가 너무 작고</li>
<li>전원이 공급되지 않으면 내용이 유실되는 휘발성 저장장치이므로</li>
</ul>
<p>보조저장장치로써 <strong>하드디스크 드라이브(HDD)</strong> 와 <strong>비휘발성 메모리 (NVM)</strong>를 사용합니다.  따라서 대부분의 시스템 및 응용프로그램은 메모리에 적재될 때 까지 보조저장장치에 저장됩니다.</p>
<p><strong>HDD는 기계식 저장장치, NVM은 전기적 저장장치 (예를 들면 SSD)</strong> 라고 불리는데, 일반적으로 전기적 저장장치가 더 성능이 좋습니다.</p>
<h1 id="컴퓨터-시스템-구조">컴퓨터 시스템 구조</h1>
<p>컴퓨터 시스템은 사용된 범용 처리기의 수에 따라 분류 가능합니다.</p>
<h2 id="단일-처리기-시스템">단일 처리기 시스템</h2>
<p><strong>코어</strong>는 명령을 실행하고 로컬로 데이터를 저장하기 위한 구성요소이며 하나의 메인 <strong>CPU</strong>는 프로세스의 명령어를 포함하여 범용 명령어 세트를 실행할 수 있습니다. 단일 처리코어를 가진 하나의 CPU로 이루어진 프로세서를 단일처리기 시스템이라 부릅니다.</p>
<h2 id="다중-처리기-시스템">다중 처리기 시스템</h2>
<p>최신 컴퓨터에서는 <strong>다중처리기 시스템 (multiprocessor system)</strong>이 존재하는데, 이는 단일코어 CPU가 있는 두 개 이상의 프로세서를 의미합니다. 프로세서 수를 늘리면 더 적은 시간에 더 많은 작업을 수행할 수 있으므로 처리량이 증가하는 장점이 있습니다. 그러나 N개의 프로세서를 쓴다고 N배 처리속도가 빨라진다고 볼 수는 없는데 이유는 여러 프로세서가 하나의 작업에 협력할 때 <strong>오버헤드</strong>가 발생하기 때문입니다.</p>
<ul>
<li>멀티코어 : 하나의 CPU 칩 안에 여러개의 코어가 포함되어 각각 독립적인 작업을 수행한다. 병렬처리를 통하여 하나의 작업을 나누어 수행함으로써 더 빠른 수행속도를 얻는데 적합하다.</li>
<li>멀티프로세서 : 여러개의 독립적인 CPU가 시스템 안에 존재하므로 각각 독자적으로 작업을 수행할 수 있다. 동시에 여러작업을 수행하는데 적합하다.</li>
<li>하지만 굳이 따지자면 둘다 비슷한일을 수행할 수 있다. CPU를 여러개 쓸수록 좋고 각각의 CPU를 쪼개서 여러개의 코어를 둘 수록 좋은 것. (멀티코어 멀티프로세서)</li>
</ul>
<h2 id="클러스터형-시스템">클러스터형 시스템</h2>
<p>둘 이상의 독자적 시스템 또는 노드들을 연결하여 구성한 시스템입니다. 다중 처리기 시스템은 하나의 독자적 시스템 안에 여러 프로세서가 존재하는것에서 차이가 있습니다. 클러스터 컴퓨터는 저장장치를 공유하고 LAN과 같은 근거리 통신망이나 연결망으로 연결됩니다.</p>
<p>클러스터 내 하나의 컴퓨터가 고장나더라도 서비스는 계속 제공되므로 높은 가용성을 제공할 수 있습니다. 병렬화라는 기법으로 컴퓨터의 각 계산 노드가 일부문제들을 해결하여 종합하도록 구현한다면, 다중처리기 시스템 보다 훨씬 큰 계산능력을 제공할 수 있습니다.</p>
<h1 id="multitasking">Multitasking</h1>
<p>보통 응용프로그램을 메모리에 올려두고 사용하게 되는데, 여러개의 응용프로그램을 메모리에 올려두고 동시에 사용할 수 있다면 CPU 효율을 높게 가져갈 수 있습니다.</p>
<p>CPU는 우리가 생각하는것보다 성능이 훨씬 좋기 때문에 한번에 하나의 프로그램만 동작하게 사용한다면, 인터럽트를 대기하는 시간이 대부분을 차지하게 되고 이 시간들이 낭비되게 됩니다.</p>
<p>따라서 여러 프로그램이 동시에 돌아가면서 CPU가 담당하는 작업을 계속해서 전환하며 마치 여러개의 작업을 동시에 수행하는것처럼 동작하게 구현하는 것을 <strong>multiprogramming</strong>이라고 부릅니다.</p>
<p>RAM에 여러개의 프로세스가 동시에 준비된 상태이더라도, CPU는 한 번에 하나의 일만 처리할 수 있기 때문에 다음 매 작업전환마다 다음에 동작할 프로그램을 선택해야 합니다. 이 때 CPU가 최대한 효율적으로 일할 수 있도록 작업을 선택하는 방법론을 <strong>CPU scheduling</strong>이라고 합니다.</p>
<h1 id="운영체제의-두가지-모드">운영체제의 두가지 모드</h1>
<p>운영체제에는 유저모드와 커널모드 두 가지 모드가 존재합니다. 그 이유는 유저가 어떤 프로그램을 다룰 때 잘못 동작하여 치명적인 오류를 발생시키는 일을 막아야 하기 때문입니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/cd54ed22-51e0-4861-9859-51869b6c42ff/image.png" alt=""></p>
<p>유저 프로세스와 커널 프로세스로 나뉘는데, 유저 프로세스에서는 HW를 직접적으로 제어할 수 없습니다. system call 을 사용하여 커널에 요청을 보내야지만 커널에서 HW를 제어하게 됩니다. </p>
<p>약간 요런느낌인데</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/1a28d562-d1e5-4707-a779-1c5a8af353bc/image.png" alt=""></p>
<p>출처 : <a href="https://blockdmask.tistory.com/69">https://blockdmask.tistory.com/69</a></p>
<p><strong>커널에는 파일시스템 및 메모리와 각종 디바이스 드라이버 등의 중요한 컴퓨터 자원이 존재</strong>하므로 <strong>유저가 이를 직접적으로 접근해서 제어하지 못하도록</strong> 해야합니다.</p>
<p>따라서 유저모드에서 유저가 어떠한 명령을 내리면 system call 이라는 명령을 통해 커널모드에 접근하여 컴퓨터자원을 사용하거나 하드웨어에서 어떠한 동작을 수행하도록 명령을 내리고 다시 유저모드로 복귀하는 형태로 동작합니다.</p>
<h1 id="가상화-기술">가상화 기술</h1>
<p>현대적인 컴퓨터에서는 여러개의 프로세스를 하나의 CPU에서 병렬적으로 처리가능합니다. 기술이 발전함에 컴퓨팅 능력이 강력해지고, 병렬적으로 처리할 수 있는 프로세스가 늘어나게 되었습니다. 따라서 다중 프로세스만 처리하지 말고 하나의 CPU를 가지고 여러개의 운영체제를 단일 컴퓨터 내에서 돌릴 수 있도록 하는 기술이 등장하게 되었고 이를 가상화 기술이라 부릅니다.</p>
<p>VMM (Virtual Machine Manager) 라 불리는 가상머신을 여러개를 띄우고 각각 다른 OS를 설치하여 OS VMM scheduling을 통해 여러 운영체제를 돌릴 수 있게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/446236e5-7c3c-498e-8b0a-2c41baa51f83/image.png" alt=""></p>
<p>단일 운영체제와 VMM을 활용한 다중운영체제를 시각화한 모습.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BOJ_1890) 점프]]></title>
            <link>https://velog.io/@renovatio_hyuns/BOJ1890-%EC%A0%90%ED%94%84</link>
            <guid>https://velog.io/@renovatio_hyuns/BOJ1890-%EC%A0%90%ED%94%84</guid>
            <pubDate>Tue, 01 Aug 2023 06:19:20 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/5396acfd-dedc-4149-b50e-db0643b4198f/image.png" alt=""></p>
<p>DP문제인지 그래프 문제인지 헷갈렸던 문제
<a href="https://www.acmicpc.net/problem/1890">https://www.acmicpc.net/problem/1890</a></p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/555ce45e-847e-40f7-abc2-8adda7a80b64/image.png" alt=""></p>
<p>이런식으로 그래프가 주어지는데, 각 칸에는 이동할 때 건너뛰는 거리가 주어진다. 
즉 이동의 규칙이</p>
<ul>
<li>(0,0) 좌표에서 시작하는데 무조건 오른쪽 혹은 아래쪽으로 이동해서 (N-1, N-1) 좌표에 도착해야 한다.</li>
<li>그런데 한번 이동할 때에는 현재 칸에 적혀있는 숫자만큼 무조건 건너뛰어야 한다.</li>
</ul>
<p>딱봐도 모양새가 그래프로 풀면 될거같이 생겨서 처음에는 DFS로 구현했다.</p>
<h2 id="왜-dfs냐면">왜 DFS냐면</h2>
<p>이문제는 최단경로나 하나의 경로만 찾는것이 아닌 모든 경우의 수를 다 구해야 하므로 어차피 완전탐색을 해야하는건 BFS를 쓰나 DFS를 쓰나 같다.
하지만 BFS를 쓰면 메모리를 많이 차지할 뿐더러 최단경로를 찾는 이점 또한 활용할 수 없기에 DFS를 사용했다.</p>
<p>그래프탐색이론 리마인드 해보자면
DFS는 이렇게 무지성으로 한가지 루트만 저장하면서 탐색하면 되지만</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/2b635a59-2f9f-4e8b-873d-d72f1f61843d/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/57eef2aa-041e-4786-afbe-d60c6ab7dcca/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/405c1267-5479-44af-ac17-e51aad6c658c/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>BFS는 모든 경로를 저장하면서 탐색하기 때문에 최단경로를 바로 찾을 수 있는 장점이 있지만 메모리를 많이 차지한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/ea422dc0-e5d2-4460-9128-98f679b72fb2/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/593a1c1d-40b7-4a18-9791-5b256a84b10e/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/af4d819a-cec4-47b4-8f36-63e087aeb12b/image.png" alt=""></th>
</tr>
</thead>
</table>
<h1 id="첫번째-시도">첫번째 시도</h1>
<pre><code class="language-python">import sys
&quot;&quot;&quot;
DFS (시간초과)
&quot;&quot;&quot;
N = int(sys.stdin.readline())
graph = list()
for _ in range(N):
    graph.append(list(map(int, sys.stdin.readline().split())))
stack = list()
answer = 0
stack.append((0, 0))
while stack:
    y, x = stack.pop()
    dist = graph[y][x]
    if y == N-1 and x == N-1:
        answer += 1
        continue
    for dir in [(1, 0), (0, 1)]:
        tar_y, tar_x = y + (dist * dir[0]), x + (dist * dir[1])
        if tar_y &gt;= N or tar_x &gt;= N:
            continue
        stack.append((tar_y, tar_x))
print(answer)
</code></pre>
<p>스택을 이용한 매우 정석적인 DFS 로 모든 경우의 수를 완전탐색, 목적지에 도착하면 카운트를 1 증가시켰다. 딱히 뭐 설명할건 없는거같다.
조금 다른점이라면 <u>이동방향이 아래, 우측방향 두가지 라는점.</u></p>
<h1 id="두번째-시도">두번째 시도</h1>
<p>조금 더 효율적인 풀이를 해보자. (사실 DP 문제로 분류되어있기 때문에 이게 정석이긴 하다.)</p>
<p>일반적인 그래프 탐색에서는 모든 방향으로 이동이 가능하기 때문에 DFS나 BFS를 사용해서 이동경로를 메모리에 저장해두어야 한다. 
그러나 이 문제에서는 이동경로가 딱 두 가지 케이스로 고정되는 특징이 존재한다. 
그 말의 뜻은 A -&gt; B로 이동한다고 했을 때 B 입장에서는 들어오는 방향이 딱 두 가지 케이스로 정의되고 그 두 가지 경우만 체크해보면 되기 때문에 굳이 이동방향을 메모리에 저장해두지 않아도 된다.
즉, B 입장에서 위에서 들어오는경우, 왼쪽에서 들어오는 경우 두 가지로 점화식을 세울 수 있다는 뜻.</p>
<ul>
<li>dp배열은 graph와 똑같은 모양을 하고 graph의 각 칸에 도달할 수 있는 경우의 수를 저장한다.</li>
<li>나머지는 0, (0, 0) 시작점을 1로 초기화한다. (모두 0인 상태에서 항상 시작점부터 시작하므로)</li>
<li>좌측에서 우측, 위에서 아래 방향으로 순서대로 보면서,현재 시점에서 갈 수 있는 목적지 두 곳에 경우의 수를 더해나간다.</li>
</ul>
<pre><code class="language-python">&quot;&quot;&quot;
DP (정해)
&quot;&quot;&quot;
N = int(sys.stdin.readline())
graph = list()
dp = [[0 for i in range(N)] for j in range(N)]
for _ in range(N):
    graph.append(list(map(int, sys.stdin.readline().split())))
dp[0][0] = 1

# 이동방향이 좌측에서 우측, 상단에서 하단으로 고정인 특징이 있으므로, 
# 가장 상단부터 우측방향으로 차례대로 훑고 지나가도 문제가 없다.
for y in range(N):
    for x in range(N):
        if y == N-1 and x == N-1:
            continue    # 마지막 목적지의 값이 0이면 무한루프를 돌기 때문에 빠져나와준다.
        dist = graph[y][x]
        for dir in [(1, 0), (0, 1)]:
            tar_y, tar_x = y + (dist * dir[0]), x + (dist * dir[1])
            if tar_y &gt;= N or tar_x &gt;= N:
                continue
            dp[tar_y][tar_x] += dp[y][x]
print(dp[-1][-1])</code></pre>
<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/22964157-806b-46e2-b83d-5c22d596266a/image.png" alt="">
DFS로 풀었을 때 테스트케이스는 통과했지만 결과적으로 시간초과가 나왔다. 
DFS의 시간복잡도는 O(V+E)인데, 이 그래프는 정점의 수가 $N^2$개이고 각 정점마다 2개의 노드가 존재하므로 $V = N^2, E = 2\times N^2$ 가 된다.
DP의 경우 2차원 배열을 한번만 훑으면 되므로 시간복잡도는 $O(N^2)$가 된다.
DFS역시 $V+E = 3N^2$이고 시간복잡도에서 상수는 생략하므로 똑같이 $O(N^2)$라고 볼 수 있찌만 실제로는 DP에 비해 같은 반복문이 3배로 돌아가기 때문에 시간초과가 나는것 같다.</p>
<blockquote>
<p><strong>배운점</strong>
그래프 탐색 문제라도 이동의 규칙이 있고 제한시간이 부족한 경우라면 DP형식으로 풀 수 있는지 고려해보자.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[BOJ_1106) 호텔]]></title>
            <link>https://velog.io/@renovatio_hyuns/BOJ1106-%ED%98%B8%ED%85%94</link>
            <guid>https://velog.io/@renovatio_hyuns/BOJ1106-%ED%98%B8%ED%85%94</guid>
            <pubDate>Mon, 31 Jul 2023 18:16:26 GMT</pubDate>
            <description><![CDATA[<p>방법만 알면 의외로 깔끔하게 풀리는 DP 문제이다.
<a href="https://www.acmicpc.net/problem/1106">https://www.acmicpc.net/problem/1106</a>
<img src="https://velog.velcdn.com/images/renovatio_hyuns/post/6416bc7e-dcf7-40b9-96f6-14dab4672939/image.png" alt=""></p>
<p>목표 고객수를 달성하기 위해 0명부터 순차적으로 고객 수를 달성하기 위해 홍보에 필요한 최소 비용을 저장해 나가는 바텀업 방식이 적합하다 생각했고 이를 위해 C만큼의 크기를 가지는 dp 배열을 만들었다.</p>
<ul>
<li>dp의 각 인덱스는 홍보를 통해 달성한 호텔의 고객의 수를 의미하며</li>
<li>배열 안의 값은 해당 인덱스 (고객 수)를 달성하기 위한 최소 비용이 저장된다.</li>
<li>인덱스 0부터 C까지 순서대로 훑어가는데<ul>
<li>매 인덱스마다 모든 홍보를 한 번씩 시도해본다.</li>
<li>현재 고객 수에서 각 홍보를 했을 때 달성될 고객 수를 바라보는데 이 때 더 작은 금액이 저장되도록 dp 배열을 업데이트한다.</li>
<li>홍보를 진행해도 목표고객수(C) 이전이라면 해당 목표 인덱스를 업데이트하고, 홍보를 진행했을 때 목표고객수를 넘어선다면 가장 마지막 인덱스를 업데이트한다.<h1 id="첫-번째-시도">첫 번째 시도</h1>
처음에 생각없이 dp값들을 0으로 초기화 해서 삽질을 좀 했다.<pre><code class="language-python">import sys
</code></pre>
</li>
</ul>
</li>
</ul>
<p>C, N = map(int, sys.stdin.readline().split())
dp = [0] * (C+1)
ad = list()
for _ in range(N):
    cost, inc = map(int, sys.stdin.readline().split())
    ad.append((cost, inc))
    # 0명인 상태에서 딱 한 번씩 홍보한 결과로 dp를 초기화한다.
    if inc &lt; C:
        dp[inc] = cost
    else:
        dp[-1] = min(cost, dp[-1]) if dp[-1] &gt; 0 else cost
for i in range(C+1):
    for cost, inc in ad:
        if dp[i] &gt; 0:    # dp[i] 가 0인 경우는 홍보가 이루어지지 않은 상태이므로 제외한다.
            if i+inc &lt; C:
                # 바라보는 dp값이 0이면 그냥 그대로 값을 대입하고, 이미 값이 저장되어 있으면 min값으로 업데이트한다.
                dp[i+inc] = min(dp[i+inc], dp[i] + cost) if dp[i+inc] &gt; 0 else dp[i] + cost
            else:
                dp[-1] = min(dp[-1], dp[i] + cost) if dp[-1] &gt; 0 else dp[i] + cost
print(dp[-1])</p>
<pre><code>내가 보기엔 큰 로직의 문제가 없는데 똑왜틀에 빠졌다. (똑같은데 왜틀려!)
게시판에 있는 모든 반례가 다 통과하는데도 틀렸습니다가 떠서 그 이유를 파악하지 못했다. 
음... 일단 내가 파악한 이 코드의 문제점은 궁극적으로 dp값을 min값으로 업데이트 해야하는 문제인데 처음에 0으로 초기화를 했다는게 가장 큰 실수였던것 같다.  

별다른 예외케이스를 고려하지않으면 0 때문에 min값이 제대로 업데이트 되지 않고 항상 0으로 고정되는 문제가 생겼기 때문에 0인경우는 어떻게~ 0이 아닌경우는 어떻게~ 와 같은 예외처리를 많이 넣게 되었다.
아마 이 부분 어딘가에서 예외를 제대로 처리하지 못해서 틀린것 같은데 도저히 찾지 못하겠다.

# 두 번쨰 시도
```python
import sys

C, N = map(int, sys.stdin.readline().split())
dp = [sys.maxsize] * (C+1)
ad = list()
dp[0] = 0
for _ in range(N):
    cost, inc = map(int, sys.stdin.readline().split())
    ad.append((cost, inc))

for i in range(C+1):
    for cost, inc in ad:
        if i+inc &lt; C:
            dp[i+inc] = min(dp[i+inc], dp[i] + cost) 
        else:
            dp[-1] = min(dp[-1], dp[i] + cost)
print(dp[-1])</code></pre><p>dp를 0이 아닌 maxsize로 초기화했다.
이러면 min 값으로 업데이트 하는데 아무런 제약이 걸리지 않으므로 고려해야 할 것이 사라진다.
딱 하나 시작점(고객 수가 0인 지점)만 0으로 초기화 해주면 된다.
왜냐하면 (현재시점 가격 + 홍보를 진행했을 때 변동가격)을 가지고 dp 안의 정보와 비교하여 min으로 업데이트 해야 하기 때문에 시작점만 0으로 해야 제대로된 홍보가격 연산이 이루어지기 때문이다. (시작지점도 maxsize면 홍보를 진행했을 때 가격을 구하는 과정에서 오버플로가 난다.)</p>
<blockquote>
<p><strong><em>오늘의 교훈</em></strong>
max를 구하는 문제면 0으로 초기화 하고
min을 구하는 문제면 maxsize로 초기화 하자.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android serving을 위한 pth to tflite convert]]></title>
            <link>https://velog.io/@renovatio_hyuns/Android-serving%EC%9D%84-%EC%9C%84%ED%95%9C-pth-to-tflite-convert</link>
            <guid>https://velog.io/@renovatio_hyuns/Android-serving%EC%9D%84-%EC%9C%84%ED%95%9C-pth-to-tflite-convert</guid>
            <pubDate>Thu, 20 Jul 2023 16:40:42 GMT</pubDate>
            <description><![CDATA[<h1 id="versioning">Versioning</h1>
<pre><code>python==3.8.5
torch==1.7.1
onnx==1.7.0
onnx_tf==1.6.0
tensorflow==2.2.0</code></pre><p><strong>versioning guide</strong></p>
<p><a href="https://github.com/onnx/onnx-tensorflow/blob/main/Versioning.md">https://github.com/onnx/onnx-tensorflow/blob/main/Versioning.md</a> </p>
<p>위 readme를 참고해서 버전을 맞추었습니다.</p>
<p>onnx가장 최신버전을 사용하는게 좋아보이는데, 제가 모델을 개발한 torch 버전에서 가장 높게 사용가능한 onnx 가 1.7.0 이어서 1.7.0 사용했습니다.</p>
<h1 id="pth-to-onnx">.pth to .onnx</h1>
<pre><code class="language-python">&quot;&quot;&quot;
.pth to .onnx
&quot;&quot;&quot;
import torch
from model import EAST
import onnx
from onnxsim import simplify

pth_path = &quot;{model_directory_path}/best.pth&quot;
onnx_path = &#39;{target_directory_path}/best.onnx&#39;


model = CustomModel()
model.load_state_dict(torch.load(pth_path, map_location=&#39;cpu&#39;))
model.eval()

# 모델을 ONNX로 변환
input_sample = torch.randn(1, 3, 1024, 1024)  # 입력 샘플 생성
torch.onnx.export(model, input_sample, onnx_path, opset_version=11)

# load your predefined ONNX model
model = onnx.load(onnx_path)

# convert model
model_simp, check = simplify(model)

assert check, &quot;Simplified ONNX model could not be validated&quot;

# use model_simp as a standard ONNX model object
onnx.save(model_simp, &quot;best_simplified.onnx&quot;)</code></pre>
<p>pytorch는 모델의 가중치만 따로 저장해서 모델에 load하여 사용하기 때문에 그래프의 형태가 pth 파일에 존재하지 않습니다. 따라서 pytorch에서 onnx모델로 export 하기 위해서는 입력 shape에 맞는 sample input을 넣어주어야 합니다.</p>
<p>또한 opset_version은 사용하는 onnx 라이브러리의 버전에 맞는 opset_version을 넣어주어야 합니다. </p>
<p>simplify(model)을 통하여 onnx변환된 모델을 좀 더 단순하게 줄일 수 있습니다.</p>
<h1 id="onnx-to-pb-frozengraph">.onnx to .pb (frozenGraph)</h1>
<pre><code class="language-python">&quot;&quot;&quot;
.onnx to .pb
&quot;&quot;&quot;
import onnx 
import torch
from onnx_tf.backend import prepare

onnx_model_path = &quot;{onnx_model_directory}/best.onnx&quot;
tf_model_path = &quot;{target_directory}/best.pb&quot;

onnx_model = onnx.load(onnx_model_path)

tf_rep = prepare(onnx_model)

tf_rep.export_graph(tf_model_path)</code></pre>
<p>onnx모델을 <strong>frozenGraph형식의 단일 .pb 파일로 변환합니다.</strong></p>
<h1 id="pb-to-tflite">.pb to .tflite</h1>
<pre><code class="language-python">&quot;&quot;&quot;
.pb to .tflite
&quot;&quot;&quot;
import tensorflow as tf

# pb 모델 경로
pb_model_path = &quot;{pb_model_directory}/best.pb&quot;
# TFLite 모델 저장 경로
tflite_model_path = &quot;{target_directory}/best.tflite&quot;

# TensorFlow Lite 포맷으로 모델 변환
converter = tf.compat.v1.lite.TFLiteConverter.from_frozen_graph(pb_model_path, #TensorFlow freezegraph .pb model file
                                                      input_arrays=[&#39;input.1&#39;], # name of input arrays as defined in torch.onnx.export function before.
                                                      output_arrays=[&#39;268&#39;, &#39;257&#39;]  # name of output arrays defined in torch.onnx.export function before.
                                                      )

converter.optimizations = [tf.lite.Optimize.DEFAULT]    # 최적화
tflite_model = converter.convert()    # tflite로 변환

# TensorFlow Lite 모델 저장
with open(tflite_model_path, &#39;wb&#39;) as f:
    f.write(tflite_model)</code></pre>
<p>input_arrays와 output_arrays에는 입출력 노드의 이름을 적어주어야 합니다. </p>
<p>각 노드의 이름을 모를 경우에는 <a href="https://netron.app/">netron</a>과 같은 모델 시각화 툴을 사용해서 확인할 수 있습니다.</p>
<p>위 링크를 타고 netron에 변환 전의 pb 모델을 업로드하여 입출력 노드의 이름을 확인하고 input_arrays와 output_arrays 파라미터에 배열로 넣어주시면 됩니다.</p>
<h1 id="validation">Validation</h1>
<p>모델 변환이 완료되었으면, 제대로 변환이 되었는지 입력을 넣어서 출력을 확인해보는 작업이 필요합니다.</p>
<pre><code class="language-python">&quot;&quot;&quot;
torch model validation
&quot;&quot;&quot;
import torch
from model import EAST
import numpy as np

# Load the .pth file
pth_path = &quot;/opt/ml/input/code/trained_best/best.pth&quot;
model = EAST()
model.load_state_dict(torch.load(pth_path, map_location=&#39;cpu&#39;))
# model = torch.load(&quot;/opt/ml/input/code/trained_medical_finanace_6000_gaussian/latest.pth&quot;)
model.eval()  # Set the model to evaluation mode

# Prepare input data
input_data = torch.full((1, 3, 1024, 1024), 0.5)  # Example input data

# Make predictions
with torch.no_grad():
    output = model(input_data)

# Print the output
print(output[0].type())
print(torch.max(output[0]), torch.min(output[0]))
print(torch.max(output[1]), torch.min(output[1]))
&quot;&quot;&quot;
tensorflow model validation
&quot;&quot;&quot;
import tensorflow as tf
import numpy as np

# FrozenGraph 모델 경로
frozen_graph_path = &#39;/opt/ml/input/code/trained_best/best.pb&#39;

# TensorFlow 2.x에서 FrozenGraph 로드
with tf.io.gfile.GFile(frozen_graph_path, &quot;rb&quot;) as f:
    graph_def = tf.compat.v1.GraphDef()
    graph_def.ParseFromString(f.read())

# 로드한 모델을 기반으로 TensorFlow 그래프 생성
with tf.Graph().as_default() as graph:
    tf.import_graph_def(graph_def, name=&quot;&quot;)

# 모델 실행
with tf.compat.v1.Session(graph=graph) as sess:
    # 입출력 노드 정의
    input_tensor = graph.get_tensor_by_name(&quot;input.1:0&quot;)  # 입력 텐서의 이름
    # input_tensor = tf.compat.v1.placeholder(tf.float32, shape=(1, 3, 1024, 1024))  # 입력 텐서 생성
    output_tensor1 = graph.get_tensor_by_name(&quot;268:0&quot;)  # 첫 번째 출력 텐서의 이름
    output_tensor2 = graph.get_tensor_by_name(&quot;257:0&quot;)  # 두 번째 출력 텐서의 이름

    # 입력 데이터
    input_data = np.full((1, 3, 1024, 1024), 0.5).astype(np.float32)
    output_data1, output_data2 = sess.run([output_tensor1, output_tensor2], feed_dict={input_tensor: input_data})

    print(np.max(output_data1), np.min(output_data1))
    print(np.max(output_data2), np.min(output_data2))

&quot;&quot;&quot;
tflite model validation
&quot;&quot;&quot;
import tensorflow as tf
import numpy as np

# TFLite 모델 파일 경로
model_path = &quot;/opt/ml/input/code/trained_best/best.tflite&quot;

# TFLite 모델 로드
interpreter = tf.lite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

# 입력 텐서와 출력 텐서 정보 가져오기
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# 입력 데이터 생성
input_data = np.full((1, 3, 1024, 1024), 0.5).astype(np.float32)

# 입력 데이터 설정
interpreter.set_tensor(input_details[0][&#39;index&#39;], input_data)

# 모델 실행
interpreter.invoke()

# 출력 데이터 가져오기
output_data1 = interpreter.get_tensor(output_details[0][&#39;index&#39;])
output_data2 = interpreter.get_tensor(output_details[1][&#39;index&#39;])

print(np.max(output_data1), np.min(output_data1))
print(np.max(output_data2), np.min(output_data2))</code></pre>
<p>.pth, .pb, .tflite 모델을 각각 load 하여 동일한 입력을 넣고 출력결과를 확인합니다.</p>
<p>결과는 아래와 같습니다.</p>
<pre><code># .pth 모델
tensor(125.5052) tensor(-0.2398)
tensor(0.0046) tensor(4.0554e-05)

# .pb 모델
125.50522 -0.23980959
0.004594922 4.0555362e-05

#.tflite 모델
124.553635 -0.23529902
0.0049833357 4.3301996e-05</code></pre><p>pth와 pb 모델은 어느정도 비슷한 출력이 나오는데 tflite에서 오차가 조금 있어 보이네요. </p>
<p>나중에 각 framework에 대한 이해가 좀 더 생기면 분석해보면 좋을것 같습니다. 그래도 비슷하게 출력이 나오는것을 보니 변환이 어느정도 잘 되었다 결론짓고 마무리하도록 하겠습니다.</p>
<p>혹시라도 변환과정에서 어려움을 겪는분 혹은 미래의 저를 위해 과정을 깔끔하게 정리해둔 글인데, 이 과정에서 겪은 각종 시행착오는 삽질 카테고리에 따로 작성해두었습니다.
<a href="https://velog.io/@renovatio_hyuns/.pth-.tflite-%EB%A1%9C%EC%9D%98-%EC%97%AC%EC%A0%95">https://velog.io/@renovatio_hyuns/.pth-.tflite-%EB%A1%9C%EC%9D%98-%EC%97%AC%EC%A0%95</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[.pth -> .tflite 로의 여정]]></title>
            <link>https://velog.io/@renovatio_hyuns/.pth-.tflite-%EB%A1%9C%EC%9D%98-%EC%97%AC%EC%A0%95</link>
            <guid>https://velog.io/@renovatio_hyuns/.pth-.tflite-%EB%A1%9C%EC%9D%98-%EC%97%AC%EC%A0%95</guid>
            <pubDate>Thu, 20 Jul 2023 16:36:15 GMT</pubDate>
            <description><![CDATA[<h1 id="변환-과정">변환 과정</h1>
<p>pth -&gt; onnx -&gt; pb -&gt; tflite 순서로 변환을 하였다.</p>
<p>pth -&gt; onnx 과정은 매우 순조로웠다. torch에 내장된 torch.onnx.export 함수에 내가 사용할 onnx 버전에 맞는 opset_version 변수만 잘 넣어줘서 변환에 성공했다.</p>
<p>onnx -&gt; pb 과정에서 약간의 고생을 하긴 했지만 다시 생각해보면 크게 어려운점은 없었던것 같다.</p>
<ul>
<li>onnx : onnx 모델을 load</li>
<li>onnx_tf : load된 onnx 모델을 tensorflow 모델로 변환</li>
</ul>
<p>위 두 개의 라이브러리를 사용했는데, onnx_tf 를 import 하는 과정에서 versioning 이 맞지 않아 import error가 났었다. 그래서 호환되는 버전을 잘 확인해서 설치하여 해결하였다.</p>
<h1 id="나에게-닥친-시련">나에게 닥친 시련</h1>
<p>문제는 pb에서 tflite 모델로 변환할 때 발생하였다.</p>
<pre><code class="language-python"># pb 모델 경로
pb_model_path = &quot;/opt/ml/input/code/trained_best/best.pb&quot;
# TFLite 모델 저장 경로
tflite_model_path = &quot;/opt/ml/input/code/trained_best/best.tflite&quot;

# Converting a GraphDef from session.
converter = tf.compat.v1.lite.TFLiteConverter.from_session(
  sess, in_tensors, out_tensors)
tflite_model = converter.convert()
open(&quot;converted_model.tflite&quot;, &quot;wb&quot;).write(tflite_model)</code></pre>
<p>위와 같이 pb모델이 저장된 파일경로를 지정해서 변환을 시도했는데 내가 지정해준 pb_model_path에 해당하는 경로가 아닌 이상한 경로가 뜨면서 savedModel 이 없다고 에러가 났다.</p>
<pre><code>File &quot;/opt/conda/lib/python3.8/site-packages/tensorflow/python/saved_model/loader_impl.py&quot;, line 110, in parse_saved_model
    raise IOError(&quot;SavedModel file does not exist at: %s/{%s|%s}&quot; %
OSError: SavedModel file does not exist at: /opt/ml/input/code/trained_medical_finanace_6000_gaussian/latest.pb/{saved_model.pbtxt|saved_model.pb}</code></pre><p>아니 나는 분명 /opt/ml/input/code/trained_medical_finanace_6000_gaussian/latest.pb 까지만 해서 saved_model path를 지정해서 넘겼는데 자꾸 내가 지정해준 경로 뒤에 뭐 이상한 경로가 추가되서 /opt/ml/input/code/trained_medical_finanace_6000_gaussian/latest.pb/{saved_model.pbtxt|saved_model.pb}</p>
<p>이런 경로에 saved model 이 없다고 떠들어댔다. 그래서 loader_impl.py 구현체 부분을 따고 들어가봤더니 이런 코드가 있었다.</p>
<pre><code class="language-python">saved_model = saved_model_pb2.SavedModel()
  if file_io.file_exists(path_to_pb):
    try:
      file_content = file_io.FileIO(path_to_pb, &quot;rb&quot;).read()
      saved_model.ParseFromString(file_content)
      return saved_model
    except message.DecodeError as e:
      raise IOError(&quot;Cannot parse file %s: %s.&quot; % (path_to_pb, str(e)))
  elif file_io.file_exists(path_to_pbtxt):
    try:
      file_content = file_io.FileIO(path_to_pbtxt, &quot;rb&quot;).read()
      text_format.Merge(file_content.decode(&quot;utf-8&quot;), saved_model)
      return saved_model
    except text_format.ParseError as e:
      raise IOError(&quot;Cannot parse file %s: %s.&quot; % (path_to_pbtxt, str(e)))
  else:
    raise IOError(&quot;SavedModel file does not exist at: %s/{%s|%s}&quot; %
                  (export_dir,
                   constants.SAVED_MODEL_FILENAME_PBTXT,
                   constants.SAVED_MODEL_FILENAME_PB))</code></pre>
<p>주목해야할 부분은 맨 마지막 else부분의 raise IOError 부분.</p>
<p>if문에서 path_to_pb에 적절한 파일이 있는지 체크해서 없으면 해당 에러를 뱉어내게 구현이 되어 있었는데 더 위쪽을 찾아보니까 구현체 코드 내에서 <strong>내가 지정해서 넘겨준 파일경로 export_dir에 뭘 추가해서 새로운 경로로 바꿔버리는 코드가 있더라.</strong></p>
<pre><code class="language-python"># Build the path to the SavedModel in pbtxt format.
  path_to_pbtxt = os.path.join(
      compat.as_bytes(export_dir),
      compat.as_bytes(constants.SAVED_MODEL_FILENAME_PBTXT))
  # Build the path to the SavedModel in pb format.
  path_to_pb = os.path.join(
      compat.as_bytes(export_dir),
      compat.as_bytes(constants.SAVED_MODEL_FILENAME_PB))</code></pre>
<p>그래서 저거 join하는부분 싹다 지워버리고 내가 지정해준 export_dir 경로 그대로 들어가게 한다음 돌렸는데 무슨 saved Model 형식이 아니라고 에러가 났다. 이때 부터 슬슬 빡이 치기 시작. <strong>아니 .pb 모델로 저장해서 경로도 이제 제대로 들어가게 고쳤는데 왜 savedModel이 아니라는거지?</strong></p>
<p>그래서 savedModel이 뭔지 좀 찾아봤다. 그 결과 알게된것  -&gt;  .pb 형식의 파일은 savedModel형식인 경우와 아닌경우 두가지로 존재할 수 있는데</p>
<ul>
<li><strong>SavedModel</strong> : TensorFlow 2.x 버전부터 도입된 모델 저장 형식으로 모델의 가중치, 그래프 구조, 변수, 연산 등을 포함하는 디렉토리 형태로 저장됨. 그러니까 SavedModel은 saved_model.pb 파일과 해당 디렉토리 안에 있는 변수 및 리소스 파일들로 구성된거다.</li>
<li><strong>FrozenGraph</strong> : TensorFlow 1.x 버전에서 주로 사용되던 모델 저장 형식으로 단일 .pb파일로 구성되고  TensorFlow 그래프 구조와 가중치를 포함하고 있으며, 일반적으로 Protobuf 형식으로 직렬화되어 있다.</li>
</ul>
<p>아!!!! 내가 저장한 방식은 FrozenGraph였는데, 위에서 SavedModel 형식을 tflite 파일로 바꾸려는 코드를 사용했기 때문에 에러가 났나보다. 그래서 export_dir에 saved_model이 저장되어있는 디렉토리 경로를 주면 라이브러리 구현체 안에서 추가적으로 경로 수정해서 파일 참조하게 작성되어 있던것이다. 이제 퍼즐이 좀 맞춰진다.</p>
<p>아 그럼 frozenGraph를 tflite로 변환하는 코드를 찾아봐야겠네.</p>
<p>구글링 했었을 때 죄다 from_session이나 from_saved_model 과 같은 함수만 나와서 삽질을 했었는데 </p>
<p><a href="https://www.tensorflow.org/api_docs/python/tf/compat/v1/lite/TFLiteConverter">https://www.tensorflow.org/api_docs/python/tf/compat/v1/lite/TFLiteConverter</a></p>
<p>docs에서 찾아보니까 <a href="https://www.tensorflow.org/api_docs/python/tf/compat/v1/lite/TFLiteConverter#from_frozen_graph">from_frozen_graph</a> 라는 함수가 있었다!!</p>
<h3 id="frozen_graph-로부터-tflite로-변환하는-코드">frozen_graph 로부터 tflite로 변환하는 코드</h3>
<pre><code class="language-python"># TensorFlow Lite 포맷으로 모델 변환
converter = tf.compat.v1.lite.TFLiteConverter.from_frozen_graph(&#39;/opt/ml/input/code/trained_medical_finanace_6000_gaussian/latest.pb&#39;, #TensorFlow freezegraph .pb model file
                                                      input_arrays=[&#39;input.1&#39;], # name of input arrays as defined in torch.onnx.export function before.
                                                      output_arrays=[&#39;268&#39;, &#39;257&#39;]  # name of output arrays defined in torch.onnx.export function before.
                                                      )

converter.optimizations = [tf.lite.Optimize.DEFAULT]    # 최적화 시키기
tflite_model = converter.convert()

# TensorFlow Lite 모델 저장
with open(tflite_model_path, &#39;wb&#39;) as f:
    f.write(tflite_model)</code></pre>
<p>이 때 파라미터로 input_arrays와 output_arrays를 넣어주어야 하는데 이게 뭐냐면 tensorflow에는 각 노드마다 이름이 붙어있다. 그래서 입력노드와 출력노드에 해당하는 이름을 적어주어야 하는데, 내가 첨부터 tensorflow로 모델을 짠게 아니라 pytorch로 짠 모델을 강제로 변환했으니 노드이름을 알 수 가 없다.</p>
<p>그래서 <a href="https://netron.app/">netron</a> 사이트에서 .pb 모델 올려서 시각화 한다음에 이름을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/renovatio_hyuns/post/9dc54c17-0f62-472b-9ad3-6c182f90c94b/image.png" alt="">
이렇게 왼쪽에 시각화된 그래프의 입출력 혹은 각 노드들을 누르면 오른쪽에 name으로 노드 이름이 나온다.</p>
<h2 id="그런데-왜-savedmodel이-아니라-frozengraph로-변경된거지">그런데 왜 savedModel이 아니라 frozenGraph로 변경된거지?</h2>
<p>이것도 좀 궁금해서 찾아봤는데 솔직히 못찾았다 ㅠㅠ 확실친 않지만 대충 파악한 바로는 onnx와 tensorflow간의 이동을 할 수 있게 해주는 라이브러리가 두 개가 있는데</p>
<ol>
<li>onnx_tf : onnx모델을 tensorflow 1.x버전의 모델(frozenGraph)로 바꾸는 라이브러리</li>
<li>tf2onnx : tensorflow 2.x 버전의 모델(savedModel)을 onnx모델로 바꾸는 라이브러리</li>
</ol>
<p>나는 onnx_tf를 썼는데 이게 1.x 버전의 모델인 frozenGraph로 바꾸는건지 모르고 그냥 썼다. 아니 frozenGraph니 savedModel이니 그런거를 몰랐다 애초에. 아니 같은 .pb 파일인데 다른방식인게 말이 되냐고 진짜 :&lt;</p>
<p>그럼 tf2onnx로 역변환 하면 onnx를 savedModel로 바꿀 수 있는거 아닌가? 싶어서 찾아보긴 했는데 역변환은 안되는것 같다. 그래서 그냥 frozenGraph -&gt; tflite로 노선 최종변경해서 결국 tflite 뽑는데 성공했다.</p>
<p><a href="https://github.com/onnx/onnx-tensorflow/blob/main/Versioning.md">https://github.com/onnx/onnx-tensorflow/blob/main/Versioning.md</a> </p>
<p>아니근데 이건 뭔데 하... 이거보고 tensorflow 2.x 버전으로 맞춰서 한건데 뭐지..?</p>
<p>이 포스팅은 TroubleShooting 을 다룬 글이므로 pth 부터 tflite로 변환하는 과정을 깔끔하게 정리한 글을 보고싶으신 분은 아래 링크를 타고 확인해주세요</p>
<p><a href="https://velog.io/@renovatio_hyuns/Android-serving%EC%9D%84-%EC%9C%84%ED%95%9C-pth-to-tflite-convert">https://velog.io/@renovatio_hyuns/Android-serving%EC%9D%84-%EC%9C%84%ED%95%9C-pth-to-tflite-convert</a></p>
]]></description>
        </item>
    </channel>
</rss>