Language/Kotlin

원시 값 포장은 value class를 활용하자!

jiihyunn 2025. 5. 1. 17:21

원시 값 포장이란?

 Int, String 과 같은 원시 타입, 문자열 변수를 변수명을 이용해 의미를 나타내지 않고, 의미있는 객체로 포장한다는 개념을 의미합니다.

 

원시 값 및 문자열을 포장하는 이유

 

코드에서 원시 값과 문자열을 포장하면 다음과 같은 이점을 얻을 수 있습니다.

  • 타입 안전성
  • 명확한 책임 분리

 

타입 안정성

class User(val email: String, val password: String) {
}

fun main() {
	val user1 = User("email", "password") // 의도한 대로 객체 생성
	val user2 = User("password", "email") // 값이 뒤바뀐 객체 생성
}

 

email과 password 모두 String 타입이기 때문에,

email이 들어가야 하는 자리에 password 값이 들어가도 컴파일러는 오류를 띄워주지 않습니다.

컴파일러가 보기에는 올바른 String 타입이 들어갔기 때문입니다.

 

class User(val email: Email, val password: Password) {
}

fun main() {
	val user = User(Email("email"), Password("password"))
}

 

따라서 원시 값을 포장하여 Email, Password 타입을 만든다면,

파라미터가 뒤바뀐 경우에 컴파일 에러를 발생하도록 의도하여 타입 안정성을 챙길 수 있습니다.

 

명확한 책임 분리

class User(val name: String, val password: String) {

    init {
        require(name.length in MIN_RANGE..MAX_RANGE) { NAME_LENGTH_EXCEPTION }
        require(NAME_CONDITION.toRegex().matches(name)) { NAME_CONDITION_EXCEPTION }
        require(password.length >= PASSWORD_MIN_LENGTH) { PASSWORD_LENGTH_EXCEPTION }
    }

    companion object {
        private const val MIN_RANGE: Int = 1
        private const val MAX_RANGE: Int = 5
        private const val NAME_CONDITION: String = "^[a-zA-Z]+$"
        private const val PASSWORD_MIN_LENGTH: Int = 8

        private const val NAME_LENGTH_EXCEPTION: String =
            "[ERROR] 이름의 길이는 $MIN_RANGE에서 $MAX_RANGE사이어야 합니다."
        private const val NAME_CONDITION_EXCEPTION: String = "[ERROR] 이름은 영어로만 이루어져야 합니다."
        private const val PASSWORD_LENGTH_EXCEPTION: String =
            "[ERROR] 비밀번호는 최소 $PASSWORD_MIN_LENGTH자 이상이어야 합니다."
    }
}

 

멤버변수가 2개밖에 없으며, 검증로직만 수행하고 있음에도 불구하고 User 클래스가 할 일이 많게 느껴지지 않나요?

 

그렇다면, 원시값을 포장해 봅시다!

 

Name

@JvmInline
value class Name(val value: String) {

    init {
        require(value.length in MIN_RANGE..MAX_RANGE) { NAME_LENGTH_EXCEPTION }
        require(NAME_CONDITION.toRegex().matches(value)) { NAME_CONDITION_EXCEPTION }
    }

    companion object {
        private const val MIN_RANGE: Int = 1
        private const val MAX_RANGE: Int = 5
        private const val NAME_CONDITION: String = "^[a-zA-Z]+$"

        private const val NAME_LENGTH_EXCEPTION: String =
            "[ERROR] 이름의 길이는 $MIN_RANGE에서 $MAX_RANGE사이어야 합니다."
        private const val NAME_CONDITION_EXCEPTION: String = "[ERROR] 이름은 영어로만 이루어져야 합니다."
    }
}

Password

@JvmInline
value class Password(val value: String) {

    init {
        require(value.length >= MIN_LENGTH) { PASSWORD_LENGTH_EXCEPTION }
    }

    companion object {
        private const val MIN_LENGTH: Int = 8

        private const val PASSWORD_LENGTH_EXCEPTION: String =
            "[ERROR] 비밀번호는 최소 $MIN_LENGTH자 이상이어야 합니다."
    }
}

User

class User(val name: Name, val password: Password) {
}

 

Name과 Password 클래스 각각에서 검증을 처리하므로, User 클래스는 값 검증에 대해 신경 쓰지 않아도 됩니다.

따라서 원시값을 포장하여 자신의 상태를 객체 스스로 관리함으로써 유지보수가 쉬워집니다.

또한 값 검증 로직이 개별 값 객체로 이동하면서 User 클래스의 코드가 단순하고 명확해져 코드 가독성이 증가합니다.

 

value class를  활용한 원시 값 포장

 

정의

인라인 클래스(Inline Classes)는 작고 경량화된 클래스를 만들기 위한 방법으로, 특정 값을 감싸는 클래스지만 런타임 오버헤드를 최소화합니다. 이런 클래스는 값 기반(value-based) 클래스라고도 불립니다.

@JvmInline // 추가!
value class Password(val value: String) // class -> value class로 변경!

 

주의 사항

  1. 단일 프로퍼티만 가질 수 있음
    • 단 하나의 프로퍼티만을 가져야 하며, 이 프로퍼티는 val이어야 합니다.
  2. 런타임 최적화
    • 컴파일된 코드에서는 인라인 클래스 구조가 제거되고 객체 내부의 값만 처리됩니다.
    • 기존 클래스는 객체로 감싸면서 메모리 오버헤드가 발생할 수 있지만,
      value class는 이 최적화로 인해 메모리 할당 및 추가적인 객체 생성이 필요하지 않습니다.
    • 예: Password("abcSecret")라는 인라인 클래스는 런타임 시 단순히 "abcSecret" 문자열로 처리됩니다.
  3. 제약 사항
    • 프로퍼티의 값으로 백킹 필드(프로퍼티의 값을 저장하기 위해 컴파일러가 자동 생성하는 필드)를 가질 수 없습니다.
      • 런타임 시 객체가 존재하지 않고 객체 내부의 값으로 대체되기 때문에, 값을 저장하기 위한 별도의 메모리 공간을 가질 수 없기 때문입니다.
      • 따라서 lateinit이나 delegated 프로퍼티가 사용 불가 합니다.
    • 상속할 수 없으며 인터페이스 구현만 가능합니다.

 

Reference

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