개발 기록 2026-10-17 docs/devlog/2026-10-17-12-선행-작업과-두번째-병렬-라운드.md GitHub에서 보기

선행 작업과 두 번째 병렬 라운드

목표

M1(데이터 기반과 GL 적재)을 10/18 계획에서 하루 당겨 끝낸다. 메인 세션이 공유 파일(의존성, 마이그레이션, 스키마, 라우트)과 접근 제어 핵심을 먼저 만들고, 코드 워커 3명이 그 위에서 병렬로 구현한다. 완료 조건은 합성 GL 2만 행 적재 테스트 통과다(WBS 1.17).

한 일

선행 작업 (메인, 순차, WBS 1.1-1.10)

커밋 내용
58e0183 Oban 2.24.2, NimbleCSV 1.3.0, 큐 ingest 1 / rules 4 / llm 2
f01ecbd Oban, Presence 를 슈퍼비전 트리에 추가
3ee2579 마이그레이션 8개 (oban_jobs 부터 audit_events append-only 트리거까지)
60bc32d 도메인 스키마 13개와 DB 제약 테스트
e073c9a AuditTrail.append/3
54d19a8 Engagements.Policy 역할별 권한표
9a50daa engagement 라우트 7개와 자리표시 LiveView
44dbb30, 92fa9e2 계약 문서 반영, Ledger.Generator 계약과 분담 조정
2f104a1 접근 제어 핵심 (fetch_role/2, authorize/3, topic/1, subscribe/2, broadcast/2)

선행 작업 끝에서 mix precommit 통과, 테스트 145개. 워커 브랜치 3개는 모두 92fa9e2 에서 갈라졌고 테스트 DB 는 MIX_TEST_PARTITION (_a, _b, _c) 으로 나눴다.

병렬 라운드 (워커 3명, WBS 1.11-1.13)

워커 범위 병합 병합 후 테스트
A Engagements 나머지 공개 함수, S1 목록과 S2 상세 f699d52, 메인 보정 2601443 179
C Ledger.Generator, 정답 라벨, Testing.Holidays 56c701d 191
B CsvParser, Row, Importer, ImportWorker ece19bd 238

병합은 끝난 순서대로 A, C, B 로 했다. 세 브랜치 모두 공유 파일(AGENTS.md 5.3)을 건드리지 않아서 충돌은 없었다. B 는 자기 worktree 에서 192개(기존 145 포함)로 끝났고, A, C 가 먼저 들어간 위에 병합해도 실패 없이 238개가 됐다.

병합 뒤 메인 작업 (WBS 1.15-1.18)

커밋 내용 테스트
cf923bb generate_sample_import/3: 생성기 출력을 <uuid>.csv, 라벨을 <uuid>.labels.csv 로 쓰고 업로드와 같은 register_import/3 경로로 등록. 기본 시드 적재 통합 테스트 246 (e2e2fd4 포함)
e2e2fd4 S2 화면에 GL 적재 목록 연결 (stream, 상태 배지, KST 시각, 출처 표기)
8a49862 CSV 오류 코드 4종 추가 (unknown_column, invalid_value, too_long, duplicate_line_number) 247
f81367d .formatter.exs import_deps 에 :oban 추가
6ecab07 10/18 구현 결과를 계약 문서(2.1, 4.2, 9.2)와 도메인 문서(7.1)에 반영

측정

모두 Apple M3 Pro, 로컬 PostgreSQL 16.4 에서 잰 실제 출력이다.

대상 결과 출처
B 의 2만 행 적재 (process_import/1, 7,000 전표 / 20,000 라인, Sandbox 안) 748.3 / 663.6 / 663.3 ms devlog -10
같은 파일 파싱만 (CsvParser.parse/3, DB 없음) 121.3 / 113.3 / 110.5 ms devlog -10
기본 시드 생성 (mix run, 1회) 130 ms, GL 2,709,233 바이트 (약 2.7 MB), 라벨 339,614 바이트 메인
통합 테스트: 생성, 등록, 적재까지 (8,341 전표 / 19,804 라인) 981.8 ms (mix test 의 가장 느린 테스트 출력) 메인

