본문 바로가기
IT/Java

[Java] Java Builder Pattern

by kyu-nahc 2024. 9. 12.
반응형

 

Builder Pattern

빌더 패턴(Builder Pattern)복잡한 객체를 생성하는 방법을 정의하는 클래스와

표현하는 방법을 정의하는 클래스를 별도로 분리하여,

서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴이다.

생성 패턴으로 인스턴스를 만드는 절차를 추상화한다.

특히 빌더 패턴은 많은 Optional 한 멤버 변수(혹은 파라미터)나

지속성 없는 상태 값들에 대해 처리해야 할 때 큰 장점을 가지고 있다.


자바에서는 점층적 생성자 패턴을 제공하는데,

이는 필수 매개변수와 함께 선택 매개변수를 0개, 1개, 2개 .. 받는 형태로,

우리가 다양한 매개변수를 입력받아 인스턴스를 생성하고 싶을 때

사용하던 생성자를 오버로딩 하는 방식이다.

만약 클래스가 인스턴스 필드들이 너무 많아질 경우 생성자에 들어갈

인자의 수가 늘어나 몇 번째 인자가 어떤 필드였는지 헷갈릴 경우가 생기게 된다. 

인스턴스가 늘어날수록 생성자 메소드 수가 늘어나고, 가독성과 유지보수 측면에도 힘들어진다.

 

이를 방지하기 위해 빌더 패턴이 탄생하게 되었다.

빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스 또는 

Nested 된 중첩 Static 클래스를 만들어 메소드를 통해 step-by-step으로

값을 입력받은 후에 최종적으로 build() 메소드로 하나의 인스턴스를 생성하여 리턴하는 패턴이다.

빌더 패턴을 이용하여 객체를 생성하는 예시 코드를 보면 다음과 같다.

 public static void main(String[] args) {
    Member member = new Member.MemberBuilder(1)
            .name("name")
            .userId("userId")
            .password("password")
            .email("email")
            .build();
}


빌더 패턴 사용법을 잠시 살펴보면,

MemberBuilder라는 빌더 클래스의 메서드를 체이닝(Chaining) 형태로 호출함으로써

인스턴스를 구성하고 마지막에 build() 메서드를 통해 최종적으로 객체를 생성하도록 되어있다.

빌더 패턴을 이용하면 더 이상 생성자 오버로딩 열거를 하지 않아도 되며,

데이터의 순서에 상관없이 객체를 만들어내 생성자 인자 순서를 파악할 필요도 없고

잘못된 값을 넣는 실수도 하지 않게 된다.

 

Builder Pattern 구조

생성할 객체와 객체 생성을 담당하는 클래스 코드는 다음과 같다.

이는 별도의 객체 생성 클래스 파일을 만드는 것이 아닌 중첩 Static Class를 이용하여 구성하였다.

Client 측에서 Member 변수를 직접 생성하지 못하도록 private 접근자를 붙여주고,

Client는 반드시 MemberBuilder를 이용해야만 Member 객체를 생성할 수 있다.

public class Member {
    private final int id;
    private String name;
    private String userId;
    private String password;
    private String email;

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", userId='" + userId + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
    // getter, setter
   
    private Member(MemberBuilder builder){
        this.id = builder.id;
        this.name = builder.name;
        this.userId = builder.userId;
        this.password = builder.password;
        this.email = builder.email;
    }

    public static class MemberBuilder {
        private final int id;
        private String name;
        private String userId;
        private String password;
        private String email;

        public MemberBuilder(int id){
            this.id = id;
        }
        public MemberBuilder name(String name){
            this.name = name;
            return this;
        }
        public MemberBuilder userId(String userId){
            this.userId = userId;
            return this;
        }
        public MemberBuilder password(String password){
            this.password = password;
            return this;
        }
        public MemberBuilder email(String email){
            this.email = email;
            return this;
        }
        public Member build(){
            return new Member(this);
        }
    }
}

 

먼저 final 변수이다.

Member 객체를 생성 시 반드시 객체 생성 초기에 할당해야 하는 값이 있을 수 있다.

이를 final로 선언하여 빌더의 생성자로 받게 하여

필수 인스턴스를 설정해 주어야 빌더 객체가 생성되도록 유도할 수 있다.

