Python의 async/await 구문은 비동기 처리를 간결하게 작성할 수 있는 구조로, 특히 I/O 바운드한 작업이나 다수의 요청을 처리하는 애플리케이션에서 중요한 역할을 합니다. 본 기사에서는 이 구문의 기본 개념부터, 실용적인 활용법과 응용 예제까지 쉽게 설명합니다. 비동기 프로그래밍의 기초를 배우며, 실제 코드 예제를 통해 이해를 깊이 쌓아봅시다.
async/await 구문의 기본 개념
Python의 async/await 구문은 비동기 프로그래밍을 간단히 구현하기 위한 키워드입니다. 이를 사용하면 시간이 오래 걸리는 작업(I/O 작업 등)을 효율적으로 처리할 수 있어 프로그램의 응답성이 향상됩니다.
비동기 프로그래밍이란
비동기 프로그래밍은 프로그램이 하나의 작업을 기다리는 동안 다른 작업을 실행할 수 있도록 하는 기술입니다. 동기 처리에서는 각 작업이 순차적으로 실행되는 반면, 비동기 처리에서는 여러 작업이 “동시에” 실행되는 것처럼 보입니다.
async와 await의 역할
- async: 함수가 비동기 함수임을 정의하는 데 사용됩니다. 이 함수는 코루틴(coroutine)이라고 불리며,
await
를 사용하여 다른 비동기 처리를 호출할 수 있습니다. - await: 비동기 처리의 결과를 기다리는 데 사용됩니다.
await
로 대기하는 동안 다른 작업이 실행되어 프로그램 전체의 효율성이 향상됩니다.
기본적인 사용 예제
다음은 async/await의 간단한 예제입니다:
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1) # 1초 대기
print("World")
# 비동기 함수 실행
asyncio.run(say_hello())
이 코드는 “Hello”를 출력한 후, 1초 대기하고 “World”를 출력합니다. await
로 대기하는 동안 다른 비동기 작업도 실행 가능합니다.
코루틴의 특징
async
로 정의된 함수는 직접 실행할 수 없으며,await
또는asyncio.run()
을 사용하여 실행해야 합니다.- 비동기 처리를 효율적으로 활용하기 위해서는 코루틴과 태스크(다음 항목에서 설명)를 적절히 조합해야 합니다.
asyncio 라이브러리의 개요와 역할
Python의 표준 라이브러리인 asyncio
는 비동기 처리를 효율적으로 관리하기 위한 툴셋을 제공합니다. 이를 통해 I/O 작업이나 다수의 태스크를 병행 처리하는 작업을 쉽게 구현할 수 있습니다.
asyncio의 역할
- 이벤트 루프 관리: 태스크의 스케줄링과 실행을 담당하는 중심적인 역할을 합니다.
- 코루틴과 태스크 관리: 비동기 처리를 태스크로 등록하여 효율적으로 실행합니다.
- 비동기 I/O 작업 지원: 파일 작업이나 네트워크 통신 등 I/O 대기 시간이 수반되는 작업을 비동기적으로 실행합니다.
이벤트 루프란?
이벤트 루프는 비동기 태스크를 순차적으로 처리하는 엔진과 같습니다. asyncio
에서는 이 루프가 비동기 함수를 관리하고, 효율적인 태스크 스케줄링을 수행합니다.
import asyncio
async def example_task():
print("Task started")
await asyncio.sleep(1)
print("Task finished")
async def main():
# 이벤트 루프 내에서 태스크 실행
await example_task()
# 이벤트 루프 시작하여 main() 실행
asyncio.run(main())
주요 asyncio 함수 및 클래스
asyncio.run()
: 이벤트 루프를 시작하고 비동기 함수를 실행합니다.asyncio.create_task()
: 코루틴을 태스크로 만들어 이벤트 루프에 등록합니다.asyncio.sleep()
: 지정된 시간만큼 비동기적으로 대기합니다.asyncio.gather()
: 여러 태스크를 한 번에 실행하고 그 결과를 얻습니다.asyncio.Queue
: 비동기 태스크 간에 데이터를 효율적으로 주고받을 수 있는 큐입니다.
간단한 응용 예제
다음은 여러 태스크를 병행 실행하는 예제입니다:
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# 병행 실행
await asyncio.gather(task1(), task2())
asyncio.run(main())
이 프로그램에서는 태스크 1과 태스크 2가 병행 실행되며, 태스크 2가 먼저 종료됩니다.
asyncio의 장점
- 많은 태스크를 효율적으로 관리할 수 있습니다.
- I/O 바운드한 작업에서 성능 향상
- 이벤트 루프를 통한 유연한 스케줄링 가능
asyncio를 이해하면 비동기 프로그래밍의 힘을 최대한 활용할 수 있게 됩니다.
코루틴과 태스크의 차이점 및 사용법
Python의 비동기 처리에서 코루틴과 태스크는 기본적인 개념입니다. 각각의 특징과 역할을 이해하고, 적절하게 사용하면 비동기 처리를 효율적으로 구현할 수 있습니다.
코루틴이란
코루틴은 비동기 함수로 정의되는 특별한 함수입니다. async def
로 정의하고, await
을 사용해 다른 비동기 작업을 실행할 수 있습니다. 코루틴은 실행 도중에 멈추고, 외부에서 재개할 수 있습니다.
예시: 코루틴 정의 및 사용
import asyncio
async def my_coroutine():
print("Start coroutine")
await asyncio.sleep(1)
print("End coroutine")
# 코루틴 실행
asyncio.run(my_coroutine())
태스크란
태스크는 코루틴을 이벤트 루프에서 실행하기 위해 래핑한 것입니다. asyncio.create_task()
를 사용하여 생성되고, 이벤트 루프에 등록되면 병행 실행됩니다.
태스크 생성 및 실행 예시
import asyncio
async def my_coroutine(number):
print(f"Coroutine {number} started")
await asyncio.sleep(1)
print(f"Coroutine {number} finished")
async def main():
# 여러 태스크 생성 후 병행 실행
task1 = asyncio.create_task(my_coroutine(1))
task2 = asyncio.create_task(my_coroutine(2))
# 태스크 완료 기다리기
await task1
await task2
asyncio.run(main())
이 예시에서는 태스크 1과 태스크 2가 동시에 시작되어 각각 병행 실행됩니다.
코루틴과 태스크의 차이점
특징 | 코루틴 | 태스크 |
---|---|---|
정의 방법 | async def 로 정의 | asyncio.create_task() 로 생성 |
실행 방법 | await 또는 asyncio.run() 으로 실행 | 이벤트 루프에서 자동 실행됨 |
병행 실행 | 단독 비동기 작업으로 작성 | 여러 비동기 작업을 동시에 실행 가능 |
사용 시점
- 코루틴은 단순한 비동기 처리를 작성할 때 사용합니다.
- 태스크는 여러 비동기 처리를 병행해서 실행하고 싶을 때 활용합니다.
응용 예제: 태스크를 사용한 병행 처리
다음은 태스크를 사용하여 여러 비동기 함수를 효율적으로 실행하는 예시입니다:
import asyncio
async def fetch_data(url):
print(f"Fetching data from {url}")
await asyncio.sleep(2) # 모의 네트워크 대기
print(f"Finished fetching data from {url}")
async def main():
urls = ["https://example.com", "https://example.org", "https://example.net"]
# 여러 태스크 생성
tasks = [asyncio.create_task(fetch_data(url)) for url in urls]
# 모든 태스크 종료 대기
await asyncio.gather(*tasks)
asyncio.run(main())
이 프로그램에서는 리스트 내포를 사용해 여러 태스크를 생성하고, 그것들을 병행 실행하고 있습니다.
주의 사항
- 태스크 실행 순서는 보장되지 않으므로 의존 관계가 있는 처리에는 적합하지 않습니다.
- 태스크는 이벤트 루프에서 스케줄링되므로, 이벤트 루프 외부에서는 사용할 수 없습니다.
코루틴과 태스크의 차이를 올바르게 이해하고, 용도에 맞게 사용하면 비동기 프로그램의 효율성을 극대화할 수 있습니다.
비동기 처리의 장점과 한계
비동기 처리는 특히 I/O 작업이 많은 애플리케이션에서 성능을 향상시키는 수단으로 유용하지만, 만능은 아닙니다. 본 절에서는 비동기 처리의 장점과 한계를 이해하고, 적절하게 활용하기 위한 기초를 설명합니다.
비동기 처리의 장점
1. 속도와 효율성 향상
- I/O 대기 중 자원 활용: 동기 처리에서는 I/O 대기 중 프로그램이 정지하지만, 비동기 처리에서는 다른 작업이 실행되므로 자원을 효율적으로 활용할 수 있습니다.
- 높은 처리량: 한 번에 많은 요청을 처리하는 서버나, 여러 네트워크 작업을 동시에 실행하는 클라이언트에 최적입니다.
2. 응답 시간 향상
- 사용자 경험 개선: 비동기 처리를 적용하여 UI를 차단하지 않고 백그라운드 작업을 실행할 수 있어 응답성이 향상됩니다.
- 대기 시간 단축: 비동기 I/O를 사용하면 다른 작업과 동시에 진행될 수 있어 전체 대기 시간이 단축됩니다.
3. 유연성 및 확장성
- 확장 가능한 설계: 비동기 프로그램은 스레드나 프로세스를 과도하게 소비하지 않으며, 시스템 자원을 효율적으로 사용합니다.
- 멀티태스킹 구현: 비동기 태스크 간에 효율적으로 전환할 수 있어, 시스템이 고부하를 견딜 수 있습니다.
비동기 처리의 한계
1. 프로그램 복잡화
비동기 처리는 구조가 직관적이지 않은 경우가 많고, 동기 처리에 비해 디버깅과 유지보수가 어려워질 수 있습니다. 특히, 다음과 같은 문제들이 발생하기 쉽습니다:
- 경쟁 상태: 여러 태스크가 동일한 자원에 접근할 때 데이터 일관성을 유지하기 어렵습니다.
- 콜백 지옥: 복잡한 의존 관계를 가진 비동기 처리에서는 코드가 읽기 어려워질 수 있습니다.
2. CPU 바운드 작업에 비효율적
비동기 처리는 주로 I/O 바운드 작업에 최적화되어 있습니다. 계산량이 많은 CPU 바운드 작업에서는 GIL(전역 인터프리터 잠금)으로 인한 제약도 있어 성능 향상을 기대할 수 없는 경우가 많습니다.
3. 적절한 설계가 필요
비동기 프로그램을 효과적으로 작동시키기 위해서는 적절한 설계와 라이브러리 선택이 필수적입니다. 잘못된 설계는 다음과 같은 문제를 일으킬 수 있습니다:
- 데드락: 태스크가 서로 종료를 기다리면서 멈추는 상태.
- 스케줄링 불일치: 비효율적인 스케줄링으로 예기치 않게 시간이 오래 걸릴 수 있습니다.
비동기 처리를 활용하는 포인트
1. 적재적소에 사용
- I/O 바운드 작업에 적용: 데이터베이스 작업, 네트워크 통신, 파일 작업 등에서 유용합니다.
- CPU 바운드 작업은 스레드나 프로세스로 대응: 병렬 처리를 보완하는 기술을 결합하여 사용합니다.
2. 고품질의 툴과 라이브러리 활용
- asyncio: 표준 라이브러리로 비동기 처리를 관리하는 기본 툴입니다.
- aiohttp: 비동기 HTTP 통신에 특화된 라이브러리입니다.
- Quart 및 FastAPI: 비동기 지원 웹 프레임워크입니다.
3. 디버깅 및 모니터링 철저히 하기
- 로그를 활용해 태스크 간의 동작을 기록하고, 디버깅에 도움이 됩니다.
asyncio
의 디버그 모드를 활성화하여 상세한 오류 정보를 얻을 수 있습니다.
비동기 처리는 적절히 설계하면 애플리케이션의 성능을 대폭 향상시킬 수 있지만, 그 한계를 이해하고 적절한 설계를 하는 것이 중요합니다.
비동기 함수 실제 작성하기
Python에서 비동기 처리를 구현하려면, async
와 await
을 조합하여 비동기 함수를 정의하고 실행합니다. 본 절에서는 비동기 함수를 작성하고, 기본적인 비동기 처리 흐름을 배웁니다.
비동기 함수의 기본 구조
비동기 함수는 async def
을 사용하여 정의합니다. 이 함수 내에서 다른 비동기 처리를 호출할 때는 await
을 사용합니다.
기본적인 비동기 함수 예시
import asyncio
async def greet():
print("Hello,")
await asyncio.sleep(1) # 비동기적으로 1초 대기
print("World!")
# 비동기 함수 실행
asyncio.run(greet())
이 예시에서는 await asyncio.sleep(1)
이 비동기 처리 실행 부분입니다. 비동기 처리를 사용하면 1초 대기 중에도 다른 작업이 진행될 수 있습니다.
비동기 함수 간의 연계
여러 비동기 함수를 호출하여 태스크 간에 연계시킬 수도 있습니다.
비동기 함수 연계 예시
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# 순차적으로 비동기 함수 실행
await task1()
await task2()
asyncio.run(main())
여기서는 main
함수가 비동기 함수로 정의되어, 다른 비동기 함수 task1
과 task2
를 순차적으로 실행합니다.
비동기 함수와 병행 처리
비동기 함수의 병행 실행에는 asyncio.create_task
를 사용합니다. 이를 통해 여러 비동기 처리를 동시에 진행할 수 있습니다.
병행 처리 예시
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# 병행 실행 태스크 생성
task1_coroutine = asyncio.create_task(task1())
task2_coroutine = asyncio.create_task(task2())
# 두 태스크 종료 기다리기
await task1_coroutine
await task2_coroutine
asyncio.run(main())
이 예시에서는 task1
과 task2
가 병행 실행됩니다. 태스크 2가 1초 만에 종료되고, 그 후에 태스크 1이 종료됩니다.
응용 예시: 간단한 비동기 카운터
다음은 비동기 함수를 사용한 카운트 예시입니다. 여러 카운트 작업이 병행하여 진행됩니다.
async def count(number):
for i in range(1, 4):
print(f"Counter {number}: {i}")
await asyncio.sleep(1) # 비동기적으로 1초 대기
async def main():
# 여러 카운트 작업 병행 실행
await asyncio.gather(count(1), count(2), count(3))
asyncio.run(main())
실행 결과
Counter 1: 1
Counter 2: 1
Counter 3: 1
Counter 1: 2
Counter 2: 2
Counter 3: 2
Counter 1: 3
Counter 2: 3
Counter 3: 3
비동기 처리를 사용하면 각 카운터가 독립적으로 작동하고 있음을 알 수 있습니다.
포인트 및 주의사항
- 비동기 처리를 사용하면 시스템 자원의 낭비를 줄이고 효율적인 태스크 관리가 가능합니다.
- 필요에 따라
asyncio.gather
나asyncio.create_task
를 적절히 사용합니다. - 비동기 함수를 실행할 때는 반드시
asyncio.run
이나 이벤트 루프를 사용해야 합니다.
비동기 함수를 기본부터 연습함으로써 비동기 처리의 응용력을 높일 수 있습니다.
병렬 처리 구현 방법: gather와 wait 활용
Python의 비동기 처리에서는 여러 작업을 효율적으로 병렬 실행하기 위해 asyncio.gather
와 asyncio.wait
를 사용합니다. 각각의 특징과 사용 방법을 이해함으로써 보다 유연한 비동기 프로그램을 구축할 수 있습니다.
asyncio.gather 개요 및 사용 예시
asyncio.gather
는 여러 비동기 작업을 묶어서 실행하고, 모든 작업이 종료될 때까지 기다립니다. 종료 후, 각각의 결과를 리스트로 반환합니다.
기본 예시
import asyncio
async def task1():
await asyncio.sleep(1)
return "Task 1 complete"
async def task2():
await asyncio.sleep(2)
return "Task 2 complete"
async def main():
results = await asyncio.gather(task1(), task2())
print(results)
asyncio.run(main())
실행 결과
['Task 1 complete', 'Task 2 complete']
특징
- 병렬 실행 중인 작업들의 종료를 기다리고, 결과를 리스트로 받는다.
- 예외가 발생하면,
gather
는 모든 작업을 중단하고 예외를 호출한 곳으로 전달한다.
asyncio.wait 개요 및 사용 예시
asyncio.wait
는 여러 작업을 병렬로 실행하고, 완료된 작업과 미완료된 작업을 세트로 반환합니다.
기본 예시
import asyncio
async def task1():
await asyncio.sleep(1)
print("Task 1 complete")
async def task2():
await asyncio.sleep(2)
print("Task 2 complete")
async def main():
tasks = [task1(), task2()]
done, pending = await asyncio.wait(tasks)
print(f"Done tasks: {len(done)}, Pending tasks: {len(pending)}")
asyncio.run(main())
실행 결과
Task 1 complete
Task 2 complete
Done tasks: 2, Pending tasks: 0
특징
- 작업의 상태(완료・미완료)를 세밀하게 확인할 수 있다.
- 작업이 중간에 종료되어도 미완료 작업을 처리할 수 있다.
asyncio.wait
의return_when
옵션을 사용하여 특정 조건에서 종료를 제어할 수 있다.
return_when 옵션 예시
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
FIRST_COMPLETED
: 첫 번째 작업이 완료되었을 때 돌아간다.FIRST_EXCEPTION
: 첫 번째 예외가 발생한 시점에서 돌아간다.ALL_COMPLETED
: 모든 작업이 종료될 때까지 기다린다(기본값).
gather와 wait의 사용 구분
- 결과를 한 번에 받고 싶은 경우:
asyncio.gather
를 사용한다. - 작업 상태를 개별적으로 관리하고 싶은 경우:
asyncio.wait
를 사용한다. - 중간에 종료하거나 예외를 처리하고 싶은 경우:
asyncio.wait
가 적합하다.
응용 예시: 병렬로 API 호출하기
다음은 여러 API를 병렬로 호출하여 응답을 받는 예시입니다:
import asyncio
async def fetch_data(api_name, delay):
print(f"Fetching from {api_name}...")
await asyncio.sleep(delay) # 모의 대기
return f"Data from {api_name}"
async def main():
apis = [("API_1", 2), ("API_2", 1), ("API_3", 3)]
tasks = [fetch_data(api, delay) for api, delay in apis]
# gather로 병렬 처리하고 결과를 수집
results = await asyncio.gather(*tasks)
for result in results:
print(result)
asyncio.run(main())
실행 결과
Fetching from API_1...
Fetching from API_2...
Fetching from API_3...
Data from API_2
Data from API_1
Data from API_3
주의사항
- 예외 처리: 병렬 작업에서 예외가 발생할 경우, 그 예외를 적절히 캐치하여 처리해야 합니다.
try
/except
를 활용합시다. - 작업 취소: 필요 없는 작업은
task.cancel()
을 사용하여 취소합니다. - 데드락에 주의: 작업들이 서로 대기하는 상황을 피하기 위한 설계가 필요합니다.
asyncio.gather
와 asyncio.wait
를 효과적으로 사용하면, 비동기 처리의 유연성과 효율성을 극대화할 수 있습니다.
비동기 I/O 예시: 파일 및 네트워크 작업
비동기 I/O는 파일 작업이나 네트워크 통신 등 대기 시간이 발생하는 작업을 효율적으로 처리하기 위한 기법입니다. asyncio
를 활용하면 비동기 I/O를 간결하게 구현할 수 있습니다. 본 섹션에서는 비동기 I/O의 기본적인 사용 방법을 구체적인 예시를 통해 설명합니다.
비동기 파일 작업
비동기 파일 작업에는 aiofiles
라이브러리를 사용합니다. 이 라이브러리는 표준 라이브러리의 파일 작업을 비동기적으로 실행할 수 있게 확장한 것입니다.
예시: 비동기 파일 읽기 및 쓰기
import aiofiles
import asyncio
async def read_file(filepath):
async with aiofiles.open(filepath, mode='r') as file:
contents = await file.read()
print(f"{filepath}의 내용:")
print(contents)
async def write_file(filepath, data):
async with aiofiles.open(filepath, mode='w') as file:
await file.write(data)
print(f"{filepath}에 데이터가 쓰여졌습니다.")
async def main():
filepath = 'example.txt'
await write_file(filepath, "Hello, Async File IO!")
await read_file(filepath)
asyncio.run(main())
포인트
aiofiles.open
을 사용하여 비동기적으로 파일을 작업할 수 있습니다.async with
구문을 사용하여 파일을 안전하게 처리합니다.- 파일 작업 중에도 다른 작업들이 진행 가능합니다.
비동기 네트워크 작업
네트워크 작업에서는 aiohttp
라이브러리를 사용하면 비동기적인 HTTP 요청이 가능합니다.
예시: 비동기 HTTP 요청
import aiohttp
import asyncio
async def fetch_url(session, url):
async with session.get(url) as response:
print(f"{url}에서 가져오는 중...")
content = await response.text()
print(f"{url}의 내용: {content[:100]}...")
async def main():
urls = [
"https://example.com",
"https://example.org",
"https://example.net"
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
포인트
aiohttp.ClientSession
을 사용하여 비동기 HTTP 통신을 수행합니다.async with
구문을 사용해 세션을 관리하고 안전하게 요청을 전송합니다.- 여러 요청을
asyncio.gather
로 병렬 처리하여 효율화를 실현합니다.
비동기 파일과 네트워크의 결합
비동기 파일 작업과 네트워크 작업을 결합하면 효율적인 데이터 수집이나 저장이 가능합니다.
예시: 다운로드한 데이터를 비동기적으로 저장
import aiohttp
import aiofiles
import asyncio
async def fetch_and_save(session, url, filepath):
async with session.get(url) as response:
print(f"{url}에서 가져오는 중...")
content = await response.text()
async with aiofiles.open(filepath, mode='w') as file:
await file.write(content)
print(f"{url}의 내용이 {filepath}에 저장되었습니다.")
async def main():
urls = [
("https://example.com", "example_com.txt"),
("https://example.org", "example_org.txt"),
("https://example.net", "example_net.txt")
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_and_save(session, url, filepath) for url, filepath in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
실행 결과 예시
example_com.txt
에https://example.com
의 내용이 저장됩니다.- 다른 URL들의 내용도 해당 파일에 저장됩니다.
비동기 I/O 사용 시 주의점
- 예외 처리 구현
네트워크 장애나 파일 쓰기 오류에 대비하여 적절한 예외 처리를 해야 합니다.
try:
# 비동기 작업
except Exception as e:
print(f"An error occurred: {e}")
- 슬로틀링 수행
많은 비동기 작업을 동시에 실행하면 시스템이나 서버에 부담을 줄 수 있습니다.asyncio.Semaphore
를 사용해 작업의 병렬 수를 제한할 수 있습니다.
semaphore = asyncio.Semaphore(5) # 최대 5개의 작업을 병렬로 실행
async with semaphore:
await some_async_task()
- 타임아웃 설정
응답이 없는 작업을 방지하기 위해 타임아웃을 설정합니다.
try:
await asyncio.wait_for(some_async_task(), timeout=10)
except asyncio.TimeoutError:
print("Task timed out")
비동기 I/O를 적절히 활용하면 애플리케이션의 효율성과 스루풋을 크게 향상시킬 수 있습니다.
응용 예시: 비동기 Web 크롤러 구축
비동기 처리를 활용하면 빠르고 효율적인 Web 크롤러를 만들 수 있습니다. 비동기 I/O를 사용하면 많은 웹 페이지를 병렬로 얻을 수 있어 크롤링 속도를 극대화할 수 있습니다. 본 섹션에서는 Python을 이용한 비동기 Web 크롤러 구현 예시를 설명합니다.
비동기 Web 크롤러 기본 구조
비동기 Web 크롤러에서는 다음 3가지 요소가 중요합니다:
- URL 리스트 관리: 크롤링 대상 URL을 효율적으로 관리합니다.
- 비동기 HTTP 통신: 비동기 라이브러리
aiohttp
로 웹 페이지를 가져옵니다. - 데이터 저장: 비동기 파일 작업으로 얻은 데이터를 저장합니다.
코드 예시: 비동기 Web 크롤러
다음은 기본적인 비동기 Web 크롤러 구현 예시입니다:
import aiohttp
import aiofiles
import asyncio
from bs4 import BeautifulSoup
async def fetch_page(session, url):
try:
async with session.get(url) as response:
if response.status == 200:
html = await response.text()
print(f"{url}에서 가져옴")
return html
else:
print(f"{url}에서 가져오기 실패: {response.status}")
return None
except Exception as e:
print(f"{url}에서 오류 발생: {e}")
return None
async def parse_and_save(html, url, filepath):
if html:
soup = BeautifulSoup(html, 'html.parser')
title = soup.title.string if soup.title else "제목 없음"
async with aiofiles.open(filepath, mode='a') as file:
await file.write(f"URL: {url}\nTitle: {title}\n\n")
print(f"{url}의 데이터 저장됨")
async def crawl(urls, output_file):
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
tasks.append(process_url(session, url, output_file))
await asyncio.gather(*tasks)
async def process_url(session, url, output_file):
html = await fetch_page(session, url)
await parse_and_save(html, url, output_file)
async def main():
urls = [
"https://example.com",
"https://example.org",
"https://example.net"
]
output_file = "crawl_results.txt"
# 초기화: 결과 파일 비우기
async with aiofiles.open(output_file, mode='w') as file:
await file.write("")
await crawl(urls, output_file)
asyncio.run(main())
코드 동작 설명
fetch_page
함수
비동기 HTTP 요청으로 웹 페이지의 HTML을 가져옵니다. 상태 코드를 확인하고 에러 처리를 수행합니다.parse_and_save
함수
BeautifulSoup을 사용해 HTML을 분석하고, 페이지 제목을 추출합니다. 그 데이터를 비동기적으로 파일에 저장합니다.crawl
함수
URL 리스트를 받아 각 URL을 병렬로 처리합니다.asyncio.gather
를 사용해 작업을 한 번에 실행합니다.process_url
함수fetch_page
와parse_and_save
를 결합한 작업을 수행합니다. 단일 URL의 전체 처리 과정을 캡슐화합니다.
실행 결과 예시
crawl_results.txt
에는 다음과 같은 데이터가 저장됩니다:
URL: https://example.com
Title: Example Domain
URL: https://example.org
Title: Example Domain
URL: https://example.net
Title: Example Domain
성능 향상을 위한 팁
- 병렬 작업 수 제한
많은 URL을 크롤링할 경우, 서버에 과부하를 주지 않도록 병렬 작업 수를 제한합니다.
semaphore = asyncio.Semaphore(10)
async def limited_process_url(semaphore, session, url, output_file):
async with semaphore:
await process_url(session, url, output_file)
- 재시도 기능 추가
일부 요청이 실패할 경우 재시도하는 로직을 추가하면 신뢰성이 향상됩니다.
주의 사항
- 합법성 확인
Web 크롤러를 운영할 때는 대상 사이트의robots.txt
나 이용 약관을 준수해야 합니다. - 에러 처리 철저
네트워크 에러나 HTML 파싱 에러를 적절히 처리하여 크롤러 동작이 중단되지 않도록 설계합니다. - 타임아웃 설정
요청에 대한 타임아웃을 설정하여 무한히 대기하지 않도록 합니다.
async with session.get(url, timeout=10) as response:
비동기 Web 크롤러는 적절한 설계와 제어를 통해 효율적이고 확장 가능한 데이터 수집을 가능하게 합니다.
결론
본 기사에서는 Python의 async/await
구문을 활용한 비동기 처리에 대해 기본부터 응용까지 자세히 설명했습니다. 비동기 처리를 이해함으로써 I/O 바운드 작업을 효율화하고 애플리케이션 성능을 향상시킬 수 있습니다.
특히, asyncio
라이브러리의 기초 및 gather
와 wait
를 활용한 병렬 처리, 비동기 I/O의 구체적인 예시, 비동기 Web 크롤러 구축과 같은 응용 예제를 통해 실용적인 기술을 배웠습니다.
비동기 프로그래밍은 적절하게 설계하면 효율적이고 확장 가능한 시스템 구축을 지원하지만, 사용할 때는 예외 처리와 합법성에 대한 고려가 중요합니다. 이 기사를 참고하여 보다 실용적인 비동기 처리를 배우고 활용해 보세요.