Node.js REST API 서버 개발 핵심 정리

Node.js REST API 서버 개발 핵심 정리

Node.js는 JavaScript를 브라우저 밖에서 실행할 수 있게 해주는 런타임이다. Chrome의 V8 JavaScript 엔진을 기반으로 동작하며, 서버 개발에서는 HTTP 요청을 처리하고 데이터베이스와 통신하는 API 서버를 만드는 용도로 많이 사용한다.

Node.js 자체가 웹 프레임워크인 것은 아니다. HTTP 서버를 직접 만들 수 있는 http 모듈을 제공하고, Express 같은 프레임워크를 추가해서 라우팅, 미들웨어, 요청 처리 등을 편하게 구성하는 방식이 일반적이다.

REST API 서버의 전체적인 흐름은 다음과 같이 볼 수 있다.

Client -> HTTP Request -> Node.js -> Express -> Middleware -> Router -> Controller -> Service -> Repository -> Database

응답은 반대 방향으로 전달된다.

Database -> Repository -> Service -> Controller -> HTTP Response -> Client

Node.js에서 서버 개발을 이해하려면 HTTP, Routing, Middleware, Controller, Service, Database, Authentication이 어떤 관계로 연결되는지를 보는 것이 중요하다.


Node.js와 Express

Node.js는 JavaScript 코드를 서버에서 실행하기 위한 런타임이다.

브라우저에서는 JavaScript가 주로 DOM 조작이나 사용자 입력 처리에 사용되지만, Node.js에서는 파일 시스템, 네트워크, 프로세스, 데이터베이스 같은 서버 기능에 접근할 수 있다.

Node.js의 기본 http 모듈만으로도 HTTP 서버를 직접 만들 수 있다.

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'Content-Type': 'application/json'
    });

    res.end(JSON.stringify({
        message: 'Hello'
    }));
});

server.listen(3000);

하지만 실제 API 서버에서는 라우팅, JSON 파싱, 인증, 에러 처리 같은 기능이 필요하다.

이런 작업을 직접 구성하는 대신 Express 같은 프레임워크를 사용할 수 있다.

const express = require('express');

const app = express();

app.use(express.json());

app.get('/users', (req, res) => {
    res.json({
        users: []
    });
});

app.listen(3000);

관계를 정리하면 다음과 같다.

JavaScript -> Node.js -> Express -> HTTP API Server

Node.js가 실행 환경이라면 Express는 그 위에서 HTTP 서버를 구성하기 위한 프레임워크다.


REST API

REST API는 HTTP의 구조를 이용해 서버의 리소스를 다루는 API 설계 방식이다.

사용자 데이터를 관리한다면 다음과 같이 구성할 수 있다.

GET /users
GET /users/10
POST /users
PUT /users/10
PATCH /users/10
DELETE /users/10

HTTP Method는 요청의 의도를 표현한다.

GET -> 데이터 조회
POST -> 데이터 생성
PUT -> 데이터 전체 수정
PATCH -> 데이터 일부 수정
DELETE -> 데이터 삭제

REST API에서는 URL이 특정 동작보다 리소스를 표현하는 방향으로 설계되는 것이 일반적이다.

GET /getUsers
POST /createUser
DELETE /deleteUser

보다는 다음과 같은 형태가 자연스럽다.

GET /users
POST /users
DELETE /users/:id

REST가 모든 API의 절대적인 규칙이라는 의미는 아니다. 실제 서비스에서는 팀의 규칙이나 API 특성에 따라 일부 변형해서 사용할 수 있다.


HTTP Request와 Response

API 서버의 가장 기본적인 역할은 HTTP Request를 받고 HTTP Response를 반환하는 것이다.

클라이언트가 다음 요청을 보낸다고 생각해보자.

Client -> GET /users/10 -> Node.js Server

요청에는 Header, URL, Parameter, Body 등의 정보가 포함될 수 있다.

GET /users/10 HTTP/1.1
Host: example.com
Authorization: Bearer TOKEN

서버는 요청을 분석하고 필요한 로직을 수행한다.

