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

Sdoubleu·2023년 3월 8일
0

Kotlin in Action

목록 보기
3/9
post-thumbnail

4장에서 다루는 내용

  1. 클래스와 인터페이스
  2. 뻔하지 않은 생성자와 프로퍼티
  3. 데이터 클래스
  4. 클래스 위임
  5. object 키워드 사용
  • 코틀린의 클래스 / 인터페이스는 자바의 클래스 / 인터페이스와 다르다
    ex) 인터페이스에 프로퍼티 선언이 들어갈 수 있음

  • 코틀린 선언은 기본적으로 final이며 public이다

  • 중첩 클래스는 기본적으로 내부 클래스가 아니다
    -> 즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다

ex)
// 중첩 클래스 
class OuterClass {
	class NestedClass
}

// 내부 클래스
class OuterClass {
	inner class InnerClass
}

내부 클래스와 중첩 클래스의 특징

  • 공통점
    -> 클래스 내부에 다른 클래스로 정의됩니다.

  • 외형적 차이점
    -> inner 키워드를 쓰면 내부클래스, 안쓰면 중첩클래스입니다.

  • 기능적 차이점
    -> 중첩클래스는 외부클래스(여기서는 OuterClass)의 참조를 가지지 않지만 내부클래스는 외부클래스의 인스턴스를 참조를 가집니다.


4.1 클래스 계층 정의

  • 코틀린에서 클래스 계층을 정의하는 방식과 자바 방식을 비교

  • 코틀린의 가시성과 접근 변경자에 대해 살펴보기

  • 코틀린 가시성/접근 변경자는 자바와 비슷하지만 아무것도 지정하지 않은 경우 기본 가시성은 다름

  • sealed는 클래스 상속을 제한


4.1.1 코틀린 인터페이스

  • 코틀린 인터페이스 안에는 추상 메소드 뿐 아니라 구현이 있는 메소드 도 정의 할 수 있음 ✔️
    -> 인터페이스에는 아무런 상태(필드)도 들어갈 수 없음❌❌
// 간단한 인터페이스 선언하기
interface Clickable {
	fun click()
}
이 코드는 click()이라는 추상 메소드가 있는 interface를 정의한다
이 interface를 구현하는 모든 비추상 클래스(또는 구체적 클래스)click()에 대한 구현을 제공해야 한다!


// 단순한 인터페이스 구현하기
// 클래스 이름뒤에 콜론(:)을 붙임으로써 클래스 확장과 인터페이스 구현을 모두 처리
class Button: Clickable { 
	override fun click() = println("I was clicked")
}

>>> Button().click()
I was clicked

-> 클래스는 인터페이스를 원하는 만큼 개수 제한 없이 구현할 수 있지만, 클래스는 오직 하나만 확장 가능

  • override 변경자는 상위 (클래스 / 인터페이스)에 있는 프로퍼티나 메소드를 오버라이드 한다는 표시
    -> 실수로 상위 클래스의 메소드를 오버라이드하는 경우를 방지

  • 상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안되기 때문에 override를 붙이거나 메소드 이름을 바꿔야 함

  • 인터페이스 메소드디폴트 구현을 제공할 수 있다

// 인터페이스 안에 본문이 있는 메소드 정의하기
interface Clickable {
	fun click() // 일반 메소드 선언
    fun showOff() = println("I'm clickable") // 디폴트 구현이 있는 메소드
}

이 인터페이스를 구현하는 클래스는 click()에 대한 구현을 제공해야 함
showOff() 의 경우 새로운 동작을 정의할 수도, 디폴트 구현을 사용할 수도 있음

