uncategorized

코틀린의 클래스, 객체, 인터페이스

요약
코틀린 인터페이스
open, final, abstact
가시성 변경자
중첩 클래스와 봉인된 클래스
뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
데이터 클래스와 클래스 위임
object 키워드

클래스 계층 정의

코틀린 인터페이스

코틀린의 인터페이스는 자바와 매우 유사하다. 상태를 가질 수 없지만 구현된 메서드(마치 디폴트 메서드)를 가질 수 있다.

인터페이스를 구현하는 쪽에서는 :을 통해 나타낸다. 코틀린에서는 클래스의 상속과 인터페이스의 구현 모두 :를 사용한다. 자바와 마찬가지로 코틀린에서 인터페이스는 무제한으로 구현할 수 있지만, 클래스는 하나만 가능하다.

자바의 @Override와 비슷한 override 변경자가 있다. 코틀린에서는 override 변경자를 반드시 적어줘야 한다.

1
2
3
4
5
6
7
8
9
10
interface Clickable {
fun click()

fun methodImpl() =
println("implemented Method in interface.")
}

class Clicker :Clickable {
override fun click() = println("click!")
}

만약 여러 인터페이스를 구현할 때 각 인터페이스에서 구현된 메서드의 시그니처가 중복되는 경우

위 예시에서 기존의 인터페이스에서 구현된 메서드와 같은 시그니처를 갖는 인터페이스를 만들고 이 또한 클래스에서 구현해보려고 한다면 문제가 생긴다.

1
2
3
4
5
6
7
8
9
10
interface Focusable {
fun focus()

fun methodImpl() = println("implemented too!")
}

class Clicker :Clickable, Focusable { // 컴파일 에러가 생길 것이다!
override fun click() = println("click!")
override fun focus() = println("focus!")
}

이런 경우 구현하는 클래스에서 충돌되는 구현 메서드를 새롭게 오버라이딩해서 재정의 해줘야 한다.
이때 상위 타입의 구현을 사용하려면 다음 코드블럭을 지켜보자. super<상위타입>.메서드() 이런 문법으로 상위 타입의 메서드를 호출할 수 있다.

1
2
3
4
5
6
7
8
class Clicker : Clickable, Focusable {
override fun click() = println("click!")
override fun focus() = println("focus!")
override fun methodImpl() {
super<Focusable>.methodImpl()
super<Clickable>.methodImpl()
}
}

사실 코틀린에서는 아직 자바의 디폴트 메서드를 지원하지 않는다.
코틀린은 자바의 디폴트 메서드가 등장하기 전인 자바 6에 호환되도록 설계됐다. 그래서 자바 인터페이스에 디폴드 메서드가 있으면 코틀린에는 해당 메서드를 추상 메서드로 인터페이스에 해놓고 따로 클래스를 만들어서 디폴트 메서드의 구현을 정적 메서드로 놓는다. 이렇게 되면 자바 클래스가 디폴트 인터페이스가 포함된 코틀린 인터페이스를 구현하려고 하면 디폴트 메서드도 자바 클래스에서 구현해줘야 한다. 왜냐면 자바에서는 코틀린의 디폴트 메서드 구현(정적 메서드)를 의존하지 않기 때문이다.

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

open, final

자바에서는 final 선언되지 않은 클래스를 상속해서 사용할 수 있다.
문제는 상속은 부모 클래스의 가정을 이해하지 않고 자식 클래스에서 가정을 깨는 구현을 했을 때, 부모 클래스가 약간의 변경이 생겨도 자식 클래스가 예상과 다르게 작동할 수 있다. (취약한 기반 클래스 문제)
코틀린에서는 상속을 제한적으로 쓰고자 기본적으로 모두 final 선언되어 있는 셈이다. 만약 상속을 하고 싶으면 부모 클래스에 open 키워드를 붙여야 한다. 그와 더불어 자식 클래스에서 오버라이딩이 가능한 메서드나 프로퍼티에도 open을 붙여야 오버라이딩이 가능하다.

1
2
3
4
5
6
7
open class RichButton: Clickable {

fun cannotOverride() {}
open fun animate() {}

override fun click() = print("rich click")
}

override 된 메서드는 당연히 오버라이딩에 열려있다. 하지만 이런 메서드도 앞에 final을 붙이면 하위에서 오버라이딩을 막을 수 있다.

