<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>noeyh_0j.log</title>
        <link>https://velog.io/</link>
        <description>천천히, 꾸준히, 한 걸음씩</description>
        <lastBuildDate>Wed, 19 Aug 2026 13:52:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>noeyh_0j.log</title>
            <url>https://velog.velcdn.com/images/noeyh_0j/profile/99c1ea39-d8c4-409b-a40c-6cfcab2d90e3/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. noeyh_0j.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/noeyh_0j" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] 디버그를 하면 앱이 삭제돼요….]]></title>
            <link>https://velog.io/@noeyh_0j/Android-%EB%94%94%EB%B2%84%EA%B7%B8%EB%A5%BC-%ED%95%98%EB%A9%B4-%EC%95%B1%EC%9D%B4-%EC%82%AD%EC%A0%9C%EB%8F%BC%EC%9A%94</link>
            <guid>https://velog.io/@noeyh_0j/Android-%EB%94%94%EB%B2%84%EA%B7%B8%EB%A5%BC-%ED%95%98%EB%A9%B4-%EC%95%B1%EC%9D%B4-%EC%82%AD%EC%A0%9C%EB%8F%BC%EC%9A%94</guid>
            <pubDate>Wed, 19 Aug 2026 13:52:53 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>오랜만에 돌아온 개발자 꿈나무 김조현입니다. 이번 글은 디버깅을 위해 빌드한 앱이 스토어에 배포된 앱과 충돌하는 문제를 해결한 과정을 풀어가는 글입니다. </p>
<p>우테코에서 진행하는 프로젝트 구현을 하는 중 나는 늘 그렇듯이 편하게 디바이스를 연결해서 작업을 하고 있었다. 하지만 옆에서 작업하는 엘리는 갤럭시 핸드폰임에도 불구하고 에뮬레이터로 디버깅을 하고 있었다.</p>
<p>“왜 에뮬레이터를 써? 핸드폰 연결해서 디버깅 해볼 수 있잖아!” 라고 물어봤을 때 엘리는 “그러면 비공개 테스트 때문에 설치한 앱이 충돌되서 에뮬레이터를 써” 라고 대답해줬다.</p>
<p>나는 처음으로 디버깅을 하면 스토어에서 설치한 앱이 삭제된다는 것을 깨달았다. 이전에도 &#39;디버깅용 앱이 구분되는 구나&#39; 라는 단순한 사실은 알았지만 이 앱의 존재 의미가 단순히 “나 이 앱의 개발자에요”, “내가 이 앱 만들었어요” 정도의 상징성 정도로만 생각을 했다.</p>
<p>그래서 생각을 해봤다. 어떻게 하면 디버깅용 앱을 따로 구현해볼 수 있을까? ‘바이링구얼’처럼 ‘여백’도 디버깅용 앱을 만든다면, 릴리즈 빌드된 앱을 삭제하지 않고도 디버깅을 할 수 있을 것이라고 생각을 했다.</p>
<p>이 글에서는 어떤 식으로 디버깅용 앱을 설정하는지에 관해 설명해보고자 한다.</p>
<p>왜 디버깅을 할 때 릴리즈 빌드 앱이 삭제되는 것일까? 이는 applicationId가 동일하기 때문이다. 이에 대해서는 아래에서 조금 더 풀어보겠다.</p>
<h1 id="1-debug-리소스-폴더-만들기">1. debug 리소스 폴더 만들기</h1>
<p>처음으로 해야 할 일은 debug 리소스 폴더를 만드는 것이다. 현재 프로젝트는 kmp 프로젝트이기 때문에 네이티브 프로젝트와 구조가 다를 수 있다. 하지만 해야할 일은 같다. 네이티브일 경우 프로젝트/src에서 새로운 <code>Android Resource Directory</code> 를 만들어주면 된다. kmp에서도 마찬가지로 <code>androidApp</code> 에서 위와 같은 과정을 시도하면 된다.</p>
<div align='center'>
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/5ca009f3-9e85-4e6c-a3ad-81c816995365/image.png" width=75%/>
</div>

<p>그러면 아래와 같이 <code>Source set</code> 이 나타나게 되는데, 여기서 debug로 생성하면 된다.</p>
<div align='center'>
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/6b7c0f42-28ad-4c80-81c6-4768a0fcac4d/image.png" width=75%/>
</div>

<h1 id="2-앱-이름-및-아이콘-설정하기">2. 앱 이름 및 아이콘 설정하기</h1>
<p>그러면 기반은 다 만든 것이다. 앱 이름을 설정한 것처럼 <code>debug/res/values/strings.xml</code> 에서 앱 이름을 만들어주면 된다. 우리 프로젝트인 여백의 경우는 아래와 같이 만들어줬다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;resources&gt;
    &lt;string name=&quot;app_name&quot;&gt;조밀&lt;/string&gt;
&lt;/resources&gt;
</code></pre>
<p>사실 이러면 끝이다. 디버그 빌드일 경우 <code>조밀</code> 이 앱 이름이 되고, 릴리즈 빌드일 경우 <code>여백</code> 이 되는 것이다. 아이콘을 바꾸는 것도 main에서 아이콘을 변경할 때와 같다. 다만 debug 안에서 이미지 리소스를 생성해야 한다.</p>
<p>또한 이때 아이콘의 이름은 Manifest에서 참조하는 <code>ic_launcher</code> 와 같은 이름으로 debug 리소스를 만들어줘야 한다. 그러면 debug 빌드에서 해당 아이콘이 main의 아이콘을 덮어 쓴다.</p>
<p>문뜩 왜 앱 이름과 아이콘을 바꿔야해요? 라는 의문이 들 수도 있다. 이 글에서 말한 문제는 릴리즈 빌드된 앱과 디버그 빌드 앱이 충돌하는 것이었다. 그렇다면 두 가지의 앱을 설치해야 할텐데 이름과 아이콘이 같다면, 어떤 것이 디버깅용 앱인지, 어떤게 릴리즈 빌드된 앱인지 구분하기 어려울 것이다.</p>
<p>이런 상황을 방지하고자 앱 이름과 아이콘을 바꾸고자 한 것이다.</p>
<h1 id="3-applicationid-변경">3. applicationId 변경</h1>
<p>이후에 빌드를 했을 때 디버깅용 앱 이름과 아이콘이 나타나는 것을 볼 수 있을 것이다. 하지만 여전히 앱이 삭제되는 것을 막을 수 없다.</p>
<p>위에서 앱이 삭제되는 것이 릴리즈 빌드 앱과 디버그 빌드 앱의 applicationId가 동일하기 때문이라고 하였다. 이 말은 applicationId를 구분한다면? 앱이 삭제되는 문제를 해결할 수 있다는 것이다.</p>
<p>이를 위해서는 androidApp/build.gradle.kts에서 아래와 같이 debug 사항을 표기해야 한다.</p>
<pre><code class="language-kotlin">android {
        buildTypes {
                debug {
                    applicationIdSuffix = &quot;.debug&quot;
                }
        }
}</code></pre>
<p>이런 식으로 적용한다면 아래와 같이 applicationId가 구분될 수 있다.</p>
<pre><code class="language-kotlin">여백
applicationId = com.yeobaek

조밀
applicationId = com.yeobaek.debug</code></pre>
<h1 id="마무리">마무리</h1>
<p>이렇게 두 가지의 앱이 충돌나지 않도록 하는 방법에 대해 정리해봤습니다! 읽어주셔서 감사합니다 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Repository를 다루는 두 가지 방법: 싱글톤과 DI]]></title>
            <link>https://velog.io/@noeyh_0j/Android-Repository%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95-%EC%8B%B1%EA%B8%80%ED%86%A4%EA%B3%BC-DI</link>
            <guid>https://velog.io/@noeyh_0j/Android-Repository%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95-%EC%8B%B1%EA%B8%80%ED%86%A4%EA%B3%BC-DI</guid>
            <pubDate>Sun, 19 Jul 2026 03:13:09 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-kotlin">class CrimeRepository private constructor(context: Context) {

    companion object {
        private var INSTANCE: CrimeRepository? = null

        fun initialize(context: Context) {
            if (INSTANCE == null) {
                INSTANCE = CrimeRepository(context)
            }
        }

        fun get(): CrimeRepository {
            return INSTANCE ?: throw IllegalStateException(&quot;CrimeRepository must be initialized&quot;)
        }
    }
}</code></pre>
<h1 id="repository-코드는-무슨-역할을-하는거야">Repository 코드는 무슨 역할을 하는거야?</h1>
<p>위 코드는 Room의 데이터를 관리하는 Repository 코드다.</p>
<p>Repository의 역할은 앱의 데이터(DB나 네트워크)를 총괄하는 관리자다. 만약 화면이 여러개인데, 각 화면이 데이터가 필요할 때마다 val repo = CrimeRepository()로 매번 새로운 관리자를 생성한다면 아래와 같은 문제가 발생할 것이다.</p>
<ul>
<li>A 화면의 관리자가 DB에 데이터를 썼는데, B화면의 관리자는 그 사실을 모를 수 있다. 즉, 데이터 싱크가 어긋나는 문제가 발생한다.</li>
<li>무거운 데이터베이스 연결 통로를 10개나 뚫어버리게 되어 스마트폰의 메모리와 배터리가 남아나지 않을 것이다.</li>
</ul>
<p>이런 문제가 발생할 수 있기에 private constructor를 사용해서 외부에서는 절대 새로운 관리자를 마음대로 생성하지 못하게 막아버리는 것이다. 오직 INSTANCE라는 변수에 딱 하나의 관리자만 만들어두고, 모든 화면이 한 명의 관리자에게만 데이터를 요청하도록 통제하는 것이다.</p>
<h1 id="그러면-왜-object는-사용하지-않는거야">그러면 왜 object는 사용하지 않는거야?</h1>
<p>싱글톤으로 선언해야 하는 이유는 이해가 됐다. 그치만 코틀린에서는 object라는 키워드로 싱글톤을 생성할 수 있다. 하지만 위 코드에서는 companion object를 사용해서 내 생각(object를 사용해 간단한 방법)보다 복잡하게 구현하였다.</p>
<p>그 이유는 Context 때문이다.</p>
<ul>
<li>코틀린의 object는 선언과 동시에 만들어지기 때문에, 생성자로 파라미터(값)을 넘겨받을 수 없다.</li>
<li>하지만 안드로이드에서 로컬 데이터베이스(Room, SQLite)를 열거나 파일(SharedPreferences)에 접근하려면 안드로이드 시스템의 환경 정보인 Context가 무조건 필수다.</li>
</ul>
<p>그렇기에 object를 쓰지 못하고, initialize(context)라는 함수를 통해 앱이 켜질 때 Context를 주입하면서 수동으로 딱 한 번만 유일한 객체를 만들도록 구현한 것이다.</p>
<h1 id="하지만-companion-object를-사용해야할까">하지만… companion object를 사용해야할까?</h1>
<p>문뜩 저 코드를 보고 이전에 만들었던 Repository 코드들이 스쳐갔다. 그 때 Repository를 선언할 때는 Repository 코드를 짜면서, 싱글톤으로 관리하기 위해 다른 싱글톤 객체를 선언했던 것 같다.</p>
<p>예를 들면 아래와 같은 느낌이다.</p>
<pre><code class="language-kotlin">class PracRepository(context: Context) {
        fun getItems(): List&lt;Item&gt; {}
        fun getItem(id: String): Item {}
}

object AppContainer {
        private var pracRepository: PracRepository? = null

        fun providePracRepository(context: Context): PracRepository {
                if (pracRepository == null) {
                        pracRepository = PracRepository(context.applicationContext)
                }
                return pracRepository!!
        }
}</code></pre>
<h1 id="두-가지-방식은-뭐가-다를까">두 가지 방식은 뭐가 다를까?</h1>
<h2 id="단일-책임-원칙single-responsibility-principle의-달성">단일 책임 원칙(Single Responsibility Principle)의 달성</h2>
<p>첫 번째 코드의 경우 CrimeRepository가 데이터베이스 접근도 해야 하고, 자기 자신이 오직 한 개만 만들어지도록 스스로 통제도 해야한다. 즉, 역할이 2개다.</p>
<p>두 번째 코드의 경우 PracRepository는 오직 데이터를 다루는 일만 한다. 객체를 어떻게 한 개만 만들어지는지를 모른다. 이는 AppContainer를 만듦으로써 책임을 분리하였다.</p>
<h2 id="확장성의-차이">확장성의 차이</h2>
<p>첫 번째 코드의 경우 UserRepository, SettingRepository 등 다른 Repository 코드가 추가될 때, 모든 클래스 안에 companion object, initialize() 코드를 똑같이 복사하여 붙여 넣어야 한다. 이는 보일러플레이트가 발생하는 것이다.</p>
<p>두 번째 코드의 경우는 위처럼 같은 코드를 복붙하는게 아니라 AppContainer라는 매니저 안에서 전체적으로 관리할 수 있다.</p>
<h2 id="테스트의-용이성">테스트의 용이성</h2>
<p>첫 번째 코드의 경우 private로 생성자가 막혀있기 때문에 나중에 Mock 데이터를 넣어서 테스트를 하고 싶어도 가짜 Repository를 생성할 방법이 없다.</p>
<p>두 번째 코드는 public으로 열려있기 때문에 매니저를 거치지 않고 Mock 데이터를 가진 새로운 Repository를 마음대로 생성해서 테스트할 수 있다.</p>
<h1 id="그러면-레포지토리-내장-싱글톤-패턴은-사용하면-안되는걸까">그러면 레포지토리 내장 싱글톤 패턴은 사용하면 안되는걸까?</h1>
<p>DI 방식이 그러면 더 좋은건가? 그럼 책에서 나온 레포지토리 내장 싱글톤 패턴은 안티 패턴인가? 그럼 책은 우리에게 안좋은 코드를 보여주는건가?</p>
<p>꼭 그렇지만은 않다. 목적과 상황에 따라 각각의 패턴을 사용하여 구현할 수 있다.</p>
<h2 id="레포지토리-내장-싱글톤-패턴을-사용할-때">레포지토리 내장 싱글톤 패턴을 사용할 때</h2>
<h3 id="1-빠르고-직관적인-초기-구현-개발-속도">1. 빠르고 직관적인 초기 구현 (개발 속도)</h3>
<p>코드가 굉장히 짧고 직관적이다. CrimeRepository.get()이면 앱 어디서든 데이터를 끄집어낼 수 있다.</p>
<p>해커톤처럼 24시간 안에 앱을 만들거나, 단순히 기능이 돌아가는지 확인하는 MVP 앱을 만들 때는 굳이 AppContainer 같은 무거운 구조를 잡을 필요가 없다.</p>
<h2 id="2-절대-변하지-않을-완벽한-확신이-있을-때">2. 절대 변하지 않을 완벽한 확신이 있을 때</h2>
<p>만약 그 Repository가 관리하는 데이터가 너무나도 단순하고, 앱이 종료될 때까지 평생 다른 데이터 소스(Mock 데이터 등)로 교체될 확률이 0에 가깝다면, 굳이 테스트나 확장을 고려한 DI 구조를 짤 필요가 없다.</p>
<h2 id="그러면-어떤-상황일-때-의존성-주입di-방식으로-넘어가야-할까">그러면 어떤 상황일 때 의존성 주입(DI) 방식으로 넘어가야 할까?</h2>
<h3 id="1-단위-테스트를-짜야할-때">1. 단위 테스트를 짜야할 때</h3>
<h3 id="2-repository가-너무-많아지고-복잡해질-때">2. Repository가 너무 많아지고 복잡해질 때</h3>
<h3 id="3-팀원들과-협업해야-할-때">3. 팀원들과 협업해야 할 때</h3>
<p>즉, 책에서 나왔던 레포지토리 내장 패턴은 틀린 것이 아니라 학습용이자 작은 규모에 적합한 패턴일 뿐이다. 기준에 따라 다르게 설계하고 구현할 수 있어야 한다는 것을 깨달았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin/android] 왜 strings.xml 파일에 문자열을 넣어 사용할까?]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%99%9C-strings.xml-%ED%8C%8C%EC%9D%BC%EC%97%90-%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%84-%EB%84%A3%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@noeyh_0j/%EC%99%9C-strings.xml-%ED%8C%8C%EC%9D%BC%EC%97%90-%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%84-%EB%84%A3%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 29 Jun 2026 04:40:40 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>우테코 방학이 시작되어 몹시 설레는 개발자 꿈나무 김조현입니다.</p>
<p>방학인데 무엇을 공부해볼까 고민하다가 찾게 된 것이 &quot;안드로이드 학습 튜토리얼 실습을 따라해보자!&quot;였다.</p>
<p>우매함의 봉우리에 있어서 어렵지 않겠지~라는 가벼운 마음으로 들어갔지만, 모르는 것들이 생각보다 많아서 학습하기 좋다고 생각이 들었다!</p>
<div align=center>
    <img src=https://velog.velcdn.com/images/noeyh_0j/post/80e0fb08-18f9-468b-86ab-a87e5c507dc7/image.png width=50%>
</div>

<p>그 학습 과정에서 몰랐던? 궁금해서 찾아본 내용을 정리하는 방식으로 이 글을 풀어보고자 한다.</p>
<h1 id="왜-stringsxml에-문자열을-넣지">왜 strings.xml에 문자열을 넣지?</h1>
<p>이 튜토리얼을 진행하는 과정에서 Roll이라는 문자열을 Text에 넣기 위해 <code>Text(”Roll”)</code>이 아닌 <code>Text(stringResource(R.string.roll))</code>이라는 형식으로 사용했다.</p>
<div align=center>
    <img src=https://velog.velcdn.com/images/noeyh_0j/post/c19136d1-ce5e-4663-95f7-709a3e35070d/image.png width=50%>
</div>

<p>이를 위해 res/values/strings.xml에 <string name=”roll”>Roll</string>을 추가도 하였다.</p>
<p>나는 지금까지 Text에 공통된 문자열을 넣기 위해 싱글톤을 사용하였는데, 왜 이렇게 하는지가 궁금해졌다.</p>
<h2 id="1-다국어-지원">1. 다국어 지원</h2>
<p>내가 지금까지 Text(”Roll”)로 작성한 것처럼 하드코딩을 한다면 이 앱에서 이 텍스트는 영원히 바꿀 수 없게 된다. 지금 이 문자열이 영어지만 한국어로 변경하기 위해 “굴리다”로 변경하려면 앱 자체를 뜯어야 한다.</p>
<p>하지만 strings.xml을 사용하면 안드로이드 시스템이 이를 해결해준다.</p>
<ul>
<li>values/strings.xml : <string name=”roll”>Roll</string></li>
<li>values-ko/strings.xml : <string name=”roll”>굴리다</string></li>
</ul>
<p>그리고 코드에서는 R.string.roll이라는 id를 가져오도록만 적어두면 앱이 실행될 때 안드로이드 시스템이 현재 스마트폰 언어 설정을 파악해 알맞은 언어의 문자열을 자동으로 넣어준다.</p>
<h3 id="그러면-values-패키지와-values-ko라는-패키지명이-이를-해주는건가">그러면? values 패키지와 values-ko라는 패키지명이 이를 해주는건가?</h3>
<p>왜 이런 생각이 들었나면 <code>안드로이드 시스템이 알아서 해준다.</code> 라는 마법같은 문장으로 이해하고자 했지만, 안드로이드 시스템은 어떻게 이를 구분할까? </p>
<p>설정된 언어를 안드로이드 시스템이 알고 있는건 이해가 되지만, 프로젝트 안에서 언어를 알맞게 적용하는 것은 별도의 문제라는 생각이 들었다.</p>
<hr>
<p>안드로이드 시스템과 프로젝트의 values를 연결해 주는 핵심 원리는 <code>구성 한정자(Configuration Qualifiers)</code>와 <code>대체(Fallback) 알고리즘</code>이라고 한다.</p>
<h4 id="구성-한정자">구성 한정자</h4>
<p>구성 한정자란 폴더 이름 뒤에 붙는 -ko, -en, -land(가로 모드), -night(다크 모드) 꼬리표들을 의미한다.</p>
<p>이처럼 안드로이드 스튜디오에서 values나 values-ko라는 폴더를 만들 때, 이 이름들은 우리가 편하게 보기 위해 이렇게 짓는 것이 아니라 안드로이드 OS와 약속된 규칙인 것이다.</p>
<h4 id="대체-알고리즘">대체 알고리즘</h4>
<p>만약 프랑스어를 사용하는 유저가 앱을 다운받았다고 가정해보자. 근데 우리 프로젝트에는 values-fr이 없는 것이다. 그러면 우리 앱이 프랑스어를 지원해주지 않는데, 프랑스어를 가져오려고 하면 터지는걸까?</p>
<p>이런 문제를 해결해주는게 대체 알고리즘이다. 리소스 매니저는 이러한 대체 시스템을 가지고 있다.</p>
<ol>
<li>사용자의 언어가 프랑스어다. values-fr 폴더를 찾아라!</li>
<li>근데 values-fr 폴더가 없네?</li>
<li>그러면 가장 기본 폴더인 values로 돌아가서 거기 존재하는 값을 꺼내와!</li>
</ol>
<p>즉, 아무 구성 한정자가 존재하지 않는 values는 해당 언어 폴더가 없을 때 기본 폴더의 역할을 하는 것이다.</p>
<h2 id="2-재사용성과-유지보수">2. 재사용성과 유지보수</h2>
<p>하드코딩을 했다면 같은 문자열이 들어간 모든 부분을 수정하기 위해 코드를 이곳 저곳 돌아다니며 수정해야 한다. 그러다보면 어쩔 수 없이 실수가 발생할 수도 있다.</p>
<p>하지만 strings.xml에 문자열을 지정해놓으면 문자열이 있는 strings.xml 파일의 특정 문자열만 수정하면 이 문자열 id를 사용하는 모든 곳에 변경이 적용된다.</p>
<p>이는 싱글톤을 사용하는 것과 비슷한 개념이라고 생각한다. 하지만 그럼에도 싱글톤이 아닌 strings.xml을 사용하는 이유는 1번과 같은 이유가 크지 않을까? 라는 생각이 든다.</p>
<h3 id="그러면-공통된-문자열을-사용할-때는-항상-싱글톤보다-stringsxml에-넣어야-하는걸까">그러면 공통된 문자열을 사용할 때는 항상 싱글톤보다 strings.xml에 넣어야 하는걸까?</h3>
<p>이 또한 상황에 따라서 알맞게 사용해야 한다. 위에서 설명한 것처럼 주사위를 굴리라는 “Roll”같은 문자열이 만약 한국어로 번역되어야 한다. 라고 하면 이는 싱글톤보단 strings.xml이 편할 것이다.</p>
<p>서버 API 주소 또는 키값 같은 경우도 공통으로 사용되니까 strings.xml에 넣어 사용하는게 좋을까? 이건 아닐 것이다. strings.xml에 넣었을 때 키값이 번역되어 버린다면 이 키에 해당하는 값을 영원히 찾지 못할 것이다.</p>
<p>즉, 공통된 문자열을 정리하되 읽는 주체에 따라 다르게 관리해야 한다는 것을 알 수 있다.</p>
<blockquote>
<ul>
<li>읽는 주체가 ‘사용자’일 때 → strings.xml</li>
<li>읽는 주체가 ‘로직’일 때 → 싱글톤(object / const val)</li>
</ul>
</blockquote>
<h1 id="마무리">마무리</h1>
<p>이전에는 문자열을 사용할 때 무조건 싱글톤으로 상수를 만들어 관리했었지만, strings.xml로 UI에 나타나는 문자열을 관리할 수 있다는 사실을 알게 되었다. 그 뿐만 아니라 다국어 지원의 경우 더 관리하기 편하다는 사실을 알고 이제부터는 UI에 나타나는 문자열을 관리할 때는 strings.xml을 통해 관리할 것 같다.</p>
<p>읽어주셔서 감사합니다! 🙇</p>
<blockquote>
<p>튜토리얼 실습
<a href="https://developer.android.com/codelabs/basic-android-kotlin-compose-build-a-dice-roller-app?hl=ko#0">https://developer.android.com/codelabs/basic-android-kotlin-compose-build-a-dice-roller-app?hl=ko#0</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 레벨2 전체 회고]]></title>
            <link>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EB%A0%88%EB%B2%A82-%EC%A0%84%EC%B2%B4-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EB%A0%88%EB%B2%A82-%EC%A0%84%EC%B2%B4-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 28 Jun 2026 13:01:03 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>안녕하세요. 회고를 통해 스스로를 돌아보며 성장하는 개발자 꿈나무 김조현입니다.</p>
