
얼마 전에 후배가 _state 가 왜 붙어있냐고 묻길래 한참 설명했거든요. 끝나고 코틀린(Kotlin) 2.4 릴리스 노트를 보니, 그 설명이 통째로 필요 없어지는 문법이 들어와 있었습니다.
기존 _state / state 패턴, 두 줄이 거슬렸던 이유
ViewModel 한 번 까보면 이런 두 줄이 화면 절반을 채우는 경우가 많아요.
private val _state = MutableStateFlow<UiState>(UiState.Idle)
val state: StateFlow<UiState> = _state.asStateFlow()
프로퍼티가 하나면 모르겠는데, query·result·loading·error 같이 네다섯 개 깔리면 ViewModel 윗단이 선언부로만 20줄 가까이 됩니다. 정작 비즈니스 로직은 그 밑에서 시작하지요.
_ 접두사는 공식 코딩 컨벤션에서도 백킹 프로퍼티 관례로 언급되긴 하지만, 언어 차원에서 강제되는 문법은 아니라 결국 팀 규칙에 가깝다고 봐요. 후배에게 "외부에 노출하지 않을 가변 백킹 필드를 구분하는 표시야" 라고 설명했는데, 듣고 나서도 "그럼 왜 언어가 직접 안 막아주냐"는 표정이더라고요. 사실 저도 처음 잡을 때 똑같은 게 걸렸던 기억이 납니다.
asStateFlow() 도 마음에 안 드는 대목이었어요. 타입을 한 단계 좁히려고만 호출하는 의례적 코드인데, 매번 래퍼 객체가 하나 더 생깁니다. 큰 비용은 아니지만 의도가 코드 한 줄에 안 들어맞는 게 거슬렸지요.

Kotlin 2.4 field 키워드, 한 줄로 묶는 explicit backing fields
코틀린 2.4.0부터 정식으로 들어온 explicit backing fields 가 이걸 한 줄로 줄여줍니다.
val state: StateFlow<UiState>
field = MutableStateFlow<UiState>(UiState.Idle)
바깥에 보이는 타입은 콜론 뒤에 적고, 그 아래 field = 초기화식 형태로 실제 백킹 필드의 초기값을 넣어 줍니다. 타입을 명시하고 싶으면 field: 타입 = 초기화식 형태로 적어도 돼요. 클래스 내부에서는 프로퍼티명이 백킹 필드 타입으로 직접 resolve 되어, state.value = ... 같은 쓰기 작업이 그대로 동작해요. 외부에서는 선언된 StateFlow 로만 보여서 캡슐화도 깨지지 않습니다.
적용 가능한 쌍은 가변→불변 패턴 전반입니다.
MutableStateFlow↔StateFlowMutableSharedFlow↔SharedFlowMutableLiveData↔LiveDatamutableListOf()↔List<T>
ViewModel 에서 자주 까는 부분들이 거의 다 포함되는 셈입니다.

ViewModel field 적용 후기, 선언부 절반이 사라진 자리
사이드 프로젝트 ViewModel 하나를 골라서 갈아엎어봤어요. 프로퍼티 다섯 개짜리 화면이었는데, 선언부가 10줄에서 5줄로 정확히 절반이 됐습니다. 시각적으로 윗단이 가벼워지는 게 체감되더라고요.
내부에서 호출하던 _query.update { ... } 같은 코드는 query.update { ... } 로 단순 치환만 해도 그대로 컴파일됐어요. 참고로 IntelliJ 정식 지원은 Kotlin 공식 What's new 2.4 문서 기준 2026.1.4부터로 명시돼 있어서, 그 이전 버전을 쓰시는 분들은 -Xexplicit-backing-fields 컴파일러 플래그를 켜줘야 할 수 있습니다. 프로퍼티명이 백킹 필드 타입으로 바로 잡혀주니 함수 시그니처는 건드릴 일이 없었습니다.
_ 접두사가 사라지니까 코드 리뷰 댓글 한 종류가 자동으로 없어집니다. "이거 _ 빠뜨렸어요" 같은 자잘한 지적이 그동안 의외로 많았거든요. 런타임 성능보다 "내부 변경, 외부 읽기 전용" 이라는 설계 의도가 한 줄에 박힌다는 점이 제일 큰 가치라고 봅니다.
Hilt 로 주입받는 SavedStateHandle 같은 케이스도 충돌 없이 잘 돌아갔어요. 백킹 필드는 어차피 private 으로 잡혀 있어서 DI 프레임워크 쪽에서 추가로 손볼 게 없었습니다.

field 키워드 한계, var 미지원과 컴포즈 state
장점만 있는 건 아닙니다. 현재 사양상 val 전용이라 var 프로퍼티는 적용할 수 없어요. mutableStateOf() 기반으로 컴포즈(Compose) state 를 캡슐화하시는 분들은 이 문법으로 바로 옮기기 어려운 것으로 보입니다. 커스텀 getter 와도 병용이 안 되니, getter 안에서 값을 변형해서 노출하던 패턴도 대상이 아닙니다.
개인적으로는 코드베이스 전체를 한 번에 갈아엎는 건 추천드리기 쉽지 않아 보입니다. 라이브 템플릿으로 2줄 패턴 찍어내는 게 이미 빠르고, 팀원이 새 문법에 익숙해지는 시간도 들어가니까요. 저는 신규 ViewModel 부터 field 문법으로 깔고, 기존 코드는 손볼 일이 생겼을 때 같이 정리하는 쪽으로 가고 있어요. 자바(Java) 와 섞여 있는 KMP 프로젝트에서는 효과가 약한 편입니다. 자바 쪽에서 호출하는 모습은 기존 방식과 똑같이 보이거든요.

두 줄 설명이 30초로 줄어든 자리
후배에게 다시 설명했을 때, "아 그럼 그냥 field 만 쓰면 되는 거네요?" 한 줄로 끝났습니다. 두 줄 패턴을 설명할 때 5분 걸렸던 자리가 30초가 됐어요. 코드가 줄어든 것보다, 신규 합류한 사람한테 설명할 게 한 가지 줄었다는 쪽이 더 마음에 들었습니다.
'Android 개발 > Kotlin' 카테고리의 다른 글
| 직렬화 라이브러리 3개를 kotlinx.serialization 한 곳으로 정리한 후기 (0) | 2026.07.14 |
|---|---|
| 코틀린 sealed Result 패턴으로 try/catch 좀비를 잡은 3주 (0) | 2026.07.07 |
| 공유 UI 말고 공유 코어부터, KMP 도입 한 주 일지 (0) | 2026.06.24 |
| Kotlin 2.4 UUID 안정화로 안드 데이터 클래스에서 라이브러리 두 개 지운 후기 (0) | 2026.06.21 |
| 코틀린 2.4-RC 핵심 4축, 안드로이드 6년차의 사이드 프로젝트 적용 기록 (0) | 2026.06.16 |