3장 - 자바 클래스에서 코틀린 클래스로
3장 - 자바 클래스에서 코틀린 클래스로를 정리한 내용입니다.
간단한 값 타입
EmailAddress 클래스는 값 타입(value type)이다.
package travelator;
import java.util.Objects;
public class EmailAddress {
private final String localPart; // <1>
private final String domain;
public static EmailAddress parse(String value) { // <2>
var atIndex = value.lastIndexOf('@');
if (atIndex < 1 || atIndex == value.length() - 1)
throw new IllegalArgumentException(
"EmailAddress must be two parts separated by @"
);
return new EmailAddress(
value.substring(0, atIndex),
value.substring(atIndex + 1)
);
}
public EmailAddress(String localPart, String domain) { // <3>
this.localPart = localPart;
this.domain = domain;
}
public String getLocalPart() { // <4>
return localPart;
}
public String getDomain() { // <4>
return domain;
}
@Override
public boolean equals(Object o) { // <5>
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EmailAddress that = (EmailAddress) o;
return localPart.equals(that.localPart) &&
domain.equals(that.domain);
}
@Override
public int hashCode() { // <5>
return Objects.hash(localPart, domain);
}
@Override
public String toString() { // <6>
return localPart + "@" + domain;
}
}
(1) final 필드로 선언된 값은 불변이다.
(2) 문자열을 파싱한 후 주 생성자를 호출해 객체를 생성하는 parse 정적 팩터리 함수가 있다.
(3) 클래스 내부 필드는 생성자에서 초기화된다.
(4) 프로퍼티를 구성하는 접근자 함수는 자바빈의 명명 규칙을 따른다.
(5) equals, hashCode 함수를 구현해서 두 객체의 필드값이 동일하다면 그 객체들은 동일한 객체이다.
(6) toString은 내부 필드값을 가지고 표준 이메일 주소 형식으로 변환한다.
인텔리J에 'Convert Java File to Kotlin File' 기능으로 EmailAddress.kt로 만들수 있다.
package travelator
import java.util.*
class EmailAddress(val localPart: String, val domain: String) {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val that = o as EmailAddress
return localPart == that.localPart && domain == that.domain
}
override fun hashCode(): Int {
return Objects.hash(localPart, domain)
}
override fun toString(): String {
return "$localPart@$domain"
}
companion object {
@JvmStatic
fun parse(value: String): EmailAddress {
val atIndex = value.lastIndexOf('@')
require(!(atIndex < 1 || atIndex == value.length - 1)) {
"EmailAddress must be two parts separated by @"
}
return EmailAddress(
value.substring(0, atIndex),
value.substring(atIndex + 1)
)
}
}
}
주 생성자 안에 val을 붙여 프로퍼티를 선언하는 형태로 클래스를 정의할 수 있다. (옆 탭은 동일한 자바 코드)
class EmailAddress(val localPart: String, val domain: String)
...
private final String localPart;
private final String domain;
public EmailAddress(String localPart, String domain) {
this.localPart = localPart;
this.domain = domain;
}
public String getLocalPart() {
return localPart;
}
public String getDomain() {
return domain;
}
...
파라미터를 한 줄에 하나씩 배치해 가독성을 높일 수 있다.
package travelator
import java.util.*
class EmailAddress(
val localPart: String,
val domain: String
) {
...
companion object {
@JvmStatic
fun parse(value: String): EmailAddress {
val atIndex = value.lastIndexOf('@')
require(!(atIndex < 1 || atIndex == value.length - 1)) {
"EmailAddress must be two parts separated by @"
}
return EmailAddress(
value.substring(0, atIndex),
value.substring(atIndex + 1)
)
}
}
}
- companion object는 최상위 함수로 리팩터링 할 수 있다.
- @JvmStatic는 기존 자바 코드에서 변경없이 코틀린 코드를 호출할 수 있게 해준다.
코틀린의 생성자에 val로 선언된 프로퍼티는 getDomain() 게터를 제공해준다.
package travelator;
public class Marketing {
public static boolean isHotmailAddress(EmailAddress address) {
return address.getDomain().equalsIgnoreCase("hotmail.com");
}
}
자바의 EmailAddress 코드는 전형적인 값 타입 클래스로 보일러플레이트 코드가 많다.
코틀린은 data 변경자를 붙여 값 타입의 데이터 클래스를 지원한다.
package travelator
data class EmailAddress(
val localPart: String,
val domain: String
) {
override fun toString(): String { // <1>
return "$localPart@$domain"
}
companion object {
@JvmStatic
fun parse(value: String): EmailAddress {
val atIndex = value.lastIndexOf('@')
require(!(atIndex < 1 || atIndex == value.length - 1)) {
"EmailAddress must be two parts separated by @"
}
return EmailAddress(
value.substring(0, atIndex),
value.substring(atIndex + 1)
)
}
}
}
(1)
데이터 클래스는 equals, hashCode, toString을 모두 자동생성 해준다.
toString은 원하는 형태로 만들어주기 위해 오버라이딩 했다.
데이터 클래스의 한계
equals, hashCode, toString 함수를 생성해준다.
프로퍼티 일부를 다른 값을 대치할 수 있는 copy 함수도 제공한다.
val postmasterEmail = customerEmail.copy(localPart = "postmaster")
돈이라는 개념을 추상화 해놓은 타입인 Money 클래스를 살펴보자.
package travelator.money;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;
import static java.math.BigDecimal.ZERO;
public class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) { // <1>
this.amount = amount;
this.currency = currency;
}
public static Money of(BigDecimal amount, Currency currency) { // <1>
return new Money(
amount.setScale(currency.getDefaultFractionDigits()),
currency);
}
public static Money of(String amountStr, Currency currency) { // <2>
return Money.of(new BigDecimal(amountStr), currency);
}
public static Money of(int amount, Currency currency) {
return Money.of(new BigDecimal(amount), currency);
}
public static Money zero(Currency userCurrency) {
return Money.of(ZERO, userCurrency);
}
public BigDecimal getAmount() { // <2>
return amount;
}
public Currency getCurrency() { // <3>
return currency;
}
@Override
public boolean equals(Object o) { // <3>
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) &&
currency.equals(money.currency);
}
@Override
public int hashCode() { // <3>
return Objects.hash(amount, currency);
}
@Override
public String toString() { // <4>
return amount.toString() + " " + currency.getCurrencyCode();
}
public Money add(Money that) { // <5>
if (!this.currency.equals(that.currency)) {
throw new IllegalArgumentException(
"cannot add Money values of different currencies");
}
return new Money(this.amount.add(that.amount), this.currency);
}
}
(1)
생성자는 비공개로 설정해 직접 객체생성을 할 수 없다.
여러개의 of 정적 팩토리 생성 함수를 통해 객체를 생성할 수 있다.
정적 팩토리 생성 함수들은 금액을 currency의 특성 상황에 맞게 일치하도록 보장한다.
of 함수는 현대 자바의 관습을 따른다. (예: LocalDate.of(...), List.of(...)...)
(2) 자바빈의 관습에 따라 게터를 제공한다.
(3) equals, hashCode는 값 타입의 특성에 맞게 구현되어 있다.
(4) toString은 사용자가 의미있는 형태로 볼 수 있도록 구현되어 있다.
(5) 동일한 Money에 대한 더하기 계산 연산자를 제공한다.
함수 이름의 set, with 접두사
BigDecimal.setScale는 객체의 상태를 변경하지 않고 setScale를 적용한 새로운 BigDecimal을 반한다. 이때는 set 접두사를 피하고 with 접두사를 사용하는 편이 좋다.
amount.withScale(currency.getDefaultFractionDigits())
코틀린은 이부분을 확장함수로 풀 수 있다.
fun BigDecimal.withScale(int scale, RoundingMode mode) = setScale(scale, mode)
Money 클래스를 코틀린으로 변환하면 다음과 같다.
package travelator.money
import java.math.BigDecimal
import java.util.*
class Money
private constructor(
val amount: BigDecimal,
val currency: Currency
) {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val money = o as Money
return amount == money.amount && currency == money.currency
}
override fun hashCode(): Int {
return Objects.hash(amount, currency)
}
override fun toString(): String {
return amount.toString() + " " + currency.currencyCode
}
fun add(that: Money): Money {
require(currency == that.currency) {
"cannot add Money values of different currencies"
}
return Money(amount.add(that.amount), currency)
}
companion object {
@JvmStatic
fun of(amount: BigDecimal, currency: Currency): Money {
return Money(
amount.setScale(currency.defaultFractionDigits),
currency
)
}
@JvmStatic
fun of(amountStr: String?, currency: Currency): Money {
return of(BigDecimal(amountStr), currency)
}
@JvmStatic
fun of(amount: Int, currency: Currency): Money {
return of(BigDecimal(amount), currency)
}
@JvmStatic
fun zero(userCurrency: Currency): Money {
return of(BigDecimal.ZERO, userCurrency)
}
}
}
- 정적 팩토리 생성 함수에는 기존 자바코드의 변경없이 호출가능하도록 @JvmStatic 어노테이션이 붙어있다.
- 데이터 클래스로 변경시 copy 함수 생성으로 인해 비공개 데이터 클래스의 생성자가 노출된다는 경고가 표시된다.
(Private data class constructor is exposed via the generated 'copy' method)
Moeny 클래스는 생성자가 private으로 감추어져 있다.
정적 팩토리 생성 함수로만 Money 객체를 만들수 있도록 되어 있다.
클래스 생성시 내부 구현에 감춰야하는 세부 사항이 있다는 뜻이다.
데이터 클래스는 copy 함수를 통해 내부 구현에 적합하지 않은 객체를 생성할 수 있는 허점이 있다.
프로퍼티 사이에 불변 조건을 유지해야 하는 값 타입은 데이터 클래스를 사용해서 정의하면 안된다.
다음으로 나아가기
데이터 클래스를 사용해 값 타입 구현시 보일러플레이트 코드들을 많이 줄일 수 있다.
값 객체 프로퍼티들이 서로 불변조건을 유지해야 하거나 내부를 캡슐화해야 한다면 데이터 클래스는 적합하지 않다.
Last updated