Kotlin in Action - 4장

김정욱·2021년 9월 29일
0

Kotlin

목록 보기
3/5
post-thumbnail

해당 글은 Kotlin in Action 도서를 읽으며 정리한 내용입니다

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

클래스 계층 정의

[ 코틀린 인터페이스 ]

  • Kotlin은 클래스와 인터페이스에 대해 기본적으로 final / public을 제공
  • Kotlin의 인터페이스는 추상 메소드 뿐 아니라 구현이 있는 메소드도 정의 가능
    • Java 8의 디폴트 메소드와 유사
    • Java의 default 처럼 특별한 키워드를 붙히지 않는다
  • Kotlin에서 상속 / 구현시 콜론(:)을 통해서 표기한다
  • Kotlin에서 오버라이드 시 override 변경자를 통해 함수나 프로퍼티 앞에 지정
    • Java와 다르게 override시 변경자를 꼭 사용해야한다
  • 여러 인터페이스를 구현시 메소드가 겹친다면 ?
    • 어느 쪽도 선택되지 않는다
    • 해당 메소드를 오버라이드(override) 하지 않으면 컴파일러 오류가 발생
interface Clickable {
  fun click()
  /* interface 내부에 구현이 있는 메소드 */
  fun showOff() = println("I'm clikable!")
}

interface Focusable {
  fun setFocus(b : Boolean) = println("I ${if (b) "got" else "lost"} focus")
  /* interface 내부에 구현이 있는 메소드 */
  fun showOff() = println("I'm focusable!")
}

/* Clickable과 Focusable에 showOff라는 겹치는 구현 메소드가 존재 */
class Button : Clickable, Focusable {
  override fun click() = println("I was clicked")
  /* 겹치는 구현 메소드는 반드시 override를 통해 구현해야 컴파일러 오류가 발생하지 X */
  override fun showOff() {
  /* 상위 타입의 showOff() 메소드는 super<>를 통해서 호출 */
    super<Clickable>.showOff()
    super<Focusable>.showOff()
  }
}

[ open, final, abstract 변경자 : 기본 final ]

  • open 변경자
    • Java에서는 final키워드를 통해 클래스의 상속을 명시적으로 금지 했다
    • Kotlin에서는 클래스, 메소드에 대해 기본적으로 final이 설정된다
    • 이 때, open 변경자를 통해서 클래스, 메소드를 상속받게 할 수 있다
    • 취약한 기반 클래스(fragile base class) 문제
      • 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로서 깨지는 문제
      • 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드하는 위험
      • 이러한 문제 예방을 위해, Kotlin에서는 기본적으로 final을 유지
    • 상속을 허용하려면 해당 클래스(class)와 메소드(method) 모두에게 open 변경자를 붙혀야 한다
    • 주의
      • 추상 메서드에 대해서는 기본적으로 open되어 있어서 바로 구현하면 된다
      • 즉, 인터페이스(interface)의 경우 모두 기본 open 변경자가 있음을 기억하자
      • 그리고 더 이상 오버라이드(override)를 막기 위한 용도로 final을 사용할 수도 있다
/* interface는 기본적으로 모든 메소드, 프로퍼티가 open */
open class RichButton : Clickable {
  fun disable() {} // 해당 함수는 기본으로 final -> 오버라이드 불가
  open fun animate() {} // open 변경자를 통해 -> 오버라이드 가능
  /* 이미 오버라이드 한 함수는 여전히 열려있다.
     만약 더 이상의 오버라이드를 막으려면 final 키워드를 사용해야 한다 */
  override fun click() {} 
}


/* 추상 클래스 */
abstract class Animated {
  /* abstact로 선언한 함수인 추상 함수는 반드시 상속받고 구현 필요 */
  abstract fun animate()
  /* 추상 클래스 내부에 있는 비 추상 함수는 기본적으로 final */
  open fun stopAnimating() {}
  fun animateTwice() {}
}
  • 상속 제어 변경자 정리 (class / method / property)
