<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>moonliam_.log</title>
        <link>https://velog.io/</link>
        <description>"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다. </description>
        <lastBuildDate>Sun, 31 Mar 2024 14:48:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>moonliam_.log</title>
            <url>https://velog.velcdn.com/images/moonliam_/profile/338774c5-994d-485c-b5cc-3572475c7d58/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. moonliam_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/moonliam_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] Compose에서 State 생성 방법]]></title>
            <link>https://velog.io/@moonliam_/Android-Compose%EC%97%90%EC%84%9C-State-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@moonliam_/Android-Compose%EC%97%90%EC%84%9C-State-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 31 Mar 2024 14:48:48 GMT</pubDate>
            <description><![CDATA[<h2 id="state란">State란?</h2>
<p><strong>Jetpack Compose</strong>에서는 UI의 상태가 변했음을 인식하기 위해 <code>State</code>를 사용한다.</p>
<p><code>Composable UI</code>가 특정 변수를 <code>State</code>로 인식하기 위해서는 <code>mutableStateOf</code> 같은 State Object로 감싸주면된다.</p>
<p>State Object로 활용하는 방법에는 <code>mutableStateOf</code> 말고도 여러가지 방법이 있다. 이번 포스트에서는 각 방법들에 대해 알아보고 어떤 차이점이 있는지 알아보도록 하겠다.</p>
<h3 id="livedata">LiveData</h3>
<p>기존의 MVVM 패턴에서 <code>ViewModel</code>이 갖고있는 데이터를 UI Layer에서 관찰하기 위해서는 <code>LiveData</code>를 사용했다.</p>
<p>하지만 <code>State</code>, <code>MutableState</code> 가 Compose에 좀 더 특화되어있기 때문에 Compose로 UI를 구성한 프로젝트에서는 <code>ViewModel</code>을 사용할 때 <code>State</code>를 사용한다.</p>
<p>물론 Compose로 구성된 프로젝트에서도 <code>LiveData</code>를 사용할 수는 있다. 하지만 이를 Composable에서 State로 인식하기 위해서는 <code>observeAsState</code> 메소드를 통해 State로 변환해주어야한다.</p>
<pre><code>class TermModel : ViewModel() {

    private val termList = mutableStateListOf&lt;Term&gt;()
    private val termListLiveData: MutableLiveData&lt;List&lt;Term&gt;&gt; = MutableLiveData()

    ...
}

@Composable
fun test(){
   val list by termModel.termListLiveData.observeAsState(listOf())
   LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .background(Color(0xDE, 0xDE, 0xDE, 0xFF))
                .clickable(onClick = {
                   //when i click the whole list, it delete the last element of list, and then automatically update view
                   list.deleteLast()
            }),
            verticalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            items(list) {
                Text(text = it)
            }
        }
}</code></pre><h3 id="mutablestateof">MutableStateOf</h3>
<p><code>mutableStateOf</code>는 Observable한 <code>MutableState&lt;T&gt;</code>를 생성해준다. 이 <code>MutableState&lt;T&gt;</code> 값이 변경될 때마다 컴포저블 함수가 Recompose 된다.</p>
<p>특정한 값 그 자체를 <code>State</code>로 쓰고 싶을때 사용할 수 있다. </p>
<pre><code>class NameViewModel : ViewModel() {
    var name by mutableStateOf&lt;String&gt;()
}

@Composable
fun NameScreen() {
    Text(
        text = viewModel.name
    )
}</code></pre><p><code>MutableState</code>를 선언하는 방법은 위의 예시 외에도 총 3가지가 있다.</p>
<ul>
<li><code>val mutableState = remember { mutableStateOf(default) }</code></li>
<li><code>var value by remember { mutableStateOf(default) }</code></li>
<li><code>val (value, setValue) = remember { mutableStateOf(default) }</code></li>
</ul>
<h3 id="mutablestateflow">MutableStateFlow</h3>
<p><code>StateFlow</code>, <code>MutableStateFlow</code>는 위의 <code>MutableStateOf</code>와는 조금 다르게 Kotlin Coroutine의 <code>Flow</code>를 State로 사용하기 위한 API이다.</p>
<p>일반적인 Flow와 차이점은 <code>StateFlow</code>는 <strong>Hot</strong> Stream이라는 것이다. 즉, <code>collector</code>의 존재여부에 상관없이 독립적으로 값을 방출한다.</p>
<p>그리고 또 다른 점은 절대 <code>complete</code>이 일어나지 않는다는 점이다. </p>
<pre><code>class CounterModel {
    private val _counter = MutableStateFlow(0) // private mutable state flow
    val counter = _counter.asStateFlow() // publicly exposed as read-only state flow

    fun inc() {
        _counter.update { count -&gt; count + 1 } // atomic, safe for concurrent use
    }
}</code></pre><p><code>StateFlow</code>, <code>MutableStateFlow</code>를 Compose에서 State로 활용하기 위해서는 <code>collectAsState</code> 또는 <code>collectAsStateWithLifecycle</code> 메소드로 변환해줘야한다.</p>
<p>이때 생명주기에 따른 데이터 손실을 방지하기 위해서 <code>collectAsStateWithLifecycle</code>을 사용하는 게 좀 더 바람직하다.
대신 <code>collectAsState</code>는 platform-agnostic한 코드에서 사용하면 된다. (<code>collectAsStateWithLifecycle</code>은 안드로이드 플랫폼 한정에서만 동작한다.)</p>
<h3 id="rxjava-to-compose-state">RxJava to Compose State</h3>
<p><strong>RxJava</strong>도 Compose <code>State</code>로 변환할 수 있다. <code>subscribeAsState()</code> 메소드로 변환해주면 된다.</p>
<p><strong>레퍼런스</strong></p>
<blockquote>
<ol>
<li><a href="https://developer.android.com/develop/ui/compose/state">안드로이드 공식 문서 State and Jetpack Compose</a></li>
<li><a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/">코틀린 공식 문서 StateFlow</a></li>
<li><a href="https://developer88.tistory.com/entry/mutableStateOf-%EC%99%80-MutableStateFlow-%EB%B9%84%EA%B5%90-%EC%B4%9D%EC%A0%95%EB%A6%AC-collectAsState">mutableStateOf와 MutableStateFlow 비교 총 정리 #collectAsState</a></li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Jetpack Navigation + BottomNavigationBar 구현하기]]></title>
            <link>https://velog.io/@moonliam_/Android-Jetpack-Navigation-BottomNavigationBar-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@moonliam_/Android-Jetpack-Navigation-BottomNavigationBar-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 22 Feb 2024 14:55:54 GMT</pubDate>
            <description><![CDATA[<h2 id="1-single-activity-architecturesaa">1. Single Activity Architecture(SAA)</h2>
<p><strong>Single Activity Architecture</strong> 이른바 <strong>SAA</strong>는 단일 혹은 아주 적은 개수의 <code>Activity</code>만 사용하고 모두 <code>Fragment</code>로 화면을 구성하는 App 구조를 말한다.</p>
<p>그리고 주로 <strong>Jetpack Navigation</strong>과 함께 사용되는 구조이다.</p>
<p>위 내용은 <a href="">구글 I/O 2018</a>에서 발표된 내용으로 실제로 Jetpack Navigation 관련 문서를 찾아보면 Fragment 탐색에 초점을 두고 있는 것을 알 수 있다.</p>
<p>만약 본인이 만들고 있는 앱이 여러개의 <code>Activity</code> 간 전환이 일어난다면 <strong>Navigation</strong>은 좋은 선택지가 아니다.</p>
<p>그렇다면 <strong>SAA 구조를 사용하는가?</strong> SAA 구조를 사용하면서 얻을 수 있는 장점은 다음과 같다.</p>
<ol>
<li><code>Fragment</code>가 <code>Activity</code> 보다 상대적으로 가볍다.</li>
<li>여러 화면 간 데이터 공유가 용이하다.</li>
<li><code>Fragment</code> 는 하나의 <code>Activity</code> 안에 여러개가 표시될 수 있다. 따라서 좀 더 동적인 UI를 구현할 수 있다. </li>
<li>관심사별로 <code>Fragment</code>를 나눠 UI 관심사를 분리할 수 있다.</li>
</ol>
<p>위와 같이 <code>Activity</code>에 비해 <code>Fragment</code>가 갖는 다양한 이점들 때문에 이런 장점을 적극 활용하기 위해서 나온 개념이 바로 <strong>Single Activity Architecture(SAA)</strong>라고 할 수 있다.</p>
<h2 id="2-jetpack-navigation">2. Jetpack Navigation</h2>
<p>사실 Jetpack Navigation에 대해서는 이전에도 한번 포스트를 쓴 적이 있다. 하지만 그때 포스트 내용이 부실하기도 하니 이번에 다시 한번 제대로 정리해보기로 했다.</p>
<p>Navigation은 크게 3가지 요소로 이루어져 있다.</p>
<ol>
<li><p><code>NavHost</code>: 현재 탐색 대상(navigation destination)이 포함된 UI 요소. 즉, 사용자가 앱을 탐색할 때 앱은 기본적으로 <code>NavHost</code> 안팎으로 대상을 전환한다.</p>
</li>
<li><p><code>NavGraph</code>: 앱 내의 탐색 대상들(Fragment)과 어떻게 연결되어있는지 등이 정의되어있는 자료구조. 앱 내의 화면 연결을 보여주는 일종의 지도 역할을 한다.</p>
</li>
<li><p><code>NavController</code>: 실질적으로 탐색 동작을 처리하는 역할. 대상을 탐색하고 딥 링크를 처리하며 대상 백 스택을 관리하는 등의 여러 메소드를 제공한다.</p>
</li>
</ol>
<h2 id="3-bottomnavigationbar-구현하기">3. BottomNavigationBar 구현하기</h2>
<p>이제 위에서 설명한 Navigation과 Compose를 활용해 BottomNavigationBar를 구현해보자.</p>
<h3 id="1-gradle에-라이브러리-추가">1. gradle에 라이브러리 추가</h3>
<pre><code>implementation(&quot;androidx.navigation:navigation-compose:2.7.7&quot;)</code></pre><p>Compose-Navigation 라이브러리를 gradle에 추가한다.</p>
<h3 id="2-bottom-navigation으로-이동할-화면-생성">2. Bottom Navigation으로 이동할 화면 생성.</h3>
<p>필자는 여기서 <code>Home</code>, <code>Rating</code>, <code>Profile</code> 3개의 화면을 만들어주었다.</p>
<pre><code>@Composable
fun HomeScreen() {
    Text(text = &quot;Home&quot;)
}

@Composable
fun RatingScreen() {
    Text(text = &quot;Rating&quot;)
}

@Composable
fun ProfileScreen() {
    Text(text = &quot;Profile&quot;)
}</code></pre><h3 id="3-bottomnavitem-생성">3. BottomNavItem 생성.</h3>
<p>Bottom Navigation으로 이동할 item 객체 클래스 <code>BottomNavItem</code> 클래스를 생성하고 그 자식으로 <code>Home</code>, <code>Rating</code>, <code>Profile</code> 을 추가한다.</p>
<pre><code>sealed class BottomNavItem(
    @StringRes val title: Int,
    @DrawableRes val icon: Int,
    val screenRoute: String
) {
    object Home : BottomNavItem(R.string.home, R.drawable.ic_home, LiamScreens.Home.name)
    object Rating : BottomNavItem(R.string.rating, R.drawable.ic_rating, LiamScreens.Rating.name)
    object Profile : BottomNavItem(R.string.profile, R.drawable.ic_profile, LiamScreens.Profile.name)
}</code></pre><h3 id="4-navhost-navgraph-생성">4. NavHost, NavGraph 생성.</h3>
<p><code>NavHost</code> 함수의 <code>NavGraphBuilder</code> 를 통해서 <code>NavGraph</code>를 생성할 수 있다.</p>
<pre><code>NavHost(
    modifier = modifier,
    navController = navController,
    startDestination = startDestination
) {
    composable(route = MyScreens.Sample.name) {
        SampleScreen()
    }

    composable(route = MyScreens.Home.name) {
        HomeScreen()
    }

    composable(route = MyScreens.Rating.name) {
        RatingScreen()
    }

    composable(route = MyScreens.Profile.name) {
        ProfileScreen()
    }

    composable(route = MyScreens.Search.name) {
        SearchScreen()
    }
}</code></pre><h3 id="5-bottom-navigation-생성">5. Bottom Navigation 생성</h3>
<pre><code>@Composable
fun MyApp() {
    val navController = rememberNavController()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            MyBottomNavigation(
                containerColor = Color.Green,
                contentColor = Color.White,
                indicatorColor = Color.Green,
                navController = navController
            )
        }
    ) {
        Box(modifier = Modifier.padding(it)) {
            MyNavHost(
                navController = navController,
                startDestination = LiamScreens.Home.name
            )
        }
    }
}

@Composable
private fun MyNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable(route = LiamScreens.Sample.name) {
            SampleScreen()
        }

        composable(route = LiamScreens.Home.name) {
            HomeScreen(
                onSearchClicked = { navController.navigate(LiamScreens.Search.name) }
            )
        }

        composable(route = LiamScreens.Rating.name) {
            RatingScreen()
        }

        composable(route = LiamScreens.Profile.name) {
            ProfileScreen()
        }

        composable(route = LiamScreens.Search.name) {
            SearchScreen()
        }
    }
}

@Composable
private fun MyBottomNavigation(
    modifier: Modifier = Modifier,
    containerColor: Color,
    contentColor: Color,
    indicatorColor: Color,
    navController: NavHostController
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Rating,
        BottomNavItem.Profile
    )

    AnimatedVisibility(
        visible = items.map { it.screenRoute }.contains(currentRoute)
    ) {
        NavigationBar(
            modifier = modifier,
            containerColor = containerColor,
            contentColor = contentColor,
        ) {
            items.forEach { item -&gt;
                NavigationBarItem(
                    selected = currentRoute == item.screenRoute,
                    label = {
                        Text(
                            text = stringResource(id = item.title),
                            style = TextStyle(
                                fontSize = 12.sp
                            )
                        )
                    },
                    icon = {
                        Icon(
                            painter = painterResource(id = item.icon),
                            contentDescription = stringResource(id = item.title)
                        )
                    },
                    onClick = {
                        navController.navigate(item.screenRoute) {
                            navController.graph.startDestinationRoute?.let {
                                popUpTo(it) { saveState = true }
                            }
                            launchSingleTop = true
                            restoreState = true
                        }
                    },
                )
            }
        }
    }
}</code></pre><blockquote>
<p>레퍼런스</p>
</blockquote>
<ol>
<li><a href="https://developer.android.com/guide/navigation">안드로이드 공식 홈페이지 [Navigation]</a></li>
<li><a href="https://velog.io/@chuu1019/Android-Jetpack-Compose-Bottom-Navigation-%EB%A7%8C%EB%93%A4%EA%B8%B0">[Android] Jetpack Compose - Bottom Navigation 만들기</a></li>
<li><a href="https://heegs.tistory.com/128">[Android] Single Activity Architecture (SAA) + Navigation</a></li>
<li><a href="https://fornewid.medium.com/navigation-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-82d23fbc85af">Navigation 훑어보기</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] View 렌더링]]></title>
            <link>https://velog.io/@moonliam_/Android-View-%EB%A0%8C%EB%8D%94%EB%A7%81</link>
            <guid>https://velog.io/@moonliam_/Android-View-%EB%A0%8C%EB%8D%94%EB%A7%81</guid>
            <pubDate>Sun, 18 Feb 2024 16:06:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 포스트는 <a href="https://www.charlezz.com/?p=34935">안드로이드 View가 렌더링 되는 과정</a> 해당 블로그의 글을 제가 이해하기 쉽게 옮겨 적은 포스트입니다. 자세한 내용은 위 글을 참고해주세요.</p>
</blockquote>
<h2 id="view의-렌더링-과정">View의 렌더링 과정</h2>
<p>안드로이드 XML로 작성한 <code>View</code>는 어떻게 최종적으로 화면에 렌더링되어 보여지는 것일까?</p>
<p><code>View</code>는 크게 <code>onMeasure()</code>, <code>onLayout()</code>, <code>onDraw()</code> 3가지 생명주기 메소드 과정을 거쳐 사용자에게 보여진다. 더 자세한 내용은 <a href="https://www.charlezz.com/?p=29013">View의 생명주기에 관한 글</a>을 참고하자. </p>
<h3 id="rasterization래스터화">Rasterization(래스터화)</h3>
<p><strong>래스터화</strong>는 문자열, 버튼 또는 도형과 같은 <strong>객체들을 픽셀로 변환</strong>시키고 <strong>스크린상의 텍스쳐로 나타내는 과정</strong>을 말한다. </p>
<p>일반적으로 래스터화는 비용이 큰 작업에 속한다. 그러므로 GPU가 래스터화 가속을 위해 사용된다. GPU는 폴리곤, 텍스쳐 등을 계산하고 처리하는데 특화되어 설계되었다(병렬처리).</p>
<p>CPU가 화면에 무언가를 그리기 위해 GPU에게 폴리곤이나 텍스쳐의 래스터화를 위임하고 GPU의 최종적인 처리결과가 화면에 나타나게 된다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/29ac68bc-a07b-4392-98fb-e09114ab3e73/image.png" alt=""></p>
<p>CPU와 GPU간 데이터 전송 처리는 일반적으로 OpenGL ES API의 호출에 의해 일어난다. <strong>안드로이의 UI 객체(점, 선, 면, 버튼, 이미지 등)가 화면에 나타나기 위해서는 항상 OpenGL ES를 거친다.</strong></p>
<p>버튼을 화면에 렌더링하는 과정을 상상해보자. 우선 버튼이 있어야하고, 이를 CPU에 의해 폴리곤과 텍스쳐로 변환시킨 뒤 GPU에게 넘겨지게 된다.</p>
<p>위에서 언급했듯이 이렇게 UI 객체를 메쉬로 변환하는 과정은 비용이 큰작업이다. 모든 변환과정이 끝난 뒤 변환된 데이터가 OpenGL ES API에 의해 CPU에서 GPU로 전달되게 된다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/f4b6a0d4-492d-43af-9fff-cd82ba29a310/image.png" alt=""></p>
<p><strong>GPU에 업로드된 UI 객체 데이터는 GPU에 메모리상에 적재된 상태로 다음 프레임을 그릴때 재사용되므로, 매번 변환과정을 거칠 필요는 없다.</strong> 단지 GPU상에 업로드된 데이터를 참조하여 어떻게 그릴지 OpenGL ES에게 알려주기만 하면된다.</p>
<p>결국 <strong>렌더링 퍼포먼스를 최적화한다는 의미는 GPU 메모리에 데이터를 얼마나 많이 적재하고, 제거하며, 이를 얼마나 잘 참조하냐는 것이다.</strong></p>
<h3 id="디스플레이-리스트display-list">디스플레이 리스트(Display List)</h3>
<p><code>View</code>의 렌더링이 필요할때 디스플레이 리스트가 생성되며, 디스플레이 리스트에서는 렌더링이 필요한 <code>View</code>에 대한 그리기 명령들이 포함되어있다.</p>
<p><code>View</code>가 렌더링 된 이후에 <code>View</code>의 속성에 대한 변화가 있다면, 간단히 디스플레이 리스트를 변경하여  렌더링할 수 있지만, 눈에 보이는 변화가 크다면 이전 디스플레이 리스트는 더이상 유효하지 않으므로 새롭게 디스플레이 리스트를 생성하고 화면 갱신이 필요하다.</p>
<p><strong>결국 <code>View</code>의 복잡도가 성능에 영향을 미치기 때문에, 디스플레이 리스트가 반복적으로 재생성되고 재실행되는 것을 제어하는것이 퍼포먼스를 향상 시킬 수 있는 방법 중 하나이다.</strong></p>
<p>사이즈가 변경되는 <code>View</code>가 있다고 가정하자. 그러면 <code>onMeasure()</code> 단계에서 계층 모든 <code>View</code>들의 새로운 사이즈를 계산하게 되고, 만약 <code>View</code>의 포지션이 변경된다면 <code>requestLayout()</code>를 호출하게 된다. 또는 <code>ViewGroup</code>이 자식 <code>View</code>들을 재배치하는경우 <code>onLayout()</code>이 호출되고 <code>View</code> 전체 계층이 다시 배치되게 된다.</p>
<p>각 <code>View</code>의 생명주기 단계는 그다지 큰 시간이 걸리지 않아 퍼포먼스에 영향이 없으나, 다른 <code>View</code>와 <code>ViewGroup</code>이 많이 연관되어 있다면 성능에 영향을 주게 된다.</p>
<p><img src="blob:https://velog.io/bc79f2d5-186f-43a2-8f50-cdfabaff3423" alt="업로드중.."></p>
<p><strong>더 나은 성능의 앱을 위해서는 <code>View</code>의 계층을 플랫하게 유지해야할 필요가 있다. 그래야 종속된 <code>View</code>들이 영향을 받지 않아 <code>View</code>가 갱신 되는데 시간이 줄어들게 된다.</strong> <code>View</code>의 계층을 플랫하게 유지하는데는 <code>ConstraintLayout</code>을 사용하면 좋다.</p>
<h2 id="렌더링-퍼포먼스">렌더링 퍼포먼스</h2>
<p>개발자는 앱의 퍼포먼스를 체크해야할 의무가 있다. UI 퍼포먼스는 애플리케이션 성능에서 주요한 비율을 차지한다. UI 퍼포먼스를 저하시키는 <strong>오버드로잉</strong>에 대해서 알아보자.</p>
<h3 id="오버드로우overdraw">오버드로우(Overdraw)</h3>
<p><strong>오버드로우(Overdraw)는 시스템이 단일 렌더링 프레임에서 같은 픽셀에 여러번 덧 그리는 것</strong>을 말한다. 이러한 GPU의 리소스 낭비로 인해 퍼포먼스가 떨어지고, 사용자에게 나쁜경험을 제공할 수 있다.</p>
<p>오버드로잉을 줄이기 위한 방법을 알아보자.</p>
<h3 id="1-레이아웃에서-불필요한-배경-제거하기">1. 레이아웃에서 불필요한 배경 제거하기</h3>
<p>레이아웃 XML 파일에서 사용자가 절대 볼 일이 없는 불필요한 배경을 최대한 제거한다.</p>
<pre><code>&lt;LinearLayout 
    android:id=&quot;@+id/parent&quot; 
    android:background=”@android:color/black”&gt;
    &lt;android.support.v4.widget.NestedScrollView&gt;
        &lt;LinearLayout 
             android:id=&quot;@+id/child&quot; 
             android:background=”@android:color/black”&gt;
             ...
        &lt;/LinearLayout&gt;
    &lt;/android.support.v4.widget.NestedScrollView&gt;
