코루틴과 비동기 처리: 고성능 애플리케이션을 위한 필수 개념

Python과 Go에서의 코루틴 및 고루틴의 차이와 웹 프레임워크 활용법

Posted by Mihyun on October 30, 2024

코루틴

  • 코루틴은 비동기 프로그래밍의 핵심 개념으로, 함수의 실행을 일시 중지했다가 필요 시 다시 이어서 실행할 수 있는 특별한 함수
  • Python에서 asyncawait 키워드를 사용하여 코루틴을 정의하며, 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가 유휴 상태로 멈추지 않도록 최적화
  1. 첫 번째 요청이 중지되는 순간 (await 키워드)
    • 첫 번째 요청이 await을 만나 대기 → 이벤트 루프가 두 번째 요청으로 전환.
    • 요청 대기: 첫 번째 요청이 await를 만나게 되면, 그 작업은 일시적으로 대기 상태로 들어감. 이때 이벤트 루프는 첫 번째 요청의 현재 상태를 저장해둔다.
    • 상태 저장: 이 저장된 상태는 프로그램 메모리 내에 보관되고, 이벤트 루프는 첫 번째 요청이 대기 상태로 들어간 순간부터 다음 대기 중인 작업(두 번째 요청)을 수행함
  2. 두 번째 요청이 작업을 수행하는 방식
    • 첫 번째 요청의 대기가 끝날 때 까지 두 번째 요청이 작업을 수행
    • 작업 전환: 첫 번째 요청이 대기 상태가 되면, 이벤트 루프는 대기 중인 두 번째 요청을 처리하기 시작
    • 동시 실행: 두 번째 요청이 실행되는 동안 첫 번째 요청은 I/O 작업을 기다리고 있는 상태이므로, 두 요청이 마치 동시에 실행되는 것처럼 처리된다
  3. 첫 번째 요청이 완료되는 순간
    • 첫 번째 요청이 대기 중이던 작업(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 런타임 관리 덕분에 성능과 확장성에서 강점이 있음.

고루틴과 스레드의 차이

  1. 메모리 소비
    • 스레드: 각 스레드는 보통 1MB 이상의 메모리 스택을 차지
    • 고루틴: 고루틴은 시작할 때 기본 스택 크기가 2KB밖에 되지 않으며, 필요에 따라 동적으로 크기 조절
  2. 컨텍스트 스위칭 비용
    • 스레드: OS 수준에서 스레드 간의 컨텍스트 스위칭이 일어나므로 비용이 많이 들며, CPU 리소스를 많이 사용
    • 고루틴: Go 런타임이 사용자 모드에서 고루틴을 관리하므로, OS 스레드의 컨텍스트 스위칭보다 훨씬 저렴한 비용으로 전환
  3. 생성 및 관리
    • 스레드: 스레드를 생성하거나 소멸할 때 시스템 리소스가 많이 소모
    • 고루틴: 고루틴은 Go 런타임에서 쉽게 생성되고 관리되며, Go 애플리케이션은 수십만 개의 고루틴을 쉽게 다룰 수 있다
  4. 멀티플렉싱
    • Go의 스케줄러는 고루틴을 적절히 분배하여 OS 스레드에 할당
    • 이로 인해 다수의 고루틴이 소수의 OS 스레드에 효율적으로 분산되어 동작할 수 있다

자바와 코루틴

  • Java는 본래 코루틴을 지원하지 않지만, 최근에는 Project Loom이라는 프로젝트를 통해 가상 스레드(Virtual Thread)라는 개념을 도입하여 코루틴과 유사한 동작을 지원하려는 움직임이 있다
    • 가상 스레드는 전통적인 OS 스레드보다 훨씬 가벼워서, 수많은 가상 스레드를 만들어도 시스템에 큰 부담을 주지 않는다고 함
    • 가상 스레드는 Java 런타임에서 관리하며, Java의 기존 동기 API(예: HTTP 요청, 데이터베이스 쿼리)와 호환되도록 설계됨
  • 코루틴과 가상 스레드의 차이점
    • 코루틴은 일반적으로 함수 수준에서 await 같은 키워드로 비동기 전환을 명시
    • 가상 스레드는 동기식 코드 스타일로 동시성을 지원하므로, 기존 코드 변경 없이도 비동기 처리가 가능해짐
  • 이외, Java에서 코루틴과 유사한 비동기 처리를 위해 Reactor나 CompletableFuture를 사용하는 방법도 있다
    • Reactor: Spring WebFlux에서 사용하는 비동기 스트림 라이브러리로, 비동기 흐름을 쉽게 관리할 수 있다
    • CompletableFuture: Java 8부터 제공된 비동기 API