<?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, 21 Mar 2026 09:05:33 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/디자인 패턴] 싱글톤 패턴에 대하여]]></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>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 12장 어노테이션과 리플렉션]]></title>
            <link>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</link>
            <guid>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</guid>
            <pubDate>Fri, 13 Feb 2026 13:12:11 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>어노테이션과 리플렉션에 대한 개념 정리로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 어노테이션이 무엇인지 어떻게 사용하는지, 리플렉션이 무엇인지에 대해서 정리해보겠습니다.</p>
<hr>
<h1 id="어노테이션에-대하여">어노테이션에 대하여</h1>
<p>어노테이션은 @와 어노테이션 이름을 선언 앞에 넣으면 됩니다. 함수나 클래스 등 다른 여러 코드 구성 요소에 어노테이션을 붙일 수 있습니다.</p>
<p>어노테이션을 사용하면 선언에 추가적인 메타데이터를 연관시킬 수 있습니다. 그 후에 어노테이션이 설정된 방식에 따라 메타데이터를 소스코드, 컴파일된  클래스 파일, 런타임에 대해 작동하는 도구를 통해 접근할 수 있습니다.</p>
<p>예를 들어 kotlin.test를 사용한다면 테스트 메소드 앞에 @Test 어노테이션을 붙일 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlin.test.*

class MyTest {
    @Test
    fun testTrue() {
        assertTrue(1 + 1 == 2)
    }
}</code></pre>
<p>@Test 어노테이션을 사용해 이 메소드를 테스트로 호출하라고 지시하는 것입니다.</p>
<p>@Deprecated 어노테이션은 사용 금지, 즉 선언이 더 이상 쓰이지 않게 될 것임을 표시하는 어노테이션입니다. 이 어노테이션은 최대 3개의 파라미터를 받습니다.</p>
<ul>
<li>message는 사용 중단 예고의 이유를 설명합니다.</li>
<li>replaceWith는 지원이 종료될 API 기능을 더 쉽게 새 버전으로 전환할 수 있게 지원합니다.</li>
<li>level은 점진적으로 사용 중단을 지원하고자 할 때 사용합니다.</li>
</ul>
<pre><code class="language-kotlin">@Deprecated(&quot;Use removeAt(index) instead.&quot;, ReplaceWith(&quot;removeAt(index)&quot;))
fun remove(index: Int) { /* ... */ }</code></pre>
<p>이 예제는 remove 함수에 @Deprecated 어노테이션을 붙여 removeAt 함수로 대체돼야 한다는 사실을 표현하는 것입니다. </p>
<p>어노테이션 인자로는 기본 타입의 값, 문자열, 이넘, 클래스 참조, 다른 어노테이션 클래스, 앞의 요소들로 이뤄진 배열이 쓰일 수 있습니다.</p>
<ul>
<li>클래스를 어노테이션 인자로 지정할 때는 ::class를 클래스 이름 뒤에 넣어야 합니다.</li>
<li>다른 어노테이션을 인자로 지정할 때는 어노테이션의 이름 앞에 @를 사용하지 않습니다.</li>
<li>배열을 인자로 지정할 때는 @RequestMapping(path = [”/foo”, “/bar”])처럼 각괄호를 사용합니다.</li>
<li>프로퍼티를 인자로 지정할 때는 어노테이션 인자를 컴파일 시점에 알 수 있어야하기 때문에 임의의 프로퍼티를 인자로 지정할 수 없습니다. 프로퍼티를 어노테이션 인자로 사용하려면 const 변경자를 붙여서 사용해야 합니다.</li>
</ul>
<hr>
<h2 id="어노테이션이-참조할-수-있는-정확한-선언-지정하기">어노테이션이 참조할 수 있는 정확한 선언 지정하기</h2>
<p>코틀린 소스코드에서 한 선언을 컴파일한 결과가 여러 자바 선언과 대응하는 경우, 여러 자바 선언에 각각 어노테이션을 붙여야 할 때가 있습니다. </p>
<p>이럴 때 <strong>사용 지점 타깃</strong> 선언을 통해 어노테이션을 붙일 요소를 정할 수 있습니다. 사용 지점 타깃은 @ 기호와 어노테이션 이름 사이에 붙으며 어노테이션 이름과는 콜론으로 분리됩니다.</p>
<pre><code class="language-kotlin">@get:JvmName(&quot;obtainCertificate&quot;)</code></pre>
<p>위 예시는 @JvmName 어노테이션을 프로퍼티 게터에 적용하라는 의미입니다.</p>
<p>명시적으로 프로퍼티의 게터와 세터의 @JvmName을 지정하고 싶으면 @get:JvmName()과 @set:JvmName()을 사용하면 됩니다.</p>
<pre><code class="language-kotlin">class CertificateManager {
    @get:JvmName(&quot;obtainCertificate&quot;)
    @set:JvmName(&quot;putCertificate&quot;)
    var certificate: String = &quot;-----BEGIN PRIVATE KEY-----&quot;
}</code></pre>
<p>이런 어노테이션이 붙은 경우 자바 코드에서는 certificate 프로퍼티를 JvmName에 사용된 이름으로 사용할 수 있습니다.</p>
<pre><code class="language-java">class Foo {
    public static void main(String[] args) {
        var certManager = new CertificateManager();
        var cert = certManager.obtainCertificate();
        certManager.putCertificate(&quot;-----BEGIN CERTIFICATE-----&quot;)
    }
}</code></pre>
<p>자바에 선언된 어노테이션을 사용해 프로퍼티에 어노테이션을 붙이는 경우 기본적으로 프로퍼티의 필드에 그 어노테이션이 붙습니다. 하지만 코틀린으로 어노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 어노테이션을 만들 수 있습니다.</p>
<p>사용 지점 타깃을 지정할 때 지원하는 타깃 목록은 아래와 같습니다.</p>
<ul>
<li>property: 프로퍼티 전체</li>
<li>field: 프로퍼티에 의해 생성되는 필드</li>
<li>get: 프로퍼티 게터</li>
<li>set: 프로퍼티 세터</li>
<li>receiver: 확장 함수나 프로퍼티의 수신 객체 파라미터</li>
<li>param: 생성자 파라미터</li>
<li>setparam: 세터 파라미터</li>
<li>delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드</li>
<li>file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스</li>
</ul>
<hr>
<h2 id="자바-api를-제어하는-방법은">자바 API를 제어하는 방법은?</h2>
<p>코틀린은 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일하는 방법과 코틀린 선언을 자바에 노출하는 방법을 제어하기 위한 어노테이션을 많이 제공합니다. 어노테이션의 일부는 자바 언어의 일부 키워드를 대신하며, 어노테이션을 사용해 코틀린 선언을 자바에 노출시키는 방법을 변경할 수 있습니다.</p>
<ul>
<li>@JvmName은 코틀린 선언이 만들어내는 자바 필드나 메소드 이름을 변경합니다.</li>
<li>@JvmStatic을 객체 선언이나 동반 객체의 메소드가 자바 정적 메소드로 노출됩니다.</li>
<li>@JvmOverloads를 사용하면 디폴트 파라미터 값이 있는 함수에 대해 컴파일러가 자동으로 오버로딩한 함수를 생성해줍니다.</li>
<li>@JvmField를 프로퍼티에 사용하면 대상 프로퍼티를 게터나 세터가 없는 공개된 자바 필드로 노출시킵니다.</li>
<li>@JvmRecord를 데이터 클래스에 사용하면 자바 레코드 클래스를 선언할 수 있습니다.</li>
</ul>
<hr>
<h2 id="어노테이션을-활용해-json-직렬화-제어하기">어노테이션을 활용해 JSON 직렬화 제어하기</h2>
<p>직렬화는 객체를 저장 장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것입니다. 역직렬화는 반대로 텍스트나 이진 형식으로 저장된 데이터에서 원래의 객체를 만들어내는 것입니다.</p>
<pre><code class="language-kotlin">data class Person(val name: String, val age: Int)

fun main() {
    val person = Person(&quot;Alice&quot;, 29)
    println(serialize(person))
    // {&quot;age&quot;: 29, &quot;name&quot;: &quot;Alice&quot;}
}</code></pre>
<p>Person 인스턴스를 serialize 함수에 전달하면 JSON표현이 담긴 문자열을 돌려받습니다.</p>
<pre><code class="language-kotlin">fun main() {
    val json = &quot;&quot;&quot;{&quot;name&quot;: &quot;Alice&quot;, &quot;age&quot;: 29}&quot;&quot;&quot;
    println(deserialize&lt;Person&gt;(json))
    // Person(name=Alice, age=29)
}</code></pre>
<p>JSON 표현을 deserialize 함수에 변환할 타입 정보를 함께 전달하면 타입 객체로 돌려받습니다.</p>
<p>이런 직렬화나 역직렬화를 어노테이션을 활용해 제어할 수 있습니다.</p>
<ul>
<li>@JsonExclude 어노테이션을 사용하면 직렬화나 역직렬화할 때 무시해야하는 프로퍼티를 표시할 수 있습니다.</li>
<li>@JsonName 어노테이션을 사용하면 프로퍼티를 표현하는 키로 프로퍼티 이름 대신 어노테이션이 지정한 문자열을 쓰게 할 수 있습니다.</li>
</ul>
<pre><code class="language-kotlin">data class Person(
    @JsonName(&quot;alias&quot;) val firstName: String,
    @JsonExclude val age: Int? = null
)</code></pre>
<p>이 예제는 firstName 프로퍼티에 대해 alias라는 이름을 사용하고, age를 직렬화와 역직렬화 대상에서 제외하는 코드입니다.</p>
<p>직렬화 대상에서 제외할 때는 프로퍼티에 반드시 기본값을 지정해야만 합니다. 기본값을 지정하지 않으면 역직렬화를 할 때 인스턴스를 새로 만들 수 없기 때문입니다.</p>
<hr>
<h2 id="어노테이션-선언하기">어노테이션 선언하기</h2>
<p>아무 파라미터도 없는 @JsonExclude 어노테이션의 선언문은 일반 클래스 선언과 비슷합니다. 단지 class 앞에 annotation 변경자가 붙어있는 점만 다릅니다.</p>
<p>어노테이션 클래스는 선언이나 식과 관련 있는 메타데이터의 구조만 정의하기 때문에 내부에 아무 코드도 들어올 수 없습니다.</p>
<pre><code class="language-kotlin">annotation class JsonExclude</code></pre>
<p>파라미터가 있는 어노테이션을 정의하려면 어노테이션 클래스의 주 생성자에 파라미터를 선언해야하며, 모든 파라미터를 val로 선언해야 합니다.</p>
<pre><code class="language-kotlin">annotation class JsonName(val name: String)</code></pre>
<hr>
<h3 id="자바-어노테이션과-비교">자바 어노테이션과 비교</h3>
<pre><code class="language-java">public @interface JsonName {
    String value();
}</code></pre>
<p>자바에서 어노테이션을 선언한 경우 value라는 메소드가 있습니다. 어노테이션을 적용할 때 value 속성은 이름을 생략할 수 있습니다. 예를 들면 @JsonName(”custom_name”) 으로 사용할 수 있습니다. 이는 value 속성 하나만 사용할 때 가능합니다. 만약 여러 속성을 사용한다면 @JsonName(value=”custom_name”, bool=false) 등으로 이름을 나타내야 합니다.</p>
<p>또한 value()속성의 이름을 name() 으로 변경해도 됩니다. 다만 속성의 이름을 바꾸면 한 가지만 사용하더라도 @JsonName(name=”custom_name”) 과 같이 이름을 반드시 명시해야 합니다. </p>
<hr>
<h2 id="메타어노테이션이란">메타어노테이션이란?</h2>
<p>메타어노테이션은 어떤 어노테이션 클래스에 적용할 수 있는 어노테이션을 의미합니다. 메타어노테이션은 컴파일러가 어노테이션을 처리하는 방법을 제어합니다.</p>
<p>메타어노테이션의 예시로는 @Target 이 있습니다. @Target 메타어노테이션은 어노테이션을 적용할 수 있는 요소의 유형을 지정합니다. 어노테이션 클래스에 대해 구체적인 @Target을 지정하지 않으면 모든 선언에 적용할 수 있는 어노테이션이 됩니다.</p>
<pre><code class="language-kotlin">@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude</code></pre>
<p>어노테이션이 붙을 수 있는 타깃이 정의된 이넘은 AnnotationTarget입니다. 이 안에는 클래스, 파일, 프로퍼티, 프로퍼티 접근자, 타입, 식 등에 대한 이넘 정의가 들어있습니다. 필요하다면 둘 이상의 타깃을 한꺼번에 선언할 수도 있습니다.</p>
<hr>
<h2 id="어노테이션-파라미터로-클래스-사용">어노테이션 파라미터로 클래스 사용</h2>
<p>클래스 참조를 파라미터로 하는 어노테이션 클래스를 선언하면 어떤 클래스를 선언 메타데이터로 참조할 수 있는 기능을 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">annotation class DeserializeInterface(val targetClass: KClass&lt;out Any&gt;)

