1. 시작은 단순했다: 그냥 패키지 설치하고 API 키만 넣으면 되는 거 아냐?
1.1 첫 번째 시행착오: 다른 AI 모델과의 차이
처음에 단순하게 생각했다. OpenAI나 Anthropic처럼 pip install 하고 API 키만 넣으면 될 줄 알았으니까.
# OpenAI는 이렇게 간단했는데...
from openai import OpenAI
client = OpenAI(api_key="my-api-key")
response = client.chat.completions.create(
messages=[{"role": "user", "content": "안녕"}]
)
# Anthropic도 비슷하게 쉬웠고...
from anthropic import Anthropic
client = Anthropic(api_key="my-api-key")
response = client.messages.create(
messages=[{"role": "user", "content": "안녕"}]
)
# 물론 쳇모델 관련 객체(인스턴스)를 만들고 기존 코드에 영향이 가지 않게 통합하는 과정도 있기는 했다..
하지만 네이버는 달랐다. SDK도 없고, HTTP 요청을 직접 구현해야 했다. 그때 처음 든 생각이 "HTTP 통신의 정확한 작동방식이 뭐지? API의 핵심 개념은 또 뭐더라..?"였다.
1.2 API가 뭔데? - 기본부터 다시 시작하기
처음에는 API라는 용어 자체가 너무 추상적이었다. 하지만 레스토랑 비유를 통해 이해하기 시작했다:
- 손님(나, 개발자) → 음식(데이터)을 주문하고 싶다
- 웨이터(API) → 주방과 손님 사이를 연결해준다
- 주방(서버) → 실제로 요청을 처리한다
1.3 바퀴를 재발명하지 말자 - 기존 코드 활용하기
나는 운이 좋게도 네이버 API를 구현해놓은 다른 개발자의 코드를 발견했다. '뉴턴도 거인의 어깨 위에서 더 멀리 보았다'고 했으니, 나도 r고수 개발자들의 코드를 참고하기로 했다.
# 다른 개발자가 구현해놓은 네이버 API 호출 코드
def _completion_with_retry(
self,
messages: list,
model: str,
temperature: float,
max_tokens: int,
top_p: float,
**kwargs,
) -> dict:
# ... 기존 코드 ...
잘한 점
- 시간을 효율적으로 사용했다
- 검증된 코드를 재사용했다
- 다른 개발자의 구현 방식을 배울 수 있었다
아쉬운 점
- 코드를 복사해 왔지만 실제로 작동하지 않았다
- HTTP 통신의 기본을 몰라서 디버깅이 어려웠다
- 이중 API 키를 어디에 넣어야 할지, Request ID는 뭔지 이해하지 못했다
1.4 복붙의 한계 - 코드를 이해하는 것의 중요성
단순히 코드를 복사해서 붙여넣기만 했을 때 마주친 문제들:
# 이런 에러들이 계속 발생했다
"error": "401 Unauthorized" # API 키가 잘못됐나?
"error": "Invalid Request ID" # Request ID가 뭐지?
"error": "Missing Required Headers" # 헤더는 또 뭐야?
이때 깨달았다. "아, 코드를 복사하는 것보다 HTTP 통신이 어떻게 작동하는지 이해하는 게 먼저구나...”
2. HTTP 통신, 처음부터 차근차근 이해하기
2.1 "카톡처럼 메시지를 주고받는 거구나" - HTTP 통신의 이해
처음에는 HTTP가 뭔지 전혀 몰랐다. 하지만 카카오톡으로 비유하면서 이해하기 시작했다:
# HTTP 요청은 이런 구조로 되어있다
{
"받는사람": "<https://api.naver.com/>...", # URL
"보내는사람_신분증": { # Headers
"X-NCP-APIGW-API-KEY": "게이트웨이키",
"X-NCP-APIGW-API-KEY-ID": "시크릿키"
},
"메시지": { # Body
"content": "안녕하세요"
}
}
배운 점
- HTTP는 그냥 정해진 규칙대로 데이터를 주고받는 방식이다
- 택배 보내기처럼 받는 사람 주소(URL), 보내는 사람 정보(Headers), 실제 내용(Body)이 필요하다
- HTTPS는 암호화된 통신이라 더 안전하다 (택배 상자를 봉인하는 것과 비슷함)
2.2 아! GET, POST, PUT, DELETE가 이런 거였어? - HTTP 메소드 이해하기
레스토랑에서 일어나는 일로 이해하니까 훨씬 쉬웠다:
# GET: 메뉴판 보기
response = requests.get("<https://restaurant.com/menu>")
# POST: 새로운 주문하기
response = requests.post("<https://restaurant.com/order>",
json={"menu": "스테이크"})
# PUT: 주문 전체 수정하기
response = requests.put("<https://restaurant.com/order/123>",
json={"menu": "샐러드"})
# DELETE: 주문 취소하기
response = requests.delete("<https://restaurant.com/order/123>")
실수했던 점
- 처음에는 모든 요청을 GET으로 보내려고 했다
- POST 요청에 데이터를 제대로 담지 않았다
- URL 구조를 이해하지 못해서 엉뚱한 주소로 요청을 보냈다
2.3 택배 송장번호 같은 거구나 - Request ID의 이해
Request ID가 왜 필요한지 처음에는 이해가 안 됐다. 하지만 택배 송장번호로 비유하니 명확해졌다:
# Request ID 생성 및 사용
request_id = str(uuid.uuid4()) # 고유한 송장번호 생성
headers = {
"X-NCP-APIGW-API-KEY": gateway_key,
"X-NCP-APIGW-API-KEY-ID": secret_key,
"X-NCP-Request-ID": request_id # 송장번호 부착
}
왜 필요한가?
- 수많은 API 요청 중 특정 요청을 추적할 수 있다
- 문제가 생겼을 때 디버깅이 쉽다
- 중복 요청을 방지할 수 있다
3. 은행 금고 같은 네이버의 이중 API 키 시스템
3.1 왜 키가 두 개야? - API 키 시스템의 이해
처음에는 정말 혼란스러웠다. OpenAI나 Anthropic은 하나의 키면 충분했는데, 네이버는 왜 두 개를 요구할까?
# OpenAI/Anthropic - 심플한 구조
client = OpenAI(api_key="하나의_키")
# 네이버 - 이중 인증 구조
headers = {
"X-NCP-APIGW-API-KEY": "게이트웨이_키", # 첫 번째 문을 여는 열쇠
"X-NCP-APIGW-API-KEY-ID": "서비스_시크릿_키" # 두 번째 문을 여는 열쇠
}
이해하게 된 보안 구조
- 게이트웨이 키: 네이버 클라우드 플랫폼 접근 권한
- 서비스 시크릿 키: HyperCLOVA 서비스 사용 권한
- 마치 은행 금고에 들어가기 위해 두 명의 직원이 각자의 키로 열어야 하는 것과 같다
3.2 헤더가 뭐길래... - HTTP 헤더의 중요성
헤더는 HTTP 요청의 '라벨'이나 '송장'같은 거였다
headers = {
# 신분증
"X-NCP-APIGW-API-KEY": gateway_key,
"X-NCP-APIGW-API-KEY-ID": secret_key,
# 택배 송장
"X-NCP-Request-ID": request_id,
# 내용물 설명
"Content-Type": "application/json"
}
실수했던 점들
- 헤더 이름을 잘못 적었다 ("X-NCP-APIGW-API-KEY" vs "x-ncp-apigw-api-key")
- Content-Type을 지정하지 않아서 서버가 요청을 이해하지 못했다
- Request ID를 매번 새로 생성하지 않았다
3.3 이제 진짜 통신이 되는구나 - 첫 성공적인 API 호출
드디어 모든 것을 제대로 설정하고 첫 성공적인 API 호출을 했다:
import requests
import uuid
def chat_with_hyperclova():
url = "<https://clovastudio.stream.naver.com/api/v1/chat-completions/HCX-002>"
headers = {
"X-NCP-APIGW-API-KEY": gateway_key,
"X-NCP-APIGW-API-KEY-ID": secret_key,
"X-NCP-Request-ID": str(uuid.uuid4()),
"Content-Type": "application/json"
}
data = {
"messages": [
{"role": "user", "content": "안녕하세요"}
]
}
response = requests.post(url, headers=headers, json=data)
return response.json()
성공의 순간
- 처음으로 200 상태 코드(성공)를 봤을 때의 기쁨
- API가 실제로 응답을 보내왔을 때의 감동
- 모든 퍼즐 조각이 맞춰진 느낌
4. 모델마다 말이 통하는 방식이 다르다 - 프롬프트 엔지니어링?
4.1 왜 플레이그라운드랑 다르지? - 첫 번째 시행착오
처음에는 XML 태그 방식를 그대로 가져왔다.
# 처음 시도했던 방식 (성능이 좋지 않았음)
messages = [
{
"role": "system",
"content": """
<system>
당신은 친절한 AI 어시스턴트입니다.
항상 존댓말을 사용하세요.
</system>
"""
}
]
문제점 발견
- 응답이 일관성이 없었다
- 문장이 너무 길거나 짧았다
- 지시사항을 제대로 따르지 않았다
4.2 마크다운이 통하는구나 - 네이버 개발자의 힌트
네이버 개발자 블로그에서 우연히 발견한 예제 코드가 힌트가 되었다:
# 마크다운 형식으로 변경한 후 (성능이 크게 개선됨)
messages = [
{
"role": "system",
"content": """
# 시스템
당신은 친절한 AI 어시스턴트입니다.
항상 존댓말을 사용하세요.
# 규칙
1. 답변은 3문장 이내로 합니다.
2. 친절하고 명확하게 답변합니다.
"""
}
]
개선된 점
- 응답의 일관성이 높아졌다
- 문장 길이가 적절해졌다
- 지시사항을 잘 따르기 시작했다
4.3 각 모델의 특성을 이해하자 - LLM별 프롬프트 최적화
각 모델마다 잘 반응하는 프롬프트 형식이 다르다는 걸 깨달았다:
# Claude AI - XML 형식을 선호
"""
<system>명확한 지시사항</system>
<human>질문</human>
<assistant>답변</assistant>
"""
# GPT-4 - JSON 형식도 잘 이해함
"""
{"role": "system", "content": "명확한 지시사항"}
{"role": "user", "content": "질문"}
"""
# HyperCLOVA - 마크다운 형식이 효과적
"""
# 시스템
명확한 지시사항
# 사용자
질문
"""
배운 점
- 각 모델은 학습된 데이터의 특성을 반영한다
- 문서화된 가이드라인이 없더라도 개발자 커뮤니티의 경험이 중요하다
- 실험과 관찰을 통한 최적화가 필요하다
5. '일단 굴러가게 하고, 그 다음에 꼭 삽질을 이해하자' +. '주의점'
처음부터 완벽하게 이해하려고 하면 아무것도 못 만들었을 것 같다. 일단 동작하는 코드를 만들고, 그 과정에서 생긴 의문점들을 하나씩 파헤치면서 오히려 더 깊이 이해하게 된 것 같다.
예를 들어, API 키가 왜 두 개인지는 처음에는 이해 못 했다. 그냥 "아, 두 개 다 넣어야 되는구나" 하고 넣었는데, 이렇게 회고하면서 보니 은행 금고처럼 이중 보안이 필요한 이유가 이해가 된다. HTTP 통신도 마찬가지다. 처음에는 그냥 "이렇게 하면 된대" 하고 따라 했는데, 이제 와서 보니 택배 보내는 것처럼 받는 사람 주소(URL)도 필요하고, 보내는 사람 정보(Headers)도 필요하고, 실제 보낼 내용(Body)도 필요한 게 당연하다.
코딩은 완벽한 이해에서 시작하는 게 아니라, 일단 만들어보고 부딪혀보면서 자기만의 지도를 그려나가는 과정인 것 같다. 처음부터 모든 걸 이해하려고 하면 겁먹고 시도조차 못 했을 텐데, 오히려 이렇게 '일단 해보고 이해하기' 방식으로 접근하니 더 많이 배운 것 같다.앞으로도 이런 식으로 접근해볼 생각이다. 일단 동작하는 걸 만들고, 그 다음에 "아, 이건 왜 이렇게 되는 거지?"라고 질문하면서 하나씩 파고들어가는 거다. 그러다 보면 어느새 내가 이해하고 있는 영역이 넓어져 있을 것 같다.
완벽한 지도를 한 번에 그리는 게 아니라, 조금씩 조금씩 새로운 길을 발견하고 표시해나가는 거다. 다음 번에도 이런 마인드로 접근해야겠다. 일단 굴러가게 만들고, 그 다음에 이렇게 회고하면서 "아, 이래서 이렇게 하는 거였구나" 하고 깨닫는 과정을 거치는 거다. 그게 더 현실적이고, 더 많이 배울 수 있는 방법인 것 같다.
물론 이런 접근 방식에도 주의할 점이 있다. "일단 돌아가게 만들자"가 "대충 만들자"가 되어서는 안 된다. 특히 보안이나 데이터 처리 같은 중요한 부분은 기본적인 이해가 필요하다. API 키를 깃허브에 실수로 올린다거나, 사용자 정보를 제대로 암호화하지 않는 등의 실수는 돌이킬 수 없는 문제를 일으킬 수 있다. 기본적인 지식이 너무 부족해서 스키마의 키를 바꾸거나 문제파악을 제대로 안하고 넘어가서 삽질한 1달의 기억이 떠오른다...
또한, '나중에 이해하자'가 '영원한 나중'이 되어서는 안 된다. 일단 동작하게 만든 후에는 반드시 시간을 내서 왜 이렇게 동작하는지, 더 나은 방법은 없는지 고민하고 공부해야 한다. 이번에 내가 HTTP 통신과 API 구조를 제대로 이해하지 못한 채로 넘어갔다면, 다음에 비슷한 작업을 할 때도 똑같이 헤맸을 것이다.
결국 균형이 중요한 것 같다. 빠르게 만들되 위험한 부분은 조심하고, 일단 동작하게 만든 후에는 반드시 회고와 학습의 시간을 가져야 한다. 이런 과정을 반복하다 보면, 점점 더 탄탄한 기본기를 가진 개발자(?)로 성장할 수 있을 것이다.
'KB' 카테고리의 다른 글
메타인지: Chat GPT o1도 하는데 나도 해야제? (2) | 2024.11.22 |
---|---|
이퀄라이저 기능 추가 회고 (1) | 2024.11.20 |
우리 앱의 피지컬과 뇌를 해부해보자 (18) | 2024.11.15 |
Open AI Realtime RAG 앱 구현 전체 과정 회고 [2편] (0) | 2024.10.31 |
Azure Open AI Realtime API, 시스템 메시지 구현 이슈 해결 [1편] (1) | 2024.10.28 |