🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

LangGraph Checkpointer와 Memory Store 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 12. · 11 Views

LangGraph Checkpointer와 Memory Store 완벽 가이드

LangGraph의 Checkpointer와 Memory Store는 AI 에이전트의 상태와 대화 기록을 영구적으로 저장하고 관리하는 핵심 기능입니다. InMemorySaver부터 PostgresSaver까지 단계별로 학습하고, Semantic Search를 통한 지능형 메모리 관리까지 마스터해봅시다.


목차

  1. InMemorySaver 실험용
  2. SqliteSaver 로컬 개발
  3. PostgresSaver 프로덕션
  4. Memory Store 개념
  5. namespace로 정보 그룹화
  6. Semantic Search 지원

1. InMemorySaver 실험용

어느 날 김개발 씨가 LangGraph로 첫 AI 챗봇을 만들고 있었습니다. "대화가 잘 되네!

근데... 프로그램을 재시작하면 이전 대화를 기억 못 하네?" 선배 박시니어 씨가 미소를 지으며 말했습니다.

"Checkpointer를 써봤어요?"

Checkpointer는 LangGraph 에이전트의 상태를 저장하고 복원하는 시스템입니다. 마치 게임의 세이브 포인트처럼, 대화의 특정 시점을 저장했다가 나중에 불러올 수 있습니다.

InMemorySaver는 가장 간단한 형태로, 메모리에만 저장되어 테스트와 실험에 최적화되어 있습니다.

다음 코드를 살펴봅시다.

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph

# 메모리 기반 체크포인터 생성
checkpointer = InMemorySaver()

# 그래프에 체크포인터 연결
graph = StateGraph(state_schema=MyState)
# ... 노드와 엣지 정의 ...
app = graph.compile(checkpointer=checkpointer)

# thread_id로 대화 세션 구분
config = {"configurable": {"thread_id": "user_123"}}
response = app.invoke({"messages": [user_input]}, config)

김개발 씨는 입사 2개월 차 주니어 개발자입니다. 최근 LangGraph를 배우기 시작했고, 첫 번째 프로젝트로 간단한 고객 상담 챗봇을 만들고 있었습니다.

대화는 잘 진행되었지만, 한 가지 큰 문제가 있었습니다. "어?

방금까지 고객 정보를 물어봤는데, 프로그램 재시작하니까 다 잊어버렸네요." 김개발 씨가 당황한 표정으로 중얼거렸습니다. 옆자리의 박시니어 씨가 화면을 들여다보며 말했습니다.

"아, Checkpointer를 아직 안 배웠구나." 그렇다면 Checkpointer란 정확히 무엇일까요? 쉽게 비유하자면, Checkpointer는 마치 RPG 게임의 세이브 포인트와 같습니다.

게임을 하다가 특정 지점에서 저장하면, 나중에 게임을 끄고 다시 켜도 그 지점부터 이어서 할 수 있습니다. LangGraph의 Checkpointer도 동일합니다.

대화의 매 순간을 저장해두었다가, 나중에 정확히 그 시점부터 대화를 이어갈 수 있습니다. Checkpointer가 없던 시절에는 어땠을까요?

개발자들은 대화 상태를 직접 관리해야 했습니다. 변수에 저장하고, 파일에 쓰고, 데이터베이스에 넣고...

코드가 복잡해지고 실수하기도 쉬웠습니다. 더 큰 문제는 상태가 여러 곳에 흩어져 있어서, 특정 시점으로 되돌리기가 거의 불가능했다는 점입니다.

바로 이런 문제를 해결하기 위해 Checkpointer 시스템이 등장했습니다. Checkpointer를 사용하면 복잡한 상태 관리 코드를 작성할 필요가 없어집니다.

LangGraph가 자동으로 매 스텝마다 상태를 저장해줍니다. 또한 같은 사용자와의 이전 대화를 정확히 이어갈 수 있습니다.

무엇보다 개발자는 비즈니스 로직에만 집중할 수 있다는 큰 이점이 있습니다. 그 중에서도 InMemorySaver는 가장 기본적인 형태입니다.

이름에서 알 수 있듯이, InMemorySaver는 모든 데이터를 메모리에만 저장합니다. 파일도 없고, 데이터베이스도 없습니다.

그냥 프로그램이 실행되는 동안만 RAM에 저장됩니다. 프로그램을 종료하면?

당연히 모든 데이터가 사라집니다. "그럼 쓸모가 없는 거 아니에요?" 김개발 씨가 물었습니다.

박시니어 씨가 고개를 저었습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 InMemorySaver()로 체크포인터 객체를 생성합니다. 이것만으로 메모리 기반 저장소가 준비됩니다.

다음으로 graph.compile(checkpointer=checkpointer)에서 그래프를 컴파일할 때 체크포인터를 연결합니다. 이제 그래프는 매 스텝마다 자동으로 상태를 저장합니다.

마지막으로 thread_id를 config에 지정하면, 같은 thread_id를 사용하는 모든 호출이 같은 대화 세션으로 처리됩니다. 실제 현업에서는 어떻게 활용할까요?

InMemorySaver는 주로 개발 초기 단계에서 사용됩니다. 새로운 그래프 로직을 테스트할 때, 데이터베이스 설정 없이 빠르게 실험할 수 있습니다.

또한 유닛 테스트를 작성할 때도 유용합니다. 테스트마다 깨끗한 상태에서 시작할 수 있고, 외부 의존성이 없어서 테스트가 빠르게 실행됩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 InMemorySaver를 프로덕션 환경에 그대로 배포하는 것입니다.

