CodeGym /행동 /C# SELF /클래스 구조 변경 시 호환성

클래스 구조 변경 시 호환성

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

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은 참조형, 0int 등).

주의: 새 속성이 nullable이 아니고 "합리적인" 기본값이 없다면 문제가 생길 수 있습니다(특히 C# 11+의 required 속성 있는 경우).

속성 삭제

속성을 삭제해도 직렬화된 데이터에 그 속성이 남아 있으면 — 대부분의 직렬화기는 "여분" 필드를 무시하고 로드는 성공할 겁니다.

다만 사용 중인 직렬화기에 따라 다릅니다. 예를 들어 JsonSerializerNewtonsoft.Json은 비교적 관대해서 예외를 던지지 않는 편이지만, 오래된 커스텀 직렬화기들은 다르게 동작할 수 있어요.

속성 이름 변경

여기서 골치아파집니다. 단순히 FirstNameName으로 이름만 바꾸면, 직렬화기는 옛 데이터의 필드와 새 객체의 속성을 매핑할 수 없습니다. 결과적으로 새 속성은 비어 있게 되고( null/0 ), 파일에 있던 값은 무시됩니다.

속성 타입 변경

예를 들어 예전에는 public int Age였는데, 갑자기 public string Age로 바꿨다고 합시다(누군가 "bessmertny" 같은 걸 쓸 수도 있으니까요). 옛 데이터를 역직렬화하려 하면 에러가 날 수도 있고("Cannot convert number to string"), 혹은 속성이 기본값을 받을 수도 있어요. 구체적 동작은 직렬화기와 타입 엄격성 설정에 따라 달라집니다.

계층 구조 변경(상속, 중첩)

기본 클래스를 바꾸거나 속성을 다른 곳으로 옮기거나, 한 클래스를 다른 클래스로 래핑하면 — 옛 직렬화 데이터는 완전히 호환되지 않을 수 있습니다. 특히 XML과 복잡한 객체 계층에서는 더 엄격하게 문제를 일으키는 경우가 많습니다.

3. 호환성 문제

호환성 문제는 어떻게 알아차리나?

호환성 문제는 보통 즉시 드러나지 않고 애매하게 나타납니다: 앱이 이상하게 동작하거나 일부 데이터가 사라지거나, 로그에 별로 도움이 되지 않는 예외가 남거나. 보통 문제는 다음 상황에서 드러납니다:

  • 사용자가 오래된 파일을 새 버전 프로그램에 불러올 때.
  • 서버가 클라이언트의 "구버전"에서 온 JSON/XML을 받을 때.
  • 외부 API와 작업하는데 그 인터페이스가 갑자기 업데이트될 때.

증상은 다양합니다: 역직렬화 오류부터 "뜻밖에" 비어있는 필드까지.

직렬화기가 호환성에 미치는 영향

직렬화기마다 동작이 다릅니다. 구조 변경에 가장 관대한 건 JSON 직렬화기들 — 표준 System.Text.JsonNewtonsoft.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: 서로 다른 라이브러리의 어트리뷰트를 섞어 쓰기.
한 속성에 JsonPropertyNameJsonProperty를 동시에 쓰면 안 됩니다.

1
설문조사/퀴즈
시리얼라이즈 설정, 레벨 45, 레슨 4
사용 불가능
시리얼라이즈 설정
오브젝트 시리얼라이즈 설정하기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION