스프링 부트/오류

[JPA] ExecutorService를 통한 엔티티가 변경되지 않는 않는 오류

주인 완 2024. 11. 19. 18:14

가천대학교 가을 축제 웨이팅 서비스 라인업지를 개발하면서, 우리 서비스는 노쇼 방지를 위해 자동 취소 로직을 구현해야 할 필요가 있었다.

 

다만 단순한 '자동 취소'를 구현하는 것이 아닌,

  • 자동 취소 시 주점의 대기 팀 숫자가 감소해야 함
  • 자동 취소는 호출 이후 특정 시점 이후에 동작해야 함
  • 자동 취소 시, 사용자한테 알림톡으로 취소되었다고 알려주어야 함

이런 정책들을 고려하면서 로직을 구현해야 했고,

 

이를 위해 ScheduledExecutorService를 도입하였다.

 

ScheduledExcutorService

ScheduledExecutorService는 원하는 작업(메서드)를 특정 시간 이후에 동작하도록 설정할 수 있는 ExecutorService의 일종이다.

ScheduledExcutorService의 JavaDoc

ScheduledExecutorService는 총 3개의 스케쥴링 방식을 지원한다.

서비스 정책에 따라 우리는 특정 시간 이후(delay) 동작하는 방식을 택했고, 해당 방식을 통한 코드는 다음과 같다.

 

자동 취소 스케쥴링 & 자동 취소 로직

자동 취소를 위한 스케쥴링 코드

알림톡 발송을 위한 변수들이 담긴 Map, 웨이팅을 가져오기 위한 waitingId, 그리고 값 수정을 위한 Pub 객체를 매개변수로 넣어주었다.(그리고 Pub을 절대 매개변수로 넣었으면 안 됐다.....)

 

자동 취소 로직

자동 취소 로직은 다음과 같다.

  1. 전달받은 waitingId를 기반으로 웨이팅 객체를 조회해서 가져온다.
  2. waiting 객체의 상태를 확인한다.
  3. 상태가 충족되면, 취소시킨다.
  4. 해당 주점의 대기 팀 수를 감소시킨다.

우리가 생각한 대로라면, 모든 로직은 정상적으로 작동해야 했었다.

 

수정한 값이 커밋되지 않는 오류

그러다 QA를 진행하던 도중, 정말 치명적인 오류 제보를 받았다. (테스트코드의 중요성..)

정말 감사한 분..ㅠㅠ

위에서 언급한 자동 취소 로직 중, 4번째 정책이 정상적으로 작동하지 않는 것이었다.

그 당시, 디버깅을 통해 몇 번이나 원인을 파악하려고 노력했고, 덕분에 다음과 같은 현상을 파악할 수 있었다.

 

그전에, 그 당시 자료들을 캡처하지 않아 글로만 남기게 되어 우선 죄송하다는 말씀을 드린다..
이후 모든 트러블슈팅 시 진행 과정을 최대한 캡처해서 남기려고 노력하고 있다. ㅠㅠ

 

  • Waiting 객체의 상태 값은 정상적으로 변경되는 것을 확인할 수 있었다.
  • 마찬가지로 Pub 객체의 값은 디버깅 시 정상적으로 감소되는 것을 확인할 수 있었다.
  • Waiting 객체의 변경된 값은 DB에 정상적으로 커밋되었다.
  • 하지만, Pub 객체의 변경된 값은 커밋되지 않았다.

여기서 우리는 Waiting 객체 덕분에 비교적 수월하게 원인을 파악할 수 있었다.

같이 값이 변경되는데, Waiting 객체만 왜 정상적으로 커밋되는 걸까? 두 객체의 차이점이 뭐지?

 

'Waiting 객체는 해당 메서드에서 바로 가져오니까 문제가 없는 게 아닐까?'

 

정답이었다.

 

원인: JPA

이제 정확히 왜 해당 이슈가 발생했는지 알아야 했고, 우리는 JPA의 영속성 컨택스트에 대한 부족한 이해도 때문에 해당 이슈가 발생했다고 생각하였다.

 

영속성 컨택스트

'엔티티를 영구 저장하는 환경'이라고 보면 된다. 여기서 '영구 저장'이라 함은, 애플리케이션 <-> DB 사이에서 엔티티들을 보관하고 관리하는 가상의 DB 역할을 수행한다고 생각하면 편하다.

 

