앞날 창창보
article thumbnail

빌더 패턴은 안티 패턴일까?

Lombok에서 지원하는 @Builder를 사용하다보면, 필수 값을 빼먹어서 곤란했던 적이 있을 것이다. 그래서 나는 솔직히 Builder를 안티패턴 취급하며, 협업에서는 지양해야 한다고, 혼자 마음 속으로 생각하고 있었다. (필수 값을 만들 수 있다는 것을 알기 전까지...)

 

빌더는 어떤 상황에서 유리할까?


빌더패턴가 좋은 상황은 정말 간단하다. 생성자를 만드는데, 매개변수가 너무 많이 필요한 상황이다. 

 

public A(String name, int classNumber, String hobby, String favoriteBook, String favoriteFood, String favoritePerson){
	값 삽입...
}

new A("이창보", 3113, "축구", 19, "EFFECTIVE JAVA 3/E", "냉소바", "호날두");

다음과 같은 생성자가 있을 때, 사실 "이창보"라는 값이 어디로 전달 되는 지 제대로 알 수 없을 뿐만 아니라, 만약 값이 필수가 아니라면, "" 이렇게 빈 값을 넣어주거나, null을 전달해야한다. 숫자의 경우라면 -1 이런 허수의 값을 전달하게 된다.

 

다음과 같은 문제를 해결하기 위해서 과거에는 점층적 생성자 패턴을 사용했다.

 

public A(String name, int classNumber, String hobby, String favoriteBook, String favoriteFood, String favoritePerson){
	값 삽입...
}

public A(String name, int classNumber, String hobby, String favoriteBook, String favoriteFood){
	this(name, classNumber, hobby, favoriteBook, favoriteFood, "")
}

public A(String name, int classNumber, String hobby, String favoriteBook){
	this(name, classNumber, hobby, favoriteBook, "", "")
}

다음과 같이, 하나씩 매개변수를 줄여나가는 방식이다. 단점은 매개변수가 많아지면, 코드가 너무 길어지고, 수정에 불리하게 된다. 나중에는 생성자를 호출할 때 더욱더 꼼꼼히 매개변수를 봐야할 수 있다.

 

그래서 또 사용했던게, 자바 빈즈 패턴이다.

A a = new A();
a.setName("이창보");
a.setClassNumber(3113);
a.setHobby("농구");
...

이렇게 객체를 생성하고, setter를 통해서 삽입해주는 형태이다. 자바 빈즈 패턴의 문제점은 객체가 완성되기 전(필요한 setter가 실행되기 전)까지는 일관성을 유지할 수 없다는 것이다. 그리고 객체를 불변으로 만들 수 없게 된다.

 

A a = A.Builder("이창보", 3113)
        .hobby("축구")
        .favoriteFood("냉모밀")
        .build();

그래서 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한  다음과 같은 빌더 패턴이 나오게 된다.

 

빌더 패턴은 어떻게 구현할 수 있을까?


public class A {
	private final String name;
    private final int classNumber;
    private final String hobby;
    private final String favoriteFood;
    ...
    
    public static class Builder {
    	//필수 매개변수
        private final String name;
        private final int classNumber;
        
        private String hobby = "";
		private String favoriteFood = "";
        ...
        
        public Builder(String name, int classNumber) {
        	this.name = name;
            this.classNumber = classNumber;
        }
        
        public Builder hobby(String hobby) {
        	this.hobby = hobby;
            return this;
        }

        public Builder favoriteFood(String favoriteFood) {
        	this.favoriteFood = favoriteFood;
            return this;
        }
        
        public A build() {
        	return new A(this);
        }
    }
    
    private A(Builder builder) {
    	this.name = builder.name;
        ...
    
    }
}

 

다음과 같이 클래스 안에 static class Builder를 생성해주는 것으로 빌더 패턴을 구현할 수 있다. 코드를 보면 이해할 수 있을 것이라고 생각하고, 불필요한 추가 설명은 하지 않겠다.

 

그렇다면 언제 빌더패턴을 사용하면 좋을까?


빌더패턴을 사용하면 좋은 상황은,

1. 모든 값이 불변이 아니고,

2. 매개변수가 3~4개 이상일 때

인 것같다.

 

모든 값이 불변이라면, 빌더패턴은 생성자보다 나은 점이 없는 다음과 같은 모양이 될 것이다.

A a = A.Builder("이창보", 3113, "축구", ...)
		.build();

 

그리고 매개변수가 너무 적다면, 굳이 빌더 패턴을 사용하지 않아도 깔끔하기 때문에, 사용할 이유가 없다.

 

Lombok의 @Builder에서는 어떻게 필수 값 설정을 할까?


첫번째 방법은 빌더를 사용하려면 필수 값을 넣게하는 방법이다.

//정의
@Builder(builderMethodName = "innerBuilder")
public class A {

    private String name;
    private int classNumber;
   
    private String hobby;
    ...

    // builder 메소드를 재정의해서 필수 필드를 입력받도록 변경
    public static ABuilder builder(String name, int classNumber) {
        return innerBuilder()
            .name(name)
            .classNumber(classNumber);
    }
}

//사용
A a = A.builder("이창보", 3113)
    .hobby("축구")
    ...
    .build();

 

 

두번째 방법은 빌더가 호출된 후 객체가 생성됐을 때 값을 검증하는 방법이다.

@Builder
public class A {
	
    @NonNull
    private String name;
    @NonNull
    private int classNumber;
   
    private String hobby;
    ...
    }
}

//사용
A a = A.builder() //name 뺴고
    .classNumber(3113)
    .hobby("축구")
    ...
    .build();

위와 같이 코드를 작성하고, name에 값을 전달하지 않으면 아래와 같이 NPE가 발생한다.

 

사용하고 에러가 뜨는 것을 알게되는 방식 2보다는 그 전에 알 수 있는 방식 1이 더 좋다고 느껴진다.

 

Lombok을 쓰면서 필수 값을 받는 방법은 위 두 방법이 가장 간단하다고 느껴졌다. 다들 값을 잘 넣을 수 있게 위의 방법을 사용하자~

 
 

 

@Builder.required와 @Builder.excepted가 있으면 좋겠다.


@Builder를 사용할 때,

//정의
@Builder
public class A {
    @Builder.Excepted
    private int id;
    @Builder.Required
    private String name;
    @Builder.Required
    private int classNumber;
    
    private String hobby;
}

//사용
A a = new A.builder("이창보", 3113)
	//.id() 없음!
	.hobby("축구")
    .build();

다음과 같이 쓸 수 있으면 좋겠어서 이슈를 남겨놓은 상태이다!

 

 

https://github.com/projectlombok/lombok/issues/3640

 

[FEATURE] Would you be able to add @Builder.Required to sure fields exist? · Issue #3640 · projectlombok/lombok

As I know, there is no way lombok sure the fields exist when using @Builder. I think Suring the value exist is essential when using build written by another developer. when defining @Builder @AllAr...

github.com

 

검색 태그