본문 바로가기

Java

Builder Pattern (빌더 패턴)

복잡한 객체를 단계별로 생성할 때 빌더 패턴을 고려할 수 있다.

특히 생성자에 여러 매개변수가 있는데 그 중 일부는 필수값이고 일부는 선택적인 경우 이 패턴이 유용할 수 있다.

빌더 패턴으로 코드 가독성을 높혀줄 수 있을 뿐만 아니라, 객체 생성 과정이 좀더 안전하고 직관적이게 되기 때문이다.

계층적 생성자 패턴 → 자바 빈즈 패턴 → 빌더 패턴

순서로 코드를 보면서 각각의 객체생성 방식을 살펴보자.

 

  1. 점층적 생성자 패턴
    public class NutritionFacts {
        private final int servingSize;  // (mL, 1회 제공량)     필수
        private final int servings;     // (회, 총 n회 제공량)  필수
        private final int calories;     // (1회 제공량당)       선택
        private final int fat;          // (g/1회 제공량)       선택
        private final int sodium;       // (mg/1회 제공량)      선택
        private final int carbohydrate; // (g/1회 제공량)       선택
    
        public NutritionFacts(int servingSize, int servings) {
            this(servingSize, servings, 0);
        }
    
        public NutritionFacts(int servingSize, int servings,
                              int calories) {
            this(servingSize, servings, calories, 0);
        }
    
        public NutritionFacts(int servingSize, int servings,
                              int calories, int fat) {
            this(servingSize, servings, calories, fat, 0);
        }
    
        public NutritionFacts(int servingSize, int servings,
                              int calories, int fat, int sodium) {
            this(servingSize, servings, calories, fat, sodium, 0);
        }
        public NutritionFacts(int servingSize, int servings,
                              int calories, int fat, int sodium, int carbohydrate) {
            this.servingSize  = servingSize;
            this.servings     = servings;
            this.calories     = calories;
            this.fat          = fat;
            this.sodium       = sodium;
            this.carbohydrate = carbohydrate;
        }
    
        public static void main(String[] args) {
            NutritionFacts cocaCola =
                    new NutritionFacts(240, 8, 100, 0, 35, 27);
        }
        
    }​

    위 클래스의 인스턴스를 만드려면 원하는 매개변수를 포함한 생성자를 호출하면 된다.
    이러한 생성자를 사용하는 경우 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에도 값을 지정해줘야 한다. 또한 매개변수가 몇십개 이상인 경우에는 코드를 점점 작성하기 어려워지고 가독성도 떨어지게 될 것이다.

  2. 자바빈즈 패턴
    public class NutritionFacts {
        // 매개변수들은 (기본값이 있다면) 기본값으로 초기화된다.
        private int servingSize  = -1; // 필수; 기본값 없음
        private int servings     = -1; // 필수; 기본값 없음
        private int calories     = 0;
        private int fat          = 0;
        private int sodium       = 0;
        private int carbohydrate = 0;
    
        public NutritionFacts() { }
        // Setters
        public void setServingSize(int val)  { servingSize = val; }
        public void setServings(int val)     { servings = val; }
        public void setCalories(int val)     { calories = val; }
        public void setFat(int val)          { fat = val; }
        public void setSodium(int val)       { sodium = val; }
        public void setCarbohydrate(int val) { carbohydrate = val; }
    
        public static void main(String[] args) {
            NutritionFacts cocaCola = new NutritionFacts();
            cocaCola.setServingSize(240);
            cocaCola.setServings(8);
            cocaCola.setCalories(100);
            cocaCola.setSodium(35);
            cocaCola.setCarbohydrate(27);
        }
    }​
     
    자바빈즈 패턴은 매개변수가 없는 생성자를 이용하여 객체를 생성한 후 setter를 이용하여 원하는 매개변수의 값을 설정하는 방식이다. 이로 인해 점층적 생성자 패턴보다 인스턴스를 만들기 쉽고 가독성도 좋아졌다.
    하지만 객체 하나를 만드려면 여러개의 setter메서드를 호출해야하고, 무엇보다도 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 있게 된다. 따라서 Thread-Safe하지 않게 되므로 또다른 문제를 야기시킬 수 있다.

  3. 빌더 패턴
    public class Person {
        private final String name; // 필수
        private final String email; // 필수
        private final int age; // 선택적
    
        private Person(Builder builder) {
            this.name = builder.name;
            this.email = builder.email;
            this.age = builder.age;
        }
    
        // Person의 빌더 정적 내부 클래스
        public static class Builder {
            private String name;
            private String email;
            private int age = 0; // 기본값
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setEmail(String email) {
                this.email = email;
                return this;
            }
    
            public Builder setAge(int age) {
                this.age = age;
                return this;
            }
    
            // build 메서드 내에서 불변식 검사
            public Person build() {
                validatePersonData();
                return new Person(this);
            }
    
            // 불변식 검사 메서드
            private void validatePersonData() {
                if (name == null || name.trim().isEmpty()) {
                    throw new IllegalStateException("Name cannot be null or empty");
                }
                if (email == null || !email.contains("@")) {
                    throw new IllegalStateException("Email must contain '@'");
                }
                if (age < 0) {
                    throw new IllegalStateException("Age cannot be negative");
                }
            }
        }
    }

    각 setter 메서드가 Builder 객체 자체를 반환하기 때문에 메서드 호출을 연쇄적으로 연결할 수 있어 코드를 간결하게 작성할 수 있다. 따라서 생성자에 전달해야 할 매개변수가 많거나, 특히 선택적 매개변수가 많을 때 코드의 가독성과 사용성을 크게 향상시킨다. 덫붙여, 불변식을 추가하여 각 매개변수를 정해진 조건 안에서만 허용할 수 있다. 위 코드에서는 'validatePersonData' 메서드를 이용하여 name, email, age의 허용범위를 제한하였다. 따라서 아래와 같이 사용할 수 있을 것으로 기대된다.

    try {
        Person person = new Person.Builder()
                        .setName("DevGenie")
                        .setEmail("devgeniegenie@example.com")
                        .setAge(-123) //오류 발생 유도
                        .build();
        System.out.println("Person created successfully.");
    } catch (IllegalStateException e) {
        System.err.println(e.getMessage()); //"Age cannot be negative"
    }

    위 코드에서는 age의 값이 0보다 작으므로 validatePersonData메서드로 인한 예외를 발생하게 될 것이다.

 


