JPA Repository에는 save메서드가 있습니다.
이는 어쩔때는 INSERT문을 사용하고, 어쩔때는 UPDATE문을 사용합니다.
이를 결정해주는건 isNew메서드를 사용하여서 해당 Entity가 새로 생긴건지 기존의 것인지를 구분합니다.
java
1 // SimpleJpaRepository.java 2 @Transactional 3 public <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를 수행합니다.
java
1 // AbstractEntityInformation.java 2 public 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
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 2 public 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 2 public 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 3 public class Board { 4 @Id 5 @GeneratedValue(strategy = GenerationType.AUTO) 6 private id; 7 8 private String content; 9 }
java
1 @Transactional 2 public 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가 수행된것을 확인할 수 있습니다.
한개나 두개의 row를 INSERT하는 경우라면 SELECT문이 발생해도 큰 지장이 없습니다.
하지만 만약 1000개의 row를 INSERT하게 된다면 1000번의 SELECT문이 발생할 것이고, 이는 성능적으로 장애를 일으킬 수 있습니다.
JPA에서는 이를 방지하기 위해 isNew메서드를 오버라이드 해와 직접 구현할 수 있습니다.
java
1 public 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기능을 활용할 수 있습니다.
@CreatedDate는 entity가 INSERT될 때 자동적으로 입력이 됩니다.
이를 이용하여서 필드값이 null일 경우에 isNew가 false임을 return하면 됩니다.
java
1 @MappedSuperclass 2 @EntityListeners(AuditingEntityListener.class) 3 @Getter 4 public abstract class Auditalbe { 5 ...... 6 @CreatedDate 7 @Column(name = "create_time", columnDefinition = "date", nullable = false, updateable = false) 8 protected LocalDateTime createDateTime; 9 ...... 10 }
java
1 public 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를 알아보겠습니다.