티스토리 뷰
LangChain 메모리 운영은 어떻게 해야 할까? PostgresStore, Redis, namespace 설계, 삭제 정책까지 실무 관점으로 정리
octo54 2026. 6. 11. 13:26LangChain 메모리 운영은 어떻게 해야 할까? PostgresStore, Redis, namespace 설계, 삭제 정책까지 실무 관점으로 정리
LangChain 메모리 운영의 핵심은 short-term memory는 thread_id로, long-term memory는 namespace로 분리하고, 개발 단계의 InMemoryStore에서 운영 단계의 persistent store로 자연스럽게 넘어갈 수 있게 설계하는 것입니다. 공식 문서도 short-term memory는 thread-scoped state이고, long-term memory는 namespace-scoped data라고 설명합니다. 또 long-term memory는 JSON 문서를 namespace + key 구조로 저장하며, 개발에는 InMemoryStore, 운영에는 PostgresStore, MongoDBStore, RedisStore 같은 persistent store를 쓰라고 안내합니다. (docs.langchain.com (LangChain Docs))
이제부터는 진짜 운영 이야기입니다.
long-term memory를 붙이는 것까지는 생각보다 재밌어요.
“이 사용자는 짧게 답하는 걸 좋아한다”
“이 사용자는 재무팀이다”
이런 걸 저장해두면 갑자기 서비스가 똑똑해진 것처럼 보이거든요.
근데 여기서 한 단계 더 가야 합니다.
- 메모리는 어디에 저장할까
- namespace는 어떻게 나눌까
- 중복된 memory는 어떻게 정리할까
- 언제 지울까
- 운영에서 InMemoryStore를 언제 버려야 할까
이걸 안 정하면, long-term memory는 금방 지저분해집니다.
오늘 글은 바로 그 정리입니다.
한 줄 요약
운영 기준 LangChain 메모리는 이렇게 보는 게 가장 편합니다.
- 대화 문맥은 thread_id + checkpointer
- 사용자 장기 기억은 namespace + store
- 개발용은 InMemoryStore
- 운영용은 PostgresStore 같은 persistent store
- 삭제 정책은 “무엇을 얼마나 오래 남길지”를 먼저 정한 뒤 코드로 옮긴다. 공식 문서는 store가 long-term memory를 JSON 문서로 저장하고, short-term memory는 thread-scoped로 관리한다고 설명합니다. (docs.langchain.com (LangChain Docs))
이 글에서 다루는 내용
이번 글에서는 아래를 같이 봅니다.
- LangChain 메모리 운영이 왜 별도 설계가 필요한지
- InMemoryStore와 운영용 store 차이
- PostgresStore, RedisStore, MongoDBStore를 언제 볼지
- namespace를 어떻게 나누면 좋은지
- 삭제 정책과 정리 정책을 어떻게 잡을지
- FastAPI + LangChain 백엔드에서 long-term memory를 어디 레이어에 둘지
왜 long-term memory는 운영이 더 중요할까
short-term memory는 같은 대화가 끝나면 어느 정도 자연스럽게 범위가 보입니다.
공식 문서도 short-term memory를 thread-scoped state라고 설명하고, 대화 하나의 문맥 관리가 목적이라고 말합니다. 반면 long-term memory는 여러 세션과 여러 thread를 가로질러 재사용되기 때문에, 무엇을 저장할지와 어떻게 찾을지가 훨씬 중요합니다. (docs.langchain.com (LangChain Docs))
그러니까 long-term memory에서 진짜 어려운 건
“저장 기술”보다 운영 기준입니다.
예를 들면 이런 질문들이요.
- 이 정보는 정말 장기 기억으로 남길 가치가 있나?
- 사용자 선호와 일시적 요청을 어떻게 구분하지?
- 같은 의미의 memory가 세 번 저장되면 어떻게 하지?
- 사용자 퇴사나 계정 삭제 시 memory는 어디까지 지워야 하지?
이게 정리되지 않으면, memory는 금방 노이즈가 됩니다.
InMemoryStore는 왜 운영용이 아닐까
공식 문서는 InMemoryStore를 개발과 테스트에 적합하다고 설명하고, 운영에서는 persistent store를 쓰라고 권장합니다. persistence 문서도 InMemoryStore는 suitable for development and testing이라고 말하고, production용으로 PostgresStore, MongoDBStore, RedisStore를 예로 듭니다. (docs.langchain.com (LangChain Docs))
이유는 너무 뻔합니다.
- 서버 재시작하면 사라짐
- 여러 인스턴스가 공유 못 함
- 백업/복구 어렵고
- 운영 데이터 보존이 안 됨
그래서 long-term memory를 진짜 운영에 넣는 순간,
저장소를 바꾸는 건 거의 필수입니다.
운영용 store는 무엇이 있나
LangChain/LangGraph 문서는 운영용 persistent store 예시로 PostgresStore, MongoDBStore, RedisStore를 반복해서 언급합니다. long-term memory 문서와 persistence 문서 모두 이 세 가지를 production store 예시로 제시합니다. 또한 reference 문서는 AsyncPostgresStore를 pgvector optional vector search가 가능한 Postgres-backed store로 설명합니다. (docs.langchain.com (LangChain Docs))
여기서 중요한 건 백엔드 이름 자체보다도
공통 인터페이스가 BaseStore라는 점입니다. persistence 문서는 이 구현들이 모두 BaseStore를 확장한다고 설명합니다. (docs.langchain.com (LangChain Docs))
즉, 설계를 잘해두면
- 지금은 InMemoryStore
- 나중엔 PostgresStore
이런 교체가 비교적 자연스럽습니다.
가장 먼저 추천하는 운영용 선택: PostgresStore
공식 long-term memory 문서는 Python 예제로 PostgresStore.from_conn_string(...)와 store.setup() 흐름을 직접 보여줍니다. 또한 PostgresStore는 store 인터페이스를 그대로 쓰면서 persistent backend로 교체하는 가장 공식적인 예시 중 하나입니다. (docs.langchain.com (LangChain Docs))
예시는 이렇게 갑니다.
from langgraph.store.postgres import PostgresStore
DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"
with PostgresStore.from_conn_string(DB_URI) as store:
store.setup()
# put / get / search 가능
제가 처음 운영 store를 고를 때 Postgres를 많이 추천하는 이유는
문서 예시도 가장 풍부하고, store/setup 흐름도 명확하게 잡히기 때문입니다.
여기에 short-term memory용 PostgresSaver까지 같이 가면 한 시스템 안에서 정리하기도 편합니다. short-term memory 문서도 production에서는 DB-backed saver를 권장합니다. (docs.langchain.com (LangChain Docs))
namespace는 어떻게 설계해야 할까
공식 문서는 long-term memory를 namespace + key + JSON document 구조로 저장한다고 설명합니다. namespace는 폴더 비슷한 개념이고, 사용자나 조직 ID를 포함해 계층적으로 구성할 수 있다고 말합니다. cross-namespace search는 content filters로 지원된다고도 설명합니다. (docs.langchain.com (LangChain Docs))
즉 namespace 설계는 사실상
메모리를 어디까지 묶어볼지 정하는 일입니다.
처음엔 보통 이 정도면 충분합니다.
namespace = ("memories", user_id)
조금 커지면 이렇게 갈 수 있습니다.
namespace = ("users", user_id, "preferences")
namespace = ("users", user_id, "facts")
namespace = ("orgs", org_id, "shared_rules")
공식 문서 예시도 (user_id, "memories"), (user_id, application_context) 같은 tuple namespace를 보여줍니다. namespace 길이는 고정이 아니고, 원하는 계층을 표현하면 됩니다. (docs.langchain.com (LangChain Docs))
제가 추천하는 실전 규칙
- 사용자 장기 기억은 ("users", user_id, "memories")
- 사용자 설정/프로필은 ("users", user_id, "profile")
- 조직 공통 규칙은 ("orgs", org_id, "rules")
이렇게 나누면 나중에 훨씬 덜 헷갈립니다.
key는 어떻게 잡아야 할까
공식 문서는 각 memory가 namespace 아래 distinct key를 가진다고 설명합니다. 보통 예시에서는 uuid나 "a-memory" 같은 key를 씁니다. (docs.langchain.com (LangChain Docs))
실무에서는 보통 두 가지 패턴이 있습니다.
1. UUID 기반
collection 방식 memory에 적합합니다.
memory_id = str(uuid.uuid4())
store.put(namespace, memory_id, {"data": "User prefers short answers"})
2. 고정 key 기반
profile 방식에 적합합니다.
store.put(("users", user_id, "profile"), "main", {...})
즉 key 전략도
profile인지 collection인지에 따라 달라지는 편이 자연스럽습니다.
검색은 어떻게 동작하나
공식 문서는 store.search(namespace, query=..., filter=...) 패턴을 보여줍니다. Store는 semantic search와 content filtering을 모두 지원한다고 memory overview에서 설명합니다. long-term memory 예제도 filter={"my-key":"my-value"}와 query="language preferences"를 함께 쓰는 예시를 보여줍니다. (docs.langchain.com (LangChain Docs))
이게 중요한 이유는 단순합니다.
- semantic search만으로는 너무 넓을 수 있고
- filter만으로는 너무 딱딱할 수 있기 때문입니다
그래서 운영에선 보통 이렇게 생각하면 편합니다.
- filter: 범위를 먼저 좁힘
- query: 의미상 관련 memory를 찾음
예를 들어:
items = store.search(
("users", user_id, "memories"),
filter={"type": "preference"},
query="answer style",
)
이런 식으로요.
삭제 정책은 왜 따로 설계해야 할까
이 부분이 진짜 운영스러운 얘기입니다.
공식 문서는 long-term memory 저장 구조와 검색 구조를 잘 설명하지만, “무엇을 얼마나 오래 둘지”는 결국 애플리케이션 정책 영역입니다. memory overview도 long-term memory에는 one-size-fits-all solution이 없고, 어떤 memory를 언제 업데이트할지 질문을 먼저 던지라고 말합니다. (docs.langchain.com (LangChain Docs))
즉 삭제 정책은 문서가 대신 정해주지 않습니다.
직접 정해야 합니다.
저는 보통 이렇게 봅니다.
바로 지우면 안 되는 것
- 사용자 장기 선호
- 권한 관련 사실
- 반복적으로 쓰이는 업무 맥락
일정 기간 후 지워도 되는 것
- 일시적인 관심사
- 한동안만 유효한 업무 메모
- 오래 지나면 가치가 낮아지는 요약 결과
즉시 삭제 가능해야 하는 것
- 사용자가 직접 “이건 기억하지 마”라고 한 경우
- 잘못 저장된 개인정보
- 퇴사/계정 삭제된 사용자 memory
즉 운영에서 진짜 필요한 건
기술적 delete 함수보다 삭제 기준입니다.
중복과 오래된 memory는 어떻게 다룰까
공식 문서는 collection 방식이 recall에 유리하지만, memory update와 search 쪽 복잡성이 커진다고 설명합니다. profile은 깔끔하지만 업데이트가 커질수록 에러프론해질 수 있다고도 하죠. (docs.langchain.com (LangChain Docs))
그래서 운영에서는 보통 아래 세 가지를 같이 합니다.
1. 중복 저장 방지
새 memory 저장 전 유사 memory 검색
2. 오래된 memory 정리
최근 N개월 안 쓴 memory를 다시 검토
3. profile/collection 병행
- collection으로 원본 기억 쌓기
- profile로 핵심 요약본 따로 유지
이 조합이 생각보다 괜찮습니다.
profile만 쓰면 갱신이 어렵고, collection만 쓰면 점점 지저분해지거든요.
hot path 저장과 background 정리는 어떻게 나눌까
공식 문서는 memory writing을 hot path와 background로 나눕니다. hot path는 응답 중 즉시 저장하고, background는 비동기적으로 정리/저장하는 방식입니다. hot path는 즉시 반영과 투명성이 장점이고, background는 응답 latency를 덜 건드린다는 장점이 있습니다. (docs.langchain.com (LangChain Docs))
운영에서는 보통 이렇게 나누면 편합니다.
hot path에 둘 것
- “앞으로 짧게 답해줘”
- “나는 재무팀이야”
- “다크 모드 좋아해”
즉시 반영할 가치가 크고, 저장 조건이 명확한 것들입니다.
background에 둘 것
- 긴 대화에서 추출한 선호 요약
- 중복 memory 병합
- 오래된 memory 정리
- profile 재생성
즉 hot path는 빠른 기억,
background는 느린 정리라고 생각하면 쉽습니다.
FastAPI 프로젝트에서는 어느 레이어에 둘까
이전 글들 흐름과 이어서 보면, 저는 보통 이렇게 둡니다.
routers/
assistant.py
services/
assistant_service.py
memory/
store.py
policies.py
agents/
internal_assistant.py
memory/store.py
- store 생성
- namespace 헬퍼
- put/get/search 함수
memory/policies.py
- 저장할지 판단
- 삭제할지 판단
- dedupe 기준
services/assistant_service.py
- 요청 받고 user_id/thread_id 연결
- hot path 저장 호출
- background 정리 enqueue
이렇게 나누면 나중에 store backend를 바꾸거나, deletion policy를 바꿔도 agent 코드는 덜 흔들립니다.
최소 운영 예시 코드
아래 코드는 collection 방식 + user namespace + dedupe 전 검색 정도만 담은 가장 작은 실전형 예시입니다.
import uuid
from typing import Iterable
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore
embeddings = init_embeddings("openai:text-embedding-3-small")
store = InMemoryStore(
index={
"embed": embeddings,
"dims": 1536,
}
)
def user_memory_namespace(user_id: str) -> tuple[str, ...]:
return ("users", user_id, "memories")
def search_user_memories(user_id: str, query: str, limit: int = 5):
namespace = user_memory_namespace(user_id)
return store.search(namespace, query=query, limit=limit)
def save_user_memory(user_id: str, text: str, memory_type: str = "preference") -> bool:
namespace = user_memory_namespace(user_id)
# 아주 단순한 dedupe
existing = store.search(namespace, query=text, limit=3)
normalized = text.strip().lower()
for item in existing:
data = item.value.get("data", "").strip().lower()
if data == normalized:
return False
store.put(
namespace,
str(uuid.uuid4()),
{
"type": memory_type,
"data": text,
},
)
return True
def list_recent_user_memories(user_id: str) -> Iterable[dict]:
namespace = user_memory_namespace(user_id)
items = store.search(namespace)
return [item.dict() for item in items]
이 코드는 공식 문서의 store.put, store.search, namespace tuple, embedding index 패턴을 그대로 따릅니다. InMemoryStore는 개발용이고, 운영에서는 같은 인터페이스를 유지한 채 persistent store로 교체하면 됩니다. (docs.langchain.com (LangChain Docs))
비교: InMemoryStore vs 운영용 store
구분InMemoryStorePostgresStore / MongoDBStore / RedisStore
| 용도 | 개발, 테스트 | 운영 |
| 재시작 후 데이터 | 사라짐 | 유지 가능 |
| 멀티 인스턴스 공유 | 어려움 | 가능 |
| 운영 백업/복구 | 사실상 없음 | 가능 |
| 공식 문서 권장 위치 | 개발 단계 | production |
이 표는 공식 persistence/long-term memory 문서의 설명을 운영 관점으로 풀어쓴 것입니다. (docs.langchain.com (LangChain Docs))
FAQ
Q. LangChain 메모리 운영은 처음부터 PostgresStore로 가야 하나요?
아닙니다. 개발 단계에서는 InMemoryStore로 충분합니다. 다만 long-term memory를 진짜 서비스 기능으로 쓰기 시작하면 운영용 persistent store로 바꾸는 게 맞습니다. 공식 문서도 그렇게 권장합니다. (docs.langchain.com (LangChain Docs))
Q. namespace는 꼭 사용자 ID를 넣어야 하나요?
꼭 그런 건 아닙니다. 공식 문서도 namespace는 any custom namespace라고 설명합니다. 다만 사용자별 memory라면 user_id를 넣는 편이 가장 자연스럽습니다. (docs.langchain.com (LangChain Docs))
Q. 삭제 정책은 LangChain이 자동으로 해주나요?
아니요. store 구조와 검색 방식은 제공하지만, 무엇을 얼마나 오래 보관할지는 애플리케이션 정책입니다. 공식 문서도 one-size-fits-all solution은 없다고 설명합니다. (docs.langchain.com (LangChain Docs))
Q. profile 방식이 더 깔끔한데 왜 collection도 같이 쓰라고 하나요?
profile은 깔끔하지만 업데이트가 커질수록 에러프론해질 수 있습니다. collection은 recall에 유리하고 추가가 쉽지만, 검색과 정리 복잡성이 있습니다. 공식 문서도 이 장단점을 각각 설명합니다. (docs.langchain.com (LangChain Docs))
Q. semantic search 없이도 long-term memory 운영이 가능한가요?
가능은 합니다. 하지만 collection 방식 memory가 늘어나면 semantic search가 훨씬 유리해집니다. 공식 문서도 Store가 semantic search와 filtering을 지원한다고 설명합니다. (docs.langchain.com (LangChain Docs))
핵심 요약
운영 기준으로 LangChain 메모리를 정리하면 이렇게 됩니다.
- short-term memory는 thread_id
- long-term memory는 namespace + key
- 개발은 InMemoryStore
- 운영은 persistent store
- profile은 깔끔하지만 업데이트가 어렵고
- collection은 유연하지만 dedupe와 정리가 중요합니다. 공식 문서가 바로 이 장단점을 설명합니다. (docs.langchain.com (LangChain Docs))
결국 long-term memory 운영의 핵심은
“기억을 어디에 저장하느냐”보다
어떻게 나누고, 언제 저장하고, 언제 지울지를 먼저 정하는 것입니다.
마무리
저는 처음에 long-term memory를 붙일 때
저장소 선택이 제일 큰 문제인 줄 알았어요.
근데 막상 해보니까 진짜 어려운 건 그게 아니었습니다.
더 어려운 건 오히려 이런 거였어요.
- 어떤 namespace가 맞는지
- 어떤 memory를 남겨야 하는지
- 중복을 어떻게 줄일지
- 오래된 걸 언제 지울지
즉, 메모리 운영은 DB 선택보다
정책 설계에 훨씬 가깝더라고요.
그걸 받아들이고 나니까 오히려 편해졌습니다.
기술은 store가 해결해주고,
진짜 품질은 내가 정한 기준에서 나온다는 게 보였거든요.
다음 글 예고
다음 글에서는
LangChain memory와 RAG는 어떻게 다를까? 사용자 기억과 문서 검색을 한 서비스에서 같이 설계하는 방법
으로 이어가겠습니다.
이제부터는 long-term memory와 RAG를 섞어서,
“사용자에 대한 기억”과 “외부 문서 근거”를 한 서비스 안에서 어떻게 역할 분리할지 보게 될 거예요.
출처
- LangChain Memory overview: short-term vs long-term, hot path vs background, profile vs collection 설명. (docs.langchain.com)
- LangChain Long-term memory: store는 JSON documents를 namespace + key 구조로 저장하고, InMemoryStore와 PostgreSQL 예시를 제공. (docs.langchain.com)
- LangGraph Add memory / Persistence: InMemoryStore는 개발용, production에서는 PostgresStore, MongoDBStore, RedisStore 같은 persistent store 권장. namespace tuple과 store.put/search 예시 제공. (docs.langchain.com)
- Postgres store reference: AsyncPostgresStore는 optional vector search using pgvector를 지원한다고 설명. (docs.langchain.com)
LangChain, LangGraph, long-term memory, PostgresStore, RedisStore, namespace 설계, AI Agent, memory 운영, LangChain Backend, 주니어개발자
'study > langchain' 카테고리의 다른 글
- Total
- Today
- Yesterday
- kotlin
- nextJS
- 생성형AI
- node.js
- Express
- Next.js
- 주니어개발자
- llm
- 백엔드개발
- CI/CD
- 쿠버네티스
- JWT
- Python
- DevOps
- rag
- 웹개발
- flax
- JAX
- PostgreSQL
- Prisma
- SpringBoot
- seo 최적화 10개
- NestJS
- LangChain
- 개발블로그
- fastapi
- SEO최적화
- REACT
- nodejs
- 딥러닝
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
