일단 문제를 함께 풀어보자ㅎㅎ
User엔티티부터!(사실 서적에서는 엔터티라고 부르던데..난 엔티티가 편하다)
Quiz
Given
JPA를 공부하셨고 구현체인 Hibernate의 ActionQueue에 대해서 들어보셨다면.. 다음 코드의 성공/실패 결과를 예측해보자
문제1
테스트코드라 사실 뭐..given , when, then
// given
...
// when & then
val exception = assertThrows<DataAccessException> {
...
}
테스트코드라 사실 뭐..given , when, then 그리고 예외를 잡는부분까지 있어야하지만 간단하게 저 테스트가 초록불이 뜰지 주황/빨간불이 뜰지 정도만 예측해보자
답은?
답은.. ActionQueue로 혼동을 주긴 했지만, 이건 delete로 인해 준영속화된 엔티티(id가 null이거나 0L이 아닌 이미 한번 영속화된 적이 있던 엔티티)를 다시 영속화를 시도하면서, merge()를 호출해서 발생하는 문제였다
해결하기 위해서
1번 방법(새로 인스턴스를 만들어서 저장) 또는 EntityManager를 통해 merge가 아닌 persist를 호출해주면 해결이 가능하다
이 글에서 주로 다룰 내용은 아니기때문에 초록불만 뜬거 보여주고 pass..
그럼 두번째 문제
문제2
한가지 수정사항이 있다
User 엔티티에 username이 unique key를 걸어줬다
Unique Key를 거는 방법은 @Table에서 주는 방법과 @Column에서 주는 방법이 있는데, 나는 그 중 @Table쪽 프로퍼티로 걸어줬다
성공하면 혁..아니지.. 성공할까 실패할까?
로직의 흐름대로라면, Boki라는 이름의 유저로 가입하고, 삭제하고 다시 가입했다
문제없지 않을까?
결과는
결과는 실패한다. 왜? 로그를 보자
어라라??
난 분명히 Insert -> Delete -> Insert 순으로 쿼리를 작성했는데
Insert 쿼리가 먼저 2번 발생하면서 Duplicate entry ... 내가 정해놓은 유니크 키 제약조건 이름이 발생했다
그럼 해당 문제는 Unique Key 문제인걸까?
뭐... 반은 맞다. "Boki"라는 이름이 중복으로 발생된다고 하니깐 말이다
일단 불난곳에 불부터 꺼야한다. 해결법을 알아보자
Solved
1. 트랜잭션 분리
테스트코드로 작업한거기때문에 이렇게 했는데, 뭐 실 서비스라면 self-invocation 회피해서 다른 클래스로 책임분리해서 트랜잭션을 분리할 것 같다
2. flush() 사용
flush()를 호출하면 JPA의 쓰기 지연 전략에서 중간 쓰기를 하게 된다(트랜잭션 로그에만) ActionQueue를 비워주면서 트랜잭션 로그에 미리 쓰기(쓰기지연 전략 -> 중간쓰기 전략)
최종적으로 Tx이 끝날때 flush()가 한 번 더 호출되면 Commit이면 트랜잭션 로그의 내용이 DB에 반영, Rollback이면 Nothing이 된다
flush()를 호출하면 insert -> delete로 대기큐를 비우면서 내부에 쌓여있던 쿼리가 트랜잭션 로그에 반영된다
고로, 나중에 들어온 insert가 추후에 실행되게 된다
Consideration - Why?
왜? 왜 insert 쿼리가 delete 쿼리보다 먼저 실행될까?
답을 바로 찾기보다 생각을 해보자
JPA를 사용할때를 떠올려보자. 엔티티 객체를 인스턴스화 한 맨 처음 id는 보통은 null을 주게 된다(때에 따라 0L)
이후, database에 갔다가 온 순간에는 ID가 생겨있다
Hibernate혹은 조상?선배님들은 이 영속화된 엔티티로 여러곳에 쓰기를 할 것이라 생각했을 것이다
Insert into 쿼리가 발생하고, 받아온 자동 채번된 ID를 갖고 있는 엔티티로 다른 테이블에 쓰기도 하고, 애플리케이션을 개발할 거라고 생각했다
그리고 delete는 사실 한 번 날리고 나면 신경을 안써도 되는 쿼리의 일종이다(요즘에는 soft delete도 나왔더라는 ~_~)
그래서 일단 Insert -> delete 쿼리 순서로 나가게 만든 것 같다고 추측해본다
그럼 여기에서 더 나아가서 select와 update는 언제 실행될까?
당~연하게도 select로 가져온 데이터를 update를 해야하기 때문에 select -> update일 것 같다
그리고 생각해보면... select -> insert -> update는 뭔가 어색하다
사실 insert into는 어떠한 문제도 일으키지 않는 착한 친구이다
처음 채번(아이디 자동 생성)한 이후로 사용될 뿐.. select/update처럼 동시성에 대한 걱정을 할 필요가 없다
그래서 아마 insert -> update -> delete 순서일 것 같다
select가 필요한 곳은 당연히 바로 사용이 되야 하고..
Answer
hibernate.event.internal.AbstractFlushingEventListener의 performExecutions 메소드를 살펴보면
내가, 아니 우리가 한 추측대로 javadoc에 쓰여져 있는 모습을 확인할 수 있었다
Hibernate가 이상한게 아니다. 그럴만한 이유가 있어서 실행될 쿼리 순서를 바꿔놓았다(최적화)
그리고 우리는 JPA를 공부했다면 쓰기지연+쿼리실행순서 by ActionQueue 때문에 중복키가 발생했다는 걸 인지했다
참고로 중복키가 없는 테이블은 없다. ID하나만 갖고 모든 것을 조회하는 건.. 그냥 스몰토이 프로젝트가 아닌가싶다
화면에 검색/필터링이 들어가고, 최종적으로 List가 아니라 단건, 페이지네이션 조회를 하기 위해서는 카디널리티가 높거나 유니크한 값이 필요하기 마련이다
나는 위와 같은 이슈를 개발하면서 겪어본 적이 없다. 혼자 왜그럴까 생각을 해보니
1. 대형 규모의 프로젝트를 한 적이 없던가?<-절망편
2. 객체별/트랜잭션별 바운더리와 책임 분리를 잘 해놓고 사용했었던가?<-희망편
아마 2번에 가까운 것 같다
저렇게 지우고, 다시 같은 값으로 쓸 일이 있는 요구사항도 없었고 그럴 일이 있다면 Update(Patch/Put HTTP Method)를 사용했을 것 같다
그럼 계속 이어서 ActionQueue에 대해서 좀 더 깊이 알아보자
'Backend > JPA' 카테고리의 다른 글
JPA 특징 + 1차 캐시(feat. EntityManager.clear()) (1) | 2024.11.23 |
---|---|
JPA Query 로그 출력(feat. 물고기를 주지말고, 물고기 잡는 법좀 알려줘라..) (31) | 2024.11.15 |
댓글