같은 주소로 요청했는데 사용자에 따라 다른 언어의 응답이 돌아오거나, 어떤 클라이언트에는 압축된 데이터가 다른 클라이언트에는 원본이 돌아오는 일이 있다. 하나의 자원이 여러 모습으로 존재하고, 그중 어느 것을 내어 줄지를 클라이언트와 서버가 맞추기 때문이다. 이 맞춤 과정을 콘텐츠 협상이라고 부른다. 이번 편은 그 협상이 어떤 원리로 이루어지는지를 다룬다.


협상은 겉으로 드러나지 않게 작동하지만, 그 원리를 알면 왜 응답이 상황마다 달라지는지가 이해된다. 클라이언트는 자신이 무엇을 선호하는지를 요청 헤더로 밝히고, 서버는 그 선호를 참고해 여러 표현 중 하나를 골라 응답한다. 이번 편은 협상의 두 방식, 형식과 언어와 압축의 협상, 그리고 이 협상이 캐시와 얽히는 지점을 순서대로 짚는다.

하나의 자원, 여러 표현

콘텐츠 협상의 전제는 하나의 자원이 여러 표현을 가질 수 있다는 것이다. 같은 문서가 여러 언어로 존재할 수 있고, 같은 데이터가 여러 형식으로 제공될 수 있으며, 같은 내용이 압축된 형태와 원본으로 함께 존재할 수 있다. 자원은 추상적인 하나이지만, 그것이 실제로 전달되는 모습은 여럿이다.


이 구분이 협상을 이해하는 열쇠다. 자원은 개념이고 표현은 그 자원이 실제로 담겨 전달되는 형태다. 같은 자원을 가리키는 하나의 주소가 있고, 그 뒤에 여러 표현이 준비되어 있으며, 요청의 맥락에 따라 그중 하나가 선택되어 돌아온다. 주소는 하나인데 응답이 달라지는 이유가 여기에 있다.


여러 표현을 하나의 주소로 묶는 것은 설계상 이점이 크다. 언어마다 다른 주소를 만들면 링크가 복잡해지고 공유가 번거로워지는데, 하나의 주소로 통일하고 협상으로 언어를 고르면 그 문제가 사라진다. 사용자는 같은 주소를 쓰면서도 자신에게 맞는 표현을 받는다. 주소의 단순함과 응답의 맞춤을 동시에 얻는 셈이다.


물론 표현마다 별도의 주소를 두는 방식도 있다. 언어별로 다른 경로를 만들어 명시적으로 구분하는 방식이다. 이 방식은 각 표현이 고유한 주소를 가지므로 공유와 색인에 유리하다. 협상으로 고르는 방식과 명시적 주소로 나누는 방식은 각각 장단이 있어, 서비스의 성격에 따라 선택한다.


자원과 표현을 나누어 보는 관점은 협상을 넘어 시스템 설계 전반에 도움이 된다. 주소는 변하지 않는 자원의 정체성을 가리키고, 그 뒤에서 실제로 오가는 것은 상황에 맞게 달라지는 표현이다. 이 둘을 구분해 두면, 자원의 정체성은 안정적으로 유지하면서도 표현은 필요에 따라 유연하게 바꿀 수 있다. 협상은 바로 이 유연성을 규약 차원에서 뒷받침하는 장치다. 하나의 개념에 여러 얼굴을 부여하는 이 발상이 웹의 확장성을 떠받친다.

협상의 두 방식

협상은 크게 두 방식으로 이루어진다. 하나는 서버가 클라이언트의 선호를 보고 대신 골라 주는 방식이고, 다른 하나는 서버가 선택지를 제시하고 클라이언트가 고르게 하는 방식이다. 실무에서 압도적으로 많이 쓰이는 것은 서버가 골라 주는 방식이다.


서버가 골라 주는 방식에서는 클라이언트가 요청 헤더에 자신의 선호를 담는다. 어떤 형식을 원하는지, 어떤 언어를 선호하는지, 어떤 압축을 받을 수 있는지를 헤더로 밝히면, 서버는 그 선호와 자신이 가진 표현을 견주어 가장 적합한 것을 골라 응답한다. 클라이언트는 결과만 받을 뿐 선택 과정에는 개입하지 않는다.


이 방식의 장점은 한 번의 요청으로 협상이 끝난다는 것이다. 클라이언트가 선호를 밝히고 서버가 고르는 일이 한 왕복 안에서 이루어지므로 빠르다. 대부분의 브라우저가 요청마다 자신의 선호를 자동으로 헤더에 담기 때문에, 개발자가 따로 신경 쓰지 않아도 협상이 자연스럽게 작동한다.


