<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>first_log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 29 Jul 2026 01:02:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>first_log</title>
            <url>https://velog.velcdn.com/images/first_woosun/profile/c95e8025-090c-4187-9e63-35fc5643139f/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. first_log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/first_woosun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[앉아서 고민하는 대신 직접 확인해 본 2주]]></title>
            <link>https://velog.io/@first_woosun/%EC%95%89%EC%95%84%EC%84%9C-%EA%B3%A0%EB%AF%BC%ED%95%98%EB%8A%94-%EB%8C%80%EC%8B%A0-%EC%A7%81%EC%A0%91-%ED%99%95%EC%9D%B8%ED%95%B4-%EB%B3%B8-2%EC%A3%BC</link>
            <guid>https://velog.io/@first_woosun/%EC%95%89%EC%95%84%EC%84%9C-%EA%B3%A0%EB%AF%BC%ED%95%98%EB%8A%94-%EB%8C%80%EC%8B%A0-%EC%A7%81%EC%A0%91-%ED%99%95%EC%9D%B8%ED%95%B4-%EB%B3%B8-2%EC%A3%BC</guid>
            <pubDate>Wed, 29 Jul 2026 01:02:07 GMT</pubDate>
            <description><![CDATA[<p>우테코 8기 과정이 시작되고 어느덧 5개월이 흘렀다.</p>
<p>그동안 레벨 1과 레벨 2, 두 번의 방학을 지나 대망의 레벨 3가 시작됐다. 우테코의 레벨 3에서는 팀 프로젝트를 진행한다.</p>
<p>다만 이번 8기의 프로젝트 진행 방식은 이전 기수와 달랐다. 이전까지는 팀을 먼저 구성한 뒤 팀에서 진행할 프로젝트의 주제를 정했다면, 이번에는 정반대의 순서로 진행됐다.</p>
<p>모든 크루가 방학 동안 일상에서 마주한 크고 작은 불편함을 수집한다. 이후 2주 동안 그 불편함을 어떻게 해결할 수 있을지, 다른 사람도 같은 불편함을 겪고 있는지, 생각한 해결 방법을 서비스로 만들었을 때 실제 사용자가 있을지를 검증한다. 팀 빌딩은 그다음인 3주 차에 진행된다.</p>
<p>이번 회고에서는 이 2주 동안 하나의 불편함에서 출발해 서비스를 구상하고 검증하면서 배우고 느낀 점을 이야기해 보려고 한다.</p>
<hr>
<h2 id="1-불편함부터-떠올려-보자">1. 불편함부터 떠올려 보자</h2>
<p>레벨 2 방학 기간에 사전 과제로 ‘불편 일기’를 작성했다.</p>
<p>불편 일기란 일상생활을 하면서 “이건 해결되면 좋겠는데?”라고 느낀 크고 작은 불편함을 기록하는 일기다.</p>
<p>나는 불편 일기를 쓰는 것부터 쉽지 않았다. 인간은 적응의 동물이라고 했던가. 분명 불편하다고 느낀 순간이 있었을 텐데, 막상 글로 적으려니 떠오르는 것이 없었다.</p>
<p>2주간의 방학 중 첫 일주일을 어영부영 보내고, 방학이 거의 끝나갈 무렵이 되어서야 부랴부랴 몰아 쓰기 시작했다. 그렇게 총 10개의 불편함을 적었지만, 그중 프로젝트 주제로 발전시킬 만한 것은 두 개 정도였다.</p>
<p>나는 그중 다음과 같은 불편함을 프로젝트 주제로 선택했다.</p>
<blockquote>
<p>길거리에서 택시를 잡아 타면 목적지를 설명하는 과정이 불편하다.</p>
</blockquote>
<hr>
<h2 id="2-무엇을-해결하려-했는가">2. 무엇을 해결하려 했는가</h2>
<p>나는 택시를 타야 할 일이 생기면 카카오 T를 애용한다.</p>
<p>택시가 정차해 있는 곳까지 직접 찾아갈 필요가 없고, 무엇보다 목적지를 미리 설정할 수 있어 탑승한 뒤 말로 설명하지 않아도 된다는 점이 편하기 때문이다.</p>
<p>반대로 말하면 카카오 T를 사용하지 않고 택시를 타면 목적지를 전달하는 과정이 불편하고 번거로워진다.</p>
<p>최근 시간이 촉박해 택시를 타야 했던 날이 있었다. 그런데 그날따라 카카오 T로 택시가 배차되지 않았다. 마음은 급한데 택시는 잡히지 않아 발만 동동 구르던 중, 마침 눈앞으로 빈 택시가 지나가 오랜만에 직접 택시를 잡아 탔다.</p>
<p>그런데 생각하지 못한 문제를 마주했다.</p>
<p>목적지를 말씀드렸지만 기사님은 그곳이 어디인지 모르셨다. 우아한형제들, 제2판교테크노밸리 등 여러 표현으로 설명해 봤지만 돌아오는 답은 같았다.</p>
<p>결국 기사님이 사용하시던 지도 앱의 음성 검색으로 목적지를 검색했다. 하지만 당시 나는 안경을 쓰고 있지 않아 검색 결과가 잘 보이지 않았다. 화면에 나열된 장소가 내가 가려는 곳이 맞는지도 확신하기 어려웠다.</p>
<p>결국 내 휴대폰으로 정확한 주소를 검색해 기사님께 다시 말씀드려야 했다.</p>
<p>이때 한 가지 생각이 들었다.</p>
<blockquote>
<p>길거리에서 잡은 택시에서도 카카오 T처럼 승객이 설정한 목적지를 기사님께 바로 전달할 수 있다면 편하지 않을까?</p>
</blockquote>
<hr>
<h2 id="3-승객만-편해서는-성립하지-않는-서비스">3. 승객만 편해서는 성립하지 않는 서비스</h2>
<p>2주 동안 아이디어를 발전시키면서 가장 기억에 남은 단어는 ‘양면 서비스’다.</p>
<p>양면 서비스란 서로 다른 두 이용자 집단을 연결해 상호작용을 돕고, 그 과정에서 가치를 만드는 서비스를 말한다.</p>
<p>내가 생각한 해결 방법의 핵심은 승객의 휴대폰에서 설정한 목적지를 택시 기사에게 빠르게 전달하는 것이었다.</p>
<p>본격적으로 서비스를 만들기 전에 같은 불편함을 느끼는 사람이 있는지, 서비스로 제공했을 때 실제로 사용할 의향이 있는지를 알아보기 위해 여러 크루를 인터뷰했다. 인터뷰를 통해 승객 입장에서는 목적지를 말로 전달하는 과정에 분명한 불편함이 있다는 것을 확인했다.</p>
<p>이때까지만 해도 아이디어가 잘 발전하고 있다고 생각했다. 그러다 문득 한 가지 의문이 들었다.</p>
<blockquote>
<p>그런데 이걸 기사님 휴대폰에는 어떻게 설치하지?</p>
</blockquote>
<p>기사님께 서비스를 어떻게 설치하게 만들 수 있을지 아무리 고민해도 답이 나오지 않았다. 결국 무작정 코치룸을 찾아가 도움을 요청했다.</p>
<p>그때 돌아온 답은 생각보다 간단했다.</p>
<blockquote>
<p>그렇다면 택시 기사님을 인터뷰해 봐라.</p>
</blockquote>
<p>내가 생각한 서비스가 동작하려면 승객과 택시 기사 양쪽의 요구가 맞아야 했다. 택시 기사 역시 길거리에서 승객을 태웠을 때 목적지를 설정하는 과정이 불편하다고 느껴야 했다.</p>
<p>기사님이 목적지 설정 과정에 불편함을 느끼지 않는다면 굳이 새로운 앱을 설치할 이유가 없다. 기사님 휴대폰에 서비스가 설치되지 않으면 승객이 아무리 사용하고 싶어도 서비스는 동작할 수 없다.</p>
<p>승객의 수요만 확인해서는 부족했다.</p>
<hr>
<h2 id="4-택시-기사에게-직접-물어보기">4. 택시 기사에게 직접 물어보기</h2>
<p>이번에는 현직 택시 기사를 어떻게 인터뷰할 수 있을지 고민하기 시작했다.</p>
<p>가장 먼저 떠올린 방법은 지역 택시 조합에 연락하는 것이었다. 택시 조합을 찾아가면 현직 택시 기사의 이야기를 들을 수 있을 것 같았다.</p>
<p>성남시 택시 조합에 전화를 걸었고, 여러 연결을 거쳐 성남시 택시 조합장님과 대화할 수 있었다.</p>
<p>조합장님과의 대화를 통해 승객과 기사의 소통 오류로 인한 불편함이 실제로 존재한다는 사실은 확인할 수 있었다. 하지만 동시에 예상하지 못했던 사실도 알게 됐다.</p>
<p>카카오 T의 영향력은 생각보다 훨씬 컸다. 일반적인 거리에서는 직접 택시를 잡는 승객이 많지 않아 내가 생각한 서비스의 효과가 크지 않을 수 있다는 의견을 들었다.</p>
<p>또한 목적지를 설정할 때 소통 오류가 자주 발생하는 대상은 주로 연령대가 높은 승객이라고 했다.</p>
<p>여기서 새로운 고민이 생겼다.</p>
<blockquote>
<p>목적지 전달에 어려움을 겪는 사람이 주로 고령층이라면, IT를 활용한 해결 방법이 과연 적절할까?</p>
</blockquote>
<p>고령층과 소통 오류가 발생하는 이유 중 하나는 주소를 검색하는 것과 같은 기존의 디지털 해결 방법을 사용하기 어렵기 때문이다. 그렇다면 또 다른 디지털 서비스를 제공하는 것이 문제를 제대로 해결할 수 있을지 의문이 들었다.</p>
<p>서비스의 대상 사용자를 좁혀야 할지, 아니면 아예 다른 주제로 넘어가야 할지 많은 고민을 했다.</p>
<p>그러던 중 생각하지 못했던 대상이 떠올랐다. 바로 공항이나 터미널에서 택시를 이용하는 승객이었다.</p>
<p>공항이나 터미널에서는 카카오 T를 호출하는 것보다 택시 승강장에서 대기 중인 택시에 탑승하는 것이 더 빠를 수 있다. 특히 공항에는 외국인 승객도 많으므로 언어 장벽으로 인한 소통 문제가 더욱 분명하게 나타날 것이라고 생각했다.</p>
<p>초기 서비스 대상을 택시 승강장에서 대기하는 기사와 승객으로 한정한다면 카카오 T로 인한 수요 문제를 피하면서 서비스의 대상도 구체화할 수 있을 것 같았다.</p>
<p>하지만 이것 역시 내 머릿속에서 만들어진 가설에 불과했다. 택시 기사에게 실제 수요가 있는지는 여전히 확인하지 못한 상태였다.</p>
<p>그래서 더 고민하는 대신 판교역으로 가서 직접 확인해 보기로 했다.</p>
<hr>
<h2 id="5-판교역-인터뷰-이후">5. 판교역 인터뷰 이후</h2>
<p>우테코 판교 캠퍼스에서 판교역으로 택시를 타고 이동하며 기사님 한 분을 인터뷰했다. 이후 판교역에 정차해 있던 택시 기사 세 분을 추가로 인터뷰했다.</p>
<p>총 네 번의 인터뷰를 통해 다음 내용을 확인하고자 했다.</p>
<ol>
<li>길거리에서 직접 택시를 잡아 타는 승객의 비율은 어느 정도인가?</li>
<li>기사들은 목적지를 찾을 때 음성 검색을 사용하는가?</li>
<li>음성 검색만으로 목적지 설정 문제를 충분히 해결할 수 있는가?</li>
<li>기사들은 승객에게 목적지를 말로 전달받는 과정을 불편하게 느끼는가?</li>
</ol>
<p>표본이 많지는 않았지만, 인터뷰 결과는 분명했다.</p>
<p>내가 생각한 서비스에 대한 택시 기사의 수요는 예상대로 절망적이었다.</p>
<p>호출 앱을 사용하지 않고 직접 택시를 잡는 승객은 하루 전체 승객의 10%도 되지 않는다는 답변을 들었다. 또한 인터뷰한 기사님들은 목적지 전달 과정에서 소통 오류가 발생하더라도 음성 검색으로 충분히 해결하고 있었다.</p>
<p>음성 검색이 잘되지 않더라도 도로명주소를 검색하면 바로 찾을 수 있기 때문에 크게 불편하지 않다고 했다.</p>
<p>무엇보다 인상적이었던 점은 기사님들이 승객에게 목적지를 말로 전달받는 것을 ‘불편함’이 아니라 업무 과정에서 발생하는 ‘당연한 일’로 받아들이고 있다는 것이었다.</p>
<p>내가 구상한 서비스를 짧게 설명했을 때도 반응은 비슷했다.</p>
<p>있으면 편리할 수는 있지만, 굳이 새로운 앱을 설치하면서까지 사용할 것 같지는 않다는 답변을 들었다.</p>
<p>실제 택시 기사들의 이야기를 들은 뒤, 이 아이디어를 우테코 팀 프로젝트로 이어가기는 어렵다는 결론을 내렸다.</p>
<p>우테코에서는 이번 프로젝트를 통해 ‘실제 사용자가 있는 서비스’를 만들어 볼 것을 권장한다. 그런데 승객에게 서비스를 제공하기 위해 반드시 참여해야 하는 택시 기사들이 현재의 방식을 불편하다고 인식하지 않고 있었다.</p>
<p>그렇다면 적어도 지금의 형태로는 해결이 필요한 문제가 아니라고 판단했다.</p>
<p>다만 승객 입장에서는 목적지 전달 과정의 불편함이 존재한다는 것을 확인했다. 팀 프로젝트로 선택할 만큼의 수요를 검증하지는 못했지만, 문제를 기술적으로 풀어 보는 경험 자체는 의미가 있을 것 같아 작은 사이드 프로젝트로 만들어 볼 생각이다.</p>
<hr>
<h2 id="6-앉아서-고민하는-대신-행동하기">6. 앉아서 고민하는 대신 행동하기</h2>
<p>2주 동안 하나의 아이디어를 우테코 프로젝트로 이어갈지 말지를 두고 정말 많은 고민을 했다.</p>
<p>다른 크루들은 하나의 아이디어를 빠르게 검증하고, 결과가 좋든 나쁘든 다음 주제로 넘어가며 여러 가능성을 확인한 것 같았다. 나는 그렇게 하지 못했다는 점에서 아쉬움이 남는다.</p>
<p>하지만 하나의 주제를 붙잡고 최대한 깊게 고민해 본 경험을 후회하지는 않는다.</p>
<p>수요에 대한 확신이 없다는 이유로 곧바로 포기하고 다른 주제로 넘어갔다면, 캠퍼스를 벗어나 외부 사람에게 직접 연락하고 인터뷰하는 경험은 하지 못했을 것이다.</p>
<p>이번 경험을 통해 내 생각에 의심이 생겼을 때는 앉아서 계속 고민하는 것보다 무엇을 확인해야 하는지 정하고 빠르게 행동하는 편이 모호함을 해소하는 데 훨씬 도움이 된다는 것을 체감했다.</p>
<p>물론 두렵지 않았던 것은 아니다.</p>
<p>길거리에서 모르는 사람에게 말을 걸어야 했기 때문에 이상한 사람으로 보이지 않을까 걱정했다. 아무도 인터뷰에 응해 주지 않으면 어떡하나 하는 걱정도 있었다.</p>
<p>하지만 이상한 사람으로 보이면 어떻고, 아무도 인터뷰에 응해 주지 않으면 어떤가.</p>
<p>행동하기 전에 했던 걱정은 대부분 실제로 일어나지도 않은 일이었다. 일어나지 않은 일을 걱정하며 행동을 미루기만 해서는 결국 아무것도 해결되지 않았다.</p>
<p>나태했다면 나태했고, 안일했다면 안일했던 2주였다. 그럼에도 머릿속에서만 맴돌던 가설을 직접 현장에 가져가 확인해 봤다는 점에서 의미 있는 시간이었다.</p>
<p>결국 이번 2주 동안 내가 얻은 것은 성공적인 프로젝트 아이디어가 아니었다.</p>
<p>대신 고민만으로는 알 수 없는 것이 있고, 그것을 알아내는 가장 빠른 방법은 직접 행동하는 것이라는 사실을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter.6 CoroutineContext]]></title>
            <link>https://velog.io/@first_woosun/Chapter.6-CoroutineContext</link>
            <guid>https://velog.io/@first_woosun/Chapter.6-CoroutineContext</guid>
            <pubDate>Wed, 15 Jul 2026 09:53:14 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-kotlin">public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&gt; Unit
): Job

public CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&gt; T
): Deferred&lt;T&gt;</code></pre>
<p>launch 함수와 async 함수는 매개변수로 context, start, block을 가진다.</p>
<p>여기서 context 매개변수를 살펴보자</p>
<p>앞서 다뤘던 내용 중에서 “3-4. CoroutineDispatcher 사용해 코루틴 실행하기” 파트가 있었다. 이 파트에서는 context 자리에 CoroutineDispatcher 객체가 사용되었다.</p>
<p>이 외에도 context 매개변수에 CoroutineName 객체를 사용할 수도 있다.</p>
<p>이 둘이 context 인자로 사용될 수 있는 이유는 이 두 객체가 CoroutineContext 객체의 구성 요소이기 때문이다.</p>
<p>CoroutineContext는 코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스로 CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합해 코루틴의 실행 환경을 설정한다.</p>
<p>즉, CoroutineContext 객체는 코루틴을 실행하고 관리하는데 핵심적인 역할을 하며, 코루틴의 실행과 관련된 모든 설정은 CoroutineContext 객체를 통해 이뤄진다.</p>
<blockquote>
<h3 id="6장에서-다루는-내용">6장에서 다루는 내용</h3>
<ul>
<li>CoroutineContext의 구성 요소</li>
<li>CoroutineContext 구성 방법</li>
<li>CoroutineContext 구성 요소에 접근하기</li>
<li>Coroutinecontext 구성 요소를 제거하는 방법</li>
</ul>
</blockquote>
<hr>
<h1 id="1-coroutinecontext의-구성-요소">1. CoroutineContext의 구성 요소</h1>
<p>CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler의 네 가지 주요한 구성 요소를 가지며, 이들 구성 요소는 다음과 같은 역할을 한다.</p>
<ol>
<li>CoroutineName → 코루틴의 이름을 설정</li>
<li>CoroutineDispatcher → 코루틴을 스레드에 할당해 실행</li>
<li>Job → 코루틴의 추상체로 코루틴을 조작하는데 사용</li>
<li>CoroutineExceptionHandler → 코루틴에서 발생한 예외를 처리</li>
</ol>
<blockquote>
<p>이들 이외에도 다양한 구성요소가 있지만 주로 이 4가지 구성 요소를 사용해 코루틴을 실행시키기 때문에 이들을 중심으로 살펴본다.</p>
</blockquote>
<hr>
<h1 id="2-coroutinecontext-구성하기">2. CoroutineContext 구성하기</h1>
<h2 id="2-1-coroutinecontext가-구성-요소를-관리하는-방법">2-1. CoroutineContext가 구성 요소를 관리하는 방법</h2>
<p>CoroutineContext 객체는 <strong>키-값</strong> 쌍으로 각 구성 요소를 관리한다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/21c40093-7f48-40cd-a9b6-1ae218394ee9/image.jpg" alt=""></p>
<ul>
<li>각 구성 요소는 고유한 키를 가짐</li>
<li>키에 중복된 값은 허용되지 않음</li>
</ul>
<p>CoroutineContext 객체는 각 구성 요소를 한 개씩만 가질 수 있음</p>
<h2 id="2-2-coroutinecontext-구성">2-2. CoroutineContext 구성</h2>
<p>CoroutineContext 객체에 구성 요소를 추가하는 방법에 대해 살펴본다.</p>
<p>CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하지만 키에 값을 직접 대입하는 방법을 사용하지 않는다.</p>
<p>대신 CoroutineContext 객체 간에 더하기 연산자를 통해 CoroutineContext 객체를 구성한다.</p>
<pre><code class="language-kotlin">val coroutineContext: CoroutineContext = 
    newSingleThreadContext(&quot;MyThread1&quot;) + CorotineName(&quot;MyCoroutine&quot;)</code></pre>
<ul>
<li>newSingleThreadContext(”MyThread1”) → CoroutineDispatcher 객체 생성</li>
<li>CoroutineName(”MyCoroutine”) → CoroutineName 객체 생성</li>
</ul>
<p>이렇게 만들어진 CoroutineContext 객체는 다음과 같은 형태가 된다.</p>
<table>
<thead>
<tr>
<th>키</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>CoroutineName 키</td>
<td>CoroutineName(”MyCoroutine”)</td>
</tr>
<tr>
<td>CoroutineDispatcher 키</td>
<td>newSingleThreadContext(”MyThread”)</td>
</tr>
<tr>
<td>Job 키</td>
<td>설정되지 않음</td>
</tr>
<tr>
<td>CoroutineExceptionHandler 키</td>
<td>설정되지 않음</td>
</tr>
<tr>
<td>- 설정되지 않은 값을 설정되지 않은 상태로 유지됨</td>
<td></td>
</tr>
</tbody></table>
<p>만들어진 CoroutineContext 객체는 코루틴 빌더 함수의 context 인자로 넘겨 코루틴을 실행하는데 사용할 수 있다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineContext: CoroutineContext = 
        newSingleThreadContext(&quot;MyThread&quot;) + CoroutineName(&quot;MyCoroutine&quot;)

    launch(context = coroutineContext) {
        println(&quot;[${Thread.currentThread().name}] 실행&quot;)
    }
}

// [MyThread @MyCoroutine#2] 실행</code></pre>
<ul>
<li>실행 결과를 통해 coroutineContext 객체의 구성 요소를 사용해 코루틴이 실행 되었음을 알 수 있다.</li>
</ul>
<h2 id="2-3-coroutinecontext-구성-요소-덮어씌우기">2-3. CoroutineContext 구성 요소 덮어씌우기</h2>
<p>CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해지면 나중에 추가된 구성 요소가 이전의 값을 덮어씌운다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineContext: CoroutineContext = 
        newSingleThreadContext(&quot;MyThread&quot;) + CoroutineName(&quot;MyCoroutine&quot;)

    val newCoroutineContext: CoroutineContext = 
        coroutineContext + CoroutineName(&quot;NewCoroutine&quot;)

    launch(context = coroutineContext) {
        println(&quot;[${Thread.currentThread().name}] 실행&quot;)
    }
}

// [MyThread @NewCoroutine#2] 실행</code></pre>
<ul>
<li>launch 코루틴이 사용하는 MyThread로 유지</li>
<li>코루틴의 이름은 NewCoroutine으로 변경됨</li>
</ul>
<p>코루틴의 이름이 변경된 이유는 CoroutineContext 객체의 각 구성 요소는 고유한 키를 갖고 있으므로 만약 같은 구성 요소에 대해 여러 객체가 입력되면 가장 나중에 들어온 값이 앞의 값을 덮어씌우기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/f911d6da-ebb6-4648-b4b3-e122dbabc11c/image.jpg" alt=""></p>
<h2 id="2-4-여러-구성-요소로-이뤄진-coroutinecontext-합치기">2-4. 여러 구성 요소로 이뤄진 CoroutineContext 합치기</h2>
<p>여러 구성 요소로 이뤄진 CoroutineContext 객체 2개가 합쳐지고 2개의 CoroutineContext 객체에 동일한 키를 가진 구성 요소가 있다면 나중에 들어온 값이 선택된다. </p>
<p>이를 확인하기 위해 newSingleThreadContext() 함수로 2개의 CoroutineContext 객체를 만들어 두 객체를 합쳐보자</p>
<pre><code class="language-kotlin">val coroutineContext1 = CoroutineName(&quot;MyCoroutine1&quot;) + newSingleThreadContext(&quot;MyThread1&quot;)
val coroutineContext1 = CoroutineName(&quot;MyCoroutine2&quot;) + newSingleThreadContext(&quot;MyThread2&quot;)
val combinedCoroutineContext = coroutineContext1 + coroutineContext2</code></pre>
<ul>
<li>coroutineContext1에 coroutineContext2 객체가 더해지므로 coroutineContext1의 구성 요소가 coroutineContext2의 구성 요소로 덮어씌워짐</li>
</ul>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/22170ea1-6ebc-4491-98f6-cb3a9118586d/image.jpg" alt=""></p>
<p>위의 예시에서는 두 CoroutineContext 객체가 동일한 구성 요소들을 갖고있었기 때문에 coroutineContext1의 구성 요소들이 coroutineContext2의 구성요소로 덮어씌워졌다.</p>
<p>만약 coroutineContext1이 coroutineContext2가 갖고있지 않은 구성요소를 갖고 있었다면 해당 구성 요소는 덮어씌워지지 않았을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/b4058f14-b6bc-4e8c-8de5-8ac6ea7b5ed9/image.jpg" alt=""></p>
<h2 id="2-5-coroutinecontext에-job-생성해-추가하기">2-5. CoroutineContext에 Job 생성해 추가하기</h2>
<p>Job 객체는 기본적으로 코루틴 빌더 함수를 통해 자동으로 생성된다.</p>
<p>하지만 Job() 함수를 직접 호출해 생성할 수도 있다.</p>
<pre><code class="language-kotlin">val myJob = Job()
val coroutineContext: CoroutineContext = Dispatchers.IO + myJob</code></pre>
<p>이 코드를 통해 생성된 coroutineContext는 다음과 같은 구조를 가진다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/c97278fb-c030-4786-ba5c-93521f67c7ef/image.jpg" alt=""></p>
<p>다만 Job 객체를 직접 생성해 추가하면 코루틴의 구조화가 깨질 수 있다. 따라서 새로운 Job 객체를 생성해 CoroutineContext 객체에 추가하는 것은 주의가 필요하다.</p>
<p>이에 대해서는 <strong>7장. 구조화된 동시성</strong>에서 자세히 다룰 예정이다.</p>
<hr>
<h1 id="3-coroutinecontext-구성-요소에-접근하기">3. CoroutineContext 구성 요소에 접근하기</h1>
<p>CoroutineContext 객체의 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요하다.</p>
<p>각 구성 요소의 키를 얻는 방법부터 알아보자</p>
<h2 id="3-1-coroutinecontext-구성-요소의-키">3-1. CoroutineContext 구성 요소의 키</h2>
<p>CoroutineContext 구성 요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들 수 있다.</p>
<p>일반적으로 CoroutineContext 구성 요소는 자신의 내부에 키를 싱글톤 객체로 구현한다. </p>
<pre><code class="language-kotlin">public data class CoroutineName.Element {
    public companion object Key: CoroutineContext.Key&lt;CoroutineName&gt;
    ...
}</code></pre>
<p>CoroutineName 클래스 내부에 CoroutineContext.Key<CoroutineName>을 구현하는 Key 객체가 Companion Object로 선언되어 있는 것을 볼 수 있다.</p>
<p>이 Key를 사용하면 CoroutineContext에서 CoroutineName에 접근할 수 있다.</p>
<pre><code class="language-kotlin">public interface Job: CoroutineContext.Element {
    public companion object Key: CoroutineContext.Key&lt;Job&gt;
}</code></pre>
<p>Job 인터페이스의 내부에도 Key가 Companion Object로 선언된 것을 볼 수 있다.</p>
<p>이는 CoroutineDispatcher와 CoroutineExceptionHandler도 마찬가지다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/918e1b71-41a0-44ae-82fc-1e11fe0954f6/image.jpg" alt=""></p>
<h2 id="3-2-키를-사용해-coroutinecontext-구성-요소에-접근하기">3-2. 키를 사용해 CoroutineContext 구성 요소에 접근하기</h2>
<h3 id="1-싱글톤-키를-사용해-coroutinecontext-구성-요소에-접근하기">1. 싱글톤 키를 사용해 CoroutineContext 구성 요소에 접근하기</h3>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineContext = CoroutineName(&quot;MyCoroutine&quot;) + Dispatchers.IO
    val nameFromContext = coroutineContext[CoroutineName.Key]
    println(nameFromContext)
}

// CoroutineName(MyCoroutine)</code></pre>
<p>이 코드에는 CoroutineName(”MyCoroutine”)과 Dispatchers.IO로 이뤄진 coroutineContext가 있다.</p>
<p>coroutineContext에 연산자 함수 get(대괄호 대체)의 인자로 CoroutineName.Key를 넘김으로써 coroutineContext를 구성하는 CoroutineName 객체만 가져올 수 있다.</p>
<h3 id="2-구성-요소-자체를-키로-사용해-구성-요소에-접근하기">2. 구성 요소 자체를 키로 사용해 구성 요소에 접근하기</h3>
<p>CoroutineContext의 주요 구성 요소인 CoroutineName, Job, CoroutineDispatcher, CoroutineExceptionHandler 객체는 Companion Object로 CoroutineContext.Key를 갖고 있기 때문에 Key를 명시적으로 사용하지 않아도 구성 요소 자체를 키로 사용할 수 있다.</p>
<p>따라서 굳이 뒤에 “.Key”를 붙이지 않아도 구성 요소에 접근할 수 있다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineContext = CoroutineName(&quot;MyCoroutine&quot;) + Dispatchers.IO
    val nameFromContext = coroutineContext[CoroutineName]
    println(nameFromContext)
}

// CoroutineName(MyCoroutine)</code></pre>
<p>연산자 함수 get의 인자로 넘긴 인자의 “.Key”를 제거했지만 출력 결과는 동일한 것을 확인할 수 있다.</p>
<h3 id="3-구성-요소의-key-프로퍼티를-사용해-구성-요소에-접근하기">3. 구성 요소의 key 프로퍼티를 사용해 구성 요소에 접근하기</h3>
<p>CoroutineContext 객체의 구성 요소들은 모두 key 프로퍼티를 가진다.</p>
<p>이 key 프로퍼티를 사용해 구성 요소에 접근할 수 있다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName: CoroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher: CoroutineDispatcher = Dispatchers.IO
    val coroutineContext = coroutineName + dispatcher

    println(coroutineContext[coroutineName.key])
    println(coroutineContext[dispatcher.key])
}

// CoroutineName(MyCoroutine)
// Dispatchers.IO</code></pre>
<p>coroutineContext를 구성하는데 사용된 coroutineName과 dispatcher 객체의 key 프로퍼티를 통해 CoroutineContext 객체의 구성 요소에 접근할 수 있음을 알 수 있다.</p>
<p>여기서 중요한 점은 구성 요소의 key 프로퍼티는 Companion Object로 선언된 Key와 동일한 객체를 가리킨다는 것이다.</p>
<p>coroutineName.key와 CoroutineName.Key는 동일한 대상을 가리킨다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)

    if (coroutineName.key === CoroutineName.Key)
        println(&quot;coroutineName.key와 CoroutineName.Key 동일합니다.&quot;)
}

// coroutineName.key와 CoroutineName.Key 동일합니다.</code></pre>
<p>동일성 연산자를 통해 coroutineName.key와 CoroutineName.Key가 동일한 객체를 가리킴을 확인할 수 있다.</p>
<hr>
<h1 id="4-coroutinecontext-구성-요소-제거하기">4. CoroutineContext 구성 요소 제거하기</h1>
<p>CoroutineContext 객체는 구성 요소를 제거하기 위한 minusKey 함수를 제공한다.</p>
<p>구성 요소의 Key를 인자로 받아 해당 구성 요소를 제거한 CoroutineContext 객체를 반환한다.</p>
<h2 id="4-1-minuskey-함수를-사용해-구성-요소-제거하기">4-1. minusKey 함수를 사용해 구성 요소 제거하기</h2>
<pre><code class="language-kotlin">val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
val dispatcher = Dispatchers.IO
val myJob = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/d7ef8000-fbd4-4af4-8579-e2f34dad44e5/image.jpg" alt=""></p>
<p>이 coroutineContext에서 CoroutineName 객체를 제거하기 위해서는 coroutineContext에 대해 minusKey 함수를 호출하고 coroutineName을 인자로 넘기면 된다.</p>
<p>이 경우 coroutineContextdptj CoroutineName만 제거돼 반환되며, 반환된 coroutineContext는 deletedCoroutineContext에 할당된다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/8bffbb43-9563-4b18-b73a-f907cb5f7e41/image.jpg" alt=""></p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO
    val myJob = Job()
    val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob

    val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)

    println(deletedCoroutineContext[CoroutineName])
    println(deletedCoroutineContext[CoroutineDispatcher])
    println(deletedCoroutineContext[Job])
}

// null
// Dispatchers.IO
// JobImple{Active}@65e2dbf3</code></pre>
<p>코드의 출력 결과를 보면 coroutineDispatcher의 CoroutineName이 제거되 null이 출력되고 나머지는 정산적으로 출력되는 것을 확인할 수 있다.</p>
<h2 id="4-2-minuskey-함수-사용시-주의할-점">4-2. minusKey 함수 사용시 주의할 점</h2>
<p>minusKey를 호출한 CoroutineContext 객체는 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val coroutineName = CoroutineName(&quot;MyCoroutine&quot;)
    val dispatcher = Dispatchers.IO
    val myJob = Job()
    val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob

    val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)

    println(coroutineContext[CoroutineName])
    println(coroutineContext[CoroutineDispatcher])
    println(coroutineContext[Job])
}

