1.0 RESTful API

오래전 매시업이라는 단어가 많이 사용됐다고 한다. 구글 지도를 비롯해 클라이언트에서 처리할 수 없는 대용량 데이터, 날씨 정보 등 관칙 시스템에서 보내는 실시간 정보를 취급하는 웹 서비스가 많이 만들어졌다. 이러한 서비스는 웹 브라우저에서 사용하는 것을 전제로 만들어졌지만, API도 제공하고 있어 웹 서비스를 조합해 사용할 수도 있다. 웹 서비스를 결합해 새로운 부가가치 창출 방법을 '매시업'이라고 한다. 이렇게 웹 서비스의 인터페이스로서 HTTP를 기반으로 한 서버/클라이언트 간 통신이 널리 이용되게 되었다. 

 

REST는 로이 필딩 씨가 2000년에 네트워크를 기반으로 하는 소프트웨어 아키텍처 논문에서 발표한 것이다. 필딩 씨는 HTTP 책정에도 관여했고, 아파티 웹 서버 프로젝트의 공동 설립자 중 한 사람이기도 하다. REST는 매시업이 활발하게 이루어지게 되고, 웹이 브라우저에서 웹 애플리케이션 간의 연계를 위한 것으로 용도가 넓어지면서 알려지고 사용되기 시작했다. 구글의 디자인 가이드에 따르면, 2010년에는 전 세계의 네트워크를 통해 공개된 API의 74%가 REST API로 되어 있다.

 

REST는 HTTP 신택스를 프로토콜의 토대로 사용했는데, 통신의 시맨틱도 HTTP를 따르자는 생강기다. 대략적으로 아래와 같은 특성을 가지고 있다.

  • API가 웹 서버를 통해 제공된다.
  • GET /users/[사용자 ID]/repositories처럼 경로에 메서드를 보내 서비스를 얻는다.
  • API가 성공했는지 스테이터스 코드로 클라이언트에 알려준다.
  • URL은 리소스의 위치를 나타내는 표현이고, 서비스의 얼굴로서 중요하다.
  • 필요에 따라 GET 매개변수, POST의 바디 등 추가 정보를 보낼 수도 있다.
  • 서버에서 오는 반환값으로는 JSON 또는 XML과 같은 구조화 텍스트나 이미지 데이터 등이 반환되는 경우가 많다.

클라이언트 관점에서는 서버에서 아래와 같은 것을 기대할 수 있다.

  • URL은 리소스의 계층을 나타낸 경로로 되어있다. 명사로만 구성된다.
  • 리소스에 대해 HTTP 메서드를 보내 리소스 취득, 갱신, 추가 등의 작업을 한다.
  • 스테이터스 코드를 보고 요청이 제대로 처리됐는지 판정할 수 있다.
  • GET 메서드는 여러 번 호출해도 상태를 변경하지 않는다.
  • 클라이언트 쪽에서 관리해야 할 상태가 존재하지 않고, 매번 요청을 독립적으로 발행할 수 있다.
  • 트랜잭션은 존재하지 않는다.

REST 원칙을 준수한 것을 RESTful이라고 표현한다.

 

1.1 REST API와 RPC의 차이

웹 서비스와 인터페이스 설계 아키텍처는 REST만이 아니다. RPC에 따른 API와 REST에 따른 API는 설계 철학이 다르다. 

 

RPC의 경우 URL은 하나로, 요청할 때 서비스 이름과 메서드 이름을 전달해 '무엇을 할 것인가' 지정한다. 객체지향적 관점에서 설명하면, 웹 서버를 통해 공개된 정적 오브젝트의 인스턴스를 URL을 통해 발견하고 그 메서드를 호출한다고 비유할 수 있다. 호출하는 창구는 URL 하나이며, HTTP 메서드는 모두 POST이다. 서버에 대한 요청은 모두 바디에 넣어 보낸다. HTTP 서버의 로그를 보더라도 동일한 URL에 대한 액세스가 있다는 것밖에는 알 수 없다.

 

REST로 정보를 갱신할 때는 수정된 리소스를 첨부해서 '이걸로 덮어쓰라'고 전송한다. GET으로 가져올 수 있는 실제 리소스 이외에는 모두 메타데이터이다. 메타데이터의 취득과 수정은 헤더로 한다.

 

