Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

EasyMock : RESTful API Mock 서버

요즘에 웹서비스를 만들면 거의 RESTful API를 만들게 되는데 Google API나 Github API처럼 이미 만들어져 있는 서비스의 Open API를 사용하는 것이 아니라면 서버쪽과 클라이언트쪽이 동시에 개발을 진행하게 된다. 이런 경우 클라이언트쪽에 부담이 커지는데 API 스펙을 미리 확인했다고 하더라도 서버가 아직 없으므로(있더라도 일부만) 클라이언트쪽 작업이 지연되거나 Mocking을 해서 반환값을 가정하에 두고 개발을 해야 한다는 점이다. 서버쪽에서 협의된 API 스펙을 Mock서버를 구현해 준다면 상황이 좀 나아질테지만 개발하기도 바쁜데 이런걸 구현하는 것 자체가 쉽지 않다.

EasyMock

EasyMock이라는 도구는 FRENDS 모임을 하다가 odyss님을 통해서 알게 되었다. 자바에서 유닛테스트를 작성할 때 사용하는 EasyMock과는 다른 도구다. EasyMock은 Node.js로 만들어진 도구인데 간단하게 JSON으로 스펙을 정의하면 이를 기반으로 해서 API Mock 서버를 띄워주는 도구이다.

API를 통한 서버와 클라이언트 작업에 대해서 많이 생각을 하고 있지만 일단 미리 서로 스펙을 확정하는게 좋은 것 같다. 프로젝트의 요구사항이 변하듯이 API 스펙도 변하기 마련이지만 스펙이 변경될 경우 서버쪽 보다는 클라이언트 쪽의 리스크가 너무 크고 원칙적으로는 변경시마다 공유가 되어야 하지만 현실적으로는 그렇게 되지 않는 경우가 많다. 그리고 공유가 되어야 한다는 점을 더 강조하기 위해서도 사전에 API를 스펙화하고 이를 구현하다가 변경사항이 생겼을 경우 이 변경을 클라이언트 쪽에서도 추적할 수 있어야 한다.

EasyMock을 썼을 때의 좋은 점은 JSON을 이용해서 상당히 적은 노력으로 API 스펙을 문서화함과 동시에 Mock서버까지 띄워주고 꽤 다양한 경우의 케이스도 커버할 수 있다. 그리고 API 스펙이 변경되어야 하는 경우 EasyMock에 정의한 내용을 변경해야 하지만(당연히 추가적으로 신경을 써야) JSON 파일이 변경되는 것이므로 형상관리를 통해서 어떻게 변경되었는지 추적하는 것이 쉽다. 즉, 서버쪽에서 EasyMock으로 API Mock서버를 관리하고 이를 바탕으로 클라이언트를 개발하다가 나중에 Mock를 실제서버로 교체하면 될 것으로 보인다.

불안요소는 EasyMock이 현재 0.2.2버전으로 상당히 낮은 버전이라는 점이다. Patrick Boos라는 개발자가 혼자서 개발하고 있는 현실이고 꽤 많은 긴으이 들어있기는 하지만 아쉬운 점이 없는 것은 아니다. 그리고 올라온 이슈나 Fork 수 등을 봐도 아직 많이 알려지지 않았다. 약간 만져본 결과 버전이 올라갔을 때 기대가 되는 프로젝트이지만 현재로써는 불안한 점임에는 틀림없다. 그리고 문서화도 기본적인 내용만 되어 있어서 세부적인 건 직접 테스트를 좀 해봐야하고 문서화라고는 README파일이 전부다.

EasyMock은 글로벌 설치를 한 뒤에 EasyMock용 API 스펙이 정의된 폴더에서 easymock 명령어를 실행하면 된다.

$ npm install -g easymock
$ easymock


EasyMock의 설정

EasyMock은 RESTful API를 타게팅해서 만들어졌고 JSON 파일은 기본적으로 _get.json, _post.json, _put.json, _delete.json과 같은 파일명을 가지고 폴더 구조를 그대로 따라간다. 예를 들어 다음과 같은 _get.json파일이 루트디렉토리에 있다고 하면(유효한 JSON이어야 한다.)

{
  "success": true
}

GET 요청을 받아서 해당 JSON을 그대로 반환해 준다.

$ curl -X GET -i http://localhost:3000/
HTTP/1.1 200 Here you go Bro
x-powered-by: Express
content-type: application/json; charset=utf-8
content-length: 21
date: Fri, 25 Oct 2013 02:11:28 GMT
connection: keep-alive

