사전 자기 서술적 메시지
개념

자기 서술적 메시지

gabury1고친 사람 github-actions[bot]

자기 서술적 메시지는 받는 쪽이 메시지 하나만 읽고 그것을 어떻게 다룰지 알 수 있는 성질입니다. 본문을 읽는 법과 다루는 법을 메시지가 스스로 밝힙니다. 덕분에 보낸 쪽과 미리 약속한 적 없는 프로그램도 그 메시지를 처리할 수 있습니다. 웹 서비스를 지을 때 지키는 설계 제약 하나가 모든 메시지에 이 성질을 요구합니다.

쉽고 빠른 이해

자기 서술적 메시지는 메시지가 자기를 읽는 법을 스스로 들고 다니게 하는 성질입니다. 웹 응답의 헤더에 「본문은 이런 형식이다」와 「10분 동안 다시 써도 된다」가 적혀 오는 것이 그렇습니다.

이게 없으면 메시지를 받는 프로그램마다 보낸 쪽의 문서를 미리 읽어 두어야 합니다. 중간에서 응답을 저장해 주는 캐시는 그 문서를 모르므로 손을 못 댑니다.

  1. 해 달라는 일을 누구나 아는 동사로 적습니다
  2. 본문의 형식을 이름표로 붙입니다
  3. 결과와 저장해 써도 되는지를 정해진 번호와 헤더로 알립니다

대가는 메시지마다 헤더가 붙어 길어진다는 것입니다. 누구나 아는 낱말로만 말해야 해서 애플리케이션에 꼭 맞는 표현을 못 쓸 때도 있습니다.

누가 받을지 모르는 공개 서비스라면 이 성질을 지키는 쪽이 낫습니다. 보내는 쪽과 받는 쪽을 한 조직이 함께 만드는 내부 통신이라면 미리 맞춘 약속으로 메시지를 줄이기도 합니다.

상세

택배 상자에 「깨지기 쉬움」과 「냉장 보관」 스티커가 붙어 있다고 해 봅시다. 그 상자를 처음 본 배송 기사도 어떻게 다룰지 압니다. 보낸 사람에게 전화해 물어볼 필요도, 상자를 열어 볼 필요도 없습니다.

메시지를 이렇게 만드는 것이 자기 서술적 메시지입니다. 메시지는 프로그램끼리 주고받는 요청이나 응답 한 덩이입니다. 받는 쪽이 그 메시지를 해석하는 데 필요한 정보가 메시지 안에 다 들어 있으면 그 메시지는 자기 서술적입니다.

이 성질은 REST(Representational State Transfer, 표현 상태 전이)에서 나온 말입니다. REST 는 웹 위의 시스템을 지을 때 받아들이는 제약의 묶음입니다. 웹 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)를 설계할 때 흔히 이 묶음을 기준으로 삼습니다.

REST 의 제약 가운데 통일된 인터페이스는 클라이언트와 서버, 그 사이에 끼는 프로그램이 모두 같은 방식으로 말하게 하는 제약입니다. 균일 인터페이스라고도 부릅니다. 메시지가 자기를 서술해야 한다는 것은 이 제약을 이루는 갈래 하나입니다. 메시지가 갖춰야 할 성질을 REST 가 제약으로 요구하는 셈입니다.

메시지 하나로 끝나는 해석

메시지를 받은 프로그램이 일을 하려면 세 가지를 알아야 합니다. 무엇을 해 달라는 요청인지, 본문을 어떤 형식으로 읽어야 하는지, 결과를 어떻게 다뤄야 하는지입니다. 자기 서술적 메시지는 이 셋에 메시지 안에서 답합니다.

반대편은 메시지 밖에서 미리 맞춘 약속에 기대는 방식입니다. 「이 주소로 오는 본문은 우리 팀 위키 3쪽에 적은 형식이다」 같은 약속입니다. 이 약속을 모르는 프로그램은 메시지를 받아도 바이트 뭉치로만 봅니다.

자기 서술적인지를 가르는 물음은 하나입니다. 이 메시지만 손에 쥔 제3의 프로그램이 그것을 올바로 다룰 수 있느냐입니다. 다룰 수 있으면 자기 서술적입니다. 보낸 쪽에게 따로 물어봐야 하면 아닙니다.

