본문 바로가기

책 요약하기/Do it! 코틀린 프로그래밍

#07-1. 추상 클래스와 인터페이스 2021-05-08

07-1 추상 클래스와 인터페이스

추상 클래스(Abstract Class)는 선언 등의 대략적인 설계 명세와 공통의 기능을 구현한 클래스입니다. '추상(abstract)'이라는 말은 '구체적이지 않은 것'을 나타냅니다. 추상 클래스를 상속하는 하위 클래스에서 추상 클래스의 내용을 더 구체화해야 합니다. 인터페이스(interface)도 대략적인 설계 명세를 가지고 몇 가지 기본적인 부분은 구현할 수 있지만 하위에서 더 자세히 구현해야 하는 점은 추상 클래스와 동일합니다. 하지만 추상클래스와 다르게 인터페이스에서는 프로퍼티에 상태 정보를 저장할 수 없습니다. 단, 다중 상속과 같이 여러 개의 인터페이스를 하나의 클래스에서 구현하는 것이 가능합니다.

package chapter_07

fun main() {
    
}
// 추상 클래스, 주 생성자에는 비추상 프로퍼티 선언의 매개변수 3개가 있음.
abstract class Vehicle(val name: String, val color: String, val weight: Double) {

    // 추상 프로퍼티(반드시 하위 클래스에서 재정의해 초기화해야 함)
    abstract var maxSpeed: Double

    // 일반 프로퍼티(초깃값인 상태를 저장할 수 있음)
    var year = "2018"

    // 추상 메서드(반드시 하위 클래스에서 구현해야 함)
    abstract fun start()
    abstract fun stop()

    // 일반 메서드
    fun displaySpecs() {
        println("Name: $name, Color: $color, Weight: $weight, Year: $year, Max Speed: $maxSpeed")
    }
}

Vehicle 클래스는 객체를 생성할 수 없고 달것에 대한 기본 설계 역할을 합니다. 주생성자의 매개변수 name, color, weight와 year은 일반 프로퍼티이며, displaySpecs() 메서드 역시 일반 메서드 입니다. 그 외 maxSpeed 프로퍼티, start(), stop()메서드는 추상 메서드로 선언되었습니다. 
따라서 추상 맴버들은 하위 클래스(자식 클래스)에서 재정의(override)가 필요합니다.
( ※ 추상 클래스 상속 시 open 키워드를 사용할 필요가 없습니다. :)

계속해서 추상 클래스를 상속하는 하위 클래스 Car와 Motorcycle을 만들어서 추가한 전체 코드입니다 :)

package chapter_07

fun main() {
    val car = Car("SuperMatiz", "yellow", 1110.0, 270.0)
    val motor = Motorcycle("DreamBike", "red", 170.0, 100.0)

    car.year = "2013"

    car.displaySpecs()
    car.start()

    motor.displaySpecs()
    motor.start()
}
/*
    결과 :
    name: SuperMatiz, color: yellow, weight: 1110.0, maxSpeed: 270.0
    Car Started
    name: DreamBike, color: red, weight: 170.0, maxSpeed: 100.0
    Bike Started
 */



// 추상 클래스, 주 생성자에는 비추상 프로퍼티 선언의 매개변수 3개가 있음.
abstract class Vehicle(val name: String, val color: String, val weight: Double) {

    // 추상 프로퍼티(반드시 하위 클래스에서 재정의해 초기화해야 함)
    abstract var maxSpeed: Double

    // 일반 프로퍼티(초깃값인 상태를 저장할 수 있음)
    var year = "2018"

    // 추상 메서드(반드시 하위 클래스에서 구현해야 함)
    abstract fun start()
    abstract fun stop()

    // 일반 메서드
    fun displaySpecs() {
        println("Name: $name, Color: $color, Weight: $weight, Year: $year, Max Speed: $maxSpeed")
    }
}

class Car(
    name: String,
    color: String,
    weight: Double,
    override var maxSpeed: Double // maxSpeed는 오버라이딩
) : Vehicle(name, color, weight) {
    override fun start() {
        // 코드 구현
        println("Car Started")
    }

    override fun stop() {
        // 코드 구현
        println("Car Stopped")
    }
}

class Motorcycle(
    name: String,
    color: String,
    weight: Double,
    override var maxSpeed: Double
) : Vehicle(name, color, weight) {
    override fun start() {
        // 코드 구현
        println("Bike Started")
    }

    override fun stop() {
        // 코드 구현
        println("Bike Stopped")
    }
}

