본문 바로가기

항해 99/Java

빌터 패턴

Builder Pattern(빌더 패턴)

복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴.

생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식

 

탄생 배경

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

 

문제점

  • 클래스 인스턴스 필드들이 많아질 수록 생성자에 들어갈 인자의 수가 늘어나 몇 번째 인자가 어떤 필드였는지 햇갈릴 경우가 발생
  • 생성자로만 필드를 선택적으로 생략할 수 있는 방법이 없음
  • 타입이 다양할 수록 생성자 메서드 수가 기하급수적으로 늘어나 가독성이나 유지보수 측면에서 나쁨

 

자바 빈(Java Beans) 패턴 : 점층적 생성자 패턴의 단점을 보완하기 위해 Setter 메서드를 사용해서 매개 변수가 없는 생성자로 객체 생성 후 클래스 필드의 초기 값을 설정하는 방식

 

문제점

  • 일관성 문제
    • 필수 매개변수를 개발자가 깜박하고 Setter 메서들 호출하지 않을 경우, 객체는 일관성이 무너진 상태가 됨
  • 불변성 문제
    • 객체를 생성한 후에도 외부적으로 Setter 메서드를 호출해 객체를 조작할 수 있어 불변성을 보장할 수 없음

빌더 패턴

위 두 패턴의 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 메서드를 통해 step-by-step으로 값을 입력받은 후에 최종적으로 build() 메서드로 하나의 인스턴스를 생성하여 리턴하는 패턴

 

 

구조

Student 클래스에 대한 객체 생성만을 담당하는 별도의 빌더 클래스를 만들려고 할 때

class Student {
    private int id;
    private String name = "아무개";
    private String grade = "freshman";
    private String phoneNumber = "010-0000-0000";

    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 +
                " }";
    }
}

 

빌더 클래스 구현

Builder 클래스를 만들고 필드 멤버 구성을 만들고자 하는 Student 클래스 멤버 구성과 똑같이 구성

class StudentBuilder {
    private int id;
    private String name;
    private String grade;
    private String phoneNumber;
    
}

 

각 멤버에 대한 Setter 메서드를 구현, 기존 Setter와 차이를 주기 위해 set 단어를 빼고 멤버이름으로만 메서드명을 설정

class StudentBuilder {
    private int id;
    private String name;
    private String grade;
    private String phoneNumber;

    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;
    }
}

 

각 Setter 함수 마지막 반환 구분인 return this의 this란 Student Builder 객체 자신을 말함, 빌더 객체 자신을 리턴함으로써 메서드 호출 후 연속적으로 빌더 메서드들을 체이닝(Chaining)하여 호출할 수 있게 됨.

 

마지막으로 Student 객체를 만들어 주는 build 메서드를 구성(빌더 클래스의 필드들을 Student 생성자의 인자에 넣어줌으로써 멤버 구성이 완료 된 Studnet 인스턴스를 얻음)

class StudentBuilder {
    private int id;
    private String name;
    private String grade;
    private String phoneNumber;

    public StudentBuilder id(int id) { ... }

    public StudentBuilder name(String name) { ... }

    public StudentBuilder grade(String grade) { ... }

    public StudentBuilder phoneNumber(String phoneNumber) { ... }

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

 

 

빌더 클래스 실행

public static void main(String[] args) {

    Student student = new StudentBuilder()
                .id(2016120091)
                .name("임꺽정")
                .grade("Senior")
                .phoneNumber("010-5555-5555")
                .build();

    System.out.println(student);
}

// 실행 결과
Student {id='2016120091', name=임꺽정, grade=Senior)

 

 

빌터 패턴 네이밍 형식

