[자율주행] 주행 로그는 어떻게 '질의 가능한 데이터'가 되는가
Data Engineering/자율주행
· 2026-07-14
들어가며
이 블로그의 글은 그동안 대부분 프론트엔드와 데브옵스에 관한 것이었다. 렌더링, 브라우저, CI/CD, 쿠버네티스 같은. 그런데 방향을 자율주행 데이터 엔지니어링으로 옮기면서, 정작 그 이야기는 한 글자도 남겨두지 않았다는 걸 깨달았다. 랜딩 페이지에는 라이다 포인트클라우드가 실시간으로 돌아가는데, 정작 그 안에서 무슨 일이 벌어지는지는 적어두지 않은 셈이다.
그래서 이 글은 그 껍데기 안에 있는 이야기다. 자율주행 차량이 쏟아내는 주행 로그가, 어떻게 사람이 질의하고 모델이 학습할 수 있는 '데이터'로 바뀌는가. 필자가 데이터 엔지니어로서 이 문제를 어떻게 바라보게 되었는지에 대한 정리이자, 앞으로 이어질 글들의 시작점이기도 하다.
주행 로그, 그 자체로는 데이터가 아니다
먼저 오해를 하나 풀고 가자. "자율주행은 데이터가 넘친다"는 말은 절반만 맞다. 넘치는 건 로그(log)지, 곧바로 쓸 수 있는 데이터(data)가 아니다.
자율주행 차량 한 대가 한 시간을 달리면, 대략 이런 것들이 동시에 기록된다.
- 라이다(LiDAR): 초당 10~20회 회전하며 만들어내는 3D 포인트클라우드. 한 프레임에 수십만 개의 점.
- 카메라: 여러 대가 각각 30fps 안팎으로 찍어내는 이미지 스트림.
- 레이더(Radar): 물체의 거리와 상대속도.
- CAN / 자차 상태: 속도, 조향각, 가속도 같은 차량 신호.
- GNSS / IMU: 위치와 자세.
이걸 다 합치면 차량 한 대에서 한 시간당 수 TB가 쌓인다. 그리고 이 기록은 보통 ROS bag이나 MCAP 같은, 시간 순서로 메시지를 이어붙인 컨테이너 포맷으로 저장된다. 문제는 여기서 시작된다.
왜 로그를 그대로 쓸 수 없는가
로그 포맷은 "기록"에 최적화되어 있지, "질의"에 최적화되어 있지 않다. 이게 핵심이다.
- 시간 순서(row-oriented)로 흘러간다. "3번 카메라 이미지만 전부 뽑아줘" 같은 요청을 하려면, 결국 파일을 통째로 순회해야 한다. 컬럼 하나만 읽는 게 불가능하다.
- 멀티모달인데 시간이 안 맞는다. 라이다 프레임과 카메라 프레임은 서로 다른 주기로 찍힌다. "이 라이다 스캔 순간에 대응하는 앞 카메라 이미지"를 얻으려면 타임스탬프를 기준으로 다시 정렬하고 보간해야 한다.
- 너무 크고, 흩어져 있다. 수천 대·수만 시간의 로그가 오브젝트 스토리지에 흩어져 있다. "비 오는 밤에 좌회전하는 장면"을 찾으려면? 로그를 열어보기 전엔 그 안에 뭐가 들었는지조차 알 수 없다.
즉, 로그는 아카이브일 뿐이다. 데이터 엔지니어의 일은 이 아카이브를 질의 가능한 자산으로 바꾸는 것이다.
로그를 '질의 가능한 데이터'로 바꾸는 파이프라인
그래서 파이프라인이 필요하다. 큰 그림은 이렇게 생겼다.
1[ 원천 주행 로그 ] (ROS bag / MCAP, 시간순, 수 TB) 2 ↓ ① 수집(Ingest) 3[ 오브젝트 스토리지 ] (원본 그대로 보존 — 재현성의 기준점) 4 ↓ ② 디코드 & 시간 동기화 5[ 프레임 단위 레코드 ] (라이다·카메라·자차상태를 하나의 '순간'으로 정렬) 6 ↓ ③ 변환 & 정규화 7[ 컬럼형 테이블 ] (Parquet / Iceberg / Lance — 컬럼만 읽는 저장) 8 ↓ ④ 카탈로그 & 인덱싱 9[ 메타데이터 카탈로그 ] (장면·조건·품질을 기술하는 검색 가능한 층) 10 ↓ ⑤ 질의 / 탐색 11[ 학습 · 디버깅 · 시각화 ] (사람과 모델이 실제로 쓰는 지점)
각 단계를 하나씩 풀어보자.
① 수집과 원본 보존
가장 먼저 할 일은, 역설적이게도 아무것도 바꾸지 않는 것이다. 원천 로그는 있는 그대로 오브젝트 스토리지에 적재해 둔다. 이게 나중에 모든 파생 데이터의 재현성 기준점(single source of truth)이 되기 때문이다. 파이프라인 로직이 바뀌어 데이터를 다시 만들어야 할 때, 되돌아갈 원본이 없으면 그 데이터는 신뢰할 수 없게 된다.
② 디코드와 시간 동기화
여기가 가장 자율주행다운 단계다. MCAP 안에 들어 있는 각 센서 메시지를 디코드한 뒤, 타임스탬프를 기준으로 하나의 '프레임'(frame)으로 묶는다. 하나의 프레임은 대략 이런 스키마를 가진다.
1{ 2 "frame_id": 481, 3 "timestamp": 1718332800.050, 4 "ego": { "speed": 12.4, "yaw_rate": -0.03, "accel": 0.8 }, 5 "lidar": { "uri": "s3://.../scene-0061/lidar/000481.bin", "points": 118500 }, 6 "cameras": { 7 "front": "s3://.../cam_front/000481.jpg", 8 "left": "s3://.../cam_left/000481.jpg" 9 }, 10 "pose": { "lat": 37.5013, "lon": 127.0396, "heading": 88.2 } 11}
핵심은 서로 다른 주기로 찍힌 센서들을 같은 시간 축 위에 올려놓는 것이다. 라이다 스캔 하나를 기준으로 삼고, 그 순간에 가장 가까운 카메라·자차 상태를 붙인다. 이 정렬이 어긋나면, 그 위에서 학습한 모델의 인지 결과도 통째로 어긋난다. 데이터 엔지니어가 여기서 실수하면 그 비용은 파이프라인 끝까지 전파된다.
③ 변환과 컬럼형 저장
프레임 단위로 정규화한 뒤에는, 시간순 행(row) 구조를 버리고 컬럼형(columnar) 저장으로 다시 쌓는다. Parquet을 테이블 포맷인 Iceberg로 관리하거나, 포인트클라우드·임베딩처럼 무거운 벡터는 Lance 같은 포맷을 쓰기도 한다.
컬럼형이 중요한 이유는 CDN 글에서 캐시 이야기를 했던 것과 결이 같다. "필요한 것만 읽는다." "속도가 30km/h 이상이면서 조향각이 큰 프레임"을 찾을 때, 컬럼형이라면 speed·yaw 컬럼만 스캔하면 된다. 수 TB짜리 원본을 다 열 필요가 없다. 질의 비용이 몇 자릿수씩 줄어든다.
④ 카탈로그와 인덱싱 — 로그를 '검색 가능'하게
이제 마지막 재료다. 프레임 하나하나에 그 프레임이 어떤 상황인지를 기술하는 메타데이터를 붙인다.
- 시간·날씨·조도 (낮/밤, 맑음/비/눈)
- 도로 유형 (교차로, 고속도로, 주차장)
- 이벤트 (급제동, 좌회전, 보행자 근접)
- 데이터 품질 (센서 드롭, 캘리브레이션 오차, 결측 프레임)
이 카탈로그가 있어야 비로소 앞에서 말한 "비 오는 밤에 좌회전하는 장면을 찾아줘" 가 SQL 한 줄이 된다.
1SELECT scene_id, frame_id 2FROM drive_frames 3WHERE weather = 'rain' 4 AND time_of_day = 'night' 5 AND maneuver = 'left_turn' 6 AND data_quality = 'ok' 7ORDER BY timestamp;
로그를 통째로 열어봐야만 알 수 있던 것이, 인덱스 조회로 바뀌는 순간이다. 여기서부터 데이터는 진짜 '데이터'가 된다.
포인트클라우드는 어떻게 다루나
표 형태의 신호는 그렇다 치고, 랜딩 페이지에서 돌아가는 그 포인트클라우드는 어떻게 저장하고 질의할까? 이미지나 스칼라 신호와는 성격이 완전히 다르다.
- 프레임 단위로 나눠 저장한다. 한 스캔(수십만 점)을 하나의 객체로 두고, 카탈로그의
frame_id로 참조한다. 굳이 전체 스캔을 하나의 거대한 파일로 묶지 않는다 — 필요한 프레임만 끊어 읽기 위해서다. - 공간·시간 인덱스를 붙인다. 점 하나하나를 질의하기보다, "이 위치·이 시간 근처의 스캔"을 빠르게 좁히는 인덱스를 얹는다.
- 장면 자체를 검색하고 싶다면 임베딩을 쓴다. 최근에는 스캔이나 장면을 벡터로 임베딩해 두고, "이 장면과 비슷한 상황"을 유사도로 찾는 방식도 늘고 있다. 규칙 기반 태그로는 잡히지 않는 롱테일 상황(희귀 시나리오)을 발굴하는 데 유용하다.
결국 포인트클라우드도 원리는 같다. 무거운 원본은 따로 두고, 그 위에 가벼운 검색 층을 올린다.
그리고, 사람이 데이터를 탐색하는 도구
여기까지가 데이터를 만드는 이야기라면, 필자가 특히 관심 있는 건 그다음이다. 만들어진 데이터를 사람이 실제로 탐색하는 도구를 짓는 것.
아무리 잘 정제된 데이터라도, 팀의 엔지니어가 s3:// 경로를 손으로 뒤지고 있다면 그 데이터는 절반만 살아있는 것이다. 그래서 데이터 위에는 결국 이런 것들이 올라간다.
- 장면 검색 포털: 위의 SQL을 UI로 감싼, 조건으로 주행 장면을 찾는 도구
- 프레임 뷰어: 라이다·카메라·인지 결과를 겹쳐 보며 한 프레임을 뜯어보는 시각화 (랜딩의 그 뷰어가 바로 이 계열이다)
- 데이터 품질 대시보드: 센서 드롭, 결측, 캘리브레이션 이상을 한눈에 보는 지표
랜딩 페이지의 실시간 라이다 뷰는 장식이 아니라, 이 마지막 단계 — "사람이 데이터를 들여다보는 도구" — 를 압축해 보여주려는 것이었다. 파이프라인의 끝은 결국 사람이 데이터를 이해하는 순간이다.
데이터 엔지니어가 끝까지 지켜야 하는 것
마지막으로, 화려한 파이프라인 그림 뒤에서 실제로 품질을 결정하는 것들을 짚어둔다. 이 축들이 무너지면 위의 모든 게 모래 위의 성이 된다.
- 재현성: 같은 원본에서 같은 파생 데이터가 다시 나와야 한다. 파이프라인 버전과 데이터를 함께 태깅해 둔다.
- 계보(lineage): 이 프레임이 어느 로그의, 어느 파이프라인 버전에서 나왔는지 추적 가능해야 한다.
- 시간 동기화의 정확성: 앞서 말했듯, 정렬이 어긋난 데이터는 조용히 틀린 답을 낸다. 가장 눈에 안 띄면서 가장 치명적이다.
- 비용: TB 단위를 다루는 이상, "필요한 것만 읽는" 구조가 곧 운영 가능성이다. 컬럼형·인덱스가 최적화가 아니라 생존 조건인 이유다.
마치며
처음의 문장으로 돌아가자. 자율주행은 로그가 넘치지, 데이터가 넘치는 게 아니다. 그 사이의 간극을 메우는 일 — 시간순 아카이브를 프레임으로 정규화하고, 컬럼형으로 다시 쌓고, 검색 가능한 카탈로그를 붙이고, 사람이 들여다볼 도구까지 올리는 일 — 이 데이터 엔지니어의 자리다.
프론트엔드에서 이쪽으로 방향을 옮기며 가장 크게 바뀐 관점은, "데이터를 어떻게 보여줄까"에서 "데이터를 어떻게 질의 가능하게 만들까"로 질문이 바뀌었다는 점이다. 랜딩의 라이다 뷰어가 껍데기라면, 이 글은 그 안의 첫 알맹이인 셈이다. 다음 글에서는 이 파이프라인의 한 단계 — 아마 시간 동기화나 컬럼형 저장 — 를 더 깊이 파보려 한다.