(번외) TR이 뭐지? 증권사 Open API를 이해하기 위한 7가지 개념

대현·2026년 9월 7일
post-thumbnail

(번외) TR이 뭐지? 증권사 Open API를 이해하기 위한 7가지 개념

주식 자동매매 프로그램의 기능 목록을 정리하면서 LS증권 Open API 문서를 살펴보았다.

처음에는 API 문서라고 해서 익숙한 내용이 나올 줄 알았다.

URL을 확인하고

요청 데이터를 보내고

응답으로 JSON을 받으면 되는 것 아닌가?

그런데 문서를 열어보니 처음 보는 표현이 계속 등장했다.

TR Code

InBlock

OutBlock

연속조회

실시간 TR

REST API와 OAuth2 정도는 들어본 적이 있었지만, 이 용어들이 하나의 요청 안에서 어떻게 연결되는지는 잘 몰랐다.

특히 TR이 가장 낯설었다.

"URL이 있는데 TR Code는 왜 또 필요한 거지?"

그리고 주문 API를 살펴보면서는 질문이 하나 더 생겼다.

"주문 요청에 성공하면 주식을 산 것 아닌가? 체결은 또 무엇이지?"

기능 목록을 제대로 정의하려면 먼저 이 개념부터 알아야 했다.

그래서 이번 글에서는 증권사 Open API를 사용하기 전에 알아두어야 할 개념을 전체 흐름을 따라 정리해보려고 한다.

예시는 현재 프로젝트에서 사용할 LS증권 Open API를 기준으로 작성했다.

다른 증권사 API도 전체적인 원리는 비슷하지만, TR Code와 필드명, 호출 제한, 주문 규칙은 다를 수 있다.


먼저 전체 구조부터 보자

처음에는 각각이 서로 다른 기술처럼 보였다.

그런데 정리하고 나니 다음과 같이 연결되어 있었다.

App Key / App Secret
        ↓
OAuth2로 Access Token 발급
        ↓
HTTP 요청의 Authorization 헤더에 토큰 전달
        ↓
TR Code로 수행할 업무 지정
        ↓
InBlock으로 요청 데이터 전달
        ↓
OutBlock으로 결과 수신
        ↓
데이터가 더 있으면 연속조회


실시간 시세와 주문 이벤트가 필요하다면
        ↓
WebSocket 연결 후 실시간 TR 등록

결국 증권사 Open API를 사용한다는 것은 단순히 URL 하나를 호출하는 일이 아니었다.

누가 요청하는지 인증하고

어떤 업무를 실행할지 지정하고

그 업무에 필요한 형식으로 데이터를 보내고

응답과 이후 상태 변화까지 추적하는 과정

에 더 가까웠다.

이제 하나씩 살펴보자.


1. REST API와 HTTP

LS증권 Open API의 일반 조회와 주문 기능은 HTTP를 이용한다.

클라이언트가 요청을 보내면 서버가 처리 결과를 응답하는 구조다.

자동매매 프로그램
        │
        │ HTTP Request
        ▼
LS증권 Open API 서버
        │
        │ HTTP Response
        ▼
자동매매 프로그램

예를 들어 현재가를 조회한다면 프로그램이 종목 정보를 담아 요청하고, 서버는 현재가와 거래량 등이 들어 있는 JSON을 응답한다.

여기까지는 일반적인 REST API와 크게 다르지 않았다.

그런데 LS증권 API 문서를 보면 여러 기능이 다음과 같은 공통 URL을 사용한다.

현재가·차트·거래량 조회  → /stock/market-data

잔고·주문 내역 조회      → /stock/accno

매수·매도·정정·취소 주문 → /stock/order

"현재가 조회와 차트 조회가 같은 URL을 사용하면 서버는 둘을 어떻게 구분하지?"

이때 등장하는 것이 TR Code다.

즉 HTTP는 요청과 응답을 전달하는 통신 방식이고, TR은 그 통신을 통해 실행할 증권 업무의 종류를 구분한다.

LS증권의 일반 요청은 대략 다음과 같은 모양이 된다.

POST /stock/market-data HTTP/1.1
Host: openapi.ls-sec.co.kr:8080
Content-Type: application/json; charset=utf-8
Authorization: Bearer {ACCESS_TOKEN}
tr_cd: t1102
tr_cont: N
tr_cont_key:

