0biglife.

[자율주행] 어노테이션 툴 만들기 ② 백엔드 — 라벨을 잃지 않고 다투지 않게

자율주행/어노테이션 툴

· 2026-09-07

[자율주행] 어노테이션 툴 만들기 ② 백엔드 — 라벨을 잃지 않고 다투지 않게

본문의 점선 밑줄 친 전문용어를 누르면 설명이 열립니다.

앞 글에서 에디터를 다뤘다. 혼자 쓰는 도구라면 거기서 끝이다. 그런데 툴은 수십 명이 몇 달 동안 쓰는 시스템이고, 그러면 반드시 이런 것들이 온다.

  • 두 사람이 같은 씬을 동시에 열었다
  • 브라우저가 죽었는데 두 시간치 작업이 날아갔다
  • 이 바뀌었는데 과거 라벨 20만 건은 어떻게 하나
  • "이 박스 누가 언제 이렇게 옮겼나"에 답해야 한다
  • 검수자가 반려했는데 그게 누구에게 어떤 상태로 돌아가나

이 글은 층이다. 그리고 여기가 축 B의 데이터 엔지니어링과 정면으로 만나는 지점이기도 하다.

라벨 스키마 — 무엇을 저장하는가

먼저 저장 단위를 정한다. 자율주행 라벨에는 자연스러운 계층이 있다.

데이터셋
└─ 로그(log_id)
   └─ 프레임(t_ns)          ← 센서 캡처 시각 기준
      └─ 객체(instance)
         ├─ 기하 (큐보이드 / 폴리라인 / 마스크 …)
         ├─ 클래스
         ├─ 속성 (가려짐·잘림·신호 색 …)
         └─ 트랙 id          ← 프레임을 넘어 이어지는 것

여기서 첫 번째 설계 결정이 나온다. 프레임 단위로 저장할 것인가, 트랙 단위로 저장할 것인가.

  • 프레임 단위 — "이 프레임의 모든 객체"를 읽고 쓴다. 에디터가 프레임을 넘나들 때 자연스럽다.
  • 트랙 단위 — "이 객체의 전 생애"를 읽고 쓴다. 트랙 편집(분리·병합)과 보간에 자연스럽다.

둘 다 필요하다. 실무에서는 객체 단위로 저장하되 (log_id, t_ns)track_id 양쪽에 인덱스를 걸어 두 방향 조회를 모두 지원하는 형태가 흔하다.

반드시 붙는 메타데이터

라벨 값만 저장하면 나중에 반드시 후회한다.

label_spec_version   어떤 규칙으로 만들었나
source               human | auto:v3 | auto+human
frame_id             어느 좌표계인가 (센서 / 차량 / 월드)
image_space          distorted | undistorted   ← 2D 라벨이면
pose_source          online | refined          ← 3D 라벨이면
created_by / at
updated_by / at

pose_source가 왜 필요한지는 측위 글에서 다뤘다. 온라인 포즈로 누적한 위에 그린 박스와 위에 그린 박스는 다른 물건이다.

스키마가 바뀔 때

은 반드시 바뀐다. "자전거 끌고 가는 사람"의 분류가 바뀌고, 속성이 하나 추가되고, 클래스가 쪼개진다. 그때 과거 라벨 20만 건을 어떻게 할 것인가.

전략 셋

방식방법언제
읽기 시 변환저장은 그대로, 읽을 때 최신 버전으로 승격변환이 단순하고 되돌릴 수 있을 때
일괄 백필배치로 전부 새 버전으로 다시 씀변환이 확정적이고 자주 읽힐 때
버전 병존옛 버전은 그대로 두고 새 데이터만 새 버전변환이 불가능할 때(사람이 다시 봐야 함)

세 번째가 가장 흔하고 가장 위험하다. 두 버전이 한 테이블에 섞이면 학습 세트에 모순된 정답이 들어간다. 그래서 버전을 컬럼으로 노출하고, 학습 세트를 뽑을 때 반드시 필터링해야 한다.

원칙 하나 — 마이그레이션은 되돌릴 수 있어야 한다. 백필 전 스냅샷을 남기고, 변환 스크립트를 코드로 관리한다. "한 번 돌리고 지운 스크립트"는 6개월 뒤에 반드시 필요해진다.

저장 API — 전체를 매번 보내지 않는다

가장 순진한 설계는 "씬 전체를 PUT"이다. 객체 300개짜리 프레임이면 저장 한 번에 수 MB고, 30초마다 자동 저장하면 네트워크가 죽는다.

부분 저장으로 간다.

PATCH /scenes/{log_id}/frames/{t_ns}
{
  "base_version": 41,
  "ops": [
    { "op": "update", "id": "obj_17", "center": [12.4, -3.1, 0.8], "yaw": 1.57 },
    { "op": "create", "tmp_id": "new_3", "class": "car", ... },
    { "op": "delete", "id": "obj_29" }
  ]
}

여기서 두 가지를 챙긴다.

