Awaitility란 무엇인가?
Awaitility는 단순히 "조건을 만족할 때까지 기다리는" 라이브러리입니다.
자바의 while 루프나 Thread.sleep을 사용하지 않고,
안정적으로 polling 하며 조건이 만족될 때까지 기다립니다.
비동기 테스트 도입 계기
Spring에서 @Async 와 같이 비동기적으로 수행되는 코드를 테스트할 때 가장 어려운 점은 "테스트 메소드가 먼저 종료되어 비동기 로직이 미처 수행되지 못하는 것"입니다. Thread.sleep() 으로 기다리는 방식 보단 적절한 시간을 두고 polling 으로 완료가 되면 종료되는게 더 효율적이라 판단하였습니다.
기본 사용법
await().atMost(5, SECONDS)
.until(() -> someService.getState().equals("DONE"));
5초 동안 기다리면서 state 가 DONE 이 되는지 확인하는 코드 입니다.
다음과 같은 요구사항에서 테스트를 해보도록 하겠습니다.
1. 게시물이 생성된다.
2. 생성된 게시물에 대한 정보로 유저에게 알림을 전송한다.
3. 알림 요청이 성공적으로 처리되면 알림 이력 테이블에 해당 정보를 저장한다.
4. 알림 요청은 시간이 수초 이상 소요되므로 비동기로 처리를 한다.
작성 코드
1. 게시물 생성 후 알림 처리를 위한 코드
- 게시물 생성 코드와 알림 전송 코드를 event 방식으로 분리하여 서비스간의 의존성을 줄였습니다. 또한 비동기 처리를 통해 서로 다른 스레드에서 수행되도록 처리하였습니다.
- 게시물 생성 서비스에서 생성 메소드
@Transactional
public Post createPostSuccess(PostCreateRequest postCreateRequest) {
Post post = postCreateRequest.toEntity();
Post saved = postJpaRepository.save(post);
eventPublisher.publishCreatedSuccessEvent(saved);
return saved;
}
- 이벤트 발행 서비스, 이벤트를 발행한다.
@Component
@RequiredArgsConstructor
public class PostEventPublisher {
private final ApplicationEventPublisher publisher;
public void publishCreatedSuccessEvent(Post post) {
publisher.publishEvent(new PostCreatedEventSuccess(post));
}
}
- 이벤트 수신 메소드에서는 트랜잭션이 커밋이 된 후에 수행되도록 처리하여 정상적으로 post 가 생성된 후에 알림이 전송되도록 되어 있고 알림이 전송된 이후에는 알림 이력을 저장합니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class PostEventHandler {
private final NotificationService notificationService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void handlePostCreatedAfterCommit(PostCreatedEventSuccess event) throws InterruptedException {
var post = event.post();
log.info("TransactionalEventListener phase = TransactionPhase.AFTER_COMMIT executed");
notificationService.send(Notification.from(post));
}
}
위와 같은 코드처럼 구성되어 있고 알림이 정상적으로 요청이 되었다면 알림 이력이 생성되는걸 확인을 하면 정상적으로 알림이 요청되었는지 검증할 수 있습니다.
실제 send 하는 부분은 테스트에서는 외부 통신으로 동작하지 않게 사용하도록 다음과 같은 처리를 해줬습니다.
NotificationService 의 send 메소드 구성
@Service
@Builder
@RequiredArgsConstructor
public class NotificationService {
private final NotificationJpaRepository notificationJpaRepository;
private final SendNotificationInterface emailSend;
@Transactional
public Notification send(Notification notification){
try {
emailSend.send();
} catch (Exception e) {
throw new RuntimeException(e);
}
return notificationJpaRepository.save(notification);
}
}
emailSend 객체는 interface 를 상속받은 구체적인 구현체 입니다.
따라서 테스트 코드에서는 실제 전송이 아닌 테스트 구현체를 통해 전송이 되는 걸 가정하고 작업을 진행할 수 있게 하였습니다.
public interface SendNotificationInterface {
void send() throws Exception;
}
2. Awaitility를 통한 비동기 테스트
- 1번 코드에서 살펴본것과 같이 이메일 전송에 대한 구현체를 별도로 만들어서 테스트 시점에 실제 이메일 전송이 아닌 이메일 전송 처리를 간단하게 구현할 수 있게 되었습니다.
- EmailSendTest 를 통해 email 전송 요청에 1초 정도 소요 된다 가정하였습니다.
@TestConfiguration
@Slf4j
public class EmailSendTest implements SendNotificationInterface {
@Override
public void send() throws InterruptedException {
log.info("test email send trigger");
Thread.sleep(1000);
}
}
- 실제 email 전송 객체가 아닌 EmailSendTest 주입
@TestConfiguration
public class TestConfig {
@Bean
public NotificationService notificationService(NotificationJpaRepository notificationJpaRepository){
return NotificationService.builder()
.notificationJpaRepository(notificationJpaRepository)
.emailSend(new EmailSendTest())
.build();
}
}
- 2초 정도 기다린 후에 notification table 에 데이터가 insert 되었는지 확인하는 테스트
@Test
@DisplayName("게시글 생성이 정상적으로 된 후 notification 은 정상적으로 1번만 발송된다.")
void createSuccessAndSendNotification() {
PostCreateRequest postCreateRequest = PostCreateRequest.builder()
.title("title")
.content("content")
.authorId("1L").build();
postService.createPostSuccess(postCreateRequest);
await().atMost(Duration.ofSeconds(2))
.pollInterval(Duration.ofMillis(200)) // 200ms마다 확인이며 설정하지 않으면 100ms 마다 수행
.untilAsserted(() -> assertThat(notificationService.findAll()).hasSize(1));
}
2초 동안 findAll()를 실행하며 size 가 1이 되는지 확인하는 코드 입니다.
untilAsserted 를 사용하면 assert 를 사용하여 검증을 할 수 있고
until 을 사용하면 해당 조건이 충족되면 끝나는 조건입니다.
사용하려는 조건에 따라 메소드를 선택하여 사용하면 됩니다.
정리
- @Async, @TransactionalEventListener와 같은 Spring 비동기 흐름에서는 테스트 종료 전에 처리가 안 되는 경우가 많습니다.
- Awaitility의 await().untilAsserted() 를 사용하면 지정 시간 내에 polling 방식으로 조건 만족 여부를 확인할 수 있습니다.
- 테스트용 구현체 (예: EmailSendTest)를 잘 활용하면 외부 의존성 없이 테스트가 가능합니다.
참고
Task Execution and Scheduling :: Spring Framework
All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time
docs.spring.io
다음 포스팅에서는 Event 및 TransactionalEventListener 에 대해 다뤄보도록 하겠습니다.
전체 코드
blog-code/spring_event_listener at main · jjjwodls/blog-code
Contribute to jjjwodls/blog-code development by creating an account on GitHub.
github.com
'Java & Spring' 카테고리의 다른 글
@EventListener, @TransactionalEventListener 설에 따른 이벤트 동작 방식 (3) | 2025.08.13 |
---|---|
Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계 (1) | 2025.07.26 |
Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지 (3) | 2025.07.24 |
Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화 (2) | 2025.07.22 |
Spring Rest Docs 적용 2 - Swagger 연동 (1) | 2025.07.21 |