RESTful API 설계와 구현
지난 장에서는 Node.js와 Express.js를 사용하여 간단한 백엔드 서버를 구축하는 방법을 학습했습니다. 이제 서버가 클라이언트의 요청을 받아 응답을 보내는 기본적인 통신 방식을 이해했습니다.
현대의 웹 애플리케이션, 특히 SPA(Single Page Application)와 모바일 앱은 클라이언트와 서버 간에 데이터를 주고받을 때, 정해진 규칙과 표준을 따르는 방식으로 통신합니다. 이 규칙이 바로 API(Application Programming Interface) 이며, 그중에서도 가장 널리 사용되는 것이 RESTful API입니다. RESTful API는 프론트엔드와 백엔드가 서로 독립적으로 개발되면서도 효과적으로 협력할 수 있도록 돕는 핵심적인 개념입니다.
이번 장에서는 RESTful API의 개념과 설계 원칙을 이해하고, Express.js를 활용하여 RESTful API를 직접 구현하는 방법을 학습할 것입니다. 이를 통해 여러분의 백엔드 서버가 프론트엔드에 필요한 데이터를 체계적으로 제공하는 "데이터 허브" 역할을 할 수 있도록 만들 수 있습니다.
API란?
API (Application Programming Interface) 는 "Application Programming Interface"의 약자로, 두 개의 소프트웨어 구성 요소가 서로 통신할 수 있도록 하는 규칙 집합입니다. 웹 개발에서는 주로 서버가 클라이언트에게 데이터를 제공하거나 특정 기능을 수행할 수 있도록 허용하는 인터페이스를 의미합니다.
- 비유: 식당에서 손님(클라이언트)이 종업원(API)에게 주문(요청)을 하면, 종업원이 주방(서버)에 전달하고, 주방은 주문을 처리하여 종업원을 통해 손님에게 음식(응답)을 가져다주는 것과 같습니다. 손님은 주방에서 음식이 어떻게 만들어지는지 알 필요 없이 종업원과 소통합니다.
API를 통해 프론트엔드(클라이언트)는 백엔드(서버)의 내부 구현을 몰라도, 정해진 규칙에 따라 데이터를 요청하고 받을 수 있습니다.
REST 개념
REST (Representational State Transfer) 는 웹 서비스를 구축하기 위한 아키텍처 스타일 중 하나입니다. RESTful API는 이 REST 아키텍처 스타일의 원칙을 따르는 API를 의미합니다. REST의 핵심 아이디어는 웹의 기존 기술과 프로토콜(HTTP, URI)을 최대한 활용하여 효율적인 웹 서비스를 만드는 것입니다.
REST의 주요 원칙 (제약 조건)
RESTful API는 다음과 같은 원칙들을 따릅니다.
-
클라이언트-서버 구조 (Client-Server)
- 서버는 API를 제공하고, 클라이언트는 사용자 인터페이스와 비즈니스 로직을 담당합니다.
- 서로의 의존성을 줄여 독립적인 개발이 가능하게 합니다.
-
무상태성 (Stateless)
- 각 요청은 이전 요청과 완전히 독립적이어야 합니다. 서버는 클라이언트의 상태를 저장하지 않습니다.
- 클라이언트가 요청을 보낼 때 필요한 모든 정보(인증 토큰 등)를 요청 자체에 포함해야 합니다.
- 장점: 서버의 확장성 증가 (여러 서버 간 부하 분산 용이), 예측 가능한 동작.
-
캐시 가능 (Cacheable)
- 클라이언트가 캐시 가능한 응답을 받을 수 있도록 하여, 네트워크 트래픽을 줄이고 응답 속도를 향상시킵니다.
- HTTP의 캐싱 메커니즘(HTTP 헤더의
Cache-Control
,ETag
등)을 활용합니다.
-
계층화된 시스템 (Layered System)
- 클라이언트는 서버와 직접 통신하는지, 중간에 프록시 서버나 로드 밸런서 등이 있는지 알 필요가 없습니다.
- 시스템의 유연성을 높이고 확장성을 제공합니다.
-
인터페이스 일관성 (Uniform Interface)
- REST 아키텍처의 핵심 원칙 중 하나입니다. 클라이언트가 특정 API에 대해 일관적인 방식으로 요청할 수 있도록 합니다.
- 이를 위해 다음의 서브 원칙들을 따릅니다:
- 자원 식별 (Identification of resources): 모든 자원(Resource)은 URI(Uniform Resource Identifier)로 명확하게 식별될 수 있어야 합니다. (예:
/users
,/products/123
) - 메시지를 통한 자원 조작 (Manipulation of resources through representations): 클라이언트가 서버의 자원을 조작하려면, 자원의 표현(Representation)을 HTTP 메시지 바디에 포함하여 전송합니다. (예: JSON, XML)
- 자체 서술적 메시지 (Self-descriptive messages): 응답 메시지 자체에 해당 자원을 어떻게 조작할 수 있는지(다른 API 경로 등)에 대한 정보가 포함되어야 합니다.
- 하이퍼미디어(HATEOAS - Hypermedia as the Engine of Application State): 애플리케이션의 상태 변경이 하이퍼링크를 통해 이루어지도록 합니다. (필수적이지는 않으나 REST의 이상적인 형태)
- 자원 식별 (Identification of resources): 모든 자원(Resource)은 URI(Uniform Resource Identifier)로 명확하게 식별될 수 있어야 합니다. (예:
RESTful API 설계 규칙
RESTful API는 자원(Resource)을 URI로 표현하고, 해당 자원에 대한 행위(CRUD: Create, Read, Update, Delete)를 HTTP 메서드로 표현하는 것을 권장합니다.
자원 (Resource) 표현 (URI)
- 명사 형태를 사용하고, 복수형을 권장합니다.
- 계층 구조를 나타낼 때는
/
를 사용합니다. - 하이픈(
-
)은 URI 가독성을 높이는 데 사용합니다. - 언더스코어(
_
)는 사용하지 않습니다. - 파일 확장자를 URI에 포함하지 않습니다. (예:
/users.json
X,/users
O)
예시
- 사용자 목록:
/users
- 특정 사용자:
/users/{id}
(예:/users/123
) - 특정 사용자의 게시물 목록:
/users/{id}/posts
- 특정 상품:
/products/{id}
행위 (CRUD) 표현 (HTTP 메서드)
HTTP 메서드를 사용하여 자원에 대한 어떤 행위를 수행할지 명확히 나타냅니다.
HTTP 메서드 | 행위 (CRUD) | 설명 |
---|---|---|
GET | Read | 자원을 조회합니다. 서버의 데이터를 변경하지 않습니다. |
POST | Create | 새로운 자원을 생성합니다. |
PUT | Update | 자원 전체를 갱신합니다. (자원이 없으면 생성할 수도 있음) |
PATCH | Update | 자원의 일부를 갱신합니다. |
DELETE | Delete | 자원을 삭제합니다. |
예시 (API 엔드포인트)
목적 | URI | HTTP 메서드 | 요청 바디 (예시) | 응답 (예시) |
---|---|---|---|---|
모든 사용자 조회 | /users | GET | (없음) | [ { id: 1, name: 'A' }, ... ] |
새 사용자 생성 | /users | POST | { "name": "New User" } | { id: 4, name: 'New User' } (생성된 자원) |
특정 사용자 조회 | /users/1 | GET | (없음) | { id: 1, name: 'A' } |
특정 사용자 정보 전체 갱신 | /users/1 | PUT | { "name": "Updated Name" } | { id: 1, name: 'Updated Name' } |
특정 사용자 이름만 변경 | /users/1 | PATCH | { "name": "Partial Update" } | { id: 1, name: 'Partial Update' } |
특정 사용자 삭제 | /users/1 | DELETE | (없음) | HTTP 204 No Content 또는 {"message": "삭제 성공"} |
응답 (Response) 형식
RESTful API의 응답은 주로 JSON(JavaScript Object Notation) 형식을 사용합니다. JSON은 경량 데이터 교환 형식으로, 사람과 기계 모두 읽고 쓰기 쉬워서 웹에서 데이터를 주고받는 표준으로 자리 잡았습니다.
- 성공 응답:
HTTP 200 OK
,201 Created
등 적절한 상태 코드와 함께 JSON 데이터 포함. - 오류 응답:
HTTP 4xx (클라이언트 오류)
,5xx (서버 오류)
등 적절한 상태 코드와 함께 오류 메시지를 JSON으로 제공. (예:{"error": "User not found"}
)
Express.js로 RESTful API 구현하기
이제 앞서 배운 Express.js를 사용하여 간단한 users
API를 구현해보겠습니다. 데이터베이스는 아직 연결하지 않으므로, 메모리에 간단한 사용자 데이터를 배열로 관리할 것입니다.
app.js
(또는 server.js
) 파일 수정
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// JSON 요청 본문을 파싱하기 위한 미들웨어
app.use(express.json());
// 임시 데이터 (데이터베이스 대신 메모리에서 관리)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextUserId = 3; // 다음 사용자 ID를 위한 변수
// --- RESTful API 라우트 정의 ---
// 1. 모든 사용자 조회 (GET /api/users)
app.get('/api/users', (req, res) => {
res.json(users); // users 배열을 JSON 형태로 응답
});
// 2. 특정 사용자 조회 (GET /api/users/:id)
// ':id'는 URL 파라미터로, req.params.id로 접근 가능
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id); // URL 파라미터는 문자열이므로 숫자로 변환
const user = users.find(u => u.id === id); // id에 해당하는 사용자 찾기
if (user) {
res.json(user); // 사용자 정보 응답
} else {
res.status(404).json({ message: 'User not found' }); // 404 Not Found 응답
}
});
// 3. 새 사용자 생성 (POST /api/users)
app.post('/api/users', (req, res) => {
const newUser = {
id: nextUserId++, // ID 자동 증가
name: req.body.name,
email: req.body.email
};
// 필수 필드 검증 (간단하게)
if (!newUser.name || !newUser.email) {
return res.status(400).json({ message: 'Name and email are required.' });
}
users.push(newUser); // users 배열에 새 사용자 추가
res.status(201).json(newUser); // 201 Created 상태 코드와 함께 새로 생성된 사용자 정보 응답
});
// 4. 특정 사용자 정보 전체 갱신 (PUT /api/users/:id)
app.put('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const updatedUser = req.body;
let userFound = false;
users = users.map(user => {
if (user.id === id) {
userFound = true;
// id는 변경하지 않고, 요청 바디의 내용으로 사용자 정보 전체를 갱신
return { ...user, ...updatedUser, id: id };
}
return user;
});
if (userFound) {
const user = users.find(u => u.id === id);
res.json(user); // 갱신된 사용자 정보 응답
} else {
res.status(404).json({ message: 'User not found' });
}
});
// 5. 특정 사용자 정보 부분 갱신 (PATCH /api/users/:id)
app.patch('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const updates = req.body; // 업데이트할 필드만 포함
let userFound = false;
users = users.map(user => {
if (user.id === id) {
userFound = true;
// 기존 사용자 정보에 업데이트할 필드만 덮어씌움
return { ...user, ...updates, id: id };
}
return user;
});
if (userFound) {
const user = users.find(u => u.id === id);
res.json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
});
// 6. 특정 사용자 삭제 (DELETE /api/users/:id)
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const initialLength = users.length;
users = users.filter(u => u.id !== id); // id에 해당하지 않는 사용자만 남김
if (users.length < initialLength) {
res.status(204).send(); // 204 No Content (삭제 성공 시, 응답 본문 없음)
} else {
res.status(404).json({ message: 'User not found' });
}
});
// 서버 시작
app.listen(port, () => {
console.log(`RESTful API 서버가 http://localhost:${port} 에서 실행 중입니다.`);
console.log(`테스트 엔드포인트:`);
console.log(`- GET /api/users`);
console.log(`- GET /api/users/1`);
console.log(`- POST /api/users (Body: {"name": "New", "email": "new@example.com"})`);
console.log(`- PUT /api/users/1 (Body: {"name": "Updated", "email": "updated@example.com"})`);
console.log(`- PATCH /api/users/2 (Body: {"email": "patched@example.com"})`);
console.log(`- DELETE /api/users/1`);
});
테스트 방법
위 코드를 app.js
에 저장하고 node app.js
(또는 nodemon app.js
)로 서버를 실행합니다.
이제 Postman, Insomnia 같은 API 테스트 도구나, 웹 브라우저의 Fetch API
를 사용하여 각 엔드포인트에 요청을 보내 테스트해 볼 수 있습니다.
GET http://localhost:3000/api/users
GET http://localhost:3000/api/users/1
POST http://localhost:3000/api/users
(Body에{"name": "새로운 사용자", "email": "new@email.com"}
JSON 데이터 포함)PUT http://localhost:3000/api/users/1
(Body에{"name": "앨리스", "email": "alice_updated@example.com"}
JSON 데이터 포함)PATCH http://localhost:3000/api/users/2
(Body에{"name": "밥 (수정됨)"}
JSON 데이터 포함)DELETE http://localhost:3000/api/users/1
마무리하며
이번 장에서는 프론트엔드와 백엔드 간의 효율적인 통신을 위한 표준적인 방법론인 RESTful API의 개념과 설계 원칙을 깊이 있게 학습했습니다.
여러분은 API의 정의와 함께, REST 아키텍처의 핵심 원칙인 클라이언트-서버 구조, 무상태성, 캐시 가능, 계층화된 시스템, 그리고 가장 중요한 인터페이스 일관성을 이해했습니다. 또한, RESTful API를 설계할 때 자원을 URI(명사, 복수형) 로 표현하고, 해당 자원에 대한 행위(CRUD)를 HTTP 메서드(GET, POST, PUT, PATCH, DELETE) 로 명확히 나타내는 규칙을 배웠습니다. 응답 형식으로는 JSON이 주로 사용됨을 확인했습니다.
마지막으로, Express.js를 사용하여 사용자(Users) 자원에 대한 GET, POST, PUT, PATCH, DELETE 요청을 처리하는 간단한 RESTful API를 직접 구현하고 테스트해보았습니다. 이를 통해 여러분의 백엔드 서버가 프론트엔드 애플리케이션에 필요한 데이터를 체계적이고 표준적인 방식으로 제공하는 방법을 익혔습니다.
이제 여러분은 클라이언트와 서버가 어떻게 상호작용하는지, 그리고 그 상호작용의 핵심인 API가 어떻게 설계되고 구현되는지에 대한 탄탄한 이해를 갖추게 되었습니다.