  • 멤버이름( ) : 추천
  • set멤버이름( ) : Setter 메서드와 혼동될 가능성 있음
  • with멤버이름( ) : 빌더 지연 생성 방식에서 미리 빌더를 설정할 때 쓰임
//1.
Student student = new StudentBuilder(2016120091)
        .name("홍길동")
        .grade("freshman")
        .phoneNumber("010-5555-5555")
        .build();
        
//2.
Student student = new StudentBuilder(2016120091)
        .setName("홍길동")
        .setGrade("freshman")
        .setPhoneNumber("010-5555-5555")
        .build();
        
//3.
Student student = new StudentBuilder(2016120091)
        .setName("홍길동")
        .setGrade("freshman")
        .setPhoneNumber("010-5555-5555")
        .build();

 

 

Builder 패턴 장단점

장점

1. 객체 생성 과정을 일관된 프로세스로 표현

  • 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한 눈에 파악할 수 있게 됨, 연속된 동일 타입의 매개 변수를 많이 설정할 경우에 발생할 수 있는 설정 오류와 같은 실수를 방지할 수 있음
  • 최근에는 IDE에서 생성자 매개변수에 대한 미리보기 힌트 기능을 제공해주기 때문에 요즘 트렌드에는 맞지 않을 수도 있음

 

2. 디폴트 매개변수 생략을 간접적으로 지원

  • 자바는 기본적으로 메서드에 대한 디폴트 매개변수를 지원하지 않음
  • 빌더라는 객체 생성 전용 클래스를 경유하여 이용함으로써 디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식으로 디폴트 매개변수를 생략하고 호출하는 효과를 간접적으로 구현할 수 있게 된다.
class StudentBuilder {
    private int id;
    private String name;
    private String grade = "freshman"; // 디폴트 매개변수 역할
    private String phoneNumber;

    ...
}

// 디폴트 필드인 grade를 제외하고 빌더 구성 및 인스턴스화
Student student1 = new StudentBuilder(2016120091)
        .name("홍길동")
        .phoneNumber("010-5555-5555")
        .build();

System.out.println(student1);

 

 

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;
    }

    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 student1 = 
        new StudentBuilder(2016120091) // 필수 멤버
        .name("홍길동") // 선택 멤버
        .build();

Student student2 = 
        new StudentBuilder(2016120091) // 필수 멤버
        .name("임꺽정") // 선택 멤버
        .grade("freshman") // 선택 멤버
        .build();

Student student3 = 
        new StudentBuilder(2016120091) // 필수 멤버
        .name("주몽") // 선택 멤버
        .grade("Senior") // 선택 멤버
        .phoneNumber("010-5555-5555") // 선택 멤버
        .build();

 

 

4. 객체 생성 단계를 지연할 수 있음

  • 객체 생성을 단계별로 구성하거나 구성 단계를 지연하거나 재귀적으로 생성을 처리할 수 있다(빌더를 재사용함으로써 객체 생성을 주도적으로 지연할 수 있는 것이다).
// 1. 빌더 클래스 전용 리스트 생성
List<StudentBuilder> builders = new ArrayList<>();

// 2. 객체를 최종 생성 하지말고 초깃값만 세팅한 빌더만 생성
builders.add(
    new StudentBuilder(2016120091)
    .name("홍길동")
);

builders.add(
    new StudentBuilder(2016120092)
    .name("임꺽정")
    .grade("senior")
);

builders.add(
    new StudentBuilder(2016120093)
    .name("박혁거세")
    .grade("sophomore")
    .phoneNumber("010-5555-5555")
);

// 3. 나중에 빌더 리스트를 순회하여 최종 객체 생성을 주도
for(StudentBuilder b : builders) {
    Student student = b.build();
    System.out.println(student);
}

 

 

5. 초기화 검증을 멤버 별로 분리

빌더를 이용하면 생성될 객체의 멤버 변수의 초기화와 검증을 각각의 멤버별로 분리해서 작성할 수 있다. 빌더의 각각의멤버 설정 메서드에서 검증 과정을 분담함으로써 유지 보수를 용이하게 하는 것임

class StudentBuilder {
	...

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

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

