CodeGym /행동 /C# SELF /딕셔너리 직렬화

딕셔너리 직렬화

C# SELF
레벨 46 , 레슨 1
사용 가능

1. 소개

딕셔너리(또는 C#의 Dictionary<TKey, TValue>)는 "키-값" 쌍의 컬렉션이에요. 고유한 식별자로 빠르게 값을 찾아야 할 때(예: 전화번호부에서 이름으로 전화번호 찾기) 아주 유용하죠.

리스트(List<T>)와 달리 항목 순서는 중요할 수 있지만, 딕셔너리는 키로 빠르게 접근하는 데 초점이 있어요. 리스트를 직렬화하면 JSON 배열로 간단히 나오지만, 딕셔너리를 직렬화할 땐 몇 가지 주의할 점이 있어요:

  • 키는 직렬화 가능한 타입이어야 해요(대부분 문자열이지만 때로는 숫자나 다른 객체일 수도 있어요).
  • JSON에는 별도의 "словарь" 같은 타입은 없어요 — 객체(object)나 배열(array)만 있어요.

이제 .NET이 딕셔너리를 어떻게 직렬화하는지, 어떤 문제가 생길 수 있는지, 그리고 그런 구조를 제대로 다루게 코드를 어떻게 짜야 할지 자세히 봅시다.

2. 문자열 키를 가진 딕셔너리 직렬화

클래식한 경우부터 시작해요 — 키와 값이 모두 문자열인 딕셔너리.


// 예제 딕셔너리: 책과 그 작가
var books = new Dictionary<string, string>
{
    ["마스터와 마르가리타"] = "미하일 불가코프",
    ["해리 포터"] = "조앤 롤링",
    ["파리대왕"] = "윌리엄 골딩"
};

// JSON 문자열로 직렬화
string json = JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("딕셔너리가 JSON으로 직렬화되었습니다:\n" + json);

// 파일에 저장(동기)
File.WriteAllText("books.json", json);
Console.WriteLine("JSON이 books.json 파일에 기록되었습니다.");

// 파일에서 다시 읽기(동기)
string jsonFromFile = File.ReadAllText("books.json");

// 딕셔너리로 역직렬화
var restoredBooks = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonFromFile);
Console.WriteLine("역직렬화 결과:");
foreach (var pair in restoredBooks)
    Console.WriteLine($"{pair.Key} -> {pair.Value}");

파일 books.json에 뭐가 들어갈까?

{
  "마스터와 마르가리타": "미하일 불가코프",
  "해리 포터": "조앤 롤링",
  "전쟁과 평화": "윌리엄 골딩"
}

작동 원리

직렬화기는 우리의 Dictionary<string, string>을 JSON 객체로 바꿉니다. 각 키는 프로퍼티 이름이 되고, 값은 그 프로퍼티 값이 되죠. 키가 문자열이고 고유하다면 이 방식이 편리합니다.

3. 비표준 키 타입을 가진 딕셔너리

문제는 키가 문자열일 때는 간단하다는 거예요. 숫자라면?


var bookIds = new Dictionary<int, string>
{
    [1001] = "마스터와 마르가리타",
    [1002] = "해리 포터",
    [1003] = "파리대왕"
};

string jsonIntKeys = JsonSerializer.Serialize(bookIds, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonIntKeys);

결과:

{
  "1001": "마스터와 마르가리타",
  "1002": "해리 포터",
  "1003": "파리대왕"
}

무슨 일이 일어난 걸까?

  • C#은 숫자 키를 문자열로 변환했어요. JSON의 프로퍼티 이름은 문자열만 허용되거든요.
  • 역직렬화할 때는 Dictionary<int, string>로 다시 바꿀 때 문자열을 숫자로 변환하려고 시도합니다.

역직렬화 예시:


var restoredBookIds = JsonSerializer.Deserialize<Dictionary<int, string>>(jsonIntKeys);
// 잘 작동해요! 키가 다시 숫자가 됩니다.
그런데 키가 복잡한 타입(예: 객체)이라면?

var dict = new Dictionary<Author, string>
{
    [new Author { Name = "골딩", BirthYear = 1911 }] = "파리대왕"
};

이런 딕셔너리를 직렬화하려 하면 예외가 발생해요:

System.NotSupportedException: Serialization and deserialization of 'Dictionary<Author, string>' instances are not supported.

왜 그럴까?

JSON 객체의 프로퍼티 이름은 문자열뿐이에요. 그래서 직렬화 시 키는 문자열로 명확히 표현 가능한 단순 타입이어야 합니다(보통 문자열이나 숫자). 복잡한 객체를 키로 쓰는 건 표준 직렬화로는 불가능해요.

4. 값으로 복잡한 객체를 가지는 딕셔너리

키는 해결했으니, 값이 복잡한 객체(예: Book이나 Author)일 때는 어떻게 되는지 봅시다.

예제


public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
}

var authorDirectory = new Dictionary<string, Author>
{
    ["bulgakov"] = new Author { Name = "미하일 불가코프", BirthYear = 1891 },
    ["golding"] = new Author { Name = "윌리엄 골딩", BirthYear = 1911 }
};

string jsonAuthors = JsonSerializer.Serialize(authorDirectory, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonAuthors);

결과:

{
  "bulgakov": {
    "Name": "미하일 불가코프",
    "BirthYear": 1891
  },
  "golding": {
    "Name": "윌리엄 골딩",
    "BirthYear": 1911
  }
}
  • 중첩된 객체는 객체 구조에 맞게 직렬화됩니다.
  • Dictionary<string, Author>로의 역직렬화도 문제없이 동작합니다.

5. 다른 객체 내부에 포함된 딕셔너리

