포스트

[Python 100일 챌린지] Day 55 - API 설계 기초

[Python 100일 챌린지] Day 55 - API 설계 기초

지금까지 API를 “사용”만 했다면, 이제 직접 “만들어” 봅시다! 😊

Day 51-54에서 requests로 다른 서비스의 API를 호출했죠? 오늘은 내가 API를 만들어서 다른 사람에게 제공하는 방법을 배웁니다! Flask라는 간단한 프레임워크로 나만의 웹 서버를 만들어봐요! 🚀

(40-50분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: API 설계 원칙 이해하기

1.0 API 제공자 vs 소비자

지금까지는 API 소비자(Consumer) 입장이었어요:

1
[우리 프로그램] --requests.get()--> [다른 서비스의 API] --응답--> [데이터 받기]

오늘부터는 API 제공자(Provider) 입장을 배웁니다:

1
[다른 사람/프로그램] --요청--> [우리가 만든 API 서버] --응답--> [데이터 제공]

💡 비유: 지금까지 식당에서 손님으로 음식을 주문했다면, 이제 주방장이 되어 메뉴를 만드는 거예요!

1.1 좋은 API란?

좋은 API의 특징:

특징 설명 예시
일관성 네이밍, 구조, 응답이 일관적 모든 리소스 URL이 복수형 (/users, /posts)
직관성 문서 없이도 이해 가능 GET /users/123 → 123번 사용자 조회
예측 가능성 결과를 예측할 수 있음 DELETE 성공 → 204 No Content
단순성 복잡하지 않고 명확함 하나의 엔드포인트는 하나의 역할만
확장 가능성 쉽게 기능 추가 가능 버전 관리 (/api/v1/...)

1.2 RESTful API 설계 원칙

REST (Representational State Transfer) 아키텍처 스타일의 핵심 원칙:

1. 자원 중심 설계 (Resource-Oriented):

1
2
3
4
5
6
7
8
9
10
11
✅ 좋은 예:
GET    /api/users          # 사용자 목록
GET    /api/users/123      # 특정 사용자
POST   /api/users          # 사용자 생성
PUT    /api/users/123      # 사용자 전체 수정
DELETE /api/users/123      # 사용자 삭제

❌ 나쁜 예:
GET    /api/getUsers
POST   /api/createNewUser
POST   /api/deleteUserById?id=123

2. HTTP 메서드 적절히 사용:

메서드 용도 예시 성공 응답
GET 조회 GET /users 200 OK
POST 생성 POST /users 201 Created
PUT 전체 수정 PUT /users/123 200 OK
PATCH 부분 수정 PATCH /users/123 200 OK
DELETE 삭제 DELETE /users/123 204 No Content

3. 무상태성 (Stateless):

1
2
3
4
5
6
7
8
# ✅ 좋은 예: 모든 요청에 필요한 정보 포함
headers = {
    'Authorization': 'Bearer token123',
    'Content-Type': 'application/json'
}

# ❌ 나쁜 예: 서버 세션에 의존
# 클라이언트가 "이전에 로그인했으니 기억하겠지" 가정

1.3 URL 설계 Best Practices

1. 명사 사용, 동사 피하기:

1
2
3
4
5
6
7
✅ /users
✅ /posts
✅ /products

❌ /getUsers
❌ /createPost
❌ /deleteProduct

2. 복수형 사용:

1
2
3
4
✅ /users/123        (일관성)
✅ /posts/456

❌ /user/123         (혼란 유발)

3. 계층 구조 표현:

1
2
3
4
5
6
7
8
# 사용자의 게시글
GET /users/123/posts

# 게시글의 댓글
GET /posts/456/comments

# 특정 댓글
GET /posts/456/comments/789

4. 필터링, 정렬, 페이지네이션:

1
2
# 쿼리 파라미터 사용
GET /users?status=active&sort=name&page=2&limit=20

1.4 Flask로 간단한 API 만들기

Flask란?

Flask는 Python으로 웹 서버를 만드는 마이크로 프레임워크예요. “마이크로”라서 배우기 쉽고, 필요한 기능만 골라 쓸 수 있어요.

1
2
# 설치
pip install flask

설치 확인:

1
python -c "import flask; print(f'Flask {flask.__version__} 설치 완료!')"

첫 번째 API 서버 만들기:

app.py 파일을 만들고 다음 코드를 작성하세요:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask, jsonify, request

# Flask 앱 생성 (__name__은 현재 파일을 의미)
app = Flask(__name__)

# 간단한 Hello World API
# @app.route: 이 URL로 요청이 오면 아래 함수를 실행해라!
@app.route('/api/hello', methods=['GET'])
def hello():
    # jsonify: Python 딕셔너리를 JSON 형식으로 변환해서 응답
    return jsonify({
        'message': 'Hello, World!',
        'status': 'success'
    })

# 상태 확인 API
@app.route('/api/health', methods=['GET'])
def health():
    return jsonify({
        'status': 'healthy',
        'version': '1.0.0'
    })

# 이 파일을 직접 실행할 때만 서버 시작
if __name__ == '__main__':
    # debug=True: 코드 수정 시 자동 재시작 + 에러 상세 표시
    # port=5000: 5000번 포트에서 서버 실행
    app.run(debug=True, port=5000)

💡 @app.route() 이해하기: @로 시작하는 건 데코레이터라는 Python 문법이에요. 지금은 “이 URL 패턴과 함수를 연결한다”고만 이해하면 돼요!

실행 및 테스트:

1
2
# 터미널 1: 서버 실행
python app.py

실행하면 다음과 같은 메시지가 나와요:

1
2
 * Running on http://127.0.0.1:5000
 * Debug mode: on

⚠️ 중요: 서버가 실행 중인 터미널은 그대로 두세요! 새 터미널을 열어서 테스트합니다.

1
2
# 터미널 2: 테스트 (새 터미널 열기!)
curl http://localhost:5000/api/hello

또는 웹 브라우저에서 http://localhost:5000/api/hello 주소로 직접 접속해도 됩니다!

💡 localhost란?: 내 컴퓨터 자체를 가리키는 특별한 주소예요. 127.0.0.1과 같은 의미입니다.

서버를 종료하려면 터미널 1에서 Ctrl+C를 누르세요.


🎯 학습 목표 2: 엔드포인트 구조 설계하기

2.1 CRUD API 설계

이 절의 Flask 앱은 1.4절의 인사말 앱을 확장해서 붙이는 조각이 아니라 app = Flask(__name__)부터 시작하는 별도 CRUD 예제입니다. 먼저 목록 조회로 초기 데이터를 보고, 사용자 한 명을 만든 다음 반환된 ID로 조회·부분 수정·삭제를 순서대로 확인하세요.

요청마다 URL만 보지 말고 HTTP 메서드·JSON 본문·응답 상태를 함께 기록해보세요. 데이터는 users 리스트에만 저장되므로 서버를 재시작하면 초기 상태로 돌아옵니다. 뒤의 중첩 리소스·검색 코드는 설계 패턴을 설명하는 확장 예제이며, 기본 CRUD와 같은 앱에서 쓰려면 필요한 데이터와 라우트를 함께 구성해야 합니다.

CRUD란?

데이터를 다루는 4가지 기본 작업의 약자예요:

  • Create (생성) → POST
  • Read (조회) → GET
  • Update (수정) → PUT/PATCH
  • Delete (삭제) → DELETE

💡 자주 쓰는 코드 패턴 미리 알기: 아래 코드에서 next((u for u in users if u['id'] == user_id), None) 패턴이 자주 나와요. 이건 “리스트에서 조건에 맞는 첫 번째 항목을 찾고, 없으면 None을 반환”하는 코드예요. 지금은 이 패턴을 통째로 외워두세요!

User 리소스 완전한 CRUD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# In-memory 데이터 (실제로는 DB 사용)
users = [
    {'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'age': 25},
    {'id': 2, 'name': 'Bob', 'email': 'bob@example.com', 'age': 30}
]
next_id = 3

# 1. CREATE - 사용자 생성
@app.route('/api/users', methods=['POST'])
def create_user():
    global next_id

    data = request.get_json()

    # 유효성 검사
    if not data or 'name' not in data or 'email' not in data:
        return jsonify({'error': 'Name and email are required'}), 400

    # 새 사용자 생성
    new_user = {
        'id': next_id,
        'name': data['name'],
        'email': data['email'],
        'age': data.get('age')  # 선택 사항
    }

    users.append(new_user)
    next_id += 1

    return jsonify(new_user), 201  # 201 Created

# 2. READ - 모든 사용자 조회
@app.route('/api/users', methods=['GET'])
def get_users():
    # 쿼리 파라미터로 필터링
    status = request.args.get('status')
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 10, type=int)

    # 페이지네이션
    start = (page - 1) * limit
    end = start + limit
    paginated_users = users[start:end]

    return jsonify({
        'data': paginated_users,
        'page': page,
        'limit': limit,
        'total': len(users)
    })

# 3. READ - 특정 사용자 조회
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    return jsonify(user)

# 4. UPDATE - 사용자 전체 수정 (PUT)
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    data = request.get_json()

    # 전체 교체 (PUT)
    user['name'] = data.get('name', user['name'])
    user['email'] = data.get('email', user['email'])
    user['age'] = data.get('age')

    return jsonify(user)

# 5. UPDATE - 사용자 부분 수정 (PATCH)
@app.route('/api/users/<int:user_id>', methods=['PATCH'])
def patch_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    data = request.get_json()

    # 제공된 필드만 업데이트
    if 'name' in data:
        user['name'] = data['name']
    if 'email' in data:
        user['email'] = data['email']
    if 'age' in data:
        user['age'] = data['age']

    return jsonify(user)

# 6. DELETE - 사용자 삭제
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    global users

    user = next((u for u in users if u['id'] == user_id), None)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    users = [u for u in users if u['id'] != user_id]

    return '', 204  # 204 No Content (성공, 본문 없음)

if __name__ == '__main__':
    app.run(debug=True)

2.2 중첩 리소스 (Nested Resources)

리소스 간에 부모-자식 관계가 있을 때 URL로 표현할 수 있어요.

💡 예시: “사용자가 작성한 게시글”을 표현하려면 /users/1/posts처럼 사용자(부모) 아래에 게시글(자식)을 배치해요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 사용자의 게시글
posts = [
    {'id': 1, 'user_id': 1, 'title': '첫 번째 글', 'content': '...'},
    {'id': 2, 'user_id': 1, 'title': '두 번째 글', 'content': '...'},
    {'id': 3, 'user_id': 2, 'title': '밥의 글', 'content': '...'}
]

# 특정 사용자의 모든 게시글
@app.route('/api/users/<int:user_id>/posts', methods=['GET'])
def get_user_posts(user_id):
    # 사용자 존재 확인
    user = next((u for u in users if u['id'] == user_id), None)
    if not user:
        return jsonify({'error': 'User not found'}), 404

    # 해당 사용자의 게시글 필터링
    user_posts = [p for p in posts if p['user_id'] == user_id]

    return jsonify({
        'user_id': user_id,
        'posts': user_posts,
        'count': len(user_posts)
    })

# 특정 사용자의 게시글 생성
@app.route('/api/users/<int:user_id>/posts', methods=['POST'])
def create_user_post(user_id):
    user = next((u for u in users if u['id'] == user_id), None)
    if not user:
        return jsonify({'error': 'User not found'}), 404

    data = request.get_json()

    new_post = {
        'id': len(posts) + 1,
        'user_id': user_id,
        'title': data['title'],
        'content': data['content']
    }

    posts.append(new_post)

    return jsonify(new_post), 201

2.3 검색과 필터링

쿼리 파라미터를 사용하면 URL에 검색 조건을 추가할 수 있어요.

💡 쿼리 파라미터란?: URL 뒤에 ?로 시작하는 부분이에요. 예: /api/users/search?name=alice&min_age=20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@app.route('/api/users/search', methods=['GET'])
def search_users():
    # 쿼리 파라미터
    name = request.args.get('name', '').lower()
    min_age = request.args.get('min_age', type=int)
    max_age = request.args.get('max_age', type=int)

    # 필터링
    results = users

    if name:
        results = [u for u in results if name in u['name'].lower()]

    if min_age:
        results = [u for u in results if u.get('age', 0) >= min_age]

    if max_age:
        results = [u for u in results if u.get('age', 999) <= max_age]

    return jsonify({
        'query': {
            'name': name,
            'min_age': min_age,
            'max_age': max_age
        },
        'results': results,
        'count': len(results)
    })

🎯 학습 목표 3: 요청과 응답 형식 정의하기

API를 사용할 때 클라이언트가 보내는 요청(Request)과 서버가 돌려주는 응답(Response)의 형식을 알아봅시다.

3.1 요청 형식 (Request Format)

1. Path Parameters (경로 매개변수):

1
2
3
4
5
# /api/users/123
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    # user_id는 URL에서 추출
    pass

2. Query Parameters (쿼리 매개변수):

1
2
3
4
5
6
# /api/users?page=2&limit=10&status=active
@app.route('/api/users')
def get_users():
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 10, type=int)
    status = request.args.get('status')