<p>레벨1이 끝난게 엊그제 같은데 벌써 레벨2 회고를 쓰고 있다니.... 하는 것이 많아서 그런지 시간이 무척 빨리 가는거 같다 ㅎㅎ...</p>
<p>지난 회고를 작성할 때 까먹은 일들이 많아서 이번 레벨부터는 짧게라도 일기를 써보자고 했는데...... 실패했다 ㅎ</p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/0b9c059c-635a-48b3-8e47-f3d9cbc5a662/image.png" alt=""></p>
<p>그래도? 작심삼일은 갔다!! 일주일치는 썼네 ㅎㅎㅎ 고생했다 나~</p>
<h1 id="장바구니-미션">장바구니 미션</h1>
<p>레벨1 때와 동일한 방식으로 진행되는 미션이다. 페어 프로그래밍 → 리뷰로 진행되었다. 많은 일들이 있었다. 무슨 미션인지, 어떤 것을 구현했는지를 명시하기 보단 무엇을 배웠는가를 적는게 더 보람차다고 생각한다. 많은 일이 있었던 만큼 많은 것을 느꼈던 미션이다. </p>
<h2 id="interface-추상화의-필요성">interface 추상화의 필요성</h2>
<p>기술적으로는 interface로 추상화하는 것이 왜 필요한가에 대해 깨달았다. 추상화를 하지 않았을 경우에는 코드가 서로 강력하게 의존되어, 하나의 코드가 변경되었을 때 다른 코드도 변경해야한다는 불편함이 있다. 간단하게 예를 들면 목 데이터를 사용하여 앱을 만들어 mock data → viewmodel → ui 형식으로 데이터의 흐름이 간다고 해보자. 이 때 mock data가 아닌 서버 또는 로컬로부터 오도록 데이터의 형식을 변경한다고 해보자. 그러면 mock data가 의존되어 있는 viewModel 코드 또한 변경되어야 할 것이다.</p>
<p>하지만 interface로 서비스가 받아오는 데이터를 추상화한다면, viewmodel의 경우는 추상화된 인터페이스를 받게되어 입력받는 data 구현체만 변경하도록 의존성을 분리할 수 있다는 것을 알았다.</p>
<h2 id="과식의-위험성">과식의 위험성</h2>
<p>개인적? 학습적으로는 과식을 하지 않겠다고 느꼈다…. 과식이라 부르는 이 단어는 이번 미션에서 지정된 범위보다 더 나아가 학습을 진행했을 때를 일컬어 부르는 것이다 ㅋㅋㅋ</p>
<p>장바구니 미션에서 도메인 데이터와 UI의 상태를 관리하기 위해 stateHolder만을 사용해도 충분했었다. 하지만 더 나아가 viewModel을 사용했었다. 그랬을 때 flow가 무엇인지, viewModelScope가 무엇인지, 왜 viewModel을 사용하면 구성 변경에서 상태가 살아남을 수 있는지 등 아무것도 모르는 상태에서 viewModel을 사용하니 리뷰어가 남겨준 질문에 대해 아무것도 대답할 수 없었다. 애초에 stateHolder로 구현함에 충분하고, 이를 활용하는 것이 이번 미션의 학습 범위이였기 때문에 내가 배울 수 있는 범위조차 제대로 학습하지 않고 더 나아가 학습하고자 했음을 느꼈다.</p>
<h2 id="비동기-학습에-대한-도전">비동기 학습에 대한 도전</h2>
<p>그리고 코루틴에 대해서도 집중적으로 많이 공부했다. 이번 미션에서 서버 데이터와 연결이 있었기에 커리큘럼에 코루틴 또한 포함되었던 것 같다.</p>
<p>우테코 오기 이전부터 코딩을 배울 때 항상 비동기라는 키워드가 어렵게 느껴졌었다. 내가 아는 코드는 위에서부터 아래로 읽고 순차적으로 동작하는 것인데, 멈추고~ 기다리고~ async~ await~ 뭐라는지 모르겠고 어떻게 동작하는지도 몰랐었다. 어려웠기에 애초에 공부를 하려고 시도를 안해봤던 것 같다.</p>
<p>그래서 이번 기회에 코루틴 즉, 비동기 흐름에 대해 제대로 이해해보는 것을 목표로 했다. 학습할 때는 “코틀린 코루틴의 정석” 책을 정독했다. 물론~ 수업도 열심히 들었다 :)</p>
<p>완벽하게 내가 어떤 상황에서든 코루틴을 야무지게 활용할 수 있다!! 까진 아니더라도, 이 코드가 무엇인지, 어떻게 흘러가는지 등 이해했다고 느낄 정도의 학습을 했다고 생각한다. 그래서 내가 이를 잘 배웠는가를 확인하면서 다른 사람들에게도 알려주기 위해 코루틴을 학습하고, 수업 범위에 겹친 내용을 방과후를 통해 발표하고자 했다.</p>
<p>이는 뒤에서 자세하게 풀어보겠다.</p>
<h1 id="미니-프로젝트">미니 프로젝트</h1>
<p>레벨2에서 가장 재밌었으면서도, 가장 알찼던 미션이라고 생각한다. 이 미니프로젝트는 4주차로 구성되었다.</p>
<h2 id="첫-번째-주차">첫 번째 주차</h2>
<p>첫 번째 주차에서는 팀 빌딩 및 주제 선정이었다. 이 미션은 먼저 크루원 각자가 하고 싶은 주제에 대해 정하고, 그 주제를 어필해서 팀을 꾸리는 방식으로 팀빌딩을 진행하는 것이었다. 하지만 나는? 엘리와 엠버를 먼저 팀으로 데리고 그 이후에 주제를 선정했다 ㅎㅎㅎ</p>
<p>제임스가 말씀해주시길 이런 의도가 아니였다고 하지만…….. 그렇게 됐다 ㅎㅎ 어쩔 수 없었다~</p>
<p>또한 아두이노도 지원해주며, 안드로이드에 종속되지 않고 다양한 플랫폼을 사용해보라는 취지도 있었기에 “집에서 나올 때 에어컨, 불 자동으로 꺼주기”, “책을 넘겨주는 독서대” 등 소프트웨어가 아닌 주제도 다양하게 나왔었다.</p>
<p>그 중에서 우리 팀이 정한 주제는 “출석앱 개선하기”였다. 현재 우테코 크루들이 사용하는 출석 앱에서는 네트워크가 연결되지 않았을 때 출석 화면이 나타나지 않는 문제가 있었다.</p>
<p>이 문제로 인해 출석 과정에서 거의 대부분의 크루가 불편함을 겪고 있다는 것을 알고, 이를 해결하기 위해 네트워크에 의존하지 않는 NFC를 이용한 출석 앱을 만들어보자! 라는 식으로 결정하였다.</p>
<p>많은 주제들 중 이 주제를 선정한 이유는 크게 두 가지가 있다.</p>
<blockquote>
<ol>
<li>현재 겪고 있는 문제에 대해 해결하는 MVP를 구현하고 싶다.</li>
<li>기간이 한달이라는 짧은 기간이며, 이 시간을 온전히 개발에만 쏟는 것이 아니기에 너무 거대한 MVP를 다루기보단 작은 MVP를 구현을 완료하고 싶었다.</li>
</ol>
</blockquote>
<p>또한 주제를 정할 때 어떤 기술을 사용할 것인지, 어느 부분까지 구현할지 등을 구체적으로 정해보며 사용한 기술은 “왜” 사용했는지, 반대로 사용하지 않거나 배제한 구현 범위는 “왜” 그렇게 선택했는지를 명확하게 정해야 했다.</p>
<p>이런 식으로 정한 후 간단하게 주간 보고 발표를 해야했다. 이 때 팀장과 팀 이름을 정했어야 했다. 팀 이름은 Bello라고 정했다 ㅋㅋㅋ.</p>
<p>큰 이유는 없고 미니언즈들의 인사가 귀여웠기에 이를 채택하였다.<del>(우테코 크루 중 벨로라는 사람이 있었다는건 나중에 알게 된 사실이다…..)</del></p>
<p>팀장을 정해야하는데, 팀장은 매주 보고 발표를 해야했다. 하지만 나는 마이크를 많이 잡고 싶었다. 잘 들어주는 크루들이 있으며, 내가 발표를 연습할 수 있는 최적의 환경이라고 생각했기에 먼저 팀장을 하기로 나섰다. </p>
<h2 id="두-번째-주차">두 번째 주차</h2>
<p>두 번째 주차에서는 사용자 인터뷰 및 가설 검증을 해야했다.</p>
<p>우리가 정한 주제가 모두가 겪는 문제인지, 다른 사람들은 어떻게 생각하는지 다른 사람들에게 인터뷰를 하며 우리가 주제를 선정하며 정했던 가설들이 적절한가를 검증해보는 시간을 가졌다. 미션에서는 사용자 인터뷰까지가 해야할 일이였지만, 우리는 따로 설문지를 만들어 더 많은 자료를 얻고자 했다.</p>
<h2 id="세-번째-주차">세 번째 주차</h2>
<p>세 번째 주차에서는 개발 중간 발표가 핵심이었다.</p>
<p>두 번째 주차까지 우리의 주제와 가설, 사용자들의 반응에 대해 조사했으니 본격적으로 개발에 들어가는 것이다. 이번 미션에서 달랐던 점은 AI 사용을 적극적으로 해도 되는 것이다. 이전 미션에서는 AI 사용을 자제하며 미션을 진행해왔었기에 이 부분이 살짝 크게 다가왔다.</p>
<p>단, AI를 사용하되 “해줘”, “딸깍”으로 사용하는게 아니라 도우미? 정도로만 사용하도록 조건이 있었다. “해줘” 처럼 AI에게 모든 것을 맡기면 결국 내가 하는 것이 없다. 왜 이렇게 만들었는지도 모르게 된다.</p>
<p>“AI에게 다양한 정보나, 기술, 방향에 대한 조언을 얻되 최종 판단은 결국 내가 해야하는 것이다.” 라는 것을 배웠다. 우리 팀도 AI를 사용할 때 무조건 “해줘” 보다는 어떤 기술을 사용해야 하는지 후보를 받고 각 후보를 어떤 상황에서 사용하는게 좋은가? 반대로 어떤 상황에서 사용하는건 좋지 않은가? 등을 물어보는데 많이 활용했다.</p>
<p>또한 개발이 이뤄질 때 어떤 순서로 개발해야 순조롭게 개발을 진행할 수 있을지 물어볼 때도 많이 활용했다.</p>
<h2 id="네-번째-주차">네 번째 주차</h2>
<p>마지막 네 번째 주차에서는 최종 발표다. 지난 4주동안 해온 모든 것을 정리해서 발표하는 시간을 가졌다.</p>
<div align="center">
    <img src=https://velog.velcdn.com/images/noeyh_0j/post/8b765303-e53d-407c-b018-c02c2bc93847/image.jpeg
width=50%>
  간단한 앱 화면~
</div>



<h2 id="어떤-점이-가장-즐거웠는가">어떤 점이 가장 즐거웠는가</h2>
<p>이 중에서 나는 인터뷰를 통해 사용자의 의견을 들어보는 과정이 무척 즐거웠다. 우리의 주제가 모든 크루들이 겪고 있는 문제여서 그런 것도 있겠지만, 인터뷰를 한 크루들이 우리 주제에 대해서 정말 깊게 생각하며 피드백을 해주고, 더 있으면 좋을거 같은 점 등을 얘기해줬다.</p>
<p>이 경험을 통해 정말 사용자들이 필요로 하는 서비스를 만드는 것이 내가 원하는 것을 만드는 것보다 재밌구나! 라는 것을 많이 느낄 수 있었다. 비록 이 출석앱이 정말 채택되지는 못했지만 이런 경험을 했다는 것 자체가 너무 의미있다고 생각한다!!!</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<p>우리는 명확하게 MVP 범위를 설정해서 개발에 큰 난관이 없었다. 하지만 “NFC”라는 특정 기능에 시야가 좁게 잡혀있어, 다른 방향을 생각해보거나 밴치마킹을 생각해보지 못한게 아쉬웠다.</p>
<p>또한 우리가 이번 프로젝트에서 네트워크에 의존되 문제가 발생하는 것을 해결하기 위해 NFC라는 다른 방향을 생각하여 구현하고자 했지만, 알고보니 API 자체가 네트워크에 의존되어 있어서 출석 방법을 바꾸기로는 현재의 문제를 해결할 수 없었다는 것을 나중에 깨달았다. 문제의 원인에 대해 확실하게 알았다면 다른 방법을 생각해볼 수 있었을텐데… 라는 아쉬움도 함께 남았다.</p>
<p>이번 미니프로젝트를 경험삼아 다음 프로젝트에서는 문제에 대한 원인을 더 자세하게 알아보고, 다른 비슷한 서비스, 앱 등이 있는지도 파악해보며 다른 사람들은 비슷한 문제를 어떻게 해결했는가에 대해서 더 알아보면 좋을거 같다는 생각이 들었다.</p>
<p>우리 팀이였던 엘리, 엠버 모두 고생 많았어ㅓ어어어어ㅓㅓㅓㅓㅓㅓ 💯💯💯💯💯💯</p>
<h1 id="방과후">방과후</h1>
<p>우테코 안드로이드에는 자신이 공부한 것을 앞에서 공유하는 활동인 “방과후”라는 것이 있다. 레벨2를 시작하면서 제임스의 기획하에 이 방과후 활동을 안드로이드 뿐만 아니라 백엔드, 프론트엔드도 함께 해보는 자리를 만들고자 했다.</p>
<p>하지만 생각보다 다른 크루는 우리에게 관심이 없었다 ㅋㅋㅋㅋ. 결국 조금 더 큰 공간에서 우리끼리 하는 것처럼 방과후를 하는 시간을 가졌다. 나는 총 두 번의 방과후를 도전했다. </p>
<h2 id="첫-번째-방과후">첫 번째 방과후</h2>
<p>“<a href="https://velog.io/@noeyh_0j/kotlin-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EA%B0%80%EC%B9%98%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">불변 객체의 가치</a>” 라는 주제를 가지고 방과후를 진행하였다. 이 주제를 정한 이유는 레벨1을 진행하면서 불변 객체와 가변 객체의 존재를 알게 되었고, 불변 객체를 선호하는 이유에 대한 궁금증이 생겼었다. 이를 알아보고 싶었기에 다음과 같은 주제를 결정하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/700cab99-b804-4e7e-b83b-3674df4d7eb4/image.png" alt=""></p>
<h2 id="두-번째-방과후">두 번째 방과후</h2>
<p>두 번째 방과후는 “<a href="https://velog.io/@noeyh_0j/%EB%B0%A9%EA%B3%BC%ED%9B%84-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EC%95%88%EC%A0%84%ED%95%9C-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC">코틀린 코루틴 환경에서의 동시성 문제</a>”라는 주제를 선정하였다. 코루틴은 비동기이다보니 동기 코드처럼 작성했을 때 동시성 문제라는게 발생할 수 있다. 간단하게 설명하면 여러 스레드가 동시에 접근하고 연산하다보니 계산 결과나 과정이 중첩되어 없어지게 되는 문제인 것이다. 코루틴 수업 이후에 사무엘이 이 문제가 왜 생기는거지? 라는 질문을 툭 던졌었다. 나는 이 때 분명 책을 읽고 공부를 했기에 알고는 있었지만, 사무엘이 이해할 수준으로 설명하는 것이 힘들었다.</p>
<p>이 때 아직 내가 설명하기엔 공부가 부족하구나 라는 것을 느끼게 되어, 더 학습하는 것을 목표로 이 주제를 선정하게 된 것이다.</p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/05b9188d-49b3-460b-a295-f2e295cd01ee/image.png" alt=""></p>
<h2 id="후기">후기</h2>
<p>방과후를 한, 두번 정도는 더 해보고자 했지만 그러지 못했던게 살짝 아쉽다. 욕심 좀 내서 매주 발표해야지!! 라는 큰 포부를 가졌지만, 생각보다 쉽지 않았다…. 그래도 이렇게 다른 사람들 앞에서 발표를 많이 해보고자 한 스스로가 기특하다! 덕분에 발표가 무서운게 아니라 즐거운거구나를 크게 느꼈다.</p>
<h1 id="지킴이-수업">지킴이 수업</h1>
<p>레벨2 지킴이로 지원했기에 지킴이 수업을 듣는 특권(필수로 2개 이상 들어야함)을 가질 수 있었다. 지킴이는 우리 팀 내에서 심리적 안정감을 위해 노력하는 역할을 맡는 것이다. 이전 기수에서는 이끔이라고 불렸던 것 같다.</p>
<p>많은 수업이 있었지만, 나는 정서 리딩과 옳은 실패라는 두 가지 수업을 들었다. 늦게 수강신청을 해서 다른 과목이 자리가 없던 영향도 있지만….. 이 두 개가 가장 흥미있었다. 우테코를 하다보면 자연스럽게 사람들과 대화를 많이하고~ 공감도 하고~ 눈치를 볼 때도 있고~ 그렇다. 이 때 불가피하게 정서 리딩이라는 것을 하게 되는거 같은데, 어떻게 하면 더 잘할 수 있을까에 대한 의문이 들어 이 수업이 끌렸던 것 같다. </p>
<p>마찬가지로 옳은 실패라는 것도 제목이 무척 흥미로웠다. 실패인데 옳은 실패가 있나? 실패는 실패지! 라는 생각도 있었다. 하지만 성공한 사람들의 얘기를 들어보면 전부 자신의 실패에 대한 얘기를 한다. 그러면 실패가 뭘까? 옳은 실패라는게 있는걸까? 라는 생각을 하며 듣게 된 것 같다.</p>
<h1 id="실패-컨퍼런스">실패 컨퍼런스</h1>
<p>레벨2가 끝나기 전에 지킴이들끼리 모여 자신의 실패에 대해 얘기하는 시간을 가져보는 실패 컨퍼런스라는게 열렸다. 우리 조는 테리, 고래, 도우너, 라이, 아티로 나를 포함해 6명이었다. 이 시간은 각자의 실패를 서로 공유하는 시간이었다.</p>
<p>팀원 모두가 얘기를 잘 들어주고, 따뜻한 말을 많이 해줘서 무척 기분이 좋았다! 이 때 회고의 실패에 대한 얘기도 나왔었는데, 여기서 회고 모임을 만들면 어떨까? 라는 말이 나와 “야르한 회고 모임”이 탄생하였다.</p>
<p>아직 구체적인 활동은 정해지지 않았지만 주간, 월간 목표를 세우가 각 주마다 주간 회고를 함께 해보는 그런 자리를 만들어보고자 생긴 모임이다. 아마 레벨3부터 시작하지 않을까? 생각한다.</p>
<h1 id="마무리">마무리</h1>
<p>오랜만에 써보는 회고(일기)다보니 적어놓고 싶은 말이 많았다. 레벨1은 적응하는데 집중을 했다고 생각하면, 레벨2는 내가 어떻게 성장할 수 있을까에 대한 고민이 더 많았던 시기같았다. 무엇을 공부해야하고, 어떻게 공부하고, 어떻게 팀원과 소통해야 할지, 의견이 맞지 않으면 어떻게 내 의견을 전달해야 기분이 나쁘지 않게 전달될까?, 내가 어떻게 하면 발전할 수 있을까? 등 어떻게 나아가야할지에 대한 고민이 많았다.</p>
<p>레벨2를 진행할 때는 정신없이 달리다보니 내 성장에 대해 생각할 틈이 없었지만, 지금와서 돌아보면 레벨1 때보다 기술적으로도 아는게 많아졌지만, 그 이상으로 마음가짐이나 생각의 깊이도 크게 성장했다고 생각한다.</p>
<p>레벨3가 시작되면 반드시 일기를 적어봐야겠다! 레벨2 회고를 작성하는데도 머리가 지끈지끈했다....</p>
<p>레벨2 마지막 수업을 할 때 KPT를 사용해서 회고를 했다. 레벨 0때도 KPT를 사용해 회고했던게 새록새록 기억이 났다. 그래서 이번 회고 글도 오랜만에 KPT를 적어보며 마무리해보겠다.</p>
<h2 id="keep">Keep</h2>
<ul>
<li>학습 뿐만 아니라, 팀원과의 의견 소통에서도 “왜?”라는 질문을 던지기</li>
<li>부끄러워하지 말고 하고자, 하고싶은 일에 대해서는 적극적으로 참여하기</li>
</ul>
<h2 id="problem">Problem</h2>
<ul>
<li>방과후 수업을 많이 하지 못해 아쉬움</li>
<li>학습 범위보다 과식한 부분</li>
</ul>
<h2 id="try">Try</h2>
<ul>
<li>매일 또는 매주 한 일 적어놓기<del>(회고 정리할 때 머리가 무척 아팠다…)</del></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] equals와 hashCode의 관계, 그리고 해시 충돌까지...]]></title>
            <link>https://velog.io/@noeyh_0j/kotlin-equals%EC%99%80-hashCode%EC%9D%98-%EA%B4%80%EA%B3%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8C%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@noeyh_0j/kotlin-equals%EC%99%80-hashCode%EC%9D%98-%EA%B4%80%EA%B3%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8C%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Wed, 10 Jun 2026 15:02:10 GMT</pubDate>
            <description><![CDATA[<h1 id="왜-이-주제를-공부하게-되었는가">왜 이 주제를 공부하게 되었는가?</h1>
<p>상태에 해당하는 객체를 만들기 위해 <code>data class</code>를 주로 사용하지만, 이 내부의 구현 함수들에 대해서는 깊게 알아보지 않았다. 내부에서 생성해주는 함수들을 알지 못해도, data class를 사용하면 <code>동등성 비교를 쉽게 할 수 있다!</code>, <code>toString()을 보기 좋게 출력할 수 있다!</code> 처럼 단순하게 생각하고 사용했다.</p>
<p>Kotlin in Action을 읽다보니 equals를 구현할 때는 항상 hashCode도 함께 구현해야한다는 규칙을 알게 되었는데, <code>왜?</code> 같이 구현해야하는지 의문이 생겼다. 그래서 이번 기회에 내부 함수에 대해서 알아보고자 했다. 그 중에 equals와 hashCode의 관계를 깊게 알아보고자 하였다.</p>
<p>hashCode()를 사용해본 적이 없다보니 더더욱 의문이 생긴 것 같다.</p>
<h1 id="equals의-동작원리">equals의 동작원리</h1>
<p>equals는 두 객체의 <code>동등성</code>을 비교한다. 즉, 값만을 비교하는 것이다.</p>
<pre><code class="language-kotlin">class Product(
    val name: String,
    val address: String
)

fun main() {
    val product1 = Product(&quot;건담&quot;, &quot;안성&quot;)
    val product2 = Product(&quot;건담&quot;, &quot;안성&quot;)

    println(product1 == product2) // false
}</code></pre>
<p>하지만 아래의 출력문은 false를 반환한다. equals를 오버라이딩하지 않았다면, 동등성 비교가 아닌 참조 비교를 한다. </p>
<p>왜냐하면 자바에서 equals는 동등성 비교(값 비교)가 아닌 참조 비교(주소 비교)다. 마찬가지로 코틀린의 == 도 내부에서 equals를 호출하여 객체를 비교한다. 그렇기에 두 값이 같아 보이더라도 == 또는 equals를 사용해서 비교하면 두 값이 다르다고 나타낸다. 하지만 오버라이딩하여 구현한다면, 두 객체의 값을 비교하여 참이 되도록 구현할 수 있다.</p>
<pre><code class="language-kotlin">class Product(
    val name: String,
    val address: String
) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Product)
            return false
        return name == other.name &amp;&amp; address == other.address
    }
}

fun main() {
    val product1 = Product(&quot;건담&quot;, &quot;안성&quot;)
    val product2 = Product(&quot;건담&quot;, &quot;안성&quot;)

    println(product1 == product2) // true
}</code></pre>
<h1 id="hashcode의-동작-원리">hashCode()의 동작 원리</h1>
<p>equals를 구현했음에도 hashSet에 넣고 사용할 때는 두 값을 다르다고 판단한다.</p>
<pre><code class="language-kotlin">class Product(
    val name: String,
    val address: String
) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Product)
            return false
        return name == other.name &amp;&amp; address == other.address
    }
}

fun main() {
    val product1 = Product(&quot;건담&quot;, &quot;안성&quot;)
    val product2 = Product(&quot;건담&quot;, &quot;안성&quot;)

    val processed = hashSetOf(product1)
    println(processed.contains(product2)) // false
}</code></pre>
<p>분명 두 객체의 <code>동등성 비교는 참</code>인데 hashSet안에는 들어가있지 않는다고 결과가 나타난다…</p>
<p>이는 객체를 생성할 때마다 hashcode가 다르게 생성되기 때문인데, equals와 마찬가지로 hashCode()를 오버라이딩해서 구현한다면, hash 값을 같게 만들 수 있다.</p>
<pre><code class="language-kotlin">class Product(
    val name: String,
    val address: String
) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Product)
            return false
        return name == other.name &amp;&amp; address == other.address
    }

    override fun hashCode(): Int = name.hashCode() * 31 + address.hashCode()
}

fun main() {
    val product1 = Product(&quot;건담&quot;, &quot;안성&quot;)
    val product2 = Product(&quot;건담&quot;, &quot;안성&quot;)

    val processed = hashSetOf(Product(&quot;건담&quot;, &quot;안성&quot;))
    println(processed.contains(Product(&quot;건담&quot;, &quot;안성&quot;))) // true
}</code></pre>
<h1 id="어-근데-equals와-hashcode는-함께-구현하라는-규칙이-있다">어? 근데 equals와 hashCode는 함께 구현하라는 규칙이 있다?</h1>
<p>내가 생각했을 때는 equals와 hashCode의 역할이 다르다. <code>equals</code>는 두 객체의 <code>동등성을 비교</code>하기 위함이고, <code>hashCode</code>는 객체가 가진 <code>고유의 값?을 설정</code>하기 위함이라고 생각한다. 또한 두 객체의 동등성을 비교할 때 hashCode를 사용하지도 않는다… 마찬가지로 hashCode를 구현할 때 동등성을 비교하지도 않는다….</p>
<p>그렇다면 왜? equals와 hashCode를 함께 구현하라는 규칙이 생긴걸까??</p>
<h1 id="해시-기반-컬렉션은-어떻게-동작하는가">해시 기반 컬렉션은 어떻게 동작하는가?</h1>
<p>hashSet이나 hashMap이 어떤 객체가 들어있는지 찾을 때는 아래와 같은 순서로 검사한다.</p>
<ol>
<li>hashCode() 호출 : 객체의 해시 코드를 구해서 몇 번 상자에 들어있는지 확인하고 그 상자로 순간 이동한다.</li>
<li>equals() 호출 : 그 상자 안에 들어있는 객체들과 하나씩 비교하면 진짜 내용물이 똑같은 것인지 최종 확인한다.</li>
</ol>
<p>만약 <code>hashCode만 구현</code>했거나 <code>equals만 구현</code>했다면, hashSet에서 객체를 찾는 동작은 항상 false가 나타날 것이다.</p>
<p>그렇기에 아래와 같은 규칙이 생긴 것이다.</p>
<blockquote>
<p>두 객체의 eqauls() 결과가 같다면, 두 객체의 hashCode() 값은 무조건 같아야 한다.</p>
</blockquote>
<p>하지만 반대로 해시 값이 같다고 해서 equals가 무조건 같을 필요는 없다.</p>
<p>즉, hashCode 함수를 구현할 때는 equals를 사용하지 않지만, 이 함수를 실행할 때는 equals를 사용해 객체가 맞는지 <code>최종 검증</code>을 한다. 그렇기에 eqauls가 참이라면 hashCode도 참이 되도록 구현해야 한다.</p>
<p>하지만…이 <code>해시 값마저 중복</code>된다면 어떻게 될까? 바로 <code>해시 충돌</code>이 발생하게 된다.</p>
<h1 id="해시-충돌이-무엇인가">해시 충돌이 무엇인가?</h1>
<p>코틀린에서 hashCode() 함수가 반환하는 값은 <code>Int 타입</code>이다. Int 타입이 표현할 수 있는 숫자의 개수는 42억 개라는 개수로 한정되어 있다. 그렇다는 것은 억지로 넣다보면 결국 <code>중복</code>되는 해시코드가 생길 수도 있다는 것이다…</p>
<p>이를 수학에서는 <code>비둘기집 원리</code>라고 부른다. (비둘기가 10마리인데 집이 9개면, 한 집에는 무조건 2마리가 들어가야 한다!)</p>
<h1 id="해시-테이블의-내부-구조">해시 테이블의 내부 구조</h1>
<p>HashMap이나 HashSet 내부에는 데이터를 저장하기 위한 고정된 크기의 배열(Array)가 선언되어 있다. 이를 <code>버킷 배열</code> 또는 <code>테이블</code>이라고 부른다.</p>
<p>코틀린/자바의 HashMap을 만들면 내부 크기가 <code>기본으로 16인 배열</code>이 생성된다.</p>
<pre><code class="language-kotlin">public class HashMap&lt;K, V&gt; extends AbstractMap&lt;K, V&gt; implements Map&lt;K, V&gt;, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    static final int DEFAULT_INITIAL_CAPACITY = 16;</code></pre>
<p>하지만 hashCode()가 반환할 수 있는 숫자는 42억 개다. 반환될 수 있는 숫자에 비해 배열의 크기가 상당히 작은데, 어떻게 배열에 넣는 것일까?</p>
<p>배열에 넣기 위해 해시코드를 그대로 사용하는 것이 아닌 Modulo 연산을 사용하여 배열의 인덱스 범위 안으로 압축한다.</p>
<blockquote>
<p>인덱스 계산 식: index = hashCode() % 배열의 크기(16)</p>
</blockquote>
<p>이 연산을 통해 다른 해시코드가 나왔더라도, 나머지 값이 같으면 배열의 같은 인덱스를 배정받게 되는 ‘해시 충돌(hash collsion)’이 매우 빈번하게 발생하게 된다.</p>
<h1 id="그러면-해시-충돌이-일어났을-때-어떻게-해결할까">그러면 해시 충돌이 일어났을 때 어떻게 해결할까?</h1>
<p>HashMap 또는 HashSet은 두 데이터가 같은 상자 번호를 들고 왔을 때, 번호가 중복되었다고 밀어내지 않는다. 대신 <code>체이닝</code>이라는 방식을 사용한다.</p>
<p>체이닝 기법의 핵심은 배열의 각 인덱스에 데이터 객체를 바로 넣는 것이 아니라, 연결 리스트의 <code>첫 번째 노드의 주소</code>값을 넣는 것이다.</p>
<p>내부적으로 해시 테이블에 저장되는 데이터는 노드 객체다. Node 객체는 아래와 같이 생겼다.</p>
<pre><code class="language-kotlin">static class Node&lt;K, V&gt; implements Map.Entry&lt;K, V&gt; {
        final int hash;
        final K key;
        V value;
        Node&lt;K, V&gt; next;

        Node(int var1, K var2, V var3, Node&lt;K, V&gt; var4) {
            this.hash = var1;
            this.key = var2;
            this.value = var3;
            this.next = var4;
        }
}</code></pre>
<p>이 방식을 활용해 객체가 배열의 빈 인덱스에 들어왔다면 새로운 Node를 생성하고, 배열의 해당 인덱스에 이 노드의 주소값을 할당한다. 만약 이미 다른 노드가 인덱스에 존재한다면 equals를 사용해 값을 비교한다. 그치만 서로 값이 다르다면, 기존 노드의 맨 뒤에 새로운 노드를 연결하는 방식이다.</p>
<h1 id="그러면-데이터-조회는-어떻게-하는거야">그러면 데이터 조회는 어떻게 하는거야?</h1>
<p>데이터 조회는 아래와 같은 순서로 적용된다.</p>
<ol>
<li><code>hashMap.get(key)</code>를 호출하면, 입력받은 <code>key</code>의 해시코드로 인덱스를 먼저 계산한다.</li>
<li>계산된 인덱스로 배열에 다이렉트 접근한다. 여기까지는 <code>O(1)</code>의 시간복잡도를 갖는다.</li>
<li><strong>만약 해당 인덱스(상자)가 텅 비어있다면? 찾는 데이터가 없다는 뜻이므로 즉시 <code>null</code>을 반환하고 종료한다.</strong></li>
<li>만약 인덱스에 노드가 존재한다면, 내가 찾는 <code>key</code>와 그 노드의 <code>key</code>가 같은지 <code>equals()</code>로 동등성을 비교한다.</li>
<li>만약 겹쳐진 다른 노드라면 <code>next</code> 포인터를 타고 다음 노드로 이동하며 <code>equals()</code> 비교를 반복한다. 이 연결 리스트를 탐색하는 과정은 최악의 경우 <code>O(N)</code>의 시간복잡도를 갖는다.</li>
</ol>
<h1 id="이렇게-동작한다-시각-자료">이렇게 동작한다~ 시각 자료</h1>
<p><a href="https://gemini.google.com/share/4ae69478d22b">https://gemini.google.com/share/4ae69478d22b</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] 수신 객체 지정 람다의 철학은?]]></title>
            <link>https://velog.io/@noeyh_0j/kotlin-%EC%88%98%EC%8B%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EC%A0%95-%EB%9E%8C%EB%8B%A4%EC%9D%98-%EC%B2%A0%ED%95%99%EC%9D%80</link>
            <guid>https://velog.io/@noeyh_0j/kotlin-%EC%88%98%EC%8B%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EC%A0%95-%EB%9E%8C%EB%8B%A4%EC%9D%98-%EC%B2%A0%ED%95%99%EC%9D%80</guid>
            <pubDate>Sun, 07 Jun 2026 05:39:52 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>코드를 뜯고 싶고 맛보고 즐기는 개발자가 되고자 하는 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 수신 객체 지정 람다의 철학에 대해 알아보고자 합니다.</p>
