본문 바로가기
BackEnd/Project

[PharmNav] Ch05. 카카오 키워드 장소 검색 api 적용하기

by 개발 Blog 2024. 9. 3.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

이번 글에서는 카카오 API를 활용하여 약국 정보를 실시간으로 검색하고, 해당 데이터를 기반으로 약국을 추천하는 시스템을 구축하는 방법을 자세히 설명한다. 기존의 공공데이터를 사용하는 방식과 달리, 카카오 API를 통해 보다 동적이고 정확한 정보를 제공할 수 있다.

 

1. 카카오 API 사용 배경

기존에 사용하던 공공데이터는 지역이 한정적이고 정적인 데이터다. 예를 들어, 약국의 위치나 정보가 변경되었을 때 이러한 변화를 즉각적으로 반영하지 못하는 단점이 있다. 이를 보완하기 위해 카카오 API를 활용하면, 사용자가 특정 위치에 있는 약국 정보를 실시간으로 조회할 수 있으며, 거리순으로 정렬된 최신 정보를 제공받을 수 있다.

https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-category

 

2. API 호출 테스트

카카오 API를 사용하기 전에 Postman을 통해 API 호출 테스트를 진행했다. 이는 실제로 우리가 원하는 결과가 제대로 반환되는지 확인하기 위한 과정이다. API 요청 URL에 필요한 파라미터를 설정하고, 인증을 위해 REST API 키를 헤더에 추가한 후 호출한다. 이 과정에서 반경 내의 약국 정보를 JSON 형식으로 반환받았고, 이를 바탕으로 개발할 로직을 구체화할 수 있었다.

https://dapi.kakao.com/v2/local/search/category.json?category_group_code=PM9&x=127.03733003036&y=37.5960650456809&radius=10000&sort=distance

 

API 호출 예시

 

3. 데이터 매핑을 위한 DTO 수정

먼저, 카카오 API의 응답 데이터를 효과적으로 처리하기 위해 DocumentDto 클래스에 placeName과 distance 필드를 추가했다.

package com.example.phamnav.api.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DocumentDto {
    
    @JsonProperty("place_name")
    private String placeName;

    @JsonProperty("address_name")
    private String addressName;

    @JsonProperty("y")
    private double latitude;

    @JsonProperty("x")
    private double longitude;

    @JsonProperty("distance")
    private double distance;
}
  • placeName: 카카오 API는 검색된 장소의 이름(이 경우 약국 이름)을 place_name이라는 필드로 반환한다. 기존의 공공데이터에서는 약국명을 이미 알고 있었기 때문에 추가적인 처리가 필요 없었지만, 카카오 API를 사용할 때는 이 값을 받아와야 한다. 이 필드를 DocumentDto에 추가함으로써, 카카오 API의 응답 데이터를 객체로 변환할 때 약국 이름을 쉽게 매핑할 수 있다.
  • distance: 카카오 API는 사용자의 위치와 약국 간의 거리를 distance라는 필드로 제공한다. 기존 방식에서는 이 거리를 사용자가 입력한 좌표와 약국의 좌표를 통해 별도의 알고리즘으로 계산해야 했지만, 이제는 이 데이터를 바로 받아와 활용할 수 있다. distance 필드를 추가함으로써, API 응답에서 제공되는 거리를 쉽게 가져올 수 있고, 이를 통해 추가적인 거리 계산 없이도 정확한 정보 제공이 가능하다.

이렇게 DTO를 수정한 후, 카카오 API 응답 데이터를 DocumentDto로 변환할 때, placeName과 distance 필드가 자동으로 매핑되어 이후의 로직에서 사용된다.

 

4. 카카오 API URI 빌더 서비스 작성

이제 카카오 API를 호출하기 위해 필요한 URI를 동적으로 생성하는 KakaoUriBuilderService를 작성했다.

package com.example.phamnav.api.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Slf4j
@Service
public class KakaoUriBuilderService {

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