우리가 엔티티를 저장(repository.save())하거나 조회(repository.find())하면 해당 엔티티 매니저는 영속성 컨택스트에 엔티티를 보관하고 관리한다.

 

엔티티 생명주기

JPA에서 엔티티는 크게 네 가지 상태로 나뉠 수 있다.

  • 영속: 영속성 컨택스트에 저장된 상태
  • 준영속: 영속성 컨택스트에 저장되었다가 분리된 상태
  • 비영속: 영속성 컨택스트와 전혀 관계가 없는 상태
  • 삭제: 삭제된 상태

우리는 여기서 문제가 되는 영속, 준영속 엔티티에 대해서만 알아볼 것이다.

 

영속 상태:
우리가 엔티티 매니저를 통해 DB에서 엔티티를 조회하면, 해당 엔티티는 영속성 컨택스트 내에서 영속 상태가 된다.

영속 상태의 엔티티는 1차 캐시에 저장되어, 동일한 쿼리문을 실행해도 JPA의 1차 캐시에서 해당 엔티티를 조회 후 가져온다.

이때 해당 엔티티에 대해 JPA는 변경 감지라는 특징을 가져, 엔티티가 수정될 경우 DB에 자동으로 변경값을 적용해 준다는 이점을 지닌다.


준영속 상태:
엔티티가 더 이상 엔티티 매니저의 관리 상태가 아닌 상태이다. 보통 트랜잭션이 분리 또는 종료되었거나 영속성 컨택스트 밖으로 엔티티가 전달된 경우에 해당 엔티티는 준영속 상태가 된다.
분리된 엔티티에 대한 변경 사항은 자동으로 동기화되지 않는다는 특징을 지닌다.

 

코드에서의 문제점

자동 취소를 위한 스케쥴링 코드

ExecutorService를 통한 스케쥴링으로 호출되는 자동 취소 메서드는 별도의 트랜잭션으로 동작하게 된다.

이때, 매개변수로 전달된 Pub 엔티티는 자동 취소 메서드의 트랜잭션 내부의 영속성 컨택스트에서 명시적으로 조회(repository.find())되지 않은 상태이다..!! 따라서 엔티티 매니저는 해당 엔티티를 영속 상태가 아닌 준영속 상태로 간주하게 되어 다음과 같은 현상이 발생한다.

  • Pub 객체의 변경 사항은 메모리 내의 객체에만 영향을 끼친다.
  • 변경된 사항은 DB에 커밋되지 않는다.

Waiting 객체가 정상적으로 커밋되었던 이유는, 자동 취소 메서드의 트랜잭션 내부에서 엔티티를 명시적으로 조회(repository.find())했기 때문에 변경 감지 기능을 JPA가 지원해 주었기 때문이다!

 

해결: 엔티티를 명시적으로 조회

Pub 엔티티를 Waiting에서 가져온다.

  • Waiting 객체는 해당 트랜잭션 내에서 영속성 컨택스트에 의해 관리된다.
  • Waiting과 연관된 Pub 엔티티를 조회하면, 엔티티 매니저는 이를 마찬가지로 영속 상태로 간주한다.
  • 따라서 두 엔티티의 변경 감지 기능이 정상적으로 작동하게 된다.

AS-IS

매개변수로 Pub 엔티티를 넘겨준다.

TO-BE

자동 취소 메서드에서 Pub 엔티티를 Waiting 엔티티로부터 명시적으로 조회해서 가져온다.

Hibernate 쿼리문

Waiting 엔티티에서 Pub 엔티티를 가져오는 쿼리문

 

마무리

결국 JPA의 작동 방식에 대한 이해도가 부족해서 발생한 문제였다.

그래도 긍정적인 점을 찾자면, 원인을 빠르게 파악해서 이를 분석하고 문제를 해결했다는 것을 들 수 있겠다.

또한 무작정 사용하기만 했던 JPA에 대한 기본기를 익히게 되어서 오히려 기본기를 보다 탄탄하게 다질 수 있게 되어 다행이라고 생각한다.

 

앞으로는 기술 스택에 대한 이론적인 부분도 꼭 짚고 넘어가, 내가 사용하고자 하는 기술에 대해 정확히 파악하고 있는 개발자가 되려고 노력하고자 한다.