KR_aiohttp - somaz94/python-study GitHub Wiki

aiohttp


1๏ธโƒฃ aiohttp ๊ธฐ์ดˆ

aiohttp๋Š” Python์—์„œ ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ๋ฐ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค. asyncio๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฉฐ, ๊ณ ์„ฑ๋Šฅ ๋น„๋™๊ธฐ ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

์ฃผ์š” ํŠน์ง•:

  • ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ๋ฐ ์„œ๋ฒ„ ์ง€์›
  • WebSocket ์ง€์›
  • ๋ฏธ๋“ค์›จ์–ด ๊ธฐ๋Šฅ ์ œ๊ณต
  • ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ
  • ์‹ ํ˜ธ ์ฒ˜๋ฆฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜
  • ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ

์„ค์น˜:

pip install aiohttp

๊ธฐ๋ณธ ์„œ๋ฒ„ ์˜ˆ์ œ:

import asyncio
from aiohttp import web

async def handle(request):
    name = request.match_info.get('name', "Anonymous")
    text = f"์•ˆ๋…•ํ•˜์„ธ์š”, {name}๋‹˜!"
    return web.Response(text=text)

async def main():
    app = web.Application()
    app.add_routes([
        web.get('/', handle),
        web.get('/{name}', handle)
    ])
    
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, 'localhost', 8080)
    await site.start()
    
    print("์„œ๋ฒ„ ์‹œ์ž‘: http://localhost:8080")
    # ์„œ๋ฒ„๊ฐ€ ๊ณ„์† ์‹คํ–‰๋˜๋„๋ก ์œ ์ง€
    await asyncio.Event().wait()

if __name__ == '__main__':
    asyncio.run(main())

๋ผ์šฐํŒ… ์‹œ์Šคํ…œ:

aiohttp๋Š” URL ๋ผ์šฐํŒ…์„ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•œ๋‹ค.

app.add_routes([
    web.get('/users', get_users),
    web.post('/users', create_user),
    web.get('/users/{id}', get_user),
    web.put('/users/{id}', update_user),
    web.delete('/users/{id}', delete_user),
])

# ๋˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‚ฌ์šฉ
routes = web.RouteTableDef()

@routes.get('/')
async def hello(request):
    return web.Response(text="Hello, world")

@routes.view('/users/{id}')
class UserView(web.View):
    async def get(self):
        return web.Response(text=f"์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ")
    
    async def put(self):
        return web.Response(text=f"์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ")

app.add_routes(routes)

์š”์ฒญ ๋ฐ ์‘๋‹ต ์ฒ˜๋ฆฌ:

aiohttp๋Š” ๋‹ค์–‘ํ•œ ํ˜•์‹์˜ ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

async def handle_json(request):
    # JSON ์š”์ฒญ ์ฒ˜๋ฆฌ
    data = await request.json()
    
    # JSON ์‘๋‹ต ๋ฐ˜ํ™˜
    return web.json_response({
        'status': 'success',
        'data': data
    })

async def handle_form(request):
    # ํผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    data = await request.post()
    name = data.get('name')
    
    # ํ…œํ”Œ๋ฆฟ ์‘๋‹ต
    return web.Response(
        text=f"<html><body><h1>์•ˆ๋…•ํ•˜์„ธ์š”, {name}๋‹˜!</h1></body></html>",
        content_type='text/html'
    )

async def handle_file(request):
    # ํŒŒ์ผ ์‘๋‹ต
    return web.FileResponse('/path/to/file.pdf')

โœ… ํŠน์ง•:

  • ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด
  • ์œ ์—ฐํ•œ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ
  • ๋‹ค์–‘ํ•œ ์‘๋‹ต ํ˜•์‹ ์ง€์›
  • ๋ฏธ๋“ค์›จ์–ด ์ฒด์ธ ๊ตฌ์„ฑ


2๏ธโƒฃ aiohttp ํด๋ผ์ด์–ธํŠธ

aiohttp๋Š” ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ ์—ฌ๋Ÿฌ ์›น ์š”์ฒญ์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•:

import aiohttp
import asyncio

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            if response.status == 200:
                return await response.text()
            return None

async def main():
    result = await fetch('https://python.org')
    print(f"๊ฒฐ๊ณผ ๊ธธ์ด: {len(result) if result else 'N/A'}")

asyncio.run(main())

๋‹ค์–‘ํ•œ HTTP ๋ฉ”์„œ๋“œ:

aiohttp ํด๋ผ์ด์–ธํŠธ๋Š” ๋ชจ๋“  ํ‘œ์ค€ HTTP ๋ฉ”์„œ๋“œ๋ฅผ ์ง€์›ํ•œ๋‹ค.

async def main():
    async with aiohttp.ClientSession() as session:
        # GET ์š”์ฒญ
        async with session.get('https://api.example.com/users') as resp:
            users = await resp.json()
        
        # POST ์š”์ฒญ
        user_data = {'name': 'ํ™๊ธธ๋™', 'email': '[email protected]'}
        async with session.post('https://api.example.com/users', json=user_data) as resp:
            result = await resp.json()
        
        # PUT ์š”์ฒญ
        update_data = {'name': '๊น€๊ธธ๋™'}
        async with session.put('https://api.example.com/users/1', json=update_data) as resp:
            updated = await resp.json()
        
        # DELETE ์š”์ฒญ
        async with session.delete('https://api.example.com/users/1') as resp:
            if resp.status == 204:
                print("์‚ฌ์šฉ์ž๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")

