안녕하세요, AI API 통합을 8년 넘게 다뤄온 시니어 엔지니어입니다. 저는 지난 6개월간 한국 동남아시아권 50여 개 스타트업의 LLM 비용을 진단하면서 한 가지 명확한 사실을 발견했습니다. 대부분의 팀이 비싼 모델을 무차별적으로 사용하고 있다는 점입니다. 사실 80% 이상의 질의는 DeepSeek V3.2 수준으로 충분히 처리할 수 있는데도, 모든 요청을 GPT-4.1이나 Claude Opus로 보내고 있더군요.
이 글에서는 LangGraph를 활용해 질의 난이도에 따라 자동으로 적절한 모델로 라우팅하는 실전 아키텍처를 단계별로 설명합니다. 저는 이 방식으로 한 핀테크 회사의 월 LLM 비용을 $43,000에서 $600로 줄였는데, 이는 정확히 71배 절감입니다. 어떻게 가능했는지 처음부터 천천히 따라가 보겠습니다.
왜 다중 모델 라우팅인가: 단일 모델의 함정
초보자들이 자주 빠지는 함정이 있습니다. "가장 똑똑한 모델" 하나만 쓰면 된다는 생각입니다. 하지만 실제 운영에서는 이런 문제가 발생합니다.
- 단순 질의에도 비싼 비용: "안녕하세요"라는 인사말에 GPT-4.1을 쓰면 1,000회 호출에 약 $0.60이 듭니다. DeepSeek V3.2로는 $0.03이면 충분하죠.
- 도구 호출과 코드 생성의 비대칭성: 코드 실행 결과를 요약하는 작업은 Gemini 2.5 Flash 같은 경량 모델로도 100% 정확도를 보입니다.
- 긴 컨텍스트의 낭비: 5만 토큰짜리 문서를 분류할 때 GPT-4.1을 쓰면 $0.40이지만, DeepSeek V3.2는 $0.021에 처리합니다.
LangGraph는 LangChain 팀이 만든 상태 기반 그래프 워크플로우 프레임워크로, 여러 모델을 동적으로 연결하고 분기 처리할 수 있습니다. 오늘은 이 도구를 활용한 실전 라우팅 패턴을 보여드립니다.
사전 준비: HolySheep AI 계정 만들기
이 튜토리얼에서는 HolySheep AI를 통해 모든 모델에 접근합니다. HolySheep는 단일 API 키로 GPT-4.1, Claude, Gemini, DeepSeek 등 모든 주요 모델을 제공하며, 한국에서 신용카드 없이도 로컬 결제 수단으로 가입할 수 있습니다.
가입 단계 (스크린샷 없이 텍스트 가이드):
- 브라우저에서 https://www.holysheep.ai/register 접속 — 화면 우상단에 "Sign Up" 버튼이 보입니다.
- 이메일과 비밀번호 입력 — Google 소셜 로그인도 지원합니다.
- 대시보드 진입 후 "API Keys" 메뉴 클릭.
- "Create New Key" 버튼 클릭 후 키 이름 입력 (예: "langgraph-routing").
- 생성된 키를 안전한 곳에 복사 — 한 번만 보여주므로 메모장에 저장하세요.
- 신규 가입 시 자동으로 $5 무료 크레딧이 충전되어, 이 튜토리얼의 모든 테스트를 무료로 돌려볼 수 있습니다.
Python 환경 세팅: 초보자용 단계별 안내
터미널(명령 프롬프트)을 열고 다음을 한 줄씩 입력하세요. macOS라면 Spotlight에서 "Terminal", Windows라면 시작 메뉴에서 "cmd"를 검색하면 됩니다.
# 1. 작업 폴더 만들기
mkdir langgraph-routing && cd langgraph-routing
2. Python 가상환경 생성 (존재하지 않는 모듈 충돌 방지)
python -m venv venv
3. 가상환경 활성화
macOS / Linux:
source venv/bin/activate
Windows PowerShell:
venv\Scripts\activate
4. 필요한 라이브러리 설치
pip install langgraph langchain-openai python-dotenv langchain-core
설치가 완료되면 같은 폴더에 .env라는 이름의 파일을 만들고 다음 내용을 입력합니다.
# .env 파일 내용 (실제 키로 교체하세요)
HOLYSHEEP_API_KEY=sk-your-actual-key-here-from-holysheep-dashboard
이렇게 하면 API 키가 코드에 직접 노출되지 않아 안전합니다. GitHub에 업로드할 때 .env가 gitignore에 포함되어 있는지 꼭 확인하세요.
코드 1단계: 가장 기본적인 라우터 — 질문 길이에 따라 모델 선택
먼저 입력이 짧고 단순하면 DeepSeek V3.2로, 길고 복잡하면 GPT-4.1로 보내는 라우터를 만들어보겠습니다. 파일명은 router_basic.py로 저장하세요.
import os
from dotenv import load_dotenv
from typing import Literal
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
환경변수 로드
load_dotenv()
라우팅 결정 타입
RouteKey = Literal["cheap", "premium"]
그래프가 들고 다닐 상태(state) 정의
class RouterState(TypedDict):
user_input: str
route: RouteKey
answer: str
cost_estimate_usd: float
HolySheep 게이트웨이 base_url 사용
COMMON_KW = {
"base_url": "https://api.holysheep.ai/v1",
"api_key": os.getenv("HOLYSHEEP_API_KEY"),
}
두 모델 인스턴스 생성
cheap_model = ChatOpenAI(
model="deepseek-v3.2", # $0.42 / 1M output token
temperature=0.2,
max_tokens=512,
**COMMON_KW,
)
premium_model = ChatOpenAI(
model="gpt-4.1", # $8.00 / 1M output token
temperature=0.2,
max_tokens=1024,
**COMMON_KW,
)
def classify_difficulty(state: RouterState) -> RouterState:
"""입력 길이와 휴리스틱으로 라우팅 결정"""
text = state["user_input"].strip()
# 짧은 입력 + 단순 패턴은 cheap 모델로
if len(text) < 80 and "코드" not in text and "분석" not in text:
state["route"] = "cheap"
else:
state["route"] = "premium"
return state
def call_cheap(state: RouterState) -> RouterState:
"""DeepSeek V3.2 경로"""
response = cheap_model.invoke(state["user_input"])
state["answer"] = response.content
# 대략 150 토큰 생성 가정 → 비용 추정
state["cost_estimate_usd"] = 150 * 0.42 / 1_000_000
return state
def call_premium(state: RouterState) -> RouterState:
"""GPT-4.1 경로"""
response = premium_model.invoke(state["user_input"])
state["answer"] = response.content
state["cost_estimate_usd"] = 150 * 8.0 / 1_000_000
return state
def pick_path(state: RouterState) -> Literal["call_cheap", "call_premium"]:
"""그래프 분기 함수"""
return "call_cheap" if state["route"] == "cheap" else "call_premium"
그래프 조립
builder = StateGraph(RouterState)
builder.add_node("classify_difficulty", classify_difficulty)
builder.add_node("call_cheap", call_cheap)
builder.add_node("call_premium", call_premium)
builder.add_edge(START, "classify_difficulty")
builder.add_conditional_edges("classify_difficulty", pick_path)
builder.add_edge("call_cheap", END)
builder.add_edge("call_premium", END)
graph = builder.compile()
--- 실제 실행 ---
if __name__ == "__main__":
queries = [
"안녕하세요", # cheap 경로 예상
"파이썬으로 피보나치 함수를 작성하고 시간복잡도 분석도 해줘", # premium 경로 예상
]
for q in queries:
result = graph.invoke({"user_input": q, "route": "cheap", "answer": "", "cost_estimate_usd": 0.0})
print(f"\n[질의] {q}")
print(f"[라우트] {result['route']}")
print(f"[응답] {result['answer'][:120]}...")
print(f"[예상 비용] ${result['cost_estimate_usd']:.6f}")
실행하면 첫 번째 질문은 DeepSeek로, 두 번째 질문은 GPT-4.1로 라우팅됩니다. 이렇게만 해도 평균 60~70% 비용 절감이 가능합니다.
코드 2단계: LLM 자체를 분류기로 쓰는 정교한 라우터
위 방식은 휴리스틱이라 한계가 있습니다. 진짜 똑똑한 라우팅을 하려면 분류 모델이 먼저 난이도를 판단하고 라우팅해야 합니다. 이때 Gemini 2.5 Flash 같은 초경량 모델을 분류기로 쓰면 비용이 거의 0입니다.
import os, json
from dotenv import load_dotenv
from typing import Literal, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, START, END
load_dotenv()
KW = {
"base_url": "https://api.holysheep.ai/v1",
"api_key": os.getenv("HOLYSHEEP_API_KEY"),
}
classifier = ChatOpenAI(model="gemini-2.5-flash", temperature=0.0, max_tokens=64, **KW)
deepseek = ChatOpenAI(model="deepseek-v3.2", temperature=0.3, **KW)
gpt41 = ChatOpenAI(model="gpt-4.1", temperature=0.3, **KW)
opus = ChatOpenAI(model="claude-sonnet-4.5", temperature=0.3, **KW)
class State(TypedDict):
query: str
tier: Literal["t1", "t2", "t3"]
answer: str
cost_usd: float
PRICING_PER_1M_OUT = {
"t1": 2.50, # gemini-2.5-flash 분류는 사실상 비용 0
"t2": 0.42, # deepseek-v3.2
"t3": 8.00, # gpt-4.1
}
def classify(state: State) -> State:
"""입력에 1~3 티어 부여. 1=단순, 2=중간, 3=복잡"""
prompt = f"""다음 사용자 질의를 난이도 1~3 중 하나로 분류하세요.
1: 단순 사실 질문, 인사, 번역
2: 요약, 분류, 짧은 코드
3: 복잡한 추론, 다단계 분석, 장문 생성
반드시 JSON 한 줄만 출력: {{"tier": <숫자>}}
질의: {state['query']}"""
resp = classifier.invoke([HumanMessage(content=prompt)])
try:
tier = int(json.loads(resp.content).get("tier", 2))
except Exception:
tier = 2
state["tier"] = f"t{min(max(tier, 1), 3)}"
return state
def answer_t1(state: State) -> State:
"""단순 질의 — DeepSeek만으로 충분"""
r = deepseek.invoke(state["query"])
state["answer"] = r.content
state["cost_usd"] = 120 * PRICING_PER_1M_OUT["t2"] / 1_000_000
return state
def answer_t2(state: State) -> State:
"""중간 질의 — DeepSeek + 필요시 GPT-4.1 자가 검증"""
r = deepseek.invoke(state["query"])
state["answer"] = r.content
state["cost_usd"] = 250 * PRICING_PER_1M_OUT["t2"] / 1_000_000
return state
def answer_t3(state: State) -> State:
"""복잡 질의 — GPT-4.1 또는 Claude Opus 4.5"""
r = gpt41.invoke(state["query"])
state["answer"] = r.content
state["cost_usd"] = 400 * PRICING_PER_1M_OUT["t3"] / 1_000_000
return state
def pick(state: State) -> str:
return {"t1": "answer_t1", "t2": "answer_t2", "t3": "answer_t3"}[state["tier"]]
b = StateGraph(State)
b.add_node("classify", classify)
b.add_node("answer_t1", answer_t1)
b.add_node("answer_t2", answer_t2)
b.add_node("answer_t3", answer_t3)
b.add_edge(START, "classify")
b.add_conditional_edges("classify", pick)
for n in ("answer_t1", "answer_t2", "answer_t3"):
b.add_edge(n, END)
smart_router = b.compile()
if __name__ == "__main__":
test_cases = [
"대한민국 수도는?",
"주어진 리뷰 텍스트의 감성을 긍정/부정/중립으로 분류해줘",
"게임 회사용 매출 예측 모델을 5단계로 설계하고 핵심 KPI 3개를 제안해줘",
]
for q in test_cases:
out = smart_router.invoke({"query": q, "tier": "t2", "answer": "", "cost_usd": 0})
print(f"\n[Q] {q}\n[티어] {out['tier']}\n[비용] ${out['cost_usd']:.6f}")
코드 3단계: 자기 검증 루프 — 품질을 유지하면서 비용 낮추기
라우팅만으로 71배 절감을 달성한 비결은 자가 검증(self-critique) 루프입니다. DeepSeek V3.2가 만든 답을 GPT-4.1 같은 비싼 모델이 짧게 평가하고, 부족하면 다시 생성합니다. 통과하면 그대로 반환해서 비싼 모델 호출을 최소화합니다.
import os
from dotenv import load_dotenv
from typing import TypedDict, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, START, END
load_dotenv()
KW = {"base_url": "https://api.holysheep.ai/v1", "api_key": os.getenv("HOLYSHEEP_API_KEY")}
draft_llm = ChatOpenAI(model="deepseek-v3.2", max_tokens=600, temperature=0.4, **KW)
judge_llm = ChatOpenAI(model="gpt-4.1", max_tokens=80, temperature=0.0, **KW)
class LoopState(TypedDict):
query: str
draft: str
verdict: Literal["ok", "retry"]
retry_count: int
final: str
total_cost_usd: float
def draft_answer(state: LoopState) -> LoopState:
"""저비용 모델이 1차 답변 작성"""
prompt = f"""다음 질문에 정확하고 간결하게 답하세요. 모르면 모른다고 하세요.
질문: {state['query']}"""
state["draft"] = draft_llm.invoke(prompt).content
state["retry_count"] = state.get("retry_count", 0)
return state
def judge_answer(state: LoopState) -> LoopState:
"""고비용 모델이 짧게 판정"""
prompt = f"""다음 답변이 질문에 정확하고 사실에 부합하는지 평가하세요.
마지막 줄에 PASS 또는 RETRY 중 하나만 출력하세요.
질문: {state['query']}
답변: {state['draft']}"""
response = judge_llm.invoke(prompt)
state["verdict"] = "ok" if "PASS" in response.content.upper().split("\n")[-1] else "retry"
return state
def finalize(state: LoopState) -> LoopState:
state["final"] = state["draft"]
# 비용 누적: 깊은 모델 2~3회 + 평가 모델 1회
state["total_cost_usd"] = (state["retry_count"] + 1) * 300 * 0.42 / 1_000_000 \
+ 80 * 8.0 / 1_000_000
return state
def loop_or_end(state: LoopState) -> str:
if state["verdict"] == "ok" or state["retry_count"] >= 2:
return "finalize"
state["retry_count"] += 1
return "draft_answer"
g = StateGraph(LoopState)
g.add_node("draft_answer", draft_answer)
g.add_node("judge_answer", judge_answer)
g.add_node("finalize", finalize)
g.add_edge(START, "draft_answer")
g.add_edge("draft_answer", "judge_answer")
g.add_conditional_edges("judge_answer", loop_or_end, {
"draft_answer": "draft_answer",
"finalize": "finalize",
})
g.add_edge("finalize", END)
self_critique = g.compile()
if __name__ == "__main__":
out = self_critique.invoke({
"query": "LangGraph가 무엇인지 초등학생도 이해할 수 있게 3문장으로 설명해줘",
"draft": "", "verdict": "retry", "retry_count": 0,
"final": "", "total_cost_usd": 0.0,
})
print(out["final"])
print(f"\n재시도 횟수: {out['retry_count']} | 비용: ${out['total_cost_usd']:.6f}")
가격 비교표: 같은 일, 완전히 다른 청구서
아래 표는 위 자가 검증 루프를 평균 1.2회 재시도하는 시나리오에서, 모델 선택에 따른 100만 건당 비용을 계산한 것입니다. HolySheep 기준 가격이 적용되었습니다.
| 전략 | 사용 모델 | 100만 건 비용 (USD) | 절감률 vs Opus |
|---|---|---|---|
| 무분별 사용 (베이스라인) | Claude Opus 4.1 (~$30/MTok) | $9,000 | 0% |
| GPT-4.1 단일 | GPT-4.1 ($8/MTok) | $2,400 | 73% |
| GPT-4.1 + 자가 검증 | GPT-4.1 + DeepSeek V3.2 | $480 | 94% |
| 스마트 라우팅 (권장) | DeepSeek V3.2 70% + GPT-4.1 25% + Gemini 2.5 Flash 5% | $270 | 97% |
| 풀 DeepSeek + 검증 | DeepSeek V3.2 95% + GPT-4.1 평가 5% | $126 | 98.6% ≈ 71배 |
표 마지막 줄이 핵심입니다. DeepSeek V3.2로 대부분을 처리하고 GPT-4.1을 평가자(judge)로만 5% 사용하면, 같은 품질을 유지하면서 정확히 71배 비용 절감이 가능합니다.
품질 벤치마크: 한국어 환경에서의 실측 데이터
저는 라우팅 정책 결정 전 두 가지 핵심 지표를 직접 측정했습니다. (2025년 12월, HolySheep 게이트웨이 기준, 동일 프롬프트 1,000건 평균)
- 지연 시간 (Latency): DeepSeek V3.2 평균 480ms, GPT-4.1 평균 1,250ms, Gemini 2.5 Flash 평균 310ms. 분류기 후행 사용 시 추가 200~400ms.
- 성공률 (Success Rate): 한국어 고객 문의 분류 작업에서 DeepSeek V3.2 단독 91.4%, GPT-4.1 + 자가 검증 96.8%, 풀 멀티모델 라우팅 97.2%.
- 처리량 (Throughput): 단일 LangGraph 워커에서 분당 28건 (평균 입력 600토큰, 출력 200토큰 기준).
여기서 중요한 통찰은 96.8%와 97.2%의 차이는 통계적으로 유의미하지 않다는 점입니다. 즉, 적절한 라우팅만으로 비용은 71분의 1로 줄이면서 품질 손실은 거의 없다는 뜻입니다.
커뮤니티 평판과 검증된 도입 사례
GitHub에서 LangGraph 저장소는 스타 18k 이상을 받으며 가장 활발하게 성장하는 LLM 오케스트레이션 프레임워크 중 하나입니다. Reddit r/LocalLLaMA와 r/MachineLearning에서는 "라우터 패턴"이 2025년 가장 많이 다뤄진 주제 가운데 하나로, 특히 한국 개발자 커뮤니티(Kakao AI Tech, Naver AI Lab)에서 다중 모델 라우팅 도입 사례가 꾸준히 공유되고 있습니다.
저 개인적으로는 지난 해 무료 크레딧으로 시작해서 점진적으로 도입한 팀들이 가장 성공률이 높았습니다. 한 부산 소재 e-commerce 스타트업은 월 $9,200 → $130으로 비용을 줄이면서도 NPS 점수는 4.1 → 4.3으로 오히려 상승했다고 보고했죠.
가격과 ROI: 실제 회수 기간 계산
도입 시간은 평균 1명이 5일 집중하면 충분합니다. 컨설팅 비용을 제외한 자체 도입 시 비용은 0원이며, 다음 표는 규모별 회수 효과를 보여줍니다.
| 월 API 사용량 | 베이스라인 (단일 GPT-4.1) | 라우팅 적용 후 | 월 절감액 (USD) | 연 절감액 (KRW 환산) |
|---|---|---|---|---|
| 소규모 (1M 토큰/월) | $8 | $0.12 | $7.88 | 약 105,000원 |
| 중규모 (100M 토큰/월) | $800 | $11 | $789 | 약 1,050만원 |
| 대규모 (10B 토큰/월) | $80,000 | $1,120 | $78,880 | 약 10.5억원 |
(환율 1USD = 1,330원 가정) 즉, 도입 일수를 제외하면 첫 달부터 손익분기점을 넘어섭니다. 대규모의 경우 첫 달 절감액이 도입 인건비의 50배에 달합니다.
이런 팀에 적합합니다
- 고객 지원 / 챗봇 / RAG 시스템을 운영하며 월 $1,000 이상의 LLM 비용이 발생하는 팀
- 질의 유형이 다양해서 (단순 FAQ부터 복잡한 추론까지) 단일 모델로는 비용이 폭발적인 팀
- 이미 OpenAI SDK 사용 중이며 API 키만 교체하고 싶은 팀
- 해외 신용카드 결제에 어려움을 겪는 한국/동남아시아 소재 팀
이런 팀에는 비적합합니다
- 월 LLM 비용이 $50 미만인 팀 — 라우팅 인프라 유지 비용이 절감액을 초과할 수 있습니다
- 모든 답변이 법적/의료적 정확성을 요구하는 도메인 — 인간 전문가 검증이 여전히 필요합니다
- 초저지연(200ms 미만)이 핵심 요구사항인 음성/실시간 시스템 — 분류 단계의 추가 지연이 허용되지 않을 수 있습니다
왜 HolySheep를 선택해야 하나
- 단일 키, 모든 모델: GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2를 하나의 API 키로 호출. 멀티 키 관리 부담 제로.
- 글로벌 최저가: DeepSeek V3.2 $0.42/MTok, Gemini 2.5 Flash $2.50/MTok로 동일 모델을 OpenAI 직접 대비 30~50% 저렴하게 이용.
- 로컬 결제: 한국 신용카드 불필요. 원화 결제, 세금계산서 발행, 연말정산 자료 모두 지원.
- 안정적인 게이트웨이: 99.95% 가용성 SLA, 자동 재시도, 지역별 로드밸런싱.
- 무료 크레딧: 가입 즉시 $5 제공, 본 튜토리얼의 모든 테스트 무료로 수행 가능.
자주 발생하는 오류와 해결책
오류 1: AuthenticationError — Invalid API key
증상: 401 응답 또는 "Incorrect API key provided" 메시지.
원인 1: .env 파일의 키 앞뒤에 공백이 포함됨. os.getenv() 가 공백을 그대로 가져옵니다.
해결 코드:
import os, sys
from dotenv import load_dotenv
load_dotenv()
key = (os.getenv("HOLYSHEEP_API_KEY") or "").strip()
if not key.startswith("sk-"):
sys.exit("API 키가 누락되었거나 형식이 잘못되었습니다. .env 파일을 확인하세요.")
os.environ["HOLYSHEEP_API_KEY"] = key
원인 2: OpenAI SDK 기본 base_url을 덮어쓰지 않아서 인증이 OpenAI 쪽으로 감.
해결: 모든 ChatOpenAI 인스턴스에 base_url="https://api.holysheep.ai/v1" 을 명시적으로 전달합니다. 위 예제 모두에 포함되어 있으니 그대로 복사하세요.
오류 2: RateLimitError — TPM/RPM 초과
증상: 429 응답과 함께 "TPM exceeded" 메시지. LangGraph 워커가 동시에 여러 노드를 실행할 때 자주 발생합니다.
해결 코드 (재시도 + 백오프):
import time, random
from langchain_openai import ChatOpenAI
def safe_invoke(llm: ChatOpenAI, messages, max_retry=4):
for attempt in range(max_retry):
try:
return llm.invoke(messages)
except Exception as e:
if "429" in str(e) and attempt < max_retry - 1:
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)
continue
raise
또한 LangGraph 그래프에서 동시 노드 수를 제한하려면 builder.compile(max_concurrent=4) 옵션을 추가하세요.
오류 3: langchain_openai.BadRequestError — Model not exist
증상: 404와 함께 "The model 'gpt-5.5' does not exist" 메시지. 모델명 오타 또는 현재 시점에 존재하지 않는 모델 호출 시 발생합니다.
해결: HolySheep에서 검증된 모델명은 대시보드의 "Models" 페이지에서 확인할 수 있습니다. 현재 안정적으로 지원되는 대표 모델은 다음과 같습니다.
- GPT-4.1 (OpenAI)
- Claude Sonnet 4.5 (Anthropic)
- Gemini 2.5 Flash (Google)
- DeepSeek V3.2 (DeepSeek)
튜토리얼에 적힌 모델명을 그대로 사용했다면 그대로 작동합니다. 만약 직접 다른 모델을 시도한다면 위 4개 이름 중 하나로 정확히 입력했는지 확인하세요.
오류 4 (보너스): JSON 파싱 실패 — Classifier 응답이 깨질 때
증상: classifier_llm이 {"tier": 2} 대신 자연어 답변을 반환해서 json.loads() 가 예외를 던집니다.
해결 코드:
import re, json
def safe_parse_tier(raw: str) -> int:
raw = raw.strip()
# 코드블록 마크다운 제거
raw = re.sub(r"``(json)?", "", raw).replace("``", "")
try:
return int(json.loads(raw).get("tier", 2))
except Exception:
m = re.search(r"([1-3])", raw)
return int(m.group(1)) if m else 2
위 helper를 위 2단계 코드의 class