[궁금시리즈] 4-1. 컬렉션(Collection)은 왜 필요할까?
프로그램을 개발하다 보면 하나의 데이터보다 여러 개의 데이터를 함께 다루는 경우가 훨씬 많다.
예를 들어 게임에서는
- 플레이어 목록
- 몬스터 목록
- 아이템 목록
- 채팅 메시지
- 접속한 유저 목록
등 수많은 데이터를 관리해야 한다. 그렇다면 이런 데이터를 변수만으로 관리할 수 있을까?
이번 글에서는 컬렉션이 등장한 이유와 왜 필요한지 알아보자.
변수만으로는 한계가 있다
예를 들어 플레이어를 저장한다고 생각해 보자.
Player player1 = new();
Player player2 = new();
Player player3 = new();
Player player4 = new();
처음에는 문제가 없어 보인다. 하지만 플레이어가 100명이라면?
player1
player2
...
player100
이런 코드를 작성해야 한다. 게다가 반복문으로 처리하는 것도 어렵다.
배열(Array)의 등장
이 문제를 해결하기 위해 배열이 등장했다.
Player[] players = new Player[100];
이제
players[0]
players[1]
처럼 관리할 수 있다.
반복문도 가능하다.
for (int i = 0; i < players.Length; i++)
{
Console.WriteLine(players[i]);
}
배열은 동일한 자료형을 연속적으로 저장하는 가장 기본적인 자료구조이다.
배열도 완벽하지 않다
배열에는 큰 제약이 하나 있다. 바로 크기가 고정된다는 점이다.
Player[] players = new Player[100];
100명을 위해 만들었는데 101명이 접속했다면?
배열의 크기는 변경할 수 없다.
반대로 10명만 접속했더라도 90칸은 비어 있게 된다.
즉, 너무 크게 만들면 메모리가 낭비되고, 너무 작게 만들면 데이터를 더 저장할 수 없다.
크기를 늘리면 되지 않을까? 배열은 생성 후 크기를 변경할 수 없다.
그래서 실제로는
100칸 배열
↓
200칸 배열 새로 생성
↓
기존 데이터 복사
↓
기존 배열 제거
라는 과정이 필요하다.
예를 들어
int[] oldArray = { 1, 2, 3 };
int[] newArray = new int[6];
Array.Copy(oldArray, newArray, oldArray.Length);
새로운 배열을 만들고 모든 데이터를 복사해야 한다.
데이터가 많을수록 비용도 커진다.
그래서 등장한 것이 컬렉션이다
컬렉션(Collection)은
여러 개의 데이터를 효율적으로 관리하기 위한 자료구조이다.
대표적인 컬렉션으로는
- List
- Dictionary<TKey, TValue>
- HashSet
- Queue
- Stack
등이 있다.
이들은 모두 상황에 맞는 방식으로 데이터를 관리하도록 설계되어 있다.
List는 무엇이 다를까?
예를 들어
List<Player> players = new();
이제
players.Add(new Player());
만 호출하면 필요할 때 내부적으로 크기를 늘려 준다.
개발자는 배열을 새로 만들거나 복사하는 과정을 직접 신경 쓸 필요가 없다.
컬렉션은 모두 같은 역할을 할까?
많은 사람들이
List
Dictionary
HashSet
Queue
Stack
를 모두 비슷한 기능으로 생각한다.
하지만 각각의 목적은 완전히 다르다.
예를 들어 List 순서를 유지하는 목록
[10][20][30][40]
Dictionary Key로 빠르게 찾기
ID
↓
Player
HashSet 중복 없는 데이터 관리
A
B
C
A를 여러 번 넣어도 하나만 저장된다.
Queue 먼저 들어온 데이터가 먼저 나온다.
입장
↓
↓
↓
퇴장
(First In First Out)
Stack 나중에 들어온 데이터가 먼저 나온다.
입장
↑
↓
퇴장
(Last In First Out)
즉, 컬렉션마다 해결하려는 문제가 다르다.
컬렉션은 왜 이렇게 종류가 많을까?
예를 들어 플레이어를 찾는 작업을 생각해 보자.
List
↓
처음부터 끝까지 탐색
반면 Dictionary는
Key
↓
바로 접근
할 수 있다.
반대로 순서를 유지하면서 데이터를 반복 처리해야 한다면 List가 더 적합하다.
즉, 모든 상황에서 가장 좋은 컬렉션은 존재하지 않는다. 문제에 맞는 자료구조를 선택하는 것이 중요하다.
실제 .NET도 인터페이스 중심으로 설계되어 있다
앞에서 Generic 인터페이스를 설명하면서 IEnumerable
예를 들어
List<T>
HashSet<T>
Queue<T>
모두
IEnumerable<T>
를 구현한다. 덕분에
foreach (var item in collection)
{
}
과
collection.Where(x => ...)
같은 코드를 컬렉션 종류와 관계없이 사용할 수 있다.
이처럼 .NET은 구현보다 인터페이스를 중심으로 컬렉션을 설계 하여 일관된 사용 방법을 제공한다.
실무에서 자주 하는 실수
많은 초보 개발자는
“데이터 여러 개면 그냥 List를 쓰면 되겠네.”
라고 생각한다.
하지만
- 검색이 많다면 Dictionary
- 중복 제거가 필요하다면 HashSet
- 순차 처리라면 Queue
- 되돌리기(Undo) 기능이라면 Stack
이 훨씬 적합하다.
List
마무리
컬렉션은 단순히 여러 개의 데이터를 저장하는 기능이 아니다. 배열의 한계를 보완하고, 데이터의 특성에 맞는 효율적인 저장과 검색을 제공하기 위해 만들어진 자료구조이다.
List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>는 모두 같은 목적을 가진 것이 아니라 각각 다른 문제를 해결하기 위해 설계된 컬렉션이다.
다음 글에서는 가장 많이 사용하는 컬렉션인 List<T>는 내부적으로 어떻게 동작할까?를 알아보며 동적 배열(Dynamic Array)의 원리를 살펴보겠다.
핵심 정리
- 컬렉션은 여러 개의 데이터를 효율적으로 관리하기 위한 자료구조이다.
- 배열은 크기가 고정되어 있어 유연성이 부족하다.
- List
는 내부적으로 배열을 관리하며 필요할 때 크기를 늘린다. - 컬렉션마다 해결하려는 문제가 다르다.
- List
는 순차 데이터, Dictionary<TKey, TValue>는 빠른 검색, HashSet 는 중복 제거, Queue 는 FIFO, Stack 는 LIFO에 적합하다. - 문제에 맞는 컬렉션을 선택하는 것이 성능과 유지보수성에 큰 영향을 준다.