Hello World

Content Negotiation 본문

Back-End/좋은글

Content Negotiation

EnterKey 2016. 1. 17. 22:25
반응형

아파치는 HTTP/1.1 규약에 기술된 내용협상(content negotiation)을 지원한다. 내용협상은 media type, 언어, 문자집합, 인코딩 등에 대해 브라우저가 제공한 선호도에 따라 자원의 가장 적합한 표현을 선택한다. 또 불완전한 협상 정보를 보내는 브라우저의 요청을 지능적으로 처리하는 기능도 있다.

기본적으로 컴파일되는 mod_negotiation 모듈이 내용협상 기능을 제공한다.

top

내용협상에 대해

자원은 여러 다른 표현을 가질 수 있다. 예를 들어, 다른 언어나 다른 media type 혹은 둘 모두가 다른 표현들이 있을 수 있다. 가장 적당한 표현을 선택하는 한가지 방법은 사용자에게 목록 페이지를 보여주고 선택하게 하는 것이다. 그러나 서버가 자동으로 선택하는 것도 가능하다. 이는 브라우저가 요청의 일부로 그들이 선호하는 표현에 대한 정보를 보내기때문에 가능하다. 예를 들어, 브라우저는 가능한한 불어로, 그러나 없다면 영어로 정보를 보고싶다고 알려줄 수 있다. 브라우저는 요청의 헤더로 그들의 기호를 나타낸다. 오직 불어로된 표현만을 요청한다면 브라우저는 다음과 같이 보낸다.

Accept-Language: fr

이런 기호는 표현이 언어별로 다를 경우에만 고려된다.

다음은 더 복잡한 요청의 예로 브라우저가 불어와 영어를 받을 수 있지만, 불어를 더 선호하고, 여러 media type을 받을 수 있지만, 일반 텍스트 보다는 HTML, 다른 media type 보다는 GIF와 JPEG을 선호한다고 알려준다.

Accept-Language: fr; q=1.0, en; q=0.5
Accept: text/html; q=1.0, text/*; q=0.8, image/gif; q=0.6, image/jpeg; q=0.6, image/*; q=0.5, */*; q=0.1

아파치는 HTTP/1.1 규약에 정의된 '서버 주도(server driven)' 내용협상을 지원한다. 아파치는 AcceptAccept-LanguageAccept-CharsetAccept-Encoding 요청 헤더를 모두 지원한다. 또, 아파치는 RFC 2295와 RFC 2296에 정의된 실험적인 내용협상인 '자연스러운(transparent)' 요청 헤더도 지원한다. 그러나 이 RFC에 정의된 '기능 협상(feature negotiation)'은 지원하지 않는다.

자원(resource)은 (RFC 2396) URI로 구별하는 개념적인 존재다. 아파치와 같은 웹서버는 자원의 표현(representations)을 제공한다. 표현은 지정된 media type, 문자집합, 인코딩 등을 가진 바이트들로 되있다. 자원은 여러 표현과 (때로는 없을 수도 있다) 연관된다. 자원에 여러 표현이 있다면 자원을 협상가능하다고(negotiable) 부르며, 이때 각 표현을 변형(variant)이라고 한다. 협상가능한 자원의 변형 종류를 협상의 범위(dimension)라고 한다.

top

아파치의 협상

자원을 협상하기위해 서버는 각 변형에 대한 정보가 필요하다. 다음 두가지 방법중 하나로 정보를 얻는다:

  • 변형을 담은 파일들을 직접 열거한 type map을 (예를 들어*.var 파일) 사용하거나,
  • 직접 지정하지않아도 서버가 파일명에서 규칙을 찾아서 결과를 선택하는 'MultiViews'를 사용한다.

type-map 파일 사용하기