HTTP Request -> Routing -> Business Logic -> Database -> HTTP Response

응답은 다음과 같은 형태가 될 수 있다.

HTTP/1.1 200 OK
Content-Type: application/json

{
    "id": 10,
    "name": "user"
}

Express에서는 다음과 같이 처리할 수 있다.

app.get('/users/:id', (req, res) => {
    const id = req.params.id;

    res.json({
        id: id,
        name: 'user'
    });
});

여기서 :id는 URL Parameter다.

/users/10 -> id = 10

Express에서는 req.params를 통해 접근한다.


Query Parameter와 Request Body

HTTP 요청에서는 데이터를 전달하는 방식이 목적에 따라 달라진다.

특정 리소스를 식별하는 값은 URL Parameter를 사용할 수 있다.

/users/10 -> URL Parameter -> id = 10

검색, 정렬, 페이지네이션 같은 조건은 Query Parameter를 사용할 수 있다.

/users?page=2&limit=20 -> Query Parameter -> page = 2, limit = 20

Express에서는 req.query로 가져온다.

app.get('/users', (req, res) => {
    const page = req.query.page;
    const limit = req.query.limit;
});

POST나 PUT 같은 요청에서 JSON 데이터를 전달할 때는 Request Body를 사용할 수 있다.

{
    "name": "user",
    "age": 25
}

Express에서 JSON Middleware를 등록하면 req.body를 통해 접근할 수 있다.

HTTP Request -> express.json() -> req.body
app.use(express.json());

app.post('/users', (req, res) => {
    const { name, age } = req.body;
});

정리하면 다음과 같다.

Resource Identifier -> URL Parameter
Search / Filter / Sort / Pagination -> Query Parameter
Create / Update Data -> Request Body

Routing

Routing은 HTTP Method와 URL에 따라 어떤 코드를 실행할지 결정하는 과정이다.

HTTP Request -> Method + URL 확인 -> Route 선택 -> Handler 실행

Express에서는 다음과 같이 정의할 수 있다.

app.get('/users', getUsers);
app.get('/users/:id', getUser);
app.post('/users', createUser);
app.delete('/users/:id', deleteUser);

API가 커지면 모든 Route를 하나의 파일에 넣는 방식은 관리하기 어려워진다.

그래서 Express Router를 사용해 기능별로 라우트를 분리할 수 있다.

const router = express.Router();

router.get('/', getUsers);
router.get('/:id', getUser);
router.post('/', createUser);

module.exports = router;

그리고 서버에서 Router를 연결한다.

app.use('/users', userRouter);

그러면 실제 요청 흐름은 다음과 같다.

GET /users -> /users Router -> getUsers
GET /users/10 -> /users Router -> getUser
POST /users -> /users Router -> createUser

Routing의 역할은 요청을 적절한 처리 코드에 연결하는 것이다.

Router 안에 모든 비즈니스 로직을 넣는 것은 별개의 문제다.


Middleware

Express에서 중요한 개념 중 하나가 Middleware다.

Middleware는 요청이 Route Handler에 도달하기 전이나 처리 과정에서 실행되는 함수다.

Request -> Middleware -> Middleware -> Route Handler -> Response

예를 들어 로그를 기록하는 Middleware를 만들 수 있다.

app.use((req, res, next) => {
    console.log(req.method, req.url);
    next();
});

next()가 호출되면 다음 Middleware 또는 Route Handler로 넘어간다.

Request -> Middleware -> next() -> Next Middleware -> Route Handler

인증도 Middleware로 처리할 수 있다.

app.use(authMiddleware);

특정 API에만 적용할 수도 있다.

router.get('/profile', authMiddleware, getProfile);

Middleware를 사용하면 인증, 로깅, 요청 검증, 에러 처리 같은 공통 기능을 API 로직과 분리할 수 있다.

대표적인 흐름은 다음과 같다.

Request -> Logging -> Authentication -> Validation -> Router -> Controller

Controller와 Business Logic

