본문 바로가기
kotlin

[Kotlin in Action] 4장: 클래스, 객체, 인터페이스

by 권성호 2023. 2. 11.

4.1 클래스 계층 정의

4.1.1 코틀린 인터페이스

  • 자바 8 인터페이스와 비슷
  • 추상메서드, 구현이 있는 메서드도 정의 가능
    • 구현 있는 메서드 == 자바 8의 디폴트 메서드
    • 자바 8 디폴트 메서드는 default 키워드를 반드시 사용해야하지만 코틀린 인터페이스는 아니다
    • Q) 많약 2개 이상의 인터페이스를 상속받은 클래스에서, 각 인터페이스가 동일한 시그니쳐의 디폴트메소드를 정의한다면?
      • 컴파일 오류가 발생 -> 명시적으로 겹치는 시그니처 메소드를 오버라이딩 해야함을 알려줌
  • 다만, 상태(필드) 는 가질 수 없음
  • 메소드 상속하는 쪽에서 override 를 필수로 붙여야함
    • override 를 강제하여 얻는 이득 -> 실수로 상위클래스의 메소드를 오버라이드하는 것을 컴파일러가 잡아줌

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

  • 자바에서 모든 메소드는 기본적으로 상속이 가능함(즉, final 이 아님)
    • 이로 인해 발생하는 문제 -> 취약한 기반 클래스
      1. 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨지는 문제
      2. 기반 클래스 입장에서 모든 하위 클래스를 분석하는 것은 불가능에 가깝기 때문에, 이런 문제를 방지하기 위해 선, 상속에 대한 엄격한 규칙을 제공할 필요가 있음
      3. 더군다나 하위클래스의 잘못된 오버라이딩으로 인해 기반클래스의 상태가 변경될 수도 있음
      4. 한마디로, 상속은 기반-하위 클래스 사이의 강결합을 유발하기 때문에 가급적 지양해야 함
        1. 기반 클래스의 캡슐화를 깨는 행위
  • 코틀린의 메서드는 기본적으로 final이다.
    • 기본적으로 상속을 지양한다는 사상으로부터 코틀린의 메서드는 기본 final로 만듦
    • 특별히 하위 클래스에서 오버라이딩하게 의도된 메서드가 아니라면 모두 final로 만들자는 사상
    • 상속을 적극적으로 사용하는 java 진영에 존재하는 프레임워크와 안 맞는 부분이 있을 수 있음
  • 코틀린에선 오버라이딩이 의도된 메서드는 명시적으로 open 변경자를 달아줘야 함
  • 오버라이딩한 메서드는 기본적으로 open이다
  • 코틀린의 abstract와 자바의 abstract는 거의 같다
    • 코틀린의 abstract 는 기본적으로 open이다
    • 추상 클래스에 구현된 비추상 메서드는 기본적으로 final이다
  • 인터페이스는 항상 open이다
    • 인터페이스는 open, final, abstract 사용 불가
  • 정리

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

  • 기본적으로 public
  • 정리

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

  • 우선 내부 <-> 중첩 차이가 뭘까?
    • 자바의 하위 class => 내부 클래스
      • 상위 클래스의 인스턴스를 통해서만 생성 가능
      • 상위 클래스 인스턴스의 참조를 항상 가지고 있음
    • 자바의 하위 static class => 중첩 클래스
      • 그냥 독립된 클래스
  • 코틀린은 기본적으로 중첩된 클래스
  • 정리
    •  

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

  • 예제 (문제상황 == 봉인된 클래스 개념이 나온 배경)
    • Expr의 구현체인 Num 과 Sum
    • Expr 의 구현체는 이것 말고도 더 늘어날 수 있고 얼마나 많은 구현체가 있는지 이 코드만 보고 알 수 없음
    • 이로 인해 항상 타입 비교 시 else 문을 넣어 부가적인 처리를 해야 함 -> 불필요한 부가 코드
    • else 이외의 모든 조건에서 실제로 모든 case를 커버하는지 컴파일 타임에는 알 수 없음
  • 위 문제에 대한 답으로 sealed 클래스가 도입됨

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

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

  • 주 생성자 목적
    1. 생성자 파라미터 지정
    2. 생성자 파리미터에 의해 초기화되는 프로퍼티 정의
  • 주 생성자의 변천사
    1. 본모습: 
    2. 1단계 생략: 
    3. 최종 생략본: 
  • 생성자도 메서드임 -> 디폴트 파라미터 지정 가능
  • 주 생성자 생략본 + 디폴트 파라미터 지정으로 인해 흔히 자바에서 여러 생성자를 오버로딩 하는 형태를 대부분 커버가능
  • 상속 상황에서의 생성자
      • 하위 클래스는 기반 클래스의 생성자를 호출해야 함
      • 위 이미지는 호출하는 방법을 보여줌(-> 생략된 형태)

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

  • 상속이 아닌 상황에선 주 생성자 + 디폴트 파라미터로 대부분의 경우가 커버되는 듯
  • 상속하는 상황에서 생성자 문법이 제약이 있다.
    • 기반 클래스를 다양한 방식으로 생성하는 방식을 주 생성자만으로 지원하지 못함
  • 따라서, 기반 클래스를 다양한 방법으로 생성하고 싶을 땐, 아래와 같이 부 생성자를 작성한다
  • 이것 말고도 불가피하게 파라미터 목록이 다른 경우에는 부 생성자를 여럿 둬야 함

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

  • 인터페이스에 프로퍼티는 선언 가능
    • 상태를 저장할 수 없기 때문에, 뒷받침하는 필드를 가질 수는 없음
    • 뒷받침하는 필드는 구현체에서 만들어야 함
  • 인터페이스 예시
interface User {
    val nickname: String
}
  • 주 생성자에서 오버라이드한 프로퍼티
class PrivateUser(override val nickname: String) : User
  • 내부적으로 getter 가 생성되고 이 getter의 뒷받침하는 필드가 만들어짐
  • 객체 생성 시 뒷받침하는 필드가 초기화되고 이후에는 접근만 가능

커스텀 게터

class SubscribingUser(val email: String): User {
    override val nickname: String 
        get() = email.substringBefore('@')
}
  • 객체 생성 시 email 이 초기화됨
  • nickname의 뒷받침하는 필드는 없고 커스텀게터를 통해 값을 얻음
  • 커스텀 게터에서는 매번 email.substringBefore('@') 코드를 실행해서 값을 리턴

프로퍼티 초기화 식

class FaceBookUser(val accountId: Int): User {
    override val nickname = getFaceBookId(accountId)
}
  • 객체 생성 시 한 번만 프로퍼티 초기화 식이 실행되어 nickname에 값이 세팅됨
  • 인터페이스에 게테와 세터가 있는 프로퍼티 선언 가능!
    • 상태(필드)만 없으면 다 된다!
interface User {
    val email: String
    val nickname: String
      get() = email.substringBefore('@')
}
  • User의 구현은 반드시 email 은 override 해야 하지만 nickname 은 옵셔널

4.2.4 게터와 세터에서 뒷받침하는 필드에 접근 -> field 키워드!

  • 위 예시처럼 게터와 세터에서 뒷받침하는 필드에 접근하기 위해 field 키워드를 사용
    • 게터에선 field 값을 읽을 수만 있고
    • 세터에선 field 값을 읽거나 변경할 수 있음

4.2.5 접근자의 가시성 변경

  • 클래스 내부에서는 변경 가능(var) 속성을 가지지만 밖에서는 읽기만 가능하도록 만드는 방법
class LengthCounter { 
  var counter: Int = 0
    private set
  
  fun addWord(word: String) {
      counter += word.length
  }
}

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

4.3.1 모든 클래스가 정의해야 하는 메서드: toString, equals, hashCode

  • sample class
class Client(val name: String, val postalCode: Int)
  1. toString
    1. 기본 제공되는 객체의 문자열 표현은 Client@b684286 형태
    2. 이 기본 구현을 변경하려면 toString method를 ovveride 해야 한다
class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
  1. equals
    1. 객체의 동일성은 == 를 통해 확인한다 <- 참조가 같은지 확인
    2. 객체의 동등성은 만드는 객체에 따라 정의하기 나름인데, 이 정의에 따라 equals 메서드를 override 한다
    3. 일반적으로 객체가 가진 모든 프로퍼티가 일치하면 동등하다는 규칙을 사용한다
  1. hashCode
    1. 자바에서는 equals를 override 하면 반드시 hashCode 도 override 해야 한다
    2. JVM 언어에서는 아래와 같은 규칙이 있다
      1. equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode를 가져야 한다
    3. java의 모든 hash container는 위 규칙을 기반으로 동작한다
    4. 즉, hash container에서 객체를 비교할 때 효율성을 위해 equals를 먼저 보지 않고 우선 hashCode를 비교해 보고
      • hashCode 가 다르면 다른 객체라 간주
      • hashCode 가 같으면 그때 equals를 호출해서 진짜 같은지 다시 확인

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

  • 어떤 클래스가 데이터를 저장하는 역할만 수행한다면, toString, equals, hashCode는 반드시 override 해야 함
  • data라는 변경자를 클래스 앞에 붙이면, 위 3개의 메서드를 포함한 여러 유용한 메소드를 자동 생성해 준다