๋™์‹œ ์š”์ฒญ ์ฒ˜๋ฆฌ:

์—ฌ๋Ÿฌ ์š”์ฒญ์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์€ aiohttp์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด๋‹ค.

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            tasks.append(fetch_url(session, url))
        results = await asyncio.gather(*tasks)
        return results

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        'https://python.org',
        'https://github.com',
        'https://stackoverflow.com'
    ]
    results = await fetch_all(urls)
    for url, html in zip(urls, results):
        print(f"{url}: {len(html)} ๋ฐ”์ดํŠธ")

asyncio.run(main())

๊ณ ๊ธ‰ ํด๋ผ์ด์–ธํŠธ ๊ธฐ๋Šฅ:

1. ์š”์ฒญ ํƒ€์ž„์•„์›ƒ ์„ค์ •:

async with session.get('https://slow-api.example.com', timeout=aiohttp.ClientTimeout(total=5)) as resp:
    # 5์ดˆ ์ด๋‚ด์— ์‘๋‹ต์ด ์˜ค์ง€ ์•Š์œผ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
    data = await resp.json()

2. ์ฟ ํ‚ค ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ:

# ์ฟ ํ‚ค ์ €์žฅ ๋ฐ ์ „์†ก
jar = aiohttp.CookieJar()
async with aiohttp.ClientSession(cookie_jar=jar) as session:
    await session.get('https://auth.example.com/login')
    # ์ด์ œ jar์— ์ธ์ฆ ์ฟ ํ‚ค๊ฐ€ ์ €์žฅ๋จ
    
    # ๋™์ผํ•œ ์„ธ์…˜์—์„œ ์š”์ฒญํ•˜๋ฉด ์ฟ ํ‚ค๊ฐ€ ์ž๋™์œผ๋กœ ์ „์†ก๋จ
    await session.get('https://auth.example.com/profile')

3. ์š”์ฒญ ํ—ค๋” ์„ค์ •:

headers = {
    'User-Agent': 'My Custom User Agent',
    'Authorization': 'Bearer token123',
    'Content-Type': 'application/json'
}

async with session.get('https://api.example.com', headers=headers) as resp:
    data = await resp.json()

4. ํ”„๋ก์‹œ ์„ค์ •:

async with session.get('https://example.com', proxy='http://proxy.example.com:8080') as resp:
    data = await resp.text()

5. SSL ์ธ์ฆ์„œ ๊ฒ€์ฆ:

# SSL ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™” (๋ณด์•ˆ ์œ„ํ—˜, ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ)
ssl_context = False

# ๋˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ์ธ์ฆ์„œ ์‚ฌ์šฉ
import ssl
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain('client.crt', 'client.key')

async with session.get('https://secure.example.com', ssl=ssl_context) as resp:
    data = await resp.text()

โœ… ํŠน์ง•:

  • ์„ธ์…˜ ๊ด€๋ฆฌ ๋ฐ ์žฌ์‚ฌ์šฉ
  • ๋น„๋™๊ธฐ ์š”์ฒญ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
  • ํƒ€์ž„์•„์›ƒ ๋ฐ ์žฌ์‹œ๋„ ์„ค์ •
  • ์š”์ฒญ/์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ
  • ๋‹ค์–‘ํ•œ ์ธ์ฆ ๋ฐฉ์‹ ์ง€์›


3๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด์™€ ํ•ธ๋“ค๋Ÿฌ

aiohttp๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ ์ „ํ›„์— ๊ณตํ†ต ๋กœ์ง์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ง€์›ํ•œ๋‹ค.

@web.middleware
async def error_middleware(request, handler):
    try:
        response = await handler(request)
        return response
    except web.HTTPException as ex:
        return web.json_response(
            {'error': str(ex)},
            status=ex.status
        )
    except Exception as ex:
        return web.json_response(
            {'error': 'Internal Server Error'},
            status=500
        )

async def handle_404(request):
    return web.json_response(
        {'error': 'Not Found'},
        status=404
    )

app = web.Application(middlewares=[error_middleware])
app.router.add_routes([
    web.get('/', hello),
    web.get('/users/{id}', get_user)
])
app.router.set_error_handler(404, handle_404)

โœ… ํŠน์ง•:

  • ์ „์—ญ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  • ์ปค์Šคํ…€ ๋ฏธ๋“ค์›จ์–ด ์ง€์›
  • ๋ผ์šฐํŒ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  • ์š”์ฒญ/์‘๋‹ต ๋ณ€ํ™˜
  • ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ์ œ์–ด


4๏ธโƒฃ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

import aiopg
from aiohttp import web

async def init_pg(app):
    conf = app['config']['postgres']
    engine = await aiopg.create_pool(
        database=conf['database'],
        user=conf['user'],
        password=conf['password'],
        host=conf['host'],
        port=conf['port'],
        minsize=1,
        maxsize=5
    )
    app['db'] = engine

async def close_pg(app):
    app['db'].close()
    await app['db'].wait_closed()

class UserHandler:
    async def get_user(self, request):
        user_id = request.match_info['id']
        async with request.app['db'].acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute(
                    'SELECT * FROM users WHERE id = %s',
                    (user_id,)
                )
                user = await cur.fetchone()
                if not user:
                    raise web.HTTPNotFound()
                return web.json_response({
                    'id': user[0],
                    'name': user[1],
                    'email': user[2]
                })

app = web.Application()
app.on_startup.append(init_pg)
app.on_cleanup.append(close_pg)

โœ… ํŠน์ง•:

  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
  • ์ปค๋„ฅ์…˜ ํ’€๋ง
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ
  • ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ
  • ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ


5๏ธโƒฃ WebSocket ๊ตฌํ˜„

aiohttp๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ์ธก ๋ชจ๋‘์—์„œ WebSocket ์—ฐ๊ฒฐ์„ ์ง€์›ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

์„œ๋ฒ„ ์ธก WebSocket:

from aiohttp import web
import json

async def websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    
    # ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์ „์†ก
    await ws.send_str(json.dumps({'action': 'connect', 'status': 'success'}))
    
    try:
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                data = json.loads(msg.data)
                print(f"๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : {data}")
                
                # ์—์ฝ” ์‘๋‹ต
                await ws.send_str(json.dumps({
                    'action': 'echo',
                    'data': data
                }))
                
            elif msg.type == web.WSMsgType.ERROR:
                print(f"WebSocket ์˜ค๋ฅ˜: {ws.exception()}")
    finally:
        # ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ ์ž‘์—…
        print("WebSocket ์—ฐ๊ฒฐ ์ข…๋ฃŒ")
    
    return ws

app = web.Application()
app.add_routes([web.get('/ws', websocket_handler)])

if __name__ == '__main__':
    web.run_app(app)

ํด๋ผ์ด์–ธํŠธ ์ธก WebSocket:

import aiohttp
import asyncio
import json

async def websocket_client():
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect('ws://localhost:8080/ws') as ws:
            # ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
            async for msg in ws:
                if msg.type == aiohttp.WSMsgType.TEXT:
                    data = json.loads(msg.data)
                    print(f"์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์ˆ˜์‹ : {data}")
                    
                    # ๋ฉ”์‹œ์ง€ ์ „์†ก
                    await ws.send_str(json.dumps({
                        'message': '์•ˆ๋…•ํ•˜์„ธ์š”, WebSocket!',
                        'timestamp': asyncio.get_event_loop().time()
                    }))
                    
                elif msg.type == aiohttp.WSMsgType.CLOSED:
                    break
                elif msg.type == aiohttp.WSMsgType.ERROR:
                    break

asyncio.run(websocket_client())

์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ˆ์ œ:

# server.py
from aiohttp import web
import json

# ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ €์žฅ
connected_clients = []

async def chat_websocket(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    
    # ์ƒˆ ํด๋ผ์ด์–ธํŠธ ์ถ”๊ฐ€
    connected_clients.append(ws)
    client_id = len(connected_clients)
    
    # ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ƒˆ ์‚ฌ์šฉ์ž ์ ‘์† ์•Œ๋ฆผ
    for client in connected_clients:
        if client != ws:
            await client.send_str(json.dumps({
                'action': 'user_joined',
                'user_id': client_id
            }))
    
    try:
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                data = json.loads(msg.data)
                
                # ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
                for client in connected_clients:
                    await client.send_str(json.dumps({
                        'action': 'chat',
                        'user_id': client_id,
                        'message': data.get('message', '')
                    }))
                    
            elif msg.type == web.WSMsgType.ERROR:
                print(f"WebSocket ์˜ค๋ฅ˜: {ws.exception()}")
    finally:
        # ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ํด๋ผ์ด์–ธํŠธ ์ œ๊ฑฐ
        connected_clients.remove(ws)
        # ์‚ฌ์šฉ์ž ํ‡ด์žฅ ์•Œ๋ฆผ
        for client in connected_clients:
            await client.send_str(json.dumps({
                'action': 'user_left',
                'user_id': client_id
            }))
    
    return ws

app = web.Application()
app.add_routes([web.get('/chat', chat_websocket)])

if __name__ == '__main__':
    web.run_app(app)

WebSocket ์ด๋ฒคํŠธ ๊ด€๋ฆฌ:

WebSocket ์—ฐ๊ฒฐ์˜ ๋‹ค์–‘ํ•œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

async def websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    
    # ์—ฐ๊ฒฐ ์‹œ์ž‘ ์ด๋ฒคํŠธ
    print("WebSocket ์—ฐ๊ฒฐ ์‹œ์ž‘")
    
    try:
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                # ํ…์ŠคํŠธ ๋ฉ”์‹œ์ง€ ์ด๋ฒคํŠธ
                print(f"ํ…์ŠคํŠธ ์ˆ˜์‹ : {msg.data}")
                await ws.send_str(f"์—์ฝ”: {msg.data}")
                
            elif msg.type == web.WSMsgType.BINARY:
                # ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฉ”์‹œ์ง€ ์ด๋ฒคํŠธ
                print(f"๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ : {len(msg.data)} ๋ฐ”์ดํŠธ")
                await ws.send_bytes(msg.data)
                
            elif msg.type == web.WSMsgType.PING:
                # Ping ์ด๋ฒคํŠธ
                await ws.pong()
                
            elif msg.type == web.WSMsgType.CLOSE:
                # ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์š”์ฒญ
                await ws.close()
                
            elif msg.type == web.WSMsgType.ERROR:
                # ์˜ค๋ฅ˜ ์ด๋ฒคํŠธ
                print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ws.exception()}")
    finally:
        # ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์ด๋ฒคํŠธ
        print("WebSocket ์—ฐ๊ฒฐ ์ข…๋ฃŒ")
    
    return ws

