KR_Tornado - somaz94/python-study GitHub Wiki
Tornado๋ ๋น๋๊ธฐ ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์น ํ๋ ์์ํฌ๋ฅผ ํฌํจํ๋ ํ์ด์ฌ ์น ์๋ฒ๋ก, ๋์ ๋์์ฑ๊ณผ ๊ธด ์ฐ๊ฒฐ์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ต์ ํ๋์ด ์์ต๋๋ค.
import tornado.ioloop
import tornado.web
import tornado.escape
import logging
from typing import Dict, Any, Optional, List, Union
# ๊ธฐ๋ณธ ์ค์
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class BaseHandler(tornado.web.RequestHandler):
"""๋ชจ๋ ํธ๋ค๋ฌ์ ๊ธฐ๋ณธ ํด๋์ค"""
def set_default_headers(self) -> None:
"""์๋ต ํค๋ ๊ธฐ๋ณธ๊ฐ ์ค์ """
self.set_header("Content-Type", "application/json")
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
def options(self, *args, **kwargs) -> None:
"""CORS preflight ์์ฒญ ์ฒ๋ฆฌ"""
self.set_status(204) # No Content
self.finish()
def write_error(self, status_code: int, **kwargs: Any) -> None:
"""์ค๋ฅ ์๋ต ํ์ํ"""
self.set_header("Content-Type", "application/json")
error_data = {
"status": "error",
"code": status_code,
"message": self._reason
}
if "exc_info" in kwargs:
exc_info = kwargs["exc_info"]
if len(exc_info) > 1 and isinstance(exc_info[1], tornado.web.HTTPError):
error_data["details"] = str(exc_info[1])
self.finish(error_data)
class MainHandler(BaseHandler):
"""๋ฉ์ธ ํ์ด์ง ํธ๋ค๋ฌ"""
def get(self) -> None:
"""GET ์์ฒญ ์ฒ๋ฆฌ"""
logger.info("๋ฉ์ธ ํ์ด์ง ์์ฒญ ๋ฐ์")
response_data = {
"status": "success",
"message": "Tornado API ์๋ฒ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค",
"version": "1.0.0",
"documentation": "/docs"
}
self.write(response_data)
class UserHandler(BaseHandler):
"""์ฌ์ฉ์ ๊ด๋ จ API ์๋ํฌ์ธํธ"""
def initialize(self, user_service: Any = None) -> None:
"""์์กด์ฑ ์ฃผ์
"""
self.user_service = user_service or {}
def get(self, user_id: str = None) -> None:
"""์ฌ์ฉ์ ์ ๋ณด ์กฐํ
Args:
user_id: ์กฐํํ ์ฌ์ฉ์ ID (None์ธ ๊ฒฝ์ฐ ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ)
"""
if user_id:
logger.info(f"์ฌ์ฉ์ ์กฐํ ์์ฒญ: ID={user_id}")
# ์ค์ ๊ตฌํ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์กฐํ
user = self.get_user(user_id)
if not user:
raise tornado.web.HTTPError(404, f"์ฌ์ฉ์ ID {user_id}๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
self.write({"status": "success", "data": user})
else:
# ๋ชจ๋ ์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ
logger.info("๋ชจ๋ ์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ ์์ฒญ")
users = self.get_all_users()
self.write({"status": "success", "data": users, "count": len(users)})
def post(self) -> None:
"""์ ์ฌ์ฉ์ ์์ฑ"""
try:
data = tornado.escape.json_decode(self.request.body)
logger.info(f"์ฌ์ฉ์ ์์ฑ ์์ฒญ: {data.get('username', 'unknown')}")
# ํ์ ํ๋ ๊ฒ์ฆ
required_fields = ["username", "email"]
for field in required_fields:
if field not in data:
raise tornado.web.HTTPError(400, f"ํ์ ํ๋ ๋๋ฝ: {field}")
# ์ฌ์ฉ์ ์์ฑ ๋ก์ง (์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ)
user_id = self.create_user(data)
# ์๋ต
self.set_status(201) # Created
self.write({
"status": "success",
"message": "์ฌ์ฉ์๊ฐ ์์ฑ๋์์ต๋๋ค",
"user_id": user_id
})
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
def put(self, user_id: str) -> None:
"""์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ"""
try:
data = tornado.escape.json_decode(self.request.body)
logger.info(f"์ฌ์ฉ์ ์
๋ฐ์ดํธ ์์ฒญ: ID={user_id}")
# ์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ
if not self.user_exists(user_id):
raise tornado.web.HTTPError(404, f"์ฌ์ฉ์ ID {user_id}๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
# ์ฌ์ฉ์ ์
๋ฐ์ดํธ ๋ก์ง
self.update_user(user_id, data)
self.write({
"status": "success",
"message": "์ฌ์ฉ์ ์ ๋ณด๊ฐ ์
๋ฐ์ดํธ๋์์ต๋๋ค",
"user_id": user_id
})
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
def delete(self, user_id: str) -> None:
"""์ฌ์ฉ์ ์ญ์ """
logger.info(f"์ฌ์ฉ์ ์ญ์ ์์ฒญ: ID={user_id}")
# ์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ
if not self.user_exists(user_id):
raise tornado.web.HTTPError(404, f"์ฌ์ฉ์ ID {user_id}๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
# ์ฌ์ฉ์ ์ญ์ ๋ก์ง
self.delete_user(user_id)
self.set_status(204) # No Content
self.finish()
# ๋์ฐ๋ฏธ ๋ฉ์๋๋ค (์ค์ ๊ตฌํ์์๋ ์๋น์ค ๊ณ์ธต์ด๋ ๋ฐ์ดํฐ ์ ๊ทผ ๊ณ์ธต์ผ๋ก ๋ถ๋ฆฌ)
def get_user(self, user_id: str) -> Dict[str, Any]:
"""์ฌ์ฉ์ ์กฐํ (์์ ๊ตฌํ)"""
# ์ํ ๋ฐ์ดํฐ
if user_id == "1":
return {"id": "1", "username": "user1", "email": "[email protected]"}
return None
def get_all_users(self) -> List[Dict[str, Any]]:
"""๋ชจ๋ ์ฌ์ฉ์ ์กฐํ (์์ ๊ตฌํ)"""
# ์ํ ๋ฐ์ดํฐ
return [
{"id": "1", "username": "user1", "email": "[email protected]"},
{"id": "2", "username": "user2", "email": "[email protected]"}
]
def create_user(self, data: Dict[str, Any]) -> str:
"""์ฌ์ฉ์ ์์ฑ (์์ ๊ตฌํ)"""
# ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
return "3" # ์ ์ฌ์ฉ์ ID
def update_user(self, user_id: str, data: Dict[str, Any]) -> None:
"""์ฌ์ฉ์ ์
๋ฐ์ดํธ (์์ ๊ตฌํ)"""
# ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
pass
def user_exists(self, user_id: str) -> bool:
"""์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ (์์ ๊ตฌํ)"""
return user_id in ["1", "2"]
def delete_user(self, user_id: str) -> None:
"""์ฌ์ฉ์ ์ญ์ (์์ ๊ตฌํ)"""
# ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ญ์
pass
def make_app(debug: bool = False) -> tornado.web.Application:
"""Tornado ์ ํ๋ฆฌ์ผ์ด์
์์ฑ
Args:
debug: ๋๋ฒ๊ทธ ๋ชจ๋ ํ์ฑํ ์ฌ๋ถ
Returns:
tornado.web.Application: ๊ตฌ์ฑ๋ ์ ํ๋ฆฌ์ผ์ด์
"""
settings = {
"debug": debug,
"autoreload": debug,
"compress_response": True
}
return tornado.web.Application([
(r"/", MainHandler),
(r"/users/?", UserHandler),
(r"/users/([0-9]+)", UserHandler),
], **settings)
if __name__ == "__main__":
# ์ ํ๋ฆฌ์ผ์ด์
์์
port = 8888
app = make_app(debug=True)
app.listen(port)
logger.info(f"์๋ฒ๊ฐ http://localhost:{port} ์์ ์์๋์์ต๋๋ค")
logger.info("Ctrl+C๋ฅผ ๋๋ฌ ์ข
๋ฃํ์ธ์")
# IOLoop ์์
tornado.ioloop.IOLoop.current().start()
โ ํน์ง:
- ๋น๋๊ธฐ ์น ์๋ฒ ๋ฐ ํ๋ ์์ํฌ
- ๊ณ ์ฑ๋ฅ ๋คํธ์ํน ์ง์
- RESTful API ๋ผ์ฐํ ์ค์
- HTTP ๋ฉ์๋ ๊ตฌํ (GET, POST, PUT, DELETE, OPTIONS)
- ์๋ฌ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ
- ๋ก๊น ํตํฉ
- CORS ์ง์
- JSON ์๋ต ํ์ํ
- ์์กด์ฑ ์ฃผ์ ํจํด
- ํ์ ํํ ์ผ๋ก ์ฝ๋ ๊ฐ๋ ์ฑ ํฅ์
Tornado๋ ๋น๋๊ธฐ I/O๋ฅผ ํ์ฉํ์ฌ ๋ง์ ๋์ ์ฐ๊ฒฐ์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
import tornado.ioloop
import tornado.web
import tornado.gen
import tornado.httpclient
import tornado.websocket
import tornado.concurrent
import time
import json
import asyncio
from typing import Dict, Any, List, Optional, Union, Awaitable
class AsyncHandler(tornado.web.RequestHandler):
"""๋น๋๊ธฐ ์์ฒญ ์ฒ๋ฆฌ ์์ """
async def get(self) -> None:
"""๋น๋๊ธฐ GET ์์ฒญ ์ฒ๋ฆฌ"""
# ๋น๋๊ธฐ ์์
์ํ
result = await self.async_operation()
self.write({"status": "success", "result": result})
async def async_operation(self) -> str:
"""๋น๋๊ธฐ ์์
์๋ฎฌ๋ ์ด์
"""
# ๋น๋๊ธฐ ๋๊ธฐ (I/O ์์
์๋ฎฌ๋ ์ด์
)
await tornado.gen.sleep(1)
return "๋น๋๊ธฐ ์์
์๋ฃ"
class ParallelRequestHandler(tornado.web.RequestHandler):
"""๋ณ๋ ฌ ๋น๋๊ธฐ ์์ฒญ ์ฒ๋ฆฌ ์์ """
async def get(self) -> None:
"""์ฌ๋ฌ API๋ฅผ ๋ณ๋ ฌ๋ก ํธ์ถ"""
# HTTP ํด๋ผ์ด์ธํธ ์์ฑ
http_client = tornado.httpclient.AsyncHTTPClient()
# ์ฌ๋ฌ ์์ฒญ์ ๋์์ ์ํ
urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/users/1",
"https://jsonplaceholder.typicode.com/todos/1"
]
start_time = time.time()
# ๋ฐฉ๋ฒ 1: asyncio.gather ์ฌ์ฉ (Python 3.7+)
responses = await asyncio.gather(*[
self.fetch_url(http_client, url) for url in urls
])
# ๋ฐฉ๋ฒ 2: tornado.gen.multi ์ฌ์ฉ
# responses = await tornado.gen.multi([
# self.fetch_url(http_client, url) for url in urls
# ])
elapsed = time.time() - start_time
# ๊ฒฐ๊ณผ ์กฐํฉ
result = {
"post": responses[0],
"user": responses[1],
"todo": responses[2],
"elapsed_time": f"{elapsed:.2f} ์ด"
}
self.write(result)
async def fetch_url(self, client: tornado.httpclient.AsyncHTTPClient, url: str) -> Dict[str, Any]:
"""๋จ์ผ URL ๋น๋๊ธฐ ์์ฒญ
Args:
client: ๋น๋๊ธฐ HTTP ํด๋ผ์ด์ธํธ
url: ์์ฒญํ URL
Returns:
Dict: ์๋ต ๋ฐ์ดํฐ
"""
try:
response = await client.fetch(url)
return json.loads(response.body)
except Exception as e:
return {"error": str(e)}
class FutureHandler(tornado.web.RequestHandler):
"""Future ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ๋น๋๊ธฐ ์ฒ๋ฆฌ"""
def get(self) -> None:
"""Future ๊ฐ์ฒด ๋ฐํ"""
self.future = tornado.concurrent.Future()
# ๋ค๋ฅธ ์ค๋ ๋/ํ๋ก์ธ์ค์์ ์์
์ํ ์๋ฎฌ๋ ์ด์
tornado.ioloop.IOLoop.current().add_callback(self.process_request)
# Future ์๋ฃ ์ ์คํํ ์ฝ๋ฐฑ ๋ฑ๋ก
self.future.add_done_callback(
lambda f: self.complete_response(f.result())
)
def process_request(self) -> None:
"""๋น๋๊ธฐ ์์
์๋ฎฌ๋ ์ด์
"""
# ์ค์ ๋ก๋ ๋ณต์กํ ๊ณ์ฐ์ด๋ I/O ์์
์ํ
tornado.ioloop.IOLoop.current().call_later(
2, lambda: self.future.set_result("์์
๊ฒฐ๊ณผ")
)
def complete_response(self, result: str) -> None:
"""Future ์๋ฃ ์ ์๋ต ๋ฐํ"""
if not self._finished:
self.write({"status": "success", "result": result})
self.finish()
class WebSocketHandler(tornado.websocket.WebSocketHandler):
"""WebSocket ํธ๋ค๋ฌ"""
# ๋ชจ๋ ํ์ฑ ์ฐ๊ฒฐ ์ ์ฅ
clients = set()
def check_origin(self, origin: str) -> bool:
"""WebSocket ์ฐ๊ฒฐ ์ค๋ฆฌ์ง ๊ฒ์ฆ
์ค์ ํ๋ก๋์
์์๋ ๋ณด์์ ์ํด ์ ํํด์ผ ํจ
"""
return True # ๋ชจ๋ ์ค๋ฆฌ์ง ํ์ฉ (๊ฐ๋ฐ์ฉ)
async def open(self) -> None:
"""WebSocket ์ฐ๊ฒฐ ์ ํธ์ถ"""
self.clients.add(self)
await self.write_message({"status": "connected", "message": "WebSocket ์ฐ๊ฒฐ๋จ"})
print(f"์ WebSocket ์ฐ๊ฒฐ: ํ์ฌ {len(self.clients)}๊ฐ ์ฐ๊ฒฐ")
async def on_message(self, message: Union[str, bytes]) -> None:
"""ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง ์์ ์ ํธ์ถ
Args:
message: ์์ ๋ ๋ฉ์์ง (๋ฌธ์์ด ๋๋ ๋ฐ์ด๋๋ฆฌ)
"""
print(f"๋ฉ์์ง ์์ : {message}")
# JSON ํ์ฑ ์๋
try:
if isinstance(message, bytes):
message = message.decode('utf-8')
data = json.loads(message)
# ์์ฝ ๊ธฐ๋ฅ
if data.get('action') == 'echo':
await self.write_message({
"status": "success",
"action": "echo",
"data": data.get('data')
})
# ๋ธ๋ก๋์บ์คํธ ๊ธฐ๋ฅ
elif data.get('action') == 'broadcast':
await self.broadcast({
"status": "success",
"action": "broadcast",
"sender": id(self),
"data": data.get('data')
})
else:
await self.write_message({
"status": "error",
"message": "์ ์ ์๋ ์ก์
"
})
except json.JSONDecodeError:
await self.write_message({
"status": "error",
"message": "์๋ชป๋ JSON ํ์"
})
def on_close(self) -> None:
"""WebSocket ์ฐ๊ฒฐ ์ข
๋ฃ ์ ํธ์ถ"""
self.clients.remove(self)
print(f"WebSocket ์ฐ๊ฒฐ ์ข
๋ฃ: ํ์ฌ {len(self.clients)}๊ฐ ์ฐ๊ฒฐ")
async def broadcast(self, message: Dict[str, Any]) -> None:
"""๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ๋ธ๋ก๋์บ์คํธ
Args:
message: ์ ์กํ ๋ฉ์์ง
"""
for client in self.clients:
try:
await client.write_message(message)
except Exception as e:
print(f"๋ธ๋ก๋์บ์คํธ ์๋ฌ: {e}")
# EventSource (Server-Sent Events) ์์
class EventSourceHandler(tornado.web.RequestHandler):
"""Server-Sent Events (EventSource) ํธ๋ค๋ฌ"""
async def get(self) -> None:
"""SSE ์คํธ๋ฆผ ์ค์ """
# EventSource ํค๋ ์ค์
self.set_header('Content-Type', 'text/event-stream')
self.set_header('Cache-Control', 'no-cache')
self.set_header('Connection', 'keep-alive')
# ์ด๊ธฐ ์ฐ๊ฒฐ ๋ฉ์์ง
await self.write_event(None, 'connected', {'status': 'connected'})
await self.flush()
# ์ฃผ๊ธฐ์ ์ผ๋ก ์ด๋ฒคํธ ์ ์ก
for i in range(5):
await tornado.gen.sleep(2) # 2์ด ๊ฐ๊ฒฉ
await self.write_event(
f"event-{i}",
'update',
{'timestamp': time.time(), 'count': i}
)
await self.flush()
# ์ข
๋ฃ ์ด๋ฒคํธ
await self.write_event(None, 'close', {'status': 'complete'})
self.finish()
async def write_event(self, id_value: Optional[str], event_type: str, data: Dict[str, Any]) -> None:
"""SSE ์ด๋ฒคํธ ์์ฑ
Args:
id_value: ์ด๋ฒคํธ ID (None์ด๋ฉด ์๋ต)
event_type: ์ด๋ฒคํธ ํ์
data: ์ด๋ฒคํธ ๋ฐ์ดํฐ
"""
if id_value:
self.write(f"id: {id_value}\n")
self.write(f"event: {event_type}\n")
self.write(f"data: {json.dumps(data)}\n\n")
# ๋น๋๊ธฐ ์ ํ๋ฆฌ์ผ์ด์
์ค์
def make_async_app(debug: bool = False) -> tornado.web.Application:
"""๋น๋๊ธฐ ์ ํ๋ฆฌ์ผ์ด์
์์ฑ"""
settings = {
"debug": debug,
"autoreload": debug
}
return tornado.web.Application([
(r"/async", AsyncHandler),
(r"/parallel", ParallelRequestHandler),
(r"/future", FutureHandler),
(r"/ws", WebSocketHandler),
(r"/events", EventSourceHandler),
], **settings)
if __name__ == "__main__":
# ์ ํ๋ฆฌ์ผ์ด์
์์
port = 8888
app = make_async_app(debug=True)
app.listen(port)
print(f"๋น๋๊ธฐ ์๋ฒ๊ฐ http://localhost:{port} ์์ ์์๋์์ต๋๋ค")
# IOLoop ์์
tornado.ioloop.IOLoop.current().start()
โ ํน์ง:
- ์ฝ๋ฃจํด(async/await) ์ง์
- ๋น๋๊ธฐ HTTP ํด๋ผ์ด์ธํธ
- ๋์ ์์ฒญ ์ฒ๋ฆฌ
- Future ๊ฐ์ฒด๋ฅผ ํตํ ๋น๋๊ธฐ ํต์
- WebSocket ์๋ฐฉํฅ ํต์
- ๋ธ๋ก๋์บ์คํธ ํจํด ๊ตฌํ
- Server-Sent Events(SSE) ์คํธ๋ฆฌ๋ฐ
- JSON ๊ธฐ๋ฐ ๋ฉ์์ง ๊ตํ
- ๋น๋๊ธฐ ํ์์์ ๋ฐ ์ฝ๋ฐฑ
- asyncio์์ ํตํฉ
- ํ์ ํํ ์ ํตํ ์ฝ๋ ๋ช ํ์ฑ
Tornado์์๋ ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ์ ์ํ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
import tornado.web
import tornado.ioloop
import tornado.gen
import tornado.escape
import aiomysql
import aiopg
import motor.motor_tornado
from typing import Dict, Any, List, Optional, Union, Awaitable
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
DB_CONFIG = {
'mysql': {
'host': 'localhost',
'port': 3306,
'user': 'user',
'password': 'password',
'db': 'tornado_db',
'charset': 'utf8mb4'
},
'postgres': {
'host': 'localhost',
'port': 5432,
'user': 'user',
'password': 'password',
'database': 'tornado_db'
},
'mongodb': {
'uri': 'mongodb://localhost:27017',
'db': 'tornado_db'
}
}
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ ๊ด๋ฆฌ ํด๋์ค
class DatabaseManager:
"""๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๊ด๋ฆฌ"""
def __init__(self) -> None:
"""์ด๊ธฐํ"""
self.mysql_pool = None
self.postgres_pool = None
self.mongodb = None
async def initialize(self) -> None:
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ด๊ธฐํ"""
# MySQL/MariaDB ์ฐ๊ฒฐ ํ (aiomysql ์ฌ์ฉ)
self.mysql_pool = await aiomysql.create_pool(
**DB_CONFIG['mysql'],
minsize=1,
maxsize=10,
autocommit=True
)
# PostgreSQL ์ฐ๊ฒฐ ํ (aiopg ์ฌ์ฉ)
self.postgres_pool = await aiopg.create_pool(
**DB_CONFIG['postgres']
)
# MongoDB ์ฐ๊ฒฐ (motor ์ฌ์ฉ)
mongo_client = motor.motor_tornado.MotorClient(DB_CONFIG['mongodb']['uri'])
self.mongodb = mongo_client[DB_CONFIG['mongodb']['db']]
print("๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ด ์ด๊ธฐํ๋์์ต๋๋ค.")
async def close(self) -> None:
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ข
๋ฃ"""
if self.mysql_pool:
self.mysql_pool.close()
await self.mysql_pool.wait_closed()
if self.postgres_pool:
self.postgres_pool.close()
await self.postgres_pool.wait_closed()
# MongoDB๋ ๋ช
์์ ์ธ ์ข
๋ฃ๊ฐ ํ์ ์์
print("๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ด ์ข
๋ฃ๋์์ต๋๋ค.")
# MySQL ๋ฐ์ดํฐ ์ก์ธ์ค ํด๋์ค
class MySQLDataAccess:
"""MySQL ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
def __init__(self, pool: aiomysql.Pool) -> None:
"""์ด๊ธฐํ
Args:
pool: MySQL ์ฐ๊ฒฐ ํ
"""
self.pool = pool
async def fetch_one(self, query: str, *args, **kwargs) -> Optional[Dict[str, Any]]:
"""๋จ์ผ ๊ฒฐ๊ณผ ์กฐํ
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
Dict ๋๋ None: ์กฐํ ๊ฒฐ๊ณผ
"""
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(query, args or kwargs)
result = await cursor.fetchone()
return result
async def fetch_all(self, query: str, *args, **kwargs) -> List[Dict[str, Any]]:
"""๋ค์ค ๊ฒฐ๊ณผ ์กฐํ
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
List[Dict]: ์กฐํ ๊ฒฐ๊ณผ ๋ชฉ๋ก
"""
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(query, args or kwargs)
result = await cursor.fetchall()
return result
async def execute(self, query: str, *args, **kwargs) -> int:
"""์ฟผ๋ฆฌ ์คํ (INSERT, UPDATE, DELETE)
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
int: ์ํฅ ๋ฐ์ ํ ์
"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, args or kwargs)
return cursor.rowcount
async def execute_many(self, query: str, params: List[tuple]) -> int:
"""๋ค์ค ์ฟผ๋ฆฌ ์คํ
Args:
query: SQL ์ฟผ๋ฆฌ
params: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๋ชฉ๋ก
Returns:
int: ์ํฅ ๋ฐ์ ํ ์
"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
result = await cursor.executemany(query, params)
return result
# PostgreSQL ๋ฐ์ดํฐ ์ก์ธ์ค ํด๋์ค
class PostgresDataAccess:
"""PostgreSQL ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
def __init__(self, pool: aiopg.Pool) -> None:
"""์ด๊ธฐํ
Args:
pool: PostgreSQL ์ฐ๊ฒฐ ํ
"""
self.pool = pool
async def fetch_one(self, query: str, *args, **kwargs) -> Optional[Dict[str, Any]]:
"""๋จ์ผ ๊ฒฐ๊ณผ ์กฐํ
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
Dict ๋๋ None: ์กฐํ ๊ฒฐ๊ณผ
"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, args or kwargs)
result = await cursor.fetchone()
if result:
columns = [desc[0] for desc in cursor.description]
return dict(zip(columns, result))
return None
async def fetch_all(self, query: str, *args, **kwargs) -> List[Dict[str, Any]]:
"""๋ค์ค ๊ฒฐ๊ณผ ์กฐํ
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
List[Dict]: ์กฐํ ๊ฒฐ๊ณผ ๋ชฉ๋ก
"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, args or kwargs)
results = await cursor.fetchall()
if results:
columns = [desc[0] for desc in cursor.description]
return [dict(zip(columns, row)) for row in results]
return []
async def execute(self, query: str, *args, **kwargs) -> int:
"""์ฟผ๋ฆฌ ์คํ (INSERT, UPDATE, DELETE)
Args:
query: SQL ์ฟผ๋ฆฌ
args, kwargs: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
Returns:
int: ์ํฅ ๋ฐ์ ํ ์
"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, args or kwargs)
return cursor.rowcount
# MongoDB ๋ฐ์ดํฐ ์ก์ธ์ค ํด๋์ค
class MongoDataAccess:
"""MongoDB ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
def __init__(self, db: motor.motor_tornado.MotorDatabase) -> None:
"""์ด๊ธฐํ
Args:
db: MongoDB ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ฒด
"""
self.db = db
async def find_one(self, collection: str, query: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""๋จ์ผ ๋ฌธ์ ์กฐํ
Args:
collection: ์ปฌ๋ ์
์ด๋ฆ
query: ์กฐํ ์ฟผ๋ฆฌ
Returns:
Dict ๋๋ None: ์กฐํ ๊ฒฐ๊ณผ
"""
result = await self.db[collection].find_one(query)
return result
async def find_many(self, collection: str, query: Dict[str, Any], limit: int = 0) -> List[Dict[str, Any]]:
"""๋ค์ค ๋ฌธ์ ์กฐํ
Args:
collection: ์ปฌ๋ ์
์ด๋ฆ
query: ์กฐํ ์ฟผ๋ฆฌ
limit: ๊ฒฐ๊ณผ ์ ํ (0์ ์ ํ ์์)
Returns:
List[Dict]: ์กฐํ ๊ฒฐ๊ณผ ๋ชฉ๋ก
"""
cursor = self.db[collection].find(query)
if limit > 0:
cursor = cursor.limit(limit)
results = []
async for document in cursor:
results.append(document)
return results
async def insert_one(self, collection: str, document: Dict[str, Any]) -> str:
"""๋จ์ผ ๋ฌธ์ ์ฝ์
Args:
collection: ์ปฌ๋ ์
์ด๋ฆ
document: ์ฝ์
ํ ๋ฌธ์
Returns:
str: ์ฝ์
๋ ๋ฌธ์ ID
"""
result = await self.db[collection].insert_one(document)
return str(result.inserted_id)
async def update_one(self, collection: str, query: Dict[str, Any], update: Dict[str, Any]) -> int:
"""๋จ์ผ ๋ฌธ์ ์
๋ฐ์ดํธ
Args:
collection: ์ปฌ๋ ์
์ด๋ฆ
query: ์กฐํ ์ฟผ๋ฆฌ
update: ์
๋ฐ์ดํธ ๋ด์ฉ
Returns:
int: ์
๋ฐ์ดํธ๋ ๋ฌธ์ ์
"""
result = await self.db[collection].update_one(query, {'$set': update})
return result.modified_count
async def delete_one(self, collection: str, query: Dict[str, Any]) -> int:
"""๋จ์ผ ๋ฌธ์ ์ญ์
Args:
collection: ์ปฌ๋ ์
์ด๋ฆ
query: ์กฐํ ์ฟผ๋ฆฌ
Returns:
int: ์ญ์ ๋ ๋ฌธ์ ์
"""
result = await self.db[collection].delete_one(query)
return result.deleted_count
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฏน์ค์ธ
class DatabaseMixin:
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ์ ์ํ ๋ฏน์ค์ธ"""
@property
def db_manager(self) -> DatabaseManager:
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ๋งค๋์ ์ ๊ทผ"""
return self.application.db_manager
@property
def mysql(self) -> MySQLDataAccess:
"""MySQL ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
return self.application.mysql_dao
@property
def postgres(self) -> PostgresDataAccess:
"""PostgreSQL ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
return self.application.postgres_dao
@property
def mongo(self) -> MongoDataAccess:
"""MongoDB ๋ฐ์ดํฐ ์ก์ธ์ค ๊ฐ์ฒด"""
return self.application.mongo_dao
# ์ฌ์ฉ์ ํธ๋ค๋ฌ
class UserDatabaseHandler(tornado.web.RequestHandler, DatabaseMixin):
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ์ฌ์ฉ์ ํธ๋ค๋ฌ"""
async def get(self, user_id: Optional[str] = None) -> None:
"""์ฌ์ฉ์ ์ ๋ณด ์กฐํ"""
if user_id:
# MySQL์ ์ฌ์ฉํ ๋จ์ผ ์ฌ์ฉ์ ์กฐํ
query = "SELECT * FROM users WHERE id = %s"
user = await self.mysql.fetch_one(query, user_id)
if not user:
raise tornado.web.HTTPError(404, "์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
self.write({"user": user})
else:
# PostgreSQL์ ์ฌ์ฉํ ์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ
query = "SELECT * FROM users LIMIT 10"
users = await self.postgres.fetch_all(query)
self.write({"users": users})
async def post(self) -> None:
"""์ ์ฌ์ฉ์ ์์ฑ (MongoDB ์ฌ์ฉ)"""
try:
data = tornado.escape.json_decode(self.request.body)
# ํ์ ํ๋ ๊ฒ์ฆ
required_fields = ["username", "email"]
for field in required_fields:
if field not in data:
raise tornado.web.HTTPError(400, f"ํ์ ํ๋ ๋๋ฝ: {field}")
# MongoDB์ ์ฌ์ฉ์ ์ถ๊ฐ
user_id = await self.mongo.insert_one("users", data)
self.set_status(201)
self.write({
"status": "success",
"message": "์ฌ์ฉ์๊ฐ ์์ฑ๋์์ต๋๋ค",
"user_id": user_id
})
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
async def put(self, user_id: str) -> None:
"""์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ (MySQL ์ฌ์ฉ)"""
try:
data = tornado.escape.json_decode(self.request.body)
# ์ฌ์ฉ์ ์กด์ฌ ํ์ธ
check_query = "SELECT id FROM users WHERE id = %s"
user = await self.mysql.fetch_one(check_query, user_id)
if not user:
raise tornado.web.HTTPError(404, "์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
# SQL ์์ฑ
set_clauses = []
params = []
for key, value in data.items():
if key != 'id': # ID๋ ์
๋ฐ์ดํธ ๋ถ๊ฐ
set_clauses.append(f"{key} = %s")
params.append(value)
if not set_clauses:
raise tornado.web.HTTPError(400, "์
๋ฐ์ดํธํ ํ๋๊ฐ ์์ต๋๋ค")
params.append(user_id) # WHERE ์ ์ ํ๋ผ๋ฏธํฐ
# ์
๋ฐ์ดํธ ์ฟผ๋ฆฌ ์คํ
query = f"UPDATE users SET {', '.join(set_clauses)} WHERE id = %s"
result = await self.mysql.execute(query, *params)
self.write({
"status": "success",
"message": "์ฌ์ฉ์ ์ ๋ณด๊ฐ ์
๋ฐ์ดํธ๋์์ต๋๋ค",
"affected_rows": result
})
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
async def delete(self, user_id: str) -> None:
"""์ฌ์ฉ์ ์ญ์ (PostgreSQL ์ฌ์ฉ)"""
query = "DELETE FROM users WHERE id = %s"
result = await self.postgres.execute(query, user_id)
if result == 0:
raise tornado.web.HTTPError(404, "์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
self.set_status(204) # No Content
self.finish()
# ํธ๋์ญ์
์์
class TransactionHandler(tornado.web.RequestHandler, DatabaseMixin):
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ํธ๋์ญ์
์ฒ๋ฆฌ ์์ """
async def post(self) -> None:
"""ํธ๋์ญ์
์์ (์ก๊ธ ์๋ฎฌ๋ ์ด์
)"""
try:
data = tornado.escape.json_decode(self.request.body)
from_account = data.get('from_account')
to_account = data.get('to_account')
amount = data.get('amount')
if not all([from_account, to_account, amount]):
raise tornado.web.HTTPError(400, "ํ์ ํ๋๊ฐ ๋๋ฝ๋์์ต๋๋ค")
if from_account == to_account:
raise tornado.web.HTTPError(400, "๊ฐ์ ๊ณ์ข๋ก ์ก๊ธํ ์ ์์ต๋๋ค")
# MySQL์ ์ฌ์ฉํ ํธ๋์ญ์
์ฒ๋ฆฌ
async with self.db_manager.mysql_pool.acquire() as conn:
# ์๋ ์ปค๋ฐ ๋นํ์ฑํ
await conn.begin()
try:
async with conn.cursor() as cursor:
# ์ถ๊ธ ๊ณ์ข ์์ก ํ์ธ
await cursor.execute(
"SELECT balance FROM accounts WHERE account_number = %s FOR UPDATE",
(from_account,)
)
from_balance_result = await cursor.fetchone()
if not from_balance_result:
raise ValueError("์ถ๊ธ ๊ณ์ข๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
from_balance = from_balance_result[0]
if from_balance < amount:
raise ValueError("์์ก์ด ๋ถ์กฑํฉ๋๋ค")
# ์ถ๊ธ ๊ณ์ข์์ ๊ธ์ก ์ฐจ๊ฐ
await cursor.execute(
"UPDATE accounts SET balance = balance - %s WHERE account_number = %s",
(amount, from_account)
)
# ์
๊ธ ๊ณ์ข ํ์ธ
await cursor.execute(
"SELECT id FROM accounts WHERE account_number = %s",
(to_account,)
)
if not await cursor.fetchone():
raise ValueError("์
๊ธ ๊ณ์ข๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
# ์
๊ธ ๊ณ์ข์ ๊ธ์ก ์ถ๊ฐ
await cursor.execute(
"UPDATE accounts SET balance = balance + %s WHERE account_number = %s",
(amount, to_account)
)
# ํธ๋์ญ์
๊ธฐ๋ก ์ถ๊ฐ
await cursor.execute(
"INSERT INTO transactions (from_account, to_account, amount, timestamp) VALUES (%s, %s, %s, NOW())",
(from_account, to_account, amount)
)
transaction_id = cursor.lastrowid
# ์ปค๋ฐ
await conn.commit()
self.write({
"status": "success",
"message": "์ก๊ธ์ด ์๋ฃ๋์์ต๋๋ค",
"transaction_id": transaction_id
})
except Exception as e:
# ๋กค๋ฐฑ
await conn.rollback()
raise tornado.web.HTTPError(400, str(e))
except ValueError as e:
raise tornado.web.HTTPError(400, str(e))
# ์ ํ๋ฆฌ์ผ์ด์
์ค์
class Application(tornado.web.Application):
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ์ ํ๋ฆฌ์ผ์ด์
"""
def __init__(self) -> None:
"""์ ํ๋ฆฌ์ผ์ด์
์ด๊ธฐํ"""
handlers = [
(r"/api/users/?", UserDatabaseHandler),
(r"/api/users/([0-9]+)", UserDatabaseHandler),
(r"/api/transactions", TransactionHandler),
]
settings = {
"debug": True,
"autoreload": True
}
super(Application, self).__init__(handlers, **settings)
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋งค๋์ ์์ฑ
self.db_manager = DatabaseManager()
# DAO ๊ฐ์ฒด ์ด๊ธฐํ (์ค์ ์ฐ๊ฒฐ์ setup_db์์ ์ํ)
self.mysql_dao = None
self.postgres_dao = None
self.mongo_dao = None
async def setup_db(self) -> None:
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ """
await self.db_manager.initialize()
# DAO ๊ฐ์ฒด ์์ฑ
self.mysql_dao = MySQLDataAccess(self.db_manager.mysql_pool)
self.postgres_dao = PostgresDataAccess(self.db_manager.postgres_pool)
self.mongo_dao = MongoDataAccess(self.db_manager.mongodb)
# ๋ฉ์ธ ํจ์
async def main() -> None:
"""์ ํ๋ฆฌ์ผ์ด์
์์"""
app = Application()
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ
await app.setup_db()
# ์๋ฒ ์์
app.listen(8888)
print("์๋ฒ๊ฐ http://localhost:8888 ์์ ์์๋์์ต๋๋ค")
# ์ข
๋ฃ ์๊ทธ๋ ๋๊ธฐ
shutdown_event = tornado.locks.Event()
await shutdown_event.wait()
# ์ข
๋ฃ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ฆฌ
await app.db_manager.close()
if __name__ == "__main__":
# ๋น๋๊ธฐ ๋ฉ์ธ ์คํ
tornado.ioloop.IOLoop.current().run_sync(main)
โ ํน์ง:
- ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ผ์ด๋ฒ ํ์ฉ
- ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์ (MySQL, PostgreSQL, MongoDB)
- ์ฐ๊ฒฐ ํ๋ง ๊ตฌํ
- ๊ธด ์ฟผ๋ฆฌ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ผ๋ก ์ฒ๋ฆฌ
- ORM ์ฌ์ฉ ์ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ์ฃผ์
- ์์ ํ ํธ๋์ญ์ ์ฒ๋ฆฌ
- ๋ฏน์ค์ธ์ ํตํ DB ์ ๊ทผ ๊ฐ์ํ
- SQL ์ธ์ ์ ๋ฐฉ์ง
- ์๋ฌ ์ฒ๋ฆฌ์ ์์ธ ์ฒ๋ฆฌ
- ์ฐ๊ฒฐ ์ ๋ฆฌ์ ์์ ๊ด๋ฆฌ
- ํ์ ํํ ์ ํตํ ์ฝ๋ ์์ ์ฑ
- ๋น๋๊ธฐ ์ปจํ ์คํธ ๋งค๋์ ํ์ฉ
Tornado ์ ํ๋ฆฌ์ผ์ด์ ์์์ ์ธ์ฆ ๋ฐ ๋ณด์ ๊ตฌํ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.
import tornado.web
import tornado.ioloop
import tornado.escape
import jwt
import bcrypt
import secrets
import datetime
import re
import os
from typing import Dict, Any, Optional, Callable, Union, Awaitable, cast, TypeVar, Type
from functools import wraps
# ๋ณด์ ์ค์
SECURITY_CONFIG = {
'cookie_secret': os.environ.get('COOKIE_SECRET', 'insecure_default_cookie_secret'),
'jwt_secret': os.environ.get('JWT_SECRET', 'insecure_default_jwt_secret'),
'jwt_algorithm': 'HS256',
'jwt_expiry_days': 7,
'xsrf_cookies': True,
'debug': False,
'password_min_length': 8,
'rate_limit_per_second': 5
}
# ์ธ์ฆ ๋ฐ์ฝ๋ ์ดํฐ (ํจ์์ฉ)
def authenticated_async(method):
"""๋น๋๊ธฐ ์ธ์ฆ ๋ฐ์ฝ๋ ์ดํฐ"""
@wraps(method)
async def wrapper(self, *args, **kwargs):
if not self.current_user:
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
# Ajax ์์ฒญ์ธ ๊ฒฝ์ฐ JSON ์ค๋ฅ ๋ฐํ
self.set_header("Content-Type", "application/json")
self.set_status(401)
self.write({"status": "error", "message": "์ธ์ฆ์ด ํ์ํฉ๋๋ค"})
return
# ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
url = self.get_login_url()
if "?" not in url:
url += "?" + tornado.escape.url_escape(self.request.uri)
self.redirect(url)
return
# ์ธ์ฆ๋ ์ฌ์ฉ์
return await method(self, *args, **kwargs)
return wrapper
T = TypeVar('T', bound='BaseHandler')
# ์ธ์ฆ ๋ฐ์ฝ๋ ์ดํฐ (ํด๋์ค ๋ฉ์๋์ฉ)
def require_role(role: str) -> Callable[[Callable[[T, ...], Awaitable[None]]], Callable[[T, ...], Awaitable[None]]]:
"""ํน์ ์ญํ ํ์ ๋ฐ์ฝ๋ ์ดํฐ"""
def decorator(method: Callable[[T, ...], Awaitable[None]]) -> Callable[[T, ...], Awaitable[None]]:
@wraps(method)
async def wrapper(self: T, *args, **kwargs):
user = self.current_user
if not user:
raise tornado.web.HTTPError(401, "์ธ์ฆ์ด ํ์ํฉ๋๋ค")
user_roles = user.get('roles', [])
if role not in user_roles:
raise tornado.web.HTTPError(
403, f"์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค. ํ์ํ ์ญํ : {role}"
)
return await method(self, *args, **kwargs)
return wrapper
return decorator
# ๊ธฐ๋ณธ ํธ๋ค๋ฌ ํด๋์ค
class BaseHandler(tornado.web.RequestHandler):
"""์ธ์ฆ๊ณผ ๋ณด์ ๊ธฐ๋ฅ์ ํฌํจํ ๊ธฐ๋ณธ ํธ๋ค๋ฌ"""
def prepare(self) -> Optional[Awaitable[None]]:
"""๊ฐ ์์ฒญ ์ ํธ์ถ - ์๋ ์ ํ, IP ์ฐจ๋จ ๋ฑ ๊ตฌํ ๊ฐ๋ฅ"""
# ์์ฒญ ๋น์จ ์ ํ ๊ฒ์ฌ (์ค์ ๋ก๋ Redis ๋ฑ์ ์ฌ์ฉํ์ฌ ๊ตฌํ)
if self.is_rate_limited():
raise tornado.web.HTTPError(
429, "์์ฒญ์ด ๋๋ฌด ๋ง์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํ์ธ์."
)
return None
def is_rate_limited(self) -> bool:
"""์์ฒญ ๋น์จ ์ ํ ๊ฒ์ฌ (์ํ ๊ตฌํ)"""
# ์ค์ ํ๋ก๋์
์์๋ Redis์ ๊ฐ์ ๋ถ์ฐ ์บ์ ์ฌ์ฉ
return False
def set_default_headers(self) -> None:
"""๋ณด์ ํค๋ ์ค์ """
self.set_header("X-Content-Type-Options", "nosniff")
self.set_header("X-XSS-Protection", "1; mode=block")
self.set_header("X-Frame-Options", "DENY")
self.set_header("Content-Security-Policy",
"default-src 'self'; script-src 'self'; object-src 'none'")
self.set_header("Strict-Transport-Security",
"max-age=31536000; includeSubDomains")
def get_current_user(self) -> Optional[Dict[str, Any]]:
"""ํ์ฌ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ"""
# JWT ์ธ์ฆ ํ์ธ
jwt_token = self.get_jwt_token()
if jwt_token:
try:
user_data = jwt.decode(
jwt_token,
SECURITY_CONFIG['jwt_secret'],
algorithms=[SECURITY_CONFIG['jwt_algorithm']]
)
return user_data
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
# ์ฟ ํค ๊ธฐ๋ฐ ์ธ์ฆ ํ์ธ
user_id = self.get_secure_cookie("user_id")
if user_id:
# ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
# ์์ ๊ตฌํ
return {"id": int(user_id), "roles": ["user"]}
return None
def get_jwt_token(self) -> Optional[str]:
"""์์ฒญ์์ JWT ํ ํฐ ์ถ์ถ"""
auth_header = self.request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
return auth_header[7:] # "Bearer " ์ดํ์ ํ ํฐ ๋ฐํ
# ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์์ ํ ํฐ ํ์ธ
token = self.get_argument('token', None)
return token
def get_login_url(self) -> str:
"""๋ก๊ทธ์ธ URL ๋ฐํ"""
return "/auth/login"
# ์ฌ์ฉ์ ๊ณ์ ํธ๋ค๋ฌ
class UserAuthHandler(BaseHandler):
"""์ฌ์ฉ์ ์ธ์ฆ ์ฒ๋ฆฌ ํธ๋ค๋ฌ"""
async def prepare_user_data(self, username: str, password: str) -> Dict[str, Any]:
"""์ฌ์ฉ์ ์ ๋ณด ์ค๋น - ์ค์ ๋ก๋ DB์์ ์กฐํ"""
# ์ด ์์ ์์๋ ํ๋์ฝ๋ฉ๋ ์ฌ์ฉ์ ์ ๋ณด ์ฌ์ฉ
if username == "admin" and password == "admin123":
return {
"id": 1,
"username": "admin",
"email": "[email protected]",
"roles": ["admin", "user"]
}
elif username == "user" and password == "user123":
return {
"id": 2,
"username": "user",
"email": "[email protected]",
"roles": ["user"]
}
return {}
def generate_jwt_token(self, user_data: Dict[str, Any]) -> str:
"""JWT ํ ํฐ ์์ฑ"""
payload = {
"id": user_data["id"],
"username": user_data["username"],
"email": user_data["email"],
"roles": user_data["roles"],
"exp": datetime.datetime.utcnow() + datetime.timedelta(
days=SECURITY_CONFIG['jwt_expiry_days']
),
"iat": datetime.datetime.utcnow(),
"jti": secrets.token_hex(16) # ๊ณ ์ ํ ํฐ ID
}
token = jwt.encode(
payload,
SECURITY_CONFIG['jwt_secret'],
algorithm=SECURITY_CONFIG['jwt_algorithm']
)
return token
# ๋ก๊ทธ์ธ ํธ๋ค๋ฌ
class LoginHandler(UserAuthHandler):
"""์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ"""
async def get(self) -> None:
"""๋ก๊ทธ์ธ ํผ ์ ๊ณต"""
if self.current_user:
self.redirect("/")
return
self.write("""
<html>
<body>
<form method="post">
{% module xsrf_form_html() %}
<input type="text" name="username" placeholder="์ฌ์ฉ์๋ช
">
<input type="password" name="password" placeholder="๋น๋ฐ๋ฒํธ">
<input type="submit" value="๋ก๊ทธ์ธ">
</form>
</body>
</html>
""")
async def post(self) -> None:
"""๋ก๊ทธ์ธ ์ฒ๋ฆฌ"""
content_type = self.request.headers.get("Content-Type", "")
if "application/json" in content_type:
try:
data = tornado.escape.json_decode(self.request.body)
username = data.get("username", "")
password = data.get("password", "")
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
else:
username = self.get_argument("username", "")
password = self.get_argument("password", "")
if not username or not password:
raise tornado.web.HTTPError(400, "์ฌ์ฉ์๋ช
๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ชจ๋ ์
๋ ฅํ์ธ์")
# ์ฌ์ฉ์ ์ ๋ณด ํ์ธ
user_data = await self.prepare_user_data(username, password)
if not user_data:
raise tornado.web.HTTPError(401, "์๋ชป๋ ์ฌ์ฉ์๋ช
๋๋ ๋น๋ฐ๋ฒํธ")
# JWT ํ ํฐ ์์ฑ
token = self.generate_jwt_token(user_data)
# ์ธ์
์ฟ ํค ์ค์
self.set_secure_cookie("user_id", str(user_data["id"]))
# ์๋ต
if "application/json" in content_type:
self.write({
"status": "success",
"message": "๋ก๊ทธ์ธ ์ฑ๊ณต",
"token": token,
"user": {
"id": user_data["id"],
"username": user_data["username"],
"roles": user_data["roles"]
}
})
else:
self.redirect("/")
# ๋ก๊ทธ์์ ํธ๋ค๋ฌ
class LogoutHandler(BaseHandler):
"""์ฌ์ฉ์ ๋ก๊ทธ์์ ์ฒ๋ฆฌ"""
def get(self) -> None:
"""๋ก๊ทธ์์ ์ฒ๋ฆฌ"""
self.clear_cookie("user_id")
self.redirect("/auth/login")
# ์ฌ์ฉ์ ๋ฑ๋ก ํธ๋ค๋ฌ
class RegisterHandler(UserAuthHandler):
"""์ฌ์ฉ์ ๋ฑ๋ก ์ฒ๋ฆฌ"""
def get(self) -> None:
"""๋ฑ๋ก ํผ ์ ๊ณต"""
self.write("""
<html>
<body>
<form method="post">
{% module xsrf_form_html() %}
<input type="text" name="username" placeholder="์ฌ์ฉ์๋ช
">
<input type="email" name="email" placeholder="์ด๋ฉ์ผ">
<input type="password" name="password" placeholder="๋น๋ฐ๋ฒํธ">
<input type="password" name="confirm_password" placeholder="๋น๋ฐ๋ฒํธ ํ์ธ">
<input type="submit" value="๋ฑ๋ก">
</form>
</body>
</html>
""")
async def post(self) -> None:
"""์ฌ์ฉ์ ๋ฑ๋ก ์ฒ๋ฆฌ"""
content_type = self.request.headers.get("Content-Type", "")
if "application/json" in content_type:
try:
data = tornado.escape.json_decode(self.request.body)
username = data.get("username", "")
email = data.get("email", "")
password = data.get("password", "")
confirm_password = data.get("confirm_password", "")
except ValueError:
raise tornado.web.HTTPError(400, "์๋ชป๋ JSON ํ์")
else:
username = self.get_argument("username", "")
email = self.get_argument("email", "")
password = self.get_argument("password", "")
confirm_password = self.get_argument("confirm_password", "")
# ์
๋ ฅ ๊ฒ์ฆ
if not username or not email or not password:
raise tornado.web.HTTPError(400, "๋ชจ๋ ํ๋๋ฅผ ์
๋ ฅํ์ธ์")
if password != confirm_password:
raise tornado.web.HTTPError(400, "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค")
if len(password) < SECURITY_CONFIG['password_min_length']:
raise tornado.web.HTTPError(
400, f"๋น๋ฐ๋ฒํธ๋ ์ต์ {SECURITY_CONFIG['password_min_length']}์ ์ด์์ด์ด์ผ ํฉ๋๋ค"
)
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
raise tornado.web.HTTPError(400, "์ ํจํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํ์ธ์")
# ๋น๋ฐ๋ฒํธ ํด์ฑ
hashed_password = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt()
).decode('utf-8')
# ์ฌ์ฉ์ ์์ฑ ๋ก์ง (์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ)
# ์ฌ๊ธฐ์๋ ์์ฑ ์ฑ๊ณต์ผ๋ก ๊ฐ์
user_id = 3 # ์์ ID
# ์๋ต
if "application/json" in content_type:
self.write({
"status": "success",
"message": "์ฌ์ฉ์ ๋ฑ๋ก ์ฑ๊ณต",
"user_id": user_id
})
else:
self.redirect("/auth/login")
# ๋ณดํธ๋ API ํธ๋ค๋ฌ
class ProtectedHandler(BaseHandler):
"""์ธ์ฆ์ด ํ์ํ ๋ณดํธ๋ ๋ฆฌ์์ค"""
@authenticated_async
async def get(self) -> None:
"""์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ"""
user = cast(Dict[str, Any], self.current_user)
self.write({
"status": "success",
"message": "๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ์ต๋๋ค",
"user": {
"id": user.get("id"),
"username": user.get("username", ""),
"roles": user.get("roles", [])
}
})
# ๊ด๋ฆฌ์ ์ ์ฉ API ํธ๋ค๋ฌ
class AdminHandler(BaseHandler):
"""๊ด๋ฆฌ์ ์ญํ ์ด ํ์ํ ๋ฆฌ์์ค"""
@authenticated_async
@require_role("admin")
async def get(self) -> None:
"""๊ด๋ฆฌ์๋ง ์ ๊ทผ ๊ฐ๋ฅ"""
user = cast(Dict[str, Any], self.current_user)
self.write({
"status": "success",
"message": "๊ด๋ฆฌ์ ๋ฆฌ์์ค์ ์ ๊ทผํ์ต๋๋ค",
"user": {
"id": user.get("id"),
"username": user.get("username", ""),
"roles": user.get("roles", [])
}
})
# CSRF ์์
class CSRFExampleHandler(BaseHandler):
"""CSRF ๋ณดํธ ์์ """
def get(self) -> None:
"""CSRF ํ ํฐ์ด ํฌํจ๋ ํผ ์ ๊ณต"""
self.write("""
<html>
<body>
<h1>CSRF ๋ณดํธ ์์ </h1>
<form method="post">
{% module xsrf_form_html() %}
<input type="text" name="data" placeholder="๋ฐ์ดํฐ">
<input type="submit" value="์ ์ถ">
</form>
</body>
</html>
""")
def post(self) -> None:
"""CSRF ํ ํฐ ๊ฒ์ฆ ํ ์ฒ๋ฆฌ"""
data = self.get_argument("data", "")
self.write({
"status": "success",
"message": "CSRF ๋ณดํธ๋ ์์ฒญ ์ฒ๋ฆฌ ์ฑ๊ณต",
"data": data
})
# ๋ณด์ ์ ํ๋ฆฌ์ผ์ด์
์ค์
def make_secure_app() -> tornado.web.Application:
"""๋ณด์ ์ค์ ์ด ์ ์ฉ๋ ์ ํ๋ฆฌ์ผ์ด์
์์ฑ"""
return tornado.web.Application([
(r"/auth/login", LoginHandler),
(r"/auth/logout", LogoutHandler),
(r"/auth/register", RegisterHandler),
(r"/api/protected", ProtectedHandler),
(r"/api/admin", AdminHandler),
(r"/csrf-example", CSRFExampleHandler),
],
cookie_secret=SECURITY_CONFIG['cookie_secret'],
xsrf_cookies=SECURITY_CONFIG['xsrf_cookies'],
debug=SECURITY_CONFIG['debug']
)
if __name__ == "__main__":
# ๋ณด์ ์ ํ๋ฆฌ์ผ์ด์
์์
app = make_secure_app()
port = 8888
app.listen(port)
print(f"๋ณด์ ์๋ฒ๊ฐ http://localhost:{port} ์์ ์์๋์์ต๋๋ค")
tornado.ioloop.IOLoop.current().start()
โ ํน์ง:
- JWT ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐ ํ ํฐ ๊ด๋ฆฌ
- ์ฟ ํค ๊ธฐ๋ฐ ์ธ์ ์ธ์ฆ
- ๋น๋ฐ๋ฒํธ ํด์ฑ ๋ฐ ๋ณด์ ์ ์ฅ
- ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด(RBAC)
- CSRF ๋ณดํธ ๋ฉ์ปค๋์ฆ
- ๋ณด์ HTTP ํค๋ ์ค์
- ํ์ ์์ ํ ๋ฐ์ฝ๋ ์ดํฐ ํจํด
- ์ ๋ ฅ ๊ฒ์ฆ ๋ฐ ์ด์ค์ผ์ดํ
- ์๋ ์ ํ ๋ฐ ๋ถํ ๋ฐฉ์ง
- ์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ๋ณด์ ๋ก๊น
- ํ์ ํํ ์ ํตํ ์ฝ๋ ์์ ์ฑ
Tornado์ ๊ณ ๊ธ ๊ธฐ๋ฅ ๋ฐ ํ๋ก๋์ ๋ฐฐํฌ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.
import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.process
import tornado
import signal
import os
import logging
import time
import socket
from typing import Dict, Any, List, Optional, Union, Awaitable
# ๋ก๊น
์ค์
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='/tmp/tornado-app.log'
)
logger = logging.getLogger('tornado')
# ํธ๋ค๋ฌ
class MainHandler(tornado.web.RequestHandler):
"""๊ธฐ๋ณธ ํธ๋ค๋ฌ"""
def get(self) -> None:
"""GET ์์ฒญ ์ฒ๋ฆฌ"""
logger.info(f"๋ฉ์ธ ํ์ด์ง ์์ฒญ: {self.request.remote_ip}")
self.write({
"status": "success",
"message": "Tornado ์๋ฒ ์คํ ์ค",
"instance": os.getpid(),
"hostname": socket.gethostname()
})
class HealthCheckHandler(tornado.web.RequestHandler):
"""ํฌ์ค ์ฒดํฌ ํธ๋ค๋ฌ"""
def get(self) -> None:
"""์ํ ํ์ธ ์๋ํฌ์ธํธ"""
self.write({
"status": "healthy",
"timestamp": time.time(),
"uptime": time.time() - start_time
})
# ํจ๋ค์ด๋ ์ปค์คํ
์ ํ๋ฆฌ์ผ์ด์
class TornadoApplication(tornado.web.Application):
"""ํ์ฅ๋ Tornado ์ ํ๋ฆฌ์ผ์ด์
"""
def __init__(self, handlers: List = None, default_host: str = None, **settings: Any) -> None:
"""์ด๊ธฐํ"""
super(TornadoApplication, self).__init__(handlers or [], default_host, **settings)
# ์ ํ๋ฆฌ์ผ์ด์
๋ฉํธ๋ฆญ
self.requests_count = 0
self.start_time = time.time()
def log_request(self, handler: tornado.web.RequestHandler) -> None:
"""์์ฒญ ๋ก๊น
ํ์ฅ"""
super().log_request(handler)
self.requests_count += 1
# ์๋ฒ ๊ตฌ์ฑ ๋ฐ ์์
def start_server(port: int, num_processes: int = 0) -> None:
"""Tornado ์๋ฒ ์์
Args:
port: ์๋ฒ ํฌํธ
num_processes: ํ๋ก์ธ์ค ์ (0์ CPU ์ฝ์ด ์๋งํผ)
"""
app = TornadoApplication([
(r"/", MainHandler),
(r"/health", HealthCheckHandler),
],
debug=False,
compress_response=True)
# HTTP ์๋ฒ ์์ฑ
server = tornado.httpserver.HTTPServer(app)
# ๋ค์ค ํ๋ก์ธ์ค ๋ชจ๋
if num_processes:
# ์ง์ ๋ ์์ ํ๋ก์ธ์ค ์ฌ์ฉ
server.bind(port)
server.start(num_processes)
logger.info(f"์๋ฒ๊ฐ {num_processes}๊ฐ ํ๋ก์ธ์ค๋ก ์์๋จ (ํฌํธ: {port})")
else:
# ๋จ์ผ ํ๋ก์ธ์ค ๋ชจ๋
server.listen(port)
logger.info(f"์๋ฒ๊ฐ ๋จ์ผ ํ๋ก์ธ์ค๋ก ์์๋จ (ํฌํธ: {port})")
# ์๊ทธ๋ ํธ๋ค๋ฌ ์ค์
def handle_signal(sig: int, frame) -> None:
"""์๊ทธ๋ ์ฒ๋ฆฌ"""
logger.warning(f"์๊ทธ๋ {sig} ์์ , ์ข
๋ฃ ์ค...")
tornado.ioloop.IOLoop.instance().add_callback_from_signal(shutdown)
def shutdown() -> None:
"""์ข
๋ฃ ์ฒ๋ฆฌ"""
logger.info("์๋ฒ ์ข
๋ฃ ์ค...")
# ํ์ฑ ์ฐ๊ฒฐ ์ข
๋ฃ๊น์ง ๋๊ธฐ
server.stop()
io_loop = tornado.ioloop.IOLoop.instance()
deadline = time.time() + 5 # ์ต๋ 5์ด ๋๊ธฐ
def stop_loop() -> None:
now = time.time()
if now < deadline and (io_loop._callbacks or io_loop._timeouts):
io_loop.add_timeout(now + 0.1, stop_loop)
else:
io_loop.stop()
logger.info("์๋ฒ๊ฐ ์ข
๋ฃ๋์์ต๋๋ค")
stop_loop()
# ์๊ทธ๋ ํธ๋ค๋ฌ ๋ฑ๋ก
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
# ์์ ๋ก๊ทธ
logger.info(f"Tornado ์๋ฒ๊ฐ ์คํ ์ค์
๋๋ค (ํฌํธ: {port})")
logger.info(f"ํ๋ก์ธ์ค ID: {os.getpid()}")
# I/O ๋ฃจํ ์์
tornado.ioloop.IOLoop.current().start()
logger.info("Tornado ์๋ฒ๊ฐ ์ข
๋ฃ๋์์ต๋๋ค")
# ํ๋ก๋์
ํ๊ฒฝ ๋ณ์ ์ค์
def configure_from_environment() -> Dict[str, Any]:
"""ํ๊ฒฝ ๋ณ์์์ ์ค์ ๋ก๋"""
config = {
'port': int(os.environ.get('PORT', 8888)),
'num_processes': int(os.environ.get('NUM_PROCESSES', 0)),
'debug': os.environ.get('DEBUG', 'false').lower() == 'true',
'log_level': os.environ.get('LOG_LEVEL', 'info').upper(),
'cookie_secret': os.environ.get('COOKIE_SECRET', 'default_cookie_secret'),
}
# ๋ก๊ทธ ๋ ๋ฒจ ์ค์
log_level = getattr(logging, config['log_level'], logging.INFO)
logger.setLevel(log_level)
return config
if __name__ == "__main__":
# ์์ ์๊ฐ ๊ธฐ๋ก
start_time = time.time()
# ํ๊ฒฝ ๋ณ์์์ ์ค์ ๋ก๋
config = configure_from_environment()
# ์๋ฒ ์์
start_server(config['port'], config['num_processes'])
โ ํน์ง:
- ๋ฉํฐ ํ๋ก์ธ์ค ๋ชจ๋ ์ง์
- ์ ์ ์ข ๋ฃ ์ฒ๋ฆฌ
- ์ํ ๋ชจ๋ํฐ๋ง ์๋ํฌ์ธํธ
- ํ๊ฒฝ ๋ณ์ ๊ธฐ๋ฐ ์ค์
- ์๋ต ์์ถ
- ๊ณ ๊ธ ๋ก๊น ์ค์
- ์๊ทธ๋ ์ฒ๋ฆฌ
- ๋ฉํธ๋ฆญ ์์ง
- ์ ํ๋ฆฌ์ผ์ด์ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ
- ํ์ฅ์ฑ ์๋ ์ํคํ ์ฒ
โ ๋ชจ๋ฒ ์ฌ๋ก:
-
๋น๋๊ธฐ ์ฝ๋ ์ต์ ํ
- async/await ํค์๋ ํ์ฉ
- ๋ธ๋กํน I/O ์ฐ์ฐ ํผํ๊ธฐ
- ๋ณ๋ ฌ ์ฒ๋ฆฌ์ asyncio.gather ํ์ฉ
- ์ ๋๋ ์ดํฐ ๊ธฐ๋ฐ ์ฝ๋ฃจํด์ ์ง์ํ๊ณ async/await ์ฌ์ฉ
-
์๋ฌ ์ฒ๋ฆฌ ๊ตฌํ
- ์ผ๊ด๋ ์ค๋ฅ ์๋ต ํ์ ์ฌ์ฉ
- ์์ธ ์ ํ์ ๋ฐ๋ฅธ ์ ์ ํ HTTP ์ํ ์ฝ๋ ๋ฐํ
- ์๊ธฐ์น ์์ ์์ธ์ ๋ํ ๋ก๊น ๊ตฌํ
- ๋๋ฒ๊ทธ ๋ชจ๋์์๋ง ์์ธํ ์ค๋ฅ ์ ๋ณด ๋ ธ์ถ
-
๋ณด์ ์ค์ ํ์ธ
- HTTPS ํ์ฑํ (ํ๋ก๋์ ํ๊ฒฝ ํ์)
- ๋ณด์ HTTP ํค๋ ์ค์
- cookie_secret ์์ ํ๊ฒ ๊ด๋ฆฌ
- xsrf_cookies ํ์ฑํ
- ์ ๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฐ ์ด์ค์ผ์ดํ ์ฒ ์ ํ
-
๋ก๊น ์ค์
- ๊ตฌ์กฐํ๋ ๋ก๊น ์ฌ์ฉ (JSON ํ์ ๊ถ์ฅ)
- ๋ก๊ทธ ํ์ ์ค์
- ์ ์ ํ ๋ก๊ทธ ๋ ๋ฒจ ์ฌ์ฉ
- ๋ฏผ๊ฐํ ์ ๋ณด ๋ง์คํน
-
์บ์ฑ ์ ๋ต
- ๋ฉ๋ชจ๋ฆฌ ์บ์ ๋๋ Redis ํ์ฉ
- ์ ์ ์ปจํ ์ธ ์บ์ฑ
- API ์๋ต ์บ์ฑ
- ์บ์ ๋ฌดํจํ ์ ๋ต ์๋ฆฝ
-
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
- ๋น๋๊ธฐ ๋๋ผ์ด๋ฒ ์ฌ์ฉ (aiomysql, aiopg, motor ๋ฑ)
- ์ฐ๊ฒฐ ํ๋ง ๊ตฌํ
- ๊ธด ์ฟผ๋ฆฌ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ผ๋ก ์ฒ๋ฆฌ
- ORM ์ฌ์ฉ ์ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ์ฃผ์
-
ํ ์คํธ ์์ฑ
- ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ ๊ตฌํ
- ๋น๋๊ธฐ ์ฝ๋ ํ ์คํธ๋ฅผ ์ํ ๋๊ตฌ ํ์ฉ
- ๋ชจํน๊ณผ ํจ์น๋ฅผ ํตํ ์์กด์ฑ ๊ฒฉ๋ฆฌ
- CI/CD ํ์ดํ๋ผ์ธ์ ํ ์คํธ ํตํฉ
-
์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- ์๋ต ์๊ฐ ๋ฐ ์ฒ๋ฆฌ๋ ์ธก์
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
- ๋ณ๋ชฉ ํ์ ์๋ณ ๋ฐ ํด๊ฒฐ
- APM(Application Performance Monitoring) ๋๊ตฌ ํ์ฉ
-
ํ์ฅ์ฑ ์ค๊ณ
- ๋ฉํฐ ํ๋ก์ธ์ค ์คํ ๊ตฌ์ฑ
- ๋ฌด์ํ(Stateless) ์ค๊ณ๋ก ์ํ ํ์ฅ ์ฉ์ดํ๊ฒ
- ๋ก๋ ๋ฐธ๋ฐ์ ๋ค์์ ์คํ
- ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ณ ๋ ค
-
๋ฐฐํฌ ์ ๋ต
- Docker ์ปจํ ์ด๋ํ
- Nginx ๋๋ HAProxy์ ํจ๊ป ์ฌ์ฉ
- Supervisor ๋๋ systemd๋ก ํ๋ก์ธ์ค ๊ด๋ฆฌ
- ๋ธ๋ฃจ-๊ทธ๋ฆฐ ๋๋ ์นด๋๋ฆฌ ๋ฐฐํฌ ๊ณ ๋ ค