4월 27일은 DeToks에서 꽤 많은 부분을 손본 날이었다. 전날에는 Task Graph 로직을 왜 DAG로 만들었는지 정리했다면, 오늘은 그 그래프가 실제 CLI 안에서 계속 이어질 수 있도록 세션과 실행 상태를 다듬었다.

쉽게 말하면 이렇다.

어제까지는 “작업 순서를 어떻게 만들까?”를 고민했다면,
오늘은 “그 작업 흐름을 끊기지 않게 어떻게 저장하고 이어갈까?”를 고민한 날이었다.

오늘 병합된 내 PR은 다섯 개였다.

#134 세션 저장과 재개 구현
#138 task type 분류 오류 수정
#142 세션 ID와 세션 안정성 보강
#145 document 작업 이후 DAG 연결성 수정
#147 task type을 실행 결과에 저장

PR이 여러 개로 나뉘어 있지만, 방향은 하나였다. DeToks가 단순히 한 번 실행되고 끝나는 데모가 아니라, 사람이 하던 작업 흐름을 기억하고 이어갈 수 있는 CLI가 되도록 만드는 것이었다.

오늘 한 일을 먼저 짧게 정리하면

오늘 작업은 크게 다섯 가지였다.

1. 세션을 저장하고 다시 불러올 수 있게 했다.
2. 이미 완료된 task는 다시 실행하지 않게 했다.
3. task type 분류가 틀리는 케이스를 테스트로 찾아 고쳤다.
4. REPL에서 이어서 입력할 때 task id가 충돌하는 문제를 막았다.
5. 멘토링을 통해 로컬 모델과 에러 처리 방향을 다시 정리했다.

여기서 세션은 “이전 작업 기록”이라고 보면 된다. CLI에서 어떤 요청을 했고, 어떤 task가 끝났고, 어떤 결과가 나왔는지를 저장해두는 파일이다.

예를 들어 이런 요청이 들어왔다고 하자.

코드 구조를 분석하고, 문제를 찾고, 수정한 뒤 테스트해줘

DeToks는 이 요청을 여러 task로 나눈다.

t1: 코드 구조 분석
t2: 문제 찾기
t3: 코드 수정
t4: 테스트 실행

만약 t1, t2까지 끝난 상태에서 프로그램이 멈췄다면, 다시 실행했을 때 처음부터 모두 다시 하면 낭비다. 그래서 오늘은 “어디까지 했는지 기억하고, 이어서 실행하는 구조”를 붙였다.

세션을 이어갈 수 있게 만들었다

가장 먼저 작업한 것은 세션 저장과 재개였다.

이전에는 실행 흐름이 한 번 끝나면 그 결과를 이어 쓰기 어려웠다. 하지만 CLI 도구라면 같은 작업을 계속 이어갈 수 있어야 한다. 특히 LLM 호출은 비용이 있고, 파일 수정은 부작용이 생길 수 있다. 이미 끝난 task를 다시 실행하는 건 피해야 했다.

그래서 오케스트레이터에 세션 로드 흐름을 넣었다. 오케스트레이터는 DeToks 안에서 전체 실행 순서를 관리하는 부분이다.

세션을 불러올 때는 아래 순서로 확인한다.

세션 파일이 있는가?
  ├─ 없다
  │   └─ 새 세션을 만든다

  └─ 있다
      ├─ 버전이 맞는지 확인한다
      ├─ 현재 task 목록과 맞는지 확인한다
      ├─ 이미 완료된 task가 있는지 확인한다
      └─ 문제가 없으면 이어서 실행한다

여기서 버전을 확인하는 이유는 간단하다. 예전 세션 파일 모양과 지금 코드가 기대하는 모양이 다를 수 있기 때문이다. 서로 다른 형식을 억지로 이어 쓰면, 나중에 더 찾기 어려운 오류가 생긴다.

또 하나 본 것은 “고아 task”였다. 고아 task는 세션 파일에는 남아 있는데, 현재 Task Graph에는 없는 task를 말한다.

세션 파일:
  t1, t2, t3 완료 기록이 있음

현재 Task Graph:
  t1, t2만 있음

문제:
  t3는 지금 실행 계획에 없는 task인데 세션에는 남아 있음

이런 상태에서 세션을 그대로 재사용하면 실행 판단이 꼬일 수 있다. 그래서 현재 그래프와 맞지 않는 세션은 안전하게 초기화하도록 했다.

이미 끝난 task는 다시 실행하지 않게 했다

세션을 불러오는 것만으로는 부족하다. 불러온 세션을 보고 “이미 끝난 task”를 건너뛰어야 한다.

