본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 13. data class

by 사바라다 2021. 10. 5.

안녕하세요. 오늘은 코틀린의 13번째 시간입니다. 오늘 여러분들과 공유할 내용은 코틀린의 data 클래스입니다.

kotlin data class에서 만들어지는 것들

Java로 코딩을 하다보면 DTO나 VO를 만들 때 get/set 메서드, toString 과 같은 코드를 만드는 일이 반복적으로 일어나게 됩니다. 이러한 반복적인 코드를 보일러플레이트 코드라고 하는데요. 자바에서 이러한 코드르 없애려고 노력하듯이 코틀린 또한 노력하고 있습니다. 자바에서는 lombok 이나 16버전 이상부터 사용학능한 record를 사용하면 됩니다만 코틀린에서는 data class 라는 타입으로 이를 해결할 수 있습니다. 예제로 한번 알아보도록 하겠습니다.

아래는 일반적인 java 코드입니다. get/set/toString/hashCode 등 의 함수를 직접 만들어주고 있습니다. 하지만 일반적으로 아래처럼 만들어주는건 너무 피곤한 일입니다. 따라서 Lombok을 추가적으로 이용하여 @ToString, @Getter 등의 어노테이션을 통해 이를 구현합니다.

public class ReadVersionResponse {

   private final String appType;

   private final String version;

   private final String developerEmail;

   public ReadVersionResponse(String appType, String version, String developerEmail) {
      this.appType = appType;
      this.version = version;
      this.developerEmail = developerEmail;
   }

   public final String getAppType() {
      return this.appType;
   }

   public final String getVersion() {
      return this.version;
   }

   public final String getDeveloperEmail() {
      return this.developerEmail;
   }

   public String toString() {
      return "ReadVersionResponse(appType=" + this.appType + ", version=" + this.version + ", developerEmail=" + this.developerEmail + ")";
   }

