π Request Dispatching
κ°μβ
μ΄ λ¬Έμλ Sprout Frameworkμ μμ² λμ€ν¨μΉ μμ€ν μ λν μ¬μΈ΅μ μΈ κΈ°μ λΆμμ μ 곡ν©λλ€. HTTP μμ²μ΄ νμ±λ νλΆν° 컨νΈλ‘€λ¬ λ©μλ μ€ν, μλ΅ μμ±κΉμ§μ μ 체 μ²λ¦¬ νμ΄νλΌμΈμ κ²ν νλ©°, Filterμ Interceptorμ μλ λ©μ»€λμ¦, Spring Frameworkμμ μ°¨μ΄μ , κ·Έλ¦¬κ³ Sproutλ§μ κ³ μ ν μ€κ³ κ²°μ λ€μ μμΈν λΆμν©λλ€.
λμ€ν¨μΉ νμ΄νλΌμΈ μν€ν μ²β
μ 체 μμ² μ²λ¦¬ νλ¦β
HttpRequest β RequestDispatcher β 컨νΈλ‘€λ¬ λ©μλ β HttpResponse
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DispatchHook β FilterChain β InterceptorChain β HandlerInvoker β
β β β
β ResponseResolver β ResponseAdvice β λ°νκ° β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
μ²λ¦¬ λ¨κ³λ³ μ€ν μμβ
μμ² λ¨κ³:
DispatchHook.beforeDispatch()
- μ μ²λ¦¬ νFilterChain.doFilter()
- νν° μ²΄μΈ μ€νInterceptorChain.applyPreHandle()
- μΈν°μ ν° μ μ²λ¦¬HandlerMethodInvoker.invoke()
- 컨νΈλ‘€λ¬ λ©μλ μ€νInterceptorChain.applyPostHandle()
- μΈν°μ ν° νμ²λ¦¬
μλ΅ λ¨κ³:
ResponseAdvice.beforeBodyWrite()
- μλ΅ μ΄λλ°μ΄μ€ResponseResolver.resolve()
- μλ΅ ν΄κ²°InterceptorChain.applyAfterCompletion()
- μΈν°μ ν° μλ£ μ²λ¦¬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;
}
μ€κ³ μμΉ
- μμ‘΄μ± μμ : ꡬ체μ μΈ κ΅¬νμ²΄κ° μλ μΈν°νμ΄μ€μ μμ‘΄
- μ»΄ν¬μ§μ ν¨ν΄: μ¬λ¬ μ λ΅ κ°μ²΄λ€μ μ‘°ν©νμ¬ λ³΅μ‘ν λ‘μ§ κ΅¬μ±
- λ¨μΌ μ± μ: λμ€ν¨μΉ μ‘°μ μλ§ μ§μ€, μ€μ μ²λ¦¬λ μ μ© μ»΄ν¬λνΈμ μμ
- νμ₯μ±: 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);
}
}
}
ν΅μ¬ μ€κ³ νΉμ§
- try-finally ν¨ν΄: μμΈ λ°μ μ¬λΆμ κ΄κ³μμ΄ νμ²λ¦¬ ν μ€ν 보μ₯
- ν¨μν μΈν°νμ΄μ€ νμ©:
this::doDispatch
λ‘ λ©μλ λ νΌλ°μ€ μ λ¬ - κ³μΈ΅νλ μ€ν: ν β νν° β μ€μ λμ€ν¨μΉ μμ
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;
}
}
μμΈ μ²λ¦¬ μ€κ³ νΉμ§
- μ²΄μΈ μ€λΈ 리μ€νμλΉλ¦¬ν°: μ¬λ¬ 리쑸λ²κ° μμ°¨μ μΌλ‘ μμΈ μ²λ¦¬ μλ
- μ‘°κΈ° μ’ λ£: 첫 λ²μ§Έ μ²λ¦¬ μ±κ³΅ μ λλ¨Έμ§ λ¦¬μ‘Έλ² μ€ν μν¨
- μ μ°ν λ°νκ°:
ResponseEntity
μ§μ λ°ν λλ μΌλ° κ°μ²΄λ₯Ό ν΅ν μΆκ° μ²λ¦¬ - νμ
μμ μ±:
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 νΉμ§
- μν κΈ°λ° μ§ν:
currentFilterIndex
λ‘ νμ¬ μ€ν μμΉ μΆμ - μ¬κ·μ νΈμΆ: κ° νν°κ° λ€μ 체μΈμ μ§μ νΈμΆ
- ν¨μν μΈν°νμ΄μ€:
Dispatcher
λ₯Ό ν΅ν μ΅μ’ μ²λ¦¬ μμ - μ ν μ€ν: νν°λ€μ΄ μμ°¨μ μΌλ‘ μ€νλ¨
Filter μΈν°νμ΄μ€β
public interface Filter extends InfrastructureBean {
void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws IOException;
}
Filter vs Servlet Filter λΉκ΅
νΉμ± | Sprout Filter | Servlet Filter |
---|---|---|
λ§€κ°λ³μ | HttpRequest , HttpResponse , FilterChain | ServletRequest , ServletResponse , FilterChain |
μ²΄ν¬ μμΈ | IOException | IOException , ServletException |
μλͺ μ£ΌκΈ° | Spring λΉ μλͺ μ£ΌκΈ° | μλΈλ¦Ώ 컨ν μ΄λ κ΄λ¦¬ |
μ€μ | @Component + DI | web.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 μ€ν ν¨ν΄
- preHandle: μλ°©ν₯ μ€ν, νλλΌλ
false
λ°ν μ μ 체 μ€λ¨ - postHandle: μλ°©ν₯ μ€ν (LIFO), 컨νΈλ‘€λ¬ μ€ν μ±κ³΅ νμλ§ μ€ν
- afterCompletion: μλ°©ν₯ μ€ν (LIFO), μμΈ λ°μ μ¬λΆ 무κ΄νκ² νμ μ€ν
Interceptor vs Filter λΉκ΅β
νΉμ± | Interceptor | Filter |
---|---|---|
μ€ν μμ | νΈλ€λ¬ λ§€ν ν | νΈλ€λ¬ λ§€ν μ |
μ κ·Ό μ 보 | νΈλ€λ¬ μ 보 μ κ·Ό κ°λ₯ | νΈλ€λ¬ μ 보 μ κ·Ό λΆκ° |
μ€ν λ¨κ³ | 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);
}
}
ν΅μ¬ μ²λ¦¬ κ³Όμ
- κ²½λ‘ λ³μ μΆμΆ:
PathPattern
μ ν΅ν΄ URLμμ{id}
κ°μ λ³μ μΆμΆ - μΈμ ν΄κ²°:
CompositeArgumentResolver
λ‘ λ©μλ νλΌλ―Έν° κ° κ²°μ - λ©μλ νΈμΆ: 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));
}
}
}
}
}
}
ν΅μ¬ μ€κ³ νΉμ§
- 리νλ μ
κΈ°λ° μ€μΊ: λ°νμμ
@ControllerAdvice
λΉλ€μ νμ - λ©μλ μ κ·Όμ±:
setAccessible(true)
λ‘ private νΈλ€λ¬ λ©μλλ νΈμΆ κ°λ₯ - μΊμ± λ©μ»€λμ¦:
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;
}
λ§€μΉ μκ³ λ¦¬μ¦ νΉμ§
- 거리 κΈ°λ° λ§€μΉ: μμΈ νμ κ³μΈ΅μμ κ°μ₯ κ°κΉμ΄(ꡬ체μ μΈ) νΈλ€λ¬ μ ν
- λ€μ€ μμΈ μ§μ:
@ExceptionHandler({Exception1.class, Exception2.class})
- μμ κ΄κ³ κ³ λ €: λΆλͺ¨ μμΈ νΈλ€λ¬κ° νμ μμΈλ μ²λ¦¬ κ°λ₯
- μ νν λ§€μΉ μ°μ : λμΌν νμ μΌ λ 거리 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;
}
}
// μ²λ¦¬λμ§ μμ μμΈλ κΈ°λ³Έ μ²λ¦¬
}
μ€ν μμ
- 컨νΈλ‘€λ¬μμ μμΈ λ°μ
- ControllerAdviceExceptionResolver μ€ν (μ΅μ°μ )
- ControllerAdviceRegistryμμ νΈλ€λ¬ κ²μ
- κ°μ₯ ꡬ체μ μΈ νΈλ€λ¬ μ ν
- νΈλ€λ¬ λ©μλ νΈμΆ
- ResponseResolverλ‘ μλ΅ λ³ν
- μ²λ¦¬ μ€ν¨ μ λ€μ ExceptionResolverλ‘ μ΄λ
Springκ³Όμ μ°¨μ΄μ λΉκ΅β
ControllerAdvice κΈ°λ₯ λΉκ΅
νΉμ± | Spring @ControllerAdvice | Sprout @ControllerAdvice |
---|---|---|
μ μ© λ²μ | μ μ λλ ν¨ν€μ§/ν΄λμ€ μ§μ | μ μλ§ μ§μ |
μ΄λ Έν μ΄μ μμΉ | ν΄λμ€ λ 벨 | ν΄λμ€ λ 벨 |
μ€μΊ λ°©μ | μ»΄ν¬λνΈ μ€μΊ μλ | BeanFactoryμμ μλ μ€μΊ |
λ©μλ μκ·Έλμ² | λ§€μ° μ μ° (Model, WebRequest λ±) | μ νμ (Exception, HttpRequest) |
μλ΅ μ²λ¦¬ | ViewResolver, HttpMessageConverter | ResponseResolver |
μΊμ± | κΈ°λ³Έ μ 곡 | μλ ꡬν (ConcurrentHashMap) |
ExceptionHandler κΈ°λ₯ λΉκ΅
νΉμ± | Spring @ExceptionHandler | Sprout @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 DispatcherServlet | Sprout RequestDispatcher |
---|---|---|
κΈ°λ° κΈ°μ | Servlet API κΈ°λ° | μμ Java κΈ°λ° |
μλͺ μ£ΌκΈ° | μλΈλ¦Ώ 컨ν μ΄λ κ΄λ¦¬ | Spring λΉ μλͺ μ£ΌκΈ° |
μ΄κΈ°ν | init() , destroy() | μμ±μ μ£Όμ |
μμΈ μ²λ¦¬ | HandlerExceptionResolver | ExceptionResolver |
λ·° ν΄κ²° | ViewResolver | ResponseResolver |
νν° μμ€ν μ°¨μ΄μ β
μ€ν μμΉμ λ²μ
// 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);
}
μ£Όμ μ°¨μ΄μ
- λ§€κ°λ³μ νμ : Servlet API vs μ체 HTTP μΆμν
- ModelAndView: Springμ λ·° λͺ¨λΈ μ λ¬, Sproutμ μΌλ° λ°νκ° μ λ¬
- κΈ°λ³Έ ꡬν: 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); // μΈμ λ°°μ΄
μ΅μ ν μ λ΅
- μ²΄μΈ κ°μ²΄ νλ§: InterceptorChain μ¬μ¬μ©
- μΈμ λ°°μ΄ μΊμ±: λμΌν λ©μλμ λν λ°°μ΄ μ¬μ¬μ©
- κ²½λ‘ λ³μ μ΅μ ν: λΉ λ§΅μΌ λ μ±κΈν€ μ¬μ©
λ³λ ¬ μ²λ¦¬ κ³ λ €μ¬νβ
μ€λ λ μμ μ±
- 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));
νμ₯μ±κ³Ό μ μ§λ³΄μμ±β
μλ‘μ΄ μ²λ¦¬ λ¨κ³ μΆκ°β
νμ₯ ν¬μΈνΈ
- DispatchHook: μμ² μ /ν μ²λ¦¬ λ‘μ§ μΆκ°
- Filter: HTTP λ 벨 μ μ²λ¦¬ (μΈμ¦, λ‘κΉ , CORS λ±)
- Interceptor: 컨νΈλ‘€λ¬ λ 벨 μ μ²λ¦¬ (κΆν κ²μ¬, λ‘κΉ λ±)
- ExceptionResolver: μμΈ μ²λ¦¬ μ λ΅ μΆκ°
- 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 νμν©λλ€!