(Effective Java) 빌더 패턴(Builder) + Lombok Builder

2022. 9. 20. 00:39독서

반응형

이펙티브 자바에서 빌더 패턴을 소개하고 있다.

 

 

스프링을 사용하는 분들 혹은 Gradle에서 Lombok을 쓰는 분들이라면

 

 

@Builder, 빌더 어노테이션에 대해서 잘 알고 있을 것이다.

 

 

Lombok은 다들 아시겠지만, 이 자체가 어떤 새로운 것을 만들어 내는 것이 아닌,

 

 

기존의 것을 쉽게 어노테이션만으로 처리할 수 있게끔 만들어 놓은 수단에 불과하다.

 

 

@Builder 역시 그중에 하나인데, 자바의 빌더 패턴을 살펴봄과 동시에

 

 

롬복의 @Builder도 같이 살펴보면 좋을 것 같다.

 

 

 

🧐생성자 쓰지 왜....


왜 빌더를 쓸까? (생성자 걍 쓰지)

 

 

아래 책의 예제 코드를 보자.

 

 

public class NutirtionFacts {
	private final int servingSize;
	private final int servings;
	private final int calories;
	private final int fat;
	private final int carbohydrate;

	public NutrtionFacts(int servingSize, int servings) {
		this(servingSize, servings, 0);
	....
}

 

이 생성자 하나를 만드는 데도 오래 걸렸다.

 

 

만약 생성자가 저 필드 수만큼 필요하다면 어떻게 될까...?

 

 

그래도 생성자를 쓸 것인가?

 

 

Builder를 모르면 그냥 썼겠지만, 알고 있음에도 불구하고 그냥 생성자를 쓴다는 것은...

 

 

강제 노동에 불과하다. (뇌피셜)

 

 

🧐자바 빈즈 패턴은 어때...?

 


자바 빈즈 패턴은 생성자를 매개변수가 없는 생성자로 정의하고,

 

 

모두 setter로 값을 주입하는 방식이다.

 

 

이렇게 되면 큰 문제가 생기는데, 바로 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 있다는 것이다.

(값 변환도 제멋대로 인건 덤)

 

 

일관성이 쉽게 무너지기 때문에 불변 클래스로 만들 수 없다는 문제도 존재한다.

 

 

 

🧐빌더패턴


책에 있는 예제 코드를 보자.

 

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val)
        { calories = val;      return this; }
        public Builder fat(int val)
        { fat = val;           return this; }
        public Builder sodium(int val)
        { sodium = val;        return this; }
        public Builder carbohydrate(int val)
        { carbohydrate = val;  return this; }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
 }

 

이렇게 하면 실제 사용할 때는 아주 간단하게

 

class Main {
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

 

이렇게 사용할 수 있다.

 

 

얼마나 좋은가!

 

 

🧐롬복(Lombok)의 빌더


그럼 롬복의 빌더는 어떻게 동작할까?

 

 

코드를 보면서 얘기해보자.

 

import lombok.Builder;
import lombok.Singular;
import java.util.Set;

@Builder
public class BuilderExample {
    @Builder.Default 
    private long created = System.currentTimeMillis();
    private String name;
    private int age;
    @Singular 
    private Set<String> occupations;
}

 

우리는 흔히 위 처럼 코드를 작성한다.

 

 

Lombok을 사용하는 프로젝트에서 builder를 쓸 때,

 

 

이렇게 Builder.Default를 사용해서 기본 값을 정의한다거나,

 

 

@Builder 어노테이션 만으로 빌더 패턴을 만들곤 한다.

 

 

그럼 실제 컴파일 과정에서는 어떻게 번역될까?

 

 

아래 코드를 보자.

 

 

import java.util.Set;

public class BuilderExample {
  private long created;
  private String name;
  private int age;
  private Set<String> occupations;
  
  BuilderExample(String name, int age, Set<String> occupations) {
    this.name = name;
    this.age = age;
    this.occupations = occupations;
  }
  
  private static long $default$created() {
    return System.currentTimeMillis();
  }
  
  public static BuilderExampleBuilder builder() {
    return new BuilderExampleBuilder();
  }
  
  public static class BuilderExampleBuilder {
    private long created;
    private boolean created$set;
    private String name;
    private int age;
    private java.util.ArrayList<String> occupations;
    
    BuilderExampleBuilder() {
    }
    
    public BuilderExampleBuilder created(long created) {
      this.created = created;
      this.created$set = true;
      return this;
    }
    
    public BuilderExampleBuilder name(String name) {
      this.name = name;
      return this;
    }
    
    public BuilderExampleBuilder age(int age) {
      this.age = age;
      return this;
    }
    
    public BuilderExampleBuilder occupation(String occupation) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }
      
      this.occupations.add(occupation);
      return this;
    }
    
    public BuilderExampleBuilder occupations(Collection<? extends String> occupations) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }

      this.occupations.addAll(occupations);
      return this;
    }
    
    public BuilderExampleBuilder clearOccupations() {
      if (this.occupations != null) {
        this.occupations.clear();
      }
      
      return this;
    }

    public BuilderExample build() {
      // complicated switch statement to produce a compact properly sized immutable set omitted.
      Set<String> occupations = ...;
      return new BuilderExample(created$set ? created : BuilderExample.$default$created(), name, age, occupations);
    }
    
    @java.lang.Override
    public String toString() {
      return "BuilderExample.BuilderExampleBuilder(created = " + this.created + ", name = " + this.name + ", age = " + this.age + ", occupations = " + this.occupations + ")";
    }
  }
}

 

어떤가!

 

 

컴파일 과정에서 저렇게 바뀐다고만 생각하지 말고

 

 

@Builder를 사용하지 않는다면 우리가 저걸 다 작성해야 된다고 생각해보자.

 

 

끔찍하다.

 

 

책에서 말하는 것처럼 빌더 패턴을 사용하면 좋지만,

 

 

빌더패턴 역시 멤버 변수가 늘어날 수록 필요한 메서드가 점진적으로 늘어가게 된다.

 

 

그렇기 때문에 Lombok의 Builder를 반드시 사용하길 바란다.

 

 

 

 

 

 

@Builder

 

projectlombok.org

 

 

 

 

반응형