interface Company{
    val name: String
}

data class CompanyImpl(override val name: String): Company

data class Person(
    val name: String,
    @DeserializeInterface(CompanyImpl::class) val company: Company
}</code></pre>
<p>@DeserializeInterface는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 사용하는 어노테이션입니다.</p>
<p>인터페이스는 인스턴스를 직접 만들 수 없기 때문에 어떤 클래스를 사용해 인터페이스를 구현할지 지정할 수 있어야합니다. 그렇기에 위의 코드에서는 CompanyImpl 클래스를 @DeserializeInterface 어노테이션의 인자로 넘깁니다.</p>
<p>KClass의 타입 파라미터는 이 KClass의 인스턴스가 가리키는 코틀린 타입을 지정합니다.</p>
<hr>
<h2 id="어노테이션-파라미터로-제네릭-클래스-받기">어노테이션 파라미터로 제네릭 클래스 받기</h2>
<p>@CustomSerializer 어노테이션은 커스텀 직렬화 클래스에 대한 참조를 인자로 받습니다. 직렬화 클래스는 ValueSerializer 인터페이스를 구현해야합니다.</p>
<pre><code class="language-kotlin">interface ValueSerializer&lt;T&gt; {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

annotation class CustomSerializer(
    val serializerClass: KClass&lt;out ValueSerializer&lt;*&gt;&gt;
)

data class Person(
    val name: String,
    @CustomSerializer(DateSerializer::class) val birthDate: Date
) </code></pre>
<p>ValueSerializer 클래스는 제네릭 클래스이므로 항상 타입 파라미터가 있습니다. 따라서 ValueSerializer 타입을 참조하려면 항상 타입 인자를 제공해야하지만 이 어노테이션이 어떤 타입에 대해 쓰일지 전혀 알 수 었습니다. 그렇기에 스타 프로젝션을 인자로 사용할 수 있습니다.</p>
<p>어노테이션의 파라미터는 ValueSerializer을 확장하는 클래스에 대한 참조만 올바른 인자로 인정됩니다. @CustomSerializer(Date::class)와 같은 어노테이션은 사용할 수 없습니다.</p>
<hr>
<h1 id="리플렉션이란">리플렉션이란?</h1>
<p>리플렉션은 실행 시점에 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법입니다.</p>
<p>보통 객체의 메소드나 프로퍼티에 접근할 때는 이름이 실제로 가리키는 선언을 정적으로 찾아내 해당하는 선언이 실제 존재함을 보장합니다. 하지만 직렬화 라이브러리의 경우는 어떤 객체든 JSON으로 변환할 수 있어야하기 때문에 특정 클래스나 프로퍼티만 참조할 수 없습니다.</p>
<p>이런 경우에 리플렉션을 사용해야 합니다.</p>
<p>코틀린에서 리플렉션을 사용하려면 보통 kotlin.reflect와 kotlin.reflect.full 패키지와 같은 코틀린 리플렉션 API를 다루면 됩니다. 차선책으로 java.lang.reflect 패키지에 정의된 자바 표준 리플렉션을 사용해도 됩니다.</p>
<hr>
<h2 id="kclass-kcallable-kfunction-kproperty">KClass, KCallable, KFunction, KProperty</h2>
<p>KClass를 사용하면 클래스 안에 있는 모든 선언을 열거하고 각 선언에 접근하거나 클래스의 상위 클래스를 얻는 등의 작업이 가능합니다. MyClass::class 라는 식을 쓰면 KClass의 인스턴스를 얻을 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlin.reflect.full.*

class Person(val name: String, val age: Int)

fun main() {
    val person = Person(&quot;Alice&quot;, 29)
    val kClass = person::class // KClass&lt;out Person&gt;의 인스턴스를 반환한다.
    println(kClass.simpleName)
    // Person
    kClass.memberProperties.forEach{ println(it.name) }
    // age
    // name
}</code></pre>
<p>KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스입니다. 이 안에는 call 메소드가 들어있습니다. call을 사용하면 함수나 프로퍼티의 게터를 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">fun foo(x: Int) = println(x)

fun main() {
    val kFunction = ::foo
    kFunction.call(42)
    // 42
}</code></pre>
<p>KCallable.call 메소드를 호출할 때는 call에 넘긴 인자의 개수와 원래 함수에 정의된 파라미터 개수가 맞아 떨어져야 합니다. 함수를 호출하기 위해 KFunction 을 사용할 수 있습니다.</p>
<p>::foo은 KFunction 클래스의 인스턴스로 KFunction1&lt;Int, Unit&gt;에는 파라미터와 반환값 타입 정보가 들어있습니다.</p>
<p>KFunction1 인터페이스를 통해 함수를 호출하려면 invoke 메소드를 이용해야 합니다. invoke는 정해진 인자만을 받아들입니다.</p>
<pre><code class="language-kotlin">import kotlin.reflect.KFunction2

fun sum(x: Int, y: Int) = x + y

fun main() {
    val kFunction: KFunction2&lt;Int, Int, Int&gt; = ::sum
    println(kFunction.invoke(1, 2) + kFunction.invoke(3, 4))
    // 10
    kFunction(1)
    // 컴파일 에러
}</code></pre>
<p>call 메소드는 모든 타입의 함수에 적용할 수 있는 일반적인 메소드지만 타입 안전성을 보장해주지 않습니다. 반면 invoke 메소드는 타입을 직접 명시하기 때문에 타입 안전성을 챙길 수 있습니다.</p>
<p>KProperty의 call 메소드는 프로퍼티의 메소드를 호출하지만 프로퍼티 인터페이스는 get 메소드를 사용해 프러퍼티 값을 얻을 수 있습니다.</p>
<pre><code class="language-kotlin">var counter = 0

fun main() {
    val kProperty = ::counter
    kProperty.setter.call(21)
    println(kProperty.get())
    // 21
}</code></pre>
<p>모든 선언에 어노테이션이 붙을 수 있기 때문에 KClass, KFunction, KParameter 등 실행 시점에 선언을 표현하는 인터페이스들은 모두 KAnnotatedElement를 확장합니다.</p>
<p>KClass는 클래스와 객체를 표현할 때 쓰입니다. KProperty는 모든 프로퍼티를 표현할 수 있고, 그 하위 클래스인 KMutableProperty는 var로 정의한 변경 가능한 프로퍼티를 표현합니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 어노테이션과 리플렉션에 대해 정리해봤습니다.</p>
<p>어노테이션과 리플렉션이라는 개념이 낯설어 공부하는데 무척 어려움을 느꼈습니다. 다만 어노테이션과 리플렉션을 활용한다면 직접 함수나 클래스를 미리 알고 있지 않아도 쉽게 사용할 수 있는 방법들을 보며 유용하게 사용할 수 있겠다는 생각이 들었습니다.</p>
<p>또한 remove라는 함수를 실제로 사용했을 때 컴파일러에서 경고와 함께 removeAt이라는 메소드로 변경하라는 오류를 본 적이 있습니다. 이런 메소드 관리도 어노테이션을 활용해 할 수 있다는 것을 보고 다양하게 활용될 수 있다는 것을 느꼈습니다.</p>
<p>이렇게 우리가 사용하는 라이브러리 안에는 다양한 자동화를 위한 도구로 어노테이션이 보이지 않는 곳에서 활용되고 있다는 것을 보며 코틀린을 보는 시야를 넓힐 수 있었습니다.</p>
<p>다음에는 DSL에 대한 내용으로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 11장 제네릭스]]></title>
            <link>https://velog.io/@noeyh_0j/11%EC%9E%A5-%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%8A%A4</link>
            <guid>https://velog.io/@noeyh_0j/11%EC%9E%A5-%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%8A%A4</guid>
            <pubDate>Thu, 12 Feb 2026 12:33:32 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>제네릭과 타입에 대한 개념에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 제네릭스를 중심으로 타입의 중요성까지 정리해보겠습니다.</p>
<hr>
<h1 id="제네릭스란">제네릭스란?</h1>
<p>제네릭스는 클래스나 메서드 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법입니다.</p>
<p>제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있습니다. 제네릭 타입의 인스턴스가 만들어질 때는 타입 파라미터를 구체적인 타입 인자로 치환합니다.</p>
<p>리스트를 다루는 함수를 작성하면 어떤 특정 타입을 저장하는 리스트 뿐만 아니라 모든 리스트를 다룰 수 있는 함수를 원할 것입니다. 이럴 때 필요한 것이 제네릭 함수입니다.</p>
<hr>
<h1 id="제네릭-함수란">제네릭 함수란?</h1>
<p>제네릭 함수는 그 자신이 타입 파라미터를 받으며, 제네릭 함수를 호출할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야 합니다.</p>
<pre><code class="language-kotlin">fun &lt;T&gt; List&lt;T&gt;.slice(indices: IntRange): List&lt;T&gt;</code></pre>
<p>함수의 타입 파라미터 T가 수신 객체와 반환 타입에 쓰입니다. 이 함수를 호출할 때 타입 인자를 명시적으로 지정할 수 있지만 대부분의 경우 컴파일러가 타입을 추론할 수 있습니다.</p>
<pre><code class="language-kotlin">fun main() {
    val letters = (&#39;a&#39;..&#39;z&#39;).toList()
    println(letters.slice&lt;Char&gt;(0..2))
    // [a, b, c]
    println(letters.slice(10..13))
    // [k, l, m, n]
}</code></pre>
<p>클래스나 인터페이스 안에 정의된 메소드, 최상위 함수, 확장 함수에서 타입 파라미터를 선언할 수 있으며, 수신 객체나 파라미터 타입에 타입 파라미터를 사용할 수 있습니다.</p>
<p>하지만 일반 프로퍼티는 타입 파라미터를 가질 수 없습니다. 클래스 프로퍼티에 여러 타입의 값을 저장할 수 없기 때문에 일반 프로퍼티는 여러 타입을 가질 수 있는 타입 파라미터를 가질 수 없습니다.</p>
<pre><code class="language-kotlin">val &lt;T&gt; List&lt;T&gt;.penultimate: T
    get() = this[size - 2]

fun main() {
    println(listOf(1, 2, 3, 4).penultimate)
    // 3
}</code></pre>
<hr>
<h2 id="제네릭-클래스를-선언하는-방법은">제네릭 클래스를 선언하는 방법은?</h2>
<p>타입 파라미터를 넣은 홑화살괄호(&lt;&gt;)를 클래스나 인터페이스 이름 뒤에 붙이면 해당 클래스나 인터페이스를 제네릭하게 만들 수 있습니다.</p>
<p>타입 파라미터를 이름 뒤에 붙이고 나면 클래스 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">interface List&lt;T&gt; {
    operator fun get(index: Int): T
    // ...
}</code></pre>
<p>제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파리미터에 대해 타입 인자를 지정해야 합니다. 이때 구체적인 타입을 넘길 수도 있고 타입 파라미터로 받은 타입을 넘길 수도 있습니다.</p>
<pre><code class="language-kotlin">// 구체적인 타입인 String을 타입 인자로 지정함.
class StringList: List&lt;String&gt; {
    override fun get(index: Int): String = TODO()
    // ...
}

// 제네릭 타입 파라미터 T를 타입 인자로 지정함.
class ArrayList&lt;T&gt;: List&lt;T&gt; {
    override fun get(index: Int): T = TODO()
    // ...
}</code></pre>
<hr>
<h2 id="타입-파라미터의-제약은">타입 파라미터의 제약은?</h2>
<p>타입 파라미터 제약은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능입니다. 예를 들어 sum 함수에Int나 Double타입은 합을 적용할 수 있지만 String은 적용할 수 없습니다.</p>
<p>이런 경우에 sum 함수가 타입 파라미터로 숫자 타입만을 허용하도록 정의하는 것입니다.</p>
<p>제약을 가하려면 타입 파라미터 이름 뒤에 콜론을 표시하고 그 뒤에 상계 타입을 적으면 됩니다.</p>
<pre><code class="language-kotlin">fun &lt;T: Number&gt; List&lt;T&gt;.sum(): T</code></pre>
<p>Number는 코틀린 표준 라이브러리에서 숫자 타입을 표현하는 모든 클래스의 상위 클래스이기 때문에 Int나 Double을 지정해도 괜찮습니다.</p>
<pre><code class="language-kotlin">fun main() {
    println(listOf(1, 2, 3).sum())
    // 6
}</code></pre>
<p>또한 타입 파라미터 T에 대한 상계를 정하고 나면 T 타입의 값을 그 상계 타입의 값으로 취급할 수 있어서 상계 타입에 정의된 메소드를 T 타입 값에 대해 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">fun &lt;T: Number&gt; oneHalf(value: T): Double {
    return value.toDouble() / 2.0
}

