Skip to content

자바 개발자의 AI 입문기 (2편) - LangChain 기초, 체이닝과 프롬프트 템플릿





TL;DR

  • LangChain: AI 호출을 체계적으로 관리하는 프레임워크
  • 프롬프트 템플릿: 재사용 가능한 질문 형식 (자바의 MessageFormat과 비슷)
  • 체이닝: 여러 AI 호출을 연결 (자바의 Stream API 파이프라인과 비슷)
  • 핵심: 복붙 대신 재사용 가능한 구조를 만드는 게 LangChain의 목적
  • 한계: LCEL 체이닝은 직관적이지만 복잡한 분기 로직에서는 LangGraph 사용 권장




이전 편 요약

1편에서는 다음을 다뤘습니다:

  • Cursor IDE 설치
  • Python 환경 세팅
  • OpenAI API 직접 호출

이번 편에서는 LangChain을 배웁니다. OpenAI API를 직접 호출하는 것보다 훨씬 체계적으로 AI를 다룰 수 있게 됩니다.





LangChain이 뭔가요?

한 줄 요약: AI 호출을 Spring처럼 체계화하는 프레임워크

1편에서 OpenAI API를 직접 호출했잖아요. 간단한 건 그걸로 충분합니다.

근데 실제 서비스에서는 이런 요구가 생겨요:

  • 프롬프트를 재사용하고 싶다
  • 여러 AI 호출을 순서대로 연결하고 싶다
  • 출력 형식을 고정하고 싶다
  • 외부 데이터(DB, 문서)를 AI에게 주고 싶다

이런 걸 체계적으로 하려면 LangChain이 필요합니다.

자바로 비유하면:

OpenAI API 직접 호출 = 순수 JDBC
LangChain 사용 = Spring Data JPA 사용




LangChain 설치

가상환경이 활성화된 상태에서:

pip install langchain langchain-openai
  • langchain: 핵심 라이브러리
  • langchain-openai: OpenAI 연동 모듈




프롬프트 템플릿 - 재사용 가능한 질문 만들기

왜 필요한가?

1편에서 만든 코드 리뷰 함수를 보세요:

# 1편의 방식 - 프롬프트가 코드에 하드코딩
messages=[
    {
        "role": "system",
        "content": """당신은 10년차 자바 시니어 개발자입니다.
        주어진 코드를 리뷰하고, 개선점을 3가지 이내로 간결하게 제안하세요."""
    },
    {
        "role": "user", 
        "content": f"다음 자바 코드를 리뷰해주세요:\n\n```java\n{code}\n```"
    }
]

문제점:

  • 프롬프트가 코드 안에 섞여 있음
  • 다른 언어 리뷰하려면 복붙해서 수정해야 함
  • 버전 관리가 어려움


LangChain 프롬프트 템플릿 방식

from langchain_core.prompts import ChatPromptTemplate

# 재사용 가능한 템플릿 정의
code_review_template = ChatPromptTemplate.from_messages([
    ("system", """당신은 {experience}년차 {language} 시니어 개발자입니다.
    주어진 코드를 리뷰하고, 개선점을 {max_points}가지 이내로 간결하게 제안하세요.
    한국어로 답변하세요."""),
    ("user", "다음 {language} 코드를 리뷰해주세요:\n\n```{language}\n{code}\n```")
])

# 템플릿에 변수 채우기
messages = code_review_template.format_messages(
    experience=10,
    language="Java",
    max_points=3,
    code="public void test() { ... }"
)

자바의 MessageFormat과 비슷하죠?

// Java - MessageFormat
String pattern = "{0}년차 {1} 개발자입니다.";
String result = MessageFormat.format(pattern, 10, "Java");


실습: 다국어 코드 리뷰어

프롬프트 템플릿의 장점을 활용해서, 여러 언어를 리뷰할 수 있는 함수를 만들어봅시다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from dotenv import load_dotenv

load_dotenv()

# LLM 모델 설정
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

# 프롬프트 템플릿
review_template = ChatPromptTemplate.from_messages([
    ("system", """당신은 {experience}년차 {language} 시니어 개발자입니다.
    주어진 코드를 리뷰하고, 개선점을 {max_points}가지 이내로 간결하게 제안하세요.
    한국어로 답변하세요."""),
    ("user", "다음 코드를 리뷰해주세요:\n\n```{language}\n{code}\n```")
])


def review_code(code: str, language: str = "Java", 
                experience: int = 10, max_points: int = 3) -> str:
    """다양한 언어의 코드를 리뷰하는 AI 함수"""
    
    # 템플릿 + 모델 연결 (체이닝의 시작)
    chain = review_template | llm
    
    # 실행
    response = chain.invoke({
        "code": code,
        "language": language,
        "experience": experience,
        "max_points": max_points
    })
    
    return response.content


# Java 코드 리뷰
java_code = """
public User findUser(Long id) {
    return userRepository.findById(id).get();
}
"""
print("=== Java 코드 리뷰 ===")
print(review_code(java_code, language="Java"))

# Python 코드 리뷰 (같은 함수로!)
python_code = """
def find_user(id):
    user = db.query(User).filter(User.id == id).first()
    return user
"""
print("\n=== Python 코드 리뷰 ===")
print(review_code(python_code, language="Python"))

하나의 템플릿으로 여러 언어를 리뷰할 수 있게 됐어요.





체이닝 - AI 호출을 파이프라인처럼 연결하기

체이닝이란?

위 코드에서 이 부분:

