@Builder, @All/NoArgsConstructor 제대로 알고 사용하자!

maketheworldwise·2022년 6월 11일
14


이 글의 목적?

프로젝트를 진행하면서 아무런 생각없이 @Builder, @AllArgsConstructor, @NoArgsConstructor 어노테이션을 같이 사용해왔다. 아무리 롬복이 편리함을 제공해주지만, 제대로 알지 않고 사용하면 독이 된다고 생각하여 이번에 정리해보고자 한다.

빌더 패턴 왜 사용하는가?

우선 빌더 패턴을 사용하는 이유부터 간단하게 알아보자. 구글링을 하면 다음과 같은 장점에 대해서 기술되어있다.

  • 필요한 데이터만 설정 가능
  • 유연성 확보
  • 가독성 향상
  • 불변성 확보

하단에 첨부한 링크에서 잘 설명이 되어있으니 해당 내용에 대해서는 과감하게 생략하자.

결국, 빌더 패턴을 사용하면 협업을 하거나 변경에 대한 요구 사항이 많은 경우에 대해 손쉽게 대처할 수 있다는 것이다.

이 부분은 직접 생각해본 내용인데, @AllArgsConstructor 어노테이션을 이용하거나 모든 필드를 파라미터로 가지는 생성자가 있을 경우에서 발생할 수 있는 문제를 해결할 수 있다고 생각했다.

@AllArgsConstructor는 선언된 필드의 순서대로 생성자를 생성해준다. 그리고 이를 이용하여 순서대로 파라미터를 지정하여 객체를 만든다고 가정했을 때, 제 3자가 선언된 필드 사이에 임의의 필드를 넣는다면 객체를 만드는 코드에서도 수정이 일어나야하며 협업하는 부분에서 문제가 발생한다.

예를 들어보면 다음과 같다.

  • 제 3자가 코드를 건들기 전
@AllArgsConstructor
public class User {
	private String name;
    private String phone;
    private int age;
}
User user = new User("kevin", "010-0000-0000", "29");
  • 제 3자가 코드를 건든 후
@AllArgsConstructor
public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;
}
// User user = new User("kevin", "010-0000-0000", "29"); -> 문제 발생!
User user = new User("kevin", "M", "010-0000-0000", "29");

@NoArgsConstructor

@NoArgsConstructor도 알아보자. @NoArgsConstructor는 기본 생성자를 생성해주는 어노테이션이다. 그리고 대부분의 코드에서는 AccessLevel.PROTECTED 속성을 부여하는데, 이는 무분별한 객체 생성에 대해 한번 더 체크할 수 있는 수단으로 설정해준다고 한다.

해당 속성을 부여하지 않았을 때의 문제를 살펴보자.

하단의 코드처럼 Setter를 이용하여 클래스 필드에 대한 값을 설정할 수 있을 때, 모든 필드가 값을 가져야만하는 상황이라고 가정해보자.

개발자의 실수로 클래스의 필드들 중 하나의 필드에 대한 값 설정을 누락시켰을 경우 객체는 불완전한 상태가 되어버린다.

@Getter
@Setter
@NoArgsConstructor
public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;
}
User user = new User();
user.setName("kevin");
user.setGender("M");
user.setPhone("010-0000-0000");

하지만, AccessLevel.PROTECTED 속성을 부여해주면 기본 생성자의 접근 제어가 되어 IDE 단계에서 누락을 방지할 수 있어 위의 문제를 해결할 수 있다. 즉, 기본 생성자의 생성을 방지하고 지정한 생성자를 사용하도록 강제하여 무조건 완전한 상태의 객체를 생성할 수 있도록 도움을 준다는 의미다.

@Getter
@Setter
@NoArgsConstructor
public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;
    
    public User(String name, String phone, int age) {
    	this.name = name;
        // Gender Default
        this.gender = "M";
        this.phone = phone;
        this.age = age;
    }
}
User user = new User("kevin", "010-0000-0000", "29");

@AllArgsConstructor

@NoArgsConstructor로 기본 생성자의 생성을 방지하고 @Builder를 이용하여 객체의 생성에 유연성을 더 해주면 좋겠지만, 이 두 개의 어노테이션을 함께 사용하려면 @AllArgsConstructor가 필요하다.

이유는 @Builder를 조금 더 살펴보면 이해할 수 있다. @Builder는 생성자 유무에 따라 다음과 같이 동작한다.

  • 생성자가 없는 경우 : 모든 멤버 변수를 파라미터로 받는 기본 생성자 생성
  • 생성자가 있을 경우 : 따로 생성자를 생성하지 않음

즉, 기본 생성자가 존재하기 때문에 @Builder에서는 생성자를 별도로 생성하지 않는데, 이 기본 생성자에는 접근 제한 속성이 부여되어있어 문제가 발생하는 것이다. 이 두 개의 어노테이션을 부여한 클래스를 컴파일 해보면 문제가 발생하는 위치를 쉽게 확인할 수 있다.

public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;

    // @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
    protected User() {}

    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    public static class UserBuilder {
        private String name;
        private String gender;
        private String phone;
        private int age;

        UserBuilder() {
        }

        public User.UserBuilder name(String name) {
            this.name = name;
            return this;
        }
        
        public User.UserBuilder gender(String gender) {
            this.gender = gender;
            return this;
        }
        
        public User.UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }
        
        public User.UserBuilder age(int age) {
            this.age = age;
            return this;
        }

        public User build() {
            /// 일치하는 생성자가 없어 문제 발생
            return new User(this.name, this.gender, this.phone, this.age);
        }
    }
}

결국 문제는 일치하는 생성자가 없어 발생하는 문제이니 생성자를 만들어주면 되는게 아닌가?! 이 말을 다른 말로 하자면, 모든 필드를 파라미터로 가지는 @AllArgsConstructor 하나만 추가해주면 해결할 수 있다는 의미다.

생성자에 @Builder 설정

개인적으로는 @AllArgsConstructor를 이용하는 편이 쉽고 코드의 길이도 줄여줄 수 있어 선호하는 편이지만, @AllArgsConstructor만이 유일한 해결책은 아니다.

또 다른 방안으로는 직접 생성자를 만들어주고, 빌더 패턴에서 해당 생성자를 사용하도록 @Builder를 추가해주면 된다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;

    @Builder
    public User(String name, String phone, int age) {
        this.name = name;
        // Gender Default
        this.gender = "M";
        this.phone = phone;
        this.age = age;
    }

    @Builder
    public User(String name, String gender, String phone, int age) {
        this.name = name;
        this.gender = gender;
        this.phone = phone;
        this.age = age;
    }
}

생성자에 @Builder를 붙여 컴파일 해보면 다음과 같다.

public class User {
	private String name;
    private String gender;
    private String phone;
    private int age;

    public User(String name, String phone, int age) {
        this.name = name;
        // Gender Default
        this.gender = "M";
        this.phone = phone;
        this.age = age;
    }

    public User(String name, String gender, String phone, int age) {
        this.name = name;
        this.gender = gender;
        this.phone = phone;
        this.age = age;
    }
    
    // @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    protected User() {
    }

    public static class UserBuilder {
        private String name;
        private String gender;
        private String phone;
        private int age;

        UserBuilder() {
        }

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

		public User.UserBuilder gender(String gender) {
            this.gender = gender;
            return this;
        }
        
        public User.UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }
        
        public User.UserBuilder age(int age) {
            this.age = age;
            return this;
        }
        
        public User build() {
            return new User(this.name, this.gender, this.phone, this.age);
        }
    }
}

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글