Skip to content

@Async로 인해 트랜잭션이 분리되어 데이터 정합성이 깨지는 문제 #19

@sgn07124

Description

@sgn07124

🐞 버그/에러 개요

비동기 메서드 @Async 사용 시 트랜잭션 분리로 인한 데이터 불일치 현상 발생

📝 상황 설명

📄 에러 대상

  • AnswerServiceImpl.saveAnswer()
  • FeedbackServiceImpl.saveFeedback()

🕵🏻‍♀️ 에러 상황

  • saveAnswer()는 트랜잭션 안에서 Answer, Question 상태를 변경하고, 내부에서 @Async @Transactional이 적용된 saveFeedback()을 호출함
  • 이후 의도적으로 RuntimeException을 발생시켜 saveAnswer()의 트랜잭션이 롤백되도록 설계
[Main Thread - saveAnswer()]
├── Answer 저장
├── Question 상태 업데이트
├── 비동기 saveFeedback() 호출
├── (예외 발생)
└── 전체 롤백

[Async Thread - saveFeedback()]
├── GPT API 호출
├── AnswerFeedback 저장
└── 독립적으로 커밋됨

문제점

  • saveAnswer()에서 발생한 예외로 인해 Answer, Question은 DB에서 롤백됨
  • 그러나 saveFeedback()은 별도 스레드에서 별도 트랜잭션으로 동작하여 커밋됨
  • 이로 인해 실제 답변(Answer)은 없는데, 피드백(AnswerFeedback)은 존재하는 불일치 상태가 발생함

✅ Resolve TODO

  • 비동기 메서드의 호출 시점/위치를 재검토 (트랜잭션 외부에서 호출되도록 리팩터링 고려)
  • 트랜잭션 전파 전략, 트랜잭션 매니저 구성 방식 확인

📚 Remarks

  • Spring에서 @Async@Transactional을 함께 사용하면, 해당 메서드는 별도 스레드에서 새로운 트랜잭션으로 실행됨
  • 호출부(saveAnswer)의 트랜잭션이 롤백되더라도, 비동기 메서드는 영향받지 않음
  • 현재 구조에서는 서비스 내부에서 비동기 메서드를 호출하기 때문에, 데이터 정합성 문제의 위험성이 항상 존재함

비동기 처리 테스트 실패 - @Async 트랜잭션 분리 현상 검증 불가 (해당 테스트를 통과해야 입증 가능)
자세한 사항은 노션 참고

@SpringBootTest
@ActiveProfiles("test")
@EnableAsync // 테스트에서도 명시적으로 활성화
class AnswerServiceIntegrationTest {
    
    @Test
    @DisplayName("비동기 시 트랜잭션 미적용 테스트")
    void saveAnswer_withAsyncFeedback_shouldCauseDataMismatch() throws InterruptedException {
        // given
        Long questionId = 1L;
        AnswerDto dto = new AnswerDto("답변입니다");
        
        // when - 예외를 던지기 전에 잠시 대기
        try {
            answerService.saveAnswer(dto, questionId);
            Thread.sleep(1000); // saveFeedback 호출이 시작될 시간을 줌
            throw new RuntimeException("일부러 트랜잭션을 깨뜨림");
        } catch (RuntimeException e) {
            // rollback 발생
        }
        
        // wait: 충분한 대기 시간
        Thread.sleep(15000);
        
        // then
        List<Answer> answers = answerMapper.findAll();
        List<AnswerFeedback> feedbacks = feedbackMapper.findAll();
        
        System.out.println("Answers count: " + answers.size());
        System.out.println("Feedbacks count: " + feedbacks.size());
        
        assertThat(answers).isEmpty(); // 롤백됨
        assertThat(feedbacks).isNotEmpty(); // rollback 안됨
    }
}

Metadata

Metadata

Assignees

Labels

🐛 Bug버그 수정 및 오류 해결

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions