본문 바로가기
코틀린/개발자를 위한 코틀린 프로그래밍

[chapter09] 추상 클래스, 인터페이스 알아보기

by 측면삼각근 2023. 9. 6.
728x90
반응형

들어가기 전에

본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다.

이전 포스팅 ⬇️

[chapter08] 컬렉션 알아보기

 

[chapter08] 컬렉션 알아보기

들어가기 전에 본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다. 이전 포스팅 ⬇️ [chapter07] 클래스 관계

messycode.tistory.com


01 추상 클래스 알아보기

추상 클래스는 abstract로 지정한 클래스이다.
추상 클래스의 특징은 직접 객체를 생성할 수 없고, 항상 다른 클래스에서 상속해서 추상 메서드를 구현해야 한다.
추상 클래스에서는 추상 속성과 추성 메서드 이외의 일반 속성과 일반 메서드도 정의할 수 있다.

1.1 추상 클래스 정의 규칙

  • 추상 클래스는 클래스 앞에 abstract를 붙여야 한다.
  • 추상 속성과 메서드를 정의할 때는 abstract예약어를 사용한다.
  • 추상 속성과 메서드는 별도의 open지시자를 작성할 필요가 없다.
  • 추상 클래스 내에 값이 할당된 속성과 구현된 메세드도 정의할 수 있다.
    보통 이런 속성과 메서드를 일반(default) 속성, 일반(default) 메서드라고 한다.
    하위 클래스에서 상속하여면 open지시자가 필요하다.
  • 구현 클래스에서 override한 속성이나 메서드가 하위 클래스에서 상속을 금지하려면 final지시자가 필요하다.
  • 추상 클래스는 일반 클래스를 상속받을 수 있다.
    일반 클래스가 추상 클래스를 상속받아 구현한 경우 이를 다시 override abstract를 붙여서 추상화시킬 수 있다.
  • 추상클래스도 주 생성자 정의가 가능하다. 구현 클래스가 생성자 있는 추상 클래스를 상속받아 구현했다면 위임호출 할 때 생성자의 인자를 전달해야 한다.
  • 추상 클래스에 아무런 속성이 없어도 구현 클래스에서 상속할 때는 위임 호출이 필요하다.
  • 추상 클래스 내부에도 초기화 블록을 정의할 수 있다.
  • 추상 클래스 내부에 다른 클래스의 확장 함수를 추상화 메서드로 정의할 수 있다.
  • 추상 클래스로 확장함수를 정의할 수 있다. 이 확장함수는 추상클래스를 상속한 클래스에서 모두 사용할 수 있다.

1.2 추상클래스

  • 추상 클래스를 정의하면 반드시 구현 클래스에서 상속을 하고 추상 속성과 추상 메서드를 구현해야 사용할 수 있다.
  • 일반 메서드를 정의하고, open을 처리해서 상속받는 클래스에서 이 메서드를 재 정의할 수 있다.
  • 추상 클래스도 주 생성자 정의가 가능하며, 초기화 블록을 제공한다.
    • 추상 클래스 -> 상속받은 클래스 순으로 실행
abstract class Mammal(val birth: String, val name: String) {
    init{
        println("Mammal 초기화")
    }

    abstract val specie: String// 추상 프로퍼티

    open fun eat() { // 일반 메서드 정의
        println("먹는다")
    }

    abstract fun sleep() // 추상 메서드 정의
}

class Human(birth: String, name: String) : Mammal(birth, name) {
    init{
        println("Human 초기화")
    }
    override val specie: String = "Human" // 추상 프로퍼티 구현
    override fun sleep() { // 추상 메서드 구현
        println("침대에서 잔다")
    }
    override fun eat() { // 일반 메서드 재정의
        println("접시에 담아 먹는다")
    }
}

fun main() {
    // Mammal 클래스는 추상 클래스이므로 인스턴스화 할 수 없다.
    val human = Human("1980-05-30", "김철수")
    // Mammal 초기화 출력
    // Human 초기화 출력
    
    human.eat() // 일반 메서드 사용 -> "먹는다" 출력
    human.sleep() // 추상 메서드 사용 -> "침대에서 잔다" 출력
}

1.3 추상 클래스 활용

추상 클래스를 정의하고 이를 Object 표현식에서 상속받아 익명 객체를 만들 수 있다.

abstract class Weapon {
    abstract fun attack()
    fun move(){
        println("Move")
    }
}

fun main() {
    val w2 = object : Weapon() { // 객체 표현식으로 익명객체 추상 메서드 구현
        override fun attack() {
            println("Attack with sword")
        }
    }

    w2.attack() // Attack with sword
    w2.move() // Move
}

추상 클래스 내에 추상 확장함수 처리

추상 클래스 안에 다른 클래스의 추상 메서드를 확장 함수로 정의할 수 있다.

abstract class Base {
    abstract fun String.extension(x: String): String // 문자열 확장 함수를 추상 메서드로 정의
}