. 네트워크가 불안하면 클라이언트가 재시도한다. 같은 요청이 두 번 도착해도 결과가 같아야 한다. 요청마다 클라이언트가 만든 키(request_id)를 붙이고, 서버는 이미 처리한 키를 기억한다. 이게 없으면 재시도 한 번에 박스가 두 개 생긴다.

임시 id. 새로 만든 객체는 아직 서버 id가 없다. 클라이언트가 tmp_id를 붙여 보내고 서버가 실제 id를 돌려주는 매핑이 필요하다.

자동 저장의 현실적인 모양

편집 → 로컬(IndexedDB)에 즉시 → 디바운스 2~3초 → 서버로 PATCH
                                              ↓ 실패
                                     로컬에 큐로 쌓고 재시도

로컬 우선이 핵심이다. 브라우저가 죽거나 네트워크가 끊겨도 작업이 남아 있어야 한다. 라벨러에게 "두 시간치가 날아갔다"는 경험을 한 번 주면 그 도구는 신뢰를 잃는다.

동시성 — 잠금이냐 병합이냐

두 사람이 같은 프레임을 편집하면 어떻게 되나.

① 작업 단위 잠금 — 대부분 이걸로 충분하다

솔직하게 말하면, 툴에서 실시간 동시 편집이 필요한 경우는 드물다. 작업은 보통 씬 단위나 로그 단위로 한 사람에게 할당되고, 두 사람이 같은 프레임을 동시에 만질 이유가 거의 없다.

그래서 가장 실용적인 답은 작업 배정 자체를 잠금으로 쓰는 것이다. 씬이 누군가에게 할당되면 다른 사람은 읽기 전용으로 연다. 구현이 단순하고, 라벨러가 이해하기 쉽다.

주의할 점은 잠금 만료다. 라벨러가 브라우저를 그냥 닫으면 잠금이 영원히 남는다. 하트비트를 받고 일정 시간 없으면 자동 해제한다.

② 낙관적 잠금 — 그래도 필요하다

잠금이 있어도 사고는 난다. 탭을 두 개 열었거나, 관리자가 강제로 편집했거나, 잠금이 만료된 뒤 원래 사용자가 저장을 시도하거나.

그래서 저장할 때 버전을 함께 보낸다.

클라이언트가 base_version 41 을 들고 저장 시도
  서버의 현재 버전이 41 → 적용, 42 로 올림
  서버의 현재 버전이 43 → 409 Conflict 반환

충돌이 나면 무엇을 하나. 덮어쓰기를 기본값으로 두면 안 된다. 최소한 "다른 사람이 이 사이에 수정했습니다. 다시 불러오시겠습니까?"를 물어야 하고, 가능하면 무엇이 달라졌는지 보여준다.

③ CRDT는 언제 필요한가

거의 필요 없다. 텍스트 문서와 달리 라벨 편집은 객체 단위로 잘 분리되고, 두 사람이 같은 박스를 동시에 옮기는 일 자체가 드물다. CRDT는 구현·디버깅·저장 비용이 모두 크므로, 정말로 실시간 협업이 요구사항일 때만 꺼낸다.

절충안으로 객체 단위 이면 대부분 충분하다 — 같은 프레임이라도 서로 다른 객체를 만지면 충돌하지 않는다.

워크플로 — 상태기계로 관리한다

는 "그리고 끝"이 아니라 파이프라인이다.

  todo ──▶ in_progress ──▶ submitted ──▶ in_review ──┬──▶ done
             ▲                                        │
             └──────────── rework ◀───────────────────┘
                          (반려)

각 전이에 규칙이 붙는다.

전이조건부수 효과
todo → in_progress할당됨잠금 획득, 시작 시각 기록
in_progress → submitted자동 검사 통과잠금 해제, 작업 시간 확정
submitted → in_review검수자 배정
in_review → rework반려 사유 필수원 작업자에게 알림
in_review → done승인라벨 스냅샷 고정

전이를 코드가 아니라 데이터로 두는 게 낫다. 상태와 허용 전이를 테이블로 정의하면, "검수 단계를 하나 더 넣자"가 배포 없이 가능해진다. 라벨링 프로세스는 조직마다 다르고 자주 바뀐다.

그리고 모든 전이를 이벤트로 기록한다. 이게 다음 절의 이자, 세 번째 글에서 다룰 계측의 원재료가 된다.

감사 로그 — 라벨 분쟁의 유일한 근거

"이 박스가 왜 이렇게 되어 있나"는 반드시 나오는 질문이다. 그리고 라벨 값만 저장하는 시스템은 이 질문에 답할 수 없다.

label_events
  event_id, log_id, t_ns, object_id
  actor        누가 (사람 id 또는 auto:v3)
  action       create | update | delete | class_change | merge | split
  before, after
  at
  session_id   어느 작업 세션에서

이게 있으면 답할 수 있는 것들:

  • 품질 문제가 특정 라벨러/특정 기간/특정 자동 라벨러 버전에 몰려 있는가
  • 검수에서 가장 자주 고쳐지는 클래스는 무엇인가 → 스펙이 모호한 지점
  • 어떤 자동 라벨을 사람이 얼마나 고쳤는가 → 자동 라벨러의 실질 정확도

