<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gjk-dev</title>
        <link>https://velog.io/</link>
        <description>틀린 부분을 지적받기 위해 업로드합니다.</description>
        <lastBuildDate>Sun, 01 Mar 2026 07:32:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gjk-dev</title>
            <url>https://velog.velcdn.com/images/gjk-dev/profile/6f48d13b-4f9b-4639-8f1c-f73c481587f9/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gjk-dev. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gjk-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[GitHub Actions의 pull_request 이벤트는 머지 커밋을 기준으로 빌드한다]]></title>
            <link>https://velog.io/@gjk-dev/GitHub-Actions%EC%9D%98-pullrequest-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%8A%94-%EB%A8%B8%EC%A7%80-%EC%BB%A4%EB%B0%8B%EC%9D%84-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%EB%B9%8C%EB%93%9C%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@gjk-dev/GitHub-Actions%EC%9D%98-pullrequest-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%8A%94-%EB%A8%B8%EC%A7%80-%EC%BB%A4%EB%B0%8B%EC%9D%84-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%EB%B9%8C%EB%93%9C%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Sun, 01 Mar 2026 07:32:03 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>CI 과정에서 이상한 컴파일 에러가 발생했다. 분명히 로컬에서는 잘 동작하고, 변경한 파일도 모두 push했는데 CI만 계속 실패하는 상황이었다.</p>
<p>에러 메시지는 다음과 같았다.</p>
<pre><code>e: CoursesViewModel.kt:74:39 No parameter with name &#39;originalCourses&#39; found.</code></pre><p>작업 내용은 <code>CoursesUiState</code>(UI 상태를 담는 data class)에서 <code>originalCourses</code> 필드를 제거하는 것이었고, 관련 코드도 모두 수정해서 push까지 완료한 상태였다. 그런데 CI는 <code>originalCourses</code>를 찾을 수 없다는 에러를 계속 뱉고 있었다.</p>
<h2 id="원인">원인</h2>
<p>결론부터 말하면, <strong>GitHub Actions의 <code>pull_request</code> 이벤트는 PR 브랜치 자체가 아니라 base 브랜치(main)와 머지한 결과를 기준으로 빌드</strong>한다.</p>
<p>상황을 정리하면 이렇다.</p>
<ul>
<li>PR 브랜치에서는 <code>CoursesUiState</code> data class의 <code>originalCourses</code> 필드를 제거</li>
<li>PR을 올려둔 사이, <code>main</code> 브랜치에 새로운 커밋이 추가됨</li>
<li>그 커밋은 <code>CoursesViewModel</code>에서 <code>CoursesUiState</code>를 생성할 때 <code>originalCourses = ...</code> 형태의 <strong>named argument</strong>를 사용 — 내가 수정한 코드가 아닌, <code>main</code>에 새로 올라온 코드임</li>
<li>두 변경사항이 파일의 서로 다른 위치를 건드렸기 때문에 텍스트 레벨의 충돌(conflict)은 발생하지 않음</li>
<li>그러나 CI는 두 브랜치를 합친 결과로 빌드하기 때문에, 이미 제거된 필드명을 named argument로 사용하는 코드가 남아 컴파일 에러 발생</li>
</ul>
<p>이처럼 텍스트 레벨에서는 충돌이 없지만 두 변경사항을 합쳤을 때 코드가 깨지는 경우를 <strong>의미적 충돌(semantic conflict)</strong> 이라 한다. Git은 이를 자동으로 잡아주지 못하고, CI가 잡아준 것이다.</p>
<h2 id="공식-문서-근거">공식 문서 근거</h2>
<h3 id="1-github-actions-공식-문서">1. GitHub Actions 공식 문서</h3>
<p><a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows">Events that trigger workflows - GitHub Docs</a></p>
<p><code>pull_request</code> 이벤트 섹션에 다음과 같이 명시되어 있다.</p>
<blockquote>
<p>Note that <code>GITHUB_SHA</code> for this event is the last merge commit of the pull request merge branch.</p>
</blockquote>
<p><code>GITHUB_SHA</code>는 GitHub Actions에서 <strong>현재 워크플로우가 빌드하는 기준 커밋의 SHA</strong>를 담고 있는 환경변수다. 즉, <code>pull_request</code> 이벤트에서는 PR 브랜치의 최신 커밋이 아니라 <strong>base 브랜치와 머지된 결과 커밋</strong>이 기준이 된다는 것을 의미한다.</p>
<p>또한 <code>pull_request_target</code> 이벤트를 설명하는 부분에서 <code>pull_request</code>와 대비하여 다음과 같이 설명한다.</p>
<blockquote>
<p>This event runs in the context of the default branch of the base repository, <strong>rather than in the context of the merge commit, as the <code>pull_request</code> event does.</strong></p>
</blockquote>
<p><code>pull_request_target</code>은 base 브랜치 컨텍스트에서 실행되는 반면, <code>pull_request</code>는 <strong>머지 커밋 컨텍스트</strong>에서 실행된다는 것을 명확히 설명하고 있다.</p>
<h3 id="2-actionscheckout-관련-문서">2. actions/checkout 관련 문서</h3>
<p>실제로 코드를 체크아웃하는 것은 <code>actions/checkout</code> 액션인데, <code>GITHUB_REF</code> 환경변수의 기본값이 <code>refs/pull/&lt;PR번호&gt;/merge</code>로 설정되기 때문에 별도 설정 없이 <code>actions/checkout</code>을 사용하면 머지된 코드를 체크아웃하게 된다.</p>
<p><a href="https://github.com/orgs/community/discussions/25961">GitHub Community Discussion</a>에서도 이를 확인할 수 있다.</p>
<blockquote>
<p>The <code>GITHUB_REF</code> of pull request event is the PR merge branch. So if you don&#39;t specify the <code>ref</code> part, it checks out the merged source code.</p>
</blockquote>
<p>만약 PR 브랜치의 실제 최신 커밋을 체크아웃하고 싶다면 다음과 같이 명시적으로 지정해야 한다.</p>
<pre><code class="language-yaml">- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}</code></pre>
<h2 id="왜-이렇게-동작하는가">왜 이렇게 동작하는가?</h2>
<p>이 동작은 <strong>의도된 설계</strong>다. CI의 목적은 단순히 &quot;이 브랜치의 코드가 동작하는가?&quot;가 아니라 <strong>&quot;이 PR을 머지했을 때 main이 깨지지 않는가?&quot;</strong> 를 검증하는 것이기 때문이다.</p>
<p>PR 브랜치만 빌드하면 main과의 의미적 충돌이 있어도 통과되어버릴 수 있다. 머지 커밋 기준으로 빌드함으로써 이런 경우를 사전에 잡아낼 수 있다.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>이번 경우의 해결 방법은 간단했다. 최신 <code>main</code>을 PR 브랜치에 rebase하여 두 변경사항이 함께 반영되도록 한 뒤, <code>CoursesViewModel</code>에서 더 이상 존재하지 않는 named argument를 제거하면 된다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>GitHub Actions의 <code>pull_request</code> 이벤트는 PR 브랜치가 아닌 <strong>base 브랜치와 머지한 결과 커밋</strong>을 기준으로 빌드한다.</li>
<li>로컬에서는 성공해도 CI에서 실패하는 경우, base 브랜치의 최신 변경사항을 PR 브랜치에 반영해보자(rebase 또는 merge).</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로세스, 스레드, 코루틴은 왜 등장했는가]]></title>
            <link>https://velog.io/@gjk-dev/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%80-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%96%88%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@gjk-dev/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%80-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%96%88%EB%8A%94%EA%B0%80</guid>
            <pubDate>Fri, 06 Feb 2026 07:13:24 GMT</pubDate>
            <description><![CDATA[<p>프로세스, 스레드, 코루틴이라는 개념은 왜 나온 것일까?
각각이 등장한 배경, 한계를 알아보며 발전 흐름을 파악해보는 것이 이 글의 목적이다.</p>
<h1 id="1-프로세스의-등장-배경">1. 프로세스의 등장 배경</h1>
<p>과거의 매우 비싼 컴퓨터를 개인이 소유할 수는 없었고, 대학교나 국가 연구소, 대기업 등에서 한 대를 들여온 후 모두가 나눠 써야 했다. 각자가 실행하고 싶은 프로그램이 있었겠지만, 초기 컴퓨터는 여러 프로그램을 동시에 실행시킬 수 없었다. 한 번에 하나씩 순차적으로 실행했지만, 이는 비싼 컴퓨터 자원을 비효율적으로 사용하는 것이었다.</p>
<p>따라서 여러 프로그램을 메모리에 올리려고 시도했으나 한 프로그램이 다른 프로그램의 메모리를 덮어쓰는 문제가 발생했다.</p>
<p>이러한 문제를 해결하기 위해 운영체제는 프로그램을 관리 가능한 실행 단위로 나눠 서로 격리하기 시작했다. 각 실행 단위는 다음을 갖는다.</p>
<ul>
<li>독립된 메모리 공간 (코드, 데이터, 스택, 힙)</li>
<li>자신만의 실행 상태(Program Counter, 레지스터 등)</li>
</ul>
<p>이렇게 프로세스(Process)라는 개념이 등장하게 된다. 프로세스란 컴퓨터에서 실행 중인 하나의 프로그램으로, 다른 프로세스의 메모리에 접근할 수 없도록 격리된다.</p>
<h1 id="2-프로세스-기반-모델의-한계-스레드의-등장-배경">2. 프로세스 기반 모델의 한계, 스레드의 등장 배경</h1>
<p>프로세스의 등장으로 안정성이 보장되었지만, 새로운 문제에 직면하게 된다. 현대의 프로그램들이 점점 더 복잡해지기 시작한 것이다. 웹 브라우저는 여러 탭을 동시에 처리해야 하고, 워드 프로세서는 타이핑과 동시에 맞춤법 검사가 이뤄져야 했다. 즉, 하나의 프로그램 안에서 여러 작업을 동시에 처리할 필요가 생겼다.</p>
<p>이를 위해 프로세스를 여러 개 만들어 해결하고자 했으나, 다음과 같은 문제가 발생했다.</p>
<ul>
<li>프로세스 생성과 전환 비용이 너무 큼 (독립된 메모리 공간 할당, 컨텍스트 스위칭 시 메모리 매핑 테이블 교체 등)</li>
<li>같은 프로그램의 프로세스들이 데이터를 공유하려면 복잡한 IPC(Inter-Process Communication) 필요</li>
</ul>
<p>같은 프로그램 안에서 동시 작업을 처리할 때마다 매번 독립된 메모리를 할당받는 것은 비효율적이었다. 프로세스보다 가벼우면서도 프로그램 내에서 자원을 공유할 수 있는 실행 단위가 필요했다.</p>
<p>이렇게 스레드(Thread)라는 개념이 등장하게 된다.</p>
<h1 id="3-스레드">3. 스레드</h1>
<p>스레드는 프로세스 내부의 실행 단위로, 다음과 같은 특징을 가진다. </p>
<ul>
<li>같은 프로세스의 스레드들은 코드, 데이터, 힙 영역을 공유 </li>
<li>각 스레드는 독립적인 스택과 레지스터 값을 가짐</li>
<li>프로세스보다 생성과 전환 비용이 훨씬 적음</li>
</ul>
<p>메모리를 공유하므로 생성 비용이 적고, 컨텍스트 스위칭 시에도 메모리 매핑 테이블을 교체할 필요가 없어 훨씬 빠르다. 또한 힙 영역을 공유하므로, 복잡한 IPC 없이 변수나 객체를 직접 공유할 수 있다.</p>
<h1 id="4-스레드-모델의-한계-코루틴의-등장-배경">4. 스레드 모델의 한계, 코루틴의 등장 배경</h1>
<p>스레드를 통해 가볍고 빠른 동시 처리가 가능해졌지만, 또 다른 문제가 나타난다. 현대의 어플리케이션들이 처리해야 할 동시 작업의 규모가 폭발적으로 증가한 것이다. 웹 서버는 수천 개의 동시 접속을 처리해야 하고, 채팅 앱은 수만 명의 사용자와 실시간으로 통신해야 했다.</p>
<p>스레드로 이를 해결하려 했지만, 다음과 같은 문제가 발생했다.</p>
<ul>
<li>스레드 생성과 관리에도 상당한 비용이 듦 (독립적인 스택 메모리, 스레드 전환 시 컨텍스트 스위칭 오버헤드)</li>
<li>비동기 작업(네트워크 요청, 파일 읽기 등)을 처리할 때 스레드가 대기 상태로 낭비됨</li>
<li>비동기 프로그래밍을 위해 콜백을 사용하면서 코드 복잡도 급증 (콜백 지옥)</li>
</ul>
<p>스레드보다 훨씬 가벼우면서도 수만 개의 동시 작업을 효율적으로 처리할 수 있는 방식이 필요했다. 또한 동기 코드처럼 읽기 쉬운 비동기 프로그래밍 방식도 요구되었다.</p>
<p>이렇게 코루틴(Coroutine)이라는 개념이 등장하게 된다.</p>
<h1 id="5-코루틴">5. 코루틴</h1>
<p>코루틴은 프로그램 레벨에서 관리되는 실행 단위로, 다음과 같은 특징을 가진다.</p>
<ul>
<li>수 KB의 가벼운 메모리 사용 (스레드는 약 1MB)</li>
<li>프로그램 내부에서 관리되어 OS 호출 없이 생성과 전환이 가능해 매우 빠름</li>
</ul>
<p>비동기 작업이 시작되면 코루틴은 스레드를 블로킹하지 않고 일시 중단(suspend)되며, 그 사이 스레드는 다른 코루틴을 실행할 수 있다. 작업이 완료되면 코루틴이 재개(resume)되어 중단되었던 지점부터 계속 실행된다. 즉 코루틴은 스레드 내에서 협력적으로 동작한다.</p>
<p>이를 통해 기존 문제들이 해결되었다.</p>
<ul>
<li>수만 개의 코루틴을 적은 수의 스레드로 효율적으로 처리할 수 있다. I/O 대기 시간 동안 스레드가 낭비되지 않고 다른 코루틴을 실행할 수 있기 때문이다.</li>
<li>async/await, suspend 같은 키워드를 통해 비동기 코드를 동기 코드처럼 순차적으로 작성할 수 있어, 콜백 지옥 없이도 읽기 쉬운 비동기 프로그래밍이 가능해졌다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose에서의 UI 이벤트 처리 구조 개선기]]></title>
            <link>https://velog.io/@gjk-dev/Jetpack-Compose%EC%97%90%EC%84%9C%EC%9D%98-UI-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC-%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@gjk-dev/Jetpack-Compose%EC%97%90%EC%84%9C%EC%9D%98-UI-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC-%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Sat, 11 Oct 2025 09:35:43 GMT</pubDate>
            <description><![CDATA[<h1 id="서론"><strong>서론</strong></h1>
<p>Jetpack Compose로 프로젝트를 진행하며 UI 이벤트 처리 구조에 대한 고민이 생겼다.</p>
<p>여기서 UI 이벤트란 UI 계층에서 다뤄져야 할 동작들을 의미한다.</p>
<p>이 글에서는 이벤트 처리 구조를 고민하고 점진적으로 개선한 과정을 공유한다.</p>
<h1 id="composable-내부에서-이벤트-처리">Composable 내부에서 이벤트 처리</h1>
<p>초기에는 Composable 내부에서 이벤트를 직접 처리했다.</p>
<p>아래 예시는 그때 작성했던 코드로, 화면 이동을 포함한 모든 이벤트를 <code>Screen</code> 내부에서 처리하고 있다.</p>
<pre><code class="language-kotlin">@Composable
fun CardsScreen() {
    val addLauncher = rememberLauncherForActivityResult(...)
    val editLauncher = rememberLauncherForActivityResult(...)

    Scaffold() {
        CardsContent(
            navigateToCardAdditionActivity = { addLauncher.launch(Intent(...)) },
            navigateToCardEditingActivity = { card -&gt; editLauncher.launch(Intent(...)) }
        )
    }
}</code></pre>
<p>위 코드는 다음과 같은 문제점을 갖는다.</p>
<ul>
<li><strong>화면 이동의 책임이 Composable 안에 존재</strong><ul>
<li>화면을 그리는 코드와 이벤트를 처리하는 코드가 함께 존재해 가독성이 떨어진다.</li>
<li>나중에 네비게이션 방식이 바뀌면 <code>Composable</code> 내부를 수정해야 해 유연성이 낮다.</li>
</ul>
</li>
<li><strong>테스트가 어려움</strong><ul>
<li>화면 이동 로직이 <code>ActivityResultLauncher</code>에 직접 의존해 단위 테스트가 불가능했다.</li>
<li>계측 테스트로만 검증할 수 있었고, 테스트 속도가 떨어진다.</li>
</ul>
</li>
</ul>
<h1 id="activity로-이벤트-처리-책임-이동"><strong>Activity로 이벤트 처리 책임 이동</strong></h1>
<p><code>Activity</code>는 이벤트 처리, <code>Screen</code>은 UI 렌더링만을 담당하기로 책임을 구분해보았다.</p>
<pre><code class="language-kotlin">@Composable
fun CardsScreen(
    onAddCard: () -&gt; Unit,
    onEditCard: (CardUiModel) -&gt; Unit
) {
    Scaffold {
        CardsContent(
            navigateToCardAdditionActivity = onAddCard,
            navigateToCardEditingActivity = onEditCard
        )
    }
}</code></pre>
<pre><code class="language-kotlin">class CardsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            val addLauncher = rememberLauncherForActivityResult(...)
            val editLauncher = rememberLauncherForActivityResult(...)

            CardsScreen(
                onAddCard = { addLauncher.launch(Intent(...)) },
                onEditCard = { card -&gt; editLauncher.launch(Intent(...)) }
            )
        }
    }
}</code></pre>
<p>이 구조를 통해 <code>Screen</code>은 화면을 그리는 역할에 집중하고,
<code>Activity</code>에서 실제 이벤트 처리를 담당하게 되어 책임이 명확히 분리되었다.</p>
<p>단위 테스트에서는 버튼 클릭 시 콜백이 호출되는지만 검증하면 되므로, 테스트도 훨씬 단순해졌다.</p>
<pre><code class="language-kotlin">@Test
fun onAddCardButtonClick_callsOnAddCard() {
    // given
    var called = false

    composeTestRule.setContent {
        CardsScreen(onAddCard = { called = true }, onEditCard = {})
    }

    // when
    composeTestRule.onNodeWithText(&quot;카드 추가&quot;).performClick()

    // then
    assertTrue(called)
}</code></pre>
<p>하지만 아직 문제가 남아있었다. 현재는 <code>Activity</code>에서 다루는 이벤트가 2가지 뿐이지만 추후 몇 개가 될지 모른다. 다루는 이벤트가 늘어날수록 파라미터의 개수가 늘어날 것이다.</p>
<pre><code class="language-kotlin">@Composable
fun CardsScreen(
    onAddCard: () -&gt; Unit,
    onEditCard: (CardUiModel) -&gt; Unit,
    onDoSomething: () -&gt; Unit,
    ...
) { ... }</code></pre>
<p>이렇게 되면 추후 새로운 이벤트를 추가할 때마다 <code>CardsScreen</code>의 모든 호출부가 수정되어야 한다.</p>
<p>또한 다음과 같이 테스트나 프리뷰를 위한 보일러 플레이트 코드도 늘어날 것이다.</p>
<pre><code class="language-kotlin">CardsScreen(
    onAddCard = {},
    onEditCard = {},
    onDoSomething = {},
    ...
)</code></pre>
<h1 id="onxxx-콜백-통합--onuievent"><strong>onXXX 콜백 통합 — onUiEvent</strong></h1>
<p>여기서 떠오른 것은 기존 XML 기반 뷰 시스템에서 이벤트들을 추상화하고 처리하던 패턴이었다.</p>
<pre><code class="language-kotlin">viewModel.uiEvent.observe(this) { event -&gt;
    when (event) {
        is CardUiEvent.NavigateToConfirmation -&gt; startActivity(Intent(this, ConfirmActivity::class.java))
        is CardUiEvent.ShowToast -&gt; Toast.makeText(this, event.message, Toast.LENGTH_SHORT).show()
    }
}</code></pre>
<p>Compose에서도 유사하게, 여러 UI 이벤트를 하나의 <code>XXXUiEvent</code>로 추상화하여 <code>Activity</code>에서 처리하도록 적용해보기로 했다.</p>
<pre><code class="language-kotlin">sealed interface CardEditingUiEvent {
    data class OnCardEdited(val editedCard: CardUiModel) : CardEditingUiEvent
    data object OnSubmit : CardEditingUiEvent
    data object OnCancel : CardEditingUiEvent
}</code></pre>
<pre><code class="language-kotlin">class CardEditingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CardEditingScreen(
                state = viewModel.uiState,
                onEvent = ::handleUiEvent
            )
        }
    }

    private fun handleUiEvent(event: CardEditingUiEvent) {
        when (event) {
            is CardEditingUiEvent.OnCardEdited -&gt; viewModel.updateCard(event.editedCard)
            CardEditingUiEvent.OnSubmit -&gt; navigateToConfirmation()
            CardEditingUiEvent.OnCancel -&gt; finish()
        }
    }
}</code></pre>
<p>이를 통해 개별 <code>onXXX</code> 콜백들을 하나의 <code>onUiEvent</code>로 통합할 수 있었다.
덕분에 프리뷰 작성이나 테스트 환경에서도 코드량이 크게 줄었다.</p>
<h1 id="stateholder에서-발생하는-이벤트도-한-곳에서-처리"><strong>StateHolder에서 발생하는 이벤트도 한 곳에서 처리</strong></h1>
<p>UI Event는 Composable 뿐 아니라 <code>StateHolder</code>에서 트리거되기도 한다.
서버 통신 결과를 나타내는 등의 이벤트가 그렇다.</p>
<p><code>StateHolder</code>에서 발생하는 이벤트는 <code>LaunchedEffect</code>를 사용해 변화를 감지하고,
Composable 이벤트와 동일한 방식으로 <code>onUiEvent</code>를 실행하도록 변경했다.</p>
<pre><code class="language-kotlin">class CardAdditionActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val stateHolder = rememberCardAdditionStateHolder()
            val state = stateHolder.uiState
            val event = stateHolder.uiEvent

            val onUiEvent: (CardAdditionUiEvent) -&gt; Unit = { event -&gt;
                when (event) {
                    CardAdditionUiEvent.AddCardSuccess -&gt; showToast(&quot;Success&quot;)
                    CardAdditionUiEvent.AddCardFailure -&gt; showToast(&quot;Failure&quot;)
                    CardAdditionUiEvent.NavigateBack -&gt; finish()
                    is CardAdditionUiEvent.UpdateCardNumber -&gt; stateHolder.updateCardNumber(evt.cardNumber)
                    is CardAdditionUiEvent.UpdateBankType -&gt; stateHolder.updateBankType(evt.bankType)
                }
            }

            LaunchedEffect(event) {
                event?.let {
                    onUiEvent(it)
                    stateHolder.clearEvent()
                }
            }

            CardAdditionScreen(state = state, onUiEvent = onUiEvent)
        }
    }
}</code></pre>
<p>이제 모든 UI Event를 <code>Activity</code> 내부에서 일괄 처리하게 되어 유지보수가 편리해질 것이다.</p>
<h1 id="결론"><strong>결론</strong></h1>
<p>처음에는 <code>Composable</code> 내부에서 모든 이벤트를 처리했지만, 이 방식은 책임이 뒤섞이고 테스트가 어려운 구조였다.</p>
<p>이후 <code>Activity</code>로 이벤트 처리를 옮겨 UI와 로직을 분리했으나, 이벤트가 늘어날수록 콜백 파라미터가 증가하는 문제가 생겼다.</p>
<p>이벤트를 추상화하고 <code>onUiEvent</code>로 이벤트 처리를 한 곳으로 통합하며 이 문제를 해결할 수 있었다.</p>
<p>이제 새로운 이벤트가 추가되어도 <code>Composable</code> 시그니처는 그대로 유지되고, <code>Activity</code>의 <code>when</code> 블록만 수정하면 된다.</p>
<p>또한 <code>StateHolder</code>에서 발생하는 이벤트까지 한 곳에서 처리해 일관된 이벤트 처리 흐름을 구축할 수 있었다.</p>
<p>UI 이벤트를 어떻게 처리할지에 따라 코드의 구조와 유지보수성이 크게 달라질 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Parameter vs CompositionLocal: 언제 무엇을 사용해야 하는가?]]></title>
            <link>https://velog.io/@gjk-dev/Parameter-vs-CompositionLocal-%EC%96%B8%EC%A0%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@gjk-dev/Parameter-vs-CompositionLocal-%EC%96%B8%EC%A0%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Sun, 21 Sep 2025 12:14:31 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>Composable 내부에서 액티비티의 메서드 호출이 필요한 상황은 종종 발생한다.</p>