서버가 재시작되면 모든 사용자의 대화 기록이 사라집니다. 고객이 "방금까지 이야기한 내용이 어디 갔죠?"라고 물어보면 답이 없습니다.

따라서 실제 서비스에는 반드시 영구 저장소를 사용해야 합니다. "아, 그래서 개발하고 테스트할 때만 쓰는 거군요!" 김개발 씨가 이해했다는 표정을 지었습니다.

박시니어 씨가 고개를 끄덕이며 덧붙였습니다. "맞아요.

실제 서비스에는 SqliteSaver나 PostgresSaver를 써야 합니다." InMemorySaver를 제대로 이해하면 LangGraph의 상태 관리 시스템을 빠르게 익힐 수 있습니다. 복잡한 설정 없이 Checkpointer의 작동 원리를 직접 체험해볼 수 있기 때문입니다.

여러분도 새로운 그래프를 만들 때는 InMemorySaver로 시작해 보세요.

실전 팁

💡 - 테스트와 개발용: InMemorySaver는 반드시 개발 환경에서만 사용하세요

  • 빠른 반복: 설정이 필요 없어 빠르게 실험하고 수정할 수 있습니다
  • 깨끗한 시작: 프로그램을 재시작하면 모든 상태가 초기화되어 테스트에 유리합니다

2. SqliteSaver 로컬 개발

김개발 씨가 InMemorySaver로 테스트를 마쳤습니다. "이제 좀 더 실전에 가깝게 만들고 싶은데요.

재시작해도 데이터가 남아있으면 좋겠어요." 박시니어 씨가 노트북을 열며 말했습니다. "그럼 SqliteSaver를 써봅시다."

SqliteSaver는 파일 기반 데이터베이스인 SQLite를 사용하는 Checkpointer입니다. 마치 워드 문서를 저장하듯이, 모든 대화 기록이 하나의 파일에 저장됩니다.

로컬 개발 환경에서 영구 저장이 필요하지만, 복잡한 데이터베이스 설정은 피하고 싶을 때 완벽한 선택입니다.

다음 코드를 살펴봅시다.

from langgraph.checkpoint.sqlite import SqliteSaver

# SQLite 파일 경로 지정
db_path = "./checkpoints.db"

# SqliteSaver 생성 (동기 방식)
checkpointer = SqliteSaver.from_conn_string(db_path)

# 비동기 방식도 지원
# from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
# checkpointer = AsyncSqliteSaver.from_conn_string(db_path)

# 그래프에 연결
app = graph.compile(checkpointer=checkpointer)

# 사용 방법은 InMemorySaver와 동일
config = {"configurable": {"thread_id": "user_123"}}
response = app.invoke({"messages": [user_input]}, config)

김개발 씨는 InMemorySaver로 기본 개념을 익혔습니다. 이제 좀 더 현실적인 문제에 부딪혔습니다.

챗봇을 테스트하다가 버그를 발견해서 코드를 수정하고 재시작하면, 테스트하던 대화가 모두 사라져버립니다. "처음부터 다시 대화를 입력하는 게 너무 번거로워요." 김개발 씨가 한숨을 쉬었습니다.

같은 시나리오를 테스트하려면 매번 같은 질문을 반복 입력해야 했습니다. 박시니어 씨가 웃으며 말했습니다.

"그럴 줄 알았어요. SqliteSaver를 써보세요." 그렇다면 SqliteSaver란 무엇일까요?

쉽게 비유하자면, SqliteSaver는 마치 자동 저장 기능이 있는 메모장과 같습니다. 워드나 노션에서 작업하면 자동으로 파일에 저장되죠.

SqliteSaver도 마찬가지입니다. 대화가 진행될 때마다 자동으로 SQLite 파일에 저장됩니다.

프로그램을 껐다 켜도, 컴퓨터를 재부팅해도 파일은 그대로 남아있습니다. SQLite는 무엇일까요?

SQLite는 별도의 서버가 필요 없는 경량 데이터베이스입니다. 하나의 파일이 곧 데이터베이스입니다.

MySQL이나 PostgreSQL처럼 설치하고 설정할 필요가 없습니다. Python에 기본으로 내장되어 있어서 별도 설치도 필요 없습니다.

스마트폰 앱들도 대부분 SQLite를 사용합니다. InMemorySaver에서 SqliteSaver로 바꾸는 것은 정말 간단합니다.

위의 코드를 보면, SqliteSaver.from_conn_string()에 파일 경로만 지정하면 됩니다. 파일이 없으면 자동으로 생성됩니다.

이미 있으면 그 파일을 사용합니다. 나머지 코드는 InMemorySaver와 완전히 동일합니다.

이것이 LangGraph의 큰 장점입니다. Checkpointer의 종류를 바꿔도 나머지 코드를 수정할 필요가 없습니다.

코드를 자세히 살펴보겠습니다. 먼저 db_path = "./checkpoints.db"로 파일 경로를 지정합니다.

현재 디렉토리에 checkpoints.db라는 파일이 생성됩니다. 다음으로 SqliteSaver.from_conn_string(db_path)로 체크포인터를 생성합니다.

내부적으로 SQLite 데이터베이스를 초기화하고 필요한 테이블을 만듭니다. 그리고 graph.compile(checkpointer=checkpointer)로 그래프에 연결하면 모든 준비가 완료됩니다.

