이전 글에서 작성한 WebClient 관련 리팩토링을 진행한 뒤에 develop 브랜치로 머지하고 dev 환경에 배포를 마쳤습니다.
https://velog.io/@da_na/WebClient-WebClient-사용해서-외부-API-호출하기
그리고 dev 환경에서 실제로 스크랩을 추가한 뒤에 스크랩이 잘 조회되는지 확인해보았습니다.
그러나,,, 아래와 같이 1개의 스크랩을 추가했는데, 아래의 사진에서 빨간색 박스로 되어 있는 것처럼 정상적인 스크랩 1개와 비정상적인 스크랩(아무것도 나오지 않는 스크랩)이 2개가 추가되었습니다.
그래서, dev DB를 확인해봤는데 2개의 스크랩이 추가되었습니다.
그래서 WebClient 리팩토링 코드에서 어느 부분이 틀렸는지 앞으로 이러한 상황을 대비하기 위해서는 어떻게 해야할지를 이야기해보겠습니다!!
@Transactional
public JSONObject crawlingItem(String pageUrl) throws ParseException {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("url", pageUrl);
WebClient webClient = WebClient.builder().baseUrl(crawlingApiEndPoint).build();
Map<String, Object> response = webClient.post()
.bodyValue(bodyMap)
.retrieve()
.bodyToMono(Map.class)
.block();
JSONParser jsonParser = new JSONParser();
Object obj = jsonParser.parse(response.get("body").toString());
JSONObject jsonObject = (JSONObject) obj;
return jsonObject;
}
@Transactional
public Scrap saveScraps(User user, String pageUrl) throws ParseException {
JSONObject crawlingResponse = webClientService.crawlingItem(pageUrl);
String type = "";
try {
type = crawlingResponse.get("type").toString();
} catch (NullPointerException e) {
throw new NotFoundException(ErrorCode.NOT_EXISTS);
}
switch (type) {
case "video":
return videoService.saveVideo(crawlingResponse, user, pageUrl);
case "article":
return articleService.saveArticle(crawlingResponse, user, pageUrl);
case "product":
return productService.saveProduct(crawlingResponse, user, pageUrl);
}
return otherService.saveOther(crawlingResponse, user, pageUrl);
}
@Transactional
public WebClientBodyResponse crawlingItem(String crawlingApiEndPoint, String pageUrl) {
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("url", pageUrl);
WebClient webClient = WebClient.builder().baseUrl(crawlingApiEndPoint).build();
try {
WebClientResponse webClientResponse = webClient.post()
.bodyValue(bodyMap)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("4xx");
})
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
throw new RuntimeException("5xx");
})
.bodyToMono(WebClientResponse.class)
.block();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(
webClientResponse != null ? webClientResponse.getBody() : null,
WebClientBodyResponse.class);
} catch (Exception e) {
return null;
}
}
@Transactional
public Scrap saveScraps(User user, String pageUrl) throws ParseException {
WebClientBodyResponse crawlingResponse = webClientService.crawlingItem(crawlingApiEndPoint, pageUrl);
return Optional.ofNullable(crawlingResponse)
.map(response -> {
String type = response.getType();
switch (type) {
case "video":
return videoService.saveVideo(response, user, pageUrl);
case "article":
return articleService.saveArticle(response, user, pageUrl);
case "product":
return productService.saveProduct(response, user, pageUrl);
case "place":
return placeService.savePlace(response, user, pageUrl);
default:
return otherService.saveOther(response, user, pageUrl);
}
})
.orElse(otherService.saveOther(new WebClientBodyResponse(), user, pageUrl));
}
.orElse(otherService.saveOther(new WebClientBodyResponse(), user, pageUrl));
이었습니다.Java Optional 클래스는 Java 8에서 추가되었으며 NullpointerException 문제를 해결할 수 있는 방법을 제공합니다.
Optional 클래스는 Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스(Wrapper class)입니다.
따라서 Optional 인스턴스는 모든 타입의 참조 변수를 저장할 수 있습니다.
이러한 Optional 객체를 사용하면 예상치 못한 NullPointerException 예외를 제공되는 메소드로 간단히 회피할 수 있습니다.
즉, 복잡한 조건문 없이도 널(null) 값으로 인해 발생하는 예외를 처리할 수 있게 됩니다.
만약 어떤 데이터가 null이 올 수도 있고 아닐 수도 있는 경우에는 Optional.ofNullbale로 생성할 수 있습니다. 그리고 이후에 orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우라도 안전하게 값을 가져올 수 있습니다.
참고 자료 1 : https://engkimbs.tistory.com/646
참고 자료 2 : https://cfdf.tistory.com/34
참고 자료 3 : http://www.tcpschool.com/java/java_stream_optional
참고 자료 4 : https://mangkyu.tistory.com/70
.orElse(otherService.saveOther(new WebClientBodyResponse(), user, pageUrl))
코드가 WebClient가 응답한 값인 crawlingResponse가 null이 아닌 경우에도 실행되므로 articleService.saveArticle(response, user, pageUrl)
과 otherService.saveOther(new WebClientBodyResponse(), user, pageUrl)
총 2번 스크랩이 저장됩니다.return Optional.ofNullable(crawlingResponse)
.map(response -> {
String type = response.getType();
switch (type) {
case "video":
return videoService.saveVideo(response, user, pageUrl);
case "article":
return articleService.saveArticle(response, user, pageUrl);
case "product":
return productService.saveProduct(response, user, pageUrl);
case "place":
return placeService.savePlace(response, user, pageUrl);
default:
return otherService.saveOther(response, user, pageUrl);
}
})
.orElse(otherService.saveOther(new WebClientBodyResponse(), user, pageUrl));
return Optional.ofNullable(crawlingResponse)
.map(response -> {
String type = response.getType();
switch (type) {
case "video":
return videoService.saveVideo(response, user, pageUrl);
case "article":
return articleService.saveArticle(response, user, pageUrl);
case "product":
return productService.saveProduct(response, user, pageUrl);
case "place":
return placeService.savePlace(response, user, pageUrl);
default:
return otherService.saveOther(response, user, pageUrl);
}
})
.orElseGet(() -> otherService.saveOther(new WebClientBodyResponse(), user, pageUrl));
@Test
void should_other_type_of_scrap_is_saved_When_webClientService_crawlingItem_returns_null() throws ParseException {
// webClientService.crawlingItem()이 null을 반환할 때, Other 타입의 Scrap이 저장되는지 확인
//given
memoRepository.deleteAll();
scrapRepository.deleteAll();
BDDMockito.when(webClientService.crawlingItem("http://localhost:123", pageUrl))
.thenReturn(null);
User user = userRepository.findById(1L).get();
//when
//then
assertThat(scrapService.saveScraps(user, pageUrl)).isInstanceOf(Other.class);
assertThat(scrapRepository.findByPageUrlAndUserAndDeletedDateIsNull(pageUrl, user)
.isPresent()).isTrue();
}
@Test
void should_one_article_scrap_is_saved_When_webClientService_crawlingItem_returns_article() throws ParseException {
// webClientService.crawlingItem()이 type을 article로 반환할 때, Article 타입의 Scrap이 1개만 저장되는지 확인
//given
memoRepository.deleteAll();
scrapRepository.deleteAll();
WebClientBodyResponse webClientBodyResponse = new WebClientBodyResponse().builder()
.title("title")
.type("article")
.build();
BDDMockito.when(webClientService.crawlingItem("test", pageUrl))
.thenReturn(webClientBodyResponse);
User user = userRepository.findById(1L).get();
//when
//then
assertThat(scrapService.saveScraps(user, pageUrl)).isInstanceOf(Article.class);
assertThat(scrapRepository.count()).isEqualTo(1);
}
따라서 앞으로는 이러한 문제가 발생하지 않도록 테스트 코드를 예외 처리뿐만 아니라 정상적인 로직 및 로직을 체계적으로 세워서 발생할 수 있는 다양한 상황을 테스트 코드에 반영해야겠다고 다짐하게되었습니다!!