[22.08.04] Daily 회고록 (Builder 패턴, @Builder, @Singular)
일간 회고록(TIL)

[22.08.04] Daily 회고록 (Builder 패턴, @Builder, @Singular)

[개인공부]

Builder : Design Pattern

- 프로젝트를 진행하다가 하나의 오류가 생겼다. 분명 Entity의 List필드를 ArrayList로 초기화해서 가지고 있는데 계속 NULL값인 것이다. 이로인해 지속적으로 NullPointerException이 발생했고, 이는 무심코 사용했던 Builder패턴 때문이였다.

Builder는 디자인 패턴 중 생성 패턴으로, 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.

 

기존 생성자는 필요로하는 필드에 대한 인자를 정의하고 작성한 순서 그대로 입력 받았어야 했던 반면, Builder는 받고자하는 필드의 순서에 상관없이 생성할 수 있다.

Spring의 @Builder 어노테이션은 기본적으로 생성자와 클래스에 붙을 수 있는데,

클래스에 어노테이션된 경우 모든 필드를 argument로 가진 private 생성자가 만들어져 이 생성자에 Builder 어노테이션을 붙인 것과 동일하게 동작한다.

 

Builder로 생성된 기본 생성 방법이 모든 필드를 argument로 가지면서 입력받지 않은 필드를 null로 만들었고, 기본 클래스 단위 변수는 null이 되어도 대입을 받으면 사용할 수 있었지만, List는 null이 되면 모든 기능을 상실해버리기 때문에 발생한 문제였다.  

해결방법은 생성자를 따로 생성하여 @Builder를 붙여 내부에서 초기화거나 @Singular를 이용하는 방법이다. 

이 방법을 이용하면 List에 추가할 때 따로 List를 접근할 수 있는 접근자를 만들지 않아도 된다.

public class BuildMe {
    private String username;
    private int age;

    BuildMe(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public static BuildMe.BuildMeBuilder builder() {
        return new BuildMe.BuildMeBuilder();
    }

    public static class BuildMeBuilder {
        private String username;
        private int age;

        BuildMeBuilder() {
        }

        public BuildMe.BuildMeBuilder username(String username) {
            this.username = username;
            return this;
        }

        public BuildMe.BuildMeBuilder age(int age) {
            this.age = age;
            return this;
        }

        public BuildMe build() {
            return new BuildMe(this.username, this.age);
        }

        public String toString() {
            return "BuildMe.BuildMeBuilder(username=" + this.username + ", age=" + this.age + ")";
        }
    }
}

@Builder 어노테이션을 붙였을 때 생성된 클래스 파일을 바이트 코드로 분석해보면 다음과 같이 변한다고 한다.

 

그래서 필수 입력을 받아야하는 필드를 제한하기 위해 BuilderClass를 따로 하나 작성했고, 이 메서드 내부에서 다른 값들을 초기화해줌으로써 생성자와 동일한 기능을 하되, 더욱 유동적으로 생성가능하도록 만들었다.

@Singular, 이 어노테이션을 사용하게되면 리스트같은 컬렉션 객체를 빌더 패턴으로 다룰 때 리스트 객체 자체를 넘기는 것이 아닌 해당 리스트에 요소를 추가하는 방식으로 생성할 수 있다. 모든 자료구조에 대해서 지원하진 않고 List, Set등 자주 사용되는 것만 가능하며 List필드 위에 붙이게 되면 다음과 같은 메서드가 추가된다.

@Singular("alias") private List<String> alias;

public BuildMe.BuildMeBuilder alias(String alias) {
            if (this.alias == null) {
                this.alias = new ArrayList();
            }

            this.alias.add(alias);
            return this;
        }

        public BuildMe.BuildMeBuilder alias(Collection<? extends String> alias) {
            if (alias == null) {
                throw new NullPointerException("alias cannot be null");
            } else {
                if (this.alias == null) {
                    this.alias = new ArrayList();
                }

                this.alias.addAll(alias);
                return this;
            }
        }

        public BuildMe.BuildMeBuilder clearAlias() {
            if (this.alias != null) {
                this.alias.clear();
            }

            return this;
        }

        public BuildMe build() {
            List alias;
            switch(this.alias == null ? 0 : this.alias.size()) {
            case 0:
                alias = Collections.emptyList();
                break;
            case 1:
                alias = Collections.singletonList((String)this.alias.get(0));
                break;
            default:
                alias = Collections.unmodifiableList(new ArrayList(this.alias));
            }

            return new BuildMe(this.username, this.age, alias);
        }
}

그렇기 때문에 인스턴스를 생성하는 파트에서 alias("ALIAS1")처럼 하면 그 객체를 초기화 하는 것(리스트를 새로 대입)이 아니라 리스트에 add할 수 있도록 자동설계가 된다. 이를 참고하면 해결할 수 있다.

 

 

디자인 패턴의 원리를 제대로 파악하지 않고 사용했다가 생겼던 문제들을 디자인 패턴을 구조를 파악하면서 해결할 수 있었고, 앞으로 디자인 패턴을 공부할 때 구조를 더욱더 확인해보는 습관을 가질 것 같다. 

출처

 

Lombok @Builder의 동작 원리

보일러플레이트 메서드(getter/setter, constructor 등)를 직접 작성하지 않아도 대신 작성해주는 Lombok를 최근에 많이 활용하고 있다. 그나마 setter 메서드같은 경우는 값을 변경시키는 메서드는 그 목

velog.io

 

 

#Spring @Builder #스프링 빌더 null #spring JPA List null error #builder design pattern #빌더오류 #@Singular