3. Request Body (요청 본문):

1
2
3
4
5
6
7
# POST /api/users
# Body: {"name": "Charlie", "email": "charlie@example.com"}
@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.get_json()
    name = data.get('name')
    email = data.get('email')

3.2 응답 형식 (Response Format)

표준 응답 구조:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 성공 응답
{
    "status": "success",
    "data": {
        "id": 123,
        "name": "Alice"
    }
}

# 에러 응답
{
    "status": "error",
    "error": {
        "code": "USER_NOT_FOUND",
        "message": "User with ID 123 not found"
    }
}

응답 헬퍼 함수:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def success_response(data, status_code=200):
    """성공 응답 생성"""
    return jsonify({
        'status': 'success',
        'data': data
    }), status_code

def error_response(message, code='ERROR', status_code=400):
    """에러 응답 생성"""
    return jsonify({
        'status': 'error',
        'error': {
            'code': code,
            'message': message
        }
    }), status_code

# 사용
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)

    if not user:
        return error_response('User not found', 'USER_NOT_FOUND', 404)

    return success_response(user)

3.3 HTTP 상태 코드

자주 사용하는 상태 코드:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2xx 성공
return jsonify(data), 200  # OK
return jsonify(data), 201  # Created
return '', 204             # No Content

# 4xx 클라이언트 오류
return jsonify(error), 400  # Bad Request
return jsonify(error), 401  # Unauthorized
return jsonify(error), 403  # Forbidden
return jsonify(error), 404  # Not Found
return jsonify(error), 409  # Conflict

