한국투자증권 API 주식 조회 기능

728x90
반응형

이전에 우리가 한국투자증권 API 신청하고 토큰 발급까지 진행했었다. 이번에는 종목 주가 정보를 조회하는 기능을 추가해보겠다.

 

먼저 application.yml에 문구를 하나 더 추가해야한다.

kis:
  base-url: "https://openapi.koreainvestment.com:9443"
  auth-url: "/oauth2/tokenP"
  app-key: "요기에 appkey"
  app-secret: "요기에 appsecretkey"
  price-url: "/uapi/domestic-stock/v1/quotations/inquire-price"

 

바로 저 price-url을 추가해야한다. 저 우측에 있는 값이 무엇이냐 하면...

 

 

한국투자증권 API에서 저렇게 하라고 해서 추가한 것이다. 어쩌겠는가 이렇게 하라면 하자.

 

https://apiportal.koreainvestment.com/apiservice-apiservice?/uapi/domestic-stock/v1/quotations/inquire-price

 

KIS Developers

한국투자증권 Open API 포탈

apiportal.koreainvestment.com

 

이번에 주식 현재가 조회 기능의 경우 우리 DB에서 불러오는 것이 아닌 WebClient를 통해 한국투자증권 API를 사용해서 정보를 불러오는 것이기 때문에 Entity를 만들 필요는 없다. DTO만 만들면 충분하다.

 

@Getter
@NoArgsConstructor
public class StockPriceResponse {
    @JsonProperty("stck_shrn_iscd") // 주식 종목 코드
    private String stockCode;

    @JsonProperty("rprs_mrkt_kor_name") // 시장명 : 코스피인지 코스닥인지
    private String marketName; 

    @JsonProperty("stck_prpr") //주식 현재가
    private String stockPriceRaw;

    @JsonProperty("prdy_vrss") //전일 대비
    private String priceChangeRaw;

    @JsonProperty("prdy_ctrt") // 전일 대비율
    private String changeRateRaw;

    @JsonProperty("acml_vol") // 누적 거래량
    private String accumulatedVolumeRaw;

    @JsonProperty("stck_oprc") // 시가
    private String openingPriceRaw;
}

 

@Getter
@NoArgsConstructor
public class KisPriceApiResponse {
    @JsonProperty("output")
    private StockPriceResponse output;
}

 

먼저 나는 KisPriceApiResonse라는 DTO를 만들고, StockPriceResponse라는 DTO를 만들었다.

 

 

이렇게 만든 이유는 한국투자증권 API의 경우 Body에 rt_cd(성공 실패 여부), msg_cd(응답 코드), msg1(응답 메세지), output(응답 상세)로 이루어져 있는데, 난 주식 현재가나 전일 대비 등의 정보가 들어있는 output만 필요하기 때문에 output으로 먼저 감싸주고, 그 다음에 내가 필요한 필드들을 추가했다.

 

@JsonProperty 어노테이션은 Jackson 라이브러리에서 자바 객체와 JSON 간의 매핑을 정의할 때 사용하는 어노테이션이다. 즉, JSON 키와 자바 필드를 연결해주는 역할을 한다. 즉, 한국투자증권 API의 Element에 있는 이름과 내가 만든 필드를 연결해주는 기능을 하는 것이다. 

 

주의 : Element의 값은 정해져있는 것이기 때문에 @JsonProperty 어노테이션 안에 있는 값의 이름은 틀리면 안된다.

 

@Service
@RequiredArgsConstructor
@Slf4j
public class KisStockService {
    private final WebClient webClient;
    private final KisAuthService kisAuthService;
    private final KisApiProperties kisApiProperties;
    private final StockRepository stockRepository;

