[Book-IT][코로나보드 실전 웹 서비스][MEMO][Chapter02:API 서버 만들기]

·19 min read

[개발PC : MacBook]

Chapter02) API 서버 만들기

학습 목표 : 코로나19 통계 데이터를 저장하고 조회하는 API를 제공하는 웹 애플리케이션 서버(API 서버)를 만들어봅시다.

노드JS와 익스프레스 프레임워크를 기반으로 구현하겠습니다.

image

API는 애플리케이션 프로그래밍 인터페이스(Appilcation Programming Interface)의 약자로 서비스에 요청을 보내고 응답을 받을 수 있는 명세와 프로토콜 세트를 말합니다.

이러한 명세에 따라 클라이언트가 서버의 API를 호출하면, 서버에서는 비즈니스 로직을 수행한 후 결과를 보내줍니다.

API 서버라고 하면 보통 HTTP 프로토콜을 사용하는 API를 제공하는 웹 애플리케이션 서버를 말합니다.

[코로나보드 API 서버 아키텍처 소개]

데이터를 데이터베이스에 저장해두고 프론트엔드의 요청에 따라 데이터를 적절히 가공해서 응답합니다.

image

API 서버는 크롤러가 수집해온 데이터를 MySQL에 보관했다가 정적 페이지 빌더가 요청할 때마다 건네주는 역할을 합니다.

image

클라이언트와는 (1) HTTP로, (2) MySQL과는 전용 커넥터 모듈을 통해 통신합니다.

여기서 클라인언트는 정적 페이지 빌더와 크롤러가 해당합니다.

API 서버는 NodeJS로 실행되고, 비즈니스 로직으로 라우팅하는 익스프레스가 올라가있고, MySQL 커넥터는 이름 그대로 MySQL을 사용하는 라이브러리이며, 시퀄라이즈 ORM(object-relational mapping)은 자바스크립트 객체와 데이터베이스 테이블을 자동으로 매핑해주어 코드 생산성을 높여주는 라이브러리입니다.

!! 런타임(runtime) : 런타임은 '컴퓨터 프로그램이 실행되고 있는 상태'라는 의미도 있지만 '특정 언어로 작성된 코드가 실행되는 환경'을 의미하기도 합니다. !!

[노드JS]

애플리케이션 서버 운영체제 위에서 독립된 프로세스로 실행하는 별로의 런타임이 필요한데, 노드JS가 바로 대표적인 자바스트립트 런타임이다.

노드JS는 운영체제 위에서 독립된 프로세스로 실행되기 때문에 '런타임에서 제공하는' 파일 시스템 입출력 모듈이나, DNS, HTTP, TCP, UDP 등의 네트워크 모듈을 이용해서 운영체제 기능을 직접 사용할 수 있으므로 웹 애플리케이션 서버를 만드는 게 가능합니다.

image

[npm]

npm은 노드 패키지 매니저(Node Package Manager)의 약자로, 다른 사람들이 개발해서 업로드해둔 오픈 소스 라이브러리들을 패키지 단위로 내려받아 사용할 수 있게 하는 도구입니다.

 

패키지 설정 파일 생성하기 : 패키지 이름, 버전, 설명, 진입 지점, 테스트 명령 등을 물어봅니다.

$ npm init

 

의존성(dependency : 이미 개발된 기능-라이브러리) 추가 / 삭제

$ npm install [패키지명]

[익스프레스(Express)]

노드JS 기반으로 작성된 빠르고 가벼운 웹 프레임워크로, 웹 애플리케이션 서버를 만들 때 항상 예시로 등장하는 유명 프레임워크입니다.

가볍고 작은 프레임워크로 스프링(Spring)같은 프레임워크에서 지원하는 데이터베이스 연결이나 사용자 인증 등 복잡한 기능이 없습니다.

 

express 프레임워크와 body-parser 라이브러리 설치

$ npm install express@4.17.1 body-parser@1.19.0

[데이터베이스 준비하기:MySQL]

MySQL은 오픈 소스 관계형 데이터베이스 시스템(Relational Database Management System. RDBMS)입니다.

관계형 데이터 모델에서는 데이터를 2차원 테이블 형태로 표현하고, 여러 테이블 사이의 관계를 표현할 수 있습니다.

 

MySQL 서버에 루트 계정으로 접속하기

$ mysql -u root -p (pwd: te12)

 