간단한 API라면 Route Handler에서 모든 작업을 처리할 수도 있다.

app.post('/users', async (req, res) => {
    const user = await db.users.insert(req.body);

    res.json(user);
});

하지만 서비스가 커지면 사용자 생성 전에 이메일 중복 확인, 비밀번호 암호화, 권한 확인, 데이터 검증 같은 로직이 추가될 수 있다.

이런 로직을 하나의 Handler에 계속 넣으면 코드가 복잡해진다.

그래서 서버를 여러 계층으로 나누는 방식을 사용할 수 있다.

Route -> Controller -> Service -> Repository -> Database

각 계층의 역할은 다음과 같이 생각할 수 있다.

Route -> HTTP Method와 URL을 Controller에 연결
Controller -> HTTP Request와 Response 처리
Service -> Business Logic 처리
Repository -> Database 접근
Database -> 실제 데이터 저장

사용자 생성 API를 예로 들면 다음과 같다.

POST /users -> UserController -> UserService -> UserRepository -> Database

Service에서는 다음과 같은 비즈니스 규칙을 처리할 수 있다.

UserService -> 이메일 중복 확인 -> 비밀번호 암호화 -> 사용자 생성 -> UserRepository

다만 이 구조를 모든 프로젝트에 똑같이 적용할 필요는 없다.

작은 프로젝트에서 지나치게 많은 계층을 만들면 오히려 코드가 복잡해질 수 있다.

중요한 것은 HTTP 처리와 비즈니스 규칙, 데이터 접근을 필요 이상으로 섞지 않는 것이다.


Database 연결

Node.js API 서버는 일반적으로 데이터베이스와 통신한다.

MySQL을 사용하는 경우를 보면 다음과 같은 구조가 될 수 있다.

Client -> Node.js -> Controller -> Service -> Repository -> MySQL

Repository는 데이터베이스 접근을 담당한다.

Service -> Repository -> SQL Query -> MySQL

ORM을 사용한다면 객체 중심으로 데이터베이스를 다룰 수 있다.

반대로 SQL을 직접 실행하는 방식도 사용할 수 있다.

어떤 방식을 선택하든 API Route마다 데이터베이스 접근 코드를 직접 작성하기보다는 별도의 데이터 접근 계층으로 분리하는 것이 관리에 유리할 수 있다.

또한 DB 연결을 요청마다 새로 생성하기보다 Connection Pool을 사용하는 것이 일반적이다.

Node.js -> Connection Pool -> Database

Connection Pool에는 여러 DB 연결이 준비되어 있다.

Node.js -> Connection Pool -> Connection 1 -> MySQL
Node.js -> Connection Pool -> Connection 2 -> MySQL
Node.js -> Connection Pool -> Connection 3 -> MySQL
Node.js -> Connection Pool -> Connection 4 -> MySQL

요청이 들어오면 기존 연결을 가져와 사용하고 작업이 끝나면 Pool에 반환한다.

Request -> Connection Pool에서 연결 획득 -> Query -> 연결 반환

이렇게 하면 요청마다 데이터베이스 연결을 새로 생성하는 비용을 줄일 수 있다.


비동기 처리와 Event Loop

Node.js 서버를 이해할 때 비동기 처리도 중요하다.

Node.js는 JavaScript 실행을 기본적으로 하나의 메인 스레드에서 처리하면서 I/O 작업을 비동기적으로 처리하는 구조를 사용한다.

예를 들어 데이터베이스 요청을 기다리는 동안 다른 요청을 처리할 수 있다.

app.get('/users', async (req, res) => {
    const users = await getUsersFromDatabase();

    res.json(users);
});

코드만 보면 await에서 서버가 멈추는 것처럼 보일 수 있지만 Node.js 프로세스 전체가 DB 응답을 기다리며 정지하는 것은 아니다.

개념적으로 다음과 같이 볼 수 있다.

Request A -> DB Request -> Waiting
Request B -> Processing
Request C -> Processing
Request D -> Processing
DB Response A -> Request A Processing -> Response A

