Spring/๊ธฐ๋Šฅ ์ •๋ฆฌ

[Spring] - Naver Api๋กœ ์ƒํ’ˆ ๊ฒ€์ƒ‰ ๋ชฉ๋ก ๋ฐ›์•„์˜ค๊ธฐ

Jwhy 2023. 9. 10. 00:27

๐Ÿ’จ ์š”์ฒญ ํ๋ฆ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
  2. ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์˜ ์ œ๋ชฉ๊ณผ ๊ด€๋ จ๋œ ์ƒํ’ˆ ๋ชฉ๋ก ๋ฐ›์•„์˜ค๊ธฐ

Naver Developers ๋“ฑ๋ก

๋งํฌ์— ๋“ค์–ด๊ฐ€ ์‚ฌ์ง„๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•œ๋‹ค.

์Šคํฌ๋ฆฐ์ƒท

ํ˜„์žฌ ํฌ์ŠคํŒ…์—์„œ ์ง„ํ–‰ํ•  ๋‚ด์šฉ์€ ๊ฒ€์ƒ‰ API์ด๋ฉฐ, ์ด ์™ธ์— ํ•„์š”ํ•œ API๋Š” ์ถ”๊ฐ€๋กœ ๋„ฃ์–ด๋„ ๋ฌด๊ด€ํ•˜๋‹ค.

Spring Boot ์ ์šฉ

๋ชจ๋“  ์ž์„ธํ•œ ์„ค๋ช…์€ ๊ณต์‹ ๋ฌธ์„œ์— ์ž˜ ๋‚˜์™€์žˆ์Šต๋‹ˆ๋‹ค!

๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•œ ๋ฒˆ ์ฝ์–ด๋ณด์‹  ํ›„ ์ œ ๊ธ€์„ ๋ด์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค! ๐Ÿ˜ƒ

application.yml

Developers์—์„œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋“ฑ๋กํ•˜๋ฉด, Client ID, Client Secret์„ ๋ฐœ๊ธ‰ํ•ด์ค€๋‹ค.
์ด๋ฅผ application.yml ์„ค์ • ํŒŒ์ผ์— ๋ณต์‚ฌํ•ด์„œ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๋‹จ, ํ…Œ์ŠคํŠธ๋ผ๋„ Secret ํ‚ค๋Š” ์™ธ๋ถ€์— ๊ณต๊ฐœ๋˜๋ฉด ์ข‹์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์„ค์ • ํŒŒ์ผ์€ ๊ฐ€๋Šฅํ•œ .gitignore์— ๋“ฑ๋กํ•ด์ค€๋‹ค.

naver:
  client-id : ...
  client-secret : ...

๋งŒ์•ฝ application.properties๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๋ผ๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

naver.client-id : ...
naver.client-secret : ...

Template ์ƒ์„ฑ

@Component
public class NaverSearchApi {

    @Value("${naver.client-id}")
    private String CLIENT_ID;

    @Value("${naver.client-secret}")
    private String CLIENT_SECRET;

    private static final String REQUEST_URL = "https://openapi.naver.com/v1/search/shop.json?";
    private static final String PARAMETER_INFO = "query=%s&display=%d&start=1&sort=%s";
}

@Component๋ฅผ ํ†ตํ•ด ๋นˆ ๋“ฑ๋ก์„ ํ•ด์ฃผ๊ณ , @Value()๋ฅผ ํ†ตํ•ด ์„ค์ • ํŒŒ์ผ์— ์žˆ๋Š” ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค.
REQUEST_URL์€ ๋ณ€๊ฒฝ๋  ์ผ์ด ์—†๊ณ , ํ˜ธ์ถœ ๋นˆ๋„๊ฐ€ ์žฆ๊ธฐ ๋•Œ๋ฌธ์— static final ํ‚ค์›Œ๋“œ๋ฅผ ๋ถ™์—ฌ ์ •์  ๋ณ€์ˆ˜๋กœ ์ €์žฅํ•ด๋‘”๋‹ค.

@Component
@Slf4j
public class NaverSearchApi {

    // ๊ธฐ์กด ์ฝ”๋“œ ์ƒ๋žต
    ...

    // ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ํ•จ๊ป˜ ๋ณด๋‚ผ ํŒŒ๋ผ๋ฏธํ„ฐ
    // ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๊ณต์‹ ๋ฌธ์„œ ํ™•์ธ
    private final int DISPLAY = 5;
    private final String SORT = "sim";

