🔥 ASP.NET Core Route Constraints (라우트 제한 조건)
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0
ASP.NET Core의 라우팅 시스템에서 라우트 제한 조건(Route Constraints)은 URL 경로의 특정 세그먼트(라우팅 매개변수)에 대한 유효성 검사 규칙을 정의하는 강력한 기능입니다.
이를 통해 특정 URL 패턴이 매칭되기 위한 조건을 명시하고, 잘못된 형식의 URL 요청이 특정 라우트에 매칭되는 것을 방지할 수 있습니다.
1️⃣ 라우트 제한 조건의 필요성
1. 정확한 라우트 매칭:
동일한 경로 프리픽스를 가지는 여러 라우트 중에서 가장 적절한 라우트를 선택할 수 있도록 돕습니다.
- 예:
/products/123
(ID로 조회)와/products/shoes
(이름으로 조회)를 구분
2. 유효성 검사:
특정 매개변수가 예상하는 형식(예: 숫자, GUID, 날짜)인지 강제하여, 컨트롤러 액션 또는 핸들러에서 타입 변환 오류를 줄입니다.
3. 보안 및 견고성:
잘못된 형식의 입력으로 인한 잠재적인 오류나 공격 시도를 줄이는 데 기여합니다.
4. URL 디자인 강화:
더욱 명확하고 의미론적인 URL 구조를 유지할 수 있도록 돕습니다.
2️⃣ 라우트 제한 조건 정의 방법
라우트 제한 조건은 라우트 템플릿 내에서 라우팅 매개변수 이름 뒤에 콜론(:
)을 붙이고 제한 조건 이름을 지정하여 정의합니다.
{매개변수명:제한조건이름}
// Program.cs 또는 Startup.cs의 UseEndpoints 블록 endpoints.MapGet("/products/{id:int}", (int id) => Results.Ok($"Product ID: {id}"));
3️⃣ 주요 라우트 제한 조건 종류 및 예시
ASP.NET Core는 다양한 내장 라우트 제한 조건을 제공합니다.
✅ 타입 기반 제한 조건
ASP.NET Core는 다양한 내장 라우트 제한 조건을 제공합니다.
제한 조건 | 설명 | 예시 | 매칭되는 URL | 매칭되지 않는 URL |
int | 32비트 정수로 매칭됩니다. | {id:int} | /items/123 | /items/abc, /items/1.5 |
bool | 불리언 값(true/false)으로 매칭됩니다. | {isActive:bool} | /status/true | /status/other |
datetime | DateTime 형식으로 매칭됩니다. | {date:datetime} | /events/2023-10-26 | /events/today |
decimal | decimal 형식으로 매칭됩니다. | {price:decimal} | /products/99.99 | /products/abc |
double | double 형식으로 매칭됩니다. | {value:double} | /data/3.14159 | /data/xyz |
float | float 형식으로 매칭됩니다. | {measurement:float} | /sensor/0.5f | /sensor/abc |
guid | GUID(Globally Unique Identifier)로 매칭됩니다. | {itemId:guid} | /item/a1b2c3d4-e5f6-7890-1234-567890abcdef | /item/invalid-guid |
long | 64비트 정수로 매칭됩니다. | {userId:long} | /users/9876543210 | /users/short |
// Program.cs 또는 Startup.cs app.UseEndpoints(endpoints => { // 정수형 ID만 허용 endpoints.MapGet("/products/{id:int}", (int id) => Results.Ok($"Getting product by ID: {id}")); // GUID 형식의 아이템 ID만 허용 endpoints.MapGet("/items/{itemId:guid}", (Guid itemId) => Results.Ok($"Getting item by GUID: {itemId}")); // 특정 날짜 형식의 이벤트만 허용 endpoints.MapGet("/events/on/{date:datetime}", (DateTime date) => Results.Ok($"Events on: {date.ToShortDateString()}")); });
✅ 길이 및 범위 기반 제한 조건
매개변수 값의 길이 또는 숫자 범위를 제한합니다.
제한 조건 | 설명 | 예시 | 매칭되는 URL | 매칭되지 않는 URL |
min(value) | 최소값 value 이상이어야 합니다. | {age:min(18)} | /users/age/18, /users/age/25 | /users/age/17, /users/age/abc |
max(value) | 최대값 value 이하여야 합니다. | {quantity:max(100)} | /order/items/50 | /order/items/101 |
range(min,max) | min과 max 사이의 값이어야 합니다. | {year:range(2000,2024)} | /posts/2023 | /posts/1999, /posts/2025 |
minlength(length) | 최소 length 길이 이상이어야 합니다. | {code:minlength(3)} | /code/abc, /code/abcd | /code/ab |
maxlength(length) | 최대 length 길이 이하여야 합니다. | {name:maxlength(10)} | /user/john | /user/someverylongname |
length(min,max) | min과 max 사이의 길이어야 합니다. | {zip:length(5,9)} | /zip/12345, /zip/12345-6789 | /zip/123, /zip/1234567890 |
app.UseEndpoints(endpoints => { // 연령이 18세 이상인 사용자만 허용 endpoints.MapGet("/users/age/{age:min(18)}", (int age) => Results.Ok($"Adult user, age: {age}")); // 재고 수량이 10개에서 100개 사이인 경우에만 유효 endpoints.MapGet("/inventory/{count:range(10,100)}", (int count) => Results.Ok($"Checking inventory count: {count}")); // 최소 5자, 최대 10자의 사용자 이름만 허용 endpoints.MapGet("/profile/{username:length(5,10)}", (string username) => Results.Ok($"Profile for user: {username}")); });
✅ 정규식 기반 제한 조건
정규 표현식(Regular Expression)을 사용하여 매개변수 값의 패턴을 지정합니다.
이는 가장 유연한 제한 조건입니다.
제한 조건 | 설명 | 예시 | 매칭되는 URL | 매칭되지 않는 URL |
regex(pattern) | pattern 정규식과 일치해야 합니다. | {code:regex(^[A-Z]{3}\\d{4}$)} | /item/ABC1234 | /item/abc1234, /item/ABC123 |
app.UseEndpoints(endpoints => { // "abc"로 시작하고 숫자로 끝나는 코드만 허용 endpoints.MapGet("/validate/{code:regex(abc\\d+)}", (string code) => Results.Ok($"Validated code: {code}")); // 상품 코드가 "PROD-"로 시작하고 4자리 숫자로 끝나는 경우만 허용 endpoints.MapGet("/productcodes/{productCode:regex(^PROD-\\d{{4}}$)}", (string productCode) => Results.Ok($"Valid product code: {productCode}")); // 참고: 정규식 내의 중괄호는 이스케이프 필요 ({{, }}) });
✅ 다른 일반적인 제한 조건
제한 조건 | 설명 | 예시 | 매칭되는 URL | 매칭되지 않는 URL |
alpha | 알파벳 문자(a-z, A-Z)만 허용합니다. | {name:alpha} | /users/john | /users/john123 |
alphanum | 알파벳 문자 또는 숫자만 허용합니다. | {token:alphanum} | /api/token/abc123 | /api/token/abc-123 |
required | 매개변수가 반드시 존재해야 합니다. (?와 함께 사용 불가) | {country:required} | /location/korea | /location/ |
url | 유효한 URL 형식이어야 합니다. | {redirectUrl:url} | /redirect/https://google.com | /redirect/not-a-url |
minlength, maxlength, length | string 형식에 대한 길이 제한 | 위 “길이 및 범위 기반” 섹션 참고 |
app.UseEndpoints(endpoints => { // 이름은 알파벳만 허용 endpoints.MapGet("/greet/{name:alpha}", (string name) => Results.Ok($"Hello, {name}!")); // 토큰은 알파벳 또는 숫자만 허용 endpoints.MapGet("/verify/{token:alphanum}", (string token) => Results.Ok($"Verifying token: {token}")); // 국가 매개변수는 반드시 제공되어야 함 endpoints.MapGet("/country/{country:required}", (string country) => Results.Ok($"Selected country: {country}")); });
✅ 사용자 지정 라우트 제한 조건 (Custom Route Constraints)
내장된 제한 조건만으로는 부족할 때, IRouteConstraint
인터페이스를 구현하여 자신만의 커스텀 라우트 제한 조건을 만들 수 있습니다.
IRouteConstraint
인터페이스 구현
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using System.Globalization; public class CustomYearConstraint : IRouteConstraint { public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { // routeKey는 매개변수 이름 (예: "year") // values는 모든 라우팅 매개변수와 그 값들을 담고 있는 딕셔너리 if (values.TryGetValue(routeKey, out object? value)) { if (value is string yearString && int.TryParse(yearString, out int year)) { // 2000년 이후의 연도만 허용하는 예시 return year >= 2000 && year <= DateTime.Now.Year + 1; } } return false; } }
제한 조건 등록
Startup.cs
의 ConfigureServices
또는 Program.cs
에서 AddRouting
메서드를 사용하여 커스텀 제한 조건을 등록합니다.
// Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddRouting(options => { options.ConstraintMap.Add("customYear", typeof(CustomYearConstraint)); }); var app = builder.Build(); app.UseRouting(); app.UseEndpoints(endpoints => { // /articles/2023 와 같이 customYear 제한 조건을 사용 endpoints.MapGet("/articles/{year:customYear}", (int year) => Results.Ok($"Articles from year: {year}")); }); app.Run(); // 사용 예시: // /articles/2023 -> 매칭됨 // /articles/1999 -> 매칭되지 않음 // /articles/2026 (현재 연도가 2025년이라면) -> 매칭되지 않음 (DateTime.Now.Year + 1 조건에 따라)
4️⃣ Endpoint Selection Order (엔드포인트 선택 순서)
라우팅 시스템이 여러 엔드포인트 중 어떤 엔드포인트를 현재 요청에 매칭시킬지 결정하는 방식에 대한 내용입니다.
이는 라우트 제한 조건(Route Constraints)의 라우트 순서와 밀접하게 관련되어 있습니다.
ASP.NET Core의 Endpoint Selection Order는 주로 다음 원칙에 따라 이루어집니다.
✅ 명시적 순서 (Order Property)
가장 직접적인 제어 방법은 엔드포인트에 Order
속성을 부여하는 것입니다. 숫자가 낮을수록 우선순위가 높습니다.
이는 MapControllerRoute
나 MapRazorPages
와 같은 메서드에는 직접 적용하기 어렵고, 주로 MapGet
등의 Minimal API 엔드포인트나 사용자 지정 RouteEndpoint
를 만들 때 사용됩니다.
app.UseEndpoints(endpoints => { // Order가 -10인 엔드포인트 (가장 높은 우선순위) app.MapGet("/priority-test", () => "High priority").WithOrder(-10); // Order가 0인 기본 엔드포인트 (기본값) endpoints.MapGet("/test", () => "Normal priority"); });
Note: WithMetadata
를 통해 RouteEndpointBuilder
에 직접 Order
를 지정하는 것은 일반적인 사용법은 아니며, 실제로는 내부적으로 복잡한 로직을 통해 우선순위가 결정되거나 RouteEndpoint.Order
속성이 설정됩니다.
예를 들어, MapRazorPages
나 MapControllers
에서 생성되는 엔드포인트는 내부적으로 우선순위를 가집니다.)
✅ 라우트 템플릿의 구체성 (Specificity):
이것이 가장 일반적이고 중요한 순서 결정 기준입니다.
라우팅 시스템은 요청 URL과 가장 “구체적으로” 일치하는 라우트 템플릿을 선호합니다.
구체성은 다음과 같은 요소에 의해 결정됩니다:
리터럴 세그먼트의 수: 더 많은 리터럴(고정된 문자열) 세그먼트를 포함하는 라우트가 더 높은 우선순위를 가집니다.
/products/all
(2개의 리터럴)이/products/{id}
(1개의 리터럴, 1개의 매개변수)보다 우선순위가 높습니다.
라우트 제한 조건의 사용: 제한 조건이 있는 매개변수는 제한 조건이 없는 매개변수보다 더 구체적인 것으로 간주됩니다.
/products/{id:int}
는/products/{id}
보다 우선순위가 높습니다. (/products/123
이 주어지면,id:int
가 먼저 매칭됨)
선택적 매개변수: 선택적 매개변수가 없는 라우트가 더 구체적인 것으로 간주됩니다.
/users/{id}
가/users/{id?}
보다 우선순위가 높습니다.
Catch-all 매개변수 (*
또는 **
): 가장 덜 구체적인 것으로 간주되어 거의 항상 마지막에 매칭됩니다.
/files/{*path}
는 일반적으로 다른 모든 구체적인 라우트 뒤에 위치해야 합니다.
✅ 정의 순서 (Declaration Order):
동일한 구체성을 가진 두 개 이상의 라우트가 있을 경우, UseEndpoints
또는 Map()
계열 메서드에서 먼저 정의된 라우트가 우선순위를 가집니다.
이 때문에 “라우트 순서” 섹션에서 언급했듯이, 더 구체적인 라우트를 항상 먼저 정의하는 것이 중요합니다.
app.UseEndpoints(endpoints => { // 이 라우트가 먼저 정의됨 endpoints.MapGet("/products/special", () => "Special Product Page"); // 이 라우트는 구체성이 동일하지만 나중에 정의됨 endpoints.MapGet("/products/{name}", (string name) => $"Product by Name: {name}"); // /products/special 요청 시, "Special Product Page"가 반환됨 // 만약 순서가 바뀌면 "/products/{name}"에 매칭되어 "Product by Name: special"이 반환될 수 있음 });
Endpoint Selection Order는 복합적인 요소들의 상호작용으로 결정됩니다.
라우트 제한 조건은 이 선택 과정에서 라우트의 “구체성”을 높여 특정 URL 패턴에 대한 우선순위를 부여하는 핵심적인 방법입니다.
개발자는 이러한 원칙을 이해하고 라우트를 신중하게 정의하여 예상치 못한 라우팅 문제를 방지해야 합니다.