Skills/Java

Java - 생성자에 매개변수가 많다면 Builder를 고려하라

aoaa 2022. 10. 19. 17:02

  이펙티브 Java의 2장은 제목과 같이 Builder 패턴에 대해 설명합니다.

item 1에서 설명한 생성자와 정적 팩토리 메서드에서는 공통적으로 매개변수가 많다면 대응하기 힘들다는 포인트가 있었습니다. 이를 Builder를 통해 어떻게 개선하는지 패러미터를 어떻게 깔끔하게 처리하는지 알아보겠습니다. 


1. 생성자 패턴

Buidler 설명에 앞서, 생성자 패턴의 맹점을 알아보겠습니다. 

public class Nutrition {
	private final int calroies;
    private servings;
    private final fat;
    private final int sodium;
    
    public Nutrition(int calroies, int servings) {
    	this(calroies, servings, 0);
	}

	public Nutrition(int fat, int sodium){
    	this(fat, sodlium, 0);
    }
    
    public Nutrition(int calroies, int servings, int fat, int sodium){
    	this.calroies = calroies;
        this.servings = servings;
        this.fat = fat;
        this.sodium = sodium;
        
}

 어떤 식품의 영양 정보를 나타내는 코드를 입니다. Nutrition 클래스의 인스턴스를 생성한다고 하면 

Nutrition sandwich = new Nutrition(100, 4, 80, 2);

 위와 같이 호출을 할게 될 것입니다. 위 코드의 생성자는 사용자가 설정하기 원치 않는 패러미터까지 포함할 수도 있는데 이런 경우에도 패러미터에 값을 어쩔수 없이 지정해줘야 합니다. 작성한 코드에서는 4개지만, 매개변수가 계속 늘어나게 된다면 코드를 읽을 때 값의 의미가 헷갈릴 수도 있고, 타입이 같은 매개변수가 늘어져 있기때문에 이는 버그로 이어질 수 도 있습니다. 

 


2. JavaBeans 패턴

  다음은 JavaBeans 패턴입니다. 

public class Nutrition {
	private int calroies;
    private int servings;
    private int fat;
    private int sodium;
    
    //Setter
    public Nutrition(){}
    
    public void setCalroies(int val) {
    	setCalroies = val;
        }
        
        ...
        
    public void setSodium(int val) {
    sodium = val;
    }
}

 JavaBeans의 Setter 메서드를 이용하여 인스턴스를 생성했습니다. 

Nutrition sandwich = new Nutrition();
sandwich.setCalories(100);
sandwich.setFat(10);

 1. 에서 설명한 생성자 패턴에서 봤던 필요없는 값에 패러미터를 넣을 필요없이 set 메서드를 통해 인스턴스를 생성할 수 있습니다. 

하지만 이런 JavaBeans 패턴을 사용해도 인스턴스를 생성하려면 메서드를 여러개 호출해야 하고, 객체 생성이 되기 전까지는 일관성이 무너진 상태에 놓입니다. 생성자 패턴에서 패러미터들이 유효한지 생성자에서 확인하는 장치가 JavaBeans에서는 없어졌기 때문입니다.

 

 생성자 패턴의 this.calroies=calroies, this.sodium=sodium과 같이 Nutrition의 인스턴스가 있을 때 항상 calories, sodium의 값이 올바르게 설정되어 있어 일관성을 나타내지만

 JavaBeans같은 경우 생성자의 인자가 없고 setter 메서드만 존재하기 때문에, setCalroies만 하고, setFat를 하기 전에는 일시적으로 멤버변수 calroies에는 올바른 값이 있지만, fat에는 올바른 값이 없는 상태가 되는데 이를 일관성이 무너진 상태라고 말합니다.

 


3. Builder 패턴

 위에서 제기한 문제점을 해결하는 패턴이 있는데 바로 Builder 패턴입니다. 

클라이언트가 필요한 객체를 직접 만드는 대신, 필수 패러미터만으로 생성자 or 정적 팩토리를 호출해 빌더 객체를 얻는 방식입니다.

이 빌더 객체가 제공하는 Setter 메서드들로 원하는 선택 패러미터를 설정하고, 패러미터가 없는 build 메서드를 호출해 필요한 객체를 얻을 수 있습니다. 설명보다 코드로 한 번 살펴보겠습니다. 

public class Nutrition {
	private final int calroies;
    private servings;
    private final fat;
    private final int sodium;
    
