The Evolution of .NET: 현대 웹 애플리케이션에서 AI 및 모바일 네이티브 기능 통합
Milan Jovanović는 최근 발전된 API 버전 관리에 반대하는 강력한 논쟁을 발표했습니다. 핵심 요점: 대부분의 팀은 계약 진화 전략이 부족하여 v2을 너무 일찍 사용합니다. 버전 관리는 호환 도구일 뿐, 설계 전략이 아닙니다.
이 주장은 Iron Software의 엔지니어링 팀과 공감합니다. 우리는 .NET 라이브러리를 제공하며, 이 말은 우리의 제품의 공개 표면이 API라는 의미입니다. 모든 메소드 시그니춰, 모든 속성, 모든 기본 동작은 수천 개의 고객 코드베이스 안에 있는 계약입니다. 주 버전 증가가 출시가 아닙니다. 이는 모든 하위 이용자에게 이주 프로젝트입니다.
다음은 라이브러리 저자 관점에서 Milan의 글을 개발자가 살펴보는 것이며, REST API든 NuGet 패키지를 제공하든 상관없이 동일한 호환성 규칙이 어떻게 적용되는지 보여줍니다.
핵심 요약
- 버전 관리는 설계 전략이 아닙니다. 공존이 실패할 때의 탈출구입니다.
- 파괴적인 변경은 URL이나 스키마가 아니라 행동에 숨깁니다.
- 네 가지 호환성 규칙: 제거하지 않기, 처리 변경하지 않기, 유효성 검사 제한하지 않기, 추가 사항을 선택사항으로 유지하기.
- 새로운 작업은 거의 항상 새로운 버전보다 저렴합니다.
- 진정한 더 이상 사용되지 않음은 런타임 신호 및 텔레메트리가 필요하며, 문서 업데이트만으로는 불충분합니다.
HTTP 규칙은 라이브러리 API에도 적용됩니다
밀란은 /orders을 위한 REST API에 대한 논의를 구성하지만, 동일한 규칙이 NuGet 패키지로 배포되는 공개 C# 클래스에도 적용됩니다. 매핑은 직접적입니다:
| REST API 변경 | NuGet 라이브러리 등가 |
|---|---|
| JSON 필드 이름 변경 | 공개 속성 이름 변경 |
| 엔드포인트 제거 | 공개 메소드 제거 |
| 요청 유효성 검증 강화 | null 허용되지 않는 매개변수 추가 |
| 작업 동작 변경 | 메소드의 내부 동작 변경 |
| 필드 추가 | 생성자 매개변수 추가 |
인기 있는 .NET 라이브러리의 주요 버전을 가져와 이름이 변경된 API를 수정하는 데 반나절을 소비한 적이 있다면, 이는 아마도 추가적으로 처리될 수 있었던 v2 결정의 수신 측에 있었던 것입니다.
소비자에게 실제로 무엇이 끔찍한 충격을 줄 수 있나요?
Milan의 목록은 정확합니다:
- 필드 제거 또는 이름 변경
- 기존 데이터의 의미 변경
- 요청 유효성 검증 강화
- 페이지닝 또는 오류 형식 변경
- 열거형 같은 값을 영구적으로 닫혀 있다고 가정
두 번째 항목은 대부분 팀이 무방비로 맞닥뜨리는 것입니다: 체계를 바꾸지 않고 기존 데이터의 의미를 변경하는 것입니다. JSON은 동일하게 보입니다. C# 시그니처는 동일하게 보입니다. 모든 것이 컴파일됩니다. 런타임에 아무 것도 던지지 않습니다. 그러나 이제 필드가 다른 것을 의미하며, 이전 의미에 의존한 모든 소비자는 조용히 잘못된 것입니다.
Milan의 예시:
// Before
{ "total": 100 }
// After
{ "total": { "amount": 100, "currency": "USD" } }같은 필드 이름. 같은 엔드포인트. 이제 total을 숫자로 파싱한 모든 클라이언트가 깨졌습니다.
라이브러리 등가는 메소드가 반환하는 것 또는 입력을 해석하는 방식을 변경하는 것입니다. 이전에 덮어썼지만 이제 덧붙이는 Save() 메서드. 기본값이 true에서 false으로 바뀌는 Trim 매개변수. 유효하지 않은 입력에 던지던 메소드가 이제 default 값을 조용히 반환하는 것입니다.
네 가지 호환성 규칙
Milan은 규칙을 요약합니다: 아무것도 제거하지 말고, 처리 규칙을 변경하지 말고, 선택적인 것을 필수로 만들지 말고, 추가된 모든 것은 선택사항이어야 한다고 합니다. 네 가지 원칙은 공개 API를 담당하는 모든 팀에게 유용합니다:
- 기존 필드 및 행동을 그대로 유지하십시오.
- 선택적 요청 데이터를 필수 데이터로 바꾸지 마십시오.
- 기존 작업의 동작을 변경하지 마십시오.
- 새로 추가된 것은 선택사항이며 기본값으로 추가하여야 합니다.
이들은 직접적으로 라이브러리 설계로 직역됩니다. "아무것도 빼지 말라"는 공개 멤버를 삭제하지 말라는 것을 의미합니다. "처리 규칙을 변경하지 말라"는 기존 메소드는 배포되었을 때 행동해야 할 방식으로 행동해야 한다는 것을 의미합니다. "선택적을 필수로 만들지 마라"는 기존 메소드에 필수 매개변수를 추가하지 말라는 것을 의미합니다. 대신 오버로드를 제공합니다. "부가 적이고 선택사항으로"는 새 기능은 새로운 메소드나 기본값이 현명한 선택 매개변수에 있어야 한다는 것을 의미합니다.
이것이 실제로 어떻게 발생하나요
이 규칙들을 설명하는 가장 명확한 방법은 실제 API 결정으로 설명하는 것입니다. 여기 우리의 사례가 있습니다.
몇 개의 릴리즈 전, IronPDF는 HTML-to-PDF 변환에 대한 더 풍부한 렌더링 옵션 세트를 지원해야 했습니다: 사용자 정의 용지 크기, 사용자 정의 여백, CSS 미디어 에뮬레이션, 헤더 및 푸터 템플릿 등. 간단한 접근 방식은 새 옵션을 수용할 수 있도록 기존의 렌더링 메소드를 변경하는 것이었습니다. 이 결정은 간단한 형태로 API를 사용하는 모든 고객을 깨뜨렸을 것입니다.
맥락상, 라이브러리는 표준 .NET 패키지 채널을 통해 설치됩니다.
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdfIronPdf NuGet 패키지는 1800만 이상의 다운로드를 축적했으며, 이는 API 안정성이 중요한 이유 중 하나이며, 모든 변화는 그 많은 통합에 영향을 미칩니다.
대신 우리가 제공한 접근 방식:
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");' The original, three-year-old API. Still works. Still unchanged.
Dim renderer As New ChromePdfRenderer()
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
' New rendering options live on an options object, not in the method signature.
renderer = New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {.HtmlFragment = "..."}
pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")결정에 대한 세 가지 관찰:
- 원래
RenderHtmlAsPdf(string html)서명은 변경되지 않습니다. 업그레이드한 고객은 코드를 수정할 필요가 없었습니다. - 새로운 기능은 소비자가 선택할 수 있는 옵션 객체에 있습니다. 메소드에는 새로운 필수 매개변수가 없습니다.
RenderingOptions의 기본값은 이전 API와 동등한 출력을 생성합니다. 어떤 것도 구성하지 않는 사람들에게 행동은 변하지 않습니다.
이는 Milan의 목록에서 1, 2, 4번 규칙이 적용된 것입니다. 제품은 발전했습니다. 계약은 변하지 않았습니다.
RenderHtmlAsPdfV2(string html, RenderingOptions options)을 배포하고 싶은 유혹이 실제로 있었습니다. API 참조 페이지에서는 더 깔끔하게 보였을 것입니다. 그러나 모든 고객에게 이주 비용이 들었을 것입니다. 우리는 다르게 선택했습니다.
관대해야 하는 독자들
Milan의 추가 사항을 대체하지 말라는 주장의 나머지 절반은 소비자도 책임을 져야 한다는 것입니다. 잘 동작하는 클라이언트는 이해하지 못하는 필드를 무시해야 합니다.
.NET에서는 System.Text.Json이 기본적으로 알 수 없는 속성을 무시하며, 이는 올바른 기본값입니다. 위험은 보통 두 곳에서 나타납니다:
- 예상하지 못한 필드를 거부하는 엄격한 스키마로 생성된 SDK
- JSON 동등성을 정확히 주장하는 계약 테스트
이 두 가지는 "알지 못하는 필드를 무시한다"는 보장을 장애물로 바꿉니다. 만약 서버가 새로운 선택적 속성을 추가하는 순간 CI가 깨진다면, 역 하향 호환성이 없는 것입니다. 당신은 호환성 정책으로 포장한 회귀 탐지기를 가지게 된 것입니다.
행동도 계약의 일부
부드러운 삭제에서 하드 삭제로 조용히 전환하는 것에 관한 밀란의 섹션은 우리가 본 이 문제에 대한 가장 명확한 서술입니다.
URL은 동일합니다. 요청 본문도 동일합니다. 응답 형식도 동일합니다. 서버에서의 조작은 다릅니다.
이는 스키마 diff에서 아무것도 잡아내지 못하는 가장 위험한 종류의 파괴적 변입니다. OpenAPI 사양은 동일합니다. 생성된 클라이언트는 컴파일됩니다. 통합 테스트는 통과됩니다. 그리고 "삭제된 주문은 복구 가능하다"는 도구를 만들기 위해 구축한 모든 소비자는 생산 중 데이터를 조용히 삭제합니다.
라이브러리 등가는 시그니처를 변경하지 않고 메소드의 작동을 변경하는 것입니다. 명시적으로 피한 예시:
- 이전에는 동기식으로 플러쉬되었고 조용히 비동기 파이어-앤-포겟이 된
Save()메서드 - 결과를 정리하던 OCR 메소드가 이제 후처리를 시작하는 것
- 읽을 수 없는 입력에 대해 던지던 바코드 리더가 이제 빈 문자열을 반환하는 것
각각은 개선된 것처럼 위장한 계약 파괴입니다. 적절한 대응은 Milan과 동일합니다: 새 메소드나 옵션을 추가하고, 기존 행동은 변하지 않게 유지하며, 텔레메트리가 안전하다고 지적할 때에만 이전 경로를 더 이상 사용하지 않는다고 합니다.
유효성 검사 강화
이 카테고리는 결국 모든 팀에 영향을 미칩니다. 두 가지 변형의 동일한 실수가 있습니다:
- 기존 선택적 필드를 필수로 만드는 것
- 새 필드를 추가하고 처음부터 필수로 표시하는 것
두 가지 모두 오래된 클라이언트를 중단시킵니다. 경로는 이동하지 않지만, 이전에 성공하던 요청이 이제 런타임에서 실패합니다.
라이브러리 버전의 이 실수는 필수 생성자 매개변수를 추가하거나 기존 선택적 매개변수를 필수로 만드는 것입니다. 모든 기존 호출자가 컴파일 시간에 깨지며, 이는 런타임 오류보다 선호되지만, 여전히 모든 소비자에게 이주 비용을 부여합니다.
더 안전한 경로:
- 전환 기간 동안 누락된 값을 허용하고 가능한 곳에서 기본값을 유추하십시오.
- 더 풍부한 입력 모양을 요구하는 새로운 오버로드 또는 빌더를 추가하십시오.
- 더 엄격한 워크플로에 대한 새로운 작업이나 생성자를 도입하십시오.
기본 규칙은 일관적입니다: 계약에 추가된 것은 필수적으로 선택사항이어야 하며, 이전에 선택사항이었던 것은 계속 선택사항이어야 합니다. 진정으로 엄격한 요구가 필요하다면, 이는 기존 작업의 제한이 아닌 새 작업에 속합니다.
새로운 작업은 거의 항상 새로운 버전보다 저렴합니다
이것이 가장 받아들이기 가치 있는 원칙입니다.
using 사례가 기존 엔드포인트가 깔끔하게 지원하는 범위를 초과하는 경우, 일반적인 반사는 플래그와 함께 엔드포인트를 오버로드하는 것입니다:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=true더 파괴적으로는, 이 변경을 버전 문제로 선언하고 /v2/orders 작업을 시작합니다. 둘 다 보통 잘못된 것입니다. 더 깔끔한 접근은 기존 것과 함께 새 작업을 추가하는 것입니다:
POST /orders
POST /orders/quote
POST /checkout-sessions각 작업은 깔끔한 계약, 독특한 권한, 독립적인 검증 및 자체 발전 경로를 가집니다. 원래 엔드포인트는 단순하게 유지됩니다. API의 나머지는 주요 버전 변화에 휘말리지 않습니다.
라이브러리 문맥에서의 등가는 선택적 매개변수로 읽기 어려울 때 기존 메소드 대신 새 매소드를 추가하는 것입니다. ExtractText()은 간단한 텍스트 추출기로 남아 있습니다. ExtractTextWithLayout()은 더 풍부한 변형이 됩니다. ExtractStructuredDocument()은 가장 풍부해집니다. 명확한 계약을 가진 세 가지 메서드가 여덟 개의 선택적 매개변수가 있는 하나의 메서드보다 바람직합니다.
계획적으로 더 이상 사용되지 않게 하십시오
이것은 대부분의 팀이 건너뛰는 API 변경 관리의 절반이며, 이즘이 전략이 작동하는지를 결정합니다.
실제 더 이상 사용되지 않음은 변경 로그의 알림이 아닙니다. 이는 네 가지 단계가 포함됩니다:
- OpenAPI 설명에서 필드나 엔드포인트를 더 이상 사용되지 않음으로 표시합니다 (.NET 세계에서는
[Obsolete]속성을 사용하여). - 런타임에서 더 이상 사용되지 않음을 신호하여 실시간 트래픽이 이를 표면화하게 하세요.
- 실제 마이그레이션 가이드에 연결하세요.
- 사용 데이터 수집으로 안전한 제거 시점을 결정하세요.
HTTP API의 경우 런타임 신호는 간단합니다:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation".NET 라이브러리의 경우, 이에 상응하는 것은 [Obsolete("대신 NewMethod 사용합니다."] 입니다. 이것은 v2026.x에서 제거될 것입니다", DiagnosticId = "IRON001")] attribute paired with a UrlFormat이 마이그레이션 페이지를 가리킵니다. 컴파일러 경고는 모든 소비자의 빌드 출력에 표면화되며, 진단 식별자는 의도적인 억제를 가능하게 하고, 링크는 소비자에게 문서화된 마이그레이션 경로를 제공합니다.
데이터 수집 단계는 타협할 수 없습니다. 어떤 고객이 더 이상 사용되지 않는 메소드에 여전히 의존하고 있는지 모르면, 제거는 추측 작업이 됩니다. 이는 활성 통합을 중단시키는 조기 제거 또는 더 이상 사용되지 않음을 무의미하게 만드는 무기한 유지 비용을 초래할 수 있습니다.
버전 관리가 적절한 때
밀란은 버전 관리에 반대하지 않으며, 우리도 마찬가지입니다. 버전 관리는 다음과 같은 경우 적절합니다:
- 오래된 의미 체계와 새로운 의미 체계가 정말로 공존할 수 없을 때
- 리소스 모델이 근본적으로 변경된 경우
- 호환성 규칙이 아무도 이해할 수 없는 계약을 강제할 경우
포인트는 버전 관리를 완전히 피하는 것이 아닙니다. 포인트는 공존이 실패했기 때문에 버전 관리를 선택하는 것이지, 처음부터 테이블에 올라온 첫 아이디어였기 때문이 아닙니다.
버전 관리가 필요할 때, 실제로 더 이상 사용되지 않음 프로세스와 짝을 이루어야 합니다. 어려운 작업은 v2을 배포하는 것이 아닙니다. 어려운 작업은 소비자를 v1에서 벗어나게 하는 것입니다.
결정 규칙
밀란의 틀은 적용하기에 올바른 것입니다:
- 대체하지 않고 추가할 수 있나요?
- 마이그레이션 창 동안 오래된 계약과 새로운 계약이 공존할 수 있나요?
- 오래된 작업을 변경하지 않고 새로운 작업을 도입할 수 있나요?
- 문서, 헤더, 및 데이터 수집으로 오래된 모양을 더 이상 사용되지 않음으로 할 수 있나요?
모든 네 가지에 '예'라고 대답할 수 있다면, 새로운 버전은 필요 없을 가능성이 높습니다. 대답이 '아니요'이고 두 세계가 정말로 공존할 수 없다면, 의도적으로 버전 관리를 수행하세요.
계약을 진화 가능하게 설계하세요. 소비자를 오늘의 코드가 아닌 오래 지속되는 통합으로 취급하세요. 호환성이 정말 다 써버렸을 때의 경우에만 버전 관리를 예약하세요.
전체 내용과 더 긴 작업 예제는, 밀란의 원본 게시글을 읽어보세요.
.NET 라이브러리를 선택할 때 주목해야 할 질문은 밀란의 글이 구축된 질문입니다: 3년 후에도 이 라이브러리가 내가 통합한 API처럼 보일까요?
그것이 우리가 매 릴리스에서 답하려고 노력하는 질문입니다. 2020년의 간단한 호출은 여전히 작동합니다. 새로운 기능은 기존 기능 옆에 선택적이고 덧붙여집니다. 강제적인 주요 버전 마이그레이션은 없습니다.
이러한 라이브러리 설계 접근 방식이 필요한 것과 맞다면, 무료 30일 체험판을 시작하고 직접 API 참조를 검토하세요. 5분 빠른 시작에는 설치, 라이선스 활성화, 및 첫 렌더링 PDF 생성이 포함됩니다. 패키지 자체는 아무 .NET 프로젝트에서 한 명령으로 가능합니다:
NuGet이 선호되는 경로가 아닌 환경에서는, 직접 다운로드가 DLL과 Windows 설치 프로그램을 제공합니다.
