queue stack dictionary hashtable csharp
핵심 요약
Queue, Stack, Dictionary 세 가지 C# 데이터 구조의 올바른 사용법과 실무 적용 방법을 다룹니다. List 남용의 문제점을 해결하고, 각 자료구조의 성능 특성과 실제 사용 사례를 통해 최적의 선택 기준을 제시합니다. .NET 6 이상에서 제공하는 안전한 메서드들과 성능 최적화 팁을 포함하여 프로덕션 코드 품질을 향상시킬 수 있습니다.
상세 요약
1. Queue (큐) - FIFO 구조
개념 및 작동 원리
Queue는 First-In-First-Out(FIFO) 방식의 자료구조로, 마치 마트의 계산대 줄처럼 먼저 들어온 데이터가 먼저 나갑니다. 예측 가능한 순서와 공정성이 필요한 상황에서 활용됩니다.
핵심 메서드
- Enqueue: 큐에 요소 추가
- Dequeue: 큐에서 첫 번째 요소 제거 및 반환
- Peek: 첫 번째 요소를 확인하되 제거하지 않음
- TryDequeue / TryPeek (.NET 6+): 안전한 버전으로, 큐가 비어있을 때도 예외 발생 없이 boolean 반환
실제 코드 예제
var waitList = new Queue<Subscriber>();
waitList.Enqueue(new Subscriber { FirstName = "Ada" });
waitList.Enqueue(new Subscriber { FirstName = "Grace" });
waitList.Enqueue(new Subscriber { FirstName = "Linus" });
while (waitList.Count > 0)
{
var nextUp = waitList.Dequeue();
Console.WriteLine($"Now serving {nextUp.FirstName}");
}
Peek와 Dequeue의 차이점
- Peek: 요소를 조회만 함 (Count 변화 없음)
- Dequeue: 요소를 반환하면서 동시에 제거 (자동으로 Count 감소)
안전한 사용 방법 (.NET 6+)
if (waitList.TryDequeue(out var served))
{
Console.WriteLine($"Served: {served.FirstName}");
}
else
{
Console.WriteLine("No one left");
}
실무 활용 사례
- 작업 큐 관리 (print queue, task queue)
- 고객 대기열 관리 시스템
- 메시지 브로커 및 비동기 작업 처리
주의사항
- InvalidOperationException: Dequeue/Peek를 빈 큐에서 호출하면 예외 발생
- .NET 6 미만: Try 메서드가 없으므로 Count 확인 필수
- 멀티스레드 환경: ConcurrentQueue 사용 권장 (System.Collections.Concurrent)
2. Stack (스택) - LIFO 구조
개념 및 작동 원리
Stack은 Last-In-First-Out(LIFO) 방식으로, 가장 최근에 들어온 데이터가 가장 먼저 나갑니다. 브라우저의 뒤로 가기 버튼이나 실행 취소(Undo) 기능의 기초가 됩니다.
핵심 메서드
- Push: 스택에 요소 추가
- Pop: 스택에서 최상단 요소 제거 및 반환
- Peek: 최상단 요소를 확인하되 제거하지 않음
- TryPop / TryPeek (.NET 6+): 안전한 버전
실제 코드 예제
var history = new Stack<string>();
history.Push("/home");
history.Push("/products");
history.Push("/cart");
var lastIn = history.Pop(); // "/cart" 반환 및 제거
Console.WriteLine(lastIn);
스택 Peek 활용
var current = history.Peek(); // "/products" 확인 (제거 안 함)
안전한 사용 방법 (.NET 6+)
if (history.TryPop(out var page))
{
Console.WriteLine($"Going back to: {page}");
}
if (history.TryPeek(out var nextPage))
{
Console.WriteLine($"Next page: {nextPage}");
}
실무 활용 사례
- 브라우저 방문 이력 관리
- 실행 취소/다시 하기(Undo/Redo) 기능
- 깊이 우선 탐색(DFS) 알고리즘
- 수식 평가 및 파싱
- 괄호 균형 검사
괄호 균형 검사 예제
문자열 내 괄호의 균형을 확인하는 알고리즘이 스택을 활용한 대표적인 예입니다.
주의사항
- InvalidOperationException: Pop/Peek를 빈 스택에서 호출하면 예외 발생
- .NET 6 미만: Try 메서드 부재로 사전 Count 확인 필수
3. Hash 기반 컬렉션 - 빠른 조회
3.1 HashTable (레거시)
개념: 오래된 비제네릭 해시 기반 자료구조로 System.Collections에 속합니다. 현대 코드에서는 거의 사용되지 않으며 레거시 코드에만 남아있습니다.
문제점:
- 모든 값이 object로 boxed되어 타입 안전성 부재
- Boxing/Unboxing으로 인한 성능 저하 및 오류 위험
- 값 추출 시 형변환 필수
var table = new System.Collections.Hashtable();
table.Add(1, 42); // int 키, int 값
table.Add(2, "100"); // int 키, string 값 (혼합 가능하지만 위험)
// 문제: 무엇의 타입인지 불명확
var value = table[1];
3.2 Dictionary<TKey, TValue> (권장)
개념: 제네릭 기반 해시 테이블로, 강타입 안전성을 제공하는 현대적 자료구조입니다. 대부분의 현대 C# 코드에서 사용되어야 합니다.
장점:
- 강타입 안전성으로 오류 방지
- Boxing/Unboxing 없음
- 빠른 조회 성능 (평균 O(1))
- 명확한 타입 시스템
실제 코드 예제
var table = new Dictionary<string, int>();
table.Add("one", 1);
table.Add("two", 2);
// 키 존재 여부 확인 후 안전하게 추출
if (table.TryGetValue("two", out int user))
{
Console.WriteLine($"Found: {user}");
}
else
{
Console.WriteLine("Key not found");
}
Dictionary 성능 최적화 팁
- 초기 크기 지정: 용량을 미리 알면 할당 횟수 감소
var dict = new Dictionary<string, int>(1000); // 초기 용량 지정
주의사항
1. 타입 안전성: 키/값 타입 명시 필수
// 좋음
var goodDict = new Dictionary<string, int>();
// 피해야 함
var badDict = new Dictionary<object, object>();
2. 커스텀 키 타입 사용 시:
반드시 **GetHashCode()**와 Equals() 오버라이드 구현 필수
public class User
{
public int Id { get; set; }
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object obj) =>
obj is User user && user.Id == this.Id;
}
3. 성능 주의:
- Contains(value): 모든 값을 순회하므로 선형 탐색 (O(n))
- Contains(key): 빠른 해시 조회 (O(1))
Dictionary 주요 메서드
- Add(key, value): 키-값 쌍 추가 (키 중복 시 예외)
- TryGetValue(key, out value): 안전한 값 추출
- ContainsKey(key): 키 존재 여부 확인 (빠름)
- ContainsValue(value): 값 존재 여부 확인 (느림)
- Remove(key): 키-값 쌍 제거
멀티스레드 환경
var concurrentDict = new ConcurrentDictionary<string, int>();
4. 자료구조 선택 가이드
| 자료구조 | 특징 | 사용 시기 | 시간복잡도 |
|---|---|---|---|
| Queue | FIFO, 예측 가능한 순서 | 작업 큐, 대기열 | O(1) |
| Stack | LIFO, 최근 순서 | Undo/Redo, DFS, 파싱 | O(1) |
| Dictionary | 고속 조회, 강타입 | 키-값 매핑, 캐싱 | O(1) |
| HashTable | 레거시, 비타입 안전 | 레거시 코드만 | O(1) |
5. 성능 최적화 팁
List 남용을 피해야 하는 이유
일반적인 List는 다양한 용도에 사용되지만, 특정 작업 패턴에는 더 적합한 자료구조들이 있습니다.
개선 사항들
-
초기 크기 할당: Stack/Queue 생성 시 용량을 미리 지정하면 재할당 감소
var queue = new Queue<Item>(1000); // 초기 용량 지정 -
Contains(value) 피하기: 대규모 데이터셋에서는 선형 탐색으로 성능 저하
- 대신 Dictionary와 ContainsKey 사용
-
불변 타입(Immutable Type) 선호: 키로 사용하는 타입은 변경되지 않아야 함
- 문자열(String)이 이상적
- 커스텀 타입은 반드시 GetHashCode/Equals 구현
-
Boxing 피하기: 비제네릭 컬렉션(HashTable) 사용 금지
- 항상 Dictionary<TKey, TValue> 사용
멀티스레드 환경 최적화
// 안전한 동시성
using System.Collections.Concurrent;
var concurrentQueue = new ConcurrentQueue<Item>();
var concurrentDict = new ConcurrentDictionary<string, int>();
6. 실무 권장 사항
순서가 중요한 경우
- 공정성(Fairness) 필요 → Queue 사용
- 최근 항목 우선 → Stack 사용
빠른 조회가 필요한 경우
- 키-값 매핑 → Dictionary<TKey, TValue> 사용
- 멀티스레드 → ConcurrentDictionary 사용
피해야 할 패턴
- 시간 순서 관리에 List 사용 → Queue 또는 Stack 사용
- HashTable 사용 → Dictionary 사용
- 커스텀 키 타입에 GetHashCode/Equals 미구현 → 성능 저하 및 오류 발생
7. 학습 리소스
추가 탐구 주제
- System.Collections.Concurrent: 멀티스레드 환경의 동시성 컬렉션
- 깊이 우선 탐색(DFS): Stack을 활용한 그래프/트리 알고리즘
- 수식 평가: Stack을 활용한 파싱 및 계산
- GetHashCode/Equals: 효율적인 해시 함수 설계