마지막 항목이 특히 값지다. "사람이 얼마나 고쳤나"는 사람 손으로 만든 평가 없이도 얻는 품질 지표다.

개인정보

주행 영상에는 얼굴과 번호판이 찍힌다. 라벨링 도구는 그걸 사람에게 보여주는 시스템이므로, 접근 통제와 접근 기록이 선택이 아니다. 누가 어떤 로그를 열었는지가 남아야 한다. 데이터 플랫폼의 거버넌스 이야기가 여기서 구체화된다.

미디어를 어떻게 주는가

에디터가 프레임을 열 때 원본 MCAP을 그대로 내려받게 하면 안 된다. 수십 GB다.

사전 변환이 답이다. 라벨링 대상 프레임을 미리 잘라 에디터가 바로 쓸 수 있는 형태로 굽는다.

MCAP (원본, 수십 GB)
   └─ 라벨링 대상 키프레임만 추출
      ├─ 점군: 다운샘플 + 이진 포맷 (프레임당 수 MB)
      ├─ 이미지: JPEG 리사이즈 (프레임당 수백 KB)
      └─ 메타: 캘리브레이션 · 에고 포즈 · 사전 라벨

여기에 붙는 것들:

  • 서명 URL — 미디어는 에서 직접, 만료 시간을 둔 URL로. API 서버가 바이트를 중계하면 병목이 된다.
  • 프레임 인덱스 — 프레임 번호 → 파일 매핑을 미리 만들어 둔다. 스크럽이 곧 인덱스 조회가 된다.
  • 캐시 헤더 — 같은 프레임을 여러 번 열게 되므로 CDN·브라우저 캐시가 크게 효과를 낸다.

라벨 DB와 학습용 계층을 나눈다

마지막이 축 B와의 접합이다. 라벨 데이터에는 성격이 다른 두 용도가 있다.

라벨 DB (OLTP)학습·분석 계층 (OLAP)
쓰임에디터가 읽고 쓴다학습 세트 생성, 통계, 품질 분석
접근한 프레임씩, 자주, 작게수백만 행을 한 번에
포맷관계형 DB / Delta
갱신실시간배치 스냅샷

둘을 하나로 하려 하면 양쪽 다 나빠진다. 라벨 DB에 학습 쿼리를 돌리면 에디터가 느려지고, 컬럼형에 실시간 편집을 넣으면 구조가 안 맞는다.

그래서 주기적으로 스냅샷을 내보낸다.

라벨 DB ──(야간 배치)──▶ Parquet 스냅샷 (버전 태그) ──▶ 학습 세트 생성

스냅샷에는 반드시 버전을 붙인다. "모델 v7 = 라벨 스냅샷 2026-09-07 + 스펙 v3 + 포즈 refined-v2" 같은 앵커가 여기서 완성된다. 조인 키는 늘 같다 — (log_id, t_ns, sensor).

정리

  • 라벨은 객체 단위로 저장하되 프레임·트랙 양방향 조회를 지원한다.
  • 값만 저장하지 말고 스펙 버전·source·좌표계·포즈 종류를 함께 남긴다.
  • 스키마 변경은 반드시 온다. 마이그레이션은 되돌릴 수 있게, 버전 병존이라면 학습 세트 생성 시 반드시 필터링한다.
  • 저장은 부분 저장 + 키 + 로컬 우선. 두 시간치를 날리는 도구는 신뢰를 잃는다.
  • 동시성은 작업 단위 잠금이 대부분 답이고, 그 위에 을 얹는다. CRDT는 대개 필요 없다.
  • 워크플로는 상태기계로, 전이는 데이터로. 모든 전이를 이벤트로 남긴다.
  • 는 라벨 분쟁의 유일한 근거이자, "사람이 자동 라벨을 얼마나 고쳤나"라는 공짜 품질 지표다.
  • 미디어는 사전 변환 + 서명 URL로 준다.
  • 라벨 DB와 학습용 계층을 분리하고 버전 붙인 스냅샷으로 잇는다.

이어서 볼 글: 에디터 쪽은 ① 에디터, 자동 라벨과 로 루프를 닫는 이야기는 ③ 휴먼인더루프, 데이터 계층은 AV 데이터 플랫폼에서 다룬다.

Index

라벨 스키마 — 무엇을 저장하는가반드시 붙는 메타데이터스키마가 바뀔 때저장 API — 전체를 매번 보내지 않는다자동 저장의 현실적인 모양동시성 — 잠금이냐 병합이냐① 작업 단위 잠금 — 대부분 이걸로 충분하다② 낙관적 잠금 — 그래도 필요하다③ CRDT는 언제 필요한가워크플로 — 상태기계로 관리한다감사 로그 — 라벨 분쟁의 유일한 근거개인정보미디어를 어떻게 주는가라벨 DB와 학습용 계층을 나눈다정리