AOP 를 이용해 로그 데이터 남기기

Solar·2020년 4월 19일
2

스프링

목록 보기
1/1

스프링 입문을 위한 자바 객체 지향의 원리와 이해 책의 내용을 정리하여, 코드에 적용시킨 예제입니다.

목적 : 카드 추가, 수정, 삭제, 이동 등의 history를 로그로 남기기 위해 AOP를 적용하도록 한다.

AOP

  • 횡단 관심사 : 다수의 모듈에 공통적으로 나타나는 부분
  • 핵심 관심사 : 각 의사 코드에서만 나타나는 부분
  • 코드 = 핵심 관심사 + 횡단 관심사

"스프링 ID가 의존성에 대한 주입이라면, 스프링 AOP는 로직 주입이라고 볼 수 있다."

객체지향에서 로직(코드)이 있는 곳은 메서드 안쪽이므로, AOP는 메서드에 코드를 주입할 수 있다.


매서드에서 코드를 주입할 수 있는 곳

  • Around : 메서드 전 구역

  • Before : 메서드 시작 전

  • After : 메서드 종료 후

  • AfterReturning : 메서드 정상 종료 후

  • AfterThrowing : 메서드에서 예외가 발생하면서 종료된 후


Pointcut - 자르는 지점? Aspect 적용 위치 지정자

  • Pointcut : 횡단 관심사를 적요할 타깃 메서드를 선택하는 지시자 (메서드 선택 필터) "타깃 클래스의 타깃 메서드 지정자"

    스프링 AOP는 Aspect를 메서드에만 지정할 수 있으므로 "타깃 메서드 지정자"라고 할 수 있지만, 다른 AOP프레임 워크에서는 속성 등에도 Aspect를 적용할 수 있기때문에 Aspect 적용 위치 지정자(지시자)가 맞는 표현이다.

  • 코드에서 "execution(* com.codesquad.todo2.domain.project.ProjectService.addCard(..))"

JointPoint - 연결점? 연결 가능한 지점!

  • 스프링 프레임워크가 관리하는 빈의 모든 메서드

  • (광의) Aspect 적용이 가능한 모든 지점

  • (협의) 호출된 객체의 메서드

  • 코드에서 ProceedingJoinPoint joinPoint

  • Pointcut이 특정한 하나의 메서드에 지정된 것이 아니라 여러 클래스의 동일한 메서드가 지정될 수 있다.

    @Before("execution(* runSomething())")이 Pointcut 일때,

    aaa.runSomething() 메서드를 호출한 상태라면 → Joinpoint는 aaa객체의 runSomething() 이 된다.

    bbb.runSomething() 메서드를 호출한 상태라면 → Joinpoint는 bbb객체의 runSomething() 이 된다.

  • jointpoint 파라미터를 이용하면 실행 시점에 실제 호출된 메서드가 무엇인지, 실제 호출된 메서드를 소유한 객체가 무엇인지, 호출된 메서드의 파라미터는 무엇인지 등의 정보를 확인할 수 있다.

  • joinPoint.getArgs() 로 본래 메서드가 받은 매개 변수를 받아온다.

  • Advice (Aspect Method)의 리턴 타입이 Original Method의 리턴 값과 타입이 맞아야 한다. ★

joinPoint.proceed() 시점

  • Around 에서 joinPoint.proceed() 에 대한 시점 이 중요

  • proceed()를 기준으로 이전 코드는 before, 이후 코드는 after로 구분된다.

  • proceed() 의 리턴 값은 Object 이다. 이는 Aspect 로 연결된 Original Method 의 리턴 값을 받을 수 있다.

    • 받을 때는 형변환이 필요하다.

Advice - 조언? 언제, 무엇을!

  • Pointcut에 언제, 무엇을 적용할지 정의한 메서드
  • 코드에서 logAddCard(), logDeleteCard(), logReorderCard()메서드

Aspect - 관점? 측면? Advisor 집합체

  • Aspect = Advice들 (언제, 무엇을) + Pointcut들 (어디에)