<p>예를 들어 뒤로가기, 권한 요청, 외부 앱 호출 등 시스템과 상호작용해야 하는 경우가 있다.</p>
<pre><code class="language-kotlin">@Composable
private fun Screen(
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier
    ) {
        IconButton(onClick = {
            // TODO: 액티비티 종료
        }) {
            Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
        }
    }
}</code></pre>
<p>이를 해결하는 방법은 대표적으로 두 가지가 있을 것이다. 인자를 통해 <code>Activity</code>에서 내려받거나, <code>CompositionLocal</code>을 사용하거나.</p>
<pre><code class="language-kotlin">// 방법 1: Activity에서 인자를 통해 내려주기
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestAppTheme {
                Screen(
                    onBackButtonClick = ::finish,
                    modifier = Modifier.fillMaxSize()
                )
            }
        }
    }
}

@Composable
private fun Screen(
    onBackButtonClick: () -&gt; Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier
    ) {
        IconButton(onClick = onBackButtonClick) {
            Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
        }
    }
}</code></pre>
<pre><code class="language-kotlin">// 방법 2: CompositionLocal을 사용하기
@Composable
private fun Screen(
    modifier: Modifier = Modifier,
) {
    val activity: Activity? = LocalActivity.current

    Column(
        modifier = modifier
    ) {
        IconButton(onClick = { activity?.finish() }) {
            Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
        }
    }
}</code></pre>
<p>인자와 <code>CompositionLocal</code>, 어떤 것을 선택할지 결정하는 기준은 무엇일까?</p>
<h1 id="본론">본론</h1>
<h2 id="compositionlocal"><code>CompositionLocal</code>?</h2>
<p><code>CompositionLocal</code>은 Composable 트리에 데이터를 암시적으로 전달하는 도구이다.</p>
<p>일반적으로 UI 트리의 하위에 있는 컴포저블에 데이터를 전달할 때는 인자를 사용해야 한다. 이렇게 하면 각 컴포저블의 의존성을 명시적으로 확인할 수 있다.</p>
<p>하지만 거의 모든 컴포저블에서 자주 사용되는 값들(테마 등)의 경우도 전부 인자를 사용하면 번거로울 수 있다.</p>
<pre><code class="language-kotlin">@Composable
fun MyApp() {
    val colors = colors()
      ...
}

// 트리 계층 구조의 하위에 존재하는 컴포저블
@Composable
fun SomeTextLabel(labelText: String, colors: Colors  ...) {
    Text(
        text = labelText,
        color = colors.onPrimary
        ...
    )
}</code></pre>
<p>따라서 인자 없이 암시적으로 필요한 의존성을 충족시키기 위해 컴포즈는 <code>CompositionLocal</code>을 사용한다. 이를 통해 UI 트리의 스코프 동안 필요한 데이터를 제공하는 객체를 생성하고 암시적으로 활용할 수 있다.</p>
<p>대표적으로 <code>MaterialTheme</code>가 내부적으로 <code>CompositionLocal</code>을 사용하고 있다.</p>
<pre><code class="language-kotlin">object MaterialTheme {
    val colorScheme: ColorScheme
        @Composable @ReadOnlyComposable get() = LocalColorScheme.current

    val typography: Typography
        @Composable @ReadOnlyComposable get() = LocalTypography.current

    val shapes: Shapes
        @Composable @ReadOnlyComposable get() = LocalShapes.current
}</code></pre>
<h2 id="언제-compositionlocal을-사용해야-하는가"><strong>언제 <code>CompositionLocal</code>을 사용해야 하는가?</strong></h2>
<p>안드로이드 공식문서에서 <code>CompositionLocal</code>을 사용하기 좋은 몇 가지 조건을 확인할 수 있다.</p>
<ul>
<li><p><strong>좋은 기본값을 가질 수 있을 때</strong></p>
<pre><code class="language-kotlin">  val LocalSpacing = compositionLocalOf { 0.dp } // 기본값: 0dp
  val LocalUser = compositionLocalOf&lt;User&gt; { error(&quot;No User provided&quot;) } // 기본값 X

  @Composable
  fun Example() {
      val spacing = LocalSpacing.current
      Text(
          &quot;Hello!&quot;,
          modifier = Modifier.padding(spacing)
      )

      val user = LocalUser.current
      Text(&quot;Hello, ${user.name}!&quot;)
  }</code></pre>
<p>  기본값이 없으면 테스트나 프리뷰에서 매번 값을 명시적으로 제공해야 해서 번거롭고, 실수로 누락 시 오류로 이어질 수 있다.</p>
</li>
<li><p><strong>트리 전역 또는 특정 하위 트리에 걸쳐 의미 있는 값일 때</strong>
<code>CompositionLocal</code>은 하위 모든 컴포저블이 잠재적으로 접근할 수 있다는 점이 특징이다. 따라서 일부 컴포저블만 쓰는 값이라면 적합하지 않다.</p>
<pre><code class="language-kotlin">val LocalSpacing = compositionLocalOf { 8.dp }

@Composable
fun MyApp() {
    CompositionLocalProvider(LocalSpacing provides 16.dp) {
        Column {
            Header()
            Content()
            Footer()
        }
    }
}

@Composable
fun Header() {
    val spacing = LocalSpacing.current
    Text(&quot;Header&quot;, modifier = Modifier.padding(spacing))
}

@Composable
fun Content() {
    val spacing = LocalSpacing.current
    Text(&quot;Content&quot;, modifier = Modifier.padding(spacing))
}

@Composable
fun Footer() {
    val spacing = LocalSpacing.current
    Text(&quot;Footer&quot;, modifier = Modifier.padding(spacing))
}</code></pre>
<p>여기서 <code>LocalSpacing</code>은 <strong>트리 전역적으로 의미가 있는 값</strong>이므로 <code>CompositionLocal</code>을 사용하는 것이 자연스럽다.</p>
</li>
<li><p><strong>뷰모델 같은 화면 단위의 구체적 의존성을 담지 않을 것</strong>
  화면 전체의 <code>ViewModel</code>을 <code>CompositionLocal</code>로 노출하는 건 권장되지 않는다. 모든 자식이 접근할 수 있지만 실제로 필요한 건 일부 컴포저블뿐이기 때문이다.</p>
<pre><code class="language-kotlin">  // 부적합한 예시
  val LocalCardViewModel = compositionLocalOf&lt;CardViewModel&gt; { error(&quot;No VM&quot;) }

  @Composable
  fun CardScreen() {
      val viewModel = LocalCardViewModel.current
      Column {
          CardHeader()
          CardContent()
          CardFooter()
      }
  }

  @Composable
  fun CardHeader() {
      val viewModel = LocalCardViewModel.current
      Text(&quot;User: ${viewModel.userName}&quot;)
  }</code></pre>
</li>
</ul>
<h2 id="localactivity"><code>LocalActivity</code>?</h2>
<p><code>LocalActivity</code>는 시스템 레벨 API에 접근할 수 있도록 Compose에서 제공하는 특수한 <code>CompositionLocal</code>이다.</p>
<p>이를 통해 <code>Activity</code>가 필요한 권한 요청, <code>startActivityForResult</code>, <code>WindowInsets</code> 제어 같은 상황에서 사용할 수 있다.</p>
<p>그러나 <code>LocalActivity</code>를 남용하면, 컴포저블이 <strong><code>Activity</code>에 직접 의존</strong>하게 되고, 재사용성과 테스트성이 떨어진다.</p>
<p>따라서 일반적인 UI 이벤트(뒤로가기 버튼 등)는 <strong>콜백 인자를 통해 외부에서 전달</strong>하는 방식이 더 바람직하다.</p>
<pre><code class="language-kotlin">@Composable
fun CardScreen(onBackClick: () -&gt; Unit) {
    Button(onClick = onBackClick) { Text(&quot;Back&quot;) }
}</code></pre>
<p>이 방식은 화면 구조가 바뀌거나, Compose Navigation을 사용하더라도 유연하게 대응할 수 있다.</p>
<h1 id="결론">결론</h1>
<ul>
<li><strong><code>CompositionLocal</code></strong>: 전역적, 환경적 값에 적합. 기본값이 안전하고, 거의 모든 하위 컴포저블이 필요로 하는 값이어야 한다.</li>
<li><strong>콜백 인자</strong>: 특정 화면/컴포저블 동작에 속하는 값이나 이벤트에는 기본적으로 권장되는 방식. 재사용성과 테스트 용이성이 높다.</li>
<li><strong><code>LocalActivity</code></strong>: 학습용이나 불가피한 시스템 API 접근 시 사용할 수 있지만, 일반 UI 이벤트에서는 인자 전달 방식을 우선적으로 선택하는 것이 좋다.</li>
</ul>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://developer.android.com/develop/ui/compose/compositionlocal">Locally scoped data with CompositionLocal  |  Jetpack Compose  |  Android Developers</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UI State를 활용하면 상태 패턴인가?: UI State, StateHolder 그리고 상태 패턴의 State와의 차이]]></title>
            <link>https://velog.io/@gjk-dev/UI-State%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%83%81%ED%83%9C-%ED%8C%A8%ED%84%B4%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@gjk-dev/UI-State%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%83%81%ED%83%9C-%ED%8C%A8%ED%84%B4%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 16 Sep 2025 10:28:17 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>Jetpack Compose를 공부하며, <strong>State Holder</strong>라는 개념에 대해 다루게 되었다. 이 과정에서 State Holder는 <strong>UI State</strong>와 무엇이 다른지 혼동되었다.</p>
