본문으로 건너뛰기

안동민 개발노트

본문 시작

RESTful API 설계와 구현

리소스 URI와 HTTP 메서드·상태 코드·JSON 응답 규칙을 세우고 Express로 사용자 CRUD API를 구현합니다.

지난 장에서는 Node.js와 Express.js로 간단한 백엔드 서버를 구축하는 방법을 학습했습니다.

이제 서버가 클라이언트 요청을 받아 응답하는 기본 통신 흐름을 이해한 상태입니다.

현대의 웹 애플리케이션, 특히 SPA(Single Page Application)와 모바일 앱은 클라이언트와 서버 간에 데이터를 주고받을 때, 정해진 규칙과 표준을 따르는 방식으로 통신합니다.

이 규칙이 바로 API(Application Programming Interface)이며, 그중에서도 가장 널리 사용되는 것이 RESTful API입니다.

RESTful API는 프론트엔드와 백엔드가 서로 독립적으로 개발되면서도 효과적으로 협력할 수 있도록 돕는 핵심적인 개념입니다.

이번 장에서는 RESTful API 개념과 설계 원칙을 정리하고, Express.js로 직접 구현하는 방법을 다룹니다.

이를 통해 백엔드 서버를 프론트엔드에 필요한 데이터를 체계적으로 제공하는 데이터 허브로 구성할 수 있습니다.

REST 리소스 계약 구성 지도

DATA-FLOW · B99 WEB FUNDAMENTALS

REST 리소스 계약 구성 지도

리소스 URI를 중심으로 method 의미, request/response representation, status와 안정적인 error, collection pagination, related resource 링크를 한 계약으로 묶는다.

REST 리소스 계약 구성 지도users collection과 user item URI를 중심으로 HTTP method, JSON representation, status와 stable error, pagination, related resource 링크를 연결한 REST 계약 지도 METHOD SEMANTICS행위는 HTTP method로GET read · POST createPUT/PATCH update · DELETE REPRESENTATION요청·응답 JSON schemarequired fields · stable shapeContent-Type · validation RESOURCE IDENTITY/users · /users/{id}collection과 item을 명사 URI로 식별 STATUS · ERROR결과를 HTTP로 관찰200·201·204 · 400·404stable error code + message COLLECTIONpagination · filter · sortcursor/limit과 next cursor목록 계약도 schema에 포함 RELATED RESOURCES/users/{id}/posts관계는 링크·중첩 URI로 탐색

Resource identity

/users collection과 /users/{id} item을 명사 URI로 식별한다.

Method semantics

GET은 조회, POST는 생성, PUT/PATCH는 갱신, DELETE는 삭제의 계약을 갖는다.

Representation schema

요청·응답 JSON의 필수 필드, 타입, Content-Type, validation을 고정한다.

Status · stable error

200·201·204와 400·404 같은 상태, 기계가 읽는 error code와 사람용 message를 함께 정의한다.

Collection contract

cursor/limit, filter, sort, next cursor의 입력·출력도 목록 schema다.

Related resources

/users/{id}/posts처럼 관계를 링크나 중첩 URI로 탐색하게 한다.

stateless는 서버가 데이터를 저장하지 않는다는 뜻이 아니라 각 요청이 처리에 필요한 인증·문맥을 스스로 제공한다는 제약이다.


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의 이상적인 형태)

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)설명
GETRead자원을 조회합니다. 서버의 데이터를 변경하지 않습니다.
POSTCreate새로운 자원을 생성합니다.
PUTUpdate자원 전체를 갱신합니다. (자원이 없으면 생성할 수도 있음)
PATCHUpdate자원의 일부를 갱신합니다.
DELETEDelete자원을 삭제합니다.
예시 (API 엔드포인트)
목적URIHTTP 메서드요청 바디 (예시)응답 (예시)
모든 사용자 조회/usersGET(없음)[ { id: 1, name: 'A' }, ... ]
새 사용자 생성/usersPOST{ "name": "New User" }{ id: 4, name: 'New User' } (생성된 자원)
특정 사용자 조회/users/1GET(없음){ id: 1, name: 'A' }
특정 사용자 정보 전체 갱신/users/1PUT{ "name": "Updated Name" }{ id: 1, name: 'Updated Name' }
특정 사용자 이름만 변경/users/1PATCH{ "name": "Partial Update" }{ id: 1, name: 'Partial Update' }
특정 사용자 삭제/users/1DELETE(없음)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 CRUD 요청 보호 흐름