    /**
     * @apiNote ๋„ค์ด๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋ฉ”์†Œ๋“œ
     * @param keyword ๋„ค์ด๋ฒ„ ์‡ผํ•‘์— ๊ฒ€์ƒ‰ํ•  ํ‚ค์›Œ๋“œ
     * */
    public List<Map<String, String>> getResult(String keyword) {
        // ํ‚ค์›Œ๋“œ๋ฅผ UTF-8 ์ธ์ฝ”๋”ฉ
        String encodedKeyword = keyword;
        try {
            encodedKeyword = URLEncoder.encode(keyword, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        // ์š”์ฒญ URL์— ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๊ทธ์— ๋งž๋Š” ๊ฐ’ ๋งคํ•‘
        String requestUrl = REQUEST_URL + PARAMETER_INFO.formatted(encodedKeyword, DISPLAY, SORT);

        // ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ RestTemplate
        RestTemplate restTemplate = new RestTemplate();

        // ๊ณต์‹ ๋ฌธ์„œ์— ๋‚˜์™€ ์žˆ๋“ฏ์ด, ์š”์ฒญ ํ—ค๋”์— ํด๋ผ์ด์–ธํŠธ ์•„์ด๋”” ๋ฐ ์‹œํฌ๋ฆฟ ์ถ”๊ฐ€
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-Naver-Client-Id", CLIENT_ID);
        headers.set("X-Naver-Client-Secret", CLIENT_SECRET);

        // RestTemplate์„ ์ด์šฉํ•ด ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์•„ ์š”์ฒญ์„ ๋ณด๋‚ด ๊ฐ’์„ String ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜(exchange) ๋ฐ›์Œ
        ResponseEntity<String> responseEntity = restTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class);

        // RestTemplate์„ ํ†ตํ•ด ๋ฐ˜ํ™˜ ๋ฐ›์€ Map์„ JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ ํ›„ ๊ฐ€๊ณตํ•œ ๋‹ค์Œ Controller์— ๋ฐ˜ํ™˜
        return getJsonObject(responseEntity.getBody());
    }
}

์œ„์™€ ๊ฐ™์ด ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋œ๋‹ค.

{
    "lastBuildDate": "Wed, 23 Aug 2023 19:49:07 +0900",
    "total": 8448,
    "start": 1,
    "display": 2,
    "items": [
        {
            "title": "<b>2021 ๋งฅ๋ถ ํ”„๋กœ M2</b> M1 Pro Max 16์ธ์น˜ A2780",
            "link": "...",
            "image": "...",
            "lprice": "21210",
            "hprice": "",
            "mallName": "...",
            "productId": "...",
            "productType": "2",
            "brand": "",
            "maker": "",
            "category1": "๋””์ง€ํ„ธ/๊ฐ€์ „",
            "category2": "๋…ธํŠธ๋ถ",
            "category3": "",
            "category4": ""
        },
        {
            "title": "<b>2021 ๋งฅ๋ถ ํ”„๋กœ M2</b> M1 Pro Max 14์ธ์น˜ A",
            "link": "...",
            "image": "...",
            "lprice": "22380",
            "hprice": "",
            "mallName": "...",
            "productId": "...",
            "productType": "2",
            "brand": "",
            "maker": "",
            "category1": "๋””์ง€ํ„ธ/๊ฐ€์ „",
            "category2": "๋…ธํŠธ๋ถ",
            "category3": "",
            "category4": ""
        }
    ]
}

์ด์ œ ์šฐ๋ฆฌ์—๊ฒŒ ํ•„์š”ํ•œ items์— ์žˆ๋Š” ๋‚ด์šฉ๋งŒ ๋นผ๋ฉด ๋œ๋‹ค.
์ดํ›„ ์ฝ”๋“œ์—์„œ List<Map<String, String>> ํ˜•ํƒœ๊ฐ€ ๋‚˜์˜ค๋Š”๋ฐ ํ•ด๋‹น ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ดํ•ดํ•˜๋ฉด ๋œ๋‹ค.

ํ•˜๋‚˜์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ Key : Value ํ˜•ํƒœ๋กœ ํ•ฉ์ณ์ ธ ํ•˜๋‚˜์˜ Map์„ ์ด๋ฃธ

{
    "title": "<b>2021 ๋งฅ๋ถ ํ”„๋กœ M2</b> M1 Pro Max 16์ธ์น˜ A2780",
    "link": "...",
    "image": "...",
    "lprice": "21210",
    "hprice": "",
    "mallName": "...",
    "productId": "...",
    "productType": "2",
    "brand": "",
    "maker": "",
    "category1": "๋””์ง€ํ„ธ/๊ฐ€์ „",
    "category2": "๋…ธํŠธ๋ถ",
    "category3": "",
    "category4": ""
}

์ฆ‰, List ์ž์ฒด์—๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋‹ด๊ธฐ๋Š” ๊ฒƒ์ด๊ณ ,
Map ์ž์ฒด์—๋Š” ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ƒ์„ธ ๊ฐ’๋“ค์ด ๋‹ด๊ธฐ๋Š” ๊ฒƒ์ด๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•  JSON์€ json-simple ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

implementation 'com.googlecode.json-simple:json-simple:1.1.1'

๊ทธ๋Ÿผ ์œ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ๊ฐ€์ ธ์™€ ๊ฐ€๊ณตํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž!

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

public class NaverSearchApi {

