[PY-010][실무] async와 await로 기다림 겹쳐 처리하기
페이지 정보

본문
[이번 수업]
Python의 `async`와 `await`로 네트워크처럼 기다림이 많은 I/O 작업을 겹쳐 처리합니다. 외부 서버 없이 내 컴퓨터 안의 작은 TCP 통신으로 흐름과 자원 정리를 익힙니다.
[선수지식]
함수, 예외, 바이트와 문자열의 차이를 알고 터미널에서 Python을 실행할 수 있어야 합니다.
[학습목표]
1. 코루틴과 이벤트 루프의 역할을 설명합니다.
2. 스트림을 비동기로 읽고 쓰며 연결을 닫습니다.
3. 작업 수, 시간 제한, 예외를 통제합니다.
[핵심개념]
`async def`로 만든 함수는 코루틴 함수입니다. 호출하면 실행 결과 대신 코루틴 객체가 생기며 `await`나 `gather`로 실행 기회를 주어야 합니다. 이벤트 루프는 준비된 작업을 실행하다 I/O를 기다리는 `await`를 만나면 다른 작업으로 전환합니다. `asyncio.run(main())`은 최상위 코루틴과 루프를 실행합니다.
`await`는 새 스레드를 만드는 명령이 아니라 현재 코루틴이 제어권을 양보하는 문법입니다. `time.sleep`이나 오래 걸리는 일반 함수는 루프를 막습니다. 비동기 코드에서는 `asyncio.sleep`을 쓰고 CPU 계산은 별도 프로세스를 검토합니다.
스트림 API의 `StreamReader`와 `StreamWriter`로 연결을 읽고 씁니다. `write` 뒤 `await drain()`으로 쓰기 흐름 제어에 협력하고, 사용 후 `close()`와 `await wait_closed()`로 정리합니다. `gather`는 완료 시각과 관계없이 전달한 순서대로 결과를 돌려줍니다.
실무에서는 `Semaphore`로 동시 수를 제한하고 `wait_for`나 timeout으로 최대 대기 시간을 둡니다. 취소와 예외가 발생해도 `finally`에서 연결을 닫고 예외를 숨기지 않습니다.
[따라하기]
다음을 async_echo.py로 저장합니다. 127.0.0.1은 내 컴퓨터만, 포트 0은 사용 가능한 임시 포트를 뜻합니다.
```python
import asyncio
async def handle(reader, writer):
try:
line = await reader.readline()
await asyncio.sleep(0.2)
writer.write(b"echo:" + line)
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
async def client(port, text):
reader, writer = await asyncio.open_connection("127.0.0.1", port)
try:
writer.write((text + "\n").encode())
await writer.drain()
return (await reader.readline()).decode().strip()
finally:
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle, "127.0.0.1", 0)
port = server.sockets[0].getsockname()[1]
loop = asyncio.get_running_loop()
started = loop.time()
async with server:
results = await asyncio.gather(
client(port, "hello"),
client(port, "world"),
)
print("결과:", results)
print(f"시간: {loop.time() - started:.1f}초")
asyncio.run(main())
```
macOS/Linux는 `python3 async_echo.py`, Windows PowerShell은 `py async_echo.py`로 실행합니다. 패키지나 인터넷은 필요 없습니다. 시간은 조금 달라도 핵심 결과는 다음과 같습니다.
```text
결과: ['echo:hello', 'echo:world']
시간: 0.2초
```
요청마다 0.2초를 기다리지만 두 클라이언트가 겹쳐 약 0.2초가 걸립니다. 서버와 각 writer는 문맥 관리자와 `finally`에서 닫힙니다.
[흔한 실수]
코루틴을 호출만 하고 `await`하지 않거나 비동기 함수 안에서 `time.sleep`을 쓰기 쉽습니다. 빈 `except`로 예외를 숨기거나 연결을 닫지 않는 것도 피합니다.
[보안 주의]
서버는 외부에 열리는 `0.0.0.0`이 아니라 `127.0.0.1`에만 연결합니다. 실제 서비스는 입력 크기, 연결 수, 읽기 시간을 제한하고 수신 문자열을 명령으로 쓰지 않습니다. 비밀값을 넣지 말고 본인 소유의 로컬 또는 허가된 격리 환경에서만 실행합니다.
[직접 해볼 과제]
세 번째 `client(port, "python")`을 추가하세요. 다음에는 `gather` 대신 하나씩 `await`해 약 0.4초가 되는지 비교하고 이유를 기록합니다.
[확인문제]
1. 코루틴 함수를 호출만 했을 때 바로 실행되지 않는 이유는 무엇인가요?
2. 비동기 함수 안에서 `time.sleep`을 피해야 하는 이유는 무엇인가요?
3. 연결을 `finally`에서 닫으면 어떤 실패 상황에 도움이 되나요?
[다음 학습]
다음 과정 JSTS-010에서는 테스트·린트·포맷 자동화를 배웁니다. PY-011에서는 pytest로 자동 테스트를 작성합니다.
[공식 참고 자료]
- Python asyncio 공식 문서: https://docs.python.org/3/library/asyncio.html
- Python 코루틴과 작업: https://docs.python.org/3/library/asyncio-task.html
- Python 스트림 공식 문서: https://docs.python.org/3/library/asyncio-stream.html
- Python 동기화 기본 도구: https://docs.python.org/3/library/asyncio-sync.html
Python의 `async`와 `await`로 네트워크처럼 기다림이 많은 I/O 작업을 겹쳐 처리합니다. 외부 서버 없이 내 컴퓨터 안의 작은 TCP 통신으로 흐름과 자원 정리를 익힙니다.
[선수지식]
함수, 예외, 바이트와 문자열의 차이를 알고 터미널에서 Python을 실행할 수 있어야 합니다.
[학습목표]
1. 코루틴과 이벤트 루프의 역할을 설명합니다.
2. 스트림을 비동기로 읽고 쓰며 연결을 닫습니다.
3. 작업 수, 시간 제한, 예외를 통제합니다.
[핵심개념]
`async def`로 만든 함수는 코루틴 함수입니다. 호출하면 실행 결과 대신 코루틴 객체가 생기며 `await`나 `gather`로 실행 기회를 주어야 합니다. 이벤트 루프는 준비된 작업을 실행하다 I/O를 기다리는 `await`를 만나면 다른 작업으로 전환합니다. `asyncio.run(main())`은 최상위 코루틴과 루프를 실행합니다.
`await`는 새 스레드를 만드는 명령이 아니라 현재 코루틴이 제어권을 양보하는 문법입니다. `time.sleep`이나 오래 걸리는 일반 함수는 루프를 막습니다. 비동기 코드에서는 `asyncio.sleep`을 쓰고 CPU 계산은 별도 프로세스를 검토합니다.
스트림 API의 `StreamReader`와 `StreamWriter`로 연결을 읽고 씁니다. `write` 뒤 `await drain()`으로 쓰기 흐름 제어에 협력하고, 사용 후 `close()`와 `await wait_closed()`로 정리합니다. `gather`는 완료 시각과 관계없이 전달한 순서대로 결과를 돌려줍니다.
실무에서는 `Semaphore`로 동시 수를 제한하고 `wait_for`나 timeout으로 최대 대기 시간을 둡니다. 취소와 예외가 발생해도 `finally`에서 연결을 닫고 예외를 숨기지 않습니다.
[따라하기]
다음을 async_echo.py로 저장합니다. 127.0.0.1은 내 컴퓨터만, 포트 0은 사용 가능한 임시 포트를 뜻합니다.
```python
import asyncio
async def handle(reader, writer):
try:
line = await reader.readline()
await asyncio.sleep(0.2)
writer.write(b"echo:" + line)
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
async def client(port, text):
reader, writer = await asyncio.open_connection("127.0.0.1", port)
try:
writer.write((text + "\n").encode())
await writer.drain()
return (await reader.readline()).decode().strip()
finally:
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle, "127.0.0.1", 0)
port = server.sockets[0].getsockname()[1]
loop = asyncio.get_running_loop()
started = loop.time()
async with server:
results = await asyncio.gather(
client(port, "hello"),
client(port, "world"),
)
print("결과:", results)
print(f"시간: {loop.time() - started:.1f}초")
asyncio.run(main())
```
macOS/Linux는 `python3 async_echo.py`, Windows PowerShell은 `py async_echo.py`로 실행합니다. 패키지나 인터넷은 필요 없습니다. 시간은 조금 달라도 핵심 결과는 다음과 같습니다.
```text
결과: ['echo:hello', 'echo:world']
시간: 0.2초
```
요청마다 0.2초를 기다리지만 두 클라이언트가 겹쳐 약 0.2초가 걸립니다. 서버와 각 writer는 문맥 관리자와 `finally`에서 닫힙니다.
[흔한 실수]
코루틴을 호출만 하고 `await`하지 않거나 비동기 함수 안에서 `time.sleep`을 쓰기 쉽습니다. 빈 `except`로 예외를 숨기거나 연결을 닫지 않는 것도 피합니다.
[보안 주의]
서버는 외부에 열리는 `0.0.0.0`이 아니라 `127.0.0.1`에만 연결합니다. 실제 서비스는 입력 크기, 연결 수, 읽기 시간을 제한하고 수신 문자열을 명령으로 쓰지 않습니다. 비밀값을 넣지 말고 본인 소유의 로컬 또는 허가된 격리 환경에서만 실행합니다.
[직접 해볼 과제]
세 번째 `client(port, "python")`을 추가하세요. 다음에는 `gather` 대신 하나씩 `await`해 약 0.4초가 되는지 비교하고 이유를 기록합니다.
[확인문제]
1. 코루틴 함수를 호출만 했을 때 바로 실행되지 않는 이유는 무엇인가요?
2. 비동기 함수 안에서 `time.sleep`을 피해야 하는 이유는 무엇인가요?
3. 연결을 `finally`에서 닫으면 어떤 실패 상황에 도움이 되나요?
[다음 학습]
다음 과정 JSTS-010에서는 테스트·린트·포맷 자동화를 배웁니다. PY-011에서는 pytest로 자동 테스트를 작성합니다.
[공식 참고 자료]
- Python asyncio 공식 문서: https://docs.python.org/3/library/asyncio.html
- Python 코루틴과 작업: https://docs.python.org/3/library/asyncio-task.html
- Python 스트림 공식 문서: https://docs.python.org/3/library/asyncio-stream.html
- Python 동기화 기본 도구: https://docs.python.org/3/library/asyncio-sync.html
- 이전글[JSTS-010][실무] 테스트·검사·코드 정리를 한 번에 실행하기 26.09.04
- 다음글[TOOL-010][실무] 비밀값 없이 안전하게 코드 공유하기 26.09.04
댓글목록
등록된 댓글이 없습니다.