열린 클래스와 스마트 캐스트
기본적으로 클래스를 final로 상속을 막으면 스마트 캐스트하기에 유리하다. 스마트 캐스트는 타입 검사 뒤 변경될 수 없는 변수에만 적용된다. 즉 val 이면서 커스텀 접근자가 있어서는 안된다. 여기에 더 나아가 프로퍼티가 final이어야 한다는 조건이 필요하다. 왜냐면 다른 클래스가 상속해서 커스텀 접근자를 정의할 수 있음을 막아야 하기 때문이다.

abstract

abstract는 자바와 거의 비슷하다. 추상 메서드나 추상 클래스를 정의할 때 사용된다.

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

가시성 변경자는 클래스 외부 접근을 제어한다. 자바와 다르게 코틀린은 아무 가시성 변경자를 안 적으면 public으로 취급한다.
코틀린에는 자바처럼 패키지 전용이 없다. 코틀린에서 패키지는 네임 스페이스를 관리하기 위함이지 가시성을 제어하기 위함이 아니다.

internal: 패키지 전용 가시성을 대체

패키지 전용 가시성를 대신하는 internal이 있다. internal은 모듈 내부에서만 볼 수 있는 변경자이다. 모듈은 같이 컴파일 되는 단위를 말한다. 패키지 기준은 다른 프로젝트더라도 같은 패키지에 클래스를 선언해서 접근 할 수 있게 되는 단점이 있다.

그 외 차이점
private는 내부에서만 접근 가능한 변경자인데 코틀린은 최상위 선언(클래스, 프로퍼티, 함수)에도 사용할 수 있다.
protected는 코틀린에서 패키지 전용 가시성이 아닌 하위 클래스 가시성을 제공할 때 쓰인다. 다만 최상위 선언에는 사용할 수 없다.

의존하려면 더 엄한 가시성을 가져야 한다.

다음 예제를 보자

1
2
3
4
5
6
7
8
9
internal open class TalkativeButton  {
private fun hello() = {}
protected fun bye() = {}
}

fun TalkativeButton.doSomething() { // public한 확장함수는 internal 클래스에 사용할 수 없다.
hello() // public한 확장함수는 private 메서드에 접근할 수 없다.
bye() // public한 확장함수는 protected 메서드에 접근할 수 없다.
}

위와 같이 다른 클래스에 의존해서 사용하려는 경우 해당 함수나 클래스의 가시성이 사용하려는 클래스의 가시성과 같거나 더 엄해야 한다. 위 경우는 확장함수의 가시성을 internal로 바꾸거나 수신 객체 타입의 가시성을 public으로 올리는 방법이 있다.
그리고 private나 protected 조건을 만족하지 않으므로 확장 함수의 메서드 호출이 안된다.

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

코틀린의 public, protected, private 변경자는 자바 바이트 코드에도 그대로 들어간다. 다만 private 클래스는 다르게 처리된다. 자바에서는 private 클래스가 안되기 때문인데, 이 경우 코틀린에서 private 클래스를 protected 클래스로 변환해서 컴파일한다.
코틀린의 internal은 자바에서 public이 된다. 모듈의 특성 상 어쩔 수 없는 부분이다.
이렇게 더 열리게 되면 의도하지 않은 접근이 가능해지는데 코틀린 컴파일러가 internal 멤버 이름을 보기 나쁘게 바꾼다. 이는 예상치 못한 상속에서 생기는 우연한 오버라이딩을 피하고 internal 클래스를 외부에서 사용하지 않도록 하기 위함이다.

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

코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근을 할 수 없다. 자바에서는 클래슨 내부에서 클래스를 선언하면 묵시적으로 내부 클래스가 된다. 즉 외부 클래스에 대한 참조를 묵시적으로 포함한다. 자바에서 이런 보이지 않는 참조를 제거하려면 중첩된 클래스에 static을 붙여주면 된다.

코틀린은 반대다. 기본적으로 외부 클래스에 대한 참조가 끊긴 중첩 클래스로 취급하고 만약 내부 클래스로 만들려면 안쪽 클래스에 inner 변경자를 붙여야 한다. 만약 내부 클래스에서 외부 클래스 참조를 접근하려면 this@외부클래스이름 으로 하면 된다.

1
2
3
4
5
class Outer {
inner class Inner {
fun getOuterReference() : Outer = this@Outer
}
}

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

sealed 변경자를 통해 자신의 상속해줄 수 있는 자식 클래스를 제한 할 수 있다. 이때 적용되는 클래스는 코틀린 1.0에는 부모의 중첩 클래스로만 해야하고, 1.1에는 같은 파일 안에 있기만 하면 된다.

