Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch1
- 2024 정보처리기사 수제비 실기
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 로그인 처리
- 스프링 입문(무료)
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 검증
- 코드로 시작하는 자바 첫걸음
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch3
- 스프링 db2 - 데이터 접근 기술
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch2
- 2024 정보처리기사 시나공 필기
- jpa 활용2 - api 개발 고급
- 게시글 목록 api
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 타임리프
- @Aspect
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch11
Archives
- Today
- Total
나구리의 개발공부기록
API 예외처리 - 시작, 스프링 부트 기본 오류 처리, HandlerExceptionResolver 시작 및 활용 본문
인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술
API 예외처리 - 시작, 스프링 부트 기본 오류 처리, HandlerExceptionResolver 시작 및 활용
소소한나구리 2024. 9. 8. 15:48 출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://inf.run/GMo43
1. API 예외처리 - 시작
- HTML 페이지의 경우 오류 페이지만 있으면 대부분의 문제를 해결할 수 있음
- API의 경우에는 고객에게 보여주는 오류 화면이 아니고 상황에 맞는 요구 스펙을 서로 정의하는 것처럼, 오류도 글로벌한 스펙이 있는 것이 아니기 때문에 클라이언트와 서버가 각 오류 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 데이터를 내려 주어야 함
1) 서블릿 오류 페이지 방식을 활용한 API 오류 처리
(1) WebServerCustomizer 다시 동작
- 예외 처리와 오류 페이지 강의에서 작성해 두었던 WebServerSustomizer 클래스의 @Component 애노테이션을 다시 주석을 해제 시켜서 동작하도록 변경
(2) ApiExceptionController 추가
- 회원을 조회하는 간단한 컨트롤러, id의 값이 ex이면 예외가 발생함
package hello.exception.api;
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
// id == ex 일때 RuntimeException 발생
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello" + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
- 이렇게 만들어두고 postman으로 localhost:8080/api/members/ex에 get요청을 보내면 (HTTP Header에 Accept가 allication/json 으로 보내야함) 기존에 만들어 둔 에러페이지가 보여짐
- API를 요청했으니 정상적일 경우 API로 JSON 형식으로 데이터가 반환되는데, 에러페이지는 HTML로 반환되는 것은 기대하는 바가 아니며 웹 브라우저가 아닌이상 HTML을 직접 받아서 할 수 있는 것은 별로 없음
- 이를 해결하려면 컨트롤러도 JSON 응답을 할 수 있도록 수정해주어야 함
(3) ErrorPageController에 errorPage500Api() 추가
- @RequestMapping()의 옵션에 produces를 MediaType.APPLICATION_JSON_VALUE로 지정하여 JSON으로 응답 타입을 지정
- 해당 메서드를 생성할 때 기존의 RequestMapping("/error-page-/500)이 달린 메서드를 지우지 않고 추가로 작성했는데, 동일한 경로로 요청이 되어도 자세한것을 우선순위로 선정하기 때문에 새로만든 errorPage500Api()가 먼저 호출됨
// produces, 클라이언트가 보내는 Accept 타입에 따라 응답 타입을 지정해줌
@RequestMapping(value="/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) {
log.info("API errorPage 500");
// JSON 변환을 위한 HashMap -> result 값 저장하면 됨
Map<String, Object> result = new HashMap<>();
// 예외를 변수에 저장
Exception ex = (Exception)httpServletRequest.getAttribute(ERROR_EXCEPTION);
result.put("status", httpServletRequest.getAttribute(ERROR_STATUS_CODE)); // 에러 상태 코드
result.put("message", ex.getMessage()); // 예외 메세지
Integer statusCode = (Integer)httpServletRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);// 오류 상태 코드
// ResponseEntity = response body에 바로 데이터를 집어 넣음
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
- ErrorPageController에 해당 컨트롤러를 추가하면 정상적으로 오류정보가 JSON으로 응답됨
- HashMap은 순서를 보장하지 않아서 출력 순서는 바뀔 수 있음(result에 put으로 값을 저장 시)
- HttpStatus.valueOf(statusCode): 우측 상단에 빨간색으로 보여지는 500 Internal Server Error로 표시된 내용
2. API 예외처리 - 스프링 부트 기본 오류 처리
- API 예외 처리도 스프링 부트가 제공하는 BasicErrorController를 사용하여 처리할 수 있음
- 실습을 위해 WebServerCustomizer의 @Component를 주석처리하여 서블릿을 통한 예외처리를 제거하도록 설정
1) postman으로 요청
- Header - Accept : text/html : 기존에 작성했던 오류 페이지가 나옴
- Header - Accept : application/json : json으로 오류가 응답 됨
2) BasicErrorController
- 스프링 부트 3.3.3 기준 교안과 BasicErrorController 구현 코드가 다르게 정의되어있으나 동작은 동일하게 함
- 자세한 내용은 BasicErrorController에 들어가서 확인해보기 -> 기존에 오류 컨트롤러와 구성 방식이 비슷함
- /error 경로를 처리하는 메서드가 2개가 정의되어 있음
(1) errorHtml() 메서드
- 요청 Accept 헤더 값이 text/html인 경우 뷰를 반환
(2) error()
- 그외의 요청에 호출되며 ResponseEntity로 HTTP Body에 JSON 데이터를 반환함
@Controller
// 에러의 기본 경로가 /error로 정의 되어있음
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// 기타 메서드들 정의
// MediaType이 text/html인경우 동작하는 메서드가 정의 되어있음, ModelAndView로 반환
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
// 동작코드 정의
}
// text/html이 아닌 경우에는 ResponseEntity로 반환 되도록 메서드가 정의 되어있음
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 동작 코드 정의
}
}
- 마찬가지로 옵션을 조정하여 다양한 오류 정보를 추가할 수 있지만, 보안상 위험할 수 있으니 간결한 메시지만 노출하고 로그를 통해서 확인
3) Html 페이지 오류 vs API 오류
- BasicErrorController를 확장하면 JSON 메시지를 변경할 수 있는데, @ExceptionHandler가 제공하는 기능을 사용하는 것이 더나은 방법이므로 확장에서 JSON 오류 메시지를 변경할 수 있다는 것 정도로만 이해하면 됨
- BasicErrorController는 HTML 페이지를 제공하는 경우 매우 편리한데 API오류 처리는 다른 차원의 문제임
- 클라이언트와 서버가 API마다 예외가 발생했을 때 정의한 JSON 스타일이 각각 다르기 때문에 응답 결과를 다르게 출력해야 할 수 있어 세밀하고 복잡하기 때문에 BasicErrorController는 HTML 화면을 처리할 때 사용하고 API 오류는 뒤에서 설명할 @ExceptionHandler를 사용하는 것이 좋음
3. API 예외 처리 - HandlerExceptionResolver 시작
- 예외가 발생해서 서블릿을 넘어 WAS 까지 예외가 절달되면 HTTP 상태코드가 500으로 처리되는데 발생하는 예외에 따라서 400, 404 등등 다른 상태코드로 처리할 수 있음
1) ApiExceptionController - 수정
- id 값을 bad로 요청했을 때 IllegalArgumentException이 발생하도록 설정하고 요청을 보내보면 500에러를 반환됨
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) { // id == ex 일때 RuntimeException 발생
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) { // id == bad 일때 IllegalArgumentException 발생
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "hello" + id);
}
2) HandlerExceptionResolver
- HandlerExceptionResolver를 통해 예외를 해결하고 동작을 새로 정의할 수 있음
- 줄여서 ExceptionResolver라고 함
- ExceptionResolver를 적용하면 컨트롤러에서 예외가 발생 되었을 때 적용전과 마찬가지로 postHandler는 호출이 안되지만, 대신 ExceptionResolver가 호출되어 예외를 해결하도록 시도하고, 예외를 해결하면 이후의 호출들을 정상 응답으로 처리할 수 있음
(1) HandlerExceptionResolver 인터페이스 구조
- handler: 핸들러(컨트롤러)정보
- Exception ex: 핸들러(컨트롤러)에서 발생한 예외
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, @Nullable Object handler, Exception ex);
}
(2) MyHandlerExceptionResolver - ExceptionResolver를 구현한 리졸버
- ExceptionResolver가 ModelAndView를 반환하는 이유는 try-catch를 하듯이 Exception을 처리해서 정상 흐름 처럼 변경하는 것이 목적임
- 여기서는 IllegalArgumentException이 발생하면 response.sendError(400)을 호출해서 HTTP 상태 코드를 400으로 지정하고 빈 ModelAndView를 반환함
- 빈 ModelAndView 반환: 뷰를 렌더링 하지 않고 정상 흐름으로 서블릿이 리턴됨
- ModelAndView 지정 반환: 지정한 정보를 가지고 뷰를 렌더링해서 반환함
- null 반환: 다은 ExceptionResolver를 찾아서 실행하고, 만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 되지 않고 기존에 발생한 예외(500)을 서블릿 밖으로 던짐
package hello.exception.resolver
import java.io.IOException;
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
// IllegalArgumentException 이면 400 오류로 반환하는 로직
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
/*
IllegalArgumentException 에러가 오면 try-catch, if 에서 정상 로직으로 처리가 됨
response.sendError()로 새로운 SC_BAD_REQUEST 와, 발생한 예외 메세지를 던짐
그러면 기존 발생한 예외는 무시되고, 서블릿 컨테이너는 새로 발생시킨 BAD_REQUEST 에러로 인식하게 됨
*/
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView(); // 새로운 ModelAndView()를 빈값으로 반환 - 정상 응답처리
}
} catch (IOException e) {
log.info("resolver ex", e);
}
return null;
}
}
(3) ExceptionResolver활용
- 예외 상태 코드 변환: 발생한 예외를 response.sendError()호출로 변경해 입력한 상태 코드에 따라 서블릿에서 오류를 처리하도록 위임하고 WAS는 서블릿 오류 페이지를 찾아서 내부 호출함(ex - 스프링 부트가 기본으로 설정한 /error)
- 뷰 템플릿 처리: ModelAndView에 값을 채우면 예외에 따른 새로운 오류 화면을 뷰 렌더링 하여 고객에게 제공
- API 응답 처리: response.getWriter().println("hello") 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하며 { "xx" : "yy"} 처럼 JSON으로 응답하면 API 응답 처리를 할 수 있음
3) WebConfig - 수정(작성한 ExceptionResolver 등록)
- extendHandlerExceptionResolvers()로 등록해야만 스프링이 기본으로 등록하는 ExceptionResolver를 사용할 수 있음
- configureHandlerExceptionResolver(...) 사용 X
@Configuration
public class WebConfig implements WebMvcConfigurer {
// ... 기타 등록된 설정들
// 작성한 핸들러 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
(4) 실행 결과
- HandlerExceptionResolver를 사용 후에 id=bad로 요청을 보내보면 500에러가 지정한 400 에러로 변경 된 것을 알 수있음
- (500 에러 -> 서버 잘못, 400 -> 클라이언트 잘못) : 상세한 내용은 HTTP 웹 기본 지식 강의 내용 확인
4. API 예외 처리 - HandlerExceptionResolver 활용
- 예외가 발생하면 WAS까지 예외가 던져지고 WAS에서 오류 페이지 정보를 찾아서 다시 /error를 호출하는 복잡하고 중복 호출되는 처리 과정을 ExceptionResolver를 활용하면 복잡한 과정없이 HandlerExceptionResolver에서 에러 처리를 끝내버릴 수 있음(WAS까지 가지 않음)
1) UserException 작성 - 사용자 정의 예외
- RuntimeException을 상속 받아서 구현
package hello.exception.exception;
public class UserException extends RuntimeException {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
2) ApiExceptionController 예외 추가
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
// ... 기존 오류들
if (id.equals("user-ex")) { // id == user-ex 일때 사용자 정의 오류 발생
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello" + id);
}
}
3) UserHandlerExceptionResolver
- 사용자 정의 예외를 처리하는 리졸버
- HTTP 요청 헤더의 ACCEPT 값이 application/json이면 JSON으로 오류를 리턴하고, 그 외의 경우에는 error/500에 있는 HTML 오류 페이지를 리턴
package hello.exception.resolver;
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
// JSON으로 만들기 위한 ObjectMapper
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
// HTTP 헤더의 accept 가 text/html, application/json 일때 각각 처리 되도록 구현
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 상태코드 400 반환
if ("application/json".equals(acceptHeader)) { // json 일때
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);// 에러 객체를 String으로 변환
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(result);
return new ModelAndView(); // 빈 ModelAndView 를 반환하여 정상 흐름을 반환
} else { // 그 예외 케이스들(text/html)
return new ModelAndView("error/500"); // 기존에 작성한 error/500 페이지를 반환
}
}
} catch(IOException e) {
log.info("resolver ex", e);
}
return null;
}
}
4) WebConfig에 작성한 UserHandlerExceptionResolver 추가
// 작성한 핸들러 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver()); // 추가
}
5) 실행
(1) Accept : application/json
- json 타입으로 사용자오류가 정상적으로 처리됨
(2) Accept : text/html
- 기존에 등록했던 error/500의 에러 페이지 html이 출력 됨
(3) 정리
- ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해버려서 서블릿 컨테이너까지 예외가 전달되지 않고 스프링 MVC에서 예외 처리가 끝이남 -> WAS 입장에서는 정상 처리가 되고 이곳에서 모두 예외를 처리할 수 있다는 것이 핵심
- 직접 ExceptionResolver를 구현하는 것은 복잡한데, 이것을 스프링이 제공하는 ExceptionResolver를 활용하면 편리하게 API 예외를 처리할 수 있음