오늘 추가한 실행 흐름은 이런 식이다.

task를 하나씩 확인한다

이 task가 실패한 이전 task 때문에 막혔는가?

이미 완료된 task인가?

완료됐다면 저장된 결과를 재사용한다

아직 안 했다면 실행한다

실행 결과를 다시 세션에 저장한다

이 흐름이 들어가면 중간에 멈췄다가 다시 실행해도 처음부터 반복하지 않아도 된다.

예를 들어:

첫 실행:
  t1 완료
  t2 완료
  t3 실행 중 실패

다시 실행:
  t1은 이미 완료 -> 건너뜀
  t2도 이미 완료 -> 건너뜀
  t3부터 다시 확인

이건 쓰는 사람 입장에서도 중요하지만, 개발하는 입장에서도 중요했다. 세션이 있어야 나중에 checkpoint, resume, REPL 이어가기 같은 기능을 자연스럽게 붙일 수 있기 때문이다.

세션 파일이 동시에 깨지지 않도록 막았다

세션을 저장하기 시작하면 다른 문제가 생긴다. 같은 세션 파일을 동시에 읽고 쓰면 파일이 꼬일 수 있다.

예를 들어 두 작업이 거의 동시에 같은 세션을 저장하려고 하면:

A 작업: 세션 파일 읽음
B 작업: 세션 파일 읽음
A 작업: 저장
B 작업: 예전 상태 기준으로 다시 저장

이러면 A 작업 결과가 B 작업 저장 때문에 사라질 수 있다.

그래서 SessionStateManager에 파일 락을 넣었다. 파일 락은 쉽게 말해 “지금 이 파일은 내가 쓰는 중이니 잠깐 기다려”라는 표시다.

세션 저장 시작

lock 파일 생성

다른 작업은 기다림

세션 읽기 또는 쓰기

작업이 끝나면 lock 해제

중요한 건 실패해도 lock을 풀어야 한다는 점이었다. 저장 중간에 에러가 났는데 lock이 그대로 남으면, 다음 작업은 계속 기다리게 된다.

그래서 finally에서 lock을 해제하도록 했다.

try:
  세션 읽기 또는 쓰기
finally:
  lock 해제

이건 화려한 기능은 아니지만, 실제 CLI가 오래 돌려면 꼭 필요한 부분이었다.

task type 분류를 더 꼼꼼히 검증했다

다음으로 많이 본 것은 task type 분류였다.

DeToks는 문장을 보고 이 작업이 어떤 종류인지 분류한다.

explore   : 찾기
analyze   : 분석하기
create    : 만들기
modify    : 수정하기
validate  : 검증하기
execute   : 실행하기
plan      : 계획하기
document  : 문서화하기

이 분류가 틀리면 뒤쪽 그래프도 같이 틀어진다. 예를 들어 Run the testsexecute로 잡히면 단순 실행처럼 보이지만, 실제 목적은 테스트 검증이므로 validate가 더 맞다.

오늘은 dataTest_Compact에 있는 106개 파일을 돌려보는 테스트를 추가했다. 하나씩 사람이 보는 대신, 실제 데이터 전체를 통과시키면서 기본 규칙이 깨지는지 확인했다.

테스트에서 본 내용은 이렇다.

문장이 잘 나뉘는가?
task type이 8개 중 하나로 잡히는가?
task id가 t1, t2, t3 순서로 만들어지는가?
depends_on이 앞선 task만 가리키는가?
input_hash 형식이 맞는가?

이 테스트를 돌리면서 make 때문에 생기는 문제가 보였다.

make는 보통 “만들다”라서 create로 분류될 수 있다. 하지만 모든 make가 만들기는 아니다.

make sure       -> 확인하다       -> validate
make changes    -> 변경하다       -> modify
make a note     -> 메모하다       -> document
make a plan     -> 계획을 세우다  -> plan

그런데 단순히 make만 먼저 잡으면 전부 create로 분류될 수 있었다. 그래서 여러 단어가 합쳐진 표현은 먼저 검사하도록 IDIOM_PATTERNS를 추가했다.

먼저 검사:
  make sure
  make changes
  make a note
  make a plan

그다음 일반 키워드 검사:
  make
  create
  fix
  run
  test

이렇게 하면 make suremake 하나로 잘못 잡는 일을 줄일 수 있다.

질문형 문장도 더 자연스럽게 처리했다

또 하나 고친 것은 질문형 문장이다.

예를 들어 이런 문장이 있다.

What should we address first?

이 문장에는 fix, run, test, create 같은 명확한 행동 키워드가 없다. 기존 흐름에서는 이런 문장이 마지막 기본값 때문에 execute로 갈 수 있었다. 하지만 실제 의미는 “무엇을 먼저 봐야 할까?”에 가깝다. 실행보다는 분석이다.

