Kotlin 클래스에 대해서…
객체 지향 프로그래밍을 하면서 클래스와 인터페이스 개념을 빼놓을 수는 없을 것 같다. Kotlin
에서도 다양한 클래스를 지원하고 있고, 그와 상응하게 Java
코드로 변환 처리는 어떻게 되고 있는지 파악하여 정확하게 Kotlin
개발을 할 필요가 있다고 생각한다.
- 클래스와 인터페이스
- 생성자와 프로퍼티
- 데이터 클래스
- 클래스 위임
- object 키워드
클래스와 인터페이스
인터페이스
Kotlin
의 인터페이스는 Java 8
의 인터페이스와 비슷하다. 추상 메소드뿐 아니라 구현이 있는 메소드도 정의할 수 있다. 하지만 인터페이스에는 아무런 상태(필드)도 저장될 수 없다.
- 콜론(
:
) :Java
에서 클래스나 인터페이스 상속은extends
또는implements
키워드를 사용하지만Kotlin
에서는 콜론(:
)을 붙이고, 클래스와 인터페이스 이름을 적는다. override
:Java
의@Override
와 비슷한 기능을 하는override
변경자는 상위 클래스나 상위 인터페이스에 정의되어 있는 프로퍼티나 메소드를 오버라이드할 때 사용한다.Kotlin
에서 인터페이스안에default
메소드를 정의할 때는,Java
와 달리default
변경자와 같은 키워드 없이 구현 가능하다.- 상위 객체의
default
메소드를 오버라이드할 때는,super
접근자를 통해 타입 지정하여 사용 가능하다.
- 상위 객체의
open, final, abstract 변경자: 기본적으로 final
(상속 제어 변경자)
객체 지향 프로그래밍을 하면서 상속을 활용한 클래스 설계 기법은 모두 익히 잘 알고 있을 것이고, 상속을 통해 많은 장점과 이점을 취할 수 있음도 잘 알고 있을 것이다. 결론부터 말하자면, Kotlin
에서는 상속을 기본적으로 금지하며 변경자 자체를 기본적으로 final
변경자로 선언되게끔 되어있다.
상속을 활용한 설계의 단점으로는, 취약한 기반 클래스 (fragile base class) 라는 문제가 있다. 이는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스 즉 상위 클래스가 변경함으로써 깨져버릴 수 있는 경우를 말한다.
Java
프로그래밍 기법을 다룬 책 중 유명한 Effective Java
에서도 “상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라” 라는 조언도 있다고 한다. Kotlin
도 이와 같은 철학을 기반으로 모든 클래스와 메소드는 기본적으로 final
로 선언된다.
open 변경자
Kotlin
는 기본적으로 final
이기 때문에, 상속이나 override
가 필요한 경우에는, open
변경자를 사용한다.
open class RichButton
: 해당 클래스는open
이고, 다른 클래스가 상속할 수 있다.fun disable()
: 해당 함수는final
이기 때문에, 하위 클래스가 이 함수를 오버라이드할 수 없다.open fun animate()
: 해당 함수는open
이기 때문에, 하위 클래스에서 이 함수를 오버라이드할 수 있다.override fun click()
: 상위 클래스에서 선언된 함수를 오버라이드하여 구현한다.
abstract 변경자
abstract
변경자는 추상 클래스를 쿠현할 때 사용한다. 추상 클래스는 인스턴스화할 수 없으며, 구현이 없는 추상 멤버가 있기 때문에 하위 클래스에서 그 추상 멤버를 오버라이드해야만 한다.
추상 멤버는 항상 open
열려 있다. 따라서 별도 open
변경자를 명시할 필요 없다.
- 인터페이스 멤버의 경우, 항상 열려있기 때문에
final
로 변경할 수 없다.- 인터페이스 멤버에게 본문이 없다면 자동으로 추상 멤버가 되며, 따로 멤버 선언 앞에
abstract
를 명시할 필요가 없다.
Kotlin 상속 제어 변경자
변경자 | 설명 |
---|---|
final |
오버라이드할 수 없음 |
open |
오버라이드할 수 있음 |
abstract |
반드시 오버라이드해야 함 |
override |
상위 클래스의 멤버를 오버라이드 함 |
override
한 멤버는 항상open
상태이다. 다시 하위 클래스에는 오버라이드를 금지하고자 한다면,final
변경자를 명시해줘야 한다.
가시성 변경자: 기본적으로 public
Kotlin
에서 가시성 변경자(visibility modifier
)는 Java
와 비슷하면서 다른 부분이 있다.
Java
에서 제공하는 패키지 전용 가시성 변경자는 Kotlin
에는 없다. 대신 같은 모듈에서만 볼 수 있는 internal
변경자가 있다. 이와 같이 비슷하면서도 다른 가시성 변경자를 가지고 있으며, 기본적으로는 public
변경자를 따른다.
Kotlin 가시성 변경자
변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public |
모든 곳에서 접근 가능 | 모든 곳에서 접근 가능 |
internal |
같은 모듈 안에서만 접근 가능 | 같은 모듈 안에서만 접근 가능 |
protected |
하위 클래스 안에서만 접근 가능 | 적용 불가 |
private |
같은 클래스 안에서만 접근 가능 | 같은 파일 안에서만 접근 가능 |
가시성 변경자 유의 사항
- 접근하고자 하는 함수나 객체에서 그보다 가시성이 낮은 경우 참조하지 못한다.
- 외부 클래스가 내부 클래스 또는 중첩된 클래스의
private
멤버에 접근 불가하다.
내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스 nested class
Java
처럼 Kotlin
에서도 클래스 안에 다른 클래스를 선언하여 사용 가능하다. 이런 클래스는 내부 클래스(inner class) 라고 정의하는데 어떤 경우에 사용하면 좋을지 먼저 살펴보면, 2가지 정보가 있을 것 같다.
- 클래스안에서 필요한 별도의 클래스를 캡슐화하거나
- 코드 정의를 가까운 곳에 두고 싶을 때
Java 의 내부 클래스 예외 상황
Java
에서는 클래스안에 있는 내부 클래스를 그냥 선언하고 사용한다면, NotSerializableException
에러가 발생한다. 이 이유는 내부 클래스가 바깥쪽 클래스에 대한 묵시적 참조를 하고 있기 때문이다. 그래서 Java
에서는 이를 해결하기 위해 내부 클래스를 static
클래스로 변경해줘서 바깥 클래스와의 참조를 없애야 한다.
Kotlin
에서는 이러한 문제 때문에 클래스 안에 생성된 클래스는 중첩 클래스(nested class) 로 기본 생성하게 하였고, 이는 static
이 붙은 Java
중첩 클래스와 같다. 그리고 바깥쪽 클래스의 참조를 하고자 한다면, inner
변경자를 통해 생성 가능하다.
Java 와 Kotlin 의 중첩 클래스 & 내부 클래스 차이
구분 | Java | Kotlin |
---|---|---|
중첩 클래스 (바깥쪽 클래스에 대한 참조 X) | static class A |
class A |
내부 클래스 (바깥쪽 클래스에 대한 참조 O) | class A |
inner class A |
내부 클래스에서 바깥쪽 클래스의 참조
this@Outer
: 내부 클래스Inner
안에서 바깥쪽 클래스Outer
접근하고자 할 때 사용
봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
봉인된 클래스(sealed class) 는 말그대로 상위 클래스는 봉인할 때 사용한다. 클래스는 봉인한다라는 의미는 상위 클래스를 상속한 하위 클래스 정의를 제한한다 라고 할 수 있다.
상위 클래스를 봉인함으로써, 하위 클래스를 검증하는 로직에서 예외 처리를 하지 않아도 되는 이점이 생긴다.
sealed
변경자가 있는 상위 클래스와 상속한 하위 클래스 정의 예제를 살펴보자.
sealed
변경자를 명시하면서,Expr
클래스는Num
과Sum
클래스 외에는 정의할 수 없게 되었다.eval()
함수에서when
검사할 때, 더이상else
문을 작성하지 않아도 된다.
클래스 초기화: 주 생성자와 초기화 블록
Java
와 비슷하면서도 다른 Kotlin
의 클래스 선언 방식에 대해 알아보고자 한다.
Kotlin
은 주primary
생성자와 부secondary
생성자를 구분하고 있으며, 초기화 블록 initializer block
을 통해 클래스를 초기화할 수 있다.
주 primary
생성자
- 주 생성자는 생성자의 파라미터를 지정
- 주 생성자는 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의
위 코드는 아래와 같다.
constructor
: 주 생성자나 부 생성자를 정의할 때 사용하는 키워드init { ... }
: 클래스의 객체가 생성될 때(인스턴스화) 실행될 초기화 블록- 주 생성자는 제한적이기 때문에 별도의 로직이 필요한 경우 초기화 블록을 사용
- 클래스안에는 여러 초기화 블록 정의 가능
생성자 파라미터 default
값 지정
isSubscribed
프로퍼티는Boolean
형이고,true
를default
값을 가진다.
모든 프로퍼티가
default
값을 가지는 경우,Kotlin
컴파일러는 자동으로 파라미터가 없는 생성자를 만들어 준다. 그런 생성자는 생성될 때default
값을 활용해서 클래스를 인스턴스화한다.
비공개 생성자
constructor
앞에private
변경자를 붙이면, 해당 생성자는 비공개가 된다.- 클래스의 유일한 생성자를 비공개 처리하면, 클래스 외부에서 생성이 불가능하다.
비공개 생성자를 가진 클래스는 객체 인스턴스화가 필요없는 유틸리티 함수를 담아두는 클래스에 활용하기 용이하다.
부 secondary
생성사
- 부 생성자는
constructor
키워드로 시작하여 정의 가능하다. super
키워드를 통해서 부 생성자도 부모 클래스의 생성자를 활용하여 인스턴스화 가능하다.this
키워드를 활용하여 같은 클래스의 다른 생성자를 활용할 수 있다.
부 생성자의 필요성
- 주된 이유는
Java
와의 상호운용성이다. - 인스턴스화에 필요한 특정 방법이 있는 경우 부 생성자를 활용할 수 있다.
인터페이스의 추상 프로퍼티
- 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드 또는 게터 등의 정보가 없다.
- 인터페이스는 아무 상태도 포함할 수 없기 때문에 상태 저장할 필요가 없다.
- 인터페이스를 구현하는 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 구현할 필요가 있다.
인터페이스 프로퍼티 구현
PrivateUser
클래스처럼 상속받은 인터페이스의 프로퍼티를 오버라이드해서 주 생성자에 포함시켜주면 된다.
하지만 필요에 의해 인터페이스의 프로퍼티를 특별하게 상태 저장할 수 있는 방법이 있다.
SubscribingUser
클래스처럼 상위 인터페이스의 프로퍼티를 커스텀 게터를 사용하여 구현할 수 있다.
Kotlin
은 이처럼 인터페이스의 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수 있다.
접근자 메소드: 게터 getter
& 세터 setter
인터페이스의 추상 프로퍼티를 구현하는 커스텀 게터/세터를 선언할 수 있지만, 뒷받침하는 필드를 참조할 수 없기 때문에 매번 동일한 결과 처리를 할 수 밖에 없고, 그로인해 처리 비용이 높아질 수 밖에 없다.
Kotlin
에서는 위와 같은 상황을 방지하기 위해서 게터/세터에서 뒷받침하는 필드에 접근할 수 있는 방법을 제시한다.
field
라는 특별한 식별자가 뒷받침하는 필드에 접근을 가능하게 한다.- 위 코드에서 세터에서의
field
는address
프로퍼티의 뒷받침하는 필드를 가리킨다.
- 위 코드에서 세터에서의
- 게터에서는
field
값을 읽을 수 있다. - 세터에서는
field
값을 읽거나 쓸 수 있다.
접근자의 가시성 변경
접근자의 가시성은 기본적으로 프로퍼티의 가시성을 따라가지만, 특별한 경우에 접근자의 가시성을 다르게 지정할 수 있다.
counter
프로퍼티의setter
메소드를private
으로 지정하여 해당 클래스에서만 접근 가능하도록 하였다.addWord()
메소드를 통해서counter
프로퍼티를 변경할 수 있다.
지금까지 Kotlin
의 클래스의 전반적인 모습과 기능을 살펴보았다.
하지만 이외에도 Kotlin
에서는 Java
와 다른 클래스를 작성할 수 있는 방법들이 있는데, 분량이 많은 관계로 다음 포스트로 넘기도록 하겠다.