// CoroutineName(MyCoroutine)
// Dispatchers.IO
// JobImple{Active}@65e2dbf3</code></pre>
<p>코드의 실행 결과를 보면 minusKey 함수가 호출된 coroutineContext는 구성 요소가 제거되지 않기 때문에 CoroutineName 객체가 제거되지 않은 것을 확인할 수 있다.</p>
<hr>
<h1 id="5-요약">5. 요약</h1>
<ol>
<li>CoroutineContext 객체는 코루틴의 실행 환경을 설정하고 관리하는 객체로 CoroutineDispatcher, CoroutineName, Job, CoroutineExceptionHandler 등의 객체를 조합해 코루틴 실행 환경을 정의한다.</li>
<li>CoroutineName 객체 → 코루틴의 이름 설정</li>
<li>CoroutineDispatcher 객체→ 코루틴을 스레드로 보내 실행</li>
<li>Job 객체 → 코루틴을 조작하는데 사용</li>
<li>CoroutineExceptionHandler → 코루틴의 예외를 처리</li>
<li>CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하며, 동일한 객체에 대해 중복을 허용하지 않는다.</li>
<li>더하기 연산자(+)를 통해 CoroutineContext의 구성 요소를 조합할 수 있다.</li>
<li>동일한 키를 가진 구성 요소가 여러개 추가될 경우 나중에 추가된 구성 요소가 이전 값을 덮어씌운다.</li>
<li>일반적으로 구성 요소의 Companion Object로 선언된 Key 프로퍼티를 사용해 키 값에 접근할 수 있다. 예를 들어 CoroutineName의 키 값은 CoroutineName.Key를 통해 접근할 수 있다.</li>
<li>키를 연산자 함수 get과 사용해 CoroutineContext 객체를 구성하는 요소에 접근할 수 있다.</li>
<li>CoroutineName, CoroutineDispatcher, CoroutineExceptionHandler는 Companion Object인 Key를 통해 CoroutineContext.Key를 구현하기 때문에 그 자체를 키로 사용할 수 있다.</li>
<li>CoroutineContext 객체의 minusKey 함수를 사용하면 CoroutineContext 객체에서 특정 구성 요소를 제거한 객체를 반환받을 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter.5 async와 Deferred]]></title>
            <link>https://velog.io/@first_woosun/Chapter.5-async%EC%99%80-Deferred</link>
            <guid>https://velog.io/@first_woosun/Chapter.5-async%EC%99%80-Deferred</guid>
            <pubDate>Wed, 08 Jul 2026 06:29:56 GMT</pubDate>
            <description><![CDATA[<p>launch 코루틴 빌더를 통해 생성되는 코루틴 → 작업 실행 후 결과를 반환하지 않음</p>
<p>하지만 네트워크 통신과 같이 코루틴을 통해 비동기로 실행하는 작업의 경우 결과를 수신받아야 하는 경우가 많음</p>
<p>코루틴 라이브러리는 비동기 작업으로 부터 결과를 수신해야 하는 경우를 위해 async 코루틴 빌더를 제공함</p>
<p><strong>launch 코루틴 빌더 함수 → 걀괏값이 없는 코루틴 객체인 Job을 반환</strong></p>
<p><strong>async 코루틴 빌더 함수 → 결괏값이 있는 코루틴 객체인 Deferred를 반환</strong></p>
<blockquote>
<h3 id="5장에서-다루는-내용">5장에서 다루는 내용</h3>
<ul>
<li>async-await을 사용해 코루틴으로부터 결괏값 수신하기</li>
<li>awaitAll 함수를 사용해 복수의 코루틴으로부터 결괏값 수신</li>
<li>withContext를 사용해 실행 중인 코루틴의 CoroutineContext 변경하기</li>
</ul>
</blockquote>
<hr>
<h1 id="1-async-사용해-결괏값-수신하기">1. async 사용해 결괏값 수신하기</h1>
<h2 id="1-1-async-사용해-deferred-만들기">1-1. async 사용해 Deferred 만들기</h2>
<pre><code class="language-kotlin">// async 빌더 함수 선언부
public fun &lt;T&gt; CoroutineScope.asycn(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&gt; T
): Deferred&lt;T&gt;</code></pre>
<ul>
<li>async 함수도 launch 함수와 마찬가지로 context 인자로 CoroutineDispatcher를 설정할 수 있음</li>
<li>start 인자로 CoroutineStart.LAZY를 설정해 코루틴이 지연 시작되도록 할 수 있음</li>
<li>코루틴에서 실행할 코드를 작성하는 block 람다식을 가짐</li>
</ul>
<p>launch 코루틴 빌더는 코루틴에서 결괏값이 반환되지 않기 때문에 Job 객체를 반환함</p>
<p>async 코루틴 빌더는 코루틴에서 결괏값을 담아 반환하기 위해 Deferred<T> 타입의 객체를 반환함</p>
<p>Deferred 객체는 Job 객체와 마찬가지로 코루틴을 추상화한 객체이지만 코루틴으로부터 생성된 결괏값을 감싸는 기능을 추가로 가지며, 이 결괏값의 타입은 제네릭 타입인 T로 표현됨</p>
<p>Deferred 객체의 반환값 타입을 지정하기 위해선 Deferred에 명시적으로 타입을 설정하거나 async 블록의 반환값으로 반환할 결괏값을 설정하면 됨</p>
<pre><code class="language-kotlin">val networkDeferred: Deferred&lt;String&gt; = async(Dispatchers.IO) {
    delay(1000L)
    return@async &quot;Dummy Response&quot;
}</code></pre>
<h2 id="1-2-await을-사용한-결괏값-수신">1-2. await을 사용한 결괏값 수신</h2>
<p>Deferred 객체 → 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체임</p>
<p>코루틴이 실행 완료될 때 결괏값이 발생하므로 언제 결괏값이 반환될지 정확히 알 수 없으며, 결괏값이 필요한 경우 결괏값이 수신될 때까지 대기해야 함</p>
<p>Deferred 객체 → 결괏값의 수신을 위해 await 함수를 제공</p>
<p>await 함수는 await의 대상이 된 Deferred 코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단, Deferred 코루틴이 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개함</p>
<p>코루틴의 실행이 완료될 때까지 대기한다는 점에서 Job 객체의 Join함수와 매우 유사하게 동작함</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val networkDeferred: Deferred&lt;String&gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async &quot;Dummy Response&quot;
    }

    val result = networkDeferred.await()
    println(result)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/9c871e0d-e854-415a-846a-5bf10b4b9921/image.jpg" alt=""></p>
<p>networkDeferred.await()를 호출하면 networkDeferred 코루틴이 완료될 때까지 runBlocking 코루틴이 일시 중단됨</p>
<p>이후 networkDeferred 코루틴으로부터 “Dummy Response”가 반환되면 runBlocking 코루틴이 재개되며, “Dummy Response”는 result 변수에 할당됨</p>
<p>println(result)를 통해 “Dummy Response”가 출력됨</p>
<hr>
<h1 id="2-deferred는-특수한-형태의-job이다">2. Deferred는 특수한 형태의 Job이다.</h1>
<p>4장에서 모든 코루틴 빌더는 Job 객체를 생성한다고 언급했었다.</p>
<p>하지만 await 코루틴 빌더는 Deferred 객체를 반환한다.</p>
<p>그렇다고 await 코루틴 빌더가 특별한 코루틴 객체를 반환하는 것은 아니다.</p>
<p>Deferred 객체는 Job 객체의 특수한 형태로 Deferred 인터페이스는 Job 인터페이스의 서브타입으로 선언된 인터페이스다.</p>
<p>Deferred 객체는 코루틴으로부터 결괏값 수신을 위해 Job 객체에 몇 가지 기능.이 추가되었을 뿐, 여전히 Job 객체의 일종이다.</p>
<pre><code class="language-kotlin">// Deferred 인터페이스의 선언부

public interface Deferred&lt;out T&gt;: Job {
    public suspend fun await(): T
    ...
}</code></pre>
<p>Deferred 인터페이스가 Job 인터페이스의 서브 타입으로 선언되었기 때문에 Deferred 객체는 Job 객체의 모든 프로퍼티를 사용할 수 있다.</p>
<p>Join을 사용해 Deferred 객체가 완료될 때까지 호출부의 코루틴을 일시 중단할 수도 있도, Deferred 객체가 취소돼야 할 경우 cancel 함수를 호출해 취소할 수 도 있다.</p>
<p>isActive, isCancelled, isCompleted와 같은 상태 프로퍼티도 사용할 수 있다.</p>
<p>정리하자면 Deferred 객체는 결괏값을 받는 기능이 추가된 Job 객체이며, Job 객체의 모든 함수와 변수를 사용할 수 있다. (특수한 형태의 Job)</p>
<hr>
<h1 id="3-복수의-코루틴으로부터-결괏값-수신하기">3. 복수의 코루틴으로부터 결괏값 수신하기</h1>
<p>프로그램을 만들 때 여러 비동기 작업으로부터 결괏값을 반환받아 병합해야 하는 경우가 자주 생긴다.</p>
<h2 id="3-1-await을-사용해-복수의-코루틴으로부터-결괏값-수신하기">3-1. await을 사용해 복수의 코루틴으로부터 결괏값 수신하기</h2>
<p>콘서트 개최 시 관람객을 2개의 플래폼에서 모집하는 상황 → 각 플랫폼에 등록된 관람객을 조화한 후 병합해야 함</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val participantDeferred1: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async arrayOf(&quot;James&quot;, &quot;Jason&quot;)
    }
    val participants1 = participantDeferred1.await()

    val participantDeferred2: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async arrayOf(&quot;Jenny&quot;)
    }
    val participants2 = participantDeferred2.await()

    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*participants1, *participants2)}&quot;)
}

// [지난 시간&quot; 2018ms] 참여자 목록: [Jamse, Jason, Jenny]</code></pre>
<ul>
<li>별표(*)가 쓰인 이유 ➡️ 배열을 <code>vararg</code> 인자로 펼칠 때 필요</li>
</ul>
<ol>
<li>시작시 startTime 변수에 시작 시간을 기록</li>
<li>participantDeferred1 코루틴을 통해 플랫폼1에서 등록한 관람객 목록을 가져옴</li>
<li>participantDeferred1.await()을 통해 플랫폼1의 서버로부터 결과가 수신될 때까지 대기</li>
<li>participantDeferred2 코루틴을 통해 플랫폼2에서 등록한 관람객 목록을 가져옴</li>
<li>participantDeferred2.await()을 통해 플랫폼2의 서버로부터 결과가 수실될 때까지 대기</li>
<li>모든 작업을 마친 후 참여자 목록을 병합해 출력</li>
</ol>
<p>코드의 실행결과를 통해 참여자 목록이 병합되고 각 서버를 호출하는데 1초씩 걸려 총 2초의 시간이 걸리는 것을 확인할 수 있음</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/84dba20a-d14b-4935-8e59-174f61310f53/image.jpg" alt=""></p>
<p>서버 호출이 총 2초가 걸린 이유 → await을 호출하면 결괏값이 반환될 때까지 호출부의 코루틴이 일시 중단되기 때문</p>
<p>Dispatchers.IO를 사용해 백그라운드 스레드에서 코루틴을 실행하더라도 await를 호출하면 코루틴이 실행 완료될 때까지 runBlocking 코루틴이 일시 중단돼 대기하게 됨</p>
<p>따라서 participantDeferred1.await()가 participantDeferred2 코루틴이 생성되기 전에 호출되면 participantDeferred2 코루틴은 순차적으로 처리됨</p>
<p>두 작업은 동시에 독립적으로 실행될 수 있는 작업임에도 순차적으로 처리되는 건 비효율적임</p>
<p>이 문제를 해결하려면 await()의 호출 시점을 두 코루틴이 모두 생성된 이후로 변경하면 된다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val participantDeferred1: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async arrayOf(&quot;James&quot;, &quot;Jason&quot;)
    }

    val participantDeferred2: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        return@async arrayOf(&quot;Jenny&quot;)
    }

    val participants1 = participantDeferred1.await()
    val participants2 = participantDeferred2.await()

    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*participants1, *participants2)}&quot;)
}

// [지난 시간&quot; 1018ms] 참여자 목록: [Jamse, Jason, Jenny]</code></pre>
<p>participantDeferred1.await()가 호출되기 전에 participantDeferred2 코루틴이 실행되므로 두 코루틴이 동시에 실행됨</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/23383297-8aa3-4324-869a-754d3f1be2fb/image.jpg" alt=""></p>
<p>두 코루틴이 동시에 실행될 수 있도록하는 것은 코루틴의 성능 측면에서 매우 중요함</p>
<p>await의 호출 시점에 따라 코루틴이 순차적으로 처리될 수도 있고 동시에 처리될 수 있을을 유의해야 한다.</p>
<h2 id="3-2-awaitall-사용한-결괏값-수신">3-2. awaitAll 사용한 결괏값 수신</h2>
<p>앞의 콘서트 관람객 예시에서는 관람객을 등록받는 플랫폼의 개수가 적었다. 만약 플랫폼의 개수가 100개로 늘어난다면 어떨까?</p>
<p>각 코루틴의 결과를 반환받기 위해 100개의 await()를 호출해야할까? 다행히도 그렇지 않다.</p>
<p>코루틴 라이브러리는 복수의 Deferred 객체로부터 결괏값을 수신하기 위한 awaitAll() 함수를 제공한다.</p>
<pre><code class="language-kotlin">public suspend fun &lt;T&gt; awaitAll(vararg deferreds: Deferred&lt;T&gt;): List&lt;T&gt;</code></pre>
<p>awaitAll 함수의 구현부를 보면 가변 인자로 Deferred 타입의 객체를 받아 인자로 받은 모든 Deferred 코루틴으로부터 결과가 수신될 때까지 호출부의 코루틴을 일시 중단한 후 결과가 모두 수신되면 Deferred 코루틴들로부터 수신한 결괏값들로 List를 만들어 반환하고 호출부의 코루틴을 재개한다.</p>
<p>participantDeferred1과 participantDeferred2의 예시에 awaitAll() 함수를 적용해보자</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val participantDeferred1: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        arrayOf(&quot;James&quot;, &quot;Jason&quot;)
    }

    val participantDeferred2: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        arrayOf(&quot;Jenny&quot;)
    }

    val results: List&lt;Array&lt;String&gt;&gt; = awaitAll(participantDeferred1, participantDeferred2)

    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}&quot;)
}

// [지난 시간: 1013ms] 참여자 목록: [Jamse, Jason, Jenny]</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/2791cbfc-c7b6-4cf0-8058-aebac83b636d/image.jpg" alt=""></p>
<p>runBlocking 코루틴에서 awaitAll() 함수가 호출되면 awaitAll() 함수의 인자로 전달된 코루틴들의 실행이 모두 완료될 때까지 runBlocking 코루틴이 일시 중단된다.</p>
<p>이후 인자로 전달된 코루틴들의 실행이 완료되면 결과들이 리스트로 만들어져 반화되고 runBlocking의 코루틴이 재개된다.</p>
<h2 id="3-3-컬렉션에-대해-awaitall-사용하기">3-3. 컬렉션에 대해 awaitAll 사용하기</h2>
<p>awaitAll 함수 → Collection 인터페이스에 대한 확장 함수로도 제공함</p>
<pre><code class="language-kotlin">public suspend fun &lt;T&gt; Collection&lt;Deferred&lt;T&gt;&gt;.awaitAll(): List&lt;T&gt;</code></pre>
<p>Collection&lt;Deferred<T>&gt;에 대해 awaitAll() 함수를 호출하면 컬렉션에 속한 Deferred 객체들의 코루틴이 모두 완료돼 결괏값을 반환할 때까지 대기한다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.CurrentTimeMillis()
    val participantDeferred1: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        arrayOf(&quot;James&quot;, &quot;Jason&quot;)
    }

    val participantDeferred2: Deferred&lt;Array&lt;String&gt;&gt; = async(Dispatchers.IO) {
        delay(1000L)
        arrayOf(&quot;Jenny&quot;)
    }

    val results: List&lt;Array&lt;String&gt;&gt; = listof(participantDeferred1, participantDeferred2).awaitAll()

    println(&quot;[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}&quot;)
}

// [지난 시간: 1015ms] 참여자 목록: [Jamse, Jason, Jenny]</code></pre>
<p>가변 인자를 받는 awaitAll() 함수를 사용한 것과 완전히 같게 동작한다.</p>
<hr>
<h1 id="4-withcontext">4. withContext</h1>
<h2 id="4-1-withcontext로-async-await-대체하기">4-1. withContext로 async-await 대체하기</h2>
<pre><code class="language-kotlin">public suspend fun &lt;T&gt; withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -&gt; T
): T</code></pre>
<p>withContext 함수가 호출되면 함수의 인자로 설정된 CoroutineContext 객체를 사용해 block 람다식을 실행하고, 완료되면 그 결과를 반환한다.</p>
<p>withContext 함수를 호출한 코루틴은 인자로 받은 CoroutineContext 객체를 사용해 block 람다식을 실행하며, block 람다식을 모두 실행하면 다시 기존의 CoroutineContext 객체를 사용해 코루틴이 재개된다.</p>
<p>이 동작은 async-await 쌍을 연속적으로 실행했을 때의 동작과 매우 유사하다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val networkDeferred: Deferred&lt;String&gt; = async(Dispatchers.IO) {
        delay(1000L)
        &quot;Dummy Response&quot;
    }

    val result = networkDeferred.await()
    println(result)
}

// Dummy Response</code></pre>
<p>이 코드에서는 async 함수를 호출해 Deferred 객체를 만들고, 곧바로 Deferred 객체에 대해 await 함수를 호출한다.</p>
<p>이런 async 함수를 호출한 후 연속적으로 await 함수를 호출해 연속적으로 결괏값 수신을 대기하는 코드는 withContext 함수로 대체할 수 있다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val result: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext &quot;Dummy Response&quot;
    }

    println(result)
}

// Dummy Response</code></pre>
<p>async-await 쌍이 withContext 함수로 대체되면 Deferred 객체가 생성되는 부분이 없어지고 “Dummy Response”가 결과로 바로 반환된다.</p>
<h2 id="4-2-withcontext의-동작-방식">4-2. withContext의 동작 방식</h2>
<p>withContext 함수는 async-await 쌍과 비슷하게 동작하는 것처럼 보이지만 내부적으로 보면 다르게 동작한다.</p>
<p>async-await 쌍은 새로운 코루틴을 생성해 작업을 처리하지만 withContext 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경만 변경해 작업을 처리한다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    println(&quot;[${Thread.currentThread().name]] runBlocking 블록 실행&quot;)
    withContext(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name]] withContext 블록 실행&quot;)
    }
}

/*
[main @coroutine#1] runBlocking 블록 실행
[DefaultDispatcher-worker-1 @coroutine#1] withContext 블록 실행
*/</code></pre>
<p>코드의 실행 결과를 보면 runBlocking 함수의 block 람다를 실행하는 스레드와 withContext 함수의 block 람다식을 실행하는 스레드는 <code>main</code>과 <code>DefaultDispatcher-worker-1</code> 로 다르지만 코루틴은 <code>coroutine#1</code>으로 같은 것을 확인할 수 있다.</p>
<p>withContext 함수는 새로운 코루틴을 만드는 개신 기존의 코루틴에서 CoroutineContext 객체만 바꿔서 실행하는 것을 알 수 있다.</p>
<p>위의 예시에서는 CoroutineContext 객체가 Dispatchers.IO로 바뀌었기 때문에 백그라운드 스레드에서 실행됐다.</p>
<blockquote>
<p>다만 CoroutineContext 객체에 대해 아직 다루지 않았기 때문에 “CoroutineContext가 변경됐다.”는 표현을 “코루틴을 실행시키는 CoroutineDispatcher 객체가 변경돼 코루틴의 실행 스레드가 변화한다.”라고 이해하는 것이 좋다.
CoroutineContext 객체에 대해서는 추후에 다룰 예정이다.</p>
</blockquote>
<p>withContext 함수가 호출되면 실행 중인 코루틴의 실행 환경이 withContext 함수의 context 인자 값으로 변경돼 실행되며, 이를 <strong>컨텍스트 스위칭(context switching)</strong>이라고 부른다.</p>
<p>context 인자로 coroutineDispatcher 객체가 넘어오면 코루틴은 해당 CoroutineDispatcher 객체를 사용해 다시 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/4d7c5c69-da4a-482d-89d1-65e3ac780ee6/image.jpg" alt=""></p>
<p>withContext 함수는 함수의 block 람다식이 실행되는 동안 코루틴의 실행 환경을 변경시킨다.</p>
<p>withContext 함수가 block 람다식을 벗어나면 다시 원래의 CoroutineContext 객체를 사용해 실행된다.</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    println(&quot;[${Thread.currentThread().name]] runBlocking 블록 실행&quot;)
    async(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name]] withContext 블록 실행&quot;)
    }.await()
}

/*
[main @coroutine#1] runBlocking 블록 실행
[DefaultDispatcher-worker-1 @coroutine#2] withContext 블록 실행
*/</code></pre>
<p>이 코드의 실행 결과를 보면 async 블록을 실행하는 코루틴은 coroutine#2로 runBlocking 블록을 실행하는 coroutine#1과 다른 것을 확인할 수 있다.</p>
<p>async-await 쌍을 사용하면 새로운 코루틴을 만들지만 await 함수가 호출돼 순차 처리 되기 때문에 동기적으로 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/fe317a06-5d0b-4d4b-9ee4-057c48ec27b2/image.jpg" alt=""></p>
<hr>
<h2 id="4-3-withcontext-사용-시-주의점">4-3. withContext 사용 시 주의점</h2>
<p>withContext 함수 → 새로운 코루틴을 만들지 않기 때문에 하나의 코루틴에서 withContext 함수가 여러 번 호출되면 순차적으로 실행됨 (withContext 여러개를 한 코루틴에서 호출하면 성능 문제가 발생할 수 있음)</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val helloString = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext &quot;Hello&quot;
    }
    val worldString = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext &quot;World&quot;
    }

    println(&quot;[${getElapsedTime(startTime)} ${helloString} ${worldString}&quot;)
}

// [지난 시간: 2018ms] Hello World</code></pre>
<p>“Hello” 문자열과 “World”문자열을 반환하는 두 코루틴이 순차 처리되어 총 2초의 실행 시간이 걸린 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/59ceadb0-552d-4089-827e-2a88c40088e0/image.jpg" alt=""></p>
<p>위의 코드에선 runBlocking 함수에 의해 하나의 코루틴만 생성된다. </p>
<p>이후 withContext 함수를 사용해 코루틴을 유지한 채로 실행 스레드풀만 Dispatchers.IO로 변경된다.</p>
<p>따라서 1초 동안 대기한 후 문자열을 반환받는 두 작업이 한 코루틴에서 순차적으로 처리되어 총 2초 정도의 시간이 걸리는 것이다.</p>
<p>이는 withContext 함수가 새로운 코루틴을 생성하지 않기 때문에 생기는 문제다.</p>
<p>이 문제를 async-await 쌍으로 해결해보자</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val startTime = System.currentTimeMillis()
    val helloDeferred = async(Dispatchers.IO) {
        delay(1000L)
        return@async &quot;Hello&quot;
    }
    val worldDeferred = async(Dispatchers.IO) {
        delay(1000L)
        return@async &quot;World&quot;
    }

    val results = awaitAll(helloDeferred, worldDeferred)

    println(&quot;[${getElapsedTime(startTime)}] ${results[0]} ${results[1]}&quot;)
}

// [지난 시간: 1013ms] Hello World</code></pre>
<p>HelloDeferred와 WorldDeferred가 모두 실행된 뒤에 awaitAll 함수를 호출해 여러 async 코루틴의 결괏값을 반환받는다.</p>
<p>두 코루틴이 병렬로 처리된 후 한번에 결괏값을 반환받기 때문에 총 1초 정도의 시간이 걸린 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/b690acb6-dede-46c7-8344-18b722ee24ff/image.jpg" alt=""></p>
<blockquote>
<p>그림에서는 백그라운드 스레드가 2개 사용되었지만 코루틴은 스레드를 사용하지 않을 때는 스레드를 양보하기 때문에 백그라운드 스레드를 하나만 사용할 수도 있다.</p>
</blockquote>
<p>withContext 함수를 사용하면 코드가 깔끔해지는 효과를 낼 수 있지만 잘못 사용하면 코루틴이 동기적으로 실행될 수 있어 사용시 주의가 필요하다.</p>
<hr>
<h2 id="4-추가-자료-withcontext를-사용한-코루틴-스레드-전환">4-추가 자료. withContext를 사용한 코루틴 스레드 전환</h2>
<pre><code class="language-kotlin">private val myDispatcher1 = newSingleThreadContext(&quot;MyThread1&quot;)
private val myDispatcher2 = newSingleThreadContext(&quot;MyThread2&quot;)

fun main() = runBlocking&lt;Unit&gt; {
    println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    withContext(myDispatcher1) {
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        withContext(myDispatcher2) {
            println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
        }
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
    println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
}

/*
[main @coroutine#1] 코루틴 실행
[MyThread1 @coroutine#1] 코루틴 실행
[MyThread2 @coroutine#1] 코루틴 실행
[MyThread1 @coroutine#1] 코루틴 실행
[main @coroutine#1] 코루틴 실행
*/</code></pre>
<p>코드의 실행 결과를 보면 모든 코루틴이 @coroutine#1인데 스레드 풀만 Main, MyThread1, MyThread2로 변경되는 것을 볼 수 있다.</p>
<p>withContext 함수를 사용하면 CoroutineDispatcher 객체를 통해 코루틴이 실행되는데 사용되는 CoroutineDispatcher 객체를 자유롭게 바꿀 수 있다.</p>
<p>withContext 함수를 통해 변경된 CoroutineDispatcher 객체는 withContext 함수 블록 냅에서만 유효하다. withContext 블록을 벗어나면 다시 이전의 CoroutineDispatcher 객체를 사용하게 되며 스레드가 다시 전환된다.</p>
<hr>
<h1 id="5-요약">5. 요약</h1>
<ol>
<li>async 함수를 사용해 코루틴을 실행하면 코루틴의 결과를 감싸는 Deferred 객체를 반환받는다.</li>
<li>Deferred 객체는 Job의 서브타입으로 Job 객체에 결괏값을 감싸는 기능이 추가된 객체다.</li>
<li>Deferred 객체에 대해 await 함수를 호출하면 결괏값을 반환받을 수 있다. await 함수를 호출한 코루틴은 Deferred 객체가 결괏값을 반환할 때까지 일시 중단 후 대기할 수 있다.</li>
<li>awaitAll 함수를 사용해 복수의 Deferred 코루틴이 결괏값을 반환할 때까지 대기할 수 있다.</li>
<li>awaitAll 함수는 컬렉션에 대한 확장 함수로도 제공된다.</li>
<li>withContext를 사용해 async-await 쌍을 대체할 수도 있다.</li>
<li>withContet 함수는 코루틴을 새로 생성하지 않는다. 코루틴의 실행 환경을 담는 CoroutineDispatcher 객체만 변경해 코루틴을 실행하므로 이를 활용해 코루틴이 실행되는 스레드를 변경할 수 있다.</li>
<li>withContext 함수는 코루틴을 새로 생성하지 않으므로 병렬로 실행되야 하는 복수의 작업을 withContext로 감싸 실행하면 순차적으로 실행된다. 이럴때는 async를 사용해 작업이 병렬로 실행될 수 있도록 해야한다.</li>
<li>withContext로 실행환경이 변경돼 실행되는 코루틴은 withContext의 작업을 모두 실행하면 다시 이전의 실행 환경으로 돌아온다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter.4 코루틴 빌더와 Job]]></title>
            <link>https://velog.io/@first_woosun/Chapter.4-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%B9%8C%EB%8D%94%EC%99%80-Job</link>
            <guid>https://velog.io/@first_woosun/Chapter.4-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%B9%8C%EB%8D%94%EC%99%80-Job</guid>
            <pubDate>Thu, 02 Jul 2026 04:23:35 GMT</pubDate>
            <description><![CDATA[<p>코루틴을 생성하는 데 사용하는 함수 → 코루틴 빌더</p>
<p>코루틴 빌더는 코루틴을 만들고 코루틴을 추상화한 Job 객체를 생성</p>
<p>코루틴 빌더인 launch 함수를 호출하면 코투린이 만들어지고, Job 객체가 생성돼 반환</p>
<p>반환된 Job 객체는 코루틴의 상태를 추적하고 제어하는데 사용됨</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch(Dispatchers.IO) { //Job 객체 반환
        println(&quot;[${Thread.currentThread().name}] 실행&quot;)
    }
}</code></pre>
<p><strong>코루틴 → 일시 중단할 수 있는 작업</strong></p>
<p>코루틴을 추상화한 Job 객체는 이에 대응해 코루틴을 제어할 수 있는 함수와 코루틴의 상태를 나타내는 상태 값들을 외부에 노출</p>
<blockquote>
<h3 id="4장에서-다루는-내용">4장에서 다루는 내용</h3>
<ul>
<li>join, joinAll 함수를 사용한 코루틴 간 순차 처리</li>
<li>CoroutineStart.LAZY를 사용한 코루틴 지연 시작</li>
<li>코루틴 실행 취소</li>
<li>코루틴의 상태</li>
</ul>
</blockquote>
<hr>
<h1 id="1-join을-사용한-코루틴-순차-처리">1. join을 사용한 코루틴 순차 처리</h1>
<p>코루틴 간에는 순차 처리가 필요한 경우가 종종 발생</p>
<ul>
<li>데이터베이스 박업을 순차적으로 처리</li>
<li>캐싱된 토큰 값이 업데이트된 이후에 네트워크 요청을 해야하는 상황</li>
</ul>
<p>Job 객체는 순차 처리가 필요한 상황을 위해 join함수를 제공해 먼저 처리돼야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만들 수 있음</p>
<h2 id="1-1-순차-처리가-안-될-경우의-문제">1-1. 순차 처리가 안 될 경우의 문제</h2>
<p>네트워크 요청 시 인증 토큰이 필요한 상황</p>
<ul>
<li>인증 토큰이 업데이트 된 이후 네트워크 요청이 실행돼야 요청이 정상적으로 처리될 수 있음</li>
<li>인증 토큰이 업데이트 되기 이전에 요청이 실행되면 문제가 생길 수 있음</li>
</ul>
<p>토큰 업데이트 작업과 네트워크 요청 작업에 순차 처리가 적용되지 않은 코드</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 시작&quot;)
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 완료&quot;)
    }

    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 네트워크 요청&quot;)
    }
}

/*
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료
*/</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/ed0687de-4156-4705-ad4c-018b99250ffd/image.jpg" alt=""></p>
<ul>
<li>토큰 업데이트 시작 이후 업데이트가 완료되기 전에 네트워크 요청이 발생하는 상황 가정</li>
<li>이 코드에서는 인증 토큰 업데이트 작업과 네트워크 요청 작업이 병렬로 실행</li>
<li>실제 네트워크 환경에서는 심각한 오류가 발생할 수 있음<ul>
<li>만료된 토큰으로 요청, Authorization 헤더 누락, 중복 토큰 갱신, 요청 실패 후 불필요한 재시도, 사용자 경험 문제, 데이터 불일치 등등…</li>
</ul>
</li>
<li>이런 문제를 해결하려면 updateTokenJob이 모두 완료된 이후 networkCallJob이 실행되어야 함</li>
</ul>
<h2 id="1-2-join-함수-사용해-순차-처리하기">1-2. join 함수 사용해 순차 처리하기</h2>
<p>Job 객체의 조인 함수를 사용하면 코루틴 간에 순차 처리가 가능함</p>
<p>JobA 코루틴이 완료된 이후에 JobB 코루틴이 실행돼야 한다면 JobB 코루틴이 실행되기 전에 JobA 코루틴에 join 함수를 호출하면 됨</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 시작&quot;)
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 완료&quot;)
    }

    updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴 일시 중단

    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 네트워크 요청&quot;)
    }
}

/*
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
*/</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/6673c29b-7f9a-4306-b2d4-d56f2b1faf58/image.jpg" alt=""></p>
<ul>
<li>Job 객체의 join 함수를 호출하면 join의 대상이 된 코루틴의 작업이 완료될 때까지 join을 호출한 코루틴이 일시 중단됨</li>
<li>runBlocking 코루틴이 updateTokenJob.join()을 호출하면 runBlocking 코루틴은 updateTokenJob 코루틴이 완료될 때까지 일시 중단됨</li>
<li>이후 updateTokenJob의 작업이 모두 완료되면 runBlocking 코루틴이 재개돼 networkCallJob 코루틴을 실행함</li>
</ul>
<p><strong>“join 함수를 호출한 코루틴은 join의 대상이 된 코루틴이 완료될 때까지 일시 중단됨”</strong></p>
<p>join 함수는 일시 중단이 가능한 지점(코루틴 등)에서만 호출할 수 있음</p>
<blockquote>
<h3 id="join-함수는-join을-호출한-코루틴만-일시-중단한다">join 함수는 join을 호출한 코루틴만 일시 중단한다.</h3>
<p>join 함수는 join함수를 호출한 코루틴을 제외하고 이미 실행중인 다른 코루틴을 일시 중단하지 않음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val updateTokenJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 시작&quot;)
        delay(100L)
        println(&quot;[${Thread.currentThread().name}] 토큰 업데이트 완료&quot;)
    }

    val independentJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 독립적인 작업 실행&quot;)
    }

    updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴 일시 중단

    val networkCallJob = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 네트워크 요청&quot;)
    }
}

/*
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-2 @coroutine#2] 독립적인 작업 실행
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
*/</code></pre>
<p>independentJob은 updateTokenJob.join()이 호출되더라도 updateTokenJob이 끝날 때까지 기다리지 않고 실행되는 것을 확인할 수 있음</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/eddcf0e3-26a2-46fc-b839-0d71991ce218/image.jpg" alt=""></p>
</blockquote>
<hr>
<h1 id="2-joinall을-사용한-코루틴-순차-처리">2. joinAll을 사용한 코루틴 순차 처리</h1>
<p>실제 개발 시에는 서로 독립적인 여러 코루틴을 병렬로 실행한 후 실행한 요청들이 모두 끝날 때까지 기다렸다가 다음 작업을 진행하는 것이 효율적</p>
<p>SNS에 이미지를 업로드 하는 경우</p>
<ul>
<li>여러 이미지를 한번에 업로드 할 때</li>
<li>코루틴을 하나만 만들어 각 이미지를 순차적으로 변환하는 것 보다</li>
<li>변환할 이미지 개수만큼 코루틴을 생성해 병렬로 처리한 후 결과를 취함하는 것이 효율적</li>
</ul>
<p>joinAll 함수는 복수의 코루틴 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단</p>
<h2 id="2-1-joinall-함수">2-1. joinAll 함수</h2>
<p>joinAll 함수의 내부 동작 → 가변 인자로 Job 타입의 객체를 받은 후 각 Job 객체에 대해 모두 join 함수를 호출</p>
<pre><code class="language-kotlin">public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach {
    it.join()
}</code></pre>
<h2 id="2-2-joinall-함수-사용해-보기">2-2. joinAll 함수 사용해 보기</h2>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val convertImageJob1: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name}] 이미지 1 변환 완료&quot;)
    }

    val convertImageJob2: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name}] 이미지 2 변환 완료&quot;)
    }

    joinAll(convertImageJob1, convertImageJob2)

    val uploadImageJob: Job = launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 이미지1, 2 업로드&quot;)
    }
}

/*
[DefaultDispatcher-worker-1 @coroutine#2] 이미지1 변환 완료
[DefaultDispatcher-worker-2 @coroutine#3] 이미지1 변환 완료
[DefaultDispatcher-worker-1 @coroutine#4] 이미지1, 2 업로드
*/</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/56a01b95-105d-4961-ac6f-75cb3d7e3303/image.jpg" alt=""></p>
<hr>
<h1 id="3-coroutinestartlazy로-코루틴-지연-시작하기">3. CoroutineStart.LAZY로 코루틴 지연 시작하기</h1>
<p>launch 함수를 통한 코루틴 생성 → 사용할 수 있는 스레드가 있을 경우 곧바로 실행</p>
<p>코루틴을 미리 생성한 후 나중에 실행해야 할 수 있음</p>
<p>코루틴 라이브러리는 생성된 코루틴을 지연 시작(Lazy Start)할 수 있는 기능을 제공함</p>
<p>지연 시작이 적용된 코루틴은 생성 후 대기 상태에 놓이며, 실행을 요청하지 않으면 실행되지 않음</p>
<p>코루틴을 지연 시작하기 위해서는 launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨 옵션을 적용해야 함</p>
<pre><code class="language-kotlin">val lazyJob: Job = launch(start = CoroutineStart.LAZY) { // 지연 코루틴
    ...
}