코드 작성

  1. 의존성 추가

    compile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.5'
    compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.5'
  2. 관심사 분리

  • @Component : 빈으로 등록한다.

    • 이 설정되는 이유 : 객체의 생성과 의존성 주ㅊ입을 스프링 프레임워크에 위임하기 위해서

    • ProjectService의 addCard(), softDeleteCard(), reorderCard() 들은 AOP 적용 대상이기 때문에 빈 등록 필요

    • LogAspect는 AOP의 Aspect 이기 때문에 등록할 필요가 있다.

  • @Aspect : 이 클래스를 이제 AOP에서 사용하겠다.

  • @Around : 대상 메서드 전역에서 이 메서드를 실행하겠다. // 런타임에 주입된다.

@Aspect
@Component
public class LogAspect {

    @Autowired
    private LogService logService;

    @Autowired
    private ProjectService projectService;

    @Around("execution(* com.codesquad.todo2.domain.project.ProjectService.addCard(..))") //Pointcut
    public Object logAddCard(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        Long projectId = (Long) args[0];
        Long categoryId = (Long) args[1];
        Long userId = (Long) args[3];

        CardDto cardDto = (CardDto) joinPoint.proceed(); //형변환 필요
        Long cardId = cardDto.getId();
        String cardTitle = cardDto.getTitle();

        String dstCategoryTitle = logService.findCategoryTitleById(categoryId);
        Project project = projectService.findProjectByIdOrHandleNotFound(projectId);

        Log log = new Log(userId, cardId, cardTitle, null, dstCategoryTitle, "added");
        project.addLog(log);
        projectService.saveProject(project);

        return cardDto;
    }

    @Around("execution(* com.codesquad.todo2.domain.project.ProjectService.softDeleteCard(..))")
    public Object logDeleteCard(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        Long projectId = (Long) args[0];
        Long categoryId = (Long) args[1];
        Long cardId = (Long) args[2];
        Long userId = (Long) args[3];

        boolean returnValue = (boolean) joinPoint.proceed(); //형변환 필요-본래 메서드의 리턴타입과 동일
        String cardTitle = logService.findCardTitleById(cardId);
        String srcCategory = logService.findCategoryTitleById(categoryId);
        Project project = projectService.findProjectByIdOrHandleNotFound(projectId);

        Log log = new Log(userId, cardId, cardTitle, srcCategory, null, "removed");
        project.addLog(log);
        projectService.saveProject(project);

        return returnValue;
    }

     @Around("execution(* com.codesquad.todo2.domain.project.ProjectService.reorderCard(..))")
    public Object logReorderCard(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        Long projectId = (Long) args[0];
        Long dstCategoryId = (Long) args[1];
        CardIds requestBody = (CardIds) args[2];
        Long userId = (Long) args[3];
        Long cardId = requestBody.getCardId();
        Project project = projectService.findProjectByIdOrHandleNotFound(projectId);
        Category srcCategory = projectService.findCategoryByCardIdFromProject(project, cardId); //이전 카테고리
        String sourceCategoryTitle = srcCategory.getTitle();
        
        boolean returnValue = (boolean) joinPoint.proceed(); // 기준이 되는 시점
      
        String cardTitle = logService.findCardTitleById(cardId);
        project = projectService.findProjectByIdOrHandleNotFound(projectId);
        String dstCategoryTitle = logService.findCategoryTitleById(dstCategoryId); //목적지 카테고리
        if (sourceCategoryTitle.equals(dstCategoryTitle)) {
            sourceCategoryTitle = null;
        }
        Log log = new Log(userId, cardId, cardTitle, sourceCategoryTitle, dstCategoryTitle, "moved");
        project.addLog(log);
        projectService.saveProject(project);
        return returnValue;
    }
}

스프링 AOP의 핵심

  • 스프링 AOP는 인터페이스(interface) 기반이다.
  • 스프링 AOP는 프록시(proxy) 기반이다.
  • 스프링 AOP는 런타임(runtime) 기반이다.
profile
nunnu

1개의 댓글

comment-user-thumbnail
2020년 4월 20일

오랜만에 올리셨네요! 나중에 참고해보겠습니다.
근데 저는 @Aspect를 사용하는 경우를 거의 보지 못한 것 같아서, 잘 사용하는지는 모르겠어요.
지난 2주간 고생 많으셨습니다!

답글 달기