    // Builder
    public static class Builder {
    	// 필수 패러미터
        private final int calroies;
        private final int servings;
        
        // 선택 패러미터
        private int fat = 0;
        private int sodium = 0;
        
        public Builder(int calories, int servings) {
        	this.calroies = calroies;
            this.servings = servings;
        }
        
       	public Builder fat(int val){
        	fat = val;
            return this;
        }
        
        public Buider sodium(int val){
        	sodium = val;
            return this;
        }
        
        public Nutrition build() {
        	return new Nutrition(this);
        }
	}
    
    private Nutrition(Builder builder) {
    	calroies = builder.calroies;
        sodium = builder.sodium;
        fat = builder.fat;
        servings = builder.servings;
        }
    }
}
Nutrition sandwith = new Nutrition.Builder
	.calroies(100)
   	.servings(10).build();

  Builder 패턴을 이용해 모든 패러미터의 기본값을들 한 곳에 모아두고, Setter 메서드가 Builder 자신을 반환하도록 하여 연쇄적으로 호출할 수 있도록 했습니다. 이 때 메서드의 호출이 흐르듯 연결된다는 뜻으로 fluent API or method chaining이라고 합니다.

 특징을 정리하자면 

1) 필요한 데이터만을 골라 인스턴스를 생성할 수 있고,

 

2) 코드의 유연성을 확보합니다. 예를 들어 Nutrition 클래스에 새로운 영양정보인 cabohydrate를 추가한다고 가정해보겠습니다.

일반적으로 새롭게 추가되는 변수로 인해 기존의 코드를 수정해야 하지만, Builder 패턴이 적용되어 있다면 유연하게 객체 값을 설정할 수 있도록 도와주기 때문에 기존의 코드를 수정할 필요가 없습니다.

 

3) 가독성이 높아집니다.

// 기존 코드
Nutrition sandwich = new sandwich(100, 40, 10);

// Builder
Nutrition sandwivh = sandwich.builder()
	.calroies(100);
    .servings(8);
    .fat(10).build;

 기존 생성자를 이용하게 되면 각 인자가 무슨 의미인지 파악하기 힘든 경우에 반해, Builder 패턴을 적용하면 어떤 데이터에 어떤 값이 설정되어 있는지 쉽게 파악할수 있어 가독성을 높일 수 있습니다.

 

4) 변경 가능성 최소화 (불변성 확보)

 Setter 메서드를 구현한다는 것은 객체의 값이 변경된다는 가능성을 열어두는 것을 의미합니다. 그렇다면 코드 유지보수 시에 데이터의 값이 할당된 지점을 찾기 힘들게 만들게 됩니다. 값을 할당하는 시점이 객체가 생성될 때뿐이라면 잘못된 값이 들어와도 그 시점을 찾기 쉽기 때문에 유지보수성을 높일 수 있을 것입니다. 이를 클래스 변수를 final로 선언 후, 객체의 생성을 Builder에게 맡긴다면 불변성을 확보할 수 있게됩니다.


4. Spring의 @Builder

 저는 글을 작성하기 전에는 Builder 패턴이 Spring에서만 지원하는 것인줄 알고 있었습니다. (아니었지만 ㅎㅎ;)

Builder 패턴 적용시마다 위와 같이 코드를 작성하게 되면 코드가 길어지고 불편해지기 때문에 Lombok 라이브러리의 @Builder 애노테이션이 이를 간편하게 생성해줍니다.

@Builder(builderMethodName = "Hibuilder")
public class Nutrition {
    private final int calroies;
    private final int servings;
    private final int fat;
    private final int sodium;
}

 클래스나 생성자 위에 @Builder 애노테이션을 붙이면 위에서 구현한 Builder 패턴이 빌드됩니다. 

 

 객체 생성 시 대부분의 경우 Builder 패턴을 적용하지만, 변수의 개수가 2개 이하인 경우에는 정적 팩토리 메서드를 사용하여 Builder의 남용으로 인한 코드의 길어짐을 방지하는 것이 좋습니다. 이는 변수 개수, 변경 가능성(불변)을 중점적으로 고려하여 Builder 패턴을 적용해야하는지 판단하면 될 것입니다. 

 

 나중에 JPA entity 객체들을 Builder 패턴 기반으로 생성하게 되는데, 그 때 다시 자세하게 알아보겠습니다.

 

 

 

 

 

 

 

 

 

 

 

참조

더보기