바쁜 개발자들을 위한 REST 논문 요약

REST란 무엇인가? REST는 Representational State Transfer의 줄임말로, 웹을 위한 네트워크 기반 아키텍처 스타일이다. REST는 Roy T. Fielding이 그의 박사학위 논문 “Architectural Styles and the Design of Network-based Software Architectures” 에서 처음 소개하였다.

그의 논문을 읽고 매우 간략히 요약해보았다. 논문의 모든 부분을 동등하게 요약하지는 않았고, REST가 무엇인가를 이해하는데 초점을 맞추어 차별적으로 요약하였다.

논문 요약

1장: Software Architecture

아키텍처 스타일(architectural style)이란, 그 스타일을 따르는 아키텍처가 지켜야 하는 제약조건들의 집합이다.

2장: Network-based Application Architectures

이 논문에서 다루는 아키텍처 스타일의 적용 범위는 네트워크 기반 애플리케이션(Network-based Application)으로 한정된다.

네트워크 기반 소프트웨어는 operation이 네트워크를 경유해서 일어난다는 사실을 사용자에게 감출 필요가 없다. 분산(distributed) 소프트웨어는 감추어야한다.

애플리케이션이기 때문에, OS나 네트워킹 소프트웨어 같은 것은 고려 대상에서 제외된다.

네트워크 기반 애플리케이션 아키텍처의 관심 사항은 다음과 같다.

  • 성능
  • 규모확장성(Scalability)
  • 단순성
  • 수정용이성(Modifiability)
  • 가시성(Visibility)
  • 이식성(Portability)
  • 신뢰성(Reliability)

3장: Network-based Architectural Styles

이 장에서는 Pipe and Filter, Layered-Client-Cache-Stateless-Server, Code on Demand 를 비롯한 여러가지 네트워크 기반 아키텍처 스타일들을 소개한다.

4장: Designing the Web Architecture: Problems and Insights

이 장에서는 웹 아키텍처의 요구사항, 해결해야할 문제를 설명하고 이를 해결하기 위한 접근 방법을 제시한다.

5장: Representational State Transfer (REST)

4장에서 제시한 접근방법의 결과물로 웹을 위한 아키텍처 스타일 “REST”를 소개한다. REST는 3장에서 소개했던 네트워크 기반 아키텍처 스타일들 몇 가지와 추가로 Uniform Interface 스타일을 함께 결합한 하이브리드 스타일이다.

  • Client-Server – 클라이언트-서버 스타일은 사용자 인터페이스에 대한 관심(concern)을 데이터 저장에 대한 관심으로부터 분리함으로써 클라이언트의 이식성과 서버의 규모확장성을 개선한다.
  • Stateless – 클라이언트와 서버의 통신에는 상태가 없어야한다. 모든 요청은 필요한 모든 정보를 담고 있어야한다. 요청 하나만 봐도 바로 뭔지 알 수 있으므로 가시성이 개선되고, task 실패시 복원이 쉬우므로 신뢰성이 개선되며, 상태를 저장할 필요가 없으므로 규모확장성이 개선된다.
  • Cache – 캐시가 가능해야한다. 즉 모든 서버 응답은 캐시 가능한지 그렇지 아닌지 알 수 있어야한다. 호율, 규모확장성, 사용자 입장에서의 성능이 개선된다.
  • Uniform Interface – 구성요소(클라이언트, 서버 등) 사이의 인터페이스는 균일(uniform)해야한다. 인터페이스를 일반화함으로써, 전체 시스템 아키텍처가 단순해지고, 상호작용의 가시성이 개선되며, 구현과 서비스가 분리되므로 독립적인 진화가 가능해진다. 이 스타일은 다음의 네 제약조건으로 이루어진다: identification of resources, manipulation of resources through representation, self-descriptive messages, hypermedia as the engine of application state
  • Layered System – 계층(hierarchical layers)으로 구성이 가능해야하며, 각 레이어에 속한 구성요소는 인접하지 않은 레이어의 구성요소를 볼 수 없어야한다.
  • Code-On-Demand (Optional) – Code-On-Demand가 가능해야한다. 서버가 네트워크를 통해 클라이언트에 프로그램을 전달하면 그 프로그램이 클라이언트에서 실행될 수 있어야한다. (Java applet이나 Javascript 같은 것을 말함) 다만 이 제약조건은 필수는 아니다.

