Post

Binary & Base64

Binary & Base64

배경

  • 다양한 형식으로 이미지 데이터를 변환하고, 효율적인 전송과 처리를 위한 데이터 포맷에 대해 정리해보았다.

Binary

바이너리(이진) 데이터

  1. 컴퓨터 내부에서 모든 데이터는 결국 0과 1로 이루어져 있지만, 0과 1로 이루어진 텍스트를 읽어봐야 의미가 없기 때문에 우리가 이해하기 쉽도록 2진수, 16진수, ASCII 문자 등으로 변환해서 본다.
    1. 컴퓨터 메모리에서 데이터는 바이트(8비트) 단위로 저장되고 처리된다.
    2. 16진수는 4비트 단위로 묶어서 표현하기에, 1바이트(8비트)는 2개의 16진수 숫자로 표현된다.
  2. 예시 - JPEG 이미지 파일의 첫 바이트
    1. 텍스트 편집기로 보면 ÿØÿà 같은 외계어로 보인다.
    2. 실제 값은 11111111 11011000 11111111 11100000 (→ 바이너리 데이터이다.)
    3. 16진수로 표현하면 0xFF 0xD8 0xFF 0xE0
      • 참고로 0xFF 0xD8는 “SOI(Start Of Image)” 마커로, JPEG 파일의 시작을 나타내는 시퀀스로서 파일이 JPEG 이미지 형식이라는 것을 알려준다.
    4. 이 데이터를 텍스트 편집기에서 열면 특정 방식으로 인코딩이 되는데
      1. ISO-8859-1(라틴-1) 방식의 경우
        • 0xFF: ISO-8859-1에서 ÿ 문자로 표시
        • 0xD8: ISO-8859-1에서 Ø 문자로 표시
        • 0xFF: ISO-8859-1에서 ÿ 문자로 표시
        • 0xE0: ISO-8859-1에서 à 문자로 표시
      2. UTF-8에서는 깨진 문자나 기호로 표시된다.
  3. 2의 이유로 바이너리 데이터를 ‘인간이 읽을 수 없는 형식’이라고 말한다. 텍스트 형식으로 읽어봐야 외계어이기 때문이다.

바이너리 데이터와 바이너리 파일

  • 데이터를 저장하기 위한 기본 단위인 파일(File)은 일반적으로 텍스트 파일과 바이너리 파일 둘로 나눌 수 있다.
    1. 텍스트 파일은 .txt, .html, .csv, .json 같은 형식으로 인간이 읽을 수 있는 형식으로 저장되어 메모장 같은 텍스트 편집기를 통해 내용을 쉽게 열람하고 수정할 수 있으며, ASCII나 UTF-8 같은 문자 인코딩을 사용하여 문자열을 저장한다. 헤더 없이 단순한 텍스트 데이터만 포함한다.
    2. 바이너리 파일은 .jpg, .png, .exe, .mp4, .zip 등으로, 파일의 모든 내용이 그대로 0과 1의 비트(바이너리 데이터)로 저장되어 있고, 이 비트들이 파일의 형식에 따라 특정 구조로 저장된다. 특정 소프트웨어나 프로그램에 의해 해석되어야 그 내용을 확인하거나 사용할 수 있어 인간이 읽기 어렵다. 또한 바이너리 파일은 파일의 형식이나 내용을 설명하는 헤더를 포함한다.
    3. .jpg 파일과 .jpg 파일의 바이너리 데이터는 어떻게 다른가?
    1. JPEG 형식으로 구조화된 데이터를 담고 있는 파일 자체
    2. JPEG 형식으로 구조화된 데이터

    라고 볼 수 있다. 파일 시스템 상에서 존재하는 구체적인 개체를 이루면 이걸 파일이라고 한다.

    책 한권과 책 한권의 내용 으로 비유할 수 있다.

  1. .jpg 파일의 바이너리 데이터는 다음과 같이 가져올 수 있다.
