비동기 프로그래밍 (Async Programming)

비동기 프로그래밍 (Async Programming)

1. 동기 vs 비동기

동기 (Synchronous)

작업이 순차적으로 실행되며, 하나가 끝나야 다음이 시작됩니다.

import time

def task(name, duration):
    print(f"{name} 시작")
    time.sleep(duration)  # 블로킹
    print(f"{name} 완료")

# 총 6초
task("작업1", 2)
task("작업2", 2)
task("작업3", 2)

비동기 (Asynchronous)

I/O 대기 중에 다른 작업을 수행할 수 있습니다.

import asyncio

async def task(name, duration):
    print(f"{name} 시작")
    await asyncio.sleep(duration)  # 비블로킹
    print(f"{name} 완료")

async def main():
    # 동시 실행 - 총 2초
    await asyncio.gather(
        task("작업1", 2),
        task("작업2", 2),
        task("작업3", 2)
    )

asyncio.run(main())

비교 다이어그램

동기 실행:
작업1: ████████
작업2:         ████████
작업3:                  ████████
시간:  0       2        4        6초

비동기 실행 (I/O 바운드):
작업1: ████████
작업2: ████████
작업3: ████████
시간:  0       2초

2. async/await 기초

코루틴 정의

async def my_coroutine():
    return "Hello, Async!"

# 코루틴 호출 → 코루틴 객체 반환
coro = my_coroutine()
print(coro)  # <coroutine object my_coroutine at ...>

# 실행하려면 await 또는 asyncio.run() 필요
result = asyncio.run(my_coroutine())
print(result)  # Hello, Async!

await 키워드

await는 코루틴, Task, Future를 기다립니다.

async def fetch_data():
    print("데이터 가져오는 중...")
    await asyncio.sleep(1)  # I/O 시뮬레이션
    return {"data": "value"}

async def main():
    result = await fetch_data()
    print(result)

asyncio.run(main())

주의: await는 async 함수 안에서만

# 에러!
# result = await fetch_data()  # SyntaxError

# 올바른 사용
async def main():
    result = await fetch_data()

3. asyncio 이벤트 루프

기본 실행

import asyncio

async def main():
    print("메인 코루틴")

# Python 3.7+
asyncio.run(main())

# 또는 수동으로
# loop = asyncio.get_event_loop()
# loop.run_until_complete(main())

현재 루프 가져오기

async def show_loop():
    loop = asyncio.get_running_loop()
    print(f"현재 루프: {loop}")

asyncio.run(show_loop())

4. Task 생성

asyncio.create_task()

코루틴을 태스크로 래핑하여 동시 실행을 예약합니다.

async def task(name, seconds):
    print(f"{name} 시작")
    await asyncio.sleep(seconds)
    print(f"{name} 완료")
    return name

async def main():
    # 태스크 생성 (즉시 예약됨)
    task1 = asyncio.create_task(task("A", 2))
    task2 = asyncio.create_task(task("B", 1))

    # 다른 작업 수행 가능
    print("태스크 생성됨")

    # 결과 대기
    result1 = await task1
    result2 = await task2

    print(f"결과: {result1}, {result2}")

asyncio.run(main())

출력:

태스크 생성됨
A 시작
B 시작
B 완료
A 완료
결과: A, B

5. 동시 실행

asyncio.gather()

여러 코루틴을 동시에 실행하고 모든 결과를 기다립니다.

async def fetch(url, delay):
    await asyncio.sleep(delay)
    return f"{url} 데이터"

async def main():
    results = await asyncio.gather(
        fetch("url1", 1),
        fetch("url2", 2),
        fetch("url3", 1),
    )
    print(results)  # ['url1 데이터', 'url2 데이터', 'url3 데이터']

asyncio.run(main())

return_exceptions=True

예외가 발생해도 다른 태스크는 계속 실행됩니다.

async def might_fail(n):
    if n == 2:
        raise ValueError("Error!")
    await asyncio.sleep(1)
    return n

async def main():
    results = await asyncio.gather(
        might_fail(1),
        might_fail(2),
        might_fail(3),
        return_exceptions=True
    )
    print(results)  # [1, ValueError('Error!'), 3]

asyncio.run(main())

asyncio.wait()

태스크 집합을 기다리며 더 세밀한 제어가 가능합니다.

async def main():
    tasks = [
        asyncio.create_task(fetch("url1", 2)),
        asyncio.create_task(fetch("url2", 1)),
        asyncio.create_task(fetch("url3", 3)),
    ]

    # 첫 번째 완료 시 반환
    done, pending = await asyncio.wait(
        tasks,
        return_when=asyncio.FIRST_COMPLETED
    )

    print(f"완료: {len(done)}, 대기 중: {len(pending)}")

    # 나머지 취소
    for task in pending:
        task.cancel()

asyncio.as_completed()

완료되는 순서대로 결과를 받습니다.

async def main():
    tasks = [
        fetch("url1", 3),
        fetch("url2", 1),
        fetch("url3", 2),
    ]

    for coro in asyncio.as_completed(tasks):
        result = await coro
        print(result)  # 완료 순서: url2, url3, url1

asyncio.run(main())

6. 타임아웃

asyncio.wait_for()

async def slow_operation():
    await asyncio.sleep(10)
    return "완료"

async def main():
    try:
        result = await asyncio.wait_for(
            slow_operation(),
            timeout=2.0
        )
    except asyncio.TimeoutError:
        print("타임아웃!")

asyncio.run(main())

asyncio.timeout() (Python 3.11+)

async def main():
    async with asyncio.timeout(2.0):
        await slow_operation()

7. 비동기 컨텍스트 매니저

async with를 사용합니다.

class AsyncResource:
    async def __aenter__(self):
        print("리소스 획득")
        await asyncio.sleep(0.1)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("리소스 해제")
        await asyncio.sleep(0.1)

async def main():
    async with AsyncResource() as resource:
        print("리소스 사용")

asyncio.run(main())

contextlib 버전

from contextlib import asynccontextmanager

@asynccontextmanager
async def async_resource():
    print("획득")
    yield "resource"
    print("해제")

async def main():
    async with async_resource() as r:
        print(f"사용: {r}")

8. 비동기 이터레이터

async for를 사용합니다.

class AsyncRange:
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __aiter__(self):
        self.current = self.start
        return self

    async def __anext__(self):
        if self.current >= self.end:
            raise StopAsyncIteration
        await asyncio.sleep(0.1)  # 비동기 작업
        value = self.current
        self.current += 1
        return value

async def main():
    async for num in AsyncRange(0, 5):
        print(num)

asyncio.run(main())

비동기 제너레이터

async def async_range(start, end):
    for i in range(start, end):
        await asyncio.sleep(0.1)
        yield i

async def main():
    async for num in async_range(0, 5):
        print(num)

9. 실전 예제: HTTP 요청

aiohttp 사용

import aiohttp
import asyncio

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

async def main():
    urls = [
        "https://example.com",
        "https://httpbin.org/get",
        "https://api.github.com",
    ]

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

        for url, result in zip(urls, results):
            print(f"{url}: {len(result)} bytes")

asyncio.run(main())

비동기 파일 I/O (aiofiles)

import aiofiles
import asyncio

async def read_file(path):
    async with aiofiles.open(path, 'r') as f:
        return await f.read()

async def write_file(path, content):
    async with aiofiles.open(path, 'w') as f:
        await f.write(content)

async def main():
    await write_file("test.txt", "Hello, Async!")
    content = await read_file("test.txt")
    print(content)

asyncio.run(main())

10. 동기 코드와 혼합

run_in_executor()

동기 함수를 비동기적으로 실행합니다.

import asyncio
import time

def blocking_io():
    """동기 I/O 작업"""
    time.sleep(2)
    return "결과"

async def main():
    loop = asyncio.get_running_loop()

    # 스레드 풀에서 실행
    result = await loop.run_in_executor(
        None,  # 기본 ThreadPoolExecutor
        blocking_io
    )
    print(result)

asyncio.run(main())

to_thread() (Python 3.9+)

async def main():
    result = await asyncio.to_thread(blocking_io)
    print(result)

비동기 함수를 동기적으로 호출

async def async_func():
    await asyncio.sleep(1)
    return "결과"

# 동기 컨텍스트에서 호출
result = asyncio.run(async_func())
print(result)

11. 세마포어와 락

Semaphore (동시 실행 제한)

async def limited_task(sem, n):
    async with sem:
        print(f"작업 {n} 시작")
        await asyncio.sleep(1)
        print(f"작업 {n} 완료")

async def main():
    sem = asyncio.Semaphore(3)  # 최대 3개 동시 실행

    tasks = [limited_task(sem, i) for i in range(10)]
    await asyncio.gather(*tasks)

asyncio.run(main())

Lock

async def worker(lock, name):
    async with lock:
        print(f"{name} 획득")
        await asyncio.sleep(1)
        print(f"{name} 해제")

async def main():
    lock = asyncio.Lock()

    await asyncio.gather(
        worker(lock, "A"),
        worker(lock, "B"),
        worker(lock, "C"),
    )

asyncio.run(main())

12. 에러 처리

태스크 예외 처리

async def risky_task():
    await asyncio.sleep(1)
    raise ValueError("오류!")

async def main():
    task = asyncio.create_task(risky_task())

    try:
        await task
    except ValueError as e:
        print(f"예외 발생: {e}")

asyncio.run(main())

여러 태스크 예외

async def main():
    tasks = [
        asyncio.create_task(task1()),
        asyncio.create_task(task2()),
    ]

    results = await asyncio.gather(*tasks, return_exceptions=True)

    for result in results:
        if isinstance(result, Exception):
            print(f"에러: {result}")
        else:
            print(f"성공: {result}")

13. 요약

개념 설명
async def 코루틴 정의
await 코루틴 실행 대기
asyncio.run() 이벤트 루프 실행
asyncio.create_task() 태스크 생성
asyncio.gather() 여러 코루틴 동시 실행
asyncio.wait() 세밀한 태스크 관리
async with 비동기 컨텍스트 매니저
async for 비동기 이터레이션
Semaphore 동시 실행 제한

14. 연습 문제

연습 1: 웹 크롤러

여러 URL을 동시에 가져오는 비동기 크롤러를 작성하세요.

연습 2: 동시 파일 처리

여러 파일을 동시에 읽고 처리하는 함수를 작성하세요.

연습 3: Rate Limiter

초당 요청 수를 제한하는 비동기 함수를 작성하세요.


다음 단계

09_Functional_Programming.md에서 함수형 프로그래밍을 배워봅시다!

to navigate between lessons