실제 현업에서는 어떻게 활용할까요? 많은 스타트업과 개인 개발자들이 로컬 개발 환경에서 SqliteSaver를 사용합니다.

예를 들어 챗봇 프로토타입을 만들 때, 복잡한 PostgreSQL 설정 없이도 실제 데이터베이스처럼 작동합니다. 또한 데모나 프레젠테이션을 준비할 때도 유용합니다.

SQLite 파일 하나만 백업하면 모든 대화 기록을 보존할 수 있습니다. 심지어 소규모 프로덕션 환경에서도 사용됩니다.

사용자가 많지 않고 트래픽이 낮은 서비스라면, SqliteSaver로 충분히 운영할 수 있습니다. 개인 블로그의 챗봇이나 소규모 팀 내부 도구 같은 경우입니다.

SQLite는 하루에 수십만 건의 읽기와 수천 건의 쓰기를 처리할 수 있습니다. 생각보다 강력합니다.

하지만 주의할 점도 있습니다. SQLite는 동시 쓰기에 약합니다.

여러 프로세스나 스레드가 동시에 쓰기를 시도하면 락이 걸립니다. 따라서 대규모 서비스나 여러 서버를 운영하는 환경에는 적합하지 않습니다.

또한 파일 기반이기 때문에 백업과 복제가 단순합니다. 하지만 반대로 말하면, 파일이 손상되면 모든 데이터를 잃을 수 있다는 의미이기도 합니다.

정기적인 백업이 필수입니다. 김개발 씨가 코드를 작성하고 실행해봤습니다.

"오! 프로그램을 재시작했는데도 이전 대화가 그대로 남아있네요!" 박시니어 씨가 미소를 지었습니다.

"맞아요. 이제 디버깅할 때 매번 처음부터 다시 입력할 필요가 없죠." 김개발 씨는 개발 속도가 훨씬 빨라진 것을 느꼈습니다.

또 다른 장점도 있습니다. SQLite 파일은 일반 파일이기 때문에 쉽게 복사하고 공유할 수 있습니다.

버그를 재현하기 어려울 때, 문제가 발생한 시점의 SQLite 파일을 동료에게 전달하면 됩니다. 동료는 그 파일을 사용해서 정확히 같은 상태에서 디버깅을 시작할 수 있습니다.

SqliteSaver를 제대로 이해하면 로컬 개발 환경을 훨씬 생산적으로 만들 수 있습니다. 복잡한 설정 없이도 영구 저장소를 사용할 수 있고, 파일 하나로 모든 것을 관리할 수 있습니다.

여러분도 다음 프로젝트에서는 SqliteSaver를 사용해 보세요.

실전 팁

💡 - 로컬 개발에 최적: 복잡한 DB 설정 없이 영구 저장 가능

  • 파일 백업: 정기적으로 .db 파일을 백업하여 데이터 손실 방지
  • 비동기 지원: AsyncSqliteSaver를 사용하면 비동기 애플리케이션에서도 활용 가능

3. PostgresSaver 프로덕션

김개발 씨의 챗봇이 드디어 베타 테스트 단계에 도달했습니다. "사용자가 100명 정도 될 것 같은데, SqliteSaver로 괜찮을까요?" 박시니어 씨가 고개를 저었습니다.

"이제는 진짜 데이터베이스가 필요한 시점이에요. PostgresSaver로 넘어갑시다."

PostgresSaver는 프로덕션급 데이터베이스인 PostgreSQL을 사용하는 Checkpointer입니다. 마치 은행의 금고처럼, 수천 명의 사용자가 동시에 접속해도 안전하게 데이터를 저장하고 불러올 수 있습니다.

실제 서비스 운영에 필요한 성능, 안정성, 확장성을 모두 제공합니다.

다음 코드를 살펴봅시다.

from langgraph.checkpoint.postgres import PostgresSaver

# PostgreSQL 연결 문자열
conn_string = "postgresql://user:password@localhost:5432/langraph_db"

# PostgresSaver 생성 (동기 방식)
checkpointer = PostgresSaver.from_conn_string(conn_string)

# 비동기 방식 (FastAPI, asyncio 환경)
# from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
# checkpointer = AsyncPostgresSaver.from_conn_string(conn_string)

# 테이블 자동 생성
checkpointer.setup()

# 그래프에 연결
app = graph.compile(checkpointer=checkpointer)

김개발 씨의 챗봇은 처음에는 작은 실험이었습니다. 하지만 점점 기능이 추가되고, 베타 테스터들의 반응도 좋았습니다.

이제 본격적으로 서비스를 오픈할 시점이 다가왔습니다. "100명 정도 동시 접속을 예상하고 있어요." 김개발 씨가 말했습니다.

박시니어 씨가 진지한 표정으로 물었습니다. "지금 SqliteSaver 쓰고 있죠?

프로덕션에는 PostgreSQL로 바꿔야 해요." 왜 PostgreSQL이 필요할까요? SQLite는 훌륭한 데이터베이스지만, 한계가 분명합니다.

한 번에 한 명만 데이터를 쓸 수 있습니다. 10명이 동시에 메시지를 보내면?

9명은 기다려야 합니다. 실제 서비스에서는 치명적인 문제입니다.

사용자들은 느린 응답을 참지 못합니다. PostgreSQL은 엔터프라이즈급 데이터베이스입니다.

