python 비동기

seongmin0302·2025년 5월 12일
0

plango 프로젝트

목록 보기
8/10

🟢Plango프로젝트에서는 아래의 TourAPI API 5개를 호출할 예정이다.

  • searchKeyword1
  • detailCommon1
  • detailIntro1
  • detailInfo1
  • detailImage1

🟢나는 아래와 같은 흐름으로 만들 예정이다.

  • searchKeyword1의 일부 정보는 프론트에 바로 전달한다.
  • 나머지 detailCommon1, detailIntro1, detailInfo1, detailImage1 4개의 TourAPI 응답은 합쳐서 프론트에게 전달된다.
  • 5개 TourAPI의 내용은 모두 합쳐져서 LLM에게 전달된다. 그후 LLM에 의해 글 생성된후 프론트에게 전달된다.

🟢어떻게 개발 하면 좋을지 정리해 보았다!

  1. 가장먼저 searchKeyword1이 호출된후 나머지 4개의 TourAPI API는 모두 비동기적으로 동시에 호출한다.(searchKeyword1의 contentid, contenttypeid가 나와야 나머지 4개를 호출 가능)
  2. API에서 받은 응답 중 LLM에 보낼 데이터 (detailCommon1, detailIntro1, detailInfo1, detailImage1, searchKeyword1)는 응답을 기다렸다가 합쳐서 한 번에 전달.
  3. 프론트는 searchKeyword1 결과 먼저 받아서 미리 렌더링하고, 나머지 정보(나머지 4개 api의 정보, LLM응답)는 조금 후에 렌더링 한다.

따라서 나는 python 비동기에 대해서 공부할 필요성을 느끼게 되었다!

python 비동기란

비동기(async) 란, 프로그램이 특정 작업을 기다리는 동안 다른 작업을 수행할 수 있도록 하는 것을 의미 합니다.

비동기 함수 선언: async def 를 사용하여 함수를 선언합니다. 이렇게 선언된 함수는 코루틴(coroutine) 을 반환합니다.

await 키워드: await 키워드는 코루틴의 실행을 일시 중지하고, 완료될 때까지 기다립니다. awaitasync 함수 내에서만 사용할 수 있습니다.

Q. await 키워드는 코루틴의 실행을 일시 중지하고, 완료될 때까지 기다린다?

A:

await 키워드는 비동기 함수(async def로 정의된 함수) 안에서 다른 비동기 작업이 끝날 때까지 기다리기 위해 사용됩니다.

1. 기본 개념

await는 "기다려!"라는 뜻입니다.
정확히 말하면, 비동기 작업이 완료될 때까지 다른 작업을 멈추고 기다리는 것입니다.
하지만 프로그램 전체가 멈추는 건 아니고, 현재 함수만 멈추고 나머지 작업은 계속 돌아갑니다.


2. 어디서 사용할까

await는 반드시 async로 정의된 함수 안에서만 쓸 수 있습니다.

async def my_func():
    result = await other_async_func()
  • async def: 비동기 함수 정의
  • await: 다른 비동기 함수(other_async_func())의 결과를 기다림

3. 예시

import asyncio

async def say_hello():
    print("Hello...")
    await asyncio.sleep(2)  # 2초 기다림
    print("...World!")

async def main():
    await say_hello()

asyncio.run(main())

실행 결과:

Hello...
(2초 후)
...World!
  • await asyncio.sleep(2)는 2초 동안 현재 함수를 멈춥니다.
  • 하지만 전체 프로그램이 멈추는 건 아닙니다. 그 사이 다른 비동기 작업(task)이 이미 실행 중이었다면, 그 작업이 계속 실행될 수 있어요.

Q. 전체 프로그램이 멈추는 건 아니다?

A:

여러 비동기 함수가 동시에 실행되는 상황에서 await어떻게 동작하는지 살펴보자!


예시1: 두 개의 비동기 함수가 "동시에" 실행되는 경우

import asyncio

async def cook_rice():
    print("🍚 밥 짓기 시작!")
    await asyncio.sleep(3)  # 3초 걸림
    print("✅ 밥 완성!")

async def boil_soup():
    print("🍲 국 끓이기 시작!")
    await asyncio.sleep(2)  # 2초 걸림
    print("✅ 국 완성!")

async def main():
    task1 = asyncio.create_task(cook_rice())
    task2 = asyncio.create_task(boil_soup())

    print("👉 요리 시작!")
    await task1
    await task2
    print("🍽️ 모든 요리 완료!")

asyncio.run(main())

실행 결과 예상

👉 요리 시작!
🍚 밥 짓기 시작!
🍲 국 끓이기 시작!
✅ 국 완성!       <-- 2초 후
✅ 밥 완성!       <-- 3초 후
🍽️ 모든 요리 완료!

