<?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>Sat, 30 May 2026 16:28:28 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/b2316b0d-4d09-4455-99d1-2efff2a00a94/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[[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>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 17장 플로우 연산자]]></title>
            <link>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</link>
            <guid>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</guid>
            <pubDate>Fri, 20 Feb 2026 16:13:36 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>플로우 연산자에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 플로우 연산자가 무엇인지, 어떻게 사용하는지 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="플로우를-조작하기">플로우를 조작하기</h1>
<p>컬렉션을 조작하기 위해 다양한 연산자를 사용하는 것처럼 플로우를 변환할 때도 비슷한 연산자를 쓸 수 있습니다. </p>
<p>시퀀스와 마찬가지로 플로우도 중간 연산자와 최종 연산자를 구분합니다. 중간 연산자는 코드를 실행하지 않고 변경된 플로우를 반환하며, 최종 연산자는 컬렉션, 개별 원소, 계산된 값을 반환하거나 아무 값도 반환하지 않으면서 플로우를 수집하고 실제 코드를 실행합니다. </p>
<hr>
<h1 id="중간-연산자의-동작-방식은">중간 연산자의 동작 방식은?</h1>
<p>중간 연산자는 플로우에 적용돼 새로운 플로우를 반환합니다. 플로우는 업스트림과 다운스트림으로 구분할 수 있습니다.</p>
<p>연산자가 적용되는 플로우를 업스트림 플로우라고 하고, 중간 연산자가 반환하는 플로우를 다운스트림 플로우라고 부릅니다. 다운스트림 플로우는 또 다른 연산자의 업스트림 플로우로 작용할 수 있습니다. 시퀀스와 마찬가지로 중간 연산자가 호출되더라도 플로우 코드가 실제로 실행되지 않으며, 반환된 플로우는 콜드 상태입니다.</p>
<hr>
<h2 id="transform-함수">transform 함수</h2>
<p>transform 함수는 업스트림 플로우의 각 원소에 대해 원하는 만큼의 원소를 다운스트림 플로우에 배출할 수 있게 해줍니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*

fun main() {
    val names = flow {
        emit(&quot;Jo&quot;)
        emit(&quot;May&quot;)
        emit(&quot;Sue&quot;)
    }
    val upperAndLowercaseNames = names.transform {
        emit(it.uppercase())
        emit(it.lowercase())
    }
    runBlocking {
        upperAndLowercaseNames.collect { print(&quot;$it&quot;) }
    }
    // JO jo MAY may SUE sue
}</code></pre>
<hr>
<h2 id="take-함수">take 함수</h2>
<p>take 함수는 수집자와 관련된 코루틴 스코프를 취소하는 방식 외에 플로우 수집을 제어된 방식으로 취소하는 또 다른 방법입니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*

fun main() {
    val temps = getTemperatures()
    temps
        .take(5)
        .collect {
            log(it)
        }
}</code></pre>
<hr>
<h2 id="플로우의-각-단계-후킹">플로우의 각 단계 후킹</h2>
<p>onCompletion 연산자는 플로우가 정상 종료되거나, 취소되거나, 예외로 종료된 후에 호출되는 람다를 지정할 수 있게 해줍니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    val temps = getTemperatures()
    temps
        .take(5)
        .onCompletion { cause -&gt;
            if (cause != null) {
                println(&quot;An error occurred! $cause&quot;)
            } else {
                println(&quot;Completed!&quot;)
            }
        }
        .collect {
            println(it)
        }
}</code></pre>
<p>onStart는 플로우의 수집이 시작될 때 첫 번째 배출이 일어나기 전에 실행됩니다.</p>
<p>onEach는 업스트림 플로우에서 배출된 각 원소에 대해 작업을 수행한 후 이를 다운스트림 플로우에 전달합니다.</p>
<p>onEmpty는 원소를 배출하지 않고 종료되는 플로우의 경우 로직을 추가로 수행하거나 기본값을 제공할 수 있습니다.</p>
<pre><code class="language-kotlin">fun main() {
    flow
        .onEmpty {
            println(&quot;Nothing - emitting default value!&quot;)
            emit(0)
        }
        .onStart {
            println(&quot;Starting!&quot;)
        }
        .onEach {
            println(&quot;On $it!&quot;)
        }
        .onCompletion {
            println(&quot;Done!&quot;)
        }
        .collect()
    }
}

fun main() {
    runBlocking {
        process(flowOf(1, 2, 3))
        // Starting!
        // On 1!
        // On 2!
        // On 3!
        // Done!
        process(flowOf())
        // Starting!
        // Nothing - emitting default value
        // On 0!
        // Done!
    }
}</code></pre>
<hr>
<h2 id="buffer-연산자">buffer 연산자</h2>
<p>buffer 연산자는 버퍼를 추가해서 다운스트림 플로우가 이미 배출된 원소를 처리하느라 바쁜 동안에도 업스트림 플로우가 원소를 배출할 수 있게 해줍니다. 즉, 업스트림 플로우의 실행을 다운스트림 플로우로부터 분리합니다.</p>
<pre><code class="language-kotlin">fun getAllUserIds(): Flow&lt;Int&gt; {
    return flow {
        repeat(3) {
            delay(200.milliseconds)
            log(&quot;Emitting!&quot;)
            emit(it)
        }
    }
}

suspend fun getProfileFromNetwork(id: Int): String {
    delay(2.seconds)
    return &quot;Profile[$id]&quot;
}

fun main() {
    val ids = getAllUserIds()
    runBlocking {
        ids
            .buffer(3)
            .map{ getProfileFromNetwork(it) }
            .collect{ log(&quot;Got $it&quot;) }
    }
}

// 433 [main @coroutine#2] Emitting!
// 638 [main @coroutine#2] Emitting!
// 839 [main @coroutine#2] Emitting!
// 2439 [main @coroutine#1] Got Profile[0]
// 4440 [main @coroutine#1] Got Profile[1]
// 6441 [main @coroutine#1] Got Profile[2]</code></pre>
<p>이 코드에서는 3개의 원소를 저장할 수 있는 버퍼를 추가하면 생성자는 새 사용자 식별자를 계속 생성해 버퍼에 넣을 수 있고, 수집자는 네트워크 요청을 계속 처리할 수 있습니다.</p>
<p>플로우에서 원소를 배출하고 처리하는 데 걸리는 시간이 변동될 때, 연산자 사슬에 버퍼를 도입하면 시스템 처리량을 늘리는 데 도움이 될 수 있습니다.</p>
<hr>
<h2 id="conflate-연산자">conflate 연산자</h2>
<p>conflate 연산자는 수집자가 바쁜 동안 배출된 항목을 그냥 버립니다. buffer와 마찬가지로 conflate를 쓰면 업스트림 플로우의 실행을 다운스트림 연산자의 실행과 분리할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*

fun main() {
    runBlocking {
        val temps = getTemperatures()
        temps
            .onEach {
                log(&quot;Read $it from sensor&quot;)
            }
            .conflate()
            .collect {
                log(&quot;Collected $it&quot;)
                delay(1.seconds)
            }
    }
}</code></pre>
<p>플로우 값이 빠르게 ‘구식’이 되고 다른 배출된 원소로 대체되는 경우 느린 수집자가 플로우에서 최신 원소만 처리하게 함으로써 성능을 유지할 수 있습니다.</p>
<hr>
<h2 id="debounce-연산자">debounce 연산자</h2>
<p>debounce 연산자는 업스트림에서 원소가 배출되지 않은 상태로 정해진 타임아웃 시간이 지나야만 항목을 다운스트림 플로우로 배출합니다. </p>
<pre><code class="language-kotlin">val searchQuery = flow {
    emit(&quot;K&quot;)
    delay(100.milliseconds)
    emit(&quot;Ko&quot;)
    delay(200.milliseconds)
    emit(&quot;Kotl&quot;)
    delay(500.milliseconds)
    emit(&quot;Kotlin&quot;)
}

fun main() = runBlocking {
    searchQuery
        .debounce(250.milliseconds)
        .collect {
            log(&quot;Searching for $it&quot;)
        }
}
// 743 [main @coroutine#1] Searching for Kotl
// 995 [main @coroutine#1] Searching for Kotlin</code></pre>
<hr>
<h2 id="flowon-연산자">flowOn 연산자</h2>
<p>flowOn 연산자는 withContext 함수와 비슷하게 코루틴 콘텍스트를 조정합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*

fun main() {
    runBlocking {
        flowOf(1)
            .onEach{ log(&quot;A&quot;) }
            .flowOn(Dispatchers.Default)
            .onEach{ log(&quot;B&quot;) }
            .flowOn(Dispatchers.IO)
            .onEach{ log(&quot;C&quot;) }
            .collect()
    }
}

// 159 [DefaultDispatcher-worker-3 @coroutine#3] A
// 165 [DefaultDispatcher-worker-1 @coroutine#2] B
// 177 [main @coroutine#1] C</code></pre>
<p>flowOn 연산자는 업스트림 플로우의 디스패처에만 영향을 미칩니다. 다운스트림 플로우는 영향을 받지 않으므로 이 연산자를 ‘콘텍스트 보존’ 연산자라고도 부릅니다.</p>
<hr>
<h1 id="최종-연산자는-업스트림-플로우를-실행하고-값을-계산한다">최종 연산자는 업스트림 플로우를 실행하고 값을 계산한다</h1>
<p>실행은 최종 연산자가 담당하며, 최종 연산자는 단일 값이나 값의 컬렉션을 계산하거나, 플로우의 실행을 촉발시켜 지정된 연산과 부소 효과를 수행합니다. 가장 일반적인 최종 연산자는 collect 입니다. </p>
<p>최종 연산자는 업스트림 플로우의 실행을 담당하기 때문에 항상 일시 중단 함수입니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 플로우 연산자에 대한 내용을 정리해봤습니다.</p>
<p>플로우를 다룰 때 상황과 필요에 맞는 다양한 연산자가 있으며, 이를 활용하면 플로우를 더 유연하게 다룰 수 있겠다는 생각이 들었습니다.</p>
<p>다음에는 오류 처리와 테스트에 대한 글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 16장 플로우]]></title>
            <link>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-16%EC%9E%A5-%ED%94%8C%EB%A1%9C%EC%9A%B0</link>
            <guid>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-16%EC%9E%A5-%ED%94%8C%EB%A1%9C%EC%9A%B0</guid>
            <pubDate>Thu, 19 Feb 2026 07:53:56 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>플로우에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 플로우가 무엇인지, 플로우가 어떻게 구성되어 있는지 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="플로우란">플로우란?</h1>
<p>플로우는 시간이 지남에 따라 나타나는 값과 작업할 수 있게 해주는 코루틴 기반의 추상화입니다. 플로우는 점진적인 로딩, 이벤트 스트림 작업, 구독 스타일 API를 모델링하는 데 사용할 수 있는 범용적인 추상화입니다.</p>
<p>플로우를 사용하면 시간이 지남에 따라 나타나는 여러 값을 다루는 상황에서 코틀린의 동시성 메커니즘을 활용할 수 있습니다.</p>
<p>플로우를 사용할 때는 flow 빌더 함수를 사용합니다. 플로우에 원소를 추가하려면 emit을 호출합니다. 빌더 함수 호출 후에는 collect 함수를 사용해 플로우의 원소를 순회할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.milliseconds