    public StudentBuilder grade(String grade) {
        if (!grade.equals("freshman") && !grade.equals("sophomore") && !grade.equals("junior") && !grade.equals("senior")) {
            throw new IllegalArgumentException(grade);
        }
        this.grade = grade;
        return this;
    }

    public StudentBuilder phoneNumber(String phoneNumber) {
        if (!phoneNumber.startsWith("010")) {
            throw new IllegalArgumentException(phoneNumber);
        }
        this.phoneNumber = phoneNumber;
        return this;
    }

    public Student build() {
        return new Student(id, name, grade, phoneNumber);
    }
}

 

 

6. 멤버에 대한 변경 가능성 최소화를 추구

Java Beans Pattern인 Setter 메서드를 통해 멤버 초기화를 하지 말아야 하는 이유: 클래스 맴버 초기화를 Setter을 통해 구성하는 것은 매우 좋지 않은 방법임.

프로그램을 개발하는데 있어 다른 사람과 협업할때 가장 중요시되는 점 중 하나가 바로 불변(immutalbe) 객체임(불변 객체란 객체 생성 이후 내부의 상태가 변하지 않는 객체).

 

불변 객체의 필요성

  1. 불변 객체는 Thread-Safe 하여 동기화를 고려하지 않아도 된다.
  2. 만일 가변 객체를 통해 작업을 하는 도중 예외(Exception)가 발생하면 해당 객체가 불안정한 상태에 빠질 수 있어 또 다른 에러를 유발할 수 있는 위험성이 있기 때문이다.
  3. 불변 객체로 구성하면 다른 사람이 개발한 함수를 위험없이 이용을 보장할 수 있어 협업에도 유지보수에도 유용하다.

클래스들은 가변적 이여야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변으로 만들어야 한다. 만약 클래스를 불변으로 만드는 것이 불가능하다면 가능한 변경 가능성을 최소화해야 한다.

 

Setter 메서드 자체를 구현하지 않음으로서 불변 객체를 간접적으로 구성이 가능, 빌더 패턴은 생성자 없이 어느 객체에 대해 '변경 가능성을 최소화' 를 추구하여 불변성을 갖게 해주게 되는 것

 

 

단점

1. 코드 복잡성 증가

  • 빌더 패턴을 적용하려면 N개의 클래스에 대해 N개의 새로운 빌더 클래스를 만들어야 해서, 클래수 수가 기하급수적으로 늘어나 관리해야 할 클래스가 많아지고 구조가 복잡해질 수 있음
  • 선택적 매개변수를 많이 받는 객체를 생성하기 위해서는 먼저 빌더 클래스부터 정의해야 함

 

2. 생성자 보다는 성능은 떨어진다.

  • 생성 비용 자체는 크지는 않지만, 어플리케이션의 성능을 극으로 중요시되는 상황이라면 문제가 될수 있다.

 

3. 지나친 빌더 남용은 금지

  • 클래스의 필드의 개수가 4개 보다 적고, 필드의 변경 가능성이 없는 경우라면 차라리 생성자나 정적 팩토리 메소드Visit Website를 이용하는 것이 더 좋을 수 있다. 
  • 빌더 패턴의 코드가 다소 장황하기 때문에 클래스 필드의 갯수와 필드 변경 가능성을 중점으로 보고 패턴을 적용 유무를 가려야한다.
  • API 는 시간이 지날수록 많은 매개변수를 갖는 경향이 있기 때문에 애초에 빌더 패턴으로 시작하는 편이 나을 때가 많다고 말하는 경향도 있다.

 

 

Builder 디자인 패턴 종류

  1. 이펙티브 자바의 빌더 패턴 : 생성시 지정해야 할 인자가 많을때 사용. 객체의 일관성 불변성이 목적.
  2. GoF의 빌더 패턴 : 객체의 생성 단계 순서를 결정해두고 각 단계를 다양하게 구현하고 싶을때 사용.

 