โœ… ํŠน์ง•:

  • ์–‘๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 
  • ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋ฉ”์‹œ์ง€ ๊ตํ™˜
  • JSON ์ง๋ ฌํ™”๋œ ๋ฉ”์‹œ์ง€
  • ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ๋ธŒ๋กœ๋“œ์บ์ŠคํŒ… ์ง€์›


6๏ธโƒฃ ์‹ค์šฉ์ ์ธ ์˜ˆ์ œ

aiohttp๋ฅผ ์‚ฌ์šฉํ•œ ์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„ ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์ž.

REST API ์„œ๋น„์Šค:

from aiohttp import web
import json

class RestAPI:
    def __init__(self):
        self.items = {}
    
    async def get_items(self, request):
        return web.json_response(list(self.items.values()))
    
    async def get_item(self, request):
        item_id = request.match_info['id']
        if item_id not in self.items:
            raise web.HTTPNotFound()
        return web.json_response(self.items[item_id])
    
    async def create_item(self, request):
        data = await request.json()
        item_id = str(len(self.items) + 1)
        self.items[item_id] = {
            'id': item_id,
            **data
        }
        return web.json_response(
            self.items[item_id],
            status=201
        )
    
    async def update_item(self, request):
        item_id = request.match_info['id']
        if item_id not in self.items:
            raise web.HTTPNotFound()
        
        data = await request.json()
        self.items[item_id].update(data)
        return web.json_response(self.items[item_id])

api = RestAPI()
app.router.add_get('/items', api.get_items)
app.router.add_get('/items/{id}', api.get_item)
app.router.add_post('/items', api.create_item)
app.router.add_put('/items/{id}', api.update_item)

ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ:

import os

async def handle_upload(request):
    reader = await request.multipart()
    
    # ํŒŒ์ผ ํ•„๋“œ ์ฒ˜๋ฆฌ
    field = await reader.next()
    filename = field.filename
    
    # ํŒŒ์ผ ์ €์žฅ
    size = 0
    with open(os.path.join('uploads', filename), 'wb') as f:
        while True:
            chunk = await field.read_chunk()
            if not chunk:
                break
            size += len(chunk)
            f.write(chunk)
    
    return web.json_response({
        'filename': filename,
        'size': size
    })

app.router.add_post('/upload', handle_upload)

์„ธ์…˜ ๊ด€๋ฆฌ:

import aiohttp_session
from aiohttp_session import setup, get_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import base64

async def init_app():
    app = web.Application()
    
    # ์„ธ์…˜ ์„ค์ •
    secret_key = base64.urlsafe_b64decode(
        'your-secret-key=='
    )
    setup(app, EncryptedCookieStorage(secret_key))
    
    return app

async def login(request):
    session = await get_session(request)
    data = await request.json()
    
    # ์ธ์ฆ ๋กœ์ง
    user_id = authenticate(data)
    if user_id:
        session['user_id'] = user_id
        return web.json_response({'status': 'logged in'})
    
    raise web.HTTPUnauthorized()

async def logout(request):
    session = await get_session(request)
    session.invalidate()
    return web.json_response({'status': 'logged out'})

โœ… ํŠน์ง•:

  • ๋น„๋™๊ธฐ ์ฝ”๋“œ ์ตœ์ ํ™”
  • ์—ฐ๊ฒฐ ํ’€๋ง ํ™œ์šฉ
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์„ธ์…˜ ๊ด€๋ฆฌ
  • ๋ณด์•ˆ ์„ค์ •
  • ๋กœ๊น… ๊ตฌํ˜„
  • ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
  • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง


7๏ธโƒฃ aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ…Œ์ŠคํŠธ

aiohttp๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ:

aiohttp๋Š” ๋‚ด์žฅ๋œ ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ†ตํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

import pytest
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer

async def hello(request):
    return web.Response(text="Hello, World!")

async def test_hello(aiohttp_client):
    # ํ…Œ์ŠคํŠธํ•  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ์„ฑ
    app = web.Application()
    app.router.add_get('/', hello)
    
    # ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
    client = await aiohttp_client(app)
    
    # ์š”์ฒญ ์ „์†ก ๋ฐ ์‘๋‹ต ๊ฒ€์ฆ
    resp = await client.get('/')
    assert resp.status == 200
    text = await resp.text()
    assert text == "Hello, World!"

pytest ํ”ฝ์Šค์ฒ˜ ์‚ฌ์šฉํ•˜๊ธฐ:

pytest-aiohttp ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๋ฉด ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์œ ์šฉํ•œ ํ”ฝ์Šค์ฒ˜๊ฐ€ ์ œ๊ณต๋œ๋‹ค.

