한글화 프로젝트
한글화 프로젝트는 지앤선과 국내 커뮤니티들이 함께 해외의 다양한 주제의 유익한 문서 등을 번역하여 영어를 읽는데 어려움이 있는 여러 사람에게 도움을 드리고자 진행 중인 프로젝트이다.
이 글은 KSUG의 임정목 님이 번역을 진행해주신 것으로 지앤선의 블로그를 통해 공개되었던 것을 재정리하여 브런치로 옮겨왔다. 원본은 여기에서 확인할 수 있다.
(Leonard Richardson이 개발한) REST 방식의 주요 요소들을 3개의 단계로 나눈 모델. 이 모델은 리소스, HTTP 메소드 (verbs), 하이퍼미디어 컨트롤을 도입한다.
최근에 나는 내 동료 몇 명이 작업 중인 Rest In Practice의 초안을 읽고 있다. 그들의 목표는 엔터프라이즈가 직면한 많은 통합 문제를 다루기 위해 Restful 웹 서비스를 어떻게 활용할지 설명하는 것이다. 이 책의 핵심은 정말 잘 동작하는 대규모로 확장가능한 분산 시스템의 실제 증거가 웹이고, 통합된 시스템을 더 쉽게 구축하기 위한 아이디어를 그것으로부터 얻을 수 있다는 것이다.
그림 1. REST를 향한 단계들
웹 스타일 시스템의 구체적인 속성들에 대한 설명을 돕기 위해 저자들은 Leonard Richardson이 개발하고 QCon 토크에서 설명한 restful 성숙도 모델을 사용한다. 이 모델은 이 기술들을 사용하는 것에 대해 생각하는 좋은 방법을 제공하기 때문에 나는 그것에 대한 나만의 설명을 해보려고 생각했다. (여기에 제시된 프로토콜 예제들은 오직 설명을 위한 것으로, 나는 그것들이 구현되거나 완전히 테스트될 가치가 있다고 생각하지 않아서 세부적으로는 문제들이 있을 수도 있다.)
이 모델의 시작점은 웹 메커니즘은 전혀 사용하지 않고 HTTP를 원격 통신(remote interaction)을 위한 전송 시스템으로 사용하는 것이다. 여기에서는 원격 프로시저 호출(Remote Procedure Invocation)에 기반한 원격 통신 메커니즘을 위한 터널링 메커니즘으로서 HTTP를 사용하는 것이다.
그림 2. 0 레벨 0 통신 예제
주치의와 약속을 예약하는 경우를 생각해 보자. 내 예약 프로그램은 주어진 날짜에 주치의가 예약되지 않은 시간대을 알아내야 한다. 따라서, 병원 예약 시스템으로 그러한 정보를 요청한다. 레벨 0 시나리오에서 병원은 정해진 URI로 서비스 엔드포인트를 제공하고 내 프로그램은 해당 엔드포인트로 요청 정보를 포함하는 문서를 보낸다.
POST /appointmentService HTTP/1.1
[various other headers]
<openSlotRequest date = "2010-01-04" doctor = "mjones"/>
그러면 서버는 다음과 같은 정보를 포함하는 문서를 반환할 것이다.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot start = "1400" end = "1450">
<doctor id = "mjones"/>
</slot>
<slot start = "1600" end = "1650">
<doctor id = "mjones"/>
</slot>
</openSlotList>
예제에서 XML을 사용하고 있지만, 사실 내용은 JSON, YAML, 키-값 쌍 등 어떤 형식이든 될 수 있다.
다음 단계는 다시 그 엔드포인트로 문서를 보내서 예약을 하는 것이다.
POST /appointmentService HTTP/1.1
[various other headers]
<appointmentRequest>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointmentRequest>
문제가 없다면, 다음과 같이 예약되었다는 응답을 받는다.
HTTP/1.1 200 OK
[various headers]
<appointment>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
다른 사람이 먼저 예약하는 등의 문제가 있다면, 다음과 같은 에러 메시지를 받을 것이다.
HTTP/1.1 200 OK
[various headers]
<appointmentRequestFailure>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<reason>Slot not available</reason>
</appointmentRequestFailure>
지금까지는 단지 POX(Plain Old XML)를 주고 받는 단순한 RPC 스타일 시스템에 불과하다. SOAP이나 XML-RPC를 사용한다 하더라도, XML 메시지를 envelope으로 감싼다는 점을 제외하면 기본적으로 동일한 메커니즘이다.
RMM (Richardson Maturity Model)에서 Rest의 영광을 향한 첫 단계는 리소스를 도입하는 것이다. 그래서 이제는 모든 요청을 단일 서비스 엔드포인트로 보내는 것이 아니라, 개별 리소스와 통신한다.
그림 3: 레벨 1은 리소스를 추가한다.
첫 요청 시 해당 의사에 대한 리소스를 가지고 있을 것이다.
POST /doctors/mjones HTTP/1.1
[various other headers]
<openSlotRequest date = "2010-01-04"/>
응답은 같은 기본 정보를 제공하지만, 각 시간대는 이제 개별적으로 어드레싱 가능한 리소스이다.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
특정 리소스로 예약한다는 것은 특정 시간대로 요청을 보내는 것이다.
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
문제가 없다면, 다음과 같이 전과 유사한 응답을 받는다.
HTTP/1.1 200 OK
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
차이는 누군가 검사 예약과 같은 것을 할 필요가 있다면, 우선 http://royalhope.nhs.uk/slots/1234/appointment 같은 URI를 갖는 예약 리소스를 얻고, 그 리소스로 요청을 보낸다.
나처럼 객체 지향을 하는 사람에게 이것은 객체 식별자의 개념과 유사하다. 함수를 호출하고 인자들을 넘기는 것이 아니라, 다른 정보를 위해 인자들을 제공하는 특정 객체의 메소드를 호출한다.
나는 레벨 0과 1의 모든 통신에서 HTTP POST 메소드를 사용했지만, 어떤 사람들은 GET을 대신 혹은 추가로 사용한다. 이 레벨에서는 별 차이가 없이 둘 다 HTTP를 통한 터널링 메커니즘으로 사용되고 있다. 레벨 2는 여기에서 벗어나, HTTP 사용법에 가능한 가깝게 HTTP 메소드를 사용한다.
그림 4: 레벨 2는 HTTP 메소드를 추가한다.
이는 시간대 목록을 얻기 위해 GET을 사용하기를 원함을 의미한다.
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
응답은 POST와 동일하다.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
레벨 2에서 이와 같은 요청을 위한 GET의 사용은 중요하다. HTTP는 GET을 상태를 크게 변화시키지 않는 안전한 오퍼레이션으로 정의한다. 이것은 GET을 안전하게 어떤 순서로든 얼마든지 호출할 수 있도록 하고, 매번 같은 결과를 얻도록 한다. 이로 인한 중요한 결과는 요청이 라우팅되는 중간의 어느 참여자든 웹을 잘 동작하게 하는 핵심 요소인 캐싱을 할 수 있다는 것이다. HTTP는 통신 상의 모든 참여자에 의해 사용될 수 있는 캐싱을 지원하는 다양한 방법을 제공한다. HTTP의 규칙을 준수함으로써 그러한 기능의 장점을 취할 수 있다.
예약을 하기 위해 상태를 변경하는 HTTP 메소드인 POST 혹은 PUT이 필요하다. 나는 전과 동일한 POST를 사용할 것이다.
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
POST와 PUT을 사용하는 것 사이의 트레이드오프는 내가 여기서 다루고 싶은 것 이상이다. 아마도 언젠가 별도의 글에서 다룰 것이다. 하지만 나는 어떤 사람들이 POST/PUT과 create/update 사이의 관계를 부정확하게 만들고 있음을 지적하고 싶다. POST와 PUT 사이의 선택은 create과 update 사이의 선택과는 다소 다르다.
레벨 1과 동일한 POST를 사용할지라도, 원격 서비스가 응답하는 방법에는 상당한 차이가 있다. 문제가 없다면, 서비스는 새 리소스가 있음을 알리는 201 응답 코드와 함께 응답한다.
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
201 응답은 클라이언트가 나중에 그 리소스의 현재 상태를 GET하기 위해 사용할 수 있는 URI를 갖는 location 속성을 포함한다. 또한 응답은 클라이언트의 추가 호출을 줄이기 위해 해당 리소스의 표현을 포함한다.
다른 사람이 먼저 예약하는 등의 문제가 있을 때 또 다른 차이가 있다.
HTTP/1.1 409 Conflict
[various headers]
<openSlotList>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
이 응답에서 중요한 부분은 무엇인가 잘못되었음을 알리기 위해 HTTP 응답 코드를 사용하는 것이다. 이 경우 409는 다른 사람이 호환되지 않는 방식으로 리소스를 이미 업데이트했음을 가리키기 위한 좋은 선택으로 보인다. 200 반환 코드를 사용하지만 에러 응답을 포함하는 방식과 달리 레벨2에서는 이처럼 명시적으로 에러 응답을 사용한다. 어떤 코드를 사용할지는 프로토콜 설계자에게 달려있지만, 에러가 발생하면 2xx가 아닌 응답이어야만 한다. 레벨 2는 HTTP 메소드와 HTTP 응답코드의 사용을 도입한다.
여기에 일관성 문제가 생겨난다. REST 옹호자들은 모든 HTTP 메소드의 사용에 대해 말한다. 또한 REST가 웹의 실제적인 성공으로부터 학습을 시도하고 있다고 말함으로써 그들의 접근법을 정당화한다. 하지만 웹은 PUT이나 DELETE를 실무에서 많이 사용하지 않는다. PUT과 DELETE의 사용이 합당한 이유들이 있지만, 웹의 존재가 그 합당한 이유들 중 하나는 아니다.
웹의 존재에 의해 지지되는 핵심 요소들은 발생한 에러의 종류를 커뮤니케이션하기 위해 상태 코드를 사용하는 것과 함께 안전한 오퍼레이션(예 GET)과 안전하지 않은 오퍼레이션 간의 강한 분리를 제공하는 것이다.
마지막 레벨은 HATEOAS(Hypertext As The Engine Of Application State)라는 보기 흉한 약어로 종종 언급되는 것을 도입한다. 그것은 예약을 하기 위해 무엇을 해야 할지 알기 위해 어떻게 빈 시간대 목록을 가져올지에 대한 문제를 해결한다.
그림 5 레벨 3은 하이퍼미디어 컨트롤을 추가한다.
레벨 2에서 보냈던 GET과 동일한 GET 요청으로 시작한다.
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
하지만 응답은 새로운 요소를 가지고 있다.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450">
<link rel = "/linkrels/slot/book"
uri = "/slots/1234"/>
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650">
<link rel = "/linkrels/slot/book"
uri = "/slots/5678"/>
</slot>
</openSlotList>
이제 각 시간대는 예약하는 방법을 알려주는 URI를 포함한 링크 요소를 가진다.
하이퍼미디어 컨트롤의 요점은 그것들이 다음에 무엇을 할 수 있는지와 그것을 하기 위해 다루어야 할 리소스의 URI를 알려준다는 것이다. 우리가 예약 요청을 어디로 보낼지 알아야 하는 것이 아니라, 응답 내 하이퍼미디어 컨트롤이 우리에게 그것을 어떻게 할지 알려준다.
POST는 레벨 2에서와 동일하다.
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
그리고 응답은 다음에 할 수 있는 다양한 것들을 위한 많은 하이퍼미디어 컨트롤을 포함한다.
HTTP/1.1 201 Created
Location: http://royalhope.nhs.uk/slots/1234/appointment
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<link rel = "/linkrels/appointment/cancel"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest"
uri = "/slots/1234/appointment/tests"/>
<link rel = "self"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime"
uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo"
uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help"
uri = "/help/appointment"/>
</appointment>
하이퍼미디어 컨트롤의 한가지 명백한 장점은 서버가 클라이언트에 문제를 일으키지 않고 URI scheme을 변경할 수 있다는 것이다. 클라이언트가 "addTest" 링크 URI를 찾는 동안, 서버 팀은 최초 진입점을 제외한 모든 URI 작업을 할 수 있다.
더 나은 장점으로 클라이언트 개발자가 프로토콜을 탐색할 수 있도록 돕는다. 링크는 클라이언트 개발자에게 다음에 무엇이 가능할지 힌트를 제공한다. 그것은 모든 정보를 제공하지는 않는다. "latest"와 "cancel" 컨트롤 둘 다 같은 URI를 가리키고 있다. 하나는 GET이고 다른 하나는 DELETE임을 알아낼 필요가 있다. 하지만 적어도 그것은 더 많은 정보를 위해 생각할 것과 프로토콜 문서 내에 유사한 URI를 찾기 위한 시작점을 제공한다.
유사하게 그것은 서버 팀이 응답 내에 새로운 링크를 넣음으로써 새로운 기능을 알릴 수 있도록 한다. 클라이언트 개발자들이 알려지지 않은 링크들을 눈여겨보고 있다면, 이러한 링크들은 더 탐색하기 위한 트리거가 될 수 있다.
하이퍼미디어 컨트롤을 표현하는 방법에 대한 절대적인 표준은 없다. 내가 여기에서 사용한 예제들은 실무에서 REST 권장사항으로 따르는 ATOM (RFC 4287)을 사용했다. 나는 타겟 URI를 위해 uri 속성과 관계의 종류를 기술하기 위해 rel 속성을 가진 <link> 요소를 사용한다. 잘 알려진 리소스 관계의 표현은 간단하다. 예를 들어, 자신을 참조하는 관계는 'self'를 사용한다. 서버의 특정 리소스와의 관계는 전체 URI(fully qualified URI)를 사용한다. ATOM에서 잘 알려진 linkrel을 위한 정의는 IANA의 Link Relation을 사용한다. 이것들은 레벨 3의 restful다움에서 리더로서 일반적으로 보여지는 ATOM에 의한 것으로 제한된다.
RMM이 REST의 요소가 무엇인지에 대해 생각하는 좋은 방법이지만 REST 자체의 레벨 정의가 아님을 강조해야겠다. Roy Fielding은 레벨 3 RMM이 REST의 선행조건임을 명백히 했다. 소프트웨어 내 많은 용어와 마찬가지로 REST는 많은 정의를 가지고 있지만, Roy Fielding이 그 용어를 만든 이후로 그의 정의가 가장 영향력있어야만 한다.
내가 발견한 RMM의 유용한 점은 restful이 내포하고 있는 기본적인 생각들을 이해하기 위한 좋은 점진적인 방법을 제공한다는 것이다. 그것은 일종의 평가 메커니즘에서 사용되어야만 하는 것이 아니라 개념을 배우기 위한 도구라고 생각한다. 나는 우리가 restful 접근법이 시스템을 통합하기 위한 옳은 방법이라는 것을 정말 확신하기 위해 아직 충분한 예를 가지고 있다고 생각하지 않지만, 나는그것이 매우 매력적인 접근법이고 대부분의 환경에서 추천할만한 것이라고 생각한다.
Ian Robinson과 이것에 대해 얘기할 때, 그는 Leonard Richardson이 처음 이 모델을 소개했을 때 그가 발견한 이 모델의 매력은 일반적인 설계 기술과 이 모델과의 관계라고 강조했다.
* 레벨 1은 큰 서비스 엔드포인트를 복수개의 리소스로 나누는 분할 & 정복(divide and conquer)을 사용해서 복잡성을 다루는 문제를 처리한다.
* 레벨 2는 불필요한 다양성을 제거하고, 동일한 방식으로 유사한 환경을 처리할 수 있도록 메소드의 표준 집합을 도입한다.
* 레벨 3은 프로토콜을 더 스스로 문서화할 수 있는 방법을 제공함으로써 발견가능성(discoverability)을 도입한다.
위와 같이 각 레벨에서 각 개념들을 도입한 결과, 우리가 제공하고자 하는 HTTP 서비스에 대해 생각하고, 이 서비스를 이용할 사용자들의 기대에 대한 틀을 잡도록 돕는 모델을 제공한다.
Acknowledgements
Savas Parastatidis, Ian Robinson, and Jim Webber made substantial comments on the drafts. Leonard Richardson was very helpful in answering my questions so that I could minimize any misinterpretations of his ideas. Aaron Swartz corrected some errors with my level 3 URIs.
18 March 2010: Initial posting