// 동일한 메소드를 구현하는 다른 인터페이스 정의
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")
    
    // 이름과 시그니처가 같은 멤버 메소드에 대해 둘 이상의 디폴트 구현이
    // 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로
    // 새로운 구현을 제공해야 한다
    
    // 상위 타입의 이름을 <> 사이에 넣어서 "super"를 지정하면
    // 어떤 상위 타입의 멤버 메소드를 호출할지 지정할 수 있다
    override fun showOff() {
    	super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

// 상속된 구현 중 단 하나만 호출해도 된다면 다음과 같이 사용 가능
override fun showOff() = super<Clickable>.showOff()

// 클래스의 인스턴스를 만들고 showOff() 제대로 작동하는지 확인
fun main(args: Array<String>) {
	val button = Button()
    button.showOff()
    >>> I'm clickable!
    >>> I'm focusable!
    button.setFocus(true)
    >>> I got focus.
    button.click()
    >>> I was clicked
}

↪ Button 클래스는 Focusable 인터페이스 안에 선언된 setFocus의 구현을 자동으로 상속한다

자바에서 코틀린의 메소드가 있는 인터페이스 구현하기
코틀린은 자바6와 호환되게 설계됐다
-> 인터페이스의 디폴트 메소드를 지원 ❌
코틀린은 디폴트 메소드가 있는 인터페이스를 일반 인터페이스와 디폴트 메소드 구현이 정적 메소드로 들어있는 클래스를 조합해 구현한다

인터페이스에는 메소드 선언만 들어가며, 인터페이스와 함께 생성되는 클래스에는 모든 디폴트 메소드 구현이 정적 메소드로 들어간다

디폴트 인터페이스가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해 구현하고 싶다면 코틀린에서 메소드 본문을 제공하는 메소드를 포함하는 모든 메소드에 대한 본문을 작성해야 한다
-> 자바에서는 코틀린의 디폴트 메소드 구현에 의존할 수 없다

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

  • 취약한 기반 클래스
    -> 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김
    -> 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 코딩한 사람의 의도와는 다른 방식으로 메소드를 오버라이드할 위험 존재💣

  • 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다

  • 오버라이드를 허용하고 싶은 메소드/프로퍼티 앞에도
    open 변경자를 붙여야 한다

// 열린 메소드를 포함하는 열린 클래스 정의하기

// 클래스는 열려있고, 다른 클래스가 이 클래스를 상속할 수 있다
open class RichButton: Clickable {

// 이 함수는 final. 하위 클래스가 이 메소드 오버라이드 불가능
	fun disable() {}
    
// 이 함수는 열려있다 하위 클래스에서 이 메소드 오버라이드 가능
    oepn fun animate() {}
    
// 이 함수는 ( 상위 클래스에서 선언된 ) 열려있는 메소드를 오버라이드 한다 
// 오버라이드한 메소드는 기본적으로 열려있다
    override fun click() {}
}

기반 클래스인터페이스의 멤버를 오버라이드하는 경우 그 메소드는 기본적으로 열려있다

↪ 오버라이드하는 메소드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메소드 앞에 final을 명시해야 한다

// 오버라이드 금지하기
open class RichButton: Clickable {
	final override fun click()
}

열린 캐스트와 스마트 캐스트

  • 코틀린에서도 클래스를 abstract로 선언 가능
    -> abstract로 선언한 추상 클래스는 인스턴스화할 수 없다

  • 추상 멤버 앞에 open 변경자를 명시할 필요 ❌

// 추상 클래스 정의하기

// 이 클래스는 추상클래스이므로, 인스턴스 만들 수 없다
abstract class Animated { 
	abstract fun animate()
    
    // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 final 이지만
    // 원한다면 open으로 오버라이드 허용 가능
    open fun stopAnimatin() {}
    
    fun animateTwice() {}
}
  • 인터페이스 멤버의 경우 final, open, abstract를 사용 ❌
    -> 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다

변경자이 변경자가 붙은 멤버는...설명
final오버라이드할 수 없음클래스 멤버의 기본 변경자
open오버라이드할 수 있음반드시 open을 명시해야 오버라이드 가능
abstract반드시 오버라이드해야 함추상 클래스의 멤버에만 이 변경자를 붙일 수 있음.
추상 멤버에는 구현이 있으면 안 된다
override상위 (클래스/인스턴스)의
멤버를 오버라이드 하는 중
오버라이드하는 멤버는 기본적으로 열려있다
하위 클래스의 오버라이드를 금지하려면 final
명시해야 함

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

  • 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다
    -> 어떤 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다

  • 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용
    -> 패키지를 가시성 제어에 사용 ❌

변경자클래스 멤버최상위 선언
public(기본 가시성)모든 곳에서 볼 수 있다모든 곳에서 볼 수 있다
internal같은 모듈 안에서만 볼 수 있다같은 모듈 안에서만 볼 수 있다
protected하위 클래스 안에서만 볼수 있다(최상위 선언에 적용할 수 없음)
private같은 클래스 안에서만 볼 수 있다같은 파일 안에서만 볼 수 있다
  • 모듈 :
    한 번에 한꺼번에 컴파일되는 코틀린 파일
    ex) 인텔리J, 이클립스, 메이븐, 그레이들 등의 프로젝트, 앤트 태스크

