AWS Back Day 71. "Spring Boot에서 AOP를 이용한 예외처리와 유효성 검사"

이강용·2023년 4월 12일
1

Spring Boot

목록 보기
6/20

스프링 AOP (Aspect Oriented Programming)

  • AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
A () {
 log를 남긴다.
}
B () {
 log를 남긴다.
}
C () {
 log를 남긴다.
}

같은 기능을 하는 코드가 100개라면??

  • 지금까지는 클래스를 생성하여, 호출하는 방법을 사용

AOP를 사용하면?

  • AOP를 사용하면, A(), B(), C() 메소드에서 로그를 남기는 코드를 중복해서 작성할 필요 없이, 공통적으로 사용하는 기능을 분리하여 Aspect로 정의하고, 해당 Aspect를 적용할 메소드들을 지정하여 개발할 수 있습니다. 이렇게 AOP를 사용하면, 애플리케이션의 유지보수성이 향상되고, 코드의 중복도가 줄어들어 개발 생산성을 높일 수 있습니다.

AOP 주요 개념

용어설명
Aspect흩어진 관심사를 모듈화 한 것, 주로 부가기능을 모듈화함
TargetAspect를 적용하는 곳 -> 클래스, 메서드
Advice실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
JoinPointAdvice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점,
필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능
PointCutJoinPoint의 상세한 스펙을 정의한 것.
'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

의존성 추가

MVN REPOSITORTY 접속 > Spring boot aop 검색

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

CourseServiceImpl 수정

@Override
	public void registeCourse(CourseReqDto courseReqDto) {
		StopWatch  stopWatch = new StopWatch();
		stopWatch.start();
		
		courseRepository.registe(courseReqDto.toEntity());
		stopWatch.stop();
		
		System.out.println("메소드 실행 시간: " + stopWatch.getTotalTimeSeconds());
		
	}

	@Override
	public List<CourseRespDto> getCourseAll() {
		
		StopWatch  stopWatch = new StopWatch();
		stopWatch.start();
		
		List<CourseRespDto> dtos = new ArrayList<>();
		courseRepository.getCourseAll().forEach(entity -> {
			dtos.add(entity.toDto());
		});
		
		stopWatch.stop();
		System.out.println("메소드 실행 시간: " + stopWatch.getTotalTimeSeconds());
		
		return dtos;
	}

postman client 요청

Server 응답

-----------

Spring AOP PointCut 표현식

표현식설명
executionAdvice를 적용할 메서드를 명시할 때 사용
within특정 타입에 속하는 메서드를 JoinPoint로 설정되도록 명시할 때 사용
bean스프링 버전 2.5 버전부터 지원하기 시작했으며, 스프링 빈을 이용하여 JoinPoint를 설정
표현식설명
execution(public com.web.aop.study..(*))com.web.aop.study 패키지에 속해 있는 파라미터가 1개인 모든 메서드
execution( com.edu...get*(..))com.edu 패키지 및 하위 패키지에 속해 있는 이름이 get으로 시작하고 파라미터가 0개 이상인 모든 메서드
execution( com.web.aop..Service.*(..))com.web.aop 패키지 및 하위 패키지에 속해 있는 이름이 Service로 끝나는 인터페이스의 파라미터가 0개 이상인 모든 메서드
execution( com.web.aop.BoardService.(..))com.web.aop.BoardService 인터페이스에 속한 파라미터가 0개 이상인 모든 메서드
execution( some(, ))메서드 이름이 some으로 시작하고 파라미터가 2개인 모든 메서드

aop(package) > TimerAop

package com.web.study.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;



@Aspect
@Component
public class TimerAop {
	
	@Pointcut("execution(public * com.web.study.controller.lecture.*.*(..))")
	private void pointCut() {}
	
	
	@Around("pointCut()")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		
		
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		
		
		//전처리 
		Object logic = joinPoint.proceed(); //proceed = 메서드 호출
		//후처리
		
		stopWatch.stop();
		System.out.println("메서드 실행 시간: " + stopWatch.getTotalTimeSeconds() + "초");
		return logic;
	}
}
// 접근지정자 public은 생략 가능 !
	@Pointcut("execution( * com.web.study.controller.lecture.*.*(..))")
	private void pointCut() {}
//import는 org.apach에서 
	private final Logger logger = LogManager.getLogger(TimerAop.class);
    
logger.info("로그 테스트");

Lombok @Slf4j 사용 시

//한 줄로 같은 결과 실행
log.info("로그 테스트");

변경 전

System.out.println(joinPoint.getSignature().getDeclaringTypeName());
		System.out.println(joinPoint.getSignature().getName());
		System.out.println("메서드 실행 시간: " + stopWatch.getTotalTimeSeconds() + "초");

변경 후

