디자인패턴

빌더 패턴

꾸진 2022. 3. 24. 16:01

빌더 패턴이란

  • 생성 패턴 중 하나로, 복잡한 객체를 단계별로 생성할 수 있게 해주는 패턴이다.
  • 동일한 생성 코드를 사용하여 객체의 다양한 유형과 패턴을 생성할 수 있게 해 준다. 

이렇게 말해도 사실 빌더 패턴이 무엇인지 감이 잘 오지 않는다.

객체를 생성하는 방법

우선은 빌더패턴은 객체를 생성하는 방법 중 하나이므로 우리가 어떤 방식으로 객체를 생성하는지 알아봐야 한다.

총 3가지 방식으로 알아볼 텐데, 첫 번째는 생성자를 사용하는 방법, 두 번째는 자바 빈(Setter)을 사용하는 방법, 마지막은 빌더 패턴을 사용하는 방법이다.

해당 방법들을 순차적으로 살펴보며 빌더 패턴에 대해 알아보자.

 

1. 생성자 사용

아래와 같이 Sandwich라는 클래스가 있고 해당 클래스에 대한 필드로 bread(빵 종류), pickle(피클 개수), cabbage(양배추 개수), ham(햄 개수)을 가지고 있다고 생각해보자

public class Sandwich{
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수
}

위와 같은 Sandwich 클래스가 있다고 가정했을 때
아래와 같이 생성자를 만들어주자.

public class Sandwich{
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수

    public Sandwich(String bread, int pickle, int cabbage, int ham) {
        this.bread = bread;
        this.pickle = pickle;
        this.cabbage = cabbage;
        this.ham = ham;
    }
}

생성자를 통해 샌드위치 인스턴스를 만드는 방법은 아래와 같을 것이다.

Sandwich sandwich = new Sandwich("flatbread", 2, 3, 1);

샌드위치가 잘 만들어졌지만 이러한 방식의 단점은 생성자를 사용할 때 매개변수의 순서를 잘 지켜야 한다는 점이 있다. 누군가가 실수로 매개변수 순서를 바꿔서 넣을 수도 있고, 생성자 인자가 더욱 많아질 경우 알아보기 힘들다는 점이 있다.

 

위와 같이 생성자 코드만 보더라도 매개변수에 들어가는 "2"라는 값이 뜻하는 게 pickle의 개수인지, cabbage의 개수인지, ham의 개수인지 알아보기 힘들기 때문이다. 이를 알아보려면 해당 클래스에 들어가서 생성자를 확인해봐야 한다.

그래서 이러한 단점을 보완한 방법이 setter를 이용한 자바 빈 패턴이다.

2. 자바 빈 사용(Setter 사용)

자바 빈 패턴은 매개변수를 받지 않는 기본 생성자를 사용해 인스턴스를 만들고 setter를 사용해 필드 값을 조작하는 방식이다. 아래와 같이 Sandwich의 기본 생성자를 만들어준 후 각 필드에 대한 setter를 만들어준다.

public class Sandwich{
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수

    public Sandwich() {
    }

    public setBread(String bread){
        this.bread = bread;
    }

    public setPickle(int pickle){
        this.pickle = pickle;
    }
    ... 생략
}

이렇게 만들어진 클래스에 대한 인스턴스를 생성하는 방법은 아래와 같을 것이다.

Sandwich sandwich = new Sandwich();
sandwich.setBread("flatbread");
sandwich.setPickle(2);
sandwich.setCabbage(3);
sandwich.setHam(1);

위와 같은 방식의 단점은 1번의 함수 호출을 통해 인스턴스를 생성하지 못하고 여러 번의 Setter를 호출해야 하고 객체의 일관성을 유지하기 어렵다는 점이 있다. 변경 불가능한(immutable) 객체 생성이 불가능함.

 

객체의 일관성을 유지하기 어렵다는 말은 다음과 같다.

 

자바 빈 규약을 따르는 setter는 public으로 어떤 곳에서나 접근이 가능하기 때문에 의도치 않은 변경을 할 수 있게 된다는 것이다.

 

예를 들자면, 샌드위치를 만드는 주방이 아닌 카운터에서도 setBread()이나 setPickle()과 같은 메소드를 호출할 수 있다는 점이다. 손님에게 나간 샌드위치가 주방에서도 만들어졌을 수 있고 카운터에서 만들어질 수 있으니 어디서 만들어졌는지 의도를 파악하기가 어렵다는 점이라는 말이다.

 

이와 같이 생성자와 자바 빈의 단점을 보완하고 장점을 합친 것이 바로 빌더 패턴이다.