<h1 id="왜-이런-고민을-하게-되었는가">왜 이런 고민을 하게 되었는가?</h1>
<p>withContext를 뜯어보는 중에 문뜩 일반 람다가 아닌 CoroutineScope.() → T를 사용한 것을 확인했다. </p>
<p>일반 람다를 넣어도 될텐데, 굳이 수신 객체 지정 람다를 파라미터로 받는 이유가 있을까?</p>
<p>예) ColumnScope.() → Unit, CoroutineScope.() → T 등</p>
<h1 id="일반-람다와-수신-객체-지정-람다의-차이는">일반 람다와 수신 객체 지정 람다의 차이는?</h1>
<p>일반 람다를 사용했을 경우 객체를 참조하기 위해 it을 사용해야 한다. 이는 객체의 프로퍼티를 사용하기 위해 계속 it을 참조해서 사용해야한다는 불편함을 겪어야 한다.</p>
<pre><code class="language-kotlin">Column { scope -&gt; // 매번 scope나 it을 받아야 함
    Text(&quot;안녕하세요&quot;)

    // Column 안에서만 쓸 수 있는 align이나 weight를 쓸 때마다
    // scope를 억지로 끌고 와야 함!
    Button(modifier = Modifier.then(scope.align(Alignment.CenterHorizontally))) {
        Text(&quot;버튼&quot;)
    }
}</code></pre>
<p>반면 수신 객체 지정 람다를 사용했을 때 람다의 내부에서 this 키워드를 사용할 수 있다. 확장 함수를 사용했을 때는 객체 스스로를 대입하기 때문에 객체를 this로 사용할 수 있다. 그러면 this를 생략할 수 있어 코드가 더 간결해지는 이점을 얻을 수 있다.</p>
<pre><code class="language-kotlin">Column { // 이 중괄호 안의 this는 이미 ColumnScope!
    Text(&quot;안녕하세요&quot;)

    // this가 생략되어 있으므로, 내 방 안(ColumnScope)에 있는 
    // align 기능을 아무런 접두사 없이 아주 자연스럽게 가져다 씀!
    Button(modifier = Modifier.align(Alignment.CenterHorizontally)) {
        Text(&quot;버튼&quot;)
    }
}</code></pre>
<p>즉, 수신 객체 지정 람다를 사용했을 때 코드가 더 간결해진다는 이점이 있고, 이는 확장 함수와 관련이 있다.</p>
<p>또한 일반 람다를 사용했을 때는 함수에 인자를 넣기 위해 중괄호 안에 계속해서 it을 참조해줘야 한다는 불편함이 있다. 반면 수신 객체는 스스로 객체를 넣어주기 때문에 this를 사용하여 개발자가 일일이 지정해주지 않아도 된다는 편리함이 있다.</p>
<h1 id="그러면-함수-작성할-때는-왜-수신-객체-지정-람다를-사용하지-않아">그러면 함수 작성할 때는 왜? 수신 객체 지정 람다를 사용하지 않아??</h1>
<p>이런 생각이 든 이유는 위에서 말했듯이 코드가 간결해진다는 이점이 있는데, 왜 코틀린 표준 라이브러리의 모든 함수에서 왜 이걸 안쓸까? 라는 의문이 들었다. </p>
<p>수신 객체 지정 람다를 사용하면 코드를 간결하게 만들어주는 것은 사실이다. 하지만 무분별하게 사용할 경우 this 참조가 충돌하는 스코프 오염이 발생하고, 코드의 출처가 불분명해진다는 단점이 존재한다.</p>
<blockquote>
<p><strong>스코프 오염</strong>이란?
내가 현재 있는 블록 안에 접근할 수 있는 변수나 함수의 이름이 너무 많고 중복되어서, 컴퓨터나 개발자가 이름이 누구의 것인지 헷갈리게 되는 현상을 말한다.</p>
</blockquote>
<pre><code class="language-kotlin">class ShoppingViewModel {
        val title = &quot;장바구니 화면&quot;

        fun updateProduct(product: Product) {
                product.apply {
                        println(title)
                }
        }
}

class Product {
        val title = &quot;멋진 운동화&quot;
}</code></pre>
<p>이 코드를 봤을 때 개발자는(나 포함) viewModel 안의 title이 들어갈지, Product 객체의 title이 들어갈지 헷갈릴 수 있다.</p>
<p> ShoppingViewModel 안에 title을 지정해놧으니 updateProduct를 실행했을 때 viewModel에 있는 title이 들어간다고 예상할 수 있다. 하지만 Product 객체를 넣었을 경우 viewModel 안에 초기화한 title이 아닌 객체의 title이 들어가게 된다.</p>
<p>스코프 오염을 막기 위해서는 다음과 같은 방법들이 있다.</p>
<ol>
<li><p>일반 람다를 사용하여 스코프를 분리하는 방법이 있다. 예를 들면 let, also를 사용하는 것이다.</p>
</li>
<li><p>안쪽의 객체를 사용할 경우 this를 명시해주는 방법이 있다.</p>
</li>
<li><p>DSL 환경에서는 @DslMarker을 사용하면 된다.</p>
<blockquote>
<p><strong>@DslMarker</strong> 는 뭘까?
 이 어노테이션을 붙이면 내부 스코프에서 외부 스코프의 함수를 몰래 호출하지 못하도록 컴파일 단계에서 차단할 수 있다. 예를 들면 Jetpack Compose나 복잡한 UI 트리를 만들 때처럼 불가피하게 스코프가 중첩될 때 사용하여 스코프가 오염되는 것을 막을 수 있다.</p>
</blockquote>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] 커스텀 접근자(get, set)를 어떤 상황에서 사용할까?]]></title>
            <link>https://velog.io/@noeyh_0j/kotlin-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%A0%91%EA%B7%BC%EC%9E%90get-set%EB%A5%BC-%EC%96%B4%EB%96%A4-%EC%83%81%ED%99%A9%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@noeyh_0j/kotlin-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%A0%91%EA%B7%BC%EC%9E%90get-set%EB%A5%BC-%EC%96%B4%EB%96%A4-%EC%83%81%ED%99%A9%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 30 May 2026 16:28:28 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>코틀린의 프로퍼티에 대한 관심을 가지고 공부하는 개발자 꿈나무 김조현입니다.</p>
<p>코틀린에서 변수명과 등호를 사용해 값을 읽고 수정할 수 있는데, 어떤 상황에서 커스텀 get()과 set()을 사용해야하는지에 대해 정리해보겠습니다.</p>
<h2 id="get이라는-키워드를-언제-왜-쓰는걸까">get()이라는 키워드를 언제 왜 쓰는걸까?</h2>
<p>변수명 호출은 백킹 필드에 존재하는 데이터를 불러오는 방식이다. 반면 get() 호출을 사용한다면 값을 메모리에 저장하지 않고 값을 호출할 수 있다.</p>
<blockquote>
<p>그러면 어떤 상황에서 쓰이는걸까? </p>
</blockquote>
<h3 id="1-여러-상태를-조합해-파생-상태를-만들-때">1. 여러 상태를 조합해 파생 상태를 만들 때</h3>
<pre><code class="language-kotlin">class BoardState(var todoTasks: Int = 5, var doneTasks: Int = 2) {
    var totalTasks: Int = todoTasks + doneTasks
}

fun main() {
    val board = BoardState()
    board.todoTasks = 10

}</code></pre>
<p>위와 같은 상황에서 todoTasks의 값은 변경되었지만, 이를 기반으로 한 totalTasks라는 상태는 갱신되지 않는다. 그렇기에 위 상황에서 board.totalTasks를 호출했을 때 12를 기대했겠지만 이전 값인 7이 호출된다.</p>
<pre><code class="language-kotlin">class BoardState(var todoTasks: Int = 5, var doneTasks: Int = 2) {
    var totalTasks: Int
        get() = todoTasks + doneTasks
}

fun main() {
    val board = BoardState()
    board.todoTasks = 10    
}</code></pre>
<p>이런 문제를 get()을 사용했을 때 해결할 수 있다. todoTasks나 doneTasks가 변해도 totalTasks를 호출했을 때는 항상 다시 계산하여 최신 값을 불러온다.</p>
<h3 id="2-가변-데이터를-불변으로-안전하게-숨길-때">2. 가변 데이터를 불변으로 안전하게 숨길 때</h3>
<pre><code class="language-kotlin">class TaskRepository {
    private val _tasks = mutableListOf&lt;String&gt;(&quot;디자인&quot;, &quot;개발&quot;)

    val tasks: List&lt;String&gt;
        get() = _tasks.toList()
}</code></pre>
<p>내부에서는 가변으로 값을 변경할 수 있지만 외부에서 호출할 때는 불변으로 변경하여 값을 수정하지 못하게 하는 방식으로 사용할 수 있다.</p>
<h3 id="3-시간이-지남에-따라-계속-값이-변해야-할-때">3. 시간이 지남에 따라 계속 값이 변해야 할 때</h3>
<pre><code class="language-kotlin">class TimeManager {
    val currentTimeMillis: Long
        get() = System.currentTimeMillis()
}</code></pre>
<p>즉, 변수명 호출은 메모리에 저장된 값을 불러오는 것이며, get() 호출은 새롭게 계산한 값을 불러오는 것이다.</p>
<h2 id="var-키워드-변수를-초기화할때는-등호를-사용해서-초기화할-수-있는데-set을-사용하는-이유가-있을까">var 키워드 변수를 초기화할때는 등호(=)를 사용해서 초기화할 수 있는데 set()을 사용하는 이유가 있을까?</h2>
<p>밖에서 등호(=)를 사용해 값을 바꾸는 것은 field에 값을 저장하는 것이다. set()과 등호는 같은 작업인 것이다. 하지만 아래와 같이 사용하여 커스텀 set()을 사용했을 때 이점을 얻을 수 있다.</p>
<h3 id="1-이상한-값이-들어오는-것을-방어할-수-있다">1. 이상한 값이 들어오는 것을 방어할 수 있다.</h3>
<pre><code class="language-kotlin">var age: Int = 0
    get() = field
    set(value) {
        if (value &gt; 0) {
            field = value
        } else {
            println(&quot;0 미만 값은 들어갈 수 없습니다.&quot;)
        }
    }</code></pre>
<h3 id="2-값이-바뀔-때-연쇄-반응을-일으키기-위해-사용할-수-있다">2. 값이 바뀔 때 연쇄 반응을 일으키기 위해 사용할 수 있다.</h3>
<pre><code class="language-kotlin">var userStatus: String = &quot;오프라인&quot;
    set(value) {
        field = value
        println(&quot;서버에 사용자가 $value 상태로 변경되었습니다.&quot;)
    }</code></pre>
<h3 id="3-들어온-값을-내-마음대로-가공해서-저장하기-위해서-사용할-수-있다">3. 들어온 값을 내 마음대로 가공해서 저장하기 위해서 사용할 수 있다.</h3>
<pre><code class="language-kotlin">var searchKeyword: String = &quot;&quot;
    set(value) {
        field = value.trim()
    }</code></pre>
<p>즉, set()의 동작은 등호(=)와 같지만 커스텀 set() 을 활용해서 값을 저장하는 방식을 내 마음대로 변경할 수 있다.</p>
<h1 id="마무리하기">마무리하기</h1>
<p>이렇게 어떤 상황에서 커스텀 접근자를 사용하면 좋은지에 대해 알아보고 정리해봤습니다.</p>
<p>긴듯 짧은듯한 글을 읽어주셔서 감사합니다 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin/coroutine] 코틀린 코루틴 환경에서의 동시성 문제와 안전한 상태관리]]></title>
            <link>https://velog.io/@noeyh_0j/%EB%B0%A9%EA%B3%BC%ED%9B%84-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EC%95%88%EC%A0%84%ED%95%9C-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@noeyh_0j/%EB%B0%A9%EA%B3%BC%ED%9B%84-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EC%95%88%EC%A0%84%ED%95%9C-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Tue, 26 May 2026 06:23:23 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>취업 시장에서 경쟁 중인 개발자 꿈나무 김조현입니다. </p>
<p>이번 글에서는 경쟁 상태에 대해 공부하면서 발표를 준비했던 내용을 정리하고자 합니다.</p>
<h2 id="수업시간에-다룬-내용">수업시간에 다룬 내용</h2>
<h3 id="경쟁-상태">경쟁 상태</h3>
<p>여러 스레드가 같은 가변 상태에 동시 접근하면 동기화 문제가 생긴다.</p>
<pre><code class="language-kotlin">var i = 0

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) { i++ }
    }
    delay(1000)
    println(i) // 10000이 아닐 수 있다
}</code></pre>
<p><strong><code>Mutex</code></strong>, <strong><code>AtomicInteger</code></strong>, 또는 단일 스레드 디스패처(<strong><code>Default.limitedParallelism(1)</code></strong>)로 해결한다.</p>
<hr>
<h1 id="예제-1">예제 1</h1>
<pre><code class="language-kotlin">var i = 0

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) { i++ }
    }
    delay(1000)
    println(i) // 10000이 아닐 수 있다
}</code></pre>
<p>이 코드의 실행 결과는 항상 다르다. 왜? 이 때 가변 객체에 접근하는 코루틴은 병렬로 처리된다. 즉, 여러 10000번의 코루틴을 실행하며 각각의 코루틴이 count라는 변수에 접근한다.</p>
<p>이 코드의 결과는 실행할 떄마다 다르게 나타난다.</p>
<h2 id="왜-그럴까">왜 그럴까?</h2>
<ol>
<li>메모리 가시성 문제</li>
<li>경쟁 상태 문제</li>
</ol>
<h1 id="메모리-가시성-문제">메모리 가시성 문제</h1>
<ul>
<li>스레드가 변수를 읽는 메모리 공간에 관한 문제로 CPU 캐시와 메인 메모리 등으로 이뤄지는 하드웨어의 메모리 구조와 연관돼 있다.</li>
<li>스레드가 변수를 변경시킬 때 메인 메모리가 아닌 CPU 캐시를 사용할 경우 CPU 캐시와 메인 메모리 간에 데이터 불일치가 생기기 때문이다.</li>
<li>예) i 변수의 값을 1000에서 1001로 변경시켰는데 변경이 CPU 캐시에만 반영되고 메모리로 전파되지 않았다면, 다른 스레드가 count에 접근했을 때 메인 메모리에서 접근을 하기 때문에 반영되지 않은 1000에 접근하게 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/be0da6c3-66f3-42cb-845e-1a1d3d71893c/image.png" alt=""></p>
<h1 id="경쟁-상태-1">경쟁 상태</h1>
<ul>
<li>여러개의 스레드가 동시에 값을 읽고 업데이트 시키면 같은 연산이 두 번 일어난다.</li>
<li>예) 두 개의 스레드가 1000에 접근해서 1을 증가시키는 연산을 동시에 실행했다. 그러면 1001이 되는 연산이 두 번 일어났기 때문에 한 번의 연산이 손실된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/37b0dfcf-88ef-4194-a159-1fcf3d3089b7/image.png" alt=""></p>
<h1 id="공유-상태에-대한-메모리-가시성-문제-해결-방법">공유 상태에 대한 메모리 가시성 문제 해결 방법</h1>
<p>공유 상태에 대한 메모리 가시성 문제란 하나의 스레드가 다른 스레드가 변경된 상태를 확인하지 못하는 것으로 서로 다른 CPU에서 실행되는 스레드들에서 공유 상태를 조회하고 업데이트할 떄 생기는 문제다.</p>
<ul>
<li>메인 메모리에는 count = 1000이라는 값이 존재한다.</li>
<li>스레드가 count 값을 증가시키는 연산을 하기 위해 메인 메모리에서 count 값을 읽어온 후, 캐시 메모리에 count = 1000을 저장한다. 스레드는 캐시 메모리의 count를 사용해 증가 연산을 실행한다.</li>
<li>연산이 완료된 후에는 1001이 되지만 이 값을 메인 메모리가 아닌 캐시 메모리에 쓴다. 즉, 캐시 메모리에 저장된 count 값은 플러시가 일어나지 않으면 메인 메모리로 전파되지 않는다.</li>
<li>스레드가 전파되지 않은 상황에서 다른 스레드가 count 증가 연산을 실행하기 위해 메인 메모리에 접근했을 때는 count가 반영이 되지 않은 상태이기에 1000으로 인식된다.</li>
<li>다른 스레드도 마찬가지의 위와 같은 과정이 반복된다.</li>
</ul>
<p>즉, 하나의 스레드에서 변경한 변수의 상태 값을 다른 스레드가 알지 못해 생기는 메모리 동기화 문제를 메모리 가시성 문제라고 한다.</p>
<h2 id="volatile-어노테이션-활용하기">@Volatile 어노테이션 활용하기</h2>
<pre><code class="language-kotlin">@Volatile
var i = 0

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) { i++ }
    }
    delay(1000)
    println(i) // 10000이 아닐 수 있다
}</code></pre>
<p>이 어노테이션을 사용하면 메모리 가시성 문제를 해결할 수 있다. 이 말은 무엇이냐? 캐시 메모리가 아닌 메인 메모리를 바라보게 해서 직접 읽고 쓰도록 하는 것이다.</p>
<p>어? 그러면 해결됐다. 하고 실행해보면 결과는 10000이 아닌 난수가 나오게 된다…</p>
<p>메모리 가시성 문제를 해결했지만, 메인 메모리의 count 변수에 동시에 접근하는 문제에 대해서는 해결하지 못했기 때문에 10000이라는 값을 보장할 수 없는 것이다. 이 문제는 공유 상태에 대한 문제다.</p>
<h3 id="그러면-언제-volatile-어노테이션을-사용해">그러면 언제 Volatile 어노테이션을 사용해?</h3>
<p>몰라….</p>
<h1 id="공유-상태에-대한-경쟁-상태-문제의-해결-방법">공유 상태에 대한 경쟁 상태 문제의 해결 방법</h1>
<p>임계구역에 여러 프로세스 및 스레드가 함부로 접근할 수 없도록 관리를 잘 해줘야 하는데, 이를 위한 방법으로 Mutex와 세마포어가 있다. (CS여서 대충 설명하고 넘겨보자)</p>
<h2 id="mutex는-뭐고-세마포어는-또-뭐야">Mutex는 뭐고? 세마포어는 또 뭐야?</h2>
<h3 id="mutex는">Mutex는?</h3>
<ul>
<li>Mutex란 Mutual Exclusion의 합성어로, 공유된 자원의 데이터나 임계영역 같은 곳에 스레드들의 running time이 서로 겹치지 않게 하나의 프로세스 또는 스레드가 접근하는 것을 막는다.</li>
<li>세마포어와 가장 큰 차이점은 공유자원에 접근할 수 있는 대상의 개수 차이다.</li>
</ul>
<h3 id="임계구역은">임계구역은?</h3>
<p>그러면 또 임계구역은 뭐야?</p>
<ul>
<li>프로그램 코드 상에서 공유 자원에 접근하는 부분을 임계구역(Critical Section)이라고 한다.</li>
</ul>
<h3 id="세마포어란">세마포어란</h3>
<ul>
<li>세마포어는 공유 자원에 여러 프로세스가 접근하는 것을 막는 것을 말한다. 세마포어는 이를 위해서 현재 공유 자원의 상태를 나타내는 카운터 변수를 사용하게 된다.</li>
<li>큰 특징으로는 뮤텍스와 다르게 세마포어는 꼭 1개의 프로세스만이 자원을 점유하지 않는다.</li>
</ul>
<h3 id="결론">결론</h3>
<ul>
<li>뮤텍스는 이진 세마포어로 세마포어의 일종이다. 가장 큰 차이점으로는 뮤텍스는 오직 1개의 프로세스 혹은 스레드만이 공유 자원에 접근할 수 있고, 세마포어는 지정된 변수의 값만큼 접근할 수 있다.</li>
</ul>
<h2 id="mutex-사용하기">Mutex 사용하기</h2>
<p>공유 변수의 변경 가능 지점을 임계 영역으로 만들어 동시 접근을 제한할 수 있다. 이를 할 수 있는 것이 Mutex객체를 활용하는 것이다.</p>
<p>Mutex 객체의 lock 일시 중단 함수가 호출되면 락을 획득하며, 해당 Mutex 객체에 대해 unlock이 호출돼 락이 해제될 때까지 다른 코루틴이 해당 임계 영역에 진입할 수 없게 된다.</p>
<pre><code class="language-kotlin">var i = 0
val mutex = Mutex()

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
            mutex.lock()
            i++
            mutex.unlock()
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<p>주의할 점은 Mutex 객체를 사용해 락을 획득한 후에는 꼭 해제해야한다. 만약 해제하지 않으면 해당 임계 영역은 다른 스레드에서 접근이 불가능하게 된다.</p>
<pre><code class="language-kotlin">var i = 0
val mutex = Mutex()

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
            mutex.lock()
            i++
        }
    }
    delay(1000)
    println(i) // 1 이후에 아무 동작도, 컴파일이 멈추지도 않는다.
}</code></pre>
<p>하지만 lock를 사용하면서 unlock을 까먹지 않고 선언하기에는 너무나 번거롭다. </p>
<p>코드가 길어질 수록 lock-unlock 쌍을 지키기 못하는 경우가 생길 수도 있다.</p>
<p>이런 문제는 Mutex 객체는 withLock 일시 중단을 사용하여 해결할 수 있다. withLock은 람다식 실행 이전에 lock이 호출되고, 람다식이 모두 실행되면 unlock을 호출해주는 메소드다.</p>
<pre><code class="language-kotlin">var i = 0
val mutex = Mutex()

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
                mutex.withLock {
                        i += 1
                      }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<h3 id="그러면-또-궁금할-수-있다-왜-세마포어로는-경쟁-상태-문제를-해결할-수-없을까">그러면 또 궁금할 수 있다! 왜 세마포어로는 경쟁 상태 문제를 해결할 수 없을까?!</h3>
