Published on
👁️

Spring MVC

Authors
  • avatar
    Name
    River
    Twitter
21Spring MVC의 실행 흐름에 대해 설명해주세요.보통
Spring MVC는 Model-View-Controller 패턴을 기반으로 한 웹 프레임워크로, 중앙집중화된 요청 처리 방식을 사용합니다. 실행 흐름은 먼저 클라이언트 요청이 들어오면 DispatcherServlet이 모든 요청을 받아 중앙에서 처리하고, HandlerMapping을 통해 요청 URL에 맞는 컨트롤러를 찾습니다. 선택된 컨트롤러는 비즈니스 로직을 수행하는 Service를 호출하고 데이터를 응답 받아 ModelAndView를 만들고 반환합니다. 이후 ViewResolver가 논리적 뷰 이름을 실제 뷰로 변환합니다. 마지막으로 뷰가 모델 데이터를 사용하여 최종 HTML을 생성하고 클라이언트에게 응답을 반환합니다.
상세 설명

Spring MVC란?

  • Spring Framework의 일부로, Model-View-Controller 패턴을 기반으로 한 웹 프레임워크
  • Front Controller 패턴을 구현하여 중앙 집중화된 요청 처리
  • 유연하고 확장 가능한 웹 애플리케이션 개발을 위한 구조 제공
  • DispatcherServlet이 핵심 역할


  • Front Controller 패턴
    • 모든 클라이언트 요청을 하나의 공통 진입점(Controller)에서 받아 처리하는 구조

Spring MVC 핵심 구성 요소

  • DispatcherServlet

    • 모든 HTTP 요청을 받는 중앙 집중식 컨트롤러
    • Front Controller 패턴의 구현체
    • 요청 처리의 전체 흐름을 조율

  • HandlerMapping

    • 요청 URL을 적절한 컨트롤러로 매핑
    • @RequestMapping@GetMapping 등의 애노테이션 정보를 기반으로 매핑
    • 여러 HandlerMapping 구현체 중 우선순위에 따라 선택

  • Controller

    • 요청을 받고, Service를 호출하고, 결과를 View에 전달하는 조정자 역할
    • 요청을 처리하고 Model 데이터를 준비
    • View 이름 또는 데이터를 반환

  • ViewResolver

    • 논리적 뷰 이름을 실제 뷰로 변환
    • JSP, Thymeleaf, FreeMarker 등 다양한 뷰 기술 지원

Spring MVC 실행 흐름 (8단계)

