개발자로 후회없는 삶 살기

[문법] 스프링 예외처리 본문

[백엔드]/[spring | 학습기록]

[문법] 스프링 예외처리

몽이장쥰 2024. 8. 5. 16:23

서론

※ 과거에 기록한 내용에서 중요한 부분만 발췌하여 모두가 이해하기 쉽게 다시 서술한다.

 

본론

- 예외 처리와 오류 페이지

스프링에서 예외가 발생했을 때 어떻게 처리하는지 알아보자

 

- 서블릿 예외처리

1) 예외가 발생하여 was에게 예외가 날라감
2) resp.sendError 호출로 예외가 발생한 것처럼 가장

서블릿은 2가지 방법으로 예외를 지원한다.

 

1. exception

자바에서 메인 메서드를 호출하면 main이라는 스레드가 생성된다. 프로그램이 진행되며 예외가 발생하면 이를 잡으면 문제가 없지만, 못 잡으면 호출한 상위 스택까지 예외가 전달되고 메인 메서드 밖까지 예외가 전달되면 스레드가 종료된다.

 

웹 어플은 하나의 스레드가 있는게 아니라, 사용자 요청별로 별도의 스레드가 할당되고 그 스레드가 서블릿, 컨트롤러를 호출한다. (was가 서블릿 생성, 스레드가 서블릿 호출) 이를 앱에서 잡으면 문제가 없지만, 잡지 못하면 예외가 was까지 전파된다.

 

톰켓까지 예외가 올라오면 어떻게 될까?

 

예외가 발생하고 이게 was까지 전달되면 기본 에러 페이지를 보여준다. 예외가 발생하고 was까지 전달되면 무조건 500 에러로 판단한다.

 

2. sendError

sendError로도 예외를 발생시킬 수 있고, was까지 예외가 전달된다. 생태 코드에 따라 에러 페이지에 상태 코드가 표시된다.

 

1) 예외 : 무조건 500 오류
2) sendError : 상태와 메세지를 지정가능

어플리케이션에서 발생한 오류가 was까지 올라오면 예외 페이지를 보여준다.

 

- 오류 화면 제공

위처럼 기본 이미지로 예외를 표시하면, 서비스가 망했다고 생각할 것이다. 서블릿에서는 예외가 was까지 전달 되었을 때 각 상황을 처리하기 위한 오류 처리 기능을 제공한다.

 

ErrorPage는 발생한 에러와 에러 발생 시 호출되는 매핑 메서드를 매칭한다. 404 에러 발생 시 /error-page/404 매핑 메서드를 호출하도록 한다. 에러 발생 시 was까지 에러가 전달되며 was가 다시 매핑 메서드를 호출하는 2번 요청 구조이다.

 

→ 실행

컨트롤러에서 예외가 발생하면 서블릿까지 예외가 전달되고 등록된 ErrorPage에 맞게 매핑 메서드를 재호출해서 오류 페이지를 렌더링한다.

 

- 오류 페이지 작동 원리

실제 예외가 올라오든, sendError를 하든 서블릿까지 오류가 올라오면 등록한 ErrorPage를 찾는다.

 

그러면 was에서 다시 에러 페이지에 맞게 컨트롤러를 호출한다.

 

- 디스패쳐 타입

예외 처리 시 실제 요청과 was의 재 요청으로 필터가 2번 호출될 수 있다. 그런데 로그인 인증 체크라면 이미 완료된 체크를 또하는 것이 매우 비효율적이다. 따라서 클라이언트의 호출인지 was의 호출인지 구분하기 위해 디스패쳐 타입이라는 정보를 제공한다.

 

고객의 첫 요청은 REQUEST로 WAS의 요청은 ERROR이다. 디스패쳐 타입은 종류가 많다. 요청의 종류를 구분하는 것이다.

 

디스패쳐 타입을 출력해보자

 

필터를 등록할 때 위처럼 하면 REQUEST와 ERROR 호출을 받는 필터라는 의미이다.

 

→ 실행

첫 번째 요청은 REQUEST지만 에러 발생 후 요청은 ERROR이다.

 

- 인터셉터

인터셉터는 필터와 다르게 등록할 때 ErrorPage 매핑 메서드 경로를 화이트 리스트에 넣어서 2번 요청을 피해야 한다.

 

- 스프링 예외 처리

지금까지 예외 페이지 처리를 하기 위해 ErrorPage를 만들고 커스텀에 등록하고 예외 처리용 컨트롤러도 만들었다. 이를 스프링이 모두 자동화 했다.

 

ErrorPage를 자동으로 등록해준다. 이때 예외가 발생하거나 resp.sendErr를 하면 재 요청시 /error를 호출한다. 에러 재요청 시 호출되는 컨트롤러는 BasicErrorController로, 이 또한 자동으로 등록해주고 매핑 메서드도 내부에 다 만들어져있다.

 

