Programming/Spring

[Spring] Spring MVC Config

soeun2537 2025. 5. 12. 18:02
우아한테크코스 레벨 2에서 학습한 내용을 정리한 글입니다.

 

💭 들어가며

Spring MVC Config라는 개념을 두 번째 미션에서 배우게 되었다. 사실 이 개념을 알지 못한 채 사용해 본 적이 있다. 바로 Passport 인증 도입기에서였다. 당시에는 커스텀 어노테이션을 사용해 컨트롤러에서 하나의 파라미터로 인증 정보를 간편하게 받아올 수 있도록 구현했는데, 그때는 뭔가 엄청난 걸 해낸 것 같아 스스로 굉장히 뿌듯했다. 그런데 이번에 제대로 공부해 보니 생각보다 별거 아니라는 느낌이 들었다...ㅎㅎ

아무튼 그때는 그냥 넘어갔던 개념을 이번 기회에 제대로 학습하고 정리해보고자 한다.

 

 

✅ Spring MVC 요청 처리 흐름

  1. 클라이언트 요청 수신
    • 사용자가 웹 브라우저를 통해 HTTP 요청을 보낸다.
  2. DispatcherServlet 처리
    • HTTP 요청이 오면 서블릿 컨테이너(Tomcat)가 이를 받아 DispatcherServlet에 전달한다. DispatcherServlet은 Spring MVC의 프론트 컨트롤러로서 요청을 적절한 핸들러로 전달한다.
  3. HandlerMapping을 통해 핸들러 결정
    • DispatcherServlet은 HandlerMapping을 사용하여 요청을 처리할 적절한 컨트롤러를 결정한다. HandlerMapping은 URL 경로나 HTTP 메소드 등의 기준으로 적절한 컨트롤러를 결정한다.
    • 해당 Handler와 함께 연결된 HandlerInterceptor 목록을 포함하는 HandlerExecutionChain 객체를 반환한다.
  4. HandlerInterceptor의 preHandle() 실행
    • 요청이 컨트롤러에 도달하기 전에 등록되어 있는 HandlerInterceptor의 preHandle() 메서드가 실행된다. 이 단계에서 인증, 로깅 등의 전처리를 수행할 수 있다.
  5. HandlerAdapter를 통한 컨트롤러 호출
    • HandlerAdapter는 결정된 컨트롤러를 호출한다.
    • 이 과정에서 HandlerMethodArgumentResolver를 사용하여 컨트롤러 메서드의 매개변수를 적절히 주입한다.
  6. 컨트롤러 실행
    • 컨트롤러는 비즈니스 로직을 처리하고, 결과를 ModelAndView 객체로 반환한다.
  7. HandlerInterceptor의 postHandle() 실행
    • 컨트롤러 실행 후, 뷰 렌더링 전에 postHandle() 메서드가 실행되어 추가적인 후처리를 수행할 수 있다.
  8. ViewResolver를 통한 뷰 결정
    • ViewResolver는 ModelAndView에서 반환된 논리적 뷰 이름을 실제 뷰로 변환한다.
  9. 뷰 렌더링
    • DispatcherServlet은 찾아낸 View 객체에게 모델 데이터를 넘겨주고 렌더링을 요청한다.
    • 결정된 뷰는 모델 데이터를 사용하여 최종 HTML을 생성한다.
  10. HandlerInterceptor의 afterCompletion() 실행
    • 뷰 렌더링 후, 요청 처리 완료 전에 afterCompletion() 메서드가 실행되어 리소스 정리 등의 작업을 수행할 수 있다.
  11. 클라이언트에 응답 반환
    • View가 생성한 응답 콘텐츠를 DispatcherServlet이 받아서 최종적으로 클라이언트에게 HTTP 응답으로 반환한다.

 

 

✅ Spring MVC Configuration

Spring MVC의 흐름만 봐서는 이해하기 어려울 수 있다. 직접 설정을 통해 동작 원리를 알아보며 익혀 보았다.

 

▶ View Controller

컨트롤러 없이 URL 요청에 대해 단순 View만 응답할 수 있는 기능
  • 정적인 뷰 응답의 경우 Controller를 대체할 수 있다.
  • URL <-> View 간 단순 매핑에 적합하다.
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}

 

▶ Interceptor

요청 전후에 실행되는 공통 로직을 삽입할 수 있는 기능
  • 인증/인가, 로깅, 성능 측정 등 공통 관심사를 분리할 수 있다.

🔽 Interceptor

public class CheckLoginInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("LOGIN_MEMBER") == null) {
            throw new UnauthorizedException("로그인이 필요합니다.");
        }
        return true;
    }
}

🔽 WebMvcConfigurer에 등록

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CheckLoginInterceptor())
                .addPathPatterns("/members/**"); // 적용할 경로 지정
    }
}
  • 컨트롤러 진입 전 로그인 여부를 확인한다.

 

▶ Argument Resolver

컨트롤러 파라미터에 값을 자동으로 주입해주는 기능
  • 컨트롤러 메서드의 파라미터에 세션, 헤더, 토큰 등의 값을 자동으로 주입해 주는 기능이다.
  • 중복되는 로직을 제거하고 코드의 가독성을 높일 수 있다.

🔽 커스텀 파라미터

public class LoginMember {
    private final Long id;

    public LoginMember(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}

🔽 커스텀 어노테이션

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

🔽 ArgumentResolver

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Login.class)
               && parameter.getParameterType().equals(LoginMember.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if (session == null) {
            throw new UnauthorizedException("로그인이 필요합니다.");
        }
        return session.getAttribute("LOGIN_MEMBER");
    }
}
  • supportsParameter는 해당 Resolver가 적용될 조건을 지정하는 메서드이다. 위 예시에서는 파라미터에 @Login 어노테이션이 붙어 있고 타입이 LoginMember일 때 동작한다.
  • resolveArgument는 해당 조건이 충족되었을 때 실행된다. 해당 파라미터에 주입할 값 반환한다.

🔽 WebMvcConfigurer에 등록

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}

🔽 컨트롤러에서 사용

@GetMapping("/reservations")
public String findMyReservation(@Login LoginMember loginMember) {
    // loginMember 객체 바로 사용
}

 

 

📍 참고 자료