어떤 클래스의 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다

  • protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다

  • 외부 클래스가 내부/중첩 클래스의 private 멤버에 접근할 수 없다

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

  • 클래스 안에 다른 클래스 선언 가능
    -> 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드 사용하는 곳 가까이에 두고 싶을 때 유용

  • 중첩 클래스
    명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한 없음

// 직렬화할 수 있는 상태가 있는 뷰 선언
interface State: Serializable

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

// 중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button: View {
	override fun getCurrentState(): State = ButtonState()
    override fun restoreState(state: State) { }
    class ButtonState: State { }
}
클래스B 안에 정의된 클래스 A자바에서는코틀린에서는
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음)static class Aclass A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함)class Ainner class A

내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면
this@Outer 라고 써야 함

class Outer {
	inner class Inner {
    	fun getOuterReference(): 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.right) + eval(e.left)
        else ->
        	throw IllegalArgumentException("Unknown exression")
    }
    
// sealed 클래스로 식 표현하기
sealed class Expr { // 기반 클래스를 sealed로 봉인
	// 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다
	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.eval
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • 상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다

  • when 식이 모든 하위 클래스를 검사하므로 별도의 else 분기가 없어도 된다

  • sealed 클래스는 자동으로 open 변경자

sealed 인터페이스를 정의할 수 없는 이유
sealed interface를 만들 수 있다면 그 interface를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일에게 없기 때문


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

  • 코틀린은 주 생성자부 생성자를 구분한다
    -> 주 생성자:
    클래스를 초기화할 때 주로 사용하는 간략한 생성자, 클래스 본문 밖에서 정의
    -> 부 생성자:
    클래스 본문 안에서 정의

  • 초기화 블록을 통해 초기화 로직을 추가할 수 있다

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

class User(val nickname: String)
  • 클래스 이름뒤에 오는 괄호로 둘러싸인 코드를 주 생성자

  • 주 생성자 목적
    1. 생성자 파라미터를 지정
    2. 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의

class User constructor(_nickname: String) {
	val nickname: String
    init { // 초기화 블록
    	nickname = _nickname 
    }
    // 생성자 프로퍼티와 프로퍼티의 이름을 같게 하고
    // this.ninkname = nickname 으로 써도 됨
}

-----------------------------------------
// 주 생성자 앞에 별 다른 애노테이션이나 가시성 변경자가 없다면
// contructor를 생략해도 된다
class User(_nickname: String) {
	val nickname = _nickname
}

constructor와 init
constructor:
주 생성자나 부 생성자 정의를 시작할 때 사용

init:
초기화 블록을 시작

  • 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어감
  • 초기화 블록은 주 생성자와 함께 사용
  • 주 생성자는 제한적이기 때문에 초기화 블록이 필요
    -> 클래스 안에 여러 초기화 블록 선언 가능
  • 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출할 필요가 있다

  • 기반 클래스를 초기화 하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다

open class User(val nickname: String) { }
class TwitterUser(nickname: String): User(nickname) { }

-----------------------------------------------
open class Button // 인자가 없는 디폴트 생성자가 만들어진다

class RadioButton: Button()
  • Button의 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다

인터페이스에는 생성자가 없기 때문에
이름 뒤에 괄호가 붙었는지 살펴보면 기반 클래스 와 인터페이스 구별 가능
ex)
interface Expr
class Num(val value: Int): Expr

open class User(val nickname: String)
class TwitterUser(nickname: String): User(nickname)

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

  • super( ) 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출
open class View {
	// 부 생성자들
	constructor(ctx: Context) {
    }
    
    constructor(ctx: Context, attr: AttributeSet) {
    }
}

----------------------------------------------------
이 클래스를 확장하면서 똑같이 부생성자를 정의할 수 있다

open class View {
	// 부 생성자들
	constructor(ctx: Context)
    : super(ctx) {
    
    }
    
    constructor(ctx: Context, attr: AttributeSet)
    : super(ctx, attr) {
    	
    }
}
  • 생성자에서 this( ) 를 통해 클래스 자신의 다른 생성자를 호출할 수 있다
open class View {
	// 이 클래스의 다른 생성자에게 위임한다
	constructor(ctx: Context): this(ctx, MY_STYLE) {
    
    }
    
    constructor(ctx: Context, attr: AttributeSet: super(ctx, attr) {
    	
    }
}
  • 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시
    상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다

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

