본문으둜 κ±΄λ„ˆλ›°κΈ°

🎒 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; // ** μ™€μΌλ“œμΉ΄λ“œ 개수
}

νŒ¨ν„΄ 컴파일 μ•Œκ³ λ¦¬μ¦˜

μƒμ„±μžλŠ” λ‹€μ–‘ν•œ 토큰 μœ ν˜•μ„ μ²˜λ¦¬ν•˜λŠ” μƒνƒœ 머신을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.

  1. λ³€μˆ˜ 토큰 ({name} λ˜λŠ” {name:regex})

    • VAR_TOKEN νŒ¨ν„΄ μ‚¬μš©: \\{([^/:}]+)(?::([^}]+))?}
    • λ³€μˆ˜ 이름과 선택적 μ»€μŠ€ν…€ μ •κ·œμ‹ μΆ”μΆœ
    • μ œμ•½λ˜μ§€ μ•Šμ€ λ³€μˆ˜μ— λŒ€ν•΄ [^/]+ κΈ°λ³Έκ°’ μ‚¬μš©
  2. μ™€μΌλ“œμΉ΄λ“œ 처리

    • * β†’ ([^/]+) (단일 경둜 μ„Έκ·Έλ¨ΌνŠΈ)
    • ** β†’ (.+?) (μ—¬λŸ¬ μ„Έκ·Έλ¨ΌνŠΈ, non-greedy)
    • ? β†’ [^/] (단일 문자)
  3. 정적 μ½˜ν…μΈ : Pattern.quote()λ₯Ό μ‚¬μš©ν•˜μ—¬ μ΄μŠ€μΌ€μ΄ν”„ 처리

λͺ…μ‹œλ„ μ•Œκ³ λ¦¬μ¦˜

compareTo λ©”μ„œλ“œλŠ” 닀쀑 κΈ°μ€€ μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

public int compareTo(PathPattern other) {
// μš°μ„ μˆœμœ„ μˆœμ„œ (μ˜€λ¦„μ°¨μˆœ = 더 λͺ…μ‹œμ ):
// 1. ** μ™€μΌλ“œμΉ΄λ“œκ°€ μ μ„μˆ˜λ‘
// 2. * μ™€μΌλ“œμΉ΄λ“œκ°€ μ μ„μˆ˜λ‘
// 3. 경둜 λ³€μˆ˜κ°€ μ μ„μˆ˜λ‘
// 4. 정적 μ½˜ν…μΈ κ°€ 길수둝
// 5. 사전식 νŒ¨ν„΄ λ¬Έμžμ—΄
}

μ΄λŠ” κ°€μž₯ λͺ…μ‹œμ μΈ νŒ¨ν„΄μ΄ λ¨Όμ € λ§€μΉ˜λ˜λ„λ‘ ν•˜μ—¬, κ΄‘λ²”μœ„ν•œ νŒ¨ν„΄μ΄ ꡬ체적인 νŒ¨ν„΄μ„ κ°€λ¦¬λŠ” 것을 λ°©μ§€ν•©λ‹ˆλ‹€.

2. HandlerMethodScanner: λ¦¬ν”Œλ ‰μ…˜ 기반 λ°œκ²¬β€‹

μŠ€μΊλ‹ μ „λž΅

μŠ€μΊλ„ˆλŠ” 닀단계 μ ‘κ·Ό 방식을 μ‚¬μš©ν•©λ‹ˆλ‹€.

  1. 빈 μ—΄κ±°: μ»¨ν…Œμ΄λ„ˆμ˜ λͺ¨λ“  λΉˆμ„ 반볡
  2. 컨트둀러 감지: @Controller μ–΄λ…Έν…Œμ΄μ…˜ 확인
  3. λ©”μ„œλ“œ λ‚΄μ„±: λ§€ν•‘ μ–΄λ…Έν…Œμ΄μ…˜μ— λŒ€ν•œ λͺ¨λ“  곡개 λ©”μ„œλ“œ 검사
  4. μ–΄λ…Έν…Œμ΄μ…˜ 처리: 메타 μ–΄λ…Έν…Œμ΄μ…˜κ³Ό 상속 처리

μ–΄λ…Έν…Œμ΄μ…˜ ν•΄κ²° μ•Œκ³ λ¦¬μ¦˜

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단계 μ ‘κ·Ό 방식을 μ‚¬μš©ν•©λ‹ˆλ‹€.

  1. νŒ¨ν„΄ λ§€μΉ­ 단계

    for (PathPattern registeredPattern : mappings.keySet()) {
    if (registeredPattern.matches(path)) {
    // λ§€μΉ­λ˜λŠ” ν•Έλ“€λŸ¬λ“€ μˆ˜μ§‘
    }
    }
  2. μ •λ ¬ 단계

    matchingHandlers.sort(Comparator.comparing(RequestMappingInfo::pattern));
  3. 선택 단계: 첫 번째(κ°€μž₯ λͺ…μ‹œμ μΈ) 맀치 λ°˜ν™˜

μ„±λŠ₯ νŠΉμ„±

  • μ‹œκ°„ λ³΅μž‘λ„: 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 κ°€λŠ₯

μ΅œμ ν™” μ „λž΅β€‹

  1. νŒ¨ν„΄ μˆœμ„œ: κ°€μž₯ λͺ…μ‹œμ μΈ νŒ¨ν„΄μ„ λ¨Όμ € 확인
  2. μ§€μ—° 컴파일: νŒ¨ν„΄λ‹Ή ν•œ 번만 μ •κ·œμ‹ 컴파일
  3. 효율적인 데이터 ꡬ쑰: HTTP λ©”μ„œλ“œμ— λŒ€ν•œ EnumMap, μˆœμ„œν™”λœ μ»¬λ ‰μ…˜μ— λŒ€ν•œ ArrayList
  4. 단락 평가: 첫 번째 λ§€μΉ˜μ—μ„œ μ‘°κΈ° λ°˜ν™˜

섀계 νŒ¨ν„΄κ³Ό 원칙​

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 ν™•μž₯

3. μ»€μŠ€ν…€ ν•Έλ“€λŸ¬ λ°œκ²¬β€‹

λŒ€μ•ˆμ μΈ HandlerMethodScanner μ „λž΅ κ΅¬ν˜„

4. μ»€μŠ€ν…€ λ§€ν•‘ μ €μž₯​

λŒ€μ•ˆμ μΈ μ €μž₯ λ©”μ»€λ‹ˆμ¦˜μœΌλ‘œ RequestMappingRegistry ꡐ체