<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>couch_potato.log</title>
        <link>https://velog.io/</link>
        <description>안드로이드 외길</description>
        <lastBuildDate>Mon, 09 Feb 2026 08:16:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>couch_potato.log</title>
            <url>https://velog.velcdn.com/images/couch_potato/profile/34cceb1d-5325-4ec8-a527-ee55d3c76529/image.PNG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. couch_potato.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/couch_potato" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] Modifier 이것저것 알아보기]]></title>
            <link>https://velog.io/@couch_potato/Android-Modifier-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/Android-Modifier-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 09 Feb 2026 08:16:21 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/009d5511-fe0a-4933-b86a-39d815ba07be/image.png
" height = "100">오늘은 Manifest Android Interview를 읽어보며 Modifier에 대해 더 잘 알아보고자 한다.
Modifier를 깊게 파보기보다는 Modifier를 넓게 살펴보는 글로 여러 메서드가 어떻게 동작하는지를 간단하게 살펴볼 것이다.</p>
<h3 id="requiredsize는-어떻게-제약-조건을-무시할까">requiredSize()는 어떻게 제약 조건을 무시할까?</h3>
<p><code>requiredSize()</code>는 부모의 제약 조건에 관계없이 고정된 크기를 적용하는 메서드이다.</p>
<p>그렇다면 <code>size()</code>, <code>height()</code> 같은 다른 크기 관련 메서드와 달리 어떻게 <code>requiredSize()</code>는 고정된 크기를 적용하는 걸까?</p>
<p><code>size()</code>와 <code>requiredSize()</code>의 내부 구조를 통해 알아보자.</p>
<pre><code class="language-kotlin">@Stable
fun Modifier.size(size: Dp) =
    this.then(
        SizeElement(
            minWidth = size,
            maxWidth = size,
            minHeight = size,
            maxHeight = size,
            enforceIncoming = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = &quot;size&quot;
                    value = size
                },
        )
    )</code></pre>
<pre><code class="language-kotlin">@Stable
fun Modifier.requiredSize(size: Dp) =
    this.then(
        SizeElement(
            minWidth = size,
            maxWidth = size,
            minHeight = size,
            maxHeight = size,
            enforceIncoming = false,
            inspectorInfo =
                debugInspectorInfo {
                    name = &quot;requiredSize&quot;
                    value = size
                },
        )
    )</code></pre>
<p>두 메서드에서 주목할 유의미한 차이는 <code>enforceIncoming</code>다.</p>
<p><code>size()</code>는 <code>enforceIncoming</code>가 true이고 <code>requiredSize()</code>는 false인데 이것이 두 메서드의 차이를 만든다.</p>
<pre><code class="language-kotlin">private class SizeElement(
    private val minWidth: Dp = Dp.Unspecified,
    private val minHeight: Dp = Dp.Unspecified,
    private val maxWidth: Dp = Dp.Unspecified,
    private val maxHeight: Dp = Dp.Unspecified,
    private val enforceIncoming: Boolean,
    private val inspectorInfo: InspectorInfo.() -&gt; Unit,
) : ModifierNodeElement&lt;SizeNode&gt;() {
    override fun create(): SizeNode =
        SizeNode(
            minWidth = minWidth,
            minHeight = minHeight,
            maxWidth = maxWidth,
            maxHeight = maxHeight,
            enforceIncoming = enforceIncoming,
        )
        ...</code></pre>
<p>SizeElement는 위와 같다. 이는 SizeNode를 생성하는데 이 SizeNode에 <code>enforceIncoming</code> 값이 전달된다.</p>
<pre><code class="language-kotlin">private class SizeNode(
    var minWidth: Dp = Dp.Unspecified,
    var minHeight: Dp = Dp.Unspecified,
    var maxWidth: Dp = Dp.Unspecified,
    var maxHeight: Dp = Dp.Unspecified,
    var enforceIncoming: Boolean,
) : LayoutModifierNode, Modifier.Node() {
    ...
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val wrappedConstraints =
            targetConstraints.let { targetConstraints -&gt;
                if (enforceIncoming) {
                    constraints.constrain(targetConstraints)
                } else {
                    val resolvedMinWidth =
                        if (minWidth.isSpecified) {
                            targetConstraints.minWidth
                        } else {
                            constraints.minWidth.fastCoerceAtMost(targetConstraints.maxWidth)
                        }
                    val resolvedMaxWidth =
                        if (maxWidth.isSpecified) {
                            targetConstraints.maxWidth
                        } else {
                            constraints.maxWidth.fastCoerceAtLeast(targetConstraints.minWidth)
                        }
                    val resolvedMinHeight =
                        if (minHeight.isSpecified) {
                            targetConstraints.minHeight
                        } else {
                            constraints.minHeight.fastCoerceAtMost(targetConstraints.maxHeight)
                        }
                    val resolvedMaxHeight =
                        if (maxHeight.isSpecified) {
                            targetConstraints.maxHeight
                        } else {
                            constraints.maxHeight.fastCoerceAtLeast(targetConstraints.minHeight)
                        }
                    Constraints(
                        resolvedMinWidth,
                        resolvedMaxWidth,
                        resolvedMinHeight,
                        resolvedMaxHeight,
                    )
                }
            }
    ...</code></pre>
<p>이 코드를 보면 <code>enforceIncoming</code>가 true면 제약 조건을 적용하고 false면 제약 조건을 적용하지 않고 자신의 크기를 적용한다.</p>
<h3 id="ongloballypositioned는-어떻게-컴포넌트의-절대-좌표를-가져올까">onGloballyPositioned는 어떻게 컴포넌트의 절대 좌표를 가져올까?</h3>
<p>먼저 onGloballyPositioned는 아래와 같이 OnGloballyPositionedNode를 생성한다.</p>
<pre><code class="language-kotlin">@Stable
fun Modifier.onGloballyPositioned(onGloballyPositioned: (LayoutCoordinates) -&gt; Unit) =
    this then OnGloballyPositionedElement(onGloballyPositioned)

private class OnGloballyPositionedElement(val onGloballyPositioned: (LayoutCoordinates) -&gt; Unit) :
    ModifierNodeElement&lt;OnGloballyPositionedNode&gt;() {
    override fun create(): OnGloballyPositionedNode {
        return OnGloballyPositionedNode(onGloballyPositioned)
    }
    ...</code></pre>
<p>OnGloballyPositionedNode는 다음과 같이 되어있다.</p>
<pre><code class="language-kotlin">private class OnGloballyPositionedNode(var callback: (LayoutCoordinates) -&gt; Unit) :
    Modifier.Node(), GlobalPositionAwareModifierNode {
    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
        callback(coordinates)
    }</code></pre>
<p>저 <code>onGloballyPositioned</code>를 호출함으로서 coordinates 값을 람다에 전달하는데 이것은 <code>LayoutNode.kt</code>에서 호출하는 것을 확인할 수 있다.</p>
<pre><code class="language-kotlin">internal class LayoutNode {
    ...    
    internal fun dispatchOnPositionedCallbacks() {
        if (layoutState != Idle || layoutPending || measurePending || isDeactivated) {
            return // it hasn&#39;t yet been properly positioned, so don&#39;t make a call
        }
        if (!isPlaced) {
            return // it hasn&#39;t been placed, so don&#39;t make a call
        }
        nodes.headToTail(Nodes.GlobalPositionAware) {
            it.onGloballyPositioned(it.requireCoordinator(Nodes.GlobalPositionAware))
        }
    }    
    ...
}</code></pre>
<p>컴포지션 단계중 Layout단계는 Measure와 Layout 단계로 나뉘는데 그중 Layout 단계가 마무리 될때 최종적으로  dispatchOnPositionedCallbacks()가 호출된다.</p>
<p>즉 최종 배치 마무리 단계에서 해당 컴포넌트의 좌표값을 호이스팅하게 되는 구조이다.</p>
<p>그럼 좌표값을 호이스팅하는 것을 알아봤으니 좌표값을 구하는 방법을 알아보자.</p>
<pre><code class="language-kotlin">internal abstract class NodeCoordinator(override val layoutNode: LayoutNode) :
    LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope {
    ...
    @OptIn(ExperimentalComposeUiApi::class)
    override fun localToRoot(relativeToLocal: Offset): Offset {
        checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
        onCoordinatesUsed()
        var coordinator: NodeCoordinator? = this
        var position = relativeToLocal
        while (coordinator != null) {
            if (ComposeUiFlags.isRectManagerOffsetUsageFromLayoutCoordinatesEnabled) {
                val layoutNode = coordinator.layoutNode
                if (
                    coordinator === layoutNode.outerCoordinator &amp;&amp;
                        !layoutNode.hasPositionalLayerTransformationsInOffsetFromRoot
                ) {
                    val offsetFromRectList =
                        layoutNode.requireOwner().rectManager.getOffsetFromRectListFor(layoutNode)
                    if (offsetFromRectList != IntOffset.Max) {
                        return position + offsetFromRectList
                    }
                }
            }
            position = coordinator.toParentPosition(position)
            coordinator = coordinator.wrappedBy
        }
        return position
    }
    ...
    open fun toParentPosition(
        position: Offset,
        includeMotionFrameOfReference: Boolean = true,
    ): Offset {
        val layer = layer
        val targetPosition = layer?.mapOffset(position, inverse = false) ?: position
        return if (!includeMotionFrameOfReference &amp;&amp; isPlacedUnderMotionFrameOfReference) {
            targetPosition
        } else {
            targetPosition + this.position
        }
    }
    ...
}</code></pre>
<p>이 부분이 NodeCoordinator가 좌표를 구하는 방법이다.</p>
<p><code>localToRoot</code> 메서드가 position 값을 반환하는데 이 position은 <code>coordinator = coordinator.wrappedBy</code>로 coordinator를 null이 될 때까지 계속 벗기면서 <code>coordinator.toParentPosition(position)</code>로 현재 position값과 부모 내의 상대적 위치값을 더해나가며 구한다.</p>
<p>즉, </p>
<ol>
<li>부모 내의 상대 위치 구하기</li>
<li>래퍼 벗기기</li>
<li>루트에 도달할 때 까지 반복</li>
</ol>
<p>이다.</p>
<blockquote>
<p>dispatchOnPositionedCallbacks()가 Layout 단계가 마무리 될 때 최종적으로 호출되기에 좌표를 첫 Layout 이후에 알게 된다.
그렇기에 이를 이용해 툴팁을 그리거나 할 경우 recomposition이 일어나게 된다.</p>
</blockquote>
<h3 id="box나-column-등에서-align이나-weight로-어떻게-부모-레이아웃에-자식의-데이터를-전달할까">Box나 Column 등에서 align이나 weight로 어떻게 부모 레이아웃에 자식의 데이터를 전달할까?</h3>
<p>이것도 NodeCoordinator에서 확인할 수 있다.
이어서 마저 확인해보자.</p>
<p>NodeCoordinator는 parentData라는 부모에게 전달하는 데이터를 가지고 있다.</p>
<pre><code class="language-kotlin">internal abstract class NodeCoordinator(override val layoutNode: LayoutNode) :
    LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope {
    ...
    override val parentData: Any?
        get() {
            // NOTE: If you make changes to this getter, please check the generated bytecode to
            // ensure no extra allocation is made. See the note below.
            if (layoutNode.nodes.has(Nodes.ParentData)) {
                val thisNode = tail
                // NOTE: Keep this mutable variable scoped inside the if statement. When moved
                // to the outer scope of get(), this causes the compiler to generate a
                // Ref$ObjectRef instance on every call of this getter.
                var data: Any? = null
                layoutNode.nodes.tailToHead { node -&gt;
                    if (node.isKind(Nodes.ParentData)) {
                        node.dispatchForKind(Nodes.ParentData) {
                            data = with(it) { layoutNode.density.modifyParentData(data) }
                        }
                    }
                    if (node === thisNode) return@tailToHead
                }
                return data
            }
            return null
        }
    ...
}</code></pre>
<p><code>tailToHead</code>는 코드상 가장 마지막에 쓴 Modifier부터 부모 방향으로 거슬러 올라가는 메서드로 트리의 tail에서 해당 coordinator가 담당하는 부분의 tail까지 올라오면서 data가 null부터 시작해서 정보를 계속 누적한다.</p>
<p>이것이 부모의 레이아웃에 닿으면 이 값을 통해 자식 레이아웃을 배치한다.
<code>Column</code> 코드를 예시로 살펴보면</p>
<pre><code class="language-kotlin">internal data class ColumnMeasurePolicy(
    private val verticalArrangement: Arrangement.Vertical,
    private val horizontalAlignment: Alignment.Horizontal,
) : MeasurePolicy, RowColumnMeasurePolicy {
    ...
    private fun getCrossAxisPosition(
        placeable: Placeable,
        parentData: RowColumnParentData?,
        crossAxisLayoutSize: Int,
        beforeCrossAxisAlignmentLine: Int,
        layoutDirection: LayoutDirection,
    ): Int {
        val childCrossAlignment = parentData?.crossAxisAlignment
        return childCrossAlignment?.align(
            size = crossAxisLayoutSize,
            layoutDirection = layoutDirection,
            placeable = placeable,
            beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
        ) ?: horizontalAlignment.align(placeable.width, crossAxisLayoutSize, layoutDirection)
    }
    ...
}</code></pre>
<p>자신의 measurePolicy에서 해당 데이터가 자신이 사용하는 align인지를 확인하며 사용하게 된다.</p>
<h3 id="마무리">마무리</h3>
<p>Modifier의 이것저것을 알아보았다.
Compose로 개발하다보면 Modifier를 정말 많이 사용한다.
이걸로 조금은 친근해진 것 같긴한데 늘 어렵다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] remember는 어떻게 값을 기억할까?]]></title>
            <link>https://velog.io/@couch_potato/Android-remember%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%92%EC%9D%84-%EA%B8%B0%EC%96%B5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@couch_potato/Android-remember%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%92%EC%9D%84-%EA%B8%B0%EC%96%B5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 28 Jan 2026 06:37:35 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>아직 이전 글들도 마무리하지 못하고 벌써 3번째 글이 나오고 있다...
이번주 내로 무조건 마무리해야겠다..</p>
<p>이번에 알아볼 것은 remember이다.
remember는 Jetpack Compose에서 상태를 기억하기 위해 사용되는 API로 Recomposition에서 상태값을 기억하지만 configure Change에서는 값을 유지하지 못한다.</p>
<p>그렇다면 어째서 remember는 recomposition에서 값을 유지하고 configure change에서는 기억하지 못하는 걸까?
내부 코드를 분석해보자.</p>
<h2 id="remember">remember</h2>
<p>remember의 내부는 다음과 같다.</p>
<pre><code class="language-kotlin">@Composable
public inline fun &lt;T&gt; remember(crossinline calculation: @DisallowComposableCalls () -&gt; T): T =
    currentComposer.cache(false, calculation)

@Composable
public inline fun &lt;T&gt; remember(
    key1: Any?,
    crossinline calculation: @DisallowComposableCalls () -&gt; T,
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}</code></pre>
<p>key가 여러 개인 remember의 경우 key 파라미터만 추가되는 구조고 각 key마다 <code>currentComposer.changed(key)</code> 연산을 해준다.</p>
<p>이 함수에 대해 주석은 다음과 같이 작성되어 있다.</p>
<blockquote>
<p>Remember the value returned by calculation if key1 compares equal (==) to the value it had in the previous composition, otherwise produce and remember a new value by calling calculation</p>
</blockquote>
<p>만약 key 값이 이전과 동일하다면 이전 값을 기억하고 그렇지 않다면 calculation을 수행하여 새로운 값을 생성하고 기억한다.</p>
<p>key 값이 없다면 재연산을 안한다.</p>
<p>그렇다면 일단 값의 변경 여부를 판별하는 <code>currentComposer.changed()</code>를 확인해보자.</p>
<h2 id="currentcomposerchanged">currentComposer.changed()</h2>
<pre><code class="language-kotlin">    @ComposeCompilerApi
    override fun changed(value: Any?): Boolean {
        return if (nextSlot() != value) {
            updateValue(value)
            true
        } else {
            false
        }
    }</code></pre>
<p>changed의 내부는 위와 같다.</p>
<h3 id="nextslot">nextSlot()</h3>
<p>여기서 <code>nextSlot()</code>이라는 함수를 key 값과 비교하는데 이 <code>nextSlot()</code>은 다음과 같다.</p>
<pre><code class="language-kotlin">internal fun nextSlot(): Any? =
    if (inserting) {
        validateNodeNotExpected()
        Composer.Empty
    } else
        reader.next().let {
            if (reusing &amp;&amp; it !is ReusableRememberObserverHolder) Composer.Empty else it
        }</code></pre>
<p>한 줄씩 설명하면</p>
<ul>
<li><code>inserting</code> : 현재 트리에 노드를 삽입하는 작업을 예약 중인 경우, 첫 트리 구성에서는 항상 참이다.</li>
<li><code>Composer.Empty</code> : 비교 대상이 없다는 의미로 Empty 반환</li>
<li><code>reader.next()</code> : 슬롯 테이블의 reader를 사용해 슬롯 테이블의 현재 값을 가져오고 reader의 인덱스를 +1, 그룹의 끝이면 Empty 반환</li>
<li><code>if (reusing &amp;&amp; it !is ReusableRememberObserverHolder)</code> : 노드가 재사용중이라면 Empty 그렇지 않다면 원래 값 반환</li>
</ul>
<h4 id="노드-재사용이란">노드 재사용이란?</h4>
<p>Compose는 UI를 그릴 때 Slot Table에 UI 구조를 저장한다.</p>
<p><code>reusing == true</code>라는 것은 <strong>&quot;UI의 껍데기(Node)는 그대로 두고, 그 안에 들어가는 데이터(State)만 싹 갈아 끼우겠다&quot;</strong>는 의미</p>
<p>보통 다음과 같은 상황에서 발생:</p>
<ul>
<li>Lazy Layout (리스트): 스크롤 시 화면에서 사라진 리스트 아이템 노드를 새로 나타나는 아이템을 위해 재사용할 때</li>
<li>MovableContent: UI 요소가 트리 상의 한 위치에서 다른 위치로 이동할 때 (예: 화면 가로/세로 전환 시 공유 요소 이동)</li>
</ul>
<p>이렇게 슬롯 테이블에서 저장된 값을 가져오고 <code>changed()</code>에서 <code>if (nextSlot() != value)</code>로 key 값과 비교한다.</p>
<p>만약 같다면 false를 그렇지 않고 다르다면 <code>updateValue()</code>로 값을 갱신후 true를 반환한다.</p>
<p>그럼 <code>updateValue()</code>를 살펴보자</p>
<h3 id="updatevalue">updateValue()</h3>
<pre><code class="language-kotlin">internal fun updateValue(value: Any?) {
    if (inserting) {
        writer.update(value)
    } else {
        if (reader.hadNext) {
            val groupSlotIndex = reader.groupSlotIndex - 1

            if (changeListWriter.pastParent) { 
                changeListWriter.updateAnchoredValue(value, reader.anchor(reader.parent), groupSlotIndex)
            } else {
                changeListWriter.updateValue(value, groupSlotIndex)
            }
        } else {
            changeListWriter.appendValue(reader.anchor(reader.parent), value)
        }
    }
}</code></pre>
<p>이것도 하나씩 설명하자면</p>
<ol>
<li>inserting으로 처음 그리는 중인지 확인 후 값 갱신</li>
<li>그렇지 않다면 <code>reader.hadNext</code>로 이미 값이 있는 상태에서 수정하는지 확인(hadNext는 이전 <code>next()</code>의 결과 Boolean을 저장하는 값)</li>
<li><code>val groupSlotIndex = reader.groupSlotIndex - 1</code>, -1을 하는 이유는 <code>nextSlot()</code>시 인덱스가 +1 되었기에 방금 읽었던 자리를 수정하기 위함</li>
<li><code>if (changeListWriter.pastParent)</code>로 이미 부모 그룹을 지나쳐버렸다면 앵커를 사용하여 수정, 그렇지 않다면 방금 위치로 수정</li>
<li>hadNext에서 false 즉 읽을 것이 없었다면 append</li>
</ol>
<p>이렇게 값을 가져오고 갱신하는 것까지 완료했다.
그렇다면 이제 다시 remember로 돌아가 <code>currentComposer.cache()</code> 연산을 수행해줄 차례이다.</p>
<h2 id="currentcomposercache">currentComposer.cache</h2>
<pre><code class="language-kotlin">@ComposeCompilerApi
public inline fun &lt;T&gt; Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -&gt; T): T {
    @Suppress(&quot;UNCHECKED_CAST&quot;)
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()
            updateRememberedValue(value)
            value
        } else it
    } as T
}</code></pre>
<p>위와 같이 되어있고 <code>rememberValue()</code>라는 함수로 값을 가져온다.
이 함수는 이름 그대로 기억하고 있는 값을 가져오는 것이다.</p>
<p>구현은 다음과 같이 되어있다.</p>
<pre><code class="language-kotlin">override fun rememberedValue(): Any? = nextSlotForCache()

@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun nextSlotForCache(): Any? {
    return if (inserting) {
        validateNodeNotExpected()
        Composer.Empty
    } else
        reader.next().let {
            if (reusing &amp;&amp; it !is ReusableRememberObserverHolder) Composer.Empty
            else if (it is RememberObserverHolder) it.wrapped else it
        }
}</code></pre>
<p>위에서 본 <code>nextSlot()</code>과 거의 유사하다. 
<code>if(it is RememberObserverHolder)</code>로 해당 값이 RememberObserver 인터페이스를 구현한 객체(예: DisposableEffect, LaunchedEffect 등)를 감싸고 있는 래퍼(Wrapper)인지 확인한다.</p>
<p>이렇게 기억하고 있는 값을 가져온 후 이전에 검사한 <code>invalid</code>와 해당 값이 Empty인지를 확인하고 해당 값을 <code>block</code>으로 계산한 값을 <code>updateRememberedValue()</code>로 갱신 후 반환한다.</p>
<h3 id="마무리">마무리</h3>
<p>즉, 모든 기억과 갱신은 Composer와 내부의 슬롯 테이블에 의존하고 있는 것을 확인할 수 있다.</p>
<p>그렇기 때문에 Configure Change시 해당 Activity와 연결된 Composition, 슬롯 테이블이 모두 날라가기 때문에 값을 기억하지 못한다.</p>
<p>다음에는 rememberSaveable을 분석해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coil은 어떻게 이미지를 가져올까]]></title>
            <link>https://velog.io/@couch_potato/Android-Coil%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EA%B0%80%EC%A0%B8%EC%98%AC%EA%B9%8C</link>
            <guid>https://velog.io/@couch_potato/Android-Coil%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EA%B0%80%EC%A0%B8%EC%98%AC%EA%B9%8C</guid>
            <pubDate>Wed, 14 Jan 2026 10:53:21 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<img src = "https://coil-kt.github.io/coil/logo.svg" >
Manifest Android Interview를 읽고 네트워크에서 이미지를 가져오는 라이브러리가 구체적으로 어떻게 이미지를 가져오는지 궁금하여 한번 까보려고 한다.

<p>그중 개인적으로 많이 사용하는 Coil을 까볼 것이다.</p>
<h3 id="coil의-asyncimage">Coil의 AsyncImage</h3>
<p>Coil에서도 가장 기본적인 AsyncImage를 분석해보려 한다.
AsyncImage는 다음과 같이 작성되어 있다.</p>
<pre><code class="language-kotlin">@Composable
@NonRestartableComposable
fun AsyncImage(
    model: Any?,
    contentDescription: String?,
    imageLoader: ImageLoader,
    modifier: Modifier = Modifier,
    placeholder: Painter? = null,
    error: Painter? = null,
    fallback: Painter? = error,
    onLoading: ((State.Loading) -&gt; Unit)? = null,
    onSuccess: ((State.Success) -&gt; Unit)? = null,
    onError: ((State.Error) -&gt; Unit)? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    filterQuality: FilterQuality = DefaultFilterQuality,
    clipToBounds: Boolean = true,
) = AsyncImage(
    state = AsyncImageState(model, imageLoader),
    contentDescription = contentDescription,
    modifier = modifier,
    transform = transformOf(placeholder, error, fallback),
    onState = onStateOf(onLoading, onSuccess, onError),
    alignment = alignment,
    contentScale = contentScale,
    alpha = alpha,
    colorFilter = colorFilter,
    filterQuality = filterQuality,
    clipToBounds = clipToBounds,
)</code></pre>
<p>으로 되어있고 이걸 한번 더 들어가면</p>
<pre><code class="language-kotlin">@Composable
private fun AsyncImage(
    state: AsyncImageState,
    contentDescription: String?,
    modifier: Modifier,
    transform: (State) -&gt; State,
    onState: ((State) -&gt; Unit)?,
    alignment: Alignment,
    contentScale: ContentScale,
    alpha: Float,
    colorFilter: ColorFilter?,
    filterQuality: FilterQuality,
    clipToBounds: Boolean,
) {
    val request = requestOfWithSizeResolver(
        model = state.model,
        contentScale = contentScale,
    )
    validateRequest(request)

    Layout(
        modifier = modifier.then(
            ContentPainterElement(
                request = request,
                imageLoader = state.imageLoader,
                modelEqualityDelegate = state.modelEqualityDelegate,
                transform = transform,
                onState = onState,
                contentScale = contentScale,
                filterQuality = filterQuality,
                alignment = alignment,
                alpha = alpha,
                colorFilter = colorFilter,
                clipToBounds = clipToBounds,
                previewHandler = previewHandler(),
                contentDescription = contentDescription,
            ),
        ),
        measurePolicy = UseMinConstraintsMeasurePolicy,
    )
}</code></pre>
<p>위와 같이 되어있다.
그중 url주소를 넣는 <code>model</code>은 <code>requestOfWithSizeResolver</code>의 파라미터로 사용된다.
그럼 이 <code>requestOfWithSizeResolver</code>를 살펴보자</p>
<h3 id="requestofwithsizeresolver">requestOfWithSizeResolver</h3>
<pre><code class="language-kotlin">/** Create an [ImageRequest] with a not-null [SizeResolver] from the [model]. */
@Composable
@NonRestartableComposable
internal fun requestOfWithSizeResolver(
    model: Any?,
    contentScale: ContentScale,
): ImageRequest {
    if (model is ImageRequest) {
        if (model.defined.sizeResolver != null) {
            return model
        } else {
            val sizeResolver = rememberSizeResolver(contentScale)
            return remember(model, sizeResolver) {
                model.newBuilder()
                    .size(sizeResolver)
                    .build()
            }
        }
    } else {
        val context = LocalPlatformContext.current
        val sizeResolver = rememberSizeResolver(contentScale)
        return remember(context, model, sizeResolver) {
            ImageRequest.Builder(context)
                .data(model)
                .size(sizeResolver)
                .build()
        }
    }
}</code></pre>
<p>주석은 이 함수를 다음과 같이 설명한다.</p>
<blockquote>
<p>Create an ImageRequest with a not-null SizeResolver from the model.
-&gt; model에서 null이 아닌 SizeResolver를 사용하여 ImageRequest를 생성한다.</p>
</blockquote>
<p>코드를 살펴보면 </p>
<ol>
<li><p>model의 타입이 ImageRequest일때  sizeResolver가 null이 아니면 바로 model을 null이면 contentScale을 파라미터로 받는 sizeResolver를 만들어 다시 ImageRequest를 빌드한다.   </p>
</li>
<li><p>model의 타입이 ImageRequest이 아니라면 data를 model로 갖는 새로운 ImageRequest를 생성한다.</p>
</li>
</ol>
<p>rememberSizeResolver는 내부적으로 아래와 같이 생겼는데 contentScale이 None인지 아닌지 여부를 판별하여 SizeResolver를 생성하는 함수이다.</p>
<pre><code class="language-kotlin">@Composable
private fun rememberSizeResolver(contentScale: ContentScale): SizeResolver {
    val isNone = contentScale == ContentScale.None
    return remember(isNone) {
        if (isNone) {
            SizeResolver.ORIGINAL
        } else {
            ConstraintsSizeResolver()
        }
    }
}</code></pre>
<p>이렇게 생성된 ImgaeRequest를 <code>validateRequest(request)</code>로 유효성 검사를 해준다.</p>
<pre><code class="language-kotlin">internal fun validateRequest(request: ImageRequest) {
    when (request.data) {
        is ImageRequest.Builder -&gt; unsupportedData(
            name = &quot;ImageRequest.Builder&quot;,
            description = &quot;Did you forget to call ImageRequest.Builder.build()?&quot;,
        )
        is ImageBitmap -&gt; unsupportedData(&quot;ImageBitmap&quot;)
        is ImageVector -&gt; unsupportedData(&quot;ImageVector&quot;)
        is Painter -&gt; unsupportedData(&quot;Painter&quot;)
    }
    validateRequestProperties(request)
}

private fun unsupportedData(
    name: String,
    description: String = &quot;If you wish to display this $name, use androidx.compose.foundation.Image.&quot;,
): Nothing = throw IllegalArgumentException(&quot;Unsupported type: $name. $description&quot;)

/** Validate platform-specific properties of an [ImageRequest]. */
internal expect fun validateRequestProperties(request: ImageRequest)</code></pre>
<p>validateRequestProperties는 android기준 다음과 같이 구현되어 있다.</p>
<pre><code class="language-kotlin">internal actual fun validateRequestProperties(request: ImageRequest) {
    require(request.target == null) { &quot;request.target must be null.&quot; }
    require(request.lifecycle == null) { &quot;request.lifecycle must be null.&quot; }
}</code></pre>
<p>이렇게 유효성 검증까지 끝난 ImageRequest를 사용해서 ContentPainterElement라는 Coil의 커스텀 Modifier로 이미지를 그리게 된다.</p>
<h3 id="contentpainterelement">ContentPainterElement</h3>
<pre><code class="language-kotlin">/**
 * A custom [paint] modifier used by [AsyncImage].
 */
