블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 

'안티패턴'에 해당되는 글 2

  1. 2014.09.25 REST API 이해와 설계 - #2 API 설계 가이드 (3)
  2. 2014.09.25 REST API의 이해와 설계-#1 개념 소개 (17)
 

REST API 이해와 설계 

#2 API 설계 가이드


REST API 디자인 가이드

그러면 REST의 특성을 이해하고 나쁜 안티패턴을 회피해서 REST API 디자인은 어떻게 해야 할까? 짧지만 여기에 몇가지 디자인 방식에 대해서 소개 한다.


REST URI는 심플하고 직관적으로 만들자

REST API를 URI만 보고도, 직관적으로 이해할 수 있어야 한다 URL을 길게 만드는것 보다, 최대 2 depth 정도로 간단하게 만드는 것이 이해하기 편하다.

  • /dogs
  • /dogs/1234

URI에 리소스명은 동사보다는 명사를 사용한다.

REST API는 리소스에 대해서 행동을 정의하는 형태를 사용한다. 예를 들어서

  • POST /dogs

는 /dogs라는 리소스를 생성하라는 의미로, URL은 HTTP Method에 의해 CRUD (생성,읽기,수정,삭제)의 대상이 되는 개체(명사)라야 한다.

잘못된 예들을 보면

  • HTTP Post : /getDogs
  • HTTP Post : /setDogsOwner

위의 예제는 행위를 HTTP Post로 정의하지 않고, get/set 등의 행위를 URL에 붙인 경우인데, 좋지 않은 예 이다. 이보다는

  • HTTP Get : /dogs
  • HTTP Post : /dogs/{puppy}/owner/{terry}

를 사용하는 것이 좋다. 그리고 가급적이면 의미상 단수형 명사(/dog)보다는 복수형 명사(/dogs)를 사용하는 것이 의미상 표현하기가 더 좋다.

일반적으로 권고되는 디자인은 다음과 같다.

리소스

POST

GET

PUT

DELETE

create

read

update

delete

/dogs

새로운 dogs 등록

dogs 목록을 리턴

Bulk 여러 dogs 정보를 업데이트

모든 dogs 정보를 삭제

/dogs/baduk

에러

baduk 이라는 이름의 dogs 정보를 리턴

baduk이라는 이름의 dogs 정보를 업데이트

baduk 이라는 이름의 dogs 정보를 삭제


리소스간의 관계를 표현하는 방법

REST 리소스간에는 서로 연관관계가 있을 수 있다. 예를 들어 사용자가 소유하고 있는 디바이스 목록이나 사용자가 가지고 있는 강아지들 등이 예가 될 수 가 있는데, 사용자-디바이스 또는 사용자-강아지등과 같은 각각의 리소스간의 관계를 표현하는 방법은 여러가지가 있다.


Option 1. 서브 리소스로 표현하는 방법

예를 들어 사용자가 가지고 있는 핸드폰 디바이스 목록을 표현해보면

  • /”리소스명”/”리소스 id”/”관계가 있는 다른 리소스명” 형태 
  • HTTP Get : /users/{userid}/devices
    예) /users/terry/devices

과 같이 /terry라는 사용자가 가지고 있는 디바이스 목록을 리턴하는 방법이 있고


Option 2. 서브 리소스에 관계를 명시 하는 방법

만약에 관계의 명이 복잡하다면 관계명을 명시적으로 표현하는 방법이 있다. 예를 들어 사용자가 “좋아하는” 디바이스 목록을 표현해보면

  • HTTP Get : /users/{userid}/likes/devices
    예) /users/terry/likes/devices

는 terry라는 사용자가 좋아하는 디바이스 목록을 리턴하는 방식이다.

Option 1,2 어떤 형태를 사용하더라도 문제는 없지만, Option 1의 경우 일반적으로 소유 “has”의 관계를 묵시적으로 표현할 때 좋으며, Option 2의 경우에는 관계의 명이 애매하거나 구체적인 표현이 필요할 때 사용한다. 


에러처리

에러처리의 기본은 HTTP Response Code를 사용한 후, Response body에 error detail을 서술하는 것이 좋다.

대표적인 API 서비스들이 어떤 HTTP Response Code를 사용하는지를 살펴보면 다음과 같다.

