> For the complete documentation index, see [llms.txt](https://devsoupe.gitbook.io/today-i-learned/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://devsoupe.gitbook.io/today-i-learned/book/undefined-1/3.md).

# 3장 - 자바 클래스에서 코틀린 클래스로

## 간단한 값 타입

* EmailAddress 클래스는 값 타입(value type)이다.

{% tabs %}
{% tab title="EmailAddress.java" %}

```java
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;
    }
}
```

{% endtab %}
{% endtabs %}

```
(1) final 필드로 선언된 값은 불변이다.
(2) 문자열을 파싱한 후 주 생성자를 호출해 객체를 생성하는 parse 정적 팩터리 함수가 있다.
(3) 클래스 내부 필드는 생성자에서 초기화된다.
(4) 프로퍼티를 구성하는 접근자 함수는 자바빈의 명명 규칙을 따른다.
(5) equals, hashCode 함수를 구현해서 두 객체의 필드값이 동일하다면 그 객체들은 동일한 객체이다.
(6) toString은 내부 필드값을 가지고 표준 이메일 주소 형식으로 변환한다.
```

* 인텔리J에 'Convert Java File to Kotlin File' 기능으로 EmailAddress.kt로 만들수 있다.

{% tabs %}
{% tab title="EmailAddress.kt" %}

```kotlin
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)
            )
        }
    }
}
```

{% endtab %}
{% endtabs %}

* 주 생성자 안에 val을 붙여 프로퍼티를 선언하는 형태로 클래스를 정의할 수 있다. (옆 탭은 동일한 자바 코드)

{% tabs %}
{% tab title="EmailAddress.kt" %}

```kotlin
class EmailAddress(val localPart: String, val domain: String)
```

{% endtab %}

{% tab title="EmailAddress.java" %}

```java
...

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;
}

...
```

{% endtab %}
{% endtabs %}

* 파라미터를 한 줄에 하나씩 배치해 가독성을 높일 수 있다.

{% tabs %}
{% tab title="EmailAddress.kt" %}

```kotlin
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)
            )
        }
    }
}
```

{% endtab %}
{% endtabs %}

```
- companion object는 최상위 함수로 리팩터링 할 수 있다.
- @JvmStatic는 기존 자바 코드에서 변경없이 코틀린 코드를 호출할 수 있게 해준다.
```

* 코틀린의 생성자에 val로 선언된 프로퍼티는 getDomain() 게터를 제공해준다.

{% tabs %}
{% tab title="Marketing.java" %}

```java
package travelator;

public class Marketing {

    public static boolean isHotmailAddress(EmailAddress address) {
        return address.getDomain().equalsIgnoreCase("hotmail.com");
    }
}
```

{% endtab %}
{% endtabs %}

* 자바의 EmailAddress 코드는 전형적인 값 타입 클래스로 보일러플레이트 코드가 많다.&#x20;
* 코틀린은 data 변경자를 붙여 값 타입의 데이터 클래스를 지원한다.&#x20;

{% tabs %}
{% tab title="EmailAddress.kt" %}

```kotlin
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)
            )
        }
    }
}
```

{% endtab %}
{% endtabs %}

```
(1) 
데이터 클래스는 equals, hashCode, toString을 모두 자동생성 해준다. 
toString은 원하는 형태로 만들어주기 위해 오버라이딩 했다.
```

## 데이터 클래스의 한계

* equals, hashCode, toString 함수를 생성해준다.
* 프로퍼티 일부를 다른 값을 대치할 수 있는 copy 함수도 제공한다.

```kotlin
val postmasterEmail = customerEmail.copy(localPart = "postmaster")
```

* 돈이라는 개념을 추상화 해놓은 타입인 Money 클래스를 살펴보자.

{% tabs %}
{% tab title="Money.java" %}

<pre class="language-java"><code class="lang-java"><strong>package travelator.money;
</strong>
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) { // &#x3C;1>
        this.amount = amount;
        this.currency = currency;
    }

    public static Money of(BigDecimal amount, Currency currency) { // &#x3C;1>
        return new Money(
            amount.setScale(currency.getDefaultFractionDigits()),
            currency);
    }


    public static Money of(String amountStr, Currency currency) { // &#x3C;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() { // &#x3C;2>
        return amount;
    }

    public Currency getCurrency() { // &#x3C;3>
        return currency;
    }

    @Override
    public boolean equals(Object o) { // &#x3C;3>
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount.equals(money.amount) &#x26;&#x26;
            currency.equals(money.currency);
    }

    @Override
    public int hashCode() { // &#x3C;3>
        return Objects.hash(amount, currency);
    }

    @Override
    public String toString() { // &#x3C;4>
        return amount.toString() + " " + currency.getCurrencyCode();
    }

    public Money add(Money that) { // &#x3C;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);
    }
}
</code></pre>

