Hello, Freakin world!

[Spring boot] Validation 후, AOP 이용해서 예외처리 하기 본문

Spring boot

[Spring boot] Validation 후, AOP 이용해서 예외처리 하기

johnna_endure 2020. 7. 14. 16:42

시나리오

 

사용자가 POST 요청으로 데이터를 보낼 때(포맷은 json), @RequestBody 애너테이션을 이용하면 스프링 내부에서 필드명을 이용해서 자동으로 값을 바인딩해줍니다. 

 

값이 제대로 되었는지 체크하는 validation 과정을 컨트롤러 메서드 내에서 수행하기 보단, 컨트롤러 로직 이전, 값을 바인딩한 직후 validation 과정을 수행하고 싶습니다. validation 과정 중 이상이 있는 경우 예외를 던지고 AOP를 @ControllAdvice를 이용해서 전역적으로 이 예외를 처리하려고 합니다.  

 

예제

 

우선 바인딩 타겟이 되는 Person이라는 클래스를 만듭니다.  

import lombok.Builder;
import lombok.Getter;

@Getter
public class Person {
	private String name;
	private int age;

	@Builder
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
}

 

그리고 간단한 컨트롤러 클래스와 메서드를 make it.

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PersonController {

	@PostMapping("/person")
	public Person post(@Validated @RequestBody Person person) {
		return person;
	}
}

 

validation 대상 객체임을 선언하기 위해 @Validated 애너테이션을 달아줍니다.

 

PostMan으로 테스트해보겠습니다. 

 

요청

응답

 

기본적인 틀이 완성됐다!

 

 

잘못된 요청 데이터를 넣어보면...

만약 요청 body가 아예 비어있다면 스프링은 자동적으로 400 BadRequest를 반환합니다.

하지만 안에 아무것도 없는 '{}'를 보내면 name = null, age = 0 이 매핑되는걸 알 수 있습니다.

 

이제 유효성 검증을 시작해보겠습니다.

 

유효성 검증(validation) 로직은 아래와 같습니다.

- name은 null이 아니다.

- age은 0이 아니다.

 

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class PersonValidator implements Validator {

	@Override
	public boolean supports(Class<?> clazz) {
		return Person.class.isAssignableFrom(Person.class);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Person person = (Person)target;
		if(person.getName() == null) throw new PersonBadRequest("이름값이 전달되지 않음.");
		if(person.getAge() == 0) throw new PersonBadRequest("나이값이 전달되지 않음.");
	}
}

 

Validate라는 인터페이스를 구현했습니다. supports 메서드는 바인딩 될 target object를 선정하는 메서드입니다. 파라미터와 target obeject의 클래스 타입을 비교하는 코드를 넣어줍니다.

validate 메서드에는 target 인스턴스를 이용해서 검증 로직을 구현하면 됩니다.

전체적으로 supports 반환값이 true라면 구현된 validate()를 호출해 검증을 수행한다는 흐름입니다. 간단하죵?

 

이제 검증기를 코드를 구현해 @Component 애너테이션을 이용해 빈으로 선언했습니다. 이제 선언한 빈을 어딘가에 등록해서 사용할 일만 남았네요.

package com.springboot.validation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice(assignableTypes = PersonController.class)
public class PersonControllerAdvice {

	@Autowired
	PersonValidator validator;

	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.addValidators(validator);
	}

	@ExceptionHandler
	public ResponseEntity<ErrorResponse> personBadRequest(PersonBadRequest ex, HttpServletRequest request) {
		ErrorResponse errorResponse = ErrorResponse.builder()
				.message(ex.getMessage())
				.method(request.getMethod())
				.path(request.getRequestURL().toString())
				.status_code(400)
				.build();

		return ResponseEntity.status(400)
				.body(errorResponse);
	}
}

 

@ControllerAdvice를 이용하면 컨트롤러 계층에서의 @InitBiner, @ModelAttribute, @ExceptionHandler 작업을 AOP를 이용해 처리할 수 있습니다.

 

우선 @RestControllerAdvice(assignableTypes = '클래스 타입')를 이용해 적용 대상 클래스를 한정했습니다.

그리고 @InitBinder 메서드 파라미터에 WebDataBinder 인스턴스를 선언하고 주입받아 검증기를 추가했습니다. 

 

@ExceptioHandler 메서드는 예외 이외에도 서블릿 요청, HttpSession등과 같은 메서드가 파라미터에 있으면 현재 컨텍스트에서의 빈을 주입받을 수 있습니다. 이를 통해 현재 요청의 url 값을 받아냈습니다.  

 

그리고 ErrorResponse라는 에러 관련 DTO를 만들어 ResponseEntity에 추가해서 응답을 만들었습니다.

 

//추가//

@InitBinder 사용시 주의점

이 애너테이션 메서드를 사용함으로서 WebDataBInder 인스턴스를 사용자가 초기화하기 때문에, 커스텀 객체를 바인딩할 경우 프로퍼티 에디터들을 직접 다시 설정해줘야 합니다. 이는 상당히 귀찮은 작업입니다...  제 기준에서는 이 애너테이션을 쓰지 않을 것 같습니다. JSR-303 라이브러리들을 이용하는 방법은 다를까요? 테스트 후 다시 기록하겠습니다.

 

 

 

자, 이제 잘 작동하는지 확인해 볼까요?

 

 

 

 

OK

Comments