본문 바로가기

프로그래밍 언어 기초/KOTLIN

[Kotlin] 코틀린 클래스 - 데이터 클래스(Data Class), 중첩 클래스(Nested Class), 내부 클래스(Inner Class), Sealed 클래스, Enum

개요

오늘은 코틀린의 클래스에 대해서 알아보도록 하겠습니다.

지금까지 코틀린을 통해 객체 및 객체지향 프로그래밍의 다양한 기술들을 살펴보았습니다.

현재 포스팅을 포함해서 앞으로는 두 편에 걸쳐 코틀린만의 `매력`을 볼 수 있는 다양한 클래스에 대해서 알아보겠습니다.


데이터 클래스, Data Class

코틀린에서는 데이터 클래스(Data Class)라는 것을 사용할 수 있습니다.

데이터 클래스는 클래스를 구성하는 프로퍼티의 getter/setter와 toString()을 기본으로 지원하는 코틀린의 클래스입니다.

이 클래스가 무슨 이점을 갖는지 모르시겠다고요?

아래의 코드를 통해 함께 살펴보도록 하겠습니다.

 

class CommonClass {
    var number = 123
}

data class DataClass(
    var number:Int = 123
)

fun main() {

    val commonClass = CommonClass()
    val dataClass = DataClass()

    println(commonClass) // CommonClass@6e8cf4c6
    println(commonClass.toString())  // CommonClass@6e8cf4c6
    println(dataClass) // DataClass(number=123)
    println(dataClass.toString()) // DataClass(number=123)

}

 

위 코드는 코틀린의 일반 클래스와 데이터 클래스를 각각 설계한 것입니다.

코틀린에서 데이터 클래스(Data Class)는 위 코드처럼 data 키워드를 통해 간편하게 만들 수 있습니다.

 

코틀린의 데이터 클래스에 대해 이야기하기 전,

자바의 클래스(Class)에 대해 잠깐 설명하도록 하겠습니다.

자바에서 클래스(Class)란 자바 메모리 영역 상에서 관리되는 데이터의 모음입니다.

 

이 클래스에 대한 정보는 자바의 메모리 영역 중 힙(heap) 영역에서 관리되고 있습니다.

각 클래스는 개발자가 만든 만큼 해당 클래스의 인스턴스를 생성해낼 수 있는데,

이때 프로그램이 인스턴스들을 살펴보며 어떤 객체인지 구분하려면

그 객체를 구분할 수 있는 고유한 `값`이 필요합니다.

이러한 고유한 `값`이 바로 해당 인스턴스의 `주솟값`이 됩니다.

자바에서는 객체의 인스턴스는 각각 고유한 주솟값을 가지게 되고, 모든 객체는 Object라는 클래스를 상속받습니다.
그런데 이 Object 클래스에 이미 구현된 toString() 메서드는 객체의 `주솟값`을 반환하도록 설계되어 있습니다.

따라서, 자바에서 클래스는 별도로 toString() 메서드를 오버라이딩(overriding)하지 않으면

이 주소 값을 반환하게 되어 출력문에서 주솟값을 출력하게 됩니다.

코틀린에서도 일반적인 클래스들은 이러한 자바의 클래스를 계승합니다.

즉, 코틀린에서도 일반 클래스는 마찬가지로 toString() 메서드를 오버라이딩 하지 않으면

주솟값을 반환하게 되어 위 코드에서 CommonClass는 출력 시 주솟값을 출력하게 됩니다.

자바에서는 이러한 문제점을 해결하기 위해 IDE 등에서 toString() 메서드를 재정의하는 것을

자동으로 빠르게 만들어내는 보일러 플레이트(Boiler Plate) 코드를 활용할 수 있는데요.

즉 레토르트 식품처럼 이미 어느 정도 완성된 코드를 개발자가 사용하는 것입니다.

코틀린에서는 여기서 더 나아가 아예 toString()메서드와 getter/setter까지 구현된 클래스를 사용할 수 있게 되었는데, 이

 클래스가 바로 데이터 클래스(Data Class)입니다.

그리고 이렇게 getter/setter 그리고 toString까지 구현된 클래스는

코틀린뿐만 아니라 자바에서도 프로그램 내에서 다양한 형태의 데이터를 담고 처리하기 좋아

데이터를 담는 컨테이너(Container)로써 유용하게 사용됩니다.

이에 대한 더욱 자세한 설명은 아래의 포스팅을 참조해주세요.

 

https://1-hee.tistory.com/95

 

[CS BASIC] 데이터와 다양한 데이터 객체들(DAO, DTO, VO)

