[궁금시리즈] 4-1. 컬렉션(Collection)은 왜 필요할까?

3 minute read

프로그램을 개발하다 보면 하나의 데이터보다 여러 개의 데이터를 함께 다루는 경우가 훨씬 많다.

예를 들어 게임에서는

  • 플레이어 목록
  • 몬스터 목록
  • 아이템 목록
  • 채팅 메시지
  • 접속한 유저 목록

등 수많은 데이터를 관리해야 한다. 그렇다면 이런 데이터를 변수만으로 관리할 수 있을까?

이번 글에서는 컬렉션이 등장한 이유와 왜 필요한지 알아보자.


변수만으로는 한계가 있다

예를 들어 플레이어를 저장한다고 생각해 보자.

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에 적합하다.
  • 문제에 맞는 컬렉션을 선택하는 것이 성능과 유지보수성에 큰 영향을 준다.