변경자붙은 멤버설명
finaloverride 불가능클래스 멤버의 기본 변경자
openoverride 가능명시해야 오버라이드 가능
abstractoverride 필수추상 클래스 멤버에만 적용
overrideoverride 하는중override된 멤버는 기본적으로 open

[ 가시성 변경자 : 기본적으로 public ]

  • 가시성 변경자(접근 변경자)
    • Java와 다르게 internal 이라는 접근 변경자가 추가됨
    • 최상위 선언을 할 때 private을 사용할 수 있다
    • Kotlin에서 protected는 해당 클래스 혹은 상속 클래스 에서만 사용 가능
    • (중요) 클래스를 확장한 함수에서는 private / protected 멤버를 사용할 수 없다
변경자클래스 멤버최상위 선언
public모든 곳에서 가능모든 곳에서 가능
internal같은 모듈 안에서만 가능같은 모듈 안에서만 가능
protected하위 클래스 안에서만 가능(최상위 선언에 적용할 수 X)
private같은 클래스 안에서만 가능같은 파일 안에서만 가능

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

  • sealed 변경자
    • 동일 파일에 정의된 하위 클래스 외에 다른 하위 클래스는 존재하지 않는다는 것을 컴파일러에게 알려주는 기능
    • when에서 sealed 클래스를 처리할 때에는 불필요한 분기가 사라진다
/* sealed 변경자를 통해서 클래스를 봉인 */
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.right) + eval(e.left)
    // else -> throw(...) 구문이 사라짐! 더이상 하위 클래스가 없기 때문에
  }

뻔하지 않은 생성자와 프로퍼티

  • Kotlin에서는 주 생성자와 / 부 생성자가 존재
  • Kotlin에서는 초기화 블록 을 통해서 초기화 로직을 추가할 수 있다

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

  • 주 생성자
    • 클래스 이름 뒤에 괄호()로 둘러 싸인 형태의 생성자
    • 생성자 파라미터를 지정하고, 파라미터로 초기화되는 프로퍼티를 정의한다
    • 많이 생략해서 쓰지만, constructor 라는 키워드를 사용
/* init 키워드를 통한 초기화 블록 + 주 생성자 방법
   --> 가장 풀어 쓴 원시적인 방법 */
class User constructor(_nickname: String) {
  val niuckname: String
  init {
    nickname = _nickname
  }
}

/* 프로퍼티 선언과 초기화 과정을 같이 한 제일 실용적인 방법
   --> constructor 키워드는 주 생성자에서는, 일반적으로 생략한다
   --> 주 생성자에 val, var키워드를 통해 초기화를 단축할 수 있음
   --> default value도 지정할 수 있다 */
class User constructor(val nickname: String = "hue") {
...
}
  • 상속, 구현을 한 클래스에서 기반 클래스의 생성자를 호출할 수 있다
    • 일반 클래스 상속시 값을 넘기지 않는다면, 빈 괄호 필요 -> User()
    • 인터페이스를 구현할 때에는 생성자가 존재하지 않으므로 괄호가 없다 -> TwitterUser : User
    • 추가적으로, 외부에서 생성을 막기위해 private 변경자를 통해 생성자를 막을 수 있다
open class User(val nickname: String ) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

/* private 변경자를 통해 생성자 막기 */
class Secreative private constructor() { ... }

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

  • 부 생성자
    • 주 생성자 외에 추가적인 생성자가 필요할 경우 생성
    • constructor 키워드를 명시해서 생성
    • super 키워드를 통해 상위 클래스 생성자를 호출할 수 있다
    • this 키워드를 통해 자신의 다른 생성자( or )를 호출할 수 있다
    • 주 생성자와 부 생성자가 둘 다 있는 경우
      => 반드시, 부 생성자에서 주 생성자를 호출해야 한다 (아니면 컴파일 오류 발생)
      => 왜냐하면, 주 생성자에 필수 값을 명시하기 때문에, 부 생성자를 통한 생성시 이것들을 놓치지 않기 위함
    • 보통 주 생성자에 default value를 지정해서 부 생성자를 여러개 만들지 않는 것을 권장
  class Button(var text: String){
      /* 부 생서자는 반드시 주 생성자를 호출해야 한다 */
      constructor(id: Int, text: String) : this(text){
          println(id)
          this.text = text
      }
  }
  val b = Button(1, "hue")
  println(b.text)

