1. 도입
컬렉션이 “박스에서 바로” 완벽하게 직렬화/역직렬화된다고 믿고 싶겠지만, 실제 프로젝트에선 그렇지 않은 경우가 많습니다. 때로는 내부 캐시 같은 특정 컬렉션을 직렬화에서 숨겨야 하고, API 계약에 맞춰 컬렉션 속성 이름을 바꿔야 할 때도 있습니다. 어떤 경우엔 어떤 요소를 저장하거나 무시할지 제어하거나 컬렉션을 특별히 변환해서 최종 JSON이 다른 서비스에서도 이해하기 쉬운 형태가 되게 해야 합니다.
다행히도 System.Text.Json은 컬렉션과 개별 요소에 적용할 수 있는 어트리뷰트를 통해 직렬화를 간단하고 명확하게 제어할 수 있게 해줍니다. 이 섹션에서는 라이브러리 모델을 확장하면서 실습으로 어떻게 동작하는지 살펴보겠습니다.
2. 컬렉션 속성 제외: [JsonIgnore]
간단한 것부터 시작합시다. 클래스에 직렬화하면 안 되는 컬렉션이 있을 수 있습니다 — 예를 들어 임시적이거나 캐시된 민감한 데이터들. 그럴 땐 당연히 [JsonIgnore]죠!
예를 들어 Library 클래스에 빠른 접근용으로만 쓰는 List<Book> Cache 속성이 있다고 해봅시다:
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
public List<Book> Books { get; set; }
[JsonIgnore]
public List<Book> Cache { get; set; } // 직렬화되지 않음!
}
// 사용 예:
var library = new Library
{
Name = "메인 도서관",
Books = new List<Book>
{
new Book { Title = "마법의 계곡", Author = new Author { Name = "투베 얀손", BirthYear = 1914 } }
},
Cache = new List<Book>
{
new Book { Title = "파충류의 군주", Author = new Author { Name = "윌리엄 골딩", BirthYear = 1911 } }
}
};
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json); // JSON에 Cache 속성 없음!
직렬화 결과는 대략 이렇게 됩니다:
{
"Name": "메인 도서관",
"Books": [
{
"Title": "마법의 계곡",
"Author": {
"Name": "투베 얀손",
"BirthYear": 1914
}
}
]
}
보이시죠? 외부로는 어떤 “캐시”도 없습니다. [JsonIgnore]로 표시된 건 숨겨져 있고, Wi-Fi 비밀번호처럼 안전합니다.
3. [JsonPropertyName]으로 컬렉션 이름 바꾸기
어떤 API는 Books 대신 "items"를 기대할 수도 있죠. C# 필드 이름을 바꾸기 싫지만 JSON에서 다른 이름을 쓰고 싶을 때가 있습니다.
방법은 다음과 같습니다:
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
[JsonPropertyName("items")]
public List<Book> Books { get; set; }
[JsonIgnore]
public List<Book> Cache { get; set; }
}
// 직렬화:
var library = new Library { Name = "지점 #1", Books = new List<Book>() };
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
출력:
{
"Name": "지점 #1",
"items": []
}
역직렬화도 JSON의 items를 C#의 Books에 잘 매핑합니다 — 양방향으로 작동합니다.
4. 컬렉션과 요소 직렬화 제어
이럴 땐 JsonIgnoreCondition.WhenWritingNull과 nullable을 같이 쓰면 됩니다.
예를 들어 컬렉션이 선택적 필드인 경우가 있어요. 방금 생성한 도서관에는 아직 책이 하나도 없을 수 있죠. JSON에 books: null가 나오지 않게 하려면 옵션으로 제어할 수 있습니다:
var library = new Library { Name = "빈 도서관" };
// Books는 초기화되지 않음 = null
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
string json = JsonSerializer.Serialize(library, options);
Console.WriteLine(json);
결과:
{
"Name": "빈 도서관"
}
반면에 빈 리스트(하지만 null이 아닌 경우)는 직렬화되면 "books": []가 됩니다. null과 빈 리스트를 구분하는 건 중요합니다 — 때로는 필드가 null일 땐 의도적으로 숨겨야 하고, 빈 리스트는 드러내야 할 때가 있으니까요.
5. 요소 속성에 대한 [JsonIgnore] 적용
직렬화 어트리뷰트는 컬렉션 내부 요소에도 적용됩니다. 리스트에 들어있는 각 객체의 특정 속성을 숨길 수 있습니다.
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
[JsonIgnore]
public string InternalCode { get; set; }
}
이제 컬렉션 Books에 들어있는 책을 직렬화할 때 InternalCode는 JSON에 나오지 않습니다.
6. “인덱스” 또는 중첩 구조로 컬렉션 주소 지정하기
컬렉션을 단순한 배열이 아니라 예를 들어 각 책이 고유 ID를 가지는 경우처럼 “맵”(dictionary) 형태로 직렬화해야 할 때가 있습니다. 이 경우 요소에 특수한 어트리뷰트 없이 표준 타입으로 딕셔너리를 선언하면 됩니다:
public class Library
{
[JsonPropertyName("catalog")]
public Dictionary<string, Book> BookCatalog { get; set; }
}
딕셔너리를 직렬화하면 키-값 쌍을 가진 객체로 변환됩니다:
var library = new Library
{
BookCatalog = new Dictionary<string, Book>
{
["978-5-699-12345-6"] = new Book { Title = "사전", Author = new Author { Name = "알려지지 않음", BirthYear = 2000 } }
}
};
JSON:
{
"catalog": {
"978-5-699-12345-6": {
"Title": "사전",
"Author": {
"Name": "알려지지 않음",
"BirthYear": 2000
}
}
}
}
이 표현은 키와 객체의 연관을 유지해야 하는 API에 유용합니다.
7. 컬렉션 직렬화 제어 시 발생하는 오류와 어려움
만약 컬렉션에 모든 요소가 제대로 초기화되어 있지 않다면(예: 리스트에 null이 있음) 기본 동작으로는 System.Text.Json이 배열 내에 그런 요소들을 null로 기록합니다.
심지어 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull로 설정해도, 배열 내부의 null 요소들은 남아 있습니다 — 이 규칙은 객체의 속성에 적용되며 컬렉션 내용물까지 자동으로 제거하지 않습니다. 이를 피하려면 직렬화 전에 컬렉션을 정리하세요: RemoveAll(b => b == null).
역직렬화 시 흔한 혼동은 이름 불일치입니다. [JsonPropertyName]을 깜빡하면 클래스는 Books를 기대하는데 JSON에는 items가 들어오면 컬렉션은 채워지지 않고 비어 있게 됩니다. 이름을 항상 확인하세요!
8. 표: 주요 어트리뷰트 적용 가능 범위
| 어트리뷰트 | 컬렉션에 적용 가능? | 컬렉션 요소에 적용 가능? | 사용 예시 |
|---|---|---|---|
|
예 | 예 | 리스트나 Book 내부 필드 숨기기 |
|
예 | 예 | Books → items 또는 Title → name 으로 이름 바꾸기 |
|
예 | 예 | private 속성을 직렬화에 포함시키기 |
|
예 | 예 | 리스트에 특별 컨버터 지정하기 |
9. 어트리뷰트가 적용된 컬렉션 직렬화 스키마
+-------------+
| Library |
+-------------+
| Name -- "Name"으로 직렬화
| Books -- [JsonPropertyName("items")], "items": [...]
| Cache -- [JsonIgnore], 직렬화되지 않음
| BookCatalog -- [JsonPropertyName("catalog")], "catalog": {...} 으로 직렬화
대략적인 JSON 결과:
{
"Name": "시립 도서관",
"items": [
{
"Title": "1984",
"Author": {
"Name": "조지 오웰",
"BirthYear": 1903
}
},
{
"Title": "위대한 기대",
"Author": {
"Name": "찰스 디킨스",
"BirthYear": 1812
}
}
],
"catalog": {
"978-1234567890": {
"Title": "크툴루의 부름",
"Author": {
"Name": "하워드 필립스 러브크래프트",
"BirthYear": 1890
}
}
}
}
10. 실무적 의미와 면접/프로젝트에서의 특징
실전에서는 항상 외부 API 계약과 직렬화 요구사항을 신경 써야 합니다. 내부 컬렉션을 “숨기는” 법, 이름 대소문자/스타일을 맞추는 법, 때로는 클라이언트 버전에 따라 직렬화 스키마를 동적으로 바꿔야 하는 상황까지 고려해야 합니다.
면접에서 자주 나오는 질문들:
- 데이터의 일부만 직렬화하려면?
- 컬렉션 속성이 JSON에 포함되지 않게 하려면?
- C# 속성 이름과 JSON 이름이 다를 때 어떻게 매핑하나?
- 컬렉션 내부의 특정 요소(예: 민감 정보)를 직렬화에서 숨길 수 있나?
정답은 어트리뷰트와 직렬화 옵션을 적절히 사용하는 것과 컬렉션 내용물을 신중히 처리하는 데 있습니다: [JsonIgnore], [JsonPropertyName], JsonSerializerOptions 등을 잘 활용하세요.
GO TO FULL VERSION