<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://hiwhwnsgh.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hiwhwnsgh.github.io/" rel="alternate" type="text/html" /><updated>2026-10-21T23:37:42+00:00</updated><id>https://hiwhwnsgh.github.io/feed.xml</id><title type="html">JunhoCho’s Dev Log</title><subtitle>A blog about software development, programming, and technology.</subtitle><author><name>Junho Cho</name></author><entry><title type="html">[KMP] KMP용 네이버 지도 Compose 라이브러리 제작 및 트러블슈팅</title><link href="https://hiwhwnsgh.github.io/dev/2026/02/27/kmp.html" rel="alternate" type="text/html" title="[KMP] KMP용 네이버 지도 Compose 라이브러리 제작 및 트러블슈팅" /><published>2026-02-27T00:00:00+00:00</published><updated>2026-02-27T00:00:00+00:00</updated><id>https://hiwhwnsgh.github.io/dev/2026/02/27/kmp</id><content type="html" xml:base="https://hiwhwnsgh.github.io/dev/2026/02/27/kmp.html"><![CDATA[<blockquote>
  <p><strong>Kotlin Multiplatform(KMP) 환경에서 플랫폼별 네이티브 SDK를 공통 UI 프레임워크인 Compose Multiplatform으로 래핑하는 과정은 단순히 인터페이스를 맞추는 것 이상의 기술적 고민을 필요로 합니다.</strong> 특히 한국 시장에서 필수적인 네이버 지도 SDK를 활용하여 Android와 iOS에서 동일한 선언적 개발 경험을 제공하기 위해 라이브러리를 구축하며 겪은 도전 과제와 트러블슈팅 사례를 정리합니다.</p>
</blockquote>

<hr />

<h2 id="1-개발-배경-및-첫-오픈소스-라이브러리-도전">1. 개발 배경 및 첫 오픈소스 라이브러리 도전</h2>

<p>기존의 네이버 지도 SDK는 각 플랫폼의 네이티브 뷰 시스템에 강하게 결합되어 있습니다. Android는 View, iOS는 UIKit 기반의 명령형 API를 사용하므로, KMP 프로젝트의 <code class="language-plaintext highlighter-rouge">commonMain</code>에서 이를 직접 제어하기에는 구조적 한계가 명확했습니다. 이를 Compose의 상태 관리 시스템과 연결하여 선언적으로 관리할 수 있는 라이브러리의 필요성을 느껴 개발을 시작하게 되었습니다.</p>

<p>특히 이번 프로젝트는 개인적인 활용을 넘어, 누구나 사용할 수 있는 <strong>오픈소스 SDK 라이브러리 형태로 배포하는 첫 시도</strong>였습니다. 배포 규격에 맞는 코드 구조 설계부터 Maven Central 배포까지의 과정을 경험하며, 단순한 기능 구현을 넘어 라이브러리로서의 안정성과 확장성에 대해 깊이 고민할 수 있었습니다.</p>

<hr />

<h2 id="2-주요-기술적-도전">2. 주요 기술적 도전</h2>

<h3 id="명령형-api의-선언적-전환">명령형 API의 선언적 전환</h3>
<p>네이버 지도 SDK는 마커나 경로선 같은 오버레이를 추가할 때 객체를 생성하고 직접 지도 인스턴스에 대입하는 명령형 방식을 따릅니다.</p>
<ul>
  <li><strong>해결 방식</strong>: <code class="language-plaintext highlighter-rouge">NaverMapScope</code>를 설계하고, 컴포저블의 생성과 소멸 시점에 맞춰 <code class="language-plaintext highlighter-rouge">DisposableEffect</code>를 통해 네이티브 객체의 등록과 해제가 자동으로 이루어지도록 추상화했습니다.</li>
</ul>

<h3 id="플랫폼별-리소스-브릿징-overlayimage">플랫폼별 리소스 브릿징 (OverlayImage)</h3>
<p>가장 복잡했던 작업 중 하나는 이미지 리소스의 통합 처리였습니다. 특히 iOS에서는 Compose의 <code class="language-plaintext highlighter-rouge">Painter</code> 리소스를 네이버 지도 SDK가 이해할 수 있는 <code class="language-plaintext highlighter-rouge">UIImage</code> 형태로 변환해야 했습니다.</p>
<ul>
  <li><strong>해결 방식</strong>: <code class="language-plaintext highlighter-rouge">CanvasDrawScope</code>를 이용해 메모리 상의 <code class="language-plaintext highlighter-rouge">ImageBitmap</code>에 이미지를 그린 뒤, 이를 다시 <code class="language-plaintext highlighter-rouge">UIImage</code>로 변환하는 브릿지 로직을 구현했습니다. 이 과정에서 플랫폼 간 밀도(Density) 차이와 좌표계 변환을 정교하게 처리했습니다.</li>
</ul>

<h3 id="개발자-경험-자동화-gradle-plugin">개발자 경험 자동화 (Gradle Plugin)</h3>
<p>네이버 지도 SDK는 별도의 전용 Maven 저장소 설정을 요구합니다. 라이브러리 사용자가 프로젝트마다 수동으로 저장소 URL을 추가하는 번거로움을 줄이기 위해 <code class="language-plaintext highlighter-rouge">naver-map-compose-gradle-plugin</code>을 별도로 제작했습니다.</p>

<hr />

<h2 id="3-주요-트러블슈팅-deep-dive">3. 주요 트러블슈팅 (Deep Dive)</h2>

<p>개발 과정에서 마주한 주요 문제들과 그 해결 과정을 정리합니다.</p>

<h4 id="case-1-kotlin-native-변환-과정에서의-문서와-실제-동작의-차이">Case 1: Kotlin Native 변환 과정에서의 문서와 실제 동작의 차이</h4>
<blockquote>
  <p><strong>현상</strong>: 네이티브 SDK를 Kotlin Native로 변환(<code class="language-plaintext highlighter-rouge">cinterop</code>)하여 사용하는 과정에서 공식 문서의 설명과 실제 동작이 다른 경우가 다수 발생했습니다.</p>
  <ul>
    <li><strong>문제</strong>: 특정 API의 리턴 타입이 문서와 다르거나, Objective-C 인터페이스가 Kotlin Native로 변환될 때 예상치 못한 타입으로 매핑되었습니다.</li>
    <li><strong>대응</strong>: 공식 문서에만 의존하지 않고 실제 생성된 라이브러리 바이너리를 직접 분석하고 테스트하며 적절한 타입을 찾아내는 과정을 거쳤습니다.</li>
  </ul>
