어드민 페이지와 api에 session을 이용한 로그인 처리, Valid를 이용한 검증을 구현했고,
이젠 api와 어드민 모두 예외처리를 구현하려고 한다.
@RestController
@RequiredArgsConstructor
public class LoginController{
private final UserService userService;
@PostMapping("/login")
public ResponseEntity<LoginResponse> loginUser(@Valid @RequestBody LoginRequest request,
HttpSession session){
User loginUser = userService.signIn(request.getAccountId(), request.getPassword());
session.setAttribute(SessionConst.LOGIN_USER, loginUser);
LoginResponse response = new LoginResponse(loginUser);
return ResponseEntity.ok(response);
}
}
/// UserService
public User signIn(String accountId, String password){
return userRepository.findByAccountId(accountId).filter(u -> u.getPassword().equals(password))
.orElseThrow(NoSuchUserException::new);
}
api는 로그인 과정에서 @Valid를 이용해 입력 값을 검증한다.
이때 id나 비밀번호에 공백이 들어오는 등 검증 오류가 발생한 경우, MethodArgumentNotValidException을 발생시킨다.
그리고 유저 로그인 중에 일치하는 유저가 없을 경우, 직접 만든 NoSuchUserException을 발생시킨다.
이러한 예외 처리를 왜 하는 것이며, 어떻게 진행되고,
어떻게 커스터마이징 하는 것이 좋은지 공부하며 예외처리를 구현했다.
예외 처리와 오류 페이지 - 서블릿 예외처리
순수 서블릿은 예외처리를 어떻게 할까?
- Exception : 종료시 throws 있으면 상위 메소드로 계속 전달됨
- 자바 메인 메소드 실행 - main 쓰레드 실행됨
- 실행 도중 예외 잡지 못하고 main을 넘어서 예외가 던저짐?
- 예외 정보를 남기고 해당 쓰레드 종료
- 웹 어플리케이션 → 하나의 쓰레드 x, 사용자 요청별로 서블릿 컨테이너가 쓰레드 할당
- try catch 없이 서블릿 밖으로 예외 던져지면?
- 톰캣같은 WAS 서버까지 예외가 전달됨
- 자바 메인 메소드 실행 - main 쓰레드 실행됨
- response.sendError : 호출한다고 에러 발생 x, 서블릿 컨테이너에 오류 발생 전달
- http 상태 코드와 오류 메세지도 전달 가능
- 컨트롤러에서 sendError → 인터셉터, 서블릿, 필터 → WAS로 예외 없이 전달
- 서블릿 컨테이너에선 응답 전에 response에 sendError 호출 확인
- sendError로 설정한 오류 코드에 맞춰 오류 페이지 보여줌
서블릿 예외 처리 - 오류 화면 작동 원리
- WAS는 Exception이 전달되거나 sendError 호출 기록이 있으면 오류 페이지 정보를 확인한다.
- ErrorPage(RuntimeException.class “/error-page/500”); 처럼 오류 페이지가 존재하면 “/error-page/500”으로 다시 요청을 보냄(HTTP 요청이 아닌 내부적인 호출)
- 이때 필터, 서블릿, 인터셉터, 컨트롤러가 전부 다 다시 호출됨
- 웹 브라우저는 이 과정을 모름 → 오직 서버 내부에서만 추가적인 호출을 함
- 또 단순한 요청이 아닌 request attribute에 오류 정보를 추가해서 넘겨줌 (exceptionType, error_message, request_uri, servletname, 등등)
- 이때 에러 경로만 처리하는 컨트롤러를 내부에 만들고, ModelAndView를 반환하게 하면 상황에 따른 오류 페이지를 제공할 수 있음
스프링 부트 오류 페이지
- 스프링 부트는 /error를 기본 경로로 기본 오류 페이지 설정
- 서블릿 밖으로 에러 던져짐 or respose.sendError 시에 기본 오류 페이지 호출
- 스프링 부트는 BasicErrorController를 자동으로 등록함 → /error 매핑해서 처리하는 컨트롤러
- 개발자는 오류 페이지만 등록하면 됨 (처리 순서)
- 뷰 템플릿 (/resources/templates/error/500.html)
- 정적 리소스(/resources/static/error/500.html)
- 적용 대상 없을 시 기본 뷰 (/resources/templates/error.html)
- 4xx.html 식으로 통합해서도 사용 가능 (구체적인게 더 우선순위 높음)
- BasicErrorContorller는 다음과 같은 정보 기본 제공
- timestamp : 시간
- status : 상태 코드 400,500
- error : 어떤 에러인지
- exception : 어떤 예외 클래스인지
- trace : 예외 trace? - 어디서 발생했는지 경로 쭉 훑음
- message : 오류 메세지
- errors : ?
- path : 클라이언트 요청 경로
- 근데 고객이 오류 정보를 아는 것은 보안상 안좋음
- application.properties에서 오류 정보에 포함할지 안할지 선택 가능
- 사용자는 간단한 오류 페이지만, 오류 로그는 서버에 남기자
API 예외 처리
api 예외는 어떻게 처리해야할까?
api는 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 보내야 한다.
그러나 아무런 설정을 하지 않은 상태에서 오류가 발생하면 body에 오류 페이지 html이 전송된다.
오류 페이지 컨트롤러도 json을 보내도록 수정해야한다.
HandlerExceptionResolver
- 스프링 mvc는 핸들러 밖으로 예외 던져지면 예외 해결 & 동작 지원
- 동작 방식 변경을 원함 (런타임 에러에서 500이 아닌 404 Http Status를 제공하고 싶다.)
- 컨트롤러에서 예외 있으면 인터셉터의 posthandle이 호출안됨
- 그 자리에서 ExceptionResolver가 호출, 예외 해결 시도
- 해결하면 정상 응답 나감
스프링이 제공하는 exceptionResolver
- ExceptionHandlerExceptionResolver : @ExceptionHandler 처리
- ResponseStatusExceptionResolver : Http 상태 코드 지정 @ResponseStatus
- DefaultHandlerExceptionResolver : 스프링 내부 기본 예외 처리
@ExceptionHandler
- 컨트롤러에 메소드에 @ExceptionHandler 어노테이션 달고 에러 파라미터로 받기
- 컨트롤러 안에서 해당 예외 터질시 Resolver 호출
- 에러를 정상 흐름으로 바꿔서 전송해줌
- @ResponseStatus 컨트롤러 메소드에 붙이면 상태 코드도 바꾸기 가능
- 이러면 예외시에도 서블릿 컨테이너 다시 호출 안함, 정상 흐름으로 리턴
- 그냥 Exception을 처리하도록 하면 → 마지막까지 처리 안된 나머지 예외 공통 처리
- 지정한 예외와 그 자식클래스까지 잡을 수 있음
- 스프링 컨트롤러처럼 여러 파라미터 넣어서 사용 가능
@ControllerAdvice
@Slf4j
@RestControllerAdvice
public class ControllerAdvice{
@ExceptionHandler(NoSuchUserException.class)
public ResponseEntity<ErrorResponse> invalidUserHandler(NoSuchUserException e){
log.info("NoSuchUserException");
ErrorResponse response = new ErrorResponse("404", e.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class) // spring Valid
public ResponseEntity<ErrorResponse> ValidateErrorHandler(MethodArgumentNotValidException e){
//에러가 여러개면 첫번째 에러만 반환하도록
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
ErrorResponse response = new ErrorResponse("404", message);
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
}
exceptionHandler를 컨트롤러에 사용하면 정상 코드, 예외 코드 처리가 하나의 컨트롤러에 들어가 있음
@RestControllerAdvice를 사용해서 분리하면 됨
대상 지정 안하면 모든 컨트롤러에 적용됨
'Spring > Spring MVC' 카테고리의 다른 글
사지방에서 Spring 공부하기 Web MVC #10 - 필터와 인터셉터 (0) | 2023.08.27 |
---|---|
사지방에서 Spring 공부하기 Web MVC #9 - 로그인 처리 (쿠키, 세션) (0) | 2023.08.26 |
사지방에서 Spring 공부하기 Web MVC #8 - Bean Validation (0) | 2023.08.26 |
사지방에서 Spring 공부하기 Web MVC #7 - Validation(검증) (0) | 2023.08.20 |
사지방에서 Spring 공부하기 Web MVC #6 - 메세지 국제화 (5) | 2023.08.19 |