Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

엘라의 개발 스케치 Note

[TIL] 내일배움캠프 77일차(23.07.30.) - 스프링 AOP 본문

내일배움캠프/TIL

[TIL] 내일배움캠프 77일차(23.07.30.) - 스프링 AOP

엘라랑이 2023. 7. 30. 23:45

To-do

  • AOP 및 어노테이션 적용 공부 -> 스터디 발표 자료 정리 마무리
  • 뚜까패 스터디 - 'AOP 및 어노테이션 적용' 발표

TIL

< Spring AOP 적용과 애너테이션 활용 >

[ 오늘의 목표: Spring AOP를 통해 새로운 애너테이션 정의해 구현하기 ]

1. Spring AOP 복습하기

1) AOP 개념 소개

* AOP?

- Aspect-oriendted Programming (AOP)은 OOP를 보완하는 수단으로, 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법
  cf) OOP: Object-Oriented Programming. 객체 지향 프로그래밍

 

  - 흩어진 관심사 ⇒ AOP를 적용하면?

흩어진 관심사
AOP 적용하기

* AOP 주요 개념

- Aspect: 부가기능 모듈
- Target: 적용되는 대상 ex) class A, class B
- Advice: 해야할 일들
- Join Point: 메소드 호출 시점
- Pointcut: 어디에 적용해야 하는지(여러 Joinpoint의 집합체)

 

* AOP 구현체 - 자바

- AspectJ: 참고문서 (위키백과) - https://ko.wikipedia.org/wiki/AspectJ
- 스프링 AOP (비교적 쉽게 접근 가능)

 

* AOP 적용 방법

- 컴파일 / 로드타임 (AspectJ에서 적용)
--- 별도의 컴파일링, 별도의 설정 등이 필요함
--- 로드 타임의 경우 약간의 성능 저하가 생길 수 있음
--- 다양한 문법을 사용 가능

- 런타임(스프링 AOP에서 적용)
--- ⇒ 프록시 기반 AOP
--- 로드 타임과 비슷한 정도의 성능 저하가 생김
--- 별도의 설정이 필요하지 않음
--- 문법이 쉽고 AOP에 많은 공부가 필요하지 않음

 

* AOP 참고문서 (위키백과) - https://ko.wikipedia.org/wiki/관점_지향_프로그래밍

 

2) 프록시 기반 AOP

* 스프링 AOP 특징

- 프록시 기반의 AOP
- 스프링 빈에만 AOP를 적용 가능
- 모든 AOP 기능을 제공하는 것이 목적이 아닌, 스프링 IoC와 연동하여 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적

 

* (스프링 AOP가 아닌) 프록시 패턴 → 기존 코드 변경 없이 접근 제어 또는 부가 기능을 추가하고자 함

* 프록시 패턴의 문제점
- 매번 프록시 클래스를 별도록 작성해야 함
- 여러 클래스에 여러 메소드에 적용하는 것이 어려움
- 객체들 관계가 복잡해 적용이 쉽지 않음

 

* 스프링 AOP 특징

- 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제를 해결해 줌

- 동적으로 프록시 객체를 생성
--- 자바가 제공하는 방법은 인터페이스 기반 프록시 생성

- 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜줌
--- 클라이언트 코드 변경 없음
--- AbstractAutoProxyCreator implements BeanPostProcessor

 

* 애노테이션 기반의 스프링 @AOP

- 의존성 추가
--- implementation 'org.springframework.boot:spring-boot-starter-aop’

- 애스팩트 정의
--- @Aspect
--- @Component (빈으로 등록해야하기 때문)

- 포인트컷 정의
--- @Pointcut
----- execution
----- @annotation
----- bean

- 포인트컷 조합: &&, ||, !

- 어드바이스 정의
--- @Before
--- @After
--- @AfterReturning (정상적 반환)
--- @AfterThrowing (예외 발생)
--- @Around

 

  • 예시코드 1
@Aspect
@Component
public class ParameterAop {

