날씨 API 구현
처음, 날씨 API를 통해 오늘의 날씨 정보를 가져오는 기능을 구현하기 위해 코드를 작성했습니다. 메서드는 두 가지 주요 기능을 가지고 있었습니다.
public WeatherResponse getWeather(final String date) {
final WeatherResponse[] responses = webClient.get()
.uri("/f-api/weather.json")
.retrieve()
.bodyToMono(WeatherResponse[].class)
.block(); // 날씨 정보 가져오기
return Arrays.stream(responses)
.filter(response -> response.date().equals(date))
.findFirst()
.orElseThrow(() -> new RuntimeException("날씨 정보를 찾을 수 없습니다."));
// 필터링 해서 날짜에 맞는 날씨 가져오기
날씨 데이터를 API로부터 가져오는 것과, 특정 날짜의 날씨를 필터링하여 반환하는 것이었습니다.
그러나, 이 두 기능이 하나의 메서드에 뭉쳐 있어 테스트할 때 어려움이 있었습니다. 단순히 필터링을 하는 건 구현이 가능했지만, 외부 데이터를 잘 가져오는지 테스트할 수 없고, 방법도 잘 모르는 상태였습니다.
리팩토링의 필요성
이런 문제를 해결하기 위해, 두 개의 메서드로 기능을 분리하기로 결정했습니다. 이제 하나의 메서드는 WebClient를 통해 날씨 데이터를 가져오고, 다른 메서드는 가져온 데이터에서 오늘의 날씨를 찾는 역할을 맡았습니다. 아래와 같은 코드로 리팩토링이 이루어졌습니다.
public List<WeatherFetchResponse> fetchWeatherData() {
return Objects.requireNonNull(webClient.get()
.uri("/f-api/weather.json")
.retrieve()
.bodyToFlux(WeatherFetchResponse.class)
.collectList()
.block());
}
public WeatherResponse getWeatherByDate(final String date, final List<WeatherFetchResponse> responses) {
return responses.stream()
.map(WeatherResponse::new)
.filter(response -> response.date().equals(date))
.findFirst()
.orElseThrow(() -> new ScheduleApplicationException(WEATHER_NOT_FOUND));
}
이제 각 메서드는 하나의 책임만 가지고 있기 때문에 더 명확하고 테스트하기 쉬워졌습니다. 또한, fetchWeatherData 메서드를 통해 날씨의 정보들을 가져오고, getWeatherByDate를 통해 필요한 데이터만 필터링하여 사용할 수 있었습니다.
테스트의 편리함
리팩토링 이후, 테스트 코드는 간단해졌습니다. getWeatherByDate 메서드가 리스트를 받아 필요한 정보를 뽑아낼 수 있도록 하여, 각 기능을 독립적으로 테스트할 수 있게 되었습니다. 이제는 날씨의 정보들을 임의로 지정해 주고, 날씨만 지정해 주면 되는 걸로 바뀌었습니다.
@Test
@DisplayName("오늘 날짜의 날씨를 가져온다.")
void getWeather_success() {
//given
final String date = "10-15";
final List<WeatherFetchResponse> responses = List.of(
new WeatherFetchResponse("10-14", "Sunny And Humid"),
new WeatherFetchResponse("10-15", "Heavy Rain Showers"),
new WeatherFetchResponse("10-16", "Windy and Warm")
);
//when
final WeatherResponse response = weatherClient.getWeatherByDate(date, responses);
//then
assertThat(response.weather()).isEqualTo("Heavy Rain Showers");
}
근본적인 문제
하지만, DTO가 서로 의존하게 되는 구조가 문제가 될 수 있다는 고민이 생겼습니다. WeatherFetchResponse에서 WeatherResponse로 변환할 때, WeatherResponse의 생성자 파라미터에 WeatherFetchResponse를 추가해야 했기 때문입니다. 이는 API 스펙이 변경될 경우 관련 로직을 통째로 바꿔야 하는 위험성을 내포하고 있었습니다.
이에 따라 튜터님에 가서 조언을 얻어왔고 해결책을 찾아 적용해 보려고 합니다.
문제점 1: 메서드 분리의 위험성
테스트를 편리하게 하려고 메서드를 분리하는 것이 항상 좋은 선택은 아닙니다. 만약 다른 곳에서 이 메서드를 재사용하려는 목적이 아니라면, public으로 분리된 메서드가 오히려 문제를 야기할 수 있습니다.
문제점 2: 기능의 응집성
각 메서드가 하나의 기능을 수행하도록 설계하는 것은 좋지만, ScheduleService.create 관점에서 보면 두 기능이 사실상 하나의 작업으로 묶일 수 있습니다. 따라서 기능의 응집성을 고려해야 합니다.
해결방법 1: WebClient를 Stubbing
기능을 다시 하나의 메서드로 통합하여 외부 클라이언트를 Stubbing 하는 방식으로 테스트를 개선했습니다. 왜냐하면 외부 클라이언트의 값이 바뀌어도, 테스트는 언제든지 성공해야 하기 때문입니다.
원래의 코드(Array → List)
public WeatherResponse getWeather(final String date) {
final List<WeatherResponse> responses = webClient.get()
.uri("/f-api/weather.json")
.retrieve()
.bodyToFlux(WeatherResponse.class)
.collectList()
.block();
return Objects.requireNonNull(responses).stream()
.filter(response -> response.date().equals(date))
.findFirst()
.orElseThrow(() -> new ScheduleApplicationException(WEATHER_NOT_FOUND));
}
@ExtendWith(MockitoExtension.class)
class WeatherClientTest {
@Mock
private WebClient webClient;
@Mock
private RequestHeadersUriSpec requestHeadersUriSpec;
@Mock
private RequestHeadersSpec requestHeadersSpec;
@Mock
private ResponseSpec responseSpec;
@InjectMocks
private WeatherClient weatherClient;
@Test
@DisplayName("날씨를 가져온다.")
void getWeather_success() {
// given
final String date = "10-15";
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri("/f-api/weather.json")).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToFlux(WeatherResponse.class))
.thenReturn(Flux.fromIterable(List.of(
new WeatherResponse("10-14", "Sunny"),
new WeatherResponse("10-15", "Humid")
)));
// when
final WeatherResponse response = weatherClient.getWeather(date);
// then
assertThat(response.weather()).isEqualTo("Humid");
}
}
WebClient를 Stubbing 한 코드입니다.
해결방법 2: Test Double 활용
Test Double을 사용하여, 추상화를 통해 가짜 객체를 사용하여 프레임워크의 도움 없이 기본적인 언어 스펙으로 구현할 수 있도록 했습니다. 이 부분은 아직 잘 알지 못해서 학습한 후 수정해 놓도록 하겠습니다.
'WEB' 카테고리의 다른 글
회원가입 후 JWT 응답 제거 (1) | 2024.10.30 |
---|---|
3N+1 문제와 프록시 강제 초기화 해결 (0) | 2024.10.23 |
JPA Update 실패 해결기 (0) | 2024.10.10 |
소프트 딜리트란? (1) | 2024.09.27 |
Spring Data JPA로 된 코드를 JDBC로 다시 짜보기 (1) | 2024.09.25 |
MDC를 이용한 로깅 도입기 (0) | 2024.09.23 |
@SpringBootTest vs @Mock (0) | 2024.09.12 |
스프링부트의 Tomcat과 Thread Pool (0) | 2024.09.10 |