<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>day_park_94.log</title>
        <link>https://velog.io/</link>
        <description>#Mobile #Android #iOS #Flutter #개발문화</description>
        <lastBuildDate>Sat, 18 Jul 2026 12:21:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>day_park_94.log</title>
            <url>https://velog.velcdn.com/images/day_park_94/profile/da950047-def5-4134-8dbc-ead359049330/image.svg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. day_park_94.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/day_park_94" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[자고 일어나면 초기화되는 화면, 범인은 안드로이드가 아니었습니다]]></title>
            <link>https://velog.io/@day_park_94/%EC%9E%90%EA%B3%A0-%EC%9D%BC%EC%96%B4%EB%82%98%EB%A9%B4-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%99%94%EB%A9%B4-%EB%B2%94%EC%9D%B8%EC%9D%80-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@day_park_94/%EC%9E%90%EA%B3%A0-%EC%9D%BC%EC%96%B4%EB%82%98%EB%A9%B4-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94-%ED%99%94%EB%A9%B4-%EB%B2%94%EC%9D%B8%EC%9D%80-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Sat, 18 Jul 2026 12:21:36 GMT</pubDate>
            <description><![CDATA[<h1 id="자고-일어나면-초기화되는-화면-범인은-안드로이드가-아니었습니다">자고 일어나면 초기화되는 화면, 범인은 안드로이드가 아니었습니다</h1>
<p>저녁에 쓰던 앱을 그대로 꺼두고 다음 날 아침 다시 켜보면, 어김없이 로그인 화면이나 초기 화면부터 다시 떠 있었습니다. 처음 몇 번은 우연이겠거니 하고 넘겼는데, 비슷한 사용자 리포트가 쌓이면서 예사롭지 않다는 걸 깨달았습니다. 신기하게도 이 현상은 모든 사람에게 똑같이 나타나지 않았고, <strong>앱을 자주 정리하는 사람에게는 거의 보이지 않았던 반면 백그라운드에 여러 앱을 쌓아두는 사람들에게서만 유독 반복</strong>됐습니다.</p>
<hr>
<h2 id="문제-현상">문제 현상</h2>
<p>앱을 켜서 어느 화면에 머물다가 홈 버튼으로 백그라운드에 보낸 뒤, 다음 날 아침 다시 실행하면 이전 화면이 아니라 처음 진입했던 상태로 돌아가 있었습니다. 스크롤 위치도, 입력하던 값도 다 사라진 채로 <code>onCreate</code>부터 다시 시작되는 식이었습니다.</p>
<p>문의가 쌓이면서 하나의 패턴이 보이기 시작했습니다. 실행 중인 앱을 습관적으로 스와이프해서 종료하는 사람에게는 문제가 없었는데, 여러 앱을 동시에 켜두고 백그라운드에 계속 쌓아두는 사람에게서만 반복해서 발생하고 있었습니다. 특정 기기나 OS 버전 탓이 아니라, 백그라운드에 살아있는 프로세스 개수가 재현 여부를 가르는 변수였던 셈입니다.</p>
<p>문제는 이걸 재현하기가 쉽지 않다는 데 있었습니다. 개발 중에 여러 앱을 밤새 켜둔 채로 방치할 수는 없다 보니, 실제 사용자 리포트로만 존재를 확인할 수 있는 현상이었습니다.</p>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<p>재현 방법을 고민하다 개발자 옵션에 있는 <strong>&quot;활동 유지 안함&quot;</strong> 설정을 떠올렸습니다. 이 옵션을 켜면 액티비티가 백그라운드로 넘어가는 즉시 시스템이 <code>onDestroy</code>를 호출해줍니다. 밤새 방치했을 때와 똑같은 상황을, 홈 버튼 한 번으로 그 자리에서 만들 수 있다는 뜻입니다.</p>
<p>켜보니 실제로 사용자 리포트와 같은 증상이 그대로 재현됐습니다. 다만 왜 특정 사용자군에서만 발생하는지는 별개로 설명이 필요했습니다.</p>
<p>백그라운드에 앱을 여러 개 쌓아두면, 시스템은 메모리를 확보하려고 우선순위가 낮은 프로세스부터 정리합니다. 이때 우리 앱의 프로세스 자체가 통째로 사라지기 때문에, 다음 날 런처 아이콘을 눌러 다시 들어가는 순간 시스템은 기존 태스크를 이어가는 게 아니라 새 프로세스에서 태스크를 처음부터 다시 구성합니다.</p>
<p>반대로 백그라운드 앱 개수가 적으면 메모리 압박이 거의 없어 프로세스가 오래 살아남습니다. 이 경우엔 런처로 다시 들어가도 기존 프로세스와 액티비티 스택이 그대로 남아 있어 <code>onCreate</code>가 다시 불릴 일이 없고, 화면 상태도 자연히 보존됩니다. 지금까지 이 문제가 눈에 띄지 않았던 것도 이 때문이었을 겁니다.</p>
<p>문제는 프로세스가 쌓인 상태에서 벌어집니다. 앱 프로세스가 먼저 종료된 채로 다시 실행되면, 이때 전달되는 인텐트에 담긴 <code>FLAG_ACTIVITY_NEW_TASK</code>, <code>FLAG_ACTIVITY_RESET_TASK_IF_NEEDED</code> 플래그 조합이 실제로 작동하면서 루트 액티비티를 새로 만들어버립니다. 그리고 <code>onSaveInstanceState</code>로 저장해두지 않은 화면 상태는 이 과정에서 그대로 사라집니다.</p>
<hr>
<h2 id="🔍-진짜-원인-재생성이-아니라-상태-저장-누락">🔍 진짜 원인: 재생성이 아니라 상태 저장 누락</h2>
<p>처음엔 &quot;액티비티가 재생성된다&quot;는 사실 자체를 문제로 봤습니다. 하지만 찾아보니 재생성 자체는 안드로이드가 원래 그렇게 설계된 정상 동작이었습니다. 진짜 원인은 따로 있었습니다. 화면의 주요 상태값들을 <code>onSaveInstanceState</code>에 저장해두지 않았던 것입니다. 재생성은 시스템 설계상 언제든 일어날 수 있는 일인데, 이를 전제로 구현하지 않은 게 문제였던 셈입니다.</p>
<hr>
<h2 id="단계별-대처-과정">단계별 대처 과정</h2>
<p><strong>[Phase 1] 이슈 최초 포착 및 리포트 (1일차)</strong></p>
<p>사용자 리포트를 통해 &quot;다음 날 재실행 시 화면이 초기화된다&quot;는 현상을 인지했습니다. 개발 환경에서는 곧바로 재현되지 않아 원인 파악을 잠시 보류했습니다.</p>
<p><strong>[Phase 2] &quot;활동 유지 안함&quot; 설정으로 강제 재현 (2일차)</strong></p>
<p>개발자 옵션을 켜고 홈 버튼으로 백그라운드 전환 직후 재진입해보니, 사용자 리포트와 동일하게 화면 상태가 초기화되는 걸 바로 확인할 수 있었습니다.</p>
<p><strong>[Phase 3] Intent Flag 및 생명주기 로그 분석 (2~3일차)</strong></p>
<p><code>onCreate</code>, <code>onDestroy</code> 호출 시점에 로그를 심어 확인한 결과, 런처 재진입 시 전달되는 인텐트에서 <code>FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED</code> 조합을 확인했습니다.</p>
<p><strong>[Phase 4] 상태 저장 로직 추가 및 검증 (3~4일차)</strong></p>
<p>누락돼 있던 화면 상태를 <code>onSaveInstanceState</code>에 저장하도록 수정한 뒤, &quot;활동 유지 안함&quot; 설정을 켠 채로 반복 테스트해 상태가 정상 복원되는 걸 확인했습니다.</p>
<hr>
<h2 id="재현-및-검증-코드">재현 및 검증 코드</h2>
<p>저장해야 할 상태값이 여러 개면 <code>putInt</code>, <code>putString</code>을 화면 곳곳에 흩뿌려 호출하기보다, 상태를 하나의 객체로 묶어 한 번에 저장하고 복원하는 편이 코드도 읽기 쉽고 누락도 줄여줍니다.</p>
<pre><code class="language-kotlin">// 화면에서 재생성 후에도 유지되어야 하는 값들을 한 곳에 모아둔 상태 객체
data class ScreenState(
    val scrollPosition: Int,
    val inputText: String
) : Parcelable {
    // Parcelable 구현은 @Parcelize 어노테이션으로 자동 생성
}

class MainActivity : AppCompatActivity() {

    private var screenState = ScreenState(scrollPosition = 0, inputText = &quot;&quot;)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 재생성된 경우에만 savedInstanceState에 값이 들어있음
        savedInstanceState?.getParcelable&lt;ScreenState&gt;(KEY_SCREEN_STATE)?.let {
            screenState = it
        }
        restoreUi(screenState)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // 화면이 파괴되기 직전 시점의 최신 상태를 스냅샷으로 저장
        outState.putParcelable(KEY_SCREEN_STATE, currentScreenState())
    }

    // 현재 UI 값을 읽어 ScreenState로 변환
    private fun currentScreenState(): ScreenState =
        screenState.copy(
            scrollPosition = scrollView.scrollY,
            inputText = editText.text.toString()
        )

    // 저장된 ScreenState를 UI에 다시 반영
    private fun restoreUi(state: ScreenState) {
        scrollView.post { scrollView.scrollTo(0, state.scrollPosition) }
        editText.setText(state.inputText)
    }

    companion object {
        private const val KEY_SCREEN_STATE = &quot;key_screen_state&quot;
    }
}</code></pre>
<p>핵심은 두 가지입니다. <code>onSaveInstanceState</code>는 파괴 직전 시점의 값을 다시 읽어서 저장해야 하고, <code>onCreate</code>에서는 <code>savedInstanceState</code>가 있을 때만 복원 로직을 태워야 최초 실행과 재생성을 구분할 수 있습니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>결국 이번 이슈는 &quot;액티비티는 언제든 재생성될 수 있다&quot;는, 안드로이드를 처음 배울 때 들었던 그 원칙을 다시 확인시켜준 경험이었습니다. 프로세스 회수 정책은 기기와 OS 버전에 따라 계속 강화되는 추세라서, 이를 전제로 하지 않은 화면 설계는 언젠가는 반드시 드러나게 됩니다.</p>
<p>며칠간의 재현과 분석 과정이 번거롭게 느껴지기도 했지만, 그동안 눈에 띄지 않았던 상태 관리 누락을 짚어낼 수 있었다는 점에서 나름 의미 있는 시간이었습니다. 비슷하게 &quot;가끔씩만 재현되는 화면 초기화&quot; 문제를 마주한 분들께 &quot;활동 유지 안함&quot; 옵션과 상태 저장 로직 점검이 도움이 되길 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OS 트러블 슈팅] Android 15~16 WebView Cold Start 크래시 분석 및 대응]]></title>
            <link>https://velog.io/@day_park_94/OS-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Android-1516-WebView-Cold-Start-%ED%81%AC%EB%9E%98%EC%8B%9C-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91</link>
            <guid>https://velog.io/@day_park_94/OS-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Android-1516-WebView-Cold-Start-%ED%81%AC%EB%9E%98%EC%8B%9C-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91</guid>
            <pubDate>Wed, 01 Jul 2026 13:29:22 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 최근 Android 16 업데이트 기기(특히 삼성 One UI 8.0 기반 기기)에서 <strong>Cold Start 시 WebView 초기화 단계에서 앱이 즉시 종료(FC)</strong>되는 크래시 이슈를 마주했습니다. </p>