HTTP 메시지가 스스로를 밝히는 방법

웹에서 쓰는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 메시지는 이 셋을 맨 앞의 줄과 헤더에 나눠 싣습니다. 헤더는 본문 앞에 붙는 「이름: 값」 꼴의 줄들입니다. 사용자 한 명을 가져오는 요청과 그 응답을 보겠습니다.

http
GET /users/42 HTTP/1.1
Host: api.example.com
Accept: application/json

첫 줄의 GET 은 「이 주소의 자원을 읽어서 달라」는 뜻입니다. 자원은 사용자 한 명이나 주문 하나처럼 주소를 붙여 가리키는 대상입니다. 이 요청에서는 42번 사용자가 자원입니다.

GET 은 읽기만 하는 요청이라 서버의 데이터를 바꾸지 않는다는 약속도 함께 들고 옵니다. 그래서 응답이 안 오면 같은 요청을 다시 보내도 탈이 없다는 것까지 이 낱말 하나로 알 수 있습니다.

Accept 헤더는 받고 싶은 본문 형식을 밝힙니다. 이 요청은 application/json 형식으로 달라고 말하고 있습니다.

http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=600

{"id": 42, "name": "Kim"}

응답 첫 줄의 200 은 요청이 성공했다는 상태 코드입니다. Content-Type 은 본문이 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 형식이라고 알립니다. Cache-Control 은 이 응답을 600초 동안 저장해 두고 다시 써도 된다고 알립니다.

application/json 처럼 본문 형식을 가리키는 이름을 미디어 타입이라고 부릅니다. 받는 쪽은 이 이름을 보고 어느 파서로 본문을 읽을지 고릅니다. 이름표가 없으면 본문을 열어 짐작하는 수밖에 없습니다.

아래 표는 방금 본 두 메시지의 부분이 각각 어느 물음에 답하는지 모은 것입니다. 값의 예 칸에는 같은 부분에 올 수 있는 다른 값도 함께 적었습니다.

메시지의 부분 답하는 물음 값의 예
요청 메서드 무엇을 해 달라는가 · 다시 보내도 되는가 GET(읽기) · PUT(통째로 바꿔 쓰기) · DELETE(지우기)
Content-Type 본문을 어떤 형식으로 읽나 application/json
상태 코드 요청이 어떻게 끝났나 200(성공) · 404(찾는 자원 없음) · 503(서버가 잠시 못 받음)
Cache-Control 저장해 두고 다시 써도 되나 max-age=600(600초 저장) · no-store(저장 금지)

누구나 아는 낱말이어야 하는 까닭

메시지가 자기를 설명해도 그 설명을 받는 쪽이 못 알아들으면 소용이 없습니다. 그래서 설명에 쓰는 낱말은 공개된 표준이 뜻을 정해 둔 것이어야 합니다. GET 과 404 와 application/json 의 뜻은 HTTP 를 다루는 프로그램이면 다 압니다.

반대로 X-Op: 7 같은 헤더를 우리 팀만 쓴다고 해 봅시다. 정보는 메시지 안에 있지만 뜻은 팀 문서에 있습니다. 메시지 밖 약속에 다시 기대게 되므로 자기 서술적이라고 할 수 없습니다.

미디어 타입도 마찬가지입니다. 공개 목록에 등록되어 뜻이 알려진 이름을 써야 처음 보는 프로그램도 알아듣습니다. 자기 서술은 메시지에 무엇을 적느냐만큼 그것을 어떤 말로 적느냐에 달려 있습니다.

중간에 끼는 프로그램이 얻는 것

이 성질이 값을 하는 곳은 클라이언트와 서버 사이에 끼는 프로그램입니다. 프록시는 요청을 대신 받아 서버로 넘겨 주는 프로그램입니다. HTTP 캐시는 응답을 저장해 두었다가 같은 요청에 대신 답합니다. CDN(Content Delivery Network, 콘텐츠 전송 네트워크)은 이런 캐시를 세계 곳곳에 깔아 둔 망입니다.

이 프로그램들은 우리 애플리케이션을 모릅니다. 주문이 무엇이고 사용자가 무엇인지 알 길이 없습니다. 그래도 헤더만 읽고 일을 할 수 있습니다. 아래 그림은 캐시가 앞의 응답을 다루는 순서입니다.

sequenceDiagram
    participant 클라이언트
    participant 캐시
    participant 서버
    클라이언트->>캐시: GET /users/42
    캐시->>서버: GET /users/42
    서버-->>캐시: 200 · Cache-Control max-age=600
    Note over 캐시: 헤더만 읽고 600초 저장
    캐시-->>클라이언트: 200
    클라이언트->>캐시: 5분 뒤 GET /users/42
    Note over 캐시: 600초 안이라 서버에 안 묻는다
    캐시-->>클라이언트: 저장해 둔 응답

캐시는 본문의 뜻을 하나도 읽지 않았습니다. 캐시가 본 것은 둘뿐입니다. 메서드가 GET 이라 저장해도 되는 요청이라는 것과 Cache-Control 이 600초를 허락했다는 것입니다. 서버는 두 번째 요청을 받지도 않습니다.

재시도를 정할 때도 메서드가 쓰입니다. 응답 없이 연결이 끊기면 HTTP 라이브러리는 다시 보낼지 정해야 합니다. 멱등성은 같은 요청을 여러 번 해도 한 번 한 것과 결과가 같은 성질입니다. GET 과 PUT 은 이 성질을 약속하므로 라이브러리가 두 번 보내도 탈이 없습니다.

모니터링은 상태 코드 덕을 봅니다. 서버 쪽 오류를 뜻하는 500번대 상태 코드만 세면 오류율이 나옵니다. 본문을 열어 볼 필요가 없습니다.

자기 서술이 깨지는 모양

흔히 보는 깨진 모양은 모든 요청을 한 주소에 POST 로 보내고 할 일을 본문에 적는 것입니다.

http
POST /api HTTP/1.1
Content-Type: application/json

{"action": "getUser", "id": 42}

이 요청은 읽기입니다. 그런데 메서드는 읽기라는 것을 말하지 않습니다. 대부분의 캐시는 POST 응답을 저장하지 않으므로 같은 사용자를 백 번 읽으면 서버가 백 번 일합니다. 재시도 라이브러리도 이 요청을 다시 보내도 되는지 모릅니다.

응답 쪽의 깨진 모양은 실패해도 상태 코드를 늘 200 으로 보내는 것입니다. 오류는 본문에 {"error": "not found"} 처럼 담습니다. 모니터링은 오류를 하나도 못 셉니다. 클라이언트는 본문을 열어 오류 필드를 찾는 코드를 따로 짜야 합니다.

두 경우 모두 정보는 메시지 안 어딘가에 있습니다. 다만 그 정보를 표준의 낱말이 아니라 우리 팀의 본문 형식으로 적었습니다. 중간의 프로그램이 못 읽는 까닭입니다.

메시지가 밝히지 않는 필드의 뜻

application/json 은 본문이 JSON 문법으로 쓰였다는 것까지만 알려 줍니다. name 필드가 사람 이름인지 상품 이름인지는 말하지 않습니다. 문법은 메시지가 밝혔지만 뜻은 여전히 API 문서에 있습니다.

뜻까지 메시지에 실으려는 방법도 있습니다. 미디어 타입을 application/vnd.example.user+json 처럼 자기 서비스 이름을 붙여 좁힐 수 있습니다. 본문 형식을 정의한 문서로 가는 링크를 응답에 함께 실을 수도 있습니다.

자기 서술은 있다와 없다로 갈리기보다 정도의 문제입니다. 헤더 수준까지는 대부분의 웹 API 가 지킵니다. 필드 하나하나의 뜻까지 메시지가 밝히는 API 는 드뭅니다.

무상태·HATEOAS 와 나뉘는 몫

이 제약 말고도 메시지 하나가 혼자 서게 만드는 제약이 REST 에 둘 더 있습니다. 셋 다 「메시지 하나로 충분하다」를 말하지만 무엇이 필요 없어지는지가 다릅니다.

무상태는 서버가 앞 요청을 기억하지 않게 만드는 제약입니다. 그래서 요청마다 필요한 맥락을 다 싣고 와야 합니다.

응답에 실린 링크를 따라 클라이언트가 다음 행동을 고르게 하는 제약도 있습니다. 이 제약을 HATEOAS(Hypermedia As The Engine Of Application State)라고 부릅니다. 클라이언트는 다음에 부를 주소를 미리 외우지 않고 응답에서 얻습니다. 이렇게 링크를 실은 응답이 이름 앞머리의 하이퍼미디어입니다.

