파이썬(Python)에서는 내장 모듈만으로도 스택과 큐 자료구조를 아주 간단하게 구현할 수 있습니다. 스택(Stack)은 "나중에 들어간 것이 먼저 나온다"는 후입선출(LIFO, Last-In First-Out) 방식으로 동작하고, 큐(Queue)는 "먼저 들어간 것이 먼저 나온다"는 선입선출(FIFO, First-In First-Out) 방식으로 동작합니다. 파이썬이 제공하는 내장 함수를 활용하면 코드가 훨씬 짧고 단순해집니다.
queue 모듈이란?
파이썬의 queue 모듈은 멀티 프로듀서(multi-producer)와 멀티 컨슈머(multi-consumer) 큐를 구현한 모듈입니다. 특히 여러 스레드 사이에서 정보를 안전하게 교환해야 하는 스레드 프로그래밍 환경에서 그 진가를 발휘합니다. 이 모듈의 Queue 클래스는 필요한 모든 잠금(locking) 처리를 이미 내장하고 있어, 별도의 동기화 코드 없이도 스레드에 안전하게 사용할 수 있습니다.
queue 모듈은 항목을 꺼내는 순서만 다른 세 가지 종류의 큐를 제공합니다.
- FIFO 큐: 가장 먼저 추가된 작업이 가장 먼저 꺼내집니다.
- LIFO 큐: 가장 최근에 추가된 항목이 가장 먼저 꺼내집니다(스택처럼 동작).
- 우선순위 큐(Priority Queue): heapq 모듈을 사용해 항목이 정렬된 상태로 유지되며, 값이 가장 작은 항목이 먼저 꺼내집니다.
queue 모듈의 클래스와 예외
queue.Queue(maxsize=0)
FIFO 큐를 위한 생성자입니다. maxsize 인자는 큐에 넣을 수 있는 항목 수의 상한을 지정하는 정수입니다. 큐가 이 크기에 도달하면 새 항목의 삽입은 기존 항목이 소비될 때까지 차단(block)됩니다. maxsize가 0 이하이면 큐의 크기는 무한대가 됩니다.
queue.LifoQueue(maxsize=0)
LIFO 큐(스택)를 위한 생성자입니다. maxsize 인자는 큐에 넣을 수 있는 항목 수의 상한을 지정하는 정수이며, 크기에 도달하면 삽입이 차단됩니다. maxsize가 0 이하이면 큐의 크기는 무한대입니다.
queue.PriorityQueue(maxsize=0)
우선순위 큐를 위한 생성자입니다. maxsize 인자는 큐에 넣을 수 있는 항목 수의 상한을 지정하는 정수이며, 크기에 도달하면 삽입이 차단됩니다. maxsize가 0 이하이면 큐의 크기는 무한대입니다.
예외: queue.Empty와 queue.Full
queue.Empty: 비어 있는 Queue 객체에 블로킹 없는 get()(또는 get_nowait())을 호출했을 때 발생하는 예외입니다.
queue.Full: 가득 찬 Queue 객체에 블로킹 없는 put()(또는 put_nowait())을 호출했을 때 발생하는 예외입니다.
Queue 객체의 주요 메서드
qsize()
큐에 현재 들어 있는 항목 수의 근삿값을 반환합니다.
empty()
큐가 비어 있으면 True, 아니면 False를 반환합니다. 다만 empty()가 True를 반환했다고 해서 이후 put() 호출이 반드시 블로킹되지 않는다는 보장은 없으며, False를 반환했다고 해서 이후 get() 호출이 반드시 블로킹되지 않는다는 보장도 없습니다.
full()
큐가 가득 차 있으면 True, 아니면 False를 반환합니다. 마찬가지로 full()의 결과는 이후 get()이나 put() 호출의 블로킹 여부를 보장하지 않습니다.
put(item[, block[, timeout]])
큐에 항목을 넣습니다. 선택 인자 block이 True이고 timeout이 None(기본값)이면, 빈 슬롯이 생길 때까지 필요한 만큼 블로킹됩니다. timeout이 양수이면 최대 timeout초까지만 블로킹되고, 그 시간 안에 빈 슬롯이 생기지 않으면 Full 예외가 발생합니다. block이 False이면 즉시 빈 슬롯이 있을 때만 항목을 넣고, 그렇지 않으면 Full 예외를 발생시킵니다(이 경우 timeout은 무시됩니다).
get([block[, timeout]])
큐에서 항목을 하나 꺼내 제거한 뒤 반환합니다. block이 True이고 timeout이 None이면, 항목이 생길 때까지 필요한 만큼 블로킹됩니다. timeout이 양수이면 최대 timeout초까지만 기다리고, 그 안에 항목이 없으면 Empty 예외가 발생합니다. block이 False이면 즉시 사용 가능한 항목이 있을 때만 반환하고, 없으면 Empty 예외를 발생시킵니다(이 경우 timeout은 무시됩니다).
task_done()
이전에 큐에 넣었던 작업이 완료되었음을 알립니다. 큐를 소비하는(consumer) 스레드에서 사용합니다. 작업을 가져오기 위해 get()을 호출할 때마다, 이어지는 task_done() 호출은 해당 작업의 처리가 끝났음을 큐에 알려줍니다.
현재 join()이 블로킹 중이라면, 큐에 넣은 모든 항목에 대해 task_done()이 호출되어 처리가 완료되는 순간 join()의 블로킹이 해제됩니다.
큐에 넣은 항목 수보다 많이 호출하면 ValueError가 발생합니다.
join()
큐의 모든 항목이 꺼내져 처리될 때까지 블로킹됩니다.
완료되지 않은 작업 수는 항목이 큐에 추가될 때마다 1씩 증가하고, 소비자 스레드가 task_done()을 호출해 항목을 꺼냈으며 모든 작업이 끝났음을 알릴 때마다 1씩 감소합니다. 이 카운트가 0이 되면 join()의 블로킹이 해제됩니다.
예제 1: 기본적인 FIFO 큐 사용
import queue # 큐의 최대 용량을 40으로 설정 Q = queue.Queue(maxsize=40) Q.put(50) Q.put(90) Q.put(10) Q.put(70) print(Q.get()) print(Q.get()) print(Q.get()) print(Q.get())
실행 결과:
50 90 10 70
항목이 넣은 순서 그대로 출력되므로, FIFO(선입선출) 방식으로 동작함을 확인할 수 있습니다.
예제 2: 오버플로우와 언더플로우 확인
import queue
Q = queue.Queue(maxsize=30)
print(Q.qsize())
Q.put(50)
Q.put(90)
Q.put(10)
Q.put(70)
print("Full: ", Q.full())
Q.put(90)
Q.put(100)
print("Full: ", Q.full())
print(Q.get())
print(Q.get())
print(Q.get())
print("Empty: ", Q.empty())
print(Q.get())
print(Q.get())
print(Q.get())
print("Empty: ", Q.empty())
print("Full: ", Q.full())
실행 결과:
0 Full: False Full: False 50 90 10 Empty: False 70 90 100 Empty: True Full: False
maxsize가 30이므로 6개의 항목을 넣어도 큐가 가득 차지 않아 full()은 False를 반환하고, 모든 항목을 꺼낸 후에는 empty()가 True를 반환합니다.
예제 3: LifoQueue로 스택 구현하기
import queue
S = queue.LifoQueue(maxsize=10)
# qsize()는 현재 큐에 들어 있는 항목 수를 반환
print(S.qsize())
S.put(50)
S.put(90)
S.put(10)
S.put(70)
S.put(90)
S.put(10)
print("Full: ", S.full())
print("Size: ", S.qsize())
# 데이터는 큐와 반대 순서(역순)로 꺼내짐
print(S.get())
print(S.get())
print(S.get())
print(S.get())
print(S.get())
print("Empty: ", S.empty())
실행 결과:
0 Full: False Size: 6 10 90 70 10 90 Empty: False
LifoQueue는 가장 나중에 넣은 항목부터 꺼내므로, 일반 큐와 반대되는 역순으로 데이터가 출력됩니다. 5번의 get() 후에도 항목이 하나 남아 있어 empty()는 False를 반환합니다.
마무리
파이썬의 queue 모듈을 활용하면 스레드에 안전한 FIFO 큐, LIFO 스택, 우선순위 큐를 직접 구현하지 않고도 바로 사용할 수 있습니다. 특히 멀티스레딩 환경에서 생산자-소비자(Producer-Consumer) 패턴을 구현할 때 매우 유용합니다. 참고로 비동기 프로그래밍을 위해서는 Python 3.7부터 제공되는 asyncio.Queue를, 스레드 동기화가 필요 없는 단순한 상황이라면 collections.deque를 사용하는 것도 좋은 대안입니다.