02. [Springboot] 공공데이터포탈 API 사용한 Controller 만들기 (API 금융위원회_주식시세정보)

zero zoo·2024년 5월 4일
0
post-thumbnail

시작

공공데이터포탈 접속 : https://www.data.go.kr/index.do
포탈 접속 이후 회원가입/로그인 진행을 먼저합니다
그리고 사용하고 싶은 데이터를 검색하여 선정, 상세정보 페이지로 들어갑니다

작성자는 주식 차트를 그리고 싶기 때문에, 금융위원회에서 제공하는 주식시세정보 데이터를 가져오기로 했습니다. (링크 : https://www.data.go.kr/data/15094808/openapi.do)
그리고 이 페이지에서 활용신청 버튼을 누릅니다

신청 페이지에서 내리다보면, 무언가 입력 작성하는 부분이 있는데,
내용을 보고 심사하거나 하지는 않는 것 같습니다.
첨부파일도 필수가 아니니, 적당한 내용으로 채워넣고 활용신청 빠르게 진행합니다

마이페이지 > 활용신청 현황 > 활용 n건 > 아래 스크롤하여 신청한 데이터 클릭
상세한 API 정보를 볼 수 있습니다

중요한 것은 하단의 End Point와 인증키!

End Point 는 데이터 요청 url을 만들 때 base url이 됩니다.
(End Point로 제공된 url 뒤에 '?' 을 붙이고, &[항목명]=[값] 형식으로 뒤로 계속 붙여줍니다.)

Encoding을 한 것과 하지 않은 버전 두가지가 제공되는데,
우리는 일반인증키(Decoding) 을 사용할 것입니다.

(위 페이지는 활용기간동안 만료되거나 하지 않고 계속 계속 볼 수 있습니다)

그러면 공공데이터 API 사용을 위한 준비는 끝났습니다!
이 데이터를 받기 위한 JAVA 코드를 작성해보도록 합시다


주요 코드

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

private String callAPI(String requestUrl) {
        
    // 결과 값을 받기 위한 String 변수들
    StringBuilder result = new StringBuilder();
    String returnLine;

    try {
    	// REST 통신을 위해, 이미 만들어진 url 이용하여 URL 구조체 선언
        URL url = new URL(requestUrl);

    	// url 스트림 open 및 연결
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.connect();
			
        // REST 통신 결과값 받기 위한 Buffer 선언
        BufferedInputStream bufferedInputStream = new BufferedInputStream(urlConnection.getInputStream());
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream, "UTF-8"));
			
        // 마지막줄이 나올 때까지 전송 결과 result 에 계속 작성
        while ((returnLine = bufferedReader.readLine()) != null) {
            result.append(returnLine);
        }

    } catch (Exception e) {
        e.printStackTrace();
    }
    return result.toString();
}

HTTP 프로토콜로 통신을 위해 함수 input으로 완성된 링크를 받고, 위와 같은 순서로 통신을 진행합니다.

참고로, URLConnection은 애플리케이션과 URL 간의 통신 링크를 나타내는 모든 클래스의 슈퍼 클래스입니다. URL이 리키는 리소스와 관련된 연결을 나타냅니다.

HttpURLConnection 은 URLConnection의 하위 클래스로, HTTP 프로토콜을 사용하여 특정 웹 서버와 통신합니다. HTTP 메서드 GET, POST, PUT, DELETE 등의 HTTP 요청과 응답을 처리할 수 있는 메서드들을 제공합니다.

이후 해당 리소스를 GET방식(urlConnection.getInputStream)으로 읽어오고, 마지막줄이 나올 때까지 StringBuilder 객체로 정의된 전송 결과(result)에 계속 작성하며 받아옵니다.

앞서 API 쿼리에 parameter에서 resultType을 json으로 잘 넣었다면, 결과값은 json형태로 나올 것입니다.

JSON 형식의 데이터 분석을 위해 StringBuilder 객체의 result 를 String 형식으로 다시 parse 하였습니다.


