사전 견고성 원칙
패턴

견고성 원칙

gabury1고친 사람 github-actions[bot]

견고성 원칙은 서로 다른 사람이 만든 프로그램이 끊기지 않고 주고받게 하려는 설계 지침입니다. 보낼 때는 약속을 엄격하게 지킵니다. 받을 때는 약속에서 조금 어긋난 것도 알아들을 수 있으면 받아 줍니다. 한쪽의 작은 실수 때문에 통신 전체가 멈추는 일을 막으려는 것입니다.

쉽고 빠른 이해

보낼 때는 엄격하게, 받을 때는 너그럽게 하라는 지침입니다. 서버에 온 요청에 모르는 항목이 하나 더 붙어 있다고 합시다. 너그러운 서버는 요청을 거절하지 않습니다. 그 항목만 건너뛰고 나머지를 처리합니다.

같은 규칙 문서를 읽어도 만든 사람마다 해석이 조금씩 갈립니다. 받는 쪽이 작은 어긋남마다 통신을 끊으면 서로 다른 사람이 만든 프로그램끼리 이어지지 않습니다.

어떻게 도는가:

  1. 보낼 때는 규칙이 허용하는 가장 기본적인 꼴로만 씁니다
  2. 받을 때는 뜻을 하나로 알아들을 수 있으면 받습니다
  3. 뜻이 둘로 읽히는 입력은 받지 않습니다

대가로 틀리게 만든 프로그램이 자기가 틀린 줄 모르고 퍼집니다. 받는 쪽마다 어긋난 입력을 메워 읽는 방식이 다르면 같은 입력을 서로 다르게 읽어 보안 구멍이 되기도 합니다. 그래서 요즘은 보안 검사를 거치는 입력이나 새로 만드는 서비스의 요청에서는 어긋난 입력을 바로 거절하는 쪽을 자주 고릅니다.

상세

편지를 쓸 때는 글씨를 또박또박 씁니다. 받은 편지는 글씨를 조금 흘려 썼어도 끝까지 읽어 냅니다.

이 원칙이 다루는 것은 프로토콜의 구현입니다. 프로토콜은 둘 이상의 프로그램이 무엇을 어떤 순서와 모양으로 주고받을지 정한 약속입니다. 이 약속을 코드로 옮긴 프로그램을 구현이라고 부릅니다.

한 약속에 구현은 여럿입니다. 회사도 언어도 다른 사람들이 같은 문서를 읽고 따로 만듭니다. 문서가 모든 경우를 다 적을 수는 없습니다. 읽는 사람마다 해석도 조금씩 갈립니다.

그래서 같은 약속을 따른다는 두 구현도 막상 붙이면 어긋납니다. 받는 쪽이 어긋남을 만날 때마다 연결을 끊으면 서로 다른 구현끼리 이어지지 않습니다. 서로 다른 구현이 문제없이 함께 도는 성질을 상호운용성이라고 부릅니다. 이 원칙은 끊김을 줄여 상호운용성을 지키려는 지침입니다.

원칙은 영어 한 문장으로 널리 불립니다. "Be conservative in what you do, be liberal in what you accept from others." 내가 하는 일은 보수적으로, 남에게서 받는 것은 너그럽게 하라는 뜻입니다.

인터넷에서 데이터를 빠짐없이 순서대로 나르는 약속이 TCP(Transmission Control Protocol, 전송 제어 프로토콜)입니다. 인터넷 초기에 TCP 의 규칙 문서를 엮던 존 포스텔(Jon Postel)이 이 문장을 적었습니다. 그래서 포스텔의 법칙이라고도 부릅니다.

보내는 쪽의 엄격함

이 소절은 원칙의 앞 절반을 봅니다. 보낼 때 무엇을 지키는지입니다.