</blockquote>

<h4 id="case-2-ios-메모리-참조-및-누수-관리">Case 2: iOS 메모리 참조 및 누수 관리</h4>
<blockquote>
  <p><strong>현상</strong>: 초기 개발 단계에서 iOS의 마커 객체가 화면에서 사라진 후에도 메모리에서 해제되지 않는 현상이 발견되었습니다.</p>
  <ul>
    <li><strong>문제</strong>: Kotlin의 Native Interop 과정에서 발생한 강한 참조 고리(Strong Reference Cycle)가 원인이었습니다.</li>
    <li><strong>대응</strong>: 오버레이 객체의 <code class="language-plaintext highlighter-rouge">map</code> 속성에 <code class="language-plaintext highlighter-rouge">null</code>을 명시적으로 대입하는 로직을 Compose의 <code class="language-plaintext highlighter-rouge">onDispose</code> 시점에 엄격하게 적용하여 해결했습니다.</li>
  </ul>
</blockquote>

<h4 id="case-3-sdk-초기화와-렌더링-시점-동기화">Case 3: SDK 초기화와 렌더링 시점 동기화</h4>
<blockquote>
  <p><strong>현상</strong>: 간헐적으로 지도가 표시되지 않거나 초기화 에러가 발생했습니다.</p>
  <ul>
    <li><strong>문제</strong>: 네이버 지도는 뷰가 생성되기 전 클라이언트 ID 초기화가 선행되어야 하는데, 초기화 요청과 렌더링 간의 경합 조건(Race Condition)이 발생했습니다.</li>
    <li><strong>대응</strong>: 초기화 상태를 싱글턴 객체에서 관리하고, 플랫폼별 초기화 로직이 완료된 것을 보장한 뒤 뷰를 노출하도록 실행 순서를 조정했습니다.</li>
  </ul>
</blockquote>

<h4 id="case-4-ios-위치-오버레이-iconwidthheight-auto-값-오류">Case 4: iOS 위치 오버레이 iconWidth/Height AUTO 값 오류</h4>
<blockquote>
  <p><strong>현상</strong>: iOS에서 위치 오버레이의 <code class="language-plaintext highlighter-rouge">iconWidth</code> 또는 <code class="language-plaintext highlighter-rouge">iconHeight</code>를 AUTO로 설정할 경우 런타임 오류가 발생했습니다.</p>
  <ul>
    <li><strong>문제</strong>: iOS 네이버 지도 SDK는 AUTO 크기 상수를 Android와 다른 방식으로 처리합니다. Android에서 통용되는 상수 값을 그대로 넘기면 플랫폼 내부에서 유효하지 않은 값으로 해석되었습니다.</li>
    <li><strong>대응</strong>: iOS 구현부에서 AUTO 값을 별도로 감지하여 플랫폼에 맞는 처리 방식으로 분기하도록 수정했습니다.</li>
  </ul>
</blockquote>

<h4 id="case-5-android-fusedlocationsource-초기화-타이밍-및-위치-권한-요청-처리">Case 5: Android FusedLocationSource 초기화 타이밍 및 위치 권한 요청 처리</h4>
<blockquote>
  <p><strong>현상</strong>: Android에서 위치 추적 기능 활성화 시 간헐적으로 위치 업데이트가 시작되지 않거나, 권한 요청 흐름이 정상 동작하지 않는 경우가 있었습니다.</p>
  <ul>
    <li><strong>문제</strong>: <code class="language-plaintext highlighter-rouge">FusedLocationSource</code>의 초기화가 Compose의 컴포지션 이후 지연되어 실행되었고, 권한 요청 결과 콜백과 위치 소스 활성화 사이의 순서가 보장되지 않았습니다.</li>
    <li><strong>대응</strong>: <code class="language-plaintext highlighter-rouge">FusedLocationSource</code>를 컴포저블 진입 시점에 즉시 초기화하도록 수정하고, 권한 요청 결과를 처리한 직후 위치 추적을 활성화하는 순서를 명시적으로 보장하도록 개선했습니다.</li>
  </ul>
</blockquote>

<h4 id="case-6-subiconanchor-기본값-오류로-인한-방향-화살표-위치-어긋남">Case 6: subIconAnchor 기본값 오류로 인한 방향 화살표 위치 어긋남</h4>
<blockquote>
  <p><strong>현상</strong>: 위치 오버레이의 방향 화살표(subIcon)가 의도한 위치에 표시되지 않고 어긋나 보이는 문제가 발생했습니다.</p>
  <ul>
    <li><strong>문제</strong>: <code class="language-plaintext highlighter-rouge">subIconAnchor</code>의 기본값이 잘못 설정되어 있어 방향 화살표가 위치 오버레이 아이콘의 하단 중앙이 아닌 엉뚱한 위치를 기준으로 렌더링되었습니다.</li>
    <li><strong>대응</strong>: <code class="language-plaintext highlighter-rouge">subIconAnchor</code>의 기본값을 <code class="language-plaintext highlighter-rouge">CenterBottom</code>으로 명시하여 위치 오버레이 아이콘 하단 중앙에 화살표가 정확히 붙도록 수정했습니다.</li>
  </ul>
</blockquote>

<hr />

<h2 id="4-결론-및-향후-계획">4. 결론 및 향후 계획</h2>

<p>첫 오픈소스 SDK 배포라는 도전 과정에서 플랫폼별 네이티브 SDK의 내부 동작 원리를 깊이 이해할 수 있었습니다. 특히 KMP 환경에서 복잡한 리소스 변환과 메모리 관리, 그리고 <strong>문서와 실제 동작 간의 괴리</strong>를 극복하는 과정은 매우 값진 경험이 되었습니다.</p>

<p><strong>향후 로드맵:</strong></p>
<ol>
  <li>마커 클러스터링(Clustering) 기능 추가</li>
  <li>더 다양한 커스텀 오버레이 지원</li>
  <li>안정성 강화를 위한 유닛 테스트 확충</li>
</ol>

