주완 님의 블로그
Android - jetpack navigation 사용시 중첩 네비게이션에 대하여 본문
동아리 과제 진행중에 질문이 들어와 해당 내용에 대해 답변을 작성하며 공부한 내용을 적어봤습니다.
Jetpack navigation을 사용해서, login, signup, home, mypage 화면 전환에 적용하는 과정이었습니다.

// ============ Navigation Graph Markers ============
@Serializable
data object AuthGraph
@Serializable
data object MainGraph
// ============ Route Hierarchy ============
// Interface: navigation에 직접 사용 X
sealed interface Route
sealed interface Auth : Route
sealed interface Main : Route
// ============ Concrete Routes ============
@Serializable
data object Login : Auth
@Serializable
data object SignUp : Auth
@Serializable
data object HomePage : Main
@Serializable
data object SearchPage : Main
@Serializable
data object MyPage : Main
@Composable
fun AppNavHost(navController: NavHostController, dataStore: DataStore<Preferences>,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = AuthGraph,
modifier = modifier
) {
authGraph(
navController = navController,
dataStore = dataStore,
snackbarHostState = snackbarHostState
)
composable<HomePage> {
Text("This is HomePage")
}
composable<SearchPage> {
Text("This is SearchPage")
}
composable<MyPage> {
MainScreen(
viewModel = MainViewModel(userRepository = UserRepository(dataStore = dataStore))
)
}
}
}
Navigator 에 대한 class 를 위와 같이 정의해주셨습니다.
일단 NavGraph와 NavHost에 대한 중첩 네비게이션은 잘 작성해주신 상태 였는데
파트원분께서 궁금하신 것은 “상속받은 class 에서 serialize 가 잘 안된다는 이슈가 있었다” 였습니다.
저희 동아리에서 배운 jetpack navigation 내용을 생각해보면,"Route는 직렬화된 데이터면 가능하다! "라고 배웠습니다.
현재는 Login이라는 객체를 직렬화해서 사용하고 있고, 그렇기 때문에 Navgraph.composable<Login> 이런식으로 사용할 수 있었습니다.
저도 질문에 대해서 생각을 하다가 잘 모르겠어서 이런 저런 블로그들을 보면서 공부를 하게 되었는데,
좀 찾아보니, 저희가 jetpack compose 에서 사용하고 있는 composable<T> 는 컴파일러와 Kotlinx Serialization 플러그인의 도움을 받아 동작한다고 합니다.
그렇게 된다면 NavType + Route Encoding 을 통해 경로를 변환하는 구조로 이루어진다는데, 저도 직렬화에 대한 내부 동작 방식은 잘 모르기에 블로그를 한 번 참고해 봤습니다.
[Kotlinx serialization] Json 직렬화/역직렬화 -Polymorphism #5
블로그에서는
abstract class Project 예제에서 발생한 SerializationException ( "Class 'OwnedProject' is not registered for polymorphic serialization in the scope of 'Project'. Mark the base class as 'sealed' or register the serializer explicitly." )는 상속 관계에서 부모가 자식의 목록을 명시적으로 알지 못할 때 발생합니다. 라고 합니다.
그럼 Navigation 에 적용해 봤을 때 Login 객체가 Auth 인터페이스를 상속받았을 때, kotlinx.serialization은 이 계층 구조를 처리하려고 시도하지만, 부모인 Auth가 sealed로 명확하게 한정되어 있지 않거나 (인터페이스로 작성했기 때문에), 자식 클래스의 목록을 알 수 없어 다형성 직렬화에 실패할 위험이 매우 큽니다.
그래서, 제시한 방법이
다형성을 갖는 계층구조의 serialization을 위한 적절한 방법은 부모를 sealed class로 만들고 자식들은 모두 @Serializable을 붙이는 방법입니다.
sealed class는 Closed Polymorphism (닫힌 다형성)을 보장하여, 컴파일 타임에 모든 자식 클래스(Login, SignUp)의 목록이 확정됩니다. kotlinx.serialization은 이 목록을 기반으로 안전하고 효율적인 Serializer를 생성하여 SerializationException 발생을 원천적으로 차단합니다.
또한, 블로그에 보시면 직렬화 결과를 출력한 것이 있는데
"type":"example.examplePoly07.EmptyResponse"와 같이 객체의 타입 식별자가 포함되는 것을 확인할 수 있습니다.
이 "type" 식별자는 기본적으로 객체의 FQN을 사용하거나, @SerialName으로 별칭을 부여한 이름입니다.Navigation 적용해 본다면 Navigation Compose는 파라미터가 없는 Route의 경우,
이 FQN 자체를 NavGraph에 등록하는 Route Key로 사용하고, 파라미터가 있는 Route는 이 FQN을 경로의 템플릿으로 사용합니다.
이 경우, 타입 을 나타내는 제네릭에 @Serializable 이 붙어 있으면 내부적으로 해당 타입의 FQN - 패키지명을 포함한 전체 이름) 을 기반으로 고유한 경로 문자열을 자동으로 생성한다고 하네요
그렇다면 현재 작성 해주신 코드를 한 번 봐보자면
sealed class Route
sealed interface Auth: Route
@Serializable
data class Login : Auth
이렇게 되어 있는것을 볼 수 있습니다.
그렇다면 앞에 말한 컴파일러의 입장에서 봤을 때, data object Login : Auth 를 컴파일 할때는, 상속에 대한 타입 시스템의 속성으로 기록 할 뿐, 객체에 대한 고유 식별자에는 “Auth”가 경로에 포함되지 않습니다.
그렇다면 Login에 대한 경로는 com.sopt.dive.navigator.Login이 되는 것인데
이렇게 된다면, Navigation 입장에서는, Login이 무엇을 상속받은 건지(Auth의 자식인지) 알 방법이 없기 때문에, Login이라는 고유한 키를 목적지로 인식하게 됩니다. 그렇기때문에 원하신
“composable<Auth.Login>” → 이런 방식으로 NavGraph를 설정할 수 없는것이죠
그렇다면, 어떤 방식으로 작성 했을 때 , 중첩 그래프 사용시 직관적으로 경로를 나타낼 수 있을 까요?
Type Safe Nested Navigation in Jetpack Compose
Login과 SignUp을 Auth 컨테이너에 넣고 싶어한 분의… ? 블로그를 찾아와 봤습니다. 여기서는

