예외 처리란?
먼저 "예외 처리"는 프로그램 실행 중에 문제가 생겼을 때(예: 로그인 실패, 없는 사용자 요청 등), 그 문제를 적절하게 처리해서 사용자에게 이해할 수 있는 응답을 보내는 걸 말한다.
Spring MVC의 예외 처리
Spring MVC에서는 컨트롤러에서 예외가 터졌을 때 자동으로 예쁘게 처리할 수 있게 도와주는 여러 기능을 제공하는데, 대표적으로
- @ExceptionHandler
- @ResponseStatus
- @ControllerAdvice
1. @ExceptionHandler
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<ErrorResponse> handleMyException(MyCustomException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("CODE", ex.getMessage()));
}
- 특정 예외가 발생하면 이 메서드가 호출
- 직접 상태 코드, 응답 메시지 등을 설정 가능
- 컨트롤러 클래스 안에 만들거나, 전체 공통 처리를 위해 @ControllerAdvice와 같이 사용
2. @ResponseStatus
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
- 예외 클래스 위에 붙여서 이 예외가 발생하면 어떤 HTTP 상태코드를 보낼지 지정 가능.
- 예를 들어, NotFoundException이 발생하면 자동으로 404 상태코드가 응답된다.
- @ResponseStatus는 단순하고 간편하지만, 응답 body를 꾸밀 수 없다.
3. @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<ErrorResponse> handleCustom(MyCustomException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("CODE", ex.getMessage()));
}
}
- 프로젝트 전체(모든 컨트롤러)에서 발생하는 예외를 한 곳에서 처리 가능
예외 처리 흐름 요약
- 예외 발생 (throw new SomeException())
- → 해당 예외를 처리할 @ExceptionHandler 또는 @ResponseStatus 있는지 찾음
- → 있으면 해당 핸들러 실행
- → 없으면 Spring이 기본 500 에러 응답
Spring Security의 예외 처리
Spring Security는 요청이 DispatcherServlet까지 도달하기 전에 먼저 필터 체인에서 요청을 가로채서 인증(authentication) 또는 인가(authorization) 를 검사한다.
이 단계에서 예외가 발생하면, Spring MVC의 컨트롤러에 도달하지 않았기 때문에, @ExceptionHandler나 @ControllerAdvice가 예외를 잡을 수 없다.
그래서 Spring Security는 자체적으로 예외 핸들러를 제공하는데, 그게 바로 아래 두 가지
- AuthenticationEntryPoint
- AccessDeniedHandler
1. AuthenticationEntryPoint
👉 인증(로그인)되지 않은 사용자가 보호된 URL에 접근하려 할 때 동작
예: JWT 토큰이 없거나 유효하지 않은 경우
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.setContentType("application/json");
response.getWriter().write("{\\"code\\":\\"AUTH_REQUIRED\\",\\"message\\":\\"로그인이 필요합니다.\\"}");
}
}
2. AccessDeniedHandler
👉 인증은 됐지만, 해당 리소스에 접근 권한이 없는 경우 동작
예: 일반 유저가 관리자 페이지에 접근 시도
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
response.setContentType("application/json");
response.getWriter().write("{\\"code\\":\\"ACCESS_DENIED\\",\\"message\\":\\"접근 권한이 없습니다.\\"}");
}
}
이 두 개를 Security 설정에 등록해야 적용된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
)
.build();
}
}
Spring Security 주요 필터 7선
| 필터 이름 | 이름 | 실무 사용 여부 |
| 1. SecurityContextPersistenceFilter | 이전 요청에서 저장된 인증 정보를 SecurityContext에 복원 | ✅ 항상 사용됨 |
| 2. UsernamePasswordAuthenticationFilter | username과 password로 로그인 처리 | ❌ JWT 쓰면 사용 안 함 |
| 3. BasicAuthenticationFilter | HTTP Basic 인증 (Authorization: Basic ...) 처리 | ❌ API에서 거의 안 씀 |
| 4. BearerTokenAuthenticationFilter | Spring에서 제공하는 Bearer Token 인증 필터 | ⚠️ Spring Security 6+에서 가능 |
| 5. ExceptionTranslationFilter | 예외 발생 시, 적절한 응답으로 바꿔줌 (401, 403) | ✅ 중요 |
| 6. FilterSecurityInterceptor | 최종적으로 권한 체크를 수행 (인가/Authorization) | ✅ 항상 사용됨 |
| 7. CorsFilter | CORS(Cross-Origin Resource Sharing) 설정 처리 | ✅ 프론트엔드 연동 시 필수 |
1️⃣ SecurityContextPersistenceFilter
- 매 요청마다 인증 정보를 담고 있는 SecurityContext를 세션에서 꺼내와 저장해 줌
- JWT 기반에서는 stateless 처리를 위해 SessionCreationPolicy.STATELESS 설정하고 사용 안 하게 되기도 함
2️⃣ UsernamePasswordAuthenticationFilter
- 사용자가 /login으로 POST할 때 아이디/비번을 읽어 인증 처리
- JWT 쓰면 거의 비활성화 (formLogin().disable())
3️⃣ BasicAuthenticationFilter
- Authorization: Basic abcdef== 형태의 요청 헤더를 해석
- 간단한 인증엔 쓰이지만, 실무 API에선 거의 안 씀
4️⃣ ExceptionTranslationFilter
- 인증/인가 도중 예외가 발생하면, 여기서 적절한 예외 응답으로 변환
- 예: AuthenticationEntryPoint, AccessDeniedHandler 호출
- 실질적으로 "401/403 응답을 반환하는 역할"을 함
- FilterSecurityInterceptor에서 발생한 예외는 ExceptionTranslationFilter가 감싸고 있다가 응답으로 바꿔서 보냄
5️⃣ FilterSecurityInterceptor
- 마지막 단계에서 접근 제어(인가)를 실제로 수행
- @PreAuthorize, hasRole(), hasAuthority() 등이 여기서 처리됨
6️⃣ CorsFilter
- 다른 도메인(예: localhost:3000 → localhost:8080)에서 요청이 오면 허용할지 검사
- 프론트엔드와 통신하는 백엔드라면 필수
요청 처리 흐름 예시 (JWT 기반)
[요청 도착]
↓
CorsFilter
↓
SecurityContextPersistenceFilter
↓
JwtAuthenticationFilter (addFilterBefore로 삽입)
↓
ExceptionTranslationFilter
↓
FilterSecurityInterceptor
↓
Controller 도달 (인증 완료된 상태)
끝.
'Study > SpringBoot' 카테고리의 다른 글
| Spring에서 Bean 생명주기 개념 정리 (0) | 2025.05.17 |
|---|---|
| JPA의 flush()는 언제, 왜 호출될까? (0) | 2025.04.12 |
| Spring Boot 테스트 코드: 통합 테스트 vs 단위 테스트 (0) | 2025.03.27 |
| SpringBoot + JPA: N+1 문제 발생 원인과 해결 방법 (0) | 2025.03.22 |
| Spring Been 객체와 프록시 패턴 (0) | 2025.03.10 |