# 5xx 서버 오류
return jsonify(error), 500  # Internal Server Error

3.4 에러 핸들링

에러가 발생했을 때 적절한 응답을 반환하는 방법입니다. Flask의 에러 핸들러를 사용하면 에러별로 일관된 응답을 만들 수 있어요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from flask import Flask, jsonify

app = Flask(__name__)

# 404 Not Found 핸들러
@app.errorhandler(404)
def not_found(error):
    return jsonify({
        'status': 'error',
        'error': {
            'code': 'NOT_FOUND',
            'message': 'Resource not found'
        }
    }), 404

# 500 Internal Server Error 핸들러
@app.errorhandler(500)
def internal_error(error):
    return jsonify({
        'status': 'error',
        'error': {
            'code': 'INTERNAL_ERROR',
            'message': 'An internal error occurred'
        }
    }), 500

# 커스텀 예외
class ValidationError(Exception):
    pass

@app.errorhandler(ValidationError)
def handle_validation_error(error):
    return jsonify({
        'status': 'error',
        'error': {
            'code': 'VALIDATION_ERROR',
            'message': str(error)
        }
    }), 400

# 사용
@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.get_json()

    if not data or 'name' not in data:
        raise ValidationError('Name is required')

    # 사용자 생성...

🎯 학습 목표 4: API 문서화 방법 익히기

