BottleH Blog

Kotlin in Action - 4장 클래스, 객체, 인터페이스

    Tags

  • Kotlin
Kotlin in Action - 4장 클래스, 객체, 인터페이스 thumbnail

📖 4.1 클래스 계층 정의

🔖 4.1.1 코틀린 인터페이스

  • 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다.
    • 다만, 인터페이스에는 아무런 상태도 들어갈 수 없다.
interface Clickable { fun click() } class Button : Clickable { override fun click() = println("I was clicked") } fun main() { Button().click() }
  • 상속이나 composition에서 모두 클래스 이름 뒤에 콜론을 붙이고 인터페이스나 클래스 이름을 적는 방식을 사용한다.
  • 클래스는 인터페이스를 원하는 개수 제한 없이 마음대로 구현할 수 있지만 클래스는 오직 하나만 확장할 수 있다.
  • 오버라이드를 할 때 override 변경자를 꼭 사용해야 한다.
    • 상위 클래스에 있는 메서드 오버라이딩 방지
interface Clickable { fun click() fun showOff() = println("I'm clickable!") }
  • 디폴트 구현 가능
interface Focusable { fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.") fun showOff() = println("I'm focusable!") }
  • 한 클래스에서 두 인터페이스를 함께 구현한 후, 동일한 showOff 디폴트 구현을 선택하려면 컴파일러 에러가 발생함.
class Button : Clickable, Focusable { override fun click() = println("I was clicked") override fun showOff() { super<Clickable>.showOff() super<Focusable>.showOff() } }

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

open class RichButton : Clickable { fun disable() {} open fun animate() {} override fun click() {} }
  • 기본적으로 모든 클래스와 메서드는 final이다.
    • 클래스에 대해 하위 클래스를 만들 수 없고, 기반 클래스의 메서드를 하위 클래스가 오버라이드할 수도 없다.
  • 어떤 클래스의 상속을 허용하려면 open 변경자를 붙여야 한다.
    • 메서드나 프로퍼티 또한 마찬가지
open class RichButton : Clickable { final override fun click() {} }
  • 명시적으로 오버라이드 금지하려면 final 표시
abstract class Animated { // 추상클래스의 인스턴스 만들 수 없음. abstract val animationSpeed: Double // 추상 프로퍼티: 하위 클래스는 반드시 값이나 접근자 제공 val keyframes: Int = 20 open val frames: Int = 60 abstract fun animate() // 추상함수: 하위 클래스는 반드시 오버라이드해야함. open fun stopAnimating() {} fun animateTwice() {} }
  • 추상클래스는 인스턴스화할 수 없다.

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

  • 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
  • public, protected, private
  • 기본적으로 public
  • module 안으로만 한정된 가시성을 위해 internal 제공
    • module: 함께 컴파일되는 코틀린 파일의 집합
  • 패키지 전용 가시성 개념이 없음
internal open class TalkativeButton { private fun yell() = println("Hey!") protected fun whisper() = println("Let's talk!") } fun TalkativeButton.giveSpeech() { // 오류 yell() // 오류 whisper() // 오류 }
  • 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메서드의 시그니처에 사용된 모든 타입의 가시성은 그 메서드의 가시성과 같거나 더 높아야 함.
  • 클래스를 확장한 함수는 그 클래스의 private이나 protected 멤버에 접근할 수 없다.

🔖 4.1.4 내부 클래스와 내포된 클래스: 기본적으로 내포 클래스