{
  "success": true
}

POST에 대한 JSON파일이 존재하지 않으므로 POST 요청을 보내면 당연히 404가 반환된다.

$ curl -X POST -i http://localhost:3000/
HTTP/1.1 404 Sorry, not here Bro
x-powered-by: Express
content-type: text/plain
content-length: 70
date: Fri, 25 Oct 2013 02:12:20 GMT
connection: keep-alive

Error: ENOENT, stat '/Users/outsider/projects/etc/api-mock/index.html'

그리고 users/_post.json파일을 만들었으면 http://localhost:3000/users/로 POST 요청을 보냈을 때 해당 JSON이 리턴된다.(마지막에 /를 붙혀주어야 하는데 이건 수정되어야 할듯...)

커스텀 헤더 및 주석

문서화를 위한 주석이나 필요한 헤더를 지정할 수도 있다.

# 성공
> Parameters:
> - id
< @header X-GitHub-Media-Type: github.beta
{
  "success": true,
  "id": 1
}

위와같이 지정했다고 하면 Github가 하는 것처럼 커스텀 미디어타입을 헤더에 지정할 수도 있고 기본적인 헤더도 원하는 형태로 덮어쓸 수 있다.

$ curl -X GET -i http://localhost:3000/
HTTP/1.1 200 Here you go Bro
x-powered-by: Express
x-github-media-type: github.beta
content-type: application/json; charset=utf-8
content-length: 32
date: Fri, 25 Oct 2013 02:26:09 GMT
connection: keep-alive

{
  "success": true,
  "id": 1
}

EasyMock의 API 문서

EasyMock은 앞에서 작성한 API를 이용해서 자동으로 문서화도 해주는데 앞에서 JSON 파일의 내용과 주석으로 추가한 부분을 포함해서 API 문서를 생성한다. http://localhost:3000/_documentation/로 접속하면 위와 같은 형태로 문서화가 된 것을 볼 수 있다. JSON으로 API 스펙을 작성하고 Mock서버를 띄움과 동시에 문서까지 제공할 수 있어서 상당히 편리하다.

config.json

전체 API 서버에 대한 설정을 config.json파일에 할 수 있다. 이 파일은 EasyMock을 실행하는 루트경로에 위치하는데 다음과 같이 생겼다.

{
  "simulated-lag": 1000,
  "cors": true,
  "jsonp": true,
  "proxy": {},
  "variables": {
    "server": "http://server.com"
  },
  "routes": []
}

simulated-lag는 지연시간이다. 실제 서버처럼 동작하도록 응답을 반환할 때 원하는 만큼 지연시간을 줄 수 있고 CORS나 JSONP를 사용한다면 여기서 설정을 하면 된다. corstrue로 설정하면 CORS관련 헤더가 응답에 추가되고 jsonptrue로 할 경우 요쳥 URL뒤에 ?callback=somthing처럼 파라미터를 추가하면 응답 JSON을 something함수에 담아서 반환해 준다. proxy는 안써봐서 잘 모르겠고 variables는 API 서버 전체에서 사용할 변수를 여기서 정의할 수 있다. JSON 파일내에서 { "image": "#{server}/img.jpg" }와 같이 작성할 경우 server라는 변수를 여기서 정의한 값으로 바꾸어서 반환한다.

패스 파라미터

위에서 아주 기본적인 설정을 만들었지만 실제로 API Mock서버를 구성하려면 훨씬 다양한 옵션이 필요한데 EasyMock이 상세설정을 어느 정도 제공하고 있다.(전부는 아니다.) 예를 들어 /users/whatever로 들어오는 모든 요청은 users/_get.json파일이 받게 되지만 실제로는 /users/1이나 /users/name과 같이 사용하는 것이 더 일반적이고 당연히 값이 달라져야 한다. 예를 들어 users/1_get.json라는 파일을 다음과 같이 만들면

# 사용자 정보 조회
> Parameters:
> - userId
{
  "userId": 1,
  "nickname": "outsider",
  "job": "programmer"
}

/users/1로 들어오는 요청을 이 파일이 받고 users/outsider_get.json와 같이 만들 경우에는 /users/outsider에 매핑된다.

임의의 아이디로 연결할 수도 있는데 이런 경우에는 config.json에 설정을 추가해야 한다.