<hr />
<p><strong>GitHub</strong>: <a href="https://github.com/kmp-naver-map/kmp-naver-map">kmp-naver-map</a><br />
<strong>Maintainer</strong>: Junho Cho (hiwhwnsgh@gmail.com)</p>]]></content><author><name>Junho Cho</name></author><category term="dev" /><category term="Android" /><category term="iOS" /><category term="Naver map" /><category term="Naver" /><category term="Kotlin" /><category term="Coroutines" /><category term="Flow" /><summary type="html"><![CDATA[Kotlin Multiplatform(KMP) 환경에서 플랫폼별 네이티브 SDK를 공통 UI 프레임워크인 Compose Multiplatform으로 래핑하는 과정은 단순히 인터페이스를 맞추는 것 이상의 기술적 고민을 필요로 합니다. 특히 한국 시장에서 필수적인 네이버 지도 SDK를 활용하여 Android와 iOS에서 동일한 선언적 개발 경험을 제공하기 위해 라이브러리를 구축하며 겪은 도전 과제와 트러블슈팅 사례를 정리합니다.]]></summary></entry><entry><title type="html">Android + Ktor로 배우는 FCM 푸시(2)</title><link href="https://hiwhwnsgh.github.io/dev/2026/01/28/fcm_be(with-ktor).html" rel="alternate" type="text/html" title="Android + Ktor로 배우는 FCM 푸시(2)" /><published>2026-01-28T00:00:00+00:00</published><updated>2026-01-28T00:00:00+00:00</updated><id>https://hiwhwnsgh.github.io/dev/2026/01/28/fcm_be(with%20:%20ktor)</id><content type="html" xml:base="https://hiwhwnsgh.github.io/dev/2026/01/28/fcm_be(with-ktor).html"><![CDATA[<h1 id="ktor-3--koin으로-fcm-알림-서버-깔끔하게-구축하기">Ktor 3(+) + Koin으로 FCM 알림 서버 깔끔하게 구축하기</h1>

<p>Ktor를 사용해 FCM 알림 서버를 구축했다. 
Ktor 3버전으로 마이그레이션하면서 겪은 라이브러리 충돌 이슈와 해결 과정을 공유한다. 
불필요한 삽질을 줄이고 싶은 분들에게 도움이 되길 바란다.</p>

<hr />

<h2 id="1-개발-환경">1. 개발 환경</h2>
<ul>
  <li><strong>Framework</strong>: Ktor 3.3.3</li>
  <li><strong>Language</strong>: Kotlin 2.2.21</li>
  <li><strong>DI</strong>: Koin 4.1.0Beta8</li>
  <li><strong>FCM</strong>: Firebase Admin SDK 9.4.1</li>
</ul>

<hr />

<h2 id="2-핵심-설정-의존성-관리-libsversionstoml">2. 핵심 설정: 의존성 관리 (libs.versions.toml)</h2>

<p>Ktor 3 버전이상을 사용한다면 Koin 설정이 핵심이다. 기존 <code class="language-plaintext highlighter-rouge">koin-ktor</code>를 사용하면 런타임 에러가 발생하므로 반드시 <code class="language-plaintext highlighter-rouge">koin-ktor3</code>를 사용해야 한다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[versions]</span>
<span class="py">ktor</span> <span class="p">=</span> <span class="s">"3.3.3"</span>
<span class="py">koin</span> <span class="p">=</span> <span class="s">"4.1.0-Beta8"</span>

<span class="nn">[libraries]</span>
<span class="c"># Ktor 3 대응을 위한 전용 라이브러리 사용</span>
<span class="nn">koin-ktor</span> <span class="o">=</span> <span class="p">{</span> <span class="py">module</span> <span class="p">=</span> <span class="s">"io.insert-koin:koin-ktor3"</span><span class="p">,</span> <span class="py">version.ref</span> <span class="p">=</span> <span class="s">"koin"</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="3-주요-트러블슈팅-및-해결-방안">3. 주요 트러블슈팅 및 해결 방안</h2>

<h3 id="1-중복-플러그인-설치-duplicatepluginexception">1) 중복 플러그인 설치 (DuplicatePluginException)</h3>
<p><code class="language-plaintext highlighter-rouge">CallLogging</code> 플러그인을 분리해서 설치하다가 발생한 에러다. Ktor는 동일한 플러그인의 중복 설치를 허용하지 않는다.</p>

<ul>
  <li><strong>해결</strong>: <code class="language-plaintext highlighter-rouge">install(CallLogging)</code> 블록 하나에 <code class="language-plaintext highlighter-rouge">level</code>, <code class="language-plaintext highlighter-rouge">format</code>, <code class="language-plaintext highlighter-rouge">callIdMdc</code> 설정을 모두 통합했다.</li>
</ul>

<h3 id="2-클래스-참조-에러-noclassdeffounderror">2) 클래스 참조 에러 (NoClassDefFoundError)</h3>
<p>서버 실행 시 <code class="language-plaintext highlighter-rouge">RoutingKt</code> 클래스를 찾지 못하는 문제가 발생했다. 이는 Ktor 3와 Koin 버전 간의 호환성 문제였다.</p>

<ul>
  <li><strong>해결</strong>: <code class="language-plaintext highlighter-rouge">koin-ktor3</code>로 라이브러리를 교체하여 즉시 해결했다.</li>
</ul>

<hr />

<h2 id="4-구조-설계-clean-architecture">4. 구조 설계 (Clean Architecture)</h2>

<p>유지보수와 확장성을 고려해 <strong>Domain, Data, Presentation</strong> 계층을 분리했다.</p>

<ul>
  <li><strong>Domain</strong>: 알림 대상(Token, Topic)과 데이터 모델 정의.</li>
  <li><strong>Data</strong>: Firebase SDK를 활용한 실질적인 전송 로직 구현.</li>
  <li><strong>Presentation</strong>: API 엔드포인트 구성 및 요청 처리.</li>
</ul>

<p>계층 분리를 통해 코드가 명확해지고 테스트가 용이해졌다.</p>

<hr />

<h2 id="5-전송-방식-token-vs-topic">5. 전송 방식: Token vs Topic</h2>

<p>실제 테스트를 통해 확인한 두 방식의 차이점이다.</p>

