자바로 배우는 AI (1) — LLM API 기초
에이전트가 뭔지, MCP가 뭔지, RAG가 뭔지, 하네스가 뭔지 하나도 모르는 상태에서 시작했다. 자바 개발자라서 자바(Anthropic 공식 SDK)로 직접 만들어보며 정리한 시리즈다. 전체 코드: github.com/dadaok/ai-playground
- 용어부터 — 결국 REST API 연동이다
- API 키는 Claude 계정과 다르다
- 프로젝트 세팅 (Gradle + 공식 자바 SDK)
- 1단계 코드 — API를 “날것으로”
- 실행하며 얻은 감(感)
- 정리
용어부터 — 결국 REST API 연동이다
먼저 큰 그림. 외부 결제 API 연동하는 것과 구조가 같다.
| 용어 | 한 줄 정의 | 자바 개발자 비유 |
|---|---|---|
| LLM | 텍스트를 넣으면 텍스트가 나오는 모델 | 외부 서비스 |
| API | 그 모델을 코드로 부르는 HTTP 창구 | 결제사 REST API |
| 엔드포인트 | 실제 요청 URL (api.anthropic.com) | pay.xxx.com/... |
| API 키 | “나 이 사람 맞음” 인증 문자열 | 시크릿 키 |
| SDK | HTTP·JSON 처리를 대신 해주는 라이브러리 | 결제사 자바 SDK |
| 토큰 | 글자를 잘게 쪼갠 단위. 요금·길이의 기준 | API 호출 건수(과금 단위) |
그리고 개념 용어.
| 용어 | 한 줄 |
|---|---|
| 프롬프트 | 모델에게 보내는 입력 텍스트 전부 |
| system 프롬프트 | 모델의 역할·규칙을 정하는 고정 지시문 |
| messages / role | 대화 내역 배열. user ↔ assistant 가 번갈아 쌓임 |
| tool use | 모델이 “이 함수 불러줘”라고 JSON으로 요청 → 내가 실행 |
| agent | 모델 + 도구 + 반복 루프 |
| RAG | 답하기 전에 관련 문서를 검색해 프롬프트에 붙여넣기 |
| MCP | 도구/데이터를 모델에 연결하는 표준 규격 (JDBC 같은 것) |
| harness | 모델을 감싸고 루프·컨텍스트·권한을 관리하는 실행 환경 |
핵심 통찰 하나만 미리: agent = LLM 호출을 while 루프에 넣고 함수(tool)를 호출시킨 것. 나머지는 다 이걸 돕는 부속품이다.
API 키는 Claude 계정과 다르다
혼동했던 부분. claude.ai 구독(Pro/Max) 과 API 키 는 완전히 별개다.
| claude.ai 웹/앱 | Anthropic API | |
|---|---|---|
| 무엇 | 사람이 브라우저에서 채팅 | 내 코드가 HTTP로 모델 호출 |
| 인증 | 계정 로그인 | x-api-key 헤더에 API 키 |
| 결제 | 월 구독(정액) | 쓴 토큰만큼 과금(선불 크레딧) |
| 발급처 | claude.ai | console.anthropic.com |
Pro 구독이 있어도 API 크레딧은 따로 충전해야 한다. 연습용은 $5면 충분하다.
또 하나. API 호출은 console이 아니라 api.anthropic.com으로 간다.
[처음 한 번] console.anthropic.com(브라우저) → 키 발급 → sk-ant-... 복사
[키 보관] export ANTHROPIC_API_KEY=sk-ant-...
[매 호출] 내 코드 ──HTTP POST──▶ api.anthropic.com/v1/messages (헤더에 키)
console은 키를 만들 때만 간다. 그 뒤로 코드는 console을 거치지 않는다.
프로젝트 세팅 (Gradle + 공식 자바 SDK)
build.gradle.kts:
plugins { application }
repositories { mavenCentral() }
dependencies {
implementation("com.anthropic:anthropic-java:2.34.0")
}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
application { mainClass = "playground.Step1Basic" }
API 키는 환경변수로만 읽는다. 코드·git 어디에도 평문으로 두지 않는다. IntelliJ에서는 Run/Debug Configuration → Environment variables 에 ANTHROPIC_API_KEY 를 넣으면 된다.
1단계 코드 — API를 “날것으로”
package playground;
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
public class Step1Basic {
public static void main(String[] args) {
// ANTHROPIC_API_KEY 환경변수를 자동으로 읽는다.
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model("claude-haiku-4-5") // 가장 저렴한 모델
.maxTokens(1024L)
.system("너는 자바 개발자에게 AI 개념을 설명하는 친절한 튜터야. 한국어로 답해.")
.addUserMessage("에이전트와 그냥 LLM 한 번 호출하는 것의 차이를 3문장으로 설명해줘.")
.build();
Message response = client.messages().create(params);
// content 는 여러 block 으로 나뉠 수 있다(텍스트, 생각, 도구호출 등). text 만 출력.
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> System.out.println(textBlock.text()));
// 토큰 사용량 = 요금의 단위. 매 호출마다 확인하는 습관.
System.out.println("input tokens : " + response.usage().inputTokens());
System.out.println("output tokens: " + response.usage().outputTokens());
}
}
실행하며 얻은 감(感)
- 이건 상태 없는(stateless) HTTP 호출이다. 서버는 우리를 기억하지 않는다.
- 대화는
messages배열이다.role = user / assistant가 번갈아 쌓인다. - 다음 턴에도 대화를 이어가려면 내가 매번 전체
messages를 다시 보낸다. (이 사실이 뒤에서 “컨텍스트 관리” 문제로 이어진다 — 5편 참고) content가 단일 문자열이 아니라 block 리스트인 이유는, 응답에 텍스트뿐 아니라 “생각(thinking)”, “도구 호출(tool_use)” 같은 다른 종류가 섞일 수 있어서다. (2편)
정리
LLM API는 특별한 기술이 아니라 인증 붙은 stateless REST 호출이다. system으로 역할을 정하고, messages에 대화를 쌓아 보내고, 응답에서 필요한 block을 꺼낸다. 다음 편에서는 여기에 “도구(tool)”를 붙여 에이전트를 만든다.