본문 바로가기
Program Language/Kotlin

[Do it! 코틀린 프로그래밍] 7. 다양한 클래스와 인터페이스

by SungJe 2021. 4. 19.

7. 다양한 클래스와 인터페이스


1. 추상 클래스와 인터페이스

추상 클래스(Abstract Class)

  • 선언 등의 대략적인 설계 명세와 공통의 기능을 구현한 클래스다.
  • 추상 클래스를 상속하는 하위 클래스는 내용을 더 구체화해야 한다.
  • 코틀린에서 추상 클래스의 정의는 abstract 키워드를 사용한다.
  • 기본적으로 상속과 오버라이딩을 허용하기 때문에 open 키워드를 사용할 필요가 없다.
/* 추상 클래스 Vehicle 사용 */
abstract class Vehicle(val name: String, val color: String, val weight: Double) {
    // Abstrect Property - 반드시 하위 클래스에서 재정의하여 초기화해야함
    abstract var maxSpeed: Double

    // Property - 초기값인 상태를 저장 가능
    var year = "2018"

    // Abstrect Method - 반드시 하위 클래스에서 구현해야함
    abstract fun start()
    abstract fun stop()

    // Method
    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") // 코드의 구현
}

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

    car.year = "2013"
    car.displaySpecs()
    car.start()

    motor.displaySpecs()
    motor.start()
}

인터페이스(Interface)

  • 추상 메서드나 일반 메서드가 포함된다.
  • 코틀린의 인터페이스는 메서드에 구현 내용을 포함할 수 있다.
  • 프로퍼티는 상태를 저장할 수 없는 추상 프로퍼티로만 선언해야 한다.
  • 다수의 인터페이스를 상속할 수 있어 확장에 용이하다.
  • 인터페이스 변경시 구현 클래스에 영향이 적다.
interface Pet {
    var category: String // 추상 프로퍼티 - abstract 키워드가 없어도 기본 추상 프로퍼티
    val msgTags: String // val 선언 시 게터 구현 가능
        get() = "I'm your lovely pet!"

    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() // [Cat] 클래스의 구현 메서드
    obj.patting() // [Pet] 인터페이스의 일반 메서드
}

인터페이스의 위임: 매개변수에 인터페이스를 위임하여 코드를 더 간략화할 수 있다.

interface Nameable {
    var name: String
}

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

class Work : Runnable {
    override fun run() {
        println("work...")
    }
}

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

fun main() {
    val person = Person(StaffName(), Work()) // 생성자를 사용해 객체를 바로 전달
    println(person.name) // [StafName] 클래스의 name 프로퍼티에 접근
    person.run() // [Work] 클래스의 run() 메서드에 접근
}

2. 데이터 클래스와 기타 클래스

데이터 클래스(Data Class): 코틀린은 데이터 전달을 위한 객체(DTO: DataTransfer Object)를 간략하게 표현할 수 있는 데이터 클레스를 제공한다.

  • 데이터 클래스의 조건
    1. 주 생성자는 최소한 하나의 매개변수를 가져야 한다.
    2. 주 생성자의 모든 매개변수는 val, var로 지정된 프로퍼티여야 한다.
    3. 데이터 클래스는 abstract, open, sealed, inner 키워드를 사용할 수 없다.
  • 제이터 클래스가 자동 생성하는 메서드
    제공된 메서드 기능
    equals() 두 객체의 내용이 같은지 비교하는 연산자
    hashCode() 객체를 구별하기 위한 고유한 정숫값 생성,
    데이터 세트나 해시 테이블을 사용하기 위한 하나의 생성된 인덱스
    copy() 빌더 없이 특정 프로퍼티만 변경하여 객체 복사
    toString() 데이터 객체를 읽기 편한 문자열로 반환
    componentN() 객체의 선언부 구조를 분해하기 위해 프로퍼티에 상응하는 메서드
  • 데이터 클래스 선언
data class Customer(var name: String, var email: String) {
    var job: String = "Unknown"
    constructor(name: String, email: String, _job: String): this(name, email) {
        job = _job
    }
    init {
        // 간단한 로직은 초기화 블럭에서
    }
}

