나는 데이터를 저장 및 업데이트할 때, 각각의 메서드로 분리하여, JPA save() 메서드를 사용하였다. 그러다 어느날 다음과 같은 코드로직을 보게되었다.
1. findById 를 통해 AEntity를 조회, a변수 할당
2. 새로운 b변수에 a변수의 정보를 참조하여, new AEntity를 생성
3. b변수save()
나는 이 코드로직을 보고선, "AEntity 를 새로 생성하는구나" 라는 생각을 했다. 하지만 Hibernate 로그를 보았을 때는 "Insert가 아닌 Update 쿼리가 발생"하였다.
나는 의문이었다. "새로운 b변수에 new AEntity 를 생성했는데 어떻게 PK 중복 에러 없이, 업데이트가 발생했는가?" 궁금했다.
문제의 원인
이 답은, 김영한님의 자바 ORM 표준 JPA 프로그래밍의 내용을 통해 알 수 있었다.
save(S) 메소드는 엔티티에 식별자 값이 없으면(null이면) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다.
이와 관련하여 SimpleJpaRepository(CrudRepository의 구현체)를 살펴보면 save() 는 아래와 같이 구현되어있다.
- isNew(): idType이 Primitive 타입이 아닌지 확인을 한다. (JpaRepository 상속 시 입력한 ID 타입을 보고 판단) 그리고 그게 null인지 아닌지 판단한다.
- persist(): 해당 객체를 영속성으로 만든다.
- merge(): 해당 객체를 조회한 후 있으면, UPDATE, 없다면 INSERT 를 통해 새로운 영속성 객체를 반환한다.
즉, findById() 를 통한 AEntity 조회된 AEntity를 기반으로 새로운 AEntity 를 생성(식별자 존재)하였고 save() 할 경우, 식별자가 존재하여 Insert 가 아닌 Update 가 발생 했던 것이다.
하지만 또 다른 의문점이 생겼다. "저장되지 않은 Entity 를 save(Insert) 할 경우에는 어떻게 되는거지? 나는 식별자를 생성하고서 DB에 save() 를 진행하는데?"
또 다른 의문점, INSERT 전 SELECT가 발생하는게 맞는가?
나는 productId 를 식별자로 사용하고 있으면서, 객체를 생성할 때, 도메인 로직을 거쳐, 해당 Product 를 생성한다.
- 도메인 로직 간, initializeProduct 를 통해, 식별자(@Id 에 해당하는 값)를 생성 및 할당한다.
- 현재 Product 및 다른 모든 도메인 엔터티에 대하여, INSERT 전 SELECT 가 발생하는 구조의 설계를 갖고 있다.
실제로 테스트 해본결과, 로그는 예상대로 SELECT 가 발생하는 것을 확인할 수 있었다. 그리고 나는 생각하게되었다. "INSERT/UPDATE 전에 SELECT를 하는 것이 맞을까?"
이에대한 내 생각은 "UPDATE 를 위한 SELECT 는 맞지만, INSERT에서는 아니다." 라고 생각한다. 그렇다면 이를 어떻게 해결할 수 있을까?
해결책: Persistable 인터페이스 구현
기존의 isNew의 경우, Id 값의 존재 여부로 결정되므로, 이를 다른 조건으로 개선하면된다.
- 방법 1. 생성시간에 대한 필드를 추가하고, 해당 필드 값의 null 여부에 따라 isNew 를 판별한다.
- 방법 2. isNew 필드를 추가하고, 해당 값을 변환하는 작업을 하는 메서드를 추가한다.
// 방법 1. 생성시간에 대한 필드를 추가하고, 해당 필드 값의 null 여부에 따라 isNew 를 판별한다.
@CreatedDate
private LocalDateTime createdDate;
@Override
public boolean isNew() {
return createdDate == null;
}
// 방법 2. isNew 필드를 추가하고, 해당 값을 변환하는 작업을 하는 메서드를 추가한다.
@Transient
private Boolean isNew = true;
@PrePersist // 영속성 객체로 만들어질 때 false로 변경
@PostLoad // DB에 저장하거나 불러온 시점 이후부터는 새로운 entity로 인식하지 않게 처리
private void markNotNew() {
isNew = false
}
아직 어느것이 더 나은 방법인지는 판단을 내리지 못했지만, 분명한것은 Persistable 인터페이스를 구현하여 기존의 INSERT API 실행 시 SELECT가 발생하지 않도록 개선해야겠는 생각이다. 우연한 기회로 이 부분을 알게되고 또 공부할 수 있어서 다행이다!
[JPA] save메서드로 살펴보는 persist와 merge 개념
JpaRepository의 구현체 SimpleJpaRepository에서 save()메서드가 내부적으로 어떻게 동작하는지 알아보자.
umanking.github.io
Spring Data JPA에서 Insert 전에 Select 하는 이유
이전 글에서 bulk insert 처리를 할 수 있는 방법 중에 하나가 @Id 값 알고 있는 경우라고 했었는데요. 실제로 잘 되는지 확인하는 과정에 발생한 문제 내용을 공유합니다.
kapentaz.github.io
JPA에서 insert, update, delete 할 때 자동으로 select 하지 않게 하는 방법
JPA에서 CRUD 중 Create(insert), Update, Delete query를 할 때, 원하지 않는 select query가 발생합니다. 다음 예제를 먼저 보겠습니다. select query가 발생하는 예제 먼저 User entity class를 생성합니다. import lombok.Ge
yjh5369.tistory.com
'개인공부' 카테고리의 다른 글
[ 코드 설계 ] 의존성 역전 원칙 과 의존성 주입 프레임 워크 (0) | 2023.10.06 |
---|---|
[ 코드 설계 ] 개방 폐쇄 원칙 OCP (0) | 2023.09.28 |
[ 코드 설계 ] 단일 책임 원칙 SRP (0) | 2023.09.26 |
[ Java ] Optional (0) | 2023.09.18 |
[ Spring ] @ControllerAdvice 를 통한 @Controller 전역 Exception 처리, Controller 를 보다 명확하게 표현하기 (0) | 2023.08.11 |