1
2
with open("example.jpg", "rb") as file:
    binary_data = file.read()
  1. .jpg 파일의 바이너리 데이터를 그래도 굳이 보고 싶다면
1
2
3
4
5
6
with open("example.jpg", "rb") as file:
    byte = file.read(1)  # 파일에서 1바이트(8비트)씩 읽어옴
    while byte:
        # 1바이트를 이진수로 변환하고 출력
        print(f'{int.from_bytes(byte, "big"):08b}', end=' ')
        byte = file.read(1)  # 다음 1바이트 읽기

이렇게 볼 수는 있지만 별 쓸 데는 없다.


base64

Base64?

Base64는 바이너리 데이터를 텍스트 형식으로 변환하는 인코딩 방식이다. 앞서 서술 했던 대로, 이미지 파일의 바이너리 데이터는 읽을 수 없는 형태로 되어 있는데, 이를 문자열 텍스트로 만들기 위해 사용한다.

1
2
3
4
with open(path, "rb") as img:
    data = "data:image/jpg;base64," + base64.b64encode(img.read()).decode(
        "utf-8"
    )

이런 식으로 바이너리 데이터img.read() 를 텍스트 형식으로 변환할 수 있고, 결과는 보통 다음과 같다.

1
"data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/...

이제 읽을 수 있는 형식이 되었다!

그런데 base64로 인코딩을 하면 데이터의 크기가 33% 증가한다.

Binary → base64 문자열 인코딩으로 33% 커지는 과정

  • ASCII 문자나 UTF-8 문자에서 문자열을 구성하는 문자, 즉 char은 1Byte, 즉 8bit로 구성되어 256가지를 표현할 수 있다. 그런데 base64 인코딩 방식에서는 8bit가 아닌 6bit만 사용해서 64가지(A-Z, a-z, 0-9, +, /)만 표현한다.
  • 바이너리 데이터는 가장 처음에 서술했듯 8비트 단위로 묶어서 처리되는데, Base64 인코딩은 이 8비트 데이터를 6비트 단위로 나누어서 처리한다.
  • 10101100 11001010 01101101 와 같은 24비트 바이너리 데이터가 있다.
  • 101011 001100 101001 101101 이렇게 base64에서는 6개 단위로 다시 쪼갠다.
  • 그러면 문자 4개가 완성되는데, 이 경우 24비트가 32비트(8*4)로 33% 증가한다.
  • Base64 인코딩 방식은 항상 24비트(즉, 3바이트) 단위로 묶어 처리하며, 이 24비트를 6비트씩 4조각으로 나누어 각각 Base64 문자 하나로 매핑한다. 이는 8비트와 6비트의 최소공배수가 24이기 때문에 정의된 방식이다.
  • 만약 바이너리 데이터가 3바이트 단위로 나누어떨어지지 않으면, 부족한 바이트를 채우기 위해 = 문자로 패딩을 추가하여 전체 길이를 4의 배수로 맞춘다.
  • 이로 인해 데이터 크기는 일반적으로 약 33% 증가하지만, 패딩의 유무에 따라 정확히 33.33%가 되지는 않을 수 있다.

그럼 Base64 인코딩을 왜 쓸까?

데이터 전송 시간은, 네트워크 환경이나 처리 방식이 동일하다면 전송되는 데이터의 크기에 비례한다고 볼 수 있다.
Base64는 데이터를 텍스트 형태로 바꾸는 인코딩 방식이지만, 약 33%의 크기 증가와 인코딩/디코딩에 따른 성능 손실이 있기 때문에 가능한 한 피하는 것이 일반적이다.
그럼에도 불구하고 Base64 인코딩이 꼭 필요한 상황이 있다.

