[Python 100일 챌린지] Day 55 - API 설계 기초
[Python 100일 챌린지] Day 55 - API 설계 기초
@app.route('/api/users')→return jsonify(users)→ 내가 만든 API 완성! 😊데이터만 쓰는 게 아니라 직접 API를 만들어 제공하기! Flask로 5분 만에 RESTful API 서버를 만들 수 있습니다!
(40-50분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 52: HTTP 메서드 - GET, POST, PUT, PATCH, DELETE
- Phase 5의 파일 입출력과 예외 처리
🎯 학습 목표 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 만들기
1
2
# 설치
pip install flask
기본 API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, jsonify, request
app = Flask(__name__)
# 간단한 Hello World API
@app.route('/api/hello', methods=['GET'])
def hello():
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__':
app.run(debug=True, port=5000)
실행 및 테스트:
1
2
3
4
5
6
# 서버 실행
python app.py
# 다른 터미널에서 테스트
curl http://localhost:5000/api/hello
curl http://localhost:5000/api/health
🎯 학습 목표 2: 엔드포인트 구조 설계하기
2.1 CRUD API 설계
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)
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 검색과 필터링
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: 요청과 응답 형식 정의하기
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 에러 핸들링
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 문서화 방법 익히기
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 테스트하고 문서화:
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 버전 관리
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: 이렇게 하세요
- 일관된 네이밍
- 복수형 리소스:
/users,/posts - 소문자 사용:
/api/users(not/API/Users)
- 복수형 리소스:
- 적절한 HTTP 메서드 사용
- GET: 조회만
- POST: 생성만
- PUT/PATCH: 수정만
- DELETE: 삭제만
- 의미 있는 상태 코드
1 2
return jsonify(user), 201 # 생성 성공 return '', 204 # 삭제 성공
- 에러 정보 명확하게
1 2 3 4
return jsonify({ 'error': 'Email already exists', 'field': 'email' }), 400
❌ DON’T: 이러지 마세요
- URL에 동사 사용
1 2 3 4 5
# 나쁜 예 @app.route('/api/getUserById/<id>') # 좋은 예 @app.route('/api/users/<id>')
- 모든 요청을 POST로
- 각 작업에 맞는 HTTP 메서드 사용
- 에러 시 항상 500 반환
- 클라이언트 오류는 4xx
- 서버 오류만 5xx
🧪 연습 문제
문제 1: Blog Post API 설계
블로그 포스트 API를 설계하세요.
요구사항:
- 게시글 CRUD (생성, 조회, 수정, 삭제)
- 목록 조회 시 페이지네이션
- 제목으로 검색
- 적절한 상태 코드 반환
💡 힌트
/api/posts엔드포인트- GET, POST, PUT, DELETE 메서드
request.args.get()for 쿼리 파라미터- 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 53: BeautifulSoup 기초 - HTML 파싱과 CSS 선택자를 학습했습니다
📚 다음 학습
- Day 56: RESTful API - RESTful 원칙을 실전 적용하고 CRUD API를 완성합니다
“늦었다고 생각할 때가 가장 빠른 때입니다. 오늘도 한 걸음 더 나아갔습니다!” 🚀
내일은 Day 56: RESTful API에서 만나요!
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
