Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

요약 - 검증과 예외 처리 본문

Spring/Spring MVC - 웹 개발 활용 기술

요약 - 검증과 예외 처리

studyHub 2024. 8. 24. 18:31

검증 (validation)

 

타임리프 스프링 검증 오류 통합 기능

BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이다. 

BindingResult 는 검증할 대상 바로 다음에 와야한다. BindingResult 는 Model에 자동으로 포함된다.

@ModelAttribute에 데이터 바인딩 오류 발생 시 오류 정보( FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출

 

BindingResult에 검증 오류를 적용하는 3가지 방법

  • @ModelAttribute의 객체에 타입 오류 등으로 바인딩 실패 시 스프링이 FieldError 생성해서 BindingResult에 넣어줌
  • 개발자가 직접 넣어준다.
  • Validator 사용

FieldError, ObjectError

- BindingResult에는 ObjectError, FieldError가 들어가는데, rejectedValue가 오류 발생시 사용자 입력 값을 저장하는 필드     타입 오류로 바인딩에 실패하면 스프링은 FieldError를 생성하면서 사용자가 입력한 값을 넣어둔다.

- codes, arguments 필드를 제공하여  오류 발생시 오류 코드로 메시지를 찾기 위해 사용한다. ( errors.properties )

- BindingResult 가 제공하는 rejectValue() , reject() 를 사용하면 FieldError , ObjectError 를 생성해준다.

MessageCodesResolver가 target.filed.code로 오류메시지를 찾아준다.
메시지 설계 tip : 레벨 전략. 범용성있게 사용하다가 -> 필요시 디테일 추가

스프링이 직접 만든 검증 오류 코드와 오류 메시지 처리도 커스텀 가능하다.

  • 개발자가 직접 설정한 오류 코드 rejectValue() 를 직접 호출
  • 스프링이 직접 검증 오류에 추가한 경우(주로 타입 정보가 맞지 않음)
    스프링은 타입 오류가 발생하면 typeMismatch 라는 오류 코드를 사용한다

Validator 분리

- 목표 : 컨트롤러의 복잡한 검증 로직을 별도로 분리하자

- 방법 : Validator 구현 클래스를 만들고, 컨트롤러에서 주입받고, WebDataBinder에 validator를 추가한 후, validate 호출.

※ WebDataBinder

    스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

     WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.

     validator를 직접 호출하는 부분을 지우고, 대신에 검증 대상 앞에 @Validated를 붙여도 된다.

@Validated 는 검증기를 실행하라는 애노테이션


Bean Validation

특정 필드에 대한 검증 로직은 대부분 빈 값인 지 아닌지 등과 같이 매우 일반적인 로직으로, 정형화 할 수 있다.

검증 로직을 애노테이션을 활용해 모든 프로젝트에 적용할 수 있게 표준화 한 것이 바로 Bean Validation이다.

 

spring-boot-starter-validation 라이브러리를 넣으면 스프링부트가 자동으로 Bean Validator를 인지하고 스프링에 통합, 자동으로 글로벌 Validator로 등록한다.(LocalValidatorFactoryBean을 글로벌 Validator로 등록)

이 Validator는 빈 필드에 붙은 @NotNull 같은 애노테이션을 보고 검증을 수행

이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid , @Validated 만 적용하면 된다.

검증 오류가 발생하면, FieldError , ObjectError 를 생성해서 BindingResult 에 담아준다.

오류 코드가 애노테이션 이름으로 등록되므로 errors.properties에 오류코드를 등록해주면 된다.

오브젝트 오류(글로벌 오류)의 경우 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다.

 


Bean Validation 한계 - 수정시 검증 요구사항

동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법은 아래 두가지가 있다.

  1. BeanValidation의 groups 기능을 사용한다. (실무에서는 주로 등록/수정용 폼 객체를 분리해서 사욯하므로 잘 사용x)
  2. Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용

Bean Validation - HTTP 메시지 컨버터

@Valid, @Validated 는 HttpMessageConverter (@RequestBody)에도 적용할 수 있다.

 

API의 경우 3가지 경우를 나누어 생각해야 한다.

  1. 성공 요청: 성공
  2. 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
  3. 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함

 

2. 실패 요청 응답 값 : HttpMessageConverter에서 요청 JSON을 ItemSaveForm 객체로 생성하는데 실패하는 경우

@RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다

예외 발생시 원하는 모양으로 예외를 처리하는 방법은 예외 처리 부분에서 다룬다.


 

예외 처리와 오류 페이지

서블릿 예외 처리

- WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)

