Link
Today
Total
10-16 22:05
Archives
관리 메뉴

초보개발자 긍.응.성

(코틀린 인 액션) 4장 클래스, 객체, 인터페이스 본문

책 정리/코틀린 인 액션

(코틀린 인 액션) 4장 클래스, 객체, 인터페이스

긍.응.성 2022. 7. 20. 01:54
반응형

4장 클래스, 객체, 인터페이스

4.1 클래스 계층 정의

코틀린 인터페이스

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!")
}

class Button: Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun showOff() = super<Clickable>.showOff()
}

fun main() {
    Button().showOff() // I'm clickable!
}
  • 인터페이스 내 함수를 선언하여 추상 메서드를 등록한다.
  • 자바와 달리 디폴트 메서드에 대해 default 키워드가 필요하지 않다.
  • 클래 스이름 뒤에 콜론(:)을 붙이고 인터페이스와 클래스 이름을 적어 상속한다.
  • 상속하는 메서드에 대해 반드시 override 변경자를 붙여야한다.
    • 붙이지 않고 상위 클래스의 메서드와 시그니처가 같다면 컴파일 에러가 발생한다.
  • 동일한 시그니처의 메서드를 가진 두 인터페이스를 구현 시 상위 타입의 메서드를 호출하는 방식으로 오버라이드 한다.

코틀린은 자바 6과 호환되게 설계되어 인터페이스의 디폴트 메서드를 지원하지 않는다. 그렇기에 내부적으로 코틀린 디폴트 메서드는 일반 인터페이스와 디폴트 메서드 구현이 정적 메서드로 들어있는 클래스를 조합해 구현한다. 그렇기에 자바에서 디폴트 메서드가 있는 코틀린 인터페이스를 구현한다면, 디폴트 메서드 들에 대해 재정의 해주어야 한다.

public class Moveable implements Clickable {

    @Override
    public void click() {
        System.out.println("I'm moveable");
    }

    @Override
    public void showOff() {
        Clickable.**DefaultImpls**.showOff(this);
    }
}
  • DefaultImpls 를 통해 디폴트 메서드를 호출해준다.

open, final, abstract 변경자(접근제어 변경자): 기본적으로 final

취약한 기반 클래스(fragile base class)

  • 기반 클래스에 대해 가졌던 가정이 하위 클래스가 기반 클래스를 변경함으로써 깨져버릴 수 있는 상태의 클래스.
  • 자바에선 final 키워드를 통해 상속을 방지한다.

open(열린) 클래스

open class RichButton: Clickable {
    fun disable() {} // 상속불가
    open fun animate() {} // 상속가능
    final override fun click() {} // 오버라이드 한 메서드이며 상속불가
}
  • 코틀린의 클래스와 메서드는 기본적으로 final이다.
  • open 변경자를 통해 상속과 오버라이드를 허용한다.
  • 오버라이드한 메서드는 기본적으로 open 되어있다. final 키워드를 통해 오버라이드를 금지할 수 있다.

abstract(추상) 클래스

abstract class Animated {
    abstract fun animate() // 구현 필수
    open fun stopAnimating() {} // 상속 가능
    fun animateTwice() {} // 상속 불가
}
  • abstract 변경자를 통해 추상 클래스, 추상 메서드를 선언한다.
  • 추상클래스의 비추상 함수는 타 메서드와 동일하게 기본적으로final 이며, open 변경자로 오버라이드를 허용할 수 있다.

상속 제어 변경자 정리

변경자 설명
final 오버라이드 불가. 클래스 멤버의 기본 변경자이다.
open 오버라이드가 가능하다
abstract 오버라이드 필수. 구현이 있으면 안된다.
override 오버라이드 가능. 기본적으로 열려있으며, final 키워드를 통해 오버라이드를 금지 할 수 있다