&lt;/LinearLayout&gt;</code></pre><p>위 코드는 <code>LinearLayout</code>과 <code>NestedScrollView</code> 둘 다 배경색이 검정색이다. 이 경우 가장 바깥쪽 <code>LinearLayout</code>만 검정색으로 배경을 지정해도 똑같이 보이기 때문에 굳이 <code>NestedScrollView</code> 배경까지 검정으로 지정해서 오버드로잉할 필요가 없다.</p>
<h3 id="2-view-계층-평탄화하기">2. View 계층 평탄화하기</h3>
<p><code>View</code> 계층을 평탄화 하기 위해 <code>ConstraintLayout</code>을 사용하면 최상의 퍼포먼스를 낼 수 있다. <code>ConstraintLayout</code>은 레이아웃 내에 또 다른 레이아웃을 포함시킬 필요없이 간단한 제약조건으로 평탄한 계층구조를 만든다. 평탄한 뷰 계층을 구성하여 퍼포먼스를 향상하자.</p>
<h3 id="3-투명도-줄이기">3. 투명도 줄이기</h3>
<p>화면에서 투명한 픽셀을 렌더링하는 것을 <strong>알파 렌더링</strong>이라고 한다. 페이드 아웃 및 그림자 같은 시각적 효과와 같은 투명한 애니메이션에는 투명도가 포함되므로 오버 드로우가 크게 발생한다. 이러한 상황에서 렌더링되는 투명 객체의 수를 줄임으로써 오버드로잉 비용을 줄일 수 있다.</p>
<p>예를 들어 <code>TextView</code> 에서 알파 값이 설정된 검정색 텍스트를 그려 회색 텍스트를 얻을 수 있는데, 이는 단순히 텍스트를 회색으로 그리는것보다 훨씬 비용이 많이 들기 때문에 그냥 투명도 없는 회색을 적용하는 편이 퍼포먼스를 높이는데 도움이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] View의 생명주기]]></title>
            <link>https://velog.io/@moonliam_/Android-View%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@moonliam_/Android-View%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Sat, 17 Feb 2024 08:35:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 포스트는 <a href="https://www.charlezz.com/?p=29013">Android에서 View의 생명주기</a> 해당 블로그의 글을 필자가 쉽게 이해하고 기록하기 위한 용도로 작성한 글입니다. 자세한 내용은 해당 블로그를 참고해주세요.</p>
</blockquote>
<h2 id="1-view란">1. View란?</h2>
<p>안드로이드 앱에서 <code>View</code>는 유저 인터페이스를 구성요소를 갖고있는 가장 기본적인 단위 클래스이다. <code>View</code>는 화면 상에서 사각형 영역을 가지며 <strong>그리기</strong> 및 <strong>이벤트 처리</strong> 등을 처리할 수 있다. 또한 <code>View</code>는 버튼이나 텍스트 필드와 같은 유저 상호작용 요소들의 집합인 <code>Widget</code>의 가장 기초 클래스이다.</p>
<p><code>View</code>의 서브 클래스인 <code>ViewGroup</code>은 레이아웃들의 기초 클래스이며 <code>View</code>들(또는 다른 <code>ViewGroup</code>)을 담는 보이지 않는 컨테이너 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/4a808288-fe7b-4446-9d56-969a8e5ce78d/image.png" alt=""></p>
<p>안드로이드의 <code>Activity</code>나 <code>Fragment</code>에 생명주기가 존재하는 것처럼 <code>View</code>에도 역시나 생명주기가 존재한다. 하지만 많은 개발자들이 <code>View</code>의 생명주기를 제대로 알지 못하고 개발을 한다. (사실 나도 그랬다...)</p>
<p>이번 포스트에서는 <code>View</code>의 생명주기와 각 단계에서 일어나는 동작에 대해 알아보고자 한다.</p>
<h2 id="2-view의-생명주기">2. View의 생명주기</h2>
<p>화면에 렌더링 된 View는 다음 그림과 같은 생명주기 메서드를 거쳐 화면에 그려진다.
<img src="https://velog.velcdn.com/images/moonliam_/post/b67ea9a6-dbda-488d-b3ba-e0875ee7684f/image.png" alt=""></p>
<p>위 생명주기의 각 단계들은 모두 중요한 역할을 하고 있다. 각각의 단계에서 어떤 일이 일어나는지 알아보자.</p>
<h3 id="1-생성자constructors">1. 생성자(Constructors)</h3>
<p>커스텀 뷰(Custom View)를 만들때 생성자를 어떤 걸 써야하는지 난감할 때가 많다. <code>View</code>의 생성자가 여러 종류가 있는데다 각각 어떤 차이점이 있는지 명확히 모르기 때문이다. </p>
<ol>
<li><code>View(Context context)</code></li>
</ol>
<p><strong>코드에서 <code>View</code>를 동적으로 만들 때 사용</strong>하는 간단한 생성자다. 여기서 매개 변수 <code>context</code>는 <code>View</code>가 실행될 때 현재 테마, 리소스 등을 구성하는데 사용된다.</p>
<ol start="2">
<li><code>View(Context context, @Nullable AttributeSet attrs)</code></li>
</ol>
<p><strong>XML에서 <code>View</code>를 전개(Inflate)할 때 호출되는 생성자</strong>로, XML 파일에서 지정된 속성을 제공하여 XML 파일에서 <code>View</code>를 구성 할 때 호출된다. 이 생성자는 기본 스타일을 사용하므로 <code>Context</code>의 테마 및 지정된 <code>AttributeSet</code>의 속성 값만 적용된다.</p>
<ol start="3">
<li><p><code>View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)</code>
XML을 통해 전개(Inflate)를 하고 <strong>테마 속성에서 클래스별 기본 스타일을 적용</strong>한다. 이 생성자는 <strong>서브 클래스가 전개(Inflate)할 때 자체 기본 스타일을 사용할 수 있도록 한다.</strong> 
예를 들어, <code>Button</code> 클래스의 생성자는 수퍼 클래스 생성자를 호출하고 <code>defStyleAttr</code>에 <code>R.attr.buttonStyle</code>을 제공한다. 이를 통해 테마의 버튼 스타일은 모든 기본 View 속성 (특히 배경)과 Button 클래스의 속성을 수정할 수 있다.</p>
</li>
<li><p><code>View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)</code>
XML을 전개하고 <strong>테마 속성 또는 Style 리소스에서 클래스 별 기본 스타일을 적용</strong>한다. 이 생성자는 서브 클래스가 전개(Inflate)할 때 자체 기본 스타일을 사용할 수 있도록한다. 위와 유사하다.
매개변수 <code>defStyleRes</code>는 <code>View</code>의 <code>defStyleAttr</code>가 0이거나 테마에서 찾을 수 없는 경우에만 기본값을 제공하는 Style 리소스 ID다. 기본값을 찾지 않으려면 0 으로 지정한다.</p>
</li>
</ol>
<h3 id="2-attachment--detachment">2. Attachment / Detachment</h3>
<p><code>View</code>가 Window에서 연결되거나 분리 될 때의 단계다. 이 단계에는 적절한 작업을 수행하기 위해 콜백을 받는 몇 가지 방법이 있다.</p>
<p><strong><code>onAttachedToWindow()</code></strong>
<code>View</code>가 Window에 연결되면 호출된다. <code>View</code>가 활성화 될 수 있고, 드로잉 할 표면이 있음을 알고있는 단계다. 따라서 리소스 할당을 시작하거나 리스너를 설정할 수 있다.</p>
<p><code>onDetachedFromWindow()</code>
<code>View</code>가 Window에서 분리 될 때 호출된다. 이 시점에서 더 이상 드로잉을 할 표면이 없다. 예약 된 자원을 정리하거나 정리하는 모든 종류의 작업을 중지해야하는 곳이다. 
이 메소드는 ViewGroup에서 View 제거를 호출하거나 액티비티가 Destroyed될 때 호출된다.</p>
<p><code>onFinishInflate()</code>
이 메소드는 <code>View</code>가 전개(Inflate)가 끝날 때 호출된다. 레이아웃의 경우 모든 Child View가 추가 된 후에 호출된다.</p>
<h3 id="순회traversals">순회(Traversals)</h3>
<p><code>View</code> 계층 구조는 부모 노드(ViewGroup)에서 분기가 있는 리프 노드(Child Views)의 트리 구조와 같기 때문에 순회 단계라고 한다. 따라서 각 메소드는 부모에서 시작하여 마지막 노드까지 순회하여 제약 조건을 정의한다.
<img src="https://velog.velcdn.com/images/moonliam_/post/c4d3f4c9-3f86-421a-9a40-7005541bb6f0/image.png" alt="">
<img src="https://velog.velcdn.com/images/moonliam_/post/4042999f-f393-46ee-915c-dead3d5415cd/image.png" alt=""></p>
<p>Measure 단계와 Layout 단계는 항상 위와 같이 순차적으로 진행된다.</p>
<h3 id="3-onmeasure">3. onMeasure()</h3>
<p><code>onMeasure()</code> 메소드는 <strong><code>View</code>의 크기를 확인하기 위해 호출</strong>된다. <code>ViewGroup</code>의 경우 계속해서 각 Child View에 대한 측정을 하고, 그에 대한 결과로 자신의 사이즈를 결정한다.</p>
<pre><code>onMeasure(int widthMeasureSpec, int heightMeasureSpec)
// @param widthMeasureSpec 부모 View에 의해 적용된 수평 공간 요구사항
// @param heightMeasureSpec 부모 View에 의해 적용된 수직 공간 요구사항</code></pre><p><code>onMeasure()</code>는 값을 반환하지 않고, <code>setMeasuredDimension()</code>을 호출하여 너비와 높이를 명시적으로 설정한다.</p>
<h3 id="measurespec">MeasureSpec</h3>
<p><code>MeasureSpec</code>은 부모에서 자식으로 전달되는 레이아웃 요구 사항을 캡슐화한다. 각 <code>MeasureSpec</code>은 너비 또는 높이에 대한 요구 사항을 나타낸다. MeasureSpec은 크기와 모드로 구성되며, 세 가지 모드가 있다.</p>
<p><strong>MeasureSpec.EXACTLY</strong>: 부모가 자식의 정확한 크기를 결정한다. 자식의 사이즈와 관계없이 주어진 경계 내에서 사이즈가 결정된다.</p>
<p><strong>MeasureSpec.AT_MOST</strong>: 자식은 지정된 크기까지 원하는 만큼 커질 수 있다.</p>
<p><strong>MeasureSpec.UNSPECIFIED</strong>: 부모가 자식에 제한을 두지 않기 때문에, 자식은 원하는 크기가 될 수 있다.</p>
<h3 id="onlayout">onLayout()</h3>
<p><code>View</code> <strong>위치를 측정</strong>하여 화면에 배치 한 후에 호출된다.</p>
<h3 id="ondraw">onDraw()</h3>
<p>크기와 위치는 이전 단계에서 계산되므로 <code>View</code>는 그것들을 기준으로 그려진다. <code>onDraw(Canvas canvas)</code> 메서드에서 생성된 <strong>캔버스 객체에는 GPU로 보낼 OpenGL-ES 명령목록(displayList)이 있다.</strong> 
<code>onDraw()</code>는 <strong>여러번 호출되므로 이곳에서 객체를 만들면 안된다.</strong></p>
<p>특정 <code>View</code>의 속성이 변경되었을 때 실행되는 두 가지 메서드가 있다.</p>
<p><strong><code>invalidate()</code></strong>
<code>invalidate()</code>는 변경 사항을 보여주고자 하는 특정 <code>View</code>에 대해 <strong>강제로 다시 그리기를 요구하는 메소드</strong>이다. <code>View</code> 모양이 변경되면 <code>invalidate()</code>를 호출해야한다고 할 수 있다.</p>
<p><strong><code>requestLayout()</code></strong>
어떤 시점에서 <code>View</code>의 경계가 변경되었다면, <strong><code>View</code>를 다시 측정하기 위해 requestLayout()을 호출하여 Measure및 Layout 단계를 다시 거칠 수 있다.</strong></p>
<p><code>View</code>에서** 메소드를 호출 할 때는 항상 UI 스레드내에서 수행해야한다.** 다른 스레드에서 작업하고 있고, 해당 스레드에서 <code>View</code>의 상태를 업데이트 하려는 경우 핸들러를 사용해야한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Retrofit Interceptor로 헤더에 액세스 토큰 넣기]]></title>
            <link>https://velog.io/@moonliam_/Android-Retrofit-Interceptor%EB%A1%9C-%ED%97%A4%EB%8D%94%EC%97%90-%EC%95%A1%EC%84%B8%EC%8A%A4-%ED%86%A0%ED%81%B0-%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@moonliam_/Android-Retrofit-Interceptor%EB%A1%9C-%ED%97%A4%EB%8D%94%EC%97%90-%EC%95%A1%EC%84%B8%EC%8A%A4-%ED%86%A0%ED%81%B0-%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Sun, 11 Feb 2024 08:42:33 GMT</pubDate>
            <description><![CDATA[<h3 id="1-retrofit-interceptor">1. Retrofit Interceptor</h3>
<p>안드로이드 앱에서 백엔드 서버와 네트워크 통신을 할 때 가장 많이 사용하는 라이브러리는 아마 <strong>Retrofit</strong>일 것이다.</p>
<p>서버에 API 요청을 할 때 사용자 인증을 위해서 OAuth2.0 방식으로 발급받은 액세스 토큰을 헤더에 넣어서 요청해야하는 경우가 많은데 매 요청마다 일일이 액세스토큰을 넣어야 한다면 여간 귀찮은 게 아니다.</p>
<p>이런 상황에서 유용하게 사용할 수 있는 게 바로 <code>Interceptor</code>다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/c56a9b29-a32b-4d7e-a994-5242c22d0600/image.png" alt=""></p>
<p><code>Interceptor</code>를 사용하면 매 API 요청을 모니터링하면서 필요에 따라 인증, 재요청 등의 작업을 수행할 수도 있다. </p>
<p><code>Interceptor</code>는 크게 <strong>1. Application Interceptor</strong>과 <strong>2. Network Interceptor</strong> 두 종류가 있다. </p>
<p>일반적으로 <code>addInterceptor</code> 메소드를 이용해서 추가하는 Interceptor들은 전부 Application Interceptor이다. 우리가 Request 헤더에 넣어줘야하는 액세스 토큰은 디바이스에 저장되어있기 때문에 이 역시 Application Interceptor에서 해줘야하는 역할이다.</p>
<p>반면 Network Interceptor는 앱의 콘텐츠와는 직접적 관계가 없는, Network의 상태 및 서버 값에 따라서 retry 하는 등의 로직을 넣는 곳이다.</p>
<p><strong>NetworkModule.kt</strong></p>
<pre><code>...

@Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient =
    OkHttpClient.Builder()
        .connectTimeout(ConnectTimeout, TimeUnit.SECONDS)
        .writeTimeout(WriteTimeout, TimeUnit.SECONDS)
        .readTimeout(ReadTimeout, TimeUnit.SECONDS)
        .addInterceptor(
            HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }
        )
        .build()

...</code></pre><p>기존의 Retrofit을 구성하는 <code>OkHttpClient</code> 구현하는 코드는 위와 같다. 여기서 이제 <code>TokenInterceptor</code> 클래스를 생성해 <code>addInterceptor()</code> 메소드로 추가해줄 것이다.</p>
<h3 id="2-tokeninterceptor-구현">2. TokenInterceptor 구현</h3>
<p><strong>TokenInterceptor.kt</strong></p>
<pre><code>class TokenInterceptor @Inject constructor(
    private val localTokenDataSource: LocalTokenDataSource
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        return runBlocking {
            val accessToken = localTokenDataSource.getAccessToken().first()
            val request = if (accessToken.isNotEmpty()) {
                chain.request().putTokenHeader(accessToken)
            } else {
                chain.request()
            }
            chain.proceed(request)
        }
    }

    private fun Request.putTokenHeader(accessToken: String): Request {
        return this.newBuilder()
            .addHeader(AUTHORIZATION, &quot;Bearer $accessToken&quot;)
            .build()
    }

    companion object {
        private const val AUTHORIZATION = &quot;authorization&quot;
    }
}</code></pre><p><code>getAccessToken()</code> 메소드가 DataStore.Preferences에서 Flow로 데이터를 가져오는 비동기 작업이기 때문에 <code>runBlocking</code> 블럭으로 전체를 감싸준다.</p>
<h3 id="3-authenticator-구현">3. Authenticator 구현</h3>
<p>여기서 끝이 아니다. 만약 액세스 토큰의 유효 기간이 만료되었거나 권한이 없는 다른 토큰을 헤더에 넣었다면 권한이 없다는 응답값을 서버로부터 받을 것이다.</p>
<p>이 경우 토큰 재발급을 서버에 요청해야하는데 이 과정을 유저가 알아차리지 못하게 앱에서 알아서 처리하게 만들고 싶다.</p>
<p>즉, 다음과 같은 프로세스로 통신이 진행된다.</p>
<ol>
<li><code>TokenInterceptor</code> 로 액세스 토큰을 헤더에 넣어서 요청 전송.</li>
<li>요청에 포함된 액세스 토큰이 만료되거나 권한이 없을 경우 401 에러 응답.</li>
<li><code>Authenticator</code>에서 401 응답 감지시 디바이스에 저장된 리프레시 토큰으로 새 토큰 발급 요청</li>
<li>새 토큰 발급 받으면 디바이스에 저장 후 갱신된 토큰으로 1번의 요청 다시 전송</li>
</ol>
<p><strong>AuthAuthenticator.kt</strong></p>
<pre><code>class AuthAuthenticator @Inject constructor(
    private val localTokenDataSource: LocalTokenDataSource
) : Authenticator {

    private companion object {
        private const val ConnectTimeout = 15L
        private const val WriteTimeout = 20L
        private const val ReadTimeout = 15L
        private val contentType = &quot;application/json&quot;.toMediaType()
    }

    override fun authenticate(route: Route?, response: Response): Request {
        if (response.code == HTTP_UNAUTHORIZED) {
            val token = runBlocking {
                localTokenDataSource.getRefreshToken().first()
            }
            // The access token is expired. Refresh the credentials.
            synchronized(this) {
                // Make sure only one coroutine refreshes the token at a time.
                return runBlocking {
                    val newTokenResult = getNewToken(token)
                    if (newTokenResult is Result.Success) {
                        val accessToken = newTokenResult.body!!.result.accessToken
                        val refreshToken = newTokenResult.body.result.refreshToken
                        // Update the access token in your storage.
                        localTokenDataSource.updateAccessToken(accessToken)
                        localTokenDataSource.updateRefreshToken(refreshToken)
                        return@runBlocking response.request.newBuilder()
                            .header(&quot;Authorization&quot;, &quot;Bearer $accessToken&quot;)
                            .build()
                    } else {
                        return@runBlocking response.request
                    }
                }
            }
        }
        return response.request
    }
}</code></pre><h3 id="4-interceptor-authenticator-적용">4. Interceptor, Authenticator 적용</h3>
<pre><code>fun providesLyfeOkHttpClient(tokenInterceptor: TokenInterceptor, authAuthenticator: AuthAuthenticator): OkHttpClient =
    OkHttpClient.Builder()
        .connectTimeout(ConnectTimeout, TimeUnit.SECONDS)
        .writeTimeout(WriteTimeout, TimeUnit.SECONDS)
        .readTimeout(ReadTimeout, TimeUnit.SECONDS)
        .addInterceptor(
            HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }
        )
        .addInterceptor(tokenInterceptor)
        .authenticator(authAuthenticator)
        .build()</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] DataStore Preferences 적용해보기]]></title>
            <link>https://velog.io/@moonliam_/Android-DataStore-Preferences-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@moonliam_/Android-DataStore-Preferences-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 04 Feb 2024 13:07:17 GMT</pubDate>
            <description><![CDATA[<h2 id="1-sharedpreferences-vs-preferences-datastore">1. SharedPreferences vs Preferences DataStore</h2>
<p>최근 사이드 프로젝트를 진행하면서 서버와의 네트워크 통신을 위해 기기에서 토큰을 관리해야했다.
기존에는 <code>SharedPreferences</code>로 구현했지만 최근 안드로이드 공식 문서에서 <code>SharedPreferences</code> 대신 <code>Preferences DataStore</code>를 사용하라고 권장하고 있길래 적용해보기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/107e029c-9c7c-46d4-b30d-4d8172745433/image.png" alt=""></p>
<p>(<code>DataStore</code>에는 <code>Preferences DataStore</code>와 <code>Proto DataStore</code> 2가지 유형이 존재하는데 이 포스트에서는 <code>Preferences DataStore</code>만 다룬다.)</p>
<p><code>Preferences DataStore</code>도 기본적으로는 <code>SharedPreferences</code> 처럼 Key-value 쌍으로 데이터를 저장한다는 점은 같다. 하지만 차이점은 크게 2가지가 있다.</p>
<ol>
<li><strong>비동기 방식으로 데이터를 저장</strong>한다.</li>
<li><strong>트랜잭션 API가 존재</strong>하여 다중 스레드 환경에서도 <strong>일관성을 보장</strong>한다.</li>
</ol>
<p>기존의 <code>SharedPreferences</code>는 데이터를 저장할 때 비동기 처리가 지원되지 않았기 때문에 ANR 문제에서 자유로울 수 없었다.</p>
<p>또한 <code>SharedPreferences</code>는 데이터를 파싱하는 과정에서 런타임 예외가 발생할 가능성도 있었다.</p>
<p>이러한 <code>SharedPreferences</code>의 단점을 보완한 게 바로 <code>DataStore</code> 이다.</p>
<h2 id="2-preferences-datastore-활용">2. Preferences DataStore 활용</h2>
<h3 id="1-buildgradle-추가">1. build.gradle 추가</h3>
<pre><code>implementation &quot;androidx.datastore:datastore-preferences:{newer_version}&quot;</code></pre><p>먼저 <code>build.gradle</code> 파일에 <code>DataStore-Preferences</code> 라이브러리를 추가해준다. </p>
<h3 id="2-datastorepreferences-객체-생성">2. DataStore<code>&lt;Preferences&gt;</code> 객체 생성</h3>
<pre><code>private val Context.dataStore: DataStore&lt;Preferences&gt; by preferencesDataStore(name = &quot;token&quot;)</code></pre><p><code>preferencesDataStore()</code> 메소드로 <code>DataStore&lt;Preferences&gt;</code> 객체를 생성해준다. Kotlin 파일 최상위에서 위와 같은 방식으로 <code>DataStore&lt;Preferences&gt;</code> 객체를 생성하면 싱글톤으로 활용할 수 있다.</p>
<h3 id="3-datastorepreferences에-데이터-저장하기">3. DataStore<code>&lt;Preferences&gt;</code>에 데이터 저장하기</h3>
<p>기본적으로 <code>DataStore</code>는 비동기 방식, 그것도 <code>Flow</code>로 데이터를 읽고 저장한다. 
<code>edit()</code> 함수는 데이터를 트랜잭션 방식으로 업데이트할 수 있게 해준다. <code>edit()</code> 함수로 생성된 코드 블럭 내부는 모두 단일 트랜잭션으로 취급된다.</p>
<pre><code>suspend fun updateAccessToken(accessToken: String) {
    context.dataStore.edit { token -&gt; 
        token[PreferencesKeys.ACCESS_TOKEN_KEY] = accessToken
    }
}</code></pre><h3 id="4-datastorepreferences의-데이터-읽기">4. DataStore<code>&lt;Preferences&gt;</code>의 데이터 읽기</h3>
<p><code>DataStore&lt;Preferences&gt;</code>의 데이터를 읽을 때는 <code>DataStore.data</code> 속성을 사용한다.
<code>DataStore.data</code>를 사용하면 <code>Flow</code> 데이터 스트림이 생성되어 원하는 데이터를 가져올 수 있다.</p>
<pre><code>suspend fun getAccessToken(): Flow&lt;String&gt; = context.dataStore.data.map { token -&gt; 
    token[PreferencesKeys.ACCESS_TOKEN_KEY].orEmpty()
}</code></pre><p><strong>레퍼런스</strong></p>
<blockquote>
<ol>
<li><a href="https://developer.android.com/topic/libraries/architecture/datastore?hl=ko">Datastore</a></li>
<li><a href="https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html">Prefer Storing Data with Jetpack DataStore</a></li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 안드로이드 ML Kit를 사용한 얼굴 인식 예제]]></title>
            <link>https://velog.io/@moonliam_/Android-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-ML-Kit%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%96%BC%EA%B5%B4-%EC%9D%B8%EC%8B%9D-%EC%98%88%EC%A0%9C</link>
            <guid>https://velog.io/@moonliam_/Android-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-ML-Kit%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%96%BC%EA%B5%B4-%EC%9D%B8%EC%8B%9D-%EC%98%88%EC%A0%9C</guid>
            <pubDate>Thu, 25 Jan 2024 08:45:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1-android-ml-kit">1. Android ML Kit</h2>