displaySpecs()는 기존에 추상 클래스가 가지고 있던 일반 메서드입니다. 각 객체(Car, Motocycle)의 start(), stop()은 추상 메서드로부터 오버라이딩되어 하위 클래스에서 구현된 메서드입니다. 추상 클래스에는 일반 프로퍼티나 메서드도 만들 수 있기 때문에 공통의 프로퍼티와 메서드를 미리 만들어 둘 수 있습니다.

 

package chapter_07

fun main() {
    val myPrinter = object: Printer() {
        override fun print() {
            println("출력합니다.")
        }
    }

    myPrinter.print()
}
/*
    결과 :
    출력합니다.
 */

abstract class Printer {
    abstract fun print() // 추상 메서드
}

만일 추상 클래스로부터 하위 클래스를 생성하지 않고 단일 인스턴스로 객체를 생성하려면 object를 사용해서 지정할 수 있습니다.

 

인터페이스

인터페이스는 abstract로 정의된 추상 메서드나 일반 메서드가 포함됩니다. 다른 객체 지향 언어와는 다르게 메서드에 구현 내용이 포함될 수 있습니다. 하지만 추상 클래스처럼 프로퍼티를 통해 상태를 저장할 수 없습니다. 인터페이스 또한 객체를 생성할 수 없고 하위 클래스를 통해 구현 및 생성해야 합니다.
특징으로는 인터페이스는 클래스가 아닙니다. 따라서 상속이라는 형태로 하위 클래스에 프로퍼티와 메서드를 전하지 않습니다. 그래서 하위 클래스보다는 구현 클래스라고 합니다. 또한 인터페이스 구현 클래스는 강한 연관을 가지지 않습니다. (상위 클래스를 상속받은 하위 클래스는 상위 클래스가 불완전하게 정의되었다면 이에 대한 영향을 받습니다 :))

자바의 인터페이스
코틀린의 인터페이스는 메서드에 구현 내용을 넣을 수 있으나 자바 8 이전 버전에는 인터페이스 메서드를 구현할 수 없었습니다. 그러나 기본 메서드(Default Method)기능을 추가하여 해당 메서드를 인터페이스 내에서 구현 할 수 있습니다.

 

인터페이스 선언과 구현

인터페이스는 interface 키워드를 사용해 선언하고 상속한 하위 클래스에서는 override를 사용해 해당 메서드를 구현해 주어야 합니다.

interface 인터페이스 이름 [: 인터페이스 이름...] {
    추상 프로퍼티 선언
    추상 메서드 선언

    [일반 메서드 선언 { ... }]
}
package chapter_07.chapter_07_1

interface Pet {
    var category: String // abstract 키워드가 없어도 기본은 추상 프로퍼티
    fun feeding() // 추상 메서드
    fun patting() { // 일반 메서드: 구현부를 포함하면 일반적인 메서드로 기본이 됨.
        println("Keep patting!") // 구현부
    }
}

class Cat(override var category: String) : Pet {
    override fun feeding() {
        println("Feed the cat a tuna can!")
    }
}

fun main() {
    val obj = Cat("small")
    println("Pet Category: ${obj.category}")
    obj.feeding() // 구현된 메서드
    obj.patting() // 기본 메서드
}
/*
    결과 :
    Pet Category: small
    Feed the cat a tuna can!
    Keep patting!

 */

해당 소스 코드에서 인터페이스는 추상 클래스와는 다르게 abstract를 붙여주지 않아도 기본적으로 추상 프로퍼티와 추상 메서드가 지정됩니다. :) 그리고 메서드에는 기본 구현부가 있으면 일반 메서드로서 기본 구현을 가집니다. 상태를 저장할 수 없기에 프로퍼티에는 기본값을 가질 수 없습니다.
Cat 클래스는 Pet인터페이스를 구현한 클래스가 됩니다. 추상 프로퍼티나 추상 메서드였던 부분을 override 키워드를 사용해 구현해 줍니다.

게터를 구현한 프로퍼티

package chapter_07.chapter_07_1

interface Pet2 {
    var category: String
    val msgTags: String // val 선언 시 게터의 구현이 가능
        get() = "I`m your lovely pet!"

    fun feeding()
    fun patting() {
        println("Keep pattin!")
    }
}

인터페이스에서는 기본적으로 프로퍼티에 값을 저장할 수 없습니다. 단, val로 선언된 프로퍼티는 게터를 통해 필요한 내용을 구현할 수 있습니다.
val로 선언된 msgTags는 초기화할 수 없지만 게터를 통해 반환값을 지정할 수 있습니다. 하지만 여전히 보조 필드인 field를 사용할 수 없습니다. var로 프로퍼티를 선언하더라도 보조 필드를 사용할 수 없기 떄문에 받은 value를 저장할 수 없습니다.

 