type map은 type-map이란 핸들러와 연결된 (혹은 이전 아파치 설정과 호환을 위해 MIME type이 application/x-type-map인) 문서다. 이 기능을 사용하려면 설정에서 type-map 핸들러에 대한 파일 확장자를 지정해야 한다. 서버 설정파일에 다음과 같이 설정하는 것이 좋다.

AddHandler type-map .var

Type map 파일은 해당하는 자원과 이름이 같아야 하고, 각 변형에 대한 항목이 있어야 한다. 항목은 여러 HTTP형식 헤더 줄로 구성된다. 변형에 대한 각각의 항목들은 빈줄로 구분한다. 항목안에서 빈줄을 사용할 수 없다. (이렇게 할 필요가 없고, 있어도 무시하지만) 여러 항목이 공통으로 가지고 있는 내용으로 map 파일을 시작하는 것이 보통이다. 다음은 map 파일 예다. 이 파일의 이름은 foo.var로, foo라는 자원을 설명한다.

URI: foo

URI: foo.en.html
Content-type: text/html
Content-language: en

URI: foo.fr.de.html
Content-type: text/html;charset=iso-8859-2
Content-language: fr, de

typemap 파일이 파일명 확장자 보다, 심지어 Multiviews를 사용하여도, 우선권을 가짐을 주의하라. 변형이 서로 다른 품질을 가진다면, 다음과 같이 (JPEG, GIF, ASCII-art에 해당하는) media type에 "qs" 파라미터로 품질(source quality)을 표시할 수 있다:

URI: foo

URI: foo.jpeg
Content-type: image/jpeg; qs=0.8

URI: foo.gif
Content-type: image/gif; qs=0.5

URI: foo.txt
Content-type: text/plain; qs=0.01

qs 값은 0.000에서 1.000 사이다. qs 값이 0.000인 변형은 절대 선택되지 않음을 주의하라. 'qs' 값이 없는 변형은 1.0으로 취급된다. qs 값은 클라이언트의 능력과는 관계없이 다른 변형들과 비교하여 그 변형의 상대적인 '품질'을 나타낸다. 예를 들어, 사진을 나타내려는 경우 JPEG 파일이 ASCII 파일보다는 항상 높은 품질을 가진다. 그러나 자원이 원래 ASCII art였다면 ASCII 표현이 JPEG 표현보다 더 높은 품질을 가질 수 있다. 그러므로 어떤 변형의 qs 값은 표현하려는 자원의 성질에 따라 다르다.

지원하는 모든 헤더 목록은 mod_negotation typemap 문서를 참고하라.

Multiviews

MultiViews는 디렉토리별 옵션이므로, httpd.conf의 <Directory><Location><Files> 섹션 혹은 (AllowOverride가 적절히 설정되었다면) .htaccess 파일의 Options 지시어에 설정할 수 있다. Options All은 MultiViews를 포함하지않음을 주의하라. 따로 직접 써줘야 한다.

MultiViews를 사용하면 다음과 같은 일이 일어난다: 서버가 /some/dir/foo에 대한 요청을 받고 /some/dir/foo에 MultiViews가 동작하며 /some/dir/foo가 존재하지 않을 경우, 서버는 디렉토리에서 이름이 foo.*인 파일들을 모든 포함하는 가상의 type map을 만든다. 클라이언트가 요청한 media type과 content-encoding을 가지고 이중에 가장 적합한 것을 선택한다.

MultiViews는 서버가 디렉토리를 참조할때 파일을 찾는 DirectoryIndex 지시어에도 적용된다. 설정파일이 다음과 같다면,

DirectoryIndex index

index.html과 index.html3이 모두 있다면 서버는 이둘 중에 하나를 결정한다. 둘 모두 없고 index.cgi가 있다면, 서버는 그것을 실행한다.

디렉토리를 읽을때 파일중 하나가 Charset, Content-Type, Language, Encoding를 판단하는 mod_mime이 모르는 확장자를 가진다면, 결과는 MultiViewsMatch 지시어 설정에 달렷다. 이 지시어는 핸들러, 필터, 다른 확장형들이 MultiViews 협상에 참여할지 여부를 결정한다.

