본문 바로가기
  • 개발하는 곰돌이
Programming Language/Kotlin & Java

[Kotlin] 빌더 패턴과 코틀린, 그리고 Default Parameter와 Named Argument를 이용하여 코틀린에서 빌더 패턴의 효과를 내보기

by 개발하는 곰돌이 2023. 2. 13.

Builder Pattern

이펙티브 자바(조슈아 블로크 著)에서는 생성자의 파라미터가 많다면 빌더를 고려하라는 내용이 있다. 클래스의 필드가 많아져서 생성자의 파라미터가 많아질수록 파라미터의 순서를 뒤바꾸거나 엉뚱한 값을 집어넣는 실수를 하기 쉬워지고, 이는 파라미터의 타입과 파라미터의 개수만 같다면 컴파일 시점에는 아무 문제가 없기 때문에 런타임에 예상치 못한 에러를 유발할 수 있다. 이런 문제를 해결하기 위해 나온 디자인 패턴이 바로 빌더 패턴(Builder Pattern)이다. 빌더 패턴은 일반적으로 클래스 내부에 해당 클래스의 필드를 그대로 갖는 Builder라는 정적 클래스를 생성한 후 객체를 생성하고자 하는 클래스에서 builder() 메소드로 빌더를 생성한다. 이후 각 파라미터마다 값을 삽입하고 Builder를 반환하여 연쇄적으로 필요한 값을 삽입하다가 마지막에 build() 메소드를 호출하여 원하던 클래스의 객체를 생성하게 된다. 아래 코드는 이펙티브 자바에서 예시로 든 NutritionFacts 클래스에서 구현한 빌더 패턴이다.

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // 필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화한다.
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }
        
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100)
                .sodium(35)
                .carbohydrate(27)
                .build();
    }
}

빌더 패턴의 장점

빌더 패턴을 사용하면서 얻을 수 있는 장점은 다음과 같다.

  • 클래스의 필드가 많아져도 어떤 필드에 무슨 값을 넣어야 할지 헷갈리지 않는다.
  • 생성자나 정적 팩토리 메소드와는 달리 필드 값을 설정하기 위해 순서에 상관없이 필드 값을 설정할 메소드를 호출할 수 있다.
  • 자바 빈즈 패턴과 달리 불변성을 보장할 수 있다.

코틀린의 생성자로 빌더 패턴의 효과를 얻어보자

[Kotlin] 기본 매개변수(Default parameter)와 명명된 인자(Named Arguments)에서 언급한 바와 같이 코틀린에서는 생성자를 포함한 모든 메소드의 파라미터가 기본 값을 가질 수 있고, 파라미터의 이름을 명시하여 해당 파라미터에 값을 대입할 수 있다. 또한, 이러한 점을 이용하여 빌더 패턴처럼 사용할 수도 있다고 언급했다.

 

위의 이펙티브 자바 예제 클래스를 빌더를 제외하고 코틀린으로 변환해보자.

class NutritionFacts(
    val servingSize: Int,
    val servings: Int,
    val calories: Int,
    val fat: Int,
    val sodium: Int,
    val carbohydrate: Int
)

6개의 필드를 가지면서 동시에 해당 필드를 모두 파라미터로 받아 객체를 생성하는 생성자 또한 선언되었다. 여기서 calories, fatsodiumcarbohydrate는 모두 선택적인 값이었으므로 Default Parameter를 이용하여 기본값을 0으로 설정한다.

class NutritionFacts(
    val servingSize: Int,
    val servings: Int,
    val calories: Int = 0,
    val fat: Int = 0,
    val sodium: Int = 0,
    val carbohydrate: Int = 0
)

코틀린에서는 이렇게 생성자만 존재하는 상태에서 생성자를 호출할 때 Named Arguments를 사용하는 것으로 자바의 빌더 패턴과 유사한 효과를 낼 수 있다. 아래는 위의 이펙티브 자바 예제 코드의 main() 메소드에서 cocaCola라는 객체를 생성하는 것과 유사하게 코틀린 클래스의 생성자와 Named Arguments를 사용하여 객체를 생성하는 코드이다.

class NutritionFacts(
    val servingSize: Int,
    val servings: Int,
    val calories: Int = 0,
    val fat: Int = 0,
    val sodium: Int = 0,
    val carbohydrate: Int = 0
)

fun main() {
    val cocaCola = NutritionFacts(
        servingSize = 240,
        servings = 8,
        calories = 100,
        sodium = 35,
        carbohydrate = 27
    )
}

이 경우 파라미터를 전달하지 않은 fat은 Default Parameter인 0으로 초기화되고, 나머지 필드들은 모두 생성자에서 필드명을 명시하고 넘겨준 값이 저장된다. 이 때, 빌더 패턴과 마찬가지로 필드명을 명시했기 때문에 생성자의 파라미터 순서는 마음껏 뒤바꿀 수 있다.

val cocaCola1 = NutritionFacts(
    servingSize = 240,
    servings = 8,
    calories = 100,
    sodium = 35,
    carbohydrate = 27
)

val cocaCola2 = NutritionFacts(
    servings = 8,
    sodium = 35,
    servingSize = 240,
    carbohydrate = 27,
    calories = 100
)

// cocaCola1과 cocaCola2는 같은 필드 값을 가진 객체이다.

여기서 자바의 빌더 패턴과 차별화되는 점이 드러난다. 빌더 패턴에서는 필수적인 필드값을 삽입하기 위해서는 처음 빌더를 생성할 때 필수 필드에 값을 저장하기 위한 파라미터를 받아야 했고, 이 경우에는 억지로 빌더를 생성하는 메소드의 이름을 바꾸지 않는 한 파라미터라는 한계로 인해 이름을 지정하거나 순서를 바꿀 수 없었다.

 

그러나 코틀린의 생성자에서 Default Parameter와 Named Arguments를 이용하여 빌더 패턴의 효과를 얻는 경우는 다르다. 모두 생성자의 파라미터이고, 값을 넘겨줄 파라미터 이름을 명시할 수 있기 때문에 필수 필드 또한 명확하게 값을 저장할 수 있고, 바로 위의 예시처럼 필수 필드도 순서를 마음껏 뒤바꿀 수 있다. 실수로 필수 필드에 값을 집어넣는 것을 누락하더라도 컴파일러가 바로 어떤 필드의 값이 누락되었는지 확인시켜준다.

Default Parameter를 지정하지 않아 필수적으로 값을 전달해야하는 servings를 누락하면 servings를 위한 값이 전달되지 않았다는 컴파일 에러가 발생한다.

이렇게 코틀린의 언어적 특성으로 인해 코틀린에서는 따로 빌더를 구현하지 않더라도 빌더의 효과를 더욱 우수하게 가져갈 수 있기 때문에 억지로 빌더를 구현할 필요는 없다고 생각한다.

댓글