https://github.com/Seungkyu-Han/Toge-do-backend
GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다
Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.
github.com
해당 프로젝트에 transaction을 걸어보려 한다.
우선 transaction 설정을 하는 이유부터 말해보려 한다.
프로젝트 중 다음과 같은 코드가 있다.
override fun disconnectFriend(id: ObjectId, friendId: ObjectId): Mono<UserDocument> {
return friendService.removeFriend(id, friendId)
.flatMap {
friendService.removeFriend(friendId, id)
}
id에 있는 friend를 삭제하고, 그 다음으로 friend에 있는 id를 삭제하는 것이다.
서로의 친구 목록에서 서로를 삭제하는 메서드이다.
다음과 같은 상황을 가정해보자.
위의 코드가 성공한 다음에, 아래에서는 에러가 발생한 상황이다.
각각 userA, userB라고 하면 userA의 친구목록에는 userB가 없지만 userB의 친구목록에는 userA가 있는 모순된 상황이 생긴다.
이러면 데이터베이스의 무결성이 깨지게 되기 때문에, 아래 코드에서 error가 생기면 위의 코드를 Rollback 해줘야 한다.
보통 이런 경우에서 기존의 Spring MVC에서는 @transactional을 사용하게 된다.
하지만 찾아보니 webflux 환경에서는 다른 스레드에서 작동한다는 특성 때문에 @transactional이 적용되지 않는다고 하여 알아보고, 방법을 찾아보려 한다.
- 1. 중간에 Error를 발생시키고 데이터베이스 모니터링 해보기
위의 코드를 그대로 사용하고, 첫번째 removeFriend는 성공 두번째 removeFriend는 Exception을 발생시켰다.
이런 경우에는 예상대로, 한쪽의 친구만 삭제되고 한쪽은 남아있게 될까?
removeFriend를 다음과 같이 작성하여, 짝수번 요청에서는 에러를 발생시켰다.
private var flag = false
override fun removeFriend(userId: ObjectId, friendId: ObjectId): Mono<UserDocument> {
return userRepository.findById(userId)
.flatMap {
flag = !flag
if(flag){
it.removeFriend(friendId)
}
else
Mono.error(FriendException(ErrorCode.NOT_FRIEND))
}
.flatMap{
userRepository.save(it)
}.onErrorMap{
when(it){
is CantRequestToMeException -> FriendException(ErrorCode.CANT_REQUEST_TO_ME)
is NotFriendException -> FriendException(ErrorCode.NOT_FRIEND)
else -> it
}
}
}
현재 데이터베이는 아래와 같은 상태이다.
서로 각자의 ObjectId를 가지고 있다.
Swagger는 발생한 에러대로 친구가 아니라는 에러가 발생했으며
데이터베이스를 보면 한쪽의 친구 요청만 끊어진 것을 볼 수 있다.
자 이제 여기에 transaction을 적용해보자.
- 2. TransactionalOperator를 적용
org.springframework.transaction.reactive 라이브러리에서 제공하는 TransactionalOperator를 이용하는 것이다.
우선 다음과 같이 Config 파일을 만들어주고
@Configuration
class TransactionalOperatorConfig {
@Bean
fun transactionalOperator(transactionManager: ReactiveMongoTransactionManager): TransactionalOperator {
return TransactionalOperator.create(transactionManager)
}
@Bean
fun reactiveMongoTransactionManager(dbFactory: ReactiveMongoDatabaseFactory): ReactiveMongoTransactionManager {
return ReactiveMongoTransactionManager(dbFactory)
}
}
여기서 중요한 부분은 해당 데이터베이스가 replica 세팅이 되어 있어야 한다는 것이다.
사용한 mongodb의 docker-compose 파일은 아래에 작성해두도록 하겠다.
해당 도커의 컨테이너에서 쉘에 들어가
rs.initiate()
이거만 입력해주면 설정이 끝난다.
이렇게 설정을 해두고
transaction을 설정하고 싶은 부분에 다음과 같이 작성해주면 된다.
transactionalOperator.transactional(Mono or Flux)
나는 해당 코드를 다음과같이 작성했다.
return transactionalOperator.transactional(friendService.removeFriend(id, friendId)
.flatMap {
friendService.removeFriend(friendId, id)
})
이렇게 removeFriend의 2개의 함수가 하나의 transaction 안에 들어왔다
이제 한 번 테스트를 해보자.
(데이터베이스를 재설정해서 방금과 id가 바뀌었다.)
이게 데이터베이스이고 다시 에러가 발생하는 API를 요청해보았다.
일단 당연히 응답은 에러이다.
데이터베이스도 확인을 해보니
다음과 같이 transaction이 적용된 것을 볼 수 있었다.
해당 방법을 이용하여 프로젝트에 transaction을 적용하려 한다.
- 3. @Transactional annotation 사용
자 그러면 진짜 webflux에서 transcationl annotation이 작동하지 않을까?
어차피 코드를 만들어본 김에 테스트 해보려 한다.
코드를 이렇게 만들어서 테스트 해보았다.
@Transactional
override fun disconnectFriend(id: ObjectId, friendId: ObjectId): Mono<UserDocument> {
// return transactionalOperator.transactional(friendService.removeFriend(id, friendId)
// .flatMap {
// friendService.removeFriend(friendId, id)
// })
return friendService.removeFriend(id, friendId)
.flatMap {
friendService.removeFriend(friendId, id)
}
현재 데이터베이스는 아래와 같다.(사실 위와 같다..)
자 이제 서버를 재실행하고 다시 에러가 발생하는 API를 요청해보자.
일단 당연하게 응답은 같다.
자 이제 데이터베이스를 확인해보자.
이렇게... transaction 적용이 된 것을 볼 수 있다...하하
그래도 프로젝트에서는 2번 방법을 사용할 것 같다.
값을 비동기로 반환하기에... 하지만 왜 영어를 쓰시는 분들이 webflux에서 @transactional을 사용하지 말라는지는 더 알아봐야 할 거 같다...
이렇게 오랫동안 고민만 했던 transaction을 프로젝트에 적용할 수 있었다.
'토이 프로젝트' 카테고리의 다른 글
WebFlux SwitchIfEmpty가 항상 시작되는 에러 (2) | 2024.12.23 |
---|---|
MSA Gateway에 CircuitBreaker 적용 (0) | 2024.12.16 |
FCM을 사용한 실시간 알림 서비스 (0) | 2024.12.13 |
SSE를 사용한 앱 실행 중 알림 서비스 (0) | 2024.12.13 |
AbstractGatewayFilterFactory 테스트하기 (1) | 2024.12.08 |