배경
최근 프로젝트에서 Slack 알림 기능을 WebClient로 구현하고, 그에 대한 단위 테스트를 작성했다.
Slack API를 직접 호출하지 않기 위해 MockWebServer를 활용했고,
비즈니스 로직에서는 Spring의 @Value를 이용해 외부 설정값을 주입했다.
@Value("${slack.enabled:true}")
private boolean slackEnabled;
그러나 테스트를 실행하자 문제가 발생했다.
✅ 테스트는 통과하지 못하고 무한히 실행 중인 상태로 멈춰 있었다.
✅ GitHub Actions에서는 6시간 제한 초과로 build가 자동 취소되었다.
✅ 로그에는 특별한 에러도 없고, WebClient 요청도 정상처럼 보였다.
도대체 왜 테스트가 끝나지 않았던 걸까?
원인
- SlackNotificationService는 비동기 방식(subscribe())으로 WebClient 호출
- MockWebServer.takeRequest()가 응답을 받지 못하고 무한 대기 상태 에 빠질 수 있다. → 테스트가 끝나지 않음
- 게다가 @Value가 제대로 주입되지 않음 → 조건문 우회도 실패
조건문 우회 로직 :
if (!slackEnabled) {
return;
}
즉 다시 정리하자면,
- 테스트에서 WebClient.subscribe() 비동기 방식으로 슬랙에 메시지를 전송
- 요청은 백그라운드 쓰레드에서 실행됨.
→ 이 줄에서 요청이 올 때까지 기다림var recordedRequest = mockWebServer.takeRequest(); - 하지만 테스트 코드에서는:
- 그런데 비동기 요청이 끝나기 전에, 테스트 메인 쓰레드는 이미 takeRequest()에 도달해서 요청을 기다리는 중
- 비동기 요청이 끝나지 않거나 실패하면→ 테스트는 절대 종료되지 않음
- → takeRequest()는 요청을 받지 못해서 계속 대기
- GitHub Actions는 워크플로 실행 시간이 6시간을 초과하면 자동으로 테스트를 취소(cancelled)
The job has exceeded the maximum execution time of 6h0m0s - → 그래서 다음 메시지가 떴던 것:
실패한 시도들
❌ application.yml 설정 수정
- test/resources/application.yml에 slack.enabled: true를 명시
- @ActiveProfiles("test") 도입
- → 문제 해결되지 않음
❌ @SpringBootTest 도입
- Spring이 @Value 주입을 포함한 전체 빈 구성 및 컨텍스트를 로딩해줌
- 하지만 Spring이 생성한 WebClient는 여전히 application.yml의 Slack base URL을 사용
- 별도로 WebClient 설정을 override하지 않았기 때문에,
- 테스트에서 사용 중인 MockWebServer와는 연결되지 않았고, 요청은 Slack으로 날아감
❌ System.setProperty() 시도
- WebClient의 baseUrl을 시스템 프로퍼티로 override하려 했다.
- 그러나 WebClient Bean 생성시 이걸 반영하지 않았어서(즉 override할게 없었다) 또 이걸 소스코드에 수정하려니 코드가 복잡해진다고 느꼈다.
✅ 최종 해결: 직접 객체 생성 + setter 주입 + block()
- WebClient와 서비스 객체를 테스트 코드에서 직접 생성
- 설정값은 setter로 수동 주입
- subscribe() 대신 block()으로 테스트용 동기 메서드 분리
slackNotificationService = new SlackNotificationService(webClient);
slackNotificationService.setSlackEnabled(true); // ✅ 수동 주입
public void sendSummaryToUserSync(...) {
...
slackWebClient.post()
...
.block(); // ✅ 테스트용
}
비슷한 문제: SlackRequestVerifier
@Value를 쓰는 또 다른 클래스에서도 동일한 문제가 있었지만,
이 역시 setter 방식으로 테스트에서 직접 주입하여 해결했다.
slackRequestVerifier = new SlackRequestVerifier();
slackRequestVerifier.setSigningSecret("dummy_secret");
slackRequestVerifier.setVerifySignature(true);
언제 setter 방식이 좋은가?
크게 세 가지이다. 다른 해결방법이 좀 지저분하다 싶으면(webclient와의 연동이 지저분해진다던지...) setter를 쓰는 것이 괜찮은 듯하다.
- @SpringBootTest가 너무 무거운 경우
- @Value나 @Autowired를 사용하는 컴포넌트를 단위 테스트하고 싶을 때
- Spring 컨텍스트 없이 빠르게 검증하고 싶을 때
테스트에서 block() vs subscribe()
상황 block() subscribe()
| 실패 요청이 왔을 때 | 테스트가 바로 실패함 | 테스트는 몰라서 통과한 것처럼 보이기도 함 |
| 요청이 너무 느릴 때 | 테스트가 timeout으로 실패 | 테스트는 이미 끝났는데 요청은 백그라운드에서 계속 처리 중일 수 있음 |
| takeRequest() 대기 중 | 요청이 오면 바로 깨짐 | 요청이 안 오면 무한 대기 (성공 여부 보장 X) |
즉 block()이라면 명확하게 실패, 성공을 알려줄 수 있다. 하지만 subscribe()는 무한 대기할 수 있는 문제가 생기는 것이다.
마무리
이번 경험은 나에게 Spring 테스트 환경에서 설정 주입이 어떻게 동작하는지,
그리고 WebClient 테스트 시 비동기 처리의 함정이 무엇인지를 깨닫게 했다. WebClient, @Value, Mock 서버 간의 연동 흐름이 정확히 일치하지 않으면 테스트는 hang되거나 실패한다는 점을 겪었다.
'프로젝트 > 디버깅 경험' 카테고리의 다른 글
| AWS Docker와 JMeter 연동 문제(빈 등록 안 됨) 디버깅 (0) | 2025.05.29 |
|---|