Back-end

JPA Entity @Builder 등 어노테이션 주의점

서채리 2023. 3. 6. 02:56

@Entity란?

JPA를 사용해 테이블과 매핑할 클래스에 붙이는 어노테이션이다.

@Entity가 붙은 클래스는 JPA가 관리하는 객체가 된다.

 

🧐 무분별한 어노테이션 사용

@Setter // 문제 1. 객체가 무분별하게 변경될 가능성 있음
@Getter
@NoArgsConstructor // 문제 2. 기본 생성자의 접근 제어자가 불명확함
@AllArgsConstructor // 문제 3. 객체 내부의 인스턴스 멤버들을 모두 가지고 있는 생성자를 생성
@Builder // 문제 4. 모든 매개변수가 생성자 param으로 들어가 객체 생성 시 받지 않아야 할 매개변수도 빌더에 노출
@Data // 문제 5. 사용하지 않는 어노테이션을 따로 exclude 할 수 없기 때문에 개별 어노테이션 사용
@Entity
public class User

문제 1. @Setter

@Setter를 사용하면 객체를 언제든 변경할 수 있는 상태이기 때문에 객체의 안정성이 보장받기 어렵다.

특히 Entity에서 @Setter 사용 시 변경 사항이 어디서 발생했는지 추적하기 힘들다.

 

값의 수정이 필요한 경우 명시적인 메서드 생성을 통해 하는 것이 좋다.

@Getter
@Entity
public class User {
    private String email;
    private String password;
    private String nickname;

    public void updateNickname(String nickname) {
        this.nickname = nickname;
    }
}

해결법:❗️@Setter 삭제 후 명시적인 메서드 생성❗️

 

 

문제 2. @NoArgsConstructor

기본 생성자(NoArgsConstructor)의 접근 제어를 PROTECTED로 설정해 놓으면 무분별한 객체 생성에 대해 한번 더 체크할 수 있다.

 

예를 들어 User 클래스에서 email, password, nickname 정보를 모두 갖고 있어야 한다면

기본 생성자 생성을 하지 않아 완전하지 않은 객체 생성을 막는다.

 

기본 생성자의 권한을 따로 설정하지 않으면 access level이 public이 되는데 그 경우 아래 상황이 발생한다.

// User.java
@Setter
@Getter
@NoArgsConstructor
public class User {
    private String email;
    private String password;
    private String nickname;
}


// Main.java
public static void main(String[] args) {
    User user = new User();
    user.setEmail("test@test.com");
    user.setPassword("testpassword1!!");
    
    // nickname 이 설정되지 않았기 때문에 user는 완전하지 않은 객체
}

User 객체를 만들어 Setter로 값을 설정할 경우 실수로 필수값을 설정하지 않아도 객체가 생성이 된다.

 

하지만 아래와 같이 변경 시 IDE 단계에서 필수값 누락을 막을 수 있다.

// User.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    private String email;
    private String password;
    private String nickname;
    
    public User(String email, String password) {
        this.email = email;
        this.password = password;
        // 파라미터가 두 개인 경우 default 값 설정
        this.nickname = "닉네임을 설정하세요";
    }
}


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

해결법:❗️@NoArgsConstructor(access = AccessLevel.PROTECTED)로 변경❗️

 

문제 3. @AllConstructor

@AllConstructor는 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성하는데, 인스턴스 멤버 변수의 순서를 바꾸면 입력 값 순서도 바뀌어 검출되지 않는 치명적인 오류를 발생시킬 수 있기 때문에 쓰지 않는 것이 좋다.

해결법:❗️@AllConstructor 삭제❗️

 

문제 4. @Builder

클래스 레벨에서 @Builder를 선언할 때 @NoArgsConstructor와 @AllArgsConstructor을 함께 사용하지 않으면 컴파일 에러가 발생한다.

 

JPA Entity를 사용할 경우 JPA는 엔티티 객체를 인스턴스화할 때 매개변수가 없는 기본 생성자를 요구한다. 만약 클래스에 @Builder만 사용하고 기본 생성자(@NoArgsConstructor)가 없다면, JPA는 객체를 생성할 수 없어 오류가 발생하게 된다.

또, @Builder는 객체를 생성할 때 내부적으로 모든 필드를 초기화할 수 있는 생성자가 필요하다. @AllArgsConstructor는 이러한 생성자를 제공하여 @Builder가 제대로 동작할 수 있도록 지원해 준다.

 

하지만 위 문제 3에서 언급한 것처럼 AllArgsConstructor는 사용하지 않는 것이 좋다.

따라서 생성자에 @Builder를 선언하면 객체 생성 시 받지 않아야 할 매개변수들도 빌더에 노출이 되는 문제점을 해결할 수 있다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    private String email;
    private String password;
    private String nickname;

    @Builder
    public User(String email, String password) {
        this.email = email;
        this.password = password;
        this.nickname = "닉네임을 설정하세요";
    }

    @Builder
    public User(String email, String password, String nickname) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
    }
}

해결법:❗️클래스에서 @Builder 삭제 후 생성자에 @Builder 붙이기❗️

 

문제 5. @Data

@Data는 @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode을 한꺼번에 설정해 주는 어노테이션이다.

그러나 @Data와 함께 포함되어 있는 lombok의 설정들(ex. callSuper, includeFieldNames, exclude)을 지정할 수 없으며 사용하지 않는 어노테이션까지 한 번에 선언이 된다.

따라서 @Data 어노테이션을 사용하지 않고 필요한 개별 어노테이션을 추가하는 것이 좋다.

해결법:❗️필요한 개별 어노테이션 각각 추가❗️

 

🤩 올바른 사용

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    
    private String password;
    
    private String nickname;

    public void updateNickname(String nickname) {
        this.nickname = nickname;
    }

    @Builder
    public User(String email, String password) {
        this.email = email;
        this.password = password;
        this.nickname = "닉네임을 설정하세요";
    }

    @Builder
    public User(String email, String password, String nickname) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
    }
}

 

 

https://velog.io/@mooh2jj/%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%97%94%ED%8B%B0%ED%8B%B0-Builder-%EC%82%AC%EC%9A%A9%EB%B2%95

https://velog.io/@cieroyou/Builder-%EC%82%AC%EC%9A%A9%EB%B2%95

https://cobbybb.tistory.com/14

https://hilucky.tistory.com/238