log.info("[ Time ] >>> {}.{}: {}초",
				joinPoint.getSignature().getDeclaringTypeName(),
				joinPoint.getSignature().getName(),
				stopWatch.getTotalTimeSeconds());

Annotation 파일 만들기

package com.web.study.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) 
public @interface TimerAspect {

}

TimerAop

package com.web.study.aop;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import lombok.extern.slf4j.Slf4j;


@Slf4j
@Aspect
@Component
public class TimerAop {
	

	
	
	// 접근지정자 public은 생략 가능 !
	@Pointcut("execution( * com.web.study..*.*(..))")
	private void pointCut() {}
	
	@Pointcut("@annotation(com.web.study.aop.annotation.TimerAspect)")
	private void annotationPoinCut() {}
	
	
	@Around("annotationPoinCut()")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		
		
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		
		
		//전처리 
		Object logic = joinPoint.proceed(); //proceed = 메서드 호출
		System.out.println(logic);
		//후처리
		
		stopWatch.stop();
		log.info("[ Time ] >>> {}.{}: {}초",
				joinPoint.getSignature().getDeclaringTypeName(),
				joinPoint.getSignature().getName(),
				stopWatch.getTotalTimeSeconds());
		return logic;
	}
}

CourseController 수정

package com.web.study.controller.lecture;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.web.study.aop.annotation.TimerAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.course.CourseReqDto;
import com.web.study.service.CourseService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CourseController {
	
	private final CourseService courseService;
	//Create
	@PostMapping("/course")
	public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto CourseReqDto) {
		courseService.registeCourse(CourseReqDto);
		return ResponseEntity.ok().body(ResponseDto.ofDefault());
	}
	//추가
	@TimerAspect
	@GetMapping("/courses")
	public ResponseEntity<? extends ResponseDto> getCourseAll() {
		return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
	}
	
	@GetMapping("/search/courses")
	public ResponseEntity<? extends ResponseDto> searchCourse(int type, String searchValue) {
		return ResponseEntity.ok().body(DataResponseDto.of(courseService.searchCourse(type,searchValue)));
	}
}

요구사항

  • @CheckNameAspect
  • CheckNameAop 생성
  • log.info 해당 메서드의 [ name ] >>> 클래스명.메서드명

풀이


ParamsAop

ParamsAop

annotation > ParamsAspect 생성

package com.web.study.aop.annotation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;




@Retention(RUNTIME)
@Target(METHOD)
public @interface ParamsAspect {

}

ParamsAop

package com.web.study.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;


@Slf4j
@Aspect
@Component
public class ParamsAop {
	
	@Pointcut("@annotation(com.web.study.aop.annotation.ParamsAspect)")
	private void pointCut() {}
	
	
	@Around("pointCut()")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		
		
		StringBuilder builder = new StringBuilder();
		
		CodeSignature codeSignature = 	(CodeSignature)joinPoint.getSignature();
		String[] parameterNames =  codeSignature.getParameterNames();
		Object[] args = joinPoint.getArgs();
		
		for(int i = 0; i < parameterNames.length; i++) {
			if(i != 0) {
				builder.append(", ");
			}
			builder.append(parameterNames[i] + ": " + args[i]);
		}
		
		log.info("[ Params ] >>> {}",builder.toString());
		
		return joinPoint.proceed();
		
	}
}

ReturnDataAop

aop > annotation > ReturnDataAspect

package com.web.study.aop.annotation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface ReturnDataAspect {

}

aop > ReturnDataAop

package com.web.study.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Component
public class ReturnDataAop {

		@Pointcut("@annotation(com.web.study.aop.annotation.ReturnDataAspect)")
		public void pointCut() {}
		
		
		@Around("pointCut()")
		public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
			Object logic = joinPoint.proceed();
			
			log.info("[ ReturnData ] >>> {}.{}: {}", 
					joinPoint.getSignature().getDeclaringType().getSimpleName(),
					joinPoint.getSignature().getName(),
					logic);
			
			return logic;
		}
}

예외 처리

controller(패키지) > advice(패키지 생성) > ApiControllerAdvice

package com.web.study.controller.advice;

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

import com.web.study.dto.ErrorResponseDto;
import com.web.study.exception.CustomException;

@RestControllerAdvice
public class ApiControllerAdvice {
	
	@ExceptionHandler(CustomException.class)
	public ResponseEntity<ErrorResponseDto> error(CustomException e)  {
		return ResponseEntity.badRequest().body(
				ErrorResponseDto.of(HttpStatus.BAD_REQUEST, e, e.getErrorMap()));
	}
}

exception(패키지) > CustomException

package com.web.study.exception;

import java.util.Map;

import lombok.Getter;


@Getter
public class CustomException extends RuntimeException {

	private static final long serialVersionUID = 2658314737117138818L;
	private Map<String, String> errorMap;
	
