Spring MVC Model과 ViewResolver: 데이터를 뷰에 전달하는 방법

Model, ModelMap, ModelAndView의 차이와 ViewResolver 체인 구조를 이해하고, Thymeleaf·InternalResourceViewResolver 설정 방법과 redirect:/forward: 접두사 활용 패턴을 정리합니다.

· 8 min read · PALDYN Team

지난 글에서 컨트롤러가 HTTP 요청에서 값을 꺼내는 방법을 살펴봤습니다. 이번 글에서는 컨트롤러가 꺼낸 데이터를 어떻게 뷰에 전달하고, Spring MVC가 뷰 이름을 실제 템플릿 파일로 어떻게 해석하는지 알아봅니다.

뷰 렌더링 전체 흐름

@Controller가 뷰 이름을 반환하면 DispatcherServletViewResolver 체인을 순서대로 탐색해 뷰 이름을 View 객체로 변환하고, View.render(model, request, response)를 호출합니다.

Spring MVC 뷰 렌더링 흐름

Model, ModelMap, ModelAndView 차이

세 가지 모두 컨트롤러 메서드에서 뷰로 데이터를 전달하는 방법이지만 API가 다릅니다.

Model 인터페이스

Spring이 주입하는 가장 간결한 방법입니다.

@GetMapping("/home")
public String home(Model model) {
    model.addAttribute("greeting", "안녕하세요!");
    model.addAttribute("users", userService.findAll());
    return "home";   // ViewResolver가 templates/home.html로 해석
}

ModelMap

Model을 구현한 클래스로, LinkedHashMap을 내부적으로 사용합니다. Model과 사실상 동일하게 쓸 수 있습니다.

@GetMapping("/dashboard")
public String dashboard(ModelMap modelMap) {
    modelMap.put("stats", statsService.getToday());
    return "dashboard";
}

ModelAndView

뷰 이름과 모델 데이터를 하나의 객체로 묶어 반환합니다. 조건부로 뷰를 선택할 때 편리합니다.

@GetMapping("/report")
public ModelAndView report(@RequestParam String format) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("data", reportService.get());
    if ("pdf".equals(format)) {
        mv.setViewName("report/pdf");
    } else {
        mv.setViewName("report/html");
    }
    return mv;
}

어떤 것을 쓸까?

실무에서는 대부분 Model 파라미터 방식을 씁니다. ModelAndView는 레거시 코드에서 자주 보이며, 조건부 뷰 전환이 필요한 특수 상황에 유용합니다.

ViewResolver 설정

ThymeleafViewResolver (Spring Boot 기본)

spring-boot-starter-thymeleaf 의존성을 추가하면 자동 설정됩니다.

# application.yml
spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML
    encoding: UTF-8
    cache: false   # 개발 중 false, 운영에서는 true

뷰 이름 "home"classpath:/templates/home.html 로 해석됩니다.

InternalResourceViewResolver (JSP)

JSP를 사용하는 레거시 프로젝트 또는 임베디드 서버 없이 외장 톰캣에 배포할 때 사용합니다.

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver r = new InternalResourceViewResolver();
        r.setPrefix("/WEB-INF/views/");
        r.setSuffix(".jsp");
        r.setOrder(2);   // 낮을수록 먼저 탐색
        return r;
    }
}

뷰 이름 "login"/WEB-INF/views/login.jsp 로 해석됩니다.

Spring Boot 임베디드 서버(Tomcat)는 WAR 언패킹 없이 /WEB-INF/ 경로를 서빙하지 못하므로, Spring Boot + JSP 조합은 권장하지 않습니다.

ContentNegotiatingViewResolver

하나의 URL로 HTML과 JSON 등 다양한 표현을 제공해야 할 때 사용합니다.

@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
    ContentNegotiatingViewResolver r = new ContentNegotiatingViewResolver();
    r.setOrder(Ordered.HIGHEST_PRECEDENCE);

    List<ViewResolver> resolvers = new ArrayList<>();
    resolvers.add(thymeleafViewResolver());   // HTML
    resolvers.add(jsonViewResolver());        // JSON
    r.setViewResolvers(resolvers);
    return r;
}

Accept: text/html → Thymeleaf 뷰, Accept: application/json → JSON 뷰가 선택됩니다.

redirect: 와 forward: 접두사

뷰 이름 접두사 패턴

redirect:

@PostMapping("/orders")
public String createOrder(@ModelAttribute OrderForm form) {
    Order order = orderService.create(form);
    // PRG 패턴: POST 처리 후 GET 리다이렉트
    return "redirect:/orders/" + order.getId();
}

