티스토리 뷰
외부 API를 받아오는 WebClient 개발 관련 포스팅입니다.
해당 github 주소입니다.
GitHub - laboratory-kkoon9/spring-batch-gradle
Contribute to laboratory-kkoon9/spring-batch-gradle development by creating an account on GitHub.
github.com
사용 목적
외부 API의 GET 호출에 필요한 쿼리 스트링, DTO로 만들기
[1]. DTO 내부 parse 메서드
첫 번째 떠올린 방법은 DTO 내에서 쿼리 스트링을 만들어주는 메서드를 만들기입니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.dto;
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.util.UriComponentsBuilder;
@Getter
public class BoxOfficeRequestDto {
private String key;
private String date;
private String row;
private String isMulti;
private String nation;
private String area;
@Builder
public BoxOfficeRequestDto(String key, String date, String row, String isMulti, String nation, String area) {
this.key = key;
this.date = date;
this.row = row;
this.isMulti = isMulti;
this.nation = nation;
this.area = area;
}
public String parse() {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance()
.queryParam("key", this.key)
.queryParam("targetDt", this.date)
.queryParam("itemPerPage", this.row)
.queryParam("multiMovieYn", this.isMulti)
.queryParam("repNationCd", this.nation)
.queryParam("wideAreaCd", this.area);
return builder.toUriString();
}
}
DTO 내부에 있을 때의 단점 [1]
요청 DTO가 늘어날 때마다 해당 DTO 내부의 parse() 메서드를 만들어야 합니다.
그렇게 되면 비슷한 코드가 중복해서 생기게 됩니다.
중복된 코드는 유지보수를 어렵게 하고, OOP 원칙에 어긋납니다.
DTO 내부에 있을 때의 단점 [2]
DTO는 Data Transfer Object의 약자로, 계층 간 데이터 교환을 하기 위해 사용하는 객체입니다.
바꿔 말하면, 데이터 교환만을 책임으로 가지는 객체인 셈이죠.
그런 객체에 쿼리 스트링을 만들 책임을 하나 더 주면 SOLID 원칙의 SRP를 위반하게 됩니다.
[2]. 중복 코드 해결 - JsonParser
중복된 코드를 다음 기능을 통해 해결해보려고 합니다.
- JsonProperty 어노테이션
- ObjectMapper
중복 코드 해결 [1]. JsonProperty 어노테이션
먼저, 위에서 사용한 DTO를 다음과 같이 수정했습니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;
@Getter
public class BoxOfficeRequestDto {
@JsonProperty("key")
private String key;
@JsonProperty("targetDt")
private String date;
@JsonProperty("itemPerPage")
private String row;
@JsonProperty("multiMovieYn")
private String isMulti;
@JsonProperty("repNationCd")
private String nation;
@JsonProperty("wideAreaCd")
private String area;
@Builder
public BoxOfficeRequestDto(String key, String date, String row, String isMulti, String nation, String area) {
this.key = key;
this.date = date;
this.row = row;
this.isMulti = isMulti;
this.nation = nation;
this.area = area;
}
}
중복의 원인이 되었던 parse() 메서드를 제거하고, JsonProperty 어노테이션을 사용했습니다.
해당 어노테이션의 동작 결과는 아래 테스트 코드에서 확인하실 수 있습니다.
이로써 DTO는 데이터 교환이라는 책임 하나만 가지게 되었습니다.
중복 코드 해결 [2]. ObjectMapper
DTO에서 빼낸 parse 기능을 책임질 클래스를 두 개 만들었습니다.
parse() [1]. DTO to JsonString
첫 번째로는 DTO를 문자열 형태의 Json으로 만드는 클래스입니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonParser<T> {
public String parse(T object) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(object);
}
}
parse() 메서드는 ObjectMapper 인스턴스에서 제공하는 writerValueAsString 메서드를 활용하여 DTO를 문자열 형태의 Json으로 만들어줍니다.
다음 테스트 코드를 보면 바로 이해가 빠릅니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.laboratorykkoon9.springbatchgradle.infra.boxoffice.dto.BoxOfficeRequestDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
public class JsonParserTest {
JsonParser<BoxOfficeRequestDto> jsonParser;
@DisplayName("전달 받은 DTO의 JsonProperty 어노테이션 내 value값으로 치환된다.")
@Test
void test_1() throws JsonProcessingException {
// given
BoxOfficeRequestDto dto = BoxOfficeRequestDto.builder()
.key("Test_key")
.date("20230101")
.row("100")
.nation("K")
.isMulti("N")
.area("101010")
.build();
// when
jsonParser = new JsonParser<>();
String result = jsonParser.parse(dto);
// then
assertAll(
() -> assertThat(result.contains("targetDt")).isTrue(),
() -> assertThat(result.contains("itemPerPage")).isTrue(),
() -> assertThat(result.contains("multiMovieYn")).isTrue(),
() -> assertThat(result.contains("repNationCd")).isTrue(),
() -> assertThat(result.contains("wideAreaCd")).isTrue()
);
}
}
result 문자열에는 JsonProperty 어노테이션 내 value 값과 일치하는 문자열들이 포함되는 걸 볼 수 있습니다.
parse() [2]. JsonString to Map
위에서 만든 문자열 형태의 Json을 파라미터로 하는 MapConverter 클래스입니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.ObjectUtils;
import java.util.Map;
import static com.laboratorykkoon9.springbatchgradle.global.constant.CommonConstants.EMPTY_ERROR_MESSAGE;
public class JsonToMapConverter {
private JsonToMapConverter() {}
public static Map<String, Object> convert(String json) throws JsonProcessingException {
if(ObjectUtils.isEmpty(json)) {
throw new IllegalArgumentException(EMPTY_ERROR_MESSAGE);
}
return new ObjectMapper().readValue(json, Map.class);
}
}
ObjectMapper에서 제공하는 readValue 메서드를 사용했습니다.
테스트 코드는 다음 링크를 참고해주세요.
GitHub - laboratory-kkoon9/spring-batch-gradle
Contribute to laboratory-kkoon9/spring-batch-gradle development by creating an account on GitHub.
github.com
QueryStringConverter
위에서 추출한 Map을 파라미터로 하는 쿼리스트링 클래스입니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.util;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
import static com.laboratorykkoon9.springbatchgradle.global.constant.CommonConstants.EMPTY_ERROR_MESSAGE;
public class QueryStringConverter {
private QueryStringConverter() {
}
public static String convert(Map<String, Object> messages) {
if (messages == null) {
throw new NullPointerException(EMPTY_ERROR_MESSAGE);
}
UriComponentsBuilder queryString = UriComponentsBuilder.newInstance();
for (String key : messages.keySet()) {
if(messages.get(key) == null) {
continue;
}
queryString.queryParam(key, messages.get(key).toString());
}
return queryString.toUriString();
}
}
위에서 만든 Map을 쿼리스트링 형태로 변경해주는 클래스입니다.
테스트 코드를 보시면 이해가 더 쉽습니다.
package com.laboratorykkoon9.springbatchgradle.infra.boxoffice.util;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.laboratorykkoon9.springbatchgradle.global.constant.CommonConstants.EMPTY_ERROR_MESSAGE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class QueryStringConverterTest {
@DisplayName("파라미터명과 값을 받아서 쿼리스트링 문자열로 컨버팅해준다.")
@Test
void test_1() {
// given
Map<String, Object> messages = new LinkedHashMap<>(); // Test를 위해 LinkedHashMap으로 선언하였습니다.
messages.put("key", "1234");
messages.put("targetDt", "20221230");
// when
String result = QueryStringConverter.convert(messages);
String expected = "?key=1234&targetDt=20221230";
// then
assertThat(result).isEqualTo(expected);
}
}
결론
DTO 내부에서 처리하던 parse 메서드를 위같은 클래스로 나누어봤습니다.
이견이나 궁금한 점 있으시면 댓글 남겨주세요~!
추가로, ObjectMapper를 인스턴스로 사용했는데, 안 좋은 예시입니다.
안 좋은 이유와 이에 대한 대안은 다른 포스팅에서 다뤄보겠습니다.
출처
Spring Boot: Customize the Jackson ObjectMapper | Baeldung
Learn how to configure the serialization and deserialization options for Jackson using Spring Boot.
www.baeldung.com
ObjectMapper (jackson-databind 2.7.0 API)
Convenience method for doing two-step conversion from given value, into instance of given value type, if (but only if!) conversion is needed. If given value is already of requested type, value is returned as is. This method is functionally similar to first
fasterxml.github.io
[Java] Jackson ObjectMapper Serialization
val MY_OBJECT_MAPPER = jacksonObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(Serializa
umbum.dev
'개발 노트' 카테고리의 다른 글
[스프링+코틀린] Execution failed for task ':test'. > No tests found for given includes (0) | 2023.01.21 |
---|---|
JpaRepository 파라미터 Mocking 시 NullPointerException (0) | 2023.01.09 |
이 로직은 도메인 영역일까? 애플리케이션 영역일까? (0) | 2022.08.07 |
DTO 엔티티 validation과 List<DTO> (0) | 2022.05.28 |
python으로 해당 월에 몇 주차인지 구하기 (2) | 2022.01.09 |
- Total
- Today
- Yesterday
- Olympiad
- 코테
- MSA
- Kotlin
- BAEKJOON
- kotest
- kkoon9
- 프로그래머스
- node.js
- 백준
- 정규표현식
- 이팩티브 자바
- Spring
- 알고리즘
- JPA
- C++
- AWS
- 클린 코드
- 객체지향
- 디자인 패턴
- 디자인패턴
- BOJ
- 클린 아키텍처
- 이펙티브 자바
- Java
- programmers
- Algorithm
- Effective Java
- 테라폼
- Spring Boot
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 27 | 28 |