통합 테스트는 CRLF 줄 끝을 바꾸지 않고 그대로 넣었고, 오류 없이 succeeded 로 끝났다. 적재된 전표번호 집합은 라벨 파일의 전표번호 집합과 같았다. WBS 리스크 표의 "Generator 출력과 CsvParser 입력 형식 불일치" 는 이것으로 닫는다.

결정과 트레이드오프

  • 병합 순서는 끝난 순서대로. 계약을 먼저 고정했기 때문에 A, B, C 사이에 코드 의존이 없었다. 셋이 다 끝나길 기다렸다 한 번에 넣는 것보다, 끝난 브랜치를 바로 검토하고 넣는 쪽이 검토 부담이 나뉜다. 대신 병합마다 mix precommit 을 돌려 앞 병합이 뒤 브랜치를 깨지 않았는지 확인했다.
  • B 검토. 계약 문서 2.1 의 Ledger 시그니처와 반환값, 9.2 의 검증 순서를 대조했고 위반은 없었다. B 가 계약에 없는 오류 코드 4개(too_long, invalid_value, unknown_column, 선택으로 duplicate_line_number)를 요청했다. 그동안은 parse_error 와 invalid_number 에 섞여 있었다.
  • 오류 코드 4종을 지금 넣은 이유. 10/19 의 ImportLive 가 코드별 한글 메시지 표를 만든다. 그 표가 생기기 전에 코드를 나누면 바뀌는 곳이 파서와 테스트뿐이지만, 표가 생긴 뒤에 나누면 메시지 표, LiveView 테스트, 이미 저장된 적재의 errors 배열까지 같이 바뀐다. 지금이 가장 싸다. unknown_column 은 알 수 없는 헤더 이름을 오류에 되풀이하지 않는다(사용자 입력을 그대로 화면에 다시 쓰지 않는다).
  • target_lines 는 MVP 에서 지원하지 않는다. 도메인 문서 7.1 은 행 수 조정 옵션을 적었지만 계약은 seed 하나만 받는다. 기본 시드가 19,804행으로 "약 2만 행" 안에 들어가므로 데모에는 충분하다. 도메인 문서에 미지원과 기본 시드 실측값을 적었다.
  • 합성 GL 의 원본 파일명에 시드를 남긴다. synthetic-gl-seed-<seed>.csv 로 저장하면 별도 컬럼 없이 S2 출처 칸에서 "합성 GL, 시드 N" 으로 보여줄 수 있다. 업로드 파일명이 우연히 같은 모양이면 합성으로 보이는 단점이 있지만, source_kind 가 generated 일 때만 이 해석을 쓰므로 문제 없다.
  • 300줄 넘는 파일. 생성된 파일(core_components.ex 505, user_auth_test.exs 401, accounts_test.exs 397) 말고 csv_parser.ex 404, ledger.ex 392, ledger_test.exs 474, generator_test.exs 381, csv_parser_test.exs 330, engagement_live_test.exs 325 줄이 권장 기준을 넘는다. 지금 나누면 M2 워커가 계약 문서와 다른 모듈 경로를 보게 되므로 이번 라운드에서는 두고, M2 에서 Ledger 가 더 커질 때 다시 본다.