그래서 모든 키워드 검사를 통과한 뒤, 마지막에 ?로 끝나는 문장은 analyze로 보내도록 했다.

다만 이 규칙을 너무 앞에 두면 문제가 생긴다.

How can we validate this?

이 문장은 질문형이지만 validate가 맞다. 그래서 질문형 규칙은 맨 마지막에 두었다.

정리하면 이렇게 된다.

키워드가 있는 질문:
  "How can we validate this?" -> validate

키워드가 없는 질문:
  "What should we address first?" -> analyze

이런 작은 분류 차이가 Task Graph 전체에 영향을 준다. 그래서 오늘은 “작은 표현 하나가 전체 흐름을 바꿀 수 있다”는 걸 다시 느꼈다.

세션 ID를 더 안전하게 바꿨다

세션을 저장하려면 세션 ID가 필요하다. 세션 ID는 각 세션을 구분하는 이름이다.

기존에는 시간값을 바탕으로 만든 12자 문자열을 썼다. 보통은 문제 없지만, 아주 짧은 시간 안에 여러 세션이 만들어지면 충돌 가능성이 있다. 충돌이 생기면 기존 세션 파일을 덮어쓸 수 있다.

그래서 세션 ID를 랜덤 base62 문자열로 바꿨다.

base62는 아래 62개 문자를 쓰는 방식이다.

A-Z
a-z
0-9

새 기준은 이렇게 잡았다.

일반 세션:
  24자 base62

REPL 세션:
  repl- + 16자 base62

그리고 새 ID를 만들었을 때 같은 ID가 이미 있으면 다시 만들도록 했다. 충돌 확률은 낮지만, 낮은 확률이라고 그냥 두기보다 충돌했을 때의 행동을 정해두는 편이 더 안전하다.

REPL에서 이어 입력할 때 생긴 문제도 고쳤다

오늘 가장 기억에 남는 문제 중 하나는 REPL 세션이었다.

REPL은 사용자가 한 번 입력하고 끝내는 게 아니라, 같은 세션에서 계속 입력을 이어가는 방식이다.

문제는 task id였다. Task Graph는 새 입력이 들어올 때마다 t1부터 task id를 만든다. 그런데 같은 세션에서는 이전 입력의 t1이 이미 완료되어 있을 수 있다.

그러면 이런 일이 생긴다.

첫 번째 입력:
  t1 실행 완료
  completed_task_ids = ["t1"]

두 번째 입력:
  새 task도 t1로 생성됨
  이미 완료된 t1이라고 착각함
  실행하지 않고 건너뜀

이건 세션 재개 기능을 붙이고 나서야 드러난 문제였다. 이전에는 매번 새로 실행했기 때문에 잘 보이지 않았다.

해결은 단순하게 잡았다. 같은 세션에서 새 입력이 들어오면, 기존 최대 task id 다음 번호부터 시작하게 했다.

첫 번째 입력 -> t1
두 번째 입력 -> t2
세 번째 입력 -> t3

그리고 이어지는 입력은 이전 결과를 참고할 가능성이 높다. 그래서 명시적인 dependency가 없어도 최근 완료 task 결과를 context로 받을 수 있게 했다.

사람은 보통 이렇게 말하기 때문이다.

방금 분석한 내용 바탕으로 수정해줘

이때 이전 결과를 전혀 넘기지 않으면, 세션을 이어가는 의미가 약해진다.

document 뒤에 작업이 이어지는 경우를 처리했다

오후에는 문서화 task와 관련된 DAG 연결성 문제도 수정했다.

기존에는 document를 거의 마지막 작업처럼 생각했다. 문서화는 보통 마지막에 하는 일이기 때문이다. 그런데 실제 요청에서는 문서화 뒤에도 작업이 이어질 수 있었다.

예를 들어:

Analyze the entire codebase
create a comprehensive documentation with examples
implement all suggested improvements
validate everything

이 요청은 자연스럽게 아래처럼 이어져야 한다.

analyze -> document -> create -> validate

하지만 기존 로직에서는 중간에 그래프가 끊길 수 있었다.

t1 analyze -> t2 document

t3 create -> t4 validate

이렇게 되면 t3가 앞 흐름과 연결되지 않은 task가 된다. DAGValidator는 이것을 고립된 노드로 보고 거부한다.

여기서 검증기가 틀린 것은 아니었다. 끊어진 그래프를 거부하는 건 맞다. 문제는 TaskGraphProcessor가 순서가 있는 요청을 끊어진 그래프로 만든 데 있었다.