인터페이스 구현의 필요성

package chapter_07.chapter_07_1

open class Animal(val name: String)

interface Pet3 {
    var category: String // abstract 키워드가 없어도 기본은 추상 프로퍼티
    fun feeding() // 추상 메서드
    fun patting() { // 일반 메서드: 구현부를 포함하면 일반적인 메서드로 기본이 됨.
        println("Keep patting!") // 구현부
    }
}

// 1. feeding의 구현을 위해 인터페이스 Pet지정
class Dog3(name: String, override var category: String) : Animal(name), Pet3 {
    override fun feeding() {
        println("Feed the dog a bone")
    }
}

class Cat3(name: String, override var category: String) : Animal(name), Pet {
    override fun feeding() {
        println("Feed the cat a tuna can!")
    }
}

class Master {
    fun playWithPet(dog: Dog3) { // 2. 각 애완동물 종류에 따라 오버로딩됨
        println("Enjoy with my dog.")
    }
    fun playWithPet(cat: Cat3) { // 3. 고양이를 위한 메서드
        println("Enjoy with my cat")
    }
}

fun main() {
    val master = Master()
    val dog = Dog3("Toto", "Small")
    val cat = Cat3("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

해당 소스 코드에서 Master 클래스의 playWithPet()은 놀고자 하는 동물에 따라 매개변수를 다르게 정한 오버로딩된 메서드입니다. 그 결과 dog와 cat 클래스에 대해 필요한 playWithPaet()메서드가 수행되었습니다.


그러나 만약 애완동물의 종류가 늘어나면 늘어난 수 만큼 오버로딩된 메서드가 필요하기에 이를 개선 해야 합니다.

package chapter_07.chapter_07_1

open class Animal5(var name: String)

interface Pet5 {
    val category: String
    get() = "test"
    val msgTags: String
        get() = "I`m your lovely pet!"

    var species: String // 종을 위한 프로퍼티
    fun feeding()
    fun patting() {
        println("Keep patting!")
    }
}

class Dog5(name: String, override var category: String): Pet5, Animal5(name) {
    override var species: String = "dog"
    override fun feeding() {
        println("Feed the dog a bone")
    }
}

class Cat5(name: String, override var category: String): Pet5, Animal5(name) {
    override var species: String = "cat"
    override fun feeding() {
        println("Feed the cat a tuna can!")
    }
}

class Master5 {
    fun playWithPet(pet: Pet5){ // 인터페이스를 객체로 매개변수를 지정
        println("Enjoy with my ${pet.species}")
    }
}

fun main() {
    val master = Master5()
    val dog = Dog5("Toto", "Small")
    val cat = Cat5("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

/*
    결과 :
    Enjoy with my dog
    Enjoy with my cat
 */

이제 Pet 인터페이스에 종을 위한 프로퍼티인 species를 선언하고 이것을 이용해 어떤 애완동물과 놀게 될지 알 수 있습니다. 이제 Master 클래스의 playWithPet() 메서드는 각 애완동물에 따라서 메서드를 오버로딩 할 필요가 없어졌습니다 :) 이렇게 Master 클래스가 독립성을 확보할 수 있다는 점에서 인터페이스는 중요한 역할을 합니다.

 

여러 인터페이스 구현

package chapter_07.chapter_07_1

interface Bird {
    val wings: Int
    fun fly()
    fun jump() {
        println("bird jump!")
    }
}

interface Horse {
    val maxSpeed: Int
    fun run()
    fun jump() {
        println("jump!, max speed: $maxSpeed")
    }
}

class Pegasus: Bird, Horse {
    override val wings: Int = 2
    override val maxSpeed: Int = 100
    override fun fly() {
        println("Fly!")
    }

    override fun run() {
        println("Run!")
    }

    override fun jump() {
        super<Horse>.jump()
        println("Pegasus Jump!")
    }
}

fun main() {
    val pegasus = Pegasus( )
    pegasus.fly()
    pegasus.run()
    pegasus.jump()
}
/*
    결과 :
    Fly!
    Run!
    jump!, max speed: 100
    Pegasus Jump!
*/

페가수스는 Bird, Horse가 가지는 모든 추상 프로퍼티와 추상 메서드를 오버라이딩하여 구현했고, 이미 구현된 jump()는 필요에 따라 오버라이딩 할 수 있습니다. 만일 이름이 동일한 경우 super<[인터페이스 이름]>.[메서드이름]() 형태로 구분할 수 있습니다.

 

인터페이스 위임

인터페이스에서도 by 위임자를 사용할 수 있습니다.

interface A {
    fun functionA(){
        println("functionA()")
    }
}

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

class ClassA : A {
}

class ClassB : B {

}

class C(val a: A, val b: B) {
    fun functionC() {
        a.functionA()
        b.functionB()

        println("functionC()")
    }
}

fun main() {
    val a = C(ClassA(), ClassB())
    a.functionC()
}

/*
    결과 :
    functionA()
    functionB()
    functionC()
 */

interface A, B를 구현한 ClassA(), ClassB() 생성자를 호출해서 객체를 생성하고 C()생성자의 인자로 넘겨줬습니다. 여기서 functionA()와 functionB() 메서드에 직접 접근하기 위해 a와 b변수를 사용했습니다. 이때 by 위임자를 사용하면 다음과 같이 간략하게 작성할 수 있습니다.

class DelegatedC(a: A, b: B): A by a, B by b {
    fun functionC() {
    	functionA()
        functionB()
    }
}

각각 a와 b를 인터페이스 A, B에 위임함으로써 해당 메서드를 사용할 떄 점(.)표기법 접근 없이 사용할 수 있게 됩니다.

 

위임을 이용한 멤버 접근

package chapter_07.chapter_07_1

interface Nameable {
    var name: String
}

class StaffName: Nameable {
    override var name: String = "Sean"
}

class Work: Runnable { // 스레드 실행을 위한 인터페이스
    override fun run() {
        println("work...")
    }
}

// 1. 각 매개변수에 해당 인터페이스를 위임
class Person(name: Nameable, work: Runnable) : Nameable by name, Runnable by work

fun main() {
    val person = Person(StaffName(), Work()) // 2. 생성자를 사용해 객체 바로 전달
    println(person.name) // 3. 여기서 StaffName 클래스의 name 접근
    person.run() // 4. 여기서 Work 클래스의 run 접근
}

1번에서 각 매개변수에 해당 인터페이스를 위임합니다. 여기서는 Person 클래스가 마치 상소고가 같은 형태로 위임을 사용하고 있습니다. 2번에서 StaffName과 Work클래스의 생성자를 통해 객체를 전달합니다. 이렇게 만든 person 객체는 3번과 4번처럼 각 클래스의 위임된 멤버에 접근할 수 있게 됩니다.

 

커피 제조기 만들어 보기

커피 제조 과정
커피를 만들려면 물을 뜨겁게 데우는 히터(Heater)가 필요합니다. 히터는 켜거나 끌 수 있고 충분히 뜨거운지도 파악할 수 있습니다. 커피는 열사이펀(Thermosiphon)을 통해 추출합니다. 열사이펀은 기압차를 통해 펌핑하는 원리로 커피액을 추출합니다. 커피 모듈은 이 열사이펀을 사용해 드립 커피를 제조합니다. 이제 커피를 제조하는 브루잉과정을 실행할 수 있게 됩니다. 브루잉 과정은 전원을 키고, 펌핑 -> 커피 완성 -> 전원 끄기 순으로 진행됩니다.
용어 선언 설명
히터 Heater 일반적인 켜기, 끄기, 뜨거운 상태를 확인
전기 히터 ElectricHeater 추상적인 히터를 구체화한 클래스
펌프 Pump 열사이펀 과정을 위한 펌핑 기능을 가진 개념
열사이펀 Thermosiphon 커피 추출을 위한 방법 중 하나
커피 모듈 CoffeeModule 커피 모듈은 추출 방법을 선택할 수 있음.(일단은 한가지)
드립 커피 모듈 MyDripCoffeeModule 커피 모듈을 전기 히터와 열사이펀을 조합해 구체화
커피 제조기 CoffeeMaker 최종 제품으로 커피 모듈을 통해 브루잉 과정을 진행

 

히터 Heater 인터페이스

package chapter_07.CoffeeMaking.Heater

interface Heater {
    fun on()
    fun off()
    fun isHot(): Boolean
}

히터를 나타내는 Heater 인터페이스에서는 히터를 켜고 끄거나 뜨거운 상태를 나타내는 메서드만 선언되어 있습니다.

 

전기 히터 ElectricHeater 클래스

package chapter_07.CoffeeMaking.Heater

class ElectricHeater(var heating: Boolean = false): Heater {
    override fun on() {
        println("[ElectricHeater] heating...")
        heating = true

    }

    override fun off() {
        heating = false
    }

    override fun isHot(): Boolean = heating
}

전기 히터를 나타내는 ElectricHeater 클래스는 heating 프로퍼티를 가지고 있으며 기본값은 false입니다. 그리고 Heater 클래스의 켜고 끄거나 끓고 있는지 확인하는 메서드를 오버라이딩해 구현했습니다.

이제 Pump인터페이스와 Pump인터페이스의 추상 메서드를 구현하는 열사이펀 Thermosiphon 클래스를 만듭니다.

package chapter_07.CoffeeMaking.Pump

interface Pump {
    fun pump()
}
package chapter_07.CoffeeMaking.Pump

import chapter_07.CoffeeMaking.Heater.Heater

class Thermosiphon(heater: Heater) : Pump, Heater by heater {
    override fun pump() {
        if (isHot()) {
            println("[Thermosiphon] pumping...")
        }
    }
}

Thermosiphon 클래스에서는 Heater by heater 위임을 통해 isHot()을 그대로 사용하고 있습니다. 이제 커피 모듈의 기본 인터피에스와 여러 기능을 조합하도록 드립 커피 모듈 MyDripCoffeeModule 클래스를 만들어 봅시다.

package chapter_07.CoffeeMaking.Brewing

import chapter_07.CoffeeMaking.Pump.Thermosiphon

interface CoffeeModule {
    fun getThermosiphon() : Thermosiphon
}
package chapter_07.CoffeeMaking.Brewing

import chapter_07.CoffeeMaking.Heater.ElectricHeater
import chapter_07.CoffeeMaking.Pump.Thermosiphon

class MyDripCoffeeModule: CoffeeModule {
    companion object {
        val eletricHeater: ElectricHeater by lazy { // lazy를 이용한 지연 초기화
            ElectricHeater()
        }
    }
    private val _thermosiphon: Thermosiphon by lazy { // 임시적인 private 프로퍼티
        Thermosiphon(eletricHeater)
    }

    // 오직 이 메서드에서만 Thermosiphon을 초기화
    override fun getThermosiphon(): Thermosiphon = _thermosiphon
}

MyDripCoffeeModule 클래스에서는 by lazy를 사용해 전기 히터 ElectricHeater와 Thermosiphon이 접근되는 시점에 초기화하도록 하고 있습니다. 여기서 눈여겨볼 부분은 THermosiphon으로 초기화하기 위해 임시 프로퍼티인 _thermosiphon을 사용해 private으로 선언한 부분입니다. 이것은 외부에서 초기화하지 못하고 오직 클래스 내부의 getTHermosiphon()에 의해서만 초기화하도록 한 것입니다. 

 

package chapter_07.CoffeeMaking.CoffeMake

import chapter_07.CoffeeMaking.Brewing.CoffeeModule
import chapter_07.CoffeeMaking.Brewing.MyDripCoffeeModule
import chapter_07.CoffeeMaking.Pump.Thermosiphon

class CoffeeMaker(val coffeeModule: CoffeeModule) {
    fun brew() {
        val theSiphon: Thermosiphon = coffeeModule.getThermosiphon()
        theSiphon.on()
        theSiphon.pump()
        println("Coffee, here! Enjoy!~")
        theSiphon.off()
    }
}

fun main() {
    val coffeeMaker = CoffeeMaker(MyDripCoffeeModule())
    coffeeMaker.brew()
}
/*
    결과 :
    [ElectricHeater] heating...
    [Thermosiphon] pumping...
    Coffee, here! Enjoy!~
 */

해당 CoffeeMaker클래스에서는 브루잉 기능을 정의하고 실행합니다.

여기서는 MyDripCoffeeModule()생성자를 인자로 받아 바로 객체를 넘겨주고 있습니다. 이 객체를 통해 열사이펀의 방법으로 객체 theSiphon을 생성하게 됩니다. 이제 coffeeMaker.brew()을 통해서 열사이펀을 켜고(on()), 펌프질한 뒤(pump()), 커피를 완성(println())하고 열사이펀 객체를 끕니다.(off())

이렇게 인터페이스를 사용하는 가장 큰 이유는 특정 구현에 의존적이지 않은 코드를 만들 수 있다는 점입니다. 그래서 기능의 정의와 구현을 분리할 수 있고 구현 내용을 확장하거나 교체하기 쉽습니다. 프로젝트가 점점 커질수록 확장이 쉬운 구조를 만들고 싶다면 인터페이스를 이용해야 합니다.

 

이상 마치겠습니다. :)