티스토리 뷰

개발 노트

쿼리 스트링 만들기

kkoon9 2023. 1. 2. 00:29

사용 목적

외부 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

중복된 코드를 다음 기능을 통해 해결해보려고 합니다.

  1. JsonProperty 어노테이션
  2. 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

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
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
글 보관함