Node.js가 I/O 중심의 서버 개발에서 많이 사용되는 이유 중 하나가 이런 비동기 처리 구조다.

하지만 CPU 연산은 다르다.

JavaScript 실행 스레드에서 매우 무거운 CPU 작업을 장시간 수행하면 다른 요청 처리에도 영향을 줄 수 있다.

CPU Heavy Task -> Event Loop 점유 -> 다른 Request 처리 지연

따라서 Node.js는 모든 종류의 서버 연산에 무조건 적합한 것이 아니라 I/O 중심 작업과 CPU 중심 작업의 특성을 구분해서 사용해야 한다.


Error Handling

API 서버에서는 정상적인 요청만 들어온다고 가정할 수 없다.

잘못된 요청, 인증 실패, 권한 부족, 데이터 미존재, 데이터베이스 오류 등 다양한 문제가 발생할 수 있다.

Express에서는 Error Middleware를 통해 공통적인 오류 처리를 구성할 수 있다.

Request -> Middleware -> Controller -> Service -> Error -> Error Middleware -> HTTP Response

예를 들어:

app.use((err, req, res, next) => {
    console.error(err);

    res.status(500).json({
        message: 'Internal Server Error'
    });
});

HTTP Status Code도 상황에 맞게 사용하는 것이 좋다.

200 -> OK
201 -> Created
400 -> Bad Request
401 -> Unauthorized
403 -> Forbidden
404 -> Not Found
409 -> Conflict
500 -> Internal Server Error

예를 들어 인증되지 않은 사용자가 인증이 필요한 API에 접근하면 401을 사용할 수 있다.

인증은 되었지만 해당 작업을 수행할 권한이 없다면 403을 사용할 수 있다.

오류 처리의 전체 흐름은 다음과 같다.

Request -> Validation -> Authentication -> Business Logic -> Error Detection -> Error Handling -> Response

Status Code뿐만 아니라 API의 오류 응답 형식도 일관되게 유지하는 것이 클라이언트 개발에 유리하다.


Authentication과 JWT

REST API에서는 서버에 세션을 저장하는 방식뿐 아니라 토큰 기반 인증도 많이 사용한다.

JWT를 사용하는 경우 일반적인 흐름은 다음과 같다.

Login -> ID / Password 검증 -> JWT 발급 -> Client 저장 -> API Request -> JWT 검증 -> User 식별 -> Business Logic

API 요청에서는 일반적으로 Authorization Header에 토큰을 전달한다.

Client -> Authorization: Bearer JWT -> API Server

Middleware에서 JWT를 검증한 뒤 사용자 정보를 요청 객체에 넣을 수도 있다.

Request -> Auth Middleware -> JWT 검증 -> User 정보 추출 -> req.user -> Controller

JWT 자체가 로그인 시스템이나 권한 관리 시스템을 제공하는 것은 아니다.

JWT는 인증 정보를 표현하고 전달하기 위한 토큰 형식이고, 실제 사용자 인증과 권한 정책은 애플리케이션에서 구현해야 한다.

실제 서비스에서는 다음 요소도 함께 고려해야 한다.

JWT -> Signature 검증 -> Expiration 검증 -> User 식별 -> Authorization 검사

Refresh Token을 사용하는 경우에는 다음과 같은 구조도 만들 수 있다.

Login -> Access Token 발급 + Refresh Token 발급 -> Client -> Access Token 만료 -> Refresh Token 검증 -> Access Token 재발급

Validation

클라이언트가 보내는 데이터를 그대로 신뢰해서는 안 된다.

회원가입 요청이 다음과 같이 들어온다고 하자.

{
    "email": "test@example.com",
    "password": "1234"
}

서버에서는 이메일 형식, 비밀번호 길이, 필수 필드 등을 검증해야 한다.

Request -> Validation -> Authentication -> Controller -> Service -> Database

Validation을 Middleware나 별도의 Schema 계층으로 분리하면 API마다 반복되는 검증 코드를 줄일 수 있다.