3. 빌더 패턴

빌더 패턴은 객체에 필수적으로 필요한 매개변수를 넣어 Builder 객체를 만든 후 Bulder 객체가 제공하는 메소드를 통해 선택적으로 필요한 필드를 채워 넣는 방식이다.

 

빌더 패턴의 목적은 유동적으로 필드에 값을 세팅하고, 객체를 생성한 후, 변경 불가능한 상태로 만드는 것이다.

어떤 방식으로 사용하는지 우선 코드로 봐보자

Sandwich sandwich = new Sandwich
                    .Builder("flatbread")
                    .pickle(2)
                    .cabbage(3)
                    .ham(1)
                    .build();

위와 같이 Builder()를 호출해 필수 매개변수인 빵 종류를 입력해 Builder 객체를 만든 후 해당 객체가 제공하는 pickle(), cabbage(), ham() 메소드를 호출해 선택적으로 필요한 필드의 값을 채워 준 뒤 build()를 호출하여 해당 클래스를 return 해주는 방식이다.

 

위와 같이 사용하려면 클래스를 어떤 방식으로 구성해야 하는지 아래와 같은 코드를 통해 알아보자.

public class Sandwich{
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수


    public static class Builder {
        //필수 매개변수
        private final String bread;

        //선택 매개변수
        private int pickle = 0;
        private int cabbage = 0;
        private int ham = 0;

        public Builder(String bread) {
            this.bread = bread;
        }

        public Builder pickle(int pickle) {
            this.pickle = pickle;
            return this;
        }

        public Builder cabbage(int cabbage) {
            this.cabbage = cabbage;
            return this;
        }

        public Builder ham(int ham) {
            this.ham = ham;
            return this;
        }

        public Sandwich build() {
            return new Sandwich(this);
        }
    }

    private Sandwich(Builder builder) {
        bread = builder.bread;
        pickle = builder.pickle;
        cabbage = builder.cabbage;
        ham = builder.ham;
    }
}

위와 같이 Sandwich 클래스 내부에 Builder라는 static 클래스를 생성해 준 뒤 Builder 생성자에서는 필수 매개변수 값을 받도록, 선택 매개변수 값은 pickle(), cabbage(), ham() 등을 호출해 채워 넣을 수 있도록 한다. 그 후 build() 메소드를 통해 Sandwich 클래스를 생성 후 리턴한다.

 

이렇게 사용하게 되면 생성자, 자바 빈 사용방법의 단점을 보완할 수 있게 된다. 즉, 유동적으로 필드에 값을 세팅하고, 객체를 생성한 후, 변경 불가능한 상태로 객체를 만들 수 있게 된다.

 

이러한 빌더 패턴의 단점은 코드가 장황해진다는 점인데, 이를 간편하게 사용할 수 있는 방법이 있다.

Lombok 라이브러리에서 @Builder 어노테이션으로 간편하게 사용이 가능하다.
Lombok @Builder 링크

@Builder
public class Sandwich {
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수
}

 

추가로

Setter를 최대한 지양해야 한다고 하는데, 그렇다면 Setter를 사용하지 않고 변경 가능한(mutable) 객체의 필드 값을 어떻게 변경해야 할까? 객체를 생성할 때는 생성자를 사용하고 특정 필드의 값을 변경할 때는 changeXXX()와 같이 메소드를 정의해 특정 로직이 포함되도록 사용한다. 아래의 예제를 보자.

public class Sandwich{
    private String bread; // 빵 종류
    private int pickle; // 피클 개수
    private int cabbage; // 양배추 개수
    private int ham; // 햄 개수

    public Sandwich(String bread, int pickle, int cabbage, int ham) {
        this.bread = bread;
        this.pickle = pickle;
        this.cabbage = cabbage;
        this.ham = ham;
    }
    
    public void changePickle(int pickle){
    	this.pickle = pickle;
        if(ham>0) this.ham--;
    }
}

예를 들어 샌드위치의 피클 개수를 변경하려 할 때, 단순히 피클 개수만 변경해서 끝나는 일이 아니라 다른 재료와의 밸런스도 고려를 해야 하기 때문에 피클 개수를 하나 뺀다면 햄의 개수도 하나 빼도록 정의하는 것이다.

 

이렇게 사용한다면 필드 값을 변경할 때 일부 로직이 포함되어 있기 때문에 setter를 사용할 때보다 객체의 일관성, 안정성이 더 확보가 된다.

 

참조: https://github.com/whiteship/study/blob/master/effective-java/item2.md

        https://devlog-wjdrbs96.tistory.com/207

        https://dodop-blog.tistory.com/265