티스토리 뷰

 

저번 글에 이어서 Effective Java 스타일의 Builder 패턴과 @Builder 어노테이션에 대해 알아보자.

먼저 이번 글에서 계속해서 사용할 예제 클래스는 다음과 같다.

@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Home {

    private String basement;
    private String structure; //required
    private int door; //required
    private int window; //required
    private String roof; //required
    private String interior;
}

 

 

Effective Java의 Builder Pattern

앞의 글과는 다르게 Home 클래스 객체를 생성하는 Builder 클래스를 Home의 내부 클래스로 구현해 볼 것이다. 일반적으로 Builder 클래스의 이름은 생성하고자 하는 객체의 클래스 이름(여기서는 Home) + -Builder 형식으로, 이 경우에는 HomeBuilder가 된다. HomeBuilder 클래스를 생성하는 방법은 다음과 같다.

 

 

1. Home 클래스의 모든 필드를 가지는 HomeBuilder 클래스를 Home 클래스 내부에 static으로 정의한다.

2. HomeBuilder에 각 필드의 이름을 메소드 이름으로 가지는 setter를 정의한다. 이때 각 메소드에서는 chaining을 위해 자기 자신(HomeBuilder)을 반환한다.

3. 마지막으로 생성된 Home 객체를 반환하는 build() 메소드를 정의한다.

 

@AllArgsConstructor
public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;

    public static class HomeBuilder {

        private String basement;
        private String structure;
        private int door;
        private int window;
        private String roof;
        private String interior;

        public HomeBuilder basement(String basement) {
            this.basement = basement;
            return this;
        }
        
        public HomeBuilder structure(String structure) {
            this.structure = structure;
            return this;
        }
        
        public HomeBuilder door(int door) {
            this.door = door;
            return this;
        }
        
        public HomeBuilder window(int window) {
            this.window = window;
            return this;
        }
        
        public HomeBuilder roof(String roof) {
            this.roof = roof;
            return this;
        }
        
        public HomeBuilder interior(String interior) {
            this.interior = interior;
            return this;
        }
        
        public Home build() {
            return new Home(basement, structure, door, window, roof, interior);
        }
    }
}

 

이후 다음과 같이 Home 객체 생성이 가능하다. @Builder 어노테이션을 한번이라도 써본 적이 있다면 익숙한 형태의 코드일 것이다. 

public class Main {

    public static void main(String[] args) {
        Home home = new Home.HomeBuilder()
                .basement("basement")
                .structure("structure")
                .door(1)
                .window(4)
                .roof("roof")
                .interior("interior")
                .build();
    }
}

 

앞선 글에서 구현했던 빌더 패턴과 굳이 비교하자면, Builder 클래스를 내부 클래스로 정의하여 전체 코드의 복잡성을 그나마 줄였으며 각 setter 메소드에서 자기 자신을 반환함으로써 객체 생성 코드를 chaining하는 것이 가능하다. 

 

 

여기서 structure, door, window, roof 필드는 필수적이다. 하지만 위의 HomeBuilder에서 해당 필드에 대한 setter를 호출하지 않으면 각 필드는 null이 된다. 다음과 같이 HomeBuilder 생성자를 정의하면 structure, door, window, roof 필드에 대한 설정을 강제할 수 있다. 

@AllArgsConstructor
public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;

    public static class HomeBuilder {

        private String basement;
        private String structure;
        private int door;
        private int window;
        private String roof;
        private String interior;

        public HomeBuilder(String structure, int door, int window, String roof) {
            this.structure = structure;
            this.door = door;
            this.window = window;
            this.roof = roof;
        }

        public HomeBuilder basement(String basement) {
            this.basement = basement;
            return this;
        }

        public HomeBuilder interior(String interior) {
            this.interior = interior;
            return this;
        }

        public Home build() {
            return new Home(basement, structure, door, window, roof, interior);
        }
    }
}
public class Main {

    public static void main(String[] args) {
        Home home = new Home.HomeBuilder("structure", 1, 4, "roof")
                .basement("basement")
                .interior("interior")
                .build();
    }
}

 

 


Lombok의 @Builder 어노테이션

Builder Pattern을 적용하고 싶은 모든 클래스마다 내부에 -Builder 클래스를 매번 정의해주기도 힘들고 해당 클래스의 복잡도 또한 높아진다. 어떻게 보면 -Builder 클래스를 정의하는 것은 반복적인 작업에 가까운데... 이 귀찮은 일을 대신해주는 아주 편리한 어노테이션이 있다. 바로 @Builder 어노테이션이다. 이제 Home 클래스에 @Builder 어노테이션을 추가해보자.

@Builder
public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;
}

 

 

이렇게 @Builder 어노테이션 하나만으로 위에서는 직접 정의한 HomeBuilder를 대체할 수있다. 

컴파일된 Home.class 파일은 다음과 같다. 어떠한가? 위에서 직접 정의한 HomeBuilder와 거의 똑같다. 다른 부분은 HomeBuilder 생성자의 접근 제한자를 protected로 바꾸고 Home 클래스의 static 메소드인 builder()를 통해 HomeBuilder 객체를 생성한다는 점이다. 

public class Home {
    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;

    Home(final String basement, final String structure, final int door, final int window, final String roof, final String interior) {
        this.basement = basement;
        this.structure = structure;
        this.door = door;
        this.window = window;
        this.roof = roof;
        this.interior = interior;
    }

    public static Home.HomeBuilder builder() {
        return new Home.HomeBuilder();
    }

    public static class HomeBuilder {
        private String basement;
        private String structure;
        private int door;
        private int window;
        private String roof;
        private String interior;

        HomeBuilder() {
        }

        public Home.HomeBuilder basement(final String basement) {
            this.basement = basement;
            return this;
        }

