운전 면허를 취득하고 알게된 사실은, 되려 경험이 많은 운전자일 수록 도로 법규나 운전 예절에 대해 망각하는 경우가 종종 있다는 것이었다.
최근 나와 JPA의 관계가 그렇게 되었다는 생각이 문득 들었다.
내 JPA에 대한 실무적인 이해도는 어느 정도일까?
JPA나 DB를 개발하는 코어 개발자가 아닌 이상 실무에서 사용하는 기술의 이해도와 응용 능력이 보다 더 중요하지 않을까?
대다수의 개발자는 프레임워크나 라이브러리보다는 현실에 존재하는 특정 도메인에 대해 개발하거나 유지보수를 한다.
그래서 이 글을 보는 여러분들도 필자와 같이 JPA의 동작에 대해 다시 한 번 되짚어 보면 좋을 것이라는 생각이 든다.
퀴즈
자, 아래와 같은 코드가 있다고 하자.
그리고 임의의 컨트롤러가 method1을 호출한다고 가정한다.
이때, JPA는 어느 곳에서 어떤 쿼리를 실행할까?
여러분들도 한 3분 정도 생각을 해보길 바란다!
부끄럽게도 나는 아래 두 가지 시나리오를 떠올렸는데, 참고로 두 가지 모두 틀렸다.
시나리오 1
단계 1
internalMethod1의 트랜잭션이 method1의 트랜잭션으로 합류한다.
JPA의 영속성 컨텍스트(이하 캐시)는 트랜잭션 단위로 관리된다.
이 경우 캐시는 method1의 범위로 관리된다.
단계 2
JPA는 영속성 컨텍스트에 쓰기 지연저장소에 쿼리를 적재한 후에 실행한다. 따라서 미리 쿼리가 발생하지 않는다.
따라서 personRepository에서 저장을 할 때 insert가 발생하지 않는다.
그리고 id값이 아직 존재하지 않기 때문에 method2의 인자에 넘어갈 때 예외가 발생한다.
시나리오 2
단계 1 는 동일하게 생각했다.
단계 2
사실 내 경험 데이터상, IDENTITY 전략의 entitiy를 save하면 id값이 바로 로딩되는 것을 보아왔다.
따라서 save절에서 insert대신에 id값을 가져오는 쿼리가 발생할 것으로 예상할 수 있다.
그리고 쓰기 지연 저장소에 최대한 쿼리를 적재하여 실행할 것이므로 insert와 update가 2번 발생하지 않고,
변경 내역이 반영된 insert 쿼리만 발생한다.
그렇다. 위 시나리오 1, 2는 모두 틀렸다.
정답은 아래처럼 출력이 된다.
insert문과 update문만 로그에 찍힌다.
save 이후에 entity의 id를 가져오지만 id를 가져오는 쿼리는 별도로 로그에 찍히지 않는다.
해석
단계 1에서 생각한 것은 옳았고 단계 2에서 잘못된 부분이 존재한다.
MySQL에서 IDENTITY 전략의 경우, insert를 함과 동시에 증가한 id값을 바로 가져온다.
즉, DB에 즉시 반영함과 동시에 JDBC의 기능인 PrepareStatement.getResultKeys를 통해 ID를 가져온다.
해당 메서드는 하이버네이트보다 저수준인 JDBC에서 동작하기 때문에 JPA 로그가 출력되지 않는다.
즉시 반영되는 것을 어떻게 알 수 있냐면, 아래와 같은 2가지 방법으로 알 수 있다.
- select for udate로 조회하여 lock이 걸리는 지 확인 (간접적인 방법)
- read uncommitted로 조회가 되는지 확인 (보다 직접적인 방법)
만일 insert 후에 rollback이 될 경우 증가한 id 값은 복원되지 않는다.
Update쿼리가 발생한 이유
이미 method1에서 insert가 발생해서 DB에 반영이 되었다.
JPA 입장에서 변경한 내역을 DB에 반영하려면 update 쿼리를 실행해야만 한다.
Insert쿼리가 즉시 반영되었는지 확인하는 법
번외로 정말 트랜잭션의 마지막에 반영되는 것이 아닌,
save에서 insert 쿼리가 발생하고 DB에 즉시 반영되었는지 확인하고 싶을 수 있다.
우선 아래처럼 쿼리의 실행 중간에 디버거를 찍도록 한다.
자, 그럼 이 순간 세 번째 데이터를 insert를 했고, id=3을 반환 값으로 가져온 상태이다.
그리고 트랜잭션이 종료되지 않았다. (Commit 혹은 Rollback이 되지 않았다.)
이 상태에서 반영이 되었는 지를 확인해보자.
방법1: 락 기반의 Select 쿼리를 통한 간접 확인
위 스크린샷에서는 1초이지만 실제로는 조회가 안 된다.반면에, id=1, 2는 정상적으로 조회가 된다
id=3인 record에 락이 걸렸고, 베타락 쿼리가 진입이 불가한 것이다!
즉 현재 id=3인 record에 Share Lock(이하 S-Lock) 혹은 Exclusive Lock(이하 X-Lock) 둘 중 하나가 걸린 것이다.
배경 지식이 잠시 필요해요!
S-Lock은 같은 S-Lock끼리는 접근을 허용하고 X-Lock은 접근이 불가능하다
X-Lock은 먼저 선점하면 이후에 접근하는 S-Lock, X-Lock은 모두 접근이 불가하다
따라서 S-Lock 조회인 select for share를 통해서 id=3인 record에 어떤 Lock이 걸렸는 지를 알아낼 수 있다.
만일 select for share를 통해서도 조회가 안 된다면 베타락이 걸린 것이다.
자, 조회가 안되는 것을 확인되었다. 요것은 베타락이다. 요거트!
사실 해당 레코드에 쓰기를 하는 것이므로 베타락이 걸리는 것은 당연해 보인다.
방법2: 격리레벨을 변경하여 확인
또 다른 방법으로 아래와 같이 격리레벨을 Read Uncommited로 변경하여 확인할 수 있다.
격리레벨 Read Uncommited는 아직 커밋되지 않은 데이터의 레코드 또한 읽을 수 있다.
아래 쿼리로 조회를 하면
이렇게 id=3인 record를 조회할 수 있다.
그렇다면 이것에 대해 난 왜 몰랐을까?
관심이 없었기 떄문에...? 그런 것 같기도 하다.
하지만 굳이 변명을 하자면, 최근 몇 개월 동안 트래픽으로 인해 N+1 문제를 수정하거나, 쿼리 튜닝을 할 일이 없었던 것 같다. DAU가 없는 서비스의 비애랄까...
하지만 그래도 그렇지! 다시 한 번 돌다리도 두들겨 보고 건너도록 하자.
위기의 순간 견고한 지식은 우리를 지켜줄 것이다 :)
응.. 근데 쓸 일이 없으면 잊어버려...
마치며
자, 이렇게 오늘도 나의 부끄러운 실수에 대해 얘기한다.
여러분들도 자주 사용하는 라이브러리(라고 하기엔 꽤 크지만)에 대한 빈도가 높은 개념들은 꼭 숙지하길 바란다.
그럼, 안녕! >.<
'Back-end > Spring Boot, JPA' 카테고리의 다른 글
스프링 배치 정리 (0) | 2024.04.27 |
---|---|
[땅굴 조사] JPA 트랜잭션이 수행되기까지 (0) | 2024.02.01 |
Spring Data Repository 확장 (짧음) (0) | 2023.12.13 |
Thread Safe하게 처리해보자 (feat. synchronized , 낙관, 비관, named) (0) | 2023.06.27 |
ArgumentResolver처리 과정 + Spring Web MVC 흐름 (0) | 2023.06.20 |
hi hello... World >< 가장 아름다운 하나의 해답이 존재한다
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!