{
  "t1102InBlock": {
    "shcode": "078020",
    "exchgubun": "K"
  }
}

여기에는 이후에 살펴볼 개념이 거의 모두 들어 있다.

Authorization → 인증 정보

tr_cd        → 실행할 TR

tr_cont      → 연속조회 여부

InBlock      → TR에 전달할 입력값

처음에는 API 문서에 흩어져 있던 용어들이었는데, 실제 요청 하나에 넣어보니 관계가 조금 보이기 시작했다.


2. OAuth2 인증

증권 API는 아무나 호출할 수 있으면 안 된다.

계좌 잔고를 조회하고 실제 주문까지 보낼 수 있기 때문이다.

LS증권 Open API에서는 먼저 App Key와 App Secret을 이용해 Access Token을 발급받는다.

App Key + App Secret
        ↓
Access Token 발급 요청
        ↓
Access Token 응답
        ↓
이후 API 요청에 토큰 사용

LS증권의 토큰 발급 요청은 다음 주소로 보낸다.

POST https://openapi.ls-sec.co.kr:8080/oauth2/token

요청에는 다음 값이 들어간다.

grant_type=client_credentials
appkey={APP_KEY}
appsecretkey={APP_SECRET}
scope=oob

여기서 사용하는 client_credentials는 OAuth 2.0 표준의 Client Credentials 방식이다.

사용자가 로그인 화면에서 직접 인증하는 흐름이 아니라, 프로그램이 발급받은 자격 증명으로 토큰을 요청하는 방식이다.

Access Token과 Bearer Token은 다른 것일까?

처음에는 이 두 표현도 서로 다른 토큰을 의미하는 줄 알았다.

그런데 둘은 바라보는 기준이 다르다.

Access Token
→ 보호된 API에 접근하기 위해 발급받은 값

Bearer Token
→ 그 값을 가진 사람이 사용할 수 있다는 전달 방식

따라서 LS증권 API 요청에서는 발급받은 Access Token을 다음과 같이 전달한다.

Authorization: Bearer {ACCESS_TOKEN}

Bearer 뒤에 들어가는 값이 바로 Access Token이다. 이 전달 방식은 OAuth 2.0 Bearer Token 표준에 정의되어 있다.

다만 Bearer 방식은 토큰을 소유한 것 자체가 권한을 의미한다.

따라서 App Secret과 Access Token이 로그, Git 저장소, 화면 등에 노출되지 않도록 관리해야 한다.

그리고 토큰은 영구적으로 사용할 수 없다.

LS증권 공식 이용안내에 따르면 개인과 법인의 Access Token은 발급 신청일 다음 날 오전 7시까지 유효하며, 만료되면 App Key와 App Secret으로 다시 발급받아야 한다.

결국 프로그램에는 단순히 토큰 발급만 필요한 것이 아니었다.

토큰 발급

토큰 안전한 저장

만료 시점 관리

만료 시 재발급

API 요청에 토큰 추가

까지 하나의 인증 흐름으로 생각해야 했다.


3. TR, TR Code, InBlock, OutBlock

이제 가장 낯설었던 TR이다.

TR은 증권 API에서 하나의 업무 요청 단위를 나타낸다.

흔히 Transaction의 약자로 설명하며, 현재가 조회나 잔고 조회, 주문처럼 서버에 요청할 수 있는 각각의 업무에 고유한 TR Code가 붙는다.

다만 여기서 말하는 Transaction은 데이터베이스에서 말하는 ACID 트랜잭션과 같은 의미로 이해하면 조금 헷갈린다.

증권 API 문맥에서는 다음처럼 생각하는 편이 이해하기 쉬웠다.

TR = 증권사 서버에 요청할 수 있는 하나의 업무 단위

예를 들면 LS증권에는 다음과 같은 TR이 있다.

TR Code업무
t1102주식 현재가 조회
t0424주식 잔고 조회
CSPAT00601현물 주식 주문
CSPAT00701현물 주식 정정 주문
CSPAT00801현물 주식 취소 주문

TR Code

TR Code는 서버에 어떤 업무를 실행할지 알려주는 식별자다.

앞에서 본 것처럼 현재가와 차트 관련 TR이 같은 /stock/market-data 주소를 사용할 수 있다.