막힌 점과 해결

  • S2 토픽 메시지 테스트가 깨졌다. e2e2fd4 에서 :import_updated 처리가 Ledger.get_import!/2 로 적재를 다시 읽게 되자, A 가 만든 "토픽 메시지를 받아도 화면이 죽지 않는다" 테스트가 없는 id 로 보낸 :import_updated 때문에 Ecto.NoResultsError 로 실패했다. 이 메시지는 실제 적재를 만들어 보내는 GL 적재 테스트가 이미 다루므로, 그 테스트에서는 :import_updated 를 빼고 나머지 세 메시지(:test_run_updated, :finding_updated, :audit_appended)만 남겼다. 없는 id 를 보내는 일은 브로드캐스트가 커밋 뒤에만 나가는 계약상 생기지 않는다.
  • Decimal.to_string/1 형식 차이. succeeded 감사 payload 의 total_debit, total_credit 는 기본(:scientific) 형식이고, engagement.created 의 performance_materiality 는 :normal 이다. 지수 표기는 지수가 양수인 Decimal 에서만 나오는데, Row 의 금액 검사가 e 를 받지 않고("1e3" 은 invalid_number, csv_parser_test) 정수 덧셈은 지수를 키우지 않으므로 실제로는 같은 문자열이 나온다. 표기 통일은 다음 Ledger 수정 때 함께 한다.

Elixir 학습 포인트

  • 바이너리 패턴 매칭. %{original_filename: "synthetic-gl-seed-" <> rest} 는 함수 머리에서 접두사가 맞을 때만 매칭되고, 나머지를 rest 로 꺼낸다. 정규식 없이 접두사 검사와 추출이 한 번에 된다.
  • stream_insert(socket, :imports, row, at: 0). stream 은 DOM id 로 행을 식별한다. 같은 id 의 행이 이미 있으면 위치는 그대로 두고 내용만 바꾸고, 없으면 at: 0 으로 맨 위에 넣는다. 그래서 상태 변경(queued 에서 succeeded)과 새 적재를 같은 handle_info 하나로 처리한다.
  • 빈 상태 행. <tr id="imports-empty" class="hidden only:table-row"> 는 tbody 에 이 행 하나만 남았을 때만 보인다(Tailwind only:). stream 은 서버가 목록 길이를 들고 있지 않으므로, 개수를 assign 으로 따로 세지 않고 CSS 로 빈 상태를 처리한다.
  • KST 는 고정 +9시간. 한국은 1988년 이후 서머타임이 없어서 UTC 에 9시간을 더하면 정확하다. 시간대 데이터베이스(tzdata) 의존성을 넣지 않았다. 원본 UTC 값은 <time datetime> 과 title 에 남겨 감사 추적에서 기준 시각이 흐려지지 않게 했다.
  • .formatter.exs 의 import_deps. Oban 은 assert_enqueued 등을 괄호 없이 쓰도록 locals_without_parens 를 내보낸다. import_deps 에 넣어야 mix format 이 괄호를 강제로 붙이지 않는다.

다음 할 일

  • 1.19: feature/foundation 을 main 으로 보내는 PR (사용자 승인 뒤 생성).
  • M2 (10/19): 실행 경로, 룰 모듈, TestRunLive 와 ImportLive 를 워커 3명에게 나눈다. 시작 전에 계약 문서 12장의 M2 배정과 오류 코드별 한글 메시지 표를 확정한다.
  • 확인 필요: 헤더만 있는 파일은 지금 전표 0건 succeeded 로 끝난다(파서 테스트 "accepts a file with a header and no rows"). 거부할지 ImportLive 설계 때 정한다.
  • 확인 필요: C 가 남긴 명세 해석 17건(devlog -11). 룰 적중 대 정답 라벨 비교 테스트(2.4)를 쓸 때 함께 검토한다.
  • B 가 남긴 ImportWorker 마지막 시도 분기 테스트는 장애 주입 수단이 생기면 추가한다(devlog -10).
  • 배포 대상(Azure az containerapp up 또는 Fly.io)은 사용자 결정 대기 (WBS 5.1).

관련 커밋

선행: 58e0183, f01ecbd, 3ee2579, 60bc32d, e073c9a, 54d19a8, 9a50daa, 44dbb30, 2f104a1, 92fa9e2. 병합: f699d52, 2601443, 56c701d, ece19bd. 병합 뒤: cf923bb, e2e2fd4, 8a49862, f81367d, 6ecab07.