database 리스트 보기 명령어

mysql> show databases;

 

database 만들기 명령어

mysql> create database coronaboard;

 

계정 생성하기('비밀번호'를 입력해서 계정을 생성할 수 있다. '@' 문자를 기준으로 앞쪽은 계정이름, 뒤쪽은 해당 계정으로부터의 접근을 허용할 호스트를 의미, 호스트를'%'로 지정하면 어떠한 호스트에서의 접근도 허용한다는 뜻, % 대신 IP{121.178.221.11}를 명시하면 지정한 IP의 호스트에서만 접근할 수 있다.)

mysql> create user 'sbj_admin'@'%' IDENTIFIED BY 'sj12**';

 

권한 부여하기(위에서 상성한 계정은 어떠한 권한도 설정된 게 없다.)

mysql> grant create, alter, drop, index, insert, select, update, delete, lock tables on coronaboard.*to 'sbj_admin'@'%';

 

테이블을 생성/변경/삭제하는 데 필요한 create, alter, drop, index 명령과 테이블의 데이터를 CRUD(Create, Read, Update, Delete)하는 데 필요한 INSERT, SELECT, UPDATE, DELETE 명령을 사용하는 권한을 부여했습니다.

마지막으로 테이블에 덤프(dump) 파일로부터 데이터를 일괄 불러올 때 테이블에 락(lock)을 걸고 해제하는 데 필요한 lock tables 권한을 부여했습니다.

[API 서버와 데이터베이스 연동하기]

API 서버는 데이터베이스 테이블을 직접 조작하지 않고 중간에 ORM을 이용합니다.

ORM을 이용하면 데이터베이스를 조작하느랴 SQL문을 직접 사용하지 않아도 됩니다.

image

우리가 사용할 ORM 모듈은 '시퀄라이즈(sequelize)'이고 MySQL과 연동하는데 'MySQL 커넥터'를 이용할 겁니다.

image

전체 연동 작업은 아래 다섯 단계로 이뤄집니다.

  1. MySQL 커넥터와 시퀄라이즈 ORM 설치

$ npm install mysql2@2.2.5 sequelize@6.3.5

  • mysql2는 MySQL 클라이언트 역할로 커넥션 풀(connection pool: 연결 요청이 오면 커넥션을 제공해주고, 처리가 끝나면 반납받아 pool에 저장해 재사용하는 방식, 접근할 때마다 새롭게 연결을 생성하면 소요되는 시간만큼 속도가 지연됨) 기능도 제공합니다.

  • sequelize는 SQL문 대신 자바스크립트 코드로 데이터를 주고 받을 수 있게 해주는 ORM 라이브러리입니다.

 

  1. 객체 모델 설계

    image

 

  1. 시퀄라이즈 객체 모델 정의

시퀄라이즈로 객체 모델을 정의하는 방법을 알아야 한다.

<노드JS 모듈 시스템 맛보기>

모듈 시스템은 파일 단위로 조직화할 수 있게 해준다.

그 핵심은 require() 함수와 module.exports 변수이다.

require()는 다른 파일에 존재하는 코드를 불러와주고, module.exports는 현재 파일에 있는 내용 중 다른 파일에 보여주고 싶은 내용만 노출할 수 있도록 해줍니다.

  • type : 자료형을 뜻하며, 시퀄라이즈가 제공하는 DataTypes에 정의된 타입 중 선택하면 됩니다.

  • allowNull : null값을 허용할지 명시합니다. true 혹은 false로 설정합니다. 데이터 삽입 시 값이 false인 속성에 값이 없다면 에러가 발생합니다.

  • autoIncrement : 인스턴스가 하나 생길 때마다 값이 1씩 자동 증가시키줍니다.

  • primaryKey : 기본키로 지정합니다. autoIncrement와 함께 true로 지정하면 id 속성이 항상 고유한 값이 됨을 보장할 수 있습니다.

https://sequelize.org/ (공식 문서)

 

  1. 데이터베이스 연결 설정
  • ~/database/index.js : 데이터베이스 연결 설정

 

  1. 데이터베이스와 객체 모델 동기화

객체 모델과 sequelize 인스턴스를 사용하면 드디어 데이터베이스에 연결해서 테이블을 생성할 수 있습니다.

