ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 데이터 저장 타입(포맷)
    공부/데이터 2025. 6. 3. 18:17

    개요

    데이터 저장 타입은 데이터를 어떻게 구조화하고 디스크나 메모리에 저장할지를 정의하는 방식을 의미합니다. 어떤 타입을 선택하느냐에 따라 데이터 처리 성능, 저장 공간 효율성, 데이터 모델링의 유연성, 시스템 간 호환성 등이 크게 달라질 수 있습니다.

    다음은 파일 포맷에 어떤 종류가 있는지를 설명합니다. 파일 포맷은 데이터가 파일 시스템(로컬 디스크, HDFS, 클라우드 스토리지 등)에 실제로 저장되는 구체적인 방식을 의미합니다.

    행 기반 포맷 (Row-based)

    데이터를 행 단위로 묶어서 저장합니다.

    csv / tsv

    가장 간단한 텍스트 기반 포맷으로 쉼표나 탭으로 각 필드를 구분합니다. 사람이 읽기 쉽고 대부분의 시스템에서 지원하지만 스키마 정보가 없어 데이터 타입 유추가 필요하고 압축률이 낮습니다. 또한 특정 컬럼만 읽는 분석 쿼리에선 비효율적입니다.

    따라서 해당 포맷은 간단한 데이터 교환이나 초기 데이터 로딩, 로그 파일에서 사용하는 것이 적합합니다.

    # csv
    id,name,age,city
    1,김철수,30,서울

    json / jsonL (줄바꿈으로 구분된 json)

    키-값 쌍으로 이루어진 텍스트 기반 포맷으로 중첩 구조 표현에 용이합니다. JSON Lines는 각 줄이 하나의 유효한 JSON 객체입니다. 사람이 읽기 쉽고 반정형 및 중첩 데이터 표현에 유연하며 웹 API에서 널리 사용되고 있습니다. 단점으론 CSV보다 파일 크기가 크고 파싱 속도가 느릴 수 있어서 분석 쿼리에는 비효율적입니다.

    따라서 해당 포맷은 웹 API 응답, 로그 데이터, 설정 파일, 반정형 데이터 저장에 사용하는 것이 적합합니다.

    # jsonL
    {"id": 1, "name": "박민준", "age": 28, "city": "인천"}
    {"id": 2, "name": "최지아", "age": 32, "city": "대구"}

    Avro

    스키마 정보를 JSON 형태로 명시적으로 정의하고 데이터는 간결한 바이너리 형태로 직렬화하여 저장하는 행 기반 포맷입니다. 강력한 스키마 진화 지원 (스키마 변경 시 이전 데이터 호환성 유지 용이), 빠른 직렬화/역직렬화 속도, 비교적 작은 파일 크기가 장점이지만 컬럼 기반 포맷 대비 분석 쿼리 성능은 떨어집니다.

    따라서 해당 포맷은 Kafka와 같은 메시징 시스템, 데이터 직렬화, 스키마 변경이 잦은 데이터 저장에 사용하는 것이 적합합니다.

    스키마 진화와 처리 방식

    데이터의 구조(스키마)는 비즈니스 요구사항 변경, 기능 추가 등으로 인해 필드가 변경되는 변화를 겪는데 이를 스키마 진화라고 합니다. Avro는 데이터를 읽을 때 Writer's Schema와 Reader's Schema를 모두 사용합니다.

    • Writer's Schema: 데이터가 생성(직렬화)될 때 사용된 스키마. Avro 데이터 파일에는 보통 Writer's Schema가 함께 저장
      • 보통 .avro 파일 안의 헤더 부분에 스키마 정보가 json 형태로 저장
    • Reader's Schema: 데이터를 읽을(역직렬화할) 때 애플리케이션이 기대하는 스키마
      • .avro 파일을 읽고자 하는 애플리케이션에 저장

    Avro는 데이터를 읽을 때 저장된 데이터의 Writer's Schema와 애플리케이션의 Reader's Schema를 비교하여 두 스키마 간의 차이점을 정의된 규칙에 따라 자동으로 해결하며 이 과정을 통해 데이터 호환성을 유지합니다.

    스키마 변경 유형별 Avro의 처리 방식(호환성 규칙)은 다음과 같습니다.

    1. 필드 추가
      • Reader's Schema에 새 필드가 있고 Writer's Schema에는 없는 경우 (이전 데이터 읽기 - Backward Compatibility)
        • 새로 추가된 필드에 기본값이 Reader's Schema에 정의되어 있다면 해당 필드에 기본값을 채움
        • 기본값이 없다면 오류가 발생할 수 있지만 보통 nullable 필드 등은 null로 처리
      • Writer's Schema에 새 필드가 있고 Reader's Schema에는 없는 경우 (새 데이터를 이전 버전 애플리케이션에서 읽기 - Forward Compatibility)
        • Avro는 Reader's Schema에 없는 해당 필드를 단순 무시
    2. 필드 삭제
      • Reader's Schema에서 필드가 삭제되고 Writer's Schema에는 있는 경우
        • Writer's Schema에는 있는 해당 필드를 무시
      • Writer's Schema에서 필드가 삭제되고 Reader's Schema에는 남아있는 경우
        • Writer's Schema의 해당 필드에 기본값이 정의되어 있으면 이 기본값을 사용하지만 기본값이 없으면 오류 발생
    3. 필드 이름 변경
      • 필드 이름을 직접 변경하는 것은 호환성을 깨뜨릴 수 있습니다. Avro는 이를 위해 필드에 별칭(alias)을 지정하는 기능을 제공합니다. Reader's Schema의 필드에 이전 필드 이름을 별칭으로 등록해두면 Writer's Schema에 이전 이름으로 기록된 데이터를 새로운 필드 이름으로 매핑하여 읽을 수 있습니다.
    4. 필드 타입 변경
      • Avro는 특정 타입 간의 타입 승격(type promotion)을 지원
        • int -> long -> float -> double
        • string -> bytes
        • bytes -> string
      • string -> int 처럼 호환되지 않는 타입 변경은 오류 발생

    즉, Avro는 파일 작성자와 해당 파일을 읽는 조회자가 각각 파일의 스키마를 인지하고 관리해야 합니다.

    열 기반 포맷 (Columnar)

    데이터를 열(Column) 단위로 묶어서 저장합니다.

    Parquet

    Hadoop 생태계에서 가장 널리 사용되는 컬럼 기반 포맷으로 데이터는 바이너리 형태이며 분석 쿼리(특히 특정 컬럼만 선택하는 경우) 성능이 매우 우수합니다. 또한 높은 압축률로 스토리지 절약에 뛰어나며 스키마 정보 내장 및 스키마 진화를 지원하고 중첩 데이터 구조를 지원합니다. 그러나 행 전체를 자주 읽거나 업데이트하는 OLTP성 작업에는 부적합합니다.

    따라서 해당 포맷은 데이터 웨어하우스나 데이터 레이크의 분석용 데이터 저장, Spark/Trino/Hive 등과의 연동(Delta Lake, Iceberg, Hudi의 기본 파일 포맷)에서 사용하는 것이 적합합니다.

    압축 및 인코딩 방식

    같은 컬럼에는 동일한 유형의 데이터가 모여 있기 때문에 압축 효율이 매우 높습니다.

    압축 코덱 (Compression Codecs)

    • Snappy: 압축률은 보통이지만 압축/해제 속도가 매우 빠릅니다. CPU 사용량이 적어 속도가 중요할 때 주로 사용됩니다. (기본값으로 많이 사용됨)
    • Gzip: 압축률은 높지만 Snappy보다 압축/해제 속도가 느립니다. 저장 공간을 최대한 줄이고 싶을 때 유용합니다.
    • LZO: Snappy와 Gzip의 중간 정도 성능을 보입니다.
    • Brotli: Gzip보다 압축률이 더 좋지만, 압축 속도가 느립니다.
    • ZSTD (Zstandard): Gzip보다 압축률이 좋으면서 압축/해제 속도도 준수하여 최근 많이 사용됩니다.
    • LZ4: Snappy와 유사하게 빠른 속도에 중점을 둔 코덱입니다.
    • 압축 안 함 (UNCOMPRESSED): 압축을 사용하지 않을 수도 있습니다.

    인코딩 방식 (Encoding Schemes)

    압축 전에 데이터의 표현 방식을 바꿔서 압축 효율을 더 높이거나 데이터 크기를 줄입니다. 컬럼의 데이터 특성에 따라 다른 인코딩 방식이 적용될 수 있습니다.

    • Dictionary Encoding (사전 인코딩): 컬럼 내에 중복되는 값이 많을 때 (예: '국가' 컬럼에 '한국', '미국', '중국' 등이 반복될 때) 고유값 사전을 만들고, 실제 데이터는 이 사전의 인덱스로 대체하여 저장합니다. 문자열 데이터에 특히 효과적입니다.
    • Run Length Encoding (RLE, 실행 길이 인코딩): 동일한 값이 연속적으로 나타날 때, 그 값과 반복 횟수만 저장합니다. (예: AAAAABBB -> A,5,B,3) 비트 패킹(Bit-packing)과 함께 사용되어 정수 저장 효율을 높입니다.
    • Delta Encoding (델타 인코딩): 주로 정수 시계열 데이터에서 이전 값과의 차이(델타)만 저장하여 수치 범위를 줄입니다.
    • Bit-Packing (비트 패킹): 정수 데이터를 표현하는 데 필요한 최소한의 비트만 사용합니다. RLE와 결합되어 많이 사용됩니다.
    • Plain Encoding: 특별한 인코딩 없이 값을 그대로 저장합니다. (예: 부동소수점 수)

    압축과 인코딩은 페이지(Page) 레벨에서 컬럼 청크(Column Chunk) 내의 데이터에 적용됩니다. 이를 통해 파일 크기를 크게 줄이고, I/O를 감소시켜 쿼리 성능을 향상시킵니다.

    스키마 정보 저장 방식

    스키마 정보는 파일 내에 저장되지만, Avro처럼 주로 파일 헤더에만 있는 것은 아니고 파일의 Footer 부분에 메타데이터로 저장됩니다.

    • 파일 구조
      1. Magic Number (헤더): 파일 시작 부분에 "PAR1"이라는 4바이트 문자열로 Parquet 파일임을 나타냄
      2. Row Group: 여러 행의 데이터를 묶은 단위로 각 Row Group은 여러 컬럼의 데이터를 포함
        • Column Chunk: 특정 컬럼의 데이터를 Row Group 단위로 모아 놓음. 각 Column Chunk는 하나 이상의 페이지로 구성
        • Page: 실제 데이터와 해당 페이지의 메타데이터(인코딩 정보 등)가 저장되는 가장 작은 단위. 여기서 압축과 인코딩이 이루어짐
      3. File Footer: 파일의 가장 마지막 부분에 위치하며 매우 중요한 메타데이터를 포함
        • File Schema: 전체 파일의 데이터 구조(컬럼명, 데이터 타입, 중첩 구조 등)를 정의
        • Row Group 메타데이터: 각 Row Group의 위치, 컬럼 청크 정보 등을 담고 있음
        • Column 메타데이터: 각 컬럼 청크의 인코딩, 압축 방식, 통계 정보(최소/최대값, null 개수 등) 등을 포함
        • 파일 포맷 버전 정보 등

    쿼리 엔진은 먼저 파일의 꼬리말을 읽어 스키마와 필요한 컬럼의 위치 정보를 파악한 후, 해당 컬럼 청크만 디스크에서 읽어옵니다. 이를 통해 불필요한 데이터 스캔을 피하고 쿼리 성능을 높입니다.

    스키마 진화와 처리 방식

    Avro와 유사하지만 해당 파일을 읽는 관점에서 별도의 스키마를 관리하는 것보단 파일의 스키마를 수용하는 측면이 더 강합니다.

    ORC (Optimized Row Columnar)

    Parquet과 유사한 컬럼 기반 포맷으로 데이터는 바이너리 형태이며 주로 Hive 환경에서 최적화된 성능을 보이기 위해 개발되었습니다. Parquet과 유사한 분석 성능 및 압축률을 보이며 Hive 트랜잭션 테이블과 잘 통합이 됩니다. 스트라이프 및 인덱스 기능으로 데이터 스키핑 효율이 우수합니다. 그러나 Parquet 대비 생태계 지원이 약간 덜 광범위할 수 있습니다.

    따라서 해당 포맷은 Hive 기반 데이터 웨어하우스, Parquet의 대안으로 사용할 수 있습니다.

    특정 상황(Hive 환경)에선 ORC가 Parquet보다 약간 더 나은 압축률을 보이는다고 합니다. 하지만 Parquet도 우수한 압축률을 보여주며 주로 어느 환경에서 사용하는지, 생태계 성숙도 등으로 결정하는 것이 좋습니다.

    Capacitor

    Google BigQuery가 내부적으로 데이터를 저장하고 관리하기 위해 사용하는 독점적인 컬럼 기반 스토리지 포맷입니다. Capacitor는 사용자가 직접 파일 형태로 다루거나 선택할 수 있는 포맷이 아닙니다. 사용자가 BigQuery에 데이터를 로드하면 BigQuery는 이 데이터를 내부적으로 Capacitor라는 고도로 최적화된 컬럼 기반 포맷으로 변환하여 자체 관리형 스토리지에 저장합니다. 이 포맷은 BigQuery의 분산 쿼리 엔진인 Dremel과 함께 작동하여 매우 빠른 분석 쿼리 성능을 내도록 특별히 설계되었습니다.

    빅쿼리를 사용한다면 parquet보다 capacitor의 압축률이 더 좋다고 하므로 빅쿼리 한정으로는 별도의 파일을 사용하는 것보단 빅쿼리 자체에 데이터를 로드하는 것이 더 좋습니다.

    인메모리 컬럼 기반 포맷

    인메모리 컬럼 기반 포맷은 데이터를 메모리에 저장할 때, 전통적인 행 단위가 아닌 열 단위로 묶어서 저장하고 처리하는 방식을 말합니다. 이 방식은 데이터 분석 워크로드에서 매우 높은 성능을 제공하기 때문에 현대 데이터 처리 시스템에서 각광받고 있습니다.

    Arrow

    디스크 저장 포맷이라기보다는 메모리 내(In-memory) 컬럼 기반 데이터 처리를 위한 표준 플랫폼이자 포맷입니다. 즉, Arrow는 직렬화/역직렬화 단계를 최소화하고, 공유 메모리에서 컬럼 기반 데이터에 직접 접근하여 분석 성능을 극대화하는 데 특화되어 있습니다. 장점으로는 매우 빠른 데이터 처리 속도, 서로 다른 시스템/언어 간 데이터 복사 없이 효율적인 데이터 공유(Zero-copy)가 가능하지만 장기 저장을 위한 디스크 포맷이 주 목적은 아닙니다. (Arrow IPC 파일 포맷으로 저장 가능)

    따라서 Pandas와 Spark 간 데이터 변환, 분산 시스템 간 데이터 전송, 고성능 데이터 분석 라이브러리 내부 포맷에서 주로 사용합니다.

    데이터 직렬화 포맷

    데이터를 직렬화(Serialization)하고 서로 다른 프로그래밍 언어로 개발된 시스템 간에 데이터를 교환하거나 원격 프로시저 호출(RPC)을 구현하는 데 널리 사용되는 포맷입니다.

    Protobuf (Protocol Buffers)

    Thrift와 유사하게 .proto 파일에 데이터 구조를 정의하고 Protobuf 컴파일러(protoc)를 사용하여 다양한 언어(Java, C++, Python, Go, C#, JavaScript, Ruby, Objective-C 등)에 대한 코드를 자동으로 생성합니다. gRPC(Google Remote Procedure Call) 프레임워크의 기본 데이터 직렬화 포맷으로 널리 사용됩니다.

    proto 파일에 메시지(데이터 구조)를 정의하며 정의된 스키마로부터 각 언어에 맞는 데이터 접근 클래스 및 직렬화/역직렬화 코드를 생성합니다. XML이나 JSON보다 데이터 크기가 작고 파싱 속도가 빠르며 필드 번호를 사용하여 하위 호환성 및 상위 호환성을 유지하면서 스키마를 변경(필드 추가/삭제 등)하기 용이하고 .proto 파일의 문법이 비교적 단순하고 명확합니다.

    장점으론 데이터 크기가 작고 직렬화/역직렬화 속도가 매우 빠르며 명확한 스키마 정의와 강력한 스키마 진화 지원으로 데이터 구조 변경에 유연하게 대응할 수 있습니다. 또한 Google 내부에서 광범위하게 사용되며 gRPC의 기본 포맷으로 채택되어 커뮤니티가 매우 크고 활발하고 문서화도 잘 되어 있습니다. 마지막으로 명확한 타입과 필드를 가진 구조화된 데이터를 다루는 데 매우 효과적입니다. 단점으론 바이너리 포맷이므로 사람이 직접 데이터를 읽거나 수정하기 어렵습니다. Protobuf 자체는 데이터 직렬화에 중점을 둡니다. RPC 구현을 위해서는 gRPC와 같은 별도의 프레임워크와 함께 사용되는 경우가 일반적입니다. (Thrift는 RPC 기능까지 포함)스키마를 미리 정의해야 하므로 스키마가 매우 유동적이거나 비정형적인 데이터에는 적합하지 않을 수 있습니다.

    Thrift

    Thrift를 사용하면 .thrift 파일에 데이터 타입과 서비스 인터페이스를 정의하고 Thrift 컴파일러를 사용하여 다양한 프로그래밍 언어(Java, C++, Python, Go, JavaScript, PHP 등)에 대한 코드를 자동으로 생성할 수 있습니다.

    thrift 파일에 데이터 구조(struct, enum, list, map 등)와 서비스 인터페이스(함수 정의)를 명확하게 정의할 수 있고 정의된 스키마로부터 각 언어에 맞는 데이터 객체(DTO) 및 RPC 클라이언트/서버 스텁(Stub) 코드를 자동으로 생성을 해줍니다. 또한 다양한 프로토콜(데이터 인코딩 방식) 및 전송 계층 (데이터 전송 방식)을 지원하며 필드 ID를 사용하여 스키마 변경(필드 추가/삭제) 시 하위 호환성을 유지하는 데 도움이 됩니다.

    장점으론 바이너리 프로토콜 사용 시 JSON이나 XML보다 데이터 크기가 작고 직렬화/역직렬화 속도가 빠르고 단순 데이터 직렬화를 넘어 서비스 인터페이스 정의 및 RPC 구현을 위한 완벽한 프레임워크를 제공합니다. 또한 오랫동안 사용되어 왔으며 대규모 시스템에서 검증되었습니다. 단점으론 Protobuf에 비해 프로토콜 및 전송 계층 옵션이 많아 처음 배울 때 다소 복잡하게 느껴질 수 있고 바이너리 프로토콜 사용 시 사람이 직접 데이터를 읽기 어렵습니다.

    YAML

    YAML은 설정 정보나 상대적으로 작은 규모의 구조화된 데이터를 표현하고 교환하기 위한 직렬화 포맷에 가깝습니다.

    HTML이나 XML처럼 복잡한 마크업 태그를 사용하는 대신 들여쓰기를 사용하여 데이터의 계층 구조를 표현하는 것이 특징입니다. 키-값 쌍 (맵/딕셔너리), 리스트(배열/시퀀스), 스칼라 값(문자열, 숫자, 불리언)을 사용하여 다양한 데이터 구조를 표현할 수 있습니다. 또한 주석(#)을 지원하여 설정 파일 등에 설명을 추가하기 용이하며 대부분의 경우, 유효한 JSON 문서는 유효한 YAML 문서이기도 합니다.

    장점으론 설정 파일(Configuration files) 작성에 매우 적합하고 사람이 직접 데이터를 읽거나 수정해야 하는 경우 편리합니다. 또한 다양한 프로그래밍 언어에서 파서(Parser)를 지원합니다. 단점으론 대용량 데이터 저장 및 분석 쿼리에는 매우 비효율적입니다. 텍스트 기반이고, 파싱 속도가 바이너리 포맷이나 최적화된 컬럼 포맷(Parquet 등)에 비해 느립니다. 또한 들여쓰기에 매우 민감하여 작은 실수로도 파싱 오류가 발생하기 쉽고 데이터 자체를 저장하는 포맷이라기보다는, 데이터를 표현하고 교환하며, 특히 설정을 기술하는 데 더 중점을 둡니다.

    댓글