   public int hashCode() {
      int var1 = (this.appType != null ? this.appType.hashCode() : 0) * 31;
      var1 = (var1 + (this.version != null ? this.version.hashCode() : 0)) * 31;
      return var1 + (this.developerEmail != null ? this.developerEmail.hashCode() : 0);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof ReadVersionResponse) {
            ReadVersionResponse var2 = (ReadVersionResponse)var1;
            if (Intrinsics.areEqual(this.appType, var2.appType) && Intrinsics.areEqual(this.version, var2.version) && Intrinsics.areEqual(this.developerEmail, var2.developerEmail)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

자바의 위와 같은 보일러플레이트 코드를 없애고 자동으로 생성해주는 것으로 코틀린에서는 언어레벨로 data 클래스가 나왔습니다. data class는 일반 class 앞에 data syntax를 붙이는 것만으로 생성이 되는 클래스입니다. data syntax를 붙이는 것 만으로 컴파일타임에 get/set/toString/hashCode/equals 등을 자동으로 생성해줍니다. 사용방법은 아래 코드와 같습니다. 정말 간편해지지 않았나요?

data class ReadVersionResponse(
    val appType: String,
    val version: String,
    var developerEmail: String
)

사용방법

코틀린 data 클래스를 통해 자동으로 만들어지는 코드는 equals, hashCode, getter, setter, toString과 같이 java의 Object 클래스에 들어있는 기본 메서드들과 코틀린 data 클래스에서 추가된 copy, componenetN가 있습니다. 아래 코드가 코틀린의 data 클래스입니다. data 클래스는 primary constructor가 필수적으로 필요합니다. 그리고 그곳에 1개 이상의 파라미터를 추가해야합니다. val은 getter만 생성되며 var는 getter와 setter 메서드가 자동으로 생성됩니다.

data class ReadVersionResponse(
    val appType: String,
    val version: String,
    var developerEmail: String
)

먼저 java의 obejct 클래스에 있던 메서드들이 어떻게 생성되는지 알아보도록 하겠습니다. 아래는 컴파일타임에 자동으로 만들어진 getter 메서드와 setter 메서드입니다. setter를 보면 kotlin의 non-nullable에 맞게 null을 check 하는 코드가 자동으로 만들어진 것을 확인할 수 있었습니다.

@NotNull
public final String getDeveloperEmail() {
    return this.developerEmail;
}
public final void setDeveloperEmail(@NotNull String var1) {
    Intrinsics.checkNotNullParameter(var1, "<set-?>");
    this.developerEmail = var1;
}

toString

객체를 String으로 표현하는 toString 메서드입니다. 내부 내용은 Lombok의 @ToString과 다르지 않다는 것을 확인할 수 있습니다. Class 이름과 내부 파라미터들이 모두 String으로 나타내는것을 확인할 수 있었습니다.

@NotNull
public String toString() {
    return "ReadVersionResponse(appType=" + this.appType + ", version=" + this.version + ", developerEmail=" + this.developerEmail + ")";
}

hashCode

hashCode 메서드입니다. hashCode는 java에서와 동일한것으로 확인되었습니다.

public int hashCode() {
    String var10000 = this.appType;
    int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
    String var10001 = this.version;
    var1 = (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31;
    var10001 = this.developerEmail;
    return var1 + (var10001 != null ? var10001.hashCode() : 0);
}

equals

두 객체의 동일성을 비교하는 equals 메서드입니다. 객체가 동치이면 true 아니라면 내부의 파라미터를 모두 비교하여 true 또는 false를 반환하는 것을 확인핤 수 있었습니다.

public boolean equals(@Nullable Object var1) {
    if (this != var1) {
        if (var1 instanceof ReadVersionResponse) {
        ReadVersionResponse var2 = (ReadVersionResponse)var1;
        if (Intrinsics.areEqual(this.appType, var2.appType) && Intrinsics.areEqual(this.version, var2.version) && Intrinsics.areEqual(this.developerEmail, var2.developerEmail)) {
            return true;
        }
        }

        return false;
    } else {
        return true;
    }
}

Copy

copy의 경우 java에서는 clone 이라는 메서드가 있었지만 이 메서드는 deep copy를 지원하지 않았기 때문에 사용되지 않는 안티 메서드였습니다. 코틀린에서는 이를 보완하여 deep copy를 지원하는 메서드로 사용됩니다. copy의 경우 단지 복사할 뿐만 아니라 특정 property를 바꾸면서 복사하는 기능도 제공해줍니다. copy의 java로 컴파일된 코는 아래와 같습니다.

@NotNull
public final ReadVersionResponse copy(@NotNull String appType, @NotNull String version, @NotNull String developerEmail) {
    Intrinsics.checkNotNullParameter(appType, "appType");
    Intrinsics.checkNotNullParameter(version, "version");
    Intrinsics.checkNotNullParameter(developerEmail, "developerEmail");
    return new ReadVersionResponse(appType, version, developerEmail);
}

추가적으로 코틀린의 copy는 특정 property를 변경하면서 copy가 가능합니다. 아래예제는 코틀린 공식문서에서 가져온 코드입니다. 아래와 같이 copy에 namedParameter로 변경할 property의 값을 추가해주시면 됩니다.

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

ComponenetN & destructuring

compoenetN 메서드가 있습니다. 해당 메서드는 생성자의 요소들을 descturcturing하여 가져올 수 있도록 합니다. desctucturing이란 것은 1개의 변수를 통해 객체로 받는 것이 아니라 내부 변수들을 이용하여 여러개의 변수로 나누어서 받는것을 말합니다. 아래 예제의 1번째 코드는 일반적으로 객체를 변수에 할당하는 것이고 2번째 줄에 있는 코드가 descturucturing하여 내부 변수를 다 가져오는 방법입니다.

val readVersionResponse = ReadVersionResponse("appType", "version", "developerEmail")
val (appType, version, developerEmail) = ReadVersionResponse("appType", "version", "developerEmail")

주의사항

아쉽지만 이러한 data class를 모든 상화에서 사용할 수 있는것은 아닙니다. data 클래스의 사용은 한계가 있고 사용지 못하는것도 있습니다. 일단 data 클래스를 사용하기 위해서는 아래의 조건을 가집니다.

  1. primary consturctor에 1개 이상의 파라미터가 존재해야합니다.
  2. primary constructor에 있는 변수들은 val 또는 var을 가져아합니다.
  3. data 클래스는 java 클래스의 final로써 abstract, open, sealed, inner 키워드를 가지지 못합니다.

1번의 조건으로 인하여 data 클래스는 JPA Entity 클래스로 가져가기에서는 무리가 있습니다. JPA의 Entity 클래스는 기본생성자가 필수입니다. 편법으로 primary constructor의 모든 파라미터에 기본값을 설정하면 기본생성자가 자동으로 추가되긴 합니다만 이렇게까지해서 Entity를 만드는것이 얼마나 효율적일지는 개인의 판단에 맡기도록 하겠습니다.

data 클래스 내부에서 equals, hashCode, toString 등의 메서드는 별도로 구현하실 수 있습니다. 이렇게하면 자동으로 코드가 생성되지 않고 직접 생성한 코드가 사용되게됩니다. 이와는 다르게 compoenetN, 그리고 copy는 직접 코드 생성이 허가되지 않습니다.

data 클래스는 primary constructor에 있는 값만을 자동생성 메서드에 이용합니다. 그래서 만약 아래 코드의 value와 같은 변수들은 자동 생성되는 메서드에서 빠지게되니 유의하시기 바랍니다.

data class ReadVersionResponse(
    val appType: String,
    val version: String,
    var developerEmail: String
) {
    val value: String = "sample";
}

마무리

오늘은 이렇게 data 클래스를 사용하는 방법과 그 안에서 자동으로 어떠한 코드들이 생성되는지, 그리고 어떻게 생성되는지에 대해서 알아보았습니다.

감사합니다.

참조

kotlinlang_data-classes

댓글