Inflearn
스프링 MVC 2편 (3)
예외처리와 오류 페이지
- 서블릿은 Exception과 response의 sendError 이 두 방식의 예외를 처리함
- Exception
- 자바 메인 메서드 실행할땐, 메인 메소드 하나만 쓰레드에 담겨 실행되고 실행 도중 예외가 발생하면(잡지못한) 예외 정보를 남기고 해당 스레드가 종료 된다.
- 웹 애플리케이션의 경우 요청별로 쓰레드가 할당되고 서블릿 컨테이너 안에서 실행된다. 만약 이 상황에서 예외를 잡지 못한다면 어떻게 되냐?
- 컨트롤러(예외 발생)→ 인터셉터→ 서블릿 → 필터 → WAS 까지 전파가 된다.
- 즉, 그냥 WAS가 500이나 404같은 기본 문구를 보여주게 됨
- sendError
- 이걸 호출한다고 에러가 바로 발생하는건 아니고, 서블릿 컨테이너한테 오류가 발생했다는 것을 전달할 수 있게 됨
- 그리고, Http상태코드. 메세지를 추가할 수 있다.
- 컨트롤러(예외발생)→인터셉터→서블릿→필터→WAS (sendError호출기록 확인 후 존재하면 설정한 오류 코드에 맞춰 기본 오류 페이지를 보여줌)
- 이런 예외 처리 화면은 보기 힘들다. (뭐 api역할만 하는 서버면 상관없다만..)
- WebServerFactoryCustomizer 얘를 implements 하고 오류들에 대해 처리 주소를 작성해주면 해당 오류가 발생하면 해당 주소로 다시 호출을 한다.
- 즉, 다시 WAS→필터→인터셉터→서블릿→컨트롤러 이렇게 쭉 호출이 진행된다.
- WAS는 예외를 감지하게되면 (exception이든 sendError로든) 오류 페이지 정보가 있는 지 확인하고 있다면 request에 오류 정보를 넣어서 다시 컨트롤러까지 쭉 요청을 보냄
- 그런데, 오류발생으로 오류 페이지를 호출하기위해 다시 한번씩 필터, 인터셉터를 더 호출하는 것은 상당히 비효율적이다.
- 즉, 클라이언트가 보낸 정상 요청인지 오류 페이지 출력을 위한 내부 요청인지 구분해야 된다.
- Servlet은 이걸 해결하기 위해 DispatcherType이란 추가 정보를 제공한다.
- 고객이 처음 요청하는 경우 dispatcherType은 REQUEST지만 에러인 경우 ERROR로 나오게 된다.
- 이 외에도, FORWARD,INCLUDE,ASYNC가 있다.
- 이렇게 필터 빈을 등록할때 반응할 DispatcherType을 지정할 수 있다.
- 인터셉터는 어떻게 처리할까 ?
- 내부적으로 DispatcherType을 받지 못하기에 필터처럼 처리할 순 없는데
- 대신, exclude패턴에 오류페이지 경로를 추가하면 에러에 반응하지 않게 됨
- 여기서 스프링 부트는 이렇게 오류페이지 생성하고 에러 컨트롤러를 자동으로 만들어서 처리한다.
- 그렇기에, 우리는 해당 컨트롤러가 처리하는 페이지만 정해진 순서에 맞게 작성해주면 된다.
- templates/error/404.html(4xx.html …)이런 뷰 템플릿이 1순위(물론 같은 급이여도 더 자세한게 더 빠른 순위)
- static/error/ .html 정적 리소스가 2순위
- 다 없으면 templates/error.html 가 마지막순위
- 또, BasicErrorController는 여러 정보들을 모델에 담아 뷰에 전달할 수 있음
- 근데 이걸 전부 다 노출시키기엔 고객에게 어지럽게됨, 그리고 보안적으로도 좋지 않다.
- 그래서, application.properties에 보여줄건지 말건지 설정을 할 수 있음
- always설정하면 항상 보이게끔 하지만 이건 올바르지 않다.
- 그래서 on-param으로 해서 개발시 디버깅을 용이하게 할 수는 있지만 실무에선 앵간해서 절대 노출하지 않는게 좋다.
- server.error.whitelabel.enabled=true
- 오류화면을 못찾을 때 whitelabel오류 페이지를 적용할 것인지 설정
- 기본값이 true다
API예외 처리
- (몰랐던 부분만 간단하게 하이라이팅 하면서 정리)
- API는 오류 응답 스펙을 정하고 JSON으로 데이터를 내려줘야 됨
- 별 처리를 안하면 오류시 오류 페이지를 반환하게 됨 (즉, html을 반환한다.)
- 동일한 매핑에서도 produces = MediaType.APPLICATION_JSON_VALUE
- 이렇게 produces를 넣어주면 요청헤더에서 json을 원하는 애가 있으면 이렇게 JSON을 만든다고 적어준 놈이 해당 요청을 받게 된다.
- 물론 이렇게 json으로 받는다고 매핑해놓으면 BasicErrorController에서 또 알아서 json형태로 오류 메세지를 돌려준다.
- 물론 이 basicErrorController를 확장해서 메세지를 변경하고 추가하거나 할 수 있지만 너무 번거롭고 굳이 할 필요도 없다.
- 예외가 서블릿을 넘어 WAS까지 가버리면 상태코드가 500으로 처리되는데, 발생 예외에 따라 400,404등등 다른 상태코드로 처리하고 싶다..
- HandlerExceptionResolver
- 원래는 WAS까지 예외가 쭉 전달되었는데
- 이 ExceptionResolver를 끼워 넣으면 요청이 왔을 때
- DispatcherServlet에서 prehandle하고 handler adpater거쳐서 핸들러(controller)로 가서 예외가 터지고 DispatcherServlet이 예외를 보고 ExceptionResolver에게 넘겨주고 커스텀하게 예외를 처리하고 ModelAndView를 넘겨주면 정상 응답처럼 넘어가게 된다.
- 즉, view에 렌더링을하는데 뭐 아무것도 없으니 아무것도 안나오고 afterCompletion실행하고 WAS에 정상처리가 되었음을 알림
- 물론 null반환하면 오류로 쭉 넘어감
- 해당 리졸버의 구현은 , 예외를 받아 (예를들어, IllegalArgumentException면) 원하는 에러로 바꿔(response.sendError()) 오류를 먹고 다시 던지고 아무것도없는 ModelAndView를 리턴해줌
- 물론, 여기서 model and view를 실제로 정보를 넣고 리턴하면 뷰를 렌더링하게 됨스프링이 제공하는 ExceptionResolver를 사용한다면? 간단하게 구현할 수 있겠쥐
- 애초에 기본적으로 3가지의 ExceptinResolver가 등록되어 있음 (순서대로 처리)
- ExceptionHandlerExceptionResolver
- @ExceptionHandler가 되어있는 애들을 처리
- 아래 두 방식은 sendError를 하기에 비효율적인 구조를 유지한다.. 그러나! 얘는 그렇지 않다.
- 예외가 발생하면 제일 처음 찾는 ExceptionResolver가 얘임
- 얘는 자기가 호출되면 @ExceptionHandler어노테이션을 찾아보고 처리될 수 있는 지 확인
- 발견하게되면 sendError를 보내서 처리하는게 아닌 바로 Resolve해주기에 바로 정상 응답을 넘겨줌
- 기본은 어노테이션 안에 처리할 예외를 지정하지만, 파라미터로 그냥 적어주면 알아서 그걸 잡음
- 이렇게 효율적으로 예외를 핸들링할 수 있게 해준다.
- 주의할건 해당 컨트롤러에서 발생된 예외만 잡는다.
- 또, 설정한 예외를 잡는과정은 bottom-up 방식으로 진행한다. (즉, A예외의 자식 B예외를 잡는 핸들러가 없으면 A예외를 잡는 핸들러를 찾고 그것도 없으면 Exception을 잡는 핸들러를 찾아봄)
- 다양한 파라미터 및 응답을 지정할 수 있음
- ResponseStatusExceptionResolver
- @ResponseStatus(code = HttpStatus.*BAD_REQUEST*) 와 같이 Http상태 코드를 지정해줌 (예외 위에 달아주는거지..)
- 위 어노테이션 및 ResponseStatusException을 처리함
- 근데, 내부를 보면 sendError를 호출하는 형식이기에 WAS에서 다시 /error를 호출하게 됨
- 개발자가 직접 변경할 수 없는 예외에는 어노테이션을 못달아주니 그냥 throw new ResponseStatusException 이렇게 던져주고 파라미터로 던지고 싶은 예외를 넣어주면 됨
- DefaultHandlerExceptionResolver
- 스프링 내부에서 발생하는 스프링 예외를 해결
- 파라미터 바인딩하는 시점에 타입이 맞지 않으면 생기는 TypeMismatchException같은걸 처리
- 원래 아무 처리를 안하면 서블릿 컨테이너까지 오류가 올라가서 500에러가 터지는데, 이건 client의 잘못이므로 400이 맞다. 그러므로 얘는 이런 오류를 400으로 변경해줌
- 얘도 sendError로 처리
- ControllerAdvice
스프링 타입 컨버터
- 수를 문자로 혹은 반대로 변환해야 할 일이 참 많다
- 기본적으론 직접 변환을 해야했음 (기본적으론 문자로 넘어오니깐)
- 근데 뭐 requestParam하면 알아서 변환해서 줌
- Pathvariable, modelattribute도 마찬가지
- 만약, 직접 만든 클래스로 변환하고 싶다면 스프링이 제공하는 컨버터 인터페이스를 구현하고 등록하면 된다.
- 구현은 그냥 org.springframework.core.conver.converter의 Converter를 구현하면 됨
- <S,T>로 받아오는데 S를 T로 변환하는 메소드만 구현하면 됨
- 근데 이렇게 그냥 직접 변환 메소드를 쭉 다 구현하는게 편할까란 의문이 든다..
- 컨버터를 다 모아서 편하게 사용할 수 있도록 ConversionService를 제공한다.
- 그냥 만든 컨버터를 다 등록하고 해당 서비스로 convert메소드를 쓰는데 source를 쓰고 원하는 클래스를 적어주면 그거에 맞춰서 바꿔줌 (있다면)
- 이렇게 사용하면, 컨버터를 등록할땐 컨버터가 무엇인지 알아야하지만, 사용하는 상황에선 알 필요가 없다.
- 이게 또 객체지향설계원칙(SOLID) 중 하나인 ISP를 따르는 것
- DefaultConversionService는 사실 ConversionService와 ConverterRegistry를 구현하는데
- 두 인터페이스는 각각 컨버터를 사용하는 것 / 컨버터를 등록하는 것 이다.
- 즉, 컨버터를 사용하는 쪽과 등록하는 쪽을 분리할 수 있기에 서로 다른 쪽을 신경쓰지 않아도 된다.
- 뭐, WebMvcConfigurer에 addFormatter오버라이드해서 거기에다가 converter더해주면 컨버터를 등록할 수 있음 (RequestParam같은데서 컨버팅이 자동으로 된다)
- RequestParam을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService사용해서 알아서 변환을 시도하고 변환에 성공하면 정상적으로 컨트롤러 내 메소드가 실행된다.
- 타임리프는 렌더링시 컨버터를 적용해서 렌더링하는 걸 간단하게 지원함
- 즉 , 객체를 문자로 변환하는 작업을 간단하게 할 수 있다.
- 변수넣을때 {}이 대괄호를 한번 더 쓰면 컨버터를 적용시킬 수 있다.
- 그냥 {}하나만 넣어주면 객체에 대고 toString을 부름 (만약 뭐 만들어놨으면 뭔가 출력되겠지만 아니면 객체 id만 나옴)
- th:field는 다양한 기능이 있는데 여기에 컨버전 기능도 같이 있음→ 대괄호 한번더 안써줘도 알아서 컨버트해줌
- 1000이란 수를 1,000이란 문자로 변환하는 등 객체를 특정한 포맷에 맞춰 문자로 출력하거나 그 반대의 역할을 하는 것에 특화된 기능을 제공하는 것이 포맷터 임
- 컨버터가 범용적인 변환기라면 포맷터는 문자에 특화된 변환기
- ! Number가 Integer,Long이런 애들의 부모임
- locale 설정으로 현지화 적용이 가능
- Number객체지만 Long으로 변환된다
- 어댑터 패턴(소공에서 배웠던거네;)을 내부에서 사용하면 포맷터가 컨버터처럼 동작하도록 지원함
- FormattingConversionService가 대표적인 포맷터를 지원하는 컨버전 서비스
- 원하는 컨버터 포맷터를 등록해놓으면 그냥 convert 메소드에서 대상, class지정만 해줘도 컨버터,포맷터가 잘 적용이 된다.
- 부트는 내부적으로 컨버전서비스를 사용한다고 함
- 포맷터를 앱에 적용하려면 WebConfiguration 파일에
- 포맷터를 등록해주면 되는데, 여기서 주의할점이 만약 문자↔숫자 컨버터가 있으면 컨버터가 우선 순위를 가지기에 포맷터 사용을 위해선 해당 컨버터를 사용하면 안된다 (등록하면 안된다)
- 이걸 등록해놓으면 뷰 템플릿에 뿌릴 때에도 포매팅이 적용되고 반대로 웹에서 param을 숫자로 포맷팅해서 받는 행위도 할 수 있음
- 스프링 기본 포맷터
- 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 포맷터를 사용할 수 있음
- @NumberFormat
- @DateTimeFormat
- 이런 애노테이션에 원하는 포맷을 패턴으로 등록해주면 원하는 포맷으로 출력이 된다!
- 컨버터를 쓰든 포맷터를 쓰든 등록 방법은 다르지만 결국 컨버전 서비스를 통해 사용할 수 있다.
- HttpMessageConverter엔 컨버전 서비스가 적용되지 않음
- 객체를 JSON으로 변환하는 작업에선 Jackson같은 라이브러리에서 변환하기에 해당 라이브러리가 지원하는 설정을 통해서 포맷을 설정해야 함
- 컨버전 서비스는 RequestParam, ModelAttribute,PathVariable, 뷰 템플릿 등에만 사용할 수 있다.
파일 업로드
- form을 전송하는 방식
- application/x-www-form-urlencoded방식

