data class랑 class랑 뭐가 그렇게 다른데?

2025. 10. 31. 17:38·외부 활동/우아한테크코스 8기

안녕하세요! 최근 우테코 프리코스 과정을 진행하면서 굉장히 많은걸 배우고 있어요.

이번 2주차 자동차 경주 미션을 진행하면서 다른 분들의 코드를 리뷰할 기회가 있었는데, 한 가지 흥미로운 점을 발견했어요. 바로 핵심 도메인 객체인 Car를 설계하는 방식이 크게 두 가지로 나눠지는 점이에요.

  1. data class를 사용해 불변(immutable) 객체로 구현한 방식
  2. 일반 class에서 내부 상태를 직접 변경하는 가변(mutable) 객체로 구현한 방식 (public var 또는 백킹 프로퍼티 활용)

저는 테스트 용이성과 코드의 안정성을 높이기 위해 data class를 선택했는데요, 이 주제로 동료들과 토론을 나누다 보니 문득 이런 궁금증이 생겼어요.

"그래서, 정말 성능 차이가 얼마나 날까?"

마침 이번 미션의 핵심 목표 중 하나가 '테스트 도구를 사용하는 방법'을 배우는 것이었기에, 이 궁금증을 직접 벤치마크 테스트를 통해 풀어보기로 했습니다.

두 방식은 이렇게 달랐어요

성능을 비교하기 위해, 자동차의 위치(position)를 업데이트하는 두 가지 방식의 Car 객체를 준비했어요.

1. 불변 객체

data class로 Car를 정의하고, move()가 호출될 때마다 position이 1 증가한 새로운 Car 객체를 copy()를 통해 반환하는 방식이에요.

private data class ImmutableCar(val name: String, val position: Int = 0) {
    fun move(): ImmutableCar = this.copy(position = position + 1)
}

2. 가변 객체

일반 class로 Car를 정의하고, position을 var 프로퍼티로 가져요. move() 메소드는 내부 position의 값을 직접 1 증가시키는 방식입니다.

private class MutableCar(val name: String, var position: Int = 0) {
    fun move() {
        this.position += 1
    }
}

그래서 직접 테스트 해보자

두 방식의 성능 차이를 명확히 확인하기 위해, 10,000대의 자동차가 10,000번 움직이는 상황을 시뮬레이션했어요. 즉, 총 1억 번의 move 연산이 발생하는 시나리오입니다 🤔

  • 불변 객체 테스트: 매 라운드마다 map 연산을 통해 새로운 자동차 리스트를 생성
  • 가변 객체 테스트: forEach로 각 자동차 객체의 move() 메소드를 호출하여 내부 상태를 변경

아래는 벤치마크에 사용된 전체 테스트 코드예요.

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.system.measureTimeMillis

class PerformanceTest {

    private val carCount = 10000
    private val roundCount = 10000

    // 1. 불변 객체 (data class)
    private data class ImmutableCar(val name: String, val position: Int = 0) {
        fun move(): ImmutableCar = this.copy(position = position + 1)
    }

    // 2. 가변 객체 (class)
    private class MutableCar(val name: String, var position: Int = 0) {
        fun move() {
            this.position += 1
        }
    }

    @Test
    @DisplayName("data class와 class의 객체 업데이트 성능 비교")
    fun `comparePerformance_ImmutableVsMutable`() {
        // 불변 객체 테스트
        var immutableCars = (1..carCount).map { ImmutableCar("car$it") }
        val immutableTime = measureTimeMillis {
            repeat(roundCount) {
                immutableCars = immutableCars.map { it.move() }
            }
        }
        println("--- 성능 테스트 결과 ---")
        println("Immutable (data class): $immutableTime ms")

        // 가변 객체 테스트
        val mutableCars = (1..carCount).map { MutableCar("car$it") }
        val mutableTime = measureTimeMillis {
            repeat(roundCount) {
                mutableCars.forEach { it.move() }
            }
        }
        println("Mutable (class): $mutableTime ms")
        println("----------------------")
    }
}

테스트 결과는?

결과는 예상대로였지만, 수치는 생각보다 더 극적이었어요. 가변 객체를 사용한 방식이 훨씬 빨랐습니다.

  • Immutable (data class): 662 ms
  • Mutable (class): 274 ms

내부 상태를 직접 변경하는 방식이 새로운 객체를 계속 생성하는 방식보다 약 2.4배 더 빠른 성능을 보여주었어요. 이 결과는 객체 생성 및 가비지 컬렉션(GC) 오버헤드가 실제로 성능에 유의미한 영향을 미친다는 것을 명확하게 보여줘요.

그렇다면 data class는 항상 나쁜 선택일까요?

저는 그렇지 않다고 생각해요. 이 벤치마크는 성능이라는 극단적 환경에서 검증한 내용이기 때문인데요!

저는 오히려 불변성이 주는 이점에 관점을 맞췄어요.

먼저 객체의 상태가 변하지 않으므로, 여러 곳에서 객체를 참조하더라도 Side Effect 걱정 없이 안전하게 사용할 수 있어요.

상태 변경이 항상 새로운 객체 생성을 통해 이루어지므로, 데이터의 흐름을 추적하기 쉽고 버그가 발생했을 때 원인을 찾기 용이해요.

결국 테스트 시 객체의 내부 상태를 확인할 필요 없이, "입력에 따라 기대하는 출력이 나왔는가"만 검증하면 되므로 테스트 코드가 매우 명확하고 간결해져요.

정답은 없지만, 현명한 선택은 있어요

"섣부른 최적화는 모든 악의 근원이다." - 도널드 커누스

class가 더 빠르다는 결과가 나왔지만, 이번 자동차 경주 미션의 요구사항(테스트 용이성, 안정성)과 규모를 고려했을 때 저는 다시 설계를 하더라도 data class를 사용할거에요.

대부분의 애플리케이션에서는 data class가 제공하는 안정성과 테스트 용이성의 이점이 약간의 성능 비용을 충분히 상쇄하고도 남는다고 생각해요.

반면, 게임 엔진, 대용량 데이터 실시간 처리 등 성능이 극도로 중요한 일부 로직에서는 객체 생성 오버헤드를 줄이기 위해 가변 class를 사용하는 것이 합리적인 선택일 수 있어요.

결국 정답은 없다고 생각해요😅 주어진 문제의 성격과 요구사항, 시스템의 제약 조건을 종합적으로 고려하여 상황에 맞는 최적의 도구를 선택하는 것. 그것이 바로 좋은 설계면서 생각하는 개발자가 되는 능력이라고 생각합니다 👋🏻

'외부 활동 > 우아한테크코스 8기' 카테고리의 다른 글

우아한테크코스 2주차 회고 '의미 있는 도전을 해보자'  (0) 2025.10.28
'외부 활동/우아한테크코스 8기' 카테고리의 다른 글
  • 우아한테크코스 2주차 회고 '의미 있는 도전을 해보자'
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    sideeffect
    backing property
    custom plugin
    AboutLibraries
    OssLicensePlugin
    Google Recommend Architecture
    API35
    우테코
    build-logic
    rememberupdatedstate
    jetpack
    Baseline Profile
    coroutine
    LaunchedEffect
    Android
    derivedstateof
    Multi-module
    compose
    runcatching
    producestate
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
data class랑 class랑 뭐가 그렇게 다른데?
상단으로

티스토리툴바