<ul>
  <li><strong>Token</strong>: 특정 기기 식별자를 이용한 1:1 메시지 전송. 개인화된 알림에 사용한다.</li>
  <li><strong>Topic</strong>: 특정 주제를 구독한 그룹에 메시지 전송. 전체 공지나 이벤트 알림에 효율적이다.</li>
</ul>

<p><strong>참고</strong>: Topic 전송 시 클라이언트가 해당 주제를 미리 구독하고 있어야 하며, 서버와 주제 이름을 정확히 일치시켜야 한다.</p>

<hr />

<h2 id="6-결론">6. 결론</h2>
<p><img src="/images/fcm_be_screenshot.png" alt="테스트 메시지 성공" /></p>

<p>FCM 서버 구축 시 <code class="language-plaintext highlighter-rouge">messageId</code>가 정상적으로 반환된다면 백엔드 로직은 성공이다. 
그럼에도 기기에 알림이 오지 않는다면 앱의 권한 설정이나 토픽 구독 여부를 확인해보면 된다.</p>]]></content><author><name>Junho Cho</name></author><category term="dev" /><summary type="html"><![CDATA[Ktor 3(+) + Koin으로 FCM 알림 서버 깔끔하게 구축하기]]></summary></entry><entry><title type="html">[Android/Kotlin] Coroutines Mutex Deep Dive: 동시성 제어 정복하기</title><link href="https://hiwhwnsgh.github.io/dev/2026/01/26/Mutex.html" rel="alternate" type="text/html" title="[Android/Kotlin] Coroutines Mutex Deep Dive: 동시성 제어 정복하기" /><published>2026-01-26T00:00:00+00:00</published><updated>2026-01-26T00:00:00+00:00</updated><id>https://hiwhwnsgh.github.io/dev/2026/01/26/Mutex</id><content type="html" xml:base="https://hiwhwnsgh.github.io/dev/2026/01/26/Mutex.html"><![CDATA[<p>안드로이드 개발을 하다 보면 여러 개의 코루틴이 하나의 변수나 자원을 동시에 수정하려고 할 때 데이터가 꼬이는 현상을 마주하게 됩니다. 오늘은 이를 해결하기 위한 핵심 도구인 <strong>Mutex</strong>와 그 대안들에 대해 깊게 파헤쳐 보겠습니다.</p>

<hr />

<h2 id="1-문제의-시작-race-condition-경쟁-상태">1. 문제의 시작: Race Condition (경쟁 상태)</h2>

<p>여러 개의 코루틴이 동시다발적으로 공유 자원을 수정하면, 연산 결과가 유실되는 현상이 발생합니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="py">counter</span> <span class="p">=</span> <span class="mi">0</span>
<span class="kd">val</span> <span class="py">jobs</span> <span class="p">=</span> <span class="nc">List</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">counter</span><span class="p">++</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 결과: 100,000을 기대했지만, 실제로는 훨씬 적은 값이 나옴</span>
</code></pre></div></div>

<hr />

<h2 id="2-해결사-mutex-mutual-exclusion">2. 해결사: Mutex (Mutual Exclusion)</h2>

<p><code class="language-plaintext highlighter-rouge">Mutex</code>는 한 번에 하나의 코루틴만 특정 코드 블록(Critical Section)을 실행할 수 있도록 보장합니다.</p>

<h3 id="핵심-특징">핵심 특징</h3>
<ul>
  <li><strong>비차단(Non-blocking)</strong>: 스레드를 멈추지 않고 코루틴만 일시 중단(suspend)시킵니다.</li>
  <li><strong>withLock</strong>: 가장 안전한 사용법으로, 작업 완료 후 자동으로 잠금을 해제합니다.</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">mutex</span> <span class="p">=</span> <span class="nc">Mutex</span><span class="p">()</span>
<span class="kd">var</span> <span class="py">counter</span> <span class="p">=</span> <span class="mi">0</span>