# pytest-aiohttp ํŒจํ‚ค์ง€ ์„ค์น˜:
# pip install pytest-aiohttp

# conftest.py
import pytest
from aiohttp import web

# ํ…Œ์ŠคํŠธ์šฉ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ์„ฑ
@pytest.fixture
def app():
    app = web.Application()
    
    async def hello(request):
        return web.Response(text="Hello, World!")
    
    async def echo(request):
        data = await request.json()
        return web.json_response(data)
    
    app.router.add_get('/', hello)
    app.router.add_post('/echo', echo)
    
    return app

# test_app.py
async def test_hello(aiohttp_client, app):
    client = await aiohttp_client(app)
    resp = await client.get('/')
    assert resp.status == 200
    text = await resp.text()
    assert text == "Hello, World!"

async def test_echo(aiohttp_client, app):
    client = await aiohttp_client(app)
    data = {"message": "Hello"}
    resp = await client.post('/echo', json=data)
    assert resp.status == 200
    json_data = await resp.json()
    assert json_data == data

๋ชจํ‚น๊ณผ ์˜์กด์„ฑ ์ฃผ์ž…:

์™ธ๋ถ€ ์˜์กด์„ฑ์ด ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ๋ชจํ‚น ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

import pytest
from unittest.mock import MagicMock, patch
from aiohttp import web

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ํด๋ž˜์Šค (์‹ค์ œ ์ฝ”๋“œ)
class Database:
    async def get_user(self, user_id):
        # ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉ์ž ์กฐํšŒ
        pass

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ•ธ๋“ค๋Ÿฌ
async def get_user_handler(request):
    user_id = request.match_info['id']
    db = request.app['db']
    
    user = await db.get_user(user_id)
    if user:
        return web.json_response(user)
    return web.Response(status=404)

# ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜
async def test_get_user(aiohttp_client):
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ์ฒด ๋ชจํ‚น
    mock_db = MagicMock()
    mock_db.get_user = MagicMock(return_value={"id": "1", "name": "ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž"})
    
    # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ์„ฑ
    app = web.Application()
    app['db'] = mock_db
    app.router.add_get('/users/{id}', get_user_handler)
    
    # ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
    client = await aiohttp_client(app)
    
    # ์š”์ฒญ ์ „์†ก ๋ฐ ์‘๋‹ต ๊ฒ€์ฆ
    resp = await client.get('/users/1')
    assert resp.status == 200
    
    data = await resp.json()
    assert data['id'] == "1"
    assert data['name'] == "ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž"
    
    # ๋ชจํ‚น๋œ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    mock_db.get_user.assert_called_once_with("1")

WebSocket ํ…Œ์ŠคํŠธ:

WebSocket ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•๋„ ์ œ๊ณต๋œ๋‹ค.

async def test_websocket(aiohttp_client):
    # WebSocket ํ•ธ๋“ค๋Ÿฌ
    async def websocket_handler(request):
        ws = web.WebSocketResponse()
        await ws.prepare(request)
        
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                await ws.send_str(f"echo: {msg.data}")
        
        return ws
    
    # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •
    app = web.Application()
    app.router.add_get('/ws', websocket_handler)
    
    # ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
    client = await aiohttp_client(app)
    
    # WebSocket ์—ฐ๊ฒฐ
    ws = await client.ws_connect('/ws')
    
    # ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐ ์‘๋‹ต ํ™•์ธ
    await ws.send_str("hello")
    msg = await ws.receive()
    assert msg.data == "echo: hello"
    
    # ์—ฐ๊ฒฐ ์ข…๋ฃŒ
    await ws.close()

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ:

์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ๋ฆ„์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

import jwt
import pytest
from aiohttp import web

# ๋ณด์•ˆ ํ‚ค
SECRET_KEY = "test_secret_key"

# ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด
@web.middleware
async def auth_middleware(request, handler):
    if request.path == '/login':
        return await handler(request)
    
    token = request.headers.get('Authorization', '').replace('Bearer ', '')
    if not token:
        return web.json_response({"error": "์ธ์ฆ ํ•„์š”"}, status=401)
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        request['user'] = payload
    except Exception:
        return web.json_response({"error": "์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ"}, status=401)
    
    return await handler(request)

# ๋กœ๊ทธ์ธ ํ•ธ๋“ค๋Ÿฌ
async def login_handler(request):
    data = await request.json()
    if data.get('username') == 'admin' and data.get('password') == 'password':
        token = jwt.encode({"user_id": 1, "username": "admin"}, SECRET_KEY, algorithm='HS256')
        return web.json_response({"token": token})
    return web.json_response({"error": "์ž˜๋ชป๋œ ์ž๊ฒฉ ์ฆ๋ช…"}, status=401)

# ๋ณดํ˜ธ๋œ ๋ฆฌ์†Œ์Šค ํ•ธ๋“ค๋Ÿฌ
async def protected_handler(request):
    user = request.get('user')
    return web.json_response({"message": f"์•ˆ๋…•ํ•˜์„ธ์š”, {user['username']}๋‹˜!"})

# ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜
async def test_auth_flow(aiohttp_client):
    # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •
    app = web.Application(middlewares=[auth_middleware])
    app.router.add_post('/login', login_handler)
    app.router.add_get('/protected', protected_handler)
    
    # ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
    client = await aiohttp_client(app)
    
    # ๋กœ๊ทธ์ธ ์—†์ด ๋ณดํ˜ธ๋œ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ ์‹œ๋„
    resp = await client.get('/protected')
    assert resp.status == 401
    
    # ์ž˜๋ชป๋œ ์ž๊ฒฉ ์ฆ๋ช…์œผ๋กœ ๋กœ๊ทธ์ธ
    resp = await client.post('/login', json={"username": "admin", "password": "wrong"})
    assert resp.status == 401
    
    # ์˜ฌ๋ฐ”๋ฅธ ์ž๊ฒฉ ์ฆ๋ช…์œผ๋กœ ๋กœ๊ทธ์ธ
    resp = await client.post('/login', json={"username": "admin", "password": "password"})
    assert resp.status == 200
    data = await resp.json()
    assert "token" in data
    
    # ํ† ํฐ์œผ๋กœ ๋ณดํ˜ธ๋œ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ
    token = data['token']
    headers = {"Authorization": f"Bearer {token}"}
    resp = await client.get('/protected', headers=headers)
    assert resp.status == 200
    
    data = await resp.json()
    assert data["message"] == "์•ˆ๋…•ํ•˜์„ธ์š”, admin๋‹˜!"

๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ ํŒ:

aiohttp ํ…Œ์ŠคํŠธ ์‹œ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŒ์ด๋‹ค.

# ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰
async def test_concurrent_requests(aiohttp_client, app):
    client = await aiohttp_client(app)
    
    # ๋™์‹œ์— ์—ฌ๋Ÿฌ ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
    import asyncio
    responses = await asyncio.gather(
        client.get('/api/1'),
        client.get('/api/2'),
        client.get('/api/3')
    )
    
    # ๊ฐ ์‘๋‹ต ๊ฒ€์ฆ
    for resp in responses:
        assert resp.status == 200

# ์‹œ๊ฐ„ ์ œ์•ฝ ์žˆ๋Š” ํ…Œ์ŠคํŠธ
async def test_with_timeout(aiohttp_client, app):
    client = await aiohttp_client(app)
    
    # 5์ดˆ ํƒ€์ž„์•„์›ƒ์œผ๋กœ ์š”์ฒญ ์‹คํ–‰
    import asyncio
    try:
        async with asyncio.timeout(5):
            resp = await client.get('/slow-api')
            assert resp.status == 200
    except asyncio.TimeoutError:
        pytest.fail("API ์‘๋‹ต์ด ๋„ˆ๋ฌด ๋А๋ฆฝ๋‹ˆ๋‹ค")

โœ… ํŠน์ง•:

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
  • ์„œ๋ฒ„ ์—”๋“œํฌ์ธํŠธ ๊ฒ€์ฆ
  • ๋น„๋™๊ธฐ ์ž‘์—… ๋ชจํ‚น
  • ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ
  • ๋ณ‘๋ ฌ ์š”์ฒญ ํ…Œ์ŠคํŠธ
  • ์ธ์ฆ ์›Œํฌํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ


8๏ธโƒฃ aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ

aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๊ณผ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์•Œ์•„๋ณด์ž.

gunicorn๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ:

aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ gunicorn๊ณผ ํ•จ๊ป˜ ๋ฐฐํฌํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋‹ค.

# app.py - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์ 
from aiohttp import web

async def hello(request):
    return web.Response(text="Hello, World!")

async def init_app():
    app = web.Application()
    app.router.add_get('/', hello)
    return app

# gunicorn ์›Œ์ปค๋ฅผ ์œ„ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜
def create_app():
    return init_app()

gunicorn ์„ค์น˜ ๋ฐ ์‹คํ–‰:

pip install gunicorn
pip install uvloop  # ์„ ํƒ์ ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์œ„ํ•ด

# aiohttp ์›Œ์ปค ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰
gunicorn app:create_app --bind 0.0.0.0:8080 --worker-class aiohttp.GunicornWebWorker

Docker์™€ ํ•จ๊ป˜ ๋ฐฐํฌํ•˜๊ธฐ:

Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ปจํ…Œ์ด๋„ˆํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

# Dockerfile
FROM python:3.10-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8080

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
CMD ["gunicorn", "app:create_app", "--bind", "0.0.0.0:8080", "--worker-class", "aiohttp.GunicornWebWorker", "--workers", "4"]

requirements.txt:

aiohttp==3.8.5
gunicorn==21.2.0
uvloop==0.17.0

Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰:

docker build -t aiohttp-app .
docker run -p 8080:8080 aiohttp-app

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์„ค์ •:

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ๋•Œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ตฌ์„ฑ์ด๋‹ค.