	//com/sparta/myvoyageblog/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
	@Pointcut("execution(* com.sparta.myvoyageblog.controller..*.*(..))")
	private void cut() {}

	//cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
	@Before("cut()")
	public void before(JoinPoint joinPoint) {

		//실행되는 함수 이름을 가져오고 출력
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		System.out.println(method.getName() + "메서드 실행");

		//메서드에 들어가는 매개변수 배열을 읽어옴
		Object[] args = joinPoint.getArgs();

		//매개변수 배열의 종류와 값을 출력
		for(Object obj : args) {
			System.out.println("type : "+obj.getClass().getSimpleName());
			System.out.println("value : "+obj);
		}
	}

	//cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
	//@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
	@AfterReturning(value = "cut()", returning = "obj")
	public void afterReturn(JoinPoint joinPoint, Object obj) {
		System.out.println("return obj");
		System.out.println(obj);
	}
}

 

  • 예시코드 2
@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {

	private final ApiUseTimeRepository apiUseTimeRepository;

	@Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
	private void product() {}
	@Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
	private void folder() {}
	@Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
	private void naver() {}

	@Around("product() || folder() || naver()")
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
		// 측정 시작 시간
		long startTime = System.currentTimeMillis();

		try {
			// 핵심기능 수행
			Object output = joinPoint.proceed();
			return output;
		} finally {
			// 측정 종료 시간
			long endTime = System.currentTimeMillis();
			// 수행시간 = 종료 시간 - 시작 시간
			long runTime = endTime - startTime;

			// 로그인 회원이 없는 경우, 수행시간 기록하지 않음
			Authentication auth = SecurityContextHolder.getContext().getAuthentication();
			if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
				// 로그인 회원 정보
				UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
				User loginUser = userDetails.getUser();

				// API 사용시간 및 DB 에 기록
				ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
				if (apiUseTime == null) {
					// 로그인 회원의 기록이 없으면
					apiUseTime = new ApiUseTime(loginUser, runTime);
				} else {
					// 로그인 회원의 기록이 이미 있으면
					apiUseTime.addUseTime(runTime);
				}

				log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
				apiUseTimeRepository.save(apiUseTime);
			}
		}
	}
}

 

*참고 - 스프링 공식 문서 : https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aoppointcuts

 

2. [실습] Spring AOP를 통해 새로운 애너테이션 정의해 구현하기

 * Annotation 만들기

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS) // Source로 하면 사라지고, runtime까지는 유지할 필요 없음
public @interface CommentCheckPostId {
}

 

 * AOP 구현하기

@Aspect
@Component
public class CheckPageAop {
	private final CommentServiceImpl commentServiceImpl;

	@Autowired
	public CheckPageAop(CommentServiceImpl commentServiceImpl) {
		this.commentServiceImpl = commentServiceImpl;
	}

	@Before("@annotation(com.sparta.myvoyageblog.exception.annotation.CommentCheckPostId) && args(postId, commentId, user)")
	public void commentCheckPostId(JoinPoint joinPoint, Long postId, Long commentId, User user) {
		// Comment Entity 가져오기
		Comment comment = commentServiceImpl.findComment(commentId);
		if (postId != comment.getPost().getId()) {
			throw new NotFoundException("해당 페이지를 찾을 수 없습니다.");
		}
	}
}

 

 * Service에 Annotation 적용하기

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {

	// 선택한 댓글 삭제
	@Override
	@Transactional
	@CommentCheckPostId // postId 받은 것과 comment DB에 저장된 postId가 다를 경우 예외 처리
	@CommentCheckUserNotEquals // 다른 유저가 삭제를 시도할 경우 예외 처리
	public void deleteComment(Long postId, Long commentId, User user) {
		commentRepository.delete(findComment(commentId));
	}
}

Next...

  • JPA 강의 듣기
  • 플러스 주차 복습 과제, 스프링 심화 개선 과제 작성
  • 알고리즘 스터디 및 공부

 

 

Comments