또한 경우에 따라 필요 없는 파라미터들에 대해 일일이 null 값을 넘겨줄 필요도 없다.

 public static class MemberBuilder {
        private final int id;
        private String name;
        private String userId;
        private String password;
        private String email;
        ... code
}

 

또한 위처럼 내부 중첩 클래스를 static으로 선언해야 이유는 다음과 같다.

정적 내부 클래스는 외부 클래스의 인스턴스 없이도 생성할 수 있는데,

일반 내부 클래스로 구성한다면 내부 클래스를 생성하기도 전에 외부 클래스를 인스턴스화해야 한다.

빌더가 최종적으로 생성할 클래스의 인스턴스를 먼저 생성해야 한다면 모순이 생긴다.

따라서 중첩 클래스로 구성 시 반드시 Staic으로 선언해주어야 한다.

  public MemberBuilder name(String name){
            this.name = name;
            return this;
        }
        public MemberBuilder userId(String userId){
            this.userId = userId;
            return this;
        }
        public MemberBuilder password(String password){
            this.password = password;
            return this;
        }
        public Member build(){
            return new Member(this);
        }
}

 

마지막으로 주목할 부분은 MemberBuilder의 메소드에서 마지막 반환 구문인 return this 부분이다.

여기서 this란 MemberBuilder 객체 자신을 말한다. 즉, 빌더 객체 자신을 리턴함으로써

메서드 호출 후 연속적으로 빌더 메서드들을 체이닝(Chaining) 하여 호출할 수 있게 된다. 

Chaining이 종료된 후에 마지막으로 빌더의 목표였던 최종 Member 객체를 만들어주는

build 메서드를 구성해 준다. 빌더 클래스의 필드들을 Member 생성자의 인자에 넣어줌으로써

멤버 구성이 완료된 Member인스턴스를 얻게 되는 것이다.

 

 

Builder Pattern 특징

패턴 장점

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

생성자 방식으로 객체를 생성하는 경우는 매개변수가 많아질수록 가독성이 급격하게 떨어진다.

클래스 변수가 많아지면 각 인자 순서마다 이 값이 어떤 멤버에 해당되는지 바로 파악이 힘들다. 

반면 다음과 같이 빌더 패턴을 적용할 경우 직관적으로 어떤 데이터에

어떤 값이 설정되는지 한눈에 파악할 수 있게 된다.

 

 

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

본래 디폴트 매개변수라는 건 인자 값을 설정해 줘도 되고 설정 안 하고 생략해도 되는 것을 말한다.

그런데 파이썬이나 자바스크립트와 달리,

자바 언어에선 기본적으로 메서드에 대한 디폴트 매개변수를 지원하지 않는다.

따라서 디폴트 매개변수를 구현하기 위해선 클래스 필드 변수에 초깃값을 미리 세팅하고, 

초깃값이 세팅된 필드 인자를 제외시킨 생성자를 따로 구현하는 식으로 설계해야 한다.

하지만 이는 결국 지나친 생성자 오버로딩 열거를 통한 본래의 문제점을 회귀한다.
빌더 패턴에서도 디폴트 매개변수를 구현하는 방법은 똑같다. 

다만 빌더라는 객체 생성 전용 클래스를 경유하여 이용함으로써,

디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식으로

마치 디폴트 매개변수를 생략하고 호출하는 효과를 간접적으로 구현할 수 있게 된다.

 


필수 멤버와 선택적 멤버를 분리 가능

객체 인스턴스는 목적에 따라 초기화가 필수인 멤버 변수가 있고 선택적인 멤버 변수가 있을 수 있다.

만일 Member 클래스의 id 필드가 인스턴스화할 때

반드시 필수적으로 값을 지정해 주어야 하는 필수 멤버 변수라고 가정해 보자.

이를 기존 생성자 방식으로 구현하려면 초기화가 필수인 멤버 변수만을 위한 생성자를 정의하고

선택적인 멤버 변수에 대응하는 생성자를 오버로딩을 통해 열거하거나,

혹은 전체 멤버를 인자로 받는 생성자만을 선언하고 매개변수에 null을 받는 식으로 구성하여야 한다.

따라서 빌더 클래스를 통해 초기화가 필수인 멤버는 빌더의 생성자로 받게 하여,