ErrorPage에 에러와 호출되는 Url도 만들어놨고, 이를 등록도 했고, 호출되는 Url의 매핑 메서드도 구현을 해 놓았으니 개발자는 오류 페이지만 basic 컨트롤러가 제공하는 룰에 따라 등록하면 된다. (만약 규칙에 맞게 템플릿을 만들지 않는다면 기본 오류 페이지가 나온다.)

 

→ 템플릿

규칙에 맞게 템플릿에 error 폴더를 만들고 404 에러가 서블릿까지 올라와서 재 호출을 하면 404 뷰가 렌더링되고, 그 외는 4xx 뷰가 렌더링된다. 4xx 예외가 발생해서 서블릿까지 올라가면 Basic 컨트롤러의 매핑 메서드를 호출하고 해당 메서드가 템플릿의 4xx.html을 렌더링하도록 구현이 되어있다.

 

- API 예외 처리

복잡한 문제는 API 오류 처리이다. json 스펙을 정해서 어떤 형식으로 보낼 건지 정의하고 json을 만들어야 한다.

 

→ API 예외 처리는 어떻게 해야할까?

화면 예외는 예쁜 뷰만 보여주면 된다. 근데 API 예외는 서버간 예외를 호출할 수도 있고 기업간 시스템 통신도 할 수 있기에 각 오류 상황에 맞는 오류 응답 스펙을 API를 호출하는 곳과 서버와 약속을 해야하고, json으로 데이터를 내려줘야 한다.

 

예외를 발생했을 때 html이 나오면 안 된다. json으로 보내야 한다.

 

→ basic 컨트롤러에 새로운 매핑 메서드

/error로 호출되는 basic 컨트롤러 매핑 메서드에 produce를 json 방식으로 만들면 json 응답을 원하는 요청 시 이 메서드가 실행된다. json을 반환하기 위해 ResponseEntity를 사용하고 map에 상태와 message를 넣고 변환하면 json이 변환된다. 이렇게 basic 컨트롤러에 매핑 메서드를 변경하는 것은 복잡하니 스프링으로 자동화 해보자

 

- 스프링 부트 기본 오류 처리

API 방식도 basic 컨트롤러로 웹 어플에서 예외가 발생하면 /error로 호출하는 것을 제공한다. 똑같이 웹에서 오류가 발생하고 스프링이 ErrorPage 등록과 뷰 페이지 보여주는 컨트롤러 매핑 메서드를 작성하고 호출하는 것을 자동화한다.

 

accept를 html로 하면 500 html을 보여주고, json으로 하면 json을 반환하도록 basic에 다 개발이 되어있다.

 

basic 컨트롤러에 미디어 타입이 html이면 이 메서드가 호출되고

 

그 외는 ResponseEntity가 호출된다.

 

-> 설명

1) error html : accept가 html인 경우 호출
2) error : 그 외 경우 호출되고 json을 바디에 반환

accept가 html이면 뷰를 반환하고 json이면 json을 반환하도록 스프링이 다 해준다. was까지 예외가 올라가고 /error를 호출하는 것은 동일하며 basic에 매핑 메서드를 다르게 구현하여 처리하는 것을 스프링이 해두었다.

 

→ 정리

basic은 뷰를 반환할 때는 편리하다. 하지만 API 방식은 회원 관련, 상품 관련 응답이 다른 스타일 json일 수 있다. 따라서 api는 Exception Handler를 사용해야 한다.

 

- HandlerExceptionResolver

예외가 발생해서 was까지 갔다면 http 상태 코드는 500이 된다. (서버 내부 예외는 무조건 500) 근데 발생하는 예외에 따라서 400, 4xx 등 다른 상태 코드로 처리하고 싶다.

 

→ 상태 코드 변환

Illegal 예외 발생 시 WAS까지 전달되면 서버 내부 예외는 무조건 500 에러가 발생하는데 이를 400 에러로 처리하고 싶다.

 

postman으로 bad라고 보내면 당연히, 예외가 서버 내부, 컨트롤러 내부에서 터지면 was 입장에서는 무조건 500이다. 스프링은 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고 동작을 새로 정의할 수 있는 방법을 제공한다.

 

→ 적용 전

was에서 디스패쳐 서블릿을 호출하고 프리 호출하고 핸들러 호출하고 포스트 안되고 에프터 호출하고 was에게 예외가 전달되고 500으로 예외를 만들고, json으로 basic이 반환된다.

 

→ ExceptionResolver 적용 후

핸드러를 사용하면, 컨트롤러에서 예외가 발생하면 포스트 대신 ExceptionResolver가 예외를 해결할 수 있는 지 시도하고 정상 처리로 응답해버린다.

 

- 리졸버 인터페이스