이러한 Auth( Email Login → Email Signup) 에 대한 플로우를 구상할 때
@Serializable
sealed class Screens {
@Serializable
object Auth {
@Serializable
object EmailSignUp
@Serializable
object EmailLogIn
}
@Serializable
object App {
@Serializable
data class Home(
val name: String = "default",
)
}
}
private fun NavGraphBuilder.authGraph(
navController: NavHostController,
) {
navigation<Screens.Auth>(
startDestination = Screens.Auth.EmailLogIn
) {
composable<Screens.Auth.EmailLogIn> {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
이렇게 중첩 그래프를 사용했다고 합니다.
상속 관계에 대한 처리와 달리 해당 구조는 Kotlin이 내부적으로 객체의 이름을 처리하는 방식에 있다고 합니다.
object, class 를 중첩으로 정의할때 Kotlin이 해당 객체의 고유식별자에 외부 객체의 이름을 포함시키기 때문인데,
// 이렇게 사용하게 된다면
sealed class Route { // 1. 외부 클래스
sealed class Auth : Route() { // 2. 내부 클래스
@Serializable
data object Login : Auth() // 3. 최종 Destination
}
// ...
}
// Login 객체에 대해 FQN (아까본 com.sopt.dive.navigator.Login과 같은) 를 생성할 때
// 오부 클래스인 Route 와 Auth의 이름을 포함하게 됩니다.
// 그렇다면 아까와는 다르게 com.sopt.dive.navigator.Route.Auth.Login 이렇게 FQN 이 작성되며, 해당 객체를 Route.Auth.Login으로 참조할 수 있는 것이죠
결론은 , Kotlin은 상속받는 객체의 FQN에는 부모 타입의 이름을 포함시키지 않지만, 내부에 중첩된 객체의 FQN에는 외부 객체의 이름을 경로처럼 포함시키기 때문에, 중첩을 사용하여 라우팅 시 원하는 계층적 경로를 문자열로 얻을 수 있는 것입니다.
해당 내용을 기반으로, 파트원분의 코드를 수정해 본다면
sealed class Route {
@Serializable
data object AuthGraph
sealed class Auth : Route() {
@Serializable
data object Login : Auth()
@Serializable
data object SignUp : Auth()
}
@Serializable
data object MainGraph
sealed class Main : Route() {
@Serializable
data object HomePage : Main()
@Serializable
data object SearchPage : Main()
@Serializable
data object MyPage : Main()
}
}
이런식으로 사용할 수 있을 것 같습니다.
Navigation Compose는 @Serializable 타입을 Route Key로 사용하지만,
polymorphic 계층 구조는 지원하지 않기 때문에,
sealed class 중첩 구조를 사용하면 가장 직관적이고 안전한 타입 세이프 라우팅이 된다.
블로그에서 제시한 코드는 모든 계층에 Serializable 을 붙이고, 아래에서는 왜 route 쪽에만 붙이는가는kotlinx serialization polymorphic 에 대해서 공부를 좀 더 해봐야 될 것 같습니다. 오버헤드나 안정성 측면에서 전자보다는 후자가 좋다 생각하는데, 다 공부해보려하니까 양이 아주 많더라구요 ㅎㅎ…
'Android' 카테고리의 다른 글
| Hilt 의 정의와 사용법 (0) | 2025.12.10 |
|---|---|
| Android에서 Domain Layer와 UseCase: 언제 필요하고 언제 생략할까? (0) | 2025.12.03 |
| android- 이미지 라이브러리 Coil (1) | 2025.11.07 |
| Android - Paging 라이브러리 (0) | 2025.11.05 |
| 회원가입 플로우 아키텍처 분석: 다중 vs 단일 ViewModel , Navigation구조 (3) | 2025.08.27 |