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
'독서' 카테고리의 다른 글
(Effective Java) 인스턴스 생성 막기 (1) | 2022.09.20 |
---|---|
(Effective Java) 싱글턴 패턴을 만드는 방법 + 싱글턴 패턴 (0) | 2022.09.20 |
(Effective Java) 생성자 대신 정적 팩터리 메서드를 고려하라 (2) | 2022.09.19 |
(함수형 코딩) 1장/ 함수형 사고란 무엇인가? (0) | 2022.07.28 |
<실용주의 프로그래머> 서평 (0) | 2022.05.11 |