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

πŸš€ Request Dispatching

κ°œμš”β€‹

이 λ¬Έμ„œλŠ” Sprout Framework의 μš”μ²­ λ””μŠ€νŒ¨μΉ­ μ‹œμŠ€ν…œμ— λŒ€ν•œ 심측적인 기술 뢄석을 μ œκ³΅ν•©λ‹ˆλ‹€. HTTP μš”μ²­μ΄ νŒŒμ‹±λœ ν›„λΆ€ν„° 컨트둀러 λ©”μ„œλ“œ μ‹€ν–‰, 응닡 μƒμ„±κΉŒμ§€μ˜ 전체 처리 νŒŒμ΄ν”„λΌμΈμ„ κ²€ν† ν•˜λ©°, Filter와 Interceptor의 μž‘λ™ λ©”μ»€λ‹ˆμ¦˜, Spring Frameworkμ™€μ˜ 차이점, 그리고 Sprout만의 κ³ μœ ν•œ 섀계 결정듀을 μƒμ„Ένžˆ λΆ„μ„ν•©λ‹ˆλ‹€.

λ””μŠ€νŒ¨μΉ­ νŒŒμ΄ν”„λΌμΈ μ•„ν‚€ν…μ²˜β€‹

전체 μš”μ²­ 처리 흐름​

HttpRequest β†’ RequestDispatcher β†’ 컨트둀러 λ©”μ„œλ“œ β†’ HttpResponse
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DispatchHook β†’ FilterChain β†’ InterceptorChain β†’ HandlerInvoker β”‚
β”‚ ↓ β”‚
β”‚ ResponseResolver ← ResponseAdvice ← λ°˜ν™˜κ°’ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

처리 단계별 μ‹€ν–‰ μˆœμ„œβ€‹

μš”μ²­ 단계:

  1. DispatchHook.beforeDispatch() - μ „μ²˜λ¦¬ ν›…
  2. FilterChain.doFilter() - ν•„ν„° 체인 μ‹€ν–‰
  3. InterceptorChain.applyPreHandle() - 인터셉터 μ „μ²˜λ¦¬
  4. HandlerMethodInvoker.invoke() - 컨트둀러 λ©”μ„œλ“œ μ‹€ν–‰
  5. InterceptorChain.applyPostHandle() - 인터셉터 ν›„μ²˜λ¦¬

응닡 단계:

  1. ResponseAdvice.beforeBodyWrite() - 응닡 μ–΄λ“œλ°”μ΄μŠ€
  2. ResponseResolver.resolve() - 응닡 ν•΄κ²°
  3. InterceptorChain.applyAfterCompletion() - 인터셉터 μ™„λ£Œ 처리
  4. DispatchHook.afterDispatch() - ν›„μ²˜λ¦¬ ν›…

핡심 ꡬ성 μš”μ†Œ 뢄석​

1. RequestDispatcher: 쀑앙 μ‘°μ •μžβ€‹

μ˜μ‘΄μ„± μ£Όμž… ꡬ쑰

@Component
public class RequestDispatcher {
private final HandlerMapping mapping;
private final HandlerMethodInvoker invoker;
private final List<ResponseResolver> responseResolvers;
private final List<ResponseAdvice> responseAdvices;
private final List<Filter> filters;
private final List<Interceptor> interceptors;
private final List<ExceptionResolver> exceptionResolvers;
private final List<DispatchHook> dispatchHooks;
}

섀계 원칙

  1. μ˜μ‘΄μ„± μ—­μ „: ꡬ체적인 κ΅¬ν˜„μ²΄κ°€ μ•„λ‹Œ μΈν„°νŽ˜μ΄μŠ€μ— 의쑴
  2. μ»΄ν¬μ§€μ…˜ νŒ¨ν„΄: μ—¬λŸ¬ μ „λž΅ 객체듀을 μ‘°ν•©ν•˜μ—¬ λ³΅μž‘ν•œ 둜직 ꡬ성
  3. 단일 μ±…μž„: λ””μŠ€νŒ¨μΉ­ μ‘°μ •μ—λ§Œ 집쀑, μ‹€μ œ μ²˜λ¦¬λŠ” μ „μš© μ»΄ν¬λ„ŒνŠΈμ— μœ„μž„
  4. ν™•μž₯μ„±: List 기반으둜 μ—¬λŸ¬ κ΅¬ν˜„μ²΄ 지원

2. 메인 λ””μŠ€νŒ¨μΉ˜ 둜직 뢄석​

dispatch() λ©”μ„œλ“œ: μ΅œμƒμœ„ μ§„μž…μ 

public void dispatch(HttpRequest<?> req, HttpResponse res) throws IOException {
try {
// 1. μ „μ²˜λ¦¬ ν›… μ‹€ν–‰
for (DispatchHook hook : dispatchHooks) {
hook.beforeDispatch(req, res);
}

// 2. ν•„ν„° 체인과 μ‹€μ œ λ””μŠ€νŒ¨μΉ˜ 둜직 μ—°κ²°
new FilterChain(filters, this::doDispatch).doFilter(req, res);
} finally {
// 3. ν›„μ²˜λ¦¬ ν›… μ‹€ν–‰ (λ°˜λ“œμ‹œ μ‹€ν–‰)
for (DispatchHook hook : dispatchHooks) {
hook.afterDispatch(req, res);
}
}
}