class Derived : Base() {
    override fun String.extension(x: String): String { // 추상 메서드를 오버라이딩
        return this + x
    }

    fun callExtension(name: String): String { // 확장 함수를 정의
        return "Hello ".extension(name) // 확장 함수를 호출
    }
}

fun main() {
    val derived = Derived()
    println(derived.callExtension("찬영")) // Hello 찬영
}

02 인터페이스 알아보기

코틀린은 단일 상속만 지원한다. 대신 인터페이스를 상속해서 추상 메서드를 구현할 수 있다. 인터페이스도 추상 클래스처럼 계층 구조를 만들 수 있다. 또, 인터페이스는 변수 등에 지정하는 자료형으로 사용할 수 있다.

2.1 클래스와 인터페이스 차이

  • 클래스는 객체를 생성해서 내부의 속성과 메서드를 사용한다.
  • 인터페이스는 추상 속성과 추상 메서드를 기본으로 처리(인터페이스 내부는 모두 추상화된 것을 기준으로 함)한다. 하지만 일반 속성과 메서드로 구현해서 지원할 수 있다.
    인터페이스도 추상 클래스처럼 객체를 생성할 수 없다.
  • 보통 인터페이스는 클래스에 상속해야 사용되지만, 인터페이스에 확장함수를 작성하면 하위의 모든 클래스의 객체는 이 확장함수를 사용할 수 있다.
  • 클래스와 인터페이스는 자료형으로 사용할 수 있어서 변수, 매개변수, 함수 반환 자료형에 자료형으로 지정할 수 있다.

2.2 상속과 구현의 차이

  • 상속(inheritance)
    • 추상 클래스나 일반 클래스 간의 관계를 구성하는 것
      상속시 슈퍼클래스와 서브클래스로 표현
  • 구현(implements)
    • 클래스가 인터페이스 내의 추상 멤버를 일반 멤버로 작성하는 것이다.
      구현은 인터페이스와 구현 클래스로 표현

2.3 인터페이스 정의 규칙

  • interface예약어를 사용
  • 모두 추상화된 것이라고 보므로, abstract지시자는 사용할 필요 없음
  • 속성과 메서드는 지시자를 별도로 붙이지 않아서 구현 여부로 추상과 일반으로 구분한다.
  • 일반 속성을 정의하면 배킹필드가 없어 반드시 게터 메서드에 초깃값을 처리해야 한다.
  • 구현 클래스나 인터페이스추상클래스와 달리 복수의 인터페이스를 상속할 수 있다.
  • 구현 클래스는 인터페이스에 정의된 추상 속성과 추상 메서드는 반드시 구현해야 한다.
  • 구현 클래스는 인터페이스의 일반 속성과 일반 메서드 중에 open지시자가 있으면 재정의 할 수 있다.
  • 인터페이스도 하나의 자료형이다. 변수나 매개변수, 반환에 자료형으로 사용할 수 있다.

2.4 인터페이스 정의

  • 인터페이스는 기본이 추상이라 별도로 abstract를 표기하지 않는다.
interface Clickable {
    fun up()
    fun down()

    // 인터페이스에 배킹필드가 없어서 반드시 게터 메서드에 초기값 처리 필요
    // val abstractProperty: String

    // property with implementation
    val gauge: Int
        get() = 0
}

class TvVolume() : Clickable { // 인터페이스 상속
    override var gauge = 10 // 오버라이드
    // 만약 gauge 를 ov/erride 하지 않더라도 인터페이스에서 선언한 기본값 0이 들어감

    override fun up() {
        gauge++
        println("Volume up")
    }

    override fun down() {
        gauge--
        println("Volume down")
    }
}

코틀린의 인터페이스 다중상속

코틀린에서는 클래스끼리 다중상속을 할 수는 없지만, 클래스가 인터페이스를 다중상속받는 것은 가능하다.

// 코틀린에서는 클래스가 여러개의 인터페이스를 다중상속 받는것이 가능하다.
interface A {
    fun interfaceFunc() {
        println("A")
    }
}

interface B {
    fun interfaceFunc() {
        println("B")
    }
}

class C : A, B {
    override fun interfaceFunc() {
        super<A>.interfaceFunc() 
        super<B>.interfaceFunc()// super<B>.interfaceFunc()을 하면 B가 출력된다.
    }
    fun functionC() {
        println("C")
    }
}

val c = C()
c.interfaceFunc() // A, B
c.functionC()

확장함수

인터페이스도 하나의 자료형이므로 확장함수를 정의할 수 있다.
인터페이스에 확장함수를 정의하면 이 인터페이스를 상속한 모든 하위 클래스에서 사용할 수 있다.

interface Context {
    fun absMethod(): String
}

class A : Context {
    override fun absMethod(): String = "A"
}

fun Context.extensionMethod(): String = "extension"


fun main() {
    val a = A()
    println(a.absMethod())
    println(a.extensionMethod())
}