fun createValues(): Flow&lt;Int&gt; {
    return flow {
        emit(1)
        delay(1000.milliseconds)
        emit(2)
        delay(1000.milliseconds)
        emit(3)
        delay(1000.milliseconds)
    }
}

fun main() = runBlocking {
    val myFlowOfValues = createValues()
    myFlowOfValues.collect{ log(it) }
}

// 120 [main @coroutine#1] 1
// 1137 [main @coroutine#1] 2
// 2137 [main @coroutine#1] 3</code></pre>
<p>출력된 타임스탬프를 보면 원소가 배출되는 즉시 표시된다는 것을 알 수 있습니다. 즉, 모든 값을 계산할 때까지 기다릴 필요가 없는 것입니다.</p>
<hr>
<h1 id="플로우의-유형은">플로우의 유형은?</h1>
<p>플로우는 콜드 플로우와 핫 플로우라는 2가지 카테고리로 나뉩니다.</p>
<ul>
<li><p>콜드 플로우는 비동기 데이터 스트림으로, 값이 실제로 소비되기 시작할 때만 값을 배출합니다.</p>
</li>
<li><p>핫 플로우는 값이 실제로 소비되고 있는지와 상관없이 값을 독립적으로 배출하며, 브로드캐스트 방식으로 동작합니다.</p>
</li>
</ul>
<hr>
<h1 id="콜드-플로우란">콜드 플로우란?</h1>
<p>콜드 플로우는 flow라는 빌더 함수를 사용하여 생성할 수 있습니다. 빌더 함수 블록 안에서는 emit 함수를 호출해 플로우의 수집자에게 값을 제공하고, 수집자가 해당 값을 처리할 때가지 빌더 함수의 실행을 중단합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.milliseconds

fun main() {
    val letters = flow {
        log(&quot;Emitting A!&quot;)
        emit(&quot;A&quot;)
        delay(200.milliseconds)
        log(&quot;Emitting B!&quot;)
        emit(&quot;B&quot;)
    }
}</code></pre>
<p>이 코드를 동작시키면 아무런 출력도 나타나지 않습니다. 이는 빌더 함수가 연속적인 값의 스트림을 표현하는 Flow&lt;T&gt; 타입의 객체를 반환하기 때문입니다. 이 플로우는 처음에 비활성 상태이며, 최종 연산자가 호출돼야만 빌더에서 정의된 계산이 시작됩니다.</p>
<p>기본적으로 수집되기 시작할 때까지 비활성 상태입니다.</p>
<p>또한 빌더 함수 안의 코드는 플로우가 수집될 때만 실행되므로 시퀀스와 마찬가지로 무한 플로우를 정의하고 반환해도 괜찮습니다.</p>
<pre><code class="language-kotlin">val counterFlow = flow {
    var x = 0
    while(true) {
        emit(x++)
        delay(200.milliseconds)
    }
}</code></pre>
<hr>
<h2 id="콜드-플로우를-수집하는-작업은">콜드 플로우를 수집하는 작업은?</h2>
<p>Flow에 대해 collect 함수를 호출하면 그 로직이 실행됩니다. 플로우를 수집하는 코드를 수집자라고 부르는데, collect를 호출할 때 플로우에서 배출된 각 원소에 대해 호출될 람다를 제공할 수 있습니다.</p>
<p>플로우를 수집할 때는 플로우 내부의 일시 중단 코드를 실행하므로 collect는 일시 중단 함수이며, 플로우가 끝날 때까지 일시 중단됩니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.milliseconds

val letters = flow {
    log(&quot;Emitting A!&quot;)
    emit(&quot;A&quot;)
    delay(200.milliseconds)
    log(&quot;Emitting B!&quot;)
    emit(&quot;B&quot;)
}

fun main() = runBlocking {
    letters.collect {
        log(&quot;Collecting $it&quot;)
        delay(500.milliseconds)
    }
}

// 132 [main @coroutine#1] Emitting A!
// 137 [main @coroutine#1] Collecting A
// 852 [main @coroutine#1] Emitting B!
// 852 [main @coroutine#1] Collecting B</code></pre>
<p>출력의 타임스탬프를 보면 수집자가 플로우의 로직을 실행하는 책임이 있습니다. 원소 A와 B 사이의 지연 시간은 약 700밀리초입니다. 이는 수집자가 플로우 빌더에 정의된 로직의 실행을 촉발해서 첫 번째 배출을 발생시키고, 수집자와 연결된 람다가 호출되면서 메시지를 기록하고 500밀리초 동안 지연됩니다. 그 후 플로우 람다가 계속 실행되며 200밀리초 동안 추가 지연과 배출이 발생합니다.</p>
<p>콜드 플로우에서 collect를 여러 번 호출하면 그 코드가 여러 번 실행됩니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds

fun main() = runBlocking {
    letters.collect {
        log(&quot;(1) Collecting $it&quot;)
        delay(500.milliseconds)
    }
    letters.collect {
        log(&quot;(2) Collecting $it&quot;)
        delay(500.milliseconds)
    }
}

// 126 [main @coroutine#1] Emitting A!
// 135 [main @coroutine#1] (1) Collecting A
// 845 [main @coroutine#1] Emitting B!
// 846 [main @coroutine#1] (1) Collecting B
// 1347 [main @coroutine#1] Emitting A!
// 1347 [main @coroutine#1] (2) Collecting A
// 2048 [main @coroutine#1] Emitting B!
// 2048 [main @coroutine#1] (2) Collecting B</code></pre>
<p>collect 함수는 플로우의 모든 원소가 처리될 때까지 일시 중단됩니다.</p>
<hr>
<h2 id="플로우-수집을-취소하기">플로우 수집을 취소하기</h2>
<p>코루틴을 취소하는 메커니즘은 플로우 수집자에게도 적용됩니다. 수집자의 코루틴을 취소하면 다음 취소 지점에서 플로우 수집이 중단됩니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds

fun main() = runBlocking {
    val collector = launch {
        counterFlow.collect {
            println(it)
        }
    }
    delay(5.seconds)
    collector.cancel()
}
// 0
// 1
// ...
// 24</code></pre>
<hr>
<h2 id="콜드-플로우의-내부-구현은">콜드 플로우의 내부 구현은?</h2>
<p>코틀린의 콜드 플로우는 일시 중단 함수와 수신 객체 지정 람다를 결합한 조합입니다. 콜드 플로우의 정의는 매우 간단하며 Flow와 FlowCollector라는 2가지 인터페이스만 필요합니다.</p>
<pre><code class="language-kotlin">interface Flow&lt;T&gt; {
    suspend fun collect(collector: FlowCollector&lt;T&gt;)
}

interface FlowCollector&lt;T&gt; {
    suspend fun emit(value: T)
}</code></pre>
<p>flow 빌더 함수를 사용해 플로우를 정의할 때 제공된 람다의 수신 객체 타입은 FlowCollector입니다. 이 때문에 빌더 안에서 emit 함수를 호출할 수 있고, emit 함수는 collect 함수에 전달된 람다를 호출하기에, 결과적으로 두 람다가 서로 호출하는 구조를 갖습니다.</p>
<pre><code class="language-kotlin">val letters = flow {
    delay(300.milliseconds)
    emit(&quot;A&quot;)
    delay(300.milliseconds)
    emit(&quot;B&quot;)
}

letters.collect { letter -&gt;
    println(letter)
    delay(200.milliseconds)
}</code></pre>
<hr>
<h2 id="채널-플로우를-사용한-동시성-플로우는">채널 플로우를 사용한 동시성 플로우는?</h2>
<p>지금까지 flow 빌더 함수를 사용해 만든 콜드 플로우는 모두 순차적으로 실행되며, 코드 블록은 일시 중단 함수의 본문처럼 하나의 코루틴으로 실행됩니다. 하지만 async와 같은 동시성으로 수행하기 적합하고 병렬로 수행하면 더 좋을 것 같은 코드도 있을 것입니다.</p>
<p>하지만 그렇게 하면 오류가 나타납니다. 이는 콜드 플로우 추상화가 같은 코루틴 안에서만 emit 함수를 호출할 수 있게 허용하기 때문입니다. 이런 상황일 때 여러 코루틴에서 배출을 허용하는 동시성 플로우를 작성하게 해주는 빌더가 필요한데, 이런 플로우를 <strong>채널 플로우</strong>라고 합니다.</p>
<p>채널 플로우는 channelFlow 빌더 함수로 만들 수 있으며, 콜드 플로우의 특별한 유형입니다.</p>
<p>채널 플로우는 순차적으로 배출을 허용하는 emit 함수를 제공하지 않는 대신 여러 코루틴에서 send를 사용해 값을 제공할 수 있습니다. 플로우의 수집자는 여전히 값을 순차적으로 수신하며, collect 람다가 그 작업을 수행합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch

suspend fun getRandomNumber(): Int {
    delay(500.milliseconds)
    return Random.nextInt()
}

val randomNumbers = channelFlow {
    repeat(10) {
        launch {
            send(getRandomNumber())
        }
    }
}

fun main() = runBlocking {
    randomNumbers.collect {
        log(it)
    }
}

// 668 [main @coroutine#1] -1309714929
// 670 [main @coroutine#1] 481147455
// 670 [main @coroutine#1] 595734353
// 670 [main @coroutine#1] 1738511056
// 670 [main @coroutine#1] 1680382856
// 671 [main @coroutine#1] 468100645
// 671 [main @coroutine#1] -621640135
// 671 [main @coroutine#1] -2125216273
// 671 [main @coroutine#1] -1894456128
// 671 [main @coroutine#1] -1665117038</code></pre>
<p>채널 플로우를 사용하면 동시적으로 실행되며 전체 실행 시간이 줄어드는 것을 확인할 수 있습니다.</p>
<p>일반적인 콜드 플로우와 채널 플로우 중 어떤 것을 쓸지 결정할 때는 플로우 안에서 새로운 코루틴을 시작하는 경우에만 채널 플로우를 선택하고, 그렇지 않으면 일반적인 콜드 플로우를 선택하는 편이 더 낫습니다.</p>
<hr>
<h1 id="핫-플로우">핫 플로우</h1>
<p>핫 플로우에서는 각 수집자가 플로우 로직 실행을 독립적으로 촉발하는 대신, 여러 구독자라고 불리는 수집자들이 배출된 항목을 공유합니다. 이는 시스템에서 이벤트나 상태 변경이 발생해서 수집자가 존재하는지 여부에 상관없이 값을 배출해야 하는 경우에 적합합니다.</p>
<p>핫 플로우는 항상 활성 상태이기 때문에 구도갖의 유무에 관계없이 배출이 발생할 수 있습니다. 코루틴에는 2가지 핫 플로우 구현이 기본적으로 제공됩니다.</p>
<ul>
<li>공유 플로우는 값을 브로드캐스트하기 위해 사용됩니다.</li>
<li>상태 플로우는 상태를 전달하는 특별한 경우에 사용됩니다.</li>
</ul>
<hr>
<h2 id="공유-플로우는">공유 플로우는?</h2>
<p>공유 플로우는 구독자가 존재하는지 여부에 상관없이 배출이 발생하는 브로드캐스트 방식으로 동작합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.random.*
import kotlin.time.Duration.Companion.milliseconds

class RadioStation {
    private val _messageFlow = MutableSharedFlow&lt;Int&gt;()
    val messageFlow = _messageFlow.asSharedFlow()

