코틀린

Do it! 코틀린 프로그래밍, 07-2 데이터 클래스와 기타 클래스

조요피 2021. 8. 1. 21:17

보통 클래스는 속성과 동작을 가지기 때문에 프로퍼티와 메서드를 멤버로 가진다.

하지만 오로지 데이터 저장을 위해서 사용한다면 일반적인 클래스가 가지는 구현부가 필요 없을 수도 있다.

오히려 구현부 때문에 메모리를 더 사용할 수도 있다.

오로지 데이터 저장에 초점을 맞추기 위해 코틀린에서는 데이터(Data) 클래스라는 특별한 클래스를 제공한다.

그 외에도 실드(Sealed), 이너(Inner), 열거형(Enum) 클래스 등을 사용할 수 있다.

데이터 전달을 위한 데이터 클래스

데이터 전달을 위한 객체를 DTO(Data Transfer Object)라고 부른다.

자바에서는 POJO(Plain Old Java Object)라고 부르기도 함.

DTO는 순수한 데이터 객체를 표현하기 때문에 보통 속성과 Getter/Setter를 가진다.

여기에 추가적으로 toString(), equals() 등과 같은 데이터를 표현하거나 비교하는 메서드를 가져야 한다.

자바에서는 이 모든 것을 정의하려면 소스 코드가 아주 길어지게 되지만 코틀린에서는 간략하게 표현할 수 있다.

코틀린에서는 DTO를 위해 데이터 클래스를 정의할 때 Getter/Setter, toString(), equals() 같은 메서드들이 자동으로 생성된다.

코틀린 데이터 클래스에서 내부적으로 자동 생성되는 메서드는 다음과 같다.

  • 프로퍼티를 위한 Getter/Setter
  • 비교를 위한 equals(), 키 사용을 위한 hashCode()
  • 프로퍼티를 문자열로 변환해 순서대로 보여주는 toString()
  • 객체 복사를 위한 copy()
  • 프로퍼티에 상응하는 component1(), component2() 등

왜 DTO를 사용해야 할까?

일종의 표준과 같은 약속을 정하면 전송하거나 받고자 하는 어떤 요소든 데이터를 쉽게 다를 수 있기 때문.

DTO는 데이터를 주고받는 표준 방법이 된다.

데이터 클래스 선언하기

데이터 클래스는 다음 조건을 만족해야 함.

  • 주 생성자는 최소한 하나의 매개변수를 가져야 한다.
  • 주 생성자의 모든 매개 변수는 val, var로 지정된 프로퍼티여야 한다.
  • 데이터 클래스는 abstract, open, sealed, inner 키워드를 사용할 수 없다.

하지만, 부 생성자나 init 블록은 추가 가능하다.

data class Demo(var name : String, val age : Int)

fun main(){
    val d1 = Demo("123", 123)
    val d2 = Demo("123", 123)

    println(d1.equals(d2))
    println(d1 == d2)
    println("${d1.hashCode()}, ${d2.hashCode()}")

    val d3 = d1.copy(name = "234")
    println(d1.toString())
    println(d3.toString())
}

//true
//true
//1509513, 1509513
//Demo(name=123, age=123)
//Demo(name=234, age=123)

객체 디스트럭처링(Destructuring)하기

디스트럭처링 한다는 것은 객체가 가지고 있는 프로퍼티를 개별 변수로 분해하여 할당하는 것.

data class Demo(var name: String, val age: Int)

fun main() {
    val d1 = Demo("123", 123)

    val (name, age) = d1
    println("$name, $age")

    val (_, email) = d1 // 첫 번째 프로퍼티(name) 제외

    val name2 = d1.component1()
    val age2 = d1.component2()
    println("$name2, $age2")

    val d2 = Demo("2", 2)
    val d3 = Demo("3", 3)
    val d4 = Demo("4", 4)
    val d5 = Demo("5", 5)

    val dList = listOf(d2, d3, d4, d5)

    for ((name, age) in dList) {
        println("$name, $age")
    }

    val myLamda = { (name, age): Demo ->
        println(name)
        println(age)
    }

    myLamda(d1)
}

//123, 123
//123, 123
//2, 2
//3, 3
//4, 4
//5, 5
//123
//123

내부 클래스 기법

코틀린은 2가지의 내부 클래스 기법이 있다.

  • 중첩(Nested) 클래스
  • 이너(Inner) 클래스

클래스 내부에 또 다른 클래스를 설계하는 이유

독립적인 클래스로 정의하기 모호한 경우나 다른 클래스에서는 잘 사용하지 않는 클래스가 필요할 때가 있기 때문.

자바 내부 클래스 종류

자바의 내부 클래스는 외부 클래스의 어떤 멤버 필드도 참조할 수 있다.