Base64가 꼭 필요한 경우

  1. 텍스트 기반 형식에서만 데이터를 주고받을 수 있을 때

    WebSocket, GraphQL, JSON, XML, SMTP(이메일 전송) 등은 기본적으로 텍스트만 다룰 수 있는 구조이기 때문에 바이너리 데이터를 직접 포함할 수 없다.
    이럴 때 Base64처럼 바이너리를 텍스트로 변환해주는 방식이 필요하다.

    예시:

    • JSON의 "image": "base64 인코딩된 문자열" 형태
    • HTML에서 <img src="data:image/png;base64,...">
  2. 브라우저나 클라이언트 환경에서 문자열 데이터만 허용될 때

    • 브라우저의 로컬스토리지, 쿠키, 또는 JS 객체로만 이미지를 주고받아야 하는 경우
    • 클라이언트 간 텍스트 기반의 안전한 데이터 공유가 필요할 때

바이너리 데이터 손상 방지를 위해 써야 할까?

과거에는 NULL 문자(\x00), 개행 문자, 캐리지 리턴(\x0D\x0A) 등이 HTTP 요청의 헤더나 본문을 망가뜨릴 수 있었지만, 현대의 HTTP 라이브러리와 서버는 이러한 문제를 방지할 수 있도록 명확하게 구분된 전송 구조를 갖추고 있어, 바이너리 전송 자체가 위험하거나 깨지는 경우는 매우 드물다.

그래서 꼭 필요하지 않은 경우엔 안 쓰는 게 낫다

  • multipart/form-data 또는 application/octet-stream 등의 방식으로 바이너리를 직접 전송할 수 있다면, Base64를 사용할 필요가 없다.
  • 예: REST API로 파일 업로드, gRPC 메시지 전송, NumPy 배열을 서버에 넘기는 딥러닝 추론 요청 등

정리

Base64가 필요한 경우Base64가 불필요한 경우
JSON, GraphQL, WebSocket, SMTPREST API multipart/form-data, application/octet-stream
브라우저 인라인 이미지 삽입gRPC, AI 모델 서버 호출 (Triton 등)
로컬 저장소, 쿠키 등 문자열 저장파일 업로드용 API 사용 가능할 때

Base64는 “바이너리를 직접 전송할 수 없는 환경”에서만 쓰는 것이 원칙이며,
그 외의 상황에서는 불필요한 데이터 크기 증가와 오버헤드만 초래할 수 있다.


JSON 형태로 전송할 때

numpy 배열

  • 이미지의 numpy 배열이라고 하면 이미지를 픽셀 단위로 표현한 배열로, 각 픽셀의 RGB 값을 숫자 형태(0~255)로 저장한다.
  • 이미지 데이터를 조작하고 분석하기 쉽게 만들어주기 때문에 컴퓨터 비전, 딥러닝, 분석 같은 이미지 처리 에 유용하지만 이미지가 압축되지 않은 상태이고 각 픽셀의 값을 메모리에 그냥 저장하기 때문에 용량이 원본 바이너리 파일보다 훨씬 크다.
  • 그래서 이미지 처리보다 전송이나 저장이 주 목적이고, 원본 파일을 유지해야 한다면 적합하지 않다.
  • 전송할 때에는 NumPy 배열을 그대로 전송할 수 없기 때문에, 이를 JSON과 같은 텍스트 형식으로 변환해야 한다. 보통 tolist() 메서드를 사용하여 NumPy 배열을 파이썬의 리스트로 변환한 후 JSON으로 직렬화한다.

tolist()

  • tolist() 메서드는 NumPy 배열을 파이썬 리스트로 변환하는 메서드이다.
  • 이미지를 NumPy 배열로 읽어오면 각 픽셀 값이 배열의 요소로 저장되며, 이 배열을 tolist()를 통해 리스트로 변환하면 각 픽셀 값이 리스트의 개별 요소로 유지된다. (예: RGB 이미지의 경우 3차원 리스트)
  • 이 리스트를 JSON으로 직렬화할 경우, 숫자는 그대로 숫자 타입으로 유지되지만 텍스트 기반 포맷으로 표현되기 때문에 숫자의 자릿수에 따라 데이터 크기가 커질 수 있다.
    예를 들어, 숫자 1은 "1"로 1바이트지만, 230은 "230"으로 3바이트가 필요하다.
  • 또한 JSON은 구조적인 기호들(예: [, ], ,, : 등)이 함께 포함되므로 전체 데이터 크기가 원본 NumPy 배열보다 훨씬 커질 수 있다.
  • 예: (H x W x C) 형태의 NumPy 배열은, 리스트로 변환 시 이중 또는 삼중 리스트 구조로 표현된다. (세로 × 가로 × 채널 수)