  • 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다
interface User {
	val nickname: String
}

↪ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다

↪ 인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 없다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다

// 인터페이스의  프로퍼티 구현하기
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('@') <- 커스텀 게터
}
// 페이스북 계정의 ID를 저장
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
  • PrivateUser는 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문을 사용
    -> 이 프로퍼티는 User의 추상 프로퍼티를 구현하고 있으므로 override를 표시 해야 한다

  • SubscribingUser는 커스터 게터로 nickname 프로퍼티를 설정
    -> 이 프로퍼티는 뒷받침하는 필드에 값을 저장 X
    매번 이메일 주소에서 별명을 계산해서 반환한다

  • FacebookUser에서는 초기화 식으로 nickname 값을 초기화

SubscribingUser와 FacebookUser의 nickname 구현 차이

  • SubscribingUser는 매번 호출될 때마다 substringBefore를 호출해 계산하는 커스텀 게터를 활용
  • FacebookUser는 객체 초기화 시 계산한 데이터를 뒷받침하는 필드에 저장했다가 불러오는 방식을 활용
  • 인터페이스에는 추상 프로퍼티뿐 아니라 게터/세터가 있는 프로퍼티를 선언할 수도 있다
    -> 그런 게터/세터는 뒷받침하는 필드를 참조할 수가 없다
interface User {
	val email: String
    val nickname: String
    // 프로퍼티에 뒷받침하는 필드가 없다
    // 매번 결과를 계산해 돌려준다
    	get() = email.substringBefore('@') 
}

↪ 하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드해야 한다
↪ nickname은 오버라이드하지 않고 상속할 수 있다

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

  • 접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다

  • 게터에서는 field 값을 읽을 수만 있고,
    세터에서는 field 값을 읽거나 쓸 수 있다

  • 변경 가능 프로퍼티의 게터와 세터 중 한쪽만 직접 정의해도 된다
    -> 게터는 필드 값을 그냥 반환해주는 뻔한 게터이기 때문에 굳이 직접 정의할 필요 X

뒷받침하는 필드가 있는 프로퍼티 / 없는 프로퍼티의 차이점
컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터/세터를 정의하건 관계없이 게터/세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다
field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재 X

(프로퍼티가 val안 경우에는 게터에 field가 없으면 되지만,
var인 경우엔 게터/세터에 field가 없어야 한다)

4.2.5 접근자의 가시성 변경

  • 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다

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

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

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

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

  • 코틀린 클래스도 toString, equals, hashCode 등을 오버라이드할 수 있다

문자열 표현: toString()

  • 코틀린의 모든 클래스도 인스턴스의 문자열 표현을 얻을 방법을 제공
    -> 주로 디버깅과 로깅 시 이 메소드 사용
// Client에 toString() 구현하기
class Client(val name: String, val postlCode: Int) {
	override fun toString() = 
    "Client(name = $name, postalCode = $postalCode)"
}

>>> val client = Client("김승완",2505)
>>> println(client)
Client(name = 김승완, postalCode = 2505)

객체의 동승성: equals()

  • 코틀린에서 == 연산자참조 동일성을 검사하지 ❌
    객체의 동등성을 검사한다
    -> == 연산equals를 호출하는 식으로 컴파일된다

코틀린에서는 == 연산자가 두 객체를 비교하는 기본적인 방법
== 는 내부적으로 equals를 호출해서 객체를 비교한다
클래스가 equals를 오버라이드하면 == 를 통해 안전하게 클래스의 인스턴스를 비교할 수 있다

참조 비교를 위해서는 === 연산자를 사용할 수 있다

해시 컨테이너: hashCode()

  • hashCode가 지켜야 하는
    "equals() 가 true를 반환하는 두 객체는 반드시 같은 hashCode() 를 반환해야 한다" 라는 제약이 존재

  • hashCode()의 값이 같을 경우에만 동등성 연산이 실행된다

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

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

  • 어떤 클래스가 데이터를 저장하는 역할만을 수행한다면
    toString, equals, hashCode를 반드시 오버라이드해야한다
    -> data라는 변경자로 인해 이런 메소드를 IDE를 통해 생성할 필요가 없어짐
