본문 바로가기

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

#07-2. 데이터 클래스와 기타 클래스 2021-05-15

07-2 데이터 클래스와 기타 클래스

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

보통 데이터 전달을 위한 객체를 DTO(Data Transfer Object)라고 부릅니다. 자바에서는 POJO(Plain Old Java Object)라고 부르기도 했습니다. DTO는 구현 로직을 가지고 있지 않고 순수한 데이터 객체를 표현하기 때문에 보통 속성과 속성을 접근하고자 하는 게터/세터를 가집니다. 추가적으로 toString(), equals() 등과 같은 데이터를 표현하거나 비교하는 메서드도 가지고 있습니다.

데이터 클래스에서 내부적으로 자동 생성되는 메서드
- 프로퍼티를 위한 게터/세터
- 비교를 위한 equals()와 키 사용을 위한 hashCode()
- 프로퍼티를 문자열로 변환해 순서대로 보여주는 toString()
- 객체 복사를 위한 copy()
- 프로퍼티에 상응하는 component1(), component2() 등

왜 DTO를 사용할까?? 이유는 일종의 표준과 같은 약속을 정하면 전송하거나 받고자 하는 어떤 요소든 데이터를 쉽게 다룰 수 있기 때문입니다. 

데이터 클래스 선언하기

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

데이터 클래스로부터는 open을 사용한 상속, abstract로 선언한 추상 클래스, 내부 클래스를 위한 inner 등을 사용할 수 없고 오로지 데이터 기술 용도로만 사용됩니다. :)

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 {
    	// 간단한 로직 포함 가능
    }
}

 

데이터 클래스가 자동 생성하는 메서드

제공된 메서드 기능
equals() 두 객체의 내용이 같은지 비교하는 연산자(고유 값은 다르지만 의미 값이 같을 때)
hashCode() 객체를 구별하기 위한 고유한 정숫값 생성,
데이터 세트나 해시 테이블을 사용하기 위한 하나의 생성된 인덱스
copy() 빌더 없이 특정 프로퍼티만 변경해서 객체 복사하기
toString() 데이터 객체를 읽기 편한 문자열로 반환하기
componentN() 객체의 선언부 구조를 분해하기 위해 프로퍼티에 상응하는 메서드


equals()는 연산자인 == 표현의 내부 실행 함수, hashCode()는 객체를 구별하기 위한 고유한 정숫값을 생성하고, 두 객체가 동등하다면 같은 정숫값을 생성합니다. :)

val cus1 = Customer("Sean", "sean@email.com")
val cus2 = Customer("Sean", "sean@email.com")
...
println(cus1 == cus2) // 동등성 비교
println(cus1.equals(cus2)) // 위와 동일한 코드
println("${cus1.hashCode()}, ${cus2.hashCode()}")
/*
    결과 :
    true
    true
    -1208413004, -1208413004


copy() 사용하면 데이터 객체를 복사하되 다른 프로퍼티 값을 가지는 것만 명시하여 변경 할 수 있습니다.

val cus3 = cus1.copy(name = "Alice") // name만 변경하고자 할 떄
println(cus1.toString())
println(cus3.toString())
/*
    결과 :
    Customer(name=Sean, email=sean@email.com)
    Customer(name=Alice, email=sean@email.com)
*/

 

객체 디스트럭처링

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

val (name, email) = cus1
println("name = $name, email = $email")
/*
    결과 :
    name = Sean, email = sean@email.com
*/

// 언더스코어(_)를 사용하여 제외
val (_, email2) = cus1 // 첫 번째 프로퍼티 제외

// 개별 프로퍼티 가져오기
val name3 = cus1.component1()
val email3 = cus1.component2()

// 만약 데이터가 많아진다면 반복문을 사용해 작성 
val customers = listOf(cus1, cus2, cus3, cus5,...) // 모든 객체를 컬렉션 List목록으로 구성