lazyJob.start() // 실행 요청</code></pre>
<hr>
<h1 id="4-코루틴-취소하기">4. 코루틴 취소하기</h1>
<p>코루틴 실행 도중 필요가 없어지면 즉시 취소해야 함 → 스레드를 점유해 성능 하락이 발생할 수 있음</p>
<p>코루틴 라이브러리의 Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공함</p>
<h2 id="4-1-cancel-사용해-job-취소하기">4-1. cancel 사용해 Job 취소하기</h2>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val longJob: Job = launch(Dispatchers.Default) {
        repeat(10) { repeatTime -&gt; 
            delay(1000L)
            println(&quot;반복횟수 ${repeatTime}&quot;)
        }
    }

    delay(3500L)

    longJob.cancel() // 코루틴 취소
}</code></pre>
<ul>
<li>총 10번의 로그를 찍는 코루틴(longJob)</li>
<li>3500 밀리초 뒤에 코루틴을 취소해 로그가 3번 까지 찍힘</li>
</ul>
<h2 id="4-2-cancelandjoin을-사용한-순차-처리">4-2. cancelAndJoin을 사용한 순차 처리</h2>
<p>cancel() 함수 → 코루틴을 즉시 취소하는 것이 아닌 Job 객체의 취소 확인용 플래그를 변경해 이 코루틴이 취소되어야 한다고 알리는 함수</p>
<p><strong>cancel 함수를 호출한다고 해당 코루틴이 즉시 취소되는 것이 아님</strong></p>
<p>취소에 대한 순차성 보장을 위해 Job 객체는 <strong>cancelAndJoin</strong> 함수를 제공</p>
<p>cancelAndJoin 함수를 사용하면 코루틴의 취소가 완료될 때까지 호출부의 코루틴이 일시 중단됨</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val longJob : Job = launch(Dispatchers.Default) { 
        ...
    }

    longJob.cancelAndJoin()

    executeAfterJobCancelled()
}</code></pre>
<ul>
<li>longJob 코루틴이 취소될 때까지 runBlocking 코루틴이 일시 중단됨</li>
</ul>
<hr>
<h1 id="5-코루틴의-취소-확인">5. 코루틴의 취소 확인</h1>
<p>cancel 함수 &amp; cancelAndJoin 함수 → Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하며, 코루틴이 이 플래그를 확인하는 시점에 취소됨</p>
<p>코루틴이 취소를 확인하는 시점은 일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println(&quot;작업 중&quot;)
        }
    }
    delay(100L)
    whileJob.cancel()
}</code></pre>
<ul>
<li>whileJob 객체의 코루틴이 100밀리초 후에 취소도리 것 같지만 실제로는 취소되지 않음</li>
<li>코루틴 블록 내부에 콜틴의 취소를 확인할 수 있는 시점이 없기 때문임</li>
<li>while 문에서 코드가 반복해 실행되고 있어 while문을 벗어날 수 없음</li>
<li>while 문 내부에도 일시 중단 시점이 없어 일시 중단이 일어나지 않음</li>
</ul>
<p>whileJob 코루틴은 코루틴의 취소를 확인할 수 있는 시점이 없어 취소가 요청됐음에도 계속해서 실행됨</p>
<p>이런 코루틴을 취소하는 방법들에 대해 알아본다.</p>
<h2 id="5-1-delay를-사용한-취소-확인">5-1. delay를 사용한 취소 확인</h2>
<p>delay 함수 → 일시 중단 함수 (suspend fun)</p>
<ul>
<li>특정 시간만큼 호출부의 코루틴을 일시 중단</li>
</ul>
<p>코루틴은 일시 중단 시점에 취소를 확인하기 때문에 while문 내부에 delay를 직접 선언하면 일시 중단 시점을 만들어 코루틴을 취소할 시점을 만들 수 있음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println(&quot;작업 중&quot;)
            delay(1L)
        }
    }
    delay(100L)
    whileJob.cancel()
}</code></pre>
<p>이 방법은 while문이 반복될 때마다 작업을 강제로 1밀리초 동안 일시 중단시킨다는 점에서 효율적이지 않다.</p>
<h2 id="5-2-yield를-사용한-취소-확인">5-2. yield를 사용한 취소 확인</h2>
<p>yield → 양보</p>
<p>yield 함수가 호출되면 코루틴은 자신이 사용중이던 스레드의 사용을 중단하면서 작업을 일시 중단함</p>
<p>작업이 일시 중단되기 때문에 코루틴 취소를 확인할 수 있는 시간이 생김</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println(&quot;작업 중&quot;)
            yield()
        }
    }
    delay(100L)
    whileJob.cancel()
}</code></pre>
<p>yield() 함수도 while문이 한 번 돌 때마다 스레드 사용이 양보되면서 일시중단되는 문제가 있음</p>
<p>경량 스레드라고 해도 매번 일시 중단되는 것은 비효츌적임</p>
<h2 id="5-3-coroutinescopeisactive를-사용한-취소-확인">5-3. CoroutineScope.isActive를 사용한 취소 확인</h2>
<p>CoroutineScope.isActive → 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 파라미터</p>
<p>코루틴 취소가 요청되면 isActive 프로퍼티의 값이 false로 바뀜</p>
<ul>
<li>while문의 인자로 this.isActive를 넘기면 코루틴 취소 요청이 발생했을 때 while문이 취소되도록 만들 수 있음</li>
</ul>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(this.isActive) {
            println(&quot;작업 중&quot;)
        }
    }
    delay(100L)
    whileJob.cancel()
}</code></pre>
<p>이 방법을 사용하면 코루틴이 잠시 멈추지도 않고 스레드 사용을 양보하지도 않으며 계속해서 작업을 이어갈 수 있어 효율적임</p>
<p>코루틴 내부의 작업이 일시 중단 지점 없이 계속된다면 명시적으로 코루틴이 취소됐는지 확인하는 코드를 넣어줌으로써 코루틴을 취소할 수 있도록 만들어줘야 함</p>
<p>그렇지 않으면 코루틴 취소가 동작하지 않을 수 있음</p>
<hr>
<h1 id="6-코루틴의-상태와-job의-상태-변수">6. 코루틴의 상태와 Job의 상태 변수</h1>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/98108001-0e75-4b5c-b79a-748d1e52fc3b/image.jpg" alt=""></p>
<p>코루틴은 총 6개의 상태를 가짐</p>
<ul>
<li>생성(New)<ul>
<li>코루틴 빌더를 통해 코루틴을 생성하면 기본적으로 생성 상태에 놓이며 자동으로 실행 상태로 넘어감</li>
<li>만약 실행 상태로 자동으로 넘어가지 않도록 하고 싶다면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 코루틴을 만들면 됨</li>
</ul>
</li>
<li>실행 중 (Active)<ul>
<li>코루틴이 생성되고 자동으로 진입하는 상태</li>
<li>코루틴이 실행중일 때 뿐만 아니라 실행된 후애 일시 중단된 때도 실행중 상태로 간주함</li>
</ul>
</li>
<li>실행 완료 중 → 구조화된 동시성 (7장에서 다룸)</li>
<li>실행 완료 (Completed)<ul>
<li>코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어감</li>
</ul>
</li>
<li>취소 중 (Canceling)<ul>
<li>Job.cancel() 등으로 코루틴에 취소 요청이 발생했을 경우 취소 중 상태로 넘어감</li>
<li>아직 취소된 상태가 아니기 때문에 코루틴은 계속해서 실행됨</li>
</ul>
</li>
<li>취소 완료 (Cancelled)<ul>
<li>코루틴의 취소 확인 시점에 취소가 확인 된 경우 취소 완료 상태가 됨</li>
<li>코루틴이 더 이상 실행되지 않음</li>
</ul>
</li>
</ul>
<p>Job 객체는 코루틴이 어떤 상태에 있는지 나타내는 상태 변수들을 외부로 공개</p>
<p>다만 Job 객체는 코루틴을 추상화한 객체이므로 노출하는 상태 변수들은 코루틴의 상태를 간접적으로만 나타냄</p>
<p>코루틴의 각 상태애서 Job 객체가 노출하는 상태 변수들의 상태 값</p>
<ul>
<li>isActive<ul>
<li>코루틴이 활성화돼 있는지의 여부</li>
<li>활성화 → true / 비활성화 → false</li>
<li>활성화 상태 = 코루틴이 실행된 후 취소가 요청되지 않았거나 실행이 완료되지 않은 상태라는 의미</li>
<li>취소가 요청되었거나 실행이 완료된 상태 → 활성화되지 않은 것으로 봄</li>
</ul>
</li>
<li>isCancelled<ul>
<li>코루틴이 취소 요청됐는지 여부</li>
<li>취소가 요청되는 즉시 true가 반환됨</li>
<li>isCancelled가 true여도 코루틴이 즉시 취소되는 것은 아님</li>
</ul>
</li>
<li>isCompleted<ul>
<li>코루틴 실행이 완료되었는지 여부</li>
<li>코루틴의 모든 코드가 실행 완료 되거나 취소 완료되면 true를 반환</li>
<li>코루틴이 실행중인 상태에서는 false를 반환</li>
</ul>
</li>
</ul>
<h2 id="6-1-생성-상태의-코루틴">6-1. 생성 상태의 코루틴</h2>
<p>코루틴이 생성만 되고 실행되지 않은 상태</p>
<p>생성 상태의 코루틴을 생성하려면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 시작이 적용된 코루틴을 생성해야 함</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch(start = CoroutineStart.LAZY) {
        delay(1000L)
    }
    printJobState(job)
}

/*
Job State
isActive &gt;&gt; false
isCancelled &gt;&gt; false
isCompleted &gt;&gt; false
*/</code></pre>
<ul>
<li>코루틴 생성 후 실행되지 않았으므로 isActive가 false임</li>
<li>취소가 요청되지 않았으므로 isCancelled가 false임</li>
<li>코루틴 실행이 완료되지 않았으므로 isCompleted가 false임</li>
</ul>
<h2 id="6-2-실행-중-상태의-코루틴">6-2. 실행 중 상태의 코루틴</h2>
<p>코루틴 빌더로 코루틴을 생성하면 CoroutineDispatcher에 의해 스레드로 보내져 실행됨</p>
<p>이때 코루틴이 실행되고 있는 상태를 ‘실행 중’ 상태라고 부름</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch {
        delay(1000L)
    }
    printJobState(job)
}

/*
Job State
isActive &gt;&gt; true
isCancelled &gt;&gt; false
isCompleted &gt;&gt; false
*/</code></pre>
<ul>
<li>코루틴이 실행된 후 취소되거나 완료되지 않았으므로 isActive가 true임</li>
<li>코루틴에 취소가 요청되지 않았으므로 isCancelled가 false임</li>
<li>코루틴이 실행 완료되지 않았으므로 isCompleted가 false임</li>
</ul>
<h2 id="6-3-실행-완료-상태의-코루틴">6-3. 실행 완료 상태의 코루틴</h2>
<p>실행 중인 코루틴이 모두 정상적으로 실행돼 실행 완료되면 코루틴은 실행 완료 상태로 변경됨</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch {
        delay(1000L)
    }
    delay(2000L)
    printJobState(job)
}

/*
Job State
isActive &gt;&gt; false
isCancelled &gt;&gt; false
isCompleted &gt;&gt; true
*/</code></pre>
<ul>
<li>코루틴이 실행 완료돼 활성화된 상태가 아니므로 isActive는 false임</li>
<li>취소가 요청되지 않고 정상적으로 실행 완료됐으므로 isCancelled는 false임</li>
<li>실행이 완료된 것이므로 isCompleted는 true임</li>
</ul>
<h2 id="6-4-취소-중인-코루틴">6-4. 취소 중인 코루틴</h2>
<p>취소가 요청됐으나 취소되지 않은 상태인 ‘취소 중’ 상태</p>
<p>단순히 취소 요청을 하면 코루틴은 곧바로 취소 중 상태에서 취소 완료 상태로 변환하므로 취소르르 확인할 수 있는 시점이 없는 코루틴을 생성하고 취소를 요청해야 취소 중 상태를 확인할 수 있음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch(Dispatchers.Default) {
        while(true) {
            //
        }
    }
    job.cancel()
    printJobState(job)
}

/*
Job State
isActive &gt;&gt; false
isCancelled &gt;&gt; false
isCompleted &gt;&gt; true
*/</code></pre>
<ul>
<li>코루틴이 취소 요청됐으므로 화성화돼 있지 않다고 판단해 isActive가 false임</li>
<li>코루틴 취소가 요청 됐으므로 isCancelled는 true임</li>
<li>취소가 완료되지 않았으므로 isCompleted가 false임</li>
</ul>
<p>여기서 중요한 점이 취소가 요청되면 실제로는 코드가 실행 중이더라도 코루틴이 활성화된 상태로 보지 않는다는 점임</p>
<h2 id="6-5-취소-완료된-코루틴">6-5. 취소 완료된 코루틴</h2>
<p>코루틴은 코루틴 취소가 요청되고 취소 요청이 확인되는 시점에 취소가 완료됨</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val job: Job = launch {
        delay(5000L)
    }
    job.cancelAndJoin()
    printJobState(job)
}

/*
Job State
isActive &gt;&gt; false
isCancelled &gt;&gt; true
isCompleted &gt;&gt; true
*/</code></pre>
<ul>
<li>취소가 완료돼 코루틴이 활성화되어 있지 않으므로 isActive가 false임</li>
<li>취소 요청 후 취소가 완료되었으므로 isCancelled와 isCompleted가 true임</li>
</ul>
<blockquote>
<p>코루틴 라이브러리 1.7.2 버전을 기준으로 Job 구현체의 toString 함수를 통해 코루틴의 상태값이 toString 문자열에 포함되도록 만들어져있음</p>
</blockquote>
<hr>
<h1 id="7-요약">7. 요약</h1>
<ul>
<li>runBlocking, launch 함수 → 코루틴 빌더 함수의 일종</li>
<li>launch 함수를 호출하면 Job 객체가 만들어져 반환되며, Job 객체는 코루틴의 상태를 추적하고 제어하는데 사용됨</li>
<li>Job 객체의 Join 함수를 호출하면 함수를 호출한 코루틴이 Job 객체의 실행이 완료될 때까지 일시 중단됨</li>
<li>JoinAll 함수를 통해 여러개의 코루틴이 실행 완료될 때까지 대기할 수 있음</li>
<li>Job 객체의 cancel 함수를 사용해 코루틴의 취소를 요청할 수 있음</li>
<li>cancel 함수를 호출한다고 코루틴이 즉시 취소되는 것이 아닌 코루틴의 취소 플래그의 상태가 바뀌고, 취소가 확인될 때 취소됨</li>
<li>cancel 함수를 호출하더라도 코루틴이 취소를 확인할 수 없는 상태에서는 계속해서 실행될 수 없음</li>
<li>delay, yield 함수와 isActive 프로퍼티 등을 호라용해 코루틴이 취소를 확인할 수 있도록 만들 수 있음</li>
<li>코루틴은 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료의 상태를 가짐</li>
<li>Job 객체는 isActive, isCancelled, isCompleted 프로퍼티를 통해 코루틴의 상태를 나타냄</li>
<li>isActive는 생성 상태일 때는 false, 코루틴이 실행되면 true로 바뀜, cancel 함수를 통해 취소 요청 또는 실행 완료시 false로 다시 바뀜</li>
<li>isCancelled는 코루틴이 취소 중이거나 취소 완료되었을 때만 true가 됨</li>
<li>isCompleted는 코루틴이 취소 완료되었거나 실행 완료되었을 때만 true가 됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 레벨 2 회고]]></title>
            <link>https://velog.io/@first_woosun/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-2-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@first_woosun/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-2-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 29 Jun 2026 07:23:51 GMT</pubDate>
            <description><![CDATA[<h1 id="명확한-의사-전달의-중요성을-배운-미니-프로젝트">명확한 의사 전달의 중요성을 배운 미니 프로젝트</h1>
<h2 id="뭐-하다-실수를-했나">뭐 하다 실수를 했나?</h2>
<p>안드로이드 트랙에선 4주간 우아한테크코스에서 정해준 미션을 수행한 이후, 미니 프로젝트를 시작했다.</p>
<p>미니 프로젝트는 지금까지 경험해왔던 우아한테크코스의 미션과 궤를 달리했다.</p>
<p>지금까지의 미션이 요구사항과 기능 구현 목록, 목표가 정해져 있었다면, </p>
<p>미니 프로젝트는 그 어떠한 것도 정해주지 않고 오롯이 크루들의 힘으로 해쳐나가야 하는 미션이었다.</p>
<p>문제 정의, 가설 수립/검증, 솔루션 설계, 플랫폼/기술 스택 결정 등 정말 많은 것을 코치의 도움 없이 해내야 했다.</p>
<p>지금까지의 활동과 다른 성격의 활동을 진행하면서 기존 미션에서 배우는 지식들과는 성격을 달리하는 지식을 많이 배웠다.</p>
<p>그중, 가장 기억에 남는 배움이 하나 있다. 그 배움은 &quot;실수&quot;와 &quot;시행착오&quot;에서 비롯되었다.</p>
<h2 id="무슨-실수를-했나">무슨 실수를 했나?</h2>
<p>대제목에서 알 수 있듯이 문서화와 관련된 실수와 시행착오가 있었다.</p>
<p>우리 프로젝트 팀이 정의한 문제 상황은 다음과 같다.</p>
<blockquote>
<p>&quot;키보드 초보자의 입장에선 키보드를 구매하기 위해 알아야 할 정보가 너무 많고, 파편화되어 있다.&quot;</p>
</blockquote>
<p>이 문제 상황에 대해 우리 팀이 내놓은 솔루션은 </p>
<p>&quot;어려운 키보드 용어를 쉽게 풀어서 설명하고, 여러 곳에 파편화되어 있는 정보들을 한 화면에서 확인할 수 있는 웹 서비스&quot;였다.</p>
<p>문제 상황도 명확했고, 솔루션도 명확했다. 이때까지만 해도 아무 문제 없는 듯 보였다.</p>
<p>문제는 리뷰어의 피드백에 대해 팀원들과 의논하는 자리에서 발생했다.</p>
<p>미니 프로젝트에선 한 주마다 그 주의 의사 결정 과정과 결과를 배정된 리뷰어에게 공유한다.</p>
<p>이를 통해 리뷰어는 우리의 결정에서 생각해 봐야 할 점, 경계헤야할 점 등에 대해 피드백을 진행해준다.</p>
<p>이때 우리가 받았던 피드백 중 다음의 내용이 있었다.</p>
<blockquote>
<p>여전히 취향의 상세한 내부를 더 파 봐야한다고 생각해요.<br>누구는 고급 축이 비싸서 안 고를 수도, 누구는 키캡이 너무 비싸서 안 고를 수도, 누구는 키보드 기판이 비싸서 안 고를 수도 있겠어요.</p>
<p>오히려 가설이 왜 이렇게 잘 맞나? 이게 맞나? 라고 생각해보면 좋겠어요. 지금 취향 선택에 대한 가설을 잘 세웠는가?</p>
<p>제 생각엔, 이런 가설이 검증됐다는 느낌이 강하게 들어요.<br>배고프면? -&gt; 밥을 먹는다. 누가 굶어죽겠어? OK 밥 앱을 만들자.</p>
</blockquote>
<p>해당 피드백은 우리가 진행한 설문조사의 결과에 대한 피드백이다.</p>
<p>우리가 진행한 설문 조사의 질문들 중 &quot;키보드를 구매할 때, 단순한 입력 기능보다, 취향(타건감, 무게, 키 배열)을 더 중요하게 고려하시나요?&quot;라는 내용이 있었다.</p>
<p>이 질문은 &quot;너무 자명한 사실에 대해 확인하는 것 아니냐&quot;는 점을 꼬집으면서도 우리에게 풀리지 않는 의문점을 남겼다.</p>
<blockquote>
<p>취향을 더 깊게 파봐야 한다고? 왜지?</p>
</blockquote>
<p>설문조사를 진행할 때 &quot;키보드를 구매할 때 어떤 요소를 주로 고려하는가?&quot;에 대해 수집했다면,</p>
<p>우리 서비스가 사용자에게 제공할 선택지를 더 세분화하고 사용자가 실제로 겪는 불편을 더 확실하게 확인할 수 있었을 것이다.</p>
<p>다만 우리가 의문점을 느끼는 부분은 &quot;우리 서비스가 사용자의 취향 파악을 이렇게 깊게 할 이유가 있나?&quot;였다.</p>
<p>우리 서비스가 제공하고자 하는 솔루션이 &quot;사용자의 취향을 입력받고 그에 맞는 키보드의 특지을 사용자가 이해하기 쉬운 언어로 풀고, 여러 곳에 흩어져 있는 정보를 취합해 주자&quot;였기 때문에</p>
<p>사용자의 취향을 입력받고 그 안에서 정보를 최대한 추출해 조건에 부합하는 목록을 생성하면 됐었다.</p>
<p>하지만 이 피드백의 성격은 우리가 생각하는 방향성과 많이 다르다고 느꼈다.</p>
<p>이 피드백에 대해 팀원들과 토론하는 데 꼬박 6시간을 할애했고, 결론이 나지 않았던 우리는 결국 리뷰어에게 연락을 취해 피드백의 의도를 물었다.</p>
<p>그때 받았던 답변은 이랬다.</p>
<blockquote>
<p>제가 생각하기에는 진짜 만들고 싶은 게 뭔지 정의하는 게 더 중요해보여요</p>
<p>취향에 따라 키보드를 쉽게 선택할 수 있게 도와주는 앱 -&gt; 너무 범용적이란 생각이 들어요. 취향은 굉장히 매니악한 분야라고 생각해서요</p>
<p>먼저 누가 타겟층인지도 잘 모르겟어요.</p>
<p>초보자들이 자신의 취향을 찾게끔 도와주는 앱인지?</p>
<p>초고수 매니악들이 내 취향을 알고 거기에 완전 맞춤형의 더 많은 선택지들을 알아서 찾아주는 것인지?</p>
<p>이 두 가지 타겟만 봐도 이미 너무 방향이 다르다고 생각해요</p>
<p>무엇을 해결할 것인가를 완전 좁혀서 생각해보면 좋겠어요</p>
</blockquote>
<p>이 답변에서 미루어 보았을 때 우리 서비스의 타겟층, 문제 솔루션 등이 리뷰어에게 제대로 전달되지 않았다는 걸 알 수 있었다.</p>
<h2 id="그래서-뭘-배웠나">그래서 뭘 배웠나?</h2>
<p>가장 크게 배운 점은 <strong>“팀 내부에서 자명하게 여겨지는 내용이라도 외부에 공유할 때는 우리의 의도와 해결 방안을 명확하게 전달해야 한다”</strong>는 것이다.</p>
<p>앞서 설명한 일련의 사건으로 인해 우리 팀은 그날 하루의 대부분을 리뷰어의 피드백 의도를 파악하고 답변을 준비하는 데 사용했다. </p>
<p>처음부터 우리가 해결하려는 문제와 제안한 솔루션을 명확하게 전달했다면 이처럼 많은 시간을 들이지 않았을 것이다.</p>
<p>의도가 충분히 전달되지 않아 같은 내용을 다시 설명해야 했고, 그 과정에서 불필요한 시간과 노력이 발생했다. </p>
<p>이런 문제가 또 발생하지 않도록 하려면 어떻게 해야할지 고민해봤다.</p>
<p>일단 문서화를 더 구체적이고 명확히 하는 것 부터 시작하려 한다.</p>
<p>미니 프로젝트를 진행하면서 중간 산출물과 문서화를 신경쓰려 했지만 잘 이뤄지지 않았다고 생각한다.</p>
<p>문서화를 더 구체적이고 명확하게 했다면 리뷰어에게 주간 보고를 올릴때 더 명확하게 우리의 의사를 전달할 수 있었을 것 같다.</p>
<p>이번 경험을 통해 명확한 문서화는 단순히 내용을 기록하는 것을 넘어, 의사소통 과정의 오해와 반복 작업을 줄이는 데에도 큰 영향을 미친다는 점을 체감했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter.3 Coroutine Dispatcher]]></title>
            <link>https://velog.io/@first_woosun/Chapter.3-Coroutine-Dispatcher</link>
            <guid>https://velog.io/@first_woosun/Chapter.3-Coroutine-Dispatcher</guid>
            <pubDate>Thu, 25 Jun 2026 05:05:23 GMT</pubDate>
            <description><![CDATA[<h2 id="3장에서-다루는-내용">3장에서 다루는 내용</h2>
<ul>
<li>코루틴 디스패처 객체의 역할</li>
<li>제한된 디스패처와 무제한 디스패처</li>
<li>제한된 디스패처 생성</li>
<li>코루틴 디스패처로 코루틴 실행하기</li>
<li>코루틴 라이브러리에 미리 정의된 디스패처의 종류와 사용처</li>
</ul>
<h1 id="3-1-coroutinedispatcher란-무엇인가">3-1. CoroutineDispatcher란 무엇인가?</h1>
<p><strong>Dispatcher</strong></p>
<ul>
<li>Dispatch → 보내다</li>
<li>Dispatcher → 무언가를 보내는 주체</li>
</ul>
<p>따라서 CoroutineDispatcher → 코루틴을 보내는 주체</p>
<p><strong>CoroutineDispatcher는 코루틴을 스레드로 보냄</strong></p>
<ul>
<li>코루틴은 일시중단 가능한 “작업”이기 때문에 스레드가 있어야 실행할 수 있음</li>
<li>코루틴을 스레드로 보내는데 사용할 수 있는 스레드나 스레드 풀을 가짐</li>
<li>코루틴을 실행 요청한 스레드에서 코루틴이 실행되도록 만들 수 있음</li>
</ul>
<h2 id="3-1-1-coroutinedispatcher의-동작-살펴보기">3-1-1. CoroutineDispatcher의 동작 살펴보기</h2>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/ca9be8ca-d84d-4e93-b90f-c1176aa70b52/image.jpg" alt=""></p>
<ul>
<li>2개의 스레드로 구성된 스레드풀을 갖는 코루틴 디스패처 객체</li>
<li>실행돼야 하는 작업을 저장하는 작업 대기열을 가짐</li>
<li>새로운 코루틴 실행 요청이 들어오면 해당 작업을 작업 대기열에 적재</li>
<li>코루틴을 실행할 수 있는 스레드가 있으면 코루틴을 해당 스레드로 보내 실행시킴</li>
<li>사용 가능한 스레드가 없다면 사용 가능한 스레드가 생길 때까지 스레드에 작업을 보내지 않고 작업 대기열에서 대기</li>
<li>사용 가능한 스레드가 생기면 대기열에서 대기중인 코루틴을 스레드로 보냄</li>
</ul>
<p>코루틴 디스패처 객체는 자신에게 실행 요청된 코루틴을 우선 작업 대기열에 적재한 후 사용할 수 있는 스레드가 생기면 스레드로 보내는 방식으로 동작</p>
<h2 id="3-1-2-coroutinedispatcher의-역할">3-1-2. CoroutineDispatcher의 역할</h2>
<p>코루틴 디스패처는 코루틴의 실행을 관리하는 주체</p>
<p>자신에게 실행 요청된 코루틴들을 작업 대기열에 적재하고 자신이 사용할 수 있는 스레드가 새로운 작업을 실행할 수 있는 상태라면 스레드로 코루틴을 보내 실행할 수 있게 만드는 역할</p>
<blockquote>
<p>코루틴 디스패처는 코루틴 실행요청이 들어오면 일반적으로 작업 대기열에 적재한 후에 스레드로 보냄
하지만 실행 옵션에 따라 작업 대기열에 적재되지 않고 즉시 실행될 수도 있음
작업 대기열이 없는 코루틴 디스패처 구현체도 있으니 이는 매우 예외적인 경우임
이는 무제한 디스패처에서 별도로 다룸</p>
</blockquote>
<h1 id="3-2-제한된-디스패처와-무제한-디스패처">3-2. 제한된 디스패처와 무제한 디스패처</h1>
<p>코루틴 디스패처는 2가지 종류가 있음</p>
<ul>
<li>제한된 디스패처 → 사용할 수 있는 스레드나 스레드풀이 제한된 디스패처</li>
<li>무제한 디스패처 → 사용할 수 있는 스레드나 스레드풀이 제한되지 않은 디스패처</li>
</ul>
<p>일반적으로 코루틴 디스패처는 객체별로 어떤 작업을 처리할지 미리 역할을 부여하고 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문에 대부분의 디스패처는 제한된 디스패처임</p>
<blockquote>
<p>입출력 작업 → 입출력 작업용 콜틴 디스패처 객체에 실행을 요청
CPU 연산 작업 → CPU 연산 작업용 코루틴 디스패처 객체에 실행을 요청</p>
</blockquote>
<p>무제한 디스패처라고 해서 코루틴이 아무 스레드에서나 실행되는 것은 아님</p>
<ul>
<li>실행 요청된 코루틴이 이전 코드가 실행되던 스레드에서 계속해서 실행되도록 함</li>
<li>실행되는 스레드가 매번 달라질 수 있고 특정 스레드로 제한되어 있지 않기 때문에 무제한 디스패처라고 불림</li>
<li>11-3에서 자세히 다룰 예정</li>
</ul>
<h1 id="3-3-제한된-디스패처-생성하기">3-3. 제한된 디스패처 생성하기</h1>
<p>코루틴 라이브러리에서 제공하는 제한된 디스패처 생성 함수를 다룸</p>
<h2 id="3-3-1-단일-스레드-디스페처">3-3-1. 단일 스레드 디스페처</h2>
<p>단일 스레드 디스패처 → 사용할 수 있는 스레드가 하나 </p>
<p>newSingleThreadContext() 함수를 통해 만들 수 있음</p>
<ul>
<li>이 함수는 문자열 타입의 name을 인자로 받음</li>
<li>name → 디스패처에서 관리하는 스레드 이름</li>
<li>반환 타입은 CoroutineDispatcher임</li>
</ul>
<pre><code class="language-kotlin">val dispatcher : CoroutineDispatcher = newSingleThreadContext(name = &quot;SingleThread&quot;)</code></pre>
<p>이 함수를 통해 만들어진 디스패처는 작업 대기열과 스레드 하나로 구성된 스레드풀을 사용할 수 있음</p>
<p>이 스레드의 이름은 인자로 넘긴 “SingleThread”가 됨</p>
<h2 id="3-3-2-멀티-스레드-디스패처-만들기">3-3-2. 멀티 스레드 디스패처 만들기</h2>
<p>코루틴 라이브러리의 newFixedThreadPoolContext() 함수를 통해 만들 수 있음</p>
<ul>
<li>스레드의 개수 (nThreads)와 스레드의 이름 (name)을 매개변수로 받음</li>
<li>만들어지는 스레드들은 인자로 받은 name값 뒤에 ‘-1’부터 시작해 숫자가 하나씩 증가하는 형식으로 이름을 붙임<ul>
<li>Thread-1, Thread-2…</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">val multiThreadDiapatcher: CoroutineDispatcher = newFixedThreadPoolContext(
    nThreads = 2,
    name = &quot;MultiThread&quot;
)</code></pre>
<p>이 함수를 통해 만들어진 디스패처는 작업 대기열과 MultiThread-1, MultiThread-2라는 이름을 가진 스레드 2개가 존재함</p>
<p>이렇게 만들어진 디스패처는 newSingleThreadContext()로 만들어진 디스패처와 모습이 매우 흡사함</p>
<p>newSingleThreadContext()가 내부적으로 newFixedThreadPoolContext()를 사용하도록 구현되어 있기 때문임</p>
<pre><code class="language-kotlin">public fun newSingleThreadContext(name: String): 
ClosableCoroutineDispatcher = 
    newFixedThreadPoolContext(1, name)</code></pre>
<h1 id="3-4-coroutinedispatcher-사용해-코루틴-실행하기">3-4. CoroutineDispatcher 사용해 코루틴 실행하기</h1>
<h2 id="3-4-1-launch의-파라미터로-coroutinedispatcher-사용하기">3-4-1. launch의 파라미터로 CoroutineDispatcher 사용하기</h2>
<ol>
<li><p>단일 스레드 디스패처 사용해 코루틴 실행하기</p>
<p> launch함수를 사용해 만든 코루틴을 특정 코루틴 디스패처 객체에 실행 요청하기 위해서는 launch 함수의 context 인자로 코루틴 디스패처 객체를 넘기면 됨</p>
<pre><code class="language-kotlin"> fun main() = runBlocking&lt;Unit&gt; {
     val dispathcer = newSingleThreadContext(name = &quot;SingleThread&quot;)
     launch(context = dispatcher) {
         println(&quot;[${Thread.currentThread().name}] 실행&quot;)
     }
 }

 // [SingleThread @coroutine#2] 실행</code></pre>
<p> 새로운 스레드의 이름을 SingleThread로 설정했으므로 launch 함수를 통해 생성된 코루틴이 실행되는 스레드가 SingleThread로 나옴</p>
</li>
<li><p>멀티 스레드 디스패처를 사용해 코루틴 실행하기</p>
<p> 단일 스레드 디스패처를 사용하는 방식과 동일</p>
<p> launch 함수의 context 인자로 multiThreadDispatcher를 넘기면 됨</p>
<pre><code class="language-kotlin"> fun main() = runBlocking&lt;Unit&gt; {
     val dispathcer = newFixedThreadPoolContext(
         nThreads = 2,
         name = &quot;SingleThread&quot;
     )
     launch(context = dispatcher) {
         println(&quot;[${Thread.currentThread().name}] 실행&quot;)
     }
     launch(context = dispatcher) {
         println(&quot;[${Thread.currentThread().name}] 실행&quot;)
     }
 }</code></pre>
<p> 단일 스레드 코루틴 디스패처와 마찬가지로 실행을 요청받은 코루틴을 작업 대기열에 적재하고 작업 가능한 스레드에 코루틴을 보내 실행</p>
<p> 다음 작업도 마찬가지로 작업 대기열에 적재 한 후 사용 가능한 스레드에 코루틴을 보내 실행함</p>
<p> 이 때 실행 환경에 따라 각 코루틴이 다른 속도로 처리될 수 있기 때문에 출력되는 스레드 이름이 같을 수 있음</p>
</li>
</ol>
<h2 id="3-4-2-부모-코루틴의-coroutinedispatcher를-사용해-자식-코루틴-실행하기">3-4-2. 부모 코루틴의 CoroutineDispatcher를 사용해 자식 코루틴 실행하기</h2>
<p>코루틴은 <strong>구조화</strong>를 제공해 콜틴 내부에서 새로운 코루틴을 실행할 수 있음</p>
<p>이 때 바깥쪽 코루틴을 <strong>부모 코루틴</strong>, 내부에서 실행되는 코루틴을 <strong>자식 코루틴</strong>이라고 함</p>
<p>구조화는 코루틴을 계층 관계로 만드는 것 뿐 아니라 부모 코루틴의 실행 환경을 자식 코루틴에 전달하는 데도 사용됨</p>
<p>만약 자식 코루틴에 코루틴 디스패처 객체가 설정되지 않았으면 부모 코루틴의 코루틴 디스패처 객체를 사용함</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    val dispathcer = newFixedThreadPoolContext(
        nThreads = 2,
        name = &quot;SingleThread&quot;
    )
    launch(context = dispatcher) { // 부모
        println(&quot;[${Thread.currentThread().name}] 부모 코루틴 실행&quot;)

        launch(context = dispatcher) { // 자식
            println(&quot;[${Thread.currentThread().name}] 자식 코루틴 실행&quot;)
        }

        launch(context = dispatcher) { // 자식
            println(&quot;[${Thread.currentThread().name}] 자식 코루틴 실행&quot;)
        }
    }
}

/*
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[MultiThread-2 @coroutine#3] 자식 코루틴 실행
[MultiThread-1 @coroutine#4] 자식 코루틴 실행
*/</code></pre>
<p>가장 상단의 launch 함수로 생성되는 코루틴이 부모 코루틴이 됨</p>
<p>이 부모 코루틴은 전용 스레드가 2개인 코루틴 디스패처를 사용</p>
<p>부모 코루틴 launch 함수의 람다식 내부에서 2개의 launch 함수를 호출해2개의 코루틴을 추가로 생성하는데 이 때 생성되는 코루틴이 자식 코루틴임 </p>
<p>이 자식 코루틴들에는 별도의 코루틴 디스패처가 설정되어 있지 않기 때문에 부모의 코루틴 디스패처를 사용함</p>
<p>부모 코루틴과 자식 코루틴이 동일한 코루틴 디스패처 객체를 사용하므로 스레드를 공유하는 것을 볼 수 있음</p>
<p>특정 코루틴 디스패처에서 여러 작업을 실행해야 한다면 부모 코루틴에 코루틴 디스패처 객체를 설정하고 그 아래 자식 코루틴을 여러개 생성하면 됨</p>
<h1 id="3-5-미리-정의된-coroutinedispatcher">3-5. 미리 정의된 CoroutineDispatcher</h1>
<h2 id="3-5-1-dispatchersio">3-5-1. Dispatchers.IO</h2>
<p>멀티 스레드 프로그래밍에서 가장 많이 사용되는 작업 → I/O 작업</p>
<ul>
<li>네트워크 통신을 위한 HTTP 요청</li>
<li>DB 작업</li>
</ul>
<p>I/O 작업 여러 개를 동시에 수행하므로 이런 요청을 동시에 수행하기 위해서는 많은 스레드가 필요함</p>
<p>코루틴 라이브러리에서는 입출력 작업을 위해 미리 정의된 Dispatchers.IO를 제공함</p>
<p>Dispatchers.IO가 최대로 사용할 수 있는 스레드의 수 → JVM에서 사용할 수 있는 프로세서 수와 64 중 큰 값으로 설정되어 있음</p>
<p>Dispatchers.IO는 싱글톤 인스턴스이므로 launch 함수의 인자로 곧바로 넘겨줄 수 있음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    launch(Dispatchers.IO) {
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}

// [DefaultDispathcer-worker-1 @coroutine#2] 코루틴 실행</code></pre>
<p>위 코드의 결과를 보면 코루틴이 실행된 스레드의 이름 앞에 DefaultDispatcher-worker가 붙은 것을 알 수 있음</p>
<p>DefaultDispatcher-worker가 붙은 스레드는 코루틴 라이브러리에서 제공하는 공유 스레드풀에 속한 스레드임</p>
<p>Dispatchers.IO는 공유 스레드풀의 스레드를 사용할 수 있도록 구현됐음</p>
<p>공유 스레드풀에 대해서는 추후 더 자세하게 다룰 예정</p>
<h2 id="3-5-2-dispatchersdefault">3-5-2. Dispatchers.Default</h2>
<p>CPU 바운드 작업 → CPU 연산이 필요한 작업</p>
<p>ex) 대용량 데이터 처리</p>
<p>Dispatchers.Default → CPU 바운드 작업이 필요할 때 사용하는 CoroutineDispatcher</p>
<p>Dispatchers.Default도 그 자체로 싱글턴 인스턴스이므로 간단하게 호출할 수 있음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    launch(Dispatchers.Default) {
        println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
    }
}

// [DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행</code></pre>
<h3 id="입출력-작업과-cpu-바운드-작업">입출력 작업과 CPU 바운드 작업</h3>
<p>두 작업의 중요한 차이 → 실행됐을 때 스레드를 지속적으로 사용하는지 여부</p>
<p>일반적으로 입출력 작업은 작업(네트워크 요청, DB 조회 요청 등)을 실행한 후 결과를 반환받을 때까지 스레드를 사용하지 않음</p>
<p>CPU 바운드 작업은 작업을 하는 동안 스레드를 지속적으로 사용</p>
<p>이 차이로 인해 CPU 바운드 작업은 스레드 기반 작업을 사용했을 때와 코루틴을 사용했을 때 효율성에 차이가 발생함</p>
<p>입출력 작업에 코루틴을 사용 → 작업 실행 후 스레드가 대기하는 동안 해당 스레드에서 다른 작업을 동시에 실행할 수 있어 효츌적</p>
<p>CPU 바운드 작업에 코루틴 사용 → 스레드가 지속적으로 사용되기 때문에 스레드 기반 작업을 사용해 실행했을 때와 처리 속도에 차이가 거의 없음</p>
<table>
<thead>
<tr>
<th></th>
<th>I/O 작업</th>
<th>CPU 바운드 작업</th>
</tr>
</thead>
<tbody><tr>
<td>스레드 기반 작업 사용 시</td>
<td>느림</td>
<td>비슷</td>
</tr>
<tr>
<td>코루틴 사용 시</td>
<td>빠름</td>
<td></td>
</tr>
</tbody></table>
<h2 id="3-5-3-limitedparallelism을-사용해-dispatchersdefault-스레드-사용-제한하기">3-5-3. limitedParallelism을 사용해 Dispatchers.Default 스레드 사용 제한하기</h2>
<p>Dispatchers.Default를 사용해 무겁고 오래 걸리는 작업을 처리하면 특정 작업을 위해 Dispatchers.Default의 모든 스레드가 사용될 수 있으며, 특정 연산을 진행하는 동안 Dispatchers.Defualt를 사용하는 다른 작업이 실행되지 못할 수 있음</p>
<p>이를 방지하기 위해 코루틴 라이브러리는 Dispatchers.Default의 일부 스레드만 사용해 특정 연산을 실행할 수 있도록 하는 limitedParallelism 함수를 지원함</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    launch(Dispatchers.Default.limitedParallelism(2)) {
        repeat(10) {
            launch {
                println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
            }
        }
    }
}

