Kotlin - Data class 이해 및 구현 방법

데이터 클래스(Data class)는 데이터 보관 목적으로 만든 클래스를 말합니다. 데이터 클래스는 프로퍼티에 대한 toString(), hashCode(), equals(), copy() 메소드를 자동으로 만들어 줍니다. 그래서 boilerplate code를 만들지 않아도 됩니다.

데이터 클래스는 클래스 앞에 data를 붙여줍니다. 예를 들어, 다음과 같이 데이터 클래스를 정의할 수 있습니다.

data class Site(val url: String, val title: String) {
    val description = ""
}

자바로 변환해서 보면, 기본적인 toString(), hashCode(), equals(), copy() 메소드가 구현된 것을 알 수 있습니다.

public final class Site {
   ....

   public Site(@NotNull String url, @NotNull String title) {
      ....
   }

   @NotNull
   public final Site copy(@NotNull String url, @NotNull String title) {
     return new Site(url, title);
   }

   @NotNull
   public String toString() {
      return "Site(url=" + this.url + ", title=" + this.title + ")";
   }

   public int hashCode() {
      return (this.url != null ? this.url.hashCode() : 0) * 31 + (this.title != null ? this.title.hashCode() : 0);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Site) {
            Site var2 = (Site)var1;
            if (Intrinsics.areEqual(this.url, var2.url) && Intrinsics.areEqual(this.title, var2.title)) {
               return true;
            }
         }

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

toString()의 인자를 보면 생성자에서 선언한 프로퍼티만 있습니다. 따라서 지역변수에 대한 것은 고려되지 않습니다.

데이터 클래스의 특징은 다음과 같습니다.

  • 데이터 클래스의 생성자(primary constructor)는 1개 이상의 프로퍼티를 선언되어야 합니다.
  • 데이터 클래스의 생성자 프로퍼티는 val 또는 var으로 선언해야 합니다.
  • 데이터 클래스에 abstract, open, sealed, inner 를 붙일 수 없습니다.
  • 클래스에서 toString(), hashCode(), equals(), copy()를 override하면, 그 함수는 직접 구현된 코드를 사용합니다.
  • 데이터 클래스는 상속받을 수 없습니다.

copy()

copy()는 객체의 복사본을 만들어 리턴합니다. 리턴되는 객체는 얕은 복사(swallow copy)로 생성됩니다. copy()의 인자로 생성자에 정의된 프로퍼티를 넘길 수 있으며, 그 프로퍼티의 값만 변경되고 나머지 값은 동일한 객체가 생성됩니다.

아래 코드는 site1의 객체를 복사할 때 title만 변경하여 새로운 객체를 생성하는 예제입니다.

val site1 = Site("kotlinlang.com",
    "Kotlin New Features (1)")
val site2 = site1.copy(title = "Kotlin New Features (2)")

println(site1)
println(site2)

두 객체를 출력해보면 title만 변경되어 copy가 된 것을 볼 수 있습니다.

Site(url=kotlinlang.com, title=Kotlin New Features (1))
Site(url=kotlinlang.com, title=Kotlin New Features (2))

주의할 점은 copy()에 전달되는 인자는 생성자에 정의된 프로퍼티만 될 수 있습니다.

toString(), hashCode(), equals()

자동으로 구현된 toString(), hashCode(), equals() 함수를 호출해보겠습니다.

toString()은 생성자에 정의된 프로퍼티만 출력하고, 클래스 내에 지역변수로 선언한 프로퍼티는 출력하지 않습니다. 지역변수도 toString()에 출력하고 싶으면 직접 오버라이드해서 구현해줘야 합니다.

아래 코드는 3개의 메소드를 모두 사용하는 예제입니다.

val site1 = Site("kotlinlang.com",
    "Kotlin New Features (1)")
val site2 = site1.copy(title = "Kotlin New Features (2)")

println(site1.toString())
println(site1.hashCode())
println(site2.toString())
println(site2.hashCode())

if (site1.equals(site2)) {
    println("Eqaul")
} else {
    println("Not Eqaul")
}

실행해보면 다음과 같습니다. 모두 다른 hashCode를 출력하고, 다른 객체로 취급되고 있습니다.

Site(url=kotlinlang.com, title=Kotlin New Features (1))
-2144111637
Site(url=kotlinlang.com, title=Kotlin New Features (2))
-2144111606
Not Eqaul

데이터 분해 및 대입(Destructuring Declarations)

Destructuring Declarations라는 개념이 있습니다. Site객체의 내부 변수를 다른 변수에 대입하려면 일반적으로 다음과 같이 해야 합니다.

val site1 = Site("kotlinlang.com", "Kotlin New Features (1)")
val url = site1.url
val title = site1.title

하지만 데이터 클래스는 Destructuring Declarations를 지원하기 때문에, 아래 코드처럼 한줄로 표현할 수 있습니다.

val site1 = Site("kotlinlang.com", "Kotlin New Features (1)")

val (url, title) = site1
// 각 변수에 다음 값들이 대입됨
// url = "Kotlin New Features (1)"
// title = "kotlinlang.com"

val/var (name1, name2) = Data class object처럼 괄호 안에 변수를 선언해주면 생성자에 정의된 프로퍼티들이 순서대로 변수에 대입됩니다.

동작 원리

사실 Destructuring Declaration이 어떻게 동작하는지 알 필요는 없지만, (Java 프로그래머라면) 코틀린 코드를 Java로 변환하고 어떻게 동작하는지 살펴보면 좀 더 깊이 이해하는데 도움이 될 수 있습니다.

위의 예제에서 사용된 코드를 Java로 변환해보면 다음과 같이 코틀린에서는 정의하지 않은 componentN() 메소드들이 생성됩니다.

public final class Site {
    public Site(String url, String title) {
        this.url = url;
        this.title = title;
        this.description = "";
    }

