π’ MVC Controller Mapping
κ°μβ
μ΄ λ¬Έμλ Sprout Frameworkμ MVC 컨νΈλ‘€λ¬ λ§€ν ꡬνμ λν κΈ°μ μ λΆμμ μ 곡νλ©°, HTTP μμ²μ 컨νΈλ‘€λ¬ λ©μλλ‘ λΌμ°ν νλ λ° μ¬μ©λλ λ΄λΆ μν€ν μ², μκ³ λ¦¬μ¦, μ€κ³ ν¨ν΄μ κ²ν ν©λλ€.
ν΅μ¬ μν€ν μ²β
κ΅¬μ± μμ μνΈμμ© νλ¦β
HTTP μμ² β HandlerMapping β RequestMappingRegistry β PathPattern λ§€μΉ
β
컨νΈλ‘€λ¬ λ°κ²¬ β HandlerMethodScanner β BeanFactory
β
νΈλ€λ¬ νΈμΆ β HandlerMethodInvoker β μΈμ ν΄κ²°
ν΅μ¬ κ΅¬μ± μμ λΆμβ
1. PathPattern: κ³ κΈ ν¨ν΄ λ§€μΉ μμ§β
ꡬν μΈλΆμ¬ν
PathPattern
ν΄λμ€λ 컀μ€ν
νμ± λ‘μ§κ³Ό ν¨κ» Javaμ μ κ·μ μμ§μ μ¬μ©νλ μ κ΅ν URL ν¨ν΄ λ§€μΉ μμ€ν
μ ꡬνν©λλ€.
public class PathPattern implements Comparable<PathPattern> {
private final String originalPattern;
private final Pattern regex; // λ§€μΉμ μν μ»΄νμΌλ μ κ·μ
private final List<String> varNames; // μμλλ‘ λ³μ μ΄λ¦λ€
private final List<Integer> varGroups; // ν΄λΉ μ κ·μ κ·Έλ£Ήλ€
private final int staticLen; // μ μ μ½ν
μΈ μ κΈΈμ΄
private final int singleStarCount; // * μμΌλμΉ΄λ κ°μ
private final int doubleStarCount; // ** μμΌλμΉ΄λ κ°μ
}
ν¨ν΄ μ»΄νμΌ μκ³ λ¦¬μ¦
μμ±μλ λ€μν ν ν° μ νμ μ²λ¦¬νλ μν λ¨Έμ μ ꡬνν©λλ€.
-
λ³μ ν ν° (
{name}
λλ{name:regex}
)VAR_TOKEN
ν¨ν΄ μ¬μ©:\\{([^/:}]+)(?::([^}]+))?}
- λ³μ μ΄λ¦κ³Ό μ νμ 컀μ€ν μ κ·μ μΆμΆ
- μ μ½λμ§ μμ λ³μμ λν΄
[^/]+
κΈ°λ³Έκ° μ¬μ©
-
μμΌλμΉ΄λ μ²λ¦¬
*
β([^/]+)
(λ¨μΌ κ²½λ‘ μΈκ·Έλ¨ΌνΈ)**
β(.+?)
(μ¬λ¬ μΈκ·Έλ¨ΌνΈ, non-greedy)?
β[^/]
(λ¨μΌ λ¬Έμ)
-
μ μ μ½ν μΈ :
Pattern.quote()
λ₯Ό μ¬μ©νμ¬ μ΄μ€μΌμ΄ν μ²λ¦¬
λͺ μλ μκ³ λ¦¬μ¦
compareTo
λ©μλλ λ€μ€ κΈ°μ€ μ λ ¬ μκ³ λ¦¬μ¦μ ꡬνν©λλ€.
public int compareTo(PathPattern other) {
// μ°μ μμ μμ (μ€λ¦μ°¨μ = λ λͺ
μμ ):
// 1. ** μμΌλμΉ΄λκ° μ μμλ‘
// 2. * μμΌλμΉ΄λκ° μ μμλ‘
// 3. κ²½λ‘ λ³μκ° μ μμλ‘
// 4. μ μ μ½ν
μΈ κ° κΈΈμλ‘
// 5. μ¬μ μ ν¨ν΄ λ¬Έμμ΄
}
μ΄λ κ°μ₯ λͺ μμ μΈ ν¨ν΄μ΄ λ¨Όμ λ§€μΉλλλ‘ νμ¬, κ΄λ²μν ν¨ν΄μ΄ ꡬ체μ μΈ ν¨ν΄μ κ°λ¦¬λ κ²μ λ°©μ§ν©λλ€.
2. HandlerMethodScanner: 리νλ μ κΈ°λ° λ°κ²¬β
μ€μΊλ μ λ΅
μ€μΊλλ λ€λ¨κ³ μ κ·Ό λ°©μμ μ¬μ©ν©λλ€.
- λΉ μ΄κ±°: 컨ν μ΄λμ λͺ¨λ λΉμ λ°λ³΅
- 컨νΈλ‘€λ¬ κ°μ§:
@Controller
μ΄λ Έν μ΄μ νμΈ - λ©μλ λ΄μ±: λ§€ν μ΄λ Έν μ΄μ μ λν λͺ¨λ κ³΅κ° λ©μλ κ²μ¬
- μ΄λ Έν μ΄μ μ²λ¦¬: λ©ν μ΄λ Έν μ΄μ κ³Ό μμ μ²λ¦¬
μ΄λ Έν μ΄μ ν΄κ²° μκ³ λ¦¬μ¦
public RequestMappingInfoExtractor findRequestMappingInfoExtractor(Method method) {
for (Annotation ann : method.getDeclaredAnnotations()) {
// μ§μ @RequestMapping λλ λ©ν μ΄λ
Έν
μ΄μ
λ λ§€ν νμΈ
RequestMapping rm = ann instanceof RequestMapping
? (RequestMapping) ann
: ann.annotationType().getAnnotation(RequestMapping.class);
if (rm == null) continue;
// ν΄λ°± κ³μΈ΅κ³Ό ν¨κ» κ²½λ‘ μΆμΆ: value() β path() β fallback
String[] paths = extractPaths(ann, rm);
HttpMethod[] methods = rm.method();
return new RequestMappingInfoExtractor(path, methods);
}
return null;
}
κ²½λ‘ κ²°ν© λ‘μ§
combinePaths
λ©μλλ URL ꡬμ±μ μ£μ§ μΌμ΄μ€λ₯Ό μ²λ¦¬ν©λλ€.
public String combinePaths(String basePath, String methodPath) {
// λ€μκ³Ό κ°μ κ²½μ°λ€μ μ²λ¦¬:
// ("", "/users") β "/users"
// ("/api", "users") β "/api/users"
// ("/api/", "/users") β "/api/users"
// ("/api", "/") β "/api"
}
3. RequestMappingRegistry: λμμ± λ§€ν μ μ₯μβ
λ°μ΄ν° ꡬ쑰 μ€κ³
private final Map<PathPattern, Map<HttpMethod, RequestMappingInfo>> mappings
= new ConcurrentHashMap<>();
μ΄ μ€μ²©λ λ§΅ ꡬ쑰λ λ€μμ μ 곡ν©λλ€.
- μ€λ λ μμ μ±: λμ μ κ·Όμ μν
ConcurrentHashMap
- ν¨μ¨μ μΈ μ‘°ν: ν¨ν΄ λ§€μΉ ν HTTP λ©μλλ‘ O(1) μ κ·Ό
- λ©λͺ¨λ¦¬ ν¨μ¨μ±: HTTP λ©μλμ λν
EnumMap
μΌλ‘ λ©λͺ¨λ¦¬ μ€λ²ν€λ κ°μ
νΈλ€λ¬ ν΄κ²° μκ³ λ¦¬μ¦
getHandlerMethod
ꡬνμ 3λ¨κ³ μ κ·Ό λ°©μμ μ¬μ©ν©λλ€.
-
ν¨ν΄ λ§€μΉ λ¨κ³
for (PathPattern registeredPattern : mappings.keySet()) {
if (registeredPattern.matches(path)) {
// λ§€μΉλλ νΈλ€λ¬λ€ μμ§
}
} -
μ λ ¬ λ¨κ³
matchingHandlers.sort(Comparator.comparing(RequestMappingInfo::pattern));
-
μ ν λ¨κ³: 첫 λ²μ§Έ(κ°μ₯ λͺ μμ μΈ) λ§€μΉ λ°ν
μ±λ₯ νΉμ±
- μκ° λ³΅μ‘λ: O(n * m) (n = λ±λ‘λ ν¨ν΄ μ, m = νκ· μ κ·μ 볡μ‘λ)
- κ³΅κ° λ³΅μ‘λ: O(p * h) (p = ν¨ν΄ μ, h = ν¨ν΄λΉ HTTP λ©μλ μ)
- μ΅μ ν: λ§€μΉκ° μμ λ μ‘°κΈ° μ’ λ£
4. HandlerMethodInvoker: λ©μλ μ€ν μμ§β
νΈμΆ νμ΄νλΌμΈ
public Object invoke(RequestMappingInfo requestMappingInfo, HttpRequest<?> request) {
// 1. κ²½λ‘ λ³μ μΆμΆ
Map<String, String> pathVariables = pattern.extractPathVariables(request.getPath());
// 2. λ³΅ν© λ¦¬μ‘Έλ²λ₯Ό ν΅ν μΈμ ν΄κ²°
Object[] args = resolvers.resolveArguments(handlerMethod, request, pathVariables);
// 3. 리νλ ν°λΈ λ©μλ νΈμΆ
return handlerMethod.invoke(controller, args);
}
μ΄ μ€κ³λ κ΄μ¬μ¬λ₯Ό λΆλ¦¬νκ³ νμ₯ κ°λ₯ν μΈμ ν΄κ²° μ λ΅μ νμ©ν©λλ€.
κ³ κΈ κΈ°μ κΈ°λ₯β
1. λ©ν μ΄λ Έν μ΄μ μ§μβ
νλ μμν¬λ Spring μ€νμΌ λ©ν μ΄λ Έν μ΄μ μ μ§μν©λλ€.
@RequestMapping(method = HttpMethod.GET)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetMapping {
String[] value() default {};
String[] path() default {};
}
μ€μΊλλ μ΄λ Έν μ΄μ λΆμμ ν΅ν΄ μ΄λ₯Ό κ°μ§ν©λλ€.
RequestMapping rm = ann.annotationType().getAnnotation(RequestMapping.class);
2. μ κ·μ μ»΄νμΌ μ΅μ νβ
ν¨ν΄μ λ±λ‘ μ ν λ² μ»΄νμΌλκ³ λͺ¨λ λ§€μΉ μμ μ μ¬μ¬μ©λ©λλ€.
this.regex = Pattern.compile(re.toString());
μ΄λ μμ² μ²λ¦¬ μ€ λ°λ³΅μ μΈ ν¨ν΄ μ»΄νμΌ μ€λ²ν€λλ₯Ό λ°©μ§ν©λλ€.
3. λ³μ κ·Έλ£Ή λ§€νβ
ꡬνμ λ³μ μ΄λ¦κ³Ό μ κ·μ μΊ‘μ² κ·Έλ£Ή μ¬μ΄μ λ³λ ¬ λ§€νμ μ μ§ν©λλ€.
private final List<String> varNames; // ["id", "category"]
private final List<Integer> varGroups; // [1, 2]
μ΄λ λ¬Έμμ΄ νμ± μμ΄ ν¨μ¨μ μΈ λ³μ μΆμΆμ κ°λ₯νκ² ν©λλ€.
μ±λ₯ λΆμβ
μ΄κΈ°ν μ±λ₯β
컨νΈλ‘€λ¬ μ€μΊλ 볡μ‘λ
- μκ°: O(C * M * A) (C = 컨νΈλ‘€λ¬ μ, M = 컨νΈλ‘€λ¬λΉ λ©μλ μ, A = λ©μλλΉ μ΄λ Έν μ΄μ μ)
- 곡κ°: O(P) (P = μ 체 λ±λ‘λ ν¨ν΄ μ)
- μ΅μ ν: μμ μ ν λ² μν, κ²°κ³Ό μΊμλ¨
λ°νμ μ±λ₯β
μμ² λΌμ°ν 볡μ‘λ
- μ΅μ μ κ²½μ°: κ³ μ ν μ μ ν¨ν΄μΌλ‘ O(1)
- νκ· μ μΈ κ²½μ°: μ λΆμ°λ ν¨ν΄μΌλ‘ O(log P)
- μ΅μ μ κ²½μ°: O(P * R) (P = ν¨ν΄ μ, R = μ κ·μ 볡μ‘λ)
λ©λͺ¨λ¦¬ μ¬μ©λ
- ν¨ν΄ μ μ₯: ν¨ν΄λΉ ~200-500λ°μ΄νΈ (μ κ·μ + λ©νλ°μ΄ν°)
- λ μ§μ€νΈλ¦¬ μ€λ²ν€λ: λ§€νλΉ ~100λ°μ΄νΈ
- μ΄ν©: 1000κ° λ―Έλ§μ μλν¬μΈνΈλ₯Ό κ°μ§ μ ν리μΌμ΄μ
μ λν΄ μΌλ°μ μΌλ‘
<1MB
κ°λ₯
μ΅μ ν μ λ΅β
- ν¨ν΄ μμ: κ°μ₯ λͺ μμ μΈ ν¨ν΄μ λ¨Όμ νμΈ
- μ§μ° μ»΄νμΌ: ν¨ν΄λΉ ν λ²λ§ μ κ·μ μ»΄νμΌ
- ν¨μ¨μ μΈ λ°μ΄ν° ꡬ쑰: HTTP λ©μλμ λν EnumMap, μμνλ 컬λ μ μ λν ArrayList
- λ¨λ½ νκ°: 첫 λ²μ§Έ λ§€μΉμμ μ‘°κΈ° λ°ν
μ€κ³ ν¨ν΄κ³Ό μμΉβ
1. μ λ΅ ν¨ν΄β
CompositeArgumentResolver
κ° νΉμ 리쑸λ²μ μμ- ν΅μ¬ λ‘μ§ μμ μμ΄ νμ₯ κ°λ₯ν μΈμ ν΄κ²° νμ©
2. ν νλ¦Ώ λ©μλ ν¨ν΄β
HandlerMethodScanner
κ° μ€μΊλ μκ³ λ¦¬μ¦ μ μ- νμ ν΄λμ€κ° νΉμ λ¨κ³λ₯Ό μ¬μ μ κ°λ₯
3. λ μ§μ€νΈλ¦¬ ν¨ν΄β
RequestMappingRegistry
κ° λ§€ν μ μ₯μ μ€μν- λ±λ‘κ³Ό μ‘°νλ₯Ό μν ν΅ν©λ μΈν°νμ΄μ€ μ 곡
4. Comparable/Comparator ν¨ν΄β
PathPattern
μ΄ λͺ μλλ³ μμ° μμ ꡬν- μΈλΆ λ‘μ§ μμ΄ μλ μ λ ¬ κ°λ₯
μ€λ₯ μ²λ¦¬ λ° μ£μ§ μΌμ΄μ€β
1. λͺ¨νΈν λ§€νβ
if (cnt != 1) {
System.out.printf("[WARN] %s.%s() - skipped: ambiguous @RequestMapping",
method.getDeclaringClass().getSimpleName(), method.getName());
return null;
}
2. μλͺ»λ ν¨ν΄ ꡬ문β
if (!varMatcher.region(i, pattern.length()).lookingAt()) {
throw new IllegalArgumentException("Invalid variable syntax at index " + i);
}
3. νΈλ€λ¬ μμβ
if (matchingHandlers.isEmpty()) {
return null;
}
μμ€ν μ λͺ νν μ€λ₯ λ©μμ§μ ν΄λ°± λμμΌλ‘ μ΄λ¬ν μλ리μ€λ₯Ό μ²λ¦¬ν©λλ€.
Spring MVCμμ λΉκ΅β
μ μ¬μ β
- μ΄λ Έν μ΄μ κΈ°λ° κ΅¬μ±
- λ³μμ μμΌλμΉ΄λλ₯Ό κ°μ§ ν¨ν΄ λ§€μΉ
- νΈλ€λ¬ λ©μλ λ΄μ±
- μΈμ ν΄κ²° νμ΄νλΌμΈ
μ°¨μ΄μ β
- λ¨μνλ μν€ν μ²: μΆμν λ μ΄μ΄κ° μ μ
- μ κ·μ κΈ°λ° λ§€μΉ: Springμ AntPathMatcher λμ μ§μ μ κ·μ μ»΄νμΌ
- κ΅¬μ± κ°λ₯μ± κ°μ: μΌλ°μ μΈ μ¬μ© μ¬λ‘μ μ§μ€
- μ±λ₯ μ§μ€: μ μ°μ±λ³΄λ€ μλμ μ΅μ ν
νμ₯μ± ν¬μΈνΈβ
1. 컀μ€ν μΈμ 리쑸λ²β
ArgumentResolver
μΈν°νμ΄μ€λ₯Ό ꡬννκ³ CompositeArgumentResolver
μ λ±λ‘
2. 컀μ€ν ν¨ν΄ λ§€μΉβ
μΆκ° ꡬ문μ μ§μνλλ‘ PathPattern
νμ₯