RPC의 경우는 데이터든 메타데이터든 똑같이 다룬다. 변경이나 응답은 전용 명령을 사용한다. 실제 동작은 API로 구현하기 나름이지만, 리소스 자체를 그대로 전송하는 디자인은 거의 없다.

 

애플리케이션에 따라 어떤 표현이 다루기 쉬운지 이름적으로는 정할 순 없다. 최근에는 드롭박스가 API를 버전 업하며 REST API를 버리고 RPC로 변경했다는 소식이 화제가 됐다.

 

1.2 Web API와 트랜잭션

RPC의 경우 트랜잭션과 비슷한 API를 제공할 수도 있지만, 트랜잭션이 필요한 단위로 원자적인 API를 제공하는 것이 현재 현실적인 해법이다.

 

JSON-RPC는 여러 요청을 배열로 모아 동시에 발행할 수 있다. 키 값 스토어 Redis API의 멀티를 사용하면 여러 명령을 묶어 동시에 실행할 수 있다. RPC가 모두 이러한 호출을 지원하는 것은 아니지만, 이렇게 여러 명령을 원자적으로 (도중에 끼어들지 못하게) 실행하면 간단한 트랜잭션처럼 다룰 수 있다. 데이터를 가져와서 가공하고 다시 데이터베이스에 넣으려면 여러 명령을 묶는 것만으로는 실현할 수 없어, 도중에 인터럽트가 발생해 병렬 처리 특유의 문제가 일어날 가능성이 있다.

 

Redis는 Lua를 사용해 서버 측에서 처리함으로써 이런 경우에 대처할 수 있게 되어 있다. 또한 MongoDB는 단순한 증가보다도 복잡한 데이터 가공을 하나의 쿼리 호출로 할 수 있게 되어 있다. 그러나 둘 다 클라이언트와 서버 양쪽에서 API 구현이나 이용에 드는 수고가 적지 않다. 트랜잭션이 필요한 경우에는 그에 상응하는 복잡한 구현이 필요하다. 

 

중요한 것은 가능한 한 원자 레벨로 API르르 나누는 것이다. Redis와 MongoDB는 일반 저장소로서 작은 단위의 API가 제공되지만, 웹 API는 가급적 한 번에 끝내는 것이 중요하다. 특히 RPC의 경우 아파치 스리프트나 구글 gRPC 등 서버와 클라이언트의 소스 코드 생성기를 사용해 구현하는 경우가 많다. 이런 경우에는 여러 요청을 동시에 발행할 수도 없다. 이 점은 HTTP도 마찬가지다. HTTP도 한 번의 액세스에 하나의 메서드가 호출된다. 복수의 요청을 원자적으로 실행할 수는 없다. HTTP/2에서는 호출 순서와 API 서버에서 처리되는 순서 또는 응답이 돌아오는 순서가 같다는 보증도 없어진다.

 

클라이언트 입장에서는 모아서 한꺼번에 처리하고 싶은 대상이 있다고 해도, 그에 해당하는 API가 반드시 있고, 그 API 하나만 호출하면 필요한 일을 할 수 있는 형태가 이상적이다. 하지만 이 이상에도 제약이 있다. "Web API: The Good Parts"에서는 다수의 불특정 개발자를 위한 API를 LSUDs라는 말로 설명한다. 다수가 사용하는 API에서는 유스 케이스를 좁힐 수 없어, 아무리 해도 크기가 작은 API 집합으로 할 수 밖에 없다. 

 

1.3 HATEOAS

HATEOASHypermedia As The Engine Of Application State는 REST의 일부인 '인터페이스의 일반화'를 사양으로 만든 것이다. 리소스 간의 관련 정보와 리소스의 조작 링크를 응답에 포함시킴으로써 리소스의 자가서술력을 향상시킨다. REST API의 성숙도 수준에서 최상위에 위치한다. 

HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: ...

<?xml version="1.0"/>
<account>
    <account_number>12345</account_number>
    <link rel="deposit" href="https://example.com/account/12345/deposit" />
    <link rel="withdraw" href="https://example.com/account/12345/withdraw" />
    <link rel="transfer" href="https://example.com/account/12345/transfer" />
    <link rel="close" href="https://example.com/account/12345/close" />
