클래스 계층 정의#
코틀린 인터페이스#
- 코틀린 인터페이스는 자바 8 인터페이스와 비슷하다.
- 상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메소드를 오버라이드할 때는
override
변경자를 붙여줘야 된다.- 실수로 상위 클래스의 메소드를 오버라이드하는 경우를 방지해준다.
- 한 클래스에서 두 인터페이스를 함께 구현했을 때, 같은 디폴트 메소드가 있다면 컴파일 오류가 발생한다. 이 경우는 하위 클래스에 직접 구현하게 강제한다.
- 상위 타입의 메소드 호출하는 방식은 아래를 참고한다.
class Button : Clickable, Focusable {
override fun click() {
println("I was Clicked")
}
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
open, final, abstract 변경자: 기본적으로 final#
- 취약한 기반 클래스(fragile base class): 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 꺠져버린 경우
- 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있다.
- 인터페이스는 항상 열려 있으며
final
로 변경할 수 없다.
상속 제어 변경자 | 이 변경자가 붙은 멤버는 | 설명 |
---|
final | 오버로이드할 수 없음 | 클래스 멤버의 기본 변경자다. |
open | 오버라이드할 수 있음 | 반드시 open을 명시해야 오버라이드할 수 있다. |
abstract | 반드시 오버라이드해야 함 | 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다. |
override | 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 | 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다. |
가시성 변경자: 기본적으로 공개#
- 어떤 클래스의 기반 타입 목록에 들어있는 타입이나제네릭 클래스의 타입 파라미터에 있는 타입의 가시성은 그 클래스의 가시성과 같거나 더 높아야된다.
- 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다.
- 코틀린의 선언과 그에 해당하는 자바 선언에 차이가 존재한다.
- 코틀린에서
private
클래스를 자바에서 패키지 전용 클래스로 컴파일한다. - 코틀린에서
internal
변경자는 바이트코드상에서는 public
이 된다. 따라서, 코틀린에서 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생긴다.- 하지만 코틀린 컴파일러가
internal
멤버의 이름을 보기 나쁘게 바꾼다.
- 코틀린에서
protected
는 하위 클래스만 볼 수 있지만, 자바에서는 같은 패키지에 속한 코드는 볼 수 있다.
변경자 | 클래스 멤버 | 최상위 선언 |
---|
public(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스#
- 중첩 클래스: 바깥쪽 클래스에 대한 참조를 저장하지 않음
- 내부 클래스: 바깥쪽 클래스에 대한 참조를 저장함
클래스 B안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
---|
중첩 클래스 | static class A | class A |
내부 클래스 | class A | inner class A |

- 코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조에 접근하려면 아래와 같이 써야된다.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한#
- 코틀린 컴파일러는
when
을 사용해 타입의 값을 겁사할 때 꼭 디폴트 분기인 else
분기르 ㄹ 덧붙이게 강제한다.- 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가
when
이 모든 경우를 처리하는지 제대로 검사할 수 없다.
sealed
클래스는 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.sealed
클래스의 하위 클래스를 정의할 때는 반드시 같은 파일에서 정의를 해야된다.when
식에서 sealed
클래스의 모든 하위 클래스를 처리한다면 디폴트 분기가 필요없다.sealed
클래스에 속한 값에 대해 디폴트 분기를 사용하지 않고 when
식을 사용하면 나중에 sealed
클래스의 상속 계층에 새로운 하위 클래스를 추가해도 when
식이 컴파일되지 않는다.sealed
클래스는 2가지 가시성 변경자의 생성자를 가질 수 있다.
sealed class Expr {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언#
클래스 초기화: 주생성자와 초기화 블록#
- 주 생성자: 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다.
constructor
: 주 생성자나 부생성자 정의를 시작할 때 사용하는 키워드- 제한적이기 떄문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다.
- 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면
constructor
를 생략해도 된다.
init
: 초기화 블록을 시작하는 키워드
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
- 주 생성자의 파라미터로 프로퍼티를 초기화 한다면 그 주 생성자 파라미터 이름 앞에
val
를 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다. - 생성자 파라미터에도 디폴트 값을 정의할 수 있다.
- 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다.
class User2 (val nickname: String, val isSubscribed: Boolean = true)
- 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다.
- 인터페이스는 생성자가 없기 때문에 어떤 클래스가 인터페이스를 구현하는 경우 그 클래스의 상위 클래스 목록에 있는 인터페이스 이름 뒤에는 아무 괄호도 없다.
class TwitterUser(nickname:String) : User(nickname)
- 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 인자가 없는 디폴트 생성자를 만들어준다.
open class Button
class RadioButton: Button()
- 클래스 외부에서 인스턴스화하지 못하게 막고 싶다면 모든 생성자를
private
으로 만들면 된다.
class Secretive private constructor()
부 생성자: 상위 클래스를 다른 방식으로 초기화#
- 인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여럿 만들지 말라. 대신 파라미터의 디폴트 값을 생성자 시그니처에 직접 명시하라.
- 부 생성자는
super()
키워드를 통해 자신에게 대응하는 상위 클래스 생성자를 호출한다. - 부 생성자는
this()
키워드를 통해 자신의 다른 생성자를 호출할 수 있다.
open class View {
constructor(ctx: String) {}
constructor(ctx: String, attr: String) {}
}
class MyButton: View {
constructor(ctx: String) : this(ctx, "abc") {}
constructor(ctx: String, attr: String): super(ctx, attr)
}
인터페이스에 선언된 프로퍼티 구현#
- 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
- 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게더 등의 정보가 들어있지 않다.
- 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다.
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: String = getFacebookName(accountId)
private fun getFacebookName(accountId: Int) : String {
return accountId.toString()
}
}
- 인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
- 게터와 세터에는 뒷받침하는 필드를 참조할 수 없다.
interface User2 {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
게터와 세터에서 뒷받침하는 필드에 접근#
- 접근자 본문에서는
field
라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.- 게터에서는
field
값을 읽을 수만 있고, 세터에서는 field
값을 읽거나 쓸 수 있다.
- 뒷받침하는 필드가 있는 프로퍼티와 그런 필드가 없는 프로퍼티의 차이
- 게터나 세터에
field
를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다. field
를 사용하지 않는 커스텀 접근자 구현을 정의하면 뒷받침하는 필드는 존재하지 않는다.
접근자의 가시성 변경#
- 접근자의 가시성은 기본적으로는 프로퍼티의 가시성과 같다.
- 원한다면
get
이나 set
앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임#
모든 클래스가 정의해야 하는 메소드#
toString()
: 인스턴스의 문자열 표현을 얻을 방법을 제공한다. 주로 디버깅과 로깅 시 이 메소드를 사용한다.equals()
: 객체의 동등성을 만족하는 조건을 제공한다.- 코틀린에서는
==
연산자는 내부적으로 equals
를 호출해서 객체를 비교한다. 따라서 클래스가 equals
를 오버라이드하면 ==
를 통해 안전하게 동등성을 비교할 수 있다. - 코틀린에서 객체의 참조를 비교할 때는
===
연산자를 사용할 수 있다. hashCode
정의를 빠뜨리면 제대로 작동하지 않는다.
hashCode()
: JVM 언어에서는 equals()
가 true
를 반환하는 두 객체는 반드시 같은 hashCode()
를 반환해야 한다는 제약이 있다.HashSet
은 원소를 비교할 때 비용을 줄이기 위해 객체의 해시 코드를 비교하고 같은 경우에만 실제 값을 비교한다.
데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성#
- 코틀린에서는
data
라는 변경자를 클래스 앞에 붙이면 아래 메소드를 컴파일러가 자동으로 만들어준다. - 불변 클래스
HashMap
등의 컨테이너에 키의 프로ㅓ퍼티를 변경하면 컨테이너 상태가 잘못될 수 있다.- 불변 객체를 주로 사용하는 프로그램에서는 스레드가 사용 중인 데이터를 다른 스레드가 변경할 수 없으므로 불변 객체를 사용하면 스레드를 동기화해야 할 필요가 줄어든다.
copy()
: 불변 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 메소드
클래스 위임: by 키워드 사용#
- 하위 클래스가 상위 클래스의 일부를 오버라이드 하면, 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 작동하지 못하는 경우가 생길 수 있다.
- 그래서 코틀린은 기본적으로 클래스를
final
로 취급하기로 결정했다.
- 하지만 종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있는데, 사용하는 하는 일반적인 방법이 데코레이터 패턴이다.
- 데코레이터 패턴의 단점은 준비 코드가 상당히 많이 필요하다는 점이다.
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 += elements.size
return innerSet.addAll(elements)
}
}
object 키워드: 클래스 선언과 인스턴스 생성#
- 객체 선언(object declaration): 싱글턴을 정의하는 방법
- 동반 객체(companion object): 인스턴스 메소드는 아니지만 어떤 클래스와 관련이 있는 메소드와 팩토리 메소드를 담을 때 쓰인다.
- 객체 식: 자바의 무명 내부 클래스(anonymouse inner class)
객체 선언: 싱글턴을 쉽게 만들기#
object Payroll {
val allEmployees = arrayListOf<Person> ()
fun calculateSalary () {
for (person in allEmployees) {
...
}
}
}
동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소#
- 코틀린 클래스 안에는 정적인 멤버가 없다.
- 패키지 수준의 최상위 함수의 경우
private
으로 표시된 비공개 멤버에 접근할 수 없다. companion
키워드를 사용하면 해당 클래스의 동반 객체를 만들 수 있다. 동반 객체는 이름을 따로 지정할 필요가 없다.- 동반 객체는
private
생성자를 호출하기 좋은 위치다.
동반 객체를 일반 객체처럼 사용#
- 동반 객체는 클래스 안에 정의된 일반 객체다.
- 동반 객체도 이름을 지정할 수 있다.
class Member private constructor(val id: Long, val name: String) {
companion object Factory {
private var nextId: Long = 1L;
fun of(name: String): Member {
return Member(nextId++, name)
}
}
}
class Member private constructor(val id: Long, val name: String) {
companion object : Factory<Member> {
private var nextId: Long = 1L;
override fun of(name: String): Member {
return Member(nextId++, name)
}
}
}
- 동반 객체는 클래스에 정의된 인스턴스를 가리키는 정적 필드로 컴파일 된다.
- 동반 객체에 이름을 붙이지 않았다면 자바 쪽에서
Companion
이라는 이름으로 그 참조에 접근할 수 있다. - 자바에서 사용하기 위해 정적인 멤버로 만들어야 되면
@JvmStatic
애노테이션을 코틀린 멤버에 붙이면 된다. @JvmField
애노테이션을 최상위 프로퍼티나 객체에 선언된 프로퍼티 앞에 붙인다.
- 동반 객체에 대해 새로운 메소드를 다른 곳에서 정의할 수 있다.
fun Member.Companion.from(name: String): Member {
return Member.of(name)
}
객체 식: 무명 내부 클래스를 다른 방식으로 작성#
- 무명 객체를 정의할 때도
object
키워드를 쓴다.

- 자바의 무명 클래스와 같이 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있다.
