[AI] LLM 01

- LLM 01 - OpenAI Programming
- LLM Optimization 분야 이해하기
- OpenAI의 프롬프트 구조
- OpenAI 플랫폼
- OpenAI REST API
- OpenAI REST API 옵션
- OpenAI REST API (응답 \& API Key 관리)
- OpenAI 라이브러리 방식
- 크롤링과 HTTP 프로토콜의 원리
- 파인튜닝(Fine-tuning)
LLM 01 - OpenAI Programming
Created: August 14, 2026 3:21 PM Class: LLM
LLM Optimization 분야 이해하기
✅ LLM이란?
LLM (Large Language Model, 대규모 언어 모델)은 방대한 양의 텍스트 데이터를 학습하여 텍스트를 이해·분석·생성할 수 는 인공지능 기술입니다.
- 대규모 텍스트 데이터를 학습해 다양한 종류의 글을 생성할 수 있음
- 트랜스포머(Transformer) 구조를 기반으로 한 신경망
- 수십억 개 이상의 파라미터(가중치)를 가진 인공 신경망으로 구성
- 대표 사례: OpenAI의 GPT (Generative Pretrained Transformer)
LLM의 특징
- 인간의 언어나 복잡한 데이터를 인식·해석할 수 있음
- 주어진 프롬프트(입력)에 따라 자연스러운 인간 언어와 유사한 텍스트를 생성
- 기존 기계학습 알고리즘보다 더 정확한 결과를 보이는 경우가 많음
- 콘텐츠 생성, 챗봇, 가상 어시스턴트 등 다양한 애플리케이션에서 활용됨
LLM의 구조
- 머신러닝, 특히 트랜스포머(Transformer) 모델이라는 신경망 구조를 기반으로 함
- 수십억 웨이트(가중치) 이상을 보유한 인공 신경망으로 구성되어, 방대한 패턴을 학습·기억함
🧭 LLM을 최적화하는 3가지 방법
LLM을 특정 목적/도메인에 맞게 최적화하는 방법은 크게 세 가지로 나눌 수 있습니다. 이 세 가지는 서로 배타적이지 않고, 필요에 따라 조합해서 사용하는 경우가 많습니다.
1. 프롬프트 엔지니어링 (Prompt Engineering)
- GPT와 같은 LLM에게 질문을 잘 던지는 방법을 설계하는 기술
- 모델 자체를 수정하지 않고, 입력(프롬프트)만 최적화해서 원하는 출력을 얻는 방식
- 별도 인프라나 학습 과정이 필요 없어 가장 접근성이 높고 비용이 저렴함
- 누구나 시도할 수 있는 일반 사용자의 skill 영역
- 예: 명확한 지시문 작성, 예시(few-shot) 제공, 출력 형식 지정 등
2. 파인 튜닝 (Fine-tuning)
- 기존 LLM을 특정 분야에 특화된 언어 데이터로 추가 학습시키는 방법
- 모델의 가중치 자체를 조정하므로, 반복적인 형식·말투·행동 패턴을 일관되게 학습시키는 데 효과적
- 모델을 직접 다루는 작업이라 AI 개발자의 영역에 가까움
- 데이터 준비와 학습 비용이 필요하므로, 프롬프트 엔지니어링보다 투자 비용이 큼
3. 검색증강생성 (RAG, Retrieval-Augmented Generation)
- 이미 알려진 웹 검색 결과나 지식 데이터베이스(문서, DB 등)를 먼저 검색한 뒤, 이를 근거(context)로 LLM이 답변을 생성하도록 하는 방식
- 모델이 학습하지 않은 최신 정보나 사내/전문 지식을 답변에 반영할 수 있음
- 검색 파이프라인 구축이 필요하므로 AI 개발자의 영역
💡 세 방법 모두 넓게 보면 아래 두 축의 문제로 이해할 수 있습니다.
- Context Optimization (모델이 무엇을 알아야 하는가?) → RAG, 파인튜닝
- LLM Optimization (모델이 어떻게 행동해야 하는가?) → 프롬프트 엔지니어링, 파인튜닝
🔀 최적화 흐름 (The Optimization Flow)