이때 서버는 요청 헤더의 tr_cd를 보고 실행할 기능을 구분한다.

tr_cd: t1102

즉 다음 세 가지는 같은 역할이 아니다.

HTTP Method → 요청을 어떤 방식으로 보낼 것인가?

URL         → 어떤 API 영역으로 보낼 것인가?

TR Code     → 그 영역에서 어떤 증권 업무를 실행할 것인가?

InBlock

TR을 선택했다면 그 업무에 필요한 값을 보내야 한다.

이 입력 데이터의 묶음이 InBlock이다.

현재가 조회 TR인 t1102를 예로 들면 요청 Body는 다음과 같은 형태가 된다.

{
  "t1102InBlock": {
    "shcode": "078020",
    "exchgubun": "K"
  }
}
shcode    → 종목코드

exchgubun → 거래소 구분

정리하면 t1102InBlock은 t1102 업무를 실행하기 위해 서버에 전달하는 입력값의 형식이다.

TR마다 필요한 값이 다르기 때문에 InBlock의 필드도 달라진다.

OutBlock

서버가 TR을 처리한 뒤 보내주는 결과 묶음은 OutBlock이다.

현재가 조회 응답을 단순화하면 다음과 같은 모습이다.

{
  "t1102OutBlock": {
    "hname": "LS증권",
    "price": 0,
    "open": 0,
    "high": 0,
    "low": 0,
    "volume": 0
  },
  "rsp_cd": "00000",
  "rsp_msg": "정상적으로 조회가 완료되었습니다."
}

실제 값은 요청 시점의 시장 데이터에 따라 달라지므로 여기서는 구조만 남겼다.

TR에 따라 OutBlock이 하나일 수도 있고 여러 개일 수도 있다.

예를 들어 t0424 잔고 조회는 다음처럼 나뉜다.

t0424OutBlock
→ 계좌 전체의 합계 정보와 연속조회 값

t0424OutBlock1
→ 종목별 잔고 목록

다만 OutBlock1은 무조건 목록이다처럼 모든 TR에 같은 규칙을 적용하면 안 된다.

각 Block의 의미는 해당 TR 문서에서 확인해야 한다.

결국 하나의 TR은 다음처럼 읽을 수 있었다.

t1102
  ├─ t1102InBlock  : 현재가 조회에 필요한 입력
  └─ t1102OutBlock : 현재가 조회 결과

이 구조를 이해하고 나니 긴 영문 약어처럼 보이던 API 문서가 조금 덜 낯설어졌다.


4. 연속조회와 Pagination

조회 결과가 많으면 서버가 모든 데이터를 한 번에 보내지 않을 수 있다.

웹 서비스를 만들 때는 보통 다음과 같은 Pagination을 떠올린다.

page=1
page=2
page=3

그런데 증권 API에서는 다음 페이지 번호 대신 이전 응답에서 받은 값을 이용해 다음 데이터를 요청하는 연속조회 방식이 자주 사용된다.

첫 번째 조회
        ↓
데이터 + 다음 조회에 사용할 연속키 수신
        ↓
연속키를 넣어 다시 요청
        ↓
다음 데이터 수신

일반적인 Cursor Pagination과 비슷한 구조다.

LS증권의 공통 헤더에는 다음 값이 있다.

tr_cont
tr_cont_key

첫 조회에서는 보통 다음과 같이 시작한다.

tr_cont: N
tr_cont_key:

응답에서 연속 데이터가 있다는 표시와 연속키를 받았다면, 다음 요청에는 해당 값을 다시 전달한다.

그런데 여기서 한 번 더 주의할 점이 있었다.

"그러면 모든 TR이 헤더의 연속키만 사용하면 되는 건가?"

그렇지는 않았다.

TR에 따라 Body 안의 CTS 필드도 함께 사용한다.

예를 들어 LS증권 공식 적용 예제의 t0424 잔고 조회에는 cts_expcode가 있다.

첫 조회
cts_expcode = 빈 값

연속조회
cts_expcode = 이전 t0424OutBlock에서 받은 cts_expcode

따라서 연속조회는 공통으로 한 번 구현하고 끝낼 수 있는 단순한 page++가 아니었다.

응답에 다음 데이터가 있는지 확인

헤더의 연속키 보관

TR별 CTS 값 보관

다음 요청에 필요한 값 전달

응답 목록 합치기

