알아보기 좋은 예외 처리를 위해 Spring Boot에서 Custom Exception을 설정해보자!!
ErrorCode enum 생성 및 설정
springboot 에서 에러를 발생시켰을 때, 전달할 에러 코드를 관리하는 enum을 생성해준다!
enum(열거형)은 서로 연관된 상수들의 집합을 정의할 때 사용하는 특별한 자료형임!
=> 주로 몇 가지 값 중 하나만 선택해야 하는 경우에 사용됨(계절, 요일, 상태 등)
package berich.backend.exception;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
DUPLICATED_MEMBER(HttpStatus.BAD_REQUEST, "001_DUPLICATED_EMAIL", "이미 가입된 이메일입니다."),
INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "002_INVALID_ARGUMENT", "누락된 정보가 있는지 확인해주세요."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "003_USER_NOT_FOUND", "사용자를 찾을 수 없습니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "004_INVALID_PASSWORD", "비밀번호가 일치하지 않습니다.");
private final HttpStatus status;
private final String code;
private final String msg;
}
status: HTTP 상태 코드
code: 프론트로 전달되는 데이터로, 전달받은 코드를 통해 해당 에러를 받았을 때, 어떠한 조치를 취해야할 지 명시할 수 있음
msg: 전달 받은 에러 코드에 대한 설명으로, 해당 에러가 어떠한 상황에 발생한 에러인지 명시
Custom Exception 생성 및 설정
위에서 만든 ErrorCode는 열거형(enum)으로 정의되는데, 이 자체는 예외 클래스가 아님!
(Spring의 기본 예외 처리 메커니즘은 ErrorCode와 같은 커스텀 정보를 인식하거나 처리할 수 있도록 설계되어 있지 않음)
=> CustomException을 통해 ErrorCode와 함께 예외를 던지는 구조를 만들어야, 프로젝트 전반에서 통일된 예외 처리가 가능함
package berich.backend.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
ErrorCode errorCode;
}
RuntimeException을 상속받는 이유?
결론은 CustomException을 비검사 예외로 처리하기 위해서이다!
자바의 예외는 크게 2가지로 나뉘는데 비검사 예외(Unchecked Exception)와 검사 예외(Checked Exception)이다
RuntimeException을 상속받은 예외들은 비검사 예외로 간주되는데 이는 컴파일러가 예외 처리 여부를 강제하지 않으며, 개발자가 선택적으로 try-catch로 처리할 수 있다!
Exception을 상속받은 예외들은 검사 예외로 간주되는데 이는 반드시 예외 처리가 필요하며, 호출하는 쪽에서 try-catch로 처리하거나 throws 키워드로 명시해야 한다!
일반적인 로직 흐름에서 예외를 간단하게 처리하고 예외 처리에 대한 강제성을 줄이기 위해 RuntimeException을 상속받아 CustomException을 비검사 예외로 처리하는 것이다!
Custom handler 생성 및 설정
우선 예외 처리를해서 CustomException 발생 시, 프론트에게 반환할 에러 응답을 정의하는 ErrorDTO를 만든다
package berich.backend.exception;
import lombok.Builder;
import lombok.Data;
import org.springframework.http.ResponseEntity;
@Builder
@Data
public class ErrorDTO {
private int status;
private String code;
private String msg;
public static ResponseEntity<ErrorDTO> toResponseEntity(ErrorCode e) {
return ResponseEntity.status(e.getStatus().value())
.body(ErrorDTO.builder()
.status(e.getStatus().value())
.code(e.getCode())
.msg(e.getMsg())
.build());
}
}
=> 생성했던 ErrorCode에 대해 Builder 패턴을 이용
이제 Custom Handler를 만든다
Custom Handler를 통해 CustomException 생성 시, ErrorDTO에 구현된 데이터 구조에 맞춰 프론트로 데이터가 보내지게 됨!
package berich.backend.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice // 전역 예외 처리 클래스임을 명시
public class CustomExceptionHandler {
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorDTO> handleCustomException(CustomException e) {
return ErrorDTO.toResponseEntity(e.getErrorCode());
}
}
이 코드는 ErrorDTO 클래스의 toResponseEntity라는 메서드를 호출하여, 예외의 에러 코드를 기반으로 ResponseEntity 객체를 생성하고 반환하는 역할을 함
@ExceptionHandler는 Spring에서 특정 예외가 발생했을 때 그 예외를 처리할 메서드를 지정하기 위해 사용함
(@ExceptionHandler의 매개변수는 "어떤 예외를 처리할지"를 지정하기 위한 클래스 타입을 전달받기 때문에 .class 를 붙이지 않으면 그냥 타입으로만 인식하여 컴파일 에러 발생)
handleCustomException은 기본적으로 Spring에 의해 자동으로 호출되는 메서드이기 때문에, 외부 클래스나 클라이언트 코드에서 직접 호출될 필요가 없음
(public으로 선언할 필요가 X)
=> 상속을 고려한 확장 가능성, 같은 패키지 내에서의 접근 허용, 그리고 외부에서의 직접 호출을 방지하기 위해 protected로 선언
ControllerAdvice vs RestControllerAdvice
Spring은 전역적으로 예외를 처리할 수 어노테이션인 ControllerAdvice 와 RestControllerAdvice를 제공한다!
그럼 이 둘의 차이는 뭘까?
@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody의 결합이다
@RestControllerAdvice는 JSON 또는 XML로 응답을 반환함
=> RESTful API에서 주로 사용
@ControllerAdvice는 뷰 템플릿을 렌더링하는 일반적인 웹 애플리케이션에서 사용함
=> 필요할 때 @ResponseBody를 통해 JSON을 반환할 수 있음
Spring에서의 예외처리 흐름을 다시 생각해보면
1. 예외가 발생하면 Spring은 먼저 전역적으로 설정된 @ControllerAdvice 또는 @RestControllerAdvice 클래스에서 해당 예외를 처리할 @ExceptionHandler를 찾음
2. 일치하는 핸들러가 없다면, 개별 컨트롤러에서 @ExceptionHandler를 찾게 됨
3. 그래도 일치하는 핸들러가 없다면, Spring의 기본 예외 처리 로직이 적용되어 기본 오류 페이지나 JSON 응답이 반환됨
springConfig 설정
package berich.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
// Spring Security 웹 보안 기능 활성화 => FilterChain 제공
@EnableWebSecurity
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return webSecurity -> webSecurity.ignoring()
.requestMatchers("/error");
}
...
}
WebSecurityCustomizer는 보안 필터 체인 설정을 커스터마이즈하는 데 사용되는 인터페이스임
=> .ignoring을 통해 /error 처럼 특정 경로를 보안 필터 체인에서 제외함
WebSecurity는 HttpSecurity의 상위에 있음!
=> WebSecurity의 ignoring에 endpoint를 만들면, Security Filter Chain이 적용되지 X
HttpSecurity vs WebSecurity
Security Config를 작성하다 궁금증이 들었다
어쩌피 특정 url 경로를 허용해주는 작업이라면 SecurityFilterChain에서 HttpSecurit로 permitAll을 하면 되는거 아닌가?
우선 permitAll의 경우 Spring Security의 필터 체인은 그대로 작동하되, 특정 경로에 대해 모든 사용자가 접근할 수 있도록 허용하는 것이다!
=> 이 방식은 여전히 Spring Security의 다른 보안 기능들(CORS 설정, CSRF 보호 등)이 적용됨!
위에서 봤던 ignoring의 경우 지정한 경로는 아예 Spring Security의 관리 대상에서 벗어나기 때문에 보안 필터 체인을 거치지 X
=> CORS, CSRF 같은 모든 보안 검사가 적용되지 않을 뿐더러 인증, 권한 검사도 당연히 못함
그래서 결론은 WebSecurity(ignoring을 쓰는 경우)는 보안과 전혀 상관없는 API에 사용하고, 그 이외에는 HttpSecurity를 사용하는 것이 좋다.
회원가입 예외 처리 테스트
잘 작동하는 것을 확인할 수 있음!
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] Docker를 이용해 EC2에 배포해보기 (4) | 2024.08.27 |
---|---|
[Spring Boot] MySQL JPA 연동 (3) | 2024.07.22 |