<h2 id="state-holder">State Holder</h2>
<p><strong>State Holder</strong>란 UI에 표시될 상태와 로직의 묶음이다. 이를 별도로 분리함으로써, UI 렌더링과 상태 관리 로직을 분리할 수 있다.</p>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count by mutableStateOf(0)
        private set

    fun increase() {
        count++
    }

    fun decrease() {
        count--
    }
}</code></pre>
<p>여기서 말하는 UI 상태가 바로 <strong>UI State</strong>다. UI State는 화면에 표시될 데이터와, UI를 설명하는 속성들의 집합으로 구성된다.</p>
<h2 id="state-holder가-jetpack-compose에-대한-의존성을-가져도-될까">State Holder가 Jetpack Compose에 대한 의존성을 가져도 될까?</h2>
<p>하지만 State Holder가 Compose에 의존해도 되는지 의문이 들었다. UI State는 화면에 대한 <strong>추상화된 정보</strong>일 뿐, <strong>특정한 UI 프레임워크에 종속될 필요는 없었다.</strong></p>
<p>따라서 State Holder는 순수하게 상태와 그에 대한 로직만을 갖기로 했다.</p>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count: Int = 0
        private set

    fun increase() {
        count++
    }

    fun decrease() {
        count--
    }
}</code></pre>
<p>그리고 이를 컴포즈를 사용하는 곳에서 <code>State</code>의 형태로 만들어주는 것이다.</p>
<pre><code class="language-kotlin">@Composable
fun Counter(modifier: Modifier = Modifier) {
    val stateHolder by remember { mutableStateOf(CounterStateHolder()) }

    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Button(onClick = stateHolder::decrease) {
            Text(&quot;-&quot;)
        }

        Text(text = stateHolder.count.toString())

        Button(onClick = stateHolder::increase) {
            Text(&quot;+&quot;)
        }
    }
}

@Preview
@Composable
private fun CounterPreview() {
    Counter()
}</code></pre>
<p>하지만 이렇게 사용하니, <code>mutableStateOf</code>에 담긴 객체가 변하지 않아 리컴포지션이 발생하지 않았다. 즉, <code>count</code> 값이 바뀌어도 Compose는 이를 감지할 수 없었다.</p>
<p>따라서 State Holder를 완전한 불변 객체로 만들고, <code>copy</code> 메서드를 사용해 새로운 객체로 상태를 갱신하도록 했다.</p>
<pre><code class="language-kotlin">data class CounterStateHolder(
    val count: Int = 0,
) {
    fun increase(): CounterStateHolder = copy(count = count + 1)

    fun decrease(): CounterStateHolder = copy(count = count - 1)
}
</code></pre>
<pre><code class="language-kotlin">@Composable
fun Counter(modifier: Modifier = Modifier) {
    var stateHolder by remember { mutableStateOf(CounterStateHolder()) }

    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Button(onClick = { stateHolder = stateHolder.decrease() }) {
            Text(&quot;-&quot;)
        }

        Text(text = stateHolder.count.toString())

        Button(onClick = { stateHolder = stateHolder.increase() }) {
            Text(&quot;+&quot;)
        }
    }
}</code></pre>
<p>이제 State Holder는 Compose에 의존하지 않으며 프로그램 또한 정상적으로 동작한다.</p>
<h2 id="stateholder-vs-uistate"><strong>StateHolder vs UiState</strong></h2>
<p>코드를 보니 이제 무언가 어색해지기 시작했다. State Holder는 이름 그대로 State를 담고 있어야 하지 않나? 하지만 내 코드는 반대로 <code>mutableStateOf</code> 함수에 State Holder 인스턴스를 전달하고 있었다.</p>
<p><code>CounterStateHolder</code> 코드를 보면 상태와 그에 대한 변경 기능을 갖고 있다. 단순히 이 객체의 이름을 <code>CounterUiState</code>로 바꾸면 더욱 자연스러울 것 같다.</p>
<pre><code class="language-kotlin">data class CounterUiState(
    val count: Int = 0,
) {
    fun increase(): CounterUiState = copy(count = count + 1)

    fun decrease(): CounterUiState = copy(count = count - 1)
}</code></pre>
<pre><code class="language-kotlin">@Composable
fun Counter(modifier: Modifier = Modifier) {
    var state by remember { mutableStateOf(CounterUiState()) }

    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Button(onClick = { state = state.decrease() }) {
            Text(&quot;-&quot;)
        }

        Text(text = state.count.toString())

        Button(onClick = { state = state.increase() }) {
            Text(&quot;+&quot;)
        }
    }
}</code></pre>
<p>이쯤 되니 혼란스러움이 극에 달했다. <strong>State Holder가 결국 UI State인가?</strong></p>
<p>State Holder는 상태와 해당 상태를 변경시키는 메서드를 갖고 있다. 이제 보니 상태 패턴의 상태와도 같아 보인다. 그렇다면 <strong>왜 State Holder라는 이름을 쓴걸까? State Holder와 UI State는 무엇이 다른걸까?</strong></p>
<h1 id="본론">본론</h1>
<p>각 용어를 정확히 정리해야 용어 간 혼동이 생기지 않을 것 같다. 상태 패턴과 State, UI State, 컴포즈의 State, State Holder, 마지막으로 State, State Holder, ViewModel의 관계까지 알아보자.</p>
<h2 id="상태-패턴">상태 패턴</h2>
<p><strong>상태 패턴</strong>(State Pattern)은 객체 지향 방식으로 상태 기계를 구현하는 디자인 패턴이다.</p>
<p>객체가 내부 상태에 따라 행동을 다르게 하도록 설계하고, 조건문을 사용해 분기하는 대신 <strong>상태 자체를 객체로 캡슐화</strong>하여 행동을 위임한다.</p>
<p>즉, 상태 패턴에서 말하는 상태란 <strong>객체가 가지고 있는 현재 조건과 상황</strong>이다. 상태는 수행 가능한 행동을 정의하고, 그 행동에 따라 <strong>다음 상태가 무엇인지</strong>를 스스로 결정한다.</p>
<h3 id="상태-정의하기">상태 정의하기</h3>
<p>문의 상태는 <code>열려있는 상태</code>, <code>닫혀있는 상태</code>, <code>잠겨있는 상태</code> 총 3가지로 구성했다.</p>
<pre><code class="language-kotlin">interface DoorState {
    fun open(): DoorState

    fun close(): DoorState

    fun lock(): DoorState
}</code></pre>
<h3 id="상태별-동작-정의하기">상태별 동작 정의하기</h3>
<p>각 상태 객체는 <strong>자신이 어떤 행동을 허용할지, 다음 상태를 무엇으로 바꿀지</strong>를 스스로 알고 있다. 상태가 바뀌지 않는 경우에는 <code>this</code> 를 그대로 반환한다.</p>
<pre><code class="language-kotlin">class Open : DoorState {
    override fun open() = this

    override fun close() = Closed()

    override fun lock() = this
}

class Closed : DoorState {
    override fun open() = Open()

    override fun close() = this

    override fun lock() = Locked()
}

class Locked : DoorState {
    override fun open() = this

    override fun close() = this

    override fun lock() = this
}</code></pre>
<p>예를 들어 <code>Closed</code> 상태에서 <code>lock()</code>을 호출하면 <code>Locked</code> 상태로 전환된다. 반면, 이미 <code>Locked</code> 상태라면 <code>lock()</code>을 다시 호출해도 아무 일도 일어나지 않으며 그대로 <code>Locked</code> 상태를 유지한다.</p>
<h3 id="상태를-사용하는-객체">상태를 사용하는 객체</h3>
<p><code>Door</code> 클래스는 단순히 현재 상태를 들고 있고, 요청이 들어올 때 <strong>현재 상태 객체에 동작을 위임한 뒤 반환된 상태로 교체</strong>한다.</p>
<pre><code class="language-kotlin">class Door {
    var state: DoorState = Closed()
        private set

    fun open() {
        state = state.open()
    }

    fun close() {
        state = state.close()
    }

    fun lock() {
        state = state.lock()
    }
}</code></pre>
<p>즉, <strong>상태 전환 로직은 <code>Door</code> 안에 있지 않고 상태 객체 안에 있다.</strong> <code>Door</code>는 그저 “지금 상태가 뭘로 바뀌었는지” 받아서 반영할 뿐이다.</p>
<p>이를 통해 <code>if-else</code>나 <code>when</code> 같은 분기문이 사라지고, 상태별 동작이 각 상태 객체 안으로 캡슐화된다.</p>
<h2 id="ui-state">UI State</h2>
<p><strong>UI State는 보여져야 하는 것들을 의미</strong>한다.</p>
<p>따라서 <strong>UI는 UI State의 시각적 표현</strong>이고, <strong>UI State가 변하면 이는 UI에 반영</strong>되어야 한다.
<img src="https://velog.velcdn.com/images/gjk-dev/post/ba945405-0392-454d-b88e-6487201affaa/image.png" alt=""></p>
<pre><code class="language-kotlin">data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List&lt;NewsItemUiState&gt; = listOf(),
    val userMessages: List&lt;Message&gt; = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)</code></pre>
<h3 id="상태-패턴의-state와의-차이점">상태 패턴의 State와의 차이점</h3>
<ul>
<li><strong>상태 패턴의 State는 객체의 행동을 결정하는 규칙</strong><ul>
<li>&quot;어떤 메서드를 호출했을 때 어떻게 반응할까?&quot;를 정의한다.<ul>
<li>문이 <code>Locked</code> 상태라면 <code>open()</code> 메서드 호출 시 아무 일도 일어나지 않음</li>
</ul>
</li>
<li><strong>능동적</strong>(상태 전환의 주체)</li>
</ul>
</li>
<li><strong>UI State는 UI를 그리기 위한 데이터</strong><ul>
<li>&quot;화면에 무엇을 보여줄까?&quot;를 정의한다.<ul>
<li>버튼이 비활성화되어야 하는지, 로딩 스피너가 보이는지, 리스트에 어떤 아이템이 있는지 등</li>
</ul>
</li>
<li><strong>수동적</strong>(상태 표현만 담당)</li>
</ul>
</li>
</ul>
<p>즉 아래의 코드는 상태 패턴의 State도 아니고, UI State도 아니다.</p>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count by mutableStateOf(0)
        private set

    fun increase() {
        count++
    }

    fun decrease() {
        count--
    }
}</code></pre>
<ul>
<li><code>count</code> 값을 변화시킬 뿐 객체의 행동 방식(<code>increase</code>, <code>decrease</code>의 세부 구현)이 달라지지 않음</li>
<li>UI에 표현될 <code>count</code> 뿐 아니라 해당 값을 변경하는 역할까지 포함하고 있음</li>
</ul>
<p>같은 이유로 아래의 코드 또한 상태 패턴의 State도 아니고, UI State도 아니다. 값과 그 값을 변형하는 함수를 함께 담아둔 단순한 불변 객체일 뿐이다.</p>
<pre><code class="language-kotlin">data class CounterStateHolder(
    val count: Int = 0,
) {
    fun increase(): CounterStateHolder = copy(count = count + 1)

    fun decrease(): CounterStateHolder = copy(count = count - 1)
}</code></pre>
<h2 id="compose에서-말하는-state">Compose에서 말하는 State</h2>
<p>Compose에서 UI를 업데이트하려면 새로운 UI State를 인자로 동일한 컴포저블을 호출해야 한다. 즉 이 상태가 업데이트될 때마다 재구성(recomposition)을 발생시켜야한다.</p>
<p>이를 위해 Compose에서는 <code>State</code> 객체를 활용한다. Compose에서는 <code>State</code>가 갖고 있는 값(<code>value</code>)이 <strong>변경</strong>되면, 해당 <code>State</code>를 사용하는 <strong>UI만 다시 그리는</strong> 재구성을 진행한다.</p>
<pre><code class="language-kotlin">@Stable
public interface State&lt;out T&gt; {
    public val value: T
}</code></pre>
<p>따라서 UI State를 <code>State</code> 객체로 감싸면, <strong>UI State가 변할 때 UI에 반영</strong>되도록 할 수 있다.</p>
<p>이 때문에 <code>CounterStateHolder</code>를 불변 객체로 만든 후 <code>State</code> 의 <code>value</code>를 다른 객체로 변경하여 UI에 반영되도록 한 것이다.</p>
<pre><code class="language-kotlin">data class CounterUiState(
    val count: Int = 0,
) {
    fun increase(): CounterUiState = copy(count = count + 1)

    fun decrease(): CounterUiState = copy(count = count - 1)
}</code></pre>
<pre><code class="language-kotlin">@Composable
fun Counter(modifier: Modifier = Modifier) {
    var state by remember { mutableStateOf(CounterUiState()) }

    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Button(onClick = { state = state.decrease() }) {
            Text(&quot;-&quot;)
        }

        Text(text = state.count.toString())

        Button(onClick = { state = state.increase() }) {
            Text(&quot;+&quot;)
        }
    }
}</code></pre>
<h3 id="왜-위-코드는-적절하지-않은가"><strong>왜 위 코드는 적절하지 않은가?</strong></h3>
<p>위 코드에서 <code>CounterUiState</code>는 이름상 <strong>UI State</strong>를 의미하지만, 실제로는 <strong>값을 변경하는 메서드</strong>(<code>increase</code>, <code>decrease</code>)<strong>까지 포함</strong>하고 있다. 이런 구조는 개발자 간의 혼동을 야기할 수 있다.</p>
<h2 id="state-holder-1">State Holder</h2>
<p><strong>State Holder</strong>란 UI State를 저장하고 상태 변환 로직을 제공하는 객체이다.</p>
<ul>
<li><strong>UI State와의 차이점</strong>: 단순히 화면에 표시될 데이터를 담는 것이 아니라, 상태를 변경하는 기능을 가진다.</li>
<li><strong>상태 패턴의 State와의 차이점</strong>: 객체의 행동 규칙을 정의하지 않고, 단순히 상태 변화만 제공한다.</li>
</ul>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count: Int = 0
        private set

    fun increase() {
        count++
    }

    fun decrease() {
        count--
    }
}</code></pre>
<p>여기서 <code>CounterUiState</code> 대신 <code>CounterStateHolder</code>라는 이름을 사용한 이유는, UI State와 달리 값의 변화를 직접 수행할 수 있기 때문이다.</p>
<p>기존 <code>View</code> 시스템에서는 <code>ViewModel</code>에서 <code>UiState</code>를 <code>LiveData</code>에 담아 UI 갱신을 유도했다. Compose에서 어떻게 활용할 수 있을까?</p>
<h3 id="counterstateholder를-state에-담는-방법"><strong>CounterStateHolder를 State에 담는 방법?</strong></h3>
<pre><code class="language-kotlin">val stateHolder by remember { mutableStateOf(CounterStateHolder()) }</code></pre>
<p>하지만 <code>increase</code>, <code>decrease</code>를 수행해도 <code>State</code> 내부 값의 변화는 없기에 UI가 변하지 않는다. 이래서 <code>CounterStateHolder</code>를 불변 객체로 만들었던 것이다. 하지만 이렇게 하면 <code>CounterStateHolder</code> 객체 자체를 교체해야 하므로 State Holder라는 개념이 갖는 의미와 맞지 않는다.</p>
<h3 id="counterstateholder-내부에서-state를-사용하는-방법">CounterStateHolder 내부에서 State를 사용하는 방법?</h3>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count by mutableStateOf(0)
        private set

    fun increase() {
        count++
    }

    fun decrease() {
        count--
    }
}</code></pre>
<p>이 방법은 State Holder가 내부적으로 UI State를 보유하고, Compose의 State 객체를 통해 UI 변경을 감지하도록 한 방식이다.</p>
<p>다만 State Holder가 Compose에 의존한다는 문제점이 존재한다.</p>
<h3 id="best-practice-state-holder는-변하지-않으면서-state-holder가-순수하도록">Best Practice: State Holder는 변하지 않으면서 State Holder가 순수하도록</h3>
<p>지금까지 공부한 내용을 모두 적용하며, 위의 두 방식의 단점을 제거한 코드는 다음과 같다.</p>
<ol>
<li><p><strong>UI State 정의</strong></p>
<pre><code class="language-kotlin"> data class CounterUiState(
     val count: Int,
 )</code></pre>
<ul>
<li>UI State는 순수하게 UI에 표시되어야 할 데이터를 표현하는 역할만 한다.</li>
</ul>
</li>
<li><p><strong>State Holder 정의</strong></p>
<pre><code class="language-kotlin"> class CounterStateHolder(
     initialCount: Int = 0,
 ) {
     var uiState = CounterUiState(initialCount)

     fun increase() {
         uiState = uiState.copy(count = uiState.count + 1)
     }

     fun decrease() {
         uiState = uiState.copy(count = uiState.count - 1)
     }
 }</code></pre>
<ul>
<li>State Holder는 UI State를 가지며, 이를 변환시킬 수 있다.</li>
<li>Compose나 특정 UI 프레임워크에 의존하지 않는다.</li>
</ul>
</li>
<li><p><strong>Compose에서 State 객체로 감싸 UI와 연결</strong></p>
<pre><code class="language-kotlin"> @Composable
 fun Counter(modifier: Modifier = Modifier) {
     val stateHolder = remember { CounterStateHolder() }
     var uiState by remember { mutableStateOf(stateHolder.uiState) }

     Column(
         modifier = modifier,
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         Text(text = uiState.count.toString())

         Row {
             Button(onClick = {
                 stateHolder.decrease()
                 uiState = stateHolder.uiState // UI에 반영
             }) { Text(&quot;-&quot;) }

             Button(onClick = {
                 stateHolder.increase()
                 uiState = stateHolder.uiState // UI에 반영
             }) { Text(&quot;+&quot;) }
         }
     }
 }</code></pre>
