- Published on
- •👁️
Spring MVC
- Authors

- Name
- River
상세 설명
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 이름
}
}
클라이언트 요청
GET /users/123 HTTP/1.1 Host: localhost:8080- 웹 브라우저나 REST 클라이언트에서 HTTP 요청 발송
- REST 클라이언트는 HTTP 요청을 보내는 도구 (Ex. postman, curl 등)
- 웹 브라우저나 REST 클라이언트에서 HTTP 요청 발송
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으로 요청 전달
HandlerMapping을 통한 핸들러 검색
*// RequestMappingHandlerMapping이 @GetMapping 정보 확인* HandlerExecutionChain handler = handlerMapping.getHandler(request); *// UserController의 getUser 메서드가 선택됨*- 요청 URL **
/users/123**을 분석하여 적절한 컨트롤러 메서드 검색 @RequestMapping계열 애노테이션 정보를 기반으로 매핑
- 요청 URL **
HandlerAdapter를 통한 핸들러 실행
*// HandlerAdapter가 실제 컨트롤러 메서드 호출* ModelAndView mv = handlerAdapter.handle(request, response, handler);- 찾은 핸들러(컨트롤러 메서드)를 실행하기 위한 어댑터 패턴 적용
- 파라미터 바인딩, 유효성 검사 등 수행
실제 컨트롤러 처리
@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 객체에 뷰에서 사용할 데이터 추가
- 논리적 뷰 이름 반환
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"
뷰에서 모델 렌더링
<!-- /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}"
- JSP는
클라이언트에게 응답 전송
<!-- 최종 생성된 HTML --> <html> <body> <h1>사용자 정보</h1> <p>이름: 홍길동</p> <p>이메일: hong@example.com</p> </body> </html>- 완성된 HTML이 HTTP 응답으로 클라이언트에 전송
- 어댑터 패턴
- 서로 다른 인터페이스를 가진 클래스들을 연결해주는 패턴
상세 설명
@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-Type | text/html (주로) | application/json (주로) |
| 클라이언트 | 웹 브라우저 | 모바일 앱, SPA, 다른 서버 |
상세 설명
@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>를 사용하거나 커스텀 응답을 설정하여 의미 있는 상태 코드와 헤더를 제공해야 한다.
주요 차이점 요약
| 구분 | @ResponseBody | ResponseEntity<T> |
|---|---|---|
| HTTP 상태 코드 | 200 OK (기본) | 임의 설정 가능 |
| HTTP 헤더 | 제한적 (Content-Type만) | 모든 헤더 설정 가능 |
| 사용 복잡도 | 간단 | 상대적으로 복잡 |
| 유연성 | 낮음 | 높음 |
| REST API 적합성 | 부분적 | 완전 적합 |
| 에러 처리 | @ExceptionHandler 의존 | 메서드 내에서 직접 처리 |
| 적합한 상황 | 단순 데이터 반환 | 복잡한 응답 제어 |
상세 설명
@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 데이터 처리 과정
HTTP 요청 본문 통째로 읽기
Content-Type: application/json {"name": "홍길동", "email": "hong@example.com", "age": 30}HttpMessageConverter가 JSON ⇒ 객체 변환 (역직렬화)
Jackson이 리플렉션으로 객체 생성
User user = objectMapper.readValue(jsonString, User.class);- 생성자 선택 규칙 (@JsonCreator > 단일 생성자 > 기본 생성자)
@ModelAttribute 처리 과정
요청 파라미터들을 개별적으로 추출
POST /users Content-Type: application/x-www-form-urlencoded name=홍길동&email=hong@example.com&age=30Spring DataBinder가 빈 객체 생성
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로만 가능
- JSON 파싱 오류 시
- @ModelAttribute
- 바인딩 오류 시 BindingResult에 저장
- 메서드에서
BindingResult파라미터로 오류 확인 가능
- @RequestBody
복잡한 객체 처리 능력
- @RequestBody
- 중첩 객체, 컬렉션, 배열 모두 자동 처리
- @ModelAttribute
- 중첩 객체 (
address.street), 컬렉션 (items[0].name) 제한적 지원
- 중첩 객체 (
- @RequestBody
사용 제약사항
- @RequestBody
- 메서드당 1개만 사용 가능 (body가 하나니까)
- @ModelAttribute
- 이론상 여러개 가능하지만 권장하지 않음
- @RequestBody
- 생성자 요구사항
- @RequestBody
- 기본 생성자 없어도 됨
- @ModelAttribute
- 반드시 기본 생성자 필요
- @RequestBody
주요 차이점 요약
| 구분 | @RequestBody | @ModelAttribute |
|---|---|---|
| 데이터 소스 | HTTP 요청 본문 | 요청 파라미터, 폼 데이터 |
| Content-Type | application/json | application/x-www-form-urlencoded |
| 처리 메커니즘 | HttpMessageConverter | Spring Data Binding |
| 객체 생성 | 역직렬화를 통한 생성 | 기본 생성자 + Setter |
| 파일 업로드 | 불가능 | 가능 (MultipartFile) |
| 에러 처리 | Exception 발생 | BindingResult에 저장 |
| 주 사용 상황 | REST API JSON 데이터 | 쿼리 파라미터, 폼 데이터, 파일 |
상세 설명
Filter vs Interceptor 개요
Filter
- 서블릿 컨테이너 레벨에서 동작
- 모든 요청에 대한 전처리/후처리
- DispatcherServlet 실행 전에 동작
- 웹 애플리케이션 전반의 공통 관심사 처리
Interceptor
- Spring MVC 레벨에서 동작
- 컨트롤러 실행 전후 처리
- Service/Repository 계층의 공통 관심사는 주로 AOP를 사용
- Spring 컨텍스트(= Spring IoC Container) 접근 가능
- DispatcherServlet 내부에서 동작
REST API 요청 처리 흐름
클라이언트 요청
↓
[Filter] ← 서블릿 컨테이너 레벨
↓
DispatcherServlet ← Spring 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
- 인코딩 처리 Filter (ex.
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
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에서는 이미 인증된 사용자 정보를 기반으로 추가적인 비즈니스 로직을 수행할 수 있다.
주요 차이점 요약
| 구분 | Filter | Interceptor |
|---|---|---|
| 동작 레벨 | 서블릿 컨테이너 | Spring MVC |
| 실행 시점 | DispatcherServlet 이전/이후 | 컨트롤러 실행 전/후 |
| Spring Bean 접근 | 제한적 (수동 설정 필요) | 완전 지원 (생성자 주입) |
| 예외 처리 | 직접 처리 필요 | @ControllerAdvice 사용 가능 |
| 설정 방법 | @Component + @Order | WebMvcConfigurer |
| 적용 범위 | 모든 요청 (정적 리소스 포함) | Spring MVC 요청만 |
| 주요 용도 | 로깅, CORS, 인코딩, 보안 헤더 | API Key 검증, Rate Limiting, 성능 측정 |
| Handler 정보 접근 | 불가능 | 가능 (메서드, 애노테이션 등) |
| 응답 처리 | HTTP Response 직접 조작 | JSON 응답 처리 최적화 |