컬렉션이란?

상태
완료
담당자
날짜
숫자
0
게임 개발에서는 다양한 객체와 데이터를 다루어야 하는데요. 이 때, 데이터를 효율적으로 관리하기 위해 컬렉션을 사용하는 것이 매우 유용합니다. 컬렉션은 데이터의 집합을 구조화하고 관리할 수 있는 도구를 제공합니다.
이번에는 C#에서 컬렉션의 정의와 종류에 대해 알아보고, 이를 어떻게 활용할 수 있는지 구체적인 예시를 통해 학습해보겠습니다.

컬렉션이란?

컬렉션은 여러 개의 데이터를 하나의 객체로 묶어서 관리할 수 있는 데이터 구조입니다.
컬렉션을 적절하게 사용하면 개발에서 효율성과 성능을 크게 향상시킬 수 있습니다.
이는 동적으로 데이터를 추가하거나 제거할 수 있으며, 데이터에 대한 다양한 조작할 수 있는 방법을 제공하는데요. 각 컬렉션의 특성에 따라 적절히 선택하여 선택하면 코드의 효율성과 가독성을 높일 수 있습니다.
그럼 컬렉션에는 어떤 종류가 있고 어떻게 사용하는지 한 번 알아봅시다.

컬렉션의 종류와 사용 예시

1. 배열 (Array)

배열은 고정된 크기의 동일한 데이터 타입의 데이터 집합을 저장하는 구조입니다. 배열의 크기는 생성 시에 결정되며, 이후에는 변경할 수 없습니다.
배열은 메모리 상에 연속적으로 저장되며, 인덱스를 사용해 요소에 접근할 수 있습니다. 배열의 크기는 고정되어 있어 추가적인 공간이 필요할 경우, 새로운 배열을 생성해야합니다.
배열 사용법
// 배열 선언 및 초기화 int[] numbers = new int[5]; // 크기가 5인 정수 배열 선언 numbers[0] = 10; // 배열의 첫 번째 요소에 값 할당, 배열은 0이 첫 번째 요소
C#
복사
위의 코드는 크기가 5인 정수 배열을 선언하고 첫 번째 요소에 10이라는 값을 할당한 코드입니다. 배열의 크기가 5라면 [0],[1],[2],[3],[4] 이렇게 5개의 요소를 가지게 됩니다. 그럼 만약 모든 요소를 채우지 않으면 어떻게 될까요?
배열의 크기를 지정하고 모든 요소를 초기화하지 않으면, 초기화되지 않은 요소들은 해당 타입의 기본값을 가지게 됩니다.
배열의 기본값
참조 타입: null
정수형 (int, long, short 등): 0
실수형 (float, double 등): 0.0
bool 타입: false
char 타입: '\0' (null 문자)
그럼 이제 사용 예시를 보며 장단점을 알아보겠습니다.
public class Enemy { public string Name { get; set; } public int Health { get; set; } } class Program { static void Main() { // 고정된 위치에 배치된 5명의 적 Enemy[] enemies = new Enemy[5]; enemies[0] = new Enemy { Name = "Goblin", Level = 10 }; enemies[1] = new Enemy { Name = "Orc", Level = 20 }; // 나머지 요소는 기본값 (null)로 초기화됨 // 배열의 요소를 순회하면서 출력 foreach (var enemy in enemies) { if (enemy != null) { Console.WriteLine($"{enemy.Level} 레벨의 {enemy.Name} 이(가) 등장했습니다."); } } } }
C#
복사
배열의 장단점
장점 : 인덱스를 사용하여 빠르게 접근할 수 있으며, 메모리 사용이 효율적입니다.
단점 : 크기가 고정되어 있어 동적 데이터 관리에 적합하지 않습니다.
그렇다면 어떤 경우에 배열을 사용하는 것이 유용할까요?
고정된 크기의 데이터가 필요한 경우 : 예를 들어 여러분이 게임을 하다가 몬스터를 잡는다고 가정합시다. 그러면 몬스터의 개체 수는 정해져있을 것이고, 지정된 리스폰 위치에서 생성될 것입니다. 이처럼 게임에서 한번에 처리해야하는 고정된 개수의 데이터를 저장할 때 유용합니다.

2. 리스트 (LIST<T>)

리스트란 크기가 동적으로 조절되는 데이터 집합을 저장하는 구조입니다. 배열과 달리 크기를 동적으로 조절할 수 있으며, 다양한 메서드를 제공하여 데이터를 쉽게 추가, 삭제, 검색할 수 있습니다.
LIST는 내부적으로 배열을 사용하지만, 크기가 자동으로 조절되므로 데이터를 추가할 때마다 새로운 배열을 생성하지 않아도 됩니다. 데이터를 추가하거나 삭제할 때 유연하게 대처할 수 있습니다.
리스트 사용법
// 리스트 선언 및 초기화 List<int> numbers = new List<int>(); // 정수 리스트 선언 numbers.Add(10); // 리스트에 값 추가
C#
복사
위의 코드는 정수형 리스트를 만들고, 리스트의 첫 번째 요소에 10의 값을 할당한 것입니다.
또한 리스트는 다양한 메서드를 제공하는데요. 리스트에는 어떤 메서드가 있는지 설명해드리겠습니다.
1.
Add(T item) : 리스트의 끝에 요소를 추가합니다.
List<string> fruits = new List<string>(); fruits.Add("Apple"); fruits.Add("Banana");
C#
복사
2.
Remove(T item) : 리스트에서 첫 번째로 일치하는 요소를 제거합니다. 요소가 여러 번 존재하면 첫 번째 요소만 제거됩니다.
//문자열 리스트를 선언하고 리스트에 Apple, Banana, Cherry 할당 List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" }; fruits.Remove("Banana"); // "Banana"를 제거합니다.
C#
복사
3.
Insert(int index, T item) : 지정된 인덱스 위치에 요소를 삽입합니다. 기존의 요소들은 한 칸씩 뒤로 밀립니다.
List<string> fruits = new List<string> { "Apple", "Cherry" }; fruits.Insert(1, "Banana"); // 인덱스 1에 "Banana"를 삽입합니다.
C#
복사
4.
Clear() : 리스트의 모든 요소를 제거합니다. 리스트의 크기는 0이 됩니다.
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" }; fruits.Clear(); // 리스트를 비웁니다.
C#
복사
리스트는 배열과 달리 크기가 동적으로 변한다고 했죠? 밑의 코드를 통해 크기가 동적으로 변하는 것을 보여드리겠습니다.
using System; using System.Collections.Generic; public class Item { public string Name { get; set; } } class Program { static void Main() { // 빈 리스트 선언 List<Item> inventory = new List<Item>(); // 리스트의 크기를 확인 Console.WriteLine($"Inventory count: {inventory.Count}"); // 리스트에 요소를 추가 inventory.Add(new Item { Name = "Sword" }); inventory.Add(new Item { Name = "Shield" }); // 리스트의 크기를 확인 Console.WriteLine($"Inventory count after adding items: {inventory.Count}"); // 리스트의 첫 번째 요소 출력 if (inventory.Count > 0) // 리스트에 요소가 있는지 확인 { Item firstItem = inventory[0]; // 첫 번째 요소 접근 Console.WriteLine($"First item: {firstItem.Name}"); } else { Console.WriteLine("The inventory is empty."); } } }
C#
복사
//출력 결과 Inventory count: 0 Inventory count after adding items: 2 First item: Sword
C#
복사
위의 코드를 실행하면 처음에는 인벤토리의 크기는 0이었지만, Sword와 Shield를 추가하여 크기가 2가 된 것을 볼 수 있습니다.
또한 배열처럼 inventory[0]을 출력하면 리스트의 첫 번째 요소가 출력됩니다.
리스트의 장단점
장점 : 데이터의 크기를 동적으로 조절할 수 있으며, 다양한 메서드를 통해 데이터 조작이 용이합니다.
단점 : 배열보다 메모리 사용이 비효율적일 수 있습니다.
그렇다면 리스트는 어떤 상황에 사용하는 것이 유용할까요?
동적으로 크기가 변경되는 데이터가 필요한 경우 : 여러분이 게임을 하다보면 다양한 아이템을 획득하고 인벤토리에 그 아이템이 추가되고 필요없는 아이템은 버릴 것입니다. 이럴 경우 리스트는 Add, Remove, Insert, Clear 등 다양한 메서드를 제공하여 아이템을 효율적으로 관리할 수 있습니다.

3. 딕셔너리 (Dictionary<TKey, TValue>)

딕셔너리란 키와 값을 쌍으로 저장하는 데이터 구조입니다. 각 키는 유일해야하며, 이를 통해 값을 빠르게 검색할 수 있습니다.
딕셔너리는 해시 테이블을 기반으로 하며, 키를 통해 값을 빠르게 검색할 수 있습니다. 키와 값은 특정 타입으로 정의됩니다.
딕셔너리 사용법
1.
딕셔너리 선언 및 초기화
딕셔너리를 선언할 때는 키와 값의 데이터 타입을 명시합니다. 초기화는 new Dictionary<TKey, TValue>()를 사용합니다.
// 문자열을 키로 하고 정수를 값을 가지는 딕셔너리 선언 및 초기화 Dictionary<string, int> ages = new Dictionary<string, int>();
C#
복사
2.
값 추가 및 업데이트
Add 메서드 또는 인덱서를 사용하여 키-값 쌍을 추가하거나 업데이트할 수 있습니다. 키가 이미 존재하는 경우 Add는 예외를 발생시키지만, 인덱서를 사용하면 값을 덮어쓸 수 있습니다.
// Add 메서드를 사용하여 키-값 쌍 추가 ages.Add("Alice", 25); // 인덱서를 사용하여 키-값 쌍 추가 또는 업데이트 ages["Bob"] = 30; // 키 "Bob"이 존재하지 않으면 추가, 존재하면 값 업데이트
C#
복사
3.
값 조회
TryGetValue 메서드를 사용하면 키가 존재하는지 확인하고 값을 안전하게 조회할 수 있습니다.
//Alice 키를 찾고, 해당 키에 대응하는 값을 반환 if (ages.TryGetValue("Alice", out int age)) { Console.WriteLine($"Alice's age is {age}"); } else { Console.WriteLine("Key not found."); }
C#
복사
4.
값 삭제
Remove 메서드를 사용하여 특정 키-값 쌍을 삭제할 수 있습니다.
ages.Remove("Bob"); // 키 "Bob"과 관련된 쌍을 삭제
C#
복사
5.
딕셔너리 순회
foreach 루프를 사용하여 딕셔너리의 모든 키-값 쌍을 순회할 수 있습니다.
foreach (var kvp in ages) { Console.WriteLine($"{kvp.Key}: {kvp.Value}"); }//kvp : KeyValuePair의 약자. 키-값 쌍을 나타냄.
C#
복사
6.
딕셔너리의 키와 값에 접근
KeysValues 속성을 사용하여 딕셔너리의 키와 값 컬렉션에 접근할 수 있습니다.
// 모든 키 출력 foreach (var key in ages.Keys) { Console.WriteLine($"Key: {key}"); } // 모든 값 출력 foreach (var value in ages.Values) { Console.WriteLine($"Value: {value}"); }
C#
복사
7.
딕셔너리의 크기 확인
Count 속성을 사용하여 딕셔너리에 저장된 키-값 쌍의 개수를 확인할 수 있습니다.
Console.WriteLine($"Number of elements in dictionary: {ages.Count}");
C#
복사
딕셔너리의 장단점
장점 : 빠른 검색 속도를 제공하며, 키를 통해 데이터를 쉽게 접근할 수 있습니다.
단점 : 메모리 사용량이 증가할 수 있으며, 키의 유일성을 보장해야합니다.
딕셔너리의 장단점을 알아보았으니, 사용 예시를 보며 어떤 상황에서 딕셔너리를 사용하는 것이 유용한지 알아보겠습니다.
using System; using System.Collections.Generic; class Program { static void Main() { // 사용자 ID를 키로 하고, 닉네임을 값으로 가지는 딕셔너리 선언 및 초기화 Dictionary<int, string> userNicknames = new Dictionary<int, string> { { 1001, "Alice" }, { 1002, "Bob" }, { 1003, "Charlie" } }; // 검색할 ID를 지정 int searchId = 1002; // ID로 닉네임 찾기 if (userNicknames.TryGetValue(searchId, out string nickname)) { Console.WriteLine($"User with ID {searchId} has the nickname: {nickname}"); } else { Console.WriteLine($"No user found with ID {searchId}"); } } }
C#
복사
특정 키에 대한 데이터 접근이 필요할 경우 : 여러분이 게임을 하면서 캐릭터 고유의 ID를 검색하여 닉네임을 찾거나 캐릭터의 정보를 조회한다고 가정합시다. 이런 경우 딕셔너리는 키를 통해 값을 빠르게 검색할 수 있어, 특정 키에 대한 데이터 접근이 필요할 때 유용합니다.

4. 큐 (Queue<T>)

큐는 데이터가 FIFO(First-In-First-Out) 방식으로 처리되는 구조입니다. FIFO는 선입선출이라고도 하는데 가장 먼저 추가된 데이터가 가장 먼저 제거되는 구조입니다. 이는 주로 데이터를 순서대로 처리할 때 유용하며 Enqueue와 Dequeue 메서드를 통해 데이터를 추가하고 제거합니다.
큐 사용 방법
// 큐 선언 및 초기화 Queue<int> numbers = new Queue<int>(); // 정수 큐 선언 numbers.Enqueue(10); // 큐에 값 추가 int firstNumber = numbers.Dequeue(); // 큐에서 값 제거 및 반환
C#
복사
Enqueue() 메서드를 이용하여 큐에 값을 추가하고 Dequeue() 메서드를 이용하여 큐에서 값을 제거합니다.
큐의 장단점
장점 : FIFO 방식으로 데이터 처리 시 유용하며, 순서대로 처리할 수 있습니다.
단점 : 특정 데이터를 무작위로 접근하거나 제거하기 어렵습니다.
그럼 활용 예제를 보며 어떤 경우 큐를 사용하는 것이 유용한지 알아보겠습니다.
using System; using System.Collections.Generic; class Program { // 공격 단계를 나타내는 열거형 정의 enum Attack { FirstAttack, SecondAttack, FinalAttack } static void Main() { // 3단계 공격 큐 선언 Queue<AttackStage> attackQueue = new Queue<Attack>(); // 큐에 3단계 공격 추가 attackQueue.Enqueue(Attack.FirstAttack); attackQueue.Enqueue(Attack.SecondAttack); attackQueue.Enqueue(Attack.FinalAttack); // 공격 처리 while (attackQueue.Count > 0) { AttackStage stage = attackQueue.Dequeue(); // 공격 단계에 따라 처리 (구현 생략) Console.WriteLine($"Processing {stage} attack."); } } }
C#
복사
데이터의 순서가 중요할 때 : 위의 코드는 여러분의 캐릭터가 공격을 했을 때, 3단 공격하는 것을 구현한 코드입니다. 이는 큐를 사용하여 3단계 공격을 순서대로 처리합니다. 큐의 FIFO 특성을 활용하여 공격 단계를 정확한 순서로 실행할 수 있습니다. 이처럼 게임에서 복잡한 공격 패턴이나 여러 단계를 순차적으로 처리할 때 큐를 사용하면 깔끔하게 구현이 가능합니다.

5. 스택 (Stack<T>)

스택은 데이터가 LIFO(Last-In-First-Out) 방식으로 처리되는 구조입니다. LIFO는 후입선출이라고도 하며, 이는 가장 마지막에 추가된 데이터가 가장 먼저 제거되는 방식입니다. 주로 최근에 추가된 데이터를 먼저 처리할 때 유용하며, Push와 Pop 메서드를 통해 데이터를 추가하고 제거합니다.
스택 사용 방법
// 스택 선언 및 초기화 Stack<int> numbers = new Stack<int>(); // 정수 스택 선언 numbers.Push(10); // 스택에 값 추가 int lastNumber = numbers.Pop(); // 스택에서 값 제거 및 반환
C#
복사
Push() 메서드를 이용해 스택에 값을 추가하고 Pop() 메서드를 이용해 스택에서 값을 제거합니다.
스택의 장단점
장점 : LIFO 방식으로 데이터를 처리하기 때문에 최근에 추가된 데이터를 우선적으로 처리할 수 있습니다.
단점 : 특정 데이터를 무작위로 접근하거나 제거하기 어렵습니다.
그럼 스택의 활용 예시를 보며 어떤 경우에 사용하는 것이 유용한지 알아보겠습니다.
using System; using System.Collections.Generic; class Program { static void Main() { // 상태 스택 선언 Stack<string> gameStateStack = new Stack<string>(); // 게임 상태 저장 SaveState("Level1", gameStateStack); SaveState("Level2", gameStateStack); SaveState("Level3", gameStateStack); // 상태 복원 RestoreState(gameStateStack); // 출력: Restoring state: Level3 RestoreState(gameStateStack); // 출력: Restoring state: Level2 } static void SaveState(string state, Stack<string> stateStack) { Console.WriteLine($"Saving state: {state}"); stateStack.Push(state); } static void RestoreState(Stack<string> stateStack) { if (stateStack.Count > 0) { string restoredState = stateStack.Pop(); Console.WriteLine($"Restoring state: {restoredState}"); } else { Console.WriteLine("No state to restore."); } } }
C#
복사
최근의 추가된 데이터를 먼저 처리할 때 : 만약 여러분이 게임을 하면서 게임 상태를 저장해둔다고 가정하겠습니다. 그런 경우 가장 마지막에 저장해둔 데이터가 스택에 쌓이고 여러분이 게임을 진행하다가 저장해둔 시점으로 돌아갈 때 유용하게 사용할 수 있습니다. 또한 전에 했던 작업으로 되돌리는 Undo 등의 기능도 스택을 이용한 것이라고 할 수 있습니다.

결론

컬렉션에 대해 잘 이해하셨나요? 각각의 컬렉션 타입은 특정 상황에서 장점이 있으며, 올바르게 활용하면 효율적이고 관리하기 쉬운 코드를 작성할 수 있습니다. 적절한 컬렉션을 선택하는 것은 게임의 성능과 유지 보수성에 큰 영향을 미칩니다. 따라서, 여러분이 컬렉션에 대해 정확히 이해하고 상황에 맞는 컬렉션을 활용하여 게임 개발에 더욱 도움이 되었으면 좋겠습니다.