AOP, 관점 지향 프로그래밍

OneTwoThree·2022년 10월 29일
0

유튜브 홍팍님 강의

AOP

지금까지 컨트롤러-서비스-리포지토리로 다양한 기능을 구현했다
CRUD 기능을 만들었다.
하지만 이러한 핵심 기능과는 별개로 부가적인 코드가 필요할 수 있다.
로깅, 보안, 트랜잭션 등의 부가기능이 각 기능에 반복적으로 작성되어야

부가기능을 특점 지점에 잘라넣는 기능
DI가 특정 객체를 주입하는 것처럼 특정 로직을 잘라넣음
앞에서 사용했던 @Transactional 같은 어노테이션

부가기능을 특정 지점에 삽입하게 함으로써 더욱 간결하고 효율적인 프로그래밍이 가능하다.

AOP 관련 주요 어노테이션

코드작성하며 써보자


실습

댓글 서비스 입출력과 확인을 위한 로깅 AOP 작성과 특정 메소드의 수행시간을 측정하는 AOP를 만들어보자

먼저 실습을 위해 application.properties로 가서 postgresql이랑 연동했던 부분을 주석처리하고 메모리db에서 실습을 윗부분을 진행하게 주석을 풀어준다.

이렇게 전에 썼던 데이터 (data.sql에 만들어놓은) 로 진행


로깅

댓글 서비스가 호출될 때 입력값, 결과값을 로그로 확인해보자

  @Transactional //DB에 접근하므로 트랙잭션 어노테이션으로 문제가 발생하면 롤백되도록 해줘야함
    public CommentDto create(Long articleId, CommentDto dto) {

        log.info("입력값 => {}",articleId);
        log.info("입력값 => {}",dto);


        // 게시글 조회 및 예외 발생
        Article article = articleRepository.findById(articleId)
                .orElseThrow(()->new IllegalArgumentException("댓글 생성 실패! 대상 게시글이 없습니다.")); //해당 id 없을경우 예외 발생

        // 댓글 엔티티 생성
        Comment comment = Comment.createComment(dto,article);

        // 댓글 엔티티를 DB로 저장
        Comment created = commentRepository.save(comment);

        // DTO로 변경하여 반환
        CommentDto createdDto = CommentDto.createCommentDto(created);
        log.info("반환값 => {}",createdDto);
        return createdDto;

    }

CommentService 클래스에서 다음과 같이 전달받는 값이랑 반환하는 값을 로그로 찍어보자. 참고로 log를 쓰기 위해서는 클래스명 위에 @Slf4j 어노테이션을 달아야함



이렇게 원하는 내용을 로그로 찍어서 확인해 볼 수 있는데,
핵심 로직인 빨간색 부분과 부가적인 내용인 파란색 부분이 섞여있어서 코드가 난해할 수 있다.

그리고 create 메소드 뿐만 아니라 update 같은 다른 메소드에서도 이런 기능이 필요하면 비슷한 코드를 또 작성해야 한다.

이러한 문제점을 AOP로 해결해보자

aop 패키지와 DebuggingAspect 클래스를 만든다.

@Aspect // AOP 클래스 선언 : 이 클래스는 부가 기능을 주입하는 클래스
@Component // IOC 컨테이너가 해당 객체를 생성 및 관리하도록 한다
@Slf4j
public class DebuggingAspect {

    //대상 메소드 선택 : CommentService#create()
    @Pointcut("execution(* com.example.firstproject.service.CommentService.create(..))")
    private void cut(){ }

    //실행 시점 설정 : cut()의 대상이 수행되기 이전
    @Before("cut()")
    public void loggingArgs(JoinPoint joinPoint) { //cut의 대상 메소드
        // 입력값 가져오기
        Object[] args = joinPoint.getArgs();


        // 클래스명
        String className = joinPoint.getTarget().getClass().getSimpleName();


        // 메소드명
        String methodName = joinPoint.getSignature().getName();


        // 입력값 로깅하기
        // CommentService()#create()의 입력값 = > 5 이런식으로. 클래스명과 입력값 필요

        for (Object obj : args){ //foreach 문
            log.info("{}#{}의 입력값 => {}",className,methodName,obj);
        }
    }

}

