데이터 클래스
어떤 클래스가 데이터를 저장하는 역할을 수행한다면 toString
, equals
, hashCode
를 반드시 오버라이드해야 한다. 코틀린은 클래스 앞에 data 변경자를 붙이는 걸로 필요한 메소드를 컴파일러가 자동으로 만든다.
data class Client(val name: String, val postalCode: Int)
데이터 클래스는 위에 언급된 메소드 뿐만 아니라 몇 가지 유용한 메소드를 더 생성한다.
copy() 메소드
데이터 클래스의 프로퍼티는 꼭 val일 필요는 없지만 모든 프로퍼티를 val로 만들어 불변(immutable) 클래스로 만드는 것을 권장한다(HashMap 등의 컨테이너에 데이터 클래스 객체를 담을 때는 불변성이 필수). 특히 다중 스레드 환경에서 스레드 동기화를 할 필요를 줄일 수 있다.
copy() 메소드는 불변 객체를 더 쉽게 활용할 수 있게 해주는데, 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 메소드다.
fun copy(
name: String = this.name,
postalCode: Int = this.postalCode
) = Client(name, postalCode)
클래스 위임
보통 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있다. 이럴 때 사용하는 일반적인 방법이 데코레이터(Decorator) 패턴이다. 데코레이터 패턴은 상속을 허용하지 않는 클래스(기존 클래스) 대신 사용할 수 있는 새로운 클래스(데코레이터)를 만들고 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고 데코레이터 내부에 기존 클래스를 필드로 유지하는 것이다. 이 패턴의 문제점은 준비 코드가 상당히 많아진다는 것이다.
class DelegatingCollection<E> : Collection<E> {
private val innerList = arrayListOf<E>()
override val size: Int get() = innerList.size
override fun contains(element: E) = innerList.contains(element)
override fun containsAll(elements: Collection<E>) = innerList.containsAll(elements)
override fun isEmpty() = innerList.isEmpty()
override fun iterator() = innerList.iterator()
}
이런 위임(delegate)를 코틀린에서는 언어 차원에서 제공한다. by 키워드를 사용하면 인터페이스의 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.
class DelegatingCollection<E>(
innerList: Collection<E> = arrayListOf()
) : Collection<E> by innerList
메소드 중 일부의 동작을 변경하고 싶은 경우 메소드를 오버라이드하면 컴파일러가 생성한 메소드 대신 오버라이드한 메소드가 쓰인다.
원소를 추가하려고 시도한 횟수를 기록하는 컬렉션을 구현해보자.
class CountingSet<E>(
val innerSet: MutableCollection<E> = hashSetOf()
) : MutableCollection<E> by innerSet { //MutableSet의 구현을 innerSet에 위임한다.
var objectsAdded = 0
//아래 두 메소드는 위임하지 않고 오버라이드한다.
override fun add(element: E): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<E>): Boolean {
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}
>>> val countingSet = CountingSet<Int>()
>>> countingSet.addAll(listOf(1, 1, 2, 3, 4, 4))
>>> println("${countingSet.objectsAdded} objects were added, ${countingSet.size} remain")
6 objects were added, 4 remain
이 코드에서 중요한 점은 CountingSet에 MutableCollection 구현 방식에 대한 의존관계가 생기지 않는다는 것이다. 예를 들면 addAll을 처리할 때 루프를 돌면서 add를 호출할 수도 있지만 다른 방식을 구현할 수도 있다. 클라이언트에서 CountingSet의 코드를 호출할 때 발생하는 일은 CountingSet 안에서 마음대로 처리할 수 있지만 결국 이 코드는 MutableCollection 인터페이스를 갖추고 있다. 따라서 MutableCollection이 변경되지 않는 한 이 코드는 계속 잘 작동할 것임을 확신할 수 있다.
object
코틀린에서는 다양한 상황에서 object 키워드를 사용하는 데, 여기에는 클래스를 정의하면서 동시에 인스턴스를 생성하는 공통점이 있다.
object 키워드는 다음과 같은 상황에 사용된다.
- 객체 선언(object declaration) : 싱글턴을 정의하는 방법 중 하나
- 동반 객체(companion object) : 인스턴스의 메소드는 아니지만 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다.
- 객체 식 : 자바의 무명 내부 클래스(anonymous inner class) 대신 쓰인다.
객체 선언
인스턴스가 하나만 필요한 클래스가 유용한 경우가 많다. 자바에서는 보통 클래스의 생성자를 private로 제한하고 정적인 필드에 그 클래스의 유일한 객체를 저장하는 싱글턴 패턴(singleton pattern)을 사용하여 구현한다.
코틀린은 객체 선언 기능을 통해 싱글턴 패턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for(person in allEmployees) {
//...
}
}
}
객체 선언 안에도 프로퍼티, 메소드, 초기화 블록이 들어갈 수 있지만 생성자(주 생성자와 부 생성자 모두)는 객체 선언에 쓸 수 없다. 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문이다.
객체 선언에 사용한 이름 뒤에 마침표를 붙이면 객체에 속한 메소드나 프로퍼티에 접근할 수 있다.
Payroll.allEmployees.add(Person())
Payroll.calculateSalary()
객체 선언도 클래스나 인터페이스를 상속할 수 있다. 특정 인터페이스를 구현해야 하는데, 구현 내부에 다른 상태가 필요하지 않은 경우 유용하게 쓸 수 있다.
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.
>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[\a, \Z]
클래스 안에서도 객체를 선언할 수 있다.
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(o1: Person, o2: Person): Int {
return o1.name.compareTo(o2.name)
}
}
}
코틀린 객체를 자바에서 사용하기
코틀린 싱글턴 객체를 사용하려면 정적 INSTANCE 필드를 사용한다.
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
동반 객체
코틀린 클래스 안에는 정적인 멤버가 없으며 static
키워드를 지원하지 않는다. 코틀린에서는 패키지 수준의 최상위 선언(자바의 정적 메소드 역할을 거의 대신할 수 있다)과 객체 선언(최상위 함수가 대신하지 못하는 역할이나 정적 필드를 대신)을 활용한다.
클래스 안에 정의된 객체 중 하나에 companion이라는 표시를 붙이면 동반 객체로 만들 수 있다. 이렇게 하면 동반 객체의 멤버를 사용하는 구문은 자바의 정적 메소드나 정적 필드와 같아진다.
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. 바깥쪽 클래스의 private 생성자도 호출할 수 있다.
이런 특징으로 팩토리 패턴 구현에 적합하다.
class User {
val nickname: String
constructor(email: String) {
nickname = email.substringBefore('@')
}
constructor(facebookAccountId: Int) {
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))
}
}
동반 객체를 일반 객체처럼 사용하기
동반 객체에 이름을 붙이거나 인터페이스를 상속하거나 확장 함수와 프로퍼티 또한 정의할 수 있다.
이름 붙이기
data class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = //...
}
}
대부분은 이름을 짓지 않아도 참조할 수 있기 때문에 굳이 이름을 짓지 않지만 필요하다면 companion object 이름
과 같은 형식으로 이름을 지정할 수 있다.
자바에서 동반 객체에 접근하기
동반 객체에 이름을 붙이지 않았다면 자바에서는 Companion
으로 접근할 수 있다.
인터페이스 구현
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
data class Person(val name: String) {
companion object : JSONFactory<Person> {
fun fromJSON(jsonText: String): Person = //...
}
}
일반 객체의 인터페이스 상속과 동일한 방법으로 인터페이스를 상속할 수 있다.
확장
data class Person(val name: String) {
companion object
}
fun Person.Companion.fromJSON(jsonText: String): Person = //...
동반 객체 안에 확장 함수 또는 확장 프로퍼티를 정의할 수 있다.
동반 객체를 확장하려면 빈 동반 객체이더라도 꼭 원래 클래스에 동반 객체를 선언해야 한다.
객체 식
무명 객체를 정의할 때도 object 키워드를 사용한다. 무명 객체는 자바의 무명 내부 클래스를 대체한다. 자바에서 흔히 무명 내부 클래스로 구현하는 event listener를 코틀린에서 구현해보면 다음과 같다.
window.addMouseListener(
object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
super.mouseClicked(e)
}
override fun mouseEntered(e: MouseEvent?) {
super.mouseEntered(e)
}
}
)
구문은 객체 선언과 같지만 이름이 빠져있다. 객체 식은 클래스를 정의하고 클래스에 속한 인스턴스를 생성하지만, 그 클래스나 인스턴스에 이름을 붙이지 않는다. 객체에 이름을 붙여야 한다면 변수에 무명 객체를 대입하면 된다.
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {}
override fun mouseEntered(e: MouseEvent?) {}
}
코틀린의 무명 클래스는 한 클래스만 확장할 수 있는 자바의 무명 내부 클래스와는 다르게 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.
자바와 다르게 final이 아닌 변수도 객체 식 안에서 사용할 수 있다.
fun main() {
var clickCount = 0
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
clickCount++
}
override fun mouseEntered(e: MouseEvent?) {}
}
}