CodeGym /행동 /C# SELF /컬렉션 직렬화 제어

컬렉션 직렬화 제어

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

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. 표: 주요 어트리뷰트 적용 가능 범위

어트리뷰트 컬렉션에 적용 가능? 컬렉션 요소에 적용 가능? 사용 예시
[JsonIgnore]
예 예 리스트나 Book 내부 필드 숨기기
[JsonPropertyName]
예 예 Books → items 또는 Title → name 으로 이름 바꾸기
[JsonInclude]
예 예 private 속성을 직렬화에 포함시키기
[JsonConverter]
예 예 리스트에 특별 컨버터 지정하기

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 등을 잘 활용하세요.

2
과제
C# SELF, 레벨 46, 레슨 4
잠금
컬렉션 속성 이름 바꾸기
컬렉션 속성 이름 바꾸기
1
설문조사/퀴즈
컬렉션 직렬화, 레벨 46, 레슨 4
사용 불가능
컬렉션 직렬화
중첩 및 계층적 객체 직렬화
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION