직렬화
직렬화는 메모리에 흩어져 있는 데이터를 한 줄로 늘어선 바이트로 바꾸는 일입니다. 그렇게 바꿔야 파일에 적어 두거나 다른 기계로 보낼 수 있습니다. 받는 쪽은 그 바이트를 차례로 읽어 원래 데이터를 다시 세웁니다.
쉽고 빠른 이해
직렬화는 메모리에 흩어져 있는 데이터를 한 줄로 늘어선 바이트로 바꾸는 일입니다. 주문 하나를 파일에 적어 두려면 주문과 거기 딸린 고객·상품 목록을 앞에서 끝까지 죽 읽히는 바이트로 펴야 합니다.
메모리 위의 데이터는 서로의 번지를 들고 이어져 있습니다. 그 번지는 프로세스 밖으로 나가면 뜻을 잃습니다. 펴 두지 않으면 파일에 적어도 다시 못 읽고 다른 기계로 보내도 못 세웁니다.
어떻게 도는가:
- 보내는 쪽이 값을 정해진 규칙대로 바이트로 적습니다
- 그 바이트를 파일에 적어 두거나 네트워크로 보냅니다
- 받는 쪽이 같은 규칙으로 읽어 원래 모양을 다시 세웁니다
대가가 있습니다. 프로세스에 매여 있던 값은 못 싣습니다. 열린 파일이나 연결은 받는 쪽에서 다시 만들어야 합니다. 값의 범위가 좁은 형식으로 적으면 되돌린 값이 원래와 달라집니다.
상세
장난감 블록으로 쌓아 올린 성을 친구에게 보내려면 어느 블록을 어디에 끼웠는지 처음부터 끝까지 한 줄로 적습니다. 친구는 그 종이를 차례로 따라가 자기 블록으로 똑같은 성을 세웁니다. 내 방의 성은 그동안 그대로 서 있습니다.
직렬화는 메모리 위의 값을 정해진 규칙에 따라 바이트를 하나씩 늘어놓은 순서열로 바꾸는 일입니다. 원본은 메모리에 그대로 남고 바이트로 된 사본이 하나 나옵니다. 주문 하나를 파일에 적어 둔다고 해봅시다. 주문이 고객을 가리키고 상품 목록을 가리키는 이 덩어리를, 앞에서 끝까지 죽 읽어 나갈 수 있는 바이트로 펴는 것이 직렬화입니다. 반대 방향은 역직렬화라고 부릅니다. 바이트 순서열을 읽어 메모리 위에 원래 모양을 다시 세우는 일입니다.
flowchart TD
subgraph 보내는 쪽
A["주문"] --> B["고객"]
A --> C["상품 목록"]
end
subgraph 받는 쪽
E["주문"] --> F["고객"]
E --> G["상품 목록"]
end
A -. 직렬화 .-> D["바이트 순서열"]
D -. 역직렬화 .-> E
주소를 한 줄로 펴는 까닭
메모리 위의 데이터는 주소로 이어져 있습니다. 값 하나가 다른 값이 놓인 번지를 들고 있고, 그 값이 또 다른 번지를 들고 있습니다. 이 번지는 그 프로세스가 살아 있는 동안 그 프로세스 안에서만 뜻이 있습니다.
프로세스 밖으로 나가는 순간 번지는 뜻을 잃습니다. 번지를 그대로 파일에 적어 두면 다음에 프로그램을 다시 켰을 때 그 자리에 다른 것이 놓여 있습니다. 다른 기계로 보내면 애초에 같은 메모리가 아닙니다.
그래서 번지로 엮어 놓은 그물을 번지 없이도 되살릴 수 있는 한 줄로 펴야 합니다. 편 결과가 바이트 순서열이고, 펴는 일이 직렬화입니다. 파일도 네트워크도 바이트를 하나씩 차례로만 받습니다. 「직렬」 이라는 이름이 여기서 옵니다. 여러 갈래로 벌어져 있던 것이 한 줄로 늘어선다는 뜻입니다.
형식이 정하는 규칙
같은 값을 두고도 바이트로 적는 방법은 여럿입니다. 그래서 보내는 쪽과 받는 쪽이 같은 규칙을 써야 합니다. 이 규칙 한 벌을 직렬화 형식이라고 부릅니다.
| 정해야 하는 것 | 안 정하면 |
|---|---|
| 값의 종류를 어떻게 알리나 | 받는 쪽이 이 바이트가 숫자인지 글자인지 모릅니다 |
| 값이 어디서 끝나나 | 다음 값이 어디서 시작하는지 못 찾습니다 |
| 정수를 몇 바이트로, 어느 바이트부터 적나 | 같은 수가 다른 수로 읽힙니다 |
| 글자를 어느 문자 인코딩으로 적나 | 글자가 깨집니다 |
| 필드 이름을 같이 실을지 | 실으면 커지고, 안 실으면 받는 쪽이 정의를 따로 알아야 합니다 |
| 한 값을 두 군데서 가리킬 때 어떻게 하나 | 사본이 둘로 갈리거나, 순환 참조에서 끝없이 돕니다 |
값이 어디서 끝나는지를 알리는 방법은 크게 둘입니다. 값 앞에 길이를 먼저 적어 두면 받는 쪽이 그만큼 읽고 멈춥니다.
block-beta columns 7 a["길이 5"] b["h"] c["e"] d["l"] e["l"] f["o"] g["다음 값"]
길이를 안 적는 형식은 대신 구분자를 둡니다. 따옴표를 닫거나 줄을 바꾸는 것이 그 자리입니다. 이때는 값 안에 구분자와 똑같은 글자가 들어 있을 때 그것을 따로 표시해 줘야 합니다.
사람이 읽는 형식과 기계가 읽는 형식
형식은 크게 둘로 갈립니다. 글자로 적는 텍스트 형식과 바이트를 그대로 쓰는 이진 형식입니다.
| 텍스트 형식 | 이진 형식 | |
|---|---|---|
| 사람이 열어 볼 때 | 그대로 읽힙니다 | 도구가 있어야 읽힙니다 |
| 같은 값의 크기 | 이진 형식에 비해 큽니다 | 텍스트 형식에 비해 작습니다 |
| 필드 이름 | 대개 같이 싣습니다 | 대개 번호나 자리 순서로 대신합니다 |
| 정의를 따로 나눠 갖나 | 안 나눠도 대충 읽힙니다 | 나눠 가져야 읽힙니다 |
직렬화가 못 싣는 값
값 가운데는 프로세스 밖으로 나가면 뜻을 잃는 것이 있습니다. 열린 파일 핸들, 연결된 소켓, 돌고 있는 스레드, 함수가 놓인 번지가 그렇습니다. 이것들은 그 프로세스와 그 기계에 매여 있어서, 바이트로 적어 보내도 받는 쪽에는 되살릴 것이 없습니다.
그래서 직렬화는 대개 이런 값을 뺍니다. 뺀 자리는 받는 쪽에서 다시 만들어야 합니다. 파일은 다시 열고 연결은 다시 맺습니다. 되살린 것이 원본과 같은 값이더라도 같은 물건은 아니라는 뜻입니다.
되돌렸을 때의 일치
직렬화의 기본 약속은 되돌리기입니다. 어떤 값을 직렬화한 다음 곧바로 역직렬화하면 원래 값과 같은 값이 나와야 합니다. 형식을 고를 때 먼저 보는 것이 이 약속이 어디까지 지켜지는가입니다.
지켜지지 않는 자리가 있습니다. 형식이 담을 수 있는 값의 범위가 원래 타입보다 좁으면 되돌린 값이 달라집니다. 아주 큰 정수를 실수로 적으면 끝자리가 깎입니다. 십진으로 적던 소수를 이진 부동소수점으로 옮기면 값이 어긋납니다. 순서가 있던 자료를 순서를 안 지키는 형식으로 적으면 순서가 사라집니다. 이런 어긋남은 그 자리에서 오류를 안 냅니다. 한참 뒤에 값이 안 맞는 것으로 드러납니다.
정의가 달라졌을 때의 호환성
보내는 쪽과 받는 쪽은 대개 따로 고쳐지고 따로 배포됩니다. 그래서 한쪽이 필드를 더하거나 지운 뒤에도 다른 쪽이 그 바이트를 읽을 수 있어야 합니다.
방향이 둘입니다. 새 코드가 옛 코드의 바이트를 읽을 수 있으면 하위 호환이고, 옛 코드가 새 코드의 바이트를 읽을 수 있으면 상위 호환입니다. 필드를 더할 때는 옛 코드가 모르는 필드를 만나도 건너뛰게 해 두고, 필드를 지울 때는 그 필드가 없어도 되도록 미리 값을 정해 둡니다. 한 번 쓴 이름이나 번호를 다른 뜻으로 다시 쓰는 것은 위험합니다. 옛 바이트에 남아 있던 값이 새 뜻으로 읽힙니다.
믿을 수 없는 바이트
역직렬화는 밖에서 온 바이트를 읽어 메모리 위에 값을 만드는 일입니다. 형식에 따라서는 그 바이트가 「어떤 타입의 값을 만들어라」까지 적을 수 있습니다. 그러면 받는 쪽은 보낸 쪽이 시키는 대로 객체를 만들게 됩니다.
그래서 믿을 수 없는 곳에서 온 바이트는 그대로 역직렬화하지 않습니다. 읽어도 되는 타입을 미리 정해 두거나, 타입을 바이트가 못 정하는 형식을 씁니다. 크기와 중첩 깊이에도 상한을 둡니다. 상한이 없으면 작은 바이트 한 덩이가 받는 쪽에 아주 큰 자료를 만들어 내게 시킬 수 있습니다.
관련 항목
직렬화 결과를 적는 데이터 형식
JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) · XML(Extensible Markup Language, 확장 가능 표시 언어) · YAML(YAML Ain't Markup Language) · CSV(Comma-Separated Values, 쉼표로 나눈 값) · Protocol Buffers · Apache Avro · Apache Thrift · MessagePack · CBOR(Concise Binary Object Representation, 간결 이진 객체 표현) · BSON(Binary JSON, 이진 JSON) · ASN.1(Abstract Syntax Notation One, 추상 구문 표기법 1)
바이트를 글자로 다시 적는 인코딩
Base64 · UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트) · 문자 인코딩 · URL 인코딩 · PEM(Privacy-Enhanced Mail) · 유니코드
직렬화와 짝을 이루는 반대 방향 연산
역직렬화 · 파싱 · 마셜링 · 언마셜링 · 인코딩 · 디코딩
직렬화한 바이트가 지나가는 전달 경로
네트워크 · 소켓 · HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 규약) · RPC(Remote Procedure Call, 원격 프로시저 호출) · gRPC · 메시지 큐 · 파일 시스템 · 표준 입출력
직렬화한 값을 담아 두는 저장소
데이터베이스 · 캐싱 · 세션 · 쿠키 · 로그 · 백업과 복구 · 객체 스토리지
바이트로 적을 때 정해야 하는 규칙
엔디언 · 바이트 순서 표식 · 길이 접두사 · 구분자 · 가변 길이 정수 · 패딩
보내는 쪽과 받는 쪽의 정의를 맞추는 규약
스키마 · 스키마 진화 · 인터페이스 정의 언어 · 하위 호환성 · 상위 호환성 · 시맨틱 버저닝 · 계약 테스트
직렬화에서 자주 나는 오류·장애
역직렬화 취약점 · 순환 참조 · 문자 깨짐 · 정밀도 손실 · 스키마 불일치 · 버퍼 오버플로 · 자원 고갈
직렬화를 언어 차원에서 대신 해 주는 장치
리플렉션 · 애너테이션 · 코드 생성 · ORM(Object-Relational Mapping, 객체-관계 매핑) · 스텁
직렬화 비용을 재는 지표
지연 · 처리량 · 페이로드 크기 · CPU(Central Processing Unit, 중앙 처리 장치) · 메모리 사용량
직렬화와 이름이나 뜻이 헷갈리는 이웃
직렬화 가능성 · 암호화 · 압축 · 해싱 · 정규화 · 영속화
직렬화 형식을 규격으로 못 박는 표준화 기구
IETF(Internet Engineering Task Force, 국제 인터넷 표준화 기구) · W3C(World Wide Web Consortium, 월드 와이드 웹 컨소시엄) · IANA(Internet Assigned Numbers Authority, 인터넷 할당 번호 관리 기관) · ISO(International Organization for Standardization, 국제 표준화 기구) · 유니코드 컨소시엄
직렬화를 전제로 도는 시스템 갈래
분산 시스템 · 마이크로서비스 · API(Application Programming Interface, 응용 프로그램 인터페이스) · 이벤트 소싱 · 체크포인트 · 프로세스 간 통신
다른 이름: serialization · serialize