단점은 서버의 선택이 항상 사용자의 뜻과 맞지는 않는다는 점이다. 헤더에 담긴 선호는 대략적인 것이라, 서버가 고른 표현이 사용자가 진짜 원한 것과 다를 수 있다. 그래서 협상으로 언어를 고르되, 사용자가 직접 언어를 바꿀 수 있는 수단을 함께 두는 것이 좋다. 자동 선택과 수동 선택을 겹쳐 두면 협상의 빈틈을 메울 수 있다.

형식을 맞추는 협상

가장 기본적인 협상은 응답의 형식을 맞추는 것이다. 클라이언트는 Accept 헤더에 자신이 받을 수 있는 형식을 나열한다. 문서를 원하는지 데이터 구조를 원하는지, 어떤 종류의 이미지를 받을 수 있는지를 이 헤더로 밝힌다. 서버는 그 목록과 자신이 제공할 수 있는 형식을 견주어 하나를 고른다.


이 헤더에는 선호의 정도를 함께 표현할 수 있다. 여러 형식을 나열하면서 각각에 우선순위를 매기면, 서버는 그 순위를 참고해 클라이언트가 더 원하는 형식을 우선한다. 예를 들어 데이터 구조를 가장 원하고 그다음으로 문서를 받아도 된다는 선호를 하나의 헤더에 담을 수 있다. 서버는 이 우선순위를 존중해 선택한다.


형식 협상은 하나의 주소가 여러 클라이언트를 상대할 때 유용하다. 같은 데이터를 브라우저에는 화면용 문서로, 프로그램에는 처리하기 좋은 데이터 구조로 내어 줄 수 있다. 각 클라이언트가 원하는 형식을 헤더로 밝히면, 서버는 같은 자원을 상대에 맞는 형식으로 변환해 응답한다. 하나의 자원이 여러 소비자를 동시에 만족시키는 것이다.


다만 서버가 클라이언트가 원하는 형식을 제공할 수 없는 경우도 있다. 이때 서버는 제공 불가를 알리는 응답을 돌려주거나, 우선순위가 낮더라도 제공 가능한 형식으로 대신 응답한다. 어느 쪽으로 처리할지는 설계의 판단이다. 협상이 늘 성공하는 것은 아니라는 점을 염두에 두어야 한다.

언어를 맞추는 협상

여러 언어로 제공되는 자원에서는 언어 협상이 이루어진다. 클라이언트는 Accept-Language 헤더에 선호하는 언어를 우선순위와 함께 나열한다. 브라우저는 대개 사용자의 설정에 따라 이 헤더를 자동으로 구성한다. 서버는 이 선호와 자신이 가진 언어 표현을 견주어 가장 적합한 언어로 응답한다.


언어 협상은 사용자 경험에 직접 영향을 준다. 사용자가 같은 주소에 접속해도 자신의 언어로 된 내용을 받으면 훨씬 편하다. 협상이 잘 작동하면 사용자는 언어를 따로 고르지 않아도 익숙한 언어의 화면을 만난다. 국제적인 서비스에서 언어 협상은 첫인상을 좌우하는 요소다.


그러나 헤더의 언어 선호가 항상 정확하지는 않다. 사용자가 외국에서 접속하거나 기기의 언어 설정과 실제 선호가 다를 때, 협상이 엉뚱한 언어를 고를 수 있다. 그래서 자동 협상만 믿기보다, 사용자가 언제든 언어를 직접 바꿀 수 있는 장치를 함께 제공해야 한다. 그 선택은 이후에도 유지되도록 기억해 두는 것이 좋다.


언어 협상과 색인의 관계도 고려할 지점이다. 하나의 주소가 여러 언어로 응답하면, 검색 엔진이 각 언어 표현을 어떻게 색인할지가 모호해진다. 그래서 색인이 중요한 서비스에서는 언어별로 별도의 주소를 두고 서로의 관계를 명시하는 방식을 함께 쓴다. 협상과 명시적 주소를 조합해 사용자 편의와 색인을 모두 챙기는 것이다.

압축 방식의 협상

전송량을 줄이기 위한 압축도 협상의 대상이다. 클라이언트는 Accept-Encoding 헤더에 자신이 풀 수 있는 압축 방식을 나열한다. 서버는 그중 하나로 본문을 압축해 보내고, 어떤 방식으로 압축했는지를 응답 헤더로 알린다. 클라이언트는 그 방식으로 압축을 풀어 원래 내용을 복원한다.


압축 협상의 이득은 크다. 문서나 데이터처럼 반복이 많은 내용은 압축하면 크기가 크게 줄어, 전송이 빨라지고 네트워크 비용이 절감된다. 대부분의 브라우저가 압축을 받을 수 있음을 자동으로 밝히므로, 서버가 압축을 지원하기만 하면 이 이득을 손쉽게 얻는다. 성능 개선의 기본기 중 하나다.


