
메시지 읽음 처리 기능 구현 중 다음과 같은 에러가 발생했습니다
Copyjakarta.persistence.TransactionRequiredException: Executing an update/delete query
프론트엔드에서는 다음과 같은 에러를 확인할 수 있었습니다
POST http://localhost:8090/api/messages/2/read 500 (Internal Server Error)
초기에는 다음과 같은 구조로 코드가 작성되어 있었습니다
@Transactional
suspend fun markMessageAsReadAndGetUnreadCount(messageId: Long, recipientId: Long): Long {
messageRepository.markMessageAsRead(messageId, recipientId)
return messageRepository.countUnreadMessagesByRecipientId(recipientId)
}
트랜잭션 처리를 위해 두 개의 계층으로 분리합니다
@Transactional
fun markMessageAsReadAndGetUnreadCount(messageId: Long, recipientId: Long): Long {
messageRepository.markMessageAsRead(messageId, recipientId)
return messageRepository.countUnreadMessagesByRecipientId(recipientId)
}
suspend fun markMessageAsReadAndGetUnreadCountSuspend(messageId: Long, recipientId: Long): Long {
return withContext(Dispatchers.IO) {
markMessageAsReadAndGetUnreadCount(messageId, recipientId)
}
}
@Modifying
@Query("UPDATE MessageRecipient mr...")
@Transactional
fun markMessageAsRead(messageId: Long, recipientId: Long)
try {
val unreadCount = messageService.markMessageAsReadAndGetUnreadCountSuspend(messageId, currentUser.id)
logger.info("Unread count after marking as read: $unreadCount")
return ResponseEntity.ok(UnreadCountResponse(unreadCount))
} catch (e: Exception) {
logger.error("Error in markMessageAsRead: ${e.message}", e)
throw e
}
Kotlin Coroutines와 Spring 트랜잭션을 함께 사용할 때는 세심한 설계가 필요 하는것을 배웠습니다.
트랜잭션 범위와 코루틴 컨텍스트의 관계를 명확히 알아야한다는것을 알게되었습니다.
적절한 로깅과 에러 처리는 운영 단계에서 매우 중요하는것을 배웠습니다.
Kotlin Coroutines와 Spring 트랜잭션의 통합 과정에서 발생하는 문제를 정확히 파악하는 것이 가장 어려웠습니다.
ThreadLocal 기반의 트랜잭션 관리와 코루틴의 스레드 전환 특성이 충돌하는 지점을 이해하고 해결방안을 도출하는 과정이 까다로웠습니다.
트랜잭션의 안정성과 코루틴의 비동기 처리 이점을 모두 활용하기 위해서입니다.
Non-suspend 함수에서 트랜잭션을 처리하고, 이를 suspend 함수로 래핑하는 방식을 통해 두 가지 목표를 모두 달성할 수 있었습니다.
Spring의 트랜잭션 전파 설정을 변경하는 방법도 고려했습니다.
완전히 리액티브한 방식으로 전환하는 것도 검토했으나, 기존 시스템과의 호환성 문제로 현재의 해결방식을 선택했습니다.
Spring 공식 문서: Kotlin 지원
Kotlin Coroutines 공식 문서
Spring Transaction Management 문서