객체 그래프를 JSON/XML로 직렬화할 때 서로를 참조하는 양방향 관계(순환 참조)가 있으면 예외가 발생하거나 무한 루프가 생길 수 있습니다. .NET에서는 여러 전략으로 이 문제를 안전하게 처리할 수 있습니다.
1. 예제 모델과 문제 상황
아래 예제는 Department가 People을, Person이 다시 Department를 참조해 순환 구조를 이룹니다. 기본 System.Text.Json 직렬화는 순환을 감지하면 예외를 던집니다.
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Collections.Generic;
public class Person
{
public string Name { get; set; }
public Department Department { get; set; }
}
public class Department
{
public string Name { get; set; }
public List<Person> People { get; set; } = new();
}
// 그래프 생성
var dept = new Department { Name = "R&D" };
var alice = new Person { Name = "Alice", Department = dept };
dept.People.Add(alice);
// 기본 직렬화 (예외 발생)
// var json = JsonSerializer.Serialize(dept);
2. System.Text.Json - ReferenceHandler.Preserve로 그래프 보존
순환을 메타데이터($id, $ref)로 표현해 원래 그래프를 복원 가능하게 합니다. 페이로드가 커지고 가독성이 떨어지지만, 복원성이 필요한 경우 적합합니다.
using System.Text.Json;
using System.Text.Json.Serialization;
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
string json = JsonSerializer.Serialize(dept, options);
3. System.Text.Json - IgnoreCycles로 참조 무시 (.NET 7+)
순환을 만드는 속성을 자동으로 생략해 직렬화합니다. 원래 그래프는 복원되지 않지만 API 응답 등 단방향 출력에 적합합니다.
using System.Text.Json;
using System.Text.Json.Serialization;
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string json = JsonSerializer.Serialize(dept, options);
4. [JsonIgnore]로 한쪽 참조 끊기
명시적으로 한쪽 참조를 무시해 기본 설정으로도 안전하게 직렬화합니다. 소비자가 양방향 정보가 필요 없다면 가장 단순합니다.
using System.Text.Json.Serialization;
public class Person
{
public string Name { get; set; }
[JsonIgnore]
public Department Department { get; set; }
}
// 이제 기본 설정으로 직렬화 가능
// string json = JsonSerializer.Serialize(dept);
5. Newtonsoft.Json 설정
Newtonsoft.Json을 사용하는 경우 두 가지 접근이 있습니다.
using Newtonsoft.Json;
// 5-1) 순환을 만드는 속성 무시
var settingsIgnore = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Formatting = Formatting.Indented
};
string jsonIgnore = JsonConvert.SerializeObject(dept, settingsIgnore);
// 5-2) 참조 보존 ($id, $ref)
var settingsPreserve = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented
};
string jsonPreserve = JsonConvert.SerializeObject(dept, settingsPreserve);
6. XML 직렬화: DataContractSerializer
XML로 직렬화하면서 그래프를 보존하려면 PreserveObjectReferences를 사용합니다. [DataContract]/[DataMember]로 제어합니다.
using System.Runtime.Serialization;
using System.IO;
using System.Text;
using System.Collections.Generic;
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public Department Department { get; set; }
}
[DataContract]
public class Department
{
[DataMember]
public string Name { get; set; }
[DataMember]
public List<Person> People { get; set; } = new();
}
var settings = new DataContractSerializerSettings
{
PreserveObjectReferences = true
};
var serializer = new DataContractSerializer(typeof(Department), settings);
using var ms = new MemoryStream();
serializer.WriteObject(ms, dept);
string xml = Encoding.UTF8.GetString(ms.ToArray());
7. DTO/Projection으로 안전한 출력 구조 만들기
실무에서는 직렬화 전용 DTO를 만들어 필요한 방향만 포함하는 것이 가장 안전하고 유지보수성도 좋습니다.
using System.Text.Json;
using System.Linq;
var dto = new
{
Department = dept.Name,
People = dept.People.Select(p => new { p.Name })
};
string json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true });
8. 선택 가이드와 주의사항
복원이 필요하면 Preserve(또는 Newtonsoft의 PreserveReferencesHandling)를, 출력만 필요하면 IgnoreCycles나 [JsonIgnore]를 사용합니다. API는 DTO로 투영하는 전략을 권장합니다. EF Core를 사용할 때는 지연 로딩으로 순환이 쉽게 생길 수 있으니 필요 속성만 Select로 투영하고, AsNoTracking을 함께 고려합니다. Preserve 계열은 페이로드가 커지고 클라이언트 파싱 복잡도가 증가하니 비용을 감안해 선택합니다.
'C#' 카테고리의 다른 글
| C# CollectionView를 이용한 데이터 필터링 및 정렬 (0) | 2026.07.13 |
|---|---|
| C# Stopwatch로 함수 호출 빈도 측정하기 (0) | 2026.07.12 |
| C# Span<T>와 stackalloc로 고성능 버퍼 생성 (0) | 2026.07.10 |
| C# 파일 시스템 감시(FileSystemWatcher)로 이벤트 처리 (0) | 2026.07.10 |
| C# BlockingCollection<T>로 생산자-소비자 패턴 구현 (0) | 2026.07.09 |