핡심 섀계 νŠΉμ§•

  1. try-finally νŒ¨ν„΄: μ˜ˆμ™Έ λ°œμƒ 여뢀와 관계없이 ν›„μ²˜λ¦¬ ν›… μ‹€ν–‰ 보μž₯
  2. ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€ ν™œμš©: this::doDispatch둜 λ©”μ„œλ“œ 레퍼런슀 전달
  3. κ³„μΈ΅ν™”λœ μ‹€ν–‰: ν›… β†’ ν•„ν„° β†’ μ‹€μ œ λ””μŠ€νŒ¨μΉ˜ μˆœμ„œ

doDispatch() λ©”μ„œλ“œ: 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직

private void doDispatch(HttpRequest<?> req, HttpResponse res) {
HandlerMethod hm = null;
Exception caughtException = null;
InterceptorChain interceptorChain = new InterceptorChain(interceptors);

try {
// 1. ν•Έλ“€λŸ¬ λ§€ν•‘
hm = mapping.findHandler(req.getPath(), req.getMethod());
if (hm == null) {
// 404 응닡 직접 생성
res.setResponseEntity(
new ResponseEntity<>("Not Found", null, ResponseCode.NOT_FOUND)
);
return;
}

// 2. 인터셉터 μ „μ²˜λ¦¬
if (!interceptorChain.applyPreHandle(req, res, hm)) {
return; // 인터셉터가 μš”μ²­μ„ μ€‘λ‹¨ν•œ 경우
}

// 3. 컨트둀러 λ©”μ„œλ“œ μ‹€ν–‰
Object returnValue = invoker.invoke(hm.requestMappingInfo(), req);

// 4. 인터셉터 ν›„μ²˜λ¦¬
interceptorChain.applyPostHandle(req, res, hm, returnValue);

// 5. 응닡 처리
setResponseResolvers(returnValue, req, res);

} catch (Exception e) {
caughtException = e;
// 6. μ˜ˆμ™Έ 처리 μœ„μž„
handleException(req, res, hm, e);
} finally {
// 7. 인터셉터 μ™„λ£Œ 처리
if (hm != null) {
interceptorChain.applyAfterCompletion(req, res, hm, caughtException);
}
}
}

3. μ˜ˆμ™Έ 처리 μ „λž΅β€‹

κ³„μΈ΅ν™”λœ μ˜ˆμ™Έ ν•΄κ²° λ©”μ»€λ‹ˆμ¦˜

// ExceptionResolver 체인을 ν†΅ν•œ μ˜ˆμ™Έ 처리
Object handledReturnValue = null;
for (ExceptionResolver resolver : exceptionResolvers) {
handledReturnValue = resolver.resolveException(req, res, hm, caughtException);
if (handledReturnValue != null) {
// 처리 성곡 μ‹œ 응닡 μ„€μ •
if (handledReturnValue instanceof ResponseEntity) {
res.setResponseEntity((ResponseEntity<?>) handledReturnValue);
} else {
setResponseResolvers(handledReturnValue, req, res);
}
return;
}
}

μ˜ˆμ™Έ 처리 섀계 νŠΉμ§•

  1. 체인 였브 λ¦¬μŠ€νŽ€μ„œλΉŒλ¦¬ν‹°: μ—¬λŸ¬ 리쑸버가 순차적으둜 μ˜ˆμ™Έ 처리 μ‹œλ„
  2. μ‘°κΈ° μ’…λ£Œ: 첫 번째 처리 성곡 μ‹œ λ‚˜λ¨Έμ§€ 리쑸버 μ‹€ν–‰ μ•ˆν•¨
  3. μœ μ—°ν•œ λ°˜ν™˜κ°’: ResponseEntity 직접 λ°˜ν™˜ λ˜λŠ” 일반 객체λ₯Ό ν†΅ν•œ μΆ”κ°€ 처리
  4. νƒ€μž… μ•ˆμ „μ„±: instanceof 검사λ₯Ό ν†΅ν•œ λŸ°νƒ€μž„ νƒ€μž… 체크

4. 응닡 처리 νŒŒμ΄ν”„λΌμΈβ€‹

ResponseResolver와 ResponseAdvice ν˜‘λ ₯

private void setResponseResolvers(Object returnValue, HttpRequest<?> req, HttpResponse res) {
if (res.isCommitted()) return; // 쀑볡 처리 λ°©μ§€

// 1. ResponseAdvice 체인 적용
Object processed = applyResponseAdvices(returnValue, req);

// 2. μ μ ˆν•œ ResponseResolver 탐색 및 적용
for (ResponseResolver resolver : responseResolvers) {
if (resolver.supports(processed)) {
ResponseEntity<?> responseEntity = resolver.resolve(processed, req);
res.setResponseEntity(responseEntity);
return;
}
}

throw new IllegalStateException("No suitable ResponseResolver found");
}

ResponseAdvice 체인 처리

private Object applyResponseAdvices(Object returnValue, HttpRequest<?> req) {
Object current = returnValue;
for (ResponseAdvice advice : responseAdvices) {
if (advice.supports(current, req)) {
current = advice.beforeBodyWrite(current, req);
}
}
return current;
}

Filter μ‹œμŠ€ν…œ 뢄석​

FilterChain κ΅¬ν˜„β€‹

체인 였브 λ¦¬μŠ€νŽ€μ„œλΉŒλ¦¬ν‹° νŒ¨ν„΄

public class FilterChain {
private final List<Filter> filters;
private final Dispatcher dispatcher;
private int currentFilterIndex = 0;

public void doFilter(HttpRequest<?> request, HttpResponse response) throws IOException {
if (currentFilterIndex < filters.size()) {
// λ‹€μŒ ν•„ν„° μ‹€ν–‰
filters.get(currentFilterIndex++).doFilter(request, response, this);
return;
}
// λͺ¨λ“  ν•„ν„° μ™„λ£Œ ν›„ μ‹€μ œ λ””μŠ€νŒ¨μ²˜ 호좜
dispatcher.dispatch(request, response);
}
}

FilterChain νŠΉμ§•

  1. μƒνƒœ 기반 μ§„ν–‰: currentFilterIndex둜 ν˜„μž¬ μ‹€ν–‰ μœ„μΉ˜ 좔적
  2. μž¬κ·€μ  호좜: 각 ν•„ν„°κ°€ λ‹€μŒ 체인을 직접 호좜
  3. ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€: Dispatcherλ₯Ό ν†΅ν•œ μ΅œμ’… 처리 μœ„μž„
  4. μ„ ν˜• μ‹€ν–‰: 필터듀이 순차적으둜 싀행됨

Filter μΈν„°νŽ˜μ΄μŠ€β€‹

public interface Filter extends InfrastructureBean {
void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws IOException;
}

Filter vs Servlet Filter 비ꡐ

νŠΉμ„±Sprout FilterServlet Filter
λ§€κ°œλ³€μˆ˜HttpRequest, HttpResponse, FilterChainServletRequest, ServletResponse, FilterChain
체크 μ˜ˆμ™ΈIOExceptionIOException, ServletException
생λͺ…μ£ΌκΈ°Spring 빈 생λͺ…μ£ΌκΈ°μ„œλΈ”λ¦Ώ μ»¨ν…Œμ΄λ„ˆ 관리
μ„€μ •@Component + DIweb.xml λ˜λŠ” @WebFilter

μ‹€μ œ Filter κ΅¬ν˜„ μ˜ˆμ‹œ: CorsFilter​

μ„€μ • 기반 CORS 처리

@Component
public class CorsFilter implements Filter {
private final AppConfig appConfig;

@Override
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws IOException {
String origin = Optional.ofNullable(request.getHeaders().get("Origin"))
.map(Object::toString)
.orElse(null);

if (origin == null || origin.isEmpty()) {
chain.doFilter(request, response); // Origin μ—†μœΌλ©΄ CORS 처리 κ±΄λ„ˆλ›°κΈ°
return;
}

// CORS 헀더 μ„€μ •
applyCorsHeaders(response, origin);

// OPTIONS ν”„λ¦¬ν”ŒλΌμ΄νŠΈ μš”μ²­ 처리
if (request.getMethod().equals(HttpMethod.OPTIONS)) {
handlePreflightRequest(request, response);
return; // 체인 μ§„ν–‰ 쀑단
}

chain.doFilter(request, response); // λ‹€μŒ 체인 계속
}
}

Interceptor μ‹œμŠ€ν…œ 뢄석​

InterceptorChain κ΅¬ν˜„β€‹

순차적 μ‹€ν–‰κ³Ό μ—­μˆœ 정리

public class InterceptorChain {
private final List<Interceptor> interceptors;

public boolean applyPreHandle(HttpRequest request, HttpResponse response, Object handler) {
for (Interceptor interceptor : interceptors) {
if (!interceptor.preHandle(request, response, handler)) {
return false; // ν•˜λ‚˜λΌλ„ false λ°˜ν™˜ μ‹œ 쀑단
}
}
return true;
}

public void applyPostHandle(HttpRequest request, HttpResponse response, Object handler, Object result) {
// μ—­μˆœμœΌλ‘œ μ‹€ν–‰ (LIFO)
for (int i = interceptors.size() - 1; i >= 0; i--) {
interceptors.get(i).postHandle(request, response, handler, result);
}
}

public void applyAfterCompletion(HttpRequest request, HttpResponse response, Object handler, Exception ex) {
// μ—­μˆœμœΌλ‘œ μ‹€ν–‰ (LIFO)
for (int i = interceptors.size() - 1; i >= 0; i--) {
interceptors.get(i).afterCompletion(request, response, handler, ex);
}
}
}

Interceptor μ‹€ν–‰ νŒ¨ν„΄

  1. preHandle: 순방ν–₯ μ‹€ν–‰, ν•˜λ‚˜λΌλ„ false λ°˜ν™˜ μ‹œ 전체 쀑단
  2. postHandle: μ—­λ°©ν–₯ μ‹€ν–‰ (LIFO), 컨트둀러 μ‹€ν–‰ 성곡 ν›„μ—λ§Œ μ‹€ν–‰
  3. afterCompletion: μ—­λ°©ν–₯ μ‹€ν–‰ (LIFO), μ˜ˆμ™Έ λ°œμƒ μ—¬λΆ€ λ¬΄κ΄€ν•˜κ²Œ 항상 μ‹€ν–‰

Interceptor vs Filter 비ꡐ​

νŠΉμ„±InterceptorFilter
μ‹€ν–‰ μ‹œμ ν•Έλ“€λŸ¬ λ§€ν•‘ ν›„ν•Έλ“€λŸ¬ λ§€ν•‘ μ „
μ ‘κ·Ό μ •λ³΄ν•Έλ“€λŸ¬ 정보 μ ‘κ·Ό κ°€λŠ₯ν•Έλ“€λŸ¬ 정보 μ ‘κ·Ό λΆˆκ°€
μ‹€ν–‰ 단계3단계 (pre/post/after)1단계 (doFilter)
처리 λ²”μœ„μ»¨νŠΈλ‘€λŸ¬ λ©”μ„œλ“œ 쀑심HTTP μš”μ²­ 전체
μ‹€ν–‰ μˆœμ„œν›„μ²˜λ¦¬λŠ” μ—­μˆœ (LIFO)항상 순차적
쀑단 λ©”μ»€λ‹ˆμ¦˜boolean λ°˜ν™˜κ°’μ²΄μΈ 호좜 μ•ˆν•¨

