Builder Pattern

Slow And Steady·2023년 8월 30일
0

디자인패턴

목록 보기
1/1

Builder Pattern

빌더 패턴(Builder Pattern) 은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴이다.


빌더 패턴 탄생 배경

우선 탄생 배경을 보기 전에 점층적 생성자 패턴 을 알아보자.

점층적 생성자 패턴

점층적 생성자 패턴은 필수 매개변수와 함께 선택 매개변수를 0개, 1개, ... 로 받는 형태로, 다양한 매개변수를 입력받아 인스턴스를 생성하고 싶을 때 사용하는 생성자 오버로딩 방식이다.

class Student {

	//필수 매개변수
	private String name;
    private int age;
    
    //선택 매개변수
    private String grade;
    private String address;
    private int classNo;
	
    public Student(String name, int age, String grade){
    	this.name = name;
        this.age = age;
        this.grade = grade;
    }
    
    public Student(String name, int age, String grade, String address, int classNo){
    	this.name = name;
        this.age = age;
        this.grade = grade;
        this.address = address;
        this.classNo = classNo;
    }
}
public static void main(String[] args) {
    Student student1 = new Student("HARRY", 16, "A");
    Student student2 = new Student("TOM", 17, "B", "US", 3);
}

이런 방식은 클래스 필드가 많으면 많을수록 생성자에 들어가는 인자수가 늘어나 몇번째 인자가 어떤 필드였는지 헷갈릴 수 있다.

또한 매개변수 특성상 순서를 따라야 하기 때문에 매개변수의 순서를 변경할 수 없다. 생성자 만으로는 필드를 선택적으로 생략할 수 있는 방법이 없다.

자바 빈(Java Beans) 패턴

위의 단점을 보완하기 위해 Setter 메소드를 사용한 자바 빈 패턴이 고안 되었다. 매개변수가 없는 생성자로 객체 생성 후 Setter 메소드를 이용해 클래스 필드의 값을 세팅하는 방식이다.

class Student {

	//필수 매개변수
	private String name;
    private int age;
    
    //선택 매개변수
    private String grade;
    private String address;
    private int classNo;
	
    public void Student(){}
    
    public void setGrade(String name) {
        this.name = name;
    }
    
    public void setGrade(int age) {
        this.age = age;
    }
    
    public void setGrade(String grade) {
        this.grade = grade;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void setClassNo(int classNo) {
        this.classNo = classNo;
    }
    
}
public static void main(String[] args) {
    Student student1 = new Student();
    student1.setName("HARRY");
    student1.setAge("16");
    student1.setGrade("A");
    
    Student student2 = new Student();
    student2.setName("TOM");
    student2.setAddress("US");
    student2.setClassNo(3);
}

기존 생성자 오버로딩에서 나타났던 문제가 사라지고, 선택적 파라미터에 대해 해당되는 Setter 메소드를 호출함으로써 유연한 객체 생성이 가능해졌다. 하지만 이러한 방식은 객체 생성 시점에 모든 값을 주입하지 않아 일관성, 불변성 문제가 나타난다.

1) 일관성 문제
필수 매개변수란 객체가 초기화될 때 반드시 설정되어야 하는 값이다. 하지만 개발자가 깜빡하고 필수 매개변수를 주입하는 메소드를 호출하지 않았다면 이 객체는 일관성이 무너진다.

2) 불변성 문제
자바 빈즈 패턴의 Seeter 메소드는 객체를 처음 생성할 때 필드값을 설정하기 위해 존재하는 메소드다. 하지만 객체를 생성했음에도 여전히 외부에서 Setter 메소드를 노출하기 있으므로 누군가에 의해서 객체가 함부로 조작될 수 있다. 이것은 불변함을 보장할 수 없다는 얘기다.


빌더 패턴

빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 메소드를 통해 값을 입력받은 후 최종적으로 build() 메소드로 하나의 인스턴스를 생성하는 패턴이다.

빌더 패턴 구조

  1. 객체 생성 할 클래스
public class Student {

    private int id;
    private String name;
    private String grade;
    private String phoneNumber;

    public Student(int id, String name, String grade, String phoneNumber) {
        this.id = id;
        this.name = name;
        this.grade = grade;
        this.phoneNumber = phoneNumber;
    }

    @Override
    public String toString() {
        return "Student { " +
                "id='" + id + '\'' +
                ", name=" + name +
                ", grade=" + grade +
                ", phoneNumber=" + phoneNumber +
                " }";
    }
}
  1. 객체 생성 담당하는 별도의 빌더 클래스
public class StudentBuilder {

    private int id;
    private String name;
    private String grade;
    private String phoneNumber = "010";

    public StudentBuilder id(int id) {
        this.id = id;
        return this;
    }

    public StudentBuilder name(String name) {
        this.name = name;
        return this;
    }