<p>해결할 수는 있다! 세마포어의 허용 개수를 1로 설정하면 된다. 이를 이진 세마포어라고 하며 뮤텍스와 동일하게 동작한다. 하지만 현대 프로그래밍 환경에서는 공유 데이터를 보호할 때 이진 세마포어보다 뮤텍스를 사용할 것을 권장한다.</p>
<ol>
<li>소유권의 유무로 인한 치명적 버그 가능성</li>
<li>코드의 의도와 가독성</li>
</ol>
<pre><code class="language-kotlin">var i = 0
val semaphore = Semaphore(1)

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
            semaphore.withPermit {
                i += 1
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<h3 id="synchronized-객체">Synchronized 객체?</h3>
<p>Synchronized는 JVM 환경에서 계속 사용해오던 동시성을 제어하는 도구였다. 하지만 코루틴 생태계로 넘어오면서 권장하지 않는 도구가 되었다.</p>
<ol>
<li><p>synchronized 객체는 자물쇠를 얻지 못하면 운영체제의 스레드 자체를 통쨰로 멈춰 세운다. (Blocking)</p>
<p> 만약 안드로이드의 Dispatchers.Main 스레드 위에서 synchronized 때문에 스레드가 멈춰버리면 앱이 버벅거리거나 멈추는 ANR 현상이 발생한다.</p>
</li>
<li><p>synchronized 블록 내부에서는 delay나 suspend 함수를 호출할 수 없다.</p>
</li>
</ol>
<pre><code class="language-kotlin">var i = 0
val lock = Any() 

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
            synchronized(lock) {
                i += 1
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<h3 id="reentrantlock-객체"><del>ReentrantLock 객체?</del></h3>
<p>ReentrantLock 객체는 Mutex 객체와 같은 기능을 가진다. 그러면 왜 Mutex를 사용하고 ReentrantLock 객체를 사용하지 않을까?</p>
<p>Mutex 객체는 lock 함수를 호출했을 떄 이미 다른 코루틴에 의해 Mutex 객체에 락이 걸려 있으면 코루틴은 기존의 락이 해제될 때까지 스레드를 양보하고 일시 중단한다.</p>
<p>이를 통해 코루틴이 일시 중단되는 동안 스레드가 블로킹되지 않도록 해서 스레드에서 다른 작업이 실행될 수 있도록 한다.</p>
<p>반면 ReentrantLock 객체는 lock 함수를 호출했을 때 이미 다른 스레드에서 락을 획득했다면 코루틴은 락이 해제될 떄까지 lock을 호출한 스레드를 블로킹하고 기다린다. 즉, 락이 해제될 떄까지 다른 코루틴이 락을 호출한 스레드를 사용할 수 없는 것이다.</p>
<pre><code class="language-kotlin">var i = 0
val reentrantLock = ReentrantLock()

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.IO) {
            launch {
                reentrantLock.lock()
                i += 1
                reentrantLock.unlock()
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<p>마찬가지로 연산이 손실되지 않는 것을 볼 수 있다.</p>
<h2 id="공유-상태-변경을-위해-전용-스레드-사용하기">공유 상태 변경을 위해 전용 스레드 사용하기</h2>
<p>이런 문제는 복수의 스레드가 공유 상태에 동시에 접근할 수 있기 떄문에 발생하는 것이다. 그러면 공유 상태에 접근할 때 하나의 전용 스레드만 사용하도록 강제하면 공유 상태에 동시에 접근하는 문제를 해결할 수 있다.</p>
<p>하나의 전용 스레드만 사용할 수 있도록 만드는 방법은 newSingleThreadContext 함수를 사용하여 단일 스레드로 구성된 객체를 만들면 된다.</p>
<pre><code class="language-kotlin">var i = 0
val countChangeDispatcher = newSingleThreadContext(&quot;CountChangeThread&quot;)

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.Default) {
            launch(countChangeDispatcher) {
                i += 1
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<p>위 코드는 Dispatchers.Default 에서 count가 되는건 동일하지만, 이 연산을 실행할 때는 countChangeDispatcher를 가지는 전용 스레드에서 동작하기 떄문에 공유 상태에 동시에 접근하는 문제를 해결할 수 있는 것이다.</p>
<h2 id="atomicinteger-객체-사용">AtomicInteger 객체 사용</h2>
<p>AtomicInteger 객체는 원자성 있는 객체로 여러 스레드가 동시에 접근해도 한 번에 하나의 스레드만 접근할 수 있도록 제한한다.</p>
<pre><code class="language-kotlin">var i = AtomicInteger(0)
suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.Default) {
            launch {
                i.getAndUpdate{
                    it + 1
                }
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장된다.
}</code></pre>
<h3 id="원자성-있는-코루틴-객체-만들기">원자성 있는 코루틴 객체 만들기</h3>
<p>AtomicReference 클래스를 사용하면 복잡한 객체의 참조에 대해 원자성을 부여할 수 있다.</p>
<pre><code class="language-kotlin">data class Counter(val name: String, val count: Int)

val atomicCounter = AtomicReference(Counter(&quot;MyCounter&quot;, 0))

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.Default) {
            launch {
                atomicCounter.fetchAndUpdate {
                    it.copy(count = it.count + 1)
                }
            }
        }
    }
    delay(1000)
    println(atomicCounter) // 10000이 보장된다.
}</code></pre>
<h4 id="원자성-객체가-아닐-경우">원자성 객체가 아닐 경우</h4>
<pre><code class="language-kotlin">data class Counter(val name: String, val count: Int)

var counter = Counter(&quot;MyCounter&quot;, 0)

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.Default) {
            launch {
                counter = counter.copy(
                    count = counter.count+ 1
                )
            }
        }
    }
    delay(1000)
    println(counter) // 10000이 보장되지 않는다.
}</code></pre>
<h3 id="원자성-있는-객체를-사용할-때-많이-하는-실수">원자성 있는 객체를 사용할 때 많이 하는 실수</h3>
<p>원자성 있는 객체의 읽기와 쓰기를 따로하는 경우다.</p>
<pre><code class="language-kotlin">var i = 0

suspend fun main() = coroutineScope {
    repeat(10_000) {
        launch(Dispatchers.Default) {
            launch {
                val currentI =  i.get()
                i.set(currentI + 1)
            }
        }
    }
    delay(1000)
    println(i) // 10000이 보장되지 않는다.
}</code></pre>
<p>이 경우에는 읽기 연산과 쓰기 연산을 따로하기 때문에 한 스레드에서 get 연산을 실행했지만 set 함수가 실행되기 전에 다른 스레드에서 get / set 연산을 실행할 수 있기 떄문에 문제가 발생한다.</p>
<p>즉, 원자성 있는 객체를 안전하게 사용하기 위해서는 하나의 스레드가 값을 읽고 쓸 동안 다른 스레드의 접근을 허용해서는 안된다.</p>
<h1 id="내-코드를-수정하기">내 코드를 수정하기</h1>
<pre><code class="language-kotlin">override suspend fun increase(
    product: Product,
    quantity: Int,
) {
    val existing = loadAll().firstOrNull { it.hasProductId(product.id) }
    if (existing == null) {
        cartServerDataSource.insertCartItem(CartContent(product, quantity))
    } else {
        cartServerDataSource.updateCartItem(
            CartContent(existing.product, existing.quantity + quantity, existing.id),
        )
    }
}

private suspend fun loadAll(): List&lt;CartContent&gt; = cartServerDataSource.pagination(0, ALL_PAGE_SIZE, emptyList())
</code></pre>
<h3 id="잘못된-시나리오">잘못된 시나리오</h3>
<ul>
<li>loadAll() → 장바구니의 전체 아이템 목록을 불러옴</li>
<li>exisiting → 전체 장바구니 중 증가시킬 상품이 존재하는가?</li>
<li>만약 존재하지 않는다면? → insertCartItem으로 장바구니에 상품 추가</li>
<li>만약 존재한다면? → updateCartItem으로 장바구니에 있는 상품의 개수를 증가</li>
<li>delay를 주었을 떄 이 시나리오는 성립하지 못한다.</li>
<li>왜? 존재하는지 읽어오고 상품이 없다면 2초 후에 상품을 추가한다.</li>
<li>하지만 2초가 지나기 전에 한번 더 버튼을 누르면 existing은 null로 인식된다. 왜냐하면 처음 실행했던 로직이 실행되지 않았기 떄문에 아직 장바구니에는 상품이 들어가지 않았다.</li>
<li>그래서 400 에러가 나타나게 된다. 그리고 앱이 이상하게 된다.</li>
</ul>
<h2 id="어떻게-수정해야할까">어떻게 수정해야할까?</h2>
<pre><code class="language-kotlin">val mutex = Mutex()

override suspend fun increase(
    product: Product,
    quantity: Int,
) {
      mutex.withLock {
          val existing = loadAll().firstOrNull { it.hasProductId(product.id) }
          if (existing == null) {
              cartServerDataSource.insertCartItem(CartContent(product, quantity))
          } else {
              cartServerDataSource.updateCartItem(
                  CartContent(existing.product, existing.quantity + quantity, existing.id),
              )
          }
    }
}
</code></pre>
<p>이런식으로 수정하면 잘 된다~ 단, loadAll()은 mutex로 접근하면 안된다. 그러면 데드락이 발생한다. 왜냐하면 increase에서 증가시키기 위해 스레드를 lock 시켰다. 이미 스레드를 사용하고 있다. 근데 그 안에서 사용하는 loadAll()도 mutex.withLock()을 사용한다면 lock걸린 스레드를 사용하기 위해 대기하는데, loadAll()의 결과를 받아야 increase 의 작업이 끝난다. 그러기 위해서는 loadAll()의 작업 결과를 받아와야한다. 그러기 위해서는 increase 의 작업이 끝나길 기다려야 한다……</p>
<p>이렇게 반복된다.</p>
<h2 id="실습-코드">실습 코드</h2>
<p><a href="https://github.com/noeyhoj/coroutine-race-condition-example">https://github.com/noeyhoj/coroutine-race-condition-example</a></p>
<h2 id="발표-자료">발표 자료</h2>
<p><a href="https://www.canva.com/design/DAHKo1aiJrQ/ewU3Z9GkQ4XYove3FCcaHA/edit">https://www.canva.com/design/DAHKo1aiJrQ/ewU3Z9GkQ4XYove3FCcaHA/edit</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] 불변 객체의 가치는 무엇일까?]]></title>
            <link>https://velog.io/@noeyh_0j/kotlin-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EA%B0%80%EC%B9%98%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@noeyh_0j/kotlin-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EA%B0%80%EC%B9%98%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sat, 16 May 2026 09:51:58 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>가변 객체와 불변 객체에 대해 알아보며 가변적인 개발자가 되고싶은 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 가변 객체가 무엇인지와 불변 객체가 무엇인지, 그리고 각각의 단점과 사용했을 때의 이점에 대해 정리해보고자 이 글을 작성하게 되었습니다.</p>
<h1 id="왜-이-주제를-선정하였는가-나의-고민">왜 이 주제를 선정하였는가? (나의 고민)</h1>
<p>가변 객체를 좋아했었다. 단순한 이유지만 가변 객체가 수정이 더 쉽기 때문이고, 불변 객체를 사용하기 위해서는 객체를 계속해서 재생성해야하기 떄문에 너무나 많은 작업이 필요했다. </p>
<p>예를 들면 특정 도메인 값을 변경하기 위해 체이닝된 다른 도메인에서 상호작용하는 도메인을 불러와야하는 번거로움이 있다. 또한 상태를 변경할 때 객체를 재생성해야하는 로직을 생성해야 한다. 가변객체의 경우 이런 로직을 생성할 필요가 없다. 왜? 상태를 직접 변경하고 불러오면 되기 때문이다.</p>
<p>하지만 리뷰어, 여기저기서 들려오는 말들에 의하면 불변 객체가 좋다. 가변 객체를 지양하라. 이런 얘기들이 들려왔다.</p>
<p>얼마나 좋길래 다들 불변객체 노래를 할까?</p>
<h2 id="가변-객체-변경과-불변-객체-변경에-대한-코드-예시">가변 객체 변경과 불변 객체 변경에 대한 코드 예시</h2>
<pre><code class="language-kotlin">class MutableUser(
    var id: Long,
    var name: String,
    var age: Int
)

fun main() {
    val user = MutableUser(1, &quot;김조현&quot;, 25)

    user.age = 26
    user.name = &quot;Haro&quot;

    println(&quot;수정된 가변 객체: ${user.name}, ${user.age}&quot;)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/34145e2c-68d6-4ad1-a2d1-b735a6b2f452/image.png" alt=""></p>
<pre><code class="language-kotlin">data class ImmutableUser(
    val id: Long,
    val name: String,
    val age: Int
)

fun main() {
    val user = ImmutableUser(1, &quot;김조현&quot;, 25)

    val updatedUser = user.copy(
        name = &quot;Haro&quot;,
        age = 26
    )

    println(&quot;원본 객체: ${user.name}, ${user.age}&quot;)
    println(&quot;새로운 불변 객체: ${updatedUser.name}, ${updatedUser.age}&quot;)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/31f91b98-f622-49fc-a2da-70acc8d9d95f/image.png" alt=""></p>
<h1 id="가변객체란">가변객체란?</h1>
<p>가변객체는 값을 바꿀 수 있는 객체다. 여기서 값은 메모리 위치에 존재하는 값을 의미한다. </p>
<p>가변객체의 경우에는 값이 변경된다고 하더라도 객체가 가지고 있는 메모리의 위치는 변하지 않고, 위치가 가리키는 값이 변경되는 것이다.</p>
<h1 id="가변성의-문제">가변성의 문제</h1>
<ul>
<li>멀티스레드에서 값을 보장하지 못함.<ul>
<li>예를 들어 하나의 객체에 대해 여러 개의 스레드가 수정을 한다면 데이터의 무결성이 깨지게 된다.</li>
</ul>
</li>
<li>값의 예측이 어렵고 변경에 있어서 위험함.<ul>
<li>이 내용은 다음과 같다. 함수를 사용해 상태를 변경하면 가변 객체의 경우는 이전 상태를 저장할 수 없다. 왜냐하면 객체 자체의 값을 바꾸기 때문이다. 그렇기에 어떻게 바뀔지, 어떤 상태를 가지고 있을지 호출하는 함수가 많아질수록 복잡해지기 때문에 예측이 어렵다는 것이다.</li>
<li>그렇기에 객체를 되돌리려면 따로 복사해서 저장하는 로직이 필요하겠죠? (방어적 복사)</li>
<li>또한 어떤 값으로 변경되는지 추적할 수 없기 때문에 변경에 유의해야한다. (연쇄효과)</li>
</ul>
</li>
<li>테스트와 디버깅이 어려움.<ul>
<li>마찬가지로 객체의 이전 상태를 기억할 수 없으니 테스트와 디버깅또한 어렵다.</li>
<li>좋은 테스트란 어떤 환경에서도 결과가 같아야합니다. 하지만 가변 객체는 이전 테스트가 남긴 흔적 때문에 다음 테스트 결과가 달라질 수 있는 문제의 가능성이 남습니다. 막으려면 객체를 생성해줘야겠죠?</li>
<li>디버깅의 핵심은 어디서 값이 변하는가 과정을 보고 찾는 것입니다. 가변 객체는 저장하지 않고 값을 변경하기 때문에 어디서 어떻게 변하는지 추적하기가 매우 어렵습니다.</li>
</ul>
</li>
<li>상태 변경 발생 시 처리를 해줘야함.<ul>
<li>가변 객체를 사용할 경우 값이 변경되었을 때 내부의 값이 바뀌는 것이지, 외부나 나타내는 값이 변경되는 것이 아닙니다. 그렇기에 상태가 바뀌어도 알아차리기 힘들죠. 그래서 주변 코드들이 일일이 감시해야 상태를 추적하여 변경도 파악할 수 있습니다.</li>
</ul>
</li>
</ul>
<h1 id="불변객체란">불변객체란?</h1>
<p>객체가 생성된 후 내부 상태가 변하지 않는 객체를 의미함. 가변객체와 반대되는 의미.</p>
<h1 id="불변-객체를-왜이렇게-좋아할까">불변 객체를 왜이렇게 좋아할까?</h1>
<p>가변객체를 사용할 때 가변객체의 수가 적다면 예측가능한 패턴만 존재할것이다. 하지만 그 수가 많아진다면 패턴은 기하급수적으로 증가할 것이다.</p>
<ul>
<li>패턴이 많아진다면 추적이 힘들어지니 코드를 이해하는 것과 디버그가 어려워질 것이다.</li>
<li>가변성이 있으면, 코드의 실행을 추론하기 어렵다.</li>
<li>멀티 스레드에서는 동기화가 필요하다. 변경이 일어나는 부분은 충돌이 발생할 수 있다.</li>
<li>많은 패턴을 하나하나 테스트하기 어렵다.</li>
<li>상태 변경시 변경에 영향을 받을 수 있는 모든 부분에 변경을 알려야 한다.</li>
</ul>
<h1 id="불변객체가-주는-핵심-가치">불변객체가 주는 핵심 가치</h1>
<ul>
<li>스레드 안전성<ul>
<li>앞에서 가변객체의 경우는 한 객체에 대해 동시에 수정하려고 할 때, 문제가 발생한다고 했습니다. 하지만 불변 객체는 한 번 생성된 후 변하지 않기 때문에 데이터가 오염될 가능성이 없는 것입니다.</li>
</ul>
</li>
<li>방어적 복사의 불필요<ul>
<li>방어적 복사란 쉽게 말하면 원본 대신 복사본을 전달하여 외부의 변경이 내부 상태에 영향을 주지 않도록 하는 코딩 기법이다. 즉, 불변성을 유지하기 위한 방법이죠. 하지만 불변객체는 이미 말 그대로 불변성을 가지고 있습니다. 그렇기에 가변 객체에서 특정 데이터를 저장하기 위해 복사하는 것처럼 방어적 복사를 할 필요가 없는 것입니다.</li>
</ul>
</li>
<li>실패 원자적인 메소드<ul>
<li>가변 객체를 사용했을 때는 작업 도중 에러가 나면 객체가 이도저도 아닌 애매한 상태로 남아있을 수 있다. 하지만 불변객체의 경우는 연산이 성공해 새로운 객체가 나오거나 아니면 기존 객체가 그대로 있거나, 둘 중 하나입니다. 그렇기에 프로그램이 비정상 종료되어도 데이터가 오염되지 않는다.</li>
</ul>
</li>
<li>부수 효과를 피해 오류 가능성을 최소화할 수 있다.<ul>
<li>부수 효과란 함수 내부에서 외부 객체의 상태를 바꾸는 것을 정의하는 단어다. 가변 객체 리스트가 있고, 특정 함수를 실행하고 나면 이 리스트는 정렬되있을까? 아니면 뭐가 추가되거나 삭제되어 있을까? 이런 식으로 의심을 할 것입니다. 하지만 불변 객체는? 절대 변하지 않습니다. 오직 새로운 결과만 반환합니다.</li>
</ul>
</li>
<li>예측 가능성 및 테스트 용이성<ul>
<li>또한 테스트도 용이하겠죠. 불변 객체는 새로운 객체를 생성하기 때문에 어떤 환경에서도 테스트가 변하지 않을 것입니다. 가변 객체처럼 상태가 변할 가능성이 없으니까요.</li>
</ul>
</li>
</ul>
<p>즉, 가변요소가 많아질수록 일관성 문제와 복잡성이 증가한다.</p>
<p>가변성을 제한하기 위해서는 요소들을 불변으로 만들면 된다.</p>
<h1 id="그러면-불변객체는-단점이-없나">그러면 불변객체는 단점이 없나?</h1>
<p>그럴리가. 장점이 있으면 단점도 있겠죠?</p>
<p>불변 객체는 객체를 계속해서 생성합니다. 그러면 자연스럽게 이런 생각이 들 것 같습니다. 계속 객체를 생성하는데, 메모리 낭비나 성능 저하같은 문제는 없을까?</p>
<p>저도 공부하는 마음이였기에 순수하게 이런 질문이 생겼습니다.</p>
<ul>
<li>예를 들면 User객체 안에 Address 객체가 있고, 그 안에 City 객체가 있을 때 City 이름 하나를 바꾸려면 City → Address → User 순으로 모든 부모 객체를 다시 생성해야 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/5bb7f1db-3317-4e8e-b61a-f1ecfd49fd44/image.png" alt=""></p>
<ul>
<li>코드의 복잡도 증가(보일러플레이트 증가) : 불변성을 유지하기 위해 작성해야 하는 부가적인 코드들이 늘어난다.</li>
<li>상태를 변경하는 대신 새로운 객체를 반환하는 메서드들을 일일이 만들어야 한다.</li>
<li>JVM이 객체 생성에 최적화되어 있다고 해도, 절대적인 생성 횟수가 많아지만 메모리 사용량은 늘어날 수밖에 없다.</li>
</ul>
<h1 id="매번-새로-객체를-만들면-메모리가-낭비되지-않는가">매번 새로 객체를 만들면 메모리가 낭비되지 않는가?</h1>
<p>낭비되죠. 하지만 JVM의 가비지 컬렉션이 단기 생존 객체를 빠르게 처리하기 때문에 가변 상태 관리로 발생되는 버그 수정 비용(삽질)보다 객체 생성 비용이 훨씬 저렴하다.</p>
<p>즉, 사람의 시간을 아끼기 위해 기계의 자원을 효율적으로 사용하는 전략이다.</p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/c2546b55-b719-482a-ae3b-087839566218/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/dd002cbb-79a2-4c52-aad5-0b5e3362a388/image.png" alt=""></p>
<h1 id="불변객체를-어떻게-만들어야할까">불변객체를 어떻게 만들어야할까?</h1>
<p>자바에서는 final로, 코틀린에서는 val 키워드를 사용하여 값을 수정할 수 없는 변수로 만들 수 있다.</p>
<p>내부 상태가 변하지 않아야하기에 setter메소드를 제공하지 않는다. 또는 내부의 상태를 보여주기 위해 방어적 복사를 통해 내부 상태를 제공해준다.</p>
<h2 id="왜-val은-불변-객체-생성이-아닐까">왜 val은 불변 객체 생성이 아닐까?</h2>
<p>val 키워드를 사용한다면 변수에 재할당을 할 수 없었으니, 불변 객체가 아닌가? 라고 생각할 수도 있을 것이다. 나 또한 그랬고,</p>
<p>하지만 val은 읽기 전용이다. 즉, 변수가 가리키는 메모리 주소값을 바꿀 수 없다는 의미다. 하지만 메모리 주소에 있는 객체가 내부적으로 값을 바꿀 수 있는 객체라면?</p>
<pre><code class="language-kotlin">class Person(var name: String) // 내부 프로퍼티가 var(가변)

fun main() {
    val user = Person(&quot;김조현&quot;) // val로 선언

    // user = Person(&quot;Haro&quot;) // 에러! (참조 재할당 불가)

    user.name = &quot;Haro&quot; // 가능! (객체 내부 상태는 변경 가능)
    println(user.name) // 출력: Haro
}</code></pre>
<p>이 코드에서 user라는 Person객체는 불변객체다. user라는 변수를 재할당할 수 없으니까.</p>
<p>하지만 user의 프로퍼티인 name은? var로 선언된 가변객체이기 때문에 값을 바꿀 수 있다. 즉, 완전한 불변 객체가 아니라는 의미다.</p>
<p>즉, 불변 객체를 사용하기 위해 val 키워드를 사용할 수는 있지만 val 키워드만을 사용하여 불변 객체를 만들 수는 없습니다.</p>
<h2 id="그러면-어떻게-해야되는가">그러면 어떻게 해야되는가?</h2>
<pre><code class="language-kotlin">data class Person(var name: String) // 내부 프로퍼티가 var(가변)

val originalHaro = User(name = &quot;Haro&quot;, age = 25)

// 나이가 한 살 먹었을 때, 기존 객체를 수정하는 대신 복사본 생성
val olderHaro = originalHaro.copy(age = 26)</code></pre>
<p>data class와 copy() 메소드를 사용하면 불변 객체를 쉽게 만들 수 있다. copy() 메소드는 객체 스스로를 복사해 반환하는 메소드다.</p>
<h2 id="불변-객체가-되기-위한-조건">불변 객체가 되기 위한 조건</h2>
<ul>
<li>모든 필드가 val인가?</li>
<li>필드에 담긴 객체들도 모두 불변인가?</li>
</ul>
<p>와 같은 조건들을 만족해야 한다는 것을 알 수 있다.</p>
<h1 id="모든-것을-불변으로-만들어야-할까">모든 것을 불변으로 만들어야 할까?</h1>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/587943fe-0f94-4986-a109-ca0bf35dcd67/image.png" alt=""></p>
<p>이 역시 아니다. 불변 객체가 좋은 상황이 있지만, 가변 객체가 좋은 상황 또한 존재한다.</p>
<p>어떤 상황이 있을까? 불변 객체의 단점이 커지는 상황이 가변 객체가 활약할 환경이겠죠? </p>
<ul>
<li>실시간 시스템을 가지는 게임 엔진이나 실시간 연산이 필요한 곳처럼 지연 시간이 낄 틈도 없는 환경에서는 가변 객체를 사용합니다.</li>
<li>또는 짧은 시간안에 수많은 값이 바뀌는 루프 안에서도 사용할 수 없겠죠. 매번 객체를 생성하면 메모리 할당 부하가 증가하기 때문입니다.</li>
<li>객체의 크기가 매우 크고 복잡한 경우, 작은 부분 하나를 바꾸기 위해 전체를 복사하는 것은 비효율적이겠죠?</li>
</ul>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 학습을 통해 가변 객체와 불변 객체가 무엇인지, 어떤 단점과 장점을 가지고 있으며, 어떻게 불변 객체를 만드는지와 어떤 상황에서 사용해야하는지 등을 알아봤습니다.</p>
<p>비록 깊게 다루지는 않았지만 주제에 맞는 깊이로 전달하기에는 이정도가 충분하다고 생각했습니다. 이 주제에 대해 공부해보며 JVM이 어떤 동작을 하길래 불변 객체를 위해 계속 객체를 생성해도 메모리에 부하가 적은지가 궁금했었습니다.</p>
<p>나중에 이 부분은 JVM에 대해 알아보면서 연관지으면 더 깊이있게 이해할 수 있을 것 같습니다 :)</p>
<p>긴 글 읽어주셔서 감사합니다 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 낭만 찾아 떠나기]]></title>
            <link>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EB%82%AD%EB%A7%8C-%EC%B0%BE%EC%95%84-%EB%96%A0%EB%82%98%EA%B8%B0</link>
            <guid>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EB%82%AD%EB%A7%8C-%EC%B0%BE%EC%95%84-%EB%96%A0%EB%82%98%EA%B8%B0</guid>
            <pubDate>Sun, 26 Apr 2026 11:26:08 GMT</pubDate>
            <description><![CDATA[<h1 id="걸어서-바다가기-프로젝트">걸어서 바다가기 프로젝트</h1>
<p>안녕하세요. 낭만을 중요시하는 개발자 꿈나무 김조현입니다.</p>
<p>이번에는 개발과 관련된 회고는 아닙니다. 이번에 작성할 회고는 버킷리스트, 도전에 관련된 회고입니다.</p>
<p>일명 &quot;걸어서 바다보러 가기&quot; 프로젝트 입니다. 말 그대로 집에서부터 오직 걷기만으로 바다보러 가는 것이죠!</p>
<h1 id="어쩌다가-시작하게-되었는가">어쩌다가 시작하게 되었는가?</h1>
<p>한 번쯤 해보고 싶었던 도전이였습니다. 걸어서 바다보러 가기라니. 너무 멋지지 않나요?</p>
<p>하지만 반대로 왜 힘들게 걸어서 가는가? 라고 생각하는 사람이 있을 것입니다. 교통편도 좋고, 시간도 더 적게 걸리는데 굳이 힘들게 왜 걸어가려고 하는가? 가장 많이 들었던 질문입니다.</p>
<p>하지만 &quot;낭만&quot;. 이 한 단어면 대답이 충분하다고 생각합니다. </p>
<h1 id="어디서부터-어디까지">어디서부터 어디까지?</h1>
<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/2327f09f-3016-425a-a2e7-0a9042917cd8/image.png" size="50%">
</div>

<p>목표로 정한 곳은 제부도가 보이는 전곡항까지입니다. 왜 제부도를 선택했느냐? </p>
<p>1박 2일로 갔다와야하기 때문에 너무 먼 곳은 무리이기 때문에 거리를 조금 신경썼습니다. 이 조건대로면 평택항 또는 전곡항 두 곳이 후보였습니다. 그 중에서 전곡항을 고르게 되었습니다. </p>
<p>왜 전곡항을 골랐는가? 라면 평택항은 내적 친밀감이 강해 익숙하지 않은 동네로 가보고 싶다는 마음이 컸습니다.</p>
<h1 id="어떤-일이-있었는가">어떤 일이 있었는가?</h1>
<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/be01bd3c-e8d4-4bd9-8b5e-cfd9a583fcc9/image.jpeg" width="50%">
</div>

<div align="center">
  분명 차도 옆인데 방생된 닭들이....
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/65f32a5c-7f89-4a74-8722-9400a1a7caa1/image.jpeg" width="50%">
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/8f3efe03-31a1-4c9d-9384-6f22069104c1/image.jpeg" width="50%">
</div>

<div align="center">
  철도 위에도 지나가보고~
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/25a7f317-8344-4a73-b0f1-676c660eeb11/image.jpeg" width="50%">
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/6890d238-bd84-470a-8f3a-648849618ed5/image.jpeg" width="50%">
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/d25ce5ba-717e-4273-9496-542a5b22e98d/image.jpeg" width="50%">
</div>

<div align="center">
  느낌있게 풍경도 보고~
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/e1b296d3-8400-4978-a68e-bc172417a4e5/image.jpeg" width="50%">
</div>

<div align="center">
  흑염소도 보고~
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/b3a9e178-9688-416f-b1bf-5a986ccc9a43/image.jpeg" width="50%">
</div>

<div align="center">
  참이슬 홍보대사(사실아님)
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/1b9bca53-dc0a-4dfe-8ebb-15be396fc577/image.jpeg" width="50%">
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/4304c271-a58d-4ce2-a8bf-85d9b222b2c1/image.jpeg" width="50%">
</div>

<div align="center">
  위험해보여서 다른 길을 찾아보려고 삽질도 해보고~
</div>

<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/06be56ff-4038-44e2-a7eb-3906af6a712a/image.jpeg" width="50%">
</div>

<div align="center">
  점심으로 국밥도 먹고~
</div>

<p>그 이후로는 비슷합니다. 8시 반까지 계속계속계속... 걸었습니다. </p>
<p>사실 걷는 것도 힘들어서 사진도 못 찍었습니다..</p>
<h1 id="성공했는가">성공했는가?!</h1>
<p>아쉽게도 바다는 보러가지 못했습니다... </p>
<p>실패의 요인이 무엇일까 생각해봤을 너무 무리한 계획이였다는 생각이 가장 크게 들었습니다. 2일 안에 70km 이상을 걸어가야 한다는 것 자체가 무리였던 것 같습니다. </p>
<p>첫 날은 화성시청을 목표로 걸었습니다. 이렇게 걸으면 첫 날에는 50km, 둘째 날에는 20km만 걸으면 됐습니다. 첫 날에 빡세게 갔다가 둘째 날은 조금 널널하게 걸으면 되겠지~ 라는 이론상 완벽한 계획이였지만 운동하지 않은 약한 몸을 배제했다는 사실을 첫째날 50km를 걸으면서 알게 되었습니다...</p>
<p>결국 화성시청에서 포기하고 집으로 돌아오게 되었습니다.</p>
<h1 id="아까운데-바다라도-보고오지">아까운데 바다라도 보고오지~</h1>
<p>아깝죠.. 화성까지 걸어간게 정말 아쉽고 아까워서 바다라도 보고 와야하나? 라는 생각도 했습니다. 하지만 그렇게 가는 바다가 의미가 있을까요?</p>
<p>이 프로젝트는 &quot;걸어서&quot; 바다가기 입니다. 바다를 가더라도 걸어서 갔을 때 의미가 있다고 생각했기 때문에 바다까지는 가지 않고 버스타고 집으로 돌아왔습니다.</p>
<p>아쉬운만큼 언젠간 또 해야겠다는 생각이 들 수 있겠죠~~</p>
<h1 id="마무리입니다">마무리입니다!</h1>
<div align="center">
  <img src="https://velog.velcdn.com/images/noeyh_0j/post/7d99d5a9-2e70-45c7-a14a-8cd9dcd703c5/image.jpeg" width="50%">
</div>

<p>이렇게 첫 번째 걸어서 바다가기 프로젝트가 실패로 마무리 되었습니다. 그렇지만 무척 재밌었습니다! </p>
<p>친구와 같이 둘이서 갔습니다. 13시간동안 쉬지 않고 얘기하고, 같이 힘들고, 뿌듯해하고 무척 의미있는 경험이였습니다.</p>
<p>싫은 기색 없이 함께 다치지 않고(외상으로...) 잘 마무리해서 다행이고, 고맙다고 생각합니다 :)</p>
<p>사진도 사실 친구가 찍은 것이지만~ 조금만 빌렸습니다~~</p>
<p>비록 정해놓은 목표의 관점에서 봤을 때는 실패했지만 이렇게 도전했다는 사실과 경험이 의미없지 않고 큰 동기부여가 됐다고 생각합니다. 앞으로 어떤 힘든 활동이 있더라도 이 경험을 생각한다면 조금 더 힘낼 수 있지 않을까~? 라는 생각도 듭니다 ㅎㅎ</p>
<p>이만 마무리해보겠습니다! 긴 글 읽어주셔서 감사합니다 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 레벨1 전체 회고]]></title>
            <link>https://velog.io/@noeyh_0j/%EB%A0%88%EB%B2%A81-%EC%A0%84%EC%B2%B4-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@noeyh_0j/%EB%A0%88%EB%B2%A81-%EC%A0%84%EC%B2%B4-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 19 Apr 2026 12:12:22 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요</h1>
