Kotlin

[Kotlin] inner class & nested class 자바와 비교해서 (feat. 직렬화)

sh1mj1 2024. 1. 6. 13:12

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처        https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

자바처럼 코틀린에서도 클래스 안에 다른 클래스를 선언할 수 있다.

이것으로 `helper 클래스`를 캡슐화하거나, 코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용하다.

 

클래스 A 안에 다른 클래스 B 를 정의할 때 B 는 두 가지 유형으로 나뉜다.

  • `nested class`
    • 바깥쪽 클래스에 대해 참조를 저장하지 않는다.
  • `inner class`
    • 바깥쪽 클래스에 대한 참조를 저장한다.

자바와 달리 코틀린에서는, 안쪽에 있는 클래스는 디폴트로 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없는 `nested class` 이다.

직렬화를 통해 inner / nested class 테스트

View 요소를 만들고 View 의 상태를 직렬화해야 한다고 하자.

이를 위해 `View` 의 상태에 대해 필요한 모든 데이터를 다른 helper 클래스로 복사하는 방식을 사용한다고 하자.

 

직렬화할 수 있는 `State` 인터페이스

import java.io.Serializable

interface State : Serializable

 

`View` 인터페이스

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}

먼저 오라클, 코틀린 공식 문서에서 직렬화와 역직렬화를 설명하는 것을 간단히 읽어보자.

  • 오라클에서는
    • 직렬화: 객체 상태를 바이트 스트림으로 바꾸는 프로세스(바이트 스트림을 객체의 복사본으로 되돌릴 수 있음)
    • 역직렬화:  직렬화된 객체 형식을 객체의 사본으로 다시 바꾸는 프로세스.
  • 코틀린에서는
    • 직렬화: 응용 프로그램에서 쓰는 데이터를 네트워크를 통해 전송하거나 DB 또는 파일에 저장 가능한 형식으로 바꾸는 프로세스.
    • 역직렬화: 외부 소스에서 데이터를 읽고 이를 런타임 객체로 바꾸는 반대 프로세스.
  • 참깨빵위에참깨방 님 글에서 자세히 설명하고 있다.

HAZELCAST 사이트에서 제공하는 그림

자바의 클래스 내부에 클래스

이제 자바에서 `Button` 이라는 클래스를 생성해보자. 

이 클래스는 `View` 를 구현한다. 그리고 `State` 를 구현하는 `ButtonState` 클래스를 내부에 가진다.

 

자바로 작성한 `Button` 클래스

public class Button implements View {
    @NotNull
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }

    @Override
    public void restoreState(@NotNull State state) {
        // ...
    }

    public class ButtonState implements State {
        // 여기에 Button 에 대한 구체적인 정보를 추가
    }
}

 

`ButtonState` 를 직렬화하는 테스트 코드

@Test()
void JavaButtonStateSerializeTest() throws IOException {
    Button button = new Button();
    Button.ButtonState state = (Button.ButtonState) button.getCurrentState();
    assertThrows(NotSerializableException.class, () -> {
        // 객체를 직렬화하여 ByteArrayOutputStream 에 쓰기 시도
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(state); // 이 시점에서 NotSerializableException 발생 예상
        oos.close();
    });
}

테스트 코드는 통과한다.

즉, `NotSerializableException` 예외가 발생한다는 것이다. 

실제로 `oos.writeObject(state);` 에서 `NotSerializableException` 이 발생한다.

 

자바에서는 클래스 내부에 있는 클래스는 자동으로 `inner class` 가 된다.

즉, 기본적으로 외부 클래스에 대한 참조를 포함한다.  

그래서 `ButtonState`(내부 클래스) 를 직렬화하려고 하면, 자동으로 `Button`(외부 클래스)도 직렬화하려고 한다.

그런데 `Button` 은 `Serializable` 인터페이스를 구현하지 않았기 때문에 직렬화가 불가능하다.

그래서 `NotSerializableException` 가 발생하는 것이다!!!

 

이 문제를 해결하려면 `ButtonState` 를 `static` 클래스로 선언해야 한다.

자바에서 내부에 있는 클래스를 `static` 으로 선언하면 `nested class` 가 되어 그 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적인 참조가 사라진다.

 

`ButtonState` 를 `static` 으로 변경

public class Button implements View {
    @NotNull @Override
    public State getCurrentState() {return new ButtonState();}
    @Override
    public void restoreState(@NotNull State state) {/* ... */}
    
    public static class ButtonState implements State {
        // 여기에 Button 에 대한 구체적인 정보를 추가
    }
}

변경한 `ButtonState` 로는 예외를 던지지 않는다.

@Test
void javaButtonState2SerializeTest() {
    Button button = new Button();
    Button.ButtonState state = (Button.ButtonState) button.getCurrentState();
    assertDoesNotThrow(() -> {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(state); // 이 시점에서 NotSerializableException 발생 예상
        oos.close();
    });
}

코틀린 클래스 내부에 클래스

같은 경우에 코틀린 코드로 `Button` 을 만들어보자.

class Button : View {
    override fun getCurrentState(): State = ButtonState()
    
    // 기본적으로 nested class, inner 가 붙으면 inner class
    /* inner*/ class ButtonState : State { 
        // 여기에 Button 에 대한 구체적인 정보를 추가
    }
}

그리고 테스트를 해보면 아래처럼 결과가 나온다.

class ButtonTest {

    @Test // 코틀린 ButtonState 가 일반 class 이면 성공
    fun kotlinButtonStateSerializeTest() {
        val button = Button()
        val state: Button.ButtonState = button.getCurrentState() as Button.ButtonState
        assertDoesNotThrow {
            val baos = ByteArrayOutputStream();
            val oos = ObjectOutputStream(baos)
            oos.writeObject(state)
            oos.close()
        }
    }

    @Test // 코틀린 ButtonState 가 inner class 이면 성공
    fun kotlinButtonStateSerializeTest2() {
        val button = Button()
        val state: Button.ButtonState = button.getCurrentState() as Button.ButtonState
        assertThrows<NotSerializableException> {
            val baos = ByteArrayOutputStream();
            val oos = ObjectOutputStream(baos)
            oos.writeObject(state)
            oos.close()
        }
    }
}

코틀린에서는 내부에 있는 클래스에 아무런 키워드를 붙이지 않으면 외부 클래스에 대한 참조를 저장하지 않는 `nested class` 가 된다. 

그리고 내부에 있는 클래스에 `inner` 키워드를 붙이면 외부 클래스에 대한 참조를 저장하는 `inner class` 가 된다.

inner class 에서 외부 클래스를 참조하기

자바 `inner class` (`static` 을 쓰지 않은 클래스)에서 외부 클래스 참조

class Outer {
    int field1 = 0;
    class Inner {
        int field1 = 1;
        Outer getOuterReference() {return Outer.this;}
        int getOuterField1() {return Outer.this.field1;}
    }
}

 

코틀린 `inner class` 에서 외부 클래스 참조

class Outer() {
    val field1: Int = 0
    inner class Inner() {
        val field1: Int = 1
        fun getOuterReference(): Outer = this@Outer
        fun getOuterField1(): Int = this@Outer.field1
    }
}

코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법도 자바와 다르다.

`inner class` 인 `Inner` 안에서 외부 클래스 `Outer` 의 참조에 접근하려면 `this@Outer` 라고 써야 한다.

자바와 코틀린의 inner / nested class 정리

결론적으로 자바와 코틀린의 `inner class` / `nested class` 를 정리하면 아래 표와 같다.

클래스 B 안에 정의된 클래스 A 자바 코틀린
`nested class`
(바깥쪽 클래스에 대한 참조를 저장하지 않는다)
`static class A` `class A`
`inner class`
(바깥쪽 클래스에 대한 참조를 저장한다)
`class A` `inner class A`

 

자바와 코틀린에서 내부에 있는 클래스에서 외부 클래스에 대한 참조

 

 

참조

https://onlyfor-me-blog.tistory.com/494

https://docs.oracle.com/javase/tutorial/jndi/objects/serial.html

https://kotlinlang.org/docs/serialization.html

https://hazelcast.com/glossary/serialization/