LostCatBox

kotlin 부모-자식 속성 override시 생성주기 주의하기

Word count: 672Reading time: 4 min
2025/02/26 11 Share

참조

본문

큰일이없으면 property에다가 override 는 쓰지않는것이좋다.

open-override 의 경우 init{} 단계에서 부모 클래스가 초기화 호출후, 자식 클래스가 init{}호출될텐데, 그럼 override된 필드는 부모 클래스의 초기화단계에서는 Null이다.

이것은 코틀린에서 상속과 프로퍼티 오버라이딩과 관련된 중요한 차이점입니다. 오버라이드를 제거하니 잘 작동하는 이유를 분석해 보겠습니다.

오버라이드를 사용할 때와 사용하지 않을 때의 차이

오버라이드를 사용할 때 (기존 코드):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class LMSMessage(

override val *id*: Long? = null,

override val *storeId*: String,

override val *from*: String,

override val *to*: String,

override val *text*: String,

override val *type*: MessageType,

val *subject*: String? = null,

override var *messageStatus*: MessageStatus = MessageStatus(),

override val *createdAt*: LocalDateTime,

override val *updatedAt*: LocalDateTime? = null

) : Message(id, storeId, from, to, text, type, messageStatus, createdAt, updatedAt) {

*// ...*

}

오버라이드를 사용하지 않을 때 (수정된 코드):

class LMSMessage(

*id*: Long? = null,

*storeId*: String,

*from*: String,

*to*: String,

*text*: String,

*type*: MessageType,

val *subject*: String? = null,

*messageStatus*: MessageStatus = MessageStatus(),

*createdAt*: LocalDateTime,

*updatedAt*: LocalDateTime? = null

) : Message(id, storeId, from, to, text, type, messageStatus, createdAt, updatedAt) {

*// ...*

}

왜 오버라이드를 제거하니 잘 작동하는가?

프로퍼티 초기화 순서 차이:

오버라이드 사용: 자식 클래스에서 오버라이드된 프로퍼티는 새 프로퍼티로 취급되며, 부모 클래스에 값을 전달하는 것과는 별개로 초기화됩니다.

오버라이드 제거: 자식 클래스의 파라미터는 단순히 부모 클래스 생성자에 전달되는 값으로만 사용됩니다.

초기화 블록의 실행 순서:

오버라이드 사용: init 블록에서 오버라이드된 프로퍼티에 접근할 때, 아직 자식 클래스의 프로퍼티가 완전히 초기화되지 않을 수 있음

오버라이드 제거: 부모 클래스의 프로퍼티만 사용하므로 초기화 순서 문제가 줄어듦

메모리 구조 차이:

오버라이드 사용: 두 개의 필드가 존재 (부모 필드 + 자식 필드)

오버라이드 제거: 하나의 필드만 존재 (부모 필드)

왜 이런 문제가 발생하는가?

코틀린에서 프로퍼티를 오버라이드할 때:

실제로는 부모 클래스의 프로퍼티에 대한 새로운 getter/setter를 정의하는 것

자식 클래스에서 오버라이드된 프로퍼티를 저장할 별도의 backing field가 생성될 수 있음

초기화 과정에서 복잡도 증가 및 타이밍 이슈 발생 가능

오버라이드를 제거하면 단순히 값을 부모 클래스 생성자에 전달만 하므로 초기화 과정이 간단해집니다.

override 키워드를 제거한 효과 분석

LMSMessage 클래스에서 override 키워드를 제거했을 때 동작하는 이유를 설명해드리겠습니다.

두 방식의 차이점

  1. override 사용 시 (문제가 있었던 코드)class LMSMessage( override val id: Long? = null, override val storeId: String, // 다른 오버라이드 프로퍼티들…) : Message(id, storeId, …)

  2. override 미사용 시 (정상 작동하는 코드)class LMSMessage( id: Long? = null, storeId: String, // 일반 파라미터들…) : Message(id, storeId, …)

중요한 차이점

프로퍼티의 존재 여부:

  • override 사용: 자식 클래스에서 실제 프로퍼티를 새로 정의합니다.

  • override 미사용: 단순히 부모 생성자에 값만 전달합니다 (프로퍼티는 부모 클래스에만 존재)

초기화 순서:

  • override 사용: 부모 프로퍼티 초기화 → 자식 프로퍼티 초기화 → 검증

  • override 미사용: 부모 프로퍼티만 초기화 → 검증

메모리 구조:

  • override 사용: 두 개의 프로퍼티 공간이 잠재적으로 존재할 수 있음

  • override 미사용: 하나의 프로퍼티만 존재 (부모 클래스에)

초기화 과정의 차이

override 키워드를 사용할 때:

부모 클래스의 생성자 호출 → 부모 init 블록 실행

이 시점에서 자식 클래스의 오버라이드된 프로퍼티는 초기화 중이거나 미완료 상태

자식 클래스의 오버라이드된 메서드(validateMessageFields())가 호출됨

오버라이드된 프로퍼티에 접근하려 하지만 아직 완전히 초기화되지 않아 문제 발생

override 키워드를 제거할 때:

생성자 파라미터는 단순히 부모 생성자에 값으로만 전달됨

모든 프로퍼티는 부모 클래스에 있으므로 초기화 순서 문제 감소

검증 메서드에서 접근하는 프로퍼티도 부모 클래스의 것만 사용

결론 및 권장 사항

불필요한 override는 피하세요: 단순히 부모 클래스에 값을 전달하기만 할 때는 override를 사용하지 마세요.

생성자 상속 원칙: 상속 구조에서는 가능한 한 단순하게 부모 생성자에 값을 전달하는 형태로 구현하세요.

깊은 상속 구조 주의: 복잡한 상속 구조에서는 팩토리 메서드 패턴이나 컴포지션 패턴을 고려하세요.

검증은 안전한 시점에: 모든 초기화가 완료된 후 검증을 수행하는 패턴을 사용하세요.

이렇게 override를 제거하는 것만으로도 초기화 순서 문제가 해결되는 경우가 많습니다. 저희가 앞서 제안한 방법들도 이러한 원리를 바탕으로 한 것입니다.

추가 검증 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.junit.jupiter.api.Test

class EtcTest {
@Test
fun `open val은 나중에 Init 되는지 테스트`() {
val dog: Dog = Dog("내가 개다", 10)
val animal = dog as Animal
print(dog)
print(animal)
}
}

open class Animal(
open val type: String,
open val name: String,
open val speed: Int
) {
init {
println("(Ani init) New animal: $type called $name with $speed speed")
}
}

class Dog(
override val name: String,
override val speed: Int,
) : Animal("Dog", name, speed) {
init {
println("(Dog init) New animal: $type called $name with $speed speed")
}
}
CATALOG
  1. 1. 참조
  2. 2. 본문
  • 3. override 키워드를 제거한 효과 분석
    1. 3.1. 두 방식의 차이점
    2. 3.2. 중요한 차이점
    3. 3.3. 초기화 과정의 차이
    4. 3.4. 결론 및 권장 사항
  • 4. 추가 검증 테스트 코드