@Controller
@RequestMapping("/api/v1/users")
public class UserController {
    
    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user/detail";  // 논리적 View 이름
    }
}

  1. 클라이언트 요청

    GET /users/123 HTTP/1.1
    Host: localhost:8080
    
    • 웹 브라우저나 REST 클라이언트에서 HTTP 요청 발송
      • REST 클라이언트는 HTTP 요청을 보내는 도구 (Ex. postman, curl 등)


  2. DispatcherServlet이 요청 수신

    public class DispatcherServlet extends FrameworkServlet {
    
        @Override
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
            *// 모든 요청을 여기서 처리
            ...
            
            // HandlerMapping에게 요청 URL에 맞는 Controller 찾아달라고 요청
            HandlerExecutionChain handler = getHandler(request);
            
            // HandlerAdapter 실행*
            ModelAndView mv = handlerAdapter.handle(request, response, handler);
        **}
    }
    
    • Front Controller로서 모든 요청을 중앙에서 처리
    • 서블릿 컨테이너가 DispatcherServlet으로 요청 전달


  3. HandlerMapping을 통한 핸들러 검색

    *// RequestMappingHandlerMapping이 @GetMapping 정보 확인*
    HandlerExecutionChain handler = handlerMapping.getHandler(request);
    *// UserController의 getUser 메서드가 선택됨*
    
    • 요청 URL **/users/123**을 분석하여 적절한 컨트롤러 메서드 검색
    • @RequestMapping 계열 애노테이션 정보를 기반으로 매핑


  4. HandlerAdapter를 통한 핸들러 실행

    *// HandlerAdapter가 실제 컨트롤러 메서드 호출*
    ModelAndView mv = handlerAdapter.handle(request, response, handler);
    
    • 찾은 핸들러(컨트롤러 메서드)를 실행하기 위한 어댑터 패턴 적용
    • 파라미터 바인딩, 유효성 검사 등 수행


  5. 실제 컨트롤러 처리

    @GetMapping("/users/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        *// 1. 서비스 레이어 호출*
        User user = userService.findById(id);
        
        *// 2. 모델에 데이터 추가*
        model.addAttribute("user", user);
        
        *// 3. 논리적 뷰 이름 반환*
        return "user/detail";
    }
    
    • Service Layer 호출하여 비즈니스 로직 수행
    • Model 객체에 뷰에서 사용할 데이터 추가
    • 논리적 뷰 이름 반환


  6. ViewResolver를 통한 뷰 결정

    @Configuration
    public class WebConfig {
    
        @Bean
        public ViewResolver viewResolver() {
            InternalResourceViewResolver resolver = new InternalResourceViewResolver();
            resolver.setPrefix("/WEB-INF/views/");
            resolver.setSuffix(".jsp");
            return resolver;
        }
    }
    
    *// "user/detail" → "/WEB-INF/views/user/detail.jsp"*
    
    • 논리적 뷰 이름을 실제 뷰 파일 경로로 변환

    • 다양한 뷰 기술 지원 (JSP, Thymeleaf 등)

    • Spring Boot의 경우 자동으로 논리적 뷰와 실제 뷰 파일 경로를 application.yml을 통해 자동 매칭한다.
      // application.yml
      spring:
      thymeleaf:
          prefix: classpath:/templates/
          suffix: .html
      
      // "user/detail" → "src/main/resources/templates/user/detail.html"
      


  7. 뷰에서 모델 렌더링

    <!-- /WEB-INF/views/user/detail.jsp -->
    <html>
    <body>
        <h1>사용자 정보</h1>
        <p>이름: ${user.name}</p>
        <p>이메일: ${user.email}</p>
    </body>
    </html>
    
    • 뷰가 Model 데이터를 사용하여 최종 HTML 생성
    • 템플릿 엔진이 동적 콘텐츠 처리
    • 템플릿 엔진은 “서버에서 동적으로 HTML을 생성하는 도구”이다.
      • JSP는 ${user.name}, Thymeleaf는 th:text="${user.name}"


  8. 클라이언트에게 응답 전송

    <!-- 최종 생성된 HTML -->
    <html>
    <body>
        <h1>사용자 정보</h1>
        <p>이름: 홍길동</p>
        <p>이메일: hong@example.com</p>
    </body>
    </html>
    
    • 완성된 HTML이 HTTP 응답으로 클라이언트에 전송
  • 어댑터 패턴
    • 서로 다른 인터페이스를 가진 클래스들을 연결해주는 패턴
22@Controller와 @RestController의 차이점을 설명해주세요.쉬움
@Controller와 @RestController 모두 Spring MVC에서 웹 요청을 처리하는 컨트롤러 애노테이션이지만, 응답 처리 방식에 차이가 있습니다. @Controller는 메서드의 반환값이 논리적 뷰 이름으로 해석되어 HTML 페이지를 렌더링하는 SSR 방식으로 사용되는 반면 @RestController는 자동으로 JSON을 반환하여 RESTful API 개발에 특화되어 있습니다. @RestController는 쉽게 @Controller + @ResponseBody의 조합으로 생각하면 됩니다. 다만, 현재 실무에서는 프론트엔드 분리 추세에 따라 신규 서비스의 경우 @RestController를 주로 사용합니다.
상세 설명

@Controller vs @RestController 개요

  • @Controller

    • 뷰 기반 웹 애플리케이션에서 사용
    • 메서드 반환값이 논리적 뷰 이름으로 해석
    • HTML 페이지 렌더링이 목적
    • Spring MVC의 전통적인 방식


  • @RestController

    • RESTful API 개발에 특화
    • 메서드 반환값이 HTTP 응답 본문으로 직접 변환
    • JSON 데이터 직접 반환
    • @Controller + @ResponseBody의 조합

