비즈니스 로직을 작성하다보면 어떤 타입의 Wrapper를 작성할 때가 있다. 예를 들어, userName을 표현할 때 단순히 String으로 나타낼 수 있지만 조금 더 도메인적인 의미를 나타내주고 싶을 때는 UserName 타입으로 표현할 수 있다.
// primitive type만 사용할 경우
data class person(
val userName: String,
val password: String,
)
// 도메인에 특화된 타입을 만들어서 사용하는 경우
data class person(
val userName: UserName,
val password: Password,
)
data class UserName(val value: String)
data class Password(val value: String)
그러나 이러한 Wrapper는 추가적인 Heap 영역에 할당되므로 런타임 오버헤드가 발생한다. 특히 Wrapping의 대상이 되는 타입이 Primitive 타입이라면 런타임 성능에 더 악영향을 미치는데, Primitive 타입은 보통 런타임에 굉장히 최적화되어 있는 반면, Primitive 타입을 Wrapping 하는 순간 더이상 그 최적화가 의미 없어지게 되기 때문이다.
이러한 문제를 해결하기 코틀린에서는 inline class
를 제공한다. inline class는 생성자로 단 하나의 값만 받을 수 있다. 물론 클래스 내에 프로퍼티와 함수를 정의할 수도 있다.
inline class UserName(val value: String)
inline class Password(val value: String) {
fun isValid() = value.isNotEmpty()
}
inline class 객체는 런타임에 wrapper로서 표현될 수도 있고, wrapping 되기 전의 타입으로 표현될 수도 있다. 마치 Int 클래스가 Primitive type인 int로 표현되기도 하고 Wrapper인 Integer 클래스로 표현되기도 하는 것과 같다.
코틀린 컴파일러는 성능상의 이유로 wrapper보다는 wrapping되기 전의 타입을 더 선호한다. 그러나 때로는 wrapper를 유지해야 할 때가 있기 마련이다. 보통 inline class를 선언해놓되 다른 타입으로 사용될 때 Wrapper로 유지되곤 한다.
코틀린 1.4.x 버전까지는 inline class로 사용되었지만, 1.5.x버전부터는 value class로 변경되었다. 이유는 koltin의 inline function이 별도로 존재하기 때문이다. 키워드는 동일하지만, 내부 컨셉이 달라서 변경되었다고 한다.
kotlin의 value class는 런타임에 해당 객체를 사용하는 객체에 property로 변환되는 클래스이다. 따라서 연관있는 클래스에 대해서 응집도를 높일 수 있고, 메모리, 그리고 성능적으로 효율을 가져갈 수 있는 방법이다.
ex)
@Entity
class User(
@Id
val email: Email,
val hashedPassword: String,
val displayName: String,
val createdAt: Instant
) {
@JvmInline
value class Email(val value: String) {
init {
if (!Pattern.matches("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$", value)) {
throw IllegalArgumentException("not match regex and email")
}
}
fun getId(): String {
return value.split("@")[0]
}
fun getHost(): String {
return value.split("@")[1]
}
}
}
컴파일시 어떤 형태인지 확인해보자. (bytecode하여 디컴파일해서 확인해보았다.) value class인 email 필드의 내부 필드인 value가 그대로 User의 필드로 내려온 것을 알 수 있다.
public class User {
@Id
@NotNull
private final String email;
@NotNull
private final String hashedPassword;
@NotNull
private final String displayName;
@NotNull
private final Instant createdAt;
...
📌 주의사항
value 클래스를 사용할 때도 일반 클래스를 사용하는 것과 완전 동일하게 사용할 수는 없다. 몇가지 주의사항을 확인해보자.
- backing field를 가질 수 없다.
- lateinit 필드를 가질 수 없다.
- interface는 사용 가능하나 상속은 불가능하다.