일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자료구조와함께배우는알고리즘입문
- d
- 이터레이터
- 알파회계
- /etc/network/interfaces
- network configuration
- 스프링부트핵심가이드
- 네트워크 설정
- 목록처리
- 친절한SQL튜닝
- baeldung
- resttemplate
- 처음 만나는 AI 수학 with Python
- 스프링 시큐리티
- Kernighan의 C언어 프로그래밍
- 데비안
- 페이징
- GIT
- 처음 만나는 AI수학 with Python
- iterator
- 리눅스
- ㅒ
- 자바편
- 선형대수
- 서버설정
- 자료구조와 함께 배우는 알고리즘 입문
- 티스토리 쿠키 삭제
- 구멍가게코딩단
- 코드로배우는스프링웹프로젝트
- 코드로배우는스프링부트웹프로젝트
- Today
- Total
bright jazz music
[effective java 3rd - ch.02 객체의 생성과 파괴] Item02 : 생성자에 매개변수가 많다면 빌더를 고려하라 본문
[effective java 3rd - ch.02 객체의 생성과 파괴] Item02 : 생성자에 매개변수가 많다면 빌더를 고려하라
bright jazz music 2022. 9. 8. 12:35정적 팩터리와 생성자에 모두 존재하는 제약:
매개변수가 많을 때 적절히 대응하기 어렵다.
대응 패턴
- 1. 점층적 생성자 패턴
- 2. 자바 빈즈 패턴
- 3. 빌더 패턴
1. 점층적 생성자 패턴(telescoping constructor pattern)
- 과거에는 점층적 생성자 패턴을 흔히 사용했다. 매개변수를 적게 가지는 생성자부터 많이 가지는 생성자까지 여러 개의 생성자를 만드는 것이다.
- 자신이 원하는 파라미터 조합을 가진 파라미터가 없을 경우, 각각 다른 생성자를 조합하여 객체를 만들거나 아니면 애초에 많은 파라미터를 가진 생성자를 사용해야 했다. 이 때 필요 없는 값에도 아규먼트를 넘겨줘야 했다.
- 매개변수가 많아지면 이 패턴은 사용하기 어려워 진다. 코드를 작성하거나 읽기 어려울 뿐만 아니라, 파라미터를 잘못된 순서로 입력하여 오류가 발생할 수도 있기 때문이다.
2. 자바 빈즈 패턴(Java Beans Pattern)
- 자바 빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 사용하여 매개변수의 값을 설정하는 방식이다.
자바 빈즈 패턴을 사용해서 객체 생성하기
package org.example;
class NutriationFacts{
//필드
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 NutriationFacts(){}
//setter메서드
public void setServingSize(int val) {servingSize = val;}
public void setServing(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;}
}
//NutriationFactsCaller.java
package org.example;
class NutriationFacts{
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 NutriationFacts(){}
public void setServingSize(int val) {servingSize = val;}
public void setServing(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;}
@Override
public String toString() {
return "NutriationFacts{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}
public void setCarbohydrate(int val) {carbohydrate = val;}
}
//호출 측
public class NutriationFactsCaller{
public static void main(String[] args){
NutriationFacts cocaCola = new NutriationFacts();
cocaCola.setServingSize(240);
cocaCola.setServing(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
System.out.println(cocaCola.toString());
}
}
//NutriationFacts{servingSize=240, servings=8, calories=100, fat=0, sodium=35, carbohydrate=27}
자바 빈즈 패턴의 장점:
점층적 생성자 패턴의 단점이 사라진다. 생성과 함께 객체의 모든 변수가 초기화 되고, 그 이후에 setter 메서드를 사용해 원하는 파라미터만 넣어 변수의 값을 변경할 수 있다. 인스턴스를 만드는 방법이 쉬워졌다.
자바 빈즈 패턴의 단점:
- 객체를 하나 만들려면 메서드를 여러 개 호출해야 한다.
- 객체가 완전히 생성되기 전까지는 일관성(Consistency)이 무너진 상태에 놓인다.
점층적 생성자 패턴에서는 파라미터가 유효한지 생성자에서만 확인하면 일관성을 유지할 수 있었다. 그러나 자바 빈즈 패턴에서는 그러한 확인이 이루어지지 않는다.
일관성이 깨진 객체는 디버깅을 어렵게 만든다. 버그를 심은 코드와 그 버그 때문에 런타임에 문제를 겪는 코드 사이에 거리가 있기 때문이다. 이처럼 일관성이 깨지는 문제 때문에 자바 빈즈 패턴에서는 클래스를 불변(immutable)로 만들 수 없다. 또, 스레드 안정성을 얻기 위해서는 추가 작업을 해줘야 한다.
일관성이 깨지는 문제의 단점을 완화하고자 생성이 끝난 객체를 freeze하고, freeze 하기 전에는 사용할 수 없도록 하는 방법을 사용하기도 한다. 그러나 이 방법은 다루기 어려워 실무에서는 쓰이는 경우가 적다. 더 큰 문제는 객체 사용 전에 프로그래머가 freeze 메서드를 확실히 호출해줬는지를 컴파일러가 보증할 방법이 없다는 것이다. 이는 런타임 오류를 야기할 가능성이 높다.
3. 빌더 패턴 (Builder Pattern)
파라미터 수가 많을 경우에 대응하는 세 번째 방법은 빌더 패턴을 사용하는 것이다. 빌더 패턴은 점층적 생성자 패턴과 자바 빈즈 패턴의 장점만 취한 패턴이다.
요약하자면, 필수 매개변수만 넣어 객체를 생성하고 선택적인 매개변수는 세터 메서드를 사용해 설정하는 것이다.
순서
- 필수 매개 변수만으로 생성자( 또는 static factory method)를 호출해 Builder 객체를 얻는다.
- Builder 객체가 제공하는 세터 기능의 메서를 사용해 원하는 선택 매개변수들을 설정한다.
- 마지막으로 매개변수가 없는 build() 메서드를 호출해 필요한 객체를 얻는다. (이 객체는 보통 불변(immutable)이다)
- Builder는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 일반적이다.
빌더 패턴의 사용
//빌더 패턴
package com.example.studyTest;
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;
//inner class
public static class Builder {
//필수 : 생성자 사용하여 생성 시 반드시 파라미터로 넣어줘야 한다.
private final int servingSize;
private final int servings;
//선택 : 파라미터로 넣어주지 않으면 0으로 초기화 됨
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;
}
//setter
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);
}
}
//NutriationFacts 생성자
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
빌더 패턴을 이용한 객체 생성
//클라이언트 측에서의 사용방식
NutritionFacts cocaCola = new NutritionFacts
.Builder(240, 8) //필수.
.calories(100) //선택
.sodium(35) //선택
.carbohydrate(27) //선택
.build(); //NutriationFacts 객체 리턴
클라이언트에서 여러 파라미터를 넣어 객체를 얻는 방법이 용이하고 가독성이 좋다. 빌더 패턴은 파이썬과 스칼라에 있는 named optional parameter를 흉내 낸 것이다.
예문을 이용한 실습
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;
//getter: 게터는 결과를 보기 위해 내가 임의로 추가함
public int getServingSize(){
return this.servingSize;
}
public int getServings(){
return this.servings;
}
public int getFat(){
return this.fat;
}
public int getCalories(){
return this.calories;
}
public int getSodium(){
return this.sodium;
}
public int getCarbohydrate(){
return this.carbohydrate;
}
//inner class
public static class Builder {
//필수 : 생성자 사용하여 생성 시 반드시 파라미터로 넣어줘야 한다.
private final int servingSize;
private final int servings;
//선택 : 파라미터로 넣어주지 않으면 0으로 초기화 됨
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;
}
//setter
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);
}
}
//NutriationFacts 생성자
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
public class Test {
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts
.Builder(240, 8) //필수.
.calories(100) //선택
.sodium(35) //선택
.carbohydrate(27) //선택
.build(); //NutriationFacts 객체 리턴
System.out.println("cocaCola.getClass() = " + cocaCola.getClass());
System.out.println("cocaCola.getServingSize() =" + cocaCola.getServingSize());
System.out.println("cocaCola.getServings() =" + cocaCola.getServings());
System.out.println("cocaCola.getCalories() = " + cocaCola.getCalories());
System.out.println("cocaCola.getFat() = "+ cocaCola.getFat());
System.out.println("cocaCola.getSodium() = " + cocaCola.getSodium());
System.out.println("cocaCola.getCarbohydrate() = " + cocaCola.getCarbohydrate());
}
}
/*
cocaCola.getClass() = class NutritionFacts
cocaCola.getServingSize() =240
cocaCola.getServings() =8
cocaCola.getCalories() = 100
cocaCola.getFat() = 0
cocaCola.getSodium() = 35
cocaCola.getCarbohydrate() = 27
*/
여기서는 유효성 검사 코드가 생략돼 있다. 그러나 잘못된 매개변수를 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고 빌드 메서드가 호출하는 생성자에서 여러 매개 변수에 걸친 불변식을 검사하는 것이 좋다. 공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다. 잘못된 점을 발견하면 IllegalArgumentException를 던지면 된다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
- ==> 추상클래스는 추상 빌더를 갖게 한다
- ==> concrete class(구체화 클래스)는 구체 빌더를 갖게 한다.
추상클래스 Pizza에 적용하는 빌더 패턴의 예
//abstract class Pizza.java
package com.example.studyTest;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;
//추상 클래스이니까 추상 builder
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();
//하위 클래스는 이 메서드를 override하여 this를 반환토록 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
- Pizza.Builder (inner)클래스는 재귀적 한정 타입을 이용하는 제네릭 타입이다.
-여기에 추상 메서드인 self()를 더해 하위 클래스에서 형변환 하지 않고도 메서드 연쇄를 지원할 수 있다.
- self 타입이 없는 자바를 위한 이 우회 방법을 simulated self-type 관용구라고 한다.
Pizza의 하위 클래스 예시 (NyPizza.java, Calzone.java)
NyPizza.java
- size 파라미터를 필수로 받는다.
//NyPizza.java
package com.example.studyTest;
import java.util.Objects;
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;
}
}
Calzone.java
- 소스 여부를 선택하는 sauceInside 파라미터를 필수로 받는다.
//Calzone.java
package com.example.studyTest;
public class Calzone extends Pizza {
private final boolean sauceInside;
//builder 클래스
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; //기본값
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
- 각 하위 클래스의 빌더가 정의한 build() 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언하였다.
- NyPizza.Builder는 NyPizza를 반환한고, Calzone.Builder는 Calzone를 반환한다.
- 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변반환 타이핑(covariant return typing)이라고 한다.
- 공변반환 타이핑 기능을 이용하면 클라이언트가 형변환에 신경쓰지 않고도 빌더를 이용할 수 있다.
사용
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Builder().addTopping(HAM).sauceInside().build();
--------------------
내 실습1
일반 클래스
package com.example.studyTest;
public class Employee {
private final String empId;
private final String name;
private final int age;
private final String address;
//내가 임의로 추가한 getter
public String getEmpId(){return this.empId;}
public String getName(){return this.name;}
public int getAge(){return this.age;}
public String getAddress(){return this.address;}
//내가 값 확인 용으로 추가한 toString()
public String toString() {
return "Employee{" +
"empId='" + empId + '\'' +
", name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
//Builder 클래스(inner class)
public static class Builder {
private final String empId; //필수 입력 필드
private final String name; //필수 입력 필드
private int age = 0; //세터로 변경 가능토록 만들기 위해 final을 붙이지 않았다.
private String address = ""; //세터로 변경 가능토록 만들기 위해 final을 붙이지 않았다.
public Builder(String empId, String name) {
this.empId = empId;
this.name = name;
}
public Builder setAge(int age){
this.age = age;
return this;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
//build(): Employee 생성자 호출. 거기에 완성된 Builder 객체를 파라미터로 넣은 뒤 Employee 객체를 반환한다.
public Employee build(){
return new Employee(this);
}
}
//Employee 생성자 : 이 클래스 밖에서는 호출할 수 없다. 클래스 내부에서만 호출이 가능
//Builder
private Employee(Builder builder){
this.empId = builder.empId;
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
}
호출
package com.example.demo.effectiveJavaTest;
import com.example.studyTest.Employee;
import org.junit.jupiter.api.Test;
public class EffectiveJavaTests {
@Test
public void builderTest01(){
Employee empTest = new Employee.Builder("1234", "홍길동").setAddress("서울").setAge(30).build();
System.out.println(empTest.toString());
//결과 Employee{empId='1234', name='홍길동', age=30, address='서울'}
}
}