JAVA

빌더 패턴(Builder pattern)을 써야할까?, @Builder

S_N_Y 2024. 2. 23. 10:18

 

#0 여는 말 : @AllArgsConstructor를 대체할 방법은?

@AllrArgsConstructor은 모든 필드값을 순서대로 파라미터로 받는 생성자이다.

그러나 필드 순서대로 생성자가 생성됨으로써 코드상 필드 순서를 바꿔도 적용되지 않아 실제 넣고싶은 값이 잘못들어가거나 파라미터에 넣을 값이 길어지면 파라미터에 각각 어떤 값이 들어가는지 개발자들이 바로 알 수가 없다.

이걸 대체할 방법이 무엇일까?

=> 바로 파라미터의 순서가 아닌 이름으로 값을 설정하는 빌더(Builder)이다

 

#1 빌더 패턴(Builder pattern) 이란?🤔 

(1) 빌더의 정의와 간단한 설명

기존의 복잡한 객체의 생성하는 과정과 표현하는 방법을 따로 분리시켜 다양한 구성의 인스턴스를 만드는 생성 패턴이다. 쉽게 말해서 생성자에 들어갈 파라미터 변수를 메서드로 하나하나 나열하고 객체를 생성할 수 있는 빌더를 bulder()함수를 통해 얻고 거기에 세팅하고자 하는 값을 세팅한 후에 build()를 통해 빌더를 작동시켜서 객체를 생성한다.

 

(2) 기본 생성자 패턴과 자바 빈 패턴의 단점을 몇 가지 해결

기본 생성자 패턴과 자바 빈 패턴을 썼을 때, 다음과 같은 문제점들이 존재한다.

기본 생성자 패턴은 간단하게 말해 인스턴스 필드들이 많아지면 해당 값이 어디에 들어가는지 헷갈려진다.

 

그리고 자바 빈 패턴의 문제는 일관성 문제와 불변성 문제가 있다. 

- 일관성 문제 : 필수 매개변수는 객체가 초기화될 때에 반드시 설정되어 있는 값인데 개발자가 깜박하고 setBun() 같은 메서드를 호출하지 않으면 이 객체는 일관성이 무너진 상태(객체가 유효하지 않은 상태)가 되어버린다. 어느던도 생성자와 결합해서 극복은 할 수 있지만 아래의 불변성 문제 때문에 자바 빈즈 패턴은 지양해야 한다.

- 불변성 문제 : setter의 경우, 객체를 생성했음에도 저것 때문에 여전히 외부적으로 setter메서드를 노출하고 있어서 협업 과정에서 누군가가가 setter를 호출하여 함부로 객체를 조작할 수 있어 매우 위험하다

 

해결하는 방법 - 빌더패턴 등장

인자 수가 많을 때의 가독성 완화

아까 위에서도 잠깐 설명했듯이 인스턴스 필드들이 많으면 많을수록 생성자에 들어갈 인자의 수가 늘어나버리면 몇번째 인자가 어떤 필드였는지 헷갈리는 경우가 많이 생긴다. 빌더를 사용하면 이름 함수로 세팅이 되어서 각각 무슨 값을 의미하는지 파악하기 쉽다.

유연하게 인자값 받기

기존 생성자의 파라미터값은 그 순서에 맞게 값을 넣어야 했다. 그러나 빌더패턴 코드를 보면 햄버거 쌓아가듯이 인스턴스를 쌓아가는데 버거의 재료 순서가 항상 동일하게 쌓아가지 않듯이 속재료들을 보다 유연하게 받아서 다양한 타입의 인스턴스를 쌓아갈 수 있다. 그래서 클래스의 선택적인 파라미터 변수가 많은 상황에서 보다 더 유용하게 사용할 수 있다.

 

#2 빌더 패턴(Builder pattern) 의 구성과 쓰는 방법

빌더 패턴의 구성은 다음과 같다.

class StudentBuilder {
    private int id; // 필수적으로 받아야할 정보
    private String name; // 아래는 선택적으로 받아도 되는 정보들
    private String grade;
    private String phoneNumber;

    public StudentBuilder id(int id) { // 필수변수는 생성자로 값을 넣는다
        this.id = id;
        return this;
    }

    // 선택변수들은 객체를 return하는 것까지
    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 생성자 호출
    }
    }