딕셔너리는 더 복잡한 객체의 필드로 자주 사용됩니다. 예를 들어, 도서관의 카탈로그에서 각 키는 장르 이름이고 값은 그 장르의 책 목록일 수 있어요.


public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class Library
{
    public Dictionary<string, List<Book>> CatalogByGenre { get; set; }
}

var library = new Library
{
    CatalogByGenre = new Dictionary<string, List<Book>>
    {
        ["공상과학"] = new List<Book>
        {
            new Book { Title = "솔라리스", Author = "스타니스와프 렘" }
        },
        ["고전"] = new List<Book>
        {
            new Book { Title = "파리대왕", Author = "윌리엄 골딩" },
            new Book { Title = "보이는 어둠", Author = "윌리엄 골딩" }
        }
    }
};

string jsonLibrary = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonLibrary);

출력 JSON 일부:

{
  "CatalogByGenre": {
    "공상과학": [
      {
        "Title": "솔라리스",
        "Author": "스타니스와프 렘"
      }
    ],
    "고전": [
      {
        "Title": "파리대왕",
        "Author": "윌리엄 골딩"
      },
      {
        "Title": "보이는 어둠",
        "Author": "윌리엄 골딩"
      }
    ]
  }
}

문제없이 작동해요 — 중첩된 딕셔너리와 컬렉션은 재귀적으로 직렬화되고 역직렬화됩니다.

6. 딕셔너리 직렬화의 특징과 함정

중복 키

딕셔너리는 키가 항상 유일해야 해요. 그런데 사람이 수동으로 중복 키가 있는 JSON을 넣으면:

{
  "foo": "first",
  "foo": "second"
}

결과: 마지막 값("second")이 첫 값을 덮어써요. 대부분의 JSON 파서가 이렇게 동작합니다 — 오류는 발생하지 않아요.

항목 순서

딕셔너리는 순서가 없는 컬렉션이에요. 직렬화 시 JSON의 키 순서는 원본과 다를 수 있어요. 순서가 중요하면 키-값 쌍의 리스트(List<KeyValuePair<string, T>>)를 쓰세요. 하지만 보통 딕셔너리는 순서가 중요하지 않습니다.

JSON과 중첩 딕셔너리

중첩 깊이는 제한이 없지만, 각 레벨은 JSON 제약(키는 문자열, 값은 유효한 JSON 객체/배열 등)을 만족해야 합니다.

JsonSerializerOptions 사용

프로퍼티 이름을 PascalCase 대신 camelCase로 바꿔야 할 때가 있어요. 특히 JavaScript 프론트엔드와 연동하면 camelCase가 표준인 경우가 많습니다.


var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};

string camelJson = JsonSerializer.Serialize(authorDirectory, options);

중요: 딕셔너리에서는 이 옵션이 딕셔너리의 키에는 영향을 주지 않고(키는 C#에서 지정된 문자열 그대로 직렬화됩니다), 딕셔너리 안에 있는 객체들의 프로퍼티 이름에만 적용돼요.

7. 복잡하고 비문자열 키 관련 문제

문자열 키나 숫자 키(int, long, Guid 등)는 기본적으로 잘 동작해요. 하지만 사용자 정의 클래스나 struct를 키로 쓰면 NotSupportedException이 발생합니다.

이런 경우에 대한 회피 방법:

  • 딕셔너리를 키/값 필드를 가진 객체 배열로 직렬화하는 등 다른 저장 포맷을 사용한다.
  • 복잡한 키를 문자열로 변환하고 다시 복원하는 JsonConverter를 직접 작성한다.
  • 키가 정말 복잡하다면 설계를 다시 검토해서 복잡한 객체를 딕셔너리 키로 쓰지 않는 게 좋다.

키-값 쌍 리스트로 직렬화하는 우회 예시


public class AuthorInfo
{
    public Author Author { get; set; }
    public string Book { get; set; }
}
// Dictionary<Author, string> 대신
var list = new List<AuthorInfo>
{
    new AuthorInfo { Author = new Author { Name = "윌리엄 골딩", BirthYear = 1911 }, Book = "파리대왕" }
};
// 이런 리스트는 문제없이 직렬화됩니다

비교: 직렬화할 때 딕셔너리 vs 키-값 리스트

컬렉션 타입 JSON 구조 언제 사용?
Dictionary<string, Book>
{ "key1": {...}, "key2": {...} }
키가 단순한 문자열이고, 빠른 조회와 유일성이 필요할 때
List<KeyValuePair<string, Book>>
[{"Key": "key1", "Value": {...}}, ... ]
키가 복잡한 타입이거나 순서 제어가 필요하고 중복을 허용하고 싶을 때

8. 면접에서 자주 나오는 질문들

1. Dictionary<DateTime, string>을 직렬화할 수 있나?
네, 하지만 키는 문자열 표현으로 변환됩니다(보통 ISO 형식인 "yyyy-MM-ddTHH:mm:ss" 같은 형태). 역직렬화할 때 로케일이나 날짜 포맷 이슈가 생길 수 있어요.

2. Dictionary<int, string>을 직렬화하면 무슨 일이 일어나나?
키는 문자열로 직렬화됩니다. 원래 딕셔너리의 키가 숫자여도 JSON에서는 문자열로 나오고, 역직렬화하면 정상적으로 숫자로 복원됩니다.

3. 왜 객체를 키로 가진 딕셔너리를 직렬화할 수 없나?
JSON 객체의 이름은 문자열만 될 수 있어서요. 객체를 프로퍼티 이름으로 쓸 수는 없습니다.

4. 복잡한 키를 가진 딕셔너리를 직렬화하고 싶으면?
구조를 다시 검토하거나, 딕셔너리를 키-값 쌍의 리스트로 직렬화해서 키를 객체로 그대로 표현하는 방법을 쓰는 게 낫습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION