티스토리 뷰

반응형

LangChain 사용자별 데이터 분리는 어떻게 할까? session_id, user_id, thread_id, 멀티테넌시 설계 방법

LangChain 백엔드에서 사용자별 데이터 분리를 하려면 HTTP 요청의 session_id를 LangGraph/LangChain의 thread_id에 연결하고, 여기에 더해 user_id, role, preferences 같은 사용자 컨텍스트를 agent state나 서비스 계층에서 분리 관리하는 구조가 가장 현실적입니다. LangChain 공식 문서는 short-term memory를 thread-scoped memory로 설명하고, 같은 대화를 이어가려면 checkpointer와 configurable.thread_id가 필요하다고 안내합니다. 또한 기본 AgentState는 messages를 관리하고, 필요하면 state_schema를 확장해 user_id, preferences 같은 필드를 추가할 수 있다고 설명합니다. FastAPI는 큰 애플리케이션에서 여러 파일과 APIRouter를 권장하고, 설정/의존성 주입을 통해 이런 분리를 더 깔끔하게 만들 수 있다고 안내합니다. 검색에 잘 걸리는 글은 질문형 제목, 첫 문단 정답 요약, 비교표, FAQ, 출처 구조가 유리하다는 업로드 메모 방향도 이번 글에 반영했습니다. (LangChain 문서)

솔직히 인증까지 붙이고 나면 바로 이 문제가 보입니다.

  • 같은 사용자가 여러 대화를 열면 어떻게 나누지?
  • session_id랑 thread_id는 같은 건가?
  • short-term memory는 사용자 전체를 기억하는 건가, 대화 하나만 기억하는 건가?
  • 정책 문서는 전 직원이 읽어도 되지만, 요청 상태는 본인 것만 봐야 하지 않나?
  • 관리자만 전체 요청을 볼 수 있게 하려면 어디서 막아야 하지?

이 글은 바로 그 지점을 정리합니다.


한 줄 요약

LangChain 멀티테넌시의 핵심은 대화 상태는 thread_id로 분리하고, 사용자 정체성은 user_id로 분리하며, 권한은 endpoint와 tool 양쪽에서 이중으로 제한하는 것입니다. LangChain은 short-term memory를 thread 단위 상태로 설명하고, state_schema 확장으로 사용자 관련 필드를 추가할 수 있다고 안내합니다. 또 persistence 문서는 thread_id가 상태 저장과 재개를 위한 핵심 키라고 설명합니다. (LangChain 문서)


이 글에서 다루는 내용

  • session_id, thread_id, user_id 차이
  • LangChain short-term memory를 사용자별로 안전하게 나누는 법
  • 멀티테넌시에서 꼭 필요한 서비스 계층 설계
  • tool 내부 권한 체크는 왜 필요한가
  • 사용자별 문맥, 권한, 데이터 범위를 어떻게 분리할까
  • 실무에서 자주 하는 실수와 FAQ

session_id, thread_id, user_id는 무엇이 다를까?

이 세 개를 섞어서 쓰면 프로젝트가 금방 꼬입니다.

user_id

이 사람 자체를 식별하는 값입니다.
예: alice, u-102, employee-77

session_id

클라이언트가 보내는 대화 단위 식별자입니다.
웹/앱 입장에서는 “이 대화창” 정도로 생각하면 됩니다.

thread_id

LangChain/LangGraph가 short-term memory를 저장·복원할 때 쓰는 키입니다.
보통은 session_id를 그대로 thread_id로 써도 괜찮습니다. LangGraph persistence 문서는 thread_id를 checkpoint의 기본 식별 키처럼 설명하고, short-term memory 문서는 configurable.thread_id를 넘겨 같은 대화를 이어가라고 안내합니다. (LangChain 문서)

즉 보통은 이렇게 가면 됩니다.

  • 프론트엔드: session_id
  • LangChain 내부: thread_id = session_id
  • 사용자 식별: user_id

이렇게 역할을 나누면 머리가 훨씬 덜 복잡해집니다.


왜 LangChain에서는 thread_id 분리가 그렇게 중요할까?

LangChain 공식 문서는 short-term memory를 single thread or conversation 안의 이전 상호작용을 기억하는 시스템이라고 설명합니다. 또한 short-term memory는 agent state의 일부로 관리되고, step이 끝날 때마다 업데이트되며, 다음 step 시작 시 다시 읽힌다고 안내합니다. (LangChain 문서)