<ul>
<li><p>Compose <code>State</code>를 통해 UI 변경을 감지하도록 연결한다.</p>
</li>
<li><p>State Holder는 <strong>값을 변경</strong>하고, Compose는 <strong>변경된 값을 화면에 반영</strong>하는 역할만 수행.</p>
</li>
<li><p>이렇게 하면 <code>State Holder</code>가 Compose에 종속되지 않으면서도 UI 업데이트가 자연스럽게 이루어진다.</p>
<pre><code class="language-kotlin">  // 기존 View + LiveData 예시
  val stateHolder = CounterStateHolder()
  val uiStateLiveData = MutableLiveData(stateHolder.uiState)

  // 버튼 클릭 시
  stateHolder.increase()
  uiStateLiveData.value = stateHolder.uiState // UI에 반영</code></pre>
</li>
</ul>
</li>
</ol>
<h2 id="state-holder의-순수성을-포기하면"><strong>State Holder의 순수성을 포기하면?</strong></h2>
<p>지금까지는 <strong>State Holder를 Compose나 특정 UI 프레임워크에 종속되지 않도록</strong> 설계했다.</p>
<p>하지만 프로젝트 환경에 따라 State Holder 내부에서 Compose <code>State</code>를 직접 사용해도 크게 문제가 없는 경우도 존재할 것이다.</p>
<pre><code class="language-kotlin">class CounterStateHolder {
    var count by mutableStateOf(0)
        private set

    fun increase() { count++ }
    fun decrease() { count-- }
}</code></pre>
<ul>
<li><strong>State Holder가 값 변경과 UI 업데이트를 동시에 처리</strong></li>
<li>Compose가 변화를 감지해 UI를 자동으로 갱신</li>
</ul>
<p>즉, <strong>순수성을 포기하면 코드가 더 직관적이고 간결</strong>해진다.</p>
<p>다만, 다른 UI 프레임워크로 재사용하려면 다시 조정해야 하는 단점이 있다.</p>
<h1 id="결론">결론</h1>
<ul>
<li>상태 패턴(State Pattern): 객체의 행동을 상태 객체에 위임하여 분기 처리 없이 상태 전환 및 행동을 수행하는 디자인 패턴<ul>
<li>상태 객체(State): 객체의 상태와 행동 방식을 정의</li>
</ul>
</li>
<li>UI State: 화면에 표시될 데이터와 속성만을 담은 객체</li>
<li>Compose State: Compose에서 UI State를 감싸, 값이 변경되면 UI를 재구성하는 객체</li>
<li>State Holder: UI State를 보유하고, 상태 변경 로직을 제공하는 객체</li>
<li>ViewModel: State Holder 역할을 수행하며, View에 대한 의존 없이 데이터 바인딩을 통해 상태를 View에 전달하는 객체</li>
</ul>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://developer.android.com/topic/architecture/ui-layer">UI layer  |  App architecture  |  Android Developers</a></p>
<p><a href="https://developer.android.com/topic/architecture/ui-layer/stateholders">State holders and UI state  |  App architecture  |  Android Developers</a></p>
<p><a href="https://developer.android.com/develop/ui/compose/state">State and Jetpack Compose  |  Android Developers</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose 이미지를 테스트하는 3가지 방법(contentDescription, testTag, Semantics)]]></title>
            <link>https://velog.io/@gjk-dev/Jetpack-Compose-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EB%8A%94-3%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95contentDescription-testTag-Semantics</link>
            <guid>https://velog.io/@gjk-dev/Jetpack-Compose-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EB%8A%94-3%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95contentDescription-testTag-Semantics</guid>
            <pubDate>Thu, 04 Sep 2025 08:01:46 GMT</pubDate>
            <description><![CDATA[<p>Jetpack Compose에서는 UI 테스트를 위해 <code>ComposeTestRule</code>을 사용해볼 수 있다. <code>onNodeWithText</code>를 통해 원하는 컴포저블을 찾고 <code>performClick</code>을 통해 이벤트를 발생시킬 수 있다.</p>
<pre><code class="language-kotlin">class Test {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `Text_표시_테스트`() {
        composeTestRule.setContent {
            Text(&quot;Hello World!&quot;)
        }

        composeTestRule
            .onNodeWithText(&quot;Hello World!&quot;)
            .assertIsDisplayed()
    }

    @Test
    fun `onClick_이벤트_테스트`() {
        var clicked = false

        composeTestRule.setContent {
            Button(onClick = {
                clicked = true
            }) {
                Text(&quot;Hello World!&quot;)
            }
        }

        composeTestRule
            .onNodeWithText(&quot;Hello World!&quot;)
            .performClick()

        assert(clicked == true)
    }
}</code></pre>
<p>이 글에서는 <code>Image</code> 컴포저블이 우리가 원하는 이미지를 잘 표시하고 있는지 체크할 수 있는 3가지 방법을 소개한다.</p>
<h1 id="테스트-준비"><strong>테스트 준비</strong></h1>
<pre><code class="language-kotlin">class Test {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Before
    fun setUp() {
        composeTestRule.setContent {
            Image(
                painter = painterResource(R.drawable.ic_launcher_foreground),
                contentDescription = null,
            )
        }
    }

    @Test
    fun `원하는_이미지를_표시하고_있는지_테스트`() {
        // TODO
    }
}</code></pre>
<h1 id="방법-1-contentdescription"><strong>방법 1: <code>contentDescription</code></strong></h1>
<p>가장 간단한 방법으로 <code>contentDescription</code>을 사용할 수 있다.</p>
<pre><code class="language-kotlin">@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = &quot;안드로이드 이미지&quot;,
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNodeWithContentDescription(&quot;안드로이드 이미지&quot;)
        .assertIsDisplayed()
}</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>구현이 간단하다.</li>
<li>테스트를 작성함으로서 자연스레 접근성을 준수하도록 할 수 있다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><code>contentDescription</code>을 <code>null</code>로 설정할 경우에는 테스트가 불가하다.</li>
<li>동일한 <code>contentDescription</code>을 가진 <code>Image</code>가 여러 개 있다면 테스트가 어려워진다.</li>
<li>앱이 여러 언어를 지원한다면 <code>contentDescription</code>도 번역된 문자열 리소스를 사용해야 하기 때문에 관리가 필요하다.</li>
</ul>
<h1 id="방법-2-testtag"><strong>방법 2: <code>testTag</code></strong></h1>
<p>테스트에서 UI 컴포넌트를 찾을 수 있도록 하는 태그를 추가하는 방법이다. 다음 API를 사용할 수 있다.</p>
<pre><code class="language-kotlin">/**
 * Applies a tag to allow modified element to be found in tests.
 *
 * This is a convenience method for a [semantics] that sets [SemanticsPropertyReceiver.testTag].
 */
@Stable
fun Modifier.testTag(tag: String) = this then TestTagElement(tag)</code></pre>
<pre><code class="language-kotlin">@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = &quot;안드로이드 이미지&quot;,
            modifier = Modifier.testTag(&quot;R.drawable.ic_launcher_foreground&quot;),
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNodeWithTag(&quot;R.drawable.ic_launcher_foreground&quot;)
        .assertIsDisplayed()
}</code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li><code>contentDescription</code> 유무와 상관없이 항상 사용할 수 있다.</li>
<li>사용자에게 노출되지 않으므로 다국어 지원 이슈로부터 자유롭다.</li>
<li>테스트용 식별자라는 명확한 목적을 갖고 있어 코드의 의도를 해치지 않는다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>프로덕션 코드에 테스트를 위한 코드가 추가된다.</li>
<li>태그로 문자열을 사용하기 때문에 오타가 발생하도 인지하기 어렵다.</li>
</ul>
<h1 id="방법-3-커스텀-semantics"><strong>방법 3: 커스텀 <code>Semantics</code></strong></h1>
<p><code>Semantics</code>는 <code>Text</code> 컴포저블이 갖는 <code>text</code> 문자열같은 데이터와 달리 컴포넌트의 <strong>의미와 역할</strong>에 관한 추가 정보를 뜻한다. 예를 들어 카메라 아이콘은 단순한 이미지일 수 있지만, 의미론적인 의미로는 ‘사진 찍기’가 될 수 있다. 시멘틱을 통해 컴포저블에 대한 추가적 컨텍스트를 제공하여 접근성, 자동완성 기능, 테스트 등에 활용할 수 있다.</p>
<pre><code class="language-kotlin">Image(
    painter = painterResource(R.drawable.camera),
    contentDescription = &quot;사진 찍기&quot;,
    modifier = Modifier.clickable { ... }
            .semantics { role = Role.Button },
)</code></pre>
<p><code>contentType</code><em>,</em> <code>role</code>, 등 접근성과 테스트를 위해 주로 사용되는 프로퍼티들은 이미 <code>SemanticsProperties.kt</code> 파일에 정의되어 있지만, <code>Drawable</code> 리소스 ID에 대한 속성은 존재하지 않는다.</p>
<p>따라서 커스텀 <code>SemanticsProperty</code>를 정의하여 사용할 수 있다.</p>
<pre><code class="language-kotlin">val DrawableResId: SemanticsPropertyKey&lt;Int&gt; = SemanticsPropertyKey(&quot;drawableResId&quot;)
var SemanticsPropertyReceiver.drawableResId: Int by DrawableResId</code></pre>
<ul>
<li><p><code>SemanticsPropertyKey</code> 객체는 고유한 키 역할을 하는 싱글톤 객체로 취급되기에 파스칼 케이스를 사용한다.</p>
<ul>
<li><p><code>SemanticsProperties.kt</code> 내부를 보면 다른 <code>SemanticsPropertyKey</code> 객체들도 그렇게 정의된 것을 확인할 수 있다.</p>
<pre><code class="language-kotlin">  object SemanticsProperties {
      val ContentDescription = AccessibilityKey&lt;List&lt;String&gt;&gt;( ... )
      val StateDescription = AccessibilityKey&lt;String&gt;(...)
  }</code></pre>
</li>
</ul>
</li>
</ul>
<p>이제 <code>Image</code>에 커스텀 시맨틱 속성을 적용하고, 이를 기반으로 테스트할 수 있다.</p>
<pre><code class="language-kotlin">@Before
fun setUp() {
    composeTestRule.setContent {
        Image(
            painter = painterResource(R.drawable.ic_launcher_foreground),
            contentDescription = &quot;안드로이드 이미지&quot;,
            modifier = Modifier.semantics { drawableResId = R.drawable.ic_launcher_foreground },
        )
    }
}

@Test
fun `원하는_이미지를_표시하고_있는지_테스트`() {
    composeTestRule
        .onNode(hasDrawableResId(R.drawable.ic_launcher_foreground))
        .assertIsDisplayed()
}

