관리 메뉴

너와 나의 스토리

[Kotlin] 클래스와 인터페이스2 - 데이터 클래스/클래스 위임/object 키워드 사용 본문

Programming Language/Kotlin

[Kotlin] 클래스와 인터페이스2 - 데이터 클래스/클래스 위임/object 키워드 사용

노는게제일좋아! 2021. 6. 15. 00:38
반응형

컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임

  • 코틀린 컴파일러는 데이터 클래스에 유용한 메서드를 자동으로 만들어준다.

 

모든 클래스가 정의해야 하는 메서드

  • 자바와 마찬가지로 코틀린 클래스도 toString, equals, hashCode 등을 오버라이드할 수 있다. 
  • 고객 이름과 우편번호를 저장하는 간단한 Client 클래스를 만들어서 예제에 사용하자.
class Client(val name: String, val postalCode: Int)

 

문자열 표현: toString()

  • 자바처럼 코틀린의 모든 클래스도 인스턴스의 문자열 표현을 얻을 방법을 제공한다.
  • 이 기본 구현을 바꾸려면 toString 메서드를 오버라이드해야 한다.
class Client(val name: String, val postalCode: Int){
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

fun main(args: Array<String>) {
    val client1 = Client("hororolol", 4122)
    println(client1) // output: Client(name=hororolol, postalCode=4122)
}

 

객체의 동등성: equals()

  • 서로 다른 Client 객체가 내부에 동일한 데이터를 포함하는 경우 그 둘을 동등한 객체로 간주해야 할 때가 있다.
val client1 = Client("hororolol", 4122)
val client2 = Client("hororolol", 4122)
println(client1==client2) // output: false
  • equals()를 오버라이딩해보자.
class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client)
            return false

        return name == other.name && postalCode == other.postalCode
    }
}

fun main(args: Array<String>) {
    val client1 = Client("hororolol", 4122)
    val client2 = Client("hororolol", 4122)
    println(client1 == client2) // output: true
}

 

해시 컨테이너: hashCode()

  • 자바에서 equals를 오버라이드할 때 반드시 hashCode도 함께 오버라이드해야 한다. 
  • 이유는 다음과 같다. 
val processed = hashSetOf(Client("hororolol", 4122))
println(processed.contains(Client("hororolol", 4122))) 
// output: false
  • 다음과 같은 결과가 나오는 이유는 Client 클래스가 hashCode 메서드를 정의하지 않았기 때문이다. 
  • JVM 언어에서는 "equals()가  true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다"라는 제약이 있다.
  • 즉, 위의 예에서 두 객체는 서로 다른 hashCode를 가지기 때문에 결과가 false가 된 것이다. 
    • HashSet은 원소를 비교할 때 먼저 객체의 해시 코드를 비교하고 해시 코드가 같은 경우에만 실제 값을 비교한다. 
  • 이 문제를 해결하기 위해 hashCode()를 오버라이딩해보자.
class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client)
            return false

        return name == other.name && postalCode == other.postalCode
    }

    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

fun main(args: Array<String>) {
    val processed = hashSetOf(Client("hororolol", 4122))
    println(processed.contains(Client("hororolol", 4122))) // output: true
}

 

 

데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성

  • 어떤 클래스가 데이터를 저장하는 역할만을 수행한다면 toString, equals, hashCode를 반드시 오버라이드해야 한다. 
  • 코틀린에서는 data라는 변경자를 클래스 앞에 붙이면 이러한 필요한 메서드들을 컴파일러가 자동으로 만들어준다. 
data class Client(val name: String, val postalCode: Int)

fun main(args: Array<String>) {
    val client = Client("hororolol", 4122)
    println(client)
    // output: Client(name=hororolol, postalCode=4122)

    val client1 = Client("hororolol", 4122)
    val client2 = Client("hororolol", 4122)
    println(client1 == client2)
    // output: true

    val processed = hashSetOf(Client("hororolol", 4122))
    println(processed.contains(Client("hororolol", 4122)))
    // output: true
}

 

 

데이터 클래스와 불변성: copy() 메서드

  • 데이터 클래스의 프로퍼티가 꼭 val일 필요는 없지만, 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변(immutable) 클래스로 만들라고 권장한다. 
  • HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적이다. 
  • 데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 한 가지 편의 메서드를 제공한다.
    • 그 메서드는 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드다. 
  • Client의 copy를 직접 구현하면 다음과 같다.
class Client(val name: String, val postalCode: Int){
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
    fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)
}

fun main(args: Array<String>) {
    val ho = Client("hororolol", 4122)
    println(ho.copy(postalCode = 4000)) // output: Client(name=hororolol, postalCode=4000)
}

 

 

클래스 위임: by 키워드 사용

  • 시스템이 변함에 따라 상위 클래스의 구현이 바뀌거나 상위 클래스에 새로운 메서드가 추가된다. 
    • 그 과정에서 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 작동하지 못하는 경우가 생길 수 있다.
    • 이런 문제를 인식하고 코틀린에서는 클래스를 기본적으로 final로 취급하기로 결정했다. 
    • 모든 클래스를 기본적으로 final로 취급하면 상속을 염두에 두고 open 변경자로 열어둔 클래스만 확장할 수 있다. 
    • 하지만 종종 상속을 허용하지 않은 클래스에 새로운 동작을 추가해야 할 때가 있다. 
  • 이럴 때 사용하는 일반적인 방법이 데코레이터(decorator) 패턴이다. 
    • 이 패턴의 핵심은 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스(decorator)를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다. 
    • 이때 새로 정의해야 하는 기능은 데코레이터의 메서드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 요청을 전달(forwarding)한다. 
  • 인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.