6장: Experience and Evaluation

REST 아키텍처 스타일은 URI, HTTP 등의 웹 표준에 반영되었다.

문제는 Uniform Interface (그리고 다음 포스팅 예고)

우리가 이 논문에서 가장 주목해야 할 부분은 바로, REST를 구성하는 스타일들 중 하나인 “Uniform Interface”다. 이 스타일에 따르면, REST API는 기본 URI와 미디어 타입의 정의만 알면 이용할 수 있어야한다.

예를 들어 Github API가 REST API라면, https://api.github.com 이라는 기본 URI와 application/json의 정의만 알면 API 문서 없이도 Github API 전체가 이용이 가능해야 한다는 말이 된다. 아마 가능하지 않을 것이다. 따라서 Github API는 REST API가 아니다. 아마 세상의 거의 모든 HTTP API가 REST API가 아닐 것이다.

Code-On-Demand를 제외한 모든 스타일을 다 따라야만 REST API이기 때문에, 자신의 API를 REST API라고 부르고 싶다면 이 Uniform Interface 스타일 역시 따라야한다.

과연 Uniform Interface 스타일을 따르는 것은 가능할까? 아니 그 이전에 따르기는 해야 하는 것일까? 그것에 대해서는 다음 포스팅에서 다룰 것이다.

웹 프로그래머를 위한 HTTP 완벽 가이드 읽는 법

HTTP 완벽가이드는 이름 그대로 HTTP를 매우 자세히 다루고 있는 책이다. HTTP를 이해해야 하는 사람은
웹 프로그래머만은 아니므로, 이 책은 웹 프로그래머만을 위해 쓰여진 책이 아니다.
따라서 웹 프로그래머가 이 책의 내용을 모두 다 완전히 이해해야 하는 것은 아니며,
중요도가 장 마다 크게 다르다.

웹 프로그래머의 시간은 유한한데 반해 공부해야 할 내용은 어마어마하게 많으므로,
중요한 것만 읽고 안 읽어도 되는 부분은 그냥 넘어가자.

무엇을 읽고 무엇을 안 읽을 것인가

I. HTTP: 웹의 기초

Part I은 모두 읽는 것이 좋다. 기초가 튼튼해야 이후의 내용을 잘 이해할 수 있다.

1장 “HTTP 개관”은 HTTP에 대해 개략적으로 설명해주므로, 이후의 내용들을
이해하는데 도움이 된다.

2장 “URL과 리소스”와 3장 “HTTP 메시지”는 반드시 읽어야 한다. 이 장들을 읽지
않으면 이후 내용들도 이해하기 어렵다. 특히 3장은 이름대로 HTTP 그 자체를
설명하는 장이다.

4장 “커넥션 관리”도 읽는 것이 좋다. HTTP 기저에서 TCP가 어떻게 동작하는지
설명한다. 읽고 나면 HTTP가 왜 느린지 이해할 수 있게 될 것이다.

II. HTTP 아키텍처

Part II는 유용한 내용이 많다. 특히 5장이 가장 중요하고 그 다음은 7장이 유용하다.

5장 “웹 서버”는 웹 서버가 어떻게 동작하는지 설명한다. 웹 프로그래머라면 반드시
이해해야 할 것이다.

6장 “프락시”도 읽는 것이 좋다. 프락시에 대한 이야기는 이후에도 계속 나오게
된다. 또한 네트워크 엔지니어들과 대화하려면 프락시 정도는 이해하는 것이 좋다.

7장 “캐시”도 읽어 두자. 제목은 캐시지만 캐시 뿐 아니라 조건부 요청(304로
응답하는 그거)도 다룬다. 웹 프로그래머라면 반드시 써먹게 될 것이다. 15장에서도
같은 내용을 다루기는 하지만 7장이 더 자세하다.