fun main() {
    val cus1 = Customer("Sean", "sean@mail.com")
    val cus2 = Customer("Sean", "sean@mail.com")
    val cus3 = cus1.copy(name = "Alice") // cus1의 name 프로퍼티만 변경

    println(cus1 == cus2) // 동등성 비교
    println(cus1.equals(cus2)) // 위와 동일
    println("${cus1.hashCode()}, ${cus2.hashCode()}"

    println(cus1.toString())
    println(cus3.toString())
}
  • 객체 디스트럭처링(Destructuring): 객체가 가지고 있는 프로퍼티를 개별 변수로 분해하여 할당하는 것을 말하다.
val cus1 = Customer("Sean", "sean@mail.com")
val cus2 = Customer("Sean", "sean@mail.com")
val bob = Customer("Bob", "bob@mail.com")
val erica = Customer("Erica", "erica@mail.com")

val customers = listOf(cus1, cus2, bob, erica) // 모든 객체를 컬렉션 List 목록으로 구성

for ((name, email) in customers) { // 객체의 프로퍼티 분해
    println("name = $name, email = $email")
}

NOTE✏️

  • 데이터 전송 객체(DTO: Data Transfer Object): 프로세스 간에 데이터를 전달하는 객체이다. Link

내부 클래스 기법

  • 중첩 클래스(Nested Class): 코틀린에서 중첩 클래스는 정적(static) 클래스처럼 다뤄져 객체 생성 없이 접근할 수 있다.
  • 내부 클래스(Inner Class): 클래스의 내부에 클래스를 정의하는 것으로 내부 클래스는 외부 클래스의 멤버에 접근할 수 있다.
  • 지역 클래스(Local Class): 특정 메서드의 블록이나 init 블록과 같이 블록 범위에서만 유효한 클래스이다. 블록을 벗어나면 더 이상 사용되지 않는다.
  • 익명 객체(Anonymous Object): 일회성으로 객체를 생성하여 사용할 수 있다. 코틀린의 익명 객체는 다중 인터페이스를 구현할 수 있다.
/* 중첩 클래스 사용하기 */
class Outer {
    val ov = 5

    class Nested {
        val nv = 10
        fun greeting() = "[Nested] Hello! $nv" // 외부 프로퍼티 ov에는 접근 불가
    }

    fun outside() {
        val msg = Nested().greeting() // 객체 생성 없이 중첩 클래스의 메서드에 접근
        println("[Outer]: $msg, ${Nested().nv}") // 중첩 클래스의 프로퍼티에 접근
    }
}

fun main() {
    val output = Outer.Nested().greeting() // static처럼 객체 생성 없이 사용
    println(output)

    // Outer.outside() // 오류! 외부 클래스의 경우 객체를 생성해야함
    val outer = Outer()
    outer.outside()
}
/* 내부 클래스, 지역 클래스, 익명 객체 사용하기 */
interface Switcher {
    fun on(): String
}

class Smartphone(val model: String) {
    private val cpu = "Exynos"

    // Inner Class
    inner class ExternalStorage(val size: Int) {
        // 외부 클래스의 프로퍼티에 접근 - private 프로퍼티도 접근 가능 - private 프로퍼티도 접근 가능
        fun getInfo() = "${model}: Installed on $cpu with ${size}Gb"
    }

    // Method
    fun powerOn(): String {
        // Local Class
        class Led(val color: String) {
            fun blink(): String = "Blinking $color on $model"
        }

        val powerStatus = Led("Red") // 지역 클래스가 사용됨

        // Anonymous Object
        val powerSwitch = object : Switcher { // 익명 객체를 사용하여 Switcher의 on() 메서드 구현
            override fun on(): String {
                return powerStatus.blink()
            }
        }
        return powerSwitch.on() // 익명 객체의 메서드 사용
    }
}

fun main() {
    val mySdcard = Smartphone("S7").ExternalStorage(32)
    println(mySdcard.getInfo())

    val myPhone = Smartphone("Note9")
    println(myPhone.powerOn())
}

열거형 클래스(Enum Class): 여러 개의 상수를 선언하고 열거된 값을 조건에 따라 선택할 수 있는 특수한 클래스다.

/* 구조 */
enum class 클래스 이름[(생성자)] {
    상수1[(값)], 상수2[(값)], 상수3[(값)], ...
    [; 프로퍼티 혹은 매서드]
}

/* 인터페이스를 통한 열거형 클래스 구현 */
interface Score {
    fun getScore(): Int
}

enum class MemberType(var prio: String) : Score {
    NORMAL("Thrid") {
        override fun getScore(): Int = 100
    },
    SILVER("Second") {
        override fun getScore(): Int = 500
    },
    GOLD("First") {
        override fun getScore(): Int = 1500
    }
}

fun main() {
    println(MemberType.NORMAL.getScore())
    println(MemberType.GOLD)
    println(MemberType.valueOf("SILVER"))
    println(MemberType.SILVER.prio)

    for (grade in MemberType.values()) {
        println("grade.name = ${grade.name}, prio = ${grade.prio}")
    }
}

실드 클래스(Sealed Class): 열거형 클래스의 확장으로 볼 수 있다. 같은 파일 안에서는 상속을 허용하지만 다른 파일에서는 상속이 불가능하게 제한한다.

/* 실드 클래스 작성하기 */
sealed class Result {
    open class Success(val message: String) : Result()
    class Error(val code: Int, val message: String) : Result()
}

class Status : Result() // 실드 클래스의 상속은 같은 파일에서만 허용
class Inside : Result.Success("Status") // 내부 클래스 상속

// 상태를 검사하기 위한 함수
fun eval(result: Result): String = when(result) {
    is Status -> "in progress"
    is Result.Success -> result.message
    is Result.Error -> result.message
    // 모든 조건을 가지므로 else문이 필요 없음
}

fun main() {
    val result = Result.Success("Good!") // Success 객체 생성
    val msg = eval(result)
    println(msg)
}

애노테이션 클래스(Annotation Class): 코드에 부가 정보를 추가하는 역할을 하는 애노테이션을 구현하는 클래스다. 자세한 내용은 공식 문서을 참고한다.Link

3. 연산자 오버로딩

  • 연산자의 작동 방식: 연산자를 사용하면 관련된 멤버 메서드를 호출하는 것과 동일하다.
/* + 연산자의 오버로딩 */
class Point(var x: Int = 0, var y: Int = 0) {
    //plus() 함수의 연산자 오버로딩
    operator fun plus(p: Point) = Point(x + p.x, y + p.y)
}

fun main() {
    val p1 = Point(3, -8)
    val p2 = Point(2, 9)

    var point = p1 + p2 // Point 객체의 + 연산
    println("point = (${point.x}, ${point.y})")
}
  • 연산자의 종류(주요 연산자의 종류와 호출 메서드에 대해서 정리)
    표현식 의미
    a + b a.plus(b)
    a - b a.minus(b)
    a * b a.times(b)
    a / b a.div(b)
    a % b a.rem(b)
    a..b a.rangeTo(b)
    대입 연산자(Augmented Assignment): 연산의 겨로가를 할당한다. 만약 +에 대응하는 plus()를 오버로딩 하면 +=는 자동으로 구현된다.
    표현식 의미
    a += b a.plusAssign(b)
    a -= b a.minusAssign(b)
    a *= b a.timesAssign(b)
    a /= b a.divAssign(b)
    a %= b a.remAssign(b)
    단일 연산자(Unary Operator)
    표현식 의미
    +a a.unaryPlus()
    -a a.unaryMinus()
    !a a.not()
    비교 연산자
    표현식 의미
    a > b a.compareTo(b) > 0
    a < b a.compareTo(b) < 0
    a >= b a.compareTo(b) >= 0
    a <= b a.compareTo(b) <= 0
    동등성 연산자: 값과 자료형의 일치(Identity)를 살펴보는 ===와 !==는 오버로딩할 수 없다.
    표현식 의미
    a == b a?.equals(b) ?: (b === null)
    a != b !(a?.equals(b) ?: (b === null))
    범위 연산자
    표현식 의미
    a in b b.contains(a)
    a !in b !b.contains(a)
    인덱스 접근 연산자(Indexed Access Operator): 게터/세터를 다루기 위한 대괄호([]) 연산자를 제공한다.
    표현식 의미
    a[i] a.get(i)
    a[i, j] a.get(i, j)
    a[i_1, ..., i_n] a.get(i_1, ..., i_n)
    a[i] = b a.set(i, b)
    a[i, j] = b a.set(i, j, b)
    a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)
    호출 연산자(Invoke Operator): 특정 객체에 인수를 넣어 처리하기 위한 연산자다.
  • 산술 연산자
/* 호출 연산자 오버로딩 */
class Manager {
    operator fun invoke(value: String) = println(value)
}

fun main() {
    val manager = Manager()
    manager.invoke("Do something for me!")
    manager("Do something for me!") // manager.invoke("...")형태로 호출됨, invoke 생략 가능

    val sum = { x: Int, y: Int -> x + y }
    println(sum.invoke(3, 10)) // 람다식에는 기본적으로 invoke가 정의됨
    println(sum(3, 10))
}