본문 바로가기

Project

Kakao API를 사용하여 키워드로 장소 검색하기

1. 왜 Kakao API를 썼는가?

오먹추 프로젝트의 목표는 사용자의 음식 선택을 도와주는 추천 기능을 만드는 것이었다.

이때 가장 중요했던 건 장소 정보였다.

예를 들어 사용자들이 '김밥', '떡볶이'처럼 투표한 메뉴를 기반으로 사용자 주변에 있는 관련 식당들을 알려줘야 했다.

 

그래서 생각났던 것이 Kakao Maps였는데 한국 기반 서비스 + 개발 문서가 깔끔 + 손쉬운 REST API를 제공 해서 선택을 하게 되었다.


2. 연동을 위한 사전 준비

개발자 등록 & 키 발급

  1. Kakao Devlopers 접속
  2. 로그인 후 내 애플리케인션에서 애플리케이션 추가하기
  3. 왼쪽 대시보드에 앱 설정 -> 앱 키에 들어가서 REST API 키 사용

3. Kakao API 호출을 위한 Client 클래스

일단 Kakao Maps의 키워드로 장소 검색하는 문서를 살펴보자!

 

문서에서는 다음 정보를 제공한다.

  • 요청 URL
GET "https://dapi.kakao.com/v2/local/search/keyword.json";
  • 쿼리 파라미터 목록
    • query : 검색어
    • x, y : 중심 좌표
    • radius : 반경 (미터 단위, 최대 20,000)
  • 헤더 설정
Authorization: KakaoAK {REST_API_KEY}

 

근데 여기까지만 보면

String url = "https://dapi.kakao.com/v2/local/search/keyword.json?query=분식&x=...&y=...";
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + apiKey);

이 정도까지만 감이 오지, 어떤 JSON이 오고 어떻게 매핑해야하는지는 나오지 않는다.


그래서 문서 밑에 cURL로 요청해서 테스트한 응답 구조가 나온다.

curl -X GET "https://dapi.kakao.com/v2/local/search/keyword.json?query=분식&x=126.978&y=37.5665&radius=2000" \
  -H "Authorization: KakaoAK {YOUR_REST_API_KEY}"

 

그러면 JSON 응답이 아래와 같이 떨어진다.

@Component
@RequiredArgsConstructor
@Slf4j
public class KakaoClient {

    @Value("${kakao.rest.api-key}")
    private String kakaoApiKey;

    private final RestTemplate restTemplate;

    private static final String KAKAO_LOCAL_URL = "https://dapi.kakao.com/v2/local/search/keyword.json";

    public List<PlaceDto> searchPlaces(String keyword, double lat, double lng, int radius) {

        String url = String.format(
                KAKAO_LOCAL_URL + "?query=%s&y=%f&x=%f&radius=%d",
                keyword, lat, lng, radius
        );


        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "KakaoAK " + kakaoApiKey);

        HttpEntity<Void> entity = new HttpEntity<>(headers);

        ResponseEntity<KakaoSearchResponse> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                entity,
                KakaoSearchResponse.class
        );

        List<KakaoDocument> docs = response.getBody().getDocuments();

        for (KakaoDocument doc : docs) {
            log.info("가게 이름: {}, 거리 : {}, 주소 : {}",
                    doc.getPlaceName(), doc.getDistance(), doc.getAddress());
        }

        return docs.stream()
                .map(PlaceDto::from)
                .toList();
    }
}

 

나머지 코드도 정리해보자.

    • RestTemplate는 HTTP 요청을 자바 코드로 보낼 수 있는 도구
    • 현재 하고 싶은 건? ➡️ 카카오 장소 검색 API에 GET 요청을 보내서 JSON 응답을 받아 오는 것

1) HttpEntity<Void> entity = new HttpEntity<>(headers);

  • 역할 : 이 코드는 HTTP 요청을 생성할 때 사용된다. HttpEntity는 HTTP 요청 또는 응답에 해당하는 헤더와 바디를 포함하는 클래스이다. 여기서는 headers 객체를 포함하여 entity를 생성하였으며 바디는 없으므로 제네릭 타입으로 Void를 사용했다.
  • 이유 : Kakao API는 요청 시 인증을 위한 헤더를 필요로 한다. 따라서 Authrozation 헤더를 포함한 httpHeaders 객체를 생성하고 이를 HttpEntity에 담아 사용한다.

HttpEntity에 header만 집어넣었기 때문에 body는 null인 모습