심플 빌더 패턴 (Effective Java)

개발자들이 빌더 패턴을 말할 때 정의되는 것이 이펙티브 자바에서 소개한 빌더 패턴이다. GOF 빌더 패턴과 구분하기 위해 심플 빌더 패턴(Simple Builder Pattern) 이라고도 불림

 

심플 빌더 패턴은 생성자가 많을 경우 또는 변경 불가능한 불변 객체가 필요한 경우 코드의 가독성과 일관성, 불변성을 유지하는 것에 중점을 둠(앞서 배운 빌터 패턴과 빌더(Builder) 클래스가 구현할 클래스의 정적 내부 클래스 (Static Inner Class)Visit Website로 구현된다는 점이 다름)

 

빌더 클래스가 static inner class 구현되는 이유

  1. 하나의 빌더 클래스는 하나의 대상 객체 생성만을 위해 사용된다. 그래서 두 클래스를 물리적으로 그룹핑함으로써 두 클래스간의 관계에 대한 파악을 쉽게 할 수 있다.
  2. 대상 객체는 오로지 빌더 객체에 의해 초기화 된다. 즉, 생성자를 외부에 노출시키면 안되기 때문에 생성자를 private로 하고, 내부 빌더 클래스에서 private 생성자를 호출함으로써 오로지 빌더 객체에 의해 초기화 되도록 설계 할 수 있다.
  3. 정적 내부 클래스는 외부 클래스의 인스턴스 없이도 생성할 수 있는데, 만일 일반 내부 클래스로 구성한다면 내부 클래스를 생성하기도 전에 외부 클래스를 인스턴스화 해야 한다.
    • 빌더가 최종적으로 생성할 클래스의 인스턴스를 먼저 생성해야 한다면 모순이 생기기 때문

 

Simple 빌더 패턴 구현

  1. 빌더 클래스를 Static Nested Class로 정의
  2. 빌더를 통해 인스턴스화 하기 때문에 대상 객체 생성자는 private로 정의
  3. 빌더 클래스의 생성자는 public으로 하며, 필수 파라미터에 대해 생성자의 파라미터로 받음
  4. 선택적 파라미터에 대해서는 메서드로 제공(메서드의 반환값은 객체 자신(this))
  5. 최종 객체를 생성하는 build( ) 메서드를 정의하여 client에 최종 생성된 결과물을 제공
  6. 생성자의 인수로 빌더 인스턴스 자기자신을 전달하고, 대상 객체 생성자에서 빌더 인스턴스의 필드를 각각 대입
class Person {
    // final 키워드로 필드들을 불변 객체로 만든다.
    private final String name;
    private final String age;
    private final String gender;
    private final String job;
    private final String birthday;
    private final String address;

    // 정적 내부 빌더 클래스
    public static class Builder {

        // 필수 파라미터
        private final String name;
        private final String age;

        // 선택 파라미터
        private String gender;
        private String job;
        private String birthday;
        private String address;

        // 필수 파라미터는 빌더 생성자로 받게 한다
        public Builder(String name, String age) {
            this.name = name;
            this.age = age;
        }

        // 선택 파라미터는 각 메서드를 통해 정의한다
        public Builder gender(String gender) {
            this.gender = gender;
            return this;
        }

        public Builder job(String job) {
            this.job = job;
            return this;
        }

        public Builder birthday(String birthday) {
            this.birthday = birthday;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        // 대상 객체의 private 생성자를 호출하여 최종 인스턴스화 한다
        public Person build() {
            return new Person(this); // 빌더 객체 자신을 넘긴다.
        }
    }

