본문 바로가기
파이썬 주식 자동매매 봇 만들기 프로젝트

[파이썬 주식 자동매매 봇 만들기 #2] 마의 구간: 접근 토큰(Access Token) 발급과 400 Bad Request의 늪

by triz-hong 2026. 5. 29.

지난 1편에서는 한국투자증권 개발자 센터(KIS Developers)에서 App Key와 App Secret을 발급받고, 프로젝트의 기반이 될 가상환경과 환경변수(.env) 세팅까지 완료했습니다. 기초 공사가 끝났으니 이제 호기롭게 코드 몇 줄 짜서 바로 현재가를 조회해 볼 수 있을 것이라 생각했습니다. 일과 시간 중에 스마트폰을 자유롭게 보지 못하는 답답한 환경을 극복하기 위해, 한시라도 빨리 스스로 구동되는 무인 시스템을 완성하고 싶었기 때문입니다.

하지만 KIS API의 세계는 생각보다 호락호락하지 않았습니다. 증권사 서버에 본인 인증을 요청하는 가장 첫 단계인 접근 토큰(Access Token) 발급에서부터, 저는 터미널 창을 가득 채우는 무자비한 400 Bad Request 에러의 늪에 빠지며 수많은 삽질을 반복해야 했습니다. 오늘 2편에서는 그 고군분투의 기록과 해결 코드를 생생하게 공유합니다.

1. 접근 토큰(Access Token)이 대체 무엇인가?

한국투자증권 API를 다룰 때 가장 먼저 명확히 이해해야 하는 개념이 바로 이 토큰 시스템입니다. 우리가 1편에서 발급받은 App Key와 App Secret은 시스템에 로그인하기 위한 고유한 신분증과 같습니다. 하지만 증권사 서버에 주문을 넣거나 시세를 조회할 때마다 이 중요한 신분증을 매번 패킷에 실어 보내는 것은 보안상 매우 위험합니다.

그래서 KIS API 서버는 OAuth2라는 표준 인증 방식을 사용합니다. "너의 App Key와 App Secret이 진짜인지 확인했으니, 앞으로 24시간 동안만 유효한 임시 출입증(Access Token)을 발급해 줄게. 오늘 하루 동안은 신분증 대신 이 출입증만 보여주고 거래해"라는 개념입니다. 즉, 우리가 만들 자동매매 프로그램이 정상적으로 작동하려면 스크립트가 실행되자마자 이 임시 출입증을 안전하게 받아오는 로직이 무조건 성공해야만 합니다.

2. 첫 번째 삽질: Content-Type 누락과 400 Bad Request의 지옥

공식 문서를 가볍게 훑어본 뒤, 파이썬의 requests 라이브러리를 이용해 아래와 같은 코드를 작성하고 실행했습니다. 당연히 가뿐하게 통과할 줄 알았습니다.


# 에러가 발생했던 잘못된 코드 예시
import requests
import os
from dotenv import load_dotenv

load_dotenv()

URL = "https://openapivts.koreainvestment.com:29443/oauth2/tokenP"

payload = {
    "grant_type": "client_credentials",
    "appkey": os.getenv("KIS_APP_KEY"),
    "appsecret": os.getenv("KIS_APP_SECRET")
}

# 단순하게 데이터를 전송했다가 에러를 마주함
response = requests.post(URL, data=payload)
print(response.status_code)
print(response.text)

결과는 처참한 400 Bad Request였습니다. 응답 메시지에는 불친절한 에러 코드와 함께 본문을 빌드할 수 없다는 메시지만 덩그러니 남았습니다. 오타가 난 것도 아니고 값도 제대로 가져왔는데 왜 거부당하는지 도무지 이해할 수가 없었습니다.

한참을 헤매다가 공식 문서의 세부 명세를 돋보기 보듯 다시 뜯어보았습니다. 원인은 웹 통신 시 데이터를 전달하는 형식에 있었습니다. KIS API 서버는 요청을 받을 때 헤더(Header)에 "Content-Type": "application/json" 형식을 명시해 주기를 강력하게 원하고 있었습니다.

게다가 파이썬 딕셔너리 구조인 payload를 requests.post의 data 인자에 그대로 넣으면, 폼 데이터(Form Data) 형태로 인코딩되어 전송됩니다. 서버가 원하는 것은 순수한 JSON 문자열 형태의 데이터였던 것입니다. 이를 해결하려면 헤더를 명시해주고 데이터를 json.dumps()로 변환하여 보내거나, requests 라이브러리의 json 파라미터를 사용해야만 했습니다.

3. 두 번째 삽질: 모의투자와 실전 투자의 도메인 혼동

데이터 형식을 JSON으로 수정했음에도 여전히 인증 오류가 발생하는 구간이 있었습니다. 알고 보니 한국투자증권은 실전 투자용 서버모의 투자용 서버의 주소(Domain) 및 포트 번호가 완전히 격리되어 운영되고 있었습니다.

만약 내가 발급받은 App Key가 모의투자용 키라면, 반드시 도메인 주소에 openapivts.koreainvestment.com:29443을 사용해야 합니다. 만약 실전용 키를 들고 모의투자 서버로 접근하거나, 반대로 모의투자 키를 들고 실전 서버 주소인 openapi.koreainvestment.com:10443으로 접근하면 서버는 가차 없이 인증을 거부하고 에러를 뿜어냅니다. 이 사소한 주소 체계와 포트 번호의 차이를 인지하지 못해 또다시 모니터 앞에서 한참을 고뇌해야 했습니다. 공식 문서의 텍스트 하나, 설정 값 하나를 다룰 때 얼마나 고도의 집중력이 필요한지 뼈저리게 깨달은 순간이었습니다.

4. 고군분투 끝에 완성한 토큰 발급 성공 코드

수많은 에러 메시지와 공식 문서를 대조해가며 정석대로 헤더를 맞추고, 데이터를 JSON으로 직렬화하여 마침내 임시 출입증을 완벽하게 받아오는 코드를 완성했습니다. 앞으로 우리 프로젝트의 핵심 모듈이 될 kis_client.py 파일의 위대한 첫걸음입니다.


import requests
import json
import os
from dotenv import load_dotenv

# .env 파일에서 환경변수 로드
load_dotenv()

# 한국투자증권 모의투자용 기본 URL 설정
BASE_URL = "https://openapivts.koreainvestment.com:29443"

def get_access_token():
    """
    한국투자증권 API 서버에 접근 토큰(Access Token)을 요청하는 함수
    """
    path = "/oauth2/tokenP"
    url = f"{BASE_URL}{path}"
    
    # 400 에러를 방지하기 위한 핵심 헤더 설정
    headers = {
        "content-type": "application/json"
    }
    
    payload = {
        "grant_type": "client_credentials",
        "appkey": os.getenv("KIS_APP_KEY"),
        "appsecret": os.getenv("KIS_APP_SECRET")
    }
    
    # json=payload 설정을 통해 딕셔너리를 자동으로 JSON 문자열로 변환하여 전송
    response = requests.post(url, headers=headers, json=payload)
    
    if response.status_code == 200:
        data = response.json()
        # 발급 성공 시 토큰 값과 만료 시간 추출
        access_token = data.get("access_token")
        expired_time = data.get("access_token_token_expired")
        print("[SUCCESS] 접근 토큰 발급 완료!")
        return access_token
    else:
        print(f"[ERROR] 토큰 발급 실패. 상태 코드: {response.status_code}")
        print(f"상세 에러 내용: {response.text}")
        return None

if __name__ == "__main__":
    token = get_access_token()
    if token:
        print(f"발급된 토큰 (앞 20자리): {token[:20]}...")

터미널에서 이 스크립트를 실행하고 마침내 [SUCCESS] 접근 토큰 발급 완료!라는 문구와 함께 콘솔창에 길고 복잡한 암호 같은 토큰 값이 출력되는 것을 보았습니다. 그 순간 온몸에 짜릿한 카타르시스가 밀려왔습니다. 몇 시간 동안 저를 괴롭히던 에러들을 극복하고, 드디어 증권사 서버의 닫힌 문을 열 수 있는 열쇠를 손에 쥐게 된 것입니다.

5. 다음 편 예고: 드디어 첫 시세 조회! 그런데 이 거대한 데이터는 뭐지?

정식 출입증을 얻었으니 이제 당당하게 한국투자증권 서버에 대고 "삼성전자 현재가 얼마야?"라고 물어볼 차례입니다. 문을 여는 것이 어려웠지, 시세를 물어보는 것은 금방 끝날 것이라 낙관했습니다.

하지만 기쁨도 잠시, 성공적으로 요청을 보낸 저에게 돌아온 것은 화면을 가득 채우다 못해 터져버릴 것 같은 수백 줄짜리 거대한 JSON 데이터 덩어리였습니다. 정제되지 않은 외계어 같은 이 수많은 필드 속에서, 내가 진짜 필요한 현재가, 전일 대비 변동폭, 거래량 데이터만 어떻게 골라내어 유의미하게 활용할 수 있을까요? 다음 3편에서는 파이썬 데이터 파싱 과정과 본격적인 시세 조회 구현 단계에서의 새로운 삽질기로 찾아오겠습니다!