Kotlin in Action

[Kotlin in Action] part4_클래스, 객체, 인터페이스

지 슈 2022. 11. 11. 02:04

클래스 계층 정의

코틀린 인터페이스

코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다.

interface Clickable {
fun click()
}

이 인터페이스는 구현되어 있지 않은 click() 추상 메서드를 정의한다. 이 인터페이스를 구현하는클래스는 click()에 대한 구현을 해야한다.

class Button : Clickable {
override fun click() = println("I was clicked")
}
>>> Button().click()
I was clicked

코틀린에서는 [ 클래스 이름 : 인터페이스와 클래스 이름 ] 으로 클래스를 확장하고 인터페이스를 구현한다. 

인터페이스 구현은 한 번에 여러 개 가능하지만 클래스 확장은 한 번에 하나만 가능하다.

 

override 변경자

override 변경자는 상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메서드를 오버라이드한다는 표시이다. 

코틀린은 이 변경자를 꼭  사용해야 한다.

 

 

open, final, abstract 변경자: 기본적으로 final

자바에서는 final 키워드를 사용하여(상속이 가능한 모든)클래스를 다른 클래스가 상속할 수 있다.

기본적으로 상속이 가능하다는 점은 상속받은 클래스가 변경될 때 하위 클래스가 설정했던 가정이 어긋나기 때문에 '취약한 기반 클래스'라는 문제가 생긴다.

이 문제의 해결방법:

코틀린의 클래스와 메서드는 기본적으로 final이다. 어떤 클래스의 상속을 허용하기 위해, 또는 오버라이드를 허용하고 싶은 메서드나 프로퍼티 앞에 open 변경자를 붙인다.

 

open class RichButton : Clickable {  //이 클래스는 상속될 수 없는 open된 클래스
fun disable() {}                     //이 클래스는 오버라이드 할 수 없다.
open fun animate() {}                //이 메서드는 오버라이드 할 수 있다. open 됨
override fun click() {}              //이 함수는 (상위에서 이미 선언된)메서드를 오버라이드 한다.
}
  • 메서드를 오버라이드하는 경우 그 메서드는 기본적으로 open되어 있다.
  • 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메서드 앞에 final을 명시한다.

**기본적으로 final 인데 오버라이드하지 못하는 메서드 앞에는 final을 명시한다는 것..

final 이 명시되어있으면 open된 메서드가 아니고, final이 명시되어있지 않으면 open된 메서드이다?

  • abstract 키워드로 선언된 추상 클래스의 추상 멤버는 항상 열려(open)있다. open 변경자 명시할 필요 없다.

 

가시성 변경자: 기본적으로 공개

가시성 변경자: 클래스 외부의 접근을 제어한다. >> 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다.

  • 아무 변경자가 없는 선언은 모두 public(공개)이다.
  • 자바의 package-private(패키지 전용)은 코틀린에서 사용하지 않는다.
  • 코틀린에는 internal이 가시성 변경자 (internal: 모듈 내부에서만 볼 수 있음)
  • 최상위 선언에 대해 private 가시성(비공개 가시성) 허용 - (최상위 선언: 클래스, 함수, 프로퍼티)
  • protected 멤버는 오직 어떤 클래스, 또는 그 클래스를 상속한 클래스에서만 보인다.

 

내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스

클래스 안에서 다른 클래스를 선언할 수 있다. 단, 코틀린의 중접 클래스(nested class)는 명시적으로 요청해야 바깥 클래스 인스턴스에 대해 접근 권한이 생긴다.

 

코틀린에서 중첩 클래스를 사용해 View 구현하는 방법

class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ }
}

***중첩 클래스?

자바와 코틀린의 중첩 클래스와 내부 클래스 관계

중첩 클래스 안에는 바깥쪽 클래스에 대한 참조가 없지만

내부 클래스에는 바깥쪽 클래스에 대한 참조가 있다.

 

 

 

바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법:

class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
} // 내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야함

 

봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한

when을 사용한 코드는 꼭 디폴트 분기인 else 분기를 예외를 위해 덧붙여야 한다. 하지만 이 디폴트 분기를 추가하면 모든 경우를 제대로 검사할 수 없기 때문에 코틀린에서는 sealed 클래스를 사용한다.

상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. 

sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.

 

 

 

뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언

코틀린은 주 생성자와 부 생성자를 구분한다.

주 생성자: 클래스를 초기화할 때 주로 사용. 클래스 본문 밖에서 정의

