Developing Myself Everyday

추상 클래스(Abstract Class)


추상 클래스는 직접 인스턴스화 할 수 없고 다른 클래스의 상위 역할만 할 수 있는 클래스를 말합니다. 클래스를 추상 클래스로 만들기 위해서는 abstract라는 변경자 키워드를 붙여야 합니다.

 

좀 더 쉽게 말하자면 기본 함수를 강제하고 기본 속성을 가질 때 사용합니다.

 

아래의 예시를 보겠습니다.

abstract class Animal {
    abstract val species: String
    abstract fun makeSound()
}

class Dog : Animal() {
    override val species: String = "개"

    override fun makeSound() {
        println("멍멍!")
    }
}

fun main() {
    val dog = Dog()
    println("종: ${dog.species}") // 출력: 종: 개
    dog.makeSound() // 출력: 멍멍!
}

 

Animal은 추상 클래스입니다. 이것은 객체를 직접 생성할 수 없으며, 하위 클래스에서 상속받아 사용해야 합니다.

 

Animal은 동물들의 기본 클래스가 될 수 있으며 species라는 추상 속성과 makeSound라는 추상 메서드를 가집니다. 이러한 속성은 모든 동물은 반드시 종이 있으며 소리를 낸다는 것을 말합니다.


추상 속성과 추상 메서드는 본문이 없이 선언만 되어 있으며 하위 클래스에서 반드시 구현되어야 합니다.

 

 

추상 클래스 내의 일반 속성과 메서드

추상 클래스 내에도 일반 속성과 메서드를 정의할 수 있습니다. 이때에는 초기값과 본문을 정의해야 합니다.

 

추상 클래스 내에서 일반 메서드를 사용하는 예를 들어보겠습니다.

 

모든 자동차는 아래의 추상 클래스와 같이 보여야 한다고 말합니다. 모든 자동차는 fuel가 있으므로 이는 일반 메서드와 속성으로 구현합니다.

 

다만 자동차의 종류는 각각일 것입니다. type을 가진다는 공통점은 있지만, 세부 내용은 알아서 채워야 합니다.

abstract class MotorVehicle {
    var fuel: Int = 0

    fun getFuel(): Int {
        return fuel
    }

    abstract fun type()
}

 

MotorVehicle을 상속받는 사람은 자신의 자동차의 타입을 정의합니다.

class MyCar : MotorVehicle() {
    override fun type() {
        println("Mercedes-Benz")
    }
}

 

 

object 활용

이전에 배웠던 object를 활용해서 익명 객체를 만들어 추상 클래스로 상속받고 이를 사용할 수 있습니다.

abstract class Animal {
    abstract val species: String
    abstract fun makeSound()
}

class Dog : Animal() {
    override val species: String = "개"

    override fun makeSound() {
        println("멍멍!")
    }
}

val cat = object : Animal() {
    override val species: String = "고양이"

    override fun makeSound() {
        println("야옹!")
    }
}

fun main() {
    val dog = Dog()
    println("종: ${dog.species}") // 출력: 종: 개
    dog.makeSound() // 출력: 멍멍!

    println("종: ${cat.species}") // 출력: 종: 고양이
    cat.makeSound() // 출력: 야옹!
}

 

 

 

 

인터페이스(Interface)


인터페이스는 계약입니다. 인터페이스를 작성하는 사람은 "이것은 이렇게 보여야 합니다." 고 말하며, 인터페이스를 사용하는 사람은 "알겠습니다. 제가 작성하는 클래스는 그렇게 보입니다" 라고 합니다.

 

인터페이스 자체는 아무것도 수행할 수 없습니다. 그저 양식일 뿐입니다. 인터페이스는 비어 있는 뼈대로 메서드의 시그니처만 있으며 본문은 없습니다. 

 

인터페이스도 추상 클래스처럼 객체를 생성할 수 없습니다.

interface MotorVehicle {
    val fuel: Int
    fun getFuel(): Int
}

class MyCar : MotorVehicle {
    override val fuel: Int = 100
    override fun type() {
        println("Mercedes-Benz")
    }
}

 

 

 

인터페이스 내의 일반 속성과 메서드

인터페이스 내에도 일반 속성과 메서드를 만들 수 있습니다. 인터페이스 속성과 메서드에는 abstract 키어드를 사용하지 않기에 get()을 정의하거나 메서드에 몸체부를 구현하면 일반 속성과 메서드로 판단합니다.

interface MotorVehicle {
    val fuel: Int
        get() = 100
    fun type() {
        println("")
    }
}

class MyCar : MotorVehicle {
    override fun type() {
        println("Mercedes-Benz")
    }
}

fun main() {
    val myCar = MyCar()
    println(myCar.fuel) // 100
    myCar.type() // Mercedes-Benz
}

 

 

object 활용

인터페이스도 object를 활용할 수 있습니다.

val bus = object : MotorVehicle {
    override fun type() {
        println("현대")
    }
}

 

 

 

 

