시작하기 전 Spring MVC 에서 사용할 컨트롤러 코드만 확인하시려면 맨 밑에 코드만 보셔도됩니다.
네이버 지역 검색 API Documents
네이버 공식 문서를 참고하여 작성했습니다.
네이버 개발자 센터
https://developers.naver.com/main/
네이버 오픈 API를 사용하는 방식은 거의 비슷합니다.
클라이언트 ID와 클라리언트 시크릿 ID를 사용하는 해야하는데요,
네이버 개발자 센터에 접속해주세요!
사용하실 API를 선택하고 등록해주세요.
검색 기능을 구현하기 때문에 검색으로 선택했습니다.
내 애플리게이션에 생성한 클라이언트에 들어가보면 사용해야할 클라이언트 ID와 시크릿 ID를 확인할 수 있습니다.
클라이언트 ID 생성을 했다면 API에 요청을 할 수 있게됐다고 생각하시면 됩니다.
저는 일단 구현하기 전에 Postman으로 요청 테스트를 해봤습니다.
Postman은 HTTP API를 간단하게 테스트 할 수 있는 도구인데요,
사용하는 방법도 어렵지 않아서 프로젝트 구현할 때나 API 테스트할 때 사용하는 것을 적극 추천합니다!!
ajax나 자바스크립트 오류로인해 데이터가 응답되지 않는건지 백엔드쪽 코드가 잘못 작성되어있는 건지 이런것들도 확인할 수 있어서 좋아요🙂
네이버 공식문서에도 설명이 잘 되어 있지만 보기 귀찮으실 수 있으니..
핵심을 빠뜨지지 않고 작성해보겠습니다!!
📌 요청과 응답은 요청 URL, 파라미터, 응답 요소의 3가지가 중요합니다!!
공식문서에서 가져온 사진입니다. 반환 형식에 따라 XML, JSON으로 나뉘는데요, 사용하실 형식으로 요청해주면됩니다!! 저는 Spring 프로젝트 구현중이라 JSON 데이터가 요청 편하기 때문에 jSON으로 했습니다.
요청 URL과 파라미터를 사용해서 요청하면됩니다.
예를들어 "경복궁"이라는 단어를 검색하고 싶다면
아래처럼 요청해주면 됩니다.
https://openapi.naver.com/v1/search/local.json?query=경복궁&display=5&start=1&sort=random
❗️❗️ 주의사항
아래 공식문서를 참고하면 트래픽문제로 최대 출력 건수(display)를 5개로 제한한다고 합니다..!!
https://developers.naver.com/notice/article/7528
응답결과들은 위의 응답 요소들을 포함하고 있습니다.
Postman 으로 요청해서 결과값들이 어떻게 나오는지 확인해볼게요🙂
요청할때는 요청 헤더에
X-Naver-Client-Id 이름으로 클라이언트 ID,
X-Naver-Client-Secret 이름으로 시크릿 ID를 반드시 같이 보내줘야합니다.
위의 정보로 요청을 해보면
{
"lastBuildDate": "Fri, 24 Nov 2023 02:11:09 +0900",
"total": 5,
"start": 1,
"display": 5,
"items": [
{
"title": "<b>경복궁</b>",
"link": "http://www.royalpalace.go.kr/",
"category": "여행,명소>궁궐",
"description": "",
"telephone": "",
"address": "서울특별시 종로구 세종로 1-91 경복궁",
"roadAddress": "서울특별시 종로구 사직로 161 경복궁",
"mapx": "1269770162",
"mapy": "375788407"
},
{
"title": "국립고궁박물관",
"link": "http://www.gogung.go.kr/",
"category": "여행,명소>박물관",
"description": "",
"telephone": "",
"address": "서울특별시 종로구 세종로 1-57 국립고궁박물관",
"roadAddress": "서울특별시 종로구 효자로 12 국립고궁박물관",
"mapx": "1269750486",
"mapy": "375765261"
},
{
"title": "<b>경복궁</b> 한옥마을점",
"link": "http://www.entas.co.kr/",
"category": "음식점>육류,고기요리",
"description": "",
"telephone": "",
"address": "인천광역시 연수구 송도동 24-17",
"roadAddress": "인천광역시 연수구 테크노파크로 180",
"mapx": "1266388695",
"mapy": "373907929"
},
{
"title": "국립민속박물관 어린이박물관",
"link": "https://www.nfm.go.kr/kids",
"category": "여행,명소>박물관",
"description": "",
"telephone": "",
"address": "서울특별시 종로구 세종로 1-1",
"roadAddress": "서울특별시 종로구 삼청로 37",
"mapx": "1269794606",
"mapy": "375817453"
},
{
"title": "<b>경복궁</b> 디큐브점",
"link": "http://www.entas.co.kr/",
"category": "음식점>육류,고기요리",
"description": "",
"telephone": "",
"address": "서울특별시 구로구 신도림동 692 디큐브시티 현대백화점 6층",
"roadAddress": "서울특별시 구로구 경인로 662 디큐브시티 현대백화점 6층",
"mapx": "1268896418",
"mapy": "375092188"
}
]
}
5개의 지역 검색 결과가 나오는 것을 확인할 수 있습니다.
응답 요소에서 확인한 것 처럼 title, link, address 등등 잘 출력되는 것을 확인할 수 있습니다!!
응답이 잘되니 컨트롤러를 생성해서 프로젝트에서 구현해보겠습니다.
응답도 잘 돼서 금방 될 줄 알았다.......😭😭😭
컨트롤러로 요청하니 데이터가 출력이 안된다......
일단 차근차근 해보겠습니다...🤔
{
"lastBuildDate": "Fri, 24 Nov 2023 04:34:31 +0900",
"total": 5,
"start": 1,
"display": 5,
"items": []
}
이런식으로 요청은 잘 되는데 items로 검색 데이터들이 응답되지 않았다.
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList() {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
String uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.toUriString();
WebClient webClient = WebClient.builder()
.baseUrl(uri)
// .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("X-Naver-Client-Id", clientId)
.defaultHeader("X-Naver-Client-Secret", clientSecret)
.build();
String responseBody = webClient.get()
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println("API Response: " + responseBody);
System.out.println("API uri: " + uri);
return ResponseEntity.ok(responseBody);
}
}
{
"lastBuildDate": "Fri, 24 Nov 2023 04:34:31 +0900",
"total": 5,
"start": 1,
"display": 5,
"items": []
}
여전히 데이터는 출력되지 않았다..
구글링 끝에 RestTemplate 클래스를 이용하는 방식으로 구현하는 예제들이 많았다.
RestTemplate 클래스도 REST API 요청을 보내고 응답을 받는 클래스입니다. RestTemplate 사용해서 구현을 해보면
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList() {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
String uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.toUriString();
RequestEntity<Void> req = RequestEntity
.get(uri)
.header("X-Naver-Client-Id", clientId)
.header("X-Naver-Client-Secret", clientSecret)
.header("Content-Type", "application/json; charset=UTF-8")
.build();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.exchange(req, String.class);
String responseBody = responseEntity.getBody();
System.out.println("API Response: " + responseBody);
System.out.println("API uri: " + uri);
return ResponseEntity.ok(responseBody);
}
}
로그를 확인하면
API Response: {
"lastBuildDate":"Fri, 24 Nov 2023 04:55:54 +0900",
"total":5,
"start":1,
"display":5,
"items":[
]
}
API uri: https://openapi.naver.com/v1/search/local.json?query=%EA%B0%95%EB%82%A8&display=5&start=1&sort=random
위의 코드로 실행을 해도
uri의 주소값은 잘 출력되는데 데이터가 자꾸 응답되지 않았다...
공식 문서를 참고해봤는데
URL url = new URL(apiUrl);
String 형식의 uri 주소를 URL 로 변환해서 사용하고있었다...
📌 결론적으로 URI 형식을 사용하거나 String 형식을 꼭 URI 형식으로 변환해줘야했다.
수정 전
String uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.toUriString();
그래서 처음 uri 를 생성할때 URI를 적용해서 생성해주었습니다.
수정 후
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.build()
.toUri();
수정 후 다시 요청해봤습니다.
심각: 경로 []의 컨텍스트 내의 서블릿 [appServlet]을(를) 위한 Servlet.service() 호출이, 근본 원인(root cause)과 함께, 예외 [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: "{?"errorMessage":"Malformed encoding (잘못된 형식의 인코딩입니다.)",?"errorCode":"SE06"}"]을(를) 발생시켰습니다.
org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: "{?"errorMessage":"Malformed encoding (잘못된 형식의 인코딩입니다.)",?"errorCode":"SE06"}"
어우... 다른 오류가 발생하기 시작했네요..
일단 오류 내용이 인코딩쪽 문제라 인코딩 코드를 추가해줬습니다.
수정 후
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
응답 로그
API Response: {
"lastBuildDate":"Fri, 24 Nov 2023 05:07:59 +0900",
"total":5,
"start":1,
"display":5,
"items":[
{
"title":"<b>강남<\/b>구청",
"link":"http:\/\/www.gangnam.go.kr\/",
"category":"공공,사회기관>구청",
"description":"",
"telephone":"",
"address":"서울특별시 강남구 삼성동 16-1 강남구청",
"roadAddress":"서울특별시 강남구 학동로 426 강남구청",
"mapx":"1270475020",
"mapy":"375173050"
},
{
"title":"코엑스",
"link":"http:\/\/www.coex.co.kr\/",
"category":"문화,예술>컨벤션센터",
"description":"",
"telephone":"",
"address":"서울특별시 강남구 삼성동 159",
"roadAddress":"서울특별시 강남구 영동대로 513",
"mapx":"1270594274",
"mapy":"375116620"
},
{
"title":"코엑스아쿠아리움",
"link":"http:\/\/www.coexaqua.com\/",
"category":"관람,체험>아쿠아리움",
"description":"",
"telephone":"",
"address":"서울특별시 강남구 삼성동 159",
"roadAddress":"서울특별시 강남구 영동대로 513",
"mapx":"1270590008",
"mapy":"375133209"
},
{
"title":"감성타코 <b>강남<\/b>역점",
"link":"http:\/\/www.instagram.com\/gamsungtaco",
"category":"음식점>멕시코,남미음식",
"description":"",
"telephone":"",
"address":"서울특별시 강남구 역삼동 820-9 지하1층 감성타코",
"roadAddress":"서울특별시 강남구 강남대로 406 지하1층 감성타코",
"mapx":"1270276280",
"mapy":"374989462"
},
{
"title":"그랜드 인터컨티넨탈 서울 파르나스",
"link":"http:\/\/seoul.intercontinental.com\/grandicparnas",
"category":"숙박>호텔",
"description":"",
"telephone":"",
"address":"서울특별시 강남구 삼성동 159-8 그랜드 인터컨티넨탈 서울 파르나스",
"roadAddress":"서울특별시 강남구 테헤란로 521 그랜드 인터컨티넨탈 서울 파르나스",
"mapx":"1270608688",
"mapy":"375095449"
}
]
}
아주 잘 응답됩니다....😭😭
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList() {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
String uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
RequestEntity<Void> req = RequestEntity
.get(uri)
.header("X-Naver-Client-Id", clientId)
.header("X-Naver-Client-Secret", clientSecret)
.header("Content-Type", "application/json; charset=UTF-8")
.build();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.exchange(req, String.class);
String responseBody = responseEntity.getBody();
return ResponseEntity.ok(responseBody);
}
}
실제로 적용하려면 검색어도 응답받아서 출력될 수 있게 @RequestParam을 사용해서더 수정해야하지만 테스트 용도로는 잘 실행됩니다.
구현하다보니 헤더에 값을 넣는 다른 방법이 있어서 아래 하나 더 작성하겠습니다!
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList() {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
String uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", "강남")
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
HttpHeaders headers = new HttpHeaders();
headers.set("X-Naver-Client-Id", clientId);
headers.set("X-Naver-Client-Secret", clientSecret);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, String.class);
String responseBody = responseEntity.getBody();
return ResponseEntity.ok(responseBody);
}
}
위 코드 처럼 HttpHeaders 이용해서 추가해도 잘 실행됩니다!
📌 참고사항
만약 Postman 에서 요청시
아래처럼 한글이 깨진다면{ "lastBuildDate":"Fri, 24 Nov 2023 05:25:11 +0900", "total":5, "start":1, "display":5, "items":[ { "title":"<b>??<\/b>??", "link":"http:\/\/www.gangnam.go.kr\/", "category":"??,????>??", "description":"", "telephone":"", "address":"????? ??? ??? 16-1 ????", "roadAddress":"????? ??? ??? 426 ????", "mapx":"1270475020", "mapy":"375173050" },
컨트롤러에서 produces = "application/json; charset=UTF-8"을 작성해준다면 해결할 수 있습니다!!
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
마지막으로 @RequestParam 적용해서 실제 프로젝트에서 사용할 코드 작성해보겠습니다.
❗️❗️ 추가로 WebClient 클래스도 이용해서 구현할건데요,
WebClient는 비동기 방식으로 HTTP 요청을 처리하고, Servlet API를 사용하지 않기 때문에 별도의 스레드를 생성하지 않아 성능이 더 좋다고 합니다.
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList(@RequestParam(name = "text")String text) {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", text)
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
RequestEntity<Void> req = RequestEntity
.get(uri)
.header("X-Naver-Client-Id", clientId)
.header("X-Naver-Client-Secret", clientSecret)
.header("Content-Type", "application/json; charset=UTF-8")
.build();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.exchange(req, String.class);
String responseBody = responseEntity.getBody();
return ResponseEntity.ok(responseBody);
}
}
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList(@RequestParam(name = "text") String text) {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", text)
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
HttpHeaders headers = new HttpHeaders();
headers.set("X-Naver-Client-Id", clientId);
headers.set("X-Naver-Client-Secret", clientSecret);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, String.class);
String responseBody = responseEntity.getBody();
return ResponseEntity.ok(responseBody);
}
}
WebClient 클래스 이용
@RestController
@RequestMapping("/naver")
public class NaverSearchController {
@GetMapping(value = "/search", produces = "application/json; charset=UTF-8")
public ResponseEntity<String> naverSearchList(@RequestParam(name = "text") String text) {
// 네이버 검색 API 클라이언트 ID
String clientId = "클라이언트 ID";
String clientSecret = "시크릿 ID";
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/local.json")
.queryParam("query", text)
.queryParam("display", 5)
.queryParam("start", 1)
.queryParam("sort", "random")
.encode(Charset.forName("UTF-8"))
.build()
.toUri();
WebClient webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("X-Naver-Client-Id", clientId)
.defaultHeader("X-Naver-Client-Secret", clientSecret)
.build();
ResponseEntity<String> responseEntity = webClient.get()
.uri(uri)
.retrieve()
.toEntity(String.class)
.block();
String responseBody = responseEntity.getBody();
return ResponseEntity.ok(responseBody);
}
}
여기까지 마무리하겠습니다!!
📌 다음 글에서는 지금 구현한 네이버 API를
진행중인 프로젝트에 활용하는 방법을 정리해보겠습니다!!!