<span class="n">mutex</span><span class="p">.</span><span class="nf">withLock</span> <span class="p">{</span>
    <span class="n">counter</span><span class="p">++</span> <span class="c1">// 안전하게 보호됨</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="딥다이브-포인트">딥다이브 포인트</h3>
<ul>
  <li><strong>tryLock()</strong>: 락이 걸릴 때까지 기다리지 않고, 즉시 성공 여부(<code class="language-plaintext highlighter-rouge">true</code>/<code class="language-plaintext highlighter-rouge">false</code>)를 반환합니다.</li>
  <li><strong>owner</strong>: 락을 소유한 대상을 지정하여 디버깅 시 추적이 가능합니다.</li>
  <li><strong>isLocked</strong>: 현재 Mutex가 잠겨 있는지 상태를 확인합니다.</li>
</ul>

<hr />

<h2 id="3-mutex-vs-atomic-atomicinteger">3. Mutex vs Atomic (AtomicInteger)</h2>

<p>단순한 숫자 카운팅이라면 <code class="language-plaintext highlighter-rouge">AtomicInteger</code>가 훨씬 효율적일 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">구분</th>
      <th style="text-align: left">Mutex</th>
      <th style="text-align: left">Atomic</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>방식</strong></td>
      <td style="text-align: left">잠금(Locking) 기반</td>
      <td style="text-align: left">잠금 없는(Lock-free) CAS 방식</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>속도</strong></td>
      <td style="text-align: left">코루틴 중단 오버헤드 발생</td>
      <td style="text-align: left">CPU 레벨 연산으로 매우 빠름</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>범위</strong></td>
      <td style="text-align: left">여러 줄의 복잡한 로직 보호</td>
      <td style="text-align: left">단일 변수의 값 변경</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="4-실무-가이드-언제-무엇을-쓸까">4. 실무 가이드: 언제 무엇을 쓸까?</h2>

<h3 id="mutex를-써야-할-때">Mutex를 써야 할 때</h3>
<ul>
  <li>공유 자원에 대해 <strong>여러 단계의 작업</strong>(읽기 -&gt; 검증 -&gt; 쓰기)이 일렬로 진행되어야 할 때.</li>
  <li>로컬 DB(Room) 작업이나 파일 I/O처럼 <strong>복잡한 비동기 로직</strong>을 보호해야 할 때.</li>
</ul>

<h3 id="atomic을-써야-할-때">Atomic을 써야 할 때</h3>
<ul>
  <li>단순한 <strong>카운터</strong>(좋아요 수, 작업 개수 등)를 올릴 때.</li>
  <li>불리언(Boolean) 값을 통해 <strong>상태 플래그</strong>를 안전하게 바꿀 때.</li>
</ul>

<hr />

<h2 id="5-벤치마크-결과-benchmark-results">5. 벤치마크 결과 (Benchmark Results)</h2>

<p>실제 안드로이드 에뮬레이터에서 100개의 코루틴이 각각 10,000번씩(총 1,000,000번) 숫자를 증가시킨 실험 결과입니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">방식</th>
      <th style="text-align: center">결과 정확도</th>
      <th style="text-align: center">소요 시간 (ms)</th>
      <th style="text-align: left">비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>No Sync</strong></td>
      <td style="text-align: center">❌ (유실 발생)</td>
      <td style="text-align: center">~4ms</td>
      <td style="text-align: left">가장 빠르지만 데이터 일관성 없음</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Mutex</strong></td>
      <td style="text-align: center">(정확함)</td>
      <td style="text-align: center">~3548ms</td>
      <td style="text-align: left">안전하지만 일시 중단 오버헤드가 큼</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>AtomicInteger</strong></td>
      <td style="text-align: center">(정확함)</td>
      <td style="text-align: center">~15ms</td>
      <td style="text-align: left">단순 연산에서 가장 압도적인 효율</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>분석</strong>: Mutex는 코루틴의 일시 중단(Suspension)과 재개 과정이 포함되어 Atomic 방식보다 훨씬 느린 결과를 보였습니다. (실험 환경에 따라 차이가 있을 수 있습니다.)</p>
</blockquote>

<hr />

<h2 id="6-마치며-안드로이드-개발자의-시점">6. 마치며 (안드로이드 개발자의 시점)</h2>

<p>서버 환경뿐만 아니라 안드로이드에서도 <code class="language-plaintext highlighter-rouge">Mutex</code>는 중요합니다. 앱의 데이터 무결성을 지키고, 경쟁 상태로 인한 예기치 못한 크래시를 방지하기 위해 반드시 숙지해야 할 개념입니다.</p>

<p>아래는 직접 작성한 예제 앱을 통해 도출한 실제 벤치마크 캡처 화면입니다.</p>

<p><img src="/images/mutex_screenshot.png" alt="Mutex 벤치마크 결과 화면" /></p>

<hr />

<h2 id="7-참고-자료-references">7. 참고 자료 (References)</h2>

<ul>
  <li><a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/">Kotlin Coroutines - Mutex API Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html">Kotlin Docs - Shared Mutable State and Concurrency</a></li>
  <li><a href="https://developer.android.com/kotlin/coroutines/coroutines-best-practices">Android Developers - Best practices for coroutines</a></li>
</ul>]]></content><author><name>Junho Cho</name></author><category term="dev" /><summary type="html"><![CDATA[안드로이드 개발을 하다 보면 여러 개의 코루틴이 하나의 변수나 자원을 동시에 수정하려고 할 때 데이터가 꼬이는 현상을 마주하게 됩니다. 오늘은 이를 해결하기 위한 핵심 도구인 Mutex와 그 대안들에 대해 깊게 파헤쳐 보겠습니다.]]></summary></entry><entry><title type="html">[Android] 일회성 이벤트 처리를 위한 SharedFlow 전략</title><link href="https://hiwhwnsgh.github.io/dev/2026/01/25/android-sharedFlow.html" rel="alternate" type="text/html" title="[Android] 일회성 이벤트 처리를 위한 SharedFlow 전략" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T00:00:00+00:00</updated><id>https://hiwhwnsgh.github.io/dev/2026/01/25/android-sharedFlow</id><content type="html" xml:base="https://hiwhwnsgh.github.io/dev/2026/01/25/android-sharedFlow.html"><![CDATA[<h3 id="1-시작하며-상태state와-이벤트event는-다르다">1. 시작하며: 상태(State)와 이벤트(Event)는 다르다</h3>

<p>안드로이드 개발을 하다 보면 데이터를 UI로 전달하는 수많은 상황을 마주합니다. 보통 <code class="language-plaintext highlighter-rouge">StateFlow</code>를 사용하여 ‘현재 상태’를 유지하지만, 때로는 <strong>“특정 시점에 단 한 번 발생하는 이벤트”</strong>를 처리해야 할 때가 있습니다.</p>

<p>예를 들어, 토스트 메시지 출력, 화면 이동, 혹은 특정 데이터 수신 알림 등이 그렇습니다. 이러한 <strong>일회성 이벤트</strong>를 가장 효율적으로 다루기 위해 제가 선택한 <code class="language-plaintext highlighter-rouge">SharedFlow</code> 전략을 공유합니다.</p>

<hr />

<h3 id="2-mutablesharedflow의-전략적-설계">2. MutableSharedFlow의 전략적 설계</h3>

<p>프로젝트에서 사용한 이벤트 전달 파이프라인의 핵심 코드입니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     <span class="k">private</span> <span class="kd">val</span> <span class="py">_eventFlow</span> <span class="p">=</span> <span class="nc">MutableSharedFlow</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;(</span>
        <span class="n">replay</span> <span class="p">=</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// 최신 이벤트 1개는 저장했다가 새로운 구독자에게 바로 전달 </span>
        <span class="n">onBufferOverflow</span> <span class="p">=</span> <span class="nc">BufferOverflow</span><span class="p">.</span><span class="nc">DROP_OLDEST</span> <span class="c1">// 버퍼가 차면 오래된 것부터 버림 (성능 최적화) </span>
    <span class="p">)</span>
    <span class="kd">val</span> <span class="py">eventFlow</span> <span class="p">=</span> <span class="n">_eventFlow</span><span class="p">.</span><span class="nf">asSharedFlow</span><span class="p">()</span>
</code></pre></div></div>

<p>이 짧은 선언에는 세 가지 중요한 설계 의도가 담겨 있습니다.</p>

<hr />

<h3 id="3-왜-sharedflow인가-핵심-파헤치기">3. 왜 SharedFlow인가? (핵심 파헤치기)</h3>

<h4 id="1-왜-stateflow가-아닌가">(1) 왜 StateFlow가 아닌가?</h4>
<p>안드로이드에서 가장 많이 쓰이는 두 Flow의 차이점은 명확합니다.</p>