아래 표는 세 제약이 각각 어떤 의존을 끊는지 견줍니다.

제약 메시지가 혼자 서려고 없애는 의존 메시지에 싣는 것
무상태 앞 요청에서 서버가 기억해 둔 맥락 누가 보냈는지(인증 토큰) · 목록의 몇째 쪽을 달라는지
자기 서술적 메시지 메시지 밖에서 미리 맞춘 해석 약속 메서드 · 미디어 타입 · 캐시 지시
HATEOAS 다음에 부를 주소를 미리 아는 것 다음 행동으로 가는 링크

무상태를 지켜도 자기 서술적이지 않을 수 있습니다. 앞의 POST /api 요청은 서버가 아무것도 기억하지 않아도 되지만 메서드가 할 일을 숨깁니다. 두 제약은 서로 다른 의존을 끊습니다.

자기 서술이 치르는 대가

메시지마다 헤더가 붙습니다. 작은 데이터를 자주 주고받으면 본문보다 헤더가 더 클 때도 있습니다. 앞에서 본 응답의 본문 {"id": 42, "name": "Kim"} 은 25바이트입니다. 그 위의 Content-Type: application/json 한 줄만 해도 30바이트입니다.

표준의 낱말로만 말해야 한다는 것도 대가입니다. 주문 취소처럼 애플리케이션에 꼭 맞는 동작은 GET·PUT·DELETE 어느 것에도 깔끔하게 안 맞습니다. 그러면 「취소」를 /orders/42/cancellation 같은 자원으로 따로 세워 POST 로 보내게 됩니다.

미리 맞춘 약속이 싼 때

서비스끼리 내부에서 주고받는 메시지는 다르게 고를 수 있습니다. 보내는 쪽과 받는 쪽을 한 조직이 함께 만들고 함께 배포하면 메시지 밖 약속을 맞추는 비용이 작습니다. 중간에 끼는 캐시도 없을 때가 많습니다.

gRPC 가 이쪽을 고른 예입니다. 메시지는 프로토콜 버퍼라는 이진 형식으로 적습니다. 이 형식은 필드 이름 없이 번호와 값만 싣습니다. 받는 쪽은 미리 받아 둔 스키마 파일이 있어야 그 번호를 해석합니다. 자기 서술을 내주고 메시지 크기와 처리 속도를 얻은 것입니다.

누가 받을지 모르는 공개 API 에서는 저울이 반대로 기웁니다. 처음 보는 클라이언트와 중간의 캐시가 따로 묻지 않고 메시지를 다룰 수 있어야 하기 때문입니다.

관련 항목

자기 서술적 메시지가 속하는 상위 제약

REST · 통일된 인터페이스 · 아키텍처 양식 · API 설계 · 자원 지향 설계

REST 가 함께 거는 다른 제약

자원 식별 · 표현 · HATEOAS · 하이퍼미디어 · 무상태 · 클라이언트-서버 · 계층 시스템 · 캐시 가능성 · 코드 온 디맨드

메시지가 자기를 밝히는 데 쓰는 HTTP 요소

HTTP · 요청 메서드 · GET · PUT · POST · 상태 코드 · 헤더 · Content-Type · 미디어 타입 · Cache-Control · Content-Encoding · Accept 헤더 · 콘텐츠 협상

헤더만 읽고 메시지를 다루는 중개자

프록시 · 리버스 프록시 · HTTP 캐시 · 공유 캐시 · CDN · 로드 밸런서 · API 게이트웨이

자기 서술이 받쳐 주는 성질

멱등성 · 안전한 메서드 · 재시도 · 캐싱 · 관측 가능성 · 하위 호환

본문 필드의 뜻까지 알리는 수단

JSON · 벤더 미디어 타입 · JSON 스키마 · OpenAPI · HAL · JSON-LD · 링크 관계

메시지 밖 스키마에 기대는 통신 방식

RPC · gRPC · 프로토콜 버퍼 · Thrift · Avro · GraphQL

다른 이름: self-descriptive message · self-descriptive messages · 자기 기술적 메시지 · 자기 서술 메시지 · 자기 설명적 메시지