HandlerMethodInvoker 뢄석​

λ©”μ„œλ“œ μ‹€ν–‰ μ „λž΅β€‹

@Component
public class HandlerMethodInvoker {
private final CompositeArgumentResolver resolvers;

public Object invoke(RequestMappingInfo requestMappingInfo, HttpRequest<?> request) throws Exception {
PathPattern pattern = requestMappingInfo.pattern();

// 1. URL νŒ¨ν„΄μ—μ„œ 경둜 λ³€μˆ˜ μΆ”μΆœ
Map<String, String> pathVariables = pattern.extractPathVariables(request.getPath());

// 2. 컨트둀러 λ©”μ„œλ“œ νŒŒλΌλ―Έν„° ν•΄κ²°
Object[] args = resolvers.resolveArguments(requestMappingInfo.handlerMethod(), request, pathVariables);

// 3. λ¦¬ν”Œλ ‰μ…˜μ„ ν†΅ν•œ λ©”μ„œλ“œ μ‹€ν–‰
return requestMappingInfo.handlerMethod().invoke(requestMappingInfo.controller(), args);
}
}

핡심 처리 κ³Όμ •

  1. 경둜 λ³€μˆ˜ μΆ”μΆœ: PathPattern을 톡해 URLμ—μ„œ {id} 같은 λ³€μˆ˜ μΆ”μΆœ
  2. 인수 ν•΄κ²°: CompositeArgumentResolver둜 λ©”μ„œλ“œ νŒŒλΌλ―Έν„° κ°’ κ²°μ •
  3. λ©”μ„œλ“œ 호좜: Java λ¦¬ν”Œλ ‰μ…˜ API둜 μ‹€μ œ 컨트둀러 λ©”μ„œλ“œ μ‹€ν–‰

ControllerAdvice 기반 μ˜ˆμ™Έ 처리 μ‹œμŠ€ν…œβ€‹

Sprout은 Spring의 @ControllerAdvice와 @ExceptionHandler μ–΄λ…Έν…Œμ΄μ…˜μ„ λͺ¨λ°©ν•œ μ˜ˆμ™Έ 처리 μ‹œμŠ€ν…œμ„ μ œκ³΅ν•©λ‹ˆλ‹€.

ControllerAdviceRegistry: μ˜ˆμ™Έ ν•Έλ“€λŸ¬ λ“±λ‘μ†Œβ€‹

μ˜ˆμ™Έ ν•Έλ“€λŸ¬ μŠ€μΊ”κ³Ό 등둝

@Component
public class ControllerAdviceRegistry {
private final List<ExceptionHandlerObject> allExceptionHandlers = new ArrayList<>();
private final Map<Class<? extends Throwable>, Optional<ExceptionHandlerObject>> cachedHandlers = new ConcurrentHashMap<>();

public void scanControllerAdvices(BeanFactory context) {
Collection<Object> allBeans = context.getAllBeans();
for (Object bean : allBeans) {
if (bean.getClass().isAnnotationPresent(ControllerAdvice.class)) {
// @ControllerAdvice 클래슀 발견
for (Method method : bean.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(ExceptionHandler.class)) {
method.setAccessible(true);
allExceptionHandlers.add(new ExceptionHandlerObject(method, bean));
}
}
}
}
}
}

핡심 섀계 νŠΉμ§•

  1. λ¦¬ν”Œλ ‰μ…˜ 기반 μŠ€μΊ”: λŸ°νƒ€μž„μ— @ControllerAdvice λΉˆλ“€μ„ 탐색
  2. λ©”μ„œλ“œ μ ‘κ·Όμ„±: setAccessible(true)둜 private ν•Έλ“€λŸ¬ λ©”μ„œλ“œλ„ 호좜 κ°€λŠ₯
  3. 캐싱 λ©”μ»€λ‹ˆμ¦˜: ConcurrentHashMap으둜 μ˜ˆμ™Έ νƒ€μž…λ³„ ν•Έλ“€λŸ¬ μΊμ‹œ

μ˜ˆμ™Έ ν•Έλ“€λŸ¬ λ§€μΉ­ μ•Œκ³ λ¦¬μ¦˜β€‹

졜적 ν•Έλ“€λŸ¬ 선택 μ „λž΅

private Optional<ExceptionHandlerObject> lookupBestMatchHandler(Class<? extends Throwable> exceptionClass) {
ExceptionHandlerObject bestMatch = null;
int bestMatchDistance = Integer.MAX_VALUE;

for (ExceptionHandlerObject handler : allExceptionHandlers) {
Method handlerMethod = handler.getMethod();
for (Class<? extends Throwable> handledExceptionType : handlerMethod.getAnnotation(ExceptionHandler.class).value()) {
if (handledExceptionType.isAssignableFrom(exceptionClass)) {
// μ˜ˆμ™Έ 계측 κ΅¬μ‘°μ—μ„œ 거리 계산
int distance = getExceptionDistance(handledExceptionType, exceptionClass);

if (distance < bestMatchDistance) {
bestMatch = handler;
bestMatchDistance = distance;
}
}
}
}
return Optional.ofNullable(bestMatch);
}