반대로 외부 클래스도 내부 클래스의 필드에 접근할 수 있다.

종류 역할
정적 클래스(Static Class) static 키워드를 가지며 외부 클래스를 인스턴스화하지 않고 바로 사용 가능한 내부 클래스(주로 빌더 클래스에 이용)
멤버 클래스(Member Class) 인스턴스 클래스로도 불리며 외부 클래스의 필드나 메서드와 연동하는 내부 클래스
지역 클래스(Local Class) 초기화 블록이나 메서드 내의 블록에서만 유효한 클래스
익명 클래스(Anonymous Class) 이름이 없고 주로 일회용 객체를 인스턴스화하면서 오버라이드 메서드를 구현하는 내부 클래스, 가독성이 떨어지는 단점

자바와 코들린의 내부 클래스 비교

정적 클래스와 멤버 클래스가 두 언어에서 반대로 되어 있으니 헷갈리지 말것!

자바 코틀린
정적 클래스(Static Class) 중첩 클래스(Nested Class), 객체 생성 없이 사용 가능
멤버 클래스(Member Class) 이너 클래스(Inner Class), 필드나 메서드와 연동하는 내부 클래스로 inner 키워드가 필요.
지역 클래스(Local Class) 지역 클래스(Local Class), 클래스의 선언이 블록 안에 있는 지역 클래스.
익명 클래스(Anonymous Class) 익명 객체(Anonymous Object), 이름이 없고 주로 일회용 객체를 사용하기 위해 object 키워드를 통해 선언.
// 자바의 멤버(이너) 클래스
class A {
	class B {
    	// 외부 클래스 A의 필드에 접근 가능
    }
}

// 코틀린의 이너 클래스
class A {
	inner class B { // 자바와 달리 inner 키워드 필요
    	// 외부 클래스 A의 필드에 접근 가능
    }
}

// 자바의 정적 클래스
class A {
	static class B { // 정적 클래스를 위해 static 키워드 사용
    }
}

// 정적 클래스처럼 사용한 코틀린의 중첩 클래스
class A {
	class B { // 코틀린에서는 아무 키워드가 없는 클래스는 중첩 클래스이며 정적 클래스처럼 사용
    	// 외부 클래스 A의 프로퍼티, 메서드에 접근할 수 없음
    }
}

중첩 클래스

코틀린에서 중첩 클래스는 기본적으로 정적(static) 클래스처럼 다뤄짐.

중첩 클래스는 객체 생성 없이 접근할 수 있다는 것.

class Demo(){
    val v = 5
    class Nested {
        val n = 10
        fun greeting() = "[nested] hi $n" // v 에는 접근 불가
    }
    fun f() {
        val msg = Nested().greeting() // 중첩 클래스의 메서드 접근
        println("[outer] $msg")
    }
}

fun main() {
    val output = Demo.Nested().greeting()
    println(output)

    // Demo.f() // Error!
    val demo = Demo()
    demo.f()
}

//[nested] hi 10
//[outer] [nested] hi 10

중첩된 클래스는 바로 바깥 클래스의 멤버에는 접근할 수 없다.

바깥 클래스가 컴패니언 객체를 가지고 있다면 접근할 수 있다.

class Demo(){
    class Nested {
        fun f(){
            println(msg)
        }
    }
    companion object {
        val msg = "hi"
    }
}

fun main() {
    Demo.Nested().f()
}

//hi

이너 클래스

클래스 안에 이너 클래스를 정의할 수 있는데 이때 이너 클래스는 바깥 클래스의 멤버들에 접근할 수 있다.

심지어 private 멤버도 접근 가능하다.

class Demo() {
    private val msg = "hi"

    inner class InnerClass {
        fun f() {
            println(msg)
        }
    }
}

fun main() {
    Demo().InnerClass().f()
}

// hi

지역 클래스

특정 메서드의 블록이나 init 블록과 같이 블록 범위에서만 유효한 클래스.

외부의 프로퍼티는 접근 가능하다.

익명 객체

자바에서는 익명 이너 클래스라는 것을 제공해 일회성으로 객체를 생성해 사용한다.

코틀린에서는 object 키워드를 사용하는 익명 객체로 이와 같은 기능을 수행한다.

자바와 다른 점은 익명 객체 기법으로 앞에서 살펴본 다중의 인터페이스를 구현할 수 있다는 것.

interface Iface {
    fun on(): String
}

class Demo() {
    fun f(): String {
        class LocalClass {
            fun lf() = "local class!"
        }

        val face = object : Iface {
            override fun on(): String {
                return LocalClass().lf()
            }
        }
        return face.on()
    }
}

fun main() {
    println(Demo().f())
}

// local class!

실드 클래스와 열거형 클래스

Sealed란 '봉인된'이라는 의미.