설명

  • cook_rice()boil_soup()는 각각 await asyncio.sleep()으로 시간이 걸리는 작업을 시뮬레이션합니다.
  • 이 두 함수는 asyncio.create_task()동시에 실행되며, await는 각각의 작업이 끝날 때까지 기다립니다.
  • await자기 함수만 멈춘다.
  • cook_rice()await asyncio.sleep(3)에서 멈추는 동안, boil_soup()자기 일정을 계속 진행할 수 있습니다.
  • 즉, 두 함수가 서로의 기다림 시간을 활용해서 동시에 실행되고 있는 것입니다.

예시2: 만약 create_task()를 안 쓰고 순차적으로 await만 쓰면?

task1 = asyncio.create_task(cook_rice())
task2 = asyncio.create_task(boil_soup())

아래는 위 두 줄의 코드를 지운 코드입니다!

async def main():
    await cook_rice()   # 먼저 밥 짓기 끝까지 기다림 (3초)
    await boil_soup()   # 그 다음 국 끓임 (2초)

실행 순서:

boil_soup()은 밥이 다 끝나야 호출된다!!

🍚 밥 짓기 시작!
✅ 밥 완성!     <-- 3초
🍲 국 끓이기 시작!
✅ 국 완성!     <-- +2초 (총 5초 소요)

결과: 총 5초 걸림 (비효율적)


예시3: 반면 create_task()로 두함수 동시에 실행하면? (예시1코드)

🍚 밥 짓기 시작!
🍲 국 끓이기 시작!
✅ 국 완성!     <-- 2초
✅ 밥 완성!     <-- 3초 (총 3초 소요)

결과: 총 3초에 모든 작업 완료 (효율적)


  • await비동기 함수 안에서만 사용 가능하며, 해당 작업이 끝날 때까지 일시 중지합니다.
  • 하지만 전체 프로그램은 멈추지 않고, 그 사이 다른 비동기 작업(task)이 이미 실행 중이었다면, 그 작업이 계속 실행될 수 있어요.
  • create_task()를 쓰면 여러 비동기 작업을 병렬적으로 처리할 수 있습니다.

Q. await task1, await task2 ??

A:

await task1, await task2는 단순히 task1task2기다리는 용도로 사용됩니다.


task1 = asyncio.create_task(cook_rice())
task2 = asyncio.create_task(boil_soup())

await task1
await task2

여기서 핵심 키워드는 두 가지입니다:

  1. create_task()비동기 작업을 백그라운드에서 실행
  2. await그 작업이 끝날 때까지 기다림

그럼 왜 await task1을 써야 하나요?

task1 = asyncio.create_task(cook_rice())

이 줄은 cook_rice()라는 비동기 작업을 예약해서 실행만 합니다.
즉시 실행은 되지만, 결과를 기다리지 않고 main 함수는 다음 줄로 넘어가 버립니다.


그래서 await task1으로 결과를 기다려야

await task1

이 줄이 없으면, main() 함수는 작업이 끝나기를 기다리지 않고 그냥 종료될 수 있습니다.
즉, 프로그램이 요리가 끝나기 전에 끝나버릴 수도 있어요.

await task1은:

  • "task1이 완료될 때까지 기다려!"

라고 명령하는 것입니다.


예시: await를 생략했을 때의 문제

async def main():
    task1 = asyncio.create_task(cook_rice())
    task2 = asyncio.create_task(boil_soup())
    print("요리를 맡겨두고 그냥 끝내버리기!")  # main은 끝났지만 task는 아직...

실행하면, 아래처럼 완료 메시지가 안 뜰 수도 있어요:

🍚 밥 짓기 시작!
🍲 국 끓이기 시작!
요리를 맡겨두고 그냥 끝내버리기!

main()이 끝났기 때문에 프로그램이 종료되고, task1task2완료되지 못함.


  • await task1을 쓰는 이유는, 해당 비동기 작업이 정상적으로 끝날 때까지 기다리기 위해서입니다.
  • 만약 await를 생략하면 작업이 중간에 끊기거나, 예외가 누락될 수 있어 프로그램이 예상대로 작동하지 않습니다.
  • 동시에 여러 작업을 create_task()로 실행한 후, await로 하나씩 기다려서 완전한 작업 종료를 보장해야 합니다.

Q. 이렇게 하면 task1 끝내고 task2하는거 아닌가? 결국 동기아냐?

await task1await task2 이렇게 하면 형식적으로는 순차적으로 기다리는 것처럼 보입니다.
하지만 중요한 건, task1, task2를 이미 동시에 시작해두었다는 점입니다.


예제1

task1 = asyncio.create_task(cook_rice())
task2 = asyncio.create_task(boil_soup())

await task1
await task2

❗ 여기서 중요한 포인트:

  • create_task()동시에 실행 시작
  • await task1task1의 완료를 기다림
  • task2는 그동안 뒤에서 계속 실행 중
  • await task2는 남은 시간만 기다리면 됨 (이미 다 끝났다면 바로 넘어감)

예제2: 타이머로 비교

import asyncio

