Recent Posts
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- mp4fpsmod
- table not found
- 깡돼후
- PytestPluginManager
- 겨울 부산
- python
- JanusWebRTC
- k8s #kubernetes #쿠버네티스
- tolerated
- JanusWebRTCGateway
- VARCHAR (1)
- JanusWebRTCServer
- 코루틴 빌더
- JanusGateway
- taint
- Spring Batch
- vfr video
- terminal
- 오블완
- kotlin
- 티스토리챌린지
- 달인막창
- 헥사고날아키텍처 #육각형아키텍처 #유스케이스
- 자원부족
- 코루틴 컨텍스트
- pytest
- 개성국밥
- Value too long for column
- preemption #
- PersistenceContext
Archives
너와 나의 스토리
[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처럼 사용할 수 있다.
- 동반 객체의 경우 'className'.Companion.'methodName()' 형태로 접근할 수 있다.
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]
반응형
'Programming Language > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 타입 시스템 - ? / !! /.? / as? / let 함수 / lateinit (0) | 2021.06.17 |
---|---|
[Kotlin] 람다 식(lambda expression)과 멤버 참조 (0) | 2021.06.16 |
[Kotlin] 클래스와 인터페이스1 - 초기화(주 생성자와 부 생성자)/가시성 변경자/프로퍼티 (0) | 2021.06.14 |
[Kotlin] 함수 정의와 호출1 - 컬렉션/확장 함수/로컬 함수/문자열을 정규식으로 나누기 (0) | 2021.06.12 |
[Kotlin] 코틀린 기초2 - 스마트 캐스트, 이터레이션(for 문, 컬렉션이나 원소 검사), 예외 처리 (0) | 2021.06.11 |
Comments