private fun hasDrawableResId(id: Int): SemanticsMatcher = SemanticsMatcher.expectValue(DrawableResId, id)</code></pre>
<h3 id="장점-2">장점</h3>
<ul>
<li>실제 <code>Drawable</code> 리소스 ID를 직접 비교하므로 오타 발생을 줄일 수 있다.</li>
<li>테스트의 의도가 &quot;특정 이미지 리소스를 사용하는가?&quot;로 더욱 명확해진다.</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>다른 방법에 비해 초기 설정(커스텀 속성 정의, <code>SemanticsMatcher</code> 정의)이 복잡하다.</li>
<li><code>painterResource</code>를 사용하지 않는 경우(예: <code>BitmapPainter</code>, <code>AsyncImage</code> 등)에는 이 방법을 적용할 수 없다.</li>
</ul>
<h1 id="결론"><strong>결론</strong></h1>
<p>이렇게 Jetpack Compose에서 이미지를 테스트하는 방법에 대해 알아보았다. 물론 정답은 없다. 상황에 맞는 방식을 적용할 수 있다. 각 방식을 간단히 정리하고 글을 마무리한다.</p>
<ul>
<li><strong><code>contentDescription</code></strong>: 접근성이 중요하고 간단한 확인만 필요할 때 좋은 선택지이다.</li>
<li><strong><code>testTag</code></strong>: <code>contentDescription</code>을 사용할 수 없거나, 코드의 의도를 명확히 하고 싶을 때 사용할 수 있는 선택지이다.</li>
<li><strong>커스텀 <code>Semantics</code></strong>: 특정 이미지 리소스를 보여주는 것이 앱의 핵심 기능과 직결되어 <strong>반드시 특정 이미지가 사용되어야 함을 보장</strong>해야 할 때 사용할 수 있는 가장 확실한 방법이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Activity (공식 문서 Introduction to activities 번역)]]></title>
            <link>https://velog.io/@gjk-dev/Android-Activity-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-Introduction-to-activities-%EB%B2%88%EC%97%AD</link>
            <guid>https://velog.io/@gjk-dev/Android-Activity-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-Introduction-to-activities-%EB%B2%88%EC%97%AD</guid>
            <pubDate>Sun, 06 Jul 2025 12:37:35 GMT</pubDate>
            <description><![CDATA[<h1 id="introduction-to-activities"><strong>Introduction to activities</strong></h1>
<p><code>Activity</code> 클래스는 안드로이드 앱의 핵심적인 구성 요소이다. <code>Activity</code>들이 어떻게 실행되고 조합되느냐가 안드로이드 애플리케이션의 기본 구성이 된다.</p>
<p>안드로이드 시스템은 <code>main()</code> 메서드를 통해 앱들을 실행하는 프로그래밍 패러다임과는 다른 방식으로 작동한다. 안드로이드 시스템은 <code>Activity</code>의 생명주기 단계에 대응하는 콜백 함수들을 호출함으로써 코드를 실행시킨다.</p>
<h1 id="the-concept-of-activities"><strong>The concept of activities</strong></h1>
<p>모바일 앱 환경은 사용자와의 상호작용이 항상 같은 위치에서 시작되지 않는다. 이 점이 데스크톱 환경과 다른 점이다.</p>
<p>대신 사용자는 비결정적으로 앱을 탐색한다. 예를 들어 홈 화면에서 이메일 앱을 실행시키면, 다양한 리스트의 이메일을 볼 수 있다. 반대로 소셜 미디어 앱을 통해 이메일 앱을 열면, 대상에게 이메일을 보내는 화면으로 바로 이동할 수도 있다.</p>
<p><code>Activity</code> 클래스는 이러한 패러다임을 가능케 한다. 하나의 앱이 다른 앱을 실행시킬 때, 다른 앱 전체를 실행시키는 것이 아닌, 앱의 액티비티를 실행시킨다. 즉, <code>Activity</code>는 앱과 사용자간의 상호작용을 위한 진입점 역할을 한다. 이를 위해 <code>Activity</code> 클래스의 하위 클래스를 구현해야 한다.</p>
<p><code>Activity</code>는 앱이 UI를 그리는 창을 제공한다. 보통 이 창이 화면 전체를 채우지만, 보다 작은 사이즈로 구성하고 다른 창 위에 띄울 수도 있다. 일반적으로 하나의 액티비티가 앱의 한 화면을 담당한다. 예를 들어 하나의 <code>Activity</code>가 설정 화면을 구현하고, 또 다른 하나의 <code>Activity</code>가 사진 선택 화면을 구현한다.</p>
<p>대부분의 앱은 다양한 화면을 포함하기에, 다양한 <code>Activity</code>로 구성되어 있다. 일반적으로 하나의 <code>Activity</code>가 앱을 실행할 때 가장 먼저 표시되는 화면인 <code>MainActivity</code>로 지정된다. 이후 각각의 <code>Activity</code>는 다른 기능을 수행하기 위해 다른 <code>Activity</code>를 실행시킬 수 있다. 예를 들어 간단한 이메일 앱에서 <code>MainActivity</code>는 받은 메일들을 보여주는 화면일 것이다. 이후 <code>MainActivity</code>는 <em>메일 작성</em>, <em>메일 자세히 보기</em> 등의 작업을 수행하기 위해 다른 <code>Activity</code>를 실행시킬 것이다.</p>
<p>응집력 있는 UX를 제공하기 위해 다양한 <code>Activity</code>들이 협력하지만, 각각의 액티비티는 서로 느슨하게 결합되어있다. 일반적인 앱에는 <code>Activity</code>간 최소한의 의존성만 존재한다. 실제로 브라우저 앱이 소셜 미디어 앱의 공유하기 <code>Activity</code>를  실행시키듯, <code>Activity</code>는 다른 앱의 액티비티를 실행시키기도 한다.</p>
<p>앱에서 <code>Activity</code>를 사용하려면 관련 정보를 앱의 매니페스트에 등록하고, <code>Activity</code>의 수명을 적절히 관리해야 한다. 이제부터 그 방법에 대해 설명한다.</p>
<h1 id="configuring-the-manifest"><strong>Configuring the manifest</strong></h1>
<p>앱에서 <code>Activity</code>를 사용하려면 해당 <code>Activity</code>와 몇 가지 속성들을 매니페스트에 선언해야 한다.</p>
<h3 id="declare-activities"><strong>Declare activities</strong></h3>
<p><code>Activity</code>를 선언하기 위해, 매니페스트 파일을 열고 <code>&lt;activity&gt;</code> 요소를 <code>&lt;application&gt;</code> 요소의 자식으로 추가한다.</p>
<pre><code class="language-xml">&lt;manifest ... &gt;
  &lt;application ... &gt;
      &lt;activity android:name=&quot;.ExampleActivity&quot; /&gt;
      ...
  &lt;/application ... &gt;
  ...
&lt;/manifest &gt;</code></pre>
<p><code>&lt;activity&gt;</code> 요소에 필요한 유일한 속성은 Activity의 클래스명을 지정하는 <code>android:name</code>이다. 그 외에도 label, icon, UI 테마 등의 <code>Activity</code> 속성을 지정할 수 있다. 이에 대한 추가 정보는 <a href="https://developer.android.com/guide/topics/manifest/activity-element"><activity></a> 문서에서 확인할 수 있다.</p>
<p><strong>중요</strong>: 앱을 출시하고 나면, 특정 <code>Activity</code>의 이름을 바꾸면 안 된다. 만약 바꿀 경우, 앱 숏컷 등 특정 기능이 동작하지 않을 수 있다. 출시 후 변경되면 안 되는 것들에 대한 자세한 정보는 <a href="http://android-developers.blogspot.com/2011/06/things-that-cannot-change.html">Things That Cannot Change</a> 문서를 통해 확인할 수 있다.</p>
<h3 id="declare-intent-filters"><strong>Declare intent filters</strong></h3>
<p><a href="https://developer.android.com/guide/components/intents-filters">Intent filter</a>는 안드로이드 플랫폼의 매우 강력한 기능이다. Intent filter를 통해 명시적인 요청 뿐 아니라, 암시적인 요청을 통해 액티비티를 실행시킬 수 있다. 명시적인 요청은 시스템에게 “Gmail 앱의 <code>SendEmailActivity</code>를 실행시켜”라고 말하는 것이다. 반대로 암시적인 요청은 시스템에게 “메일을 전송할 수 있는 화면을 보여주는 <code>Activity</code>를 실행시켜”라고 말하는 것이다. 시스템 UI가 사용자에게 특정 작업을 위해 어떤 앱을 사용할지 묻는다면, intent filter가 사용된 것이다.</p>
<p><code>&lt;activity&gt;</code> 요소의 <code>&lt;intent-filter&gt;</code> 속성을 선언함으로서 해당 기능을 사용할 수 있다. <code>&lt;intent-filter&gt;</code>는 <code>&lt;action&gt;</code>과 필요에 따라 <code>&lt;category&gt;</code>, <code>&lt;data&gt;</code>요소로 이뤄진다. 이러한 요소들을 결합해 <code>Activity</code>가 응답할 수 있는 인텐트의 유형을 지정한다. 아래 코드는 텍스트 데이터를 전송하고 다른 <code>Activity</code>로부터 요청을 수신할 수 있는 <code>Activity</code>를 선언하는 방법이다.</p>
<pre><code class="language-xml">&lt;activity android:name=&quot;.ExampleActivity&quot; android:icon=&quot;@drawable/app_icon&quot;&gt;
    &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.intent.action.SEND&quot; /&gt;
        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
        &lt;data android:mimeType=&quot;text/plain&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/activity&gt;</code></pre>
<p>위 예시에서 <code>&lt;action&gt;</code>은 해당 <code>Activity</code>가 데이터를 전송한다는 것을 명시한다. <code>&lt;category&gt;</code>를 <code>DEFAULT</code>로 선언하면 <code>Activity</code>가 실행 요청을 수신할 수 있도록 한다. <code>&lt;data&gt;</code>는 이 <code>Activity</code>가 보낼 수 있는 데이터의 타입을 명시한다. 다음 코드는 위 <code>Activity</code>를 호출하는 방법을 보여준다.</p>
<pre><code class="language-kotlin">val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    type = &quot;text/plain&quot;
    putExtra(Intent.EXTRA_TEXT, textMessage)
}
startActivity(sendIntent)</code></pre>
<p>외부에서 다른 앱이 해당 <code>Activity</code>를 실행시키지 못하게 독립적으로 구성하고 싶다면 intent filter를 사용할 필요가 없다. 다른 앱이 이용 불가능하도록 하려는 <code>Activity</code>는 intent filter 없이 명시적인 <code>Intent</code>를 통해서만 실행되어야 한다. 어떻게 <code>Activity</code>가 <code>Intent</code>에 응답하는지 확인하려면 <a href="https://developer.android.com/guide/components/intents-filters">Intents and Intent Filters</a> 문서를 참고해라.</p>
<h3 id="declare-permissions"><strong>Declare permissions</strong></h3>
<p>매니페스트의 <code>&lt;activity&gt;</code> 태그를 통해 어떤 앱들이 특정 <code>Activity</code>를 실행할 수 있는지 제어할 수 있다. 만약 부모 <code>Activity</code>와 자식 <code>Activity</code>가 매니페스트에 같은 권한을 갖고 있지 않다면, 부모 <code>Activity</code>는 자식 <code>Activity</code>를 실행시킬 수 없다. 만약 부모 <code>Activity</code>에 <code>&lt;uses-permission&gt;</code> 요소를 선언한다면, 각 자식 <code>Activity</code>들은 일치하는 <code>&lt;uses-permission&gt;</code> 요소를 가져야 한다.</p>
<p>당신의 앱이 <em>SociaApp</em>이라는 이름의 가상 앱을 통해 소셜 미디어에 게시글을 공유하려고 한다고 가정하자. <em>SocialApp</em>은 스스로 자신을 호출하려는 앱이 가져야 하는 권한을 정의해야 한다.</p>
<pre><code class="language-xml">&lt;manifest&gt;
&lt;activity android:name=&quot;....&quot;
   android:permission=”com.google.socialapp.permission.SHARE_POST”

/&gt;</code></pre>
<p>이제 <em>SocialApp</em>을 실행하려면 당신의 앱은 <em>SocialApp</em>의 메니페스트에 설정된 권한과 일치해야 한다.</p>
<pre><code class="language-xml">&lt;manifest&gt;
   &lt;uses-permission android:name=&quot;com.google.socialapp.permission.SHARE_POST&quot; /&gt;
&lt;/manifest&gt;</code></pre>
<p>권한과 보안에 대한 추가 정보는 <a href="https://developer.android.com/guide/topics/security/security">Security and Permissions</a> 문서에서 확인할 수 있다.</p>
<h1 id="managing-the-activity-lifecycle"><strong>Managing the activity lifecycle</strong></h1>
<p><code>Activity</code>는 자신의 수명 동안 여러 상태를 거치게 된다. 일련의 콜백을 통해 상태 간 전환을 다룰 수 있다. 다음 섹션에서 이러한 콜백들을 소개한다.</p>
<h3 id="oncreate"><strong>onCreate()</strong></h3>
<p>시스템이 <code>Activity</code>를 생성할 때 호출되는 콜백 함수로, 반드시 구현해야 하는 함수이다. 해당 함수의 구현은 <code>View</code>를 생성하고 데이터를 리스트에 연결하는 등, <code>Activity</code>에 필요한 구성 요소들을 초기화해야 한다. 앱의 UI를 위해 <code>setContentView()</code>를 반드시 호출해야 하는 곳이기도 하다.</p>
<p><code>onCreate()</code> 함수가 수행되고 나면, 다음 콜백 함수는 항상 <code>onStart()</code>가 된다.</p>
<h3 id="onstart"><strong>onStart()</strong></h3>
<p><code>onCreate()</code> 함수가 끝나면 <code>Activity</code>는 Started 상태가 되고 사용자에게 보이기 시작한다. <code>onStart()</code>는 <code>Activity</code>가 포그라운드에 나와 상호작용할 수 있도록 최종적으로 준비하는 코드를 포함한다.</p>
<p><strong>onResume()</strong></p>
<p>이 콜백 함수는 <code>Activity</code>가 사용자와 상호작용하기 직전에 시스템에 의해 호출된다. 이 시점에서 <code>Activity</code>는 Activity Stack의 최상위가 되고, 유저의 입력을 수집한다. 앱의 핵심 기능 대부분은 <code>onResume()</code> 메서드에 구현되어 있다.</p>
<p><code>onPause()</code> 콜백 함수는 항상 <code>onResume()</code> 다음에 호출된다.</p>
<h3 id="onpause"><strong>onPause()</strong></h3>
<p><code>Activity</code>가 포커스를 잃고 Paused 상태로 돌입하면 시스템은 <code>onPause()</code>를 호출한다. 유저가 뒤로가기나 최근 사용한 앱 버튼을 눌렀을 때 이 상태가 된다. 시스템이 <code>Activity</code>의 <code>onPause()</code> 함수를 호출한다는 것은 엄밀히 말하면 <code>Activity</code>가 부분적으로 표시된다는 의미이다. 하지만 대부분의 경우 사용자가 <code>Activity</code>를 종료하고 <code>Activity</code>가 Stopped 또는 Resumed 상태가 될 것이라는 것을 의미한다.</p>
<p>Paused 상태의 <code>Activity</code>에서도 UI가 업데이트될 것이라 예상된다면 UI 업데이트를 지속할 수 있다. 예를 들어, 네비게이션 지도 화면이나 미디어를 보여주는 <code>Activity</code>에서 포커스를 잃었더라도 UI가 계속 업데이트될 수 있다.</p>
<p><code>onPause()</code> 함수에서 앱 또는 사용자 데이터를 저장하거나, 네트워크 통신을 호출하거나, 데이터베이스 트랜잭션을 실행해서는 안 된다. 데이터 저장에 관해서는 <a href="https://developer.android.com/guide/components/activities/activity-lifecycle#saras">Saving and restoring activity state</a> 문서를 참고하라.</p>
<p><code>onPause()</code>의 다음 콜백은 <code>Activitiy</code>가 Paused 상태가 되고 난 후의 상황에 따라 <code>onStop()</code>또는 <code>onResume()</code>이 된다.</p>
<h3 id="onstop"><strong>onStop()</strong></h3>
<p><code>Activity</code>가 더 이상 사용자에게 보이지 않을 때, 시스템은 <code>onStop()</code>을 호출한다. <code>Activity</code>가 파괴되고 새 <code>Activity</code>가 시작되거나, 기존 <code>Activity</code>가 Resumed 상태가 되면서 <code>Activity</code>를 덮어버릴 때 호출될 수 있다. 이 모든 경우에 Stopped 상태가 된 <code>Activity</code>는 더 이상 보이지 않게 된다.</p>
<p>시스템이 호출하는 다음 콜백은 <code>onRestart</code> 또는 <code>onDestroy</code>이다. <code>Activity</code>가 다시 사용자와 상호작용한다면 <code>onRestart()</code>가 되고, 완전히 제거된다면 <code>onDestroy()</code>가 된다.</p>
<h3 id="onrestart"><strong>onRestart()</strong></h3>
<p>Stopped 상태의 <code>Activity</code>가 새로 시작하려고 할 때, 시스템은 <code>onRestart()</code>를 호출한다. <code>onRestart()</code>는 앱이 Stopped 상태로 돌입했을 때의 <code>Activity</code> 상태를 복원한다.</p>
<p>이 콜백은 항상 <code>onStart()</code> 이후에 수행된다.</p>
<h3 id="ondestroy"><strong>onDestroy()</strong></h3>
<p><code>Activity</code>가 파괴되기 전에 시스템은 <code>onDestroy()</code>를 호출한다.</p>
<p>이 콜백은 <code>Activity</code>가 수신하는 마지막 콜백이다. <code>onDestroy()</code>는 일반적으로 <code>Activity</code> 또는 프로세스가 파괴될 때, 사용되던 모든 자원이 해제됨을 보장하기 위해 구현된다. </p>
<p>Activity 생명 주기와 콜백을 더 자세히 다루려면 <a href="https://developer.android.com/guide/components/activities/activity-lifecycle">The Activity Lifecycle</a> 문서를 참고하라.</p>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://developer.android.com/guide/components/activities/intro-activities">https://developer.android.com/guide/components/activities/intro-activities</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[commit() vs commitAllowingStateLoss() vs commitNow()]]></title>
            <link>https://velog.io/@gjk-dev/commit-vs-commitAllowingStateLoss-vs-commitNow</link>
            <guid>https://velog.io/@gjk-dev/commit-vs-commitAllowingStateLoss-vs-commitNow</guid>
            <pubDate>Sun, 18 May 2025 12:48:26 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p><code>DialogFragment</code>를 <code>dismiss</code>하는 방법은 3가지가 존재한다. <code>dismiss</code>, <code>dismissAllowingStateLoss</code>, <code>dismissNow</code>가 그것이다.</p>
<p><code>DialogFragment</code>는 <code>Fragment</code>의 하위 타입이기 때문에 <code>FragmentTransaction</code>을 통해 추가되고 제거된다.</p>
<p><code>dismiss</code>, <code>dismissAllowingStateLoss</code>, <code>dismissNow</code>는 <code>FragmentTransaction</code>을 수행하기 위해 각각 <code>commit</code>, <code>commitAllowingStateLoss</code>, <code>commitNow</code>를 호출한다.</p>
<pre><code class="language-java">FragmentTransaction ft = getParentFragmentManager().beginTransaction();
ft.setReorderingAllowed(true);
ft.remove(this);
// allowStateLoss and immediate should not both be true
if (immediate) {
    ft.commitNow();
} else if (allowStateLoss) {
    ft.commitAllowingStateLoss();
} else {
    ft.commit();
}</code></pre>
<p><code>commit</code>, <code>commitAllowingStateLoss</code>, <code>commitNow</code>는 각각 무슨 차이점이 있는지 알아보자.</p>
<h1 id="본론">본론</h1>
<h2 id="commit-is-asynchronous">Commit is asynchronous</h2>
<p><code>commit</code>을 사용해도 트랜잭션이 즉시 실행되지는 않는다. 그 대신 가능할 때 해당 트랜잭션을 수행하라고 메인 스레드에 예약해둔다. 필요할 경우 <code>commitNow</code>를 사용하면 즉각적으로 UI 스레드에서 프래그먼트 트랜잭션을 수행한다.</p>
<p>이러한 이유로 <code>commitNow</code>는 <code>addToBackStack</code>과 함께 사용할 수 없다. 비슷하게 하기 위해서는 <code>commit</code>을 호출한 후 <code>executePendingTransactions</code>를 통해 수행되어야 할 트랜잭션을 즉시 수행하도록 하면 된다. 이 방식을 사용하면 <code>addToBackStack</code>을 활용할 수 있다. 그렇지만 대부분의 경우 <code>commit</code>을 사용하면 된다.</p>
<p>이제 <code>commit</code>, <code>commitNow</code>, 그리고 <code>commitAllowingStateLoss</code>에 대해 알아보자.</p>
<h3 id="commit">commit()</h3>
<pre><code class="language-java">public abstract int commit()</code></pre>
<p>트랜잭션을 예약해둔다. 위에서 설명한 것처럼 즉시 수행되는 것이 아닌, 메인 스레드가 준비되었을 때 수행되도록 예약된다.</p>
<p>이 메서드를 통해 트랜잭션을 수행하려면 액티비티가 자신의 상태를 저장하기 이전에 수행해야 한다. 그 이후에 수행될 경우 익셉션이 발생한다.</p>
<p>이는 커밋 이후의 상태가 유실될 수 있기 때문이다. 액티비티가 상태 복원을 할 때 저장된 상태를 사용해야 하는데, 상태를 저장한 이후에 커밋이 수행됐기 때문이다. 만약 상태가 유실되어도 괜찮다면 <code>commitAllowStateLoss</code>를 사용할 수 있다.</p>
<p><code>addToBackStack</code>을 통해 추가된 트랜잭션의 ID(백스택 내부에서 사용되는 ID)를 반환한다. 추가되지 않았다면 음수를 반환한다.</p>
<h3 id="commitallowingstateloss">commitAllowingStateLoss()</h3>
<p>위에서 설명한 것처럼 액티비티 상태 저장 이후 커밋을 수행해야 할 때 사용한다. 이 커밋은 나중에 액티비티가 상태를 복원할 때 유실될 수 있다.</p>
<p>따라서 사용자에게 UI 상태가 예기치 않게 바뀌어도 괜찮을 때에만 사용해야 한다.</p>
<h3 id="commitnow">commitNow()</h3>
<p>동기적으로 <code>commit</code>한다. 이 메서드를 통해 추가된 프래그먼트는 호스트 액티비티의 생명주기 상태까지 초기화되고, 이 메서드를 통해 제거된 프래그먼트는 이 메서드가 끝나기 전까지 완전히 제거된다.</p>
<p>이 방식으로 트랜잭션을 수행하면 프래그먼트는 호스트 생명주기를 관찰하는 호스트 전용의 캡슐화된 구성요소로 추가된다. 따라서 프래그먼트가 언제 초기화되고 준비되는지 순서가 보장된다. 뷰를 가진 프래그먼트들은 뷰들도 바로 생성되어 부착된다.</p>
<p><code>commit</code> 후 <code>executePendingTransactions</code>를 호출하는 것보다 <code>commitNow</code>를 호출하는 것이 권장된다. 전자는 바로 수행하고 싶은 트랜잭션 외에 남아있는 트랜잭션 모두를 수행하기 때문이다.</p>
<p>이 방식대로 수행된 트랜잭션들은 <code>FragmentManager</code>의 백 스택에 추가될 수 없다. 만약 백 스택에 추가된다면 비동기적으로 수행됐을 트랜잭션들의 순서가 깨지기 때문이다.</p>
<pre><code class="language-kotlin">val ft1 = fragmentManager.beginTransaction()
ft1.add(R.id.container, FragmentA())
ft1.addToBackStack(&quot;A&quot;)
ft1.commit() // 예약됨

val ft2 = fragmentManager.beginTransaction()
ft2.add(R.id.container, FragmentB())
ft2.commitNow() // 즉시 실행됨</code></pre>
<p>위와 같은 코드가 있을 때, 예상되는 순서는 <code>FragmentA</code>가 추가된 후 <code>FragmentB</code>가 추가되는 것이다. 따라서 이후 <code>popBackStack</code>을 하면 A로 돌아가는 것이 기대된다.</p>
<p>하지만 <code>commitNow</code>가 붙은 순간 백스택 내부에 저장된 트랜잭션들의 순서를 예상할 수 없다. 따라서 이후 <code>popBackStack</code>을 호출해도 어떤 작업이 수행될지가 불명확하다. 그러므로 이 메서드 호출 전 <code>addToBackStack</code>을 호출했다면 <code>IllegalStateException</code>이 발생한다.</p>
<p>이 메서드 또한 <code>commit</code>과 같은 이유로 액티비티의 상태가 저장되기 전에만 수행될 수 있다.</p>
<h1 id="결론">결론</h1>
<p>세가지 메서드는 다음과 같이 비교할 수 있다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>실행 시점</th>
<th>백스택 추가 가능</th>
<th>상태 저장 이후 호출 가능</th>
<th>적절한 사용 상황</th>
<th>상황 예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>commit()</code></td>
<td><strong>비동기 (예약됨)</strong></td>
<td>가능 (<code>addToBackStack()</code>)</td>
<td>❌ 안 됨</td>
<td>일반적인 트랜잭션</td>
<td>화면 전환, 탭 전환 등</td>
</tr>
<tr>
<td><code>commitAllowingStateLoss()</code></td>
<td><strong>비동기 (예약됨)</strong></td>
<td>가능 (<code>addToBackStack()</code>)</td>
<td>✅ 가능</td>
<td>상태 저장 이후 트랜잭션을 수행해야 할 때</td>
<td>액티비티가 종료될 때 다이얼로그 닫기 등</td>
</tr>
<tr>
<td><code>commitNow()</code></td>
<td><strong>동기 (즉시 실행)</strong></td>
<td>❌ 불가능 (<code>addToBackStack()</code> 하면 예외)</td>
<td>❌ 안 됨</td>
<td>뷰가 즉시 필요할 때</td>
<td>자식 프래그먼트를 삽입한 후 해당 프래그먼트를 바로 참조해야 할 경우, 테스트에서 프래그먼트를 즉시 붙일 경우 등</td>
</tr>
</tbody></table>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://developer.android.com/guide/fragments/transactions">Fragment transactions  |  App architecture  |  Android Developers</a>
<a href="https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction.html">FragmentTransaction  |  API reference  |  Android Developers</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[addToBackStack, setReorderingAllowed]]></title>
            <link>https://velog.io/@gjk-dev/addToBackStack-setReorderingAllowed</link>
            <guid>https://velog.io/@gjk-dev/addToBackStack-setReorderingAllowed</guid>
            <pubDate>Sun, 18 May 2025 08:52:23 GMT</pubDate>
            <description><![CDATA[<h1 id="addtobackstack">addToBackStack?</h1>
<p><code>addToBackStack</code>는 백 스택에 해당 트랜잭션을 추가하는 메서드이다. 추가된 트랜잭션은 <code>commit</code>된 이후에도 저장되며, 이후 스택에서 꺼내어 사용할 수 있다.</p>
<p>트랜잭션을 스택에서 꺼내며 작업을 되돌리려면 <code>setReorderingAllowed</code> 메서드를 통해 추가할 트랜잭션의 <code>reorderingAllowed</code> 속성을 <code>true</code>로 설정해주어야 한다.</p>
<h1 id="setreorderingallowed">setReorderingAllowed?</h1>
<p>트랜잭션 내부 및 트랜잭션간의 작업들을 최적화할지 설정하는 메서드이다. <code>true</code>로 설정할 경우 중복되거나 어차피 취소될 작업들을 제거한다.</p>
<pre><code class="language-kotlin">val transaction = fragmentManager.beginTransaction()
transaction.add(R.id.container, FragmentA())  // 첫 번째 명령
transaction.replace(R.id.container, FragmentB())  // 두 번째 명령
transaction.setReorderingAllowed(true)
transaction.commit()</code></pre>
<p>이런 작업이 있다고 가정할 때, <code>FragmentA</code>는 즉시 <code>FragmentB</code>로 <code>replace</code>되기에 사실상 생성할 필요가 없어진다. 시스템 입장에서는 <code>FragmentA</code>를 생성할 필요가 없다고 판단해 제거한다.</p>
<p>하지만 이는 <code>onCreate</code>, <code>onDestroy</code>같은 생명주기를 거치지 않을 수도 있다는 의미이기 때문에 주의해야 한다. 이 과정에서 부작용이 발생할 수 있다.</p>
<pre><code class="language-kotlin">트랜잭션 1: Fragment A를 추가
트랜잭션 2: Fragment B를 추가
트랜잭션 3: Fragment A를 제거</code></pre>
<p>위 3개의 트랜잭션이 순서대로 수행된다고 가정하자. 중복된 작업을 제거하지 않는다면 <code>FragmentB</code>가 생성될 때 <code>FragmentA</code>가 존재할 것이라고 생각할 것이다. 하지만 <code>setReorderingAllowed(true)</code>를 통해 중복 작업이 제거되며 예상과는 달리 <code>FragmentA</code>가 생성조차 되지 않았다는 상황에 처한다.</p>
<p>더 나은 트랜지션을 위해 프래그먼트 내부의 상태 변화 순서를 재정렬할 수도 있다. 추가될 프래그먼트의 <code>onCreate</code>가 기존 프래그먼트의 <code>onDestroy</code> 호출 전에 호출될 수 있다. 이렇게 하면 파괴 → 생성 사이에 생기는 격차가 없어지며 자연스럽게 전환이 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코틀린 공식문서 Reflection 번역]]></title>
            <link>https://velog.io/@gjk-dev/%EC%BD%94%ED%8B%80%EB%A6%B0-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-Reflection-%EB%B2%88%EC%97%AD</link>
            <guid>https://velog.io/@gjk-dev/%EC%BD%94%ED%8B%80%EB%A6%B0-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-Reflection-%EB%B2%88%EC%97%AD</guid>
            <pubDate>Tue, 22 Apr 2025 12:20:02 GMT</pubDate>
            <description><![CDATA[<p><strong>리플렉션</strong>은 프로그램 구조 내부를 동적으로(런타임 때) 들여다볼 수 있는 언어 혹은 라이브러리 기능의 집합이다.</p>
<p>함수형, 반응형 프로그래밍에서는 사용할 함수, 프로퍼티를 <strong>동적으로(런타임 시에) 결정</strong>할 일이 많다. 따라서 리플렉션(예를 들어, 런타임에 1급 시민인 프로퍼티 또는 함수의 이름이나 유형을 학습하는 것)은 필수적이다. </p>
<ul>
<li><p><strong>컴파일 타임에 결정 VS 런타임에 결정 🤔</strong></p>
<ul>
<li><p><strong>컴파일 타임에 결정</strong>: 실행 전(컴파일 타임)에 <strong>어떤 함수를 호출할지, 어떤 변수를 참조할지</strong> 결정</p>
<pre><code class="language-kotlin">  fun greet() {
      println(&quot;Hello!&quot;)
  }

  fun main() {
      greet()  // 컴파일 타임에 &quot;greet()&quot; 함수가 실행될 것임이 확정됨
  }</code></pre>
</li>
<li><p><strong>런타임에 결정</strong>: 실행 중(런타임)에 동적으로 <strong>어떤 함수를 호출할지, 어떤 변수를 참조할지</strong> 결정</p>
<pre><code class="language-kotlin">  fun greet() {
      println(&quot;Hello!&quot;)
  }

  fun bye() {
      println(&quot;Goodbye!&quot;)
  }

  fun main() {
      val input = readln()  // 사용자 입력을 받음 (실행될 때까지 어떤 값이 들어올지 모름)

      if (input == &quot;hi&quot;) {
          greet()  // 런타임에 결정됨
      } else {
          bye()  // 런타임에 결정됨
      }
  }</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="jvm-dependency">JVM dependency</h2>
<p>JVM 플랫폼은 Kotlin 컴파일러 배포에 리플렉션 기능을 위해 <code>kotlin-reflect.jar</code>를 런타임 컴포넌트로써 포함할 수 있다.</p>
<p><code>kotlin-reflect.jar</code>는 리플렉션을 사용하지 않는 애플리케이션의 경우 라이브러리 크기를 줄이기 위해 별도의 아티팩트로 선언되어있다.</p>
<ul>
<li><strong>컴파일러 배포(compiler distribution)</strong>: Kotlin을 사용할 수 있도록 제공되는 도구들의 묶음 (Kotlin 컴파일러 + 라이브러리)</li>
<li><strong>런타임 컴포넌트(runtime component)</strong>: 프로그램 실행 중(런타임)에 필요한 라이브러리, 기능</li>
<li><strong><code>.jar</code>(JAR, Java ARchive)</strong>: Java 라이브러리를 담고 있는 압축 파일<ul>
<li>여러 <code>.class</code> 파일(바이트코드)을 하나로 파일로 묶은 것</li>
</ul>
</li>
<li><strong>아티팩트(artifact)</strong>: 빌드된 결과물로, Kotlin 컴파일러가 제공하는 추가적인 라이브러리 파일</li>
</ul>
<p>리플렉션을 Gradle 프로젝트에서 사용하기 위해서는 <code>kotlin-reflect</code>에 대한 의존성을 추가해주어야 한다.</p>
<pre><code class="language-kotlin">dependencies {
    implementation(kotlin(&quot;reflect&quot;))
}</code></pre>
<h2 id="class-references">Class references</h2>
<p>가장 기본적인 리플렉션 기능은 Kotlin 클래스의 런타임 참조를 얻는 것이다. 정적으로 알려진 Kotlin 클래스에 대한 참조를 얻으려면 class literal 구문을 사용할 수 있다.</p>
<ul>
<li><p><strong>런타임 참조(runtime reference)</strong>: 프로그램 실행 중(runtime)에 특정 클래스나 객체를 참조할 수 있는 기능</p>
</li>
<li><p><strong>정적으로 알려진 Kotlin 클래스(statically known Kotlin class)</strong>: 컴파일 타임에 이미 어떤 클래스인지 결정된 클래스</p>
</li>
<li><p><strong>클래스 리터럴 구문(class literal syntax)</strong>: 클래스 정보를 참조할 때 사용하는 문법, <code>::class</code></p>
<pre><code class="language-kotlin">  class MyClass

  fun main() {
      val c: KClass&lt;MyClass&gt; = MyClass::class
  }</code></pre>
<ul>
<li>얻은 참조는 <code>KClass&lt;T&gt;</code> 자료형을 갖는다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>JVM에서 Kotlin 클래스 참조는 Java 클래스 참조와 다르다. Java 클래스 참조를 얻으려면 <code>KClass</code> 인스턴스의 <code>.java</code> 프로퍼티를 사용할 수 있다.</p>
</blockquote>
<h3 id="bound-class-references"><strong>Bound class references</strong></h3>
<p>객체를 리시버로 사용하는 <code>::class</code> 구문으로 해당 객체의 클래스에 대한 참조를 얻을 수 있다.</p>
<pre><code class="language-kotlin">fun main() {
    val string: Any = &quot;&quot;
    println(string::class.qualifiedName)
}

결과: 
kotlin.String</code></pre>
<ul>
<li>선언된 리시버의 타입(<code>Any</code>)에 무관하게 실제 객체의 타입(<code>String</code>)을 얻게 된다.</li>
</ul>
<h2 id="callable-references">Callable references</h2>
<p>함수, 프로퍼티, 생성자에 대한 참조 또한 호출되거나, 함수 유형의 인스턴스로 사용될 수 있다.</p>
<p>모든 호출 가능한(callable) 참조에 대한 공통 슈퍼타입은 <code>KCallable&lt;out R&gt;</code>이다. 타입 파라미터 <code>R</code>은 반환 값 타입을 의미한다. 따라서 프로퍼티 타입의 경우엔 프로퍼티의 타입이고, 생성자의 경우 생성된 타입이다.</p>
<pre><code class="language-kotlin">class MyClass {
    val property = &quot;&quot;

    fun function1() {}

    fun function2() {}
}

fun main() {
    val property: KCallable&lt;String&gt; = MyClass::property // 실제 타입: KProperty&lt;String&gt;
    val constructor: KCallable&lt;MyClass&gt; = ::MyClass // 실제 타입: KFunction0&lt;MyClass&gt;
    val function1: KCallable&lt;Unit&gt; = MyClass::function1 // 실제 타입: KFunction1&lt;MyClass, Unit&gt;
    val function2: KCallable&lt;Unit&gt; = MyClass::function2 // 실제 타입: KFunction1&lt;MyClass, Unit&gt;
}</code></pre>
<ul>
<li>이 또한 리시버의 타입에 무관하게 실제 객체의 타입을 얻을 수 있다.</li>
</ul>
<h3 id="function-references"><strong>Function references</strong></h3>
<p>아래와 같은 기명 함수는 <code>isOdd(5)</code>와 같이 호출된다.</p>
<pre><code class="language-kotlin">fun isOdd(x: Int) = x % 2 != 0</code></pre>
<p>위 함수를 함수 타입의 변수로 사용하고 싶다면, <code>::</code> 연산자를 사용할 수 있다.</p>
<pre><code class="language-kotlin">val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))</code></pre>
<p><code>::isOdd</code>는 함수 타입 <code>(Int) → Boolean</code>이 저장된 변수이다.</p>
<p>함수 참조는 <code>KFunction&lt;out R&gt;</code> 의 하위 타입이다. 파라미터 개수에 따라 달라지는데, 예를 들어 매개변수가 3개라면 <code>KFunction3&lt;T1, T2, T3, R&gt;</code>이 된다.</p>
<p><code>::</code>는 예상 타입을 문맥을 통해 알 수 있을 경우, 오버로드된 함수에도 사용될 수 있다.</p>
<pre><code class="language-kotlin">fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == &quot;brillig&quot; || s == &quot;slithy&quot; || s == &quot;tove&quot;

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // refers to isOdd(x: Int)</code></pre>
<p>반대로 메서드 참조를 변수에 저장할 때 타입을 명시적으로 선언해 필요한 문맥을 제공할 수도 있다.</p>
<pre><code class="language-kotlin">val predicate: (String) -&gt; Boolean = ::isOdd   // refers to isOdd(x: String)</code></pre>
<p>클래스의 멤버 또는 확장을 사용하려면, <code>String::toCharArray</code> 형태로 사용할 수 있다.</p>
<p>확장 함수에 대한 참조를 변수에 저장하더라도, 추론된 타입에는 리시버가 없다. 대신 리시버를 첫 번째 파라미터로 추가된 형태로 존재한다.</p>
<p>만약 리시버가 존재하는 타입으로 저장하려면, 타입을 명시하면 된다.</p>
<pre><code class="language-kotlin">fun main() {
    val isEmptyStringList1: (List&lt;String&gt;) -&gt; Boolean = List&lt;String&gt;::isEmpty
    val isEmptyStringList2: List&lt;String&gt;.() -&gt; Boolean = List&lt;String&gt;::isEmpty
}</code></pre>
<p><strong>예시: 함수 조합</strong></p>
<pre><code class="language-kotlin">fun &lt;A, B, C&gt; compose(f: (B) -&gt; C, g: (A) -&gt; B): (A) -&gt; C {
    return { x -&gt; f(g(x)) }
}</code></pre>
<p>위 함수는 전달된 두 함수를 조합한다.(<code>compose(f, g) = f(g(*))</code>) 이를 callable 참조에 적용할 수 있다.</p>
<pre><code class="language-kotlin">fun length(s: String) = s.length