/*
[DefaultDispatcher-worker-2 @coroutine#3] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#4] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#5] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#6] 코루틴 실행
.
.
.
[DefaultDispatcher-worker-2 @coroutine#11] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#12] 코루틴 실행
*/</code></pre>
<ul>
<li>Dispatchers.Default.limitedParallelism(2)를 통해 Dispatchers.Default의 여러 스레드 중 2개의 스레드만 사용해 10개의 코루틴을 실행</li>
</ul>
<h2 id="3-5-4-공유-스레드풀을-사용하는-dispatchersio와-dispatchersdefault">3-5-4. 공유 스레드풀을 사용하는 Dispatchers.IO와 Dispatchers.Default</h2>
<p>Dispatchers.IO와 Dispatchers.Default를 사용한 코드에서 발견할 수 있는 공통점 → 코루틴을 실행한 스레드의 이름에 DefaultDispatcher-worker가 표시됨</p>
<p>Dispatchers.IO와 Dispatchers.Default는 코루틴 라이브러리의 공유 스레드풀을 사용한다는 것을 의미</p>
<p>코루틴 라이브러리는 스레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공</p>
<p>이 공유 스레드풀에는 스레드를 무한으로 생성할 수 있음</p>
<p>코루틴 라이브러리는 공유 스레드풀에 스레드를 생성하고 사용할 수 있도록 하는 API를 제공</p>
<p>Dispatchers.IO와 Dispatchers.Default는 모두 위에서 언급한 API를 사용해 구현됐기 때문에 공유 스레드풀을 사용함</p>
<p>다만 공유 스레드풀에서 Dispatchers.IO와 Dispatchers.Default가 사용하는 스레드는 구분됨</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/f328c822-1d8d-44c4-95bb-da85d05f07f5/image.jpg" alt=""></p>
<p>newFixedThreadPoolContext 함수로 만들어지는 디스패처는 자신만 사용할 수 있는 전용 스레드풀을 생성하는 것과 다르게 Dispatchers.IO와 Dispatchers.Default는 공유 스레드풀의 스레드를 사용함</p>
<blockquote>
<h3 id="dispatchersio의-limitedparallelism">Dispatchers.IO의 limitedParallelism</h3>
<p>Dispatchers.Default에 limitedParallelism 함수를 사용하면 스레드풀의 스레드 중 일부만 사용하도록 제한할 수 있음</p>
<p>하지만 Dispatchers.IO의 limitedParallelism은 조금 다르게 동작함</p>
<p>Dispatchers.IO의 limitedParallelism은 공유 스레드풀의 스레드로 구성된 새로운 스레드풀을 만들어내며, 스레드의 수를 제한 없이 만들 수 있음</p>
<pre><code class="language-kotlin">fun main() = runBlocking&lt;Unit&gt; {
    launch(Dispatchers.IO.limitedParallelism(100)) {
        reapeat(200) {
                launch {
                    Thread.sleep(1000L)
                    println(&quot;[${Thread.currentThread().name}] 코루틴 실행&quot;)
                }
        }
    }
}

/*
[DefaultDispatcher-worker-60] 코투린 실행
[DefaultDispatcher-worker-74] 코투린 실행
[DefaultDispatcher-worker-68] 코투린 실행
.
.
.
[DefaultDispatcher-worker-85] 코투린 실행
[DefaultDispatcher-worker-49] 코투린 실행
[DefaultDispatcher-worker-98] 코투린 실행
*/</code></pre>
<ul>
<li>Dispatchers.IO.limitedParallelism(100)를 통해 공유 스레드풀 상에서 100개의 스레드로 구성된 스레드풀을 만들어냄</li>
</ul>
<p>Dispatchers.IO의 limitedParallelism함수는 공유 스레드풀 상에서 Dispatchers.IO나 Dispatchers.Default와 관계 없는 스레드로 구성된 스레드풀을 만들어냄</p>
<p>특정 작업이 다른 작업에 영향을 받지 않아야 해, 별도 스레드 풀에서 실행되는 것이 필요할 때 사용</p>
<p>다만 공유 스레드풀에서 새로운 스레드를 만들어내는 작업이라 비용이 비싼 작업이므로 남용하면 안됨</p>
</blockquote>
<h2 id="3-5-5-dispatchersmain">3-5-5. Dispatchers.Main</h2>
<p>UI가 있는 애플리케이션에서 메인 스레드를 사용하기 위해 사용되는 특별한 CoroutineDispatcher 객체</p>
<p>코루틴 라이브러리 추가만으론 사용할 수 없고 별도의 라이브러리(ex. Android)를 추가해야 사용할 수 있음</p>
<p>UI 스레드(Main Thread)를 사용하는 라이브러리가 없는 상태로 Diapatchers.Main을 호출하면 IllegalStateException이 발생함</p>
<p>Dispatchers.Main의 구현체를 제공하는 라이브러리가 없어 생기는 오류임</p>
<h1 id="3-6-3장-요약">3-6. 3장 요약</h1>
<ul>
<li>CoroutineDispatcher 객체는 코루틴을 스레드로 보내 실행하는 객체로 코루틴을 작업 대기열에 적재한 후 사용이 가능한 스레드로 보내 실행</li>
<li>제한된 디스패처 → 사용할 수 있는 스레드가 특정 스레드 또는 스레드풀로 제한됨</li>
<li>무제한 디스패처 → 코루틴을 실행하는 데 사용할 수 있는 스레드가 제한되지 않음</li>
<li>newSingleThreadContext와 newFixedThreadPoolContext 함수를 사용해 제한된 디스패처 객체를 생성할 수 있음</li>
<li>launch 함수로 코루틴을 실행할 때 context 인자로 CoroutineDispatcher 객체를 넘기면 해단 CoroutineDispatcher 객첼르 사용해 코루틴이 실행됨</li>
<li>자식 코루틴은 기본적으로 부모 코루틴의 CoroutineDispatcher 객체를 상속받아 사용함</li>
<li>코루틴 라이브러리는 미리 정의된 Dispatchers.IO, Dispatchers.Default, Dispatchers.Main을 제공함</li>
<li>Dispatchers.IO는 입출력 작업을 위한 디스패처 객체로, 네트워크 요청이나 파일 I/O등에 사용됨</li>
<li>Dispatchers.Default는 CPU 바운드 작업을 위한 디스패처 객체로, 대용량 데이터 처리 등에 사용됨</li>
<li>limitedParallelism() 함수를 사용해 특정 여산을 위해 사용되는 Dispatchers.Default의 스레드 수를 제한할 수 있음</li>
<li>Dispatchers.Default와 Dispatchers.IO는 코루틴 라이브러리에서 제공하는 공유 스레드풀을 사용함</li>
<li>Dispatchers.Main은 메인 스레드에서 실행되어야 하는 작업에 사용되는 디스패처로, 사용하기 위해서는 Dispatchers.Main의 구현체를 제공하는 라이브러리 의존성을 추가해야 함</li>
<li>Diapatchers.Main은 일반적으로 UI가 있는 애플리케이션에서 UI를 업데이트하는  데 사용됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter.1 스레드 기반 작업의 한계와 코루틴의 등장]]></title>
            <link>https://velog.io/@first_woosun/%EC%8A%A4%EB%A0%88%EB%93%9C-%EA%B8%B0%EB%B0%98-%EC%9E%91%EC%97%85%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-%EB%93%B1%EC%9E%A5</link>
            <guid>https://velog.io/@first_woosun/%EC%8A%A4%EB%A0%88%EB%93%9C-%EA%B8%B0%EB%B0%98-%EC%9E%91%EC%97%85%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-%EB%93%B1%EC%9E%A5</guid>
            <pubDate>Tue, 09 Jun 2026 12:58:28 GMT</pubDate>
            <description><![CDATA[<h1 id="다루는-내용">다루는 내용</h1>
<ul>
<li>멀티 스레드 프로그래밍이 어떻게 변화했는지</li>
<li>기존 멀티스레드 프로그래밍의 한계를 어떻게 극복했는지<ul>
<li>JVM의 프로세스와 스레드에 대한 이해 </li>
</ul>
</li>
</ul>
<hr>
<h1 id="1-jvm-프로세스와-스레드">1. JVM 프로세스와 스레드</h1>
<p>일반적인 코틀린 어플리케이션 → main 함수를 통해 생성</p>
<ul>
<li>어플리케이션 실행 시 JVM은 프로세스를 시작하고 메인 스레드를 생성해 main 함수 내부의 코드 실행</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    println(&quot;Hello World!&quot;)
}

// 메인 스레드가 main 함수 내부의 println(&quot;Hello World!&quot;)를 실행 후 더 실행할 코드가 없으므로 종료</code></pre>
<p>메인 스레드 → 프로세스의 시작과 끝은 함께 하는 매우 중요한 역할을 함</p>
<ul>
<li>예외가 발생해 메인 스레드가 종료되면 프로세스도 함께 종료</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    println(&quot;메인 스레드 시작&quot;)
    throw Exception(&quot;Dummy Exception&quot;)
    println(&quot;메인 스레드 종료&quot;)
}

// &quot;메인 스레드 시작&quot;은 출력되지만 메인 스레드에서 발생한 예외로 인해 프로세스가 종료돼
// &quot;메인 스레드 종료&quot;는 출력되지 않음</code></pre>
<ul>
<li>JVM의 프로세스 → 기본적으로 메인 스레드를 단일 스레드로 해서 실행되어 메인 스레드가 종료되면 함께 종료되는 특징을 가짐</li>
</ul>
<blockquote>
<p>다만 메인 스레드가 항상 프로세스와 끝을 함께 하는 것은 아님
JVM의 프로세스는 사용자 스레드가 모두 종료될 때 종료되며, 메인 스레드는 사용자 스레드 중 하나임
만약 멀티 스레드 환경에서 사용자 스레드가 여러 개인 경우 메인 스레드에서 예외가 발생해 전파되더라도 프로세스는 강제 종료되지 않음
(추후 사용자 스레드와 데몬에서 자세히 다룰 예정)</p>
</blockquote>
<hr>
<h1 id="2-단일-스레드의-한계와-멀티-스레드-프로그래밍">2. 단일 스레드의 한계와 멀티 스레드 프로그래밍</h1>
<p>스레드 하나만 사용하는 어플리케이션 → 단일 스레드 어플리케이션</p>
<ul>
<li>main 함수를 통해 애플리케이션을 실행하면 메인 스레드를 단일 스레드로 실행</li>
</ul>
<p>단일 스레드에서 실행되는 어플리케이션은 치명적인 문제가 있음</p>
<h2 id="2-1-단일-스레드-어플리케이션의-한계">2-1. 단일 스레드 어플리케이션의 한계</h2>
<p><strong>스레드 → 하나의 작업을 실행중일 때 다른 작업을 동시에 수행하지 못함</strong></p>
<ul>
<li>메인 스레드도 마찬가지</li>
<li>실행하는 작업이 오래 걸리면 해당 작업이 처리되는 동안 다른 작업을 수행하지 못함 → 응답성 문제 발생 가능</li>
</ul>
<h3 id="단일-스레드의-응답성-문제">단일 스레드의 응답성 문제</h3>
<p><strong>1. 안드로이드 휴대폰에서 동작하는 어플</strong></p>
<ul>
<li><p>안드로이드 어플의 경우 UI를 그리는 작업과 사용자 상호작용 이벤트 처리를 메인 스레드에서 수행함</p>
</li>
<li><p>네트워크 요청 후 응답을 기다리는 작업, 복잡한 연산 작업 등 오래 걸리는 작업을 메인 스레드에서 처리하면 그 동안 UI를 그리는 작업을 할 수 없어 휴대폰이 멈추거나 버벅이는 원인이 됨</p>
<ul>
<li>실제론 일정 시간동안 응답이 없다면 ANR(Android Not Response)이 발생하나, 이는 다루지 않겠음</li>
</ul>
</li>
</ul>
<p><strong>2. 서버 사이드 작업</strong></p>
<ul>
<li>클라이언트로 부터 오래 걸리는 작업 요청이 들어왔을 때 단일 스레드만 사용한다면 요청을 처리하는 속도가 늦어져 응답 속도가 늦어짐</li>
</ul>
<p>*<em>단일 스레드만 사용해 작업할 경우 → 해야 할 작업이 다른 작업에 의해 방해받거나 작업 속도가 느려질 수 있음
*</em></p>
<h2 id="2-2-멀티-스레드-프로그래밍을-통한-단일-스레드의-한계-극복">2-2. 멀티 스레드 프로그래밍을 통한 단일 스레드의 한계 극복</h2>
<p><strong>멀티 스레드 프로그래밍 → 스래드를 여러개 사용해 작업을 처리하는 프로그래밍 기법</strong></p>
<ul>
<li>프로세스 → 멀티 스레드 프로그래밍을 통해 여러개의 스레드로 작업을 실행</li>
<li>각각의 스레드가 한 번에 하나의 작업을 처리할 수 있으므로 여러 작업을 동시에 처리하는 것이 가능해짐</li>
</ul>
<h3 id="멀티-스레드를-통한-단일-스레드의-반응성-문제-해결">멀티 스레드를 통한 단일 스레드의 반응성 문제 해결</h3>
<p><strong>1. 안드로이드 휴대폰에서 동작하는 어플</strong></p>
<ul>
<li>메인 스레드 대신 별도의 스레드가 처리할 수 있도록 만들어 반응성 문제를 해결</li>
<li>오래 걸리는 작업을 백그라운드 스레드에서 처리하도록 만들면 메인 스레드는 오래 걸리는 작업을 처리하지 않아도 되기 때문에 UI가 멈추거나 사용자 입력을 받지 못하는 현상을 방지할 수 있음</li>
</ul>
<p><strong>2. 서버 사이드 작업</strong></p>
<ul>
<li>오래 걸리는 작업이 요총됐을 때 작업을 스레드 간에 독립적인 작은 작업으로 나눈 후 각 작업이 서로 다른 스레드에서 수행되도록 만들면 응답 속도를 높일 수 있음</li>
<li>DB 3개를 조회하는 경우 각 스레드가 하나씩 조회한 후 결과를 병합하면 단일 스레드에 비해 빠르게 처리할 수 있음 <strong>(병렬 처리)</strong></li>
</ul>
<hr>
<h1 id="3-코루틴이-등장하기-이전의-멀티-스레드-프로그래밍-스레드-스레드풀">3. 코루틴이 등장하기 이전의 멀티 스레드 프로그래밍 (스레드, 스레드풀)</h1>
<p>말티 스레드 프로그래밍 → 계속 변화해옴 (이전 방식의 한계 극복을 위해)</p>
<p>코루틴 → 멀티 스레드 프로그래밍의 한계를 극복하기 위해 등장</p>
<p>따라서 멀티 스레드 프로그래밍의 변천사를 추적하는 것이 좋음</p>
<h2 id="3-1-thread-클래스를-사용하는-방법과-한계">3-1. Thread 클래스를 사용하는 방법과 한계</h2>
<h3 id="1-thread-클래스를-사용해-스레드-다루기">1. Thread 클래스를 사용해 스레드 다루기</h3>
<pre><code class="language-kotlin">class ExampleThread : Thread() {
    override fun run() {
        println(&quot;${Thread.currentThread().name} 새로운 스레드 시작&quot;)
        Thread.sleep(2000L)
        println(&quot;${Thread.currentThread().name} 새로운 스레드 종료&quot;)
    }
}
// [현재 작업중인 스레드 이름] 새로운 스레드 시작
// (2초 대기)
// [현재 작업중인 스레드 이름] 새로운 스레드 종료

fun main() {
    println(&quot;${Thread.currentThread().name} 메인 스레드 시작&quot;)
    ExampleThread().start()
    Thread.sleep(1000L)
    println(&quot;${Thread.currentThread().name} 메인 스레드 종료&quot;)
}

// 결과: 
// [main] 메인 스레드 시작
// [Thread-0] 새로운 스레드 시작
// [main] 메인 스레드 종료
// [Thread-1] 새로운 스레드 종료 </code></pre>
<ul>
<li>메인 스레드와 새로운 스레드의 로그가 섞여 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/51fc4c5a-8304-4d3a-9d2b-3351c5419173/image.jpg" alt=""></p>
<pre><code>1. “[main] 메인 스레드 시작” 문구 출력
2. 새로운 스레드가 생성되고 “[Thread-0] 새로운 스레드 시작” 문구 출력
3. 1초 뒤 “[main] 메인 스레드 종료” 문구 출력
4. 다시 1초 뒤 “[Thread-0] 새로운 스레드 종료” 문구 출력</code></pre><blockquote>
<h3 id="사용자-스레드와-데몬-스레드">사용자 스레드와 데몬 스레드</h3>
<p>아까 위에서 “JVM 프로세스는 일반적으로 메인 스레드와 함께 종료된다”라고 했었다. 하지만 Thread 클래스의 예시를 보면 메인 스레드가 먼저 종료되었음에도 Thread-0이 여전히 살아남아 작업을 이어갔다.
이는 메인 스레드와 Thread-0 둘 다 사용자 스레드이기 때문이다.</p>
<p>JVM은 스레드를 사용자 스레드와 데몬 스레드로 구분한다.</p>
<p><strong>사용자 스레드 → 우선도가 높은 스레드</strong></p>
<p><strong>데몬 스레드 → 우선도가 낮은 스레드</strong></p>
<p>JVM 프로세스가 종료되는 시점은 우선도가 높은 사용자 스레드가 모두 종료될 때이다.</p>
<p>단일 스레드 프로그래밍의 경우 메인 스레드만 사용자 스레드이기 때문에 메인 스레드가 종료될 때 JVM 프로세스도 종료되었다.</p>
<p>하지만 멀티 스레드를 사용하는 프로세스에서는 스레드 중 사용자 스레드가 모두 종료되는 시점에 프로세스가 종료된다.</p>
<p>Thread 클래스를 상속한 클래스로 생성한 스레드는 기본적으로 사용자 스레드로 생성된다. 따라서 위의 예시에서 생성된 Thread-0은 사용자 스레드로 생성된다.</p>
<p>사용자 스레드로 생성되었기 때문에 메인 스레드가 종료되었음에도 살아남아서 작업을 이어갔던 것이다.</p>
<p>만약 Thread 클래스를 통해 데몬 스레드를 생성하고 싶다면 isDeamon = true 속성을 적용해주면 된다.</p>
<pre><code class="language-kotlin">ExampleThread().apply {
    isDeamon = true,
}.start()</code></pre>
<p>위에서 작성했던 예시를 데몬 스레드로 생성하면 다음의 코드와 같이 작성할 수 있다.</p>
<pre><code class="language-kotlin">fun main() {
    println(&quot;${Thread.currentThread().name} 메인 스레드 시작&quot;)

    ExampleThread().apply{
        isDeamon = true,
    }.start()

    Thread.sleep(1000L)
    println(&quot;${Thread.currentThread().name} 메인 스레드 종료&quot;)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/c26d141b-9dea-47ab-8856-7cc8477d1a7b/image.jpg" alt=""></p>
<p>Thread-0이 데몬 스레드로 생성되었기 때문에 메인 스레드가 종료되면 프로세스도 종료되어 Thread-0이 실행중에 강제로 종료된다.</p>
<p>데몬 스레드는 중요한 스레드가 아니기 때문에 강제 종료되더라도 프로세스가 정상 종료된다.</p>
</blockquote>
<h3 id="2-thread-클래스를-직접-다루는-방법의-한계">2. Thread 클래스를 직접 다루는 방법의 한계</h3>
<p><strong>1. Thread 클래스를 상속한 클래스를 인스턴스화 해 실행할 때마다 매번 새로운 스레드가 생성됨</strong></p>
<ul>
<li>스레드 → 생성비용이 비쌈</li>
<li>매번 새로운 스레드를 생성하는 것은 성능적으로 좋지 않음</li>
</ul>
<p><strong>2. 스레드 생성과 관리에 대한 책임이 개발자에게 있음</strong></p>
<ul>
<li>프로그램의 복잡성이 증가함</li>
<li>실수로 인해 오류나 메모리 누수를 발생시킬 가능성이 증가함</li>
</ul>
<p>이런 문제들을 해결하려면 한 번 생성한 스레드를 간편하게 재사용할 수 있어야 하고, 스레드의 관리를 구축한 시스템에서 책임질 수 있도록 헤야함 </p>
<p>이런 역할을 위해 Executor 프레임워크가 만들어졌음</p>
<h2 id="3-2-executor-프레임워크를-통해-스레드풀-사용하기">3-2. Executor 프레임워크를 통해 스레드풀 사용하기</h2>
<p><strong>Executor 프레임워크 → 개발자의 스레드 관리 부담 문제 해결과 생성된 스레드 재사용성을 높이기 위해 등장</strong></p>
<ul>
<li>스레드 생성/관리를 위해 <strong>스레드풀</strong> 개념 도입</li>
<li>스레드풀 → 스레드의 집합</li>
</ul>
<p><strong>스레드풀을 관리하고 사용자로부터 요청받은 작업을 각 스레드에 할당하는 시스템을 더한 것이 Executor 프레임워크임</strong></p>
<ul>
<li>작업 처리를 위해 스레드풀을 미리 생성해 놓고 작업을 요청 받으면 쉬고 있는 스레드에 작업을 분배</li>
<li>각 스레드는 작업을 끝내더라도 스레드를 종료하지 않고 다음 작업이 들어오면 재사용됨</li>
</ul>
<p><strong>스레드풀에 속한 스레드의 생성과 관리 및 분배에 대한 책임을 Executor 프레임워크가 담당</strong></p>
<ul>
<li>개발자는 스레드 관리를 직접 하지 않고 아래의 작업만 수행하면 됨<ul>
<li>스레드풀에 속할 스레드의 개수 설정</li>
<li>스레드풀을 관리하는 시스템에 작업을 제출</li>
</ul>
</li>
</ul>
<h4 id="3-2-1-executor-프레임워크-사용해-보기">3-2-1. Executor 프레임워크 사용해 보기</h4>
<p>Executor 프레임워크에서 사용자가 사용할 수 있는 함수는 크게 2가지임</p>
<p><strong>1. 스레드풀을 생성하고 생성된 스레드풀을 관리하는 객체를 반환받는 함수</strong>
<strong>2. 스레드풀을 관리하는 객체에 작업을 제출하는 함수</strong></p>
<pre><code class="language-kotlin">fun main() {
    val startTime = System.currentTimeMillis()
    val executorService: ExecutorService = Executors.newFixedThreadPool(2)

    // 작업1 제출
    executorService.submit {
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업 1 시작&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업 1 완료&quot;)
    }

    // 작업2 제출
    executorService.submit {
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업 2 시작&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업 2 완료&quot;)
    }

    executorService.shutdown() // 제출된 작업이 모두 끝난 뒤에는 ExecutorService가 종료될 수 있도록 shutdown 함수를 호출
}

fun getElapsedTime(startTime: Long): String = 
    &quot;지난 시간: ${System.currentTimeMiilis() - startTime}ms&quot;

/*
[pool-1-thread-1][지난 시간: 4ms] 작업1 시작
[pool-2-thread-1][지난 시간: 4ms] 작업2 시작
[pool-1-thread-1][지난 시간: 1009ms] 작업1 완료
[pool-2-thread-1][지난 시간: 1009ms] 작업2 완료
*/

// 서로 다른 스레드에서 실행되기 때문에 출력 순서, 사용 스레드는 다를 수 있음</code></pre>
<ul>
<li>작업1과 작업2는 각각 서로 다른 스레드에서 실행됐음을 알 수 있음</li>
<li>종료 시간을 통해 두 작업이 병렬로 실행됐음을 알 수 있음</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val startTime = System.currentTimeMillis()
    val executorService: ExecutorService = Executors.newFixedThreadPool(2)

    // 작업1 제출
    executorService.submit {
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업1 시작&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업1 완료&quot;)
    }

    // 작업2 제출
    executorService.submit {
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업2 시작&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업2 완료&quot;)
    }

    // 작업3 제출
    executorService.submit {
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업3 시작&quot;)
        Thread.sleep(1000L)
        println(&quot;[${Thread.currentThread().name()}][${getElapsedTime(startTime)}] 작업3 완료&quot;)
    }

    executorService.shutdown() // 제출된 작업이 모두 끝난 뒤에는 ExecutorService가 종료될 수 있도록 shutdown 함수를 호출
}

fun getElapsedTime(startTime: Long): String = 
    &quot;지난 시간: ${System.currentTimeMiilis() - startTime}ms&quot;

/*
[pool-1-thread-1][지난 시간: 4ms] 작업1 시작
[pool-2-thread-1][지난 시간: 4ms] 작업2 시작
[pool-1-thread-1][지난 시간: 1009ms] 작업1 완료
[pool-2-thread-1][지난 시간: 1011ms] 작업2 완료
[pool-1-thread-1][지난 시간: 1012ms] 작업3 완료
[pool-1-thread-1][지난 시간: 2016ms] 작업3 완료
*/</code></pre>
<ul>
<li>작업3이 추가될 경우 작업1과 작업2는 동시에 실행되지만 작업 3은 1이 완료된 후에 실행되는 것을 알 수 있음</li>
<li>작업3이 실행 요청됐을 때 스레드풀에 있느 ㄴ스레드 2개가 이미 작업1과 작업2를 처리하는 중이었기 때문임</li>
</ul>
<h4 id="3-2-2-executorservice-내부-구조와-동작">3-2-2. ExecutorService 내부 구조와 동작</h4>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/03020827-db95-4bf0-a4af-39908d2107b6/image.jpg" alt=""></p>
<ul>
<li>작업이 제출되면 작업 대기열에 적재됨</li>
<li>작업 대기열에 들어간 작업은 스레드로 할당되기를 기다림</li>
<li>스레드풀에 쉬고있는 스레드가 있다면 작업을 할당함</li>
<li>모든 스레드에 작업이 할당되어 있을 때 새로운 작업이 작업 대기열에 들어오면 스레드의 작업이 끝날 때까지 작업 대기열에서 대기함</li>
</ul>
<p>위의 일련의 동작을 개발자는 전혀 신경쓰지 않아도 됨</p>
<p>스레드를 분배하는 일은 ExecutorService 객체가 알아서 함</p>
<h4 id="3-2-3-executor-프레임워크의-의의와-한계">3-2-3. Executor 프레임워크의 의의와 한계</h4>
<p>Executor 프레임워크는 개발자의 스레드 관리 부담을 해결하고 스레드 재사용을 편하게 할 수 있도록 만들었다는 점에서 혁신적임</p>
<p>하지만 스레드 블로킹이라는 문제가 존재함</p>
<h4 id="스레드-블로킹">스레드 블로킹</h4>
<p><strong>스레드가 아무것도 하지 못하고 사용될 수 없는 상태에 있는 것을 의미함</strong></p>
<ul>
<li>스레드는 비싼 자원이기 때문에 사용될 수 없는 상태에 놓이는 것이 반복되면 애플리케이션 성능 하락의 원인이 될 수 있음</li>
</ul>
<p><strong>스레드 블로킹을 발생시키는 원인</strong></p>
<ul>
<li>여러 스레드가 동기화 블록에 동시에 접근하는 경우 (Race-condition)</li>
<li>Mutex 또는 Semaphore로 인해 공유되는 자워에 접근할 수 있는 스레드가 제한되는 경우</li>
</ul>
<p><strong>Executor 프레임워크에서 스레드 블로킹이 종종 발생함</strong></p>
<ul>
<li>작업의 결과를 전달받을 때는 Future객체를 통해 언제 올지 모르는 값을 기다려야 함<ul>
<li>Future 객체의 get 함수를 호출한 스레드는 결과값을 반환받을 때까지 블로킹됨</li>
</ul>
</li>
</ul>
<h3 id="3-이후의-멀티-스레드-프로그래밍과-한계">3. 이후의 멀티 스레드 프로그래밍과 한계</h3>
<p>Executor 프레임워크 이후에도 멀티 스레드 프로그래밍의 문제를 보완하기 위한 다양한 방법이 만들어졌음</p>
<ul>
<li>기존 Future 객체의 스레드 블로킹을 줄이고 작업 체이닝 기능을 제공하는 CompletableFuture</li>
<li>결괏값을 데이터 스트림으로 처리해 스레드 블로킹을 방지하고 스레드풀을 손쉽게 전환할 수 있도록 한 RxJava</li>
</ul>
<p>이 외에도 다양한 멀티 스레드 프로그래밍 방법이 등장했었음</p>
<p>다루지 않는 이유는 지금까지 다룬 내용들이 코루틴과 관련해서 알아야 할 중요하고 근본적인 한 가지 문제점을 갖는 녀석들이기 때문</p>
<p>아래에서는 이 “근본적인 문제점”을 다룸</p>
<h1 id="4-기존-멀티-스레드-프로그래밍의-한계와-코루틴">4. 기존 멀티 스레드 프로그래밍의 한계와 코루틴</h1>
<h2 id="4-1-기존-멀티-스레드-프로그래밍의-한계">4-1. 기존 멀티 스레드 프로그래밍의 한계</h2>
<p>멀티 스레드 프로그래밍은 단점을 보완하며 발전해 왔음</p>
<p>하지만 기존 멀티 스레드 프로그래밍은 스레드 기반으로 작업한다는 한계가 있음</p>
<h3 id="멀티-스레드-프로그래밍이-단일-스레드-프로그래밍의-문제를-해결하기-위해-생겼다면서-스레드-기반-작업이-왜-한계인가">“멀티 스레드 프로그래밍이 단일 스레드 프로그래밍의 문제를 해결하기 위해 생겼다면서 스레드 기반 작업이 왜 한계인가?”</h3>
<p><strong>스레드는 생성과 전환 비용이 비쌈</strong></p>
<ul>
<li>스레드가 아무 작업도 하지 않고 대기하고 있다면 컴퓨터의 자원이 낭비됨</li>
<li>한 스레드가 다른 스레드의 작업의 결과를 받기 위해 대기하고 있다면 스레드 블로킹이 발생한 것</li>
<li>스레드 블로킹은 스레드라는 비싼 자원을 사용할 수 없게 만든 다는 점에서 성능에 매우 치명적인 영향을 줌</li>
</ul>
<p><strong>스레드 블로킹은 스레드 기반 작업을 하는 멀티 스레드 프로그래밍에서 피할 수 없는 문제임</strong></p>
<ul>
<li>간단한 작업에서는 콜백, 체이닝 등을 통해 이를 회피할 수는 있지만,</li>
<li>작업이 많아지고 작업 간의 종속성이 복잡해질수록 스레드 블로킹을 피하기 어렵고, 스레드의 성능을 제대로 발휘할 수 없을 수도 있음</li>
<li>실제 애플리케이션에선 작업간의 종속성이 복잡하고 네트워크 작업도 수없이 많기 때문에 스레드 블록킹의 발생은 필연적이라 할 수 있음</li>
</ul>
<blockquote>
<h1 id="체이닝-함수">체이닝 함수</h1>
<p>한 함수의 수행 결과를 다른 함수로 연결해 호출하는데 사용</p>
<ul>
<li>함수가 실행 완료됐을 때 실행할 콜백 함수를 등록하는 느낌</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val startTime = System.currentTimeMillis()
    val excutor = Excutor.newFixedThreadPool(2)

    val completableFuture = CompletableFuture.supplyAsync({
        Thread.sleep(1000L)
        return@supplyAsync &quot;결과&quot;
    }, executor)

    completableFuture.thenAccept { result -&gt; 
        println(&quot;[${getElapsedTime(startTime)}] $result 처리&quot;)
    }

    println(&quot;[${getElapsedTime(startTime)}] $result 처리&quot;)

    executor.shutdown()
}

/*
[지난 시간: 11ms] 다른 작업 실행
[지난 시간: 1008ms]
*/</code></pre>
</blockquote>
<h2 id="2-코루틴은-스레드-블로킹-문제를-어떻게-극복하는가">2. 코루틴은 스레드 블로킹 문제를 어떻게 극복하는가?</h2>
<p><strong>작업 단위 코루틴을 통해 스레드 블로킹 문제를 해결</strong></p>
<ul>
<li>작업 단위 코루틴 → 스레드에서 작업 실행 도중 일시 중단할 수 있는 작업 단위</li>
<li>코루틴은 작업이 일시 중단되면 스레드 사용 권한을 양보</li>
<li>양보된 스레드는 다른 작업을 실행하는 데 사용할 수 있음</li>
<li>일시 중단된 코루틴 → 작업 재개 시점에 다시 스레드에 할당돼 실행</li>
</ul>
<p><strong>코루틴은 경량 스레드라고 불린다.</strong></p>
<ul>
<li>코루틴을 코루틴 스케쥴러에 넘기면 사용할 수 있는 스레드나 스레드풀에 해당 코루틴을 분배해 작업을 수행</li>
<li>코루틴은 실행 도중 중단될 수 있기 때문에 해당 코루틴이 사용중이던 스레드를 다른 코루틴에 양보하는 것이 가능하기 때문에 스레드 블로킹이 발생하지 않음</li>
</ul>
<p><strong>코루틴과 멀티 스레드 프로그래밍 비교</strong></p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/cf8e4c78-0f43-4177-b3b5-53ce84da1bd5/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/8878c5bb-9cf0-4e29-88c9-e5484cd250be/image.jpg" alt=""></p>
<p><strong>코루틴이 경량 스레드라고 불리는 이유 정리</strong></p>
<ul>
<li>코루틴은 스레드를 사용하고 있지 않을 때 스레드 사용 권한을 양보함<ul>
<li>스레드 사용 최적화</li>
<li>스레드 블로킹 방지</li>
</ul>
</li>
<li>코루틴은 스레드에 비해 생성과 전환 비용이 적게 들고, 스레드에 붙였다 땠다 할 수 있어 작업을 생성하고 전환하는 데 필요한 리소스와 시간이 매우 줄어듬</li>
</ul>
<h1 id="5-1장-요약">5. 1장 요약</h1>
<ol>
<li>JVM 상에서 실행되는 코틀린 애플리케이션 → 메인 스레드에서 실행</li>
<li>단일 스레드는 한번에 하나의 작업만 수행할 수 있음 → 복잡한 작업이나 시간이 오래 걸리는 작업을 수행하면 응답성이 떨어질 수 있음</li>
<li>멀티 스레드를 통해 2번의 문제를 해결할 수 있음</li>
<li>Thread 클래스를 상속해 스레드를 생성하고 관리할 수 있으나 생성된 스레드의 재사용이 어려워 리소스 낭비를 일으킴</li>
<li>Executor 프레임워크는 스레드풀을 통해 스레드의 생성과 관리를 최적화하고 스레드의 재사용성을 높임</li>
<li>하지만 기존 멀티 스레드 프로그래밍 방식들은 스레드 블로킹이라는 문제를 근본적으로 해결하지 못했음</li>
<li>스레드 블로킹 → 스레드가 작업을 기다리면서 리소스를 낭비하지만 아무 일도 하지 않는 상태</li>
<li>코루틴은 스레드 블로킹 문재를 해결하기 위해 등장함</li>
<li>코루틴은 스레드 사용 권한을 양보하고 작업을 일시 중단해 다른 작업이 스레드를 사용할 수 있도록 함</li>
<li>일시 중단 후 재개된 코루틴은 재개 시점에 사용할 수 있는 스레드에 할당됨</li>
<li><strong>코루틴은 스레드에 비해 생성과 전환 비용이 적게 들고 스레드에 자유룝게 땠다 붙였다 할 수 있어 경량 스레드라고 불림</strong></li>
<li>코루틴을 통해 스레드 블로킹을 방지할 수 있어 애플리케이션의 응답성을 높일 수 있음</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[많은 시간이 지나고 끄적이는 회고]]></title>
            <link>https://velog.io/@first_woosun/%EB%A7%8E%EC%9D%80-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EC%A7%80%EB%82%98%EA%B3%A0-%EB%81%84%EC%A0%81%EC%9D%B4%EB%8A%94-%EA%B8%80</link>
            <guid>https://velog.io/@first_woosun/%EB%A7%8E%EC%9D%80-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EC%A7%80%EB%82%98%EA%B3%A0-%EB%81%84%EC%A0%81%EC%9D%B4%EB%8A%94-%EA%B8%80</guid>
            <pubDate>Sun, 12 Apr 2026 16:10:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/first_woosun/post/dcdf51da-1bc7-4f40-8d9f-336e850e6514/image.jpg" alt=""></p>
