🌐 웹개발

REST API

Representational State Transfer

HTTP 메서드(GET, POST, PUT, DELETE)를 사용하는 API 설계 스타일. 무상태성이 핵심 원칙.

📖 상세 설명

REST API(Representational State Transfer)는 2000년 Roy Fielding의 박사 논문에서 처음 제안된 아키텍처 스타일로, 웹 서비스 설계의 사실상 표준이 되었습니다. HTTP 프로토콜의 메서드(GET, POST, PUT, DELETE, PATCH)를 활용하여 리소스를 CRUD(Create, Read, Update, Delete) 방식으로 조작합니다. URL은 리소스를 명사형으로 표현하고, HTTP 메서드가 동작을 나타내는 것이 RESTful 설계의 핵심입니다.

REST의 6가지 제약 조건은 클라이언트-서버 분리, 무상태성(Stateless), 캐시 가능성, 계층화된 시스템, 코드 온 디맨드(선택), 통합 인터페이스입니다. 특히 무상태성은 각 요청이 독립적이며 서버가 클라이언트의 상태를 저장하지 않음을 의미합니다. 이로 인해 서버 확장이 용이하고 로드 밸런싱이 간단해집니다.

RESTful API 설계에서는 리소스 명명 규칙이 중요합니다. 복수형 명사(/users), 계층적 관계 표현(/users/1/posts), 쿼리 파라미터를 통한 필터링(/users?role=admin)이 권장됩니다. HTTP 상태 코드도 의미있게 사용해야 합니다. 200(성공), 201(생성됨), 400(잘못된 요청), 401(인증 필요), 404(없음), 500(서버 오류) 등이 있습니다.

현대 API 설계에서는 버전 관리(/api/v1/), 페이지네이션, HATEOAS(Hypermedia as the Engine of Application State), OpenAPI(Swagger) 문서화가 중요합니다. GraphQL, gRPC와 같은 대안이 있지만, REST는 간단함과 HTTP 표준 활용으로 여전히 가장 널리 사용됩니다. API Gateway, Rate Limiting, CORS 설정도 실무에서 필수적인 고려사항입니다.

💻 코드 예제

Express.js REST API
// Express.js로 RESTful API 구현
import express from 'express';

const app = express();
app.use(express.json());

interface User {
    id: number;
    name: string;
    email: string;
}

let users: User[] = [
    { id: 1, name: '홍길동', email: 'hong@example.com' },
    { id: 2, name: '김영희', email: 'kim@example.com' },
];

// GET /users - 전체 사용자 목록 조회
app.get('/api/v1/users', (req, res) => {
    const { role, limit = 10, offset = 0 } = req.query;
    let result = users;

    // 페이지네이션 적용
    result = result.slice(Number(offset), Number(offset) + Number(limit));

    res.json({
        data: result,
        pagination: {
            total: users.length,
            limit: Number(limit),
            offset: Number(offset),
        }
    });
});

// GET /users/:id - 특정 사용자 조회
app.get('/api/v1/users/:id', (req, res) => {
    const user = users.find(u => u.id === Number(req.params.id));

    if (!user) {
        return res.status(404).json({
            error: 'Not Found',
            message: `User with id ${req.params.id} not found`
        });
    }

    res.json({ data: user });
});

// POST /users - 새 사용자 생성
app.post('/api/v1/users', (req, res) => {
    const { name, email } = req.body;

    // 유효성 검사
    if (!name || !email) {
        return res.status(400).json({
            error: 'Bad Request',
            message: 'name and email are required'
        });
    }

    const newUser: User = {
        id: users.length + 1,
        name,
        email
    };

    users.push(newUser);

    // 201 Created와 Location 헤더 반환
    res.status(201)
       .header('Location', `/api/v1/users/${newUser.id}`)
       .json({ data: newUser });
});

// PUT /users/:id - 전체 업데이트 (Idempotent)
app.put('/api/v1/users/:id', (req, res) => {
    const index = users.findIndex(u => u.id === Number(req.params.id));

    if (index === -1) {
        return res.status(404).json({ error: 'Not Found' });
    }

    const { name, email } = req.body;
    users[index] = { id: Number(req.params.id), name, email };

    res.json({ data: users[index] });
});