val oddLength = compose(::isOdd, ::length)
val strings = listOf(&quot;a&quot;, &quot;ab&quot;, &quot;abc&quot;)

println(strings.filter(oddLength)) // filter를 한 번만 호출</code></pre>
<h3 id="property-references"><strong>Property references</strong></h3>
<p>코틀린에서 프로퍼티들을 일급 객체로서 접근하려면, <code>::</code> 연산자를 사용할 수 있다.</p>
<pre><code class="language-kotlin">val x = 1

fun main() {
    println(::x.get())
    println(::x.name)
}

결과
1
x</code></pre>
<p>표현식 <code>::x</code>는 <code>KProperty0&lt;Int&gt;</code> 타입의 프로퍼티 객체로 간주된다. <code>KProperty0&lt;Int&gt;</code>는 게터를 사용해 값을 읽거나, <code>name</code> 프로퍼티를 통해 프로퍼티 이름을 얻을 수 있다.</p>
<pre><code class="language-kotlin">public actual interface KCallable&lt;out R&gt; : KAnnotatedElement {
        ...
    public actual val name: String
    ...
}</code></pre>
<p><code>var y = 1</code>과 같이 가변으로 선언된 프로퍼티의 경우 <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-mutable-property/index.html"><code>KMutableProperty0&lt;Int&gt;</code></a> 타입의 객체로 간주된다.</p>
<pre><code class="language-kotlin">var y = 1

fun main() {
    ::y.set(2)
    println(y)
}