<p>스스로를 회고할 수 있는 멋진 사람이 되고자 하는 개발자 꿈나무 김조현입니다.</p>
<p>레벨 1회고… 블로그에도 오랜만에 오니 뭘 써야할지 막막하네요...</p>
<p>오랜만에 쓰는 회고록이다보니 회고록이 어떻게 써야하는 글인지 다시 찾아봤다.</p>
<blockquote>
<p>회고록은 개인이 지나온 삶의 특정 시기나 주요 사건, 경험을 돌이켜보며 사실에 기반하여 기록한 글이다.</p>
</blockquote>
<p>그렇다면 이번 글은 2월 24일 우테코가 시작한 날부터 4월 17일 방학식까지 내가 느낀 경험을 정리하면 될 것 같다.</p>
<p>이럴 줄 알았으면 한 문장씩이라도 기록해놓을걸,, 하는 후회가 있지만, 이미 늦은걸 어떡할까.. 다음 레벨부터는 이런 상황을 상정하고 조금씩이라도 무엇을 했는지 기록해봐야겠다. 잘 기억이 안나니 큰 키워드별로 작성해보겠다.</p>
<h1 id="입학식">입학식</h1>
<p>이것도 참 재미? 있었다. 안 그래도 낯을 심하게 가리는데 처음 본 사람들과 함께 5분짜리 연극을 전부 준비하여 무대에서 공연하라니.. 두근두근한 마음으로 가기 위해 아무 정보도 찾아보지 않고 갔던 나에게는 너무나 과하게 신선한 OT내용이였다.</p>
<p>이 뿐만 아니라 동시에 미션도 같이 공지되었다. 미션의 내용은 Gemini Canvas를 활용한 웹앱 만들기였다. 각 주제에 맞는 웹앱을 한 개 이상식 만드는것인데 주제는 다음과 같다.</p>
<ul>
<li>유틸리티</li>
<li>게임</li>
<li>학습</li>
<li>페어 프롬프트 릴레이</li>
</ul>
<p>위의 3개는 어느정도 큰 설명이 없이 느낌이 왔었다. 게임이면 게임, 학습이면 학습, 유틸리티면 좋은거 등. 하지만 페어 프롬프트 릴레이? 이건 처음 들어본다. 페어 프롬프트 릴레이는 쉽게 말하면 코드 치는 사람 따로, 코드 구성하는 사람 따로(머리 따로 몸 따로), 각 역할을 가진 둘이서 서로 번갈아가며 프롬프트를 작성해 제미나이 웹앱을 만드는 것이다.</p>
<p>내 블로그의 썸내일도 이 때 유틸리티 앱으로 만든 썸내일 생성기다(우하하)</p>
<ul>
<li><a href="https://gemini.google.com/share/8b71e909aa55">썸내일 생성기</a></li>
</ul>
<p>날짜를 추가했을 때 현재 날짜가 기본으로 지정되도록 하고 싶지만 귀찮아서 안고치고 그냥 쓰고 있다...ㅎ</p>
<h1 id="연극">연극</h1>
<p>일주일의 짧은 기간을 준비하고 공연을 했다.. 그냥 했다….. 뇌정지가 와서 대본대로 하지 못했던게 아쉽고 팀원들에게 미안했다. 이렇게 연극도 연극을 하고 끝이 아니라 연극 회고라는 활동을 이어서 진행하였다. 이 연극 회고에서는 잘 기억은 안나지만 대충 어떤 점을 잘했었고, 어떤 점이 부족했었다. 이런식으로 자신에 대한 회고를 하는 활동이였던 것 같다.</p>
<p>연극 끝. 이게 아니라 이 활동을 어떻게 했고, 무엇이 잘한 점이며 부족하다 느낀 점은 무엇인지 돌아볼 수 있는 시간을 가지는 것이 좋았다고 생각한다.</p>
<h1 id="소프트-스킬-활동">소프트 스킬 활동</h1>
<p>소프트 스킬이라는 것도 우테코에 와서 처음 접해봤다. 소프트 스킬. 직역하면 부드러운 기술? 소프트 스킬이란 쉽게 말하면 조직 또는 타인과 협업할 때 원활하게 상호작용할 수 있는 기술이다. 예를 들면 커뮤니케이션, 팀워크, 리더쉽 등이 있다. </p>
<p>이 활동에서는 크게 두 가지 활동이 기억남는다. </p>
<p>첫 번째는 스스로의 단점을 극복할 수 있는 소프트 스킬 강화 목표를 세우고, 그에 맞는 실험을 계획하는 활동이다. 강화 목표를 세워보면서 내가 어떤 점이 스스로 부족하다 느끼는지 돌아볼 수 있었고, 이를 혼자서 계획하고 실천하는 것이 아니라 활동을 팀원들과 공유해야된다. 즉, 내가 단점이라 생각한 부분을 공개해야 하는 것이다. </p>
<p>말만 들었을 때는 왜? 굳이? 라는 생각이 들었다. 내 단점이라 생각하는 부끄러운 점을 상대방과 공유한다니. 살짝 거부감은 들었었지만, 하다보니 왜 이런 활동을 하는지 알 것만 같았다. 이 활동을 하면서 용기를 얻을 수 있었다. 나만 부족하다 느낀 점이 알고보니 다른 사람도 경험한 적 있는 일이고, 이런 식으로 자신은 행동했다 라는 점에서 힘이 됐던 것 같다.</p>
<p>두 번째는 레벨1을 회고하는 글쓰기 활동이다.</p>
<p>첫 번째 활동은 레벨1을 시작할 때 한 활동이고, 두 번째는 레벨1이 마무리될 때 한 활동이다. 지난 1달 반동안 어떤 일들이 있었는지 천천히 돌아보고, 내가 어떤 부분에서 달라졌구나, 처음에는 힘들었지만 잘 적응했구나를 느낄 수 있었다.</p>
<h1 id="페어-프로그래밍">페어 프로그래밍</h1>
<p>이 활동도 우테코에 와서 처음 들어보고 경험했다. 둘이서 코드 하나를 작성한다니, 마치 스타1 생컨 모드가 떠올랐다.</p>
<p>처음에는 되게 부담스러웠다. 혼자서도 못짜는 코드를 둘이서 짜라니. 게다가 낯도 많이 가려 잘 할 수 있을까? 라는 걱정이 제일 많이 됐다. 그치만 괜한 걱정이였다. 내가 부족한 부분을 페어가 채워주고, 반대로 페어가 부족한 부분을 내가 채워줄 수 있었다.</p>
<p>코딩에서도 다른 사람이 나와는 어떻게 다르게 코드를 짜는 지도 확인해볼 수 있었지만, 어떤 생각을 가지고 코드를 짜는지도 서로 공유할 수 있던 점이 매우 재밌었다. 나는 이렇게 생각하는데, 페어는 저렇게 생각하네? 이런 상황이 자주 발생하다보니 내 생각을 전달하고자 매우 노력했다. </p>
<p>그렇다고 무조건 상대방이 말한 코드가 맞느냐? 그건 또 아니였다. 내 생각에는 내가 생각한게 더 좋아보이는데. 라는 생각이 든다면, 이제부터 진짜다. 상대방을 설득하기 위해 열심히 나의 의견과 생각을 어필해야 한다. 코드를 작성하는 것보다 이런 생각을 공유하는 활동을 경험했다는 것에서 배운 점과 성장이 많이 일어났다고 생각한다.</p>
<p>본격적인 미션 시작</p>
<p>연극과 회고가 끝남과 동시에 안드로이드 오리엔테이션을 시작했다. 솔직히 오리엔테이션 내용은 기억이 안난다. </p>
<p>하지만? 미션 내용은 기억이 난다. 첫 번째 미션은 Compose를 사용해 칸반보드의 카드를 만드는 미션이였다. 단, 페어 프로그래밍으로 진행해야됐다.</p>
<h1 id="테코톡">테코톡</h1>
<p>레벨1 회고에서 테코톡도 빼놓을 수 없을 것 같다. 안드로이드 18명 중 레벨1에서 발표하는 사람이 4명 밖에 없는데 그 중 한명이 나다..</p>
<p>내가 한 주제는 “단위 테스트로 나누는 것이 반드시 좋은 것일까?” 이다. 솔직히 얘기해보면 급하게 정한 주제다보니 살짝 정이 가질 않았었다. 이 주제의 취지는 테스트 코드를 배우고 단위 테스트에 배웠는데, 왜 통합 테스트는 알려주지 않는 걸까? 라는 단순한 이유에서 나온 주제다.</p>
<p>정이 가질 않았을 뿐이지, 맘에 들지 않는 주제는 아니였다. 우테코에서 처음 다루게 된 테스트 코드를 조금 더 알아보고자 하는 마음도 있었기 때문이다. 과제에 치이다보니 발표 준비를 정말 빠듯하게 했는데, 그래도 어찌저찌 잘 마무리해서 다행이다. </p>
<p>이 주제에 대한 테코톡을 준비하면서 테스트 코드를 어떻게 사용해야할까? 나는 어떻게 사용하는가? 를 제일 많이 고민했던 것 같다. 이건 레아가 수업시간에 말해주신 내용이지만 무척 멋진 말이여서 테코톡에서 내가 인용해서 사용했고, 나 또한 이 말이 맞다고 생각한다. 테스트 코드를 위한 코드를 짜다보면 내가 뭐하고 있었지? 라는 생각이 들 때가 종종 있었다. 테스트 코드는 내 코드를 검증하기 위한 수단이지, 목적이 되어서는 안된다.</p>
<blockquote>
<p>테스트는 수단일 뿐 목적이 아니다.</p>
</blockquote>
<p>긴장도 엄청 많이 했지만 안드로이드 크루들의 얼굴을 보니 안심되어 생각했던 것보다 떨지 않고 잘 했다고 생각한다.</p>
<h1 id="리뷰어와-소통하기">리뷰어와 소통하기</h1>
<p>미션에 관한것은 크게 두 가지로 나눌 수 있다. 첫 번째는 Compose를 활용한 칸반보드 데스크탑 앱 만들기, 두 번째는 순수 Kotlin만 사용하여 객체지향적으로 설계하기다.</p>
<p>정말 쉬운게 하나 없었다. 그 전까지 대학에서 프로젝트를 하더라도 내 생각을 코드로 작성할 수 있으니 나쁘지 않구나 스스로를 이렇게 생각했는데, 우물안의 개구리였다. 배울 점이 너무너무너무나도 많았다.</p>
<p>먼저 코드를 작성할 때 근거가 있어야 한다. 라는 말이 무척 와닿았다. 미션을 진행하면 리뷰어에게 리뷰 요청을 해야하는데, 내가 아무생각 없이 코드를 작성하거나, 그냥 남들 이렇게 쓰니까 나도 따라해야지 같은 의미를 갖지 않은 코드를 작성하면, 리뷰어가 왜 이런 식으로 작성했어요? 이 코드는 어떤 장점이 있나요? 같은 리뷰를 남겨줬을 때 막막해진다…</p>
<p>또한 내가 어떤 의도로 이러한 코드를 작성했다가 명확하다면, 리뷰어의 의도를 생각해보며 어떤 점을 수용할지, 내 생각이 이런데 맞지 않는가? 같은 자유로운 대화가 된다. 즉, 내가 배울 수 있는 폭이 넓어지며 내 생각을 전달하면서 의도를 설명하는 것을 연습해볼 수 있다.</p>
<p>나는 이런식으로 생각했다. 리뷰를 통해 코드의 틀린 점을 피드백 받을 수 있지만, 그보다 리뷰어가 남겨준 리뷰어의 생각과 의도를 파악해보며, 내 생각을 전달 또는 리뷰어의 의견을 수용하는 과정이 코드의 피드백보다 더 의미있는 경험이라고 생각한다.</p>
<p>막말로 코드는 AI가 나보다 더 잘 짜고 AI에게 시키면 된다. 하지만 이런 AI를 쓰기 위해서는 의도와 목표가 명확해야하고 그러기 위해서는 내 생각을 잘 전달할 수 있어야한다. 내 생각을 정리하고 표현하는 것은 나만이 할 수 있는 일이다. AI는 내 생각을 읽을 수 없다.</p>
<h1 id="마무리">마무리</h1>
<p>뭔가 맥락이 없어 읽는 사람이 불편할 수도 있겠다는 생각이 들었다. 하지만 누군가를 위해서보단 내가 어떤 걸 했는지를 돌아보는 목적으로 쓴 글이기에 만약 이 글을 읽는 사람이 있다면 죄송합니다.</p>
<p>미션에 대한 내용도 적을 수는 있지만, 기술이 얼마나 늘었는지보다 그 과정에서 배운 내면적인 성장을 중심으로 적고 싶었다. 그러다보니 소프트 스킬이나 리뷰어와 활동, 페어 프로그래밍에 힘이 좀 들어간거 같다.</p>
<p>이전의 나와 비교해봤을 때, 협업이 싫었던 대화를 꺼렸던 발표가 싫었던 내가 협업을 즐기고 대화를 재밌어하며 발표에도 재미를 느끼게 변화하였다.</p>
<p>한달 반이라는 짧은 기간에 이렇게 많은 변화가 생겼다는게 스스로 돌아봐도 놀랍다 ㅋㅋ</p>
<p>2레벨 회고때는 더 구체적으로 쓸 수 있게끔 미리미리 적어놔야지….</p>
<p>이렇게 그림없이 주구장창 글만 적은 회고글을 마무리하겠습니다. 읽어주셔서 감사합니다 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin/디자인 패턴] 싱글톤 패턴에 대하여]]></title>
            <link>https://velog.io/@noeyh_0j/Kotlin%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@noeyh_0j/Kotlin%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sat, 21 Mar 2026 09:05:33 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>개발에 대한 이해도를 높이고자 공부하는 개발자 꿈나무 김조현입니다.</p>
<p>오늘은 싱글톤 패턴에 대한 정리글을 써보겠습니다.</p>
<h1 id="왜-공부하는가">왜 공부하는가?</h1>
<p>왜 이걸 공부하냐? object가 싱글톤 패턴이며, enum 객체또한 마찬가지로 싱글톤 패턴이며, 클래스 내에서 companion object를 사용해도 그 객체가 싱글톤 패턴을 가진다고 어디선가 듣고 사용해봤다.</p>
<p>하지만 문법도 알고 쓰는 방법도 알지만 왜 쓰는지를 모른다. 결국 근본적으로 싱글톤 패턴이 무엇인지 알아야 이런 문법과 활용을 잘 할 수 있지 않을까? 라는 생각이 들어 싱글톤 패턴에 대해 공부하고자 마음을 잡았다.</p>
<h1 id="싱글톤-패턴은-무엇인가">싱글톤 패턴은 무엇인가?</h1>
<p>싱글톤은 클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 디자인 패턴이다. 즉, 쉽게 말하면 클래스의 객체를 한개만 생성해서 그 객체만을 사용하는 것이다.</p>
<p>싱글톤 패턴의 큰 장점 중 하나는 해당 인스턴스에 대한 전역 접근 지점을 제공하는 것이다. 그치만 전역 접근을 제공하는 것이라면 코틀린의 경우 객체를 전역 변수로 지정하면 되는 것이 아닌가? 라는 의문이 들 수도 있다.</p>
<p>필수 객체들을 전역 변수로 정의했다고 가정했을 때, 이 변수들의 사용이 편해질 수 있지만 모든 코드가 잠재적으로 해당 변수의 내용을 덮어쓸 수 있다는 위험이 존재한다. 싱글톤 패턴은 전역 변수와 마찬가지로 모든 곳에서부터 일부 객체에 접근할 수 있지만, 이 패턴은 다른 코드가 해당 인스턴스를 덮어쓰지 못하도록 보호한다.</p>
<h2 id="싱글톤-패턴의-장점">싱글톤 패턴의 장점</h2>
<ul>
<li>하나의 인스턴스로 관리하니 클래스의 데이터의 공유가 쉽다.</li>
<li>하나의 인스턴스만을 사용함으로서 메모리 리소스를 줄일 수 있다는 장점이 있다.</li>
<li>처음 로딩 이후 추가적인 세팅이나 로딩을 필요로 하지 않기 때문에 초기화 부분의 생략이 가능하다.</li>
</ul>
<h2 id="싱글톤-패턴의-단점">싱글톤 패턴의 단점</h2>
<ul>
<li>싱글톤 패턴은 하나만 생성되어야 하지만 멀티스레드 환경에서 1개만 생성된다는 것을 보장할 수 없다.</li>
<li>자기 자신의 생성자를 private로 두었기 때문에 상속을 받는 것에 어려움을 가지게 된다.</li>
<li>싱글톤으로 만들어지는 클래스의 경우 기본적으로 자기 자신의 인스턴스를 관리하는 역할과 작업을 처리하는 2가지의 역할을 맡게 되므로 객체 지향의 5원칙 중 하나인 단일 책임 원칙을 위반한다.</li>
</ul>
<h2 id="그럼-언제-사용하는가">그럼 언제 사용하는가?</h2>
<ul>
<li>초기에 넣어야 할 데이터가 없을 때</li>
<li>데이터의 변경이 자주 일어나지 않을 때</li>
<li>독립적으로 호출할 수 있는 메소드 및 필드가 있는 클래스</li>
</ul>
<h2 id="그러면-멀티스레드에서는-싱글톤-패턴을-사용할-수-없는가">그러면 멀티스레드에서는 싱글톤 패턴을 사용할 수 없는가?</h2>
<p>아니다. 사용할 수 있다. 문제점이 있지만 이를 해결하는 다양한 방법이 있지만 다루지는 않겠다. (아직 필요성을 느끼지 못했다)</p>
<h2 id="object-키워드">object 키워드</h2>
<p>코틀린에서 싱글톤 패턴을 선언할 수 있도록 예약된 키워드다. 객체 선언의 초기화는 스레드로부터 안전하며 처음 접근할 때 완료된다.</p>
<h2 id="companion-object-키워드">companion object 키워드</h2>
<p>클래스의 인스턴스 없이 어떤 클래스 내부에 접근하고 싶다면 클래스 내부에 객체를 선언할 때 companion 식별자를 붙은 object를 선언하는 것이다. companion object는 클래스에서 한 개만 가질 수 있다.</p>
<p>companion object는 프로세스 시작 시 인스턴스가 생성되며 클래스가 사용되지 않아도 메모리상에 인스턴스가 계속 올라가있는 것이다.</p>
<h1 id="공부하면서-생긴-궁금한-점">공부하면서 생긴 궁금한 점</h1>
<blockquote>
<p>object나 companion object나 싱글톤 패턴을 구현하는 것이 같고 다른 점은 클래스 내에서 선언하냐의 차이 뿐인데 그렇다면 왜 굳이 클래스 내에서 선언을 해야하는가?</p>
</blockquote>
<h2 id="1-클래스와의-관계를-나타내기-위해서-사용한다">1. 클래스와의 관계를 나타내기 위해서 사용한다.</h2>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/72527fb7-9a72-472c-9509-7c78e2959210/image.png" alt=""></p>
<p>예를 들면 내가 작성한 이 코드와도 같다. Tag라는 클래스와 isTagError는 서로 관련이 있는 로직이기에 한 클래스에 묶는 것으로 사용할 수 있다.</p>
<h2 id="2-캡슐화와-관련이-있다">2. 캡슐화와 관련이 있다.</h2>
<p>companion object는 해당 클래스 내부에 정의되기 떄문에, 클래스의 private 생성자나 private 변수에 접근할 수 있다. 반면 obejct는 접근할 수 없다.</p>
<ul>
<li>제미나이 피셜) 팩토리 패턴이 companion object를 사용하는 예시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/e6217c78-c988-4cc5-bdc1-542fbb8aa449/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/285c9c9f-c194-4afe-99d0-a2a88de0688f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/4abd18cc-368a-4e6f-aff3-326390f9782e/image.png" alt=""></p>
<blockquote>
<p>Cannot access &#39;constructor(name: String, age: Int): Person&#39;: it is private in &#39;org.example.Person&#39;.</p>
</blockquote>
<p>위 코드를 보면 companion object와 object가 어디에서 다른지 눈으로 확인할 수 있다. 먼저 private 생성자에 대한 객체를 만들고자 하는 person2를 보면 에러가 나타난다. 이는 생성자가 비공개여서 접근할 수 없어 발생하는 오류이며 object또한 마찬가지고 같은 오류가 나타난다.</p>
<p>하지만 companion object는 다르다. 같은 클래스 내에 있는 객체이기 때문에 가시성이 비공개여도 생성자에 접근할 수 있다.</p>
<p>즉, person2, person3은 가시성이 비공개인 생성자를 통핸 객체를 생성할 수 없지만 person4는 생성할 수 있게 되는 것이다.</p>
<h1 id="결론">결론</h1>
<p>싱글톤 패턴이란 값을 가지지 않는 하나의 객체를 전역에서 다루고자 할 때 사용되는 디자인 패턴이며, 이 패턴을 사용했을 때는 메모리 상의 부담을 줄일 수 있고, 데이터를 전역에서 불러올 수 있다는 장점이 있다.</p>
<p>코틀린에서는 object와 companion object를 사용해 선언할 수 있으며 kotlin에서 사용하는 enum 클래스의 enum 객체 또한 싱글톤 패턴이 적용된 예시다.</p>
<p>부족한 부분이나 틀린 내용이 있다면 편하게 말씀해주시면 감사하겠습니다!</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
<blockquote>
<p>📚 참고자료
<a href="https://refactoring.guru/ko/design-patterns/singleton">https://refactoring.guru/ko/design-patterns/singleton</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] value class에 대하여]]></title>
            <link>https://velog.io/@noeyh_0j/kotlin-value-class%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@noeyh_0j/kotlin-value-class%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 08 Mar 2026 05:01:18 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>코틀린에 대해 깊이 파해치고자 하는 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 kotlin의 value class에 대해 정리해보겠습니다.</p>