top

협상방법

아파치가 type-map 파일이나 디렉토리에 있는 파일명들로 주어진 자원에 대한 변형 목록을 얻게되면 '최적의' 변형을 결정하기위해 두 방법중 하나를 사용한다. 아파치 내용협상 기능을 사용하기위해 정확히 협상이 어떻게 일어나는지 자세히 알 필요는 없다. 그러나 궁금한 사람을 위해 이 방법을 설명한다.

두가지 협상방법이 있다:

  1. 아파치 알고리즘을 사용하여 서버가 주도하는 협상은 일반적인 경우에 사용한다. 아파치 알고리즘은 아래서 자세히 설명한다. 이 알고리즘을 사용하면 아파치는 더 나은 결과를 얻기위해 종종 특정 범위의 품질계수(quality factor)를 '조작한다'. 아파치가 품질계수를 조작하는 방법은 아래서 자세히 설명한다.
  2. 자연스러운(Transparent) 내용협상은 브라우저가 RFC 2295에 정의된 방법으로 요청할 경우에만 사용한다. 이 협상방법은 '최적의' 변형을 결정할 권한을 브라우저에게 부여한다. 그래서 결과는 브라우저의 알고리즘에 달렸다. 자연스러운 협상과정중에 브라우저는 아파치에게 RFC 2296에 정의된 '원격 변형선택 알고리즘(remote variant selection algorithm)'을 요청할 수 있다.

협상의 범위

범위설명
Media Type브라우저는 Accept 헤더로 선호를 나타낸다. 각 항목은 품질계수를 가질 수 있다. 변형의 설명도 품질계수를 ("qs" 파라미터) 가질 수 있다.
Language브라우저는 Accept-Language 헤더로 선호를 나타낸다. 각 항목은 품질계수를 가질 수 있다. 변형은 여러 언어를 가질 (혹은 아무 언어도 없을) 수 있다.
Encoding브라우저는 Accept-Encoding 헤더로 선호를 나타낸다. 각 항목은 품질계수를 가질 수 있다.
Charset브라우저는 Accept-Charset 헤더로 선호를 나타낸다. 각 항목은 품질계수를 가질 수 있다. 변형은 media type의 파라미터로 문자집합을 나타낼 수 있다.

아파치 협상 알고리즘

