[시리즈] 파이썬에서 여러 작업을 동시에 처리하기

파이썬 동시성 처리: 비동기, 스레딩, 멀티프로세싱 비교

Intro

오늘은 휴일이다. 황금같은 휴일 시간을 그냥 흘려보내고 싶지는 않다. 시간을 조금이라도 더 효율적으로 쓰고, 남는 시간에는 마음 편히 쉬려고 한다.

오늘 해야 할 일이 몇 가지 있다. 세탁기를 돌려야 하고, 청소도 해야 한다. 샤워도 해야 하고, 밀려있던 공부도 하려고 한다. 이 모든 일을 마친 뒤에는 꿀같은 휴식을 취할 예정이다.

그렇다면 조금이라도 더 빨리 쉬기 위해서는 이 일들을 어떻게 처리해야 할까? 이 상황을 통해 동기와 비동기의 개념을 이해해보겠다.

1. 동기

(1) 동기의 개념

먼저 동기 방식 (Synchronous) 부터 살펴보자.

동기란, 어떤 작업을 시작하면 그 작업이 끝날 때까지 기다린 뒤 다음 작업을 진행하는 방식을 가리킨다. 즉, 하나의 일이 완료되어야만 다음 일을 시작할 수 있는 것이다.

앞서 예시로 든 휴일의 할 일들을 동기 방식으로 처리한다고 생각해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
(1) 먼저 세탁기를 돌린다.
    그리고 세탁이 끝날 때까지 아무것도 하지 않고 기다린다.  
    
(2) 세탁이 끝나면 그제서야 청소를 시작한다.
    로봇청소기에게 청소를 맡겼지만, 청소가 끝날 때까지 또 기다린다.  
    
(3) 청소가 끝나면 샤워를 하고, 샤워가 끝난 뒤에는 공부를 시작한다.  

그리고 공부까지 모두 끝마친 후에야 드디어 휴식을 취할 수 있다.  

(2) 동기 방식의 코드

  • 이 동기 방식을 파이썬 코드로 작성해보면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
start = time.time()

def time_log(msg):
    print(f"{time.time()-start:4.1f}초 :: {msg}")

def run_washing_machine():
    time_log("세탁기 시작")
    time.sleep(4)
    time_log("세탁기 종료")
    
def run_robot_vacuum():
    time_log("로봇청소기 시작")
    time.sleep(3)
    time_log("로봇청소기 종료")

def take_shower():
    time_log("샤워 시작")
    time.sleep(2)
    time_log("샤워 종료")

def study():
    time_log("공부 시작")
    time.sleep(2)
    time_log("공부 종료")

def main():
    run_washing_machine()
    run_robot_vacuum()
    take_shower()
    study()
    print(f"\n[최종 종료 시각] {time.time() - start:4.1f}초")

main()
  • 실행 결과
1
2
3
4
5
6
7
8
9
10
 0.0초 :: 세탁기 시작
 4.0초 :: 세탁기 종료
 4.0초 :: 로봇청소기 시작
 7.0초 :: 로봇청소기 종료
 7.0초 :: 샤워 시작
 9.0초 :: 샤워 종료
 9.0초 :: 공부 시작
11.0초 :: 공부 종료

[최종 종료 시각] 11.0초

(3) 동기 방식에 대한 평가

이 방식은 흐름이 단순하고 이해하기 쉽다는 장점이 있다. 작업이 순서대로 진행되기 때문에, 결과를 예측하기도 쉽다.

하지만 단점도 있다. 세탁기나 로봇청소기처럼 내가 직접 계속 신경 쓰지 않아도 되는 작업을, 그 작업이 끝날 때까지 가만히 기다려야만 한다는 점이다. 그 시간 동안 다른 일을 할 수 있음에도 불구하고 아무것도 하지 못하고 비효율적으로 시간을 보내게 된다.

즉, 동기 방식은 작업의 순서는 명확하지만, 굳이 직접 처리하지 않아도 되는 작업에 대한 대기 시간이 길어질 수 있는 방식인 것이다.


2. 비동기

(1) 비동기의 개념

비동기 방식이란, 어떤 작업을 시작한 뒤 그 작업이 끝날 때까지 기다리지 않고 다음 작업을 진행하는 방식이다. 작업이 완료되면, 그 때 결과를 확인하거나 후속 작업을 처리하게 된다.

휴일의 할 일을 다시 예로 들어보면 다음과 같다.