- multipart/form-data
- 문자 및 바이너리를 동시에 전송할 때 사용
- 여러개의 part로 나누어 폼에 넣어 보내서 multi part 라는 이름을 가짐
- HttpServletRequest 에 getParts를 사용하면 이 part들을 얻을 수 있다.
- spring.servlet.multipart.enabled=true 이걸 끄면 multipart file을 거절할 수 있음
- 즉, HttpServletRequest 객체가 RequestFacade로 변하는데 옵션을 끄면 여기서 멈추기에 multipart를 지원하지 않는 상태가 되는거고
- 켜게 되면 StandardMultipartHttpServletRequest객체로 또 변해서 multipart를 지원하게 됨
- Part에 메소드들로
- getInputStream으로 데이터를 직접 읽을 수 있음
- write으론 경로 넣어주면 거기다 바로 써줌
- 근데 스프링에선 MultiPartFile이란 인터페이스로 이걸 매우 간편하게 지원해줌
- 그냥 파라미터로 MultiPartFile을 받을 수 있음
- transferTo로 new File해서 경로 넣어서 파일 저장도 할 수 있음
- html에서 여러개 파일 한번에올리게하려면 multiple로 하면 됨
- 보통 파일은 DB에 저장하지 않고 S3같은 곳 혹은 그냥 서버의 스토리지 공간에만 저장함
- 사용자가 업로드한 이름, 그리고 그 이름을 UUID같은 걸로 바꾼 저장된 이름 이렇게 쌍으로 이름들만 저장을 쭉 함
- 파일을 보내줄 땐 다양한 방법이 있다만
- Resource 를 사용해서 (responsebody인 api) UrlResource를 만들어 리턴하는 방식이 있음
- 경로를 이용해서 파일을 가져오는건데 모든 파일 저장 방식은 file: 경로 이렇게 접근해서 바로 가져오는거
- !!물론 이런 방식이면 보안에 꽤 취약하다..
- 파일을 그냥 웹에 던져주면 그냥 파일 조회하는 것 밖에 되지 않음
- 저장이 되게 하려면 CONTENT_DISPOSITION 이라는 헤더를 추가해줘야 한다
- "attachment; filename=\\""+encode+"\\"";
- 이런식으로 적어줘야 함
- 또, 한글 , 특수문자 같은 이름은 깨질 수 있기에 UriUtils가 제공하는 encode메소드로 UTF-8로 인코딩 해서 주는게 best다.