그래서 두 가지를 고쳤다.

1. "create documentation" 같은 표현을 document로 더 잘 잡게 했다.
2. document 뒤에도 명시적인 후속 작업이 있으면 흐름이 이어지게 했다.

수정 후에는 기대한 대로 하나의 흐름이 된다.

t1 analyze -> t2 document -> t3 create -> t4 validate

이 작업은 Day 6에서 정리한 DAG 원칙과도 이어진다. 검증을 느슨하게 만드는 게 아니라, 그래프를 만드는 쪽을 더 정확하게 고치는 게 맞았다.

task type을 실행 결과에도 저장했다

마지막으로 task type을 세션 결과에 저장하도록 했다.

이전에는 task가 성공했는지, 어떤 출력이 나왔는지는 저장했지만, 그 task가 어떤 종류였는지는 빠져 있었다.

기존 결과는 이런 형태였다.

{
  "task_id": "t1",
  "success": true,
  "summary": "...",
  "raw_output": "..."
}

수정 후에는 type도 같이 남긴다.

{
  "task_id": "t1",
  "success": true,
  "summary": "...",
  "raw_output": "...",
  "type": "analyze"
}

이게 왜 필요하냐면, 나중에 세션을 다시 볼 때 단순히 “성공했다/실패했다”만으로는 부족하기 때문이다.

t1 실패

라고만 남는 것보다,

t1 analyze 실패

라고 남는 편이 훨씬 이해하기 쉽다. 어떤 종류의 작업에서 문제가 생겼는지 알 수 있기 때문이다.

그래서 성공한 task뿐 아니라 실패한 task, dependency 때문에 skipped 처리된 task에도 type을 남기도록 했다.

멘토링에서는 모델 방향을 다시 잡았다

오후에는 최재흥 강사님께 팀 멘토링을 받았다. 가장 큰 주제는 로컬 모델을 CLI에 포함할지, 아니면 API 통신을 쓸지였다.

팀 안의 고민은 이랬다.

CLI라면 가벼워야 한다
  vs
번역 품질을 생각하면 큰 모델이 필요하다

소형 모델은 가볍지만 한국어를 영어 개발 명령으로 바꾸는 품질이 부족했다. 반대로 3GB 이상 모델은 품질은 나아지지만 설치 용량과 메모리 사용량이 부담이었다.

강사님이 먼저 던진 질문은 이거였다.

CLI의 목표가 무엇인가?

경량성이 목표라면 작은 모델을 쓰고 정확도를 조금씩 올리는 방향이 맞다. 정확도가 목표라면 API 통신을 허용해서 더 큰 모델을 쓰는 게 맞다.

이 질문을 듣고 나니 팀에서 흔들리던 이유가 보였다. 우리는 CLI 앞단에서 작게 개입하는 도구를 만들고 싶어 했지만, 동시에 번역 품질도 놓치고 싶지 않았다. 둘 다 한 번에 잡으려고 하니 판단이 어려웠던 것 같다.

멘토링 후 팀의 결론은 이렇게 정리했다.

기본 방향:
  로컬 모델 번들링 유지

추가로 정해야 할 것:
  - 예상 사용자 기기 스펙
  - 허용할 메모리 사용량
  - API 방식과의 성능 비교
  - 자체 서버 호스팅은 비용 문제로 제외

여기서 로컬 모델 번들링은 CLI를 설치할 때 모델도 같이 포함하는 방식이다. 처음에는 “CLI에 3GB 모델을 넣는 게 맞나?” 싶었는데, 강사님은 오히려 처음부터 용량을 명시하고 설치하는 편이 쓰는 사람에게 더 자연스러울 수 있다고 했다.

중간에 갑자기 큰 파일을 추가로 받게 하는 것보다, 처음부터 “이 도구는 모델을 포함해서 이 정도 용량이 필요합니다”라고 알려주는 편이 낫다는 이야기였다.

결국 중요한 건 무조건 작게 만드는 것도, 무조건 큰 모델을 쓰는 것도 아니었다. 우리가 찾아야 하는 건 성능과 정확도가 둘 다 나쁘지 않은 지점이었다.

에러 처리는 둘 중 하나를 고르는 문제가 아니었다

멘토링에서 에러 처리 이야기도 나왔다.

팀 안에서는 이런 고민이 있었다.

try-catch로 처리할까?
아니면 결과값에 성공/실패를 담을까?

강사님 피드백은 명확했다.

둘 중 하나가 아니라 둘 다 써야 한다.

예외는 try-catch로 잡아야 한다. 하지만 catch 안에서 에러를 그냥 없애면 안 된다. 잡은 에러를 바탕으로 실패 결과를 만들어서 반환해야 한다.

오늘 세션 로드 실패 방어도 이 방향과 맞았다.

나쁜 흐름은 이렇다.

세션 로드 실패

catch에서 조용히 처리

새 세션 생성

기존 세션을 덮어쓸 위험

더 나은 흐름은 이렇다.

세션 로드 실패

catch에서 에러를 잡음

실패 결과를 반환

저장 중단

기존 세션 보호

실패를 없었던 일처럼 넘기는 것보다, 실패했다는 사실을 명확히 남기는 것이 더 안전하다.

모듈 책임은 너무 일찍 고정하지 않기로 했다

멘토링에서 또 도움이 된 부분은 모듈 간 책임이었다.

DeToks는 여러 모듈이 이어져 있다.

Role 1

Task Graph

State & Context

Orchestrator

CLI

각 모듈이 데이터를 주고받다 보니 “이 검증은 어디서 해야 하지?”, “이 변환은 어느 모듈 책임이지?” 같은 고민이 계속 생겼다.

강사님은 처음부터 모든 책임을 완벽하게 나누려고 하지 말라고 했다. 우선 팀이 합의한 데이터 형태로 개발하고, 실제 충돌이 생기면 그때 컨버터 함수를 두는 방식이 현실적이라고 했다.

컨버터 함수는 쉽게 말해 “한 모듈의 데이터를 다른 모듈이 원하는 모양으로 바꿔주는 함수”다.

모듈 A의 데이터

converter

모듈 B가 원하는 데이터

이 방식은 지금 DeToks 상황에 잘 맞았다. 모든 것을 처음부터 하나의 완벽한 모양으로 묶으려고 하면 오히려 개발 속도가 느려질 수 있다. 지금은 통합하면서 실제로 부딪히는 지점을 보고 고치는 편이 맞다.

오늘 작업을 하나의 흐름으로 보면

오늘 작업을 전체 흐름으로 그리면 이렇다.

사용자 입력

문장 분리

task type 분류
  - make sure 같은 숙어 먼저 처리
  - 키워드 없는 질문은 analyze로 처리

Task Graph 생성
  - document 뒤 후속 작업 연결

DAG 검증

오케스트레이터 실행
  - 세션 로드
  - 이미 완료된 task skip
  - task type 전달

세션 저장
  - 파일 락
  - 세션 버전 확인
  - 실패 task 기록

전날에는 그래프 자체를 어떻게 만들지에 집중했다. 오늘은 그 그래프가 세션 안에서 어떻게 유지되는지 봤다.

그래프만 보면 t1, t2, t3이면 충분하다. 하지만 세션이 붙으면 이야기가 달라진다.

이 t1은 예전에 끝난 t1인가?
아니면 방금 새로 만든 t1인가?

이 세션 파일은 지금 코드와 맞는가?
이 task는 다시 실행해야 하는가?
이전 결과를 다음 입력에 넘겨야 하는가?

오늘 작업은 이런 질문들에 대한 답을 하나씩 코드로 넣은 과정이었다.

마무리

오늘은 눈에 확 보이는 새 기능을 만든 날이라기보다, DeToks가 실제 CLI처럼 버틸 수 있게 바닥을 다진 날에 가까웠다.

세션을 저장하고 다시 불러오는 일은 간단해 보였지만, 막상 붙여보니 세션 ID 충돌, 파일 락, 로드 실패, REPL 이어가기, checkpoint 복구 같은 문제가 따라왔다. task type 분류도 마찬가지였다. make sure 같은 작은 표현 하나가 전체 그래프 흐름을 바꿀 수 있었다.

멘토링을 들으면서 프로젝트 방향도 다시 정리했다. 로컬 모델을 쓸지 API를 쓸지는 단순히 용량 문제로만 볼 수 없었다. CLI의 목표, 사용자 기기 성능, 번역 품질, 메모리 사용량을 같이 봐야 했다. 에러 처리도 실패를 숨기는 방향이 아니라, 실패를 기록으로 남기는 방향이 맞다는 기준이 생겼다.

오늘 작업을 지나면서 DeToks는 “작업 순서를 만드는 도구”에서 조금 더 나아갔다. 이제는 그 작업을 저장하고, 이어가고, 실패를 남기고, 다시 설명할 수 있는 형태에 가까워졌다. 아직 통합하면서 더 고칠 부분은 많지만, 오늘은 확실히 프로젝트가 실제 CLI 쪽으로 한 단계 더 가까워진 날이었다.

Community

Comments

0 comments

Comments appear immediately. Use report if something needs review.

No comments yet.