public static void main(String[] args) {

    Student student = new StudentBuilder()
                .id(2016120091) // 필수값 입력
                .name("임꺽정") // 나머지는 선택적으로..!
                .grade("Senior")
                .phoneNumber("010-5555-5555")
                .build();
}

// 네이밍 형식은 예를 들어 위의 .name의 경우, .name도 있고 .setName도 있고 .withName도 있다

 

#3 빌더 패턴의 장단점이 있다면? - 무조건 좋은 것이 아니다🙅

좋은 점을 간단하게 위에서 나열했지만 무조건 좋다는 것은 아니다.

이유는 다음과 같이 정리해보려고 한다.

 

<장점>

1. 객체 생성 과정을 일관된 프로세스로 표현 : 매개변수가 많으면 가독성 ↓ - 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한 눈에 파악 가능하다. (근데 이젠 웬만한 IDE에서는 매개변수에 대한 미리보기 힌트 기능을 제공해줘서 이 장점은 그렇게 크지 않음)

2. 디폴트 매개변수를 설정 가능 : 빌더 패턴을 통해 초기화가 필수인 맴버는 빌더의 생성자로 받게 하여 필수 멤버를 성ㅇ정해주어야 빌더 객체가 생성되도록 유도할 수 있다. 위의 코드블럭 예시와 같이 선택적인 맴버도 설정 가능

3. setter메서드가 없으므로 변경 불가능한 객체 가능

4. 한 번에 객체를 생성하여 객체의 일관성을 유지

5. build() 함수 자체가 null인지 체크해줘서 검증이 가능

등등 여러가지 존재한다.

 

<단점>

1. 코드의 복잡해보일 수 있는 문제 : 빌더 패턴을 적용하려면 n개의 클래스에 대해 n개의 새로운 빌더 클래스를 만들어야 해서 클래스 수가 엄청엄청 많이 늘어나니 관리해야 할 클래스가 많아지고 구조가 복잡해질 수 있다. 다만 이 부분은 여느 다른 디자인 패턴이 가지는 단점이기도 하다..!

2. 생성자보다 성능이 떨어지는 문제 : 메번 메서드를 호출하여 빌더를 거쳐서 new로 인스턴스화하기 때문에 당연하게도 성능이 떨어질 수밖에 없다, 생성 비용 자체는 크지 않다고 들었는데 어플리케이션의 성능을 매우 중요시하는 플젝일 경우라면 문제가 될 수 있다고 한다.

3. 빌더 남용하지 말자 : 클래스의 필드 갯수가 5개 정도..이상 벗어나지 않는 적당한 필드 갯수이거나 필드의 변경 가능성이 없는 경우라면 차라리 생성자나 정적 팩토리 메서드를 이용하는 것이 더 좋을 수도 있다. 빌더는 코드가 많이 장황해지기 때문이다. (근데 API는 시간이 지날수록 많은 매개변수를 갖는 경향이 있어서 애초에 빌더 패턴으로 시작하는 편이 나을 때가 많다고 하는 글도 많았다)

++) 24.03.15 업데이트 : 빌더 어노테이션을 들어가보면 static으로 선언되어있는데 데이터 영역의 메모리가 신경쓰인다면 조심해야 한다. 그러나 빌더패턴 정도의 유의미한 메모리 차지가 있는지는 말이 많이 갈려서 해당 사항만 기억하고 사용하는 정도로 생각하면 좋다..!

 

#4@Builder

@builder : 빌더 클래스와 이를 반환하는 builder() 메서드 생성

위처럼 만든 빌더 패턴을 직접 만들지 않아도 롬복에서 지원하는 annotation 하나로 클래스를 생성할 수 있다.

클래스 또는 생성자 위에 @Builder를 붙여주면 빌더패턴 코드가 만들어진다.

@Builder
public class Person {
    private final String name;
    private final int age;
    private final int phone;
}

// ------------------------------

Person person = Person.builder() // 빌더어노테이션으로 생성된 빌더클래스 생성자
    .name("seungjin")
    .age(25)
    .phone(1234)
    .build();

 

+) 주의할 점 :

자동으로 시퀸스값을 증가시켜주는 필드도 있는데 붙여버리면 위험할 수 있으니 그런 부분에슨 빌더 에노테이션 붙이지 말기..!

 

 

참고 글 :

https://pamyferret.tistory.com/67

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%B9%8C%EB%8D%94Builder-%ED%8C%A8%ED%84%B4-%EB%81%9D%ED%8C%90%EC%99%95-%EC%A0%95%EB%A6%AC