<p>Play Console Vitals 상에서 크래시 지수가 <strong>평소 대비 약 150,000% 급등</strong>할 정도로 심각했던 사안이었는데요. 앱 자체의 코드 변경이 없었던 상황에서 어떤 가설을 세우고 원인을 추적했는지, 그리고 제조사 기술 문의와 앱 레벨에서의 방어 코드 작성 과정을 공유합니다.</p>
<p><img src="https://velog.velcdn.com/images/day_park_94/post/0d817882-e7ed-4a04-aea9-3a639645caeb/image.png" alt=""></p>
<hr>
<h2 id="1-문제-현상">1. 문제 현상</h2>
<p>Android 16 업데이트 기기에서 프로세스가 새로 시작되는 Cold Start 시점에 앱이 즉시 종료되는 현상이 감지되었습니다.</p>
<ul>
<li><strong>크래시 로그:</strong> <code>android.app.ActivityThread.performLaunchActivity</code> 단계에서 <code>InflateException</code> 및 <code>NotFoundException</code> 발생</li>
<li><strong>발생 시점:</strong> 레이아웃 XML에 선언된 커스텀 WebView가 인플레이션(Inflation)되어 즉시 초기화될 때 발생</li>
<li><strong>특이 사항:</strong> &#39;영향을 받는 사용자&#39;와 &#39;이벤트&#39;가 1:1로 매칭되며, 대개 OS 업데이트 후 최초 1회성으로 집중 발생</li>
</ul>
<hr>
<h2 id="2-가설-수립-및-정밀-분석">2. 가설 수립 및 정밀 분석</h2>
<p>처음 로그를 파악했을 때 <code>ResourcesImpl</code> 리다이렉션 키워드가 눈에 띄었습니다. 앱 코드는 동일한 상태에서 OS 버전이나 기기 환경에 따라 현상이 갈렸기 때문에, 내부 로직 결함보다는 <strong>OS 프레임워크 및 리소스 매핑의 가설</strong>을 세우고 접근했습니다.</p>
<h3 id="🔍-진짜-원인-android-16의-리소스-참조-위반-정책-강화">🔍 진짜 원인: Android 16의 리소스 참조 위반 정책 강화</h3>
<p>구글 및 제조사 기술 문의를 통해 확인한 정확한 메커니즘은 다음과 같았습니다.</p>
<ol>
<li><strong>GMS(Google Mobile Services) 업데이트</strong>가 런타임에 일어날 때, 앱이 갱신된 리소스 경로를 즉각 반영하지 못하고 과거의 경로를 참조하는 일시적인 위반(<code>Violation</code>) 현상이 발생합니다.</li>
<li>Android 15 이전까지는 이런 Race Condition이 발생해도 유연하게 넘어갔으나, <strong>Android 16부터는 리소스 참조 위반 정책이 강화</strong>되었습니다.</li>
<li>결과적으로 <code>ResourcesImpl</code> 리다이렉션 실패 시 예외 처리 없이 프레임워크 단에서 <strong>앱 프로세스를 즉시 크래시(FC)</strong> 처리해 버리는 것이 문제의 본질이었습니다.</li>
</ol>
<hr>
<h2 id="3-단계별-대처-과정">3. 단계별 대처 과정</h2>
<p>문제 포착부터 제조사 소통, 내부 데이터 검증까지의 대처 흐름입니다.</p>
<ul>
<li><strong>[Phase 1] 이슈 최초 포착 및 리포트 (2025.09 ~ 12)</strong>
Android 16 베타 사용자 유입 시점에 문제를 먼저 감지하여 <strong>Google Issue Tracker에 최초 리포트를 등록</strong>하고 장기 모니터링에 착수했습니다.</li>
<li><strong>[Phase 2] 기술 문의 및 정밀 분석 (2026.01 ~ 02)</strong>
정식 업데이트 기기가 늘어나며 크래시가 급증하자, <strong>삼성 개발자 지원팀에 1차 기술 문의</strong>를 진행했습니다. 단순 앱 오류가 아님을 설명하기 위해 과거 관련 QA 리포트 분석 데이터를 근거로 제시했습니다.</li>
<li><strong>[Phase 3] 로그 제출 및 이슈 공식화 (2026.03)</strong>
내부에서 의도적으로 발생 시킨 <code>dumpstate</code> 로그를 함께 제출하여 기술 문의를 구체화했고, 플랫폼 단의 리소스 매핑 결함이라는 최종 피드백을 확인받았습니다.</li>
<li><strong>[Phase 4] 효과 검증 (2026.03 ~ 현재)</strong>
제조사의 수정 패치(One UI 8.5 이상)가 반영된 기기들을 위주로 <strong>Play Console 데이터를 모니터링하여 크래시가 해소된 것을 확인</strong>했습니다.</li>
</ul>
<hr>
<h2 id="4-해결-방법-및-방어-코드">4. 해결 방법 및 방어 코드</h2>
<p>제조사 펌웨어 업데이트를 통해 근본적으로 해소되는 문제이지만, 사용자의 OS 업데이트 지연이나 파편화 환경을 고려하여 <strong>앱 레벨에서 안전하게 구동될 수 있도록 방어 코드</strong>를 추가했습니다.</p>
<h3 id="application-레벨-방어-코드">Application 레벨 방어 코드</h3>
<p>Application 클래스의 <code>onCreate()</code> 시점에 GMS 패키지의 런타임 의존성을 강제로 확인하도록 유도하는 방식입니다. 이렇게 하면 GMS 업데이트가 발생하더라도 앱 프로세스가 올바른 리소스 컨텍스트를 즉시 갱신하거나 안정적으로 재시작할 수 있는 환경을 만들어 줍니다.</p>
<pre><code class="language-kotlin">@SuppressLint(&quot;CheckResult&quot;)
override fun onCreate() {
    super.onCreate()
    initGmsPackageDependency() // GMS 의존성 컨텍스트 갱신 유도
    // ..기존 초기화 로직
}