8장 “통합점: 게이트웨이, 터널, 릴레이”는 꼭 읽어야 하는 건 아니다. 나중에
궁금해지면 읽어도 별 상관은 없다.

9장 “웹 로봇”은 로봇이나 검색엔진에 관심이 있다면 읽어보자. 웹 서비스를
운영하게 된다면 웹 로봇이 무슨 원리와 규칙으로 동작하는지 궁금해질 것이다. 혹은 그냥 웹에 대한 교양이라는 느낌으로 읽어도 좋다.

10장 “HTTP/2.0″는 HTTP/2가 궁금하다면 읽어보자. HTTP/2의 목적은 성능 개선이니, HTTP/1이 느린 것이 불만인 사람도 읽어보자. HTTP/2가 완성되기 전에 쓴
것이긴 하지만 최신 명세와 크게 다른 점은 없을 것이다.

III. 식별, 인가, 보안

Part III는 13장 빼고는 대체로 유용하다.

11장 “클라이언트 식별과 쿠키”를 읽으면 쿠키에 대해 올바르게 이해할 수 있게 된다.

12장 “기본 인증”도 읽으면 좋다. 기본 인증(Basic Authentication)은 여전히 종종
쓰이기 때문이다. 그리고 매우 쉬워서 읽기도 좋다.

13장 “다이제스트 인증”은 읽을 필요가 없다. 다이제스트 인증 쓰는 것은 거의 본
일이 없다. 이걸 공부해도 써 먹을 일은 아마 없을 것이다. 심지어 내용도 복잡해서
읽어도 이해가 잘 안된다. 여럿이 같이 스터디 중이라면 이 장은 그냥 제끼자.

14장 “보안 HTTP”은 HTTPS를 다루고 있다. 읽는 것이 좋다. 몰라도 HTTPS를 쓸 수는
있겠지만, 왜 HTTPS가 안전한지 이해하고 싶다면 읽는 것이 좋다.

IV. 엔터티, 인코딩, 국제화

파트 I가 HTTP에 대한 기본적인 이해를 위해 필요했다면, 파트 IV는 HTTP를 제대로
쓰기 위해서 필요하다. 여기서 다루는 내용들은 대체로 웹 서버나 웹 프레임워크가
알아서 처리해주지 않아서 웹 프로그래머가 이해해야 하는 것들이 많다. 가급적 모두
읽도록 하자.

그 중에서도 16장 “국제화”에서 다루는 내용은 비단 웹 프로그래밍이나 HTTP에만
적용되는 내용이 아니라 다국어를 다루는 모든 프로그래머가 알아야 할 내용이므로 활용 범위가 매우 넓다.

V. 콘텐츠 발행 및 배포

이 파트의 내용은 선택적으로 필요에 따라 읽으면 된다. 스터디를 하고 있다면 이
파트 전체를 생략해도 괜찮다.

18장 “웹 호스팅”은 웹 서비스 운영을 시작하게 되면 그때 읽어도 무방하다.

19장 “배포 시스템”은 FrontPage와 WebDAV을 다루는데, 지금 FrontPage나 WebDAV을 쓸 일이 없다면 읽지
않아도 무방하다.

20장 “리다이렉션과 부하 균형”은 앞부분만 좀 읽고 넘어가도 된다. 이 장에서
다루고 있는 캐시 배열 라우팅 프로토콜 같은 거 나는 써본 일이 없다. 혹시
네트워크 엔지니어 역할까지 겸하고 있다면 알 필요가 있을지도 모르겠는데, 나는
그런 경험을 해 본 일이 없어서 잘 모르겠다.

21장 “로깅과 사용추적”도 역시 필요에 따라 읽으면 된다. 로그 포맷에 대한 내용은
사용하고 있는 웹 서버의 매뉴얼을 읽어도 충분할 것이고, 사용 추적은 필요하지
않을 수도 있다.

VI. 부록

부록은 대체로 HTTP 명세나 IANA 웹사이트 등에서 찾아볼 수 있는 것들이다. 책이
두꺼워서 들고다니기 무겁다면 잘라내도 좋다. 그냥 인터넷에서 찾아봐도 된다.