아파치는 브라우저에게 보낼 '최적의' 변형을 (있다면) 선택하기위해 아래 알고리즘을 사용한다. 이 알고리즘은 변경할 수 없다. 다음와 같이 동작한다:

  1. 먼저, 협상의 각 범위에 대해 해당하는 Accept* 헤더를 검사하고, 각 변형에 품질값을 매긴다. 어떤 범위의 Accept* 헤더가 받아들이지 않는 변형은 후보에서 제외한다. 어떤 변형도 남지않으면 4 단계로 간다.
  2. 후보에서 하나씩 제외하여 '최적의' 변형을 찾는다. 다음 각 검사는 순서대로 일어난다. 각 검사에서 선택되지않은 변형은 제외된다. 각 검사후 한 변형만 남으면 이를 최적의 변형으로 선택하고 3 단계로 간다. 여러 변형이 남으면 다음 검사를 진행한다.
    1. Accept 헤더의 품질계수와 변형의 media type에 대한 품질값을 곱하여 가장 높은 값을 가진 변형을 선택한다.
    2. 가장 높은 언어(language) 품질계수를 가진 변형을 선택한다.
    3. Accept-Language 헤더에 (있다면) 나온 언어의 순서 혹은 LanguagePriority 지시어에 (있다면) 나온 언어의 순서를 가지고 가장 적합한 언어를 가진 변형을 선택한다.
    4. 가장 높은 (text/html media type의 버전을 나타내는) 'level' media 파라미터를 가진 변형을 선택한다.
    5. Accept-Charset 헤더를 가지고 가장 적합한 charset media 파라미터를 가진 변형을 찾는다. 헤더가 없다면 ISO-8859-1 문자집합을 가장 선호한다. text/* media type을 가지지만 명시적으로 특정 문자집합과 연결되지않은 변형은 ISO-8859-1로 가정한다.
    6. ISO-8859-1이 아닌 charset media 파라미터를 가진 변형들을 선택한다. 그런 변형이 없다면, 대신 모든 변형을 선택한다.
    7. 가장 적합한 인코딩을 가진 변형을 선택한다. user-agent에 적합한 인코딩을 가진 변형이 있다면 그 변형만을 선택한다. 그렇지않고 인코딩된 변형과 인코딩안된 변형이 같이 있다면 인코딩안됨 변형만을 선택한다. 변형이 모두 인코딩되었거나 모두 인코딩안된 경우 모든 변형을 선택한다.
    8. content length가 가장 적은 변형을 선택한다.
    9. 남은 것중 첫번재 변형을 선택한다. 이는 type-map 파일의 앞에 나왔거나, 디렉토리에서 변형을 읽은 경우 파일명을 ASCII 코드 순서로 하여 앞에 나오는 것이다.
  3. 이제 알고리즘이 '최적의' 변형을 선택했다. 이것을 응답으로 보낸다. HTTP 응답 헤더 Vary는 협상의 범위를 나타내게 된다. (브라우저와 캐쉬는 자원을 캐쉬할때 이 정보를 사용할 수 있다.) 끝.
  4. 이 단계에 도달했다면 (모두 브라우저가 받지못하기 때문에) 어떤 변형도 선택이 안된 경우다. ("No acceptable representation"를 뜻하는) 상태 406과 내용으로 사용가능한 변형의 목록을 담은 HTML 문서를 응답을 보낸다. 또, HTML Vary 헤더는 변형의 범위를 나타낸다.
top

품질계수 조작하기

아파치는 종종 위의 아파치 협상 알고리즘을 엄격히 지키지않고 품질계수를 변경한다. 이유는 완전하고 정확한 정보를 보내지않는 브라우저에게 (알고리즘의) 더 나은 결과를 보내기 위해서다. 널리 쓰이는 브라우저중 일부는 자주 잘못된 변형을 선택하도록 Accept 헤더를 보낸다. 브라우저가 완전하고 올바른 정보를 보낸다면, 조작을 하지않는다.

Media Type과 와일드카드

Accept: 요청 헤더는 media type에 대한 선호를 나타낸다. 또, *는 어떤 문자열이라도 가능하기때문에 "image/*"나 "*/*" 같이 '와일드카드' media type을 사용할 수도 있다. 그래서 다음과 같은 요청은:

Accept: image/*, */*

"image/"로 시작하는 어떤 type과 다른 어떤 type도 가능함을 의미한다. 어떤 브라우저는 자신이 실제로 다룰 수 있는 type에 추가로 와일드카드를 보낸다. 예를 들면:

Accept: text/html, text/plain, image/gif, image/jpeg, */*

이유는 직접 열거한 type을 선호하지만 다른 표현이 있다면 그것도 괜찮음을 나타내기 위해서다. 브라우저가 실제로 원한 것은 다음과 같이 명시적으로 품질값을 사용한 것이다.

Accept: text/html, text/plain, image/gif, image/jpeg, */*; q=0.01

직접 열거한 type은 품질계수가 없어서 기본값인 (가장 높은) 1.0을 가진다. 와일드카드 */*는 낮은 선호도 0.01을 가지므로 직접 열거한 type에 맞는 변형이 없는 경우에만 다른 type들이 사용된다.

Accept: 헤더에 q 계수가 전혀 없고 "*/*"가 있다면, 아파치는 바람직한 행동을 위해 q 값으로 0.01을 지정한다. 또, "type/*" 형태의 와일드카드에는 ("*/*"보다는 더 선호하도록) 0.02를 지정한다. Accept:헤더에서 q 계수를 가지는 media type이 있다면 이런 특별한 값을 추가하지 않는다. 그래서 명시적인 정보를 보내는 브라우저의 요청은 요청한데로 처리한다.