마지막 페이지에서 반복 종료

가 필요하다.

잘못 구현하면 같은 페이지를 계속 요청할 수도 있다.

그래서 자동매매 프로그램에서는 연속키가 이전과 같은지, 최대 반복 횟수를 넘지 않았는지까지 확인하는 편이 안전하다고 생각했다.


5. Rate Limit

API는 원하는 만큼 무한히 호출할 수 있는 것이 아니다.

일정 시간 동안 허용되는 요청 수가 정해져 있는데, 이를 Rate Limit이라고 한다.

처음에는 LS증권 Open API 전체에 하나의 호출 제한이 있을 것이라고 생각했다.

그런데 API 가이드를 확인해보니 제한은 TR마다 달랐다.

작성 시점의 LS증권 주식 시세, 계좌, 주문 API 가이드에서 확인한 예시는 다음과 같다.

TR Code업무초당 전송 건수
t1102주식 현재가 조회10건
t0424주식 잔고 조회2건
CSPAQ12200현물계좌 예수금·주문가능금액 조회1건
CSPAT00601현물 주식 주문10건
CSPAT00701현물 주식 정정 주문3건
CSPAT00801현물 주식 취소 주문3건

즉 Rate Limit은 단순히 서버 오류를 막기 위한 예외 처리가 아니었다.

처음부터 요청 구조에 반영해야 하는 설계 조건이었다.

예를 들어 관심 종목 100개의 현재가를 다음처럼 바로 조회하면 문제가 생길 수 있다.

for (String symbol : symbols) {
    requestCurrentPrice(symbol);
}

코드는 단순하지만 요청이 짧은 시간에 몰린다.

따라서 실제로는 다음과 같은 방법을 생각해야 한다.

TR별 요청 큐 분리

초당 허용 건수에 맞춘 Rate Limiter 적용

반복 조회 결과 캐싱

여러 종목을 한 번에 조회하는 TR 검토

실시간 데이터는 WebSocket 사용

그리고 주문 요청은 조회 요청보다 재시도를 더 조심해야 한다.

조회에 실패하면 같은 요청을 다시 보내도 결과가 크게 달라지지 않을 수 있다.

하지만 주문 요청의 응답을 받지 못했다고 바로 다시 주문하면 같은 주문이 두 번 접수될 가능성이 있다.

따라서 주문은 단순한 무한 재시도보다 주문 내역을 먼저 확인한 뒤 재시도 여부를 판단하는 방식이 필요하다.

API 가이드의 전송 건수는 변경될 수 있으므로 실제 구현 시점에는 각 TR 문서를 다시 확인해야 한다.


6. WebSocket과 실시간 데이터

현재가를 한 번 조회하는 것은 REST API로 가능하다.

그런데 가격 변화를 계속 확인하려면 어떻게 해야 할까?

가장 먼저 떠오른 방법은 일정 간격으로 현재가 API를 반복 호출하는 것이었다.

현재가 조회
1초 대기
현재가 조회
1초 대기
현재가 조회
...

하지만 종목 수가 늘어나면 요청도 계속 늘어난다.

Rate Limit도 신경 써야 하고, 가격이 바뀌지 않아도 매번 요청을 보내야 한다.

이럴 때 사용하는 것이 WebSocket이다.

REST API와 WebSocket의 차이를 단순하게 표현하면 다음과 같다.

REST API

클라이언트 ── 요청 ──> 서버
클라이언트 <─ 응답 ─── 서버


WebSocket

클라이언트 <════ 연결 유지 ════> 서버
                    │
                    ├─ 체결가 변경
                    ├─ 호가 변경
                    └─ 주문 체결 알림

REST API는 필요한 시점에 요청하고 결과를 받는 방식이다.

WebSocket은 연결을 유지한 상태에서 원하는 실시간 항목을 등록하고, 새로운 데이터가 생기면 서버가 메시지를 보내는 방식이다.

LS증권의 국내주식 실시간 시세 WebSocket 운영 주소는 현재 실시간 시세 API 가이드에 다음과 같이 안내되어 있다.

wss://openapi.ls-sec.co.kr:9443/websocket/stock

연결한 뒤에는 다음과 같은 메시지로 원하는 실시간 TR과 종목을 등록한다.