1
2
3
4
5
6
7
(1) 먼저 세탁기를 돌린다. 
    세탁기는 알아서 작동하므로, 세탁이 끝날 때까지 가만히 기다릴 필요가 없다.  
    
(2) 세탁기를 작동시킨 뒤 로봇청소기를 작동시킨다.
    청소 역시 로봇청소기가 알아서 하므로, 청소가 끝날 때까지 기다리지 않아도 된다.  
    
(3) 그 사이에 나는 샤워를 하고, 샤워를 마친 뒤에는 공부를 시작한다.  

공부하는 동안 세탁이 끝났다는 알림이 올 수도 있고, 로봇청소기가 청소를 마쳤다는 알림이 올 수도 있다. 그러면 적절한 시점에 빨래를 널거나 청소 상태를 확인하면 된다.

비동기 방식은, 내가 직접 붙잡고 있지 않아도 되는 작업을 먼저 실행해두고, 그 시간 동안 나는 다른 일을 처리하는 방식인 것이다. 이렇게 하면 전체 시간을 훨씬 효율적으로 사용할 수 있다.

(2) 비동기 방식의 코드

  • 비동기 방식을 파이썬 코드로 작성해보면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import time
import asyncio

start = time.time()

def time_log(msg):
    print(f"{time.time()-start:4.1f}초 :: {msg}")

async def run_washing_machine(): # 비동기로 처리하고 싶은 함수 앞에는 async 키워드를 붙인다.
    time_log("세탁기 시작")
    await asyncio.sleep(4)       # await : 이 비동기 작업이 끝날 때까지 기다리되, 프로그램 전체의 다른 실행 흐름을 멈추지는 말라
    time_log("세탁기 종료")
    
async def run_robot_vacuum():
    time_log("로봇청소기 시작")
    await asyncio.sleep(3)       # time 함수는 동기이므로 블로킹이 발생한다. 블로킹을 예방하려면 asyncio.sleep 을 사용한다.
    time_log("로봇청소기 종료")

async def take_shower():
    time_log("샤워 시작")
    await asyncio.sleep(2)
    time_log("샤워 종료")

async def study():
    time_log("공부 시작")
    await asyncio.sleep(2)
    time_log("공부 종료")

async def do_my_work(): # 동기로 처리할 두 개의 작업은 하나의 흐름으로 묶음
    await take_shower()
    await study()

async def main():
    await asyncio.gather( # gather : 비동기 처리로 같이 시작할 작업 묶음 --> 이 작업들을 동시에 시작하고, 전부 끝날 때까지 기다려라
        run_washing_machine(),
        run_robot_vacuum(),
        do_my_work()
    )
    print(f"\n[최종 종료 시각] {time.time() - start:4.1f}초")

asyncio.run(main()) # 비동기 함수를 실행시키는 진입점에는 asyncio.run(함수)
  • 실행 결과
1
2
3
4
5
6
7
8
9
10
 0.0초 :: 세탁기 시작
 0.0초 :: 로봇청소기 시작
 0.0초 :: 샤워 시작
 2.0초 :: 샤워 종료
 2.0초 :: 공부 시작
 3.0초 :: 로봇청소기 종료
 4.0초 :: 세탁기 종료
 4.0초 :: 공부 종료

[최종 종료 시각]  4.0초

(3) 비동기 방식에 대한 평가

비동기 방식의 가장 큰 장점은 대기 시간을 효율적으로 사용할 수 있다는 점이다.

동기 방식에서는 각 작업을 순서대로 처리했기 때문에 11초가 걸렸다. 반면, 비동기 방식에서는 세탁기와 로봇청소기를 먼저 실행해두고, 그 사이에 샤워와 공부를 진행했기 때문에 전체 작업 시간이 4초로 줄어들었다.

즉, 비동기 방식은 내가 직접 붙잡고 있지 않아도 되는 작업을 기다리는 동안 다른 일을 처리할 수 있게 해준다. 서버 API 요청, 파일 읽기, 데이터베이스 조회처럼 결과가 나오기까지 시간이 걸리는 작업에 적합한 방식이다.

하지만 비동기가 항상 좋은 것은 아니다. 작업이 끝난 뒤 무엇을 해야 하는지, 그리고 어떤 작업이 먼저 끝나야 하는지를 잘 관리해야 한다. 예를 들어 세탁이 끝났는데 빨래를 널지 않으면, 세탁은 끝났어도 할 일이 완전히 끝난 것은 아니다.


3. 동기와 비동기의 차이

동기와 비동기의 가장 큰 차이점은 바로 기다리는 방식에 있다.

