0Pricing
AI Agents · 강의

에이전트 개발자를 위한 비동기 Python

asyncio 기초, async def, await, 이벤트 루프를 통해 비동기 방식의 사고 모델을 익힙니다.

에이전트 개발자를 위한 비동기 Python은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

에이전트에 비동기 방식을 사용하는 이유

에이전트는 LLM 응용 프로그래밍 인터페이스 호출, 웹 요청, 데이터베이스 질의처럼 입출력 중심인 호출을 많이 수행합니다. 동기식 코드는 이러한 호출이 완료될 때까지 유휴 상태로 기다립니다. 비동기식 코드는 기다리는 동안 다른 작업을 실행하므로 처리량이 크게 향상됩니다.

비동기 기초: async def와 await

async def는 코루틴 함수를 선언합니다. await는 기다리는 작업이 완료될 때까지 실행을 일시 중지하고, 그동안 이벤트 루프가 다른 코루틴을 실행할 수 있도록 합니다.

import asyncio

async def fetch_data(source: str) -> str:
    print(f'Starting fetch from {source}')
    await asyncio.sleep(1)  # Simulates a network call
    print(f'Finished fetch from {source}')
    return f'Data from {source}'

async def main():
    # Sequential: takes 2 seconds total
    result1 = await fetch_data('source-A')
    result2 = await fetch_data('source-B')
    print('Sequential results:', result1, result2)

asyncio.run(main())

# asyncio.run() starts the event loop and runs main()
# It is the entry point for async programs

이벤트 루프

이벤트 루프는 asyncio의 핵심입니다. 이벤트 루프는 코루틴과 입출력 콜백의 대기열을 관리하고, 준비된 작업을 실행합니다. 모든 비동기 코드는 하나의 스레드에서 이벤트 루프 내부에 실행됩니다.

import asyncio

async def task_a():
    print('Task A: start')
    await asyncio.sleep(2)
    print('Task A: done')

async def task_b():
    print('Task B: start')
    await asyncio.sleep(1)
    print('Task B: done')

async def main():
    # asyncio.gather runs both tasks concurrently
    # Total time: ~2 seconds (not 3)
    await asyncio.gather(task_a(), task_b())
    print('Both tasks complete')

# Expected output order:
# Task A: start
# Task B: start
# Task B: done   <- after 1s
# Task A: done   <- after 2s
# Both tasks complete
asyncio.run(main())

코루틴과 스레드

코루틴은 협력형입니다. await를 사용해 명시적으로 제어권을 양보합니다. 스레드는 선점형입니다. OS가 언제든 스레드 사이를 전환할 수 있습니다. 코루틴은 더 가볍고, 입출력 작업에서는 GIL 문제가 없으며, 동작을 추론하기도 더 쉽습니다.

import asyncio
import threading
import time

# Thread approach: multiple OS threads
def thread_worker(name):
    print(f'Thread {name}: start')
    time.sleep(1)  # Blocks the thread
    print(f'Thread {name}: done')