fun main() {
    println(oneHalf(3))
    // 1.5
}</code></pre>
<hr>
<h2 id="명시적으로-표시하여-널이-될-수-있는-타입-인자-제외시키는-방법은">명시적으로 표시하여 널이 될 수 있는 타입 인자 제외시키는 방법은?</h2>
<p>아무런 상계를 정하지 않은 타입 파라미터는 Any?를 상계로 정한 파라미터와 같습니다.</p>
<p>타입 T에 물음표가 붙어있지 않더라도 해당하는 타입 인자로 널이 될 수 있는 타입을 사용할 수도 있습니다.</p>
<p>만약 항상 널이 될 수 없는 타입만 타입 인자로 받게 만들려면 타입 파라미터에 Any를 상계로 하는 제약을 가해야 합니다.</p>
<pre><code class="language-kotlin">class Processor&lt;T: Any&gt; {
    fun process(value: T) {
        value.hashCode()
    }
}</code></pre>
<p>Any 뿐만 아니라 다른 널이 될 수 없는 타입을 사용해 상계를 정해도 타입 파라미터가 널이 아닌 타입으로 제약됩니다.</p>
<hr>
<h2 id="자바와-상호운용할-때는-제네릭-타입을-널이-될-수-없음으로-표시해야-한다">자바와 상호운용할 때는 제네릭 타입을 ‘널이 될 수 없음’으로 표시해야 한다</h2>
<p>자바에서는 어노테이션을 활용해 특정 부분에서만 널이 될 수 없도록 지정할 수 있습니다. 하지만 코틀린에서는 이런 제약을 직접 변경할 수 없기에 자바의 코드와는 다른 결과를 가져올 수 있습니다.</p>
<pre><code class="language-java">import org.jetbrains.annotations.NotNull;

public interface JBox&lt;T&gt; {
    /**
    * 널이 될 수 없는 값을 박스에 넣는다.
    */
    void put(@NotNull T t);
    /**
    * 널 값이 아닌 경우 값을 박스에 넣고
    * 널 값인 경우 아무것도 하지 않는다.
    */
    void putIfNotNull(T t);
}</code></pre>
<pre><code class="language-kotlin">class KBox&lt;T: Any&gt;: JBox&lt;T&gt; {
    override fun put(t: T) { /* ... */ }
    override fun putIfNotNull(t: T) { /* 문제 생김 */ }
}</code></pre>
<p>이런 문제를 해결하기 위해 코틀린은 타입을 사용하는 지점에서 절대로 널이 될 수 없다고 표시하는 방법을 제공합니다. 이런 표시는 문법적으로 T &amp; Any 로 표현됩니다.</p>
<pre><code class="language-kotlin">class KBox&lt;T: Any&gt;: JBox&lt;T&gt; {
    override fun put(t: T &amp; Any) { /* ... */ }
    override fun putIfNotNull(t: T) { /* ... */ }
}</code></pre>
<hr>
<h1 id="실행-시점-제네릭스-동작은">실행 시점 제네릭스 동작은?</h1>
<p>JVM의 제네릭스는 보통 타입 소거를 사용해 구현됩니다.이는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 의미입니다.</p>
<hr>
<h2 id="실행-시점에-제네릭-클래스의-타입-정보를-찾을-때-한계는">실행 시점에 제네릭 클래스의 타입 정보를 찾을 때 한계는?</h2>
<p>위에서 설명한 것처럼 제네릭 타입 인자 정보는 런타임에 지워집니다. 예를 들어 각 String 과 Int 타입의 원소를 가지는 리스트가 있다면 두 List 객체가 어떤 타입을 원소로 저장하는지 실행 시점에는 알 수 없습니다. 이런 점 때문에 타입 인자를 다룰 때 한계가 생깁니다.</p>
<pre><code class="language-kotlin">val list1: List&lt;String&gt; = listOf(&quot;a&quot;, &quot;b&quot;)
val list2: List&lt;Int&gt; = listOf(1, 2, 3)</code></pre>
<p>다음으로 타입 소거로 인해 실행 시점에 타입 인자를 검사할 수 없습니다. 예를 들어 리스트가 문자열로 이뤄진 리스트인지 정수로 이뤄진 리스트인지 is 검사를 통해 타입 인자로 지정한 타입을 검사할 수 없는 것입니다.</p>
<pre><code class="language-kotlin">fun readNumbersOrWords(): List&lt;Any&gt; {
    val input = readln()
    val words: List&lt;String&gt; = input.split(&quot;,&quot;)
    val numbers: List&lt;Int&gt; = words.mapNotNull { it.toIntOrNull() }
    return numbers.ifEmpty{ words }
}

fun printList(l: List&lt;Any&gt;) {
    when(l) {
        is List&lt;String&gt; -&gt; println(&quot;Strings: $l&quot;)
        is List&lt;Int&gt; -&gt; println(&quot;Integers: $l&quot;)
    }
}

fun main() {
    val list = readNumbersOrWords()
    printList(list)
}</code></pre>
<p>그렇기에 printList에서 오류가 발생합니다. 즉, 실행 시점에 리스트임은 확실히 알 수 있지만 어떤 타입의 원소가 들어있는 리스트인지는 알 수 없습니다.</p>
<p>다만 저장해야 하는 타입 정보의 크기가 줄어 애플리케이션의 전체 메모리 사용량이 줄어든다는 장점도 있습니다.</p>
<p>as나 as? 캐스팅에도 제네릭 타입을 사용할 수 있습니다. 하지만 기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅해도 캐스팅에 성공한다는 점을 조심해야 합니다. 실행 시점에서는 제네릭 타입의 타입 인자를 알 수 없기 때문에 항상 성공하는 것입니다. 이때는 컴파일러가 경고를 해줍니다.</p>
<pre><code class="language-kotlin">fun printSum(c: Collection&lt;*&gt;) {
    val intList = c as? List&lt;Int&gt;
        ?: throw IllegalArgumentException(&quot;List is expected&quot;)
    println(intList.sum()
}

fun main() {
    printSum(listOf(1, 2, 3))
    // 6
    printSum(setOf(3, 4, 5))
    // IllegalArgumentException: List is expected
    println(listOf(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;))
    // ClassCastException: String cannot be cast to Number
}</code></pre>
<p>하지만 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 오류가 발생합니다.</p>
<p>그치만 타입 정보가 주어진 경우에는 is 검사를 수행할 수 있습니다.</p>
<pre><code class="language-kotlin">fun printSum(c: Collection&lt;Int&gt;) {
    when(c) {
        is List&lt;Int&gt; -&gt; println(&quot;List sum: ${c.sum()}&quot;)
        is Set&lt;Int&gt; -&gt; println(&quot;Set sum: ${c.sum()}&quot;)
    }
}

fun main() {
    printSum(listOf(1, 2, 3))
    // List sum: 6
    printSum(setOf(3, 4, 5))
    // Set sum: 12
}</code></pre>
<hr>
<h2 id="타입-인자를-실체화-시키는-방법은">타입 인자를 실체화 시키는 방법은?</h2>
<p>타입 인자 정보가 실행 시점에 지워지는 코틀린 제네릭 타입을 피할 수 있는 방법은 인라인 함수를 사용하는 것이다. 인라인 함수를 사용하면 타입 파라미터가 실체화됩니다.</p>
<p>인라인 함수를 만들고 타입 파라미터를 reified로 지정하면 실행 시점에 검사할 수 있습니다.</p>
<pre><code class="language-kotlin">inline fun &lt;reified T&gt; isA(value: Any) = value is T

fun main() {
    println(isA&lt;String&gt;(&quot;abc&quot;))
    // true
    println(isA&lt;String&gt;(123))
    // false
}</code></pre>
<hr>
<h2 id="인라인-함수에서만-실체화된-타입-인자를-쓸-수-있는-이유는">인라인 함수에서만 실체화된 타입 인자를 쓸 수 있는 이유는?</h2>
<p>컴파일러는 인라인 함수의 본문을 구현한 바이트코드를 그 함수가 호출되는 모든 지점에 삽입합니다. 즉 컴파일러는 실체화된 타입 인자를 사용해 인라인 함수를 호출하는 각 부분의 정확한 타입 인자를 할 수 있는 것입니다.</p>
<p>만들어진 바이트코드는 타입 파라미터가 아니라 구체적인 타입을  사용하므로 실행 시점에 벌어지는 타입 소거의 영향을 받지 않습니다.</p>
<p>다만 자바 코드에서는 reified 타입 파라미터를 사용하는 인라인 함수를 호출할 수 없다는 점을 주의해야합니다. 자바에서는 코틀린 인라인 함수를 다른 보통 함수처럼 호출하기 때문에 실제로 인라이닝되지 않기 때문입니다.</p>
<hr>
<h2 id="클래스-참조를-실체화된-타입-파라미터로-대신하기">클래스 참조를 실체화된 타입 파라미터로 대신하기?</h2>
<p>API에 대한 코틀린 어댑터를 구축하는 경우 실체화된타입 파라미터를 사용할 수 있습니다. 예를 들면 표준 자바 API인 ServiceLoader를 사용해 서비스를 읽어 들이려면 아래와 같이 호출해야 합니다. </p>
<pre><code class="language-kotlin">val serviceImpl = serviceLoader.load(Service::class.java)</code></pre>
<p>실체화된 타입 파라미터를 활용하면 더 읽기 쉬운 코드로 작성할 수 있습니다.</p>
<pre><code class="language-kotlin">inline fun &lt;reified T&gt; loadService() {
    return ServiceLoader.load(T::class.java)
}

val serviceImpl = loadService&lt;Service&gt;()</code></pre>
<hr>
<h2 id="실체화된-타입-파라미터가-있는-접근자를-정의하기">실체화된 타입 파라미터가 있는 접근자를 정의하기</h2>
<p>제네릭 타입에 대해 프로퍼티 접근자를 정의하는 경우 프로퍼티를 inline으로 표시하고 타입 파라미터를 reified로 하면 타입 인자에 쓰인 구체적인 클래스를 참조할 수 있습니다. </p>
<pre><code class="language-kotlin">inline val &lt;reified T&gt; T.canonical: String
    get() = T::class.java.canonicalName

fun main() {
    println(listOf(1, 2, 3).canonical)
    // java.util.List
    println(1.canonical)
    // java.lang.Integer
}</code></pre>
<hr>
<h2 id="실체화된-타입-파라미터의-제약은">실체화된 타입 파라미터의 제약은?</h2>
<p>실체화된 타입 파라미터는 다음과 같은 경우에 사용할 수 있습니다.</p>
<ul>
<li>타입 검사와 캐스팅</li>
<li>코틀린 리플렉션 API(::class)</li>
<li>코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)</li>
<li>다른 함수를 호출할 때 타입 인자로 사용</li>
</ul>
<p>하지만 아래와 같은 일은 할 수 없습니다.</p>
<ul>
<li>타입 파라미터 클래스의 인스턴스 생성하기</li>
<li>타입 파라미터 클래스의 동반 객체 메소드 호출하기</li>
<li>실체화된 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기</li>
<li>클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기</li>
</ul>
<hr>
<h1 id="변성이란">변성이란?</h1>
<p>변성은 List&lt;String&gt;과 List&lt;Any&gt;같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념입니다.</p>
<hr>
<h2 id="인자를-함수에-넘겨도-안전한지-어떻게-알까">인자를 함수에 넘겨도 안전한지 어떻게 알까?</h2>
<p>변성은 인자를 함수에 넘겨도 안전한지 판단하게 해줍니다. 예를 들어 List&lt;Any&gt; 타입의 파라미터를 받는 함수에 List&lt;String&gt;을 넘기면 절대로 안전합니다. String클래스는 Any를 확장하기 때문입니다.</p>
<pre><code class="language-kotlin">fun printContents(list: List&lt;Any&gt;) {
    println(list.joinToString())
}

fun main() {
    printContents(listOf(&quot;abc&quot;, &quot;bac&quot;))
    // abc, bac
}</code></pre>
<p>다만 리스트를 변경하는 함수일 경우 컴파일러가 호출을 금지하는 것을 볼 수 있습니다.</p>
<pre><code class="language-kotlin">fun addAnswer(list: MutableList&lt;Any&gt;) {
    list.add(42)
}

