Kotlin :: 클래스, 객체, 인터페이스 3

1편 : Kotlin :: 클래스, 객체, 인터페이스 1
2편 : Kotlin :: 클래스, 객체, 인터페이스 2


Data 클래스

자바에서 클래스를 생성하면 반듯이 하는 3가지 함수가 있다.

  • toString
  • equals
  • hashCode 특히 equals와 hashCode는 반듯이 필요하며 둘은 함께 동반해야 하는데 자바의 규칙중에 equals가 동일한 객체는 반듯이 같은 hashCode를 반환해야 한다 때문이다. 코틀린에서는 위와 같은 필수 함수들을 data라는 키워드를 통해 한 번에 해결할 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // original
    class Client(val name: String, val postalCode: Int) {
    override fun toString(): String = "Client(name=$name, postalCode=$postcalCode)"
    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
    }

    // data class
    data class Client(val name: String, val postalCode: Int)

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

코틀린에서는 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다. (물론 프로퍼티를 val이 아닌 var로 선언해도 된다.) 그렇기 때문에 코틀린에서는 원본을 두고 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메소드를 제공하고 있다. 간단한 정의와 사용법은 아래와 같다.

1
2
3
4
5
6
7
class Client(val name: String, val postalCode: Int) {
fun copy(name: String = this.name, postalCode = this.postalCode) = Client(name, postalCode)
}

>>> val funlee = Client("funlee", 1234)
>>> println(funlee.copy(postalCode = 4444))
>>> Client(name=funlee, postalCode=4444)

클래스 위임: by 키워드

책에는 데코레이터 패턴이라고 나오나 이 부분은 오역인 것 같아 제 개인적인 견해로 해석하였습니다.

델리게이트 패턴(Delegation Kotlin)은 상속과는 다르게 유연한 모듈간의 구성을 할 수 있도록 도와주는 패턴이다. 코틀린에서는 클래스가 기본적으로 final이므로 상속이 불가능하다. 이는 상속으로 인한 복잡한 종속성을 배제하기 위함으로 코틀린의 기본 철학이기 때문에 위임이라는 델리게이트 패턴을 직접적으로 제공하고 있다.

델리게이트 패턴은 내부에 가지고 있는 객체의 멤버 함수만큼 똑같이 만들어야 하기 때문에 사전 작업이 너무 많이 필요하다. 그렇기에 코틀린은 이마저도 쉽게 할 수 있도록 by라는 키워드로 위임을 지원한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vanila delegation
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.interator()
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}

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

만약 위임받는 객체의 메소드에 다른 기능을 추가하기 위해서는 그 메소드만 오버라이드하면 된다. 오버라이드 한 메소드는 기본 메소드 대신 컴파일되어 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
): MutableCollection<T> by innerSet {
var objectAdded = 0

override fun add(element: T): Boolean {
objectAdded++
return innerSet.add(element)
}

override fun addAll(elements: Collection<T>): Boolean {
objectAdded += elements.size
return innerSet.addAll(elements)
}
}

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

싱글턴

프로그램이 실행되고 종료할 때까지 단 하나의 객체로 사용하기 위한 패턴을 싱글턴 패턴이라 하고 많은 언어에서는 이 싱글턴 패턴을 직접 구현한다. 자바에서는 생성자를 private으로 선언하고 static으로 객체를 선언한 후 해당 객체를 리턴해주는 함수를 제공한다.

코틀린에서는 thread-safe 한 싱글턴 패턴을 만들기 위한 복잡함을 없애고 object라는 키워드로 싱글턴을 구현할 수 있도록 제공하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
object Payroll {
val allEmployees = arrayListOf<Person>()

fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}

>>> Payroll.allEmployees.add(Person(...))
>>> Payroll.calculateSalary()

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

코틀린 클래스 안에는 정적인 멤버가 없다. 자바 static 키워를 지원하지 않는다는 뜻이다. 그 이유인 즉 static 키워드로 작성 된 함수는 코틀린의 최상위 함수로 대체할 수 있기 때문이다. 하지만 최상위 함수가 모두 처리할 수 있는 것은 아니다. 클래스 내부의 private 은 접근하지 못하기 때문에 결국 클래스 내부에 이와 같은 함수를 정의해야 한다.

이럴 때 사용할 수 있는 것이 바로 companion object이다. companion object로 클래스 내부에 선언하면 그 클래스의 동반 객체로 만들 수 있으며 이름을 넣기도 혹은 뺄 수도 있다. 여기서 말하는 동반 객체는 static을 대체하긴 하지만 static과는 다르다. object 클래스이므로 객체 내부에 존재하는 싱글턴 객체이다. 특히 이 동반 객체는 팩토리 메소드를 제공하는 데 있어 탁월한 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
class User private constructor(val name: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@')
fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
}
}

>>> val subscribingUser = User.newSubscribingUser("funlee@kakao.com")
>>> val facebookUser = User.newFacebookUser(4)
>>> println(subscribingUser.name)
>>> funlee

동반 객체를 일반 객체처럼 사용

위의 예제에서는 동반 객체에 이름을 사용하지 않았지만 이름도 넣을 수 있다. 말 그대로 객체이므로 선언이 가능하기 때문이다.

1
2
3
4
5
6
7
8
9
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}

>>> person = Person.Loader.fromJSON("{name: 'funlee'}")
>>> println(person.name)
>>> funlee

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

동반 객체도 객체이므로 인터페이스를 구현할 수 있으며 인터페이스를 구현한 동반 객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}

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

// 3
fun loadFromJSON<T>(factory: JSONFactory<T>): T {
...
}
loadFromJSON(Person)
  1. JSON을 역직렬화하여 객체를 생성할 수 있는 인터페이스 선언
  2. Person 클래스의 동반 객체가 JSONFactory의 fromJSON을 Person을 리턴하는 함수로 구현
  3. JSON으로부터 각 원소를 만들어내는 추상 팩토리가 있다면 Person 객체를 그 팩토리에 넘겨 객체 생성

동반 객체 확장

동반 객체도 객체이므로 확장이 가능하며 사용법은 클래스.Companion(혹은 이름).확장함수형식이다.

1
2
3
4
5
6
7
8
9
10
class Person(val firstName: String, val lastName: String) {
companion object {
}
}

Person.Companion.fromJSON(json: String) : Person {
...
}

>>> val p = Person.fromJSON(json)

그리고 중요한 것은 동반 객체에 확장함수를 선언할 땐 빈 동반 객체라도 꼭 있어야 한다는 점이다.

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

object 키워드를 싱글턴과 같은 객체를 정의하고 그 객체에 이름을 붙일 때만 사용하지 않는다. 무명 객체를 정의할 때도 object 키워드를 쓰는데 이땐 싱글턴 객체가 아닌 매번 호출 시 새로운 객체가 생성이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 바로 선언
window.addMouseListener(
object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}
)

// 객체 이름 사용
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}

출처 : Kotlin in Acation

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×