@Controller 상세 분석

  • 기본 동작 방식

    @Controller
    public class WebController {
        
        @GetMapping("/users")
        public String listUsers(Model model) {
            List<User> users = userService.findAll();
            model.addAttribute("users", users);
            return "user/list";    *// 뷰 이름 반환*
        }
        
        
        @GetMapping("/home")
        public ModelAndView home() {
            ModelAndView mv = new ModelAndView();
            mv.setViewName("home");
            mv.addObject("message", "Welcome!");
            return mv;    *// ModelAndView 반환*
        }
    }
    
    • 반환값 "user/list"는 논리적 뷰 이름
    • ViewResolver가 실제 뷰 파일로 변환 (/WEB-INF/views/user/list.jsp)
    • Model 객체를 통해 뷰에 데이터 전달
      • ModelAndView로 한번에 전달 가능


  • @ResponseBody와 함께 사용

    @Controller
    public class HybridController {
        
        // 일반적인 뷰 반환
        @GetMapping("/users/form")
        public String userForm() {
            return "user/form";
        }
        
        // JSON 데이터 반환
        @GetMapping("/api/users")
        @ResponseBody
        public List<User> getUsers() {
            return userService.findAll();  // JSON으로 변환됨
        }
        
        // 개별 메서드마다 @ResponseBody 적용 가능
        @PostMapping("/api/users")
        @ResponseBody
        public ResponseEntity<User> createUser(@RequestBody User user) {
            User savedUser = userService.save(user);
            return ResponseEntity.ok(savedUser);
        }
    }
    

@RestController 상세 분석

  • 기본 동작 방식

    @RestController
    public class UserRestController {
        
        @GetMapping("/api/users")
        public List<User> getAllUsers() {
            return userService.findAll();  // 자동으로 JSON 변환
        }
        
        @PostMapping("/api/users")
        public ResponseEntity<User> createUser(@RequestBody User user) {
            User savedUser = userService.save(user);
            return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
        }
    }
    
    • 모든 메서드에 자동으로 @ResponseBody 적용
    • 반환값이 HTTP 응답 본문으로 직접 변환
    • JSON 형태로 데이터 반환 (기본)

HTTP Message Converter의 역할

  • JSON 변환 과정

    @RestController
    public class ProductController {
        
        @GetMapping("/api/products/{id}")
        public Product getProduct(@PathVariable Long id) {
            return new Product(id, "Laptop", 1200000);
        }
    }
    
    • 실제 HTTP 응답

      HTTP/1.1 200 OK
      Content-Type: application/json
      
      {
      "id": 1,
      "name": "Laptop",
      "price": 1200000
      }
      


  • 다양한 Content-Type 지원

    @RestController
    public class DataController {
        
        // JSON 반환 (기본)
        @GetMapping(value = "/api/data", produces = "application/json")
        public Map<String, Object> getJsonData() {
            return Map.of("message", "Hello JSON", "timestamp", System.currentTimeMillis());
        }
        
        // XML 반환
        @GetMapping(value = "/api/data/xml", produces = "application/xml")
        public User getXmlData() {
            return new User("John", "john@example.com");
        }
        
        // 단순 텍스트 반환
        @GetMapping(value = "/api/health", produces = "text/plain")
        public String healthCheck() {
            return "OK";
        }
    }
    
    • JSON 반환이 기본이지만 다른 content-type도 가능하다.

하이브리드 패턴 (레거시 코드)

// 웹 페이지와 API를 함께 제공하는 컨트롤러
@Controller
public class DashboardController {
    
    // 대시보드 페이지 렌더링
    @GetMapping("/dashboard")
    public String dashboard() {
        return "dashboard";  // dashboard.html
    }
    
    
    // AJAX 요청을 위한 JSON 데이터 제공
    @GetMapping("/dashboard/stats")
    @ResponseBody
    public Map<String, Object> getDashboardStats() {
        return Map.of(
            "totalUsers", userService.getTotalCount(),
            "activeUsers", userService.getActiveCount(),
            "todayOrders", orderService.getTodayCount()
        );
    }
    
    
    // 차트 데이터 제공
    @GetMapping("/dashboard/chart-data")
    @ResponseBody
    public List<ChartData> getChartData(@RequestParam String period) {
        return analyticsService.getChartData(period);
    }
}

Content Negotiation

@RestController
public class FlexibleController {
    