가시성 변경자(visibility modifier): 기본적으로 공개

  • 가시성 변경자를 통해 클래스 외부 접근을 제어한다.
  • 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용하기에, 자바에 존재하던 default(package-private) 변경자가 사라졌다.
  • 코틀린은 모듈 내부에서만 볼수있는 변경자로 internal 이 존재한다.
    • 모듈은 한 번에 한꺼번에 컴파일 되는 코틀린 파일들을 의미한다.
  • 변경자를 명시하지 않을 시 기본적으로 public 으로 동작한다.
  • 최상위 선언에 private 선언이 가능하며 해당 파일 내부에서만 접근 가능하다.
  • 코를린의 protected는 패키지와 무관하며 상속 관계에서만 접근할 수 있다.
변경자 클래스멤버 최상위 선언
public 모든 곳에서 볼 수 있다 모든 곳에서 볼 수 있다
internal 같은 모듈 안에서만 볼 수 있다 같은 모듈 안에서만 볼 수 있다
protected 하위 클래스 안에서만 볼 수 있다 (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다 같은 파일 안에서만 볼 수 있다
internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk");
}
fun TalkativeButton.giveSpeech() {
    yell()
    whisper()
}
  • 코틀린은 상대적으로 더 낮은 가시성 변경자는 참조하지 못한다.
  • 확장함수 givenSpeech 는 기본 변경자 public으로 internal 타입인 TalkativeButton을 참조하지 못한다.
  • 메소드 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다

코틀린의 가시성 변경자와 자바

내부적으로 코틀린 소스가 컴파일되며 자바의 바이트코드로 변환된다.

  • 코틀린 private 클래스는 자바의 public 클래스로 만들어진다. (자바는 private 클래스를 만들수 없다).
  • 코틀린의 internal는 자바의 public 접근자로 컴파일 되는데, 이는 자바에서 타 모듈이라도 해당 클래스에 접근하게 하여 internal의 의미를 해칠 수 있다. 하지만 코틀린 컴파일러는 internal 멤버의 이름을 mangle 하여 자바에서 접근을 꺼려지도록 한다.

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

interface State: Serializable
interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}

class Button: View {
    override fun getCurrentState(): State = ButtonStatus()
    override fun restoreState(state: State) {}
    class ButtonStatus: State {}
}
  • 코틀린의 내부 클래스는 기본적으로 중첩 클래스이다(자바의 static class).
  • 일반 내부 클래스 정의를 위해선 inner 변경자를 붙여준다.
class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}
  • this@'outerClassName'을 통해 내부 클래스에서 바깥 클래스 참조에 접근 가능하다

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

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 Expr.Num -> e.value
    is Expr.Sum -> eval(e.left) + eval(e.right)
}
  • sealed 변경자는 상위 클래스를 상속한 하위 클래스의 종류를 제한한다.
  • 컴파일러에서 sealed 클래스의 자식 클래스가 어떤 것이 있는지 알 수 있다.
  • sealed 클래스는 open이다.

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

  • 주 생성자: 클래스를 초기화 할 때 주로 사용하는 간략한 생성자
  • 부 생성자: 클래스 본문안에서 정의
  • 초기화 블록

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

class User(val nickname: String) // 주 생성자
// 아래는 명시적으로 풀어낸 생성자 선언이다

class User constructor(_nickname: String) {
    val nickname: String
    init {
        nickname = _nickname
    }
} 
  • 주 생성자는 괄호만으로 이루어진 코드를 의미한다.
  • 생성자 파라미터를 지정하고 프로퍼티를 정의할 수 있다.
  • constructor 키워드는 생성자를 정의하기 위해 사용한다.
  • init 키워드를 통해 초기화 블록을 지정하며, 클래스 내에 여러 초기화 블록이 존재할 수 있다.
class User(val nickname: String, val isSubscribed: Boolean = true)
  • 생성자 파라미터에 대한 디폴트 값도 지정할 수 있다.
  • 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다.
open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

open class Button
class RadioButton: Button()
  • 기반(상위) 클래스가 있다면, 하위 클래스는 기반 클래스의 생성자를 호출한다.
  • 인자가 없는 디폴트 생성자를 만들 수 있으며, 상속한 하위 클래스는 빈 생성자를 호출해야한다.
