with ChatGPT 시리즈는 ChatGPT의 내용과 개인의 생각을 토대로 학습해 보는 컨텐츠입니다.
Kotlin은 Java와 마찬가지로 객체지향 언어의 핵심인 상속과 인터페이스 개념을 지원하지만, 설계 철학이 다르기 때문에 문법과 구조에 몇 가지 중요한 차이가 존재합니다.
이번 글에서는 다음 항목들을 중심으로 Kotlin의 상속 및 인터페이스 구현 방식을 정리하겠습니다.
- Kotlin에서 클래스는 기본적으로 final이다
- 클래스 상속 구조와 open, override 키워드
- 인터페이스 선언과 다중 구현
- Java와 Kotlin의 상속/인터페이스 차이점
- 실무에서의 활용 예시
🔹 1. Kotlin의 클래스는 기본적으로 final
Java에서는 클래스를 선언하면 암묵적으로 상속이 허용됩니다.
하지만 Kotlin에서는 모든 클래스는 기본적으로 상속이 불가능한 final 상태입니다.
취약한 기반 클래스 (fragile base class)라는 문제는 기반 클래스 구현을 변경함으로써 하위 클래스가 잘못된 동작을 하게 되는 경우를 뜻한다. 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙 (어떤 메서드를 어떻게 오버라이드해야 하는지 등)을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드할 위험이 있다. 모든 하위 클래스를 분석하는 것은 불가능하므로 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 '취약'하다.
이 문제를 해결하기 위해 ... [이펙티브 자바]에서는 "상속을 위한 설계와 문서를 갖춰라. 그럴 수 없다면 상속을 금지하라"는 조언을 한다. 이는 특별히 하위 클래스에서 오버라이드하도록 의도된 클래스와 메서드가 아니라면 모두 final로 만들라는 뜻이다.
코틀린도 마찬가지 철학을 따른다. 자바의 클래스와 메서드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메서드는 기본적으로 파이널 (final)이다.
- 코틀린 인 액션, 177p
class Animal // 기본적으로 final이라 상속 불가
✅ 상속을 허용하려면 open 키워드가 필요
open class Animal {
open fun sound() = println("Unknown sound")
}
- 클래스에 open을 붙여야 하위 클래스에서 상속 가능
- 메서드도 open을 붙이지 않으면 오버라이딩 불가
🔹 2. 상속과 override
open class Animal {
open fun sound() = println("Unknown")
}
class Dog : Animal() {
override fun sound() = println("멍멍!")
}
- override는 반드시 명시적으로 사용해야 하며, 오타나 실수 방지에 효과적
- final override로 오버라이딩은 허용하되 더 이상 재오버라이드 금지도 가능
override final fun sound() = println("멍멍!") // Dog에서 더 이상 오버라이드 못 함
🔹 3. 인터페이스 선언과 다중 구현
Kotlin의 인터페이스는 Java와 유사하지만, 프로퍼티 (property)를 가질 수 있고, 기본 구현도 제공할 수 있다는 점에서 더 유연합니다.
interface Flyable {
fun move() // 일반 메서드 선언
fun fly() { // 디폴트 구현 정의
println("날아오릅니다!")
}
}
✅ 클래스에 인터페이스 구현
class Bird : Flyable // Flyable의 기본 구현을 그대로 사용
✅ 여러 인터페이스 구현 및 충돌 처리
interface A {
fun greet() = println("Hello from A")
}
interface B {
fun greet() = println("Hello from B")
}
class C : A, B {
override fun greet() {
super<A>.greet()
super<B>.greet()
}
}
- 다중 인터페이스에서 동명이 메서드가 겹치면 명시적으로 super를 지정해야 함
🔹 4. Java와 Kotlin의 차이점 요약
항목 | Java | Kotlin |
클래스 기본 성격 | 상속 가능 | final (상속 불가) |
상속 허용 | 별도 키워드 없음 | open 키워드 필요 |
메서드 오버라이드 | @Override (선택적) | override (필수) |
인터페이스 메서드 | Java 8 이후 default 지원 | 기본적으로 구현 가능 |
다중 상속 | 클래스 다중 상속 불가 | 인터페이스 다중 구현 지원 (충돌 처리 필요) |
속성 (프로퍼티) | 변수 = 필드 + getter/setter | 프로퍼티 자체를 선언 가능 |
🔹 5. 실무에서 어떻게 쓰일까?
✅ 공통 동작 정의 (Template Method 패턴 등)
open class BaseService {
open fun before() = println("기본 작업 시작")
open fun process() = println("기본 처리")
fun run() {
before()
process()
}
}
✅ 서비스 간 공통 인터페이스 정의
interface ApiClient {
fun fetch(): String
}
class RealApiClient : ApiClient {
override fun fetch() = "Real Response"
}
- 테스트 시엔 FakeApiClient로 대체하기 쉬움 (의존성 주입과 궁합이 좋음)
🚀 결론
항목 | Java | Kotlin |
클래스 다중 상속 | ❌ 불가 | ❌ 불가 |
인터페이스 다중 구현 | ✅ 가능 | ✅ 가능 |
default 메서드 | ✅ (Java 8+) | ✅ 기본 지원 |
다중 구현 충돌 처리 | Interface.super.method() | super<인터페이스>.method() |
Kotlin의 상속 및 인터페이스 설계는 불필요한 상속을 방지하고, 실수 없는 확장을 유도하는 구조입니다.
덕분에 실무에서 보다 안정적인 코드 작성이 가능합니다.
🤔 추가로 생각해 볼 질문들
- Kotlin에서 abstract class와 interface는 언제 선택해야 할까?
- open class를 무분별하게 사용하는 것이 왜 문제가 될 수 있을까?
- default 구현이 있는 인터페이스는 진짜 클래스처럼 행동할 수 있을까?
- Kotlin에서 인터페이스에 프로퍼티를 선언하면 어떤 구조로 컴파일될까?
- sealed class와 interface의 역할은 어떻게 다를까?