<p>최근 구글에서 On-Device용 AI 칩을 발표했다. 내가 AI 개발자는 아니지만 호기심이 생겨서 알아보다가 <strong>안드로이드에서 사용할 수 있는 <code>ML Kit</code> 라는 이름의 공식 AI 라이브러리</strong>가 있는 걸 발견했다. 오늘은 해당 라이브러리를 이용해 가볍게 사진에서 사람의 얼굴을 인식하는 예제를 구현해볼 예정이다.</p>
<h3 id="1-ml-kit-라이브러리-import하기">1. ML Kit 라이브러리 import하기</h3>
<p>ML Kit 라이브러리를 import 하는 방법은 2가지가 있다. 
<strong>1. 번들로 필요한 모델을 빌드시간에 정적으로 연결하는 방법</strong>
<strong>2. Google Play 서비스를 통해 모델을 동적으로 다운로드하는 방법이다.</strong></p>
<p>1번의 방법으로 하면 앱 용량이 더 커지지만 (약 6.9MB 정도) 대신 모델이 빌드 과정에서 모두 연결되기 때문에 빠르게 모델을 사용할 수 있다. 반면 2번 방법은 용량은 작아지지만 모델을 동적으로 다운받기 때문에 처음 모델을 이용할 때 다운받는 시간이 필요하다.</p>
<p>둘 중에 개발하고자 하는 앱에 더 맞는 방법을 택하면 될 것 같다.</p>
<p><strong>모델을 앱과 번들로 묶는 방법</strong></p>
<pre><code>dependencies {
  // ...
  // Use this dependency to bundle the model with your app
  implementation &#39;com.google.mlkit:face-detection:16.1.5&#39;
}</code></pre><p><strong>Google Play 서비스에서 모델을 사용하는 경우</strong></p>
<pre><code>dependencies {
  // ...
  // Use this dependency to use the dynamically downloaded model in Google Play Services
  implementation &#39;com.google.android.gms:play-services-mlkit-face-detection:17.1.0&#39;
}</code></pre><pre><code>&lt;application ...&gt;
      ...
      &lt;meta-data
          android:name=&quot;com.google.mlkit.vision.DEPENDENCIES&quot;
          android:value=&quot;face&quot; &gt;
      &lt;!-- To use multiple models: android:value=&quot;face,model2,model3&quot; --&gt;
&lt;/application&gt;</code></pre><h3 id="2-mainactivity-ui-구성">2. MainActivity UI 구성</h3>
<p>버튼을 누르면 먼저 권한 체크를 한 뒤, 권한 확인 후 이미지를 가져오고 해당 이미지를 분석해 얼굴로 인식된 곳에 초록색 박스를 표시하는 동작을 구현할 것이다.</p>
<p>먼저 UI부터 구성한다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

@Composable
private fun MainScreen() {
    val context = LocalContext.current

    var imageBitmap by remember { mutableStateOf(ImageBitmap(32, 32)) }

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Button(
            onClick = {
                // TODO 
            }
        ) {
            Text(text = &quot;갤러리에서 불러오기&quot;)
        }

        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            drawImage(
                image = imageBitmap
            )
            // TODO (얼굴에 사각형 그리기)
        }
    }
}</code></pre><p>이제 버튼을 눌렀을 때 로직을 구성한다. <code>onClick()</code> 로직은 다음과 같다.</p>
<ol>
<li>이미지 접근 권한 확인 (checkPermission)</li>
<li>권한이 있을 경우 갤러리에서 이미지 불러오기</li>
<li>권한이 없을 경우 권한 동의하기</li>
</ol>
<p>먼저 갤러리 이미지 접근 권한을 확인하는 로직을 구성하자.</p>
<h3 id="3-permissionchecker-기능-추가">3. PermissionChecker 기능 추가</h3>
<p><code>PermissionChecker.kt</code> 파일을 따로 생성해 함수를 추가해준다. 예제에서는 이미지 접근 권한만 확인하면 되지만 어떤 권한 요청이 필요하더라고 확인할 수 있게 구현했다.</p>
<pre><code>fun checkSinglePermission(
    context: Context,
    permission: String
): Boolean {
    if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
        Log.d(&quot;test5&quot;, &quot;권한이 이미 존재합니다.&quot;)
        return true
    }
    return false
}

fun checkMultiplePermission(
    context: Context,
    permissions: List&lt;String&gt;
): Boolean {
    permissions.forEach {
        if (ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED)
            return false
    }
    return true
}</code></pre><p>그 다음 <code>MainActivity</code>에 적용한다.</p>
<pre><code>Button(
    onClick = {
        if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
            // TODO (이미지 가져오기)
        } else {
            // TODO (권한 요청하기)
        }
    }
) {
    Text(text = &quot;갤러리에서 불러오기&quot;)
}</code></pre><h3 id="4-권한-요청하기">4. 권한 요청하기</h3>
<p>권한이 없을 때 사용자에게 권한을 요청하는 기능을 추가한다.</p>
<pre><code>...
    val requestPermissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { isGranted -&gt;
        if (isGranted) {
            Log.d(&quot;test5&quot;, &quot;권한이 동의되었습니다.&quot;)
        } else {
            Log.d(&quot;test5&quot;, &quot;권한이 거부되었습니다.&quot;)
        }
    }

    ...

    Button(
            onClick = {
                if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
                    // TODO (이미지 가져오기)
                } else {
                    requestPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES)
                }
            }
        ) {
            Text(text = &quot;갤러리에서 불러오기&quot;)
        }</code></pre><p>Compose에서 기존의 <code>startActivityForResult</code>를 사용하려면 <code>rememberLauncherForActivityResult</code>를 통해 <code>Launcher</code> 객체를 가져와야한다.</p>
<p>그런데 이 <code>rememberLauncherForActivityResult</code>는 <code>Composable</code> 함수라서 외부로 빼기가 쉽지 않다. UI 레이어와 비즈니스 로직을 분리하고 싶은데 유독 이 <code>rememberLauncherForActivityResult</code>를 사용할 때는 잘 안된다. 나중에 이 부분도 한번 찾아봐야겠다.</p>
<p>아무튼 이제 갤러리에서 이미지를 불러오는 과정만 진행하면 된다.</p>
<h3 id="5-갤러리에서-이미지-불러오기">5. 갤러리에서 이미지 불러오기</h3>
<pre><code>...

val pickGalleryLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) {
        if (it == null)
            throw NullPointerException()
        val source = ImageDecoder.createSource(context.contentResolver, it)
        imageBitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -&gt;
            decoder.setTargetSize(1080, 1080)
        }.asImageBitmap()
    }

    ...

    Button(
            onClick = {
                if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
                    pickGalleryLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
                } else {
                    requestPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES)
                }
            }
        ) {
            Text(text = &quot;갤러리에서 불러오기&quot;)
        }</code></pre><h3 id="6-facedetector로-얼굴-인식하기">6. FaceDetector로 얼굴 인식하기</h3>
<p>먼저 이미지를 처리하고 얼굴을 인식해 줄 <code>ImageProcessor</code> 를 구현한다.</p>
<pre><code>object ImageProcessor {

    private val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
        .build()

    private val detector = FaceDetection.getClient(highAccuracyOpts)

    suspend fun processInputImage(image: InputImage): List&lt;Face&gt; {
        return suspendCoroutine { continuation -&gt;
            detector.process(image).addOnSuccessListener {  faces -&gt;
                continuation.resume(faces)
            }
            .addOnFailureListener {
                throw it
            }
        }
    }
}</code></pre><p><code>FaceDetection.getClient()</code> 메소드를 통해 <code>FaceDetector</code> 객체를 가져올 수 있는데, 이 때 <code>FaceDetectorOptions</code> 객체를 이용해 설정을 지정할 수 있다.</p>
<p>위 예제에서는 <code>PERFORMANCE_MODE_ACCURATE</code>, <code>LANMARK_MODE_ALL</code>, <code>CLASSIFICATION_MODE_ALL</code> 을 사용했는데 각각 정확도 위주, 얼굴의 표지점 표시(눈, 코, 입 등), 카테고리 분류(웃음, 눈 뜸 등)을 뜻한다.</p>
<p>나는 얼굴 인식 기능만 사용할 거라 위 설정이 굳이 필요하지는 않았지만 한번 넣어봤다. 설정에 관한 자세한 정보는 <a href="https://developers.google.com/ml-kit/vision/face-detection/android?hl=ko#1.-configure-the-face-detector">공식 문서</a>에서 확인해보자.</p>
<p>이제 <code>ImageProcessor</code>로 이미지를 분석할 수 있다. 나머지는 갤러리에서 불러온 이미지를 <code>ImageProcessor</code>에 넣고 결과값으로 나타난 얼굴 영역을 이미지 위에 그리는 것 뿐이다.</p>
<pre><code>...

    val faces = remember { mutableStateListOf&lt;Face&gt;() }

    LaunchedEffect(imageBitmap) {
        faces.clear()

        val image = InputImage.fromBitmap(imageBitmap.asAndroidBitmap(), 0)
        faces.addAll(ImageProcessor.processInputImage(image))
    }

    ...

        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            drawImage(
                image = imageBitmap
            )
            faces.forEach { face -&gt;
                val rect = face.boundingBox
                drawRect(
                    color = Color.Green,
                    style = Stroke(
                        width = 2.dp.toPx()
                    ),
                    topLeft = Offset(rect.left.toFloat(), rect.top.toFloat()),
                    size = Size(rect.width().toFloat(), rect.height().toFloat()),
                )
            }
        }</code></pre><p>이미지를 불러와서 <code>imageBitmap</code> 값이 변하면 <code>LaunchedEffect</code> 블럭 내 코드가 동작한다.</p>
<p><code>ImageProcessor.processInputImage()</code>로 불러온 이미지를 분석하여 나온 얼굴값 <code>List&lt;Face&gt;</code>를 <code>faces</code>에 모두 넣어준다.</p>
<p>그리고 <code>Canvas</code>에서 각 얼굴마다 <code>boundingBox</code> 객체를 가져와 사각형으로 해당 얼굴 위치를 그려준다.</p>
<h3 id="7-결과">7. 결과</h3>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/ac81ccc4-3335-4e16-9dfb-634bb9bd643a/image.jpg" alt=""></p>
<p>얼굴 인식이 정상적으로 작동되는 것을 확인할 수 있다. 실제로 사용해보면 꽤 속도도 빠르고 정확도도 높아서 배포된 공식 라이브러리만으로도 괜찮은 앱을 만들 수 있을 거 같다는 생각이 들었다.</p>
<p><a href="https://github.com/Moonsyn/FaceDetcetion">Github 링크</a>
참고문서: <a href="https://developers.google.com/ml-kit/vision/face-detection/android?hl=ko">안드로이드 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Array<Int> vs IntArray]]></title>
            <link>https://velog.io/@moonliam_/Kotlin-ArrayInt-vs-IntArray</link>
            <guid>https://velog.io/@moonliam_/Kotlin-ArrayInt-vs-IntArray</guid>
            <pubDate>Thu, 18 Jan 2024 09:55:17 GMT</pubDate>
            <description><![CDATA[<h2 id="1-arrayint-vs-intarray">1. Array&lt;Int&gt; vs IntArray</h2>
<p>얼마 전 코딩테스트를 준비하는데 가능한 언어가 Kotlin, Swift 뿐이라 열심히 Kotlin으로 코테를 준비하고 있었다. (보통은 파이썬으로 코테를 하는데... 꽤 당황스러웠다.)</p>
<p>그러던 중 의문이 들었던 게 문제에 주어지는 Int형 배열의 자료형이 <code>Array&lt;Int&gt;</code>로 나올 때도 있고 <code>IntArray</code>로 나올 때도 있었다는 것이다.</p>
<p>작동하는 방법 자체는 거의 비슷한 거 같은데 무슨 차이가 있을까? 궁금한 마음에 살펴보기로 했다.</p>
<h2 id="2-기본형-타입primitive-type-vs-참조형-타입reference-type">2. 기본형 타입(Primitive Type) vs 참조형 타입(Reference Type)</h2>
<p>Java 변수에는 크게 2종류가 있다. </p>
<p>바로 <strong>기본형 타입(Primitive Type)</strong>과 <strong>참조형 타입(Reference Type)</strong>이다.</p>
<p>기본형 타입에는 <strong>논리형(boolean), 문자형(char), 정수형(byte, short, int, long), 실수형(float, double)</strong>이 존재한다.</p>
<p>이들 기본형 타입 변수들의 특징은 </p>
<ol>
<li>변수의 <strong>선언과 동시에 메모리 생성</strong></li>
<li>모든 값 타입은 <strong>메모리의 스택(stack)에 저장됨</strong></li>
<li>저장공간에 <strong>실제 자료 값을 가진다</strong></li>
</ol>
<p>반면에 <strong>참조형 타입이란 기본형 타입을 제외한 나머지</strong>를 말한다고 보면 된다. 배열, 클래스, 인터페이스 등등 모두 참조형이다.</p>
<p>참조형 타입은 기본형 타입과는 다르게</p>
<ol>
<li>실제 값이 저장되지 않고, <strong>자료가 저장된 공간의 주소를 저장</strong>한다. </li>
<li>메모리의 <strong>힙(heap)에 실제 값을 저장</strong>하고, 그 <strong>참조값(주소값)을 갖는 변수는 스택에 저장</strong>한다.</li>
</ol>
<p>위 내용을 그림으로 요약하자면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/d3c74d15-0fe7-46fc-b667-ca19e06188d7/image.png" alt=""></p>
<h2 id="3-wrapper-클래스와-박싱-언박싱">3. Wrapper 클래스와 박싱, 언박싱</h2>
<p>이처럼 자바 변수는 기본형 타입과 참조형 타입으로 나뉘어져있는데 이로 인해 생길 수 있는 문제가 있다.</p>
<p>예를 들어 특정 메소드가 인수로 객체 타입만을 요구한다면 기본형 타입 변수를 사용할 수 없게 되어버리는 것이다.</p>
<p>또한, 멀티스레드 환경에서 기본형 변수를 동기화 데이터로 사용하려면 이를 객체화해야 할 필요가 있다.</p>
<p>이를 위해서 나온 개념이 바로 <strong>Wrapper 클래스</strong>이다.</p>
<p>*<em>Wrapper 클래스는 자바의 모든 기본형 타입을 값으로 갖는 객체를 생성할 수 있다. *</em></p>
<p>값을 포장하여 해당 값을 갖는 새로운 객체로 만들어준다고 생각하면 이해가 쉬울 것이다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/5e8f6529-dedf-4fd1-b8c3-f4eb160213d4/image.png" alt=""></p>
<pre><code>int a = 10; // 기존의 기본형 타입으로 선언
Integer b = new Integer(10); 기본형 타입을 래퍼 클래스를 이용해 객체화</code></pre><p>기본형 타입과 참조형 타입을 다루다 보면 자연스럽게 위와 같은 객체화 작업을 수행하거나 반대로 객체화된 변수를 다시 기본형 타입으로 값을 꺼내야 할 필요가 생긴다.</p>
<p>이러한 동작을 <strong>박싱(boxing), 언박싱(unboxing)</strong>이라고 한다.</p>
<ol>
<li><strong>박싱(Boxing)</strong>: 기본 타입의 데이터 → 래퍼 클래스의 인스턴스로 변환</li>
<li><strong>언박싱(UnBoxing)</strong>: 래퍼 클래스의 인스턴스에 저장된 값 → 기본 타입의 데이터로 변환</li>
</ol>
<p>이러한 박싱, 언박싱 작업은 당연히 프로그램 실행 시 추가적인 연산 작업을 필요로 하고 지나치게 많은 박싱, 언박싱 작업이 발생할 경우 성능에 영향을 미칠 수 있다.</p>
<p>따라서, 볼륨이 큰 프로그램을 짠다거나 할 때는 *<em>박싱, 언박싱 같은 오토 캐스팅(auto casting) 작업이 지나치게 많이 발생하는 건 아닌지 꼼꼼히 살펴봐야한다. *</em></p>
<h2 id="3-다시-arrayint-vs-intarray">3. 다시 Array&lt;Int&gt; vs IntArray</h2>
<p>다시 돌아와서 Kotlin의 <code>Arrat&lt;Int\&gt;</code>와 <code>IntArray</code> 두 배열을 살펴보자. 위에 쭉 설명해서 알겠지만 <strong>이 둘의 차이점은 바로 int 정수 타입을 기본형으로 사용하냐 참조형으로 사용하냐의 차이이다.</strong></p>
<p>Kotlin의 <code>Array&lt;Int&gt;</code>는 참조형 타입 변수를 사용하며 자바에서 <code>Integer[]</code> 동일한 개념이라고 볼 수 있다.
한편, <code>IntArray</code>는 기본형 타입 변수를 사용하며 자바의 <code>int[]</code>와 같은 개념이다.</p>
<p>이는 정수형 변수 뿐만 아니라 자바에서 사용되는 모든 기본형 타입으로 존재할 수 있는 변수들에 모두 해당되는 이야기이다.</p>
<p>물론 <code>Array&lt;Int&gt;</code>와 <code>IntArray</code>를 서로 변환하는 것은 어렵지 않다.</p>
<p><code>Array&lt;Int&gt;</code>는 <code>toIntArray()</code> 메소드를 이용해서 <code>IntArray</code> 타입으로 바꿀 수 있으며 그 반대는 <code>toTypedArray()</code> 메소드를 이용하면 된다.</p>
<p>하지만 위에서도 적었듯이 이러한 박싱, 언박싱 과정은 프로그램 수행 과정에서 오버헤드를 일으키기 때문에 적절하게 사용하는 것이 중요하다.</p>
<blockquote>
<p>레퍼런스</p>
</blockquote>
<ol>
<li><a href="https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EB%B3%80%EC%88%98%EC%9D%98-%EA%B8%B0%EB%B3%B8%ED%98%95-%EC%B0%B8%EC%A1%B0%ED%98%95-%ED%83%80%EC%9E%85">☕ JAVA 변수의 기본형 &amp; 참조형 타입 차이 이해하기</a></li>
<li><a href="https://inpa.tistory.com/entry/JAVA-%E2%98%95-wrapper-class-Boxing-UnBoxing">☕  자바 Wrapper 클래스와 Boxing &amp; UnBoxing 총정리</a></li>
<li><a href="https://javarevisited.blogspot.com/2015/09/difference-between-primitive-and-reference-variable-java.html?utm_content=bufferfaa9a&amp;utm_medium=social&amp;utm_source=twitter.com&amp;utm_campaign=buffer#axzz8P9wAu7gI">10 Difference between Primitive and Reference variable in Java - Example Tutorial</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] MVI Pattern - (1)]]></title>
            <link>https://velog.io/@moonliam_/Android-MVI-Pattern-1</link>
            <guid>https://velog.io/@moonliam_/Android-MVI-Pattern-1</guid>
            <pubDate>Mon, 15 Jan 2024 10:33:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-compose--mvvm-is-good">1. Compose + MVVM... is Good?</h2>
<p>최근 진행 중인 사이드 프로젝트에서 Jetpack Compose를 다루기 시작하면서 Compose에 대해 이것 저것 알아보는 시간이 늘어나고 있다.</p>
<p>Compose로 개발을 하면서 느낀 건 바로 <code>State</code> 관리의 중요성이다. Compose는 기존의 안드로이드 xml처럼 명령형 UI가 아닌 선언형 UI다. 이미 선언된 UI에 표시되는 데이터를 변경하기 위해서는 <code>State</code>를 변경하여 <code>Recomposition (재구현)</code>을 진행해야한다. </p>
<p>즉, Compose를 사용하면서 <code>State</code>를 다루지 않겠다는 건 화면 UI에 초기 데이터 이후 어떤 데이터 변경도 표시하지 않겠다는 이야기다.</p>
<p>이렇게 <code>State</code> 관리의 중요성을 알다보니 생각보다 꽤 머리가 복잡해졌다. 여러가지 이유가 있지만 그 중 하나는 기존의 가장 보편적인 안드로이드 디자인 패턴인 <strong>MVVM 패턴이 Compose에 과연 잘 맞나?</strong> 하는 의문이 들기 시작한 것이다. </p>
<p>내가 가장 크게 느낀 부분은 바로 <strong>생명주기의 차이</strong>이다.</p>
<p><code>Composable</code> 구성요소들은 <code>State</code>가 변경되는 <code>Recomposition</code>이 일어나면서 재구성이 일어나면서 새로 생성된다. 하지만 <code>ViewModel</code>은 종속된 Activity나 Fragment가 완전히 종료될때까지 동일한 인스턴스를 호출한다.</p>
<p>그렇기때문에 만약 <code>ViewModel</code>에서 특정 <code>Composable</code>의 생명주기에 의존하는 값이나 메소드를 가지고 있을 경우 의도치 않은 다른 결과를 불러일으킬 수도 있다.</p>
<p>실제로 <a href="https://developer.android.com/jetpack/compose/interop/compose-in-existing-arch?hl=ko#viewmodel">안드로이드 공식 홈페이지</a>에서도 이 점에 대해서 주의할 것을 명시하고 있다.</p>
<pre><code>class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen(&quot;user1&quot;)
                    GreetingScreen(&quot;user2&quot;)
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
) {
    val messageUser by viewModel.message.observeAsState(&quot;&quot;)
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData(&quot;Hi $userId&quot;)
    val message: LiveData&lt;String&gt; = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress(&quot;UNCHECKED_CAST&quot;)
    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {
        return GreetingViewModel(userId) as T
    }
}</code></pre><p>위처럼 <code>GreetingScreen</code>이 서로 다른 <code>userId</code> 값을 값고 2번 호출되어도 <code>GreetingViewModel</code>은 <code>GreetingActivity</code>가 완전히 종료되기 전까지 동일한 인스턴스를 반환하기 때문에 2번의 <code>GreetingScreen</code>은 모두 &quot;user1&quot;에 대한 인사말을 표시하게된다.</p>
<h2 id="2-mvi-패턴이란">2. MVI 패턴이란?</h2>
<p>이런저런 이유로 Compose에 대해서 좀 더 알아보고 있을 때 우연히 <strong>MVI 패턴</strong>에 대한 이야기를 들었다. 찾아보니 대충 MVI 패턴을 사용하면 <code>State(상태)</code> 관리를 좀 더 쉽게 다룰 수 있다는 이야기를 들었고 Compose로 UI를 구성한 앱에서 효과적인 디자인 패턴이라는 아닐까하는 생각이 들었다.</p>
<h3 id="mvi-패턴의-구조">MVI 패턴의 구조</h3>
<p><strong>MVI</strong> 패턴은 <strong>Model, View, Intent</strong> 크게 3가지 구성요소로 이루어져있다.</p>
<ul>
<li><strong>Model</strong>: UI에 반영될 <strong>상태</strong>를 의미한다. 데이터를 의미하는 MVP, MVVM에서의 정의와는 다르다.</li>
<li><strong>View</strong>: UI 그 자체. View, Activity, Fragment, Compose 등이 될 수 있다.</li>
<li><strong>Intent</strong>: 우리가 흔히 안드로이드에서 다루는 그 Intent와는 다르다. MVI에서는 <strong>사용자 액션 및 시스템 이벤트에 따른 결과</strong>라고 해석할 수 있다. 
<img src="https://velog.velcdn.com/images/moonliam_/post/650ccc93-0e2b-4fac-8634-65abc0a1cd05/image.png" alt=""></li>
</ul>
<p>쉽게 설명하자면, User(사용자)가 View를 클릭하거나 하는 등의 행동(Action)을 취하면 이 행동이 <code>Intent(의도)</code>가 되어서 <code>Model</code>에 전달된다. 이 <strong>Intent가 Model, 즉 상태를 업데이트한다. 그리고 이 변경된 상태가 다시 View에 반영된다.</strong></p>
<p>MVI의 핵심은 <strong>단방향 흐름(Uni-directional Flow)</strong> 구조이다. 그래서 아래 그림처럼 표현하기도 한다.
<img src="https://velog.velcdn.com/images/moonliam_/post/991ba66e-05ac-440a-9d55-eb413496bf21/image.png" alt=""></p>
<p>기존의 MVVM 관점에서 MVI 패턴을 계층 구조로 나누어 보자면 아래 그림처럼 표시할 수 있다.
<img src="https://velog.velcdn.com/images/moonliam_/post/3079d40d-6b81-4a12-af1f-9ca16f0fc80f/image.png" alt=""></p>
<p>그렇다! 나는 MVVM과 MVI를 완전히 다른 새로운 개념이라고 인식했는데 그게 아니었다. MVI에서도 여전히 상태 관리를 위해 <code>ViewModel</code>을 사용하며 기존의 MVVM에서 Compose의 상태 관리를 좀 더 수월하게 하기 위해 나온 개념이라고 생각하는 게 맞는 거 같다.</p>
<h2 id="3-예제">3. 예제</h2>
<h3 id="uistate-uievent-uieffect-인터페이스">UiState, UiEvent, UiEffect 인터페이스</h3>
<p>먼저 3개의 인터페이스가 필요하다.</p>
<pre><code>interface UiState