- WAS(sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (response.sendError())

=> 에러 코드에 맞춰 tomcat이 기본으로 제공하는 오류 화면이 나타난다.

 

기본 오류 페이지 커스터마이징

1. 웹서버 커스터마이저 클래스를 만들고 오류 페이지 등록

2. 오류 페이지를 처리할 수 있는 컨트롤러 생성

필터의 경우 DispatcherType, 인터셉터는 excludePathPatterns를 활용하면 필터/인터셉터는 건너뛸 수 있다.
1. WAS(/error-ex, dispatchType=REQUEST) → 필터 → 서블릿 → 인터셉터 → 컨트롤러 
2. WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500, dispatchType=ERROR) → 필터(x) → 서블릿 → 인터셉터(x) → 컨트롤러 → View

 

스프링 부트 - 오류 페이지

스프링 부트는 이런 과정을 모두 기본으로 제공한다.

1. ErrorPage 를 자동으로 등록한다. 이때 /error 라는 경로로 기본 오류 페이지를 설정한다.

2. BasicErrorController 라는 컨트롤러를 자동으로 등록한다.

 

따라서 개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서
해당 경로 위치에 HTTP 상태 코드 이름의 뷰 파일을 넣어두면 된다!

BasicErrorController 의 처리 순서

  1. 뷰 템플릿
    resources/templates/error/500.html
    resources/templates/error/5xx.html
  2. 정적 리소스( static , public )
    resources/static/error/400.html
    resources/static/error/404.html
    resources/static/error/4xx.html
  3. 적용 대상이 없을 때 뷰 이름( error )
    resources/templates/error.html

API 예외 처리

스프링 부트가 제공하는 BasicErrorController  HTML 페이지를 제공하는 경우에는 매우 편리하다.

그런데 API 오류 처리는 형식이 매번 다를 수 있으므로, BasicErrorController 를 확장해서 사용해야만 한다.

이보다는 API 오류 처리는 뒤에서 설명할 @ExceptionHandler 를 사용하자.


HandlerExceptionResolver

예외가 발생해서 서블릿을 넘어 WAS까지 예외가 전달되면 HTTP 상태코드가 500으로 처리된다.
발생하는 예외에 따 라서 400, 404 등등 다른 상태코드로 처리하고 싶으면, HandlerExceptionResolver를 활용할 수 있다.

 

스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있도록 HandlerExceptionResolver를 제공한다.

ExceptionResolver 로 예외를 해결해도 postHandle() 은 호출되지 않는다.

ExceptionResolver 활용

  1. 예외 상태 코드 변환 → response.sendError(400)
  2. 뷰 템플릿 / API 응답 처리 → BasicErrorController 없이 바로 응답
    HandlerExceptionResolver 인터페이스를 직접 구현한 후 ExceptionResolver를 등록한 사례

ExceptionResolver 를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver 에서 예외를 처리해버린다.

따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 난다.

결과적으로 WAS 입장에서는 정상 처리가 된 것이다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다.

 

그런데 HandlerExceptionResolver 를 직접 사용하기는 복잡하다. 특히 API 오류 응답의 경우 response 에 직접 데이터를 넣어야 해서 매우 번거롭고, ModelAndView 를 반환해야 하는 것도 API에는 잘 맞지 않는다.

스프링은 이 문제를 해결하기 위해 @ExceptionHandler 라는 매우 혁신적인 예외 처리 기능을 제공한다. 

우선 스프링이 제공해주는 ExceptionResolver들을 알아보자.


API 예외 처리 - 스프링이 제공하는 ExceptionResolver

스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.

HandlerExceptionResolverComposite 에 다음 순서로 등록
1. ExceptionHandlerExceptionResolver  →  @ExceptionHandler
2. ResponseStatusExceptionResolver    →  HTTP 응답 코드 변경 (response.sendError)
3. DefaultHandlerExceptionResolver      →   스프링 내부 예외 처리 (eg.TypeMismatchException을 400으로 변경)

API 예외 처리 - @ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver 이다.

스프링은 ExceptionHandlerExceptionResolver 를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높다. 실무에서 API 예외 처리는 대부분 이 기능을 사용한다

@ExceptionHandler 애노테이션을 선언하고,  해당 컨트롤러에서 처리하고 싶은 예외를 지정 해주면 된다.

실행 흐름

  1. 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
  2. 예외가 발생했으로 ExceptionResolver가 작동, 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행.
  3. ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리 할 수 있는 @ExceptionHandler가 있는지 확인.
  4. illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용.
    따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.
  5. @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다. (기본 200)

@ControllerAdvice

@ExceptionHandler 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다. @ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있다.

Controller에서 예외 처리 코드를 제거하고 Advice에 생성

- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여

- @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)

 

 


참고 자료 & 이미지 출처
스프링 MVC 2편 (김영한 님)


 

'Spring > Spring MVC - 웹 개발 활용 기술' 카테고리의 다른 글

스프링 타입 컨버터  (0) 2025.01.24
요약 - 로그인 처리  (0) 2024.08.25
API 예외 처리  (0) 2024.08.11
예외 처리와 오류 페이지  (0) 2024.08.10
로그인 처리2 - 필터, 인터셉터  (0) 2024.08.10