2026-08-23-TIL: 파일로 주고받는 상태와 A2A 프로토콜
최근 2주는 다섯 달 가까이 멈춰 있던 Starkraft를 다시 연 기간이었다. 결정적(deterministic) RTS 시뮬레이션 코어는 거의 완성해 두었고, 이번에는 libGDX 클라이언트를 반복해서 플레이할 수 있는 상태로 만드는 데 집중했다. 그 사이 Dudecide 프론트엔드를 붙였고, monticker 테스트를 다시 짰다.
요즘 한 작업
Starkraft: 클라이언트 안정화
먼저 안정화 계획을 문서로 썼다. 목표는 폴리싱이 아니라 “반복되는 내부 플레이 세션에서 믿고 쓸 수 있는 클라이언트”다. 종료 조건으로는 크래시가 없을 것, HUD가 월드를 가리지 않을 것, 핵심 명령이 일관되게 동작할 것, 클라이언트 I/O가 깨지지 않을 것을 적었다.
커밋은 대부분 이 목록을 따라갔다. HUD 위 클릭이 월드로 새지 않게 입력 상태를 정리하고, 작은 화면에서 HUD를 압축했다. 바뀌지 않은 HUD는 매 프레임 다시 만들지 않도록 캐시하고, 하드코딩된 색은 테마 토큰으로 옮겼다.
Dudecide, spring-internals-lab, monticker
Dudecide는 백엔드를 마저 채우고 Next.js 프론트엔드를 붙였다. 로컬 스택을 처음 실제로 띄워 보니 CORS와 JSON 필드 이름이 프론트엔드와 맞지 않아서 둘 다 고쳤다. spring-internals-lab은 지금까지 만든 축소 구현을 하나의 캡스톤 앱으로 묶었고, 생략했던 NESTED 전파를 JDBC savepoint로 추가했다.
monticker에서는 백엔드 모듈마다 Testcontainers 기반 통합 테스트 소스셋을 만들었다. 그러자 바로 버그가 하나 나왔다. findTopByOrderByEventTimeDesc는 넘긴 Pageable 크기와 상관없이 항상 1건만 돌려주고 있었다. Spring Data 메서드 이름에서 Top 뒤에 숫자가 없으면 결과가 1건으로 고정되기 때문이다. 목 기반 단위 테스트로는 보이지 않던 문제라, 이름을 findByOrderByEventTimeDesc로 바꿔 Pageable이 크기를 정하게 했다.
iterview는 제품 정의를 “이력서 방어”로 다시 쓰고, 그에 맞춰 화면을 작업 공간 단위로 다시 디자인했다.
프로세스 사이에서 파일로 상태를 주고받을 때
Starkraft 런처는 시뮬레이션과 클라이언트를 별도 프로세스로 띄우고, 둘은 파일로 통신한다. 클라이언트는 일시정지 여부와 배속을 control 파일에 쓰고, 시뮬레이션은 틱마다 그 파일을 읽는다. 반대로 시뮬레이션은 스냅샷을 NDJSON 파일에 한 줄씩 덧붙이고, 런처는 첫 스냅샷이 생기면 클라이언트를 붙인다. 두 경로 모두 읽는 쪽이 아직 쓰는 중인 파일을 보는 문제가 있었다.
control 파일은 Files.writeString(path, ...)으로 바로 덮어쓰고 있었다. 이 호출은 파일을 비운 다음 내용을 쓰기 때문에, 그 사이에 읽으면 빈 파일이나 반쯤 쓰인 내용을 보게 된다. 같은 디렉터리에 임시 파일을 다 쓴 뒤 이름을 바꾸는 방식으로 고쳤다.
1
2
3
4
5
6
Files.writeString(temp, text)
try {
Files.move(temp, path, REPLACE_EXISTING, ATOMIC_MOVE)
} catch (_: AtomicMoveNotSupportedException) {
Files.move(temp, path, REPLACE_EXISTING)
}
같은 파일 시스템 안의 rename은 원자적이라, 읽는 쪽은 이전 내용이나 새 내용 중 하나만 본다. 임시 파일을 같은 디렉터리에 만드는 이유도 여기에 있다.
스냅샷 쪽은 판단 기준이 문제였다. 런처는 “파일 크기가 0보다 크면” 첫 스냅샷이 준비됐다고 봤는데, NDJSON은 한 줄이 끝나야 레코드 하나가 완성된다. 기준을 “개행 문자가 있는가”로 바꾸고, 반쪽 레코드를 먼저 쓰는 테스트로 확인했다. 정리하면 덮어쓰는 파일은 rename으로 교체하고, 덧붙이는 파일은 줄 단위로만 읽는다.
느낀 점
종료 조건에 “클라이언트 I/O가 깨지지 않을 것”을 적어 둔 덕분에 이런 문제를 따로 찾아보게 됐다. 평소 플레이에서는 거의 재현되지 않는 버그라 목록에 없었다면 넘어갔을 것 같다. monticker의 Top 버그도 비슷했다. 목으로 리포지토리를 대신하는 테스트는 메서드 이름이 실제로 어떤 쿼리가 되는지 검증하지 못한다.
A2A 프로토콜
얼마 전 Google이 만든 Agent2Agent(A2A) 프로토콜이 Linux Foundation 산하 Agentic AI Foundation(AAIF)으로 옮겨 갔다는 소식이 있었다. 이 재단에는 이미 MCP, AGENTS.md, goose 같은 프로젝트가 들어가 있다.
MCP와 A2A는 다루는 경계가 다르다. MCP는 에이전트가 도구와 데이터에 붙는 방식을 표준화하고, A2A는 서로 다른 회사나 프레임워크로 만든 에이전트끼리 서로를 찾고 메시지를 주고받는 방식을 정한다. 각 에이전트는 자기 능력과 접근 방법을 적은 Agent Card라는 메타데이터를 HTTP와 JSON으로 공개하고, 다른 에이전트가 이를 보고 작업을 맡기는 식이다.
다만 A2A는 이미 작년에 Linux Foundation 프로젝트로 넘어가 있었다. 이번에 바뀐 건 누가 통제하느냐보다 어느 재단이 관리하느냐에 가깝다. 에이전트 관련 표준이 한 지붕 아래 모였다는 의미는 있지만, 실제 채택으로 이어질지는 기사에서도 지켜봐야 한다고 본다. 이번에 프로세스 둘이 파일 하나를 두고 계약을 맞추느라 애쓴 걸 생각하면, 에이전트끼리의 계약을 표준으로 정하려는 이유는 이해가 된다.
참고:
다음에 확인할 것
- Starkraft 안정화 종료 조건을 하나씩 확인하고 폴리싱 단계로 넘어갈 수 있는지 판단하기
- control 파일 방식에서 배속 변경이 몇 틱 늦게 반영되는지
- Dudecide의 OpenAPI 생성 클라이언트와 실제 JSON 필드 이름이 다시 어긋나지 않게 하는 장치
댓글
아직 댓글이 없습니다