Android 개발/Kotlin

Kotlin 2.3.20 name-based destructuring, 안드 코드 7곳에 끼워본 일주일

stackD 2026. 6. 1. 16:00

 

지난 주 금요일 밤, PR 리뷰하다가 User 클래스 필드 순서가 바뀐 걸 봤어요. 구조분해 쓰는 자리가 17곳. 등에서 식은땀이 흐르더군요.

 

email 과 name 이 둘 다 String 이라 컴파일러는 입도 뻥긋 안 하고, 회원가입 화면엔 사용자 이름 자리에 이메일이 박혀 나갈 뻔했습니다. 그 자리에서 바로 코틀린(Kotlin) 2.3.20에 Experimental로 들어온 이름 기반 구조분해(name-based destructuring, KEEP-0438) 가 떠올랐어요. 한 주 동안 안드로이드 프로젝트 7곳에 직접 끼워본 기록을 정리했습니다.

 

Kotlin 2.3.20 name-based destructuring 문법

문법은 이렇게 나뉩니다.

data class User(val email: String, val name: String)

// 1) 기존 위치 기반 (괄호)
val (email, name) = user

// 2) 신규: 이름 기반 (순서 무관, 각 변수에 val)
(val name, val email) = user

// 3) 이름 기반 + 별칭(rename) — (val 새이름 = 필드명) 방향
(val displayName = name, val email) = user

 

여기서 한 가지 더 챙겨야 할 게, 이름 기반을 켜면 위치 기반을 위한 새 대괄호 문법이 같이 들어온다는 점이에요. Pair·Triple·List처럼 순서가 곧 의미인 자리는 이 대괄호로 명시합니다.

val point = Pair(10, 20)

// 위치 기반 신규 문법 (대괄호)
val [x, y] = point

 

활성화 옵션은 셋입니다.

  • only-syntax: 새 명시 문법만 추가로 허용하고, 기존 val (a, b) 의 동작은 바꾸지 않아요.
  • name-mismatch: 기존 위치 기반 분해에서 변수명이 실제 필드명과 어긋나면 경고를 띄워줍니다. 마이그레이션 도우미 성격이에요.
  • complete: 기존 괄호 단축형 val (a, b) 까지 통째로 이름 기반으로 해석하고, 위치 기반은 대괄호로 넘깁니다.

일단은 only-syntax 로 들이미는 게 안전하다고 봅니다. 큰 코드베이스라면 complete 로 넘어가기 전에 name-mismatch 로 한 번 훑어 경고부터 정리하는 흐름을 권합니다. 컴파일 결과는 위치 기반과 거의 같은 바이트코드(사용처에서 getter 직접 호출, 데이터 클래스의 componentN() 생성은 그대로) 라 런타임 오버헤드는 없는 것으로 알려져 있어요.

 

 

필드 순서 한 줄로 터진 조용한 버그

문제의 PR은 단순했습니다. data class User(val email: String, val name: String) 에서 두 필드 위치가 뒤바뀐 거였어요. 그런데 이걸 분해하는 코드 17곳 중 val (name, email) = user 로 쓴 자리들이 전부 의미가 뒤집혔거든요. 타입이 같으니 컴파일러는 침묵, 테스트도 mock 데이터로 통과, 리뷰어도 못 잡고 지나가는 그림입니다.

 

이름 기반으로 바꾸면 컴파일러가 필드명을 직접 검증합니다.

// 필드 순서가 어떻게 바뀌어도 의미가 깨지지 않습니다
(val name, val email) = user

 

name 은 user.name, email 은 user.email. 데이터 클래스의 필드 순서를 뒤집어도, 새 필드를 끼워 넣어도 분해 코드가 다칠 일이 없는 자리입니다. 컴파일러가 못 잡던 버그가 컴파일 에러로 전환되는 지점이에요.

ViewModel·Repository·Compose 분해 자리

첫 번째는 ViewModel의 UI State입니다.

data class UiState(
    val isLoading: Boolean,
    val items: List<Item>,
    val error: Throwable?,
)

// in Composable
val state by viewModel.uiState.collectAsStateWithLifecycle()
(val isLoading, val items, val error) = state

 

UiState 에 필드를 하나 추가해도 기존 분해 코드가 그대로 살아남습니다. 두 번째는 Repository 결과 분해. 캐시·원본을 같이 돌려주는 모양에선 별칭이 잘 먹혀요.

