본문 바로가기
Java

Lombok의 @Builder를 사용해야 하는 이유와 주의점

by 풀잎 :) 2022. 8. 9.

안녕하세요.

 

오늘은 Java Library인 Lombok의 @Builder에 대해서 이야기해보려고 합니다.

 

Lombok의 @Builder

Lombok은 Java의 Boilerplate 코드를 상당히 줄여줄 수 있어서 자주 사용하는 라이브러리입니다.

반복적이고 지루한 코드 작성을 어노테이션 단 하나로 줄여주는 굉장한 기능을 제공해줍니다.

 

그런데 Lombok은 강력한 기능을 제공해주는 만큼 부작용도 있기 때문에 주의해서 사용해야 합니다.

 

Lombok에서 제공해 주는 기능이 많지만 그 중 @Builder에 대해서 글을 써보려고 합니다.

(@Builder에 대해 알고 계시는 분들은 2번 주의점 부분부터 보시면 됩니다.)

 

 

1. Builder 패턴을 사용해야 하는 이유

Lombok @Bilder는 빌더 패턴으로 객체를 생성할 수 있도록 도와줍니다.

(여기서 말하는 빌더 패턴은 Effective Java에서 조슈아 블로크가 소개한 빌더 패턴을 말합니다.)

 

아래와 같은 Member Class가 있을 때, 보통 객체를 생성하는 방법은 다음과 같습니다.

 

// 점층적 생성자 패턴
public class Member {
    private String id;
    private String name;
    private int point;
    
    public Member(String id) {
    	this.id = id;
    }
    
    public Member(String id, String name) {
    	this.id = id;
        this.name = name;
    }
    
    public Member(String id, String name, String point) {
    	this.id = id;
        this.name = name;
        this.point = point;
    }
}

// 객체 생성 예시
Member member1 = new Member("1");
Member member2 = new Member("2", "회원2");
Member member3 = new Member("3", "회원3", 0);

// 자바 빈즈 패턴
public class Member {
    private String id;
    private String name;
    private int point;
    
    public Member() {
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPoint(int point) {
        this.point = point;
    }
}

// 객체 생성 예시
Member member = new Member();
member.setId("1");
member.setName("회원1");
member.setPoint(10000);

 

첫 번째 방법은 생성자를 만들어 객체를 생성하는 방법입니다.

 

일반적인 객체 생성 방법이지만 단점이 있다면 새로운 멤버 변수가 추가될 경우 새로 생성자를 만들어 줘야 하고 특정 멤버 변수만 가지고 생성하고 싶을 때도 생성자를 새로 생성해야 하는 번거로움이 있습니다.

 

그 때문에 멤버 변수의 개수에 따라 코드의 크기 기하급수적 커질 수 있다는 점과 멤버 변수의 개수가 많으면 가독성이 떨어진다는 점이 단점입니다.

 

 

두 번째 방법은 기본생성자를 이용해 객체를 생성하고 setter를 이용해 값을 입력해주는 방법입니다.

 

언뜻 보면 두 번째 방법은 멤버 변수가 추가될 때 단순히 setter method만 추가해주면 되기 때문에 첫 번째 방법보다 좋은 방법처럼 보입니다.

 

그리나 두 방법은 첫 번째 방법 보다도 좋지 않을 수 있는 데, setter method를 이용해 값을 입력하기 때문에 불변 객체를 생성할 수 없고, 하나의 객체를 생성하는 데 여러 setter method를 호출해야 하고, 객체가 생성이 완료될 때까지 일관성이 깨진 상태로 존재한다는 점이 큰 단점입니다.

 

 

때문에 Effetive Java에서는 생성자의 인자가 많은 경우 빌더 패턴을 이용해 객체를 생성하는 것을 고려하라고 되어 있는데 빌더 패턴을 사용하면 아래 코드처럼 객체를 생성할 수 있습니다.

 

// 빌더 패턴
public class Member {
    private String id;
    private String name;
    private int point;

    private Member(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.point = builder.point;
    }

    public static class Builder {
        private String id;
        private String name;
        private int point;

        public Builder() {
        }

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

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

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

// 객체 생성
Member member = new Member.Builder()
	.id("1")
	.name("회원")
	.point(1000)
	.build();

 

빌더 패턴을 사용했을 때의 장점은 객체를 생성할 때 유연하게 생성할 수 있다는 점입니다. 빌더 패턴을 사용하면 특정 멤버 변수에만 값을 입력해서 객체를 생성할 수 있습니다. 그리고 점진적 생성자 패턴에 비해 가독성이 좋고, 자바 빈즈 패턴에서는 불가능한 불변 객체를 생성하는 것도 가능합니다.

 

다만 단점은 다른 패턴에 비해 필요한 코드량이 많다는 점입니다. 매번 Builder 클래스를 만드는 것은 번거롭다는 점이 단점이죠. 멤버 변수의 개수가 적다면 빌더 패턴으로 객체를 생성하는 방법은 꽤 귀찮은 일입니다.

 

하지만! Lombok의 @Builder를 사용하면 엄청 손쉽게 빌더 패턴을 이용할 수 있습니다.

 

// Lombok의 @Builder를 이용
@Builder
public class Member {

