[Kotlin] 언제나 헷갈리는 인터페이스와 추상클래스

H.Zoon·2024년 9월 9일
0
post-thumbnail

코틀린에서 추상 클래스와 인터페이스는 어떻게 다를까?

개발을 진행하다가 문득 공통화를 하고싶을때가 있다. 그때마다 추상 클래스와 인터페이스의 사이에서 갈등하게 된다.. 이번기회에 확실하게 잡기위해 공부한 내용을 기록해보기로 하였다.

Q1. 추상 클래스와 인터페이스??

추상 클래스는 클래스 간의 공통적인 기능을 정의하고, 특정 메서드는 하위 클래스에서 반드시 구현하도록 하는 클래스다. 추상 클래스는 그 자체로는 인스턴스를 생성할 수 없고, 이를 상속받은 하위 클래스에서 구체화되어야 한다.

인터페이스는 클래스가 구현해야 할 메서드들을 정의한 추상적인 개념으로, 하나 이상의 인터페이스를 동시에 구현할 수 있다. 코틀린에서는 인터페이스에 디폴트 메서드(기본 구현을 제공하는 메서드)를 정의할 수 있다.

Q2. 추상 클래스의 주요 특징은 ??

추상 클래스의 주요 특징은 다음과 같다:

  • 단일 상속: 하나의 클래스만 상속할 수 있다.
  • 상태 저장 가능: 클래스 내에 속성을 정의하고, 이 속성에 상태를 저장할 수 있다.
  • 일반 메서드: 구현된 일반 메서드를 포함할 수 있으며, 이를 상속받는 클래스에서 사용할 수 있다.
  • 추상 메서드: 구현이 없는 추상 메서드를 포함하며, 상속받는 클래스에서 반드시 구현해야 한다.

추상 클래스 예시:

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() 메서드를 구현한다.

Q3. 인터페이스의 주요 특징은??

인터페이스의 주요 특징은 다음과 같다:

  • 다중 구현 가능: 하나의 클래스가 여러 인터페이스를 동시에 구현할 수 있다.
  • 상태 저장 불가: 인터페이스는 상태를 저장할 수 없다. 속성은 가질 수 있으나 게터만 정의할 수 있다.
  • 디폴트 메서드: 기본 구현을 제공하는 메서드를 정의할 수 있으며, 구현 클래스에서 재정의하지 않고 그대로 사용할 수 있다.

인터페이스 예시:

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 클래스에서 그대로 사용된다.

Q4. 표로 한눈에 정리해보기

추상 클래스와 인터페이스의 차이점은 아래와 같다:

특징추상 클래스인터페이스
상속단일 상속만 가능다중 구현 가능
상태 저장상태(속성)를 저장할 수 있음상태 저장 불가
구현 메서드일반 메서드를 가질 수 있음디폴트 메서드를 가질 수 있음
추상 메서드구현이 필요 없는 추상 메서드를 포함할 수 있음구현이 필요 없는 추상 메서드를 포함할 수 있음
사용 목적주로 클래스 간의 공통 기능을 제공하며, 상태 관리에 유리함상태가 없는 공통 동작이나 규약을 정의하는 데 유리함

Q5. 잠깐.. 추상 클래스의 일반 메서드와 인터페이스의 디폴트 메서드는 동일한가?

두 메서드는 비슷한 역할을 하지만, 중요한 차이가 있다.

  • 추상 클래스의 일반 메서드는 클래스에 상태를 저장할 수 있기 때문에, 이 상태를 기반으로 메서드를 정의할 수 있다. 예를 들어, 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.")
    }
}

이렇게 추상 클래스의 일반 메서드는 상태를 사용할 수 있지만, 인터페이스의 디폴트 메서드는 상태를 다룰 수 없다.

물론, 추상 클래스와 인터페이스 중 어떤 것을 선택해야 하는지에 대한 상황을 이해하기 쉽게 예제를 추가해보겠습니다.

Q6. 교과서적인 내용은 그만 예제로 바로 적용해보자

상황에 따라 추상 클래스와 인터페이스 중 어느 것을 사용하는 것이 더 적절할지 결정하는 몇 가지 기준이 있다.

1. 공통적인 동작과 상태 관리가 필요한 경우: 추상 클래스 사용

만약 여러 클래스가 공통적으로 가져야 할 상태(속성)와 메서드를 제공하고 싶다면 추상 클래스가 더 적합하다. 예를 들어, 여러 종류의 동물이 모두 "이름"과 "소리"라는 공통된 속성을 갖고 있으면서, 각각 다른 방식으로 울음을 표현해야 한다면 추상 클래스를 사용할 수 있다.

예시: 동물 클래스

// 추상 클래스는 상태(이름)를 저장할 수 있다.
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()라는 추상 메서드를 각자 다르게 구현한다.

2. 다양한 클래스에 공통된 행위(동작)를 정의하고 싶을 때: 인터페이스 사용

만약 여러 클래스가 동일한 행위를 해야 하지만, 그 외에 공통된 속성이나 상태가 필요하지 않다면 인터페이스를 사용하는 것이 좋다. 예를 들어, 여러 종류의 탈것(자동차, 자전거, 트럭)이 "주행"과 "정지"라는 행위를 수행해야 하지만, 각 클래스는 독립적인 구현을 가져야 한다면 인터페이스가 적합하다.

예시: 탈것 인터페이스

// 인터페이스는 상태가 없으며, 모든 탈것이 '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()은 인터페이스에서 제공된 디폴트 메서드를 그대로 사용한다.

언제 추상 클래스를, 언제 인터페이스를 선택할까?

  1. 공통적인 상태(속성)를 저장하거나 관리해야 할 때추상 클래스를 사용한다.

    • 예: 여러 종류의 동물이 이름과 같은 속성을 가질 때.
  2. 다양한 클래스가 같은 행위(동작)를 공유하지만, 각기 다른 방식으로 구현해야 할 때인터페이스를 사용한다.

    • 예: 다양한 탈것이 주행과 정지 같은 공통된 행위를 가져야 할 때.
  3. 다중 구현이 필요한 경우인터페이스를 사용한다. 코틀린은 다중 상속을 허용하지 않기 때문에, 클래스가 여러 개의 부모 클래스에서 상속받을 필요가 있는 경우에는 추상 클래스 대신 인터페이스를 사용해야 한다.

결론

코틀린에서 추상 클래스와 인터페이스는 각각 고유한 목적을 가진다. 추상 클래스는 클래스 간의 공통 기능을 정의하고, 상태 관리가 필요할 때 사용하기 적합하다. 반면, 인터페이스는 다중 구현을 지원하며, 여러 클래스에 공통된 행위나 규약을 제공할 때 유리하다. 두 개념을 적절히 활용해 코드를 더욱 유연하고 재사용 가능하게 만들 수 있다.

0개의 댓글