콘텐츠로 이동

Spring MVC

Spring MVC

개념

Spring MVC는 프론트 컨트롤러 패턴 기반의 웹 프레임워크다. 모든 HTTP 요청이 DispatcherServlet 하나를 거쳐 적절한 컨트롤러로 분배된다.

요청 처리 흐름

HTTP 요청
  → DispatcherServlet
    → HandlerMapping (어떤 컨트롤러?)
    → HandlerAdapter (어떻게 호출?)
    → Controller (비즈니스 로직)
    → ViewResolver (어떤 뷰?)
  → HTTP 응답

핵심 구성 요소

컴포넌트 역할
DispatcherServlet 모든 요청을 받는 프론트 컨트롤러
HandlerMapping URL → 컨트롤러 매핑
HandlerAdapter 컨트롤러 실행 방식 처리
ViewResolver 뷰 이름 → 실제 뷰 변환
Model 뷰에 전달할 데이터 저장소

왜 쓰는가?

서블릿 기반 개발은 URL마다 서블릿 클래스를 만들어야 했다. DispatcherServlet이 이를 통합해 공통 처리(인코딩, 보안, 로깅)를 중앙화하고, 개발자는 비즈니스 로직만 집중할 수 있다.

기본 사용

@Controller
public class MemberController {

    @GetMapping("/members")
    public String list(Model model) {
        model.addAttribute("members", memberService.findAll());
        return "members/list"; // ViewResolver가 templates/members/list.html로 변환
    }

    @PostMapping("/members")
    public String create(@ModelAttribute MemberDto dto) {
        memberService.save(dto);
        return "redirect:/members";
    }
}

주요 어노테이션

어노테이션 설명
@Controller MVC 컨트롤러, 뷰 이름 반환
@RequestMapping URL 매핑 (클래스/메서드 레벨)
@GetMapping, @PostMapping HTTP 메서드별 단축 어노테이션
@PathVariable URL 경로 변수 (/members/{id})
@RequestParam 쿼리 파라미터
@ModelAttribute 폼 데이터 → 객체 바인딩

언제 쓰는지

상황 설명
서버사이드 렌더링 Thymeleaf·JSP로 HTML을 서버에서 생성·반환할 때
폼 기반 웹 페이지 @ModelAttribute로 폼 데이터를 바인딩·검증할 때
관리자 화면 SPA 없이 서버 렌더링 방식의 백오피스

REST API 중심 서비스라면 @RestController를 사용한다. DispatcherServlet 흐름은 동일하다.

장점

장점 설명
공통 처리 중앙화 인코딩, 보안, 로깅을 DispatcherServlet에서 일괄 처리
어노테이션 기반 간결함 @Controller, @GetMapping 등으로 URL 매핑이 명확
유연한 확장 HandlerInterceptor, ArgumentResolver 등 확장 포인트 다수
다양한 뷰 지원 Thymeleaf, JSP, Mustache, JSON 등 뷰 전략 교체 가능

단점 / 주의할 점

상황 문제 해결
redirect: vs forward: 혼동 redirect는 새 요청, forward는 같은 요청 POST 후 redirect(PRG 패턴) 사용
Model에 민감 정보 담기 뷰에 노출될 수 있음 DTO로 필요한 데이터만 전달
컨트롤러에 비즈니스 로직 테스트 어렵고 재사용 불가 Service 레이어로 분리

특징

  • 프론트 컨트롤러 패턴: 모든 요청이 DispatcherServlet 하나를 경유 — 공통 로직 중앙화
  • ArgumentResolver: 컨트롤러 파라미터(@PathVariable, @RequestBody, Model 등)를 자동으로 준비해줌
  • @Controller vs @RestController: @RestController = @Controller + @ResponseBody — 뷰 반환 대신 객체를 JSON으로 직렬화

베스트 프랙티스

  • 컨트롤러는 얇게 — 요청 수신·응답 반환만 담당, 비즈니스 로직은 Service 계층으로 분리
  • DTO 사용 — 엔티티를 뷰에 직접 넘기지 말고 필요한 데이터만 담은 DTO 사용
  • PRG 패턴 — 폼 제출 후 redirect:로 중복 제출 방지
  • @Valid 활용 — 입력 검증은 @ModelAttribute + Bean Validation 어노테이션으로 처리

