Queue란 무엇인가?
Java에서 Queue는 데이터 구조의 일종으로, 데이터를 선입선출(FIFO, First-In-First-Out) 방식으로 처리합니다. 이 글에서는 Queue의 기본 개념부터 자바에서 제공하는 다양한 Queue 인터페이스와 구현 클래스까지 심도 있게 설명하고, 코드 예제들을 통해 Queue의 활용법을 명확히 전달하겠습니다.
Queue는 대기열이라는 의미로, 데이터를 순차적으로 처리하는 자료구조입니다. Stack이 마지막에 들어온 데이터부터 처리하는 LIFO(Last-In-First-Out) 구조라면, Queue는 처음에 들어온 데이터부터 처리하는 FIFO(First-In-First-Out) 구조입니다. 이는 우리가 일상생활에서 사용하는 줄 서기와 같은 방식입니다. 예를 들어, 티켓을 구입하기 위해 줄을 서면 먼저 줄을 선 사람부터 먼저 티켓을 받게 되죠.
이러한 구조는 다음과 같은 경우에 유용합니다.
- 작업 처리 순서가 중요한 경우 (예: 프린터 작업 큐)
- 비동기 작업의 순차적 처리 (예: 메시지 대기열)
자바에서 Queue 인터페이스는 java.util 패키지에 포함되어 있으며, 다양한 클래스들이 이 인터페이스를 구현하고 있습니다.
자바에서 제공하는 Queue 인터페이스
자바의 Queue 인터페이스는 컬렉션 프레임워크의 일환으로, 다음과 같은 주요 메서드를 제공합니다.
- add(E e): 요소를 큐의 끝에 추가합니다. 큐가 가득 차면 IllegalStateException을 발생시킵니다.
- offer(E e): 요소를 큐의 끝에 추가합니다. 큐의 크기에 제한이 있을 때 공간이 없으면 false를 반환합니다.
- remove(): 큐의 앞에 있는 요소를 제거하고 반환합니다. 큐가 비어있으면 NoSuchElementException을 발생시킵니다.
- poll(): 큐의 앞에 있는 요소를 제거하고 반환합니다. 큐가 비어있으면 null을 반환합니다.
- element(): 큐의 앞에 있는 요소를 반환합니다. 큐가 비어있으면 NoSuchElementException을 발생시킵니다.
- peek(): 큐의 앞에 있는 요소를 반환합니다. 큐가 비어있으면 null을 반환합니다.
- size(): 큐에 있는 요소의 개수를 반환합니다.
- isEmpty(): 큐가 비어있는지 여부를 확인하여, 비어있으면 true를 반환하고 그렇지 않으면 false를 반환합니다.
- clear(): 큐의 모든 요소를 제거합니다.
이러한 메서드들은 상황에 맞게 사용되며, 예외 처리를 유연하게 할 수 있는 offer(), poll(), peek() 등을 사용할 수 있는 점이 큰 장점입니다.
Queue의 구현 클래스들
자바에서는 Queue 인터페이스를 구현한 여러 가지 클래스들이 있습니다. 이 중 자주 사용되는 몇 가지를 소개하겠습니다.
LinkedList
LinkedList 클래스는 Queue와 Deque 인터페이스를 구현하며, 양방향 연결 리스트를 기반으로 요소를 관리합니다. LinkedList는 큐의 앞과 뒤에서의 삽입 및 삭제가 빠르기 때문에, 대기열이나 작업 큐의 역할을 할 때 적합합니다.
Queue<String> queue = new LinkedList<>();
queue.offer("첫 번째 요소");
queue.offer("두 번째 요소");
System.out.println(queue.poll()); // 출력: 첫 번째 요소
System.out.println(queue.peek()); // 출력: 두 번째 요소
JavaArrayDeque
ArrayDeque 클래스는 Queue와 Deque 인터페이스를 구현하며, 배열을 사용하여 큐를 구현한 클래스입니다. ArrayDeque는 스택과 큐 양쪽의 기능을 모두 제공하며, null 요소를 허용하지 않고, 동적 배열을 사용하여 효율적으로 요소를 관리합니다. 자바에서 ArrayDeque는 LinkedList보다 빠른 성능을 제공하기 때문에 스택과 큐의 대체로 자주 사용됩니다.
Queue<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
System.out.println(queue.poll()); // 출력: A
JavaPriorityQueue
PriorityQueue는 요소들이 자연 순서(natural ordering) 또는 지정된 비교자에 따라 정렬된 상태로 유지되는 우선순위 큐입니다. 예를 들어, 숫자가 작은 순서대로 요소를 처리하거나 특정 기준에 따라 높은 우선순위의 작업부터 처리하고자 할 때 사용합니다.
Queue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(10);
priorityQueue.offer(5);
priorityQueue.offer(20);
System.out.println(priorityQueue.poll()); // 출력: 5 (작은 수부터)
JavaBlockingQueue
BlockingQueue는 자바에서 동시성 프로그래밍을 위해 제공되는 특별한 종류의 큐입니다. BlockingQueue는 java.util.concurrent 패키지에 속하며, 생산자-소비자 패턴과 같은 멀티스레드 환경에서의 작업 처리에 매우 유용합니다. 대표적으로 ArrayBlockingQueue와 LinkedBlockingQueue가 있습니다.
ArrayBlockingQueue
ArrayBlockingQueue는 고정된 크기의 배열을 사용하는 큐로, 생성 시 크기를 지정해야 합니다. 이 큐는 생산자와 소비자가 동시에 접근할 수 있도록 설계되어 있으며, 큐가 가득 찼을 때 추가 작업을 블록하고 비어있을 때 제거 작업을 블록합니다.
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
blockingQueue.put(1);
blockingQueue.put(2);
System.out.println(blockingQueue.take()); // 출력: 1
JavaLinkedBlockingQueue
LinkedBlockingQueue는 연결 리스트 기반의 큐로, 크기를 지정하지 않으면 기본적으로 Integer.MAX_VALUE 만큼의 요소를 가질 수 있습니다. 이 큐는 요소의 추가와 제거 작업이 동시에 일어나더라도 안전하게 사용할 수 있어 생산자-소비자 문제 해결에 자주 사용됩니다.
BlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
linkedBlockingQueue.put("데이터 1");
linkedBlockingQueue.put("데이터 2");
System.out.println(linkedBlockingQueue.take()); // 출력: 데이터 1
JavaQueue 사용 시 주의사항
1. 동기화 문제
일반적인 큐 클래스(LinkedList, PriorityQueue 등)는 동기화를 지원하지 않으므로 멀티스레드 환경에서 안전하지 않습니다. 멀티스레드 환경에서 큐를 사용할 경우에는 BlockingQueue나 Collections.synchronizedQueue()와 같은 동기화된 큐를 사용하는 것이 좋습니다.
2. Null 요소 금지
대부분의 자바 큐 구현체(PriorityQueue, ArrayDeque 등)는 null 요소를 허용하지 않습니다. 이는 null 값이 반환될 경우, 큐가 비어 있는지 아니면 실제로 null 값이 저장된 것인지 구분할 수 없기 때문입니다.
예제: 은행 고객 대기열 시스템
아래는 간단한 은행 고객 대기열 시스템을 구현한 예제입니다. 고객이 은행을 방문하면 대기열에 등록되고, 창구 직원이 대기 중인 고객을 처리하는 방식입니다.
import java.util.LinkedList;
import java.util.Queue;
public class BankService {
public static void main(String[] args) {
Queue<String> customerQueue = new LinkedList<>();
// 고객이 대기열에 등록됩니다.
customerQueue.offer("고객 A");
customerQueue.offer("고객 B");
customerQueue.offer("고객 C");
// 창구에서 고객을 처리합니다.
while (!customerQueue.isEmpty()) {
String customer = customerQueue.poll();
System.out.println(customer + "님이 창구에서 서비스를 받고 있습니다.");
}
}
}
Java위 예제에서는 LinkedList를 사용하여 고객 대기열을 구현했습니다. 고객이 대기열에 등록될 때는 offer() 메서드를 사용하고, 고객을 처리할 때는 poll() 메서드를 사용하여 대기열에서 고객을 제거하면서 동시에 처리합니다.
결론
이 글에서는 자바에서 Queue 인터페이스가 어떻게 구성되고, 어떤 상황에서 사용될 수 있는지 심도 있게 다뤘습니다. 자바의 다양한 큐 구현체인 LinkedList, PriorityQueue, ArrayDeque뿐만 아니라 멀티스레드 환경에서 유용한 BlockingQueue까지 살펴보았습니다. 각 큐의 특성과 사용 방법을 이해하고 적재적소에 맞게 활용한다면, 효율적인 데이터 처리와 멀티스레드 환경에서의 작업을 안전하게 수행할 수 있을 것입니다.