Intro
웹애플리케이션 개발시 아주 기본적인 CRUD 구현 말고도 부가기능이 필요할 것이다.
필요한 부가기능의 예시로,
잘 동작하는지 확인하는 로깅,
메서드 성능측정을 위한 수행시간 확인 기능, (@RunningTime)
핵심 비즈니스로직 (은행이라면 입금 출금 이체 등) 등이 있겠다.
한편 핵심 비즈니스 로직은 또다시 로깅, 보안, 트랜잭션 등 부가기능을 포함한다.
그래서 반복적으로(복붙) 작성될 여지가 있는데, 그러면 당연히 코드가 중복되어 더러워진다.
=> 이 문제에서 AOP 기법이 나온다!
AOP
로깅, 보안, 트랜잭션 등 부가기능(로직)을 필요한 특정시점에 주입해주는 것이다.
DI가 특정 객체를 주입해주는 것과 비슷하게, 특정 로직을 주입해주는 것이다.
트랜잭션 처리를 위해 지금까지 사용한 @Transactional 또한 AOP에 해당한다.
간단히 어노테이션을 붙임으로써 처리과정에 문제발생시 롤백을 쉽게 할 수 있었다.
스프링은 AOP를 위한 다양한 어노테이션을 제공하고,
위 예시처럼 간결 + 효율적인 개발을 가능케한다.
어떤클래스를 AOP클래스(부가기능을 수행하는 클래스)로 선언하기 위해서 @Aspect 어노테이션을 사용한다.
예시를 보며 AOP관련 어노테이션을 구경해보자.
예시1) 로깅 AOP 작성
∨ 로깅 AOP가 필요한 이유
: 로그를 찍어주는 것은 부가기능인데 댓글서비스의 메인로직 사이사이에 넣는 것이 보기 좋지 않다. 게다가 로그를 찍는 기능은 모든 기능 전체에서 부가기능으로 사용하는 것이다. 그렇기 때문에 AOP로 작성하도록 하자.
∨ 로깅 AOP 클래스를 만들어보자.
이를 통해 댓글 서비스 호출시 입력값, 결과값을 로그로 확인해보도록 하자.
=> 다시말해 특정 시점에 로깅이라는 부가기능을 실행하게 된다. AOP 개념!
ⓐ 입력값 로깅
∨ 어떤 메서드 시점에 부가기능을 주입해서 쓸 건지 @Pointcut에 명시한다.
∨ 삽입할 기능을 정의한 메서드에 삽입시점을 정의! @Before을 사용했다.
@Aspect // AOP 클래스 선언 : 부가 기능을 수행하는 클래스다라고 선언
@Component
@Slf4j
public class DebuggingAspect {
// 주입 대상이 되는 메서드를 지정하는 @Pointcut
@Pointcut("execution(* com.example.firstproject.service.CommentService.create(..))")
private void cut() {}
// 삽입할 기능을 정의
// 실행 시점을 설정 : cut()의 대상이 수행되기 이전으로
@Before("cut()")
public void loggingArgs(JoinPoint joinPoint) { // JoinPoint : cut()의 대상 메소드(는 아니고 메소드를 둘러싼 어떤 결합지점인데 일단 이렇게 알아두기)
// if 이런식으로 로깅하고 싶다 : "클래스명#메소드명 의 입력값 => XX"
// 입력값, 클래스명, 메서드명 가져오기
Object[] args = joinPoint.getArgs();
String className = joinPoint.getTarget()
.getClass()
.getSimpleName();
String methodName = joinPoint.getSignature( )
.getName();
// 입력값 로깅하기
for (Object obj : args) {
log.info("{}#{}의 입력값 => {}", className, methodName, obj);
}
}
}
ⓑ 반환값 로깅
이번엔 아래 코드를 추가해보자.
실행시점을 @AfterReturning("cut()")으로 설정한다.
cut()에 지정된 대상(메서드)를 성공적으로 호출하고 리턴된 시점을 의미한다.
∨ 리턴된 것은 @AfterReturning에 returning = "returnObj"로 명시해주어야 하고, 메서드의 파라미터에 동일한 이름을 사용해 넣어줘야 메서드가 리턴된 것을 인식할 수 있다.
@AfterReturning(value = "cut()", returning = "returnObj")
public void loggingReturnValue(JoinPoint joinPoint,
Object returnObj) {
// 클래스명, 메소드명 가져오기
String className = joinPoint.getTarget()
.getClass()
.getSimpleName();
String methodName = joinPoint.getSignature()
.getName();
// 반환값 로깅
log.info("{}#{}의 반환값 => {}", className, methodName, returnObj);
}
※ 참고 : 코드의 중복 (로직 복붙)은 사실 좋지 않으므로, 나중에 클린코딩 & 리팩터링을 공부해야 한다.
CommentService의 모든 메서드에 로깅을 적용하고 싶다면?
간단하다.
@Pointcut("execution(* com.example.firstproject.service.CommentService.*(..))")
private void cut() {}
여기까지 한 것은?
: 댓글서비스에서 댓글생성 메서드 create()의 호출 전후시점에서 입력/반환값을 로깅해보았다.
: 댓글서비스의 모든 메서드 호출 전후시점에 입력/반환값을 로깅해보았다.
: 로깅을 AOP로 구현했다는 것이 핵심이다.
: 댓글서비스에 로깅코드를 넣은게 아니라 AOP를 통해 로깅코드를 쇽쇽 찔러넣은 것! 넘깔끔하다.
: @Aspect, @Pointcut, @Before, @AfterReturning
예시2) 메서드 수행시간 측정 AOP 작성
(참고)
@RunningTime이라는 커스텀 annotation을 만들어볼 것이다!
왜?? 메서드 수행시간 측정하고 싶으면 이 @RunningTime만 딱 붙여서 간편하게 쓰려고..
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RunningTime {
}
이제 AOP 클래스를 만들쟛~! (@Aspect)
이번에는 실행시점을 호출전인 @Before이나 호출후인 @AfterReturning이 아니라,
호줄전후인 @Around를 사용할 것이다.
메서드 수행시간을 측정해야 하니까 호출전후가 시점이 되어야겠쥐..
∨ 아래 코드처럼 @Pointcut을 두개 두었다.
- @Around("cut() && enableRunningTime()")처럼 이중조건으로 쓰기 위해서다
∨ @Around를 사용할 때 메서드의 파라미터로는 ProceedingJoinPoint가 들어간다.
- 대상을 실행까지도 할 수 있는 JoinPoint다.
∨ 시간 측정을 위해 스프링 기능을 이용한다. StopWatch()
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
// 특정 어노테이션을 대상으로 지정
@Pointcut("@annotation(com.example.firstproject.annotation.RunningTime)")
private void enableRunningTime() {}
// 기본 패키지의 모든 메서드를 대상으로 지정
@Pointcut("execution(* com.example.firstproject..*.*(..))")
private void cut() {}
// 부가기능 삽입 시점 설정 - 두 조건을 모두 만족하는 대상을 전후로!
@Around("cut() && enableRunningTime()")
public void loggingRunningTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 메소드 수행 전
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 메소드 수행
Object returningObj = joinPoint.proceed(); // 타겟팅된 대상을 proceed()
// 메소드 종료 후
stopWatch.stop();
String methodName = joinPoint.getSignature()
.getName();
log.info("{}의 총 수행 시간 => {} sec", methodName, stopWatch.getTotalTimeSeconds());
}
}
우와 ㅠㅠ
'web +a' 카테고리의 다른 글
ㅡ (0) | 2022.08.03 |
---|---|
웹애플리케이션 아키텍처 - 현황 (0) | 2022.08.03 |
스프링부트 ~28 | oracle DB연동 (0) | 2022.07.31 |
스프링부트 ~27 | 댓글 삭제 (+ JS) (0) | 2022.07.29 |
스프링부트 ~26 | 역시 js로 댓글수정기능 추가 (+모달 사용!) (0) | 2022.07.28 |