java springboot로 공공데이터API 불러와 저장하기

웹에서 종목코드(itemId) 와 조회 일수(day)를 input으로 넘겨주면,
해당 종목코드의 해당하는 데이터 중 최근 day 일수 만큼의 주식 차트를 그려주려 합니다.

또한, 휘발성이긴 하지만 임시 Repository를 만들어 주식데이터를 저장,
이 데이터들을 불러와 차트를 그릴겁니다.
(Repository와 Controller 관련 내용은 코드에 포함되어 있지만, 별도 포스트로 따로 다루도록 하겠습니다.)

RestController 선언(Controller, Model, Service)
@RestController
public class APIController {
    private final PriceService priceService;

    @Autowired
    public APIController(PriceService priceService) {
        this.priceService = priceService;
    }
...

Controller 외에도 주식 데이터를 일별로 나눠 Class(Price) 형식으로 받고, 이를 사용하는 함수들을 PriceService에 저장하였기에 위와 같이 Controller 정의를 시작하였습니다.

serviceKey 사용시 주의할 점

/* 퍼센트 인코딩된 쿼리에 + 를 %2B 로 직접 변환해 줌으로써 해결 */
//    String SERVICEKEY = "n6~~~+Lvw==";
    String SERVICEKEY = "n6~~~%2BLvw==";

공공데이터포털에서 제공하는 apiKey는 'W3C recommendations for URI addressing'에 따라 +를 %2B로 변환하는데,
URLComponents에서는 그냥 + 그대로 뒀기 때문에 키가 틀리는 오류가 아래와 같이 발생합니다.
퍼센트 인코딩된 쿼리에 + 를 %2B 로 직접 변환해 줌으로써 해결합니다...

웹 코드(js) 에서 사용하게될 api 정의

@GetMapping("/api/apiRequest")
    public String getStockPrice(
            @RequestParam String itemId, 	// 종목코드 
            @RequestParam String day		// 불러올 데이터 일 수 
    ) throws IOException, ParseException {
    
    	// 결과 값을 받기 위한 String 변수들
        String returnString;
        String resultString;

		// 주식데이터 Repository 초기화
        priceService.clearStore();

        // API 호출할 url 만들기
        String requestUrl = makePriceUrl(SERVICEKEY, "400", "1", itemId, day);

        // API 호출하기
        returnString = callAPI(requestUrl);

        // Repository 에 투자정보 저장하기
        resultString = savePriceData(returnString);

        return resultString;
    }

공공데이터 API를 호출하기 위한 url을 만들고, 호출한 후, 이를 저장하는 순서로 진행됩니다.
@GetMapping을 통해 요청을 받을 것입니다.

RequestMethod의 경우, GET, POST, PUT, DELETE 등 총 8가지가 있고,
@RequestMapping(method = RequestMethod.GET, value = "/input")
위와 같이 쓰는 방법도 있지만,
@GetMapping과 같이 메서드 옵션이 적용된 어노테이션이 있기 때문에 이 편이 더 효율적이고 유용합니다.

REST 전송을 위한 url 생성

// REST 전송을 위한 url 생성
private String makePriceUrl(String serviceKey, String numOfRows, String pageNo, String itemId, String day) {
    String baseUrl = "https://apis.data.go.kr/1160100/service/GetStockSecuritiesInfoService/getStockPriceInfo?";
    String returnUrl = baseUrl + "serviceKey=" + serviceKey + "&numOfRows=" + numOfRows + "&pageNo=" + pageNo;

    String todayDate = todayDate();
    String prevDate = prevDate(Integer.parseInt(day));

    returnUrl = returnUrl + "&resultType=json";
    returnUrl = returnUrl + "&likeSrtnCd=" + itemId;
    returnUrl = returnUrl + "&beginBasDt=" + prevDate + "&endBasDt=" + todayDate;

	return returnUrl;
}

기본적으로 공공데이터API는 serviceKey(인증키) , numOfRows(한 페이지에 제공받을 데이터 수), pageNo(페이지 수) 를 필수로 받습니다.

또한, 여러 타입의 데이터(XML, JSON 등) 를 제공하는 API의 경우, resultType를 통해 제공받을 데이터의 형식을 지정할 수 있습니다.

추가로, 작성자가 선택한 데이터는 likeSrtnCd(종목코드) beginBasDt(시작일자) endBasDt(종료일자) 등의 선택 데이터를 통해 제공 데이터에 조건 필터를 줄 수 있었습니다.

input을 더 추가하고 싶으면, url 뒤에 &[항목명]=[값] 형식으로 url 을 늘려줍니다.

API를 호출

private String callAPI(String requestUrl) {
        
    // 결과 값을 받기 위한 String 변수들
    StringBuilder result = new StringBuilder();
    String returnLine;

    try {
    	// REST 통신을 위해, 이미 만들어진 url 이용하여 URL 구조체 선언
        URL url = new URL(requestUrl);

    	// url 스트림 open 및 연결
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.connect();
			
        // REST 통신 결과값 받기 위한 Buffer 선언
        BufferedInputStream bufferedInputStream = new BufferedInputStream(urlConnection.getInputStream());
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream, "UTF-8"));
			
