자바 - Builder Pattern

namkun·2023년 2월 24일
0

JAVA

목록 보기
17/20

이래저래 일을 하다보면, 객체를 생성해서 전달하는 일이 굉장히 잦다.
이런 경우에는 어떤 방법을 사용하는 것이 가장 좋은지에 대해서 알아보도록 하자.

예를 들어 다음과 같은 영양제 클래스가 있다고 해보자.

public class Supplements {
        private int vitaminA;
        private int vitaminB;
        private int vitaminC;
        private int vitaminD;
}

이 클래스를 객체로 만들어서 다른 쪽으로 전달한다고 생각해보면 보통은 아래와 같이 생성자를 만들 것이다.

public Supplements(int vitaminA, int vitaminB, int vitaminC, int vitaminD) {
    this.vitaminA = vitaminA;
    this.vitaminB = vitaminB;
    this.vitaminC = vitaminC;
    this.vitaminD = vitaminD;
}

그런데 만약에 어떤 영양제는 비타민 C가 없을 수도, 비타민 A가 없을 수도 있다.

이런 경우에는 그에 맞춰서 생성자를 생성해줘야 하는데, 위의 예시에서는 비타민이 4개만 있어서 생성해줘야하는 생성자의 개수가 그렇게 많지는 않지만, 우리가 평소에 먹는 영양제를 생각해보면...비타민이 생각 이상으로 많이 들어가는 경우에는 엄청난 경우의 수가 발생할 수 있다.

이런 경우 매번 필요시에 생성자를 구현하는 것은 굉장히 비 효율적이라고 생각한다.

이 다음으로 생각해볼 만한 것은 자바 빈즈 패턴이다.
매개 변수가 없는 생성자로 객체를 만든 뒤, Setter 메서드를 호출해서 원하는 매개 변수의 값을 설정하는 방식이다.
자바 빈즈 패턴에 맞게 위의 영양제 클래스에 코드를 추가하면 아래와 같다.

public class Supplements {
        private int vitaminA;
        private int vitaminB;
        private int vitaminC;
        private int vitaminD;
 
    public Supplements() {
    }
 
    public int getVitaminA() {
        return vitaminA;
    }
 
    public void setVitaminA(int vitaminA) {
        this.vitaminA = vitaminA;
    }
 
    public int getVitaminB() {
        return vitaminB;
    }
 
    public void setVitaminB(int vitaminB) {
        this.vitaminB = vitaminB;
    }
 
    public int getVitaminC() {
        return vitaminC;
    }
 
    public void setVitaminC(int vitaminC) {
        this.vitaminC = vitaminC;
    }
 
    public int getVitaminD() {
        return vitaminD;
    }
 
    public void setVitaminD(int vitaminD) {
        this.vitaminD = vitaminD;
    }
}

위처럼 작성하고 외부에서 작성할 때는 다음과 같이 사용하면 된다.

public class Main{
    public static void Main(String[] args){
        Supplements tablet = new Supplements();
        tablet.setVitaminA(100);
        tablet.setVitaminB(10);
        tablet.setVitaminC(60);
        tablet.setVitaminD(50);
    }
}

이렇게 한 알에 들어갈 영양소를 정하고, 이 한 알에 들어가 있는 영양소가 얼마나 들어있는지 알고 싶다면 Getter 메서드를 이용해서 가져오면 된다.

그러나 위와 같은 방식에 대해서는 단점이 있다.
객체 하나를 만드려면 setter 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다.

또한 객체를 불변으로 만들 수 없고 스레드 안정성을 얻으려면 객체를 얼리는 메서드(freeze)를 따로 생성해줘야만 한다.

이러한 단점을 해결해 줄 수 있는 것이 빌더 패턴이다.

빌더 패턴은 생성자 패턴의 안정성, 그리고 자바빈즈 패턴의 가독성을 모두 겸비하고 있는 패턴이라고 할 수 있다.

데이터들은 자바빈즈 패턴처럼 받되, 데이터 일관성을 위해 정보들을 다 받은 후에 객체를 생성한다.

빌더 패턴을 만들면 다음과 같이 만들 수 있다.

  • 클래스 내부에 Builder 클래스 생성
  • 각 멤버 변수별로 메서드를 생성, 각 메서드는 내부에서 변수 값을 Set하고 Builder 객체를 리턴
  • build() 메서드는 필수 멤버변수의 null 체크를 하고 지금까지 set된 builder를 바탕으로 클래스의 생성자를 호출하고 인스턴스를 리턴