[ 인터페이스(interface) 내부에 있는 프로퍼티(property) 구현 ]

  • 필수적으로 override 키워드를 사용해서 생성
  • 추상 프로퍼티인 경우 뒷받침하는 필드게터 등의 정보가 없다
  • 상태를 저장할 필요가 있다면 하위 클래스에서 상태 저장을 위한 프로퍼티를 만들어야 한다
    (var이나 val로 선언 후 값을 초기화 하면, 뒷받침하는 필드를 생성해서 자동으로 getter가 만들어짐)
/* interface */
interface User {
    val nickname: String
}

/* 주 생성자를 통해 override하면 뒷받침하는 필드가 생성 */
class PrivateUser(override val nickname: String) : User{}

/* 커스텀 getter를 별도로 만들어준 모습 */
class SubscribingUser(val email: String) : User{
    override val nickname: String
      get() = email.substringBefore('@')
}
/* 뒷받침하는 필드에 특정 값을 넣어둔 뒤 꺼내 사용하는 모습 */
class FacebookUser(val accountId: Int) : User{
    override val nickname = "특정 값"
}

val pu = PrivateUser("hue1")
println(pu.nickname) // hue1

val su = SubscribingUser("hue2@kakaocorp.com")
println(su.nickname) // hue2

val fu = FacebookUser(8)
println(fu.nickname) // 특정 값

[ 뒷받침하는 필드가 없는 경우 ]

  • interface 내부에 뒷받침하는 필드가 없는 경우에는 필수적으로 오버라이드 하지 않아도 된다
interface User {
  val email: String
  /* nickname은 email로부터 바로 값을 반환하므로, 뒷받침 하는 필드가 없다
     => 즉, User 구현시 nickname은 필수적으로 오버라이드 하지 않아도 된다 */
  val nickname: String
    get() = email.substringBefore('@')
}

[ 게터와 세터에서 뒷받침하는 필드에 접근 => $field ]

  • 프로퍼티 중, 뒷받침하는 필드를 가지는 경우 field 이름의 변수로 값에 접근할 수 있다
  • getter에서는 field값을 읽기만 가능 / setter에서는 field값을 읽거나 쓸 수 있다
  • 추가적으로 getter와 setter에 가시성을 변경할 수도 있다
    (public, internal, protected, private)
class User(val name: String){
  var address: String = "unspecified"
    internal set(value: String) {
      println("${field} -> ${value}")
      field = value
    }
}

val u = User("hue")
u.address = "kakao" // unspecified -> kakao

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

  • Kotlin은 equals, hashCode, toString 처럼 기계적인 생성이 필요한 작업을 보이지 않는 곳에서 처리해준다
  • 결과적으로 data 키워드를 통한 데이터 클래스를 통해 이러한 메소드 생성을 컴파일러에게 위임할 수 있다

[ toString() ]

  • 인스턴스의 문자열 표현을 얻기 위한 메소드
  • 주로 디버깅과 로깅시 이 메소드를 사용
class Client(val name: String, val postalCode, Int){
  override fun toString() = "name=$name, postalCode=$postalCode"
}

val client1 = Client("hue",1234)
println(client1) // "name=hue, postalCode=1234"

