코루틴
- 코루틴은 비동기 프로그래밍의 핵심 개념으로, 함수의 실행을 일시 중지했다가 필요 시 다시 이어서 실행할 수 있는 특별한 함수
- Python에서
async
와await
키워드를 사용하여 코루틴을 정의하며, I/O 대기 중에도 다른 작업을 처리할 수 있게 해 CPU의 유휴 시간을 줄인다
예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
async def fetch_data():
print("데이터를 가져오는 중...")
await asyncio.sleep(2) # 2초 동안 대기
print("데이터 가져오기 완료!")
return {"data": "샘플 데이터"}
async def main():
result = await fetch_data()
print(result)
asyncio.run(main())
-
await
키워드를 통해asyncio.sleep
동안 다른 작업으로 전환할 수 있습니다. - 비동기 작업의 의미
await asyncio.sleep(2)
는 2초 동안 아무 작업도 하지 않고 기다리는 코드- 이때
await
키워드가 없으면 2초 동안 프로그램이 멈춘 상태로 다른 작업을 전혀 하지 못하지만,await
를 사용하면 다른 작업을 병렬로 수행할 수 있음. 즉,await
는 비동기적인 pause 라고 보면 됨
- 동기와 비동기의 차이:
await
가 없다면 프로그램이 2초 동안 멈추지만,await
를 사용하면 2초를 기다리는 동안 다른 비동기 함수가 실행될 기회를 얻게 됨- 예를 들어, 여러 코루틴을 실행하면서
await
가 있는 부분에서 잠시 멈췄다가 다른 작업을 하게 되므로 동시에 여러 작업을 효율적으로 처리할 수 있다
비동기 코루틴 과 이벤트 루프: 비동기 작업은 어떻게 이루어지는가
- 이벤트 루프
- 모든 비동기 작업을 관리한다. 현재 대기 중인 작업이 있는지 주기적으로 확인하고, 어떤 작업이 완료될 때까지 대기할 필요가 있을 때 다른 작업으로 전환할 수 있게 해준다.
await
로 대기 상태에 들어가면 이벤트 루프는 다른 작업을 우선 처리하여 CPU가 유휴 상태로 멈추지 않도록 최적화
- 첫 번째 요청이 중지되는 순간 (await 키워드)
- 첫 번째 요청이 await을 만나 대기 → 이벤트 루프가 두 번째 요청으로 전환.
- 요청 대기: 첫 번째 요청이
await
를 만나게 되면, 그 작업은 일시적으로 대기 상태로 들어감. 이때 이벤트 루프는 첫 번째 요청의 현재 상태를 저장해둔다. - 상태 저장: 이 저장된 상태는 프로그램 메모리 내에 보관되고, 이벤트 루프는 첫 번째 요청이 대기 상태로 들어간 순간부터 다음 대기 중인 작업(두 번째 요청)을 수행함
- 두 번째 요청이 작업을 수행하는 방식
- 첫 번째 요청의 대기가 끝날 때 까지 두 번째 요청이 작업을 수행
- 작업 전환: 첫 번째 요청이 대기 상태가 되면, 이벤트 루프는 대기 중인 두 번째 요청을 처리하기 시작
- 동시 실행: 두 번째 요청이 실행되는 동안 첫 번째 요청은 I/O 작업을 기다리고 있는 상태이므로, 두 요청이 마치 동시에 실행되는 것처럼 처리된다
- 첫 번째 요청이 완료되는 순간
- 첫 번째 요청이 대기 중이던 작업(I/O 작업 등)을 완료하면, 이벤트 루프는 이를 감지하고, 첫 번째 요청으로 다시 돌아가 그 후속 작업을 처리하게 합니다.
- 작업 복귀: 첫 번째 요청의 작업을 이어서 완료하고, 응답을 반환한 후 이벤트 루프는 다시 두 번째 요청으로 돌아와 남은 작업을 처리한다.
FastAPI
- FastAPI는 Python의 (*)ASGI 서버와 함께 코루틴을 활용해 단일 스레드에서 다수의 비동기 요청을 동시에 처리
-
이를 통해 요청이
await
상태에 들어가면 다른 요청을 처리할 수 있어 고성능 API를 구축할 수 있다 - 예제:
1 2 3 4 5 6 7
from fastapi import FastAPI app = FastAPI() @app.get("/hello") async def say_hello(): return {"message": "Hello, 비동기 처리 완료!"}
- Spring Boot + Apache 서버, WSGI gunicorn
- 스레드풀 방식: 요청이 들어오면 스레드풀에서 하나의 스레드를 할당해 해당 요청을 처리
- 작업 중인 스레드: 할당된 스레드는 요청이 끝날 때까지 다른 요청을 처리하지 않으며, 작업이 끝나면 스레드풀에 반환된다. 이를 통해 각 스레드는 고유하게 할당된 작업을 완료할 때까지 기다려야 한다는 것을 알 수 있음.
- FastAPI (비동기 방식)
- 단일 스레드: 같은 스레드에서 여러 비동기 요청을 받는다.
- 비동기 방식의 핵심은 요청이 대기 상태에 들어갈 때만 스레드를 다른 요청으로 전환한다는 점
- 요청 대기와 전환: 첫 번째 요청이
await
로 인해 대기 상태가 되면, 다른 요청을 즉시 전환해 처리. 이후 첫 번째 요청이 준비되면 다시 스레드로 돌아와 처리를 완료하고 응답한다 - 이로써 FastAPI는 비동기적으로 작업을 처리해 I/O 작업에 의한 지연을 줄이고, 하나의 스레드에서 여러 요청을 동시에 관리할 수 있게 된다.
(*)ASGI 서버
- ASGI (Asynchronous Server Gateway Interface)
- 비동기 Python 웹 애플리케이션과 서버 간의 표준 인터페이스
- 특징: ASGI는 HTTP뿐 아니라 WebSocket과 같은 비동기 프로토콜을 지원해 Django나 Flask보다 실시간 처리에 유리
- 용도: FastAPI나 Starlette 같은 비동기 프레임워크와 Uvicorn 같은 비동기 서버가 ASGI를 통해 상호작용할 수 있음
- WSGI와 ASGI의 차이: WSGI는 동기 웹 요청을 처리하는 데 적합하며, ASGI는 WebSocket과 같은 비동기 프로토콜을 지원하여 실시간 처리에 유리.
고루틴
- 차이점: Go의 고루틴은 Python의 코루틴과 유사한 개념이지만, 스레드보다 가볍게 설계되어 수십만 개의 고루틴을 효율적으로 실행
- 고루틴은 메모리를 적게 사용하며 Go 런타임이 관리하는 동적 스케줄링 덕분에 컨텍스트 스위칭 비용이 낮고 동시성 처리에 유리
- 이러한 특징 덕분에 Go는 고성능, 고확장성을 요구하는 서버 애플리케이션 개발시 장점이 많다
장점
- 가벼운 메모리로 많은 개수를 동시에 생성하고 관리할 수 있음.
- 고효율 스케줄링으로 컨텍스트 스위칭 비용이 낮음.
- 동적 스택 크기와 Go 런타임 관리 덕분에 성능과 확장성에서 강점이 있음.
고루틴과 스레드의 차이
- 메모리 소비
- 스레드: 각 스레드는 보통 1MB 이상의 메모리 스택을 차지
- 고루틴: 고루틴은 시작할 때 기본 스택 크기가 2KB밖에 되지 않으며, 필요에 따라 동적으로 크기 조절
- 컨텍스트 스위칭 비용
- 스레드: OS 수준에서 스레드 간의 컨텍스트 스위칭이 일어나므로 비용이 많이 들며, CPU 리소스를 많이 사용
- 고루틴: Go 런타임이 사용자 모드에서 고루틴을 관리하므로, OS 스레드의 컨텍스트 스위칭보다 훨씬 저렴한 비용으로 전환
- 생성 및 관리
- 스레드: 스레드를 생성하거나 소멸할 때 시스템 리소스가 많이 소모
- 고루틴: 고루틴은 Go 런타임에서 쉽게 생성되고 관리되며, Go 애플리케이션은 수십만 개의 고루틴을 쉽게 다룰 수 있다
- 멀티플렉싱
- Go의 스케줄러는 고루틴을 적절히 분배하여 OS 스레드에 할당
- 이로 인해 다수의 고루틴이 소수의 OS 스레드에 효율적으로 분산되어 동작할 수 있다
자바와 코루틴
- Java는 본래 코루틴을 지원하지 않지만, 최근에는 Project Loom이라는 프로젝트를 통해 가상 스레드(Virtual Thread)라는 개념을 도입하여 코루틴과 유사한 동작을 지원하려는 움직임이 있다
- 가상 스레드는 전통적인 OS 스레드보다 훨씬 가벼워서, 수많은 가상 스레드를 만들어도 시스템에 큰 부담을 주지 않는다고 함
- 가상 스레드는 Java 런타임에서 관리하며, Java의 기존 동기 API(예: HTTP 요청, 데이터베이스 쿼리)와 호환되도록 설계됨
- 코루틴과 가상 스레드의 차이점
- 코루틴은 일반적으로 함수 수준에서 await 같은 키워드로 비동기 전환을 명시
- 가상 스레드는 동기식 코드 스타일로 동시성을 지원하므로, 기존 코드 변경 없이도 비동기 처리가 가능해짐
- 이외, Java에서 코루틴과 유사한 비동기 처리를 위해 Reactor나 CompletableFuture를 사용하는 방법도 있다
- Reactor: Spring WebFlux에서 사용하는 비동기 스트림 라이브러리로, 비동기 흐름을 쉽게 관리할 수 있다
- CompletableFuture: Java 8부터 제공된 비동기 API