코틀린에서의 클래스와 객체, 인터페이스는 자바와 유사하면서도 고유의 강력한 기능을 제공합니다. 이번 챕터에서는 코틀린의 클래스 설계와 사용법을 학습하고, 클래스 계층 구조와 가시성 변경자, 내부 및 중첩 클래스, sealed
클래스를 이해해보겠습니다.
코틀린에서 클래스와 인터페이스는 각각 class
와 interface
키워드를 통해 정의합니다. 코틀린 인터페이스는 자바 8과 유사하게 기본 메서드 구현을 가질 수 있지만, 필드는 포함하지 않습니다.
interface Clickable {
fun click() // 추상 메서드
fun showOff() = println("I'm clickable!") // 디폴트 구현 메서드
}
class Button : Clickable {
override fun click() = println("Button clicked!")
}
Button
클래스는 Clickable
인터페이스를 구현하며, showOff()
는 기본 구현을 그대로 사용합니다.
자바와 달리 코틀린에서는 extends
와 implements
를 사용하지 않고, :
기호로 상속과 인터페이스 구현을 모두 처리합니다. 인터페이스와 클래스 모두 여러 개의 인터페이스를 구현할 수 있지만, 클래스 상속은 한 번만 가능합니다.
open
, final
, abstract
변경자final
코틀린에서는 클래스와 메서드가 기본적으로 final
이며, 상속이 금지됩니다. 클래스나 메서드를 상속 가능하게 하려면 open
변경자를 명시해야 합니다.
open class RichButton : Clickable { // open 키워드를 사용해 상속 가능하게
fun disable() {} // 기본적으로 final
open fun animate() {} // 하위 클래스에서 override 가능
override fun click() {} // 상위 클래스의 메서드 override
}
abstract
클래스는 인스턴스를 생성할 수 없으며, abstract
메서드는 하위 클래스에서 반드시 구현해야 합니다. abstract
메서드는 기본적으로 open
이므로 추가로 open
키워드를 명시할 필요는 없습니다.
abstract class Animated {
abstract fun animate() // 하위 클래스에서 반드시 구현해야 함
open fun stopAnimating() {}
fun animateTwice() {
animate()
animate()
}
}
주의사항: 코틀린에서는 override 키워드가 필수입니다. 이를 통해 실수로 상위 메서드를 재정의하는 상황을 방지할 수 있습니다.
코틀린에서는 다음 네 가지 가시성 변경자를 지원합니다:
public
(기본값): 어디서나 접근 가능private
: 같은 클래스 내부에서만 접근 가능protected
: 하위 클래스에서만 접근 가능internal
: 같은 모듈 안에서만 접근 가능특히 internal
은 모듈 내에서만 접근 가능하도록 하여, 패키지 전용 가시성 대신 모듈을 기준으로 접근을 제한합니다.
internal class InternalClass { ... }
주의사항: 자바와 달리 protected는 같은 패키지가 아닌 하위 클래스에서만 접근 가능합니다.
코틀린에서는 기본적으로 중첩 클래스는 외부 클래스에 대한 참조를 갖지 않습니다. 중첩 클래스가 외부 클래스에 대한 참조가 필요하다면 inner
키워드를 사용해 내부 클래스로 선언해야 합니다.
class Outer {
inner class Inner {
fun getOuterReference() = this@Outer
}
}
주의사항: 중첩 클래스와 내부 클래스를 사용할 때 외부 클래스의 인스턴스를 참조하지 않는다면 메모리 누수를 방지하기 위해 중첩 클래스를 사용하는 것이 좋습니다.
sealed
클래스: 클래스 계층 정의 시 계층 확장 제한sealed
클래스는 클래스 계층 구조를 제한하는 데 유용합니다. sealed
클래스를 상속하는 하위 클래스들은 반드시 같은 파일에 정의되어야 하므로, 계층 구조의 확장을 통제할 수 있습니다.
sealed class Expr {
data class Num(val value: Int) : Expr()
data class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when (e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
}
when
구문에서 sealed
클래스의 모든 하위 클래스가 처리되면 else
가 필요 없습니다. 이는 컴파일 타임에 모든 경우의 수가 처리되었는지 검증할 수 있도록 도와줍니다.
코틀린의 생성자는 주 생성자와 부 생성자로 나눌 수 있으며, 주 생성자는 클래스 선언과 함께 정의되고, 부 생성자는 constructor
키워드를 통해 여러 개 선언할 수 있습니다.
주 생성자는 클래스명 옆에 위치하며, init
블록을 통해 초기화 로직을 정의할 수 있습니다.
class User(val nickname: String) {
init {
println("User created: $nickname")
}
}
부 생성자는 주 생성자 외에 추가적인 초기화 방식이 필요할 때 사용합니다.
class MyButton : View {
constructor(ctx: Context) : super(ctx) { ... }
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) { ... }
}
주의사항: 일반적으로 코틀린에서는 여러 부 생성자 대신 주 생성자와 디폴트 파라미터를 사용하는 것이 좋습니다.
데이터 클래스는 equals
, hashCode
, toString
, copy
등의 메서드를 자동으로 생성하여, 데이터를 저장하고 비교하는 데 유용합니다. 선언할 때 data
키워드를 사용합니다.
data class Client(val name: String, val postalCode: Int)
val client1 = Client("John Doe", 12345)
println(client1) // 출력: Client(name=John Doe, postalCode=12345)
코틀린의 by
키워드를 통해 인터페이스의 구현을 다른 객체에 위임할 수 있습니다. 이를 통해 코드 중복 없이 재사용성을 높일 수 있습니다.
interface Printer {
fun print()
}
class DocumentPrinter : Printer {
override fun print() = println("Printing document")
}
class PrinterManager(printer: Printer) : Printer by printer
val manager = PrinterManager(DocumentPrinter())
manager.print() // 출력: Printing document
주의사항: 클래스 위임은 인터페이스 구현이 반복되는 경우 코드 중복을 줄이는 데 유용하지만, 과도한 위임은 오히려 복잡성을 증가시킬 수 있으니 주의가 필요합니다.