https://book.naver.com/bookdb/book_detail.nhn?bid=14097515
이펙티브 자바
자바 플랫폼 모범 사례 완벽 가이드 - JAVA 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브
book.naver.com
이 글은 이펙티브 자바 Effective Java를 읽고 희미한 기억을 또렷한 기록으로 바꾸기 위해 작성했습니다
들어가기 전에
클래스의 필수 매개변수와 선택 매개변수가 있다고 가정해봅시다.
자 그러면 클래스의 인스턴스를 만들 때 필수 매개변수에는 값이 있어야 하고 선택 매개변수에는 값이 있을 수도 없을 수도 있습니다.
이럴 때 생성자에 매개변수가 많으면 어떻게 해야 할까요?
정적 팩터리와 생성자에는 똑같은 제약이 있는 데, 바로 선택적 매개변수가 많을 때 적절히 대응하기가 어렵습니다.
이번에 다룰 내용은 생성자 매개변수가 많을 때 생성자 패턴을 이용해서 해결할 수 있습니다.
생성자 패턴은 점층적 생성자 패턴, 자바빈즈 패턴, 빌더 패턴으로 나눌 수 있습니다.
그러면 각 패턴을 다루면서 알아보고 이 책에서는 왜 빌더를 고려하라고 했는지 알아보겠습니다.
점층적 생성자 패턴(Telescoping Constructor Pattern)
자, 주변에 아무 식품 포장의 영양정보를 확인 해볼까요?

