빌더 패턴(Builder pattern)이란?
객체를 정의하고 그 객체를 생성할 때 보통 생성자를 통해 생성하는 것을 생각한다.
Bag bag = new Bag("name", 1000, "memo");
하지만 생성자를 통해 객체를 생성하는데 몇 가지 단점이 있어 객체를 생성하는 별도 builder를 두는 방법이 있다. 이를 빌더 패턴이라고 한다.
Bag bag = Bag.builder()
.name("name")
.money(1000)
.memo("memo")
.build();
객체를 생성할 수 있는 빌더를 builder() 함수를 통해 얻고 거기에 셋팅하고자 하는 값을 셋팅하고 마지막에 build()를 통해 빌더를 작동 시켜 객체를 생성한다.
빌더를 써야하는 이유
객체를 생성하는 방법이 생성자말고 빌더를 통하는 것이 있다는 것을 알게 되었다.
그렇다면 이미 생성자라는 것이 있는데 빌더 패턴은 왜 생겼고 빌더 패턴을 왜 사용하는 것일까?
1. 생성자 파라미터가 많을 경우 가독성이 좋지 않다.
위에 예시에서 Bag 클래스는 생성자 파라미터를 3개만 받는다. 3개까지는 괜찮다. 하지만 생성자 파라미터로 받아야하는 값이 수업이 많아진다면? 각 값들이 어떤 값을 의미하는지 이해하기 힘들 것이다.
Bag bag = new Bag("name", 1000, "memo", "abc", "what", "is", "it", "?");
하지만 이를 빌더 패턴으로 구현하면 각 값들은 빌더의 각 값들의 이름 함수로 셋팅이 되지 각각 무슨 값을 의미하는지 파악하기 쉽다.
따라서 생성자로 설정해야하는 값이 많을 경우 빌더를 쓰는 것이 생성자보다 가독성이 좋다. 이는 같은 타입의 다른 변수의 값을 서로 바꿔 넣는 것을 방지할 수도 있다.
Bag bag = Bag.builder()
.name("name")
.money(1000)
.memo("memo")
.letter("This is the letter")
.box("This is the box")
.build();
2. 어떤 값을 먼저 설정하던 상관 없다
생성자의 경우는 정해진 파라미터 순서대로 꼭 값을 넣어줘야한다. 순서를 무시하고 값을 넣으면 에러가 발생하거나 엉뚱한데 값이 들어갈 수 있다.
public Bag(String name, int money, String memo) {
this.name = name;
this.money = money;
this.memo = memo;
}
하지만 빌더 패턴은 빌더의 필드 이름으로 값을 설정하기 때문에 순서에 종속적이지 않다.
그냥 쓰이는 곳에서 어떤 필드를 먼저 설정해야하는지 굳이 순서를 생각할 필요 없이 편하게 설정하면 된다.
Bag bag = Bag.builder()
.name("name")
.memo("memo") // memo를 money 대신 먼저!
.money(1000)
.build();
@Builder
위와 같은 이유로 생성자 대신 빌더를 사용하곤 하는데 그렇다면 빌더는 어떻게 구현해야하나?
우선 코드로 빌더를 만들고 그 안에서 멤버 필드별로 값을 설정하고 빌더를 반환하는 함수를 만들면 된다.
하지만 이 방법은 멤버 필드 별로 함수를 생성해야하는 등 생성자보다 더 번거롭고 해당 클래스에 들어갔을 때 빌더로만 몇십줄을 차지해서 코드 읽기도 불편하다. 그래서 나온 것인 @Builder 어노테이션이다.
@Builder는 룸북의 어노테이션으로 이런걸 보면 진짜 룸북 없었으면 어떡하나라는 생각을 들게 만든다.
@Builder 어노테이션의 javadoc을 보면 이 어노테이션은 아래와 같이 멤버 필드들에 대해 빌더를 만들어준다.
만일 이 어노테이션이 없었다면 After 부분을 일일이 직접 구현해줘야했다.
// Before:
@Builder
class Example<T> {
private T foo;
private final String bar;
}
// After:
class Example<T> {
private T foo;
private final String bar;
private Example(T foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public static <T> ExampleBuilder<T> builder() {
return new ExampleBuilder<T>();
}
public static class ExampleBuilder<T> {
private T foo;
private String bar;
private ExampleBuilder() {}
public ExampleBuilder foo(T foo) {
this.foo = foo;
return this;
}
public ExampleBuilder bar(String bar) {
this.bar = bar;
return this;
}
@java.lang.Override public String toString() {
return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
}
public Example build() {
return new Example(foo, bar);
}
}
}
사용법은 아주 간단하다. 빌더 패턴을 적용할 객체에 @Builder 어노테이션을 달기만 하면된다.
@Builder
public class Bag {
private String name;
private int money;
private STring memo;
}
어노테이션만 달면 빌더가 생기고 위에 썼던 것과 같이 빌더를 통해 객체를 생성할 수 있다.
Bag bag = Bag.builder()
.name("name")
.money(1000)
.memo("memo")
.build();
@Builder 어노테이션 옵션
어노테이션을 달기만 하면 빌더를 쓸 수 있다.
그렇다면 어노테이션에 다른 옵션은 없는걸까?
@Builder에 어노테이션 값으로 넣을 수 있는 것은 아래와 같다.
builderMethodName
@Builder 어노테이션을 사용하면 빌더를 생성하는 메서드의 이름은 기본 값인 builder()이다. 이를 새롭게 네이밍 할 수 있는 어노테이션 값이다.
이 객체를 사용하고자 하는 사람에게 도움이 되는 방향으로 빌더 메서드 이름을 정의하자. 보통은 기본 값인 builder()를 그대로 사용한다.
Bag bag = Bag.builder()
...
buildMethodName
builder()로 얻은 빌더에 필드 값들을 입력하고 마지막에 객체를 생성하는 동작인 빌드 메서드의 이름을 네이밍 할 수 있는 어노테이션 값이다.
기본 값은 build()로 create() 등 필요한대로 네이밍하면 된다. 이에 맞춰서 빌더 메소드도 같이 리네이밍해주기도 한다.
Bag bag = Bag.builder()
.build();
builderClassName
이건 솔직히 어떨 때 사용해야하는지는 모르겠지만 보통 @Builder를 사용하면 각 필드들의 값을 셋팅하는 메서드의 반환값의 빌더 클래스의 이름이 000Builder로 자동 설정된다. 예를 들어 아래와 같은 Bag 클래스에 @Builder를 적용하면 각 필드를 설정하는 메서드의 반환값은 BagBuilder가 된다.
이걸 이제 사용자가 원하는 대로 리네이밍 할 수 있다.
Bag bag = Bag.builder()
.name("name") // return BagBuilder
.money(1000) // return BagBuilder
.memo("memo") // return BagBuilder
.build();
builderClassName을 test로 설정 시 아래와 같이 필드를 설정하는 메서드의 반환 값이 test가 된다.
toBuilder
boolean 값으로 설정할 수 있는 어노테이션 값으로 기본 값은 false이다. 이 값을 true로 설정시 빌더로 만든 인스턴스에서 toBuilder() 메서드를 호출해 그 인스턴스 값을 베이스로 빌더 패턴으로 새로운 인스턴스를 생성할 수 있다.
기본 값인 false로 설정할 경우 toBuilder() 메서드 자체가 존재하지 않는 메서드라는 에러가 발생한다.
Bag bag1 = Bag.builder()
.name("name")
.money(1000)
.memo("memo")
.build();
Bag bag2 = bag1.toBuilder().money(8000).build();
access()
builder 클래스의 접근제어자를 어떻게 할 것인지에 대한 설정 값이다. 기본은 public으로 되어 있다.
AccessLevel access() default lombok.AccessLevel.PUBLIC;
위에 정리한 것 외에도 빌더 패턴에는 불변성을 유지할 수 있다는 등의 장점도 있지만 이는 빌더패턴만의 장점이라기보다는 생성자를 써도 Setter 설정만 안 하면 유지할 수 있는 것이어서 따로 정리는 하지 않았다.
'Spring Framework' 카테고리의 다른 글
Spring 404 JAVA 설정 예외처리 법 (0) | 2024.04.23 |
---|---|
23.03.30 (0) | 2023.03.30 |
23.02.15 redirect, forward 방식의 차이점 (0) | 2023.02.15 |