728x90
목차
1. AOP(Aspect Oriented Programming)
■ AOP란?
AOP는 관점 지향 프로그래밍으로 불립니다. 관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하는 것이며 모듈화는 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말합니다.예를 들어 소스 코드상에서 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라고 부릅니다. 이러한 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지입니다.
■ Spring AOP 특징
- 접근 제어 및 부가기능을 추가하기 위해서 Proxy 패턴 기반의 AOP 구현체, Proxy 객체를 사용
- Spring Bean에만 AOP를 적용 가능
- 모든 AOP 기능을 제공하는 것이 아닌 Spring IoC와 연동하여 엔터프라이즈 애플리케이션에서의 가장 흔한 문제인 중복코드, 객체들 간의 관계 복잡도 증가, Proxy 클래스 작성의 번거로움 등에 대한 해결책을 지원
■ AOP 용어
- Aspect는 흩어진 관심사를 모듈화 한 것을 말하며 주로 부가기능을 모듈화
- Target은 Aspect를 적용하는 위치(Class, Method...)
- Advice는 실질적인 부가기능을 담은 구현체
- JoinPoint는 Advice가 적용될 위치이며 다양한 시점에 적용 가능
- PointCut은 JointPoint에서 보다 더 구체적으로 Advice가 실행될 지점을 설정
■ AOP 설치
- MVN에서 AOP를 버전에 상관없이 dependency에 작성 후 버전을 지우고 설치
2. TimerAop
■ TimerAop
- AOP를 하기 위해 TimerAop에 @Aspect, IoC 컨테이너에 등록을 위해 @Component를 선언
- @Slf4j를 사용하면 log값 설정 가능하며 log.info()를 이용해 console창에 log입력 가능
■ @Pointcut
- @Pointcut에서는 기능이 실행될 지점을 정하는데 execution과 @annotation 두가지를 사용 가능
- excution
- 괄호안에 수식어.리턴타입.클래스명.메소드명(파라미터)로 지점 설정 가능
- 수식어는 생략 가능(public 등)
- 리턴타입에는 리턴타입 명시
- 클래스명과 메소드명 작성시 클래스명은 풀 패키지명으로 명시(패키지 작성중 ..을 사용하면 하위 패키지 모두 포함)
- 파라미터에 ..작성하면 0개 이상을 의미
- *는 모든 값을 표현
- 메소드명이 get*으로 표현하면 get으로 시작하는 모든 메소드를 의미
- @annotation
- annotation을 직접 만들어 사용
- 만든 annotation의 풀 패키지명을 명시
- 기능을 사용할 지점에 해당 annotation을 적용
■ @Around
- @Around 괄호 안에는 @Pointcut로 지정한 메소드를 작성하며 논리 연산자 적용 가능
- @Around를 적용한 메소드 내 파라미터에 ProceedingJoinPoint를 넣은 뒤 proceed함수를 통해 메소드를 호출
- Advice가 Target 메소드를 감싸서 Target 메소드 호출전과 호출후에 Advice 기능 수행
- joinPoint는 해당 메소드에 있는 모든 정보를 가지고 있음
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 {
// private final Logger logger = LogManager.getLogger(TimerAop.class);
// 접근지정자 public은 생략 가능
// ..하위의 모든 클래스 및 모든 메소드에 적용
@Pointcut("execution(* com.web.study..*.*(..))")
private void pointCut() {}
@Pointcut("@annotation(com.web.study.aop.annotation.TimerAspect)")
private void annotationPointCut() {}
@Around("annotationPointCut()&&pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//전처리
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object logic = joinPoint.proceed(); // proceed = 메소드 호출
// 후처리
stopWatch.stop();
// logger.info("로그 테스트");
log.info("[ Time ] >>> {}.{}: {}초",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
stopWatch.getTotalTimeSeconds());
// System.out.println(joinPoint.getSignature().getDeclaringTypeName());
// System.out.println(joinPoint.getSignature().getName());
// System.out.println("메소드 실행 시간: " + stopWatch.getTotalTimeSeconds() + "초");
return logic;
}
}
■ TimerAspect(annotation)
- annotation을 생성 시 @Retention, @Target 설정 가능
- 아래 이미지 과정을 통해 annotation을 생성하거나 아래 코드처럼 직접 지정하여 생성 가능
- @Rentention은 해당 메소드가 실행되면 AOP를 실행하라고 지정하는 어노테이션
- @Target은 사용하는 타입을 설정할 수 있으며 중괄호를 사용하면 여러가지 타입이 작성 가능
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}) // 중괄호를 사용한 이유는 여러가지 Type을 쉼표로 적용 가능
public @interface TimerAspect {
}
■ CourseController
- 적용할 메소드에 만든 annotation인 @TimerAspect 지정
- 해당 메소드가 실행되면 AOP 기능 실행
package com.web.study.controller.lecture;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
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.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
@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())));
}
}
- console 결과
3. 예시
■ CheckNameAop
a. CheckNameAop
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;
@Aspect
@Slf4j
@Component
public class CheckNameAop {
@Pointcut("@annotation(com.web.study.aop.annotation.CheckNameAspect)")
private void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object logic = joinPoint.proceed();
log.info("[ name ] >>> {}.{}",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName());
return logic;
}
}
b. CheckNameAspect
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 CheckNameAspect {
}
c. CourseController
package com.web.study.controller.lecture;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
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.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
@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())));
}
}
- console 결과
■ ParamsAop
a. ParamsAop
- 호출 후 기능이 없으면 return에 바로 메소드 호출
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();
}
}
b. 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 {
}
c. CourseController
package com.web.study.controller.lecture;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
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.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
@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())));
}
}
- console 결과
■ ReturnDataAop
a. 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)")
private 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;
}
}
b. 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 {
}
c. CourseServiceImpl
package com.web.study.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import com.web.study.aop.annotation.ReturnDataAspect;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.response.course.CourseRespDto;
import com.web.study.repository.CourseRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class CourseServiceImpl implements CourseService{
private final CourseRepository courseRepository;
@Override
public void registeCourse(CourseReqDto courseReqDto) {
courseRepository.saveCourse(courseReqDto.toEntity());
}
@Override
public List<CourseRespDto> getCourseAll() {
List<CourseRespDto> dtos = new ArrayList<>();
courseRepository.getCourseAll().forEach(entity -> {
dtos.add(entity.toDto());
});
return dtos;
}
@ReturnDataAspect
@Override
public List<CourseRespDto> searchCourse(int type, String searchValue) {
Map<String, Object> parameterMap = new HashMap<>();
parameterMap.put("type", type);
parameterMap.put("searchValue", searchValue);
List<CourseRespDto> dtos = new ArrayList<>();
courseRepository.searchCourse(parameterMap).forEach(entity -> {
dtos.add(entity.toDto());
});
return dtos;
}
}
- console 결과