부 생성자: 클래스 본문 안에서 정의

 

코틀린에서는 초기화 블록(initializer block)으로 초기화 로직을 추가한다.

 

클래스 초기화: 주 생성자와 초기화 블록

constructor 키워드: 주 생성자와 부 생성자를 정의할 때 사용

init 키워드: 초기화 블록을 시작할 때 사용. 주 생성자는 초기화 블록과 함께 사용

class User constructor(_nickname: String) {  // 파라미터가 하나인 주 생성자
val nickname: String
init {      //초기화 블록록
nickname = _nickname
}
}
  • 초기화 블록 안에만 주 생성자의 파라미터를 참조할 수 있다.
  • 주 생성자 파라미터 이름 앞에 val을 추가하면 프로퍼티 정의와 초기화를 간략화할 수 있다.
class User(val nickname: String)
//val은 이 파라미터에 상응하는 프로퍼티가 생성된다는 것
  • 클래스의 인스턴스를 만들려면 new키워드 없이 생성자 직접 호출
  • 클래스의 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출
  • 기반 클래스를 초기화하려면 기반클래스이름 뒤에 ()치고 생성자 인자를 넘긴다.

 

부 생성자: 상위 클래스를 다른 방식으로 초기화

생성자를 여러 개 만드는 것을 지양하지만

여러 개가 필요한 경우가 있다. 부 생성자는 constructor 키워드로 시작한다.

class MyButton : View {
constructor(ctx: Context)  //상위 클래스의 생성자 호출
: super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet)  //상위 클래스의 생성자 호출
: super(ctx, attr) {
// ...
}
}
  • super() 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출한다.
class MyButton : View {
constructor(ctx: Context): this(ctx, MY_STYLE) {
// ...
}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
// ...
}
}
//this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.
  • this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.
  • MyButton 클래스의 생성자가 파라미터 디폴트 값을 넘겨서 같은 클래스의 다른 생성자에게 생성을 위임한다.

 

인터페이스에 선언된 프로퍼티 구현

코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다. 이렇게!

interface User {
val nickname: String
}

인터페이스를 구현하는 방법:

class PrivateUser(override val nickname: String) : User  //주 생성자에 있는 프로퍼티
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore('@') )  //커스텀 게터
}
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId)  //프로퍼티 초기화 식
}
>>> println(PrivateUser("test@kotlinlang.org").nickname)
test@kotlinlang.org
>>> println(SubscribingUser("test@kotlinlang.org").nickname)
test

 

게터와 세터에서 뒷받침하는 필드에 접근

프로퍼티의 두가지 유형: 값을 저장하는 프로퍼티, 커스텀 접근자에서 매번 값을 계산하는 프로퍼티