다만 이미 압축된 형식에는 압축을 다시 적용해도 이득이 없다. 이미 압축된 이미지나 영상을 또 압축하면 크기가 거의 줄지 않으면서 처리 비용만 든다. 그래서 서버는 압축이 효과적인 형식에만 압축을 적용하고, 이미 압축된 형식은 그대로 보낸다. 무엇을 압축할지 가리는 판단이 필요하다.


압축 방식도 여러 종류가 있고 효율과 속도에서 차이가 난다. 어떤 방식은 더 작게 압축하지만 처리에 시간이 더 걸리고, 어떤 방식은 압축률은 낮아도 빠르다. 서버는 지원하는 방식 중 상황에 맞는 것을 골라 쓴다. 클라이언트가 여러 방식을 받을 수 있다고 밝히면, 서버가 그중 적절한 것을 선택한다.

Vary 헤더와 캐시

협상은 캐시와 얽히면서 미묘한 문제를 낳는다. 같은 주소인데 요청에 따라 응답이 달라지므로, 캐시가 한 표현을 저장했다가 다른 선호를 가진 요청에 잘못 내어 줄 수 있다. 한국어를 원한 사용자에게 앞서 저장된 영어 응답이 돌아가는 식이다. 이 문제를 막는 것이 Vary 헤더다.


Vary 헤더는 이 응답이 어떤 요청 헤더에 따라 달라지는지를 캐시에 알린다. 언어에 따라 응답이 달라진다면 언어 헤더를, 압축 방식에 따라 달라진다면 압축 헤더를 이 Vary 에 명시한다. 캐시는 이 정보를 보고, 그 헤더가 다른 요청에는 저장된 표현을 재사용하지 않고 서버에 다시 묻는다.


이 헤더를 빠뜨리면 협상과 캐시가 충돌한다. 캐시가 협상의 결과를 구분하지 못한 채 하나의 표현을 모두에게 내어 주기 때문이다. 그래서 협상하는 응답에는 반드시 그 협상의 기준이 된 헤더를 Vary 에 담아야 한다. 협상과 캐시를 함께 쓸 때 이 헤더는 선택이 아니라 필수다.


반대로 Vary 에 너무 많은 헤더를 담으면 캐시의 효율이 떨어진다. 응답이 달라지는 기준이 많아질수록 캐시가 저장해야 할 표현의 가짓수가 늘고, 같은 표현이 재사용될 확률은 줄어든다. 그래서 실제로 응답을 달라지게 하는 헤더만 정확히 담아야 한다. 협상의 정확성과 캐시의 효율 사이에서 균형을 잡는 것이다.

협상 설계의 실무

협상을 설계할 때 먼저 정할 것은 어떤 축으로 협상할지다. 형식으로 협상할지, 언어로 협상할지, 압축으로 협상할지를 자원의 성격에 따라 정한다. 모든 자원이 모든 축으로 협상할 필요는 없다. 협상이 필요한 축만 열어 두는 것이 단순하고 견고하다.


협상의 자동 선택에는 언제나 수동 선택을 보완으로 두는 것이 좋다. 헤더 기반의 자동 협상은 편리하지만 사용자의 실제 뜻과 어긋날 수 있으므로, 사용자가 직접 형식이나 언어를 고를 수 있는 길을 함께 열어 둔다. 자동으로 맞추되 어긋나면 사용자가 바로잡을 수 있게 하는 것이 안전한 설계다.


협상하는 응답에는 Vary 헤더를 정확히 다는 것을 잊지 말아야 한다. 협상의 이득을 캐시가 무너뜨리지 않도록, 응답이 달라지는 기준을 캐시에 정확히 알려야 한다. 협상과 캐시는 각각 강력하지만 함께 쓸 때는 이 헤더로 조율해야 서로를 방해하지 않는다.


협상은 잘 보이지 않는 만큼 문제도 잘 보이지 않는다. 응답이 예상과 다른 형식이나 언어로 돌아올 때, 원인을 코드에서 찾다가 실제로는 협상 헤더에 있었던 경우가 적지 않다. 그래서 응답이 상황마다 달라지는 현상을 만나면, 요청 헤더의 선호와 응답의 표현을 나란히 놓고 협상이 어떻게 이루어졌는지를 먼저 살펴야 한다. 협상의 존재를 알고 있는 것만으로도 이런 문제의 원인을 훨씬 빠르게 짚을 수 있다. 보이지 않는 규칙일수록 그 존재를 아는 사람과 모르는 사람의 진단 속도 차이가 크다.


이번 편은 하나의 자원이 여러 표현을 가진다는 전제에서 출발해 협상의 두 방식, 형식과 언어와 압축의 협상, 그리고 캐시와의 조율을 다루었다. 지금까지 요청과 응답의 구조부터 협상까지 규약의 구성 요소를 훑었다면, 다음 편에서는 이 요소들을 엮어 자원 중심으로 시스템을 설계하는 원칙인 레스트를 살펴본다.