본문 바로가기

Project/날씨 기반 여행 플래너

[날씨 기반 여행 플래너] RestTemplate으로 제네릭 클래스에 json 매핑하기 (feat. 제네릭의 타입 소거, 익명 클래스)

작년 11월, 멀티캠퍼스에서 백엔드 개발 교육 과정을 수료했다.

 

최종 프로젝트를 진행하면서 마주했던 문제들을 노션에 정리해놨는데,

프로젝트 복기 겸..! 누군가에게 도움이 될까 싶어..! 글로 정리해보고자 한다.

 

프로젝트를 간단히 설명하자면, '날씨 기반 여행 플래너'로 날씨를 바탕으로 여행 계획을 수립할 수 있는 웹 서비스이다.

Java, Spring boot, MyBatis, Oracle 등의 기술을 활용하였고,

날씨와 여행 장소 데이터는 기상청과 한국관광공사의 Open API를 이용하였다.


내가 맡은 역할 중 하나는 한국관공공사의 TourAPI를 이용하여 국내 여행 장소를 DB에 저장하는 일이었다.

 

TourAPI에서는 지역기반관광정보, 소개정보, 이미지정보.. 여러 데이터를 제공해주는데,

주소, 이름과 같은 대표 정보와 함께 음식 메뉴와 같은 상세 정보까지 보여주는 것을 요구사항으로 잡았기에 여러 API를 활용해야 했다.

 

TourAPI의 응답 형식은 동일했기에 다음 코드와 같이 제네릭을 활용하여 클래스를 재사용하고자 했다.

하지만, RestTemplate으로 데이터를 받아오는 과정에서 ClassCastException이 발생했다.

 

더보기
public class ResponseDTO<T> {
    private Response<T> response;

    public Response<T> getResponse() {
        return response;
    }

    public void setResponse(Response<T> response) {
        this.response = response;
    }
}

 

public class Response<T> {
    private Body<T> body;

    public Body<T> getBody() {
        return body;
    }

    public void setBody(Body<T> body) {
        this.body = body;
    }
}

 

public class Body<T> {
    private Items<T> items;

    public Items<T> getItems() {
        return items;
    }

    public void setItems(Items<T> items) {
        this.items = items;
    }
}

 

public class Items<T> {

    private List<T> item;

    public List<T> getItem() {
        return item;
    }

    public void setItem(List<T> item) {
        this.item = item; 
    }

}

 

ResponseDTO<RestrauntDto> res = restTemplate.getForObject(uri, ResponseDTO.class);

 

 

왜 안될까..를 반복하다 제네릭의 타입 소거에 대해 알게 되었다. 제네릭은 런타임 시 타입이 소거되어 클래스 파일(.class)에 제네릭 타입에 대한 정보가 없어진다. 따라서 코드에 바인딩 할 객체를 적어주어도 RestTemplate이 어떤 객체에 바인딩할 지 알 수 없는 것이다.

 

그렇다면 어떻게 해야 할까..?? 바로 상속 or 구현을 이용해야 한다!

다음 코드와 같이 제네릭 타입을 가지고 있는 클래스나 인터페이스를 상속 혹은 구현 받는 클래스를 만든다면,

자식 클래스는 부모 클래스의 구체적인 타입 파라미터를 포함하여 컴파일 돼 메타데이터로 가지게 된다.

이 정보는 자바의 리플렉션으로 'getGenericInterfaces()'와 'getGenericSuperclass()', 'getActualTypeArguments()' 메서드를 통해 추출할 수 있게 되고, 이를 활용하면 RestTemplate은 json 데이터를 객체에 바인딩할 수 있게 된다.

ResponseDTO<RestrauntDto> res = new ResponseDTO<RestrauntDto>(){};

 

 

Spring에서는 이를 편하게 구현해주는 클래스를 제공해준다! 바로 'ParameterizedTypeReference<T>' 클래스이다.
익명 클래스로 'ParameterizedTypeReference<ResponseDTO<RestrauntDto>>'를 상속 받는 객체를 생성해주면, 해당 객체는 'ResponseDTO<RestrauntDto>'에 대한 정보를 알 수 있다.

ResponseEntity<ResponseDTO<RestrauntDto>> response = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<ResponseDTO<RestrauntDto>>() {});

 


'ParameterizedTypeReference<T>' 클래스를 보면 생성자에서 'getGenericSuperclass()'으로 상속 받은 클래스의 타입인 ' ParameterizedTypeReference'를 추출하고 'getActualTypeArguments()'로 지정한 타입인 'ResponseDTO<PlaceDto>'를 가져와 클래스의 필드에 저장한다.

 

 

이렇게 저장된 type은 RestTemplate의 exchange 함수에서 사용하게 된다. (ResponseEntity<T> 객체로 응답을 추출하는 곳에서..)