    public StudentBuilder grade(String grade) {
        this.grade = grade;
        return this;
    }

    public StudentBuilder phoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
        return this;
    }

    public Student build() {
        return new Student(id, name, grade, phoneNumber); // Student 생성자 호출
    }
}

Builder 클래스 생성 후 필드 구성을 동일하게 작성한다. 각 필드에 대한 Setter 메서드를 구현한다.

Setter 함수 마지막 반환 구문인 return this 의 this 는 StudentBuilder 객체 자신을 말한다. 빌더 객체 자신을 리턴함으로써 메서드 호출 후 연속적으로 빌더 메서드를 체이닝하여 호출할 수 있다.

마지막으로 Student 객체를 만들어주는 build 메소드를 작성한다.

  1. 빌더 클래스 실행하기
public static void main(String[] args) {
        Student student = new StudentBuilder()
                .id(20173212)
                .name("HARRY")
                .grade("A")
                .phoneNumber("010-1234-5678")
                .build();

        System.out.println(student);

        Student student2 = new StudentBuilder()
                .id(20183302)
                .name("TOM")
                .build();

        System.out.println(student2);
    }

Student { id='20173212', name=HARRY, grade=A, phoneNumber=010-1234-5678 }
Student { id='20183302', name=TOM, grade=null, phoneNumber=010 }

빌더 패턴 장단점

장점

  1. 객체 생성 과정을 명확히 표현
    생성자 방식으로 객체를 생성하는 경우 매개변수가 많아질수록 가독성이 나빠진다.
    빌더 패턴을 적용하면 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한눈에 파악 가능하다.

  2. 데이터 순서 상관없이 객체 생성 가능
    Setter 메소드 호출을 통해 필드 값을 주입하기 때문에 순서는 상관 없다.

  3. 필수 필드와 선택적 필드 분리 가능
    빌더 클래스를 통해 초기화가 필수인 필드는 빌더의 생성자로 받게 하여 빌더 객체가 생성되도록 유도하고, 선택적인 멤버는 빌더의 메서드로 받도록 한다.

class StudentBuilder {
    // 초기화 필수 멤버
    private int id;

    // 초기화 선택적 멤버
    private String name;
    private String grade;
    private String phoneNumber;

    // 필수 멤버는 빌더의 생성자를 통해 설정
    public StudentBuilder(int id) {
        this.id = id;
    }

    // 나머지 선택 멤버는 메서드로 설정
    public StudentBuilder name(String name) {
        this.name = name;
        return this;
    }
    
   	...
    
}
Student student1 = 
        new StudentBuilder(20173212) // 필수 멤버
        .name("HARRY") // 선택 멤버
        .build();

Student student2 = 
        new StudentBuilder(21082325) // 필수 멤버
        .name("TOM") // 선택 멤버
        .grade("A") // 선택 멤버
        .build();

단점

  1. 코드 복잡성 증가
    빌더 패턴을 적용하려면 N개의 클래스에 대해 N개의 새로운 빌더 클래스를 생성해야 한다.

  2. 생성자 보다 성능 저하
    매번 메소드를 호출하여 빌더를 거쳐 객체 생성하기 때문이다.


Lombok의 @Builder

개발자가 좀더 편하게 빌더 패턴을 이용하기 위헤 Lombok에서는 별도의 어노테이션을 지원한다.
클래스에 @Builder 어노테이션만 붙여주면 클래스를 컴파일 할 때 자동으로 클래스 내부에 빌더 API가 만들어진다.

@Builder
@AllArgsConstructor
@ToString
class Student {
   	private int id;
    private String name;
    private String grade;
    private String phoneNumber;
}
  1. @Builder : Student 빌더 클래스와 이를 반환하는 builder() 메서드 생성
  2. @AllArgsConstructor : 전체 인자를 갖는 생성자를 자동으로 생성
  3. @ToString : toString() 메서드 자동 생성

필수 파라미터 빌더 구현

@Builder 어노테이션으로 빌더 패턴을 구현하면 필수 파라미터 적용을 지정해줄수가 없다.
대상 객체 안에 별도의 builder() 정적 메서드를 구현함으로써, 빌더 객체를 생성하기 전에 필수 파라미터를 설정하도록 유도할수 있고, 또한 파라미터 검증 로직도 추가해줄 수 있다.

@Builder
@AllArgsConstructor
@ToString
class Student {
   	private int id;
    private String name;
    private String grade;
    private String phoneNumber;

    // 필수 파라미터 빌더 메서드 구현
    public static StudentBuilder builder(String name, String age) {
        // 빌더의 파라미터 검증
        if(id == null || name == null){
            throw new IllegalArgumentException("필수 파라미터 누락");
        }

        return new StudentBuilder().id(id).name(name);
    }
}
profile
BackEnd Developer

0개의 댓글