    private String id;
    private String name;
    private int point;
}

// 객체 생성
Member member = Member.Builder()
	.id("1");
    	.name("회원1");
    	.point(1000);

 

단순히 class에 @Builder 어노테이션을 붙여주는 것만으로 빌더 패턴을 사용할 수 있습니다. Lombok의 @Builder를 이용하면 빌더 패턴을 사용할 때 Builder 클래스를 만들어야 한다는 단점을 지운채로 빌더 패턴을 사용할 수 있게 됩니다.

 

2. Lombok의 @Builder를 사용할 때의 주의점

복잡한 코드로 작성해야 하는 빌더 패턴을 어노테이션 한 개로 구현해주는 Lombok이지만 사용 시 주의해야 할 점이 있습니다. @Builder는 클래스 위, 생성자 위, 메서드 위에 붙여서 사용할 수 있는데 클래스 위에 붙여서 사용할 때 잘 모르고 넘어가는 부분이 있습니다.

 

그건 바로 모든 멤버 변수를 가지고 default 접근제어자로 생성자를 만든다는 것입니다.

 

이해하기 쉽도록 @Builder를 클래스 위에 붙였을 때, Lombok이 생성한 코드를 보여 드리겠습니다.

 

// 클래스 위에 @Builder를 붙인 경우
@Builder
public class Member {
    private String id;
    private String name;
    private int point;
}

// Lombok이 생성한 코드
public class Member {
    private String id;
    private String name;
    private int point;

    // default 접근제어자로 생성자를 생성
    Member(final String id, final String name, final int point) {
        this.id = id;
        this.name = name;
        this.point = point;
    }

    public static Member.MemberBuilder builder() {
        return new Member.MemberBuilder();
    }

    public static class MemberBuilder {
        private String id;
        private String name;
        private int point;

        MemberBuilder() {
        }

        public Member.MemberBuilder id(final String id) {
            this.id = id;
            return this;
        }

        public Member.MemberBuilder name(final String name) {
            this.name = name;
            return this;
        }

        public Member.MemberBuilder point(final int point) {
            this.point = point;
            return this;
        }

        public Member build() {
            return new Member(this.id, this.name, this.point);
        }
    }
}

 

default 생성자가 생긴 것을 확인 할 수 있습니다.

 

방금 전에도 말했듯이 여기서 주의해야 할 점이 2가지가 생기는데요.

 

첫 번째는 모든 멤버 변수를 대상으로 생성자를 생성한다는 점,

두 번째는 default 접근제어자로 생성자를 생성한다는 점입니다.

 

@Builder를 클래스 위에 붙이는 경우 모든 멤버 변수를 대상으로 생성자를 생성합니다. 모든 멤버 변수를 가지고 객체를 생성해야 하는 경우엔 상관없지만 특정 멤버 변수를 제외하고 생성하고 싶을 때는 문제가 될 수 있습니다.

 

그리고 Lombok의 공식문서를 보면 다음과 같은 사실을 알 수 있습니다.

 

Finally, applying @Builder to a class is as if you added @AllArgsConstructor(access = AccessLevel.PACKAGE) to the class and applied the @Builder annotation to this all-args-constructor. This only works if you haven't written any explicit constructors yourself. If you do have an explicit constructor, put the @Builder annotation on the constructor instead of on the class.

 

클래스 위에 @Builder를 달았을 때, 명시적으로 생성자를 만들지 않으면 @AllArgsConstructor(access = AccessLevel.PACKAGE)를 추가한 것과 같다는 언급이 있습니다. 즉 default 접근제어자로 생성자를 생성한다는 말입니다. 접근제어자가 default이기 때문에 같은 패키지 내에서는 빌더 패턴이 아닌 생성자를 직접 호출해서 객체를 생성할 수 있게 됩니다.

 

같은 패키지 안에서도 빌더 패턴으로만 객체를 생성하도록 강제하고 싶으면 아래 코드처럼 private 생성자 위에 @Builder를 달아주면 됩니다.

 

// private 생성자로 제한하는 방법
public class Member {
    private String id;
    private String name;
    private int point;
    
    @Builder
    private Member(String id, String name, int point) {
        this.id = id;
        this.name = name;
        this.point = point;
    }
}

// 특정 멤버 변수를 제외하고 객체를 생성하도록 강제하는 방법
public class Member {
    private String id;
    private String name;
    private int point;
    
    // 객체를 생성할 때 point를 빼고 생성하도록 강제
    @Builder
    private Member(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

private 생성자를 만들고 생성자 위에 @Builder를 달아주게 되면 제가 말한 2가지 단점을 무시하고 사용할 수 있습니다.

 

정리하면 @Builder를 클래스 위에 달아줄 때 2가지 문제점이 생길 수 있지만 @Builder를 생성자 위에 달아줌으로써 문제를 해결할 수 있습니다. 그러므로 필요한 경우에는 생성자 위에 @Builder를 달아서 사용하면 됩니다.

 

그럼 긴 글 읽어주셔서 감사합니다 :)

댓글