<ul>
  <li><strong>StateFlow</strong>: 항상 현재 상태(State)를 가지고 있어야 합니다. 초기값이 필수이며, UI는 언제나 최신 상태를 반영합니다.</li>
  <li><strong>SharedFlow</strong>: <strong>특정 이벤트(Event)</strong>를 전달할 때 사용합니다. 초기값이 필요 없으며, 발생한 사건을 구독자들에게 전파하는 데 집중합니다.</li>
</ul>

<blockquote>
  <p>[!NOTE]
“현재 로딩 중인가?”는 상태(<code class="language-plaintext highlighter-rouge">StateFlow</code>)이지만, “데이터 수신이 완료됨!”은 이벤트(<code class="language-plaintext highlighter-rouge">SharedFlow</code>)입니다. 이벤트가 없을 때 억지로 “상태 없음”이라는 초기값을 유지할 필요가 없기 때문에 <code class="language-plaintext highlighter-rouge">SharedFlow</code>가 이벤트 처리에 더 적합합니다.</p>
</blockquote>

<h4 id="2-replay--1의-마법-최신-데이터-보존">(2) <code class="language-plaintext highlighter-rouge">replay = 1</code>의 마법: 최신 데이터 보존</h4>
<p>일반적인 이벤트는 발행되는 순간 구독자가 없으면 소멸합니다. 하지만 <code class="language-plaintext highlighter-rouge">replay = 1</code>은 보험과 같습니다.</p>

<ul>
  <li><strong>의미</strong>: “가장 최근에 발생한 이벤트 1개를 기억하고 있다가, 새로운 구독자가 나타나면 즉시 전달해 줘”라는 뜻입니다.</li>
  <li><strong>실제 활용</strong>: 사용자가 잠시 화면을 전환하거나 configuration change(화면 회전 등)가 일어나는 찰나에 이벤트가 발생하더라도, 다시 구독을 시작할 때 해당 이벤트를 놓치지 않고 처리할 수 있게 해줍니다.</li>
</ul>

<h4 id="3-onbufferoverflow--bufferoverflowdrop_oldest">(3) <code class="language-plaintext highlighter-rouge">onBufferOverflow = BufferOverflow.DROP_OLDEST</code></h4>
<p>시스템 리소스는 유한합니다. 이벤트가 처리 속도보다 빠르게 대량으로 발생할 때를 대비해야 합니다.</p>

<ul>
  <li><strong>의미</strong>: 버퍼가 꽉 차면 <strong>가장 오래된 낡은 메시지를 버리고</strong> 최신 데이터를 담습니다.</li>
  <li><strong>전략적 선택</strong>: 사용자에게 가장 중요한 것은 ‘지금 당장 벌어진 최신 일’입니다. 성능을 유지하면서도 사용자에게 가장 유효한 정보를 보장하기 위한 설정입니다.</li>
</ul>

<hr />

<h3 id="4-compose에서의-활용-sample">4. Compose에서의 활용 (Sample)</h3>

<p>Compose 환경에서는 다음과 같이 이 이벤트를 안전하게 수집할 수 있습니다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">EventScreen</span><span class="p">(</span><span class="n">viewModel</span><span class="p">:</span> <span class="nc">ViewModel</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">lifecycleOwner</span> <span class="p">=</span> <span class="nc">LocalLifecycleOwner</span><span class="p">.</span><span class="n">current</span>
    <span class="nc">LaunchedEffect</span><span class="p">(</span><span class="n">viewModel</span><span class="p">.</span><span class="n">eventFlow</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">viewModel</span><span class="p">.</span><span class="n">eventFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">message</span> <span class="p">-&gt;</span>
            <span class="c1">// 토스트를 띄우거나 화면을 이동하는 등의 이벤트 처리</span>
            <span class="nf">println</span><span class="p">(</span><span class="s">"Received event: $message"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<hr />

<h3 id="5-마치며">5. 마치며</h3>

<p>단순히 <code class="language-plaintext highlighter-rouge">Flow</code>를 사용하는 것을 넘어, <code class="language-plaintext highlighter-rouge">replay</code>와 <code class="language-plaintext highlighter-rouge">BufferOverflow</code> 정책을 고민하는 과정에서 <strong>안드로이드의 비동기 스트림</strong>이 얼마나 정교한지 배울 수 있었습니다.</p>

<p>이벤트의 성격에 따라 이 파라미터들을 조절하는 것만으로도, 훨씬 견고하고 반응성 좋은 앱을 만들 수 있습니다. 오늘 공유한 <code class="language-plaintext highlighter-rouge">SharedFlow</code> 전략이 여러분의 프로젝트에도 도움이 되길 바랍니다.</p>

<hr />
<p><strong>참고 자료</strong></p>
<ul>
  <li><a href="https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko">Kotlin 공식 문서 - SharedFlow</a></li>
  <li><a href="https://developer.android.com/kotlin/flow">Android Developers - Flow Guide</a></li>
</ul>]]></content><author><name>Junho Cho</name></author><category term="dev" /><category term="Android" /><category term="Kotlin" /><category term="Coroutines" /><category term="Flow" /><summary type="html"><![CDATA[1. 시작하며: 상태(State)와 이벤트(Event)는 다르다]]></summary></entry><entry><title type="html">Android + Ktor로 배우는 FCM 푸시(1)</title><link href="https://hiwhwnsgh.github.io/dev/2026/01/25/fcm.html" rel="alternate" type="text/html" title="Android + Ktor로 배우는 FCM 푸시(1)" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T00:00:00+00:00</updated><id>https://hiwhwnsgh.github.io/dev/2026/01/25/fcm</id><content type="html" xml:base="https://hiwhwnsgh.github.io/dev/2026/01/25/fcm.html"><![CDATA[<h1 id="androidfcm-알림-서비스-구현-전-테스트로-시작해-딥다이브까지-fcm-내부-원리-파헤치기">[Android/FCM] 알림 서비스 구현 전, 테스트로 시작해 딥다이브까지: FCM 내부 원리 파헤치기</h1>

<h3 id="0-시작하며-그저-테스트-한-번-해보려-했을-뿐인데">0. 시작하며: “그저 테스트 한 번 해보려 했을 뿐인데…”</h3>

<p>최근 프로젝트를 진행하며 알림 기능을 넣기 전, FCM(Firebase Cloud Messaging)이 어떻게 돌아가는지 가볍게 테스트를 시작했습니다.</p>