실무에서는?

  • REST API 중심: @RestController를 주로 사용. DispatcherServlet 흐름은 REST에서도 동일
  • 뷰 렌더링: 관리자 화면 등 서버사이드 HTML이 필요한 경우에만 @Controller + Thymeleaf 사용
  • 전역 예외 처리: @ControllerAdvice + @ExceptionHandler로 예외 일괄 처리 → Exception Handler 참고
  • 공통 로직: 인증·인가·로깅은 HandlerInterceptor로 분리

내부 동작 원리

DispatcherServlet.doDispatch() 상세 흐름

실제로 DispatcherServletdoDispatch() 메서드 하나로 모든 요청을 처리한다.

① HandlerMapping 탐색
   → 등록된 HandlerMapping 목록 순회
   → URL, HTTP 메서드, 헤더 조건과 일치하는 @RequestMapping 메서드를 찾음
   → "GET /members" → MemberController.list() 찾아냄
   → 결과: HandlerExecutionChain (컨트롤러 메서드 + 인터셉터 목록)

② HandlerAdapter 선택
   → 찾은 컨트롤러 메서드를 "어떻게 호출할지" 담당하는 어댑터를 선택
   → @RequestMapping 메서드라면 RequestMappingHandlerAdapter 선택

③ 인터셉터 preHandle() 실행
   → 등록된 HandlerInterceptor들의 preHandle() 순서대로 실행
   → false 반환하면 여기서 요청 중단

④ HandlerAdapter.handle() — 실제 컨트롤러 실행
   → ArgumentResolver로 메서드 파라미터 준비 (자세한 설명 아래)
   → 컨트롤러 메서드 호출
   → ReturnValueHandler로 반환값 처리

⑤ ViewResolver (뷰 렌더링인 경우)
   → 컨트롤러가 "members/list" 문자열 반환
   → ViewResolver가 "templates/members/list.html" 실제 경로로 변환
   → Thymeleaf 등으로 HTML 렌더링

⑥ 인터셉터 postHandle() / afterCompletion() 실행

⑦ HTTP 응답 전송

ArgumentResolver — 파라미터는 어떻게 채워지나?

컨트롤러 메서드에 @PathVariable, @RequestParam, @RequestBody, Model 등 다양한 파라미터가 붙는다. RequestMappingHandlerAdapter가 호출 전에 각 파라미터를 자동으로 채워주는 역할을 한다.

// 이 메서드를 호출하려면 id, model 파라미터가 필요하다
@GetMapping("/members/{id}")
public String find(@PathVariable Long id, Model model) { ... }
RequestMappingHandlerAdapter
  → 파라미터 목록 검사
  → @PathVariable Long id
       → PathVariableMethodArgumentResolver
       → URL에서 {id} 부분 추출 → "42" → Long으로 변환 → 42L
  → Model model
       → ModelMethodProcessor
       → 현재 요청의 Model 객체를 그대로 주입
  → 모든 파라미터 준비 완료 → 메서드 호출
ArgumentResolver 처리하는 파라미터
PathVariableMethodArgumentResolver @PathVariable
RequestParamMethodArgumentResolver @RequestParam
RequestResponseBodyMethodProcessor @RequestBody (JSON → 객체 변환)
ServletModelAttributeMethodProcessor @ModelAttribute
ModelMethodProcessor Model
HttpServletRequestMethodArgumentResolver HttpServletRequest

HandlerMapping 내부 — URL을 어떻게 찾나?

RequestMappingHandlerMapping은 애플리케이션 시작 시점에 모든 @Controller 클래스를 스캔해 @RequestMapping 정보를 Map에 미리 캐싱해 둔다.

애플리케이션 시작
  → @Controller 빈들 스캔
  → 각 메서드의 @RequestMapping, @GetMapping 등 정보 읽음
  → Map<RequestMappingInfo, HandlerMethod>에 저장

요청 처리 시 (매번)
  → Map에서 URL + HTTP 메서드로 조회 → O(1) 성능
  → HandlerMethod (컨트롤러 + 메서드 정보) 반환

@RestController는 뭐가 다른가? @RestController = @Controller + @ResponseBody. @ResponseBody가 있으면 뷰 이름 반환 대신 반환 객체를 HTTP 응답 바디에 직접 직렬화(JSON 변환)한다. DispatcherServlet 흐름은 동일하지만 ⑤단계(ViewResolver)를 건너뛰고 HttpMessageConverter로 직렬화한다.