<h1 id="value-class는-무엇인가">value class는 무엇인가?</h1>
<p>값을 클래스로 감싸 도메인 특화 타입을 만드는 것이 유용할 수 있지만, 이로 인해 추가적인 힙 메모리 할당으로 인한 런타임 오버헤드가 발생합니다. 더욱이 감싸는 타입이 기본형인 경우 성능 저하가 심각한데, 이는 기본형 타입은 런타임에 의해 집중적으로 최적화되는 반면, 감싸는 타입은 특별한 처리를 받지 못하기 때문입니다.</p>
<p>이런 문제를 해결하기 위해 <code>value class</code>가 등장한 것입니다. value class는 코틀린에서 지원하는 단 하나의 프로터피를 감싸는 클래스입니다.</p>
<h1 id="어떻게-사용하는가">어떻게 사용하는가?</h1>
<p>클래스 앞에 <code>value 키워드</code>를 붙이면 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">value class Password(private val s: String)</code></pre>
<p>인라인 클래스는 기본 생성자에서 초기화되는 단일 속성을 가져야 합니다. </p>
<pre><code class="language-kotlin">@JvmInline
value class Title(val content: String) {
    init {
        require(content.isNotBlank()) { throw IllegalArgumentException(&quot;[ERROR] 제목의 내용이 존재해야 합니다.&quot;) }
    }
}</code></pre>
<p>value class를 JVM 백엔드에서 사용하기 위해서는 <code>@JvmInline</code> 어노테이션을 작성해야 합니다.</p>
<h1 id="value-class의-특징은">value class의 특징은?</h1>
<p>일반 클래스처럼 <code>속성</code>과 <code>함수</code>를 선언할 수 있으며, <code>init 블록</code>과 <code>보조 생성자</code>를 가질 수 있습니다. </p>
<pre><code class="language-kotlin">@JvmInline
value class Person(private val fullName: String) {
    init {
        require(fullName.isNotEmpty()) {
            &quot;Full name shouldn&#39;t be empty&quot;
        }
    }

    constructor(firstName: String, lastName: String) : this(&quot;$firstName $lastName&quot;) {
        require(lastName.isNotBlank()) {
            &quot;Last name shouldn&#39;t be empty&quot;
        }
    }

    val length: Int
        get() = fullName.length

    fun greet() {
        println(&quot;Hello, $fullName&quot;)
    }
}

fun main() {
    val name1 = Person(&quot;Kotlin&quot;, &quot;Mascot&quot;)
    val name2 = Person(&quot;Kodee&quot;)
    name1.greet() // the `greet()` function is called as a static method
    println(name2.length) // property getter is called as a static method
}</code></pre>
<p>하지만 <code>backing field</code>를 가질 수 없으며, <code>lateinit 프로퍼티</code> 를 사용하지 않습니다.</p>
<p>인터페이스 상속은 가능하지만 다른 클래스를 상속할 수 없으며 항상 <code>final</code>입니다. </p>
<h1 id="data-class와는-무엇이-다른가">data class와는 무엇이 다른가?</h1>
<p>value class는 <code>val만 허용</code>하며, 이도 <code>하나만 사용</code>하도록 제한됩니다. 반면 data class는 <code>val, var 모두 사용</code>이 가능합니다.</p>
<p>data class는 데이터 관리를 위해 equals, toString, hashCode, copy, componentN 등을 자동으로 생성해주지만, value class는 <code>equals</code>와 <code>hashCode</code>만을 자동으로 생성합니다.</p>
<p>즉, data class는 데이터 보관 및 관리에 초점을 두어 <code>여러 데이터를 관리하거나 비교할 때</code> 주로 사용하고, value class는 래핑을 사용한 성능 최적화를 초점으로 두기 때문에 <code>기본 타입 하나를 감싸 성능 최적화를 하고싶을 때</code> 주로 사용합니다.</p>
<h1 id="참고자료">참고자료</h1>
<blockquote>
<p><a href="https://kotlinlang.org/docs/inline-classes.html#representation">Inline value classes[코틀린 공식 문서]</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[유연함의 힘] 4장 내면의 과학자를 깨워라]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-4%EC%9E%A5-%EB%82%B4%EB%A9%B4%EC%9D%98-%EA%B3%BC%ED%95%99%EC%9E%90%EB%A5%BC-%EA%B9%A8%EC%9B%8C%EB%9D%BC</link>
            <guid>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-4%EC%9E%A5-%EB%82%B4%EB%A9%B4%EC%9D%98-%EA%B3%BC%ED%95%99%EC%9E%90%EB%A5%BC-%EA%B9%A8%EC%9B%8C%EB%9D%BC</guid>
            <pubDate>Wed, 04 Mar 2026 13:59:28 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>실험 정신이 되살아난 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 유연함의 힘 4장 &#39;내면의 과학자를 깨워라&#39;에 대한 내용을 정리해보겠습니다.</p>
<hr>
<h1 id="학습-목표를-추구하는-동안-새로운-행동을-시도하라">학습 목표를 추구하는 동안 새로운 행동을 시도하라</h1>
<p>유연성이란 다른 무언가를 시도하는 능력을 말합니다. 더 나아가 학습을 극대화하는 지름길은 특정한 실험을 계획하고 행동하는 것입니다.</p>
<p>특정 경험을 하는 동안 계획한 실험을 수행한다면 그 일을 잘하건 못하건 실험 결과를 얻는다. 실험 결과가 긍정적이라면 당연히 좋은 것이고, 실험 결과가 부정적이더라도 그 행동이 부정적이라는 사실을 알 수 있게 됩니다.</p>
<p>목표를 성취하기 위해 실험을 시도하는 일은 매우 중요합니다. 목표까지 완주하는 일은 그 자체로도 어렵지만 이것을 더 어렵게 만드는 조건들이 있습니다.</p>
<p>계획을 수립하고 얼마간 시간이 흐른 뒤에 그 계획을 실행해야 하는 경우와 도전적인 경험의 한복판에서 계획을 끝까지 실행해야 할 때 등이 있습니다.</p>
<p>그런 경험에서 무언가를 배울 확률이 큰데, 이를 피하지 않으려면 어떻게 해야할까? 가 이번 장의 큰 주제입니다.</p>
<p>이 책에서는 스트레스를 유발하는 어려운 활동에서 유익하고 귀중한 교훈을 최대한 얻어내기 위해 무엇이든 시도하라 합니다.</p>
<p>유연함의 기술을 시도할 때 실험은 필수적입니다. 아주 작은 변화라도 과거에 해온 행동과 다르기만 하다면 무엇이든 상관 없습니다. 실험의 목적 자체는 ‘새로운 무언가를 시도하는 것’이기 때문입니다. 이 실험을 하는 이유는 기존의 안전지대에서 빠져나와 무언가를 실제로 개선할 수 있다는 사실을 스스로 확인하는 것입니다.</p>
<p>익숙하지 않은 무언가를 시도할 때 약간의 불편함을 느끼는 것은 자연스러운 부작용입니다. 즉, 불편함은 성장에 반드시 따르는 부산물이라는 의미입니다.</p>
<p>학습하고 성장하기 위해 목표를 설정했다고 합시다. 그치만 어떻게 해야할지는 모를 것이기에 이 때는 시도해 볼 만한 실험을 한 가지 이상 상상해보는 것입니다. 복잡하고 도전적인 목표를 달성하기 위해 필요한 행동은 종종 명확하지 않기 때문에 실험은 목표와 실천을 연결하는 가교가 되어줍니다.</p>
<p>학습에 대한 실험은 과학 실험과 동일하게 시행착오 과정입니다. 모든 시도는 문제를 새로이 통찰하게 하며 오류조차도 유익한 정보를 제공합니다. 성공 여부를 확신하지 못해도 열린 마음으로 무언가를 시도해야 합니다. 실패를 보는 관점을 재구성할수록 상황은 더욱 좋아집니다.</p>
<p>유연성 강화 실험의 단계는 과학 실험과 비슷합니다.</p>
<ul>
<li>새로운 방식을 시험한다.</li>
<li>새로운 행동이 학습 목표를 달성하는 데 도움이 되는지를 확인한다.</li>
<li>모든 단계를 계속 반복하고, 반복할 때마다 새로운 점을 배운다.</li>
</ul>
<p>다만 과학 실험과 다른 점은 상황에 맞추어 일부 규칙을 무시할 수도 있는 것입니다. 유연성 강화 실험의 목표는 새로운 개념, 이론을 찾는 것이 아닌 원하는 삶을 살기 위해 필요한 일상적인 전략을 찾는 것에 의미를 둡니다.</p>
<p>유연함의 기술이 포함된 실험은 철저히 개인적이며, 개인이 만족하는 결과에 따라 유연하게 실험을 바꿀 수 있습니다. </p>
<p>유연성 강화 실험의 장점은 학습하고 성장할 때 주도권을 가질 수 있다는 것입니다.</p>
<hr>
<h1 id="실험을-어떻게-계획할까">실험을 어떻게 계획할까</h1>
<p>유연함의 기술을 일환으로 실험을 계획하기 위해서는 먼저 개인의 리더십 기술과 개인적 효율을 발달시키기 위해 어떤 노력을 할 수 있을지 아이디어를 떠올리는 것입니다.</p>
<p>그런 다음 미래의 경험을 이용해 그 아이디어를 어떻게 시험할지 상상하는 것입니다. 실험의 마지막 단계에서는 성공 여부를 판단해야 합니다. 이 아이디어가 옳았는지 틀렸는지 판단할 때 사용할 근거를 미리 결정해야 합니다.</p>
<p>새로운 무언가를 시도해서 추이를 지켜본 다음 피드백과 결과에 기초해 다음 단계를 결정하는 것은 유연함의 기술이 추구하는 전략입니다.</p>
<p>만약 실험 아이디어가 잘 생각나지 않을 때는 친구, 동료, 멘토, 코치를 적극적으로 활용하면 좋습니다.</p>
<hr>
<h1 id="어떤-완벽주의자의-열-가지-실험-훈련법">어떤 완벽주의자의 열 가지 실험 훈련법</h1>
<ul>
<li>자기 인식력 재고</li>
<li>자기 연민하기</li>
<li>파멸적인 생각 멈추기</li>
<li>새롭게 프레이밍하기</li>
<li>내려놓기</li>
<li>삶을 지속적인 하나의 실험으로 생각하기</li>
<li>피드백 구하기</li>
<li>즉흥적으로 해보기</li>
<li>믿기</li>
<li>아군 만들기</li>
</ul>
<hr>
<h1 id="실험을-가로막는-심리적-장애물">실험을 가로막는 심리적 장애물</h1>
<p>실험 때문에 사람들이 걱정하는 경우는 크게 두 가지가 있습니다.</p>
<ul>
<li>일관성에 대한 걱정</li>
<li>잠재적인 실패</li>
</ul>
<p>이에 관한 실험이 있습니다. 장기간에 걸쳐 일관된 행동 방침을 따르는 리더와 행동 방침이 일관되지 못한 리더가 각 과제를 성공적으로 수행했는지 실패했는지에 대한 정보를 가지고 사람들이 생각하는 리더의 성과를 점수로 매기는 실험입니다.</p>
<p>일관적으로 행동하지 않았어도 성공적인 결과를 이끌어 낸 리더는 상당히 좋은 평가를 받았으며, 반대로 일관적으로 행동했으나 과제에서 실패한 리더는 낮은 평가를 받았습니다.</p>
<p>이 실험의 결과를 보고 방식과 전략을 수시로 바꿔 줏대 없어 보이는 리더일지라도 결과가 좋으면 좋은 평가를 받을 수 있다는 것을 알 수 있습니다.</p>
<p>즉, 실패를 두려워한다면 실질적인 위험이 따르지만 그 위험은 충분히 관리할 수 있습니다.</p>
<hr>
<h1 id="실패를-대하는-올바른-자세">실패를 대하는 올바른 자세</h1>
<p>특정 경험에서 추구하고 싶은 유연성 강화 목표가 있을 때 전문가들은 ‘실행 의도’라고 부르는 무언가를 설정하라 말합니다. 실행 의도는 자신을 목표에서 이탈하게 만들 잠정적 사건을 깊이 고려하고 그런 일이 벌어졌을 때의 행동 수칙을 미리 계획하는 행위를 말합니다. 실행 의도는 두 가지로 구성된 계획입니다.</p>
<ul>
<li>실험에 걸림돌이 될 수 있는 우발적인 장애물</li>
<li>그 장애물에 효과적으로 대응하는 방법</li>
</ul>
<p>이런 실행 의도를 수립하면 세 가지 큰 이득이 있습니다.</p>
<ul>
<li>직면할 장애물이 무엇인지 명백히 드러난다.</li>
<li>사전 계획에 근거해 고려할 선택지가 압축된다.</li>
<li>자동적, 목표 지향적, 성공적으로 행동할 확률이 커진다.</li>
</ul>
<p>단, 유연성 강화 실험에서 첫술에 배부르기를 기대하면 안됩니다. 어떤 것이든 하나의 실험에서 배울 수 있는 교훈은 한정되어 있습니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>3장에서 목표를 설정하는 방법에 대해 배웠다면 4장에서는 목표를 어떻게 수행하는지에 관한 내용이었습니다.</p>
<p>많은 목표를 세워도 제대로 수행하지 못했던 이유가 이 책에서 말한 것처럼 내가 꾸준하게 일관적으로 할지에 대한 걱정과 만약 실패하면 어쩌지에 대한 불안함이 큰 작용을 한 것 같습니다.</p>
<p>하지만 이번 장을 읽으면서 일관적이지 않더라도 결과가 성공적이라면 좋은 평가를 받는다는 실험 결과를 보고 일단 시도하는 것이 중요하구나를 느꼈습니다. 또한 이 책에서 말했듯이 실패에 대한 위험은 내가 관리할 수 있는 영역이기 때문에 실패에 대한 걱정으로 시도하지 않는 것보다 훨씬 좋은 영향을 줄 것이라는 믿음이 생겼습니다.</p>
<p>실패에 대한 결과도 앞으로 자신의 길을 찾는데 고르지 말아야 할 선택지를 걸러준다는 긍정적인 결과를 가져온다는 말을 보고 실험을 하지 말아야 할 이유가 없다고 느꼈습니다.</p>
<p>앞으로의 목표에 대해 잘 수립하고 실험을 통해 발전해나가는 모습을 꿈꾸며 이번 글을 마치겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[유연함의 힘] 3장 성과와 성장, 두 마리 토끼를 모두 잡는 법]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-3%EC%9E%A5-%EC%84%B1%EA%B3%BC%EC%99%80-%EC%84%B1%EC%9E%A5-%EB%91%90-%EB%A7%88%EB%A6%AC-%ED%86%A0%EB%81%BC%EB%A5%BC-%EB%AA%A8%EB%91%90-%EC%9E%A1%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-3%EC%9E%A5-%EC%84%B1%EA%B3%BC%EC%99%80-%EC%84%B1%EC%9E%A5-%EB%91%90-%EB%A7%88%EB%A6%AC-%ED%86%A0%EB%81%BC%EB%A5%BC-%EB%AA%A8%EB%91%90-%EC%9E%A1%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Mon, 02 Mar 2026 11:18:18 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>한층 더 유연한 목표를 정하고 싶은 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 유연함의 힘 3장 &#39;성과와 성장, 두 마리 토끼를 모두 잡는 법&#39;에 대한 내용을 정리해보겠습니다.</p>
<hr>
<h1 id="유연성-강화-목표란-무엇인가">유연성 강화 목표란 무엇인가</h1>
<p>도전, 변화, 잠재력 성장의 기회가 찾아왔을 때 유연성 강화라는 목표를 세우면 그 경험을 더욱 효과적으로 활용해 성과와 성장이라는 두 마리 토끼를 다 잡을 수  있습니다.</p>
<p>유연성 강화 목표는 개선하고 성장시키고자 하는 영역에서 시작됩니다. 유연성 강화 목표를 무엇으로 삼건 중요한 점은 다가올 경험과 관련해 목표를 세우고, 목표를 달성하기 위해 노력하는 것입니다.</p>
<p>유연성 강화 목표를 세운다는 말은 완벽하지 못한 자신의 여러 측면을 인정하고 그중 하나를 집중적으로 개선하겠다는 뜻입니다. 유연성 강화 목표는 기업 목표, 스마트 목표와 차별화되는 중요하고 독특한 여러 특징이 있습니다.</p>
<ul>
<li>자신이 직접 정한다.</li>
<li>성취가 아니라 학습과 관련 있다. 즉, 구체적인 직무나 그 직무를 수행하는 더 나은 방법과 관련해 배워야 하는 것은 전혀 고려하지 않습니다.</li>
</ul>
<hr>
<h1 id="환상을-목표로-전환하라">환상을 목표로 전환하라</h1>
<p>목표는 계층적입니다. ‘좋은 사람이 되고 싶다.’, ‘가족이나 이웃에게 헌신하고 싶다.’ 처럼 가치관에 기반하는 상위 목표가 있는가 하면 ‘직장 동료와의 관계를 개선하고 싶다.’, ‘이웃을 더 많이 도와주고 싶다.’ 같은 하위 목표도 있습니다.</p>
<p>‘계층’이라는 말에서 유추할 수 있듯 하위 목표는 상위 목표를 달성하기 위한 수단이라고 볼 수 있습니다.</p>
<p>자신이 이루고 싶은 목표를 글로 적는 것도 매우 효과적입니다. 단, 어떤 표현을 사용하는지가 매우 중요합니다.</p>
<p>상상력과 연상 작용을 자극하는 표현일수록, 상상하는 미래의 이미지가 생생할수록 설득력도 커집니다.</p>
<p>다른 사람을 관찰함으로써 <strong>희망 목표</strong>를 결정할 수도 있습니다. 타인의 행동에 근간이 되는 목표를 추론해서 그것을 자신의 목표로 설정하는 방법은 목표 설정법 중에서도 일반적 방법입니다. 이런 식으로 목표가 널리 퍼지는 현상을 <strong>목표 전염</strong>이라고 부릅니다.</p>
<p>유연성 강화 목표를 설정하는 일은 만만찮은 도전이지만 목표 전염이 그것을 가능하게 해줍니다.</p>
<p>물론 롤 모델이 없이도 목표를 설정할 수 있습니다. 이 훈련법을 ‘<strong>최상의 자아 재발견</strong>’이라고 명명했습니다. 최상의 자아를 재발견하기 위해서는 먼저 주변 사람들에게 당신의 가장 멋진 모습이 무엇인지 알려달라고 요청해야 합니다. </p>
<p>재발견된 최상의 자아는 대담한 성장을 열망하게 만드는 촉매제가 될 뿐 아니라 성장을 이루기 위한 방향도 제시합니다. 또한 이 모든 과정은 이미 보유하고 있는 능력과 자질을 기본으로 합니다.</p>
<hr>
<h1 id="현재의-고통을-목표로-승화시켜라">현재의 고통을 목표로 승화시켜라</h1>
<p>희망 목표가 미래를 향한 환상에서 시작되었다면 ‘<strong>회피성 목표</strong>’는 우리가 현재 느끼는 고통에서 파생합니다. </p>
<p>심리적이든 신체적이든 고통에서 목표를 찾으면 자신은 물론 주변에 안겨주는 고통을 줄이기 위해 변화를 모색합니다. 이러한 이유로 미래 경험에서 이루고 싶은 구체적인 목표를 수립한다면 고통은 성장을 촉발하는 강력한 자극제가 됩니다.</p>
<hr>
<h1 id="희망과-고통을-하나의-목표에-담아내다">희망과 고통을 하나의 목표에 담아내다</h1>
<p>가끔 우리는 열망과 고통 회피에서 더 나은 미래를 향한 환상과 현재의 고통에서 벗어나고픈 욕구를 모두 반영한 목표를 유연성 강화 목표를 설정합니다. 이런 혼합형 목표는 매우 열정적이고 지속적인 노력을 유발합니다.</p>
<p>현재의 부정적인 측면에 집중하는 동시에 미래를 긍정적이고 환상적으로 그리는 사람은 변화에 방점을 둔 목표를 달성하기 위해 최선을 다하는 경향을 보입니다.</p>
<p>그러나 그런 노력이 저절로 목표 지향적인 행동으로 전환되지 않으며, 목표를 이루기 위한 실천 계획을 수립하는 경우에만 그렇게 될 수 있습니다.</p>
<hr>
<h1 id="유연성-강화-목표는-유연하다">유연성 강화 목표는 유연하다</h1>
<p>유연성 강화 목표는 여러 얼굴을 갖습니다.</p>
<ul>
<li>단순하고 직접적이다.</li>
<li>상당히 구체적일 수 있다.</li>
<li>비교적으로 복잡하고 미묘한 뉘앙스가 포함된 목표도 있다.</li>
</ul>
<hr>
<h1 id="사례로-알아보는-보편적인-유연성-강화-목표">사례로 알아보는 보편적인 유연성 강화 목표</h1>
<p>그렇다면 유연성 강화 목표를 쉽게 도출하는 방법은 무엇일까요? 이 책에서는 머릿속에 제일 먼저 떠오르는 생각이 앞으로 추구해야 할 목표일 확률이 크다고 말합니다.</p>
<p>대부분의 사람은 자신의 문제가 무엇이고 무엇을 개선해야 하는지 잘 알기 때문입니다.</p>
<p>가치 있는 유연성 강화 목표가 무엇인지 찾기 어려워한다면 어떤 일을 하는 동시에 개인적으로 어떤 점을 성장시킬 수 있을까? 내용 중심 목표와 개인적인 유연성 강화 목표를 구분한 다음 두 가지 목표를 한꺼번에 다루는 법을 알아내는 것은 유연함의 기술을 이용할 때 필요합니다.</p>
<p>반면 목표가 너무 많을 때도 있습니다. 이런 상황에서는 선택과 집중이 관건입니다. 만약 여러 목표를 동시에 추구한다면 주의가 흐트러져 어디에도 집중하지 못할 확률이 큽니다.</p>
<p>너무 모호해서 유연성 강화 목표를 삼기에 가치가 없는 것들도 있습니다. 이 목표를 더 구체적으로 표현하려면 정확히 어떤 대인관계 기술을 얻고 싶고 이를 위해 언제 집중할지 정해야 합니다. 구체성은 목표를 훨씬 유용하게 만들어줍니다.</p>
<p>반면 지나치게 구체적인 목표를 수립할 수도 있습니다. 이런 목표는 자신이 개발하고 싶은 기술이 아니라 전술을 설명한 것에 지나지 않습니다.</p>
<p>목표가 충분히 구체적인지 혹은 지나치게 구체적인지 확인하는 방법은 ‘내 목표 선언문 그대로 누군가에게 달성하라고 한다면 그 사람은 무엇을 어떻게 해야 하는지 이해할 수 있을까?’라고 자문하면 됩니다. 반대로 목표가 지나치게 구체적인지 알고 싶을 때는 ‘내 목표 선언문의 내용을 달성한다면 정말로 원하는 능력을 얻을 수 있을까?’라고 자문하면 됩니다.</p>
<p>둘 중 어느 하나라도 부정적인 대답이 나온다면 적절한 목표를 세우도록 더 노력해야 합니다.</p>
<hr>
<h1 id="목표를-더-세밀하게-조정하라">목표를 더 세밀하게 조정하라</h1>
<p>목표를 설정했다면 이번에는 두 가지 일을 해야 합니다.</p>
<ul>
<li>목표 설명 방식을 점검하라.</li>
</ul>
<p>개선 과정을 중심으로 표현된 목표는 동기와 추진력을 부여하는 효과가 있습니다. 또한 우리가 성장 마인드셋을 유지할 때도 도움이 됩니다. 반대로 ‘잘하다’, ‘최고가 되다’처럼 구체적인 최종 상태를 설명하는 문구는 피해야 합니다. </p>
<ul>
<li>최종 목적지보다 여정의 측면에서 목표를 표현하라.</li>
</ul>
<p>한 연구 결과를 보면 ‘목표를 향해 나아가는 과정이라고 생각할수록 그 목표를 계속 추구할 가능성이 커진다.’라고 합니다. 목표 자체를 하나의 과정이라고 표현함으로써 도중에 겪는 성공과 실패를 포함한 학습 과정을 전체적인 맥락으로 바라보았고, 이는 결국 전반적인 성장에 대한 만족감으로 발현되었습니다.</p>
<hr>
<h1 id="헌신이-차이를-만든다">헌신이 차이를 만든다</h1>
<p><strong>헌신</strong>은 ‘목표를 달성하려는 결의에 찬 투지’, ‘그 과정에 노력을 아끼지 않겠다는 굳은 의지’, ‘목표를 이루기 위한 열정적이고 부단한 노력’ 등을 의미합니다. 실제로 유연성 강화 목표를 추구하는 과정에서 헌신이 가장 중요한 요소라는 점을 증명한 연구도 있습니다.</p>
<p>경험이 최고의 스승이라는 말이 있지만, 경험에는 부정적인 면도 있습니다. 주의를 분산하게 하는 수많은 요소도 양산한다는 것입니다. </p>
<p>하지만 헌신하고자 하는 학습 목표를 경험에 결합함으로써 집중력을 유지할 수 있습니다.</p>
<ul>
<li>목표와 장애물을 미리 생각하라</li>
<li>단순한 헌신을 넘어 몰입으로</li>
<li>목표를 알려라</li>
</ul>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>3장은 유연성 강화 목표가 무엇인지, 어떻게 목표를 잡아야하는지에 대한 내용이였습니다.</p>
<p>이번 장에서는 목표를 조정할 때 최종 목적지보다 과정의 측면에서 목표를 표현하라는 내용이 가장 인상깊었습니다. 지난 세월동안 목표를 세웠었고 그 중에 당연히 실패한 목표도 많았습니다. 이 실패한 목표와 책의 내용을 생각하며 읽어보니 번뜩였습니다.</p>
<p>가장 많이 실패한 목표로는 역시 다이어트입니다. 다이어트를 할 때는 항상 5키로만 빼자, 이런 식으로 최종 목표에 기반하는 목표를 정했었습니다. 하지만 이것은 잘못된 목표 설정 방법이였습니다. 이 책에서 말한 데로 다이어트 목표를 정한다고 하면 &#39;하루에 1시간씩 러닝하기&#39; 등과 같이 움직이는 과정을 목표로 설정해야 했습니다.</p>
<p>앞으로 살면서 많은 목표를 정해야 할 일이 생길텐데 지금부터 연습해보며 목표를 잘 정의해보며 스스로도 성장할 수 있는 사람이 되고 싶습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[유연함의 힘] 2장 학습을 부르는 마인드셋]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-2%EC%9E%A5-%ED%95%99%EC%8A%B5%EC%9D%84-%EB%B6%80%EB%A5%B4%EB%8A%94-%EB%A7%88%EC%9D%B8%EB%93%9C%EC%85%8B</link>
            <guid>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-2%EC%9E%A5-%ED%95%99%EC%8A%B5%EC%9D%84-%EB%B6%80%EB%A5%B4%EB%8A%94-%EB%A7%88%EC%9D%B8%EB%93%9C%EC%85%8B</guid>
            <pubDate>Sun, 01 Mar 2026 11:20:57 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>배움을 추구하는 개발자 꿈나무 김조현입니다.</p>