추가적인 옵션

JPEG 압축

  • 데이터의 크기를 줄이기 위한 방법이다. 이미지 데이터를 효율적으로 저장하기 위해 사용되는 손실 압축 기술로, 원본 이미지를 압축하여 데이터 크기를 줄이지만 압축 과정에서 일부 정보가 손실 된다.
  • 중요한 정보를 유지하면서 불필요한 부분(사람의 눈에 잘 인식되지 않는 부분)은 제거하는 기술인데, 컬러 공간 변환, 샘플링, DCT 변환, 양자화, 엔트로피 부호화 등등 여러가지가 있다.
  • 압축률을 높이면 파일의 크기는 더 줄어들지만 그만큼 이미지 품질이 떨어진다. 반면 무손실 압축을 사용하여 원본 데이터를 그대로 복원할 수 있는 PNG와 같은 형식도 있다.
    • 그러나 같은 해상도의 이미지라도 JPEG가 PNG보다 훨씬 크기가 작다. (알파 채널 지원 등의 이유)
    • 고품질, 투명도, 로고, 그래픽, 텍스트PNG
    • 파일 크기, 전송 속도, 사진, 웹 최적화JPEG
  • 결론적으로는 이미지 압축을 통해 데이터의 절대적인 크기를 줄여 전송 소요 시간을 줄이는 방법이다.

다른 전송 방법

  • HTTP의 multipart/form-data 형식을 사용하여 파일 업로드처럼 이미지를 전송

    구분바이너리 데이터 전송파일 전송 (Multipart/Form-Data)
    전송 데이터 형식순수한 바이너리 데이터 그대로 전송multipart/form-data 형식으로 파일과 텍스트 함께 전송
    메타데이터 전송파일 데이터만 전송 가능, 메타데이터 전송 어려움파일과 함께 텍스트 필드로 메타데이터 전송 가능
    전송 크기원본 크기 그대로 (최소한의 헤더 정보)멀티파트 헤더 정보가 추가되어 약간의 크기 증가
    적합한 상황이미지/바이너리 파일 자체가 전송의 주 목적일 때파일 업로드 API, 이미지와 다른 메타데이터를 함께 전송할 때

    이런 방법도 있다고 하는데 추가적으로 공부를 해봐야 할 것 같다.

  • 이미지를 서버에 미리 업로드해 두고, 이미지 URL만을 전송
    • 이미지 자체의 전송은 없고, 경로만 공유하므로 전송 효율성 자체에는 영향이 없다.
  • Blob(Binary Large Object)
    • Blob은 주로 이미지, 오디오, 비디오와 같은 멀티미디어 파일 바이너리를 객체 형태로 저장한 것을 의미한다. 그래서, DB에 이미지 파일을 그대로 데이터로 저장하고 싶을 때, 바로 Blob 포멧으로 변환한 뒤 저장하면 된다. 브라우저에서도 JS로 Blob 데이터에 접근하고 사용할 수 있다.
    • Base64와 다른 점은
      • Base64는 바이너리 데이터를 다루기 위해 텍스트 형태로 저장한 포멧
      • Blob은 바이너리 데이터를 다루기 위해 객체 형태로 저장한 포멧
    • 그리고 Blob은 객체이기 때문에 다양한 코드 활용성을 지니고 있어, base64로도 변환할 수 있고 buffer로도 변환할 수도 있다.


https://developer.mozilla.org/en-US/docs/Glossary/MIME_type
https://www.iana.org/assignments/media-types/media-types.xhtml

This post is licensed under CC BY 4.0 by the author.