[kotlin] 주생성자와 init 블럭의 실행순서

GuruneLee·2023년 1월 1일
1

Let's Study 공부해요~

목록 보기
28/36
post-thumbnail

init block은 필드에 값이 할당된 후 실행되는가?

문제

Kotlin IN ACTION 이라는 책에서 다음과 같은 부분을 확인할 수 있다.

초기화 블록에는 클래스의 객체가 만들어질 때(인스턴스화될 때) 실행될 초기화 코드가 들어간다.
초기화 블록은 주 생성자와 함께 사용된다.

그런데 함께 사용된다 라는 말이 조금 거슬린다.
가령, init block 내부에서 인스턴스에 정의된 메서드를 사용해도 문제가 없는가?

class MyClass(
     val foo: Int;
) {
    init {
        this.do()
    }
 
    fun do() {
        foo = 1;
    }
}

만약 이게 제대로 동작한다면, '함께 사용' 이란 말이 조금 어색하지 않나...? 하는 생각이다.
(메서드는 분명 인스턴스 생성 이후에 사용할 수 있을것이기 때문)

테스트

테스트 1

하지만 걱정과는 다르게 위 코드는 잘 동작한다.

다음과 같은 테스트를 만들어서 시행해보니 통과한다. (Junit5)

internal class MyClassTest {
    @Test
    fun doTest() {
        val myClass = MyClass(123)
 
        myClass.foo eq 1
    }
}

왜 그런지 잘 모르겠어서 조금 더 파보았다.

테스트 2

다음과 같이 NoInit.ktYesInit.kt 클래스를 만들어서 bytecode를 비교해봤다.

// NoInit.kt
class NoInit( var foo: Int )
 
// YesInit.kt
class YesInit( var foo: Int ) {
    init {
        this.dod()
    }
    fun dod() {
        this.foo = 1
    }
}

bytecode로 변환 후 init 메서드를 확인해보자

// NoInit.class
L0
  LINENUMBER 8 L0
  ALOAD 0
  INVOKESPECIAL java/lang/Object.<init> ()V
  ALOAD 0
  ILOAD 1
  PUTFIELD NoInit.foo : I
  RETURN
 L1
  LOCALVARIABLE this LNoInit; L0 L1 0
  LOCALVARIABLE foo I L0 L1 1
  MAXSTACK = 2
  MAXLOCALS = 2
 
// YesInit.class
L0
  LINENUMBER 8 L0
  ALOAD 0
  INVOKESPECIAL java/lang/Object.<init> ()V
  ALOAD 0
  ILOAD 1
  PUTFIELD YesInit.foo : I
 L1
  LINENUMBER 11 L1
  NOP
 L2
  LINENUMBER 12 L2
  ALOAD 0
  INVOKEVIRTUAL YesInit.dod ()V
 L3
  LINENUMBER 13 L3
  RETURN
 L4
  LOCALVARIABLE this LYesInit; L0 L4 0
  LOCALVARIABLE foo I L0 L4 1
  MAXSTACK = 2
  MAXLOCALS = 2

음... 난 Bytecode를 처음보니, 직접 비교해보자! (궁금하면 jvm instruction set 문서를 살펴봐도 좋다)

NoInit과 YesInit 의 L0 는 거의 같다

L0
 LINENUMBER 8 L0
 ALOAD 0
 INVOKESPECIAL java/lang/Object.<init> ()V
 ALOAD 0
 ILOAD 1
 PUTFIELD (NoInit.foo/YesInit.foo) : I
 (RETURN)
  • 아마 변수를 스택에 로드하고, 생성자를 통해 들어온 값을 할당하는 부분인 듯 하다.
  • NoInit.class 에선 이게 끝이니 RETURN이 포함된 모습이다.

YesInit의 이후 부분을 살펴보자

L1
 LINENUMBER 11 L1
 NOP
L2
 LINENUMBER 12 L2
 ALOAD 0
 INVOKEVIRTUAL YesInit.dod ()V
L3
 LINENUMBER 13 L3
 RETURN
  • L1LINENUMBER 11 L1 이 부분은 실제 line-11에 위치한 init 구문을 의미하는 듯 하다
  • 필드 할당(L0) 이후, this.dod()를 시행하는 것을 알 수 있다.
    → 우선, 필드에 값 주입 이후 init block이 실행되는 것을 확인했다.

하지만, 인스턴스화 되어서야 실행할 수 있는 dod() 메서드는 어떻게 실행 하는걸까?

자, 이번엔 YesInit.kt 의 인스턴스를 생성하는 Do.ktDodo() 메서드를 만들어서 Bytecode화 해봤다

// Do.kt
class Do {
    fun Dodo() {
        YesInit(123)
    }
}
 
// bytecode
  public final Dodo()V
   L0
    LINENUMBER 22 L0
    NEW YesInit
    DUP
    BIPUSH 123
    INVOKESPECIAL YesInit.<init> (I)V
    POP
   L1
    LINENUMBER 23 L1
    RETURN

L0의 이 부분을 자세히 보자

NEW YesInit
DUP
BIPUSH 123
INVOKESPECIAL YesInit.<init> (I)V
  • NEW는 새로운 객체를 만드는 구문이다.
  • 어라? 객체를 만든 다음에서야 YesInit.<init> 을 호출한다!!

객체를 미리 만들고 (dod() 메서드가 사용가능한 상태가 되고) 나서 <init>을 따로 호출하는 것을 볼 수 있다.

그렇다. 인스턴스를 만드는 것과, 이를 초기화 하는것은 별개의 프로세스인 것이다. 이렇게 프로세스가 분리되어 있으므로, init block에서 메서드를 호출할 수 있었던 것이다.

결론

: 필드에 값 주입 이후 init block이 실행된다. 이 과정은 '인스턴스 초기화' 과정에 속한다.
bytecode를 확인해 봤을때, '인스턴스 생성'과 '인스턴스 초기화'는 분리되어있는 과정이라 추론 가능하다.
따라서, 인스턴스 메서드는 '생성'과정 이후 available 하고, 초기화 과정해서 이 메서드를 활용하는것은 어색하지 않다.

profile
Today, I Shoveled AGAIN....

0개의 댓글