2) restTemplate.exchange(...)

  • 역할 : exchange() 메서드는 지정된 HTTP 메서드를 사용하여 주어진 URL에 요청을 보내고, 응답을 ResponseEntity로 반환한다. 여기서는 GET 메서드를 사용하여 KakaoAPI에 요청을 보내고, 응답을 KakaoSearchResponse 타입으로 받는다.
  • 이유 : exchange() 메서드는 다양한 HTTP 메서드를 지원하며, 요청과 응답에 대한 세부적인 설정이 가능하다. 이를 통해 요청 헤더를 설정하고, 응답을 원하는 타입으로 변환할 수 있다.

3) List<KakaoDocument> docs = response.getBody().getDocumnets();

  • 역할 : 응답 본문에서 documents 리스트를 추출한다. response.getBody()는 KakaoSearchResponse 객체를 반환하며, getDocuments()를 통해 실제 데이터 리스트를 가져온다
  • 이유 : KakaoAPI의 응답은 documents라는 키 아래에 검색 결과를 담고 있다. 따라서 이를 추출하여 이후의 로직에서 활용한다.

4) return docs.stream().map(PlaceDtp::from).toList();

  • 역할 : docs 리스트의 각 KakaoDocument 객체를 PlaceDto 객체로 변환한 후, 리스트로 수집하여 반환한다.

4. Kakao API 연동 시 사용된 DTO 클래스

그래서 위 cURL의 응답 문서 바탕으로 DTO를 구성했다.

프로젝트에서는 응답 값으로 받은 docmuents에서 몇개의 정보만 활용하려고 한다.

 

1) KakaoSearchResponse - 카카오 응답 전체를 담는 최상위 컨테이너

@Getter
@NoArgsConstructor
public class KakaoSearchResponse {

    private List<KakaoDocument> documents;

}

 

이 클래스는 왜 존재할까?

  • 카카오 API에서 검색 결과를 받을 때
{
  "documents": [ ... ],
  "meta": { ... }
}

 

이런식으로 documents라는 Key 아래 실제 장소 리스트들이 들어가 있다.

그래서 이 documents 키와 매핑되는 리스트가 필요하고, 그걸 담은게 바로 KakaoSearchResponse 클래스이다.


2) KakaoDocument - 장소 하나하나의 정보를 담는 클래스

@Getter
@NoArgsConstructor
public class KakaoDocument {

    @JsonProperty("place_name")
    private String placeName;
    @JsonProperty("address_name")
    private String address;
    @JsonProperty("phone")
    private String phone;
    @JsonProperty("distance")
    private String distance;
    @JsonProperty("place_url")
    private String placeUrl;
}

 

이 클래스는 왜 존재할까?

  • 아까 말한 documents 배열 안에는 여러 장소의 정보가 있다.
  • 각각의 장소가 JSON 객체로 묶여있고 그 하나의 장소 단위를 그대로 자바 객체로 옮긴 게 이 KakaoDocument 클래스이다.
  • 예를 들어 밑의 JSON 하나가
{
  "place_name": "김밥천국",
  "address_name": "서울 중구 어딘가",
  "phone": "02-1234-5678",
  ...
}

➡️ 바로 하나의 KakaoDocument 객체가 되는거다. 즉, 이 클래스는 장소 하나의 원본 데이터를 담당한다.


3) PlaceDto - 서비스 내부에서 사용할 가공된 응답 클래스

@Getter
@AllArgsConstructor
public class PlaceDto {
    private String name;
    private String address;
    private String phone;
    private String distance;
    private String uri;

    public static PlaceDto from(KakaoDocument document) {
        return new PlaceDto(
            document.getPlaceName(),
            document.getAddress(),
            document.getPhone(),
            document.getDistance(),
            document.getPlaceUrl()
        );
    }
}

 

❓ 얘는 왜 또 따로 있는 걸까?

  • 우리가 클라이언트(프론트엔드 등)에게 그대로 KakaoDocument를 넘겨도 되지만 서비스의 응답 형태는 서비스에 맞게 정의하는게 좋으므로 PlaceDto는 응답에 꼭 필요한 필드만 골라 담아서 API 응답 전용 객체로 사용하는 것이다.

5. 클래스 간 흐름 정리

카카오 API 응답 (JSON)
       ↓
KakaoSearchResponse ← documents → List<KakaoDocument>
       ↓
docs.stream().map(PlaceDto::from)
       ↓
List<PlaceDto> (최종 사용자 응답)