import os
import logging
from aiohttp import web

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ตฌ์„ฑ ๊ฐ€์ ธ์˜ค๊ธฐ
HOST = os.getenv('HOST', '0.0.0.0')
PORT = int(os.getenv('PORT', 8080))
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
DEBUG = os.getenv('DEBUG', 'false').lower() == 'true'

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(
    level=getattr(logging, LOG_LEVEL),
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('aiohttp_app')

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™”
async def init_app():
    app = web.Application(debug=DEBUG)
    
    # ๊ฐ ํ™˜๊ฒฝ(๊ฐœ๋ฐœ, ํ…Œ์ŠคํŠธ, ํ”„๋กœ๋•์…˜)์— ๋งž๋Š” ๊ตฌ์„ฑ ๋กœ๋“œ
    app['config'] = {
        'database_url': os.getenv('DATABASE_URL'),
        'redis_url': os.getenv('REDIS_URL'),
        'secret_key': os.getenv('SECRET_KEY', 'insecure-secret'),
    }
    
    # ์ข…์†์„ฑ ์„ค์ •, ๋ผ์šฐํŠธ ๋“ฑ๋ก
    # [์ƒ๋žต]
    
    return app

# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์ง์ ‘ ์‹คํ–‰
if __name__ == '__main__':
    web.run_app(init_app(), host=HOST, port=PORT)

์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€๋ฆฌ:

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

import time
import psutil
import socket
from aiohttp import web

# ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก
start_time = time.time()

async def health_check(request):
    """๊ธฐ๋ณธ ์ƒํƒœ ํ™•์ธ ์—”๋“œํฌ์ธํŠธ"""
    return web.json_response({"status": "ok"})

async def readiness_check(request):
    """์„œ๋น„์Šค๊ฐ€ ์ค€๋น„๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ"""
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์บ์‹œ ๋“ฑ ์™ธ๋ถ€ ์ข…์†์„ฑ ์—ฐ๊ฒฐ ํ™•์ธ
    database_ok = await check_database(request.app)
    cache_ok = await check_cache(request.app)
    
    if database_ok and cache_ok:
        return web.json_response({"status": "ready"})
    
    # ์ค€๋น„๋˜์ง€ ์•Š์Œ ์‘๋‹ต (Kubernetes ๋“ฑ์ด ์ธ์‹ํ•  ์ˆ˜ ์žˆ์Œ)
    return web.json_response(
        {"status": "not ready", "details": {"database": database_ok, "cache": cache_ok}},
        status=503
    )

async def metrics(request):
    """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”ํŠธ๋ฆญ ์—”๋“œํฌ์ธํŠธ"""
    process = psutil.Process()
    
    # ๋‹ค์–‘ํ•œ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
    uptime = time.time() - start_time
    cpu_usage = process.cpu_percent()
    memory_info = process.memory_info()
    memory_usage = memory_info.rss / (1024 * 1024)  # MB ๋‹จ์œ„
    open_connections = len(process.connections())
    
    # ์‹œ์Šคํ…œ ์ •๋ณด
    hostname = socket.gethostname()
    
    return web.json_response({
        "uptime_seconds": uptime,
        "cpu_usage_percent": cpu_usage,
        "memory_usage_mb": memory_usage,
        "open_connections": open_connections,
        "hostname": hostname
    })

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ƒํƒœ ์—”๋“œํฌ์ธํŠธ ๋“ฑ๋ก
app = web.Application()
app.router.add_get('/health', health_check)
app.router.add_get('/ready', readiness_check)
app.router.add_get('/metrics', metrics)

๋ณด์•ˆ ๊ฐ•ํ™”:

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐฉ๋ฒ•์ด๋‹ค.

from aiohttp import web
from cryptography.fernet import Fernet
import secrets
import aiohttp_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage

# ๋ณด์•ˆ ํ—ค๋” ๋ฏธ๋“ค์›จ์–ด
@web.middleware
async def security_headers_middleware(request, handler):
    response = await handler(request)
    
    # ๋ณด์•ˆ ๊ด€๋ จ ํ—ค๋” ์ถ”๊ฐ€
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    
    return response

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •
async def init_app():
    # ๋ณด์•ˆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ
    app = web.Application(middlewares=[security_headers_middleware])
    
    # ์•ˆ์ „ํ•œ ์„ธ์…˜ ์„ค์ •
    fernet_key = Fernet.generate_key()
    secret_key = base64.urlsafe_b64decode(fernet_key)
    aiohttp_session.setup(app, EncryptedCookieStorage(secret_key))
    
    # ์•ˆ์ „ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ์„ ์œ„ํ•œ ์„ค์ •
    app['pwd_context'] = CryptContext(
        schemes=["argon2", "bcrypt"],
        deprecated="auto"
    )
    
    # CSRF ํ† ํฐ ์ƒ์„ฑ ๋„์šฐ๋ฏธ
    app['create_csrf_token'] = lambda: secrets.token_hex(32)
    
    return app

# CSRF ๋ณดํ˜ธ ๋ฏธ๋“ค์›จ์–ด
@web.middleware
async def csrf_middleware(request, handler):
    if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
        token = request.headers.get('X-CSRF-Token')
        session = await aiohttp_session.get_session(request)
        
        if token is None or token != session.get('csrf_token'):
            return web.json_response(
                {"error": "CSRF ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."},
                status=403
            )
    
    return await handler(request)

์Šค์ผ€์ผ๋ง ์ „๋žต:

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ˆ˜ํ‰์ ์œผ๋กœ ํ™•์žฅํ•˜๊ธฐ ์œ„ํ•œ ์ „๋žต์ด๋‹ค.

# ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ ๋’ค์—์„œ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •

# nginx.conf
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://aiohttp_servers;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

upstream aiohttp_servers {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003;
    server 127.0.0.1:8004;
}

๋ฌด์ค‘๋‹จ ๋ฐฐํฌ:

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ค‘๋‹จ ์—†์ด ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

# systemd ์„œ๋น„์Šค ํŒŒ์ผ ์˜ˆ์‹œ
# /etc/systemd/system/aiohttp-app.service

[Unit]
Description=aiohttp application
After=network.target

[Service]
User=appuser
Group=appuser
WorkingDirectory=/path/to/app
ExecStart=/path/to/venv/bin/gunicorn app:create_app --bind 0.0.0.0:8080 --worker-class aiohttp.GunicornWebWorker
Restart=on-failure
RestartSec=5s

# ๊ทธ๋ ˆ์ด์Šคํ’€ ์ข…๋ฃŒ ๊ด€๋ จ ์„ค์ •
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30s

[Install]
WantedBy=multi-user.target

ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ์˜ ๋ฐฐํฌ:

ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ aiohttp ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •์ด๋‹ค.

# kubernetes-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aiohttp-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: aiohttp-app
  template:
    metadata:
      labels:
        app: aiohttp-app
    spec:
      containers:
      - name: aiohttp-app
        image: your-registry/aiohttp-app:latest
        ports:
        - containerPort: 8080
        env:
        - name: PORT
          value: "8080"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        # ์ƒํƒœ ํ™•์ธ ํ”„๋กœ๋ธŒ
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

โœ… ํŠน์ง•:

  • ๊ทธ๋ ˆ์ด์Šคํ’€ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ
  • ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ์—”๋“œํฌ์ธํŠธ
  • ์ปจํ…Œ์ด๋„ˆํ™” ์ง€์›
  • ์ˆ˜ํ‰ ํ™•์žฅ ์ „๋žต
  • ํ™˜๊ฒฝ ๊ธฐ๋ฐ˜ ๊ตฌ์„ฑ
  • ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฐํฌ


์ฃผ์š” ํŒ

โœ… ๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • ์„ธ์…˜ ์žฌ์‚ฌ์šฉ: ์—ฌ๋Ÿฌ ์š”์ฒญ์— ๋™์ผํ•œ ClientSession ์ธ์Šคํ„ด์Šค๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋น„๋™๊ธฐ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ์ž: ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด async with ๊ตฌ๋ฌธ ํ™œ์šฉ
  • ํƒ€์ž„์•„์›ƒ ์„ค์ •: ๋ชจ๋“  ์™ธ๋ถ€ ์„œ๋น„์Šค ํ˜ธ์ถœ์— ์ ์ ˆํ•œ ํƒ€์ž„์•„์›ƒ ์„ค์ •
  • ์ ์ ˆํ•œ ์ƒํƒœ ์ฝ”๋“œ: ์˜๋ฏธ ์žˆ๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ์™€ ์‘๋‹ต ๋ณธ๋ฌธ ๋ฐ˜ํ™˜
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ์žก์•„ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„
  • ์—ฐ๊ฒฐ ํ’€๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๊ฒฐ ํ’€๋ง
  • ์ฒญํฌ ์ฒ˜๋ฆฌ: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋Š” ์ฒญํฌ ๋‹จ์œ„๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ
  • ๊ฒฝ๋กœ ์ •๊ทœํ™”: ๋ช…ํ™•ํ•œ ๊ฒฝ๋กœ ๊ตฌ์กฐ์™€ ๋ผ์šฐํŒ… ํŒจํ„ด ์„ค๊ณ„
  • ์ปจํ…์ŠคํŠธ ํ™œ์šฉ: request['app']์„ ํ†ตํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ ํ™œ์šฉ
  • ๋กœ๊น… ์ „๋žต: ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…์œผ๋กœ ๋ฌธ์ œ ํ•ด๊ฒฐ ์šฉ์ด์„ฑ ํ™•๋ณด
  • graceful ์ข…๋ฃŒ: ๋ชจ๋“  ์—ฐ๊ฒฐ๊ณผ ์ž‘์—…์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ข…๋ฃŒ๋˜๋„๋ก ์ฒ˜๋ฆฌ
  • ์•”ํ˜ธํ™” ์ ์šฉ: ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ์— ์ ์ ˆํ•œ ์•”ํ˜ธํ™” ์ ์šฉ
  • CSRF ๋ณดํ˜ธ: ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ์— CSRF ํ† ํฐ ๊ฒ€์ฆ
  • CORS ์„ค์ •: ์ ์ ˆํ•œ CORS ์ •์ฑ…์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ๋ณด์•ˆ ๊ฐ•ํ™”
  • API ๋ฌธ์„œํ™”: OpenAPI ๋˜๋Š” ์œ ์‚ฌํ•œ ๋„๊ตฌ๋กœ API ๋ฌธ์„œํ™”
  • ๋น„๋™๊ธฐ ํŒจํ„ด ์ดํ•ด: ๋ธ”๋กœํ‚น ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ asyncio.to_thread() ํ™œ์šฉ


โš ๏ธ **GitHub.com Fallback** โš ๏ธ