결과
2</code></pre>
<p>프로퍼티 참조는 하나의 제네릭 파라미터가 존재하는 함수가 필요할 때 사용할 수 있다.</p>
<pre><code class="language-kotlin">val strs = listOf(&quot;a&quot;, &quot;bc&quot;, &quot;def&quot;)
println(strs.map(String::length))

결과
[1, 2, 3]</code></pre>
<p>확장 프로퍼티의 경우에는 다음과 같다.</p>
<pre><code class="language-kotlin">fun main() {
    class A(
        val p: Int,
    )

    val prop = A::p
    println(prop.get(receiver = A(1)))
}

결과
1</code></pre>
<h3 id="interoperability-with-java-reflection"><strong>Interoperability with Java reflection</strong></h3>
<p>JVM 플랫폼 스탠다드 라이브러리는 자바 리플렉션 객체와 상호 운용을 위한 여러 확장 기능들을 제공한다. 예를 들어 필드나 게터 메서드를 찾기 위해, 아래와 같이 작성할 수 있다.</p>
<pre><code class="language-kotlin">import kotlin.reflect.jvm.*

class A(val p: Int)

fun main() {
    println(A::p.javaGetter) // prints &quot;public final int A.getP()&quot;
    println(A::p.javaField)  // prints &quot;private final int A.p&quot;
}</code></pre>
<p>자바 클래스와 일치하는 코틀린 클래스를 얻으려면 <code>.kotlin</code> 확장 프로퍼티를 사용할 수 있다.</p>
<pre><code class="language-kotlin">fun getKClass(o: Any): KClass&lt;Any&gt; = o.javaClass.kotlin

public val &lt;T : Any&gt; Class&lt;T&gt;.kotlin: KClass&lt;T&gt;
    @JvmName(&quot;getKotlinClass&quot;)
    get() = Reflection.getOrCreateKotlinClass(this) as KClass&lt;T&gt;</code></pre>
<h3 id="constructor-references"><strong>Constructor references</strong></h3>
<p>생성자 또한 메서드나 프로퍼티처럼 참조될 수 있다. 생성자와 파라미터 형식이 같고, 해당 타입의 객체를 반환하는 함수(형 객체)를 필요로 하는 곳이라면 생성자를 참조해 사용할 수 있다. 생성자는 <code>::</code> 연산자 뒤에 클래스 이름을 붙여 참조할 수 있다. 다음 예시는 파라미터가 없고, 반환값이 <code>Foo</code>인 함수를 파라미터로 받는 함수이다.</p>
<pre><code class="language-kotlin">class Foo

fun function(factory: () -&gt; Foo) {
    val x: Foo = factory()
}</code></pre>
<p><code>Foo</code> 클래스의 파라미터가 없는 생성자를 <code>::Foo</code>를 통해 참조하면 다음과 같이 사용할 수 있다.</p>
<p>생성자에 대한 참조는 파라미터의 개수에 따라 타입이 결정되며, <code>KFunction&lt;out R&gt;</code>의 하위 타입이다.</p>
<h3 id="bound-function-and-property-references"><strong>Bound function and property references</strong></h3>
<p>특정 객체의 메서드 또한 참조할 수 있다.</p>
<pre><code class="language-kotlin">val numberRegex = &quot;\\d+&quot;.toRegex()
println(numberRegex.matches(&quot;29&quot;))

val isNumber = numberRegex::matches
println(isNumber(&quot;29&quot;))</code></pre>
<p><code>isNumber</code>는 <code>numberRegex</code>의 <code>matches</code>에 대한 참조를 사용하고 있다.  이러한 참조는 참조값의 리시버(이 경우 <code>numberRegex</code>)에 구속된다. 위 예시처럼 호출(<code>()</code>)하거나, 아래처럼 함수 타입으로 사용할 수 있다.</p>
<pre><code class="language-kotlin">val numberRegex = &quot;\\d+&quot;.toRegex()
val strings = listOf(&quot;abc&quot;, &quot;124&quot;, &quot;a70&quot;)
println(strings.filter(numberRegex::matches))</code></pre>
<p>bound된 참조의 타입과 unbound된 참조의 타입은 다음과 같이 비교된다. bound된 callable 참조는 리시버가 존재하기 때문에, 더 이상 파라미터로 쓰이지 않는다.</p>
<pre><code class="language-kotlin">val isNumber: (CharSequence) -&gt; Boolean = numberRegex::matches

val matches: (Regex, CharSequence) -&gt; Boolean = Regex::matches</code></pre>
<p>프로퍼티 참조 또한 bound될 수 있다.</p>
<pre><code class="language-kotlin">val prop = &quot;abc&quot;::length
println(prop.get())</code></pre>
<p>리시버에 대해 <code>this</code>를 명시해줄 필요는 없다. <code>this::foo</code> 와 <code>::foo</code> 는 똑같다.</p>
<h3 id="bound-constructor-references"><strong>Bound constructor references</strong></h3>
<p>bound된 callable 참조를 사용해 외부 클래스의 인스턴스로 inner class의 클래스 생성자에 접근할 수 있다.</p>
<pre><code class="language-kotlin">class Outer {
    inner class Inner
}

val o = Outer()
val boundInnerCtor = o::Inner</code></pre>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://kotlinlang.org/docs/reflection.html">Reflection | Kotlin</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다?]]></title>
            <link>https://velog.io/@gjk-dev/%ED%98%B8%EC%B6%9C-%EC%8B%9C-%EC%9D%B8%EC%9E%90-%EC%A4%91-%EC%96%B4%EB%8A%90-%ED%95%98%EB%82%98%EB%9D%BC%EB%8F%84-%EC%9D%B4%EB%A6%84%EC%9D%84-%EB%AA%85%EC%8B%9C%ED%95%98%EA%B3%A0-%EB%82%98%EB%A9%B4-%ED%98%BC%EB%8F%99%EC%9D%84-%EB%A7%89%EA%B8%B0-%EC%9C%84%ED%95%B4-%EA%B7%B8-%EB%92%A4%EC%97%90-%EC%98%A4%EB%8A%94-%EB%AA%A8%EB%93%A0-%EC%9D%B8%EC%9E%90%EB%8A%94-%EC%9D%B4%EB%A6%84%EC%9D%84-%EA%BC%AD-%EB%AA%85%EC%8B%9C%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@gjk-dev/%ED%98%B8%EC%B6%9C-%EC%8B%9C-%EC%9D%B8%EC%9E%90-%EC%A4%91-%EC%96%B4%EB%8A%90-%ED%95%98%EB%82%98%EB%9D%BC%EB%8F%84-%EC%9D%B4%EB%A6%84%EC%9D%84-%EB%AA%85%EC%8B%9C%ED%95%98%EA%B3%A0-%EB%82%98%EB%A9%B4-%ED%98%BC%EB%8F%99%EC%9D%84-%EB%A7%89%EA%B8%B0-%EC%9C%84%ED%95%B4-%EA%B7%B8-%EB%92%A4%EC%97%90-%EC%98%A4%EB%8A%94-%EB%AA%A8%EB%93%A0-%EC%9D%B8%EC%9E%90%EB%8A%94-%EC%9D%B4%EB%A6%84%EC%9D%84-%EA%BC%AD-%EB%AA%85%EC%8B%9C%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Thu, 20 Mar 2025 10:08:18 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>다음은 <strong>Kotlin in Action</strong> 책 내용이다.</p>
<blockquote>
<p>호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.</p>
</blockquote>
<p>그런데 이를 직접 실험해보니, 컴파일 에러가 나지 않았다. 그래서 왜 <code>해야 한다</code>고 말했는지 궁금했다.</p>
<h1 id="본론">본론</h1>
<p>알아본 결과, <strong>명시된 인수 이후의 인수에 이름을 명시하지 않으면 원래 컴파일 에러가 났었다</strong>고 한다.</p>
<blockquote>
<p>In Kotlin 1.3, when you called a function with named arguments, you had to place all the arguments without names (positional arguments) before the first named argument. For example, you could call f(1, y = 2), but you couldn&#39;t call f(x = 1, 2).</p>
</blockquote>
<p>하지만 위 글 이후에 적혀있듯 <strong>2020년 8월 17일 Kotlin 1.4.0가 출시</strong>되며 순서 관계없이 원하는 인수에만 이름을 명시할 수 있다고 한다.</p>
<blockquote>
<p>In Kotlin 1.4, there is no such limitation – you can now specify a name for an argument in the middle of a set of positional arguments. Moreover, you can mix positional and named arguments any way you like, as long as they remain in the correct order.</p>
</blockquote>
<pre><code class="language-kotlin">fun reformat(
    str: String,
    uppercaseFirstLetter: Boolean = true,
    wordSeparator: Char = &#39; &#39;
) {
    // ...
}

//Function call with a named argument in the middle
reformat(&quot;This is a String!&quot;, uppercaseFirstLetter = false , &#39;-&#39;)</code></pre>
<h1 id="결론">결론</h1>
<p>책의 1부 코틀린 소개 직전, <strong>들어가며</strong>에는 다음과 같이 쓰여있다.</p>
<blockquote>
<p>이 책은 코틀린 1.0과 1.1(이 책이 나온 2016년 기준으로는 개발 중이었음. 2022년 2월 초 최신 버전은 1.6.10)에 초점을 맞춘다. 1.1과 그 이전 버전의 차이가 있는 부분은 본문에서 따로 언급하지만, 개발 중인 내용은 일부 다루지 못한 부분이 있다. 새로운 기능이나 변경 사항에 대해서는 <a href="https://kotlinlang.org%EC%97%90">https://kotlinlang.org</a> 에 있는 온라인 문서를 참고하라.</p>
</blockquote>
<p>즉, <strong>1.4.0에서 추가된 해당 기능이 책 내용에 반영되지</strong> 않은 것 같다.
책 표지에는 <strong>Kotlin 1.6 대응</strong>이라고 쓰여 있었는데, 왜 반영이 안 되어있는지는 잘 모르겠다.</p>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://kotlinlang.org/docs/whatsnew14.html#mixing-named-and-positional-arguments">What&#39;s new in Kotlin 1.4.0 | Kotlin</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단위 테스트(Unit Test)]]></title>
            <link>https://velog.io/@gjk-dev/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Unit-Test</link>
            <guid>https://velog.io/@gjk-dev/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Unit-Test</guid>
            <pubDate>Mon, 17 Mar 2025 08:17:54 GMT</pubDate>
            <description><![CDATA[<h1 id="단위-테스트란"><strong>단위 테스트란?</strong></h1>
<p>단위 테스트란 소스 코드의 특정 모듈(함수, 클래스 등)이 의도된 대로 동작하는지를 검증하는 테스트를 말한다.
JUnit과 같은 도구를 활용해 프로덕션 코드를 작은 단위로 나눠 테스트할 수 있다.</p>
<h1 id="단위-테스트의-특징"><strong>단위 테스트의 특징</strong></h1>
<ul>
<li><strong>빠르고 독립적인 테스트</strong></li>
</ul>
<p>단위 테스트의 목적은 프로그램을 작은 단위로 나누고 고립시켜 <strong>각각의 부분이 잘 동작하는지 확인</strong>하는 것이다. 단위 테스트를 자주 진행하면 코드 수정으로 인한 문제 발생 시 <strong>어느 부분이 잘못되었는지를 빠르게 캐치</strong>할 수 있다.</p>
<ul>
<li><strong>주석을 대체</strong></li>
</ul>
<blockquote>
<p>내가 이렇듯 주석을 무시하는 이유가 무엇이냐고? 거짓말을 하니까. 항상도 아니고 고의도 아니지만 너무 자주 거짓말을 하니까. 주석은 오래될수록 코드에서 멀어진다. 오래될수록 완전히 그릇될 가능성도 커진다. 이유는 단순하다. 프로그래머들이 주석을 유지하고 보수하기란 현실적으로 불가능하니까.</p>
<ul>
<li>로버트 C. 마틴 (Robert Cecil Martin), 클린 코드</li>
</ul>
</blockquote>
<p>주석은 프로그래밍 코드에 남긴 메모이다. 이를 통해 <strong>프로덕션 코드가 어떤 일을 하는지</strong>를 알려줄 수 있다. 문제는 그것에 대한 검증이 없다는 것이다. 반대로 단위 테스트는 코드가 어떤 일을 하는지를 <strong>검증</strong>한다.</p>
<p>또한 프로덕션 코드의 변경은 테스트 코드의 변경을 야기하기에 최신화된 테스트 코드를 제공할 수 있다. 하지만 프로덕션 코드가 변경되었음에도 주석이 변하지 않는 경우는 흔하다. 단위 테스트로 주석을 대체하면 <strong>계속해서 업데이트되는 문서를 제공</strong>할 수 있다.</p>
<h1 id="first-원칙에-따른-단위-테스트-작성">FIRST 원칙에 따른 단위 테스트 작성</h1>
<p>FIRST 규칙은 로버트 마틴이 제안한 규칙으로, 효율적이고 깔끔한 단위 테스트를 작성하기 위한 원칙이다.</p>
<ol>
<li><strong>Fast (빠르게)</strong><ul>
<li>단위 테스트는 빨라야 한다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다.<ul>
<li>자주 돌리지 않으면 문제를 초기에 찾아낼 수 없다.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Independent (독립적으로)</strong><ul>
<li>각 테스트는 서로 의존하면 안 된다.<ul>
<li>한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다.</li>
<li>어떤 순서로 실행해도 괜찮아야 한다.</li>
</ul>
</li>
<li>테스트가 서로에게 의존하면 테스트가 실패할 때 원인을 진단하기 어려워진다.</li>
</ul>
</li>
<li><strong>Repeatable (반복가능하게)</strong><ul>
<li>테스트는 어떤 환경에서도 반복 가능해야 한다.<ul>
<li>네트워크에 연결되지 않은 환경도 포함된다.</li>
</ul>
</li>
<li>테스트가 반복 가능하지 않다면, 테스트를 수행하지 않을 핑계가 생기고 이는 초기에 문제를 찾아낼 수 없게 된다.</li>
</ul>
</li>
<li><strong>Self-Validating (자가검증하는)</strong><ul>
<li>테스트의 결과는 성공 또는 실패만 존재해야 한다.<ul>
<li>통과 여부를 알기 위해 로그 파일을 읽게 해서는 안 된다.</li>
</ul>
</li>
<li>테스트가 스스로 성공과 실패를 결정하지 않으면 판단이 주관적으로 변한다.</li>
</ul>
</li>
<li><strong>Timely (적시에)</strong><ul>
<li>단위 테스트는 실제 코드를 구현하기 직전에 구현해야 한다.<ul>
<li>실제 코드를 먼저 구현하면 테스트하기 어려운 코드가 나올 확률이 높다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr>
<h1 id="ref">REF</h1>
<p>예고르 부가옌코 (Yegor Bugayenko), 엘레강트 오브젝트</p>
<p>로버트 C. 마틴 (Robert Cecil Martin), 클린 코드</p>
<p><a href="https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%9B_%ED%85%8C%EC%8A%A4%ED%8A%B8">유닛 테스트</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 getter, setter를 사용해야 할까?]]></title>
            <link>https://velog.io/@gjk-dev/%EC%99%9C-getter-setter%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-sn4af770</link>
            <guid>https://velog.io/@gjk-dev/%EC%99%9C-getter-setter%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-sn4af770</guid>
            <pubDate>Fri, 07 Mar 2025 07:01:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/gjk-dev/post/96aeead3-384f-4b33-8737-62c02a752c9d/image.png" alt=""></p>
<h1 id="서론">서론</h1>
<p><em><code>getter</code>, <code>setter</code>를 두지 않고 그냥 필드를 <code>public</code>으로 선언하면 되지 않나요? 왜 그렇게 하지 않을까요?</em></p>
<p>위 질문에 대한 답을 하기 위해 알아보았다.</p>
<h2 id="게터-세터란">게터, 세터란?</h2>
<p>자바에서 필드의 값을 얻거나 필드를 초기화하기 위한 함수이다.</p>
<p>Kotlin에서는 자동으로 생성된다.</p>
<pre><code class="language-kotlin">class Person {
    var name: String? = null
        get() = field // 기본 게터
        set(value) {
            field = value // 기본 세터
        }

    fun getName(): String { // 우연히 게터와 이름이 같을 뿐, 로직이 다름
        return name ?: &quot;Unknown&quot;
    }

    fun fetchName(): String? { // 우연히 게터와 로직이 같을 뿐, 이름이 다름
        return name
    }
}</code></pre>
<h1 id="본론">본론</h1>
<p><code>getter</code>, <code>setter</code>를 사용하라는 주장을 확장하면 <strong>필드에 직접 접근하지 말고 메서드를 쓰라</strong>는 말과 같다. 그 이유는 필드에 접근하는 것이 <strong>캡슐화를 위반</strong>하기 때문이다.</p>
<aside>
💡

<p><strong>캡슐화: 상태와 행동을 하나의 객체 안에 모으는 것</strong></p>
<p>객체를 사용하면 나중에 변경될 가능성이 높은 내부 구현(특히 상태)을 외부로부터 감출 수 있다. 이를 통해 변경의 여파를 통제할 수 있다.</p>
</aside>

<p><code>getter</code>, <code>setter</code>를 통해 얻고 세팅하는 과정을 추상화하여 <strong>견고한 부분만을 공개</strong>하고, 내부적으로 어떻게 하는지에 대한 <strong>세부사항은 감출 수 있다.</strong></p>
<h2 id="그럼-getter-setter를-사용하면-캡슐화가-지켜지는-것인가">그럼 getter, setter를 사용하면 캡슐화가 지켜지는 것인가?</h2>
<p>질문은 원점으로 돌아온다.</p>
<p><code>*getter</code>, <code>setter</code>도 결국 필드를 노출하는 것인데, 어떻게 캡슐화가 지켜진다고 할 수 있을까?*</p>
<p>이 질문은 매우 날카로운 질문이다. 다음 <code>name</code>, <code>age</code>의 <code>getter</code>, <code>setter</code>는 필드를 그대로 드러낸다.</p>
<pre><code class="language-kotlin">class Person(
    val name: String,
    val age: Int,
)

fun main() {
    val gio = Person(&quot;지오&quot;, 20)
    println(&quot;안녕하세요. 저는 ${gio.name}이고, ${gio.age}살입니다.&quot;)
}</code></pre>
<p><code>getter</code>, <code>setter</code>를 통해 <code>Person</code>의 상태를 얻어와 <code>main</code>에서 자기소개를 하고 있다. 하지만 다음과 같이 제약사항이 발생한다면 어떨까?</p>
<blockquote>
<p>저희 커뮤니티에서 자기소개를 할 때에는 앞으로 나이를 밝히지 않겠습니다.</p>
</blockquote>
<p>이는 즉 자기소개에 대한 요구사항의 변경이다. 자기소개를 한다는 사실 자체는 변하지 않지만 자기소개에 관한 <strong>세부사항</strong>의 변경이다.
하지만 이는 <code>Person</code> 뿐 아니라 사용처인 <code>main</code>의 수정 또한 필요로 한다.</p>
<pre><code class="language-kotlin">class Person(
    val name: String,
    val age: Int,
)

fun main() {
    val gio = Person(&quot;지오&quot;, 20)
    println(&quot;안녕하세요. 저는 ${gio.name}입니다.&quot;)
}</code></pre>
<p>간단한 요구사항인데도 만약 <code>Person</code>의 사용처가 다양했다면 지옥이 펼쳐졌을 것이다.</p>
<p>하지만 캡슐화를 잘 사용했다면 다음과 같은 코드를 작성했을 것이다.</p>
<pre><code class="language-kotlin">class Person(
    private val name: String,
    private val age: Int,
) {
    fun introduce() {
        println(&quot;안녕하세요. 저는 ${name}이고, ${age}살입니다.&quot;)
    }
}

fun main() {
    val gio = Person(&quot;지오&quot;, 20)
    gio.introduce()
}</code></pre>
<p><code>getter</code>, <code>setter</code>를 아예 사용하지 않고 견고한 부분인 메서드만을 사용해 코드를 수행했다. 이 때 같은 제약사항이 변하면 다음과 같이 코드를 수정하면 된다.</p>
<pre><code class="language-kotlin">class Person(
    private val name: String,
    private val age: Int,
) {
    fun introduce() {
        println(&quot;안녕하세요. 저는 ${name}입니다.&quot;)
    }
}

fun main() {
    val gio = Person(&quot;지오&quot;, 20)
    gio.introduce()
}</code></pre>
<p>이렇게만 보면 큰 차이가 없어 보이지만 다음에 주목해야 한다.</p>
<blockquote>
<p>코드의 수정이 <code>Person</code> 내부에서만 일어났다.</p>
</blockquote>
<p>객체의 사용처는 1곳보다 많을 가능성이 높다. 위 코드는 캡슐화의 원칙을 지켜 <strong>변경의 여파를 제한</strong>했고, <strong>코드 수정을 최소화</strong>하는 효과를 얻는다.</p>
<h1 id="결론">결론</h1>
<ol>
<li><strong>캡슐화를 위반</strong>하면 <strong>변경의 여파를 제한할 수 없다.</strong></li>
<li><code>getter</code>, <code>setter</code>를 사용하지 않으면 <strong>캡슐화를 위반</strong>하기 쉽다.</li>
<li><code>getter</code>, <code>setter</code>를 사용했더라도 <strong>상태를 드러내면 캡슐화에 위반</strong>된다.</li>
<li>객체가 가져야 할 <strong>상태</strong>(데이터)에 초점을 두지 말고, 객체가 수행해야 할 <strong>행동</strong>(동작)에 초점을 두어 개발하자.</li>
</ol>
<p><strong>→ 객체 지향 생활 체조 원칙 9. getter/setter/프로퍼티를 쓰지 않는다.</strong></p>
<hr>
<h1 id="ref">REF</h1>
<p>조영호, 오브젝트: 코드로 이해하는 객체지향 설계</p>
<p><a href="https://kotlinlang.org/docs/properties.html#getters-and-setters">Properties | Kotlin</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로퍼티를 수정하는 방법]]></title>
            <link>https://velog.io/@gjk-dev/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EB%A5%BC-%EC%88%98%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@gjk-dev/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EB%A5%BC-%EC%88%98%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 06 Mar 2025 04:11:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/gjk-dev/post/7b627cd6-1f15-47ed-96bd-c80b067b4dcf/image.png" alt="">