class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
  • by 키워드를 사용하지 않으면 아래의 코드처럼 모든 메서드를 다 override해줘야 한다.
class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll (elements:Collection<T>): Boolean = innerList.containsAll(elements)
}
  • by 키워드를 사용함으로써 컴파일러가 자동으로 override를 해주기 때문에 변경하고 싶은 메서드만 오버라이딩해서 사용하면 된다. 

 

 

object 키워드: 클래스 선언과 인스턴스 생성

  • object 키워드를 사용하는 상황들
    • 객체 선언은 싱글턴을 정의하는 방법 중 하나다.
    • 동반 객체(companion object)는 인스턴스 메서드는 아니지만 어떤 클래스와 관련 있는 메서드와 팩토리 메서드를 담을 때 쓰인다. 동반 객체 메서드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있다.
    • 객체 식은 자바의 anonymous inner class 대신 쓰인다.

 

객체 선언: 싱글턴을 쉽게 만들기

  • 객체지향 시스템을 설계하다 보면 인스턴스가 하나만 필요한 클래스가 유용한 경우가 많다.
  • 코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 
    • 객체 선언: 클래스 선언 + 그 클래스에 속한 단일 인스턴스의 선언
  • 객체 선언은 object 키워드로 시작한다. 
  • 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 한 문장으로 처리한다.
  • 예: 객체 선언으로 회사 급여 대장 만들기. 급여 대장은 한 회사에 하나이므로 싱글턴을 쓰자.
object Payrool {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            //...
        }
    }
}
  • 객체 선언 안에 프로퍼티, 메서드, 초기화 블록 등이 들어갈 수 있지만, 생성자는 객체 선언에 쓸 수 없다.
    • 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문이다. 
  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.
  • 클래스 안에서 객체를 선언할 수 있다.

 

 

동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소

  • 코틀린 클래스 안에는 정적인 멤버가 없다. 
  • 코틀린 언어는 자바 static 키워드를 지원하지 않는다. 
  • 그 대신 코틀린에서는 패키지 수준의 최상위 함수와 객체 선언을 활용한다.
    • 대부분의 경우 최상위 함수를 활용하는 편을 더 권장한다.
    • 하지만, 최상위 함수는 private으로 표시된 클래스에 비공개 멤버로 접근할 수 없다. 
    • 반면에 , 클래스에 중첩된 객체 선언의 멤버 함수로 정의하면 접근할 수 있다.
    • 아래의 예제 코드를 보면 이해할 수 있을 것이다.
class A(val name: String) {
    private fun returnName(): String {
        return name + "입니다"
    }
    object nestedObject {
        fun printName(): String {
            val a = A("hororolol")
            return a.returnName()
        }
    }
}
/*
fun topLevelFunc(): String {
    val a = A("hororolol")
    return a.returnName()     // returnName()이 private이므로 접근 못 함
}
*/
  • 클래스(A) 안에 정의된 object 중 하나에 companion이라는 키워드를 붙이면 그 클래스(A)의 동반 객체(companion object)로 만들 수 있다.
  • 동반 객체의 멤버 호출:
    • 동반 객체의 경우 'className'.Companion.'methodName()' 형태로 접근할 수 있다.
      • companion object에 이름을 붙이면 'className'.'companionName'.'methodName()' 형태로 접근
    • 코틀린에서는 Companion을 생략하고 사용할 수 있도록 지원해준다.
    • 이로 인해 companion object는 static이 아니지만 static처럼 사용할 수 있다.
class A(val name: String) {
    private fun returnName(): String {
        return name + "입니다"
    }

    companion object { // companion object의 경우 객체 이름 따로 지정할 필요 없음
        fun printName(): String {
            val a = A("hororolol")
            return a.returnName()
        }
    }
}

fun main(args: Array<String>) {
    println("name: " + A.Companion.printName())
    println("name: " + A.printName())
    // output: hororolol입니다
}

 

 

부 생성자를 대체: 동반 객체 안에 팩토리 클래스 정의

  • 다음의 부 생성자가 2개인 클래스를 살펴보자
class User {
    val nickname: String

    constructor(email: String) { // 부 생성자 1
        nickname = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) { // 부 생성자 2
        nickname = getFacebookName(facebookAccountId)
    }
}
  • 이를 동반 객체 안에서 팩토리 클래스를 정의하는 방식으로 변경해보자.
class User private constructor(val nickname: String) {
    // 주 생성자를 비공개로 만듦
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
    }
}
  • 이렇게 정의된 동반 객체의 메서드는 다음과 같이 호출할 수 있다.
val subScribingUser = User.newSubscribingUser("email@mail.com")
val facebookUser = User.newFacebookUser(4)

 

동반 객체에서 인터페이스 구현

interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person = ...
    }
}

 

 

 

 

 

 

출처:

- [Kotlin IN ACTION]

반응형
Comments