fun main() {
    val strings = mutableListOf(&quot;abc&quot;, &quot;bac&quot;)
    addAnswer(strings)
    println(strings.maxBy{ it.length }) // 이 시점에서 예외가 발생할 것이다.
}</code></pre>
<p>이 예제를 보고 MutableList&lt;Any&gt;가 필요한 곳에 MutableList&lt;String&gt;을 넘기면 안된다는 사실을 알 수 있습니다.</p>
<hr>
<h2 id="클래스-타입-하위-타입">클래스, 타입, 하위 타입</h2>
<p>제네릭 클래스가 아닌 클래스에서는 클래스 이름을 바로 타입으로 쓸 수 있습니다. 예를 들어 var x: String이라고 쓰면 String 클래스의 인스턴스를 저장하는 변수를 정의할 수 있습니다.</p>
<p>제네릭 클래스에서는 올바른 타입을 얻으려면 제네릭 타입의 타입 파라미터를 구체적인 타입 인자로 바꿔줘야 합니다. 예를 들어 List는 타입이 아니라 클래스입니다. 하지만 타입 인자를 치환한 List&lt;Int&gt;, List&lt;String?&gt; 등은 모두 타입입니다. 즉, 각각의 제네릭 클래스는 무수히 많은 타입을 만들어낼 수 있습니다.</p>
<p>이런 타입 사이의 관계를 파악하기 위해서는 하위 타입이라는 개념을 잘 알아야 합니다. 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이라고 할 수 있습니다. 예를 들어 Int는 Number의 하위 타입이지만 String의 하위 타입은 아닌 것입니다.</p>
<p>하위 타입과 반대되는 개념을 상위 타입이라고 합니다. A 타입이 B 타입의 하위 타입이라면 B는 A의 상위 타입입니다.</p>
<p>하위 타입인지가 중요한 이유는 컴파일러는 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행하기 때문입니다.</p>
<pre><code class="language-kotlin">fun test(i: Int) {
    val n: Number = i

    fun f(s: String) { /* ... */ }
    f(i)
    // 컴파일 오류가 발생합니다.
}</code></pre>
<p>String은 CharSequence의 하위 타입인 것처럼 어떤 인터페이스를 구현하는 클래스의 타입은 그 인터페이스의 하위 타입입니다.</p>
<p>널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입입니다. 널이 될 수 없는 타입의 값은 널이 될 수 있는 타입의 변수에 저장할 수 있지만, 반대의 경우는 성립하지 않기 때문입니다.</p>
<pre><code class="language-kotlin">val s: String = &quot;abc&quot;
val t: String? = s
// 성립한다.</code></pre>
<p>즉, 제네릭 타입을 얘기할 때 특히 하위 클래스와 하위 타입의 차이가 중요해집니다.</p>
<p>어떤 제네릭 타입에 대해 서로 다른 두 타입 A와 B에 대해 MutableList&lt;A&gt;가 항상 MutableList&lt;B&gt;의 하위 타입도 아니고 상위 타입도 아닌 경우에 대해 무공변이라고 말합니다. </p>
<p>A가 B의 하위 타입이면 List&lt;A&gt;는 List&lt;B&gt;의 하위 타입이 되는 클래스나 인터페이스를 공변적이라고 말합니다.</p>
<hr>
<h2 id="공변성은-하위-타입-관계를-유지한다">공변성은 하위 타입 관계를 유지한다</h2>
<p>공변적은 클래스는 제네릭 클래스에 대해 A가 B의 하위 타입일 때 Producer&lt;A&gt;가 Producer&lt;B&gt;의 하위 타입인 경우를 말합니다. 예를 들면 Cat이 Animal의 하위 타입이기 때문에 Producer&lt;Cat&gt;은 Producer&lt;Animal&gt;의 하위 타입입니다.</p>
<p>제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 넣어야 합니다.</p>
<pre><code class="language-kotlin">interface Producer&lt;out T&gt; {
    fun produce(): T
}</code></pre>
<p>클래스의 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">open class Animal {
    fun feed() { /* ... */ }
}

class Herd&lt;T: Animal&gt; {
    val size: Int get() = { /* ... */ }
    operator fun get(i: Int): T { /* ... */ }
}

fun feedAll(animals: Herd&lt;Animal&gt;) {
    for (i in 0..&lt;animals.size) {
        animals[i].feed()
    }
}

class Cat: Animal() {
    fun cleanLitter() { /* ... */ }
}

fun takeCareOfCats(cats: Herd&lt;Cat&gt;) {
    for (i in 0..&lt; cats.size) {
        cats[i].cleanLitter()
    }
    // feedAll(cats)
}</code></pre>
<p>feedAll에 Cat타입을 넘기면 타입 불일치 에러가 나타나게 됩니다. Herd 클래스의 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 떄문에 고양이 무리는 동물 무리의 하위 클래스가 아니게 된 것입니다.</p>
<p>Herd 클래스를 공변적인 클래스로 만들면 강제 캐스팅을 하지 않고 타입 불일치 문제를 해결할 수 있습니다.</p>
<pre><code class="language-kotlin">class Herd&lt;out T: Animal&gt; {
    val size: Int get() = { /* ... */ }
    operator fun get(i: Int): T { /* ... */ }
}

fun takeCareOfCats(cats: Herd&lt;Cat&gt;) {
    for (i in 0..&lt; cats.size) {
        cats[i].cleanLitter()
    }
    feedAll(cats)
}</code></pre>
<p>하지만 타입 파라미터를 공변적으로 지정하면 클래스 내부에서 타입 안전성을 보장하기 위해 공변적 파라미터의 위치를 항상 아웃 위치에 놓는 방식으로 사용 방법을 제한합니다. 이는 클래스가 T 타입의 값을 생산할 수는 있지만 소비할 수는 없다는 의미입니다.</p>
<p>클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은 인과 아웃 위치로 나뉩니다. T 함수가 반환 타입에 쓰인다면 T는 아웃 위치에 존재하는 것이고 T 타입의 값을 생산합니다. T가 함수의 파라미터 타입에 쓰인다면 T는 인 위치에 있으며 T 타입의 값을 소비합니다. </p>
<p>즉, 타입 파라미터 T에 붙은 out 키워드는 하위 타입 관계가 유지되고, T를 아웃 위치에서만 사용할 수 있다는 의미를 가집니다.</p>
<p>MutableList&lt;T&gt;를 타입 파라미터 T에 대해 공변적인 클래스로 선언할 수 없습니다. MutableList&lt;T&gt;에는 T를 인자로 받아 그 타입의 값을 반환하는 메소드가 있기 때문에 T가 인과 아웃 위치에 동시에 쓰이기 때문입니다.</p>
<pre><code class="language-kotlin">inteface MutableList&lt;T&gt; : List&lt;T&gt;, MutableCollection&lt;T&gt; {
    override fun add(element: T): Boolean
}</code></pre>
<p>위의 인터페이스에서 볼 수 있듯이 T가 인 위치에서 사용됩니다.</p>
<p>다만 생성자 파라미터는 인이나 아웃 위치 어느 쪽도 아닙니다. 변성은 제네릭 타입의 인스턴스 역할을 하는 클래스 인스턴스를 잘못 사용하는 일이 없게 방지하는 역할을 하는데 생성자는 나중에 호출할 수 있는 메소드가 아니기 때문에 위험할 여지가 없어 상관이 없는 것입니다.</p>
<p>하지만 var이나 val 키워드를 생성자 파라미터에 적는다면 게터나 세터를 정의하는 것과 같습니다. 그렇기에 val 프로퍼티는 아웃 위치, var 프로퍼티는 아웃과 인 모두에 해당합니다.</p>
<hr>
<h2 id="반공변성은-하위-타입-관계를-뒤집는다">반공변성은 하위 타입 관계를 뒤집는다</h2>
<p>반공변성은 공변성을 거울에 비친 상이라고 할 수 있습니다. 반공변 클래스의 하위 타입 관계는 그 클래스의 타입 파라미터의 상하위 타입 관계와 반대입니다.</p>
<pre><code class="language-kotlin">interface Comparator&lt;in T&gt; {
    fun compare(e1: T, e2: T): Int { /* ... */ }
}</code></pre>
<p>이 인터페이스의 메소드는 T 타입의 값을 소비하기만 하고, 이는 T가 in 위치에서만 쓰인다는 의미입니다. 따라서 T 앞에 in 키워드를 붙여야하는 것입니다.</p>
<p>반공변성은 어떤 클래스에 대해 타입 B가 타입 A의 하위 타입일 때 Consumer&lt;A&gt;가 Consumer&lt;B&gt;의 하위 타입인 관계가 성립하는 것을 의미합니다.</p>
<hr>
<h2 id="사용-지점-변성을-사용해-타입이-언급되는-지점에서-변성-지정">사용 지점 변성을 사용해 타입이 언급되는 지점에서 변성 지정</h2>
<p>클래스를 선언하면서 변성을 지정하면 그 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치므로 편리합니다. 이런 방식을 선언 지점 변성이라고 부릅니다. </p>
<p>자바에서는 타입 파라미터가 있는 타입으로 사용할 때마다 그 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시해야 합니다. 이런 방식을 사용 지점 변성이라고 부릅니다.</p>
<p>코틀린도 사용 지점 변성을 지원합니다. 따라서 타입 파라미터가 공변적인지 반공변적인지 선언할 수 없는 경우에도 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있습니다.</p>
<hr>
<h2 id="스타-프로젝션이란">스타 프로젝션이란?</h2>
<p>스타 프로젝션이란 제네릭 타입 인자 정보가 없음을 표현하고자할 때 <em>을 사용해 표현합니다. 예를 들어 타입이 알려지지 않은 리스트를 List&lt;</em>&gt;이라는 구문으로 표현할 수 있습니다.</p>
<p>조심해야할 점은 MutableList&lt;<em>&gt;는 MutableList&lt;Any?&gt;와 같지 않습니다. MutableList&lt;Any?&gt;는 모든 타입의 원소를 담을 수 있음을 알 수 있는 리스트지만 MutableList&lt;</em>&gt;는 어떤 정해진 구체적인 타입의 원소만을 담는 리스트지만 정확히 모른다는 사실을 표현하는 것입니다.</p>
<p>즉, MutableList&lt;*&gt;은 String과 같은 구체적인 원소를 저장하기 위해 만들어진 것입니다.</p>
<pre><code class="language-kotlin">fun printFirst(list: List&lt;*&gt;) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}

fun main() {
    printFirst(listOf(&quot;Sveta&quot;, &quot;Seb&quot;, &quot;Dima&quot;, &quot;Roman&quot;))
    // Sveta
}</code></pre>
<hr>
<h2 id="타입-별명">타입 별명</h2>
<p>타입 별명은 복잡한 제네릭 타입이나 함수형 타입을 여러 곳에서 매번 반복해 사용하는 것을 피하고 싶을 때 유용하게 사용할 수 있습니다. 타입 별명은 typealias 키워드 뒤에 별명을 적어 타입 별명을 선언할 수 있습니다.</p>
<pre><code class="language-kotlin">typealias NameCombiner = (String, String, String, String) -&gt; String

val authorsCombiner: NameCombiner = { a, b, c, d -&gt; &quot;$a et al.&quot; }
val bandCombiner: NameCombiner = { a, b, c, d -&gt; &quot;$a, $b &amp; The Gang&quot; }

fun combineAuthors(combiner: NameCombiner) {
    println(combiner(&quot;Sveta&quot;, &quot;Seb&quot;, &quot;Dima&quot;, &quot;Roman&quot;))
}

fun main() {
    combineAuthors(bandCombiner)
    // Sveta, Seb &amp; The Gang
    combineAuthors(authorsCombiner)
    // Sveta et al.
    combineAuthors{ a, b, c, d -&gt; &quot;$d, $c &amp; Co.&quot; }
    // Roman, Dima &amp; Co.
}</code></pre>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 제네릭스의 개념부터 타입 별명까지 개념을 정리해봤습니다.</p>
<p>제네릭스라는 개념을 이번 장을 공부하며 처음 알게 되었습니다. 기존 예제에서 나오는 파라미터 타입 T가 무엇인지 이해하지 못했지만 이번 장을 통해 제네릭 함수에 대해 이해하게 되었습니다.</p>
<p>또한 컬렉션이 어떤 원리로 모든 타입을 가질 수 있는가에 대한 의문을 가지고 있었는데, 컬렉션과 관련된 대부분의 기능을 구현할 때 타입 파라미터를 활용해 String, Number 뿐만 아니라 커스텀 클래스에 대한 타입도 들어갈 수 있다는 것을 알게 되었습니다. </p>
<p>다음에는 어노테이션과 리플렉션에 대한 개념으로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 10장 고차 함수: 람다를 파라미터와 반환값으로 사용]]></title>
            <link>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</link>
            <guid>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</guid>
            <pubDate>Wed, 11 Feb 2026 08:57:58 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>고차 함수에 대한 정의와 사용 방법에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>이번 글에서는 고차 함수가 무엇인지, 고차 함수의 비용을 줄이기 위한 방법 등에 대해 정리해보겠습니다.</p>
<hr>
<h1 id="고차-함수란">고차 함수란?</h1>
<p>고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수를 말합니다.</p>
<hr>
<h2 id="함수-타입을-지정하는-방법은">함수 타입을 지정하는 방법은?</h2>
<p>함수 타입을 지정하려면 함수 파라미터의  타입을 괄호 안에 넣고 그 뒤에 화살표를 추가한 다음, 함수의 반환 타입을 지정하면 됩니다.</p>
<pre><code class="language-kotlin">val sum: (Int, Int) -&gt; Int = { x, y -&gt; x + y }
val action: () -&gt; Unit = { println(42) }</code></pre>
<p>Unit 타입은 의미 있는 값을 반환하지 않는 함수 반환 타입에 쓰는 타입입니다. 그냥 함수를 정의한다면 Unit 반환 타입 지정을 생략해도 되지만 함수 타입을 선언할 때는 Unit을 생략할 수 없습니다.</p>
<p>또한 널이 될 수 있는 함수 타입 변수를 정의할 수도 있습니다. </p>
<pre><code class="language-kotlin">val canReturnNull: (Int, Int) -&gt; Int? = { x, y -&gt; null }</code></pre>
<p>다만 함수의 반환 타입이 아니라 함수 타입 전체가 널이 될 수 있는 타입을 선언하기 위해서는 함수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야 합니다.</p>
<pre><code class="language-kotlin">val funOrNull: ((Int, Int) -&gt; Int)? = null</code></pre>
<hr>
<h2 id="인자로-받은-함수를-호출하는-방법은">인자로 받은 함수를 호출하는 방법은?</h2>
<p>인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같습니다. 또한 함수 타입에서 파라미터의 이름을 지정할 수도 있습니다. </p>
<p>다만, 함수 타입의 람다를 정의할 때 파라미터의 이름이 꼭 함수 타입 선언의 파라미터 이름과 일치하지 않아도 됩니다.</p>
<pre><code class="language-kotlin">fun twoAndThree(operation: (Int, Int) -&gt; Int) {
    val result = operation(2, 3)
    println(&quot;The result is $result&quot;)
}