개요 데이터의 중요성 오늘은 프로그래밍에서 사용 가능한 다양한 객체들의 유형에 대해서 알아보도록 하겠습니다. 프로그램에서 필요한 ‘정보’를 다루는 방법은 정말 다양한데요. 단순히

1-hee.tistory.com

 


Sealed Class

코틀린의 클래스는 개발자의 의도에 따라 다양한 관계(Relationship)를 맺을 수 있고,

이러한 관계는 상속(Inheritance)을 통해 실재하는 것이 됩니다.

그러나 무분별한 클래스 간의 관계 형성 및 상속은 초기 클래스 설계 목적을 위배하거나,

의미상 연관이 매우 적은 관계를 형성하여 다른 개발자가 가독성과 직관적 사용이 어렵게 할 수 있습니다.

그러므로 때에 따라서는 이 클래스 간의 상속은 엄격히 제한될 필요가 있습니다.
코틀린에서는 이에 활용할 수 있는 클래스, Sealed Class를 제공합니다.

Sealed class는 특정 클래스를 상속받거나 해당 클래스의 객체를 생성하는 것을 제한하는 Kotlin의 클래스입니다.

이것은 상속을 더 엄격하게 제어하고, 클래스 계층 구조의 안정성을 높이며, 코드의 안정성을 보장하는 데 사용됩니다.

일반적으로 Kotlin의 클래스는 다른 클래스에 상속될 수 있습니다.

그러나 Sealed class로 지정된 클래스는 그 클래스 내부에서만 상속될 수 있습니다.

이는 Sealed class로 지정된 클래스의 하위 클래스가 오직 같은 파일 내에 선언되어야 함을 의미합니다.

Sealed class를 사용하면 클래스 계층 구조의 모든 하위 클래스를 알 수 있으므로,

when 식(when expression)과 같은 조건문에서 모든 하위 클래스에 대한 처리를 지정하는 것이 간단해집니다.

아래의 코드를 살펴보겠습니다.

sealed class State {
    object Idle : State()
    data class Loading(val progress: Int) : State()
    data class Success(val result: String) : State()
    data class Error(val message: String) : State()
}

fun handleState(state: State) {
    when (state) {
        is State.Idle -> println("Idle state")
        is State.Loading -> println("Loading ${state.progress}%")
        is State.Success -> println("Success: ${state.result}")
        is State.Error -> println("Error: ${state.message}")
    }
}



위 코드에서 State 클래스는 sealed로 선언되었으며, 상태를 나타내는 다양한 하위 클래스를 포함합니다.

handleState 함수는 State 객체를 받아들이고 해당 객체의 형식에 따라 다른 작업을 수행합니다.

이때 sealed class를 사용하면 when 식에서 모든 하위 클래스를 다루도록 보장할 수 있습니다.


중첩 클래스(Nested Class)와 내부 클래스(Inner class)

코틀린에서 클래스는 상속을 통해 부모-자식의 관계를 형성할 수 있었습니다.

그런데, 이 자식 클래스의 개수가 많아지게 되면 어떻게 될까요?

물론 코드의 가독성이나 유지보수에의 이점으로 인해 별도로 분리하여 관리할 수도 있겠지만,

특정 클래스에 의미상으로 종속되어 용례가 제한적인 경우라면 

오히려 이렇게 분리하는 것이 코드를 판독하고 클래스 간의 관계를 파악하는 데 걸림돌이 될 수 있습니다.

따라서 이때에는 중첩 클래스(Nested Class)와 내부 클래스(Inner Class)를 사용할 수 있습니다.

코틀린에서 중첩 클래스(Nested Class)는 아래와 같이 사용할 수 있습니다.

 

class Outer {
    private val outerProperty: Int = 10

    class Nested {
        fun nestedMethod() {
            println("Nested method")
        }
    }
}


코틀린에서 내부 클래스(Inner Class)는 아래와 같이 사용할 수 있습니다.

 

class Outer {
    private val outerProperty: Int = 10

    inner class Inner {
        fun innerMethod() {
            println("Inner method accessing outer property: $outerProperty")
        }
    }
}

 

이 두 클래스는 모두 클래스 안에 클래스를 선언할 수 있다는 점에서 공통점을 가집니다.

하지만 아래의 세 가지 부분에서는 서로 차이가 있습니다.

 

1) 외부 클래스의 인스턴스에 대한 접근 여부

  • 내부 클래스(inner class)는 외부 클래스의 인스턴스에 대한 암묵적인 참조를 가지므로, 외부 클래스의 멤버에 직접적으로 접근할 수 있습니다.
  • 내부 클래스는 외부 클래스의 상태에 종속적이면 유용합니다.
  • 중첩 클래스(nested class)는 외부 클래스의 인스턴스에 대한 참조를 가지지 않으므로, 외부 클래스의 멤버에 직접적으로 접근할 수 없습니다.
  • 중첩 클래스는 외부 클래스의 인스턴스와 독립적으로 작동할 때에 유용합니다.

 