internal data class ContentPainterElement(
    private val request: ImageRequest,
    private val imageLoader: ImageLoader,
    private val modelEqualityDelegate: AsyncImageModelEqualityDelegate,
    private val transform: (State) -&gt; State,
    private val onState: ((State) -&gt; Unit)?,
    private val filterQuality: FilterQuality,
    private val alignment: Alignment,
    private val contentScale: ContentScale,
    private val alpha: Float,
    private val colorFilter: ColorFilter?,
    private val clipToBounds: Boolean,
    private val previewHandler: AsyncImagePreviewHandler?,
    private val contentDescription: String?,
) : ModifierNodeElement&lt;ContentPainterNode&gt;() {

    override fun create(): ContentPainterNode {
        val input = Input(imageLoader, request, modelEqualityDelegate)

        // Create the painter during modifier creation so we reuse the same painter object when the
        // modifier is being reused as part of the lazy layouts reuse flow.
        val painter = AsyncImagePainter(input)
        painter.transform = transform
        painter.onState = onState
        painter.contentScale = contentScale
        painter.filterQuality = filterQuality
        painter.previewHandler = previewHandler
        painter._input = input

        return ContentPainterNode(
            painter = painter,
            constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver,
            alignment = alignment,
            contentScale = contentScale,
            alpha = alpha,
            colorFilter = colorFilter,
            clipToBounds = clipToBounds,
            contentDescription = contentDescription,
        )
    }

    override fun update(node: ContentPainterNode) {
        val previousIntrinsics = node.painter.intrinsicSize
        val previousConstraintSizeResolver = node.constraintSizeResolver
        val input = Input(imageLoader, request, modelEqualityDelegate)
        val painter = node.painter
        painter.transform = transform
        painter.onState = onState
        painter.contentScale = contentScale
        painter.filterQuality = filterQuality
        painter.previewHandler = previewHandler
        painter._input = input

        val intrinsicsChanged = previousIntrinsics != painter.intrinsicSize

        node.alignment = alignment
        node.constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver
        node.contentScale = contentScale
        node.alpha = alpha
        node.colorFilter = colorFilter
        node.clipToBounds = clipToBounds

        if (node.contentDescription != contentDescription) {
            node.contentDescription = contentDescription
            node.invalidateSemantics()
        }

        val constraintSizeResolverChanged =
            previousConstraintSizeResolver != node.constraintSizeResolver

        // Only remeasure if intrinsics have changed.
        if (intrinsicsChanged || constraintSizeResolverChanged) {
            node.invalidateMeasurement()
        }

        // Redraw because one of the node properties has changed.
        node.invalidateDraw()
    }

    override fun InspectorInfo.inspectableProperties() {
        name = &quot;content&quot;
        properties[&quot;request&quot;] = request
        properties[&quot;imageLoader&quot;] = imageLoader
        properties[&quot;modelEqualityDelegate&quot;] = modelEqualityDelegate
        properties[&quot;transform&quot;] = transform
        properties[&quot;onState&quot;] = onState
        properties[&quot;filterQuality&quot;] = filterQuality
        properties[&quot;alignment&quot;] = alignment
        properties[&quot;contentScale&quot;] = contentScale
        properties[&quot;alpha&quot;] = alpha
        properties[&quot;colorFilter&quot;] = colorFilter
        properties[&quot;clipToBounds&quot;] = clipToBounds
        properties[&quot;previewHandler&quot;] = previewHandler
        properties[&quot;contentDescription&quot;] = contentDescription
    }
}</code></pre>
<p>여기서 Compose의 UI 트리를 조금 알아보자
Compose는 다음과 같은 단계로 UI를 그린다.
<img src = "https://developer.android.com/static/develop/ui/compose/images/compose-phases.png?hl=ko"></p>
<p>Composition 단계에서 Compose 런타임은 컴포저블 함수를 실행하고 UI를 나타내는 트리 구조를 구성하는데 이때의 노드가 Layout 단계에서 사용되는 정보를 담고 있는 Layout 노드이다.</p>
<img src = "https://developer.android.com/develop/ui/compose/images/composition-screenshot.png">

<p>androidx.compose.ui.node.NodeChain.kt 를 살펴보면 아래와 같은 코드가 있다.</p>
<pre><code class="language-kotlin">...
else if (layoutNode.applyingModifierOnAttach &amp;&amp; beforeSize == 0) {
            // common case where we are initializing the chain and the previous size is zero. In
            // this case we just do all inserts. Since this is so common, we add a fast path here
            // for this condition. Since the layout node is currently attaching, the inserted nodes
            // will not get eagerly attached, which means we can avoid dealing with the coordinator
            // sync until the end, which keeps this code path much simpler.
            coordinatorSyncNeeded = true
            var node = paddedHead
            while (i &lt; after.size) {
                val next = after[i]
                val parent = node
                node = createAndInsertNodeAsChild(next, parent)
                logger?.nodeInserted(0, i, next, parent, node)
                i++
            }
            syncAggregateChildKindSet()
            ...</code></pre>
<p>모든 Modifier는 이 NodeChain에 등록되는데 이 과정에서 Layout 노드가 처음 트리에 attach 될때 <code>createAndInsertNodeAsChild()</code> 라는 함수를 호출한다.</p>
<p>이 함수는 다음과 같다.</p>
<pre><code class="language-kotlin">private fun createAndInsertNodeAsChild(
    element: Modifier.Element,
    parent: Modifier.Node,
): Modifier.Node {
    val node =
        when (element) {
            is ModifierNodeElement&lt;*&gt; -&gt;
                element.create().also {
                    it.kindSet = calculateNodeKindSetFromIncludingDelegates(it)
                }
            else -&gt; BackwardsCompatNode(element)
        }
    checkPrecondition(!node.isAttached) {
        &quot;A ModifierNodeElement cannot return an already attached node from create() &quot;
    }
    node.insertedNodeAwaitingAttachForInvalidation = true
    return insertChild(node, parent)
}</code></pre>
<p>여기서 바로 <code>ModifierNodeElement</code>의 <code>create()</code>함수를 호출한다.</p>
<p>이제 다시 ContentPainterElement로 돌아오면 create 함수가 다음과 같이 되어있고 여기서 painter가 실제 이미지를 그리는 객체이다.</p>
<pre><code class="language-kotlin">override fun create(): ContentPainterNode {
    val input = Input(imageLoader, request, modelEqualityDelegate)

    // Create the painter during modifier creation so we reuse the same painter object when the
    // modifier is being reused as part of the lazy layouts reuse flow.
    val painter = AsyncImagePainter(input)
    painter.transform = transform
    painter.onState = onState
    painter.contentScale = contentScale
    painter.filterQuality = filterQuality
    painter.previewHandler = previewHandler
    painter._input = input

    return ContentPainterNode(
        painter = painter,
        constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver,
        alignment = alignment,
        contentScale = contentScale,
        alpha = alpha,
        colorFilter = colorFilter,
        clipToBounds = clipToBounds,
        contentDescription = contentDescription,
    )
}</code></pre>
<p>그럼 이 AsyncImagePainter가 이미지를 어떻게 그리는지 살펴보자</p>
<pre><code class="language-kotlin">/**
 * A [Painter] that that executes an [ImageRequest] asynchronously and renders the [ImageResult].
 */
@Stable
class AsyncImagePainter internal constructor(
    input: Input,
) : Painter(), RememberObserver {
    private var painter: Painter? by mutableStateOf(null)
    private var alpha: Float = DefaultAlpha
    private var colorFilter: ColorFilter? = null

    private var isRemembered = false
    private var rememberJob: Job? = null
        set(value) {
            field?.cancel()
            field = value
        }

    private var drawSizeFlow: MutableSharedFlow&lt;Size&gt;? = null
    private var drawSize = Size.Unspecified
        set(value) {
            if (field != value) {
                field = value
                drawSizeFlow?.tryEmit(value)
            }
        }

    internal lateinit var scope: CoroutineScope
    internal var transform = DefaultTransform
    internal var onState: ((State) -&gt; Unit)? = null
    internal var contentScale = ContentScale.Fit
    internal var filterQuality = DefaultFilterQuality
    internal var previewHandler: AsyncImagePreviewHandler? = null

    internal var _input: Input? = input
        set(value) {
            if (field != value) {
                field = value
                restart()
                if (value != null) {
                    inputFlow.value = value
                }
            }
        }

    private val inputFlow: MutableStateFlow&lt;Input&gt; = MutableStateFlow(input)
    val input: StateFlow&lt;Input&gt; = inputFlow.asStateFlow()

    private val stateFlow: MutableStateFlow&lt;State&gt; = MutableStateFlow(State.Empty)
    val state: StateFlow&lt;State&gt; = stateFlow.asStateFlow()

    override val intrinsicSize: Size
        get() = painter?.intrinsicSize ?: Size.Unspecified

    override fun DrawScope.onDraw() {
        drawSize = size
        painter?.apply { draw(size, alpha, colorFilter) }
    }

    override fun applyAlpha(alpha: Float): Boolean {
        this.alpha = alpha
        return true
    }

    override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
        this.colorFilter = colorFilter
        return true
    }

    override fun onRemembered() = trace(&quot;AsyncImagePainter.onRemembered&quot;) {
        (painter as? RememberObserver)?.onRemembered()
        launchJob()
        isRemembered = true
    }

    private fun launchJob() {
        val input = _input ?: return

        rememberJob = scope.launchWithDeferredDispatch {
            val previewHandler = previewHandler
            val state = if (previewHandler != null) {
                // If we&#39;re in inspection mode use the preview renderer.
                val request = updateRequest(input.request, isPreview = true)
                previewHandler.handle(input.imageLoader, request)
            } else {
                // Else, execute the request as normal.
                val request = updateRequest(input.request, isPreview = false)
                input.imageLoader.execute(request).toState()
            }
            updateState(state)
        }
    }

    override fun onForgotten() {
        rememberJob = null
        (painter as? RememberObserver)?.onForgotten()
        isRemembered = false
    }

    override fun onAbandoned() {
        rememberJob = null
        (painter as? RememberObserver)?.onAbandoned()
        isRemembered = false
    }

    /**
     * Launch a new image request with the current [Input]s.
     */
    fun restart() {
        if (_input == null) {
            rememberJob = null
        } else if (isRemembered) {
            launchJob()
        }
    }

    /**
     * Update the [request] to work with [AsyncImagePainter].
     */
    private fun updateRequest(request: ImageRequest, isPreview: Boolean): ImageRequest {
        // Connect the size resolver to the draw scope if necessary.
        val sizeResolver = request.sizeResolver
        if (sizeResolver is DrawScopeSizeResolver) {
            sizeResolver.connect(lazyDrawSizeFlow())
        }

        return request.newBuilder()
            .target(
                onStart = { placeholder -&gt;
                    val painter = placeholder?.asPainter(request.context, filterQuality)
                    updateState(State.Loading(painter))
                },
            )
            .apply {
                if (request.defined.sizeResolver == null) {
                    // If the size resolver isn&#39;t set, use the original size.
                    size(SizeResolver.ORIGINAL)
                }
                if (request.defined.scale == null) {
                    // If the scale isn&#39;t set, use the content scale.
                    scale(contentScale.toScale())
                }
                if (request.defined.precision == null) {
                    // AsyncImagePainter scales the image to fit the canvas size at draw time.
                    precision(Precision.INEXACT)
                }
                if (isPreview) {
                    // The request must be executed synchronously in the preview environment.
                    coroutineContext(EmptyCoroutineContext)
                }
            }
            .build()
    }

    private fun updateState(state: State) {
        val previous = stateFlow.value
        val current = transform(state)
        stateFlow.value = current
        painter = maybeNewCrossfadePainter(previous, current, contentScale) ?: current.painter

        // Manually forget and remember the old/new painters.
        if (previous.painter !== current.painter) {
            (previous.painter as? RememberObserver)?.onForgotten()
            (current.painter as? RememberObserver)?.onRemembered()
        }

        // Notify the state listener.
        onState?.invoke(current)
    }

    private fun ImageResult.toState() = when (this) {
        is SuccessResult -&gt; State.Success(
            painter = image.asPainter(request.context, filterQuality),
            result = this,
        )
        is ErrorResult -&gt; State.Error(
            painter = image?.asPainter(request.context, filterQuality),
            result = this,
        )
    }

    private fun lazyDrawSizeFlow(): Flow&lt;Size&gt; {
        var drawSizeFlow = drawSizeFlow
        if (drawSizeFlow == null) {
            drawSizeFlow = MutableSharedFlow(
                replay = 1,
                onBufferOverflow = DROP_OLDEST,
            )
            val drawSize = drawSize
            if (drawSize.isSpecified) {
                drawSizeFlow.tryEmit(drawSize)
            }
            this.drawSizeFlow = drawSizeFlow
        }
        return drawSizeFlow
    }

    /**
     * The latest arguments passed to [AsyncImagePainter].
     */
    @Poko
    class Input(
        val imageLoader: ImageLoader,
        val request: ImageRequest,
        val modelEqualityDelegate: AsyncImageModelEqualityDelegate,
    ) {

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            return other is Input &amp;&amp;
                imageLoader == other.imageLoader &amp;&amp;
                modelEqualityDelegate == other.modelEqualityDelegate &amp;&amp;
                modelEqualityDelegate.equals(request, other.request)
        }

        override fun hashCode(): Int {
            var result = imageLoader.hashCode()
            result = 31 * result + modelEqualityDelegate.hashCode()
            result = 31 * result + modelEqualityDelegate.hashCode(request)
            return result
        }
    }

    /**
     * The current state of the [AsyncImagePainter].
     */
    sealed interface State {

        /** The current painter being drawn by [AsyncImagePainter]. */
        val painter: Painter?

        /** The request has not been started. */
        data object Empty : State {
            override val painter: Painter? get() = null
        }

        /** The request is in-progress. */
        data class Loading(
            override val painter: Painter?,
        ) : State

        /** The request was successful. */
        data class Success(
            override val painter: Painter,
            val result: SuccessResult,
        ) : State

        /** The request failed due to [ErrorResult.throwable]. */
        data class Error(
            override val painter: Painter?,
            val result: ErrorResult,
        ) : State
    }

    companion object {
        /**
         * A state transform that does not modify the state.
         */
        val DefaultTransform: (State) -&gt; State = { it }
    }
}</code></pre>
<p>이 글에서 알아볼 것은 어떻게 네트워크에서 가져오고 그리는가이다.</p>
<p>다른 부분은 배제하고 <code>launchedJob()</code> 함수를 살펴보자</p>
<pre><code class="language-kotlin">private fun launchJob() {
    val input = _input ?: return

    rememberJob = scope.launchWithDeferredDispatch {
        val previewHandler = previewHandler
        val state = if (previewHandler != null) {
            // If we&#39;re in inspection mode use the preview renderer.
            val request = updateRequest(input.request, isPreview = true)
            previewHandler.handle(input.imageLoader, request)
        } else {
            // Else, execute the request as normal.
            val request = updateRequest(input.request, isPreview = false)
            input.imageLoader.execute(request).toState()
        }
        updateState(state)
    }
</code></pre>
<p>또한 preview가 아닌 실제에서 어떻게 가져오는가가 목적이기에 바로 imageLoader의 excute 함수를 확인하자.</p>
<h3 id="imageloader">ImageLoader</h3>
<p>ImageLoader의 실제 구현체는 RealIamgeLoader이다.
RealImageLoader의 코드는 다음과 같은데 전체 코드중 살펴볼 코드만 일부 작성한 것이다.</p>
<pre><code class="language-kotlin">internal class RealImageLoader(
    val options: Options,
) : ImageLoader {
    ...
    override val components = options.componentRegistry.newBuilder()
        .addServiceLoaderComponents(options)
        .addAndroidComponents(options)
        .addJvmComponents(options)
        .addAppleComponents(options)
        .addCommonComponents()
        .add(EngineInterceptor(this, systemCallbacks, requestService, options.logger))
        .build()
    ...
    override suspend fun execute(request: ImageRequest): ImageResult {
        if (!needsExecuteOnMainDispatcher(request)) {
            // Fast path: skip dispatching.
            return execute(request, REQUEST_TYPE_EXECUTE)
        } else {
            // Slow path: dispatch to the main thread.
            return coroutineScope {
                // Start executing the request on the main thread.
                val job = async(options.mainCoroutineContextLazy.value) {
                    execute(request, REQUEST_TYPE_EXECUTE)
                }

                // Update the current request attached to the view and await the result.
                getDisposable(request, job).job.await()
            }
        }
    }

    private suspend fun execute(initialRequest: ImageRequest, type: Int): ImageResult {
        // Wrap the request to manage its lifecycle.
        val requestDelegate = requestService.requestDelegate(
            request = initialRequest,
            job = coroutineContext.job,
            findLifecycle = type == REQUEST_TYPE_ENQUEUE,
        ).apply { assertActive() }

        // Apply this image loader&#39;s defaults and other configuration to this request.
        val request = requestService.updateRequest(initialRequest)

        // Create a new event listener.
        val eventListener = options.eventListenerFactory.create(request)

        try {
            // Fail before starting if data is null.
            if (request.data == NullRequestData) {
                throw NullRequestDataException()
            }

            // Set up the request&#39;s lifecycle observers.
            requestDelegate.start()

            // Enqueued requests suspend until the lifecycle is started.
            if (type == REQUEST_TYPE_ENQUEUE) {
                requestDelegate.awaitStarted()
            }

            // Set the placeholder on the target.
            val cachedPlaceholder = request.placeholderMemoryCacheKey?.let { memoryCache?.get(it)?.image }
            request.target?.onStart(placeholder = cachedPlaceholder ?: request.placeholder())
            eventListener.onStart(request)
            request.listener?.onStart(request)

            // Resolve the size.
            val sizeResolver = request.sizeResolver
            eventListener.resolveSizeStart(request, sizeResolver)
            val size = sizeResolver.size()
            eventListener.resolveSizeEnd(request, size)

            // Execute the interceptor chain.
            val result = withContext(request.interceptorCoroutineContext) {
                RealInterceptorChain(
                    initialRequest = request,
                    interceptors = components.interceptors,
                    index = 0,
                    request = request,
                    size = size,
                    eventListener = eventListener,
                    isPlaceholderCached = cachedPlaceholder != null,
                ).proceed()
            }

            // Set the result on the target.
            when (result) {
                is SuccessResult -&gt; onSuccess(result, request.target, eventListener)
                is ErrorResult -&gt; onError(result, request.target, eventListener)
            }
            return result
        } catch (throwable: Throwable) {
            if (throwable is CancellationException) {
                onCancel(request, eventListener)
                throw throwable
            } else {
                // Create the default error result if there&#39;s an uncaught exception.
                val result = ErrorResult(request, throwable)
                onError(result, request.target, eventListener)
                return result
            }
        } finally {
            requestDelegate.complete()
        }
    }
...</code></pre>
<p>여기에 excute함수가 다음과 같이 구현되어 있다.</p>
<pre><code class="language-kotlin">override suspend fun execute(request: ImageRequest): ImageResult {
    if (!needsExecuteOnMainDispatcher(request)) {
        // Fast path: skip dispatching.
        return execute(request, REQUEST_TYPE_EXECUTE)
    } else {
        // Slow path: dispatch to the main thread.
        return coroutineScope {
            // Start executing the request on the main thread.
            val job = async(options.mainCoroutineContextLazy.value) {
                execute(request, REQUEST_TYPE_EXECUTE)
            }

            // Update the current request attached to the view and await the result.
            getDisposable(request, job).job.await()
        }
    }
}</code></pre>
<p>저 excute 함수 중 살펴볼 부분은 아래와 같다.</p>
<pre><code class="language-kotlin">private suspend fun execute(initialRequest: ImageRequest, type: Int): ImageResult {
    ...

====================================================
        // Execute the interceptor chain.
        val result = withContext(request.interceptorCoroutineContext) {
            RealInterceptorChain(
                initialRequest = request,
                interceptors = components.interceptors,
                index = 0,
                request = request,
                size = size,
                eventListener = eventListener,
                isPlaceholderCached = cachedPlaceholder != null,
            ).proceed()
        }
====================================================
  ...
}</code></pre>
<p>이 result가 최종 결과물로 저 결과물이 AsyncImagePainter에서  AsyncImagePainter.State로 변환되고 이것을 그리는 것이다.</p>
<p>proceed함수는 다음과 같다.</p>
<pre><code class="language-kotlin">override suspend fun proceed(): ImageResult {
        val interceptor = interceptors[index]
        val next = copy(index = index + 1)
        val result = interceptor.intercept(next)
        checkRequest(result.request, interceptor)
        return result
    }</code></pre>
<p>이것만 보면 사실 어떻게 이루어지는지 잘 감이 오지 않는다.</p>
<img src = "https://lh3.googleusercontent.com/gg/AIJ2gl9peHm_CND7P5WogQEZnnitQemcDReqGWjj45ffcR8bu6fURaGbImkWleUUm5Lf_ovKF2gMWsBngJJ6azvp-uETafPY2usngdyruNPOQ20JDYrnkCvNH18EcMltjl4uOGpL8h1Fyrk7-hBZ2JDczIXpiCeJIUX0vbwVPi1SxspxvrEPr704=s1024-rj-mp2" >

<p>위 이미지는 Pluu Dev님의 <a href="https://pluu.github.io/blog/android/2024/09/22/coil-intercept/">Coil 요청 가로채기</a>에서 사용된 이미지를 조금 변형한 것인데 위가 완벽한 내용은 아니지만 대충 이런 느낌으로 이해해주면 좋을 듯 하다.</p>
<p>이 인터셉터들이 순서대로 체크하며 정말 네트워크 통신을 해야하는지 확인 후 네트워크 통신을 하게 된다.</p>
<p>인터셉터는 아래와 같은 코드로 add되었다.</p>
<pre><code class="language-kotlin">override val components = options.componentRegistry.newBuilder()
        .addServiceLoaderComponents(options)
        .addAndroidComponents(options)
        .addJvmComponents(options)
        .addAppleComponents(options)
        .addCommonComponents()
        .add(EngineInterceptor(this, systemCallbacks, requestService, options.logger))
        .build()</code></pre>
<p>그럼 이 EngineInterceptor의 코드를 살펴보자</p>
<h3 id="engineinterceptor">EngineInterceptor</h3>
<p>아래의 코드는 EngineInterceptor에서 이 글에서 살펴볼 부분만 일부 가져온 것이다.</p>
<pre><code class="language-kotlin">/** The last interceptor in the chain which executes the [ImageRequest]. */
internal class EngineInterceptor(
    private val imageLoader: ImageLoader,
    private val systemCallbacks: SystemCallbacks,
    private val requestService: RequestService,
    private val logger: Logger?,
) : Interceptor {
    ...
  private suspend fun fetch(
      components: ComponentRegistry,
      request: ImageRequest,
      mappedData: Any,
      options: Options,
      eventListener: EventListener,
  ): FetchResult {
      val fetchResult: FetchResult
      var searchIndex = 0
      while (true) {
          val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
          checkNotNull(pair) { &quot;Unable to create a fetcher that supports: $mappedData&quot; }
          val fetcher = pair.first
          searchIndex = pair.second + 1

          eventListener.fetchStart(request, fetcher, options)
          val result = fetcher.fetch()
          try {
              eventListener.fetchEnd(request, fetcher, options, result)
          } catch (throwable: Throwable) {
              // Ensure the source is closed if an exception occurs before returning the result.
              (result as? SourceFetchResult)?.source?.closeQuietly()
              throw throwable
          }

          if (result != null) {
              fetchResult = result
              break
          }
      }
      return fetchResult
  }
  ...
}</code></pre>
<p>이 <code>fetch()</code> 함수를 보면 
다음의 단계를 거친다.</p>
<pre><code class="language-kotlin">// 1. 등록된 컴포넌트 중 mappedData(URL 등)를 처리할 수 있는 Fetcher를 찾음
val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
val fetcher = pair.first

// 2. 그 Fetcher에게 데이터를 가져오라고 시킴 (실제 네트워크 통신 지점)
val result = fetcher.fetch()</code></pre>
<p>저 fetcher는 Fetcher 인터페이스이고 이 구현체는 NetworkFetcher이다.</p>
<h2 id="마무리">마무리</h2>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://pluu.github.io/blog/android/2024/09/22/coil-intercept/">https://pluu.github.io/blog/android/2024/09/22/coil-intercept/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] ViewModel은 어떻게 값을 유지할까?]]></title>
            <link>https://velog.io/@couch_potato/Android-ViewModel%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%92%EC%9D%84-%EC%9C%A0%EC%A7%80%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@couch_potato/Android-ViewModel%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%92%EC%9D%84-%EC%9C%A0%EC%A7%80%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 07 Jan 2026 09:25:08 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>ComponentActivity 코드</p>
<pre><code class="language-kotlin">@Suppress(&quot;deprecation&quot;)
final override fun onRetainNonConfigurationInstance(): Any? {
    // Maintain backward compatibility.
    val custom = onRetainCustomNonConfigurationInstance()
    var viewModelStore = _viewModelStore
    if (viewModelStore == null) {
        // No one called getViewModelStore(), so see if there was an existing
        // ViewModelStore from our last NonConfigurationInstance
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            viewModelStore = nc.viewModelStore
        }
    }
    if (viewModelStore == null &amp;&amp; custom == null) {
        return null
    }
    // &#39;NonConfigurationInstances&#39;라는 내부 전용 보관 상자 생성
    val nci = NonConfigurationInstances()
    nci.custom = custom
    nci.viewModelStore = viewModelStore // 여기에 현재의 ViewModel들이 담긴 바구니를 넣음
    return nci // 이 상자를 안드로이드 시스템(OS)에 맡김
}</code></pre>
<p>onPause() -&gt; onStop() -&gt; [이 시점에 호출] -&gt; onDestroy()</p>
<pre><code class="language-java">ActivityThread.java 
void performDestroyActivity(ActivityClientRecord r, boolean finishing, boolean getNonConfigInstance, String reason) {
    Class &lt; ? extends Activity &gt; activityClass;
    if (localLOGV) Slog.v(TAG, &quot;Performing finish of &quot; + r);
    activityClass = r.activity.getClass();
    if (finishing) {
        r.activity.mFinished = true;
    }
    performPauseActivityIfNeeded(r, &quot;destroy&quot;);
    if (!r.stopped) {
        callActivityOnStop(r, false /* saveState */, &quot;destroy&quot;);
    }
    if (getNonConfigInstance) {
        try {
            r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException (&quot;Unable to retain activity &quot;
                    + r.intent.getComponent().toShortString() + &quot;: &quot; + e.toString(), e);
            }
        }
    }
    ...
}</code></pre>
<p>이후 액티비티 재생성시 다음 코드로부터 ViewModelStore를 가져옴</p>
<pre><code class="language-kotlin">ComponentActivity.kt
override val viewModelStore: ViewModelStore
    /**
     * Returns the [ViewModelStore] associated with this activity
     *
     * Overriding this method is no longer supported and this method will be made `final` in a
     * future version of ComponentActivity.
     *
     * @return a [ViewModelStore]
     * @throws IllegalStateException if called before the Activity is attached to the
     *   Application instance i.e., before onCreate()
     */
    get() {
        checkNotNull(application) {
            (&quot;Your activity is not yet attached to the &quot; +
                &quot;Application instance. You can&#39;t request ViewModel before onCreate call.&quot;)
        }
        ensureViewModelStore()
        return _viewModelStore!!
    }

private fun ensureViewModelStore() {
    if (_viewModelStore == null) {
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            _viewModelStore = nc.viewModelStore
        }
        if (_viewModelStore == null) {
            _viewModelStore = ViewModelStore()
        }
    }
}</code></pre>
<p><code>lastNonConfigurationInstance</code>로 파괴되기 전에 저장한 기존의 ViewModelStore를 가지고 있는<code>NonConfigurationInstances</code>를 가져와서 값으로 설정한다.</p>
<p>정리는 완전히 죽을 때만 하고 이것은 ComponentActivity의 init에서 설정</p>
<pre><code class="language-kotlin">init {
    ...
    @Suppress(&quot;LeakingThis&quot;)
    lifecycle.addObserver(
        LifecycleEventObserver { _, event -&gt;
            if (event == Lifecycle.Event.ON_DESTROY) {
                // Clear out the available context
                contextAwareHelper.clearAvailableContext()
                // And clear the ViewModelStore
                if (!isChangingConfigurations) {
                    viewModelStore.clear() &lt;- 여기
                }
                reportFullyDrawnExecutor.activityDestroyed()
            }
        }
    )
    ...
}</code></pre>
<h2 id="마무리">마무리</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] R8의 traceNewInstance() 핥아보기]]></title>
            <link>https://velog.io/@couch_potato/Android-R8%EC%9D%98-trace-%ED%95%A5%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/Android-R8%EC%9D%98-trace-%ED%95%A5%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 10 Nov 2025 08:39:00 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Manifest Android Interview의 R8 스터디용 R8을 조금만 핥아보는 글입니다.</p>
<p>이 글은 <code>traceNewInstance()</code> 메소드 하나만 다루는 아주 짧은 글입니다.</p>
<h2 id="tracenewinstance">traceNewInstance()</h2>
<pre><code class="language-java">private DexClass traceNewInstance(
      DexType type,
      ProgramMethod context,
      InstantiationReason instantiationReason,
      KeepReason keepReason) {
    DexClass clazz = resolveBaseType(type, context);
    if (clazz != null &amp;&amp; clazz.isProgramClass()) {
      DexProgramClass programClass = clazz.asProgramClass();
      if (clazz.isAnnotation() || clazz.isInterface()) {
        markTypeAsLive(programClass, graphReporter.registerClass(programClass, keepReason));
      } else {
        worklist.enqueueMarkInstantiatedAction(
            programClass, context, instantiationReason, keepReason);
      }
    }
    return clazz;
  }</code></pre>
<p>이것은 R8이 <code>new ClassA()</code>와 같은 새로운 인스턴스 생성이 발견되면 호출되는 메소드이다.
한줄씩 무엇인지 살펴보면</p>
<ol>
<li><code>DexClass clazz = resolveBaseType(type, context);</code> <ul>
<li>type이 배열일 수도 있기 때문에 기본 클래스 타입(base type)을 찾아서 DexClass로 반환</li>
</ul>
</li>
<li><code>if (clazz != null &amp;&amp; clazz.isProgramClass())</code><ul>
<li>해당 클래스가 앱 코드인지 확인한다. (라이브러리나 플랫폼 클래스는 제외)</li>
<li>이후 <code>asProgramClass()</code>로 변환</li>
</ul>
</li>
<li><code>if (clazz.isAnnotation() || clazz.isInterface())</code><ul>
<li>어노테이션이나 인터페이스는 new로 직접 인스턴스화할 수 없으니 &quot;이 타입이 코드에서 참조된다&quot;는 의미로 <code>markTypeAsLive()</code>를 통해 live(살아있는 상태)로 표시</li>
</ul>
</li>
<li>` worklist.enqueueMarkInstantiatedAction(<pre><code>     programClass, context, instantiationReason, keepReason);`</code></pre><ul>
<li>그 외에 일반 클래스는 실제 인스턴스 생성이 가능하므로
worklist(작업 큐)에 “이 클래스가 인스턴스화되었다”는 작업을 추가한다. </li>
</ul>
</li>
</ol>
<p>이후 R8의 reachability analysis(도달성 분석) 단계 worklist를 순회하며 사용 중을 판단한다.</p>
<h2 id="결론">결론</h2>
<p>스터디를 위해 빠르게 정신없이 글을 3개나 썼는데
막상 써지는 걸 보면 평소에 내가 얼마나 게을렀나를 반성할 수 있었다..ㅎㅎ;;;
추후 worklist 순회하는 부분도 봐야겠다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] ART의 Zygote를 활용한 콜드 스타트 최적화 핥아보기]]></title>
            <link>https://velog.io/@couch_potato/Android-ART%EC%9D%98-Zygote%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BD%9C%EB%93%9C-%EC%8A%A4%ED%83%80%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%95%A5%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/Android-ART%EC%9D%98-Zygote%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BD%9C%EB%93%9C-%EC%8A%A4%ED%83%80%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%95%A5%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 10 Nov 2025 07:51:48 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>Manifest Android Interview에서 ART를 더 알아보기 위한 글이다.