1
2
3
4
sealed class Expr {
class Num : Expr()
class Sum : Expr()
}

이렇게 정해놓으면 유리한 점이 분기 처리할 때가 유리하다. 자식이 무한하게 허용되면 when 식으로 처리할 때 else로 그 외 처리를 해줘야 한다. 그리고 자식이 생길 때 분기로 처리하기를 놓칠 수 있다.

1
2
3
4
5
6
fun eval(e: Expr): Int =
when (e) {
is Num -> 1
is Sum -> 2
else -> throw Exception()
}

하지만 sealed 되어 있으면 자식이 한정적이라 정해진 자식들만 체크하면 된다. 만약 놓친 자식이 있으면 컴파일 에러가 나서 미리 알 수 있다.

1
2
3
4
5
fun eval(e: Expr) : Int =
when (e) {
is Expr.Num -> 1
is Expr.Sum -> 2
}

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

코틀린은 주 생성자와 부 생성자가 있다. 주 생성자는 클래스를 초기화할 때 쓰이고 클래스 본문 밖에서 정의된다. 부 생성자는 클래스 본문 안에서 사용되는 생성자이다.
또한 코틀린에서는 초기화 블록을 지원해서 초기화 로직을 모을 수 있다.

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

코틀린에서 주 생성자는 다음과 같이 쓰인다.

1
class User(val nickName: String)

클래스 이름뒤에 괄호로 둘러 쌓인 내용을 주 생성자라고 한다.

이를 최대한 명시적으로 풀어쓰면 다음과 같다.

1
2
3
4
5
6
7
class User constructor(_nickName: String) {
val nickName: String

init {
nickName = _nickName
}
}

여전히 주 생성자는 존재하는데 이때 constructor는 생성자의 정의를 시작함을 의미한다.
그리고 init은 클래스의 객체가 만들어질 때 실행되는 초기화 로직을 모은 초기화 블록을 선언한다. 초기화 블록은 별도의 코드를 포함할 수 없는 주 생성자와 함께 많이 쓰인다.
그리고 생성자 파라미터 앞 _는 프로퍼티와 파라미터를 구분하기 위해서 적었는데 기존 자바의 this.nickName = nickName 이렇게 해도 된다.

위 예시를 좀 더 개선하면, 일단 주 생성자 앞에 별도의 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다. 그리고 프로퍼티 초기화는 굳이 초기화 블록에서 할 필요가 없고 프로퍼티 선언에서 해도 된다.

1
2
3
class User (_nickName: String) {
val nickName = _nickName
}

하지만 위 예시도 굳이 val 파리미터를 본문에서 하지말고 주 생성자 안에서 해버리면 간단하다.

1
class User(val nickName: String)

참고로 주 생성자에서 디폴트 값과 이름 있는 선언도 가능하다.

1
2
3
4
class User(val nickName: String = "untitled")

User().nickName // untitled
User(nickName = "Klay") // Klay

모든 파라미터에 디폴트값이 있으면 자동으로 파라미터가 없는 생성자를 만들어준다.
DI 프레임워크 중 기본 생성자가 반드시 필요한 경우에 이런 기능이 유리하다고 한다.

만약 기반 클래스가 있다면 다음과 같이 기반 클래스에 파라미터를 넘겨줄 수 있다.

1
class GoodUser(val nickName: String) : User(nickname)

참고로 만약 기반 클래스가 기본 생성자만 있다하더라도 하위 클래스는 생성자 (괄호) 를 적어줘야 한다.

1
2
3
open class Book

class GoodBook : Book()

인터페이스는 생성자가 없으니 하위 클래스가 구현할 때 괄호를 써주지 않는다.

마지막으로 private 한 생성자를 만드는 방법이다.
이는 동반 객체에 유용하다고 한다. (반면 유틸 클래스나 싱글턴에는 이렇게 하지 않느다. 확장 함수나 객체 선언하는 방식을 한다.)

1
class CannotConstructUser private constructor()

부 생성자

일반 적으로 코틀린에서는 디폴트 값을 지원해서 여러 생성자를 만들 일이 없다.
그래도 여러 생성자가 필요한 경우가 있다면 부 생성자를 이용한다.
super를 통해 기반 클래스의 생성자에 인자 전달도 되고, this를 통해 자신의 다른 생성자에게 생성을 위임할 수 있다.

1
2
3
4
5
6
7
8
9
open class View {
constructor(i: Int) {}
constructor(s: String) {}
}

class GoodView : View {
constructor(i:Int) : this(i.toString()) {}
constructor(s: String) : super(s) {}
}

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

