개발을 진행하다가 문득 공통화를 하고싶을때가 있다. 그때마다 추상 클래스와 인터페이스의 사이에서 갈등하게 된다.. 이번기회에 확실하게 잡기위해 공부한 내용을 기록해보기로 하였다.
추상 클래스는 클래스 간의 공통적인 기능을 정의하고, 특정 메서드는 하위 클래스에서 반드시 구현하도록 하는 클래스다. 추상 클래스는 그 자체로는 인스턴스를 생성할 수 없고, 이를 상속받은 하위 클래스에서 구체화되어야 한다.
인터페이스는 클래스가 구현해야 할 메서드들을 정의한 추상적인 개념으로, 하나 이상의 인터페이스를 동시에 구현할 수 있다. 코틀린에서는 인터페이스에 디폴트 메서드(기본 구현을 제공하는 메서드)를 정의할 수 있다.
추상 클래스의 주요 특징은 다음과 같다:
abstract class Vehicle(val brand: String) {
abstract fun drive() // 추상 메서드
fun printBrand() { // 일반 메서드
println("Brand: $brand")
}
}
class Car(brand: String) : Vehicle(brand) {
override fun drive() {
println("The car is driving")
}
}
fun main() {
val car = Car("Toyota")
car.printBrand() // "Brand: Toyota" 출력
car.drive() // "The car is driving" 출력
}
위 예제에서 Vehicle
은 추상 클래스다. 이 클래스는 drive()
라는 추상 메서드와 printBrand()
라는 일반 메서드를 가지고 있다. Car
클래스는 Vehicle
을 상속받아 drive()
메서드를 구현한다.
인터페이스의 주요 특징은 다음과 같다:
interface Drivable {
fun drive() // 추상 메서드
fun stop() { // 디폴트 메서드
println("The vehicle has stopped.")
}
}
class Truck : Drivable {
override fun drive() {
println("The truck is driving")
}
}
fun main() {
val truck = Truck()
truck.drive() // "The truck is driving" 출력
truck.stop() // "The vehicle has stopped." 출력
}
Drivable
는 인터페이스로, drive()
메서드는 추상 메서드이며 Truck
클래스에서 구현해야 한다. stop()
은 디폴트 메서드로 기본 구현이 제공되어, Truck
클래스에서 그대로 사용된다.
추상 클래스와 인터페이스의 차이점은 아래와 같다:
특징 | 추상 클래스 | 인터페이스 |
---|---|---|
상속 | 단일 상속만 가능 | 다중 구현 가능 |
상태 저장 | 상태(속성)를 저장할 수 있음 | 상태 저장 불가 |
구현 메서드 | 일반 메서드를 가질 수 있음 | 디폴트 메서드를 가질 수 있음 |
추상 메서드 | 구현이 필요 없는 추상 메서드를 포함할 수 있음 | 구현이 필요 없는 추상 메서드를 포함할 수 있음 |
사용 목적 | 주로 클래스 간의 공통 기능을 제공하며, 상태 관리에 유리함 | 상태가 없는 공통 동작이나 규약을 정의하는 데 유리함 |
두 메서드는 비슷한 역할을 하지만, 중요한 차이가 있다.
Vehicle
클래스의 printBrand()
메서드는 brand
라는 속성을 사용해 동작한다.// 추상 클래스의 일반 메서드 예시
abstract class Vehicle(val brand: String) {
fun printBrand() {
println("This vehicle is a $brand.")
}
}
// 인터페이스의 디폴트 메서드 예시
interface Drivable {
fun stop() {
println("The vehicle has stopped.")
}
}
이렇게 추상 클래스의 일반 메서드는 상태를 사용할 수 있지만, 인터페이스의 디폴트 메서드는 상태를 다룰 수 없다.
물론, 추상 클래스와 인터페이스 중 어떤 것을 선택해야 하는지에 대한 상황을 이해하기 쉽게 예제를 추가해보겠습니다.
상황에 따라 추상 클래스와 인터페이스 중 어느 것을 사용하는 것이 더 적절할지 결정하는 몇 가지 기준이 있다.
만약 여러 클래스가 공통적으로 가져야 할 상태(속성)와 메서드를 제공하고 싶다면 추상 클래스가 더 적합하다. 예를 들어, 여러 종류의 동물이 모두 "이름"과 "소리"라는 공통된 속성을 갖고 있으면서, 각각 다른 방식으로 울음을 표현해야 한다면 추상 클래스를 사용할 수 있다.
// 추상 클래스는 상태(이름)를 저장할 수 있다.
abstract class Animal(val name: String) {
abstract fun makeSound()
fun printName() {
println("This animal's name is $name.")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("Woof")
}
}
class Cat(name: String) : Animal(name) {
override fun makeSound() {
println("Meow")
}
}
fun main() {
val dog = Dog("Buddy")
dog.printName() // "This animal's name is Buddy."
dog.makeSound() // "Woof"
val cat = Cat("Whiskers")
cat.printName() // "This animal's name is Whiskers."
cat.makeSound() // "Meow"
}
Animal
추상 클래스는 공통된 속성인 name
을 관리하며, 모든 동물에 적용할 수 있는 printName()
메서드를 제공한다.makeSound()
라는 추상 메서드를 각자 다르게 구현한다.만약 여러 클래스가 동일한 행위를 해야 하지만, 그 외에 공통된 속성이나 상태가 필요하지 않다면 인터페이스를 사용하는 것이 좋다. 예를 들어, 여러 종류의 탈것(자동차, 자전거, 트럭)이 "주행"과 "정지"라는 행위를 수행해야 하지만, 각 클래스는 독립적인 구현을 가져야 한다면 인터페이스가 적합하다.
// 인터페이스는 상태가 없으며, 모든 탈것이 'drive'와 'stop'을 구현해야 한다.
interface Drivable {
fun drive()
fun stop() {
println("The vehicle has stopped.")
}
}
class Car : Drivable {
override fun drive() {
println("The car is driving")
}
}
class Bike : Drivable {
override fun drive() {
println("The bike is driving")
}
}
fun main() {
val car = Car()
car.drive() // "The car is driving"
car.stop() // "The vehicle has stopped."
val bike = Bike()
bike.drive() // "The bike is driving"
bike.stop() // "The vehicle has stopped."
}
Drivable
인터페이스는 모든 탈것이 가져야 할 공통된 행위인 drive()
와 stop()
을 정의한다.drive()
를 각자 다른 방식으로 구현할 수 있지만, stop()
은 인터페이스에서 제공된 디폴트 메서드를 그대로 사용한다.공통적인 상태(속성)를 저장하거나 관리해야 할 때는 추상 클래스를 사용한다.
다양한 클래스가 같은 행위(동작)를 공유하지만, 각기 다른 방식으로 구현해야 할 때는 인터페이스를 사용한다.
다중 구현이 필요한 경우는 인터페이스를 사용한다. 코틀린은 다중 상속을 허용하지 않기 때문에, 클래스가 여러 개의 부모 클래스에서 상속받을 필요가 있는 경우에는 추상 클래스 대신 인터페이스를 사용해야 한다.
코틀린에서 추상 클래스와 인터페이스는 각각 고유한 목적을 가진다. 추상 클래스는 클래스 간의 공통 기능을 정의하고, 상태 관리가 필요할 때 사용하기 적합하다. 반면, 인터페이스는 다중 구현을 지원하며, 여러 클래스에 공통된 행위나 규약을 제공할 때 유리하다. 두 개념을 적절히 활용해 코드를 더욱 유연하고 재사용 가능하게 만들 수 있다.