세줄 요약

매우 바쁘다면 1-3장만 읽자. 그 정도만 읽어도 큰 도움이 된다.

조금 바쁘다면 1-5, 7, 11, 12, 14, 15, 16, 17장을 읽자.

13, 19장은 관심있는 사람만 읽자.

400 Bad Request와 403 Forbidden의 의미에 대해

HTTP/1.1

HTTP/1.1을 정의한 최신 명세인 RFC 2616에 따르면, 흔히 알고 있는 것과는 달리 의외로 403 Forbidden은 권한(authorization)에 대한 에러가 아니다. 그냥 요청은 이해하지만 수행을 거절(refuse)하겠다는 의미이다.

그리고 또 의외로 400 Bad Request는 그냥 “요청이 잘못되었다”라는 의미가 아니라 “요청의 syntax가 잘못되어서 이해를 못하겠다”라는 의미를 담은 응답이다.

400 Bad Request에 대한 설명 (( http://tools.ietf.org/html/rfc2616#section-10.4.1 ))

The request could not be understood by the server due to malformed syntax.

403 Forbidden에 대한 설명 (( http://tools.ietf.org/html/rfc2616#section-10.4.4 ))

The server understood the request, but is refusing to fulfill it.

HTTP/1.1 개정판

그런데 HTTP/1.1 개정판에선 흔히 알려진대로가 거의 맞다. 403 Forbidden은 권한(authorization)에 대한 에러가 맞고, 400 Bad Request는 그냥 요청에 문제가 있어서 처리를 못하겠다(혹은 안하겠다)는 에러다.

400 Bad Request에 대한 설명 (( http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-22#section-6.5.1 ))

The server cannot or will not process the request, due to a client error (e.g., malformed syntax).

403 Forbidden에 대한 설명 (( http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-22#section-6.5.3 ))

The server understood the request, but refuses to authorize it.

요새 점점 RFC 2616보다 개정판을 따르는게 낫겠다는 생각이 들고 있다. 별 차이 없겠지 싶어서 그냥 RFC 2616을 따르고 있었는데, 의외로 이렇게 차이가 나는 부분이 있다. 어차피 개정판이라고 해봐야 설명만 고친거고 프로토콜의 요구사항 자체는 둘 다 같으니 동작에 문제를 일으키는 일은 없겠지.

절대경로가 "//"로 시작하는 URL은 스펙 위반일까?

http://example.org//foo/bar 와 같이 path의 절대경로가 //로 시작하는 것은, URI의 정의에 대한 최신 명세인 RFC 3986을 위반한 것이다. 절대 경로는 “//”로 시작해서는 안된다.

  path-absolute   ; begins with "/" but not "//"

그러나 HTML에서 <a href="//foo/bar">test</a> 와 같은 상대경로를 사용하는 것은 아무 문제가 없는데, 왜냐하면 HTML의 가장 최신 표준인 4.01(아직 5는 드래프트)은 RFC 3986보다 훨씬 오래된 RFC 1808에 따라 상대경로를 해석하기 때문이다. RFC 1808의 “2.4.3. Parsing the Network Location/Login”에 보면 url이 //로 시작하는 경우, // 이후부터 다음번 / 이전까지의 문자열을 net_loc(network location과 login information)로 삼게 되어있으므로, //foo/barhttp://foo/bar로 해석된다.

또한 HTTP/1.1 역시 URI를 해석할 때 RFC 3986이 아닌 그보다 오래된 RFC 2396을 따르므로 앞의 URL은 HTTP/1.1 명세를 위반한 것도 아니다.

그러나 HTTP/1.1은 곧 개정될 예정이며, 이 개정될 HTTP/1.1은 RFC 3986을 따르므로 결국 명세를 위반하는 것이 되지 않을까 걱정이 살짝 되는데, 다행히도 HTTP Working Group에서 몇 주 전에 이 문제를 발견하고서는 논의를 거친 끝에 절대경로가 //로 시작해도 되는 것으로 예외처리를 했기 때문에 위반이 아니게 될 것이다. 다만 그렇다고 해도 어디까지나 이건 HTTP에 국한된 이야기이므로, HTTP 응용프로그램이 아닌 소프트웨어를 개발할 때는 //로 시작하는 절대경로를 사용하면 RFC 3986을 위반하게 된다는 점을 신경써야 할 것이다.

그리고 마지막으로, 로이 필딩에 따르면 RFC 3986이 절대경로가 //로 시작할 수 없도록 막은 것은, URI 규칙을 새로운 ABNF로 재작성하는 과정에서 발생한 사고(accident)라고 한다.

> Roy, do you recall whether there's a reason why we would
> want to rule out a path starting with "//"?

No, it is an accident of the transition to new URI ABNF and
should be raised as an issue.  There are several different ways to
fix it, depending on how lenient we want to be with parsing.

위 메일의 전문은 여기서 확인할 수 있다.

HTTP/1.1의 Range 요청과 이를 활용한 Pagination

HTTP/1.1은 Range 요청을 지원한다. 클라이언트가 Range 헤더를 통해 어떤 리소스의 일부분만을 요청하면, 서버는 그 부분만을 반환하는 방식으로 동작한다.

/file.zip를 100바이트에서 200바이트까지만 가져오는 클라이언트의 요청과 그에 대한 서버의 응답은 다음과 같다.

요청

GET /file.zip HTTP/1.1
Range: bytes=100-200
...

응답

HTTP/1.1 206 Partial Content
Content-Range: bytes 100-200/500
...

위의 예에서 단위는 ‘bytes’를 사용했다. 사실 HTTP/1.1을 정의한 RFC 2616에서는, HTTP/1.1에서 정의한 단위는 bytes 뿐이며 HTTP/1.1 구현체는 bytes 이외의 다른 단위는 무시할 수 있다(MAY)고 되어있다. 이해할 수 없는 단위에 대해서 서버와 클라이언트가 어떻게 동작해야 하는지에 대해 명확히 정의가 되어있지 않아서 직접 단위를 정의해서 쓰기가 매우 겁이 난다. 무슨 일이 벌어질 지 확신할 수 없기 때문이다.

다행히 httpbis(곧 제출될 HTTP/1.1의 개정판)에서는 훨씬 명확하게 정의되어있다.

If a range unit is not understood in a request, a server MUST ignore
the whole Range header field (Section 5.4).  If a range unit is not
understood in a response, an intermediary SHOULD pass the response to
the client; a client MUST fail.

만약 서버가 요청의 range 단위를 이해할 수 없다면, 반드시(MUST) 모든 Range
헤더 필드를 무시해야 한다. 만약 응답의 range 단위를 이해할 수 없다면, 중개자 (( 캐시 같은 것들 ))
는 응답을 클라이언트로 넘겨줘야 하며, 클라이언트는 반드시(MUST) 실패해야 한다.

Range 요청을 이용한 pagination 구현

이러한 HTTP의 미래에 조금 용기를 얻어서, 나는 Range 요청을 이용하여 웹 애플리케이션의 pagination (( 목록 하단에 표시되는 Prev 1 2 3 4 5 Next 이런 것.)) 을 구현했다. 직접 정의한 “pages”라는 단위와 HTTP/1.1의 Accept-Ranges, Range, Content-Range 헤더를 이용한다. 각 헤더의 사용방법은 다음과 같다.

Accept-Ranges

어떤 리소스가 pagination을 지원한다면, 서버는 그 사실을 알리기 위해 해당 리소스에 대한 일반적인 GET 요청에 대한 응답에 “pages” 값을 갖는 Accept-Ranges 헤더를 포함시킨다.

예:

Accept-Ranges: pages

Range

클라이언트는 다음과 같은 형식의 Range 헤더를 이용해 이 리소스의 특정 페이지만을 요청할 수 있다.

Range             = pages-unit "=" page-number
pages-unit        = "pages"
page-number       = 1*DIGIT
DIGIT             = <any US-ASCII digit "0".."9">

예:

Range: pages=1

Content-Range

서버는 Range 요청에 대해 다음과 같은 형식의 Content-Range 헤더를 통해 206 Partial Content로 응답한다. (HTTP/1.1의 bytes-range-spec과 차이가 있음에 유의하라)

Content-Range     = pages-unit SP page-number "/" complete-length
pages-unit        = "pages"
page-number       = 1*DIGIT
complete-length   = 1*DIGIT
SP                = <US-ASCII SP, space (32)>

예를 들어 총 두 페이지로 이루어진 목록에서 첫번째 페이지만을 반환하는 응답에서의 Content-Range는 다음과 같다.

Content-Range: pages 1/2

서버는 상황에 따라 클라이언트가 요청한 것과 다른 페이지를 돌려줄 수도 있다. 이러한 상황에 대한 예외처리의 책임은 클라이언트에게 있다.

이 구현에 대한 전체 문서는 여기를 보라.

왜 굳이 Range 요청을 이용하는가?

왜 Pagination을 구현할 때 굳이 Range 요청을 이용해야 하는가? 단지 동작하게 만든다는 이유 뿐이라면 query string에 page=1 같은 key-value를 넣기만 해도 충분할 것이다.

그러나 내가 원하는 것은 표준화된 인터페이스와 동작이다. HTTP를 이해하는 개발자라면, “Range 요청을 활용해서 pagination을 구현했다”는 한마디로도 대강의 동작방식을 이해할 수 있을 것이며, 예외상황에 대한 처리도 명세를 따르면 되기 때문에 매번 개발자들끼리 토론하고 협의할 필요도 줄어들 것이다. 혹시 HTTP를 모르는 개발자라고 할 지라도, 특정 애플리케이션만의 pagination 방식을 익혀서 사용하는 것 보다는, 널리 쓰이고 있으며 앞으로도 널리 쓰일 표준화된 Range 요청에 대해 배워두는 편이 경력에 도움이 될 것이다.

iOS6 사파리의 POST 캐싱 버그에 대해

iOS6의 사파리는, 기본적으로 POST 요청에 대한 응답을 캐싱한다.

여기서 치명적인 문제가 발생한다. 서버에서 요청의 대상이 되는 리소스가 갱신되었음에도 불구하고, 사파리는 캐시했던 이전 응답을 사용자에게 보여주게 될 수 있다.

문제가 발생하는지에 대한 테스트는 여기에서 해볼 수 있다. 이 테스트에서 브라우저는 두 번의 POST 요청을 서버에게 보내고 서버는 그 요청에 대해 각각 다른 응답을 한다. 그러나 iOS6의 사파리에서는 두 번 전부 같은 응답을 받게 될 것이다.

회피책

이 문제를 회피하는 두 가지 방법이 있다.

  • 클라이언트 사이드: 캐시되지 않도록 query string에 매번 바뀌는 임의의 문자열(예: 현재 시각)을 붙인다.
  • 서버 사이드: POST 요청에 대한 응답에, Pragma: no-cache 나 Cache-control: no-cache 를 명시해준다.

위 둘 중 하나만 적용해도 문제가 발생하지 않는다. 자세한 설명은 Guillermo Rauch의 ‘iOS6 AJAX 버그 이해하기‘를 보라.

누구의 잘못인가?

애플의 잘못이다. HTTP/1.1을 정의한 RFC 2616의 POST에 대한 설명을 읽어보면, POST 요청에 대한 응답은 기본적으로 캐시해서는 안됨을 알 수 있다. 응답에 Cache-Control 이나 Expires 헤더가 정의되어있는 경우에만 캐시를 할 것인지에 대해 고려할 수 있다.

자세한 것은 HTTPbis Working Group의 의장인 Mark Nottingham이 이번 건에 대해 포스팅한 ‘POST 캐싱‘을 읽어보라.

웹서비스에 한글 아이디를 허용하게 되면 난감해지는 HTTP적인 이유

그건 Basic Authentication 때문이다.

웹서비스에 한글 아이디를 허용하면 Basic Authentication을 적용하기 어려워진다.

뭐가 문제인가

HTTP의 Basic Authentication은 매우 단순한 인증 체계이다. 클라이언트가 사용자의 아이디와 비밀번호를 “아이디:비밀번호” 꼴로 만들어 base64 인코딩하여 전송하면, 서버가 확인 후 틀렸으면 401 Unauthorized를 돌려주고 맞았으면 요청을 적절히 처리한다.

보통 웹브라우저로 사이트에 들어가는데 팝업이 짠 뜨면서 로그인을 요구하면 대개 이 Basic Authentication이라고 보면 된다. 좀 더 복잡한 Digest Authentication이란 것도 있긴 한데 있긴 한데 별로 인기가 없다. 어차피 안전하지 않은 건 둘 다 마찬가지라 그럴바엔 그냥 쉬운 것을 선호하게 되는 모양이다.

근데 이 Basic Authentication 체계에서는 한글 아이디나 비밀번호를 사용할 수 없다.

Basic Authentication를 정의한 RFC 2617에 따르면, 아이디는 “:”를 제외한 0자 이상의 TEXT이고 비밀번호는 그냥 0자 이상의 TEXT다. 그럼 대체 TEXT는 뭘까?

The TEXT rule is only used for descriptive field contents and values
that are not intended to be interpreted by the message parser. Words
of *TEXT MAY contain characters from character sets other than ISO-
8859-1 [22] only when encoded according to the rules of RFC 2047

HTTP/1.1을 정의한 RFC 2616에 따르면 TEXT는 ISO-8859-1로 정의된 문자집합에 속한 문자만을 사용해야하며, 그렇지 않은 경우에는 RFC 2047의 인코딩 규칙을 따라야 한다.

RFC 2047의 인코딩 규칙이란 대략 요렇게 생긴 것들이다. 아마 웹브라우저로 파일 다운로드 받다 뭔가 잘못되었을 때 종종 봤을 것이다. (( 웹브라우저마다 파일 이름을 코딩하는 방식이 제각각이라 그런 문제가 종종 생긴다. 자세한 것은 여기를 보라. ))

=?iso-8859-1?q?this is some text?=

자 그럼, 저 요상하게 생긴 방식으로 인코딩을 하면 아무 문제없이 한글 아이디를 써도 Basic Authentication이 가능할 것 같은 기분이 든다.

근데 아니다.

예전에 Stackoverflow에 Basic Authentication의 아이디와 패스워드는 대체 뭘로 디코딩해야 하는가에 대한 질문이 올라온 일이 있었는데, 답변은 “ISO-8859-1” 흑은 “undefined” 니까 네가 하고 싶은대로 해라.였다.

아니 스펙에 분명 RFC 2047도 된다고 하는데 이 답변자는 뭐하시는 분이길래 하고 봤더니만 Julian Reschke였다. HTTP/1.1을 고쳐쓰는 httpbis의 저자 중 한명이다. 그에 따르면 httpbis에선 RFC 2047을 적용 가능함에 대한 언급이 빠진다고 한다.

실제 구현들은 어떠한가

“ISO-8859-1” 혹은 “undefined” 라니 참으로 애매해다. 이런 모호한 상황을 유명 웹서버들은 어떻게 해결했을까. 구현을 들여다보자.

다음은 Apache Tomcat의 구현이다.

username = new String(buf, 0, colon);
password = new String(buf, colon + 1,
        authorizationCC.getEnd() - colon - 1);

인코딩을 정해주지 않았다. 이런 경우 시스템 설정을 따라갈 것이다.

jetty6은 ISO-8859-1로 디코딩한다.

credentials = B64Code.decode(credentials,StringUtil.__ISO_8859_1);

cpython은 ascii로 디코딩한다. ISO-8859-1 보다 작은 문자집합이다.

authorization = base64.decodebytes(authorization).
                decode('ascii')

한번 자신이 애용하는 웹서버의 소스코드를 들여다보라. 아마 UTF-8로 디코딩하는 경우는 거의 없을 것이다.

결론

웹애플리케이션을 만들면서 아이디를 한글도 가능하게 하려고 생각하고 있다면 다시 한번 생각해보길 바란다. Basic Authentication을 붙일 일이 생기면 골치아파질 수 있다.