ASP.NET Web API에서 DefaultHttpControllerSelector 클래스는 URI를 통해 전달된 요청에 맞는 적절한 컨트롤러 액션 메서드를 선택하는 역할을 담당합니다.
쿼리 문자열 기반 버전 관리의 문제점
예를 들어, 아래와 같이 쿼리 문자열을 통해 버전 정보를 전달하는 방식으로 API 버전 관리를 구현해야 한다고 가정해 보겠습니다.
v=1 → StudentsV1Controller (버전 1)
v=2 → StudentsV2Controller (버전 2)
이때 https://localhost:58174/api/student?v=1 처럼 쿼리 문자열에 버전 정보를 담아 요청하면 404 Not Found 오류가 반환됩니다.
그 이유는 DefaultHttpControllerSelector 내부의 SelectController() 메서드가 'StudentsController'라는 이름의 컨트롤러를 찾기 때문입니다. 하지만 실제 프로젝트에는 StudentsV1Controller와 StudentsV2Controller만 존재하므로 컨트롤러를 찾지 못하고 404 오류가 발생하게 됩니다.
CustomControllerSelector 구현
이 문제를 해결하려면 DefaultHttpControllerSelector 클래스를 상속받는 사용자 정의 컨트롤러 선택기인 CustomControllerSelector를 직접 작성해야 합니다.
예제 코드
using System.Net.Http;
using System.Web;
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();
// 버전 번호의 기본값은 "1"로 설정
string versionNumber = "1";
var versionQueryString = HttpUtility.ParseQueryString(request.RequestUri.Query);
if (versionQueryString["v"] != null)
{
versionNumber = versionQueryString["v"];
}
if (versionNumber == "1")
{
controllerName = controllerName + "V1";
}
else if (versionNumber == "2")
{
controllerName = controllerName + "V2";
}
HttpControllerDescriptor controllerDescriptor;
if (controllers.TryGetValue(controllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
return null;
}
}
}기본 컨트롤러 선택기를 교체하기
다음 단계는 기본 컨트롤러 선택기를 위에서 만든 사용자 정의 컨트롤러 선택기로 교체하는 것입니다. 이 작업은 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 }
);
}
}버전별 컨트롤러 작성
이제 각 버전에 해당하는 컨트롤러를 작성합니다. 먼저 버전 1용 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;
}
}
}다음은 버전 2용 StudentV2Controller입니다. 버전 2에서는 Name 필드가 FirstName과 LastName으로 분리된 모델 구조 변화가 있다고 가정합니다.
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;
}
}
}실행 결과
아래 출력 결과는 쿼리 문자열에 버전 정보(v=1, v=2)를 포함하여 요청했을 때 StudentV1Controller와 StudentV2Controller에서 각각 반환되는 응답을 보여줍니다. 같은 URL 경로라도 v 매개변수 값에 따라 서로 다른 버전의 컨트롤러가 실행되는 것을 확인할 수 있습니다.