  • 자바와 달리 내포 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
interface State : Serializable interface View { fun getCurrentState(): State fun restoreState(state: State) {} } class Button : View { override fun getCurrentState(): State = ButtonState() override fun restoreState(state: State) {} class ButtonState : State {} }
  • 내포된 클래스에 아무런 변경자도 없으면 자바 static 내포 클래스와 같다.
class Outer { inner class Inner { fun getOuterReference(): Outer = this@Outer } }
  • 내부 클래스에서 바깥쪽 클래스의 참조에 접근하려면 this@Outer라고 써야 한다.

🔖 4.1.5 봉인된 클래스: 확장이 제한된 클래스 계층 정의

interface Expr class Num(val value: Int) : Expr class Sum(val left: Expr, val right: Expr) : Expr fun eval(e: Expr): Int = when (e) { is Num -> e.value is Sum -> eval(e.left) + eval(e.right) else -> throw IllegalArgumentException("Unknown expression") }
  • when을 사용할 때 else는 강제이다.
sealed class Expr class Num(val value: Int) : Expr() class Sum(val left: Expr, val right: Expr) : Expr() fun eval(e: Expr): Int = when (e) { is Num -> e.value is Sum -> eval(e.left) + eval(e.right) }
  • sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스의 가능성을 제한할 수 있다.
  • 하위 클래스들은 반드시 컴파일 시점에 알려져야 하며, sealed 클래스가 정의된 패키지와 같은 패키지에 속해야 하며, 모든 하위클래스가 같은 모듈 안에 위치해야 한다.
  • 디폴트 분기(else)가 필요 없다.
  • sealed 변경자는 클래스가 추상 클래스임을 명시한다.
    • abstract를 붙일 필요가 없음.
    • 추상 멤버를 선언한 수 있음.
sealed interface Toggleable { fun toggle() } class LightSwitch : Toggleable { override fun toggle() = println("Lights!") } class Camera: Toggleable { override fun toggle() = println("Camera!") }
  • 봉인된 인터페이스도 똑같은 규칙을 따름.
  • 봉인된 인터페이스가 속한 모듈이 컴파일되고 나면 이 인터페이스에 대한 새로운 구현을 밖에서 추가할 수 없다.

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

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

class User(val nickname: String)
  • 클래스 이름 뒤에 오는 괄호로 둘러쌓인 코드를 주 생성자라고 부른다.
class User constructor(_nickname: String) { val nickname: String init { nickname = _nickname } }
  • constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다.
  • init 키워드는 초기화 블록을 시작한다.
  • 주 생성자 앞에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.
class User(val nickname: String, val isSubscribed: Boolean = true)
  • 생성자 파라미터에도 기본값을 정의할 수 있다.
open class User(val nickname: String) class SocialUser(nickname: String) : User(nickname)
  • 기반 클래스의 생성자가 인자를 받아야 한다면 클래스의 주 생성자에서 기반 생성자를 호출해야 할 필요가 있다.
  • 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 인자가 없는 디폴트 생성자를 만들어준다.
    • 기반 클래스의 이름 뒤에 빈 괄호 필요
    • 인터페이스의 경우 생성자가 없으므로 괄호가 없음.
class Secretive private constructor(private val agentName: String) {}
  • 클래스 외부에서 인스턴스화하지 못하게 막고 싶다면 private 선언

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

open class Downloader { constructor(url: String?) {} constructor(uri: URI?) {} }
  • 코틀린은 코틀린의 디폴트 파라미터 값과 이름 붙은 인자 문법을 사용하므로 부생성자가 필요할 일이 적다.
  • 상위 클래스에서 super() 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출한다.
  • this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.
class MyDownloader : Downloader() { constructor(url: String?) : this(URI(url)) constructor(uri: URI?) : super(uri) }
  • 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.
  • 부 생성자가 필요한 주된 이유는 자바 상호운영성이다.

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

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 SocialUser(val accountId: Int) : User { override val nickname: getFacebookName(accountId) } fun getNameFromSocialNetwork(accountId: Int) = "bottleh$accountId"
  • 인터페이스의 프로퍼티 구현하기
  • 인터페이스에 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
    • 위와 같은 게터와 세터는 뒷받침하는 필드를 참조할 수 없다.
interface EmailUser { val email: String val nickname: String get() = email.substringBefore('@') }
  • 하위 클래스는 email을 반드시 오버라이딩
  • nickname 상속 가능

🛠️ 함수 대신 프로퍼티를 사용할 때