        // 마지막줄이 나올 때까지 전송 결과 result 에 계속 작성
        while ((returnLine = bufferedReader.readLine()) != null) {
            result.append(returnLine);
        }

    } catch (Exception e) {
        e.printStackTrace();
    }
    return result.toString();
}

HTTP 프로토콜로 통신을 위해 함수 input으로 완성된 링크를 받고, 위와 같은 순서로 통신을 진행합니다.

위에 설명이 있으니 자세한 사항은 pass

JSON 형식의 API output 저장하기

// API에서 받은 데이터 repository 에 차례대로 저장
private String savePriceData(String data) throws ParseException {
    JSONObject dataJson = jsonParser(data);
    Object response = dataJson.get("response");
    Object body = ((JSONObject) response).get("body");

    // 결과 데이터 중 JSONObject Array의 길이 output
    int totalCount = Integer.parseInt(((JSONObject) body).get("totalCount").toString());
    List<Price> outputList;

	Object items = ((JSONObject) body).get("items");
    Object item = ((JSONObject) items).get("item");
	
    // 결과 데이터 중 JSONObject Array 형식의 결과값
	JSONArray itemConverted = (JSONArray) item;

	int i;
	for (i = 0; i < totalCount; i++) {
        JSONObject element = (JSONObject) itemConverted.get(i);
        
        // 미리 정의된 class안 Price 에 필요한 인자만 get하여 저장
		Price add = new Price();
        add.setItemId((String) element.get("srtnCd"));
        add.setName((String) element.get("itmsNm"));
        add.setDate((String) element.get("basDt"));
        add.setOpen(Integer.parseInt((String) element.get("mkp")));
        add.setClose(Integer.parseInt((String) element.get("clpr")));
        add.setHigh(Integer.parseInt((String) element.get("hipr")));
        add.setLow(Integer.parseInt((String) element.get("lopr")));

        priceService.addDayPrice(add);
    }
    outputList = priceService.listAll();
    
    List<Object> result;
    result = chartParser(outputList, totalCount);
    return result.toString();
}

사용자별로 API 결과값 사용방법이 다르겠지만, 작성자는 Spring 프레임워크의 Repository에 결과값들을 저장 후 이를 불러오는 방식으로 사용하였습니다.
........ Springboot 공부목적이었습니다

이후 chartParser 라는 자체 함수를 통해 내가 원하는 방식의 JSONObject를 String으로 적어 List로 만들고, return 하였습니다.

이외의 함수들

// 오늘 날짜 데이터 형식에 맞게 받아 return
private String todayDate() {
    LocalDate today = LocalDate.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
    return formatter.format(today);
}