<h1 id="1달동안-뭐했니-7주차-회고">1달동안 뭐했니? (7주차 회고)</h1>
<p>처음 벨로그를 개설할 때만 해도 &quot;주마다 한번씩 회고록 써야지!&quot;라며 당차게 시작했었는데...</p>
<p>빡빡한 일정, 올라가는 난이도, 점점 부족해지는 체력 등 이런저런 핑계를 대며</p>
<p>&quot;미션할 시간도 없는데 회고록을 어떻게 써?&quot;라고 스스로를 달래며 회피하다가</p>
<p>이대로는 안된다는 생각이 들어 간만에 끄적이게 된 회고록입니다.</p>
<h1 id="성장했다고-생각하는-부분">성장했다고 생각하는 부분</h1>
<h2 id="다형성이-어떤-녀석인지-조금-알-것-같다">다형성이 어떤 녀석인지 조금 알 것 같다.</h2>
<p>대학교 4년동안 컴퓨터 공학 전공으로 대학을 다니며 수도 없이 들어왔던 interface, 상속.</p>
<p>언제, 어떻게, 왜 써야 하는지를 몰라 작업할 때 스스로 떠올려내 써 본 기억이 없는 친구입니다.</p>
<p><del>(마치 이름은 많이 들어봤는데 대화는 해본 적 없어 서먹서먹한 고등학교 동창같은 느낌...)</del></p>
<p>이 동창 녀석에 대해 이해하게 된 계기는 지난 6주차에 진행한 &quot;칸반 보드 관리&quot;미션에서 </p>
<p>레아가 수업해준 다형성을 통한 조건문 줄이기 라이브 코딩의 도움이 컸습니다. </p>
<p><strong>갓.레.아</strong></p>
<p>동일한 관심사를 갖고 비슷한 역할을 하지만 내부에서 가져야 할 상태가 다른 녀석을 구현할 때</p>
<p>다형성을 사용하면 유용하다는 걸 수업을 통해 알 수 있었고,</p>
<p>테코톡 일정으로 남들보다 미션 진행이 뒤쳐져 있었던 저는</p>
<p>다른 크루들은 각자의 기준과 고민을 통해 요구사항을 구현하고 있을 때</p>
<p>조금 치사하지만 레아의 수업을 듣고 이 규칙들을 어떻게 구현할지 고민하는 시간을 건너 뛰었습니다.</p>
<p>하지만 수업에서 라이브 코딩으로 보여준 예시를 완전히 그대로 쓰는 건 의미가 없을 것 같아 구조를 조금 수정해서 제 코드에 적용했습니다.</p>
<pre><code class="language-kotlin">interface TaskRules {
    val isDeletable: Boolean

    fun moveTo(targetStatus: TaskStatus): TaskRules

    val requireAssignee: Boolean
}

class Todo : TaskRules {
    override val isDeletable: Boolean = true

    override fun moveTo(targetStatus: TaskStatus): TaskRules {
        return when (targetStatus) {
            TaskStatus.TO_DO -&gt; this
            TaskStatus.IN_PROGRESS -&gt; InProgress()
            TaskStatus.REVIEW, TaskStatus.DONE -&gt; throw IllegalStateException()
        }
    }

    override val requireAssignee: Boolean = false
}
.
.
.</code></pre>
<p>이 구현체 까지는 별 차이가 없습니다.</p>
<p>라이브 코딩에선 이 규칙의 구현체가 task 객체를 들고 있었는데</p>
<p>&quot;규칙이 task 객체를 들고 있다.&quot; 라는 문장에서 기시감을 느껴</p>
<p>저는 이 규칙들을 task 객체가 들고 있도록 구조를 변경했습니다.</p>
<pre><code class="language-kotlin">data class KanbanTask(val data: TaskData, val status: TaskStatus) {
    private var taskRules: TaskRules = when (status) {
        TaskStatus.TO_DO -&gt; Todo()
        TaskStatus.IN_PROGRESS -&gt; InProgress()
        TaskStatus.REVIEW -&gt; Review()
        TaskStatus.DONE -&gt; Done()
    }

    val isDeletable get() = taskRules.isDeletable

    init {
        validateAssignee(taskRules, data)
    }

    fun changeStatus(targetStatue: TaskStatus): KanbanTask {
        val nextRules = taskRules.moveTo(targetStatue)

        validateAssignee(nextRules, data)
        taskRules = nextRules

        return copy(status = targetStatue)
    }</code></pre>
<p>이렇게 구조를 수정해도 괜찮을까? 라는 걱정도 있었지만 일단 해봤고 미션을 제출했습니다.</p>
<p>물론 엄청나게 많은 피드백을 받고, 고쳐야할 부분도 많았습니다.</p>
<p>그런데 리뷰어한테서</p>
<blockquote>
<p>상태전이에 대한 규칙을 상태패턴으로 적용한 점이 좋았습니다 👍
덕분에 도메인과 UI로직이 잘 분리되어 있네요.</p>
</blockquote>
<p>라는 피드백을 받았습니다.</p>
<p>... 정말 기분이 좋았습니다. </p>
<p>상태 전이 규칙 관련한 대부분의 코드가 레아의 라이브 코딩에서 따온 코드이긴 해도</p>
<p>스스로 고민하고 구조를 바꿔서 적용하기로 한 판단이 좋게 받아 들여졌다는 사실에 입가에 미소가 번졌습니다.</p>
<p>이렇게 얻은 다형성에 대한 자신감으로 다음 미션에도 다형성을 활용 했습니다.</p>
<h2 id="영화-예매-미션에서-배운-점">영화 예매 미션에서 배운 점</h2>
<p>다음 미션인 영화 예매 미션은 총 4단계로 구성되어 있었습니다.</p>
<blockquote>
</blockquote>
<p>1단계: 도메인 설계
2단계: view와 controller 구현 
3단계: DB에 연결
4단계: Spring boot를 통한 REST API 구현</p>
<p><del>3, 4단계 마감이 얼마 남지 않았지만 아직 1, 2단계 병합도 안된 막막한 미션...</del></p>
<p>이 미션에는 여러 할인 정책, 좌석 별 가격에 대한 요구사항이 있었고, </p>
<p>얼핏보기에 interface-class 구조로 구현하기 좋아 보이는 녀석들 이었습니다.</p>
<p>그래서 저는 이 녀석들을 다형성으로 제작하자고 페어에게 적극 주장했고 페어가 이를 받아줘</p>
<p>해보고 싶은데로 코드를 작성 할 수 있었습니다. <del>(고마워요 신밧드)</del></p>
<p>이렇게 그렇게 두 요구사항 모두 interface-class 구조로 구현하고 미션을 제출했습니다.</p>
<h3 id="다형성도-너무-남발하면-좋지-않다">다형성도 너무 남발하면 좋지 않다</h3>
<pre><code class="language-kotlin">interface SeatGrade {
    val price: Money
}

class GradeS : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_S)
}

class GradeA : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_A)
}

class GradeB : SeatGrade {
    override val price: Money = Money(SeatPrice.PRICE_B)
}</code></pre>
<p>이 코드는 저와 페어가 작성한 좌석별 금액에 대한 interface와 그 구현체입니다.</p>
<p>겉보기엔 깔끔하고 이뻐보였습니다. 하지만 리뷰어는 이 코드에 대해</p>
<blockquote>
<p> interface - class 구조로 만들 필요가 있었을까요?
다른 구현의 선택지는 없을지 고민해보시면 좋겠어요.</p>
</blockquote>
<p>라고 피드백을 받았습니다.</p>
<p>이 피드백을 읽고 &quot;다형성을 쓰기엔 과한 부분&quot;이라는 생각이 들었습니다.</p>
<p>의자의 등급은 가격을 들고만 있지, 그 가격으로 어떤 행위를 하는 녀석이 아니었습니다.</p>
<p>할인 정책의 경우 할인률을 갖고 입력 받은 가격에 할인을 <strong>&quot;적용해주는&quot;</strong> 행위를 들고 있기라도 하지</p>
<p>이 녀석은 상태만 들고 있는 녀석이라 interface-class로 구현하는게 과했다고 생각합니다.</p>
<h2 id="ocp도-조금은-알-것-같다">OCP도 조금은 알 것 같다,,?!</h2>
<p>Open Close Principle? 확장에는 열려있고 수정에는 닫혀있다?? <del>그게 뭔데;;;</del></p>
<p>라고 생각하며 OCP라는 게 있다~ 정도만 생각하고 있다가 정말 생각치 못하게 깨달음을 었었습니다.</p>
<p>controller를 구현하는 과정에서 생성된 예약들을 controller에서 들고 있었고,</p>
<p>저는 이게 정말 맘에 들지 않아 어떻게 수정할지 고민하다가 크루에게 도움을 청했습니다.</p>
<p>&quot;이거 너무 맘에 안드는데 어떻게 생각하세요??&quot;</p>
<p>그 크루도 controller에서 상태를 직접 들고 있는건 좋지 않다고 동의 해줬고 어떤 해결 방법이 있는지 힌트도 주었습니다.</p>
<p>그 힌트 중 domain에 대한 피드백도 있었습니다.</p>
<pre><code class="language-kotlin">class PayCalculator(
    private val reservations: List&lt;Reservation&gt;,
) {
    private var totalPrice: Money = Money(0)
    private val timeDiscountPolicy = TimeDiscountPolicy()
    private val movieDayDiscountPolicy = MovieDayDiscountPolicy()
    private val cardDiscountPolicy = CardDiscountPolicy()
    private val cashDiscountPolicy = CashDiscountPolicy()</code></pre>
<p>가격을 계산해 주는 객체에서 할인 정책들을 프로퍼티로 직접 들고 있었는데 이 부분을 짚어주며</p>
<p>유연하지 않다고 해주었습니다. </p>
<p>리뷰어에게도 같은 부분에 같은 리뷰를 받았었기 때문에 이를 어떻게 해결해야 할지 조언을 구했습니다.</p>
<p>크루가 해준 조언은 &quot;외부에서 파라미터로 주입 받아라&quot;였습니다.</p>
<p>이 말을 듣고 OCP에 대해 조금은 감을 잡은 것 같습니다.</p>
<p>OCP는 뭐다! 라고 명쾌하게 정의하지 못하지만 정책같은 것들을 객체에 직접 선언하여 <strong>고정</strong> 하지 말자 라는 건 알 것 같습니다.</p>
<pre><code class="language-kotlin">class PayCalculator {
    fun calculateInitPrice(reservations: List&lt;Reservation&gt;): Money {
        var initPrice = Money(0)

        for (reservation in reservations) {
            val info = reservation.getReservationInfo()
            initPrice += info.price
        }

        return initPrice
    }

    fun calculateTimeDiscountedPrice(
        price: Money,
        reservations: List&lt;Reservation&gt;,
        discountPolicies: List&lt;DiscountPolicy&gt;,
    ): Money {
        var discountedPrice = price
        for (reservation in reservations) {
            for (discountPolicy in discountPolicies) {
                discountedPrice = discountPolicy.applyDiscount(discountedPrice, reservation)
            }
        }

        return discountedPrice
    }</code></pre>
<p>피드백과 조언들을 반영한 코드입니다.</p>
<p>할인 정책은 같은 interface를 공유한다는 점을 이용해 정책들을 list 형태로 주입받아 적용하도록 코드를 수정했습니다.</p>
<p>하지만 여기서 걸리는 점은 list로 받으면 할인을 적용하는 순서가 list에 저장된 순서에 의해 결정된다는 것입니다.</p>
<p>이 문제에 대해선 아직 더 고민을 해봐야 할 것 같습니다.</p>
<p>아무튼 &quot;확장에는 열려있고 변경에는 닫혀있다&quot; 라는 문장이 코드에 어떻게 반영되는지</p>
<p><strong>느낌</strong>을 알 것 같습니다. 한문장으로 정의하기엔 아직 깨달음이 부족합니다...</p>
<h1 id="마무리">마무리</h1>
<p>시골에서 올라와 바쁜 도시 생활에 차차 적응되면서도 </p>
<p>한편으론 지치기도 하고 잘 해내고 있는지 의문이 들기도 하는 한달 이었습니다. </p>
<p>아직 공부해야할 것도 산더미고 해야할 일도 많고 신경써야 하는 것도 많아 정신이 없지만</p>
<p>레벨 1의 마지막을 잘 마무리 할 수 있기를 바라며</p>
<p>1달을 건너뛰고 쓰는 회고록을 마칩니다.</p>
<p><del>(회고 쓰면서 발견 했는데 PayCalculator에 오류가...)</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 레벨 1 회고 (2) - 우테코에서 생존 당하기]]></title>
            <link>https://velog.io/@first_woosun/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%ED%9A%8C%EA%B3%A0-2-%EC%9A%B0%ED%85%8C%EC%BD%94%EC%97%90%EC%84%9C-%EC%83%9D%EC%A1%B4-%EB%8B%B9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@first_woosun/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%ED%9A%8C%EA%B3%A0-2-%EC%9A%B0%ED%85%8C%EC%BD%94%EC%97%90%EC%84%9C-%EC%83%9D%EC%A1%B4-%EB%8B%B9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Mar 2026 14:04:03 GMT</pubDate>
            <description><![CDATA[<h2 id="안녕하세요">안녕하세요!</h2>
<p>크루 닉네임 <strong>투핸더</strong>입니다. 주말동안 야무지게 놀다가 일요일 밤 자기 전에 생각나서 끄적이는 회고록입니다.
<del>일요일에 적다가 미션 제출 찐빠나서 월요일에 마무리했다.</del></p>
<hr>
<h2 id="이번-주엔-뭘-했지">이번 주엔 뭘 했지?</h2>
<ol>
<li>SS 연극</li>
<li>안드로이드 트랙 미션 시작 (KanbanBoard Mission)<ul>
<li>첫 번째 PR 제출 및 피드백 반영</li>
<li>두 번째 PR 제출</li>
</ul>
</li>
<li>SS 워크샵 (학습 마인드셋, 단점 개선을 위한 단기적 실험 계획 세우기)</li>
</ol>
<h2 id="keep">Keep</h2>
<ul>
<li>연극조 크루들 외에도 다른 안드로이드 트랙 크루들과도 이래저래 대화 나누면서 친해진 것 같아서 기분이 좋습니다.</li>
<li>학습 마인드셋 가져가기<ul>
<li>“우테코에 가면 나보다 경험도 많고 개발적으로도, 사회인으로서도 나보다 성숙한 사람이 많을 텐데 그 사람들을 보면서 내 부족한 점을 파악하고 그 사람들의 행동으로 부터 배우자!” 라는 다짐을 우테코 시작 전부터 가지고 있었습니다. 모르는 것도 많고 개발자로써도, 사회인으로써도 부족한 점이 많지만 여러 크루들로부터 배우고 성장하겠습니다. 또한 우테코 생활에서 도움의 손길을 선뜻 내어주시는 여러 크루들에게 감사하다는 말을 전하고 싶습니다!!</li>
</ul>
</li>
</ul>
<h2 id="problem">Problem</h2>
<ul>
<li>과제 진행 과정에서 실수가 많다. (대부분 마음이 급해서 발생한 실수들인 것 같습니다…)<ul>
<li>레포를 로컬로 가져와서 브랜치의 현제 상태를 원격 저장소와 sync 시키고 나서 작업을 했어야 하는데 그냥 진행해서 원격 저장소에 push가 안되는 일이 있었습니다. 이 때도 크루에게 도움을 받아 문제를 해결하고 미션 결과물을 제출할 수 있었습니다.</li>
<li>이번 미션의 2단계 미션을 집에서 진행하고 PR을 생성한 후 리뷰 요청을 남겼는데 slack 리뷰 요청에 대한 메세지가 남지 않아 뭔가 이상함을 느끼고 있었습니다. 이 때 slack에 한 크루로부터 개인 DM 하나가 날아왔습니다. 제가 생성한 PR을 보고 제 PR이 개인 브랜치가 아닌 main 브랜치로 생성되있는 점을 짚어주셔서 문제점을 파악하고 해결할 수 있었습니다.</li>
</ul>
</li>
</ul>
<h2 id="try">Try</h2>
<ul>
<li>뭔가 일을 하기 전에 해야할 일을 정리하고 마음을 가다듬기<ul>
<li>마음만 급해서 확인해야 할 요소 또는 선행 과제를 체크하지 않고 일단 진행했다가 피를 많이 봤습니다. 우테코의 여러 피로 쓰인 규칙과 팁들이 왜 생긴 건지 직접 체감할 수 있었습니다…</li>
<li>이제부턴 미션을 수행하기 전에 요구사항을 정독하고 체크리스트를 만들어 수행해야 할 일과 수행한 일을 관리해보려 합니다.</li>
</ul>
</li>
</ul>
<h2 id="마무리하며">마무리하며</h2>
<p>다른 사람들 보다 뒤쳐지진 않을까, 내가 잘 하고 있는 걸까, 돈 문제 등등 여러 걱정들 때문에 마음이 급해져 허둥대다가 끝난 한 주였습니다. </p>
<p>다음 주를 여유롭게 시작하고 싶어 주말에 과제를 해 놓고 브랜치 분리라던지 README 최신화 라던지 이래저래 빼먹고 머지시킬 브랜치도 틀리고 북치고 장구치고 하다가 새벽까지 문제 해결하고 늦게 자고 등등 많은 일들이 있었습니다.</p>
<p>그 와중에 제가 실수했다는 걸 저보다 빨리 캐치해 알려주신 안드로이드 크루에게 감사드립니다… (사람 여러번 살리셨습니다 아키….)</p>
<p>쥐뿔도 모르는 상태로 시작한 우테코에서 많은 분들의 도움을 받으며 하루하루 넘기고 있는 요즘입니다. 이러다 보면 언젠가 성장해있는 저를 만날 수 있겠죠? 화이팅 하겠습니다. 😌😌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 레벨 1 첫번째 회고]]></title>
            <link>https://velog.io/@first_woosun/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%EC%B2%AB%EB%B2%88%EC%A7%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@first_woosun/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%EC%B2%AB%EB%B2%88%EC%A7%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 01 Mar 2026 14:37:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2026-03-01에 작성한 회고록입니다.</p>
</blockquote>
<h2 id="안녕하세요-저는-투핸더입니다">안녕하세요! 저는 <strong>‘투핸더’</strong>입니다.</h2>
<p>우테코 레벨 1이 시작되고 한 주가 지났습니다. </p>
<p>지난 일주일간 어떤 걸 했고 뭘 느꼈는지 정리하고 공유하고자 합니다.</p>
<div align="center">
  <img src="https://velog.velcdn.com/images/first_woosun/post/58de76fe-673d-4d6b-87b3-a933ab2d70d7/image.jpg" width="700">
</div>
(책 읽다가 갑자기 찍은 사진입니다. 그냥 올려보고 싶었습니다 ㅋㅋㅋ)


<h2 id="무엇을-했나">무엇을 했나?</h2>
<ul>
<li><p>우테코 OT</p>
<ul>
<li>여러 코치님들의 닉네임과 앞으로의 생활이 어떤 식으로 흘러갈지 알 수 있었습니다.</li>
</ul>
</li>
<li><p>온보딩 미션 (연극, Gemini 웹앱 제작)</p>
<ul>
<li>다음 주에 연극을 하는데 저희 조는 메시지보단 “일단 웃기자!”라는 생각으로 준비했습니다.</li>
<li>웹앱 4개를 만들었는데 개인적으로 퀄리티가 많이 아쉽습니다. 미션 요구사항을 체크하고 빠르게 구현에 들어갔어야 하는데 요구사항을 반복해서 읽느라 구현에 할애해도 아쉬운 시간을 만족스럽게 쓰지 못했습니다.</li>
</ul>
</li>
<li><p>README 작업 (프로젝트, 졸업 작품)</p>
<ul>
<li>프로젝트를 진행하면서 미루고 미루던 작업을 주말 동안 몰아서 하느라 죽을 맛이었습니다…</li>
<li><a href="https://github.com/first-woosun/cozyIoT">졸업 작품</a></li>
<li><a href="https://github.com/HaYoung1018/DDIP_Client_rev">팀 프로젝트</a></li>
</ul>
</li>
</ul>
<h2 id="keep">Keep</h2>
<p>적어보려고 이것저것 떠올려 봤지만 뭔가 “나 이거 잘했다!” 라고 하기엔 당연한 것들인 거 같아서 이번엔 패스하겠습니다.</p>
<h2 id="problem">Problem</h2>
<h3 id="1-까먹는다-너무-잘">1. 까먹는다. 너무 잘..</h3>
<p>우테코가 2월 24일 화요일에 시작돼 4일 동안 출근했고 이 짧은 시간 동안에도 생각보다 할 게 많았습니다.</p>
<p>웹앱도 만들어야 하고, 연극도 준비해야 하고, 책도 읽어야 하고…</p>
<p>이런저런 일들을 다른 사람들과 같이 하다 보니 그 과정에서 정말 많은 대화를 하게 되는데,</p>
<p>방금 대화하고 뒤돌아서면 얼마 지나지 않아 대부분의 내용을 까먹고 이후 작업을 할 때 여러 번 되물었던 것 같습니다.</p>
<p>물어봤던 걸 또 물어보고, 또 기억 안 나서 다시 물어보자니 시간을 뺏는 것 같아 죄송하고 참 난감한 상황을 마주했었습니다.</p>
<h3 id="2-요구사항을-정확히-파악하지-않는다">2. 요구사항을 정확히 파악하지 않는다.</h3>
<p>우테코에서 미션을 진행하려면 LMS에서 미션을 확인한 다음 구현하는 형식으로 흘러가는 것 같습니다.</p>
<p>이번 주에 진행한 Gemini 웹앱 미션도 LMS에서 자세한 내용을 확인할 수 있었고, 제가 온 신경을 쏟은 부분은 구현 요구사항이었습니다.</p>
<p>하지만 우테코의 미션에는 구현 외에도 제출 이후에 해야 할 일들, 피드백 반영, 피드백에 대한 소감 등 해야 할 추가적인 일들이 있었고, 이를 뒤늦게 알아차려 시간이 얼마 남지 않았거나 시간이 지난 후에 작업을 부랴부랴 진행했습니다.</p>
<h2 id="try">Try</h2>
<h3 id="1-일단-적어두자-이런거-저런거-다">1. 일단 적어두자. 이런거 저런거 다</h3>
<p>2개의 Problem에 모두 적용할 수 있는 방법입니다. </p>
<p>간단한 토론, 공지사항, 미션 요구사항 등을 요약하고 기록해 놓으면 하루를 시작할 때 오늘 할 일을 정리 해놓거나 앞으로 해야 할 일들을 정리하는데 도움이 될 거라 생각합니다.</p>
<p>이를 실천하기 위해 26일 목요일부터 Daily Diary를 적고 있습니다.</p>
<p>하루를 시작할 때 1차적으로 TODO를 기록하고 일과를 진행하면서 틈틈히 체크하고 추가하면서 할 일을 관리하고 있습니다.</p>
<h3 id="2-미션-시작-전-요구사항을-정리한-문서를-만들기">2. 미션 시작 전 요구사항을 정리한 문서를 만들기</h3>
<p>본격적으로 미션을 수행하기 전에 LMS의 공지글을 정리하고 작업을 시작해보려 합니다.</p>
<p>아직 템플릿을 구체화하진 않았지만, 간단히 생각해보자면</p>
<ol>
<li>요구사항</li>
<li>TODO</li>
<li>제출 체크리스트</li>
<li>제출 후 해야할 일</li>
</ol>
<p>이 정도를 작성해놓을 것 같습니다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>4일이라는 짧은 시간동안 우테코를 찍먹 해봤는데, 그 짧은 시간 동안 스스로 부족한 점, 고치면 좋을 점 등 성장할 수 있는 방법들을 몇 개 발견할 수 있었습니다.</p>
<p>첫 주 미션으로 “유연함의 힘”이라는 도서를 읽는 미션도 있었는데, 읽으면서 앞으로의 마인드 셋을 어떻게 잡아야 할지도 감이 오는 것 같습니다.</p>
<p>정말 많은 것들이 몰아쳐서 연극 조 크루들과는 대화를 많이 한 것 같은데 다른 크루들과는 크게 대화를 못해서 아쉽다는 생각도 듭니다.</p>
<p>하지만 한편으론 이름과 얼굴을 잘 기억하지 못하는 점 때문에 여러 크루와 왁왁 대화를 해도 내일 내가 그 크루의 닉네임을 기억할 수 있을까? 하는 걱정도 피어오릅니다.</p>
<p>먼저 다가가는 것은 자신 있지만 그러고 나서 투페이스 마냥 다음 날 닉네임을 기억하지 못하더라도 미워하지 말아 주세요…😂😂</p>
<h2 id="앞으로-잘-부탁드립니다">앞으로 잘 부탁드립니다!!</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[4주차 회고]]></title>
            <link>https://velog.io/@first_woosun/4%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@first_woosun/4%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 23 Feb 2026 13:16:46 GMT</pubDate>
            <description><![CDATA[<h2 id="계획이-얼마나-지켜졌나">계획이 얼마나 지켜졌나?</h2>
<h3 id="공부를-계획만큼-하지-못했다">공부를 계획만큼 하지 못했다…</h3>
<p>처음 계획을 세울 당시에는 “분량이 많긴 해도 열심히 하면 4주 안에 할 수 있지 않을까?”하는 생각으로 계획을 세웠습니다.</p>
<p>하지만 언제나 그렇듯 현실은 계획대로 흘러가지 않았고, 계획을 세울 당시에 생각나지 않았던 일정, 생각하지 못한 변수 등 여러 요소들로 인해 처음 계획한 분량의 대략 70% 정도 학습을 진행했습니다.</p>
<h3 id="계획은-못-지켰지만-얻은-건-많다">계획은 못 지켰지만 얻은 건 많다!</h3>
<p>일단 가장 큰 건 규칙적인 생활이 어느 정도 자리를 잡았다는 것입니다.</p>
<p>종강만 하면 놀지 못한 것이 아쉬워 밤늦게까지 게임을 하고 다음 날 점심은 돼서야 눈을 떴었는데 </p>
<p>레벨 0을 진행하면서 이런 생활 습관에서 벗어나 아침에 침대에서 일어나 집안일을 하고 공부를 하는 등 부지런한 생활을 할 수 있는 기반이 만들어졌습니다.</p>
<p>한 주를 마무리하며 적는 회고록에서도 얻은 것이 있습니다.</p>
<p>해결해야 하는 일이 있거나 문제가 있을 때 이를 따로 기록해두지 않아 잊혀진 것들이 많았는데 회고록을 작성하면서 문제점들을 글로 작성하니 문제를 잊지 않고 개선하기 위해 노력을 할 수 있었습니다.</p>
<p>회고록을 작성하며 스스로를 돌아보고 돌아본 내용을 글로 적는 것이 어떻게 도움이 되는지 직접 느끼며 앞으로의 여정에서 방향을 잃지 않을 수 있는 이정표를 만들어갈 수 있겠다는 생각이 듭니다.</p>
<hr>
<h2 id="앞으로의-숙제">앞으로의 숙제</h2>
<ul>
<li><p>못 다한 공부 이어가기</p>
<p>  처음 계획 세운 공부를 마치지 못했지만 여기서 끝내지 않고 하루 30분이라도 시간을 내어 코틀린 공부를 이어 나갈 생각입니다. 물론 우테코 과정을 진행하다 보면 자연스레 공부하게 될 수도 있지만 처음 공부하는 것과 한 번 공부한 내용을 다시 다루는 것에는 큰 차이가 있을 거라 생각해 교재를 끝까지 정독하려 합니다.</p>
</li>
<li><p>계획을 지키지 못했을 때 실현 가능한 범위로 분할하기</p>
<p>  레벨 0을 진행하는 동안 계획을 지키지 못한 상황에서 저는 계획을 수정하기보다는 최초의 계획을 따라가기 위해 공부를 강행했습니다. 이렇게 해보니 의욕이 생기기보다는 공부해야 할 양에 짓눌려 의지도 떨어지고, 계획을 지키지 못할 현실이 보이니 점점 집중력도 떨어지는 것을 경험했습니다. 그래서 앞으로는 계획을 수행하는 데 차질이 생기면 현재 상황을 분석하고 대략적인 가용 시간을 작성해 실현 가능한 목표를 재구성하여 의지와 집중력이 떨어지지 않도록 계획을 수정하는 연습을 해보려 합니다.</p>
</li>
</ul>
<hr>
<h2 id="입학을-하루-앞두고-한-특별한-경험">입학을 하루 앞두고 한 특별한 경험</h2>
<p>실제로 출근하기 전 버스 노선 점검과 이동 소요 시간 등을 확인하기 위해 사전 답사 차원으로 우아한 형제들 사옥을 다녀왔습니다.</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/2e8a6d6e-d415-483a-80a3-e073797ccd7e/image.jpg" alt=""></p>
<p>직접 두 눈으로 보니 앞으로 이곳으로 출퇴근 한다는 사실이 실감났습니다.</p>
<p>사옥 앞에 저와 마찬가지로 사옥을 촬영하고 있는 사람들이 있었습니다. </p>
<p>같이 우테코를 진행하시는 분들이지 않을까 하여 먼저 다가가 말을 걸었습니다.</p>
<p>다행히 두 분 모두 8기 크루 분들이었고 간단한 스몰 토킹을 할 수 있었습니다.</p>
<p>(BE 정콩이님, FE 도넛님 만나서 정말 반가웠습니다!! ㅋㅋㅋㅋㅋ)</p>
<p>간단한 스몰 토킹 후 두 분께서 카페 가서 같이 공부하자고 먼저 권해주셔서 혼자 할 예정이었던 공부를 다른 사람과 함께 할 수 있어서 외롭지 않았습니다! ㅋㅋㅋㅋㅋㅋ</p>
<p>말주변이 없어 먼저 질문을 하지 못했지만 먼저 이런저런 것들을 질문해주시며 대화를 이끌어주셔서 즐겁게 답사를 진행할 수 있었습니다!! 😆😆</p>
<p>오늘의 경험이 정말 특별하고 즐거웠습니다. 앞으로 쌓아가야 할 인연들에 “내가 잘 할 수 있을까?” 하는 걱정이 있었지만 오늘 만난 소중한 인연 덕분에 걱정에서 기대로 변할 수 있었습니다.</p>
<p>아직 미숙하고 아는 것도 많이 없는 시골 출신 개발자 지망생이지만 소중한 인연으로 기억될 수 있도록 노력하겠습니다!</p>
<p>10개월의 여정 잘 부탁드립니다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어노테이션 선언과 적용]]></title>
            <link>https://velog.io/@first_woosun/%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%84%A0%EC%96%B8%EA%B3%BC-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@first_woosun/%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%84%A0%EC%96%B8%EA%B3%BC-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 23 Feb 2026 11:23:17 GMT</pubDate>
            <description><![CDATA[<ul>
<li>어노테이션 사용을 통해 선언에 추가적인 메타 데이터를 연관시킬 수 있음</li>
<li>어노테이션 설정 방식에 따라 소스코드, 컴파일된 클래스 파일, 런타임에 대해 작동하는 도구를 통해 메타데이터에 접근할 수 있음</li>
</ul>
<h2 id="어노테이션을-적용해-선언에-표지-남기기">어노테이션을 적용해 선언에 표지 남기기</h2>
<ul>
<li>어노테이션 적용 방법 → @와 어노테이션 이름을 선언 앞에 작성</li>
</ul>
<pre><code class="language-kotlin">@annotaionName
/*선언부*/</code></pre>
<ul>
<li><p>함수, 클래스 등 여러 코드 구성 요소에 어노테이션 적용 가능</p>
</li>
<li><p>JUnit 프레임워크와 kotlin.test를 사용한다면 테스트 메서드 앞에 “@test”를 붙일 수 있음</p>
</li>
</ul>
<pre><code class="language-kotlin">import kotlin.test.*

class MyTest{
        @test // 어놑이션을 통해 JUnit 프레임워크에 이 메서드를 테스트로 호출하라고 지시
        fun testTrue() {
                assertTrue(1 + 1 == 2)
        }
} </code></pre>
<h3 id="deprecated-어노테이션">@Deprecated 어노테이션</h3>
<ul>
<li><p>주석이 달린 선언을 더 이상 사용하지 않음으로 표시</p>
</li>
<li><p>선언이 다른 선언에 의해 대체 되었거나 해당 기능을 더 이상 지원하지 않게 됐음</p>
</li>
<li><p>최대 3가지의 파라미터를 받음</p>
<pre><code class="language-kotlin">  @Target(allowedTargets = [AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.TYPEALIAS])
  annotation class Deprecated(
      val message: String, 
      val replaceWith: ReplaceWith = ReplaceWith(&quot;&quot;), 
      val level: DeprecationLevel = DeprecationLevel.WARNING
  )</code></pre>
<ol>
<li>message → 사용 중단의 이유 설명 및 대체 API 사용을 권장하는 메시지</li>
<li>replaceWith (선택적) → 더 이상 사용하지 않을 API를 대체할 코드 조각을 지정</li>
<li>level → 점진적인 사용 중단<ul>
<li>level의 단계<ol>
<li>WARNING(경고) → 사용자에게 경고를 보내지만 컴파일이나 런타임이 중단되진 않음</li>
<li>ERROR(오류 )→ 이 선언을 사용하는 새로운 코드가 컴파일 되지 못하게 막음</li>
<li>HIDDEN(숨김) → API가 컴파일된 코드가 남아 예전에 컴파일된 코드와의 이진 호환성을 유지</li>
</ol>
</li>
</ul>
</li>
</ol>
</li>
<li><p>@Deprecated 예시</p>
</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>
<ul>
<li>이런 선언이 있는 경우 remove함수를 사용하면</li>
</ul>
<h3 id="어노테이션의-인자">어노테이션의 인자</h3>
<ul>
<li>기본 타입의 값</li>
<li>문자열 이넘</li>
<li>클래스 참조</li>
<li>다른 어노테이션 클래스</li>
<li>위 요소들의 배열</li>
</ul>
<ol>
<li>클래스를 어노테이션 인자로 지정 → @MyAnnotation(MyClass::class)</li>
<li>다른 어노테이션을 인자로 지정 <ul>
<li>@Deprecated의 replaceWith도 어노테이션임</li>
<li>어노테이션 이름 앞에 @를 붙이지 않음</li>
</ul>
</li>
<li>배열을 인자로 지정<ul>
<li>@RequestMapping(path = [”/foo”, “/bar”])의 형식</li>
<li>각괄호를 사용하거나 arrayOf() 함수를 사용할 수 있음</li>
<li>자바에 선언한 어노테이션 클래스를 사용하면 value 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환됨</li>
</ul>
</li>
</ol>
<h3 id="어노테이션-인자-→-컴파일-시점에-알-수-있어야-함">어노테이션 인자 → 컴파일 시점에 알 수 있어야 함</h3>
<ul>
<li>임의의 프로퍼티를 인자로 지정할 수 없음</li>
<li>프로퍼티를 인자로 사용하려면 const로 선언해야 함<ul>
<li>const 변경자 → 프로퍼티를 컴파일 시점 상수로 취급</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">    const val TEST_TIMEOUT = 10L

    class MyTest {
            @Test
            @Timeout(TEST_TIMEOUT)
            fun testMethod() {
                    //...
            }
    }</code></pre>
<ul>
<li>const 변경자를 사용하지 않으면 “Only const val can be used in constant expression” 컴파일 오류가 발생함</li>
</ul>
<hr>
<h2 id="어노테이션-타깃-어노테이션이-참조할-수-있는-정확한-선언-지정">어노테이션 타깃 (어노테이션이 참조할 수 있는 정확한 선언 지정)</h2>
<ul>
<li>한 선언을 컴파일한 결과가 여러 자바 선언과 대응하는 경우가 자주 있음</li>
<li>코틀린 선언과 대응하는 여러 자바 선언에 각각 어노테이션을 붙일 경우가 있음<ul>
<li>코틀린 프로퍼티 → 자바 필드, 게터 메서드, 세터 메서드 및 그 파라미터 선언과 대응</li>
<li>주 생성자에서프로퍼티를 선언한 경우 접근자 메서드와 파라미터 외에 자바 생성자 파라미터와도 대등</li>
</ul>
</li>
<li>어노테이션을 붙일 때 이런 요소 중 어느 요소에 어노테이션을 붙일지 표시해야 함</li>
</ul>
<h3 id="사용-지점-타깃-선언">사용 지점 타깃 선언</h3>
<ul>
<li>어노테이션을 적용할 수 있는 요소의 종류 지정 (클래스, 함수, 속성, 표현식 등)</li>
<li>@와 어노테이션 이름 사이에 타깃을 지정</li>
</ul>
<pre><code class="language-kotlin">@target:AnnotationName

@get:JvmName(&quot;obtainCertificate&quot;)
@set:JvmName(&quot;putCertificate&quot;)</code></pre>
<ul>
<li>자바에서 선언된 어노테이션 → 기본적으로 프로퍼티의 필드에 어노테이션을 선언함</li>
<li>코틀린 어노테이션 → 프로퍼티에 직접 적용할 수 있는 어노테이션을 만들 수 있음</li>
</ul>
<h3 id="사용-지점-타깃-목록">사용 지점 타깃 목록</h3>
<ul>
<li>property → 프로퍼티 전체 (자바에서 선언된 어노테이션에선 사용 불가)</li>
<li>field → 프로퍼티에 의해 생성되는 필드</li>
<li>get → 프로퍼티 게터</li>
<li>set → 프로퍼티 세터</li>
<li>reciver → 확장 함수나 프로퍼티의 수신 객체 파라미터</li>
<li>param → 생성자 파라미터</li>
<li>setparam → 세터 파라미터</li>
<li>delegate → 위임 프로퍼티의 위인 인스턴스를 담아둔 필드</li>
<li>file → 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스<ul>
<li>package 선언보다 더 상위에 사용</li>
</ul>
</li>
</ul>
<h3 id="suppress-어노테이션">@Suppress 어노테이션</h3>
<ul>
<li>어노테이션 인자로 클래스, 함수 선언, 타입, 임의의 식을 받음</li>
<li>컴파일 경고를 억제</li>
</ul>
<pre><code class="language-kotlin">// 안전하지 못한 캐스팅 경고 무시하는 로컬 변수 선언
fun test(list: List&lt;*&gt;) {
        @Suppress(&quot;UNCHECKED_CAST&quot;)
        val strings = list as List&lt;String&gt;
}</code></pre>
<h3 id="자바-api를-제어하는-어노테이션">자바 API를 제어하는 어노테이션</h3>
<ul>
<li>@JvmName → 코틀린 선언이 만들어내는 자바 필드나 메서드의 이름을 변경</li>
<li>@JvmStatic → 객체 선언이나 동반 객체의 메서드에 적용하면 자바 정적 메서드로 노출됨</li>
<li>@JvmOverloads → 디폴트 파라미터가 있는 함수에 대해 자도으로 오버로딩한 함수를 생성</li>
<li>@JvmField → 대상 프로퍼티를 게터나 세터가 없는 public 자바 필드로 노출</li>
<li>@JvmRecord → data class에 사용하여 자바 레코드 클래스를 선언할 수 있음</li>
</ul>
<hr>
<h2 id="어노테이션을-활용해-json-직렬화-제어">어노테이션을 활용해 Json 직렬화 제어</h2>
<ul>
<li>직렬화 → 객체를 저장 장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환<ul>
<li>Json 형태로 주로 직렬화 함</li>
<li>코틀린 객체를 직렬화하기 위한 kotlinx.serialization 라이브러리가 있음</li>
<li>Jackson, GSON 등 자바 객체를 직렬화하기 위한 라이브러리도 호환됨</li>
</ul>
</li>
<li>역직렬화 → 텍스트나 이진 형식으로 저장된 데이터에서 원래의 객체로 변환</li>
</ul>
<h3 id="person-클래스-직렬화-역질렬화-예제">Person 클래스 직렬화, 역질렬화 예제</h3>
<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;}

    // 역직렬화
    val json = &quot;&quot;&quot;{&quot;name&quot; : &quot;Alice&quot;, &quot;age&quot; : 29}&quot;&quot;&quot;
    println(deserialize&lt;Person&gt;(json)) // Json 객체의 타입 정보를 명시해줘야 함
    // Person(name=Alice, age=29)
}</code></pre>
<h3 id="어노테이션을-통해-직렬화-역직렬화-방법-제어">어노테이션을 통해 직렬화, 역직렬화 방법 제어</h3>
<ul>
<li>Jkid 라이브러리 → 기본적으로 모든 프로퍼티를 직렬화하며 프로퍼티 이름을 키로 사용<ul>
<li>어노테이션을 통해 이런 동작을 변경할 수 있음</li>
</ul>
</li>
</ul>
<ol>
<li>@JsonExclude → 직렬화, 역직렬화 시 무시할 프로퍼티 지정</li>
<li>@JsonName → 키/값 쌍의 키를 지정</li>
</ol>
<pre><code class="language-kotlin">data class Person(
        @JsonName(&quot;alias&quot;) val firstName: String,
        @JsonExclude val age: Int? = null
)</code></pre>
<ul>
<li>@JsonExclude로 직렬화 대상에서 제외한 프로퍼티에는 반드시 기본값을 지정해야 함<ul>
<li>역직렬화로 인스턴스를 만들 때 필요</li>
</ul>
</li>
</ul>
<h3 id="어노테이션-선언">어노테이션 선언</h3>
<ul>
<li><p>어노테이션 클래스 → 선언이나 식과 관련 있는 메타데이터의 구조만 정의</p>
<ul>
<li><p>내부에 아무 코드도 들어있을 수 없음</p>
</li>
<li><p>@JsonExclude 예시</p>
<pre><code class="language-kotlin">annotation class JsonExclude</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>파라미터가 있는 어노테이션 → 클래스의 주 생성자에 파라미터를 선언</p>
<ul>
<li><p>일반적인 주 생성자 구문을 사용하면서 val로 선언</p>
</li>
<li><p>@JsonName 예시</p>
<pre><code class="language-kotlin">annotation class JsonName(val name: String)</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="자바-어노테이션-선언과-비교">자바 어노테이션 선언과 비교</h3>
<pre><code class="language-java">public @interface JsonName {
        String value();
}</code></pre>
<ul>
<li>자바 어노테이션 → value() 라는 메서드가 있음</li>
<li>어떤 어노테이션을 적용할 떄 value를 제외한 모든 애트리뷰트에 이름을 명시해야 함</li>
</ul>
<h3 id="메타어노테이션-어노테이션을-처리하는-방법-제어">메타어노테이션 (어노테이션을 처리하는 방법 제어)</h3>
<ul>
<li><p>어노테이션 클래스에도 어노테이션을 붙일 수 있음</p>
</li>
<li><p>어노테이션 클래스에 적용할 수 있는 어노테이션 → <strong>메타어노테이션</strong></p>
</li>
<li><p>메타어노테이션은 컴파일러가 어노테이션을 처리하는 방법을 제어함</p>
</li>
<li><p>@Target 메타어노테이션</p>
<ul>
<li><p>어노테이션을 적용할 수 있는 요소의 유형을  지정</p>
</li>
<li><p>@Target을 지정하지 않으면 모든 선언에 적용할 수 있는 어노테이션이 됨</p>
<pre><code class="language-java">@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude</code></pre>
</li>
<li><p>AnnotationTarget → 어노테이션 적용 가능한 타깃이 정의된 이넘</p>
<pre><code class="language-kotlin">  @Target(AnnotationTarget.CLASS, 
                  AnnotationTarget.FUNCTION,
          AnnotationTarget.TYPE_PARAMETER, 
          AnnotationTarget.VALUE_PARAMETER,
          AnnotationTarget.EXPRESSION)</code></pre>
