ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링부트 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에서의 디스패처 역할 (리플렉션)

    1. 주소 매핑
    2. 주소 매핑을 위해 관련 IoC 를 메모리에 다 띄움(@Controller/@RestController)
    3. 매핑된 함수의 매개변수 확인 및 Request 된 값을 비교
    4. 주입
    • 이렇게 까지 해주지면 유효성검사는 전혀 해주지 않는걸 볼 수 있다. 또한 디스패처의 내부를 건들일 수 도 없다. 대신 스프링에서 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을 못넣는다 왜? 조인포인트 자체가 필요 없다. 이해할 개념이 아니다. 전 후 에만 처리하니 상관이 없다는것. 

     

Designed by Tistory.