	public CustomException(String message) {
		super(message);
	}
	
	public CustomException(String message, Map<String, String> errorMap) {
		super(message);
		this.errorMap = errorMap;
	
	}
}

CourseController

package com.web.study.controller.lecture;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.web.study.aop.annotation.CheckNameAspect;
import com.web.study.aop.annotation.ParamsAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.course.CourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CourseController {
	
	private final CourseService courseService;
	//Create
	@PostMapping("/course")
	public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto CourseReqDto) {
		courseService.registeCourse(CourseReqDto);
		return ResponseEntity.ok().body(ResponseDto.ofDefault());
	}
	
	@CheckNameAspect
	@GetMapping("/courses")
	public ResponseEntity<? extends ResponseDto> getCourseAll() {
		
		if(1 == 1) {
			throw new CustomException("예외 만들기");
		}
		
		return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
	}
	
	
	@ParamsAspect
	@GetMapping("/search/courses")
	public ResponseEntity<? extends ResponseDto> searchCourse(int type, String searchValue) {
		
		Map<String, String> errorMap=  new HashMap<>();
		if (type < 1 || type > 3) {
			errorMap.put("type", "type은 1에서 3의 사이값만 사용할 수 있습니다.");
		}
		
		if(searchValue == null ) {
			errorMap.put("searchValue", "searchValue는 필수입니다.");
		}else {
			if(searchValue .isBlank()) {
				errorMap.put("searchValue", "searchValue는 공백일 수 없습니다.");
				
			}
		}
		if(!errorMap.isEmpty()) {
			throw new CustomException("유효성 검사 실패", errorMap);
		}
		
		return ResponseEntity.ok().body(DataResponseDto.of(courseService.searchCourse(type,searchValue)));
	}
}

유효성 검사

MVN Spring Boot Validation 의존성 추가

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@Valid 어노테이션

dto > request > course > SearchCourseReqDto

package com.web.study.dto.request.course;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Setter
@Getter
@ToString
public class SearchCourseReqDto {
	
	@Min(value = 1)
	@Max(value = 3)
	private int type;
	
	
	@NotBlank(message = "검색 내용을 입력해주세요")
	private String searchValue;
}

CourseController 추가

@ParamsAspect
@GetMapping("/search/courses")
public ResponseEntity<? extends ResponseDto> searchCourse(@Valid SearchCourseReqDto searchCourseReqDto , BindingResult bindingResult) {
		
        Map<String, String> errorMap  = new HashMap<>();
		
		
        if(bindingResult.hasErrors()) {
			bindingResult.getFieldErrors().forEach(error -> {
				errorMap.put(error.getField(), error.getDefaultMessage());
			});
			
			throw new CustomException("유효성 검사 실패", errorMap);
		}
		
		return ResponseEntity.ok().body(DataResponseDto.of(
				courseService.searchCourse(searchCourseReqDto.getType(),searchCourseReqDto.getSearchValue())));
	}
  • POSTMAN GET 요청 > {{localhost}}/search/courses?type=0&searchValue=

Aop로 처리하기

aop > annotation > ValidAspect

package com.web.study.aop.annotation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;




@Retention(RUNTIME)
@Target(METHOD)
public @interface ValidAspect {

}

aop > ValidationAop

package com.web.study.aop;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;

import com.web.study.exception.CustomException;


@Aspect
@Component
public class ValidationAop {
	
		@Pointcut("@annotation(com.web.study.aop.annotation.ValidAspect)")
		private void pointCut() {}
		
		@Around("pointCut()")
		public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
			
			BeanPropertyBindingResult bindingResult = null;
			
			
			
			for(Object obj : joinPoint.getArgs()) {
				if(obj.getClass() ==  BeanPropertyBindingResult.class) {
					bindingResult = (BeanPropertyBindingResult) obj;
				}
			}
			
			if(bindingResult.hasErrors()) {
				Map<String, String> errorMap = new HashMap<>();
				
				bindingResult.getFieldErrors().forEach(error ->{
					
					errorMap.put(error.getField(), error.getDefaultMessage());
				});
				
				throw new CustomException("유효성 검사 실패", errorMap);
			}
			
			return joinPoint.proceed();
		}
}

CourseController 수정

@ValidAspect
	@ParamsAspect
	@GetMapping("/search/courses")
	public ResponseEntity<? extends ResponseDto> searchCourse(@Valid SearchCourseReqDto searchCourseReqDto , BindingResult bindingResult) {
		
		
		return ResponseEntity.ok().body(DataResponseDto.of(
				courseService.searchCourse(searchCourseReqDto.getType(),searchCourseReqDto.getSearchValue())));
	}

Java Regular Expression 이란?

profile
HW + SW = 1

0개의 댓글