interface UiEvent

interface UiEffect</code></pre><ul>
<li><code>UiState</code>: View의 상태(State)를 나타낸다.</li>
<li><code>UiEvent</code>: 사용자(User)의 행동(Action)을 나타낸다.</li>
<li><code>UiEffect</code>: 에러 메세지 표시와 같이 단 한번만 보여주고자 하는 <code>Side Effect</code>를 나타낸다.</li>
</ul>
<h3 id="baseviewmodel-구현">BaseViewModel 구현</h3>
<p>다음은 <code>ViewModel</code>의 Base 클래스를 구성한다.</p>
<pre><code>abstract class BaseViewModel&lt;Event : UiEvent, State : UiState, Effect : UiEffect&gt; : ViewModel() {

    // Create Initial State of View
    private val initialState : State by lazy { createInitialState() }
    abstract fun createInitialState() : State

    // Get Current State
    val currentState: State
        get() = uiState.value

    private val _uiState : MutableStateFlow&lt;State&gt; = MutableStateFlow(initialState)
    val uiState = _uiState.asStateFlow()

    private val _event : MutableSharedFlow&lt;Event&gt; = MutableSharedFlow()
    val event = _event.asSharedFlow()

    private val _effect : Channel&lt;Effect&gt; = Channel()
    val effect = _effect.receiveAsFlow()

}</code></pre><p>특이한 점은 <code>State</code>, <code>Event</code>, <code>Effect</code>를 각각 <code>StateFlow</code>, <code>SharedFlow</code>, <code>Channel</code>로 다르게 관리한다는 것이다.</p>
<h4 id="stateflow-vs-sharedflow-vs-channel">StateFlow vs SharedFlow vs Channel</h4>
<p><code>StateFlow</code>는 초기값을 가지고 있다는 점만 제외하면 기존의 <code>LiveData</code>와 크게 다르지 않다. <strong>초기값을 가져야하고 항상 최신 값을 필요로 하는 UiState에 적절하다.</strong></p>
<p><code>SharedFlow</code>는 발생하는 이벤트 구독자(Subscribers)가 0명일수도 있고 여러명일수도 있다.(이벤트 공유) 만약 구독자가 한명도 없다면, 이벤트는 그대로 무시된다(dropped). <strong>이벤트를 처리해야하는 구독자(subscriber)가 존재하지 않는다면 무시될 필요가 있는 UiEvent에 적절하다.</strong></p>
<p>반면에 <code>Channel</code>은 각각의 이벤트가 오직 하나의 구독자에게만 전달된다.(이벤트 공유X) 만약 구독자가 없을 때 이벤트가 발생했다면 채널 버퍼가 가득차자마자 구독자가 나타날때까지 일시중지된다(suspend). 따라서 이벤트가 무시되지 않는다. 
<code>Channel</code>은 Hot Stream이기도 하고 방향이 변경되거나 UI가 다시 표시될 때 Side Effect를 다시 표시할 필요가 없다. 단순하게 <code>SingleLiveEvent</code> 동작을 복제하고 싶기 때문에 <code>Channel</code>을 사용한다.</p>
<p>다음은 <code>UiState</code>, <code>UiEvent</code>, <code>UiEffect</code> 각각에 대한 setter 메소드를 구성해준다.</p>
<pre><code>    /**
     * Set new Event
     */
    fun setEvent(event : Event) {
        val newEvent = event
        viewModelScope.launch { _event.emit(newEvent) }
    }


    /**
     * Set new Ui State
     */
    protected fun setState(reduce: State.() -&gt; State) {
        val newState = currentState.reduce()
        _uiState.value = newState
    }

    /**
     * Set new Effect
     */
    protected fun setEffect(builder: () -&gt; Effect) {
        val effectValue = builder()
        viewModelScope.launch { _effect.send(effectValue) }
    }</code></pre><p><code>Events</code>를 처리하기 위해서 <code>event</code> Flow를 수집(collect)해야 한다. 이는 <code>ViewModel</code>의 <code>init</code> 블럭에서 처리한다.</p>
<pre><code>    init {
        subscribeEvents()
    }

    /**
     * Start listening to Event
     */
    private fun subscribeEvents() {
        viewModelScope.launch {
            event.collect {
                handleEvent(it)
            }
        }
    }

    /**
     * Handle each event
     */
    abstract fun handleEvent(event : Event)</code></pre><p>이렇게 기본적인 <code>BaseViewModel</code> 구현은 끝났다. 이제 <code>MainActivity</code>와 <code>MainViewModel</code>사이를 이어줄 <code>MainContract</code> 구현을 할 차례다.</p>
<h3 id="maincontract-구현">MainContract 구현</h3>
<pre><code>class MainContract {

    // Events that user performed
    sealed class Event : UiEvent {
        object OnRandomNumberClicked : Event()
        object OnShowToastClicked : Event()
    }

    // Ui View States
    data class State(
        val randomNumberState: RandomNumberState
    ) : UiState

    // View State that related to Random Number
    sealed class RandomNumberState {
        object Idle : RandomNumberState()
        object Loading : RandomNumberState()
        data class Success(val number : Int) : RandomNumberState()
    }

    // Side effects
    sealed class Effect : UiEffect {

        object ShowToast : Effect()

    }

}</code></pre><p>이 예제에서는 2개의 이벤트만이 존재한다. <code>OnRandomNumberClicked</code>는 사용자가 임의의 숫자 버튼을 클릭했을 때 호출된다. 물론 토스트 메세지를 호출하고 <code>SingleLiveEvent</code> 동작을 시뮬레이트하는 토스트 버튼도 있다.</p>
<p><code>RandomNumberState</code>는 <code>Idle, Loading 그리고 Success</code>라는 각기 다른 State를 갖는 StateHolder 클래스이다. </p>
<p><code>State</code>는 UI 상태를 따르는 간단한 데이터 클래스이다.
<code>Effect</code>는 <code>Event</code> 결과에 따라 한번만 보여주고 싶은 Side Effect들이 있는 클래스이다.</p>
<p><code>RandomNumberState</code>처럼 View State를 sealed 클래스로 사용하지 않고 <code>State</code> 데이터 클래스에 변수로써 표현해도 된다.</p>
<pre><code>date class State(
    val isLoading: Boolean = false,
    val randomNumber: Int = -1,
    val error: String? = null
): UiState</code></pre><p><code>MainContract</code>도 구현 완료됐으니 이제 <code>MainViewModel</code>에서 실제 로직에 적용할 차례다.</p>
<h3 id="mainviewmodel-구현">MainViewModel 구현</h3>
<pre><code>class MainViewModel&lt;E: Event, S: State, E: Effect&gt; : BaseViewModel() {
    /**
     * Create initial State of Views
     */
    override fun createInitialState(): MainContract.State {
        return MainContract.State(
            MainContract.RandomNumberState.Idle
        )
    }

    /**
    * Handle each event
    */
    override fun handleEvent(event: MainContract.Event) {
        when (event) {
            is MainContract.Event.OnRandomNumberClicked -&gt; {
                generateRandomNumber() 
            }
            is MainContract.Event.OnShowToastClicked -&gt; {
                setEffect { MainContract.Effect.ShowToast }
            }
        }
    }

    /**
     * Generate a random number
     */
    private fun generateRandomNumber() {
        viewModelScope.launch {
            // Set Loading
            setState { copy(randomNumberState = MainContract.RandomNumberState.Loading) }
            try {
                // Add delay for simulate network call
                delay(5000)
                val random = (0..10).random()
                if (random % 2 == 0) {
                    // If error happens set state to Idle
                    // If you want create a Error State and use it
                    setState { copy(randomNumberState = MainContract.RandomNumberState.Idle) }
                    throw RuntimeException(&quot;Number is even&quot;)
                }
                // Update state
                setState { copy(randomNumberState = MainContract.RandomNumberState.Success(number = random)) }
            } catch (exception : Exception) {
                // Show error
                setEffect { MainContract.Effect.ShowToast }
            }
        }
    }
}</code></pre><p><code>handleEvent</code>에서 이벤트에 대한 처리를 진행한다. 만약 <code>Contract</code>에 새로운 이벤트가 추가되면 <code>handleEvent</code>에도 해당 이벤트에 대한 처리 코드를 추가해줘야한다.</p>
<p><code>OnRadomNumberClicked</code> 이벤트가 발생하면 <code>generateRandomNumber</code> 메소드를 호출한다. 이 메소드에서는 먼저 State를 <code>Loading</code>으로 바꾼 다음에 결과값에 따라서 State를 다시 <code>Success</code> 또는 <code>Idle</code>로 변경한다.</p>
<p>만약 에러가 발생했을 경우 토스트 메세지를 띄우는 <code>Effect</code>를 설정할 수도 있다.</p>
<p>이제 마지막으로 UI 단계에 적용하는 일만 남았다.</p>
<h3 id="mainactivity">MainActivity</h3>
<pre><code>binding.generateNumber.setOnClickListener {
    viewModel.setEvent(MainContract.Event.OnRandomNumberClicked)
}
binding.showToast.setOnClickListener {
    viewModel.setEvent(MainContract.Event.OnShowToastClicked)
}</code></pre><p>버튼을 클릭할때마다 해당하는 이벤트를 발생시킨다.</p>
<pre><code>// Collect Ui State
lifecycleScope.launchWhenStarted {
    viewModel.uiState.collect {
        when (it.randomNumberState) {
            is MainContract.RandomNumberState.Idle -&gt; { binding.progressBar.isVisible = false }
            is MainContract.RandomNumberState.Loading -&gt; { binding.progressBar.isVisible = true }
            is MainContract.RandomNumberState.Success -&gt; {
                binding.progressBar.isVisible = false
                binding.number.text = it.randomNumberState.number.toString()
            }
        }
    }
}

// Collect Side Effects
lifecycleScope.launchWhenStarted {
    viewModel.effect.collect {
        when (it) {
            is MainContract.Effect.ShowToast -&gt; {
                binding.progressBar.isVisible = false
                // Simple method that shows a toast
                showToast(&quot;Error, number is even&quot;)
            }
        }
    }
}</code></pre><p>UI를 업데이트하기 위해서 <code>uiState</code>를 구독해 최신 State를 collect하고 이에 맞춰 View를 업데이트 한다.</p>
<p><code>LiveData</code>처럼 동작하기 위해서 <code>launchWhenStarted</code> 메소드를 사용한다. 이를 통해 Flow는 생명주기가 최소 <code>STARTED</code> 상태가 되고 나서야 State를 collect할 것이다.</p>
<h3 id="요약">요약</h3>
<p>위에 구현된 예제 앱을 요약해보자.</p>
<p>먼저, 버튼 클릭과 같은 사용자 액션 <code>Event</code>를 발생시킨다. 그러면 그 이벤트의 결과로 변경되지않는 새로운 State를 설정한다. 이 State는 Idle, Loading, Success가 될 수 있다. <code>StateFlow</code>를 사용했기 때문에 새로운 State가 설정되자마자 UI를 이에 맞춰 업데이트한다.</p>
<p>만약 에러가 발생하거나 Toast 메세지 등을 띄워야한다면 새로운 <code>Effect</code>를 설정해 처리한다.</p>
<h2 id="4-mvi-패턴의-장단점">4. MVI 패턴의 장단점</h2>
<p><strong>장점</strong></p>
<ol>
<li>상태 객체는 불변이므로 스레드로부터 안전하다.</li>
<li>State, Event, Effect 등의 모든 액션이 같은 파일에 있어 화면에서 일어나는 일을 한눈에 쉽게 이해할 수 있다.</li>
<li>상태를 유지하는 것이 쉽다.</li>
<li>데이터 흐름이 단방향으로 흐르기 때문에 추적이 쉽다.</li>
</ol>
<p><strong>단점</strong></p>
<ol>
<li>많은 보일러플레이트 코드가 발생한다.</li>
<li>많은 객체를 생성해야 하기 때문에 높은 메모리 관리가 필요하다.</li>
<li>하나의 화면에서 많은 뷰와 복잡한 로직을 가지게 될 경우, State는 거대해지고 이 State를 단지 하나를 사용하는 대신 StateFlow를 추가 사용하여 더 작은 것으로 분할할 수 있다.</li>
</ol>
<h2 id="5-결론">5. 결론</h2>
<p><strong>MVI</strong> 패턴은 MVC, MVVM과 같은 MVx 패턴에 가장 최근에 추가된 패턴이다. MVVM과 공통점이 많지만 상태 관리 측면에서 좀 더 구조화된 방법을 갖고 있다. 전체 예제 소스 코드는 <a href="https://github.com/yusufceylan/MVI-Playground">링크</a>에서 확인할 수 있다.</p>
<p>추가로 <strong>MVI 패턴을 쉽게 다룰 수 있게 해주는 라이브러리도 있다니 시간나면 한번 살펴보자</strong> 
<a href="https://github.com/orbit-mvi/orbit-mvi">라이브러리 Github 링크</a></p>
<blockquote>
<p>레퍼런스</p>
</blockquote>
<ol>
<li><a href="https://developer.android.com/jetpack/compose/interop/compose-in-existing-arch?hl=ko">Compose를 기존 앱 아키텍처와 통합</a></li>
<li><a href="https://developer.android.com/jetpack/compose/libraries?hl=ko">Compose 및 기타 라이브러리</a></li>
<li><a href="https://www.charlezz.com/?p=46365">Android 프로젝트에 MVI 도입하기</a></li>
<li><a href="https://proandroiddev.com/mvi-architecture-with-kotlin-flows-and-channels-d36820b2028d">MVI Architecture with Kotlin Flows and Channels</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS / Network] TCP vs UDP]]></title>
            <link>https://velog.io/@moonliam_/CS-Network-TCP-vs-UDP</link>
            <guid>https://velog.io/@moonliam_/CS-Network-TCP-vs-UDP</guid>
            <pubDate>Fri, 12 Jan 2024 08:26:33 GMT</pubDate>
            <description><![CDATA[<p>최근 기술 면접을 보러 간 회사에서 <strong>TCP 통신과 UDP 통신의 차이점에 대해서 설명해주세요</strong>라는 질문을 받았는데 순간 말문이 턱 막혔다.</p>
<p>생각해보니까 안드로이드 개발자라고 안드로이드나 객체지향 프로그래밍 쪽 공부만 하다보니 다른 CS 지식에 소홀했다는 생각이 들었다.</p>
<p>게다가 학부생 시절에도 네트워크 쪽은 워낙 좋아하지 않았어서... 특히 약한 부분이었는데 허를 제대로 찔렸다는 느낌이었다.</p>
<p>다행히 다른 질문에는 잘 대답해서 면접은 통과했지만 공부는 필요하다고 느꼈다.</p>
<h2 id="1-osi-7계층">1. OSI 7계층</h2>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/39f3c092-2f70-4510-8ec2-f4968211ceb6/image.png" alt="">
TCP, UDP 이야기에 앞서 네트워크 통신 과정에 대한 이해가 필요하다. <strong>OSI 7계층 모델은 바로 이 네트워크 통신 과정을 총 7개의 계츠응로 구분한 산업 표준 모델</strong>이다.</p>
<h3 id="왜-계층으로-나눌까">왜 계층으로 나눌까?</h3>
<ul>
<li>통신이 일어나는 과정을 단계별로 알 수 있고, 특정한 곳에 이상이 생기면 그 단계만 수정할 수 있기 때문이다.</li>
</ul>
<p>OSI 7계층은 각각 밑에서부터 순서대로 다음과 같이 구성되어있다.</p>
<ol>
<li>*<em>물리 계층 (Physical Layer): *</em>데이터를 전기적인 신호로 변환해서 주고받는 기능을 담당</li>
<li>*<em>링크 계층 (Link Layer): *</em>물리 계층으로 송수신되는 정보를 관리하여 안전하게 전달되도록 도와주는 역할</li>
<li>*<em>네트워크 계층 (Network Layer): *</em>데이터를 목적지까지 가장 안전하고 빠르게 전달하는 기능 담당</li>
<li>*<em>전송 계층 (Transport Layer): *</em>TCP와 UDP 프로토콜을 통해 통신 활성화. 포트를 개방하고 프로그램들이 전송할 수 있도록 제공</li>
<li>*<em>세션 계층 (Session Layer): *</em>데이터가 통신하기 위한 논리적 연결 담당. TCP/IP 세션을 만들고 없애는 책임</li>
<li>*<em>표현 계층 (Presentation Layer): *</em>데이터 표현에 대한 독립성 제공 및 암호화 역할 담당</li>
<li>*<em>응용 계층 (Application Layer): *</em>통신의 최종 목적지. 응용 프로세스와 직접 관계하여 일반적인 응용 서비스를 수행. 사용자 인터페이스, 전자우편, 데이터베이스 관리 등의 서비스 제공.</li>
</ol>
<h2 id="2-tcpip-모델">2. TCP/IP 모델</h2>
<p>위의 OSI 7계층 모델은 어디까지나 참조 모델일뿐 실제 인터넷 프로토콜은 이 구조를 완전히 따르지는 않는다. <strong>인터넷 프로토콜 스택(Internet Protocol Stack)</strong>은 현재 대부분 <strong>TCP/IP</strong>를 따른다.</p>
<p>TCP/IP는 인터넷 프로토콜 중 가장 중요한 역할을 하는 <code>TCP</code>와 <code>IP</code>의 합성어로 <strong>데이터의 흐름 관리, 정확성 확인, 패킷의 목적지 보장</strong>을 담당한다. 데이터의 정확성 확인은 <code>TCP</code>가, 패킷을 목적지까지 전송하는 일은 <code>IP</code>가 담당한다.
<img src="https://velog.velcdn.com/images/moonliam_/post/f3fda06b-3ab5-4750-ac08-8db2f5bfe6b3/image.png" alt=""></p>
<ul>
<li>TCP/IP 모델은 위 이미지처럼 OSI 7계층과 다르게 4계층으로 구성되어있다.</li>
<li>OSI 모델의 응용, 표현, 세션 계층의 구현은 TCP/IP 모델에서 응용 계층 안에 다 이루어진다.</li>
</ul>
<h2 id="3-tcp-vs-udp">3. TCP vs UDP</h2>
<p><code>TCP (Transmission Control Protocol)</code>, <code>UDP (User Datagram Protocol)</code>. 둘 모두 TCP/IP 모델의 <strong>전송계층</strong>에서 사용되는 프로토콜의 한 종류이다. <strong>전송계층은 IP애 의해 전달되는 패킷의 오류를 검사하고 재전송 요구 등의 제어를 담당</strong>하는 계층이다.</p>
<p>따라서 <strong>TCP와 UDP의 차이점은 IP에 의해 패킷을 전달하고 이 과정에서 오류 검사 및 재전송 등을 하는 방법의 차이점과 같다.</strong></p>
<p>간단하게 요약하자면 <strong>TCP는 신뢰성, 연결지향적이고 UDP는 비신뢰성, 비연결성</strong>을 지향한다.</p>
<p>이는 두 프로토콜의 데이터 송신 방법의 차이를 비교한 그림으로 이해할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/fc3378c7-804e-4fe3-a6e6-121f442e262d/image.png" alt=""></p>
<p><code>TCP</code>는 데이터(패킷)의 전송 과정의 신뢰성을 보장하기 위해 수신자에게 수신 가능여부를 확인하고 송신한 후에는 정상 수신 여부를 계속 확인한다. (흐름제어 / 혼잡제어)
반면 <code>UDP</code>는 수신자의 상태에 상관없이 일방적으로 데이터를 송신한다.</p>
<p>따라서 <strong>신뢰성이 요구되는 애플리케이션에서는 TCP를 사용</strong>하고 <strong>간단한 데이터를 빠른 속도로 전송하고자 하는 애플리케이션에서는 UDP를 사용</strong>한다.</p>
<h2 id="4-정리">4. 정리</h2>
<table>
<thead>
<tr>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>연결지향형 프로토콜</td>
<td>비 연결지향형 프로토콜</td>
</tr>
<tr>
<td>바이트 스트림을 통한 연결</td>
<td>메세지 스트림을 통한 연결</td>
</tr>
<tr>
<td>혼잡제어, 흐름제어</td>
<td>혼잡제어와 흐름제어 지원 X</td>
</tr>
<tr>
<td>순서 보장, 상대적으로 느림</td>
<td>순서 보장되지 않음, 상대적으로 빠름</td>
</tr>
<tr>
<td>신뢰성 있는 데이터 전송 - 안정적</td>
<td>데이터 전송 보장 X</td>
</tr>
<tr>
<td>세그먼트 TCP 패킷</td>
<td>데이터그램 UDP 패킷</td>
</tr>
<tr>
<td>HTTP, Email, File transfer에서 사용</td>
<td>DNS, Broadcasting (도메인, 실시간 동영상 서비스에서 사용)</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>출처</strong></p>
</blockquote>
<ol>
<li><a href="https://velog.io/@hidaehyunlee/%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A0%84%EB%8B%AC%EB%90%98%EB%8A%94-%EC%9B%90%EB%A6%AC-OSI-7%EA%B3%84%EC%B8%B5-%EB%AA%A8%EB%8D%B8%EA%B3%BC-TCPIP-%EB%AA%A8%EB%8D%B8">&quot;데이터가 전달되는 원리&quot; OSI 7계층 모델과 TCP/IP 모델</a></li>
<li><a href="https://velog.io/@hidaehyunlee/TCP-%EC%99%80-UDP-%EC%9D%98-%EC%B0%A8%EC%9D%B4">TCP와 UDP 차이를 자세히 알아보자</a></li>
<li><a href="https://gyoogle.dev/blog/computer-science/network/OSI%207%EA%B3%84%EC%B8%B5.html">OSI 7계층</a></li>
<li><a href="https://gyoogle.dev/blog/computer-science/network/%ED%9D%90%EB%A6%84%EC%A0%9C%EC%96%B4%20&amp;%20%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4.html">TCP/IP (흐름제어/혼잡제어)</a></li>
<li><a href="https://gyoogle.dev/blog/computer-science/network/UDP.html">UDP</a></li>
<li><a href="https://livenow14.tistory.com/57">[Network] TCP 연결과정에 대해 알아보자</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Compose로 그림판 구현하기 -3 ]]></title>
            <link>https://velog.io/@moonliam_/Android-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3</link>
            <guid>https://velog.io/@moonliam_/Android-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3</guid>
            <pubDate>Fri, 12 Jan 2024 07:12:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 프로젝트의 전체 코드는 <a href="https://github.com/Moonsyn/DrawingScreen">Github 링크</a>에서 확인할 수 있습니다.</p>