</account>

위는 HATEOAS의 예제이다. <link> 태그로 주어진 정보가 HATEOAS를 위해 추가된 것으로, 이 오브젝트에 대해 실행할 수 있는 작업과 관련 오브젝트의 링크를 포함한다. API에 대한 사전 지식이 전혀 없이 링크를 따라가는 것만으로 API의 기능을 충분히 사용할 수 있게 되는 것이다. 사용자가 브라우저로 웹사이트를 탐색할 때도 페이지의 내용을 보고 링크를 따라가며 원하는 페이지를 찾는다. 그런 일을 API가 할 수 있게 되는 것이 HATEOAS가 이상으로 하는 상태이다.

 

1.4 RESTful와 REST-ish

REST API는  계층화된 리소스이다. 예를들어, URL에 동사가 들어가거나 /api 문자가 들어가고, 포맷을 나타내는 문자(/json)나 버전이 들어가는 등 원래는 리소스의 주소여야 하는 URL에 불필요한 정보가 들어가는 사례도 볼 수 있다. REST를 제안한 필딩 씨는 이렇게 설계된 웹 서비스가 'REST'를 자칭하면, 격렬하게 공격하는 것으로 유명하다. 

 

기본적으로는 REST를 지키고 싶지만, 완전히 RESTful하게 됐는지 자신이 없을 때가 있다. 그럴 때는 'REST-ish'라고 부르는 방법이 있다.

 

2.0 메서드

REST API를 이용할 때는 서비스가 지정하는 메서드를 사용한다. 기본적으로 사용하는 메서드는 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS로 7가지이다. 이 중 자주 사용되는 것은 GET, POST, PUT, DELETE 4가지이다.

 

HTTP 사양에서 메서드의 분류에는 '안전'과 '멱등'이라는 두 가지 지표가 있다. '안전'은 실행해도 리소스를 변화시키지 않는 것이다. '멱등'은 서버의 리소스는 변경되지만, 몇 번을 실행해도 결과가 바뀌지 않는다는 것이다. GET, HEAD, OPTIONS는 안전한 메서드이고, PUT, DELETE는 멱등한 메서드로 정의되어 있다. RESTful한 API에서도 이 규칙을 따라야 한다.

 

 

3.0 스테이터스 코드

API를 이용할 때 스테이터스 코드는 상세한 오류 내용이나 서버의 상황을 알려주는 귀중한 정보원이 된다. 오류 발생 시 어떤 스테이터스 코드를 반환할 것인지는 서비스에 따라 차이가 있고, 오류 유형 별로 반환할 스테이터스 코드 목록을 문서로 제공하기도 한다. 

 

3.1 100번대 (정보)

100번대는 성공이나 실패가 결정되기 이전 상태라는 것을 나타낸다.

  • 100 Continue
  • 101 Switching Protocols
  • 102 Processing
  • 103 Early Hints

둘 다 바디는 포함하지 않는다. 

 

3.2 200번대 (성공)

정상 종료했을 때 200번대의 응답이 반환된다.

  • 200 OK
  • 201 Created
  • 202 Accepted
  • 203 Non-Authoriative Information
  • 204 No Content
  • 205 Reset Conent
  • 206 Parial Content
  • 207 Multi-Status
  • 208 Already Reported
  • 226 IM Used

 

3.3 300번대 (리디렉트)

  • 300 Multiple Choices
  • 301 Moved Permanently
  • 302 Found
  • 303 See Other
  • 304 Not Modified
  • 307 Temporary Redirect
  • 308 Permanent Redirect

 

3.4 400번대 (클라이언트 오류)

400번대 스테이터스 코드는 클라이언트에서 기인하는 오류이다. 요청할 URL이 틀렸거나 필요한 헤더가 부족할 때 등등 반환된다.

 

400 번대의 스테이터스 코드는 너무 많은 관계로 궁금하다면 링크를 통해 확인할 수 있다.

 

3.5 500번대 (서버 오류)

500번대 스테이터스 코드는 서버 오류로 다양한 종류가 있지만, 클라이언트 요청에는 문제가 없기 때문에 서비스를 받으려면 서버가 복구될 때까지 기다리는 수밖에 없다.

 

