IT/Java

Effective Java: 생성자에 많은 매개변수가 필요할 때: 빌더 패턴 고려하기

Dev. Sean 2024. 1. 24. 12:34
반응형

프로그래밍을 하다 보면, 때때로 매개변수가 많은 객체를 생성해야 하는 상황에 직면합니다.

이럴 때 전통적인 생성자나 정적 팩터리 방법을 사용하면, 코드의 복잡성과 유지 보수의 어려움이 가중됩니다.

특히 매개변수 중 일부가 선택적일 경우, 이 문제는 더욱 심각해집니다.

 

이러한 문제를 해결하는 방법인 '빌더 패턴(Builder Pattern)'에 대해 소개하려고 합니다.

 

점층적 생성자 패턴의 한계

점층적 생성자 패턴은 매개변수의 수에 따라 여러 생성자를 오버로딩하는 방식입니다.

예를 들어, 직원의 정보를 관리하는 Employee 클래스가 있다고 가정해 봅시다.

이 클래스에는 이름, 나이, 이메일, 주소 등 다양한 필드가 있을 수 있습니다.

필수 정보는 이름과 나이이고, 나머지는 선택적입니다.

점층적 생성자 패턴을 사용하면, 선택적 매개변수의 모든 조합에 대해 별도의 생성자를 제공해야 합니다.

이런 접근 방식은 매개변수의 수가 늘어날수록 코드를 관리하기 어려워지는 문제가 있습니다.

 

자바빈즈 패턴의 단점

자바빈즈 패턴은 매개변수가 없는 생성자를 통해 객체를 생성한 후, 세터 메서드를 통해 원하는 매개변수의 값을 설정하는 방식입니다.

이 방법은 코드의 가독성은 높이지만, 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이는 문제가 있습니다.

또한, 불변 객체를 만들 수 없어 스레드 안전성을 보장하기 어렵습니다.

 

빌더 패턴의 도입

빌더 패턴은 위의 두 패턴의 단점을 해결하면서 안전성과 가독성을 모두 잡을 수 있는 방법입니다.

이 패턴에서는 클라이언트가 필요한 객체를 직접 만드는 대신 빌더 객체를 통해 원하는 매개변수를 설정합니다.

마지막으로 build() 메서드를 호출하여 필요한 객체를 완성합니다.

이 패턴의 가장 큰 장점은 매개변수가 많은 경우에도 가독성이 높고 안전하게 객체를 생성할 수 있다는 것입니다.

 

빌더 패턴의 간단한 예제

새로운 예시로, 학생 정보를 관리하는 Student 클래스를 생각해 봅시다.

이 클래스에는 학번, 이름, 학년, 전공, 이메일 등 다양한 필드가 있습니다.

여기서 학번과 이름은 필수 정보이고, 나머지는 선택적 정보입니다.

 

public class Student {
    private final String studentId; // 필수
    private final String name;      // 필수
    private final int grade;        // 선택
    private final String major;     // 선택
    private final String email;     // 선택

    public static class Builder {
        // 필수 매개변수
        private final String studentId;
        private final String name;

        // 선택 매개변수 - 기본값으로 초기화
        private int grade = 1;
        private String major = "미정";
        private String email = "";

        public Builder(String studentId, String name) {
            this.studentId = studentId;
            this.name = name;
        }

        public Builder grade(int val) {
            grade = val;
            return this;
        }

        public Builder major(String val) {
            major = val;
            return this;
        }

        public Builder email(String val) {
            email = val;
            return this;
        }

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

    private Student(Builder builder) {
        studentId = builder.studentId;
        name = builder.name;
        grade = builder.grade;
        major = builder.major;
        email = builder.email;
    }
}

 

사용 예:

Student student = new Student.Builder("20220001", "홍길동")
                        .grade(2)
                        .major("컴퓨터공학")
                        .email("hong@example.com")
                        .build();

 

이처럼 빌더 패턴을 사용하면 클라이언트 코드가 간결해지고, 매개변수의 의미가 명확해져 가독성이 높아집니다.

매개변수가 많을수록 빌더 패턴의 장점이 더욱 도드라져 보입니다.

따라서 매개변수가 많은 객체를 생성할 때는 빌더 패턴을 고려해보는 것이 좋습니다.

 

빌더 패턴과 추상 클래스를 활용한 차량 구성 시스템

프로그래밍에서 복잡한 객체를 효율적으로 생성하고자 할 때, '빌더 패턴'과 '추상 클래스', 그리고 '메서드 오버라이드'는 매우 유용한 도구입니다.

차량 구성 시스템을 통해 이 개념들을 어떻게 활용할 수 있는지 살펴보겠습니다.

차량 클래스의 기본 구조

우선, 차량을 나타내는 기본 클래스인 Vehicle 클래스를 만듭니다.

이 클래스는 차량의 기본적인 속성과 기능을 정의하는 추상 클래스로 시작합니다.

차량의 유형, 브랜드, 모델 등의 필드를 포함할 수 있습니다.

public abstract class Vehicle {
    public enum Type { CAR, TRUCK, SUV }
    private final Type type;
    private final String brand;
    private final String model;

    protected Vehicle(Builder<?> builder) {
        this.type = builder.type;
        this.brand = builder.brand;
        this.model = builder.model;
    }

    // 추상 클래스 내의 Builder
    abstract static class Builder<T extends Builder<T>> {
        private final Type type;
        private String brand;
        private String model;

        public Builder(Type type) {
            this.type = type;
        }

        public T brand(String brand) {
            this.brand = brand;
            return self();
        }

        public T model(String model) {
            this.model = model;
            return self();
        }

        abstract Vehicle build();

        // 하위 클래스가 구현할 self 메서드
        protected abstract T self();
    }
}

 

하위 클래스와 오버라이드

다음으로, Vehicle 클래스의 하위 클래스를 만듭니다.

예를 들어, Car와 Truck이라는 구체적인 차량 종류를 나타내는 클래스를 정의할 수 있습니다.

이 클래스들은 Vehicle의 Builder를 상속하고, 특정 차량 종류에 맞는 추가적인 속성을 정의합니다.

 

public class Car extends Vehicle {
    private final boolean isElectric;

    public static class Builder extends Vehicle.Builder<Builder> {
        private boolean isElectric = false;

        public Builder() {
            super(Type.CAR);
        }

        public Builder isElectric(boolean isElectric) {
            this.isElectric = isElectric;
            return this;
        }

        @Override
        public Car build() {
            return new Car(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private Car(Builder builder) {
        super(builder);
        isElectric = builder.isElectric;
    }
}

 

이와 같은 방식으로 Truck 클래스도 정의할 수 있습니다.

클라이언트 코드

클라이언트 코드에서는 이러한 빌더를 사용하여 각기 다른 유형의 차량 객체를 생성할 수 있습니다.

 

Car electricCar = new Car.Builder()
                        .brand("Tesla")
                        .model("Model S")
                        .isElectric(true)
                        .build();

Truck pickupTruck = new Truck.Builder()
                            .brand("Ford")
                            .model("F-150")
                            .payloadCapacity(1000)
                            .build();

 

 

결론

이 예시에서 볼 수 있듯, 빌더 패턴과 추상 클래스, 그리고 메서드 오버라이드를 사용하면, 다양한 유형의 객체를 효율적이고 유연하게 생성할 수 있습니다.

이러한 방식은 코드의 가독성을 높이고, 유지 보수를 용이하게 만듭니다.

객체 생성에 관련된 복잡성이 높을수록 이러한 패턴의 장점이 더욱 빛을 발합니다.

반응형