요약
코틀린 인터페이스
open, final, abstact
가시성 변경자
중첩 클래스와 봉인된 클래스
뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
데이터 클래스와 클래스 위임
object 키워드
클래스 계층 정의
코틀린 인터페이스
코틀린의 인터페이스는 자바와 매우 유사하다. 상태를 가질 수 없지만 구현된 메서드(마치 디폴트 메서드)를 가질 수 있다.
인터페이스를 구현하는 쪽에서는 :
을 통해 나타낸다. 코틀린에서는 클래스의 상속과 인터페이스의 구현 모두 :
를 사용한다. 자바와 마찬가지로 코틀린에서 인터페이스는 무제한으로 구현할 수 있지만, 클래스는 하나만 가능하다.
자바의 @Override
와 비슷한 override
변경자가 있다. 코틀린에서는 override
변경자를 반드시 적어줘야 한다.
1 | interface Clickable { |
만약 여러 인터페이스를 구현할 때 각 인터페이스에서 구현된 메서드의 시그니처가 중복되는 경우
위 예시에서 기존의 인터페이스에서 구현된 메서드와 같은 시그니처를 갖는 인터페이스를 만들고 이 또한 클래스에서 구현해보려고 한다면 문제가 생긴다.
1 | interface Focusable { |
이런 경우 구현하는 클래스에서 충돌되는 구현 메서드를 새롭게 오버라이딩해서 재정의 해줘야 한다.
이때 상위 타입의 구현을 사용하려면 다음 코드블럭을 지켜보자. super<상위타입>.메서드()
이런 문법으로 상위 타입의 메서드를 호출할 수 있다.
1 | class Clicker : Clickable, Focusable { |
사실 코틀린에서는 아직 자바의 디폴트 메서드를 지원하지 않는다.
코틀린은 자바의 디폴트 메서드가 등장하기 전인 자바 6에 호환되도록 설계됐다. 그래서 자바 인터페이스에 디폴드 메서드가 있으면 코틀린에는 해당 메서드를 추상 메서드로 인터페이스에 해놓고 따로 클래스를 만들어서 디폴트 메서드의 구현을 정적 메서드로 놓는다. 이렇게 되면 자바 클래스가 디폴트 인터페이스가 포함된 코틀린 인터페이스를 구현하려고 하면 디폴트 메서드도 자바 클래스에서 구현해줘야 한다. 왜냐면 자바에서는 코틀린의 디폴트 메서드 구현(정적 메서드)를 의존하지 않기 때문이다.
open, final, abstract 변경자: 기본적으로 final
open, final
자바에서는 final 선언되지 않은 클래스를 상속해서 사용할 수 있다.
문제는 상속은 부모 클래스의 가정을 이해하지 않고 자식 클래스에서 가정을 깨는 구현을 했을 때, 부모 클래스가 약간의 변경이 생겨도 자식 클래스가 예상과 다르게 작동할 수 있다. (취약한 기반 클래스 문제)
코틀린에서는 상속을 제한적으로 쓰고자 기본적으로 모두 final 선언되어 있는 셈이다. 만약 상속을 하고 싶으면 부모 클래스에 open
키워드를 붙여야 한다. 그와 더불어 자식 클래스에서 오버라이딩이 가능한 메서드나 프로퍼티에도 open
을 붙여야 오버라이딩이 가능하다.
1 | open class RichButton: Clickable { |
override
된 메서드는 당연히 오버라이딩에 열려있다. 하지만 이런 메서드도 앞에 final
을 붙이면 하위에서 오버라이딩을 막을 수 있다.
열린 클래스와 스마트 캐스트
기본적으로 클래스를 final로 상속을 막으면 스마트 캐스트하기에 유리하다. 스마트 캐스트는 타입 검사 뒤 변경될 수 없는 변수에만 적용된다. 즉val
이면서 커스텀 접근자가 있어서는 안된다. 여기에 더 나아가 프로퍼티가final
이어야 한다는 조건이 필요하다. 왜냐면 다른 클래스가 상속해서 커스텀 접근자를 정의할 수 있음을 막아야 하기 때문이다.
abstract
abstract
는 자바와 거의 비슷하다. 추상 메서드나 추상 클래스를 정의할 때 사용된다.
가시성 변경자: 기본적으로 공개
가시성 변경자는 클래스 외부 접근을 제어한다. 자바와 다르게 코틀린은 아무 가시성 변경자를 안 적으면 public
으로 취급한다.
코틀린에는 자바처럼 패키지 전용이 없다. 코틀린에서 패키지는 네임 스페이스를 관리하기 위함이지 가시성을 제어하기 위함이 아니다.
internal: 패키지 전용 가시성을 대체
패키지 전용 가시성를 대신하는 internal
이 있다. internal
은 모듈 내부에서만 볼 수 있는 변경자이다. 모듈은 같이 컴파일 되는 단위를 말한다. 패키지 기준은 다른 프로젝트더라도 같은 패키지에 클래스를 선언해서 접근 할 수 있게 되는 단점이 있다.
그 외 차이점
private
는 내부에서만 접근 가능한 변경자인데 코틀린은 최상위 선언(클래스, 프로퍼티, 함수)에도 사용할 수 있다.protected
는 코틀린에서 패키지 전용 가시성이 아닌 하위 클래스 가시성을 제공할 때 쓰인다. 다만 최상위 선언에는 사용할 수 없다.
의존하려면 더 엄한 가시성을 가져야 한다.
다음 예제를 보자
1 | internal open class TalkativeButton { |
위와 같이 다른 클래스에 의존해서 사용하려는 경우 해당 함수나 클래스의 가시성이 사용하려는 클래스의 가시성과 같거나 더 엄해야 한다. 위 경우는 확장함수의 가시성을 internal로 바꾸거나 수신 객체 타입의 가시성을 public으로 올리는 방법이 있다.
그리고 private나 protected 조건을 만족하지 않으므로 확장 함수의 메서드 호출이 안된다.
코틀린 가시성 변경자와 자바
코틀린의 public, protected, private 변경자는 자바 바이트 코드에도 그대로 들어간다. 다만 private 클래스는 다르게 처리된다. 자바에서는 private 클래스가 안되기 때문인데, 이 경우 코틀린에서 private 클래스를 protected 클래스로 변환해서 컴파일한다.
코틀린의 internal은 자바에서 public이 된다. 모듈의 특성 상 어쩔 수 없는 부분이다.
이렇게 더 열리게 되면 의도하지 않은 접근이 가능해지는데 코틀린 컴파일러가 internal 멤버 이름을 보기 나쁘게 바꾼다. 이는 예상치 못한 상속에서 생기는 우연한 오버라이딩을 피하고 internal 클래스를 외부에서 사용하지 않도록 하기 위함이다.
내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스
코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근을 할 수 없다. 자바에서는 클래슨 내부에서 클래스를 선언하면 묵시적으로 내부 클래스가 된다. 즉 외부 클래스에 대한 참조를 묵시적으로 포함한다. 자바에서 이런 보이지 않는 참조를 제거하려면 중첩된 클래스에 static을 붙여주면 된다.
코틀린은 반대다. 기본적으로 외부 클래스에 대한 참조가 끊긴 중첩 클래스로 취급하고 만약 내부 클래스로 만들려면 안쪽 클래스에 inner
변경자를 붙여야 한다. 만약 내부 클래스에서 외부 클래스 참조를 접근하려면 this@외부클래스이름
으로 하면 된다.
1 | class Outer { |
봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
sealed
변경자를 통해 자신의 상속해줄 수 있는 자식 클래스를 제한 할 수 있다. 이때 적용되는 클래스는 코틀린 1.0에는 부모의 중첩 클래스로만 해야하고, 1.1에는 같은 파일 안에 있기만 하면 된다.
1 | sealed class Expr { |
이렇게 정해놓으면 유리한 점이 분기 처리할 때가 유리하다. 자식이 무한하게 허용되면 when
식으로 처리할 때 else
로 그 외 처리를 해줘야 한다. 그리고 자식이 생길 때 분기로 처리하기를 놓칠 수 있다.
1 | fun eval(e: Expr): Int = |
하지만 sealed
되어 있으면 자식이 한정적이라 정해진 자식들만 체크하면 된다. 만약 놓친 자식이 있으면 컴파일 에러가 나서 미리 알 수 있다.
1 | fun eval(e: Expr) : Int = |
뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
코틀린은 주 생성자와 부 생성자가 있다. 주 생성자는 클래스를 초기화할 때 쓰이고 클래스 본문 밖에서 정의된다. 부 생성자는 클래스 본문 안에서 사용되는 생성자이다.
또한 코틀린에서는 초기화 블록을 지원해서 초기화 로직을 모을 수 있다.
클래스 초기화: 주 생성자와 초기화 블록
코틀린에서 주 생성자는 다음과 같이 쓰인다.
1 | class User(val nickName: String) |
클래스 이름뒤에 괄호로 둘러 쌓인 내용을 주 생성자라고 한다.
이를 최대한 명시적으로 풀어쓰면 다음과 같다.
1 | class User constructor(_nickName: String) { |
여전히 주 생성자는 존재하는데 이때 constructor
는 생성자의 정의를 시작함을 의미한다.
그리고 init
은 클래스의 객체가 만들어질 때 실행되는 초기화 로직을 모은 초기화 블록을 선언한다. 초기화 블록은 별도의 코드를 포함할 수 없는 주 생성자와 함께 많이 쓰인다.
그리고 생성자 파라미터 앞 _
는 프로퍼티와 파라미터를 구분하기 위해서 적었는데 기존 자바의 this.nickName = nickName
이렇게 해도 된다.
위 예시를 좀 더 개선하면, 일단 주 생성자 앞에 별도의 어노테이션이나 가시성 변경자가 없다면 constructor
를 생략해도 된다. 그리고 프로퍼티 초기화는 굳이 초기화 블록에서 할 필요가 없고 프로퍼티 선언에서 해도 된다.
1 | class User (_nickName: String) { |
하지만 위 예시도 굳이 val 파리미터를 본문에서 하지말고 주 생성자 안에서 해버리면 간단하다.
1 | class User(val nickName: String) |
참고로 주 생성자에서 디폴트 값과 이름 있는 선언도 가능하다.
1 | class User(val nickName: String = "untitled") |
모든 파라미터에 디폴트값이 있으면 자동으로 파라미터가 없는 생성자를 만들어준다.
DI 프레임워크 중 기본 생성자가 반드시 필요한 경우에 이런 기능이 유리하다고 한다.
만약 기반 클래스가 있다면 다음과 같이 기반 클래스에 파라미터를 넘겨줄 수 있다.
1 | class GoodUser(val nickName: String) : User(nickname) |
참고로 만약 기반 클래스가 기본 생성자만 있다하더라도 하위 클래스는 생성자 (괄호) 를 적어줘야 한다.
1 | open class Book |
인터페이스는 생성자가 없으니 하위 클래스가 구현할 때 괄호를 써주지 않는다.
마지막으로 private 한 생성자를 만드는 방법이다.
이는 동반 객체에 유용하다고 한다. (반면 유틸 클래스나 싱글턴에는 이렇게 하지 않느다. 확장 함수나 객체 선언하는 방식을 한다.)
1 | class CannotConstructUser private constructor() |
부 생성자
일반 적으로 코틀린에서는 디폴트 값을 지원해서 여러 생성자를 만들 일이 없다.
그래도 여러 생성자가 필요한 경우가 있다면 부 생성자를 이용한다.super
를 통해 기반 클래스의 생성자에 인자 전달도 되고, this를 통해 자신의 다른 생성자에게 생성을 위임할 수 있다.
1 | open class View { |
인터페이스에 선언된 프로퍼티 구현
추상 프로퍼티
코틀린에서는 인터페이스에 추상 프로퍼티를 선언할 수 있다.
1 | interface User { |
이는 해당 인터페이스를 구현하는 쪽은 nickName을 접근할 수 있는 방법을 제공해야 함을 의미한다. 인터페이스가 실제 상태를 가지는 것은 아니다.
추상 프로퍼티를 구현하는 세가지 예시를 보자
1. 주 생성자로 프로퍼티 구현
1 | class PrivateUser(override val nickName: String) : User5 |
간단하다 override만 붙여주면 된다.
2. 커스텀 게터
1 | class EmailUser(val email: String) : User5 { |
역시 override가 붙은 프로퍼티에 커스텀 게터를 구현하면 된다. 다만 커스텀 게터는 호출때마다 구현된 내용이 실행된다.
3. 프로퍼티 초기화 식
1 | class FacebookUser(val id: Int) : User5 { |
프로퍼티 초기화 식은 프로퍼티에 접근할 때마다 구현된 내요을 실행하지 않는다. 초기화 할 때 한번만 한다. 이 점이 2번과 가장 큰 차이다. 만약 getFacebookName
이 비용이 큰 메서드라고 상상해보면 프로퍼티에 접근할 때마다 호출되는 커스텀 게터 방식이 많이 불리햇을 것이다.
마지막으로 인터페이스에는 커스텀 게터와 세터가 있는 프로퍼티를 추가할 수 있다. 물론 실제 값이 있는 건 아니니까 참조할 수 없다.
1 | interface User { |
게터와 세터에서 뒷받침하는 필드에 접근
프로퍼티에 저장된 값을 변경할 때 특정 문자열을 출력하려고 한다고 해보자.
1 | class User(val name: String) { |
흠 세터를 직접 구현해줬다. 그런데 field
는 무엇인가? field
는 현재 접근자에 해당하는 필드에 접근할 수 있게 한다. field
를 사용하지 않는 커스텀 접근자는 뒷받침 하는 필드가 생기지 않는다.
접근자의 가시성 변경
접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 하지만 get
이나 set
앞에 접근자를 선언해서 가시성을 변경할 수 있다.
1 | class User8 { |
컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
모든 클래스가 정의해야 하는 메서드
모든 코틀린 클래스는 toString
, equals
, hashCode
등을 오버라이딩해야 한다.
코틀린은 이런 메서드 구현을 자동으로 생성해줄 수 있다.
1 | class Client(val name: String, val postalCode: Int) |
다음과 같은 클래스를 예시로 오버라이딩 해보자.
toString()
1 | class Client(val name: String, val postalCode: Int) { |
equals()
참고로 코틀린은 ==
연산자가 내부적으로 equals
메서드를 호출해서 객체를 비교한다. 참조 비교를 위해서는 ===
연산자를 호출해서 사용할 수 있다.
1 | class Client(val name: String, val postalCode: Int) { |
hashCode
자바에서 equals
를 재정의할 때는 반드시 hashCode
도 재정의해야 한다. JVM 언어에서는 equals
가 true를 반환하는 두 객체를 반드시 같은 hashCode
를 반환해야 한다. Hash
를 활용하는 자료구조에서는 원소 비교 비용을 줄이기 위해 먼저 객체의 해시 코드를 비교하고 같은 경우에만 실제 값을 비교한다.
1 | class Client(val name: String, val postalCode: Int) { |
데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성
코틀린은 이런 메서드를 컴파일러가 생성해준다.data
변경자를 붙여주기만 하면 된다. data
변경자가 붙은 클래스를 데이터 클래스라고 부른다.
1 | data class Client(val name: String, val postalCode: Int) |
이때 주의할 점은 equals
와 hashCode
는 주 생성자에서 선언된 모든 프로퍼티를 기준으로 만들어진다. 주 생성자 외부에서 선언된 프로퍼티는 고려되지 않는다.
데이터 클래스는 equals
, hashCode
, toString
외에도 유용한 메서드 몇 개를 더 만들어준다.
불변한 데이터 클래스를 쉽게 복사: copy
데이터 클래스를 쉽게 복제하는 메서드를 제공해준다. 밑은 copy
메서드를 이해를 돕기위해 직접 구현한 예시이다.
1 | class Client(val name: String, val postalCode: Int) { |
클래스 위임: by 키워드 사용
어떤 클래스에 기능을 추가해야 할 때 데코레이터 패턴을 사용한다고 해보자. 데코레이터 패턴은 기존 클래스의 인터페이스를 데코레이터가 제공하되 데코레이터 내부에 기존 클래스 인스턴스를 필드로 갖고 메서드 호출시 기존 클래스에게 메시지를 전달하는 것이다. 그리고 데코레이터에 추가하고자 하는 메서드를 구현하면 된다. 이때 새로운 기능에 기존 클래스의 메서드를 활용할 수 있다.
하지만 이런 방법은 지나치게 준비 코드가 많다. 코틀린에서는 인터페이스를 구현할 때 by
키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에게 위임 중이라는 사실을 명시할 수 있다.
예를 들어 ArrayList를 감싸는 클래스를 만들어보자. Collection의 인터페이스를 내부 리스트에게 위임하도록 구현해야 했다.
1 | class DelegatingCollection<T> : Collection<T> { |
하지만 코틀린의 by
를 써보자. Set에서 추가된 원소를 카운팅하는 기능을 추가해보자.
구현한 인터페이스 뒤에 by
와 함께 프로퍼티 이름을 적어준 것을 확인할 수 있다. 이러면 위임 메서드를 컴파일러가 알아서 만들어준다. 다만 여기서 자동으로 만들어진 메서드 대신 개발자가 직접 구현하고 싶으면 override 붙여서 구현해주면 된다.
1 | class CountingSet<T> ( |
object 키워드: 클래스 선언과 인스턴스 생성
코틀린에서 object
키워드를 다양한 상황에서 사용하지만 모든 경우 클래스를 정의하면서 동시에 인스턴스를 생성한다는 공통점이 있다. 다양한 상황에 대해서 알아보자.
객체 선언: 싱글턴을 쉽게 만들기
코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.
모든 직원의 급여 대장을 관리하는 객체가 필요하다고 해보자. 이 객체가 굳이 여러개일 필요는 없으니 객체 선언으로 싱글턴 객체로 만들어보자.
1 | object Payroll { |
객체 선언은 object
키워드를 사용하면 된다.객체 선언은 그 클래스를 정의하고 해당 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리한다.
하지만 생성자는 객체 선언에 사용할 수 없다. 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문이다.
객체 선언도 클래스나 인스턴스를 상속받을 수 있다. 예를 들어 특정 클래스를 위해 Comparator
를 구현한 객체는 여러 개가 필요없다.
1 | object FileComparator : Comparator<File> { |
싱글톤과 의존관계 주입
싱글턴 패턴과 마찬가지로 객체 선언은 대규모 시스템에서 안좋은 경우가 있다.
객체 생성을 제어할 수 없고 파라미터를 지정할 수 없기 때문이다.
그래서 단위 테스트하거나 시스템 설정이 바뀔 때 의존 객체를 바꿔줄 수 없다. 만약 이런 기능이 필요하다면 의존 관계 주입 프레임워크를 사용해보자.
클래스 안에 객체 선언
클래스 안에 객체 선언을 해도 그 객체는 싱글톤이다. 외부 클래스가 인스턴스화 된다고 객체 선언이 여러번 객체로 만들어지는게 아니다!
1 | data class OuterClass(val name: String) { |
자바에서 코틀린 객체 선언된 객체를 접근하기
코틀린 객체 선언은 자바에서 정적 필드를 가진 클래스로 컴파일된다. 이때 정적 필드 이름은 항상INSTANCE
다.OuterClass.InnerObject.INSTANCE.hello()
동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소
코틀린 클래스 안에는 정적인 멤버가 없다. 코틀린은 static
키워드를 지원하지 않는다. 대신 최상위 함수와 객체 선언을 사용한다. 일반적으로 최상위 함수를 추천하지만 특정 클래스 내부의 private 프로퍼티나 메서드에 접근하지 못하는 경우는 해당 클래스 안에서 객체 선언을 통해 접근하기도 한다.
1 | data class NamedPerson(private val name: String) { |
이를 companion
키워드를 통해 중첩된 객체 선언에서 클래스 이름을 제거할 수 있다.(물론 이름을 붙여줄 수도 있다.) 마치 자바의 정적 멤버 처럼 활용할 수 있게 된다.
1 | data class NamedPerson2(private val name: String) { |
동반 객체를 통해 private 생성자 호출: 팩토리 메서드
동반 객체는 외부 클래스의 private 프로퍼티, 메서드, 생성자에 접근할 수 있다. 그래서 팩토리 패턴을 사용하기 좋은 조건을 가졌다.
1 | class Man private constructor(val name: String) { |
팩터리 패턴이냐 여러 부 생성자냐
상황에 따라 여라가지 부 생성자로 객체를 만들어주도록 할 수 있다. 이를 동반 객체를 통한 팩토리 패턴을 활용해서 이름 있는 메서드로 가독성을 높일 수 있는데, 문제는 클래스를 확장해야 할 경우 동반 객체 멤버를 오버라이딩 할 수 없으므로 여러 생성자를 사용하는 편이 낫다.
동반 객체를 일반 객체처럼 사용
1 | interface Creatable<T> { |
동반 객체도 인터페이스를 구현할 수 있으며, 만들어진 동반 객체가 매개변수로 사용됐음을 주목하라. 이때 동반 객체의 이름이 없어서 Boy
로 인자를 전달해줬음을 주목하자.
자바에서 코틀린 동반 객체
자바에서 코틀린 동반 객체는 이름이 있으면 해당 이름으로 정적 멤버 접근하면 되고 이름이 없는 경우는Companion
이라는 이름의 정적 멤버로 접근하면 된다.
동반 객체 확장
이름 없는 동반 객체를 만들어서 비즈니스 객체에서 보고 싶지 않은 코드를 분리할 수 있다. 예를 들어 JSON으로부터 역직렬화 함수를 만들어서 제공하고 싶은데 이를 비즈니스 클래스 내부에 위치하고 싶지 않을 때 사용해보자.
1 | // 비즈니스 모듈 |
객체 식: 무명 내부 클래스를 다른 방식으로 작성
무명 객체를 정의할 때도 object
키워드를 사용한다. 무명 객체는 자바의 무명 내부 클래스를 대신한다.
이때 중요한 점은 무명 객체로 object
키워드를 사용하면 싱글톤이 아님을 명심하자. 무명 객체와 동반 객체나 객체 선언과 다르다.
1 | var listener = object : MouseAdapter() { |
그리고 무명 객체(객체 식)은 그 식이 포함된 함수의 변수에 접근할 수 있다.
1 | fun foo(window: Window) { |