이 말은 곧:

  • thread_id가 같으면 같은 대화로 이어지고
  • thread_id가 다르면 완전히 다른 대화가 됩니다

라는 뜻입니다.

예를 들어 같은 사용자 alice라도,

  • alice-chat-1
  • alice-chat-2

이렇게 두 개의 thread_id를 쓰면
서로 다른 대화로 memory가 분리됩니다.
이게 정말 중요해요.
같은 사용자라도 여러 업무 대화를 동시에 열 수 있으니까요.


short-term memory는 사용자 전체를 기억하는 게 아니다

이건 꼭 짚고 가야 합니다.

LangChain memory overview는 short-term memory를 thread-scoped conversation state로 설명하고, 여러 대화나 세션을 넘어 저장되는 정보는 long-term memory로 분리해 설명합니다. (LangChain 문서)

즉:

short-term memory

  • 이번 대화창 안에서만 기억
  • 직전 질문, 답변, tool 결과 문맥
  • thread_id 기준 분리

long-term memory

  • 여러 대화를 넘어 유지
  • 사용자 선호, 프로필, 반복되는 설정
  • 별도 저장소 개념 필요

그래서 “사용자별 데이터 분리”를 얘기할 때
처음부터 모든 걸 short-term memory에 넣으면 안 됩니다.

그건 대화 상태지, 사용자 프로필 저장소가 아니거든요.


가장 현실적인 멀티테넌시 구조

반응형

저는 보통 이렇게 나누는 걸 추천합니다.

user_id        -> 사용자 식별
session_id     -> 프론트엔드 대화창 식별
thread_id      -> LangChain memory 식별
user_context   -> role, scopes, preferences 같은 현재 사용자 정보

그리고 실제 호출은 이렇게 갑니다.

  1. FastAPI에서 JWT로 current_user 확인
  2. session_id를 받아 thread_id로 사용
  3. current_user 정보는 agent input의 user_context로 넣음
  4. tool에서는 user_context를 보고 데이터 접근 범위 제한

이 구조가 좋은 이유는 단순합니다.

  • 대화 상태와 사용자 신원 분리
  • 같은 사용자 여러 대화 지원
  • 권한 체크 위치가 명확
  • 나중에 long-term memory 붙이기 쉬움

LangChain은 state_schema로 AgentState를 확장해 user_id, preferences 같은 필드를 추가할 수 있다고 설명합니다. (LangChain 문서)


코드로 보면 이렇게 시작하면 된다

1) 사용자 컨텍스트를 명시적으로 넘기기

result = agent.invoke(
    {
        "messages": [{"role": "user", "content": question}],
        "user_context": {
            "user_id": current_user.username,
            "role": current_user.role,
            "scopes": current_user.scopes,
        },
    },
    {
        "configurable": {
            "thread_id": session_id,
        }
    },
)

이렇게 해두면 agent가 대화 문맥뿐 아니라
현재 사용자의 권한 정보도 같이 볼 수 있습니다.
LangChain short-term memory 문서의 custom state 예시도 user_id, preferences 같은 필드를 state에 추가하는 방식을 보여줍니다. (LangChain 문서)


state_schema 확장은 언제 필요할까?

LangChain 공식 문서는 기본적으로 agents가 AgentState의 messages 키를 써서 short-term memory를 관리한다고 설명하고, 더 많은 필드가 필요하면 state_schema를 사용해 확장하라고 안내합니다. 예시로 user_id, preferences를 추가합니다. (LangChain 문서)

예를 들어 이런 경우면 확장을 고려할 만합니다.

  • user_id를 매 step마다 명시적으로 state에 두고 싶다
  • 사용자 선호를 메모리 안에서 계속 참조하고 싶다
  • tool이 state에서 직접 사용자 정보를 읽어야 한다
  • before_model / after_model middleware에서 사용자 컨텍스트를 다루고 싶다

예시:

from langchain.agents import AgentState, create_agent
from langgraph.checkpoint.memory import InMemorySaver


class CustomAgentState(AgentState):
    user_id: str
    role: str
    preferences: dict


agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[...],
    state_schema=CustomAgentState,
    checkpointer=InMemorySaver(),
)

이건 초반엔 과할 수 있지만,
멀티테넌시가 진짜 중요해지는 순간 꽤 유용합니다. (LangChain 문서)


Tool 권한 체크는 왜 endpoint 권한과 별개로 필요할까?

이건 정말 중요합니다.

FastAPI JWT나 OAuth2 scopes는
“이 endpoint를 호출할 수 있나”를 막는 데 좋습니다.
하지만 실제 데이터 접근 범위는 tool 안에서 한 번 더 보는 게 맞습니다. FastAPI security 문서는 인증과 권한을 dependency/scopes로 다루는 방식을 안내하지만, 그건 어디까지나 API 접근 계층입니다. (FastAPI)

예를 들어:

  • /assistant/ask는 직원이면 누구나 접근 가능
  • 하지만 요청 상태 tool은 본인 요청만 조회 가능
  • manager는 팀 요청까지 가능
  • admin은 전체 가능

이건 endpoint보다 데이터 레벨 규칙이죠.
그러니까 tool 내부 체크가 필요합니다.

def can_read_request(request_owner: str, requester_username: str, requester_role: str) -> bool:
    if requester_role in {"manager", "admin"}:
        return True
    return request_owner == requester_username

이런 규칙은 tool 안에 두는 게 더 자연스럽습니다.


사용자별 문서 검색도 실제론 분리 대상이다

많은 분이 문서 검색은 그냥 다 같이 써도 된다고 생각하는데,
사내 도우미에선 이것도 아닐 수 있습니다.

예를 들어:

  • 전사 공통 정책 문서
  • 팀별 운영 문서
  • 관리자 전용 지침
  • 인사팀 내부 문서

이런 건 검색 범위 자체가 사용자 role에 따라 달라져야 할 수 있죠.

LangChain retrieval과 agent 구조는 “어떤 문서를 agent가 볼 수 있게 할지”를 서비스 쪽에서 설계할 수 있게 해줍니다. 그리고 FastAPI bigger applications 문서가 권장하는 계층 분리 구조를 따르면, service 계층에서 사용자 역할에 맞는 retriever/tool만 선택적으로 쓰게 만드는 것도 자연스럽습니다. (FastAPI)

즉 문서 검색도 결국 멀티테넌시 문제입니다.

  • 같은 질문이라도
  • 누가 묻느냐에 따라
  • 검색 가능한 문서가 달라질 수 있다

이걸 처음부터 염두에 두는 편이 좋습니다.


추천하는 서비스 계층 구조

FastAPI는 큰 애플리케이션에서 파일과 라우터, 의존성을 나누라고 권장합니다. 이 흐름을 LangChain 서비스에도 그대로 가져오면 멀티테넌시가 훨씬 깔끔해집니다. (FastAPI)

routers/
  assistant.py        # HTTP 처리
services/
  assistant_service.py # user/session/thread 매핑
agents/
  internal_assistant.py # agent 조립
tools/
  request_status.py   # 실제 데이터 접근 + 권한 검사
infra/
  memory.py           # checkpointer
schemas/
  auth.py
  assistant.py

이 구조가 좋은 이유는 명확합니다.

  • router는 인증 사용자 주입
  • service는 user_id, session_id, thread_id 연결
  • agent는 LangChain 조립만
  • tool은 실제 접근 제어만

역할이 보이면, 나중에 권한 버그가 나도 찾기 쉽습니다.


비교: 잘못된 멀티테넌시 vs 괜찮은 멀티테넌시

구분나쁜 예좋은 예

대화 분리 사용자당 thread 1개만 사용 사용자 1명당 여러 session_id/thread_id 허용
사용자 컨텍스트 FastAPI에서만 확인 agent/service/tool까지 전달
권한 체크 endpoint에서만 확인 endpoint + tool 내부 이중 체크
문서 검색 전 사용자 동일 범위 role/team 기준 검색 범위 분리 가능
memory short-term memory에 모든 걸 넣음 short-term과 long-term 성격 분리

이런 비교표는 검색에도 잘 맞고, 독자도 바로 감을 잡기 좋습니다. 업로드 메모에서도 비교표가 AI 검색형 콘텐츠에 유리하다고 정리돼 있었습니다.


실무에서 자주 하는 실수

실수 1. user_id와 thread_id를 같은 개념으로 본다

