Tiny Star
본문 바로가기
Dev Log/Kotlin

[Kotlin] 클래스의 품격: data class와 enum class 내부 동작 원리부터 실무 활용까지

by 별터 2026. 1. 4.

자바 개발자가 코틀린을 처음 접했을 때 가장 먼저 감탄하는 포인트는 바로 '간결함'입니다. 그 간결함의 최전선에는 데이터를 다루는 두 가지 특별한 클래스, data classenum class가 있습니다.

 

하지만 단순히 "코드가 짧아져서 좋다"라고만 알고 넘어가기에는 이들이 제공하는 기능이 너무나 강력합니다. 오늘은 이 두 클래스가 컴파일 시점에 어떤 코드를 만들어내는지, 그리고 실무에서(특히 우테코 미션에서) 어떻게 활용해야 200% 활용하는 것인지 깊이 있게 파헤쳐 보겠습니다.


1. Data Class: 보일러플레이트의 종결자

 

우리는 보통 데이터를 담는 객체(DTO, VO)를 만들 때 equals, hashCode, toString을 습관적으로 재정의합니다. 코틀린의 data classdata 키워드 하나로 이 모든 것을 대신해줍니다.


1-1. 컴파일러가 대신 만들어주는 것들

 

data class를 선언하면 컴파일러는 내부적으로 다음 함수들을 자동으로 생성합니다.

  1. equals(): 객체의 내용(프로퍼티 값)이 같은지 비교
  2. hashCode(): 객체의 내용을 기반으로 해시코드 생성 (HashMap 등에서 필수)
  3. toString(): "Person(name=Kim, age=25)" 형태의 가독성 좋은 문자열 반환
  4. componentN(): 구조 분해 선언(Destructuring Declaration)을 위한 함수
  5. copy(): 불변 객체의 데이터 복사를 위한 함수

1-2. 구조 분해 선언 (Destructuring Declaration)

 

많은 분들이 data class를 쓰면서도 잘 모르는 기능 중 하나입니다. componentN 함수 덕분에 객체의 프로퍼티를 순서대로 변수에 해체하여 할당할 수 있습니다.

data class LottoNumber(val index: Int, val number: Int)

fun main() {
    val lotto = LottoNumber(1, 42)

    // 객체를 분해해서 변수에 바로 할당
    val (idx, num) = lotto 

    println("순서: $idx, 번호: $num")
}

반복문에서 Map의 키와 값을 다룰 때나, 함수에서 여러 값을 한 번에 반환하고 싶을 때 매우 유용합니다.


1-3. copy()의 진가: 불변성(Immutability) 유지

함수형 프로그래밍과 안전한 멀티스레딩 환경을 위해 객체는 불변(Immutable) 으로 설계하는 것이 좋습니다. val 프로퍼티만 가진 data class는 값을 바꿀 수 없는데, 값 하나만 바꾸고 싶을 때 어떻게 해야 할까요?

 

이때 copy()를 사용합니다.

val p1 = Person("Starter", 25)
// p1은 건드리지 않고, 나이만 변경된 새로운 객체 p2 생성
val p2 = p1.copy(age = 26)

이는 객체의 주소값만 복사하는 얕은 복사가 아니라, 생성자를 통해 새로운 객체를 만드는 방식이므로 사이드 이펙트(Side Effect)로부터 안전합니다.


2. Enum Class: 단순 나열 그 이상, '상태와 행동'의 집합

 

과거 C나 Java 초기의 Enum은 단순히 0, 1, 2 정수값에 이름을 붙인 것에 불과했습니다. 하지만 코틀린의 enum class완전한 클래스입니다. 프로퍼티를 가질 수 있고, 인터페이스를 구현할 수 있으며, 추상 메서드를 가질 수도 있습니다.


2-1. 전략 패턴(Strategy Pattern)의 구현체


Enum 상수가 각각 다른 행동을 해야 한다면 어떻게 할까요? 예를 들어 계산기 연산자를 Enum으로 만든다면요.

enum class Operator(val symbol: String) {
    PLUS("+") {
        override fun apply(x: Int, y: Int) = x + y
    },
    MINUS("-") {
        override fun apply(x: Int, y: Int) = x - y
    }; // 추상 메서드 정의 시 세미콜론 필수

    abstract fun apply(x: Int, y: Int): Int
}

fun main() {
    val result = Operator.PLUS.apply(10, 20) // 30
}

이렇게 추상 메서드를 정의하고 각 상수가 이를 구현하게 하면, if-elseswitch 문 없이도 다형성(Polymorphism)을 이용해 깔끔한 코드를 짤 수 있습니다. 우테코 로또 미션의 LottoResult 같은 경우도 등수별 상금 계산 로직을 Enum 안에 넣으면 응집도가 매우 높아집니다.


2-2. 인터페이스 구현

Enum 클래스도 인터페이스를 구현할 수 있습니다.

interface Scorable {
    fun getScore(): Int
}

enum class Grade : Scorable {
    A { override fun getScore() = 100 },
    B { override fun getScore() = 80 }
}

3. Data Class vs Enum Class: 심층 비교

이 둘을 구분하는 핵심 질문은 "인스턴스의 개수가 정해져 있는가?"입니다.

  • Enum Class (정적 집합)
    • 세상에 태어날 객체의 개수가 이미 코드 작성 시점에 정해져 있습니다. (요일, 카드 무늬, 로또 등수)
    • JVM 상에서 싱글톤(Singleton)으로 존재하므로 `==` 비교가 매우 빠릅니다(참조 비교).
    • 메모리 효율: 객체를 새로 생성하지 않고 재사용합니다.
  • Data Class (동적 데이터)
    • 프로그램 실행 중에 무한히 많은 인스턴스를 찍어낼 수 있습니다. (사용자 정보, 좌표, 로또 번호 조합)
    • `equals()`를 통해 값을 비교합니다.
    • 메모리: 필요할 때마다 `new` (힙 메모리 할당)가 일어납니다.

4. 마무리

  • DTO나 VO처럼 데이터 보관 및 전송이 목적이다 👉 `data class`
  • 로직의 분기 기준이나, 한정된 종류의 타입을 정의한다 👉 `enum class`

이 두 클래스를 적절히 활용하면, 불필요한 코드는 줄어들고 비즈니스 로직의 의도는 명확해집니다. 특히 `enum class`에 로직을 위임하여 '상태를 가지는 객체가 스스로 일하게 하는' 객체지향적인 설계를 연습해 보시길 바랍니다.