// PATCH /users/:id - 부분 업데이트
app.patch('/api/v1/users/:id', (req, res) => {
    const user = users.find(u => u.id === Number(req.params.id));

    if (!user) {
        return res.status(404).json({ error: 'Not Found' });
    }

    Object.assign(user, req.body);
    res.json({ data: user });
});

// DELETE /users/:id - 삭제
app.delete('/api/v1/users/:id', (req, res) => {
    const index = users.findIndex(u => u.id === Number(req.params.id));

    if (index === -1) {
        return res.status(404).json({ error: 'Not Found' });
    }

    users.splice(index, 1);

    // 204 No Content - 본문 없이 성공 응답
    res.status(204).send();
});

// 에러 핸들링 미들웨어
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
    console.error(err.stack);
    res.status(500).json({
        error: 'Internal Server Error',
        message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
    });
});

app.listen(3000, () => console.log('API server running on port 3000'));
fetch API 클라이언트
// REST API 클라이언트 유틸리티
const API_BASE = 'https://api.example.com/v1';

async function apiClient<T>(
    endpoint: string,
    options: RequestInit = {}
): Promise<T> {
    const response = await fetch(`${API_BASE}${endpoint}`, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${getToken()}`,
            ...options.headers,
        },
        ...options,
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || `HTTP ${response.status}`);
    }

    // 204 No Content 처리
    if (response.status === 204) {
        return null as T;
    }

    return response.json();
}

// CRUD 작업
const api = {
    // Read
    getUsers: () => apiClient<{ data: User[] }>('/users'),
    getUser: (id: number) => apiClient<{ data: User }>(`/users/${id}`),

    // Create
    createUser: (data: Omit<User, 'id'>) =>
        apiClient<{ data: User }>('/users', {
            method: 'POST',
            body: JSON.stringify(data),
        }),

    // Update
    updateUser: (id: number, data: Partial<User>) =>
        apiClient<{ data: User }>(`/users/${id}`, {
            method: 'PATCH',
            body: JSON.stringify(data),
        }),

    // Delete
    deleteUser: (id: number) =>
        apiClient<null>(`/users/${id}`, { method: 'DELETE' }),
};

🗣️ 실무 대화 예시

💼 API 설계 회의
"주문 취소 API를 어떻게 설계할까요? DELETE /orders/{id}로 하면 될까요?"

"취소는 삭제가 아니라 상태 변경이에요. PATCH /orders/{id}로 status를 'cancelled'로 바꾸거나, POST /orders/{id}/cancel 같은 action 엔드포인트를 쓰는 게 RESTful합니다. DELETE는 데이터 자체를 지울 때만 쓰세요."
🔍 코드 리뷰
"GET 요청에 body를 넣어서 복잡한 검색 조건을 보내려고 하는데요."

"GET에 body 넣는 건 HTTP 스펙상 권장되지 않고, 일부 클라이언트나 프록시에서 무시될 수 있어요. 복잡한 검색은 POST /users/search로 만들거나, GraphQL을 검토해보세요. 쿼리 파라미터가 너무 길어지면 URL 길이 제한에도 걸릴 수 있거든요."
📱 기술 면접
"REST API에서 PUT과 PATCH의 차이점을 설명해주세요."

"PUT은 리소스 전체를 대체하는 멱등성(idempotent) 연산이에요. 같은 요청을 여러 번 보내도 결과가 같아요. PATCH는 부분 업데이트로, 변경할 필드만 보내면 됩니다. 예를 들어 사용자 이메일만 바꿀 때 PUT은 전체 사용자 데이터를 보내야 하지만, PATCH는 이메일 필드만 보내면 돼요."

⚠️ 주의사항

  • 동사형 URL 피하기 - /getUsers, /createUser 대신 /users에 HTTP 메서드로 동작을 표현하세요. URL은 리소스(명사)만 나타내야 합니다.
  • 적절한 상태 코드 사용 - 모든 응답을 200으로 보내고 body에 에러를 담지 마세요. 400, 401, 403, 404, 500 등 의미있는 상태 코드를 활용해야 클라이언트가 적절히 처리할 수 있습니다.
  • 버전 관리 필수 - API가 변경될 때 기존 클라이언트가 깨지지 않도록 /api/v1/, /api/v2/ 같은 버전 관리를 처음부터 도입하세요.

🔗 관련 용어

📚 더 배우기