하위 호환성
하위 호환성
LangGraph 그래프를 배포할 때 그래프 코드의 변경이 저장된 상태의 동작 방식에 영향을 줄 수 있어요. 영속 상태(checkpoints)는 그래프 실행 사이와 실행 중간에 유지되기 때문에, 그래프 구조와 코드의 변경이 어떻게 저장된 상태와 상호작용하는지 이해하는 게 중요해요.
출처: 하위 호환성 - 공식 문서
준비 운동 삼아 하나 물어볼게요. 만약 사용자가 그래프를 실행하면서 ConvoThread(thread_id="abc123")로 대화를 진행하다가, 나중에 내가 그래프의 기능을 바꿔서 재배포한다면 어떻게 될까요? 이 제목을 다 읽고 나면, 정확히 무슨 일이 일어나는지, 그리고 상태 저장 기록용으로 저장된 트레이스를 해석하는 데 어떤 영향을 주는지 알게 될 거예요.
전제: 그래프가 상태에 접근하는 방법
abstract 상태 개념(LangGraph Persistence)에 대해 배울 때, 어떤 그래프는 상태가 "어떻게" 저장되는지에 대해 channels, tasks, versions의 관점에서 이야기했어요. 상태를 다루는 대부분의 그래프는 평범한 MessageGraph와 StateGraph, 그리고 두 내장 도우미인 add_messages 리듀서와 Migrate 스키마를 사용해요.
그러나 그래프가 상태에 접근하는 방법에는 두 가지가 있어요:
StateGraph/MessageGraph:StateGraph와MessageGraph의 상태는 채널과 리듀서의 조합입니다. 채널은 크게 두 가지 유형이 있어요:LastValue(마지막 값만 유지)와Topic(모든 값을 누적). 추가적으로StateGraph는 캐노니컬한Migrate스키마를 기본으로 하는데, 이 스키마는add_messages, 업데이트, 삭제를 포함한 유용한 동작들을 제공해요.Pregel:Pregel상태는 오직channels로만 정의됩니다.StateGraph의 유용한 추상화를 원하지 않는 경우 Fiji 스타일의 접근이라고 할 수 있어요. 확장하는 사람들은Pregel의channels파라미터와state_schema를 사용하곤 해요.
요컨대, 그래프가 상태에 접근하는 방법을 아는 것은 하위 호환성 문제에서 정말 중요해요.
hard vs soft 상태
소프트 상태 (Soft State)
소프트 상태는 그래프의 상태에 부작용(side-effect)이 없는 변경으로, 대개 다음 두 가지를 의미해요:
- 서로 다른 메시지들이 서로를 정상적으로 처리하는 것.
- 그래프 버전 간에 바이너리/JSON 직렬화 형식을 유지하는 것.
예를 들어, {"type": "human", "content": "야, 날씨 어떠니?"}와 {"type": "ai", "content": "지금 맑아!"}라는 두 메시지를 생각해봐요. 이것들이 서로를 알든 모르든, 이 메시지들은 서로 "충돌"하지 않아요. 소프트 상태 변경의 목표는 LLM이 이전 그래프 버전에서 저장한 메시지를 올바르게 읽을 수 있게 하는 거예요.
하드 상태 (Hard State)
하드 상태는 직렬화 형식이 이전 버전의 그래프와 호환되지 않게 바뀌면서, 새 버전의 그래프가 이전 상태를 읽거나 처리할 수 없게 되는 변경을 뜻해요. 이건 다음 중 하나로 해결돼요:
- 상태를 무시하거나 손실(elision)함으로써 불일치를 피하는 것.
- 상태를 이전 버전으로 되돌릴 수 있게 마이그레이션하는 것.
요컨대 소프트 상태와 하드 상태를 구분하면, 어떤 그래프 변경이 기존 사용자에게 "안전"한지 판단할 수 있어요.
실제 하위 호환성 시나리오
이제 여러 실제 시나리오를 살펴봐요.
채널 메모리에 대한 하위 호환성
시나리오 A: 예측 가능한 노드, 예측 가능한 생성 필드
첫 번째 시나리오에서, 실행할 중간 노드는 "처리할 메시지"의 리스트를 기반으로 결정되고(즉, "메시지 버퍼"의 유무), 그래프의 생성(construct)은 예측 가능해서 이런 질문에 답할 수 있어요.
-
어떤 노드가 이전 상태를 기반으로 실행되지? 다음 노드는
ShouldContinue에 의해next에서 정해져요.ShouldContinue는 모든 메시지가 "처리됨" 상태인지 확인해요. 즉,not full_check명령을 내보내면Send를 트리거해 재실행할 수 있죠. 그래서 이 경우ShouldContinue가 "다음 노드"가 됩니다. -
이전 상태가 실제로 이전 버전의 그래프와 호환되는가?
ShouldContinue가 "생성"한 메시지는 빈 채널("처리된" 채널)이에요.LastValue채널은 마지막 값이 되니까, 문제가 없어요. -
그래프의 로직(코드)이 호환되는가?
ExecuteModel노드가 "생성"한final메시지는 누적되면서 없어지지 않는Topic채널이에요. 그래서 이 상태도 괜찮아요. 추가적으로novel이라고 표시된 채널들은 절대 저장되지 않을 거예요.
결론: 이 시나리오는 소프트 상태라서 안전해요. 그래프를 재배포해도 기존 사용자는 문제가 없어요.
시나리오 B: 예측 가능한 노드, 예측 불가능한 생성 필드
두 번째 시나리오에서, 실행할 노드는 여전히 "메시지 버퍼" 리스트에 기반해서 결정되지만, 그래프의 결과 계산(상태 생성) 은 예측 불가능해요. 예를 들어, ShouldContinue가 이전 상태에 존재하는지 런타임에 확인하는 보조 채널을 사용한다고 가정해보죠.
-
어떤 노드가 이전 상태를 기반으로 실행되지? 이전과 동일하게 실행할 다음 노드는
ShouldContinue에 의해 결정돼요. -
이전 상태가 실제로 이전 버전의 그래프와 호환되는가? 이전 그래프 버전은
ShouldContinue노드의 출력에서 추가 보조 채널을 생성했나요? 예를 들어, 상태가 "현재 노드 이름"을 추적한다면, 어떤 시점에서 이 상태는 이전에 존재하지 않았던LastValue유형 보조 채널 "my_let"을 포함하게 될 수 있어요. 이 경우 저장된 상태와 새 그래프 사이에 하드 불일치가 생겨요. 특히 메시지가 실행될 새 그래프 노드(예:node_b)의 경로가 현재 상태에 의존한다면, 새 코드가 그 채널을 잘못 읽어 스키마 오류가 날 수 있어요. -
그래프의 로직(코드)이 호환되는가? 이전 버전의 그래프가
foo와bar라는 두 보조 채널을 출력했지만, 새 그리듀서가 이전 채널을 기대한다면 완전한 하드 상태 중단이 발생해요. 이는 신규 추가 상태를 의미하기도 하고, 기존 상태의 누락을 의미하기도 해요.
시나리오 C: 예측 불가능한 노드
세 번째 시나리오에서, 실행할 노드가 전적으로 "런타임 상태" 에 기반해서 결정되는 경우를 생각해봐요. 두 개의 다른 노드가 동시에 상태를 만들 수 있어서, 실행할 다음 노드를 컴파일 타임에 알 수 없어요.
-
어떤 노드가 이전 상태를 기반으로 실행되지? 이전 그래프가 "동시 실행을 허용"하는 상태라면, 이전 상태에 남아 있는
tasks는 계속 실행될 거예요. 새로 배포된 그래프가 이런 중간 상태를 원하지 않는다면 하드 중단이 됩니다. -
이전 상태가 실제로 이전 버전과 호환되는가? 이 경우 디렉터드 그래프
canvkit그리듀서가 실행되어서, 새 그래프가 의존하는 채널이 저장된 상태에 없다면 종료할 수 있어요.
받아칠 것
이런 종류의 불일치를 예방하는 보편적인 방법은, 저장된 상태와 예상 상태 사이의 저장 스키마를 강제하는 것이 없기 때문에 확인하기 어려울 수 있어요. 여기서 절대적인 규칙은: make the graph's state and storage as predictable as possible(그래프의 상태와 저장을 가능한 한 예측 가능하게 만든다)입니다. 궁극적으로는 다양한 LangGraph 스키마(예: MessageGraph, StateGraph의 Migrate 스키마, 또는 수기로 작성한 Pregel 스키마)로 인해, 새 그래프가 저장된 상태의 특성대부분을 추론할 수 있게 하는 게 목표예요.
메시지 표시 형식 변경에 대한 하위 호환성
LangGraph는 사용자 지정 메시지 유형(language model과 상호작용하는 하위 클래스)을 지원해요. 예를 들어 Anthropic이 AIMessage 스타일을 정의하면 AnthropicMessage의 하위 클래스로 만들 수 있어요. 하지만 스키마 타입은 런타임에 존재해야 해요. 즉, 저장된 상태에 있는 클래스를 메모리에 로드할 수 있어야 해요. 그래서 그래프를 바꿀 때:
- AI 메시지를 사용자가 커스텀 메시지로 바꿀 때, SDK/Langchain 클래스에 없던 이상한 형식을 사용하면 직렬화 오류가 날 수 있어요.
- 커스텀 필드를 추가하면 Python의 하위 클래스이지만 메시지를 로드하는 데 문제가 생길 수 있어요.
타임 트래블과 상태 마이그레이션에 대한 하위 호환성
타임 트래블은 LangGraph의 강력한 기능이에요. 그러나 타임 트래블이 동작하려면 저장된 상태를 검사·수정할 수 있어야 해요. 그래프 버전을 올리고 새 코드로 이전 상태에 접근하는 것과 관련해서는 주의해서 다뤄야 해요. 타임 트래블 호환성을 취할 때는 일반적으로 다음 두 가지 옵션이 있어요:
- 전역 옵션이 무시돼요. 특정
_ts.next값(예: 마지막 실행)에서 state/command를 수정할 때,_ts가 "불일치 상태"로 표시되면 그래프가 새 상태를 전역적으로 롤백하거나 일관성을 유지하려고 해요. - 부분적으로 소프트/하드 상태로 취급이 변해요. 타임 트래블 시나리오에서
public명령이 그래프를 재실행하는 방식 때문에, 일부 하드 상태 오류가 예외 대신 재실행에서 "무시"될 수 있어요.