    fun beginBroadcasting(scope: CoroutineScope) {
        scope.launch {
            while(true) {
                delay(500.milliseconds)
                val number = Random.nextInt(0..10)
                log(&quot;Emitting $number!&quot;)
                _messageFlow.emit(number)
            }
        }
    }
}</code></pre>
<p>핫 플로우를 만들 때는 플로우 빌더를 사용하는 대신 가변적인 플로우에 대한 참조를 얻습니다. 배출이 구독자 유무와 관계없이 발생하므로 당사자가 실제 배출을 수행하는 코루틴을 시작할 책임이 있습니다. 이는 별다른 어려움 없이 여러 코루틴에서 가변 공유 플로우에 값을 배출할 수 있다는 뜻이기도 합니다.</p>
<p>RadioStation 클래스의 인스턴스를 생성하고 beginBroadcasting 함수를 호출하면 구독자가 없어도 브로드캐스트가 즉시 시작됩니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    RadioStation().beginBroadcasting(this)
}</code></pre>
<p>구독자를 추가하는 방법은 콜드 플로우를 수집하는 것과 동일하게 collect를 호출하면 됩니다. 배출이 발생할 때마다 제공한 람다가 실행되지만 구독자는 구독 시작 이후에 배출된 값만 수신한다는 점을 유의해야 합니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    val radioStation = RadioStation()
    radioStation.beginBroadcasting(this)
    delay(600.milliseconds)
    radioStation.messageFlow.collect {
        log(&quot;A collecting $it!&quot;)
    }
}</code></pre>
<p>공유 플로우는 브로드캐스트 방식으로 작동하기 때문에 구독자를 추가해서 이미 존재하는 messageFlow의 배출을 수신할 수 있습니다. 하지만 공유 플로우 구독자는 구독을 시작한 이후에 배출된 값만 수신합니다. 구독자가 구독 이전에 배출된 원소도 수신하기를 원한다면 MutableSharedFlow를 생성할 때 replay 파라미터를 사용해 새 구독자를 위해 제공할 값의 캐시를 설정할 수 있습니다.</p>
<pre><code class="language-kotlin">private val _messageFlow = MutableSharedFlow&lt;Int&gt;(replay = 5)</code></pre>
<p>이렇게 바꾸고 나면 600밀리초가 지난 다음에 수집자를 시작하더라도 구독 직전에 발생한 최대 5개의 값을 수신할 수 있습니다.</p>
<hr>
<h2 id="콜드-플로우를-공유-플로우로-전환하기">콜드 플로우를 공유 플로우로 전환하기</h2>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.random.*
import kotlin.time.Duration.Companion.milliseconds

fun querySensor(): Int = Random.nextInt(-10..30)

fun getTemperatures(): Flow&lt;Int&gt; {
    return flow {
        while(true) {
            emit(querySensor())
            delay(500.milliseconds)
        }
    }
}
</code></pre>
<p>이 코드는 500밀리초 간격으로  수집되는 값의 스트림을 제공하는 함수입니다. 이 함수를 여러 번 호출하려 한다면 각 수집자가 센서에 독립적으로 질의를 하게 됩니다.</p>
<pre><code class="language-kotlin">fun celsiusToFahrenheit(celsius: Int) = celsius * 9.0 / 5.0 + 32.0

fun main() {
    val temps = getTemperatures()
    runBlocking {
        launch {
            temps.collect {
                log(&quot;$it Celsius&quot;)
            }
        }
        launch {
            temps.collect {
                log(&quot;${celsiusToFahrenheit(it)} Fahrenheit&quot;)
            }
        }
    }
}</code></pre>
<p>이런 경우 반환된 플로우를 두 수집자가 공유해야하며, 이들 무도 같은 원소를 받아야 합니다.</p>
<p>shareIn 함수를 사용하면 주어진 콜드 플로우를 한 플로우인 공유 플로우로 변환할 수 있습니다. 이 변환은 플로우의 코드가 실행되게 하므로 shareIn을 코루틴 안에서 호출해야 합니다.</p>
<p>shareIn은 CoroutineScope 타입의 scope 파라미터와 플로우가 실제로 언제 시작돼야 하는지 정의하는 started 파라미터를 사용합니다.</p>
<p>started는 아래와 같은 여러 가지 다른 동작을 지정할 수 있습니다.</p>
<ul>
<li>Eagerly는 플로우 수집을 즉시 시작합니다.</li>
<li>Lazily는 첫 번째 구독자가 나타나야만 수집을 시작합니다.</li>
<li>WhileSubscribed는 첫 번째 구독자가 나타나야 수집을 시작하고, 마지막 구독자가 사라지면 플로우 수집을 취소합니다.</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val temps = getTemperatures()
    runBlocking {
        val sharedTemps = temps.shareIn(this, SharingStarted.Lazily)
        launch {
            sharedTemps.collect {
                log(&quot;$it Celsius&quot;)
            }
        }
        launch {
            sharedTemps.collect {
                log(&quot;${celsiusToFahrenheit(it)} Fahrenheit&quot;)
            }
        }
    }
}</code></pre>
<hr>
<h2 id="상태-플로우는">상태 플로우는?</h2>
<p>상태 플로우는 변수의 상태 변화를 쉽게 추적할 수 있는 공유 플로우의 특별한 버전입니다. 상태 플로우를 생성하는 방법은 공유 플로우를 생성하는 것과 비슷합니다. 클래스의 private 속성으로 MutableStateFlow를 생성하고, 같은 변수의 읽기 전용 StateFlow버전을 노출합니다.</p>
<p>값을 배출하는 emit을 사용하는 대신, 값을 갱신하는 update 함수를 사용합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

class ViewCounter {
    private val _counter = MutableStateFlow(0)
    val counter = _counter.asStateFlow()
    fun increment() {
        _counter.update{ it + 1 }
    }
}

fun main() {
    val vc = ViewCounter()
    vc.increment()
    println(vc.counter.value)
    // 1
}</code></pre>
<p>가변 상태 플로우로 표현한 현재 상태를 value 속성으로 접근할 수 있습니다. 이 속성은 일시 중단 없이 값을 안전하게 읽을 수 있게 해줍니다.</p>
<p>또한 공유 플로우처럼 상태 플로우도 collect 함수를 호출해 시간에 따라 값을 구독할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

enum class Direction{ LEFT, RIGHT }

class DirectionSelector {
    private val _direction = MutableStateFlow(Direction.LEFT)
    val direction = _direction.asStateFlow()

    fun turn(d: Direction) {
        _direction.update{ d }
    }
}

fun main() = runBlocking {
    val switch = DirectionSelector()
    launch {
        switch.direction.collect {
            log(&quot;Direction now $it&quot;)
        }
    }
    delay(200.milliseconds)
    switch.turn(Direction.RIGHT)
    delay(200.milliseconds)
    switch.turn(Direction.LEFT)
    delay(200.milliseconds)
    switch.turn(Direction.LEFT)
}</code></pre>
<p>이 코드를 출력해보면 LEFT라는 인자를 2번 연속으로 전달했음에도 구독자가 한 번만 호출된다는 점을 볼 수 있습니다. 이는 상태 플로우가 동등성 기반 통합을 수행하기 때문입니다. 즉, 값이 실제로 달라졌을 때만 구독자에게 값을 배출한다는 뜻입니다. 이전 값과 새 값이 같으면 배출이 발생하지 않습니다.</p>
<hr>
<h2 id="콜드-플로우를-상태-플로우로-변환하기">콜드 플로우를 상태 플로우로 변환하기</h2>
<p>stateIn 함수를 사용해 콜드 플로우를 상태 플로우로 변환할 수 있습니다. 이렇게 하면 원래 플로우에서 배출된 최신 값을 항상 읽을 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds

fun main() {
    val temps = getTemperatures()
    runBlocking {
        val tempState = temps.stateIn(this)
        println(tempState.value)
        delay(800.milliseconds)
        println(tempState.value)
    }
}</code></pre>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 플로우가 무엇인지, 어떻게 나눠지며 사용되는지 등에 대해 정리해봤습니다.</p>
<p>코루틴만 사용했을 때는 delay를 선언해도 여러 값을 순차적으로 가져오는 동작이 되지 않고, 코루틴 주기가 끝난 후에 한 번에 받아오는 것만이 가능했습니다. 하지만 플로우라는 개념을 배우고 데이터를 받는 즉시 불러올 수 있는 것을 알게 되었습니다.</p>
<p>또한 콜드 플로우와 핫 플로우라는 것을 사용하여 하나의 수집자 또는 여러 개의 수집자에게 값을 배출할 수 있어 상황에 따라 유연하게 플로우를 적용할 수 있다는 사실을 배웠습니다.</p>
<p>다음에는 플로우 연산자에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 15장 구조화된 동시성]]></title>
            <link>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</link>
            <guid>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</guid>
            <pubDate>Wed, 18 Feb 2026 06:54:02 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>구조화된 동시성에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 구조화된 동시성이 무엇인지, 코루틴 스코프가 무엇인지 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="구조화된-동시성은">구조화된 동시성은?</h1>
<p>애플리케이션 안에서 코루틴과 그 생애 주기의 계층을 관리하고 추적할 수 있는 기능이 코루틴의 핵심에 내장돼있으며, 구조화된 동시성은 수동으로 시작된 각 코루틴을 일일히 추적하지 않아도 기본적으로 작동합니다.</p>
<p>구조화된 동시성을 통해 각 코루틴은 <strong>코루틴 스코프</strong>에 속하게 됩니다.</p>
<p>코루틴 스코프는 코루틴 간의 부모-자식 관계를 확립하는데 도움을 줍니다. launch와 async 코루틴 빌더 함수들은 CoroutineScope 인터페이스의 확장 함수입니다.</p>
<p>launch나 async를 사용해 새로운 코루틴을 만들면 새로운 코루틴은 자동으로 해당 코루틴의 자식이 되며, 부모 코루틴은 자식 코루틴이 완료될 때까지 프로그램이 종료되지 않습니다.</p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        launch {
            delay(1.seconds)
            launch {
                delay(250.milliseconds)
                log(&quot;Grandchild done&quot;)
            }
            log(&quot;Child 1 done!&quot;)
        }
        launch {
            delay(500.milliseconds)
            log(&quot;child 2 done!&quot;)
        }
        log(&quot;Parent done!&quot;)
    }
}

// 135 [main @coroutine#1] Parent done!
// 657 [main @coroutine#3] child 2 done!
// 1154 [main @coroutine#2] Child 1 done!
// 1405 [main @coroutine#4] Grandchild done</code></pre>
<p>이는 구조화된 동시성 덕분입니다. 코루틴 간에는 부모-자식 관계가 있으므로 runBlocking은 여전히 어떤 자식 코루틴이 작업 중인지 알고, 모든 작업이 완료될 때까지 기다립니다. 또한 실행한 코루틴이나 그 자손을 수동으로 추적할 필요가 없으며, 수동으로 await를 호출할 필요도 없습니다. 구조화된 동시성이 이를 자동으로 처리하기 때문입니다.</p>
<hr>
<h2 id="코루틴-스코프-생성하기">코루틴 스코프 생성하기</h2>
<p>새로운 코루틴을 만들면 코루틴 자체는 CoroutineScope를 생성합니다. 하지만 새로운 코루틴을 만들지 않고도 코루틴 스코프를 그룹화할 수 있는데, 이 때 coroutineScope 함수를 사용할 수 있습니다.</p>
<p>coroutineScope는 일시 중단 함수로, 새로운 코루틴 스코프를 생성하고 해당 영역 안의 모든 자식 코루틴이 완료될 때까지 기다립니다. 이 함수의 전형적인 사용 사례는 동시적 작업 분해로 여러 코루틴을 활용해 계산을 수행하는 것입니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds

