• @NoArgsConstructor(access = AccessLevel.PROTECTED)를 이용하여 의미있는 생성자를 만들어 보자( + @Bulider)

    2022. 6. 6.

    by. 내이름은 킹햄찌

    목표

    @NoArgsConstructor와 함께 생성자를 만드는 어노테이션들을 알아보고 의미 있는 생성자를 만드는 방법을 알아보자.

     

     

    내용

    Entity위에서 자주 볼 수 있는 어노테이션은@NoArgsConstructor, @AllArgsConstructor ,@Bulider 가 있다.

    이 어노테이션 들의 공통된 기능은 생성자를 만들어 주것인데 Entity에는 생성자가 왜필요할까 

    그 이유는 JPA에서 Entity 프록시를 만들기 위해 반드시 기본 생성자가 하나를 생성해야 하기 때문이다.

    하지만 위의 어노테이션 없이도 @Entity 에서도 기본 생성자는 만들어 진다.

    그렇다면 위의 어노테이션들을 어떤 이유에 사용하는 것인가

     

     

     

    1. @NoArgsConstructor

    어노테이션의 이름과 같이 파라미터가 없는 기본생성자이다. 기본생성자는 @Entity 에서 만들어 주는데도 어노테이션을 사용하는 이유는 @NoArgsConstructor(access = AccessLevel.PROTECTED) 와 같이 접근 제한을 하여 기본 생성자의 무분별한 생성를 막아서 의도하지 않은 엔티티를 만드는 것을 막을 수 있기 때문이다.

    기본 생성자의 권한이 public일 경우 userId, nickname, password를 모두 가져야하는 객체를 생성할때를 가정해보자

     

     

    생성자 제한을 걸지 않았을 경우

    @NoArgsConstructor
    @Setter
    @Getter
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    }
    
    public static void main(String[] args) {
    		Member member = new Member();
    		member.setUserId = "newid123"
    		member.setPassword = "security"
    }
    

    위와같이 파라미터가 없는 기본생성자를 생성하게 되면 나머지 맴버변수들의 값들을 전달하기 위해 setter를 사용할 수 밖에 없고, setter를 이용하여 id와 nickname을 지정했지만 nickname을 set하지 않아 nickname을 가지지 않는 불완전한 객체가 만들어지게 된다.

     

     

    생성자 제한을 걸게될 경우

    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    
    		public Member(String userId, String password){
    				this.userId = userId;
    				this.password = password;
    				this.userId = userId + "님"
    		}
    }
    
    public static void main(String[] args) {
    		Member member = new Member("newid123","security");
    }
    
    

    위와 같은 상황이 발생하더라도 임시로라도 userid를 닉네임으로 사용하게 끔 하여 불완전한 객체의 가능성을 차단한다.

     

     

    2. @AllArgsConstructor

    모든 생성자를 파라미터를 받아야 하는 생성자를 만들게 된다. 위의 예제에서 만약 테이블을 식별하는 id와 테이블 createAt, updateAt을 저장하는 필드가 있고 table id생성을 SQL에 위임하는 전략을 사용했을 경우에는 모든 인자를 전달할 수 없을 뿐만 아니라 createAt과 updataAt은 @CreationTimestamp, @UpdateTimestamp 어노테이션에서 처리할 수 있기 때문에 이 방법들을 사용할 수 있는 방법들을 고려해보았을때 사용에 제한이 될 수 있다.

     

     

    3. @Bulider

    빌더 클래스를 따로 만들지 않고 클래스위에 붙여주는 것만으로도 빌더 패턴 코드가 빌드된다

    @Bulider
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    }
    
    public static void main(String[] args) {
    		Member member = Member.builder() // 빌더어노테이션으로 생성된 빌더클래스 생성자
    								    .userId("userId")
    								    .nickname("newNickname")
    								    .password("1234")
    								    .build();
    }
    

    @Bulider은 Class(Type)이 Target인 경우 생성자가 있을때는 따로 생성자를 생성하지 않고, 생성자가 없는 경우 모든 멤버변수를 파라미터로 받는 기본 생성자를 생성한 후 모든 멤버 변수를 설정할 수 있는 Builder Class를 생성하게 된다. 이로 인해 코드의 가독성을 포함한 Bulider패턴이 가지고 있는 이점을 누릴수 있게 되지만 멤버 변수를 설정할 수 있는 Builder Class를 만들기 때문입니다.

    이제 이 어노테이션들을 같이 사용하여 좋은 코드들을 만들 수 있을지도 모른다.

     

     

    생성자 전략

     

     

    1. @AllArgsConstructor + @Bulider

    @AllArgsConstructor 
    @Bulider 
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    }
    
    

    @AllArgsConstructor 를 이용하여 모든 맴버변수를 받는 생성자를 만든 후 @Bulider 를 사용하여 빌더 패턴으로 생성자를 만들 수 있는 방법이다. 모든 파라미터를 받아야만 한다.

     

     

    2. @NoArgsConstructor + @Bulider

    이 글의 목적인 @NoArgsConstructor 과 @Bulider 를 조합한 방법이다.

    @NoArgsConstructor(access = AccessLevel.PROTECTED) 
    @Bulider 
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    }
    

    1번 전략의 코드는 정상적으로 컴파일되겠지만 위의 2번 전략의 코드는 error를 뱉어내는 코드이다.

    이유는 @NoArgsConstructor가 접근 권한에 관계없이 기본생성자를 만들게 되는데 @Bulider는 생성자가 존재하기 때문에 생성자를 따로 만들지 않고 모든 멤버변수를 받는 생성자를 찾기 때문에 에러가 발생한 것이다. 그렇다면 어떻게 사용을 하라는 소리인가

    https://projectlombok.org/features/Builder

    롬북의 공식 문서에서 따온 내용이다. 구글 번역기를 번역기 돌려보면 Class위에 @Builder을 붙이는 것은 @AllArgsConstructor(access = AccessLevel.PACKAGE)에 @Builder를 적용한 것과 같고 이것은 생성자를 명시하지 않았을 경우에만 사용이 가능하다고 합니다. 명시적으로 생성자를 작성했을 경우 @Builder를 Class가 아닌 생성자 위에 명시하라고 한다.

    @NoArgsConstructor(access = AccessLevel.PROTECTED) 
    public class Member{
        private String userId;
        private String nickname;
        private String password;
    
    		@Bulider 
    		public Member(String userId, String password){
    						this.userId = userId;
    						this.password = password;
    						this.userId = userId + "님"
    		}
    }
    

    그렇다면 위와 같은 코드를 작성할 수 있을 것 같다. @NoArgsConstructor(access = AccessLevel.PROTECTED) 를 이용하여 기본생성자를 만드는 것을 막고 롬북의 @Builder 어노테이션을 이용하여 원하는 형식으로만 생성자를 만들 수 있게 되었습니다.

     

     

    실제 코드 적용

    package spring.postproject.Member.Entity;
    
    import lombok.*;
    import spring.postproject.Common.EntityDate;
    import spring.postproject.Comment.Entitiy.Comment;
    import spring.postproject.Post.Entity.Post;
    import spring.postproject.Excetion.ExceptionBoard;
    
    import javax.persistence.*;
    import java.util.ArrayList;
    import java.util.List;
    
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @Entity
    public class Member extends EntityDate {
    
        private static final int MAX_LENGTH_NICKNAME = 30;
        private static final int MAX_LENGTH_PASSWORD = 20;
    
        @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
        @Column(name = "member_id")
        private Long id;
    
        @Column(nullable = false, unique = true)
        private String userId;
    
        @Column(nullable = false, length = 20,unique = true)
        private String nickname;
    
        @Column(nullable = false, length = 20)
        private String password;
    
        @Enumerated(EnumType.STRING)
        private MemberRoll memberRoll;
    
        @OneToMany(mappedBy = "member",cascade = CascadeType.ALL)
        private List<Post> postList = new ArrayList<>();
    
        public void updateNickName(String nickname){
            validationNickname(nickname);
            this.nickname = nickname;
        }
    
        public void updatePassword(String password){
            validationPassword(password);
            this.password = password;
        }
    
        public void updateRole(MemberRoll memberRoll){
            this.memberRoll = memberRoll;
        }
    
        public void validationNickname(String nickName){
            if(nickName.isBlank()|| nickName.length() > MAX_LENGTH_NICKNAME){
                throw ExceptionBoard.INVALID_LENGTH.getException();
            }
        }
    
        public void validationPassword(String password){
            if(password.isBlank()|| password.length() > MAX_LENGTH_PASSWORD){
                throw ExceptionBoard.INVALID_LENGTH.getException();
            }
        }
    
        @Builder
        public Member(String userId, String nickName, String password){
            validationNickname(nickName);
            validationPassword(password);
            this.userId = userId;
            this.nickname = nickName;
            this.password = password;
        }
    
    }
    

    위에서 설명한 것처럼 기본생성자는 막고 본인이 의도한 생성자만 열어두었는데 Entity내의 Validation 검증을 하는 함수들을 거치도록 설계하였다. 왜냐하면 Controller에서 받은 파라미터의 값 검증을 Controller와 Entity 두군데에서 하는 것보다 엔티티에서 하는게 깔끔해 보이는 것 같아 보여서이다. DB에 저장되기전 멤버변수를 검증하는 방법은 @Size, @Valid 의 방법으로 검증할 수 있다는 것은 이해하고 있다. 하지만 해당 어노테이션들을 이용하여 검증하지 않는 이유는 Entity에서 예외를 발생신 후 @ControllerAdvice를 통하여 예외처리를 통합하여 관리는 방법을 선택했기 때문이다.

    @ControllerAdvice에서 예외를 잡으면 Controller 단에서 잡겠다는 건데 굳이 문제가 되는 값을 Controller -> Service ->Entity단까지 밀어 넣는 이유가 뭘까 라고 생각 할 수도 있는데 아직 초보자의 눈에는 예외처리를 통합하여 한번에 처리하는게 좋지 않을까(?) 라는 생각이 들어서이다.

    아직 응애 웹개발자 수준도 되지 않는터라 시행착오를 다 맞아가며 개발을 하고 있다...

    시행착오가 기술을 체화시키는데 직빵이지만 혹여 지나가던 시니어 개발자분들은 관심으로 이 나약한 응애 개발자를 옳은 길으로 인도하소서...

     

    ref

    https://cobbybb.tistory.com/14#--%--%--NoargsConstructor-AccessLevel-PROTECTED-�%--%--%--%--Builder�%A-�%--�%--%A-��%--%--�%--��%-A%A-�%--%--�%A-%B-%--�%A-%--%--�%-D%--%--�%B-%--�%-B%A-�%--%--�%A-%--%--�%--%-A�%-D%--�%B-%-C

    https://www.popit.kr/실무에서-lombok-사용법/

    https://wooktae.tistory.com/34

    https://erjuer.tistory.com/106

    https://velog.io/@gowjr207/Entity-에-쓰이는-어노테이션

    댓글