
운전 면허를 취득하고 알게된 사실은 경험이 많은 운전자일 수록 되려 도로 법규와 운전 예절에 대해 망각하는 경우가 있다는 것이다.
최근 나와 JPA의 관계가 그렇게 되었다는 생각이 들었다.
우리의 JPA에 대한 실무적인 이해도는 어느 정도일까?
JPA나 DB 등을 개발하는 코어 개발자가 아닌 이상 실무에서 사용하는 기술의 이해와 응용 능력이 보다 더 중요하지 않을까?
대다수의 개발자는 현실에 존재하는 도메인을 다룬다.
그래서 이 글을 보는 여러분들도 필자와 같이 JPA의 동작에 대해 다시 한 번 되짚어 보면 좋을 것이라는 생각이 든다.
문제
아래와 같은 코드가 있다고 가정하자.
컨트롤 코드는 찍지 않았지만, 컨트롤을 거쳐 method1을 호출한다고 가정하자.
이때, JPA는 몇번째 라인에서 무슨 쿼리를 실행할까?
이 글을 보는 여러분들도 한 3분 정도 생각을 해보길 바란다!
나는 아래 두 가지 시나리오를 떠올렸는데, 부끄럽게도 두 가지 모두 답이 아니다.
시나리오 1
단계 1
method2의 트랜잭션이 method1의 트랜잭션으로 합류한다.
JPA의 영속성 컨텍스트(이하 캐시)는 트랜잭션 단위로 관리하기 때문에,
method2를 포함한 method1의 전체 스코프가 트랜잭션이자 캐시의 범위이다
단계 2
JPA는 캐시의 쓰기 지연저장소에 쿼리를 적재한 후에 실행한다. 따라서 미리 쿼리가 발생하지 않는다.
따라서 personRepository.save시 insert가 발생하지 않는다.
그리고 id값이 아직 존재하지 않기 때문에 method2의 인자에 넘어갈 때 예외가 발생한다. (코틀린 Not Null 단언)
결론: insert쿼리 발생(X), NotNull 예외 발생
시나리오 2
단계 1 는 동일하게 생각했다.
단계 2
사실 내 경험 데이터상, IDENTITY 전략의 entitiy를 save하면 id값이 바로 로딩되는 것을 보아왔다.
따라서 save절에서 insert대신에 id값을 가져오는 Select 쿼리가 발생할 것으로 예상할 수 있다.
그리고 쓰기 지연 저장소에 최대한 쿼리를 적재하여 실행할 것이므로 insert와 update가 모두 발생하지 않고
변경 내역이 반영된 insert 쿼리만 발생한다.
결론: Select 쿼리 + Insert 쿼리 발생 (Update쿼리 발생(X))
위 시나리오들은 틀렸으며 실제로는 아래처럼 출력이 된다.
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 쿼리를 통한 간접 확인
(현재 상황은 메서드를 2번의 호출 후 3번째 호출하는 중이다)
위 사진에서는 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를 통해서도 조회가 안 된다면 베타락이 걸린 것이다.
그리고 조회가 안되는 것을 확인되었다.
사실 해당 레코드에 쓰기를 하는 것이므로 베타락이 걸리는 것은 당연해 보인다.
베타락이 걸렸다는 뜻은, WAS에서 DB로 Insert쿼리를 쿼리를 보낸 이후 DB의 트랜잭션이 아직 끝나지 않았다는 뜻이다
방법2: 격리레벨을 변경하여 확인
또 다른 방법으로 아래와 같이 격리레벨을 Read Uncommited로 변경하여 확인할 수 있다.
격리레벨 Read Uncommited는 아직 커밋되지 않은 데이터의 레코드 또한 읽을 수 있다.
아래 쿼리로 조회를 하면
이렇게 id=3인 record를 조회할 수 있다.
참고로 Read Committed에서는 조회가 안된다.
이 사실들을 종합해보면 DB에 데이터를 쓰고 있다는 뜻이다.
그렇다면 이것에 대해 난 왜 몰랐을까?
굳이 변명을 하자면, 최근 수개월 동안 트래픽 등의 이유로 N+1 문제를 수정하거나, 쿼리 튜닝을 할 일이 없었던 것 같다. (DAU가 없는 서비스의 비애)
이번 기회에 잘 알게 되어 수확이 있었다. 위기의 순간 견고한 지식은 우리를 지켜줄 것이다 :)
그러나 쓸 일이 없으면 잊어버린다...
마치며
요새 MSA, DDD 등 힙합 개발 스킬들이 난무한다.
그러나 실전에서 마주하는 빈도 높은 개념은 DB, WAS 레벨의 문제라고 생각한다.
우리 모두 실생활에서 더 자주 접하는 개념들에 대해 단디 견고히 토대를 쌓도록 하자.
그럼, 안녕!
'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 >< 가장 아름다운 하나의 해답이 존재한다
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!