</li>
<li><p>메타어노테이션을 직접 만들 수 있음</p>
<pre><code class="language-java">  @Target(AnnotationTarget.ANNOTATION_CLASS)
  annotation class BindingAnnotation

  @BindingAnnotation
  annotation class MyBinding</code></pre>
</li>
<li><p>대상을 PROPERTY로 지정한 어노테이션 → 자바에서 사용할 수 없음</p>
<ul>
<li>AnotationTarget.FIELD를 두 번째 타깃으로 추가해야 함</li>
<li>어노테이션을 코틀린 프로퍼티와 자바 필드에 적용할 수 있어짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="retention-어노테이션">@Retention 어노테이션</h3>
<ul>
<li>정의한 어노테이션의 유지 수준 지정<ul>
<li>소스 수준</li>
<li>.class 파일 수준</li>
<li>실행 시점에 리플렉션을 통한 접근 허용</li>
</ul>
</li>
<li>자바 → .class 수준이 디폴트<ul>
<li>하지만 대부분의 어노테이션을 런타임에도 사용할 수 있어야 함</li>
</ul>
</li>
<li>코틀린 → 디폴트로 RUNTIME으로 지정</li>
</ul>
<hr>
<h2 id="어노테이션-파라미터로-클래스-사용">어노테이션 파라미터로 클래스 사용</h2>
<ul>
<li>클래스 참조를 파라미터로 하는 어노테이션 클래스를 선언하여 클래스를 어노테이션 파라미터로 사용할 수 있음</li>
</ul>
<pre><code class="language-java">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>
<ul>
<li>역직렬화 과정에서 company 프로퍼티를 표현하는 Json을 읽으면 Json을 역직렬화 하면서 CompanyImpl의 인스턴스를 만들어 Person 인스턴스의 company 프로퍼티를 설정함</li>
</ul>
<h3 id="desirializeinterface-구현">@DesirializeInterface 구현</h3>
<pre><code class="language-java">annotation class DesirializeInterface(val targetClass: KClasee&lt;out Any&gt;)</code></pre>
<ul>
<li>KClass 타입 → 코틀린 클래스에 대한 참조를 저장<ul>
<li>KClass의 타입 파라미터 → 인스턴스가 가리키는 코틀린 타입을 지정</li>
<li>CompanyImpl::class의 타입 → KClass<CompanyImpl><ul>
<li>KClass<out Any>의 하위 타입</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="어노테이션-파라미터로-제네릭-파라미터-받기">어노테이션 파라미터로 제네릭 파라미터 받기</h2>
<ul>
<li><p>jkid → 기본 타입이 아닌 프로퍼티를 내포된 객체로 직렬화 함</p>
</li>
<li><p>이런 기본 동작을 변경하려면 직렬화하는 로직을 직접 제공하면 됨</p>
</li>
<li><p>@CustomSerializer</p>
<ul>
<li><p>커스텀 직렬화 클래스에 대한 참조를 인자로 받음</p>
<ul>
<li><p>ValueSerializer 인터페이스를 구현한 클래스</p>
</li>
<li><p>직렬화, 역직렬화 제공</p>
<pre><code class="language-java">interface ValueSerializer&lt;T&gt; {
      fun toJsonValue(value: T) : Any?
      fun fromJsonValue(jsonValue: Any?): T
}</code></pre>
</li>
</ul>
</li>
<li><p>Person 클래스 적용 예제</p>
<pre><code class="language-java">  data class Person(
          val name: String,
          @CustomSerializer(DateSerializer::class) val birthDate: Date
  )</code></pre>
</li>
<li><p>@CustomSerializer 구현</p>
<pre><code class="language-java">  annotation class CustomSerializer(
          val serializerClass: KClass&lt;out ValueSerializer&lt;*&gt;&gt;
  )</code></pre>
<ul>
<li><p>ValueSerializer → 제네릭 클래스 (타입 파라미터가 있음)</p>
<ul>
<li><p>어떤 파라미터가 쓰일지 알 수 없음 → 스타프로젝션 사용</p>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/58c42533-383f-4ec0-aab0-a4b228089bc7/image.jpg" alt=""></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[변성은 제네릭과 타입 인자 사이의 하위 타입 관계를 기술]]></title>
            <link>https://velog.io/@first_woosun/%EB%B3%80%EC%84%B1%EC%9D%80-%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%9D%B8%EC%9E%90-%EC%82%AC%EC%9D%B4%EC%9D%98-%ED%95%98%EC%9C%84-%ED%83%80%EC%9E%85-%EA%B4%80%EA%B3%84%EB%A5%BC-%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@first_woosun/%EB%B3%80%EC%84%B1%EC%9D%80-%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%9D%B8%EC%9E%90-%EC%82%AC%EC%9D%B4%EC%9D%98-%ED%95%98%EC%9C%84-%ED%83%80%EC%9E%85-%EA%B4%80%EA%B3%84%EB%A5%BC-%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Mon, 23 Feb 2026 11:20:32 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<ul>
<li>변성 → 기저 타입이 같고 타입 인자가 다른 타입의 관계를 설명하는 개념</li>
<li>변성을 잘 활용하면 타입 안전성을 보장하는 API를 만들 수 있음</li>
</ul>
<hr>
<h2 id="변성은-인자를-함수에-넘겨도-안전한지-판단하게-해줌">변성은 인자를 함수에 넘겨도 안전한지 판단하게 해줌</h2>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/70828c00-377c-4d04-bab2-7c0dc52a797c/image.png" alt=""></p>
<ul>
<li>어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어 Any 타입에 String 타입을 넘길 수 없음<ul>
<li>원소를 변경하거나 추가하지 않는다면 안전함</li>
</ul>
</li>
<li>코틀린에선 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전성을 제어할 수 있음</li>
</ul>
<hr>
<h2 id="클래스-타입-하위-타입">클래스, 타입, 하위 타입</h2>
<h3 id="하위-타입">하위 타입</h3>
<ul>
<li>A 타입이 필요한 곳에 B 타입을 넣어도 문제가 없다면 B는 A의 하위 타입임<ul>
<li>Int는 Number의 하위 타입임</li>
<li>String은 Number의 하위 타입이 아님</li>
</ul>
</li>
</ul>
<h3 id="상위-타입">상위 타입</h3>
<ul>
<li>하위 타입의 반대</li>
</ul>
<h3 id="하위-타입인지가-왜-중요한가">하위 타입인지가 왜 중요한가?</h3>
<ul>
<li>컴파일러 → 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행</li>
</ul>
<pre><code class="language-kotlin">fun test(i: Int) {
    val n: Number = i

    fun f(s: String) { /*...*/ }
    f(i)
}</code></pre>
<ul>
<li>Int는 Number의 하위 타입이어서 컴파일됨</li>
<li>Int는 String의 하위 타입이 아님 → 함수 f의 호출은 컴파일되지 않음</li>
</ul>
<h3 id="어떤-인터페이스를-구현한하는-클래스의-타입은-그-인처페이스의-하위-타입임">어떤 인터페이스를 구현한하는 클래스의 타입은 그 인처페이스의 하위 타입임</h3>
<ul>
<li>Int 클래스 → Number 클래스의 하위 클래스<ul>
<li>Int 타입 → Number 타입의 하위 타입</li>
</ul>
</li>
<li>String 클래스 → CharSequence 클래스의 하위 클래스<ul>
<li>String 타입 → CharSequence 타입의 하위 타입</li>
</ul>
</li>
</ul>
<h3 id="널이-될-수-있는-타입의-하위-타입과-하위-클래스는-같지-않음">널이 될 수 있는 타입의 하위 타입과 하위 클래스는 같지 않음</h3>
<ul>
<li>널이 될 수 있는 타입 → 널이 될 수 없는 타입의 하위 타입</li>
<li>하지만 두 타입은 같은 클래스에 속함<ul>
<li>널이 될 수 없는 값을 널이 될 수 있는 값에 저장할 수 있음</li>
<li>그 반대는 안됨</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">val s: String = &quot;abc&quot;
val t: String? = s</code></pre>
<h3 id="무공변">무공변</h3>
<ul>
<li>서로 다른 두 타입 A, B에 대해 A가 B의 상위 타입도 하위 타입도 아닌 경우를 <strong>무공변</strong>이라고 함</li>
<li>자바에서는 모든 클래스가 무공변임</li>
</ul>
<hr>
<h2 id="공변성">공변성</h2>
<h3 id="공변적인-클래스인터페이스">공변적인 클래스(인터페이스)</h3>
<ul>
<li><p>제네릭 클래스에 대해 A가 B의 하위 타입일 때 <code>Class&lt;A&gt;</code>가 <code>Class&lt;B&gt;</code>의 하위 타입인 경우</p>
</li>
<li><p>“하위 타입 관계를 유지한다.”라고 함</p>
</li>
<li><p>제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시 → out 키워드</p>
<pre><code class="language-kotlin">  interface Producer&lt;out T&gt;{
          fun produce(): T
  }</code></pre>
</li>
<li><p>클래스의 타입 파라미터를 공변적으로 만들면 파라미터 타입과 타입 인자가 정확히 일치하지 않아도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있음</p>
</li>
</ul>
<h3 id="무공변-컬렉션-역할을-하는-클래스-예시">무공변 컬렉션 역할을 하는 클래스 예시</h3>
<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 feddAll(animals: Herb&lt;Animal&gt;) {
    for(i in 0..&lt;animals.size){
        animals[i].feed()
    }
}</code></pre>
<h3 id="무공변-클래스-사용">무공변 클래스 사용</h3>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/85335cae-b1d9-4c9b-9cff-6ff61ecdd20f/image.png" alt=""></p>
<ul>
<li>feedAll 함수에 cats를 넘기면 타입 불일치 오류가 발생함<ul>
<li>Herb 클래스의 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 때문에 Cats는 Herb의 하위 클래스가 아님</li>
<li>명시적 타입 캐스팅을 통해 해결은 가능하나 코드가 복잡해짐</li>
</ul>
</li>
<li>Herb 클래스는 List와 비슷한 API를 제공하며 데이터를 추가하거나 변경할 수 없음<ul>
<li>Herb를 공변적인 클래스로 만들 수 있음</li>
</ul>
</li>
</ul>
<h3 id="컬렉션-역할을-하는-공변적인-클래스">컬렉션 역할을 하는 공변적인 클래스</h3>
<pre><code class="language-kotlin">class Herd&lt;out T : Animal&gt; { // T는 공변적임
    /*...*/
}

fun takeCareOfCats(cats: Herb&lt;Cat&gt;) {
    for(i in 0 ..&lt; cats.size) {
        cats[i].cleanLitter()
    }
    feddAll(cats) // 캐스팅을 할 필요 없음
}</code></pre>
<h3 id="모든-클래스를-공변적으로-만들면-안전한가">모든 클래스를 공변적으로 만들면 안전한가?</h3>
<ul>
<li>공변적으로 만들면 안전하지 못한 클래스도 있음</li>
<li>타입 파라미터를 공변적으로 지정 → 클래스 내부에서 그 파라미터를 사용하는 방법을 제한</li>
<li>타입 안전성을 보장하려면 타입 파라미터는 항상 out 위치에 있어야 함<ul>
<li>클래스가 T 타입의 값을 생성할 수는 있으나 소비할 수는 없음</li>
</ul>
</li>
</ul>
<h3 id="타입-파라미터를-사용할-수-있는-지점">타입 파라미터를 사용할 수 있는 지점</h3>
<ul>
<li>인(in)과 아웃(out)으로 나뉨</li>
<li>T라는 타입 파라미터를 사용하는 함수가 맴버로 있는 클래스<ul>
<li>T가 함수의 반환 타입 → 아웃 위치 (T 타입의 값을 생성)</li>
<li>T가 함수의 파라미터 타입 → 인 위치 (T 타입의 값을 소비)</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/cd74ed98-213f-45ae-961d-915297bfa168/image.jpg" alt=""></p>
<ul>
<li>제네릭 타입 인자 앞에 out 키워드를 사용하면 클래스 안에서 T를 함수의 파라미터 타입으로 사용하지 못하도록 제한<ul>
<li>T의 사용법을 제한하며 T로 인해 생기는 하위 타입 관계의 타입 안전성을 보장</li>
</ul>
</li>
</ul>
<h3 id="out-키워드의-의미">out 키워드의 의미</h3>
<ul>
<li>하위 타입의 관계가 유지됨</li>
<li>T를 아웃 위치에만 사용할 수 있음</li>
</ul>
<h3 id="list의-공변성">List의 공변성</h3>
<pre><code class="language-kotlin">interface List&lt;out T&gt; : Collection&lt;T&gt; {
        operator fun get(index: Int): T
        /* ... */
}</code></pre>
<ul>
<li>코틀린 List는 읽기 전용 (원소 추가, 변경 불가) → List는 T에 대해서 공변적임</li>
</ul>
<h3 id="타입-파라미터를-다른-타입의-타입-인자로-사용">타입 파라미터를 다른 타입의 타입 인자로 사용</h3>
<pre><code class="language-kotlin">interface List&lt;out T&gt; : Collection&lt;T&gt; {
        fun subList(fromIndex: Int, toIndex: Int): List&lt;T&gt;
}</code></pre>
<ul>
<li>List<T>를 반환하는 subList 메서드</li>
</ul>
<h3 id="mutablelistt를-타입-파라미터-t에-대해-공변적일-수-없음">MutableList<T>를 타입 파라미터 T에 대해 공변적일 수 없음</h3>
<ul>
<li>T를 인자로 받아 그 타입의 값을 반환하는 메서드가 있기 때문</li>
</ul>
<pre><code class="language-kotlin">interface MutableList&lt;T&gt; : List&lt;T&gt;, MutableCollection&lt;T&gt; {
        override fun add(element: T): Boolean
}</code></pre>
<ul>
<li>공변적으로 선언할 경우 “Type parameter T is declared as ‘out’ but occurs in ‘in’ position”이라는 오류를 보고함</li>
</ul>
<h3 id="생서자-파라미터의-위치">생서자 파라미터의 위치</h3>
<ul>
<li>생성자 파라미터는 인, 아웃 어느 쪽도 아님</li>
<li>타입 파라미터가 out이라 하더라고 그 타입을 생성자 파라미터 선언에 사용할 수 있음</li>
</ul>
<pre><code class="language-kotlin">class Herd&lt;T: Animal&gt;(vararg animals: T) { /* ... */ }</code></pre>
<ul>
<li>변성은 코드에서 위험할 여지가 있는 메서드를 호출할 수 없게 만듦<ul>
<li>클래스 인스턴스를 잘못 사용하는 일이 없게 방지</li>
</ul>
</li>
<li>생성자는 인스턴스를 생성한 뒤에 호출할 수 있는 메서드가 아니므로 위험의 여지가 없음</li>
<li>val이나 var 키워드를 생성자 파라미터에 사용하면 getter/setter를 정의하는 것과 같음<ul>
<li>읽기 전용 프로퍼티 → 아웃 위치</li>
<li>변경 가능 프로퍼티 → 인/아웃 위치</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">class Herd&lt;T: Animal&gt;(val leadAnimal: T, vararg animals: T) { /*...*/ }</code></pre>
<ul>
<li>leadAnimal이 변경 가능 프로퍼티 이므로 T를 out으로 표시할 수 없음</li>
</ul>
<h3 id="위치-규칙-→-외부에서-볼-수-있는-클래스-api에만-적용할-수-있음">위치 규칙 → 외부에서 볼 수 있는 클래스 API에만 적용할 수 있음</h3>
<ul>
<li>비공개 메서드의 파라미터 → 인도 아니고 아웃도 아님</li>
<li>변성 규칙 → 클래스 내부 구현에는 적용되지 않음</li>
</ul>
<pre><code class="language-kotlin">class Herd&lt;out T: Animal&gt;(private val leadAnimal: T, vararg animals: T) { /*...*/ }</code></pre>
<ul>
<li>leadAnimal 프로퍼티가 비공개이기 때문에 T를 공변적으로 선언해도 안전함</li>
</ul>
<hr>
<h2 id="반공변성">반공변성</h2>
<ul>
<li>반공변성 → 공변성의 반전<ul>
<li>반공변 클래스의 하위 타입 관계 → 타입 파라미터 상하위 관계와 반대</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">interface Comparator&lt;in T&gt; {
        fun compare(e1: T, e2: T): Int { /*...*/ }
}</code></pre>
<ul>
<li>이 인터페이스의 메서드 → T 타입의 값을 소비하기만 함<ul>
<li>T 앞에 in이 붙어야 함</li>
</ul>
</li>
<li>어떤 타입에 대해 Comparator를 구현하면 그 타입의 하위 타입에 속하는 모든 값을 비교할 수 있음</li>
</ul>
<pre><code class="language-kotlin">sealed class Fruit {
        abstract val weight: Int
}

data class Apple(override val weight: Int, val color: String): Fruit()

data class Orange(override val weight: Int, val juicy: Boolean): Fruit()</code></pre>
<ul>
<li>이 코드에 Comparator<Fruit>를 구현하면 모든 하위 타입을 비교할 수 있음</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val weightComparator = Comparator&lt;Fruit&gt; { a, b -&gt; a.weight - b.weight}

    val fruits: List&lt;Fruit&gt; = listOf(Orange(180, true), 
                              Apple(100, &quot;green&quot;)
                              )

    val apples: List&lt;Apple&gt; = listOf(Apple(50, &quot;red&quot;),        
                                Apple(120, &quot;green&quot;), 
                              Apple(155, &quot;yellow&quot;)
                              )

    println(fruits.sortedWith(weightComparator))
    // [Apple(weight=100, color=green), Orange(weight=180, juicy=true)]
    println(apples.sortedWith(weightComparator))
    // [Apple(weight=50, color=red), Apple(weight=120, color=green), Apple(weight=155, color=yellow)]
}</code></pre>
<ul>
<li>sortedWith 함수 → <code>Comparator&lt;String&gt;</code>을 요구<ul>
<li>String보다 더 일반적인 타입을 비교할 수 있는 Comparator를 넘겨도 안전함</li>
</ul>
</li>
<li>어떤 타입의 객체를 Comparator로 비교할 때 그 타입의 조상 타입을 비교할 수 있는 Comparator를 사용할 수 있음<ul>
<li><code>Comparator&lt;Any&gt;</code>가 <code>Comparator&lt;String&gt;</code>의 하위 타입이라는 뜻</li>
<li>보통 Any는 String의 상위 타입임</li>
<li>서로 다른 타입 인자에 대해 Comparator의 하위 타입 관계는 타입 인자의 하위 타입 관계와 반대</li>
</ul>
</li>
</ul>
<h3 id="반공변성의-정의">반공변성의 정의</h3>
<ul>
<li>타입 B가 타입 A의 하위 타입일 때 <code>Class&lt;A&gt;</code>가 <code>Class&lt;B&gt;</code>의 하위 타입인 관계가 성립하면 제네릭 클래스는 타입 인자 T에 대해 반공변임</li>
</ul>
<h3 id="in-키워드">in 키워드</h3>
<ul>
<li>in 키워드가 붙은 타입이 클래스 내부의 메서드 안으로 전달돼 소비된다는 뜻</li>
<li>공변성과 마찬가지로 타입 파라미터의 사용을 제한하여 특정 하위 타입 관계에 도달할 수 있음</li>
<li>in 키워드를 붙이면 그 타입 파라미터를 in 위치에서만 사용할 수 있음</li>
</ul>
<h3 id="공변성-반공변성-무공변성-요약">공변성, 반공변성, 무공변성 요약</h3>
<ol>
<li>공변성<ul>
<li>타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지</li>
<li>T를 아웃 위치에서만 사용할 수 있음</li>
</ul>
</li>
<li>반공변성<ul>
<li>타입 인자의 하위 타입 관계가 제네릭 타입에서 반전됨</li>
<li>T를 인 위치에서만 사용할 수 있음</li>
</ul>
</li>
<li>무공변성<ul>
<li>하위 타입 관계가 성립하지 않음</li>
<li>T를 아무 위치에서나 사용할 수 있음</li>
</ul>
</li>
</ol>
<hr>
<h2 id="사용-지점-변성">사용 지점 변성</h2>
<ul>
<li>클래스를 선언하면서 변성을 지정<ul>
<li>클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼침</li>
</ul>
</li>
<li>자바에서는 타입 파라미터를 사용할 때 그 타입 파라미터가 상위/하위 타입 중 어떤 타입으로 대치할 수 있는지 명시해야 함</li>
</ul>
<h3 id="코틀린의-사용-지점-변성">코틀린의 사용 지점 변성</h3>
<ul>
<li><p>특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있음</p>
</li>
<li><p>MutableList같은 상당수의 인터페이스 &gt; 타입 파라미터로 지정된 타입을 소비하는 동시에 생산할 수 있음 (공변적이지도 반공변적이지도 않음)</p>
<ul>
<li><p>이런 인터페이스 안에서 한 변수가 함수 안에서 생성자와 소비자 중 단 한 가지 역할을 담당하는 경우가 있음</p>
<pre><code class="language-kotlin">fun &lt;T&gt; copyData(source: MutableList&lt;T&gt;,
               destination: MutableList&lt;T&gt;) {
  for (item in source) {
      destination.add(item)
  }
}</code></pre>
</li>
<li><p>두 컬렉션 모두 무공변 타입이지만 하나는 읽기만 하고 하나는 쓰기만 함</p>
</li>
<li><p>이런 경우 두 컬렉션의 타입이 정확하게 일치하지 않아도 됨</p>
</li>
<li><p>문자열 컬렉션에서 객체의 컬렉션으로 복사해도 문제 없음</p>
</li>
<li><p>여러 리스트 타입에 대응하는 두 번째 제네릭 타입 파라미터를 도입할 수 있음</p>
<pre><code class="language-kotlin">fun &lt;T: R, R&gt; copyData(source: MutableList&lt;T&gt;,
               destination: MutableList&lt;R&gt;) {
               // 원본의 원소 타입 -&gt; 대상 원소 타입의 하위 타입
  for (item in source) {
      destination.add(item)
  }
}

fun main() {
  val ints = mutableListOf(1, 2, 3)
  val anyItems = mutableListOf&lt;Any&gt;()
  copyData(ints, anyItems) // Int가 Any의 하위 타입이기 때문에 호출 가능
  println(anyItems)
  // [1, 2, 3]
}</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="아웃-프로젝션-타입-파라미터를-사용">아웃-프로젝션 타입 파라미터를 사용</h3>
<pre><code class="language-kotlin">fun &lt;T&gt; copyData(source: MutableList&lt;out T&gt;,
                 destination: MutableList&lt;T&gt;) {
    for (item in source) {
        destination.add(item)
    }
}</code></pre>
<ul>
<li>타입 파라미터에 변성 변경자를 사용하여 간단하게 구현</li>
<li>변성 변경자를 사용하지 않은 경우 이런 경고가 출력됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/a7429b3e-4d13-44fa-b354-6f72b3fff107/image.png" alt=""></p>
<h3 id="타입-프로젝션">타입 프로젝션</h3>
<ul>
<li>타입 선언에 사용한 파라미터를 사용하는 위치라면 어디에나 변성 변경자를 붙일 수 있음<ul>
<li>파라미터 타입, 로컬 변수 타입, 함수 반환 타입 등등…</li>
</ul>
</li>
<li>타입 변성에 사용한 파라미터를 사용하는 곳에 변성 변경자를 붙이면 <strong>타입 프로젝션</strong>이 일어남<ul>
<li>일반적인 MutableList가 아닌 MutableList를 프로젝션한(제약을 가한) 타입을 만듬</li>
</ul>
</li>
</ul>
<p>사용 지점 변성을 사용하면 타입 인자로 사용할 수 있는 타입의 범위가 넓어짐</p>
<hr>
<h2 id="스타-프로젝션">스타 프로젝션</h2>
<ul>
<li>제네릭 타입 인자 정보가 없음을 표현<ul>
<li>원소 타입이 알려져 있지 않은 List → List&lt;*&gt;로 표현할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="mutablelist-≠-mutablelistany">MutableList&lt;*&gt; ≠ MutableList&lt;Any?&gt;</h3>
<ul>
<li>Any? → 모든 타입의 원소를 담을 수 있음</li>
<li><ul>
<li>→ 정해진 구체적인 타입의 원소를 담을 수 있지만 그 원소의 타입을 정확히 모름</li>
</ul>
</li>
<li>원소 타입을 모른다고 아무 타입이나 다 넣을 수 있는 건 아님</li>
</ul>
<h3 id="스타-프로젝션-→-아웃-프로젝션-타입으로-인식됨">스타 프로젝션 → 아웃 프로젝션 타입으로 인식됨</h3>
<ul>
<li>어떤 리스트의 원소 타입을 모르더라도 안전하게 Any? 타입으로 꺼내올 수 있음</li>
<li>하지만 타입을 모르는 리스트에 아무 원소나 마음대로 넣을 수는 없음</li>
</ul>
<h3 id="스타-프로젝션을-사용하는-경우">스타 프로젝션을 사용하는 경우</h3>
<ol>
<li>타입 파라미터를 시그니처에 언급하지 않는 경우</li>
<li>데이터를 읽기는 하지만 구체적인 타입은 신경 쓰지 않는 경우</li>
</ol>
<pre><code class="language-kotlin">fun printFirst(list: List&lt;*&gt;) { // 모든 리스트를 인자로 받을 수 있음
    if (list.isNotEmpty()) { // isNoEmpty -&gt; 제네릭 파라미터를 사용하지 않음
        println(list.first()) 
        // first -&gt; Any? 타입을 반환함
    }
}

fun main() {
    printFirst(listOf(&quot;Sveta&quot;, &quot;Seb&quot;, &quot;Dima&quot;, &quot;Roman&quot;))
    // Sveta
}</code></pre>
<ul>
<li>제네릭 파라미터가 어떤 타입인지 굳이 알 필요 없을 때만 스타 프로젝션을 사용<ul>
<li>스타 프로젝션을 사용할 때는 값을 만들어내는 메서드만 호출 가능</li>
<li>반환한 값의 타입은 신경 쓰지 않음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="타입-별명">타입 별명</h2>
<ul>
<li>기존 타입에 다른 이름을 부여</li>
</ul>
<pre><code class="language-kotlin">typealias name = typeName</code></pre>
<ul>
<li>긴 제네릭 타입을 짧게 만들수 있음</li>
</ul>
<pre><code class="language-kotlin">typealias Combiner = (String, String, String, String) -&gt; String
// 4개의 문자열을 받고 새로 조함한 문자열을 반환하는 함수형 타입 파라미터

val authorsCombiner: Combiner = { a, b, c, d -&gt; &quot;$a et al.&quot; }
// 변수 선언에 타입 별명 사용 가능

fun combineAuthors(combiner: Combiner) { // 함수 파라미터에 타입 별명 사용 가능
    println(combiner(&quot;Sveta&quot;, &quot;Seb&quot;, &quot;Dima&quot;, &quot;Roman&quot;))
}

fun main() {
    combineAuthors(authorsCombiner) // 타입 별명은 원래 타입으로 해소됨
    // Sveta et al.
    combineAuthors{ a, b, c, d -&gt; &quot;$d, $c &amp; Co&quot; } // 람다를 전달해도 됨
    // Roman, Dima &amp; Co
}</code></pre>
<ul>
<li>타입 별명을 사용하면 코드의 가독성을 올릴 수 있지만 다는 개발자가 볼 때 타입 별명의 정의를 찾아봐야 한다는 단점도 있음</li>
<li>타입 별명이 타입 안전성을 추가해주진 않음<ul>
<li>컴파일시 원래 타입으로 치환되기 때문</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[실행 시점 제네릭스 동작 (소거, 실체화)]]></title>
            <link>https://velog.io/@first_woosun/%EC%8B%A4%ED%96%89-%EC%8B%9C%EC%A0%90-%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%8A%A4-%EB%8F%99%EC%9E%91-%EC%86%8C%EA%B1%B0-%EC%8B%A4%EC%B2%B4%ED%99%94</link>
            <guid>https://velog.io/@first_woosun/%EC%8B%A4%ED%96%89-%EC%8B%9C%EC%A0%90-%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%8A%A4-%EB%8F%99%EC%9E%91-%EC%86%8C%EA%B1%B0-%EC%8B%A4%EC%B2%B4%ED%99%94</guid>
            <pubDate>Mon, 23 Feb 2026 11:12:51 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<ul>
<li>JVM의 제네릭스 → 타입 소거를 사용해 구현<ul>
<li>실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 의미</li>
</ul>
</li>
<li>타입 소거는 어떤 영향을 미치는가?</li>
<li>함수를 inline으로 선언해 제약을 우회하는 방법</li>
<li>실체화된 타입 파라미터</li>
</ul>
<hr>
<h2 id="실행-시점에-제네릭-클래스의-타입-정보를-찾을-때-한계">실행 시점에 제네릭 클래스의 타입 정보를 찾을 때 한계:</h2>
<p>타입 검사와 캐스팅</p>
<ul>
<li>코틀린의 타입 인자 정보 → 런타임에 지워짐(자바와 동일)<ul>
<li>제네릭 클래스 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않음</li>
<li>List<String> 객체를 만들고나면 그 객체를 List로만 볼 수 있음</li>
<li>이 리스트가 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없음</li>
</ul>
</li>
</ul>
<h3 id="타입-소거로-생기는-한계">타입 소거로 생기는 한계</h3>
<ol>
<li><p>타입 인자를 저장하지 않음 → 실행 시점에 타입 인자를 검사할 수 없음</p>
<ul>
<li><p>ex) 어떤 리스트가 문자열로 이뤄진 리스트인지 실행 시점 에는 검사할 수 없음</p>
</li>
<li><p>파라미터의 타입 인자에 따라 다른 동작을 수행할 수 없음</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;)
      }
      // when 식에서 Cannot check for an instance of erased type 오류 발생
}

