본문 바로가기
kotlin/insight

Kotlin 에서의 원시값 포장과 그에 적합한 클래스 선택하기

by 코드 이야기 2023. 4. 8.
728x90

 

서론 

원시값들을 모두 포장하다보니 클래스 외부에서 비교를 할 때 아래와 같이 코드가 길어지고 복잡해지는 경우가 많았다.

  • 예제) 로또의 번호들이 중복되었는지 검사하는 코드
require(numbers.size == numbers.distinctBy { it.number }.size) { 
    LOTTERY_NUMBERS_DUPLICATE_ERROR 
}

따라서, 외부에서 사용시 복잡해지지 않도록 equals를 override하였다.
두 객체를 비교할 때 객체의 주소가 아닌 객체의 내부 변수를 이용해 비교를 하도록 하였다.

 

하지만 이렇게 원시값을 포장할 때는 class 보다는 data class가 더 적절할 수 있다.

때에 따라서는 value class가 더 적절할 수도 있다.

class, data class, value class의 특징을 정리해 보았다.

 

각각의 클래스를 사용해 같은 코드가 어떻게 다른 것인지 살펴보도록 하자.


class (override)

class LotteryNumber(
    val number: Int
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as LotteryNumber

        if (number != other.number) return false
        return true
    }

    override fun hashCode(): Int = number

    override fun toString(): String = "$number"
}

 


data class

class와 data class는 equals(), hashCode(), toString(), componentN(), copy()의 로직이 다르다.
코틀린 코드를 자바 코드로 역컴파일 하여 확인할 수 있다.

  • equals()
    • class에서는 객체의 주소를 사용해 동일성비교한다.
    • data class에서는 객체 내부 변수를 사용해 동등성비교한다.
// class
public boolean equals(@Nullable Object other) {
   if ((LotteryNumber)this == other) {
      return true;
   } else if (Intrinsics.areEqual(this.getClass(), other != null ? other.getClass() : null) ^ true) {
      return false;
   } else if (other == null) {
      throw new NullPointerException("null cannot be cast to non-null type lotto.domain.LotteryNumber");
   } else {
      LotteryNumber var10000 = (LotteryNumber)other;
      return this.number == ((LotteryNumber)other).number;
   }
}

public int hashCode() { return this.number; }

// data class

public boolean equals(@Nullable Object var1) {
   if (this != var1) {
      if (var1 instanceof LotteryNumber) {
         LotteryNumber var2 = (LotteryNumber)var1;
         if (this.number == var2.number) {
            return true;
         }
      }

      return false;
   } else {
      return true;
   }
}

public int hashCode() { return Integer.hashCode(this.number); }

 

 

  • toString()
    • class: 객체의 주소
    • data class: LotteryNumber(number=42)
    • data class는 class를 위와 같은 형태의 String으로 바꿔준다.
      따라서 동등성 비교를 할 때는 문제가 되지 않지만
      객체 내부의 변수를 그대로 출력하고자 한다면 오버라이드를 해주어야 한다.
  • copy()
    • 동일한 객체로 복사를 한다.

 

value class (= inline class)

  • data class와 equals(), hashCode(), toString()의 로직이 같다.
  • 객체를 생성할 때 발생하는 비용을 줄여주는 class
  • 하나의 immutable 매개변수만 가질 수 있고, @JvmInline annotaion과 함께 사용해야 한다.
  • 그 외에도 data class와 다르게 equals, toString, hasCode만 자동 생성이 되고,
    "=="만 허용이 된다. ("===" 불가)
    (동등성을 만족할 때 동일성도 만족하기 때문)
@JvmInline
value class LotteryNumber(
    private val number: Int
) {
    ...
}
  • Mangling (value class가 객체 생성 비용을 줄여주는 방식)
    • 컴파일 중에는 LotteryNumber 타입이지만 바이트코드에서 Int로 변경하는 방식
      (일반 class를 생성하는 것보다 기본형을 생성하는 것이 비용 발생이 적다.)

 

 

결론

  • data class가 적합할 때 
    • 내부 변수가 가변값이거나 여럿일 때
    • 동일성 비교를 하지 않고, 동등성 비교만 필요할 때 
      (같은 내부 변수를 가지는 객체는 여럿 존재할 수 있음)
  • value class가 적합할 때
    • 내부 변수가 불변값 하나일 때
    • 동일성 비교를 하지 않고, 동등성 비교만 필요할 때
      (같은 내부 변수가 곧 같은 객체, 원시 타입이기 때문)

이렇게 간단히 정리해볼 수 있다.

하지만, value class를 사용할 때는 data class에 비해 주의할 점이 많다.

(부생성자를 통해 원시값을 받아 value class 타입으로 포장할 수 없는 점 등)

 


참고

 

 

 

 

728x90

댓글