728x90
정적 팩토리 메소드와 생성자 모두 매개변수가 많이 필요한 경우에 적절히 대응하기 어렵다.
해결책 1: 생성자
- 점층적 생성자 패턴(Telescoping Constructor Pattern)은 필수 매개변수와 함께 선택 매개변수를 0개, 1개, 2개 .. 받는 형태로, 우리가 다양한 매개변수를 입력받아 인스턴스를 생성하고 싶을때 사용하던 생성자를 오버로딩 하는 방식이다.
public class NutritiaonFacts {
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 NutritiaonFacts(int servingSize, int servings){
this(servingSize, servings, 0);
}
// 아래로 선택 매개변수를 n개씩 받는 생성자
public NutritiaonFacts(int servingSize, int servings, int calories){
this(servingSize, servings, calories, 0);
}
public NutritiaonFacts(int servingSize, int servings, int calories, int fat){
this(servingSize, servings, calories, fat, 0);
}
public NutritiaonFacts(int servingSize, int servings, int calories, int fat, int sodium){
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritiaonFacts(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;
}
}
- NutritiaonFact라는 클래스를 예로 들고있다.
- 해당 클래스는 필수 필드와 부가적인 필드를 가질 수 있는데, 그런 경우에 필수적인 매개변수를 가진 생성자에 부가적인 필드를 하나씩 추가하며 여러 생성자를 만들 수 있다.
- 그리고 설정하고 싶은 매개변수를 최소한으로 사용하는 생성자를 사용해서 인스턴스를 만들 수 있다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
- 이런 생성자를 쓰다보면 필요없는 매개변수도 넘겨야 하는 경우가 발생하는데, 보통 0 같은 기본값을 넘긴다. 물론 이런 방법이 동작하긴 하지만 이런 코드는 작성하기도 어렵고 읽기도 어렵다. (가독성이나 유지보수면에서 좋지 않다.)
- 코드를 읽을 때 각 값의 의미가 무엇인지 헷갈리고, 매개변수가 몇개인지도 주의해서 세어보아야 한다. 또한 클라이언트가 실수로 매개변수의 순서를 바꿔서 보내도 컴파일러는 알아채지 못하고, 런타임에 엉뚱한 동작을 할 수 있다.
해결책 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(){}
// 세터 메서드들
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) 문제와 불변성(immutable) 문제가 나타나게 된다.
- 이 방법의 단점은 객체 하나를 만드려면 메서드를 여러 개 호출 하여야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다. (일관성이 무너지면 버그를 심은 코드와 버그로 인해 런타임에 문제가 나는 코드가 물리적으로 떨어져있어 디버깅이 어렵다.)
일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 개발자가 추가 작업을 해야한다.
- 일관성 문제: 필수 매개변수란 객체가 초기화될때 반드시 설정되어야 하는 값이다. 하지만 개발자가 깜빡하고
setServingSize()이나setServings()메서드를 호출하지 않았다면 이 객체는 일관성이 무너진 상태가 된다. 즉, 객체가 유효하지 않은 것이다. 만일 다른곳에서NutritionFacts인스턴스를 사용하게 된다면 런타임 예외가 발생할 수도 있다. 이는 객체를 생성하는 부분과 값을 설정하는 부분이 물리적으로 떨어져 있어서 발생하는 문제점이다. 물론 이는 어느정도 생성자(Constructor)와 결합하여 극복은 할 수 있다. 하지만 다음에 소개할 불변성의 문제 때문에 자바 빈즈 패턴은 지양해야 한다. - 불변성 문제: 자바 빈즈 패턴의 Setter 메서드는 객체를 처음 생성할때 필드값을 설정하기 위해 존재하는 메서드이다. 하지만 객체를 생성했음에도 여전히 외부적으로 Setter 메소드를 노출하고 있으므로, 협업 과정에서 언제 어디서 누군가 Setter 메서드를 호출해 함부로 객체를 조작할수 있게 된다. 이것을 불변함을 보장할 수 없다고 얘기한다.
해결책 3: 빌더 패턴 (Builder pattern)
- 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴
- 빌더 패턴은 만들려는 객체를 바로 만들지 않고 필수적인 매개변수만으로 생성자를 호출해 Builder객체를 얻은 다음 빌더 객체가 제공하는 세터와 비슷한 메소드를 사용해서 부가적인 필드를 채워넣고 최종적으로 build라는 메소드를 호출해서 만들려는 객체를 생성한다.
public class NutritiaonFacts {
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 {
// 필수 매개변수
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; }
}
}
- 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다. 이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API(fluent API) 혹은 메서드 연쇄(method chaining)라 한다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
- 일단 쓰기 쉽고, 무엇보다도 읽기 쉽다. 잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식(invariant)을 검사해야한다.
- 공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야한다. 검사에 잘못된 점이 발견되면 예외로 던진다.
- 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
public class ImmutableObject {
private final String name;
private final int age;
// private 생성자로 직접 객체 생성을 막고, Builder를 통해서만 객체 생성 가능하도록 함
private ImmutableObject(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
// Getter 메서드를 통해서만 필드 값을 읽을 수 있도록 함
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 빌더 클래스 정의
public static class Builder {
private String name;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
// 객체의 불변성을 보장하며, 필드들의 검사를 수행하여 잘못된 값이 들어오면 예외를 던짐
public ImmutableObject build() {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
return new ImmutableObject(this);
}
}
// 예시에서는 toString() 메서드만 오버라이딩하여 객체 정보를 출력할 때 사용
@Override
public String toString() {
return "ImmutableObject{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
// 빌더를 사용하여 객체 생성
ImmutableObject immutableObject = new ImmutableObject.Builder()
.name("John")
.age(30)
.build();
System.out.println(immutableObject); // 출력: ImmutableObject{name='John', age=30}
// 불변 객체이므로 값 변경 불가능
// immutableObject.setName("Jane"); // 컴파일 오류 발생
}
}
728x90
'Programming Language > Java' 카테고리의 다른 글
| [이펙티브 자바]Item.04-인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.04.05 |
|---|---|
| [이펙티브 자바]Item.03-private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2024.04.02 |
| [이펙티브 자바]Item.01-생성자 대신 정적 펙터리 메서드를 고려하라 (0) | 2024.03.15 |
| [Java] Build Pattern (0) | 2022.11.13 |
| [Java] record 클래스 (0) | 2022.11.13 |