4.3.3 클래스 위임: by

  • 일반적으로 기존 클래스의 코드를 재사용하면서 확장하는 방법에는 상속과 합성이 있다
  • 상속을 사용하면 부모-자식 강결합이 발생한다
  • 부모-자식 사이의 결합도를 낮추면서 확장하는 방법으로 데코레이터페던을 사용한다
  • 데코레이터페턴은 준비코드가 많이 필요하다
    • 기존 부모 객체의 기능을 일부만 확장할 것인데,
    • 확장하지 않는 다른 메서드들에 대해 모두 위임처리를 위한 메서드를 구현해줘야 함
  • 이러한 준비 코드를 줄여주기 위해 코틀린은 by 키워드를 제공한다
  • 예시 
     

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

  • object 키워드는 다양한 상황에서 사용됨
  • but, 클래스를 정의하면서 동시에 객체를 생성한다는 공통점이 있음

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

object Payroll {
    val allEmployees = arrayListOf<String>()

    fun calculate() {
        // todo....
    }
}
  • object라는 키워드 -> 클래스를 정의 + 그 클래스의 인스턴스를 만들어서 변수에 저장하는 작업을 내포
  • 클래스와 마찬가지로 프로퍼티, 메서드, 초기화블록이 들어갈 수 있음
  • 클래스와 달리 생성자를 가질 수는 없음
    • 선언과 동시에 만들어지기 때문에 특정 생성자를 호출하는 개념이 아니라서
  • 선언한 객체 이름에 점(.)을 찍는 형태로 내부 프로퍼티 & 메서드에 접근 가능
Payroll.caculate()	// <- 이런식으로 사용가능
  • object를 통해 만들어지는 싱글톤 객체도 상속을 지원한다
object CaseOneComparator : Comparator<File> {
    override fun compare(o1: File, o2: File): Int {
        return o1.path.compareTo(o2.path, ignoreCase = true)
    }
}

fun someMethod() {
    val files = listOf(File("/z"), File("/c"))
    files.sortedWith(CaseOneComparator)
}
  • 요런 식으로 특정 클래스를 상속받은 클래스를 싱글톤으로 만들 수 있다.
    • 클래스를 만들고 -> 객체를 생성해서 사용할 경우 object를 사용해 코드를 줄일 수 있는 경우가 있을 것 같다. 
    • 어떠한 상태 저장도 필요 없는 경우 유용하게 사용될 듯
  • 싱글톤 객체도 특정 클래스 내부에 정의할 수 있다.
    • 이렇게 되면, 클래스의 인스턴스마다 object로 선언된 객체가 생성되는 것은 아니다
data class Person(val name: String) {
    object NameComparator: Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int {
            return o1.name.compareTo(o2.name)
        }
    }
}

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

  • 코틀린에서 static을 지원 안 함 -> 정적 멤버 개념이 없음
  • 그 대신 최상위 함수 & 객체 선언을 지원
  • 최상위 함수의 경우 private으로 선언된 클래스 비공개 멤버에 접근이 불가함
  • 따라서, 클래스 인스턴스와 관계없이 호출되어야 하지만, 클래스 내부 정보에 접근해야 할 필요가 있는 메서드가 필요할 때, 클래스에 중첩된 객체 선언의 멤버함수로 정의해야 한다.
    • 그런 함수의 대표적인 예시가 팩토리 메서드임
  • 특정 클래스에 종속되는 정적 멤버와 메서드가 있다면, 그건 동반 객체 내부에 선언하는 게 좋다.
  • 특정 클래스에 독립되는 정적 메서드는 최상위함수로 만들면 될 듯

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

  • 동반 객체도 일반 객체와 동일하게 이름을 붙이거나 인터페이스 상속, 확장함수나 프로퍼티 정의가 가능하다.

동반객체에 이름 붙이기

class Person(val name: String) {
    companion object Loader {
        fun fromJson(jsonText: String): Person {
            // ...
        }
    }
}

fun test() {
    Person.Loader.fromJson(/*....*/)
}

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

interface JsonFactory<T> {
    fun fromJson(jsonText: String): T
}

class Person(val name: String) {
    companion object : JsonFactory<Person>{
        override fun fromJson(jsonText: String): Person {
            TODO("Not yet implemented")
        }
    }
}

동반객체 확장

// 비즈니스 로직 모듈
class Person(val name: String) {
    companion object {

    }
}

// 클라이언트 통신 모듈
fun Person.Companion.fromJson(json: String): Person {
    // ...
}

val p = Person.fromJson(/*...*/)

4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 지정

  • 자바에서 익명 클래스 만들던걸 object 키워드로 할 수 있다
  • 이 경우에는 싱글톤을 만들 때와 달리 변수에 대입하게 되면 여러 개의 인스턴스를 만들 수도 있다
  • 대부분 특정 클래스나 인터페이스를 상속받으면서 무명 클래스를 만든다.
val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent?) {
        super.mouseClicked(e)
    }
}

댓글