본문 바로가기

Kotlin/Play Kotlin Example

Kotlin 공식 Example로 공부하기 - Special Classes(data class, enum class, sealed class, object, companion object)

Special Classes

  • 저는 전문 번역가도 아니고, 의역을 넘어 오역, 심지어 그냥 제가 읽고 싶은대로 읽은 내용이 있을 수 있습니다.
  • 개인 공부를 한 것을 포스트로 남기고 있으며 틀린 부분이 있으면 지적해주시면 수정하도록 하겠습니다.
  • 원문 : https://play.kotlinlang.org/byExample/03_special_classes/01_Data classes

Data Classes (데이터 클래스)

Data Class를 사용하면 값을 저장하는 클래스(ex. DTO, VO)를 쉽게 만들 수 있습니다.

Data Class는 자동으로 메소드들을 제공합니다.

  • 주 생성자에 선언된 모든 프로퍼티를 기준으로 컴파일러가 자동으로 생성해줍니다.
    • equals() / hashCode() pair
    • toString()
    • copy()
    • componentN() function 프로퍼티 선언순서로 값을 가져오는 함수 (Destructuring Declarations에서 다룸)
data class User(val name: String, val id: Int)             // 1

fun main() {
    val user = User("Alex", 1)
    println(user)                                          // 2 User(name=Alex, id=1)

    val secondUser = User("Alex", 1)
    val thirdUser = User("Max", 2)

    println("user == secondUser: ${user == secondUser}")   // 3
    println("user == thirdUser: ${user == thirdUser}")

    println(user.hashCode())                               // 4
    println(thirdUser.hashCode())

    // copy() function
    println(user.copy())                                   // 5
    println(user.copy("Max"))                              // 6
    println(user.copy(id = 2))                             // 7

    println("name = ${user.component1()}")                 // 8
    println("id = ${user.component2()}")
}
  1. data 키워드를 이용해서 데이터 클래스를 정의합니다.
  2. toString 메소드가 자동으로 생성되어 println 으로 멋지게 출력하도록 합니다.
  3. 자동 생성된 equals 메소드는 두 인스턴스의 모든 프로퍼티가 같으면 두 인스턴스는 같다고 판단합니다.
  4. 같은 데이터 클래스 인스턴스는 같은 hashCode() 값을 갖습니다. (user.hashCode()=secondUser.hashCode())
  5. 자동 생성된 copy() 메소드로 새로운 인스턴스를 쉽게 만들 수 있습니다.
  6. 복사할 때, 특정 속성을 변경할 수 있습니다. copy 는 클래스의 생성자와 같은 순서로 매개변수를 적용합니다. (name:String, id:Int 이므로 user.copy("Max") 했을 때, name:String이 생성자에서 제일 앞에 있으므로 name이 "Max"로 변경된 것을 확인할 수 있습니다.)
  7. 지정 인자(named arguments)와 함께 copy 를 사용하여 생성자의 매개 변수 순서와 상관없이 속성 값을 변경할 수 있습니다.
  8. 자동 생성된 componentN 함수로 프로퍼티 값을 불러올 수 있습니다.(N은 클래스에 선언된 프로퍼티 순서)

Enum Classes (열거형 클래스)

Enum 클래스는 고유한 값의 집합을 나타내는 타입을 모델링하는데 사용됩니다. (ex. States, Modes, Directions, ...)

enum class State {
    IDLE, RUNNING, FINISHED                           // 1
}

fun main() {
    val state = State.RUNNING                         // 2
    val message = when (state) {                      // 3
        State.IDLE -> "It's idle"
        State.RUNNING -> "It's running"
        State.FINISHED -> "It's finished"
    }
    println(message)
}
  1. 3가지 열거형 인스턴스를 갖는 단순한 enum 클래스를 정의합니다. 열거형 인스턴스의 수는 항상 유한해야하고 모두가 구분되어야 합니다.
  2. 클래스 이름으로 열거형 인스턴스에 접근합니다.
  3. Enum을 사용할 때, 만약 when expression이 출중하다면(enum의 모든 경우의 수를 다 사용했다면) else 케이스가 필요하지 않다고 추론할 수 있습니다.

열거형은 다른 클래스처럼 프로퍼티와 메소드를 가질 수 있습니다. 세미콜론으로 열거형 인스턴스와 구분

enum class Color(val rgb: Int) {                      // 1
    RED(0xFF0000),                                    // 2
    GREEN(0x00FF00),
    BLUE(0x0000FF),
    YELLOW(0xFFFF00);

    fun containsRed() = (this.rgb and 0xFF0000 != 0)  // 3
}

fun main() {
    val red = Color.RED
    println(red)                                      // 4
    println(red.containsRed())                        // 5
    println(Color.BLUE.containsRed())                 // 6
}
  1. 프로퍼티와 메소드를 갖는 열거형 클래스를 정의합니다.
  2. 각 인스턴스는 생성자의 매개 변수에 대한 인수 전달을 해야합니다.
  3. 열거형 클래스의 멤버 변수는 인스턴스 정의로부터 세미콜론에 의해 구분됩니다.
  4. 디폴트(default) toString 메소드는 인스턴스의 이름을 리턴합니다. (여기서는 "RED")
  5. 열거형 인스턴스의 메소드를 호출합니다.
  6. 열거형 클래스 이름을 통해 메소드를 호출합니다.

Sealed Classes

Sealed class를 사용하면 상속의 사용을 제한할 수 있습니다.

sealed class를 선언하면, sealed class가 선언된 동일한 파일 내에서만 서브 클래스를 만들 수 있습니다.

sealed class가 선언된 파일 외부에서 서브클래스를 생성할 수 없습니다.