    // private 생성자 - 생성자는 외부에서 호출되는것이 아닌 빌더 클래스에서만 호출되기 때문에
    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.gender = builder.gender;
        this.job = builder.gender;
        this.birthday = builder.birthday;
        this.address = builder.address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", gender='" + gender + '\'' +
                ", job='" + job + '\'' +
                ", birthday='" + birthday + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
public static void main(String[] args) {

    Person person = new Person
            .Builder("홍길동", "26") // static inner class 초기화 (필수 파라미터)
            .gender("man") // 선택 파라미터
            .job("Warrior")
            .birthday("1800.10.10")
            .address("조선")
            .build();

    System.out.println(person);
}

 

 

 

디렉터 빌더 패턴(GOF)

복잡한 객체의 생성 알고리즘과 조립 방법을 분리하여 빌드 공정을 구축하는것이 목적(빌더를 받아 조립 방법을 정의한 클래스를 Director라 함)

 

심플 빌더 패턴은 하나의 대상 객체에 대한 생성만을 목적을 두지만, 디렉터 빌더 패턴은 여러가지의 빌드 형식을 유연하게 처리하는 것에 목적을 둠(일반적인 빌더 패턴을 고도화 시킨 패턴이라 볼 수 있음)

 

Director 빌더 패턴 구조

  • Builder : 빌더 추상 클래스
  • ConcreteBuilder : Builder의 구현체, Product 생성을 담당
  • Director : Builder에서 제공하는 메서드들을 사용해 정해진 순서대로 Product 생성하는 프로세스를 정의
  • Product : Director가 Builder로 만들어낸 결과물

클라이언트가 직접 빌더의 모든 API를 사용하는 게 아닌, Director을 통해 간단하게 인스턴스를 얻어올 수 있고 코드를 재사용할 수 있도록 함

 

 

Director 빌더 패턴 구현

일반적인 자바 데이터를 저장하고 있는 Data 객체를 Builder 인터페이스를 통해 적절한 문자열 포맷으로 변환하는 예제

  • PlainTextBuilder : Data 인스턴스의 데이터들을 평이한 텍스트 형태로 만드는 API
  • JSONBuilder : Data 인스턴스의 데이터들을 JSON 형태로 만드는 API
  • XMLBuilder : Data 인스턴스의 데이터들을 XML 형태로 만드는 API

각 문자열 포맷으로 변환하는 프로세스를 Director 객체가 담당하게 된다. 클라이언트는 Director를 이용하여 간단한 메서드 호출만으로 복잡한 빌드 공정 과정을 알필요없이 결과물을 얻을 수 있음

 

코드

class Data {
    private String name;
    private int age;

