Hello, Freakin world!

Spring AOP 기반 validation 수행하기 본문

Spring boot

Spring AOP 기반 validation 수행하기

johnna_endure 2020. 7. 24. 04:06

거두절미하고 바로 예제로 고고!

 

우선 Person 도메인 클래스를 정의합니다.

package com.aop.validationexam;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Getter @ToString
@RequiredArgsConstructor
public class Person {
	@NotNull(message = "name is null")
	private final String name;
	@Min(1)
	private final int age;
}

 

@NotNull, @Min 등의 애너테이션들은 JSR-303 (validation을 위한 자바 라이브러리)에 포함된 애너테이션입니다.

 

 

그 다음 웹 요청 정보를 바인딩할 컨트롤러를 작성합니다.

package com.aop.validationexam;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class PersonController {

	private final PersonService personService;

	@PostMapping("/person")
	public Person post(@RequestBody Person person){
		personService.save(person);
		return person;
	}
}

 

컨트롤러 내에 person 정보를 저장하는 서비스 계층을 추가합니다.

그리고 단순히 값을 확인하기 위해 파라미터를 그대로 리턴하도록 합니다.

 

아래는 서비스 클래스 코드입니다.

package com.aop.validationexam;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class PersonService {
	private List<Person> personList = new ArrayList<>();
	private static Logger logger = LoggerFactory.getLogger(PersonService.class);

	public void save(Person person) {
		logger.info("save 호출됨.");
		personList.add(person);
	}
}

 

간편한 구현을 위해 데이터베이스를 사용하지는 않고 리스트에 객체 정보를 저장하도록 구현했습니다.

 

 컨트롤러 계층이 제대로 동작하는지 간단한 스프링 테스트를 작성합니다.

	@Test
	public void post_기본테스트() throws Exception {
		Person person = new Person("name", 32);

		mockMvc.perform(
				post("/person")
						.contentType(MediaType.APPLICATION_JSON)
						.content(gson.toJson(person)))
				.andExpect(status().isOk())
				.andExpect(content().json(gson.toJson(person)));
	}

 

잘 동작하네요!

 

이제 본격적으로 잘못된 요청을 보내면서 유효성 검증 로직을 추가해보도록 하겠습니다!

 

검증 시나리오

 

1. AOP를 이용해서 검증 대상 객체(Person)가 서비스 메서드의 인수로 넘겨져 실행되기 이전 시점을 인터셉트하여 검증을 수행합니다.

2. 검증에 실패하는 경우, RuntimeException을 상속한 예외를 던집니다.

3. ControllerAdvice를 이용해서 컨트롤러 레벨에서 예외를 핸들링합니다.

 

 

애스팩트 작성하기

import ...

@RequiredArgsConstructor
@Component
@Aspect
public class PersonAspect {

	private static Logger logger = LoggerFactory.getLogger(PersonAspect.class);
	private final Validator validator;  //hibernate-validator 

	@Pointcut("within(com.aop.validationexam.PersonService) && @args(javax.validation.Valid)")
	public void validationPointcut(){ }

	@Before("validationPointcut() && args(param)")
	public void validatePerson(Object param) {
		logger.info("validation 호출됨.");
        	//validation
		Set<ConstraintViolation<Object>> violationSet = validator.validate(param);
		JsonObject jsonObject = new JsonObject(); //gson
		violationSet.stream().forEach(c -> {
        		// 검증 오류가 있을 경우 오류 정보들을 이용해 Json 객체를 만든다.
			jsonObject.addProperty(c.getPropertyPath().toString(), c.getMessage());
		});
		String desc = jsonObject.toString();
		logger.info(desc);
        	//검증 오류가 있다면 생성한 Json 문자열을 예외에 담아 던진다. 
		if(jsonObject.size() != 0)throw new ValidationException(desc);
	}
}

 

@Pointcut

within 지시자를 이용해서 탐색 범위를 PersonService로 좁혔습니다. 간단하게 만들기 위해 타입을 특정했지만, 만약com.aop.validationexam..*Service 과 같이 .. 과 * 과 같은 validationexam 하위의 모든 Service 타입을 대상으로 할 수도 있습니다.( .. 는 하위 서브 패키지를 대상으로 하고 * 는 모든 문자열을 나타내는 와일드카드 입니다.)

 

@args 지시자를 이용하면 괄호 안에 정의한 클래스의 애너테이션을 이용해 매칭할 수 있습니다. 

위에서는 @Valid 라는 애너테이션을 사용했으므로 Person 객체에 @Valid 애너테이션을 추가합니다.

(@Valid 본래 용도와는 상관없이 그냥 마커용으로 사용했습니다)

@Valid
@Getter @ToString
@RequiredArgsConstructor
public class Person {
	@NotNull(message = "name is null")
	private final String name;
	@Min(1)
	private final int age;
}

 

포인트컷을 어드바이스 애너테이션에 직접 설정할 수도 있지만 그것보다 작은 범위의 포인트컷을 생성해 조합하면서 재사용하는 것이 좋습니다.

 

@Before

매칭 메서드가 실행되기 이전에 실행되는 어드바이스의 한 종류를 나타내는 애너테이션입니다.

속성값으로 선언된 포인트컷을 불러와 사용하고 있으며 args 속성을 추가해 매칭 메서드의 파라미터에 접근할 수 있도록 했습니다.

validator는 hibernate-validator 라이브러리를 주입받아 사용하고 있습니다.  gradle에 JSR-303과 hibernate-validator 의존성을 추가하면 스프링은 자동으로 hibernate-validator 빈을 생성해 validator에 주입할 수 있습니다.

  

 

예외 클래스

package com.aop.validationexam;

public class ValidationException extends RuntimeException {
	public ValidationException(String description) {
		super(description);
	}
}

 

ControllerAdvice 작성하기

package com.aop.validationexam;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class PersonControllerAdvice {

	@ExceptionHandler
	public ResponseEntity<String> validationHandler(ValidationException e){
		return ResponseEntity.status(HttpStatus.BAD_REQUEST)
				.body(e.getMessage());
}
}

 

간단하게 예외를 잡아서 예외 메세지를 응답 바디에 실어서 보내기로 합니다.

 

이제 PostMan으로 테스트를 해보면...

속성을 하나씩 추가해서 테스트해도 모두 동작합니다!

 

 

아래는 전체코드입니다.

https://github.com/johnna-endure/validationexam

 

johnna-endure/validationexam

Contribute to johnna-endure/validationexam development by creating an account on GitHub.

github.com

 

Comments