동기는 작업이 끝날 때까지 기다리고, 비동기는 작업을 시작해두고 기다리는 동안 다른 일을 한다. 동기 방식은 단순하고 직관적이나, 오래 걸리는 작업이 있다면 전체 흐름이 막히고 작업시간이 길어질 수 있다.

반면 비동기 방식은 대기 시간을 효율적으로 사용할 수 있다는 장점이 있지만, 여러 작업이 동시에 진행되기 때문에 작업 완료 시점과 후속 처리를 잘 관리해야 한다.


4. 그렇다면 비동기가 항상 좋을까?

(1) 결론 - 비동기가 항상 좋은 것은 아니다.

결론부터 말하면, 비동기가 항상 좋은 것은 아니다. 비동기는 "기다리는 시간을 효율적으로 쓰기 위한 방식"이지, 모든 상황에서 동기보다 우월한 방식은 아니다.

비동기는 여러 작업을 동시에 진행하듯 처리할 수 있기 때문에, 기다리는 시간을 효율적으로 사용할 수 있다. 하지만 그만큼 작업의 흐름을 관리하기가 어려워질 수 있고, 작업 완료 시점을 잘못 다루면 예기치 않은 문제가 발생할 수도 있다.

예를 들어, 세탁기를 돌려놓고, 로봇청소기를 작동시키고, 샤워까지 했다고 가정해보자. 그런데 세탁이 끝났다는 알림을 놓치면 젖은 빨래가 세탁기 안에 방치되어 냄새가 날 수도 있다. 그리고, 로봇청소기가 중간에 에러로 멈췄는데 확인하지 못하면 청소가 제대로 끝나지 않을 수도 있다. 즉, 비동기 방식에서는 작업을 시작하는 것만큼이나 작업이 끝난 뒤 어떻게 처리할지가 중요한 것이다.

(2) 개발에서의 비동기

개발에서도 비슷한 일이 일어날 수 있다. 원격 서버에 데이터를 요청해놓고, 애플리케이션 코드에서는 다른 작업을 이어서 실행하도록 하는 것은 효율적일 것이다. 하지만, 서버에 요청한 데이터가 도착하기 전에 그 데이터가 필요한 코드를 먼저 실행해버리면 문제가 생기는 것이다.

때문에 동기 방식으로 처리해야 적절한 경우도 있다. 어떤 작업들은 반드시 순서대로 처리되어야 하는데, 예를 들어 “로그인” 작업이 그렇다. 사용자가 로그인을 해야만 사용자 정보를 가져올 수 있고, 사용자 정보가 있어야만 개인화된 화면을 보여줄 수 있는 경우이다.

1
2
3
로그인
→ 사용자 정보 조회
→ 개인화 화면 표시

이러한 작업을 무리하게 비동기로 처리하려고 하면 오히려 코드가 복잡해지고, 버그가 생기기 쉬워진다.

따라서 비동기는 시간이 오래 걸리는 작업에 대해, 그리고 이 작업을 기다리는 동안 다른 일을 할 수 있는 경우에 유용하다. 이러한 작업은 대표적으로 API 요청, 파일 읽기, 데이터베이스 조회, 타이머처럼 결과가 나올 때까지 시간이 걸리는 작업들이다.

반면, 작업의 순서가 중요하거나, 앞선 작업의 결과가 다음 작업에 반드시 필요한 경우에는 동기적인 흐름이 더 이해하기 쉽고 안전할 수 있다.

(3) 동기와 비동기 중 무얼 선택해야 할까?

결국 중요한 것은, “비동기냐 동기냐”가 아니라, “작업의 성격에 맞는 방식을 선택하는 것”이다.

비동기는 빠르게 보이기 위한 기술이 아니라, 기다리는 시간을 낭비하지 않기 위한 방식이다. 하지만 기다림을 없애는 대신, 완료 시점과 후속 처리를 관리해야 하는 책임이 생긴다.

따라서 비동기를 사용할 때는 항상 아래 세 가지 질문을 함께 생각해야 한다.

1
2
3
1. 이 작업을 기다리는 동안 다른 일을 해도 되는가?  
2. 이 작업의 결과가, 기다리는 동안 수행할 작업에 꼭 필요한가?
3. 작업이 끝난 뒤 무엇을 처리해야 하는가?

[시리즈] 파이썬에서 여러 작업을 동시에 처리하기

파이썬 동시성 처리: 비동기, 스레딩, 멀티프로세싱 비교

Comments