private int getExceptionDistance(Class<?> fromClass, Class<?> toClass) {
if (fromClass.equals(toClass)) return 0; // μ •ν™•ν•œ νƒ€μž… 맀치

int distance = 0;
Class<?> current = toClass;
while (current != null && !current.equals(fromClass)) {
current = current.getSuperclass();
distance++;
}
return (current != null) ? distance : Integer.MAX_VALUE;
}

λ§€μΉ­ μ•Œκ³ λ¦¬μ¦˜ νŠΉμ§•

  1. 거리 기반 λ§€μΉ­: μ˜ˆμ™Έ νƒ€μž… κ³„μΈ΅μ—μ„œ κ°€μž₯ κ°€κΉŒμš΄(ꡬ체적인) ν•Έλ“€λŸ¬ 선택
  2. 닀쀑 μ˜ˆμ™Έ 지원: @ExceptionHandler({Exception1.class, Exception2.class})
  3. 상속 관계 κ³ λ €: λΆ€λͺ¨ μ˜ˆμ™Έ ν•Έλ“€λŸ¬κ°€ ν•˜μœ„ μ˜ˆμ™Έλ„ 처리 κ°€λŠ₯
  4. μ •ν™•ν•œ 맀치 μš°μ„ : λ™μΌν•œ νƒ€μž…μΌ λ•Œ 거리 0으둜 μ΅œμš°μ„  선택

ControllerAdviceExceptionResolver κ΅¬ν˜„β€‹

μ˜ˆμ™Έ ν•΄κ²° 및 λ©”μ„œλ“œ 호좜

@Component
@Order(0) // μ΅œμš°μ„  μ‹€ν–‰
public class ControllerAdviceExceptionResolver implements ExceptionResolver {

@Override
public Object resolveException(HttpRequest<?> request, HttpResponse response,
Object handlerMethod, Exception exception) {
Optional<ExceptionHandlerObject> handlerOptional =
controllerAdviceRegistry.getExceptionHandler(exception.getClass());

if (handlerOptional.isPresent()) {
ExceptionHandlerObject exceptionHandler = handlerOptional.get();
Method handlerMethodRef = exceptionHandler.getMethod();
Object handlerInstance = exceptionHandler.getBean();

// λ‹€μ–‘ν•œ λ©”μ„œλ“œ μ‹œκ·Έλ‹ˆμ²˜ 지원
Object handlerReturnValue = invokeExceptionHandler(handlerMethodRef, handlerInstance, exception, request);

// ResponseResolverλ₯Ό ν†΅ν•œ 응닡 처리
return processHandlerReturnValue(handlerReturnValue, request, response);
}
return null; // μ²˜λ¦¬ν•˜μ§€ λͺ»ν•¨
}
}

μ§€μ›ν•˜λŠ” λ©”μ„œλ“œ μ‹œκ·Έλ‹ˆμ²˜ νŒ¨ν„΄

// 1. λ§€κ°œλ³€μˆ˜ μ—†μŒ
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument() { }

// 2. μ˜ˆμ™Έλ§Œ λ°›κΈ°
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) { }

// 3. μ˜ˆμ™Έ + μš”μ²­
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e, HttpRequest request) { }

// 4. μš”μ²­ + μ˜ˆμ™Έ (μˆœμ„œ λ°”λ€œ)
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(HttpRequest request, IllegalArgumentException e) { }

λ©”μ„œλ“œ 호좜 μ „λž΅

private Object invokeExceptionHandler(Method handlerMethodRef, Object handlerInstance,
Exception exception, HttpRequest request) {
int paramCount = handlerMethodRef.getParameterCount();
Class<?>[] paramTypes = handlerMethodRef.getParameterTypes();

if (paramCount == 0) {
return handlerMethodRef.invoke(handlerInstance);
} else if (paramCount == 1 && paramTypes[0].isAssignableFrom(exception.getClass())) {
return handlerMethodRef.invoke(handlerInstance, exception);
} else if (paramCount == 2) {
// λ§€κ°œλ³€μˆ˜ μˆœμ„œμ— 관계없이 νƒ€μž…μœΌλ‘œ νŒλ‹¨
if (paramTypes[0].isAssignableFrom(exception.getClass()) &&
paramTypes[1].isAssignableFrom(HttpRequest.class)) {
return handlerMethodRef.invoke(handlerInstance, exception, request);
} else if (paramTypes[0].isAssignableFrom(HttpRequest.class) &&
paramTypes[1].isAssignableFrom(exception.getClass())) {
return handlerMethodRef.invoke(handlerInstance, request, exception);
}
}
throw new UnsupportedOperationException("Unsupported method signature");
}

ExceptionHandlerObject: ν•Έλ“€λŸ¬ 메타데이터​

λ‹¨μˆœν•œ 메타데이터 홀더

public class ExceptionHandlerObject {
private final Method method;
private final Object bean;

public ExceptionHandlerObject(Method method, Object bean) {
this.method = method;
this.bean = bean;
method.setAccessible(true); // private λ©”μ„œλ“œλ„ 호좜 κ°€λŠ₯
}
}

μ–΄λ…Έν…Œμ΄μ…˜ μ •μ˜β€‹

ControllerAdvice μ–΄λ…Έν…Œμ΄μ…˜

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ControllerAdvice {
// 클래슀 λ ˆλ²¨μ—λ§Œ 적용
}

ExceptionHandler μ–΄λ…Έν…Œμ΄μ…˜

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ExceptionHandler {
Class<? extends Throwable>[] value(); // μ²˜λ¦¬ν•  μ˜ˆμ™Έ νƒ€μž… λ°°μ—΄
}

전체 μ˜ˆμ™Έ 처리 흐름​

RequestDispatcherμ—μ„œμ˜ 톡합

// doDispatch() λ©”μ„œλ“œ λ‚΄ μ˜ˆμ™Έ 처리 λΆ€λΆ„
catch (Exception e) {
caughtException = e;

Object handledReturnValue = null;
for (ExceptionResolver resolver : exceptionResolvers) {
// ControllerAdviceExceptionResolverκ°€ @Order(0)으둜 μ΅œμš°μ„  μ‹€ν–‰
handledReturnValue = resolver.resolveException(req, res, hm, caughtException);
if (handledReturnValue != null) {
// @ExceptionHandlerμ—μ„œ 처리됨
setResponseResolvers(handledReturnValue, req, res);
return;
}
}
// μ²˜λ¦¬λ˜μ§€ μ•Šμ€ μ˜ˆμ™ΈλŠ” κΈ°λ³Έ 처리
}

μ‹€ν–‰ μˆœμ„œ

  1. μ»¨νŠΈλ‘€λŸ¬μ—μ„œ μ˜ˆμ™Έ λ°œμƒ
  2. ControllerAdviceExceptionResolver μ‹€ν–‰ (μ΅œμš°μ„ )
  3. ControllerAdviceRegistryμ—μ„œ ν•Έλ“€λŸ¬ 검색
  4. κ°€μž₯ ꡬ체적인 ν•Έλ“€λŸ¬ 선택
  5. ν•Έλ“€λŸ¬ λ©”μ„œλ“œ 호좜
  6. ResponseResolver둜 응닡 λ³€ν™˜
  7. 처리 μ‹€νŒ¨ μ‹œ λ‹€μŒ ExceptionResolver둜 이동

Spring과의 차이점 비ꡐ​

ControllerAdvice κΈ°λŠ₯ 비ꡐ

νŠΉμ„±Spring @ControllerAdviceSprout @ControllerAdvice
적용 λ²”μœ„μ „μ—­ λ˜λŠ” νŒ¨ν‚€μ§€/클래슀 μ§€μ •μ „μ—­λ§Œ 지원
μ–΄λ…Έν…Œμ΄μ…˜ μœ„μΉ˜ν΄λž˜μŠ€ 레벨클래슀 레벨
μŠ€μΊ” λ°©μ‹μ»΄ν¬λ„ŒνŠΈ μŠ€μΊ” μžλ™BeanFactoryμ—μ„œ μˆ˜λ™ μŠ€μΊ”
λ©”μ„œλ“œ μ‹œκ·Έλ‹ˆμ²˜λ§€μš° μœ μ—° (Model, WebRequest λ“±)μ œν•œμ  (Exception, HttpRequest)
응닡 처리ViewResolver, HttpMessageConverterResponseResolver
캐싱기본 μ œκ³΅μˆ˜λ™ κ΅¬ν˜„ (ConcurrentHashMap)

ExceptionHandler κΈ°λŠ₯ 비ꡐ

νŠΉμ„±Spring @ExceptionHandlerSprout @ExceptionHandler
λ§€κ°œλ³€μˆ˜ 지원20+ μ’…λ₯˜ (Model, HttpSession λ“±)4κ°€μ§€ νŒ¨ν„΄λ§Œ 지원
λ°˜ν™˜κ°’ 지원ResponseEntity, Model, View λ“±ResponseResolver 의쑴
μ˜ˆμ™Έ νƒ€μž… λ§€μΉ­λŸ°νƒ€μž„ ν•΄κ²°μ»΄νŒŒμΌνƒ€μž„ + λŸ°νƒ€μž„
μˆœμ„œ μ œμ–΄@Order 지원거리 기반 μ•Œκ³ λ¦¬μ¦˜
비동기 지원DeferredResult, Callable미지원

μ„±λŠ₯ 및 λ©”λͺ¨λ¦¬ νŠΉμ„±β€‹

캐싱 μ „λž΅

// μ˜ˆμ™Έ νƒ€μž…λ³„ ν•Έλ“€λŸ¬ μΊμ‹±μœΌλ‘œ μ„±λŠ₯ μ΅œμ ν™”
private final Map<Class<? extends Throwable>, Optional<ExceptionHandlerObject>> cachedHandlers = new ConcurrentHashMap<>();

public Optional<ExceptionHandlerObject> getExceptionHandler(Class<? extends Throwable> exceptionClass) {
return cachedHandlers.computeIfAbsent(exceptionClass, this::lookupBestMatchHandler);
}

μ‹œκ°„ λ³΅μž‘λ„

  • 첫 번째 쑰회: O(n Γ— m) (n = ν•Έλ“€λŸ¬ 수, m = 처리 κ°€λŠ₯ν•œ μ˜ˆμ™Έ νƒ€μž… 수)
  • μΊμ‹œ 적쀑: O(1)
  • μ˜ˆμ™Έ 거리 계산: O(d) (d = 상속 계측 깊이)

