DevSoupe
  • 🙂welcome
    • Hi there, I'm Seth!
  • ✍️blog
    • macOS 개발 환경 설정
    • SDKMAN!으로 자바 버전 관리
    • 도커 환경에서 MySQL 설치 및 접속
    • 도커를 사용하여 GitLab Runner 구성
    • 스프링부트 H2 DB mem 모드 사용시 테이블 접근
    • Git 서브모듈 삭제
  • 📚Book
    • 자바에서 코틀린으로 (코틀린으로 리팩터링하기)
      • 1장 - 소개
      • 2장 - 자바 프로젝트에서 코틀린 프로젝트
      • 3장 - 자바 클래스에서 코틀린 클래스로
      • 4장 - 옵셔널에서 널이 될 수 있는 타입으로
    • 오브젝트 (코드로 이해하는 객체지향 설계)
    • 이펙티브 코틀린
      • 1장 - 안정성
        • 아이템 1 - 가변성을 제한하라
        • 아이템 2 - 변수의 스코프를 최소화하라
        • 아이템 3 - 최대한 플랫폼 타입을 사용하지 말라
        • 아이템 4 - inferred 타입으로 리턴하지 말라
        • 아이템 5 - 예외를 활용해 코드에 제한을 걸어라
        • 아이템 6 - 사용자 정의 오류보다는 표준 오류를 사용하라
        • 아이템 7 - 결과 부족이 발생할 경우 null과 Failure를 사용하라
        • 아이템 8 - 적절하게 null을 처리하라
        • 아이템 9 - use를 사용하여 리소스를 닫아라
        • 아이템 10 - 단위 테스트를 만들어라
  • 🧑‍🏫Seminar
    • 우아한 모노리스
    • 우아한 객체지향
    • 점진적 추상화
  • 🌎English
    • 영어 피트니스 50일의 기적 ①
      • PART 1 워밍업
        • 1. '말문 트기'란?
      • PART 2 말문 트기 훈련 코스
        • DAY 1
          • STEP 1. Do (~해, ~하지 마)
          • STEP 2. Do + 말늘리기
    • 라이브 아카데미 토들러
      • Lesson 001 - 기본적인 문장 구성하기
      • Lesson 002 - 문장 만들기
Powered by GitBook
On this page
  • 간단한 값 타입
  • 데이터 클래스의 한계
  • 다음으로 나아가기
  1. Book
  2. 자바에서 코틀린으로 (코틀린으로 리팩터링하기)

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 8 months ago

📚