03 봉인 클래스 알아보기

봉인 클래스도 추상 클래스이다.
별도로 지정해 사용하는 이유는 특정 추상 클래스를 상속해 구현하는 것을 제한하기 위함이다.

3.1 봉인 클래스 정의 규칙

  • 봉인 클래스는 항상 최상위 클래스가 되어야 하므로 가장 먼저 정의한다.
  • 봉인 클래스를 상속하는 서브 클래스는 반드시 같은 파일 내에 선언한다.
    단, 봉인 클래스를 상속하지 않고 그 서브클래스를 상속한 경우는 같은 파일에 작성하지 않아도 된다.
  • 봉인 클래스 내부에 서브클래스 정의도 가능하다. (내부 클래스는 private 생성자만 가진다.)
  • 봉인 클래스는 기본적으로 abstract클래스 이다.
  • 서브클래스는 class, data class, object 모두 가능
  • 봉인클래스에 확장 함수를 추가할 수 있다.
sealed class SealedClass // 봉인클래스 정의
class AClass : SealedClass() // 봉인클래스 상속
class BClass : SealedClass()
object CObject : SealedClass()
data class DDataClass(val name: String) : SealedClass()

sealed class SealedClass1{ // 봉인 클래스 정의
    class AClass1 : SealedClass() // 클래스 내부에 정의
    class BClass1 : SealedClass()
    object CObject1 : SealedClass() // object 정의도 상속 가능
    data class DDataClass1(val name: String) : SealedClass() // 데이터 클래스도 상속 가능

    val a = AClass1()
}

val a = AClass()
val a2 = SealedClass1.DDataClass1("name") // 봉인 클래스 내부 참조

봉인 클래스 내의 속성과 상속 처리

봉인 클래스의 생성자 정의와 봉인 클래스를 상속한 클래스를 다른 클래스가 어떻게 처리하는지 상속 방식의 차이

sealed class A(var name: String) // 봉인 클래스에 생성자 정의
class B : A("B클래스") // 봉인 클래스 상속한 클래스 정의
class C : A("C클래스")

sealed class AA private constructor(var name: String) {
    class B : AA("B클래스")// 내부 클래스에서 위임 호출
    class C : AA("C클래스")
}

sealed class Fruit() {
    class Apple : Fruit()
    class Banana : Fruit()
    open class Unknown : Fruit() {
        fun print() = "Unknown"
    }
}

fun main() {
    println(B().name) // B클래스
    println(AA.B().name)

    class Tomato : Fruit.Unknown() // 내부의 클래스도 상속 가능
    // 다른 파일인 경우 봉인클래스를 상속한 경우 내부의 클래스를 상속할 수 있다.
    println(Tomato().print()) // Unknown
}

sealed class의 사용

  • 제한된 계층 모델링: 변형이 가능한 제한된 집합을 가질 수 있는 형식을 모델링 하려는 경우
  • 상태머신: 상태머신에서 상태 및 상태 전환을 모델링함
    • sealed class의 서브 클래스는 고유한 상태를 가지며, 상태 간에 허용되는 전환을 sealed class 계층으로 정의 할 수 있음
  • 결과 형식: 함수형 프로그래밍에서 성공, 실패 또는 사용자 지정 결과와 같은 여러 기능을 가질 수 있는 결과를 나타내는 데 사용됨
  • APi응답: api 작업시, 성공 응답, 오류 및 기타 응답 유형
  • 이벤트 처리: 이벤트 기반 시스템에서 다양한 유형의 이벤트 또는 작업을 나타냄
sealed class Shape {
    class Circle(val radius: Double) : Shape()
    class Rectangle(val width: Double, val height: Double) : Shape()
    class Triangle(val base: Double, val height: Double) : Shape()
}

서로 다른 모양을 나타내는 세개의 하위 클래스가 있는 sealed class의 활용

fun calculateArea(shape: Shape): Double {
    return when (shape) {
        is Shape.Circle -> Math.PI * shape.radius * shape.radius
        is Shape.Rectangle -> shape.width * shape.height
        is Shape.Triangle -> 0.5 * shape.base * shape.height
    }
}

아래와 같이 api응답으로도 사용할 수 있다.

sealed class ApiResponse<out T> {
    data class Success<out T>(val data: T) : ApiResponse<T>()
    data class Error(val message: String) : ApiResponse<Nothing>()
}

// Example usage:
fun fetchDataFromApi(): ApiResponse<String> {
    // Simulate a successful API call
    val responseData = "Data from the API"
    return ApiResponse.Success(responseData)
}

fun main() {
    val apiResponse: ApiResponse<String> = fetchDataFromApi()

    when (apiResponse) {
        is ApiResponse.Success -> {
            val data = apiResponse.data
            println("API Success: $data")
        }
        is ApiResponse.Error -> {
            val errorMessage = apiResponse.message
            println("API Error: $errorMessage")
        }
    }
}
반응형