- AOP는 Aspect Oriented Programming의 약자로
관점 지향 프로그래밍
이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서모듈화
란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
A () {
log를 남긴다.
}
B () {
log를 남긴다.
}
C () {
log를 남긴다.
}
같은 기능을 하는 코드가 100개라면??
- 지금까지는 클래스를 생성하여, 호출하는 방법을 사용
- AOP를 사용하면,
A()
,B()
,C()
메소드에서 로그를 남기는 코드를 중복해서 작성할 필요 없이, 공통적으로 사용하는 기능을 분리하여 Aspect로 정의하고, 해당 Aspect를 적용할 메소드들을 지정하여 개발할 수 있습니다. 이렇게 AOP를 사용하면, 애플리케이션의 유지보수성이 향상되고, 코드의 중복도가 줄어들어 개발 생산성을 높일 수 있습니다.
AOP 주요 개념
용어 | 설명 |
---|---|
Aspect | 흩어진 관심사를 모듈화 한 것, 주로 부가기능을 모듈화함 |
Target | Aspect를 적용하는 곳 -> 클래스, 메서드 |
Advice | 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체 |
JoinPoint | Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능 |
PointCut | JoinPoint의 상세한 스펙을 정의한 것. '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 응답
표현식 | 설명 |
---|---|
execution | Advice를 적용할 메서드를 명시할 때 사용 |
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)));
}
}
풀이
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();
}
}
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>
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 > 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())));
}