λ©”λͺ¨λ¦¬ μ‚¬μš©

  • ν•Έλ“€λŸ¬ μ €μž₯: 각 @ExceptionHandler λ©”μ„œλ“œλ§ˆλ‹€ ExceptionHandlerObject μΈμŠ€ν„΄μŠ€
  • μΊμ‹œ μ €μž₯: 쑰회된 μ˜ˆμ™Έ νƒ€μž…λ§ˆλ‹€ Optional<ExceptionHandlerObject> μ €μž₯
  • λ©”μ„œλ“œ μ ‘κ·Όμ„±: setAccessible(true) 호좜둜 λ³΄μ•ˆ 검사 λΉ„μš© μ ˆμ•½

Spring Frameworkμ™€μ˜ 비ꡐ 뢄석​

μ•„ν‚€ν…μ²˜ 차이점​

Spring DispatcherServlet vs Sprout RequestDispatcher

μΈ‘λ©΄Spring DispatcherServletSprout RequestDispatcher
기반 기술Servlet API 기반순수 Java 기반
생λͺ…μ£ΌκΈ°μ„œλΈ”λ¦Ώ μ»¨ν…Œμ΄λ„ˆ 관리Spring 빈 생λͺ…μ£ΌκΈ°
μ΄ˆκΈ°ν™”init(), destroy()μƒμ„±μž μ£Όμž…
μ˜ˆμ™Έ 처리HandlerExceptionResolverExceptionResolver
λ·° ν•΄κ²°ViewResolverResponseResolver

ν•„ν„° μ‹œμŠ€ν…œ 차이점​

μ‹€ν–‰ μœ„μΉ˜μ™€ λ²”μœ„

// Spring: μ„œλΈ”λ¦Ώ μ»¨ν…Œμ΄λ„ˆ 레벨
public class SpringFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// μ„œλΈ”λ¦Ώ μ»¨ν…Œμ΄λ„ˆμ—μ„œ μ‹€ν–‰
// DispatcherServlet 이전에 μ‹€ν–‰
}
}

// Sprout: μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 레벨
public class SproutFilter implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
// RequestDispatcherμ—μ„œ 직접 μ‹€ν–‰
// μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ»¨ν…μŠ€νŠΈ λ‚΄μ—μ„œ μ‹€ν–‰
}
}

라이프사이클 관리

  • Spring: μ„œλΈ”λ¦Ώ μ»¨ν…Œμ΄λ„ˆκ°€ ν•„ν„° 생λͺ…μ£ΌκΈ° 관리, @WebFilter λ˜λŠ” web.xml μ„€μ •
  • Sprout: Spring IoC μ»¨ν…Œμ΄λ„ˆκ°€ 관리, @Component둜 μžλ™ 등둝

Interceptor κ΅¬ν˜„ 차이점​

HandlerInterceptor vs Interceptor

// Spring HandlerInterceptor
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { return true; }
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}

// Sprout Interceptor
public interface Interceptor extends InfrastructureBean {
boolean preHandle(HttpRequest request, HttpResponse response, Object handler);
void postHandle(HttpRequest request, HttpResponse response, Object handler, Object result);
void afterCompletion(HttpRequest request, HttpResponse response, Object handler, Exception ex);
}

μ£Όμš” 차이점

  1. λ§€κ°œλ³€μˆ˜ νƒ€μž…: Servlet API vs 자체 HTTP 좔상화
  2. ModelAndView: Spring은 λ·° λͺ¨λΈ 전달, Sprout은 일반 λ°˜ν™˜κ°’ 전달
  3. κΈ°λ³Έ κ΅¬ν˜„: Spring은 default λ©”μ„œλ“œ, Sprout은 λͺ¨λ“  λ©”μ„œλ“œ κ΅¬ν˜„ ν•„μš”

μ„±λŠ₯ 뢄석​

μ‹œκ°„ λ³΅μž‘λ„β€‹

μš”μ²­ 처리 과정별 λ³΅μž‘λ„

  • DispatchHook μ‹€ν–‰: O(h) (h = ν›… 개수)
  • Filter 체인: O(f) (f = ν•„ν„° 개수)
  • ν•Έλ“€λŸ¬ λ§€ν•‘: O(log n) (n = λ“±λ‘λœ ν•Έλ“€λŸ¬ 수, 트리 탐색)
  • Interceptor 체인: O(i) (i = 인터셉터 개수)
  • 인수 ν•΄κ²°: O(p) (p = λ©”μ„œλ“œ νŒŒλΌλ―Έν„° 수)
  • λ©”μ„œλ“œ μ‹€ν–‰: O(1) (λ¦¬ν”Œλ ‰μ…˜ 호좜)
  • 응닡 처리: O(r + a) (r = 리쑸버 수, a = μ–΄λ“œλ°”μ΄μŠ€ 수)

전체 μ‹œκ°„ λ³΅μž‘λ„: O(h + f + log n + i + p + r + a)

λ©”λͺ¨λ¦¬ μ‚¬μš© νŒ¨ν„΄β€‹

객체 생성과 GC μ••λ°•

// λ§€ μš”μ²­λ§ˆλ‹€ μƒμ„±λ˜λŠ” 객체듀
InterceptorChain interceptorChain = new InterceptorChain(interceptors); // 체인 객체
Map<String, String> pathVariables = pattern.extractPathVariables(request.getPath()); // 경둜 λ³€μˆ˜ λ§΅
Object[] args = resolvers.resolveArguments(handlerMethod, request, pathVariables); // 인수 λ°°μ—΄

μ΅œμ ν™” μ „λž΅

  1. 체인 객체 풀링: InterceptorChain μž¬μ‚¬μš©
  2. 인수 λ°°μ—΄ 캐싱: λ™μΌν•œ λ©”μ„œλ“œμ— λŒ€ν•œ λ°°μ—΄ μž¬μ‚¬μš©
  3. 경둜 λ³€μˆ˜ μ΅œμ ν™”: 빈 맡일 λ•Œ 싱글톀 μ‚¬μš©

