| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 템플릿 메서드 패턴
- Spring Batch
- 코드카타
- 트러블슈팅
- 프로그래머스
- 추상클래스
- 이펙티브 자바
- Spring
- lv1
- 프록시 패턴
- 토스
- 계산기
- Til
- 백엔드
- 디자인 패턴
- 김영한
- 성능 개선
- 빌더 패턴
- Effective Java
- java
- 자바
- GoF 23
- spring boot
- 배치
- 로드밸런서
- 스케줄러
- DB
- redis
- 스프링
- 스프링 배치
- Today
- Total
김코딩
[ Effective Java ] 2. 생성자에 매개변수가 많다면 빌더를 고려하라 본문
빌더(Builder)란?
빌더(Builder)는 복잡한 객체를 단계별로 생성할 수 있게 해주는 생성패턴이다. 처리하기 어려운 많은 매개변수를 가진 객체를 깔끔하고 안전하게 만들 수 있도록 도와준다.
// 일반적인 생성자 방식
Person person = new Person("김철수", 30, "서울", "010-1234-5678", "kim@email.com", true, false);
// 빌더 방식
Person person = Person.builder()
.name("김철수")
.age(30)
.address("서울")
.phoneNumber("010-1234-5678")
.email("kim@email.com")
.isMarried(true)
.hasChildren(false)
.build();
기존 방식들의 한계
1. 점층적 생성자 패턴 (Telescoping Constructor Pattern)
- 매개변수가 많은 클래스에서 전통적으로 사용하던 방식이다.
- 매개변수 1개 ~ 전부다 받는 생성자까지 늘려가는 방식이다.
- 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
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 NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
문제점들 :
- 매개변수 순서 혼동
// 이런 실수가 자주 발생
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35);
// 매개변수 순서를 바꿔서 잘못 입력할 가능성
NutritionFacts pepsi = new NutritionFacts(240, 100, 8, 0, 35); //calories와 servings 순서 바뀜
- 불필요한 매개변수 설정
// sodium만 설정하고 싶어도 fat까지 0으로 설정해야 함
NutritionFacts juice = new NutritionFacts(200, 1, 80, 0, 15); // fat=0을 억지로 설정
- 생성자 개수가 너무 많아짐
필드가 10개면 생성자가 몇 개나 필요할까? 2^10 = 1024개
실제로는 합리적인 조합만 만들어도 수십 개가 필요
2. 자바빈즈 패턴 (JavaBeans Pattern)
- 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식
public class NutritionFacts {
private int servingSize = -1; // 필수: 기본값 없음
private int servings = -1; // 필수: 기본값 없음
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// setter 메서드들
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
사용 예시:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
장점 :
- 점층적 생성자 패턴의 단점들이 사라짐
- 각 매개변수의 의미가 명확함
- 원하는 매개변수만 설정 가능
단점 :
- 객체 일관성(consistency) 파괴
NutritionFacts facts = new NutritionFacts();
// 이 시점에서 객체는 불완전한 상태 (servingSize = -1, servings = -1)
facts.setServingSize(240);
// 여전히 불완전 (servings = -1)
// 만약 여기서 다른 스레드가 이 객체를 사용한다면?
// 또는 setter 호출을 깜빡한다면?
facts.setServings(8);
// 이제야 완전한 상태
- 불변 클래스로 만들 수 없음
// setter가 있으면 불변 객체가 될 수 없음
public final class ImmutableNutritionFacts {
private final int servingSize;
private final int servings;
// setter 메서드를 만들 수 없음 (final 필드이므로)
// public void setServingSize(int val) { servingSize = val; } //컴파일 에러
}
- 런타임 에러 가능성
public class NutritionFacts {
private int servingSize = -1;
public int calculateTotalCalories() {
if (servingSize <= 0) {
throw new IllegalStateException("servingSize가 설정되지 않았습니다!");
}
return servings * calories;
}
}
NutritionFacts facts = new NutritionFacts();
facts.setServings(8);
facts.setCalories(100);
// servingSize 설정을 깜빡함
facts.calculateTotalCalories(); //RuntimeException 발생
빌더 패턴의 등장
- 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴이 등장하였다.
- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
빌더 패턴의 동작 방식
1. 빌더 객체 획득 : 필수 매개변수로 빌더 생성
2. 선택적 매개변수 설정 : 체이닝 방식으로 값 설정
3. 최종 객체 생성 : build() 메서드 호출
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;
// private 생성자 - 빌더를 통해서만 생성 가능
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값으로 초기화
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;
}
// 최종 NutritionFacts 객체 생성
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
사용 예시:
// 깔끔하고 읽기 쉬운 코드
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) // 필수 매개변수
.calories(100) // 선택 매개변수들을 원하는 순서로
.sodium(35)
.carbohydrate(27)
.build(); // 최종 객체 생성
// 필요한 것만 설정 가능
NutritionFacts water = new NutritionFacts.Builder(240, 1)
.build(); // calories, fat, sodium, carbohydrate는 모두 기본값 0
// 순서는 상관없음
NutritionFacts juice = new NutritionFacts.Builder(200, 1)
.carbohydrate(20) // 순서를 바꿔도 됨
.calories(80)
.sodium(5)
.build();
빌더 패턴의 장점
- 가독성과 명확성
// 기존 생성자 방식 - 매개변수의 의미가 불분명
NutritionFacts pepsi = new NutritionFacts(240, 8, 100, 3, 35, 27);
// 빌더 방식 - 각 값의 의미가 명확
NutritionFacts pepsi = new NutritionFacts.Builder(240, 8)
.calories(100)
.fat(3)
.sodium(35)
.carbohydrate(27)
.build();
- 불변 객체 생성
public class NutritionFacts {
private final int servingSize; // final 필드 - 불변성 보장
private final int servings;
// ... 다른 final 필드들
// setter 메서드 없음 - 생성 후 변경 불가
}
- 유연한 객체 생성
// 필수값만 설정
NutritionFacts simple = new NutritionFacts.Builder(240, 1).build();
// 필요한 것만 선택적으로 설정
NutritionFacts partial = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.build(); // fat과 carbohydrate는 기본값 사용
- 매개변수 유효성 검사
public static class Builder {
public Builder(int servingSize, int servings) {
if (servingSize <= 0) {
throw new IllegalArgumentException("servingSize는 양수여야 합니다: " + servingSize);
}
if (servings <= 0) {
throw new IllegalArgumentException("servings는 양수여야 합니다: " + servings);
}
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
if (val < 0) {
throw new IllegalArgumentException("calories는 음수가 될 수 없습니다: " + val);
}
calories = val;
return this;
}
public NutritionFacts build() {
// 최종 검증도 가능
if (calories > 1000 && fat == 0) {
throw new IllegalStateException("고칼로리 식품인데 지방이 0인 것은 이상합니다");
}
return new NutritionFacts(this);
}
}
빌더 패턴의 단점
- 코드량 증가와 복잡성
// 간단한 클래스도 빌더 때문에 코드가 2-3배 길어짐
public class Person {
private final String name;
private final int age;
// 생성자만 있다면 이것으로 끝
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// 빌더 패턴 적용 시 - 코드가 훨씬 길어짐
public class Person {
private final String name;
private final int age;
private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
public static class Builder {
private final String name;
private int age;
public Builder(String name) {
this.name = name;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(this);
}
}
}
- 성능 오버헤드
// 일반 생성자: 객체 1개 생성
Person person = new Person("김철수", 25);
// 빌더 패턴: 객체 2개 생성 (Builder + Person)
Person person = new Person.Builder("김철수")
.age(25)
.build();
- 매개변수가 적을 때는 오히려 복잡
// 매개변수 2-3개일 때는 생성자가 더 간단하고 명확
Point point1 = new Point(10, 20); // 간단하고 직관적
// 빌더는 오히려 과도함
Point point2 = new Point.Builder()
.x(10)
.y(20)
.build(); // 너무 복잡
Effective Java에서
생성자나 정적 팩터리 메서드가 처리해야 할 매개변수가 많다면 빌더패턴을 선택하는게 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
Effective Java에서는 빌더패턴은 매개변수가 4개 이상은 되어야 값어치를 한다고 한다.
'개발 서적 > Effective Java' 카테고리의 다른 글
| [ Effective Java ] 1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2025.09.25 |
|---|