    public Data(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

abstract class Builder {
    // 상속한 자식 클래스에서 사용하도록 protected 접근제어자 지정
    protected Data data;

    public Builder(Data data) {
        this.data = data;
    }

    // Data 객체의 데이터들을 원하는 형태의 문자열 포맷을 해주는 메서드들 (머리 - 중간 - 끝 형식)
    public abstract String head();
    public abstract String body();
    public abstract String foot();

}

 

// Data 데이터들을 평범한 문자열로 변환해주는 빌더
class PlainTextBuilder extends Builder {

    public PlainTextBuilder(Data data) {
        super(data);
    }

    @Override
    public String head() {
        return "";
    }

    @Override
    public String body() {
        StringBuilder sb = new StringBuilder();

        sb.append("Name: ");
        sb.append(data.getName());
        sb.append(", Age: ");
        sb.append(data.getAge());

        return sb.toString();
    }

    @Override
    public String foot() {
        return "";
    }
}

// Data 데이터들을 JSON 형태의 문자열로 변환해주는 빌더
class JSONBuilder extends Builder {

    public JSONBuilder(Data data) {
        super(data);
    }

    @Override
    public String head() {
        return "{\n";
    }

    @Override
    public String body() {
        StringBuilder sb = new StringBuilder();

        sb.append("\t\"Name\" : ");
        sb.append("\"" + data.getName() + "\",\n");
        sb.append("\t\"Age\" : ");
        sb.append(data.getAge());

        return sb.toString();
    }

    @Override
    public String foot() {
        return "\n}";
    }
}

// Data 데이터들을 XML 형태의 문자열로 변환해주는 빌더
class XMLBuilder extends Builder {
    public XMLBuilder(Data data) {
        super(data);
    }

    @Override
    public String head() {
        StringBuilder sb = new StringBuilder();

        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
        sb.append("<DATA>\n");

        return sb.toString();
    }

    @Override
    public String body() {
        StringBuilder sb = new StringBuilder();

        sb.append("\t<NAME>");
        sb.append(data.getName());
        sb.append("<NAME>");
        sb.append("\n\t<AGE>");
        sb.append(data.getAge());
        sb.append("<AGE>");

        return sb.toString();
    }

    @Override
    public String foot() {
        return "\n</DATA>";
    }
}

 

// 각 문자열 포맷 빌드 과정을 템플릿화 시킨 디렉터
class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    // 일종의 빌드 템플릿 메서드라 보면 된다
    public String build() {
        StringBuilder sb = new StringBuilder();

		// 빌더 구현체에서 정의한 생성 알고리즘이 실행됨
        sb.append(builder.head());
        sb.append(builder.body());
        sb.append(builder.foot());

        return sb.toString();
    }
}

public static void main(String[] args) {
    // 1. 포맷할 자바 데이터 생성
    Data data = new Data("홍길동", 44);

    // 2. 일반 텍스트로 포맷하여 출력하기
    Builder builder1 = new PlainTextBuilder(data);
    Director director1 = new Director(builder1);
    String result1 = director1.build();
    System.out.println(result1);

    // 3. JSON 형식으로 포맷하여 출력하기
    Builder builder2 = new JSONBuilder(data);
    Director director2 = new Director(builder2);
    String result2 = director2.build();
    System.out.println(result2);

    // 4. XML 형식으로 포맷하여 출력하기
    Builder builder3 = new XMLBuilder(data);
    Director director3 = new Director(builder3);
    String result3 = director3.build();
    System.out.println(result3);
}
  • Director는 템플릿화 한 메서드를 통해 일관된 프로세스로 인스턴스를 만드는 빌드 과정을 단순화 하고, 클라이언트 쪽에선 Director가 제공하는 메서드를 호출하므로써 코드를 재사용할 수 있게 됨
  • 빌더는 부품을 만들고, Director는 빌더가 만든 부품을 조합해 제품을 만든다고 할 수 있음

 

GOF의 빌더 패턴은 여러 디자인 패턴의 짬뽕

지금까지 구현해본 GOF의 디렉터 빌더 패턴은 어찌 보면 빌드 과정을 추상화 및 단순화한 Facade 패턴Visit Website과 빌드 과정 코드를 템플릿화한 Template Method 패턴Visit Website 그리고 원하는 문자열 형식의 각 빌드 전략 알고리즘을 정의한 Strage 패턴Visit Website을 짬뽕 시킨 디자인 패턴이라고 볼수도 있다.

 

 

Lombok의 @Builder

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

 

빌더 어노테이션 구현하기

  • @Builder : PersonBuilder 빌더 클래스와 이를 반환하는 builder() 메서드 생성
  • @AllArgsConstructor(access = AccessLevel.PRIVATE) : @Builder 어노테이션을 선언하면 전체 인자를 갖는 생성자를 자동으로 만드는데, 이를 private 생성자로 설정
  • @ToString : toString() 메서드 자동 생성
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.ToString;

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
class Person {
    private final String name;
    private final String age;
    private final String gender;
    private final String job;
    private final String birthday;
    private final String address;
}

public static void main(String[] args) {
    Person person = Person.builder()
            .name("홍길동")
            .age("26")
            .gender("man") // 선택 파라미터
            .job("Warrior")
            .birthday("1800.10.10")
            .address("조선")
            .build();
}

 

 

필수 파라미터 빌더 구현하기

@Builder 어노테이션으로 빌더 패턴을 구현하면 필수 파라미터 적용을 지정해줄수가 없다.

대상 객체 안에 별도의 builder() 정적 메서드를 구현함으로써, 빌더 객체를 생성하기 전에 필수 파라미터를 설정하도록 유도할수 있고, 또한 파라미터 검증 로직도 추가해줄 수 있다.

 

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
class Person {
    private final String name;
    private final String age;
    private final String gender;
    private final String job;
    private final String birthday;
    private final String address;

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

