[자율주행] 컬럼형 저장과 레이크하우스 — 페타바이트를 질의 가능하게
Data Engineering/자율주행
· 2026-07-14
들어가며
세 편으로 이어온 자율주행 데이터 파이프라인 이야기의 마지막이다.
이번 글은 정렬까지 끝낸 데이터를 실제로 어디에, 어떻게 쌓는가에 대한 것이다. 1편에서 "시간순 행 구조를 버리고 컬럼형으로 다시 쌓는다"고 한 줄로 넘어갔던 바로 그 지점을, 이번엔 제대로 뜯어본다. 그리고 그 위에 올라가는 레이크하우스(Lakehouse) 라는 구조가 왜 자율주행처럼 페타바이트급·계속 늘어나는 데이터에 잘 맞는지, 마지막으로 그게 재현성과 어떻게 연결되는지까지 이어간다.
다시, 왜 컬럼형인가
행(row) 저장과 컬럼(columnar) 저장의 차이는 "무엇을 함께 붙여 두느냐"다.
1[ 행 저장 ] frame1{speed,yaw,lat,...} | frame2{...} | frame3{...} 2 → 한 프레임의 모든 필드가 붙어 있다 (기록·트랜잭션에 유리) 3 4[ 컬럼 저장 ] speed[frame1,frame2,frame3,...] | yaw[...] | lat[...] 5 → 같은 컬럼의 값이 붙어 있다 (분석·스캔에 유리)
수백만 프레임에서 "속도 ≥ 30이면서 조향각이 큰 프레임"을 찾을 때, 컬럼형은 speed·yaw 두 컬럼만 읽는다. 나머지 무거운 필드는 디스크에서 건드리지도 않는다. 이게 두 가지 최적화로 이어진다.
- 프로젝션 푸시다운(projection pushdown): 필요한 컬럼만 읽는다.
- 프레디케이트 푸시다운(predicate pushdown): 각 데이터 블록의 min/max 통계를 보고, 조건에 맞을 리 없는 블록은 아예 스킵한다.
자율주행 데이터의 대표 포맷인 Parquet은 내부적으로 이렇게 생겼다.
1Parquet 파일 2├─ Row Group (수십만 행 묶음) 3│ ├─ Column Chunk: speed (인코딩 + 압축 + min/max 통계) 4│ ├─ Column Chunk: yaw (인코딩 + 압축 + min/max 통계) 5│ └─ Column Chunk: ... 6├─ Row Group ... 7└─ Footer (스키마 + 각 청크 위치·통계)
컬럼마다 값의 성질에 맞는 인코딩(딕셔너리·런렝스·델타)이 붙고, 그 위에 압축이 들어간다. 같은 컬럼끼리 모아두니 값이 비슷해 압축률도 훨씬 좋다. "필요한 것만, 그마저도 스킵하며 읽는다" — 페타바이트에서 살아남는 방식이다.
자율주행 데이터의 이중성: 작은 표 + 거대한 덩어리
그런데 자율주행 데이터를 전부 Parquet에 때려 넣을 수는 없다. 데이터가 성질이 완전히 다른 두 부류로 나뉘기 때문이다.
- 작고 질의 대상인 것: 프레임의 스칼라 메타데이터 — 속도, 조향각, 날씨, 이벤트, 품질 지표. 표로 만들기 좋고 자주 필터링된다.
- 크고 통째로 읽는 것: 포인트클라우드 스캔, 카메라 이미지. 프레임당 수백 KB~수 MB. 필터의 대상이 아니라 "찾은 다음 통째로 가져오는" 것이다.
그래서 보통 이중 구조로 간다.
1[ 컬럼형 테이블 ] frame_id, timestamp, speed, weather, quality, ... 2 └─ lidar_uri → s3://.../000481.bin 3 └─ cam_front → s3://.../000481.jpg 4[ 오브젝트 스토리지 ] 무거운 blob 원본을 URI로 참조
메타데이터는 컬럼형 테이블에서 빠르게 질의하고, 무거운 blob은 오브젝트 스토리지에 두고 URI로 참조만 한다. 최근에는 Lance처럼 blob·벡터까지 컬럼형으로 다루면서 랜덤 액세스가 빠른 포맷도 쓴다 — 학습 때 특정 프레임만 콕 집어 읽는 패턴에 잘 맞는다. 어느 쪽이든 원칙은 같다. 가벼운 검색 층과 무거운 원본을 분리한다.
레이크 + 웨어하우스 = 레이크하우스
이제 이 Parquet 파일들을 그냥 S3에 쭉 올려두면 될까? 안 된다. "S3에 Parquet 뭉치"만으로는 데이터셋을 운영할 수 없다. 빠지는 게 너무 많다.
- 어제 들어온 드라이브를 추가하는데, 읽는 중인 학습 잡과 충돌 없이(ACID) 쓸 수 있나?
- 새 센서가 붙어 컬럼이 하나 늘면, 기존 데이터를 다 다시 쓰지 않고 스키마를 진화시킬 수 있나?
- "지난달에 학습한 그 버전의 데이터셋"으로 정확히 되돌아갈 수 있나?
- 작은 파일이 수백만 개로 쪼개졌을 때 어떻게 정리하나?
전통적으로 이건 데이터 웨어하우스의 몫이었고, 값싼 무한 저장은 데이터 레이크(오브젝트 스토리지)의 몫이었다. 레이크하우스는 이 둘을 합친다. 값싼 오브젝트 스토리지 위에, 웨어하우스가 주던 트랜잭션·스키마·거버넌스를 얹는 것이다. 그리고 그걸 가능하게 하는 게 테이블 포맷이다.
테이블 포맷이 얹는 것: Iceberg / Delta / Hudi
Apache Iceberg, Delta Lake, Apache Hudi 같은 테이블 포맷은, 실은 Parquet 파일들 위에 얹는 메타데이터 층이다. 데이터 자체는 여전히 Parquet이지만, 그 위에 "어떤 파일들이 지금 이 테이블을 구성하는가"를 기술하는 스냅샷·매니페스트를 둔다. 이 한 층이 다음을 준다.
- ACID 트랜잭션: 쓰기가 원자적이다. 읽는 쪽은 항상 일관된 스냅샷만 본다. 적재와 학습이 동시에 돌아도 안전하다.
- 스키마 진화: 컬럼 추가·삭제·타입 변경을 기존 파일 재작성 없이 처리한다. 센서·라벨 필드가 계속 느는 자율주행에 필수다.
- 숨은 파티셔닝(hidden partitioning): 사용자가 파티션 경로를 몰라도, 조건만 걸면 알아서 관련 파일만 스캔한다.
- 타임 트래블(time travel): 매 쓰기가 새 스냅샷을 만들어, 과거 특정 시점의 테이블을 그대로 조회할 수 있다.
이 마지막 항목이, 자율주행 데이터 엔지니어에게 특히 중요하다.
재현성은 스냅샷에서 나온다
1편에서 "같은 원본에서 같은 파생 데이터가 다시 나와야 한다"고 했고, 2편에서 "동기화가 검증된 프레임만 학습셋에 담는다"고 했다. 그런데 데이터셋은 살아있다. 매일 드라이브가 추가되고, 라벨이 보정되고, 품질 필터가 갱신된다. 그렇다면 "우리가 학습한 데이터셋"이란 대체 무엇인가?
레이크하우스의 답은 명확하다. 데이터셋 = 특정 스냅샷.
1-- 학습 시점의 테이블 스냅샷을 그대로 재현 (Iceberg 예시) 2SELECT scene_id, frame_id, lidar_uri 3FROM drive_frames 4FOR SYSTEM_VERSION AS OF 3821977011024 -- 그때 그 snapshot id 5WHERE weather = 'rain' 6 AND lidar_cam_offset_ms < 5 -- 2편에서 만든 동기화 품질 게이트 7 AND deskew_applied = true;
학습 실험에 스냅샷 ID를 함께 태깅해 두면, 몇 달 뒤에도 "그 모델이 본 정확히 그 데이터"를 되살릴 수 있다. 데이터가 계속 바뀌어도 실험은 고정된다. 재현성이 파이프라인 문서가 아니라 저장 계층의 기능으로 보장되는 것이다. 이게 레이크하우스를 쓰는 가장 큰 이유 중 하나다.
파티셔닝과 파일 레이아웃
페타바이트에서 질의가 빠르려면, 파일이 잘 잘려 있어야 한다.
- 파티셔닝: 보통 수집 날짜·드라이브·지역으로 나눈다. "지난주 서울 주행"을 질의하면 그 파티션만 스캔하고 나머지는 건드리지 않는다(파티션 프루닝).
- 작은 파일 문제(small files problem): 실시간 적재는 자잘한 파일을 양산한다. 파일이 수백만 개가 되면 메타데이터 오버헤드로 질의가 느려진다.
- 컴팩션(compaction): 그래서 주기적으로 작은 파일을 적당한 크기(수백 MB)로 합친다. 테이블 포맷이 이걸 트랜잭션으로 안전하게 처리해준다.
즉, 좋은 레이아웃은 저절로 생기지 않는다. 파티션 전략을 정하고, 컴팩션을 돌리는 것까지가 저장 계층의 일이다.
포인트클라우드는 어디에 두나
앞의 이중 구조로 돌아오면, 포인트클라우드는 크게 두 갈래로 둔다.
- blob + 인덱스: 스캔을 오브젝트 스토리지에 두고, Parquet 테이블에
uri와 공간·시간 인덱스만 둔다. 단순하고 어떤 엔진에서도 읽힌다. - ML 네이티브 포맷(Lance 등): 포인트클라우드·임베딩을 컬럼형으로 두면서 특정 프레임 랜덤 액세스가 빠르다. 버전 관리도 내장이라 학습 데이터로더에 잘 맞는다.
여기에 장면 임베딩을 벡터 컬럼으로 함께 두면, 1편에서 말한 "이 장면과 비슷한 상황 찾기"가 같은 테이블 안에서 유사도 검색으로 이어진다. 규칙 태그로 안 잡히는 롱테일 시나리오를 발굴하는 통로다.
데이터 엔지니어의 몫: 테이블을 '운영'하는 일
정리하면, 저장 계층에서 데이터 엔지니어의 일은 "한 번 쌓기"가 아니라 "계속 운영하기" 다.
- 컴팩션: 작은 파일을 주기적으로 합쳐 질의 성능을 유지한다.
- 스냅샷 만료·오펀 정리: 타임 트래블용 스냅샷을 무한정 두면 스토리지가 샌다. 보존 정책을 정하고, 참조 잃은 파일을 청소한다.
- 파티션·클러스터링 튜닝: 실제 질의 패턴을 보고 파티션 키를 조정한다.
- 거버넌스·비용: 누가 무엇을 읽는지, 어떤 테이블이 비용을 먹는지 관리한다.
이 운영이 무너지면, 아무리 잘 설계한 스키마도 몇 달 뒤엔 느리고 비싼 테이블이 된다. 저장은 만드는 순간이 아니라 유지되는 동안 가치를 낸다.
마치며 — 세 편을 닫으며
세 편을 한 문장으로 잇는다. 원천 로그를 프레임으로 정규화하고(1편), 그 프레임의 시간을 맞추고(2편), 정렬된 데이터를 컬럼형 레이크하우스에 쌓아 질의·재현 가능하게 만든다(3편). 이 세 단계를 지나야, 랜딩 페이지에서 돌아가는 그 라이다 뷰어가 "예쁜 3D"가 아니라 "질의 가능한 데이터의 마지막 표현"이 된다.
프론트엔드에서 자율주행 데이터 엔지니어링으로 방향을 옮기며 가장 크게 바뀐 질문은, 결국 "데이터를 어떻게 보여줄까"에서 "데이터를 어떻게 질의 가능하고 재현 가능하게 만들까"로의 이동이었다. 이 시리즈는 그 질문에 대한 첫 세 개의 답이다. 다음엔 이 위에서 실제로 돌아가는 것들 — 장면 검색, 데이터 품질 대시보드, 학습 데이터로더 — 을 하나씩 더 구체적으로 다뤄보려 한다.