좋은 API는 문서화가 잘 되어 있어야 해요. 다른 개발자가 API를 쉽게 이해하고 사용할 수 있도록 문서를 작성하는 방법을 배워봅시다.

4.1 수동 문서화 (Markdown)

API 문서 예시 (API_DOCS.md):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# User API Documentation

## Get All Users

**Endpoint**: `GET /api/users`

**Query Parameters**:
- `page` (integer, optional): 페이지 번호 (default: 1)
- `limit` (integer, optional): 페이지당 항목 수 (default: 10)

**Response**: 200 OK
```json
{
  "data": [
    {
      "id": 1,
      "name": "Alice",
      "email": "alice@example.com",
      "age": 25
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 50
}

Errors:

  • 500: Internal server error

Create User

Endpoint: POST /api/users

Request Body:

1
2
3
4
5
{
  "name": "Charlie",
  "email": "charlie@example.com",
  "age": 28
}

Response: 201 Created

1
2
3
4
5
6
{
  "id": 3,
  "name": "Charlie",
  "email": "charlie@example.com",
  "age": 28
}

Errors:

  • 400: Validation error (name or email missing) ```

4.2 코드 내 문서화 (Docstrings)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@app.route('/api/users', methods=['GET'])
def get_users():
    """
    Get all users with pagination and filtering.

    Query Parameters:
        page (int): Page number (default: 1)
        limit (int): Items per page (default: 10)
        status (str): Filter by status

    Returns:
        200: List of users with pagination info
        {
            "data": [...],
            "page": 1,
            "limit": 10,
            "total": 50
        }

    Example:
        GET /api/users?page=2&limit=20&status=active
    """
    pass

