참조
본문
큰일이없으면 property에다가 override 는 쓰지않는것이좋다.
open-override 의 경우 init{} 단계에서 부모 클래스가 초기화 호출후, 자식 클래스가 init{}호출될텐데, 그럼 override된 필드는 부모 클래스의 초기화단계에서는 Null이다.
이것은 코틀린에서 상속과 프로퍼티 오버라이딩과 관련된 중요한 차이점입니다. 오버라이드를 제거하니 잘 작동하는 이유를 분석해 보겠습니다.
오버라이드를 사용할 때와 사용하지 않을 때의 차이
오버라이드를 사용할 때 (기존 코드):
1 | class LMSMessage( |
왜 오버라이드를 제거하니 잘 작동하는가?
프로퍼티 초기화 순서 차이:
오버라이드 사용: 자식 클래스에서 오버라이드된 프로퍼티는 새 프로퍼티로 취급되며, 부모 클래스에 값을 전달하는 것과는 별개로 초기화됩니다.
오버라이드 제거: 자식 클래스의 파라미터는 단순히 부모 클래스 생성자에 전달되는 값으로만 사용됩니다.
초기화 블록의 실행 순서:
오버라이드 사용: init 블록에서 오버라이드된 프로퍼티에 접근할 때, 아직 자식 클래스의 프로퍼티가 완전히 초기화되지 않을 수 있음
오버라이드 제거: 부모 클래스의 프로퍼티만 사용하므로 초기화 순서 문제가 줄어듦
메모리 구조 차이:
오버라이드 사용: 두 개의 필드가 존재 (부모 필드 + 자식 필드)
오버라이드 제거: 하나의 필드만 존재 (부모 필드)
왜 이런 문제가 발생하는가?
코틀린에서 프로퍼티를 오버라이드할 때:
실제로는 부모 클래스의 프로퍼티에 대한 새로운 getter/setter를 정의하는 것
자식 클래스에서 오버라이드된 프로퍼티를 저장할 별도의 backing field가 생성될 수 있음
초기화 과정에서 복잡도 증가 및 타이밍 이슈 발생 가능
오버라이드를 제거하면 단순히 값을 부모 클래스 생성자에 전달만 하므로 초기화 과정이 간단해집니다.
override 키워드를 제거한 효과 분석
LMSMessage 클래스에서 override 키워드를 제거했을 때 동작하는 이유를 설명해드리겠습니다.
두 방식의 차이점
override 사용 시 (문제가 있었던 코드)class LMSMessage( override val id: Long? = null, override val storeId: String, // 다른 오버라이드 프로퍼티들…) : Message(id, storeId, …)
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 | import org.junit.jupiter.api.Test |