엔티티 클래스 단위에서 @Setter나 @Allargs, @Builder 사용을 지양하는 이유 - 2-7-team/user-service GitHub Wiki

엔티티 클래스에서 Setter, AllargsConstructor 사용을 지양하는 이유

1. 무분별한 새로운 ID포함 객체 생성 예방

다음과 같은 엔티티가 존재합니다.

@Setter
@AllArgsConstructor

@Entity
@Getter
@Table(name = "p_user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name="username")
    private String userName;

    @Column
    private String password;

    @Column(name = "nickname")
    private String nickName;

    @Column
    private String email;

    @Column
    private UserRole role;

}


JPA 는 @GeneratedValue와 같은 방법으로 자동으로 엔티티 생성 시 ID를 자동 생성하도록 설정할 수 있습니다. 그러나 SETTER나 AllArgs와 같은 어노테이션을 사용하면, 개발자가 손쉽게 ID 필드에 값을 삽입할 수 있게 됩니다.

예시)

  • 만약 개발자가 DB로부터 조회해온 객체가 아니라, 직접 새로운 객체를 생성한 뒤(비영속),
User user = new User(); 
user.setId(1L); 
user.setName("홍길동");
user.setEmail(null) ;

와 같은 객체를 생성하였다고 한 뒤, 이를 db에 저장하려고 합니다.

이때 JpaRepository의 구현체인 simpleJpaRepository의 save 메소드에 따르면,

  @Transactional
  public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
      this.entityManager.persist(entity);
      return entity;
    } else {
      return (S)this.entityManager.merge(entity);
    }
  }

동작 방식은 다음과 같습니다.

    1. isNew(entity)메소드를 통해 엔티티 존재하는 지 판단.

이때 판단은 entity의 id가 null 혹은 0일 경우로 판단합니다.

    1. isNew(entity) 메소드를 통해 해당 ID의 엔티티가 존재하지 않을 경우에, persist() 메소드 실행하여 영속성 컨텍스트에 저장.

OR

    1. isNew(entity) 메소드를 통해 해당 ID 존재할 경우, merge() 메소드 실행.

merge()의 동작 방식은 다음과 같습니다.

    1. merge(entity)에서 entity.getId()를 통해 해당 비영속 객체의 id를 가져와 실제 db에서 기존 영속 객체 찾음.
    1. db에 존재할 경우, 해당 영속 객체에 entity의 값 모두 복사하여 저장. or 새 영속 객체 생성 후 db에 저장.

이때 전달된 entity는 persist()와 다르게 영속 컨텍스트에 저장이 되지 않고, 비영속으로 남아있게 됩니다.

이처럼 만약 id가 존재할 경우에 merge()를 호출하여 문제가 발생할 수 있습니다.

save 시 merge() 호출의 문제점

1.업데이트 시 만약 요청을 통해 비영속 객체를 생성한 뒤, save()로 업데이트를 수행한다면 merge()의 동작방식은 변경해야하는 필드만 업데이트 되는 방식이 아닌, 전체 데이터를 덮어 씌우게 되는데 **이때 비영속 객체의 값을 저장하는 과정에서 null과 같은 잘못된 데이터가 저장될 가능성이 존재합니다. **

ex)

User user = new User(); 
user.setId(1L); 
user.setName("홍길동");
//user.setEmail(null) ;  이름만 업데이트하려는 상황

userRepository.save(user); // merge() 호출 시 기존 email값도 null로 덮어쓰게됨.

따라서 업데이트의 경우에는 merge() 사용보다는, dirty Checking을 사용해 업데이트 하고 싶은 필드만 업데이트하는 것이 권장됩니다.

클래스 단위로 해당 setter나 allargsconstructor를 허용하면, 의도치 않게 비영속 객체를 손쉽게 생성할 수 있게 되고

id를 손쉽게 생성할 수 있게 되면서 save 시 merge()가 실행될 수 있는 가능성이 존재하므로 save 호출 시에는 최대한 merge() 사용을 지양하는 것이 좋습니다.

( db로 부터 조회해온 객체는 영속 상태이므로 setter를 통해 변경 시 dirty checking 가능합니다)

  1. merge()는 넘어온 비영속 객체를 영속성 컨텍스트에 등록하지 않습니다.

따라서 다음과 같이

User user = new User(); 
user.setId(1L); 
user.setName("홍길동");
user.setEmail(null) ;

User merged = em.merge(user);     // 새로운 영속성 객체 생성. user는 영속성 객체가 아니다.

user.setName("철수")   // Dirty Checking 안됨.

과 같은 문제가 발생할 수 있습니다.

Builder

클래스 단위에서의 Builder의 사용도 setter와 AllargsConstructor의 문제점을 동시에 가지고 있으므로, 메소드 단위에서 선언하는 것이 좋습니다.

결론

따라서 id의 생성 책임, 전략을 개발자가 아닌 DB나 JPA에 위임하고, 개발자가 직접 id를 생성할 수 없게 막는 것이 권장됩니다.

또한 업데이트가 필요 시, save()의 merge() 사용보다는 dirty checking과 같은 방법을 사용하고, save 메소드는 id가 null인 새로운 객체만 전달하도록 하는 것이 좋습니다.