엘라의 개발 스케치 Note
[TIL] 내일배움캠프 77일차(23.07.30.) - 스프링 AOP 본문
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 주요 개념
- 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 강의 듣기
- 플러스 주차 복습 과제, 스프링 심화 개선 과제 작성
- 알고리즘 스터디 및 공부
'내일배움캠프 > TIL' 카테고리의 다른 글
Comments