코틀린에서의 인터페이스와 추상클래스의 차이에 대해 알아보자
- 구현적 관점에서의 차이
- 개념적 관점에서의 차이
구현적인 관점에서의 차이
- 프로퍼티 선언
- 다중, 단일 상속
구현적인 관점에서 (코틀린의) 인터페이스와 추상클래스의 큰 차이는 없다고 생각한다.
프로퍼티 선언
인터페이스와 추상클래스 모두 자체로는 생성이 불가능하며 구현부(함수 바디)가 있는 함수가 존재할 수 있다.
뿐만 아니라 프로퍼티 또한 둘다 선언할 수 있는데 여기서 조금의 차이가 발생한다.
코틀린 인 액션 (이하 코인액)에서는 이렇게 말한다. (말이 어렵다면 넘어가도 좋다)
인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
물론 그런 게터와 세터는 뒷받침하는 필드를 참조할 수 없다. (뒷받침하는 필드가 있다면 인터페이스에 상태를 추가하는 셈인데 인터페이스는 상태를 저장할 수 없다.)
예를 하나 살펴보자
interface User {
val email: String // 추상 프로퍼티
val nickname: String // 게터가 있는 프로퍼티 - 프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다.
get() = email.substringBefore('@')
}
하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드해야 한다. 반면 nickname은 오버라이드하지 않고 상속할 수 있다.
인터페이스에 선언된 프로퍼티와 달리 클래스에 구현된 프로퍼티는 뒷받침하는 필드를 원하는 대로 사용할 수 있다.
말이 꽤나 어렵게 다가올 수 있다.
쉽게 말하면, 인터페이스는 backing field(뒷받침하는 필드)를 갖지 않는(없는) 프로퍼티만 선언할 수 있다.
하지만 추상클래스는 일반적인 클래스와 같이 backing field를 갖는 프로퍼티를 선언하고 초기화할 수 있다.
interface User {
// 추상 프로퍼티 - 하위 클래스에서 반드시 오버라이드 해야함
val email: String
// 계산된 프로퍼티 - backing field가 없고 - 게터만 존재한다
val nickname: String
get() = email.substringBefore('@')
}
abstract class User {
// 추상 프로퍼티 - 하위 클래스에서 반드시 오버라이드 해야함
abstract val email: String
// 계산된 프로퍼티 - backing field가 없고 게터만 존재한다
val nickname: String
get() = email.substringBefore('@')
// 여기가 차이점!! 추상 클래스만 가능
// 스토어드 프로퍼티 - backing field를 갖는다
val company: String = "우아한테크코스"
}
이렇게 인터페이스와 추상클래스에는 프로퍼티 선언에 있어서 차이점이 존재한다.
다중, 단일 상속
다음으로 하위 객체에서의 다중, 단일 상속의 차이점이 존재한다.
인터페이스는 하위 객체에서 다중 구현이 가능하다.
그러나 추상클래스는 클래스이므로 하위 객체에서 단일 상속만 가능하다.
즉 정리하면 아래 두 가지 차이점이 존재한다.
- 추상 클래스는 backing field를 갖는 스토어드 프로퍼티를 선언할 수 있다. 하지만 인터페이스는 backing field가 없는 추상 프로퍼티 혹은 계산된 프로퍼티만 선언할 수 있다.
- 하위 객체에서 인터페이스는 다중 구현이 가능하지만 추상클래스는 단일 상속만 가능하다.
그럼 위 두가지 경우에만 구분해서 사용하면 될까?
대체 어떤 상황에서 인터페이스와 추상클래스를 구분해서 사용해야할까?
개념적 관점에서의 차이
흔히 인터페이스는 '구현 ' 한다, 추상클래스는 '상속 ' 한다라고 말한다.
똑같이 콜론(:)을 사용해서 가져오는데 왜 다르게 부를까?
인터페이스
인터페이스의 사전적 의미는 '접점'이다.
그래서 우리는 코틀린(혹은 자바)이 아닌 '유저 인터페이스'라는 용어에서도 인터페이스를 만나볼 수 있다.
이름이 '유저 인터페이스'인 이유는 유저가 서비스(시스템)와 만나는(의사소통하는) 접점이기 때문일 것이다.
그럼 코틀린에서 인터페이스는 어떤 접점일까?
개인적으로 해당 기능을 구현하는 사람과 사용하는 사람 사이의 설명서로써 접점의 역할을 한다고 생각한다.
인터페이스를 사용할 때를 생각해보자
인터페이스에 (구현해야할) 필요한 기능들을 나열한다.
해당 기능을 구현할 사람은 인터페이스에 작성되어있는대로 기능들을 구현한다.
해당 기능을 사용할 사람은 인터페이스에 작성되어있는대로 기능들을 사용한다.
사용할 사람은 내부가 어떻게 구현되었는지는 굳이 알 필요가 없어진다.
인터페이스대로 어떻게 사용할 수 있는지만 알면 되니까~!
이렇게 보았을 때 인터페이스를 (강요가 수반된) 설명서 혹은 명세서 라고 볼 수 있을 것 같다.
그럼 인터페이스는 해당 기능을 구현하는 사람과 사용하는 사람에게 설명서 즉 접점이 될 것이다.
추상 클래스
자 그럼 추상클래스는 어떨까?
추상클래스는 함수 몸체(구현부)가 존재할 수 있고, 스토어드 프로퍼티를 선언할 수 있다.
즉, 자체로 생성될 수 없고 상속이 가능하다는 점을 제외하면 일반 클래스와 다르지 않다.
그러므로 여러 객체가 존재하고 여러 객체 사이에 공통점들이 존재할 때,
공통점들을 하나로 묶고 상속할 수 있게 해주는 존재가 추상클래스라고 생각한다.
그럼 open 키워드를 쓰면 되잖아? 라고 물어볼 수 있을 것이다.
- 자체로 생성 가능
- 추상 메서드 선언 불가능
우선 오픈 클래스는 자체로 생성될 수 있다.
우리가 블랙잭 게임을 하는 참여자들을 Participant라는 이름의 오픈 클래스로 만들었다고 해보자.
블랙잭 게임은 딜러와 플레이어가 모두 게임에 참여한다.
그럼 Dealer와 Player를 Participant를 상속받아 만들었다고 해보자.
우리는 블랙잭 게임 안에서 Dealer와 Player만 존재할 것이라고 예상할 것이다.
하지만 이 경우 게임 내 어디선가 우리 몰래 Participant 인스턴스가 생성되어 돌아다니는 문제가 발생할 수 있다.
또 오픈 클래스는 추상 메서드를 가질 수 없다.
우리는 공통적인 부분들을 부모클래스에 모아놓길 원한다.
그래서 공통적인 부분의 구체적인 구현을 부모클래스에 두었다. (여기까지는 오픈 클래스와 다르지 않다)
하지만 공통적인 행동이면서 조금 다르게 작동하는 기능이 있다면 어떻게 할 것인가?
예를 들어 휴대폰을 Phone이라는 이름으로 만들었다.
Phone을 상속받아 Galaxy와 IPhone을 만들었다고 해보자. 두 휴대폰 모두 삼성페이와 애플페이로 결제 기능이 존재한다.
삼성페이는 NFC와 MST방식, 애플페이는 NFC방식으로 결제 기능에 대한 구현 내용은 다를 것이다.
하지만 두 클래스 모두 결제 기능이 존재하므로 Galaxy와 IPhone 클래스 모두 pay라는 함수를 구현하도록 하고 싶다.
이 때 Phone이 오픈클래스가 아닌 추상클래스라고 생각해보자.
추상 메서드로 pay 함수를 선언하고 Galaxy와 IPhone 모두에서 pay 함수를 오버라이드하여 구현을 강제할 수 있다.
그냥 오픈클래스로 Phone을 만들고 오버라이드 없이 Galaxy와 IPhone에서 pay를 각각 선언한거랑 뭐가 달라? 라고 물어볼 수 있다.
pay를 추상메서드로 만드는 것은 하위 클래스에 구현을 강제할 뿐 아니라, 해당 객체가 Phone이라는 것만 알아도 pay를 사용할 수 있도록 해준다.
Galaxy와 IPhone 객체들이 섞여있는 List<Phone>이 있다고 해보자.
이 때 pay가 Phone의 추상메서드라면 해당 리스트의 아이템에서 바로 pay를 호출할 수 있다.
(Galaxy와 IPhone에 각각 선언했다면 우리는 Phone인 것만 알기 때문에 pay를 호출할 수 없다.)
고로 공통적으로 필요한 기능이지만 하위 클래스들에서 다른 구현이 필요하다면 추상 메서드를 활용하면 좋다.
이제 오픈클래스가 아닌 추상클래스를 사용하는 이유에 대해 알았을 것이다.
위에서 설명하지는 않았지만 Phone이 오픈 클래스일때 pay를 구현부를 비워둔 오픈 메서드로 만들고 하위 클래스에서 오버라이드하여 기능을 새로 작성한다고 하면,
List<Phone>의 아이템에서도 Galaxy와 IPhone 각각의 pay 기능을 잘 실행할 수 있을 것이다.
하지만 이는 실수로 하위 클래스에서 pay의 기능을 재정의 하지 않았을 때 문제가 발생하므로 적합하지 않다.
마무리
마무리하며 내 생각을 정리하자면
인터페이스는 주로 구체적인 구현이 없는 명세, 설명을 사용한다.
추상클래스는 주로 명세 뿐만 아니라 공통된 부분에 대한 구체적인 구현이 필요할 때 사용한다.
내가 어떤 목적으로 사용할 것인지 잘 생각해보자
이상 끝!!
'Kotlin' 카테고리의 다른 글
[Kotlin] 확장 함수! 신기한 당신의 정체는? (0) | 2023.04.24 |
---|---|
[Kotlin] 프로퍼티 용어 정리 (0) | 2023.04.05 |
[Kotlin] 코틀린 인터페이스(Interface) (0) | 2023.02.21 |
[Kotlin] 코틀린의 헷갈리는 생성자와 프로퍼티 알아보기 (Java와 비교하여) (0) | 2023.02.20 |