✔ 내용이 좀 빈약해서 공식 문서의 Sealed Class 부분 첨부합니다.

sealed class Mammal(val name: String)                                                   // 1

class Cat(val catName: String) : Mammal(catName)                                        // 2
class Human(val humanName: String, val job: String) : Mammal(humanName)

fun greetMammal(mammal: Mammal): String {
    when (mammal) {                                                                     // 3
        is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"    // 4
        is Cat -> return "Hello ${mammal.name}"                                         // 5     
    }                                                                                   // 6
}

fun main() {
    println(greetMammal(Cat("Snowy")))
}
  1. sealed 클래스를 정의합니다. (sealed 키워드를 붙입니다.)
  2. 서브클래스를 정의합니다. 모든 서브클래스는 반드시 sealed 클래스가 정의된 파일에 있어야 합니다.
  3. when expression의 인자(argument)로 sealed class의 인스턴스를 사용합니다.
  4. 스마트 캐스트가 이뤄져 MammalHuman 으로 캐스팅됩니다.
  5. 스마트 캐스트가 이뤄져 MammalCat 으로 캐스팅됩니다.
  6. else 케이스는 필요하지 않습니다. sealed class의 가능한 모든 서브 클래스가 when 케이스에 포함되어 있으므로 else 케이스는 필요하지 않습니다. non-sealed 슈퍼클래스를 쓴다면 else 케이스는 필요합니다.

Object Keyword

코틀린에서 클래스와 객체는 대부분의 객체 지향 언어와 동일한 방식으로 동작합니다.

(클래스는 청사진(붕어빵틀), 객체는 클래스의 인스턴스(붕어빵))

보통 클래스를 정의하고 그 클래스의 인스턴스를 여러 개 생성합니다.

import java.util.Random

class LuckDispatcher {                    //클래스(붕어빵틀) 정의
    fun getNumber() {                     //메소드 정의
        var objRandom = Random()
        println(objRandom.nextInt(90))
    }
}

fun main() {
    val d1 = LuckDispatcher()             //인스턴스 생성
    val d2 = LuckDispatcher()

    d1.getNumber()                        //인스턴스의 메소드 호출
    d2.getNumber()
}

코틀린에는 object 키워드도 있습니다.

이 키워드는 단일 구현으로 데이터 타입을 얻는데 사용됩니다.

자바개발자라면 단일 구현의 의미를 싱글톤(Singleton)이라고 생각하면 됩니다.

싱글톤은 두 개의 스레드가 동시에 클래스의 인스턴스를 생성하려고 하더라도 오직 한 개의 인스턴스만 생성되는 것을 보장하는 것을 말합니다.

코틀린에서 이를 달성하기 위해서는 간단하게 object 키워드로 선언하면 됩니다.

object 키워드로 생성한 인스턴스에는 클래스도 없고 생성자도 없습니다 오직 게으른 인스턴스(lazy instance)입니다.

왜 게으르다(lazy) 할까요? 왜냐하면 객체에 접근할 때 한 번 생성되기 때문입니다. 반대로, 접근하기 전까지는 생성되지 않습니다.

Object Expression

다음은 **object expression**의 기본 사용법입니다.

클래스 선언에서 사용할 필요가 없습니다. 단일 객체를 생성하고 멤버를 선언한 후 하나의 함수 내에서 접근합니다.

이와 같은 객체는 자바에서 종종 익명 클래스의 인스턴스 생성됩니다.

fun rentPrice(standardDays: Int, festivityDays: Int, specialDays: Int): Unit {  //1

    val dayRates = object {                                                     //2
        var standard: Int = 30 * standardDays
        var festivity: Int = 50 * festivityDays
        var special: Int = 100 * specialDays
    }

    val total = dayRates.standard + dayRates.festivity + dayRates.special       //3

    print("Total price: $$total")                                               //4

}

fun main() {
    rentPrice(10, 2, 1)                                                         //5
}
  1. 파라미터를 갖는 함수를 생성합니다.
  2. 결과 값을 계산할 때 사용할 객체를 생성합니다.
  3. 객체의 프로퍼티에 접근합니다.
  4. 결과를 출력합니다.
  5. 함수를 호출합니다. 이 때가 실제로 객체가 만들어질 때 입니다.

Object Declaration

object 선언으로도 사용할 수 있습니다. 이것은 expression 이 아니고 변수 할당에도 사용할 수 없습니다.

object 선언된 객체에 직접 접근하여 사용해야합니다.

object DoAuth {                                                 //object 선언 
    fun takeParams(username: String, password: String){         //object 메소드 정의
        println("input Auth parameters = $username:$password")
    }
}

fun main(){
    DoAuth.takeParams("foo", "qwerty") //메소드 호출, 이 때 실질적으로 객체가 생성됩니다.
}

Companion Objects

클래스 안에 있는 object 선언은 다른 유용한 사례를 정의합니다 : 동반자 객체(companion object)

문법적으로 자바의 static 메소드와 유사하다. 클래스 이름을 한정자(Qualifier)로 사용하여 객체 멤버를 호출합니다.

만약 코틀린에서 companion object를 사용할 계획이라면, package-level 함수 대신에 사용할 것을 고려해봐야합니다.

class BigBen {                                  //클래스 정의
    companion object Bonger {                   //companion object 정의, 이름은 생략 가능
        fun getBongs(nTimes: Int) {             //companion object 메소드 정의
            for (i in 1 .. nTimes) {
                print("BONG ")
            }
        }
    }
}

fun main() {
    BigBen.getBongs(12)                         //클래스 이름을 통한 companion object 메소드 호출
}