필수 멤버를 설정해 주어야 빌더 객체가 생성되도록 유도하고,

선택적인 멤버는 빌더를 이용하면, 사용자로 하여금

필수 멤버와 선택 멤버를 구분하여 객체 생성을 유도할 수 있다.

 

 

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

많은 개발자들이 자바 프로그래밍을 하면서 멤버에 값을 할당할 때

흔히 사용하는 것이 Setter 메서드인데,

그중 클래스 멤버 초기화를 Setter을 통해 구성하는 것은 매우 좋지 않은 방법이다.

 

일반적으로 프로그램을 개발하는 데 있어서

다른 사람과 협업할 때 가장 중요시되는 점 중 하나가 바로 불변(immutalbe) 객체이다.

불변 객체란 객체 생성 이후 내부의 상태가 변하지 않는 객체이다.

불변 객체는 오로지 읽기(get) 메소드만을 제공하며 쓰기(set)는 제공하지 않는다.

대표적으로 자바에서 final 키워드를 붙인 변수가 바로 불변이다. 

현업에서 불변 객체를 이용해 개발해야 하는 이유로는 다음과 같다.

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

따라서 클래스들은 가변적 이여야 하는

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

예를 들어 경우에 따라 변수에 final 키워드를 붙일 수 없는 상황이 생길 수도 있다.

이때는 Setter 메서드 자체를 구현하지 않음으로써 불변 객체를 간접적으로 구성이 가능하다.

그러면 결국은 생성자를 이용하라는 것인데 역시나 지나친 생성자 오버로딩 문제가 발생하게 된다.

그래서 연구된 것이 빌더 클래스이다.

 

즉, 최종 정리하자면 빌더 패턴은 생성자 없이 어느 객체에 대해

'변경 가능성을 최소화'를 추구하여 불변성을 갖게 해 주게 되는 것이다.

 

패턴 단점

코드 복잡성 증가

우선 빌더 패턴을 적용하려면 N개의 클래스에 대해 N개의 새로운 빌더 클래스를 만들어야 해서,

클래스 수가 기하급수적으로 늘어나 관리해야 할 클래스가 많아지고 구조가 복잡해질 수 있다.

또한 선택적 매개변수를 많이 받는 객체를 생성하기 위해서는 먼저 빌더 클래스부터 정의해야 한다.

 

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

매번 메서드를 호출하여 빌더를 거쳐 인스턴스화하기 때문에 성능이 떨어진다.

비록 생성 비용 자체는 크지는 않지만,

애플리케이션의 성능을 극으로 중요시되는 상황이라면 문제가 될 수 있다.

 

 

Builder Pattern 예시

Builder Pattern은 이미 많이 사용 중이며 어노테이션까지 나온 상태이다.

먼저 Lombok@Builder이다.

@Builder
@AllArgsConstructor
public class User {
    private String firstName;
    private String lastName;
    private String email;
    private int age;
}

 

개발자가 좀 더 편하게 빌더 패턴을 이용하기 위해

Lombok에서는 별도의 어노테이션을 지원하는 것이다.

클래스에 @Builder 어노테이션만 붙여준다면 클래스를 컴파일할 때,

자동으로 클래스 내부에 빌더 API가 만들어진다.
또한 아래 RestClient 객체를 생성할 때도 Builder Pattern이 적용된 것을 볼 수 있다.

 User user = restClient.get()
            .uri("http://localhost:5000/auth/member/test")
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .body(User.class);

 

위와 같이 Builder Pattern은 Spring framework에서도 많이 사용되며,

여러 장점이 존재하기에 객체 생성을 할 때 유용하게 사용할 수 있다.

따라서 객체를 생성할 때 간단한 경우이면 생성자나 정적 팩토리 메서드를 사용하여도 문제가 없지만

객체가 조금은 복잡해질경우 Builder Pattern을 이용하는 것을 고려해야 한다.

 

 

 

참고자료

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#spring_framework

https://charliezip.tistory.com/17

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Singleton Pattern  (2) 2024.09.14
[Java] Java Command Pattern  (0) 2024.09.13
[Java] Java Factory Pattern  (1) 2024.09.11
[Java] Java Decorator Pattern  (0) 2024.09.09
[Java] Java Observer Pattern  (7) 2024.09.08

loading