// 오늘 기준 day 일 이전의 날짜 데이터 형식에 맞게 받아 return
private String prevDate(int day) {
    LocalDate dateVal = LocalDate.now();
    dateVal = dateVal.minusDays(day);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
    return formatter.format(dateVal);
}

// String 으로 된 JSON 오브젝트 내용 받아 JSON 오브젝트 형식으로 parse
private JSONObject jsonParser(String data) throws ParseException {
    JSONParser jsonParser = new JSONParser();
    Object obj = jsonParser.parse(data);
    JSONObject jsonObj = (JSONObject) obj;
    return jsonObj;
}

// List<Price> 형식으로 정리된 데이터 웹에서 필요하고 받을 수 있는 Sting 형식으로 parse
private List<Object> chartParser(List<Price> listPrice, int cnt) {
    List<Object> result = new ArrayList<>(cnt);
    System.out.println("cnt : " + cnt);

    int i;
    for (i = 0; i < cnt; i++) {
        String obj = "{\"name\":\"" + listPrice.get(i).getName() +
                "\",\"date\":\"" + listPrice.get(i).getDate() +
                "\",\"openClose\":["
                + listPrice.get(i).getOpen() + "," + listPrice.get(i).getClose()
                + "],\"highLow\":["
                + listPrice.get(i).getHigh() + "," + listPrice.get(i).getLow()
                + "]}";
        result.add(obj);
    }
    return result;
}

이로써 공공데이터포탈 API 사용하여 Springboot 데이터 조회 Controller 만든 과정을 정리하였습니다.
이후 포스트에서는 이 결과 데이터를 받는 Repository와 이 데이터로 차트를 그리는 코드에 대한 리뷰를 남기겠습니다.
:)


전체코드 (APIController.java)

@RestController
public class APIController {

    private final PriceService priceService;

    @Autowired
    public APIController(PriceService priceService) {
        this.priceService = priceService;
    }

    /* 공공데이터포털에서 제공하는 apiKey는 'W3C recommendations for URI addressing'에 따라 +를 %2B로 변환하는데,
    URLComponents에서는 그냥 + 그대로 뒀기 때문에 키가 틀리는 오류가 발생.
    퍼센트 인코딩된 쿼리에 + 를 %2B 로 직접 변환해 줌으로써 해결 */
//    String SERVICEKEY = "n6~~~+Lvw==";
    String SERVICEKEY = "n6~~~%2BLvw==";

    @GetMapping("/api/apiRequest")
    public String getStockPrice(
            @RequestParam String itemId, 	// 종목코드 
            @RequestParam String day		// 불러올 데이터 일 수 
    ) throws IOException, ParseException {
    
    	// 결과 값을 받기 위한 String 변수들
        String returnString;
        String resultString;

		// 주식데이터 Repository 초기화
        priceService.clearStore();

        // API 호출할 url 만들기
        String requestUrl = makePriceUrl(SERVICEKEY, "400", "1", itemId, day);

        // API 호출하기
        returnString = callAPI(requestUrl);

        // Repository 에 투자정보 저장하기
        resultString = savePriceData(returnString);

        return resultString;
    }

    private String callAPI(String requestUrl) {
        
        // 결과 값을 받기 위한 String 변수들
        StringBuilder result = new StringBuilder();
        String returnLine;

        try {
        	// REST 통신을 위해, 이미 만들어진 url 이용하여 URL 구조체 선언
            URL url = new URL(requestUrl);

        	// url 스트림 open 및 연결
            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.connect();
			
            // REST 통신 결과값 받기 위한 Buffer 선언
            BufferedInputStream bufferedInputStream = new BufferedInputStream(urlConnection.getInputStream());
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream, "UTF-8"));
			
            // 마지막줄이 나올 때까지 전송 결과 result 에 계속 작성
            while ((returnLine = bufferedReader.readLine()) != null) {
                result.append(returnLine);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return result.toString();
    }
	