  • 예외를 던지지 않는다.
  • 계산 비용이 적게 든다.
  • 객체 상태가 바뀌지 않으면 여러 번 호출해도 항상 같은 결과를 돌려준다.

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

class User(val name: String) { var address: String = "unspecified" set(value: String) { println( """ Address was changed for $name: "$field" -> "$value". """.trimIndent()) field = value } }
  • 세터에서 뒷받침하는 필드 접근
  • 접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.
  • var인 경우에는 게터나 세터 모두에 field가 없어야 한다.

🔖 4.2.5 접근자의 가시성 변경

class LengthCounter { var counter: Int = 0 private set // 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다. fun addWord(word: String) { counter += word.length } }
  • 클래스 외부에서 이 프로퍼티에 값을 쓰려하면 컴파일 시점 오류가 발생

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

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

class Customer(val name: String, val postalCode: Int)

🛠️ 문자열 표현: toString()

  • 기본 제공되는 객체의 문자열 표현은 클래스 이름과 객체의 주소를 표현함.
class Customer(val name: String, val postalCode: Int) { override fun toString() = "Customer(name=$name, postalCode=$postalCode)" }

🛠️ 객체의 동등성: equals()

class Customer(val name: String, val postalCode: Int) { override fun equals(other: Any?): Boolean { if (other == null || other !is Customer) return false return name == other.name && postalCode == other.postalCode } }
  • 코틀린에서 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다.
  • Any는 Java의 Object에 대응되는 모든 클래스의 최상위 클래스

🛠️ 해시 컨테이너: hashCode()

class Customer(val name: String, val postalCode: Int) { override fun equals(other: Any?): Boolean { if (other == null || other !is Customer) return false return name == other.name && postalCode == other.postalCode } override fun hashCode(): Int = name.hashCode() * 31 + postalCode }
  • JVM 언어에서는 hashCode가 지켜야 하는 equals()가 true를 반환하는 두 객체는 반드시 같은 hasCode()를 반환해야 한다.

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

data class Customer(val name: String, val postalCode: Int)
  • equals, hasCode, toString을 모두 포함한다.

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

class Customer(val name: String, val postalCode: Int) { fun copy(name: String = this.name, postalCode: Int = this.postalCode): Customer = Customer(name, postalCode) }
  • 불변 객체를 사용하면 프로그램에 대해 훨씬 쉽게 추론할 수 있다.
  • 데이터 클래스는 copy 메서드를 제공한다.
    • 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드

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

Decorator 패턴

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 containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements) override fun iterator(): Iterator<T> = innerList.iterator() } class DelegatingCollection<T>(innerList: Collection<T> = mutableListOf<T>()) : Collection<T> by innerList
  • 상속을 허용하지 않는 클래스에게 새로운 동작을 추가해야 할 때 쓰는 방법
  • 기존 클래스 대신 사용할 수 있는 데코레이터(새로운 클래스)를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하고 기존 클래스를 데코레이터 내부 필드로 유지하는 것
  • 새로 정의해야 하는 기능은 데코레이터의 메서드로 새로 정의
  • 기존 기능이 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 요청을 전달
  • 단점: 준비 코드가 상당히 많이 필요함.
  • 코틀린은 by 키워드를 통해 제공함.

📖 4.4 object 키워드: 클래스 선언과 인스턴스 생성을 한꺼번에 하기

object 키워드를 쓰는 상황

  • 객체 선언
  • 동반 객체
  • 객체 식
    • 자바의 익명 내부 클래스

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

object Payroll { val allEmployees = arrayListOf<Person>() fun calculateSalary() { for (person in allEmployees) { } } }
  • 객체 선언은 object 키워드로 시작
  • 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어 변수에 저장하는 모든 작업을 한 문장으로 처리
object CaseInsensitiveFileComparator : Comparator<File> { override fun compare(file1: File, file2: File): Int { return file1.path.compareTo(file2.path, ignoreCase = true) } } fun main() { println( CaseInsensitiveFileComparator.compare( File("/User"), File("/user") ) ) }
  • 일반 객체(클래스 인스턴스)를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.

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