fun main() {
    twoAndThree{ a, b -&gt; a + b }
    // The result is 5
    twoAndThree{ a, b -&gt; a * b }
    // The result is 6
}</code></pre>
<hr>
<h2 id="자바에서-코틀린-함수-타입을-사용하기">자바에서 코틀린 함수 타입을 사용하기?</h2>
<p>자바에서 코틀린 람다를 사용할 때는 자동 SAM(Single Abstract Method) 변환을 사용해 자바에서 사용할 수 있게끔 변환합니다.</p>
<pre><code class="language-kotlin">// 코틀린 함수
fun processTheAnswer(f: (Int) -&gt; Int) {
    println(f(42))
}

// 자바 호출
processTheAnswer(number -&gt; number + 1);
// 43</code></pre>
<p>그렇기에 자바에서 람다를 인자로 받는 코틀린 표준 라이브러리의 확장 함수를 쉽게 사용할 수 있지만, 수신 객체를 명시적으로 전달해야합니다.</p>
<p>또한 Unit을 반환하는 함수나 람다를 자바로 작성할 수도 있습니다. 하지만 코틀린 Unit 타입에는 값이 존재하므로 자바에서는 그 값을 명시적으로 반환해줘야 합니다.</p>
<pre><code class="language-java">import kotlin.collections.CollectionsKt;

public static void main(String[] args) {
    List&lt;String&gt; strings = nuwArrayList();
    strings.add(&quot;42);
    CollectionsKt.forEach(strings, s -&gt; {
        System.out.println(s);
        return Unit.INSTANCE;
    });
}</code></pre>
<hr>
<h2 id="함수-타입의-자세한-구현은">함수 타입의 자세한 구현은?</h2>
<p>내부에서 코틀린 함수 타입은 일반 인터페이스입니다. 함수 타입의 변수는 FunctionN 인터페이스를 구현합니다. 이 때 인터페이스는 함수 파라미터 개수에 따라 다른 인터페이스를 구현합니다. Function0&lt;R&gt;은 인자를 받지 않으며, Function1&lt;P1, R&gt;은 인자를 하나 받는 등으로 구현됩니다.</p>
<p>각 인터페이스는 invoke라는 유일한 메소드가 정의돼 있습니다.</p>
<p>FunctionN 인터페이스는 컴파일러가 생성한 합성 타입으로, 코틀린 표준 라이브러리에서 정의를 찾을 수 없습니다. 대신 필요할 때마다 컴파일러는 이런 인터페이스를 생성해주며, 이는 개수 제한 없이 원하는 만큼 파라미터를 사용하는 함수에 대한 인터페이스를 사용할 수 있다는 의미입니다.</p>
<pre><code class="language-kotlin">interface Function&lt;P1, out R&gt; {
    operator fun invoke(p1: P1): R
}

fun processTheAnswer(f: Function1&lt;Int, Int&gt;) {
    println(f.invoke(42))
}</code></pre>
<hr>
<h2 id="함수-타입의-파라미터에-대해-기본값을-지정하는-방법은">함수 타입의 파라미터에 대해 기본값을 지정하는 방법은?</h2>
<p>파라미터를 함수 타입으로 선언할 때도 마찬가지로 기본값을 지정할 수 있습니다.</p>
<pre><code class="language-kotlin">fun &lt;T&gt; Collection&lt;T&gt;.joinToString(
    separator: String = &quot;, &quot;,
    prefix: String = &quot;&quot;,
    postfix: String = &quot;&quot;,
    transform: (T) -&gt; String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex())
        if (index &gt; 0) result.append(separator)
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

fun main() {
    val letters = listOf(&quot;Alpha&quot;, &quot;Beta&quot;)
    println(letters.joinToString())
    // Alpha, Beta
    println(letters.joinToString{ it.lowercase() })
    // alpha, beta
    println(letters.joinToString(separator = &quot;! &quot;, postfix = &quot;! &quot;,
        transform = { it.uppercase() }))
    // ALPHA!, BETA!
}</code></pre>
<p>람다의 기본값을 지정하지 않았다면 위 코드처럼 유연하게 각 원소를 문자열로 변환하는 것을 제어할 수 없었을 것입니다. 이처럼 람다에 기본값을 지정하는 것은 함수의 유연성을 늘릴 수 있게 됩니다.</p>
<p>다른 접근 방법으로 널이 될 수 있는 함수 타입을 사용할 수도 있습니다. 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수를 직접 호출할 수 없기 때문에 null 여부를 명시적으로 검사하여 구현할 수 있습니다. </p>
<pre><code class="language-kotlin">fun &lt;T&gt; Collection&lt;T&gt;.joinToString(
    separator: String = &quot;, &quot;,
    prefix: String = &quot;&quot;,
    postfix: String = &quot;&quot;,
    transform: ((T) -&gt; String)? = null
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex())
        if (index &gt; 0) result.append(separator)
        val str = transform?.invoke(element)
            ?: element.toString()
        result.append(str)
    }

    result.append(postfix)
    return result.toString()
}</code></pre>
<hr>
<h2 id="함수를-함수에서-반환하기">함수를 함수에서 반환하기?</h2>
<p>함수를 반환하는 함수는 프로그램의 상태나 다른 조건에 따라 달라질 수 있는 로직이 있을 경우에 유용하게 사용할 수 있습니다.</p>
<p>예를 들면 사용자가 선택한 배송 수단에 따라 배송비를 계산하는 방법이 달라질 경우에도 이를 활용할 수 있습니다.</p>
<p>다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정해야 합니다. 그리고 return 식에 람다, 멤버 참조, 함수 타입의 값을 계산하는 식 등을 넣으면 됩니다.</p>
<pre><code class="language-kotlin">enum class Delivery{ STANDARD, EXPEDITED }

class Order(val itemCount: Int)

fun getShippingCostCalculator(delivery: Delivery): (Order) -&gt; Double {
    if (delivery == Devlivery.EXPEDITED) {
        return { order -&gt; 6 + 2.1 * order.itemCount }
    }
    return { order -&gt; 1.2 * order.itemCount }
}

fun main() {
    val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
    println(&quot;Shipping costs ${calculator(Order(3))}&quot;)
    // Shipping costs 12.3
}</code></pre>
<hr>
<h2 id="람다를-활용해-중복을-줄여-코드-재사용성을-높이는-방법은">람다를 활용해 중복을 줄여 코드 재사용성을 높이는 방법은?</h2>
<p>함수 타입과 람다식은 재사용하기 좋은 코드를 만들 때 아주 유용합니다. 람다를 사용하면 코드 중복을 간결하고 쉽게 제거할 수 있습니다.</p>
<pre><code class="language-kotlin">data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log = listOf(
    SiteVisit(&quot;/&quot;, 34.0, OS.WINDOWS),
    SiteVisit(&quot;/&quot;, 22.0, OS.MAC),
    SiteVisit(&quot;/login&quot;, 12.0, OS.WINDOWS),
    SiteVisit(&quot;/signup&quot;, 8.0, OS.IOS),
    SiteVisit(&quot;/&quot;, 16.3, OS.ANDROID)
)</code></pre>
<pre><code class="language-kotlin">val averageWindowsDuration = log
    .filter{ it.os == OS.WINDOWS }
    .map(SiteVisit::duration)
    .average()

fun main() {
    println(averageWindowsDuration)
    // 23.0
}</code></pre>
<p>위의 코드는 윈도우 사용자의 평균 방문 시간을 출력하는 코드입니다. 만약 맥 사용자의 평균 방문 시간을 알고 싶다면, 또한 특정 페이지의 평균 방문 시간을 알고 싶다면 위의 함수를 계속 람다와 인자만 바꿔 재사용해야합니다.</p>
<p>고차함수를 사용하면 이런 문제를 해결할 수 있습니다.</p>
<pre><code class="language-kotlin">fun List&lt;SiteVisit&gt;.averageDurationFor(predicate: (SiteVisit) -&gt; Boolean) =
    filter(predicate).map(SiteVisit::duration).average()

fun main() {
    println(
        log.averageDurationFor {
            it.os in setOf(OS.ANDROID, OS.IOS)
        }
    )
    // 12.15
    println(
        log.averageDurationFor {
            it.os == OS.IOS &amp;&amp; it.path == &quot;/signup&quot;
        }
    )
    // 8.0
}</code></pre>
<hr>
<h1 id="인라인-함수를-사용해-람다의-부가-비용-없애기">인라인 함수를 사용해 람다의 부가 비용 없애기</h1>
<p>어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인됩니다. 이는 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트코드로 컴파일 한다는 의미입니다.</p>
<pre><code class="language-kotlin">import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock

inline fun &lt;T&gt; synchronized(lock: Lock, action: () -&gt; T): T {
    lock.lock()
    try {
        return action()
    }
    finally {
        lock.unlock()
    }
}

fun foo(l: Lock) {
    println(&quot;Before sync&quot;)
    synchronized(1) {
        println(&quot;Action&quot;)
    }
    println(&quot;After sync&quot;)
}</code></pre>
<p>위는 synchronized함수를 inline으로 선언하여 호출한 코드입니다. 이 함수를 호출하면 컴파일할 때는 아래와 같이 실행됩니다.</p>
<pre><code class="language-kotlin">fun __foo__(l: Lock) {
    println(&quot;Before sync&quot;)
    l.lock()
    try {
        println(&quot;Action&quot;)
    }    finally {
        l.unlock()
    }
    println(&quot;After sync&quot;)
}</code></pre>
<p>synchronized 함수와 람다 코드의 본문이 인라이닝되어 foo 함수를 변경한 후 실행합니다. </p>
<pre><code class="language-kotlin">class LockOwner(val lock: Lock) {
    fun runUnderLock(body: () -&gt; Unit) {
        synchronized(lock, body)
    }
}</code></pre>
<p>또한 인라인 함수를 호출하면서 람다를 넘기는 대신에 함수 타입의 변수를 넘길 수도 있습니다. 하지만 이런 경우 변수에 저장된 람다의 코드를 알 수 없습니다. 그렇기 때문에 람다 본문은 인라이닝되지 않습니다.</p>
<pre><code class="language-kotlin">class LockOwner(val lock: Lock) {
    fun __runUnderLock__(body: () -&gt; Unit) {
        lock.lock()
        try {
            body()
        }
        finally {
            lock.unlock()
        }
    }
}</code></pre>
<hr>
<h2 id="인라인-함수의-제약">인라인 함수의 제약</h2>
<p>일반적으로 인라인 함수의 본문에서 람다식을 바로 호출하거나 다른 인라인 함수의 인자로 전달하는 경우에는 그 람다를 인라이닝할 수 있지만, 그런 경우가 아니라면 오류 메시지와 함께 컴파일되지 않습니다.</p>
<p>이런 문제를 해결하기 위해 noinline 변경자를 파라미터 앞에 붙이면 인라이닝을 금지시킬 수 있어 인라이닝이 안되는 파라미터를 구분할 수 있습니다.</p>
<pre><code class="language-kotlin">inline fun foo(inline: () -&gt; Unit, noinline notInlined: () -&gt; Unit) {}</code></pre>
<hr>
<h2 id="컬렉션-연산-인라이닝">컬렉션 연산 인라이닝</h2>
<pre><code class="language-kotlin">data class Person(val name: String, val age: Int)

val people = listOf(Person(&quot;Alice&quot;, 29), Person(&quot;Bob&quot;, 31))