/**
 * Android 16 대응 GMS 패키지 의존성 강제 등록
 * 런타임 리소스 참조 위반(Violation)으로 인한 크래시 방지용 방어 코드
 */
private fun initGmsPackageDependency() {
    // Android 16(SDK 36) 환경에서만 작동하도록 제한
    if (Build.VERSION.SDK_INT != Build.VERSION_CODES.V) { 
        DebuggingLog.d(&quot;[Application] GMS 의존성 등록 불필요 - SDK: ${Build.VERSION.SDK_INT}&quot;)
        return
    }

    try {
        // GMS 패키지 명을 기반으로 컨텍스트를 강제 로드하여 컨텍스트 갱신 유도
        createPackageContext(
            &quot;com.google.android.gms&quot;, // GMS_PACKAGE_NAME
            CONTEXT_IGNORE_SECURITY or CONTEXT_INCLUDE_CODE
        )
        DevLog.d(&quot;[Application] GMS 패키지 의존성 등록 성공&quot;)
    } catch (e: Throwable) {
        DebuggingLog.d(&quot;[Application] GMS 패키지 의존성 등록 실패: ${e.message}&quot;)
    }
}
</code></pre>
<p>마치며
새로운 안드로이드 플랫폼이 출시될 때마다 강화되는 리소스/보안 정책은 종종 예측하기 어려운 사이드 이펙트를 만들어내곤 합니다.</p>
<p>이번 이슈는 원인 불명의 크래시 앞에서 정확한 로그(dumpstate) 분석과 기술 문의 과정을 통해 플랫폼 단의 변화를 빠르게 캐치하고, 앱 레벨에서 유연하게 대응할 수 있는 대응책을 구축해 냈다는 점에서 의미 있는 경험이었습니다. targetSdk 36 전환이나 Android 16 환경을 대응하고 계신 개발자분들께 작은 도움이 되었으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[싱글톤 패턴(Singletone Pattern)은 언제나 옳다?]]></title>
            <link>https://velog.io/@day_park_94/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4Singletone-Pattern%EC%9D%80-%EC%96%B8%EC%A0%9C%EB%82%98-%EC%98%B3%EB%8B%A4</link>
            <guid>https://velog.io/@day_park_94/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4Singletone-Pattern%EC%9D%80-%EC%96%B8%EC%A0%9C%EB%82%98-%EC%98%B3%EB%8B%A4</guid>
            <pubDate>Fri, 19 May 2023 06:53:02 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요">1. 개요</h2>