쉽게 비유하자면, SQLite가 개인 금고라면 PostgreSQL은 은행 금고입니다. 수천 명이 동시에 입출금을 해도 문제없이 작동합니다.

트랜잭션 관리, 동시성 제어, 복제, 백업 등 대규모 서비스에 필요한 모든 기능을 제공합니다. 전 세계 수많은 기업들이 PostgreSQL을 사용합니다.

LangGraph에서 PostgreSQL로 전환하는 것은 놀라울 만큼 간단합니다. 위의 코드를 보면, 연결 문자열만 바꾸면 됩니다.

postgresql://user:password@host:port/database 형식입니다. 나머지 코드는 SqliteSaver와 거의 동일합니다.

이것이 바로 추상화의 힘입니다. Checkpointer 인터페이스를 통해 구현 세부사항을 숨기고, 개발자는 비즈니스 로직에만 집중할 수 있습니다.

코드를 단계별로 살펴보겠습니다. 먼저 연결 문자열을 준비합니다.

여기에는 사용자 이름, 비밀번호, 호스트 주소, 포트 번호, 데이터베이스 이름이 포함됩니다. 실제 프로덕션에서는 환경 변수로 관리해야 합니다.

다음으로 PostgresSaver.from_conn_string()로 체크포인터를 생성합니다. 그리고 중요한 단계가 있습니다.

checkpointer.setup()을 호출하면 필요한 테이블들이 자동으로 생성됩니다. 실제 현업에서는 어떻게 배포할까요?

대부분의 클라우드 서비스는 관리형 PostgreSQL을 제공합니다. AWS RDS, Google Cloud SQL, Azure Database 등입니다.

이런 서비스를 사용하면 데이터베이스 운영 부담이 크게 줄어듭니다. 자동 백업, 모니터링, 장애 복구 등이 모두 자동으로 처리됩니다.

김개발 씨가 테스트 환경을 구축했습니다. 먼저 로컬에 PostgreSQL을 Docker로 설치했습니다.

그리고 연결 문자열을 환경 변수에 저장했습니다. 코드를 실행하니까 checkpointer.setup()이 자동으로 테이블을 만들었습니다.

"오, 생각보다 쉽네요!" 박시니어 씨가 고개를 끄덕였습니다. 하지만 프로덕션 배포는 좀 더 신중해야 합니다.

첫째, 보안입니다. 데이터베이스 접근은 반드시 암호화된 연결을 사용해야 합니다.

연결 문자열에 sslmode=require를 추가합니다. 비밀번호는 절대 코드에 하드코딩하면 안 됩니다.

환경 변수나 비밀 관리 서비스를 사용해야 합니다. 둘째, 연결 풀링입니다.

데이터베이스 연결은 비용이 큰 작업입니다. 매번 새로 연결하면 성능이 떨어집니다.

PostgresSaver는 내부적으로 연결 풀을 관리하지만, 적절한 풀 크기 설정이 중요합니다. 동시 사용자 수를 고려해서 설정해야 합니다.

셋째, 모니터링입니다. 프로덕션 환경에서는 반드시 데이터베이스 성능을 모니터링해야 합니다.

쿼리 속도, 연결 수, 디스크 사용량 등을 추적합니다. 문제가 발생하기 전에 미리 대응할 수 있습니다.

비동기 환경에서는 어떻게 사용할까요? FastAPI나 asyncio를 사용하는 애플리케이션이라면 AsyncPostgresSaver를 사용해야 합니다.

동기 버전을 비동기 환경에서 사용하면 전체 애플리케이션이 블로킹됩니다. 한 명의 사용자가 데이터베이스 응답을 기다리는 동안, 다른 사용자들도 모두 대기하게 됩니다.

AsyncPostgresSaver를 사용하면 이런 문제를 피할 수 있습니다. 김개발 씨가 베타 서비스를 오픈했습니다.

처음 며칠간은 떨렸지만, PostgreSQL은 안정적으로 작동했습니다. 100명의 동시 사용자도 문제없이 처리했습니다.

데이터도 안전하게 저장되었습니다. "이제 진짜 프로덕션 서비스를 운영하는 거네요." 김개발 씨가 뿌듯해했습니다.

PostgresSaver를 제대로 이해하면 안정적인 프로덕션 서비스를 구축할 수 있습니다. 대규모 트래픽도 감당할 수 있고, 데이터도 안전하게 보호됩니다.

여러분도 실제 서비스를 런칭할 때는 반드시 PostgresSaver를 사용하세요.

실전 팁

💡 - 환경 변수 사용: DB 비밀번호는 절대 코드에 하드코딩하지 말고 환경 변수로 관리

  • SSL 연결: 프로덕션에서는 반드시 sslmode=require로 암호화된 연결 사용
  • 비동기 버전: FastAPI나 asyncio 환경에서는 AsyncPostgresSaver를 사용

4. Memory Store 개념

챗봇이 안정적으로 운영되고 있었습니다. 그런데 한 사용자가 불만을 제기했습니다.

"저번 주에 제 취향을 말씀드렸는데, 왜 기억을 못 하시나요?" 김개발 씨가 당황했습니다. Checkpointer로 모든 대화를 저장하고 있었는데 말이죠.

박시니어 씨가 설명했습니다. "Checkpointer와 Memory Store는 다른 개념이에요."

Memory Store는 대화 세션을 넘어서 장기적으로 기억해야 할 정보를 저장하는 시스템입니다. 마치 고객 프로필 데이터베이스처럼, 사용자의 선호도, 과거 기록, 중요한 사실들을 영구적으로 저장합니다.