  • 코틀린 클래스 안에는 정적인 멤버가 없다.
    • 패키지 수준의 최상위 함수와 객체 선언을 활용
    • 대부분 최상위 함수를 활용하는 편을 더 권장한다.
class MyClass { companion object { fun callMe() { println("Companion object called") } } } fun main() { MyClass.callMe() }
  • 클래스 안에 정의된 객체 중 하나에 companion이라는 특별한 표시를 붙일 수 있다.
  • 객체 멤버에 접근할 때 자신을 감싸는 클래스의 이름을 통해 직접 사용할 수 있게 된다.
fun main() { val myObject = MyClass() myObject.callMe() // error }
  • 해당 클래스의 인스턴스는 동반 객체의 멤버에 접근할 수 없다.
class User { val nickname: String constructor(email: String) { nickname = email.substringBefore('@') } constructor(socialAccountId: Int) { nickname = getSocialNetworkName(socialAccountId) } } class User private constructor(val nickname: String) { companion object { fun newSubscribingUser(email: String) = User(email.substringBefore('@')) fun newSocialUser(accountId: Int) = User(getNameFromSocialNetwork(accountId)) } }
  • 동반 객체가 private 생성자를 호출하기 좋은 위치다.
  • 바깥쪽 클래스의 private 생성자도 호출할 수 있다.
  • 즉, 팩토리 패턴을 구현하기 가장 적합한 위치가 될 수 있다.
  • 팩토리 메서드는 그 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수도 있다.

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

class Person(val name: String) { companion object Loader { fun fromJSON(jsonText: String): Person = /* */ } }
  • 필요하다면 동반객체에 이름을 붙일 수 있다.

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

interface JSONFactory<T> { fun fromJSON(jsonText: String): T } class Person(val name: String) { companion object : JSONFactory<Person> { override fun fromJSON(jsonText: String): Person = /* */ } }
  • 동반 객체도 인터페이스를 구현할 수 있다.

🛠️ 동반 객체 확장

class Person(val firstName: String, val lastName: String) { companion object { } } fun Person.Companion.fromJSON(json: String): Person { }
  • 마치 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 호출할 수 있다.
  • 동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다.

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

interface MouseListener { fun onEnter() fun onClick() } class Button(private val listener: MouseListener) { } fun main() { Button(object : MouseListener { override fun onEnter() {} override fun onClick() {} }) }
  • 객체 식을 사용해 익명 객체를 만든다.
  • 객체 선언과 같지만 이름이 빠졌다.
  • 객체 식은 익명 객체 안에서 여러 메서드를 오버라이드해야 하는 경우에 훨씬 더 유용하다.

📖 4.5 부가 비용 없이 타입 안전성 추가: 인라인 클래스

fun addExpense(expense: Int) { // 비용을 미국 달러의 센트 단위로 저장 }
  • 200엔 지출을 추가하려면 Int 타입으로만 받기 때문에 다른 의미의 값을 전달하는 것을 막을 수 없다.
  • 전형적인 해법은 Int 대신 Class를 사용하는 것
class UsdCent(val amount: Int) fun addExpense(expense: UsdCent) { // 비용을 미국 달러의 센트 단위로 저장 }
  • 위와 같이 구현하면 GC 비용이 증가하게 된다.
@JvmInline value class UsdCent(val amount: Int)
  • 인라인 클래스를 사용하면 성능을 희생하지 않고 타입 안정성을 얻을 수 있다.
  • 실행 시점에 UsdCent의 인스턴스는 감싸진 프로퍼티로 대체된다.
  • 인라인으로 표시하려면 클래스가 프로퍼티를 하나만 가져야 하며, 그 프로퍼티는 주 생성자에서 초기화돼야 한다.
  • 인라인 클래스는 클래스 계층에 참여하지 않는다.
    • 다른 클래스를 상속할 수도, 다른 클래스가 상속할 수도 없음.
    • 인터페이스 상속, 메서드 정의, 계산된 프로퍼티를 제공
Written by@BottleH
Back-End Developer

GitHub