FLOWCHART · B99 WEB FUNDAMENTALS

Express CRUD 요청 보호 흐름

REST API 계약은 resource URI, HTTP method 의미, representation schema와 status/error behavior를 함께 정의한다. Express 구현은 parse·validate·authorize·lookup·mutate·respond의 경계를 명시해야 한다.

Express CRUD 요청 가드레일과 상태 코드 요청의 route와 body, 인증과 권한, 리소스 존재를 차례로 검사하고 각 실패를 400·405, 401·403, 404로 종료하며 통과한 CRUD만 2xx로 응답하는 흐름 HTTP 요청method · path · body route·body유효? 인증·권한통과? 리소스존재? CRUD 실행read · create · update · delete 2xx단일 성공 응답 400 / 405입력 / method 오류 401 / 403미인증 / 권한 없음 404대상 없음 ONE REQUEST → ONE TERMINAL RESPONSE · NO FALL-THROUGH
  1. route·body 검사

    method나 path가 없으면 405, body가 유효하지 않으면 400으로 종료한다.

  2. 인증·권한 검사

    신원이 없으면 401, 행동 권한이 없으면 403으로 종료한다.

  3. 리소스 확인

    대상이 없으면 404로 종료한다.

  4. CRUD 실행

    모든 가드를 통과한 요청만 read·create·update·delete를 수행한다.

  5. 응답 종결

    각 요청은 정확히 하나의 상태 코드와 body로 끝나며 다음 handler로 흘러가지 않는다.

가드는 오류를 모아 두는 상자가 아니라 실행을 막는 종료점이다. 실패 status와 성공 2xx가 한 요청에서 중복 전송되지 않는다.

이제 앞서 배운 Express.js를 사용하여 간단한 users API를 구현해보겠습니다.

데이터베이스는 아직 연결하지 않으므로, 메모리에 간단한 사용자 데이터를 배열로 관리할 것입니다.

app.js (또는 server.js) 파일 수정
app.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

REST API를 구현할 때는 URL, 메서드, 상태 코드, JSON 모양이 같은 계약을 가리키는지 함께 점검해야 합니다.

설계가 끝났다면 자원, 행위, 응답, 테스트가 같은 API 계약을 가리키는지 마지막으로 확인합니다.


RESTful API 설계 정리

이번 정리에서는 URI 규칙과 HTTP 메서드 매핑을 실제 자원 설계 기준으로 다시 점검합니다.

이번 장에서는 프론트엔드와 백엔드 간의 효율적인 통신을 위한 표준적인 방법론인 RESTful API의 개념과 설계 원칙을 깊이 있게 학습했습니다.

이번 절에서는 API 정의, REST 원칙, URI/HTTP 메서드 설계, JSON 응답 형식을 정리했습니다.

RESTful API는 자원을 URI로 표현하고, CRUD 행위를 HTTP 메서드로 구분해 인터페이스 일관성을 유지합니다.

마지막으로 Express.js에서 사용자 자원에 대한 GET, POST, PUT, PATCH, DELETE 요청을 처리하는 CRUD 라우트 구현 흐름을 확인했습니다.

이 절에서는 클라이언트와 서버의 요청·응답 흐름, API 경로 설계, 메서드 선택, 응답 형식, 오류 처리 기준을 정리했습니다.

REST API는 URL을 리소스 명사로 정하고, HTTP 메서드와 응답 코드를 행위와 결과에 맞춰 선택합니다.