    public URI buildUriByCategorySearch(double latitude, double longitude, double radius, String category) {

        double meterRadius = radius * 1000;

        UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(KAKAO_LOCAL_CATEGORY_SEARCH_URL);
        uriBuilder.queryParam("category_group_code", category);
        uriBuilder.queryParam("x", longitude);
        uriBuilder.queryParam("y", latitude);
        uriBuilder.queryParam("radius", meterRadius);
        uriBuilder.queryParam("sort", "distance");

        URI uri = uriBuilder.build().encode().toUri();

        log.info("[KakaoAddressSearchService buildUriByCategorySearch] uri: {} ", uri);

        return uri;
    }
}
  • URI 생성: 이 서비스는 사용자가 입력한 위도, 경도, 검색 반경, 그리고 카테고리 코드(약국의 경우 "PM9")를 받아, 이 정보를 바탕으로 카카오 API의 요청 URI를 생성한다.
  • 파라미터 설정: URI를 생성할 때, UriComponentsBuilder를 사용하여 API에 필요한 쿼리 파라미터들을 설정한다. 예를 들어, x와 y는 각각 경도와 위도를 나타내며, radius는 검색 반경, sort는 결과를 정렬하는 기준(이 경우 거리순 정렬)을 의미한다.
  • 디버깅 지원: URI가 생성된 후, 이를 로그에 기록하여 올바르게 생성되었는지 확인할 수 있도록 했다. 이렇게 하면, API 요청 중 발생할 수 있는 오류를 쉽게 파악하고 디버깅할 수 있다.

이 과정에서 생성된 URI는 이후 API 요청을 보낼 때 사용되며, 필요한 데이터를 정확하게 가져오기 위해 필수적이다.

 

5. 카카오 API를 통한 약국 검색 서비스 구현

이제 실제로 카카오 API를 호출하여 약국 정보를 가져오는 KakaoCategorySearchService를 구현했다.

package com.example.phamnav.api.service;

import com.example.phamnav.api.dto.KakaoApiResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoCategorySearchService {

    private final KakaoUriBuilderService kakaoUriBuilderService;

    private final RestTemplate restTemplate;

    private static final String PHARMACY_CATEGORY = "PM9";

    @Value("${KAKAO.REST.API.KEY}")
    private String kakaoRestApiKey;

    public KakaoApiResponseDto requestPharmacyCategorySearch(double latitude, double longitude, double radius) {
        URI uri = kakaoUriBuilderService.buildUriByCategorySearch(latitude, longitude, radius, PHARMACY_CATEGORY);

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
        HttpEntity httpEntity = new HttpEntity<>(headers);

        return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, KakaoApiResponseDto.class).getBody();
    }
}
  • API 호출 준비: 먼저, KakaoUriBuilderService를 통해 생성된 URI를 사용하여 API 호출을 준비한다. 호출할 때는 RestTemplate을 사용하며, 요청에 필요한 URI와 함께 HTTP 헤더에 API 인증 키를 설정해 준다. 이 인증 키는 Authorization 헤더에 KakaoAK {API_KEY} 형식으로 포함된다.
  • API 응답 처리: API 요청이 성공적으로 이루어지면, 응답으로 받은 JSON 데이터를 KakaoApiResponseDto 객체로 변환하여 반환한다. 이 객체에는 약국 정보가 담긴 여러 개의 DocumentDto 리스트가 포함되어 있다. 이 데이터는 이후 단계에서 사용자에게 약국을 추천할 때 사용된다.

이 서비스는 API 호출과 응답 처리의 핵심 부분을 담당하며, 약국 검색 기능의 근간이 된다.

 

6. DirectionService 수정

DirectionService에서는 기존에 공공데이터를 사용하여 약국 정보를 처리하던 로직을 대체하고, 카카오 API를 통해 실시간으로 받아온 데이터를 사용하도록 수정했다.

package com.example.phamnav.direction.service;

import com.example.phamnav.api.dto.DocumentDto;
import com.example.phamnav.api.service.KakaoCategorySearchService;
import com.example.phamnav.direction.entity.Direction;
import com.example.phamnav.direction.repository.DirectionRepository;
import com.example.phamnav.pharmacy.service.PharmacySearchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class DirectionService {
    private static final int MAX_SEARCH_COUNT = 3; // 약국 최대 검색 갯수
    private static final double RADIUS_KM = 10.0; // 반경 10 km

    private final KakaoCategorySearchService kakaoCategorySearchService;

    ....
    
    // pharmacy search by category kakao api
    public List<Direction> buildDirectionListByCategoryApi(DocumentDto inputDocumentDto) {
        if(Objects.isNull(inputDocumentDto)) return Collections.emptyList();

        return kakaoCategorySearchService
                .requestPharmacyCategorySearch(inputDocumentDto.getLatitude(), inputDocumentDto.getLongitude(), RADIUS_KM)
                .getDocumentList()
                .stream().map(resultDocumentDto ->
                        Direction.builder()
                                .inputAddress(inputDocumentDto.getAddressName())
                                .inputLatitude(inputDocumentDto.getLatitude())
                                .inputLongitude(inputDocumentDto.getLongitude())
                                .targetPharmacyName(resultDocumentDto.getPlaceName())
                                .targetAddress(resultDocumentDto.getAddressName())
                                .targetLatitude(resultDocumentDto.getLatitude())
                                .targetLongitude(resultDocumentDto.getLongitude())
                                .distance(resultDocumentDto.getDistance() * 0.001) // km 단위
                                .build())
                .limit(MAX_SEARCH_COUNT)
                .collect(Collectors.toList());
    }
    ...
}
  • 약국 검색 로직: 사용자가 입력한 위치(위도와 경도)를 기반으로, 카카오 API를 사용해 지정된 반경 내에서 약국들을 검색한다. 검색된 약국 정보는 DocumentDto로 변환된 후, 이를 Direction 객체로 매핑하여 리스트로 반환한다.
  • 데이터 매핑: 각 DocumentDto에는 약국의 이름, 주소, 좌표(위도, 경도), 그리고 사용자 위치로부터의 거리가 포함되어 있다. 이러한 정보를 Direction 객체에 매핑하여, 사용자에게 약국을 추천할 때 사용할 데이터를 준비한다. 여기서 중요한 점은, API에서 반환된 거리 정보는 이미 km 단위로 변환되어 있어 추가적인 계산 없이 바로 사용 가능하다는 것이다.
  • 결과 제한: API에서 반환된 약국 리스트 중에서 최대 MAX_SEARCH_COUNT개만을 선택하여 반환하도록 제한을 두었다. 이를 통해 사용자에게 제공되는 선택지를 간소화하고, 가장 가까운 약국들을 추천할 수 있도록 했다.