    @GetMapping("/api/user/{id}")
    public User getUser(@PathVariable Long id, HttpServletRequest request) {
        // Accept 헤더에 따라 자동으로 JSON/XML 변환
        return userService.findById(id);
    }
}
  • 서버가 지원하는 포맷 내에서 클라이언트의 Accept 헤더에 따라 자동 변환된다
    • 클라이언트 요청 예시
      • Accept: application/json → JSON 응답
      • Accept: application/xml → XML 응답
      • Accept: */* → 기본값 (JSON) 응답

주요 차이점 요약

구분@Controller@RestController
주 사용 목적웹 페이지 렌더링REST API 제공
반환값 처리뷰 이름으로 해석HTTP 응답 본문으로 직접 변환
@ResponseBody개별 메서드에 필요 시 추가모든 메서드에 자동 적용
View Resolver사용함사용하지 않음
Content-Typetext/html (주로)application/json (주로)
클라이언트웹 브라우저모바일 앱, SPA, 다른 서버
23@ResponseBody와 ResponseEntity의 차이점을 설명해주세요.쉬움
@ResponseBody와 ResponseEntity는 모두 HTTP 응답을 처리하는 방법이지만, 제어할 수 있는 범위에 차이가 있습니다. @ResponseBody는 메서드의 반환값을 HTTP 응답 본문으로 직접 변환하는 간단한 방식으로, 주로 데이터만 반환할 때 사용합니다. 이 경우 HTTP 상태 코드는 정상 응답 시 기본적으로 200 OK가 되고, 헤더를 직접 제어하기 어렵습니다. 반면 ResponseEntity는 응답 본문뿐만 아니라 HTTP 상태 코드, 헤더까지 모두 명시적으로 제어할 수 있으며 예외 처리 까지 가능한 유연한 방식입니다.
상세 설명

@ResponseBody vs ResponseEntity<T> 개요

  • @ResponseBody

    • 메서드 반환값을 HTTP 응답 본문으로 직접 변환
    • 간단한 데이터 반환에 특화
    • HTTP 상태 코드는 기본값(200 OK) 사용
    • 헤더 제어 제한적


  • ResponseEntity<T>

    • HTTP 응답 전체를 세밀하게 제어
    • 응답 본문 + 상태 코드 + 헤더 모두 설정 가능
    • 복잡한 응답 처리에 적합
    • REST API의 표준적인 응답 방식

@ResponseBody

  • 기본 사용 및 한계

    @Controller
    public class SimpleController {
        
        @GetMapping("/api/users")
        @ResponseBody
        public List<User> getUsers() {
            return userService.findAll();
            // HTTP/1.1 200 OK
    }
    
    • 반환값이 자동으로 JSON으로 변환

    • 정상 처리 시 HTTP 상태 코드는 200 OK (기본값)

      다른 상태 코드 설정 불가

    • 헤더 설정 불가

ResponseEntity<T>

  • 기본 사용법

    @RestController
    public class FlexibleController {
        
        @GetMapping("/users")
        public ResponseEntity<List<User>> getUsers() {
            List<User> users = userService.findAll();
            return ResponseEntity.ok(users);
            // HTTP/1.1 200 OK
        }
        
        
        @GetMapping("/users/{id}")
        public ResponseEntity<User> getUser(@PathVariable Long id) {
            return userService.findById(id)
                    .map(ResponseEntity::ok)                    
                    // 200 OK*.orElse(ResponseEntity.notFound().build()); 
                    // 404 Not Found
        }
        
        
        @PostMapping("/users")
        public ResponseEntity<User> createUser(@RequestBody User user) {
            User savedUser = userService.save(user);
            return ResponseEntity.status(HttpStatus.CREATED)    
            // 201 Created*.body(savedUser);
        }
        
        
        @DeleteMapping("/users/{id}")
        public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
            if (userService.existsById(id)) {
                userService.deleteById(id);
                return ResponseEntity.noContent().build();      // 204 No Content
            }
            return ResponseEntity.notFound().build();           // 404 Not Found
        }
    }
    
    • 다양한 상태 코드 활용 가능


  • 응답 헤더 설정

    @RestController
    public class HeaderController {
        
        @GetMapping("/api/data")
        public ResponseEntity<String> processData(@RequestBody String data) {
            String result = dataService.process(data);
            
            HttpHeaders headers = new HttpHeaders();
            headers.add("X-Request-ID", UUID.randomUUID().toString());
            
            return ResponseEntity.ok()
                    .headers(headers)
                    .body(result);
        }
    }
    


  • 조건부 응답 처리

    @RestController
    public class ConditionalController {
        
        @PostMapping("/orders")
        public ResponseEntity<?> createOrder(@RequestBody Order order) {
            try {
                Order savedOrder = orderService.save(order);
                return ResponseEntity.status(HttpStatus.CREATED).body(savedOrder);
                
            } catch (InsufficientStockException e) {
                return ResponseEntity.status(HttpStatus.CONFLICT)
                        .body(Map.of("error", "재고 부족"));
                        
            } catch (InvalidOrderException e) {
                return ResponseEntity.badRequest()
                        .body(Map.of("error", "잘못된 주문 데이터"));
            }
        }
    }
    


  • 예외 처리와 통합

    @RestController
    public class UserController {
        
        @GetMapping("/users/{id}")
        public ResponseEntity<User> getUser(@PathVariable Long id) {
            return userService.findById(id)
                    .map(ResponseEntity::ok)
                    .orElse(ResponseEntity.notFound().build());
        }
        
        
        @ExceptionHandler(ValidationException.class)
        public ResponseEntity<Map<String, String>> handleValidation(ValidationException ex) {
            return ResponseEntity.badRequest()
                    .body(Map.of("error", ex.getMessage()));
        }
    }
    

중요

REST API 설계 시에는 적절한 HTTP 상태 코드 사용이 매우 중요하다. @ResponseBody는 간단한 데이터 반환에는 편리하지만, RESTful한 API를 구축하려면 ResponseEntity<T>를 사용하거나 커스텀 응답을 설정하여 의미 있는 상태 코드와 헤더를 제공해야 한다.

주요 차이점 요약

구분@ResponseBodyResponseEntity<T>
HTTP 상태 코드200 OK (기본)임의 설정 가능
HTTP 헤더제한적 (Content-Type만)모든 헤더 설정 가능
사용 복잡도간단상대적으로 복잡
유연성낮음높음
REST API 적합성부분적완전 적합
에러 처리@ExceptionHandler 의존메서드 내에서 직접 처리
적합한 상황단순 데이터 반환복잡한 응답 제어
24@RequestBody와 @ModelAttribute의 차이점을 설명해주세요.보통
@RequestBody와 @ModelAttribute는 HTTP 요청 데이터를 Java 객체로 바인딩하는 방식에 차이가 있습니다. @RequestBody는 HTTP 요청의 본문(body) 전체를 Java 객체로 변환하며, 주로 JSON 데이터를 처리할 때 사용됩니다. @ModelAttribute는 요청 파라미터, 폼 데이터, 쿼리 스트링을 객체의 필드에 바인딩하는 데 사용됩니다. 실무에서는 JSON 데이터에는 @RequestBody를, 쿼리 파라미터나 폼 데이터에는 @ModelAttribute를 사용합니다.
상세 설명

@RequestBody vs @ModelAttribute 개요

  • @RequestBody

    • HTTP 요청의 body 전체를 읽어서 객체로 변환
    • HttpMessageConverter 사용 (주로 MappingJackson2HttpMessageConverter)
    • JSON 전체를 한번에 역직렬화해서 객체 생성
    • Jackson이 리플렉션으로 생성자 호출하고 필드 설정
    • Content-Type 제약
      • application/json, application/xml 등만 가능


  • @ModelAttribute

    • 요청 파라미터들을 객체 필드에 바인딩
      • 쿼리 스트링, 폼 데이터
    • Spring DataBinder 사용
    • 빈 객체 생성 후 ⇒ setter 메서드로 하나씩 필드 바인딩
    • PropertyEditor/Converter로 타입 변환
    • Content-Type 제약
      • Content-Type 상관없음 (쿼리 스트링은 아예 Content-Type 없음)

사용 예시

  • @RequestBody - JSON 데이터

    @RestController
    public class UserApiController {
        
        @PostMapping("/api/users")
        public ResponseEntity<UserResponse> createUser(@RequestBody UserCreateRequest request) {
            return ResponseEntity.ok(userService.createUser(request));
        }
    }
    
    • JSON 요청 본문을 DTO로 변환

    • 클라이언트 요청 예시

      POST /api/users// 
      Content-Type: application/json  
      
      {
          "name": "홍길동",
          "email": "hong@example.com",   
          "age": 30
      }
      


  • @ModelAttribute

    @RestController
    public class UserSearchController {
        
        @GetMapping("/api/users")
        public ResponseEntity<List<UserResponse>> searchUsers(@ModelAttribute UserSearchRequest request) {
            List<UserResponse> users = userService.searchUsers(request);
            return ResponseEntity.ok(users);
        }
    }
    
    • 쿼리 파라미터를 DTO로 바인딩
    • 요청 URL
      • GET /api/users?name=홍&minAge=20&maxAge=40
    • UserSearchRequest 객체 자동 바인딩
      • name = "홍", minAge = 20, maxAge = 40

데이터 바인딩 과정 비교

  • @RequestBody - JSON 데이터 처리 과정

    1. HTTP 요청 본문 통째로 읽기

      Content-Type: application/json
      {"name": "홍길동", "email": "hong@example.com", "age": 30}
      
    2. HttpMessageConverter가 JSON ⇒ 객체 변환 (역직렬화)

    3. Jackson이 리플렉션으로 객체 생성

      User user = objectMapper.readValue(jsonString, User.class);
      
      • 생성자 선택 규칙 (@JsonCreator > 단일 생성자 > 기본 생성자)


  • @ModelAttribute 처리 과정

    1. 요청 파라미터들을 개별적으로 추출

      POST /users
      Content-Type: application/x-www-form-urlencoded
      name=홍길동&email=hong@example.com&age=30
      
    2. Spring DataBinder가 빈 객체 생성

    3. setter 메서드로 하나씩 필드 바인딩

      User user = new User();  // 반드시 기본 생성자 필요
      user.setName(request.getParameter("name"));
      user.setAge(Integer.parseInt(request.getParameter("age")));
      
      • Spring이 기본 생성자로 빈 객체 생성 후 setter 호출

파일 처리

  • @RequestBody

    • 불가능 (JSON은 바이너리 못담음)


  • @ModelAttribute

    • MultipartFile 처리 가능

      @RestController
      public class FileController {
          
          // @ModelAttribute로 파일 + 메타데이터 처리
          @PostMapping("/api/files")
          public ResponseEntity<FileResponse> uploadFile(@ModelAttribute FileUploadRequest request) {
              return ResponseEntity.ok(fileService.uploadFile(request));
          }
      }
      
      public class FileUploadRequest {
          private MultipartFile file;    // 파일
          private String title;          // 메타데이터
          private String description;    // 메타데이터
          // getters, setters...
      }
      

주요 특징 비교

  • 에러 처리 방식

    • @RequestBody
      • JSON 파싱 오류 시 HttpMessageNotReadableException 발생
      • BindingResult 사용 불가능 (Spring 공식 제한 사항)
      • 예외 처리는 @ExceptionHandler로만 가능
    • @ModelAttribute
      • 바인딩 오류 시 BindingResult에 저장
      • 메서드에서 BindingResult 파라미터로 오류 확인 가능


  • 복잡한 객체 처리 능력

    • @RequestBody
      • 중첩 객체, 컬렉션, 배열 모두 자동 처리
    • @ModelAttribute
      • 중첩 객체 (address.street), 컬렉션 (items[0].name) 제한적 지원


  • 사용 제약사항

    • @RequestBody
      • 메서드당 1개만 사용 가능 (body가 하나니까)
    • @ModelAttribute
      • 이론상 여러개 가능하지만 권장하지 않음


  • 생성자 요구사항
    • @RequestBody
      • 기본 생성자 없어도 됨
    • @ModelAttribute
      • 반드시 기본 생성자 필요

주요 차이점 요약

구분@RequestBody@ModelAttribute
데이터 소스HTTP 요청 본문요청 파라미터, 폼 데이터
Content-Typeapplication/jsonapplication/x-www-form-urlencoded
처리 메커니즘HttpMessageConverterSpring Data Binding
객체 생성역직렬화를 통한 생성기본 생성자 + Setter
파일 업로드불가능가능 (MultipartFile)
에러 처리Exception 발생BindingResult에 저장
주 사용 상황REST API JSON 데이터쿼리 파라미터, 폼 데이터, 파일
25Filter와 Interceptor의 차이점을 설명해주세요.보통
Filter와 Interceptor는 모두 요청과 응답을 가로채어 공통 로직을 처리할 수 있는 메커니즘이지만, 동작 레벨과 기능에서 차이가 있습니다. Filter는 서블릿 컨테이너 레벨에서 동작하여 모든 요청에 대해 전처리와 후처리를 수행하며, 주로 인코딩, 보안, 로깅 등의 웹 애플리케이션 전반적인 관심사를 처리합니다. 반면 Interceptor는 Spring MVC 레벨에서 동작하여 컨트롤러 실행 전후와 뷰 렌더링 전후에 로직을 수행할 수 있으며, Spring의 DI 컨테이너를 활용할 수 있어 비즈니스 로직과 관련된 공통 처리에 적합합니다.
상세 설명

Filter vs Interceptor 개요

  • Filter

    • 서블릿 컨테이너 레벨에서 동작
    • 모든 요청에 대한 전처리/후처리
    • DispatcherServlet 실행 전에 동작
    • 웹 애플리케이션 전반의 공통 관심사 처리


  • Interceptor

    • Spring MVC 레벨에서 동작
    • 컨트롤러 실행 전후 처리
      • Service/Repository 계층의 공통 관심사는 주로 AOP를 사용
    • Spring 컨텍스트(= Spring IoC Container) 접근 가능
    • DispatcherServlet 내부에서 동작

REST API 요청 처리 흐름

클라이언트 요청
[Filter] ← 서블릿 컨테이너 레벨
DispatcherServletSpring MVC 시작
[Interceptor] preHandle()
HandlerAdapter
Controller  (REST API)
[Interceptor] postHandle()
JSON 응답 생성
[Interceptor] afterCompletion()
DispatcherServlet 완료
[Filter] 후처리
클라이언트 응답

Filter 구현

@Component
@Order(1)
@Slf4j
public class RequestLoggingFilter implements Filter {
    
    @Override
    public void doFilter(
            ServletRequest request, ServletResponse response, FilterChain chain
        ) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        String uri = httpRequest.getRequestURI();
        
        // MDC에 요청 ID 설정 (로그 추적용)
        MDC.put("requestId", requestId);
        
        long startTime = System.currentTimeMillis();
        log.info("API 요청 시작: {} {}", httpRequest.getMethod(), uri);
        
        try {
            chain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            log.info("API 요청 완료: {} {} - {}ms (status: {})", 
                    httpRequest.getMethod(), uri, duration, httpResponse.getStatus());
            MDC.clear();
        }
    }
}
  • 주요 특징

    • chain.doFilter()로 다음 필터 또는 서블릿으로 요청 전달
    • 예외 발생 시에도 후처리 로직 실행 보장 (finally 블록)
    • Spring Bean 주입 시 @Autowired 대신 수동 설정 필요

  • 예시

    • 인코딩 처리 Filter (ex. request.setCharacterEncoding("UTF-8");)
    • 보안 헤더 설정 Filter
    • CORS 처리 Filter

Interceptor 상세 분석

@Component
@RequiredArgsConstructor
@Slf4j
public class ApiKeyInterceptor implements HandlerInterceptor {
    
    private final ApiKeyService apiKeyService;
    private final ObjectMapper objectMapper;
    
    @Override
    public boolean preHandle(
        HttpServletRequest request, HttpServletResponse response, Object handler
    ) throws Exception {
        
        String apiKey = request.getHeader("X-API-KEY");
        
        if (!apiKeyService.isValidApiKey(apiKey)) {
            sendErrorResponse(response, "Invalid API Key");
            return false;
        }
        
        // 유효한 API Key의 사용자 정보를 요청에 설정
        String userId = apiKeyService.getUserIdByApiKey(apiKey);
        request.setAttribute("userId", userId);
        
        return true;
    }

    @Override
    public void afterCompletion(
        HttpServletRequest request, HttpServletResponse response, 
    Object handler, Exception ex
    ) throws Exception {
        
        String userId = (String) request.getAttribute("userId");
        if (userId != null && response.getStatus() == 200) {
            // API 사용량 기록
            apiKeyService.recordApiUsage(userId, request.getRequestURI());
        }
        
        if (ex != null) {
            log.error("API 처리 중 예외 발생: {}", request.getRequestURI(), ex);
        }
    }
    
    
    private void sendErrorResponse(
        HttpServletResponse response, String message
    ) throws IOException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        
        ErrorResponse errorResponse = new ErrorResponse("UNAUTHORIZED", message);
        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}
  • 주요 특징

    • preHandle() 반환값으로 요청 진행 여부 결정
    • Spring Bean 주입 가능 (@Autowired)
    • ModelAndView 접근 가능 (View에 데이터 추가)
    • 핸들러 메서드 정보 접근 가능

  • 예시

    • 성능 모니터링 Interceptor

Interceptor 등록 설정

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    
    private final ApiKeyInterceptor apiKeyInterceptor;
    private final RateLimitInterceptor rateLimitInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**");
        
        registry.addInterceptor(apiKeyInterceptor)
                .addPathPatterns("/api/private/**")
                .excludePathPatterns("/api/public/**");
    }
}

실무 활용 사례

  • Filter 주요 용도

    • 문자 인코딩 (모든 요청/응답의 UTF-8 인코딩 처리)
    • CORS 처리 (브라우저 Cross-Origin 정책 대응)
    • 보안 헤더 (XSS, CSRF 방지 등 보안 헤더 설정)
    • 요청/응답 로깅 (모든 HTTP 요청에 대한 통합 로그 수집 및 추적)
    • 압축 처리 (Gzip 압축 등 응답 최적화)
    • IP 기반 접근 제어 (특정 IP 대역의 요청 차단 또는 허용)

  • Interceptor 주요 용도

    • API Key 검증 (외부 API 호출 시 인증 처리)
    • Rate Limiting (IP/사용자별 요청 제한)
    • 성능 모니터링 (API별 응답 시간 측정 및 메트릭 수집)
    • API 사용량 추적 (사용자별 API 호출 기록)
    • 요청 검증 (특정 헤더나 파라미터 유효성 체크)

예외 처리 차이

  • Filter의 예외 처리

    • Filter에서 발생한 예외는 직접 처리해야 함
    • Spring의 @ControllerAdvice 사용 불가
    • HTTP 상태 코드와 JSON 응답을 직접 설정

  • Interceptor의 예외 처리

    • Spring MVC의 예외 처리 메커니즘 사용 가능
    • @ControllerAdvice@ExceptionHandler로 통합 처리
    • afterCompletion() 메서드에서 예외 정보 확인 가능
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ErrorResponse> handleException(Exception ex) {
            // Interceptor에서 발생한 예외도 여기서 처리됨
            return ResponseEntity.internalServerError()
                    .body(new ErrorResponse("INTERNAL_ERROR", ex.getMessage()));
        }
    }
    

중요

Filter는 서블릿 컨테이너 레벨에서 동작하므로 Spring의 예외 처리 메커니즘을 사용할 수 없습니다. 반면 Interceptor는 Spring MVC 레벨에서 동작하므로 @ExceptionHandler를 통한 예외 처리가 가능합니다.

Spring Security와의 관계

  • Filter vs Interceptor 역할 분담

    • Spring Security Filter
      • JWT 토큰 검증, 사용자 인증/인가
    • Interceptor
      • 비즈니스 로직 관련 추가 검증 (API Key, Rate Limiting 등)


  • Spring Security의 필터들은 Filter 체인의 일부로 동작하며, DispatcherServlet 이전에 인증/인가를 처리한다.

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
        
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            return http
                    .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/public/**").permitAll()
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated())
                    .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                    .build();
        }
    }
    

  • 이후 Interceptor에서는 이미 인증된 사용자 정보를 기반으로 추가적인 비즈니스 로직을 수행할 수 있다.

주요 차이점 요약

구분FilterInterceptor
동작 레벨서블릿 컨테이너Spring MVC
실행 시점DispatcherServlet 이전/이후컨트롤러 실행 전/후
Spring Bean 접근제한적 (수동 설정 필요)완전 지원 (생성자 주입)
예외 처리직접 처리 필요@ControllerAdvice 사용 가능
설정 방법@Component + @OrderWebMvcConfigurer
적용 범위모든 요청 (정적 리소스 포함)Spring MVC 요청만
주요 용도로깅, CORS, 인코딩, 보안 헤더API Key 검증, Rate Limiting, 성능 측정
Handler 정보 접근불가능가능 (메서드, 애노테이션 등)
응답 처리HTTP Response 직접 조작JSON 응답 처리 최적화