{
  "header": {
    "token": "{ACCESS_TOKEN}",
    "tr_type": "3"
  },
  "body": {
    "tr_cd": "S3_",
    "tr_key": "005930"
  }
}

여기서 S3_는 KOSPI 체결 실시간 TR이고, tr_key에는 등록할 종목코드가 들어간다.

실시간 데이터도 결국 TR을 사용한다는 점이 흥미로웠다.

다만 REST API와 달리 HTTP 요청마다 Authorization: Bearer ...를 보내는 형태가 아니라, WebSocket 등록 메시지의 header.token에 Access Token을 넣는다.

그러면 모든 기능을 WebSocket으로 만들면 될까?

그렇지는 않다.

둘은 경쟁 관계라기보다 역할이 다르다.

REST API
→ 토큰 발급, 잔고 조회, 현재 상태 조회, 주문 요청

WebSocket
→ 실시간 시세, 호가 변화, 주문 접수·체결 이벤트 수신

자동매매 프로그램에서는 보통 두 방식을 함께 사용하게 된다.

또한 WebSocket 연결은 네트워크 문제로 끊길 수 있다.

따라서 실제 구현에서는 다음도 필요하다.

연결 종료 감지

재연결

기존 실시간 종목 재등록

연결이 끊긴 동안의 주문 상태 다시 조회

특히 주문 상태는 실시간 메시지만 믿기보다, 재연결 후 REST 조회 결과와 맞춰보는 과정이 필요해 보였다.


7. 주문 생명주기

마지막은 주문이다.

처음에는 주문 흐름을 다음처럼 생각했다.

매수 주문 요청
        ↓
주식 매수 완료

그런데 실제로는 주문 요청과 체결 사이에 여러 상태가 존재한다.

그리고 다음처럼 일렬로 이해해서도 안 된다.

주문 → 접수 → 체결 → 미체결 → 정정/취소

왜냐하면 미체결은 체결이 끝난 뒤의 다음 단계가 아니기 때문이다.

접수된 주문은 시장 상황에 따라 다음과 같이 나뉜다.

주문 요청
    │
    ├─ 거부
    │
    └─ 접수
        │
        ├─ 미체결
        │     └─ 정정 또는 취소
        │
        ├─ 부분 체결
        │     └─ 남은 수량 정정 또는 취소
        │
        └─ 전량 체결

이 구조를 알고 나니 주문 성공이라는 표현부터 조심해서 사용해야 한다는 생각이 들었다.

주문 요청

프로그램은 종목, 수량, 가격, 매수·매도 구분, 주문 유형 등을 담아 주문을 요청한다.

LS증권의 현물 주식 주문 TR은 CSPAT00601이다.

주문 전에 주문 가능 수량과 금액을 확인하려면 CSPBQ00200 같은 조회 TR도 함께 사용할 수 있다.

접수

증권사 서버가 주문을 받아 주문번호를 반환했다면 주문이 접수된 것이다.

LS증권 주문 응답에서는 OrdNo와 주문 시각 등의 정보를 확인할 수 있다.

하지만 주문번호를 받았다고 해서 전량 체결된 것은 아니다.

주문 접수 성공 ≠ 체결 완료

이 차이가 가장 중요했다.

실시간 TR에서는 SC0으로 주문 접수 이벤트를 받을 수 있다.

체결과 미체결

접수된 주문의 가격과 수량이 시장에서 맞으면 체결이 발생한다.

한 번에 모든 수량이 체결될 수도 있지만 일부 수량만 체결될 수도 있다.

예를 들어 10주를 주문했다고 생각해보자.

주문수량 10주
        ↓
3주 체결
        ↓
체결수량 3주 + 미체결수량 7주

이 상태가 부분 체결이다.

따라서 프로그램은 단순히 체결 여부만 Boolean 값으로 관리하기 어렵다.

주문수량

누적 체결수량

미체결수량

체결가격

주문 상태

를 함께 관리해야 한다.

LS증권에서는 SC1 실시간 TR로 주문 체결 이벤트를 받을 수 있고, 주문·체결 내역이나 미체결 현황을 조회하는 TR로 현재 상태를 다시 확인할 수도 있다.

정정과 취소

아직 체결되지 않은 수량이 있다면 주문 가격이나 수량을 정정하거나 남은 주문을 취소할 수 있다.

LS증권에서는 다음 TR을 사용한다.