영양 정보에는 1회 내용량(100g), 총 내용량, 1회 (100g) 제공량당 칼로리 같은 필수 항목과
총 지방, 포화지방, 콜레스테롤, 나트륨 등 20개가 넘는 선택 항목으로 이루어집니다.
하지만 대부분의 제품은 이 선택 항목이 대다수가 0입니다. (당류나, 지방 등 0으로 되어있는 제품도 있기 때문)
이럴 때 점층적 생성자 패턴으로 코드를 짜 보면 아래와 같다.
public class NutritionFacts {
private final int servingSize; // (m1, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
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;
}
}
필수 매개 변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, 2개 받는 생성자..
생성자 오버로딩으로 여러 개 생성자를 만들고
NutritionFacts cocaCola = new NutritionFacts(240, 8);
NutritionFacts cocaCola1 = new NutritionFacts(240, 8, 100);
NutritionFacts cocaCola2 = new NutritionFacts(240, 8, 100, 0);
NutritionFacts cocaCola3 = new NutritionFacts(240, 8, 100, 0, 35);
NutritionFacts cocaCola4 = new NutritionFacts(240, 8, 100, 0, 35, 27);
매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하는 방식입니다.
점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵습니다.
왜냐하면 어떤 매개변수에 값을 넣을지도, 혹여나 순서를 바꿔서 건네줘도 컴파일러는 알아채지 못하고 동작하기 때문입니다.
아! 그러면 매개변수가 없는 생성자로 객체를 만들고 메소드를 호출하는 방식은 있나??
자바빈즈 패턴 (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() {}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
필수 매개변수에는 기본값을 설정해주고, 메서들을 통해 값을 넣을 수 있습니다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
점층적 생성자 패턴에 비해 코드는 길어졌지만
클라이언트 소스에서 어떤 값에 매개 변수를 넣었는지에 대한 부분은 사라지게 되어
인스턴스를 만들기 쉽고 더 읽기 쉬운 코드가 되었습니다.
하지만!!
자바빈즈 패턴에서는 객체 하나를 만들려면 메소드를 여러 개 호출해야 하고,
객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됩니다.
점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 사라진 것입니다
일관성이 깨진 객체를 만들면 버그를 심은 코드와 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 떨어져 있을 것이므로 디버깅도 만만치 않습니다.
이 문제로 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없고 스레드 안정성을 얻으려면 프로그래머가 추가 작업을 해줘야 합니다.
아~ 그러면 점층적 생성자 패턴의 일관성과 자바빈즈 패턴의 가독성을 겸비한 패턴 어디 없나요?
빌더 패턴(Builder Pattern)
빌더 패턴은 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 패턴입니다.
빌더 패턴을 이용하면 필요한 객체를 직접 만드는 것 대신, 필수 매개 변수만으로 생성자(혹은 정적 팩토리)를 호출해 빌더 객체를 얻습니다.
그런 다음 빌더 객체가 제공하는 일종의 세터 메소드로 원하는 매개변수들을 설정하고 마지막으로 매개변수가 없는 build 메소드를 호출해 우리에게 필요한 객체(보통은 불변)를 얻게 되는 방식입니다.
public class NutritionFacts {
private final int servingSize; // (m1, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
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 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;
}
}
이렇게 NutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값들을 한 곳에 모으고 빌더의 세터 메소드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있습니다.
이런 방식을 메소드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API(fluent API) 혹은 메소드 연쇄(method chaining)라 합니다.
클라이언트에서 이 클래스를 사용해보겠습니다
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.carbohydrate(100).sodium(35).carbohydrate(27).build();
어떤 매개변수에 값을 넣었는지 쓰기 쉽고 읽기 쉬워졌습니다.
빌더 패턴은 명명된 선택적 매개변수(파이썬과 스칼라에 있는)를 흉내 낸 것입니다.
이번 장에서는 유효성 검사 코드는 생략했으며,
잘못된 매개변수를 최대한 일찍 발견하기 위해선 빌더와 생성자와 메소드에서 입력 매개변수를 검사하고 build메소드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하고, 공격에 대비해 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드로 검사해야 하고, 검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던지면 됩니다
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋습니다
계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴
이 책에서는 피자를 예시로 다뤘습니다.
우린 뉴욕 피자, 칼초네 피자를 만들 수 있습니다.
뉴욕 피자와 칼초네의 공통점은 "피자"입니다.
그러므로 상위 클래스인 "피자"를 만들고 하위 클래스인 뉴욕 피자, 칼초네 피자를 만들겠습니다.
상위 클래스인 피자에는 토핑만 매개변수로 두겠습니다.
뉴욕 피자는 크기를 필수로 받고, 칼초네 피자는 소스를 안에 넣을지 선택을 필수로 받겠습니다.
자 그러면 왜 계층적으로 설계된 클래스와 잘 어울린다고 했는지 확인해보겠습니다
피자 클래스
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
//하위 클래스는 이 메소드를 재정의하여 this를 반환하도록 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
Pizza.Builder클래스는 재귀적 타입 한정을 이용하여 제너릭 타입입니다.
추상 메소드인 self를 더해 하위 클래스에서 형변환하지 않고도 메소드 연쇄를 지원할 수 있습니다
자바에는 self타입이 없어 위와 같은 방법을 시뮬레이트 한 셀프 타입 관용구라고 합니다.
자 이제 피자의 하위 클래스인 뉴욕피자와 칼초네 피자를 만들어보겠습니다
뉴욕피자 클래스
public class NyPizza extends Pizza{
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
뉴욕 피자는 크기 매개변수를 필수로 받고, 빌더가 정의한 build메소드에는 뉴욕 피자 클래스를 반환합니다
칼초네 피자
public class Calzone extends Pizza{
//일반적으로 토마토 소스를 넣지 않는다.
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
칼초네 피자는 소스를 안에 넣을지 선택하는 매개변수를 필수로 받고 빌더가 정의한 build 메소드는 칼초네 클래슬 반환하도록 선언합니다.
뉴욕 피자와 칼초네 피자는 피자 클래스의 메소드가 정의한 반환 타입을 하지 않았습니다.
이처럼 하위 클래스의 메소드가 상위 클래스의 메소드가 정의한 반환 타입이 아닌, 하위 타입을 반환하는 기능을 공변환 타이핑(covariant return typing)이라고 합니다.
공변환 타이핑을 이용하면 클라이언트가 형변환에 신경 쓰지 않고 빌더를 사용할 수 있습니다.
그러면 뉴욕 피자와 칼초네 피자를 만들어보겠습니다!
NyPizza nyPizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside()
.build();
빌더를 이용하면 가변 인수(varargs) 매개 변수를 여러 개 사용할 수 있기에 생성자로는 누릴 수 없는 이점이다.
각각을 적절한 메소드로 나눠 선언하거나 메소드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 도 있다.
addTopping 메소드가 구현한 예다
이처럼 빌더 패턴은 상당이 유연하다.
그래도 빌더 패턴
빌더 패턴에 장점만 있는 것은 아니다
객체를 만들려면, 그에 앞서 빌더부터 만들어야 한다.
그래서 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
점층적 생성자 패턴보단 코드가 장황하니 4개 이상은 되어야 값어치를 한다고 한다.
하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있다.
그래서 애초에 빌더로 시작하자!
롬복이 설치되어 있다면 @Builder를 이용하면 빌더 클래스를 따로 만들지 않고도 사용할 수 있다.
느낀 점
생성자 패턴의 흐름에 따라 각 패턴의 장단점을 알았고 어떤 단점을 보완해서 나왔는지 명확하게 알게 되어 좋았다. 😆
개발 이야기는 아니지만
계층적으로 설계된 빌더 패턴을 작성할 때
왜 칼초네 피자의 소스 기본값은 false이지? 궁금했다
우선 칼초네 피자도 몰랐고 피자 하면 반죽 위에 토핑과 소스가 듬뿍 있다고 생각했기 때문!
그래서 소스는 없고 토핑만 있는 피자인가?? 토핑이 많으니 소스는 선택사항인가?? 하면서 검색해봤다
칼초네 피자

칼초네 피자는 이탈리아 요리 중 하나로, 소금에 절인 밀가루 반죽 사이에 채소, 햄 또는 치즈 등을 넣고 만두처럼 굽는 요리로
일반적으로 토마토소스를 넣지 않는다 [!!!]
아하!! 만두처럼 만드는 피자이기에 소스를 넣지 않고 찍먹 하는 피자였던 것이다!
어떻게 보면 사소한 궁금증이지만
좋은 코드를 만들기 위해서는 좋은 설계를 작성해야하는데
그러기 위해서는 도메인의 이해가 중요하다라는 것을 다시 한번 되새기며.
이렇게 칼초네 피자도 알아갔다.
'책 > 기록' 카테고리의 다른 글
[Effective Java] 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.06.20 |
---|---|
[Effective Java] 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.06.20 |
[Effective Java] 아이템1 생성자 대신 정적 팩토리 메서드를 고려하라 (0) | 2022.06.16 |
[프로그래머의 뇌] 변수의 역할 (0) | 2022.06.03 |
개발자에서 아키텍트로 #3 -설계-전략-고안하기 (0) | 2021.08.04 |