fun main() {
      val list = readNumbersOrWords()
      printList(list)
}</code></pre>
</li>
<li><p>실행 시점에서 어떤 값이 List임은 확실히 알수 있지만 그 리스트가 어떤 타입을 저장하는지 알 수 없음</p>
</li>
<li><p>타입 인자를 저장하지 않기 때문에 애플리케이션의 전체 메모리 사용량이 줄어든다는 장점은 있음</p>
</li>
<li><p>스타 프로젝션( * )을 사용하면 최소한 대상이 List 임은 확인할 수 있음 (추후 자세히 다룸)</p>
<ul>
<li>여전히 원소 타입은 알 수 없음</li>
</ul>
</li>
</ul>
</li>
<li><p>as나 as? 같은 캐스팅</p>
<ul>
<li><p>캐스팅에도 제네릭 타입을 사용할 수 있음</p>
</li>
<li><p>기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅을 해도 성공함</p>
<ul>
<li>캐스팅은 항상 성공</li>
</ul>
</li>
<li><p>이런 캐스팅은 컴파일러가 unchecked cast라는 경고를 해줌</p>
<ul>
<li>단순 경고일 뿐 컴파일은 진행됨</li>
</ul>
<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(1, 2, 3))
      // IllegalArgumentException: List is expected
      printSum(listOf(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;))
      // ClassCastException: String connot cast to Number
}</code></pre>
</li>
<li><p>잘못된 타입의 원소가 들어있는 경우 ClassCastException이 발생</p>
<ul>
<li>as ? 캐스트는 성공하지만 sum 함수에서 예외가 발생</li>
<li>sum은 Number 타입의 값에 대해서 작동</li>
<li>String을 Number로 사용하려 했기 때문에 ClassCastException이 발생</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="실행-시점에-타입-정보가-주어진-경우">실행 시점에 타입 정보가 주어진 경우</h3>
<ul>
<li>이 경우 is 검사를 수행할 수 있음</li>
</ul>
<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>
<ul>
<li><p>인라인 함수의 타입 파라미터 → 실체화됨</p>
<ul>
<li>실행 시점에 인라인 함수의 실제 타입 인자를 알 수 있음</li>
</ul>
</li>
<li><p>reified 키워드를 통해 타입 파라미터를 실체화 시킬 수 있음</p>
<ul>
<li><p>일반 함수에 reified 키워드를 사용하면 경고 메시지가 나옴</p>
<p>  <img src="attachment:ef02daed-aa83-4435-869f-5d10a6e9e7e3:image.png" alt="image.png"></p>
</li>
</ul>
</li>
</ul>
<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>
<h3 id="filterisinstance-예제">filterIsInstance 예제</h3>
<ul>
<li>표준 라이브러리 함수로, 인자로 받은 컬렉션에서 지정한 타입의 원소만 모아서 리스트로 반환</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val items = listOf(&quot;one&quot;, 2, &quot;three&quot;)
    println(items.filterIsInstance&lt;String&gt;())
    // [one, three]
}</code></pre>
<ul>
<li>filterIsInstance 함수의 내부 (간소화 예시)</li>
</ul>
<pre><code class="language-kotlin">inline fun &lt;reified T&gt; Iterable&lt;*&gt;.filterIsInstance(): List&lt;T&gt; {
    val destination = mutableListOf&lt;T&gt;()
    for (element in this) {
        if(element is T) { // 각 원소가 인자로 지정한 클래스의 인스턴스인지 검사
            destination.add(element)
        }
    }
    return destination
}</code></pre>
<h3 id="inline-함수에서만-reified-키워드를-쓸-수-있는-이유">inline 함수에서만 reified 키워드를 쓸 수 있는 이유</h3>
<ul>
<li>inline 함수 → 함수를 호출한 모든 위치에 바이트 코드를 삽입<ul>
<li>컴파일러는 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성해 삽입할 수 있음</li>
<li>만들어진 바이트 코드는 구체적인 타입을 사용하므로 타입 소거의 영향을 받지 않음</li>
</ul>
</li>
</ul>
<h3 id="자바-코드에서-reified-키워드를-사용한-inline-함수를-호출할-수-없음">자바 코드에서 reified 키워드를 사용한 inline 함수를 호출할 수 없음</h3>
<ul>
<li>자바에서는 코틀린 inline 함수를 일반 함수처럼 호출 → 실제로 인라이닝 되지 않음</li>
<li>바이트코드를 직접 삽입하지 않기 때문에 구체적인 클래스를 참조하지 못함</li>
</ul>
<hr>
<h2 id="클래스-참조를-실체화된-타입-파라미터로-대신해-javalangclass-파라미터-피하기">클래스 참조를 실체화된 타입 파라미터로 대신해 java.lang.Class 파라미터 피하기</h2>
<ul>
<li>java.lang.Class 타입 인자를 파라미터로 받는 API에 대한 코틀린 어댑터 구축 → 실체화된 파라미터를 자주 사용<ul>
<li>JDK의 ServiceLoader가 있음<ul>
<li>어떤 추상 클래스나 인스턴스를 구현한 인스턴스를 반환</li>
</ul>
</li>
<li>실체화된 타입 파라미터를 통해 이런 API를 쉽게 호출 할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="serviceloader-사용-방법">ServiceLoader 사용 방법</h3>
<pre><code class="language-kotlin">val serviceImpl = ServiceLoader.load(Service::class.java)</code></pre>
<ul>
<li>::class.java 구문 → 코틀린 클래스에 대응하는 java.lang.Class 참조를 얻는 방법</li>
<li>Service::class.java → Service.class라는 자바 코드와 완전히 같음</li>
</ul>
<h3 id="serviceloader에서-타입-파라미터-사용">ServiceLoader에서 타입 파라미터 사용</h3>
<pre><code class="language-kotlin">val serviceImpl = loadService&lt;Service&gt;()</code></pre>
<ul>
<li><p>읽어 들일 서비스 클래스를 loadService 함수의 타입 인자로 지정</p>
</li>
<li><p>loadService 함수</p>
<pre><code class="language-kotlin">  inline fun &lt;reified T&gt; loadService(): ServiceLoader&lt;T?&gt;? {
      return ServiceLoader.load(T::class.java)
  }</code></pre>
</li>
</ul>
<h3 id="안드로이드의-startactivity-함수-간단하게-만들기">안드로이드의 startActivity 함수 간단하게 만들기</h3>
<pre><code class="language-kotlin">inline fun &lt;reified T : Activity&gt; Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

startActivity&lt;DetailActivity&gt;() // 액티비티를 표시하는 메서드를 호출</code></pre>
<hr>
<h2 id="실체화된-타입-파라미터가-있는-접근자-정의">실체화된 타입 파라미터가 있는 접근자 정의</h2>
<ul>
<li>제네릭 타입에 대해 프로퍼티 접근자를 정의할 수 있음<ul>
<li>프로퍼티를 inline으로 표시</li>
<li>타입 파라미터를 reified로 표시</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">inline val &lt;reified T&gt; T.cannonical: String
    get() = T::class.java.canonicalName

fun main() {
    println(listOf(1, 2, 3).cannonical)
    // java.util.List
    println(1.cannonical)
    // java.lang.Integer
}</code></pre>
<hr>
<h2 id="실체화된-파라미터의-제약">실체화된 파라미터의 제약</h2>
<ul>
<li>실체화의 개념으로 인해 생기는 제약과 현제 코틀린이 실체화를 구현하는 방식에 의해 생기는 제약이 있음</li>
</ul>
<ol>
<li>실체화된 타입 파라미터를 사용할 수 있는 경우<ul>
<li>타입 검사와 캐스팅 (is, !is, as, as?)</li>
<li>코틀린 리플렉션 API (::class)</li>
<li>코틀린 타입에 대응하는 java.lang.Class 얻기 (::class.java)</li>
<li>다른 함수를 호출할 때 타입 인자로 사용</li>
</ul>
</li>
<li>실체화된 타입 파라미터의 제약<ul>
<li>타입 파라미터 클래스의 인스턴스 생성</li>
<li>타입 파라미터의 동반 객체 메서드 호출</li>
<li>실체화된 타입 파라미터를 요구하는 함수에 실체화되지 않은 타입 인자를 넘기기</li>
<li>클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정</li>
</ul>
</li>
</ol>
<h3 id="클래스-프로퍼티-인라인-함수가-아닌-함수의-타입-파라미터를-reified로-지정">클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정</h3>
<ul>
<li>실체화된 타입 파라미터를 사용하는 함수 → 자신에게 전달되는 모든 람다를 인라이닝 함<ul>
<li>람다 내부에서 타입 파라미터를 사용하는 방식에 따라 람다를 인라이닝 할 수 없는 경우가 생기기도 함</li>
<li>성능 문제로 람다를 인라이닝 하고 싶지 않을 수도 있음</li>
<li>noinline 변경자를 통해 인라이닝을 금지할 수 있음</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[제네릭 타입 파라미터: 타입 인자를 받는 타입]]></title>
            <link>https://velog.io/@first_woosun/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%ED%83%80%EC%9E%85-%EC%9D%B8%EC%9E%90%EB%A5%BC-%EB%B0%9B%EB%8A%94-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@first_woosun/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%ED%83%80%EC%9E%85-%EC%9D%B8%EC%9E%90%EB%A5%BC-%EB%B0%9B%EB%8A%94-%ED%83%80%EC%9E%85</guid>
            <pubDate>Mon, 23 Feb 2026 11:12:04 GMT</pubDate>
            <description><![CDATA[<ul>
<li>제네릭스를 사용해 타입 파라미터를 받는 타입을 정의할 수 있음</li>
<li>제네릭 클래스에 구체적인 타입을 넘기면 타입을 인스턴스화 할 수 있음</li>
</ul>
<pre><code class="language-kotlin">val authors = listOf(&quot;Dmitry&quot;, &quot;Svetlana&quot;)</code></pre>
<ul>
<li><p>컴파일러는 자동으로 author의 타입이 List<String>임을 추론함</p>
</li>
<li><p>빈 리스트를 정의할 때는 타입 인자를 명시해줘야 함</p>
</li>
</ul>
<pre><code class="language-kotlin">val readers: MutableList&lt;String&gt; = mutableListOf() // 변수 타입 지정
val readers = mutableListOf&lt;String&gt;() // 함수 타입 지정</code></pre>
<hr>
<h2 id="제네릭-타입과-함께-동작하는-함수와-프로퍼티">제네릭 타입과 함께 동작하는 함수와 프로퍼티</h2>
<h3 id="제네릭-함수-→-여러-타입에-대응-가능">제네릭 함수 → 여러 타입에 대응 가능</h3>
<ul>
<li>타입 파라미터를 받음</li>
<li>호출 시 구체적인 타입을 인자로 넘겨야 함</li>
</ul>
<h3 id="컬렉션-함수-→-대부분-제네릭-함수">컬렉션 함수 → 대부분 제네릭 함수</h3>
<pre><code class="language-kotlin">// slice함수 예시
fun &lt;T&gt; List&lt;T&gt;.slice(indices: IntRange) : List&lt;T&gt;</code></pre>
<ul>
<li><T>의 의미 (순서대로)<ol>
<li>타입 파라미터 선언</li>
<li>수신객체의 타입 파라미터</li>
<li>반환 객체의 타입 파라미터</li>
</ol>
</li>
</ul>
<h3 id="컬렉션-호출-시-타입-인자를-명시적으로-지정할-수-있으나-대부분-컴파일러가-추론할-수-있음">컬렉션 호출 시 타입 인자를 명시적으로 지정할 수 있으나 대부분 컴파일러가 추론할 수 있음</h3>
<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>
<h3 id="filter-함수">filter 함수</h3>
<pre><code class="language-kotlin">// 함수 시그니처
fun &lt;T&gt; List&lt;T&gt;.filter(predicate: (T) -&gt; Boolean): List&lt;T&gt;</code></pre>
<pre><code class="language-kotlin">fun main() {
        val authors = listOf(&quot;Sveta&quot;, &quot;Seb&quot;, &quot;Roman&quot;, &quot;Dima&quot;)
        val readers = mutableListOf(&quot;Seb&quot;, &quot;Hadi&quot;)
        println(readers.filter { it !in authors })
        // [Hadi]
}</code></pre>
<ul>
<li>람다 파라미터의 it 변수의 타입은 컴파일러에 의해 추론됨<ul>
<li>filter의 수신 객체인 readers의 타입인 List<String>로 부터 it의 타입이 String임을 추론</li>
</ul>
</li>
</ul>
<h3 id="타입-파라미터를-사용할-수-있는-위치">타입 파라미터를 사용할 수 있는 위치</h3>
<ul>
<li>클래스나 인터페이스 안에 정의된 메서드</li>
<li>최상위 함수</li>
<li>확장 함수<ul>
<li>수신 객체, 파라미터 타입</li>
</ul>
</li>
</ul>
<h3 id="제네릭-확장-프로퍼티">제네릭 확장 프로퍼티</h3>
<ul>
<li>제네릭 함수를 구현하는 구문으로 제네릭 확장 프로퍼티를 선언할 수 있음</li>
</ul>
<pre><code class="language-kotlin">// 모든 List 타입에 대한 확장 파라미터
val &lt;T&gt; List&lt;T&gt;.penultimate: T
        get() = this[size - 2]

fun main() {
        println(listOf(1, 2, 3, 4).penultimate) // 파라미터 타입이 Int로 추론됨
        //3
}</code></pre>
<hr>
<h2 id="제네릭-클래스-선언">제네릭 클래스 선언</h2>
<ul>
<li>자바와 마찬가지로 홑화살괄호를 클래스나 인터페이스 이름 뒤에 작성<ul>
<li>타입 파라미터를 다른 일반 타입처럼 사용할 수 있음</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">interface List&lt;T&gt; {
    operator fun get(index: Int): T
    /* ... */
} </code></pre>
<h3 id="제네릭-클래스의-확장-클래스">제네릭 클래스의 확장 클래스</h3>
<ul>
<li>기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정<ul>
<li>구체적인 타입을 넘기거나</li>
<li>타입 파라미터로 받은 타입을 넘길 수 있음</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">// 구체적인 타입 지정
class StringList : List&lt;T&gt; {
        override fun get(index: Int): String = this[index]
        /* ... */
}

// 제네릭 타입 파라미터를 List의 타입 인자로 넘김
class ArrayList&lt;T&gt;: List&lt;T&gt; {
        override fun get(index: Int): T = this[index]
        /* ... */
}</code></pre>
<ol>
<li>StringList<ul>
<li>String 타입의 원소만 저장 → String을 기반 타입으로 사용</li>
<li>하위 클래스에서 함수를 오버라이드 하거나 사용하려면 타입 인자에 String을 명시해야 함<ul>
<li>ex) get 함수의 반환 타입으로 String을 명시함</li>
</ul>
</li>
</ul>
</li>
<li>ArrayList<ul>
<li>자신만의 타입 파라미터를 정의하면서 기반 클래스의 타입 인자로 사용<ul>
<li>ArrayList의 T와 List의 T는 같지 않음</li>
</ul>
</li>
</ul>
</li>
</ol>
<ul>
<li>클래스가 자신을 타입 인자로 참조할 수 있음</li>
</ul>
<pre><code class="language-kotlin">// Comparable 인터페이스 구현 예제
interface Comparable&lt;T&gt; {
        fun compareTo(other: T): Int
}

class String : Comparable&lt;String&gt; {
        override fun compareTo(other: String): Int = TODO()
}</code></pre>
<hr>
<h2 id="타입-파라미터-제약">타입 파라미터 제약</h2>
<ul>
<li>클래스나 함수에 사용할 수 있는 타입을 제한하는 기능<ul>
<li>ex) 특정 함수에 대해 타입 파라미터로 숫자 타입 만을 허용하도록 정의할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="어떤-타입을-제네릭-타입의-상계upper-bound로-정의">어떤 타입을 제네릭 타입의 상계(upper bound)로 정의</h3>
<ul>
<li>제네릭 타입을 인스턴스화할 때 사용하는 타입 인자는 반드시 그 상계 타입이거나 그 상계 타입의 하위 타입이어야 함</li>
</ul>
<pre><code class="language-kotlin">// 상계 지정 방법
fun &lt;T : Number&gt; List&lt;T&gt;.sum(): T</code></pre>
<ul>
<li>타입 파라미터 이름 뒤에 콜론( : )을 표시하고 그 뒤에 상계 타입 명시</li>
</ul>
<pre><code class="language-kotlin">fun main() {
        println(ListOf(1, 2, 3, 4).sum())
        // 10
}</code></pre>
<ul>
<li>Number → 코틀린 표 준 라이브러리의 숫자 타입의 최상위 클래스</li>
<li>상계를 정하고 나면 T 타입의 값을 상계 타입으로 취급할 수 있음</li>
</ul>
<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>
<h3 id="두-값-비교-예제">두 값 비교 예제</h3>
<pre><code class="language-kotlin">fun &lt;T: Comparable&lt;T&gt;&gt; max(first: T, second: T): T {
        return if (first &gt; second) first else second
}

fun main() {
        println(max(&quot;kotlin&quot;, &quot;java&quot;))
        //kotlin
}</code></pre>
<ul>
<li>비교할 수 없는 값을 입력하면 커파일 오류 발생</li>
</ul>
<pre><code class="language-kotlin">println(max(&quot;kotlin&quot;, 42))
// ERROR: Type parameter bound for T is not satisfied:
//         inferred type Any is not a subtype of Comparable&lt;Any&gt;</code></pre>
<h3 id="타입-파라미터에-둘-이상의-제약-선언">타입 파라미터에 둘 이상의 제약 선언</h3>
<pre><code class="language-kotlin">fun &lt;T&gt; ensureTrailingPeriod(seq: T) where T : CharSequence, T : Appendable { // 타입 파라미터 제약 목록
        if(!seq.endWith(&#39;.&#39;)) { // CharSequence 인터페이스의 확장 함수 호출
                seq.append(.) // Appendable 인터페이스에 정의된 메서드 호출
        }
}

fun main() {
        val helloWorld = StringBuilder(&quot;Hello World&quot;)
        ensureTrailingPeriod(helloWorld)
        println(helloWorld)
        //Hello World.
}</code></pre>
<ul>
<li>StringBuilder → CharSequence와 Appendable을 모두 사용하는 클래스</li>
</ul>
<hr>
<h2 id="널이-될-수-없는-타입-제한하기">널이 될 수 없는 타입 제한하기</h2>
<ul>
<li>상계를 정하지 않은 타입 파라미터 → Any? 타입으로 치환</li>
<li>널 가능성을 제한하려면 명시적으로 타입 상계를 지정해줘야 함</li>
</ul>
<pre><code class="language-kotlin">class Processor&lt;T: Any&gt; {
        fun process(value: T) {
                value.hashCode()
        }
}</code></pre>
<ul>
<li>&lt;T: Any&gt; → T 타입이 항상 널이 될 수 없는 타입이 되도록 보장</li>
<li>Any 타입 외에도 다른 널이 될 수 없는 타입을 사용해 상계를 정할 수 있음</li>
</ul>
<h3 id="자바와의-상호-운용">자바와의 상호 운용</h3>
<ul>
<li>자바에서 어노테이션 @NotNull을 사용해 널값을 차단한 경우<ul>
<li>&lt;t : T &amp; Any&gt;와 같은 구문으로 절대로 널이 될 수 없는 값으로 표시할 수 있음</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다에서 반환: 고차 함수에서 흐름 제어]]></title>
            <link>https://velog.io/@first_woosun/%EB%9E%8C%EB%8B%A4%EC%97%90%EC%84%9C-%EB%B0%98%ED%99%98-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98%EC%97%90%EC%84%9C-%ED%9D%90%EB%A6%84-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@first_woosun/%EB%9E%8C%EB%8B%A4%EC%97%90%EC%84%9C-%EB%B0%98%ED%99%98-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98%EC%97%90%EC%84%9C-%ED%9D%90%EB%A6%84-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Fri, 20 Feb 2026 05:28:31 GMT</pubDate>
            <description><![CDATA[<h2 id="람다-안의-return문-람다를-둘러싼-함수에서-반환">람다 안의 return문: 람다를 둘러싼 함수에서 반환</h2>
<ul>
<li>람다 안에서 return → 그 람다를 호출한 함수의 실행이 종료되고 반환됨</li>
<li>return이 선언된 람다를 호출한 함수를 반환하게 만드는 return 문을 <strong>“비로컬 return”</strong>이라고 함</li>
</ul>
<pre><code class="language-java">data class Person(val name: String, val age: Int)

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

fun lookForAlice(people: List&lt;Person&gt;) {
        people.forEach {
                if(it.name == &quot;Alice&quot;){
                        println(&quot;Found!&quot;)
                        return // 비로컬 return
                }
        }
        println(&quot;Alice is not found&quot;)
}</code></pre>
<ul>
<li>return이 바깥 블록을 반환시키는 경우 → 인라인 함수에서만 가능</li>
</ul>
<hr>
<h2 id="람다로부터-반환-레이블을-사용한-return">람다로부터 반환: 레이블을 사용한 return</h2>
<ul>
<li><p>람다식에도 로컬 return문을 쓸 수 있음</p>
</li>
<li><p>람다의 로컬 return → break와 비슷한 역할</p>
<ul>
<li>람다의 실행을 멈추고 람다를 호출한 함수의 실행을 이어감</li>
</ul>
</li>
<li><p>로컬 return과 비로컬 return을 구분하는 방법 → 레이블 사용</p>
<pre><code class="language-java">  fun lookForAlice(people: List&lt;Person&gt;) {
          people.forEach label@{ // 람다 식 앞에 레이블 작성
                  if(it.name != &quot;Alice&quot;) return@label // 리턴 식 뒤에 레이블 작성
                  println(&quot;Found Alice!&quot;) // return이 실행되지 않았을 경우 출력
          }
  }</code></pre>
<ul>
<li><p>람다 식 앞에 레이블을 붙이고 return 키워드 뒤에 레이블을 추가</p>
<ul>
<li>람다 레이블 → 레이블 이름 뒤에 @</li>
<li>리턴 레이블 → 리턴 키워드 뒤에 @와 레이블 이름</li>
</ul>
</li>
<li><p>람다를 인자로 밭은 함수의 이름을 사용해도 됨</p>
<pre><code class="language-java">  people.forEach {
          if(it.name != &quot;Alice&quot;) return@forEach
          println(&quot;Found Alice!&quot;)
  } </code></pre>
</li>
</ul>
</li>
<li><p>람다식에는 레이블 이름이 1개만 붙을 수 있음</p>
</li>
</ul>
<h3 id="this-식의-레이블">this 식의 레이블</h3>
<ul>
<li>수신 객체 지정 람다 → this로 수신 객체 지정 (13장 내용)</li>
<li>수신 객체 지정 람다 앞에 레이블을 붙인 경우 this 뒤에 그 레이블을 붙여 수신객체를 지정할 수 있음</li>
</ul>
<pre><code class="language-java">fun main() {
        println(StringBuilder().apply sb@{ // this@sb를 통해 이 람다의 수신 객체에 접근 가능
                listOf(1, 2, 3).apply {
                        this@sb.append(this.toString())
                }
        })
        // [1, 2, 3]
}</code></pre>
<hr>
<h2 id="익명-함수--기본적으로-로컬-return">익명 함수 : 기본적으로 로컬 return</h2>
<ul>
<li>익명 함수 → 람다를 정의하는 또 다른 방법<ul>
<li>함수에 전달할 수 있는 코드 블록 작성 가능</li>
</ul>
</li>
<li>익명 함수에서 return을 쓰면 기본적으로 로컬 return임<ul>
<li>레이블을 지정할 필요 없음</li>
</ul>
</li>
</ul>
<pre><code class="language-java">fun findForAlice(people: List&lt;Person&gt;) {
        people.forEach(fun (person) {
                if(person.name == &quot;Alice&quot;) return
                println(&quot;${person.name} is not Alice&quot;)
        })
}</code></pre>
<ul>
<li>함수 이름 생략</li>
<li>파라미터 타입을 컴파일러가 추론해줌</li>
</ul>
<pre><code class="language-java">people.filter (fun (person): Boolean {
        return person.age &lt; 30
})</code></pre>
<ul>
<li>익명 함수에도 반환 타입을 지정할 수 있음</li>
</ul>
<pre><code class="language-java">people.filter(fun (person) = person.age &lt; 30)</code></pre>
<ul>
<li>식을 본문으로 사용할 수 있음</li>
<li>식을 본문으로 사용할 경우 반환 타입은 생략할 수 있음</li>
</ul>
<h3 id="return은-가장-가까운-fun-키워드를-반환시킴">return은 가장 가까운 fun 키워드를 반환시킴</h3>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/ba0cd9db-eac0-40ad-a6fc-04f759207d50/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인라인 함수로 람다의 부가 비용 없애기]]></title>
            <link>https://velog.io/@first_woosun/%EC%9D%B8%EB%9D%BC%EC%9D%B8-%ED%95%A8%EC%88%98%EB%A1%9C-%EB%9E%8C%EB%8B%A4%EC%9D%98-%EB%B6%80%EA%B0%80-%EB%B9%84%EC%9A%A9-%EC%97%86%EC%95%A0%EA%B8%B0</link>
            <guid>https://velog.io/@first_woosun/%EC%9D%B8%EB%9D%BC%EC%9D%B8-%ED%95%A8%EC%88%98%EB%A1%9C-%EB%9E%8C%EB%8B%A4%EC%9D%98-%EB%B6%80%EA%B0%80-%EB%B9%84%EC%9A%A9-%EC%97%86%EC%95%A0%EA%B8%B0</guid>
            <pubDate>Fri, 20 Feb 2026 05:27:21 GMT</pubDate>
            <description><![CDATA[<h3 id="람다의-부가비용">람다의 부가비용?</h3>
<ul>
<li>람다 → 익명 클래스로 컴파일<ul>
<li>람다식마다 새로운 클래스가 생김</li>
<li>람다가 변수를 캡처한 경우 호출할 때마다 새로운 객체가 생김</li>
<li>같은 기능을 하는 코드를 직접 실행하는 것보다 효율성이 떨어짐</li>
</ul>
</li>
</ul>
<h3 id="인라인-함수">인라인 함수?</h3>
<ul>
<li>inline 변경자를 통해 선언</li>
<li>inline 변경자가 선언된 함수를 호출하면 컴파일러는 그 위치에 함수를 구현한 코드를 넣어줌</li>
</ul>
<hr>
<h2 id="인라이닝-작동-방식">인라이닝 작동 방식</h2>
<ul>
<li>어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인됨<ul>
<li>인라인으로 선언한 함수를 호출하는 코드가 함수 본문을 번역한 바이트코드로 컴파일 됨</li>
</ul>
</li>
<li>인라인 함수에 람다를 인자로 넘기면 람다의 본문도 함께 인라이닝 됨<ul>
<li>이 람다로 만들어진 바이트코드는 람다를 인자로 받은 함수의 일부분으로 간주되어 익명 클래스를 생성하지 않음</li>
</ul>
</li>
<li>인라인 함수에 람다 대신 함수 타입의 변수를 넘길 수도 있음<ul>
<li>변수에 저장된 람다의 코드를 알 수 없어 람다 본문은 인라이닝 되지 않음</li>
<li>일반적인 람다 호출과 같이 컴파일 됨</li>
</ul>
</li>
</ul>
<hr>
<h2 id="인라인-함수의-제약">인라인 함수의 제약</h2>
<ul>
<li>람다를 사용하는 모든 함수를 인라이닝할 수는 없음<ul>
<li>함수 본문에서 파라미터로 받은 람다를 호출하면 인라이닝됨</li>
<li>람다를 변수에 저장하고 그 변수를 호출하면 람다를 표현하는 객체가 존재해야 하기 때문에 인라이닝 할 수 없음</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">class FunctionStorage {
        var myStorageFunction((Int) -&gt; Unit)? = null
        inline fun storerFunction(f: (Int) -&gt; Unit) {
                myStorageFunction = f // 전달된 파라미터를 저장
        }
}</code></pre>
<ul>
<li>컴파일러는 Illegal usage of inline-parameter라는 메시지와 함께 인라이닝을 금지시킴</li>
</ul>
<h3 id="여러-람다-중-일부만-인라이닝-하기">여러 람다 중 일부만 인라이닝 하기</h3>
<ul>
<li>noinline 변경자를 파라미터 이름 앞에 붙여 인라이닝을 제한할 수 있음</li>
</ul>
<pre><code class="language-kotlin">inline fun foo(inline: () -&gt; Unit, noinline NotInLined: () -&gt; Unit) {
        /* ... */
}</code></pre>
<hr>
<h2 id="컬렉션-연산-인라이닝">컬렉션 연산 인라이닝</h2>
<ul>
<li>코틀린 표준 라이브러리의 컬렉션 함수 → 대부분 람다를 인자로 받음<ul>
<li>이 람다가 성능에 악형향을 끼치는가?</li>
</ul>
</li>
</ul>
<ol>
<li><p>람다 사용</p>
<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>
</li>
<li><p>직접 기능 구현</p>
<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() {
         val result = mutableListOf&lt;Person&gt;()
         for (person in people) {
                 if(person.age &gt; 30) result.add(person)
         }
         println(result)
         // [Person(name=Alice, age=29)]
 }</code></pre>
<ul>
<li>1번 코드와 2번 코드의 바이트 코드는 거의 동일함<ul>
<li>filter 함수 → 인라인 함수</li>
<li>filter 함수의 바이트코드는 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 인라이닝 됨</li>
</ul>
</li>
<li>코틀힌 표준 라이브러리에서 제공하는 인라인 함수는 성능 걱정없이 사용해도 무관함</li>
</ul>
</li>
</ol>
<h3 id="중간-리스트의-부가-비용">중간 리스트의 부가 비용</h3>
<pre><code class="language-kotlin">fun main() {
        println(
                people.filter { it.age &gt; 30 }
                .map(Person::name)
        )
        // [Bob]
}</code></pre>
<ul>
<li>filter와 map의 본문은 인라이닝됨</li>
<li>하지만 filter에서 걸러낸 결과를 저장하는 중간 리스트를 만듬</li>
<li>map에서 이 중간 리스트를 읽어 최종 결과를 만들어냄</li>
</ul>
<p>처리할 원소가 적다면 중간 리스트의 부가 비용이 크지 않다면 원소가 많아질수록 중간 리스트의 부가 비용을 고려해야 함</p>
<h3 id="assequence를-통한-부가-비용-감소">asSequence를 통한 부가 비용 감소</h3>
<ul>
<li>asSequence를 통해 리스트 대신 시퀀스로 처리하면 중간 리스트로 인한 부가 비용을 줄일 수 있음<ul>
<li>각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현됨</li>
<li>최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출함</li>
</ul>
</li>
<li>시퀀스 연산에서는 람다가 인라이닝 되지 않음<ul>
<li>크기가 작은 컬렉션은 일반 컬렉션 연산이 더 성능이 좋을 때도 있음</li>
<li>시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션의 크기가 큰 경우임</li>
</ul>
</li>
</ul>
<hr>
<h2 id="인라인-함수는-언제-선언하는가">인라인 함수는 언제 선언하는가</h2>
<p>인라인 키워드를 사용한다고 무조건 성능 개선 효과를 얻는 것은 아님</p>
<p>람다를 인자로 받는 함수만 성능이 좋아질 가능성이 있음</p>
<ul>
<li>일반 함수 → 인라인 키워드를 사용해도 이익이 거의 없음<ul>
<li>일반 함수는 바이트코드에서 각 함수에 대한 구현이 딱 한번만 있으면 됨</li>
<li>함수를 호출하는 부분에서 따로 중복이 발생하지 않음</li>
</ul>
</li>
<li>코틀린 인라인 함수<ul>
<li>바이트코드에서 각 함수 호출 지점을 인라인 함수 본문으로 대치하여 코드 중복이 발생함</li>
</ul>
</li>
<li>람다를 인자로 받는 인라인 함수<ul>
<li>인라이닝을 통해 없앨 수 있는 부가 비용이 많음<ul>
<li>함수 호출 비용을 줄일 수 있음</li>
<li>람다를 표현하는 익명 클래스의 객체를 만들 필요 없음</li>
</ul>
</li>
<li>일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있음 (비로컬 return 등)</li>
</ul>
</li>
</ul>
<p>함수의 크기가 큰 경우 인라인이 비효율적일 수 있음</p>
<ul>
<li>함수의 바이트코드를 모든 호출 지점에 복사해 넣으면 바이트 코드 전체의 크기가 커질 수 있음</li>
</ul>
<hr>
<h2 id="자원-관리를-위해-인라인된-람다-사용">자원 관리를 위해 인라인된 람다 사용</h2>
<p>람다로 중복을 없앨 수 있는 일반적인 패턴 중 한가지 → 자원 관리</p>
<ul>
<li>보통 try/finally문으로 자원 획득과 자원 해제를 제어</li>
</ul>
<h3 id="withlock">withLock</h3>
<ul>
<li>Lock 인터페이스의 확장 함수</li>
</ul>
<pre><code class="language-kotlin">// 코틀린 라이브러리의 withLock 함수 정의
fun &lt;T&gt; Lock.withLock(action: () -&gt; T): T {
        lock()
        try {
                return action()
        } finally {
                unlock()
        }
}

// withLock 사용 방법
val l: Lock = ReentrantLock()
l.withLock{
        // 락에 의해 보호되는 자원 사
}</code></pre>
<h3 id="use-함수">use 함수</h3>
<ul>
<li>닫을 수 있는 자원(Closeable)에 대해 호출하는 확장 함수</li>
<li>람다를 호출하고 사용 후 자원을 확실하게 닫음</li>
<li>use 함수는 인라인 함수임</li>
</ul>
<pre><code class="language-kotlin">import java.io.BufferedReader
import java.io.FileReader

fun readFirstLineFromfile(fileName: String): String {
        BufferReader(FileReader(fileReader)).use { br -&gt; 
                return br.readLine()        
        }
}</code></pre>
<h3 id="uselines-함수">useLines 함수</h3>
<ul>
<li>File, Path 객체에 대해 정의돼 있음</li>
<li>람다가 문자열 시퀀스에 접근하게 해줌</li>
</ul>
<pre><code class="language-kotlin">import kotlin.io.path.Path
import kotlin.io.path.useLines

fun readFirstLineFromFile(fileName: String): String {
        Path(fileName).useLines {
                return it.first()
        }
}</code></pre>
<blockquote>
<h3 id="코틀린에서는-try-with-resources를-사용하지-말라">코틀린에서는 try-with-resources를 사용하지 말라</h3>
<ul>
<li>try-with-resources → 자바에서 Closeable 자원에 사용할 수 있는 구문</li>
</ul>
<pre><code class="language-java">static String readFirstLineFromFile(String fileName) throw IOException {
        try(BufferfReader br = new BufferdReader(new FileReader(fileName))) {
                return br.readLine();
        }
}</code></pre>
<ul>
<li>이 기능을 코틀린에서 use 같은 함수를 통해 간단하게 구현할 수 있음</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[다른 함수를 인자로 받거나 반환하는 함수 정의 : 고차 함수]]></title>
            <link>https://velog.io/@first_woosun/%EB%8B%A4%EB%A5%B8-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%9D%B8%EC%9E%90%EB%A1%9C-%EB%B0%9B%EA%B1%B0%EB%82%98-%EB%B0%98%ED%99%98%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98-%EC%A0%95%EC%9D%98-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@first_woosun/%EB%8B%A4%EB%A5%B8-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%9D%B8%EC%9E%90%EB%A1%9C-%EB%B0%9B%EA%B1%B0%EB%82%98-%EB%B0%98%ED%99%98%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98-%EC%A0%95%EC%9D%98-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98</guid>
            <pubDate>Mon, 16 Feb 2026 11:46:38 GMT</pubDate>
            <description><![CDATA[<p>고차 함수 → 다른 함수를 인자로 받거나 반환하는 함수</p>
<ul>
<li>람다 or 함수 참조를 통해 함수를 값으로 표현할 수 있음</li>
<li>이를 통해 함수를 인자로 넘기거나 반환 받는 것이 가능</li>
</ul>
<p>filter 함수도 고차 함수임</p>
<ul>
<li>람다를 인자로 받기 때문</li>
</ul>
<pre><code class="language-jsx">list.filter { it &gt; 0 }</code></pre>
<p>이제 고차 함수를 정의하는 방법을 알아본다. 고차 함수를 정의하려면 먼저 함수 타입을 알아야 함</p>
<hr>
<h2 id="함수-타입은-람다의-파라미터-타입과-반환-타입을-지정한다">함수 타입은 람다의 파라미터 타입과 반환 타입을 지정한다</h2>
<h3 id="람다를-로컬-변수에-대입">람다를 로컬 변수에 대입</h3>
<ul>
<li>코틀린의 타입 추로으로 변수 타입을 지정하지 않아도 람다를 변수에 대입할 수 있음</li>
</ul>
<pre><code class="language-kotlin">val sum = { x: int, y: Int -&gt; x + y } 
val action = { println(42) }</code></pre>
<ul>
<li>컴파일러 → sum과 action이 함수 타입임을 추론함</li>
</ul>
<h3 id="각-변수에-구체적인-타입-선언-추가">각 변수에 구체적인 타입 선언 추가</h3>
<pre><code class="language-kotlin">val sum: (Int, Int) -&gt; Int = {x, y -&gt; x + y}
val action: () -&gt; Unit = { println(42) }</code></pre>
<ul>
<li>함수 파라미터의 타입을 괄호 안에 명시</li>
<li>그 뒤에 화살표를 추가</li>
<li>화살표 다음에 함수의 반환 타입을 지정</li>
<li>Unit 타입 → 값을 반환하지 않는 함수 반환 타입에 쓰는 특별한 타입<ul>
<li>함수 타입을 선언할 때는 반환할 때는 반환 타입을 반드시 명시해야 하므로 Unit 타입을 명시해야 함</li>
</ul>
</li>
<li>함수 타입을 명시한다면 람다 내부에는 타입을 생략해도 됨</li>
</ul>
<h3 id="함수-타입도-반환-타입을-null이-될-수-있는-타입으로-지정할-수-있음">함수 타입도 반환 타입을 null이 될 수 있는 타입으로 지정할 수 있음</h3>
<pre><code class="language-kotlin">var canReturnNull: (Int: Int) -&gt; Int? = { x, y -&gt; null }</code></pre>
<h3 id="함수-타입-자체를-null이-될-수-있는-타입으로-정의할-수-있음">함수 타입 자체를 null이 될 수 있는 타입으로 정의할 수 있음</h3>
<ul>
<li>힘수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야 함</li>
</ul>
<pre><code class="language-kotlin">var funOrNull: ((Int, Int) -&gt; Int)? = null</code></pre>
<hr>
<h2 id="인자로-전달-받은-함수-호출">인자로 전달 받은 함수 호출</h2>
<p>고차 함수를 어떻게 구현하는지 알아본다</p>
<h3 id="간단한-고차-함수-정의">간단한 고차 함수 정의</h3>
<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>
<ul>
<li>인자로 받은 함수를 호출하는 방법은 일반 함수를 호출하는 구문과 같음</li>
</ul>
<blockquote>
<h3 id="파라미터-이름과-함수-타입">파라미터 이름과 함수 타입</h3>
</blockquote>
<ul>
<li>함수 타입에서 파라미터 이름을 지정할 수 있음<pre><code class="language-kotlin">fun twoAndThree(operation: (operandA: Int, operandB: Int) -&gt; Int) { // 함수 타입인 파라미터를 선언
      val result = operation(2, 3) // 함수 타입인 파라미터를 호출
      println(&quot;The result is $result&quot;)
}
fun main() { 
      twoAndThree { operandA, operandB -&gt; operandA + operandB } // API에서 지정한 이름을 람다에 사용할 수 있음
      // The result is 5
      twoAndThree { alpha, beta -&gt; alpha + beta } // 그냥 원하는 이름도 사용할 수 있음
      // The result is 5
}</code></pre>
<blockquote>
</blockquote>
</li>
</ul>
<h3 id="filter-함수-구현-예제">filter 함수 구현 예제</h3>
<p>예제를 단순하게 유지하기 위해 String에 대한 filter를 구현</p>
<pre><code class="language-kotlin">fun String.filter(predicate: (Char) -&gt; Boolean): String { 
        return buildString { 
                for (char in this@filter) { //입력 문자열 이터레이션
                        if (predicate(char)) append(char) // predicte 파라미터로 전달받은 함수를 호출
                }
        }
}

fun main() { 
        println(&quot;ab1c&quot;.filter { it in &#39;a&#39; .. &#39;z&#39; })
        ///abc
}</code></pre>
<ul>
<li>filter 함수 → 문자열의 각 문자가 술어를 만족하는지 검사</li>
<li>술어를 만족하는 문자는 StringBuilder의 append를 사용해 결과를 만들고 반환</li>
<li>레이블이 붙은 this → 10.6절에서 자세히 다룸</li>
</ul>
<hr>
<h2 id="자바에서-코틀린-함수-타입-사용">자바에서 코틀린 함수 타입 사용</h2>
<ul>
<li>SAM 변환을 통해 코틀린 람다를 자바 메서드에 옮길 수 있음</li>
<li>함수 타입을 사용하는 코틀린 코드를 자바에서 쉽게 호출할 수 있음’</li>
<li>자바 람다는 자동으로 코틀린 함수 타입으로 변환됨</li>
</ul>
<pre><code class="language-kotlin">/* 코틀린 선언*/
fun processTheAnswer(f: (Int) -&gt; Int) {
        println(42)
}

/* 자바 호출 */
processTheAnswer(number -&gt; number + 1);</code></pre>
<h3 id="자바에서-코틀린-함수-타입-호출시-수신-객체를-명시해줘야-함">자바에서 코틀린 함수 타입 호출시 수신 객체를 명시해줘야 함</h3>
<pre><code class="language-kotlin">import kotlin.collections.CollectionsKt;

/ *... */
public static void main(String[] arg) {
        List&lt;String&gt; strings = new ArrayList();
        strings.add(&quot;42&quot;);
        CollectionsKt.forEach(strings, s -&gt; { // 코틀린 표준 라이브러리 함수 호출 가능
                System.out.println(s);
                return Unit.INSTANCE; // Unit 타입의 값을 명시적으로 반환해야 함
        });
}</code></pre>
<ul>
<li>Unit을 반환하는 함수나 람다를 호출할 때는 Unit 타입의 값을 명시적으로 반환해줘야 함<ul>
<li>Unit 타입에는 값이 존재하기 때문</li>
</ul>
</li>
<li>값이 존재하기 때문에 Unit 타입을 반환하는 코틀린 함수에 void 타입 자바 람다를 넘길 수 없음</li>
</ul>
<h3 id="함수-타입의-자세한-동작-방식">함수 타입의 자세한 동작 방식</h3>
<ul>
<li>함수 타입의 변수 → FunctionN 인터페이스를 구현<ul>
<li>인자의 개수에 따라 FunctionN의 N값이 바뀜</li>
</ul>
</li>
<li>각 인터페이스에는 invoke라는 유일한 메서드가 있음<ul>
<li>이 메서드를 호출하면 함수가 호출됨</li>
<li>invoke에 대해선 13장에 자세히 다룸</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">interface Function1&lt;P1, out R&gt;{
        operator fun invoke(p1: P1): R
}</code></pre>
<ul>
<li>함수 타입의 변수 → 함수에 대응하는 FunctionN 인터페이스를 구현하는 클래스의 인스턴스<ul>
<li>invoke 메서드에는 람다 본문이 들어감</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">fun processTheAnswer(f: Function1&lt;Int, Int&gt;) {
        println(f.invoke(42))
}</code></pre>
<ul>
<li>FunctionN 인터페이스 → 컴파일러가 생성한 합성 타입으로 코틀린 표준 라이브러리에서 이들의 정의를 찾을 수 없음<ul>
<li>컴파일러가 필요할 때 이런 함수를 생성</li>
<li>파라미터의 개수 제한 없이 파라미터를 사용하는 함수에 대한 인터페이스를 사용할 수 있음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="함수-타입의-파라미터-기본값-지정과-null-이-될-수-있는-타입">함수 타입의 파라미터 기본값 지정과 null 이 될 수 있는 타입</h2>
<h3 id="함수-타입에-기본값-지정">함수 타입에 기본값 지정</h3>
<p>함수 타입의 파라미터에 대한 기본값으로 람다식을 지정해놓으면 호출할 때마다 람다를 넘겨주지 않아도 되고 필요 시 람다를 넘겨 원하는 기능을 추가할 수 있음</p>
<pre><code class="language-kotlin">fun &lt;T&gt; Collection&lt;T&gt;.joinToString(
        separator: String = &quot;,&quot;,
        prefix: String,
        postfix: String,
        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)) // transform 파라미터에 대한 인자로 받은 함수 호출
        }

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