//Client를 데이터 클래스로 선언하기
data class Client(val name: String, val postalCode: Int)
  • 자바에서 요구하는 모든 메소드를 포함한다
    1. 인스턴스 간 비교를 위한 equals
    2. HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
    3. 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString

  • equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어진다

  • 생성된 equals 메소드모든 프로퍼티 값의 동등성을 확인

  • hashCode 메소드모든 프로퍼티의 해시 값을 바탕으로 계산한 해시 값을 반환
    -> 주 생성자 밖에 정의된 프로퍼티는 equals/hashCode를 계산할 때 고려의 대상이 X

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

  • 데이터 클래스의 프로퍼티가 val일 필요 X

  • 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들기를 권장
    -> HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적

  • copy():
    객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 메소드
    -> 복사본은 원본과 다른 생명주기를 가짐

class Client(val name: String, val postalCode: Int) {
	...
    fun copy(name: String = this.name,
    		postalCode: Int = this.postalCode) =
		Client(name, postalCode)
}

>>> val lee = Client("이계영",4122)
>>> println(lee.copy("김승완,2505)
Client(name = 김승완, postalCode = 2505)

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

코틀린은 기본적으로 클래스를 final로 취급한다
모든 클래스를 기본적으로 final로 취급하면 상속을 염두에 두고 open 변경자로 열어둔 클래스만 확장할 수 있다

종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있다
-> 이럴 때 사용하는 일반적인 방법이 데코레이터(Decorator) 패턴이다

  • 데코레이터 패턴:
    상속을 허용하지 않는 기존 클래스 대신 사용할 수 있는 새로운 클래스(데코레이터)를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것입니다. 이 때 새로 정의해야하는 기능은 데코레이터의 메소드에 새로 정의하고 기존 기능이 그대로 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달(forwarding)합니다.
    -> 단점: 준비 코드가 상당히 많이 필요

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

// 기본 틀
interface A { ... }
class B : A { }
val b = B()
// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by b

----------------------------------------------

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BasImpl(10)
    Derived(b).print()
}

----------------------------------------------

// 다음으로는 위임으로 구현된 인터페이스의 멤버를 오버라이딩

interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { print(x) }
}

class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc")}
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage()
    Derived(b).printMessageLine()
}
>>> abc10

----------------------------------------------
주의!
이 방법으로 오버라이딩 된 멤버는 위임된 객체의 멤버에서 호출되는 것이 아닙니다. 
해당 멤버는 자체적으로 구현된 인터페이스 멤버에만 접근할 수 있습니다.

interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x = $x"
    override fun print() { println(message) }
}

class Derived(b: Base) : Base by b {
    // This property is not accessed from b's implementation of 'print'
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print()
    println(derived.message)
}
>>> BaseImpl: x = 10
	Message of Derived

상속과 델리게이션 중 어떤 것을 선택해야 할 지

  • 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
  • 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라

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

  • object 키워드
    모든 경우 클래스를 정의하면서 동시에 인스턴스(객체)를 생성한다
    1. 객체 선언은 싱글턴을 정의하는 방법 중 하나
    2. 동반 객체는 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다
    3. 객체 식은 자바의 무명 내부 클래스 대신 쓰인다

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

  • 싱글턴 패턴
    객체의 인스턴스를 1개만 생성하여 계속 재사용하는 패턴

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

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

객체 선언은 object 키워드로 시작
-> 클래스를 정의하고 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리

  • 생성자(주/부 생성자)는 객체 선언에 쓸 수 없다

  • 일반 클래스 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다
    -> 객체 선언에는 생성자 정의가 필요 X

  • 변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 .를 붙이면
    객체에 속한 메소드나 프로퍼티에 접근할 수 있다

  • 객체 선언도 클래스나 인터페이스 상속 가능
    -> 인터페이스 구현에 다른 상태가 필요하지 않을 경우에 유용

  • 클래스 안에서 객체를 선언 가능
    -> 그런 객체도 인스턴스는 단 하나

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

  • 코틀린 클래스 안에는 정적인 멤버가 없다
    -> 자바 static 키워드를 지원 X
    -> 코틀린에서는 패키지 수준의 최상위 함수와 객체 선언을 활용

  • 클래스 안에 정의된 객체 중 하나에 companion 을 붙이면 그 클래스의 동반 객체로 만들 수 있다
    -> 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다

  • 팩토리 메소드는 선언된 클래스의 하위 클래스 객체를 반환할 수 도 있다

  • 생성할 필요가 없는 객체를 생성하지 않을 수도 있다

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

  • 동반 객체는 클래스 안에 정의된 일반 객체이다
    -> 동반 객체에 이름을 붙이거나,인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다

  • 특별히 이름을 지정하지 않으면 Companion이 된다

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

  • 인터페이스를 구현하는 동반 객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있다

