[Node.js] TDD를 이용하여 API 서버를 개발해보자.

2020. 11. 4. 23:49NodeJS/TDD

들어가며

 다음과 같은 경험과 배경지식이 있다면, 이번 포스팅 글을 어렵지 않게 읽을 수 있습니다.

  • ES6 문법, ExpressJS를 써본 경험

  • 모카(MochaJs), 슈드(ShouldJs), 슈퍼 테스트(SupertestJs)를 써본 경험

  • TDD의 정의와 목적

  • Babel 컴파일러 설치 및  사용 경험

 

API(Application Programming Interface)

 API란 무엇일까? API응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 말한다. 이상 위키피디아에서 나온 정의였다. 우리는 위키피디아 정의에서 인터페이스의 정의에 대해 생각해 볼 필요가 있다. 네이버 영영사전에서 기록된 인터페이스(Interface)의 뜻은 접속기다. 접속이란 단어는 게임을 한 번쯤 해본 우리에게 굉장히 익숙하고 친근한 단어다. 예를 들어, 우리는 메이플스토리라는 게임을 하기 위해 게임 접속을 한다. 게임접속은 게임과 사용자를 연결해주는 역할을 한다.

 

 

 단지 대상만 다를 뿐 API도 마찬가지다. API는 두 개 이상 프로그램을 서로 연결시켜주는 매개체다. 연결이 된다면, 사용자는 자신이 원하는 대로 프로그램 정보를 다운받을 수 있을 뿐만 아니라, 수정, 삭제, 심지어 자신이 원하는 정보를 추가할 수 있다. API는 우리에게 굉장히 일상적이고 흔한 인터페이스다. 

 

 

