Spring
Spring | @TransactionalEventListener
치킨닮은닭
2023. 9. 17. 01:00
@TransactionalEventListener
- 이벤트 처리 로직에서 트랜잭션을 적용해야 하는 경우 사용한다.
- phase 옵션으로 어떤 식으로 트랜잭션 내에서 동작을 할 지 결정할 수 있다.
- TransactionPhase.AFTER_COMMIT
- 기본값
- 트랜잭션이 commit 되었을 때 이벤트 로직을 실행한다.
- TransactionPhase.AFTER_ROLLBACK
- 트랜잭션이 rollback 되었을 때 이벤트 로직을 실행한다.
- TransactionPhase.AFTER_COMPLETION
- 트랜잭션이 완료(commit 또는 rollback) 되었을 때 이벤트 로직을 실행한다.
- TransactionPhase.BEFORE_COMMIT
- 트랜잭션이 commit 되기 전에 이벤트를 실행한다.
- TransactionPhase.AFTER_COMMIT
Example
옵션값 별로 어떻게 동작하는지 테스트해보자. 간단하게 User를 DB에 저장하고 이벤트를 발행하는 서비스를 작성하였다. DB저장은 Spring data JPA를 이용하여 구현하였다.
User
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class User {
@Id
private String id;
@Column(length = 15)
private String password;
@Column
private String name;
User(String id, String password, String name) {
this.id = id;
this.password = password;
this.name = name;
}
public static User from(UserCreationRequest request) {
return new User(
request.getId(),
request.getPassword(),
request.getName()
);
}
public UserDto toDto() {
return new UserDto(this.id, this.name);
}
UserDto
@Getter
@ToString
public class UserDto {
private final String id;
private final String name;
public UserDto(String id, String name) {
this.id = id;
this.name = name;
}
}
UserCreationRequest
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserCreationRequest {
private String id;
private String password;
private String name;
public UserCreationRequest(String id, String password, String name) {
this.id = id;
this.password = password;
this.name = name;
}
}
CustomEventPublisher
@Component
@RequiredArgsConstructor
public class CustomEventPublisher {
private final ApplicationEventPublisher publisher;
public void publish(@NonNull CustomTransactionalEvent event) {
publisher.publishEvent(event);
}
}
UserService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final CustomEventPublisher publisher;
private final UserRepository repository;
@Transactional
public UserDto create(UserCreationRequest request) {
repository.findById(request.getId())
.ifPresent(user -> {
throw new RuntimeException(user.getId() + " is already exist.");
});
final User user = repository.save(User.from(request));
// 이벤트 발행
publisher.publish(new CustomTransactionalEvent());
return user.toDto();
}
}
UserService는 유저 생성 요청을 인자로 받아 DB에 유저 정보를 저장하고 CustomEventPublisher를 통해 CustomTransactionEvent를 발행한다. 발행한 커스텀 이벤트는 아래에 작성한 4가지의 이벤트 리스너에서 처리된다.
@Component
@Slf4j
public class CustomEventListener {
@TransactionalEventListener(
classes = { CustomTransactionalEvent.class },
phase = TransactionPhase.AFTER_COMMIT
)
public void onListenAfterCommit(CustomTransactionalEvent event) {
log.info("##### listening after commit event. event = {}", event);
}
@TransactionalEventListener(
classes = { CustomTransactionalEvent.class },
phase = TransactionPhase.BEFORE_COMMIT
)
public void onListenBeforeCommit(CustomTransactionalEvent event) {
log.info("##### listening before commit event. event = {}", event);
}
@TransactionalEventListener(
classes = { CustomTransactionalEvent.class },
phase = TransactionPhase.AFTER_COMPLETION
)
public void onListenAfterCompletion(CustomTransactionalEvent event) {
log.info("##### listening after completion event. event = {}", event);
}
@TransactionalEventListener(
classes = { CustomTransactionalEvent.class },
phase = TransactionPhase.AFTER_ROLLBACK
)
public void onListenAfterRollback(CustomTransactionalEvent event) {
log.info("##### listening after rollback event. event = {}", event);
}
}
4가지의 리스너는 UserService에서 발행된 CustomTransactionalEvent를 각각 다른 phase 옵션으로 처리한다. 실제로 설명과 같이 동작하는지 테스트해보자! @SpringBootTest를 이용하여 애플리케이션을 실행시킨 후 로그를 확인해보았다.
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {
@Autowired
private UserService service;
@Test
@DisplayName("Commit 이벤트 테스트")
void triggerCommitTransactionalTest() {
// given : 정상 commit 처리 되도록 작성
final UserCreationRequest request = new UserCreationRequest("user1",
"password1",
"홍길동");
// when
assertDoesNotThrow(() -> service.create(request));
}
@Test
@DisplayName("Rollback 이벤트 테스트")
void triggerRollbackTransactionalTest() {
// given : column 설정보다 긴 문자열로 일부러 Rollback 유발
final UserCreationRequest request = new UserCreationRequest("user2",
"password_is_tooooooo_long",
"홍길동");
// when
assertThrows(InvalidDataAccessResourceUsageException.class,
() -> service.create(request));
}
}
Test Result
Commit 이벤트 테스트 로그
- Spring : BEFORE_COMMIT 이벤트 리스너 동작.
- DB : Commit Initialize
- DB : Commit
- Spring : AFTER_COMMIT, AFTER_COMPLETION 이벤트 리스너 동작
Rollback 이벤트 테스트 로그
- Spring : BEFORE_COMMIT 이벤트 리스너 동작.
- DB : Commit Initialize
- DB : 오류 발생
- DB : Rollback
- Spring : AFTER_ROLLBACK, AFTER_COMPLETION 이벤트 리스너 동작