data class Loaded<T>(val data: T, val source: Source)

(val cached = data, val origin = source) = repo.fetchUser(id)

 

data 라는 이름이 호출부 문맥에선 너무 일반적이라, 별칭으로 cached 라고 다시 박아주면 코드가 한 호흡에 읽힙니다.

세 번째는 Jetpack Compose remember 인데요, 여기는 솔직히 호불호가 갈리더라고요.

// 권장 (Compose 관용구)
var text by remember { mutableStateOf("") }

// destructuring 을 굳이 보여야 한다면, 비권장임을 명시
// (참고용) val (value, setValue) = remember { mutableStateOf("") }

 

개인적으로 짧은 컴포저블 안에선 기존 문법이 손에 잘 붙어서 굳이 바꾸지 않았습니다. 반대로 같은 함수에 MutableState 가 4~5개 쌓이는 자리는, 위치 기반으로 가면 state1.value / state2.value 가 시각적으로 너무 닮아서 헷갈리거든요. 이럴 땐 이름 기반으로 가는 게 훨씬 덜 헷갈리는 것 같아요.

 

 

Room·Retrofit·Pair 이름 기반 적용 사례

네 번째는 Room @Query 결과. 조인해서 컬럼이 7~8개씩 따라 나오는 DTO는, 컬럼 한 줄 추가될 때마다 분해 자리에서 사고가 나는 경우가 많거든요. 이름 기반으로 바꿔두면 SELECT 절 순서가 어떻게 바뀌어도 무관합니다.

 

다섯 번째는 Retrofit DTO → 도메인 모델 매핑. 백엔드가 응답 JSON 필드 순서를 살짝 바꾸는 것만으로도 위치 기반 분해는 박살 나는 자리였습니다. 이름 기반은 백엔드 스키마 변경에 한 단계 더 견고해요.

 

여섯 번째는 Pair·Triple. 가볍게 묶어 쓸 때 first/second 만으로는 의미가 휘발됩니다. 다만 Pair·Triple 자체는 필드명이 first/second 라 이름 기반의 이점이 적으니, 순서가 의미인 이 자리는 새 대괄호 문법(val [a, b])으로 두거나, 아래처럼 의미 있는 도메인 타입으로 바꿔주는 게 낫습니다.

 

// 의미 있는 도메인 타입에서 이점이 살아남
data class ScoreRow(val userId: String, val score: Int)
(val userId, val score) = repo.topScore()

 

일곱 번째는 테스트 픽스처. createTestData() 가 여러 객체를 한꺼번에 돌려줄 때 (val user, val expectedState) = createTestData() 식으로 풀면, 테스트가 무엇을 검증하는지 첫 줄에서 바로 보입니다.

 

Kotlin 2.5 / 2.7 기본값 전환 전 체크리스트

도입 전에 챙겨두실 게 몇 가지 있어요.

  1. IDE 지원은 아직 들쭉날쭉합니다. 구버전 IntelliJ 에선 유효한 문법에도 빨간 줄이 그어질 수 있어요. @Suppress 로 임시 처리하시면 됩니다.
  2. Java 상호운용은 여전히 componentN() 순서에 묶여 있습니다. 라이브러리 공개 API의 필드 순서는 함부로 바꾸지 않는 게 안전한 것 같습니다.
  3. 코틀린 2.5.0(2026년 말)에서 Stable 진입, 2.7.0(2027년 말)부터는 val (a, b) = x 의 기본 의미가 이름 기반으로 전환될 예정이라는 얘기입니다. 즉, 큰 코드베이스라면 2.7 전환 전까지 마이그레이션 가이드를 팀에 공유해두시는 게 좋겠어요.
  4. 컨벤션은 "필드 3개 이상 + 같은 타입 2개 이상" 일 때만 이름 기반으로 갑니다. 좌표 Offset(x, y) 처럼 위치가 곧 의미인 자리는 대괄호 위치 기반으로 두는 게 자연스러워요.

저는 그 17곳짜리 PR을 결국 이름 기반으로 한 번 더 갈아엎고 머지했습니다. diff 줄 수는 늘었지만, 다음에 누군가 필드 순서를 다시 만진다고 해도 컴파일러가 먼저 손들고 막아줄 거란 안심이 그제야 들더라고요.

 

 

결국 그날 밤 식은땀의 정체는, "위치" 라는 가장 약한 약속에 17곳을 묶어둔 제 손이었던 셈입니다.