<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>두잇뚜의 블로그</title>
    <link>https://doitddo.tistory.com/</link>
    <description>두잇뚜의 이것저것</description>
    <language>ko</language>
    <pubDate>Tue, 19 May 2026 13:45:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>du.it.ddu</managingEditor>
    <image>
      <title>두잇뚜의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3191232/attach/c284bd8353d845269d949c2a5e701e4a</url>
      <link>https://doitddo.tistory.com</link>
    </image>
    <item>
      <title>[Android] Compose에서 BindService 사용하기</title>
      <link>https://doitddo.tistory.com/181</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/background-work/services/bound-services&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/background-work/services/bound-services&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742475875924&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;바인딩된 서비스 개요 &amp;nbsp;|&amp;nbsp; Background work &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 바인딩된 서비스 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 바인드된 서비스란 클라이언트-&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/background-work/services/bound-services&quot; data-og-url=&quot;https://developer.android.com/develop/background-work/services/bound-services?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zcdG4/hyYvspbpLm/Bk4ez1SWdHTvCT9M1DIHrK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/1innC/hyYvqkC8R2/3pyAsGHAqj3J42vlFZsIKK/img.png?width=526&amp;amp;height=567&amp;amp;face=0_0_526_567&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/background-work/services/bound-services&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/background-work/services/bound-services&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zcdG4/hyYvspbpLm/Bk4ez1SWdHTvCT9M1DIHrK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/1innC/hyYvqkC8R2/3pyAsGHAqj3J42vlFZsIKK/img.png?width=526&amp;amp;height=567&amp;amp;face=0_0_526_567');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;바인딩된 서비스 개요 &amp;nbsp;|&amp;nbsp; Background work &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 바인딩된 서비스 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 바인드된 서비스란 클라이언트-&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔씩 앱이 실행중이지 않을 때 앱이 백그라운드에서 무언가 작업이 실행되어야 하는 경우가 있습니다.&lt;br /&gt;예를 들면 만보기, 파일 업로드/다운로드, 음악 플레이어 등과 같은 것들이 있습니다.&lt;br /&gt;이런 것들은 보통 서비스를 활용해서 구현하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 플레이어와 같은 경우, 앱이 실행되어 있을 때 음악을 멈추거나 앞뒤로 음악을 바꾸는 등의 기능이 필요합니다.&lt;br /&gt;따라서 액티비티와 같은 UI와 실제 음악 실행을 처리하는 서비스의 통신이 필요합니다.&lt;br /&gt;이 때 사용할 수 있는 컴포넌트가 BindService 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose의 도입 전에는 Activity/Fragment에서 서비스를 실행하고 바인딩해왔지만, Compose 이후로 Single Activity Application 아키텍처를 도입하거나, 또는 Compose에서 처리 하는 것을 원할때가 있습니다.&lt;br /&gt;오늘은 Compose 함수에서 BindService를 다루는 방법을 포스팅합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BindService 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 임의의 BindService를 구현해봅시다.&lt;br /&gt;BindService를 구현할 때에는, Service와 Binder를 상속하는 나만의 Binder 클래스가 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742476398459&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyBindService : Service() {
    private val myBinder: MyBinder by lazy {
        MyBinder()
    }
    
    override fun onBind(intent: Intent): IBinder {
        super.onBind(intent)
        return binder
    }
    
    fun call() {
        // 이 Binder에서 이 함수를 불러서 외부와 상호작용 할 수 있습니다.
    }

    inner class MyBinder : Binder() {
        fun callServiceFunction() {
            call()
        }
    }   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binder 클래스를 상속하는 MyBinder를 구현한 뒤에, onBind 함수에서 인스턴스를 반환하도록 만들어주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Compose에서 사용할 함수 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BindService를 실행하기 위해선 Context와 ServiceConnection, Intent 세 가지가 필요합니다.&lt;br /&gt;이것들을 활용하여 BindService를 Composable 함수에서 실행할 수 있도록 구현해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1742476567257&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BindServiceWrapper&amp;lt;out T : Binder&amp;gt;(
    private val context: Context,
    private val intentCreator: () -&amp;gt; Intent
) {
    private var _bound: T? by mutableStateOf(null)

    val bound: T?
        get() = _bound

    private val conn = object : ServiceConnection {
        override fun onServiceDisconnected(name: ComponentName?) {
            _bound = null
        }

        @Suppress(&quot;UNCHECKED_CAST&quot;)
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            _bound = service as? T
        }
    }

    fun bind() {
        context.bindService(intentCreator.invoke(), conn, Context.BIND_AUTO_CREATE)
    }

    fun unbind() {
        context.unbindService(conn)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, BindService를 실질적으로 실행하고 중단하며, Binder 객체를 관리하는 class를 하나 만듭니다.&lt;br /&gt;BindService를 실행한 후에 연결에 성공하면 ServiceConnection의 콜백을 통해 알림을 받게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바인딩된 Binder 객체를 Compose의 상태로 관리하는 이유는, Composable 함수에서 리컴포지션 상황에서도 객체를 유지할 수 있게 하기 위함입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742476983385&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun &amp;lt;T : Binder&amp;gt; rememberBindService(
    createIntent: (Context) -&amp;gt; Intent,
    dispose: (T?) -&amp;gt; Unit,
): BindServiceWrapper&amp;lt;T&amp;gt; {
    val context = LocalContext.current
    val wrapper = remember(context) {
        BindServiceWrapper&amp;lt;T&amp;gt;(context) {
            createIntent(context)
        }
    }

    DisposableEffect(context) {
    
        onDispose {
            dispose.invoke(wrapper.bound)
            wrapper.unbind()
        }
    }

    return wrapper
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 만든 BindServiceWrapper를 활용하는 Composable 함수를 만듭니다.&lt;br /&gt;BindService를 실행하기 위해 Intent를 생성하는 람다와 Composable 함수가 dispose 되었을 때의 처리를 위한 람다를 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composable 함수가 dispose되면 더이상 Binder 객체와 상호작용할 UI가 없으므로, 불필요한 메모리의 낭비(또는 누수)를 방지하기 위해 DisposableEffect를 활용하여 리소스를 제거해줍시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 프로젝트 스펙에 맞게 적절히 수정하여 사용하면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 예시&lt;/h2&gt;
&lt;pre id=&quot;code_1742477285997&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MyScreen() {
    val context = LocalContext.current
    
    val bindService = rememberBindService&amp;lt;MyService.MyBinder&amp;gt;(
        createIntent = {
            Intent(context, MyService::class.java)
        },
        dispose = {
            // do something
        }
    )
    
    Column {
        Button {
            bindService.bind()
        }
        
        Button {
            bindService.bound?.callServiceFunction()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composable 함수 내에서 rememberBindService&amp;lt;T&amp;gt; 함수를 사용해서 생성한 뒤, 필요한 시점에 따라 BindService를 실행하거나 함수를 호출하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>bindService</category>
      <category>compose</category>
      <category>foregroundservice</category>
      <category>jetpack</category>
      <category>service</category>
      <category>바인드서비스</category>
      <category>안드로이드</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/181</guid>
      <comments>https://doitddo.tistory.com/181#entry181comment</comments>
      <pubDate>Thu, 20 Mar 2025 22:30:01 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Firebase Crashlytics 적용 후 iOS에서 PhaseScriptExecution가 발생할 때</title>
      <link>https://doitddo.tistory.com/180</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-12 오후 12.36.45.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqfae/btsMIDpt2yy/Z7w9RwBwUOy3oJ9dWmeD60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqfae/btsMIDpt2yy/Z7w9RwBwUOy3oJ9dWmeD60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqfae/btsMIDpt2yy/Z7w9RwBwUOy3oJ9dWmeD60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnqfae%2FbtsMIDpt2yy%2FZ7w9RwBwUOy3oJ9dWmeD60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1108&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2025-03-12 오후 12.36.45.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 프로젝트를 개발 후 iOS 빌드를 하였으나, FlutterFire에서 위와 같은 에러를 발생시키고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 환경에서 크래쉬가 발생했을 때 Firebase Crashlytics로 크래쉬 로그를 보내고 로그를 확인하기 위해선 디버깅을 위한 심볼 파일인 dSYM을 업로드 해 주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Flutter여서가 아니고 네이티브 iOS를 개발하더라도 마찬가지입니다.&lt;br /&gt;아무튼 FlutterFire에서 이를 업로드하는 스크립트에 문제가 있다고 합니다. 에러로그를 쭉쭉 내려봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-12 오후 12.36.59.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6f7q7/btsMI9hhSRe/SS8Ay6cSVz2eSbUKiEo1e0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6f7q7/btsMI9hhSRe/SS8Ay6cSVz2eSbUKiEo1e0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6f7q7/btsMI9hhSRe/SS8Ay6cSVz2eSbUKiEo1e0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6f7q7%2FbtsMI9hhSRe%2FSS8Ay6cSVz2eSbUKiEo1e0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1310&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2025-03-12 오후 12.36.59.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러로그를 내리다보니 flutterfire_cli와 관련된 문제이고 해결책을 알려주고 있습니다.&lt;br /&gt;Flutter 환경의 Firebase 설정을 위해 CLI 의존성이 필요한데, 이것이 호환이 잘 안되는 모양입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741750812748&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// FlutterFire CLI 설치 여부를 확인합니다. 
dart pub global list | grep flutterfire_cli

// 아래 명령어로 설치하고 활성화할 수 있습니다.
dart pub global activate flutterfire_cli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 개의 커맨드를 통해 이슈를 해결할 수 있습니다.&lt;br /&gt;빌드 에러는 로그에서 항상 힌트를 얻을 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <category>Android</category>
      <category>crashlytics</category>
      <category>firebase</category>
      <category>Flutter</category>
      <category>flutterfire</category>
      <category>ios</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/180</guid>
      <comments>https://doitddo.tistory.com/180#entry180comment</comments>
      <pubDate>Wed, 12 Mar 2025 12:41:40 +0900</pubDate>
    </item>
    <item>
      <title>[Compose Multiplatform] CMP 프로젝트와 SPM(Swift Package Manager)을 함께 사용하기 (카카오, 네이버 로그인을 곁들인)</title>
      <link>https://doitddo.tistory.com/179</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 앱을 개발할 때 로그인 기능은 거의 필수로 구현해야할 기능 중 하나입니다.&lt;br /&gt;로그인의 거부감을 없애고 사용자에게 편리함, 친숙함을 제공하기 위해 구글, 카카오, 네이버, 애플 등과 같은 OAuth 기반의 소셜 로그인은 가장 자주 활용되는 로그인 수단 중 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 CMP로 사이드 프로젝트를 진행하던 중, 이러한 소셜 로그인이 필요하게 되었습니다.&lt;br /&gt;하지만 소셜 로그인들은 KMP 프로젝트를 위한 SDK를 제공하지 않고 Android, iOS와 같은 각 플랫폼의 고유한 SDK를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 CMP 프로젝트에서 소셜 로그인을 구현하기 위해서는 Android, iOS 네이티브 영역의 SDK에서 그 기능을 구현하고 공통 코드 영역에서 이러한 기능을 사용할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMP 프로젝트는 Cocoapods를 사용할 수 있기 때문에 SDK를 사용할 수는 있을 것입니다.&lt;br /&gt;하지만 (제가 알기로) iOS 에서는 Cocoapods의 사용을 지양하고 SPM(Swift Package Manager)를 사용하는 추세이다 보니 SPM으로 구성하는 방법이 궁금해져 찾아본 결과, 아직은 공식적으로 제공하고 있지 않은 상태였습니다.&lt;br /&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issue/KT-53877/Support-Swift-Package-Manager-in-Kotlin-Multiplatform&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtrack.jetbrains.com/issue/KT-53877/Support-Swift-Package-Manager-in-Kotlin-Multiplatform&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740242940568&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Support Swift Package Manager in Kotlin Multiplatform : KT-53877&quot; data-og-description=&quot;There is a gradle plugin for Cocoapods package manager integration: https://kotlinlang.org/docs/native-cocoapods.html The plugin allows: - use 3d-party pods as dependencies in Kotlin projects - integrate Kotlin framework to Xcode project via Cocoapods - pu&quot; data-og-host=&quot;youtrack.jetbrains.com&quot; data-og-source-url=&quot;https://youtrack.jetbrains.com/issue/KT-53877/Support-Swift-Package-Manager-in-Kotlin-Multiplatform&quot; data-og-url=&quot;https://youtrack.jetbrains.com/issue/KT-53877&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issue/KT-53877/Support-Swift-Package-Manager-in-Kotlin-Multiplatform&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://youtrack.jetbrains.com/issue/KT-53877/Support-Swift-Package-Manager-in-Kotlin-Multiplatform&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Support Swift Package Manager in Kotlin Multiplatform : KT-53877&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;There is a gradle plugin for Cocoapods package manager integration: https://kotlinlang.org/docs/native-cocoapods.html The plugin allows: - use 3d-party pods as dependencies in Kotlin projects - integrate Kotlin framework to Xcode project via Cocoapods - pu&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;youtrack.jetbrains.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글의 마지막 댓글을 보시면 SPM을 사용할 수 있도록 플러그인을 개발해주신 아주 고마운 분이 계십니다.&lt;br /&gt;&lt;a href=&quot;https://frankois944.github.io/spm4Kmp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://frankois944.github.io/spm4Kmp/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740243077037&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;SPM For KMP documentation&quot; data-og-description=&quot;An alternative to the dying CocoaPods with custom Kotlin/Swift bridge for KMP.&quot; data-og-host=&quot;frankois944.github.io&quot; data-og-source-url=&quot;https://frankois944.github.io/spm4Kmp/&quot; data-og-url=&quot;https://frankois944.github.io/spm4Kmp/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ugvmz/hyYf11vkdN/KnK49uToZDn89OXkmKQuSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/btUzYH/hyYfUOUifB/niedrm9OeJtgRwRrazRO0k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://frankois944.github.io/spm4Kmp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://frankois944.github.io/spm4Kmp/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ugvmz/hyYf11vkdN/KnK49uToZDn89OXkmKQuSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/btUzYH/hyYfUOUifB/niedrm9OeJtgRwRrazRO0k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SPM For KMP documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An alternative to the dying CocoaPods with custom Kotlin/Swift bridge for KMP.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;frankois944.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이 플러그인을 활용해서 CMP 프로젝트에서 네이버, 카카오 로그인을 구현하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고) 이 포스팅에서 사용된 예시는 다음 Github에서 모든 코드를 확인할 수 있습니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/duitddu/template-cmp-circuit&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/duitddu/template-cmp-circuit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740245585385&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - duitddu/template-cmp-circuit: Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit&quot; data-og-description=&quot;Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit - duitddu/template-cmp-circuit&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot; data-og-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/doPjpf/hyYfKZJ5Fx/ULjnizcMah7X0ou8gUc7eK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cDVwCf/hyYjAVsBT5/6EKMoLmcl1X6DEj2FFgwU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/duitddu/template-cmp-circuit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/doPjpf/hyYfKZJ5Fx/ULjnizcMah7X0ou8gUc7eK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cDVwCf/hyYjAVsBT5/6EKMoLmcl1X6DEj2FFgwU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - duitddu/template-cmp-circuit: Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit - duitddu/template-cmp-circuit&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Swift 코드를 Kotlin 코드에서 사용하려면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버, 카카오 로그인 SDK는 각 네이티브 영역에서 제공되고 있습니다. 따라서 Android의 경우 Kotlin 코드로 네이티브 영역에서 기능을 구현하고 공통 영역에서 사용해야 하며, iOS는 Swift 코드로 네이티브 영역에서 기능을 구현하고 공통 영역에서 사용해야 합니다.&lt;br /&gt;Android는 Kotlin 코드로 작성되어 있기 때문에 문제가 없지만, iOS는 Swift 코드를 Kotlin 코드로 사용해야 하기 때문에 별도의 작업이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/native-objc-interop.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kotlinlang.org/docs/native-objc-interop.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740243215108&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Interoperability with Swift/Objective-C | Kotlin&quot; data-og-description=&quot; &quot; data-og-host=&quot;kotlinlang.org&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/native-objc-interop.html&quot; data-og-url=&quot;https://kotlinlang.org/docs/native-objc-interop.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rl3Z7/hyYjAgRpX6/fIZPx7hIIMK4dqw3PgnVOK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/native-objc-interop.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kotlinlang.org/docs/native-objc-interop.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rl3Z7/hyYjAgRpX6/fIZPx7hIIMK4dqw3PgnVOK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Interoperability with Swift/Objective-C | Kotlin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 코드를 Kotlin 코드에서 사용하기 위해 위와 같은 공식문서에서 가이드 하고 있습니다.&lt;br /&gt;내용을 요약하자면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째, Swift 코드는 KMP에서 사용할 수 없고, Objective-C 코드만 사용할 수 있습니다.&lt;br /&gt;두번째, Swift 코드를 사용하고 싶다면 @objc, @objcMembers 를 사용할 수 있습니다.&lt;br /&gt;세번째, Objective-C 코드를 Kotlin에서 사용하려면 C Interop를 통해 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Objective-C&lt;/span&gt;를 Kotlin에서 사용하기 위해 별도로 생성해야 하는 코드나 설정법이 여간 귀찮다는거죠.&lt;br /&gt;무튼 이 문제가 SPM을 아직 공식적으로 지원하지 않기 때문에 발생합니다.&lt;br /&gt;이를 해결해주는 플러그인을 개발해주신 분께 감사하게 생각하고 공식적으로 지원되길 바래봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;SPM 사용을 위한 환경 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 네이버, 카카오 로그인 SDK 공식 문서를 따라서 Android, iOS 각각에 의존성 설정을 해줍시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Android 설정&lt;/h4&gt;
&lt;pre id=&quot;code_1740243653820&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kotlin {
    ...
    
    androidMain.dependencies {
        ...
        implementation(libs.kakao.user.v2)
        implementation(libs.naver.login)
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android는 toml 파일에 의존성을 선언하고 androidMain 영역에 의존성을 추가하면 됩니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;iOS 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS는 Project &amp;gt; Package Dependencies에서 네이버, 카카오 로그인 SDK를 추가해줍시다.&lt;br /&gt;iOS도 어려울 것 없고 마우스만 딸깍 하면 됩니다. 화면을 참고합시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-23 오전 2.02.12.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr9iDw/btsMtvMAwWw/BzrVSfFq2Y3MzbcVpIUtN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr9iDw/btsMtvMAwWw/BzrVSfFq2Y3MzbcVpIUtN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr9iDw/btsMtvMAwWw/BzrVSfFq2Y3MzbcVpIUtN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcr9iDw%2FbtsMtvMAwWw%2FBzrVSfFq2Y3MzbcVpIUtN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;601&quot; data-filename=&quot;스크린샷 2025-02-23 오전 2.02.12.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;601&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;플러그인 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 말씀드렸던 플러그인을 추가해줍니다. 이것도 어렵지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740243855123&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// libs.versions.toml
[versions]
...
kmp-spm = &quot;0.3.3&quot;

[libraries]
...

[plugins]
...
kmp-spm = { id = &quot;io.github.frankois944.spmForKmp&quot;, version.ref = &quot;kmp-spm&quot; }

// build.gradle.kts
plugins {
    ...
    alias(libs.plugins.kmp.spm)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;플러그인 환경 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPM을 위해 추가한 플러그인은 빌드 타임에 SPM을 위한 코드들을 생성해줍니다.&lt;br /&gt;코드 생성을 위해 몇 가지 환경 설정이 필요합니다. 어려운 내용은 없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740244011310&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// gradle.properties
...
kotlin.mpp.enableCInteropCommonization=true

// build.gradle.kts
kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget -&amp;gt;
        ...
        iosTarget.compilations {
            val main by getting {
                cinterops.create(&quot;nativeIosShared&quot;)
            }
        }
    }
}

swiftPackageConfig {
    create(&quot;nativeIosShared&quot;) {
        // 이 경로는 원하는 대로 변경해도 됩니다.
        customPackageSourcePath = &quot;../iosApp&quot;
        
        // 이 버전은 원하는 대로 변경해도 됩니다.
        minIos = &quot;15.0&quot;

        dependency(
            SwiftDependency.Package.Remote.Version(
                url = URI(&quot;https://github.com/kakao/kakao-ios-sdk&quot;),
                version = &quot;2.23.0&quot;,
                products = {
                    add(&quot;KakaoSDK&quot;)
                }
            ),
            SwiftDependency.Package.Remote.Version(
                url = URI(&quot;https://github.com/naver/naveridlogin-sdk-ios-swift&quot;),
                version = &quot;5.0.0&quot;,
                products = {
                    add(&quot;NidThirdPartyLogin&quot;)
                }
            ),
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 타겟에 C Interop를 위한 설정이 추가되고, SPM의 브릿지를 위한 패키지 경로, iOS의 최소 버전, 의존성 등을 선언해야 합니다.&lt;br /&gt;여기서 customPackageSourcePath를 별도로 지정했는데, 지정하지 않으면 cmoposeApp/src 하위가 디폴트 경로가 됩니다.&lt;br /&gt;저는 XCode 프로젝트에서 Swift 코드를 작성하기 편하도록 iosApp 하위로 지정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 지정된 경로 하위에 &quot;nativeIosShared&quot; 라는 폴더에서 Swift 브릿지 코드를 작성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Swift 브릿지 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPM 코드를 Kotlin에서 사용할 수 있도록 브릿지 코드가 필요합니다. 네이버, 카카오 로그인을 위한 Swift 브릿지 코드를 작성해봅시다.&lt;br /&gt;예시는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740244209115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// iosApp/nativeIosShared

// 카카오 로그인
@objcMembers public class KakaoLoginBridge: NSObject {
    public func request(
        success: @escaping (String) -&amp;gt; Void,
        failure: @escaping () -&amp;gt; Void,
        cancel: @escaping () -&amp;gt; Void
    ) {
        if UserApi.isKakaoTalkLoginAvailable() {
            UserApi.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in
                self?.handle(oauthToken: oauthToken, error: error, success: success, failure: failure, cancel: cancel)
            }
        } else {
            UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in
                self?.handle(oauthToken: oauthToken, error: error, success: success, failure: failure, cancel: cancel)
            }
        }
    }
    
    ...   
}

@objcMembers public class NaverLoginBridge: NSObject {
    public func request(
        success: @escaping (String) -&amp;gt; Void,
        failure: @escaping () -&amp;gt; Void,
        cancel: @escaping () -&amp;gt; Void
    ) {
        NidOAuth.shared.requestLogin { ret in
            ...
        }   
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 일부 생략했습니다. 중요한 것은 @objc 또는 @objcMemebers와 NSObject를 사용해서 Swift 코드를 Objective-C 코드에서 사용할 수 있게 하였습니다.&lt;br /&gt;내부 구현은 네이버, 카카오 SDK를 통해 로그인을 하고 결과를 콜백처리로 되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Gradle Sync 또는 빌드를 하면 플러그인에서 이 브릿지 코드를 Kotlin에서 사용할 수 있도록 코드를 생성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;소셜 로그인 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 네이티브 영역에서 네이버, 카카오 로그인을 구현해야 합니다.&lt;br /&gt;이를 위해 공통 영역에서 인터페이스를 만들고 각 네이티브 영역에서 인터페이스를 구현해 구현체를 주입하는 방식으로 구현됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공통 영역 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 영역의 코드를 공통 영역에서 사용해야 하니 공통영역에서 인터페이스를 먼저 선언합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740244552308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// commonMain
sealed interface SocialAuthResult&amp;lt;out T&amp;gt; {
    data class Success&amp;lt;T&amp;gt;(val data: T) : SocialAuthResult&amp;lt;T&amp;gt;
    data object UserCancelled : SocialAuthResult&amp;lt;Nothing&amp;gt;
    data object Error : SocialAuthResult&amp;lt;Nothing&amp;gt;
}

interface SocialAuthProvider&amp;lt;T&amp;gt; {
    @Composable
    fun get(): SocialAuthenticator&amp;lt;T&amp;gt;
}

interface SocialAuthenticator&amp;lt;T&amp;gt; {
    suspend fun authenticate(): SocialAuthResult&amp;lt;T&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인을 위해 제공되는 모듈은 위 인터페이스를 따라야 합니다.&lt;br /&gt;일부 소셜 로그인은 안드로이드에서 Activity Context를 요구하기 때문에 Composable 함수 내의 Context가 필요합니다. 따라서 소셜 로그인 기능을 제공하는 SocialAuthProvider 인터페이스와 실제 인증을 실행하는 SocialAuthenticator 인터페이스를 만들고, SocialAuthProvider의 함수를 Composable 함수로 만들어 Activity Context에 접근할 수 있도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 소셜 로그인은 각 플랫폼마다 반환하는 토큰 등이 달라지므로 Generic을 활용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740244765049&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// commonMain
data class NaverUser(
    val accessToken: String
)
interface NaverAuthProvider : SocialAuthProvider&amp;lt;NaverUser&amp;gt;

data class KakaoUser(
    val accessToken: String
)
interface KakaoAuthProvider : SocialAuthProvider&amp;lt;KakaoUser&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스에 따라 네이버, 카카오를 위한 응답 모델과 각 네이티브 영역에서 구현해야할 인터페이스를 선언합니다.&lt;br /&gt;인터페이스 구현체가 네이티브 영역에서의 구현체가 주입될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;안드로이드 네이티브 영역 코드 작성&lt;/h4&gt;
&lt;pre id=&quot;code_1740244979535&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// androidMain
internal class KakaoAuthProviderImpl(
    private val appKey: String
) : KakaoAuthProvider {

    @Composable
    override fun get(): SocialAuthenticator&amp;lt;KakaoUser&amp;gt; {
        val context = LocalContext.current
        return KakaoAuthenticator(context, appKey)
    }
}

private class KakaoAuthenticator(
    private val context: Context,
    appKey: String
) : SocialAuthenticator&amp;lt;KakaoUser&amp;gt; {

    init {
        KakaoSdk.init(context, appKey)
    }

    override suspend fun authenticate(): SocialAuthResult&amp;lt;KakaoUser&amp;gt; = suspendCancellableCoroutine { cont -&amp;gt;
        if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
            UserApiClient.instance.loginWithKakaoTalk(context) { token, error -&amp;gt;
                cont.handle(token, error)
            }
        } else {
            UserApiClient.instance.loginWithKakaoAccount(context) { token, error -&amp;gt;
                cont.handle(token, error)
            }
        }
    }
}

internal class NaverAuthProviderImpl(
    private val oauthClientId: String,
    private val oauthClientSecret: String,
    private val oauthClientName: String
) : NaverAuthProvider {

    @Composable
    override fun get(): SocialAuthenticator&amp;lt;NaverUser&amp;gt; {
        val context = LocalContext.current
        return NaverAuthenticator(context, oauthClientId, oauthClientSecret, oauthClientName)
    }
}

private class NaverAuthenticator(
    private val context: Context,
    oauthClientId: String,
    oauthClientSecret: String,
    oauthClientName: String
): SocialAuthenticator&amp;lt;NaverUser&amp;gt; {

    init {
        NaverIdLoginSDK.initialize(context, oauthClientId, oauthClientSecret, oauthClientName)
    }

    override suspend fun authenticate(): SocialAuthResult&amp;lt;NaverUser&amp;gt; = suspendCancellableCoroutine { cont -&amp;gt;
        val callback = object : OAuthLoginCallback {
            ...
        }
        NaverIdLoginSDK.authenticate(context, callback)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인 코드는 콜백인 경우가 많습니다. suspendCacellableCoroutine을 활용해서 코루틴을 활용할 수 있도록 만들어줍니다.&lt;br /&gt;각 소셜 로그인의 SDK의 사용법에 따라 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;iOS 네이티브 영역 코드 작성&lt;/h4&gt;
&lt;pre id=&quot;code_1740245096019&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// iosMain
@OptIn(ExperimentalForeignApi::class)
internal class KakaoAuthProviderImpl(
    private val bridge: KakaoLoginBridge
) : KakaoAuthProvider {

    @Composable
    override fun get(): SocialAuthenticator&amp;lt;KakaoUser&amp;gt; {
        return KakaoAuthenticatorImpl(bridge)
    }
}

@OptIn(ExperimentalForeignApi::class)
private class KakaoAuthenticatorImpl(
    private val bridge: KakaoLoginBridge
) : SocialAuthenticator&amp;lt;KakaoUser&amp;gt; {
    override suspend fun authenticate(): SocialAuthResult&amp;lt;KakaoUser&amp;gt; = suspendCoroutine { cont -&amp;gt;
        bridge.requestWithSuccess(
            success = {
                ...
            },
            failure = {
                ...
            },
            cancel = {
                ...
            }
        )
    }
}

@OptIn(ExperimentalForeignApi::class)
internal class NaverAuthProviderImpl(
    private val bridge: NaverLoginBridge
) : NaverAuthProvider {

    @Composable
    override fun get(): SocialAuthenticator&amp;lt;NaverUser&amp;gt; {
        return NaverAuthenticatorImpl(bridge)
    }
}

@OptIn(ExperimentalForeignApi::class)
private class NaverAuthenticatorImpl(
    private val bridge: NaverLoginBridge
) : SocialAuthenticator&amp;lt;NaverUser&amp;gt; {

    override suspend fun authenticate(): SocialAuthResult&amp;lt;NaverUser&amp;gt; = suspendCoroutine { cont -&amp;gt;
        bridge.requestWithSuccess(
            success = {
                ...
            },
            failure = {
                ...
            },
            cancel = {
                ...
            },
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 만든 브릿지 코드는 각각 Swift로 작성된 클래스의 이름을 따라 NaverLoginBridge, KakaoLoginBridge 라는 Kotlin 클래스가 생성되어 있으며, 이를 사용할 수 있습니다.&lt;br /&gt;마찬가지로 Swift 코드를 콜백 형태로 구현했으므로 suspendCoroutine을 사용해서 코루틴으로 바꿔줍시다.&lt;br /&gt;(Android처럼 suspendCacellableCoroutine을 사용할 수 있으면 좋은데, 메서드를 찾을 수 없었습니다. 아마 플랫폼 차이가 아닐까 합니다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 구현체를 Koin 등을 활용해 구현체를 주입하고 공통 영역에서 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;공통 영역 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통코드에서 사용 예시는 다음과 같습니다. 이 예시에서는 Koin을 사용해 주입합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740245376616&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// commonMain

enum class SocialAuthType {
    NAVER,
    KAKAO
}

@Composable
fun Login(
    ...
) {
    val scope = rememberCoroutineScope()
    val kakaoAuthenticator = koinInject&amp;lt;KakaoAuthProvider&amp;gt;().get()
    val naverAuthenticator = koinInject&amp;lt;NaverAuthProvider&amp;gt;().get()
    
    val socialAuthLaunch: (SocialAuthType) -&amp;gt; Unit = remember {
        { type -&amp;gt;
            scope.launch {
                when (type) {
                    SocialAuthType.NAVER -&amp;gt; {
                        val ret = naverAuthenticator.authenticate()
                        // handle result
                    }

                    SocialAuthType.KAKAO -&amp;gt; {
                        val ret = kakaoAuthenticator.authenticate()
                        // handle result
                    }
                }
            }
        }
    }
    
    SocialAuthButtons { type -&amp;gt;
        socialAuthLaunch.invoke(type)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 예시는 다를 수 있습니다만, Android에서 Activity Context의 주입을 위해 위와 같이 Composable 함수에서 각 소셜에 맞는 Provider를 통해 실제 소셜 로그인을 수행하는 클래스를 획득해야 하는 제약이 있습니다. (다른 구현이 있다면 알려주세요.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예시는 소셜 로그인이었지만, 이것을 활용하면 그 어떤 네이티브 영역의 코드든 충분히 CMP에서 활용할 수 있을 것 같습니다.&lt;br /&gt;플러그인 개발자분에게 감사한 마음과 SPM이 공식적으로 지원되길 바라면서 포스팅을 마치겠습니다.&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>CMP</category>
      <category>compose</category>
      <category>compose multiplatform</category>
      <category>ios</category>
      <category>KMP</category>
      <category>Kotlin MultiPlatform</category>
      <category>SPM</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/179</guid>
      <comments>https://doitddo.tistory.com/179#entry179comment</comments>
      <pubDate>Sun, 23 Feb 2025 02:33:31 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Compose 성능 개선 - Compose Compiler Metrics Report와 Restartable, Skippable</title>
      <link>https://doitddo.tistory.com/178</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발을 하시는 분이라면 Compose로 개발을 하고 계시거나 개발할 계획이 있을 것입니다.&lt;br /&gt;Compose는 안드로이드 앱에서 UI를 만들기 위해 사용하는 도구이기 때문에, 사용자에게 안정적이고 빠른 화면을 보여주기 위해 성능을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해선 Compose가 어떻게 동작하는지에 대한 이해, 그리고 그 이해를 바탕으로 효과적으로 동작할 수 있도록 코드를 작성하는 것이 중요할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나름대로 이해도 해보고 잘 작성했다고 하더라도 실제로 잘 한게 맞는지를 확인하기 위해선 어떤 지표를 확인하는게 좋겠죠.&lt;br /&gt;이 포스팅에선 지표를 확인하고 이해해보는 것을 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compose Compiler Metrics Report&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI를 효율적으로 작성한다는 것은 그려내는 시간을 단축하는 것도 좋지만, 불필요한 갱신이 발생하지 않도록 하는것도 매우 중요한 부분입니다.&lt;br /&gt;Compose는 최초 화면에 나타난 이후, 상태의 변화에 따라 0번 이상 리컴포지션(Recomposition)이 발생하게 됩니다.&lt;br /&gt;화면에 표현해야할 데이터(상태)가 변경되면 당연히 리컴포지션을 거쳐 다시 그려내는게 맞지만 그렇지 않은 상황에서도 다시 그려낸다면 비효율이 발생하여 결국 성능에 영향을 미치게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 그럼 어디에서 불필요한 리컴포지션이 발생하는지를 알아야겠죠. 이 때 활용할 수 있는 것이 Compose Compiler Metrics Report입니다.&lt;br /&gt;안드로이드 프로젝트에 다음과 같은 코드를 추가합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1739719738078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts (:app)
android {

    ...
    
    kotlinOptions {
        freeCompilerArgs += listOf(&quot;-P&quot;,
            &quot;plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${rootProject.file(&quot;.&quot;).absolutePath}/compose-metrics&quot;
        )
        freeCompilerArgs += listOf(&quot;-P&quot;,
            &quot;plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${rootProject.file(&quot;.&quot;).absolutePath}/compose-reports&quot;
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어는 빌드 시에 Compose compiler가 프로젝트의 Composable에 대한 지표와 세부적인 보고서를 생성해줍니다.&lt;br /&gt;빌드를 해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.31.08.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4sqOW/btsMkAmYSDu/JZScrq8cWfxFo4lf7ogen1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4sqOW/btsMkAmYSDu/JZScrq8cWfxFo4lf7ogen1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4sqOW/btsMkAmYSDu/JZScrq8cWfxFo4lf7ogen1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4sqOW%2FbtsMkAmYSDu%2FJZScrq8cWfxFo4lf7ogen1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;658&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.31.08.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 디렉토리로 이동하면 compose-metrics와 compose-reports 폴더가 생성되었고, 그 내부에 무언가 파일들이 많이 생성된 것을 알 수 있습니다.&lt;br /&gt;UnitTest, AndroidTest는 제외하고 각각의 내용에 대해 확인해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;app-debug-module.json&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.32.28.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ffzc/btsMkVEssXg/6XATcxfOnLREChH846mez1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ffzc/btsMkVEssXg/6XATcxfOnLREChH846mez1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ffzc/btsMkVEssXg/6XATcxfOnLREChH846mez1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ffzc%2FbtsMkVEssXg%2F6XATcxfOnLREChH846mez1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;696&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.32.28.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose-metrics는 프로젝트에 존재하는 Composable 함수들에 대한 수치적인 지표를 생성해줍니다.&lt;br /&gt;전체 Composable 함수는 몇 개인지, Skippable, Restartable 에 해당하는 Composable 함수는 무엇인지와 같은 지표가 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RestartableComposables&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것에 해당하는 Composable 함수들은 Compose 함수의 상태가 변경되어 리컴포지션(Recomposition)이 발생했을 때 재시작될 수 있는 함수를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;SkippableComposables&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것에 해당하는 Composable 함수들은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Compose 함수의 상태가 변경되어 리컴포지션(Recomposition)이 발생했을 때, Composable 함수가 추적하는 파라미터(상태)가 이전과 동일하다면 스킵할 수 있는 함수들을 의미합니다.&lt;br /&gt;우리는 리컴포지션이 발생하더라도 상태가 변경되지 않으면 Skip될 수 있도록 Composable을 구성하는 것을 목표로 해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;app-debug-classes.txt&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.42.42.png&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxlV3T/btsMkJjSXBP/UVx3GXltc5TROsfmobFz2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxlV3T/btsMkJjSXBP/UVx3GXltc5TROsfmobFz2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxlV3T/btsMkJjSXBP/UVx3GXltc5TROsfmobFz2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxlV3T%2FbtsMkJjSXBP%2FUVx3GXltc5TROsfmobFz2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1104&quot; height=&quot;846&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.42.42.png&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 프로젝트에서 사용되는 클래스들의 Stable 여부를 파악해줍니다.&lt;br /&gt;위 예시에서 어떤 클래스는 stable claass로, 어떤 클래스는 unstable class로 나타나 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739720645650&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Compose의 안정성 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose의 안정성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose는 유형을 안정적이거나 불안정&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&quot; data-og-url=&quot;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/07BV0/hyYfYJsnhV/fCQaLt9FLyBtCFUIOxDJyk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/ui/compose/performance/stability?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/07BV0/hyYfYJsnhV/fCQaLt9FLyBtCFUIOxDJyk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose의 안정성 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose의 안정성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose는 유형을 안정적이거나 불안정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose의 Stability(안정성)은 Composable 함수의 리컴포지션에 아주 중요한 역할을 합니다.&lt;br /&gt;Composable는 관찰하고 있는 파라미터(상태)의 변화에 따라 리컴포지션을 수행하는데, 리컴포지션이 발생했을 때 자신이 관찰하고 있는 파라미터(상태)가 변경되지 않았다면 생략(Skip)될 수 있습니다. 이 때, &quot;변경되지 않았다&quot;의 기준은 값 자체가 변경되지 않는 것도 맞지만, 그 값이 안정적인(Stable) 상태가 아니라면 생략하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Composable의 생태에 해당하는 값, 클래스를 Compose Compiler가 stable로 인지해야 합니다.&lt;br /&gt;이것에 대해서는 다음 기회에 좀 더 상세히 다루겠습니다. 궁금하시다면 구글의 공식 문서를 참고해보시는 것을 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;app_debug-composables.csv&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.50.43.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WlXjt/btsMjIGpMwP/9tcTbv5XHG5JlDmcp29wgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WlXjt/btsMjIGpMwP/9tcTbv5XHG5JlDmcp29wgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WlXjt/btsMjIGpMwP/9tcTbv5XHG5JlDmcp29wgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWlXjt%2FbtsMjIGpMwP%2F9tcTbv5XHG5JlDmcp29wgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;604&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.50.43.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 Composable 함수의 이름과 Skippable, Restartable 인지에 대한 여부가 표시되어 있습니다.&lt;br /&gt;1이라면 true를 의미합니다.&lt;br /&gt;우리는 여기서 Restartable이 1이지만 Skippable이 0 인 Composable 함수를 찾아 개선할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;app_debug-composables.txt&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.54.49.png&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btmpPH/btsMjPSNmnH/yEr9ZXTgDhIJkJK3aM5wMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btmpPH/btsMjPSNmnH/yEr9ZXTgDhIJkJK3aM5wMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btmpPH/btsMjPSNmnH/yEr9ZXTgDhIJkJK3aM5wMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtmpPH%2FbtsMjPSNmnH%2FyEr9ZXTgDhIJkJK3aM5wMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2494&quot; height=&quot;896&quot; data-filename=&quot;스크린샷 2025-02-17 오전 12.54.49.png&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 Composable 함수들의 restartable, skippable 여부, 그리고 함수의 파라미터(상태)들이 stable, unstable 여부를 나타냅니다.&lt;br /&gt;위 예시에서 OkCancelDialog를 보면 함수 자체는 restartable, skippable 이지만 unstable 파라미터가 존재하는 것을 알 수 있습니다.&lt;br /&gt;우리는 Composable 함수의 파라미터들이 모두 stable한 것이 좋으므로 이 파일을 확인하여 stable한 파라미터가 될 수 있도록 개선할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose의 동작 방식을 이해하고 Stability를 신경쓰며 개발한다고 해도 놓치는 부분이 생길 수 있습니다.&lt;br /&gt;사실 이 포스팅의 예시를 만들기 위해 일부로 바꾼 부분도 있지만, 놓친 부분이 있었거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기적으로 오늘 학습했던 지표들을 활용해 Composable 함수들을 분석하고 개선해 간다면, 앱의 품질과 성능을 개선하고, 앞으로 더 신경써서 개발할 수 있는 기반을 마련하면 좋을 것 같습니다. :)&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>compose</category>
      <category>Jetpack Compose</category>
      <category>Metrics</category>
      <category>restartable</category>
      <category>skippable</category>
      <category>stability</category>
      <category>리컴포지션</category>
      <category>안드로이드</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/178</guid>
      <comments>https://doitddo.tistory.com/178#entry178comment</comments>
      <pubDate>Mon, 17 Feb 2025 01:01:55 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 의존성 주입 - Starting with Koin Annotations (feat. Compose, KMP, Circuit)</title>
      <link>https://doitddo.tistory.com/177</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 Compose Multiplatform을 활용하여 사이드 프로젝트를 진행중입니다.&lt;br /&gt;Android, iOS, Flutter를 전부 실무에서 활용해본 경험으로 미루어 봤을때 꽤 좋은 선택지가 될 것이라는 기대 덕분인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 아키텍처가 무엇인지 고민하던 찰나, Slack에서 개발한 Circuit 아키텍처가 마음에 들어 사용해보고 있습니다.&lt;br /&gt;현재 구글이 KMP를 적극 지원중이고 여러 Jetpack 라이브러리들이 KMP를 지원하도록 업데이트 되고 있으니 가까운 미래에 바뀔지도 모르지만요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처를 구성한다는 것은 의존성 주입 또한 중요한 문제가 될 텐데요.&lt;br /&gt;안드로이드 개발자라면 Jetpack Hilt를 사용하고 있을테지만 안타깝게도 KMP에서는 사용이 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 코틀린으로 작성된 Koin이 좋은 대안이 될 수 있습니다만, Koin은 Kotlin DSL을 활용해서 런타임에 주입해주는 방식이었기 때문에 의존성 주입을 잘 구성하지 않았을 때 Hilt와 달리 컴파일 타임에 발견이 안되고 런타임에 알 수 있기 때문에 크래쉬 발생 리스크가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 꽤 이전부터 Koin도 KSP와 어노테이션을 활용한 코드생성 방식을 지원하고 있었고 이러한 단점을 해소하고 있었습니다.&lt;br /&gt;&lt;a href=&quot;https://insert-koin.io/docs/reference/koin-annotations/start/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://insert-koin.io/docs/reference/koin-annotations/start/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739509773298&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Starting with Koin Annotations | Koin&quot; data-og-description=&quot;The goal of Koin Annotations project is to help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you. The goal is to help developer experience to scale and go fast  , thanks to Kotlin Compilers.&quot; data-og-host=&quot;insert-koin.io&quot; data-og-source-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/start/&quot; data-og-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/start&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FF5Iu/hyYcThLn8Y/b0fyK0FAFK9F7k2txAfUK1/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/bwCeeB/hyYcVmhXsu/Tjj5t01PdDPNXkqI6pCOz0/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667&quot;&gt;&lt;a href=&quot;https://insert-koin.io/docs/reference/koin-annotations/start/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/start/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FF5Iu/hyYcThLn8Y/b0fyK0FAFK9F7k2txAfUK1/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/bwCeeB/hyYcVmhXsu/Tjj5t01PdDPNXkqI6pCOz0/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Starting with Koin Annotations | Koin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The goal of Koin Annotations project is to help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you. The goal is to help developer experience to scale and go fast  , thanks to Kotlin Compilers.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;insert-koin.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 사이드 프로젝트에 활용하려고 만들고 있는 Circuit 템플릿 프로젝트를 마이그레이션 하였고, 이 과정에 대해 포스팅합니다.&lt;br /&gt;내용은 굉장히 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Starting with Koin Annotations&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이든 시작전에 공식문서를 한번 보는걸 습관화하는게 좋습니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;공식 문서를 바탕으로 내용을 가볍게 이해해봅시다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://insert-koin.io/docs/setup/annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://insert-koin.io/docs/setup/annotations&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739509928236&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Koin Annotations | Koin&quot; data-og-description=&quot;Setup Koin Annotations for your project&quot; data-og-host=&quot;insert-koin.io&quot; data-og-source-url=&quot;https://insert-koin.io/docs/setup/annotations&quot; data-og-url=&quot;https://insert-koin.io/docs/setup/annotations&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bUvzPg/hyYf0GHTRy/kRBKVCMdwdjWOESipcLqfK/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/eoUG3x/hyYfAH1qkY/KF6HxRjl0Awg7RrKuiEVT1/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667&quot;&gt;&lt;a href=&quot;https://insert-koin.io/docs/setup/annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://insert-koin.io/docs/setup/annotations&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bUvzPg/hyYf0GHTRy/kRBKVCMdwdjWOESipcLqfK/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/eoUG3x/hyYfAH1qkY/KF6HxRjl0Awg7RrKuiEVT1/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Koin Annotations | Koin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Setup Koin Annotations for your project&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;insert-koin.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크를 참고해서 KSP와 Koin annotations의존성을 추가해줍시다.&lt;br /&gt;KSP는 작성일 기준 2.0.20-1.0.25와 호환됩니다.&lt;br /&gt;안드로이드 기준으로 다음과 같은 의존성 추가가 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739510018860&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
   id(&quot;com.google.devtools.ksp&quot;) version &quot;$ksp_version&quot;
}


dependencies {
    // Koin
    implementation(&quot;io.insert-koin:koin-android:$koin_version&quot;)
    // Koin Annotations
    implementation(&quot;io.insert-koin:koin-annotations:$koin_annotations_version&quot;)
    // Koin Annotations KSP Compiler
    ksp(&quot;io.insert-koin:koin-ksp-compiler:$koin_annotations_version&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMP의 경우 의존성 추가 방법이 따로 있습니다. 뒤에서 다시 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Koin Compile Safety&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koin의 Compile Safety 동작을 위해 활성화해야할 옵션이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739511190634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// in build.gradle or build.gradle.kts
ksp {
    arg(&quot;KOIN_CONFIG_CHECK&quot;,&quot;true&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 gradle에 선언하면, Koin 어노테이션으로 선언된 의존성들이 올바른지 체크합니다.&lt;br /&gt;이제 Koin을 사용할 때 런타임 크래쉬를 걱정하는 일이 없겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Koin Annotations&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koin에서 제공하는 어노테이션들에 먼저 알아보겠습니다.&lt;br /&gt;모두를 정리하진 않고, 핵심적인 부분만 정리하겠습니다. 더 자세히 알고자 하시는 분들은 공식문서 참고를 권장합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@Module&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koin 모듈을 정의하기 위해 사용합니다. 예시 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739510554083&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 모듈 선언
@Module
@ComponentScan
class MyModule

// 생성된 코드를 import 해야 합니다.
import org.koin.ksp.generated.*

fun main() {
    val koin = startKoin {
        modules(
            // KSP에 의해 코드가 생성됩니다.
            MyModule().module
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 @ComponentScan 어노테이션은, 어노테이션이 달려있는 컴포넌트들을 모두 스캔하고 수집합니다. 스캔 대상은 현재 패키지와 모든 서브 패키지를 포함합니다.&lt;br /&gt;만약 특정 패키지를 대상으로 하고 싶다면 @ComponentScan(&quot;com.my.package&quot;)와 같이 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드를 하고나면 KSP에 의해 코드가 생성되고, 모듈 객체의 .module 속성으로 접근할 수 있습니다.&lt;br /&gt;만약 여러 모듈이 있다면 다음과 같은 방법도 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739510780712&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
class ModuleA

@Module(includes = [ModuleA::class])
class ModuleB


fun main() {
    startKoin {
        modules(
          // 모듈 A와 모듈 B가 모두 생성됩니다.
          ModuleB().module
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈간의 의존관계를 정의해서 상위 모듈의 선언만으로 하위 모듈들을 모두 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@Single&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Single 어노테이션은 DSL 방식 Koin의 single { ... } 와 동일합니다.&lt;br /&gt;즉, 이 어노테이션은 컴포넌트를 Singletone으로 생성합니다.&lt;br /&gt;다음과 같이 정의할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739510972378&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Single
class MyComponent1(val myDependency : MyDependency)

@Single(binds = [MyBoundType::class])
class MyComponent2(val myDependency : MyDependency) : MyInterface&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@Factory&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Factory 어노테이션은 DSL 방식 Koin의 factory { ... } 와 동일합니다.&lt;br /&gt;즉, 이 어노테이션은 컴포넌트를 매번 필요할 때 마다 재생성합니다.&lt;br /&gt;다음과 같이 정의할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739511076998&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Factory
class MyComponent1(val myDependency : MyDependency)

@Factory(binds = [MyBoundType::class])
class MyComponent2(val myDependency : MyDependency) : MyInterface&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;모듈 내에서 컴포넌트 선언하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모듈 내에서 직접 컴포넌트를 선언하고 싶다면, 다음과 같이 할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739511996237&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
class MyModule {

  @Single
  fun myComponent(myDependency : MyDependency) = MyComponent(myDependency)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으론 모듈에서 담당해야할 컴포넌트가 무엇인지 알 수 있어서 선호하는 방식입니다.&lt;br /&gt;Hilt에서 마이그레이션 한다면 이 구조가 굉장히 효율적일 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KMP에서 Koin Annotations 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://insert-koin.io/docs/reference/koin-annotations/kmp&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://insert-koin.io/docs/reference/koin-annotations/kmp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739511301706&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Annotations for Definitions and Modules in Kotlin Multiplatform App | Koin&quot; data-og-description=&quot;KSP Setup&quot; data-og-host=&quot;insert-koin.io&quot; data-og-source-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/kmp&quot; data-og-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/kmp&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byYhOO/hyYahJ9YqK/iUUB2t1hHSkl8DsyWxv8dk/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/bBv67R/hyYf16InJv/Trn4gdjc2rYAOlcY76D4e0/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667&quot;&gt;&lt;a href=&quot;https://insert-koin.io/docs/reference/koin-annotations/kmp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://insert-koin.io/docs/reference/koin-annotations/kmp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byYhOO/hyYahJ9YqK/iUUB2t1hHSkl8DsyWxv8dk/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667,https://scrap.kakaocdn.net/dn/bBv67R/hyYf16InJv/Trn4gdjc2rYAOlcY76D4e0/img.png?width=667&amp;amp;height=667&amp;amp;face=0_0_667_667');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Annotations for Definitions and Modules in Kotlin Multiplatform App | Koin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;KSP Setup&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;insert-koin.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMP에서 Koin annotations를 위한 설정은 위 문서에 나와있습니다. 문서를 참고하셔도 쉽게 할 수 있습니다.&lt;br /&gt;다음과 같이 의존성을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739511699282&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts

plugins {
    ...
    alias(libs.plugins.ksp)
}

kotlin {

    sourceSets {
        commonMain.dependencies {
            implementation(libs.koin.core)
            api(libs.koin.annotations)
        }
        
        ...
    }
    
    sourceSets.named(&quot;commonMain&quot;).configure {
        kotlin.srcDir(&quot;build/generated/ksp/metadata/commonMain/kotlin&quot;)
    }       
}

dependencies {
    add(&quot;kspCommonMainMetadata&quot;, libs.koin.ksp.compiler)
    add(&quot;kspAndroid&quot;, libs.koin.ksp.compiler)
    add(&quot;kspIosX64&quot;, libs.koin.ksp.compiler)
    add(&quot;kspIosArm64&quot;, libs.koin.ksp.compiler)
    add(&quot;kspIosSimulatorArm64&quot;, libs.koin.ksp.compiler)
}

project.tasks.withType(KotlinCompilationTask::class.java).configureEach {
    if(name != &quot;kspCommonMainKotlinMetadata&quot;) {
        dependsOn(&quot;kspCommonMainKotlinMetadata&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Circuit 모듈 마이그레이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존의 Circuit 모듈을 마이그레이션 해 보겠습니다.&lt;br /&gt;Circuit 모듈은 Presenter.Factory, Ui.Factory, Circuit 세 가지 컴포넌트가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Presenter.Factory, Ui.Factory&lt;/h3&gt;
&lt;pre id=&quot;code_1739511926726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CircuitPresenterFactory : Presenter.Factory {
    override fun create(
        screen: Screen,
        navigator: Navigator,
        context: CircuitContext
    ): Presenter&amp;lt;*&amp;gt; {
        return when (screen) {
            is MainScreen -&amp;gt; MainPresenter(screen, navigator)
            else -&amp;gt; throw Exception(&quot;Invalid Screen Detected! :: $screen&quot;)
        }
    }
}

class CircuitUiFactory : Ui.Factory {
    override fun create(screen: Screen, context: CircuitContext): Ui&amp;lt;*&amp;gt;? {
        return ui&amp;lt;CircuitUiState&amp;gt; { state, modifier -&amp;gt;
            when (state) {
                is MainScreen.State -&amp;gt; Main(state, modifier)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presenter.Factory, Ui.Factory 인터페이스를 구현하는 클래스를 정의했습니다.&lt;br /&gt;이 클래스는 마이그레이션 하기 전과 다르지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Circuit Module&lt;/h3&gt;
&lt;pre id=&quot;code_1739512110171&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module
class CircuitModule {
    @Factory(binds = [Presenter.Factory::class])
    fun circuitPresenterFactoryComponent() = CircuitPresenterFactory()

    @Factory(binds = [Ui.Factory::class])
    fun circuitUiFactoryComponent() =  CircuitUiFactory()

    @Single
    fun circuit(
        presenterFactory: CircuitPresenterFactory,
        uiFactory: CircuitUiFactory
    ) = Circuit.Builder()
        .addPresenterFactory(presenterFactory)
        .addUiFactory(uiFactory)
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit 모듈을 생성합니다.&lt;br /&gt;@Module, @Factory, @Single 어노테이션을 사용하여 구현해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presenter.Factory, Ui.Factory는 @Factory를 사용하여 필요할 때 재생성하고, Circuit은 앱에서 단 하나의 인스턴스만 가지므로 @Single을 사용하여 Singletone으로 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App Module&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 전체 모듈을 포함하는 모듈을 정의합니다.&lt;br /&gt;이는 앱의 동작동안 어떤 모듈들과 의존성들을 가지고 있는지 명확하게 하려는 의도를 가지고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739512335957&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module(
    includes = [
        CircuitModule::class
    ]
)
@ComponentScan
class AppModule&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 CircuitModule만을 포함하지만, 나중에 여러 레이어(Presentation, Domain, Data)가 추가된다면 다수의 모듈들이 앱 모듈에 포함될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;생성한 모듈 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Koin을 Android, iOS 플랫폼에서 각각 사용해주는 일만 남았습니다.&lt;br /&gt;Koin을 초기화하는 공통 함수를 정의해서 AppModule을 포함할 수 있도록 코드를 작성하고, 각 플랫폼에서 호출해줍니다.&lt;br /&gt;예시 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739512445832&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun initKoin(config: KoinAppDeclaration? = null) {
    startKoin {
        config?.invoke(this)
        printLogger()
        modules(
            AppModule().module
        )
    }
}

// Android
class Application : Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin {
            androidContext(this@Application)
        }
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val koin = getKoin()
            val circuit: Circuit = koin.get()
            App(circuit)
        }
    }
}

// iOS
fun MainViewController() = ComposeUIViewController(
    configure = {
        initKoin()
    }
) {
    val koin = getKoin()
    val circuit: Circuit = koin.get()
    App(circuit)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Android, iOS 네이티브를 둘 다 구현할 수 있어 크로스 플랫폼은 살짝 멀리 하는 경향이 있었습니다.&lt;br /&gt;Flutter와 비교했을 때 생산성의 차이는 있겠습니다만, 저는 생산성이 높은 편이고 이슈 핸들링과 같은 부분들에 있어 결국 네이티브가 낫다는 개인적인 결론이 있었기 때문인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최근 CMP의 등장과 발전으로 다시금 크로스 플랫폼에 관심을 두게 되고 여러가지 새로운 기술들과 사실들을 알게 되어 흥미진진합니다. 그리고 그동안 Koin의 런타임 리스크때문에 너무 멀리하고 있었나 하는 생각이 들어 반성하는 시간을 가지게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅의 전체 코드는 다음 Github에 공유되어 있습니다. (아직 템플릿 치고 많이 허접합니다.&lt;br /&gt;업데이트 예정이니 의견을 주셔도 좋겠습니다. ㅋ.ㅋ)&lt;br /&gt;&lt;a href=&quot;https://github.com/duitddu/template-cmp-circuit&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/duitddu/template-cmp-circuit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739512820846&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - duitddu/template-cmp-circuit: Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit&quot; data-og-description=&quot;Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit - duitddu/template-cmp-circuit&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot; data-og-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/XTl1V/hyYf5VzEHX/8mMKrXusJJMZlvP0hl9jSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Q1jbi/hyYfVrS4qx/EhURG0FBdZ9DphIiuqHcTk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/duitddu/template-cmp-circuit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/duitddu/template-cmp-circuit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/XTl1V/hyYf5VzEHX/8mMKrXusJJMZlvP0hl9jSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Q1jbi/hyYfVrS4qx/EhURG0FBdZ9DphIiuqHcTk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - duitddu/template-cmp-circuit: Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin Multiplatform Mobile(Android, iOS) + Compose + Circuit - duitddu/template-cmp-circuit&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 포스팅이 저와 같이 CMP에 관심을 가지고 계신 분에게 도움이 되길 바라며 마칩니다.&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>Circuit</category>
      <category>CMP</category>
      <category>compose</category>
      <category>KMP</category>
      <category>Koin</category>
      <category>아키텍처</category>
      <category>안드로이드</category>
      <category>의존성주입</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/177</guid>
      <comments>https://doitddo.tistory.com/177#entry177comment</comments>
      <pubDate>Fri, 14 Feb 2025 15:02:42 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 안드로이드 메모리 누수에 관하여</title>
      <link>https://doitddo.tistory.com/176</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 운영하고 있는 서비스는 Firebase Crashlytics를 사용해서 앱의 로그를 수집하고 있습니다.&lt;br /&gt;99.99%정도는 크래쉬가 발생하지 않지만 일부 크래쉬가 리포트되고 있는데, 그 중 OOM이 꽤 비중을 차지하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.09.39.png&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1016&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baSMDS/btsL6UZ5lZu/5L1pqw9mCvn17FXEOvQhL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baSMDS/btsL6UZ5lZu/5L1pqw9mCvn17FXEOvQhL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baSMDS/btsL6UZ5lZu/5L1pqw9mCvn17FXEOvQhL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaSMDS%2FbtsL6UZ5lZu%2F5L1pqw9mCvn17FXEOvQhL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1342&quot; height=&quot;1016&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.09.39.png&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1016&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 또는 동영상이 보이거나 백그라운드에서 센서를 활용하는 등의 기능이 있어서인지 OOM이 크래쉬의 주된 원인이 되고 있는데요.&lt;br /&gt;이러한 OOM의 원인은 메모리에 비해 큰 이미지, 동영상과 같은 것을 로드할때도 발생하지만 메모리 누수에 의해 발생하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 누수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 누수는 필요하지 않은 메모리를 계속 차지하고 있는 것을 말합니다.&lt;br /&gt;사용하지 않는 메모리가 계속 공간을 차지하기 때문에 필요한 메모리들이 할당될 공간이 점점 적어지고 결과적으로 메모리가 부족한 상황에 이르게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드가 동작하는 JVM 환경에서는 더이상 사용되지 않는 객체의 참조가 GC에 의해 제거되지 않아 발생합니다.&lt;br /&gt;가비지 컬렉터(GC)는 메모리에서 제거해야할 객체의 대상의 참조가 없으면 불필요하다고 판단하여 제거하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 안드로이드에서 이러한 메모리 누수는 어떤 경우에 발생할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 메모리 누수 케이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 메모리 누수의 케이스는 다양합니다. 모든 케이스를 다룰 순 없고, 어떠한 경우가 메모리 누수를 발생시킬 수 있는 상황인지 대표적인 사례로 설명하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫번째, Activity Context를 Jetpack ViewModel에 전달할 때 발생합니다.&lt;/h4&gt;
&lt;pre id=&quot;code_1738649769894&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyActivity : AppCompatActivity() {
    val viewModel: MyViewModel by viewModels()
    
    private fun doSomething() {
        viewModel.doSomething(this)
    }
}

class MyViewModel : ViewModel() {
    fun doSomething(context: Context) {
        viewModelScope.launch {
            ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack ViewModel은 Activity보다 더 긴 라이프사이클을 가지고 있습니다.&lt;br /&gt;Activity가 Destroy가 되고 나서야 비로소 ViewModel이 제거가 되기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLDvLN/btsL5jmJ1v4/TJfb5gf42VsappZnYr86p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLDvLN/btsL5jmJ1v4/TJfb5gf42VsappZnYr86p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLDvLN/btsL5jmJ1v4/TJfb5gf42VsappZnYr86p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLDvLN%2FbtsL5jmJ1v4%2FTJfb5gf42VsappZnYr86p0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;543&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Activity보다 긴 라이프 사이클을 가진 ViewModel에서 Activity Context를 참조하게 되면 Activity가 Destroy되어 메모리에서 정리가 되어야하지만 ViewModel에서 참조를 유지하기 때문에 메모리에서 제거되지 못합니다. 따라서 메모리 누수에 이르게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ViewModel에서 Activity Context를 참조하지 않도록 해야 합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;두번째, Fragment에서 DataBinding/ViewBinding을 Non-Null로 사용하면 발생합니다.&lt;/h4&gt;
&lt;pre id=&quot;code_1738649924408&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyFragment : Fragment() {
    private lateinit var binding: MyFragmentBinding
    
    override fun onCreateView(...): View? {
        binding = ...
        
        return binding.root
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment는 View에 대한 라이프사이클을 별도로 가지고 있으며, 이에 따라 View는 생성되고 파괴됨을 반복합니다.&lt;br /&gt;이 과정에서 View가 파괴되었음에도 binding 객체를 Non-Null로 관리하여 이전 객체를 그대로 사용하면 메모리에서 정리되지 못하기 때문에 메모리 누수가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Binding 객체를 Nullable로 선언하고 View의 생성과 파괴에 따라 적절하게 처리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;안드로이드 메모리 프로파일링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 케이스 외에도 다양한 상황에서 메모리 누수는 발생할 수 있습니다. 개발을 하면서 이런 것들을 신경써서 나지 않게 한다면 제일 좋겠지만, 예상치 못하는 부분에서 발생할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우리는 개발 후에 앱의 성능, 품질을 개선하기 위해 이러한 요소들을 찾아 개선해야 하는 숙명을 가지고 있습니다.&lt;br /&gt;이 요소를 찾는 방법으로 안드로이드 스튜디오에서 프로파일러를 제공해줍니다.&lt;br /&gt;&lt;a href=&quot;https://developer.android.com/studio/profile/memory-profiler?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/studio/profile/memory-profiler?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738650433840&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;메모리 프로파일러로 앱의 메모리 사용량 검사 &amp;nbsp;|&amp;nbsp; Android Studio &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;끊김 현상, 멈춤, 심지어 비정상 종료를 일으킬 수 있는 메모리 누수 및 메모리 변동을 식별하는 데 도움이 되는 Android 프로파일러의 메모리 프로파일러 구성요소를 알아보세요.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/studio/profile/memory-profiler?hl=ko&quot; data-og-url=&quot;https://developer.android.com/studio/profile/memory-profiler?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chngLf/hyX7T3mPzj/F1WQvhlTwKHopYBiNi6Xy0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/studio/profile/memory-profiler?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/studio/profile/memory-profiler?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chngLf/hyX7T3mPzj/F1WQvhlTwKHopYBiNi6Xy0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;메모리 프로파일러로 앱의 메모리 사용량 검사 &amp;nbsp;|&amp;nbsp; Android Studio &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;끊김 현상, 멈춤, 심지어 비정상 종료를 일으킬 수 있는 메모리 누수 및 메모리 변동을 식별하는 데 도움이 되는 Android 프로파일러의 메모리 프로파일러 구성요소를 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 스튜디오 Ladybug Feature Drop | 2024.2.2 기준으로, View &amp;gt; Tool Windows &amp;gt; Profiler로 접근하면 다음과 같은 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.27.47.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zb40Z/btsL5u9r4lc/osKO2sxqmIy9xC4Pwv3k51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zb40Z/btsL5u9r4lc/osKO2sxqmIy9xC4Pwv3k51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zb40Z/btsL5u9r4lc/osKO2sxqmIy9xC4Pwv3k51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZb40Z%2FbtsL5u9r4lc%2FosKO2sxqmIy9xC4Pwv3k51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;382&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.27.47.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 항목들도 앱의 성능 프로파일링에 도움이 되기 때문에 확인해보면 좋겠습니다만, 이 포스팅에서는 Analyze Memory Usage와 Track Memory Comsumption을 확인해보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Analyze Memory Usage&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에뮬레이터 또는 기기에서 앱을 실행하고 이 버튼을 누르면 현재 앱의 힙 메모리를 덤프해 줍니다. 그 결과로 다음과 같은 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.32.28.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E2ITs/btsL52EPKPf/PTsc10o4asxmimw5XW7BZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E2ITs/btsL52EPKPf/PTsc10o4asxmimw5XW7BZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E2ITs/btsL52EPKPf/PTsc10o4asxmimw5XW7BZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE2ITs%2FbtsL52EPKPf%2FPTsc10o4asxmimw5XW7BZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1834&quot; height=&quot;741&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.32.28.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 앱에서 사용한 모든 클래스에 대한 메모리 할당과 크기같은 것들을 확인할 수 있습니다. 보시면 노란색 경고 아이콘과 함께 숫자가 나타나 있는걸 볼 수 있는데, 저것이 메모리 누수를 나타냅니다.&lt;br /&gt;Show all classes에서 Show activity/fragment leaks로 변경해보거나 스크롤을 내려봅시다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7t7FW/btsL510dPjF/uFYFFCaS0Rt04xRmWvqN5k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1837&quot; data-origin-height=&quot;220&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.31.34.png&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느곳에서 메모리 누수가 발생했는지 확인할 수 있습니다. 제 앱의 LoginActivity에서 메모리 누수가 발생했네요.&lt;br /&gt;ReportFragment는 제 앱의 코드가 아니므로 눈을 가리도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Track Memory Comsumption&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에뮬레이터 또는 기기에서 앱을 실행하고 이 버튼을 누르면 현재 앱의 메모리 상황을 보여줍니다. 다음과 같은 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.36.42.png&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnn3UO/btsL7tnsc6x/OFtDmOrtNQCBs30NeKnFdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnn3UO/btsL7tnsc6x/OFtDmOrtNQCBs30NeKnFdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnn3UO/btsL7tnsc6x/OFtDmOrtNQCBs30NeKnFdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnn3UO%2FbtsL7tnsc6x%2FOFtDmOrtNQCBs30NeKnFdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1829&quot; height=&quot;732&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.36.42.png&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실행되어있는 액티비티, 그리고 각 영역의 메모리 크기와 같은 데이터를 확인할 수 있습니다.&lt;br /&gt;앱을 사용해보면서 메모리가 비약적으로 높아지거나 회수가 안되는 지점을 확인해볼 수 있겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단에 Table 탭을 Visualization 탭으로 변경하면 다음과 같이 좀 더 시각화된 데이터를 볼 수 있는데, 굉장히 복잡해서 분석은 쉽지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.41.08.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccGLhM/btsL7x4pPyK/njznQBc7lpZKtC3PXxvG91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccGLhM/btsL7x4pPyK/njznQBc7lpZKtC3PXxvG91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccGLhM/btsL7x4pPyK/njznQBc7lpZKtC3PXxvG91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccGLhM%2FbtsL7x4pPyK%2FnjznQBc7lpZKtC3PXxvG91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1822&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2025-02-04 오후 3.41.08.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;메모리 프로파일러를 통해 앱의 메모리 사용량과 누수의 분석에 대해 알아보았습니다.&lt;br /&gt;그런데 이것으로 메모리 누수의 원인을 파악하고 제거하기는 사실 쉽지 않습니다. 좋은 방법이 없을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Leakcanary&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://square.github.io/leakcanary/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://square.github.io/leakcanary/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738651354370&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;LeakCanary&quot; data-og-description=&quot;  Documentation issue? Report or edit LeakCanary   LeakCanary is a memory leak detection library for Android. LeakCanary&amp;rsquo;s knowledge of the internals of the Android Framework gives it a unique ability to narrow down the cause of each leak, helping &quot; data-og-host=&quot;square.github.io&quot; data-og-source-url=&quot;https://square.github.io/leakcanary/&quot; data-og-url=&quot;https://square.github.io/leakcanary/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://square.github.io/leakcanary/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://square.github.io/leakcanary/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;LeakCanary&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Documentation issue? Report or edit LeakCanary   LeakCanary is a memory leak detection library for Android. LeakCanary&amp;rsquo;s knowledge of the internals of the Android Framework gives it a unique ability to narrow down the cause of each leak, helping&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;square.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 고맙게도, 메모리 누수의 탐지와 원인을 분석하기 쉽도록 오픈소스가 제공되고 있습니다. 스퀘어를 찬양합시다.&lt;br /&gt;다음과 같이 의존성 설정을 하면 디버그앱 실행 시 자동으로 동작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738651436221&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2660&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdAqhz/btsL7vyLx3x/z3H4QAAQObQqbeFBBgH581/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdAqhz/btsL7vyLx3x/z3H4QAAQObQqbeFBBgH581/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdAqhz/btsL7vyLx3x/z3H4QAAQObQqbeFBBgH581/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdAqhz%2FbtsL7vyLx3x%2Fz3H4QAAQObQqbeFBBgH581%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;519&quot; height=&quot;494&quot; data-origin-width=&quot;2660&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에 메모리 누수가 있다면 Leakcanary가 이를 탐지하고 분석하여 알림을 줍니다. 알림을 통해 위와 같은 화면을 진입할 수 있으며 각 항목에 대한 로그를 세세하게 보여줍니다. 우리는 이 로그를 따라 의심되는 지점을 찾아 메모리 누수를 제거할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Out Of Memory 핸들링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 메모리 누수를 제거했다 하더라도 사용자의 기기, 앱 사용량, 앱 기능 등에 의해 메모리는 얼마든지 터져나갈 수 있습니다.&lt;br /&gt;사용자는 다수의 앱을 실행하고 있기 때문에 다른 앱에 의해 메모리가 가득차게 되면 프로세스 우선순위에 따라 우리의 앱이 사용할 메모리가 부족해져 종료시켜버리기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이런 상황을 어떻게 핸들링할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/performance/memory?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/topic/performance/memory?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738651680378&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;앱 메모리 관리 &amp;nbsp;|&amp;nbsp; App quality &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;Android용으로 개발할 때 사전에 메모리 사용량을 줄이는 방법을 알아봅니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/topic/performance/memory?hl=ko&quot; data-og-url=&quot;https://developer.android.com/topic/performance/memory?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bsA431/hyYcdeGstf/zGksNMdNqb15jwBjaX41kK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/performance/memory?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/topic/performance/memory?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bsA431/hyYcdeGstf/zGksNMdNqb15jwBjaX41kK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;앱 메모리 관리 &amp;nbsp;|&amp;nbsp; App quality &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android용으로 개발할 때 사전에 메모리 사용량을 줄이는 방법을 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 메모리 관리를 위해 onTrimMemory를 통해 메모리의 문제에 대한 알림을 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738651809416&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyApp : Application() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        // do something
    }
}

class MyActivity : AppCompatActivity() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        // do something
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션 클래스 또는 액티비티 등에서 onTrimMemory를 오버라이드 할 수 있고 이에 따라 핸들링할 수 있습니다.&lt;br /&gt;여기서 level은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_BACKGROUND&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_COMPLETED (SDK 35에서 deprecated)&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_MODERATE (SDK 35에서 deprecated)&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL (SDK 35에서 deprecated)&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW (SDK 35에서 deprecated)&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE&lt;/li&gt;
&lt;li&gt;ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 레벨에 따라 메모리 위험도가 다릅니다. API 35 이후부터 많은 필드가 deprecated 되었습니다.&lt;br /&gt;이에 더해 다음과 같이 직접 메모리에 대한 정보를 제공받을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738652248666&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo -&amp;gt;
        activityManager.getMemoryInfo(memoryInfo)
    }
}

fun doSomethingMemoryIntensive() {
    val isLowMemory = getAvailableMemory().lowMemory
    // do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지를 조합해서 앱의 메모리가 어떠한지 확인하고 적절한 행동을 취할 수 있겠습니다.&lt;br /&gt;예를들어 메모리나 적다면 리소스를 제거한다거나, 사용자에게 알림을 주거나, 앱을 종료시키거나 등이 있겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Thread.setDefaultUncaughtExceptionHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 앱에서 예외를 아무리 처리해도 놓치는 경우 또는 OOM의 발생은 앱의 크래쉬로 이어집니다.&lt;br /&gt;이 크래쉬를 막을 순 없지만, 오류를 수집하거나 앱의 재실행 등은 가능할 수 있습니다. 예를들어 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738652425454&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Thread.setDefaultUncaughtExceptionHandler { thread, e -&amp;gt;
            if (e is OutOfMemoryError) {
                handleOOM()
            } else {
                handleUnexpectedException(e)
            }
        }
    }

    private fun handleOOM() {
        // do something
    }
    
    private fun handleUnexpectedException(e: Throwable) {
        // do something
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 OOM 또는 놓친 예외에 대한 최후의 처리를 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 성능과 품질, 안정성을 위해서 메모리 관리는 늘 신경쓰고 작업해야하는 부분입니다. 그러나 개발하면서 신경써도 틈새가 생기기 마련이기 때문에, 이를 적절히 검출하고 처리하는 방법을 알고 있는 것이 좋습니다.&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>gc</category>
      <category>JVM</category>
      <category>leakcanry</category>
      <category>OOM</category>
      <category>Profiler</category>
      <category>메모리</category>
      <category>메모리관리</category>
      <category>메모리누수</category>
      <category>안드로이드</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/176</guid>
      <comments>https://doitddo.tistory.com/176#entry176comment</comments>
      <pubDate>Tue, 4 Feb 2025 16:01:57 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Jetpack Compose - Layout으로 커스텀 레이아웃 만들기</title>
      <link>https://doitddo.tistory.com/175</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중에 커스텀 레이아웃이 필요했습니다.&lt;br /&gt;스크롤이 가능한 Column에서 staggered layout이 필요했는데, LazyVerticalStaggeredGrid을 사용하기엔 중첩 스크롤 문제가 발생했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이러한 상황에서 사용 가능한 staggered layout을 구현하기로 했습니다.&lt;br /&gt;이를 위해 커스텀 레이아웃을 구현하기 위해 기본적으로 이해가 필요한 부분이 있었기 때문에 이에 대해 포스팅합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layout&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;Compose에서 기본적으로 제공하고 있는 Scaffold, Surface, Column, Row 등은 Layout를 기본적으로 사용하여 구현되어 있습니다.&lt;br /&gt;다음은 하나의 예로, Column 컴포저블 함수의 코드입니다.&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1738503602665&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -&amp;gt; Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 문서의 Jetpack Compose &amp;gt; Custom layouts에서도 Layout 컴포저블을 사용하여 커스텀 레이아웃을 구현하는 방법을 가이드하고 있습니다.&lt;br /&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/layouts/custom&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/compose/layouts/custom&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738503747766&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;맞춤 레이아웃 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 레이아웃 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서 UI 요소는 호출될 때 UI 요소&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/ui/compose/layouts/custom&quot; data-og-url=&quot;https://developer.android.com/develop/ui/compose/layouts/custom?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/I6EUx/hyYccGFsCs/NV9A0oAyTZIkBjiJKIMKKK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/FXZlX/hyX7SpBe4J/aFshSMU3Zck6Y8kEEhmzo1/img.png?width=560&amp;amp;height=449&amp;amp;face=0_0_560_449,https://scrap.kakaocdn.net/dn/q2aSv/hyYb7ZFd1E/N8QnkLohnxcJwsNRtsGN01/img.png?width=301&amp;amp;height=383&amp;amp;face=0_0_301_383&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/layouts/custom&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/ui/compose/layouts/custom&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/I6EUx/hyYccGFsCs/NV9A0oAyTZIkBjiJKIMKKK/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/FXZlX/hyX7SpBe4J/aFshSMU3Zck6Y8kEEhmzo1/img.png?width=560&amp;amp;height=449&amp;amp;face=0_0_560_449,https://scrap.kakaocdn.net/dn/q2aSv/hyYb7ZFd1E/N8QnkLohnxcJwsNRtsGN01/img.png?width=301&amp;amp;height=383&amp;amp;face=0_0_301_383');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;맞춤 레이아웃 &amp;nbsp;|&amp;nbsp; Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 레이아웃 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서 UI 요소는 호출될 때 UI 요소&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리는 커스텀 레이아웃을 구현하기 위해 Layout 컴포저블을 이해할 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout 컴포저블의 코드는 다음과 같습니다.  다른 오버로딩된 함수들도 있지만, 이 함수로 동작하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738503774438&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UiComposable
@Composable
inline fun Layout(
    content: @Composable @UiComposable () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val compositeKeyHash = currentCompositeKeyHash
    val localMap = currentComposer.currentCompositionLocalMap
    val materialized = currentComposer.materialize(modifier)
    ReusableComposeNode&amp;lt;ComposeUiNode, Applier&amp;lt;Any&amp;gt;&amp;gt;(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            @OptIn(ExperimentalComposeUiApi::class)
            set(compositeKeyHash, SetCompositeKeyHash)
            set(materialized, SetModifier)
        },
        content = content
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부의 코드는 잠시 잊읍시다. 중요한 것은 MeasurePolicy와 content 함수를 우리가 구현해야 된다는 것 입니다.&lt;br /&gt;content는 직관적으로 Layout에 그려질 컴포저블 함수임을 이해할 수 있습니다.&lt;br /&gt;MeasurePolicy에 대해 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MeasurePolicy&lt;/h2&gt;
&lt;pre id=&quot;code_1738504092328&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun interface MeasurePolicy {
    fun MeasureScope.measure(
        measurables: List&amp;lt;Measurable&amp;gt;,
        constraints: Constraints
    ): MeasureResult
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MeasurePolicy는 레이아웃의 측정과 동작을 정의하는 인터페이스입니다.&lt;br /&gt;MeasureScope 에서 레이아웃에 그려질 컴포저블의 크기나 위치 등을 측정하여 결과를 반환하는 measure 함수가 존재합니다.&lt;br /&gt;실제 인터페이스에는 intrinsic 사이즈 측정과 관련된 확장함수도 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 레이아웃을 만들면 내부에 그려질 레이아웃들의 크기와 위치 등을 직접 계산하게 되므로 이 인터페이스를 구현하게 됩니다.&lt;br /&gt; Layout 컴포저블을 사용한다면, 다음과 같은 람다로 구현할 수 있습니다. (SAM)&lt;/p&gt;
&lt;pre id=&quot;code_1738504341294&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  Layout(
        modifier = modifier,
        content = content,
    ) { measurables, constraints -&amp;gt;
        val placeables = measurables.map { measurable -&amp;gt;
            measurable.measure(constraints)
        }
        
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 여러 파라미터들이 나오는데 결국 뭔가 측정 가능한 것들로 제약조건에 따라 측정후에 배치할 수 있는 요소들을 얻는 것처럼 보입니다.&lt;br /&gt;이것을 이해하기 위해 Measurable, Constraints, Placeable 에 대해서 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Measurable&lt;/h3&gt;
&lt;pre id=&quot;code_1738504586111&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Measurable : IntrinsicMeasurable {
    fun measure(constraints: Constraints): Placeable
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Measurable은 측정 가능한 컴포저블 요소를 말합니다.&lt;br /&gt;요소가 그려지기 위한 제약조건을 바탕으로 측정을 수행하고 Placeable을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Constraints&lt;/h3&gt;
&lt;pre id=&quot;code_1738504693644&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Immutable
@JvmInline
value class Constraints(
    @PublishedApi internal val value: Long
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Constraints는 컴포저블이 그려지는 레이아웃의 제약조건을 나타내는 클래스입니다. 내부에 꽤 많은 필드가 존재하는데, 레이아웃의 최소, 최대 너비와 높이와 같은 필드들을 가지고 있습니다.&lt;br /&gt;하위 컴포저블 요소들을 그려낼 때 이 제약조건 내에서 측정될 수 있도록 구현하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Placeable&lt;/h3&gt;
&lt;pre id=&quot;code_1738504904029&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class Placeable : Measured {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Placeable은 상위 레이아웃에 의해 배치될 수 있는 하위 레이아웃을 의미합니다.&lt;br /&gt;Placeable은 일반적으로 Measuable의 측정 결과에 의해 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 레이아웃 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 레이아웃을 구현하기 위해선, 다음과 같은 절차가 필요할 수 있음을 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째, Layout 컴포저블에 원하는 레이아웃에 따라 MeasurePolicy를 구현해야 합니다.&lt;br /&gt;두번째, Measureble 리스트의 항목들을 측정해서 그 결과로 Placeable 리스트를 얻습니다.&lt;br /&gt;세번째, Placeable 리스트를 레이아웃에 배치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 코드를 작성하면 다음과 같은 형태입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738505226727&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -&amp;gt; Unit
) {
    Layout(
        modifier = modifier,
        content = content,
    ) { measurables, constraints -&amp;gt;
        val placeables = measurables.map { measurable -&amp;gt;
            measurable.measure(constraints)
        }
        
        val layoutWidth = ...
        val layoutHeight = ...
        
        layout(layoutWidth, layoutHeight) {
            placeables.forEach { placeable -&amp;gt;
            	val x = ...
                val y = ...
                
                placeable.placeRelative(x, y)
            }
        }
    }   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 레이아웃의 형태에 따라 로직을 수정하는 것을 제외하면 위와 같은 틀에서 코드를 작성하게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 안드로이드 뷰에 비해 커스텀 레이아웃을 구현하는 것이 굉장히 간편해진 것 같습니다.&lt;br /&gt;어렵지 않으니 필요한 커스텀 레이아웃이 있다면 한번 구현해보면 좋겠습니다.&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>compose</category>
      <category>custom layout</category>
      <category>Jetpack Compose</category>
      <category>layout</category>
      <category>안드로이드</category>
      <category>커스텀 레이아웃</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/175</guid>
      <comments>https://doitddo.tistory.com/175#entry175comment</comments>
      <pubDate>Sun, 2 Feb 2025 23:11:50 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] JWT 사용해서 인증하기</title>
      <link>https://doitddo.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즈음 사이드 프로젝트를 하면서 Firebase를 백엔드로 활용하고 있습니다.&lt;br /&gt;Functions와 Node.js의 조합으로 백엔드를 구현하고 있고 그 외에 필요한 서비스들을 붙여가고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 개발하면 회원가입, 로그인과 같은 인증을 구현하는것이 거의 필수적이다보니 Firebase에서 제공해주는 인증 서비스를 사용하여 구현하였습니다만, 속도가 매우매우 느려서 전체적인 API 퍼포먼스가 너무 안좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Firebase의 인증을 버리고 직접 DB와 인증 토큰 처리를 구현했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JWT란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 Json Web Token의 약어로, 인터넷 표준 인증 방식입니다.&lt;br /&gt;이름을 봤을때에는 인증에 필요한 정보들이 Json 처럼 생겨있을 것 같고, 암호화된 Token 형식일 것 같이 느껴지죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 {Header}.{Payload}.{Signature} 의 구조로, 세가지 요소가 점(.)으로 구분되어 있는 형태입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Header&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header는 토큰의 타입과 해시에 사용된 알고리즘 정보 등의 정보를 담는 영역으로 base64로 인코딩되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738401268456&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;typ&quot;: &quot;JWT&quot;,
    &quot;alg&quot;: &quot;HS512&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Header가 위와 같다면 토큰의 타입은 JWT, 해시 알고리즘은 HS512 입니다.&lt;br /&gt;여기서 해시 알고리즘이 필요한 이유는 JWT는 토큰의 위변조 방지를 위한 서명이 포함되어 있는데, 서명을 암호화하기 위해 해시 알고리즘이 필요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Payload&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰에서 사용할 정보의 조각들인 Claim이 담겨있습니다.&lt;br /&gt;즉, JWT를 통해 실제로 알 수 있는 데이터를 뜻하며, 서버와 클라이언트간에 주고받는 사용자 정보입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738401557017&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;sub&quot;: &quot;12345678&quot;,
    &quot;userId&quot;: &quot;abcdefg&quot;,
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Payload는 base64로 인코딩되어 있으며, 누구나 읽을 수 있는 정보이기 때문에 민감한 정보를 삽입하면 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클레임 중에는 토큰을 위해 미리 정의된 등록된 클레임(Registered Claim) 들이 존재합니다.&lt;br /&gt;예를 들면 iss(토큰 발급자), sub(토큰 제목), aud(토큰 대상자), exp(토큰 만료 시간) 등이 있습니다.&lt;br /&gt;Payload에 어떤 데이터를 삽입할 때 이 클레임들과 중복되지 않도록 합시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Signature&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그니처(서명)은 토큰의 유효성을 검증할 때 사용하는 암호화 코드입니다.&lt;br /&gt;시그니처는 Header와 Payload를 Base64로 각각 인코딩한 뒤 점(.) 으로 잇습니다.&lt;br /&gt;그리고 그 값을 암호화 알고리즘으로 해싱하는데, 이 때 서버의 비밀키를 사용합니다.&lt;br /&gt;마지막으로 해싱된 값을 Base64로 인코딩한 값이 Signature가 됩니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Node.js에서 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 고맙게도 Node.js에서 JWT를 편하게 사용할 수 있는 패키지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/jsonwebtoken&quot;&gt;https://www.npmjs.com/package/jsonwebtoken&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738401945282&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;jsonwebtoken&quot; data-og-description=&quot;JSON Web Token implementation (symmetric and asymmetric). Latest version: 9.0.2, last published: a year ago. Start using jsonwebtoken in your project by running &amp;#96;npm i jsonwebtoken&amp;#96;. There are 32516 other projects in the npm registry using jsonwebtoken.&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/jsonwebtoken&quot; data-og-url=&quot;https://www.npmjs.com/package/jsonwebtoken&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n83Ab/hyX73RVu5D/3CwqMfcsDug2cOOj2KkU0k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/jsonwebtoken&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/jsonwebtoken&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n83Ab/hyX73RVu5D/3CwqMfcsDug2cOOj2KkU0k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;jsonwebtoken&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSON Web Token implementation (symmetric and asymmetric). Latest version: 9.0.2, last published: a year ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 32516 other projects in the npm registry using jsonwebtoken.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;패키지 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 npm을 사용하여 jsonwebtoken 패키지를 설치해줍니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt; npm install jsonwebtoken&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;암호화에 사용할 키 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 암호화에 사용할 비밀키를 정의합시다.&lt;br /&gt;샘플 또는 사이드 프로젝트에서는 온라인에서 제공되는 제네레이터를 사용하는데, 불안한다면 다른 방법을 써도 됩니다.&lt;br /&gt;회사와 같은 곳이라면 서버에서 직접 생성하는 것이 옳습니다.&lt;br /&gt;&lt;a href=&quot;https://jwtsecret.com/generate&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jwtsecret.com/generate&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738402088453&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JwtSecret.com - Generate JWT Secrets Online&quot; data-og-description=&quot;Full secret is hidden for security.&quot; data-og-host=&quot;jwtsecret.com&quot; data-og-source-url=&quot;https://jwtsecret.com/generate&quot; data-og-url=&quot;https://jwtsecret.com/generate&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://jwtsecret.com/generate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jwtsecret.com/generate&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JwtSecret.com - Generate JWT Secrets Online&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Full secret is hidden for security.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jwtsecret.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 키를 dotenv 등을 사용해서 불러오도록 하면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토큰 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1738402235763&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const jwt = require('jsonwebtoken');

// 토큰 만료시간을 적절하게 설정하세요. ex) &quot;60&quot;, &quot;2 days&quot;, &quot;10h&quot;, &quot;7d&quot;
const TOKEN_EXPIRATION = &quot;2h&quot;;
const SECRET_KEY = process.env.SECRET_KEY;

function sign(value) {
    const token = jwt.sign({ field: value }, SECRET_KEY, { expiresIn: TOKEN_EXPIRATION });
    return token;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰은 sign 함수로 생성합니다.&lt;br /&gt;sign 함수에 필요한 정보들을 json 형태로 전달하고, 비밀키, 토큰 만료시간 같은 것들을 전달하면 토큰이 생성됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;토큰 인증&lt;/h4&gt;
&lt;pre id=&quot;code_1738402367107&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jwt.verify(token, secretKey, (err, decoded) =&amp;gt; {
    if (err) {
        // 에러 처리
    } else {
        // 성공 처리
        // decoded가 Payload를 의미합니다.
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 인증은 verify 함수로 합니다.&lt;br /&gt;검증할 토큰과 비밀키를 전달하면 에러 또는 복원된 Payload 값을 콜백해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 직접 서버와 클라이언트의 인증을 JWT를 통해 구현하면 관리 비용은 늘어나겠지만, Firebase 인증과 같은 서비스를 사용함으로써 발생하는 성능 저하를 개선할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실제 서비스에서 어플리케이션을 개발한다면 사용자 관리와 인증을 대부분 직접 개발하게 되므로 JWT를 이해하고 있다면 큰 이점이 될 수 있습니다.&lt;/p&gt;</description>
      <category>개발/Backend</category>
      <category>firebase</category>
      <category>json web token</category>
      <category>jwt</category>
      <category>node.js</category>
      <category>인증</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/174</guid>
      <comments>https://doitddo.tistory.com/174#entry174comment</comments>
      <pubDate>Sat, 1 Feb 2025 18:37:14 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin Coroutines, 에러 처리와 SupervisorJob</title>
      <link>https://doitddo.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘날 Kotlin 기반의 안드로이드 앱을 개발하고 있다면 Coroutines를 사용하고 있을 것입니다.&lt;br /&gt;그렇다면 십중팔구 Jetpack ViewModel에서 제공하는 ViewModelScope를 사용하고 있을텐데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModelScope를 생성하는 코드를 보면 SupervisorJob이 등장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738077766458&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package androidx.lifecycle.viewmodel.internal

internal fun createViewModelScope(): CloseableCoroutineScope {
    val dispatcher = try {
        Dispatchers.Main.immediate
    } catch (_: NotImplementedError) {
        EmptyCoroutineContext
    } catch (_: IllegalStateException) {
        EmptyCoroutineContext
    }
    return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob은 무엇이고 왜 ViewModelScope에서 기본적인 Job으로 사용하고 있을까요?&lt;br /&gt;코루틴의 에러처리 관점에서 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴에서 에러처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅(&lt;a href=&quot;https://doitddo.tistory.com/172&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://doitddo.tistory.com/172&lt;/a&gt;)에서, 코루틴은 구조화된 동시성 원칙을 따르고 있기 때문에 자식 코루틴에서 에러가 발생하면 부모 코루틴까지 전파가 되어 전체 코루틴이 취소됨을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 우리는 자식 코루틴의 에러로 인해 전체 코루틴이 취소되지 않도록 에러 처리를 해 주어야 합니다.&lt;br /&gt;가장 쉬운 방법은 try-catch를 사용하는 방법이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738078053582&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CoroutineScope(context).launch {
    launch {
        try {
            // do something
        } catch (e: Exception) {
            // Handle error
        }
        
        launch {
            try {
                // do something
            } catch(e: Exception) {
                // Handle error
            }
        }
    }
    
    launch {
        try {
            // do something
        } catch(e: Exception) {
            // Handle error
        }
    }
    
    // do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 코드는 모든 코루틴에서 try-catch가 반복됩니다. 그리고 코루틴이 취소되었을 때 발생하는 CancellationException 까지 잡아버리기 때문에 코루틴의 취소가 정상적으로 이루어지지 않게 됩니다. 따라서 CancellationException은 별도로 처리하는 코드가 반복이 되어 버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 에러 처리는 되겠지만 반복 코드의 발생으로 인해 나이스한 방향이 아닐 것이라 생각할 것입니다.&lt;br /&gt;자 그렇다면 CoroutineExceptionHandler를 사용하면 어떨까요?&lt;/p&gt;
&lt;pre id=&quot;code_1738078529526&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val exceptionHandler = CoroutineExceptionHandler { context, throwable -&amp;gt; 
    ...
}
CoroutineScope(context + exceptionHandler).launch {
    launch {
        // do something
        
        launch {
            // do something
        }
    }
    
    launch {
        // do something
    }

    // do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, 에러가 발생하면 CoroutineExceptionHandler가 에러를 캐치하지만 코루틴이 에러로 인해 취소되는 것을 막을수는 없습니다.&lt;br /&gt;자식 코루틴에서 발생한 에러가 부모로 전파되는 것은 막지 못하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 에러처리를 하는 것이 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SupervisorJob&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴의 에러로 인해 부모 코루틴이 취소되고 결과적으로 전체 코루틴이 취소되는 근본적인 이유는, 자식 코루틴의 에러가 부모에게 전파되기 때문입니다.&lt;br /&gt;그렇다면 자식 코루틴의 에러를 부모에게 전파하지 않는다면 문제가 해결이 될 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 관점에서 사용할 수 있는 것이 SupervisorJob입니다.&lt;br /&gt;SupervisorJob은 자식 코루틴에서 에러가 발생해도 부모 코루틴으로 전파하지 않도록 막습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738079023342&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SupervisorJob을 생성하는 함수를 호출하면 내부적으로 SupervisorJobImpl을 반환합니다.&lt;br /&gt;그리고 SupervisorJobImpl은 JobImpl의 childCancelled를 오버라이드하여 false를 반환하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738079133864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {

    ...
    
    private fun cancelParent(cause: Throwable): Boolean {
        ...
        return parent.childCancelled(cause) || isCancellation
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 코드를 타고 들어가다보면 JobSupport 클래스에 도달하게 되고, cancelParent 함수를 만날 수 있습니다.&lt;br /&gt;이 함수의 마지막 줄에는 자신의 부모 Job의 childCancelled를 예외와 함께 호출합니다.&lt;br /&gt;SupervisorJob은 childCancelled가 false를 반환하도록 오버라이드 되어있기 때문에, 취소되지 않은 코루틴이라면 함수의 결과가 항상 false입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The method that is invoked when the job is cancelled to possibly propagate cancellation to the parent. Returns true if the parent is responsible for handling the exception, false otherwise. Invariant: never returns false for instances of CancellationException, otherwise such exception may leak to the CoroutineExceptionHandler.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancelParent 함수는 위와 같은 주석이 작성되어 있습니다.&lt;br /&gt;주석에서 알 수 있듯이 cancelParent 함수를 통해 코루틴에서 오류가 발생했을 때 코루틴을 취소하면서 부모에게 취소를 전파하기 위해 호출하는 함수입니다.&lt;br /&gt;따라서 SupervisorJob은 이 함수의 false이므로 부모에게 취소가 전파되지 않음을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModelScope는 부모-자식 코루틴 간의 에러 전파와 취소를 막기 위해 SupervisorJob을 사용하고 있음을 알 수 있었습니다.&lt;br /&gt;만약 다른 코루틴 스코프를 생성해서 사용한다면, 코루틴의 에러 처리에 대한 내용을 이해하고 SupervisorJob을 활용하면 더욱 견고한 코루틴을 작성할 수 있을 것입니다.&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>Coroutines</category>
      <category>Job</category>
      <category>supervisorjob</category>
      <category>ViewModel</category>
      <category>안드로이드</category>
      <category>에러처리</category>
      <category>에러핸들링</category>
      <category>코루틴</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/173</guid>
      <comments>https://doitddo.tistory.com/173#entry173comment</comments>
      <pubDate>Wed, 29 Jan 2025 00:50:57 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin Coroutines, 구조화된 동시성 (Structed Concurrency)</title>
      <link>https://doitddo.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘날, 안드로이드 개발은 Kotlin이 메인 프로그래밍 언어로 자리잡고 있습니다.&lt;br /&gt;이로 인해 비동기 작업은 Kotlin Coroutines(코루틴)이 완전히 자리를 잡고 있는데요.&lt;br /&gt;일부 역사가 긴 서비스를 보유한 회사에서는 Rx 계열을 사용하는 경우도 있지만, 아마 코루틴으로 마이그레이션하는 작업을 진행하지 않을까 예상됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 구조화된 동시성이라는 원칙을 따르고 있습니다.&lt;br /&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737526724104&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Coroutines basics | Kotlin&quot; data-og-description=&quot; &quot; data-og-host=&quot;kotlinlang.org&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency&quot; data-og-url=&quot;https://kotlinlang.org/docs/coroutines-basics.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4PIFn/hyX4v1ObAz/4LqaFtFMkyBKc5bqfUtUt0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4PIFn/hyX4v1ObAz/4LqaFtFMkyBKc5bqfUtUt0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Coroutines basics | Kotlin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 동시성은 동시성 작업간에 부모-자식과 같은 관계를 형성하여 가독성, 유지보수성, 안정성이 뛰어난 동시성 코드를 작성할 수 있게 해주는 접근 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 코루틴은 어떻게 구조화된 동시성을 제공할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineScope 알아보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 실행하기 위해 우리는 CoroutineScope가 필요합니다.&lt;br /&gt;Android에서 개발을 하고 있다면 ViewModelScope, LifecycleScope와 같은 것들을 일반적으로 사용하고 있을텐데, 이들은 모두 CoroutineScope 인터페이스를 구현한 하나의 구현체입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737949761318&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
    ContextScope(coroutineContext + context)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kotlinx.coroutines 패키지의 CoroutineScope에서 확인할 수 있듯이, CoroutineScope는 CoroutineContext 속성을 가지는 인터페이스입니다.&lt;br /&gt;그런데 파일을 보면 CoroutineContext를 합쳐 ContextScope를 만들어내는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;plus 오퍼레이터를 구현한 것을 볼 수 있습니다.&lt;br /&gt;CoroutineContext는 무엇일까요?&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CoroutineContext 알아보기&lt;/h2&gt;
&lt;pre id=&quot;code_1737950087383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlin.coroutines

@SinceKotlin(&quot;1.3&quot;)
public interface CoroutineContext {
    public operator fun &amp;lt;E : Element&amp;gt; get(key: Key&amp;lt;E&amp;gt;): E?
    public fun &amp;lt;R&amp;gt; fold(initial: R, operation: (R, Element) -&amp;gt; R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
    public fun minusKey(key: Key&amp;lt;*&amp;gt;): CoroutineContext
    
    public interface Key&amp;lt;E : Element&amp;gt;
    public interface Element : CoroutineContext {
    public val key: Key&amp;lt;*&amp;gt;
        public override operator fun &amp;lt;E : Element&amp;gt; get(key: Key&amp;lt;E&amp;gt;): E? = ...
        public override fun &amp;lt;R&amp;gt; fold(initial: R, operation: (R, Element) -&amp;gt; R): R = ...
        public override fun minusKey(key: Key&amp;lt;*&amp;gt;): CoroutineContext = ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext는 현재 실행되고 있는 코루틴에 대한 요소의 집합체입니다. 코루틴이 동작하는 쓰레드, 취소 처리, 디버깅 정보등과 같은 정보를 담고 있습니다.&lt;br /&gt;이러한 코루틴에 대한 요소들은 Element라는 인터페이스로 구현이 되며, Element는 또 다시 CoroutineContext를 구현하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코루틴은 CoroutineContext들이 엮이는 구조로 이해할 수 있습니다.&lt;br /&gt;그럼 우리가 사용하는 CoroutineContext 또는 Element는 무엇이 있는지 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineDispatcher&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 코루틴이 동작할 쓰레드를 지정하기 위해 Disaptcher를 지정합니다.&lt;br /&gt;예를들면 Dispatchers.Default, Dispatchers.IO, Dispatchers.Main, Dispatchers.Main.immediate와 같은 것들이 있습니다.&lt;br /&gt;코드를 타고 들어가면 이들은 모두 CoroutineDispatcher를 구현하고 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737950711886&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ...
}

public abstract class AbstractCoroutineContextElement(public override val key: Key&amp;lt;*&amp;gt;) : Element {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 CoroutineDispatcher는 Element 인터페이스를 구현하는 AbstractCoroutineContextElement 추상 클래스를 상속하고 있음을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 CoroutineDispatcher는 CoroutineConext가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Job&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job은 코루틴의 실행 결과로 반환되는 객체입니다. (정확히는 Job 인터페이스의 구현체를 반환합니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1737952263705&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key&amp;lt;Job&amp;gt;
    
    @ExperimentalCoroutinesApi
    public val parent: Job?
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean
    public fun start(): Boolean
    public fun cancel(cause: CancellationException? = null)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job은 CoroutineContext.Element를 구현하고 있고, CoroutineContext.Element는 CoroutineContext를 구현하고 있는 구조이기 때문에 Job은 결과적으로 CoroutineContext 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CoroutineExceptionHandler&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 예기치 못한 예외처리를 위해 CoroutineExceptionHandler를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737953064781&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key&amp;lt;CoroutineExceptionHandler&amp;gt;
    public fun handleException(context: CoroutineContext, exception: Throwable)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler는 CoroutineContext.Element를 구현하고 있고, CoroutineContext.Element는 CoroutineContext를 구현하고 있는 구조이기 때문에 CoroutineExceptionHandler는 결과적으로 CoroutineContext 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineName&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineName은 실행중인 코루틴의 이름을 지정하는 클래스입니다.&lt;br /&gt;코루틴의 동작 자체에 영향을 주지는 않고 디버깅 시에 활용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737953125541&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package kotlinx.coroutines

public data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    public companion object Key : CoroutineContext.Key&amp;lt;CoroutineName&amp;gt;
    override fun toString(): String = &quot;CoroutineName($name)&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineName은 AbstractCoroutineContextElement를 구현하고 있습니다.&lt;br /&gt;AbstractCoroutineContextElement는&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CoroutineContext.Element를 구현하고 있고, CoroutineContext.Element는 CoroutineContext를 구현하고 있는 구조이기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;CoroutineExceptionHandler는 결과적으로 CoroutineContext 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; 구조화된 동시성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext는 plus 오퍼레이터를 구현하고 있기 때문에,  요소들을 합쳐 CoroutineContext를 만들수 있습니다.&lt;br /&gt;그리고 원하는 CoroutineScope에서 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737953844217&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val job = Job()
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -&amp;gt; ... }
val dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate
val name: CoroutineName = CoroutineName(&quot;MyCoroutineName&quot;)
val context: CoroutineContext = job + exceptionHandler + dispatcher + name

CoroutineScope(context).launch {
    // Do something
}
viewModelScope.launch(context) {
    // Do something
}
lifecycleScope.launch(context) {
    // Do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 실행한 코루틴 내에서 또 다른 코루틴을 실행하면 어떻게 될까요?&lt;/p&gt;
&lt;pre id=&quot;code_1737953911028&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&amp;gt; Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope는 확장함수로 launch를 구현하고 있습니다. 즉,&amp;nbsp; CoroutineScope 내에서는 얼마든지 새로운 코루틴을 실행할 수 있음을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수에 디폴트로 전달되는 CoroutineConext가 EmptyCoroutineContext인 것을 볼 수 있습니다.&lt;br /&gt;이것은 CoroutineConext를 구현하는 싱글턴 객체(object) 입니다.&lt;br /&gt;왜 EmptyCoroutineContext가 디폴트로 전달될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 CoroutineConext의 plus 오퍼레이터의 구현입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737954130410&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface CoroutineContext {
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element -&amp;gt;
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
            
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 현재의 CoroutineContext에 새로운 CoroutineContext를 더하려고 할 때, 그 대상이 EmptyCoroutineContext이면 fold 동작을 시키지 않고 바로 자기 자신을 반환하는 것을 볼 수 있습니다.&lt;br /&gt;fold 내부에서도 루프 중 EmptyCoroutineContext를 만났을 때는 예외 처리가 되어있는 것을 볼 수가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 plus 함수는 현재 CoroutineContext와 전달된 CoroutineContext의 요소들을 모두 포함하는 CoroutineContext를 반환하며, 중복되는 요소는 제거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 결과 덕분에 상위 CoroutineContext에서 launch를 통해 하위 CoroutineContext를 실행하면 특별한 EmptyCoroutineContext를 전달하지 않는 한 상위 CoroutineContext를 따르게 되는 하위 코루틴이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성된 하위 코루틴은 Job을 반환하게 되고, 이 Job이 상위 코루틴과 참조를 가지게 됩니다.&lt;br /&gt;코루틴은 트리구조와 같은 상위-하위, 또는 부모-자식 구조를 형성하게 되며, 다음과 같은 특징을 가지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째, 상위(또는 부모) 코루틴은 하위(또는 자식) 코루틴이 완료될 때 까지 코루틴을 종료하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737954776898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1(부모)
CoroutineScope(context).launch {
    // 2(1의 자식)
    launch {
        // do something 2
        
        // 2-1(2의 자식)
        launch {
            // do something 3
        }
    }
    
    // 3(1의 자식)
    launch {
        // do something 4
    }
    
    // do something 5
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 위와 같은 코루틴이 있다고 해 봅시다.&lt;br /&gt;1(부모) 코루틴은 자신의 작업을 실행하고 2, 3자식 코루틴이 완료될 때 까지 기다립니다.&lt;br /&gt;2(1의 자식) 코루틴은 2-1(2의 자식) 코루틴이 완료될 때 까지 기다립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째, 상위(또는 부모) 코루틴이 취소되면 하위 코루틴은 모두 취소됩니다.&lt;br /&gt;첫번째 예시를 동일하게 가정하겠습니다.&lt;br /&gt;1(부모) 코루틴이 취소되면 2(1의 자식), 3(1의 자식) 코루틴이 취소됩니다.&lt;br /&gt;2(1의 자식) 코루틴이 취소되면 그의 자식인 2-1(2의 자식) 코루틴도 취소됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번째, 하위 코루틴이 취소되더라도 부모 코루틴은 취소되지 않습니다.&lt;br /&gt;첫번째 예시를 동일하게 가정하겠습니다.&lt;br /&gt;2(1의 자식) 또는 3(1의 자식) 코루틴이 취소되더라도 1(부모) 코루틴은 취소되지 않습니다.&lt;br /&gt;2-1(2의 자식) 코루틴이 취소되더라도 2(1의 자식) 코루틴은 취소되지 않습니다.&lt;br /&gt;이러한 특징 덕분에 어떤 하나의 코루틴이 취소되어도 부모, 또 다른 자식 코루틴들은 동작할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 이런 특징들로 하여 구조화된 동시성을 제공하고 있습니다.&lt;br /&gt;우리는 비동기 코드들을 마치 동기적인 것 처럼 위에서 아래로 작성할 수 있습니다. 이 덕에 읽기 쉽고 얼마든지 코루틴을 추가하고 제거할 수 있죠.&lt;br /&gt;또한 하위 코루틴이 취소되더라도 상위 코루틴은 취소되지 않는 특징 덕분에 특별한 예외처리 없이도 안정적으로 비동기 코드를 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴의 예외 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명했던 코루틴의 특징 중 하나는 자식 코루틴이 취소되어도 부모 코루틴이 취소되지 않는다는 것이 있었습니다.&lt;br /&gt;그럼 자식 코루틴이 &quot;예외&quot;를 발생시켜도 동일하게 동작할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면 다음과 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737955336874&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1(부모)
CoroutineScope(context).launch {
    // 2(1의 자식)
    launch {
        // do something 2
        
        // 2-1(2의 자식)
        launch {
            throw Exception(&quot;are parent coroutines safe?&quot;)
        }
    }
    
    // 3(1의 자식)
    launch {
        // do something 4
    }
    
    // do something 5
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안타깝게도, 위 코루틴은 부모 코루틴까지 취소되어 나머지&amp;nbsp; 상황이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 취소는 Cancellationexception을 발생시키는데, 이것은 코루틴 내부적으로 예외처리가 되어 있습니다.&lt;br /&gt;그리고 코루틴은 이 예외를 감지하여 취소처리를 하게 됩니다.&lt;br /&gt;그렇기 때문에 코루틴이 에러 상황에 빠지지 않고 취소임을 인지하고 작업을 이어나갈 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 외의 예외는 코루틴에서 에러가 발생했음을 의미하고 이것을 상위로 전파하게 됩니다.&lt;br /&gt;그렇기 때문에 부모 코루틴도 에러가 발생하게 되어 코루틴이 취소가 되고, 부모 코루틴이 취소됨에 따라 하위 코루틴도 모두 취소가 되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황을 방지하기 위한 것으로 SupervisorJob을 사용할 수 있는데, 이것에 대한 포스팅은 다음으로 이어가겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발/Android</category>
      <category>Android</category>
      <category>coroutine</category>
      <category>Kotlin</category>
      <category>structed concurrency</category>
      <category>구조화된 동시성</category>
      <category>동시성</category>
      <category>비동기</category>
      <category>안드로이드</category>
      <category>코루틴</category>
      <category>코틀린</category>
      <author>du.it.ddu</author>
      <guid isPermaLink="true">https://doitddo.tistory.com/172</guid>
      <comments>https://doitddo.tistory.com/172#entry172comment</comments>
      <pubDate>Mon, 27 Jan 2025 14:29:13 +0900</pubDate>
    </item>
  </channel>
</rss>