추상 프로퍼티

코틀린에서는 인터페이스에 추상 프로퍼티를 선언할 수 있다.

1
2
3
interface User {
val nickName: String
}

이는 해당 인터페이스를 구현하는 쪽은 nickName을 접근할 수 있는 방법을 제공해야 함을 의미한다. 인터페이스가 실제 상태를 가지는 것은 아니다.

추상 프로퍼티를 구현하는 세가지 예시를 보자

1. 주 생성자로 프로퍼티 구현

1
class PrivateUser(override val nickName: String) : User5

간단하다 override만 붙여주면 된다.

2. 커스텀 게터

1
2
3
4
class EmailUser(val email: String) : User5 {
override val nickName: String
get() = email.substringBefore('@')
}

역시 override가 붙은 프로퍼티에 커스텀 게터를 구현하면 된다. 다만 커스텀 게터는 호출때마다 구현된 내용이 실행된다.

3. 프로퍼티 초기화 식

1
2
3
4
5
class FacebookUser(val id: Int) : User5 {
override val nickName = getFacebookName(id)
}

fun getFacebookName(id: Int) = id.toString()

프로퍼티 초기화 식은 프로퍼티에 접근할 때마다 구현된 내요을 실행하지 않는다. 초기화 할 때 한번만 한다. 이 점이 2번과 가장 큰 차이다. 만약 getFacebookName이 비용이 큰 메서드라고 상상해보면 프로퍼티에 접근할 때마다 호출되는 커스텀 게터 방식이 많이 불리햇을 것이다.

마지막으로 인터페이스에는 커스텀 게터와 세터가 있는 프로퍼티를 추가할 수 있다. 물론 실제 값이 있는 건 아니니까 참조할 수 없다.

1
2
3
4
5
interface User {
val nickName: String
val email: String
get() = email.substringBefore('@')
}

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

프로퍼티에 저장된 값을 변경할 때 특정 문자열을 출력하려고 한다고 해보자.

1
2
3
4
5
6
7
class User(val name: String) {
var address = "undefined"
set(_address) {
println("$field -> $_address")
field = _address
}
}

흠 세터를 직접 구현해줬다. 그런데 field는 무엇인가? field는 현재 접근자에 해당하는 필드에 접근할 수 있게 한다. field를 사용하지 않는 커스텀 접근자는 뒷받침 하는 필드가 생기지 않는다.

접근자의 가시성 변경

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 하지만 get이나 set앞에 접근자를 선언해서 가시성을 변경할 수 있다.

1
2
3
4
class User8 {
var name: String = "hi"
private set
}

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

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

모든 코틀린 클래스는 toString, equals, hashCode 등을 오버라이딩해야 한다.
코틀린은 이런 메서드 구현을 자동으로 생성해줄 수 있다.

1
class Client(val name: String, val postalCode: Int)

다음과 같은 클래스를 예시로 오버라이딩 해보자.

toString()

1
2
class Client(val name: String, val postalCode: Int) {
override fun toString() = "client"

equals()

참고로 코틀린은 == 연산자가 내부적으로 equals메서드를 호출해서 객체를 비교한다. 참조 비교를 위해서는 === 연산자를 호출해서 사용할 수 있다.

1
2
3
4
5
6
7
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client)
return false;
return name == other.name && postalCode == other.postalCode
}
}

hashCode

자바에서 equals를 재정의할 때는 반드시 hashCode도 재정의해야 한다. JVM 언어에서는 equals가 true를 반환하는 두 객체를 반드시 같은 hashCode를 반환해야 한다. Hash를 활용하는 자료구조에서는 원소 비교 비용을 줄이기 위해 먼저 객체의 해시 코드를 비교하고 같은 경우에만 실제 값을 비교한다.

1
2
3
4
class Client(val name: String, val postalCode: Int) {
//...
override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}

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

코틀린은 이런 메서드를 컴파일러가 생성해준다.
data 변경자를 붙여주기만 하면 된다. data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.

1
data class Client(val name: String, val postalCode: Int)

이때 주의할 점은 equalshashCode는 주 생성자에서 선언된 모든 프로퍼티를 기준으로 만들어진다. 주 생성자 외부에서 선언된 프로퍼티는 고려되지 않는다.

데이터 클래스는 equals, hashCode, toString 외에도 유용한 메서드 몇 개를 더 만들어준다.

불변한 데이터 클래스를 쉽게 복사: copy