    // REST 전송을 위한 url 생성
    private String makePriceUrl(String serviceKey, String numOfRows, String pageNo, String itemId, String day) {
        String baseUrl = "https://apis.data.go.kr/1160100/service/GetStockSecuritiesInfoService/getStockPriceInfo?";
        String returnUrl = baseUrl + "serviceKey=" + serviceKey + "&numOfRows=" + numOfRows + "&pageNo=" + pageNo;

        String todayDate = todayDate();
        String prevDate = prevDate(Integer.parseInt(day));

        returnUrl = returnUrl + "&resultType=json";
        returnUrl = returnUrl + "&likeSrtnCd=" + itemId;
        returnUrl = returnUrl + "&beginBasDt=" + prevDate + "&endBasDt=" + todayDate;

        System.out.println("url : " + returnUrl);
        return returnUrl;
    }
	
    // 오늘 날짜 데이터 형식에 맞게 받아 return
    private String todayDate() {
        LocalDate today = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        return formatter.format(today);
    }
    
    // 오늘 기준 day 일 이전의 날짜 데이터 형식에 맞게 받아 return
    private String prevDate(int day) {
        LocalDate dateVal = LocalDate.now();
        dateVal = dateVal.minusDays(day);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        return formatter.format(dateVal);
    }
    
    // String 으로 된 JSON 오브젝트 내용 받아 JSON 오브젝트 형식으로 parse
    private JSONObject jsonParser(String data) throws ParseException {
        JSONParser jsonParser = new JSONParser();
        Object obj = jsonParser.parse(data);
        JSONObject jsonObj = (JSONObject) obj;
        return jsonObj;
    }

	// List<Price> 형식으로 정리된 데이터 웹에서 필요하고 받을 수 있는 Sting 형식으로 parse
    private List<Object> chartParser(List<Price> listPrice, int cnt) {
        List<Object> result = new ArrayList<>(cnt);
        System.out.println("cnt : " + cnt);

        int i;
        for (i = 0; i < cnt; i++) {
            String obj = "{\"name\":\"" + listPrice.get(i).getName() +
                    "\",\"date\":\"" + listPrice.get(i).getDate() +
                    "\",\"openClose\":["
                    + listPrice.get(i).getOpen() + "," + listPrice.get(i).getClose()
                    + "],\"highLow\":["
                    + listPrice.get(i).getHigh() + "," + listPrice.get(i).getLow()
                    + "]}";
            result.add(obj);
        }
        return result;
    }
    
    // API에서 받은 데이터 repository 에 차례대로 저장
    private String savePriceData(String data) throws ParseException {
        JSONObject dataJson = jsonParser(data);
        Object response = dataJson.get("response");
        Object body = ((JSONObject) response).get("body");

        int totalCount = Integer.parseInt(((JSONObject) body).get("totalCount").toString());
        List<Price> outputList;

        Object items = ((JSONObject) body).get("items");
        Object item = ((JSONObject) items).get("item");

        JSONArray itemConverted = (JSONArray) item;

        int i;
        for (i = 0; i < totalCount; i++) {
            JSONObject element = (JSONObject) itemConverted.get(i);
            Price add = new Price();
            add.setItemId((String) element.get("srtnCd"));
            add.setName((String) element.get("itmsNm"));
            add.setDate((String) element.get("basDt"));
            add.setOpen(Integer.parseInt((String) element.get("mkp")));
            add.setClose(Integer.parseInt((String) element.get("clpr")));
            add.setHigh(Integer.parseInt((String) element.get("hipr")));
            add.setLow(Integer.parseInt((String) element.get("lopr")));

            priceService.addDayPrice(add);

        }
        outputList = priceService.listAll();
        List<Object> result;
        result = chartParser(outputList, totalCount);
        return result.toString();
    }

}
profile
한방향으로 지그재그

0개의 댓글