fun main() {
    println(people.filter{ it.age &lt; 30 })
    // [Person(name=Alice, age=29)]
}</code></pre>
<pre><code class="language-kotlin">fun main() {
    val result = mutableListOf&lt;Person&gt;()
    for (person in people) {
        if (person.age &lt; 30) result.add(person)
    }
    println(result)
    // [Person(name=Alice, age=29)]
}</code></pre>
<p>첫 번째 코드는 람다식을 활용한 filter 코드이고, 두 번째 코드는 람다식을 사용하지 않은 filter 구현 코드입니다. 코틀린에서 filter 함수는 인라인 함수이기 때문에 첫 번째 코드에서 함수에 전달된 람다 본문은 인라이닝 됩니다. 그렇기에 filter를 사용한 코드와 사용하지 않은 코드의 바이트코드는 거의 같아 성능에 차이가 거의 없습니다.</p>
<p>하지만 filter와 map을 연쇄해서 사용한다면 이야기가 다릅니다.</p>
<pre><code class="language-kotlin">fun main() {
    prinltn(
        people.filter{ it.age &gt; 30 }
            .map(Person::name)
    )
    // [Bob]
}</code></pre>
<p>이 때는 리스트를 걸러낸 결과를 저장하는 중간리스트를 만들며, filter 함수에서 걸러낸 원소를 중간 리스트에 추가하며, map 함수에서 중간 리스트를 읽어 사용합니다.</p>
<p>이렇게 되면 원소가 많아질수록 부가 비용도 커지게 되는 것입니다. asSequence를 사용하면 부가 비용을 줄일 수 있습니다.</p>
<p>하지만 시퀀스 연산으로 성능을 향상시킬 수 있는 경우는 컬렉션의 크기가 큰 경우 뿐이기 떄문에 모든 컬렉션 연산에 asSequence를 붙이면 안됩니다.</p>
<hr>
<h2 id="언제-함수를-인라인으로-선언할까">언제 함수를 인라인으로 선언할까?</h2>
<p>일반 함수 호출의 경우 JVM이 코드 실행을 분석해 가장 이익이 되는 방햐응로 호출을 인라이닝합니다. 반면 람다를 인자로 받는 함수를 인라이닝할 경우 이익이 더 많습니다. </p>
<p>첫 번째로 함수 호출 비용을 줄일 수 있을 뿐 아니라 람다를 표현하는 클래스와 객체를 만들 필요가 없어지기 때문에 인라이닝을 통해 없앨 수 있는 부가 비용이 상당합니다. 둘째로 JVM이 함수 호출과 람다를 인라이닝해주지 못합니다. 마지막으로 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있습니다. </p>
<p>하지만 inline 변경자를 함수에 붙일 때는 코드 크기에 주의를 기울여야 합니다. 인라이닝하는 함수가 큰 경우는 함수의 모든 호출 부분에 함수의 본문이 들어가 바이트 코드가 전체적으로 아주 커질 수도 있기 때문입니다.</p>
<hr>
<h2 id="자원-관리를-위해-인라인된-람다를-사용하기">자원 관리를 위해 인라인된 람다를 사용하기</h2>
<p>람다로 중복을 없앨 수 없는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리입니다.</p>
<p>자원 관리 패턴을 만들 때 보통 사용하는 방법은 try/finally 문을 사용하여 try 블록을 시작하기 전에 자원을 획득하고, finally 블록에서 자원을 해제하는 것입니다.</p>
<p>코틀린에서 제공하는 자원 관리 함수는 withLock, use, userLines 함수가 있습니다.</p>
<hr>
<h1 id="람다에서-반환하는-방법은">람다에서 반환하는 방법은?</h1>
<p>람다를 사용할 때 람다의 본문 안에서 사용한 return은 다양한 역할로 사용될 수 있습니다.</p>
<hr>
<h2 id="람다를-둘러싼-함수에서-반환하기">람다를 둘러싼 함수에서 반환하기</h2>
<pre><code class="language-kotlin">fun lookForAlice(people: List&lt;Person&gt;) {
    people.forEach {
        if (it.name == &quot;Alice&quot;) {
            println(&quot;Found!&quot;)
            return
        }
    }
    println(&quot;Alice is not found&quot;)
}</code></pre>
<p>람다 안에서 reutnr을 사용하면 람다에서만 반환되는 것이 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환하게 됩니다. 이런 식으로 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 비로컬 return이라고 부릅니다.</p>
<p>이런 경우 return은 반복문을 끝내지 않고 메소드를 반환시킵니다.</p>
<hr>
<h2 id="람다로부터-반환하기">람다로부터 반환하기</h2>
<p>로컬 return을 사용해 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어나갈 수 있습니다. 람다 안에서 로컬 return은 for 루프의 continue와 비슷한 역할을 합니다.</p>
<p>로컬 return과 비로컬 return을 구분하기 위해서는 레이블을 사용해야 합니다. return으로 실행을 끝내고 싶은 람다식 앞에 레이블을 붙이고, return 키워드 뒤에 레이블을 추가하면 됩니다.</p>
<pre><code class="language-kotlin">fun lookForAlice(people: List&lt;Person&gt;) {
    people.forEach label@{
        if (it.name != &quot;Alice&quot;) return@label
        println(&quot;Found!&quot;)
    }
}</code></pre>
<p>이 코드는 return이 실행되지 않은 경우에만 즉, 이름이 Alice인 경우에만 아래의 println문을 실행합니다.</p>
<p>또는 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 됩니다.</p>
<pre><code class="language-kotlin">fun lookForAlice(people: List&lt;Person&gt;) {
    people.forEach {
        if (it.name != &quot;Alice&quot;) return@forEach
        println(&quot;Found!&quot;)
    }
}</code></pre>
<p>람다식에 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없습니다.</p>
<hr>
<h2 id="익명-함수를-활용하기">익명 함수를 활용하기</h2>
<p>익명 함수는 람다식을 작성하는 다른 문법적 형태지만 람다와 익명 함수는 return 식을 쓸 수 있다는 점에서 차이가 있습니다.</p>
<pre><code class="language-kotlin">fun lookForAlice(people: List&lt;Person&gt;) {
    people.forEach(fun(person) {
        if(person.name == &quot;Alice&quot;) return
        println(&quot;${person.name} is not Alice&quot;)
    })
}

fun main() {
    lookForAlice(people)
    // Bob is not Alice
}</code></pre>
<p>익명 함수 안에서 레이블이 붙지 않은 return 식은 익명 함수 자체를 반환시킬 뿐 익명 함수를 둘러싼 다른 함수를 반환시키지 않습니다. 따라서 익명 함수 본문의 return은 익명 함수를 반환시키고, 익명 함수 밖의 다른 함수를 반환시키지 못합니다.</p>
<p>또한 익명 함수도 일반 함수와 같은 반환 타입 지정 규칙을 따르기 때문에 반환 타입을 명시해야 합니다. 하지만 식을 본문으로 하는 익명 함수의 반환 타입은 생략할 수 있습니다.</p>
<pre><code class="language-kotlin">people.filter(fun(person): Boolean {
    return person.age &lt; 30 
}

people.filter(fun(person) = person.age &lt; 30)</code></pre>
<p>익명 함수는 람다 구문으로 쓸 때 레이블을 많이 붙여야 하는 코드를 짧게 쓸 때 도움이 됩니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 고차 함수와 함수의 비용을 줄이기 위한 인라인 함수에 대해 정리해봤습니다.</p>
<p>고차 함수의 개념에 대해서는 이번에 공부하면서 알게 되었지만 우테코 마지막 미션에서 앱을 만드는 과정에서 사용해본 경험이 있습니다.</p>
<p>그 당시에는 &#39;함수에 함수를 넣는 문법이 있구나&#39; 정도로만 알고 따로 공부를 하지 않은 상태로 사용하면서 어떤 때는 함수 자체를 파라미터로 넣고 어떨 때는 함수 이름만 넣는지 헷갈리며 고차 함수 사용할 때 엄청 애먹었던 기억이 있습니다.</p>
<p>이번 장을 공부하며 다음에 고차함수를 사용할 때는 확실히 그런 실수가 줄어들겠다는 생각과 인라인 함수도 활용하며 코드의 성능도 챙겨볼 수 있겠다는 자신감이 생겼습니다.</p>
<p>다음에는 제네릭스에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin in Action 2/e] 9장 연산자 오버로딩과 다른 관례]]></title>
            <link>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</link>
            <guid>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</guid>
            <pubDate>Mon, 09 Feb 2026 02:04:33 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>
<hr>
<h2 id="이항-산술-연산-오버로딩">이항 산술 연산 오버로딩</h2>
<pre><code class="language-kotlin">data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2)
    // Point(x=40, y=60)
}</code></pre>
<p>위 코드는 Point 객체에 대한 plus 함수를 선언한 것입니다. plus 함수 앞의 operator는 연산자를 오버로딩하는 함수 앞에 반드시 붙여야하는 키워드입니다.</p>
<p>operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 알 수 있으며, 실수로 관례에서 사용하는 함수 이름을 사용하는 경우를 막아줄 수 있습니다.</p>
<p>operator 변경자를 추가해 plus 함수를 선언하면 + 기호로 두 객체를 더할 수 있습니다.</p>
<p>또한 확장 함수로 정의할 수도 있습니다.</p>
<p>코틀린에서는 프로그래머가 직접 연산자를 만들어 사용할 수 없고 언어에서 미리 정해둔 연산자만 오버로딩할 수 있으며, 관례에 따르기 위해 클래스에서 정의해야 하는 이름이 연산자별로 정해져 있습니다. </p>
<p>직접 함수를 구현하더라도 연산자 우선순위는 표준 숫자 타입에 대한 연산자 우선 순위와 같습니다.</p>
<table>
<thead>
<tr>
<th>식</th>
<th>함수 이름</th>
</tr>
</thead>
<tbody><tr>
<td>a * b</td>
<td>times</td>
</tr>
<tr>
<td>a / b</td>
<td>div</td>
</tr>
<tr>
<td>a % b</td>
<td>mod</td>
</tr>
<tr>
<td>a + b</td>
<td>plus</td>
</tr>
<tr>
<td>a - b</td>
<td>minus</td>
</tr>
</tbody></table>
<hr>
<h2 id="복합-대입-연산자-오버로딩">복합 대입 연산자 오버로딩</h2>
<p>plus와 같은 연산자를 오버로딩하면 코틀린은 + 연산자뿐 아니라 그와 관련 있는 연산자인 +=도 자동으로 함께 지원합니다. +=, -= 등의 연산자를 복합 대입 연산자라고 부릅니다.</p>
<pre><code class="language-kotlin">fun main() {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point)
    // Point(x=4, y=6)
}</code></pre>
<p>복합 대입 연산자는 변수가 변경 가능한 경우(var)에만 사용할 수 있습니다. 하지만 변경 가능한 컬렉션의 경우는 참조를 바꾸기보다 원래 내부 상태를 변경하는 것이기 때문에 val로 선언해도 괜찮습니다.</p>
<pre><code class="language-kotlin">fun main() {
    val numbers = mutableListOf&lt;Int&gt;()
    numbers += 42
    println(numbers[0])
    // 42
}</code></pre>
<p>이 때 += 연산자는 plusAssign 함수를 사용합니다.</p>
<p>이론적으로 += 연산자는 plus와 plusAssign을 모두 컴파일할 수 있습니다. 하지만 어떤 클래스가 이 두 함수를 모두 정의하고 둘 다 +=에 사용 가능한 경우 오류가 발생합니다.</p>
<p>이를 해결하기 위한 방법은 일반 함수 호출을 사용하는 것입니다. 하지만 제일 좋은 방법은 plus와 plusAssign 연산을 동시에 정의하지 않는 것입니다.</p>
<p>클래스가 변경 불가능하다면 plus와 같이 새로운 값을 반환하는 연산만을 추가하고, 변경이 가능한 클래스를 설계한다면 plusAssign을 추가하는 등 한 가지만 정의하는 것이 좋습니다.</p>
<hr>
<h2 id="단항-연산자-오버로딩">단항 연산자 오버로딩</h2>
<p>-a와 같은 단항 연산자 오버로딩도 이항 연산자와 마찬가지로 미리 정해진 이름의 함수를 선언하면서 operator로 표시하면 됩니다.</p>
<pre><code class="language-kotlin">operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

fun main() {
    val p = Point(10, 20)
    println(-p)
    // Point(x=-10, y=-20)
}</code></pre>
<p>단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 가지지 않습니다.</p>
<blockquote>
<p>오버로딩할 수 있는 단항 산술 연산자</p>
</blockquote>
<table>
<thead>
<tr>
<th>식</th>
<th>함수 이름</th>
</tr>
</thead>
<tbody><tr>
<td>+a</td>
<td>unaryPlus</td>
</tr>
<tr>
<td>-a</td>
<td>unaryMinus</td>
</tr>
<tr>
<td>!a</td>
<td>not</td>
</tr>
<tr>
<td>++a, a++</td>
<td>inc</td>
</tr>
<tr>
<td>--a, a--</td>
<td>dec</td>
</tr>
</tbody></table>
<hr>
<h1 id="비교-연산자를-오버로딩해서-객체-관계를-검사하는-방법은">비교 연산자를 오버로딩해서 객체 관계를 검사하는 방법은?</h1>
<p>산술 연산자와 마찬가지로 모든 객체에 대해 비교 연산을 수행할 수 있습니다. 또한 eqals나 compareTo를 호출해야 하는 자바와 달리 코틀린에서는 == 비교 연산자를 직접 사용할 수 있어 자바보다 코드가 더 간결합니다. </p>
<hr>
<h2 id="동등성-연산자">동등성 연산자</h2>
<p>동등성 연산자는 == 연산자를 호출하여 사용할 수 있습니다. == 연산자 호출은 컴파일될 때 equals 메소드를 호출로 됩니다. != 연산자를 사용하는 식도 eqauls 호출로 컴파일됩니다. ==와 != 는 내부에서 인자가 null인지 검사하므로 널이 될 수 있는 값에도 적용할 수 있습니다.</p>
<p>equals 메소드를 구현하면 다음과 같습니다.</p>
<pre><code class="language-kotlin">class Point(val x: Int, val y: Int) {
    override fun equals(obj: Any?): Boolean {
        if (obj === this) return true
        if (obj !is Point) return false
        return obj.x == x &amp;&amp; obj.y == y
    }
}