    public StockPriceDto getStockPrice(String stockCode) {
        String accessToken = kisAuthService.getAccessToken();

        Stock stock = stockRepository.findByStockCode(stockCode)
                .orElseThrow(() -> new BusinessException(KisApiErrorCode.STOCK_NOT_FOUND));


        KisPriceApiResponse apiResponse = webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .path(kisApiProperties.getPriceUrl())
                        .queryParam("fid_cond_mrkt_div_code", "J") // 조건 시장 분류 코드(J:KRX, NX:NXT, UN:통합)
                        .queryParam("fid_input_iscd", stockCode) // 입력 종목코드(종목코드 (ex 005930 삼성전자))
                        .build())
                .header("Authorization", accessToken) // 접근 토큰
                .header("appKey", kisApiProperties.getAppkey()) //앱키
                .header("appSecret", kisApiProperties.getAppsecret()) //앱시크릿키
                .header("tr_id", "FHKST01010100") //거래ID
                .retrieve()
                .bodyToMono(KisPriceApiResponse.class)// 서버에서 온 JSON을 DTO 클래스(KisPriceApiResponse)로 매핑
                .onErrorMap(error -> {
                    log.error("{}의 주식 가격을 불러오는 데 실패했습니다. 에러: {}", stockCode, error.getMessage());
                return new BusinessException(KisApiErrorCode.STOCK_PRICE_FETCH_FAILED);
                })
                .block(); //동기식 객체로 변환

        StockPriceResponse raw = apiResponse.getOutput();

        return new StockPriceDto(
                raw.getStockCode(),
                stock.getStockName(),
                raw.getMarketName(),
                raw.getStockPriceRaw(),
                raw.getPriceChangeRaw(),
                raw.getChangeRateRaw(),
                raw.getAccumulatedVolumeRaw(),
                raw.getOpeningPriceRaw()
        );
    }
}

 

다음은 서비스 단이다. stockCode(종목 코드)를 받아서 주식 가격과 정보들을 DTO로 반환하는 메소드를 만들어서 진행하였다. 이전에 만든 접근 토큰을 가져와서 한국투자증권 API를 호출하는 방식이다. 쿼리 파라미터나, 헤더에 들어있는 정보들은 필수인 값들은 모두 넣어줘야 한다. 단 content-type의 경우 기본값이 지정되어있어서 생략해도 문제가 없다. 주식 API 정보를 가져올 때에는 API 문서를 잘 확인하는 것이 매우 중요하다.

 

그런데 여기서 작은 문제가 있었다. 한국투자증권 API 주식현재가 시세 API 문서를 보면 종목 이름을 불러올 수 있는 기능이 없다.... 왜 없는지는 나도 몰?루

 

그래서 나는 어떻게 해결했느냐. 바로 종목 코드와 기업 이름은 내 DB에 저장해서 불러오는 방식을 사용했다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "stock_id")
    private Long stockId;

    @Column(nullable = false, unique = true)
    private String stockCode;

    @Column(nullable = false)
    private String stockName;
}

 

그러기 위해 먼저 주식 Entity를 새로 생성한다. 그러면 내 데이터베이스에 stock 테이블이 생성된다.

그 다음, 종목 코드와 기업 이름을 내 데이터베이스에 삽입해줘야 하는데.... 이 정보들을 어떻게 불러와야 하나 고민을 많이 했다.

 

chatGPT를 사용해서 긁어오려고 했는데, 내가 무료버전이여서 그런가 SQL 파일로 주지를 않는 것이다. 근데 백수가 무슨 돈이 있어서 유료 결제를 하겠는가.

 

그래서 나는 파이썬을 사용해서 웹 크롤링으로 네이버페이 증권에서 종목 코드와 기업명 정보들을 불러와서 이걸 SQL 문으로 변환하는 프로그램을 사용하기로 결정했다.

 

pip install requests beautifulsoup4 pandas tqdm
pip install lxml

 

먼저 라이브러리들을 설치한다.

 

 

import requests
import pandas as pd
from bs4 import BeautifulSoup
from tqdm import tqdm