</blockquote>
<h2 id="1-그림판에-viewmodel-적용하기">1. 그림판에 ViewModel 적용하기</h2>
<p>이번 포스트에서는 저번에 말한대로 DrawingScreen에 ViewModel을 적용해보도록 할 거다. 
ViewModel을 적용하는 이유는 그림판에 그림을 그리다보면 화면을 이리저리 움직여서 회전하게 되는 경우가 많은데, 그리기와 관련된 Path 데이터를 View에서 관리하면 화면 회전시마다 그림판의 그림이 모두 날아가버리기 때문이다.</p>
<p>기존 코드에서는 point, points, path, paths, pathStyle 총 5개의 데이터를 View에서 관리한다. 여기서 <code>paths</code>와 <code>pathStyle</code>만 <strong>ViewModel</strong> 패턴을 적용해볼 것이다.</p>
<p>그 이유는 point, points, path는 한 획을 그리고 손을 떼면 바로 초기화가 되는 데이터인데다가 드래그 움직임에 따라 계속해서 변화가 발생하기 때문에 <strong>ViewModel &lt;-&gt; View 간 데이터 전송 과정이 지나치게 많이 발생하는 것을 방지</strong>하기 위해서이다.</p>
<p>물론 나머지 데이터 모두 ViewModel을 적용해도 상관없다.</p>
<h3 id="1-drawingviewmodel-생성">1. DrawingViewModel 생성</h3>
<pre><code>class DrawingViewModel: ViewModel() {
    // MutableLiveData는 수정이 가능함
    private val _paths = NonNullLiveData&lt;MutableList&lt;Pair&lt;Path, PathStyle&gt;&gt;&gt;(
         mutableListOf()
    )
    private val _pathStyle = NonNullLiveData(
        PathStyle()
    )

    private val removedPaths = mutableListOf&lt;Pair&lt;Path, PathStyle&gt;&gt;()

    // LiveData는 외부에서 수정이 불가능하게 설정
    // getter를 사용하여 데이터를 읽는 과정만 수행 가능
    val paths: LiveData&lt;MutableList&lt;Pair&lt;Path, PathStyle&gt;&gt;&gt;
        get() = _paths
    val pathStyle: LiveData&lt;PathStyle&gt;
        get() = _pathStyle

    fun updateWidth(width: Float) {
        val style = _pathStyle.value
        style.width = width

        _pathStyle.value = style
    }

    fun updateColor(color: Color) {
        val style = _pathStyle.value
        style.color = color

        _pathStyle.value = style
    }

    fun updateAlpha(alpha: Float) {
        val style = _pathStyle.value
        style.alpha = alpha

        _pathStyle.value = style
    }

    fun addPath(pair: Pair&lt;Path, PathStyle&gt;) {
        val list = _paths.value
        list.add(pair)
        _paths.value = list
    }

    fun undoPath() {
        val pathList = _paths.value
        if (pathList.isEmpty())
            return
        val last = pathList.last()
        val size = pathList.size

        removedPaths.add(last)
        _paths.value = pathList.subList(0, size-1)
    }

    fun redoPath() {
        if (removedPaths.isEmpty())
            return
        _paths.value = (_paths.value + removedPaths.removeLast()) as MutableList&lt;Pair&lt;Path, PathStyle&gt;&gt;
    }
}</code></pre><p>위처럼 DrawingViewModel 클래스를 생성해준다.</p>
<h3 id="2-nonnulllivedata-클래스-생성">2. NonNullLiveData 클래스 생성</h3>
<p>위의 DrawingViewModel을 보면 일반적인 MutableLiveData가 아닌 NonNullLiveData를 사용했다.</p>
<pre><code>/**
 * Returns the current value.
 * Note that calling this method on a background thread does not guarantee that the latest
 * value set will be received.
 *
 * @return the current value
 */
@SuppressWarnings(&quot;unchecked&quot;)
@Nullable
public T getValue() {
    Object data = mData;
    if (data != NOT_SET) {
        return (T) data;
    }
    return null;
}</code></pre><p>위 코드를 보면 알 수 있듯이 일반적인 LiveData의 getValue() 메소드는 T가 NonNull 타입이어도 초기값이 설정되어있지 않으면 null을 리턴한다.</p>
<p>이러한 특성 때문에 MutableLiveData를 업데이트할 때 계속 null-check를 하는 번거로운 과정을 없애기 위해 MutableLiveData를 상속받는 NonNullLiveData 클래스를 만들어 사용했다.</p>
<pre><code>class NonNullLiveData&lt;T: Any&gt;(defaultValue: T) : MutableLiveData&lt;T&gt;(defaultValue) {

    init {
        value = defaultValue
    }

    override fun getValue() = super.getValue()!!
}</code></pre><p><strong>paths, pathStyle State로 Observe하기</strong></p>
<pre><code>val paths by viewModel.paths.observeAsState()
val pathStyle by viewModel.pathStyle.observeAsState()</code></pre><p>observeAsState를 사용하면 LiveData를 옵저빙하면서도 State로 사용할 수 있다.</p>
<p><strong>ViewModel 패턴에 맞게 로직 수정</strong>
DrawingScreen.kt</p>
<pre><code>@Composable
fun DrawingScreen(
    viewModel: DrawingViewModel
) {
    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        DrawingCanvas(
            viewModel = viewModel
        )
    }
}

@Composable
fun DrawingCanvas(
    viewModel: DrawingViewModel
) {
    var point by remember { mutableStateOf(Offset.Zero) } // point 위치 추적을 위한 State
    val points = remember { mutableListOf&lt;Offset&gt;() } // 새로 그려지는 path 표시하기 위한 points State

    var path by remember { mutableStateOf(Path()) } // 새로 그려지고 있는 중인 획 State

    val paths by viewModel.paths.observeAsState()
    val pathStyle by viewModel.pathStyle.observeAsState()

    Canvas(
        modifier = Modifier
            .size(360.dp)
            .background(Color.White)
            .aspectRatio(1.0f)
            .clipToBounds()
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = { offset -&gt;
                        point = offset
                        points.add(point)
                    },
                    onDrag = { _, dragAmount -&gt;
                        point += dragAmount
                        points.add(point)
                        // onDrag가 호출될 때마다 현재 그리는 획을 새로 보여줌
                        path = Path()
                        points.forEachIndexed { index, point -&gt;
                            if (index == 0) {
                                path.moveTo(point.x, point.y)
                            } else {
                                path.lineTo(point.x, point.y)
                            }
                        }
                    },
                    onDragEnd = {
                        viewModel.addPath(Pair(path, pathStyle!!.copy()))
                        points.clear()

                        path = Path()
                    }
                )
            },
    ) {
        paths?.forEach { pair -&gt;
            drawPath(
                path = pair.first,
                style = pair.second
            )
        }

        drawPath(
            path = path,
            style = pathStyle!!
        )
    }

    Spacer(modifier = Modifier.height(12.dp))
    // Undo, Redo 버튼
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        DrawingUndoButton {
            viewModel.undoPath()
        }

        Spacer(modifier = Modifier.width(24.dp))

        DrawingRedoButton {
           viewModel.redoPath()
        }
    }
    // 획 스타일 조절하는 영역
    DrawingStyleArea(
        onSizeChanged = { viewModel.updateWidth(it) },
        onColorChanged = { viewModel.updateColor(it) },
        onAlphaChanged = { viewModel.updateAlpha(it) }
    )
}

...</code></pre><p>MainActivity.kt</p>
<pre><code>class MainActivity : ComponentActivity() {

    private val viewModel: DrawingViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            DrawingScreenTheme {
                // A surface container using the &#39;background&#39; color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    DrawingScreen(viewModel)
                }
            }
        }
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Design Pattern] SOLID 원칙]]></title>
            <link>https://velog.io/@moonliam_/Design-Pattern-SOLID-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@moonliam_/Design-Pattern-SOLID-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Mon, 08 Jan 2024 09:13:26 GMT</pubDate>
            <description><![CDATA[<h2 id="0-design-smells">0. Design Smells</h2>
<p>Design Smell이란 _나쁜 디자인을 나타내는 증상_같은 것을 의미한다.</p>
<p>Design Smell에는 아래 4가지 종류가 있다.
<strong>1. Rigidity(경직성)</strong>
시스템이 변경하기 어렵다. 기능 하나의 변경을 위해서 다른 것들도 같이 변경해야할 때 경직성이 높다고 말한다. 
<strong>2. Fragility(취약성)</strong>
시스템의 특정 부분을 수정했을 때, 관련이 없는 다른 부분에 영향을 줄 가능성이 높다면 취약성이 높다고 한다. 수정사항과 관련되어있지 않는 부분에도 영향을 끼치기 때문에 관리 비용이 커지며 시스템의 신용성 또한 떨어진다.
<strong>3. Immobility(부동성)</strong>
부동성이 높다면 재사용하기 위해서 시스템을 분리해서 컴포넌트를 만드는 것이 어렵다. 주로 개발자가 이전에 구현되었던 모듈과 비슷한 기능을 하는 모듈을 만들려고 할 때 문제점을 발견한다.
<strong>4. Viscosity(점착성)</strong>
디자인 점착성과 환경 점착성. 2가지로 나눌 수 있다. 이 중 환경 점착성은 개발환경이 느리고 효율적이지 못할 때 나타난다. 컴파일 시간이 매우 길다면 큰 규모의 수정이 필요하더라도 개발자는 recompile 시간이 길기 때문에 작은 규모의 수정으로 문제를 해결하려고 할 것이다.</p>
<h2 id="1-solid-원칙이란">1. SOLID 원칙이란?</h2>
<p>『클린 코드』, 『클린 아키텍처』등 유명한 IT 도서의 저자인 Robert C. Martin이 고안한 개념으로 <strong>객체 지향 프로그래밍에서 지켜져야할 소프트웨어 설계의 5가지 기본 원칙</strong>을 뜻한다.</p>
<p>이러한 원칙은 소프트웨어 설계와 구조를 개선하여 유지보수정, 확장성, 재사용성이 좋고 이해하기 쉬운 코드를 작성하는데 도움을 주며 위의 Design Smell도 줄일 수 있다.</p>
<h3 id="1-srp-단일-책임-원칙single-responsibility-principle">1. SRP 단일 책임 원칙(Single Responsibility Principle)</h3>
<ul>
<li><strong>모듈(클래스)은 오직 하나의 이유로 수정되어야 한다.</strong></li>
<li>수정의 이유가 하나라는 것은 <strong>해당 모듈이 여러 대상 or 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다.</strong></li>
</ul>
<p>만약 어떤 모듈이 여러 액터에 대해 책임을 가지고 있다면??
-&gt; 여러 액터들로부터 변경 요구가 올 수 있고 따라서 해당 모듈을 수정해야하는 이유 역시 여러개가 될 수 있다.</p>
<p>예를 들어, 사용자의 입력 정보를 받고 비밀번호를 암호화하여 데이터베이스에 저장하는 로직이 있다고 하자.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public void addUser(final String email, final String pw) {
        final StringBuilder sb = new StringBuilder();

        for(byte b : pw.getBytes(StandardCharsets.UTF_8)) {
            sb.append(Integer.toString((b &amp; 0xff) + 0x100, 16).substring(1));
        }

        final String encryptedPassword = sb.toString();
        final User user = User.builder()
            .email(email)
            .pw(encryptedPassword).build();

        userRepository.save(user);
    }
}</code></pre><p>위 <code>UserService</code>에게 들어올 수 있는 변경사항 요청 예시는 아래와 같다.</p>
<ol>
<li>사용자를 추가(addUser)할 때, 역할(Role)에 대한 정의가 필요하다.</li>
<li>사용자의 비밀번호 암호화 방식에 개선이 필요하다.</li>
<li>기타 등등...</li>
</ol>
<p><code>UserSErvice</code>의 수정에 필요한 이유가 최소 2가지 이상이다. 그 이유는 위 코드가 2가지 이상의 책임을 갖기 때문이다.</p>
<ol>
<li>비밀번호 암호화</li>
<li>User 생성</li>
</ol>
<p>따라서 위 코드는 SRP 원칙에 위배된다. 이를 고치기 위해선 비밀번호 암호화 책임을 맡는 클래스와 유저 생성 책임을 맡는 클래스를 분리해야한다.</p>
<pre><code>@Component
public class SimplePasswordEncoder {

    public String encryptPassword(final String pw) {
        final StringBuilder sb = new StringBuilder();

        for(byte b : pw.getBytes(StandardCharsets.UTF_8)) {
            sb.append(Integer.toString((b &amp; 0xff) + 0x100, 16).substring(1));
        }

        return sb.toString();
    }
}

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final SimplePasswordEncoder passwordEncoder;

    public void addUser(final String email, final String pw) {
        final String encryptedPassword = passwordEncoder.encryptPassword(pw);

        final User user = User.builder()
            .email(email)
            .pw(encryptedPassword).build();

        userRepository.save(user);
    }
}</code></pre><p><code>SimplePasswordEncoder</code> 라는 비밀번호 암호화 책임을 갖는 클래스를 생성해 <code>UserService</code>에서 분리함으로써 SRP 원칙을 지켰다.</p>
<h3 id="2-ocp-개방-폐쇄-원칙-open-closed-principle">2. OCP 개방 폐쇄 원칙 (Open-Closed Principle)</h3>
<ul>
<li><strong>자신의 확장에는 열려있고 주변의 변화에는 닫혀 있어야한다.</strong></li>
<li>확장에 대해 열려있다: 요구사항이 변경될 때, 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.</li>
<li>수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.</li>
<li>즉, <strong>기존의 코드를 변경하지 않고 기능을 수정 및 추가할 수 있어야한다</strong>는 뜻이다.</li>
</ul>
<p>만약 OCP 원칙이 지켜지지않는다면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 모두 잃어버리는 셈이고 OOP를 사용하는 의미가 사라진다.</p>
<p>위 예시에서 비밀번호 암호화를 강화해야한다는 요구사항이 들어왔다고 가정하자. 이를 위해 SHA-256 알고리즘을 사용하는 방식으로 새롭게 <code>PasswordEncoder</code>를 생성했다.</p>
<pre><code>@Component
public class SHA256PasswordEncoder {

    private final static String SHA_256 = &quot;SHA-256&quot;;

    public String encryptPassword(final String pw)  {
        final MessageDigest digest;
        try {
            digest = MessageDigest.getInstance(SHA_256);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException();
        }

        final byte[] encodedHash = digest.digest(pw.getBytes(StandardCharsets.UTF_8));

        return bytesToHex(encodedHash);
    }

    private String bytesToHex(final byte[] encodedHash) {
        final StringBuilder hexString = new StringBuilder(2 * encodedHash.length);

        for (final byte hash : encodedHash) {
            final String hex = Integer.toHexString(0xff &amp; hash);
            if (hex.length() == 1) {
                hexString.append(&#39;0&#39;);
            }
            hexString.append(hex);
        }

        return hexString.toString();
    }
}</code></pre><p>그런데 이 새로운 비밀번호 암호화 정책을 적용하려고 하니 암호화 정책과 무관한 <code>UserService</code>를 수정해줘야하는 문제가 발생한다.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final SHA256PasswordEncoder passwordEncoder;

    ...

}</code></pre><p>이는 새로운 기능의 확장이 기존 코드를 수정하지 않아야한다는 OCP 원칙에 위배된다. 만약 나중에 또 다시 암호화 정책을 변경해야한다는 요구사항이 오면 또 다시 <code>UserService</code>도 변경이 필요해진다.</p>
<blockquote>
<p>OCP 는 <strong>추상화 (인터페이스)</strong> 와 <strong>상속 (다형성)</strong> 등을 통해 구현해낼 수 있다. <strong>자주 변화하는 부분을 추상화함</strong>으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 함으로써 유연함을 높이는 것이 핵심이다.</p>
</blockquote>
<pre><code>public interface PasswordEncoder {
    String encryptPassword(final String pw);
}

@Component
public class SHA256PasswordEncoder implements PasswordEncoder {

    @Override
    public String encryptPassword(final String pw)  {
        ...
    }
}

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public void addUser(final String email, final String pw) {
        final String encryptedPassword = passwordEncoder.encryptPassword(pw);

        final User user = User.builder()
            .email(email)
            .pw(encryptedPassword).build();

        userRepository.save(user);
    } 
}</code></pre><p>OCP가 본질적으로 이야기하는 것은 <strong>추상화</strong>이다. 그리고 이는 <em>런타임 의존성</em>과 <em>컴파일타임 의존성</em>에 대한 이야기이다.</p>
<ul>
<li>런타임 의존성: 애플리케이션 실행 시점에서의 객체들의 관계(의존성)</li>
<li>컴파일타임 의존성: 코드에 표현된 클래스들의 관계(의존성)</li>
</ul>
<p>객체지향 프로그래밍에서 런타임 의존성과 컴파일타임 의존성은 동일하지않다. 위 예제에서 <code>UserService</code>는 컴파일 시점에서 추상화된 <code>PasswordEncoder</code>에 의존하고 있지만 런타임 시점에서는 구현체 클래스 <code>SHA256PasswordEncoder</code>에 의존한다.</p>
<p>핵심은 <strong>변하는 것들은 숨기고(추상화) 변하지 않는 것들에 의존하게 하는 것</strong>이다.</p>
<h3 id="3-isp-인터페이스-분리-원칙-interface-segregation-principle">3. ISP 인터페이스 분리 원칙 (Interface Segregation Principle)</h3>
<ul>
<li><strong>사용하지 않는 메소드에 의존하면 안된다.</strong></li>
<li>클라이언트는 반드시 <strong>자신이 사용하는 메소드에만 의존</strong>해야한다는 원칙이다.</li>
<li>한 클래스는 <strong>자신이 사용하지 않는 인터페이스는 구현하지 않아야한다.</strong>
  -&gt; 하나의 통상적인 인터페이스보다 차라리 여러개의 세부적인(구체적인) 인터페이스가 낫다.</li>
</ul>
<blockquote>
<p>각 클라이언트가 필요로하는 인터페이스들을 분리함으로써, <strong>클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않아야한다</strong>는 것이 <strong>핵심</strong>이다.</p>
</blockquote>
<p>예를 들어 파일 읽기/쓰기 기능을 갖는 구현 클래스가 있는데 어떤 클라이언트는 읽기 작업만을 필요로 한다면 별도의 읽기 인터페이스를 만들어 제공해주는 것이다.</p>
<p>위 예시에 이어서 비밀번호를 변경할 때, 입력한 비밀번호가 기존의 비밀번호와 동일한지 검사해야하는 로직을 다른 Authentication 로직에 추가해야 한다고 가정하자. 그러면 우리는 다음과 같은 <code>isCorrectPassword</code>라는 퍼블릭 인터페이스를 <code>SHA256PasswordEncoder</code>에 추가해줄 것이다.</p>
<pre><code>@Component
public class SHA256PasswordEncoder implements PasswordEncoder {

    @Override
    public String encryptPassword(final String pw)  {
        ...
    }

    public String isCorrectPassword(final String rawPw, final String pw) {
        final String encryptedPw = encryptPassword(rawPw);
        return encryptedPw.equals(pw);
    }
}</code></pre><p>하지만 <code>UserService</code>에서는 비밀번호 암호화를 위한 <code>encryptPassword</code> 만 필요로 하고 <code>isCorrectPassword</code>는 알 필요가 없다. 
그리고 새로 추가될 <code>Authentication</code> 로직에서는 <code>isCorrectPassword</code> 에 접근하기 위해 구체 클래스인 <code>SHA256PasswordEncoder</code>를 주입받아야 하는데 그러면 불필요한 <code>encryptPassword</code>에도 접근 가능해지고 ISP 분리 원칙을 위반하게 된다.</p>
<p>위 상황을 해결하기 위해서는 비밀번호 검사를 의미하는 <code>PasswordChecker</code>라는 별도의 인터페이스를 만들고 이를 주입받도록 하는 것이다.</p>
<pre><code>public interface PasswordChecker {
    String isCorrectPassword(final String rawPw, final String pw);
}

@Component
public class SHA256PasswordEncoder implements PasswordEncoder, PasswordChecker {

    @Override
    public String encryptPassword(final String pw)  {
        ...
    }

    @Override
    public String isCorrectPassword(final String rawPw, final String pw) {
        final String encryptedPw = encryptPassword(rawPw);
        return encryptedPw.equals(pw);
    }
}</code></pre><h3 id="4-dip-의존성-역전-원칙-dependency-inversion-principle">4. DIP 의존성 역전 원칙 (Dependency Inversion Principle)</h3>
<ul>
<li><p>**자신(high level module)보다 변하기 쉬운 모듈(low level module)에 의존해서는 안된다.</p>
</li>
<li><p>의존 관계를 맺을 떄, 변하기 쉬운 것(구체적인 것)보다는 변하기 어려운 것(추상적인 것)에 의존해야한다는 원칙이다.
  -&gt; 구체화된 클래스가 아닌 <strong>추상 클래스</strong>나 <strong>인터페이스</strong>에 의존하는 것이 좋다.</p>
</li>
<li><p>저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다.</p>
</li>
<li><p>고수준 모듈: 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈</p>
</li>
<li><p>저수준 모듈: 입력과 출력으로부터 가까운(HTTP, 데이터베이스, 캐시 등과 관련된) 구현 모듈</p>
</li>
</ul>
<p>객체 지향 프로그래밍에서는 객체들 사이에 메세지를 주고 받기 위해 의존성이 생기는데, 의존성 역전의 원칙은 올바른 의존 관계를 위한 원칙에 해당된다. </p>
<p>위의 예시에서 <code>SimplePasswordEncoder</code> 는 변하기 쉬운 암호화 알고리즘 구현체 클래스인데 <code>UserService</code>가 <code>SimplePasswordEncoder</code>에 직접 의존하는 것은 DIP에 위배된다. 따라서 <code>UserService</code>가 추상화에 의존하도록 변경해야했고 <code>PassowordEncoder</code> 인터페이스를 만들어 의존케함으로써 해결했다.</p>
<p><code>PassowrdEncoder</code>의 다른 구현체 클래스에 변경이 생겨도 <code>UserService</code>는 고수준 모듈인 <code>PasswordEncoder</code> 인터페이스에 의존하므로 암호화 정책이 바껴도 다른 곳으로 전파되지 않는다.</p>
<p>이처럼 <strong>의존성 역전 원칙(DIP)은 개방 폐쇄 원칙(OCP)와 밀접한 관련이 있다.</strong></p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/889b6ffe-0426-4b18-8e96-352e670ca15f/image.png" alt=""></p>
<h3 id="5-lsp-리스코프-치환-원칙-liskov-substitution-principle">5. LSP 리스코프 치환 원칙 (Liskov Substitution Principle)</h3>
<ul>
<li><strong>base 클래스에서 파생된 클래스는 base 클래스를 대체해서 사용할 수 있어야한다.</strong></li>
<li>즉 <strong>하위 타입은 상위 타입을 대체할 수 있어야한다</strong>는 뜻이다.</li>
<li>리스코프 치환 원칙이 잘 지켜지면 해당 객체를 사용하는 클라이언트는 객체 타입이 하위 타입으로 변경되어도 차이점을 인식하지 못해야한다.</li>
</ul>
<p>아래 예시를 살펴보자.</p>
<pre><code>@Getter
@Setter
@AllArgsConstructor
public class Rectangle {