추상 클래스와 인터페이스의 차이


추상 클래스와 인터페이스는 매우 비슷하지만 사용 방식에 차이가 있습니다.

 

추상 클래스는 기본 함수를 강제하고 기본 속성을 가질 때 사용합니다. 예를 들어 조류는 새들의 기본 클래스가 될 수 있고 나이, 종 등의 속성을 가질 수 있습니다. 이러한 속성은 새의 행동을 변경해서는 안되는 것들입니다.

 

그러나 모든 새가 소리나거나 날아가는 방식이 같지 않습니다. (타조는 날지 못합니다) 그렇기에 이러한 기능들은 인터페이스를 통해 구현되어야 합니다.

 

 

상속과 구현

추상 클래스와 인터페이스는 상속구현 관점에서도 차이가 있습니다.

 

클래스 간의 계층 관계를 상속이라고 합니다. 추상 클래스는 인터페이스와 달리 클래스이기에 클래스와 추상 클래스 간의 계층 관계를 항속이라 합니다.

 

반면 클래스와 인터페이스의 계층 관계는 구현이라고 합니다. 그렇기에 자바의 클래스 간 다중 상속을 허용하지 않는다는 규칙은 클래스와 인터페이스의 관계에서는 해당되지 않습니다.

 

아래의 코드를 보겠습니다. 

abstract class AbstractClassA {
    abstract fun commonMethod()
}

abstract class AbstractClassB {
    abstract fun commonMethod()
}

class Example : AbstractClassA(), AbstractClassB() { // 에러발생
    override fun commonMethod() {
        println()
    }
}

만약 클래스에서 다중 상속을 허용한다면, 자식 클래스에서 부모 클래스의 어떤 메서드를 호출해야 할지 자식은 판단할 수 없습니다. 이런 다이아몬드 문제가 발생하기 때문에 클래스간의 다중 상속은 허용되지 않습니다.

 

 

반면에 인터페이스에서는 구현할 메서드의 대한 정의만 하고 있기 때문에 아래와 같은 코드가 문제 없습니다.

interface InterfaceA {
    fun commonMethod()
}

interface InterfaceB {
    fun commonMethod()
}

class Example : InterfaceA, InterfaceB {
    override fun commonMethod() {
        println()
    }
}

 

 

 

 

봉인 클래스(Sealed Class)


Sealed 클래스란 추상 클래스이기에 추상 클래스의 역할을 합니다.

 

Sealed 클래스와 추상 클래스의 가장 큰 차이는, Sealed 클래스를 상속하는 클래스는 반드시 동일 패키지(파일) 안에 있어야 한다는 점입니다.

 

이런 차이로 인해 라이브러리나 프레임워크를 개발할 때는 추상 클래스를 활용하는 경우가 많습니다. 또한 multi-module project인 경우에는 다른 모듈에 있는 Sealed 클래스를 상속할 수 없기 때문에, 이런 경우에는 추상 클래스를 사용합니다.

 

sealed class Result {
    data class Success(val message: String) : Result()
    data class Error(val errorMessage: String) : Result()
    object Loading : Result() 
}

 

 

Sealed 클래스에 When 사용

Sealed 클래스를 사용할 때 가장 큰 이점은 when 표현식에서 사용할 때 나타납니다. 문장이 모든 경우를 다루는 것을 확인할 수 있는 경우, 문장에 else 절을 추가할 필요가 없습니다:

fun processResult(result: Result) {
    when (result) {
        is Result.Success -> {
            println("Success: ${result.message}")
        }
        is Result.Error -> {
            println("Error: ${result.errorMessage}")
        }
        Result.Loading -> {
            println("Loading...")
        }
    }
}

 

 

Sealed 클래스 내의 Object와 data class

Sealed 클래스는 내의 속성은 개별 인스턴스로 처리됩니다. 만약 속성의 값이 중요한 값을 포함하지 않는다면 불필요한 인스턴스를 생성하지 않고 하나의 인스턴스만 생성해서 재사용하는 것이 좋습니다.

 

이를 가능하게 해주는 것이 Object입니다. 

 

아래의 코드를 보겠습니다.

sealed class HttpError(val code: Int) {
    data class Unauthorized(val reason: String): HttpError(401)
    object NotFound: HttpError(404)
}

object로 선언된 NotFound 오류는 중요한 값을 포함하지 않기에 싱글톤 객체로 표현하는 것이 합리적입니다.

 

그럼 Unauthorized 오류는 어떨까요?

 

Unauthorized 오류의 경우 `reason`을 포함해야 합니다. Unauthorized 오류는 서로 다른 `reason` 값을 가진 여러 인스턴스를 생성할 수 있어야 합니다. 이러한 경우에는 데이터 클래스가 사용될 수 있습니다. 

 

 

Reference

 

What is the difference between an interface and abstract class?

What exactly is the difference between an interface and an abstract class?

stackoverflow.com

 

profile

Developing Myself Everyday

@배준형

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