프로젝트에 ObjectMapepr 말고 JSONObject 적용기

SionBackEnd·2023년 4월 2일
0

Spring(봄)

목록 보기
20/22

개요

네이버 쇼핑 검색API를 통해서 받아온 결과값을 그대로 클라이언트측에게 전달하였다.
하지만, 검색API는 우리 프로젝트에서 가장 많이 사용될 API 이므로 이런 불필요한 데이터를 계속해서 전달하게 되면 통신 비용이 너무 크다는 생각이 들어서 리펙터링 포인트로 지정하였다.

JSONObject란

JSONObject란 json형태의 데이터를 더욱 편하게 관리할 수 있게 도와주는 라이브러리 이다.

 // https://mvnrepository.com/artifact/org.json/json
 implementation group: 'org.json', name: 'json', version: '20230227'

스프링에서는 jackson라이브러리의 ObjectMapper 클래스도 제공해주는데 굳이 외부 라이브러리를 사용하게 된이유는 아래와 같습니다.

내가 느낀 ObjectMapper의 한계

JSONObject 라이브러리 처럼 ObjectMapper도 직렬화와 역직렬화를 지원한다. 하지만, 문제는 직렬화 역직렬화 하기에 들어가는 비용이 많이 든다고 생각했다.

ObjectMapper objectMapper = new ObjectMapper();
        try {
            String json = "{\"name\":\"zooneon\",\"age\":25,\"address\":\"seoul\"}";
            Person person = objectMapper.readValue(json, Person.class);
            log.info("person.Age={}", person.getAge());
            log.info("person.Name={}", person.getName());
            log.info("person.Address={}", person.getAddress());
        } catch (JsonProcessingException e) {
            log.error(e.getMessage());
        }

--------------------------------------------------

@Getter
public class Person {
    private String name;
    private Integer age;
    private String address;
}

위와 같은 간단한 직렬화 역직렬화 코드를 예시로 문제점을 설명해 보겠다.
직렬화 역직렬화시 필요한 모든 필드를 작성해야한다.
이것이 큰문제가 되는것이 naverAPI로 부터 받아오는 필드의 개수가 적지 않은 수 이며 배열타입이 존재하므로 추가적인 작업을 진행해주어야한다.
(만약 Person 필드에 json 역직렬화 해줄 필드가 존재하지 않는다면 JsonProcessingException 가 발생한다.)
단순 네이버 API를 사용했을때 응답값을 역직렬화 해주기 위해서 응답값의 json을 모두 필드로 만들어 주어야한다는것이 너무 비효율적이라고 생각했다. (개인적으로는 코드의 가독성도 매우 떨어진다고 생각했다.)

그에비해 JSONObject라는 라이브러리가 생각보다 너무 간편하고 훨씬 가독성이 좋아진다고 판단했다.(json에서 데이터를 뽑아쓴다는 느낌의 메서드들이 존재했다.) 물론 JSONObject라이브러리도 dto를 만들어 데이터를 담아두지만, ObjectMapper처럼 불필요한 데이터를 위한 필드를 작성하지 않기 때문에 훨씬 더 효율이 좋다고 생각했다.

JSONObject를 사용해보자.

아래는 네이버로부터 받게되는 데이터이다. 이렇게 불필요해 보이는 데이터들을 계속해서 클라이언트측에 전송해주었다니 미안할 따름이다.. JSONObject를 사용해보기 위해서는 의존성을 주입받아야 한다. 위의 의존성을 확인하자 오늘자 기준 최신 버전이다.

	"lastBuildDate": "Mon, 03 Apr 2023 16:23:31 +0900",
    "total": 80032,
    "start": 1,
    "display": 2,
    "items": [
        {
            "title": "Apple <b>아이맥</b> 24형 2021년형 M1 8코어 CPU 및 7코어 GPU 256GB 실버 (MGTF3KH/A)",
            "link": "https://search.shopping.naver.com/gate.nhn?id=27104851525",
            "image": "https://shopping-phinf.pstatic.net/main_2710485/27104851525.20220705153344.jpg",
            "lprice": "1557330",
            "hprice": "",
            "mallName": "네이버",
            "productId": "27104851525",
            "productType": "1",
            "brand": "Apple",
            "maker": "Apple",
            "category1": "디지털/가전",
            "category2": "PC",
            "category3": "브랜드PC",
            "category4": ""
        },
        {
            "title": "Apple <b>아이맥</b> 24형 2021년형 M1 8코어 CPU 및 8코어 GPU 256GB 옐로 (Z12S0001L)",
            "link": "https://search.shopping.naver.com/gate.nhn?id=27104154532",
            "image": "https://shopping-phinf.pstatic.net/main_2710415/27104154532.20220705155913.jpg",
            "lprice": "1895200",
            "hprice": "",
            "mallName": "네이버",
            "productId": "27104154532",
            "productType": "1",
            "brand": "Apple",
            "maker": "Apple",
            "category1": "디지털/가전",
            "category2": "PC",
            "category3": "브랜드PC",
            "category4": ""
        }
    ]
}        