<img src="https://velog.velcdn.com/images/gjk-dev/post/ddef2f86-9e6e-4c97-a417-70be5f7c2fde/image.png" alt=""></p>
<h1 id="서론">서론</h1>
<blockquote>
<p>data class에서 프로퍼티를 수정하는 방법이 고민됩니다.
프로퍼티 선언 시 <code>val</code> 선언 후 <code>copy</code>로 수정할 수도 있고,
프로퍼티를 <code>var</code>로 선언하여 기존의 객체를 유지하면서 프로퍼티만 수정할 수도 있습니다.
<code>val</code>로 선언 후 <code>copy</code>로 수정하면 가변성을 제한할 수 있지만, 새로운 인스턴스를 생성해야 합니다. 두 가지 방법 중 어느 것이 더 좋은 방법이라고 생각하시나요?</p>
</blockquote>
<p>프로퍼티를 <code>var</code>로 선언하면 위험하다고 생각하지만, 그 생각에 근거를 찾지 못해 알아보았다.</p>
<h1 id="본론">본론</h1>
<h2 id="가변의-단점">가변의 단점</h2>
<p><code>var</code>는 읽고 쓸 수 있는 프로퍼티(read-write property)를 선언하는 키워드이다.</p>
<p>클래스에서는 다음과 같이 선언할 수 있다.</p>
<pre><code class="language-kotlin">class BankAccount(
    var amount: Int = 0, // 잔액
)</code></pre>
<p>위처럼 <code>var</code>로 프로퍼티를 선언하면 계속해서 변하는 상태(Ex: 계좌의 잔액, 사람의 신장 등)를 표현하기 좋다.</p>
<p>하지만 다음과 같은 단점들 또한 존재한다:</p>
<ol>
<li><p>상태가 의도한 대로 변하지 않으면 <strong>클래스가 예상치 못한 상황</strong>에 처할 수 있다.</p>
<pre><code class="language-kotlin"> class Person(var age: Int) {
     fun `떡국 먹은 횟수 자랑하기`() {
         println(&quot;떡국 ${age}번 먹었어요.&quot;)
     }
 }

 fun main() {
     val gio = Person(20)
     // 누군가가 아래와 같은 짓을 한다면..?
     gio.age = -1
     gio.`떡국 먹은 횟수 자랑하기`()
 }

 결과: 
 떡국 -1번 먹었어요.</code></pre>
</li>
<li><p>상태 변경이 다양한 곳에서 일어날 수 있으므로 <strong>코드의 실행을 추론하기 어렵다.</strong></p>
<pre><code class="language-kotlin"> class Person {
     var hp: Int = 400

     fun sleep() {
         hp += 50
     }

     fun eat() {
         hp += 30
     }

     fun drinkSoju() {
         hp -= 70
     }

     val isHealthy: Boolean get() = hp &gt; 400
 }

 fun main() {
     val gio =
         Person().apply {
             sleep()
             eat()
             eat()
             drinkSoju()
             eat()
             eat()
             drinkSoju()
             drinkSoju()
             sleep()
         }
     println(gio.isHealthy)
 }</code></pre>
</li>
<li><p>멀티스레드 프로그램의 경우 적절한 동기화가 없으면 충돌이 발생할 수 있다.</p>
<pre><code class="language-kotlin"> suspend fun main() {
     var count = 0
     coroutineScope {
         repeat(1_000) {
             launch {
                 delay(50)
                 count += 1
             }
         }
     }
     println(count)
 }</code></pre>
</li>
</ol>
<h3 id="val은-불변인가">val은 불변인가?</h3>
<pre><code class="language-kotlin">fun main() {
    val list1 = mutableListOf(1, 2, 3)
    val list2 = mutableListOf(1, 2, 3)
    println(&quot;list1 == list2: ${list1 == list2}&quot;)
    list1.add(4)
    println(&quot;list1 == list2: ${list1 == list2}&quot;)
}</code></pre>
<p><code>list1</code>, <code>list2</code>가 불변으로 선언되었는데도 비교 결과가 달라졌다.
<code>list1</code>이 <code>add()</code>를 통해 갖고 있는 요소가 달라졌기 때문이다.</p>
<p>위에서 확인할 수 있듯, <code>val</code>로 선언된 프로퍼티가 변경 가능한 객체를 가질 경우, 값이 변할 수 있다.
따라서 <code>val</code>은 불변보다, <strong>재할당 불가 프로퍼티</strong> 또는 <strong>읽기 전용 프로퍼티</strong>라고 표현하는 것이 맞을 것 같다.</p>
<blockquote>
<p>Use the <code>val</code> keyword to declare <strong>variables that are assigned a value only once.</strong></p>
<p><strong>- <a href="https://kotlinlang.org/docs/basic-syntax.html#variables">Kotlin 공식 문서</a></strong></p>
</blockquote>
<blockquote>
<p><code>val</code>은 읽기 전용 프로퍼티지만, 변경할 수 없음(불변, immutable)을 의미하는 것은 아니라는 것을 기억하기 바랍니다.
<strong>- Effective Kotlin</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gjk-dev/post/8dc28ebb-7667-4582-a738-0e8b66522944/image.png" alt=""></p>
<h2 id="val의-단점-귀찮음">val의 단점: 귀찮음</h2>
<p><code>val</code>로 선언된 프로퍼티의 단점은 무엇이 있을까? 바로 <strong>변경하기 귀찮다는 것</strong>이다…</p>
<pre><code class="language-kotlin">class PersonWithVar(
    val name: String,
    var age: Int,
    val email: String,
    val phoneNumber: String,
) {
    fun `1살 더 먹기`() {
        age++
    }
}

fun main() {
    val gio =
        PersonWithVar(
            &quot;Gio&quot;,
            20,
            &quot;giovannijunseokim@gmail.com&quot;,
            &quot;010-1234-5678&quot;,
        )
    gio.`1살 더 먹기`()
    println(gio.age)
}

결과: 21</code></pre>
<pre><code class="language-kotlin">class PersonWithVal(
    val name: String,
    val age: Int,
    val email: String,
    val phoneNumber: String,
) {
    fun `1살 더 먹기`(): PersonWithVal =
        PersonWithVal(
            name,
            age + 1,
            email,
            phoneNumber,
        )
}

fun main() {
    var gio =
        PersonWithVal(
            &quot;Gio&quot;,
            20,
            &quot;giovannijunseokim@gmail.com&quot;,
            &quot;010-1234-5678&quot;,
        )
    gio = gio.`1살 더 먹기`()
    println(gio.age)
}

결과: 21</code></pre>
<p>이름만 수정하는 메서드, 나이만 수정하는 메서드, 이메일만 수정하는 메서드, 전화번호만 수정하는 메서드까지 만든다면 코드가 매우 길어질 것이다. 이는 귀찮음의 문제를 넘어 <strong>가독성의 문제</strong>로도 넘어간다.</p>
<p>하지만 위는 하나의 함수를 사용하면 훨씬 간단해질 수 있다.</p>
<pre><code class="language-kotlin">class PersonWithVal(
    val name: String,
    val age: Int,
    val email: String,
    val phoneNumber: String,
) {
    fun `1살 더 먹기`(): PersonWithVal = this.copy(age = this.age + 1)

    fun `이름 변경하기`(newName: String): PersonWithVal = this.copy(name = newName)

    fun `이메일 변경하기`(newEmail: String): PersonWithVal = this.copy(email = newEmail)

    fun `전화번호 변경하기`(newPhoneNumber: String): PersonWithVal = this.copy(phoneNumber = newPhoneNumber)

    fun copy(
        name: String = this.name,
        age: Int = this.age,
        email: String = this.email,
        phoneNumber: String = this.phoneNumber,
    ): PersonWithVal = PersonWithVal(name, age, email, phoneNumber)
}</code></pre>
<p>공통된 부분을 추출하여 <code>copy</code> 메서드를 생성했다. 이 메서드는 프로퍼티의 현재 값을 파라미터의 기본값으로 제공한다.</p>
<p>놀랍게도 코틀린은 data 클래스를 사용하면 직접 적지 않고도 이 코드를 생성할 수 있다.</p>
<pre><code class="language-kotlin">data class PersonDataClass(
    val name: String,
    val age: Int,
    val email: String,
    val phoneNumber: String,
)

class PersonNormalClass(
    val name: String,
    val age: Int,
    val email: String,
    val phoneNumber: String,
) {
    fun copy(
        name: String = this.name,
        age: Int = this.age,
        email: String = this.email,
        phoneNumber: String = this.phoneNumber,
    ): PersonNormalClass = PersonNormalClass(name, age, email, phoneNumber)
}</code></pre>
<p>두 클래스를 디컴파일해보면 다음과 같다.</p>
<pre><code class="language-kotlin">public final class PersonNormalClass {
   ...

   @NotNull
   public final PersonNormalClass copy(@NotNull String name, int age, @NotNull String email, @NotNull String phoneNumber) {
      Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
      Intrinsics.checkNotNullParameter(email, &quot;email&quot;);
      Intrinsics.checkNotNullParameter(phoneNumber, &quot;phoneNumber&quot;);
      return new PersonNormalClass(name, age, email, phoneNumber);
   }
   ...
}

public final class PersonDataClass {
     ...
   @NotNull
   public final PersonDataClass copy(@NotNull String name, int age, @NotNull String email, @NotNull String phoneNumber) {
      Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
      Intrinsics.checkNotNullParameter(email, &quot;email&quot;);
      Intrinsics.checkNotNullParameter(phoneNumber, &quot;phoneNumber&quot;);
      return new PersonDataClass(name, age, email, phoneNumber);
   }
   ...
}</code></pre>
<h3 id="그럼-계속해서-객체가-생성될텐데">그럼 계속해서 객체가 생성될텐데?</h3>
<p>메모리 관점에서 좋은 지적이다. <code>var</code>를 사용했다면 추가적인 객체 생성이 발생하지 않는다.
결국 <code>val</code>를 사용하는 것과 <code>var</code>를 사용하는 것은 각각의 장단점이 있기에 상황에 맞게 선택해야 한다.</p>
<p>나는 <code>var</code>로 인해 생기는 문제는 유지보수에 영향을 주고, <code>val</code>로 인해 생기는 문제는 메모리에 영향을 준다고 생각한다. 그리고 둘을 비교하자면, 현 상황에서는 유지보수를 선택해야 한다고 생각한다. 인건비가 메모리보다 비싸기 때문이다.</p>
<p>끝으로 무어의 법칙을 소개한다.</p>
<blockquote>
<p>무어의 법칙은 반도체 집적회로의 성능이 24개월마다 2배로 증가한다는 법칙이다. 경험적인 관찰에 바탕을 두고 있다. 인텔의 공동 설립자인 고든 무어가 1965년에 내 놓은 것이다.
<strong>- 무어의 법칙 - 위키백과</strong></p>
</blockquote>
<h1 id="결론">결론</h1>
<ol>
<li><code>var</code>로 선언된 프로퍼티는 여러번 값을 할당할 수 있으므로 클래스가 <strong>예상치 못한 상황</strong>에 처하기 쉽고, <strong>코드 실행 예측이 어려우며</strong>, 멀티스레드 환경에서는 <strong>적절한 동기화를 요한다.</strong></li>
<li><code>val</code>은 불변이 아닌, 재할당이 불가능한 읽기 전용 프로퍼티이기에 위 문제가 어느정도 해결된다.<ol>
<li>만약 이 경우 프로퍼티를 수정하고 싶다면, 원하는 값을 가진 새 객체를 만들어야 한다.</li>
</ol>
</li>
<li><code>copy</code> 메서드를 만들면 특정한 프로퍼티 값만 수정된 객체를 쉽게 얻을 수 있다.</li>
<li>data class는 <code>copy</code> 메서드를 자동으로 생성해주고, 이를 통해 <code>val</code> 프로퍼티를 사용하는 것이 수월해진다.</li>
</ol>
<hr>
<h1 id="ref">REF</h1>
<p><a href="https://kt.academy/article/ek-mutability">Effective Kotlin Item 1: Limit mutability</a></p>
<p><a href="https://kotlinlang.org/docs/basic-syntax.html#variables">Basic syntax | Kotlin</a></p>
<p><a href="https://ko.wikipedia.org/wiki/%EB%AC%B4%EC%96%B4%EC%9D%98_%EB%B2%95%EC%B9%99">무어의 법칙</a></p>
]]></description>
        </item>
    </channel>
</rss>