redirect: 접두사를 인식하는 것은 UrlBasedViewResolverREDIRECT_URL_PREFIX 상수입니다. RedirectView를 생성해 302 응답을 보냅니다.

리다이렉트 URL에 도메인 상대경로 대신 절대 URL을 쓰고 싶으면 http:// 또는 https://로 시작하면 됩니다.

forward:

@GetMapping("/legacy")
public String forwardToNew() {
    return "forward:/api/v2/resource";
}

RequestDispatcher.forward()를 사용하며, 클라이언트는 URL 변화를 알지 못합니다. 서블릿 체인 내부에서만 이동하므로 @RestController가 반환하는 JSON 응답으로 포워드하는 것도 가능합니다.

@ModelAttribute 메서드: 공통 모델 데이터

@Controller
public class ShopController {

    // 이 컨트롤러의 모든 뷰에 카테고리 목록을 자동으로 추가
    @ModelAttribute("categories")
    public List<Category> loadCategories() {
        return categoryService.findAll();
    }

    @GetMapping("/products")
    public String products(Model model) {
        model.addAttribute("products", productService.findAll());
        // model에 "categories"는 이미 담겨 있음
        return "products";
    }
}

메서드 레벨 @ModelAttribute는 같은 컨트롤러 내 모든 @RequestMapping 메서드 이전에 호출됩니다. 모든 뷰에 공통으로 필요한 데이터(메뉴, 로그인 사용자 정보 등)를 한 곳에서 관리할 수 있습니다.

FlashAttribute: 리다이렉트 후 메시지 전달

리다이렉트 이후에는 Model이 사라지기 때문에, 플래시 메시지(성공/오류 알림)를 전달하려면 RedirectAttributes를 사용합니다.

@PostMapping("/users")
public String create(
        @ModelAttribute @Valid CreateUserForm form,
        BindingResult result,
        RedirectAttributes ra) {
    if (result.hasErrors()) {
        return "users/new";
    }
    userService.create(form);
    // 리다이렉트 후 한 번만 읽히고 사라짐
    ra.addFlashAttribute("successMsg", "사용자가 생성되었습니다.");
    return "redirect:/users";
}

@GetMapping("/users")
public String list(Model model) {
    // model에 "successMsg"가 자동으로 담겨 있음 (한 번만)
    model.addAttribute("users", userService.findAll());
    return "users/list";
}

addFlashAttribute()로 저장한 값은 세션에 임시 저장되고, 리다이렉트 이후 첫 번째 요청에서 자동으로 Model에 추가됩니다.

addAttribute()를 쓰면 URL 쿼리 파라미터로 추가됩니다(?successMsg=...). 민감한 정보는 쿼리 파라미터로 노출하지 말고 반드시 addFlashAttribute()를 사용합니다.

ViewResolver Order 설정

여러 ViewResolver가 등록되면 order 값이 낮을수록 먼저 탐색합니다. null을 반환하면 다음 리졸버로 넘어갑니다.

@Bean
public ThymeleafViewResolver thymeleafViewResolver() {
    ThymeleafViewResolver r = new ThymeleafViewResolver();
    r.setOrder(1);         // 가장 먼저 탐색
    r.setCharacterEncoding("UTF-8");
    return r;
}

@Bean
public InternalResourceViewResolver jspViewResolver() {
    InternalResourceViewResolver r = new InternalResourceViewResolver();
    r.setPrefix("/WEB-INF/views/");
    r.setSuffix(".jsp");
    r.setOrder(2);         // Thymeleaf가 null 반환 시 탐색
    return r;
}

InternalResourceViewResolver는 항상 non-null을 반환하므로 체인의 마지막에 배치해야 합니다.

핵심 정리

  • Model은 Spring이 주입하는 인터페이스로, addAttribute()로 뷰에 데이터를 전달합니다.
  • ModelAndView는 뷰 이름과 모델을 하나로 묶어 조건부 뷰 선택 시 편리합니다.
  • ViewResolver 체인: Thymeleaf(기본) → InternalResource(JSP) → BeanName 순으로 탐색.
  • redirect:/path는 302 리다이렉트, forward:/path는 서버 내부 포워드입니다.
  • PRG 패턴: POST 처리 후 redirect:로 응답해 폼 중복 제출을 방지합니다.
  • RedirectAttributes.addFlashAttribute()로 리다이렉트 후 플래시 메시지를 안전하게 전달합니다.

지난 글: Spring MVC 파라미터 바인딩 완전 정복: @PathVariable부터 @ModelAttribute까지

다음 글: Spring MVC 정적 리소스 처리: CSS·JS·이미지를 효율적으로 서빙하는 법


읽어주셔서 감사합니다. 😊