<p>이번 글은 유연함의 힘 2장 &#39;학습을 부르는 마인드셋&#39;의 내용을 정리해보겠습니다.</p>
<hr>
<h1 id="경험을-이해하는-두-가지-시선">경험을 이해하는 두 가지 시선</h1>
<p>경험을 프레이밍할 때 가장 많이 사용되는 방법은 <strong>성과 증명 마인드셋</strong>입니다. 이는 도전 상황을 마주했을 때 본인의 능력과 기술을 자신은 물론 주변에까지 증명하려는 목표를 가지고 행동합니다.</p>
<p>하지만 성과 증명 마인드셋을 지닌 사람은 실패가 예상되는 상황을 피하고 자신의 능력과 성과를 증명하는 일에만 지나치게 집중하기 때문에 때때로 성과를 강화하기보다 성과를 갉아먹는다는 문제가 있습니다.</p>
<p>또한 성과 증명 마인드셋은 학습을 촉진하기는커녕 오히려 학습에 도움이 되지 않습니다. </p>
<p>무지함을 드러내고 싶지 않아 질문을 피한다던지 부정적인 말을 듣고싶지 않아 피드백을 거부하는 등 능력을 증명하는 일에만 온전히 집중하면 장기적으로 자신에게는 도움이 될 행동은 철저히 뒷전으로 밀려날지도 모릅니다.</p>
<p>이와 반대되는 프레이밍 방법을 이 책에서는 <strong>학습 마인드셋</strong>이라고 정의합니다.</p>
<p>학습 마인드셋을 지닌 사람은 모든 상황에서 잠재되어 있는 학습의 기회를 찾으려고 합니다. 학습 성향의 사람은 장기간에 걸쳐 자신의 기술을 발달시키고 늘 과거의 자신보다 더 나아지려고 노력합니다.</p>
<p>즉, 성과 증명 마인드셋과 반대로 자연스레 질문을 하고, 새로운 무언가를 시도하고, 가정에 이의를 제기하고, 주변에 도움과 조언을 구하는 등 실제 학습과 기술 개발로 이어지는 행동을 촉발합니다.</p>
<p>정리하자면 마인드셋은 결과물인 성과 자체가 아니라 일과 삶에서 성과를 어떻게 만들어 내는가 하는 태도에 가깝습니다.</p>
<hr>
<h1 id="학습과-성취와-대인관계는-마음먹기에-달렸다">학습과 성취와 대인관계는 마음먹기에 달렸다</h1>
<p>이 책에서 소개한 해나라는 인물은 가장 경험이 많은 두 선생님이 참관 수업을 하며 평가를 한다는 것을 알려줍니다. 이 때 해나가 성과 증명 마인드셋을 가지고 있다면 평가자들을 잠재적인 위협자라고 인식할 확률이 크며 이를 시작으로 악순환이 시작될 것입니다.</p>
<p>하지만 학습 마인드셋을 가지게 된다면 평가 받는 상황을 위기가 아니라 기회로 생각할 것입니다. 교사들이 해주는 피드백을 꺼리지 않고 자신이 교사로서의 역량을 발달시키기 위해 사용할 수 있는 조언으로 받아들일 것입니다. 여기까지만 봐도 배울 수 있는 확률이 클 것 같지 않나요? 또한 불안함이 줄어든 만큼 평가 시간에 더 자신 있는 모습을 보여줄 수 있습니다.</p>
<p>이는 학습 마인드셋이 주는 가장 중요한 보상입니다. 학습에 초점을 맞추면 자신의 기존 지식을 증명하는 일보다 지식 창고를 불리는 데에 더욱 집중하기 마련입니다.</p>
<p>이 사례를 보면 학습 마인드셋을 취한다면 더 큰 성장을 단기간에 할 수 있습니다. 반면 얼마나 뛰어난지 증명하는 데에만 몰두한다면 오히려 역효과를 일으킬 수도 있습니다. 지나친 욕심은 화를 부릅니다. 능력을 증명하려 너무 열심히 노력하는 것은 외려 그 능력을 최악으로 떨어뜨리는 경향이 있습니다. </p>
<p>결론적으로 마인드셋은 중요합니다. </p>
<hr>
<h1 id="마인드셋은-선택할-수-있다">마인드셋은 선택할 수 있다</h1>
<p>어떤 마인드셋을 주 성향으로 선택하든 그것은 고정불변이 아닙니다. 언제든 마인드셋을 바꿀 수 있습니다. </p>
<p>유연함의 기술을 활용하려면 특정 상황마다 마인드셋을 유연하게 바꿀 수 있어야 합니다.</p>
<p>유연성을 강화하는 첫 단계는 곧 다가올 경험을 어떻게 생각하는지 명확히 분석하는 일에서 시작합니다. 곧 다가올 경험이 시험이라고 생각하는지 아니면 새로운 무언가를 배울 수 있는 기회라고 생각하는지 이것을 명확히 분석하면 유연함의 기술을 활용해 학습 성향으로 마인드셋을 전환할 수 있습니다.</p>
<p>1장에서 학습과 성장에 도움을 주는 경험의 여섯 가지 특성을 소개했습니다. 우리는 이런 특성을 품은 도전을 맞닥뜨렸을 때 더 익숙한 성과 증명 마인드셋에 의존하기 쉽지만 역설적이게도 이런 상황일수록 학습 마인드셋을 선택해야 합니다. 학습 마인드셋에는 특별한 보상이 따르기 때문입니다.</p>
<p>리더십 기술을 배우려 노력할 때만 학습 마인드셋이 유익한 것은 아닙니다. 학습 마인드셋은 개인의 삶에서도 매우 유익한 도구입니다.</p>
<hr>
<h1 id="마인드셋은-어떻게-바꿀-수-있을까">마인드셋은 어떻게 바꿀 수 있을까</h1>
<p>마인드셋은 어떻게 바꿀 수 있을까? 일단 자신과의 내적 대화에 집중하는 방법을 추천해줍니다. 내적 목소리를 바꾸기 위해 ‘나는 완벽하지 않아. 나는 내가 불완전하다는 사실을 인정해. 나는 여전히 진행형이야.’ 처럼 자기 주문을 외우는 것입니다. </p>
<p>이런 주문을 통해 잠재적인 자아를 정립한다면 일관된 자아감을 제공하고 성장 가능성을 허용하는 새로운 정체성을 내면화한 상태가 됩니다.</p>
<p>성과 증명 마인드셋에서 학습 마인드셋으로 옮겨 가기 위해 사용할 수 있는 또 다른 방법은 자신의 깊이를 연민하는 것입니다. 학습 마인드셋은 좌절이나 실패에 직면했을 때 특히 중요한 역할을 합니다.</p>
<p>그런 상황일수록 자신에게 친절하고 관대해져야 합니다. 구체적으로 지금의 난관이 무언가를 배우고 성장할 기회라는 사실을 상기하고, 예전에 넘어졌다가 다시 일어난 경험을 떠올리면 됩니다. 학습 마인드셋은 불안을 누그러뜨리고 자신감을 끌어올리며 더욱 개방적이고 적극적으로 무언가를 탐색하게 만듭니다. </p>
<p>학습 마인드셋을 가지는 것이 유연함의 기술의 기본입니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 마인드셋을 강조했습니다.</p>
<p>대부분 우리는 회사 또는 학교에서 성적, 성과를 요구하기 때문에 또는 내가 더 높은 실적을 쌓기 위해 같은 이유로 성과 증명 마인드셋을 가지고 있습니다. 하지만 이는 성과라는 한정된 목표만 바라보기 때문에 좁은 시야를 가지며, 자신을 깎아내리기를 꺼리며, 완벽을 추구하려고 하는 경향이 강합니다. </p>
<p>이런 마인드셋은 학습에서 좋은 결과를 가져오지 못하고 오히려 금방 지치고 포기하게 만듭니다. 이 책에서 학습할 때는 학습 마인드셋이라는 프레임을 착용하는 것을 권유합니다. 학습 마인드셋은 자신의 부족한 부분을 인정하며 더 성장한 자신의 모습을 바라보는 것을 목표로 설정하기 때문에 학습 과정에서 질문하고, 피드백하는 등의 행동은 자연스럽게 자신의 성장을 도와주게 됩니다.</p>
<p>비록 내가 성과 증명 마인드셋을 가지고 있다 하더라도 이 책에서는 자유롭게 마인드셋을 바꿀 수 있어, 학습할 때는 학습 마인드셋을 착용할 수 있다고 얘기합니다.</p>
<p>이번 2장을 다양한 사례들과 함께 읽어보며 학습 마인드셋이 어떤 영향을 미치는지 느낄 수 있었습니다.
저 또한 어려운 도전을 할 때 항상 마음속으로 &#39;어려워봤자 얼마나 어렵겠어. 못하면 어때, 거기서 무언가 얻을 수 있겠지. 이 때도 잘 이겨냈잖아, 이번에도 할 수 있을거야&#39;같은 말을 되뇌이는데, 이런 내용이 책에 적혀있어 무척 놀랐었습니다.</p>
<p>학습 마인드셋을 착용하는 방법에 대해 배워봤으니 이를 실천해보며 저만의 유연함의 기술을 익힐 수 있으면 좋겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[유연함의 힘] 1장 경험은 가장 훌륭한 스승]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-1%EC%9E%A5-%EA%B2%BD%ED%97%98%EC%9D%80-%EA%B0%80%EC%9E%A5-%ED%9B%8C%EB%A5%AD%ED%95%9C-%EC%8A%A4%EC%8A%B9</link>
            <guid>https://velog.io/@noeyh_0j/%EC%9C%A0%EC%97%B0%ED%95%A8%EC%9D%98-%ED%9E%98-1%EC%9E%A5-%EA%B2%BD%ED%97%98%EC%9D%80-%EA%B0%80%EC%9E%A5-%ED%9B%8C%EB%A5%AD%ED%95%9C-%EC%8A%A4%EC%8A%B9</guid>
            <pubDate>Sat, 28 Feb 2026 12:21:51 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>유연함의 힘을 읽고 소프트 스킬에 대한 흥미를 가지게 된 개발자 꿈나무 김조현입니다.</p>
<p>우테코에서 읽어오라는 과제가 있었기에 반강제로 읽기 시작하게 된 책이지만 추천해준 것에는 이유가 있다고 생각하기 때문에 제대로 읽고 정리해볼 생각입니다.</p>
<p>이번 글에서는 유연함의 힘 1장 &#39;경험은 가장 훌륭한 스승&#39;의 내용을 정리해보겠습니다.</p>
<hr>
<h1 id="행동하면서-배워라">행동하면서 배워라</h1>
<p>대다수의 리더는 리더십의 핵심 내용을 책상이 아닌 <strong>경험</strong>에서 얻습니다. 책의 앞에서 설명한 제프 파크스의 일화를 읽어보면서도 느낄 수 있습니다. </p>
<p><strong>소프트 스킬</strong>은 기본 원칙이 단순명료해서 누구나 쉽게 이해할 수 있지만, 이를 잘 활용하는 것은 어렵습니다. 실제 업무 상황에서 사용하려면 더욱 어려울 것이고요.</p>
<p>그치만 도전 상황에서 해당 기술을 직접 활용하는 것 외에는 방법이 없습니다. 그런 경험에서 얻은 구체적인 교훈은 머릿속 깊이 각인되어 도전 상황에 직면할 때마다 떠올리게 됩니다.</p>
<p>이 책을 읽으면서 <strong>70-20-10 법칙</strong>을 알게 되었습니다. 좋은 관리자가 되는 방법의 70%는 경험으로 배웠으며, 20%는 멘토나 동료 등에게 배웠고 10%는 책이나 수업 등 책상에서 배운 사람들의 대략적인 비율입니다.</p>
<p>즉, 리더십을 키우기 위해 또는 교훈을 배우기 위해서는 귀중한 교훈을 얻을 수 있는 업무를 직접 부딪치며 성장하는 것입니다.</p>
<p>그치만 이 접근법은 또 다른 질문을 유발합니다. 의미 있는 교훈을 얻을 확률이 가장 큰 업무는 정확히 어떤 것일까? 리더십 연구가들은 이 답을 찾기 위해 노력했으며, 학습을 촉발한 경험을 몇 가지 범주로 묶을 수 있었습니다.</p>
<p>리더십 연구가들은 도전 의욕을 크게 자극하고 학습 효과가 큰 경험은 여섯 가지 뚜렷한 특징을 보인다고 주장했습니다.</p>
<hr>
<h2 id="익숙하지-않은-책임을-떠안아라">익숙하지 않은 책임을 떠안아라</h2>
<p>익숙하지 않은 책임이 포함된 경험은 성장할 수 있는 커다란 기회가 됩니다.</p>
<p>처음 대면하는 상황을 다루어야 할 때, 안전지대에서 벗어나 새로운 행동을 시도하고 다양한 방식으로 목표에 접근하라고 합니다.</p>
<hr>
<h2 id="변화를-주도하라">변화를 주도하라</h2>
<p>변화를 주도하는 일은 미래의 많은 리더가 맞닥뜨릴 가장 도전적인 상황이자 가장 많이 배울 수 있는 경험 중 하나입니다. 단, 변화에 성공하려면 몇 가지가 선행되어야 합니다.</p>
<ul>
<li>현재 상태가 어떤지 면밀히 진단해야 한다.</li>
<li>어째서 이런 상태가 되었는지 철저히 조사해야 한다.</li>
<li>그  변화를 바라보는 구성원 각자의 반응을 살펴야 한다.</li>
<li>변화를 지지하거나 지지하지 않는 심리적, 정서적인 이유도 이해해야 한다.</li>
<li>어떻게 하면 그들에게 최대한 좋은 영향을 미칠 수 있을지도 알아야 한다.</li>
</ul>
<hr>
<h2 id="부담이-큰-일에-도전하라">부담이 큰 일에 도전하라</h2>
<p>모든 업무가 똑같이 중요하지 않으며, 어떤 업무는 위험과 보상 수준이 이례적일 만큼 클 뿐 아니라 조직 전체의 미래에 중대한 영향을 미칠지도 모릅니다. 부담이 큰 도전은 당연히 정신을 집중하게 만들고, 궁극적으로 그 경험에서 많은 교훈을 얻을 가능성도 커집니다.</p>
<hr>
<h2 id="경계를-초월하라">경계를 초월하라</h2>
<p>경계를 초월해야 하는 일은 미래에 요긴하게 사용할 수 있는 기술을 익히고 지식을 습득할 수 있는 굉장한 무대일 확률이 큽니다.</p>
<hr>
<h2 id="다양한-사람과-협업하라">다양한 사람과 협업하라</h2>
<p>인종, 민족, 성별, 문화, 배경, 가치관, 관점이 다른 사람들과 함께 일해야 하는 상황에는 장단점이 존재합니다. 단점으로는 오해와 갈등이 생길 확률이 커질 수 밖에 없습니다. 하지만 그런 오해와 갈등을 겪으며 동시에 창조적인 아이디어를 교환하고 생산적인 발견으로 이어질 수도 있다는 장점도 있습니다.</p>
<hr>
<h2 id="역경에-직면하라">역경에 직면하라</h2>
<p>모든 역경은 사실상 자신의 패기를 시험하는 무대입니다. 이를 경험하며 성장 가능성에 눈뜬다면 다시없을 학습 기회가 됩니다. 대부분의 뛰어난 리더들은 모두 잘나갈 때가 아닌 바닥으로 추락했을 때 가장 핵심이 되는 교훈을 얻었습니다.</p>
<hr>
<h1 id="경험은-언제나-훌륭한-스승인가">경험은 언제나 훌륭한 스승인가</h1>
<p>이 책에서 소개한 두 연구 결과를 보면 공통적으로 매우 도전적인 직무는 ‘기술 개발’이라는 가시적이고 실질적인 결과를 가져다준다고 결론 내렸습니다. 하지만 반드시 그런 것은 아니었습니다. 이들은 도전 난이도가 최고일 때 긍정적 영향이 오히려 감소한다고 주장했습니다.</p>
<p>과도한 도전의 부작용을 경고했지만 대부분의 상황에서 도전과 학습 사이에 긍정적인 관계가 성립됩니다. 특히 도전 의욕을 자극하는 특징을 더욱 완벽히 반영한 경험일수록 학습 가능성이 더 크다고 확신합니다.</p>
<p>단, 행동 주체가 이 과정에서 무언가를 배우려는 동기를 가지고 있어야 한다는 선행 조건이 있습니다.</p>
<p>그렇기에 학습 동기를 가졌다는 전제하에 매우 도전적인 경험을 하는 것이 수준 높은 리더십 기술을 개발하게 하는 좋은 방법이라고 말합니다.</p>
<hr>
<h1 id="스스로-리더십을-개발하라">스스로 리더십을 개발하라</h1>
<p>앞에서 말한 동기를 가진 사람을 한계치로 내몰고 시험하면서 도전적으로 직무를 맡기는 것은 만으로 위대한 리더로 만들 수 없습니다. 이 방법은 문제를 절반밖에 해결하지 못합니다. 나머지 절반을 채우려면 경험에서 배우려고 의식적으로 노력해야 합니다. </p>
<p>이는 무언가를 경험했다고 반드시 성장하거나 배우지 못한다는 뜻이 있지만, 반대로 생각해보면 각자가 자기 주도권과 힘을 가졌다는 뜻으로 현재 무엇을 경험하는 중이든 그것을 활용해 스스로 성장하려고 노력하면 됩니다.</p>
<p>즉, 무언가를 배우기 위해 더욱 주도적이고 적극적으로 행동해야 하며 이것이 바로 <strong>유연함의 기술의 핵심</strong>입니다.</p>
<hr>
<h1 id="마음챙김으로-경험-학습의-우등생이-되어라">마음챙김으로 경험 학습의 우등생이 되어라</h1>
<p>경험에서 무언가를 배우려면 마음챙김에 더욱 신경 써야합니다. 마음챙김은 지금 이 순간에 의도적이고 적극적으로 주의를 기울임으로써 각정하는 ‘<strong>인식</strong>’을 말합니다.</p>
<p>이 책은 방심 상태일 때 배울 기회를 놓치는, 일종의 학습 기회비용에 집중합니다. 즉, 매 순간 무언가 배우기 위해서는 약간 속도를 늦추고 주의를 기울여야 합니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이 책을 읽으면서 우테코를 시작할 때 작성한 자기소개서의 질문이 떠올랐습니다. 도전해본 경험의  과정에서 무엇을 배웠고, 실패했다면 실패의 과정에서 무엇을 배웠는지? 이런 내용의 질문이었던 것 같습니다.</p>
<p>우테코 자기소개서 질문이 떠올랐던 이유는 아마 1장에서 경험의 중요성을 강조했기 때문이 아닐까라는 생각이 듭니다.</p>
<p>70-20-10 법칙이라는 법칙과 다양한 사람들의 사례, 연구 결과, 리더들과의 대화 내용 등을 소개하며 경험의 중요성을 강조하지만, 어쩌면 제가 경험한 도전이 있었고 그 과정에서 가장 많은 성장을 했다는 것을 알고 있기에 이 글에서 얘기하는 경험의 중요성이 어떤 의미인지 공감하고 이해할 수 있었던 것 같습니다.</p>
<p>자연스럽게 생각해보면 떠올릴 수 있는 생각이고, 당연하다고도 생각할 수 있는 내용일 수도 있지만 실천하기 어렵다는 말이 가장 공감됐습니다. </p>
<p>앞으로 2~7장에서는 소프트 스킬 활용법을 설명한다고 하니 잘 배워서 사용해볼 수 있으면 좋겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 백준 1912: 연속합]]></title>
            <link>https://velog.io/@noeyh_0j/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%EC%A4%80-1912-%EC%97%B0%EC%86%8D%ED%95%A9</link>
            <guid>https://velog.io/@noeyh_0j/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%EC%A4%80-1912-%EC%97%B0%EC%86%8D%ED%95%A9</guid>
            <pubDate>Thu, 26 Feb 2026 14:32:00 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>알고리즘 고수가 되고싶은 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 <a href="https://www.acmicpc.net/problem/1912">백준 1912번</a> 문제를 코틀린으로 풀어본 과정을 정리할 것입니다.</p>
<hr>
<h1 id="문제">문제</h1>
<p>n개의 정수로 이루어진 임의의 수열이 주어진다. 우리는 이 중 연속된 몇 개의 수를 선택해서 구할 수 있는 합 중 가장 큰 합을 구하려고 한다. 단, 수는 한 개 이상 선택해야 한다.
예를 들어서 10, -4, 3, 1, 5, 6, -35, 12, 21, -1 이라는 수열이 주어졌다고 하자. 여기서 정답은 12+21인 33이 정답이 된다.</p>
<hr>
<h2 id="입력">입력</h2>
<p>첫째 줄에 정수 n(1 ≤ n ≤ 100,000)이 주어지고 둘째 줄에는 n개의 정수로 이루어진 수열이 주어진다. 수는 -1,000보다 크거나 같고, 1,000보다 작거나 같은 정수이다.</p>
<h2 id="출력">출력</h2>
<p>첫째 줄에 답을 출력한다.</p>
<hr>
<h3 id="예제-입력-1">예제 입력 1</h3>
<pre><code>10
10 -4 3 1 5 6 -35 12 21 -1</code></pre><h3 id="예제-출력-1">예제 출력 1</h3>
<pre><code>33</code></pre><h3 id="예제-입력-2">예제 입력 2</h3>
<pre><code>10
2 1 -4 3 4 -4 6 5 -5 1</code></pre><h3 id="예제-출력-2">예제 출력 2</h3>
<pre><code>14</code></pre><h3 id="예제-입력-3">예제 입력 3</h3>
<pre><code>5
-1 -2 -3 -4 -5</code></pre><h3 id="예제-출력-3">예제 출력 3</h3>
<pre><code>-1</code></pre><hr>
<h1 id="첫-번째-풀이">첫 번째 풀이</h1>
<pre><code class="language-kotlin">fun main() {
    val result = mutableListOf&lt;Int&gt;()
    val n = readln().toInt()
    val nList = readln().split(&quot; &quot;).map{ it.toInt() }

    for (i in 0..nList.size) {
        for (j in i + 1..nList.size) {
            result.add(nList.subList(i, j).sumOf{ it })
        }
    }
    println(result.maxOf{ it })
}</code></pre>
<h2 id="결과">결과</h2>
<blockquote>
<p>시간 초과</p>
</blockquote>
<p>첫 번째 풀이는 리스트를 경우의 수대로 전부 쪼개서 그 리스트들의 합중 가장 큰 수를 출력하도록 풀었습니다. 하지만 시간초과로 틀리는 결과를 가져왔습니다.</p>
<p>이 때 들었던 생각은 이중 for문과 subList를 계속 사용하면서 시간복잡도가 커졌구나 라는 생각을 가졌습니다. </p>
<p>이 문제를 해결하기 위해 이중 for문 또는 subList를 하지 않는 방향으로 풀어보고자 노력했습니다. 하지만 제가 가진 지식으로는 못풀겠다는 생각이 들어 풀이를 참고한 후에 맞췄습니다.</p>
<hr>
<h1 id="두-번째-풀이">두 번째 풀이</h1>
<pre><code class="language-kotlin">import kotlin.math.max

fun main() {
    val n = readln().toInt()
    val nList = readln().split(&quot; &quot;).map{ it.toInt() }

    val dp = IntArray(n)
    dp[0] = nList[0]
    var maxNum = dp[0]

    for (i in 1 until n) {
        dp[i] = max(nList[i], dp[i - 1] + nList[i])
        maxNum = max(dp[i], maxNum)
    }

    println(maxNum)
}</code></pre>
<h2 id="결과-1">결과</h2>
<blockquote>
<p>정답</p>
</blockquote>
<p>이 풀이에서는 DP를 사용하여 풀었습니다. DP에 대해서는 따로 글을 작성하여 후에 링크를 첨부하겠습니다.</p>
<p>알고리즘의 순서는 다음과 같습니다.</p>
<ul>
<li>리스트의 크기만큼의 배열을 만들고 배열의 첫 번째 값은 입력 리스트의 첫 번째 값으로 저장합니다.</li>
<li>각 배열에는 해당 인덱스의 리스트 값과 해당 인덱스까지 더한 값을 비교하여 더 큰 수를 배열의 다음 인덱스에 지정합니다. 
이런 풀이가 허용되는 이유는 해당 인덱스까지 리스트의 수를 전부 더한 값이 리스트의 다음 인덱스의 값보다 작다면 앞에서 더한 값은 최선이 아니게 됩니다. 그렇기에 배열의 다음 인덱스를 리스트의 앞의 수를 더한 수가 아닌 리스트의 다음 인덱스의 값으로 지정하는 과정으로 쓸데없는 계산을 막아줄 수 있습니다.</li>
<li>배열을 지정한 후에 최대값과 비교하여 더 큰 수를 최대값으로 지정합니다.</li>
</ul>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>처음에는 이 문제에 대한 정리글을 쓸까? 고민을 했었습니다. 왜냐하면 이 문제는 제가 스스로 푼 문제가 아니라 다른 사람들의 풀이를 참고해서 풀은 문제이기 때문입니다. </p>
<p>하지만 블로그의 목적은 내가 잘한 것을 자랑하는게 아니라 내가 공부 과정에서 배우고 느낀 점을 공유하고 글로 정리해보는 공간이라고 생각이 드는 순간 일단 써보고 봐야겠다고 생각했습니다.</p>
<p>또한 처음에는 풀지 못하는 문제에 대해 풀이를 보는 것이 무척 꺼렸었습니다. 하지만 크루분이 했던 1시간 지나도 풀지 못하는 문제는 계속 봐도 못푼다는 말을 생각하며 풀이를 봤습니다. 문제를 푸는 것만 공부가 아니라 내가 모르는 것을 알고 배우는 것도 공부구나 라는 것을 깨달을 수 있었습니다.</p>
<p>풀이를 보며 잊고 지내던 DP 개념을 다시 떠올리며 문제를 이해할 수 있었습니다. 사실 DP로 푼다는 것을 알아도 이 문제를 풀 수 있었을까? 라는 생각이 들긴하지만, 다음에 이런 문제를 풀면 시도는 해볼 수 있겠다라는 자신감이 생겼습니다.</p>
<p>이렇게 못푼 문제는 캘린더에 따로 적어놓고 3일 후에 다시 풀어보며 개념 정리를 확실히 하는 방향으로 학습하고자 합니다.</p>
<p>DP도 하향식, 상향식, 메모이제이션? 처럼 다양한 종류를 가지는데 이는 아직 확실하게 알지 못해 따로 DP에 대한 개념 정리글을 작성해보며 공부할 계획입니다.</p>
<p>이렇게 저의 첫 알고리즘 풀이 글을 마무리해보겠습니다. 매일 하나씩 풀고 써보려고 노력해보겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 2026.02.16 ~ 2026.02.22]]></title>
            <link>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.16-2026.02.22</link>
            <guid>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.16-2026.02.22</guid>
            <pubDate>Sun, 22 Feb 2026 08:29:26 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>한 주의 회고와 함께 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 주는 명절과 졸업식이 함께 있던 주였기 때문에 학습에 대해서는 아쉬운 부분이 많지만, 지난 4주동안 열심히 달려온 자신을 돌볼 수 있었기에 나름 알차게 보낸 일주일이라고 생각합니다.</p>
<p>크게 이번 주 계획에 대한 설명과 KPT, 학습 정리 글, 마무리로 회고를 하겠습니다.</p>
<hr>
<h1 id="이번주의-계획은">이번주의 계획은?</h1>
<img src="https://velog.velcdn.com/images/noeyh_0j/post/bdcc9763-d4dd-416c-86bf-9e503ec1b406/image.png">

