-
스프링부트 API/AOP 02 - AOP/Validation체크스프링/API-AOP 2021. 9. 18. 21:54
유투버 '데어프로그래밍'님의 강의 참조
01 AOP란?
- AOP(Aspect Oriented Programming)의 약자로 흩어진 ASPECTS들을 모아 모듈화 하는 기법이다. 주로 현재까지 OOP개념과 그것의 기반으로 프로그래밍을 배웠다면 이제는 스프링부트에서 나온 개념 AOP를 알아야 한다
- OOP의 경우는 사용자 관점 위주로 객체지향적으로 만들었다. 하지만 개발을 하다보면 주 업무는 사용자에 대한 것이지만 개발자/운영자를 위한 추가 부분(업무 로직)이 필요 하다. 어떤 로직을 기준으로 핵신적인 관점(사용자) VS 부가적인 관점(개발자/운영자)로 나누어서 보고 그 관점을 기준으로 각각 모듈화 하는 것, 즉 '관점 지향 프로그래밍' AOP라고 불린다.
- AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화를 하겠다는 의미. 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견 할 수 있는데 이것을 흩어진 관심사(Crosscutting concern)라고 부른다.
- 이러한 흩어진 관심사를 Aspects로 모듈화 하고 핵심적인 비지니스 로직에서 분리하여 재사용 하겠다는 것
02 Validation에서의 디스패처 역할 (리플렉션)
- 주소 매핑
- 주소 매핑을 위해 관련 IoC 를 메모리에 다 띄움(@Controller/@RestController)
- 매핑된 함수의 매개변수 확인 및 Request 된 값을 비교
- 주입
- 이렇게 까지 해주지면 유효성검사는 전혀 해주지 않는걸 볼 수 있다. 또한 디스패처의 내부를 건들일 수 도 없다. 대신 스프링에서 AOP를 지원 해준다. 이 AOP를 함수 실행 전 혹은 이후에 넣어서 유효성 검사를 할 수 있다
03 스프링에서의 AOP란?
- 필터를 직접 만들어서 원하는 부분에서 필터를 거쳐 유효성검사를 해도 되지만 스프링에서 AOP 라이브러리를 직접 제공 해준다
- 예로 필터 + 리플렉션을 이용하면 필터를 거쳐 어떠한 함수를 타게하고 함수가 종료 된 이후에 작동까지 설정 가능하게 할 수 있다. 이러한 기술적인 것들을 'AOP'라고 부른다
- 스프링에서 AOP는 핵심기능과 공통기능으로 나눌 수 있다.
예로 핵심기능 - 회원가입 / 공통기능 - 유효성검사/세션체크/로그검사 등
핵심기능은 회원가입 처리만 하면되지만 이 회원가입이 처리되기위해서 공통적으로 들어가야하는 기능들이 있는데 모든 핵심기능들은 이러한 공통기능을 가지게 되어 있다. - 하지만 하나의 핵심기능이 여러가지의 공통 기능을 가지게 되면 함수가 무거워지고 유지보수에도 힘이 든다. 또한 공통기능들은 대부분 여러가지 핵심기능들에게 똑같이 부여되는 경우가 많다
- 그래서 따로 공통기능을 빼서 AOP를 만들고 특정 함수 실행 전 후 로 포인트컷을 하여 부여 할 수 있다.
- 스프링에서 AOP 프로세스는 하나의 메모리(프록시 공간) 을 띄우고 거기에 쓰고자하는 'A' 라는 함수를 불러와 그 함수 앞뒤로 포인트컷을 하여 공통 기능을 실행하도록 하는 것. 즉 A가 실행되는게 아닌 AOP가 실행되어 핵심 기능 + 공통 기능을 전부 수행 하게 함
- 보통 함수 전에 처리하고싶으면 필터를, 앞 뒤로 다 처리하고싶으면 AOP라이브러리를 쓰는게 좋다
04 AOP 만들어보기
- Validation 기능을 구현부터 한 뒤 이 기능을 AOP로 바꿀 예정. 즉 유효성 검사 로직이 AOP로 들어가게 됨.
- Validation 디펜던시 넣기
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
- 컨트롤러에 삽입
@PostMapping("/user") public CommonDto<String> save(@RequestBody JoinReqDto dto, @Valid BindingResult bindingResult) { System.out.println("save()"); System.out.println("user: "+dto); userRepository.save(dto); return new CommonDto<>(HttpStatus.OK.value(),"ok"); }
- @Valid의 기능으로 이 어노테이션을 선언하고 리퀘스트에서 넘어오는 값에대한 유효성을 검사시키고 그 검사 값이 @Valid의 BindingResult에 담기게 하는 기능
- Dto를 받기때문에 Dto 안에 어노테이션을 이용해서 유효성 검사를 걸어보자
@Data public class JoinReqDto { @NotNull(message = "You did not insert any keys for Username") @NotBlank(message = "Insert a Username") @Size(max = 20, message = "ID is too long") private String username; @NotNull(message = "Insert a Password") private String password; private String phone; }
- 즉 이값들이 Request를 통해 필터를 타게되고 걸려있는 어노테이션 유효성 검사를 진행하고 오류가 걸리면 이 오류값들이 bindingResult에 담기게 되는 것
- 이제 컨트롤러에서 bindingResult를 쓰면 된다.
@PostMapping("/user") public CommonDto<?> save(@Valid @RequestBody JoinReqDto dto, BindingResult bindingResult) { if(bindingResult.hasErrors()) { Map<String, String> errorMap = new HashMap<>(); for(FieldError error:bindingResult.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } return new CommonDto<>(HttpStatus.BAD_REQUEST.value(),errorMap); } System.out.println("save()"); System.out.println("user: "+dto); userRepository.save(dto); return new CommonDto<>(HttpStatus.OK.value(),"ok"); }
- 제네릭을 '?'로 선언해서 어떠한 타입이든 사용가능하도록 설정
- bindingResult가 오류가 있으면 if문을 타고 bindingResult의 리턴값이 Map 형태이므로 Map으로 변수를 선언
- @Valid 사용시 실패한 내용을 모두 리턴해주기 때문에 for문 가동
- 거기에 HashMap을 줘서 담을 수 있도록 세팅
- FieldError = 오류를 표시하는 객체, 즉 오류 표시를 포문을 돌면서 HashMap의 put 기능으로 키와 밸류를 넣음
(키값은 오류가 터진 변수, 밸류는 설정한 메세지가 담김) - 요청이 잘못된것은 이 로직을 타게하고 DB에서의 오류는 핸들러나 스프링에서 띄어주는 오류를 사용하는게 좋다
- 더 좋은 것은 해쉬맵도 응답시에 JSON으로 파싱이 된다는 것!
→ 업데이트에 적용
- Controller
@PutMapping("/user/{id}") public CommonDto update(@PathVariable int id, @Valid @RequestBody UpdateReqDto dto , BindingResult bindingResult) { if(bindingResult.hasErrors()) { Map<String, String> errorMap = new HashMap<>(); for(FieldError error:bindingResult.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } return new CommonDto<>(HttpStatus.BAD_REQUEST.value(),errorMap); } System.out.println("update()"); userRepository.update(id, dto); return new CommonDto<>(HttpStatus.OK.value()); }
- Dto (Black/Notnull 같은 개념)
@Data public class UpdateReqDto { @NotBlank(message = "Did not put any password") private String password; private String phone; }
- 코드 로직을 보면 앞서 설명한것처럼 공통기능이 핵심기능 사이에 껴있는걸 볼 수 있다. 그리고 공통적으로 핵심기능 전에 삽입이 되어있다. 필터로 만들어서 뿌리면 좋겠다고 생각할수도 있지만 필터는 전처리밖에되질 않는다. (필터+리플렉션을 구현해서 해도되지만 굳이 왜? 스프링이서 AOP를 지원해준다 적극적으로 쓰자!)
1) Advice 세팅 ( 공통기능을 Advice(Aspect의 기능 자체)라고 부름 )
- Dependency 삽입
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2) BindingAdvice 세팅 및 1차 테스트
@Component @Aspect public class BindingAdvice { //@Before(앞) //@After(뒤) @Around("execution(* com.cos.aop.web..*Controller.save(..))") public void validationCheck(ProceedingJoinPoint proceedingJoinPoint) { //메서드에 있는 모든걸 다들고와서 넣음 String type = proceedingJoinPoint.getSignature().getDeclaringTypeName(); String method = proceedingJoinPoint.getSignature().getName(); System.out.println("type: "+type); System.out.println("method: "+method); } }
5) AOP 완성
@Component @Aspect public class BindingAdvice { //@Before(앞) //@After(뒤) @Around("execution(* com.cos.aop.web..*Controller.*(..))") public Object validationCheck(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { //메서드에 있는 모든걸 다들고와서 넣음 String type = proceedingJoinPoint.getSignature().getDeclaringTypeName(); String method = proceedingJoinPoint.getSignature().getName(); System.out.println("type: "+type); System.out.println("method: "+method); Object[] args = proceedingJoinPoint.getArgs(); for (Object arg : args) { if (arg instanceof BindingResult) { BindingResult bindingResult = (BindingResult)arg; if(bindingResult.hasErrors()) { Map<String, String> errorMap = new HashMap<>(); for(FieldError error:bindingResult.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } return new CommonDto<>(HttpStatus.BAD_REQUEST.value(),errorMap); } } } return proceedingJoinPoint.proceed(); } }
- @Around - 앞뒤 적용
- execution(....) → 문법은 찾아서 공부 (지금 세팅은 모든 컨트롤러에 타입 상관없이 적용)
- AOP는 Object로 타입을 맞추는게 좋음
- ProceedingJoinPoint → 유효성 체크를 위한 각각의 함수가 들고 있는 파라미터들에게 접근 할 객체
(예: @PathVariable int id, @Valid @RequestBody UpdateReqDto dto, BindingResult bindingResult ← 여기 찾음) - proceedingJoinPoint 가 들고있는 각 기능들 (타입/이름 등 알 수 있음)
- Object 최상위 타입으로 배열형을 선언하고 (어떤 타입이 들어올 지 모르니) proceedingJoinPoint가 파라미터들에게 접근하니 getArgs()로 파라미터들 즉 아규먼트들 들고옴
(예: save 함수 : UpdateReqDto dto, BindingResult bindingResult 2개) - 포문을 돌면서 arg에 BindingResult가 포함되어 있으면 아래 코드 실행 (아래 코드들은 AOP전 직접 메소드에 구현한것, 즉 위에서 설명했듯이 이제 AOP 에 넣어서 진행)
- IF문을 실행하면서 bindingResult에 파라미터들이 담긴 arg를 담고 에러가 있으면 아래코드 실행
- 정상이면 return으로 조인포인트로 가라 즉 검사를 진행했던 함수에 문제가 없으니 그 함수를 실행해라!
→ 함수들에게 적용되어있던 직접 적은 유효성 검사를 지우고 테스트
@PostMapping("/user") public CommonDto<?> save(@Valid @RequestBody JoinReqDto dto, BindingResult bindingResult) { System.out.println("save()"); System.out.println("user: "+dto); userRepository.save(dto); return new CommonDto<>(HttpStatus.OK.value(),"ok"); } @PutMapping("/user/{id}") public CommonDto update(@PathVariable int id, @Valid @RequestBody UpdateReqDto dto , BindingResult bindingResult) { System.out.println("update()"); userRepository.update(id, dto); return new CommonDto<>(HttpStatus.OK.value()); }
- 여기서 최종적으로 코드를 다 짜보면 왜 Model에다가 @Valid 기능(NotNull/NotBlank)를 걸지 않는지를 알 수 있다. 모델에 걸어버리면 모든 변수를 다받아오는데 어떨때는 필요없는 변수까지 받아오게되기도 하고 어떨때는 @Valid기능을 걸어야할 변수만 필요할때도 있기 때문이다. 즉 분기를 못시킨다! 반드시 'DTO'를 만들어 쓰는 습관을 드리자!
굿! 6) 전처리/후처리만 해보기
@Component @Aspect public class BindingAdvice { @Before("execution(* com.cos.aop.web..*Controller.*(..))") public void beforeTest() { System.out.println("Before-Log saved."); } @After("execution(* com.cos.aop.web..*Controller.*(..))") public void afterTest() { System.out.println("After-Log saved."); }
- Before/After은 항상 전/후에 실행된다!
- 전처리 후처리에는 proceedingJoinPoint을 못넣는다 왜? 조인포인트 자체가 필요 없다. 이해할 개념이 아니다. 전 후 에만 처리하니 상관이 없다는것.
'스프링 > API-AOP' 카테고리의 다른 글
스프링부트 API/AOP 03 - Sentry 및 Log 적용 (0) 2021.09.19 스프링부트 API/AOP 01 - 기초 및 RestController 개념 (0) 2021.09.17