SWM에서 프로젝트를 진행하는 과정에서 카카오 도서 검색 API를 사용하여 도서를 검색해 오는 기능을 구현하고, 테스트 코드를 작성하는 과정에서 아래와 같은 이슈가 나타났다.
Unable to use auto-configured MockRestServiceServer since MockServerRestTemplateCustomizer has not been bound to a RestTemplate
java.lang.IllegalStateException: Unable to use auto-configured MockRestServiceServer since MockServerRestTemplateCustomizer has not been bound to a RestTemplate
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerAutoConfiguration$DeferredRequestExpectationManager.getDelegate(MockRestServiceServerAutoConfiguration.java:116)
at org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerAutoConfiguration$DeferredRequestExpectationManager.expectRequest(MockRestServiceServerAutoConfiguration.java:88)
at org.springframework.test.web.client.MockRestServiceServer.expect(MockRestServiceServer.java:107)
at org.springframework.test.web.client.MockRestServiceServer.expect(MockRestServiceServer.java:92)
이슈 발생 과정
KakaoBookSearchApiService에서는 RestTemplate을 사용하여 카카오 도서 검색 API를 호출하도록 구현하였다. 코드는 아래와 같다.
@Service
public class KakaoBookSearchApiService implements PlatformBookSearchApiService {
@Value("${kakao.api.key}")
private String API_KEY;
@Autowired
private final RestTemplate restTemplate;
private static final String API_URL_WITH_KEYWORD = "https://dapi.kakao.com/v3/search/book?query=%s&sort=%s&page=%d";
private static final String API_URL_WITH_ISBN = "https://dapi.kakao.com/v3/search/book?target=isbn&query=%s&sort=%s&page=%d";
private static final String REQUEST_NEXT_URL = "/books/search?keyword=%s&sort=%s&page=%d";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String KAKAO_AUTHORIZATION_HEADER_PREFIX = "KakaoAK ";
@Override
public BookSearchResultResponse searchWithKeyword(String keyword, String sort, int page) {
return searchBookFromKakaoApi(API_URL_WITH_KEYWORD, keyword, sort, page);
}
@Override
public BookSearchResultResponse searchWithISBN(String isbn) {
return searchBookFromKakaoApi(API_URL_WITH_ISBN, isbn, "accuracy", 1);
}
private BookSearchResultResponse searchBookFromKakaoApi(String baseUrl, String searchKeyword, String sort, int page) {
HttpHeaders headers = new HttpHeaders();
headers.add(AUTHORIZATION_HEADER, KAKAO_AUTHORIZATION_HEADER_PREFIX + API_KEY);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<HttpHeaders> httpEntity = new HttpEntity<>(headers);
String requestUri = String.format(baseUrl, searchKeyword, sort, page);
ResponseEntity<KakaoBookSearchResponse> response = restTemplate.exchange(requestUri,
HttpMethod.GET, httpEntity, KakaoBookSearchResponse.class);
...
}
}
위 Service 코드는 다른 Service 코드와 다르게 외부 API를 사용하기 때문에, @ExtendWith(MockitoExtension.class)만을 사용해서는 단위 테스트를 할 수 없다.
외부 API를 테스트 하기 위해서는 아래와 같이 @RestClientTest를 사용할 수 있다.
- @RestClientTest는 외부 API를 사용할 때, Rest 클라이언트(카카오 API)를 쉽게 mock으로 대체하여 테스트를 할 수 있다.
- MockRestServiceServer라는 임시 서버를 Bean으로 생성해 주기 때문에 주입받아 사용하여, 원하는 형태의 요청이 오면 지정된 값으로 응답을 줄 수 있다.
- @SpringBootTest와 달리 최소한의 Context만 사용하여 테스트를 진행한다.
@RestClientTest(value = KakaoBookSearchApiService.class)
class KakaoBookSearchApiServiceTest {
private static final String KAKAO_BOOK_SEARCH_API_URL = "https://dapi.kakao.com/v3/search/book";
private static final String JSON_FILE_PATH = "src/test/resources/kakaoapi/";
private static final String EXIST_ISBN = "9788980782970";
private static final String NOT_EXIST_ISBN = "978898078297011";
@Autowired
private KakaoBookSearchApiService kakaoBookSearchApiService;
@Autowired
private MockRestServiceServer mockServer;
@DisplayName("카카오 도서 검색의 결과는 최대 10권이며, 10권이 넘어가면 expectNextRequestUrl이 존재한다.")
@Test
void searchFromKakaoApiAndResultMoreThan10() throws IOException {
// given
String expectNextRequestUrl = "/books/search?keyword=docker&sort=accuracy&page=2";
String expectResult = readJsonFile("resultMoreThan10FromKeyword.json");
mockServer
.expect(requestTo(KAKAO_BOOK_SEARCH_API_URL + "?query=docker&sort=accuracy&page=1"))
.andRespond(withSuccess(expectResult, MediaType.APPLICATION_JSON));
// when
BookSearchResultResponse bookSearchResultResponse = kakaoBookSearchApiService
.searchWithKeyword("docker", "accuracy", 1);
// then
assertAll(
() -> assertThat(bookSearchResultResponse.getBookSearchResult().size()).isEqualTo(10),
() -> assertThat(bookSearchResultResponse.getNextRequestUrl()).isEqualTo(expectNextRequestUrl)
);
}
private String readJsonFile(String fileName) throws IOException {
return new String(Files.readAllBytes(Paths.get(JSON_FILE_PATH + fileName)));
}
}
- @RestClientTest를 선언하고, 테스트하고 싶은 서비스 코드와 MockRestServiceServer를 주입받는다.
- mockServer.expect /. andRespond를 통해 어떤 요청이 들어올 때, 어떤 응답을 반환할지에 대해서 결정할 수 있다.
@RestClientTest와 MockRestServiceServer를 통해, 저의 목적대로 외부 API 클라이언트를 Mocking 하고 온전히 제가 작성한 kakaoBookSearchApiService에 대해서 단위 테스트를 진행할 수 있었다.
이슈 발생
하지만 아래와 같은 오류가 발생했다. 해당 오류는 MockServerRestTemplateCustomizer가 RestTemplate를 바인딩하지 못해 MockServiceServer를 자동 구성할 수 없어 발생한 오류이다.
이슈 해결 과정
MockServiceServer는 RestTemplate를 바인딩하지 못하니 직접 바인딩을 해줘야 한다.
아래와 같이 MockRestServiceServer.createServer를 통해서 RestTemplate을 직접 바인딩하여, 오류를 해결할 수 있다.
@Autowired
private RestTemplateBuilder restTemplateBuilder;
private MockRestServiceServer mockServer;
@BeforeEach
public void setup() {
RestTemplate restTemplate = restTemplateBuilder.build();
mockServer = MockRestServiceServer.createServer(restTemplate);
}
추가 기록 ( + RestTemplateBuilder 사용하기)
이슈와는 조금 다른 문제이지만, 기존 코드에서 개선한 부분이 있어서 추가적으로 기록하고자 한다.
기존 코드에서는 단순히 RestTemplate을 아래와 같이 빈 등록을 하여 사용하였다.
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
하지만 위의 방법보다는 RestTemplateBuilder를 빈으로 등록하고, RestTemplateBuilder를 주입받아 RestTemplate을 생성하는 것이 더 권장하는 방법이라고 한다.
@Bean
public RestTemplateBuilder restTemplateBuilder() {
RestTemplateBuilder builder = new RestTemplateBuilder();
builder.setConnectTimeout(Duration.ofMillis(15000));
builder.setReadTimeout(Duration.ofMillis(15000));
return builder;
}
RestTemplateBuilder는 RestTemplate을 생성하는 빌더 역할을 하며, 인증 정보, 요청/응답 로깅, 커넥션 타임아웃 등을 다양한 구성 옵션을 쉽게 적용할 수 있다. RestTemplate을 직접 주입받아 사용하는 것보다 RestTemplate의 설정을 더 유연하고 일관성 있게 제공할 수 있다.
Service 코드에서는 아래와 같이 RestTemplate을 주입받을 수 있다.
private final RestTemplate restTemplate;
public KakaoBookSearchApiService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
'개발 > Spring' 카테고리의 다른 글
[Spring] @Async를 사용하여 비동기 처리 (+ 쓰기, 읽기 서비스 분리) (0) | 2023.08.12 |
---|---|
[Spring] JPA Fetch Join 사용시, MultipleBagFetchException 발생 (3) | 2023.08.03 |
[Spring] Spring Rest Docs로 API 문서 자동화하기 (0) | 2023.07.03 |
[Spring] Spring Boot 3 사용해보자! (0) | 2023.06.10 |
JWT (2) 스프링에서 JWT 사용하기 (0) | 2023.05.31 |