일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Logback
- 다익스트라
- Zuul
- 유레카
- Gradle
- spring boot
- 플로이드 와샬
- 메모이제이션
- spring cloud
- 백트래킹
- 완전 탐색
- 이분 매칭
- 스프링 시큐리티
- docker-compose
- 주울
- 구현
- 트리
- ZuulFilter
- 스택
- 이분 탐색
- 게이트웨이
- dp
- 비트마스킹
- 서비스 디스커버리
- 구간 트리
- 도커
- Spring Cloud Config
- Java
- 달팽이
- BFS
- Today
- Total
Hello, Freakin world!
Spring AOP 기반 validation 수행하기 본문
거두절미하고 바로 예제로 고고!
우선 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
'Spring boot' 카테고리의 다른 글
[Spring] 컨트롤러 메서드 파리미터 JSON 바인딩 여부 테스트 (0) | 2020.08.10 |
---|---|
[Spring AOP] 포인트컷의 종류와 성능 (0) | 2020.07.24 |
[Spring AOP] 포인트컷 지시어들(Designators) (0) | 2020.07.18 |
[Spring AOP] 포인트컷 선언하기 (0) | 2020.07.18 |
[Spring AOP] Aspect 선언하기 (0) | 2020.07.18 |