병렬 처리 고렀사항​

μŠ€λ ˆλ“œ μ•ˆμ „μ„±

  • RequestDispatcher: μƒνƒœ μ—†λŠ” μ»΄ν¬λ„ŒνŠΈ, μŠ€λ ˆλ“œ μ•ˆμ „
  • FilterChain: currentFilterIndex μƒνƒœ 보유, μš”μ²­λ³„ μΈμŠ€ν„΄μŠ€ ν•„μš”
  • InterceptorChain: μƒνƒœ μ—†μŒ, μŠ€λ ˆλ“œ μ•ˆμ „

λ™μ‹œμ„± μ΅œμ ν™”

// ν˜„μž¬: λ§€ μš”μ²­λ§ˆλ‹€ μƒˆ 체인 생성
new FilterChain(filters, this::doDispatch).doFilter(req, res);

// μ΅œμ ν™”: ThreadLocal 기반 체인 μž¬μ‚¬μš© κ°€λŠ₯
private final ThreadLocal<FilterChain> chainCache = ThreadLocal.withInitial(() -> new FilterChain(filters, this::doDispatch));

ν™•μž₯μ„±κ³Ό μœ μ§€λ³΄μˆ˜μ„±β€‹

μƒˆλ‘œμš΄ 처리 단계 좔가​

ν™•μž₯ 포인트

  1. DispatchHook: μš”μ²­ μ „/ν›„ 처리 둜직 μΆ”κ°€
  2. Filter: HTTP 레벨 μ „μ²˜λ¦¬ (인증, λ‘œκΉ…, CORS λ“±)
  3. Interceptor: 컨트둀러 레벨 μ „μ²˜λ¦¬ (κΆŒν•œ 검사, λ‘œκΉ… λ“±)
  4. ExceptionResolver: μ˜ˆμ™Έ 처리 μ „λž΅ μΆ”κ°€
  5. ResponseResolver: μƒˆλ‘œμš΄ 응닡 ν˜•μ‹ 지원

μ„€μ •κ³Ό μžλ™ ꡬ성​

빈 등둝과 μˆœμ„œ μ œμ–΄

@Configuration
public class WebConfig {
@Bean
@Order(1) // μ‹€ν–‰ μˆœμ„œ μ œμ–΄
public Filter corsFilter() {
return new CorsFilter();
}

@Bean
@Order(2)
public Filter authenticationFilter() {
return new AuthenticationFilter();
}
}

ν…ŒμŠ€νŠΈ κ°€λŠ₯성​

λ‹¨μœ„ ν…ŒμŠ€νŠΈ μ „λž΅

@Test
void testRequestDispatching() {
// Given
HttpRequest request = mockRequest("/api/users/123");
HttpResponse response = mockResponse();
HandlerMapping mapping = mockMapping();

RequestDispatcher dispatcher = new RequestDispatcher(
mapping, invoker, resolvers, advices,
List.of(), List.of(), List.of(), List.of()
);

// When
dispatcher.dispatch(request, response);

// Then
assertEquals(ResponseCode.OK, response.getStatusCode());
}

λ³΄μ•ˆ 고렀사항​

ν˜„μž¬ λ³΄μ•ˆ λ©”μ»€λ‹ˆμ¦˜β€‹

1. ν•Έλ“€λŸ¬ λ§€ν•‘ λ³΄μ•ˆ

  • ν•Έλ“€λŸ¬κ°€ μ—†λŠ” 경우 404 응닡 λ°˜ν™˜
  • 직접적인 μ˜ˆμ™Έ λ…ΈμΆœ λ°©μ§€

2. μ˜ˆμ™Έ 처리 λ³΄μ•ˆ

  • μŠ€νƒ 트레이슀 μ½˜μ†” 좜λ ₯ (운영 ν™˜κ²½ λΆ€μ μ ˆ)
  • ExceptionResolverλ₯Ό ν†΅ν•œ μ•ˆμ „ν•œ μ˜ˆμ™Έ λ³€ν™˜

λ³΄μ•ˆ κ°œμ„  사항​

1. 정보 λ…ΈμΆœ λ°©μ§€

// ν˜„μž¬: 디버깅 정보 λ…ΈμΆœ
System.err.println("Exception caught in doDispatch: " + e.getMessage());
e.printStackTrace();

// κ°œμ„ : λ‘œκΉ… 레벨 기반 μ œμ–΄
if (logger.isDebugEnabled()) {
logger.debug("Exception in doDispatch", e);
} else {
logger.error("Request processing failed: {}", request.getPath());
}

2. μž…λ ₯ 검증 κ°•ν™”

// ꢌμž₯: μš”μ²­ 크기 μ œν•œ
public void dispatch(HttpRequest<?> req, HttpResponse res) {
validateRequest(req); // 크기, 헀더 개수 λ“± 검증
// ... 기쑴 둜직
}

3. CSRF/XSS λ°©μ§€

@Component
public class SecurityFilter implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
applyCsrfProtection(request, response);
applyXssProtection(response);
chain.doFilter(request, response);
}
}

취약점 및 κ°œμ„ μ μ— λŒ€ν•œ μƒˆλ‘œμš΄ 의견 및 μ œμ•ˆ, PR ν™˜μ˜ν•©λ‹ˆλ‹€!