이 서비스는 최종적으로 사용자에게 제공할 약국 리스트를 구성하는 역할을 하며, 기존의 공공데이터 기반 로직을 대체하여 더 정확하고 실시간으로 업데이트된 데이터를 사용할 수 있게 한다.

 

7. PharmacyRecommendationService 수정

마지막으로, PharmacyRecommendationService를 수정하여, 새로운 카카오 API 기반의 약국 추천 시스템을 통합했다.

package com.example.phamnav.pharmacy.service;

import com.example.phamnav.api.dto.DocumentDto;
import com.example.phamnav.api.dto.KakaoApiResponseDto;
import com.example.phamnav.api.service.KakaoAddressSearchService;
import com.example.phamnav.direction.entity.Direction;
import com.example.phamnav.direction.service.DirectionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;

@Slf4j
@Service
@RequiredArgsConstructor
public class PharmacyRecommendationService {

    private final KakaoAddressSearchService kakaoAddressSearchService;
    private final DirectionService directionService;

    public void recommendPharmacy(String address) {

        KakaoApiResponseDto kakaoApiResponseDto = kakaoAddressSearchService.requestAddressSearch(address);

            if (Objects.isNull(kakaoApiResponseDto) || CollectionUtils.isEmpty(kakaoApiResponseDto.getDocumentList())) {
                log.error("[PharmacyRecommendationService] Input address: {}", address);
            return;
        }

        DocumentDto documentDto = kakaoApiResponseDto.getDocumentList().get(0);

//        List<Direction> directionList = directionService.buildDriectionList(documentDto);
        List<Direction> directionsList1 = directionService.buildDirectionListByCategoryApi(documentDto);
        directionService.saveAll(directionsList1);

    }
}
  • 주소 검색: 사용자가 입력한 주소를 바탕으로, 먼저 카카오 API를 통해 해당 주소의 좌표(위도와 경도)를 얻어낸다. 이 좌표는 이후 약국 검색에 사용된다.
  • 약국 추천: 좌표를 얻은 후, DirectionService의 새로운 메서드를 사용하여 해당 좌표를 기준으로 가까운 약국들을 검색한다. 이 과정에서 카카오 API를 통해 실시간으로 약국 정보를 받아오며, 그 결과를 리스트로 반환한다.
  • 데이터 저장 및 활용: 최종적으로 추천된 약국 리스트는 데이터베이스에 저장되거나, 사용자에게 바로 제공될 수 있다. 이를 통해 사용자는 자신의 위치에서 가장 가까운 약국을 추천받을 수 있으며, 추천된 약국 리스트는 이후에 다시 활용될 수 있다.

이 서비스는 전체 시스템의 핵심 기능인 약국 추천을 담당하며, 사용자의 위치를 기반으로 최적의 약국을 선택하여 제공하는 역할을 한다.

 

이번 작업을 통해 카카오 API를 활용하여 실시간으로 약국 정보를 검색하고, 보다 정확하고 최신의 데이터를 바탕으로 약국 추천 시스템을 구현할 수 있었다. 기존의 공공데이터 방식에서 벗어나, 동적이고 유연한 서비스 제공이 가능해졌으며, 이를 통해 사용자 경험을 크게 향상시킬 수 있을 것이다. 앞으로 이 시스템을 바탕으로 더욱 다양한 기능을 추가하여 서비스의 완성도를 높여 나가겠다.