suspend fun generateValue(): Int {
    delay(500.milliseconds)
    return Random.nextInt(0, 10)
}

suspend fun computeSum() {
    log(&quot;Computing a sum...&quot;)
    val sum = coroutineScope {
        val a = async { generateValue() }
        val b = async { generateValue() }
        a.await() + b.await()
    }
    log(&quot;Sum is $sum&quot;)
}

fun main() = runBlocking {
    computeSum()
}

// 107 [main @coroutine#1] Computing a sum...
// 643 [main @coroutine#1] Sum is 4</code></pre>
<p>coroutineScope를 사용하여 두 코루틴 a, b를 병렬로 실행하며, 그 결과를 사용해 값을 계산해 로그에 남깁니다.</p>
<hr>
<h2 id="코루틴-스코프를-컴포넌트와-연관시키기">코루틴 스코프를 컴포넌트와 연관시키기</h2>
<p>coroutineScope 함수가 작업을 분해하는 데 사용되는 반면 구체적 생명주기를 정의하고, 동시 처리나 코루틴의 시작과 종료를 관리하는 클래스를 만들고 싶을 때도 있습니다. 이런 시나리오에서는 CoroutineScope 생성자 함수를 사용해 새로운 독자적인 코루틴 스코프를 생성할 수 있습니다.</p>
<p>이 함수는 실행을 일시 중단하지 않으며, 단순히 새로운 코루틴을 시작할 때 쓸 수 있는 새로운 코루틴 스코프를 생성하기만 합니다.</p>
<p>CoroutineScope는 해당 코루틴 스코프와 연관된 코루틴 콘텍스트를 파라미터로 받습니다. 기본적으로 CoroutineScope를 디스패처만으로 호출하면 새로운 Job이 자동으로 생성됩니다. 하지만 대부분 실무에서는 CoroutineScope와 함께 SupervisorJob을 사용하는 것이 좋습니다.</p>
<p>SupervisorJob은 동일한 영역과 관련된 다른 코루틴을 취소하지 않고, 처리되지 않은 예외를 전파하지 않게 해주는 특수한 Job입니다.</p>
<pre><code class="language-kotlin">class ComponentWithScope(dispatcher: CoroutineDispatcher = 
    Dispatchers.Default) {
    private val scope = CoroutineScope(dispatcher + SupervisorJob())

    fun start() {
        log(&quot;Starting!&quot;)
        scope.launch {
            while(true) {
                delay(500.milliseconds)
                log(&quot;Component working!&quot;)
            }
        }
        scope.launch {
            log(&quot;Doing a one-off task...&quot;)
            delay(500.milliseconds)
            log(&quot;Task done!&quot;)
        }
    }

    fun stop() {
        log(&quot;Stopping!&quot;)
        scope.cancel()
    }
}

fun main() {
    val c = ComponentWithScope()
    c.start()
    Thread.sleep(2000)
    c.stop()
}

// 60 [main] Starting!
// 134 [DefaultDispatcher-worker-2 @coroutine#2] Doing a one-off task...
// 642 [DefaultDispatcher-worker-2 @coroutine#2] Task done!
// 642 [DefaultDispatcher-worker-1 @coroutine#1] Component working!
// 1143 [DefaultDispatcher-worker-2 @coroutine#1] Component working!
// 1644 [DefaultDispatcher-worker-2 @coroutine#1] Component working!
// 2115 [main] Stopping!</code></pre>
<p>Component 클래스의 인스턴스를 생성하고 start를 호출하면 컴포넌트 내부에서 코루틴이 시작됩니다. 그 후 stop을 호출하면 컴포넌트의 생명주기가 종료됩니다.</p>
<blockquote>
<p>coroutineScope와 CoroutineScope</p>
</blockquote>
<ul>
<li>coroutineScope는 작업의 동시성을 실행하기 위해 분해할 때 사용합니다. 여러 코루틴을 시작하고, 그들이 모두 완료될 때까지 기다리며, 결과를 계산할 수도 있습니다.</li>
<li>CoroutineScope는 코루틴을 클래스의 생명주기와 연관시키는 영역을 생성할 때 쓰입니다. 이 함수는 영역을 생성하지만 추가 작업을 기다리지 않고 즉시 반환됩니다.</li>
</ul>
<hr>
<h1 id="globalscope란">GlobalScope란?</h1>
<p>GlobalScope는 전역 수준에 존재하는 코루틴 스코프입니다. 이는 코루틴을 전역적으로 사용할 수 있기 때문에 매력적으로 보이지만 몇 가지 단점이 있습니다.</p>
<p>구조화된 동시성이 제공하는 모든 이점을 포기해야 합니다. 전역 범위에서 시작된 코루틴은 자동으로 취소되지 않으며, 생명주기에 대한 개념도 없기에 리소스 누수가 발생하거나 불필요한 작업을 계속 수행하면서 계산 자원을 낭비하게 될 가능성이 큽니다.</p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        GlobalScope.launch {
            delay(1000.milliseconds)
            launch {
                delay(250.milliseconds)
                log(&quot;Grandchild done&quot;)
            }
            log(&quot;Child 1 done!&quot;)
        }
        GlobalScope.launch {
            delay(500.milliseconds)
            log(&quot;Child 2 done!&quot;)
        }
        log(&quot;Parent done!&quot;)
    }
}
// 131 [main @coroutine#1] Parent done!</code></pre>
<p>이 코드는 GlobalScope를 사용함으로써 구조화된 동시성에서 자동으로 설정되는 계층 구조가 깨져서 즉시 종료됩니다. 따라서 부모 코루틴이 없으므로 자식 코루틴이 완료되기 전에 종료되는 것입니다.</p>
<hr>
<h1 id="코루틴-콘텍스트와-구조화된-동시성">코루틴 콘텍스트와 구조화된 동시성</h1>
<p>코루틴 콘텍스트는 구조화된 동시성 개념과 밀접한 관련이 있으며, 이는 코루틴 간의 부모-자식 계층을 따라 상속됩니다. 새로운 코루틴을 시작할 때 코루틴 콘텍스트에서는 먼저 자식 코루틴은 부모의 콘텍스트를 상속받습니다. 그런 다음 새로운 코루틴은 부모-자식 관계를 설정하는 역할을 하는 새 Job 객체를 생성합니다.</p>
<p>이런 관계이기 때문에 디스패처를 지정하지 않고 새로운 코루틴을 시작한다면 Dispatchers.Default가 아니라 부모 코루틴의 디스패처에서 실행됩니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.job