예외를 파라미터로 받고, 이런 예외가 넘어왔으면 정상적인 모델 뷰로 반환한다. 컨트롤러에서 예외가 발생한 후 예외를 리졸버에 넣어주는데 만약 해결할 수 있는 예외라면 (instanceof) 400 오류로 500 에러를 덮어버린다. sendError가 예외를 받아서, 모델 뷰를 반환하여 정상 흐름으로 was까지 반환이 된다.

 

그러다 was까지 가면 400에러를 감지하고 /error를 다시 호출한다. was에서 다시 예외를 호출하면 basic 방식도 적용되는 것이다.

 

이 모델 뷰는 빈 모델 뷰로 렌더링을 하지 않는다.

 

→ 반환 값의 따른 동작 방식

1) 빈 모델 뷰 : 뷰 렌더링 없이 정상 응답
2) 모델 뷰 지정 : error/500 뷰 렌더링
3) null : 리졸버은 여러 개 등록할 수 있는 데 만약 처리할 수 있는 리졸버가 없으면 처음에 발생한 예외가 was로 간다.

 

- 리졸버 활용

1) 예외 상태 코드 변환 : sendError로 예외를 덮어버림
2) 뷰 템플릿 처리 : 모델 뷰를 채워서 새로운 오류 화면 랜더링
3) API 응답 처리 : HTTP 응답 바디에 직접 데이터를 넣으면 JSON으로 응답 가능

 

- HandlerExceptionResolver API 예외처리

예외 리졸버를 사용하지 않으면 Was까지 갔다가 다시 /error를 호출하는 복잡한 결과를 보인다. 이를 깔끔하게 리졸버로 끝내보자

 

리졸버 구현체에서 sendError를 하면 WAS에서 다시 에러를 호출한다. 이를 모델 뷰를 반환하면 리졸버에서 아예 에러를 해결하고 끝내버릴 수 있다. 또한 API 방식은 accept가 json이냐 html이냐에 따라서 다르게 동작하며, 이를 여기서 한번에 처리할 수 있다.

 

→ 결과

같은 호출인데 html이면 error/500이 resp에 담기고

 

json 방식이면, API 스펙이 반환된다. 근데 이를 직접 구현하는 것이 복잡해서 스프링이 기본 리졸버를 제공한다.

 

- 스프링 예외 리졸버

스프링이 리졸버를 3개 등록해준다. 예외 발생 시 3개를 순서대로 예외를 해결할 수 있는지 시도한다.

 

1. ResponseStatusExceptionResolver

1) @ResponseStatus가 붙은 예외 발생 시
2) ResponseStatus 예외 발생 시

위 2가지 경우 예외를 해결하려고 시도한다.

 

원래는 500 에러가 나야하는데 @ResponseStatus가 있으면 400에러로 자동으로 바꿔준다.

 

→ 실행

예외가 터지면 ResponseStatusExceptionResolver가 예외를 해결할 수 있나 시도하고, 400으로 바꾸고 sendError로 새로운 상태 코드로 호출하고 새로운 모델 뷰를 반환한다.

 

새로운 오류로 WAS까지 갔다가 basic의 /error를 호출해서 400 에러에 빈 모델 뷰를 반환한다.

 

accept를 html로 하면 basic의 모델 뷰를 반환하는 매핑 메서드를 400대로 호출한다. 예외를 sendError로 재호출하는 것을 반드시 알아야 한다.

 

2. DefaultHandlerExceptionResolver

스프링 내부 예외를 처리한다. 대표적으로 타입 에러 시 typemismatch 예외를 터트리고 was에서 500 에러가 나야하는데 스프링은 자동으로 400 오류로 처리한다.

 

Integer에 문자열을 넣으면 400 에러가 뜨는 이유가

 

DefaultHandlerExceptionResolver가 이미 해결해서 400대로 바꿔준다.

 

내부를 보면 거의 모든 예외가 들어있는데 스프링의 내부 예외를 다양하게 처리해준다.

 

- ControllerAdvice

위는 작성한 컨트롤러만 가능하다. 다른 컨트롤러에서도 공통으로 사용하려면 컨트롤 어드바이스를 쓴다. 여기에 @ExeptionHandler 메서드들을 가져온다. 이렇게 하면 예외를 호출하는 부분과 해결하는 부분을 구분하게 된다.

 

-> 모든 컨트롤러가 아닌 대상을 지정하는 방법

RestController에만 처리하고 싶으면 어노테이션을 붙인다.

'

하위 패키지를 지정하면 하위 컨트롤러에만 지정이 된다. 상품과 주문 컨트롤러는 다른 예외 처리를 해야할텐데 그때 컨트롤 어드바이스를 따로 만들고 각각 다른 코드를 작성한다.

 

특정 컨트롤러 하나만 지정하는 방법도 있다.

Comments