[Kotlin] 프로퍼티와 Backing Field
Kotlin 프로퍼티의 backing field 개념과 field 키워드의 역할을 정리하고, 커스텀 getter에서 field를 참조하지 않아 발생한 실전 버그 사례를 분석한다.
1. 프로퍼티란 — Java 필드와의 차이
Java
1 | public class Person { |
Kotlin
1 | class Person { |
핵심
Kotlin의var name은 겉보기엔 필드 같지만 실제로는 getter/setter를 포함한 접근 단위다.p.name은 직접 필드 접근이 아니라 함수 호출이다.
2. 프로퍼티의 3가지 구성요소
컴파일러는 var name: String = ""를 이렇게 풀어쓴다.
1 | var name: String = "" |
| 구성요소 | 역할 | 접근 방법 |
|---|---|---|
backing field (field) |
실제 값이 저장되는 숨겨진 메모리 공간 | 접근자 내부에서만 field 키워드로 |
getter (get()) |
값을 반환. 생략 시 get() = field 자동 생성 |
p.name (읽기) |
setter (set(value)) |
값을 저장. 생략 시 set(v) { field = v } 자동 생성 |
p.name = "x" (쓰기) |
구조도
1 | 외부 코드 |
3. field 키워드
field는 프로퍼티 접근자 내부에서만 쓸 수 있는 특별한 식별자. 자기 자신의 backing field를 가리킨다.
1 | var count: Int = 0 |
왜 field가 필요한가 — 재귀 함정
1 | var count: Int = 0 |
count는 프로퍼티명이고, 읽으면 getter가 호출되고, 그 getter가 또 count를 읽고… 끝없이 반복.
→ 그래서 Kotlin은 backing field에 직접 접근하는 field라는 예약어를 따로 둠.
4. Backing Field는 언제 생기는가
참고: 모든 프로퍼티가 backing field를 갖는 것은 아니다.
컴파일러는 접근자에서field참조가 있을 때만 backing field를 생성한다.
생성되는 경우
1 | var name: String = "" // 기본 setter/getter가 field 사용 → 생성 |
생성되지 않는 경우 (Computed Property)
1 | val fullName: String |
5. 실전 사례 — volume 버그 분석
Before
1 | override var volume: Float = 1.0f |
문제 상황
- setter는
field를 쓰므로 backing field는 생성됨- getter는
field를 읽지 않음 → 저장된 값이 바깥으로 나올 방법이 없음field는 쓰기만 하고 읽히지 않는 dead storage
재현 시나리오
1 | mediator.volume = 0f // setter → field = 0f, player.volume = 0f |
재초기화 경로에서의 실제 버그
1 | playerBucket = ExoPlayerPool.acquire(context).also { bucket -> |
→ mute 상태로 재생되어야 할 영상이 갑자기 소리를 냄 (UX 사고 가능).
After
1 | override var volume: Float = 1.0f |
해결
커스텀 getter를 제거하면 기본 getter가field를 반환. setter가 저장한 값이 정확히 읽힌다. 재초기화 시volumegetter가field(0f)를 반환 → mute 유지 ✅
Setter 순서 변경 이유
1 | // Before |
기능적으로는 동일하지만 “저장(field)이 전파(player)보다 먼저” 라는 불변식 유지. player.volume 쓰기가 동기 콜백을 발동시키고 그 콜백이 getter를 읽는다면, 옛 순서에서는 field가 아직 이전 값을 반환할 수 있음. 엣지 케이스 방어.
6. Backing Field vs Backing Property
주의: 이름은 비슷하지만 다른 개념이다.
Backing field: 언어가 자동 생성
Backing property: 개발자가 수동 생성
Backing Property 패턴 — StateFlow 예시
1 | class ViewModel { |
uiState(외부용)가 _uiState(내부용)를 뒤에서 받치고(backing) 있어서 backing property라고 부름.
비교
| Backing Field | Backing Property | |
|---|---|---|
| 누가 만드나 | Kotlin 컴파일러 자동 | 개발자가 수동 |
| 접근 방법 | 접근자 안에서 field 키워드 |
클래스 안에서 _uiState 같은 변수명 |
| 용도 | 프로퍼티의 실제 저장소 | 가변성/타입을 외부에 가리기 |
| 개수 | 프로퍼티당 0개 또는 1개 | 쌍(private + public)으로 존재 |
7. Q&A
Q. Kotlin 프로퍼티와 Java 필드의 차이는?
Java 필드는 순수 메모리 저장소이고, 접근하려면 getter/setter를 별도로 작성해야 한다. Kotlin 프로퍼티는 저장소 + getter + setter를 하나의 선언에 묶은 언어 수준의 추상화. p.name 구문은 필드 직접 접근처럼 보이지만 실제로는 접근자 호출로 컴파일된다.
Q.
field키워드는 왜 필요한가?
접근자 안에서 자기 자신의 프로퍼티명을 쓰면 재귀 호출이 되기 때문. field는 재귀 없이 backing field에 직접 접근하기 위한 특별한 식별자로, 접근자 안에서만 유효하다.
Q. Backing field가 없는 프로퍼티도 있나?
있다. getter/setter가 field를 전혀 참조하지 않으면 컴파일러는 backing field를 만들지 않는다. 계산된 프로퍼티(computed property)가 대표적.
1 | val isEmpty: Boolean |
Q. Backing field와 backing property의 차이는?
Backing field는 언어가 자동 생성하는 숨겨진 저장 공간(field 키워드로 접근). Backing property는 개발자가 수동으로 만드는 private 프로퍼티로 public 프로퍼티의 가변성을 숨기는 패턴. 예: _uiState: MutableStateFlow → uiState: StateFlow.
Q. 커스텀 getter를 만들 때 주의할 점은?
field를 써야 할 때 쓰지 않으면, 저장된 값을 읽을 방법이 사라져var의 반쪽이 죽은 코드가 된다 (volume 버그 사례).- getter는 호출될 때마다 실행되므로, 비싼 연산을 넣으면 성능 문제가 된다.
- 외부에서
p.name을 읽는 건 함수 호출이므로, 연달아 읽을 때 값이 바뀔 수 있음을 가정해야 한다 (특히 스레드 관점).
8. 핵심 요약
1 | 프로퍼티 = (backing field?) + getter + setter(var인 경우) |
핵심 감각: “getter 호출은 함수 호출이다”를 체화하면 대부분의 혼동이 풀린다.
관련 링크
[Kotlin] 프로퍼티와 Backing Field
https://june0122.github.io/2026/04/15/kotlin-property-backing-field/