HTTP 요청에서 Accept 헤더는 클라이언트(브라우저)가 서버에게 어떤 데이터 형식으로 응답을 받고자 하는지 알려주는 역할을 합니다. 이러한 데이터 형식은 흔히 MIME 타입이라고 불리며, MIME은 Multipurpose Internet Mail Extensions(다목적 인터넷 메일 확장)의 약자입니다.
Accept 헤더를 통한 버전 전달
API 버전 정보는 아래와 같이 Accept 헤더에 담아 서버로 전송할 수 있습니다.
Version=1 → StudentsV1Controller
Version=2 → StudentsV2Controller
그런데 서버 쪽에서 Accept 헤더의 버전 정보를 처리하지 않으면 문제가 발생합니다. 현재 프로젝트에는 StudentsV1과 StudentsV2 컨트롤러만 존재하기 때문에, 라우팅 시점에 버전을 구분하지 못하면 404 Not Found 오류가 반환됩니다.
이 문제를 해결하려면 기본 컨트롤러 선택 로직을 대체하는 CustomControllerSelector를 직접 구현해야 합니다. 이 클래스는 DefaultHttpControllerSelector를 상속하여 작성합니다.
CustomControllerSelector 구현
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
namespace WebAPI.Custom{
public class CustomControllerSelector : DefaultHttpControllerSelector{
private HttpConfiguration _config;
public CustomControllerSelector(HttpConfiguration config) : base(config){
_config = config;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request){
var controllers = GetControllerMapping();
var routeData = request.GetRouteData();
var controllerName = routeData.Values["controller"].ToString();
string versionNumber = "";
var acceptHeader = request.Headers.Accept.Where(a => a.Parameters
.Count(p => p.Name.ToLower() == "version") > 0);
if (acceptHeader.Any()){
versionNumber = acceptHeader.First().Parameters
.First(p => p.Name.ToLower() == "version").Value;
}
HttpControllerDescriptor controllerDescriptor;
if (versionNumber == "1"){
controllerName = string.Concat(controllerName, "V1");
}
else if (versionNumber == "2"){
controllerName = string.Concat(controllerName, "V2");
}
if (controllers.TryGetValue(controllerName, out controllerDescriptor)){
return controllerDescriptor;
}
return null;
}
}
}
위 코드의 동작 방식을 간단히 정리하면 다음과 같습니다.
- 요청의 라우트 데이터에서 컨트롤러 이름을 추출합니다.
- Accept 헤더에서
version매개변수 값을 읽어옵니다. - 버전 값이 "1"이면 컨트롤러 이름 뒤에 V1을, "2"이면 V2를 붙여 실제 컨트롤러를 결정합니다.
기본 컨트롤러 셀렉터 교체 (WebApiConfig.cs)
다음으로, 기본 컨트롤러 셀렉터를 앞서 만든 커스텀 셀렉터로 교체해야 합니다. 이 작업은 WebApiConfig.cs 파일에서 수행합니다. 여기서 IHttpControllerSelector를 CustomControllerSelector로 교체하는데, 그 이유는 DefaultHttpControllerSelector가 IHttpControllerSelector 인터페이스를 구현하고 있기 때문입니다.
public static class WebApiConfig{
public static void Register(HttpConfiguration config){
config.Services.Replace(typeof(IHttpControllerSelector), new
CustomControllerSelector(config));
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}버전별 컨트롤러 예제
StudentV1Controller
using DemoWebApplication.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
namespace DemoWebApplication.Controllers{
public class StudentV1Controller : ApiController{
List<StudentV1> students = new List<StudentV1>{
new StudentV1{
Id = 1,
Name = "Mark"
},
new StudentV1{
Id = 2,
Name = "John"
}
};
public IEnumerable<StudentV1> Get(){
return students;
}
public StudentV1 Get(int id){
var studentForId = students.FirstOrDefault(x => x.Id == id);
return studentForId;
}
}
}
V1 모델은 학생 이름을 단일 Name 필드로 표현합니다.
StudentV2Controller
using DemoWebApplication.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
namespace DemoWebApplication.Controllers{
public class StudentV2Controller : ApiController{
List<StudentV2> students = new List<StudentV2>{
new StudentV2{
Id = 1,
FirstName = "Roger",
LastName = "Federer"
},
new StudentV2{
Id = 2,
FirstName = "Tom",
LastName = "Bruce"
}
};
public IEnumerable<StudentV2> Get(){
return students;
}
public StudentV2 Get(int id){
var studentForId = students.FirstOrDefault(x => x.Id == id);
return studentForId;
}
}
}
V2 모델은 이름을 FirstName과 LastName 두 개의 필드로 분리하여 더 세분화된 구조를 제공합니다. 이처럼 버전마다 응답 모델의 구조가 달라질 수 있기 때문에 Accept 헤더 기반 버전 관리가 유용합니다.
실행 결과 확인
아래는 Accept 헤더에 버전 정보를 포함하여 요청했을 때 StudentV1과 StudentV2 컨트롤러에서 반환되는 결과입니다.


정리
ASP.NET Web API에서 Accept 헤더 기반 버전 관리를 구현하려면 다음 세 가지 단계를 거칩니다.
DefaultHttpControllerSelector를 상속받는 CustomControllerSelector를 작성하여 Accept 헤더의 version 값을 해석합니다.- WebApiConfig.cs에서
IHttpControllerSelector를 커스텀 셀렉터로 교체합니다. - 버전별로 V1, V2 컨트롤러와 모델을 분리하여 관리합니다.
이 방식을 사용하면 URL이나 쿼리 문자열을 변경하지 않고도 HTTP 표준인 Accept 헤더만으로 깔끔하게 API 버전을 관리할 수 있습니다.