보낼 때는 약속이 허용하는 여러 꼴 가운데 가장 기본적이고 흔한 꼴을 고릅니다. 상대가 어떻게 만들어졌는지 모르니 누구라도 알아들을 꼴로 보내는 것입니다.

웹 요청을 주고받는 약속이 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)입니다. HTTP 메시지 앞머리에는 이름과 값으로 부가 정보를 적는 헤더가 붙습니다. 그중 날짜 헤더는 메시지를 만든 시각을 적습니다.

받는 쪽은 날짜를 적는 꼴 셋을 모두 읽어야 합니다. 그중 둘은 예전 구현들이 쓰던 꼴입니다. 보내는 쪽은 Sun, 06 Nov 1994 08:49:37 GMT 꼴 하나로만 씁니다.

약속이 「써도 된다」고만 한 선택 기능도 아껴 씁니다. 상대가 그 기능을 안다고 확인됐을 때만 씁니다. 모르는 상대에게 보내면 상대가 거기서 멈출 수 있기 때문입니다.

받는 쪽의 너그러움

이 소절은 원칙의 뒤 절반을 봅니다. 받는 쪽이 어디까지 받아 주고 어디서 거절하는지입니다.

받는 쪽은 입력이 약속에서 조금 어긋나도 뜻을 알아들을 수 있으면 받습니다. 빈칸이 하나 더 들어갔거나 모르는 항목이 붙어 온 경우가 그렇습니다. 입력을 읽어 뜻을 뽑아내는 코드를 파서라고 부릅니다. 너그러운 파서는 이런 차이를 스스로 메워 읽습니다.

너그러움에도 선이 있습니다. 뜻이 둘로 읽히는 입력은 받지 않습니다. 어느 뜻을 골라도 보낸 쪽이 뜻한 것과 어긋날 수 있기 때문입니다.

flowchart TD
    A["입력이 도착한다"] --> B{"약속대로인가"}
    B -->|예| C["처리한다"]
    B -->|아니오| D{"뜻을 하나로 알아들을 수 있나"}
    D -->|예| E["받아서 처리한다"]
    D -->|아니오| F["거절한다"]

받는 쪽은 입력 하나를 두고 두 번 묻습니다. 약속에서 어긋난 입력도 둘째 물음을 통과하면 받아 줍니다.

모르는 필드를 건너뛰는 서버

이 소절은 백엔드에서 가장 자주 만나는 모습으로 받는 쪽의 너그러움을 봅니다. 클라이언트가 보낸 요청에 서버가 모르는 필드가 들어 있을 때입니다. 필드는 요청 본문, 곧 요청에 실려 온 데이터 안에서 이름과 값이 짝을 이룬 항목 하나입니다.

아래 두 함수는 사용자 정보를 담은 요청 본문을 읽습니다. 본문은 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)으로 오고, 서버가 이를 풀어 만든 파이썬 dict 가 body 입니다. 앞 함수는 아는 필드만 골라 읽습니다. 뒤 함수는 모르는 필드가 하나라도 있으면 거절합니다.

Python
def read_user(body):
    return {"id": body["id"], "name": body["name"]}

def read_user_strict(body):
    if set(body) != {"id", "name"}:
        raise ValueError("unknown field")
    return read_user(body)

이제 보내는 쪽이 새 판을 먼저 내놓아 age 필드를 더해 보낸다고 합시다. 두 함수에 같은 본문을 넣어 봅니다.

Python
new = {"id": 7, "name": "kim", "age": 30}

read_user(new)  # {'id': 7, 'name': 'kim'}
read_user_strict(new)  # ValueError

너그러운 쪽은 age 를 건너뛰고 아는 것만 읽습니다. 엄격한 쪽은 요청 전체를 거절합니다.

새 판을 따로 내보낼 수 있는 까닭

받는 쪽이 모르는 필드를 건너뛰면 보내는 쪽은 받는 쪽을 기다리지 않고 새 필드를 더할 수 있습니다. 받는 쪽은 준비가 됐을 때 그 필드를 읽기 시작하면 됩니다. 옛 판이 새 판의 입력을 받아 내는 이 성질을 상위 호환이라고 부릅니다.