fun main() = runBlocking(CoroutineName(&quot;A&quot;)) {
    log(&quot;A&#39;s job: ${coroutineContext.job}&quot;)
    launch(CoroutineName(&quot;B&quot;)) {
        log(&quot;B&#39;s job: ${coroutineContext.job}&quot;)
        log(&quot;B&#39;s parent: ${coroutineContext.job.parent}&quot;)
    }
    log(&quot;A&#39;s children: ${coroutineContext.job.children.toList()}&quot;)
}

// 122 [main @A#1] A&#39;s job: &quot;A#1&quot;:BlockingCoroutine{Active}@763d9750
// 140 [main @A#1] A&#39;s children: [&quot;B#2&quot;:StandaloneCoroutine{Active}@3c5a99da]
// 142 [main @B#2] B&#39;s job: &quot;B#2&quot;:StandaloneCoroutine{Active}@3c5a99da
// 142 [main @B#2] B&#39;s parent: &quot;A#1&quot;:BlockingCoroutine{Completing}@763d9750</code></pre>
<hr>
<h1 id="취소란">취소란?</h1>
<p>취소는 코드가 완료되기 전에 실행을 중단하는 것을 의미합니다. 겉으로는 취소가 흔한 일이 아닐 것처럼 보일 수 있지만, 거의 모든 현대 애플리케이션은 계산 작업을 취소할 수 있어야 견고하고 효율적입니다.</p>
<ul>
<li>취소는 불필요한 작업을 막아줍니다.</li>
<li>취소는 메모리나 리소스 누수를 방지하는데 도움을 줍니다.</li>
<li>취소는 오류 처리에서도 중요한 역할을 합니다.</li>
</ul>
<hr>
<h2 id="취소-촉발">취소 촉발</h2>
<p>launch 코루틴 빌더는 Job을 반환하고 async 코루틴 빌더는 Deferred를 반환하지만, 둘 다 cancel을 호출해 해당 코루틴의 취소를 촉발할 수 있습니다.</p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        val launchedJob = launch {
            log(&quot;I&#39;m launched!&quot;)
            delay(1000.milliseconds)
            log(&quot;I&#39;m done!&quot;)
        }
        val asyncDeferred = async {
            log(&quot;I&#39;m async&quot;)
            delay(1000.milliseconds)
            log(&quot;I&#39;m done!&quot;)
        }
        delay(200.milliseconds)
        launchedJob.cancel()
        asyncDeferred.cancel()
    }
}

// 153 [main @coroutine#2] I&#39;m launched!
// 153 [main @coroutine#3] I&#39;m async</code></pre>
<hr>
<h2 id="시간제한이-초과된-후-자동으로-취소-호출하기">시간제한이 초과된 후 자동으로 취소 호출하기</h2>
<p>withTimeout과 withTimeoutOrNull 함수는 계산에 쓸 최대 시간을 제한하면서 값을 계산할 수 있게 해줍니다. </p>
<p>withTimout함수는 타임아웃이 되면 예외를 발생시킵니다. 타입아웃을 처리하려면 try 블록으로 withTimeout 호출을 감싸고 발생한 TimeoutCancellationException을 잡아내야 합니다.</p>
<p>비슷하게 withTimeoutOrNull 함수는 타임아웃이 발생하면 null을 반환합니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.milliseconds

suspend fun calculateSomething(): Int {
    delay(3.seconds)
    return 2 + 2
}

fun main() = runBlocking {
    val quickResult = withTimeoutOrNull(500.milliseconds) {
        calculateSomething()
    }
    println(quickResult)
    // null
    val slowResult = withTimeoutOrNull(5.seconds) {
        calculateSomething()
    }
    println(slowResult)
    // 4
}</code></pre>
<hr>
<h2 id="취소는-모든-자식-코루틴에게-전파된다">취소는 모든 자식 코루틴에게 전파된다</h2>
<p>코루틴을 취소하면 해당 코루틴의 모든 자식 코루틴도 자동으로 취소됩니다. 그렇기에 불필요한 작업을 계속하거나 불필요하게 데이터를 메모리에 더 오래 유지하는 코루틴이 남지 않습니다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking {
    val job = launch {
        launch {
            launch {
                launch {
                    log(&quot;I&#39;m started&quot;)
                    delay(500.milliseconds)
                    log(&quot;I&#39;m done!&quot;)
                }
            }
        }
    }
    delay(200.milliseconds)
    job.cancel()
}

// 130 [main @coroutine#5] I&#39;m started</code></pre>
<hr>
<h2 id="취소-메커니즘은">취소 메커니즘은?</h2>
<p>취소 메커니즘은 CancellationException이라는 특수한 예외를 특별한 지점에서 던지는 방식으로 작동합니다.</p>
<p>취소된 코루틴은 일시 중단 지점에서 CancellationException을 던집니다. 코루틴은 예외를 사용해 코루틴 계층에서 취소를 전파하기 때문에 이 예외를 실수로 삼켜버리거나 직접 처리하지 않도록 주의해야 합니다.</p>
<pre><code class="language-kotlin">suspend fun doWork() {
    delay(500.milliseconds)
    throw UnsupportedOperationException(&quot;Didn&#39;t work!&quot;)
}

fun main() {
    runBlocking {
        withTimeoutOrNull(2.seconds) {
            while(true) {
                try {
                    doWork()
                } catch(e: Exception) {
                    println(&quot;Oops: ${e.message}&quot;)
                }
            }
        }
    }
}
// Oops: Didn&#39;t work!
// Oops: Didn&#39;t work!
// Oops: Didn&#39;t work!
// Oops: Timed out waiting for 2000ms
// ....</code></pre>
<p>위 코드는 catch에서 모든 종류의 예외를 잡기 때문에 안에서 던진 예외가 밖으로 전달되지 못해 무한히 반복되는 현상이 나타납니다.</p>
<p>코틀린 코루틴에 기본적으로 포함된 모든 함수는 취소가 가능합니다. 하지만 직접 작성한 코드에서는 직접 코루틴을 취소 가능하게 만들어야 합니다.</p>
<pre><code class="language-kotlin">suspend fun doCpuHeavyWork(): Int {
    log(&quot;I&#39;m doing work!&quot;)
    var counter = 0
    val startTime = System.currentTimeMillis()
    while (System.currentTimeMillis() &lt; startTime + 500) {
        counter++
    }
    return counter
}

fun main() {
    runBlocking {
        val myJob = launch {
            repeat(5) {
                doCpuHeavyWork()
            }
        }
        delay(600.milliseconds)
        myJob.cancel()
    }
}

// 142 [main @coroutine#2] I&#39;m doing work!
// 643 [main @coroutine#2] I&#39;m doing work!
// 1143 [main @coroutine#2] I&#39;m doing work!
// 1643 [main @coroutine#2] I&#39;m doing work!
// 2143 [main @coroutine#2] I&#39;m doing work!</code></pre>
<p>위 코드에서는 2번 출력되고 종료될 줄 알았지만 5번 전부 출력되는 결과가 나타납니다. 취소는 함수 안의 일시 중단 지점에서 CancellationException을 던지는 방식으로 작동합니다. 여기서 doCpuHeavyWork 함수는 suspend 변경자로 표시돼 있지만 일시 중단 지점을 포함하고 있지 않습니다. 그렇기에 계속해서 작업을 수행하는 것입니다.</p>
<p>일시 중단 함수는 스스로 취소 가능하게 로직을 제공해야 합니다. delay()를 중간에 넣어 인위적으로 취소 지점을 추가해줄 수도 있지만, 코루틴에는 코드를 취소 가능하게 만드는 유틸리티 함수들이 있습니다.</p>
<hr>
<h2 id="코루틴이-취소됐는지-확인하기">코루틴이 취소됐는지 확인하기</h2>
<p>코루틴이 취소됐는지 확인할 때는 CoroutineScope의 isActive 속성을 확인합니다. 이 값이 false라면 코루틴은 더 이상 활성 상태가 아닙니다.</p>
<pre><code class="language-kotlin">val myJob = launch {
    repeat(5) {
        doCpuHeavyWork()
        if(!isActive) return@launch
    }
}</code></pre>
<p>isActive를 확인해서 false일 때 명시적으로 반환하는 대신, 코틀린 코루틴은 편의 함수로 ensureActive를 제공합니다. 이 함수는 코루틴이 더 이상 활성 상태가 아닐 경우 CancelationException을 던집니다.</p>
<pre><code class="language-kotlin">val myJob = launch {
    repeat(5) {
        doCpuHeavyWork()
        ensureActive()
    }
}</code></pre>
<hr>
<h2 id="다른-코루틴에게-기회를-주기">다른 코루틴에게 기회를 주기</h2>
<p>yield 함수는 코드 안에서 취소 가능 지점을 제공할 뿐만 아니라 현재 점유된 디스패처에서 다른 코루틴이 작업할 수 있게 해줍니다.</p>
<pre><code class="language-kotlin">suspend fun doCpuHeavyWork(): Int {
    log(&quot;I&#39;m doing work!&quot;)
    var counter = 0
    val startTime = System.currentTimeMillis()
    while (System.currentTimeMillis() &lt; startTime + 500) {
        counter++
        yield()
    }
    return counter
}

fun main() {
    runBlocking {
        launch {
            repeat(3) {
                doCpuHeavyWork()
            }
        }
        launch {
            repeat(3) {
                doCpuHeavyWork()
            }
        }
    }
}

// 137 [main @coroutine#2] I&#39;m doing work!
// 139 [main @coroutine#3] I&#39;m doing work!
// 638 [main @coroutine#2] I&#39;m doing work!
// 643 [main @coroutine#3] I&#39;m doing work!
// 1138 [main @coroutine#2] I&#39;m doing work!
// 1143 [main @coroutine#3] I&#39;m doing work!</code></pre>
<p>yield 함수를 호출하면 서로 다른 코루틴들이 교차 실행되며 두 코루틴이 번갈아가며 작업을 처리할 수 있게 됩니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 구조화된 동시성에 대해 정리해봤습니다.</p>
<p>지난 장에서 배웠던 코루틴 빌드 함수가 coroutineScope의 확장함수이며, 새로운 코루틴을 생성할 때마다 coroutineScope가 생성되며, 각 코루틴 스코프는 부모-자식 관계를 자동으로 가지게 되어 편리하게 활용할 수 있다는 점을 알게 되었습니다.</p>
<p>또한 코루틴 관련 함수면 취소가 무조건 된다고 생각했었지만, 직접 만든 코루틴 함수는 취소가 될 수 있도록 직접 선언해줘야한다는 사실도 배웠습니다.</p>
<p>다음에는 플로우에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 14장 코루틴]]></title>
            <link>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-14%EC%9E%A5-%EC%BD%94%EB%A3%A8%ED%8B%B4</link>
            <guid>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-14%EC%9E%A5-%EC%BD%94%EB%A3%A8%ED%8B%B4</guid>
            <pubDate>Tue, 17 Feb 2026 12:41:50 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>코루틴의 개념에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 동시성과 병렬성이 무엇인지, 코루틴이 무엇이고 어떻게 사용되는지 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="동시성과-병렬성이란">동시성과 병렬성이란?</h1>
<p><strong>동시성</strong>은 코드를 여러 부분으로 나눠서 동시에 수행할 수 있는 능력을 말하고, <strong>병렬성</strong>은 여러 작업을 여러 CPU 코어에서 물리적으로 동시에 실행하는 것을 말합니다. </p>
<hr>
<h1 id="코틀린의-동시성-처리-방법은">코틀린의 동시성 처리 방법은?</h1>
<p>코틀린은 <strong>코루틴</strong>이라는 것을 사용해 동시성을 처리합니다. 코루틴은 비동기적으로 실행되는 넌블로킹 동시성 코드를 우아하게 작성할 수 있게 해줍니다. 스레드와 비교했을 때 훨씬 더 가볍게 작동하며, 구조화된 동시성을 통해 동시성 작업과 그 생명주기를 관리할 수 있는 기능도 제공합니다.</p>
<p>코틀린에서 코루틴을 사용할 때 기본적인 추상화인 <strong>일시 중단 함수</strong>를 사용합니다.</p>
<p>일시 중단 함수는 스레드를 블록시키는 단점이 없이 순차적 코드처럼 보이는 동시성 코드를 작성할 수 있게 해줍니다.</p>
<hr>
<h1 id="스레드와-코루틴의-차이는">스레드와 코루틴의 차이는?</h1>
<p>JVM에서 병렬 프로그래밍과 동시성 프로그래밍을 위한 고전적인 추상화는 <strong>스레드</strong>를 사용하는 것입니다. 스레드는 서로 독립적으로 동시에 실행되는 코드 블록을 지정할 수 있게 해줍니다. </p>
<p>thread 함수를 사용하면 새 스레드를 시작할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlin.concurrent.thread

fun main() {
    println(&quot;I&#39;m on ${Thread.currentThread().name}&quot;)
    thread {
        println(&quot;And I&#39;m on ${Thread.currentThread().name}&quot;)
    }
}

// I&#39;m on main
// And I&#39;m on Thread-0</code></pre>
<p>스레드는 애플리케이션을 더 반응성 있게 만들어주고, 여러 코어에 작업을 분산시켜 현대적 시스템을 더 효율적으로 사용할 수 있게 해주지만, 스레드를 사용하는 데는 비용이 듭니다.</p>
<p>또한 스레드가 어떤 작업이 완료되길 기다리는 동안에는 블록되어 다른 의미있는 작업을 할 수 없으며, 그냥 자면서 시스템 자원을 차지합니다.</p>
<p>코틀린은 스레드에 대한 대안으로 <strong>코루틴</strong>이라는 추상화를 도입했습니다. 스레드보다 코루틴을 사용했을 때 생기는 장점은 다음과 같습니다.</p>
<ul>
<li>코루틴은 초경량 추상화입니다. 그렇기에 생성하고 관리하는 비용이 저렴합니다.</li>
<li>코루틴은 시스템 자원을 블록시키지 않고 실행을 일시 중단할 수 있으며, 나중에 중단된 지점에서 실행을 재개할 수 있습니다.</li>
<li>코루틴은 구조화된 동시성이라는 개념을 통해 동시 작업의 구조와 계층을 확립하며, 취소 및 오류 처리를 위한 메커니즘을 제공합니다.</li>
</ul>
<hr>
<h1 id="일시-중단-함수란">일시 중단 함수란?</h1>
<p><strong>일시 중단 함수</strong>는 코루틴의 가장 기본적인 구성 요소이자 실행을 잠시 멈출 수 있는 함수를 의미합니다. 동시성을 사용하지 않고 여러 함수를 호출하는 코드를 작성해보면 아래와 같습니다.</p>
<pre><code class="language-kotlin">fun login(credentials: Credentials): UserID
fun loadUserData(userID: UserID): UserData
fun showData(data: UserData)

fun showUserInfo(credentials: Credentials) {
    val userID = login(credentials)
    val userData = loadUserData(userID)
    showData(userData)
}</code></pre>
<p>이 작업에서는 함수에서 사용되는 시간보다 데이터를 불러오며 대기하는 과정에서 생기는 시간이 더 많아 계산 자원을 낭비하게 됩니다. 이제 일시 중단 함수를 사용해 구현해보겠습니다. 일시 중단 함수는 함수명 앞에 suspend 변경자를 붙이면 됩니다.</p>
<pre><code class="language-kotlin">suspend fun login(credentials: Credentials): UserID
suspend fun loadUserData(userID: UserID): UserData
fun showData(data: UserData)

suspend fun showUserInfo(credentials: Credentials) {
    val userID = login(credentials)
    val userData = loadUserData(userID)
    showData(userData)
}</code></pre>
<p>suspend 변경자를 붙은 것은 함수가 실행을 잠시 멈출 수도 있다는 의미입니다. 일시 중단은 기저 스레드를 블록시키지 않는 대신 함수 실행이 중단되면 다른 코드가 같은 스레드에서 실행될 수 있도록 합니다. </p>
<p>이 때는 코드 구조를 변경하지 않으며 순차적으로 보이고 동작합니다.</p>
<p>일시 중단 함수는 실행을 일시 중단할 수 있기 때문에 일반 코드 아무 곳에서나 호출할 수 없습니다. 일시 중단 함수는 실행을 중단할 수 있는 코드 블록 안에서만 호출할 수 있습니다. 일반적인 일시 중단 코드가 아닌 코드에서 일시 중단 함수를 호출하려고 하면 오류가 발생합니다.</p>
<p>맨 처음에 일시 중단 함수를 호출하는 가장 간단한 방법은 프로그램의 main 함수를 suspend로 하는 것입니다. 이는 일반적으로 규모가 작은 프로그램에서 자주 쓰입니다.</p>
<p>더 범용적이고 강력한 방법은 <strong>코루틴 빌더 함수</strong>를 사용하는 것입니다. 코루틴 빌더는 새로운 코루틴을 생성하는 역할을 하며, 일시 중단 함수를 호출하기 위한 일반적인 진입점으로 사용됩니다.</p>
<p>코루틴 빌더 함수는 다음과 같습니다.</p>
<ul>
<li>runBlocking은 블로킹 코드와 일시 중단 함수를 연결할 때 쓰입니다.</li>
<li>launch는 값을 반환하지 않는 새로운 코루틴을 시작할 때 쓰입니다.</li>
<li>async는 비동기적으로 값을 계산할 때 쓰입니다.</li>
</ul>
<hr>
<h2 id="runblocking-함수">runBlocking 함수</h2>
<p>runBlocking 함수를 사용하면 새 코루틴을 생성하고 실행하며, 해당 코루틴이 완료될 때까지 현재 스레드를 블록시킵니다. </p>
<pre><code class="language-kotlin">import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds

suspend fun doSomethingSlowly() {
    delay(500.milliseconds)
    println(&quot;I&#39;m done&quot;)
}

fun main() = runBlocking {
    doSomethingSlowly()
}</code></pre>
<p>runBlocking을 사용할 때는 하나의 스레드를 블로킹합니다. 그러나 이 코루틴 안에서는 추가적인 자식 코루틴을 얼마든지 시작할 수 있고, 이 자식 코루틴들은 다른 스레드를 더 이상 블록시키지 않습니다.</p>
<hr>
<h2 id="launch-함수">launch 함수</h2>
<p>launch 함수는 새로운 자식 코루틴을 시작하는데 쓰입니다. 이는 일반적으로 어떤 코드를 실행하되 그 결과값을 기다리지 않는 경우에 적합합니다.</p>
<pre><code class="language-kotlin">private var zeroTime = System.currentTimeMillis()
fun log(message: Any?) =
    println(&quot;${System.currentTimeMillis() - zeroTime} &quot; +
        &quot;[${Thread.currentThread().name}] $message&quot;)

fun main() = runBlocking {
    log(&quot;The first, parent, coroutine starts&quot;)
    launch {
        log(&quot;The second coroutine starts and is ready to be suspended&quot;)
        delay(100.milliseconds)
        log(&quot;The second coroutine is resumed&quot;)
    }
    launch {
        log(&quot;The third coroutine can run in the meantime&quot;)
    }
    log(&quot;The first coroutine has launched two more coroutines&quot;)
}

// 117 [main @coroutine#1] The first, parent, coroutine starts
// 125 [main @coroutine#1] The first coroutine has launched two more coroutines
// 127 [main @coroutine#2] The second coroutine starts and is ready to be suspended
// 143 [main @coroutine#3] The third coroutine can run in the meantime
// 241 [main @coroutine#2] The second coroutine is resumed</code></pre>
<p>이 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다. 이 코드에는 3개의 코루틴이 시작됩니다. 첫 번째는 runBlocking에 의해 시작된 부모 코루틴이고, 두 번째와 세 번째는 launch 호출에 의한 자식 코루틴입니다.</p>
<p>coroutine#2가 delay 함수를 호출하면 코루틴이 일시 중단 됩니다. 이를 일시 중단 지점이라고 하며, 이 때 coroutine#2는 지정된 시간 동안 일시 중단되고, 메인 스레드는 다른 코루틴이 실행될 수 있게 합니다. 그 결과 coroutine#3이 작업을 시작할 수 있습니다. 지정된 100 밀리초 후 coroutine#2가 작업을 재개하고 프로그램이 완료되는 것입니다.</p>
<p>그렇다면 일시 중단된 코루틴은 어디로 갈까요? 일시 중단될 때 해당 시점의 상태 정보는 메모리에 저장되고, 이 정보를 바탕으로 나중에 실행을 복구하고 재개할 수 있습니다.</p>
<p>launch를 사용하면 새로운 기본 코루틴을 시작할 수 있습니다. launch 함수는 Job 타입의 객체를 반환하는데, 이를 사용하면 코루틴 실행을 제어할 수 있습니다.</p>
<hr>
<h2 id="async-빌더">async 빌더</h2>
<p>async 빌더 함수는 비동기 계산을 수행할 때 사용할 수 있습니다. async 함수는 launch와 마찬가지로 실행할 코드를 코루틴으로 전달할 수 있지만 Deferred&lt;T&gt; 인스턴스입니다. Deferred를 사용해 주로 할 일은 await라는 일시 중단 함수로 결과를 기다리는 것입니다.</p>
<pre><code class="language-kotlin">suspend fun slowlyAddNumbers(a: Int, b: Int): Int {
    log(&quot;Waiting a bit before calculating $a + $b&quot;)
    delay(100.milliseconds * a)
    return a + b
}

fun main() = runBlocking {
    log(&quot;Starting the async computation&quot;)
    val myFirstDeferred = async { slowlyAddNumbers(2, 2) }
    val mySecondDeferred = async { slowlyAddNumbers(4, 4) }
    log(&quot;Waiting for the deferred value to be available&quot;)
    log(&quot;The first result: ${myFirstDeferred.await()}&quot;)
    log(&quot;The second result: ${mySecondDeferred.await()}&quot;)
}

// 119 [main @coroutine#1] Starting the async computation
// 126 [main @coroutine#1] Waiting for the deferred value to be available
// 131 [main @coroutine#2] Waiting a bit before calculating 2 + 2
// 142 [main @coroutine#3] Waiting a bit before calculating 4 + 4
// 343 [main @coroutine#1] The first result: 4
// 543 [main @coroutine#1] The second result: 8</code></pre>
<p>이 코드를 실행하면 다음과 같은 결과가 나옵니다. async를 호출할 때마다 새로운 코루틴을 시작함으로써 두 계산이 동시에 일어나게 했습니다. launch와 마찬가지로 async를 호출한다고 해서 코루틴이 일시 중단되는 것은 아닙니다. await을 호출하면 그 Deferred에서 결괏값이 사용 가능해질 때까지 루트 코루틴이 일시 중단됩니다.</p>
<p>Deferred 객체는 아직 사용할 수 없는 값을 의미하기에 그 값을 계산하거나 어디서 읽어와야만 합니다.</p>
<hr>
<h1 id="어디서-코드를-실행할지-정하는-방법은">어디서 코드를 실행할지 정하는 방법은?</h1>
<p>코루틴에서는 디스패처를 사용해 코루틴을 실행할 스레드를 정합니다. 디스패처를 선택함으로써 코루틴을 특정 스레드로 제한하거나 스레드 풀에 분산시킬 수 있으며, 코루틴이 한 스레드에서만 실행될지 여러 스레드에서 실행될지 결정할 수 있습니다. </p>
<hr>
<h2 id="스레드-풀이란">스레드 풀이란?</h2>
<p>스레드 풀은 스레드 집합을 관리하고, 집합에 속한 스레드들 위에서 코루틴 실행을 허용합니다. 작업이 실행될 때마다 새 스레드를 할당하는 대신, 스레드 풀은 일정한 수의 스레드를 유지하면서 내부 논리와 구현에 따라 들어오는 작업을 분배합니다. </p>
<hr>
<h1 id="디스패처를-선택하기">디스패처를 선택하기</h1>
<p>코루틴은 기본적으로 부모 코루틴에서 디스패처를 상속 받으므로 모든 코루틴에 대해 명시적으로 디스패처를 지정할 필요가 없습니다. 하지만 선택할 수 있는 디스패처들이 있습니다.</p>
<hr>
<h2 id="dispatchersdefault">Dispatchers.Default</h2>
<p>Dispatchers.Default는 가장 일반적인 디스패처로, 일반적인 작업에 사용할 수 있습니다. 이 디스패처는 CPU 코어 수만큼의 스레드로 구성된 스레드 풀을 기반으로 합니다. 즉, 기본 디스패처에서 코루틴을 스케줄링하면 여러 스레드에서 코루틴이 분산돼 실행되며, 멀티코어 시스템에서는 병렬로 실행될 수 있습니다.</p>
<hr>
<h2 id="dispatchersmain">Dispatchers.Main</h2>
<p>UI 프레임워크를 사용할 때는 특정 작업을 UI 스레드나 메인 스레드라고 불리는 특정 스레드에서 실행해야할 때가 있습니다. 예를 들어 사용자 인터페이스 요소를 다시 그리는 작업같은 것을 안전하게 실행하려면 디스패치할 때 Dispatchers.Main을 사용해야 합니다.</p>
<hr>
<h2 id="dispatchersio">Dispatchers.IO</h2>
<p>기본 디스패처의 스레드 수는 CPU 코어 수와 동일하기 떄문에, 예를 들어 듀얼 코어 기계에서 2개의 스레드를 블로킹하는 작업을 호출하면 기본 스레드 풀이 소진돼 다른 코루틴은 완료될 때까지 실행되지 못합니다. 이런 상황을 처리하기 위해 설계된 것이 Dispatchers.IO입니다.</p>
<p>이 디스패처에서 실행된 코루틴은 자동으로 확장되는 스레드 풀에서 실행되며 CPU 집약적이지 않은 작업에 적합합니다.</p>
<hr>
<h2 id="코루틴-빌더에-디스패처를-전달하기">코루틴 빌더에 디스패처를 전달하기</h2>
<p>runBlocking, launch, async 같은 모든 코루틴 빌더 함수는 코루틴 디스패처를 명시적으로 지정할 수 있습니다. </p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        log(&quot;Doing some work&quot;)
        launch(Dispatchers.Default) {
            log(&quot;Doing some background work&quot;)
        }
    }
}</code></pre>
<p>launch 함수에 기본 디스패처를 인자로 전달해 코루틴을 시작합니다.</p>
<hr>
<h2 id="코루틴-안에서-디스패처를-바꾸기">코루틴 안에서 디스패처를 바꾸기</h2>
<p>이미 실행 중인 코루틴에서 디스패처를 바꿀 때는 withContext 함수에 다른 디스패처를 전달하면 됩니다. </p>
<pre><code class="language-kotlin">launch(Dispatchers.Default) {
    val result = performBackgroundOperation()
    withContext(Dispatchers.Main) {
        updateUI(result)
    }
}</code></pre>
<p>이 코드를 실행하면 기본 디스패처에서 메인 디스패처로 변경해서 UI를 갱신하는 것입니다.</p>
<hr>
<h1 id="코루틴과-디스패처는-스레드-안전성-문제에-대해-완벽하지-않다">코루틴과 디스패처는 스레드 안전성 문제에 대해 완벽하지 않다</h1>
<p>한 코루틴은 항상 순차적으로 실행됩니다. 즉, 어느 단일 코루틴의 어떤 부분도 병렬로 실행되지 않으며 이는 단일 코루틴에 연관된 데이터가 전형적인 동기화 문제를 일으키지 않는다는 것을 의미합니다. 하지만 여러 코루틴이 동일한 데이터를 읽거나 변경하는 경우에는 문제가 발생합니다.</p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        var x = 0
        repeat(10_000) {
            launch(Dispatchers.Default) {
                x++
            }
        }
        delay(1.seconds)
        println(x)
    }
}
// 9,923</code></pre>
<p>이 경우는 예상한 숫자인 10000보다 작은 결과가 나타납니다. 여러 코루틴이 같은 데이터를 수정하고 있기 때문에 다중 스레드 디스패처에서 실행되면 일부 증가 작업이 서로의 결과를 덮어쓰는 상황이 발생할 수 있기 때문입니다.
이런 상황을 해결하기 위해서 코루틴은 Mutex 잠금을 제공하며, 이를 통해 코드 임계 영역이 한 번에 하나의 코루틴만 실행되게 보장할 수 있습니다.</p>
<pre><code class="language-kotlin">fun main() {
    runBlocking {
        val mutex = Mutex()
        var x = 0
        repeat(10_000) {
            launch(Dispatchers.Default) {
                mutex.withLock {
                    x++
                }
            }
        }
        delay(1.seconds)
        println(x)
    }
}
// 10000</code></pre>
<p>코루틴을 사용할 때는 스레드를 사용할 떄와 같은 동시성 문제가 발생합니다. 데이터가 한 코루틴에만 연관돼 있다면 기본적으로 예상한 대로 코드가 동작하지만 여러 코루틴이 병렬로 동일한 데이터를 변경한다면 스레드와 마찬가지로 동기화나 잠금 처리를 해야합니다.</p>
<hr>
<h1 id="코루틴은-코루틴-콘텍스트에-추가적인-정보를-담고-있다">코루틴은 코루틴 콘텍스트에 추가적인 정보를 담고 있다</h1>
<p>withContext 함수에 서로 다른 디스패처를 인자로 전달했습니다. 하지만 파라미터의 이름을 보면 CoroutineDispatcher가 아니라 CoroutineContext입니다.</p>
<p>각 코루틴은 추가적인 문맥 정보를 담고 있는데, 이 문맥은 CoroutineContext라는 형태로 제공됩니다. 이는 여러 요소로 이뤄진 집합과 같습니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 코루틴에 대한 기초 개념에 대해 정리해봤습니다.</p>
<p>이전 우테코 오픈 미션을 진행해보면서 앱을 만들었는데, 이 때 코루틴을 사용한 경험이 있습니다. 그 당시에는 앱의 UI와 데이터 전송을 부드럽게 하기 위한 목적으로 사용했었습니다. 개념에 대해서만 짧게 공부하고 사용했었기에 다루는데 어려움을 가졌었습니다.</p>
<p>이번 기회에 코루틴에 공부해보니 코루틴의 철학과 스레드보다 유용한 점에 대해 알게 되었고, 디스패처를 상황에 따라 사용하며 명시적으로 지정하며 스레드를 효율적으로 지정할 수 있다는 활용 방법에 대해서도 배울 수 있었습니다.</p>
<p>다음에는 구조화된 동시성에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 2026.02.09 ~ 2026.02.15]]></title>
            <link>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.09-2026.02.15</link>
            <guid>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.09-2026.02.15</guid>
            <pubDate>Sun, 15 Feb 2026 14:58:35 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>한 주의 회고와 함께 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 주는 동생과 함께 서울 나들이도 다녀오고 바빠지기 전에 친구도 만나고 이사짐 정리도 하고 매일 도서관에 출석체크도 하면서 나름 알차게 보낸 한 주였다고 생각합니다 ㅎㅎ.</p>
<img src=https://velog.velcdn.com/images/noeyh_0j/post/f456ccdc-7c99-4779-b800-d39269b2a03c/image.jpeg width=50%>

<blockquote>
<p>치이카와 팝업 스토어 (서울에 간 이유)</p>
</blockquote>
<p>크게 이번 주 계획에 대한 설명과 KPT, 학습 정리 글, 마무리로 회고를 하겠습니다.</p>
<hr>
<h1 id="이번주의-계획은">이번주의 계획은?</h1>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/afa4d5c4-5992-40a8-a777-e301fdd3ea1d/image.png" alt="thumbnail"></p>
<p>지난 2주의 학습 내용은 제가 활용했던 개념도 많고 써본 경험도 있으니 학습에서 큰 어려움을 겪지 않았었습니다. 하지만 이번 주는 기초보다는 활용에 가까운 개념이다보니 학습 도중 많은 어려움을 겪었고 쉽지 않았습니다...</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>
<li>자주 까먹는 것은 배워도 사용하지 않아서 그런 것이라고 생각이 들었습니다. 영어공부도 단어를 열심히 외우고 문법을 공부해도 조금만 쉬면 다 까먹는 것과 비슷하다고 느꼈습니다. 다른 분들의 회고를 봤을 때 알고리즘 문제도 올리시는 것을 보고 저도 하루에 한 문제씩이라도 알고리즘 문제를 풀며 문법을 까먹지 않게 하는 것이 좋을 것 같다고 생각합니다.</li>
</ul>
<hr>
<h1 id="학습내용-정리">학습내용 정리</h1>
<p>학습한 내용은 개인 블로그에 정리하였습니다.</p>
<blockquote>
</blockquote>
<ul>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-9%EC%9E%A5-%EC%97%B0%EC%82%B0%EC%9E%90-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9%EA%B3%BC-%EB%8B%A4%EB%A5%B8-%EA%B4%80%EB%A1%80">9장 연산자 오버로딩과 다른 관례</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-10%EC%9E%A5-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98-%EB%9E%8C%EB%8B%A4%EB%A5%BC-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EC%99%80-%EB%B0%98%ED%99%98%EA%B0%92%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9">10장 고차 함수: 람다를 파라미터와 반환값으로 사용</a></li>
<li><a href="https://velog.io/@noeyh_0j/11%EC%9E%A5-%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%8A%A4">11장 제네릭스</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-11%EC%9E%A5-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EA%B3%BC-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98">12장 어노테이션과 리플렉션</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-13%EC%9E%A5-DSL-%EB%A7%8C%EB%93%A4%EA%B8%B0">13장 DSL 만들기</a></li>
</ul>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>학습에 있어 어려운 부분도 많았고 쉽지 않았지만 그래도 어떻게 계획한 만큼의 학습 목표는 달성하여 스스로 기특하다고 느낀 한 주였습니다.</p>
<p>어렵기도 했지만 흥미있게 봤던 부분도 많았습니다. 가장 재밌게 공부한 부분은 관례에 관한 내용입니다. 코틀린 이전에는 파이썬과 다트를 조금 했었습니다. 프로그래밍에서 리스트 인덱싱할 때 중괄호를 사용한 인덱싱이라던가 정수 연산을 할 때 +,- 등의 기호를 사용하는 것이 당연하다고 생각했고 코틀린도 앞에 두 언어와 동일하게 사용하여 이질감이 없었습니다.</p>
<p>하지만 언어의 안을 들여다봤을 때 코틀린에서 관례라는 개념을 사용해 사용되는 함수들을 기호로 사용해 호출될 수 있도록 도와주는 것이었고, 이런 개념을 기존에 본 적 없어서 그런지 재밌게 보고, 인상 깊게 머릿속에 남았습니다.</p>
<p>반면 어렵다고 느낀 것은 어노테이션과 리플렉션 부분이었습니다. 이론이 어렵다는 듯 느낀 부분도 있지만 어떻게 사용하는지에 대해 생각해 볼 때 더욱 막막했었습니다. 어노테이션과 리플렉션 부분은 나중에 코틀린을 다양하게 활용해 보고 다시 봐봐야겠다고 생각했습니다.</p>
<p>또 이번 주에는 가장 큰 문제도 해결했습니다! 바로 닉네임 짓기입니다..ㅋㅋㅋ</p>
<p>정말 많이 생각하고 정한 닉네임은 &#39;하로&#39;입니다. 제가 최근에 다시 관심이 생긴 취미가 건담 프라모델인데 건담의 마스코트같은 캐릭터의 이름이 &#39;하로&#39;입니다. 이왕 짓는 거 &quot;내가 좋아하는 캐릭터로 지으면 어떨까?&quot; 라는 생각도 있었고 발음하기 편한게 좋을 거 같다는 생각도 하며 &#39;하로&#39;로 정하게 되었습니다.</p>
<p><img src=https://velog.velcdn.com/images/noeyh_0j/post/20ffb2c4-d632-4ee7-9799-e458c84e7dca/image.png width=50%> | <img src=https://velog.velcdn.com/images/noeyh_0j/post/1cc1a317-d235-4fd8-b6ab-17467b8197b3/image.png>
| - | - |</p>
<blockquote>
<p>이 친구가 하로입니다. 무려 팔 다리도 나올 수 있습니다!</p>
</blockquote>
<p>벌써 돌아오는 주가 마지막 주입니다. 이 말은 우테코 시작하기까지 1주일밖에 남지 않았다는 의미인데 떨리기도 하지만 설레기도 하면서 두근두근합니다 ㅋㅋㅋ</p>
<p>돌아오는 주는 명절과 졸업식 등 할 게 많은 마지막 주지만 그래도 할 수 있는 만큼 최선을 다해 마무리해 보겠습니다.</p>
<p>다들 명절 잘 보내세요!!! 읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 13장 DSL 만들기]]></title>
            <link>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-13%EC%9E%A5-DSL-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-13%EC%9E%A5-DSL-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 14 Feb 2026 07:34:56 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>DSL에 대한 내용 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 DSL이 무엇인지, 어떻게 만들고 사용할 수 있는지 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="dsl이란">DSL이란?</h1>
<p>DSL은 도메인 특화 언어라는 의미로 특정 과업 또는 영역에 초점을 맞춘 언어입니다. 반대되는 개념으로는 범용 프로그래밍 언어입니다.</p>
<p>모든 코드는 궁극적으로 코드의 가독성과 유지 보수성을 가장 좋게 유지하는 것이 목표입니다. 이는 클래스끼리 상호작용하는 코드에서는 API를 깔끔하게 작성하기 위해 노력해야합니다.</p>
<p>코틀린은 API를 깔끔하게 만들기 위해 확장 함수, 중위 함수 호출, 연산자 오버로딩 등 다양한 편의성 기능을 지원합니다. 코틀린 DSL은 API에서 더 나아가 코틀린의 문법적 특성과 여러 메서드 호출에서 구조를 만들어내는 능력 위에 구축되며 더 표현력이 풍부하고 사용하기 편합니다.</p>
<p>가장 익숙한 DSL은 SQL과 정규식이 있습니다. 이 두 언어는 데이터베이스와 문자열 조작이라는 특정 작업에서 유용하지만, 이들만으로 전체 애플리케이션을 작성하는 경우는 없습니다. 이런 DSL은 스스로 제공하는 기능을 제한함으로써 더 효율적으로 자신의 목표를 달성할 수 있습니다.</p>
<p>DSL은 압축적인 문법을 사용함으로써 특정 영역에 대한 연산을 더 간결하게 기술할 수 있습니다.</p>
<p>DSL은 범용 프로그래밍 언어에 비해 선언적이라는 점이 중요합니다. 범용 프로그래밍 언어는 명령적이며 어떤 연산을 완수하기 위해 필요한 각 단계를 순서대로 정확히 기술합니다. 반면 선언적 언어인 DSL은 원하는 결과를 기술하기만 하면 세부 실행은 엔진에게 맡깁니다. 이 방법에서 엔진은 결과를 얻는 과정을 전체적으로 최적화하기 때문에 선언적 언어가 더 효율적인 경우가 많습니다.</p>
<p>그러나 DSL에는 범용 언어로 만든 호스트 애플리케이션과 함께 조합하기가 힘들다는 단점이 있습니다. 이런 문제를 해결하기 위해 코틀린은 내부에서 DSL을 만들 수 있게 해줍니다.</p>
<hr>
<h1 id="내부-dsl이란">내부 DSL이란?</h1>
<p>내부 DSL은 범용 언어로 작성된 프로그램의 일부로, 범용 언어와 동일한 문법을 사용합니다. 이는 독립적인 문법 구조를 갖는 외부 DSL과는 반대되는 개념입니다.</p>
<pre><code class="language-sql">SELECT Country.name, COUNT(Customer.id)
        FROM Country
INNER JOIN Customer
        ON Country.id = Customer.country_id
    GROUP BY Country.name
    ORDER BY COUNT(Customer.id) DESC
    LIMIT 1</code></pre>
<pre><code class="language-kotlin">(Country innerJoin Customer)
    .slice(Country.name, Count(Customer.id))
    .selectAll()
    .groupBy(Country.name)
    .orderBy(Count(Customer.id), order = SortOrder.DESC)
    .limit(1)</code></pre>
<p>첫 번째 코드는 외부 DSL로 작성한 SQL 코드이며, 두 번째 코드는 코틀린의 내부 DSL로 작성한 똑같은 SQL 코드입니다.</p>
<p>두 코드의 동작은 같지만 내부 DSL을 사용한 코드는 orderBy()나 selectAll() 등 코틀린 메소드를 활용해 SQL 코드를 작성했습니다. </p>
<p>코틀린 DSL에서는 보통 람다를 내포시키거나 메소드 호출을 연쇄시키는 방식으로 구조를 만들어 더 읽기 쉽게 만듭니다. 또한 DSL 구조의 장점은 같은 맥락을 매 함수 호출 시마다 반복하지 않고도 재사용할 수 있다는 점입니다.</p>
<hr>
<h2 id="dsl에서-수신-객체-지정-람다를-사용하는-이유는">DSL에서 수신 객체 지정 람다를 사용하는 이유는?</h2>
<p>람다를 수신 객체 지정 람다로 바꾸면 코드를 더 단순하게 변경할 수 있습니다. buildString 함수를 예로 들어보겠습니다.</p>
<pre><code class="language-kotlin">fun buildString(
    builderAction: (StringBuilder) -&gt; Unit
): String {
    val sb = StringBuilder()
    builderAction(sb)
    return sb.toString()
}

fun main() {
    val s = buildString {
        it.append(&quot;Hello, &quot;)
        it.append(&quot;World!&quot;)
    }
    println(s)
    // Hello, World!
}</code></pre>
<p>이 람다 본문에서는 매번 it을 사용해 인스턴스를 참조해야합니다. 하지만 수신 객체 지정 람다로 바꾼다면 it 접두사를 사용하지 않고 append만을 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">fun buildString(
    builderAction: StringBuilder.() -&gt; Unit
): String {
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}

fun main() {
    val s = buildString {
        this.append(&quot;Hello, &quot;)
        append(&quot;World!&quot;)
    }
    println(s)
    // Hello, World!
}</code></pre>
<p>완전한 문장은 this.append()지만 생략할 수 있습니다. buildString의 함수 선언이 일반 함수 타입 대신 확장 함수 타입으로 바뀌었습니다. 확장 함수 타입 선언은 람다의 파라미터 목록에 있던 수신 객체 타입을 파라미터 목록을 여는 괄호 앞으로 빼내 중간에 마침표를 붙인 형태입니다. 이런 타입을 수신 객체 타입이라고 부르며, 람다에 전달되는 그런 타입의 객체를 수신 객체라고 부릅니다.</p>
<p>또한 확장 함수 타입의 변수를 정의해 확장 함수 타입 변수를 확장 함수처럼 호출하거나 수신 객체 지정 람다를 요구하는 함수에 인자로 넘길 수 있습니다.</p>
<pre><code class="language-kotlin">val appendExcl: StringBuilder.() -&gt; Unit = { this.append(&quot;!&quot;) }

fun main() {
    val stringBuilder = StringBuilder(&quot;Hi&quot;)
    stringBuilder.appendExcl()
    println(stringBuilder)
    // Hi!
    println(buildString(appendExcl))
    // !
}</code></pre>
<p>또한 함수 시그니처를 보면 람다에 수신 객체가 있는지와 람다가 어떤 타입의 수신 객체를 요구하는지도 알 수 있습니다.</p>
<hr>
<h2 id="html-빌더-안에서-사용되는-수신-객체-지정-람다">HTML 빌더 안에서 사용되는 수신 객체 지정 람다</h2>
<p>HTML을 만들기 위한 코틀린 DSL을 HTML 빌더라고 부릅니다. 코틀린 빌더는 타입 안전성을 보장하기 때문에 더 튼튼하고 사용하기 편리합니다.</p>
<pre><code class="language-kotlin">fun createSimpleTable() = createHTML().
    table {
        tr {
            td{ +&quot;cell&quot; }
        }
    }</code></pre>
<p>이 코드는 코틀린 HTML 빌더를 사용해 간단한 HTML 표를 만든 것입니다. 각 블록의 이름 결정 규칙은 각 람다의 수신 객체에 의해 결정됩니다. table에 전달된 수신 객체는 TABLE이라는 특별한 타입이며 그 안에는 tr 메소드 정의가 있습니다.</p>
<pre><code class="language-kotlin">open class Tag

class TABLE: Tag {
    fun tr(init: TR.() -&gt; Unit)
}

class TR: Tag {
    fun td(init: TD.() -&gt; Unit)
}

class TD: Tag</code></pre>
<p>이런 식으로 각 클래스는 모두 Tag를 확장하며 자신의 내부에 들어갈 수 있는 태그를 생성하는 메소드가 들어있습니다. tr과 td는 init 파라미터는 모두 확장 함수 타입이며 각 메소드에 전달된 람다 수신 객체 타입인 TR과 TD를 지정합니다.</p>
<p>수신 객체 지정 람다가 다른 수신 객체 지정 람다 안에 들어가면 안쪽 람다에서 this@를 사용해 외부 람다에 정의된 수신 객체를 사용할 수 있습니다. 이런 식이면 내포 깊이가 깊은 구조에서는 어떤 식의 수신 객체가 무엇인지 분명하지 않아서 혼동이 올 수 있습니다.</p>
<p>이를 막기 위해 코틀린은 @DslMarker 어노테이션을 사용해 내포된 람다에서 외부 람다의 수신 객체에 접근하지 못하게 제한하는 기능을 제공합니다. @DslMarker는 메타어노테이션으로 HtmlTagMarker에 대해 적용되어 있습니다.</p>
<pre><code class="language-kotlin">@DslMarker
annotation class HtmlTagMarker</code></pre>
<p>Tag 클래스에 @HtmlTagMarker 어노테이션을 적용함으로써 영역 안에서 암시적 수신 객체가 2개가 될 수 없도록 막을 수 있습니다.</p>
<pre><code class="language-kotlin">fun createTable() =
    table {
        tr {
            td {
            }
        }
    }

fun main() {
    println(createTable())
    // &lt;table&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
}</code></pre>
<p>table 함수는 TABLE 태크의 새 인스턴스를 만들고 초기화해 반환합니다. tr, td도 마찬가지로 동작하며 &lt;table&gt; 태그 안에 tr 인스턴스를 새로 만들고, 그 자식도 마찬가지로 안에 새로 인스턴스를 만들어 추가합니다. 이런 식으로 주어진 태그를 초기화하고 바깥쪽 태그의 자식으로 추가하는 로직을 거의 모든 태그가 공유합니다.</p>
<p>이런 기능을 상위 클래스인 Tag로 뽑아내서 doInit이라는 멤버로 만들 수 있습니다. doInit은 자식 태그에 대한 참조를 저장하는 일과 인자로 전달받은 람다를 호출하는 일을 책임집니다.</p>
<pre><code class="language-kotlin">@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
open class Tag(val name: String) {
    private val children = mutableListOf&lt;Tag&gt;()

    protected fun &lt;T: Tag&gt; doInit(child: T, init: T.() -&gt; Unit) {
        child.init()
        children.add(child)
    }

    override fun toString() = &quot;&lt;$name&gt;${children.joinToString(&quot;&quot;)}&lt;/$name&gt;&quot;
}

fun table(init: TABLE.() -&gt; Unit) = TABLE().apply(init)

class TABLE: Tag(&quot;table&quot;) {
    fun tr(init: TR.() -&gt; Unit) = doInit(TR(), init)
}

class TR: Tag(&quot;tr&quot;) {
    fun td(init: TD.() -&gt; Unit) = doInit(TD(), init)
}

class TD: Tag(&quot;td&quot;)

fun createTable() =
    table {
        tr {
            td {
            }
        }
    }

fun main() {
    println(createTable())
    // &lt;table&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
}</code></pre>
<p>전체 HTML을 만드는 코드의 예시입니다.</p>
<hr>
<h1 id="invoke-관례란">invoke 관례란?</h1>
<p>invoke는 괄호를 사용함으로써 operator 변경자가 붙은 invoke 메소드 정의가 들어있는 클래스 객체를 함수처럼 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">class Greeter(val greeting: String) {
    operator fun invoke(name: String) {
        println(&quot;$greeting, $name!&quot;)
    }
}

fun main() {
    val bavarianGreeter = Greeter(&quot;servus&quot;)
    bavarianGreeter(&quot;Dmitry&quot;)
    // Servus, Dmitry!
}</code></pre>
<p>일반적인 람다 호출도 실제로는 invoke 관례를 적용한 것입니다. invoke 관례를 사용하면 임의의 객체를 함수처럼 다룰 수 있습니다. invoke 메소드를 활용하면 DSL API의 유연성을 늘릴 수 있습니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 DSL이 무엇인지, 어떤 곳에서 사용되는지, invoke 관례에 대해 정리해봤습니다.</p>
<p>DSL은 도메인 특화 언어로 SQL이나 정규식처럼 특정한 목적에서 유용하게 사용되는 것을 알게 되었습니다. 또한 내부 DSL이라는 코틀린의 문법적 특성에 맞는 DSL을 코틀린에서 지원하고, 기존의 외부 DSL과 언어를 연결하는 것이 힘들었던 부분을 해결하기 위한 것임을 알 수 있었습니다.</p>
<p>내부 DSL에는 수신 객체 지정 람다를 사용하여 매번 객체를 it참조해야했던 반복되는 부분을 없애 코드의 가독성을 늘렸다는 것을 배울 수 있었습니다. DSL을 설계할 때도 어노테이션을 활용하며 이전 장에서 배웠던 어노테이션의 개념이 코틀린에서 빼놓을 수 없는 개념이라는 것을 다시 한 번 느낄 수 있었습니다.</p>
<p>이번 장을 마무리로 코틀린을 코틀린답게 사용하는 방법에 대해 알아봤습니다.</p>
<p>다음에는 코틀린의 동시성을 책임지기 위한 개념인 코루틴에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
    </channel>
</rss>