fun main() {
    println(Point(10, 20) == Point(10, 20))
    // true
    println(Point(10, 20) != Point(5, 5))
    // true
    println(null == Point(1, 2))
    // false
}</code></pre>
<p>앞에서 산술 연산자를 구현할 때는 operator 키워드를 사용했지만 equals는 override 키워드를 사용했습니다. 이렇게 정의한 이유는 Any의 equals에는 operator가 붙어있어 이 메소드를 오버라이드하는 경우 operator를 붙이지 않아도 자동으로 상위 클래스의 operator가 적용되기 때문입니다.</p>
<hr>
<h2 id="순서-연산자">순서 연산자</h2>
<p>자바에서 정렬이나 최댓값, 최솟값 등 값을 비교해야 하는 알고리즘을 사용할 때는 Comparable 인터페이스를 구현해야 합니다. Comparable에 들어있는 compareTo 메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내 주며, 코틀린도 똑같은 Compareable 인터페이스를 지원합니다.</p>
<p>비교 연산자를 사용할 때는 Comparable 인터페이스에 있는 compareTo 메소드를 호출하는 관례를 제공합니다.</p>
<pre><code class="language-kotlin">class Person(val firstName: String, val lastName: String): Comparable&lt;Person&gt; }
    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other, Person::lastName, Person::firstName)
    }
}

fun main() {
    val p1 = Person(&quot;Alice&quot;, &quot;Smith&quot;)
    val p2 = Person(&quot;Bob&quot;, &quot;Johnson&quot;)
    println(p1 &lt; p2)
    // false
}</code></pre>
<p>compareTo를 구현할 때도 equals와 마찬가지로 Comparable의 compareTo에 operator 변경자가 붙어있기 때문에 함수 앞에 operator를 붙일 필요가 없습니다.</p>
<p>위의 코드에서 두 객체를 비교할 때, 두 값이 같으면 0을 반환하고 아니라면 0이 아닌 숫자를 반환합니다.</p>
<hr>
<h1 id="컬렉션과-범위에-대해-사용할-수-있는-관례는">컬렉션과 범위에 대해 사용할 수 있는 관례는?</h1>
<p>a[b]라는 식처럼 컬렉션에서 인덱스를 사용해 원소를 설정하거나 가져올 때, in 연산자를 사용해 원소가 컬렉션 범위에 속하는지 검사하거나 원소를 이터레이션할 때 모두 관례를 사용합니다.</p>
<h2 id="인덱스로-원소-접근">인덱스로 원소 접근</h2>
<p>코틀린에서 맵에 접근할 때 대괄호를 사용합니다. 이 때 인덱스 접근 연산자를 사용해 원소를 읽는 연산은 get 연산자 메소드로, 원소를 쓰는 연산은 set 연산자 메소드로 변환됩니다.</p>
<pre><code class="language-kotlin">operator fun Pointer.get(index: Int): Int {
    return when(index) {
        0 -&gt; x
        1 -&gt; y
        else -&gt; throw IndexOutOfBoundsException(&quot;Invalid coordinate $index&quot;)
    }
}

fun main() {
    val p = Point(10, 20)
    println(p[1])
    // 20
}</code></pre>
<p>get 관례를 구현할 때도 get이라는 메소드를 만들고 operator 변경자를 붙이면 됩니다. x[a, b] 처럼 대괄호를 사용한 접근은 get 함수 호출로 변환됩니다.</p>
<pre><code class="language-kotlin">data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -&gt; x = value
        1 -&gt; y = value
        else -&gt; throw IndexOutOfBoundsException(&quot;Invalid coordinate $index&quot;)
    }
}

fun main() {
    val p = MutablePoint(10, 20)
    p[1] = 42
    println(p)
    // MutablePoint(x=10, y=42)
}</code></pre>
<p>set도 get과 동일하게 선언할 수 있습니다. x[a, b] = c처럼 대괄호를 사용한 대입문은 set 함수로 컴파일됩니다.</p>
<hr>
<h2 id="어떤-객체가-컬렉션에-들어있는지-검사">어떤 객체가 컬렉션에 들어있는지 검사</h2>
<p>객체가 컬렉션에 들어있는지 검사하는 연산자로는 in 연산자가 있습니다. in 연산자에 대응하는 함수는 contains 입니다.</p>
<pre><code class="language-kotlin">data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x..&lt;lowerRight.x &amp;&amp;
     p.y in upperLeft.y..&lt;lowerRight.y
}

fun main() {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    // true
    println(Point(5, 5) in rect)
    // false
}</code></pre>
<p>이는 어떤 점이 사각형 영역에 들어가는지 판단하는 코드입니다. 이 때 in을 사용해 Point 객체가 Rectangle 범위에 들어가는지 contains 함수를 호출하여 판단하게 됩니다.</p>
<p>이 코드에서 범위에 사용되는 ..&lt; 연산자는 열린 범위로 끝 값을 포함하지 않는 범위를 의미합니다.</p>
<hr>
<h2 id="객체로부터-범위-만들기">객체로부터 범위 만들기</h2>
<p>코틀린에서 범위를 만들 때는 .. 연산자를 사용합니다. .. 연산자는 rangeTo 함수를 호출합니다. rangeTo 함수는 범위를 반환하며 어떤 원소가 그 범위 안에 들어있는지 검사할 수 있게 해줍니다.</p>
<pre><code class="language-kotlin">import java.time.LocalDate

fun main() {
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10)
    println(now.plusWeeks(1) in vacation)
    // true
}</code></pre>
<p>rangeTo 연산자는 다른 산술 연산자보다 우선순위가 낮기 때문에 혼동을 피하기 위해 괄호로 인자를 감싸주는 것이 좋습니다.</p>
<p>rangeTo 연산자와 비슷하게 rangeUntil 연산자는 열린 범위를 반듭니다.</p>
<hr>
<h2 id="자신의-타입에-대해-루프-수행">자신의 타입에 대해 루프 수행</h2>
<p>코틀린에서 for 루프는 범위 검사와 똑같이 in 연산자를 사용하고, list.iterator()를 호출해서 이터레이터를 얻은 후, hasNext와 next 호출을 반복하는 식으로 동작합니다.</p>
<pre><code class="language-kotlin">operator fun CharSequence.iterator(): CharIterator

fun main() {
    for (c in &quot;abc&quot;) { }
}</code></pre>
<p>또한 클래스 안에 직접 iterator 메소드를 구현할 수 있습니다. 직접 구현할 때는 인터페이스가 요구하는 대로 hasNext와 next함수를 구현해야 합니다.</p>
<pre><code class="language-kotlin">import java.time.LocalDate

operator fun ClosedRange&lt;LocalDate&gt;.iterator(): Iterator&lt;LocalDate&gt; = 
    object: Iterator&lt;LocalDate&gt; {
        var current = start
        override fun hasNext() = 
            current &lt;= endInclusive
        override fun next(): LocalDate {
            val thisDate = current
            current = current.plusDays(1)
            return thisDate
        }
}

fun main() {
    val newYear = LocalDate.ofYearDay(2042, 1)
    val daysOff = current.plusDays(1)
    for (dayOff in daysOff) { println(dayOff) }
    // 2041-12-31
    // 2042-01-01
}</code></pre>
<hr>
<h1 id="구조-분해-선언">구조 분해 선언</h1>
<p>구조 분해 선언은 복합적인 값을 분해해서 별도의 여러 지역 변수를 한꺼번에 초기화할 수 있도록 도와줍니다.</p>
<pre><code class="language-kotlin">fun main() {
    val p = Point(10, 20)
    val (x, y) = p
    println(x)
}</code></pre>
<p>구조 분해 선언은 componentN이라는 함수를 호출합니다. 여기서 N은 구조 분해 선언에 있는 변수 위치에 따라 붙는 번호입니다. val (x, y)라면 초기화할 때 x는 component1()로, y는 component2()로 초기화됩니다.</p>
<pre><code class="language-kotlin">class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}</code></pre>
<hr>
<h2 id="루프에-구조-분해-선언-활용하기">루프에 구조 분해 선언 활용하기</h2>
<p>변수 선언 뿐만 아니라 루프 안에서도 구조 분해 선언을 사용할 수 있습니다. 특히 맵의 원소에 대한 이터레이션할 때 유용합니다.</p>
<pre><code class="language-kotlin">fun printEntries(map: Map&lt;String, String&gt;) {
    for ((key, value) in map) {
        println(&quot;$key -&gt; $value&quot;)
    }
}

fun main() {
    val map = mapOf(&quot;Oracle&quot; to &quot;Java&quot;, &quot;JetBrains&quot; to &quot;Kotlin&quot;)
    printEntries(map)
    // Oracle -&gt; Java
    // JetBrains -&gt; Kotlin
}</code></pre>
<p>위 코드는 객체를 이터레이션하는 관례와 구조 분해 선언 관례를 활용한 코드입니다. 맵 항목에 대한 이터레이터를 반환하고, component1과 component2를 제공하여 key, value를 초기화합니다.</p>
<hr>
<h2 id="_-문자를-사용하여-구조-분해-값-무시하기">_ 문자를 사용하여 구조 분해 값 무시하기</h2>
<p>구조 분해 선언을 통해 여러 변수를 한 번에 초기화하는 것은 분명 좋지만 변수 중 일부가 필요 없을 경우도 있습니다. 예를 들면 아래의 코드에서 Person 클래스에서 firstName과 age만 필요로 한다고 하면 나머지 파라미터는 필요가 없는 것입니다.</p>
<pre><code class="language-kotlin">data class Person(
    val firstName: String,
    val lastName: String,
    val age: Int,
    val city: String,
}

fun introducePerson(p: Person) {
    val (firstName, lastName, age, city) = p
    println(&quot;This is $firstName, aged $age.&quot;)
}</code></pre>
<p>이런 경우에는 쓰이지 않는 이름들이 코드를 지저분하게 하고 있습니다.</p>
<p>구조 분해 선언의 경우 맨 뒤의 구조 분해 선언은 생략할 수 있습니다. 위 코드의 경우에는 city가 되겠습니다. 하지만 lastName 선언을 없앨 때는 다른 방법을 사용해야 합니다.</p>
<p>코틀린에서 사용하지 않는 구조 분해 선언에 대해서는 _ 문자를 사용하면 됩니다.</p>
<p>필요 없는 구조 분해 선언을 없앤 코드는 아래와 같습니다.</p>
<pre><code class="language-kotlin">fun introducePerson(p: Person) {
    val (firstName, _, age) = p
    println(&quot;This is $firstName, aged $age.&quot;)
}</code></pre>
<p>구조 분해 선언 구현에서 주의할 점은 인자의 위치를 가장 중요하게 봐야합니다. 구조 분해의 결과는 대입될 변수의 이름보다 componentN 함수에 의해 이뤄지기 때문에 순서가 가장 중요합니다.</p>
<hr>
<h1 id="프로퍼티-접근자-로직을-재활용하는-방법은">프로퍼티 접근자 로직을 재활용하는 방법은?</h1>
<p>위임 프로퍼티를 사용하면 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 접근자 로직을 매번 재구현할 필요 없이 쉽게 구현할 수 있습니다.</p>
<p>이런 특성의 기반에는 위임이 있습니다. 위임이란 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하도록 맡기는 디자인 패턴을 말합니다. 이 때 작업을 처리하는 도우미 객체를 위임 객체라고 부릅니다.</p>
<p>위임 프로퍼티의 문법은 파라미터의 타입 뒤에 by 와 위임에 사용할 클래스의 인스턴스를 명시하면 됩니다.</p>
<pre><code class="language-kotlin">class Foo {
    var p: Type by Delegate()
}</code></pre>
<p>이 때 Delegate 인스턴스가 위임 객체가 되는 것입니다.</p>
<p>by 키워드를 사용할 때는 위임 관례에 따라 getValue와 setValue 메소드를 제공해야합니다. 즉, Delegete 클래스를 위임 클래스로 사용하기 위해서는 아래와 같이 구현해야 합니다</p>
<pre><code class="language-kotlin">class Delegate {
    operator fun getValue(/* ... */) { /* ... */ }
    operator fun setValue(/* ... */, value: Type) { /* ... */ }
    operator fun provideDelegate(/* ... */): Delegate { /* ... */ }
}

class Foo {
    var p: Type by Delegate()
}