{% endtab %}
{% endtabs %}

```
(1) 
생성자는 비공개로 설정해 직접 객체생성을 할 수 없다.
여러개의 of 정적 팩토리 생성 함수를 통해 객체를 생성할 수 있다.
정적 팩토리 생성 함수들은 금액을 currency의 특성 상황에 맞게 일치하도록 보장한다. 
of 함수는 현대 자바의 관습을 따른다. (예:  LocalDate.of(...), List.of(...)...)
(2) 자바빈의 관습에 따라 게터를 제공한다.
(3) equals, hashCode는 값 타입의 특성에 맞게 구현되어 있다.
(4) toString은 사용자가 의미있는 형태로 볼 수 있도록 구현되어 있다.
(5) 동일한 Money에 대한 더하기 계산 연산자를 제공한다.
```

{% hint style="info" %}
**함수 이름의 set, with 접두사**

BigDecimal.setScale는 객체의 상태를 변경하지 않고 setScale를 적용한 새로운 BigDecimal을 반한다. 이때는 set 접두사를 피하고 with 접두사를 사용하는 편이 좋다.

amount.withScale(currency.getDefaultFractionDigits())

\
코틀린은 이부분을 확장함수로 풀 수 있다.

fun BigDecimal.withScale(int scale, RoundingMode mode) = setScale(scale, mode)
{% endhint %}

* Money 클래스를 코틀린으로 변환하면 다음과 같다.

{% tabs %}
{% tab title="Money.kt" %}

```kotlin
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)
        }
    }
}
```

{% endtab %}
{% endtabs %}

```
- 정적 팩토리 생성 함수에는 기존 자바코드의 변경없이 호출가능하도록 @JvmStatic 어노테이션이 붙어있다. 
- 데이터 클래스로 변경시 copy 함수 생성으로 인해 비공개 데이터 클래스의 생성자가 노출된다는 경고가 표시된다. 
(Private data class constructor is exposed via the generated 'copy' method)
```

* Moeny 클래스는 생성자가 private으로 감추어져 있다.
* 정적 팩토리 생성 함수로만 Money 객체를 만들수 있도록 되어 있다.
* 클래스 생성시 내부 구현에 감춰야하는 세부 사항이 있다는 뜻이다.
* 데이터 클래스는 copy 함수를 통해 내부 구현에 적합하지 않은 객체를 생성할 수 있는 허점이 있다.

{% hint style="info" %}
프로퍼티 사이에 불변 조건을 유지해야 하는 값 타입은 데이터 클래스를 사용해서 정의하면 안된다.
{% endhint %}

## 다음으로 나아가기

* 데이터 클래스를 사용해 값 타입 구현시 보일러플레이트 코드들을 많이 줄일 수 있다.&#x20;
* 값 객체 프로퍼티들이 서로 불변조건을 유지해야 하거나 내부를 캡슐화해야 한다면 데이터 클래스는 적합하지 않다.&#x20;