ART의 코드는 너무나도 방대하여 그중에서 Zygote에 대한 내용만을 아주 살짝 핥아만 볼 것이다.</p>
<p>이 글은 말 그대로 핥아보는 글이라 내부 원리를 너무 깊이 다루진 않는다.</p>
<h1 id="zygote">Zygote</h1>
<p>일단 Zygote가 무엇인가 안드로이드 공식문서에는 다음과 같이 설명한다.</p>
<blockquote>
<p>Zygote는 동일한 애플리케이션 바이너리 인터페이스 (ABI)를 사용하는 모든 시스템 및 앱 프로세스의 루트 역할을 하는 Android 운영체제의 프로세스입니다.</p>
</blockquote>
<p>좀더 설명하자면
Zygote는 모든 안드로이드 앱 프로세스의 부모 역할을 수행한다.
기기가 부팅되면 Zygote는 가상 머신을 초기화하고 필요한 시스템 및 앱 클래스를 메모리에 미리 로딩하여, 애플리케이션이 시작될 때마다 이 과정을 반복하지 않고 재사용함으로써 앱 실행 속도를 단축시킨다.</p>
<p>앱은 이 Zygote를 <code>fork()</code>해서 새로운 프로세스를 만들고 이 fork된 자식 프로세스 안에서 앱의 코드(Dex 파일), Application 클래스, Activity 등이 로드된다.</p>
<h2 id="prezygotefork">PreZygoteFork()</h2>
<p>이것은 fork 직전에 호출되는 함수이다.</p>
<pre><code class="language-cpp">void Runtime::PreZygoteFork() {
  if (GetJit() != nullptr) {
    GetJit()-&gt;PreZygoteFork();
  }
  if (!heap_-&gt;HasZygoteSpace()) {
    Thread* self = Thread::Current();
    // This is the first fork. Update ArtMethods in the boot classpath now to
    // avoid having forked apps dirty the memory.

    // Ensure we call FixupStaticTrampolines on all methods that are
    // initialized.
    class_linker_-&gt;MakeInitializedClassesVisiblyInitialized(self, /*wait=*/ true);

    ScopedObjectAccess soa(self);
    UpdateMethodsPreFirstForkVisitor visitor(class_linker_);
    class_linker_-&gt;VisitClasses(&amp;visitor);
  }
  heap_-&gt;PreZygoteFork();
  PreZygoteForkNativeBridge();
}</code></pre>
<p>복제를 준비하는 코드다.
<code>if (GetJit() != nullptr) { GetJit()-&gt;PreZygoteFork(); }</code>는 
fork전 JIT(Just-In-Time) 컴파일러가 있다면
Zygote fork 전 상태를 정리하고 JIT 캐시를 freeze 상태로 만든다.</p>
<p>이것을 왜 하냐면 Zygote가 fork될 때,
JIT 캐시나 컴파일된 코드 영역이 그대로 복제되면 부모(zygote)와 자식(앱) 간에 메모리 충돌이나 불필요한 Copy-On-Write가 생길 수 있다고 한다.</p>
<p><code>!heap_-&gt;HasZygoteSpace()</code>는 런타임의 힙에 Zygote 전용 공유 메모리 영역(zygote space)이 이미 생성되어 있는지를 반환하는데 false면 초 Zygote 생성(또는 최초 fork 전) 이라는 것을 의미한다.</p>
<p>검사 이유는 최초 한 번만 수행해야 할 초기화를 반복하지 않기 위해서이다.
이후 코드는 다음과 같다.</p>
<ul>
<li><code>Thread* self = Thread::Current();</code>
  →  현재 실행 중인 스레드 핸들을 가져온다</li>
<li><code>class_linker_-&gt;MakeInitializedClassesVisiblyInitialized(self, /*wait=*/ true);</code>
→ 이미 초기화된 클래스들을 완전 초기화된 상태로 다른 스레드에게 보이게(동기화) 만든다.(다른 스레드 중 초기화 진행중이면 대기)</li>
<li><code>ScopedObjectAccess soa(self);</code>
→ 네이티브 코드에서 VM 객체에 안전하게 접근하도록 스코프를 설정한다.</li>
<li><code>UpdateMethodsPreFirstForkVisitor visitor(class_linker_);</code>
→ 각 클래스의 메서드를 미리 업데이트할 방문자(visitor)를 만든다.</li>
<li><code>class_linker_-&gt;VisitClasses(&amp;visitor);</code>
→ 모든 클래스들을 순회하며 visitor로 메서드/트램폴린 등을 확정(초기화)한다.</li>
</ul>
<p>최초 Zygote 초기화 블록 종료</p>
<p><code>heap_-&gt;PreZygoteFork();</code> 는 Runtime의 <code>PreZygoteFork()</code>가 아닌 힙의 fork를 수행하는 것으로 GC heap 내부의 zygote 관련 영역을 준비한다.</p>
<p><code>PreZygoteForkNativeBridge()</code>는 Zygote가 fork되기 전에
CPU 변환 계층(Native Bridge)의 상태를 고정시키는 함수이다.</p>
<h4 id="native-bridge">Native Bridge</h4>
<p>이게 뭔가 하니 GPT는 다음과 같이 설명한다.</p>
<blockquote>
<p>안드로이드에서는앱이 꼭 ARM용 기기에서 ARM 코드만 쓸 필요는 없어.
예를 들어,
기기는 x86 CPU 인데
앱이 ARM용 네이티브 코드(.so 파일) 만 제공하는 경우가 있어.
이럴 때 “Native Bridge” 라는 녀석이
👉 ARM용 코드를 x86에서 돌릴 수 있게 중간에서 변환 해줘.
즉,
“CPU가 못 읽는 네이티브 코드를 대신 번역해서 실행시켜주는 번역기”</p>
</blockquote>
<h2 id="postzygotefork">PostZygoteFork</h2>
<p>이것은 fork 직후 실행되는 함수이다.</p>
<pre><code class="language-cpp">void Runtime::PostZygoteFork() {
  jit::Jit* jit = GetJit();
  if (jit != nullptr) {
    jit-&gt;PostZygoteFork();
    // Ensure that the threads in the JIT pool have been created with the right
    // priority.
    if (kIsDebugBuild &amp;&amp; jit-&gt;GetThreadPool() != nullptr) {
      jit-&gt;GetThreadPool()-&gt;CheckPthreadPriority(
          IsZygote() ? jit-&gt;GetZygoteThreadPoolPthreadPriority()
                     : jit-&gt;GetThreadPoolPthreadPriority());
    }
  }
  // Reset all stats.
  ResetStats(0xFFFFFFFF);
}</code></pre>
<pre><code class="language-cpp">jit::Jit* jit = GetJit();
if (jit != nullptr) {
  jit-&gt;PostZygoteFork();
  ...
}</code></pre>
<p>fork 전 중단했던 JIT을 다시 활성화하고 새로 생성된 앱 프로세스에서 JIT thread pool을 다시 세팅한다.
Zygote 시절의 스레드들은 fork로 복제되면 동작 보장이 안 되기 때문에,
새로 앱 프로세스 전용의 JIT thread pool 을 만드는 것이다.</p>
<pre><code class="language-cpp">if (kIsDebugBuild &amp;&amp; jit-&gt;GetThreadPool() != nullptr) {
      jit-&gt;GetThreadPool()-&gt;CheckPthreadPriority(
          IsZygote() ? jit-&gt;GetZygoteThreadPoolPthreadPriority()
                     : jit-&gt;GetThreadPoolPthreadPriority());
    }</code></pre>
<p>이건 디버그용 코드로 방금 생성된 스레드풀의 스레드들의 우선순위를 점검하는 코드이다.</p>
<p><code>ResetStats(0xFFFFFFFF);</code> 를 통해 최종적으로 ART 내부의 런타임 통계(메서드 호출 수, GC 횟수 등)를 전부 초기화한다.
초기화하는 이유는 Zygote fork시 그 통계 데이터도 그대로 복사되어 자식 앱 프로세스에 남기 때문이라고 한다.</p>
<h2 id="결론">결론</h2>
<p>ART 너무 어렵다!
이건 또 언제 공부해야 하나 싶다..!!!
물론 코드를 뒤집어 깔 필요는 없겠지만 그래도 동작 원리는 알면 좋을 것 같은데...
심지어 이 글에서 다룬 내용도 이해 못했다...ㅎ</p>
<p>내부 코드가 정확히 어떻게 동작하는지까지 알아보는 건 너무도 시간이 오래 걸리고 어려워 추후 ART를 정말 싹싹 긁어먹을 때 해봐야지....</p>
<p>용어</p>
<ul>
<li>트램폴린 : ART 런타임에서 JNI(Java Native Interface) 메서드와 같은 네이티브 코드를 호출하기 위한 내부적인 기술</li>
</ul>
<h4 id="참고">참고</h4>
<p><a href="https://source.android.com/docs/core/runtime/zygote?hl=ko">https://source.android.com/docs/core/runtime/zygote?hl=ko</a>
<a href="https://cs.android.com/android/platform/superproject/+/master:art/runtime/runtime.cc">https://cs.android.com/android/platform/superproject/+/master:art/runtime/runtime.cc</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] DataStore는 데이터를 어떻게 저장할까]]></title>
            <link>https://velog.io/@couch_potato/Andorid-DataStore%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@couch_potato/Andorid-DataStore%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sun, 09 Nov 2025 16:09:16 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>Manifest Android Interview를 읽으며 안드로이드 파일 시스템에 대해 더욱 알아보기 위한 글이다.</p>
<p>구성 요소나 개념에 대한 내용은 책에 이미 잘 나와있으니 내가 알아볼 것은 코드에서 시작해서 실제 파일 시스템에 어떻게 접근하고 처리하는지를 알아볼 것이다.</p>
<p>대표적인 저장 방식인 DataStore를 다루고자 한다.</p>
<h1 id="datastore">DataStore</h1>
<pre><code class="language-kotlin">dataStore.edit { prefs -&gt;
      prefs[ACCESS_TOKEN] = cryptoManager.encrypt(accessToken)
}</code></pre>
<p>흔히 위와 같은 형태로 DataStore를 활용한다.
이 <code>edit</code>은 다음과 같이 작성되어 있다.</p>
<pre><code class="language-kotlin">public suspend fun DataStore&lt;Preferences&gt;.edit(
    transform: suspend (MutablePreferences) -&gt; Unit
): Preferences {
    return this.updateData {
        // It&#39;s safe to return MutablePreferences since we freeze it in
        // PreferencesDataStore.updateData()
        it.toMutablePreferences().apply { transform(this) }
    }
}</code></pre>
<p>그렇다면 이 <code>updateData</code>를 살펴보자</p>
<pre><code class="language-kotlin">public suspend fun updateData(transform: suspend (t: T) -&gt; T): T</code></pre>
<p>Ctrl+B로 추적하면 실제 구현체까지는 나오지 않는다.
실제 구현체는 External Libraries에 있는 AAR 목록들에서 찾아볼 수 있는 androidx.datastore.core.DataStoreImpl에 있다.</p>
<pre><code class="language-kotlin">override suspend fun updateData(transform: suspend (t: T) -&gt; T): T {
        val parentContextElement = coroutineContext[UpdatingDataContextElement.Companion.Key]
        parentContextElement?.checkNotUpdating(this)
        val childContextElement = UpdatingDataContextElement(
            parent = parentContextElement,
            instance = this
        )
        return withContext(childContextElement) {
            val ack = CompletableDeferred&lt;T&gt;()
            val currentDownStreamFlowState = inMemoryCache.currentState
            val updateMsg =
                Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
            writeActor.offer(updateMsg)
            ack.await()
        }
    }</code></pre>
<p>각 코드를 하나씩 살펴보자</p>
<pre><code class="language-kotlin">val parentContextElement = coroutineContext[UpdatingDataContextElement.Companion.Key]
parentContextElement?.checkNotUpdating(this)
val childContextElement = UpdatingDataContextElement(
   parent = parentContextElement,
   instance = this
)</code></pre>
<p>먼저 <code>val parentContextElement = coroutineContext[UpdatingDataContextElement.Companion.Key]</code>
여기서 <code>UpdatingDataContextElement.Companion.Key</code>은 아래와 같다.</p>
<pre><code class="language-kotlin">internal class UpdatingDataContextElement(
    private val parent: UpdatingDataContextElement?,
    private val instance: DataStoreImpl&lt;*&gt;
) : CoroutineContext.Element {

    companion object {
        internal val NESTED_UPDATE_ERROR_MESSAGE = &quot;&quot;&quot;
                Calling updateData inside updateData on the same DataStore instance is not supported
                since updates made in the parent updateData call will not be visible to the nested
                updateData call. See https://issuetracker.google.com/issues/241760537 for details.
            &quot;&quot;&quot;.trimIndent()
        internal object Key : CoroutineContext.Key&lt;UpdatingDataContextElement&gt;
    }</code></pre>
<p>즉 해당 DataStore가 호출된 코루틴에서 UpdatingDataContextElement를 받아온다.
<code>parentContextElement?.checkNotUpdating(this)</code> 
UpdatingDataContextElement가 있는지 확인하고 있다면 지금 해당 코루틴에서 DataStore가 업데이트 중인지 <code>checkNotUpdating</code>를 통해 확인한다.</p>
<p>만약 동일 인스턴스에 대한 중첩 호출(재진입)이 감지되면, 이는 교착 상태(Deadlock)를 유발할 수 있으므로 즉시 예외를 발생시킨다.</p>
<pre><code class="language-kotlin">val childContextElement = UpdatingDataContextElement(
   parent = parentContextElement,
   instance = this
)</code></pre>
<p>이제 자식이 사용할 새로운 UpdatingDataContextElement를 만든다.</p>
<p>처음에는 이게 잘 이해가 안 갔는데 
안드로이드 DataStore에서 예를 들어</p>
<pre><code class="language-kotlin">// 바깥
dataStoreA.updateData {
    // 안쪽
    dataStoreA.updateData { 

        ... 
    }
}</code></pre>
<p>이런식으로 중첩되게 호출되면 데드락이 발생할 수 있지만</p>
<pre><code class="language-kotlin">// 바깥
dataStoreA.updateData {
    // 안쪽
    dataStoreB.updateData { 

        ... 
    }
}</code></pre>
<p>와 같은 경우는 허용되어야 한다.
그렇기에 같은 DataStore에 대하여 <code>parentContextElement?.checkNotUpdating(this)</code>로 작성중인지를 확인하고 다른 DataStore에 대한 중첩호출은 허용되도록 <code>val childContextElement = UpdatingDataContextElement(
   parent = parentContextElement,
   instance = this
)</code>로 부모-자식 관계를 형성해주는 것이다.</p>
<p>마지막으로 </p>
<pre><code class="language-kotlin">val ack = CompletableDeferred&lt;T&gt;()
val currentDownStreamFlowState = inMemoryCache.currentState
val updateMsg =
   Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
writeActor.offer(updateMsg)
ack.await()</code></pre>
<p>이것은 경쟁 상태를 방지하고 모든 업데이트를 순차적으로 처리하기 위한 로직이다.</p>
<ol>
<li><code>val ack = CompletableDeferred&lt;T&gt;()</code><ul>
<li>비동기적으로 수행될 업데이트 작업의 최종 결과를 수신하기 위한 객체를 생성    </li>
</ul>
</li>
<li><code>val currentDownStreamFlowState = inMemoryCache.currentState</code><ul>
<li>불필요한 디스크 읽기를 줄이기 위해 현재 메모리에 캐시된 데이터 상태를 가져옴</li>
</ul>
</li>
<li><code>val updateMsg =
Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)</code><ul>
<li>실제 업데이트를 수행할 Actor에게 전달할 메시지(작업 단위)를 생성</li>
</ul>
</li>
<li><code>writeActor.offer(updateMsg)</code><ul>
<li>생성된 업데이트 메시지를 실제 작업을 처리하는 actor의 메시지 큐에 전송</li>
</ul>
</li>
<li><code>ack.await()</code><ul>
<li>업데이트 작업을 완료하고 결과를 반환할 때까지 대기</li>
</ul>
</li>
</ol>
<p>writeActore는 다음과 같이 정의되어 있다.</p>
<pre><code class="language-kotlin">private val writeActor = SimpleActor&lt;Message.Update&lt;T&gt;&gt;(
   scope = scope,
   onComplete = {
      // We expect it to always be non-null but we will leave the alternative as a no-op
      // just in case.
      it?.let {
         inMemoryCache.tryUpdate(Final(it))
      }
      // don&#39;t try to close storage connection if it was not created in the first place.
      if (storageConnectionDelegate.isInitialized()) {
         storageConnection.close()
      }
   },
   onUndeliveredElement = { msg, ex -&gt;
      msg.ack.completeExceptionally(
         ex ?: CancellationException(
            &quot;DataStore scope was cancelled before updateData could complete&quot;
         )
      )
   }
) { msg -&gt;
   handleUpdate(msg)
}</code></pre>
<p>그중 실제로 메시지를 처리하는 <code>handleUpdate</code>를 살펴보자.</p>
<pre><code class="language-kotlin">private suspend fun handleUpdate(update: Message.Update&lt;T&gt;) {
   update.ack.completeWith(
      runCatching {
         val result: T
         when (val currentState = inMemoryCache.currentState) {
            is Data -&gt; {
               // We are already initialized, we just need to perform the update
               result = transformAndWrite(update.transform, update.callerContext)
            }

            is ReadException, is UnInitialized -&gt; {
               if (currentState === update.lastState) {
                  // we need to try to read again
                  readAndInitOrPropagateAndThrowFailure()

                  // We&#39;ve successfully read, now we need to perform the update
                  result = transformAndWrite(update.transform, update.callerContext)
               } else {
                  // Someone else beat us to read but also failed. We just need to
                  // signal the writer that is waiting on ack.
                  // This cast is safe because we can&#39;t be in the UnInitialized
                  // state if the state has changed.
                  throw (currentState as ReadException).readException
               }
            }

            is Final -&gt; throw currentState.finalException // won&#39;t happen
         }
         result
      }
   )
}</code></pre>
<p>그중 살펴볼 것은 </p>
<pre><code class="language-kotlin">is Data -&gt; {
   result = transformAndWrite(update.transform, update.callerContext)
}

is ReadException, is UnInitialized -&gt; {
   if (currentState === update.lastState) {
      readAndInitOrPropagateAndThrowFailure()
      result = transformAndWrite(update.transform, update.callerContext)
   } else {
      throw (currentState as ReadException).readException
   }
}</code></pre>
<p>각각 DataStore가 이미 초기화되었고, 메모리에 정상적인 데이터를 갖고 있다는 <code>Data</code>와 이 막 시작되어 디스크에서 데이터를 한 번도 읽지 않은 상태인 <code>UnInitialized</code>이다.
둘 모두 <code>result = transformAndWrite(update.transform, update.callerContext)</code>로 최종적으로 update하는 건 같다.</p>
<p><code>readAndInitOrPropagateAndThrowFailure()</code>는 그럼 뭘 할까</p>
<pre><code class="language-kotlin">private suspend fun readAndInitOrPropagateAndThrowFailure() {
   val preReadVersion = coordinator.getVersion()
   try {
      readAndInit.runIfNeeded()
   } catch (throwable: Throwable) {
      inMemoryCache.tryUpdate(ReadException(throwable, preReadVersion))
      throw throwable
   }
}
internal abstract class RunOnce {
    private val runMutex = Mutex()
    private val didRun = CompletableDeferred&lt;Unit&gt;()
    protected abstract suspend fun doRun()

    suspend fun awaitComplete() = didRun.await()

    suspend fun runIfNeeded() {
        if (didRun.isCompleted) return
        runMutex.withLock {
            if (didRun.isCompleted) return
            doRun()
            didRun.complete(Unit)
        }
    }
}
private va readAndInit = InitDataStore(initTasksList)
private inner class InitDataStore(
        initTasksList: List&lt;suspend (api: InitializerApi&lt;T&gt;) -&gt; Unit&gt;
    ) : RunOnce() {
    ...
    override suspend fun doRun() {
            val initData = if ((initTasks == null) || initTasks!!.isEmpty()) {
                // if there are no init tasks, we can directly read
                readDataOrHandleCorruption(hasWriteFileLock = false)
            } else {
                // if there are init tasks, we need to obtain a lock to ensure migrations
                // run as 1 chunk
                coordinator.lock {
                    val updateLock = Mutex()
                    var initializationComplete = false
                    var currentData = readDataOrHandleCorruption(hasWriteFileLock = true).value

                    val api = object : InitializerApi&lt;T&gt; {
                        override suspend fun updateData(transform: suspend (t: T) -&gt; T): T {
                            return updateLock.withLock {
                                check(!initializationComplete) {
                                    &quot;InitializerApi.updateData should not be called after &quot; +
                                        &quot;initialization is complete.&quot;
                                }

                                val newData = transform(currentData)
                                if (newData != currentData) {
                                    writeData(newData, updateCache = false)
                                    currentData = newData
                                }

                                currentData
                            }
                        }
                    }

                    initTasks?.forEach { it(api) }
                    // Init tasks have run successfully, we don&#39;t need them anymore.
                    initTasks = null
                    updateLock.withLock {
                        initializationComplete = true
                    }
                    // only to make compiler happy
                    Data(
                        value = currentData,
                        hashCode = currentData.hashCode(),
                        version = coordinator.getVersion()
                    )
                }
            }
            inMemoryCache.tryUpdate(initData)
        }
}</code></pre>
<ul>
<li><code>RunOnce</code>는 오직 한번만 실행되는 것을 보장하는 도구다   </li>
</ul>
<p>이 <code>doRun</code>이 실행되는데 <code>coordinator.lock</code>은 파일 락(File Lock) 등을 사용하여, 이 앱의 모든 프로세스(여러 앱 인스턴스 포함)를 통틀어 오직 하나의 코루틴만이 이 람다 블록에 진입할 수 있도록 보장한다.</p>
<p>이중 디스크에서 데이터를 읽어오고 만약 파일이 손상되었으면(corrupted) 복구(handle)하는 함수인 <code>readDataOrHandleCorruption</code>를 살펴보자</p>
<pre><code class="language-kotlin">private suspend fun readDataOrHandleCorruption(hasWriteFileLock: Boolean): Data&lt;T&gt; {
        try {
            return if (hasWriteFileLock) {
                val data = readDataFromFileOrDefault()
                Data(data, data.hashCode(), version = coordinator.getVersion())
            } else {
                val preLockVersion = coordinator.getVersion()
                coordinator.tryLock { locked -&gt;
                    val data = readDataFromFileOrDefault()
                    val version = if (locked) coordinator.getVersion() else preLockVersion
                    Data(
                        data,
                        data.hashCode(),
                        version
                    )
                }
            }
        } catch (ex: CorruptionException) {
            var newData: T = corruptionHandler.handleCorruption(ex)
            var version: Int // initialized inside the try block

            try {
                doWithWriteFileLock(hasWriteFileLock) {
                    // Confirms the file is still corrupted before overriding
                    try {
                        newData = readDataFromFileOrDefault()
                        version = coordinator.getVersion()
                    } catch (ignoredEx: CorruptionException) {
                        version = writeData(newData, updateCache = true)
                    }
                }
            } catch (writeEx: Throwable) {
                // If we fail to write the handled data, add the new exception as a suppressed
                // exception.
                ex.addSuppressed(writeEx)
                throw ex
            }

            // If we reach this point, we&#39;ve successfully replaced the data on disk with newData.
            return Data(newData, newData.hashCode(), version)
        }
    }</code></pre>
<p>이중 <code>readDataFromFileOrDefault()</code>를 통해 드디어 storage에서 파일을 읽어오게 된다!</p>
<pre><code class="language-kotlin">private suspend fun readDataFromFileOrDefault(): T {
    return storageConnection.readData()
}
private val storageConnectionDelegate = lazy {
    storage.createConnection()
}
internal val storageConnection by storageConnectionDelegate</code></pre>
<p>먼저 <code>createConnection()</code>를 살펴보자.</p>
<pre><code class="language-kotlin">interface Storage&lt;T&gt; {
    fun createConnection(): StorageConnection&lt;T&gt;
}
interface StorageConnection&lt;T&gt; : Closeable {
    suspend fun &lt;R&gt; readScope(
        block: suspend ReadScope&lt;T&gt;.(locked: Boolean) -&gt; R
    ): R

    suspend fun writeScope(block: suspend WriteScope&lt;T&gt;.() -&gt; Unit)

    val coordinator: InterProcessCoordinator
}

interface ReadScope&lt;T&gt; : Closeable {

    suspend fun readData(): T
}

interface WriteScope&lt;T&gt; : ReadScope&lt;T&gt; {

    suspend fun writeData(value: T)
}</code></pre>
<p>이 인터페이스의 유일한 목적은 <code>createConnection()</code> 메소드를 제공하는 것이다.
<code>StorageConnection&lt;T&gt;</code>는 <code>createConnection()</code>이 반환하는 객체로, 실제로 파일(디스크)에 <code>read()</code> 및 <code>write()</code> 작업을 수행하는 구체적인 로직을 담고 있고 실제 구현체는 다음과 같다.</p>
<pre><code class="language-kotlin">class FileStorage&lt;T&gt;(
    private val serializer: Serializer&lt;T&gt;,
    private val coordinatorProducer: (File) -&gt; InterProcessCoordinator = {
        createSingleProcessCoordinator(it)
    },
    private val produceFile: () -&gt; File
) : Storage&lt;T&gt; {

    override fun createConnection(): StorageConnection&lt;T&gt; {
        val file = produceFile().canonicalFile

        synchronized(activeFilesLock) {
            val path = file.absolutePath
            check(!activeFiles.contains(path)) {
                &quot;There are multiple DataStores active for the same file: $path. You should &quot; +
                    &quot;either maintain your DataStore as a singleton or confirm that there is &quot; +
                    &quot;no two DataStore&#39;s active on the same file (by confirming that the scope&quot; +
                    &quot; is cancelled).&quot;
            }
            activeFiles.add(path)
        }

        return FileStorageConnection(file, serializer, coordinatorProducer(file)) {
            synchronized(activeFilesLock) {
                activeFiles.remove(file.absolutePath)
            }
        }
    }

    internal companion object {
        @GuardedBy(&quot;activeFilesLock&quot;)
        internal val activeFiles = mutableSetOf&lt;String&gt;()

        internal val activeFilesLock = Any()
    }
}

internal class FileStorageConnection&lt;T&gt;(
    private val file: File,
    private val serializer: Serializer&lt;T&gt;,
    override val coordinator: InterProcessCoordinator,
    private val onClose: () -&gt; Unit
) : StorageConnection&lt;T&gt; {

    private val closed = AtomicBoolean(false)
    private val transactionMutex = Mutex()

    override suspend fun &lt;R&gt; readScope(
        block: suspend ReadScope&lt;T&gt;.(locked: Boolean) -&gt; R
    ): R {
        checkNotClosed()

        val lock = transactionMutex.tryLock()
        try {
            return FileReadScope(file, serializer).use {
                block(it, lock)
            }
        } finally {
            if (lock) {
                transactionMutex.unlock()
            }
        }
    }

    override suspend fun writeScope(block: suspend WriteScope&lt;T&gt;.() -&gt; Unit) {
        checkNotClosed()
        file.createParentDirectories()

        transactionMutex.withLock {
            val scratchFile = File(file.absolutePath + &quot;.tmp&quot;)
            try {
                FileWriteScope(scratchFile, serializer).use {
                    block(it)
                }
                if (scratchFile.exists() &amp;&amp; !scratchFile.atomicMoveTo(file)) {
                    throw IOException(
                        &quot;Unable to rename $scratchFile to $file. &quot; +
                        &quot;This likely means that there are multiple instances of DataStore &quot; +
                        &quot;for this file. Ensure that you are only creating a single instance of &quot; +
                        &quot;datastore for this file.&quot;
                    )
                }
            } catch (ex: IOException) {
                if (scratchFile.exists()) {
                    scratchFile.delete() // Swallow failure to delete
                }
                throw ex
            }
        }
    }

    public override fun close() {
        closed.set(true)
        onClose()
    }

    private fun checkNotClosed() {
        check(!closed.get()) { &quot;StorageConnection has already been disposed.&quot; }
    }

    private fun File.createParentDirectories() {
        val parent: File? = canonicalFile.parentFile

        parent?.let {
            it.mkdirs()
            if (!it.isDirectory) {
                throw IOException(&quot;Unable to create parent directories of $this&quot;)
            }
        }
    }
}</code></pre>
<p>일단 <code>StorageConnection</code>을 생성하는 <code>createConnection()</code>부터 살펴보면 다음과 같이 되어있다.</p>
<pre><code class="language-kotlin">override fun createConnection(): StorageConnection&lt;T&gt; {
   val file = produceFile().canonicalFile

   synchronized(activeFilesLock) {
      val path = file.absolutePath
      check(!activeFiles.contains(path)) {
         &quot;There are multiple DataStores active for the same file: $path. You should &quot; +
                 &quot;either maintain your DataStore as a singleton or confirm that there is &quot; +
                 &quot;no two DataStore&#39;s active on the same file (by confirming that the scope&quot; +
                 &quot; is cancelled).&quot;
      }
      activeFiles.add(path)
   }

   return FileStorageConnection(file, serializer, coordinatorProducer(file)) {
      synchronized(activeFilesLock) {
         activeFiles.remove(file.absolutePath)
      }
   }
}</code></pre>
<p>여길 보면 <code>produceFile().canonicalFile</code>로 파일을 불러오는데 그렇다면 이 <code>produceFile()</code>이 뭔지를 알아야 한다. <code>produceFile</code>은 <code>FileStorage</code>의 파라미터로 <code>() -&gt; File</code>이다. 이건 </p>
<pre><code class="language-kotlin">public actual object DataStoreFactory {
    @JvmOverloads // Generate constructors for default params for java users.
    public fun &lt;T&gt; create(
        serializer: Serializer&lt;T&gt;,
        corruptionHandler: ReplaceFileCorruptionHandler&lt;T&gt;? = null,
        migrations: List&lt;DataMigration&lt;T&gt;&gt; = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -&gt; File
    ): DataStore&lt;T&gt; = create(
        storage = FileStorage(serializer = serializer, produceFile = produceFile),
        corruptionHandler = corruptionHandler,
        migrations = migrations,
        scope = scope
    )
    ...
}</code></pre>
<p>이 팩토리에서 DataStore가 생성될 때 함께 생성되는데 여기서도 <code>produceFile</code>이 뭔지 알수 없다. 그렇다면 이 팩토리 함수가 호출되는 곳을 찾아가보자.</p>
<pre><code class="language-kotlin">private val Context.tokenDataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME)</code></pre>
<p>DataStore 생성을 위임할 때 사용되는 이 <code>preferencesDataStore</code>가 실제 DataStore를 생성하니 이것을 따라가자.</p>
<pre><code class="language-kotlin">@Suppress(&quot;MissingJvmstatic&quot;)
public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler&lt;Preferences&gt;? = null,
    produceMigrations: (Context) -&gt; List&lt;DataMigration&lt;Preferences&gt;&gt; = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty&lt;Context, DataStore&lt;Preferences&gt;&gt; {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}
internal class PreferenceDataStoreSingletonDelegate internal constructor(
    private val name: String,
    private val corruptionHandler: ReplaceFileCorruptionHandler&lt;Preferences&gt;?,
    private val produceMigrations: (Context) -&gt; List&lt;DataMigration&lt;Preferences&gt;&gt;,
    private val scope: CoroutineScope
) : ReadOnlyProperty&lt;Context, DataStore&lt;Preferences&gt;&gt; {

    private val lock = Any()

    @GuardedBy(&quot;lock&quot;)
    @Volatile
    private var INSTANCE: DataStore&lt;Preferences&gt;? = null

    /**
     * Gets the instance of the DataStore.
     *
     * @param thisRef must be an instance of [Context]
     * @param property not used
     */
    override fun getValue(thisRef: Context, property: KProperty&lt;*&gt;): DataStore&lt;Preferences&gt; {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                val applicationContext = thisRef.applicationContext

                INSTANCE = PreferenceDataStoreFactory.create(
                    corruptionHandler = corruptionHandler,
                    migrations = produceMigrations(applicationContext),
                    scope = scope
                ) {
                    applicationContext.preferencesDataStoreFile(name)
                }
            }
            INSTANCE!!
        }
    }
}</code></pre>
<p><code>PreferenceDataStoreFactory</code>가 실제 인스턴스를 생성하는 곳을 찾았다!
저 람다가 바로 <code>produceFile</code>이다.</p>
<pre><code class="language-kotlin">public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile(&quot;$name.preferences_pb&quot;)
public fun Context.dataStoreFile(fileName: String): File =
    File(applicationContext.filesDir, &quot;datastore/$fileName&quot;) </code></pre>