받는 쪽이 엄격하면 양쪽을 한꺼번에 바꿔야 합니다. 서버 한 대와 클라이언트 한 벌이면 어렵지 않습니다. 서로 다른 회사가 만든 수많은 구현이 얽힌 인터넷에서는 한꺼번에 바꾸는 날을 잡을 수 없습니다.

너그러움이 쌓이면 생기는 일

이 소절은 원칙이 오래 쓰일 때 치르는 대가를 봅니다. 받는 쪽의 너그러움이 보내는 쪽의 실수를 가려 주는 데서 시작합니다.

어떤 구현이 약속과 조금 다른 꼴로 보낸다고 합시다. 받는 쪽이 다 받아 주니 보낸 쪽은 오류를 한 번도 보지 못합니다. 틀린 줄 모르니 고칠 까닭도 없습니다.

그 사이 같은 꼴로 보내는 구현이 늘어납니다. 이제 받는 쪽을 새로 만드는 사람은 그 틀린 꼴도 받아야 합니다. 안 받으면 다른 데서는 멀쩡히 돌던 연결이 새 구현에서만 끊깁니다.

flowchart TD
    A["약속과 다른 꼴로 보낸다"] --> B["받는 쪽이 받아 준다"]
    B --> C["보낸 쪽은 틀린 줄 모른다"]
    C --> D["같은 꼴로 보내는 구현이 는다"]
    D --> E["새로 만드는 받는 쪽도 받아야 한다"]
    E --> B

이 고리는 한 바퀴 돌 때마다 틀린 꼴을 더 굳힙니다. 끝내 그 꼴은 문서에 없는데도 모두가 따르는 규칙이 됩니다. 이런 규칙을 사실상 표준이라고 부릅니다.

웹 페이지를 적는 언어인 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)이 이 길을 걸었습니다. 브라우저들은 닫는 태그가 빠지거나 순서가 엉킨 문서도 화면에 그려 주었습니다. 고쳐 읽는 방식이 브라우저마다 달라서 같은 문서가 브라우저마다 다르게 보였습니다. 결국 틀린 문서를 고쳐 읽는 방법까지 표준에 적어 모든 브라우저가 같이 따르게 했습니다.

같은 입력을 둘이 다르게 읽을 때

이 소절은 너그러움이 보안 구멍이 되는 경우를 봅니다. 받는 쪽마다 너그러운 정도가 다를 때 생깁니다.

웹 요청 하나는 서버에 닿기 전에 여러 프로그램을 거치곤 합니다. 앞에는 요청을 받아 검사하고 뒤로 넘기는 리버스 프록시가 섭니다. 뒤에는 요청을 처리하는 애플리케이션 서버가 섭니다. 둘은 같은 요청을 따로 읽습니다.

HTTP 요청에는 본문 길이를 바이트 수로 알리는 Content-Length 헤더가 붙습니다. 받는 쪽은 이 값만큼 읽으면 본문이 끝났다고 봅니다. 그 뒤에 오는 바이트는 다음 요청의 시작으로 읽습니다.

공격자는 이 헤더를 값을 달리해 두 번 붙여 보냅니다. 첫째 값은 본문을 길게, 둘째 값은 짧게 적습니다. 두 프로그램 모두 거절하지 않고 받아 주되, 앞 프로그램은 첫째 값을, 뒤 프로그램은 둘째 값을 믿습니다.

sequenceDiagram
    participant 공격자
    participant P as 리버스 프록시
    participant S as 애플리케이션 서버
    공격자->>P: Content-Length 가 둘인 요청
    Note over P: 첫째 값(긴 쪽)을 믿고 요청 하나로 본다
    P->>S: 검사를 마치고 넘긴다
    Note over S: 둘째 값(짧은 쪽)만큼 읽고 본문을 끝낸다
    Note over S: 남은 바이트를 다음 요청으로 읽는다
    Note over S: 둘째 요청은 검사를 거치지 않았다

