[Python 100일 챌린지] Day 87 - REST API 만들기
[Python 100일 챌린지] Day 87 - REST API 만들기
지금까지 우리는 HTML 페이지를 반환하는 웹사이트를 만들었습니다. 🌐 하지만 모바일 앱, 다른 웹사이트, IoT 기기들은 어떻게 우리 데이터를 사용할까요?
답은 REST API입니다!
API(Application Programming Interface)는 프로그램 간 소통 방법입니다. REST API는 HTTP를 사용해 JSON 형식으로 데이터를 주고받는 표준 방식이죠.
오늘은 Flask로 RESTful API를 만듭니다. Instagram, Twitter, GitHub… 모든 서비스가 API를 제공합니다! 우리도 만들어봅시다! 💡
(35분 완독 ⭐⭐⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 86: CRUD 애플리케이션 - CRUD 패턴
- Day 85: SQLite - 데이터베이스
- Day 12: 딕셔너리 - JSON과 유사한 구조
🎯 학습 목표 1: REST API 개념과 설계 원칙
API란?
1
2
3
4
5
6
7
8
# API (Application Programming Interface)
# = 프로그램들이 서로 대화하는 방법
웹사이트 (HTML) 모바일 앱
↓ ↓
└───────→ API ←─────────┘
↓
데이터베이스
REST란?
REST (REpresentational State Transfer)는 웹 API 설계 방식입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# REST 6가지 원칙:
1. 클라이언트-서버 구조
- 클라이언트와 서버가 독립적으로 개발
2. 무상태성 (Stateless)
- 각 요청은 독립적, 서버는 상태 저장 안 함
3. 캐시 가능
- 응답은 캐시 가능 여부 표시
4. 계층화
- 여러 계층으로 구성 가능 (프록시, 게이트웨이 등)
5. 일관된 인터페이스
- URL 구조와 HTTP 메서드 표준화
6. 코드 온디맨드 (선택)
- 서버가 클라이언트에 실행 가능한 코드 전송 가능
HTTP 메서드와 CRUD 매핑
1
2
3
4
5
6
7
HTTP 메서드 CRUD 설명 예시
────────────────────────────────────────────────────────
GET Read 데이터 조회 GET /api/books
POST Create 데이터 생성 POST /api/books
PUT Update 데이터 전체 수정 PUT /api/books/1
PATCH Update 데이터 일부 수정 PATCH /api/books/1
DELETE Delete 데이터 삭제 DELETE /api/books/1
RESTful URL 설계
1
2
3
4
5
6
7
8
9
10
11
12
13
# ✅ 좋은 예 (RESTful)
GET /api/books # 모든 책 조회
GET /api/books/1 # ID 1인 책 조회
POST /api/books # 새 책 추가
PUT /api/books/1 # ID 1인 책 전체 수정
DELETE /api/books/1 # ID 1인 책 삭제
GET /api/books?genre=소설 # 장르별 필터링
# ❌ 나쁜 예 (Non-RESTful)
GET /api/getBooks
POST /api/createBook
GET /api/book_delete?id=1
POST /api/updateBookById
RESTful 설계 규칙:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 명사 사용 (동사 X)
✅ /api/users
❌ /api/getUsers
2. 복수형 사용
✅ /api/books
❌ /api/book
3. 계층 구조 표현
✅ /api/users/1/posts
❌ /api/getUserPosts?userId=1
4. 소문자 사용
✅ /api/blog-posts
❌ /api/BlogPosts
5. 언더스코어보다 하이픈
✅ /api/user-comments
❌ /api/user_comments
🎯 학습 목표 2: JSON 응답 반환하기
JSON이란?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# JSON (JavaScript Object Notation)
# = 데이터 교환을 위한 경량 형식
# Python 딕셔너리와 거의 동일!
# Python 딕셔너리
user = {
"name": "홍길동",
"age": 25,
"email": "[email protected]"
}
# JSON (문자열)
'''
{
"name": "홍길동",
"age": 25,
"email": "[email protected]"
}
'''
Flask에서 JSON 응답 반환
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
# api_basic.py
from flask import Flask, jsonify
app = Flask(__name__)
# 1. 기본 JSON 응답
@app.route('/api/hello')
def hello():
return jsonify({
'message': 'Hello, API!',
'status': 'success'
})
# 2. 리스트 반환
@app.route('/api/numbers')
def numbers():
return jsonify([1, 2, 3, 4, 5])
# 3. 복잡한 구조
@app.route('/api/user')
def user():
return jsonify({
'user': {
'id': 1,
'name': '홍길동',
'email': '[email protected]',
'roles': ['admin', 'user']
},
'timestamp': '2025-06-25T10:00:00Z'
})
if __name__ == '__main__':
app.run(debug=True)
테스트 (curl):
1
2
3
4
5
6
7
curl http://localhost:5000/api/hello
# 응답:
# {
# "message": "Hello, API!",
# "status": "success"
# }
jsonify() vs dict 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import jsonify
@app.route('/test1')
def test1():
# ❌ 단순 딕셔너리 반환 (자동 변환되지만 권장 안 함)
return {'message': 'hello'}
@app.route('/test2')
def test2():
# ✅ jsonify() 사용 (권장)
# - Content-Type: application/json 헤더 자동 설정
# - 적절한 HTTP 상태 코드 처리
return jsonify({'message': 'hello'})
🎯 학습 목표 3: RESTful CRUD API 구현하기
완전한 Books 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
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# books_api.py
from flask import Flask, jsonify, request
import sqlite3
from datetime import datetime
app = Flask(__name__)
DATABASE = 'books.db'
def get_db():
db = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
def init_db():
"""데이터베이스 초기화"""
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
published_year INTEGER,
isbn TEXT UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
# 1. GET /api/books - 모든 책 조회
@app.route('/api/books', methods=['GET'])
def get_books():
"""모든 책 조회"""
db = get_db()
cursor = db.cursor()
# 쿼리 파라미터로 필터링
author = request.args.get('author')
year = request.args.get('year', type=int)
query = 'SELECT * FROM books WHERE 1=1'
params = []
if author:
query += ' AND author LIKE ?'
params.append(f'%{author}%')
if year:
query += ' AND published_year = ?'
params.append(year)
query += ' ORDER BY created_at DESC'
cursor.execute(query, params)
books = cursor.fetchall()
db.close()
# Row 객체를 딕셔너리로 변환
books_list = [dict(book) for book in books]
return jsonify({
'count': len(books_list),
'books': books_list
})
# 2. GET /api/books/<id> - 특정 책 조회
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
"""특정 책 조회"""
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
book = cursor.fetchone()
db.close()
if not book:
return jsonify({'error': 'Book not found'}), 404
return jsonify(dict(book))
# 3. POST /api/books - 새 책 추가
@app.route('/api/books', methods=['POST'])
def create_book():
"""새 책 추가"""
# JSON 데이터 받기
data = request.get_json()
# 필수 필드 검증
if not data:
return jsonify({'error': 'No data provided'}), 400
title = data.get('title')
author = data.get('author')
if not title or not author:
return jsonify({'error': 'Title and author are required'}), 400
# 선택 필드
published_year = data.get('published_year')
isbn = data.get('isbn')
# 데이터베이스에 추가
db = get_db()
cursor = db.cursor()
try:
cursor.execute('''
INSERT INTO books (title, author, published_year, isbn)
VALUES (?, ?, ?, ?)
''', (title, author, published_year, isbn))
db.commit()
book_id = cursor.lastrowid
# 추가된 책 조회
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
new_book = cursor.fetchone()
return jsonify({
'message': 'Book created successfully',
'book': dict(new_book)
}), 201 # 201 Created
except sqlite3.IntegrityError:
return jsonify({'error': 'ISBN already exists'}), 409 # 409 Conflict
finally:
db.close()
# 4. PUT /api/books/<id> - 책 전체 수정
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
"""책 전체 수정"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
title = data.get('title')
author = data.get('author')
published_year = data.get('published_year')
isbn = data.get('isbn')
if not title or not author:
return jsonify({'error': 'Title and author are required'}), 400
db = get_db()
cursor = db.cursor()
# 책이 존재하는지 확인
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
if not cursor.fetchone():
db.close()
return jsonify({'error': 'Book not found'}), 404
try:
cursor.execute('''
UPDATE books
SET title = ?, author = ?, published_year = ?, isbn = ?
WHERE id = ?
''', (title, author, published_year, isbn, book_id))
db.commit()
# 수정된 책 조회
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
updated_book = cursor.fetchone()
return jsonify({
'message': 'Book updated successfully',
'book': dict(updated_book)
})
except sqlite3.IntegrityError:
return jsonify({'error': 'ISBN already exists'}), 409
finally:
db.close()
# 5. PATCH /api/books/<id> - 책 일부 수정
@app.route('/api/books/<int:book_id>', methods=['PATCH'])
def patch_book(book_id):
"""책 일부 수정"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
db = get_db()
cursor = db.cursor()
# 기존 책 조회
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
book = cursor.fetchone()
if not book:
db.close()
return jsonify({'error': 'Book not found'}), 404
# 변경할 필드만 업데이트
update_fields = []
params = []
if 'title' in data:
update_fields.append('title = ?')
params.append(data['title'])
if 'author' in data:
update_fields.append('author = ?')
params.append(data['author'])
if 'published_year' in data:
update_fields.append('published_year = ?')
params.append(data['published_year'])
if 'isbn' in data:
update_fields.append('isbn = ?')
params.append(data['isbn'])
if not update_fields:
return jsonify({'error': 'No fields to update'}), 400
params.append(book_id)
try:
query = f"UPDATE books SET {', '.join(update_fields)} WHERE id = ?"
cursor.execute(query, params)
db.commit()
# 수정된 책 조회
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
updated_book = cursor.fetchone()
return jsonify({
'message': 'Book updated successfully',
'book': dict(updated_book)
})
except sqlite3.IntegrityError:
return jsonify({'error': 'ISBN already exists'}), 409
finally:
db.close()
# 6. DELETE /api/books/<id> - 책 삭제
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
"""책 삭제"""
db = get_db()
cursor = db.cursor()
# 책이 존재하는지 확인
cursor.execute('SELECT * FROM books WHERE id = ?', (book_id,))
book = cursor.fetchone()
if not book:
db.close()
return jsonify({'error': 'Book not found'}), 404
cursor.execute('DELETE FROM books WHERE id = ?', (book_id,))
db.commit()
db.close()
return jsonify({
'message': 'Book deleted successfully',
'deleted_book': dict(book)
})
if __name__ == '__main__':
init_db()
app.run(debug=True)
API 테스트
1. 모든 책 조회 (GET):
1
2
3
4
5
6
7
8
9
10
curl http://localhost:5000/api/books
# 응답:
# {
# "count": 3,
# "books": [
# {"id": 1, "title": "파이썬 기초", "author": "홍길동", ...},
# ...
# ]
# }
2. 특정 책 조회 (GET):
1
curl http://localhost:5000/api/books/1
3. 새 책 추가 (POST):
1
2
3
4
5
6
7
8
curl -X POST http://localhost:5000/api/books \
-H "Content-Type: application/json" \
-d '{
"title": "Flask REST API",
"author": "김철수",
"published_year": 2025,
"isbn": "978-1234567890"
}'
4. 책 수정 (PUT):
1
2
3
4
5
6
7
curl -X PUT http://localhost:5000/api/books/1 \
-H "Content-Type: application/json" \
-d '{
"title": "파이썬 고급",
"author": "홍길동",
"published_year": 2025
}'
5. 일부 수정 (PATCH):
1
2
3
curl -X PATCH http://localhost:5000/api/books/1 \
-H "Content-Type: application/json" \
-d '{"published_year": 2026}'
6. 책 삭제 (DELETE):
1
curl -X DELETE http://localhost:5000/api/books/1
🎯 학습 목표 4: 에러 처리와 상태 코드
HTTP 상태 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2xx 성공
200 OK # 요청 성공
201 Created # 리소스 생성 성공
204 No Content # 성공했지만 반환할 내용 없음
# 4xx 클라이언트 에러
400 Bad Request # 잘못된 요청
401 Unauthorized # 인증 필요
403 Forbidden # 권한 없음
404 Not Found # 리소스 없음
409 Conflict # 충돌 (중복된 데이터 등)
422 Unprocessable Entity # 검증 실패
# 5xx 서버 에러
500 Internal Server Error # 서버 오류
503 Service Unavailable # 서비스 사용 불가
에러 핸들러 구현
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
from flask import jsonify
@app.errorhandler(404)
def not_found(error):
"""404 에러 처리"""
return jsonify({
'error': 'Not Found',
'message': 'The requested resource was not found'
}), 404
@app.errorhandler(500)
def internal_error(error):
"""500 에러 처리"""
return jsonify({
'error': 'Internal Server Error',
'message': 'An unexpected error occurred'
}), 500
@app.errorhandler(400)
def bad_request(error):
"""400 에러 처리"""
return jsonify({
'error': 'Bad Request',
'message': str(error)
}), 400
일관된 응답 형식
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
def success_response(data, message='Success', status=200):
"""성공 응답"""
return jsonify({
'success': True,
'message': message,
'data': data
}), status
def error_response(message, status=400, errors=None):
"""에러 응답"""
response = {
'success': False,
'message': message
}
if errors:
response['errors'] = errors
return jsonify(response), status
# 사용 예:
@app.route('/api/books/<int:book_id>')
def get_book(book_id):
book = get_book_from_db(book_id)
if not book:
return error_response('Book not found', 404)
return success_response(book)
입력 검증 함수
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
def validate_book_data(data, required_fields=None):
"""책 데이터 검증"""
if required_fields is None:
required_fields = ['title', 'author']
errors = []
# 필수 필드 체크
for field in required_fields:
if field not in data or not data[field]:
errors.append(f'{field} is required')
# 타입 검증
if 'published_year' in data and data['published_year']:
year = data['published_year']
if not isinstance(year, int) or year < 1000 or year > 2100:
errors.append('Invalid published_year')
# ISBN 형식 검증 (선택)
if 'isbn' in data and data['isbn']:
isbn = data['isbn'].replace('-', '')
if not isbn.isdigit() or len(isbn) not in [10, 13]:
errors.append('Invalid ISBN format')
return errors
# 사용:
@app.route('/api/books', methods=['POST'])
def create_book():
data = request.get_json()
errors = validate_book_data(data)
if errors:
return error_response('Validation failed', 422, errors)
# 책 생성 로직...
return success_response(new_book, 'Book created', 201)
💻 실전 예제
예제: Postman 대신 Python으로 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
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
# test_api.py
import requests
import json
BASE_URL = 'http://localhost:5000/api'
def test_create_book():
"""책 추가 테스트"""
print("📝 새 책 추가 테스트")
data = {
'title': 'Flask API 마스터',
'author': '박개발',
'published_year': 2025,
'isbn': '978-1111111111'
}
response = requests.post(f'{BASE_URL}/books', json=data)
print(f"상태 코드: {response.status_code}")
print(f"응답: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
return response.json().get('book', {}).get('id')
def test_get_books():
"""모든 책 조회"""
print("\n📚 모든 책 조회")
response = requests.get(f'{BASE_URL}/books')
data = response.json()
print(f"총 {data['count']}권")
for book in data['books']:
print(f" - {book['title']} by {book['author']}")
def test_update_book(book_id):
"""책 수정"""
print(f"\n✏️ 책 {book_id} 수정")
data = {'published_year': 2026}
response = requests.patch(f'{BASE_URL}/books/{book_id}', json=data)
print(f"응답: {response.json()['message']}")
def test_delete_book(book_id):
"""책 삭제"""
print(f"\n🗑️ 책 {book_id} 삭제")
response = requests.delete(f'{BASE_URL}/books/{book_id}')
print(f"응답: {response.json()['message']}")
if __name__ == '__main__':
# 1. 책 추가
book_id = test_create_book()
# 2. 전체 조회
test_get_books()
# 3. 수정
if book_id:
test_update_book(book_id)
# 4. 삭제
if book_id:
test_delete_book(book_id)
# 5. 다시 조회
test_get_books()
⚠️ 주의사항
1. CORS (Cross-Origin Resource Sharing)
1
2
3
4
5
6
7
8
9
10
# 프론트엔드가 다른 도메인에 있을 때 필요
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 모든 도메인 허용
# 또는 특정 도메인만 허용
CORS(app, resources={
r"/api/*": {"origins": "http://localhost:3000"}
})
설치:
1
pip install flask-cors
2. API 버저닝
1
2
3
4
5
6
7
8
9
# ✅ URL에 버전 포함
@app.route('/api/v1/books')
@app.route('/api/v2/books')
# ✅ 또는 헤더로 관리
@app.before_request
def check_api_version():
version = request.headers.get('API-Version', 'v1')
g.api_version = version
3. Rate Limiting (요청 제한)
1
2
3
4
5
6
7
8
9
10
11
12
# Flask-Limiter 사용
from flask_limiter import Limiter
limiter = Limiter(
app,
default_limits=["100 per hour"]
)
@app.route('/api/books')
@limiter.limit("10 per minute")
def get_books():
# ...
🧪 연습 문제
문제: 댓글 API 추가하기
책에 댓글을 달 수 있는 API를 추가하세요:
GET /api/books/<book_id>/comments- 특정 책의 댓글 조회POST /api/books/<book_id>/comments- 댓글 추가DELETE /api/comments/<comment_id>- 댓글 삭제
💡 힌트
1
2
3
4
5
6
7
8
CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id INTEGER NOT NULL,
author TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (book_id) REFERENCES books(id)
);
- book_id를 URL 파라미터로 받기
- JOIN으로 책 정보와 함께 조회 가능
📝 요약
이번 Day 87에서 학습한 내용:
- REST 개념: HTTP 메서드, RESTful 설계 원칙
- JSON 응답:
jsonify(), 딕셔너리 변환 - CRUD API: GET, POST, PUT, PATCH, DELETE
- 에러 처리: 상태 코드, 에러 핸들러, 검증
핵심 패턴:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/api/resource', methods=['GET'])
def get_all():
return jsonify(data), 200
@app.route('/api/resource/<id>', methods=['GET'])
def get_one(id):
return jsonify(data), 200
@app.route('/api/resource', methods=['POST'])
def create():
data = request.get_json()
return jsonify(new_data), 201
@app.route('/api/resource/<id>', methods=['PUT'])
def update(id):
data = request.get_json()
return jsonify(updated_data), 200
@app.route('/api/resource/<id>', methods=['DELETE'])
def delete(id):
return jsonify({'message': 'Deleted'}), 200
🔗 관련 자료
📚 다음 학습
Day 88: 사용자 인증 ⭐⭐⭐⭐⭐
내일은 로그인, 세션, 비밀번호 해싱 등 사용자 인증 시스템을 구현합니다!
“좋은 API는 개발자에게 최고의 선물입니다!” 🎁
Day 87/100 Phase 9: 웹 개발 입문 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
