4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린에서도 자바처럼 직접 toString(), equals(), hashCode() 등을 생성해줄 수도 있지만, 코틀린에서는 이런 메소드들을 알아서 생성해주는 기능이 있다. (근데 사실 지금은 자바도 record 클래스가 나오긴 했다)
4.3.2 데이터 클래스(data class): 모든 클래스가 정의해야 하는 메소드 자동 생성
data class Client(val name: String, val postalCode: Int)
- 인스턴스 간 비교를 위한 equals
- HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
- 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString
데이터 클래스와 불변성: copy() 메소드
데이터 클래스의 프로퍼티는 val, var를 모두 허용한다. 하지만 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다. HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적이다. 데이터 클래스 객체를 키로 하는 값을 컨테이너에 담은 다음에 키로 쓰인 데이터 객체의 프로퍼티를 변경하면 컨테이너 상태가 잘못될 수 있다. 불변 객체를 주로 사용하는 프로그램에서는 스레드가 사용 중인 데이터를 다른 스레드가 변경할 수 없으므로 스레드를 동기화해야 할 필요가 줄어든다.
데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 한 가지 편의 메소드인 copy 메소드를 제공한다. 객체를 메모리 상에서 직접 바꾸는 대신 복사본을 만들며, 복사본은 원본과 다른 생명 주기를 가지고 원본에 어떠한 영향도 끼치지 않는다.
Client의 copy 메소드를 직접 구현한다면 다음과 같을 것이다.
...
fun copy(name: String = this.name, postalCode: Int = this.postalCode) =
Client(name, postalCode)
}
>>> val lee = Client("이계영", 4122)
>>> pritln(lee.copy(postalCode = 4000))
Client(name=이계영, postalCode=4000)
4.3.3 클래스 위임: by 키워드 사용
종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있다. 이럴 때 사용하는 일반적인 방법이 데코레이터 패턴이다. 이 패턴의 핵심은 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다. 이 때 새로 정의해야 하는 기능은 데코레이터의 메소드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달한다.
이런 접근 방법의 단점은 준비 코드가 상당히 많이 필요하다는 점이다. 예를 들어 Collection 같이 비교적 단순한 인터페이스를 구현하면서 아무 동작도 변경하지 않는 데코레이터를 만들 때조차도 다음과 같이 복잡한 코드를 작성해야 한다.
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 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다. 다음은 앞의 예제를 위임을 사용해 재작성한 코드다.
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
클래스 안에 있던 모든 메소드 정의가 없어졌다. 컴파일러가 전달 메소드를 자동으로 생성하며 자동 생성한 코드의 구현은 DelegatingCollection에 있던 구현과 비슷하다.
메소드 중 일부의 동작을 변경하고 싶은 경우 메소드를 오버라이드하면 컴파일러가 생성한 메소드 대신 오버라이드한 메소드가 쓰인다. 기존 클래스의 메소드에 위임하는 기본 구현으로 충분한 메소드는 따로 오버라이드할 필요가 없다.
'Kotlin' 카테고리의 다른 글
[Kotlin in Action] 5.1 람다 식과 멤버 참조 (0) | 2025.03.18 |
---|---|
[Kotlin in Action] 4.4 object 키워드: 클래스 선언과 인스턴스 생성 (0) | 2025.03.16 |
[Kotlin in Action] 4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 (0) | 2025.03.15 |
[Kotlin in Action] 4.1 클래스 계층 정의 (0) | 2025.03.15 |
[Kotlin in Action] 3.6 코드 다듬기: 로컬 함수와 확장 (0) | 2025.03.14 |
댓글