    // ๊ธฐ์กด ์ฝ”๋“œ ์ƒ๋žต
    ...

    /**
     * @apiNote RestTemplate์„ ํ†ตํ•ด ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜๋Š” ๋ฉ”์†Œ๋“œ
     * @param resultData RestTemplate์„ ํ†ตํ•ด ๋ฐ›์•„์˜จ JSON ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด ๋ฐ์ดํ„ฐ
     * */
    private List<Map<String, String>> getJsonObject(String resultData) {
        // Key : Value ์ง์œผ๋กœ ๋œ ํ•˜๋‚˜์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
        List<Map<String, String>> extractedItems = new ArrayList<>();

        // ์•ž์„œ RestTemplate์„ ํ†ตํ•ด ๋ฐ˜ํ™˜ ๋ฐ›์€ ๋ฌธ์ž์—ด์„ JSONObject(Json ๊ฐ์ฒด)๋กœ ๋ณ€ํ™˜
        JSONObject json = (JSONObject) JSONValue.parse(resultData);

        // ๋ณ€ํ™˜ํ•œ JSON ๊ฐ์ฒด์—์„œ ํ‚ค ๊ฐ’์ด item์ธ ๋ถ€๋ถ„๋งŒ ๊ฐ€์ ธ์˜ด
        JSONArray documents = (JSONArray) json.get("items");

        // item์— ๋‹ด๊ธด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํƒ์ƒ‰ํ•˜๋ฉฐ, ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€๊ณต
        for (Object item : documents) {

            // ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด Map ์ƒ์„ฑ 
            Map<String, String> map = new LinkedHashMap<>();

            // JSONArray์—์„œ ๊บผ๋‚ด์˜จ ๊ฐ์ฒด๊ฐ€ Object ํƒ€์ž…์ด๊ธฐ ๋•Œ๋ฌธ์— JSONObject๋กœ ๋ณ€ํ™˜
            JSONObject obj = (JSONObject) item;

            // ํ•„์š”ํ•œ ๊ฐ’๋“ค๋งŒ ๊ฐ€์ ธ์™€ map์— ์ถ”๊ฐ€
            map.put("title", (String) obj.get("title"));
            map.put("link", (String) obj.get("link"));
            map.put("brand", (String) obj.get("brand"));
            map.put("lprice", (String) obj.get("lprice"));

            // ์™„์„ฑ๋œ map์„ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฌผ List์— ์ถ”๊ฐ€
            extractedItems.add(map);
        }
        return extractedItems;
    }
}

์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ•˜๋ฉด, ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค.

{
    "title": "Apple <b>๋งฅ๋ถ</b> <b>ํ”„๋กœ</b> 14 2023๋…„ <b>M2 Pro</b> 10์ฝ”์–ด ์‹ค๋ฒ„ (MPHH3KH/A)",
    "link": "...",
    "brand": "Apple",
    "lprice": "2600000"
},
{
    "title": "MNW93KH/A Apple <b>๋งฅ๋ถ</b><b>ํ”„๋กœ</b><b>m2</b> <b>๋งฅ๋ถ</b>16์ธ์น˜ 2023 <b>M2 Pro</b> 1tb ๊ทธ๋ ˆ์ด",
    "link": "...",
    "brand": "Apple",
    "lprice": "3189000"
},
{
    "title": "Apple <b>๋งฅ๋ถ</b> <b>ํ”„๋กœ</b> 16 2023๋…„ <b>M2 Pro</b> 12์ฝ”์–ด ์ŠคํŽ˜์ด์Šค ๊ทธ๋ ˆ์ด (MNW93KH/A)",
    "link": "...",
    "brand": "Apple",
    "lprice": "3250000"
},
{
    "title": "Apple <b>๋งฅ๋ถ</b> <b>ํ”„๋กœ</b> 14 2023๋…„ <b>M2 Pro</b> 10์ฝ”์–ด ์ŠคํŽ˜์ด์Šค ๊ทธ๋ ˆ์ด (MPHE3KH/A)",
    "link": "...",
    "brand": "Apple",
    "lprice": "2542000"
},
{
    "title": "Apple 2022 <b>๋งฅ๋ถ</b> <b>ํ”„๋กœ</b> 13 <b>M2</b>  ์ŠคํŽ˜์ด์Šค ๊ทธ๋ ˆ์ด  GPU 10์ฝ”์–ด  256GB  24GB  Z16R0001M",
    "link": "...",
    "brand": "Apple",
    "lprice": "2200000"
}

์ „์ฒด ์ฝ”๋“œ๋Š” Github๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!

๐Ÿค” ํšŒ๊ณ 

์กฐ๊ธˆ ๋” ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ๊ฐ€๊ณตํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ณต๋ถ€ํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™๋‹ค!