코틀린의 인터페이스를 알아보도록 하자
코틀린의 인터페이스는 자바의 인터페이스와는 차이가 있다.
- 코틀린의 인터페이스에서는 프로퍼티 선언이 가능하지만 자바는 불가능하다.
- 자바에는 implements를 통해 인터페이스를 구현하지만 코틀린은 콜론(:)을 사용한다.
- 코틀린의 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다. (이는 자바 8의 디폴트 메서드와 비슷하다고 한다)
그럼 지금부터 코틀린 인터페이스의 예시들을 보며 알아가보도록 하자.
자바 코드는 이해를 돕기 위해 참고용으로 매 코드마다 넣어놨다.
override 키워드와 인터페이스
우선 간단하게 인터페이스를 선언해보자
interface Soccer {
fun kick()
}
자바 코드
// java도 큰 차이는 없다
public interface Soccer {
void kick();
}
위 코드는 kick 이라는 추상 메서드가 있는 Soccer 라는 인터페이스를 정의한다.
Soccer 인터페이스를 구현하는 모든 비추상 클래스는 kick 을 구현해야한다.
그럼 Soccer 인터페이스를 구현해보자!
// kotlin
class Player() : Soccer {
override fun kick() {
println("공을 찬다.")
}
}
자바 코드
// java
public class Player implements Soccer {
@Override
public void kick() {
System.out.println("공을 찬다.");
}
}
// java decompile
final class Player implements Soccer {
public Player() {
}
public void kick() {
System.out.println("공을 찬다.");
}
}
자 이러면 Soccer 인터페이스 구현이 끝났다.
우리 방금 kick 메서드를 구현할 때 override 키워드를 사용했다.
override 변경자는 상위 클래스나 인터페이스에 있는 프로퍼티나 메서드를 오버라이드한다는 표시이다.
여기서도 자바와의 차이가 발생하는데
자바는 @Override 어노테이션을 붙여주지 않아도 오버라이드가 되는 반면 (물론 거의 붙이긴 한다)
코틀린은 override 를 붙여야만한다.
// java code
interface Soccer {
public void kick();
}
class Player implements Soccer {
public void kick(){ // 위에 @Override 어노테이션을 붙여주지 않아도 된다.
System.out.println("공을 찬다.");
}
}
이는 실수로 상위 클래스의 메서드를 오버라이드하는 경우를 방지해준다.
예를 들어 override 변경자를 강제하지 않는다면 Player 에서 Soccer 에 존재하는 메서드를 모른 채 같은 이름의 메서드를 만들 수도 있기 때문이다.
물론 지금은 kick 이 추상 메서드이기에 오버라이드하지 않으면 컴파일 에러로 빨간 줄이 생기겠지만,,
구현이 있는 메서드라면 어떨까?
interface Soccer {
fun kick()
fun throwIn() = println("공을 양손으로 던진다.")
fun tackle() {
println("공을 뺏기 위해 다리를 뻗는다.")
}
}
위 코드처럼 인터페이스의 메서드도 디폴트 구현을 제공할 수 있다.
default 키워드를 붙여야하는 자바 8과 달리
코틀린에서는 특별한 키워드 없이 메서드 본문을 메서드 시그니처 뒤에 추가하면 된다.
자바 코드
// java
public interface Soccer {
void kick();
default void throwIn() {
System.out.println("공을 양손으로 던진다.");
}
default void tackle() {
System.out.println("공을 뺏기 위해 다리를 뻗는다.");
}
}
// decompile kotlin to java
public interface Soccer {
void kick();
void throwIn();
void tackle();
public static final class DefaultImpls {
public static void throwIn(@NotNull domain.Soccer $this) {
System.out.println("공을 양손으로 던진다.");
}
public static void tackle(@NotNull domain.Soccer $this) {
System.out.println("공을 뺏기 위해 다리를 뻗는다.");
}
}
}
위 코드를 보면 디컴파일된 자바 코드가 default 메서드가 아닌 추가 클래스의 정적 메서드로 구현되어있다.
위에서 분명 자바의 default 메서드라고 했는데 왜 이런 일이 발생한걸까?
default는 자바 8부터 생긴 키워드이다. 그러므로 그 이전의 자바를 타겟으로 하는 코틀린의
인터페이스의 구현부가 있는 메서드는 위와 같이 정적 메서드로 변환되었다.
하지만 이상하다,, 나는 자바 11을 타겟으로 잡고있기 때문이다.
코틀린은 1.2버전에서 @JvmDefault 어노테이션을 만들어 코틀린 코드가 자바 8을 타겟으로 할 때
해당 어노테이션을 통해 구현부가 있는 인터페이스의 메서드를 default 메서드로 변환해주었다.
이 어노테이션은 compiler argument가 -Xjvm-default 라는 특정 컴파일러 모드에서만 작동한다.
그러나 구현부를 가지고 있는 모든 메서드에 어노테이션을 붙여주는 것은 꽤나 번거로운 일이다.
그래서 코틀린은 자바 8 이상을 타겟으로 할 땐 어노테이션 없이 default 메서드로 생성하길 원했다.
하지만 다른 코틀린 버전과 다른 모드로 컴파일된 애플리케이션의 라이브러리나 모듈을 혼합할 때 문제가 생길 수도 있기 때문에 빠르게 진행되기가 어렵다고한다.
또한 향후 버전의 코틀린 컴파일러는 기본 메서드의 이전 체계를 계속 이해하면서 새 체계로 천천히 마이그레이션할 예정이라고한다.
코틀린 1.4에서 인터페이스에서 default 메서드를 생성하기 위한 새로운 모드를 발표했다.
코틀린 코드 타겟이 자바 8 이상이고 인터페이스에서 default 메서드를 생성하고싶다면 compiler argument를
-Xjvm-default=all 과 -Xjvm-default=all-compatibility 두 가지 모드 중 한 가지 사용하면 된다.
all 모드는 컴파일러에 의해 DefaultImpls 객체(정적 메서드를 갖고있는) 없이
오로지 default 메서드로만 만들어진다.
all-compatibility 모드는 DefaultImpls과 default 메서드 모두 생성해준다.
이건 all 모드를 사용할 때 오래된 코드를 리컴파일 어쩌고 하면 어떤 문제가 생겨서 all-compatibility를 쓴다는데 난 잘 모르겠으니 링크를 남긴다.
Kotlin 1.4-M3: Generating Default Methods in Interfaces | The Kotlin Blog (jetbrains.com)
그래서 gradle에서 위 사진처럼 freeCompilerArgs를 all로 변경해주었더니 default 메서드로 디컴파일되었다.
위 사진이 맞는 문법인지는 모르겠다. 나도 그냥 구글링 하다가 찾은거라,,
다른 모드로 바꾸고 싶다면 -Xjvm-default= 뒷부분을 바꿔주면 된다.
추가로 이상한 모드를 입력하고 돌려보면 아래처럼 intellij 아래와 같은 모드들이 있다고 알려준다.
인터페이스에 구현부가 있는 메서드는 다음 코드처럼 굳이 오버라이드하여 구현하지 않아도 된다.
interface Soccer {
fun kick()
fun throwIn() = println("공을 양손으로 던진다.")
fun tackle() {
println("공을 뺏기 위해 다리를 뻗는다.")
}
}
class Player() : Soccer {
override fun kick() {
println("공을 찬다.")
}
}
자바 코드
// java
public class Player implements Soccer {
@Override
public void kick() {
System.out.println("공을 찼습니다.");
}
}
// decompile kotlin to java
final class Player implements Soccer {
public Player() {
}
public void kick() {
System.out.println("공을 찼습니다.");
}
public void throwIn() {
DefaultImpls.throwIn(this);
}
public void tackle() {
DefaultImpls.tackle(this);
}
}
이런 경우 override 변경자를 강제하여 실수로 상위의 메서드를 오버라이드 하는 경우를 방지할 수 있다.
Soccer 와 Player 가 다른 파일에 있었다고 해보자.
그럼 Soccer 가 있는 파일에 직접 들어가보기 전까지는 어떤 메서드들이 있는지 알기 어렵다.
이때 @Override 어노테이션을 강제하지 않는 자바라고 생각해본다면
Soccer 에 tackle 이라는 메서드가 있는지 모른 채 Player 에서 같은 이름의 tackle 을 새로 구현하게 될 것이다.
그럼 자기도 모르는 사이에 Soccer 의 tackle 을 오버라이드해서 새로 구현한 상황이 되어버린다.
public interface Soccer {
void kick();
default void throwIn() {
System.out.println("공을 양손으로 던진다.");
}
default void tackle() {
System.out.println("공을 뺏기 위해 다리를 뻗는다.");
}
}
public class Player implements Soccer {
@Override
public void kick() {
System.out.println("공을 찼습니다.");
}
public void tackle(){
System.out.println("다리를 뻗어 공을 뺏는다.");
}
}
이런 상황을 방지하기 위해 코틀린은 override 키워드를 강제한다.
프로퍼티와 인터페이스
자 그럼 코틀린 인터페이스의 또 한가지 차이점을 알아보자.
자바의 인터페이스는 불가능하지만 코틀린의 인터페이스에선 프로퍼티의 선언이 가능하다.
위에서 봤던 Soccer 인터페이스에 프로퍼티를 추가해보겠다.
interface Soccer {
val ball: Ball
fun kick()
fun throwIn() = println("공을 양손으로 던진다.")
fun tackle() = println("공을 뺏기 위해 다리를 뻗는다.")
}
class Ball() {
var posX: Int = 0
var posY: Int = 0
}
축구공의 역할을 하는 ball 프로퍼티를 선언했다.
그럼 구현부가 있는 메서드처럼 프로퍼티도 초기화가 가능할까?
아쉽게도 불가능하다.
그럼 인텔리제이가 시키는대로 "Convert property initializer to getter" 를 해보겠다.
이렇게 커스텀 접근자를 통해 값을 가져오는 것은 가능하다.
여기서 우리가 알 수 있는 것은 프로퍼티 선언은 가능하지만 필드는 가질 수 없다는 것이다.
즉, 코틀린의 인터페이스는 프로퍼티 선언이 가능하지만 아무런 상태(필드)도 들어갈 수 없다.
자바 코드
자바로 생각해보면 ball 프로퍼티 선언은 필드 없이 getBall()이라는 메서드로 존재할 것이고
get() = Ball()이 추가된 경우라면 getBall()에 대한 구현부가 정적 메서드로 존재할 것이다.
// ball 프로퍼티를 선언만 했을 때
public interface Soccer {
@NotNull
Ball getBall();
void kick();
void throwIn();
void tackle();
public static final class DefaultImpls {
public static void throwIn(@NotNull Soccer $this) {
System.out.println("공을 양손으로 던진다.");
}
public static void tackle(@NotNull Soccer $this) {
System.out.println("공을 뺏기 위해 다리를 뻗는다.");
}
}
}
// ball 프로퍼티를 선언하고 커스텀 접근자 get()을 사용했을 때
public interface Soccer {
@NotNull
Ball getBall();
void kick();
void throwIn();
void tackle();
public static final class DefaultImpls {
@NotNull
public static Ball getBall(@NotNull Soccer $this) {
return new Ball();
}
public static void throwIn(@NotNull Soccer $this) {
System.out.println("공을 양손으로 던진다.");
}
public static void tackle(@NotNull Soccer $this) {
System.out.println("공을 뺏기 위해 다리를 뻗는다.");
}
}
}
그리고 ball 프로퍼티가 선언된 Soccer 인터페이스를 구현한 Player 는 아래와 같다.
class Player(override val ball: Ball) : Soccer {
override fun kick() {
println("공을 찬다.")
}
}
자바 코드
public final class Player implements Soccer {
@NotNull
private final Ball ball;
public Player(@NotNull Ball ball) {
Intrinsics.checkNotNullParameter(ball, "ball");
super();
this.ball = ball;
}
@NotNull
public Ball getBall() {
return this.ball;
}
public void kick() {
System.out.println("공을 찬다.");
}
public void throwIn() {
DefaultImpls.throwIn(this);
}
public void tackle() {
DefaultImpls.tackle(this);
}
}
여러 인터페이스를 구현
코틀린도 자바와 마찬가지로 클래스는 하나의 상위 클래스만 상속 받을 수 있으며
여러 상위 인터페이스를 개수 제한 없이 구현할 수 있다.
그럼 여러 인터페이스를 구현하는 경우를 한번 살펴보자
interface Soccer {
val ball: Ball
fun kick() = println("공을 찬다.")
fun throwIn() = println("공을 양손으로 던진다.")
fun tackle()
}
interface Taekwondo {
val state: State
fun kick() = println("발차기를 한다.")
fun guard()
}
Soccer 와 Taekwondo 라는 두 인터페이스가 있다.
두 인터페이스는 kick 이라는 같은 이름의 메서드가 존재한다. (기능은 다르다)
그럼 이 경우 Soccer 와 Taekwondo 를 상위 인터페이스로 갖는 Player 는 어떻게 구현해야할까?
현재 guard 와 tackle 만 구현한 상황이다.
이런 경우 같은 이름의 여러 인터페이스 메서드를 상속하고있기 때문에 kick 메서드의 구현을 강제한다.
즉 코틀린 컴파일러는 두 메서드를 아우르는 구현을 하위 클래스에 직접 구현하게 강제한다.
이렇게 kick 을 구현해주면 에러가 사라진다.
그렇다면 super 를 사용해 상위 타입의 메서드를 사용하고싶다면 어떻게 해야할까?
super 와 꺾쇠(<>)를 이용하여 사용할 수 있다.
override fun kick() {
super<Soccer>.kick()
super<Taekwondo>.kick()
}
이렇게 코틀린 인터페이스의 사용법에 대해 알아보았다.
다음 게시물은 코틀린 클래스들의 여러 키워드로 돌아오겠다!
'Kotlin' 카테고리의 다른 글
[Kotlin] 확장 함수! 신기한 당신의 정체는? (0) | 2023.04.24 |
---|---|
[Kotlin] 프로퍼티 용어 정리 (0) | 2023.04.05 |
[Kotlin] 인터페이스와 추상클래스의 차이 (1) | 2023.03.22 |
[Kotlin] 코틀린의 헷갈리는 생성자와 프로퍼티 알아보기 (Java와 비교하여) (0) | 2023.02.20 |