메소드들의 이름은 중요하지 않다. 어노테이션이 중요하다.
@Pointcut은 어느 메소드를 대상으로 할지 정하는 어노테이션이다.
@Before는 cut의 대상이 수행되기 이전에 부가기능을 넣는다는 것이다.
그리고 loggingArgs 메소드 안에 부가기능을 넣으면 된다.
매개변수인 joinPoint는 cut의 대상 메소드를 의미한다.
getArgs , getTarget 등 메소드들을 써서 원하는 값들을 가져온다. 궁금하면 구글링해보자
이렇게 원하는 내용을 log에 찍어주게 했다.

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

@AfterReturning 어노테이션은 cut의 대상 메소드 호출, 성공적으로 반환한 후 부가기능을 실행한다는 뜻이다. 그리고 returning = "returnObj"는 cut의 대상 메소드의 반환값이다.
returnObj라는 이름으로 매개변수를 작성해서 메소드 내에서 활용가능하다.
이를 이용해서 클래스명, 메소드명, 반환값을 로깅해준다.

이렇게 작성하면 CommentService의 create() 메소드 대상으로는 부가기능이 작동한다. 하지만 댓글 수정 기능을 사용하면 로깅되는 내용이 없다. 왜냐하면

 @Pointcut("execution(* com.example.firstproject.service.CommentService.create(..))")
    private void cut(){ }

이렇게 대상 메소드를 create 만 지정해 놓았으니 당연하다.

@Pointcut("execution(* com.example.firstproject.service.CommentService.*(..))")

이렇게 create이 아닌 *를 입력해서 CommentService의 모든 메소드를 대상으로 작성한 내용들을 적용할 수 있다.
(..)은 매개변수에 상관없이 적용한다는 뜻
자세한 문법은 구글에 "aop pointcut 표현식" 검색해서 찾아볼 것

대상을 *(..) 로 바꾸자 update 메소드에 대해서도 로그가 뜬다.

update 메소드를 보면 로깅에 관한 코드는 하나도 작성하지 않았는데 AOP를 통해서 부가기능을 넣었다.

메소드 수행시간 측정

annotation 패키지를 만들고 RunningTime 어노테이션을 만들자

@Target({ElementType.TYPE, ElementType.METHOD}) //어노테이션 적용 대상
@Retention(RetentionPolicy.RUNTIME) //어노테이션 유지 기간 (RUNTIME 시점까지 유지)
public @interface RunningTime {
    
}

이렇게 작성해 준다.
@Target과 @Retention을 검색해보면 자세히 알 수 있다.

package com.example.firstproject.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.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();
        // 메소드 종료 후
        stopWatch.stop();
        String methodName = joinPoint.getSignature()
                .getName();
        log.info("{}의 총 수행 시간 => {} sec", methodName, stopWatch.getTotalTimeSeconds());
    }
}

aop 패키지에 PerformanceAspect를 만들고 이렇게 작성한다.
@PointCut을 2개를 써줬는데 하나는 어떤 어노테이션을 사용할지, 하나는 어떤 메소드를 대상으로 할지 정한것이다.
그리고 부가기능 메소드는 @Around로 대상 메소드 전후로 부가기능을 삽입하며, 조건을 &&로 묶어서 작성함
즉 이 부가기능은 기본 패키지의 모든 메소드 중 @RunningTime 어노테이션이 달린 메소드를 대상으로 삽입한다.
그리고 @Around를 이용한 부가기능 메소드는 대상 메소드 매개변수가 ProceedingJointPoint 객체다.
throws Throw는 proceed의 예외처리를 위함이다.
스프링부트에서 제공하는 StopWatch를 이용해 시간을 재고 로깅한다.

   //댓글 삭제
    @RunningTime
    @DeleteMapping("/api/comments/{id}")
    public ResponseEntity<CommentDto> delete(@PathVariable Long id){
        // 서비스에게 위임
        CommentDto deletedDto = commentService.delete(id);
        // 결과 응답
        return ResponseEntity.status(HttpStatus.OK).body(deletedDto);
    }

CommentApiController의 delete 메소드에 @RunningTime 어노테이션을 달아서 시간을 측정해보자.

요약

  • aop 패키지에 DebuggingAspect와 PerformanceAspect를 추가
  • @Aspect와 @Component 어노테이션
  • @Pointcut, @Before, @AfterReturning
  • 실행시간 측정을 위해 RunningTime이라는 어노테이션을 직접 만듬
  • @Around

0개의 댓글