<p>다음과 같이 파일을 생성하는 것을 확인할 수 있다.</p>
<pre><code class="language-java">public File(File var1, String var2) {
    if (var2 == null) {
        throw new NullPointerException();
    } else {
        if (var1 != null) {
            if (var1.path.isEmpty()) {
                this.path = FS.resolve(FS.getDefaultParent(), FS.normalize(var2));
            } else {
                this.path = FS.resolve(var1.path, FS.normalize(var2));
            }
        } else {
            this.path = FS.normalize(var2);
        }

        this.prefixLength = FS.prefixLength(this.path);
    }
}</code></pre>
<p>그렇다면 이 <code>FS</code>, 파일시스템은 어떻게 되어있을까</p>
<pre><code class="language-java">static {
    if (IoOverNio.IS_ENABLED_IN_GENERAL) {
        FS = new IoOverNioFileSystem(DefaultFileSystem.getFileSystem());
    } else {
        FS = DefaultFileSystem.getFileSystem();
    }
   ...
}</code></pre>
<p>일단 위와 같이 <code>FS</code>를 선언한다. </p>
<pre><code class="language-java">class IoOverNioFileSystem extends FileSystem {
    private final FileSystem parent;

    IoOverNioFileSystem(FileSystem var1) {
        this.parent = var1;
    }
    ...
}
final class DefaultFileSystem {
    private DefaultFileSystem() {
    }

    public static FileSystem getFileSystem() {
        return new UnixFileSystem();
    }
}</code></pre>
<p>여기서 UnixFileSystem과 IoOverNioFileSystem을 간단히 설명하자면</p>
<ul>
<li>UnixFileSystem : 구형 레거시, Java 7 이전에 사용됨, Unix/Linux 시스템에서 파일 작업을 할 때 사용되던 실제 구현체로 직접 네이티브(C) 코드를 호출하여 운영체제와 통신</li>
<li>IoOverNioFileSystem : 신형 어댑터, Java 7 이후에 사용됨, 클래스의 요청을 처리하는 &#39;어댑터(Adapter)&#39;로 실제 <code>DefaultFileSystem</code>이 처리한다.</li>
</ul>
<p>여기서 처음 알게된 건데 DefaultFileSystem는 위의 코드에서 확인할 수 있다시피 UnixFileSystem을 리턴한다. 결국 처리는 UnixFileSystem가 하고 IoOverNioFileSystem는 정말 어댑터의 역할만을 수행한다.</p>
<p>그렇다면 File 생성자의</p>
<pre><code class="language-java">this.path = FS.resolve(FS.getDefaultParent(), FS.normalize(var2));</code></pre>
<p>에서 이 <code>resolve</code>는 아래와 같이 parent에 위임하고</p>
<pre><code class="language-java">class IoOverNioFileSystem extends FileSystem {
    ...
    public String resolve(String var1, String var2) {
        return this.parent.resolve(var1, var2);
    }
    ...
}</code></pre>
<p>이 parent는 바로 <code>DefaultFileSystem.getFileSystem()</code> 가 리턴하는 <code>UnixFileSystem</code>이다.
UnixFileSystem의 resolve는 아래와 같이 되어있다.</p>
<pre><code class="language-java">private static String trimSeparator(String var0) {
   int var1 = var0.length();
   return var1 &gt; 1 &amp;&amp; var0.charAt(var1 - 1) == &#39;/&#39; ? var0.substring(0, var1 - 1) : var0;
}

public String resolve(String var1, String var2) {
   if (var2.isEmpty()) {
      return var1;
   } else if (var2.charAt(0) == &#39;/&#39;) {
      return var1.equals(&quot;/&quot;) ? var2 : trimSeparator(var1 + var2);
   } else {
      return var1.equals(&quot;/&quot;) ? trimSeparator(var1 + var2) : trimSeparator(var1 + &#39;/&#39; + var2);
   }
}</code></pre>
<p>즉 실제 파일 경로를 생성한다.
결국 </p>
<pre><code class="language-kotlin">public fun Context.dataStoreFile(fileName: String): File =
    File(applicationContext.filesDir, &quot;datastore/$fileName&quot;) </code></pre>
<p>는 <code>applicationContext.filesDir</code>, &quot;datastore/$fileName&quot;로 만든 파일 경로를 갖는 File 인스턴스를 반환한다.</p>
<p>조금 많이 돌아왔는데 결국은 이렇게 File 인스턴스를 만드는 함수가 바로 <code>produceFile</code>이고 이렇게 얻은 File 인스턴스로 절대경로 등의 값으로</p>
<pre><code class="language-kotlin">override fun createConnection(): StorageConnection&lt;T&gt; {
   val file = produceFile().canonicalFile

   synchronized(activeFilesLock) {
      val path = file.absolutePath
      check(!activeFiles.contains(path)) {
         &quot;There are multiple DataStores active for the same file: $path. You should &quot; +
                 &quot;either maintain your DataStore as a singleton or confirm that there is &quot; +
                 &quot;no two DataStore&#39;s active on the same file (by confirming that the scope&quot; +
                 &quot; is cancelled).&quot;
      }
      activeFiles.add(path)
   }

   return FileStorageConnection(file, serializer, coordinatorProducer(file)) {
      synchronized(activeFilesLock) {
         activeFiles.remove(file.absolutePath)
      }
   }
}</code></pre>
<p><code>FileStorageConnection</code>를 반환하는 것이다. 
그리고 이 <code>FileStorageConnection</code>이 </p>
<pre><code class="language-kotlin">override suspend fun &lt;R&gt; readScope(
        block: suspend ReadScope&lt;T&gt;.(locked: Boolean) -&gt; R
    ): R {
        checkNotClosed()

        val lock = transactionMutex.tryLock()
        try {
            return FileReadScope(file, serializer).use {
                block(it, lock)
            }
        } finally {
            if (lock) {
                transactionMutex.unlock()
            }
        }
    }
    ...
}</code></pre>
<p>와 같고 이 <code>readScope</code>는 다음과 같다.</p>
<pre><code class="language-kotlin">internal open class FileReadScope&lt;T&gt;(
   protected val file: File,
   protected val serializer: Serializer&lt;T&gt;
) : ReadScope&lt;T&gt; {

    private val closed = AtomicBoolean(false)

    override suspend fun readData(): T {
        checkNotClosed()
        return try {
            FileInputStream(file).use { stream -&gt;
                serializer.readFrom(stream)
            }
        } catch (ex: FileNotFoundException) {
            if (file.exists()) {
                // Re-read to prevent throwing from a race condition where the file is created by
                // another process after the initial read attempt but before `file.exists()` is
                // called. Otherwise file exists but we can&#39;t read it; throw FileNotFoundException
                // because something is wrong.
                return FileInputStream(file).use { stream -&gt;
                    serializer.readFrom(stream)
                }
            } else {
                serializer.defaultValue
            }
        }
    }
    ...
}</code></pre>
<p>즉 <code>FileInputStream</code>으로 파일을 읽어오는 것이다.
그럼 이제 다시 <code>DataStoreImpl</code>로 돌아가서 이렇게 파일에서 읽어온 데이터로 </p>
<pre><code class="language-kotlin">private suspend fun readDataOrHandleCorruption(hasWriteFileLock: Boolean): Data&lt;T&gt; {
   try {
       return if (hasWriteFileLock) {
           val data = readDataFromFileOrDefault()
           Data(data, data.hashCode(), version = coordinator.getVersion())
       }
       ...
   }
      ...
}</code></pre>
<p><code>Data</code> 인스턴스를 만들고 <code>InitDataStore</code>가 <code>doRun()</code>에서 </p>
<pre><code class="language-kotlin">var currentData = readDataOrHandleCorruption(hasWriteFileLock = true).value</code></pre>
<p>의 value인 Preferences로 </p>
<pre><code class="language-kotlin">val initData = if ((initTasks == null) || initTasks!!.isEmpty()) {
   readDataOrHandleCorruption(hasWriteFileLock = false)
} else {
   coordinator.lock {
      val updateLock = Mutex()
      var initializationComplete = false
      var currentData = readDataOrHandleCorruption(hasWriteFileLock = true).value

      val api = object : InitializerApi&lt;T&gt; {
         override suspend fun updateData(transform: suspend (t: T) -&gt; T): T {
            return updateLock.withLock {
               check(!initializationComplete) {
                  &quot;InitializerApi.updateData should not be called after &quot; +
                          &quot;initialization is complete.&quot;
               }

               val newData = transform(currentData)
               if (newData != currentData) {
                  writeData(newData, updateCache = false)
                  currentData = newData
               }

               currentData
            }
         }
      }

      initTasks?.forEach { it(api) }
      initTasks = null
      updateLock.withLock {
         initializationComplete = true
      }
      Data(
         value = currentData,
         hashCode = currentData.hashCode(),
         version = coordinator.getVersion()
      )
   }
}
inMemoryCache.tryUpdate(initData)</code></pre>
<p>메모리 캐시를 이 Data의 value인 Preferences로 만든 Data 인스턴스인 <code>initData</code>로 <code>tryUpdate</code>를 통해 업데이트한다.</p>
<pre><code class="language-kotlin">androidx.datastore.core/DataStoreInMemoryCache
val currentState: State&lt;T&gt;
    get() = cachedValue.value

val flow: Flow&lt;State&lt;T&gt;&gt;
    get() = cachedValue
fun tryUpdate(
   newState: State&lt;T&gt;
): State&lt;T&gt; {
   val updated = cachedValue.updateAndGet { cached -&gt;
      when (cached) {
         is ReadException&lt;T&gt;, UnInitialized -&gt; {
            newState
         }
         is Data&lt;T&gt; -&gt; {
            if (newState.version &gt; cached.version) {
               newState
            } else {
               cached
            }
         }

         is Final&lt;T&gt; -&gt; {
            cached
         }
      }
   }
   return updated
}</code></pre>
<p>즉 state가 Data로 업데이트되었으니 <code>handleUpdate()</code>에서 초기화 작업이 완료된 것이다.</p>
<p>이제 마지막으로 <code>transformAndWrite()</code>를 통해 작성을 해준다.</p>
<pre><code class="language-kotlin">private suspend fun transformAndWrite(
   transform: suspend (t: T) -&gt; T,
   callerContext: CoroutineContext
): T = coordinator.lock {
   val curData = readDataOrHandleCorruption(hasWriteFileLock = true)
   val newData = withContext(callerContext) { transform(curData.value) }

   curData.checkHashCode()

   if (curData.value != newData) {
      writeData(newData, updateCache = true)
   }
   newData
}

internal suspend fun writeData(newData: T, updateCache: Boolean): Int {
   var newVersion = 0

   storageConnection.writeScope {
      newVersion = coordinator.incrementAndGetVersion()
      writeData(newData)
      if (updateCache) {
         inMemoryCache.tryUpdate(Data(newData, newData.hashCode(), newVersion))
      }
   }

   return newVersion
}</code></pre>
<p>최종적으로 작성되는 곳은 <code>storageConnection.writeScope { ...  writeData(newData) ... }</code>로 이것은 아래와 같다.</p>
<pre><code class="language-kotlin">internal class FileWriteScope&lt;T&gt;(file: File, serializer: Serializer&lt;T&gt;) :
   FileReadScope&lt;T&gt;(file, serializer), WriteScope&lt;T&gt; {

   override suspend fun writeData(value: T) {
      checkNotClosed()
      val fos = FileOutputStream(file)
      fos.use { stream -&gt;
         serializer.writeTo(value, UncloseableOutputStream(stream))
         stream.fd.sync()
      }
   }
}</code></pre>
<p>DataStore에서 사용할 때 키-값의 형식으로 사용하는데 이 Preferences와 디스크에 저장될 .preferences_pb 파일(바이너리 데이터) 사이를 서로 번역/변환하는 것을 <code>serializer</code>가 수행한다.</p>
<h3 id="preferencesfileserializer">PreferencesFileSerializer</h3>
<pre><code class="language-kotlin">internal object PreferencesFileSerializer : Serializer&lt;Preferences&gt; {
   internal const val fileExtension = &quot;preferences_pb&quot;

   override val defaultValue: Preferences
      get() {
         return emptyPreferences()
      }

   @Throws(IOException::class, CorruptionException::class)
   override suspend fun readFrom(input: InputStream): Preferences {
      val preferencesProto = PreferencesMapCompat.readFrom(input)

      val mutablePreferences = mutablePreferencesOf()

      preferencesProto.preferencesMap.forEach { (name, value) -&gt;
         addProtoEntryToPreferences(name, value, mutablePreferences)
      }

      return mutablePreferences.toPreferences()
   }

   @Suppress(&quot;InvalidNullabilityOverride&quot;) // Remove after b/232460179 is fixed
   @Throws(IOException::class, CorruptionException::class)
   override suspend fun writeTo(t: Preferences, output: OutputStream) {
      val preferences = t.asMap()
      val protoBuilder = PreferenceMap.newBuilder()

      for ((key, value) in preferences) {
         protoBuilder.putPreferences(key.name, getValueProto(value))
      }

      protoBuilder.build().writeTo(output)
   }

   private fun getValueProto(value: Any): Value {
      return when (value) {
         is Boolean -&gt; Value.newBuilder().setBoolean(value).build()
         is Float -&gt; Value.newBuilder().setFloat(value).build()
         is Double -&gt; Value.newBuilder().setDouble(value).build()
         is Int -&gt; Value.newBuilder().setInteger(value).build()
         is Long -&gt; Value.newBuilder().setLong(value).build()
         is String -&gt; Value.newBuilder().setString(value).build()
         is Set&lt;*&gt; -&gt;
            @Suppress(&quot;UNCHECKED_CAST&quot;)
            Value.newBuilder()
               .setStringSet(StringSet.newBuilder().addAllStrings(value as Set&lt;String&gt;))
               .build()
         is ByteArray -&gt; Value.newBuilder().setBytes(ByteString.copyFrom(value)).build()
         else -&gt;
            throw IllegalStateException(
               &quot;PreferencesSerializer does not support type: ${value.javaClass.name}&quot;
            )
      }
   }

   private fun addProtoEntryToPreferences(
      name: String,
      value: Value,
      mutablePreferences: MutablePreferences
   ) {
      return when (value.valueCase) {
         Value.ValueCase.BOOLEAN -&gt;
            mutablePreferences[booleanPreferencesKey(name)] = value.boolean
         Value.ValueCase.FLOAT -&gt; mutablePreferences[floatPreferencesKey(name)] = value.float
         Value.ValueCase.DOUBLE -&gt; mutablePreferences[doublePreferencesKey(name)] = value.double
         Value.ValueCase.INTEGER -&gt; mutablePreferences[intPreferencesKey(name)] = value.integer
         Value.ValueCase.LONG -&gt; mutablePreferences[longPreferencesKey(name)] = value.long
         Value.ValueCase.STRING -&gt; mutablePreferences[stringPreferencesKey(name)] = value.string
         Value.ValueCase.STRING_SET -&gt;
            mutablePreferences[stringSetPreferencesKey(name)] =
               value.stringSet.stringsList.toSet()
         Value.ValueCase.BYTES -&gt;
            mutablePreferences[byteArrayPreferencesKey(name)] = value.bytes.toByteArray()
         Value.ValueCase.VALUE_NOT_SET -&gt; throw CorruptionException(&quot;Value not set.&quot;)
         null -&gt; throw CorruptionException(&quot;Value case is null.&quot;)
      }
   }
}</code></pre>
<p>각각을 간단하게 정리하면</p>
<ol>
<li><code>internal const val fileExtension = &quot;preferences_pb&quot;</code></li>
</ol>
<ul>
<li>SharedPreferences의 XML과 달리 DataStore는 Protocol Buffers (Protobuf)라는 효율적인 바이너리 포맷 사용</li>
<li>Protocol Buffers : 데이터의 구조를 미리 약속하고, 이 구조에 맞춰 데이터를 효율적인 바이너리 형태로 변환하는 기술</li>
</ul>
<ol start="2">
<li><code>readFrom()</code></li>
</ol>
<ul>
<li>InputStream (.preferences_pb 파일)에서 바이너리 데이터를 읽어 Protobuf 객체(Preferences)로 파싱하는 함수</li>
</ul>
<ol start="3">
<li><code>writeTo()</code></li>
</ol>
<ul>
<li>Preferences를 Map으로 변환한 후 Kotlin 타입을 Protobuf의 Value 객체로 Wrapping후 바이너리 데이터로 직렬화하여 OutputStream에 작성</li>
</ul>
<p>그렇다면 이 <code>writeTo</code>를 했으니 실제로 파일에 작성되었을까?
그렇지 않다!</p>
<p>아직은 물리적 디스크에 저장되지 않고 메모리에 있는 버퍼(시스템 캐시)에 복사된다.
만약 OS가 이 버퍼를 디스크에 쓰기 전에 앱이 강제 종료되거나 전원이 꺼지면 데이터는 유실된다!</p>
<p>그것을 방지하는 것이 바로 마지막에 있는 <code>stream.fd.sync()</code>이다.</p>
<h3 id="filedescriptor">FileDescriptor</h3>
<p>저 fd는 FileDescriptor이고 이 FileDescriptor는 아래와 같이 되어있다.</p>
<pre><code class="language-java">public final class FileDescriptor {
    ...
    public native void sync() throws SyncFailedException;
}</code></pre>
<p>이 <code>sync()</code>는 native 키워드가 붙어있는 함수로 실제 OS의 함수를 호출한다.</p>
<p><code>sync()</code>는 지금 바로 메모리 버퍼에 있는 모든 데이터를 물리적 디스크에 동기화하고 OS에 명령하는 함수이다.
이 함수의 핵심은 실제 물리적 매체에 기록될 때까지 반환되지 않아 이 함수가 리턴되면, 데이터가 디스크에 안전하게 저장되었음을 100% 보장한다는 것이다.</p>
<p>즉, DataStore는 단순 작성이 아닌 실제 물리적 디스크에 저장하는 것까지 보장한다.</p>
<h2 id="결론">결론</h2>
<p>지금까지 DataStore가 어떻게 파일을 읽고 쓰는지를 알아보았다.</p>
<p>사실 대부분의 DataStore의 코드는 race condition을 방지하고 무결성과 원자성을 보장하는데 집중되어 있고 실제 입출력은 흔히들 아는 <code>FileOutputStream</code>과 <code>FileInputStream</code>로 수행한다.</p>
<p>아무생각없이 사용만했던 DataStore에 대해 조금은 이해한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Gradle의 Provider와 Property에 대해서 알아보자]]></title>
            <link>https://velog.io/@couch_potato/Android-Gradle%EC%9D%98-Provider%EC%99%80-Property%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@couch_potato/Android-Gradle%EC%9D%98-Provider%EC%99%80-Property%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 03 Sep 2025 05:11:24 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>필자는 nowinandroid에 몇주전 pr을 올렸었다.
이 과정에서 가볍게 공부만 하고 넘어갔던 내용들을 보다 깊이있게 공부해보고자 한다.
이번에는 그중에서 Provider와 Property를 알아볼 것이다.</p>
<h3 id="provider와-property">Provider와 Property</h3>
<p>그럼 일단 Provider와 Property가 무엇인지부터 간단히 알아보자.
공식 문서에는 다음과 같이 설명한다.</p>
<blockquote>
<p><code>Property</code> - Represents a value that can be queried and changed.
<code>Provider</code> - Represents a value that can only be queried and cannot be changed.</p>
</blockquote>
<p>한국어로 번역하면 다음과 같다.</p>
<ul>
<li>Provider : 값을 조회하고 변경할 수 있는 것</li>
<li>Property : 값을 조회만 할 수 있고 변경할 수 없는 것</li>
</ul>
<p>으로 Provider와 Property는 빌드스크립트에서 value와 configuration을 관리한다.</p>
<p>일단은 이정도만 알아두고 Gradle의 Lazy Configuration에 대해 알아보자.</p>
<h2 id="lazy-configuration">Lazy Configuration</h2>
<p>빌드가 복잡해짐에 따라 특정한 값을 추적하기가 어려워지는데 Gradle은 Lazy Configuration이라는 것을 통해 이 문제를 해결한다.</p>
<p>Gradle의 Lazy Property은 3가지 주요 이점을 제공한다.</p>
<ul>
<li><strong>Deferred Value Resolution</strong>(값 결정 지연)</li>
<li><strong>Automatic Task Dependency Management</strong>(자동 task 의존성 관리)</li>
<li><strong>Improved Build Performance</strong>(빌드 성능 향상)</li>
</ul>
<p>자동 Task 의존성 관리만 간단히 설명하자면 
어느 한 Task의 출력을 다른 Task의 입력으로 연결하여 의존성을 자동으로 파악할 수 있다는 것이다.</p>
<p>즉</p>
<pre><code class="language-kotlin">taskB.input.set(taskA.output)</code></pre>
<p>이렇게 한 Task의 출력을 다른 Task의 입력으로 연결해주면 Gradle은 알아서 TaskA부터 실행한다.</p>
<p>Lazy Property는 Provider와 Property라는 2가지 인터페이스로 나타낸다.
<strong>Provider</strong></p>
<ul>
<li>읽기 전용 프로퍼티</li>
<li><code>Provider.get()</code>으로 현재 값 반환</li>
<li><code>Provider.map(Transformer)</code>로 다른 <code>Provider</code> 생성 가능</li>
</ul>
<p><strong>Property</strong></p>
<ul>
<li>읽고 쓰기가 모두 가능한 프로퍼티</li>
<li><code>Provider</code>를 <code>extend</code>한다.</li>
<li><code>Property.set(T)</code>로 값 지정 이때 기존의 값은 덮어씌움</li>
<li><code>Property.set(Provider)</code>는 프로퍼티의 값으로 다른 <code>Provider</code>를 지정하며, 기존에 있던 값은 덮어쓴다. 
이를 통해 값이 아직 정해지지 않은 상태에서도 <code>Provider</code>와 <code>Property</code> 인스턴스를 서로 연결할 수 있다.</li>
</ul>
<h3 id="실제-사용-예시">실제 사용 예시</h3>
<pre><code class="language-kotlin">public abstract class Download extends DefaultTask {
    @Input
    public abstract Property&lt;URI&gt; getUri(); // abstract getter of type Property&lt;T&gt;

    @TaskAction
    void run() {
        System.out.println(&quot;Downloading &quot; + getUri().get()); // Use the `uri` property
    }
}</code></pre>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://docs.gradle.org/current/userguide/properties_providers.html">https://docs.gradle.org/current/userguide/properties_providers.html</a>
<a href="https://docs.gradle.org/current/userguide/lazy_configuration.html#lazy_configuration">https://docs.gradle.org/current/userguide/lazy_configuration.html#lazy_configuration</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 컨벤션 플러그인을 사용해서 멀티 모듈을 효율적으로 관리해보자]]></title>
            <link>https://velog.io/@couch_potato/Android-%EC%BB%A8%EB%B2%A4%EC%85%98-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88%EC%9D%84-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@couch_potato/Android-%EC%BB%A8%EB%B2%A4%EC%85%98-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88%EC%9D%84-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 24 Aug 2025 13:05:42 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>안드로이드 프로젝트를 멀티 모듈로 진행하다보면 필연적으로 똑같은 플러그인을 똑같이 적용하는 상황을 마주한다.
그럴 때마다 동일한 플러그인을 반복적으로 작성하는 비효율적이라고 느껴지는데 이것을 어떻게 해결할 수 있을까?</p>
<p>구글에 쳐보면 바로 알 수 있듯이 Convention Plugin을 통해 이 문제를 해결할 수 있다고 한다.</p>
<p>하지만 본인은 컨벤션 플러그인에 대해 여러 글을 보아도 이해가 잘 가지 않았다!</p>
<p>글마다 방식도 다르고 플러그인을 <code>dependencies</code>에 추가하라 하기에 따라 해도 해당 플러그인이 없다고 아래와 같이 에러가 발생했다.
<img src="https://velog.velcdn.com/images/couch_potato/post/e732f8a0-34eb-4c22-a8b5-27a3fa0e6ad5/image.png" alt=""></p>
<p>또한 필자는 nowinandroid를 많이 참고하는 편인데 nowinandroid의 컨벤션 플러그인은 또 여타 레퍼런스들과는 사뭇 다르다!</p>
<img src ="https://velog.velcdn.com/images/couch_potato/post/e003ccf4-8067-4119-8133-a795ac7dde9f/image.png" width = "300" >

<p>그러다 보니 어떻게 해야하는지도 감도 안오고 복붙하니 되긴 되는데
어째서 되는 지 이해가 하나도 안 됐다...</p>
<p>그래서 아예 Gradle부터 하나씩 공부해서 싹다 뒤집어 까보기로 했고
그 과정을 기록하게 되었다.</p>
<p>이 글이 내가 공부하고 이해한 과정을 그대로 담았다.(그래서 틀릴 수도 있다...)
그렇기 때문에 나같은 사람이 또 있다면 이 글이 도움이 되길 바란다...</p>
<h2 id="multi-project-build">multi-project build</h2>
<p>Convention Plugin에 대해 이야기하기 전에 먼저 Gradle의 multi-project build를 알아야 한다.</p>
<blockquote>
<p>multi-project build란 무엇인가?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/fc18b882-35e9-4810-a0cd-ee6c0f686623/image.png" alt=""></p>
<blockquote>
<p>While some small projects and monolithic applications may contain a single build file and source tree, it is often more common for a project to have been split into smaller, interdependent modules. The word &quot;interdependent&quot; is vital, as you typically want to link the many modules together through a single build.
Gradle supports this scenario through multi-project builds
This is sometimes referred to as a multi-module project.</p>
</blockquote>
<p>Gradle의 공식 문서에는 위와 같이 적혀있다. 내용은 아래와 같다.</p>
<blockquote>
<p>프로젝트가 더 작고 상호의존적인 모듈로 분할되는 것이 일반적이고 Gradle은 multi-project builds로 그것을 지원한다
이것은 때때로 multi-module project라 불린다.</p>
</blockquote>
<p>즉 멀티 프로젝트란 안드로이드에서의 멀티 모듈을 말한다.
아래의 예시로 좀더 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/548306ec-9fe4-4f9c-a133-d73fa66fb7be/image.png" alt=""></p>
<pre><code>├── .gradle
│   └── ⋮
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle.kts 
├── sub-project-1
│   └── build.gradle.kts    
├── sub-project-2
│   └── build.gradle.kts    
└── sub-project-3
    └── build.gradle.kts</code></pre><p>이것은 3개의 서브 프로젝트을 포함하는 멀티 프로젝트 빌드의 구조인데 어디선가 많이 본듯하다</p>
<img src ="https://velog.velcdn.com/images/couch_potato/post/20fcc72b-ee97-4a4f-8c3f-61221b44d4b0/image.png" width = "150" height = "150">
바로 안드로이드 멀티 모듈 환경이다.

<p>그렇다면 이 멀티 모듈의 빌드 구조를 알아보자</p>
<h3 id="multi-project-build-standards">Multi-Project Build Standards</h3>
<p>Gradle community는 멀티 모듈 빌드 구조에 대해서 2가지 표준을 갖고 있다.
<img src="https://velog.velcdn.com/images/couch_potato/post/4c4ed158-3883-41dd-a0b2-e30fbc9a8a21/image.png" alt=""></p>
<p><strong>1. Multi-Project Builds using buildSrc</strong> - buildSrc는 모든 빌드 로직을 포함하는 Gradle 프로젝트 루트의 하위 프로젝트와 같은 디렉토리
<strong>2. Composite Builds</strong> - build-logic이 재사용 가능한 빌드 로직을 포함하는 Gradle 프로젝트 루트의 빌드 디렉토리인 다른 빌드를 포함하는 빌드.</p>
<p>그중 Convention Plugin은 Composite Builds이므로
BuildSrc가 뭔지만 간단히 알고 넘어가자</p>
<h4 id="buildsrc란">BuildSrc란</h4>
<blockquote>
<p>멀티 모듈에서 공통 빌드 로직(플러그인 등)을 한 곳에 모아두고 여러 모듈에서 쉽게 재사용하기 위한 특별한 디렉토리로, Gradle이 자동으로 인식하고 빌드 로직을 모든 모듈에 제공한다.</p>
</blockquote>
<p>라고 하는데 이렇게 보면 Composite Builds랑 무슨 차이인가 싶다.
애초에 목적은 똑같다.
그렇다면 우리는 차이점을 통해 BuildSrc를 알아보자.</p>
<ul>
<li>BuildSrc는 단일 빌드의 일부다.</li>
<li>그렇기에 메인 프로젝트와 생명주기를 같이 하여 결합도가 높다.</li>
<li>내부의 코드 한 줄만 바뀌어도 전체 빌드 로직 캐시가 무효화되어 빌드 속도에 영향을 준다. -&gt; 캐시 효율이 좋지 않다.</li>
</ul>
<p>이 글에서는 이정도만 알아도 괜찮다.
용도가 같으니 Composite Builds를 이해할수록 BuildSrc도 더 이해가 갈 것이다.</p>
<h3 id="composite-builds">Composite Builds</h3>
<p>그렇다면 Composite Builds를 알아보자.
Gradle 공식문서에서는 Composite Builds를 아래와 같이 말하고 있다.</p>
<blockquote>
<p>Composite Builds, also referred to as included builds, are best for sharing logic between builds (not subprojects) or isolating access to shared build logic (i.e., convention plugins).</p>
</blockquote>
<p>Composite Builds는 빌드간 로직(서브 모듈간 X)을 공유하거나 공유 빌드 로직(즉, 컨벤션 플러그인)에 대한 액세스를 격리하는 데 가장 좋다고 한다.</p>
<pre><code>.
├── gradle
├── gradlew
├── settings.gradle.kts
├── build-logic
│   ├── settings.gradle.kts
│   └── conventions
│       ├── build.gradle.kts
│       └── src/main/kotlin/shared-build-conventions.gradle.kts
├── mobile-app
│   └── build.gradle.kts
├── web-app
│   └── build.gradle.kts
├── api
│   └── build.gradle.kts
├── lib
│   └── build.gradle.kts
└── documentation
    └── build.gradle.kts</code></pre><p>BuildSrc와 달리 플러그인은 <code>build.gradle.kts</code>와 <code>settings.gradle.kts</code>와 함께 build-logic 자체 빌드로 이동한다.</p>
<p>일단 지금은 플러그인에 대해서는 넘어가고 빌드 구조만을 먼저 이해하자.</p>
<p>여기서 용어를 정리하고 가자면 <code>빌드 != 모듈(프로젝트)</code>이다.</p>
<ul>
<li><p><strong>빌드(Build)</strong> : 전체 빌드 프로세스를 의미한다. 즉, gradle 명령어를 실행할 때 발생하는 모든 작업을 포괄하는 개념으로 하나의 빌드는 하나 이상의 프로젝트(Project)로 구성된다.</p>
</li>
<li><p><strong>프로젝트(Project)</strong> :  빌드의 기본 단위이다. 각 프로젝트는 자체적인 소스 코드, 리소스, 테스트 등을 가지고 있으며, build.gradle 또는 build.gradle.kts 파일로 프로젝트의 빌드 로직을 정의한다. 멀티 프로젝트 빌드에서는 여러 프로젝트가 계층적으로 구성될 수 있다.</p>
</li>
</ul>
<p>안드로이드를 예시로 더 간단하게 생각하면 <code>settings.gradle.kts</code>가 있으면 빌드, 없으면 모듈이다.</p>
<p>그러므로 <code>settings.gradle.kts</code>가 루트 프로젝트에만 있는 일반적인 멀티 모듈 프로젝트들은 단일 빌드다.</p>
<pre><code>my-composite
├── settings.gradle.kts
├── build.gradle.kts
├── my-app
│   ├── settings.gradle.kts
│   └── app
│       ├── build.gradle.kts
│       └── src/main/java/org/sample/my-app/Main.java
└── my-utils
    ├── settings.gradle.kts
    ├── number-utils
    │   ├── build.gradle.kts
    │   └── src/main/java/org/sample/numberutils/Numbers.java
    └── string-utils
        ├── build.gradle.kts
        └── src/main/java/org/sample/stringutils/Strings.java</code></pre><p>이와 같이 독립적인 빌드 <code>my-utils</code>와 <code>my-app</code>이 있을 때