threads = [threading.Thread(target=thread_worker, args=(i,)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print('---')

# Coroutine approach: single-threaded event loop
async def coro_worker(name):
    print(f'Coro {name}: start')
    await asyncio.sleep(1)  # Suspends, does NOT block other coroutines
    print(f'Coro {name}: done')

async def main():
    await asyncio.gather(*[coro_worker(i) for i in range(3)])

asyncio.run(main())
# Both approaches run 3 tasks in ~1 second total, but coroutines use one thread

asyncio.run() 진입점

asyncio.run()은 새 이벤트 루프를 만들고, 주어진 코루틴이 완료될 때까지 실행한 다음 루프를 닫습니다. 파이썬 3.7 이상에서 비동기 프로그램의 표준 진입점입니다.

import asyncio

async def agent_main():
    print('Agent starting')
    # All agent async work goes here
    results = await asyncio.gather(
        asyncio.sleep(0.1),  # Simulated LLM call
        asyncio.sleep(0.1),  # Simulated DB query
    )
    print('Agent done')
    return 'complete'

# Run the agent
result = asyncio.run(agent_main())
print('Result:', result)

# WRONG: calling asyncio.run() inside an already-running event loop
# In Jupyter notebooks, use: await agent_main() directly
# Or: nest_asyncio.apply() then asyncio.run()

흔한 실수: 비동기 컨텍스트에서 블로킹하기

비동기 코드 안에서는 블로킹 함수(time.sleep, requests.get, 동기식 파일 입출력)를 절대 호출하지 마십시오. 이렇게 하면 전체 이벤트 루프가 차단되어 모든 동시 실행이 중단됩니다.

import asyncio
import time
import httpx

# WRONG: blocks the event loop
async def bad_agent_step():
    time.sleep(2)          # Blocks all other coroutines
    # requests.get(url)    # Also blocks - do NOT use requests in async code
    return 'done'

# RIGHT: use async equivalents
async def good_agent_step():
    await asyncio.sleep(2)  # Suspends, other coroutines can run
    
    async with httpx.AsyncClient() as client:
        response = await client.get('https://api.example.com/data')
    return response.text

# For CPU-intensive work: use run_in_executor
import concurrent.futures

async def cpu_intensive_step(data: str):
    loop = asyncio.get_event_loop()
    with concurrent.futures.ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, expensive_cpu_fn, data)
    return result

def expensive_cpu_fn(data):
    # CPU-bound work runs in separate process
    return data.upper()

print('Blocking vs non-blocking patterns demonstrated')

await 누락: 조용한 버그

await를 잊어도 오류가 발생하지 않습니다. 결과 대신 코루틴 객체가 반환되기 때문입니다. 이러한 조용한 버그는 후속 오류나 빈 결과를 일으킵니다.

import asyncio

async def get_answer() -> str:
    await asyncio.sleep(0.1)
    return 'The answer is 42'

async def bad_call():
    result = get_answer()   # WRONG: forgot await
    print(type(result))     # <class 'coroutine'> - not a string!
    # Using result as a string here causes AttributeError or wrong behavior
    return result

async def good_call():
    result = await get_answer()  # CORRECT
    print(type(result))           # <class 'str'>
    return result

async def main():
    bad = await bad_call()
    print('Bad result:', bad)    # coroutine object, not the string
    
    good = await good_call()
    print('Good result:', good)  # 'The answer is 42'
    
    # Clean up the uncollected coroutine
    if asyncio.iscoroutine(bad):
        bad.close()

asyncio.run(main())

중첩된 이벤트 루프 문제

이미 실행 중인 이벤트 루프(예: 주피터, FastAPI) 안에서 asyncio.run()을 호출하면 RuntimeError가 발생합니다. 해결 방법으로는 await를 직접 사용하거나, 노트북에서는 nest_asyncio를 사용하는 방법이 있습니다.

import asyncio

async def my_agent_coroutine():
    await asyncio.sleep(0.1)
    return 'done'

# In FastAPI or other async frameworks, the event loop is already running
# Use await directly in async endpoints:
async def fastapi_endpoint():
    # WRONG inside async context:
    # result = asyncio.run(my_agent_coroutine())  # RuntimeError!
    
    # CORRECT: just await
    result = await my_agent_coroutine()
    return result

# In Jupyter notebooks: install nest_asyncio
# import nest_asyncio
# nest_asyncio.apply()
# Then asyncio.run() works

# Detect if running in event loop:
def run_agent(coro):
    try:
        loop = asyncio.get_running_loop()
        # Already in async context
        import concurrent.futures
        with concurrent.futures.ThreadPoolExecutor() as pool:
            future = pool.submit(asyncio.run, coro)
            return future.result()
    except RuntimeError:
        # No running loop
        return asyncio.run(coro)

print('Nested event loop solution defined')

asyncio.create_task

asyncio.create_task()를 사용하면 코루틴을 즉시 기다리지 않고 실행하도록 예약할 수 있습니다. 이를 통해 여러 작업을 시작한 뒤 나중에 완료를 기다릴 수 있습니다.

import asyncio

async def background_job(job_id: int) -> str:
    await asyncio.sleep(0.5)
    return f'Job {job_id} completed'

async def main():
    # Start all tasks without waiting
    task1 = asyncio.create_task(background_job(1))
    task2 = asyncio.create_task(background_job(2))
    task3 = asyncio.create_task(background_job(3))
    
    # Do other work while tasks run
    print('Tasks started, doing other work...')
    await asyncio.sleep(0.1)
    print('Other work done')
    
    # Now wait for all tasks
    results = await asyncio.gather(task1, task2, task3)
    print('All results:', results)
    
    # Or wait for the first to complete
    task_a = asyncio.create_task(background_job(4))
    task_b = asyncio.create_task(background_job(5))
    done, pending = await asyncio.wait([task_a, task_b], return_when=asyncio.FIRST_COMPLETED)
    for t in pending:
        t.cancel()  # Cancel remaining tasks
    print('First result:', done.pop().result())

asyncio.run(main())

비동기 컨텍스트 관리자

많은 비동기 라이브러리는 async with를 사용하는 비동기 컨텍스트 관리자를 사용합니다. 이를 통해 비동기 코드에서 연결과 리소스가 올바르게 설정되고 정리됩니다.

import asyncio
import httpx

async def fetch_multiple_urls(urls: list) -> list:
    # async with ensures the client is properly closed
    async with httpx.AsyncClient(timeout=10.0) as client:
        # Fetch all URLs concurrently
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks, return_exceptions=True)
        
        results = []
        for url, response in zip(urls, responses):
            if isinstance(response, Exception):
                results.append({'url': url, 'error': str(response)})
            else:
                results.append({'url': url, 'status': response.status_code})
        return results