서버가 믿은 둘째 값은 본문이 더 일찍 끝난다고 말합니다. 그래서 서버는 남은 바이트를 새 요청의 시작으로 읽습니다. 리버스 프록시의 검사를 안 거친 요청 하나가 뒤로 숨어 들어간 것입니다. 이 공격이 HTTP 요청 스머글링입니다.

그래서 보안 검사를 거치는 입력일수록 너그러움을 줄입니다. 어긋난 입력을 저마다 고쳐 읽게 두지 않고 거절하면 모든 프로그램이 같은 입력을 같은 뜻으로 읽습니다.

오늘날 긋는 선

이 소절은 원칙을 지금 어떻게 적용하는지를 봅니다. 앞의 대가가 알려지면서 받는 쪽의 너그러움을 두 갈래로 나눠 보게 됐습니다.

하나는 약속이 미리 허락한 너그러움입니다. 문서가 「모르는 필드는 건너뛴다」처럼 받는 쪽의 동작을 정해 둔 경우입니다. 그때 건너뛰는 것은 실수를 봐주는 일이 아니라 약속을 지키는 일입니다. 새 판이 끼어들 곳을 이렇게 미리 열어 둔 것을 확장 지점이라고 부릅니다.

다른 하나는 약속에 없는 실수를 봐주는 너그러움입니다. 앞 두 소절의 대가는 대부분 이쪽에서 나옵니다. 이런 너그러움 대신 어긋난 입력을 바로 거절해 문제를 드러내는 태도를 빠른 실패라고 부릅니다. 새로 만드는 프로토콜과 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)에서는 빠른 실패 쪽을 고르는 경우가 많습니다.

어느 쪽으로 기울지는 상황이 정합니다. 아래 표는 흔한 상황 다섯을 받는 쪽의 선택으로 정리합니다.

상황 받는 쪽의 선택 까닭
고칠 수 없는 옛 구현과 이어야 한다 너그럽게 받는다 상대를 바꿀 방법이 없다
문서가 무시하라고 정한 모르는 필드 건너뛴다 새 판을 따로 내보낼 수 있다
새로 만드는 프로토콜과 API 엄격하게 받는다 약속에 없는 어긋남은 틀린 구현이 퍼지기 전에 드러난다
보안 검사를 거치는 입력 엄격하게 받는다 두 프로그램이 다르게 읽으면 구멍이 된다
뜻이 둘로 읽히는 입력 거절한다 어느 뜻을 골라도 누군가와 어긋난다

보내는 쪽의 엄격함은 어느 줄에서도 바뀌지 않습니다. 갈리는 것은 받는 쪽이 어디까지 받아 주느냐뿐입니다.

관련 항목

이 원칙이 지키려는 성질

상호운용성 · 하위 호환 · 상위 호환성 · 양방향 호환성 · 스키마 진화

이 원칙이 적용되는 약속과 형식

프로토콜 · TCP · HTTP · HTML · JSON · 직렬화

받는 쪽의 입력 처리를 이루는 요소

파서 · 필드 · 헤더 · 확장 지점 · 입력 검증

너그러움이 낳는 문제

사실상 표준 · HTTP 요청 스머글링 · 프로토콜 경직화 · 공격 표면 · 취약점

이 원칙과 맞세워지는 설계 지침

빠른 실패 · 방어적 프로그래밍 · 계약에 의한 설계 · 종단 간 원칙

이 원칙을 적고 다듬어 온 표준 기구와 문서

IETF · RFC · 표준화 기구 · 적합성 시험

다른 이름: robustness principle · Postel's law · 포스텔의 법칙 · 포스텔 법칙 · 견고성의 원칙