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를 반드시 사용하길 바란다.
'독서' 카테고리의 다른 글
(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 |