본문 바로가기

생각

따닥 이슈와persist context flush

 

귀뽀연님이 겪으신 이슈를 공유받다가 해결했던 경험을 기록해두려고합니다. 상황은 이러했습니다. 

  • 클라이언트에서 따닥이슈로 이력 저장 요청이 중복으로 발생했다.
  • 이때의 에러는 DataIntegrityViolationException
  • 해당 에러를 try - catch 로 잡아서, 500 -> 400 error 로 응답 수정했다.
  • 그리고 log 레벨을 error -> warn 으로 변경했다.
  • 하지만 여전히 500 에러가 발생했고, 변경했던 warning 로그도 당연히 찍히지않았다.

여전히 500 에러가 발생하는 상황

 

 

이 에러는 어디서 언제 발생한 것 일까요? 맞춰보세요.

 

 

문제의 시발점은 42번 라인입니다. 해당 로직을 타고 들어가보겠습니다.

 

 

21번 라인에서 repository.save() 를 수행하고 있습니다. 해당 위치에서 DataIntegrityViolationException 이 발생하고 있는 것은 분명한데, 왜 try - catch 에서 잡히지 않은걸까요? 데이터 저장과 조회 관련 이슈를 파악할 때는 위치와 더불어 시점도 중요하다는 점이 힌트가 될 수 있을 것 같습니다. 

JPA 영속성 컨텍스트는 save() 와 saveAll() 호출시 바로 query 를 실행하지 않고 buffer write 에 해당 수행을 모아둡니다. 이 쓰기 지연상태가 되면 1차 캐시에만 우선적으로 엔티티 객체를 저장해둡니다. 해당 캐시가 실질적으로 저장되는 시점은 메소드가 정상적으로 종료되는, 그러니까 @Transactional 에서 벗어나게 되는 메소드 종료 시점입니다. @Transactional 의 기본값은 `Propagation.REQUIRED` 이고 이 설정 값은 해당 트랜잭션 내부에서 메소드를 실행하기 때문에 try - catch 를 벗어나는 시점에 실질적으로 insert query 가  발생하는 건데요.. 이 때, DataIntegrityViolationException 이 발생하고 있었습니다.

이를 해결하기 위해서는 떠오른 방법은 세 가지 입니다. 

1. 컨트롤러에서 try-catch 로 DataIntegrityViolationException 을 잡는다. 
2. @Transactional 을 제거한다. 
3. 저장시 flush() 호출한다.

 

 

1. 컨트롤러에서 try-catch 로 DataIntegrityViolationException 을 잡는다. 

첫번째 방법은 그다지 우아해보이지 않았습니다. 컨트롤러 단에서 발생하는 DataIntegrityViolationException 을 글로벌 exception으로 잡을 수도 없고, try-catch 로 묶어두자니 save() 를 호출하는 service가 그 오류 처리를 담당해야하는 것이 맞다고 생각이 들었습니다. 정상 동작과 오류 처리 동작을 뒤섞고 싶지도 않았고 코드 구조에 근간을 흔들고 싶지 않았습니다. ㅋㅋ

 

2. @Transactional 을 제거한다. 
두번째 방법은 첫번째 보다는 나아보였지만 마찬가지로 별로이긴합니다. 그나마 나아보였던 이유는, 해당 코드가 유저 액션에 대한 이력 저장 로직이었기 때문이었습니다. 여기서 문제가 발생한다하더라도 유저가 서비스를 이용하는데에 큰 지장이 없다고 판단했습니다. 다만 영속성 컨텍스트를 이용할 수 없고 트랜잭션 처리가 힘들어지기 때문에 그 지점에 유의해야할 것 같습니다.

 

3. 저장시 flush() 호출한다.
세번째 방법은 저장시 바로 insert query 가 발생할 수 있도록 flush() 를 호출하는 것입니다. 

 

saveAndFlush() 를 이용하면 영속성 컨텍스트에 캐싱됨에 동시에 insert query 가 발생됩니다. 그래서 다음 요청 처리 시점에 바로 중복키 요청을 감지할 수 있습니다. 해당 시점에 예외가 발생하면 service 에서 잡아낼 수 있습니다.

마지막 방법이 궁극의 차선책으로 보이는데요, saveAndFlush 를 사용했을 때의 부작용을 짚어볼 필요가 있었습니다. saveAndFlush 는 jpa 영속성 컨텍스트가 주는 많은 이점을 반감하는 기능입니다. DB 에 변경 내용이 동기화될 때마다, DB 와의 통신이 발생하는 것은 어떤 서비스 또는 도메인에서는 치명적일 수 있습니다. 대부분의 경우에는 트랜잭션을 커밋할 때 변경 사항을 DB 에 반영하는 것이 성능상 이점이 있습니다. 

또, 같은 트랜잭션 내에서 다른 데이터도 함께 운용되고 있다면 그 데이터 또한 saveAndFlush 를 해주어야할 것 같다는 생각이 듭니다. 데이터의 일관성을 지키기 위해서 입니다. 트랜잭션 롤백이 발생하더라도 이미 데이터 베이스에 영향을 미친 경우도 함께 고려해야합니다. 예를 들어 SCDF 를 이용하고 있고 그 대상 테이블에 insert 를 감지한다면 SCDF 를 소싱받고 있는 비즈니스에는 그 영향이 이미 전파됩니다. 이런 경우 보상 이벤트 발행 등의 재처리 과정이 필요할 수도 있을 것 같다는 생각입니다.

유저의 액션을 이력으로 적재해두는 도메인에서 어떤 방법이 제일 현명할까요. 도메인에 따라- 비즈니스에 따라- 사람에 따라- 상황에 따라- 달라지는 이런 결정들이 개발을 참 어렵게도 만들고 재미지게도 만드는 것 같습니다.