for((name, email) in customers) { // 반복문을 사용해 모든 객체의 프로퍼티 분해
	println("name = $name, email = $email")
}

// 함수로부터 객체가 반환될 경우에도 사용 가능
fun myFunc(): Customer {
	reuturn Customer("Mickey", "mic@abc.com")
}
...
val (name, email) = myFunc()

// 람다식을 통한 출력
val myLambda = {
    (nameLa, emailLa): Customer ->
    println(nameLa)
    println(emailLa)
}
myLambda(cus1)

람다식의 경우 객체를 소괄호로 감싸 두었기 떄문에 하나의 객체로 람다식에 전달하고, 여기에 2개의 매개변수가 디스트럭처링된 프로퍼티를 각각 출력합니다.`

fun main() {
    val cus1 = Customer("sean", "sean@email.com")

    fun myLambda(cus: Customer) {
        println(cus.name)
        println(cus.email)
    }

    val myLambda2 = {
        cus: Customer ->
        println(cus.name)
        println(cus.email)
    }

    val myLambda3 = {
        (name, email): Customer ->
        println(name)
        println(email)
    }

    myLambda(cus1)
    myLambda2(cus1)
    myLambda3(cus1)
    
    // myLambda, myLambda2, myLambda3식 모두 같은 결과의 식
}

 

내부 클래스 기법

코틀린은 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 키워드를 통해 선언됨.

 

중첩 클래스

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

package chapter_07.chapter_07_2

class Outer {
    val ov = 5

    class Nested {
        val nv = 10
        fun greeting() = "[Nested] Hello ! $nv" // 외부의 ov에는 접근 불가
        fun t() = println("$test")
    }

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

    companion object {
        const val test = "korea"
    }
}

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

    // Outer.outside() // 오류! 외부 클래스의 경우는 객체를 생성해야 함.
    val outer = Outer()
    outer.outside()
}
/*
    결과 :
    [Nested] Hello ! 10
    [Outer]: [Nested] Hello ! 10, 10

 */

 

이너 클래스

이너(Inner)클래스는 중첩 클래스와 달리 바깥 클래스의 멤버들에 접근 할 수 있습니다. 또한 private 멤버도 접근이 가능합니다.

package chapter_07.chapter_07_2

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


    inner class ExternalStorage(val size: Int) {
        val test = "test"
        fun getInfo(): String = "${model}: Installed on $cpu with ${size}Gb" // 바깥 클래스의 프로퍼티 접근
    }
}

fun main() {
    val mySdcard = Smartphone("S7").ExternalStorage(32)
    println(mySdcard.getInfo())
}
/*
    결과 :
    S7: Installed on Exynos with 32Gb

 */

이너클래스인 ExternalStorage는 Smartphone 클래스의 프로퍼티 model과 cpu에 접근하고 있습니다. 특히 cpu는 private 멤버이지만 접근이 가능한 것을 알 수 있습니다.

 

지역 클래스

package chapter_07.chapter_07_2

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

    inner class ExternalStorage(val size: Int) {
        fun getInfo() = "${model}: Installed on $cpu with ${size}Gb"
    }

    fun powerOn(): String {
        class Led(val color: String) { // 지역 클래스 선언
            fun blink(): String = "Blinking $color on $model" // 외부의 프로퍼티는 접근 가능
        }
        val powerStatus = Led("Red") // 여기서 지역 클래스가 사용됨
        return powerStatus.blink()
    } // powerOn() 블록 끝
}

fun main() {
    val myPhone = Smartphone2("Note9")
    myPhone.ExternalStorage(128)
    println(myPhone.powerOn())
}
/*
    결과 :
    Blinking Red on Note9
 */

여기서 사용된 Led 클래스는 Smartphone 클래스의 메서드인 powerOn()에서만 유효한 클래스입니다. 단, Led 클래스에서 외부의 멤버인 프로퍼티에는 접근할 수 있습니다.

 

익명 객체

자바에서는 익명 이너 클래스라는 것을 제공해 일회성으로 객체를 생성해 사용합니다. 코틀린에서는 object 키워드를 사용하는 익명 객체로 이와 같은 기능을 수행합니다. 

package chapter_07.chapter_07_2

interface Switcher { // 1. 인터페이스 선언
    fun on(): String
}

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

    inner class ExternalStorage(val size: Int) {
        fun getInfo() = "${model}: Installed on $cpu with ${size}Gb"
    }

    fun powerOn(): String {
        class Led(val color: String) {
            fun blink(): String = "Blinking $color on $model"
        }
        val powerStatus = Led("Red")
        val powerSwitcher = object: Switcher { // 2. 익명 객체를 사용해 Switcher의 on()을 구현
            override fun on(): String {
                return powerStatus.blink()
            }
        }
        return powerSwitcher.on()
    }
}
/*
    결과 :
    Blinking Red on Note9
 */

2번에서 object를 사용해 Switcher 인터페이스의 on()메서드를 구현하고 있습니다. 따라서 Switcher인터페이스로부터 만들어진 객체는 이름이 없으며 powerSwitch 프로퍼티를 위해 일회성으로 사용됩니다.

 

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

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

 

실드 클래스

실드 클래스를 선언하려면 sealed 키워드를 class와 함께 사용합니다. 실드 클래스 그 자체는 추상 클래스와 같기 때문에 객체를 만들 수는 없습니다. 또한 생성자도 기본적으로는 private이며 private이 아닌 생성자는 허용하지 않습니다. 실드 클래스는 같은 파일 안에서는 상속이 가능하지만, 다른 파일에서는 상속이 불가능하게 제한됩니다. 블록 안에 선언되는 클래스는 상속이 필요한 경우 open키워드로 선언될 수 있습니다.

package chapter_07.chapter_07_2

// 실드 클래스를 선언하는 첫 번째 방법
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")

// 실드 클래스를 선언하는 두 번쨰 방법
sealed class Result2

open class Success2(val message: String): Result2()
class Error2(val code: Int,val message: String): Result2()

class Status2: Result2()
class Inside2: Success2("Status")

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

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

먼저 Result 클래스는 실드 클래스입니다. 이 클래스 블록에는 Success와 Error클래스가 묶여 있습니다. 그리고 Success 클래스는 상속할 수 있도록 open 키워드를 가지고 있습니다. 이때는 마지막 줄의 Inside 클래스와 같이 상속해서 정의할 수 있습니다. 실드 클래스 자체를 상속할 때는 같은 파일에서만 가능합니다.

실드 클래스는 특정 객체 자료형에 따라 when문과 is에 의해 선택적으로 실행할 수 있습니다. 그리고 모든 경우가 열거되었으므로 else문이 필요 없습니다. 만일 이것을 이너 클래스나 중첩 클래스로 구현하려고 하면 모든 경우의 수를 컴파일러가 판단할 수 없어 else문을 가져야 합니다.

 

열거형 클래스

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

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

각 상수는 Direction 클래스의 객체로 취급되고 쉼표(,)로 구분됩니다.
또 다른 예로 다음과 같이 주 생성자를 가지고 값을 초기화할 수 있습니다. 다음은 각 요일마다 숫자를 지정한 열거형 클래스입니다.

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

각 상수의 값은 매개면수르 통해 초기화될 수 있습니다. 여기서는 num으로 Int형 값을 각각 지정하고 있습니다. when문을 사용해 각 정숫값에 따른 실행이 가능하고, 이때 쉼표를 사용해 여러 케이스를 표현할 수 있습니다.

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

 

인터페이스를 통한 열거형 클래스 구현

package chapter_07.chapter_07_2

interface Score {
    fun getScore(): Int
}


enum class MemberType(var prio: String) : Score { // Score를 구현할 열거형 클래스
    NORMAL("Third") {
        override fun getScore(): Int = 100 // 구현된 메서드
    },
    SILVER("Second") {
        override fun getScore(): Int = 500
    },
    GOLD("First") {
        override fun getScore(): Int = 1000
    }
}

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}")
    }
}
/*
    결과 :
    100
    GOLD
    SILVER
    Second
    grade.name = NORMAL, prio = Third
    grade.name = SILVER, prio = Second
    grade.name = GOLD, prio = First

 */

 

애노테이션 클래스

애노테이션(Annotation)은 코드에 부가 정보를 추가하는 역할을 합니다. @ 기호와 함께 나타내는 표기버븡로 주로 컴파일러나 프로그램 실행 시간에서 사전 처리를 위해 사용합니다.
ex) @Test는 유닛 테스트를 위해 사용, @JvmStatic 자바 코드에서 컴패니언 객체를 접근 가능하게 합니다.
※ 사실상 애노테이션 클래스 제작 기법은 고급 기법이라 만들기 보다는 사용하는 경우가 많습니다 :)

애노테이션 선언

annotation class 애노테이션 이름

annotation class Fancy // 선언
@Fancy class MyClass{...} // 사용 시
- @Target: 애노테이션이 지정되어 사용할 종류(클래스 ,함수, 프로퍼티 등)를 정의
- @Retention: 애노테이션을 컴파일된 클래스 파일에 저장할 것인지 실행 시간에 반영할 것인지 결정
- @Repeatable: 애노테이션을 같은 요소에 여러 번 사용 가능하게 할지를 결정
- @MustBeDocumented: 애노테이션이 API의 일부분으로 문서화하기 위해 사용
// 사용 예
@Target(AnnotationTarget.CLASS ,AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE) // 애노테이션의 처리 방법 - SOURCE: 컴파일 시간에 제거됨
@MustBeDocumented
annotation class Fancy

애노테이션 클래스 정의 위에 애노테이션을 사용하고 있습니다. 여기서 @Retention은 애노테이션의 처리 방법을 기술하고 있습니다. SOURCE를 사용하면 컴파일 시간에 애노테이션이 제거될 것이고, binary를 사용하면 클래스 파일에 포함되지만 리플렉션에 의해 나타나지 않습니다. RUNTIME을 사용하면 애노테이션은 클래스 파일에 저장되고 리플렉션에 의해 나타납니다.

리플렉션
프로그램을 실행할 때 프로그램의 특정 구조를 분석해 내는 기법, 예를 들어 어떤 함수를 정의하는데 함수의 매개변수로 클래스 타입을 선언하고 실행할 때 매개변수로 전달된 클래스의 이름, 클래스의 메서드나 프로퍼티를 알아내는 작업이 리플렉션입니다.
package chapter_07.chapter_07_2

import kotlin.reflect.KClass
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.memberProperties

class User(val id: Int, val name: String, var grade: String = "Normal") {
    fun check() {
        if (grade == "Normal")println("You need to get the Silver grade")
    }
}

fun main() {
    // 타입을 출력
    println(User::class) // 클래스 레퍼런스를 위해 ::class 사용
    val classInfo = User::class
    classInfo.memberProperties.forEach {
        println("Property name: ${it.name}, type: ${it.returnType}")
    }
    classInfo.memberFunctions.forEach {
        println("Function name: ${it.name}, type: ${it.returnType}")
    }

    // 함수에 전달해 자료형을 알아냄
    getKotlinType(User::class)
}

fun getKotlinType(obj: KClass<*>) {
    println(obj.qualifiedName)
}

/*
    결과 :
    class chapter_07.chapter_07_2.User
    Property name: grade, type: kotlin.String
    Property name: id, type: kotlin.Int
    Property name: name, type: kotlin.String
    Function name: check, type: kotlin.Unit
    Function name: equals, type: kotlin.Boolean
    Function name: hashCode, type: kotlin.Int
    Function name: toString, type: kotlin.String
    chapter_07.chapter_07_2.User
 */

 

 

이상 마치겠습니다 :)