[ equals() / hashCode() ]

  • equals() 와 hashCode() 오버라이딩
    • 객체를 올바르게 비교하기 위해서는 euqals()와 hashCode() 모두 오버라이딩 해야 한다
    • 왜냐하면, JVM 언어에서는 euqals()가 true를 반환하는 두 객체는 반드시 같은 hashCode() 를 반환하는 규칙이 있기 때문
    • 즉, 개발자는 직접 정의한 객체에 대해 올바른 비교를 위해 euqals()뿐만 아니라, hashCode()도 오버라이딩을 꼭 해주어야 한다
  • == 연산
    • Java
      • 원시(primitive) 타입인 경우 값을 비교하지만, 참조(reference) 타입은 주소를 비교했다
      • 즉, 객체의 값을 비교하기 위해서는 euqals()를 오버라이딩 해서 값을 비교했었다
        (이미 있는 참조 객체들은 이미 구현이 되어있고, 직접 정의하는 객체는 직접 작성)
    • Kotlin
      • == 연산을 하면, 내부적으로 equals를 호출한다
        -> 우리는 직접 정의한 객체의 euqals()만 오버라이딩 하면, ==로 객체 비교 가능!
      • === 연산을 통해서 참조 비교 가능
class Client(val name: String, val postalCode, Int){
  /* Any는 Java에서 java.lang.Object와 대응 */
  override fun equals(other: Any?) : Boolean {
    if(other == null || other !is Client)
      return false
    return (name == other.name && postalcode == other.postalCode)
  }

  /* hashCode() 오버라이드 */
  override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}

[ 데이터 클래스 ]

  • 앞서 직접 구현한 toString() / equals() / hashCode() 오버라이딩을 컴파일러에게 위임하는 클래스
  • data 변경자를 클래스 이름 앞에 붙혀 선언
  • euqals()와 hashCode()는 주 생성자에 나열된 모든 프로퍼티(property)를 고려해 자동 생성 됨
    • 주 생성자 밖에 선언된 프로퍼티는 대상에서 제외된다는 점을 꼭 기억하자!
  • 데이터 클래스는 불변 객체인 것을 권장한다
    • 불변 객체인 경우 프로그램을 추론하기 쉬워지며
    • 스레드가 사용중인 데이터를 변경할 수 없어서, 스레드 동기화 필요가 줄어든다
  • copy() 메서드
    • 코틀린 컴파일러가 데이터 클래스 인스턴스를 불변 객체로 쉽게 활용하기 위해 제공하는 편의 메소드
    • 복사본을 만들어 반환하는 메소드
/* data키워드를 통해서 데이터 클래스로 생성 */
data class Client(val name: String, val postalCode, postalCode)

[ 클래스 위임 : by 키워드 ]

  • 상속을 허용하지 않는 클래스에서 새로운 동작을 추가할 때
    • 데코레이터(decorator) 패턴 사용
      => 1. 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 정의
      => 2. 새로운 클래스가 기존 클래스와 같은 인터페이스를 제공하도록 만든다
      (기존 클래스를 데코레이터 내부에 필드로 유지)
      => 3. 새로 정의해야 하는 기능을 데코레이터의 메소드에 새로 정의
      => 4. 기존 기능이 필요하면 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달
  • by 키워드 : 기존 기능을 간편하게 위임 가능
    • 일일히 기능을 위임하는 메소드를 작성할 필요가 없어진다
    • 만약 위임하는 기능 중 변경이 필요하면 override를 통해 대체할 수 있다
/* by 키워드를 통해 기존 기능은 innterList에 위임 */
class DelegatingCollection<T> (
  innterList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}

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

  • object 키워드는 모든 경우 클래스를 정의하면서 동시에 인스턴스를 생성한다
    • 객체 선언을 통한 싱글턴(singleton) 정의
    • 동반 객체 인스턴스
    • 객체 식 (Java의 무명 내부 클래스)

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

  • 코틀린은 객체 선언 을 통해서 싱글턴을 언어에서 기본 지원
  • 객체 선언
    • 클래스 선언 + 단일 인스턴스의 선언 동시에 처리
    • object 라는 키워드 사용
    • 주 / 부 생성자 모두 존재할 수 없다
      => 생성자 호출 없이 즉시 만들어지기 때문!
