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 구조 | 언제 사용? |
|---|---|---|
|
|
키가 단순한 문자열이고, 빠른 조회와 유일성이 필요할 때 |
|
|
키가 복잡한 타입이거나 순서 제어가 필요하고 중복을 허용하고 싶을 때 |
8. 면접에서 자주 나오는 질문들
1. Dictionary<DateTime, string>을 직렬화할 수 있나?
네, 하지만 키는 문자열 표현으로 변환됩니다(보통 ISO 형식인 "yyyy-MM-ddTHH:mm:ss" 같은 형태). 역직렬화할 때 로케일이나 날짜 포맷 이슈가 생길 수 있어요.
2. Dictionary<int, string>을 직렬화하면 무슨 일이 일어나나?
키는 문자열로 직렬화됩니다. 원래 딕셔너리의 키가 숫자여도 JSON에서는 문자열로 나오고, 역직렬화하면 정상적으로 숫자로 복원됩니다.
3. 왜 객체를 키로 가진 딕셔너리를 직렬화할 수 없나?
JSON 객체의 이름은 문자열만 될 수 있어서요. 객체를 프로퍼티 이름으로 쓸 수는 없습니다.
4. 복잡한 키를 가진 딕셔너리를 직렬화하고 싶으면?
구조를 다시 검토하거나, 딕셔너리를 키-값 쌍의 리스트로 직렬화해서 키를 객체로 그대로 표현하는 방법을 쓰는 게 낫습니다.
GO TO FULL VERSION