티스토리 뷰

728x90
data class Cash(val dollars: Int) {
    fun mul(factor: Int): Cash {
        return Cash(dollars * factor)
    }
}

우아한테크코스에서 수업을 들으며, "불변 객체"에 대해 학습했다.

불변 객체의 특징과 장점은 아래와 같다.

  • 생성자를 통해서만 초기화한다.
  • 변경할 수 없기 때문에 동시에 접근해도 항상 같은 값을 보장한다.
  • 실패 원자성을 보장한다. (메서드를 수행하다가 예외가 발생하는 경우에도 메서드 수행 전 상태와 동일함을 보장)
  • 도메인의 불변 객체를 값 객체(VO)라고도 한다.

이러한 불변 객체에도 단점이 있다.

값을 변경할 때마다 인스턴스를 생성하기 때문에 메모리 측면에서 비효율적일 수 있다는 점이다.

(하지만 불변 객체는 메모리를 걱정하는 것보다 훨씬 더 많은 이점을 준다고, 썬 마이크로가 말했다...)

 

어쨌든 불변 객체의 인스턴스 생성이 걱정된다면, value class를 생성하면 된다.

@JvmInline
value class Cash(val dollars: Int) {
    fun mul(factor: Int): Cash {
        return Cash(dollars * factor)
    }
}

fun main() {
    val cash = Cash(3)
}

value class의 생성자를 호출하면, 인스턴스가 생성된 것처럼 보이지만 실제로는 인스턴스가 생성된 것이 아니다.

생성자를 호출했는데 인스턴스가 생성되지 않았다니, 이게 무슨 소리일까?

자바로 디컴파일한 코드를 보자.

 

public final class Cash {
   private final int dollars;

   public final int getDollars() {
      return this.dollars;
   }

   private Cash(int dollars) {
      this.dollars = dollars;
   }

   public static int constructor_impl(int dollars) {
      return dollars;
   }

   public static final Cash box_impl(int v) {
      return new Cash(v);
   }
}
int cash = Cash.constructor-impl(3);

(디컴파일 코드 중 일부만 가져왔다.)

실제 Cash 클래스는 생성자가 private으로 제한되어있다.

main()에서 Cash 인스턴스를 생성하는 코드는

new Cash(3);이 아닌 Cash.constructor-impl(3);이라는 코드로 디컴파일 되었다.

constructor-impl() 함수는 인스턴스를 생성하지 않고, 생성자 파라미터로 들어온 값을 바로 반환하기만 한다.

 

즉, 코틀린 코드에서는 Cash 인스턴스처럼 보이지만 실제로는 값 자체를 사용한다.

 

디컴파일된 Cash 클래스를 보면, 값 자체를 반환하는 constructor-impl()와 생성자를 호출하여 실제 인스턴스를 반환하는 box_impl() 함수가 있다.

코틀린에서 일반적으로 생성자를 호출하면 constructor-impl()이 호출되는 것을 확인했다.

그러면 box_impl()는 언제 호출될까?

 

    @Test
    fun test() {
        val cash1 = Cash(3)
        val cash2 = Cash(3)

        assertThat(cash1).isEqualTo(cash2) // passed
        assertThat(cash1).isSameAs(cash2) // failed
    }

생성자를 호출하면(실제로는 생성자가 아닌 constructor-impl) 값 자체가 바로 반환되기 때문에

cash1과 cash2는 항상 같을 거라고 예상할 수 있다.

하지만 cash1과 cash2끼리 동일성(주소값) 비교를 하면, 테스트가 통과되지 않는다.

Int 값 3과 3을 비교하는 건데, 왜 주소값이 같지 않을까?

 

   @org.junit.jupiter.api.Test
   public final void test() {
      int cash1 = Cash.constructor-impl(3);
      int cash2 = Cash.constructor-impl(3);
      Assertions.assertThat(Cash.box-impl(cash1)).isEqualTo(Cash.box-impl(cash2));
      Assertions.assertThat(Cash.box-impl(cash1)).isSameAs(Cash.box-impl(cash2));
   }

위 테스트 코드를 디컴파일 해보면 위와 같은 코드가 나온다.

cash1과 cash2에는 constructor-impl()이 호출되어 값 자체가 저장되지만,

assertThat(), isEqualTo(), isSameAs()의 인자로 들어갈 때는 box-impl()가 호출됨을 알 수 있다.

즉, 실제 인스턴스가 생성된 것이다.

 

public SELF isEqualTo(Object expected)
public SELF isSameAs(Object expected)

실제 인스턴스가 왜 생성되었는지는, 함수의 시그니처를 보면 알 수 있다.

바로 함수의 인자가 Object 타입이기 때문이다.

Object, 즉 Reference Type이 필요하기 때문에 실제 인스턴스를 생성한 것이다.

인스턴스를 새로 생성하여 주소값이 달랐기 때문에 동일성을 비교하는 테스트는 통과하지 않았다.

 

    @Test
    fun test() {
        val cash1 = Cash(3)
        val cash2 = Cash(3)

        assertThat(cash1).isEqualTo(cash2) // passed
        assertThat(cash1).isSameAs(cash2) // failed
    }

사실 이 코드를 value class가 아닌 일반 값 객체를 사용했다면, 인스턴스는 두 번밖에 생성되지 않았을 것이다. (cash1, cash2)

그런데 value class를 사용하면서 함수의 인자로 전달될 때마다 인스턴스가 생성되어, 인스턴스는 총 네 번 생성되었다. (box-impl 네 번 호출)

이처럼 value class를 사용해서 오히려 더 비효율적인 경우가 있을 수 있으니 경우에 따라 적절히 사용해야겠다.

 

 

 

추가적으로 value class에 대해 간단히 알아본 내용을 정리해본다.

*

    @Test
    fun test() {
        val cash1 = Cash(3)
        val cash2 = Cash(3)

        assertThat(cash1 === cash2).isTrue() // error
        // Identity equality for arguments of types Cash and Cash is forbidden
    }

value class의 인스턴스끼리의 === 비교는 컴파일 단계에서 금지한다.

 

*

생성자 프로퍼티에 단 하나의 프로퍼티만 존재해야 한다.

var이 아닌 val이어야 한다.

 

*

data class는 equals(), toString(), hashCode(), copy(), componentN()를 자동 생성해준다.

반면에 value class는 equals(), toString(), hashCode()만 자동 생성해준다.

 

*

backing field를 사용할 수 없다.

 

 

 

참고

https://kotlinlang.org/docs/inline-classes.html

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
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
글 보관함