<p>단순히 공식 문서를 보고 코드를 복사해서 붙여넣을 수도 있었지만, 코드 속에 보이는 생소한 어노테이션들과 데이터 흐름을 추적하다 보니 어느덧 <strong>안드로이드의 비동기 처리와 멀티스레드 안정성</strong>을 공부하는 딥다이브 여정이 되었습니다. 그 과정에서 배운 핵심 내용들을 정리해 봅니다.</p>

<hr />

<h3 id="1-코드-속의-숨은-안전장치-volatile과-guardedby">1. 코드 속의 숨은 안전장치: @Volatile과 @GuardedBy</h3>

<p>Firebase 라이브러리 내부나 싱글톤 패턴을 구현한 <code class="language-plaintext highlighter-rouge">FcmRepositoryImpl</code>을 분석하다 보면 재미있는 어노테이션들이 등장합니다.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">@Volatile</code></strong>: 변수의 값을 CPU 캐시가 아닌 <strong>메인 메모리</strong>에서 직접 읽고 쓰게 합니다. 여러 스레드가 동시에 접근할 때 “값의 불일치”를 방지하여, 싱글톤 객체가 중복 생성되는 것을 막아주는 핵심 역할을 합니다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">@GuardedBy("FirebaseMessaging.class")</code></strong>: 이 어노테이션은 “이 데이터는 위험하니까 반드시 지정된 자물쇠(Lock)를 소유한 상태에서만 사용해!”라는 뜻입니다. 멀티스레드 환경에서 발생할 수 있는 데이터 오염(Race Condition)을 방지하기 위한 선언적인 안전장치입니다.</li>
</ul>

<hr />

<h3 id="2-수신대기소--firebasemessagingservice">2. 수신대기소 : FirebaseMessagingService</h3>

<h4 id="1-firebasemessagingservice란">(1) FirebaseMessagingService란?</h4>

<p>FCM 서버로부터 백그라운드에서 메시지를 수신하고, 토큰이 변경되었을 때 이벤트를 처리하는 안드로이드 서비스(Service) 클래스입니다.
이 클래스는 사용자가 앱을 사용하고 있지 않을 때도(백그라운드 상태) 시스템에 의해 실행되어 메시지를 처리할 수 있게 해줍니다.</p>