    private int width, height;

    public int getArea() {
        return width * height;
    }

}

public class Square extends Rectangle {

    public Square(int size) {
        super(size, size);
    }

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}</code></pre><p><code>Rectangle</code>은 직사각형을 뜻하는 상위 클래스이고 <code>Square</code>는 정사각형으로 <code>Rectangle</code>로부터 상속받는 하위 클래스이다.</p>
<p><code>Square</code>의 생성자를 보면 <code>size</code>라는 1개의 변수만을 생성자로 받고 이를 이용해 <code>width</code>, <code>height</code>가 모두 설정되도록 오버라이딩 되어있다.</p>
<p>하지만 이를 이용하는 클라이언트는 직사각형의 너비와 높이가 다르다고 가정할 것이고 직사각형을 resize하기 위해 아래와 같은 <code>resize()</code> 메소드를 만들 수 있다.</p>
<pre><code>public void resize(Rectangle rectangle, int width, int height) {
    rectangle.setWidth(width);
    rectangle.setHeight(height);
    if (rectangle.getWidth() != width &amp;&amp; rectangle.getHeight() != height) {
        throw new IllegalStateException();
    }
}</code></pre><p>문제는 <code>resize()</code>의 파라미터로 정사각형인 <code>Square</code>이 전달되는 경우다. <code>Rectangle</code>은 <code>Square</code>의 부모 클래스이므로 <code>Square</code> 역시 전달이 가능하다. 하지만 <code>Square</code>는 가로와 세로가 모두 동일하게 설정되므로 예를 들어 다음과 같은 메소드를 호출하면 문제가 발생할 것이다.</p>
<pre><code>Rectangle rectangle = new Square();
resize(rectangle, 100, 150);</code></pre><p>이 경우 엄연히 부모 클래스와 자식 클래스의 행동이 호환되지 않으므로 리스코프 치환 원칙이 위배된다. </p>
<p>이 외에도 리스코프 치환 원칙이 위반되는 상황 예시로는 다음과 같다.</p>
<ol>
<li>하위 클래스가 상위 클래스에서 선언한 기능을 위반하는 경우<ul>
<li>상위 클래스가 주문 정렬을 위한 sortOrdersByAmount() 함수를 구현해두었는데, 하위 클래스에서 생성 날짜에 따라 정렬되도록 변경한 경우</li>
</ul>
</li>
</ol>
<ol start="2">
<li>하위 클래스가 입력, 출력 및 예외에 대한 상위 클래스의 계약을 위반하는 경우<ul>
<li>상위 클래스에서 오류가 발생하면 null을, 값을 얻을 수 없으면 빈 컬렉션을 반환하게 해두었는데, 하위 클래스에서 오류가 발생하면 예외를 발생시키고, 값을 얻을 수 없을 때 null을 반환하도록 변경한 경우</li>
<li>상위 클래스에서는 입력 시 모든 정수를 허용하지만, 하위 클래스에서는 음수일 때 예외를 발생시키는 경우</li>
<li>상위 클래스에서 던지는 예외는 ArgumentException 뿐인데, 하위 클래스에서 다른 예외도 던지는 경우</li>
</ul>
</li>
</ol>
<ol start="3">
<li>하위 클래스가 상위 클래스의 주석에 나열된 특별 지침을 위반하는 경우<ul>
<li>상위 클래스에 예금을 인출하는 withdraw() 메서드에 사용자의 출금 금액이 잔액을 초과해서는 안된다는 주석이 있을 때, 하위 클래스에서는 가능한 경우</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>출처</strong></p>
</blockquote>
<ol>
<li><a href="https://gyoogle.dev/blog/design-pattern/SOLID.html">An overview of design pattern - SOLID, GRASP</a></li>
<li><a href="https://mangkyu.tistory.com/194">[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID
출처: https://mangkyu.tistory.com/194 [MangKyu&#39;s Diary:티스토리]</a></li>
<li><a href="https://velog.io/@haero_kim/SOLID-%EC%9B%90%EC%B9%99-%EC%96%B4%EB%A0%B5%EC%A7%80-%EC%95%8A%EB%8B%A4">SOLID 원칙, 어렵지 않다!</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Clean Architecture 개념 ]]></title>
            <link>https://velog.io/@moonliam_/Android-Clean-Architecture-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@moonliam_/Android-Clean-Architecture-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 27 Dec 2023 14:52:25 GMT</pubDate>
            <description><![CDATA[<h2 id="1-클린-아키텍처clean-architecture는-무엇일까">1. 클린 아키텍처(Clean Architecture)는 무엇일까?</h2>
<p>요즘 개발을 하다 보면 프로젝트에 클린 아키텍처를 적용했다던가 클린 아키텍처 경험이 있는 개발자를 구한다던가. 클린 아키텍처(Clean Architecture)에 대한 이야기가 굉장히 많이 나온다.</p>
<p>그렇다면 클린 아키텍처란 무엇일까? 클린 아키텍처란 아주  유명한 책인 『클린 코드(Clean Code)』를 저술한 로버트 마틴(Robert C. Martin)이 제시한 개념으로 <strong>복잡한 소프트웨어 시스템을 보다 관리 가능하고 유지보수 가능한 형태로 구축하기 위한 지침을 제공</strong>하기 위해서 등장했다.</p>
<h3 id="주요-원칙">주요 원칙</h3>
<p>클린 아키텍처의 주요 원칙은 다음과 같다.</p>
<p><strong>의존성 역전 원칙(Dependency Inversion Principle)</strong>
고수준 모듈은 저수준 모듈에 의존해서는 안되며, 양쪽 모듈 모두 추상화에 의존해야 합니다. 이를 통해 느슨한 결합을 유지할 수 있습니다.</p>
<p><strong>경계(Boundary)의 분리</strong>
시스템을 여러 영역으로 나누고, 각 영역 사이의 인터페이스를 정의하여 각 영역의 독립성을 보장합니다.</p>
<p><strong>인터페이스 분리 원칙(Interface Segregation Principle)</strong>
클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 즉, 인터페이스는 클라이언트의 요구에 딱 맞는 형태로 분리되어야 합니다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/4aff026a-bdbe-4db7-bbee-51b43580d21c/image.png" alt=""></p>
<p>클린 아키텍처하면 가장 많이 나오는 청사진 이미지이다. <strong>프레임워크와 드라이버, 인터페이스 어댑터, 애플리케이션 업무 규칙, 엔터프라이즈 업무 규칙</strong> 총 4가지 계층으로 나뉘어져 있으며 각 계층은 바깥에서 안쪽으로 의존성을 가진다.</p>
<h2 id="2-클린-아키텍처-왜-사용할까">2. 클린 아키텍처 왜 사용할까?</h2>
<p>A라는 앱을 개발하는데 B라는 앱과 통합을 하게되었다. 이때 받을 수 있는 요구사항들을 가정해보자.</p>
<blockquote>
<p>A 앱 시스템이 잘 되어있으니 A 앱의 핵심 기능은 유지하고 UI와 DB 쪽만 바꿔주세요.</p>
</blockquote>
<p>아니면 혹은</p>
<blockquote>
<p>A 앱이 너무 잘되니 서비스를 웹으로 확장해봅시다.</p>
</blockquote>
<p>만약 이런 상황에서 클린 아키텍처를 도입하지 않았다면? 상당히 머리가 아플 것이다. 코드를 하나 하나 뜯어보면서 이건 냅두고 이건 고치고 근데 이거 건들면 다른게 또...</p>
<p>하지만 클린 아키텍처를 도입했다면 좀 더 수월하게 위 작업을 수행할 수 있을 것이다. </p>
<p>첫번째 요구사항의 경우 애플리케이션 업무 규칙, 엔터프라이즈 업무 규칙은 냅두고 프레임워크와 드라이버, 인터페이스 어댑터만 수정하면 된다.</p>
<p>두번째 요구사항의 경우도 프레임워크와 드라이버 영역만 수정하면 된다.</p>
<p>이처럼 클린 아키텍처는 필수적인 비즈니스 로직은 바꾸지않고 언제든지 DB와 프레임워크에 구애받지않고 교체할 수 있게 해주는 셈이다.</p>
<p>좀 더 구체적으로 정의하면 아래처럼 4가지 이유로 정의할 수 있다.</p>
<p><strong>유지 보수성 향상</strong>
클린 아키텍처는 의존성 관리 및 모듈화를 통해 애플리케이션의 유지 보수성이 단순 MVVM 패턴보다 향상된다. 각 계층은 명확하게 정의되고 분리되어 있으므로 변경 요구사항에 맞춰 변경이 쉽게 이루어질 수 있다.</p>
<p><strong>테스트 용이성</strong>
유지 보수성 향상과 동일 이유로 각 계층이 독립적으로 테스트 가능하도록 설계되어 있어 테스트 용이성을 높일 수 있다. 유닛 테스트, 통합 테스트 등을 보다 쉽게 테스트를 진행할 수 있다.</p>
<p><strong>미래 확장성</strong>
새로운 기능을 추가하거나 기존 기능을 변경하려는 경우 변경의 범위를 최소화하며 애플리케이션의 확장에 유용하다.</p>
<p><strong>비즈니스 로직 분리</strong>
비즈니스 로직을 다른 계층으로 분리하여 비즈니스 룰과 데이터베이스 접근을 분리시킨다. 이것은 비즈니스 로직을 단일 위치에서 유지하고 이해하기 쉽게 만든다.</p>
<h2 id="3-클린-아키텍처-in-안드로이드">3. 클린 아키텍처 in 안드로이드</h2>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/9662e8e8-808e-4310-9932-1854a2a131e8/image.png" alt="">
일반적인 클린 아키텍처 구조에서는 의존성이 무조건 바깥에서 안쪽으로 향하지만 안드로이드에서는 계층을 <em>프레젠테이션 계층(UI 계층), 도메인 계층, 데이터 계층</em> 총 3가지로 나누고 <strong>프레젠테이션 계층과 데이터 계층이 각각 도메인 계층을 바라보며 의존성을 가지는 형태로 구성된다.</strong></p>
<p><strong>MVVM 패턴</strong>과 더불어 데이터 흐름과 함께 좀 더 알아보기 쉬운 이미지는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/ef7b1da8-c8f3-464f-9044-8a91952fd86f/image.png" alt=""></p>
<p>각 계층이 의미하는 바는 다음과 같다.</p>
<ul>
<li><strong>프리젠테이션 계층(Presentation Layer)</strong>
  <strong>- 뷰(View)</strong>: 직접적으로 플랫폼 의존적인 구현, 즉 UI 화면 표시와 사용자 입력을 담당. 단순하게 프레젠터가 명령하는 일만 수행한다.
  <strong>- 뷰모델(ViewModel)</strong>: 사용자 입력이 왔을 때 어떤 반응을 해야 하는지에 대한 판단을 하는 영역. 무엇을 그려야 할지도 알고 있는 영역이다.</li>
<li><strong>도메인 계층(Domain Layer)</strong>
  <strong>- 유즈 케이스(Use Case)</strong>: 비즈니스 로직이 들어 있는 영역.
  <strong>- 엔티티(Entity)</strong>: 앱의 실질적인 데이터.</li>
<li><strong>데이터 계층(Data Layer)</strong>
  <strong>- 리포지터리(Repository)</strong>: 유즈 케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역으로, 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 할 수 있도록 함.
  <strong>- 데이터 소스(Data Source)</strong>: 실제 데이터의 입출력이 여기서 실행되는 부분.</li>
</ul>
<p>여기서 Repository는 왜 도메인 계층과 데이터 계층에 걸쳐져 있는지, 이렇게 하면 위에서 언급한 클린 아키텍처 원칙에 위배되는 게 아닌지 궁금해할수 있다.</p>
<p>이는 도메인 계층에서 UseCase로 데이터를 요청할 때, Repository를 참조하게되는데 이렇게되면 상위 계층인 도메인 계층이 데이터 계층에 의존성을 가지게 되므로 클린 아키텍처 원칙에 위배되는 게 맞다. </p>
<p>이건 상위 모듈에서 인터페이스를 생성하고 의존 관계를 역전시켜 모듈을 분리함으로써 해결할 수 있다. 이를 <strong>의존성 역전</strong>이라고 한다. 사실 위에 클린 아키텍처 주요 원칙에도 설명되어있다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/c2069a7d-f6dd-4549-9644-95ddc2825355/image.png" alt=""></p>
<p>위 이미지처럼 도메인 계층에 <strong>리포지터리 인터페이스</strong>를 만들고 하위 계층인 데이터 계층에서 <strong>리포지터리 구현체</strong>가 인터페이스를 참조하게하면 된다.</p>
<h2 id="4-예제-코드">4. 예제 코드</h2>
<p>클린 아키텍처에서 계층(Layer)과 구성 요소간의 연결은 인터페이스를 활용하여 구성되며, 다른 구성 요소간의 의존성은 의존성 주입(DI)를 통해 최소화한다.</p>
<p><code>UserData</code>를가져오는 <strong>UseCase</strong>를 구현한다고 가정해보자.</p>
<p>UserDataUseCase 는 Domain 모듈에 위치하게 되며 이 UseCase는 동일 위치(Domain)에 있는 Repository의 getUserData() 을 호출하게 됩니다.</p>
<pre><code>class UserDataUseCase(
    private val repository: UserDataRepository
) : BaseUseCase&lt;String, UserDataEntity&gt;() {
    override suspend fun execute(parameters: String?): Result&lt;BestItemEntity&gt; {
        return 
      // todo.. result 성공, 실패에 대한 처리 로직 추가
        }
    }
}</code></pre><p>이때 정의한 UserDataRepository는 interface로 구성되어있다.</p>
<pre><code>interface UserDataRepository {
    suspend fun getUserData(): Result&lt;UserDataEntity&gt;
}</code></pre><p>UserDataUseCase의 리포지토리는 DI로 정의하여 <code>@Provides</code>를 구성해야한다. API 통신은 Activity의 Lifecycle을 따르도록 DI Component를 <code>ActivityRetainedComponene</code> 로 정의한다.</p>
<pre><code>@Module
@InstallIn(ActivityRetainedComponent::class)
class UserDataUseCaseModule{
    @Provides
    fun provideUserDataUseCase(repository: UserDataRepository) = UserDataUseCase(repository)
}</code></pre><p>UserDataRepository 의 implement 처리는 Data 모듈(Module) 에서 처리한다. Repository는 DataSource를 호출하게되며 DI로 Repository와 Datasource의 의존을 구성한다.</p>
<pre><code>class UserDataRepositoryImpl @Inject constructor(
    private val remote: UserDataRemoteDataSource
) : UserDataRepository {
    override suspend fun getUserData(): Result&lt;UserDataEntity&gt; {
        val response = remote.getUserData()
        // todo.. response에 대한 처리 로직 추가
    }
}</code></pre><p>Repository 에 대한 @Provides 의 정의는 Data 모듈(Module) 에 구성되어 있으며, UseCase와 동일하게 DI Component를 ActivityRetainedComponent 로 정의한다.</p>
<pre><code>@Module
@InstallIn(ActivityRetainedComponent::class)
class UserDataRepositoryModule {
    @Provides
    fun provideUserDataRepository(
        remote: UserDataRemoteDataSource
    ): UserDataRepository = UserDataRepositoryImpl(remote)
}</code></pre><p>도메인 계층(Layer)과 데이터 계층(Layer) 간의 연결을 DI(Dependency Injection)를 활용하여 설정함으로써, 의존성을 적절히 관리하고 클린 아키텍처를 구성할 수 있다.</p>
<blockquote>
<p>레퍼런스)</p>
</blockquote>
<ol>
<li><a href="https://meetup.nhncloud.com/posts/345">[Android] 요즘 핫한 Clean Architecture 왜 쓰는 거야?</a></li>
<li><a href="https://medium.com/cj-onstyle/android-%EB%B2%84%ED%8B%B0%EC%BB%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%98-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%8F%84%EC%9E%85-a26d833e103c">왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android / Compose] Compose로 그림판 구현해보기 - 2]]></title>
            <link>https://velog.io/@moonliam_/Android-Compose-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-2</link>
            <guid>https://velog.io/@moonliam_/Android-Compose-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-2</guid>
            <pubDate>Mon, 25 Dec 2023 09:25:32 GMT</pubDate>
            <description><![CDATA[<h2 id="0-시작하기-앞서">0. 시작하기 앞서</h2>
<p>완성된 전체 코드가 필요하다면 <a href="https://github.com/Moonsyn/DrawingScreen">Github 링크</a>를 참고해주세요.</p>
<h2 id="1-undo-redo-기능-구현하기">1. Undo, Redo 기능 구현하기</h2>
<p>그림을 그리다보면 획을 잘못 그어서 이전 화면으로 돌아가고 싶거나 반대로 지웠던 획을 다시 불러오고 싶을 때가 있다. 각각 PC 환경에서는 <code>Ctrl+Z</code>, <code>Ctrl+Shift+Z</code> 단축키를 눌러 사용할 수 있지만 모바일 환경에서는 그럴 수 없으니 직접 구현해보도록 하자.</p>
<p>해당 기능을 구현하기에 앞서 이전 코드에서 수정해야할 부분이 있다.</p>
<pre><code>Canvas(
    modifier = Modifier.size(360.dp)
            .background(Color.White)
            .aspectRatio(1.0f)
            .pointerInput(Unit) {
                detectDragGestures(

                    ...

                    onDragEnd = {
                        paths.add(Pair(path, pathStyle.copy()))
                        points.clear()

                        path = Path() // 이 부분을 추가해줘야한다.
                    }
                )
            },
) {
    ....    
}</code></pre><p><code>onDragEnd</code> 에서 <code>path</code>를 초기화를 해주지 않으면 드래그가 끝나도 <code>path</code> 그리는 과정이 아직 끝나지 않은 것으로 인식해 <code>Undo</code> 버튼을 눌렀을 때 가장 최근에 그린 획이 지워지지 않고 그 이전 획부터 지워지는 현상이 발생한다. 따라서 해당 부분을 추가해 <code>path</code>를 초기화를 해주자.</p>
<p>이제 Undo, Redo 기능을 구현할 준비는 끝났다.</p>
<ol>
<li><p>먼저 Undo, Redo 기능을 사용하기 위한 버튼을 추가한다.</p>
<pre><code>@Composable
fun DrawingCanvas() {

 ...

 // Undo, Redo 버튼
 Row(
     modifier = Modifier.fillMaxWidth(),
     horizontalArrangement = Arrangement.Center
 ) {
     DrawingUndoButton {
         // Undo
     }

     Spacer(modifier = Modifier.width(24.dp))

     DrawingRedoButton {
         // Redo
     }
 }   
}
</code></pre></li>
</ol>
<p>@Composable
fun DrawingUndoButton(
    onClick: () -&gt; Unit
) {
    Button(onClick = { onClick() }) {
        Text(text = &quot;Undo&quot;)
    }
}</p>
<p>@Composable
fun DrawingRedoButton(
    onClick: () -&gt; Unit
) {
    Button(onClick = { onClick() }) {
        Text(text = &quot;Redo&quot;)
    }
}</p>
<pre><code>
`Undo` 기능을 구현하는 것은 간단하다. 버튼을 누를 때마다 `paths` 안에 저장되어 있는 획들을 가장 최근 거부터 하나씩 제거해주면 된다.

이 때 `Redo` 기능으로 제거했던 획을 다시 불러와야할 수도 있기 때문에 `paths` 에서 제거된 획을 따로 저장할 `removedPaths` 리스트를 만들고 `paths` 에서 가장 최근 획부터 제거해 해당 리스트에 저장하도록 하면 된다.

2. `removedPaths` 리스트를 만들고 `paths`에서 가장 최근 획부터 제거하면서 `removedPaths`에 저장한다.</code></pre><p>val removedPaths = remember { mutableStateListOf&lt;Pair&lt;Path, PathStyle&gt;&gt;() }</p>
<p>// Undo, Redo 버튼
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.Center
) {
    DrawingUndoButton {
        if (paths.isEmpty()) return@DrawingUndoButton
        // Undo
        val lastPath = paths.removeLast()
        removedPaths.add(lastPath)
    }</p>
<pre><code>...</code></pre><p>}  </p>
<pre><code>
`Redo` 기능도 같은 방식으로 구현하면 된다.

3. `Redo` 기능은 `removedPaths`에서 가장 최근 획부터 제거해 `paths` 리스트에 저장한다.
</code></pre><p>DrawingRedoButton {
    if (removedPaths.isEmpty()) return@DrawingRedoButton
    // Redo
    val lastRemovedPath = removedPaths.removeLast()
    paths.add(lastRemovedPath)
}</p>
<pre><code>
위처럼 구현하면 어렵지않게 Undo, Redo 기능을 구현할 수 있다. 생각보다 쉽게 끝났다.

## 2. 획 스타일 변경하기 (두께, 투명도, 색상)
그림을 그리기 위해선 당연히 다양한 크기와 색상을 가진 브러시로 그릴 수 있어야한다. 이번에는 획 스타일을 바꿀 수 있는 기능을 구현해보자.

이를 구현하기 위해서 먼저 획 스타일을 기존 Compose의 `drawPath` 함수에서 어떻게 적용하는지 알아보자.
</code></pre><p>fun drawPath(
    path: Path,
    color: Color,
    /<em>@FloatRange(from = 0.0, to = 1.0)</em>/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)</p>
<pre><code>`color`, `alpha`, `style`, `colorFilter`, `blendMode` 등 다양하게 속성을 바꿀 수 있다. 이 중 나는 `path`, `color`, `alpha`, `style` 만 사용할 거기 때문에 해당 속성을 포함한 Data Class인 `PathStyle` 클래스를 만들어 속성을 관리하기로 했다.

1. `PathStyle` 데이터 클래스 생성</code></pre><p>data class PathStyle(
    var color: Color = Color.Black,
    var alpha: Float = 1.0f,
    var width: Float = 10.0f
)</p>
<pre><code>
`PathStyle` 클래스를 생성했으면 기존의 획을 저장하는 부분 코드도 변경할 필요가 있다. `paths`, `removedPaths`에 획 정보를 저장할 때, `Path` 뿐만 아니라 `PathStyle` 정보도 같이 저장해야 해당 획을 그릴 때 속성 값들도 제대로 불러와서 그릴 수 있다.

2. 기존 `paths`, `removedPaths` 에 획을 저장하는 부분에 `PathStyle`도 같이 저장할 수 있게 코드를 수정한다.</code></pre><p>@Composable
fun DrawingCanvas() {
    ...</p>
<pre><code>val paths = remember { mutableStateListOf&lt;Pair&lt;Path, PathStyle&gt;&gt;() } // 다 그려진 획 리스트 State

val removedPaths = remember { mutableStateListOf&lt;Pair&lt;Path, PathStyle&gt;&gt;() }

val pathStyle = PathStyle()

Canvas(
    modifier = Modifier
        .size(360.dp)
        .background(Color.White)
        .aspectRatio(1.0f)
        .pointerInput(Unit) {
            detectDragGestures(

                ...

                onDragEnd = {
                    paths.add(Pair(path, pathStyle.copy()))
                    points.clear()

                    path = Path()
                }
            )
        },
) {
    paths.forEach { pair -&gt;
        drawPath(
            path = pair.first,
            style = pair.second
        )
    }

    drawPath(
        path = path,
        style = pathStyle
    )
}

...</code></pre><p>}</p>
<pre><code>3. `drawPath` 함수에 `PathStyle` 을 넘겨주면 `color`, `alpha`, `width` 를 매핑해주는 `internal fun`을 선언한다.</code></pre><p>internal fun DrawScope.drawPath(
    path: Path,
    style: PathStyle
) {
    drawPath(
        path = path,
        color = style.color,
        alpha = style.alpha,
        style = Stroke(width = style.width)
    )
}</p>
<pre><code>
`PathStyle` 변경 기능을 위한 준비가 완료되었으니 이제 기능만 구현하면된다. 두께와 투명도 변경은 `Slider` 를 이용하고 색상 변경은 간단하게 6가지 색상 정도로 구성된 팔레트를 구현한다.

4. `PathStyle` 변경을 담당할 UI 컴포넌트 영역 `DrawingStyleArea` 를 구현한다.</code></pre><p>@Composable
fun DrawingCanvas() {</p>
<pre><code>...

// 획 스타일 조절하는 영역
DrawingStyleArea(
    onSizeChanged = { pathStyle.width = it },
    onColorChanged = { pathStyle.color = it },
    onAlphaChanged = { pathStyle.alpha = it }
)</code></pre><p>}</p>
<p>@Composable
fun DrawingStyleArea(
    onSizeChanged: (Float) -&gt; Unit,
    onColorChanged: (Color) -&gt; Unit,
    onAlphaChanged: (Float) -&gt; Unit
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Text(
                modifier = Modifier
                    .width(72.dp)
                    .padding(horizontal = 8.dp),
                text = &quot;두께&quot;,
                textAlign = TextAlign.Center
            )</p>
<pre><code>        var size by remember { mutableStateOf(10.0f) }

        Slider(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 12.dp),
            value = size,
            valueRange = 1.0f..30.0f,
            onValueChange = {
                size = it
                onSizeChanged(it)
            }
        )
    }

    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(
            modifier = Modifier
                .width(72.dp)
                .padding(horizontal = 8.dp),
            text = &quot;투명도&quot;,
            textAlign = TextAlign.Center
        )

        var alpha by remember { mutableStateOf(1.0f) }

        Slider(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 12.dp),
            value = alpha,
            valueRange = 0.0f..1.0f,
            onValueChange = {
                alpha = it
                onAlphaChanged(it)
            }
        )
    }

    DrawingColorPalette(
        onColorChanged = onColorChanged
    )
}</code></pre><p>}</p>
<p>@Composable
fun DrawingColorPalette(
    onColorChanged: (Color) -&gt; Unit
) {
    var selectedIndex by remember { mutableStateOf(0) }
    val colors = listOf(Color.Black, Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Yellow)</p>
<pre><code>Row(
    modifier = Modifier.fillMaxWidth()
        .padding(horizontal = 12.dp),
    horizontalArrangement = Arrangement.SpaceBetween
) {
    colors.forEachIndexed { index, color -&gt;
        Box(
            modifier = Modifier.size(36.dp)
        ) {
            Image(
                modifier = Modifier
                    .fillMaxSize()
                    .clip(CircleShape)
                    .clickable {
                        selectedIndex = index
                        onColorChanged(color)
                    },
                painter = ColorPainter(color),
                contentDescription = &quot;색상 선택&quot;
            )

            if (selectedIndex == index) {
                Image(
                    modifier = Modifier.align(Alignment.Center),
                    painter = painterResource(id = R.drawable.ic_check),
                    contentDescription = &quot;선택된 색상 체크 표시&quot;
                )
            }
        }
    }
}</code></pre><p>}</p>
<p>```</p>
<p>이렇게해서 Undo, Redo기능과 획의 두께, 색상, 투명도 등을 변경할 수 있는 아주 기본적인 그림판을 구현해보았다. 최종 결과물은 아래에서 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/5414ef16-d3f7-4939-8030-f8a454b3a758/image.gif" alt=""></p>
<p>하지만 지금 이 그림판에는 치명적인 문제가 있다. 바로 화면 회전 등 Activity가 생명주기 상 <code>onDestroy</code>가 발생하고 다시 생성되거나 하는 경우 그림 데이터가 전부 날아간다는 것이다.</p>
<p>이를 해결하기 위해서 다음 포스트에서는 <strong>ViewModel 패턴을 적용해 Activity가 종료되기 전까지 그림 데이터가 온전히 보존</strong>되도록 구현해볼 것이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android / Compose] Compose로 그림판 구현해보기 - 1]]></title>
            <link>https://velog.io/@moonliam_/Android-Compose-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-1</link>
            <guid>https://velog.io/@moonliam_/Android-Compose-Compose%EB%A1%9C-%EA%B7%B8%EB%A6%BC%ED%8C%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-1</guid>
            <pubDate>Fri, 22 Dec 2023 10:33:47 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-개요">프로젝트 개요.</h2>
<p>예전에 Bemong을 개발할 때 직접 앱 내에 내장 그림판을 구현한다고 꽤 애를 먹었던 기억이 있다. 그 당시에는 100% Java로만 구현을 했는데 비슷한 동작을 하는 View를 새로 Compose로 다시 구현해보는 프로젝트이다. </p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/ccc8e04d-878b-468c-9323-3eb2a6907d04/image.png" alt="">
(출처: <a href="https://hyunsun99.tistory.com/47">https://hyunsun99.tistory.com/47</a>)</p>
<p>아마 실제 만들어보면 생긴 건 많이 다르겠지만 위와 비슷한 기능을 가질 것이다.</p>
<h2 id="구현하고자-하는-기능">구현하고자 하는 기능</h2>
<ol>
<li>사용자의 터치를 인식하여 움직임에 따라 획을 그리는 기능</li>
<li>획마다 다양한 색상, 브러시 등을 적용하는 기능</li>
<li>Undo, Redo 기능 (Ctrl+Z, Ctrl+Shift+Z 기능이라고 생각하면 될 것 같다.)</li>
</ol>
<h2 id="1-프로젝트-생성">1. 프로젝트 생성</h2>
<p><a href="https://github.com/Moonsyn/DrawingScreen">깃 허브 링크</a>
<code>DrawingScreen</code> 이라는 이름으로 깃허브 레포지토리를 생성한다.</p>
<p>그리고 AndroidStudio에서 새 프로젝트를 생성해준다.</p>
<pre><code>class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DrawingScreenTheme {
                // A surface container using the &#39;background&#39; color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting(&quot;Android&quot;)
                }
            }
        }
    }
}</code></pre><p>[New Project]를 생성할 때 [Empty Activity]로 설정해서 생성하면 위처럼 Compose 기본 세팅을 해준다.</p>
<h2 id="2-drawingscreenkt-생성">2. DrawingScreen.kt 생성</h2>
<p><code>DrawingScreen</code> 파일을 따로 생성하고 <code>MainActivity</code> 에서는 <code>DrawingScreen</code> 만 띄우는 걸로 변경한다.</p>
<p>MainActivity.kt</p>
<pre><code>setContent {
    DrawingScreenTheme {
        // A surface container using the &#39;background&#39; color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            DrawingScreen()
        }
    }
}</code></pre><p>DrawingScreen.kt</p>
<pre><code>@Composable
fun DrawingScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        DrawingCanvas()
    }
}</code></pre><h2 id="3-손가락-이동을-따라-획을-그리는-기능-구현">3. 손가락 이동을 따라 획을 그리는 기능 구현</h2>
<p>해당 기능을 구현하기 위해서는 먼저 <code>Canvas</code> 와 <code>Modifier.pointerInput()</code> 에 대해서 이해할 필요가 있다.</p>
<h3 id="canvas">Canvas</h3>
<p>Compose에서 맞춤 항목을 그리는 방법으로는 <code>Modifier.drawWithContent</code>, <code>Modifier.drawBehind</code>, <code>Modifier.drawWithCache</code> 처럼 <code>Modifier</code>에 내장된 함수를 활용하는 것이다.</p>
<p>하지만 이번에 나는 아예 그리기에 기능이 치중된 UI를 구현할 것이기 때문에 그런 경우에는 그리기를 실행하는 Composable인 <code>Canvas</code>를 사용할 수 있다.</p>
<p>Compose에서 <code>Canvas</code>는 기존의 Java/Kotlin 환경에서 제공하는 같은 이름의 클래스와 유사한 기능을 제공한다. 하지만 Compose를 기반으로 하고 있기 때문에 좀 더 직관적이고 간단하게 구현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/c9588e77-55e9-4535-adb4-7f1da6d63ea0/image.png" alt=""></p>
<p><code>Canvas</code>의 사용법은 어렵지 않다. 위와 같은 색이 칠해진 직사각형을 그리고 싶다면 아래처럼 하면 된다.</p>
<pre><code>Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}</code></pre><p>위에서 보듯이 <code>Canvas</code>는 <code>drawRect</code>, <code>drawOval</code>과 같은 그리기 함수를 사용할 수 있는 <code>DrawScope</code>를 제공하기 때문에 무언가 그리고자 할 때는 <code>DrawScope</code> 블럭 내에서 구현하면 된다.</p>
<h3 id="modifierpointerinput">Modifier.pointerInput()</h3>
<p><code>Canvas</code> 가 있다면 도형을 그릴 수 있지만 사용자의 입력, 손 동작 등을 받아 처리하는 것은 다른 문제이다.</p>
<p>사용자에게 보여지는 Compose UI는 사용자와 상호작용할 수 있어야하고 이는 <code>Modifier.pointerInput()</code>에서 처리할 수 있다.</p>
<p><code>pointerInput()</code>은 단순 클릭부터 시작해서 탭, 스크롤, 드래그, 스와이프, 플링, 멀티 터치 등 다양한 입력 상황을 처리할 수 있는 API를 제공한다.</p>
<p>이 중 나는 오늘 획을 쭉 이어서 그리는 동작에 대해 구현을 할 것이기 때문에 <strong>드래그</strong> 쪽에만 집중하겠다.</p>
<h3 id="detectdraggestures">detectDragGestures</h3>
<p><code>detectDragGestures</code>는 사용자의 드래그 동작을 처리할하는 detector를 붙여 처리할 수 있게 해주는 API이다.</p>
<p>공식 홈페이지에서 제공하는 함수에 대한 설명을 보면 <code>onDragStart</code>, <code>onDragEnd</code>, <code>onDragCancel</code> 등 입력의 시작과 끝을 처리하고 <code>onDrag</code> 에서 드래그하면서 실시간으로 offset 값의 변화를 보여준다.</p>
<pre><code>suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -&gt; Unit = { },
    onDragEnd: () -&gt; Unit = { },
    onDragCancel: () -&gt; Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -&gt; Unit
): Unit</code></pre><p>이를 이용해서 사용자가 획을 쭉 이어 그릴 때마다 변화하는 offset 값들을 받아 획을 만들고 이를 <code>Canvas</code>에서 그려준다면 그리기 기능을 구현할 수 있다.</p>
<pre><code>var point by remember { mutableStateOf(Offset.Zero) } // point 위치 추적을 위한 State
val points = remember { mutableListOf&lt;Offset&gt;() } // 새로 그려지는 path 표시하기 위한 points State

var path by remember { mutableStateOf(Path()) } // 새로 그려지고 있는 중인 획 State
val paths = remember { mutableStateListOf&lt;Path&gt;() } // 다 그려진 획 리스트 State</code></pre><p>먼저 드래그 위치에 따라 변경되는 offset 값을 저장할 mutableStateOf 변수 <code>point</code>이 필요하다. 이렇게 변화한 point의 움직임을 저장할수 있는 리스트 변수 <code>points</code>도 선언해준다. </p>
<p>드래그 할때마다 실시간으로 <code>points</code>에 저장된 점들로 하나의 <code>Path</code>를 만들어 정상적으로 획이 그려지고 있음을 보여줘야하기 때문에 이를 보여줄 <code>path</code> 변수를 선언한다.</p>
<p>마지막으로 완성되어있는 <code>path</code> 들을 저장한 <code>paths</code> 리스트 변수를 선언해 리컴포지션이 일어나도 이전의 획들도 전부 보여줄 수 있게 한다.</p>
<p>이제 이 변수들을 이용해 구현한 <code>Canvas</code>의 코드는 아래와 같다.</p>
<pre><code>var point by remember { mutableStateOf(Offset.Zero) } // point 위치 추적을 위한 State
val points = remember { mutableListOf&lt;Offset&gt;() } // 새로 그려지는 path 표시하기 위한 points State

var path by remember { mutableStateOf(Path()) } // 새로 그려지고 있는 중인 획 State
val paths = remember { mutableStateListOf&lt;Path&gt;() } // 다 그려진 획 리스트 State

Canvas(
    modifier = Modifier.size(360.dp)
        .background(Color.White)
        .aspectRatio(1.0f)
        .pointerInput(Unit) {
            detectDragGestures(
                onDragStart = { offset -&gt;
                    point = offset
                    points.add(point)
                },
                onDrag = { _, dragAmount -&gt;
                    point += dragAmount
                    points.add(point)
                    // onDrag가 호출될 때마다 현재 그리는 획을 새로 보여줌
                    path = Path()
                    points.forEachIndexed { index, point -&gt;
                        if (index == 0) {
                            path.moveTo(point.x, point.y)
                        } else {
                            path.lineTo(point.x, point.y)
                        }
                    }
                },
                onDragEnd = {
                    paths.add(path)
                    points.clear()
                }
            )
        },
) {
    // 이미 완성된 획들
    paths.forEach { path -&gt;
        drawPath(
            path = path,
            color = Color.Black,
            style = Stroke()
        )
    }
    // 현재 그려지고 있는 획
    drawPath(
        path = path,
        color = Color.Black,
        style = Stroke()
    )
}</code></pre><p><code>detectDragGestures</code> 에서 사용자의 드래그 동작을 처리한다.</p>
<ol>
<li><code>onDragStart</code>: 사용자가 새로운 획을 그리기 시작함. <code>point</code>에 시작점을 위치를 저장한다. <code>points</code> 리스트에도 현재 지점값을 저장한다.</li>
<li><code>onDrag</code>: 사용자가 획을 그리는 중. offset이 변화할 때마다 새로운 <code>point</code> 값을 <code>points</code> 리스트에 저장한다. 그리고 새로운 <code>Path</code>를 만들어 현재 그려진 부분까지 선을 이어서 그린다. </li>
<li><code>onDragEnd</code>: 획 완성. 완성된 획을 <code>paths</code> 리스트에 저장하고 <code>points</code> 를 클리어한다.</li>
</ol>
<p>그리고 <code>Canvas</code>의 <code>DrawScope</code> 에서는 이미 완성된 획들이 저장되어있는 <code>paths</code> 변수의 획들을 차례대로 그리고 이어서 현재 그려지고 있는 획도 <code>drawPath</code>를 통해 그려준다.</p>
<p>다음에는 획 되돌리기 기능을 구현해보도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] rememberCoroutineScope vs LaunchedEffect]]></title>
            <link>https://velog.io/@moonliam_/AndroidCompose-rememberCoroutineScope-vs-LaunchedEffect</link>
            <guid>https://velog.io/@moonliam_/AndroidCompose-rememberCoroutineScope-vs-LaunchedEffect</guid>
            <pubDate>Wed, 20 Dec 2023 15:00:53 GMT</pubDate>
            <description><![CDATA[<h3 id="compose와-부수-효과side-effect">Compose와 부수 효과(Side Effect)</h3>
<p>기본적으로 <code>Composable</code> 함수 안에서는 <strong>기존의 방식으로 코루틴을 사용할 수 없다.</strong> 대신 Compose에서도 코루틴을 구현할 수 있도록 <strong>Effect API</strong>라는 것을 제공한다.</p>
<p>안드로이드에서는 Compose 함수 외부에서 앱 상태가 변화하는 거를 <strong>부수 효과(Side Effect)</strong>라고 한다. 그리고 공식문서에는 이러한 부수 효과를 일으키는 요소가 Compose 함수 내에 존재해서는 안된다고 정의하고 있다.</p>
<blockquote>
<p>컴포저블의 수명 주기 및 속성(예: 예측할 수 없는 리컴포지션 또는 다른 순서로 컴포저블의 리컴포지션 실행, 삭제할 수 있는 리컴포지션)으로 인해 컴포저블에는 부수 효과가 없는 것이 좋습니다. 
<a href="https://developer.android.com/jetpack/compose/side-effects?hl=ko">출처: 안드로이드 공식 홈페이지</a></p>
</blockquote>
<p>Compose 구성요소들은 <strong>언제 리컴포지션이 발생할 지 모르기 때문에</strong> Compose 함수에서 부수 효과를 발생시키는 경우 예상치 못한 현상이 발생할 수도 있다는 뜻이다.</p>
<p>하지만 부수 효과가 필요할 때도 있기 때문에 이를 위해서 나온 게 바로 <strong>부수 효과(Side Effect) API</strong>이다.</p>
<h3 id="launchedeffect">LaunchedEffect</h3>
<p>부수효과 API의 가장 대표적인 컴포지션이 바로 <code>LaunchedEffect</code> 이다. <code>LaunchedEffect</code>를 사용하면 컴포저블 함수 내에서도 안전하게 정지 함수를 호출할 수 있다.</p>
<p><code>LaunchedEffect</code>는 <code>state</code> 형태의 <code>key</code> 값과 실행 블럭을 매개변수로 받는다. <code>key</code> 값에 변화가 생기면 실행 블럭 내부에 있는 코드가 실행된다.</p>
<pre><code>@Composable
fun MyScreen(
    state: UiState&lt;List&lt;Movie&gt;&gt;,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        // `LaunchedEffect` will cancel and re-launch if
        // `scaffoldState.snackbarHostState` changes
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `state.hasError` is false, and only start when `state.hasError` is true
            // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
            scaffoldState.snackbarHostState.showSnackbar(
                message = &quot;Error message&quot;,
                actionLabel = &quot;Retry message&quot;
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}</code></pre><p>위의 <code>LaunchedEffect</code> 의 경우 <code>scaffoldState.snakbarHostState)</code>를 key 값으로 받고 있으며 해당 값에 변화가 생길 경우 <code>LaunchedEffect</code> 실행 블럭 내부의 코드가 실행되어 스낵바를 보여준다.</p>
<h3 id="remembercoroutinescope">rememberCoroutineScope</h3>
<p>부수효과를 구현하는 또 다른 방법으로는 <code>rememberCoroutineScope</code> 함수로 호출된 컴포지션에 바인딩된 <code>CoroutineScope</code>를 반환하여 해당 scope 안에서 suspend 함수를 구현하는 방법이다.</p>
<p><code>rememberCoroutineScope</code>는 호출된 컴포지션에 바인딩되어있는 scope를 반환하기 때문에 해당 컴포지션이 취소되면 coroutineScope도 같이 취소된다.</p>
<pre><code>@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen&#39;s lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(&quot;Something happened!&quot;)
                    }
                }
            ) {
                Text(&quot;Press me&quot;)
            }
        }
    }
}</code></pre><p><code>val scope = rememberCoroutineScope()</code>를 통해서 생성된 scope는 <code>MovieScreen</code> 이라는 컴포저블 함수의 생명주기를 따르게 된다. </p>
<p>이때 주의해야할 점은 해당 scope로 생성된 코루틴은 어디까지나 <code>MovieScreen</code>이라는 컴포저블 함수의 <strong>생명주기만을 따르는 거지 실제 코루틴의 위치는 컴포저블 함수 외부에 있다</strong>는 사실이다.</p>
<p>처음에 설명했듯이 컴포저블 함수 내부에서는 외부의 값을 바꾸는 부수효과를 줄 수 없다. 대신 부수효과를 주기 위한 코루틴을 외부에 생성하고 이 코루틴의 생명주기가 Composable 함수 생명주기를 따르게 만들 뿐이다.</p>
<h3 id="remembercoroutinescope-사용시-주의-사항">rememberCoroutineScope 사용시 주의 사항</h3>
<p><code>rememberCoroutineScope</code>는 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하거나코루틴 하나 이상의 수명 주기를 수동으로 관리해야 할 때(예: 사용자 이벤트가 발생할 때 애니메이션을 취소해야 하는 경우) 유용하게 사용할 수 있다.</p>
<p>반면에 외부에 새로 코루틴을 만든다는 특성 때문에 사용시 주의사항이 있다.</p>
<p>바로 <strong>재구현(Recomposition)</strong>이 일어날 때는 <code>rememberCoroutineScope</code>로 생성된 코루틴이 취소되지 않는다는 것이다.</p>
<p>예를 들어, 컴포저블 함수 안에 <code>Button</code>을 만들고 <code>onClick</code> 블럭에 <code>rememberCoroutineScope</code>로 코루틴을 생성하며 이 작업으로 재구현이 발생한다고 가정해보자.</p>
<p>사용자가 버튼을 누를때마다 재구현된 Button에서 계속해서 새로운 코루틴이 생성될 것이다.</p>
<p>이처럼 재구현이 상당히 자주 일어나는 화면에서 <code>rememberCoroutineScope</code>를 설정했다면 지나치게 많은 코루틴이 생성되어 심할 경우 앱 크래시를 유발할 수도 있다.</p>
<p>레퍼런스)</p>
<ol>
<li><a href="https://heegs.tistory.com/123">[Jetpack] Compose 사용하기 - 2. Side Effect와 Coroutine 1</a></li>
<li><a href="https://developer.android.com/jetpack/compose/side-effects?hl=ko">Compose의 부수 효과</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android / Compose] LazyColumn/LazyRow 알아보기]]></title>
            <link>https://velog.io/@moonliam_/Android-Compose-LazyColumnLazyRow-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@moonliam_/Android-Compose-LazyColumnLazyRow-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 18 Dec 2023 08:45:00 GMT</pubDate>
            <description><![CDATA[<p>요즘 Compose 기반의 앱 개발 팀 프로젝트에 참여하고 있다. 나로써는 처음으로 Compose를 사용해보는 프로젝트였는데 쓰면 쓸 수록 지금까지 왜 xml기반으로 앱을 구현했는지 이해가 안될 정도로 너무 쉽고 편리했다. </p>
<p>xml과 코드를 분리해서 UI를 구현할 때보다 코드량이 엄청나게 줄어들었고 선언형 API를 사용하기 때문에 코드가 직관적이어서 쉽게 기능을 이해할 수 있고 다른 사람이 쓴 코드를 이해하는데도 크게 어렵지않다. (이거는 내 팀원분이 굉장히 코드를 잘 짜주셔서 그런 것도 있다.) </p>
<p>이러한 장점을 가장 크게 느낀 부분이 바로 기존의 <code>RecyclerView</code>에 해당하는 뷰를 Compose의 <code>LazyColumn</code>, <code>LazyRow</code>를 이용해서 구현했을 때다.</p>
<p><code>RecyclerView</code>를 만들기 위해선 <code>ViewHolder</code>, <code>Adapter</code> 클래스도 만들어주고 또 xml에서 각 아이템의 레이아웃도 만들어줘야하고 할 게 굉장히 많았는데, Compose의 <code>LazyColumn</code>, <code>LazyRow</code>를 이용해서 구현하니 비교도 안될 정도로 간단하게 구현할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/22e1d68a-0385-4e97-966b-5dd8d7f923c8/image.png" alt=""></p>
<p><code>LazyColumn</code>, <code>LazyRow</code>는 기존의 RecyclerView와 동일하게 리스트에 속한 모든 View를 한번에 그리지 않고 스크롤하면서 화면에 보여지게 될 때만 그리게 함으로써 리소스 사용을 최적화하기 위한 용도로 만들어졌다.</p>
<p>Android 공식 홈페이지 예제 코드로 RecyclerView의 기능을 얼마나 간단하게 짤 수 있는지 살펴보자.</p>
<h3 id="간단-예제-코드">간단 예제 코드</h3>
<pre><code>@Composable
fun MessageList(messages: List&lt;Message&gt;) {
    LazyColumn {
        items(messages) { message -&gt;
            MessageRow(message)
        }
    }
}</code></pre><p><code>LazyColumn</code> 에서 아이템을 추가하기 위해선 <code>LazyList Scope DSL</code> 블록안에 추가하고자 하는 아이템을 넣으면 된다. (<code>LazyRow</code>, <code>LazyGrid</code>도 동일)</p>
<p>아이템을 추가하는 API는 크게 2가지가 있다.</p>
<ol>
<li><code>item</code> 블록을 사용해 하나 추가</li>
<li><code>items</code> 블록을 사용해 여러 개 추가</li>
</ol>
<p><code>item</code>의 인덱스가 필요할 때는 <code>itemsIndexed</code> API를 사용하면 된다.</p>
<pre><code>LazyColumn {
    itemsIndexed(messages) { index, message -&gt;
        MessageRow(index, message)
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/moonliam_/post/7997d23b-6c5b-4809-beb9-d6f26f1e0d6c/image.png" alt=""></p>
<blockquote>
<p>단, Compose의 <code>LazyColumn</code>, <code>LazyRow</code> 등은 RecyclerView와 다르게 하위 항목을 재사용하지 않는다. 대신 스크롤에 따라 새로운 Composable 객체를 발행(emit)한다. 이 방법이 안드로이드 View를 초기화하는 것보다 상대적으로 비용도 적고 성능도 더 좋다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] ViewModel에서 Context를 참조하면 안되는 이유]]></title>
            <link>https://velog.io/@moonliam_/Android-ViewModel%EC%97%90%EC%84%9C-Context%EB%A5%BC-%EC%B0%B8%EC%A1%B0%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@moonliam_/Android-ViewModel%EC%97%90%EC%84%9C-Context%EB%A5%BC-%EC%B0%B8%EC%A1%B0%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 15 Dec 2023 14:40:05 GMT</pubDate>
            <description><![CDATA[<h2 id="1-context가-정확히-뭐지">1. Context가 정확히 뭐지?</h2>
<p>안드로이드 개발을 하다보면 필연적으로 <code>Context</code>라는 녀석을 사용하게 된다.</p>
<p><code>Context</code>를 안드로이드 공식 홈페이지에서는 다음과 같이 설명하고 있다.</p>
<blockquote>
<p><strong>Interface to global information about an application environment.</strong> This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
<a href="https://developer.android.com/reference/android/content/Context">출처</a></p>
</blockquote>
<p>필자의 뛰어나지 않은 영어 실력으로 해석 및 요약해보자면... <strong>애플리케이션에 대한 글로벌 정보를 가지고 있는 인터페이스.</strong> 여기서 글로벌 정보란 패키지명, 리소스 정보와 같은 앱 자체 데이터들을 이야기하는 거 같다. 뿐만 아니라 다른 <strong>액티비티를 실행</strong>하거나 <strong>브로드캐스팅</strong>, <strong>인텐트 정보 받기</strong> 등 애플리케이션 단계에서의 동작을 가능하게 해주는 것 역시 이 <code>Context</code>이다.</p>
<p><code>Context</code>에 대한 좀 더 자세한 이야기는 다른 포스트에서 다루도록 하고 일단은 ViewModel 이야기로 넘어가보자.</p>
<h2 id="2-viewmodel에서-context를-참조하면-안되는-이유">2. ViewModel에서 Context를 참조하면 안되는 이유</h2>
<p>이를 이해하기 위해선 먼저 <code>ViewModel</code>과 <code>Activity</code>의 생명주기에 대해서 알아야한다.</p>
<p><img src="https://velog.velcdn.com/images/moonliam_/post/6e8def24-8050-4b37-82c3-3565f08b9b70/image.png" alt=""></p>
<p>ViewModel의 생명주기는 Activity가 최초 생성될 때 함께 시작한다.</p>
<p>하지만 Activity가 <code>onDestroy</code> 되고 다시 <code>onCreate</code> 되는 경우 ViewModel은 소멸되지 않고 그대로 유지되는 것을 확인할 수 있다.</p>
<p>가장 대표적인 예시로 <strong>화면 회전</strong>을 들 수 있다. 안드로이드 기기에서 화면을 회전할 경우 UI를 다시 그리기 위해 Activity가 <code>onDestroy</code> 를 호출해 종료한 다음 다시 <code>onCreate</code> 처음부터 생성된다.</p>
<p>하지만 Activity가 이런 소멸과 생성을 반복하는 동안 ViewModel은 소멸되지 않고 유지된다. <strong>ViewModel이 완전히 종료되는 것은 Activity가 완전히 종료되었을 때 (혹은 Fragment가 완전히 분리되었을 때) 뿐이다.</strong></p>
<p>자 이제 여기서 <code>ViewModel</code>이 <code>Context</code>를 참조하고 있다고 가정해보자. 그리고 그 상태에서 화면 회전이 일어나 Activity가 소멸되고 다시 생성되었다면?</p>
<p><strong>ViewModel은 이미 소멸되었던 Activity의 Context를 참조</strong>하게 될 것이고 이는 충돌이나 예외 등의 문제로 이어질 수 있다.</p>
<h2 id="3-그럼-아예-context를-참조할-방법이-없는가">3. 그럼 아예 Context를 참조할 방법이 없는가?</h2>
<p>ViewModel에서 Context를 참조하면 안된다는 것을 알겠는데 그럼에도 불구하고 부득이하게 참조해야만 하는 상황이 발생한다면?</p>
<p>이런 경우를 위해서 Application을 참조하고 있는 <code>AndroidViewModel</code>이라는 ViewModel의 확장 클래스가 존재한다.</p>
<p><code>AndroidViewModel</code>을 상속받아 ViewModel을 구현하면 예시와 같은 방식으로 Application Context를 활용할 수 있다.</p>
<pre><code>val context = getApplication&lt;Application&gt;().applicationContext</code></pre><p>위의 내용을 정리한 공식 문서 내용으로 마무리 하겠다.</p>
<blockquote>
<p>주의: ViewModel은 뷰, Lifecycle 또는 활동 컨텍스트 참조를 포함하는 클래스를 참조해서는 안 됩니다.
 ViewModel 객체는 뷰 또는 LifecycleOwners의 특정 인스턴스화보다 오래 지속되도록 설계되었습니다. 이러한 설계로 인해 뷰 및 Lifecycle 객체에 관해 알지 못할 때도 ViewModel을 다루는 테스트를 더 쉽게 작성할 수 있습니다. ViewModel 객체에는 LiveData 객체와 같은 LifecycleObservers가 포함될 수 있습니다. 그러나 ViewModel 객체는 LiveData 객체와 같이 수명 주기를 인식하는 Observable의 변경사항을 관찰해서는 안 됩니다. 예를 들어 ViewModel은 시스템 서비스를 찾는 데 Application 컨텍스트가 필요하면 AndroidViewModel 클래스를 확장하고 생성자에 Application을 받는 생성자를 포함할 수 있습니다(Application 클래스가 Context를 확장하므로).</p>
</blockquote>
<p>레퍼런스)</p>
<ol>
<li><a href="https://dev-repository.tistory.com/116">Android ViewModel에서 Context를 올바르게 사용하는 방법</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Sealed Class and Interface]]></title>
            <link>https://velog.io/@moonliam_/Kotlin-Sealed-Class-and-Interface</link>
            <guid>https://velog.io/@moonliam_/Kotlin-Sealed-Class-and-Interface</guid>
            <pubDate>Thu, 30 Nov 2023 16:34:02 GMT</pubDate>
            <description><![CDATA[<h3 id="sealed-class-왜-필요한가">Sealed Class? 왜 필요한가</h3>
<p>하나의 부모 Class <code>Parent</code>가 존재하고 해당 Class를 상속받는 여러개의 자식 Class <code>Child</code>가 여러개 존재한다고 가정해보자.</p>
<p>이때 컴파일러는 <code>Parent</code>를 상속받는 자식 Class <code>Child</code>가 얼마나 존재하는지 애초에 존재는 하는지 알지 못한다.</p>
<p>예를 들어, 사용자의 상태를 클래스로 나타내기 위해 추상 클래스 <code>PersonState</code>를 만들고 이를 상속받는 3개의 자식 클래스 <code>Running</code>, <code>Walking</code>, <code>Idle</code>을 만들고자 할 때, 아래와 같이 코드로 구현할 수 있다.</p>
<pre><code>abstract class PersonState

class Running : PersonState()
class Walking : PersonState()
class Idle : PersonState()</code></pre><p>이때 각 <code>PersonState</code> 별로 상태 메세지를 얻고자 한다면 </p>
<pre><code>fun getStateMessage(personState: PersonState): String {
    return when (personState) {
        is Running -&gt; &quot;Person is running&quot;
        is Walking -&gt; &quot;Person is walking&quot;
        is Idle -&gt; &quot;Person is doing nothing&quot;
    }
}</code></pre><p>위와 같이 <code>getStateMessage</code> 함수를 만들 수 있다.</p>
<p>하지만 <strong>이 코드는 <code>else</code> branch를 추가하라는 오류 메세지를 발생시킨다.</strong></p>
<p>개발자는 <code>PersonState</code>의 자식 클래스가 <code>Running</code>, <code>Walking</code>, <code>Idle</code> 3가지 밖에 없다는 걸 알지만 컴파일러 입장에서는 <code>PersonState</code>의 자식 클래스가 얼마나 있는지 모르니 <code>else</code>로 예외 처리를 필요하다고 하는 거다.</p>
<p>하지만 반대로 <code>else</code> branch를 추가하는 대신 다른 <code>PersonState</code> 상태에 대해 처리해주지 않는다면?</p>
<pre><code>fun getStateMessage(personState: PersonState): String {
    return when (personState) {
        is Running -&gt; &quot;Person is running&quot;
        is Walking -&gt; &quot;Person is walking&quot;
        else -&gt; &quot;No State&quot;
    }
}</code></pre><p>이 코드는 정상적으로 컴파일된다. 하지만 <code>Idle</code> 상태를 가지고 있을때도 &quot;No State&quot;를 출력하는 정상적이지 않은 동작을 할 것이다.</p>
<p>위와 같은 문제를 <strong>Sealed Class</strong>를 이용해서 효과적으로 해결할 수 있다.</p>
<h3 id="sealed-class란">Sealed Class란</h3>
<p><strong><code>Sealed Class</code></strong>는 추상 클래스(abstract class)의 하나로써 상속 받는 <strong>자식 클래스의 종류를 제한하는 특성을 갖고 있다.</strong></p>
<p>위의 예시 코드를 Sealed Class를 사용해서 구현하면 아래와 같다.</p>
<pre><code>sealed class PersonState

class Running : PersonState()
class Walking : PersonState()
class Idle : PersonState()</code></pre><pre><code>fun getStateMessage(personState: PersonState): String {
    return when (personState) {
        is Running -&gt; &quot;Person is running&quot;
        is Walking -&gt; &quot;Person is walking&quot;
        is Idle -&gt; &quot;Person is doing nothing&quot;
    }
}</code></pre><p>Sealed Class로 PersonState 클래스를 구현하면 <code>getStateMessage</code> 함수가 오류를 발생시키지 않는다.</p>
<p>Sealed Class가 자식 클래스를 <code>Running</code>, <code>Walking</code>, <code>Idle</code> 3가지로만 제한했다는 것을 컴파일러가 알고 있기 때문이다.</p>
<h3 id="object로-상속-받기">Object로 상속 받기</h3>
<pre><code>sealed class PersonState

class Running : PersonState()
class Walking : PersonState()
class Idle : PersonState()</code></pre><p>이 Sealed Class는 정상적으로 작동하나 한 가지 문제가 있다. class에 주의(밑줄 표시)가 뜨는 것을 확인할 수 있는데 경고문은 다음과 같다.</p>
<blockquote>
<p><code>sealed</code> subclass has no state and no overridden <code>equals()</code></p>
</blockquote>
<p>즉, 상태(변수)가 있거나 <code>equals()</code>를 override할 때만 class로 상속 받아야한다는 뜻이다.</p>
<p>위의 예시와 같은 경우에는 class가 아닌 <code>object</code> 로 상속받을 수 있다.</p>
<pre><code>sealed class PersonState

object Running : PersonState()
object Walking : PersonState()
object Idle : PersonState()</code></pre><h3 id="sealed-interface">Sealed Interface</h3>
<p><code>sealed</code> 는 Class 뿐만 아니라 <code>Interface</code>에서도 비슷하게 활용 가능하다.</p>
<pre><code>sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error</code></pre><p>레퍼런스)</p>
<blockquote>
<ol>
<li><a href="https://kotlinworld.com/165">[Kotlin] Kotlin sealed class란 무엇인가?</a></li>
<li><a href="https://kotlinlang.org/docs/sealed-classes.html">Sealed classes and interfaces</a></li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Coroutine Flow (2)]]></title>
            <link>https://velog.io/@moonliam_/Kotlin-Coroutine-Flow-2</link>
            <guid>https://velog.io/@moonliam_/Kotlin-Coroutine-Flow-2</guid>
            <pubDate>Mon, 06 Nov 2023 10:08:08 GMT</pubDate>
            <description><![CDATA[<h2 id="flow-중간-연산자">Flow 중간 연산자</h2>
<p>Flow는 Collections, Sequence와 같이 연산자를 이용해 변환될 수 있다. 이러한 중간 연산자는 업스트림 Flow에 적용되어 다운스트림 Flow를 반환한다. 이러한 연산자들은 Flow만큼 차갑다. 연산자를 호출하는 것 자체는 suspend 함수가 아니다. 빠르게 작동하여 새롭게 변환된 Flow를 반환한다.</p>
<p>기본 중간 연산자들은 <code>map</code> 혹은 <code>filter</code>와 같은 친숙한 이름을 가지고 있다. 이 연산자들이 Sequence에서 사용될 때와 차이점은 이 연산자들 내부 코드 블록에서는 suspend 함수를 호출할 수 있다는 점이다.</p>
<p>예를 들어 요청을 수행하는 것이 오래 걸리는 작업이고 suspend 기능으로 구현되어 있는 경우에도, 요청들을 받는 Flow를 <code>map</code> 연산자를 사용해 결과에 매핑할 수 있다.</p>
<pre><code>suspend fun performRequest(request: Int): String {
    delay(1000) // imitate long-running asynchronous work
    return &quot;response $request&quot;
}

fun main() = runBlocking&lt;Unit&gt; {
    (1..3).asFlow() // a flow of requests
        .map { request -&gt; performRequest(request) }
        .collect { response -&gt; println(response) }
}</code></pre><p>위 코드의 결과는 아래와 같으면 각 줄은 이전 줄로부터 1초 후에 나타난다.</p>
<pre><code>response 1
response 2
response 3</code></pre><h3 id="transform-연산자">Transform 연산자</h3>
<p>Flow 변환 연산자들 중 가장 일반적인 것은 <code>transform</code> 이다. <code>map</code>이나 <code>filter</code>와 같은 간단한 변환을 구현할 수도 있고 임의의 횟수만큼 값을 <code>emit</code> 할 수도 있다.</p>
<p>예를 들어 아래와 같이 오래걸리는 비동기 요청을 수행하기 전에 문자열을 방출(emit)하고 그 응답을 기다릴 수 있다.</p>
<pre><code>(1..3).asFlow() // a flow of requests
    .transform { request -&gt;
        emit(&quot;Making request $request&quot;) 
        emit(performRequest(request)) 
    }
    .collect { response -&gt; println(response) }</code></pre><pre><code>Making request 1
response 1
Making request 2
response 2
Making request 3
response 3</code></pre><p>이 외에도 방출 크기를 한정할 수 있는 연산자 <code>take</code> 등이 있다.</p>
<h2 id="flow-터미널-연산자">Flow 터미널 연산자</h2>
<p>Flow의 터미널 연산자는 flow의 수집(collection)을 시작하는 <strong>일시정지 함수</strong>이다. 가장 기본적으로 <code>collect</code> 연산자가 있으며 이외에 <code>toList</code>, <code>toSet</code>, <code>first</code>, <code>single</code>, <code>reduce</code>, <code>fold</code> 등 다양한 터미널 연산자가 존재한다.</p>
<pre><code>val sum = (1..5).asFlow()
    .map { it * it } // squares of numbers from 1 to 5                           
    .reduce { a, b -&gt; a + b } // sum them (terminal operator)
println(sum)</code></pre><pre><code>15</code></pre><h2 id="flow는-순차적이다">Flow는 순차적이다</h2>
<p>특수한 연산자를 사용하지 않는 한 각 개별 Flow의 컬렉션은 순차적으로 동작한다. 여기서 컬렉션은 터미널 연산자를 호출하는 Coroutine에서 직접 동작하며 이때 기본적으로 어떠한 새로운 Coroutine도 실행되지 않는다.</p>
<p>방출된 각 값들은 <code>transform</code>과 같은 중간 연산자들에 의해 업스트림에서 다운스트림으로 ㅓ리된 후 터미널 연산자에게 전달된다.</p>
<p>다음은 정수 중 짝수를 필터링한 후 문자열에 매핑하는 코드이다.</p>
<pre><code>(1..5).asFlow()
    .filter {
        println(&quot;Filter $it&quot;)
        it % 2 == 0              
    }              
    .map { 
        println(&quot;Map $it&quot;)
        &quot;string $it&quot;
    }.collect { 
        println(&quot;Collect $it&quot;)
    }</code></pre><pre><code>Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5</code></pre><h2 id="flow-context">Flow Context</h2>
<p>Flow의 수집은 언제나 Coroutine을 호출하는 Context상에서 일어난다. 만약 <code>simple</code>이라는 Flow가 있다면, 다음 코드의 <code>simple</code> flow는 구체적인 구현과 상관없이 코드 작성자가 지정한 Context상에서 실행된다.</p>
<p>이러한 Flow의 성질을 <strong>컨텍스트 보존(context preservation)</strong>이라 부른다.</p>
<pre><code>withContext(context) {
    simple().collect { value -&gt; 
        println(value) // run in the specified context
    }
}</code></pre><p>따라서 기본적으로 <code>flow { ... }</code> 빌더 내부의 코드는 해당 Flow의 collector가 제공하는 Context 상에서 실행된다. 예를 들어, <code>simple</code> 함수의 구현이 호출되는 스레드를 출력하고 3개의 숫자들을 방출한다고 해보자.</p>
<pre><code>fun simple(): Flow&lt;Int&gt; = flow {
    log(&quot;Started simple flow&quot;)
    for (i in 1..3) {
        emit(i)
    }
}  

fun main() = runBlocking&lt;Unit&gt; {
    simple().collect { value -&gt; log(&quot;Collected $value&quot;) } 
}</code></pre><pre><code>[main @coroutine#1] Started simple flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3</code></pre><p><code>simple().collect</code>가 메인 스레드에서 호출되므로, <code>simple</code> flow의 body 또한 메인 스레드에서 호출된다. </p>
<h2 id="withcontext를-사용할-때-주의점">withContext를 사용할 때 주의점</h2>
<p>하지만 CPU를 오래 사용하는 코드는 <em>Dispatchers.Default</em> Context에서 실행되어야 할 수도 있고, UI를 업데이트하는 코드는 <em>Dispatchers.Main</em> Context에서 실행되어야 할 수 있다.</p>
<p>일반적으로 <code>withContext</code>는 Coroutine을 사용하는 코드의 Context를 변경하는데 사용되지만, <code>flow { ... }</code> 빌더의 코드는 Context 보존 특성을 준수해야하므로 다른 Context에서 방출하는 것은 허용되지 않는다.</p>
<pre><code>fun simple(): Flow&lt;Int&gt; = flow {
    // The WRONG way to change context for CPU-consuming code in flow builder
    kotlinx.coroutines.withContext(Dispatchers.Default) {
        for (i in 1..3) {
            Thread.sleep(100) // pretend we are computing it in CPU-consuming way
            emit(i) // emit next value
        }
    }
}

fun main() = runBlocking&lt;Unit&gt; {
    simple().collect { value -&gt; println(value) } 
}</code></pre><p>이 코드는 아래의 Exception을 생성한다.</p>
<pre><code>Exception in thread &quot;main&quot; java.lang.IllegalStateException: Flow invariant is violated:
        Flow was collected in [CoroutineId(1), &quot;coroutine#1&quot;:BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],
        but emission happened in [CoroutineId(1), &quot;coroutine#1&quot;:DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default].
        Please refer to &#39;flow&#39; documentation or use &#39;flowOn&#39; instead
    at ...</code></pre><h2 id="다른-coroutinecontext에서-실행하기">다른 CoroutineContext에서 실행하기</h2>
<p>앞에서 언급한 것처럼 <code>flow { ... }</code> 빌더 생산자는 수집하는 <code>CoroutineContext</code> 에서 실행된다. 따라서 다른 <code>CoroutineContext</code> 에서 값을 <code>emit</code> 할 수 없다.</p>
<p>하지만 상황에 따라서 다른 <code>Context</code>에서 Flow를 수집하거나 값을 방출해야할 수도 있다. 이렇게 Flow의 Context를 변경하려면 중간 연산자 <code>flowOn</code> 을 사용해야 한다.</p>
<h3 id="flowon-연산자">flowOn 연산자</h3>
<p><code>flowOn</code>은 <em>업스트림 흐름*의 Context를 변경한다. 즉, 생산자 및 중간 연산자가 <code>flowOn</code> 전에 적용된다. *다운스트림 흐름</em> (<code>flowOn</code> 이후의 중간 연산자 및 소비자)는 영향을 받지 않으며 흐름에서 <code>collect</code> 하는데 사용되는 <code>CoroutineContext</code>에서 실행된다. <code>flowOn</code> 연산자가 여러 개 있는 경우 각 연산자는 현재 위치에서 업스트림을변경한다. </p>
<pre><code>class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val userData: UserData,
    private val defaultDispatcher: CoroutineDispatcher
) {
    val favoriteLatestNews: Flow&lt;List&lt;ArticleHeadline&gt;&gt; =
        newsRemoteDataSource.latestNews
            .map { news -&gt; // Executes on the default dispatcher
                news.filter { userData.isFavoriteTopic(it) }
            }
            .onEach { news -&gt; // Executes on the default dispatcher
                saveInCache(news)
            }
            // flowOn affects the upstream flow ↑
            .flowOn(defaultDispatcher)
            // the downstream flow ↓ is not affected
            .catch { exception -&gt; // Executes in the consumer&#39;s context
                emit(lastCachedNews())
            }
}</code></pre><p>위 코드에서 <code>onEach</code> 및 <code>map</code> 연산자는 <code>Dispatchers.Default</code>를 사용하는 반면, <code>catch</code> 연산자와 소비자는 <code>viewModelScope</code>에 사용되는 <code>Dispatchers.Main</code>에서 실행된다.</p>
]]></description>
        </item>
    </channel>
</rss>