언어(language) 협상의 예외

아파치 2.0은 언어 협상이 실패한 경우 부드럽게 복구하기위해 협상 알고리즘에 새로 예외를 몇개 추가했다.

클라이언트가 서버에 페이지를 요청했을때 서버가 브라우저가 보낸 Accept-language에 맞는 페이지를 단 한개만 찾으면 문제가 없지만, 그러지 않은 경우 서버는 클라이언트에게 "No Acceptable Variant"나 "Multiple Choices" 응답을 보낸다. 이런 오류문을 피하기위해 이 경우 Accept-language를 무시하고 클라이언트의 요청에 명확히 맞지는 않지만 문서를 보내도록 아파치를 설정할 수 있다.ForceLanguagePriority 지시어는 서버가 이런 오류문중 하나 혹은 둘다를 무시하고 LanguagePriority 지시어로 판단하도록 한다.

또, 서버는 맞는 언어를 못찾은 경우 부모언어를 찾을 수도 있다. 예를 들어 클라이언트가 영국영어를 뜻하는 en-GB 언어로 문서를 요청한 경우, HTTP/1.1 표준에 따르면 서버는 en으로만 표시된 문서를 일반적으로 선택하지 못한다. (그래서 영국영어를 이해하는 독자가 일반적인 영어도 이해할 수 있으므로 Accept-Language 헤더에 en-GB만 포함하고 en을 포함하지않으면 거의 확실히 잘못된 설정임을 유의하라. 불행히도 현재 많은 클라이언트들은 이런 식으로 기본설정되있다.) 다른 언어를 찾지 못하여 서버가 "No Acceptable Variants" 오류를 보내거나 LanguagePriority로 돌아가야 한다면, 서버는 하위언어 규약을 무시하고 en-GB를 en 문서에 대응한다. 암묵적으로 아파치는 부모언어를 매우 낮은 품질값으로 클라이언트의 허용언어 목록에 추가한다. 그러나 클라이언트가 "en-GB; q=0.9, fr; q=0.8"을 요청하고 서버에 "en"과 "fr" 문서가 있다면, "fr" 문서가 선택됨을 주의하라. 이는 HTTP/1.1 표준을 지키고, 올바로 설정된 클라이언트와 효율적으로 동작하기위함이다.

사용자가 선호하는 언어를 알아내기위한 (쿠키나 특별한 URL-경로 같은) 고급 기법을 지원하기위해 아파치 2.0.47부터 mod_negotiation은 prefer-language라는 환경변수를 인식한다. 이 환경변수가 존재하고 적절한 언어태그를 포함한다면, mod_negotiation은 해당하는 변형을 선택하려고 시도한다. 그런 변형이 없다면 일반적인 협상과정을 시작한다.

예제

SetEnvIf Cookie "language=(.+)" prefer-language=₩1,217.67

top

자연스러운(transparent) 내용협상의 확장

아파치는 다음과 같이 자연스러운 내용확장 프로토콜을 (RFC 2295) 확장한다. 변형 목록의 새로운 {encoding ..}는 특정 content-encoding을 가진 변형만을 지칭한다. RVSA/1.0 알고리즘은 (RFC 2296) 목록에서 인코딩된 변형을 인식할 수 있고, 인코딩이 Accept-Encoding 요청 헤더에 맞는 경우 인코딩된 변형들도 후보로 사용하도록 확장되었다. RVSA/1.0 구현은 최적의 변형을 찾기 전에 계산된 품질계수를 소수점 5자리에서 반올림하지 않는다.

top