구글의 gdata 서비스의 경우 10개, 넷플릭스의 경우 9개, Digg의 경우 8개의 Response Code를 사용한다.  (정보 출처:  http://info.apigee.com/Portals/62317/docs/web%20api.pdf)

Google GData

200 201 304 400 401 403 404 409 410 500


Netflix

200 201 304 400 401 403 404 412 500


Digg

200 400 401 403 404 410 500 503

여러 개의 response code를 사용하면 명시적이긴 하지만, 코드 관리가 어렵기 때문에 아래와 같이 몇가지 response code만을 사용하는 것을 권장한다.

  • 200 성공
  • 400 Bad Request - field validation 실패시
  • 401 Unauthorized - API 인증,인가 실패
  • 404 Not found ? 해당 리소스가 없음
  • 500 Internal Server Error - 서버 에러

추가적인 HTTP response code에 대한 사용이 필요하면 http response code 정의 http://en.wikipedia.org/wiki/Http_error_codes 문서를 참고하기 바란다.

다음으로 에러에는 에러 내용에 대한 디테일 내용을 http body에 정의해서, 상세한 에러의 원인을 전달하는 것이 디버깅에 유리하다.

Twillo의 Error Message 형식의 경우

  • HTTP Status Code : 401
  • {“status”:”401”,”message”:”Authenticate”,”code”:200003,”more info”:”http://www.twillo.com/docs/errors/20003"}

와 같이 표현하는데, 에러 코드 번호와 해당 에러 코드 번호에 대한 Error dictionary link를 제공한다.

비단 API 뿐 아니라, 잘 정의된 소프트웨어 제품의 경우에는 별도의 Error 번호 에 대한 Dictionary 를 제공하는데, Oracle의 WebLogic의 경우에도http://docs.oracle.com/cd/E24329_01/doc.1211/e26117/chapter_bea_messages.htm#sthref7 와 같이 Error 번호, 이에 대한 자세한 설명과, 조치 방법등을 설명한다. 이는 개발자나 Trouble Shooting하는 사람에게 많은 정보를 제공해서, 조금 더 디버깅을 손쉽게 한다. (가급적이면 Error Code 번호를 제공하는 것이 좋다.)

다음으로 에러 발생시에, 선택적으로 에러에 대한 스택 정보를 포함 시킬 수 있다.

에러메세지에서 Error Stack 정보를 출력하는 것은 대단히 위험한 일이다. 내부적인 코드 구조와 프레임웍 구조를 외부에 노출함으로써, 해커들에게, 해킹을 할 수 있는 정보를 제공하기 때문이다. 일반적인 서비스 구조에서는 아래와 같은 에러 스택정보를 API 에러 메세지에 포함 시키지 않는 것이 바람직 하다.

log4j:ERROR setFile(null,true) call failed.

java.io.FileNotFoundException: stacktrace.log (Permission denied)

at java.io.FileOutputStream.openAppend(Native Method)

at java.io.FileOutputStream.(FileOutputStream.java:177)

at java.io.FileOutputStream.(FileOutputStream.java:102)

at org.apache.log4j.FileAppender.setFile(FileAppender.java:290)

at org.apache.log4j.FileAppender.activateOptions(FileAppender.java:164)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

그렇지만, 내부 개발중이거나 디버깅 시에는 매우 유용한데, API 서비스를 개발시, 서버의 모드를 production과 dev 모드로 분리해서, 옵션에 따라 dev 모드등으로 기동시, REST API의 에러 응답 메세지에 에러 스택 정보를 포함해서 리턴하도록 하면, 디버깅에 매우 유용하게 사용할 수 있다.


API 버전 관리

API 정의에서 중요한 것중의 하나는 버전 관리이다. 이미 배포된 API 의 경우에는 계속해서 서비스를 제공하면서,새로운 기능이 들어간 새로운 API를 배포할때는 하위 호환성을 보장하면서 서비스를 제공해야 하기 때문에, 같은 API라도 버전에 따라서 다른 기능을 제공하도록 하는 것이 필요하다.

API의 버전을 정의하는 방법에는 여러가지가 있는데,

  • Facebook ?v=2.0
  • salesforce.com /services/data/v20.0/sobjects/Account

필자의 경우에는

  • {servicename}/{version}/{REST URL}
  • example) api.server.com/account/v2.0/groups

형태로 정의 하는 것을 권장한다.

이는 서비스의 배포 모델과 관계가 있는데, 자바 애플리케이션의 경우, account.v1.0.war, account.v2.0.war와 같이 다른 war로 각각 배포하여 버전별로 배포 바이너리를 관리할 수 있고, 앞단에 서비스 명을 별도의 URL로 떼어 놓는 것은 향후 서비스가 확장되었을 경우에, account 서비스만 별도의 서버로 분리해서 배포하는 경우를 생각할 수 있다.

외부로 제공되는 URL은 api.server.com/account/v2.0/groups로 하나의 서버를 가르키지만, 내부적으로, HAProxy등의 reverse proxy를 이용해서 이런 URL을 맵핑할 수 있는데, api.server.com/account/v2.0/groups를 내부적으로 account.server.com/v2.0/groups 로 맵핑 하도록 하면, 외부에 노출되는 URL 변경이 없이 향후 확장되었을때 서버를 물리적으로 분리해내기가 편리하다.


페이징

큰 사이즈의 리스트 형태의 응답을 처리하기 위해서는 페이징 처리와 partial response 처리가 필요하다.

리턴되는 리스트 내용이 1,000,000개인데, 이를 하나의 HTTP Response로 처리하는 것은 서버 성능, 네트워크 비용도 문제지만 무엇보다 비현실적이다. 그래서, 페이징을 고려하는 것이 중요하다.

페이징을 처리하기 위해서는 여러가지 디자인이 있다.

예를 들어 100번째 레코드부터 125번째 레코드까지 받는 API를 정의하면

Facebook API 스타일 : /record?offset=100&limit=25

  • Twitter API 스타일 : /record?page=5&rpp=25 (RPP는 Record per page로 페이지당 레코드수로 RPP=25이면 페이지 5는 100~125 레코드가 된다.)
  • LikedIn API 스타일 : /record?start=50&count=25
  • 구현 관점에서 보면 , 페이스북 API가 조금 더 직관적이기 때문에, 페이스북 스타일을 사용하는 것을 권장한다.
  • record?offset=100&limit=25

100번째 레코드에서부터 25개의 레코드를 출력한다.


Partial Response 처리

리소스에 대한 응답 메세지에 대해서 굳이 모든 필드를 포함할 필요가 없는 케이스가 있다.

 예를 들어 페이스북 FEED의 경우에는 사용자 ID, 이름, 글 내용, 날짜, 좋아요 카운트, 댓글, 사용자 사진등등 여러가지 정보를 갖는데, API를 요청하는 Client의 용도에 따라 선별적으로 몇가지 필드만이 필요한 경우가 있다. 필드를 제한하는 것은 전체 응답의 양을 줄여서 네트워크 대역폭(특히 모바일에서) 절약할 수 있고, 응답 메세지를 간소화하여 파싱등을 간략화할 수 있다.

그래서 몇몇 잘 디자인된, REST API의 경우 이러한 Partial Response 기능을 제공하는데, 주요 서비스들을 비교해보면 다음과 같다.

  • Linked in : /people:(id,first-name,last-name,industry)
  • Facebook : /terry/friends?fields=id,name
  • Google : ?fields=title,media:group(media:thumnail)

Linked in 스타일의 경우 가독성은 높지만 :()로 구별하기 때문에, HTTP 프레임웍으로 파싱하기가 어렵다. 전체를 하나의 URL로 인식하고, :( 부분을 별도의 Parameter로 구별하지 않기 때문이다.

Facebook과 Google은 비슷한 접근 방법을 사용하는데, 특히 Google의 스타일은 더 재미있는데, group(media:thumnail) 와 같이 JSON의 Sub-Object 개념을 지원한다.

Partial Response는 Facebook 스타일이 구현하기가 간단하기 때문에, 필자의 경우는 Facebook 스타일의 partial response를 사용하는 것을 권장한다.


검색 (전역검색과 지역검색)

검색은 일반적으로 HTTP GET에서 Query String에 검색 조건을 정의하는 경우가 일반적인데, 이 경우 검색조건이 다른 Query String과 섞여 버릴 수 있다. 예를 들어 name=cho이고, region=seoul인 사용자를 검색하는 검색을 Query String만 사용하게 되면 다음과 같이 표현할 수 있다.

  • /users?name=cho&region=seoul

그런데, 여기에 페이징 처리를 추가하게 되면

  • /users?name=cho&region=seoul&offset=20&limit=10

페이징 처리에 정의된 offset과 limit가 검색 조건인지 아니면 페이징 조건인지 분간이 안간다. 그래서, 쿼리 조건은 하나의 Query String으로 정의하는 것이 좋은데

  • /user?q=name%3Dcho,region%3Dseoul&offset=20&limit=10

이런식으로 검색 조건을 URLEncode를 써서 “q=name%3Dcho,region%3D=seoul” 처럼 (실제로는 q= name=cho,region=seoul )표현하고 Deleminator를 , 등을 사용하게 되면 검색 조건은 다른 Query 스트링과 분리된다.

물론 이 검색 조건은 서버에 의해서 토큰 단위로 파싱되어야 한다.

다음으로는 검색의 범위에 대해서 고려할 필요가 있는데, 전역 검색은 전체 리소스에 대한 검색을, 리소스에 대한 검색은 특정 리소스에 대한 검색을 정의한다.

예를 들어 시스템에 user,dogs,cars와 같은 리소스가 정의되어 있을때,id=’terry’인 리소스에 대한 전역 검색은

  • /search?q=id%3Dterry

와 같은 식으로 정의할 수 있다. /search와 같은 전역 검색 URI를 사용하는 것이다.

반대로 특정 리소스안에서만의 검색은

  • /users?q=id%3Dterry

와 같이 리소스명에 쿼리 조건을 붙이는 식으로 표현이 가능하다.


HATEOS를 이용한 링크 처리

HATEOS는 Hypermedia as the engine of application state의 약어로, 하이퍼미디어의 특징을 이용하여 HTTP Response에 다음 Action이나 관계되는 리소스에 대한 HTTP Link를 함께 리턴하는 것이다.

예를 들어 앞서 설명한 페이징 처리의 경우, 리턴시, 전후페이지에 대한 링크를 제공한다거나

{  

   [  

      {  

         "id":"user1",

         "name":"terry"

      },

      {  

         "id":"user2",

         "name":"carry"

      }

   ],

   "links":[  

      {  

         "rel":"pre_page",

         "href":"http://xxx/users?offset=6&limit=5"

      },

      {  

         "rel":"next_page",

         "href":"http://xxx/users?offset=11&limit=5"

      }

   ]

}

와 같이 표현하거나

연관된 리소스에 대한 디테일한 링크를 표시 하는 것등에 이용할 수 있다.

{  

   "id":"terry",

   "links":[  

      {  

         "rel":"friends",

         "href":"http://xxx/users/terry/friends"

      }

   ]

}

HATEOAS를 API에 적용하게 되면, Self-Descriptive 특성이 증대되어 API에 대한 가독성이 증가되는 장점을 가지고 있기는 하지만, 응답 메세지가 다른 리소스 URI에 대한 의존성을 가지기 때문에, 구현이 다소 까다롭다는 단점이 있다.

요즘은 Spring과 같은 프레임웍에서 프레임웍 차원에서 HATEOAS를 지원하고 있으니 참고하기 바란다.http://spring.io/understanding/HATEOAS


단일 API 엔드포인트 활용

API 서버가 물리적으로 분리된 여러개의 서버에서 동작하고 있을때, user.apiserver.com, car.apiserver.com과 같이 API 서비스마다 URL이 분리되어 있으면 개발자가 사용하기 불편하다. 매번 다른 서버로 연결을 해야하거니와 중간에 방화벽이라도 있으면, 일일이 방화벽을 해제해야 한다.

API 서비스는 물리적으로 서버가 분리되어 있더라도 단일 URL을 사용하는 것이 좋은데, 방법은 HAProxy나 nginx와 같은 reverse proxy를 사용하는 방법이 있다. HAProxy를 앞에 새우고 api.apiserver.com이라는 단일 URL을 구축한후에

HAProxy 설정에서

  • api.apiserver.com/user는 user.apiserver.com 로 라우팅하게 하고
  • api.apiserver.com/car 는 car.apiserver.com으로 라우팅 하도록 구현하면 된다.

이렇게 할 경우 향후 뒷단에 API 서버들이 확장이 되더라도 API를 사용하는 클라이언트 입장에서는 단일 엔드포인트를 보면 되고, 관리 관점에서도 단일 엔드포인트를 통해서 부하 분산 및 로그를 통한 Audit(감사)등을 할 수 있기 때문에 편리하며, API 에 대한 라우팅을 reverse proxy를 이용해서 함으로써 조금 더 유연한 운용이 가능하다.


REST에 문제점


그렇다면 이렇게 많은 장점이 있는 REST는 만능인가? REST역시 몇가지 취약점과 단점을 가지고 있다.


JSON+HTTP 를 쓰면 REST인가?

REST에 대한 잘못된 이해중의 하나가, HTTP + JSON만 쓰면 REST라고 부르는 경우인데, 앞의 안티패턴에서도 언급하였듯이, REST 아키텍쳐를 제대로 사용하는 것은, 리소스를 제대로 정의하고 이에대한 CRUD를 HTTP 메서드인 POST/PUT/GET/DELETE에 대해서 맞춰 사용하며, 에러코드에 대해서 HTTP Response code를 사용하는 등, REST에 대한 속성을 제대로 이해하고 디자인해야 제대로된 REST 스타일이라고 볼 수 있다.

수년전 뿐만 아니라 지금에도 이러한 안티패턴이 적용된 REST API 형태가 많이 있기 때문에, 제대로된 REST 사상의 이해 후에, REST를 사용하도록 해야 한다.


표준 규약이 없다

REST는 표준이 없다. 그래서 관리가 어렵다. 

SOAP 기반의 웹 서비스와 같이 메시지 구조를 정의하는 WSDL도 없고, UDDI와 같은 서비스 관리체계도 없다. WS-I이나 WS-*와 같은 메시지 규약도 없다. 

REST가 최근 부각되는 이유 자체가 WebService의 복잡성과 표준의 난이도 때문에 Non Enterprise 진영(Google, Yahoo, Amazone)을 중심으로 집중적으로 소개된 것이다. 데이터에 대한 의미 자체가 어떤 비즈니스 요건처럼 Mission Critical한 요건이 아니기 때문에, 서로 데이터를 전송할 수 있는 정도의 상호 이해 수준의 표준만이 필요했지 Enterprise 수준의 표준이 필요하지도 않았고, 벤더들처럼 이를 주도하는 회사도 없었다.

단순히 많이 사용하고 암묵적으로 암암리에 생겨난 표준 비슷한 것이 있을 뿐이다(이런 것을 Defactor 표준이라고 부른다). 

그런데 문제는 정확한 표준이 없다 보니, 개발에 있어 이를 관리하기가 어렵다는 것이다. 표준을 따르면   몇 가지 스펙에 맞춰서  맞춰 개발 프로세스나 패턴을 만들 수 있는데, REST에는 표준이 없으니 REST 기반으로 시스템을 설계하자면 사용할 REST에 대한 자체 표준을 정해야 하고, 어떤 경우에는 REST에 대한 잘못된 이해로 잘못된 REST 아키텍처에 ‘이건 REST다’는 딱지를 붙이기도 한다. 실제로 WEB 2.0의 대표 주자격인 Flickr.com도 REST의 특성을 살리지 못하면서 RPC 스타일로 디자인한 API를 HTTP + XML을 사용했다는 이유로 Hybrid REST라는 이름을 붙여 REST 아키텍쳐아키텍처에 대한 혼란을 초래했다.

근래에 들어서 YAML등과 같이 REST 에 대한 표준을 만들고자 하는 움직임은 있으나, JSON의 자유도를 제약하는 방향이고 Learning curve가 다소 높기 때문에, 그다지 확산이 되지 않고 있다.

이런 비표준에서 오는 관리의 문제점은, 제대로된 REST API 표준 가이드와, API 개발 전후로 API 문서(Spec)을 제대로 만들어서 리뷰하는 프로세스를 갖추는 방법으로 해결하는 방법이 좋다.


기존의 전통적인 RDBMS에 적용 시키기에 쉽지 않다.

예를 들어 리소스를 표현할 때,  리소스는 DB의 하나의 Row가 되는 경우가 많은데, DB의 경우는 Primary Key가 복합 Key 형태로 존재하는 경우가 많다. (여러 개의 컬럼이 묶여서 하나의 PK가 되는 경우) DB에서는 유효한 설계일지 몰라도, HTTP URI는 / 에 따라서 계층 구조를 가지기 때문에, 이에 대한 표현이 매우 부자연 스러워진다.

예를 들어 DB의 PK가 “세대주의 주민번호”+”사는 지역”+”본인 이름일 때” DB에서는 이렇게 표현하는 것이 하나 이상할 것이 없으나, REST에서 이를 userinfo/{세대주 주민번호}/{사는 지역}/{본인 이름} 식으로 표현하게 되면 다소 이상한 의미가 부여될 수 있다.

이외에도 resource에 대한 Unique한 Key를 부여하는 것에 여러가지 애로점이 있는데, 이를 해결하는 대안으로는 Alternative Key (AK)를 사용하는 방법이 있다. 의미를 가지지 않은 Unique Value를 Key로 잡아서 DB Table에 AK라는 필드로 잡아서 사용 하는 방법인데. 이미 Google 의 REST도 이러한 AK를 사용하는 아키텍쳐를 채택하고 있다.

그러나 DB에 AK 필드를 추가하는 것은 전체적인 DB설계에 대한 변경을 의미하고 이는 즉 REST를 위해서 전체 시스템의 아키텍쳐에 변화를 준다는 점에서 REST 사용시 아키텍쳐적인 접근의 필요성을 의미한다. 

그래서 근래에 나온 mongoDB나 CouchDB,Riak등의 Document based NoSQL의 경우 JSON Document를 그대로 넣을 수 있는 구조를 갖추는데,하나의 도큐먼트를 하나의 REST 리소스로 취급하면 되기 때문에 REST의 리소스 구조에 맵핑 하기가 수월하다.



  • REST API 이해와 설계 - #1 개념 잡기 http://bcho.tistory.com/953
  • REST API 이해와 설계 - #2 디자인 가이드  http://bcho.tistory.com/954
  • REST API 이해와 설계 - #3 보안 가이드  http://bcho.tistory.com/955


REST API의 이해와 설계

#1-개념 소개


REST는 웹의 창시자(HTTP) 중의 한 사람인 Roy Fielding의 2000년 논문에 의해서 소개되었다. 현재의 아키텍쳐가 웹의 본래 설계의 우수성을 많이 사용하지 못하고 있다고 판단했기 때문에, 웹의 장점을 최대한 활용할 수 있는 네트워크 기반의 아키텍쳐를 소개했는데 그것이 바로 Representational safe transfer (REST)이다.


REST의 기본

REST는 요소로는 크게 리소스,메서드,메세지 3가지 요소로 구성된다.

예를 들어서 “이름이 Terry인 사용자를 생성한다” 라는 호출이 있을 때

“사용자”는 생성되는 리소스 , “생성한다” 라는 행위는 메서드 그리고 ‘이름이 Terry인 사용자’는 메시지가 된다

이를 REST 형태로 표현해보면

HTTP POST , http://myweb/users/

{  

   "users":{  

      "name":"terry"

   }

}

와 같은 형태로 표현되며, 생성한다의 의미를 갖는 메서드는 HTTP Post 메서드가 되고, 생성하고자 하는 대상이 되는 사용자라는 리소스는 http://myweb/users 라는 형태의 URI로 표현이 되며, 생성하고자 하는 사용자의 디테일한 내용은 JSON 문서를 이용해서 표현된다.


HTTP 메서드

REST에서는 앞에서 잠깐 언급한바와 같이, 행위에 대한 메서드를 HTTP 메서드를 그대로 사용한다.

HTTP 에는 여러가지 메서드가 있지만 REST에서는 CRUD(Create Read Update Delete)에 해당 하는 4가지의 메서드만 사용한다.

메서드

의미

Idempotent

POST

Create

No

GET

Select

Yes

PUT

Update

Yes

DELETE

Delete

Yes


각각 Post,Put,Get,Delete는 각각의 CRUD 메서드에 대응된다. 여기에 idempotent라는 분류를 추가 했는데, Idempotent는 여러 번 수행을 해도 결과가 같은 경우를 의미한다.

예를 들어 a++는 Idempotent 하지 않다고 하지만(호출시마다 값이 증가 되기 때문에), a=4와 같은 명령은 반복적으로 수행해도 Idempotent하다. (값이 같기 때문에) 

POST 연산의 경우에는 리소스를 추가하는 연산이기 때문에, Idempotent하지 않지만 나머지 GET,PUT,DELETE는 반복 수행해도 Idempotent 하다. GET의 경우 게시물의 조회수 카운트를 늘려준다던가 하는 기능을 같이 수행했을 때는 Idempotent 하지 않은 메서드로 정의해야 한다.

Idempotent의 개념에 대해서 왜 설명을 하냐 하면, REST는 각 개별 API를 상태 없이 수행하게 된다. 그래서, 해당 REST API를 다른 API와 함께 호출하다가 실패하였을 경우, 트렌젝션 복구를 위해서 다시 실행해야 하는 경우가 있는데, Idempotent 하지 않은 메서드들의 경우는 기존 상태를 저장했다가 다시 원복해줘야 하는 문제가 있지만, Idempotent 한 메서드의 경우에는 반복적으로 다시 메서드를 수행해주면 된다.

예를 들어 게시물 조회를 하는 API가 있을때, 조회시 마다 조회수를 올리는 연산을 수행한다면 이 메서드는 Idempotent 하다고 볼수 없고, 조회하다가 실패하였을 때는 올라간 조회수를 다시 -1로 빼줘야 한다. 즉 Idempotent 하지 않은 메서드에 대해서는 트렌젝션에 대한 처리가 별다른 주의가 필요하다.


REST의 리소스

REST는 리소스 지향 아키텍쳐 스타일이라는 정의 답게 모든 것을 리소스 즉 명사로 표현을 하며, 각 세부 리소스에는 id를 붙인다.

즉 사용자라는 리소스 타입을 http://myweb//users라고 정의했다면, terry라는 id를 갖는 리소스는 http://myweb/users/terry 라는 형태로 정의한다.

REST의 리소스가 명사의 형태를 띄우다 보니, 명령(Operation)성의 API를 정의하는 것에서 혼돈이 올 수 있다.

예를 들어서 “Push 메서지를 보낸다”는 보통 기존의 RPC(Remote Procedure Call)이나 함수성 접근해서는 /myweb/sendpush 형태로 잘못 정의가 될 수 있지만, 이러한 동사형을 명사형으로 바꿔서 적용해보면 리소스 형태로 표현하기가 조금더 수월해 진다.

“Push 메시지 요청을 생성한다.”라는 형태로 정의를 변경하면, API 포맷은 POST/myweb/push 형태와 같이 명사형으로 정의가 될 수 있다. 물론 모든 형태의 명령이 이런 형태로 정의가 가능한 것은 아니지만, 되도록이면 리소스 기반의 명사 형태로 정의를 하는게 REST형태의 디자인이 된다. 

REST API의 간단한 예제

그러면, 간단한 REST API의 예제를 살펴보도록 하자 간단한 사용자 생성 API를 살펴보면

사용자 생성

다음은 http://myweb/users 라는 리소스를 이름은 terry, 주소는 seoul 이라는 내용(메시지)로 HTTP Post를 이용해서 생성하는 정의이다. 

HTTP Post, http://myweb/users/

{  

   "name":"terry",

   "address":"seoul"

}


조회

다음은 생성된 리소스 중에서 http://myweb/users 라는 사용자 리소스중에, id가 terry 인 사용자 정보를 조회해오는 방식이다. 조회이기 때문에, HTTP Get을 사용한다.

HTTP Get, http://myweb/users/terry

업데이트

다음은 http://myweb/users 라는 사용자 리소스중에, id가 terry 인 사용자 정보에 대해서, 주소를 “suwon”으로 수정하는 방식이다. 수정은 HTTP 메서드 중에 PUT을 사용한다.

HTTP PUT, http://myweb/users/terry

{  

   "name":"terry",

   "address":"suwon"

}


삭제

마지막으로 http://myweb/users 라는 사용자 리소스중에, id가 terry 사용자 정보를 삭제 하는 방법이다.

HTTP DELETE, http://myweb/users/terry


API 정의를 보면 알겠지만 상당히 간단하다. 단순하게 리소스를 URI로 정해준 후에, 거기에 HTTP 메서드를 이용해서 CRUD를 구현하고 메시지를 JSON으로 표현하여 HTTP Body에 실어 보내면 된다. POST URI에 리소스 id가 없다는 것을 빼면 크게 신경쓸 부분이 없다. 


REST의 특성

REST의 기본적인 개념을 이해했으면 조금 더 상세한 REST의 특성에 대해서 알아보도록 하자


유니폼 인터페이스(Uniform Interface)

REST는 HTTP 표준에만 따른 다면, 어떠한 기술이라던지 사용이 가능한 인터페이스 스타일이다. 예를 들어 HTTP + JSON으로 REST API를 정의했다면, 안드로이드 플랫폼이건, iOS 플랫폼이건, 또는 C나 Java/Python이건 특정 언어나 기술에 종속 받지 않고 HTTP와 JSON을 사용할 수 있는 모든 플랫폼에 사용이 가능한 느슨한 결함(Loosely coupling) 형태의 구조이다.

※ 흔히들 근래에 REST를 이야기 하면, HTTP + JSON을 쉽게 떠올리는데, JSON은 하나의 옵션일뿐, 메시지 포맷을 꼭 JSON으로 적용해야할 필요는 없다. 자바스크립트가 유행하기전에만 해도 XML 형태를 많이 사용했으며, 근래에 들어서 사용의 편리성 때문에 JSON을 많이 사용하고 있지만, XML을 사용할 경우, XPath,XSL등 다양한 XML 프레임웍을 사용할 수 있을뿐만 아니라 메시지 구조를 명시적으로 정의할 수 있는 XML Scheme나 DTD등을 사용할 수 있기 때문에, 복잡도는 올라가더라도, 메시지 정의의 명확성을 더할 수 있다. 


무상태성/스테이트리스(Stateless)

REST는 REpresentational State Transfer 의 약어로 Stateless (상태 유지하지 않음)이 특징 중의 하나이다.

상태가 있다 없다는 의미는 사용자나 클라이언트의 컨택스트를 서버쪽에 유지 하지 않는다는 의미로,쉽게 표현하면 HTTP Session과 같은 컨텍스트 저장소에 상태 정보를 저장하지 않는 형태를 의미한다.

상태 정보를 저장하지 않으면 각 API 서버는 들어오는 요청만을 들어오는 메시지로만 처리하면 되며, 세션과 같은 컨텍스트 정보를 신경쓸 필요가 없기 때문에 구현이 단순해진다.


캐슁 가능(Cacheable)

REST의 큰 특징 중의 하나는 HTTP라는 기존의 웹 표준을 그대로 사용하기 때문에, 웹에서 사용하는 기존의 인프라를 그대로 활용이 가능하다. 

HTTP 프로토콜 기반의 로드 밸런서나 SSL은 물론이고, HTTP가 가진 가장 강력한 특징중의 하나인 캐슁 기능을 적용할 수 있다.일반적인 서비스 시스템에서 60%에서 많게는 80%가량의 트렌젝션이 Select와 같은 조회성 트렌젝션인 것을 감안하면, HTTP의 리소스들을 웹캐쉬 서버등에 캐슁하는 것은 용량이나 성능 면에서 많은 장점을 가지고 올 수 있다.구현은 HTTP 프로토콜 표준에서 사용하는 “Last-Modified” 태그나 E-Tag를 이용하면 캐슁을 구현할 수 있다.

아래와 같이 Client가 HTTP GET을 “Last-Modified” 값과 함께 보냈을 때, 컨텐츠가 변화가 없으면 REST 컴포넌트는 “304 Not Modified”를 리턴하면 Client는 자체 캐쉬에 저장된 값을 사용하게 된다.


 

그림  1 Last Modified 필드를 이용한 캐슁 처리 방식

이렇게 캐쉬를 사용하게 되면 네트웍 응답시간 뿐만 아니라, REST 컴포넌트가 위치한 서버에 트렌젝션을 발생시키지 않기 때문에, 전체 응답시간과 성능 그리고 서버의 자원 사용률을 비약적으로 향상 시킬 수 있다.


자체 표현 구조(Self-descriptiveness)

REST의 가장 큰 특징 중의 하나는 REST API 자체가 매우 쉬워서 API 메시지 자체만 보고도 API를 이해할 수 있는 Self-descriptiveness 구조를 갖는 다는 것이다. 리소스와 메서드를 이용해서 어떤 메서드에 무슨 행위를 하는지를 알 수 있으며, 또한 메시지 포맷 역시 JSON을 이용해서 직관적으로 이해가 가능한 구조이다. 

대부분의 REST 기반의 OPEN API들이 API 문서를 별도로 제공하고 있지만, 디자인 사상은 최소한의 문서의 도움만으로도 API 자체를 이해할 수 있어야 한다.


클라이언트 서버 구조 (Client-Server 구조)

근래에 들면서 재 정립되고 있는 특징 중의 하나는 REST가 클라이언트 서버 구조라는 것이다. (당연한 것이겠지만).

REST 서버는 API를 제공하고, 제공된 API를 이용해서 비즈니스 로직 처리 및 저장을 책임진다.

클라이언트의 경우 사용자 인증이나 컨택스트(세션,로그인 정보)등을 직접 관리하고 책임 지는 구조로 역할이 나뉘어 지고 있다.  이렇게 역할이 각각 확실하게 구분되면서, 개발 관점에서 클라이언트와 서버에서 개발해야 할 내용들이 명확하게 되고 서로의 개발에 있어서 의존성이 줄어들게 된다.

계층형 구조 (Layered System)

계층형 아키텍쳐 구조 역시 근래에 들어서 주목받기 시작하는 구조인데, 클라이언트 입장에서는 REST API 서버만 호출한다.

그러나 서버는 다중 계층으로 구성될 수 있다. 순수 비즈니스 로직을 수행하는 API 서버와 그 앞단에 사용자 인증 (Authentication), 암호화 (SSL), 로드밸런싱등을 하는 계층을 추가해서 구조상의 유연성을 둘 수 있는데, 이는 근래에 들어서 앞에서 언급한 마이크로 서비스 아키텍쳐의 api gateway나, 간단한 기능의 경우에는 HA Proxy나 Apache와 같은 Reverse Proxy를 이용해서 구현하는 경우가 많다.


REST 안티 패턴

REST의 개념과 전체적인 특징에 대해서 살펴보았다. 이제는 REST 디자인시 하지 말아야 할 안티 패턴에 대해서 알아보도록 하자.


GET/POST를 이용한 터널링

가장 나쁜 디자인 중 하나가 GET이나 POST를 이용한 터널링이다.

http://myweb/users?method=update&id=terry 이 경우가 전형적인 GET을 이용한 터널링이다. 메서드의 실제 동작은 리소스를 업데이트 하는 내용인데, HTTP PUT을 사용하지 않고, GET에 쿼리 패러미터로 method=update라고 넘겨서, 이 메서드가 수정 메세드임을 명시했다.

대단히 안좋은 디자인인데, HTTP 메서드 사상을 따르지 않았기 때문에, REST라고 부를 수 도 없고, 또한 웹 캐쉬 인프라등도 사용이 불가능하다.

또 많이 사용하는 안좋은예는 POST를 이용한 터널링이다. Insert(Create)성 오퍼러이션이 아닌데도 불구하고, JSON 바디에 오퍼레이션 명을 넘기는 형태인데 예를 들어 특정 사용자 정보를 가지고 오는 API를 아래와 같이 POST를 이용해서 만든 경우이다. 

HTTP POST, http://myweb/users/

{  

   "getuser":{  

      "id":"terry",   

}

}


Self-descriptiveness 속성을 사용하지 않음

앞서 특징에서 설명한 바와 같이 REST의 특성중 하나는 자기 서술성(Self-descriptiveness) 속성으로 REST URI와 메서드 그리고 쉽게 정의된 메시지 포맷에 의해서 쉽게 API를 이해할 수 있는 기능이 되어야 한다. 

특히나 자기 서술성을 깨먹는 가장 대표적인 사례가 앞서 언급한 GET이나 POST를 이용한 터널링을 이용한 구조가 된다.


HTTP Response code를 사용하지 않음

다음으로 많이 하는 실수중의 하나가 Http Response code를 충실하게 따르지 않고, 성공은 200, 실패는 500 과 같이 1~2개의 HTTP response code만 사용하는 경우이다. 심한 경우에는 에러도 HTTP Response code 200으로 정의한후 별도의 에러 메시지를 200 response code와 함께 보내는 경우인데, 이는 REST 디자인 사상에도 어긋남은 물론이고 자기 서술성에도 어긋난다.


오랜만의 블로그 포스팅입니다.

그 동안 예전 REST 관련 글들이 오래되서, 새로운 디자인 가이드와 보안 가이드들을 추가해서 업데이트 하였습니다

많은 공유와 피드백 부탁드립니다.


  • REST API 이해와 설계 - #1 개념 잡기 http://bcho.tistory.com/953
  • REST API 이해와 설계 - #2 디자인 가이드  http://bcho.tistory.com/954
  • REST API 이해와 설계 - #3 보안 가이드  http://bcho.tistory.com/955