Developing Myself Everyday
Published 2023. 5. 25. 21:53
Abstract Class & Interface Android/Kotlin

 보통의 상위 클래스는 자기 자신에 대한 인스턴스를 만들 수 있는 타입들이다. 하지만 별도의 인스턴스가 있을 수 없고 구체적인 경우를 구현한 다른 클래스의 인스턴스만 만들 수 있는 추상적인 개념을 표현하는 클래스라면 인스턴스 생성은 바람직하지 않다.

 

 그렇다면 추상적인 개념을 표현하려면 어떤 타입의 클래스를 사용해야 할까? 이 질문에 대한 대답이 바로 추상 클래스이다.

 

추상 클래스란?


 추상 클래스는 자바에서도 지원하는 방식이다. 추상 클래스는 직접 인스턴스화 할 수 없고 다른 클래스의 상위 역할만 할 수 있는 클래스를 뜻한다. 클래스를 추상 클래스로 만들기 위해서는 abstract라는 변경자 키워드를 붙여야 한다.

 

abstract class Entity(val name: String)

// Success: 하위 클래스에서 위임 호출
class Person(name: String, val age: Int): Entity(name)

// Error: 추상 클래스의 인스턴스를 생성할 수 없음
val entity = Entity("Unknown")

 

 위의 예제를 보면 추상 클래스에도 생성자가 있을 수 있다. 추상 클래스와 비추상 클래스의 차이는 추상 클래스의 생성자가 오직 하위 클래스의 생성자에서 위임 호출로만 호출될 수 있다는 점이다. 

 

 추상 클래스의 또다른 특징은 추상 멤버를 정의할 수 있다는 것이다. 아래의 코드를 봐보자.

 

import kotlin.math.PI

abstract class Shape {
    abstract val width: Double
    abstract val height: Double
    abstract fun area(): Double
}

class Circle(val radius: Double): Shape() {
    val diameter get() = 2 * radius
    override val width: Double get() = diameter
    override val height: Double get() = diameter
    override fun area(): Double = PI * radius * radius
}

class Rectangle(
    override val width: Double, 
    override val height: Double
) : Shape() {
    override fun area(): Double = width * height
}

fun Shape.print() {
    println("Bounds: $width * $height, area: ${area()}")
}

fun main() {
    Circle(10.0).print()
    Rectangle(3.0, 5.0).print()
}

 

 추상 멤버 자체는 구현을 가질 수 없으므로 추상 멤버를 정의할 때는 몇 가지 제약이 있다.

  1. 추상 프로퍼티를 초기화할 수 없고 명시적인 접근자나 by 절을 추가할 수 없다.
  2. 추상 함수에는 본문이 없어야 한다.
  3. 추상 프로퍼티와 함수 모두 명시적으로 반환 타입을 적어야 한다. 본문이나 초기화 코드가 없으므로 타입을 추론할 수 없기 때문이다.

 

인터페이스란?


 근본적으로 인터페이스는 추상 혹은 비추상 메서드나 프로퍼티를 포함하지만 자체적인 인스턴스 상태나 생성자를 만들 수 없는 타입이다. 

 

 인터페이스 멤버는 디폴트가 추상 멤버다. 인터페이스는 클래스나 다른 인터페이스의 상위 타입이 될 수 있다. 비추상 클래스가 인터페이스를 상속할 때는 모든 추상 멤버에 대한 구현을 제공해야 한다. 마찬가지로 클래스가 인터페이스를 상속한 클래스에 있는 인터페이스 멤버를 상속해 구현할 때도 override 키워드를 추가해야 한다.

 

interface Vehicle{
    fun move() {
        println("I'm moving")
    }
}

interface Car : Vehicle {
    override fun move() {
        println("I'm riding")
    }
}

 

 위의 예시에서 상위 타입 이름 뒤에 괄호가 없다는 것을 알 수 있다. 이 이유는 인터페이스에는 생성자가 없어서 하위 클래스를 초기화할 때 호출해야 할 코드가 없기 때문이다.

 

※ 주의
인터페이스가 다른 클래스를 상송할 수 없다는 것을 알고 있어야 한다. Any 클래스만 유일하게 가능하다.

인터페이스가 인터페이스 상속 가능
인터페이스가 클래스 상속 불가

 

자바와 마찬가지로 코틀린 인터페이스도 다중 상속을 지원한다.

 

interface Car {
    fun ride() 
}

interface Plane {
    fun fly()
}

interface Boat {
    fun sail()
}

interface Vehicle: Car, Plane

class How: Vehicle, Boat {
    override fun ride() {
        println("I'm riding")
    }

    override fun fly() {
        println("I'm flying")
    }

    override fun sail() {
        println("I'm sailing")
    }
}

 

 만약 한 타입이 동일한 시그니처를 가지는 멤버가 들어있는 다른 인터페이스를 둘 이상 상속한다고 가정해보자. 

 

interface Car {
    fun move() {
        println("I'm moving")
    }
}

interface Plane {
    fun move()
}

class How:  Car, Plane {
    override fun move() {
        super.move()
    }
}

fun main() {
    How().move()
}

 

 이런 식으로 구현이 되면 super 호출 자체가 모호해진다. 이런 상황에는 super를 상위 타입으로 한정시킨 키워드를 사용해야 한다.

 

interface Car {
    fun move() {
        println("I'm riding")
    }
}

interface Plane {
    fun move() {
        println("I'm flying")
    }
}

class How:  Car, Plane {
    override fun move() {
        super<Car>.move()
        super<Plane>.move()
    }
}

fun main() {
    How().move()
}

 

 이렇게 해서 추상 클래스와 인터페이스에 대해 알아보았다.

'Android > Kotlin' 카테고리의 다른 글

Sealed Class  (0) 2023.05.26
Enum Class  (0) 2023.05.26
변수 캡슐화하기 (Encapsulate Variable)  (0) 2023.05.12
REST API, Retrofit  (0) 2023.05.01
REST API에서 HATEOAS란?  (0) 2023.05.01
profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!