<h4 id="2-핵심-메서드-2가지">(2) 핵심 메서드 2가지</h4>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyFirebaseMessagingService</span> <span class="p">:</span> <span class="nc">FirebaseMessagingService</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onMessageReceived</span><span class="p">(</span><span class="n">remoteMessage</span><span class="p">:</span> <span class="nc">RemoteMessage</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">body</span> <span class="p">=</span> <span class="n">remoteMessage</span><span class="p">.</span><span class="n">notification</span><span class="o">?.</span><span class="n">body</span> <span class="o">?:</span> <span class="n">remoteMessage</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="s">"body"</span><span class="p">]</span> <span class="o">?:</span> <span class="s">"New Message"</span>
        <span class="n">fcmRepository</span><span class="p">.</span><span class="nf">emitMessage</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onNewToken</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Log</span><span class="p">.</span><span class="nf">d</span><span class="p">(</span><span class="nc">TAG</span><span class="p">,</span> <span class="s">"Refreshed token: $token"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="onmessagereceivedremotemessage-remotemessage"><code class="language-plaintext highlighter-rouge">onMessageReceived(remoteMessage: RemoteMessage)</code></h5>

<ul>
  <li><strong>역할:</strong> FCM 서버에서 보낸 메시지가 내 기기에 도착했을 때 호출됩니다.</li>
  <li><strong>포그라운드(앱 실행 중):</strong> 메시지가 오면 이 메서드가 즉시 호출됩니다. 작성하신 코드처럼 <code class="language-plaintext highlighter-rouge">fcmRepository.emitMessage(body)</code>를 통해 UI에 바로 뿌려줄 수 있죠.</li>
  <li><strong>백그라운드(앱 종료/숨김):</strong>
    <ul>
      <li><strong>Notification 메시지</strong>: 시스템이 자동으로 알림창을 띄우고, 이 메서드는 호출되지 않을 수 있습니다.</li>
      <li><strong>Data 메시지</strong>: 앱이 백그라운드에 있어도 이 메서드가 호출되어 데이터를 처리할 수 있습니다.</li>
    </ul>
  </li>
</ul>

<h5 id="onnewtokentoken-string"><code class="language-plaintext highlighter-rouge">onNewToken(token: String)</code></h5>

<ul>
  <li><strong>역할:</strong> 기기의 등록 토큰이 처음 생성되거나, 보안상의 이유로 갱신되었을 때 호출됩니다.</li>
  <li><strong>지식 포인트:</strong>
    <ul>
      <li>토큰은 영구적이지 않습니다. 앱을 지웠다 깔거나, 데이터를 삭제하면 바뀝니다.</li>
      <li>실제 서비스라면: 여기서 받은 token을 백엔드 서버(API 서버)로 보내서 저장해야 합니다. 그래야 나중에 서버가 <strong>이 사용자에게 푸시 보내줘</strong>라고 FCM에 요청할 수 있으니까요.</li>
    </ul>
  </li>
</ul>

<h4 id="3-왜-이게-서비스service인가요">(3) 왜 이게 ‘서비스(Service)’인가요?</h4>

<p>안드로이드에서 서비스는 <strong>화면 없이 뒤에서 돌아가는 컴포넌트</strong>입니다.</p>

<ul>
  <li>푸시 알림은 사용자가 유튜브를 보든, 게임을 하든, 폰을 꺼두든 상관없이 와야 합니다.</li>
  <li>따라서 Activity(화면)가 아닌 Service(백그라운드 작업자) 형태를 띠는 것입니다.</li>
</ul>

<hr />

<h3 id="3-끊김-없는-이벤트-전달-sharedflow-전략">3. 끊김 없는 이벤트 전달: SharedFlow 전략</h3>
<p>메시지를 <code class="language-plaintext highlighter-rouge">UI(Compose/ViewModel)</code>로 전달하기 위해 <code class="language-plaintext highlighter-rouge">MutableSharedFlow</code>를 사용했습니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">private</span> <span class="kd">val</span> <span class="py">_messageFlow</span> <span class="p">=</span> <span class="nc">MutableSharedFlow</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;(</span>
        <span class="n">replay</span> <span class="p">=</span> <span class="mi">1</span><span class="p">,</span>
        <span class="n">onBufferOverflow</span> <span class="p">=</span> <span class="nc">BufferOverflow</span><span class="p">.</span><span class="nc">DROP_OLDEST</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>여기에는 두 가지 전략이 담겨 있습니다.</p>

<ul>
  <li>1.<code class="language-plaintext highlighter-rouge">replay = 1</code>: 사용자가 잠시 화면을 나갔다 돌아와도, 가장 최근에 온 메시지 1개는 놓치지 않고 보여주기 위함입니다.</li>
  <li><code class="language-plaintext highlighter-rouge">2.DROP_OLDEST</code>: 메시지가 너무 많이 쏟아질 경우, 성능을 위해 오래된 것은 버리고 가장 최신 것만 유지하는 효율적인 관리 방식입니다.</li>
</ul>

<hr />

<h3 id="4-비즈니스-로직의-중심-fcmrepositoryimpl-설계">4. 비즈니스 로직의 중심: FcmRepositoryImpl 설계</h3>

<p>FCM 서버와 통신하고 데이터를 가공하여 UI로 전달하는 핵심 로직은 <code class="language-plaintext highlighter-rouge">FcmRepositoryImpl</code>에서 담당합니다. 이 클래스에는 라이브러리 설계의 정석이라 불리는 몇 가지 기법이 적용되어 있습니다.</p>

<h4 id="1-비동기-task를-코루틴으로-await의-활용">(1) 비동기 Task를 코루틴으로: <code class="language-plaintext highlighter-rouge">await()</code>의 활용</h4>
<p>Firebase SDK의 많은 메서드는 <code class="language-plaintext highlighter-rouge">Task&lt;T&gt;</code> 기반의 콜백 형태입니다. 이를 현대적인 코틀린 코루틴 환경에서 깔끔하게 다루기 위해 <code class="language-plaintext highlighter-rouge">await()</code>를 사용했습니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">getFcmToken</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">try</span> <span class="p">{</span>
            <span class="nc">FirebaseMessaging</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">().</span><span class="n">token</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"Error: ${e.message}"</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>이 방식은 코드의 가독성을 높일 뿐만 아니라, 비동기 작업 중 에러가 발생했을 때 <code class="language-plaintext highlighter-rouge">try-catch</code>를 통해 예외 처리를 훨씬 직관적으로 할 수 있게 해줍니다.</p>

<h4 id="2-안전한-인스턴스-보장-double-checked-locking">(2) 안전한 인스턴스 보장: Double-checked locking</h4>
<p>Dagger나 Hilt 같은 의존성 주입(DI) 도구가 사용하지 않고 간략하게 앱 전역에서 단 하나의 인스턴스만 보장되도록 <strong>싱글톤 패턴</strong>을 구현했습니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
        <span class="cm">/**
         * DI(의존성 주입) 도구가 완벽히 설정되지 않은 환경을 위한 싱글톤 인스턴스 접근 메서드입니다.
         * Thread-safe한 이중 잠금(Double-checked locking) 패턴을 사용합니다.
         */</span>
        <span class="nd">@Volatile</span>
        <span class="k">private</span> <span class="kd">var</span> <span class="py">INSTANCE</span><span class="p">:</span> <span class="nc">FcmRepositoryImpl</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>

        <span class="k">fun</span> <span class="nf">getInstance</span><span class="p">():</span> <span class="nc">FcmRepositoryImpl</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nc">INSTANCE</span> <span class="o">?:</span> <span class="nf">synchronized</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span>
                <span class="nc">INSTANCE</span> <span class="o">?:</span> <span class="nc">FcmRepositoryImpl</span><span class="p">().</span><span class="nf">also</span> <span class="p">{</span> <span class="nc">INSTANCE</span> <span class="p">=</span> <span class="n">it</span> <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>여기서 사용된 <strong><code class="language-plaintext highlighter-rouge">@Volatile</code></strong>은 여러 CPU 캐시 간의 데이터 불일치를 막고 메인 메모리에서 직접 값을 읽게 하여, 멀티스레드 환경에서도 원자적인 인스턴스 생성을 보장합니다.</p>

<h4 id="3-관심사의-분리-soc">(3) 관심사의 분리 (SoC)</h4>
<p><code class="language-plaintext highlighter-rouge">FcmRepositoryImpl</code>은 내부적으로 <code class="language-plaintext highlighter-rouge">MutableSharedFlow</code>를 통해 메시지를 발행(<code class="language-plaintext highlighter-rouge">emitMessage</code>)하고, 외부에는 오직 읽기 전용인 <code class="language-plaintext highlighter-rouge">Flow</code>만 반환(<code class="language-plaintext highlighter-rouge">getMessageFlow</code>)합니다.
이러한 설계는 UI 레이어(Activity/Compose)가 데이터 발행 로직에 직접 관여하지 못하게 차단하고, 오직 데이터의 변화만 감지하도록 하여 <strong>데이터의 흐름을 단방향(UDA)</strong>으로 유지합니다.</p>

<hr />

<h3 id="5-마치며-테스트-그-이상의-수확">5. 마치며: 테스트 그 이상의 수확</h3>
<p><img src="/images/fcm_screenshot.png" alt="설정화면" /></p>

<p>처음에는 단순히 “알림이 잘 오나?” 확인하려 시작한 테스트였지만, 그 과정에서 <strong>동시성 제어, 서비스 컴포넌트의 역할, 그리고 비동기 작업을 중단 함수로 변환하는 기법</strong>까지 깊게 고민해 볼 수 있었습니다.</p>

<p>단순히 기능을 구현하는 것을 넘어, <strong>“이 코드가 왜 이렇게 설계되었을까?”</strong>를 질문하고 그 해답을 소스 코드 안에서 찾는 과정이 개발자로서 한 단계 더 성장하는 밑거름이 된다는 것을 다시 한번 느꼈습니다.</p>]]></content><author><name>Junho Cho</name></author><category term="dev" /><summary type="html"><![CDATA[[Android/FCM] 알림 서비스 구현 전, 테스트로 시작해 딥다이브까지: FCM 내부 원리 파헤치기]]></summary></entry></feed>