        public Home.HomeBuilder structure(final String structure) {
            this.structure = structure;
            return this;
        }

        public Home.HomeBuilder door(final int door) {
            this.door = door;
            return this;
        }

        public Home.HomeBuilder window(final int window) {
            this.window = window;
            return this;
        }

        public Home.HomeBuilder roof(final String roof) {
            this.roof = roof;
            return this;
        }

        public Home.HomeBuilder interior(final String interior) {
            this.interior = interior;
            return this;
        }

        public Home build() {
            return new Home(this.basement, this.structure, this.door, this.window, this.roof, this.interior);
        }
    }
}

 

그러나 문제가 있다. 필수적인 필드인 structure, door, window, roof에 대한 setter를 호출하지 않으면 이 또한 null 값이 설정된다. 우리가 직접 Builder를 정의했던 경우에는 HomeBuilder 생성자의 매개변수로 해당 필드들을 넘기도록 하였는데 @Builder 어노테이션을 사용할 때에는 어떻게 해야 할까?

 

 

 

@Builder에서 필수적인 필드 설정 강제하기 

아주 간단하게 required한 필드에 @NonNull 어노테이션을 붙여줌으로써 builder() 메소드에서 해당 필드에 대한 setter를 호출하도록 유도할 수는 있다. 하지만 이 또한 컴파일 오류가 아닌 NPE(NullPointerException), 즉 실행 오류가 발생한다. 일반 생성자와 같이 필드 설정을, 즉 필드를 매개변수로 넘겨주는 것을 강제하고 싶다면 @Builder 어노테이션에 의해 생성되는 builder() 메소드를 오버로딩해주면 된다. 

@Builder
public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;

    public static HomeBuilder builder(String structure, int door, int window, String roof) {
        return new HomeBuilder()
                .structure(structure)
                .door(door)
                .window(window)
                .roof(roof);
    }
}

 

 

이렇게 builder() 메소드를 오버로딩해주면 컴파일된 Home.class 파일에는 다음의 builder() 메소드 하나만이 존재하게 되어 필수적인 필드들에 대한 설정을 강제할 수 있다.

public static Home.HomeBuilder builder(String structure, int door, int window, String roof) {
    return (new Home.HomeBuilder()).structure(structure).door(door).window(window).roof(roof);
}
public static void main(String[] args) {
    Home home = Home.builder("structure", 1, 4, "roof")
            .basement("basement")
            .interior("interior")
            .build();
}

 

 

 

@Builder의 설정값

@Builder 어노테이션에 대해 좀 더 자세히 알아보는 것을 끝으로 글을 마무리하려고 한다. @Builder 어노테이션이 정의된 파일을 보면 다음과 같이 @TargetTYPE, METHOD, CONSTRUCTOR로 설정되어 있는 것을 알 수 있다. 따라서 @Builder 어노테이션은 클래스, 메소드, 생성자 위에 위치시킬 수 있다. 

 

Builder 어노테이션

 

 

다음과 같이 일부 필드만을 매개변수로 받는 생성자 위에 @Builder를 위치시키면, 생성되는 HomeBuilder를 통해서는 structure, door, window, roof 필드값만을 설정할 수 있다.

public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;

    @Builder
    public Home(String structure, int door, int window, String roof) {
        this.structure = structure;
        this.door = door;
        this.window = window;
        this.roof = roof;
    }
}

 

 

반면 메소드에 @Builder가 붙으면 VoidBuilder라는 이름의 빌더가 자동으로 생성된다.

public class Home {

    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;
    private List<String> rooms; //추가

    @Builder
    public void addRooms(List<String> rooms) {
        if (this.rooms == null) this.rooms = new ArrayList<>();
        this.rooms.addAll(rooms);
    }
}
public class Home {
    private String basement;
    private String structure;
    private int door;
    private int window;
    private String roof;
    private String interior;
    private List<String> rooms;

    public Home() {
    }

    public void addRooms(List<String> rooms) {
        if (this.rooms == null) {
            this.rooms = new ArrayList();
        }

        this.rooms.addAll(rooms);
    }

    public Home.VoidBuilder builder() {
        return new Home.VoidBuilder();
    }

    public class VoidBuilder {
        private List<String> rooms;

        VoidBuilder() {
        }

        public Home.VoidBuilder rooms(final List<String> rooms) {
            this.rooms = rooms;
            return this;
        }

        public void build() {
            Home.this.addRooms(this.rooms);
        }
    }
}

위와 같은 컴파일된 Home.class가 생성되는데 딱히 메소드에 @Builder 어노테이션을 사용할 일은 없을 것 같으므로 참고로만 알아두자. 

 

 

 

정말 마지막으로 @Builder 어노테이션의 각 속성과 의미는 다음과 같다. 

 

builderMethodName

Builder 클래스 객체를 반환하는 메소드 이름, 기본값 builder

 

buildMethodName

생성된 객체를 반환하는 Builder 클래스 내 메소드 이름, 기본값 build

 

builderClassName

Builder 클래스 이름, 기본값 생성하는 객체의 클래스 이름 + -Builder

 

toBuilder

toBuilder() 메소드 자동 생성 여부, 기본값 false

//Home 클래스의 메소드
public Home.HomeBuilder toBuilder() {
    return (new Home.HomeBuilder()).basement(this.basement).structure(this.structure).door(this.door).window(this.window).roof(this.roof).interior(this.interior);
}

 

access

Builder 클래스, builder(), build(), setter 메소드 등의 접근 제한자 설정, 기본값 PUBLIC

 

setterPrefix

setter 메소드의 prefix 설정

 

 

 

끝.

 

 

 

참고

https://stackoverflow.com/questions/29885428/required-arguments-with-a-lombok-builder

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함