- 
          
          [FCM] FCM으로 notification 구현 - 5 (클라이언트쪽 구현2)Android 2025. 10. 27. 18:45728x90반응형https://developer-hh.tistory.com/60 이 글에 이어서... 
 앱 내에서 스크린 이동을 하기 위해 여러 방법을 찾아보았고 일단 내가 이해할 수 있는 방법은 이 두 가지였다 - DeepLink - intent에서 추출한 주소값을 compose navigation의 navController에 사용해서 이동 하지만 딥링크는 광고나 이벤트 같은 걸 할 때 쓴다고 알고 있었기 때문에 맞지 않는 거 같았다 두 번째 방법은 우리가 compose에서 아래와 같이 사용하는 navigation controller를 사용하는 것이다 val navController = rememberNavController()하지만 MainActivity에서 navController를 사용수는 없다... 왜냐하면 rememberNavController는 컴포저블 함수 내에서만 사용이 가능하기 때문이다... 그래서 처음에는 intent에 주소값이 있다면 setContent를 다르게 주고 이동하는 방식으로 해결하려고 했다 하지만 그렇게 되면 모든 UI를 새로 다시 다 만들고 이동하게 된다 알림이 백만개라고 치면 하나하나의 알림을 터치할 때마다 ui를 매번 새로 만든다면 엄청난 리소스 낭비가 발생할 것이다... 그 다음으로는 메인액티비티에 변수를 하나 만들고 이동할 주소값을 저장해서 넘겨주는 것도 해보았는데 일단 변수를 메인액티비티에서 관리하는 것도 별로라고 생각했고, navController가 생성되기 전에 이동을 자꾸 하려고 해서 제대로 된 스크린으로 이동하지 않았다 그리고 마지막으로 성공한 방법은 compose의 rememberNavController가 생성된 이후에 이동시키는 것이었다 약간 리액트에서 전역상태쓰듯이 compose의 rememberNavController가 생성된 후에 전역상태에 할당하고 이 상태가 null이 아니면 이동하는 식으로 하면 어떨까하는 생각이었다 일단 다른 곳에서도 쓰일 수 있게 NavigationManager라는 object를 만들었다 그리고 rememberNavController()는 NavHostController를 리턴하기에 타입을 NavHostController?로 주었다 또한 intent에서 오는 route 값도 선언해주었다 object NavigationManager { private var navController: NavHostController? = null private var pendingRoute: AppRoutes? = null/** * navHostController 등록 및 pending route 처리 * * NavHostController 변수에 저장 후 호출함 * NavController 등록 후 pendingRoute가 있다면 해당 화면으로 이동함 * * @param controller 등록할 NavHostController */ fun setNavController(controller: NavHostController) { navController = controller // pendingRoute가 있다면 실행 후 null로 초기화 pendingRoute?.let { route -> navigateTo(route) pendingRoute = null } }처음에 navController가 생성되어있으면 NavigationManager의 navController에 할당한 후에 pendingRoute가 있는 경우 이동하게 하였다 /** * 지정된 route로 이동함 * @param route 이동할 화면의 route * * NavController가 사용 가능한 경우 이동, 아니라면(앱 시작, 백그라운드 상태 등) pending 처리해서 NavController 등록할 때 실행됨 * */ fun navigateTo(route: AppRoutes) { navController?.let { controller -> controller.navigate(route) { launchSingleTop = true } } ?: run { pendingRoute = route } }그리고 route가 있고 navController가 null이 아닌 경우 이동할 수 있게 하였고 아닌 경우 이동해야할 라우트(pendingRoute)에 intent로부터 들어온 route 값을 넣어주었다 근데 이러한 IDE에서 이런 경고를 줬다  android context class를 static field에 두지 마라~(Context를 가리키는 context 필드를 가지고 있는 NavHostController에 대한 정적 참조) 이건 메모리 누수다~ 라고 한다   여러가지 뒤져보니까 context를 static field에 할당하면 계속 참조되기때문에 메모리 누수가 일어난다는 말인 거 같았다 그러니까 내가 만든 NavgationManager의 navController가 NavHostController를 참조하는 경우에 NavHostController는 NavController를 상속하고 이 때 context를 NavController의 생성자로 넘겨준다 
 NavController 내부에는 navContext가 있고, 이 navContext가 context 필드를 가지고 사용하고 있는데,
 이 context 필드는 NavHostController에서 넘겨준 context(현재 Application의 context)를 참조하고 있는 거 같다(틀리다면 알려주세요...)쉽게 보자면 object NavigationManager의 변수 navController -> NavHostController -> NavController -> navContext -> context -> 현재 Application의 context(아마도 MainActivity) 이렇게 참조하는 관계가 만들어지는 것이다 참조하는 건 알겠는데 이게 무슨 문제냐고 할 수 있다 쉽게 말하자면 가비지 컬렉터가 계속 어디선가 참조되는 객체를 수거하지 않고, 이 때문에 수거되지 못한 객체가 메모리에 남아 메모리 누수를 일으킬 수 있다는 거였다 더보기가비지 컬렉터란? 더이상 참조되고 있지 않은 객체들을 알아서 정리해주는 기능 우리가 사용하는 객체들(ex: Activity, View 등) 혹은 직접 만든 클래스 등은 Heap 메모리에 있음 가비지 컬렉터는 참조되고 있지 않은 객체들을 Heap 메모리에서 알아서 자동으로 정리해주는 역할을 함 만약 앱을 종료한다고 쳐보자 compose의 remember가 종료되면서 rememberNavController가 정리된다 하지만 내가 만든 NavigationManager는 object이기 때문에 앱이 살아있는 동안 계속 존재하고 object NavigationManager는 계속 NavHostController를 참조하고 있고, 결국 MainActivity의 context를 계속 간접적으로 참조하고 있다고 볼 수 있다 만약 앱을 종료한다면 가비지 컬렉터가 MainActivity의 참조관계를 확인한 후, 참조하고 있는 곳이 있다면 가비지 컬렉트를 진행하지 않고(메모리에 계속 남아있음), 참조하고 있는 것이 없다면 가비지 컬렉팅을 한다(메모리 반환) 앱을 종료할 때 MainActivity를 참조하는 NavController가 삭제되어야하고 이를 삭제하려면 NavHostController가 삭제되어야 한다(NavController도 참조하고 있는게 없어야 삭제되기 때문에) 그런데 내가 만든 NavigationManager는 object로 선언되어있기 때문에 앱이 종료될 때까지 남아있다. NavHostController는 앱이 종료될 때까지 계속 메모리에 있기 때문에 참조 카운트가 0이 되지 못한다(계속 참조되고 있음). 따라서 가비지 컬렉터는 MainActivity를 메모리에서 수거하지 못한다 즉, MainActivity가 종료되었다고 해도 메모리에 계속 남아있기 때문에 메모리 누수가 발생하는 것이다 
 이후 앱을 다시 실행할 때마다 새롭게 MainActivity가 생성되고 수거되지 못한 이전 MainActivity들은 메모리에 계속 남아있아 쌓일 것이다 결국 불필요하게 메모리 사용량이 늘어나서 앱이 무거워질 것이다근데 난 쓰고 싶은데; 메모리 누수를 막으면서도 쓸 수 있는 방법은 뭘까 찾아보다가 WeakReference라는 개념을 알게 되었다 https://kotlinlang.org/docs/native-memory-manager.html#monitor-gc-performance https://appmaster.io/ko/blog/kotlin-memori-gwanri-mic-gabiji-sujib#yaghan-camjo-hwalyong https://medium.com/@dev.leehyeonbin/kotlin-weak-reference-de50f0f0d3a6 https://developer.android.com/reference/java/lang/ref/WeakReference https://medium.com/@mahmoudelfoulyyy/understanding-strong-and-weak-references-in-kotlin-c28b164b2d7e https://proandroiddev.com/memory-leaks-in-android-a-guide-for-android-developers-448fa86ced27 
 참조하는 방법에는 4가지가 있다- strong reference - soft reference - weak reference - phantom reference 더보기1. Strong Reference 우리가 일반적으로 객체를 참조해서 쓰는 방법이다 ``` class Color(val name: String) { 
 fun getColor() {
 println(" $this color is $name")
 }
 }
 fun main() {
 var red: Color? = Color("red")
 red?.getColor() // Color@4fca772d color is red
 }``` 객체가 강하게 참조되는 경우 가비지 컬렉팅 대상이 되지 않지만 잘 관리 하지 않으면 메모리 누수가 일어날 수도 있음 nullable하게 처리해서 참조를 사용하지 않을때는 null로 할당해 추후에 가비지 컬렉터가 수거할 수 있도록 할 수 있다 2. Weak Reference 참조하는 객체에 강한 참조가 남아있지 않은 경우 가비지 컬렉터에 의해서 수집될 수 있도록 함 ``` // 참조하고 있는 객체에 강한 참조가 남아있는 경우 import kotlin.system.* 
 import java.lang.ref.WeakReference
 class Color(val name: String) {
 fun getColor() {
 println(" $this color is $name")
 }
 }
 fun main() {
 var strongRed: Color? = Color("red")
 val weakRed = WeakReference(strongRed)
 System.gc() // 강한 참조는 여전히 존재하고 가비지 컬렉터만 실행
 
 println(weakRed.get()?.name) // 출력 : "red"
 }// 참조하고 있는 객체에 강한 참조가 남아있지 않은 경우 class Color(val name: String) { 
 fun getColor() {
 println(" $this color is $name")
 }
 }
 fun main() {
 var strongRed: Color? = Color("red")
 val weakRed = WeakReference(strongRed)strongRed = null 
 System.gc() // stongRed에 null을 할당하고 가비지 컬렉터 실행
 
 println(weakRed.get()?.name) // 출력 : null
 }``` 3. Soft Reference weak reference와 비슷하지만 JVM이 메모리가 부족하다고 판단할 때만 가비지 컬렉팅이 됨 (outOfMemoryError 전까지) ex: 캐시 구현 등에 사용 ``` import kotlin.system.* 
 import java.lang.ref.WeakReference
 import java.lang.ref.SoftReference
 class Color(val name: String) {
 fun getColor() {
 println(" $this color is $name")
 }
 }
 fun main() {
 var strongRed: Color? = Color("red")
 val softRed = SoftReference(strongRed)
 strongRed = null // null로 할당 -> 가비지 컬렉터가 수거가능하도록 함
 println(softRed.get()?.name) // 출력 : "red"
 System.gc() // 가비지 컬렉터 실행
 
 println(softRed.get()?.name) // 출력 : "red"
 }
 ```4. Phantom Reference 객체가 사라진 것을 확인하고 추가 작업을 한다고 하는데 잘 이해가 안 가서 좀 더 공부가 필요하다... https://proandroiddev.com/memory-leaks-in-android-a-guide-for-android-developers-448fa86ced27 이 글을 참조해서 보았을 때 나의 경우 내부적으로 context를 참조(위 링크 - 사용예시 3번)하고 있기 때문에 WeakReference를 사용해서 약한 참조를 사용했다 
 /** * NavHostController가 생성되면 스크린을 이동할 수 있음 * */ object NavigationManager { private var navController: WeakReference<NavHostController>? = null private var pendingRoute: AppRoutes? = null /** * 지정된 route로 이동함 * @param route 이동할 화면의 route * * NavController가 사용 가능한 경우 이동, 아니라면(앱 시작, 백그라운드 상태 등) pending 처리해서 NavController 등록할 떄 실행됨 * */ fun navigateTo(route: AppRoutes) { navController?.get()?.let { controller -> controller.navigate(route) { launchSingleTop = true } } ?: run { pendingRoute = route } } /** * navHostController 등록 및 pending route 처리 * * NavHostController 변수에 저장 후 호출함 * NavController 등록 후 pendingRoute가 있다면 해당 화면으로 이동함 * * @param controller 등록할 NavHostController */ fun setNavController(controller: NavHostController) { navController = WeakReference(controller) // pendingRoute가 있다면 실행 후 null 할당 pendingRoute?.let { route -> navigateTo(route) pendingRoute = null } } /** * navController 객체를 계속 참조하지 않도록 해제 * */ fun releaseNavController() { navController?.clear() navController = null pendingRoute = null } }그래서 나는 WeakReference를 사용했다 setNavController(controller: NavHostController)로 NavHostController에 대한 약한 참조를 걸고 pendingRoute(intent로 들어온 이동할 스크린 주소)가 있다면 NavigateTo()를 통해서 이동할 수 있게 했다 navigateTo(route: AppRoutes)에서는 navHostController가 null이 아니라면 get()해서 생성된 객체를 가져온 후 해당되는 route로navigate 하도록 했다 만약 아직 생성이 되지 않았다면 pendingRoute에 이동할 스크린의 주소값을 저장하고 이동할 수 있게 했다 그리고 자원해제하는 부분(releaseNavController())에서 navController.clear와 navController = null 이렇게 두 개를 해주었다 차이점은 clear()의 경우 WeakReference의 내부 참조를 해체하는 역할을 한다 private var navController: WeakReference<NavHostController>? = null이렇게 되어있는데 WeakReference가 내부적으로 참조하는 NavHostController를 해체하겠다는 것이다 그리고 navController = null의 부분은 아예 WeakReference 참조까지 해체하겠다는 뜻이다 @Composable fun AppNavigation() { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination fun onBackClick() { navController.navigateUp() } LaunchedEffect(navController) { NavigationManager.setNavController(navController) } // 이하 생략 }저렇게 하고나서 네비게이션 그래프들이 모여있는 AppNavigation(진입점)에서 LaunchedEffect로 rememberNavController의 NavHostController를 키값으로 두고 NavigationManager.setNavController()를 통해서 compose의 rememberNavController로 생성된 NavHostController를 NavigationManager로 넘겨주고 원하는 스크린으로 이동할 수 있게 해주었다 @OptIn(ExperimentalMaterial3Api::class) @Composable fun NotificationListScreen( onNavigationUp: () -> Unit, viewModel: NotificationViewModel = hiltViewModel() ) { // 생략 fun handleNavigationRoute(route: String) { val route = RouteParser.getRoute(route) NavigationManager.navigateTo(route) } // 이하 생략 }알림 리스트 스크린에서도 따로 rememberNavController를 작성하지 않고 NavigationManager를 통해 이동할 수 있게 했다 
 맞는 방법인지는 잘 모르겠다 그래도 시간이 지난 후에 보면 또 다른 좋은 방법을 찾아낼 수 있지 않을까 싶다 그리고 구현하면서 가비지 컬렉터도 조금이나마 공부할 수 있게 되었고 가비지 컬렉터가 참조하는 타입도 여러가지가 있다는 걸 알게 되었다(팬텀 레퍼런스는 더 찾아봐야할듯 너무 어렵다;) 아직도 배울게 산더미구나하는 생각이 든다 - 참고 https://d2.naver.com/helloworld/329631 https://kotlinlang.org/docs/native-memory-manager.html#monitor-gc-performance 
 https://appmaster.io/ko/blog/kotlin-memori-gwanri-mic-gabiji-sujib#yaghan-camjo-hwalyonghttps://medium.com/@dev.leehyeonbin/kotlin-weak-reference-de50f0f0d3a6 https://developer.android.com/reference/java/lang/ref/WeakReference https://medium.com/@mahmoudelfoulyyy/understanding-strong-and-weak-references-in-kotlin-c28b164b2d7e https://proandroiddev.com/memory-leaks-in-android-a-guide-for-android-developers-448fa86ced27 반응형'Android' 카테고리의 다른 글[Android] ZonedDateTime, LocalDateTime, Instant의 차이 (0) 2025.10.29 [FCM] FCM으로 notification 구현 - 4 (클라이언트쪽 구현1) (0) 2025.09.27 [FCM] FCM으로 notification 구현하기 - 3(Firebase Functions로 functions 구현) (0) 2025.09.25 [FCM] FCM으로 Notification 구현하기 - 1 (0) 2025.09.23 [Jetpack Compose] 텍스트의 일부분 스타일 변경하기 (2) 2025.08.06