본문 바로가기
Back end/Spring

[Spring] AOP(Aspect Oriented Programming)

by 더 이프 2023. 7. 25.
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 결과

     

    출처

     

    [Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP

    | 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로

    engkimbs.tistory.com