본문 바로가기

web +a

스프링부트 ~30 | 관점지향 프로그래밍 (AOP, aspect-oriented programming)

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());
    }
}

 

 

 


우와 ㅠㅠ 

 

반응형
다른 블로그