# Async generators for streaming
async def stream_agent_events():
    events = ['thinking', 'searching', 'generating', 'done']
    for event in events:
        await asyncio.sleep(0.2)  # Simulate event arrival
        yield event

async def consume_stream():
    async for event in stream_agent_events():
        print(f'Event: {event}')

asyncio.run(consume_stream())

비동기 코드의 오류 처리

asyncio.gather(..., return_exceptions=True)를 사용하면 전체 일괄 작업을 중단하지 않고 개별 작업의 실패를 포착할 수 있습니다. 각 결과의 예외 유형을 확인하십시오.

import asyncio

async def might_fail(task_id: int) -> str:
    await asyncio.sleep(0.1)
    if task_id == 2:
        raise ValueError(f'Task {task_id} failed')
    return f'Task {task_id} succeeded'

async def robust_gather():
    tasks = [might_fail(i) for i in range(1, 5)]
    
    # return_exceptions=True: exceptions are returned as values, not raised
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    successes = []
    failures = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            failures.append({'task': i + 1, 'error': str(result)})
        else:
            successes.append(result)
    
    print(f'Succeeded: {len(successes)}, Failed: {len(failures)}')
    print('Failures:', failures)
    return successes, failures

asyncio.run(robust_gather())

지식 확인: 비동기 파이썬

에이전트 개발을 위한 비동기 파이썬에 대한 이해도를 확인해 보십시오.

비동기 파이썬 요약

에이전트 개발자를 위한 비동기 파이썬의 핵심 규칙은 다음과 같습니다. 모든 입출력 중심 작업에는 async def/await를 사용하십시오. 비동기 컨텍스트에서 블로킹 함수를 호출하지 마십시오. 병렬 실행에는 asyncio.gather()를 사용하십시오. 오류를 허용하는 병렬 호출에는 return_exceptions=True를 사용하십시오. 리소스 관리에는 async with를 사용하십시오. CPU 집약적 작업은 프로세스 풀 실행기에서 실행하십시오.

자주 묻는 질문

“에이전트 개발자를 위한 비동기 Python” 강의는 무료인가요?

네 — “에이전트 개발자를 위한 비동기 Python” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“에이전트 개발자를 위한 비동기 Python”에서 뭘 배우나요?

asyncio 기초, async def, await, 이벤트 루프를 통해 비동기 방식의 사고 모델을 익힙니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“에이전트 개발자를 위한 비동기 Python” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 에이전트 개발자를 위한 비동기 Python
  2. 이벤트 큐와 메시지 브로커
  3. 비차단 병렬 도구 실행
  4. 비동기 에이전트 프레임워크: LangChain과 그 너머
← AI Agents(으)로 돌아가기