class Secretive private constructor() {}
  • 비공개로 주 생성자를 만들 수 있다.
  • 외부에서 인스턴스화 할 수 없다.
  • 싱글턴, 유틸 클래스의 경우 인스턴스화를 방지하기 위해 private생성자를 사용할 때가 있다, 코틀린 언어에서 지원하는 방식으로 풀어낼 수 있다.
    • 정적 유틸리티 함수 → 최상위 함수
    • 싱글턴 → 객체 선언

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

open class View {
    constructor(ctx: Context) { ... }
    constructor(ctx: Context, attr: AttributeSet) { ... }
}
class MyButton: View {
    constructor(ctx: Context) : super(ctx) {
        // ... 
    }
    constructor(ctx: Context, attr: AttributeSet)
        : super(ctx, attr) {
        // ...
    }
}
  • 부 생성자는 클래스 내부에 constructor 키워드를 통해 정의할 수 있다.

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

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)

    private fun getFacebookName(accountId: Int): String = { ... }
}
  • 주 생성자에 추상 프로퍼티를 오버라이드 하여 구현할 수 있다.
  • 커스텀 게터를 통해 프로퍼티를 설정할 수 있다.
    • 게터를 통한 호출 시 매번 계산이 이루어 진다.
  • 초기화 식을 통해 필드에 값을 저장하여 구현하였다.
    • 초기화 식을 통해 지정한 데이터는 프로퍼티의 backing field에 저장되기에 게터 호출마다 계산되지 않는다.

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

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
        Address was changed for $name:
        "$field" -> "$value"""".trimIndent())
            field = value
        }
}
  • field 라는 식별자를 통해 뒷받침하는 필드를 참조한다
  • 게터에서는 값을 읽을 수만 있고, 세터에서는 쓰기까지 가능하다.
  • 뒷받침 필드는 기본적으로 생성 되지만, 커스텀 접근자에 구현 시 field 를 사용하지 않으면 존재하지 않는다.

접근자의 가시성 변경

class LengthCounter {
    var counter: Int = 0
        private set // 클래스 밖에서 이 프로퍼티의 값은 바꿀 수 없다

    fun addWord(word: String) {
        count += word.length
    }
}
  • 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다
  • 가시성 변경자를 추가하여 접근자의 가시성을 변경할 수 있다.

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

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

  • equals, hashCode, toString 과 같이 기계적으로 구현할 수 있는 메서드들이 존재한다.
  • 코틀린의 == 연산자는 참조 동일성이 아닌 객체의 동등성을 검사한다(내부적으로 equals호출)
  • === 연산자를 통해 참조 비교가 가능하다.

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

data class Client(val name: String, val postalCode: Int)
  • data 클래스로 정의하면 equals, hashCode, toString, copy 메서드를 컴파일러가 자동으로 만들어준다.
  • equals: 모든 프로퍼티 값의 동등성 확인.
  • hashCode: 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시값 반환.
  • copy: 일부 프로퍼티를 변경하며 객체를 복사하는 메서드.

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

class CountingSet<T>(
    val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
    var objectsAdded = 0
    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectsAdded++
        return innerSet.addAll(elements)
    }
}
  • by 키워드를 통해 인터페이스에 대한 구현을 다른 객체에 위임할 수 있다.
    • 데코레이터 패턴에서 위임하는 코드를 언어에서 지원한다.
  • 위임하지 않을 메서드는 직접 오버라이드하여 구현할 수 있다.

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

코틀린의 object 키워드는 클래스를 정의하면서 동시에 인스턴스를 생성한다.

  • 객체 선언(object declaration)은 싱글턴을 정의하는 방법 중 하나다
  • 동반 객체(companion object)는 인스턴스 메서드는 아니지만 어떤 클래스와 관련 있는 메서드와 팩토리 메서드를 담을 때 쓰인다

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

object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
  • object 키워드를 통해 객체를 선언한다.
  • 객체 선언은 클래스의 정의와 인스턴스를 생성 의미한다.
  • 객체 선언엔 생성자를 명시할 수 없다(싱글턴).
  • object 명을 명시하여 싱글턴 객체를 참조한다.
object CaseInsensitiveFileComparator: Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}

data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int = 
            p1.name.compareTo(p2.name)
    }
}

val persons = listOf(Person("Bob"), Person("Alice"))
println(persons.sortedWith(Person.NameComparator))
// [Person(name=Alice), Person(name=Bob)]
  • 객체 선언도 상속은 가능하다.
  • 클래스 안에서도 객체 선언이 가능하다
Payroll.INSTANCE.calculateSalary()
  • 자바에서 INSTANCE 필드를 통해 코틀린 object 를 사용한다.

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

코틀린 언어는 static 키워드를 지원하지 않기에 클래스 내 정적인 멤버는 없다. 대신 패키지 수준의 최상위 함수와 객체 선언을 활용한다.

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("bob@gmail.com")
val facebookUser = User.newFacebooUser(4)
  • companion 키워드를 통해 클래스 내부의 객체를 동반 객체로 지정한다.
  • 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다.
  • 동반 객체의 프로퍼티나 메서드에 접근할 때 동반객체가 정의된 클래스 이름으로 접근 가능하다.
  • 바깥쪽 클래스의 private 생성자도 호출가능하기에 동반 객체가 팩토리 패턴을 구현하기 적합하다.

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

  • 동반 객체는 클래스 안에 정의된 일반 객체이다.
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object Leader: JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person = ...
    }
}
  • 동반 객체에 이름을 붙일 수 있다.
  • 이름을 명시 하지 않을 시 동반 객체의 기본 이름은 Companion 이다.
  • 동반 객체도 인터페이스를 구현할 수 있다.
  • 동반 객체는 일반 객체와 비슷한 방식으로, 정적 필드로 컴파일된다.
  • 자바에서 사용 시 Companion이라는 이름으로 참조 가능하다.

코틀린 동반 객체와 정적 멤버

@JvmStatic

class Bar {
    companion object {
        @JvmStatic var barSize: Int = 0
    }
}

// Kotlin -> Java

public final class Bar {
    private static int barSize;
    public static final int getBarSize() {
        return barSize;
    }
    public static final void setBarSize(int var0) {
        barSize = var0;
    }

    public static final class Companion {
        // getter, setter
    }
}
  • @JvmStatic 애노테이션을 통해 동반 객체의 일반 멤버를 클래스의 정적 멤버로 만들수 있다.

@JvmField

class Bar {
    companion object {
    @JvmField var barSize: Int = 0
  }
}

// Kotlin -> Java

public final class Bar {
    @JvmField
    public static int barSize;
}
  • @JvmField 애노테이션을 통해 동반 객체의 일반 멤버를 getter/setter가 없는 정적 필드로 만들수 있다.

동반 객체 확장

// 비즈니스 로직 모듈
class Person(val firstName: String, val lastName: String) {
    companion object { // 빈 동반 객체 선언
    }
}

// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String): Person { // 확장 함수 선언
    ...
}
  • 동반 객체에 확장 함수를 선언하여 관심사를 분리할 수 있다.
  • 동반 객체 확장 함수를 위해 동반객체를 선언한다.
  • ? 동반 객체의 확장함수에서는 private, protected 필드에 접근이 가능할까?

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

fun countClicks(window: Window) {
    var clickCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent?) {
            clickCount++
        }
    })
}
  • 코틀린에서 무명 객체를 정의할 때도 object 키워드를 쓴다. 대신 이름을 명시하지 않는다.
  • 무명 객체는 자바에서 무명 내부 클래스이다.
  • 객체 선언과 달리 무명 객체는 싱글턴이 아니다(새로운 인스턴스 생성).
  • 코틀린의 무명 클래스는 여러 인터페이스를 구현할 수 있다.
  • 자바와 달리 final이 아닌 변수도 객체식 안에서 사용할 수 있다.
반응형
Comments