def get_stock_list(market_code):
    """
    네이버 금융에서 시장별(KOSPI: 0, KOSDAQ: 1) 종목 목록을 가져오는 함수
    """
    stocks = []
    base_url = "https://finance.naver.com/sise/sise_market_sum.naver"
    params = {'sosok': market_code, 'page': 1}

    # 첫 페이지 요청
    response = requests.get(base_url, params=params)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "lxml")

    # 마지막 페이지 번호 추출
    last_page_tag = soup.select_one("td.pgRR a")
    if last_page_tag:
        last_page = int(last_page_tag['href'].split('page=')[-1])
    else:
        last_page = 1

    market_name = "KOSPI" if market_code == 0 else "KOSDAQ"

    for page in tqdm(range(1, last_page + 1), desc=f"Scraping {market_name} stocks"):
        params['page'] = page
        response = requests.get(base_url, params=params)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "lxml")

        # 테이블 찾기
        table = soup.find("table", {"class": "type_2"})
        if not table:
            continue

        tbody = table.find("tbody")
        if not tbody:
            continue

        rows = tbody.find_all("tr")
        for row in rows:
            cols = row.find_all("td")
            if len(cols) < 2:
                continue
            a_tag = cols[1].find("a")  # 종목명 컬럼
            if a_tag and 'href' in a_tag.attrs:
                href = a_tag['href']
                stock_code = href.split("code=")[-1]
                stock_name = a_tag.text.strip()
                stock_name = stock_name.replace("'", "''")  # SQL 이스케이프
                stocks.append({'code': stock_code, 'name': stock_name})

    return stocks

def create_sql_file(all_stocks):
    """
    수집된 종목 목록으로 SQL INSERT 문을 생성하여 파일에 저장
    """
    filename = "stocks.sql"
    with open(filename, "w", encoding="utf-8") as f:
        f.write("-- 네이버 증권 상장 기업 목록 SQL 스크립트\n")
        f.write("-- 테이블 생성 예시:\n")
        f.write("-- CREATE TABLE stock (\n")
        f.write("--     id INT AUTO_INCREMENT PRIMARY KEY,\n")
        f.write("--     stock_code VARCHAR(10) NOT NULL UNIQUE,\n")
        f.write("--     stock_name VARCHAR(255) NOT NULL\n")
        f.write("-- );\n\n")

        f.write("INSERT INTO stock (stock_code, stock_name) VALUES\n")
        insert_statements = []
        for stock in all_stocks:
            insert_statements.append(f"('{stock['code']}', '{stock['name']}')")
        f.write(",\n".join(insert_statements))
        f.write(";\n")

    print(f"\nSQL 파일 '{filename}' 생성 완료!")
    print(f"총 {len(all_stocks)}개의 종목 정보 저장됨.")

if __name__ == "__main__":
    print("네이버 금융에서 KOSPI 종목 수집 중...")
    kospi_stocks = get_stock_list(0)
    print("네이버 금융에서 KOSDAQ 종목 수집 중...")
    kosdaq_stocks = get_stock_list(1)

    all_stocks = kospi_stocks + kosdaq_stocks

    # 중복 제거
    unique_stocks = list({s['code']: s for s in all_stocks}.values())

    create_sql_file(unique_stocks)

 

그 다음 파이썬을 사용해서 네이버페이 증권에서 종목 코드와 기업 이름을 웹 크롤링으로 수집해오고, 이걸 SQL 파일로 저장하는 방식을 사용했다. (역시 파이썬은 신이다.)

숭배해야해

 

return new StockPriceDto(
                raw.getStockCode(),
                stock.getStockName(),
                raw.getMarketName(),
                raw.getStockPriceRaw(),
                raw.getPriceChangeRaw(),
                raw.getChangeRateRaw(),
                raw.getAccumulatedVolumeRaw(),
                raw.getOpeningPriceRaw()
        );

 

그래서 서비스단으로 보면 getStockName은 StockPriceResponse의 raw가 아닌 stock인 것이다.

 

 

최종적으로 다음과 같이 결과가 잘 나오는 것을 확인할 수 있다.

728x90
반응형