        // 필수 파라미터를 미리 빌드한 빌더 객체를 반환 (지연 빌더 원리)
        return new PersonBuilder().name(name).age(age);
    }
}

public static void main(String[] args) {

    Person person = Person.builder("홍길동", "26") // 필수 파라미터
            .gender("man") // 선택 파라미터
            .job("Warrior")
            .birthday("1800.10.10")
            .address("조선")
            .build();

    System.out.println(person);
}

 

 

실무에서 사용하는 Builder 패턴

Java

  • java.lang.StringBuilder의 append()
  • java.lang.StringBuffer의 append()
  • java.nio.ByteBuffer의 put() - CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer 도 마찬가지
  • javax.swing.GroupLayout.Group의 addComponent()
  • java.lang.Appendable의 구현체
  • java.util.stream.Stream.Builder

StringBuilder
빌더에 해당하는 StringBuilder를 생성하고, 빌더가 제공하는 append 메서드로 파라미터를 구성하고, 최종적으로 toString을 호출해서 String 객체를 생성하는 일련의 과정이 빌더 패턴임

public static void main(String[] args) {
    String result = new StringBuilder()
            .append("hello ")
            .append("world!")
            .toString(); // build()

    System.out.println(result);
}



StreamBuilder
Stream API가 제공하는 StreamBuilder도 마찬가지이다. StreamBuilder를 사용하면 Stream에 들어갈 요소를 add 할 수 있고, 최종적으로 build를 호출해서 stream 객체를 생성

public static void main(String[] args) {
    Stream.Builder<String> stringBuilder = Stream.builder();
    Stream<String> stream = stringBuilder.add("hello").add("world!").add("bye..").build();

    stream.forEach(System.out::println);
}

 

 

Spring Framework
스프링 프레임워크에선 UriComponents 인스턴스를 Uricomponentsbuilder를 통해서 만들 수 있다. uri를 하드코딩해서 만드는 것보다 안전하게 만들 수 있다.

public static void main(String[] args) {
    UriComponents uriComponents = UriComponentsBuilder.newInstance()
            .scheme("https")
            .host("kangworld.tistory.com")
            .build();

    System.out.println(uriComponents);
}

 

 

빌터 패턴을 적용할 필요가 없는 예외

  • 객체의 생성을 라이브러리로 위임하는 경우
    • 엔티티(Entity) 객체나 도메인(Domain) 객체로부터 DTO를 생성하는 경우라면 직접 빌더를 만들고 하는 작업이 번거로우므로 MapStruct나 Model Mapper와 같은 라이브러리를 통해 생성을 위임할 수 있음
  • 변수의 개수가 2개 이하이며, 변경 가능성이 없는 경우
    • 변수가 늘어날 가능성이 거의 없으며, 변수의 개수가 2개 이하인 경우에는 정적 팩토리 메소드를 사용하는 것이 더 좋을 수도 있음

변수의 개수와 변경 가능성 등을 중점적으로 보고 빌더 패턴을 적용할지 판단

'항해 99 > Java' 카테고리의 다른 글

WIL-4  (0) 2024.03.03
객체지향 생활체조 9가지 원칙  (1) 2024.03.01
WIL - 3  (0) 2024.02.25
프로그래밍 테스트 및 IDE 프로젝트 빌드 오류 해결  (0) 2024.02.19
Java - 모던 자바  (0) 2024.02.19