동반 객체 확장

  • 클래스에 동반 객체가 있으면 그 객체 안에 함수를 정의함으로써 클래스에 대해 호출할 수 있는 확장 함수를 만들 수 있다
    -> c라는 클래스 안에 동반 객체가 있고 그 동반 객체(c.Companion) 안에 func를 정의하면 외부에서는 func()를 c.func()로 호출할 수 있다

  • 동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언 해야 한다
    -> 빈 객체라도 동반 객체가 꼭 있어야 한다

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

  • 무명 객체
    익명 클래스로부터 생성되는 객체를 뜻한다
    익명 클래스는 다른 클래스들과 달리 이름을 가지지 않는 클래스이다
    -> 이름을 가지지 않는 익명 클래스로부터 무명 객체를 생성할 수 있다

  • 보통 함수를 호출하면서 인자로 무명 객체를 넘기기 때문에 클래스와 인스턴스 모두 이름이 필요하지 않다
    -> 객체에 이름을 붙여야 한다면 변수에 무명 객체를 대입하면 된다

  • 코틀린 무명 클래스
    여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다

객체 선언과 달리 무명 객체는 싱글턴이 X
객체 식이 쓰일 때마다 새로운 인스턴스가 생성된다

  • 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있다
    -> final이 아닌 변수도 객체 식 안에서 사용할 수 있다
    -> 객체 식 안에서 그 변수의 값을 변경할 수 있다

⭐ 정리

  1. 코틀린의 인터페이스는 디폴트 구현을 포함할 수 있고, 프로퍼티도 포함할 수 있다

  2. 모든 코틀린 선언은 기본적으로 final이며 public이다

  3. 선언이 final이 되지 않게 만들려면 open을 붙여야 한다

  4. internal 선언은 같은 모듈 안에서만 볼 수 있다

  5. 중첩 클래스는 기본적으로 내부 클래스가 아니다
    바깥쪽 클래스에 대한 참조를 중첩 클래스 안에 포함시키려면 inner 키워드를 중첩 클래스 선언 앞에 붙여서 내부 클래스로 만들어야 한다

  6. sealed 클래스를 상속하는 클래스를 정의하려면 반드시 부모 클래스 정의 안에 중첩 클래스로 정의해야 한다

  7. 초기화 블록, 부 생성자를 통해 클래스 인스턴스를 더 유연하게 초기화 가능하다

  8. 🔥field 식별자를 통해 프로퍼티 접근자 안에서 프로퍼티의 데이터를 저장하는 데 쓰이는 뒷받침하는 필드를 참조할 수 있다

  9. 데이터 클래스를 사용하면 컴파일러가 equals, toString, hashCode 등의 메소드를 자동으로 생성해준다

  10. 🔥클래스 위임을 사용하면 위임 패턴을 구현할 때 필요한 수많은 성가신 준비 코드를 줄일 수 있다

  11. 🔥객체 선언을 사용하면 코틀린답게 싱글턴 클래스를 정의할 수 있다

  12. 🔥동반 객체는 자바의 정적 메소드와 필드 정의를 대신한다

  13. 🔥동반 객체도 다른 객체와 마찬가지로 인터페이스를 구현할 수 있다
    외부에서 동반 객체에 대한 확장 함수와 프로퍼티를 정의할 수 있다

  14. 🔥코틀린 객체 식은 자바의 무명 내부 클래스를 대신한다
    코틀린 객체 식은 여러 인스턴스를 구현하거나 객체가 포함된 영역에 있는 변수의 값을 변경할 수 있는 등 자바 무명 내부 클래스보다 더 많은 기능을 제공한다


📌 참고자료

중첩클래스와 내부클래스에 대해 더 자세히 알고 싶으면 ( Click )

kotlin class에 정의되어야 하는 equals, hashCode, toString - kotlinworld ( Click )

kotlin의 클래스 위임은 어떻게 동작하는지 ( Click )

kotlin의 팩토리 메소드에 대한 이해 ( Click )
kotlin의 팩토리 메소드에 대한 이해2 - kotlinworld ( Click )

kotlin의 무명 객체에 대해 - kotlinworld ( Click )

profile
개발자희망자

0개의 댓글