데이터 클래스를 쉽게 복제하는 메서드를 제공해준다. 밑은 copy 메서드를 이해를 돕기위해 직접 구현한 예시이다.

1
2
3
4
5
6
7
8
9
class Client(val name: String, val postalCode: Int) {
fun copy(name: String = this.name, postalCode = this.postalCode) {
return Client(name, postalCode)
}
}

// 사용법
val client = Client("Klay", 1)
val copiedClient = client.copy("Curry")

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

어떤 클래스에 기능을 추가해야 할 때 데코레이터 패턴을 사용한다고 해보자. 데코레이터 패턴은 기존 클래스의 인터페이스를 데코레이터가 제공하되 데코레이터 내부에 기존 클래스 인스턴스를 필드로 갖고 메서드 호출시 기존 클래스에게 메시지를 전달하는 것이다. 그리고 데코레이터에 추가하고자 하는 메서드를 구현하면 된다. 이때 새로운 기능에 기존 클래스의 메서드를 활용할 수 있다.

하지만 이런 방법은 지나치게 준비 코드가 많다. 코틀린에서는 인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에게 위임 중이라는 사실을 명시할 수 있다.

예를 들어 ArrayList를 감싸는 클래스를 만들어보자. Collection의 인터페이스를 내부 리스트에게 위임하도록 구현해야 했다.

1
2
3
4
5
6
7
8
9
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()

override val size: Int get() = innerList.size
override fun containsAll(elements: Collection<T>) = innerList.containsAll(elements)
override fun contains(element: T) = innerList.contains(element)
override fun isEmpty() = innerList.isEmpty()
override fun iterator(): Iterator<T> = innerList.iterator()
}

하지만 코틀린의 by를 써보자. Set에서 추가된 원소를 카운팅하는 기능을 추가해보자.
구현한 인터페이스 뒤에 by와 함께 프로퍼티 이름을 적어준 것을 확인할 수 있다. 이러면 위임 메서드를 컴파일러가 알아서 만들어준다. 다만 여기서 자동으로 만들어진 메서드 대신 개발자가 직접 구현하고 싶으면 override 붙여서 구현해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CountingSet<T> (
val innerSet :MutableCollection<T> = HashSet()
) : MutableCollection<T> by innerSet {

var addCount = 0

override fun add(element: T): Boolean {
addCount++
return innerSet.add(element)
}

override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return innerSet.addAll(elements)
}
}

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

코틀린에서 object 키워드를 다양한 상황에서 사용하지만 모든 경우 클래스를 정의하면서 동시에 인스턴스를 생성한다는 공통점이 있다. 다양한 상황에 대해서 알아보자.

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

코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.

모든 직원의 급여 대장을 관리하는 객체가 필요하다고 해보자. 이 객체가 굳이 여러개일 필요는 없으니 객체 선언으로 싱글턴 객체로 만들어보자.

1
2
3
4
5
object Payroll {
val allEmployees = arrayListOf<Person>()

fun doSomething() {}
}

객체 선언은 object 키워드를 사용하면 된다.객체 선언은 그 클래스를 정의하고 해당 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리한다.
하지만 생성자는 객체 선언에 사용할 수 없다. 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문이다.

객체 선언도 클래스나 인스턴스를 상속받을 수 있다. 예를 들어 특정 클래스를 위해 Comparator를 구현한 객체는 여러 개가 필요없다.

1
2
3
4
object FileComparator : Comparator<File> {
override fun compare(o1: File, o2: File)
= o1.path.compareTo(o2.path, ignoreCase = true)
}

싱글톤과 의존관계 주입
싱글턴 패턴과 마찬가지로 객체 선언은 대규모 시스템에서 안좋은 경우가 있다.
객체 생성을 제어할 수 없고 파라미터를 지정할 수 없기 때문이다.
그래서 단위 테스트하거나 시스템 설정이 바뀔 때 의존 객체를 바꿔줄 수 없다. 만약 이런 기능이 필요하다면 의존 관계 주입 프레임워크를 사용해보자.

클래스 안에 객체 선언

클래스 안에 객체 선언을 해도 그 객체는 싱글톤이다. 외부 클래스가 인스턴스화 된다고 객체 선언이 여러번 객체로 만들어지는게 아니다!

1
2
3
4
5
6
7
8
9
10
11
12
data class OuterClass(val name: String) {

object InnerObject {
fun hello() {
println("hello")
}
}
}

fun main() {
OuterClass.InnerObject.hello()
}