<code>my-app</code>이 저 두 라이브러리의 함수를 사용하고자 한다면 <code>my-utils</code>를 직접 의존하는 것이 아니라 각각을 아래와 같이 의존한다.</p>
<pre><code>dependencies {
    implementation(&quot;org.sample:number-utils:1.0&quot;)
    implementation(&quot;org.sample:string-utils:1.0&quot;)
}</code></pre><p>실제 안드로이드 환경에서도 각각의 독립적인 빌드가 다른 빌드의 모듈에 있는 함수나 클래스를 사용하려면 각 모듈을 추가해야 한다.</p>
<p>그 이유는 Gradle의 의존성 관리 메커니즘과 관련이 있는데
Gradle은 프로젝트 간의 의존성을 관리할 때, 프로젝트의 <strong>결과물(출력물)</strong> 을 기준으로 한다.</p>
<p><code>my-app</code>프로젝트가 <code>number-utils</code> 프로젝트의 기능을 사용하고 싶을 때, <code>my-app</code>은 <code>number-utils</code>가 컴파일되어 생성된 라이브러리(JAR 파일)를 필요로 한다.</p>
<p>만약 빌드 자체에 의존하는 방식이라면, Gradle은 어떤 모듈의 결과물을 사용해야 할지 알 수 없다. 빌드는 여러 프로젝트(모듈)로 구성될 수 있으므로, <code>my-utils</code> 빌드에 의존한다는 것은 <code>number-utils</code>와 <code>string-utils</code> 중 어떤 것을 사용할지 명확하지 않기 때문이다.</p>
<p>그렇다면, <code>my-utils</code>빌드에 있는 모든 모듈(<code>number-utils</code>, <code>string-utils</code>)을 <code>my-app</code>에서 사용하고 싶을 때, 매번 개별적으로 의존성을 추가해야 할까? 이 문제를 해결하고, 독립된 여러 빌드를 통합적으로 관리하기 위해 등장한 것이 바로 <strong>Composite Builds</strong>다.</p>
<p>이 Composite Builds에 포함된 빌드를 <strong>included build</strong>라고 한다.</p>
<p>이러한 Composite Builds를 설정하는 방법은 간단하다. <code>settings.gradle.kts</code> 파일에 포함하고 싶은 빌드의 위치를 명시해주면 된다.</p>
<p>안드로이드를 예시로 들면 루트 프로젝트의 <code>settings.gradle.kts</code>에 다음과 같이 빌드를 <code>includeBuild</code>로 추가하면 된다.</p>
<pre><code class="language-kotlin">pluginManagement {
    includeBuild(&quot;build-logic&quot;)
    repositories {
        google {
            content {
                includeGroupByRegex(&quot;com\\.android.*&quot;)
                includeGroupByRegex(&quot;com\\.google.*&quot;)
                includeGroupByRegex(&quot;androidx.*&quot;)
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}</code></pre>
<p>지금까지 Composite Builds를 통해 컨벤션 플러그인이 어떻게 동작하는지 기본적인 지식을 알았다.
물론 이것만으로는 아직 컨벤션 플러그인을 이해하기엔 부족하다.
이번에는 플러그인에 대해 알아보자.</p>
<h2 id="plugin">Plugin</h2>
<p>일단 플러그인이 무엇인가 살펴보면 Gradle에서는 다음과 같이 정의한다.</p>
<blockquote>
<p>A plugin is a reusable piece of software that provides additional functionality to the Gradle build system.</p>
</blockquote>
<p>한국어로 직역하면 Gradle 빌드 시스템에 추가적인 기능을 제공하는 재사용가능한 소프트웨어 조각이라고 한다.
플러그인은 크게 3종류가 있다.</p>
<ul>
<li>Script Plugins</li>
<li>Precompiled Script Plugins</li>
<li>BinaryPlugins
각각을 알아보자<h3 id="script-plugins">Script Plugins</h3>
이것은 Gradle에서도 추천하지 않는 방식이다.
그 이유는 일단 재사용성이라는 이점으로 플러그인을 사용하는데 재사용성이 그리 좋지 않기 때문이다.
하지만 동작 방식을 아는 것은 중요한데 왜냐하면 우리가 최종적으로 수행할 방식과 유사한 부분이 있기 때문이다.</li>
</ul>
<p>예시 코드를 살펴보자.</p>
<pre><code class="language-kotlin">// build.gradle.kts
// Define a plugin
class HelloWorldPlugin : Plugin&lt;Project&gt; {
    override fun apply(project: Project) {
        project.tasks.register(&quot;helloWorld&quot;) {
            group = &quot;Example&quot;
            description = &quot;Prints &#39;Hello, World!&#39; to the console&quot;
            doLast {
                println(&quot;Hello, World!&quot;)
            }
        }
    }
}

// Apply the plugin
apply&lt;HelloWorldPlugin&gt;()</code></pre>
<p><code>HelloWorldPlugin</code>라는 직접 정의한 플러그인으로 <code>Plugin&lt;Project&gt;</code>를 구현한다.
그렇기 때문에 <code>apply</code>를 override해주어야한다.
내부는 간단하다.
그냥 helloWorld라는 task를 <code>register</code>로 등록해주는 것이 전부다.</p>
<pre><code class="language-Bash">./gradlew helloWorld</code></pre>
<p>로 확인해보면 Hello World가 출력된다.
하지만 이것은 빌드 스크립트에 직접 적는 inline 방식으로 적용된다.</p>
<h3 id="precompiled-script-plugins">Precompiled Script Plugins</h3>
<p>직접 적는 거면 굳이 쓸 필요가 있을까?
그렇기에 우리는 사전에 컴파일된 플러그인을 사용해야한다.
일단 Gradle에서 어떻게 설명하는지를 보자.</p>
<blockquote>
<p>Precompiled script plugins are Groovy DSL or Kotlin DSL scripts compiled and distributed as Java class files packaged in some library.
They are meant to be consumed as a binary Gradle plugin, so they are applied to a project using the <code>plugins {}</code> block.</p>
</blockquote>
<p>한국말로 번역하면 다음과 같다.</p>
<blockquote>
<p>Precompiled script plugin은 <code>.gradle.kts</code> 또는 <code>.gradle</code> 확장자를 가진 Gradle 스크립트 파일을 미리 컴파일(Precompiled)하여, 다른 프로젝트에서 바이너리(binary) 라이브러리처럼 사용할 수 있도록 만든 Gradle 플러그인입니다.</p>
</blockquote>
<p>예시 코드를 살펴보자</p>
<pre><code class="language-kotlin">// plugin/src/main/kotlin/my-plugin.gradle.kts
// This script is automatically exposed to downstream consumers as the `my-plugin` plugin
tasks {
    register(&quot;myCopyTask&quot;, Copy::class) {
        group = &quot;sample&quot;
        from(&quot;build.gradle.kts&quot;)
        into(&quot;build/copy&quot;)
    }
}</code></pre>
<p>위와 같이 플러그인을 정의한 후</p>
<pre><code class="language-kotlin">// consumer/build.gradle.kts
plugins {
    id(&quot;my-plugin&quot;) version &quot;1.0&quot;
}</code></pre>
<p>사용할 모듈의 <code>build.gradle.kts</code>의 <code>plugins</code>에 <code>.gradle.kts</code>를 뺀 이름으로 추가하면 사용할 수 있다.</p>
<p>이렇게 보면 감이 잘 안 올수 있다.
조금 더 익숙한 예시를 보자.</p>
<pre><code class="language-kotlin">// plugin/src/main/kotlin/my-plugin.gradle.kts
// 1. 어떤 플러그인들을 기반으로 할지 먼저 적용
plugins {
    id(&quot;com.android.library&quot;)
    id(&quot;org.jetbrains.kotlin.android&quot;)
}

// 2. 안드로이드 관련 공통 설정
android {
    compileSdk = 34

    defaultConfig {
        minSdk = 26
        testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

// 3. 공통으로 사용할 라이브러리 추가
dependencies {
    implementation(&quot;androidx.core:core-ktx:1.13.1&quot;)
    implementation(&quot;androidx.appcompat:appcompat:1.7.0&quot;)
    testImplementation(&quot;junit:junit:4.13.2&quot;)
}</code></pre>
<p>똑같이 자신이 사용할 플러그인을 정의한 후</p>
<pre><code class="language-kotlin">// consumer/build.gradle.kts
plugins {
    id(&quot;my-plugin&quot;)
}

dependencies {
    // 이 모듈에만 필요한 특별한 라이브러리만 추가
    implementation(&quot;com.google.android.material:material:1.12.0&quot;)
}</code></pre>
<p>사용할 모듈에 추가해주면 된다.</p>
<p>뭔가 슬슬 컨벤션 플러그인에 다가가는 게 느껴진다.
사실 이 방식으로도 컨벤션 플러그인을 구현할 수 있다.
실제로 Precompiled Plugin으로 <code>build-logic</code>을 구성하는 프로젝트도 쉽게 찾아볼 수 있기도 하고 말이다.</p>
<p>하지만 나는 신실한 nowinandroid 신봉자다.
nowinandroid는 마지막으로 설명할 방식으로 구현하였다.
그럼 플러그인의 남은 한 종류를 알아보자.</p>
<h3 id="binary-plugins">Binary Plugins</h3>
<blockquote>
<p>Binary plugins are compiled plugins typically written in Java or Kotlin DSL that are packaged as JAR files. They are applied to a project using the <code>plugins {}</code> block. They offer better performance and maintainability compared to script plugins or precompiled script plugins.</p>
</blockquote>
<p>간단히 설명하면 다음과 같다.</p>
<blockquote>
<p>Binary Plugins는 Java나 Kotlin DSL로 작성된 컴파일된 플러그인으로 script plugins이나 precompiled plugins보다 더 나은 성능과 유지보수성을 갖는다.</p>
</blockquote>
<p>예제 코드로 좀더 살펴보자.</p>
<pre><code class="language-kotlin">//plugin/src/main/kotlin/plugin/MyPlugin.kta
class MyPlugin : Plugin&lt;Project&gt; {
    override fun apply(project: Project) {
        project.run {
            tasks {
                register(&quot;myCopyTask&quot;, Copy::class) {
                    group = &quot;sample&quot;
                    from(&quot;build.gradle.kts&quot;)
                    into(&quot;build/copy&quot;)
                }
            }
        }
    }
}</code></pre>
<p>익숙한 모양새이다. Script Plugins처럼 직접 플러그인을 정의한 후</p>
<pre><code class="language-kotlin">//consumer/build.gradle.kts
plugins {
    id(&quot;my-plugin&quot;) version &quot;1.0&quot;
}</code></pre>
<p>precompiled plugins처럼 추가해주면 된다.</p>
<p>그렇다면 Script Plugin과 Binary Plugin은 뭐가 다르길래 똑같이 생겨놓고 이런 차이가 생기는 걸까?</p>
<p>바로 <code>build.gradle.kts</code>에 직접 코드를 작성하는 것과, 별도의 프로젝트 폴더(src/main/kotlin)에 작성하는 것은 Gradle이 코드를 처리하는 방식 자체를 완전히 바꾸기 때문이다.</p>
<p>Gradle 공식문서에서는 차이점을 다음과 같이 서술한다.</p>
<blockquote>
<ul>
<li>A binary plugin is compiled into bytecode, and the bytecode is shared.</li>
</ul>
</blockquote>
<ul>
<li>A script plugin is shared as source code, and it is compiled at the time of use.</li>
</ul>
<p>즉, Binary Plugin은 사전에 바이트코드로 컴파일되고 그 바이트코드가 공유되며 필요한 곳에서는 바로 실행만 하면 되는 것이며 Script Plugin은 소스코드가 공유되고 사용될 때 그곳에서 즉석으로 컴파일된다는 차이가 있다.</p>
<p><a href="https://docs.gradle.org/current/userguide/plugins.html">Gradle</a>에서도 최종적으로는 Binary Plugin을 사용하는 것을 권장한다.</p>
<p>지금까지 그러면 Composite Builds와 플러그인에 대해 알아보았다.</p>
<p>이제는 슬슬 컨벤션 플러그인을 어떻게 작성하면 될지 감이 올 것이다.
본격적으로 컨벤션 플러그인을 만들어보자.</p>
<h2 id="컨벤션-플러그인">컨벤션 플러그인</h2>
<p>지금까지의 내용을 조합하면 컨벤션 플러그인은 Composite Build이고 사전에 만들어놓은 플러그인을 미리 컴파일한 뒤 그것을 공유함으로서 플러그인과 라이브러리 관리를 용이하게 하는 것이다.</p>
<h3 id="build-logic-생성">build-logic 생성</h3>
<p>그럼 가장 먼저 Composite Build 구조를 세팅하자.
<strong>Composite Buids</strong>에서의 예시코드와 동일하게 <code>build-logic</code>이라는 자체 빌드를 생성해준다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/dcc06ae2-0858-4f1a-81a6-5db993491bc8/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/36baa87d-b634-43ca-a330-2fed7c1d5619/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>이때는 Java or Kotlin 모듈로 생성해주는데 이때는 아직 별도의 빌드가 아닌 모듈이기에 <code>settings.gradle.kts</code>가 없다.
그렇기에 이것을 직접 추가해준다.
내용은 루트 프로젝트의 <code>settings.gradle.kts</code>를 복붙해주면 된다.
여기서 만약 <code>build-logic</code>의 <code>gradle.properties</code>를 별도로 설정하고 싶다면 추가해줘도 된다.</p>
<p>이렇게 하면 <code>settings.gradle.kts</code>에 Code insight unavailable이라고 Load Script Configurations을 누르라고 하는데 막상 눌러도 해결되지 않을 것이다.</p>
<p>이것은 처음 모듈을 만들 때 메인 프로젝트의 <code>settings.gradle.kts</code>에 모듈로서 추가되었기 때문이다.
하지만 우리는 이것을 별도의 빌드로서 <code>includeBuild</code>로 추가해주어야 한다.
그러니 본 프로젝트의 <code>settings.gradle.kts</code>에서 자동으로 추가된</p>
<pre><code class="language-kotlin">include(&quot;:build-logic:convention&quot;)</code></pre>
<p>을 제거하고 
<code>pluginManagement</code>에 <code>includeBuild(&quot;build-logic&quot;)</code>를 추가해준다.</p>
<h3 id="커스텀-플러그인-생성">커스텀 플러그인 생성</h3>
<p>이제 본인이 만들고 싶은 커스텀 플러그인을 만들자.
이 글에서는 Hilt에 필요한 플러그인과 라이브러리를 한번에 관리할 수 있는 HiltConventionPlugin을 만들 것이다.</p>
<p>그전에 먼저 버전 카탈로그를 다른 빌드인 <code>build-logic</code>에서도 사용하기 위한 세팅을 해주자.
<code>build-logic</code>의 <code>settings.gradle.kts</code>의 <code>dependencyResolutionManagement</code>에 아래와 같이 버전 카탈로그를 설정해준다.</p>
<pre><code class="language-kotlin">dependencyResolutionManagement {
    repositories {
        google {
            content {
                includeGroupByRegex(&quot;com\\.android.*&quot;)
                includeGroupByRegex(&quot;com\\.google.*&quot;)
                includeGroupByRegex(&quot;androidx.*&quot;)
            }
        }
        mavenCentral()
    }
    versionCatalogs {
        create(&quot;libs&quot;) {
            from(files(&quot;../gradle/libs.versions.toml&quot;))
        }
    }
}</code></pre>
<p>이후 <code>convention</code>모듈의 <code>build.gradle.kts</code>에 다음과 같이 작성해준다.</p>
<pre><code class="language-kotlin">plugins {
    `kotlin-dsl`
}

group = &quot;com.example.conventionplugin.buildlogic&quot;

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
    compilerOptions {
        jvmTarget = JvmTarget.JVM_17
    }
}

dependencies {
    compileOnly(&quot;com.android.tools.build:gradle:8.12.1&quot;) 
    //AGP 버전은 stable한 것으로 하자.
    //2025-08-24 기준 최신 버전은 
    //lint와의 충돌이슈가 있어 8.10.1을 사용하는 것을 권장한다.
}</code></pre>
<p>그다음에 <code>convention</code> 모듈에 <code>ProjectExtensions</code>라는 파일을 추가한다.
사실 이름이 저럴 필요는 없지만 nowinandroid는 위의 이름으로 되어있다. nowinandroid를 따라서 나쁠 건 없지 않을까 싶다ㅎㅎ;;
이 <code>ProejctExtensions</code>에 아래와 같이 작성해준다.</p>
<pre><code class="language-kotlin">import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

val Project.libs
    get(): VersionCatalog = extensions.getByType&lt;VersionCatalogsExtension&gt;().named(&quot;libs&quot;)</code></pre>
<p>이러면 이제 버전 카탈로그를 <code>libs</code>로 간단히 사용할 수 있다.
이제 <code>HiltConventionPlugin</code>을 작성해보자.</p>
<pre><code class="language-kotlin">class HiltConventionPlugin : Plugin&lt;Project&gt; {
    override fun apply(target: Project) {
        with(target) {
            apply(plugin = &quot;com.google.devtools.ksp&quot;)
            apply(plugin = &quot;dagger.hilt.android.plugin&quot;)

            dependencies {
                &quot;ksp&quot;(libs.findLibrary(&quot;hilt.compiler&quot;).get())
                &quot;implementation&quot;(libs.findLibrary(&quot;hilt.android&quot;).get())
            }
        }
    }
}</code></pre>
<p>이건 nowinandroid를 기반으로 간단하게 작성한 것이다.
이것에 hilt를 완전히 전담하게 하고 싶다면 </p>
<pre><code class="language-kotlin">&quot;implementation&quot;(libs.findLibrary(&quot;androidx.hilt.navigation.compose&quot;).get())</code></pre>
<p>이것도 추가해주면 된다.
nowinandroid는 별도의 feature용 플러그인에 추가한다.
본인의 프로젝트에 맞게 설정해주자.</p>
<h3 id="플러그인-등록">플러그인 등록</h3>
<p>이제 이렇게 작성한 플러그인을 등록해줘야 한다.
<code>convention</code>모듈의 <code>build.gradle.kts</code>의 맨 아래 자신의 플러그인을 등록해준다.</p>
<pre><code class="language-kotlin">dependencies {
...
}
gradlePlugin {
    plugins {
        register(&quot;hilt&quot;) {
            id = libs.plugins.custom.hilt.get().pluginId
            implementationClass = &quot;HiltConventionPlugin&quot;
        }
    }
}</code></pre>
<ul>
<li><code>id</code> : 등록할 플러그인의 아이디 설정, 버전 카탈로그에 본인이 사용할 아이디 <pre><code class="language-toml">[plugins]
android-application = { id = &quot;com.android.application&quot;, version.ref = &quot;agp&quot; }
kotlin-android = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }
kotlin-compose = { id = &quot;org.jetbrains.kotlin.plugin.compose&quot;, version.ref = &quot;kotlin&quot; }
android-library = { id = &quot;com.android.library&quot;, version.ref = &quot;agp&quot; }
jetbrains-kotlin-jvm = { id = &quot;org.jetbrains.kotlin.jvm&quot;, version.ref = &quot;jetbrainsKotlinJvm&quot; }
</code></pre>
</li>
</ul>
<p>custom-hilt = { id = &quot;custom.hilt&quot; }</p>
<pre><code>본인이 하고 싶은 플러그인 아이디를 설정한다.
+ `implementationClass` : 등록할 플러그인 파일 이름

이제 생성과 등록을 모두 완료하였으니 사용하고 싶은 모듈에 추가해서 사용하면 된다.
```kotlin
// app/build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.custom.hilt)
}</code></pre><h2 id="마무리">마무리</h2>
<p>지금까지 컨벤션 플러그인을 만드는 방법을 알아보았다.</p>
<p><code>Project</code>의 <code>CommonExtension</code>로 아예<code>buildConfig</code>나 <code>compileOptions</code>등을 관리할 수도 있다.
이 부분은 다루지 않았지만 지금까지의 내용으로 다른 코드를 보고 자신에게 필요한 build-logic을 구성할 수 있을 것이다.</p>
<p>하지만 단순히 작성할 줄 아는 정도로는 조금 아쉽다.
다음으로는 <code>Project</code>를 비롯한 빌드에 관련된 것들을 좀 더 뒤집어 까볼 예정이다.</p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://docs.gradle.org/current/userguide/plugins.html">https://docs.gradle.org/current/userguide/plugins.html</a>
<a href="https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html">https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html</a>
<a href="https://docs.gradle.org/current/userguide/intro_multi_project_builds.html">https://docs.gradle.org/current/userguide/intro_multi_project_builds.html</a>
<a href="https://docs.gradle.org/current/userguide/composite_builds.html#composite_builds">https://docs.gradle.org/current/userguide/composite_builds.html#composite_builds</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Serializable vs Parcelize in Compose]]></title>
            <link>https://velog.io/@couch_potato/Android-Serializable-vs-Parcelize-in-Compose</link>
            <guid>https://velog.io/@couch_potato/Android-Serializable-vs-Parcelize-in-Compose</guid>
            <pubDate>Mon, 04 Aug 2025 05:20:12 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>안드로이드 앱을 만들다보면 <code>@Serializable</code>을 사용하는 경우가 굉장히 많다.   </p>
<p>하지만 직렬화 인터페이스는 <code>Serializable</code>만 있는 것이 아니다.<br><code>Parcelable</code>도 존재한다.</p>
<p>비교에 들어가기 전 두 인터페이스의 차이를 간단하게 설명하자면
<code>Serializable</code>은 리플렉션을 사용해 직렬화를 수행하고
<code>Parcelable</code>은 그렇지 않아 속도가 빠르다는 이점이 있다.   </p>
<p>그렇다면 무조건 <code>Parcelable</code>을 사용하는 것이 좋지 않을까 싶지만 그렇지 않다.</p>
<p>이것은 두 인터페이스의 용도가 다르기 때문이다.</p>
<h3 id="parcelable과-serializable">Parcelable과 Serializable</h3>
<p>일반적으로 <code>Serializable</code>를 dto나 Entity에서 흔히 사용하는데 <code>Serializable</code>은 표준 자바 인터페이스로 서버와 안드로이드 클라이언트 간 데이터 모델을 공유하기 쉽다. </p>
<p>또한, <code>Retrofit</code> 같은 네트워크 통신 라이브러리들이 JSON 변환을 기본적으로 지원하여 서버와의 통신에 사용하기 편리하다. </p>
<p>즉, 두 인터페이스는 용도가 다르다.</p>
<ul>
<li><code>Serializable</code> : 서버와의 통신, Java 표준 인터페이스라서 서버 개발자와 안드로이드 개발자가 동일한 모델을 공유 가능</li>
<li><code>Parcelable</code> : 안드로이드 SDK 특화, 안드로이드 컴포넌트 간에 데이터를 효율적으로 전달할 때 사용, Intent에 데이터를 추가할 때, Bundle을 사용할 때 사용</li>
</ul>
<p>하지만 안드로이드 컴포넌트 간에 데이터 전달에도 편의상 <code>Serializable</code>을 사용하는 경우가 많다.</p>
<p>본인도 그냥 <code>Serializable</code> 하나만 써도 되지 않을까 싶은 의문이 들어 두 인터페이스의 성능을 비교해보고자 한다.</p>
<p>이미 이것을 비교한 <a href="https://medium.com/@limgyumin/parcelable-vs-serializable-%EC%A0%95%EB%A7%90-serializable%EC%9D%80-%EB%8A%90%EB%A6%B4%EA%B9%8C-bc2b9a7ba810">좋은 글</a>이 있다.
하지만 이것은 2018년에 작성되었고 코틀린, Compose환경도 아닌만큼 <code>Parcelize</code>가 추가된 현재는 어떤 차이가 있는지 알아보자.</p>
<h2 id="serializable">Serializable</h2>
<pre><code class="language-kotlin">@Serializable
data class Person(
    val name: String,
    val age: Int
)
    val person = Person(SAMPLE_NAME, SAMPLE_AGE)

    val timeNs = measureNanoTime {
        val serialized = json.encodeToString(person)
        val restored = json.decodeFromString&lt;Person&gt;(serialized)
    }

    val timeMs = timeNs / 1_000_000.0
    Log.d(&quot;Test&quot;, &quot;1. JSON Serializable: $timeNs ns (${String.format(&quot;%.3f&quot;, timeMs)} ms)&quot;)</code></pre>
<p>위와 같이 간단한 data class를 직렬화 역직렬화를 해보자.</p>
<p>테스트 결과 다음과 같은 시간이 걸렸다.
<img src="https://velog.velcdn.com/images/couch_potato/post/d085dd62-5a28-4d90-9ef7-ea3788377b35/image.png" alt=""></p>
<h2 id="parcelize">Parcelize</h2>
<pre><code class="language-kotlin">@Parcelize
data class Person(
    val name: String,
    val age: Int
) : Parcelable</code></pre>
<p>동일한 data class를 <code>@Parcelize</code> 어노테이션으로만 바꿔준다.</p>
<pre><code class="language-kotlin">val person = Person(SAMPLE_NAME, SAMPLE_AGE)

    val timeNs = measureNanoTime {
        val parcel = Parcel.obtain()
        parcel.writeParcelable(person, 0)
        parcel.setDataPosition(0)
        val restored = parcel.readParcelable&lt;Person&gt;(Person::class.java.classLoader)!!
        parcel.recycle()
    }

    val timeMs = timeNs / 1_000_000.0
    Log.d(&quot;Test&quot;, &quot;2. Parcelize Parcelable: $timeNs ns (${String.format(&quot;%.3f&quot;, timeMs)} ms)&quot;)</code></pre>
<p>테스트 결과 아래와 같은 시간이 걸렸다.
<img src="https://velog.velcdn.com/images/couch_potato/post/091e5503-c50b-4352-8c20-02dc69d76d18/image.png" alt=""></p>
<p>유의미할 정도로 큰 속도 차이가 존재한다!</p>
<p>이전의 <code>Parcelable</code> 인터페이스는 직접 직렬화/역직렬화 코드를 작성해야했지만 <code>Parcelize</code>는 그것도 필요없다.</p>
<h2 id="custom-serializable">Custom Serializable</h2>
<pre><code class="language-kotlin">@Serializable
data class Person(
    val name: String,
    val age: Int
) {
    fun writeObject(out: ObjectOutputStream) {
        out.writeUTF(name)
        out.writeInt(age)
    }

    fun readObject(input: ObjectInputStream): Person {
        val name = input.readUTF()
        val age = input.readInt()
        return Person(name, age)
    }
}</code></pre>
<p>이번에는 직접 직렬화/역직렬화를 한 <code>Serializable</code>이다.</p>
<pre><code class="language-kotlin">val person = Person(SAMPLE_NAME, SAMPLE_AGE)

    val timeNs =  measureNanoTime {
        val byteOut = ByteArrayOutputStream()
        val objectOut = ObjectOutputStream(byteOut)
        person.writeObject(objectOut)
        objectOut.flush()
        val bytes = byteOut.toByteArray()

        val byteIn = ByteArrayInputStream(bytes)
        val objectIn = ObjectInputStream(byteIn)
        val restored = person.readObject(objectIn)
    }
    val timeMs = timeNs / 1_000_000.0
    Log.d(&quot;Test&quot;, &quot;3. Custom JSON Serializable: $timeNs ns (${String.format(&quot;%.3f&quot;, timeMs)} ms)&quot;)</code></pre>
<p>동일하게 직렬화/역직렬화를 수행해준다.
<img src="https://velog.velcdn.com/images/couch_potato/post/e777557f-d5ff-4244-bfd1-bdcba3a31bd7/image.png" alt="">기존의 <code>@Serializable</code>을 사용한 것보다 훨씬 빠르다.</p>
<p>하지만 이 또한 <code>@Parcelize</code>에 비하면 느리다.</p>
<h2 id="결론">결론</h2>
<p>현재는 <code>Parcelable</code> 인터페이스도 직접 로직을 구현할 필요가 없어진만큼 안드로이드 컴포넌트간의 데이터 전달에는 <code>@Parcelize</code>를 사용하는 것이 성능상으로 굉장한 이점을 갖기에 되도록이면         <code>@Parcelize</code>를 사용해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coil vs Glide in Compose]]></title>
            <link>https://velog.io/@couch_potato/Android-Coil-vs-Glide-in-Compose</link>
            <guid>https://velog.io/@couch_potato/Android-Coil-vs-Glide-in-Compose</guid>
            <pubDate>Wed, 19 Feb 2025 03:04:05 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>안드로이드 앱을 만들다보면 이미지url을 사용하는 경우가 많다.
이미지url을 사용하는 방법도 Coil, Glide 등 굉장히 다양하다.
그렇다면 무엇을 사용하는 게 좋을까? 라는 의문이 자연스럽게 생긴다.
그래서 Coil과 Glide를 차이를 찾아보던 중 <a href="https://techblog.lotteon.com/glide-vs-coil-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%82%AC%EC%9A%A9%EB%9F%89-%EB%B9%84%EA%B5%90-cb93cffb9fc0">롯데ON 기술블로그</a>에서 좋은 글을 발견했다.</p>
<p>그러나 위 글이 작성된 시점은 2022년이다.
현재 Coil이 보다 안정화되고 Compose가 보다 활성화된 현재 시점에서 주로 쓰는 <code>AsyncImage()</code>로 Compose 환경에서의 차이를 한번 확인해보고 싶어졌다.</p>
<h1 id="테스트-환경">테스트 환경</h1>
<ul>
<li>기기 : 삼성 갤럭시 S24</li>
<li>Coil 버전 : 2.2.2</li>
<li>Glide 버전(Compose) : 1.0.0-alpha.1</li>
<li>이미지는 원본이미지 사용</li>
</ul>
<img src="https://velog.velcdn.com/images/couch_potato/post/80050f5c-c1a5-42d7-8472-5ed10c139c5d/image.png" width="30%">


<h1 id="테스트-절차">테스트 절차</h1>
<p>Coil과 Glide를 사용해서 각각 이미지를 불러오고 이를 Android Studio Profiler로 확인해준다.</p>
<ul>
<li>처음 앱을 실행했을 경우(앱 제거 후 재설치)</li>
<li>캐시가 저장된 후 실행했을 경우</li>
</ul>
<p>두 경우의 상황에서 각각 Profiler를 통해 검사해준다.</p>
<p>시점은 <strong>앱 실행 후 15초 지점</strong>으로 몇번의 테스트 결과 이 상황에서 안정값과 이전 값의 비교를 확인하기 용이했다.</p>
<h1 id="테스트-코드">테스트 코드</h1>
<ol>
<li>Coil<pre><code class="language-kotlin">@Composable
fun LoadImage(imageUrl: String) {
 AsyncImage(
     model = imageUrl,
     contentDescription = &quot;Coil Image&quot;
 )
}</code></pre>
</li>
<li>Glide<pre><code class="language-kotlin">@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun LoadImage(imageUrl: String) {
 GlideImage(
     model = imageUrl,
     contentDescription = &quot;Glide Image&quot;
 )
}</code></pre>
<h1 id="테스트-실행">테스트 실행</h1>
<h2 id="coil">Coil</h2>
<h3 id="처음-실행">처음 실행</h3>
<img src="https://velog.velcdn.com/images/couch_potato/post/ae913dcd-c116-48e9-af1b-515958fd8145/image.png" alt=""><h3 id="캐시가-저장된-후">캐시가 저장된 후</h3>
<img src="https://velog.velcdn.com/images/couch_potato/post/0b0901c3-c6d7-498c-8d22-9b1acfab16a2/image.png" alt=""></li>
</ol>
<h2 id="glide">Glide</h2>
<h3 id="처음-실행-1">처음 실행</h3>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/841c2231-0ae4-44a1-80c7-3401d50b6aa6/image.png" alt=""></p>
<h3 id="캐시가-저장된-후-1">캐시가 저장된 후</h3>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/0fbf18d5-0220-4228-85f1-513b2103b047/image.png" alt=""></p>
<h1 id="분석">분석</h1>
<ol>
<li><p>Coil은 비동기로 실행되는 만큼 실제 환경에서 이미지가 순차적으로 로딩되는 것을 확인할 수 있었다.
반면에 Glide는 모든 이미지가 한번에 로딩되는 것을 확인할 수 있었다.</p>
</li>
<li><p>캐시값이 없는 처음 실행하는 상황에서 Coil은 <strong>133.5MB</strong>, Glide는 <strong>141MB</strong>로 Coil이 메모리 사용량이 더 적었다.</p>
</li>
<li><p>캐시값이 저장된 후에는 Coil은 <strong>129.9MB</strong>, Glide는 <strong>119.8MB</strong>로 Glide가 메모리 사용량이 더 적었다.</p>
</li>
<li><p>두 상황에서 Coil은 상대적으로 메모리 사용량에 대한 편차가 작았다.
 Coil의 메모리 사용량 차 : <strong>3.6MB</strong>
 Glide의 메모리 사용량 차 : <strong>21.2MB</strong></p>
</li>
</ol>
<h1 id="결론">결론</h1>
<p>✔️ Glide</p>
<ul>
<li>반복되는 이미지 사용이 많아 캐시 데이터 사용이 많을 경우</li>
</ul>
<p>✔️ Coil</p>
<ul>
<li>다양한 이미지를 사용해서 캐시의 사용이 어려울 경우</li>
<li>어느 상황이던간에 일정한 성능이 필요한 경우</li>
</ul>
<p>물론 Glide는 View기반이고 Coil이 보다 Compose 친화적으로 최근에는 Coil을 추천하는 추세이다.
그렇지만 상황에 따라 필요한 라이브러리를 적절히 사용하는 것이 좋아보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Hilt를 왜 쓸까?]]></title>
            <link>https://velog.io/@couch_potato/Android-Hilt%EB%A5%BC-%EC%99%9C-%EC%93%B8%EA%B9%8C</link>
            <guid>https://velog.io/@couch_potato/Android-Hilt%EB%A5%BC-%EC%99%9C-%EC%93%B8%EA%B9%8C</guid>
            <pubDate>Thu, 06 Feb 2025 00:37:53 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>기업들의 기술 스택 요구사항을 보면 늘 <code>Hilt</code>가 빠지지 않는다.
경험이 많지 않은 나는 아직까지 <code>Hilt</code>의 필요성을 느끼지 못했고 &#39;굳이 의존성 주입을 해야하나?&#39; 라는 생각도 했었다.
또한 <code>Hilt</code>를 사용해본적이 없어서 언제 어느 상황에서 <code>Hilt</code>를 사용해야 할지 감도 안 왔었다.
최근 NowInAndroid 클론앱을 만들며 안드로이드를 공부하면서 <code>Hilt</code>를 왜 쓰는 지 알아보고자 한다.</p>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/721dcd9f-bd44-4b8b-bc9a-bd208a0aaa0e/image.png" alt=""></p>
<p>NIA는 위의 아키텍처 패턴을 따르고 있고 나도 최대한 이를 따르고자 하였다.
Data Layer에 속하는 <code>Repository</code>가 저장소에서 값을 받아오면 UI Layer의 <code>ViewModel</code>에서는 <code>Repository</code>를 호출해 데이터를 가져오고, Compose에서 관찰할 수 있도록 상태를 관리하는 구조로 설계 후 구현을 시작했다.</p>
<h2 id="hilt-적용-이전">Hilt 적용 이전</h2>
<pre><code class="language-kotlin">class UserDataRepository {
    private val firestore = FirebaseFirestore.getInstance()

    suspend fun getUser(userId: String): User? = suspendCancellableCoroutine { cont -&gt;
        firestore.collection(&quot;users&quot;).document(userId).get()
            .addOnSuccessListener { document -&gt;
                val user = document.toObject(User::class.java)
                cont.resume(user) { }
            }
            .addOnFailureListener { exception -&gt;
                cont.resumeWithException(exception)
            }
    }
}</code></pre>
<p><code>UserDataRepository</code>에서 FirebaseFirestore를 직접 인스턴스화하여 Firestore에 저장된 user정보를 받아온다.</p>
<pre><code class="language-kotlin">class UserViewModel(private val repository: UserRepository) : ViewModel() {

    private val _user = mutableStateOf&lt;User?&gt;(null)
    val user: State&lt;User?&gt; = _user

    fun fetchUser(userId: String) {
        viewModelScope.launch {
            try {
                val userData = repository.getUser(userId)
                _user.value = userData
            } catch (e: Exception) {
                // 예외 처리 로직
            }
        }
    }
}</code></pre>
<p><code>UserViewModel</code>은 <code>UserRepository</code>를 매개변수로 호출해 데이터를 가져오고, Compose에서 관찰할 수 있도록 상태를 관리한다.</p>
<pre><code class="language-kotlin">@Composable
fun UserScreen(
    userId: String,
    viewModel: UserViewModel = viewModel(factory = UserViewModelFactory(UserRepository()))
) {
    val user = viewModel.user.value

    LaunchedEffect(userId) {
        viewModel.fetchUser(userId)
    }

    Scaffold(
        topBar = {
            TopAppBar(title = { Text(text = &quot;User Profile&quot;) })
        }
    ) { paddingValues -&gt;
        //내부로직
    }
}</code></pre>
<p><code>View</code>인 <code>UserScreen</code>은 <code>UserViewModel</code>을 다음과 같은 팩토리로 받아온다.</p>
<pre><code class="language-kotlin">class UserViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory {
    @Suppress(&quot;UNCHECKED_CAST&quot;)
    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {
        if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            return UserViewModel(repository) as T
        }
        throw IllegalArgumentException(&quot;Unknown ViewModel class&quot;)
    }
}</code></pre>
<p><code>ViewModelProvider.Factory</code>는 안드로이드 MVVM 아키텍처에서 ViewModel을 생성할 때 커스텀 생성 로직(Repository 의존성 주입)을 적용할 수 있도록 해주는 인터페이스이다.</p>
<h3 id="hilt를-적용하지-않을-때의-어려움">Hilt를 적용하지 않을 때의 어려움</h3>
<p>기본적으로 안드로이드에서는 <code>ViewModelProvider</code>를 사용하여 ViewModel 인스턴스를 생성하는데 이것은
다음과 같은 기본 생성자로만 생성할 수 있다. </p>
<pre><code class="language-kotlin">val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)</code></pre>
<p>그런데 만약 ViewModel에 아래처럼 Repository같은 인자를 전달하거나 특정한 초기화 로직이 필요한 경우</p>
<pre><code class="language-kotlin">class UserViewModel(private val repository: UserRepository) : ViewModel() {
    // UserRepository를 받아야 하는 ViewModel
}</code></pre>
<p><code>ViewModelProvider(this).get(UserViewModel::class.java)</code>를 호출하면 안드로이드 프레임워크가 기본 생성자만 호출하려고 하기 때문에 에러가 발생한다.</p>
<p>기본 생성자만 호출하려는 이유는 다음과 같다고 한다.</p>
<blockquote>
<ul>
<li>ViewModelProvider는 내부적으로 ViewModel을 생성하고 OS가 상태를 저장하도록 관리함.</li>
</ul>
</blockquote>
<ul>
<li>기본 생성자가 없는 경우, OS는 ViewModel을 어떻게 초기화해야 할지 모름.</li>
<li>만약 액티비티/프래그먼트가 재생성될 때 ViewModel을 다시 만들어야 하는데, 개발자가 직접 생성한 인자를 기억할 방법이 없음.</li>
</ul>
<p>그렇기에 이런 경우 <code>ViewModelProvider.Factory</code>를 구현하여 커스텀 팩토리를 만들어야 한다.</p>
<p>그러나 프로젝트의 규모가 커질수록 ViewModel이 많아질 수 있고 이때마다 팩토리를 만들어줘야 한다는 어려움이 있다.
뿐만 아니라 Dao를 비롯한 아키텍처 구성 요소가 늘어나면 늘어날수록 일일이 의존성 관리를 하는 것은 너무도 어렵고 비효율적인 일이다.</p>
<p>여기서 <code>Hilt</code>의 필요성이 나온다.</p>
<h3 id="hilt-적용">Hilt 적용</h3>
<p>가장 먼저 해주어야할 것은 Hilt dependency를 추가하는 것이다.
이것은 <a href="https://developer.android.com/training/dependency-injection/hilt-android?hl=ko">Android Developers</a>의 설명을 따라주면 된다.</p>
<pre><code class="language-kotlin">@HiltAndroidApp
class MyApplication : Application() {
    // 초기화 작업 등 필요한 코드 작성
}</code></pre>
<p><code>Application</code> 클래스를 만들어 app의 최상위에 <code>@HiltAndroidApp</code> 어노테이션을 추가한다.</p>
<pre><code class="language-kotlin">class MainActivity : ComponentActivity() {
    // Hilt가 필요한 의존성 주입 처리
}</code></pre>
<p>그리고 Activity나 Fragment에는 <code>@AndroidEntryPoint</code> 어노테이션을 추가해주어야 한다.
최상위에만 <code>@HiltAndroidApp</code>을 추가하면 되는 줄 알고 <code>MainActivity</code>에 추가했다가 에러가 발생했다.
반드시 Application을 상속한 클래스에 붙이자.</p>
<pre><code class="language-kotlin">@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {

    @Provides
    @Singleton
    fun provideFirebaseFirestore(): FirebaseFirestore {
        return FirebaseFirestore.getInstance()
    }
}</code></pre>
<p>FirebaseFirestore 인스턴스를 주입하는 모듈을 만들어준다.</p>
<pre><code class="language-kotlin">class UserRepository @Inject constructor(
    private val firestore: FirebaseFirestore
) {
    suspend fun getUser(userId: String): User? = suspendCancellableCoroutine { cont -&gt;
        firestore.collection(&quot;users&quot;).document(userId).get()
            .addOnSuccessListener { document -&gt;
                val user = document.toObject(User::class.java)
                cont.resume(user) { }
            }
            .addOnFailureListener { exception -&gt;
                cont.resumeWithException(exception)
            }
    }</code></pre>
<p>그리고 Repository 클래스에서 생성자 주입을 활용한다.
이렇게 할 경우 이전 코드에서 직접 인스턴스화했던 FirebaseFirestore를 모듈화하여 결합도를 낮출 수 있다.
또한 기존 코드는 FirebaseFirestore를 다른 클래스에서 사용려면 다른 클래스에서도 직접 인스턴스화를 해야 했으나 Hilt는 인스턴스를 직접 생성하지 않고 생성자 또는 필드 주입을 통해 외부에서 주입받는다.</p>
<pre><code class="language-kotlin">@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    // Compose에서 사용할 수 있도록 상태 관리
    private val _user = mutableStateOf&lt;User?&gt;(null)
    val user: State&lt;User?&gt; = _user

    fun fetchUser(userId: String) {
        viewModelScope.launch {
            try {
                val userData = repository.getUser(userId)
                _user.value = userData
            } catch (e: Exception) {
                // 에러 처리
            }
        }
    }
}</code></pre>
<pre><code class="language-kotlin">@Composable
fun UserScreen(userViewModel: UserViewModel = hiltViewModel()) {
    //내부로직
}</code></pre>
<p>이번엔 이전에 팩토리를 만들어주어야 했던 <code>UserViewModel</code>을 비교해보자
가장 큰 차이는 이전 코드와 달리 팩토리가 없다는 것이다.
별도로 의존성을 주입해주어야 했던 이전 코드와 다르게 <code>Hilt</code>를 사용하면 <code>@Inejct</code> 어노테이션을 통해 Hilt가 알아서 의존성을 주입해준다.
또한 <code>@HiltViewModel</code> 어노테이션과 생성자 주입을 사용하면 Hilt가 자동으로 ViewModel 인스턴스를 관리하고 Compose에서는 <code>hiltViewModel()</code>로 ViewModel을 쉽게 가져올 수 있다.</p>
<h2 id="결론">결론</h2>
<p>Hilt는 단순히 코드 작성이 편한 것 뿐만 아니라 Hilt 자체적으로 수명 주기를 관리하고 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성의 이점을 갖고있다.
직접 인스턴스를 생성할 때 생기는 강한 결합도를 낮추고 코드의 재사용성과 테스트 용이성, 유연성 그리고 확장성을 높이는 의존성 주입(DI)를 보다 간편하고 효율적으로 사용하고자 Hilt를 사용한다고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OOP] 객체지향 설계의 5가지 원칙 SOLID]]></title>
            <link>https://velog.io/@couch_potato/OOP-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID</link>
            <guid>https://velog.io/@couch_potato/OOP-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID</guid>
            <pubDate>Sat, 21 Dec 2024 08:56:04 GMT</pubDate>
            <description><![CDATA[<p>SOLID 원칙은 소프트웨어 개발시 보다 <strong>Understandable, Flexible, Maintainable</strong> 하도록 도와주는 5가지 원칙을 말한다.
즉 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 사용하는 원칙이다.</p>
<h1 id="solid">S.O.L.I.D</h1>
<p>SOLID는 5가지 원칙의 앞글자를 따서 약어로 만든 단어로 각 원칙은 다음과 같다,</p>
<ul>
<li><strong>S</strong>ingle-Reponsibility Principle(SRP)</li>
<li><strong>O</strong>pen Close Principle(OCP)</li>
<li><strong>L</strong>iskov Subsituation Principle(LSP)</li>
<li><strong>I</strong>nterface Segregation Principle(ISP)</li>
<li><strong>D</strong>ependency Inversion Principle(DIP)</li>
</ul>
<h1 id="single-responsibility-principle">Single-Responsibility Principle</h1>
<p>Single-Responsibility Principle(단일 책임 원칙)은 한 마디로 다음과 같다.</p>
<blockquote>
<p>** 객체는 한 가지 책임/역할만 가져야 한다.**</p>
</blockquote>
<p>즉 객체가 변경될 경우 하나의 이유로만 변경이 되어야 한다고 볼 수 있다.</p>
<h3 id="왜-지켜야-하는가">왜 지켜야 하는가?</h3>
<p>객체가 모든 일을 다하면 편하고 좋을텐데 왜 책임을 분리할까?
그 이유는 만약 객체가 너무도 많은 책임을 가지고 있을 때 객체의 크기가 굉장히 커짐
-&gt; <strong>해당 객체에서 문제가 발생했을 때 연쇄적인 변화, side effect가 커진다.</strong></p>
<h3 id="어떻게-적용할-수-있을까">어떻게 적용할 수 있을까?</h3>
<p>추상화를 통해 혼재된 책임을 각각의 책음으로 분리하여 <strong>하나의 책임만 갖도록 설계</strong>한다.
SRP를 통해 다음과 같은 문제점을 해결할 수 있다.</p>
<ul>
<li><strong>Divergent Change</strong></li>
<li><blockquote>
<p>떨어뜨려야 할 것을 모아둘 때 발생하는 문제로 <strong>혼재된 책임을 각각의 개별 클래스로 분할하여 하나의 책임만을 갖도록 하여 해결</strong></p>
</blockquote>
</li>
<li><strong>Shotgun Surgery</strong></li>
<li><blockquote>
<p>모아둬야 할 것이 산발적으로 분포되어 있을 때 발생하는 문제로** 책임을 기존의 클래스에 모으거나 새로운 크래스를 만들어 해결**</p>
</blockquote>
</li>
</ul>
<h1 id="open-close-principle">Open Close Principle</h1>
<p>Open Close Principle(개방폐쇄원칙)은 한 마디로 다음과 같다.</p>
<blockquote>
<p><strong>객체는 확장에 열려있고 변경에는 닫혀있어야 한다.</strong></p>
</blockquote>
<p>그렇다면 확장에 열려있다는 것은 무엇이고 변경에 닫혀있다는 것은 무엇일까?</p>
<ul>
<li><strong>확장에 열려있다</strong> : 객체의 행위가 확장될 수 있다. 행위를 추가해 객체가 하는 일을 바꿀 수 있다.</li>
<li><strong>변경에 닫혀있다</strong> : 객체의 확장에 기존 구성요소의 수정이 일어나지 말아야 한다.</li>
</ul>
<h3 id="왜-지켜야-하는가-1">왜 지켜야 하는가?</h3>
<p>기능을 확장할 때마다 기존의 코드를 수정해야 한다면 비용이 너무 많이 소모된다.
-&gt; <strong>기존 코드를 변경하지 않고</strong> 쉽게 확장할 수 있어 재사용성과 유연성과 같은 <strong>객체 지향의 장점을 극대화</strong></p>
<h3 id="어떻게-적용할-수-있을까-1">어떻게 적용할 수 있을까?</h3>
<p>다음의 단계를 통해 OCP를 적용할 수 있다.</p>
<ol>
<li>변경(확장)할 것과 변하지 않을 것을 구분한다.</li>
<li>이 두 모듈이 만나는 지점에 인터페이스를 정의한다. (두 지점이 소통하는 부분)</li>
<li>구현에 의존하기 보다는 정의한 인터페이스를 의존하도록 코드를 작성한다.</li>
</ol>
<p>코드로 보면 다음과 같다.</p>
<pre><code class="language-kotlin">class PaymentProcessor {
    fun processPayment(method: String) {
        when (method) {
            &quot;CreditCard&quot; -&gt; println(&quot;CreditCard 결제&quot;)
            &quot;PayPal&quot; -&gt; println(&quot;PayPal 결제&quot;)
            else -&gt; println(&quot;Unknown payment method&quot;)
        }
    }
}</code></pre>
<p>위처럼 <code>PaymentProcessor</code>가 정의되어 있을 때 애플페이와 같은 새로운 결제 수단을 추가하려면 <code>when</code>문에 새로운 조건을 추가해야 한다.</p>
<p>위 코드는 매우 간단하여 추가가 어렵지 않지만 프로그램이 거대해질수록 직접 추가하는 데에는 비용이 많이 소모된다.</p>
<p>그렇기에 변경할 부분인 결제수단과 변하지 않는 부분인 결제 그 자체로 구분하고 이것을 인터페이스로 정의하고 이를 의존하도록 수정해준다.</p>
<pre><code class="language-kotlin">interface PaymentMethod {
    fun pay()
}

class CreditCardPayment : PaymentMethod {
    override fun pay() {
        println(&quot;CreditCard 결제&quot;)
    }
}
class PayPalPayment : PaymentMethod {
    override fun pay() {
        println(&quot;PayPal 결제&quot;)
    }
}
class ApplePayPayment : PaymentMethod {
    override fun pay() {
        println(&quot;ApplePay 결제&quot;)
    }
}</code></pre>
<p>이렇게 수정하면 새로운 결제 수단을 추가하기에도 원활하고 기존의 결제처리방식을 수정할 때에도 해당 결제방식만 수정해주면 된다.</p>
<h1 id="liskov-substitution-principle">Liskov Substitution Principle</h1>
<p>Liskov Subsititution Principle(리스코프 치환 원칙)은 한 마디로 다음과 같다.</p>
<blockquote>
<p><strong>서브 타입은 항상 기반 타입으로 교체할 수 있어야 한다.</strong></p>
</blockquote>
<p>즉 <strong>서브 타입은 언제나 기반 타입과 호환</strong>될 수 있어야 하며(하위 클래스가 상위 클래스로 바뀌어도 문제 없어야함)
서브 타입은 <strong>기반 타입이 정해둔 약속을 지켜야 한다</strong>.
여기서 말하는 약속은 기반 타입이 정의한 public 인터페이스, 예외 처리등을 말한다.</p>
<h3 id="왜-지켜야-하는가-2">왜 지켜야 하는가?</h3>
<p><strong>LSP가 지켜지지 않는다는 것은 OCP가 지켜지지 않는다는 것과 같다.</strong>
OCP는 상속을 통해 지켜지고 LSP는 규약이 준수된 상속 구조를 보장한다.
그렇기에 LSP에서 주장하는 기반 타입과의 호환이 보장되지 않으면 적절한 수행이 어렵다.</p>
<h3 id="어떻게-적용할-수-있을까-2">어떻게 적용할 수 있을까?</h3>
<p>기반 타입과 서브 타입이 <code>IS-A 관계</code>를 가져야 한다.
예를 들면
&quot;정사각형 is a 직사각형이다&quot; -&gt; 어색하지 않음
&quot;정사각형의 높이 변경 is a 직사각형의 높이 변경&quot; -&gt; 어색함</p>
<p>코드로 보면 다음과 같다,</p>
<pre><code class="language-kotlin">// 부모 클래스 : 직사각형
open class Rectangle(var width: Int, var height: Int) {
    open fun setWidth(newWidth: Int) {
        width = newWidth
    }

    open fun setHeight(newHeight: Int) {
        height = newHeight
    }

    fun area(): Int {
        return width * height
    }
}

// 자식 클래스 : 정사각형
class Square(side: Int) : Rectangle(side, side) {
    override fun setWidth(newWidth: Int) {
        width = newWidth
        height = newWidth // 정사각형은 너비와 높이가 항상 같아야 함
    }

    override fun setHeight(newHeight: Int) {
        height = newHeight
        width = newHeight // 정사각형은 너비와 높이가 항상 같아야 함
    }
}

fun main() {
    val rectangle: Rectangle = Square(5) // 정사각형을 직사각형으로 취급

    rectangle.setWidth(10) // 너비 변경
    println(&quot;Width: ${rectangle.width}, Height: ${rectangle.height}&quot;) // 너비와 높이가 둘 다 변경됨 -&gt; 직사각형의 예상과 다름
}</code></pre>
<p>그렇다면 이 문제를 어떻게 해결할 수 있을까?</p>
<pre><code class="language-kotlin">open class Rectangle(var width: Int, var height: Int) {
    fun area(): Int {
        return width * height
    }
}

// 정사각형 클래스 정의 
class Square(side: Int) {
    private val rectangle = Rectangle(side, side) // 내부적으로 직사각형을 포함

    var side: Int
        get() = rectangle.width
        set(value) {
            rectangle.width = value
            rectangle.height = value 
        }

    fun area(): Int {
        return rectangle.area()
    }
}</code></pre>
<p>다양한 방법이 있겠지만 이런 식으로 
정사각형 <code>Has-A</code> 직사각형 
관계를 갖도록 수정해볼 수 있다.</p>
<h1 id="interface-segregation-principle">Interface Segregation Principle</h1>
<p>Interface Segregation Principle(인터페이스 분링 원칙)은 한 마디로 다음과 같다.</p>
<blockquote>
<p>인터페이스는 자신의 클라이언트가 사용할 메소드만 가지고 있어야 한다.</p>
</blockquote>
<p>만약 어떤 클래스를 이용하는 클라이언트가 여러 개고 이들이 해당 클래스의 특정 부분집합만을 이용한다면 이들을 따로 인터페이스로 분리한다.</p>
<h3 id="왜-지켜야-하는가-3">왜 지켜야 하는가?</h3>
<p>예를 들어 인터페이스가 너무도 비대해진다면 해당 인터페이스의 일정 부분만 필요하더라도 <strong>구현이 강제되고 변경에 영향을 받는다</strong>는 문제가 발생한다.</p>
<p>SRP가 클래스의 단일 책임을 강조한다면 ISP는 인터페이스의 단일 책임을 강조한다.</p>
<h3 id="어떻게-적용할-수-있을까-3">어떻게 적용할 수 있을까?</h3>
<p>하나의 큰 인터페이스를 여러 개의 작은 인터페이스로 나누어 각 클라이언트가 자신에게 필요한 인터페이스만 의존하도록 설계해야 한다.</p>
<p>코드로 보면 다음과 같다.</p>
<pre><code class="language-kotlin">interface Worker {
    fun work()
    fun eat()
    fun sleep()
}

class OfficeWorker : Worker {
    override fun work() {
        println(&quot;Office worker is working.&quot;)
    }

    override fun eat() {
        println(&quot;Office worker is eating.&quot;)
    }

    override fun sleep() {
        println(&quot;Office worker is sleeping.&quot;)
    }
}

class Robot : Worker {
    override fun work() {
        println(&quot;Robot is working.&quot;)
    }

    override fun eat() {
        throw UnsupportedOperationException(&quot;Robots do not eat.&quot;)
    }

    override fun sleep() {
        throw UnsupportedOperationException(&quot;Robots do not sleep.&quot;)
    }
}</code></pre>
<p>코드를 보면 <code>Robot</code>과 <code>OfficeWorker</code>는 모두 <code>Worker</code> 인터페이스를 구현하지만 <code>Robot</code>같은 경우에는 먹지도 자지도 않지만 불필요하게 구현해야 한다는 문제가 있다.
단순하게 이를 해결해보면</p>
<pre><code class="language-kotlin">interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

class OfficeWorker : Workable, Eatable, Sleepable {
    override fun work() {
        println(&quot;Office worker is working.&quot;)
    }

    override fun eat() {
        println(&quot;Office worker is eating.&quot;)
    }

    override fun sleep() {
        println(&quot;Office worker is sleeping.&quot;)
    }
}

class Robot : Workable {
    override fun work() {
        println(&quot;Robot is working.&quot;)
    }
}</code></pre>
<p>각각의 인터페이스를 분리하고 각 클래스가 필요한 기능만을 구현하는 방식으로 분리할 수 있다.</p>
<h1 id="dependency-inversion-principle">Dependency Inversion Principle</h1>
<p>Dependency Inversion Principle(의존성 역전 원칙)은 한 마디로 다음과 같다.</p>
<blockquote>
<p>구체적인 것이 추상적인 것에 의존해야 한다.</p>
</blockquote>
<p>상위 객체가 하위 객체에 의존하지 않고 추상화된 인터페이스를 통해서 상호작용하도록 의존성을 반전시킨다.</p>
<h3 id="왜-지켜야-하는가-4">왜 지켜야 하는가?</h3>
<p>상위 객체가 하위 객체에 의존을 가진다면 요구사항이 변경됨에 따라 <strong>기능의 확장 및 변경, 재사용이 어렵다</strong>는 문제가 발생한다.</p>
<h3 id="어떻게-적용할-수-있을까-4">어떻게 적용할 수 있을까?</h3>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/5a402cd4-6f65-4aff-aca3-3a4052cbb702/image.png" alt="">
동일 레벨에서는 추상화에 의존하고 하위레벨이 상위 레벨의 추상화를 의존하게 만들어 관계를 역전시키는 방식으로 적용할 수 있다.
이렇게만 보면 이해가 조금 어렵다.</p>
<p>이해를 돕기위해 DIP가 지켜지지 않는 상황의 예시를 코드로 보면 다음과 같다.</p>
<pre><code class="language-kotlin">// 저수준 : 소리 알람
class SoundAlarm {
    fun ring() {
        println(&quot;Beep! Beep! 알람이 울립니다!&quot;)
    }
}

// 고수준 : 알람 시스템
class AlarmSystem {
    private val soundAlarm = SoundAlarm() // 저수준에 직접 의존

    fun triggerAlarm() {
        soundAlarm.ring() // 소리 알람만 작동 가능
    }
}

fun main() {
    val alarmSystem = AlarmSystem()
    alarmSystem.triggerAlarm()
}</code></pre>
<p>상위 개념인 알람 시스템이 하위 개념인 구체적인 소리 알람에 직접적으로 의존하여 소리 알람이 아닌 진동 알람과 같은 추가적인 확장이 어렵다.
이를 해결보면</p>
<pre><code class="language-kotlin">// 추상화 : 알람 인터페이스
interface Alarm {
    fun trigger()
}

// 저수준1 : 소리 알람
class SoundAlarm : Alarm {
    override fun trigger() {
        println(&quot;Beep! Beep! 알람이 울립니다!&quot;)
    }
}

// 저수준2 : 진동 알람
class VibrationAlarm : Alarm {
    override fun trigger() {
        println(&quot;Bzzzz! 진동 알람이 울립니다!&quot;)
    }
}

// 고수준 : 알람 시스템
class AlarmSystem(private val alarm: Alarm) {
    fun triggerAlarm() {
        alarm.trigger() // 추상화된 인터페이스를 통해 호출
    }
}

fun main() {
    // 소리 알람 사용
    val soundAlarmSystem = AlarmSystem(SoundAlarm())
    soundAlarmSystem.triggerAlarm()

    // 진동 알람 사용
    val vibrationAlarmSystem = AlarmSystem(VibrationAlarm())
    vibrationAlarmSystem.triggerAlarm()
}
</code></pre>
<p><code>AlarmSystem</code>은 <code>Alarm</code> 인터페이스만 의존하여 구체적인 알람에는 의존하지 않는다.
이렇게 구현할 경우 새로운 알람 방식에 대해서도 <code>AlarmSystem</code>코드를 수정할 필요없이 쉽게 추가할 수 있다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://www.nextree.co.kr/p6960/">https://www.nextree.co.kr/p6960/</a>
<a href="https://www.youtube.com/watch?v=WAufmN_ki4o&amp;list=PLp_FpnyDwvuA9vzOImAAn4COaa9yK71hd">https://www.youtube.com/watch?v=WAufmN_ki4o&amp;list=PLp_FpnyDwvuA9vzOImAAn4COaa9yK71hd</a>
<a href="https://www.youtube.com/watch?v=7c0tqHLfxlE">https://www.youtube.com/watch?v=7c0tqHLfxlE</a>
<a href="https://enhancement.tistory.com/5">https://enhancement.tistory.com/5</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] MVP, MVVM, 클린 아키텍쳐]]></title>
            <link>https://velog.io/@couch_potato/Kotlin-MVP-MVVM-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90</link>
            <guid>https://velog.io/@couch_potato/Kotlin-MVP-MVVM-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90</guid>
            <pubDate>Thu, 21 Nov 2024 17:17:06 GMT</pubDate>
            <description><![CDATA[<h1 id="아키텍처-패턴이란">아키텍처 패턴이란</h1>
<p>아키텍처 패턴은 소프트웨어 시스템의 구성 요소와 그 요소들 간의 관계를 정의하는 설계 원칙으로 소프트웨어의 구조와 동작을 정의하기 위한 고수준의 설계 청사진이다.</p>
<h2 id="아키텍처-패턴-장점">아키텍처 패턴 장점</h2>
<ul>
<li><strong>코드 재사용성 증가</strong><ul>
<li>이미 검증된 패턴을 사용하여 새로운 시스템을 개발할 때 시간과 비용을 절약할 수 있고 잘 정의된 컴포넌트는 재사용 가능성을 높인다.</li>
</ul>
</li>
<li><strong>코드 가독성 향상</strong><ul>
<li>표준화된 구조를 제공하여 다른 개발자도 쉽게 이해할 수 있어 협업을 용이하게 한다.</li>
</ul>
</li>
<li><strong>개발 속도 향상</strong> <ul>
<li>설계 표준화로 인해 팀이 설계 방식을 빠르게 이해하고 개발에 착수할 수 있다.</li>
</ul>
</li>
<li><strong>확장성 향상</strong> <ul>
<li>모듈 간 결합도가 낮아 시스템의 기능을 추가하거나 변경하는 것이 용이하게 설계되어 시스템의 수명을 연장시킨다.</li>
</ul>
</li>
</ul>
<h2 id="아키텍처-패턴의-단점">아키텍처 패턴의 단점</h2>
<ul>
<li><strong>남용의 위험</strong><ul>
<li>간단한 문제에 대해서도 아키텍처 패턴을 적용하려는 것은 오히려 시스템을 복잡하게 만들 수 있다.</li>
</ul>
</li>
<li>특정 패턴에 너무 의존하게 되면 오히려 아키텍처 패턴의 장점인 유연성을 저하시킬 수 있다.</li>
<li><strong>초기 비용</strong><ul>
<li>아키텍처 설계 및 패턴 적용에 시간과 노력이 필요하고 팀원들이 해당 패턴에 대한 이해가 있어야 한다.</li>
</ul>
</li>
</ul>
<p>-&gt; 단점이 기술적인 부분이 아닌 단순 사용자의 역량에 달려있다는 점에서 디자인 패턴의 유의미한 단점이 없다고 생각한다.</p>
<h2 id="디자인-패턴과-아키텍처-패턴의-차이점">디자인 패턴과 아키텍처 패턴의 차이점</h2>
<p>아키텍처 패턴은 시스템의 전체적인 설계를 담당하고, 디자인 패턴은 코드의 세부적인 구현을 담당한다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th>아키텍처 패턴</th>
<th align="center">디자인 패턴</th>
</tr>
</thead>
<tbody><tr>
<td align="left">수준</td>
<td>시스템 전체</td>
<td align="center">코드의 특정 부분</td>
</tr>
<tr>
<td align="left">목적</td>
<td>시스템 구조 정의</td>
<td align="center">코드 품질 향상</td>
</tr>
<tr>
<td align="left">예시</td>
<td>MVC, MVP, MVVM</td>
<td align="center">Singleton, Observer, Adatper</td>
</tr>
</tbody></table>
<h1 id="mvp">MVP</h1>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/9304c169-df7a-43d1-ad80-58d55b446966/image.png" alt="">
MVP 패턴은 <code>Model</code>, <code>View</code>, <code>Presenter</code>로 구성된 아키텍처 패턴으로  MVC 패턴에서 <code>Controller</code> 대신 <code>Presenter</code>가 존재한다.</p>
<h3 id="구성요소">구성요소</h3>
<p><strong><code>View</code> *<em>: 사용자 인터페이스를 담당. 사용자와 직접 상호작용하는 부분이며, 사용자 입력을 받고 결과를 표시
*</em><code>Model</code></strong> : 애플리케이션의 데이터를 표현하고, 데이터 처리 로직을 담당. 비즈니스 로직을 담당하는 부분
<strong><code>Presenter</code></strong> : <code>Model</code>과 <code>View</code> 사이의 중개자 역할. 사용자의 입력을 받아 <code>Model</code>에 전달하고, <code>Model</code>에서 처리된 결과를 <code>View</code>에 전달. <code>View</code>와 <code>Model</code> 사이의 의존성을 줄여 테스트를 용이하게 만들고, 비즈니스 로직을 <code>View</code>로부터 분리.</p>
<h3 id="mvc와의-차이점">MVC와의 차이점</h3>
<p>MVC 패턴과의 차이점은 MVC 패턴에서는 <code>View</code>에서 <code>Model</code>을 직접 control할 수 있었지만 MVP 패턴에서는 <code>View</code>가 직접 <code>Model</code>을 Control할 수 없어 <code>View</code>와 <code>Model</code> 서로 의존하지 않게 되었으나 여전히 <code>View</code>와 <code>Presenter</code> 사이의 강한 의존성이 존재한다.</p>
<h3 id="작동-방식">작동 방식</h3>
<ol>
<li><strong>사용자 이벤트 발생</strong>: 사용자가 <code>View</code>에 있는 버튼을 클릭하거나 입력을 하는 등의 이벤트 발생</li>
<li><strong>View에서 Presenter로 전달</strong>: 발생한 이벤트는 <code>View</code>에서 <code>Presenter</code>로 전달</li>
<li><strong>Presenter에서 Model 처리</strong>: <code>Presenter</code>는 전달받은 이벤트에 따라 <code>Model</code>에 필요한 작업을 요청</li>
<li><strong>Model 처리 결과</strong>: <code>Model</code>은 요청된 작업을 수행하고 결과를 <code>Presenter</code>에게 반환</li>
<li><strong>Presenter에서 View 업데이트</strong>: <code>Presenter</code>는 <code>Model</code>에서 받은 결과를 바탕으로 <code>View</code>를 업데이트</li>
</ol>
<h3 id="mvp-패턴의-장점">MVP 패턴의 장점</h3>
<ul>
<li><strong>UI와 비즈니스 로직의 분리</strong><ul>
<li><code>View</code>와 <code>Model</code> 간 결합도가 낮아 각 컴포넌트를 독립적으로 변경하거나 테스트 가능</li>
</ul>
</li>
<li><strong>테스트 용이성</strong><ul>
<li><code>Presenter</code>가 UI와 독립적이기 때문에 <code>View</code>를 대체하여 비즈니스 로직을 단위 테스트하기가 쉬움.</li>
</ul>
</li>
<li><strong>유지보수성 향상</strong><ul>
<li>UI 로직(View)과 애플리케이션 로직(Model)을 명확히 분리하여 코드를 쉽게 이해하고 수정 가능</li>
</ul>
</li>
<li><strong>재사용성 증가</strong><ul>
<li><code>Presenter</code>와 <code>Model</code>은 <code>View</code>와 독립적이기에 다른 <code>View</code>에서 동일한 <code>Presente</code>나 <code>Model</code> 재사용 가능</li>
</ul>
</li>
<li><strong>확장성</strong><ul>
<li>새로운 기능을 추가할 때 기존 코드를 최소한으로 수정하며 확장 가능</li>
</ul>
</li>
</ul>
<h3 id="mvp-패턴이-적합한-경우">MVP 패턴이 적합한 경우</h3>
<ul>
<li>UI 로직이 복잡하고, 유지보수성이 중요한 프로젝트</li>
<li>다양한 UI와 복잡한 사용자 상호작용을 처리해야 하는 경우</li>
<li>단위 테스트를 통해 비즈니스 로직의 안정성을 보장하려는 경우</li>
</ul>
<h1 id="mvvm">MVVM</h1>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/e5a815f7-9f45-4489-8e64-fe08d00ed43b/image.png" alt=""></p>
<p>MVVM 패턴은 UI와 비즈니스 로직을 분리하여 애플리케이션의 유지보수성, 테스트 용이성, 확장성을 높이는 높이기 위해 설계된 아키텍처 패턴이다.
특히, 데이터 바인딩을 활용하여 UI와 데이터를 연결하는 방식으로 사용자 인터페이스 개발 생산성을 향상시키는 데 중점을 두고 있다.</p>
<h3 id="구성요소-1">구성요소</h3>
<p><strong><code>View</code> *<em>: 사용자 인터페이스를 담당. 사용자와 직접 상호작용하는 부분이며, 사용자 입력을 받고 결과를 표시 
*</em><code>Model</code></strong> : 애플리케이션의 데이터를 표현하고, 데이터 처리 로직을 담당. 비즈니스 로직을 담당하는 부분
<strong><code>ViewModel</code></strong> : <code>View</code>와 <code>Model</code> 사이의 중개자 역할. <code>View</code>에 노출될 데이터 관리. 사용자의 입력을 <code>Model</code>에 전달. <code>Model</code>에서 처리된 결과를 <code>View</code>에 반영. 데이터 바인딩을 통해 <code>View</code>와 <code>ViewModel</code> 사이의 데이터를 자동으로 동기화.</p>
<h3 id="mvp와의-차이점">MVP와의 차이점</h3>
<p>MVP 패턴은 <code>View</code>와 <code>Presenter</code>가 일대일 관계라면 MVVM은 <code>View</code>와 <code>ViewModel</code>간의 관계가 1대 N으로 하나의 <code>ViewModel</code>을 다양한 <code>View</code>에 사용할 수 있다.
또한 <code>ViewModel</code>은 <code>Presenter</code>에 비해 상대적으로 <code>View</code>와 느슨하게 결합되어 있다.</p>
<p><code>ViewModel</code>은 <code>View</code>에 표시될 데이터를 준비하고 사용자 입력을 처리하여 <code>Model</code>에 전달하는 역할을 수행하고 <code>View</code>의 구체적인 구현에 대한 지식이 필요하지 않다.
그에 비해 <code>Presenter</code>는 <code>View</code>의 상태를 관리하고 사용자 입력을 처리하는 등 다양한 책임을 수행하여 <code>View</code>에 대한 의존도가 높다.</p>
<h3 id="작동방식">작동방식</h3>
<ol>
<li><strong>사용자 이벤트 발생</strong>: 사용자가 <code>View</code>에 있는 버튼을 클릭하거나 입력하는 등의 이벤트가 발생</li>
<li><strong>View에서 ViewModel로 전달</strong>: 발생한 이벤트는 <code>View</code>에서<code>ViewModel</code>로 전달</li>
<li><strong>ViewModel에서 Model 처리</strong>: <code>ViewModel</code>은 전달받은 이벤트에 따라 <code>Model</code>에 필요한 작업 요청</li>
<li><strong>Model 처리 결과</strong>: <code>Model</code>은 요청된 작업을 수행하고 결과를 <code>ViewModel</code>에게 반환</li>
<li><strong>ViewModel에서 View 업데이트</strong>: <code>ViewModel</code>은 <code>Model</code>에서 받은 결과를 바탕으로 <code>ViewModel</code>의 속성을 업데이트하고 데이터 바인딩을 통해 <code>View</code>가 자동으로 업데이트됨</li>
</ol>
<h3 id="mvvm-패턴의-장점">MVVM 패턴의 장점</h3>
<ul>
<li><strong>데이터 바인딩</strong><ul>
<li>데이터 바인딩을 통해 UI와 데이터를 자동으로 동기화하여 개발 생산성을 높임</li>
</ul>
</li>
<li><strong>테스트 용이성</strong> <ul>
<li><code>ViewModel</code>을 독립적으로 테스트할 수 있어 테스트 커버리지를 높이고 버그 조기 발견 가능</li>
</ul>
</li>
<li><strong>유지보수성</strong><ul>
<li>각 구성 요소의 역할이 명확하게 분리되어 있어 코드의 변경 용이. 시스템의 유지보수 쉬움</li>
</ul>
</li>
<li><strong>확장성</strong><ul>
<li>새로운 기능을 추가하거나 기존 기능을 변경할 때 다른 구성 요소에 미치는 영향 최소화 가능</li>
</ul>
</li>
<li><strong>UI와 비즈니스 로직 분리</strong><ul>
<li>UI와 비즈니스 로직을 분리하여 각각 독립적으로 개발하고 테스트 가능</li>
</ul>
</li>
</ul>
<h3 id="mvvm-패턴이-적합한-경우">MVVM 패턴이 적합한 경우</h3>
<ul>
<li>복잡한 UI와 상태 관리를 요구하는 애플리케이션</li>
<li><code>ViewModel</code>과 데이터 바인딩 라이브러리를 활용하여 실시간 데이터 업데이트가 필요한 경우</li>
<li>테스트 가능하고 유지보수하기 쉬운 아키텍처가 중요한 프로젝트</li>
<li>확장성과 재사용성이 중요한 중대형 프로젝트</li>
</ul>
<h1 id="클린-아키텍처">클린 아키텍처</h1>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/b324305d-49ac-464e-a3ff-66bd7a950fda/image.png" alt="">
클린 아키텍처는 외부 프레임워크나 UI, 데이터베이스 등의 변화에 유연하게 대응할 수 있도록 시스템의 내부와 외부를 명확하게 분리하는 것을 목표로 하는 아키텍처 패턴이다.</p>
<p>클린 아키텍처는 시스템의 구성을 4가지로 나눈다. 먼저 각 구성에 대해 알아보면</p>
<h2 id="시스템의-구성">시스템의 구성</h2>
<ul>
<li><strong>엔티티(Entities)</strong><ul>
<li>핵심 비즈니스 로직을 캡슐화</li>
<li>메서드를 가지는 객체, 일련의 데이터 구조와 함수의 집합</li>
<li>가장 변하지 않으며 외부로부터 영향을 받지 않는 영역
ex) 사용자, 상품, 주문 등 실제 세계의 개념을 모델링한 객체</li>
</ul>
</li>
<li><strong>유즈 케이스(Use Cases)</strong><ul>
<li>애플리케이션의 특화된 비즈니스 로직을 포함한다.</li>
<li>시스템의 모든 유즈 케이스를 캡슐화하고 구현한다.</li>
<li>엔티티로 들어오고 나가는 데이터 흐름을 조정하고 조작한다.<ul>
<li>사용자가 시스템에 요구하는 기능을 구현하는 부분
ex) 사용자 등록, 상품 조회, 주문 생성 등</li>
</ul>
</li>
</ul>
</li>
<li><strong>인터페이스 어댑터(Interface Adapter)</strong><ul>
<li>일련의 어댑터들로 구성한다.</li>
<li>외부 인터페이스에서 들어오는 데이터를 유즈 케이스와 엔티티에서 처리하기 편한 방식으로 변환하며, 유즈 케이스와 엔티티에서 나가는 데이터를 외부 인터페이스에서 처리하기 편한 방식으로 변환
ex) 컨트롤러, 프레젠터, 게이트웨이 등</li>
</ul>
</li>
<li><strong>프레임워크와 드라이버(Frameworks &amp; Drivers)</strong><ul>
<li>시스템을 실행하기 위한 환경을 제공</li>
<li>시스템의 핵심 업무와는 관련 없는 세부 사항
ex) 프레임워크나, 데이터베이스, 웹 서버 등</li>
</ul>
</li>
</ul>
<blockquote>
<p>소프트웨어 아키텍처는 선을 긋는 기술이며, 나는 이러한 선을 경계(boundary)라고 부른다.
경계는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소을 알지 못하도록 막는다. - Robert C. Martin, Clean Architecture</p>
</blockquote>
<h2 id="android에서의-적용">Android에서의 적용</h2>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/eecf4c83-d0d0-4e6a-b982-0c87c4124af8/image.png" alt=""></p>
<h3 id="프리젠테이션-계층presentation-layer"><strong>프리젠테이션 계층(Presentation Layer)</strong></h3>
<ul>
<li>사용자와 직접 상호작용하는 계층으로 UI를 구성하고, 사용자의 입력을 받아 도메인 계층으로 전달</li>
</ul>
<p><strong><code>View, UI</code></strong>: 직접적으로 플랫폼 의존적인 구현으로 UI 화면 표시와 사용자 입력 담당
<strong>Presenter가 명령하는 일만 수행</strong>
<strong><code>Presenter</code></strong>: <code>ViewModel</code>과 유사한 역할, 사용자 입력이 왔을 때 어떤 반응을 해야 하는지에 대한 판단을 하는 영역으로  <code>View</code>가 단순히 명령만 수행하도록 함</p>
<h3 id="도메인-계층domain-layer">도메인 계층(Domain Layer)</h3>
<ul>
<li>앱의 핵심 비즈니스 로직과 규칙이 포함된 계층으로 프레젠테이션 계층이나 데이터 계층과 독립적</li>
</ul>
<p><strong><code>Use Case</code></strong>: 비즈니스 로직이 들어 있는 영역
<strong><code>Entity</code></strong>: 앱의 실질적인 데이터</p>
<h3 id="데이터-계층data-layer">데이터 계층(Data Layer)</h3>
<ul>
<li>애플리케이션의 데이터 관리 및 영속성을 담당</li>
<li>외부 데이터 소스(API, 데이터베이스 등)와 상호작용하며, 도메인 계층에서 요구하는 데이터를 제공</li>
</ul>
<p><strong><code>Repository</code></strong>: 유즈 케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역으로 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 가능
<strong><code>Data Source</code></strong>: 실제 데이터의 입출력이 실행되는 영역</p>
<p>상위 계층은 기본적으로 하위 계층을 참고하지만 실제로 도메인 계층은 데이터 계층을 참고하고 있지 않다.
-&gt; 리포지터리에서 이루어지는 <strong><code>의존성 역전 법칙</code></strong> 때문</p>
<blockquote>
<p>의존성 역전이란?
객체 지향 프로그래밍에서 의존 관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 이 원칙을 따르면, 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존 관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
<a href="https://meetup.nhncloud.com/posts/345">https://meetup.nhncloud.com/posts/345</a></p>
</blockquote>
<h3 id="클린-아키텍처의-장점">클린 아키텍처의 장점</h3>
<ul>
<li><strong>유지보수성 증가</strong>
  -비즈니스 로직과 구현 세부사항이 분리되어, 한 계층의 변경이 다른 계층에 영향을 최소화
  -&gt; 데이터베이스를 변경해도 유스케이스와 엔티티는 영향을 받지 않음.</li>
<li><strong>확장성 향상</strong><ul>
<li>새로운 기능이나 기술 스택을 추가하거나 교체하는 것이 용이</li>
</ul>
</li>
<li><strong>테스트 용이성</strong><ul>
<li>비즈니스 로직이 UI나 데이터베이스와 독립적이므로, 테스트가 쉽고 빠름</li>
<li>단위 테스트 작성 시 Mocking을 통해 외부 의존성을 최소화</li>
</ul>
</li>
<li><strong>재사용성 증가</strong><ul>
<li>엔티티와 유스케이스는 특정 애플리케이션에 종속되지 않으므로, 여러 프로젝트에서 재사용 가능</li>
</ul>
</li>
<li><strong>기술 독립성</strong><ul>
<li>특정 프레임워크나 도구에 종속되지 않으므로, 기술 스택 변경이 자유로움</li>
</ul>
</li>
</ul>
<h3 id="클린-아키텍처의-단점">클린 아키텍처의 단점</h3>
<ul>
<li>초기 설계 비용이 높고 숙련된 개발자가 아니라면 구조를 이해하고 적용하는 데 시간이 필요하다.</li>
</ul>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://doosicee.tistory.com/entry/Architecture-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80">https://doosicee.tistory.com/entry/Architecture-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80</a>
<a href="https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVP-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80">https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVP-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80</a>
<a href="https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80">https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80</a>
<a href="https://velog.io/@toma/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90">https://velog.io/@toma/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90</a>
<a href="https://meetup.nhncloud.com/posts/345">https://meetup.nhncloud.com/posts/345</a>
<a href="https://medium.com/@justfaceit/clean-architecture%EB%8A%94-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%84%EC%99%80%EC%A3%BC%EB%8A%94%EA%B0%80-1-%EA%B2%BD%EA%B3%84%EC%84%A0-%EA%B3%84%EC%B8%B5%EC%9D%84-%EC%A0%95%EC%9D%98%ED%95%B4%EC%A4%80%EB%8B%A4-b77496744616">https://medium.com/@justfaceit/clean-architecture%EB%8A%94-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%84%EC%99%80%EC%A3%BC%EB%8A%94%EA%B0%80-1-%EA%B2%BD%EA%B3%84%EC%84%A0-%EA%B3%84%EC%B8%B5%EC%9D%84-%EC%A0%95%EC%9D%98%ED%95%B4%EC%A4%80%EB%8B%A4-b77496744616</a></p>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li>비즈니스 로직 : 컴퓨터 프로그램에서 실세계의 규칙에 따라 데이터를 생성·표시·저장·변경하는 부분</li>
<li>ViewModel에서의 데이터바인딩 : ViewModel에 있는 데이터가 변경되면 이 변경 사항이 자동으로 View에 반영되고, 반대로 View에서 사용자가 입력한 데이터가 ViewModel에 자동으로 반영되는 것</li>
<li>테스트 커버리지 :  시스템 및 소프트웨어에 대해 충분히 테스트가 되었는지를 나타내는 정도</li>
<li>DTO(Data Transfer Object, 데이터 전송 객체) : 프로세스 간에 데이터를 전달하는 객체</li>
<li><strong>의존성 역전</strong><ul>
<li>의존성 역전 원칙은 객체지향 프로그래밍에서 고수준 모듈이 저수준 모듈의 구체적인 구현에 의존하지 않고, 추상적인 인터페이스에 의존하도록 설계하는 원칙입니다. 쉽게 말해, 상위 계층이 하위 계층에 의존하는 것이 아니라, 둘 다 추상적인 개념에 의존하도록 만드는 것입니다.</li>
</ul>
</li>
<li><strong>왜 의존성 역전을 하는가?</strong></li>
<li><em>유연성 증가*</em>: 시스템의 특정 부분을 변경해야 할 때, 다른 부분에 미치는 영향을 최소화할 수 있습니다.</li>
<li><em>테스트 용이성 향상*</em>: 의존성이 적은 모듈은 독립적으로 테스트하기 쉽습니다.</li>
<li><em>재사용성 증가*</em>: 추상적인 인터페이스에 의존하기 때문에, 다양한 구체적인 구현체와 함께 사용할 수 있습니다.</li>
<li><em>확장성 증가*</em>: 새로운 기능을 추가하거나 기존 기능을 변경하기 쉽습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 디자인패턴(Singleton, Adapter, Observer)]]></title>
            <link>https://velog.io/@couch_potato/Kotlin-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4Singleton-Adapter-Observer</link>
            <guid>https://velog.io/@couch_potato/Kotlin-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4Singleton-Adapter-Observer</guid>
            <pubDate>Fri, 08 Nov 2024 08:19:24 GMT</pubDate>
            <description><![CDATA[<h1 id="singleton-pattern이란">Singleton Pattern이란</h1>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/deaceb8d-0f3d-404a-abbb-9f8d84cf21b0/image.png" alt=""></p>
<blockquote>
<p>The singleton pattern is one of the creational design patterns that is used to limit the instances of a class to one. That means with the Singleton pattern, only one instance of a class exists in the whole project with global access.
Singleton is a creational design pattern that lets you ensure that a class has only one instance while providing a global access point to this instance.</p>
</blockquote>
<p>Singleton Pattern은 <strong>객체의 인스턴스가 오직 1개만 생성</strong>되는 패턴으로 생성자가 여러 차례 호출되어도 실제로 생성되는 객체는 하나이고 맨 처음에 생성된 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 return한다.</p>
<h2 id="kotlin에서-구현하기">Kotlin에서 구현하기</h2>
<pre><code class="language-kotlin">public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

    public void say() {
        System.out.println(&quot;hi, there&quot;);
    }
}</code></pre>
<p>Singleton Pattern의 기본적인 구현 방법으로 유의해야할 점은 다음과 같다.</p>
<ul>
<li>외부에서 추가적인 객체 생성을 막기 위해 해당 클래스 생성자가 private인지 확인해야한다. </li>
<li>클래스 내부에 getInstance() 함수를 선언하여 외부로 객체를 반환할 때 이미 생성된 instance를 반환한다.</li>
</ul>
<p>다른 방법으로는 <code>companion object</code>를 사용하는 방법이 있다.</p>
<pre><code class="language-kotlin">class EventManager private constructor() {
    companion object{
        private val instance = EventManager()

        fun sharedInstance(): EventManager {
            return instance
        }
    }
}</code></pre>
<p>먼저 <code>companion object</code>는 클래스 내부의 자바의 static과 비슷한 역할을 수행한다고 볼 수 있다.
(물론 정확하게 분류하면 companion object는 객체이며 하나의 클래스에는 오직 하나의 companion object만 존재할 수 있는 등 엄연히 다르다.)</p>
<ul>
<li><code>private constructor()</code>를 통해 외부에서 직접 인스턴스를 생성할 수 없다.</li>
<li><code>EventManager</code> 클래스 내부에 <code>companion object</code>가 있기 때문에, <code>sharedInstance()</code> 메서드에 <code>EventManager.sharedInstance()</code>와 같이 직접 접근할 수 있다.</li>
<li><code>EventManage</code>의 유일한 인스턴스가 <code>companion object</code> 내부에서 생성되어 이 변수는 private이므로 외부에서는 접근할 수 없다.</li>
</ul>
<p>여기서 보다 더 안전하게 Singleton Pattern을 구현하고자 한다면 Singleton 인스턴스를 <code>@Votaile</code>로 표시하면 된다.</p>
<h4 id="volatile">Volatile</h4>
<p><code>@Volatile</code>은 변수에 대한 스레드 간 일관성을 보장하기 위해 사용되는 키워드로 주로 멀티스레드 환경에서 특정 변수에 여러 스레드가 동시에 접근할 때, <strong>각 스레드가 항상 최신의 값을 읽도록 강제</strong>하는 역할을 한다.</p>
<pre><code class="language-kotlin">class Singleton private constructor() {

    companion object {

        @Volatile private var instance: Singleton? = null

        fun getInstance() =
            instance ?: synchronized(this) { 
                instance ?: Singleton().also { instance = it }
            }
    }
}</code></pre>
<p>이 코드에서는 instance가 null일 경우 <code>synchronized</code> 블록에 진입하여 인스턴스를 생성한다. 이 블록은 한 번에 하나의 스레드만 진입할 수 있어, 여러 스레드가 동시에 인스턴스를 생성하는 것을 방지한다.</p>
<p><code>@Volotile</code>로 표시했을 때는 다음과 같은 이점이 존재한다.</p>
<ul>
<li>인스턴스에 대한 읽기 및 쓰기가 컴파일러나 프로세서에 의해 재정렬되지 않는다.</li>
<li>한 스레드에서 변경한 내용은 다른 스레드에서 즉시 볼 수 있다.</li>
</ul>
<h2 id="그렇다면-왜-singleton-pattern을-사용할까">그렇다면 왜 Singleton Pattern을 사용할까?</h2>
<p><strong>1. 메모리 측면</strong></p>
<ul>
<li>한 개의 인스턴스만을 고정 메모리 영역에 생성하고 이후 외부에서 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다. 또한 이미 생성된 인스턴스를 활용하니 속도 측면에서도 이점이 있다고 볼 수 있다.</li>
</ul>
<p><strong>2. 데이터 공유가 쉽다</strong></p>
<ul>
<li>Singleton 인스턴스는 전역으로 사용되는 인스턴스이기에 다른 클래스의 인스턴스들이 접근해서 사용할 수 있다.
* 이떄 동시 접근 문제가 발생할 수 있어 <code>@Volatile</code>을 사용하거나 설계에 유념</li>
</ul>
<h2 id="singleton-pattern의-단점">Singleton Pattern의 단점</h2>
<p>*<em>1. 확장성 및 테스트 가능성 감소 *</em></p>
<ul>
<li>글로벌 상태를 만들면 애플리케이션의 여러 구성 요소 간에 종속성과 결합을 도입하여 수정 또는 확장하기 어렵게 만들 수 있다. 
또한 싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트가 결정적으로 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화시켜주어 한다.</li>
</ul>
<p>*<em>2.복잡성 및 위험 증가 *</em></p>
<ul>
<li>동시성 문제, private 생성자로 인한 자식 클래스 생성의 어려움 등 Singleton 클래스의 설계 및 구현에 주의가 필요하다. 제대로 수행하지 않으면 버그, 장기생존으로 인한 메모리 누수 또는 성능 병목 현상의 원인이 될 수 있다.</li>
</ul>
<h1 id="adapter-pattern이란">Adapter Pattern이란</h1>
<p>Adapter Pattern은 기존 클래스의 인터페이스를 사용하고자 하는 다른 인터페이스로 변환해주어 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 어댑터로서 사용되는 구조 패턴이다.</p>
<p>Adapter Pattern의 구성요소는</p>
<ul>
<li>Target : Client가 사용하고자 하는 인터페이스</li>
<li>Adaptee : Client가 갖고 있는 인터페이스</li>
<li>Adapter : Target 인터페이스를 구현하는 클래스</li>
<li>Client : Target 인터페이스를 사용하고자 하는 주체</li>
</ul>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/788865a5-f495-4985-b574-c204817052d1/image.png" alt=""></p>
<pre><code class="language-kotlin">// A 인터페이스
interface AInterface {
    fun methodA()
}

// B 인터페이스
interface BInterface {
    fun methodB()
}

// A 인터페이스 구현 클래스
class AClass : AInterface {
    override fun methodA() {
        println(&quot;A 클래스의 methodA 호출&quot;)
    }
}

// 어댑터 클래스: B 인터페이스를 구현하면서 내부적으로 A 클래스를 사용
class AtoBAdapter(private val aClass: AClass) : BInterface {
    override fun methodB() {
        println(&quot;어댑터가 B 인터페이스의 methodB를 호출함.&quot;)
        aClass.methodA()  // A 클래스의 methodA를 호출하여 B 인터페이스에 맞게 동작
    }
}

// 클라이언트 코드: B 인터페이스를 기대
fun clientCode(bInterface: BInterface) {
    bInterface.methodB()
}

// 사용 예시
fun main() {
    val aClass = AClass()               // A 인터페이스를 구현하는 클래스
    val adapter = AtoBAdapter(aClass)    // AtoBAdapter로 AClass를 B 인터페이스에 맞게 변환

    clientCode(adapter)  // 클라이언트 코드는 B 인터페이스를 사용하는 것처럼 작동
}
</code></pre>
<p><code>AClass</code> 객체를 <code>AtoBAdapter</code>로 감싸서 <code>clientCode()</code>에 전달함으로써 <code>AClass</code>를 <code>BInterface</code>처럼 사용할 수 있다.</p>
<p>Adapter Pattern은 호환 작업 방식에 따라 두 종류로 나뉜다.</p>
<blockquote>
<ul>
<li>객체 어댑터(Object Adatper)</li>
</ul>
</blockquote>
<ul>
<li>클래스 어댑터(Class Adatper)</li>
</ul>
<h2 id="객체-어댑터object-adapter">객체 어댑터(Object Adapter)</h2>
<p>객체 어댑터는 Composition(합성)을 통해 구현한다.</p>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/71704096-f120-419f-a128-16c6996067e4/image.png" alt=""></p>
<p>객체 어댑터 방식은 기존 클래스(Adaptee)의 인스턴스를 Adapter 클래스 내부에 포함시켜 필요한 인터페이스(Target)를 변환해주는 방식으로 Adatper 클래스가 원래 클래스의 인스턴스를 <strong>포함</strong>하고, 해당 인스턴스의 메서드를 호출하여 인터페이스 간의 호환성을 제공하는 방식이다.</p>
<p>코드로 보면 이와 같은 형태이다.</p>
<pre><code class="language-kotlin">interface TargetInterface {
    fun request() // 클라이언트가 호출하는 메서드
}

class AdapteeClass {
    fun specificRequest() {
        println(&quot;기존 클래스의 specificRequest 메서드 호출&quot;)
    }
}

class ObjectAdapter(private val adaptee: AdapteeClass) : TargetInterface {
    override fun request() {
        println(&quot;어댑터에서 request를 호출하여 specificRequest로 변환&quot;)
        adaptee.specificRequest() 
    }
}

fun clientCode(target: TargetInterface) {
    target.request()
}

fun main() {
    val adaptee = AdapteeClass()                
    val adapter = ObjectAdapter(adaptee)        

    clientCode(adapter) 
}
</code></pre>
<p>Client는 <code>TargetInterface</code>의 <code>request()</code>를 호출하지만 <code>ObjectAdapter</code>를 통해 <code>AdapteeClass</code>의 <code>speicifcRequest()</code>가 호출된다.</p>
<h3 id="객체-어댑터object-adapter의-장단점">객체 어댑터(Object Adapter)의 장단점</h3>
<p><strong>장점</strong></p>
<ul>
<li>객체 어댑터는 합성을 통해 구현되는데 합성은 두 객체 사이의 의존성은 런타임에 해결하므로 런타임 중에 Adaptee가 결정되어 유연하다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>어댑터마다 추가적인 객체가 생성되기 때문에, 빈번한 어댑터 사용이 필요한 경우 성능이 저하될 수 있다.</li>
</ul>
<h2 id="클래스-어댑터class-adapter">클래스 어댑터(Class Adapter)</h2>
<p>클래스 어댑터는 상속(Inheritance)를 통해 구현된다.
<img src="https://velog.velcdn.com/images/couch_potato/post/8b8af4d4-37d9-45d8-a16f-8ed1fd194f18/image.png" alt="">
Adapter 클래스가 두 인터페이스를 모두 구현하고 기존 클래스의 기능을 상속하여 원하는 인터페이스로 변환하는 방식의 Adapter이다.</p>
<p>객체 어댑터와 다른 점은 Adaptee(Service)를 상속했기 때문에 따로 객체 구현없이 바로 코드 재사용이 가능하다는 것이다.
즉 클래스 어댑터는 클라이언트와 서비스 양쪽에서 행동들을 상속받기에 객체를 래핑할 필요가 없다.
그러나 클래스 어댑터는 다중 상속을 통해 구현되므로 다중 상속을 지원하지 않는 언어에서는 구현하기 용이하지 않다.</p>
<p>코드로 보면 이와같은 형태이다</p>
<pre><code class="language-kotlin">interface BInterface {
    fun methodB()
}

open class AClass {
    fun methodA() {
        println(&quot;A 클래스의 methodA 호출&quot;)
    }
}

class ClassAdapter : AClass(), BInterface {
    override fun methodB() {
        println(&quot;어댑터에서 B 인터페이스의 methodB를 호출함&quot;)
        methodA()  
    }
}

fun clientCode(bInterface: BInterface) {
    bInterface.methodB()
}

fun main() {
    val adapter = ClassAdapter()  
    clientCode(adapter)           
}</code></pre>
<p><code>Client</code>는 <code>ClassAdpater</code>는 <code>BInterface</code>를 구현하고, <code>AClass</code>를 상속하여 <code>methodB()</code>를 override하여 상속받은 <code>methodA()</code>를 호출한다.</p>
<h3 id="클래스-어댑터class-adapter의-장단점">클래스 어댑터(Class Adapter)의 장단점</h3>
<p><strong>장점</strong></p>
<ul>
<li>어댑터가 Adaptee를 상속받기 때문에, 추가 객체 생성 없이 Adaptee의 메서드를 바로 호출할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>Adaptee의 서브클래스이기 때문에 Adaptee의 서브클래스로서의 제한을 받는다.</li>
</ul>
<h2 id="adapter-pattern의-장점">Adapter Pattern의 장점</h2>
<p><strong>1.호환성 제공</strong></p>
<ul>
<li>기존 코드를 수정하지 않고도 클라이언트가 원하는 인터페이스로 변환하여 호환성을 제공한다.</li>
</ul>
<p><strong>2.재사용성 증가</strong></p>
<ul>
<li>외부 라이브러리나 기존 클래스 등 수정이 불가능한 코드도 클라이언트 요구에 맞게 변환하여 재사용할 수 잇다.</li>
</ul>
<p><strong>3.유연성 제공</strong></p>
<ul>
<li>어댑터를 이용해 여러 인터페이스를 서로 호환할 수 있으므로 코드의 유연성이 높아진다.</li>
</ul>
<h2 id="어댑터-패턴의-단점">어댑터 패턴의 단점</h2>
<p><strong>복잡도 증가</strong></p>
<ul>
<li>간단한 경우에도 새로운 클래스를 추가하게 되어 코드의 복잡도가 증가할 수 있고 이에 따라 코드가 다소 난잡해질 경우 유지보수가 어려워진다.</li>
</ul>
<h1 id="observer-pattern이란">Observer Pattern이란</h1>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/d57cc761-ccf5-49e6-8d44-29c73a6f601d/image.png" alt=""></p>
<blockquote>
<p>옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.</p>
</blockquote>
<ul>
<li>위키피디아</li>
</ul>
<p>즉 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 알림을 보내는 방식으로 <strong>상태 변화가 여러 객체에 전파되어야 하는 경우</strong>에 다른 객체의 상태 변화를 별도의 함수 호출 없이 즉각적으로 알 수 있어 이벤트에 대한 처리를 자주해야 하는 프로그램에서 매우 유용하다.</p>
<h2 id="observer-pattern의-구성">Observer Pattern의 구성</h2>
<p>Observer Pattern의 구성요소는 패턴의 이름대로 객체를 Observe하는 <code>Observer</code>와 Observe되는 <code>Subject</code>로 이루어져있다.
(<code>Subject</code>는 <code>Publisher</code>, <code>Observable</code>라고도 하고 <code>Observer</code>는 <code>Subscriber</code>라고도 한다.)
<img src="https://velog.velcdn.com/images/couch_potato/post/b17e7954-978c-441b-b8d8-4a343b5dba66/image.png" alt=""></p>
<h3 id="subject">Subject</h3>
<ul>
<li>Observe 되는 객체</li>
</ul>
<p><code>Subject</code>는 기본적으로 3개의 기능을 갖는다. 각각 <code>attach</code>, <code>detach</code>, <code>notify</code>로 </p>
<ul>
<li>Observe하는 객체들을 등록하는 <code>attach</code></li>
<li>Observe하는 객체를 제거하는 <code>detach</code></li>
<li>Observe하는 객체들에게 상태변화를 알려주는 <code>notify</code></li>
</ul>
<p>의 기능을 수행한다.</p>
<p>코드로 보면</p>
<pre><code class="language-kotlin">class Subject {
    private val observers = mutableListOf&lt;Observer&gt;()

    fun move() {
        println(&quot;Subject 이동&quot;)
           notify(&quot;Subject가 이동함&quot;)
    }

    fun stop() {
        println(&quot;Subject 정지&quot;)
        notify(&quot;Subject가 멈춤&quot;)
    }

    fun attach(observer: Observer) {
        observers.add(observer)
    }

    fun detach(observer: Observer) {
        observers.remove(observer)
    }

    fun notify(message: String) {
        observers.forEach { it.update(message) }
    }
}</code></pre>
<p>중요한 점은 Subject의 상태가 변하면 <code>notify()</code>를 호출하여 <code>Observer</code>를 <code>update</code>해준다.</p>
<h3 id="observer">Observer</h3>
<ul>
<li>Observe 하는 객체</li>
</ul>
<p><code>Observer</code>는 <code>Subject</code>가 <code>notify</code>한 내용을 통해 <code>update</code>하는 기능이 필요하다.</p>
<p>코드로 보면</p>
<pre><code class="language-kotlin">interface Observer {
    fun update(message: String)
}

class ConcreteObserver(private val name: String) : Observer {
    override fun update(message: String) {
        println(&quot;$name received message: $message&quot;)
    }
}</code></pre>
<p><code>Subject</code>로 부터 <code>notify</code>된 내용을 토대로 <code>update</code> 메소드를 통해 상태 변화를 자동으로 수행할 수 있다.</p>
<blockquote>
<p>참고로 Java에서는 Observer 클래스를 지원했었으나 멀티스레딩 환경에서의 비효율성과 유연성 문제로 Java 9부터 Deprecated 되었다.</p>
</blockquote>
<h3 id="발생할-수-있는-문제">발생할 수 있는 문제</h3>
<p>그러나 여기서도 <strong>동시성에 대한 문제가 발생할 수 있다</strong>.
예를 들어 <code>notify</code>가 실행되는 동안 다른 스레드에서 새로운 <code>Observer</code>가 <code>attach</code>를 통해 등록되면, 이 새로운 <code>Observer</code>는 현재 진행 중인 알림 과정에 포함되지 않는 문제가 발생할 수 있다.</p>
<p>이것을 해결하는 다양한 방법들 중 하나는 <code>lock</code>을 이용하는 것이다.</p>
<blockquote>
<p>Lock은 스레드가 공유 자원에 접근할 때, 하나의 스레드만 접근하도록 잠금(lock)을 걸어 데이터 일관성을 보장하는 도구</p>
</blockquote>
<p>어느 한 메소드가 실행중이라면 다른 메소드를 실행하지 못하도록 lock을 걸어주는 것이다.</p>
<pre><code class="language-kotlin">import java.util.concurrent.locks.ReentrantLock

class Subject {
    private val observers = mutableListOf&lt;Observer&gt;()
    private val lock = ReentrantLock()

    fun attach(observer: Observer) {
        lock.lock()
        try {
            observers.add(observer)
        } finally {
            lock.unlock()
        }
    }

    fun detach(observer: Observer) {
        lock.lock()
        try {
            observers.remove(observer)
        } finally {
            lock.unlock()
        }
    }

    fun notify(message: String) {
        val snapshot = lock.withLock { observers.toList() }
        for (observer in snapshot) {
            observer.update(message)
        }
    }
}

interface Observer {
    fun update(message: String)
}

class ConcreteObserver(private val name: String) : Observer {
    override fun update(message: String) {
        println(&quot;$name received message: $message&quot;)
    }
}

fun main() {
    val subject = Subject()
    val observer1 = ConcreteObserver(&quot;Observer 1&quot;)
    val observer2 = ConcreteObserver(&quot;Observer 2&quot;)

    subject.attach(observer1)
    subject.attach(observer2)
    subject.notify(&quot;New Update!&quot;)
}

</code></pre>
<p>위 코드에서는 <code>notify</code>, <code>attach</code>, <code>detach</code> 메소드에서 <code>lock</code>을 사용하여 동시에 <code>Observer</code>가 추가되거나 제거되는 상황을 방지하고 <code>notify</code> 메소드에서는 <code>Observer</code> 목록을 스냅샷으로 복사하여, 알림을 보내는 도중에 목록이 변경되는 문제를 방지한다.</p>
<p>그 외에는 <code>@Synchronized</code> 블록을 사용하는 방법도 있다.</p>
<pre><code class="language-kotlin">class Subject {
    private val observers = mutableListOf&lt;Observer&gt;()

    @Synchronized
    fun attach(observer: Observer) {
        observers.add(observer)
    }

    @Synchronized
    fun detach(observer: Observer) {
        observers.remove(observer)
    }

    @Synchronized
    fun notify(message: String) {
        // Snapshot을 만들어 동시성 문제 방지
        val snapshot = observers.toList()
        for (observer in snapshot) {
            observer.update(message)
        }
    }
}</code></pre>
<h2 id="observer-pattern의-장점">Observer Pattern의 장점</h2>
<p><strong>1.느슨한 결합</strong></p>
<ul>
<li>Subject와 Observer 간의 의존성을 줄여준다. Subject가 변경되더라도 Observer를 직접 수정할 필요가 없다.</li>
</ul>
<p><strong>2.확장성</strong></p>
<ul>
<li>새로운 Observer를 쉽게 추가할 수 있다. 즉, 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계할 수 있는 개방 폐쇄 원칙을 준수한다.</li>
</ul>
<h2 id="observer-pattern의-단점">Observer Pattern의 단점</h2>
<p><strong>1.메모리 누수 가능성</strong></p>
<ul>
<li>Observer 등록 해제(Detach)를 제대로 관리하지 않으면 메모리 누수가 발생할 수 있다.</li>
</ul>
<p><strong>2.알림 순서 보장 어려움</strong></p>
<ul>
<li>다수의 Observeer가 있을 경우 알림의 순서를 보장하기 어려워 이에 따른 추가 구현이 필요하다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<h3 id="singleton">Singleton</h3>
<p><a href="https://medium.com/@ZahraHeydari/singleton-pattern-in-kotlin-b09380c53b14">https://medium.com/@ZahraHeydari/singleton-pattern-in-kotlin-b09380c53b14</a>
<a href="https://velog.io/@seongwon97/%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80">https://velog.io/@seongwon97/%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80</a>
<a href="https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/">https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/</a>
<a href="https://onlyfor-me-blog.tistory.com/441">https://onlyfor-me-blog.tistory.com/441</a></p>
<h3 id="adatper">Adatper</h3>
<p><a href="https://jusungpark.tistory.com/22">https://jusungpark.tistory.com/22</a>
<a href="https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%96%B4%EB%8C%91%ED%84%B0Adaptor-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90">https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%96%B4%EB%8C%91%ED%84%B0Adaptor-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90</a>
<a href="https://refactoring.guru/ko/design-patterns/adapter">https://refactoring.guru/ko/design-patterns/adapter</a>
<a href="https://velog.io/@haero_kim/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%9D%B4%EB%AF%B8-%EC%96%B4%EB%8C%91%ED%84%B0-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%95%8C%EA%B3%A0-%EC%9E%88%EB%8B%A4">https://velog.io/@haero_kim/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%9D%B4%EB%AF%B8-%EC%96%B4%EB%8C%91%ED%84%B0-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%95%8C%EA%B3%A0-%EC%9E%88%EB%8B%A4</a></p>
<h3 id="observer-1">Observer</h3>
<p><a href="https://pjh3749.tistory.com/266">https://pjh3749.tistory.com/266</a>
<a href="https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4">https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4</a>
<a href="https://velog.io/@hanna2100/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-2.-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%98%88%EC%A0%9C-observer-pattern">https://velog.io/@hanna2100/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-2.-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%98%88%EC%A0%9C-observer-pattern</a>
<a href="https://refactoring.guru/design-patterns/observer">https://refactoring.guru/design-patterns/observer</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 상속, 추상 클래스, 인터페이스]]></title>
            <link>https://velog.io/@couch_potato/Kotlin-%EC%83%81%EC%86%8D-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@couch_potato/Kotlin-%EC%83%81%EC%86%8D-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Sat, 26 Oct 2024 08:05:55 GMT</pubDate>
            <description><![CDATA[<p>동아리의 루키 온보딩 활동으로 단순히 개념만 알고 있었던
코틀린의 상속, 추상 클래스, 인터페이스에 대해 더욱 알아보고자 한다.</p>
<p>추상 클래스와 인터페이스에 대해 알아보기 전에 먼저 상속에 대해 알아보자</p>
<h1 id="상속">상속</h1>
<p>상속은 기존 클래스의 특성과 기능을 새로운 클래스가 물려받는 것으로 
코드의 재사용성을 높이고, 유사한 기능을 가진 클래스들을 효과적으로 관리할 수 있다는 장점이 있다.</p>
<p>그중 기존 클래스를 슈퍼클래스, 기존 클래스를 상속받는 클래스를 서브클래스라고 한다.</p>
<p>코틀린에서 클래스는 기본적으로 <code>final</code>으로 만약 해당 클래스를 상속받고 싶다면 <code>open</code>을 붙여줘야 햔다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/56ad701a-81a1-4b8d-8d46-af33d3363219/image.png" alt="">open X</th>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/ecd9ff88-f0a9-4254-9b9f-3c819e6b4d62/image.png" alt=""> open O</th>
</tr>
</thead>
</table>
<p>마찬가지로 코틀린의 메소드도 <code>final</code>이 기본값이어서 서브클래스에서 메소드를 재정의하는 <code>override</code>를 하고 싶다면 <code>open</code>을 붙여줘야 한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/47aa2d39-a234-4728-bdef-dd9b2ac314b2/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/couch_potato/post/4dde1d42-26ce-4ac4-8ebf-3cc2e8f4d637/image.png" alt=""></th>
</tr>
</thead>
</table>
<h3 id="매개변수화된-생성자를-사용한-상속">매개변수화된 생성자를 사용한 상속</h3>
<pre><code>open class Person(val name : String){
    open fun eat(){
        println(&quot;$name eat rice&quot;)
    }
}

class Student : Person(&quot;Kim&quot;)</code></pre><p>서브클래스를 생성할 때 슈퍼클래스의 매개변수화된 생성자를 사용하려면 서브클래스 정의할 때 입력한다.
만약 미리 정의하지 않고 서브클래스를 생성할 때 서브클래스에서 매개변수로 받고 그것을 슈퍼클래스로 넘겨주고자 한다면</p>
<pre><code>open class Person(val name : String){
    open fun eat(){
        println(&quot;$name eat rice&quot;)
    }
}

class Student(name : String) : Person(name)

fun main(){
    val man = Student(&quot;Kim&quot;)
    man.eat()
}</code></pre><p>위와 같이 서브클래스의 파라미터로 받은 값을 다시 슈퍼클래스로 넘겨주면 된다.</p>
<p>여기서 알아야할 것은 <strong>오직 한 개의 클래스만을 상속할 수 있다</strong>는 것이다.</p>
<p>그렇다면 언제 어떻게 사용될지 몰라 메소드 내부를 미리 정의하지 않거나 여러 개의 클래스를 상속받고 싶다면 어떻게 해야할까?</p>
<p>그 해답이 바로 <strong><code>추상 클래스</code></strong> 와 <strong><code>인터페이스</code></strong> 이다.</p>
<h2 id="추상-클래스abstract-class">추상 클래스(Abstract Class)</h2>
<p>추상 클래스는 말 그대로 추상적인 클래스로 <strong>아직 명확하게 정의되지 않은</strong> 클래스이다.</p>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/82edbc8b-4e19-498b-b6b9-baae6dcd5e5f/image.png" alt="">
추상클래스는 일반 클래스와 달리 <code>open</code>을 사용하지 않아도 되지만 <code>abstract</code>를 클래스와 메소드에 붙여주어야 한다.</p>
<p>앞서 말했듯 아직 명확하게 정의되지 않은 클래스이기 때문에 <strong>추상 클래스를 직접 선언하여 인스턴스를 생성할 수 없다.</strong>
또한 추상 클래스 내부의 <code>abstract</code>가 붙은 추상 메소드들은 반드시 상속받은 서브클래스에서 정의가 되어야 한다.
(그렇지 않다면 위처럼 에러가 발생한다)                </p>
<p>그러나 서브클래스에서 추상 클래스의 <strong>추상 메소드를 정의하지 않고도 사용할 수 있는 방법이 있다!</strong>
바로 <code>object</code>를 사용하는 것이다.
이번에 공부하면서 처음 알게된 것인데 object를 사용하면 추상메소드를 정의하지 않고도 바로 사용할 수 있다. 
이렇게</p>
<pre><code>abstract class Person{
    abstract fun eat()
}

fun main(){
    val man = object : Person(){
        override fun eat() {
            println(&quot;쿰척쿰척&quot;)
        }
    }
}</code></pre><p>추상 클래스는 일정한 기본 특성을 공유하지만, <strong>일부 기능을 명시적으로 정의</strong>하도록 강제하고 싶을 때 유용하게 사용할 수 있다.</p>
<h2 id="인터페이스interface">인터페이스(Interface)</h2>
<p><strong>인터페이스</strong>는 추상클래스처럼 <strong>인터페이스를 직접 선언하여 인스턴스를 생성할 수 없다.</strong></p>
<pre><code>interface Human {
    fun walk()
    fun eat(){println(&quot;GOOD&quot;)}
}

class Male : Human{
    override fun walk(){
        println(&quot;저벅저벅&quot;)
    }
}</code></pre><p>그러나 추상 클래스와는 다르게 <code>abstract</code>를 사용하지 않고 일반 클래스와도 다르게 <code>open</code>을 사용하지 않고도 <code>override</code>할 수 있다.</p>
<p>또한 클래스와 다르게 <strong>다중 상속이 가능</strong>하다는 장점이 있다</p>
<pre><code>interface A {
    fun foo() { print(&quot;A&quot;) }
    fun bar()
}

interface B {
    fun foo() { print(&quot;B&quot;) }
    fun bar() { print(&quot;bar&quot;) }
}

class C : A {
    override fun bar() { print(&quot;bar&quot;) }
}

class D : A, B {
    override fun foo() {
        super&lt;A&gt;.foo()
        super&lt;B&gt;.foo()
    }

    override fun bar() {
        super&lt;B&gt;.bar()
    }
}</code></pre><p>하지만 <strong>인터페이스</strong>는 추상 클래스와 비슷하지만 차이점은 상태를 저장할 수 없다. 
<img src="https://velog.velcdn.com/images/couch_potato/post/a47ec964-f506-4f60-b4b2-9fb4b37cb28e/image.png" alt="">
위와 같이 값을 저장하면 에러가 발생한다.
이것도 이번에 공부하며 새롭게 알게된 것인데 여기서 또다시 인터페이스에 값을 저장할 수 있는 방법이 있다!</p>
<pre><code>interface Person {
    val name: String get() = &quot;Kim&quot;
}

class Male:Person{

}

fun main(){
    val man = Male()
    println(man.name)
}</code></pre><p>이때는 무조건 val로 선언해야한다는 제약이 있긴하지만 위와 같이 get(), 게터로 구현이 가능하다!</p>
<h3 id="일반-상속-대신-추상-클래스와-인터페이스를-사용하는-이유">일반 상속 대신 추상 클래스와 인터페이스를 사용하는 이유</h3>
<p>1.유연성
-인터페이스를 통해 다중 상속이 가능하므로, 특정 클래스가 여러 동작을 가질 수 있게 할 수 있다.</p>
<p>2.강제성
-추상 클래스와 인터페이스는 구현을 강제할 수 있어, 특정 메소드가 반드시 필요하다는 것을 보장한다.</p>
<p>3.유지보수성
-추상 클래스와 인터페이스를 사용하면 코드가 더 명확해지고 유지보수가 쉬워지고 코드의 재사용성이 높아진다.</p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://play.kotlinlang.org/byExample/01_introduction/07_Inheritance">https://play.kotlinlang.org/byExample/01_introduction/07_Inheritance</a>
<a href="https://kotlinlang.org/docs/interfaces.html#resolving-overriding-conflicts">https://kotlinlang.org/docs/interfaces.html#resolving-overriding-conflicts</a>
<a href="https://velog.io/@k906506/Kotlin-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-VS-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4">https://velog.io/@k906506/Kotlin-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-VS-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 웹모바일 9기 챌린지 후기]]></title>
            <link>https://velog.io/@couch_potato/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%EB%AA%A8%EB%B0%94%EC%9D%BC-9%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%EB%AA%A8%EB%B0%94%EC%9D%BC-9%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 19 Aug 2024 08:33:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/couch_potato/post/fcc64f00-c6bd-4bbf-8e3b-d7a776452587/image.png" alt=""></p>
<p>실력을 기르자는 생각으로 지원했던 네이버 부스트캠프의 챌린지 과정이 끝났다.</p>
<h2 id="지원-동기">지원 동기</h2>
<p>3학년이 될 때까지 별다른 프로젝트 경험도 없고 그렇다고 기본기가 탄탄하지도 않았었기에 방학 동안 실력을 기르자는 생각으로 냅다 지원하게 되었고 커리큘럼을 보았을 때 내가 부족한 부분을 채울 수 있겠다 생각해서 지원하게 되었다.</p>
<p>이번 부스트캠프 챌린지 과정은 전공 수업에서 배웠으나 내 것을 온전히 만들지 못 했던 것들을 소화하는 기간이었다고 생각한다. </p>
<h2 id="챌린지-과정">챌린지 과정</h2>
<p>챌린지는 매일 다른 CS 지식을 기반으로 하는 미션이 나오고 그 다음 날 피어 세션에서 서로의 코드를 리뷰하고 피드백을 하는 시간을 가졌는데 다른 사람과 개발에 대해서 대화를 나눠본 적도 없고 누군가의 앞에서 내 코드를 발표한 적도 없어서 굉장히 새로운 경험이었다.</p>
<p>첫째날 미션을 했을 때 굉장히 쉬웠어서 &quot;아 챌린지도 할만 하겠구나&quot;라는 생각을 했었는데 바로 다음 날부터 그 주의 미션을 1개도 성공하지 못 했었다....</p>
<p>부스트캠프에서 강조한 부분은 <strong>성장</strong>이었다.
미션을 얼마나 구현하기 보다는 그날의 미션에서 제시하는 CS지식을 학습하고 온전히 자신의 언어로 정리하는 것을 중요시했고 다른 캠퍼들과 정보 공유를 굉장히 장려하였다.</p>
<p>문제는 나는 매일 미션 하는 것도 겨우겨우 했고 못한 날도 굉장히 많았다.
그러다보니 학습한 내용을 바로바로 써먹고 다시 코드를 짜는 것을 반복하여 정작 내가 배운 내용을 정리하지 않았고 정보 공유에도 소홀히하게 되었다.</p>
<p>아는 게 없다보니 알려줄 정보도 없고 또한 내 미션하기 바빠서 다른 거 할 여유가 없었다...ㅋㅋ
내가 더 열심히 했었다면이라는 생각이 조금 남는다.</p>
<h2 id="3차-코딩-테스트">3차 코딩 테스트</h2>
<p>3차 코딩 테스트는 기존 챌린지 과정에서 배웠던 내용을 기반으로 나왔고 시간이 빠듯했지만 어떻게든 전부 풀 수 있었다.
이때까지만 해도 멤버십 가능성이 있겠다고 생각했는데....</p>
<h2 id="수료-후기">수료 후기</h2>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/dd617a8d-750b-48bc-b47f-9ee58d283fc7/image.png" alt=""></p>
<p>멤버십에서는 아쉽게도 선정되지 못 했다..
어떻게 보면 당연한 결과같기도 하다.
부스트캠프에서 강조한 학습이 내가 학습이 했다 하더라도 그것을 보여줄 그리고 내가 얼만큼 이해했는지를 알 수 있는 학습 정리가 미흡했는데 어떻게 알겠나
변명아닌 변명은 챌린지 과정 기간이 공모전 기간과 겹쳐서 3주차에는 거의 제대로 한 날도 드물었던 것...
또한 다른 캠퍼들과의 정보 공유 참여도 거의 안 했었어서 이러한 소극적인 참여가 아쉬웠던 것 같다.</p>
<p>그래도 나에게 굉장히 도움이 많이된 과정이었다.
알기만 했던 개념들을 실제로 구현해보면서 보다 이해할 수 있었고
몰랐던 개념들도 알게 되면서 기본기가 보다 탄탄해진 것 같다.</p>
<h2 id="앞으로">앞으로..</h2>
<p>이렇게 배운 CS 지식으로 이를 온전히 내것으로 만들기 위해 개인 프로젝트를 통해 안드로이드에 적용해보는 시간을 가져야 할 것 같다.
MVVM, Hilt.. 배울 게 너무도 많다</p>
<p>냅다 실력을 기르고자 공모전과 네부캠을 같이 해봤는데 내 실력으로 두 가지를 모두 잡기에는 무리였나보다
멤버십이 되지 않았으니 얌전히 학교를 다니라는 뜻으로 알고 학교나 열심히 다녀야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] MVVM에 대해 알아보기]]></title>
            <link>https://velog.io/@couch_potato/Android-MVVM%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/Android-MVVM%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 20 May 2024 02:25:51 GMT</pubDate>
            <description><![CDATA[<h2 id="안드로이드-아키텍쳐">안드로이드 아키텍쳐</h2>
<p>안드로이드 아키텍쳐는 크게 4가지로 분류할 수 있다</p>
<ul>
<li>MVC</li>
<li>MVP</li>
<li>MVVM</li>
<li>MVI</li>
</ul>
<h3 id="mvc">MVC</h3>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/e5cbd8ce-1b28-4aeb-ac48-96943d834c39/image.png" alt=""></p>
<p>Model View Controller로 구성되는 아키텍쳐 패턴으로 웹에서 주로 사용되던 패턴이었다.
문제점은 <strong>안드로이드에서는 Activity나 Fragment가 View와 Controller 역할을 동시에 수행하는 경우가 많다</strong>.
즉 의존성이 강하여 테스트에 용이하지 않고 유지보수가 어렵다
또한 UI 로직이 Controller에 집중되어 복잡한 UI 환경에서는 Controller가 지나치게 비대해지는 문제가 존재했다.
이를 개선하고자 MVP 패턴이 등장하였다.</p>
<h3 id="mvp">MVP</h3>
<p>Model View Presenter로 구성되는 아키텍쳐 패턴으로 MVC의 문제점을 보완하고자 View와 Model사이에 Presenter라는 중재자를 두어서 
사용자의 입력을 View에서 처리하고 그에 따라 Model을 업데이트하며 Model의 데이터를 View에 전달하여 UI를 업데이트한다.
View와 Model이 서로 의존하지 않게 되었으나 여전히 View와 Presenter 사이의 강한 의존성이 존재하였다.</p>
<p>이 의존성을 보다 줄이기 위해 등장한 패턴이 MVVM이다.</p>
<p>MVI 패턴은 별도의 글로 공부해보거나 이후 추가해야겠다.</p>
<h2 id="mvvm이란">MVVM이란?</h2>
<p><img src="https://velog.velcdn.com/images/couch_potato/post/b8af49e4-be43-498b-a18a-0a4e80b0da57/image.png" alt=""></p>
<p>Model View ViewModel로 구성되는 아키첵텍쳐 패턴으로 UI 및 비 UI 코드를 분리하기 위한 UI 아키텍처 디자인 패턴이다.</p>
<p>왜 MVVM을 사용할까?</p>
<ul>
<li>팀 단위의 큰 프로젝트에서의 작업 용이</li>
<li>의존성을 낮추어 테스트 용이</li>
<li>유지보수 용이</li>
</ul>
<p>또한 MVVM은 상태의 유지를 도와준다는 이점이 존재한다.</p>
<p>View에 해당하는 MainActivity에서 Model을 다룬다면 화면 회전 등과 같은 과정에서 데이터의 온전함을 보장할 수 없지만 MVVM은 상태가 유지될 수 있도록 해준다.</p>
<p>ViewModel은 View의 존재를 모른다, 즉 MVVM에서는 ViewModel에서 UI를 조작하지 않는다.
그렇기에 ViewModel은 유지하면서 View만 교체하여 테스트해볼 수도 있고 다른 View에서도 사용하여 코드 재사용성을 높일 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Jetpack Compose의 by에 대해 알아보기]]></title>
            <link>https://velog.io/@couch_potato/Android-Jetpack-Compose%EC%9D%98-by%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@couch_potato/Android-Jetpack-Compose%EC%9D%98-by%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 12 Mar 2024 12:28:54 GMT</pubDate>
            <description><![CDATA[<p>안드로이드에서 remember function을 통해 mutableStateOf로 State를 사용할 때
기본적으로 </p>
<pre><code>val 변수명 =  remember { mutableStateOf()}</code></pre><p>를 사용하지만 by를 사용하면</p>
<pre><code>var 변수명 by remember { mutableStateOf()}</code></pre><p>로 사용할 수 있고 이렇게 by를 사용하면 
state의 value를 가져와서 사용하는 것이 아닌 state 그 자체가 값이기에
변수명.value가 아닌 변수명을 바로 사용함으로써 값을 가져올 수 있다.</p>
<p>그러나 이 경우에는 val로 선언하는 것이 아닌 var로 선언해야 값을 변경해줄 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Jetpack Compose]]></title>
            <link>https://velog.io/@couch_potato/Jetpack-Compose%EC%99%80-xml</link>
            <guid>https://velog.io/@couch_potato/Jetpack-Compose%EC%99%80-xml</guid>
            <pubDate>Tue, 12 Mar 2024 12:20:41 GMT</pubDate>
            <description><![CDATA[<p> Jetpack Compose는 UI 프레임워크로 
 기존에는 xml을 통해 UI 정보를 구축하였으나 구글이 Jetpack Compose를 사용하기로 결정함에 따라 최근에는 Jetpack Compose를 사용하는 추세로 알고 있다.</p>
<p><strong>장점</strong>
반응성이 좋아 속도가 더 빠름, 더 편리
Kotlin에 직접 코드 작성 가능</p>
<p>Flutter도 위젯(Widget)으로 똑같은 개념 사용</p>
<h1 id="remember와-mutable-state">Remember와 Mutable State</h1>
<p>Composable의 state를 유지하고 업데이트하는 데에는 2가지 개념이 필요하다.
1.Remember funcion
2.Mutable state property delegate</p>
<h4 id="remember-function">Remember function</h4>
<p>Jetpack compose에서 지속적이고 기억되는 state를 만들기 위해 사용
Composable이 recomposition되었을 때에도 state를 유지할 수 있도록 해줌
매초 여러번 일어나는 화면의 새로고침인 composable의 재생산에도 작동</p>
<h4 id="mutable-state">Mutable state</h4>
<p>1.Composable이 새로 렌더링되었을 때에도 데이터가 보존되는
recompostion을 통과할 때에도 변하지 않는 state를 만드는 경우
2.값을 바꿨을 때 state값을 업데이트하는 경우</p>
]]></description>
        </item>
    </channel>
</rss>