계층적으로 설계된 클래스

빌더패턴을 이용하면 계층적으로 클래스를 설계하기에 용의하다. self-type 이디엄과 제네릭과 을 활용하여 타입 안정적이면서도 계층적 클래스 구조를 띄는 코드를 작성해 보았다.

 

  1. Car abstract class : 기본 클래스와 빌더
    public abstract class Car {
        private final String manufacturer;
        private final String model;
        private final int year;
    
        public static abstract class Builder<T extends Builder<T>> {
            private String manufacturer;
            private String model;
            private int year;
    
            public T manufacturer(String manufacturer) {
                this.manufacturer = manufacturer;
                return self();
            }
    
            public T model(String model) {
                this.model = model;
                return self();
            }
    
            public T year(int year) {
                this.year = year;
                return self();
            }
    
            abstract Car build();
    
            // Subclasses must override this method to return "this"
            protected abstract T self();
        }
    
        protected Car(Builder<?> builder) {
            manufacturer = builder.manufacturer;
            model = builder.model;
            year = builder.year;
        }
    
        // Car methods...
    }​


  2. ElectricCar class : 서브클래스와 빌더
    public class ElectricCar extends Car {
        private final int batteryCapacity; // in kWh
    
        public static class Builder extends Car.Builder<Builder> {
            private int batteryCapacity;
    
            public Builder batteryCapacity(int capacity) {
                this.batteryCapacity = capacity;
                return this;
            }
    
            @Override
            public ElectricCar build() {
                return new ElectricCar(this);
            }
    
            @Override
            protected Builder self() {
                return this;
            }
        }
    
        private ElectricCar(Builder builder) {
            super(builder);
            batteryCapacity = builder.batteryCapacity;
        }
    
        // ElectricCar methods...
    }​


  3. SportsCar class : 서브클래스와 빌더
    public class SportsCar extends Car {
        private final boolean isConvertible;
    
        public static class Builder extends Car.Builder<Builder> {
            private boolean isConvertible = false; // Default
    
            public Builder isConvertible(boolean convertible) {
                isConvertible = convertible;
                return this;
            }
    
            @Override
            public SportsCar build() {
                return new SportsCar(this);
            }
    
            @Override
            protected Builder self() {
                return this;
            }
        }
    
        private SportsCar(Builder builder) {
            super(builder);
            isConvertible = builder.isConvertible;
        }
    
        // SportsCar methods...
    }​


  4. 객체 생성 예제
    ElectricCar electricCar = new ElectricCar.Builder()
                                .manufacturer("Tesla")
                                .model("Model 3")
                                .year(2020)
                                .batteryCapacity(75)
                                .build();
    
    SportsCar sportsCar = new SportsCar.Builder()
                            .manufacturer("Porsche")
                            .model("911")
                            .year(2021)
                            .isConvertible(true)
                            .build();

    각 서브 클래스들은 self 메서드를 통해서 메서드 체이닝의 타입 안정성을 보장하며, 이를 이용하여 상속 구조에서도 보다 유연하게 객체를 생성할 수 있다.

 

ChatGPT4 DALL.E : 자바 빌더패턴을 의미하는 이미지 그려줘