4.3 Postman Collection

💡 Postman이란?: API를 테스트할 수 있는 무료 도구예요. curl 대신 GUI로 편하게 API를 호출하고, 요청/응답을 저장해서 문서로 만들 수 있어요. postman.com에서 다운로드하세요.

Postman으로 API 테스트하고 문서화:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "info": {
    "name": "User API",
    "description": "User management API"
  },
  "item": [
    {
      "name": "Get All Users",
      "request": {
        "method": "GET",
        "url": "http://localhost:5000/api/users",
        "description": "Retrieve all users"
      }
    },
    {
      "name": "Create User",
      "request": {
        "method": "POST",
        "url": "http://localhost:5000/api/users",
        "body": {
          "mode": "raw",
          "raw": "{\"name\": \"Test\", \"email\": \"test@example.com\"}"
        }
      }
    }
  ]
}

4.4 API 버전 관리

API를 업데이트할 때 기존 사용자들이 갑자기 오류를 겪지 않도록 버전을 관리해요.

💡 왜 버전 관리가 필요한가요?: 새 버전에서 응답 형식이 바뀌면 기존 앱이 망가질 수 있어요. 버전을 분리하면 기존 앱은 v1을 계속 쓰고, 새 앱은 v2를 쓸 수 있어요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 버전 1
@app.route('/api/v1/users', methods=['GET'])
def get_users_v1():
    return jsonify(users)

# 버전 2 (다른 응답 형식)
@app.route('/api/v2/users', methods=['GET'])
def get_users_v2():
    return jsonify({
        'version': 'v2',
        'users': users,
        'meta': {
            'total': len(users)
        }
    })

💡 실전 팁 & 주의사항

✅ DO: 이렇게 하세요

  1. 일관된 네이밍
    • 복수형 리소스: /users, /posts
    • 소문자 사용: /api/users (not /API/Users)
  2. 적절한 HTTP 메서드 사용
    • GET: 조회만
    • POST: 생성만
    • PUT/PATCH: 수정만
    • DELETE: 삭제만
  3. 의미 있는 상태 코드
    1
    2
    
    return jsonify(user), 201  # 생성 성공
    return '', 204             # 삭제 성공
    
  4. 에러 정보 명확하게
    1
    2
    3
    4
    
    return jsonify({
        'error': 'Email already exists',
        'field': 'email'
    }), 400
    

❌ DON’T: 이러지 마세요

  1. URL에 동사 사용
    1
    2
    3
    4
    5
    
    # 나쁜 예
    @app.route('/api/getUserById/<id>')
    
    # 좋은 예
    @app.route('/api/users/<id>')
    
  2. 모든 요청을 POST로
    • 각 작업에 맞는 HTTP 메서드 사용
  3. 에러 시 항상 500 반환
    • 클라이언트 오류는 4xx
    • 서버 오류만 5xx

🧪 연습 문제

💡 실습 팁: 2.1절에서 배운 User CRUD 코드를 참고해서 비슷하게 만들어보세요!

문제 1: Blog Post API 설계

블로그 포스트 API를 설계하세요.

