Spring을 활용하여 트랜잭션을 관리하다 보면 @Transaction
Annotation을 사용하게 된다.
그런데 만약 해당 Annotation이 적용된 로직에서 외부 API Call이 있다면 어떻게 될까?
롤백
가장 먼저 고민이 드는 부분이 롤백이다.
만약 중간에 Exception이 발생하여 rollback이 되었다고 외부 호출은 rollback 될까?
@Transactional
fun test() {
testRepository.save(Test().apply { name = "test" })
apiCallClient.testCall()
throw RuntimeException()
}
@Service
class ApiCallClient {
private val restClient = RestClient.create("http://localhost:8080")
fun testCall() {
val response = restClient.get()
.uri("/api-call")
.header("Content-Type", "application/json")
.retrieve()
.body(String::class.java)
println("Response: $response")
}
}
/api-call은 Test 엔티티를 저장하는 외부 API라고 가정했다. 결과를 확인해 보자.
H2를 켜서 보면 실제로 최상위 트랜잭션은 Exception으로 인해 롤백이 되었지만, Apicall로 이루어진 저장은 성공적으로 된 것을 볼 수 있다. 한마디로 외부 API 호출은 트랜잭션 롤백의 영향을 받지 않는다는 것을 알 수 있다.
외부호출은 rollback 되지 않는데 어떻게 해야 할까?
여러 가지 방안이 떠오를 수 있다. 생각해 본 부분들을 아래에 기술해 보았다.
- 외부호출을 가장 마지막에 수행한다.
- ⇒ 로직이 수행됨에 있어서, 외부 호출을 가장 마지막에 둔다면 로직의 순서에 따라 Exception이 먼저 발생하면 Call을 하지 않을 것이다. 하지만 중간에 외부 API call 값이 필요한 순간이 온다면 적절한 방법은 아닐 것이다.
- 실패에 대한 보상 트랜잭션?
- 분산 트랜잭션을 제어하기 위한 Saga Pattern을 활용하는 방법이다.
- 실패에 대한 이벤트를 발생시키고 외부호출을 하는 쪽이 해당 이벤트를 consume 하여 해당 호출에 대한 롤백을 수행한다.
- 보상트랜잭션 + 외부호출 데이터 저장 전에 수행하면 데이터의 무결성이 잘 보장될 것이다.
- 하지만 데이터를 먼저 저장해야 하고 나중에 외부호출이 필요한 경우에는? 데이터에 대한 보상트랜잭션도 고민해야 한다.
- 외부 호출을 제어할 순 없을까?
- ⇒ 대게 외부 API 호출은 제어할 수 없다. 우리 API를 쓰는 클라이언트의 입장에서는 멱등하게 값을 내릴 순 있겠지만, 외부 API를 호출하는 쪽에서는 이를 컨트롤할 수 없다. 그리고 외부 API + 우리 로직으로 인하여 발생하는 DB 저장이 존재한다면 결국 롤백이 제대로 되어야 한다.
- 트랜잭션 커밋 혹은 롤백 단위로 이벤트 리스너를 활용한다?
- ⇒ 이게 방법이 될 수 도 있다. 특정 트랜잭션이 커밋되었을 때만 행하는 외부 API call이면 굳이 같은 트랜잭션에 태우는 것이 아니라 이벤트 리스너를 통해 이를 구현할 수 있을 것이다. (ex - 회원가입 시 이메일 전송
결론
여러 가지 상황이 있겠지만, 명확하게 이거다! 하는 솔루션은 없다는 것을 알게 된다. 결국 사용하는 사용자 입장에서는 외부 API Call에 대해 트랜잭션을 분리하여 Db Connection을 물고 늘어지는 현상을 최소화하고 외부 API Call에 대한 명확한 처리가 되어있어야 한다.
결과적으로 네트워크 요청에 대해 잘 대응해야 한다는 것이다.
MSA 구조에서는 Saga Pattern이던 어떤 방식이던 네트워크 요청으로 이를 주고받는 형태이다. 결국 요청자 입장에서는 알 수 없는 에러나 예외에 대한 처리가 되어 있어야 하고 이에 대한 Next Flow를 가정해야 한다. 그리고 제공하는 입장에서는 멱등하게 API를 제공해야 한다.
이는 곳 동일한 요청인가?부터 시작하여 특정 API를 식별할 수 있는 식별자를 통하여 관리하는 것도 방법이 될 수 있다.
그래서 MSA 환경에서는 성공, 실패, 알 수 없는 무언가에 대한 처리가 잘되어야 한다. 결국 응답자와 호출자는 이에 맞게 적절한 처리가 필요하다. (물론 어렵지만…)
'서버 개발(생각과 구현)' 카테고리의 다른 글
Custom Swagger를 통해 협업, 업무 효율성 동시에 잡기 (0) | 2024.12.27 |
---|---|
JPA N+1의 의도와 문제 해결 (0) | 2024.12.26 |
락의 필요성과 실제 사용을 위한 분산 락, 낙관적 락, 비관적 락 탐구 (0) | 2024.12.22 |
서킷 브레이커(CircuitBreaker)가 필요한 경우 (0) | 2024.12.21 |
Spring Retry(재시도)가 필요한 경우 (0) | 2024.12.20 |