fun main() {
        val letters = listOf(&quot;Alpha&quot;, &quot;Beta&quot;)
        println(letters.joinToString()) //transform 기본 함수 사용
        // Alpha, Beta
        println(letters.joinToString { it.lowercase() }) // 람다를 인자로 전달
        // alpha, beta
        println(letters.joinToString(seperator=&quot;! &quot;, postfix=&quot;! &quot;, transform={ it.uppercase() }))
        // 이름 붙은 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달
        // ALPHA! BETA!
}</code></pre>
<ul>
<li>함수 타입에 대한 기본값 선언도 = 뒤에 람다를 넣으면 됨</li>
</ul>
<h3 id="null이-될-수-있는-함수-타입">null이 될 수 있는 함수 타입</h3>
<ul>
<li>null이 될 수 있는 함수 타입으로 하수를 받으면 그 함수를 직접 호출할 수 없음<ul>
<li>null 여부를 명시적으로 검사하면 호출은 할 수 있음</li>
<li>invoke를 사용하여 간결하게 해결할 수도 있음</li>
</ul>
</li>
<li>함수 타입 → invoke 메서드를 구현하는 인터페이스<ul>
<li>일반 메서드처럼 invoke도 안전한 호출을 사용할 수 있음</li>
</ul>
</li>
</ul>
<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>
<h3 id="함수를-반환하는-함수">함수를 반환하는 함수</h3>
<pre><code class="language-kotlin">enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

fun getShppingCostCalculator(delivery: Delivery): (Order) -&gt; Double { // 함수를 반환하는 함수 선언
        if(delivery == Delivery.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>
<ul>
<li>함수에서 함수를 반한하려면 반환 타입으로 함수 타입을 지정해줘야 함<ul>
<li>getShppingCostCalculator는 Order 객체를 받아 Double 타입을 반환하는 함수를 반환함</li>
</ul>
</li>
<li>함수를 반환하려면 return식에 람다, 맴버 참조, 함수 타입의 값을 계산하는 식을 넣으면 됨</li>
</ul>
<h3 id="함수를-반환하는-함수를-ui-코드에서-사용하기">함수를 반환하는 함수를 UI 코드에서 사용하기</h3>
<pre><code class="language-kotlin">data class Person(
    val firstName: String,
    val lastName: String,
    val phoneNumber: String?
)

class ContactListFilters {
    var prefix:String = &quot;&quot;
    var onlyWithPhoneNumber: Boolean = false

    fun getPrediction(): (Person) -&gt; Boolean { // 함수를 반환하는 함수 정의
        val startWithPrefix = { p: Person -&gt;
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if(!onlyWithPhoneNumber) {
            return startWithPrefix // 함수 타입의 변수 반환
        }
        return { startWithPrefix(it) &amp;&amp; it.phoneNumber != null } // 람다를 반환
    }
}

fun main() {
    val contact = listOf(
        Person(&quot;Dmitry&quot;, &quot;Jemerov&quot;, &quot;123-4567&quot;),
        Person(&quot;Svetlana&quot;, &quot;Isakova&quot;, null)
    )
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = &quot;Dm&quot;
        onlyWithPhoneNumber = true
    }

    println(
        contact.filter(contactListFilters.getPrediction()) // 반환한 함수를 filter에 인자로 넘김
    )
    // [Person(firstName=Dmitry, lastName=Jemerov, phoneNumber=123-4567)]
}</code></pre>
<ul>
<li>getPredicate 메서드 → filter 함수에 인자로 넘길 수 있는 함수를 반환함</li>
</ul>
<hr>
<h2 id="람다를-통한-코드-재사용성-높이기">람다를 통한 코드 재사용성 높이기</h2>
<p>람다식 → 재사용성을 높이는 훌륭한 도구</p>
<ul>
<li>반복적으로 사용되는 코드를 람다로 추출해 함수에 인자로 넘기면 코드를 재사용 하면서 코드 중복을 제거할 수 있음</li>
</ul>
<h3 id="웹-사이트-방문-기록-분석-예시">웹 사이트 방문 기록 분석 예시</h3>
<p>데이터 정의</p>
<pre><code class="language-kotlin">data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

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

val log = listOf(
    SiteVisit(&quot;/&quot;, 34.0, OS.WINDOW),
    SiteVisit(&quot;/&quot;, 22.0, OS.MAC),
    SiteVisit(&quot;/login&quot;, 12.0, OS.WINDOW),
    SiteVisit(&quot;/signup&quot;, 8.0, OS.IOS),
    SiteVisit(&quot;/&quot;, 16.3, OS.ANDROID)
)</code></pre>
<ol>
<li><p>사이트 방문을 하드 코딩한 필터</p>
<pre><code class="language-kotlin"> val averageWindowsDuration = log
     .filter { it.os == OS.WINDOW }
     .map(SiteVisit::duration)
     .average()

 fun main() {
     println(averageWindowsDuration)
     //23.0
 }</code></pre>
</li>
<li><p>일반 함수를 통해 코드 중복 제거</p>
<pre><code class="language-kotlin"> fun List&lt;SiteVisit&gt;.averageDurationFor(os: OS) =
     filter { it.os == os }.map(SiteVisit::duration).average()

 fun main() {
     println(log.averageDurationFor(OS.WINDOW))
     // 23.0
     println(log.averageDurationFor(OS.MAC))
     // 22.0
 }</code></pre>
<ul>
<li>OS를 파라미터로 뽑아내 코드 중복을 해결</li>
</ul>
</li>
<li><p>하드 코딩된 필터를 로컬 함수로 정의하기</p>
</li>
</ol>
<pre><code class="language-kotlin">fun main() {
    val averageMobileDuration = log
        .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
        .map(SiteVisit::duration)
        .average()
    println(averageMobileDuration)
    //12.15
}</code></pre>
<ol start="4">
<li>고차 함수를 사용해 중복 제거하기</li>
</ol>
<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>
<ul>
<li>중복되는 코드를 고차 함수로 뽑아냄으로써 코드 중복을 제거할 수 있음</li>
<li>뿐 만 아니라 함수 타입을 사용하면 필요한 조건을 파라미터로 뽑아낼 수 있음<ul>
<li>복잡한 로직을 간단하게 구현 가능</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로퍼티 접근자 로직 재활용: 위임 프로퍼티]]></title>
            <link>https://velog.io/@first_woosun/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%A0%91%EA%B7%BC%EC%9E%90-%EB%A1%9C%EC%A7%81-%EC%9E%AC%ED%99%9C%EC%9A%A9-%EC%9C%84%EC%9E%84-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0</link>
            <guid>https://velog.io/@first_woosun/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%A0%91%EA%B7%BC%EC%9E%90-%EB%A1%9C%EC%A7%81-%EC%9E%AC%ED%99%9C%EC%9A%A9-%EC%9C%84%EC%9E%84-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0</guid>
            <pubDate>Mon, 16 Feb 2026 11:44:18 GMT</pubDate>
            <description><![CDATA[<ul>
<li>프로퍼티 접근자 로직을 매번 재구현할 필요 없이 쉽게 구현할 수 있음<ul>
<li>자신의 값을 필드가 아닌 DB 테이블, 브라우저 세션, 맵 등에 저장할 수 있음</li>
</ul>
</li>
<li>위임 → 작업을 도우미 객체가 처리하도록 맡기는 디자인 패턴<ul>
<li>도우미 객체 = 위임 객체</li>
</ul>
</li>
<li>도우미 객체를 직접 작성할 수도 있지만 코틀린 언어가 제공하는 기능을 활용할 수도 있음</li>
</ul>
<hr>
<h2 id="위임-프로퍼티의-기본-문법과-내부-동작">위임 프로퍼티의 기본 문법과 내부 동작</h2>
<pre><code class="language-kotlin">import java.lang.reflect.Type

class Delegate(){
    operator fun getValue(/*...*/) { /*...*/ } // getter를 구현하는 로직

    operator fun setValue(/*...*/, value: Type) { /*...*/ } // setter를 구현하는 로직

    operator fun provideDelegate(/*...*/): Delegate { /*...*/ } // 위임 객체 생성, 제공
}

class Foo {
    var p : Type by Delegate() // by 키워드 : 프로퍼티와 위임 객체를 연결
}

fun main() {
    val foo = Foo() // provideDelegate 호출
    val oldValue = foo.p // delegate.getValue 호출
    foo.p = newValue // delegate.setValue 호출
}</code></pre>
<ul>
<li>관례에 따라 Delegate 클래스는 getValue와 setValue를 제공해야 함</li>
<li>변경 가능한 프로퍼티만 setValue응 사용</li>
<li>provideDelegate 함수는 선택적으로 구현<ul>
<li>최초 생성시 검증 로직</li>
<li>위임이 인스턴스화 되는 방식 변경</li>
</ul>
</li>
<li>foo.p → Delegate 타입의 위임 프로퍼티 객체에 있는 메서드를 호출</li>
</ul>
<hr>
<h2 id="by-lazy를-사용한-지연-초기화">by lazy()를 사용한 지연 초기화</h2>
<ul>
<li>객체의 일부분을 초기화하지 않고 나중에 필요할 때 초기화하는 패턴<ul>
<li>초기화 과정에서 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화 하지 않아도 되는 프로퍼티에 사용할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="뒷받침하는-프로퍼티를-사용해-지연-초기화-구현">뒷받침하는 프로퍼티를 사용해 지연 초기화 구현</h3>
<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 // 데이터를 저장하고 emails의 위임 객체 역할을 하는 프로퍼티

    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 // 최초로 emails에 접근할 때만 이메일을 가져옴
    // Alice의 이메일을 가져옴
    p.emails
}</code></pre>
<ul>
<li><p>뒷바침하는 프로퍼티를 사용</p>
<ul>
<li>외부로는 읽기 전용 프로퍼티를 제공하고 내부에는 가변 프로퍼티를 사용</li>
<li>데이터 캡슐화 강화, 가변 데이터의 안전한 관리, 읽기 전용 API 제공</li>
</ul>
</li>
<li><p>_emails → 값을 저장, null이 될 수 있는 타입</p>
</li>
<li><p>emails → _emails에 대한 읽기 연산을 제공, null이 될 수 없는 타입</p>
</li>
<li><p>뒷바침하는 프로퍼티의 이름은 관례를 따름</p>
<ul>
<li>비공개 프로퍼티 앞에 언더바( _ )를 붙임</li>
<li>공개 프로퍼티는 아무것도 붙이지 않음</li>
</ul>
</li>
<li><p>뒷받침하는 프로퍼티 사용의 단점</p>
<ol>
<li>코드가 복잡함</li>
<li>스레드 안전한 구현이 아님<ul>
<li>loadEmails 함수에 여러 스레드에서 동시 접근할 경우 일관성이 망가질 수 있음</li>
</ul>
</li>
</ol>
<ul>
<li>이 문제들을 위임 프로퍼티를 사용해 해결할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="위임-프로퍼티를-통한-지연-초기화-구현">위임 프로퍼티를 통한 지연 초기화 구현</h3>
<ul>
<li>위임 프로퍼티를 사용하면 간결하게 구현할 수 있음<ul>
<li>데이터를 저장할 때 쓰이는 뒷파침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해줌</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">class Person(val name: String) {
        val emails by lazy { loadEmails(this) }
}</code></pre>
<ul>
<li>lazy 함수<ul>
<li>코틀린 관례에 맞는 시그니처의 getValue 메서드가 들어있는 객체를 반환함</li>
<li>by 키워드를 함께 사용해 위임 프로퍼티를 만들 수 있음</li>
<li>인자 → 값을 초기화 할 때 호출할 람다임</li>
<li>기본적으로 thread-safe한 함수임</li>
<li>필요하면 동기화 락을 lazy 함수에 전달할 수 있음</li>
<li>다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 동기화를 생략할 수도 있음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="위임-프로퍼티-구현">위임 프로퍼티 구현</h2>
<h3 id="observable-예시">observable 예시</h3>
<ul>
<li>observable : 어떤 객체를 UI에 표시하는 경우 객체가 바뀌면 자동으로 UI도 바뀌는 프로그램</li>
</ul>
<ol>
<li>위임 프로퍼티 없이 구현</li>
</ol>
<pre><code class="language-kotlin">fun interface Observer {
        fun onChange(name: String, oldValue: Any?, newValue: Any?)
}

open class Observable {
        val observers = mutableList&lt;Observer&gt;()
        fun notifyObservers(propName: String, oldValue: Any?, newValue: Any?) {
                for(obs in observers){
                        obs.onChange(propName, oldValue, newValue)
                }
        }
}</code></pre>
<ul>
<li>Observable 클래스 → Observer 리스트 관리</li>
<li>NotifyObservers → 등록된 모든 Observer의 onChange 함수를 통해 프로퍼티의 이전 값과 새 값을 전달</li>
<li>Observers → onChage에 대한 구현만 제공하면 됨</li>
</ul>
<pre><code class="language-kotlin">class Person(val name: String, age: Int, salary: Int): Observable() {
        var age: Int = age
                set(newValue){
                        val oldValue = field // 뒷받침하는 프로퍼티에 접근할 때 field 식별자를 사용
                        field = newValue
                        notifyObservers(&quot;age&quot;, oldValue, newValue)
                }

        var salary: Int = salary
                set(newValue){
                        val oldValue = field
                        field = newValue
                        notifyObservers(&quot;salary&quot;, oldValue, newValue)
                }
}

fun main() {
        val p = Person(&quot;seb&quot;, 28, 1000) 
        p.observers += Observer {propName, OldValue, newValue -&gt; 
                // 함수형 인터페이스에 대한 간편한 구문을 사용해
                // 옵저버를 생성하고 이를 등록하여 프로퍼티의 변경을 기다림
                println(                                             
                    &quot;&quot;&quot;
                    Property $propName changed from $oldValue to $newValue!
                    &quot;&quot;&quot;.trimIndent()
                )
        } 
        p.age = 29
        // Property age changed from 28 to 29
        p.salary = 1500
        // Property salary changed from 1000 to 1500
}</code></pre>
<ul>
<li>Field 키워드를 사용해 age, salary 프로퍼티를 뒷받침하는 필드에 접근</li>
<li>Setter코드의 중복이 많음</li>
</ul>
<ol start="2">
<li>도우미 클래스를 통해 프로퍼티 변경 통지 구현</li>
</ol>
<pre><code class="language-kotlin">class ObservableProperty(
        val propName: String,
        var propValue: Int,
        val observable: Observable
) {
        fun getValue(): Int = propValue
        fun setValue(newValue: Int) {
                val oldValue = propName
                propValue = newValue
                observable.notyiObservers(propName, oldValue, newValue)
        }
}

class Person(val name: String, age: Int, salary: Int): Observable() {
        val _age = observableProperty(&quot;age&quot;, age, this)
        var age: Int
                get() = _age.getValue()
                set(newValue) {
                        _age.setValue(newValue)
                }
        val _salary = ObservableProperty(&quot;salary&quot;, salary, this)
        var salary: Int
                get() = _salary.getValue()
                set(newValue) {
                        _salary.setValue(newValue)
                }
}</code></pre>
<ul>
<li>프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경을 통지</li>
<li>중복을  상당 부분 제거했으나 아직 각각의 프로퍼티마다 ObservableProperty를 만들고 작업을 위임하는 코드가 중복됨</li>
<li>코틀린 위임 프로퍼티 기능ㅇ로 이런 중복 조차 없앨 수 있음</li>
</ul>
<ol start="3">
<li>프로퍼티 위임 객체</li>
</ol>
<pre><code class="language-kotlin">import kotiln.reflect.KProperty 

class ObservableProperty(var propValue: Int, val observable: Obvservable) { 
        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>
<ul>
<li>코틀린 관례에 사용하는 다른 함수처럼 getValue, setValue 함수에도 operator 변경자가 붙음</li>
<li>GetValue와 setValue는 파라미터 2개를 받음<ul>
<li>ThisRef → 설정하거나 읽을 프로퍼티가 들어있는 인스턴스</li>
<li>Prop → 프로퍼티를 표현하는 객체<ul>
<li>KProperty 타입의 객체를 사용하여 프로퍼티를 표현</li>
</ul>
</li>
</ul>
</li>
<li>KProperty 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에서는 name 프로퍼티를 없앰</li>
</ul>
<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>
<ul>
<li>By 키워드를 사용하여 위임 객체를 지정<ul>
<li>Getter/setter를 직접 지저하는 등의 작업을 컴파일러가 자동으로 해줌</li>
</ul>
</li>
</ul>
<ol start="4">
<li>Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기</li>
</ol>
<ul>
<li>ObservableProperty 클래스를 직접 구현하는 대신 표준 라이브러리 기능 활용하기</li>
<li>앞에서 구현한 Observable 클래스오는 연결되어 있지 않음</li>
</ul>
<pre><code class="language-kotlin">import kotiln..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>
<ul>
<li>프로퍼티 값의 변경을 통지받을 때 쓰일 람다를 Delegates 라이브러리 클래스에 넘겨야 함</li>
<li>By의 오른쪽에 있는 식 → 꼭 새 인스턴스를 만들 필요는 없음<ul>
<li>함수 호출, 다른 프로퍼티, 다른 식 등도 올 수 있음</li>
<li>다만 결과 객체는 컴파일러가 호출할 수 있는 올바른 타입의 getValue와 setValue를 반드시 제공해야 함</li>
</ul>
</li>
</ul>
<blockquote>
<p>프로퍼티 위임은 완전히 제네릭하여 모든 타입에 사용할 수 있음</p>
</blockquote>
<hr>
<h2 id="위임-프로퍼티의-동작-방식">위임 프로퍼티의 동작 방식</h2>
<p>위임 프로퍼티 클래스 예시</p>
<pre><code class="language-kotlin">class C { 
        var prop: Type by MyDelegate()
}

val c = C()</code></pre>
<ul>
<li>MyDelegate 클래스의 인스턴스 → 감춰진 프로퍼티에 저장됨<ul>
<li><delegate>라는 이름으로 부름</li>
</ul>
</li>
<li>프로퍼티 표현 → KProperty 타입의 객체를 사용<ul>
<li><property>라는 이름으로 부름</li>
</ul>
</li>
</ul>
<p>컴파일러는 다음의 코드를 생성</p>
<pre><code class="language-kotlin">class C { 
        private val &lt;delegate&gt; = MyDelegate()

        var prop: Type
                get() = &lt;delegate&gt;.getValue(this, &lt;property&gt;)
                set(value: Type) = &lt;delegate&gt;.setValue(this, &lt;property&gt;, value)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/dd201e3e-41a6-4d76-b82b-99a4e863e6ff/image.jpeg" alt=""></p>
<ul>
<li>프로퍼티 값이 저장될 장소를 바꿀 수 있음 (맵, db 테이블, 웹 쿠키 등)</li>
<li>프로퍼티를 읽거나 쓸 때 실행할 작업을 변경할 수 있음</li>
</ul>
<hr>
<h2 id="맵에-위임해서-동적으로-애트리뷰트-접근">맵에 위임해서 동적으로 애트리뷰트 접근</h2>
<p>자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 ㅍ로퍼티를 활용하는 경우가 자주 있음</p>
<ul>
<li>C++에선 그런 객체를 <strong>확장 가능한 객체(expando object)</strong>라고 부름</li>
<li>속성을 모두 맴에 저장하되 특별한 처리가 필요한 정보에 접근하도록 프로퍼티를 제공할 수 있음</li>
</ul>
<pre><code class="language-kotlin">class Person { 
        private val _attributes = mutableMap&lt;String, String&gt;()

        fun setAttributes(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>
<ul>
<li>추가 데이터를 객체에 읽어 들이기 위해 일반적인 API를 사용</li>
<li>한 프로퍼티를 처리하기 위해 구체적인 API를 제공</li>
</ul>
<h3 id="값을-맵에-저장하는-위임-프로퍼티-사용">값을 맵에 저장하는 위임 프로퍼티 사용</h3>
<p>By 키워드 뒤에 맵을 직접 넣음</p>
<pre><code class="language-kotlin">class Person {
        private val _attributes = mutableMapOf&lt;String, String&gt;()

        fun setAttribute(attrName: String, value: String) {
                _attribute[attrName] = value
        }

        var name: String by _attributes // 위임 프로퍼티로 맵 사용
}</code></pre>
<ul>
<li>코드가 작동하는 이유 → 표준 라이브러리가 Map과 MutableMap 인터페이스에 getValue와 setValue 확장 함수를 제공하기 때문</li>
<li><a href="http://P.name">P.name</a> → _attribute.getValue(p, prop)을 호출</li>
<li>_attribute.getValue(p, prop)는 -attributes[prop.name]을 통해 구현</li>
</ul>
<hr>
<h2 id="실전-프레임워크가-위임-프로퍼티를-활용하는-방법">실전 프레임워크가 위임 프로퍼티를 활용하는 방법</h2>
<ul>
<li>객체 프롶티를 저장하거나 병경하는 방법을 바꿔 프레임워크 개발에 활용</li>
</ul>
<h3 id="위임-프로퍼티를-사용해-데이터베이스-칼럼에-접근">위임 프로퍼티를 사용해 데이터베이스 칼럼에 접근</h3>
<ul>
<li>User라는 데이터베이스 테이블이 있음<ul>
<li>Name이라는 문자열 타입의 칼럼</li>
<li>Age라는 정수 타입의 칼럼</li>
</ul>
</li>
<li>User와 Users라는 클래스를 정의해 데이터베이스에 있는 모든 사용자 엔티티를 User클래스로 가져오고 저장할 수 있음</li>
</ul>
<pre><code class="language-kotlin">object Users: IdTable() { // 데이터베이스 테이블
        val name = varchar(&quot;name&quot;, length = 50).index() // 테이블 칼럼
        val age = integer(&quot;age&quot;)
}

class User(id: EntityID): Entity(id) { // 각 테이블에 들어있는 구체적인 엔티티
        var name: String by Users.name // 데잍베이스에 저장된 사용자의 이름 값
        var age: Int by Users.age
}</code></pre>
<ul>
<li>Users 객체 → 데잍베이스 테이블을 표현<ul>
<li>데이터베이스는 전체에 단 하나만 존재하는 테이블을 표현하므로 싱글턴 객체로 선언</li>
<li>객체의 프로퍼티는 칼럼을 표현</li>
</ul>
</li>
<li>User의 상위 클래스인 Entity 클래스 → 데이터베이스 칼럼을 엔티티의 속성값으로 연결해주는 매핑<ul>
<li>데이터베이스에서 가져온 name, age가 있음</li>
<li>User의 프로퍼티에 접근할 때 자동으로 Entity 클래스에 정의된 데이터베이스 매핑으로부터 필요한 값을 가져옴</li>
<li>객체를 변경하면 객체가 변경됨(dirty) 상태로 변하고 나중에 데이터베이스에 적절히 변경을 반영해줌</li>
</ul>
</li>
</ul>
<h3 id="데이터베이스-접근-분석">데이터베이스 접근 분석</h3>
<ul>
<li>Users의 칼럼 타입을 명시적으로 지정한 모습</li>
</ul>
<pre><code class="language-kotlin">object Users: IdTable() {
        val name: Column&lt;String&gt; = varchar(&quot;name&quot;, 50).index()
        val age: Column&lt;Int&gt; = integer(&quot;age&quot;)
}</code></pre>
<ul>
<li><p>프레임워크는 Column 클래스 안에 getValue, setValue 메서드를 정의</p>
<ul>
<li><p>코틀린 위임 객체에 관례에 따른 시그니처 요구 사항을 충족</p>
<pre><code class="language-kotlin">operator fun &lt;T&gt; Column&lt;T&gt;.getValue(o: Entity, desc: KProperty&lt;*&gt;): T { 
      // 데이터베이스에서 칼럼 값 가져오기
}

operator fun &lt;T&gt; Column&lt;T&gt;.setValue(o: Entity, desc: KProperty&lt;*&gt;, value: T) { 
      // 데이터베이스의 값 변경하기
}</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="column-프로퍼티를-위임-프로퍼티에-대한-위임-객체로-사용할-수-있음">Column 프로퍼티를 위임 프로퍼티에 대한 위임 객체로 사용할 수 있음</h3>
<ul>
<li>User.age += 1 → user.ageDelegate.setValue(user.Delegate.getValue()+1)와 비슷한 코드로 변환됨</li>
<li>GetValue/setValue → 데이터베이스에서 데이터를 가져오고 기록하는 작업을 처리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[component 함수를 사용해 구조 분해 선언 제공]]></title>
            <link>https://velog.io/@first_woosun/component-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%EA%B5%AC%EC%A1%B0-%EB%B6%84%ED%95%B4-%EC%84%A0%EC%96%B8-%EC%A0%9C%EA%B3%B5</link>
            <guid>https://velog.io/@first_woosun/component-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%EA%B5%AC%EC%A1%B0-%EB%B6%84%ED%95%B4-%EC%84%A0%EC%96%B8-%EC%A0%9C%EA%B3%B5</guid>
            <pubDate>Mon, 16 Feb 2026 11:41:26 GMT</pubDate>
            <description><![CDATA[<ul>
<li>구조 분해를 사용하면 복합적인 값을 분해해서 별도의 여러 지역 변수를 한꺼번에 초기화할 수 있음</li>
</ul>
<h3 id="사용-방법">사용 방법</h3>
<pre><code class="language-kotlin">fun main() {
        val p = Point(10, 20)
        val (x, y) = p // p의 여러 컴포넌트로 초기화
        println(x)
        // 10
        println(y)
        // 20
}</code></pre>
<ul>
<li>일반 변수 선언과 비슷해 보이지만 = 왼쪽에 여러 변수를 괄호로 묶음</li>
<li>구조 분해 선언은 관례를 사용함 → componenN 이라는 함수를 호출</li>
</ul>
<p><img src="https://velog.velcdn.com/images/first_woosun/post/59aa347d-5a5c-47c9-93a5-1e797c7d21de/image.png" alt=""></p>
<ul>
<li>data  클래스의 주 생성자에 들어있는 프로퍼티 → 컴파일러가 자동으로 componentN 함수를 만들어줌</li>
</ul>
<pre><code class="language-kotlin">class Person(val x: Int, val y: Int) {
        operator fun component1() = x
        operator fun component2() = y
}</code></pre>
<h3 id="예시">예시</h3>
<pre><code class="language-kotlin">data class NameComponent(val name: String, val extension: String)

fun splitFilename(fullName: String): NameComponent {
    val (name, extension) = fullName.split(&#39;.&#39;, limit = 2)
    return NameComponent(name, extension)
}

fun main() {
    val (name, ext) = splitFilename(&quot;example.txt&quot;)
    println(name)
    // example
    println(ext)
    // txt
}</code></pre>
<ul>
<li>componentN을 무한히 선언할 수는 없음<ul>
<li>코틀린 표준 라이브러리에서는 맨 앞의 다섯 원소에 대한 componentN을 제공</li>
</ul>
</li>
</ul>
<hr>
<h2 id="구조-분해-선언과-루프">구조 분해 선언과 루프</h2>
<ul>
<li>변수 선언이 들어갈 수 있는 모든 곳에 구조 분해 선언을 사용할 수 있음</li>
</ul>
<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>
<ul>
<li><p>위 예제는 2가지의 관례를 사용함</p>
<ol>
<li>이터레이션 관례<ul>
<li>코틀린 표준 라이브러리 → map에 대한 확장함수로 iteration이 들어있음</li>
<li>맵 항목에 대한 이터레이터를 반환</li>
</ul>
</li>
<li>구조 분해 선언<ul>
<li>코틀린 라이브러리 → map.Entry에 대한 확장함수로 component1, component2를 제공</li>
</ul>
</li>
</ol>
</li>
<li><p>람다가 data class나 map 같은 복합적인 파라미터로 받을 때도 구조 분해 선언을 쓸 수 있음</p>
</li>
</ul>
<pre><code class="language-kotlin">map.forEach { (key, value) -&gt;
        println(&quot;$key -&gt; $value&quot;)
}</code></pre>
<hr>
<h2 id="_-문자를-사용해-구조-분해-값-무시">_ 문자를 사용해 구조 분해 값 무시</h2>
<ul>
<li>컴포넌트가 여럿 있는 객체에 대한 구조 분해 선언 중 일부 필요하지 않은 컴포넌트를 무시</li>
</ul>
<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, _, age) = p
    println(&quot;This is $firstName, aged $age&quot;)
}

fun main() {
    val me = Person(&quot;yoo&quot;, &quot;wuseon&quot;, 26, &quot;ganwondo&quot;)
    introducePerson(me)
    // This is yoo, aged 26
}</code></pre>
<blockquote>
<h3 id="코틀린-구조-분해의-한계">코틀린 구조 분해의 한계</h3>
</blockquote>
<ul>
<li>구조 분해 연산의 결과 → 인자의 위치에 따라 결정됨<ul>
<li>코드를 리펙터링 할 때 클래스 프로퍼티의 순서를 바꾸면 미묘한 문제가 발생할 수 있음</li>
<li>이 문제를 해결하기 위해 이름 기반 구조 분해가 개발되고 있는 중임</li>
</ul>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>