의존성을 주입 받았다면, 어떻게 설계할지 생각해보자.
간단하게 restAPI로 부터 받은 데이터를 클라이언트에게 내가 원하는 데이터만 전송해주는것이다. 나는 클라이언트측에게 서로 합의된 데이터를 DTO클래스에 담아서 전송해주었다. 그렇다면, 이 데이터도 서로 합의된 데이터만 DTO클래스에 담아서 전송해주면 될것같다.

할일

  1. DTO클래스 만들기
  2. 응답받은 데이터 중 필요한 데이터만 DTO클래스에 담아주기
  3. 클라이언트에게 전송해주기

아주 간단하다.

DTO클래스 만들기

나같은 경우 2가지의 DTO클래스를 만들어주었다.
Items 배열을 해결해줄 DTO와 페이지 정보와 Items배열을 담고있는 DTO

    @Getter
    @Builder
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class SearchItem {
        private Long productId;
        private String title;
        private String link;
        private String image;
        private Integer price;
    }

    @Getter
    @Builder
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class ResponseSearchItem {
        private Integer start;
        private Integer size;
        private List<SearchItem> items;
    }

딱 내가 필요한 정보들만 추렸다.

응답받은 데이터 중 필요한 데이터만 DTO클래스에 담아주기

생성자를 사용해서 담아줄까 생각을 했지만, 현재 프로젝트에서 Mapper를 통해서 데이터를 담아주기 때문에 Mapper를 사용하기로 했다. (훨씬 코드가 간결해진다)

@Mapper(componentModel = "spring", typeConversionPolicy = ReportingPolicy.IGNORE)
public interface ItemMapper {

    private ItemDto.SearchItem toSearchItem(JSONObject jsonObject) {
        return ItemDto.SearchItem.builder()
                .title(jsonObject.getString("title"))
                .price(jsonObject.getInt("lprice"))
                .productId(jsonObject.getLong("productId"))
                .link(jsonObject.getString("link"))
                .image(jsonObject.getString("image"))
                .build();
    }
    
    default List<ItemDto.SearchItem> toSearchItemList(JSONArray jsonArray) {
        List<ItemDto.SearchItem> SearchItemList = new ArrayList<>();
        for (int i = 0; i < jsonArray.length(); i++)
            SearchItemList.add(toSearchItem(jsonArray.getJSONObject(i)));
        return SearchItemList;
    }

    @Mapping(source = "searchItemList", target = "items")
    ItemDto.ResponseSearchItem toResponseSearchItem(List<ItemDto.SearchItem> searchItemList, Integer size, Integer start);
    
    }

참고로 위에 @Mapping이라는 어노테이션은 MapStruct 라이브러리를 사용해서 작성되어있는것이다. MapStruct에 대해서는 따로 설명하진 않겠다.

자바 1.8부터 인터페이스에 default메서드를 작성해줄 수 있기 때문에 라이브러리가 해결하기 어려운 부분은 이렇게 직접 작성해주었다.

이제 위의 메서드를 통해 클라이언트에게 전송할 데이터를 만들어보자.

 private ItemDto.ResponseSearchItem toResponseSearchItem(String body) {
        JSONObject jsonObject = new JSONObject(body);
        try {
            List<ItemDto.SearchItem> searchItemList = itemMapper.toSearchItemList(jsonObject.getJSONArray("items"));
            Integer start = jsonObject.getInt("start");
            Integer size = jsonObject.getInt("display");
            return itemMapper.toResponseSearchItem(searchItemList, size, start);
        } catch (JSONException e) {
            log.error("JsonException={}", e.getMessage());
            throw new BusinessLogicException(ErrorCode.NAVER_JSON_ERROR);
        }
    }

mapper를 통해서 json 데이터들을 클라이언트에게 제공할 데이터로 순조롭게 변경해주는것을 볼 수 있다. try/catch는 throw를 던지지 말고 꼭 내부에서 처리해주도록 하자.
그렇지 않으면 예외를 상속받기 때문에 JSONObject에 강하게 의존하게 된다.

참고한 사이트

profile
많은 도움 얻어가시길 바랍니다!

0개의 댓글