📌 Spring AOP(Aspect Oriented Programming)에 대한 자세한 개념들은 아래 포스팅을 참고해주세요.
우리 프로젝트에서는 Spring AOP를 활용하여 로그 기능과 알림 기능을 구현하였다.
어떻게 구현했는지에 대해 자세히 살펴보려고 한다 !
우리 프로젝트에서는 실행 결과가 나오는 모든 시점, Controller/Service 코드의 실행 전/후에 로그를 남겨 줄 것이므로
아래와 같이 LogAspect
를 구현하였다.
@Aspect // (1)
@Slf4j
@Profile("local") // (2)
@Component // (3)
public class LogAspect {
@Pointcut("execution(* com.yata.backend..*(..))") // (4)
public void all() {
}
@Pointcut("execution(* com.yata.backend..*Controller.*(..))") // (5)
public void controller() {
}
@Pointcut("execution(* com.yata.backend..*Service.*(..))") // (6)
public void service() {
}
@Around("all()") // (7)
public Object logging(ProceedingJoinPoint joinPoint) throws Throwable { // (7-1)
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
log.info("log = {}" , joinPoint.getSignature());
log.info("timeMs = {}", timeMs);
}
}
@Before("controller() || service()") // (8)
public void beforeLogic(JoinPoint joinPoint) throws Throwable { // (8-1)
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
log.info("method = {}", method.getName());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if(arg != null) {
log.info("type = {}", arg.getClass().getSimpleName());
log.info("value = {}", arg);
}
}
}
@After("controller() || service()") // (9)
public void afterLogic(JoinPoint joinPoint) throws Throwable { // (9-1)
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
log.info("method = {}", method.getName());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if(arg != null) {
log.info("type = {}", arg.getClass().getSimpleName());
log.info("value = {}", arg);
}
}
}
}
위에 번호 순서대로 살펴보면,
(1) @Aspect
➜ 이 클래스가 Aspect를 나타내는 클래스임을 명시
(2) @Profile("local")
➜ local 환경에서만 나타나도록 정의
(3) @Component
➜ 스프링 빈으로 등록
(4) @Pointcut("execution(* com.yata.backend..*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 메서드를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )
(5) @Pointcut("execution(* com.yata.backend..*Controller.*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 Controller를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )
(6) @Pointcut("execution(* com.yata.backend..*Service.*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 Service를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )
(7) @Around("all()")
➜ 메서드 호출 전/후, 예외 발생 시점에 해당 부가기능(Advice) 실행
➜ 인자로 위 (4)에서 설정했던 포인트 컷을 넣어줌
(7-1) Advice 정의
@Around("all()") public Object logging(ProceedingJoinPoint joinPoint) throws Throwable { // (7-1) long start = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); return result; } finally { long finish = System.currentTimeMillis(); long timeMs = finish - start; log.info("log = {}" , joinPoint.getSignature()); log.info("timeMs = {}", timeMs); } }
@Around()
애너테이션의 사용으로, 해당 Advice의 첫번째 파라미터는 ProceedingJoinPoint
사용
✔️
ProceedingJoinPoint
➜JoinPoint
를 확장한 것
➜ 실제 메소드 실행을 직접 제어하는 기능을 제공하는 객체
⠀
✔️JoinPoint
➜ Advice가 적용되는 시점에서의 메소드 실행 정보를 제공하는 인터페이스
System.currentTimeMillis()
로 현재 시간을 측정 == 메서드 실행 시작 시간
joinPoint.proceed()
로 Advice가 적용된 메서드 실행 후 그 결과를 반환하고,
반환된 결과를 Object result
변수에 저장
( 원래 메서드의 반환값이기 때문에 Object 타입 )
finally
블록 - 위의 메서드 실행이 완료된 후에 실행되는 코드
➜ Advice 이후의 추가 로직 수행
( 주로 알림 로깅과 같은 작업이 이루어짐 )
System.currentTimeMillis()
를 다시 호출하여 메서드 실행 완료 시간 측정
long timeMs = finish - start;
로 시작 시간과 완료 시간의 차이를 계산하여 메서드 실행에 걸린 시간 계산
log.info()
로 메서드 시그니처와 메서드 실행 시간 출력
joinPoint.getSignature()
result
(8) @Before("controller() || service()")
➜ Controller 또는 Service 가 실행되는 시점 전에 해당 부가기능(Advice) 실행
➜ 인자로 위 (5),(6)에서 설정했던 포인트 컷을 넣어줌
(8-1) Advice 정의
@Before("controller() || service()") public void beforeLogic(JoinPoint joinPoint) throws Throwable { // (8-1) MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); log.info("method = {}", method.getName()); ⠀ Object[] args = joinPoint.getArgs(); for (Object arg : args) { if(arg != null) { log.info("type = {}", arg.getClass().getSimpleName()); log.info("value = {}", arg); } } }
➜ Advice가 적용된 메서드의 실행 전에, 해당 메서드의 이름과 인자값들을 로그로 출력하는 역할
( 이를 통해 메서드 호출 시 전달되는 인자값들을 확인 / 모니터링 가능 )
Advice의 첫번째 파라미터로 JoinPoint
사용
✔️
JoinPoint
➜ Advice가 적용되는 시점에서의 메소드 실행 정보를 제공하는 인터페이스
joinPoint.getSignature()
로 현재 Advice가 적용된 메서드의 시그니처를 가져옴
MethodSignature
를 이용하여 메서드 시그니처로부터 Method
객체를 얻음
( 이를 통해 메서드의 이름을 알 수 있음 )
log.info()
로 메서드의 이름 출력
joinPoint.getArgs()
로 메서드의 인자값들을 가져옴
for loop
로 각 인자값에 대해 아래 작업 수행
arg.getClass().getSimpleName()
로 인자값의 타입 가져옴arg
가 null이 아닌 경우, 타입과 값을 로그에 출력(9) @After("controller() || service()")
➜ 실행 결과 / 예외에 상관없이,
Controller 또는 Service 가 실행되는 시점 후에 해당 부가기능(Advice) 실행
➜ 인자로 위 (5),(6)에서 설정했던 포인트 컷을 넣어줌
(9-1) Advice 정의
@After("controller() || service()") public void afterLogic(JoinPoint joinPoint) throws Throwable { // (9-1) MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); log.info("method = {}", method.getName()); ⠀ Object[] args = joinPoint.getArgs(); for (Object arg : args) { if(arg != null) { log.info("type = {}", arg.getClass().getSimpleName()); log.info("value = {}", arg); } }
➜ Advice가 적용된 메서드의 실행 후에, 해당 메서드의 이름과 인자값들을 로그로 출력하는 역할
( 이를 통해 메서드 호출 이후에 전달되는 인자값들을 확인 / 모니터링 가능 )
beforeLogic()
메서드와 상세 내용이 같음우리 프로젝트에서는 Custom Annotation에 Advice를 붙여줄 것이므로 아래와 같이 NotifyAspect
를 구현하였다.
📌 Custom Annotation을 생성하여 Spring AOP를 적용하는 방법에 대해서는 아래 포스팅을 참고해주세요.
<[Project] Custom Annotaiton을 이용하여 Spring AOP 프로젝트에 적용하기 !>
@Aspect
➜ 이 애너테이션으로 이 클래스가 Aspect를 나타내는 클래스임을 명시
@Component
➜ @Aspect
가 붙은 클래스를 Aspect로 쓰기 위해선 이 애너테이션으로 스프링 빈으로 등록해야함
@EnableAsync
➜ @Async
애너테이션으로 비동기적 처리를 할 수 있게 함
➜ 클래스 레벨에 붙여줌
@Pointcut("@annotation(com.yata.backend.domain.notify.annotation.NeedNotify)")
➜ 해당 애너테이션을 가지고 있는 메서드의 조인포인트에 Advice(부가기능)을 주입해라 ~
위 포인트컷의 타겟 명세 (
com.yata.backend.domain.notify.annotation.NeedNotify
)는
위에서 만든 Custom Annotation의 위치임 !
@Async
➜ Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation
➜ 메서드 레벨에 붙여줌
✔️
@Async
애너테이션의 제약조건
- public 메서드 일 것
- 동일 클래스에서 호출하는 Self-invocation 이어서는 안됨
( 즉, inner method에서는 사용 불가 )
⠀
👉@Async
가 적용된 method의 경우, 프록시가 메서드를 가로채 다른 Thread에서 실행 시켜주는 동작 방식이기 때문에 메서드는 public이어야 하고,
Self-invocation를 사용하게되면 프록시를 무시하고 바로 메서드를 호출하기 때문에 사용이 불가하다.
[ 참고 Effective Advice on Spring Async: Part 1 ]
💡 만약 Spring Boot에서 간단히 사용하고 싶다면,
단순히 Application Class에@EnableAsync
Annotation을 추가하고,
비동기로 작동하길 원하는 메서드에@Async
Annotation을 붙여주면 사용이 가능하다.
[ 참고 ]
✔️ 동기적 방식 vs 비동기적 방식
- 동기적 방식
➜ 요청을 보낸 후, 해당 응답을 받아야만 다음으로 넘어갈 수 있는 실행 방식
⠀- 비동기적 방식
➜ 요청을 보낸 후, 응답과 관계없이 바로 다음 동작을 실행할 수 있는 방식
@AfterReturning(pointcut = "annotationPointcut()", returning = "result")
➜ Target에 대한 내용이 예외없이 성공적으로 수행되면, 결과값 반환 후 해당 내용(Advice)이 주입됨
⠀
➜ 첫번째 인자로 포인트컷 / 두번째 인자로 Target 메서드의 반환값을 받음
( 바로 포인트컷을 넣어주어도 되는데, 유지보수성/확장성을 위해 따로 메서드를 만들어 넣어줌 )
NotifyInfo
notifyService.send
➜ send()
메서드를 통해 알림을 생성하고 클라이언트에게 전달
📌
notifyService.send()
에 대한 자세한 내용은 아래 포스팅을 참고해주세요.
여기까지 log 기능과 notify 기능을 Aspect로 분리한 후 프로젝트를 실행시켜보면
위와 같이 LogAspect와 NotifyAspect가 정상적으로 실행되는 것을 알 수 있다 !!
( 오른 쪽 형광펜으로 칠 한 값들은 NotifyAspect에서 send()
한 값들이다. )
📌 다음 포스팅은 실시간 알림 구현에 대한 내용입니다.
⠀
👉 [Project] SSE(Servcer-Sent-Events)로 실시간 알림 기능 구현하기 !