async def timer(name, delay):
    print(f"⏱️ {name} 시작")
    await asyncio.sleep(delay)
    print(f"✅ {name} 완료 ({delay}초)")

async def main():
    task1 = asyncio.create_task(timer("타이머1", 3))
    task2 = asyncio.create_task(timer("타이머2", 2))

    await task1  # ⏳ 타이머1 끝날 때까지 기다림 (task2도 이때 계속 실행 중!)
    await task2  # task2는 이미 끝났거나 거의 끝나서 바로 넘어감

asyncio.run(main())

실행 순서

⏱️ 타이머1 시작
⏱️ 타이머2 시작
✅ 타이머2 완료 (2초)
✅ 타이머1 완료 (3초)

해설

  • create_task()로 두 작업을 동시에 시작
  • await task1: 타이머1이 3초 동안 실행됨 → 그 사이에 타이머2도 알아서 실행됨
  • 타이머2는 2초 만에 끝나버림 → await task2 할 때는 이미 끝났기 때문에 즉시 통과

예시3: create_task()를 안 쓰고 순차적으로 await만 쓰면?

await timer("타이머1", 3)
await timer("타이머2", 2)

실행 결과:

⏱️ 타이머1 시작
✅ 타이머1 완료 (3초)
⏱️ 타이머2 시작
✅ 타이머2 완료 (2초)

총 5초 걸림 (비효율적)


await task1await task2는 순차 실행이 아니라 순차적으로 완료를 기다리는 것입니다.
이미 시작한 작업들이기 때문에, await는 그 작업의 남은 시간만 기다리는 역할이에요.

Q. asyncio.create_task() 방식과 asyncio.gather()방식?

A:

asyncio.create_task() 방식과 asyncio.gather() 방식은 겉보기엔 비슷해 보이지만, 내부적으로 동작 방식과 용도에 차이가 있어요.


먼저 공통점부터

두 방식 모두 "비동기 함수 여러 개를 동시에 실행"할 수 있습니다.
즉, task(1), task(2), task(3)이 순서대로 실행되는 게 아니라, 거의 동시에 시작되고 각자의 sleep() 시간이 지난 뒤 종료됩니다.


asyncio.create_task() 방식 설명

task1 = asyncio.create_task(say_hello())
task2 = asyncio.create_task(say_hi())
await task1
await task2

이 방식은 두 단계로 나뉩니다:

1. create_task()는 "작업을 이벤트 루프에 등록해서 백그라운드에서 실행"하게 합니다.

즉시 실행되도록 예약만 해두는 거예요.
이때 함수는 바로 실행되기 시작하지만, 그 결과는 아직 기다리지 않아요.

2. await task1, await task2그 작업이 끝나기를 기다리는 단계입니다.

즉, 각각의 작업이 끝나야 다음 줄로 넘어갈 수 있어요.

이 방식의 특징은 각 작업을 개별적으로 추적할 수 있다는 점이에요.
예를 들어 중간에 취소하거나, 결과값을 따로 관리하거나, 순서를 바꾸는 게 가능합니다.


asyncio.gather() 방식 설명

await asyncio.gather(
    task(1),
    task(2),
    task(3)
)

gather()는 내부적으로 아래처럼 작동합니다:

  1. task(1), task(2), task(3)동시에 실행합니다.
  2. 이 함수들의 모든 실행이 끝날 때까지 기다립니다.
  3. 각각의 결과를 리스트로 반환합니다 (필요하면).

즉, "하나의 묶음"처럼 동작해요. 이 묶음 전체가 끝나야 다음 코드로 넘어갈 수 있습니다.

이 방식은 단순히 동시에 여러 코루틴을 실행하고, 결과를 함께 기다릴 때 아주 간단하고 직관적이에요.


다시한번 정리하면

create_task()

  • 비동기 작업을 개별적으로 등록하고 관리할 수 있다.
  • 작업 객체(Task)를 변수로 들고 있어서 중간에 취소하거나 예외 처리도 개별로 가능.
  • 작업을 등록만 해두고, 언제 await할지는 개발자가 정할 수 있다.

gather()

  • 여러 작업을 한번에 실행하고, 전체가 끝날 때까지 한꺼번에 기다린다.
  • 결과도 한꺼번에 모아서 리턴된다.

비유로 정리하면:

  • create_task()여러 배달 주문을 따로 걸어두고, 각 배달이 도착하면 하나씩 확인하는 방식.
  • gather()세 개를 한꺼번에 시키고, 전부 도착할 때까지 문 앞에서 기다리는 방식.

언제 어떤 걸 쓰면 좋을까?

  • 개별 작업을 따로 취소하거나, 결과를 개별로 추적해야 할 때create_task()
  • 단순히 동시에 여러 작업을 실행하고 모두 끝나길 기다리기만 하면 될 때gather()




아래 블로그도 읽어보니 많은 도움이 되었다!

profile
컴튜터공학과 재학중

0개의 댓글