본문 바로가기

Java

@NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder

@NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder를 함께 사용할때 주의할 점에 대해서 서술합니다.
"왜" 안되는지와 "왜" 이렇게 해결 할 수 있는지에 대해 집중하여 서술합니다.

1. 왜 NoargsConstructor(AccessLevel.PROTECTED)를 사용하는가?

@NoargsConstructor(AccessLevel.PROTECTED)

 

Entity나 DTO를 사용할때 @NoargsConstructor(AccessLevel.PROTECTED) 어노테이션을 많이 사용하는 편입니다.

기본 생성자의 접근 제어를 PROTECTED로 설정해놓게 되면 무분별한 객체 생성에 대해 한번 더 체크할 수 있는 수단이 되기 때문입니다.

 

예를 들어 User라는 Class는 name, age, email 정보를 모두 가지고있어야만 되는 상황일경우에

기본 생성자를 막는것은 이를 도와주는 좋은 수단이 됩니다.

 

만약 기본 생성자의 권한이 public 이라면 아래 상황이 발생하게 됩니다.

 

/// User.java
@Getter
@Setter
@NoArgsConstructor
public class User {
    private String name;
    private Long age;
    private String email;
}


/// Main.java
public static void main(String[] args) {
    User user = new User();
    user.setName("testname");
    user.setEmail("test@test.com");
    
    /// age가 설정되지 않았으므로 user는 완전하지 않은 객체
}

 

User의 멤버변수들을 설정할 방법이 없으니 Setter를 만들어서 값을 설정하지만
실수로 setAge()를 누락할 경우 객체는 불완전한 상태가 되버립니다.

 

하지만 아래와 같이 변경하게 되면 IDE 단계에서 누락을 방지할 수 있게 되어 훨씬 수월하게 작업할 수 있게 되고 이는 엄청난 가치입니다.

 

/// User.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    private String name;
    private Long age;
    private String email;
    
    public User(Long age, String email) {
    	/// 파라미터가 두 개인 경우 name은 default 설정
        this.name = "blank name";
        this.age = age;
        this.email = email;
    }
}


/// Main.java
public static void main(String[] args) {
    User user = new User(15, "test@a.com");
    
    /// 기본 생성자가 없고 객체가 지정한 생성자를 사용해야하기 때문에
    /// 무조건 완전한 상태의 객체가 생성되게 된다.
}

2. @NoargsConstructor(AccessLevel.PROTECTED)와 @Builder를
함께 사용하면 좀 더 간단하지 않을까

보통 @Builder를 사용하게 되면 조금 더 자유롭게 객체를 생성할 수 있게 됩니다.

@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    private String name;
    private Long age;
    private String email;
}

 

위 처럼 @NoargsConstructor(AccessLevel.PROTECTED)와 @Builder를 함께 사용해서 compile하면 바로 에러가 발생합니다.

 

@Builder는 Class(Type)이 Target일 경우에 생성자 유무에 따라 아래와 같이 동작합니다.

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

위 과정 이후에 모든 멤버 변수를 설정할 수 있는 Builder Class를 생성합니다.

그런데 만약 @NoargsConstructor(AccessLevel.PROTECTED)라는 생성자가 있는 상태에서 @Builder를 사용하면
아래와 같이 컴파일 되게 됩니다.

 

public class User {
    private String name;
    private Long age;
    private String email;


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

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

    public static class UserBuilder {
        private String name;
        private Long age;
        private String email;

        UserBuilder() {
        }

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

        public User.UserBuilder age(Long age) {
            this.age = age;
            return this;
        }

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

        public User build() {
            /// 일치하는 생성자가 없다.
            return new User(this.name, this.age, this.email); 
        }
    }
}

 

User에는 기본 protected 생성자가 이미 존재해서 따로 생성자를 만들지 않았지만 
build()를 보면 모든 파라미터를 받는 생성자로 객체를 build 하려하는 과정에서 알맞는 생성자를 찾을 수 없게 되었습니다.

 

3. @NoargsConstructor(AccessLevel.PROTECTED)와 @Builder를 함께 사용할 수 없을까?

불가능하다고 하더라도 @NoargsConstructor(AccessLevel.PROTECTED)와 @Builder는

의미있는 객체를 생성하기 위한 좋은 방법이자 제약조건입니다.

함께 사용하기 위한 방법은 위 예제들을 정확히 이해했다면 어렵지 않게 해결할 수 있습니다.

해결 방법

1. @AllArgsConstructor

@Builder
@AllArgsConstructor
public class User {
    private String name;
    private Long age;
    private String email;
}

 

모든 멤버변수를 받는 생성자가 없는것이 이유이기 때문에 모든 멤버변수를 받는 생성자를 만들어주면 됩니다.

 

 

2. 생성자에 설정하는 @Builder

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    private String name;
    private Long age;
    private String email;

    @Builder
    public User(Long age, String email) {
        this.name = "test_name";
        this.age = age;
        this.email = email;
    }

    @Builder
    public User(String name, Long age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

 

생성자별로 설정되는 멤버변수 내용을 정의하고 생성자에 @Builder를 설정하게되면

해당 생성자를 사용하는 Builder가 생성되어 의미있는 객체만 생성할 수 있게 됩니다.

 

위 코드를 Java코드만으로 변환하여 작성하게 되면 아래와 같습니다.

 

public class User {
    private String name;
    private Long age;
    private String email;

    public User(Long age, String email) {
        this.name = "test_name";
        this.age = age;
        this.email = email;
    }

    public User(String name, Long age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

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

    protected User() {
    }

    public static class UserBuilder {
        private Long age;
        private String email;
        private String name;

        UserBuilder() {
        }

        public User.UserBuilder age(Long age) {
            this.age = age;
            return this;
        }

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

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

'Java' 카테고리의 다른 글

@NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder  (0) 2020.07.31
[Java8 비동기] CompletableFuture  (0) 2020.07.27