"routes": [
  "/users/:userId"
]

config.jsonroutes부분에 위와 같이 userId라는 값이 패스파라미터임을 지정하면 users/userId_get.json라는 파일을 다음과 같이 생성할 수 있다.

# 사용자 정보 조회
> Parameters:
> - userId
{
  "userId": "#{userId}",
  "nickname": "outsider",
  "job": "programmer"
}

이런 경우 #{userId}부분에 패스파라미터로 전달된 값이 자동으로 들어가게 된다. 쿼리 파라미터(?q=test처럼)로 들어온 값도 동일하게 #{ }표현식을 이용해서 응답에 사용할 수 있다. 주의할 점은 이 값이 문자열일 경우에는 쌍따옴표(")로 감싸주지 않으면 JSON이 유효하지 않아서 오류가 난다는 것이다.

패스 파라미터가 여러개 이어지는 경우에도 동일한 규칙으로 만들 수 있다. config.json에 다음과 같이 라우팅 규칙을 추가하고

"routes": [
  "/teams/:teamId",
  "/teams/:teamId/:playerId"
]

teams/teamId_get.json파일과 teams/teamId/playerId_get.json 파일을 생성하면 된다. 물론 playerId_get.json파일에서는 teamIdplayerId의 변수를 모두 사용할 수 있다.

템플릿

위와 같은 규칙으로 API 서버를 구성하려면 비슷한 형태에서 약간의 값만 다른 JSON 파일을 만들어야 하는데 이런 경우 템플릿 기능을 이용할 수 있다. 템플릿 파일들은 모두 _templates 폴더 아래 위치한다. _templates/teams.json이라는 템플릿을 다음과 같이 만들었다면

{
  "name": "team name",
  "city": "Seoul"
}

teams/_get.json파일에서 다음과 같이 사용할 수 있다.

{
  "results": [
    "{{teams}}",
    "{{teams}}",
    "{{teams}}"
  ]
}

이에 대한 요청을 보내면 다음과 같이 템플릿을 사용해서 변환된 결과가 반환된다.

curl -X GET http://localhost:3000/teams/
{
  "results": [
    {
      "name": "team name",
      "city": "Seoul"
    },
    {
      "name": "team name",
      "city": "Seoul"
    },
    {
      "name": "team name",
      "city": "Seoul"
    }
  ]
}

하지만 이렇게 값이 동일한 템플릿을 계속 사용할 일은 현실적으로 없기 때문에 템플릿에서도 파라미터를 사용할 수 있다. _templates/teams.json파일에서 다음과 같이 변수를 사용해서 정의를 하고

{
  "name": "#{_1}",
  "city": "#{_2}"
}

teams/_get.json파일에서 템플릿을 사용하면서 다음과 같이 파라미터를 전달할 수 있다.(이런저런 테스트를 해봤는데 파라미터에서 공백을 사용하는 방법은 아직 못 찾았다.)

{
  "results": [
    "{{teams(Bulls,Chicago)}}",
    "{{teams(Pistons,Detroit)}}",
    "{{teams(Knicks,NewYork)}}"
  ]
}

이제 요청을 보내면 다음과 같이 각기 다른 리스트가 이쁘게 반환된다.

$ curl -X GET http://localhost:3000/teams/
{
  "results": [
    {
      "name": "Bulls",
      "city": "Chicago"
    },
    {
      "name": "Pistons",
      "city": "Detroit"
    },
    {
      "name": "Knicks",
      "city": "NewYork"
    }
  ]
}


아쉬운 점

소소한 버그나 설정의 아쉬움 등(위에서 간간히 언급했던) 부분은 차차 해결될 꺼라고 하더라도 얼마전에 포스팅했던 I/O docs랑 병행해서 사용하고 있는데 사실 이렇게 되면 중복해서 관리해야하는 부담이 좀 있다.(둘은 용도가 좀 달라서 하나만 쓰기도 좀 머하고...) 좀 더 아름다운 모양을 생각한다면 EasyMock의 문서화가 I/O docs같은 형태로 제공되거나 한번의 작업으로 두 가지가 다 해결되었으면 하는 바램이 좀 있다. 아무튼 맨앞에 얘기했던대로 좀 더 인기있는 프로젝트가 되어서 프로젝트 개발상태에 대한 불안감이 좀 더 사라지길 바랄 뿐...

2013/10/25 15:09 2013/10/25 15:09