[그림 1] 네이버 날씨(https://weather.naver.com/)

 

 

 [그림 1]오늘의 날씨를 알아보기 위해, 내가 매일 접속하는 네이버 날씨라는 웹페이지다. 네이버 날씨의 경우에도 API가 적용되고 있다. 즉, 네이버 날씨를 디스플레이 하는 클라이언트 프로그램과 날씨정보를 제공하는 서버 프로그램이 API로 하여금 연결된 것이다. 이렇게 날씨 서비스(정보)를 제공해주는 서버를 API 서버라고 한다. 그렇다면 API 서버는 날씨 정보를 어떻게 제어하는 것일까?

 

 

 데이터 정보CRUD(Create, Read, Update, Delete) 방식으로 다뤄진다. 웹 데이터도 마찬가지다. 다만 REST라는 HTTP 아키텍처의 원칙을 지켜야한다. 여기서 REST란, 웹의 장점을 최대한 활용할 수 있도록 HTTP 프로토콜에 맞게 디자인된 아키텍처를 말한다. REST의 기본원칙을 철저히 지킨 디자인을 Restful이라한다.

 

 

Restful API 규칙

 Rest에는 중요한 두 가지 규칙이 있다. URL은 자원을 표현하고, 행위에 대한 정의HTTP 메서드를 이용하여 Restful API를 설계해야한다. 정리하면 다음과 같다.

 

  1. URL은 정보의 자원을 표현해야 한다.

  2. 자원에 대한 행위는 HTTP 요청 메소드(GET, POST, PUT, DELETE 등)로 표현해야 한다.

 리소스명동사보다 명사를 사용해야한다. URL자원을 표현하는데 중점을 두어야하며, get 같이 행위에 대한 표현을 하지 말아야한다.

 

"/users/1", "/users/profile", "/video/1"

 

HTTP  요청 메서드는 주로 5가지를 이용한다.

Method Action 역할 페이로드
GET index/retrieve 모든/특정 리소스 조회 X
POST create 리소스 생성 O
PUT replace 전체 교체 O
PATCH modify 일부 수정 O
DELETE delete 모든/특정 리소스 삭제 X

 

 응답할 때 주로 쓰이는 응답 코드(상태 코드)는 다음과 같다. 응답 코드(상태 코드)HTTP 요청이 성공했는지 실패했는지를 서버에서 알려주는 코드다. 먼저 200번 대 코드는 서버가 클라이언트의 요청을 성공적으로 처리했다는 것을 의미한다.

 

상태코드 의미
200(OK)  응답이 성공적으로 완료되었으니 다음 동작을 이어가도 좋습니다.
201(Created)  주로 POST, PUT 요청에 대한 응답에 주로 사용된다. 클라이언트의 요청을 서버가 잘 처리했고, 새 자원이 생겼다는 것을 의미한다.
202(Accepted)  클라이언트의 요청은 정상적이다. 하지만 서버 작업 완료까지 시간이 좀 걸리니 결과를 나중에 알려주겠다.
204(No Content)  클라이언트의 요청은 정상적이다. 하지만 컨텐츠를 제공하지 않는다. PUT 사용 시, 기존 자원과 비교하여 변경된 것이 없을 때 이  응답코드를 사용한다. 하지만 DELETE 사용시에는, 자원을 삭 제해서 참조할 자원이 더 이상 Http body로 응답하는 것이 무의미할 때 이 응답코드를 사용한다.

 

 하지만 400번 대 코드는 클라이언트의 요청이 유효하지 않아 서버가 해당 요청을 수행하지 않았다는 것을 의미한다. 또한 400번 대 코드는 오류 발생 시 파라미터 위치(params, body, path), 사용자 입력 값, 에러 이유를 꼭 명시하는 것이 좋다.

 

상태코드 의미
400(Bad Request)  클라이언트의 요청이 유효하지 않아 더 이상 작업을 진행할 수 없습니다. 오류 발생 시 파라미터 위치 또는 에러 이유를 꼭 명시해주도록 하자.
401(Unathenticated)  상태 코드 401은 비 인증, 비 승인을 의미한다. 즉 인증(승인)이 안돼서 자원을 이용할 수 없다는 것을 의미한다. 게임을 예로 들어보자. 바람의 나라라는 게임의 오중공격이라는 마법이 있다. 이 마법은 전사라는 직업을 가진 유저만 배울 수 있다. 왜냐하면 오중공격은 전사만이 배울 수 있도록 때문이다. 하지만 주술사가 배우려면 직업적 이유로 승인이 안돼기 때문에 상태코드 401을 보낼 수 있다.
403(Forbidden)  클라이언트가 권한이 없어 다음 진행을 할 수 없는 경우를 말한다. 게임을 예로 들어보자. 전사라는 직업을 가진 유저가 오중공격을 배우려고한다. 하지만 전사의 레벨이 낮아 오중공격을 배울 수 없다. 이때 상태코드 403을 보낼 수 있다.
404(Not Found)  요청한 클라이언트 자원이나 경로(URL)를 찾을 수 없다는 의미다. 즉 검색어를 이용해서 검색을 했지만, 아무런 결과를 받아볼 수 없는 상황일 때 상태코드 404를 보낸다.
405(Method Not Found)  자원은 존재하지만 해당 자원이 지원하지 않는 메소드일 때 응답하는 코드다. 예를 들어 "/users/:id"의 경우, Get, Put, Delete 메소드는 가능하지만, Post 메소드는 사용할 수 없다. 이럴 경우에 상태코드 405를 보낸다.
409(Conflict)  클라이언트의 요청이 서버의 상태와 충돌한 경우를 말한다. 게임을 예로 들어보자. 우리는 게임 아이디를 만들 때 중복 체크를 한다. 왜냐하면 아이디는 고유 값이어야만 하기 때문이다. 따라서 이미 존재하는 아이디가 있을 경우, 상태코드 409를 보낸다.
429(Too Many Request)  클라이언트가 일정 시간동안 너무 많은 요청을 보낸 경우를 말한다. 예를 들어 홈페이지에 로그인하려할 때, 5회 이상 실패했을 경우 상태코드 429를 보낸다.

 

실습(TDD)

 먼저, ExpressJS를 설치하고 Babel 컴파일러(선택)을 설치한 후, server.js 파일을 만들어 아래와 같이 코드를 입력해서 서버를 만들어주자.

 

body-parser 미들웨어를 사용해도 되고, express.json()을 사용해도 된다. 입력 후 마지막에 모듈화를 잊어버리지 말자.

코드는 깃허브에 업로드했습니다. Link
import express from "express";
import bodyParser from "body-parser";
import apiRouter from "./Routers/apiRouter";

const app = express();

app.use(bodyParser.json());
app.use("/", apiRouter);

app.get("/", (req, res) => {
  res.send("TEST DRIVEN DEVELOPMENT USING RESTFUL API");
});

app.listen(3000, () => console.log("✅Listening On: http://localhost:3000"));

export default app;

 

 apiRouter.js라는 파일을 생성 후, 다음과 같이 코드를 입력해서 라우터 구성을 하자.

import express from "express";
import {
  addUser,
  deleteUser,
  getUsers,
  updateUser,
} from "../Controllers/apiController";

const apiRouter = express.Router();

apiRouter.get("/users/:id", getUsers);
apiRouter.post("/users", addUser);
apiRouter.put("/users/:id", updateUser);
apiRouter.delete("/users/:id", deleteUser);

export default apiRouter;

 

MVC구조를 위해 컨트롤러를 관리하는 파일 apiController.js을 만들어주자.

import { users } from "../db";

// code 400: 잘못된 문법으로 인해 서버가 요청을 이해할 수 없음
// code 409: 요청이 현재 서버 상태와 충돌할 때를 의미

export const getUsers = (req, res) => {
  const id = Number(req.params.id);
  if (Number.isNaN(id)) return res.status(400).end();

  const user = users.filter((user) => user.id === id)[0];
  if (!user) return res.status(404).end();

  res.status(200).json(user);
};

export const addUser = (req, res) => {
  const name = req.body.name;
  if (!name) return res.status(400).end();

  const isConflict = users.find((user) => user.name === name);
  if (isConflict) return res.status(409).end();

  const newId = users[users.length - 1].id + 1;
  const user = { id: newId, name };
  users.push(user);

  res.status(201).json({ users, user });
};

export const updateUser = (req, res) => {
  const id = Number(req.params.id);
  const name = req.body.name;
  if (Number.isNaN(id) || !name) return res.status(400).end();

  const user = users.find((user) => user.id === id);
  if (!user) return res.status(404).end();

  user.name = name;
  res.status(201).json(user);
};

export const deleteUser = (req, res) => {
  const id = Number(req.params.id);
  if (Number.isNaN(id)) return res.status(400).end();

  const index = users.findIndex((user) => user.id === id);
  if (index === -1) return res.status(404).end();
  users.splice(index, 1);
  res.status(200).json(users);
};

 

이제 공유 유저 객체 db.js를 만들어주자. json파일을 만들어서 제어하는 방법도 있다.

export let users = [
  { id: 1, name: "유재석" },
  { id: 2, name: "지석진" },
  { id: 3, name: "김종국" },
  { id: 4, name: "양세찬" },
  { id: 5, name: "송지효" },
  { id: 6, name: "전소민" },
];

 

이제 server.spec.js 파일을 만들어서 테스트케이스를 작성하자.

import request from "supertest";
import should from "should";
import app from "./server";

describe("Get /users/:id", () => {
  describe("성공 시", () => {
    it("응답 번호 200을 반환", (done) => {
      request(app).get("/users/1").expect(200).end(done);
    });
  });
  describe("실패 시", () => {
    it("id가 숫자가 아닌경우, 응답 코드 400번을 반환", (done) => {
      request(app).get("/users/cheonyulin").expect(400).end(done);
    });

    it("없는 id를 보냈을 때, 응답 코드 404를 반환", (done) => {
      request(app).get("/users/99").expect(404).end(done);
    });
  });
});

describe("Post /users", () => {
  describe("성공 시", () => {
    let name = "Yulin Cheon";
    let body = null;

    before((done) => {
      request(app)
        .post("/users")
        .send({ name })
        .expect(201)
        .end((err, res) => {
          body = res.body;
          done();
        });
    });

    it("업데이트 된 유저 리스트를 반환", () => {
      body.users.should.be.instanceof(Array);
    });

    it("추가 된 유저의 이름을 반환", () => {
      body.user.should.have.property("name", name);
    });
  });

  describe("실패 시", () => {
    let name = "유재석";

    it("빈 객체를 보낼 경우, 응답 코드 400번을 반환", (done) => {
      request(app).post("/users").send({}).expect(400).end(done);
    });

    it("중복 되는 이름이 있을 경우, 응답 코드 409번을 반환", (done) => {
      request(app).post("/users").send({ name }).expect(409).end(done);
    });
  });
});

describe("Put /users/:id", () => {
  describe("성공 시", () => {
    let body = null;
    let name = "강개리";

    before((done) => {
      request(app)
        .put("/users/2")
        .send({ name })
        .expect(201)
        .end((err, res) => {
          body = res.body;
          done();
        });
    });

    it("유저 객체를 반환", () => {
      body.should.have.property("name", name);
    });
  });

  describe("실패 시", () => {
    it("id가 숫자가 아닌 경우, 응답 코드 400를 반환", (done) => {
      request(app)
        .put("/users/cheonyulin")
        .send({ name: "하동훈" })
        .expect(400)
        .end(done);
    });
    it("빈 객체를 보냈을 때, 응답 코드 400를 반환", (done) => {
      request(app).put("/users/3").send({}).expect(400).end(done);
    });
    it("없는 id를 보냈을 때, 응답 코드 404를 반환", (done) => {
      request(app)
        .put("/users/99")
        .send({ name: "하동훈" })
        .expect(404)
        .end(done);
    });
  });
});

describe("Delete /users/:id", () => {
  describe("성공 시", () => {
    it("응답 번호 200을 반환", (done) => {
      request(app).delete("/users/4").expect(200).end(done);
    });
  });

  describe("실패 시", () => {
    it("id가 숫자가 아닌 경우, 응답 코드 400를 반환", (done) => {
      request(app).delete("/users/cheonyulin").expect(400).end(done);
    });

    it("없는 id를 보냈을 때, 응답 코드 404를 반환", (done) => {
      request(app).delete("/users/99").expect(404).end(done);
    });
  });
});

 

이제 yarn test 또는 npm test를 입력해서 출력 결과를 확인해보도록 하자.

 

[그림 1] 출력 결과

 

참고자료

'NodeJS > TDD' 카테고리의 다른 글

[Node.js] 목록 조회 API 테스트 코드 만들기  (0) 2020.11.03
[Node.js] 슈퍼 테스트(Super Test)  (0) 2020.11.03
[Node.js] Should.js 란  (0) 2020.10.31
[Node.js] 모카(Mocha)  (2) 2020.10.27