이펙티브 코틀린 Item 34: 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라

woga·2023년 9월 16일
0

코틀린 공부

목록 보기
37/54
post-thumbnail

객체를 정의하고 생성하는 방법 중 가장 기본적인 방법은 기본 생성자를 사용하는 것이다.

일반적으로 이를 활용해서 객체를 만드는 것이 좋다. 기본 생성자로 객체를 만들 때는 객체의 초기 상태를 나타내는 아규먼트를 전달한다. 일단 데이터를 표현하는 가장 기본적인 데이터는 아래와 같다.

data class Student(
	val name: String,
    val surname: String,
    val age: Int

일반적으로 기본 생성자가 좋은 방식인 이유를 이해하려면 생성자와 관련된 자바 패턴들을 이해하는 것이 좋다.

  • 점층적 생상자 패턴
  • 빌더 패턴

이 패턴들에서 어떤 문제가 있는지 그리고 코틀린을 활용해 이를 해결하는 방법에 대해서 살펴보자

점층적 생성자 패턴

점층적 생성자 패턴을 여러 가지 종류의 생성자를 사용하는 굉장히 간단한 패턴이다.

class Pizze {
	val size: String
   	val cheese: Int
    val olives: Int
    val bacon: Int
    
    constructor(size: String, cheese: Int, olives: Int, bacon: Int) {
    	this.size = size
        this.cheese = cheese
        this.olives = olives
        this.bacon = bacon
    }
    constructor(size: String, cheese: Int, olives: Int): this(size, cheese, olives, 0)
    constructor(size: String, cheese: Int): this(size, cheese, 0)
    constructor(size: String): this(size, 0)

이 코드는 그렇게 좋은 코드가 아니다. 코틀린에서는 일반적으로 다음과 같이 디폴트 아규먼트를 사용한다

class Pizza(
	val size: String,
    val cheese: Int = 0,
    val olives: Int = 0,
    val bacon: Int = 0
)

이런 디폴트 아규먼트는 코드를 단순하고 깔끔하게 만든다. 또한 점층적 생성자보다 훨씬 다양한 기능을 제공한다.

ex)

val myFavorite = Pizza("L", olives = 3), val myFavorite = Pizza("L", olives = 3, cheese = 1)

그러므로 이 디폴트 아규먼트의 장점은 아래와 같다.

  • 파라미터들의 값을 원하는 대로 지정할 수 있다.
  • 아규먼트를 원하는 순서로 지정할 수 있다.
  • 명시적으로 이름을 붙여서 아규먼트를 지정하므로 의미가 훨씬 명확하다

마지막 이유가 특히 중요하다

만약 객체를 만든다고 가정하자

val villagePizze = Pizza("L", 1, 2, 3)

코드는 짧지만 이해하기 어렵다. 이 파라미터들이 어떤 위치가 베이컨인지 치즈인 등 구분하지 못한다.
그런데 디폴트 아규먼트가 있다면?

val villagePizze = Pizza(size = "L", cheese = 1, olives = 2, bacon = 3)

사용하는 생성자가 점층적 생성자 패턴보다 훨씬 강력하다. 자바는 객체를 만들 때 이 패턴 외에 빌더 패턴도 많이 사용한다.

빌더 패턴

자바에서는 이름 있는 파라미터와 티폴트 아규먼트를 사용할 수 없다. 그래서 자바에서는 빌더 패턴을 사용한다. 빌더 패턴을 사용하면 다음과 같은 장점이 있다

  • 파라미터에 이름을 붙일 수 있다.
  • 파라미터를 원하는 순서로 지정할 수 있다.
  • 디폴트 값을 지정할 수 있다

빌더 패턴은 예를 들면 다음과 같다.

class Pizza private constructor(
	val size: String,
    val cheese: Int,
    val olives: Int,
    val bacon: Int
) {
	class Builder(private val size: String) {
    	private var cheese: Int = 0
        private var olives: Int = 0
        private var bacon: Int = 0
        
        fun setCheese(value: Int): Builder = apply {
        	cheese = value
        }
        
        fun setOlives(value: Int): Builder = apply {
        	olives = value
        }
        //...
        
        fun build() = Pizza(size, cheese, olives, bacon)
     }
 }

빌더 패턴을 활용하면 다음과 같이 파라미터에 이름을 붙여서 지정할 수 있다

val myFavorite = Pizza.Builder("L").setOlivese(3).build()

val villagePizze = Pizza.Builder("L")
	.setCheese(1)
    .setOlives(2)
    .build()

이전에 언급했던 것처럼 이러한 두 가지 장점은 코틀린의 디폴트 아규먼트와 이름 있는 파라미터도 가지고 있다.

val villagePizza = Pizza(
	size = "L",
    cheese = 1,
    olives = 2,
    bacon = 3
)

이렇듯 디폴트 아규먼트는 장점이 많은데 결국 빌더 패턴보다 파라미터를 사용하는 것이 좋은 이유를 간단하게 정리해보면 다음과 같다.

  • 코드가 더 짧아진다.

  • 코드가 더 명확해진다.

  • 코드로 더 사용하기 쉽다.

  • 동시성과 관련된 문제가 없다.
    - 코틀린의 함수 파라미터는 항상 immutable이다. 반면 대부분의 빌더 패턴에서 프로퍼티는 mutable이다. 따라서 빌더 패턴의 빌더 함수를 쓰레드 안전(thread-safe)하게 구현하는 것은 어렵다.

물론 무조건 빌더 패턴 대신 기본 생성자를 사용해야 한다는 것은 아니다.

빌더 패턴이 좋은 경우는 다음과 같다

  • Dialog
val dialog = AlertDialog.Builder(context)
	.setMessage(R.string.fire_missiles)
    .setPositiveButton(R.string.fire, { d, id ->
    	// 미사일 발사!
    })
    .setNegativeButton(R.string.cancelm, { d, id ->
    	// 사용자가 대화상자 취소
    })
    .create()

val router = Router.Builder()
	.addRoute(path = "/home". ::showHome)
    .addRoute(path = "/users", ::showUsers)
    .build()

빌더 패턴을 사용하지 않고 이를 구현하면 추가적인 타입들을 만들고 활용해야한다. 코드가 오히려 복잡해진다

val dialog = AlertDialog(context,
	message = R.string.fire_missiles,
    positive...
)

val router = Router(
	routes = listOf(
    	Route("/home", ::showHome),
        Route("/users", ::showUsers)
    )
)

이런 코드는 코틀린 커뮤니티에서 좋게 받아들여지지 않는다. 일반적으로 이런 코드는 다음과 같이 DSL(Domain Specific Language) 빌더를 사용한다

val dialog = context.alert(R.string.fire_missiles) {
	positiveButton(R.string.fire) {
    	// missiles fire!
    }
    negativeButton {
    	// user click cancel button
    }
}

val route = route {
	"/home" directsTo ::showHome
    "users" direcsTo ::showUsers
 }

이렇게 DSL 사용하는게 전통적인 빌더패턴보다 유연하고 명확해서 코틀린에서는 이와 같은 형태의 코드를 많이 사용한다.

DSL 만드는 것도 쉽지 않지만 빌더 또한 그렇다. 그렇기 때문에 시간을 더 투자해서 유연하고 가독성 좋은 DSL을 사용하자.

또한 전형적인 빌더 패턴은 팩토리로 사용할 수 있다.

그럼에도 실무에서 보기 어려운 형태의 코드로 보인다.

결론적으로 코틀린에서는 빌더 패턴을 거의 사용하지 않는다. 빌더 패턴은 다음과 같은 경우에만 사용하다

  • 빌더 패턴을 사용하는 다른 언어로 작성된 라이브러리를 그대로 옮길 때

  • 디폴트 아규먼트와 DSL을 지원하지 않는 다른 언어에서 쉽게 사용할 수 있게 API를 설계할 때

이를 제외하면, 빌더 패턴 대신에 디폴트 아규먼트를 갖는 기본 생성자 또는 DSL을 사용하는 것이 좋다.

정리

일반적인 프로젝트에서는 기본 생성자를 사용해 객체를 만든다. 코틀린에서는 점층적 생성자 패턴을 사용하지 않는다.

대신 디폴트 아규먼트를 사용하자! 빌더 패턴도 마찬가지다. 기본 생성자를 사용하는 코드로 바꾸거나 DSL 활용하는 것이 좋다.

profile
와니와니와니와니 당근당근

0개의 댓글