BackEnd/Java

[Java]@Transactionaleventlistener 다루기

꾹꾹이 2023. 9. 15.
728x90

문제 상황

 

실제 근무 중인 곳에서 발생한 문제입니다.

엔티티를 DB에 저장한 후 외부 api를 호출하는 메서드가 있습니다.

이 메서드는 트랜잭션으로 묶여있기 때문에 외부 api 호출 시 장애가 발생하면 DB 저장 작업까지 모두 RollBack이 되었습니다. 문제는 메서드가 호출되면 DB 저장은 필수로 이루어져야 하기 때문에 RollBack 현상이 발생하면 안 된다는 것입니다. 

 

@Transactionaleventlistener이란?

 

메서드를 트랜잭션을 묶어서 처리하는 경우 트랜잭션이 끝나는 시점에 호출하는 이벤트 리스너입니다.

예를 들어 회원가입 후 쿠폰을 발급하는 로직이 있다고 가정했을 때, 회원가입은 정상적으로 완료되었지만 쿠폰 발급이 실패한 경우 회원가입 처리까지 rollback 되는 현상이 발생하게 됩니다. 쿠폰 발급에 실패하더라도 회원가입을 정상 완료 시키고자 할 때 Transactionaleventlistener를 사용하여 rollback을 막을 수 있습니다.

또한, 쿠폰 발급이 끝날 때까지 기다릴 필요가 없기 때문에 성능을 높이는 용도로 사용할 수도 있습니다.

 

Transactionaleventlistener 옵션

 

1. TransactionPhase.AFTER_COMMIT : default 값. 트랜잭션이 commit 되었을 때 이벤트 실행

2. TransactionPhase.BEFORE_COMMIT : 트랜잭션이 rollback 되었을 때 이벤트 실행

3. TransactionPhase.AFTER_COMPLETION : 트랜잭션이 completion(commit or rollback)되었을 때 이벤트 실행

4. TransactionPhase.ROLLBACK : 트랜잭션이 commit 되기 전에 이벤트 실행

 

제 경우에는 디폴트 설정으로 문제를 해결할 수 있었습니다. 

 

작업 순서는

Transaction 시작 -> 메서드 실행 -> commit -> 이벤트 리스너 호출 -> 이벤트 리스너 실행 -> Transaction 종료

 

Transactionaleventlistener  사용 시 유의할 점은 이벤트 리스너를 사용한다고 해서 트랜잭션과 이벤트 리스너가 완전히 독립적으로 분리된 것은 아닙니다.

여전히 하나의 트랜잭션으로 묶여있으며 트랜잭션이 완전히 끝난 것이 아닙니다.

이게 무슨 말인가 하실 수 있는데요. 트랜잭션 끝나고 이벤트 리스너 호출하는 거 아니었냐?

 

정확히 설명하자면 트랜잭션이 완전히 종료된 것이 아니며 commit 이후에 이벤트를 호출한 것일 뿐이지 트랜잭션이 끝나지 않은 상태입니다.

 

저의 경우에는 이벤트 리스너로 외부 api만을 호출했기 때문에 문제가 없었습니다.

 

하지만 데이터를 저장하는 작업을 이벤트 리스너에서 처리한다면 저장이 되지 않는 현상이 발생합니다.

원래 데이터 저장은 트랜잭션이 끝나는 시점에 이루어지기 때문입니다.

따라서, 이벤트 리스너에서는 read만 가능할 뿐, C(create), U(update), D(delete)가 불가능합니다.

 

그렇다면 이벤트 리스터에서 cud 작업을 해야할 때는 어떻게 해결해야 하냐?

 

이벤트 리스너를 별개의 트랜잭션으로 분리하면 됩니다.

@Transactional의 propagation을 REQUIRES_NEW로 변경하면 됩니다.

 

REQUIRES_NEW는 부모 메서드의 트랜잭션을 이어받지 않고 별도의 새로운 트랜잭션을 생성하는 옵션입니다.

디폴트 옵션은 REQUIRED로 부모 트랜잭션을 자식이 이어받습니다.

 

@TransactionalEventListener를 선언했던 메서드에

@Transactional(Trasactional.TxType.REQUIRES_NEW)를 추가하면 간단히 해결할 수 있습니다.

 

 

Transactionaleventlistener를 사용할 때는 목적이 중요한 것 같습니다.

단순히 기능의 분리만을 목적으로 사용한다거나 에러 처리를 막기 위해서 사용하려는 것이라면 조금 더 생각을 해봐야 하지 않을까 싶습니다.

에러처리를 무시함으로써 rollback을 막고자 하는 의도라면

에러 발생의 요지가 있는 메서드를 try catch로 감싸고 에러 발생 시 로그만 남기는 방법만으로도 간단히 구현이 가능합니다.

 

저의 경우에는 '애초에 같은 트랜잭션에 묶이면 안되는 기능이 왜 같은 서비스 안에 존재하는 거지?'라는 의문이 생겼습니다.

필수로 완료되어야 하는 작업과 실패 가능성이 있는 메서드가 한 서비스 안에 존재하고 있는 상황 자체가 문제라고 인지했습니다.

그래서 단순히 에러 처리만 해결하는 방법이 아닌 이벤트 분리를 선택한 것입니다.

 

설계, 개발 전에는 항상 '목적'이 무엇인지 고민하는 버릇을 들이는 것이 좋을 것 같습니다.

댓글