1. 도입
상상해보세요: 당신이 Person 클래스 하나를 만들고 객체를 파일에 직렬화했는데, 몇 달 뒤에 주소 같은 새 필드를 추가하거나 몇몇 속성의 타입을 바꿨습니다. 겉보기엔 사소한 일 같지만, 이전 포맷으로 저장된 데이터를 로드(역직렬화)하려고 하면 놀랄 일이 생길 수 있어요: 어떤 값은 로드되지 않거나 예외가 발생하고, 일부 값은 비어 있거나 심지어 잘못될 수도 있습니다.
이런 동작은 역호환성 위반의 전형적인 사례예요. 실제 개발에서는 학생들이 세미콜론을 빼먹는 것보다(즉, 아주 자주) 더 자주 발생합니다.
문제를 예제로 보여주기
간단한 학습용 미니프로젝트를 보죠. 현재 단계에서 클래스가 다음과 같았다고 합시다:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
이 클래스의 인스턴스를 JSON으로 직렬화합니다:
Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);
파일에 저장된 내용:
{"Name":"Alice","Age":35}
이제 일주일 뒤, 앱을 좀 더 트렌디하게 만들려고 주소 필드를 추가합니다:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; } // 새로운 필드
}
그리고 옛 파일을 로드해봅니다:
string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);
무슨 일이 일어날까요? 객체에 주소가 없을 겁니다: Address 속성은 null이 됩니다. 예외는 발생하지 않았어요. 당장은 괜찮아 보입니다... 하지만 타입을 바꾸거나 필드를 삭제하거나 진짜로 "재미있는" 변경을 하면 문제가 시작될 수 있습니다!
2. 어떤 종류의 변경이 있고 — 그 영향은?
클래스 구조의 변경은 직렬화에 다양한 영향을 줍니다. 몇 가지 전형적인 시나리오를 살펴봅시다.
새 속성 추가
이건 가장 안전한 편이에요. 옛 데이터(그 속성이 없던 데이터)는 문제없이 역직렬화됩니다: 새 속성은 기본값을 갖게 됩니다 (null은 참조형, 0은 int 등).
주의: 새 속성이 nullable이 아니고 "합리적인" 기본값이 없다면 문제가 생길 수 있습니다(특히 C# 11+의 required 속성 있는 경우).
속성 삭제
속성을 삭제해도 직렬화된 데이터에 그 속성이 남아 있으면 — 대부분의 직렬화기는 "여분" 필드를 무시하고 로드는 성공할 겁니다.
다만 사용 중인 직렬화기에 따라 다릅니다. 예를 들어 JsonSerializer와 Newtonsoft.Json은 비교적 관대해서 예외를 던지지 않는 편이지만, 오래된 커스텀 직렬화기들은 다르게 동작할 수 있어요.
속성 이름 변경
여기서 골치아파집니다. 단순히 FirstName을 Name으로 이름만 바꾸면, 직렬화기는 옛 데이터의 필드와 새 객체의 속성을 매핑할 수 없습니다. 결과적으로 새 속성은 비어 있게 되고( null/0 ), 파일에 있던 값은 무시됩니다.
속성 타입 변경
예를 들어 예전에는 public int Age였는데, 갑자기 public string Age로 바꿨다고 합시다(누군가 "bessmertny" 같은 걸 쓸 수도 있으니까요). 옛 데이터를 역직렬화하려 하면 에러가 날 수도 있고("Cannot convert number to string"), 혹은 속성이 기본값을 받을 수도 있어요. 구체적 동작은 직렬화기와 타입 엄격성 설정에 따라 달라집니다.
계층 구조 변경(상속, 중첩)
기본 클래스를 바꾸거나 속성을 다른 곳으로 옮기거나, 한 클래스를 다른 클래스로 래핑하면 — 옛 직렬화 데이터는 완전히 호환되지 않을 수 있습니다. 특히 XML과 복잡한 객체 계층에서는 더 엄격하게 문제를 일으키는 경우가 많습니다.
3. 호환성 문제
호환성 문제는 어떻게 알아차리나?
호환성 문제는 보통 즉시 드러나지 않고 애매하게 나타납니다: 앱이 이상하게 동작하거나 일부 데이터가 사라지거나, 로그에 별로 도움이 되지 않는 예외가 남거나. 보통 문제는 다음 상황에서 드러납니다:
- 사용자가 오래된 파일을 새 버전 프로그램에 불러올 때.
- 서버가 클라이언트의 "구버전"에서 온 JSON/XML을 받을 때.
- 외부 API와 작업하는데 그 인터페이스가 갑자기 업데이트될 때.
증상은 다양합니다: 역직렬화 오류부터 "뜻밖에" 비어있는 필드까지.
직렬화기가 호환성에 미치는 영향
직렬화기마다 동작이 다릅니다. 구조 변경에 가장 관대한 건 JSON 직렬화기들 — 표준 System.Text.Json과 Newtonsoft.Json 둘 다 보통 파일에 있는 알 수 없는 속성을 무시하고, 객체에 없는 필드는 직렬화하지 않아요.
XML 쪽은 좀 더 엄격한 편입니다: 루트 엘리먼트나 계층 구조가 바뀌면 오류가 날 가능성이 큽니다.
이진 포맷에서는 순서나 타입이 바뀌면 예외가 날 수 있어요!
4. 위험 최소화 방법 — 접근법과 실무 팁
가능한 문제를 최소화(혹은 완전히 회피)하기 위한 몇 가지 방법입니다.
클래스와 데이터에 버전 사용
직렬화 객체나 파일에 특별 필드 Version을 추가하세요. 그걸로 파일이 어떤 구조 버전으로 만들어졌는지 알 수 있고, 로딩 시 적절한 처리를(예: 데이터 업그레이드) 할 수 있습니다.
public class PersonV2
{
public int Version { get; set; } = 2;
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
이름 매핑 속성 사용(직렬화용)
JSON이나 XML에서는 속성이 직렬화될 때 어떤 이름을 가질지 명시할 수 있어요. 속성 이름을 바꿀 때는 옛 이름을 보존해 매핑해주면 됩니다:
public class Person
{
[JsonPropertyName("FirstName")] // System.Text.Json 용
[JsonProperty("FirstName")] // Newtonsoft.Json 용
public string Name { get; set; }
public int Age { get; set; }
}
nullable 타입과 기본값 활용
새 필드가 옛 데이터에 없을 가능성이 있으면 nullable로 하거나 기본값을 설정해서 역직렬화에 문제 없게 만드세요:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Address { get; set; } = "Unknown";
}
"알 수 없는 필드" 이벤트 처리
Newtonsoft.Json에서는 모르는 필드가 있을 때 훅을 걸어 처리할 수 있습니다. 예를 들어 로그를 남기거나 별도 처리를 할 수 있어요.
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
try
{
var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
Console.WriteLine("역직렬화 실패: " + ex.Message);
}
데이터 마이그레이션
변경이 크면 마이그레이션 단계를 고려하세요: 예전 구조로 읽어서 새 구조로 변환하는 과정을 두는 겁니다.
// 예: PersonV1에는 address가 없었음
public class PersonV1 { public string Name; public int Age; }
// 새 클래스 — address 포함
public class PersonV2 { public string Name; public int Age; public string Address; }
// 마이그레이션:
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);
PersonV2 migrated = new PersonV2
{
Name = oldPerson.Name,
Age = oldPerson.Age,
Address = "Unknown"
};
5. 까다로운 경우와 예상치 못한 오류
필드 불변성(invariant)과 required 속성
C# 11 이상부터는 required 속성이 도입됐습니다. 필드가 required로 표시되어 있으면, 해당 필드가 데이터에 없을 때 역직렬화가 오류를 일으킬 수 있어요:
public class Person
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public required int Age { get; set; }
public string Address { get; set; }
}
옛 데이터에 Age가 없다면 구조 불일치에 대한 예외가 발생합니다.
타입 변경: int → string
// 예전:
public class Record { public int Count; }
// 지금:
public class Record { public string Count; }
데이터에 "Count":42처럼 숫자가 들어있다면 문자열로 역직렬화할 때 스마트 컨버전이 동작할 수 있지만, 반대 방향은 예외가 날 수도 있습니다.
기본 클래스 삭제
직렬화된 객체가 상속 구조를 가졌는데 그 계층 구조를 바꾸면 옛 파일을 역직렬화할 때 오류가 날 수 있습니다. 어떤 경우는 조용히 실패하고 어떤 경우는 명확한 예외를 던집니다.
6. 호환성 작업 시 흔한 실수
실수 #1: 기존 속성을 아무 생각 없이 변경하기.
속성 이름을 바꾸거나 타입을 변경하면 기존 직렬화된 데이터가 손실됩니다.
실수 #2: 새 필드에 대해 nullable을 깜빡하기.
새 속성은 nullable 하거나 합리적인 기본값을 가져야 합니다.
실수 #3: 역호환성 테스트를 안 함.
클래스를 변경했다면 옛 파일/데이터가 여전히 제대로 로드되는지 반드시 확인하세요.
실수 #4: 서로 다른 라이브러리의 어트리뷰트를 섞어 쓰기.
한 속성에 JsonPropertyName과 JsonProperty를 동시에 쓰면 안 됩니다.
GO TO FULL VERSION