Checkpointer가 단기 기억이라면, Memory Store는 장기 기억입니다.

다음 코드를 살펴봅시다.

from langgraph.store.postgres import PostgresStore

# Memory Store 생성
store = PostgresStore(conn_string="postgresql://user:pass@localhost/db")
store.setup()

# 사용자별 메모리 저장
namespace = ("user_memories", "user_123")
store.put(namespace, "preferences", {
    "favorite_color": "blue",
    "dietary": "vegetarian",
    "language": "Korean"
})

# 메모리 검색
memory = store.get(namespace, "preferences")
print(memory.value)  # {"favorite_color": "blue", ...}

# 그래프에서 Memory Store 사용
app = graph.compile(checkpointer=checkpointer, store=store)

김개발 씨는 혼란스러웠습니다. Checkpointer로 모든 대화를 저장하고 있었습니다.

thread_id를 사용해서 같은 사용자의 대화를 이어갈 수도 있었습니다. 그런데 왜 사용자의 취향을 기억하지 못한다는 걸까요?

박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "Checkpointer는 대화의 흐름을 저장하는 거예요.

마치 녹음기처럼 말이죠. 하지만 사용자가 새로운 대화를 시작하면?

새로운 thread_id가 생기고, 이전 대화와 연결되지 않습니다." 김개발 씨가 고개를 끄덕였습니다. "아, 그래서 매번 다시 물어봐야 하는 거군요." 바로 이 문제를 해결하는 것이 Memory Store입니다.

쉽게 비유하자면, Checkpointer는 대화 녹음 파일이고, Memory Store는 고객 관리 시스템입니다. 커피숍을 생각해봅시다.

바리스타는 손님과 나눈 대화를 기억합니다(Checkpointer). 하지만 그 손님이 다음 날 다시 와도 알아보기 어렵습니다.

반면 회원 시스템에 "아메리카노 2샷, 얼음 많이"라고 저장되어 있다면(Memory Store)? 언제든 그 정보를 활용할 수 있습니다.

Memory Store는 어떤 정보를 저장할까요? 사용자 선호도가 대표적입니다.

좋아하는 색상, 음식 제한사항, 선호하는 언어 등입니다. 또한 사용자와 관련된 사실들도 저장합니다.

직업, 관심사, 과거 구매 이력 등입니다. 심지어 대화 패턴이나 스타일도 저장할 수 있습니다.

"이 사용자는 간결한 답변을 선호한다" 같은 메타 정보입니다. Checkpointer와 Memory Store는 함께 작동합니다.

위의 코드를 보면, graph.compile()에 checkpointer와 store를 모두 전달합니다. 그래프는 두 가지를 동시에 사용합니다.

Checkpointer로 현재 대화의 맥락을 유지하고, Memory Store에서 장기 기억을 가져옵니다. 이 두 가지가 결합되면 정말 똑똑한 챗봇이 만들어집니다.

코드를 자세히 살펴보겠습니다. 먼저 PostgresStore()로 Memory Store를 생성합니다.

Checkpointer와 마찬가지로 PostgreSQL을 사용합니다. 하지만 저장하는 데이터의 형태가 다릅니다.

다음으로 store.put()으로 데이터를 저장합니다. namespace는 데이터를 조직화하는 방법입니다.

뒤에서 자세히 설명하겠습니다. "preferences"는 키이고, 딕셔너리가 값입니다.

실제 현업에서는 어떻게 사용할까요? 전자상거래 챗봇을 예로 들어봅시다.

고객이 "파란색 신발 보여줘"라고 했습니다. 챗봇은 이 정보를 Memory Store에 저장합니다.

다음 주에 고객이 다시 방문해서 "저번에 본 신발 아직 있나요?"라고 물었습니다. Checkpointer만 있다면?

챗봇은 "어떤 신발을 말씀하시는 건가요?"라고 물을 수밖에 없습니다. 하지만 Memory Store가 있다면?

"네, 파란색 신발 말씀이시죠?"라고 즉시 대답할 수 있습니다. 고객 지원 챗봇에서도 강력합니다.

"제 구독은 언제 끝나나요?" 같은 질문에 답하려면, 사용자의 구독 정보를 알아야 합니다. 이런 정보는 대화 기록에 없습니다.

외부 시스템에서 가져와서 Memory Store에 저장해두면, 매번 API를 호출할 필요가 없습니다. 빠르고 효율적입니다.

김개발 씨가 Memory Store를 추가했습니다. 사용자가 처음 대화를 시작할 때, 기본 정보를 물어봅니다.

이름, 선호 언어, 관심 분야 등입니다. 이 정보를 Memory Store에 저장합니다.

다음 번에 같은 사용자가 새로운 대화를 시작하면? 챗봇은 "안녕하세요, 김철수님!

오늘도 프로그래밍에 대해 이야기하실 건가요?"라고 인사합니다. 사용자가 감탄했습니다.

"오, 날 기억하네!" 하지만 주의할 점이 있습니다. Memory Store에 너무 많은 데이터를 저장하면 안 됩니다.

모든 대화 기록을 Memory Store에 복사하는 것은 의미가 없습니다. 그것은 Checkpointer의 역할입니다.

Memory Store에는 정말 중요하고 재사용 가능한 정보만 저장해야 합니다. 또한 개인정보 보호도 중요합니다.

민감한 정보를 저장할 때는 암호화와 접근 제어가 필수입니다. Memory Store를 제대로 이해하면 사용자 경험을 크게 향상시킬 수 있습니다.

대화가 끊어져도 관계는 지속됩니다. 챗봇이 단순한 도구가 아니라 기억하는 비서가 됩니다.

여러분도 사용자 정보를 장기적으로 관리해야 한다면 Memory Store를 활용해 보세요.

실전 팁

💡 - 선택적 저장: 모든 정보가 아닌 중요하고 재사용 가능한 정보만 Memory Store에 저장

  • 개인정보 보호: 민감한 정보는 암호화하고 적절한 접근 제어 설정
  • Checkpointer와 병용: 단기 기억(Checkpointer)과 장기 기억(Memory Store)을 함께 사용

5. namespace로 정보 그룹화

김개발 씨가 Memory Store를 사용하기 시작했습니다. 사용자 선호도도 저장하고, 과거 주문 내역도 저장했습니다.

그런데 데이터가 점점 많아지면서 혼란스러워졌습니다. "이 데이터가 어디에 속하는 건지 헷갈려요." 박시니어 씨가 웃으며 말했습니다.

"namespace를 제대로 활용해야 해요."

namespace는 Memory Store의 데이터를 계층적으로 조직화하는 방법입니다. 마치 파일 시스템의 폴더 구조처럼, 데이터를 논리적인 그룹으로 나눕니다.

사용자별, 카테고리별, 목적별로 데이터를 분류하여 관리와 검색을 쉽게 만듭니다.

다음 코드를 살펴봅시다.

from langgraph.store.postgres import PostgresStore

store = PostgresStore(conn_string=conn_string)

# 계층적 namespace 사용
user_id = "user_123"

# 사용자 프로필 정보
profile_ns = ("users", user_id, "profile")
store.put(profile_ns, "basic", {"name": "김철수", "age": 28})

# 사용자 선호도
preferences_ns = ("users", user_id, "preferences")
store.put(preferences_ns, "food", {"diet": "vegetarian"})

# 사용자 구매 이력
orders_ns = ("users", user_id, "orders")
store.put(orders_ns, "order_001", {"item": "책", "date": "2025-01-15"})

# 특정 namespace의 모든 항목 검색
items = store.search(("users", user_id))

김개발 씨의 Memory Store는 점점 복잡해졌습니다. 처음에는 사용자 선호도만 저장했습니다.

그런데 기능이 추가되면서 주문 이력, 검색 기록, 알림 설정 등 다양한 데이터를 저장하게 되었습니다. "user_123"이라는 사용자의 데이터만 해도 수십 개가 되었습니다.

어떤 키가 무엇을 의미하는지 파악하기 어려웠습니다. "이거 완전 스파게티 코드 같아요." 김개발 씨가 한숨을 쉬었습니다.

박시니어 씨가 코드를 살펴보며 말했습니다. "namespace를 제대로 설계하지 않아서 그래요." namespace란 무엇일까요?

쉽게 비유하자면, namespace는 도서관의 분류 체계와 같습니다. 도서관에 수만 권의 책이 있다고 상상해봅시다.

모든 책을 한 곳에 쌓아두면? 찾기 불가능합니다.

그래서 대분류(인문, 과학, 예술), 중분류(철학, 심리학), 소분류로 나눕니다. namespace도 마찬가지입니다.

데이터를 논리적인 계층으로 구조화합니다. LangGraph의 namespace는 튜플 형태입니다.

위의 코드를 보면 ("users", user_id, "profile") 같은 형태입니다. 이것은 세 단계의 계층 구조입니다.

첫 번째는 최상위 카테고리입니다. "users"는 사용자 관련 데이터라는 의미입니다.

두 번째는 특정 사용자의 ID입니다. 세 번째는 데이터의 유형입니다.

"profile"은 프로필 정보를 의미합니다. 왜 이렇게 복잡하게 나눌까요?

첫째, 검색이 쉬워집니다. 특정 사용자의 모든 데이터를 가져오고 싶다면?

store.search(("users", user_id))를 호출하면 됩니다. 해당 사용자의 프로필, 선호도, 주문 이력이 모두 반환됩니다.

namespace가 없다면? 모든 데이터를 뒤져서 일일이 확인해야 합니다.

둘째, 충돌을 방지합니다. 두 명의 사용자가 우연히 같은 키를 사용한다고 해봅시다.

둘 다 "order_001"이라는 주문을 가지고 있습니다. namespace가 없다면 두 데이터가 충돌합니다.

하지만 ("users", "user_123", "orders")("users", "user_456", "orders")는 완전히 다른 공간입니다. 충돌이 불가능합니다.

코드를 자세히 살펴보겠습니다. profile_ns = ("users", user_id, "profile")로 namespace를 정의합니다.

이것은 단순한 튜플입니다. 특별한 클래스나 복잡한 구조가 아닙니다.

그리고 store.put(profile_ns, "basic", data)로 데이터를 저장합니다. "basic"은 프로필 내의 세부 키입니다.

이름과 나이 같은 기본 정보를 의미합니다. 실제 현업에서는 어떻게 설계할까요?

namespace 설계는 애플리케이션의 도메인에 따라 다릅니다. 전자상거래라면 ("users", user_id, "orders"), ("users", user_id, "cart"), ("products", product_id, "reviews") 같은 구조가 자연스럽습니다.

고객 지원 챗봇이라면 ("tickets", ticket_id, "history"), ("customers", customer_id, "preferences") 같은 구조가 적합합니다. 중요한 원칙이 있습니다.

일관성을 유지해야 합니다. 어떤 곳에서는 ("user", user_id)를 쓰고, 다른 곳에서는 ("users", user_id)를 쓰면 안 됩니다.

팀 전체가 같은 네이밍 컨벤션을 따라야 합니다. 문서화도 중요합니다.

namespace 구조를 문서로 정리해두면, 새로운 팀원이 빠르게 이해할 수 있습니다. 김개발 씨가 namespace를 재설계했습니다.

모든 사용자 데이터를 ("users", user_id, category) 구조로 통일했습니다. 시스템 설정은 ("system", "config", key) 구조로 분리했습니다.

임시 데이터는 ("temp", session_id, type) 구조로 관리했습니다. "훨씬 깔끔해졌어요!" 코드를 읽기도 쉬워지고, 버그도 줄어들었습니다.

검색 기능도 강력합니다. store.search(("users", user_id))를 호출하면 해당 namespace 아래의 모든 데이터가 반환됩니다.

필요에 따라 더 구체적으로 검색할 수도 있습니다. store.search(("users", user_id, "orders"))는 주문 이력만 가져옵니다.

이런 계층적 검색 덕분에 대용량 데이터에서도 빠르게 원하는 정보를 찾을 수 있습니다. 또 다른 활용법도 있습니다.

namespace를 사용해서 멀티테넌시를 구현할 수 있습니다. 여러 조직이 같은 시스템을 사용한다면?

최상위 namespace를 조직 ID로 사용하면 됩니다. ("org_abc", "users", user_id), ("org_xyz", "users", user_id) 이런 식입니다.

각 조직의 데이터가 완전히 격리됩니다. namespace를 제대로 이해하면 Memory Store를 훨씬 효율적으로 사용할 수 있습니다.

데이터가 논리적으로 조직화되고, 검색이 빨라지고, 유지보수가 쉬워집니다. 여러분도 처음부터 namespace 구조를 신중하게 설계해 보세요.

실전 팁

💡 - 일관된 네이밍: 팀 전체가 같은 namespace 컨벤션을 따르도록 문서화

  • 계층 구조 설계: 최상위는 도메인, 중간은 엔티티 ID, 하위는 데이터 유형으로 구성
  • 검색 활용: search() 메서드로 namespace 기반 계층적 검색 가능

6. Semantic Search 지원

챗봇이 점점 똑똑해졌습니다. 그런데 한 사용자가 까다로운 질문을 했습니다.

"저번에 파스타 레시피 얘기했잖아요. 뭐였죠?" 문제는 "파스타"라는 단어를 직접 쓰지 않고 "이탈리아 면 요리"라고 했었다는 점입니다.

김개발 씨가 고민했습니다. "키워드 검색으로는 찾을 수가 없는데..." 박시니어 씨가 답했습니다.

"Semantic Search가 필요해요."

Semantic Search는 단순한 키워드 매칭이 아니라 의미를 이해하는 검색입니다. 마치 사람처럼 "파스타"와 "이탈리아 면 요리"가 같은 의미라는 것을 알고 검색합니다.

임베딩 벡터를 사용해 유사한 의미의 데이터를 찾아내어, 훨씬 자연스러운 대화를 가능하게 합니다.

다음 코드를 살펴봅시다.

from langgraph.store.postgres import PostgresStore
from langchain_openai import OpenAIEmbeddings

# Embedding 모델과 함께 Store 생성
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
store = PostgresStore(
    conn_string=conn_string,
    embeddings=embeddings
)

# 벡터 검색이 가능한 인덱스 생성
store.setup()

# 텍스트와 함께 저장 (자동으로 임베딩 생성)
namespace = ("recipes", "user_123")
store.put(namespace, "recipe_001", {
    "text": "크림 파스타 만드는 법: 생크림과 파르메산 치즈를...",
    "category": "italian"
})

# 의미 기반 검색
results = store.search(
    namespace,
    query="이탈리아 면 요리 어떻게 만들어요?",
    limit=5
)

김개발 씨는 Memory Store에 많은 데이터를 저장했습니다. 레시피, 사용자 질문, 상담 기록 등입니다.

하지만 검색에는 한계가 있었습니다. 정확한 키워드를 알아야만 찾을 수 있었습니다.

실제 사용자들은 정확한 용어를 쓰지 않습니다. "그거 있잖아, 저번에 말한 그거" 같은 모호한 표현을 씁니다.

또는 같은 의미를 다른 단어로 표현합니다. "자동차 수리"와 "차량 정비"는 같은 의미지만, 문자열 검색으로는 다른 것으로 취급됩니다.

"이런 검색을 어떻게 구현하죠?" 김개발 씨가 물었습니다. 박시니어 씨가 화이트보드에 "Semantic Search"라고 썼습니다.

Semantic Search란 무엇일까요? 쉽게 비유하자면, Semantic Search는 마치 의미를 이해하는 사서와 같습니다.

일반 도서관 사서에게 "공룡 책 있어요?"라고 물으면, 제목에 "공룡"이 들어간 책만 찾아줍니다. 하지만 똑똑한 사서는 "쥬라기 시대", "티라노사우루스", "화석 연구" 같은 책도 함께 추천합니다.

의미상 관련이 있다는 것을 알기 때문입니다. Semantic Search는 임베딩을 사용합니다.

임베딩은 텍스트를 숫자 벡터로 변환하는 기술입니다. 비슷한 의미를 가진 단어나 문장은 비슷한 벡터로 표현됩니다.

"파스타"와 "이탈리아 면 요리"의 벡터는 서로 가깝습니다. "파스타"와 "자동차"의 벡터는 멀리 떨어져 있습니다.

이 거리를 계산해서 유사한 문서를 찾는 것이 Semantic Search입니다. LangGraph의 Memory Store는 Semantic Search를 기본 지원합니다.

위의 코드를 보면, embeddings 파라미터에 임베딩 모델을 전달합니다. OpenAI의 text-embedding 모델을 사용했지만, 다른 모델도 가능합니다.

Cohere, HuggingFace, 심지어 로컬 모델도 사용할 수 있습니다. 중요한 것은 LangChain의 Embeddings 인터페이스를 따르기만 하면 된다는 점입니다.

코드를 자세히 살펴보겠습니다. 먼저 OpenAIEmbeddings()로 임베딩 모델을 생성합니다.

이 모델은 텍스트를 1536차원의 벡터로 변환합니다. 다음으로 PostgresStore()를 생성할 때 embeddings를 전달합니다.

그러면 Store는 자동으로 벡터 검색을 위한 테이블과 인덱스를 설정합니다. PostgreSQL의 pgvector 확장을 사용합니다.

데이터를 저장할 때는 어떻게 될까요? store.put()으로 데이터를 저장하면, Store는 자동으로 "text" 필드를 임베딩으로 변환합니다.

원본 텍스트와 함께 벡터도 데이터베이스에 저장됩니다. 개발자는 임베딩 생성을 신경 쓸 필요가 없습니다.

모든 것이 자동으로 처리됩니다. 검색은 더욱 간단합니다.

store.search(namespace, query="...")를 호출하면 됩니다. query는 일반 텍스트입니다.

내부적으로 이 텍스트가 벡터로 변환되고, 데이터베이스에서 유사한 벡터를 찾습니다. 결과는 유사도 순으로 정렬되어 반환됩니다.

가장 관련성 높은 데이터가 먼저 나옵니다. 실제 현업에서는 어떻게 활용할까요?

고객 지원 챗봇을 예로 들어봅시다. 수천 개의 과거 상담 기록이 Memory Store에 저장되어 있습니다.

고객이 "인터넷이 안 돼요"라고 문의했습니다. Semantic Search는 "연결 문제", "네트워크 장애", "Wi-Fi 오류" 같은 키워드로 저장된 과거 사례들을 찾아냅니다.

정확히 같은 단어가 아니어도 의미상 관련된 모든 사례를 검색합니다. 추천 시스템에도 강력합니다.

사용자가 "편안한 신발 추천해줘"라고 했습니다. 상품 설명에 "편안한"이라는 단어가 없어도 괜찮습니다.

"쿠션이 좋은", "오래 신어도 피로하지 않은", "발에 잘 맞는" 같은 표현들도 모두 검색됩니다. Semantic Search가 의미를 이해하기 때문입니다.

김개발 씨가 Semantic Search를 추가했습니다. 테스트 결과는 놀라웠습니다.

사용자가 "그 빨간 거 뭐였더라?"라고 물었을 때, 과거에 대화했던 "빨간색 원피스"를 정확히 찾아냈습니다. "이거 진짜 마법 같은데요!" 박시니어 씨가 웃으며 말했습니다.

"마법이 아니라 수학이에요. 좋은 수학." 하지만 주의할 점도 있습니다.

첫째, 비용입니다. 임베딩 API는 호출할 때마다 비용이 발생합니다.

대량의 데이터를 저장할 때는 비용을 고려해야 합니다. 가능하다면 로컬 임베딩 모델을 사용하는 것도 방법입니다.

둘째, 성능입니다. 벡터 검색은 일반 SQL 쿼리보다 느립니다.

특히 데이터가 많아질수록 검색 시간이 증가합니다. 적절한 인덱싱과 limit 설정이 중요합니다.

모든 데이터를 검색할 필요는 없습니다. 상위 5-10개만 가져와도 충분한 경우가 많습니다.

셋째, 품질입니다. 임베딩 모델의 품질이 검색 품질을 결정합니다.

영어는 대부분의 모델이 잘 작동하지만, 한국어는 다국어 모델이나 한국어 특화 모델을 사용해야 합니다. 도메인 특화 용어가 많다면, 파인튜닝된 모델을 고려해야 할 수도 있습니다.

Semantic Search를 제대로 이해하면 사용자 경험을 혁신적으로 개선할 수 있습니다. 사용자는 정확한 키워드를 기억할 필요가 없습니다.

자연스럽게 질문하면 챗봇이 알아서 관련 정보를 찾아냅니다. 여러분도 Memory Store에 Semantic Search를 추가해 보세요.

마법 같은 경험을 제공할 수 있습니다.

실전 팁

💡 - 적절한 limit: 모든 결과가 아닌 상위 5-10개만 가져와 성능 최적화

  • 다국어 모델: 한국어 데이터는 다국어 또는 한국어 특화 임베딩 모델 사용
  • 비용 관리: 대량 데이터는 로컬 임베딩 모델 사용을 고려

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#LangGraph#Checkpointer#MemoryStore#PostgresSaver#SemanticSearch#AI,LLM,Python,LangGraph

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.