같은 사용자가 여러 대화를 열 수 있기 때문에, 보통 둘은 다르게 봐야 합니다. short-term memory는 thread-scoped입니다. (LangChain 문서)

실수 2. short-term memory에 사용자 장기 프로필까지 다 넣으려 한다

그건 long-term memory 또는 별도 저장소 개념에 더 가깝습니다. (LangChain 문서)

실수 3. endpoint 인증만 붙이고 tool 내부 권한 체크를 안 한다

특히 요청 상태, 결재 상태, 내부 문서는 이게 위험합니다.

실수 4. manager/admin 권한을 프롬프트로만 제어하려 한다

권한은 가능하면 deterministic하게 막아야 합니다. LangChain guardrails와 HITL 문서가 강조하는 것도 이런 deterministic enforcement입니다. (docs.langchain.com)

실수 5. 운영에 InMemorySaver를 그대로 쓴다

LangChain 문서는 프로덕션에선 DB-backed checkpointer를 쓰라고 권장합니다. (LangChain 문서)


FAQ

Q. session_id와 thread_id는 꼭 다르게 가져가야 하나요?

꼭 그럴 필요는 없습니다. 보통 프론트의 session_id를 LangChain의 thread_id로 그대로 매핑해도 충분합니다. 중요한 건 “대화 단위 키”라는 의미를 분명히 하는 것입니다. LangGraph persistence는 thread_id가 상태 저장/복원의 핵심 키라고 설명합니다. (LangChain 문서)

Q. 같은 사용자면 대화도 하나로 합치는 게 낫지 않나요?

보통은 아닙니다. short-term memory는 thread 단위라서, 같은 사용자가 여러 업무를 동시에 할 수 있으면 thread를 분리하는 편이 더 자연스럽습니다. LangChain도 short-term memory를 single thread/conversation 단위로 설명합니다. (LangChain 문서)

Q. 사용자 선호나 팀 정보는 어디에 두는 게 좋나요?

대화 안에서만 필요한 건 state/schema 확장으로 갈 수 있고, 여러 세션을 넘어 유지해야 하는 건 long-term memory나 별도 DB가 더 적합합니다. (LangChain 문서)

Q. 관리자 권한은 FastAPI scope만으로 충분한가요?

endpoint 접근만 보면 충분할 수 있지만, 실제 데이터 접근 범위는 tool 내부에서 한 번 더 제한하는 편이 더 안전합니다.

Q. 운영에서 멀티테넌시 memory는 뭘 써야 하나요?

개발 단계에선 InMemorySaver로 시작해도 되지만, 운영에서는 DB-backed checkpointer가 권장됩니다. LangChain short-term memory 문서는 production에서 Postgres-backed saver 예시를 제공합니다. (LangChain 문서)


핵심 요약

  • LangChain 사용자별 데이터 분리의 핵심은 user_id와 thread_id를 분리해서 보는 것입니다. (LangChain 문서)
  • short-term memory는 thread-scoped이고, 사용자 전체 프로필을 대신하는 개념이 아닙니다. (LangChain 문서)
  • endpoint 인증과 tool 내부 권한 체크는 역할이 다르므로 둘 다 필요합니다. (FastAPI)
  • state_schema를 확장하면 user_id, role, preferences 같은 필드를 agent state에 추가할 수 있습니다. (LangChain 문서)
  • 검색에 잘 걸리는 글처럼, 멀티테넌시 설명도 질문형 제목 + 첫 문단 정답 요약 + 비교표 + FAQ + 출처 구조가 이해와 검색에 모두 유리합니다.

출처

  • LangChain Short-term memory: short-term memory는 thread-scoped이고 checkpointer와 thread_id가 필요함. (LangChain 문서)
  • LangChain Memory overview: short-term memory와 long-term memory 구분. (LangChain 문서)
  • LangGraph Persistence: thread_id가 상태 저장/복원 키 역할. (LangChain 문서)
  • FastAPI Settings / Bigger Applications / OAuth2 JWT: 설정 주입, 파일 구조 분리, 인증 흐름 참고. (FastAPI)
  • 업로드해주신 검색형 글쓰기 메모: 질문형 제목, 첫 문단 정답 요약, 비교표, FAQ 전략 반영.

LangChain, FastAPI, 멀티테넌시, session_id, thread_id, short-term memory, AI Agent, 사내도우미, 백엔드설계, 주니어개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
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
글 보관함
반응형