포맷
고친 사람 github-actions[bot]
포맷은 데이터를 바이트로 적는 방법을 미리 정해 둡니다. 그래서 서로 모르는 프로그램끼리도 같은 파일을 읽고 씁니다. 디스크를 비우는 작업과 화면에 찍는 서식도 같은 낱말로 부릅니다.
쉽고 빠른 이해
포맷은 데이터를 바이트로 적는 순서와 모양을 정해 둡니다. 사진 파일 하나를 어느 프로그램으로 열어도 같은 그림이 나오는 것이 그 덕입니다.
약속이 없으면 그 파일을 만든 프로그램만 그 파일을 읽습니다. 데이터를 다른 도구로 넘기거나 오래 보관해 두고 나중에 꺼내는 길이 막힙니다.
어떻게 도나:
- 적는 쪽이 정해진 순서대로 값을 늘어놓아 바이트로 바꿉니다
- 읽는 쪽이 같은 순서를 알고 그 바이트를 도로 값으로 나눕니다
- 규칙을 벗어난 바이트를 만나면 잘못된 파일로 보고 거절합니다
사람이 열어 볼 파일은 문자로 적는 포맷을, 기계끼리 빠르게 주고받는 데이터는 바이트로 적는 포맷을 고릅니다.
대가는 한번 퍼진 포맷을 고치기 어렵다는 것입니다. 이미 세상에 나간 파일들이 옛 규칙으로 적혀 있어서, 새 규칙을 들이더라도 옛 규칙을 계속 읽어 줘야 합니다.
상세
은행 창구의 입금 신청서를 떠올리면 됩니다. 어느 칸이 계좌번호고 어느 칸이 금액인지 종이에 정해져 있기 때문에, 받아 든 직원은 글씨만 읽으면 무엇이 무엇인지 압니다.
포맷은 데이터를 바이트로 적는 규칙입니다. 어떤 값이 올 수 있고, 값들을 어떤 순서로 늘어놓고, 값 하나를 몇 바이트로 어떻게 적을지를 정합니다. 설정 파일 한 개, 사진 파일 한 장, 서버가 돌려주는 응답 본문이 전부 어느 포맷에 맞춰 적힌 바이트 덩어리입니다.
메모리 안의 데이터를 이 규칙에 맞춰 바이트로 바꾸는 작업이 직렬화입니다. 받은 바이트를 규칙대로 읽어 데이터로 되돌리는 작업은 역직렬화입니다. 포맷은 그 두 작업이 서로 어긋나지 않게 잡아 주는 약속입니다.
포맷이 필요한 것은 데이터를 적는 쪽과 읽는 쪽이 대개 남이기 때문입니다. 두 프로그램이 같은 사람 손에서 같은 시기에 만들어졌다는 보장이 없습니다. 게다가 파일은 만든 프로그램보다 오래 살아남습니다.
미리 공개된 규칙 하나를 사이에 두면 서로를 몰라도 데이터가 오갑니다. 이렇게 따로 만들어진 시스템이 맞물려 도는 성질이 상호운용성입니다.
포맷이 정하는 세 층
포맷 하나를 뜯어보면 정해야 할 것이 세 층으로 쌓여 있습니다. 위층이 정해져도 아래층이 안 정해지면 바이트가 안 나옵니다.
flowchart TD
subgraph L1["1층 · 어떤 값이 올 수 있나"]
A["수 · 문자열 · 목록"]
end
subgraph L2["2층 · 값을 어떻게 이어 붙이나"]
B["구분자 · 길이 먼저 · 고정 길이"]
end
subgraph L3["3층 · 값 하나를 어떤 바이트로 옮기나"]
C["문자 인코딩 · 엔디언"]
end
L1 --> L2
L2 --> L3
L3 --> D["파일에 쓸 바이트"]
첫째는 어떤 값이 올 수 있느냐입니다. 숫자만 담는 포맷이 있고 숫자와 문자열과 목록까지 담는 포맷이 있습니다. 포맷이 안 정한 종류의 값은 그 포맷으로 적을 방법이 아예 없습니다. 담고 싶은 것이 그 목록 밖이면 다른 포맷을 고르거나 억지로 문자열에 욱여넣게 됩니다.
둘째는 값들을 어떻게 이어 붙이느냐입니다. 값 사이에 쉼표 같은 구분자를 끼우는 방법이 있습니다. 값 앞에 그 값의 길이를 먼저 적어 두는 방법도 있습니다.
값이 놓일 곳을 아예 못 박아 두기도 합니다. 첫 값은 1~4번 바이트, 둘째 값은 5~8번 바이트 하는 식입니다. 어느 쪽을 골랐느냐가 읽는 쪽이 바이트를 어디서 끊을지를 정합니다.
셋째는 값 하나를 실제 바이트로 옮기는 단계입니다. 문자를 바이트로 옮기는 규칙은 문자 인코딩이 정합니다. 같은 수를 앞 바이트부터 적을지 뒤 바이트부터 적을지는 엔디언이 정합니다. 여기까지 정해져야 비로소 파일에 쓸 바이트가 나옵니다.
세 층 중 하나라도 적는 쪽과 읽는 쪽이 다르게 알고 있으면 읽는 쪽은 엉뚱한 값을 얻습니다. 바이트 자체에는 뜻이 없고, 뜻은 전부 규칙 쪽에 있기 때문입니다.
텍스트 포맷과 이진 포맷
포맷은 값을 사람이 읽는 문자로 적느냐, 값의 바이트 표현 그대로 적느냐로 크게 둘로 갈립니다. 앞쪽을 텍스트 포맷, 뒤쪽을 이진 포맷이라고 부릅니다.
텍스트 포맷은 숫자도 문자로 적습니다. 12345 라는 수를 문자 다섯 개로 늘어놓습니다. JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)이나 CSV(Comma-Separated Values, 쉼표로 나눈 값)가 그렇게 적습니다. 아무 편집기로나 열어서 눈으로 확인하고 손으로 고칠 수 있습니다.
이진 포맷은 같은 수를 네 바이트짜리 정수 그대로 적습니다. PNG(Portable Network Graphics) 이미지 파일이 그렇게 적힌 파일입니다. 문자로 바꾸는 단계가 없으니 저장 공간도 덜 먹고 읽는 쪽도 바이트를 값으로 그냥 옮깁니다. 대신 편집기로 열면 깨진 글자만 보이고, 규칙을 아는 도구가 없으면 안에 무엇이 들었는지 알 길이 없습니다.
| 텍스트 포맷 | 이진 포맷 | |
|---|---|---|
| 값을 적는 법 | 사람이 읽는 문자로 | 값의 바이트 표현 그대로 |
| 눈으로 확인 | 편집기로 바로 | 전용 도구가 있어야 |
| 같은 값의 크기 | 자릿수만큼 늘어난다 | 정해진 바이트로 끝난다 |
| 읽어 들이는 절차 | 문자를 값으로 되돌리는 계산이 붙는다 | 바이트를 값에 그대로 옮긴다 |
| 손으로 고치기 | 편집기로 된다 | 규칙을 아는 도구가 필요하다 |
표의 축들이 한 방향을 가리킵니다. 텍스트 쪽은 사람이 들여다보는 값을 치르고 저장 공간과 계산을 내주고, 이진 쪽은 그 반대입니다. 그래서 사람이 열어 볼 일이 잦은 설정 파일은 텍스트로 가고, 기계끼리 쉴 새 없이 주고받는 데이터는 이진으로 갑니다.
파일을 받아 들고 포맷을 알아내는 법
바이트만 봐서는 그것이 어느 규칙으로 적힌 것인지 알 수 없습니다. 그래서 포맷을 알려 주는 표지를 따로 둡니다. 실무에서 마주치는 표지는 셋입니다.
첫째는 파일 이름 끝에 붙는 파일 확장자입니다. 사람이 보기에 편하지만 이름만 바꾸면 그만이라 내용과 어긋날 수 있습니다.
둘째는 파일 맨 앞에 박아 둔 고정된 바이트 값입니다. 이것을 매직 넘버라고 부릅니다. 포맷마다 다른 값을 쓰기로 정해 두었으므로, 읽는 쪽은 앞머리 몇 바이트만 떼어 보고 어느 포맷인지 가립니다. 이름과 달리 파일 안에 들어 있어서 파일을 옮겨도 따라다닙니다.
셋째는 데이터를 건네주면서 포맷 이름을 따로 말해 주는 방법입니다. 웹에서는 MIME(Multipurpose Internet Mail Extensions, 다목적 인터넷 메일 확장) 타입이라는 이름표를 씁니다.
그 이름표는 Content-Type이라는 칸에 실어 보냅니다. 보내는 데이터가 어느 포맷인지를 적어 데이터와 함께 부치는 칸입니다.
이진 포맷은 이 표지를 아예 구조 안에 넣습니다. 파일 앞머리에 매직 넘버를 둡니다. 그 뒤에는 읽는 데 필요한 값을 모은 머리말이 옵니다.
파일 앞쪽에 모아 두는 이 머리말이 헤더입니다. 크기나 버전 번호가 여기 들어갑니다. 실제 데이터는 그다음입니다.
읽는 쪽은 이 배치를 앞에서 뒤로 밟아 갑니다. 앞머리를 보고 어느 포맷인지 가립니다. 헤더를 읽어 뒤에 무엇이 얼마나 오는지 알아냅니다. 데이터는 그 안내대로 잘라 냅니다.
flowchart TD A["매직 넘버<br/>어느 포맷인지 가린다"] --> B["헤더<br/>뒤에 무엇이 얼마나 오는지 안다"] B --> C["데이터<br/>그 안내대로 잘라 낸다"]
포맷이 새 버전으로 갈 때
포맷은 한번 퍼지면 고치기 어렵습니다. 이미 만들어져 흩어진 파일들이 옛 규칙으로 적혀 있고, 그 파일들을 읽던 옛 도구도 계속 돌고 있기 때문입니다. 규칙을 바꾸면 둘 중 한쪽이 깨집니다.
오래 쓰이는 포맷은 처음부터 바뀔 것을 예상하고 빈 칸을 마련해 둡니다. 헤더에 버전 번호를 두어 어느 규칙으로 적힌 것인지 밝힙니다. 값 앞에는 길이를 적어 둡니다. 그러면 읽는 쪽이 모르는 값을 건너뜁니다.
새 도구가 옛 파일을 읽어 내는 성질이 하위 호환입니다. 반대로 옛 도구가 새 파일을 읽어 내는 것은 상위 호환입니다. 앞쪽은 새 도구에 옛 규칙을 남겨 두면 되지만, 뒤쪽은 옛 도구를 고칠 수 없으니 새 규칙이 애초에 옛 도구를 배려해야 합니다.
배려의 알맹이는 모르는 것을 만났을 때 어떻게 하라고 미리 정해 두는 것입니다. 건너뛰라고 정해 두면 옛 도구가 새 파일을 읽다 모르는 항목을 흘려보내고 아는 것만 씁니다. 거절하라고 정해 두면 대신 잘못 읽어 낸 값이 조용히 퍼지는 것을 막습니다.
프로토콜·스키마·문자 인코딩과 가르는 선
포맷은 비슷한 말들과 자주 섞여 쓰입니다. 세 이웃과의 경계를 한 번씩 그어 두면 헷갈릴 것이 줄어듭니다.
프로토콜은 둘 이상이 메시지를 어떤 순서로 주고받는지를 정합니다. 포맷은 그 메시지 한 덩이를 어떤 바이트로 적는지를 정합니다. 그래서 프로토콜 안에 포맷이 들어갑니다. 무엇을 먼저 보내고 무엇을 기다리는지가 프로토콜이고, 그 하나하나의 생김새가 포맷입니다.
스키마는 한 포맷 안에서 어떤 이름의 값이 있어야 하고 그 값이 어떤 범위여야 하는지를 정합니다. 포맷이 문법이라면 스키마는 그 문법으로 쓰는 특정 문서의 약속입니다. 같은 포맷을 쓰는 두 서비스가 서로 다른 스키마를 둘 수 있습니다.
문자 인코딩은 포맷의 한 층입니다. 앞서 본 셋째 층이 이것이고, 문자 하나를 어떤 바이트로 옮길지만 맡습니다. 그 위에서 값을 어떻게 늘어놓을지는 포맷이 따로 정합니다.
같은 낱말이 가리키는 다른 작업
저장장치를 쓸 수 있게 준비하는 작업도 포맷이라고 부릅니다. 디스크의 기존 내용을 지우고 파일 시스템이 쓸 구조를 새로 까는 작업입니다. 이 뜻에서는 대개 「포맷한다」처럼 동사로 씁니다.
값을 사람이 읽을 문자열로 찍는 규칙도 포맷이라고 부릅니다. 날짜를 어떤 모양으로 찍을지, 소수점 아래를 몇 자리까지 보일지를 정하는 서식 문자열이 그런 것입니다.
셋이 한 낱말을 나눠 쓰는 것은 뿌리가 같기 때문입니다. 셋 다 「미리 정해 둔 모양에 맞춘다」를 말합니다. 다만 맞추는 대상이 각각 바이트, 저장장치, 화면에 찍히는 글자로 다릅니다.
관련 항목
포맷이 바이트를 쪼개는 방법
구분자 · 고정 길이 필드 · 가변 길이 필드 · 엔디언 · 옥텟 · 패딩
파일이 어느 포맷인지 알려 주는 표지
파일 확장자 · 매직 넘버 · 파일 시그니처 · MIME · Content-Type
문자를 바이트로 옮기는 포맷
문자 인코딩 · 문자 집합 · UTF-8 · UTF-16 · ASCII · EUC-KR · 바이트 순서 표시
텍스트로 적는 데이터 포맷
JSON · XML · YAML · CSV · TOML · INI 파일
바이트로 적는 데이터 포맷
Protobuf · PNG · 클래스 파일 · 실행 파일 · JAR · SPIR-V · TZif
데이터를 포맷으로 바꾸고 되돌리는 작업
직렬화 · 역직렬화 · 파서 · 파싱 · 압축 · Base64
포맷이 지켜야 하는 약속
하위 호환 · 상호운용성 · 스키마 · 스키마 진화 · 명세 · 표준
포맷과 맞세워지는 이웃 개념
프로토콜 · 인터페이스 · API · 계약 · 중간 표현
같은 낱말이 가리키는 다른 작업
디스크 포맷 · 파일 시스템 · 서식 문자열 · 출력 서식
다른 이름: format · data format · file format · 데이터 포맷 · 파일 포맷