/* 회사에 급여 대장은 하나만 필요하니 객체 선언으로 싱글톤으로 생성 */
object Payroll {
  val allEmployees = arrayListOf<Person>()
  fun calculateSalary() {
  ...
}

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

  • Kotlin은 static 키워드를 지원하지 않는다
    • 최상위 함수 / 객체 선언 2개를 통해 자바의 정적 메소드 역할을 거의 대신할 수 있다
    • 하지만, 최상위 함수 는 클래스의 private 멤버에 접근할 수 없다는 한계가 있다
      => 결국 클래스 내부에 객체를 만들어서 이용하게 된다
      => 이 때, 클래스 내부에 객체를 동반 객체 로 만들면, 클래스의 이름으로 바로 접근할 수 있다!
  • 동반 객체
    • companionobject 키워드를 통해, 클래스 내부에 동반 객체를 생성해서 클래스로 접근하는 것
    • 필요하다면 companion object {이름} 을 통해 이름을 부여할 수도 있음
      (기본으로 Companion 이라는 이름으로 지정됨)
    • 즉, Java에서 정적 메소드 호출이나 필드 사용과 동일하게 사용이 가능해 진다
    • 팩토리 메서드 패턴을 구현하기에 유용하다
      => 불필요하게 객체를 생성하지 않고 바로 접근해서 사용할 수 있기 때문!
      (객체의 생성을 다른 인스턴스를 통해 하는 것)
class A private constructor(val secret: String) {
   /* A클래스의 메소드 */
   fun pp() = println("Call A method")
  /* companion 키워드를 통해 동반 객체로 생성 */
  companion object {
    fun bar() : A {
        return A("hue")
    }
  }
}

/* 정적 메소드를 호출하는 것 처럼 사용
   => 팩토리 메서드 패턴처럼 대신 생성을 하게 할 수 있음 */
val a = A.bar()
a.pp()

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

  • 무명 객체를 정의할 때에도 object 키워드를 사용할 수 있다
  • 무명 객체는 싱글톤이 아니다
    (객체 식이 쓰일 때 마다 새로운 인스턴스가 생성)
/* object 키워드로 무명 객체를 생성
   => 싱글턴이 아님! */
var listener = object : MouseAdapter() {
  override fun mouseClicked(e : MouseEvent){ ... }
}

[ 요약 ]

  • Kotlin의 인터페이스도 Java와 비슷하게 디폴트 구현을 포함할 수 있고, 프로퍼티포함할 수 있다
  • 모든 Kotlin 선언은 기본적으로 final / public 이다
  • 선언이 final이 되지 않게하려면, open 을 사용해야 한다
  • internal 선언은 같은 모듈 안에서만 볼 수 있다
  • sealed 클래스를 통해 계층 확장 제한을 컴파일러에게 알릴 수 있다
  • 초기화 블록(init)부 생성자(constructor)를 활용해 클래스 인스턴스를 더 유연하게 초기화 할 수 있다
  • field 식별자를 통해 프로퍼티 접근자(getter / setter)에서 데이터를 저장하는 뒷받침하는 필드참조할 수 있다
  • data 키워드데이터 클래스를 사용하면, 컴파일러equals, hashCode, toString, copy 등의 메소드를 자동으로 생성해준다
  • by 키워드를 통한 클래스 위임은 위임 패턴을 구현할 때 필요한 성가신 코드를 줄일 수 있다
  • object 키워드를 사용해 객체 선언을 하면, 싱글턴 클래스를 정의할 수 있다
  • company 키워드를 통한 동반 객체는 Java의 정적 메소드필드 정의를 대신한다
  • 동반 객체에서도 확장 함수확장 프로퍼티를 정의할 수 있다
  • Kotlin의 객체 식은 자바의 무명 내부 클래스를 대신한다
    (object 키워드를 사용하지만, 무명 클래스의 경우 싱글턴이 X)
profile
Developer & PhotoGrapher

0개의 댓글