-
Notifications
You must be signed in to change notification settings - Fork 0
KR_Tornado
somaz edited this page Apr 23, 2025
·
5 revisions
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": "user1@example.com"}
return None
def get_all_users(self) -> List[Dict[str, Any]]:
"""모든 사용자 조회 (예시 구현)"""
# 샘플 데이터
return [
{"id": "1", "username": "user1", "email": "user1@example.com"},
{"id": "2", "username": "user2", "email": "user2@example.com"}
]
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": "admin@example.com",
"roles": ["admin", "user"]
}
elif username == "user" and password == "user123":
return {
"id": 2,
"username": "user",
"email": "user@example.com",
"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로 프로세스 관리
- 블루-그린 또는 카나리 배포 고려