난이도 ⭐️
작성 날짜 2025.05.23
아이쿠 프로젝트에서는 포인트의 증감과 관련된 모든 메서드에서의 동일한 로직 호출로 발생하는 문제를 해결하기 위해 이를 이벤트 Pub/Sub으로 처리하고 있다.
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void pointChangeEvent(PointChangeEvent event) {
pointChangeFacade.makePointChange(event.getMemberId(), event.getSign(), event.getPointAmount(), event.getReason(), event.getReasonId());
}
포인트 증감을 발생시키는 이벤트를 Listener로 구독하여 포인트 증감 이벤트 발생 시 해야하는 로직이 담긴 Facade 클래스의 메서드를 실행한다.
테스트 코드는 다음과 같다.
handler.pointChangeEvent(
new PointChangeEvent(member.getId(), PointChangeType.PLUS, 100, PointChangeReason.EVENT, 11L)
);
assertThat(member.getPoint()).isEqualTo(100);
그런데 여기서 문제가 발생했다!

알고보니 @Async의 문제였다.
호출되는 리스너에 달려있는 Async 어노테이션을 주석처리하니 테스트가 통과하였다.
테스트는 분리된 스레드 작업이 끝나기를 기다려주지 않기 때문에 포인트 증가 로직이 반영되기 전에 테스트를 진행해버린다!
🤔 비동기는 테스트를 어떻게 해야될까?
테스트의 실행환경에서는 비동기가 필요 없다.
AsyncConfigurer를 통해 비동기를 처리하는 방식을 동기로 처리하도록 바꿔준다.
@Configuration
public class TestAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return new SyncTaskExecutor();
}
}
// 테스트 클래스
@Import(TestAsyncConfig.class)

잘 통과한다!
사실 1번 방법이 번거롭기도 해서, Async일 필요성을 다시 확인해보았다.
- 응답 시간을 줄이기 위한 목적
포인트 증가에서 비관적 락을 사용하기 때문에 약간의 딜레이가 있을 수 있지만, 성능에 크게 영향을 주지는 않는다.
로그를 남기는 부분도 마찬가지로 I/O 부하는 크게 없다.
- 실패해도 주 로직에는 영향이 없어야 할 때
이 부분이 핵심 내용인 것 같다!
다른 서비스에서는 SAGA 패턴을 통해 롤백할 정도로 트랜잭션을 강제로 붙이고 있다.
다시 말해서, 포인트의 증감 로직은 이벤트를 발생시키는 Pub 로직에서도 Sub 로직의 영향을 받아야 한다.
+) 추가로 Async를 사용했을 때 테스트와 디버깅 비용이 더 발생한다는 문제도 있다.
결론 : 현재 상황에선 Async가 필요하지 않다!
Async 때문에 발생한 테스트 문제 덕분에 Async의 필요성을 돌아보게 되는 계기가 되었다.
결론
기술을 쓰기 전에... 항상 의심하는 습관을 갖자!