실드 클래스는 미리 만들어 놓은 자료형들을 묶어서 제공하기 때문에 어떤 의미에서는 열거형(Enum) 클래스의 확장으로 볼 수 있다.

실드 클래스

sealed 키워드를 사용한다.

실드 클래스는 추상 클래스와 같기 때문에 객체를 만들 수는 없다.

생성자는 private만 허용한다.

같은 파일 안에서는 상속이 가능하지만, 다른 파일에서는 상속이 불가능하다.

블록 안에 선언되는 클래스는 상속이 필요한 경우 open 키워드로 선언될 수 있다.

sealed class Result { // 실드 클래스 선언 첫번째 방법
    open class Success(val message: String) : Result()
    class Error(val code: Int, val message: String) : Result()
}

class State : Result() // 실드 클래스 상속은 같은 파일에서만 가능
class Inside : Result.Success("Status") // 내부 클래스 상속
sealed class Result // 실드 클래스 선언 두번째 방법

open class Success(val message: String) : Result()
class Error(val cdoe: Int, val message: String) : Result()

class Status : Result()
class Inside : Success("Status")

fun main() {
    val result = Success("Good")
    val msg = eval(result)
    println(msg)
}

fun eval(result: Result): String = when (result) {
    is Status -> "in progress"
    is Success -> result.message
    is Error -> result.message
}

// Good

실드 클래스는 특정 객체 자료형에 따라 when문과 is에 의해 선택적으로 실행할 수 있다.

위의 코드에서는 모든 경우가 열거되었으므로 else문이 필요없다.

위의 코드를 이너 클래스나 중첩 클래스로 구현하려고 하면 모든 경우의 수를 컴파일러가 판단할 수 없어 else문을 가져야 한다.

하지만 실드 클래스를 사용하면 필요한 경우의 수를 직접 지정할 수 있다.

열거형 클래스

여러 개의 상수를 선언하고 열거도니 값을 조건에 따라 선택할 수 있는 특수한 클래스.

실드 클래스와 거의 비슷하지만 실드 클래스처럼 다양한 자료형을 다루지 못함.

enum 키워드와 함께 선언할 수 있고 자료형이 동일한 상수를 나열할 수 있다.

enum class Demo {
    NORTH, SOUTH, WEST, EAST
}

enum class Demo2(val num: Int) {
    MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4),
    FRIDAY(5), SATURDAY(6), SUNDAY(7)
}

enum class Demo3(val a:String, val b: String){
    SAY_HI("good", "morning"), SAY_BYE("good", "bye"); // 세미콜론으로 끝을 알림

    fun say(){
        println("$a $b")
    }
}

fun main() {
    println(Demo.EAST)
    println(Demo.SOUTH.ordinal)

    val day = Demo2.MONDAY
    when (day.num) {
        1, 2, 3, 4, 5 -> println("Weekday")
        6, 7 -> println("Weekend")
    }

    val v = Demo3.SAY_BYE
    v.say()
}

//EAST
//1
//Weekday
//good bye

열거형 클래스에서 인터페이스의 메서드를 구현할 수도 있다.

interface Score {
    fun getScore(): Int
}

enum class Demo(var prio: String) : Score {
    SILVER("Seconed") {
        override fun getScore(): Int {
            return 500
        }
    },
    GOLD("First") {
        override fun getScore(): Int {
            return 1500
        }
    }
}

fun main() {
    println(Demo.GOLD.getScore())
    println(Demo.GOLD)
    println(Demo.valueOf("SILVER"))
    println(Demo.SILVER.prio)
    println("========================")
    for (grade in Demo.values()) {
        println("${grade.name} ${grade.prio} ${grade.ordinal}")
    }
}

//1500
//GOLD
//SILVER
//Seconed
//========================
//SILVER Seconed 0
//GOLD First 1

애노테이션 클래스

Annotation은 코드에 부가 정보를 추가하는 역할을 한다.

@ 기호와 함께 나타내는 표기법으로 주로 컴파일러나 프로그램 실행 시간에서 사전 처리를 위해 사용.

애노테이션 클래스를 직접 만드는 것은 고급 기법에 해당하기 때문에 프레임워크를 제작하지 않는 한 사용할 일은 별로 없다. (그렇다면 넘어간다!)

애노테이션의 위치

@Anno class MyClass {
	@Anno fun myMethod(@Anno myProperty: Int) : Int {
    	return (@Anno 1)
    }
}

class Foo @Anno constructor(dependency: MyDependency) { ... }

class Foo {
	var x: MyDependency? = null
    	@Anno set
}

*생성자에 애노테이션을 사용하면 constructor를 생략할 수 없다.

*프로퍼티의 Getter/Setter에서도 사용할 수 있다.