CSPAT00701 → 현물 주식 정정 주문

CSPAT00801 → 현물 주식 취소 주문

여기서 정정과 취소의 대상은 이미 체결된 수량이 아니라 남아 있는 미체결 수량이다.

실시간 이벤트는 각각 SC2 정정, SC3 취소로 확인할 수 있고, 주문이 거부되면 SC4 거부 이벤트가 발생할 수 있다.

결국 주문 기능을 구현한다는 것은 주문 API 한 번을 호출하는 것으로 끝나지 않는다.

주문 가능 금액과 수량 확인

주문 요청

주문번호 저장

접수 또는 거부 확인

부분 체결과 전량 체결 구분

미체결수량 관리

필요하면 정정 또는 취소

실시간 이벤트와 조회 결과 일치 여부 확인

까지 이어지는 하나의 생명주기로 봐야 했다.


자동매매 프로그램에서는 어떻게 연결될까?

지금까지 알아본 개념을 현재 만들고 있는 자동매매 프로그램의 흐름에 대입해보면 다음과 같다.

1. App Key와 App Secret으로 Access Token 발급

2. REST API로 계좌와 보유 종목의 현재 상태 조회

3. 필요한 TR Code와 InBlock을 이용해 데이터 요청

4. OutBlock을 프로그램 내부 데이터 형식으로 변환

5. 연속조회가 필요하면 다음 데이터까지 수집

6. WebSocket으로 실시간 시세와 주문 이벤트 구독

7. 전략의 BUY / SELL 판단에 따라 주문 요청

8. 주문번호를 기준으로 접수·체결·미체결 상태 추적

9. 필요하면 남은 수량 정정 또는 취소

처음에는 다음 정도의 기능만 있으면 된다고 생각했다.

현재가 조회

매수

매도

그런데 Open API의 동작 방식을 살펴보니 그 사이에 필요한 기능이 훨씬 많았다.

인증 관리

TR별 요청과 응답 변환

연속조회

호출 속도 제한

실시간 연결 관리

주문 상태 추적

중복 주문 방지

즉 API 문서를 읽는 과정은 단순히 호출 방법을 찾는 일이 아니었다.

앞으로 프로그램에 어떤 책임과 상태가 필요한지를 찾는 과정이기도 했다.


정리

이번에 알아본 내용을 짧게 정리하면 다음과 같다.

REST API / HTTP
→ 요청과 응답을 전달하는 통신 방식

OAuth2
→ App Key와 App Secret으로 Access Token을 발급받는 인증 절차

Bearer Token
→ 발급받은 Access Token을 API 요청에 전달하는 방식

TR Code
→ 실행할 증권 업무를 구분하는 식별자

InBlock
→ 해당 TR에 보내는 입력 데이터

OutBlock
→ 해당 TR에서 받는 출력 데이터

연속조회
→ 이전 응답의 연속키나 CTS 값을 이용해 다음 데이터를 조회하는 방식

Rate Limit
→ TR별로 정해진 시간당 또는 초당 요청 제한

WebSocket
→ 연결을 유지하며 실시간 시세와 주문 이벤트를 받는 방식

주문 생명주기
→ 주문 요청부터 접수, 거부, 미체결, 부분 체결, 전량 체결, 정정, 취소까지의 흐름

가장 크게 생각이 바뀐 부분은 두 가지였다.

첫 번째는 URL만 안다고 API를 호출할 수 있는 것이 아니라는 점이다.

증권사 API에서는 TR Code와 Block 구조를 함께 이해해야 요청과 응답을 제대로 읽을 수 있었다.

두 번째는 주문 요청의 성공이 거래 완료를 의미하지 않는다는 점이다.

주문은 접수된 뒤에도 시장 상황에 따라 미체결, 부분 체결, 전량 체결로 나뉜다.

따라서 자동매매 프로그램은 주문을 보내는 기능보다 주문의 결과를 끝까지 추적하는 기능이 더 중요할 수도 있다.

이제 이 개념들을 기준으로 앞에서 정리했던 기능 목록을 다시 보면, 왜 토큰 관리와 연속조회, Rate Limit, 체결 확인 같은 기능이 필요한지 조금 더 명확하게 이해할 수 있을 것 같다.


참고 자료

profile
도전을 멈추지 않는 개발자

0개의 댓글