8.6. 예외

asyncio 스크립트 내부의 예외는 일반 Python에서와 거의 동일하게 동작합니다 – 무언가가 잡을 때까지 호출 체인을 따라 위로 전파됩니다. 거의라고 한 이유는 작업들이 병렬로 실행되기 때문에, “위로” 가는 경로가 그 작업을 생성한 경로가 아니기 때문입니다. 이 페이지는 일반적인 형태마다 예외가 어디로 가는지를 다룹니다.

8.6.1. 하나의 코루틴 내부

코루틴 내부의 try/except 는 그것이 await 하는 어떤 것이든 발생시키는 예외를 평소와 같은 방식으로 잡습니다:

async def fetch_with_retry(url):
    for attempt in range(3):
        try:
            return await fetch(url)
        except OSError as e:
            last_error = e
    raise last_error

여기에는 asyncio에 특화된 것이 없습니다 – except 절은 fetch 내부에서 발생한 예외를 마치 일반 함수 호출이었던 것처럼 봅니다.

8.6.2. 애플리케이션이 대기 중인 작업 안에서

Task 로 실행되는 코루틴이 예외를 발생시키면, 그 예외는 작업에 저장됩니다. 다음에 무언가가 그 작업을 await 하면, 예외는 그 await 지점에서 다시 발생합니다:

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

같은 원리가 asyncio.gather() 에도 적용됩니다. 기본 동작 – 하나의 자식이 예외를 발생시키면 나머지는 취소되고 예외는 gather 밖으로 전파되는 것 – 은 바로 이 메커니즘에서 나옵니다.

8.6.3. 아무도 대기하지 않는 작업 안에서

아무도 await 하지 않는 작업은 주의가 필요한 경우입니다. 예외는 여전히 발생하고, 루프는 그 작업이 처리되지 않은 예외와 함께 끝났음을 알아채지만, 예외가 표면화될 await 가 없습니다. 기본 동작은 sys.stderr 를 통해 트레이스백을 출력하고 계속 실행하는 것입니다 – 무인 진단에는 괜찮지만, 알고 싶어 했던 애플리케이션에는 잘 맞지 않습니다.

올바른 해결책은 보통 작업을 대기하는 것입니다. 핸들을 기억해 두었다가 종료 시에 그것을 대기하여 직접 하거나, gather() 또는 wait_for() 를 통해 암묵적으로 할 수 있습니다. 타임아웃과 취소 페이지의 “애플리케이션 종료하기” 패턴은 일반적인 스크립트가 생성하는 장기 실행 백그라운드 작업에 대해 이 경우를 처리합니다.

8.6.4. 사용자 정의 예외 핸들러

조용히 트레이스백을 출력하고 계속하는 것으로 충분하지 않을 때, 루프는 애플리케이션이 다른 일을 하도록 재정의할 수 있는 훅 – Loop.set_exception_handler – 을 노출합니다:

def handler(loop, context):
    print("asyncio:", context.get("message"))
    if "exception" in context:
        sys.print_exception(context["exception"])

loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)

context 인자는 'message', 'exception', 'future' 키를 가진 딕셔너리입니다. 특정 경고 스타일 이벤트에서는 exception이 없을 수 있는데, 그래서 예제에서 .get() 을 사용합니다.

전형적인 용도는 실패를 플래시에 기록하거나, 오류 LED를 점멸하거나, 워치독 재부팅으로 격상하는 것입니다. 루프 제어 페이지는 루프 훅의 전체 영역을 다룹니다.

8.6.5. KeyboardInterrupt

스크립트가 외부로부터 중단될 때 – 보통 IDE가 정지를 요청할 때 – 그 요청은 스크립트 내부에 KeyboardInterrupt 로 도착합니다. asyncio.run() 내부에서는 다른 처리되지 않은 예외와 같은 방식으로 전파됩니다: main 이 취소되고, 루프가 추적 중인 모든 작업도 취소되며, KeyboardInterruptasyncio.run() 밖으로 다시 발생합니다. finally 절은 빠져나가는 도중에 실행되므로, 취소 페이지에서 다룬 동일한 정리 패턴이 이를 처리합니다.