위 다이어그램은 OpenAI가 소개한 “LLM 최적화 프레임워크”를 기반으로 합니다. 두 축으로 구성됩니다.
- 세로축 (Context Optimization): 모델이 알아야 할 정보가 얼마나 충분한가
- 가로축 (LLM Optimization): 모델이 얼마나 원하는 방식으로 행동하는가
| 구간 | 설명 |
|---|---|
| Prompt Engineering (좌하단) | 기본 프롬프트로 시작 → 결과 평가(Evaluate) → few-shot 예시 추가 |
| Fine-tuning (우하단) | 특화 데이터로 모델 자체를 재학습시켜 행동/형식을 교정 |
| RAG (좌상단) | 단순 검색 추가 → HyDE 검색 + 사실 확인(fact-checking) 단계 추가 등으로 고도화 |
| All of the above (우상단) | RAG로 얻은 콘텐츠를 학습 예시로 활용해 파인튜닝까지 결합 |
일반적인 실무 흐름은 다음과 같이 진행되는 경우가 많습니다.
- 프롬프트 엔지니어링으로 먼저 시작해 평가 기준(baseline)을 세운다
- 모델이 정보를 몰라서 틀린다면 → RAG로 관련 지식을 검색해 제공
- 정보는 맞는데 형식·일관성·말투가 안 맞는다면 → 파인튜닝으로 행동을 교정
- 필요하면 RAG와 파인튜닝을 함께 사용해 정확도를 더 끌어올린다
이 흐름은 항상 “프롬프트 → RAG → 파인튜닝” 순서로 단순하게 진행되는 것은 아니며, 실제로는 문제의 원인(지식 부족 vs. 행동/형식 불일치)을 파악해 필요한 축으로 왔다 갔다 하며 반복 개선하는 과정에 가깝습니다.
📊 세 가지 방법 비교
| 구분 | 프롬프트 엔지니어링 | 파인 튜닝 | RAG |
|---|---|---|---|
| 핵심 목적 | 입력을 잘 설계해 원하는 출력 유도 | 모델 자체를 특정 분야에 맞게 재학습 | 외부 지식을 검색해 답변에 반영 |
| 모델 수정 여부 | ❌ (수정 없음) | ✅ (가중치 조정) | ❌ (모델은 그대로, 입력 컨텍스트만 보강) |
| 적합한 상황 | 형식·톤·간단한 지시 조정 | 반복되는 행동 패턴·전문 형식 학습 | 최신 정보·사내 문서 등 지식 보강 |
| 비용/난이도 | 낮음 | 높음 (데이터·학습 비용) | 중간 (검색 파이프라인 구축 필요) |
| 주요 사용자 | 일반 사용자 | AI 개발자 | AI 개발자 |
OpenAI의 프롬프트 구조
✅ OpenAI API가 받는 값
OpenAI의 Chat Completions API는 하나의 문자열이 아니라, role과 content로 이루어진 메시지들의 리스트(messages)를 입력으로 받습니다.
model = "gpt-4o",
messages = [
{"role": "system", "content": "너는 번역을 잘하는 기계야"},
{"role": "assistant", "content": "정중한 표현으로 could, would를 많이 사용해"},
{"role": "user", "content": "라면먹고 갈래요를 맛있게 영어로 번역해줘"},
{"role": "user", "content": "그리고 캘리포니아 발음으로 읽어줘"}
]
각 메시지는 순서대로 쌓여 모델에게 전달되며, 모델은 이 메시지들의 흐름을 하나의 대화로 인식하고 다음 응답을 생성합니다.
🎭 챗GPT와 역할놀이(페르소나)를 한다
OpenAI의 프롬프트 구조는 결국 모델에게 역할극(페르소나)을 부여하는 구조입니다. role 값에 따라 같은 텍스트라도 모델이 받아들이는 의미가 달라집니다.
| role | 의미 | 설명 |
|---|---|---|
| system | 역할 지시 | 모델이 대화 전체에서 유지해야 할 역할·성격·규칙을 지시 |
| assistant | 정보 제공 | 모델이 참고할 주변 설명, 모범 답안, 기타 정보를 미리 제시 |
| user | 요청/질문 | 사용자가 실제로 원하는 정확한 질문이나 요구사항을 지시 |
role: system — 역할을 지시
- 대화 전체를 관통하는 모델의 정체성과 행동 규칙을 설정하는 메시지
- 예시:
"너는 번역을 잘하는 기계야"→ 모델이 앞으로 “번역가”라는 페르소나로 행동하도록 고정 - 대화 맨 앞에 위치하며, 모델이 이후 모든 응답에서 이 지시를 기준으로 행동하도록 하는 기준점 역할
role: assistant — 주변 정보·모범답안 제시
- 원래는 모델이 생성한 응답이 담기는 자리이지만, 개발자가 미리 예시 답변이나 참고 정보를 채워 넣는 용도로도 사용 가능
- 예시:
"정중한 표현으로 could, would를 많이 사용해"→ 실제 사용자의 요청은 아니지만, 모델이 답변할 때 참고할 스타일 가이드/모범답안 역할 - 여러 개의 assistant 메시지를 이어 붙이면, 모델에게 여러 조건이나 예시를 단계적으로 학습시키는 효과를 줄 수 있음
role: user — 정확한 질문·요구 지시
- 실제 사용자가 모델에게 무엇을 해달라고 요청하는지를 담는 메시지
- 예시:
"라면먹고 갈래요를 맛있게 영어로 번역해줘","그리고 캘리포니아 발음으로 읽어줘" - 하나의 요청에 대해 user 메시지를 여러 번 이어 붙여 요구사항을 세분화할 수도 있음
🧩 구조 예시로 보는 역할 분배
트로트 작사를 요청하는 예시를 보면, 세 role이 어떻게 나뉘어 쓰이는지 명확히 드러납니다.
role, system : 넌 트로트 작사가야
role, assistant : 사랑의 시련을 표현해줘
role, assistant : 인생의 굴곡을 표현해줘
role, assistant : 후렴구를 반복하여 중독적이어야 해
role, assistant : 놀자 놀자 젊어서 놀자 와 같은 표현을 해줘
role, assistant : 노래는 2절이야
role, assistant : 노래는 전체 2분 30초 길이야
role, user : 트로트 가사를 써줘
이 구조를 역할별로 나누어 보면:
- system 1개 → “트로트 작사가”라는 정체성/역할을 단 한 번 고정
- assistant 6개 → 주제, 표현 방식, 후렴구 조건, 분량, 길이 등 세부 조건과 참고 정보를 겹겹이 쌓음
- user 1개 → 최종적으로 “가사를 써줘”라는 실행 요청만 짧게 전달
즉, 잘 짜여진 프롬프트는 system이 큰 틀(역할)을 잡고, assistant가 그 역할 안에서 지켜야 할 세부 규칙·정보를 층층이 쌓아두며, user는 그 위에서 실제 실행을 요청하는 역할 분업 구조로 이루어져 있습니다.
🔍 참고: role의 확장
OpenAI Chat Completions API는 system / user / assistant 세 가지 기본 role 외에도, 함수·도구 호출 결과를 전달하는 developer 메시지와 tool 메시지도 지원합니다. 다만 프롬프트 설계의 기본 뼈대는 여전히 system·assistant·user 세 역할의 조합입니다.
OpenAI 플랫폼
✅ OpenAI REST API 이해
OpenAI를 실제로 사용하려면, 결국 REST API를 통해 서버와 데이터를 주고받는 방식을 이해해야 합니다. 낯선 용어가 많아 보이지만, 결국은 “클라이언트가 서버에게 요청을 보내고 응답을 받는” 통신의 기본 원리 위에 OpenAI만의 규칙이 얹혀 있는 구조입니다.
REST API
- REST(Representational State Transfer)는 웹에서 자원을 주고받기 위한 API 설계 방식으로, 주소(URL)와 HTTP 메서드를 이용해 데이터를 요청하고 응답받는 방식
- OpenAI API도 REST 방식을 따르기 때문에, 정해진 주소로 정해진 형식의 데이터를 보내면 정해진 형식의 응답을 받을 수 있음
HTTP 프로토콜 이해
- HTTP(HyperText Transfer Protocol): 웹페이지 등을 전송하는 통신 규약(프로토콜)
- HTTPS: HTTP와 동일한 통신 방식이지만, 통신 시 데이터를 암호화하여 주고받는다는 차이가 있음 (OpenAI API도 HTTPS 사용)
데이터를 주고받는 방식
- 서로 데이터를 주고받을 때는 주소(URL)에 대해 데이터를 담아 보내는 방식으로 통신
- 이때 보내는 데이터는 헤더(header)와 실제 내용(body)으로 구성됨
- 헤더: 통신 방식, 데이터 형식, 인증 정보 등 부가 정보
- 바디: 실제로 전달하고 싶은 데이터 내용
GET 방식과 POST 방식
- GET 방식: 주소에 입력값을 함께 붙여서 보내는 방식 (예:
주소?key=value처럼 URL 자체에 데이터를 실어 보냄) - POST 방식: 주소는 그대로 두고, 내용(body)에 입력값을 넣어서 보내는 방식
- OpenAI의 채팅 요청은 body에 모델명, 메시지 등 많은 데이터를 실어야 하므로 POST 방식을 사용
학습 팁
- REST API, HTTP 같은 개념은 처음엔 낯설지만, 반복해서 외우고 익히는 것이 빠른 습득 방법
- 기초 개념(외움의 바탕)이 튼튼해야 그 위에 실전 활용(집짓기)이 가능함
📦 HTTP 메시지 구조
HTTP로 주고받는 메시지는 다음 네 부분으로 구성됩니다.
| 구성 요소 | 설명 |
|---|---|
| start-line (시작 라인) | 요청/응답의 첫 줄. 어떤 메서드(GET/POST 등)로, 어떤 주소에, 어떤 프로토콜 버전으로 요청하는지 명시 |
| header (헤더) | 통신에 대한 부가 정보 (데이터 형식, 인증 키 등) |
| empty line (공백 라인, CRLF) | 헤더와 바디를 구분해주는 빈 줄 |
| message body (메시지 바디) | 실제로 주고받는 데이터 내용 |
🔗 OpenAI 요청/응답 예시 (클라이언트 ↔︎ 서버)
요청(클라이언트 → 서버)할 때 필요한 정보
- 어느 서버로 가는가 (주소):
https://api.openai.com - 요청 서비스(엔드포인트):
/v1/chat/completions - 데이터 중 헤더:
- “난 JSON으로 통신할거야” →
Content-Type: application/json - “나의 신원은 이 key야” →
Authorization: Bearer {API_KEY}(OpenAI에서 발급받은 API 키를 헤더에 담아 내가 누구인지 인증)
- “난 JSON으로 통신할거야” →
- 데이터 중 데이터(바디): 입력값을 JSON 형식으로 담아 보냄 (예: 모델명, messages 배열 등)
응답(서버 → 클라이언트)으로 돌아오는 정보
- 데이터 중 헤더: “JSON 값으로 보낼게” → 응답도
Content-Type: application/json형식임을 헤더에 명시 - 데이터 중 데이터(바디): 결과값이 JSON 형식으로 담겨서 돌아옴 (예: 모델이 생성한 답변, 사용 토큰 수 등)
흐름 정리
- 클라이언트가
https://api.openai.com/v1/chat/completions주소로 POST 요청을 보냄 - 요청 헤더에는 데이터 형식(JSON)과 인증 정보(API 키)를 담음
- 요청 바디에는 모델명, 대화 메시지(messages) 등 실제 입력 데이터를 JSON으로 담음
- 서버는 이를 처리한 뒤, 헤더에 응답 형식(JSON)을 담고 바디에 결과 데이터(생성된 답변 등)를 JSON으로 담아 클라이언트에게 돌려줌
즉, OpenAI API를 사용한다는 것은 결국 정해진 주소로, 정해진 헤더와 바디 형식(JSON)을 갖춰 HTTP POST 요청을 보내고, 같은 형식의 JSON 응답을 받는 과정이라고 이해할 수 있습니다.
OpenAI REST API
✅ OpenAI REST API
- OpenAI platform에서는 REST API를 지원함
- 즉, 입력으로 정해진 JSON 데이터를 보내면 JSON 데이터로 응답하는 구조
🔗 REST API는 처음 header와 DATA를 보낸다
- 다음 주소(
https://api.openai.com/v1)에 지정된 REST API들이 있음 - 그중 우리는 chat을 써보기로 함
URL = "https://api.openai.com/v1/chat/completions"
이 주소는 OpenAI가 제공하는 여러 REST API 엔드포인트 중, 대화형 응답(chat)을 생성하는 기능을 호출하기 위한 주소입니다.
📋 헤더(header)
헤더는 HTTP 프로토콜상 HTML/JSON 문장이 아닌, 세션 처리 중에 보내는 정보입니다. 헤더에는 “JSON을 보내겠다”는 정보와, 베어러(Bearer) 방식의 인증키를 보내는 작업이 담깁니다.
headers = {
"Authorization": f"Bearer{API_KEY}",
"Content-Type": "application/json"
}
Authorization:Bearer {API_KEY}형식으로, 나의 OpenAI API 키를 담아 “나는 인증된 사용자다”를 증명Content-Type:application/json— “나는 지금 JSON 형식의 데이터를 보낸다”는 것을 서버에게 알림
📦 보내는 데이터(data)
보내는 JSON 데이터에는 모델명(model)과 질문(messages)이 필수로 포함되어야 합니다.
data = {
"model": "model-identifier",
"messages": [
{"role": "user", "content": context}
],
}
model: 사용할 모델의 이름/식별자 (예:gpt-4o등)messages: 대화 메시지 배열. 최소한role: "user"와 그에 담길content(실제 질문/요청 내용)가 있어야 함
🧭 정리하면
- OpenAI REST API를 사용하려면 주소(URL), 헤더(header), 데이터(data) 세 가지를 갖춰 요청을 보내야 함
- 주소는
https://api.openai.com/v1/chat/completions처럼 사용하려는 기능(chat)에 맞는 엔드포인트를 선택 - 헤더에는 데이터 형식(JSON)과 인증 정보(API_KEY, Bearer 방식)를 담음
- 데이터(body)에는 모델명(model)과 대화 내용(messages)을 최소한 포함해 JSON으로 보냄
OpenAI REST API 옵션
✅ OpenAI REST API
- OpenAI platform에서는 REST API를 지원함
- 즉, 입력으로 정해진 JSON 데이터를 보내면 JSON 데이터로 응답하는 것
🎛️ 요청에 옵션을 넣어서 보낼 수 있다
- 요청 데이터에는 model, messages 외에도 다양한 옵션값을 함께 넣어 보낼 수 있음
- 정확한 옵션의 종류와 의미는 매뉴얼(공식 문서)을 참고하는 것이 가장 정확함
- 옵션 하나하나를 모두 완벽히 설명하기는 어렵고, 실전에서는 직접 값을 바꿔가며 결과를 비교하는 방식으로 익히는 경우가 많음
이론적 배경
- OpenAI 기본 모델은 질문(한글) → 영어로 내부 처리 → 답변(영어) → 한글로 변환하는 방식으로 작동하는 경우가 많음
- 이 때문에 작은/경량 모델에서는 아래에서 설명하는 옵션들의 세부 조정(디테일)이 잘 느껴지지 않는 수준으로 답이 나올 수 있음
- 옵션값을 바꿔가며 직접 실험해보는 것이 가장 확실한 학습 방법
그래도 가장 자주 조절하는 옵션은 온도(temperature)
- 답변의 창의성/무작위성을 조절하는 값으로, 실무에서 가장 빈번하게 조정되는 옵션
📦 요청 데이터 예시
model = "model-identifier"
messages = [
{"role": "system", "content": "You can speak Korean very well, 너는 한국어로 이야기하는 인공지능이야"}, # 시스템의 역할이 뭔지 알려줌
{"role": "assistant", "content": "판교지역은 한국의 실리콘밸리라고 할 수 있어"}, # 도움되는 정보를 제공
{"role": "assistant", "content": "나는 스테이크를 좋아해"},
{"role": "user", "content": "판교에서 유명한 음식점을 소개하는 문장을 반페이지 정도 써줘?"} # 원하는 지시
]
# 아래의 조정값들이 한글에서는 잘 안먹는 것 같다.. 영어로 번역되었다 한국어로 바뀌어서 그런지....
temperature = 0.7, # 0~1, 0에 가까울수록 온도가 낮아 딱딱하고 맞게.. 0.9가 가장 핫소리 섞여
max_tokens = 1024, # 응답받을 메시지 최대 토큰(단어) 수 설정
top_p = 1, # 토큰 샘플링 확률을 설정.. 즉 1이면 거의 대다수 100% 확률 빈도의 토큰(단어/문장)을 사용..일반적 단어
frequency_penalty = 0, # 일반적으로 나오지 단어를 억제하는 정도.. 욕이 튀어나올 수도
presence_penalty = 0, # 동일한 단어나 구문이 반복되는 것을 억제하는 정도
stop = ["할말 끝", "끝났"], # 생성된 텍스트에서 종료 구문을 설정
🔧 옵션별 설명
| 옵션 | 역할 | 설명 |
|---|---|---|
| temperature | 답변의 무작위성(창의성) 조절 | 0~1(또는 그 이상) 범위. 0에 가까울수록 결정적이고 딱딱한 답변, 1에 가까울수록 다양하고 창의적인(때로는 엉뚱한) 답변이 나옴 |
| max_tokens | 응답 길이 제한 | 모델이 생성할 수 있는 응답의 최대 토큰(단어 조각) 수를 설정. 이 값을 넘으면 답변이 중간에 잘릴 수 있음 |
| top_p | 토큰 샘플링 범위 조절 (nucleus sampling) | 확률 누적치가 top_p 이하인 토큰들만 후보로 사용. 1이면 거의 모든 후보 단어를 고려(일반적인 단어 위주), 값이 작을수록 후보군이 좁아져 더 예측 가능한 답변이 나옴 |
| frequency_penalty | 반복 단어 억제 | 이미 자주 등장한 단어가 또 나올 확률을 낮춤. 값이 높을수록 같은 단어의 반복을 억제하지만, 너무 높으면 부자연스러운 단어(욕설 등)가 튀어나올 수도 있음 |
| presence_penalty | 새로운 주제 유도 | 이미 등장한 단어/구문 자체가 다시 나오는 것을 억제해, 새로운 단어나 화제를 꺼내도록 유도 |
| stop | 생성 종료 시점 지정 | 지정한 문자열이 생성되면 그 시점에서 응답 생성을 멈춤. 대화의 끝을 표시하거나 불필요한 이어지는 텍스트를 막을 때 사용 |
참고
- 위 조정값들은 영어 프롬프트에서 효과가 더 뚜렷하게 나타나는 경향이 있음. 한국어 요청은 모델 내부에서 영어로 번역되었다가 다시 한국어로 변환되는 과정을 거치는 경우가 있어, temperature 등 세부 옵션의 효과가 상대적으로 덜 느껴질 수 있음
OpenAI REST API (응답 & API Key 관리)
📥 REST API에서 리턴된 내용
REST API 요청을 보내면, 서버는 JSON 형식의 응답을 돌려줍니다. 이 안에서 우리가 실제로 필요한 답변은 choices → message → content 경로를 따라가면 찾을 수 있습니다 (즉, 응답 JSON을 파싱해서 원하는 값만 꺼내 써야 함).
응답 예시
{
"id": "chatcmpl-BJUqI9UCZCo963jTVfzl42sazRxa4",
"object": "chat.completion",
"created": 1743986490,
"model": "ft:gpt-3.5-turbo-0125:kopoctc::B3I2DFJJ",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "블라우스와 스커트를 추천해드릴게요. 봄날씨에 적합하고 결혼식에도 적합한 옷차림이에요. 스커트는 플...",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 93,
"completion_tokens": 229,
"total_tokens": 322,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": null
}
주요 필드 설명
| 필드 | 설명 |
|---|---|
| id | 이번 응답(completion)을 식별하는 고유 ID |
| object | 응답의 종류. chat.completion은 채팅 응답임을 의미 |
| created | 응답이 생성된 시각 (유닉스 타임스탬프) |
| model | 이번 응답을 생성한 모델 이름 (예: 파인튜닝된 모델의 경우 ft: 접두사가 붙음) |
| choices | 실제 생성된 답변들의 배열. 여러 개의 답변(n 옵션)을 요청하면 그 개수만큼 배열이 채워짐 |
| choices[].message.role | 응답 메시지의 역할. 모델이 생성한 답변은 항상 assistant |
| choices[].message.content | 우리가 실제로 원하는 답변 텍스트가 담기는 자리 |
| choices[].finish_reason | 응답이 어떻게 종료되었는지 (stop: 정상 종료, length: 최대 토큰 도달로 잘림 등) |
| usage | 이번 요청에 사용된 토큰 수 정보 (prompt_tokens: 입력 토큰, completion_tokens: 응답 토큰, total_tokens: 합계) — 과금과 직결되는 정보 |
핵심 포인트
- 우리가 코드에서 꺼내 써야 하는 값은 결국
choices[0].message.content한 곳 - 나머지 필드(
usage,id,model등)는 상황에 따라 참고용으로 사용 (예: 토큰 사용량 모니터링, 어떤 모델이 응답했는지 확인 등)
🔑 발급받은 API Key, 환경변수에 저장하기
- OpenAI에서 발급받은 API KEY는 프로그램 코드에 직접 쓰지 않고, 시스템 환경변수에 저장해두는 것이 권장되는 방식
- 그렇지 않으면 프로그램(코드) 안에 키 값을 매번 직접 써줘야 함 (코드에 키가 그대로 노출되어 보안상 위험함)
왜 환경변수를 쓰는가
- 대부분의 OpenAI 메뉴얼(공식 문서)에 있는 예시 코드는, KEY 값을 시스템 환경변수에 저장한 뒤 프로그램에서 불러 사용하는 방식을 전제로 작성되어 있음
- 이 방식을 그대로 따라 써도 되고, 필요하면 코드에 직접 넣어 쓸 수도 있음 (다만 보안·관리 측면에서는 환경변수 저장이 더 안전한 방법)
환경변수 등록 예시 (Windows 기준)
setx OPENAI_API_KEY "your_api_key_here"
이렇게 등록해두면, 코드에서는 키 값을 직접 쓰지 않고 OPENAI_API_KEY라는 이름으로 환경변수를 불러와 사용할 수 있습니다.
import os
api_key = os.environ.get("OPENAI_API_KEY")
🛠️ OpenAI를 사용하는 두 가지 방법
- REST API를 직접 가져다 쓰는 방식
- 앞서 정리한 것처럼 URL, header, data를 직접 구성해 HTTP 요청을 보내는 방식
- 어떤 언어/환경에서도 사용 가능하지만, 요청/응답 구조를 직접 다뤄야 함
- OpenAI에서 제공하는 라이브러리(SDK)를 이용하는 방식
- OpenAI가 공식 제공하는 라이브러리(예: Python의
openai패키지)를 설치해서 사용 - REST API 호출 과정을 라이브러리가 대신 처리해주므로, 더 간단한 코드로 동일한 기능을 사용할 수 있음
- OpenAI가 공식 제공하는 라이브러리(예: Python의
두 방식 모두 결국 같은 REST API를 호출하는 것이지만, 직접 HTTP 요청을 구성하느냐, 라이브러리가 감싸놓은 함수를 호출하느냐의 차이입니다.
OpenAI 라이브러리 방식
✅ OpenAI 라이브러리 방식
- OpenAI에서 제공하는 라이브러리(SDK)를 이용하면 프로그래밍을 할 수 있음
- 눈치가 있다면, 이 라이브러리 방식도 결국 REST API와 유사함을 알 수 있음
- OpenAI 플랫폼 매뉴얼을 보면 라이브러리 사용법이 아주 잘 정리되어 있음
🔀 REST API 방식과의 차이
지금까지 배운 REST API 방식과 라이브러리 방식은 결국 같은 곳(같은 서버, 같은 엔드포인트)에 요청을 보내는 동일한 작업입니다. 다만 그 요청을 누가 어떻게 감싸서 만들어주느냐가 다릅니다.
| 구분 | REST API 방식 (직접 호출) | 라이브러리(SDK) 방식 |
|---|---|---|
| URL 지정 | https://api.openai.com/v1/chat/completions 처럼 주소를 직접 명시 | OpenAI(base_url=..., api_key=...)로 클라이언트 객체를 한 번만 생성해두면, 이후에는 주소를 신경 쓸 필요 없음 |
| 헤더(header) 구성 | Authorization: Bearer {API_KEY}, Content-Type: application/json을 직접 딕셔너리로 작성 | 클라이언트 생성 시 api_key=API_KEY만 넘기면, 인증 헤더는 라이브러리가 자동으로 붙여줌 |
| 요청 전송 방법 | HTTP POST 요청을 직접 구성 (예: requests.post(url, headers=..., json=data)) | client.chat.completions.create(...)처럼 함수 호출 한 줄로 대체됨 |
| 요청 데이터(JSON) 작성 | data = {...} 딕셔너리를 만들어 JSON으로 직접 변환·전송 | model=, messages= 등 파이썬 함수의 인자(argument)로 그대로 전달 (내부적으로 라이브러리가 JSON으로 변환해서 보냄) |
| 응답(response) 처리 | 응답 JSON을 직접 파싱 (response.json()["choices"][0]["message"]["content"]처럼 키로 접근) | 응답이 파이썬 객체로 반환되어, completion.choices[0].message.content처럼 점(.) 표기법(속성 접근)으로 바로 사용 가능 |
| 에러 처리 | HTTP 상태 코드를 직접 확인하고 예외 처리를 구현해야 함 | 라이브러리가 자체적으로 예외(exception)를 발생시켜, try/except로 더 파이썬스럽게 처리 가능 |
🧭 한마디로 정리하면
- REST API 방식: 주소(URL) + 헤더 + JSON 데이터를 내가 직접 손으로 조립해서 HTTP 요청을 보내는 방식
- 라이브러리 방식: 그 조립 과정을 OpenAI가 미리 함수로 만들어둔 것 → 함수 호출 한 번으로 동일한 REST API 요청이 내부적으로 실행됨
즉, 라이브러리 방식은 REST API 호출을 더 편하게 감싸놓은 래퍼(wrapper)일 뿐, 실제로 서버와 주고받는 내용(주소, 헤더, 메시지 구조, 옵션값 등)은 REST API 방식과 동일합니다.
⚠️ 주의할 점
- 보내는 JSON을 key=value 형식으로 쓰는 것에 유의해야 함
- 라이브러리 방식에서는
model="gpt-4o",messages=[...]처럼 파이썬 함수 인자 형태(key=value)로 작성 - 이 값들이 내부적으로는 결국 REST API가 요구하는 JSON 바디(
{"model": "gpt-4o", "messages": [...]})로 변환되어 전송됨 - 즉, 겉모습은 파이썬 함수 호출처럼 보이지만, 실제로는 REST API 때 배운 것과 동일한 데이터(model, messages, temperature, max_tokens 등)를 그대로 채워 넣는 것
- 라이브러리 방식에서는
크롤링과 HTTP 프로토콜의 원리
✅ 크롤링하다 막히기나 하고
- 웹사이트를 크롤링하려면, 먼저 HTTP 프로토콜의 원리를 알아야 함
- 자, curl을 통해 http 요청 시
H(헤더)가 붙는다는 것을 확인해보자 - 낯선 웹사이트를 curl로 그대로 호출하면, 생각보다 접속이 잘 안 되는 경우가 많음 → 이유를 원리 차원에서 이해할 필요가 있음
🌐 브라우저는 사실 훨씬 많은 정보를 함께 보내고 있다
우리가 브라우저 주소창에 웹사이트 주소만 입력하고 접속하면, 겉으로는 URL 하나만 요청하는 것처럼 보이지만, 실제로는 브라우저가 그 뒤에서 다양한 부가 정보(헤더)를 함께 실어 보냅니다.
이걸 직접 확인하는 방법:
- 크롬에서 원하는 웹사이트에 접속
- F12(개발자 도구) → 네트워크(Network) 탭을 열기
- 페이지를 다시 로드(엔터 한번 다시 치기)해서 요청을 잡음
- 해당 요청 URL에서 오른쪽 마우스 → Copy → Copy as cURL로 복사
이렇게 복사한 curl 명령어를 열어보면, 우리가 입력한 URL 하나에 10개 가까운 헤더가 자동으로 실려 있는 것을 볼 수 있습니다.
📋 브라우저가 자동으로 붙이는 대표적인 헤더들
| 헤더 | 의미 | 원리 |
|---|---|---|
| Accept | “나는 이런 형식의 응답을 받을 수 있어” | HTML로 요청하면 서버는 HTML을, XML로 요청하면 XML을, 이미지(png/jpg)로 요청하면 이미지를 반환하도록 주소의 확장자나 이 헤더 값에 따라 응답 형식이 결정됨 |
| Accept-Language | “나는 이 언어로 된 응답을 원해” | 예: 한국어 우선(ko-KR) 설정 시, 서버가 다국어를 지원하면 한국어 콘텐츠를 우선 내려줌 |
| Connection: keep-alive | “이 연결을 끊지 말고 계속 유지해줘” | 매 요청마다 새로 연결을 맺지 않고, 하나의 연결로 여러 요청을 이어서 처리해 성능을 높임 |
| DNT (Do Not Track) | “나를 추적하지 말아줘” | 사용자의 트래킹 거부 의사를 서버에 전달하는 헤더 (지켜지는지는 서버 정책에 따라 다름) |
| Upgrade-Insecure-Requests | “가능하면 더 안전한(HTTPS) 접속으로 해줘” | 브라우저가 향상된 보안 접속을 요청한다는 의미 |
| User-Agent | “나는 이런 브라우저/운영체제야” | 브라우저 종류, 버전, OS 정보 등 내 환경 정보를 서버에 알려줌. 서버는 이 값을 보고 모바일용/PC용 페이지를 다르게 내려주기도 함 |
즉, 우리가 평소에 인식하지 못했을 뿐, 브라우저는 매 요청마다 “나는 누구고, 무엇을 원하고, 어떤 형식을 받을 수 있는지”를 헤더에 자동으로 실어 보내는 것입니다.
🖥️ Windows 환경에서 curl 명령어 다룰 때 주의할 점
- Cmd(명령 프롬프트)에서는 줄바꿈을
^로 처리해야 함 (Copy as cURL로 복사한 명령어에^가 붙어 있는 이유) - (참고로 PowerShell에서는 줄바꿈 기호가 ```(백틱)으로 다름 — 실행 환경에 따라 줄바꿈 문자가 다르다는 점만 알아두면 됨)
🔐 왜 헤더가 중요한가 — 원리 차원의 결론
- HTTP 요청은 URL만으로 이루어지지 않는다. 실제로는 URL + 다양한 헤더가 하나의 패키지로 전송됨
- 서버(또는 서버 앞단의 보안 장비)는 이 헤더 조합을 보고, “이건 진짜 사람이 정상적인 브라우저로 접속한 요청인지”를 판단하는 근거로 사용함
- 만약 이런 헤더들이 거의 없거나 브라우저가 보내는 것과 다르게 어색한 조합으로 요청이 오면, 비정상적이거나 자동화된(봇/크롤러) 요청으로 의심받을 수 있음
- 그 결과, 향상된 보안 계층(예: WAF/L7 방화벽 등 응용 계층 보안 장비)이 해당 요청을 자동으로 차단하는 경우가 발생함 → 이것이 바로 “크롤링이 막히는” 현상의 원리적 원인 중 하나
- 그러므로 개발자가 특정 사이트 하나만 우회하는 것보다, “운영자(보안 담당자)가 어떤 기준으로 정상/비정상 요청을 구분하는지”를 이해하는 쪽이 더 본질적인 접근이라는 관점도 있음 (공격자와 방어자 모두 서로의 원리를 알아야 한다는 의미)
🧭 핵심 요약 원리
웹사이트에 접속한다는 것 = URL을 요청하는 것이 아니라, “URL + 나를 설명하는 여러 헤더 정보”를 하나의 HTTP 요청으로 함께 보내는 것
이 원리는 특정 사이트 하나에 국한된 것이 아니라, 모든 웹사이트가 공통으로 따르는 HTTP 프로토콜의 기본 동작 방식입니다. 크롤링 코드를 작성할 때 단순히 URL만 요청하면 브라우저가 자동으로 채워주던 헤더 정보가 빠지게 되고, 이 차이 때문에 서버가 요청을 다르게(혹은 차단 대상으로) 처리하게 되는 것입니다.
[교수님 홈페이지] http://iamhpd8.cafe24.com/
curl --url ^"http://iamhpd8.cafe24.com/^" ^
-H ^"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7^" ^
-H ^"Accept-Language: ko,ko-KR;q=0.9,en;q=0.8^" ^
-H ^"Cache-Control: max-age=0^" ^
-H ^"Connection: keep-alive^" ^
-H ^"Upgrade-Insecure-Requests: 1^" ^
-H ^"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36^" ^
--insecure
curl --url ^"https://www.google.com/^" ^
-H ^"accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7^" ^
-H ^"accept-language: ko,ko-KR;q=0.9,en;q=0.8^" ^
-H ^"available-dictionary: :7+SR5NB8qv2v+Wvwi15vwRoVb/4bMsMh2QjOSpKvNeY=:^" ^
-H ^"cache-control: max-age=0^" ^
-b ^"[쿠키 데이터]" ^
-H ^"downlink: 10^" ^
-H ^"priority: u=0, i^" ^
-H ^"rtt: 50^" ^
-H ^"sec-ch-prefers-color-scheme: light^" ^
-H ^"sec-ch-ua: ^\^"Not=A?Brand^\^";v=^\^"99^\^", ^\^"Google Chrome^\^";v=^\^"151^\^", ^\^"Chromium^\^";v=^\^"151^\^"^" ^
-H ^"sec-ch-ua-arch: ^\^"x86^\^"^" ^
-H ^"sec-ch-ua-bitness: ^\^"64^\^"^" ^
-H ^"sec-ch-ua-form-factors: ^\^"Desktop^\^"^" ^
-H ^"sec-ch-ua-full-version: ^\^"151.0.7922.138^\^"^" ^
-H ^"sec-ch-ua-full-version-list: ^\^"Not=A?Brand^\^";v=^\^"99.0.0.0^\^", ^\^"Google Chrome^\^";v=^\^"151.0.7922.138^\^", ^\^"Chromium^\^";v=^\^"151.0.7922.138^\^"^" ^
-H ^"sec-ch-ua-mobile: ?0^" ^
-H ^"sec-ch-ua-model: ^\^"^\^"^" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" ^
-H ^"sec-ch-ua-platform-version: ^\^"19.0.0^\^"^" ^
-H ^"sec-ch-ua-wow64: ?0^" ^
-H ^"sec-fetch-dest: document^" ^
-H ^"sec-fetch-mode: navigate^" ^
-H ^"sec-fetch-site: same-origin^" ^
-H ^"sec-fetch-user: ?1^" ^
-H ^"upgrade-insecure-requests: 1^" ^
-H ^"user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36^" ^
-H ^"x-browser-channel: stable^" ^
-H ^"x-browser-copyright: Copyright 2026 Google LLC. All Rights Reserved.^" ^
-H ^"x-browser-validation: UnE0jsLmyOUuqSXxegAkM63DjAc=^" ^
-H ^"x-browser-year: 2026^" ^
-H ^"x-client-data: CKmdygEIk6HLAQiFoM0BCLfCzwEIrNuUMAio3JQwCO7flDAYwanKARiGu88BGKzflDA=^"
[네이버] https://www.naver.com/

curl --url ^"https://www.naver.com/^" ^
-H ^"accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7^" ^
-H ^"accept-language: ko,ko-KR;q=0.9,en;q=0.8^" ^
-H ^"cache-control: max-age=0^" ^
-b ^"[쿠키 데이터]" ^
-H ^"priority: u=0, i^" ^
-H ^"sec-ch-ua: ^\^"Not=A?Brand^\^";v=^\^"99^\^", ^\^"Google Chrome^\^";v=^\^"151^\^", ^\^"Chromium^\^";v=^\^"151^\^"^" ^
-H ^"sec-ch-ua-mobile: ?0^" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" ^
-H ^"sec-fetch-dest: document^" ^
-H ^"sec-fetch-mode: navigate^" ^
-H ^"sec-fetch-site: none^" ^
-H ^"sec-fetch-user: ?1^" ^
-H ^"upgrade-insecure-requests: 1^" ^
-H ^"user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36^"
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
public class OpenAIRestClient {
// 타임아웃 설정 (ms)
private static final int connTimeout = 5000;
private static final int readTimeout = 15000;
private static final String URL_ADDR = "https://api.openai.com/v1/chat/completions";
private static final String API_KEY = System.getenv("OPENAI_API_KEY");
public static void main(String[] args) {
// ===== 프롬프트 튜닝 영역 =====
String model = "gpt-4o";
String systemPrompt = "너는 번역을 잘하는 기계야";
String assistantPrompt = """
정중한 표현으로
could, would를
많이 사용해
""";
String userPrompt = """
라면먹고 갈래요를
맛있게 영어로
번역해줘
""";
double temperature = 0.7;
int maxTokens = 1024;
double topP = 1.0;
double frequencyPenalty = 0.0;
double presencePenalty = 0.0;
// ==============================
try {
String requestBody = buildRequestBody(
model,
systemPrompt, assistantPrompt, userPrompt,
temperature, maxTokens, topP, frequencyPenalty, presencePenalty
);
String responseBody = sendRequest(requestBody);
String content = extractContent(responseBody);
System.out.println("결과 -> " + content);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
// 여러 줄 프롬프트를 한 줄로 변환
private static String reqData(String multiLineText) {
StringBuilder sb = new StringBuilder();
String[] lines = multiLineText.split("\\r?\\n");
for (String line : lines) {
String trimmed = line.trim();
if (!trimmed.isEmpty()) {
if (sb.length() > 0) sb.append(" ");
sb.append(trimmed);
}
}
return sb.toString();
}
// 요청 JSON body 조립
@SuppressWarnings("unchecked")
private static String buildRequestBody(
String model,
String systemPrompt, String assistantPrompt, String userPrompt,
double temperature, int maxTokens, double topP,
double frequencyPenalty, double presencePenalty
) {
JSONObject root = new JSONObject();
root.put("model", model);
JSONArray messages = new JSONArray();
messages.add(buildMessage("system", reqData(systemPrompt)));
messages.add(buildMessage("assistant", reqData(assistantPrompt)));
messages.add(buildMessage("user", reqData(userPrompt)));
root.put("messages", messages);
root.put("temperature", temperature);
root.put("max_tokens", maxTokens);
root.put("top_p", topP);
root.put("frequency_penalty", frequencyPenalty);
root.put("presence_penalty", presencePenalty);
return root.toJSONString();
}
@SuppressWarnings("unchecked")
private static JSONObject buildMessage(String role, String content) {
JSONObject message = new JSONObject();
message.put("role", role);
message.put("content", content);
return message;
}
// HTTP POST 요청 전송 후 응답 body 반환
private static String sendRequest(String requestBody) throws Exception {
HttpURLConnection urlConnection = null;
OutputStream outputStream = null;
BufferedWriter bufferedWriter = null;
BufferedReader bufferedReader = null;
String readLine = null;
StringBuilder buffer = null;
String retStr = "";
try {
URL url = new URL(URL_ADDR);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setConnectTimeout(connTimeout);
urlConnection.setReadTimeout(readTimeout);
// 헤더 설정
urlConnection.setRequestProperty("Authorization", "Bearer " + API_KEY);
urlConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
urlConnection.setDoOutput(true);
urlConnection.setInstanceFollowRedirects(true);
// 요청 body 전송
outputStream = urlConnection.getOutputStream();
bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
bufferedWriter.write(requestBody);
bufferedWriter.flush();
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// 정상 응답 읽기
bufferedReader = new BufferedReader(
new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));
buffer = new StringBuilder();
while ((readLine = bufferedReader.readLine()) != null) {
buffer.append(readLine).append("\n");
}
retStr = buffer.toString();
} else {
// 에러 응답 처리
retStr = String.format("에러코드 : %s\n내용 : %s\n",
urlConnection.getResponseCode(),
urlConnection.getResponseMessage());
}
} catch (Exception ex) {
ex.printStackTrace();
retStr = "Exception발생 1";
} finally {
try {
if (bufferedWriter != null) { bufferedWriter.close(); }
if (outputStream != null) { outputStream.close(); }
if (bufferedReader != null) { bufferedReader.close(); }
} catch (Exception ex) {
ex.printStackTrace();
retStr = "Exception발생 2";
}
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return retStr;
}
// 응답 JSON에서 choices[0].message.content 추출
private static String extractContent(String responseBody) throws Exception {
JSONParser parser = new JSONParser();
JSONObject root = (JSONObject) parser.parse(responseBody);
JSONArray choices = (JSONArray) root.get("choices");
JSONObject firstChoice = (JSONObject) choices.get(0);
JSONObject message = (JSONObject) firstChoice.get("message");
return (String) message.get("content");
}
}
결과 -> “Would you like to come over for some ramen?” 또는 “How about coming over for some delicious ramen?”라고 번역할 수 있습니다.
Process finished with exit code 0
파인튜닝(Fine-tuning)
✅ Fine-tuning이란
- Fine-Tuning은 이미 학습된(사전 학습된) 모델을 특정 작업에 맞게 추가로 훈련시키는 과정
- 일반적으로 사전 학습(pre-training)된 모델을 사용하여, 새로운 데이터에 맞게 조정함으로써 특정 도메인이나 작업에 대해 더 높은 성능을 발휘할 수 있도록 함
- 자연어 처리, 이미지 인식, 음성 인식 등 다양한 분야에서 널리 사용됨
쉽게 이해하기
- 기존의 언어모델은 범용적인 지식을 가진 모델 → 파인튜닝된 모델은 특별한 지식 분야의 지식을 더 가지게 함
- 비유: 기존 모델이 국어 50점, 영어 50점, 수학 50점을 받는 사람이라면, 수학 점수만 80점으로 끌어올리도록 수학만 집중적으로 더 학습시키는 것
🔀 전이학습(Transfer Learning) vs 파인튜닝(Fine-Tuning)
두 개념 모두 기존 모델을 가져와 파라미터·레이어 등을 조절하고 데이터로 다시 학습시킨다는 점은 같지만, 목적과 접근 방식에서 차이가 있습니다.
- 전이학습: 예를 들어 엑스레이 사진 판독 모델을 학습하려 할 때, 아무것도 없는 깡통 모델로 처음부터 학습하는 게 아니라 꽃 이미지를 보고 종류를 맞추도록 학습된 모델을 가져와서 조정하는 방식
- 파인튜닝: 그 모델을 엑스레이 데이터로 다시 학습시켜, 학습 데이터의 양과 학습 시간을 단축하면서도 양질의 모델 학습을 할 수 있게 하는 기법
즉, 전이학습이 조금 더 큰 개념(다른 도메인의 지식을 가져와 새로운 문제에 적용)이고, 파인튜닝은 그 안에서 사전 훈련된 모델을 특정 작업에 세부적으로 최적화하는 절차라고 볼 수 있습니다.
비교표
| 구분 | 전이학습 (Transfer Learning) | 파인튜닝 (Fine-Tuning) |
|---|---|---|
| 목적 | 대규모 데이터셋에서 학습된 지식을 새로운 작업에 적용 | 사전 훈련된 모델을 특정 작업에 맞추어 추가 조정 |
| 핵심 과정 | 기존 모델의 지식을 새로운 문제에 전달 | 기존 모델의 세부 사항을 새로운 문제에 최적화 |
| 데이터 요구량 | 상대적으로 적음 | 상대적으로 적으나, 모델을 특정 작업에 맞게 조정 필요 |
| 적용 예시 | ImageNet으로 학습된 모델을 특정 동식물 분류에 적용 | 의료 영상 분석을 위해 일반 이미지 분류 모델을 조정 |
| 학습 시간 | 새로운 학습보다 단축 | 사전 훈련된 모델을 기반으로 하므로 단축 |
| 적합성 | 다양한 작업에 일반적인 모델 적용 가능 | 매우 특정한 작업이나 데이터셋에 최적화 필요 |
🐶 실습: 파인튜닝해보자 (강아지 이름 짓기)
실습 목표
- GPT 모델 중 하나를 골라 “눈 온 마당에 뒹굴고 있는 강아지 이름 지어줘”라고 명령했을 때, 강아지 이름을 짓는 것에 특화되도록 파인튜닝
절차
- 아래 형식으로 파인튜닝용 학습 데이터를 만듦
- 이 데이터를 메모장으로 저장하고, 확장자를
.jsonl로 지정 (예:dog_name.jsonl) - 제공된 파일을 그대로 활용
데이터 안의 역할(role) 구조
| role | 내용 |
|---|---|
| system | “강아지 이름을 지어드립니다.” — 모델의 역할을 고정 |
| user | 강아지의 특정 상황·모습 등 다양한 질문 |
| assistant | 그 상황에 맞는 귀엽고 권장할 만한 이름을 답안으로 제시 |
학습 데이터 형식 (JSONL)
JSONL(JSON Lines)은 한 줄에 하나씩 완결된 JSON 객체를 나열하는 형식입니다. 파인튜닝은 이렇게 “system-user-assistant로 구성된 대화 예시”를 수십~수백 줄 모아서, 모델이 이 패턴(질문 → 이름 짓기 답변)을 반복적으로 학습하도록 만드는 방식입니다.
{"messages": [{"role": "system", "content": "강아지 이름을 지어드립니다."}, {"role": "user", "content": "귀가 축늘어지고 코가 까만 강아지 이름은?"}, {"role": "assistant", "content": "깜죽이"}]}
{"messages": [{"role": "system", "content": "강아지 이름을 지어드립니다."}, {"role": "user", "content": "눈이오면 까만눈 두개와 깜모만 하나만 보이는 강아지 이름은?"}, {"role": "assistant", "content": "까만구슬새개"}]}
{"messages": [{"role": "system", "content": "강아지 이름을 지어드립니다."}, {"role": "user", "content": "시골장터에서 오천원 주고 사온 강아지 이름은?"}, {"role": "assistant", "content": "오천이"}]}
{"messages": [{"role": "system", "content": "강아지 이름을 지어드립니다."}, {"role": "user", "content": "바둑무늬가 있는 강아지 이름 만들어줘"}, {"role": "assistant", "content": "바둑이"}]}
{"messages": [{"role": "system", "content": "강아지 이름을 지어드립니다."}, {"role": "user", "content": "심부름도 잘하고 주인을 잘따르는 똑똑한 강아지 이름 지어줘"}, {"role": "assistant", "content": "똑강지"}]}
이 실습의 핵심 원리
- 한 줄 한 줄이 “이런 질문(user)이 오면, 이런 답(assistant)을 해야 한다”는 하나의 학습 예시(샘플)
- system 메시지는 모든 줄에서 동일하게 “강아지 이름을 지어드립니다”로 고정 → 모델에게 일관된 역할을 반복 주입
- user 메시지는 다양한 상황(생김새, 습관, 만난 계기 등)을 다양하게 제시
- assistant 메시지는 그 상황에 어울리는 짧고 창의적인 이름을 정답 예시로 제공
- 이런 데이터를 다량으로 학습시키면, 모델이 새로운(학습 데이터에 없던) 상황을 물어봐도 비슷한 패턴으로 짧고 재치있는 이름을 짓는 방식을 스스로 체득하게 됨
즉, 프롬프트 엔지니어링이 “매번 질문할 때마다 지시문을 잘 짜는 것”이라면, 파인튜닝은 “이런 식으로 답해야 한다는 예시 자체를 모델에게 학습시켜서, 프롬프트를 짧게 줘도 원하는 스타일로 답하게 만드는 것”이라는 차이가 이번 실습에서 드러납니다.
