본문 바로가기
C#

Dictionary 에서 구조체를 TValue 로 사용시, 주의할 점

by Oz Driver 2025. 7. 24.

구조체는 스택 메모리에 생성되기 때문에 값 복사가 일어납니다. 그리고 이로 인해 몇 가지 주의해야 할 점이 있는데,  Dictionary 에서는 이 부분이 특히 더 민감하게 작동합니다. 이번 글에서는 이와 관련해서 자세히 설명해 보겠습니다. 

 

구조체는 값 복사

다음과 같은 코드가 있다고 가정해 보겠습니다.

그리고 "쌍검" 의 수량을 +1 증가하는 코드를 작성해 보려고 합니다. 

struct Item
{
    public string name;
    public string desc;
    public int price;
    public int quantity;
    public float weight;
}

// Dictionary 선언
Dictionary<string, Item> itemTable = new Dictionary<string, Item>();

// 아이템 추가
itemTable.Add( "쌍검", new Item { name = "쌍검", 
                                 desc = "쌍검입니다.", 
                                 price = 100, 
                                 quantity = 0, 
                                 weight = 1.5f } );

 

직접 수정은 컴파일 오류

다음과 같이 직접적인 접근은 컴파일 오류를 일으킵니다.

언뜻 보면, itemTable["쌍검"] 은 원본처럼 보이지만, itemTable[TKey] 는 내부적으로 get; 이 동작하여 반환된 구조체의 복사본입니다. 그렇게 때문에 구조체 복사본의 field 를 아무리 변경해도 원본 field 는 변하지가 않습니다.

다른 컬렉션은 구조체의 복사본을 수정해도 원본 값이 변하지 않을 뿐, 오류를 발생시키지 않지만, Dictionary 만예외적으로 컴파일 오류를 발생시킵니다.

// item 이 구조체이므로 컴파일 오류를 일으킴
itemTable["쌍검"].quantity += 1;

 