500 번대의 스테이터스 코드도 링크를 통해 확인할 수 있다.

 

 

4.0 바디

바디에는 서버로부터 반환된 정보가 저장된다. 웹 API가 공개되기 시작한 무렵은 거의 XML이였지만, 최근은 JSON을 사용하는 경우가 많다. RESTful API의 엄격한 규칙을 따른다면, Accept: application/json 헤더를 부여하고 서버와 니고시에이션하는 것이 관습이지만, JSON만 지원하는 경우라면 헤더를 부여하지 않아도 서버는 응답해줄 것이다.

 

5.0 실제 REST API 살펴보기 (PAY.jp)

깃허브나 슬랙 등 많은 서비스가 REST API를 제공한다. 브라우저로 조작할 필요가 있었떤 작업을 자동화할 수 있다. PAY.jp라는 결제 플랫폼이 있다. 해당 API 이용자는 토큰을 사용해야한다. 웹사이트에 로그인하면 액세스 토큰을 생성하고 다운로드할 수 있다. 대부분이 OAuth2를 자주 사용하고 있고 해당 플랫폼도 OAuth2를 사용한다. 사용자 ID와 암호를 사용하지 않고 토큰을 사용하는 이유는 만약 유출이 되어도 토큰 단위로 무효로 할 수 있기 때문이다. 

 

토큰을 어떻게 서비스 전달할지는 서브시에 따라 차이가 있지만, PAY.jp는 BASIC 인증의 사용자 이름으로 전달한다. BASE64로 인코딩된 액세스 토큰을 Authorization 헤더에 넣어 서버에 전달한다.

 

PAY.js의 통신 프로토콜은 HTTPS, 메서드는 GET/POST/DELETE, 응답 형식은 JSON으로 제공한다. 

 

6.0 실제 REST API 살펴보기 (Github)

구글이 Go 언어용 깃허브 API 액세스 라이브러리를 제공하므로 직접 액세스해볼 필요는 없을지도 모르지만, 모범 코드가 있으니 확인해보자. 링크에 잘 정리가 되어있으니 확인해보고 싶으면 살펴봐도 좋다.

 

7.0 REST API에 액세스할 때 주의할 점

7.1 타임아웃

클라이언트를 작성할 때 Context를 넘겨주곤한다. 이 Context는 취소 처리, 시간 만료 등을 통일적으로 처리하는 방법으로서 Go 언어 1.7부터 도입됐다. 비동기인 여러 기능을 실행하거나 Goroutine을 넘어선 처리를 실행할 때 바텀업으로 타임아웃 처리를 하려고 하면, 읽기 편한 코드를 쓸 수 없다. 또한 여러 부분으로 나누어진 처리를 할 때도 Context 자체가 데이터를 가질 수 있도록 되어 있어, 세션 간에 공유하는 데이터 등을 가지게 할 수 있다.

 

아래 코드는 2초 후 타임 아웃이 된다. 함수를 빠져나올 때 cancel()을 호출한다. 

import (
	"context"
	"time"
)

func timeout() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	// 타임아웃 기능이 있는 클라이언트
	oauth2.NewClient(ctx, conf.TokenSource(ctx, token))
	// 긴 처리
}

 

7.2 액세스 수 제한

어떤 웹 서비스든 1초당 호출 횟수의 상한이 정해져 있따. 대개 1초에 10회 정도로 설정되어 있을 것이라고 생각한다. 액세스 속도를 조정하는 패키지가 준표준 라이브러리에 있다.

  • https://godoc.org/golang.org/x/time/rate
$ go get golang.org/x/time/rate

 

rate 패키지를 이용한 액세스 수 제한 (go 코드)

package main

import (
	"context"
	"time"

	"golang.org/x/time/rate"
)

func main() {
	// 1초당 상한 횟수
	RateLimit := 10
	// 토큰의 최대 보유 수
	BucketSize := 10
	ctx := context.Background()
	e := rate.Every(time.Second/RateLimit)
	l := rate.NewLimiter(e, BucketSize)

	for _, task := range tasks {
		err := l.Wait(ctx)
		if err != nil {
			panic(err)
		}
		// 여기서 task 실행
	}
}

 

 

 

 

Reference

  • 리얼월드 HTTP

+ Recent posts