<blockquote>
<p><strong>A:</strong> &quot;싱글톤 패턴은 무엇인가요?&quot;
<strong>B:</strong> &quot;객체를 단 하나의 인스턴스로만 생성하여, 프로세스가 돌아가는 동안 전역에서 접근할 수 있도록 하는 디자인 패턴입니다.&quot;
<strong>A:</strong> &quot;그렇다면 이 패턴의 위험성이나 단점에 대해 말씀해주세요.&quot;
<strong>B:</strong> &quot;...&quot;</p>
</blockquote>
<p>개발자라면 누구나 싱글톤 패턴을 다뤄봤거나 알고 있을 것입니다. </p>
<p>하지만 <strong>&quot;싱글톤 패턴의 위험성을 충분히 고민하고 쓰고 계시나요?&quot;</strong>라는 질문에는 선뜻 답하지 못하는 경우가 많습니다. 편리함 속에 가려진 싱글톤의 이면과, 올바른 구현 방식에 대한 고민을 기록으로 남겨둡니다.</p>
<hr>
<h2 id="2-싱글톤-패턴이란">2. 싱글톤 패턴이란?</h2>
<p>전 세계 개발자들의 교과서인 <em>&#39;Head First: Design Pattern&#39;</em>에서는 싱글톤을 다음과 같이 정의합니다.</p>
<blockquote>
<p><strong>싱글톤 패턴(Singleton Pattern)</strong>은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.</p>
</blockquote>
<h3 id="왜-사용할까-장점">왜 사용할까? (장점)</h3>
<ul>
<li><strong>생성 코스트 절감:</strong> 객체를 생성할 때마다 힙(Heap) 영역에 리소스를 할당해야 합니다. 인스턴스 생성 비용이 크거나 공유해야 하는 리소스라면, 데이터(Data) 영역에 단 한 번만 할당하여 성능상 이점을 얻을 수 있습니다.</li>
<li><strong>공유 리소스 관리 및 접근:</strong> 데이터베이스 연결 pool, 로깅, 전역 설정 정보 등 앱 전반에서 일관된 상태를 유지해야 하는 시스템을 효율적으로 관리할 수 있습니다.</li>
</ul>
<hr>
<h2 id="3-싱글톤-패턴의-단점과-위험성">3. 싱글톤 패턴의 단점과 위험성</h2>
<p>사용하기엔 참 편리하지만, 오남용 시 객체지향 아키텍처를 망치기도 합니다.</p>
<ol>
<li><strong>높은 결합도와 OCP 위배:</strong> 다른 클래스들이 이 전역 객체를 직접 참조하게 되면서 클래스 간 결합도가 극도로 높아집니다. 이는 시스템 확장을 어렵게 만들어 <strong>개방-폐쇄 원칙(OCP, Open-Closed Principle)</strong>을 위배하는 원인이 됩니다.</li>
<li><strong>테스트의 어려움:</strong> 전역 상태를 공유하기 때문에 독립적인 단위 테스트(Unit Test)를 수행하기 어렵습니다. 매 테스트마다 상태를 초기화해 주어야 하는 번거로움이 생깁니다.</li>
<li><strong>멀티스레드 환경에서의 원자성 붕괴:</strong> 동기화 처리가 제대로 되지 않으면, 인스턴스가 단 하나만 존재해야 한다는 대전제가 깨지고 <strong>n개의 인스턴스가 생성되는 치명적인 결함</strong>이 발생할 수 있습니다.</li>
</ol>
<hr>
<h2 id="4-kotlin에서의-싱글톤-구현과-한계-분석">4. Kotlin에서의 싱글톤 구현과 한계 분석</h2>
<p>멀티스레드 환경(Thread-Safe)을 고려하며 발전해 온 싱글톤 구현 기법들의 장단점을 짚어보겠습니다.</p>
<h3 id="①-무늬만-싱글톤-잘못된-예시">① 무늬만 싱글톤 (잘못된 예시)</h3>
<p><code>companion object</code> 내에서 호출할 때마다 새로운 객체를 반환한다면 싱글톤의 정의가 무너집니다.</p>
<pre><code class="language-kotlin">class FakeSingleton private constructor() {
    companion object {
        // 호출할 때마다 새로운 객체를 인스턴스화함
        fun getInstance(): FakeSingleton = FakeSingleton()
    }
}</code></pre>
<h3 id="②-synchronized-thread-safe-하나-성능-저하">② Synchronized (Thread-Safe 하나 성능 저하)</h3>
<p>@Synchronized 키워드나 Double-Checked Locking을 사용하여 스레드 안전성을 보장하는 방식입니다.</p>
<pre><code class="language-Kotlin">class SynchronizedSingleton private constructor() {
    companion object {
        @Volatile
        private var instance: SynchronizedSingleton? = null

        @Synchronized
        fun getInstance(): SynchronizedSingleton {
            if (instance == null) {
                instance = SynchronizedSingleton()
            }
            return instance!!
        }
    }
}</code></pre>
<p>단점: Synchronized 블록은 동기화 락을 획득하는 과정에서 많은 비용(Cost)을 소모하므로 성능 저하의 주원인이 됩니다.</p>
<h3 id="③-kotlin-by-lazy-기법-위임-활용">③ Kotlin by lazy 기법 (위임 활용)</h3>
<p>호출되는 시점에 초기화(Lazy Initialization)되며, 코틀린 내부적으로 스레드 동기화(Thread-safe)를 기본 보장합니다.</p>
<pre><code class="language-Kotlin">class LazySingleton private constructor() {
    companion object {
        val instance: LazySingleton by lazy { LazySingleton() }
    }
}</code></pre>
<p>특징: 기본적으로 LazyThreadSafetyMode.SYNCHRONIZED로 동작하므로 멀티스레드 환경에서도 안전합니다. (다만 모드를 명시적으로 변경 시 원자성이 깨질 수 있습니다.)</p>
<h3 id="④-bill-pugh-solution-initialization-on-demand-holder-기법">④ Bill Pugh Solution (Initialization-on-demand holder 기법)</h3>
<p>JVM의 클래스 로딩 시점을 이용한 전통적이고 가장 우수한 기법입니다. JVM이 클래스를 초기화하는 과정에서 원자성을 보장하는 특성을 활용합니다.</p>
<pre><code class="language-Kotlin">class HolderSingleton private constructor() {
    companion object {
        val instance: HolderSingleton
            get() = Holder.INSTANCE
    }

    private object Holder {
        val INSTANCE = HolderSingleton()
    }
}</code></pre>
<p>장점: HolderSingleton 클래스가 로드될 때는 Holder가 모르는 상태였다가, instance가 최초로 호출되는 시점에만 내부 Holder 클래스가 로드되며 인스턴스가 생성됩니다. 구조적으로 Thread-safe 하며 성능 저하가 없습니다.</p>
<h2 id="5-결론-어떻게-써야-할까">5. 결론: 어떻게 써야 할까?</h2>
<p>가장 좋은 방법은 코틀린이 언어 차원에서 제공하는 object 키워드를 사용하거나, 수동 싱글톤보다는 Hilt, Koin 같은 DI(의존성 주입) 프레임워크를 활용하여 객체의 생명주기를 컨테이너가 싱글톤으로 관리하도록 위임하는 것입니다.</p>
<p>객체를 싱글톤으로 설계하기 전, &quot;이 객체가 정말 전역 상태를 가져야만 하는가?&quot;를 스스로에게 꼭 질문해 보아야 합니다. 구조에 대한 깊은 고민 없는 싱글톤의 무분별한 사용은 결국 거대한 기술 부채로 돌아온다는 점을 명심해야겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>