<p>이번 주차는 코루틴을 중심으로 예외 처리와 테스트까지를 학습 목표로 삼은 주차였습니다. 이것과 더해 코틀린 문법이 머릿속에서 희미해지기 전에 알고리즘 문제를 풀며 문법에 대한 내용도 잊지 않도록 해보자는 목표를 정했었습니다.</p>
<p>하지만 이 목표는 이루지 못했습니다. 바쁜 일정으로 인해 공부와 알고리즘 풀이를 함께 병행하는 것이 쉽지 않았습니다.</p>
<p>이번 주차는 알고리즘 문제 풀이보다 기존에 계획에 세워놓았던 개념들을 학습하는 것이 더 높은 우선순위라고 판단해서 기존 학습 목표에 대해 공부했습니다.</p>
<h2 id="keep">Keep</h2>
<ul>
<li>틈틈이 공부하는 시간을 가지며 정해놓은 학습 목표를 밀리지 않고 이룬 점</li>
<li>적절한 휴식시간도 자주 챙기며 무리하지 않게 학습을 수행한 점</li>
</ul>
<h2 id="problem">Problem</h2>
<ul>
<li>부족한 시간으로 인해 학습 목표는 이뤘지만 깊이가 부족한 점</li>
<li>모르는 부분에 대해 깊게 다뤄보지 못한 점</li>
</ul>
<h2 id="try">Try</h2>
<ul>
<li>앞에서도 말했듯이 이번 주차는 시간을 쪼개고 쪼개서 남는 시간에 공부를 했습니다. 그렇다보니 개념을 생각해보는 시간이 적어 단순히 읽었다라는 느낌이 강하게 들었습니다. 그렇기 때문에 제가 쓴 글을 읽어보며 이해가 부족하다고 생각하는 내용을 다시 읽어보며 복습하는 시간을 가져야겠다고 생각했습니다.</li>
</ul>
<hr>
<h1 id="학습한-내용정리">학습한 내용정리</h1>
<p>학습한 내용은 개인 블로그에 정리하였습니다.</p>
<blockquote>
</blockquote>
<ul>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-14%EC%9E%A5-%EC%BD%94%EB%A3%A8%ED%8B%B4">14장 코루틴</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-15%EC%9E%A5-%EA%B5%AC%EC%A1%B0%ED%99%94%EB%90%9C-%EB%8F%99%EC%8B%9C%EC%84%B1">15장 구조화된 동시성</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-16%EC%9E%A5-%ED%94%8C%EB%A1%9C%EC%9A%B0">16장 플로우</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-17%EC%9E%A5-%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%97%B0%EC%82%B0%EC%9E%90">17장 플로우 연산자</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-18%EC%9E%A5-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC%EC%99%80-%ED%85%8C%EC%8A%A4%ED%8A%B8">18장 오류 처리와 테스트</a></li>
</ul>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 주차를 마지막으로 첫 레벨 0의 학습 회고를 마무리하게 되었습니다. 회고록을 주기적으로 작성해보면서 미션을 마지막으로 회고록 작성을 끝내는 것이 아쉽다는 생각이 들었습니다.</p>
<p>자신이 공부한 과정과 내용을 돌아볼 수 있는 시간을 가져보며, 단순히 반성하고 고치는 방법을 생각하는 것을 넘어 앞으로 계획을 어떻게 세울지까지 영향을 줄 수 있는 좋은 습관이라고 느꼈습니다. </p>
<p>미션을 통해 제출하는 회고록은 끝나겠지만 일주일에 한 번씩, 이게 힘들다면 최소한 한 달에 한 번은 회고록을 작성해보는 습관을 가져보도록 노력해보고자 합니다.</p>
<p>마지막 주차니까 4주 전에 작성했던 학습 계획서를 다시 보며 핵심 목표를 잘 이뤘나 스스로 생각해봤습니다.</p>
<blockquote>
<p>⭐️ <strong>내가 작성한 최종 목표</strong>
우테코 과정을 안정적으로 진행하기 위해 4주의 학습 기간을 최대한 활용해서 앞으로 코틀린을 단순히 외워서 사용하지 않도록 언어의 이해도를 높이는 것이 중요하다고 생각합니다.
그렇기에 학습 과정에서 코틀린다운 코드란 무엇인가? 코틀린의 기반이 된 자바와 어떤 차이점을 가지고 있는가? 이 두 가지 질문을 의식하며 학습하여 코틀린뿐만 아니라 자바에 대한 이해도도 함께 올려서, 레벨1 미션을 수행하기 전까지  이해를 기반으로 익숙하게 사용할 수 있게 되는 것이 목표입니다.</p>
</blockquote>
<p>코틀린의 핵심 철학이 무엇인지, 자바와 어떻게 같고 다른 동작 방식을 가지는지 등에 대해서는 4주 전보다 아는게 많아지고 볼 수 있는 시야가 넓어졌음을 자신있게 말할 수 있습니다.</p>
<p>하지만 자바와 코틀린은 뭐가 다르고, 코틀린다운 코드는 무엇이냐 물어봤을 때는 책처럼 명쾌하고 정확한 대답을 할 수 있다고 단언할 수는 없을 것 같아 최종 목표의 절반정도만 달성했다고 생각합니다. 그래도 열심히 공부하며 노력한 자신을 칭찬해주며 오늘 하루는 뿌듯한 마음으로 쉬어주려 합니다.</p>
<p>제가 부족한 부분은 안드로이드 크루분들과 함께 채워나가며, 반대로 제가 채워줄 수 있는 부분에 대해서도 열정을 아끼지 않으며 함께 성장할 수 있으면 좋겠습니다!</p>
<p>돌아오는 주부터 우테코 정규 과정을 시작하게 되는데 무척 설레네요 ㅎㅎ. 빠지는 사람 없이 모두 뵙고 인사할 수 있으면 좋겠습니다!! 읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 18장 오류 처리와 테스트]]></title>
            <link>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-18%EC%9E%A5-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC%EC%99%80-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-18%EC%9E%A5-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC%EC%99%80-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sun, 22 Feb 2026 07:22:27 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>오류 처리와 테스트에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 코루틴에서 오류 처리를 하는 다양한 개념과 테스트 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="코루틴-내부에서-던져진-오류-처리">코루틴 내부에서 던져진 오류 처리</h1>
<p>일시 중단 함수나 코루틴 빌더 안에 작성한 코드도 예외를 발생시킬 수 있습니다. 이런 예외를 처리하기 위해 launch나 async 호출을 try-catch로 감싼다면 효과가 없습니다. 이들이 코루틴 빌더 함수이기 때문입니다. </p>
<p>코루틴 빌더는 실행할 새로운 코루틴을 생성하는데, 이 새로운 코루틴에서 발생한 예외는 catch 블록에 의해 잡히지 않습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    try {
        launch {
            throw UnsupportedOperationException(&quot;Ouch!&quot;)
        }
    } catch (u: UnsupportedOperationException) {
        println(&quot;Handled $u&quot;)
    }
}</code></pre>
<p>이 코드를 실행하면 launch 빌더 안에서 예외가 잡히지 않습니다. 이 예외를 올바르게 처리하기 위해서는 launch에 전달되는 람다 블록 안에 try-catch 블록을 넣는 것입니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    launch {
        try {
            throw UnsupportedOperationException(&quot;Ouch!&quot;)
        } catch (u: UnsupportedOperationException) {
            println(&quot;Handled $u&quot;)
        }
    }
}</code></pre>
<p>async로 생성된 코루틴이 예외를 던진다면 그 결과에 대해 await를 호출할 때 이 예외가 다시 발생합니다. await가 원하는 타입의 의미 있는 값을 돌려줄 수 없기 때문에 예외를 던져야만 합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    val myDeferredInt: Deferred&lt;Int&gt; = async {
        throw UnsupportedOperationException(&quot;Ouch!&quot;)
    }
    try {
        val i: Int = myDeferredInt.await()
        println(i)
    } catch (u: UnsupportedOperationException) {
        println(&quot;Handled $u&quot;)
    }
}</code></pre>
<p>이 코드를 실행하면 await()를 감싼 try-catch에서 예외를 잡는 것을 확인할 수 있습니다.</p>
<hr>
<h1 id="코틀린-코루틴에서-오류-전파">코틀린 코루틴에서 오류 전파</h1>
<p>코루틴의 구조적 동시성 패러다임은 자식 코루틴에서 발생한 잡히지 않은 예외가 부모 코루틴에 의해 어떻게 처리되는지에 영향을 줍니다. 자식에게 작업을 나누는 방식에 따라 자식의 오류를 처리하는 방식도 달라집니다.</p>
<ul>
<li>코루틴이 작업을 동시적으로 분해해 처리하는 경우 자식 중 하나의 실패는 더 이상 최종 결과를 얻을 수 없다는 점을 의미합니다. 즉, 한 자식의 실패가 부모의 실패로 이어집니다.</li>
<li>하나의 자식이 실패해도 전체 실패로는 이어지지 않을 때가 있습니다. 자식들에게 벌어진 실패를 부모가 처리해야 하지만 자식의 실패로 인해 시스템 전체가 실패하면서 멈추지 말아야 하는 경우를, 자식이 부모의 실행을 감독한다고 합니다. 즉, 이런 경우 한 자식의 실패가 부모의 실패로 이어지지 않습니다.</li>
</ul>
<hr>
<h2 id="자식이-실패하면-모든-자식을-취소하는-코루틴">자식이 실패하면 모든 자식을 취소하는 코루틴</h2>
<p>코루틴 간의 부모-자식 계층이 Job 객체를 통해 구축되며, SupervisorJob 없이 생성된 경우 자식 코루틴에서 발생한 잡히지 않은 예외는 부모 코루틴을 예외로 완료시키는 방식으로 처리됩니다. 즉, 실패한 자식 코루틴은 자신의 실패를 부모에게 전파하며 부모는 불필요한 작업을 막기 위해 다른 모든 자식을 취소합니다.</p>
<p>그러면서 같은 예외를 발생시키면서 자신의 실행을 완료시키고, 자신의 상위 계층으로 예외를 전파합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

fun main(): Unit = runBlocking {
    launch {
        try {
            while(true) {
                println(&quot;Heartbeat!&quot;)
                delay(500.milliseconds)
            }
        } catch (e: Exception) {
            println(&quot;Heartbeat terminated: $e&quot;)
            throw e
        }
    }
    launch {
        delay(1.seconds)
        throw UnsupportedOperationException(&quot;Ow!&quot;)
    }
}
// Heartbeat!
// Heartbeat
// Heartbeat terminated: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=&quot;coroutine#1&quot;:BlockingCoroutine{Cancelling}@73ad2d6</code></pre>
<p>이런 식으로 runBlocking을 포함한 모든 코루틴 빌더는 일반적인 감독이 아닌 코루틴을 생성합니다. 그렇기에 한 코루틴이 잡히지 않은 예외로 종료되면 다른 자식 코루틴도 취소됩니다.</p>
<hr>
<h3 id="구조적-동시성은-코루틴-스코프를-넘는-예외에만-영향을-미친다">구조적 동시성은 코루틴 스코프를 넘는 예외에만 영향을 미친다</h3>
<p>형제 코루틴을 취소하고 예외를 코루틴 계층 상위로 전파하는 이 동작은 코루틴 스코프를 넘는 처리되지 않은 예외에만 영향을 미칩니다. 따라서 처음부터 스코프를 넘는 예외를 던지지 않으면 위 동작을 피할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

fun main(): Unit = runBlocking {
    launch {
        try {
            while(true) {
                println(&quot;Heartbeat!&quot;)
                delay(500.milliseconds)
            }
        } catch (e: Exception) {
            println(&quot;Heartbeat terminated: $e&quot;)
            throw e
        }
    }
    launch {
        try {
            delay(1.seconds)
            throw UnsupportedOperationException(&quot;Ow!&quot;)
        } catch {
            println(&quot;Caught $u&quot;)
        }
    }
}
// Heartbeat!
// Heartbeat!
// Caught java.lang.UnsupportedOperationException: Ow!
// Heartbeat!
// Heartbeat!</code></pre>
<p>이렇게 예외를 던지는 코루틴을 try로 감싼다면 예외가 발생한 다음에도 하트비트 코루틴이 계속 텍스트를 출력하는 것을 볼 수 있습니다.</p>
<p>처리하지 않은 예외를 코루틴 계층 위쪽으로 전파하고 형제 코루틴을 취소하는 것은 애플리케이션에서 구조적 동시성 패러다임을 강제하는 데 도움이 됩니다. 하지만 처리하지 않은 예외 하나가 전체 애플리케이션을 무너뜨려서는 안됩니다.</p>
<hr>
<h2 id="슈퍼바이저는-부모와-형제가-취소되지-않게-한다">슈퍼바이저는 부모와 형제가 취소되지 않게 한다</h2>
<p>슈퍼바이저는 자식이 실패하더라도 생존합니다. 일반 Job을 사용하는 스코프와 달리 슈퍼바이저는 일부 자식이 실패를 보고하더라도 실패하지 않습니다. 코루틴이 자식 코루틴의 슈퍼바이저가 되려면 그 코루틴에 연관된 Job이 일반적인 Job이 아니라 SupervisorJob이어야 합니다.</p>
<p>SupervisorJob은 Job과 마찬가지 역할을 하지만 예외를 부모에게 전파하지 않으며, 다른 자식 작업이 실패해도 취소되지 않게 합니다. </p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

fun main(): Unit = runBlocking {
    supervisorScope {
        launch {
             try {
                 while(true) {
                     println(&quot;Heartbeat!&quot;)
                     delay(500.milliseconds)
                    }
                } catch(e: Exception) {
                    println(&quot;Heartbeat terminated: $e&quot;)
                    throw e
                }
            }
            launch {
                delay(1.seconds)
                throw UnsupportedOperationException(&quot;Ow!&quot;)
            }
    }
}</code></pre>
<p>앞에서 실행했던 예제인 하드비트 코루틴이 계속 실행되도록 코드를 수정하면 launch 호출을 supervisorScope로 감싸면 됩니다.</p>
<hr>
<h1 id="coroutineexceptionhandler">CoroutineExceptionHandler</h1>
<p>자식 코루틴은 처리되지 않은 예외를 부모 코루틴에 전파합니다. 이때 예외가 슈퍼바이저에 도달하거나 계층의 최상위로 가서 부모가 없는 루트 코루틴에 도달하면 예외는 더 이상 전파되지 않습니다. 이 시점에서 처리되지 않은 예외는 CoroutineExceptionHandler라는 특별한 핸들러에게 전달됩니다.</p>
<p>CoroutineExceptionHandler를 코루틴 콘텍스트에 제공하면 처리되지 않은 예외를 처리하는 동작을 커스텀화 할 수 있습니다. 이 핸들러는 코루틴 콘텍스트와 처리되지 않은 예외를 람다의 파라미터로 받습니다.</p>
<pre><code class="language-kotlin">val exceptionHandler = CoroutineExceptionHandler { context, exception -&gt;
    println(&quot;[ERROR] $exception&quot;)
}</code></pre>
<p>이 예외 핸들러는 콘솔에 [ERROR]라는 커스텀 접두어와 함께 처리되지 않은 예외를 로그로 남깁니다.</p>
<p>자식 코루틴, 즉 코루틴 스코프에서 시작된 코루틴이나 다른 코루틴에서 시작된 코루틴은 처리되지 않은 예외의 처리를 부모에게 위임하며 계층의 최상위에 이를 때까지 이런 위임이 계속됩니다. 따라서 중간에 있는 CoroutineExceptionHandler라는 것은 존재하지 않습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*

private val topLevelHandler = CoroutineExceptionHandler { _, e -&gt;
    println(&quot;[TOP] ${e.message}&quot;)    
}

private val intermediateHandler = CoroutineExceptionHandler { _, e -&gt;
    println(&quot;[INTERMEDIATE] ${e.message}&quot;)    
}

@OptIn(DelicateCoroutinesApi::class)
fun main() {
    GlobalScope.launch(topLevelHandler) }
        launch(intermediateHandler) {
            throw UnsupportedOperationException(&quot;Ouch!&quot;)
        }
    }
    Thread.sleep(1000)
}
// [TOP] Ouch!</code></pre>
<hr>
<h2 id="coroutineexceptionhandler를-launch와-async에-적용할-때의-차이점">CoroutineExceptionHandler를 launch와 async에 적용할 때의 차이점</h2>
<p>CoroutineExceptionHandler를 살펴볼 때 예외 핸들러는 계층의 최상위 코루틴이 launch로 생성된 경우에만 호출됩니다. 최상위 코루틴이 async로 생성된 경우에는 CoroutineExceptionHandler가 호출되지 않습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds

class ComponentWithScope(dispatcher: CoroutineDispatcher = 
    Dispatchers.Default) {
    private val exceptionHandler = CoroutineExceptionHandler { _, e -&gt;
        println(&quot;[ERROR] ${e.message}&quot;)
    }

    private val scope = CoroutineScope(SupervisorJob() + dispatcher +
        exceptionHandler)

    fun action() = scope.launch {
        async {
            throw UnsupportedOperationException(&quot;Ouch!&quot;)
        }
    }
}

fun main() = runBlocking {
    val supervisor = ComponentWithScope()
    supervisor.action()
    delay(1.seconds)
}
// [ERROR] Ouch!</code></pre>
<p>이 코드의 경우 에러 메세지가 출력이 되는 것을 확인할 수 있습니다. </p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds

class ComponentWithScope(dispatcher: CoroutineDispatcher = 
    Dispatchers.Default) {
    private val exceptionHandler = CoroutineExceptionHandler { _, e -&gt;
        println(&quot;[ERROR] ${e.message}&quot;)
    }

    private val scope = CoroutineScope(SupervisorJob() + dispatcher +
        exceptionHandler)

    fun action() = scope.async {
        launch {
            throw UnsupportedOperationException(&quot;Ouch!&quot;)
        }
    }
}

fun main() = runBlocking {
    val supervisor = ComponentWithScope()
    supervisor.action()
    delay(1.seconds)
}
// [ERROR] Ouch!</code></pre>
<p>바깥의 코루틴을 async로 시작하도록 구현을 변경하면 코루틴 예외 핸들러가 호출되지 않는 것을 볼 수 있습니다.</p>
<p>최상위 코루틴이 async로 시작되면 이 예외를 처리하는 책임은 await()를 호출하는 Deferred의 소비자에게 있습니다. 따라서 코루틴 예외 핸들러는 이 예외를 무시할 수 있습니다. 그리고 소비자 코드는 await 호출을 try-catch 블록으로 감싸는 방식으로 예외를 처리할 수 있습니다.</p>
<hr>
<h1 id="플로우에서-예외-처리">플로우에서 예외 처리</h1>
<p>일반적인 코틀린 함수나 일시 중단 함수와 마찬가지로 플로우도 예외를 던질 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class UnhappyFlowException: Exception()

val exceptionalFlow = flow {
    repeat(5) { number -&gt;
        emit(number)
    }
    throw UnhappyFlowException()
}

fun main() = runBlocking {
    val transformedFlow = exceptionalFlow.map {
        it * 2
    }
    try {
        transformedFlow.collect {
            print(&quot;$it &quot;)
        }
    } catch (u: UnhappyFlowException) {
        println(&quot;\nHandled: $u&quot;)
    }
    // 0 2 4 6 8 
    // Handled: UnhappyFlowException
}</code></pre>
<p>이 예제는 플로우를 수집하면 5개의 원소가 배출된 다음에 UnhappyFlowException라는 커스텀 예외가 발생합니다.</p>
<p>일반적으로 플로우의 일부분에서 예외가 발생하면 collect에서 예외가 던져집니다. 이는 collect 호출을 try-catch 블록으로 감싸면 예상대로 동작한다는 의미입니다. 이때 플로우에 중간 연산자가 적용됐는지 여부와는 관계가 없습니다.</p>
<hr>
<h2 id="catch-연산자로-업스트림-예외-처리">catch 연산자로 업스트림 예외 처리</h2>
<p>catch는 플로우에서 발생한 예외를 처리할 수 있는 중간 연산자입니다. 이 함수에 연결된 람다 안에서 플로우에 발생한 예외에 접근할 수 있습니다. 이 연산자는 취소 예외를 자동으로 인식하기 때문에 취소가 발생한 경우에는 catch 블록이 호출되지 않습니다. </p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    exceptionalFlow
        .catch { cause -&gt;
            println(&quot;\nHandled: $cause&quot;)
            emit(-1)
        }
        .collect {
            print(&quot;$it &quot;)
        }
        // 0 1 2 3 4 
        // Handled: UnhappyFlowException
        // -1 
}</code></pre>
<p>catch 연산자는 오직 업스트림에 대해서만 작동하며, 플로우 처리 파이프라인의 앞쪽에서 발생한 예외들만 잡아냅니다. collect 람다 안에서 발생한 예외를 처리하려면 collect 호출을 try-catch 블록으로 감싸면 됩니다.</p>
<hr>
<h2 id="retry-연산자로-플로우-수집-재시도하기">retry 연산자로 플로우 수집 재시도하기</h2>
<p>retry 연산자는 catch와 마찬가지로 업스트림의 예외를 잡습니다. 개발자는 예외를 처리하고 Boolean 값을 반환하는 람다를 사용하여 람다가 true를 반환하면 재시도를 할 수 있으며, 재시도 동안 업스트림의 플로우가 처음부터 다시 수집되면서 모든 중간 연산이 다시 실행됩니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
import kotlin.random.Random

class CommunicationException: Exception(&quot;Communication failed!&quot;)

val unreliableFlow = flow {
    println(&quot;Starting the flow!&quot;)
    repeat(10) { number -&gt;
        if (Random.nextDouble() &lt; 0.1) throw CommunicationException()
        emit(number)
    }
}

fun main() = runBlocking {
    unreliableFlow
        .retry(5) { cause -&gt;
            println(&quot;\nHandled: $cause&quot;)
            cause is CommunicationException
        }
        .collect { number -&gt;
            print(&quot;$number &quot;)
        }
}

// Starting the flow!
// 0 1 
// Handled: CommunicationException: Communication failed!
// Starting the flow!
// 0 1 2 3 4 5 6 
// Handled: CommunicationException: Communication failed!
// Starting the flow!
// 0 1 2 3 4 5 6 7 8 
//Handled: CommunicationException: Communication failed!
// Starting the flow!
// 0 1 2 3 4 5 6 
// Handled: CommunicationException: Communication failed!
// Starting the flow!
// 
// Handled: CommunicationException: Communication failed!
// Starting the flow!
// 0 1 2 3 4 5 6 7 8 9 </code></pre>
<p>이런 식으로 몇 번의 반복되는 과정을 통해 플로우의 모든 원소가 성공적으로 수집되는 모습을 볼 수 있습니다.</p>
<hr>
<h1 id="코루틴과-테스트-플로우">코루틴과 테스트 플로우</h1>
<p>코루틴을 사용하는 코드를 위한 테스트도 일반적인 테스트와 마찬가지로 작동합니다. 테스트 메소드에서 코루틴을 사용하려면 runTest 코루틴 빌더를 사용하면 됩니다. </p>
<p>runBlocking 빌더 함수는 일반 코틀린 코드와 동시성 코들린 코드 사이에 다리를 놓는 역할을 하기 때문에 일시 중단 함수나 코루틴, 플로우를 사용하는 코드를 테스트할 때도 이를 쓸 수 있지만, 테스트가 실시간으로 실행된다는 단점이 있습니다. 이는 delay가 지정된 경우에 지연 시간이 전부 실행된다는 뜻입니다.</p>
<p>즉, 테스트 스위트가 커지면 불필요한 긴 실행 시간이 누적되면서 전체 애플리케이션 테스트 속도가 느려질 수 있습니다. </p>
<hr>
<h2 id="테스트를-빠르게-만들기">테스트를 빠르게 만들기</h2>
<p>코틀린 코루틴은 가상 시간을 사용해 테스트 실행을 빠르게 진행할 수 있게 해줍니다. 가상 시간을 사용할 때는 지연이 자동으로 빠르게 진행되기 때문에 runBlocking 빌더 함수를 사용했을 때의 단점을 해결할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

class PlaygroundTest {
    @Test
    fun testDelay() = runTest {
        val startTime = System.currentTimeMillis()
        delay(20.seconds)
        println(System.currentTimeMillis() - startTime)
    }
}</code></pre>
<p>runTest는 속도를 높이기 위해 특별한 테스트 디스패처와 스케줄러를 사용합니다.</p>
<p>runBlocking과 마찬가지로 runTest의 디스패처는 단일 스레드입니다. 따라서 기본적으로 모든 자식 코루틴은 동시에 실행되며 테스트 코드와 병렬로 실행되지 않습니다. 단일 스레드 디스패처를 공유하는 경우 다른 코루틴이 코드를 실행하려면 코드가 일시 중단 지점을 제공해야 하며, runTest도 마찬가지입니다.</p>
<p>delay()나 yield() 또는 다른 일시 중단 함수 호출을 중간에 추가하면 됩니다. 또한 테스트 디스패처에서는 TestCoroutineScheduler를 통해 가상 시간을 더 세밀하게 제어할 수 있습니다.</p>
<p>runTest 빌더 함수의 블록 안에서는 TestScope라는 특수한 스코프에 접근할 수 있으며, 이 스코프는 TestCoroutineScheduler 기능을 사용할 수 있게 해줍니다. 이 스케줄러의 핵심 함수는 다음과 같습니다.</p>
<ul>
<li>runCurrent는 현재 실행하게 예약된 모든 코루틴을 실행합니다.</li>
<li>advanceUntilIdel는 예약된 모든 코루틴을 실행합니다.</li>
</ul>
<hr>
<h2 id="터빈으로-플로우-테스트">터빈으로 플로우 테스트</h2>
<p>플로우 기반 코드는 종종 더 복잡하며 무한한 플로우나 더 까다로운 불변성을 다뤄야 할 수도 있습니다. 플로우 테스트 작성에 도움을 주는  터빈 라이브러리를 통해 이런 경우를 지원할 수 있습니다.</p>
<p>터빈의 핵심 기능은 플로우의 확장 함수인 test 함수입니다. test 함수는 새 코루틴을 실행하며 내부적으로 플로우를 수집합니다. test의 람다에서 awaitItem, awaitComplete, awaitError 함수를 테스트 프레임워크의 일반 단언문과 함께 사용해서 플로우에 대한 불변 조건을 지정하고 검증할 수 있습니다. 또한 플로우가 방출한 모든 원소가 테스트에 의해 적절히 소비되도록 보장해줍니다.</p>
<pre><code class="language-kotlin">@Test
fun doTest() = runTest {
    val results = myFlow.test {
        assertEquals(1, awaitItem())
        assertEquals(2, awaitItem())
        assertEquals(3, awaitItem())
        awaitComplete()
    }
}</code></pre>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 오류 처리와 테스트에 대해 정리해봤습니다.</p>
<p>코루틴에서 예외처리도 일반적인 코드와 마찬가지로 처리할 수 있으며, 코루틴의 경계를 주의하면서 예외처리를 하지 않으면 모든 자식 코루틴이 취소될 수 있다는 사실을 배웠습니다.</p>
<p>또한 코루틴 테스트를 진행할 때 runBlocking 빌더 함수를 사용해도 되지만 실시간 테스트를 진행하기 때문에 지연 시간을 전부 계산해야되서 테스트에 오래걸린다는 단점이 있어 runTest를 사용해 테스트를 진행하면 더 빠르게 테스트를 할 수 있다는 것을 알게 되었습니다.</p>
<p>이렇게 18장을 마무리로 Kotlin in Action 2/e 책의 개념 정리를 마치겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
    </channel>
</rss>