Queue
Queue<T>
는 .NET에서 제공하는 제네릭 큐 자료 구조로, FIFOFirst-In-First-Out 방식으로 데이터를 관리합니다. System.Collections.Generic
네임스페이스에 포함되어 있으며, 데이터가 입력된 순서대로 처리해야 하는 상황에서 유용하게 사용됩니다.
Queue의 기본 사용법
Queue<T>
는 데이터가 먼저 들어온 데이터가 먼저 나가는 선입선출 방식의 컬렉션입니다. 다음은 Queue<int>
의 기본적인 사용 예제입니다:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Queue<int> queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
// 요소 출력 및 제거
while (queue.Count > 0)
{
int item = queue.Dequeue();
Console.WriteLine(item);
}
// 출력:
// 1
// 2
// 3
}
}
Queue<T>
의 특징
- FIFO 구조:
Queue
는 선입선출 방식을 사용하여, 먼저 들어온 데이터가 먼저 처리됩니다. - 타입 안전성: 제네릭 타입을 사용하여 타입 안전성을 보장합니다.
- 빠른 추가 및 제거:
Enqueue
와Dequeue
를 사용하여 요소를 빠르게 추가하고 제거할 수 있습니다.
장점
- 간단한 데이터 흐름 관리: 큐는 데이터가 입력된 순서대로 처리해야 하는 상황에 적합합니다. 예를 들어, 프린터 작업 큐나 이벤트 처리 시스템에서 사용됩니다.
- 빠른 Enqueue 및 Dequeue: 큐의 양 끝에서만 삽입 및 삭제가 이루어지므로, 이러한 작업은 평균적으로 O(1)의 시간 복잡도를 가집니다.
- 타입 안전성: 제네릭을 통해 타입을 명시함으로써, 컴파일 시 타입 오류를 방지할 수 있습니다.
단점
- 중간 요소 접근 불가: 큐는 선입선출 방식으로 동작하기 때문에 중간의 특정 요소를 직접 접근할 수 없습니다. 이는 순차적으로 모든 요소를 탐색해야 하므로 O(n)의 시간 복잡도를 가지게 됩니다.
- 순차 접근: 중간의 특정 요소를 검색하거나 수정하는 작업이 어렵습니다. 큐는 특정한 상황에서만 적합한 자료 구조입니다.
주요 메서드와 속성
생성자와 초기화
기본 생성자
빈 Queue
를 생성합니다.
Queue<int> queue = new Queue<int>();
- 기본 생성자로 초기화하면 내부 용량은
0
이며, 첫 번째 요소 추가 시 용량이 4로 설정됩니다.
초기 용량 지정
큐의 초기 용량을 설정하여 성능을 최적화할 수 있습니다.
Queue<int> queue = new Queue<int>(capacity: 100);
- 초기 용량 설정을 통한 최적화를 참조하세요.
요소 추가 및 제거
Enqueue
큐의 끝에 요소를 추가합니다.
queue.Enqueue(5);
- 큐의 끝에 새로운 요소를 추가하며, 용량이 가득 찰 경우 자동으로 배열의 크기를 증가시킵니다.
Dequeue
큐의 앞에서 요소를 제거하고 반환합니다.
int item = queue.Dequeue();
- 큐가 비어 있을 경우
InvalidOperationException
이 발생합니다.
요소 탐색
Peek
큐의 앞에 있는 요소를 제거하지 않고 반환합니다.
int frontItem = queue.Peek();
- 큐가 비어 있을 경우
InvalidOperationException
이 발생합니다.Peek
메서드는 요소를 제거하지 않으므로 큐의 상태를 유지합니다.
기타 유용한 메서드와 속성
Count
큐에 포함된 요소의 개수를 반환합니다.
int count = queue.Count;
- 현재 큐에 남아 있는 요소의 개수를 확인할 수 있습니다.
Contains
큐에 특정 요소가 있는지 확인합니다.
bool contains = queue.Contains(3);
- 요소를 찾기 위해 큐 전체를 순차적으로 탐색하므로 O(n)의 시간 복잡도를 가집니다.
Clear
큐의 모든 요소를 제거합니다.
queue.Clear();
ToArray
큐의 요소를 배열로 복사합니다.
var array = queue.ToArray();
Queue<T>
와 다른 자료 구조의 비교
Stack<T>
:Stack
은 LIFOLast-In-First-Out 방식으로 데이터를 처리하며, 가장 나중에 추가된 요소가 가장 먼저 제거됩니다. 반면Queue
는 FIFO 방식으로 데이터를 처리합니다. 따라서 데이터를 처리하는 순서에 따라 적절한 자료 구조를 선택해야 합니다.LinkedList<T>
:LinkedList
를 사용하여 큐를 쉽게 구현할 수도 있지만, 특별한 이유가 없는 한 .NET에서 제공하는Queue<T>
를 사용하는 것이 더 효율적입니다.Queue<T>
는 큐의 기능을 수행하는 데 최적화되어 있으며, 추가적인 메모리 관리나 복잡한 구현을 피할 수 있습니다.
Queue<T>
의 활용
실시간 데이터 처리와 버퍼로서의 큐 사용
Queue<T>
는 실시간 데이터 처리와 버퍼buffer 역할로도 자주 사용됩니다. 데이터가 생성되는 속도와 처리되는 속도가 다를 때, 큐를 사용하여 데이터를 임시로 저장하고 순차적으로 처리할 수 있습니다.
- 네트워크 패킷 처리: 수신된 패킷을 큐에 저장하고 순차적으로 처리합니다.
- 이벤트 처리 시스템: 발생한 이벤트를 큐에 넣고, 이벤트 핸들러가 순서대로 처리합니다.
- 데이터 스트림 버퍼링: 입력과 처리가 비동기적으로 이루어질 때 데이터 손실을 방지합니다.
- 생산자-소비자 문제: 생산자가 데이터를 빠르게 생성하는 반면 소비자가 느리게 처리할 때, 큐를 버퍼로 사용하여 두 작업 간의 속도 차이를 완화할 수 있습니다.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
class Program
{
static void Main()
{
Queue<int> buffer = new Queue<int>();
object lockObject = new object();
// 생산자 스레드
Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
lock (lockObject)
{
buffer.Enqueue(i);
Console.WriteLine($"생산: {i}");
}
Thread.Sleep(100); // 생산 속도 조절
}
});
// 소비자 스레드
Task.Run(() =>
{
while (true)
{
lock (lockObject)
{
if (buffer.Count > 0)
{
int item = buffer.Dequeue();
Console.WriteLine($"소비: {item}");
}
}
Thread.Sleep(150); // 소비 속도 조절
}
}).Wait();
}
}
BFS 알고리즘
그래프나 트리 구조를 탐색할 때 BFSBreadth-First Search, 너비 우선 탐색 알고리즘을 구현하는 데 Queue
가 자주 사용됩니다. BFS는 먼저 방문할 노드를 큐에 넣고, 큐에서 순서대로 노드를 꺼내면서 탐색하는 방식으로 동작하며, 경로 찾기 및 최단 거리 계산에 유용합니다.
using System;
using System.Collections.Generic;
class Graph
{
private int vertices;
private List<int>[] adjList;
public Graph(int vertices)
{
this.vertices = vertices;
adjList = new List<int>[vertices];
for (int i = 0; i < vertices; i++)
{
adjList[i] = new List<int>();
}
}
public void AddEdge(int v, int w)
{
adjList[v].Add(w);
}
public void BFS(int startVertex)
{
bool[] visited = new bool[vertices];
Queue<int> queue = new Queue<int>();
visited[startVertex] = true;
queue.Enqueue(startVertex);
while (queue.Count > 0)
{
int vertex = queue.Dequeue();
Console.Write(vertex + " ");
foreach (int adjacent in adjList[vertex])
{
if (!visited[adjacent])
{
visited[adjacent] = true;
queue.Enqueue(adjacent);
}
}
}
}
}
class Program
{
static void Main()
{
Graph graph = new Graph(4);
graph.AddEdge(0, 1);
graph.AddEdge(0, 2);
graph.AddEdge(1, 2);
graph.AddEdge(2, 0);
graph.AddEdge(2, 3);
graph.AddEdge(3, 3);
Console.WriteLine("BFS starting from vertex 2:");
graph.BFS(2);
// 출력: 2 0 3 1
}
}
큐를 활용한 비동기 프로그래밍
비동기 작업을 관리할 때 Queue<T>
를 활용하여 작업을 순차적으로 처리하는 것이 일반적입니다.
비동기 이벤트 처리: 큐를 사용해 비동기적으로 발생하는 이벤트를 순서대로 처리할 수 있습니다. 이는 이벤트가 발생한 순서를 보장하며, 각 이벤트가 처리되는 동안 다음 이벤트가 기다릴 수 있도록 합니다.
Task Queue 구현: 작업을 큐에 추가하고, 각 작업을 비동기로 처리하면서 순서를 유지할 수 있습니다. 이를 통해 복잡한 비동기 작업 흐름을 쉽게 관리할 수 있습니다.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
taskQueue.Enqueue(() => Task.Run(() => Console.WriteLine("Task 1 실행")));
taskQueue.Enqueue(() => Task.Run(() => Console.WriteLine("Task 2 실행")));
taskQueue.Enqueue(() => Task.Run(() => Console.WriteLine("Task 3 실행")));
while (taskQueue.Count > 0)
{
var task = taskQueue.Dequeue();
await task();
}
}
}
- 작업을 큐에 추가하고, 하나씩 비동기로 실행함으로써 작업의 순서와 비동기 처리를 동시에 유지할 수 있습니다.
TryDeque 구현
Queue<T>
에서 안전하게 요소를 제거하려면, TryDequeue
와 유사한 동작을 직접 구현할 수 있습니다. Dequeue
메서드는 큐가 비어 있을 때 InvalidOperationException
예외를 발생시키기 때문에, 이를 방지하려면 큐의 요소 유무를 먼저 확인한 후 Dequeue
를 호출하는 방식을 사용할 수 있습니다.
public static bool TryDequeue<T>(Queue<T> queue, out T result)
{
if (queue.Count > 0)
{
result = queue.Dequeue();
return true;
}
else
{
result = default;
return false;
}
}
// 사용 예시
Queue<int> queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
if (TryDequeue(queue, out int result))
{
Console.WriteLine($"Dequeued: {result}");
}
else
{
Console.WriteLine("Queue is empty.");
}
스레드 안전성
Queue<T>
는 스레드 안전하지 않기 때문에 동기성 문제가 발생할 수 있습니다. 여러 스레드가 동시에 Queue<T>
에 접근하여 요소를 추가Enqueue하거나 제거Dequeue하려고 할 때, 예상치 못한 동작이나 데이터 손실이 발생할 수 있습니다.
private readonly Queue<int> queue = new Queue<int>();
private readonly object lockObject = new object();
public void EnqueueSafe(int item)
{
lock (lockObject)
{
queue.Enqueue(item);
}
}
public bool TryDequeueSafe(out int result)
{
lock (lockObject)
{
if (queue.Count > 0)
{
result = queue.Dequeue();
return true;
}
else
{
result = default;
return false;
}
}
}
이 방식은 스레드 안전성을 확보할 수 있지만, lock
을 사용하면 성능에 영향을 미칠 수 있고 다수의 스레드가 자주 접근할 경우 병목bottleneck이 발생할 수 있습니다.
다중 스레드 환경에서의 큐 사용은 ConcurrentQueue<T>
사용을 고려하세요.