$node index.js Executing (default): CREATE TABLE IF NOT EXISTS GlobalStat (id INTEGER UNSIGNED NOT NULL auto_increment , cc CHAR(2) NOT NULL, date DATE NOT NULL, confirmed INTEGER NOT NULL, death INTEGER, released INTEGER, tested INTEGER, testing INTEGER, negative INTEGER, PRIMARY KEY (id)) ENGINE=InnoDB; Executing (default): SHOW INDEX FROM GlobalStat Database is ready! Server is running on port 9090.  

coronaboard database 선택

mysql> use coronaboard;

mysql> use coronaboard; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A

Database changed  

table 확인

mysql> show tables;

mysql> show tables; +-----------------------+ | Tables_in_coronaboard | +-----------------------+ | GlobalStat | | KeyValue | +-----------------------+ 2 rows in set (0.01 sec)  

 

테이블 스키마 확인

mysql> describe GlobalStat;

mysql> describe GlobalStat; +-----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------+--------------+------+-----+---------+----------------+ | id | int unsigned | NO | PRI | NULL | auto_increment | | cc | char(2) | NO | MUL | NULL | | | date | date | NO | | NULL | | | confirmed | int | NO | | NULL | | | death | int | YES | | NULL | | | released | int | YES | | NULL | | | tested | int | YES | | NULL | | | testing | int | YES | | NULL | | | negative | int | YES | | NULL | | +-----------+--------------+------+-----+---------+----------------+ 9 rows in set (0.01 sec)

[API 만들기]

이제 클라이언트가 서버에 정의된 API를 호출해서 원하는 데이터를 저장하거나 조회할 수 있도록 연결하는 일만 남았습니다.

필요한 통계 데이터 API는 총 3가지

  • 모든 국가, 모든 날짜에 대한 전체 통계 데이터를 불러오는 API

  • 새로운 데이터를 삽입하거나 또는 기존 데이터를 갱신하는 API

  • 잘못 입력된 데이터를 삭제하는 API

 

<라우터와 컨트롤러>

서버에서 여러 API들을 제공하려면 HTTP 요청에 따라 실행될 코드를 메서드(method)와 경로(path)별로 미리 연결해줘야 하고, 이걸 라우팅(routing)이라고 부릅니다.

image

익스프레스 프레임워크에서 라우팅을 정의하는 기본적인 함수 호출 방식은 다음과 같습니다.

app.METHOD(PATH, HANDLER)

  • app은 익스프레스 인스턴스를 의미합니다.

  • METHOD에는 HTTP 메서드를 적어줍니다. (GET, POST, PUT, PATCH, DELETE 등)

  • PATH에는 말 그대로 서버상의 경로를 지정합니다.

  • HANDLER에는 METHOD와 PATH에 일치하는 HTTP 요청을 처리해줄 콜백 함수(핸들러)를 등록합니다.

!! 콜백 함수(callback function) : 콜백 함수는 일반적인 함수와 호출 방식이 다릅니다. 일반적인 함수는 사용자가 직접 함수를 호출해 실행합니다. 하지만 콜백 함수는 콜백 함수를 등록해둔 코드가 실행되면서 호출됩니다(다른 코드가 내부적으로 어떻게 작성되었는지에 따라 호출 여부나 호출 시기가 달라질 수 있습니다). !!

 

!! RESTful API란?

REST(representational state transfer)는 서버에서 어떠한 리소스르 제공할 때 이 리소스에 접근하는 API의 설계 방식이다.

REST에서 통신을 할 때 HTTP 프로토콜을 사용합니다.

제공하고자 하는 리소스들에 각각 URI(uniform resource identifier)를 지정한 후, 각 지정된 URI에 대해서 HTTP 메서드인 POST, GET, PUT, DELETE 등을 이용해서 해당 리소스를 생성하고, 읽고, 수정하고 삭제합니다. !!

 

<HTTP 요청과 응답 객체>

콜백 함수의 첫 번째 인수로 제공되는 req 객체에서 가장 많이 사용되는 속성은 headers, params, query, body 네 가지입니다.

 

  • headers 속성은 HTTP 요청에 포함된 모든 HTTP 헤더 정보를 키값 쌍 형태로 제공합니다.

