트러블슈팅

Repeatable Read로 인한 문제

삼록이 2025. 12. 3. 13:02
문제.
분명 DB에 있는 칼럼 값을 스프링에서 읽어 오지 못하고 null이 발생하는 문제

 

문제

개발을 하다 문제가 생겼다.

예를 들어,

유저테이블에 

id가 1인 삼록 row가 있다고 하자 이때 주소테이블에 '부산시 연제구~~' 라는 값이 있다.

스프링에서 select address from user where = #{id}인 마이바티스 쿼리를 날리는데 id가 1인 변수를 전달해도 

계속해서 null값이 뜨는 문제였다.

 

DB에서 sql쿼리를 직접 날리면 결과가 조회되는데 왜 스프링에서 가져오지 못할까?

변수명이 틀린 것도 아니고 오타가 있는 것도 아니고 스프링에서 왜 null값이 조회되는지 처음엔 답답했다.

 

원인 1. @Transactional 

문제의 원인은 트랜잭션이었다.

먼저 전체적인 조회 구조를 살펴볼 필요가 있다.

예를들어, 전체적인 구조는 클라이언트에서 어떤 버튼을 누르면

스프링에서 다른 애플리케이션(ex.파이썬)으로 하여금 유저 테이블에 주소를 넣는다.

그런다음 즉시 스프링에서 그 주소를 가지고 오는 구조였다.

그리고 이러한 로직을 실행하는 스프링 메서드는 @Transactional 어노테이션이 붙어있었다.

그리고 연결되어있는 DB는 mySQL이다.

 

원인 2. 트랜잭션 격리 수준

mySQL의 기본 트랜잭션 격리 수준은 REPEATABLE READ다.

(참고로 Oracle, PostgreSQL의 격리수준은 READ COMMITED)

(*트랜잭션의 격리수준에 대한 설명은 https://gotopm.tistory.com/55 의 6번 항목 참고)

 

@Transactional어노테이션이 붙어있으므로,

스프링이 처음 메서드를 실행할 때 트랜잭션이 시작된다.

트랜잭션이 시작되면 MySQL은 스냅샷을 생성한다.

이 스냅샷을 생성할 때 id가 1인 삼록의 row에는 바로 주소칼럼이 비워져있다.

그리고 스프링에서 다른 애플리케이션(ex.파이썬)으로 하여금 유저 테이블에 주소를 넣어도

이 트랜잭션안에서는 처음 스냅샷을 보고 판단하기에 주소가 없는 null값으로 판단한 것이다.

 

만약 연결되어있는 DB가 Oracle이나 PostgreSQL이었다면 기본 트랜잭션 격리수준이 READ COMMITED로 설정되어있었을 것이고 이 경우에는 커밋이 완료된 데이터는 바로 읽을 수 있으므로 파이썬이 유저테이블에 주소를 인서트 커밋을 완료하자마자 스프링에서는 정상적으로 값을 읽어올 수 있었을 것이다.

 

해결

이 경우에는 2가지 방법이 있다.

트랜잭션이 없는 상태에서 조회오도록 해야하거나 MySQL 격리 수준을 READ COMMITED로 변경이 필요하다.

그래서 간단하게 트랜잭션이 없는 상태에서 조회해오는 방법을 선택했다.

바로 @Transactional 어노테이션을 제거하거나 @Transactional(propagation = Propagation.NOT_SUPPORTED) 옵션을 사용하면 트랜잭션이 없는 상태에서 조회해올 수 있다.

 

*Transactional어노테이션을 제거한것과 Propagation.NOT_SUPPORTED 옵션을 준 것의 차이는?

 

즉 향후에 트랜잭션을 실행시키는 다른  메서드(상위 메서드) 내부에서 다시 이 문제가 일으킨 메서드(하위 메서드)를 실행시킨다면  propagation = NOT_SUPPORTED 옵션을 붙여놓으면 이 메서드는 상위 트랜잭션을 중단 시키고 트랜잭션이 없는 상태로 실행된다.

 

스프링 트랜잭션이 REPEATABLE READ 스냅샷을 고정해버려서,
트랜잭션 시작 이후에 외부에서 INSERT한 row가 현재 트랜잭션에서는 절대 보이지 않았던 것이 문제의 본질이었다.