public class Supplements {
        private int vitaminA;
        private int vitaminB;
        private int vitaminC;
        private int vitaminD;
 
        public static class Builder{
 
            // 필수 값은 초기화 안해도 되지만, 선택적으로 넣는 값에는 초기화 필요
            private int vitaminA;
            private int vitaminB;
            private int vitaminC = 0;
            private int vitaminD = 0;
 
            // 필수 인자를 강제화하기 위해서 생성자에 추가
            public Builder(int vitaminA, int vitaminB){
                this.vitaminA = vitaminA;
                this.vitaminB = vitaminB;
            }
 
            public Builder vitaminC(int val){
                vitaminC = val;
                return this;
            }
 
            public Builder vitaminD(int val){
                vitaminD = val;
                return this;
            }
 
            public Supplements build(){
                return new NutritionalSupplements(this);
            }
        }
 
        private Supplements(Builder builder){
            vitaminA = builder.vitaminA;
            vitaminB = builder.vitaminB;
            vitaminC = builder.vitaminC;
            vitaminD = builder.vitaminD;
        }
}

이렇게 생성한 builder 는 다음과 같이 사용한다.

public static void main(String[] args) {
    Supplements tablet = new Supplements
            .Builder(100, 50)
            .vitaminC(50)
            .vitaminD(60)
            .build();
}

영양제 클래스 밑에 builder 클래스가 있고, 해당 클래스를 외부에서 부를 때 생성자 builder를 부른다.

builder로 자기자신을 리턴해주면서 다른 필요한 영양소도 설정해준다.

그 다음 build 메소드를 불러 영양제의 매개변수로 자기자신을 넣는다

이렇게 하면 영양제의 생성자가 Builder를 받아 객체를 만들고, 이렇게 만들어진 객체가 호출된 쪽에 반환되는 형식이다.

정리하자면, 빌더패턴을 적용하면 다음과 같은 장점이 있다.

  • 불필요한 생성자의 제거
  • 데이터의 순서에 상관없이 객체 생성 가능
  • 여러 객체를 순환하면서 객체 생성 가능
  • 특정 필드는 빌더 객체가 채울 수 있도록 할 수 있다.
  • 명시적 선언으로 이해하기가 쉽고 각 인자가 어떤 의미인지 알기 쉽다.(가독성)
  • 생성된 객체에 대해 접근할 수 있는 setter 메서드가 없으므로 변경 불가능한 객체를 만들 수 있다.(객체불변성)
  • build() 메서드를 통해서 한번에 객체를 생성하므로 객체 일관성이 깨지지 않는다.
  • build() 메서드가 null인지 체크해주므로 검증이 가능한다. (set 하지 않은 객체의 변수에 대해 get을 하게되는경우 NPE 등의 예외 발생)

그렇다고 단점이 없는 것은 아니다

  • builder 생성 비용이 큰 건 아니지만, 민감한 상황에서는 문제가 될 수 있다.
  • 직접 builder 패턴을 작성하는 경우 코드가 길어지기때문에 매개변수가 4개 이상은 되어야 효과가 좋다.
  • 그러나 또 너무 많은 매개변수를 갖는 경우에는 코드가 너무 길어져서 문제가 될 수 있다 ^^; (이런 경우에는 객체를 분리하자)

필요할 때 잘 써먹어보도록 하자!


Lombok @Builder

사실 위처럼 builder 클래스를 직접 구현하지 않아도 편리하게 롬복의 @Builder 어노테이션을 이용해서 만들 수 있다.

클래스 or 생성자위에 어노테이션을 붙여주면 알아서 빌더패턴 코드가 빌드된다.

(생성자 위에 사용했을 땐, 생성자에 포함되어있는 필드들을 대상으로 생성된다.)

public class Supplements { 
    private int vitaminA;
    private int vitaminB;
    private int vitaminC;
    private int vitaminD;
 
    @Builder
    public Supplements(int vitaminA, int vitaminB, int vitaminC, int vitaminD) {
        this.vitaminA = vitaminA;
        this.vitaminB = vitaminB;
        this.vitaminC = vitaminC;
        this.vitaminD = vitaminD;
    }
}

사용법은 동일하게 Builder 객체 생성해서 값을 넣어주면 된다!

어노테이션을 사용해서 Builder를 사용하는 방법은 추가로 정리하도록 하겠다!

profile
개발하는 중국학과 사람

0개의 댓글