class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:    //뒷받침하는 필드 값 읽기
"$field" -> "$value".""".trimIndent())
field = value   //뒷받침 하는 필드 값 변경하기
}
}

프로퍼티의 값을 바꿀 때는 user.address="new value"처럼 필드 설정 구분을 사용한다.

위에서 address의 게터는 필드 값을 반환해주는 게터이다.

 

접근자의 가시성 변경

get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

class LengthCounter {
var counter: Int = 0
private set    //이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없음
fun addWord(word: String) {
counter += word.length
}
}

외부에서 값을 바꾸지 못하게 내부에서만 길이를 변경하게 만드려면 세터의 가시성을 private으로 지정한다.

 

 

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

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

코틀린 클래스도 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)"
}
//Client에 toString() 구현하기
>>> val client1 = Client("Alice", 342562)
>>> println(client1)
Client(name=Alice, postalCode=342562)
//이렇게 구현된다

 

객체의 동등성: equals()

코틀린에서 ==연산자는 객체의 동등성을 검사한다.

따라서 ==연산은 equals를 호출하는 식으로 컴파일 된다. equals를 오버라이드한다.

class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client)  //"other"가 Client인지 검사 / is는 갓의 타입을 검사
return false
return name == other.name &&  //두 객체의 프로퍼티 값이 서로 같은지 검사
postalCode == other.postalCode
}
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

 

해시 컨테이너: hashCode()

코틀린은 'equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다'는 제약이 있다.

따라서 Client가 hashCode를 구현해야 해야 equals()를 불렀을 때 원하는 결과를 얻을 수 있다.

class Client(val name: String, val postalCode: Int) {
...
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
//Client에서 hashCode() 구현하기

 

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

data 변경자를 클래스 앞에 붙이면 클래스가 데이터를 저장하는 역할을 하게 할 수 있다.

이 클래스를 데이터 클래스라고 부른다.

 

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

코틀린은 데이터 클래스를 읽기 전용으로 만들어서 불변 클래스로 만드는 것을 권장한다.

copy 메서드는 데이터 클래스 인스턴스를 불변 객체로 쉽게 활용할 수 있게 한다.

이는 객체르 복사하면서 일부 프로퍼티를 변경할 수 있게 한다.

class Client(val name: String, val postalCode: Int) {
...
fun copy(name: String = this.name,
postalCode: Int = this.postalCode) =
Client(name, postalCode)
}
>>> val bob = Client("Bob", 973293)   //copy 메서드 사용방법
>>> println(bob.copy(postalCode = 382555))
Client(name=Bob, postalCode=382555)

 

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

인터페이스 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에게 위임 중이라는 것을 명시할 수 있다.

class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
  • 클래스 안의 모든 메서드 정의를 없앤다
  • 메서드의 일부 동작을 변경하고 싶은 경우 메서드를 오버라이드하면 컴파일러가 생성한 메서드는 없어진다. 기본 구현으로 충분한 메서드는 따로 오버라이드할 필요 없다.

 

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

object 키워드를 사용해서 클래스를 정의하면서 동시에 인스턴스(객체)를 생성할 수 있다.

객체 선언: 싱글턴을 정의한다.

동반 객체: 어떤 클래스와 관련 있는 메서드와 팩토리 메서드를 담을 때 쓰인다.

자바의 무명 내부 클래스 대신 객체 식을 쓴다.

 

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

자바에서는

클래스 생성자를 private로 제한하고 정적 필드에 그 클래스의 유일한 객체를 저장하는 싱글턴 패턴을 사용한다.

 

코틀린은 객체 선언을 통해 싱글턴을 지원한다.

객체 선언: 클래스 선언 과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.

object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}
  • 생성자는 객체 선언에 쓸 수 없다.
  • 객체 선언에 사용한 이름 뒤에 마침표(.)를 붙이면 객체에 속한 메서드나 프로퍼티에 접근할 수 있다.
  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.

 

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

부생성자가 여러 개 있는 클래스를 정의하는 더 유용한 방법은

팩토리 메서드를 통해서 클래스의 인스턴스를 생성할 수 있다.

class User private constructor(val nickname: String) {  //주 생성자를 비공개로 만든다
companion object {         //동반 객체를 선언한다
fun newSubscribingUser(email: String) =
User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) =  //페이스북 사용자 ID로 사용자를 만드는 팩토리 메서드
User(getFacebookName(accountId))
}
}
//부 생성자를 팩토리 메서드로 대신하기
>>> val subscribingUser = User.newSubscribingUser("bob@gmail.com")
>>> val facebookUser = User.newFacebookUser(4)
>>> println(subscribingUser.nickname)
bob
//클래스 이름으로 동반 객체의 메서드 호출할 수 있다.

 

 

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

동반 객체에 이름을 붙이거나 인터페이스를 상속하게 하거나 확장 함수와 프로퍼티를 정의하게 할 수 있다.

 

동반객체에 이름 붙이기

companion object Loader 같은 방법으로 동반 객체에 이름을 붙일 수 있다.

특별히 이름을 지칭하지 않으면 동반 객체 이름은 자동으로 Companion이 된다.

 

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

동반객체에서 인터페이스를 구현하게 할 수 있다.

override fun fromJSON(jsonText: String): Person = ...

이런 식으로 인터페이스를 구현할 수 있다.

 

동반 객체 확장

C라는 클래스 안에 동반 객체가 있고 그 동반 객체(C.Companion) 안에 func를 정의하면

외부에서는 func()를 C.func()로 호출할 수 있다.

 

 

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

무명 객체를 정의할 때도 object 키워드를 쓴다.

window.addMouseListener(
object : MouseAdapter() {        //MouseAdapter를 확장하는 무명 객체를 선언
override fun mouseClicked(e: MouseEvent) {   //MouseAdapter의 메서드를 오버라이드 한다
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
}
)
  • 코틀린 무명 클래스는 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.
  • 객체 선언과 달리 무명 객체는 싱글턴이 아니다. 객체 식이 쓰일 때마다 새로운 인스턴스가 생성된다.
  • final이 아닌 변수도 객체 식 안에서 사용할 수 있다.