본문 바로가기

Language & Framework/삽질기록

삽질기록 (21) 비동기 이벤트 테스트하기

 

모임의 주최자가 예약을 승인하거나 거절하면 예약 참여를 신청한 회원에게 이메일을 전송한다.

이 때 메일은 외부 서비스이기 때문에 만약 해당 서비스까지 Transaction이 이어진다면 불필요하게 처리 시간이 길어진다는 문제가 있다.

또한 메일 전송은 주요 관심사가 아님에도 메일 전송이 실패하면 요청에 대한 수락/거절도 불가능해진다.

 

메일 전송이 실패하든 성공하든 그게 모임에 대한 수락/거절의 결과에 영향을 끼치면 안되는 거 아닐까?

또한 어차피 무조건 모임에 대한 수락/거절만 성공했을 때 이메일의 결과는 "내 알 바?"라면 굳이 해당 처리가 끝날 때까지 유저가 기다려야할 이유도 없다.

 

그래서 해당 부분을 비동기 이벤트로 구현했고, 위와 같은 형태로 실행된다.

 

파라미터로 flag를 사용한 게 마음에 걸리기는 하는데.. 어떻게 해야 좋을지 아직 잘 모르겠다 

 

 

 

 

 

ReservationConfirmService에서 결과에 대한 이벤트를 발행하면 Requires_new 옵션으로 인해 새로운 트랜잭션이 생성되고 내가 사전에 준비한 AsyncThreadPool에 있는 쓰레드를 이용해 해당 요청을 처리하게 되므로 기존의 트랜잭션은 종료되고 유저는 MailService의 성공, 진행 여부와 아무 관계 없이 응답을 받는다.

phase는 이벤트를 어떻게 바인딩할 것인지에 대한 설정으로, AFTER_COMMIT은 이전 트랜잭션이 성공적으로 commit되었을 때만 작동하게 한다. 기본값이라서 굳이 작성하지 않아도 될 것 같다.

 

이렇게 해피하게 호호 비동기 최고~ 이러고 끝나면 좋겠지만, 비동기의 단점은 시스템이 복잡해질수록 로직을 파악하기 위한 시간이 오래 걸리고, 디버깅하기 힘들고, 테스트코드 작성도 번거로워진다.

 

다행히 우리 프로젝트에 그렇게 복잡한 시스템이 없으므로 큰 문제는 아니고, 비동기가 잘 작동하고 있는지 확인하고 이후에는 메일 서비스에 대한 테스트만 따로 진행하면 된다.

 

이를 위해 필요한 준비물은 @RecordApplicationEvents와 ApplicationEvents다.

잘 모르는 어노테이션, 클래스, 메서드에 대해 궁금하면 소스코드를 읽어보자. 소스코드님은 모든 것을 알고 계신다.

 

근데 종종 너무 방대해서 언제 다 읽지 싶은 것들도 있다.

 

 

 

 

@RecordApplicationEvnets는 클래스 레벨 어노테이션이고 어쩌고 저쩌고..

단일 테스트에 대한 모든 application event를 기록하는 것을 지시하는 역할을 한다고 한다.

또한 ApplicationEvents API를 통해 기록에 접근할 수 있다.

 

 

 

 

어플리케이션 이벤트는 모든 어플리케이션의 encapsulates한다. (싱글 테스트 메서드가 실행되는 동안 발생하는 ~)
네 테스트에서 ApplicationEvents를 사용하고 싶으면 따라하셈

1. 너의 테스트 클래스에 @RecordApplicationEvents 주석이 붙어있는지 확인해라.
2. ApplicationEventsTestExecutionListener가 등록되었는지 확인해라.
근데 이건 그냥 기본적으로 등록된다. 기본 리스너를 포함하지 않는 TestExecutionListeners를 통해 커스텀 설정했을 때만 수동적으로 등록하면 된다. (무슨 말인지 잘 모르겠음)
3. 주석을 달아라 ApplicationEvents에 @Autowired를, 그리고 사용해라 이놈의 인스턴스를 너의 테스트에서.
4. JUnit Jupiter를 사용할 때, 넌 optional하게 선언할 수 있다 ApplicationEvents의 파라미터를 (테스트 or 라이프사이클 메서드에서) @Autowired를 필드에다가 갖다 붙이는 것 대신에 (Jupiter를 안 써서 무슨 말인지 모르겠음)

 

아주 친절한 소스코드 주석과 불친절한 번역이 조화를 이룬다.

2번 4번은 무슨 말인지 잘 모르겠지만, 1번 3번만 봐도 뭐하라는 말인지 알 수 있다.

 

그리고 아래 stream 메서드에 보면 싱글 테스트가 진행되는 동안 발생한 모든 이벤트를 Stream으로 반환한다고 한다.

이제 뭐해야할지 알겠죠?

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Transactional
@SpringBootTest
@RecordApplicationEvents
public class ReservationIntegrationTest {
    @Autowired
    ApplicationEvents applicationEvents;
 
    @Test
    void mailEventListenerTest() throws Exception {
        //given
        ReflectionTestUtils.setField(reservation, "reservationState", ReservationState.PAYMENT_SUCCESS);
 
        PatchReservationDto request = new PatchReservationDto(Boolean.FALSE.toString());
        String json = objectMapper.writeValueAsString(request);
 
        //when
        ResultActions perform = mockMvc.perform(patch("/meetings/{meetingId}/reservations/{reservationId}", meeting.getId(), reservation.getId())
                .contentType(APPLICATION_JSON)
                .content(json)
                .header(JWT_HEADER, hostToken));
 
        //then
        long result = applicationEvents.stream(ReservationConfirmedEvent.class).count();
        Assertions.assertThat(result).isEqualTo(1L);
    }
}
cs

 

다른 테스트 코드도 많고, 필드랑 어노테이션도 많아서 죄다 지우고 필요한 것만 남겼다.

이렇게 하면 통합 테스트를 진행하며 발생한 이벤트 수를 카운트할 수 있다.

 

 

 

고민되는 점

비동기로 진행하는 것까지는 좋으나, 이벤트 실패 시 어떻게 처리하는 게 맞는지 모르겠다.

분산환경에서는 DB에 저장해놓고 N번까지 반복적으로 수행하거나 하는 방식으로 처리하기도 하고 여러가지 방법이 있는 것 같은데.. 단순히 단일 프로세스에서 이벤트 발행 시 어떻게 해야하는 거지?

 

지금 생각나는 방법으로는 try catch를 이용해서 재시도하게 하고 N번까지 실패하면 로그에 저장하는 게 최선인 것 같은데, 좀 더 생각해봐야할 것 같다.