중요한 것은 클라이언트에서 검증했다고 하더라도 서버에서 다시 검증해야 한다는 점이다.

Client Validation -> Request -> Server Validation -> Business Logic

서버 입장에서 외부에서 들어오는 모든 요청은 신뢰할 수 없는 입력이다.

Validation은 단순한 편의 기능이 아니라 외부 입력과 내부 시스템 사이의 경계를 담당한다.


환경 변수와 설정 관리

DB 비밀번호나 JWT Secret 같은 민감한 값을 소스 코드에 직접 작성하면 안 된다.

const dbPassword = 'my-password';

대신 환경 변수를 사용할 수 있다.

const dbPassword = process.env.DB_PASSWORD;

개발 환경에서는 .env 파일을 사용할 수 있다.

PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password
JWT_SECRET=secret

환경 변수의 기본적인 흐름은 다음과 같다.

Environment -> Node.js Process -> process.env -> Application Configuration

.env에는 민감한 정보가 들어갈 수 있기 때문에 Git에 그대로 커밋하지 않도록 관리해야 한다.

운영 환경에서는 Docker, Kubernetes, 클라우드 환경의 Secret이나 환경 변수 관리 시스템 등을 사용할 수 있다.


API 서버의 전체 구조

앞에서 설명한 내용을 하나로 묶으면 일반적인 Node.js REST API 서버는 다음과 같은 흐름으로 볼 수 있다.

Client -> HTTP Request -> Express -> Middleware -> Router -> Controller -> Service -> Repository -> Database

각 단계에서 담당하는 역할은 다음과 같다.

Client -> API 요청 생성
HTTP -> Request와 Response 전달
Express -> HTTP 서버 구성
Middleware -> 공통 요청 처리
Router -> Endpoint와 Handler 연결
Controller -> HTTP 요청과 응답 처리
Service -> Business Logic 처리
Repository -> Database 접근
Database -> 데이터 저장 및 조회

응답은 반대 방향으로 이동한다.

Database -> Repository -> Service -> Controller -> HTTP Response -> Client

인증과 검증까지 포함하면 다음과 같이 볼 수 있다.

Client -> HTTP Request -> Logging -> Authentication -> Validation -> Router -> Controller -> Service -> Repository -> Database

오류가 발생하면 별도의 Error Handling 계층으로 전달될 수 있다.

Request -> Middleware -> Controller -> Service -> Error -> Error Middleware -> HTTP Response

Node.js와 다른 서버 기술의 관계

Node.js REST API 서버는 하나의 완성된 서버 아키텍처라기보다 서버를 구성하는 실행 환경과 프레임워크에 가깝다.

필요한 기능에 따라 여러 기술을 조합할 수 있다.

Node.js -> Express -> REST API
Node.js -> MySQL -> Persistent Data
Node.js -> Redis -> Cache / Session
Node.js -> Kafka -> Event Streaming
Node.js -> JWT -> Authentication

전체 시스템으로 묶으면 다음과 같은 구조가 될 수 있다.

Client -> Node.js -> Express -> Service -> MySQL
                                  |
                                  -> Redis
                                  |
                                  -> Kafka

Express가 HTTP 요청을 받고, Service가 비즈니스 로직을 처리하고, MySQL이 영속 데이터를 저장하고, Redis가 캐시나 세션에 사용되고, Kafka가 서비스 간 비동기 이벤트 전달을 담당하는 식이다.

GraphQL을 사용하는 경우에는 REST 대신 GraphQL을 API 계층으로 사용할 수도 있다.

Client -> Node.js -> GraphQL -> Service -> Database

중요한 것은 Node.js가 이런 기능을 자동으로 제공하는 것이 아니라는 점이다.

Node.js -> Runtime
Express -> HTTP Framework
JWT -> Authentication Mechanism
MySQL -> Database
Redis -> In-Memory Data Store
Kafka -> Event Streaming Platform

필요한 기술을 조합해서 하나의 서버 시스템을 구성한다.


실무에서 생각해야 할 부분

간단한 CRUD API는 Node.js와 Express만으로도 빠르게 만들 수 있다.

