Kotlin - 04. Part2. 데이터 클래스, object 키워드, 동반 객체

Kotlin 의 다양한 클래스 활용법

  • 데이터 클래스 data class
  • 클래스 위임 by
  • object 키워드

컴파일러가 생성한 메소드

Java 는 클래스가 equals, hashCode, toString 등의 메소드를 구현해야하기 때문에, IDE 에서 자동으로 이런 메소드를 자동으로 기계적으로 구현해주고 있다. 하지만 자동으로 그런 필수 메소드를 구현하다가 코드베이스가 번잡해지는 면이 있다고 한다.

Kotlin 에서는 그런 면을 해소하기 위해 기계적으로 생성하는 작업을 안보이는 곳에서 해주고 있다고 한다. 그 예로 생성자나 프로퍼티의 접근자 등이 있을 것이다.

하지만 그 외에도 Kotlin 에서는 더 많은 기능이 있다고 한다. data class 데이터 클래스와 클래스 위임 패턴이다.


데이터 클래스

Kotlin 에서는 equals, hashCode, toString 을 오버라이드할 필요없이, data 라는 변경자를 통해서 손쉽게 구현 가능하다.

data class User(
    val name: String
)

data 변경자 역할

  • 인스턴스 간 비교를 위한 equals()
  • HashMap 과 같은 Hash 기반 컨테이너에서 키로 사용할 수 있는 hashCode()
  • 클래스의 각 필드를 선언 순서대로 표시하는 문자열 만들어주는 toString()

데이터 클래스의 특징

  • 모든 프로퍼티를 읽기 전용으로 하고, 데이터 클래스 자체를 불변 클래스로 만드는 것이 권장 사항이다.
  • 구조 분해를 통해 데이터 클래스의 객체 활용성을 높여준다. (구조 분해는 7장에서 다룬다.)
  • 불변 클래스인 데이터 클래스의 활용성을 높이기 위해 copy() 메소드를 생성해준다.
copy() 메소드

copy() 메소드는 객체를 복사하면서 일부 프로퍼티를 변경할 수 있는 메소드이다.

data class DataClassUser(
    val name : String,
    val age: Int
)

fun main() {
    val user1 = DataClassUser(name = "JIM", age = 32)
    val user2 = user1.copy(age = 33)
    println(user1)      // DataClassUser(name=JIM, age=32)
    println(user2)      // DataClassUser(name=JIM, age=33)
}

클래스 위임 by

객체지향적인 시스템의 단점 중에는 상속이 깨질 수도 있다는 점이 있다. Kotlin 에서는 이런 단점을 좀 더 유연하게 해결하기 위해 자식 클래스의 역할을 위임하는 데코레이션 패턴의 역할을 하는 by 키워드 사용법이 있다.

데코레이션 decorator 패턴
상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들면서, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 패턴

클래스를 상속할 경우, 해당 클래스의 멤버 프로퍼티를 모두 오버라이드하게 된다. by 키워드로 클래스의 역할을 위임하면 오버라이드를 굳이 하지 않아도 된다.

class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList()
) : Collection<T> by innerList

object 키워드 다양한 역할

Kotlin 에서 object 라는 키워드는 크게 3가지 역할을 할 수 있다.

  • 객체 선언 object declaration 할 때는, 싱글턴을 정의하는 방법 중 하나로 사용한다.
  • 동반 객체 companion object 는 인스턴스 메소드가 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 사용한다.
  • 객체 식은 Java 의 무명 내부 클래스 대신 무명 객체 anonymous object 사용한다.

손쉬운 싱글턴 클래스 구현: object

싱글턴 이란 패턴은 최초 한번만 인스턴스를 생성하고 메모리에 할당하여 공유하는 디자인 패턴이다. 객체지향 시스템에서 자주 보이는 패턴 방식이면서 Java 에서도 많이 볼 수 있는 패턴이다.

Kotlinobject 키워드를 사용하면 Java 에서 싱글턴 클래스를 만드는 방법보다 훨씬 쉽게 만들 수 있다.

class Person(val name: String)

object People {
    val people = arrayListOf<Person>()

    fun printName() {
        people.map(Person::name).also { println(it) }
    }
}

fun main() {
    People.people.add(Person("JIM"))
    People.people.add(Person("KIM"))
    People.printName()    // [JIM, KIM]
}

object 의 몇가지 특징

  • 일반 객체를 사용할 수 있는 곳에는 항상 싱글턴 객체를 사용할 수 있다.
  • 클래스 안에서 객체를 선언할 수 있고, 그런 객체도 인스턴스는 단 하나뿐이다.

동반 객체: companion object

Kotlin 에서는 Javastatic 키워드를 지원하지 않는다. 따라서 클래스 안에서 정적인 멤버를 가질 수 없게 된다.

그에 대안으로 패키지 수준의 최상위 함수와 객체 선언을 할 수 있지만, 그런 경우 특정 클래스 안에 있는 private 비공개 멤버에 접근할 수 없는 문제가 생긴다. 그래서 클래스의 인스턴스와 관계없이 호출될 수 있지만, 클래스 내부 정보에 접근할 수 있는 멤버 함수를 정의해햐 하는 팩토리 함수를 Kotlin 에서는 특별한 키워드로 지원하고 있다. 바로 companion object 이다.

companion 키워드

class A {
    companion object {
        fun bar() = println("Companion object called")
    }
}

A.bar()       // Companion object called

companion 키워드는 해당 클래스의 동반 객체로 만들 수 있게 한다. 이런 동반 객체는 Java 의 정적 메소드와 정적 필드 사용과도 같다.

동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. (팩토리 패턴의 메소드를 작성하기 용이하다.)

class User private constructor(val nickName: String) {
    companion object {
        fun newNickName(email: String) = User(email.substringBefore('@'))
    }
}

val user = User.newNickName("jimmyberg.kim@gmail.com")
println(user.nickName)      // jimmyberg.kim

동반 객체의 특징

  • 동반 객체는 클래스 안에 정의된 일반 객체다.
  • 동반 객체에 이름을 지정할 수 있다.
  • 동반 객체가 인터페이스를 상속할 수 있다.
  • 동반 객체 안에 확장 함수와 프로퍼티를 정의 가능하다.

객체 식: 무명 내부 클래스를 다른 방식으로 작성

object 키워드는 싱글턴과 같은 객체를 생성할 때 뿐만 아니라, 무명 객체 anonymous object를 정의할 때도 사용한다.

무명 객체는 Java 에서의 무명 클래스를 대신하지만, 아래와 같은 차이점이 있다.

  • 여러 인터페이스를 구현 가능하다.
  • 클래스를 확장하면서 인터페이스 구현 가능하다.
  • final 이 아닌 변수도 객체 식 안에서 사용 가능하다.
fun countClicks(window: Window) {
    var clickCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }
    })
}

출처