2) 정적(static)인 성격의 클래스 필요성

  • 내부 클래스(inner class)는 외부 클래스의 인스턴스에 종속적이므로, 외부 클래스의 상태에 접근하거나 변경해야 할 때 사용됩니다.
  • 중첩 클래스(nested class)는 정적(static)인 성격을 가지므로 외부 클래스의 인스턴스와 독립적으로 작동합니다.
  • 따라서, 외부 클래스의 상태에 종속적이지 않고 단순히 그룹화된 클래스가 필요할 때 중첩 클래스를 사용할 수 있습니다.

 

3) 접근 제한자(private, protected 등)의 활용

  • 내부 클래스(inner class)는 외부 클래스의 private 멤버에도 접근할 수 있습니다.
  • 따라서 외부 클래스의 내부 구현을 숨기고 외부에 노출하지 않고도 내부 구현에 접근해야 할 때 내부 클래스를 사용할 수 있습니다.
  • 중첩 클래스(nested class)는 외부 클래스의 private 멤버에 접근할 수 없으므로, 외부 클래스의 구현을 완전히 독립적으로 유지하면서 그룹화된 클래스를 생성할 수 있습니다.

 

class Outer {
    private val outerProperty: Int = 10
    val publicProperty:Int = 11

    inner class Inner {
        fun innerMethod() {
            println("Inner method accessing outer property: $outerProperty")
            println("publicProperty : $publicProperty")
        }
    }

    class Nested {
        fun nestedMethod() {
            println("Nested method")
        }
    }
}

fun main() {

  val nested = Outer.Nested()
  nested.nestedMethod() // Nested method

  val outer = Outer()
  val innerClass = outer.Inner()
  innerClass.innerMethod() // Inner method accessing outer property: 10
}

 


Enum

코틀린에서 임의의 고정된 값은 해당 데이터를 구분 및 판단하는 데 좋은 기준점이 될 수 있습니다.

여기서 더 나아가 이러한 상수들을 모아 하나로 관리해볼 수도 있는데요,

코틀린에서는 서로 연관된 상수들의 집합을 정의하는 특별한 클래스, Enum 클래스를 제공합니다.

Enum에서 각 상수는 해당 Enum 클래스의 인스턴스이며, 상숫값은 해당 Enum의 멤버로 정의됩니다.

Kotlin의 Enum은 Java의 Enum과 비슷하지만 몇 가지 추가 기능을 제공합니다.

일반적으로 Enum은 switch-case 문이나 when과 함께 사용되어 여러 상황에 따른 다양한 동작을 정의하는 데 유용합니다.

또한 Enum 클래스는 안전한 타입으로 사용되므로 잘못된 값이 들어가는 것을 방지할 수 있습니다.

코틀린에서 Enum 클래스는 아래와 같이 사용할 수 있습니다.

 

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

fun main() {

  val direction = Direction.NORTH

    when (direction) {
        Direction.NORTH -> println("Go North")
        Direction.SOUTH -> println("Go South")
        Direction.EAST -> println("Go East")
        Direction.WEST -> println("Go West")
    }


    println(direction.name); // NORTH
    println(direction.ordinal); // 0

    println(Direction.valueOf("NORTH")) // NORTH
    println(Direction.values());  // [LDirection;@4769b07b

}


이 코드에서는 Direction이라는 Enum 클래스를 정의하고,

그 안에 NORTH, SOUTH, EAST, WEST라는 상수들을 정의했습니다.

when 식을 사용하여 direction 변수의 값에 따라 다른 동작을 수행하도록 처리할 수 있습니다.

또한 Enum 클래스는 다음과 같은 속성과 메서드를 가질 수 있습니다.

  • name: Enum 상수의 이름을 반환합니다.
  • ordinal: Enum 상수의 순서(0부터 시작)를 반환합니다.
  • values(): Enum 클래스에 정의된 모든 상수의 배열을 반환합니다.
  • valueOf(String): 지정된 이름과 일치하는 Enum 상수를 반환합니다. 해당하는 상수가 없으면 IllegalArgumentException이 발생합니다.

위의 Enum 코드에서도 마찬가지로 name, ordinal, values() 등의 속성과 메서드를 활용할 수 있습니다.



참고 자료

https://kotlinlang.org/docs/data-classes.html

 

Data classes | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/sealed-classes.html

 

Sealed classes and interfaces | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/nested-classes.html

 

Nested and inner classes | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/enum-classes.html

 

Enum classes | Kotlin

 

kotlinlang.org