fun main() {
    val foo = Foo()
    val oldValue = foo.p
    foo.p = newValue
}</code></pre>
<p>getValue는 게터를 구현하는 로직을 담고, setValue는 세터를 구현하는 로직을 담습니다. provideDelegete는 위임 객체를 생성하거나 제공하는 로직을 담습니다.</p>
<p>Foo 클래스의 p에서 by Delegete()를 사용해 프로퍼티와 위임 객체를 연결한 것입니다.</p>
<p>foo.p라는 프로퍼티를 사용하면 delegete.getValue()를 호출하며, 프로퍼티 값을 변경할 때는 delegete.setValue()를 호출합니다.</p>
<hr>
<h2 id="위임-프로퍼티를-활용한-지연-초기화-방법은">위임 프로퍼티를 활용한 지연 초기화 방법은?</h2>
<p>지연 초기화란 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제그 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴입니다. 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">class Email { /* ... */ }
fun loadEmails(person: Person): List&lt;Email&gt; {
    println(&quot;${person.name}의 이메일을 가져옴&quot;)
    return listOf(/* ... */)
}

class Person(val name: String) {
    private var _emails: List&lt;Email&gt;? = null

    val emails: List&lt;Email&gt;
        get() {
            if (_emails == null) {
                _emails = loadEmails(this)
            }
            return _emails!!
        }
}

fun main() {
    val p = Person(&quot;Alice&quot;)
    p.emails
    // Load emails for Alice
    p.emails
}</code></pre>
<p>위 코드에서 사용된 변수명 앞에 _를 붙이는 프로퍼티를 사용하는 기법을 뒷받침하는 프로퍼티라고 합니다. 이 프로퍼티는 값을 저장하고, 다른 프로퍼티는 값을 읽는 연산을 제공합니다. 뒷받침하는 프로퍼티를 사용할 때는 두 프로퍼티의 타입이 다르기 때문에 둘 다 사용해야 합니다.</p>
<p>하지만 위 경우는 지연 초기화를 해야하는 프로퍼티가 많아질 때 제대로 동작한다고 말할 수 없습니다. 구현이 스레드 안전하지 않기 때문입니다. 최선의 경우 자원을 낭비하게 되지만 최악의 경우 애플리케이션 상태의 일관성이 사라질 수 있습니다.</p>
<p>이 때 위임 프로퍼티를 사용하면 코드를 더 간단하게 할 수 있습니다. 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해줍니다.</p>
<p>위임 객체를 반환하는 표준 라이브러리 함수는 lazy입니다.</p>
<pre><code class="language-kotlin">class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}</code></pre>
<p>lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환합니다. 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있습니다.</p>
<hr>
<h2 id="위임-프로퍼티-구현">위임 프로퍼티 구현</h2>
<pre><code class="language-kotlin">import kotlin.reflect.KProperty

class ObservableProperty(val propValue: Int, val obserbable: Observable) {
    operator fun getValue(thisRef: Any?, prop: KProperty&lt;*&gt;): Int = propValue
    operator fun setValue(thisRef: Any?, prop: KProperty&lt;*&gt;, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        observable.notifyObservers(prop.name, oldValue, newValue)
    }
}</code></pre>
<p>위임 프로퍼티 구현은 위와 같습니다. 코틀린 관례에 사용하는 다른 함수와 마찬가지로 함수명 앞에 operator 변경자를 붙입니다.</p>
<p>두 함수가 받는 thisRef 파라미터는 바로 설정하거나 읽을 프로퍼티가 들어있는 인스턴스이고, prop은 프로퍼티를 표현하는 객체입니다.</p>
<p>코틀린은 KProperty 타입 객체를 사용해 프로퍼티를 표현하는데 KProperty.name을 통해 메소드가 처리할 프로퍼티의 이름을 알 수 있습니다.</p>
<pre><code class="language-kotlin">class Person(val name: String, age: Int, salary: Int): Observable() {
    var age by ObservableProperty(age, this)
    var salary by ObservableProperty(salary, this)
}</code></pre>
<p>by 키워드를 사용해 위임 객체를 지정할 수 있습니다.</p>
<p>위처럼 위임 프로퍼티를 직접 구현하는 방식도 있지만 코틀린의 표준 라이브러리인 Delegates를 사용하면 쉽게 관찰 가능한 프로퍼티 로직을 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">import kotlin.properties.Delegates

class Person(val name: String, age: Int, salary: Int): Observable() {
    private val onChange = { property: KProperty&lt;*&gt;, oldValue: Any?,
        newValue: Any? -&gt;
            notifyObservers(property.name, oldValue, newValue)
    }
    var age by Delegates.observable(age, onChange)
    var salary by Delegates.observable(salary, onChange)
}</code></pre>
<hr>
<h2 id="맵에-위임해서-동적으로-프로퍼티-접근하기">맵에 위임해서 동적으로 프로퍼티 접근하기</h2>
<pre><code class="language-kotlin">class Person {
    private val _attributes = mutableMapOf&lt;String, String&gt;()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    var name: String
        get() = _attributes[&quot;name&quot;]!!
        set(value) {
            _attributes[&quot;name&quot;] = value
        }
}

fun main() {
    val p = Person()
    val data = mapOf(&quot;name&quot; to &quot;Seb&quot;, &quot;company&quot; to &quot;JetBrains&quot;)
    for ((attrName, value) in data)
        p.setAttribute(attrName, value)
    println(p.name)
    // Seb
    p.name = &quot;Sebastian&quot;
    println(p.name)
    // Sebastian
}</code></pre>
<p>Person 클래스를 위임 프로퍼티를 활용하여 리펙토링하면 다음과 같습니다.</p>
<pre><code class="language-kotlin">class Person {
    private val _attributes = mutableMapOf&lt;String, String&gt;()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    var name: String by _attributes
}</code></pre>
<p>이런 코드가 작동하는 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장함수를 제공하기 때문입니다.</p>
<p>p.name은 _attributes.getValue(p, prop)을 호출하고, _attributes.getValue(p, prop)는 다시 _attributes[prop.name]을 통해 구현됩니다.</p>
<p>이처럼 맵을 위임 객체로 사용하는 위임 프로퍼티를 통해 다양한 속성을 제공하는 객체를 유연하게 다룰 수도 있습니다.</p>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>이번 글에서는 관례, 연산자 오버로딩, 위임 프로퍼티 등에 대해 정리해봤습니다.</p>
<p>확실히 이번 장부터 코틀린 코드를 활용한다는 느낌을 강하게 받았습니다. 기존에 코드를 작성할 때는 숫자 뿐만 아니라 문자열도 + 기호를 사용해 더하는 연산을 했었는데, 이 안에도 코틀린의 특성이 담겨있다는 것을 처음 알게 되었습니다. 또한 대괄호를 사용해 인덱스로 접근하는 식이라던지, 연산자를 활용하는 것은 언어에서 제공하는 기초 문법으로만 알고 있었는데 사용자가 직접 오버라이딩해서 커스텀 객체에 대해서도 연산을 활용할 수 있다는 사실을 알게 되었습니다.</p>
<p>또한 by 키워드를 활용해 위임 프로퍼티를 사용하는 것은 반복되는 코드를 줄이는 등 보일러플레이트를 해결할 수 있다는 사실을 알게 되었습니다.</p>
<p>다음에는 고차 함수를 만들고 사용하는 방법에 대한 정리글로 돌아오겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록] 2026.02.02 ~ 2026.02.08]]></title>
            <link>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.02-2026.02.08</link>
            <guid>https://velog.io/@noeyh_0j/%ED%9A%8C%EA%B3%A0%EB%A1%9D-2026.02.02-2026.02.08</guid>
            <pubDate>Sun, 08 Feb 2026 06:18:37 GMT</pubDate>
            <description><![CDATA[<h1 id="안녕하세요">안녕하세요!</h1>
<p>한 주의 회고와 함께 돌아온 개발자 꿈나무 김조현입니다.</p>
<p>학습에 대한 내용은 따로 글로 정리하기 때문에 회고록에서는 저의 공부 습관이나 학습하면서 느낀 점 등을 위주로 작성하면 좋을 것 같다고 생각했습니다.</p>
<p>크게 이번 주 계획에 대한 설명과 KPT, 학습 정리 글, 마무리로 회고를 하겠습니다.</p>
<hr>
<h1 id="이번주의-계획은">이번주의 계획은?</h1>
<p><img src="https://velog.velcdn.com/images/noeyh_0j/post/e35827f0-ba29-4c6e-b127-b82d50a031b7/image.png" alt=""></p>
<p>이번 주는 특히 키워드 부분에서 많은 양의 학습 분량을 가지고 있습니다. </p>
<p>계획을 짤 때 크게 2주씩 나눠서, 첫 2주차는 기본적인 코틀린 문법의 사용법과 개념에 대해 익히고 남은 2주는 문법의 활용 방법에 대해 공부해야겠다고 큰 틀로 정해놨었습니다.</p>
<p>저의 학습 목표를 달성하기 위해 이번 주차에 학습량이 많이 생겼던것 같습니다.</p>
<p>이번 주는 특별한 일정이 없었기 때문에 공부에만 집중할 수 있어 다행히 순조롭게 계획에 맞게 학습 목표를 달성했다고 생각합니다.</p>
<h2 id="keep">Keep</h2>
<ul>
<li>밀리지 않고 계획에 맞게 학습 목표를 달성한 점</li>
<li>뽀모도로 타이머를 활용해 학습한 점</li>
<li>예시를 실행해보며 결과를 눈으로 직접 확인하며 학습한 점</li>
</ul>
<h2 id="problem">Problem</h2>
<ul>
<li>학습량에 따라가기 벅차 &#39;왜?&#39;라는 질문이 지난 주에 줄어들었다고 느낀 점</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-4%EC%9E%A5-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B0%9D%EC%B2%B4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4">4장 클래스, 객체, 인터페이스</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-5%EC%9E%A5-%EB%9E%8C%EB%8B%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D">5장 람다를 활용한 프로그래밍</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-6%EC%9E%A5-%EC%BB%AC%EB%A0%89%EC%85%98%EA%B3%BC-%EC%8B%9C%ED%80%80%EC%8A%A4">6장 컬렉션과 시퀀스</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-7%EC%9E%A5-%EB%84%90%EC%9D%B4-%EB%90%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B0%92">7장 널이 될 수 있는 값</a></li>
<li><a href="https://velog.io/@noeyh_0j/Kotlin-in-Action-2e-8%EC%9E%A5-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%B0%B0%EC%97%B4">8장 기본 타입, 컬렉션, 배열</a></li>
</ul>
<hr>
<h1 id="마무리입니다">마무리입니다!</h1>
<p>저번 주가 본격적으로 나아가기 위한 준비 운동 같은 일주일이였다면, 이번 주는 본격적으로 시작한 느낌이 들었던 일주일이었습니다. 특별한 일정이 없어 공부에만 몰입하다보니 일주일이 금방 간 것 같습니다.</p>
<p>공부할 부분이 많았던 것도 힘들었었지만, 이보다 더 많은 고민을 하게 했던 것은 닉네임 짓기였습니다,,,</p>
<p>이번 기회로 이름 짓는데 정말 소질 없다는 것을 다시 한 번 느꼈습니다.. 내일까지 제출해야 하는데 회고를 쓰고 있는 지금까지도 닉네임을 정하지 못해 발을 동동 구르고 있습니다 ㅎㅎ.. 제가 약 1년동안 불리게 될 이름이라고 생각하니 쉽게 정하지 못하게 되는 것 같습니다.</p>
<p>또한 이번 주에는 지난 주에 느꼈던 공부의 집중에 대한 문제점을 다른 분의 회고를 읽어보며 뽀모도로 타이머를 활용하는 방식으로 문제 해결을 시도해봤습니다. 이 방법을 시도해보며 확실히 집중하는 시간이 늘어 지난주보다 집중 시간이 늘어났다는 것이 느껴졌습니다.</p>
<p>제 회고를 잘 쓰고 공유하는 것도 중요하지만, 이런 것처럼 다른 분들의 회고를 읽어보며 제 학습에 대한 아이디어를 얻는 것도 무척 중요하다고 느낄 수 있던 경험을 했습니다.</p>
<p>회고를 쓰면서 좋다고 생각되는 부분은 저를 돌아볼 시간을 가지는 것입니다. 지난 주의 회고를 다시 읽어보고 이번주의 회고를 쓰면서 &quot;이렇게 자신을 돌아본 적이 있었나?&quot;라는 생각이 들었습니다. 이전까지는 이렇게 긴 시간 저를 돌아보며 습관이나 문제점을 고쳐보려 한 적이 없었습니다. </p>
<p>회고라는 방식으로 일주일 동안 자신이 한 일에 대해 돌아보는 것이 목표에 대해 길을 잃지 않도록 도와준다고 느껴졌습니다.</p>
<p>지난주보다 나아지기 위해 문제를 해결하려 노력했지만 그럼에도 다른 아쉬운 부분이 계속 생기게 되는 것 같습니다. 그래도 계속 아쉬운 부분을 고쳐보며 더 성장할 수 있도록 나아가겠습니다.</p>
<p>읽어주셔서 감사합니다!🙂‍↕️</p>
]]></description>
        </item>
    </channel>
</rss>