자바에서 코틀린 객체 선언된 객체를 접근하기
코틀린 객체 선언은 자바에서 정적 필드를 가진 클래스로 컴파일된다. 이때 정적 필드 이름은 항상 INSTANCE다.
OuterClass.InnerObject.INSTANCE.hello()

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

코틀린 클래스 안에는 정적인 멤버가 없다. 코틀린은 static 키워드를 지원하지 않는다. 대신 최상위 함수와 객체 선언을 사용한다. 일반적으로 최상위 함수를 추천하지만 특정 클래스 내부의 private 프로퍼티나 메서드에 접근하지 못하는 경우는 해당 클래스 안에서 객체 선언을 통해 접근하기도 한다.

1
2
3
4
5
6
7
8
9
10
11
12
data class NamedPerson(private val name: String) {

object InnerObject {
fun hello(namedPerson: NamedPerson) {
println("hello ${namedPerson.name}")
}
}
}

fun hello(namedPerson: NamedPerson) {
println("hello ${namedPerson.name}") // 접근하지 못한다.
}

이를 companion 키워드를 통해 중첩된 객체 선언에서 클래스 이름을 제거할 수 있다.(물론 이름을 붙여줄 수도 있다.) 마치 자바의 정적 멤버 처럼 활용할 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
data class NamedPerson2(private val name: String) {
companion object {
fun hello(namedPerson: NamedPerson2) {
println("hello ${namedPerson.name}")
}
}
}

fun main() {
NamedPerson2.hello(NamedPerson2("Klay"))
}

동반 객체를 통해 private 생성자 호출: 팩토리 메서드

동반 객체는 외부 클래스의 private 프로퍼티, 메서드, 생성자에 접근할 수 있다. 그래서 팩토리 패턴을 사용하기 좋은 조건을 가졌다.

1
2
3
4
5
6
7
8
class Man private constructor(val name: String) {
companion object {
fun create(name: String) = Man(name)
}
}
fun main() {
Man.create("Klay")
}

팩터리 패턴이냐 여러 부 생성자냐
상황에 따라 여라가지 부 생성자로 객체를 만들어주도록 할 수 있다. 이를 동반 객체를 통한 팩토리 패턴을 활용해서 이름 있는 메서드로 가독성을 높일 수 있는데, 문제는 클래스를 확장해야 할 경우 동반 객체 멤버를 오버라이딩 할 수 없으므로 여러 생성자를 사용하는 편이 낫다.

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

1
2
3
4
5
6
7
8
9
10
11
interface Creatable<T> {
fun of(name: String) : T
}

class Boy(val name: String) {
companion object : Creatable<Boy> {
override fun of(name: String) = Boy(name)
}
}

fun <T> createSomething(name:String, creatable: Creatable<T>) = creatable.of(name)

동반 객체도 인터페이스를 구현할 수 있으며, 만들어진 동반 객체가 매개변수로 사용됐음을 주목하라. 이때 동반 객체의 이름이 없어서 Boy로 인자를 전달해줬음을 주목하자.

자바에서 코틀린 동반 객체
자바에서 코틀린 동반 객체는 이름이 있으면 해당 이름으로 정적 멤버 접근하면 되고 이름이 없는 경우는 Companion이라는 이름의 정적 멤버로 접근하면 된다.

동반 객체 확장

이름 없는 동반 객체를 만들어서 비즈니스 객체에서 보고 싶지 않은 코드를 분리할 수 있다. 예를 들어 JSON으로부터 역직렬화 함수를 만들어서 제공하고 싶은데 이를 비즈니스 클래스 내부에 위치하고 싶지 않을 때 사용해보자.

1
2
3
4
5
6
7
8
9
// 비즈니스 모듈
class Human(val id: Int) {
companion object
}

// 클라이언트 서버 통신 모듈
fun Human.Companion.fromJson(jsonValue: String) : Human {
return Human(jsonValue.toInt())
}

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

무명 객체를 정의할 때도 object 키워드를 사용한다. 무명 객체는 자바의 무명 내부 클래스를 대신한다.
이때 중요한 점은 무명 객체로 object 키워드를 사용하면 싱글톤이 아님을 명심하자. 무명 객체와 동반 객체나 객체 선언과 다르다.

1
2
3
4
5
6
7
8
9
var listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
// logic...
}

override fun mousePressed(e: MouseEvent?) {
// logic...
}
}

그리고 무명 객체(객체 식)은 그 식이 포함된 함수의 변수에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
fun foo(window: Window) {
var clickAmount = 0

window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
clickAmount++
}
})
}
Share