요구사항:

  • 게시글 CRUD (생성, 조회, 수정, 삭제)
  • 목록 조회 시 페이지네이션
  • 제목으로 검색
  • 적절한 상태 코드 반환
💡 힌트
  1. /api/posts 엔드포인트
  2. GET, POST, PUT, DELETE 메서드
  3. request.args.get() for 쿼리 파라미터
  4. 201 Created, 204 No Content, 404 Not Found
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from flask import Flask, jsonify, request

app = Flask(__name__)

posts = [
    {'id': 1, 'title': '첫 포스트', 'content': '내용 1', 'author': 'Alice'},
    {'id': 2, 'title': '두 번째 포스트', 'content': '내용 2', 'author': 'Bob'}
]
next_id = 3

# 목록 조회 (페이지네이션 + 검색)
@app.route('/api/posts', methods=['GET'])
def get_posts():
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 10, type=int)
    search = request.args.get('search', '').lower()

    # 검색
    filtered = posts
    if search:
        filtered = [p for p in posts if search in p['title'].lower()]

    # 페이지네이션
    start = (page - 1) * limit
    end = start + limit
    paginated = filtered[start:end]

    return jsonify({
        'data': paginated,
        'page': page,
        'total': len(filtered)
    })

# 생성
@app.route('/api/posts', methods=['POST'])
def create_post():
    global next_id
    data = request.get_json()

    if not data or 'title' not in data or 'content' not in data:
        return jsonify({'error': 'Title and content required'}), 400

    new_post = {
        'id': next_id,
        'title': data['title'],
        'content': data['content'],
        'author': data.get('author', 'Anonymous')
    }

    posts.append(new_post)
    next_id += 1

    return jsonify(new_post), 201

# 조회
@app.route('/api/posts/<int:post_id>', methods=['GET'])
def get_post(post_id):
    post = next((p for p in posts if p['id'] == post_id), None)

    if not post:
        return jsonify({'error': 'Post not found'}), 404

    return jsonify(post)

# 수정
@app.route('/api/posts/<int:post_id>', methods=['PUT'])
def update_post(post_id):
    post = next((p for p in posts if p['id'] == post_id), None)

    if not post:
        return jsonify({'error': 'Post not found'}), 404

    data = request.get_json()
    post.update(data)

    return jsonify(post)

# 삭제
@app.route('/api/posts/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
    global posts

    post = next((p for p in posts if p['id'] == post_id), None)

    if not post:
        return jsonify({'error': 'Post not found'}), 404

    posts = [p for p in posts if p['id'] != post_id]

    return '', 204

if __name__ == '__main__':
    app.run(debug=True)

📝 오늘 배운 내용 정리

주제 핵심 내용
API 설계 원칙 일관성, 직관성, 예측 가능성, REST 아키텍처
엔드포인트 리소스 중심, HTTP 메서드, URL 구조
요청/응답 경로/쿼리 파라미터, JSON, 상태 코드
문서화 Markdown, Docstring, Postman, 버전 관리

핵심 코드 패턴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Flask 기본 구조
from flask import Flask, jsonify, request

app = Flask(__name__)

# CRUD 패턴
@app.route('/api/resource', methods=['GET'])     # 목록 조회
@app.route('/api/resource', methods=['POST'])    # 생성
@app.route('/api/resource/<id>', methods=['GET'])    # 단일 조회
@app.route('/api/resource/<id>', methods=['PUT'])    # 수정
@app.route('/api/resource/<id>', methods=['DELETE']) # 삭제

# 응답 패턴
return jsonify(data), 200   # 성공
return jsonify(error), 404  # 실패

🔗 관련 자료


📚 이전 학습

Day 54: 웹 스크래핑 고급 ⭐⭐⭐

어제는 동적 콘텐츠, 페이지네이션, 재시도 로직, 윤리적 스크래핑을 배웠습니다!

📚 다음 학습

Day 56: RESTful API 실전 ⭐⭐⭐

내일은 RESTful 원칙을 실전 적용하고 CRUD API를 완성합니다!


“늦었다고 생각할 때가 가장 빠른 때입니다. 오늘도 한 걸음 성장했어요!” 🚀

Day 55/100 Phase 6: 웹 스크래핑과 API #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.