하이퍼링크와 이름규칙에 대하여

언어(language) 협상을 사용한다면 파일은 여러 확장자를 가지고 확장자의 순서는 보통 관계없으므로 파일명에 여러 다른 이름규칙을 사용할 수 있다. (자세한 내용은 mod_mime 문서를 참고하라.)

전형적인 파일은 MIME-type 확장자 (예를 들어html), 경우에 따라 encoding 확장자 (예를 들어gz), 파일에 여러 언어 변형이 있는 경우 물론 언어 확장자를 (예를 들어en) 가진다.

예제:

  • foo.en.html
  • foo.html.en
  • foo.en.html.gz

다음은 몇몇 파일명과 그 파일에 대한 유효하고 유효하지않은 하이퍼링크를 보인다:

파일명유효한 하이퍼링크유효하지않은 하이퍼링크
foo.html.enfoo
foo.html
-
foo.en.htmlfoofoo.html
foo.html.en.gzfoo
foo.html
foo.gz
foo.html.gz
foo.en.html.gzfoofoo.html
foo.html.gz
foo.gz
foo.gz.html.enfoo
foo.gz
foo.gz.html
foo.html
foo.html.gz.enfoo
foo.html
foo.html.gz
foo.gz

위 표를 보면 하이퍼링크에 어떤 확장자도 없는 이름을 (예를 들어foo) 항상 사용할 수 있음을 알 수 있다. 이 경우 장점은 문서의 실제 종류를 숨길 수 있어서, 예를 들어 하이러링크 참조를 수정하않고 html파일을 shtml이나 cgi로 변경할 수 있다는 점이다.

계속 하이퍼링크에 MIME-type을 (예를 들어foo.html) 사용하고 싶다면 (encoding 확장자가 있다면 이것도 포함하여) 언어 확장자를 MIME-type 확장자보다 오른쪽에 (예를 들어foo.html.en) 두어야한다.

top

캐쉬에 대하여

캐쉬가 표현을 저장하면 표현과 요청 URL을 연관시킨다. 다음번 그 URL을 요청하면 캐쉬는 저장된 표현을 사용한다. 그러나 서버와 협상이 가능한 자원인 경우 첫번째 요청한 변형만 캐쉬되어 이후 요청은 캐쉬된 잘못된 응답을 얻을 수 있다. 이를 막기위해 아파치는 보통 내용협상후 반환되는 모든 요청에 HTTP/1.0 클라이언트가 캐쉬를 못하도록 표시를 한다. 또, 아파치는 협상한 응답의 캐쉬를 허용하는 HTTP/1.1 프로토콜의 기능을 지원한다.

CacheNegotiatedDocs 지시어는 HTTP/1.0 호환 클라이언트(브라우저 혹은 캐쉬)가 보낸 요청에 대해 협상한 응답을 캐쉬할 수 있게 한다. 이 지시어는 서버나 가상호스트 설정에 사용하며, 아규먼트를 받지않는다. 이 지시어는 HTTP/1.1 클라이언트의 요청과는 관계가 없다.

HTTP/1.1 클라이언트에게 아파치는 응답의 협상 범위를 알려주는 Vary HTTP 응답헤더를 보낸다. 이 정보를 사용하여 다음 요청을 캐쉬된 복사본으로 대체할 수 있는지 판단할 수 있다. 헙상의 범위와 관계없이 캐쉬된 복사본을 권한다면 force-no-vary 환경변수를 설정한다.


반응형

'Back-End > 좋은글' 카테고리의 다른 글

[펌]MVVM 패턴  (0) 2016.01.26
[펌]Lets' Encrypt로 무료로 HTTPS 지원하기  (0) 2016.01.20
[펌]HATEOAS  (0) 2016.01.14
[펌][프로그래머가 알아야 할 보안] HTTPS를 뚫는 자전거 공격  (0) 2016.01.14
Maven 정리  (0) 2016.01.14
Comments