    public final String component1() {
        return this.url;
    }

    public final String component2() {
        return this.title;
    }
    ...
}

여기서 생성된 componentN()의 리턴 값은 url, title 변수에 대입이 됩니다.

public final class Example {

   public static final void main(@NotNull String[] args) {
      Site site1 = new Site("kotlinlang.com", "Kotlin New Features (1)");
      String url = site1.component1();
      String title = site1.component2();
   }
}

다시 정리하면, Destructuring Declaration은 컴파일 시점에 componentN() 메소드를 자동 생성하며, 선언한 변수에 리턴값을 넣어주도록 변환됩니다.

주의할 점

Destructuring Declaration은 생성자에 정의된 프로퍼티의 순서대로 변수에 대입을 합니다.

예를 들어, 다음 코드에서 클래스의 생성자의 프로퍼티는 url, title 순서로 정의가 되었지만 선언한 변수는 title, url 순서로 정의되었습니다.

data class Site(val url: String, val title: String) { }

val site1 = Site("kotlinlang.com", "Kotlin New Features (1)")

val (title, url) = site1
// 각 변수에 다음 값들이 대입됨
// title = "kotlinlang.com"
// url = "Kotlin New Features (1)"

순서가 뒤바껴도 컴파일러가 이름을 보고 올바른 값을 넣어줄 것 같지만, 결과를 보면 이름을 고려하지 않고 순서대로 대입해주고 있습니다.

내부적으로 어떻게 동작되는지 설명을 하면, 컴파일 시점에 생성자에 정의된 인자를 리턴하는 componentN() 메소드가 생성되며 여기서 숫자 N은 프로퍼티의 순서를 의미합니다. 즉, 첫번째 프로퍼티의 값을 리턴하는 메소드는 component1()으로 정의가 됩니다. 따라서, 첫번째로 선언한 변수 title에 component1()의 리턴값인 site의 값이 할당되며, 두번째로 선언한 변수 url에 component2()의 리턴 값인 프로퍼티 title의 값이 대입됩니다.

말로 설명하려고하니 더 헷갈리는 것 같지만, 순서대로 대입된다는 것만 알아두시면 될 것 같습니다.

만약 순서가 다를 때 컴파일 에러라도 발생하면 좋겠지만, 타입이 모두 String이라서 컴파일도 잘 됩니다. 그래서 항상 순서에 주의를 기울여야 합니다.

일반 클래스에서 Destructuring Declarations 사용 방법

Destructuring Declarations는 일반 클래스에서도 사용할 수 있습니다. 하지만 componentN() 메소드들을 직접 구현해줘야 합니다. 자세한 것은 Kotlin - Destructuring Declaration에서 확인해주세요.

정리

데이터 클래스가 무엇인지에 대해서 알아보았습니다. 일반적으로 사용되는 메소드들이 자동으로 생성되기 때문에 boilerplate 코드를 작성할 필요가 없습니다. 그래서 데이터를 저장하는 목적의 클래스로 사용하기 좋습니다. 또한 데이터 클래스는 Destructuring Declarations를 지원하기 때문에 코드를 간결하고 가독성있게 만들어줍니다.

참고

Loading script...
codechachaCopyright ©2019 codechacha