하지만 실제 서비스에서는 API 하나를 만드는 것보다 운영 가능한 구조를 만드는 것이 더 중요하다.

실무에서는 다음 요소를 함께 고려해야 한다.

Authentication
Authorization
Validation
Error Handling
Logging
Database Connection Pool
Transaction
Caching
Rate Limiting
Security
Environment Configuration
Monitoring
Testing

문제가 생기는 대표적인 구조는 Controller에서 모든 기술을 직접 사용하는 경우다.

Controller -> Database
Controller -> Redis
Controller -> JWT
Controller -> Kafka
Controller -> Business Logic

이렇게 되면 Controller가 HTTP 처리부터 비즈니스 규칙, 데이터베이스, 캐시, 메시지 처리까지 모두 담당하게 된다.

반대로 모든 코드를 하나의 Service에 몰아넣는 것도 문제가 될 수 있다.

Controller -> Massive Service -> Everything

중요한 것은 계층을 무조건 많이 만드는 것이 아니다.

변경되는 책임을 적절한 경계로 분리하는 것이 핵심이다.


서버 요청의 전체 흐름

Node.js REST API 서버에서 하나의 요청이 들어와 처리되는 과정을 전체적으로 연결하면 다음과 같다.

Client
-> HTTP Request
-> Express
-> Middleware
-> Authentication
-> Validation
-> Router
-> Controller
-> Service
-> Repository
-> Database
-> Repository
-> Service
-> Controller
-> HTTP Response
-> Client

예를 들어 사용자 조회라면:

GET /users/10
-> Authentication
-> Validation
-> User Router
-> User Controller
-> User Service
-> User Repository
-> MySQL
-> User Repository
-> User Service
-> User Controller
-> JSON Response
-> Client

사용자 생성이라면:

POST /users
-> Authentication
-> Validation
-> User Router
-> User Controller
-> User Service
-> Email 중복 확인
-> Password 암호화
-> User Repository
-> MySQL
-> User Repository
-> User Service
-> User Controller
-> HTTP Response
-> Client

이 흐름을 이해하면 Express의 개별 API를 외우는 것보다 서버가 실제로 어떻게 동작하는지 훨씬 명확하게 볼 수 있다.


정리

Node.js REST API 서버의 핵심 구조는 다음과 같이 정리할 수 있다.

Client -> HTTP Request -> Middleware -> Router -> Controller -> Service -> Repository -> Database

응답은 반대 방향으로 돌아온다.

Database -> Repository -> Service -> Controller -> HTTP Response -> Client

Node.js는 JavaScript를 서버에서 실행하기 위한 런타임이고, Express는 그 위에서 HTTP API를 구성하기 쉽게 해주는 프레임워크다.

REST API에서는 HTTP Method와 URL을 이용해 리소스를 표현하고, URL Parameter, Query Parameter, Request Body 등을 통해 데이터를 전달한다.

Middleware는 인증, 검증, 로깅 같은 공통 처리를 담당할 수 있고, Router는 요청을 적절한 Handler에 연결한다.

Controller는 HTTP 요청과 응답을 다루고, Service는 비즈니스 로직을 처리하며, Repository는 데이터베이스 접근을 담당하도록 분리할 수 있다.

여기에 JWT, Redis, MySQL, Kafka 같은 기술을 목적에 맞게 조합해서 하나의 서버 시스템을 구성한다.

Node.js 서버 개발에서 중요한 것은 Express 문법 자체가 아니다.

HTTP Request
-> Middleware
-> Router
-> Controller
-> Service
-> Repository
-> Database
-> Response

요청이 들어와서 어떤 계층을 거쳐 데이터를 처리하고, 다시 어떤 형태로 응답되는지를 이해하는 것이 핵심이다.

이 구조가 잡히면 Express의 세부 기능이나 다른 Node.js 라이브러리를 추가로 학습할 때도 각각의 기술이 어느 위치에서 어떤 역할을 담당하는지 자연스럽게 연결할 수 있다.