한마디로, "구조체의 복사본을 변경하는 것이니 당신의 의도대로 동작하지 않습니다" 라는 경고인 것입니다. 한편으로는, 왜 다른 컬렉션에는 오류를 발생시키지 않을까 궁금합니다.  ( 이렇게 된 정확한 이유는 C# 설계자가 아니기 때문에 잘 모르겠습니다. ㅠ ).

 

복사본만 변경됨

다시 한번 강조하지만, 구조체는 값 복사가 일어나기 때문에, out 의 item 은 복사본입니다. 

따라서 이 값을 수정해도 원본 데이터에는 반영되지 않습니다. 

즉, itemTable["쌍검"].quantity 의 결과값은 0 입니다. 

// 복사본을 가져와서 수정. 원본은 변경되지 않음.
if (items.TryGetValue("쌍검", out Item item))
{
    item.quantity += 1;
    
    // 수량 0 출력 
    Console.WriteLine(items["쌍검"].quantity); 
}

 

복사본을 원본에 다시 복사해야 함

따라서 이 경우, 수정한 복사본을 다시 원본 데이터에 넣어주어야 합니다. 

이렇게 해야, itemTable["쌍검"].quantity 의 결과값은 1 이 됩니다.

참고로 복사가 일어나는 횟수는 out Item item 에서 1번, items["쌍검"] = item 에서 또 한번, 총 2회의 값 복사가 발생합니다. 구조체 크기가 크지 않기 때문에, 이 정도는 무시할만 합니다. 

// 복사본을 가져와서 수정 후, 원본에 다시 복사.
// items["쌍검"].quantity 처럼 값을 직접 수정한 것이 아님. 
if (items.TryGetValue("쌍검", out Item item))
{
    item.quantity += 1;
    items["쌍검"] = item;
    Console.WriteLine(items["쌍검"].quantity);
}

 

클래스는 참조 복사 

클래스는 참조 복사가 일어나므로, 복사본을 수정하면 원본도 변경됩니다. 따라서 Item 구조체를 클래스로 바꾸면 간단히 해결됩니다. itemTable["쌍검"].quantity 은 1 을 출력합니다. 이렇게 직접 변경해도 구조체일 때와는 다르게 컴파일러는 오류를 발생시키지 않습니다. 의도대로 원본이 수정된 것이니, 오류를 발생시키지 않는 것은 당연한 일입니다. 

class Item
{
    public string name;
    public string description;
    public int price;
    public int quantity;
    public float weight;
}

if (items.TryGetValue("쌍검", out Item item))
{
    item.quantity += 1;
    
    // 수량 1 출력 
    Console.WriteLine(items["쌍검"].quantity); 
}

 

위에서는 Item 의 크기가 작기 때문에 값 복사로 인한 복사 비용은 무시할만 하다하였으나, 구조체의 크기가 크다면 class 로 선언하는 것을 고려해야 합니다. 결정적으로 구조체 사용시 값 복사에 따른 실수가 발생할 여지가 크기 때문에 컬렉션과 함께 사용할 경우에는 class 로 선언하는 것을 적극 고려할 만 합니다.

 

Item 클래스 개선 

Item 클래스의 field 를 살펴보면 변하지 않는 값들이 존재합니다. 여기서는 이름, 설명, 가격, 무게 같은 것들입니다. 

만약 프로젝트가 대규모라면, 코드의 명확성과 유지 보수 등을 위해서, 변하는 값과 변하지 않는 값들을 명시적으로 분리해서 관리하는 것이 좋을 듯 합니다. 

 

// 변하지 않는 field
class ItemInfo
{
    public string name;
    public string description;
    public int price;
    public float weight;
}

// 변하는 field
class InventoryItem
{
    public ItemInfo info;
    public int quantity;
}

 

이렇게 분리해두면, 추후에 ItemInfo 클래스만을 이용해 DB 를 구축하는 등의 활용도를 높일 수 있습니다. 

Dictionary<string, InventoryItem> items = new Dictionary<string, InventoryItem>();

ItemInfo longSword = new ItemInfo { name = "장검", 
                                    desc = "장검입니다.", 
                                    price = 100, 
                                    weight = 1.5f };
                                    
ItemInfo spear = new ItemInfo { name = "창", 
                                desc = "창입니다.", 
                                price = 100, 
                                weight = 1.5f };
                                
ItemInfo bow = new ItemInfo { name = "활", 
                              desc = "활입니다.", 
                              price = 100, 
                              weight = 1.5f };
                              
ItemInfo dualSword = new ItemInfo { name = "쌍검", 
                                    desc = "쌍검입니다.", 
                                    price = 100, 
                                    weight = 1.5f };
                                    
ItemInfo gun = new ItemInfo { name = "총", 
                              desc = "총입니다.", 
                              price = 100, 
                              weight = 1.5f };


items.Add("장검", new InventoryItem { info = longSword, quantity = 0 });
items.Add("창", new InventoryItem { info = spear, quantity = 0 });
items.Add("활", new InventoryItem { info = bow, quantity = 0 });
items.Add("쌍검", new InventoryItem { info = dualSword, quantity = 0 });
items.Add("권총", new InventoryItem { info = gun, quantity = 0 });


// 복사본을 수정하면 원본도 같이 수정됩니다. (참조 복사)
if (items.TryGetValue("쌍검", out InventoryItem? item))
{
    item.quantity += 1;                
    item.info.desc = "날카로운 쌍검입니다.";
    item.info.price = (int)(item.info.price * 1.1f);

    Console.WriteLine(item.quantity);                
    Console.WriteLine(item.info.desc);
    Console.WriteLine(item.info.price);
}

 

요약

•   구조체는 값이 복사되므로 복사본을 수정해도 원본에는 반영되지 않습니다.

•   따라서, 컬렉션에서 구조체를 다룰 때는 특히 주의해야 합니다. 

•   복사 비용이 부담스럽거나, 원본 데이터를 직접 다루는 것이 편하다면 클래스로 선언하는 것이 좋습니다.

•   Item 클래스처럼 코드의 명확성과 확장성을 위해 field 내부를 개념적으로 분리해 두는 것이 좋습니다.