chain = review_template | llm

| 연산자로 템플릿과 모델을 연결했어요. 이게 체이닝입니다.

자바의 Stream API와 비슷한 느낌:

// Java Stream - 데이터 파이프라인
list.stream()
    .filter(x -> x > 0)
    .map(x -> x * 2)
    .collect(toList());

// LangChain - AI 파이프라인
# prompt | llm | output_parser


실습: 코드 리뷰 → 개선 코드 생성 체인

두 단계의 AI 호출을 연결해봅시다:

  1. 코드 리뷰 (문제점 파악)
  2. 개선된 코드 생성
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

# 1단계: 코드 리뷰 프롬프트
review_prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 자바 시니어 개발자입니다. 코드의 문제점을 3줄 이내로 짧게 지적하세요."),
    ("user", "코드:\n```java\n{code}\n```")
])

# 2단계: 개선 코드 생성 프롬프트
improve_prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 자바 시니어 개발자입니다. 리뷰 내용을 반영해서 개선된 코드만 출력하세요."),
    ("user", "원본 코드:\n```java\n{code}\n```\n\n리뷰 내용:\n{review}")
])


def review_and_improve(code: str) -> dict:
    """코드 리뷰 후 개선 코드 생성"""
    
    # 1단계 체인: 리뷰 생성
    review_chain = review_prompt | llm | StrOutputParser()
    review_result = review_chain.invoke({"code": code})
    
    # 2단계 체인: 개선 코드 생성
    improve_chain = improve_prompt | llm | StrOutputParser()
    improved_code = improve_chain.invoke({
        "code": code,
        "review": review_result
    })
    
    return {
        "original": code,
        "review": review_result,
        "improved": improved_code
    }


# 테스트
sample_code = """
public User findUser(Long id) {
    User user = userRepository.findById(id).get();
    if (user == null) {
        throw new RuntimeException("User not found");
    }
    return user;
}
"""

result = review_and_improve(sample_code)

print("=== 리뷰 결과 ===")
print(result["review"])
print("\n=== 개선된 코드 ===")
print(result["improved"])

실행 결과:

=== 리뷰 결과 ===
1. `.get()` 대신 `.orElseThrow()` 사용 필요
2. `RuntimeException` 대신 커스텀 예외 권장
3. `.get()` 이후 null 체크는 불필요

=== 개선된 코드 ===
public User findUser(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}

두 단계의 AI 호출이 자연스럽게 연결됐어요.





StrOutputParser - 출력 정리하기

체인에서 StrOutputParser()가 보이셨죠?

chain = prompt | llm | StrOutputParser()

LLM의 응답은 AIMessage 객체로 옵니다. .content를 매번 꺼내기 귀찮으니까, 파서가 자동으로 문자열만 추출해주는 거예요.

# 파서 없이
response = (prompt | llm).invoke({"code": code})
print(response.content)  # .content 필요

# 파서 있으면
response = (prompt | llm | StrOutputParser()).invoke({"code": code})
print(response)  # 바로 문자열

나중에 JSON이나 객체로 파싱하는 파서도 배울 거예요.





Spring과의 공통점 발견

LangChain을 쓰다 보면 Spring과 비슷한 패턴이 보여요.

LangChainSpring
프롬프트 템플릿MessageSource, 템플릿 엔진
체이닝 (| 연산자)HandlerInterceptor 체인
LLM 주입의존성 주입 (DI)
OutputParserResponseBodyAdvice

결국 “관심사 분리”와 “재사용성”이라는 같은 원칙을 따르고 있는 거예요.

프롬프트는 프롬프트대로, 모델은 모델대로, 파싱은 파싱대로 분리해서 조합하는 방식이 LangChain의 철학입니다.





정리 - 이번 편에서 배운 것

  1. LangChain: AI 호출을 체계화하는 프레임워크
  2. 프롬프트 템플릿: 변수를 넣어 재사용 가능한 질문 형식
  3. 체이닝: | 연산자로 템플릿 → 모델 → 파서 연결
  4. StrOutputParser: LLM 응답에서 문자열만 추출

1편에서 만든 코드 리뷰 함수가, 이제는 어떤 언어든 리뷰할 수 있고, 개선 코드까지 자동 생성하는 파이프라인이 됐습니다.





다음 편 예고

다음 편에서는 RAG (Retrieval-Augmented Generation) 기초를 다룹니다.

RAG가 뭐냐면:

  • AI가 모르는 정보(우리 회사 문서, 최신 데이터)를 검색해서 답변에 활용하는 기술
  • “문서 → 벡터 변환 → 저장 → 검색 → 답변 생성” 파이프라인

다음 편 주요 내용:

  • Embedding이란?
  • Vector DB (ChromaDB) 사용법
  • 문서를 벡터로 저장하는 방법

(3편) RAG 기초로 계속





실습 체크리스트

따라하고 나서 아래 항목들을 점검해보세요:

  • langchain langchain-openai 설치 확인 (pip list | grep langchain)
  • 프롬프트 템플릿 변수 치환 동작 확인
  • 체이닝 (prompt | llm | StrOutputParser()) 정상 실행 확인
  • Java/Python 코드 모두 동일 템플릿으로 리뷰 가능 확인
  • 2단계 체인 (리뷰 → 개선 코드 생성) 실행 확인




참고:

LangChain 공식 문서: https://python.langchain.com/docs
LCEL (LangChain Expression Language): https://python.langchain.com/docs/concepts/lcel




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site