1. 직렬화에서 속성이 왜 필요한가?
객체를 JSON이나 XML로 직렬화할 때, 마치 로봇 청소기가 방을 스캔하는 것처럼 동작합니다. 눈에 보이는 것(공개된 속성/필드)은 전부 가방에 들어가고(파일이나 문자열로), 나머지는 무시됩니다. 그런데 가끔 로봇이 내 가방 속 내용을 몰래 보여주지 않았으면 하거나, 항목 이름을 깔끔하게 바꾸고 싶을 때가 있죠 — 예를 들어 러시아어 대신 영어로.
바로 이때 속성이 등장합니다! 속성으로 직렬화 과정을 제어할 수 있어요: 불필요한 것을 숨기고, 이름을 바꾸고, 순서를 붙이고, 객체의 일부를 무시하게 하거나 시리얼라이저에게 특별한 규칙이 있다고 알려줄 수 있습니다. 마치 물건에 붙이는 스티커 같아요: "건드리지 마세요", "중요", "JSON에서 이름 바꿔".
직렬화 제어의 전형적 과제
- 출력 문서에서 속성이나 필드의 이름을 변경하기 (예: JSON에서 FirstName 대신 "first_name" 사용)
- 일부 필드/속성을 직렬화나 역직렬화에서 숨기기 (예: 비밀번호, 내부 카운터)
- 기본값 처리나 null 값 처리 제어하기
- 요소 순서 지정하기 (XML에서 특히 중요)
- XML 요소에 추가 속성(attributes) 붙이기
각 직렬화 플랫폼 — System.Text.Json, Newtonsoft.Json, 또는 XmlSerializer — 은 이러한 목적을 위해 자체 속성 세트를 제공합니다.
2. System.Text.Json용 속성: 최신 스타일
.NET의 기본 JSON 직렬화기는 괜찮은 속성 목록을 제공하며, 이 속성들은 System.Text.Json.Serialization 네임스페이스에 있습니다.
가장 유용한 속성들:
| 속성 | 용도 | 사용 예 |
|---|---|---|
|
JSON에서 속성 이름을 변환 | |
|
속성을 직렬화에서 완전히 제외 | |
|
공개 필드(public field)를 직렬화하도록 포함 | |
|
특정 조건에서 속성을 무시 | |
사용 예:
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; } // JSON에서 이름이 'first_name'이 됨
[JsonIgnore]
public string Password { get; set; } // JSON에 포함되지 않음
[JsonPropertyName("born_year")]
public int? YearOfBirth { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Nickname { get; set; } // null이면 JSON에 포함되지 않음
}
이런 사람을 직렬화해봅시다:
var person = new Person
{
FirstName = "Ivan",
Password = "123456",
YearOfBirth = 2000,
Nickname = null
};
string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}
참고: 비밀번호와 별명은 JSON에서 사라졌습니다! (비밀번호는 항상 무시되고, 별명은 null일 때만 제외됩니다).
중요한 이유: 실무에서 클라이언트(또는 악의적 사용자가) 비밀번호, 토큰, 내부 카운터 등 민감한 정보를 보지 못하게 하고 싶을 때가 많습니다. [JsonIgnore] 같은 속성으로 한 줄만에 해결할 수 있습니다.
3. Newtonsoft.Json용 속성: 유연성의 왕
Newtonsoft.Json을 사용하면(문서) 기능이 더 많고, 오래된 프로젝트에 익숙하면 더 편하게 느껴질 수도 있습니다.
주요 속성:
| 속성 | 목적 |
|---|---|
|
JSON에서 속성 이름을 지정하고, 순서나 필수 여부 등 설정 가능 |
|
필드/속성을 직렬화에서 제외 |
|
역직렬화 시 필수 항목으로 요구 |
|
복잡한 경우를 위한 커스텀 컨버터 지정 |
|
기본값 직렬화 처리 제어 |
실전 예:
using Newtonsoft.Json;
public class UserProfile
{
[JsonProperty("login")]
public string Username { get; set; }
[JsonIgnore]
public string InternalNotes { get; set; }
[JsonProperty(Required = Required.Always)]
public string Email { get; set; }
}
JsonProperty에는 필수성(Required), 순서(Order) 등 추가 옵션이 있습니다. 예:
[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }
4. XML용 속성: 클래식 스타일
XmlSerializer는 자신의 속성 세트를 사용하며, 이들은 System.Xml.Serialization 네임스페이스에 있습니다.
자주 쓰이는 것들:
| 속성 | 용도 |
|---|---|
|
다른 이름의 XML 요소로 직렬화 |
|
속성을 XML 요소의 attribute로 변환 |
|
XML 직렬화에서 속성이나 필드 제외 |
|
컬렉션에 대해 XML 배열 이름 지정 |
|
컬렉션 항목의 XML 요소 이름 지정 |
|
루트 XML 태그의 이름 변경 |
실전 예:
using System.Xml.Serialization;
[XmlRoot("human")]
public class Person
{
[XmlElement("firstname")]
public string Name { get; set; }
[XmlAttribute("years")]
public int Age { get; set; }
[XmlIgnore]
public string Secret { get; set; }
}
var person = new Person { Name = "Anna", Age = 32, Secret = "42" };
직렬화 후의 XML은 대략 이렇습니다:
<human years="32"><firstname>Anna</firstname></human>
Secret 필드는 XML에 포함되지 않았고, Age는 중첩된 태그가 아니라 attribute로 직렬화된 점을 확인하세요.
5. 유용한 뉘앙스
내부 동작: 속성은 어떻게 작동하나
직렬화기가 클래스를 만났을 때, 리플렉션으로(이건 .NET의 마법, 자세한 내용은 System.Reflection 참조) 메타데이터를 읽습니다. 속성으로 JsonIgnore나 XmlElement 같은 표시가 있는지 확인하고, 그에 따라 데이터를 결과 문서에 넣거나 건너뜁니다.
이 방식은 "데이터 스키마"를 비즈니스 로직과 분리하는 편리한 수단입니다. 클래스는 비즈니스 로직을 담고, 속성은 직렬화를 위한 여권 같은 역할을 하죠.
주요 속성 대응표
| 기능 | System.Text.Json | Newtonsoft.Json | XmlSerializer |
|---|---|---|---|
| 이름 변경 | |
|
|
| 무시하기 | |
|
|
| 커스텀 포맷 | |
|
— (IXmlSerializable로 처리, 번거로움) |
| 루트 객체 | — | — | |
| 컬렉션 | — | — | |
6. 고급 시나리오
표준 직렬화 방식으로는 부족할 때가 있습니다. 예를 들어 날짜를 ISO 문자열이 아니라 UNIX timestamp로 저장하고 싶다면, 커스텀 컨버터를 연결해야 합니다.
System.Text.Json에서:
public class UnixDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}
public class LogEntry
{
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime EventTime { get; set; }
}
이제 직렬화할 때 EventTime은 문자열이 아니라 숫자로 표현됩니다.
Newtonsoft.Json에서:
public class BoolToYesNoConverter : JsonConverter<bool>
{
public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
=> writer.WriteValue(value ? "yes" : "no");
public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
=> (string)reader.Value == "yes";
}
public class Answer
{
[JsonConverter(typeof(BoolToYesNoConverter))]
public bool IsCorrect { get; set; }
}
이제 불리언 값은 "yes" 또는 "no"로 직렬화되고, 역직렬화 시 true 또는 false로 복원됩니다.
7. 특징과 함정
속성을 추가할 때 기억하세요: 서로 다른 직렬화기는 서로 다른 속성을 사용합니다. JSON과 XML을 동시에 다루거나(심지어 Newtonsoft.Json과 System.Text.Json을 동시에 쓰는 경우에도) 필요한 속성을 둘 다 달아주지 않으면 깜짝 놀랄 결과가 생깁니다.
속성 이름은 속성으로 오버라이드하지 않으면 직렬화기가 기본 이름을 사용합니다.
상속을 조심하세요: 자식 클래스는 부모의 public 필드/속성을 상속하고, 부모에 붙은 속성도 자식에 적용됩니다. 이 점은 종종 놀라움을 주지만 의도된 동작입니다.
흔한 실수: 개발자가 실수로 직렬화되어야 할 필드에 JsonIgnore를 붙이거나(또는 반대로 민감한 데이터를 무시하지 않는 경우), 예를 들어 XmlElement를 private 필드에 붙이고 왜 직렬라이저가 보지 못하냐고 당황하는 경우가 많습니다. (XmlSerializer는 public 멤버만 처리합니다!)
GO TO FULL VERSION