JPA의 save 메서드 동작과정과 SELECT 없이 INSERT하기

2023-06-09
ꞏ
유영진
ꞏ
show5116

JPA Repository에는 save메서드가 있습니다.
이는 어쩔때는 INSERT문을 사용하고, 어쩔때는 UPDATE문을 사용합니다.
이를 결정해주는건 isNew메서드를 사용하여서 해당 Entity가 새로 생긴건지 기존의 것인지를 구분합니다.

SimpleJpaRepository 구현체

java
1// SimpleJpaRepository.java
2@Transactional
3public <S extends T> S save(S entity) {
4 if (entityInformation.isNew(entity)) {
5 em.persist(entity);
6 return entity;
7 } else {
8 return em.merge(entity);
9 }
10}

위는 JPA Repository의 구현체인 SimpleJpaRepository에서 save메서드의 구현입니다.
isNew메서드로 Entity가 새롭게 만들어진 것인지, 기존의 것인지를 구분하고 새로운것이면 persist를 아니면 merge를 수행합니다.

isNew

java
1// AbstractEntityInformation.java
2public boolean isNew(T entity) {
3 ID id = getId(entity);
4 Class<ID> idType = getIdType();
5
6 if (!idType.isPrimitive()) {
7 return id == null;
8 }
9
10 if (id instanceof Number) {
11 return ((Number) id).longValue() == 0L;
12 }
13
14 throw new IlleaglArgumentException(String.format("Unsupported primitive id type %s!", idType));
15}

위는 AbstractEntityInformation에서의 isNew메서드 구현체 입니다.
ID의 타입이 기본 타입일 경우 0일 때, 참조 타입일 경우 null인 경우에 true를 반환합니다.
이는 @GeneratedValue에 맞춰서 구현되어 있기 때문인데, 해당 어노테이션을 사용할 경우에는 INSERT시에 id값을 주지 않기 때문입니다.
사실상 @GeneratedValue를 사용하지 않는 Entity면 false를 리턴하게 됩니다.

persist vs merge

  • persist

    persist는 최초로 새엇ㅇ된 Entity를 영속화 합니다.
    flush메서드가 호출될 때 INSERT문을 사용하게 됩니다.

  • merge

    merge는 detached상태의 Entity를 다시 영속화 합니다.
    detached란 한번이라도 영속화가 되었지만 현재는 영속화가 되어있지 않은 상태 입니다.
    다시 영속화 하는 과정에서 id값을 통해 SELECT문으로 값을 확인하게 됩니다.
    flush메서드가 호출될 때 SELECT의 결과가 있다면 UPDATE를 없다면 INSERT를 사용합니다.

여기서 주의해야할 점은 merge입니다.
save의 구현체를 보면 persist의 경우에는 기존의 Entity를 return하지만, merge의 경우에는 새로운 Entity를 return 합니다.

java
1@Transactional
2public void test() {
3 User user = User.builder()
4 .id("id")
5 .name("이름")
6 .build();
7
8 User insertUser = userRepository.save(user); // INSERT
9 System.out.println(user == insertUser); // false
10
11 insertUser.updateName("새로운 이름");
12 User updateUser = userRepository.save(insertUser); // UPDATE
13 System.out.println(insertUser == updateUser); // false
14}

위의 테스트 코드를 동작하면 insertUser와 user도 서로 다르고, updateUser도 서로다른 Entity인 것을 확인할 수 있습니다.
이 때 이전에 있던 Entity들은 더이상 영속성 관리 대상이 아니게 됩니다.
그에 따른 dirtyCheck도 수행하지 않게 됩니다.

java
1@Transactional
2public void test() {
3 User user = User.builder()
4 .id("id")
5 .name("이름")
6 .build();
7
8 User insertUser = userRepository.save(user); // INSERT
9
10 insertUser.updateName("새로운 이름");
11 User updateUser = userRepository.save(insertUser); // UPDATE
12
13 insertUser.updateName("또다른이름"); // dirtyCheck로 인한 UPDATE가 발생하지 않음.
14 // updateUser.updateName("또다른이름"); 으로 하면 UPDATE가 제대로 발생함.
15}

또다른 테스트 코드를 실행해보면 insertUser의 name을 변경하여도 dirtyCheck로 인한 UPDATE문이 발생하지 않는 것을 확인할 수 있습니다.
따라서 save이후에 어떠한 로직을 수행할 경우 반드시 return 값을 새로이 받아서 사용하는 것이 좋습니다.
만약에 @GeneratedValue를 사용한다면 결과가 달라질 수 있습니다.

java
1@Entity
2@Builder
3public class Board {
4 @Id
5 @GeneratedValue(strategy = GenerationType.AUTO)
6 private id;
7
8 private String content;
9}
java
1@Transactional
2public void test() {
3 Board board = Board.builder()
4 .content("내용").build();
5
6 Board insertBoard = boardRepository.save(board);
7 System.out.println(board == insertBoard); // true
8 board.updateContent("새로운 내용"); // dirtyCheck로인한 UPDATE가 발생함
9}

위의 테스트 코드에선 persist로 인해 같은 엔티티를 return하였고, 그 결과로 dirtyCheck가 수행된것을 확인할 수 있습니다.

SELECT 없이 INSERT 하기(Persistable 구현)

한개나 두개의 row를 INSERT하는 경우라면 SELECT문이 발생해도 큰 지장이 없습니다.
하지만 만약 1000개의 row를 INSERT하게 된다면 1000번의 SELECT문이 발생할 것이고, 이는 성능적으로 장애를 일으킬 수 있습니다.
JPA에서는 이를 방지하기 위해 isNew메서드를 오버라이드 해와 직접 구현할 수 있습니다.

java
1public class User implements Persistable<String> {
2 @Id
3 private String id;
4
5 @Override
6 public String getId() {
7 return id;
8 }
9
10 @Override
11 boolean isNew() {
12 return false;
13 }
14}

Auditing 활용

지난번의 Auditing 포스트의 Auditing기능을 활용할 수 있습니다.
@CreatedDate는 entity가 INSERT될 때 자동적으로 입력이 됩니다.
이를 이용하여서 필드값이 null일 경우에 isNew가 false임을 return하면 됩니다.

java
1@MappedSuperclass
2@EntityListeners(AuditingEntityListener.class)
3@Getter
4public abstract class Auditalbe {
5 ......
6 @CreatedDate
7 @Column(name = "create_time", columnDefinition = "date", nullable = false, updateable = false)
8 protected LocalDateTime createDateTime;
9 ......
10}
java
1public class User extends Auditable implements Persistable<String> {
2 @Id
3 private String id;
4
5 @Override
6 public String getId() {
7 return id;
8 }
9
10 @Override
11 boolean isNew() {
12 return createDateTime = null;
13 }
14}

이렇게 구현할 경우 createDateTime이 null일 경우 SELECT문 없이 INSERT를 사용합니다.

마치며

JPA Repository의 save메서드를 분석하여 보았습니다.
save 메서드를 통하여서 UPDATE를 할 경우, null인 데이터들은 그대로 null로 update가 될 수 있습니다.
다음번에는 이를 방지하기 위한 방법과 dirtyCheck로 인한 UPDATE를 알아보겠습니다.