요청한 클라이언트가 응답받기를 원하는 언어 정보가 포함된 Accept-Language 헤더나 사용자 인증이 필요한 API를 사용할 때 발급받는 인증 토큰(access token)을 넣는 Authorization 헤더 등이 대표적입니다.

 

  • params 속성은 경로 매개변수를 사용하는 때만 필요하고, 경로 매개변수(path parameter)란 라우팅 할때 사용하는 경로의 일부를 고정 값이 아닌 매개변수로 사용하는 것을 말합니다.

예를 들어 경로에 ':cc'와 같이 ':' 문자로 시작하는 부분이 있다면 해당 위치에는 동적인 값을 입력할 수 있는 데, 이 값에 req.params.cc를 통해 접근할 수 있습니다.

 

  • query 속성은 경로의 뒤쪽에 추가로 붙어 있는 쿼리 스트링을 파싱해서 쓸 수 있게 해줍니다.

요청 URL에서 경로와 쿼리 스트링은 다음 처럼 '?'를 기준으로 구분됩니다.

/global-stat?startDate=2023-10-10&endDate=2023-11-11 쿼리 스트링은 '매개변수=값&매개변수=값' 형태이며, 원하는 만큼의 매개변수 개수를 지정할 수 있습니다.

요청이 위와 같은 URL로 들어왔을 때 req.query 속성을 읽으면 다음과 같은 객체가 반환됩니다.

{ startDate: '2023-10-10', endDate: '2023-11-11' }  

  • body 속성은 HTTP 요청의 바디 내용을 담고 있습니다.

HTTP 바디는 XML, JSON, x-www-form-urlencoded와 같이 다양한 형식으로 보낼 수 있기 때문에 실제 요청에 사용된 형식에 맞는 파서를 미리 익스프레스 인스턴스에 등록해줘야 합니다.

그렇지 않으면 req.body가 존재하지 않거나 잘못된 내용이 들어 있을 수 있습니다.

~/index.js 파일을 보면 app.use(bodyParser.json()); 코드에서 JSON 바디 파서를 등록하는 코드입니다.

최근 개발되는 API들은 거의 모두 JSON 형식을 사용하기에 JSON 파서 하나만 등록했습니다.

 

  • 응답 객체(res)

// 비어 있는 응답을 전송 res.end();

// 'hello'라는 일반 텍스트(plain text) 응답 전송 res.send('hello');

// 자바스크립트 객체를 JSON 형식의 문자열로 변환해서 응답 전송 res.json({ result: 'hello' });

// 성공 상태 코드(status code) res.status(200).end();

// 요청한 리소스르 찾을 수 없음 res.status(404).send('Requested resource is not found');

// 서버 에러인 500 설정 res.status(500).json({ error: 'Unknown error' });  

<API 구현하기>

~/controller/global-stat.controller.js

image

 

 

$curl --request GET http://localhost:9090/    {"message":"Hello CourtAlami!"}% 

 

$curl --request GET http://localhost:9090/global-stats {"result":[{"id":2,"cc":"KR","date":"2020-02-14","confirmed":28,"death":0,"released":7,"tested":7242,"testing":535,"negative":6679}]}%

 

$curl --location --request POST http://localhost:9090/global-stats --header 'Content-Type: application/json' --data-raw '{ "cc": "GM", "date": "2023-10-12", "confirmed": "30", "death": 0, "negative": 23134, "released": 9, "tested": 192, "testing": 123 }' {"result":"success"}% 

 

$curl --location --request DELETE http://localhost:9090/global-stats --header 'Content-Type: application/json' --data-raw '{ "cc": "KR", "date": "2020-02-14" }' {"result":"success"}%

 

$curl --location --request POST http://localhost:9090/global-stats --header 'Content-Type: application/json' --data-raw '{ "cc": "GM", "date": "2023-10-12", "confirmed": "hello" }' {"error":"SequelizeDatabaseError: Incorrect integer value: 'hello' for column 'confirmed' at row 1"}%   

 

<다양한 값을 저장하는 범용 API 만들기>

테이블 하나에 키값 쌍을 한 행으로 저장해서 어떤 데이터든 유연하게 저장하고 불러올 수 있는 key-value API를 만들겠습니다.

~/database/key-value.model.js

~/controller/key-value.controller.js

 

$curl --request GET http://localhost:9090/key-value/dfs {"error":"TypeError: Cannot read properties of undefined (reading 'findOne')"}% 

 

 

curl --location --request POST http://localhost:9090/key-value --header 'Content-Type: application/json' --data-raw '{ "key": "test", "value": "hello" }'