외부 API를 이용해보는 경험을 쌓고자 백과사전 검색기능을 프로젝트에 추가해보기로 했다.
네이버 검색(백과사전) API를 이용해서, 검색어를 입력하면 검색결과를 보여주도록 만들어보자.
사전 준비
https://developers.naver.com/docs/serviceapi/search/blog/blog.md#%EB%B8%94%EB%A1%9C%EA%B7%B8
위 링크로 접속해서, API 이용방법을 숙지.
(로그인 후 이용신청을 해야 id와 시크릿키를 발급받을 수 있다)
파라미터는... 일단은 검색어 query만 신경쓰면 될 것 같다. (display와 start는 페이징을 위해 필요한것 같은데, 지금은 생략함)
< HTTP/1.1 200 OK
< Server: nginx
< Date: Mon, 26 Sep 2016 01:50:00 GMT
< Content-Type: text/xml;charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Keep-Alive: timeout=5
< Vary: Accept-Encoding
< X-Powered-By: Naver
< Cache-Control: no-cache, no-store, must-revalidate
< Pragma: no-cache
<
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Naver Open API - encyc ::'주식'</title>
<link>http://search.naver.com</link>
<description>Naver Search Result</description>
<lastBuildDate>Mon, 26 Sep 2016 10:50:00 +0900</lastBuildDate>
<total>28034</total>
<start>1</start>
<display>10</display>
<item>
<title><b>주식</b></title>
<link>http://openapi.naver.com/l?AAAB2NPQ+CMBiEf83LSPrxAu3QAQoYYhwZdCNQRSMFaiXh31tMbrjncpdbv8btCioNOYdCH0YUIMtodOauRu8X4DmwOsgbN31i223Gxf08hcRY7/bYjhZ4Pcx9MwAvKUUmmQCW9s+DkUhkB3XePGa3/0ucCk4iryhmKBJMCcFERpPSTXVdX1ty0XijnCzh45zRdcnb07sNux+iWggwrgAAAA==</link>
<description> <b>주식</b>회사의 자본을 이루는 단위로서의 금액 및 이를 전제로 한 주주의 권리·의무(주주권). <b>주식</b>회사는 자본단체이므로 자본이 없이는 성립할 수 없다. 자본은 사원인 주주(株主)의 출자이며, 권리와 의무의... </description>
<thumbnail />
</item>
...
</channel>
</rss>
응답예시.
네이버에서 사용예를 제공해준다.
// 네이버 검색 API 예제 - 블로그 검색
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
public class ApiExamSearchBlog {
public static void main(String[] args) {
String clientId = "YOUR_CLIENT_ID"; //애플리케이션 클라이언트 아이디
String clientSecret = "YOUR_CLIENT_SECRET"; //애플리케이션 클라이언트 시크릿
String text = null;
try {
text = URLEncoder.encode("그린팩토리", "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("검색어 인코딩 실패",e);
}
String apiURL = "https://openapi.naver.com/v1/search/blog?query=" + text; // JSON 결과
//String apiURL = "https://openapi.naver.com/v1/search/blog.xml?query="+ text; // XML 결과
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("X-Naver-Client-Id", clientId);
requestHeaders.put("X-Naver-Client-Secret", clientSecret);
String responseBody = get(apiURL,requestHeaders);
System.out.println(responseBody);
}
private static String get(String apiUrl, Map<String, String> requestHeaders){
HttpURLConnection con = connect(apiUrl);
try {
con.setRequestMethod("GET");
for(Map.Entry<String, String> header :requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
return readBody(con.getErrorStream());
}
} catch (IOException e) {
throw new RuntimeException("API 요청과 응답 실패", e);
} finally {
con.disconnect();
}
}
private static HttpURLConnection connect(String apiUrl){
try {
URL url = new URL(apiUrl);
return (HttpURLConnection)url.openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
} catch (IOException e) {
throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
}
}
private static String readBody(InputStream body){
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는 데 실패했습니다.", e);
}
}
}
너무 잘 나와있어서 그대로 사용해도 될 것 같다..
1. Service
@Service
@Transactional
public class NaverSearchAPIService {
public static NaverSearchResDto main(String arg) {
String clientId = "id"; //애플리케이션 클라이언트 아이디
String clientSecret = "secret"; //애플리케이션 클라이언트 시크릿
String text = null;
try {
text = URLEncoder.encode(arg, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("검색어 인코딩 실패",e);
}
String apiURL = "https://openapi.naver.com/v1/search/encyc.json?query=" + text; // JSON 결과
//String apiURL = "https://openapi.naver.com/v1/search/blog.xml?query="+ text; // XML 결과
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("X-Naver-Client-Id", clientId);
requestHeaders.put("X-Naver-Client-Secret", clientSecret);
String responseBody = get(apiURL,requestHeaders);
//
System.out.println(responseBody);
// return responseBody;
return NaverSearchResDto.fromJson(responseBody);
}
private static String get(String apiUrl, Map<String, String> requestHeaders){
HttpURLConnection con = connect(apiUrl);
try {
con.setRequestMethod("GET");
for(Map.Entry<String, String> header :requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
return readBody(con.getErrorStream());
}
} catch (IOException e) {
throw new RuntimeException("API 요청과 응답 실패", e);
} finally {
con.disconnect();
}
}
private static HttpURLConnection connect(String apiUrl){
try {
URL url = new URL(apiUrl);
return (HttpURLConnection)url.openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
} catch (IOException e) {
throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
}
}
private static String readBody(InputStream body){
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는 데 실패했습니다.", e);
}
}
}
네이버에서 제공하는 예시코드를 그대로 갖다쓰고, 필요한 몇가지 부분만 수정했다.
우선 id와 secret값을 본인이 발급받은 값으로 수정해야한다.
(이 정보들은 설정파일에 작성해두고, @Value로 끌어다 쓰는것이 좋을 것 같다)
그리고, 기존의 main메소드는 JSON데이터를 String타입으로 반환하는데, 나는 객체로 반환되었으면 좋겠다는 생각이 들어서 Dto.fromJson()메소드를 별도로 작성하고 JSON을 DTO로 변환하여 리턴하였다.
(뷰로 데이터를 넘겨야하는데 String으로 넘겨도 뷰에서 잘 적용할 수 있을까?라는 의문이 들어서.. 일단 변환해보기로 함.)
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class NaverSearchResDto {
private String lastBuildDate;
private int total;
private int start;
private int display;
private List<SearchItemDto> items = new ArrayList<>();
public static NaverSearchResDto fromJson(String json){
ObjectMapper objectMapper = new ObjectMapper();
try {
// JSON을 Dto 객체로 매핑
return objectMapper.registerModule(new JavaTimeModule()).readValue(json, NaverSearchResDto.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@Getter @Setter
public class SearchItemDto {
private String title;
private String link;
private String description;
private String thumbnail;
}
객체로 매핑하기 위해서 SearchItemDto, NaverSearchResDto 클래스를 작성했다.
2. Controller
@Controller
@RequiredArgsConstructor
@Slf4j
public class SearchController {
private final SearchService searchService;
@GetMapping("/search")
public String search(@RequestParam String keyword, Model model){
NaverSearchResDto naverSearchResDto = NaverSearchAPIService.main(keyword);
model.addAttribute("res",naverSearchResDto);
model.addAttribute("keyword",keyword);
return "search";
}
}
파라미터로 키워드를 받고, 서비스로 넘겨서 결과를 반환받는다.
이후 뷰로 전달.
3.View
{{>layouts/header}}
<div class="container p-5">
<h2>"{{keyword}}"에 대한 검색 결과</h2>
{{#res}}
<p>검색량: {{total}}</p>
<div class="row row-cols-1 row-cols-md-2 g-4">
{{#items}}
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{{title}}}</h5>
{{#thumbnail}}
<img src="{{thumbnail}}" class="card-img-top" alt="{{title}}" style="max-width: 200px;">
{{/thumbnail}}
<p class="card-text">{{{description}}}</p>
<a href="{{{link}}}" target="_blank" class="btn btn-secondary">자세히 보기</a>
</div>
</div>
</div>
{{/items}}
</div>
{{/res}}
{{^res}}
<p>검색 결과가 없습니다.</p>
{{/res}}
</div>
<div id="searchResults"></div>
</div>
{{>layouts/footer}}
끝.
이번 예시에서는 내가 뷰를 만들어야해서 객체로 변환한 후 model.addAttribute()로 값을 전달했는데, 이것이 비효율적인것 같다는 생각이 들었다.
(네이버API에서 JSON으로 잘 포장해서 보내줬는데 내가 중간에서 다 풀어헤치고 다시 포장해서 보낸것 같음)
REST API를 설계하는것이라면 객체로 변환하지 않고 JSON 그대로 전달해도 될 것 같다.
'웹개발 > SpringBoot' 카테고리의 다른 글
파일 업로드 구현하기 (0) | 2023.07.27 |
---|---|
검색 기능 적용하기 (0) | 2023.07.26 |
페이징 기능 적용하기 (0) | 2023.07.26 |
swagger 작성해보기, Springboot swagger 연동하기 (0) | 2023.07.05 |
MySQL 설치 및 Spring 연동 방법 (0) | 2023.06.09 |
댓글