본문 바로가기

JAVA/Spring

AOP

반응형

AOP

  • AOP (Aspect Oriented Programing) 관점지향 프로그래밍은 여러 클래스에 나뉜 책임을 애스팩트(관점)라고 부르는 별도의 클래스에 캡슐화하는 접근방식을 말한다.
  • 여러 클래스에 걸쳐있는 책임은 횡단 관심사(cross-cutting concern)라고 부른다.
  • 횡단 관심사의 예로 로킹, 트랜잭션 처리, 캐싱, 보안 등을 들을 수 있다.
  • 스프링은 내부에서 트랜잭션, 캐싱, 보안 등의 선언적인 서비스를 구현하기 위해 AOP 프레임워크를 제공한다.
  • 스프링 AOP 프레임워크 대신 AspectJ를 애플리케이션에서 AOP 프레임워크로 사용할 수도 있다.

간단한 AOP

  • 서비스 레이어에 정의된 클래스의 메서드에 전달되는 인수를 모두 획득하고 싶을 경우, 메서드 인수를 로그에 남기는 간단한 접근 방법으로 메서드마다 로그 로직을 작성하는 방법이 있다.
  • 모든 메서드가 인수를 로그에 남기는 책임을 추가로 진다는 것이다. 메서드 인수를 로그에 남기는 책임이 여러 클래스와 메서드에 걸쳐 분산되어 있으므로 이것을 횡단 관심사를 표현한다.
  • 자바 클래스(애스팩트)를 정의하고 횡단 관심사에 대한 구현을 자바 클래스에 추가한다.
  • 정규식을 사용해 횡단 관심사를 적용할 메서드를 지정한다.
  • AOP 용어에서 횡단 관심사를 구현하는 애스팩트의 메서드를 어드바이스라고 부른다.
  • 각 어드바이스는 그 어드바이스를 적용할 메서드를 구현하는 포인트컷과 연관되어 있다.
  • 어드바이스를 적용할 메서드를 가리켜 조인 포인트라고 한다.
  • 스프링 AOP에서 AspectJ 애너테이션 스타일이나 XML 스키마 스타일로 애스팩트를 개발할 수 있다.
  • AspectJ 애너테이션 스타일에서는 @Aspect, @Pointcut, @Before 같은 AspectJ 애너테이션을 사용해 애스팩트를 개발한다.
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    private Logger logger = LogManager.getLogger(LoggingAspect.class);

    @Before(value = "execution(* sample.spring.chapter11.bankapp.service.*Service.*(..))")
    public void log(JoinPoint joinPoint) {
        logger.info("Entering "
                + joinPoint.getTarget().getClass().getSimpleName() + "'s "
                + joinPoint.getSignature().getName());
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            logger.info("args[" + i + "] -->" + args[i]);
        }
    }
}
  • 타입 수준 애너테이션인 @AspectJ의 @Aspect는 LoggingAspect 클래스가 AOP 애스팩트라고 지정한다.
  • @Before 애너테이션의 value속성은 스프링 AOP 프레임워크가 어드바이스를 적용할 메서드(대상 메서드)를 식별할 때 사용하는 포인트 컷 식을 지정한다.
  • (execution(* sample.spring.bankapp.service.Service.(...)))이 service 패키지에 정의된 이름 중 Service로 끝나느 클래스에 정의된 모든 public 메서드에 LoggingAspect의 log 메서드를 적용한다
  • JoinPoint 인수는 어드바이스를 적용할 대상 메서드를 표현한다. joinPoint 인스턴스를 사용해서 대상 메서드에 전달된 인수의 정보를 가져온다.
  • 애스팩트를 스프링 컨테이너에 등록해서 AOP 프레임워크가 애스팩트를 알수 있게 해야한다.
  • LoggingAspect에는 스프링 @Component를 설정하여 스프링 컨테이너가 자동으로 애스팩트를 등록한다.
public class BankApp {
    public static void main(String args[]) throws Exception {
        ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(
                "classpath:META-INF/spring/applicationContext.xml");

        BankAccountService bankAccountService = context
                .getBean(BankAccountService.class);
        BankAccountDetails bankAccountDetails = new BankAccountDetails();
        bankAccountDetails.setBalanceAmount(1000);
        bankAccountDetails.setLastTransactionTimestamp(new Date());

        bankAccountService.createBankAccount(bankAccountDetails);

        FixedDepositService fixedDepositService = context
                .getBean(FixedDepositService.class);
        fixedDepositService.createFixedDeposit(new FixedDepositDetails(1, 1000,
                12, "someemail@somedomain.com"));
        context.close();
    }
}
  • createBankAccount, createFixedDeposit 메서드를 실행하기전에 LoggingAspect의 log 메서드가 실행된다.

스프링 AOP 프레임워크

  • 스프링 AOP 프레임워크는 프록시 기반이다.
  • 어드바이스의 대상 객체마다 프록시 객체가 만들어진다.
  • 프록시는 AOP 프레임워크에 의해 호출하는 객체와 대상 객체 사이에 도입되는 중간 객체다.
  • 실행시점에서 프록시는 대상 객체를 호출을 가로채고 대상 메서드에 적용할 어드바이스를 실행한다.
  • 스프링 AOP에서 대상 객체는 스프링 컨테이너에 등록된 빈 인스턴스다.
              1.createBankAccount                                       3.createBankAccount
BankApp --------------------->    BankAccountService에 대한 프록시   ----------------->  BankAccountService
                                              | 2. log
                                              V
                                         LoggingAspect                                  
  • BankAccountService에 대한 프록시는 BankAccountService의 createBankAccount 메서드 호출을 가로챈다.
  • BankAccountService에 대한 프록시는 우선 LoggingAspect의 log 메서드를 실행한 다음에 BankAccountService의 createBankAccount 메서드를 실행한다.
  • LoggingAspect 애스팩트의 log 메서드와 같은 어드바이스의 실행시점은 어드바이스 유형에 따라 다르다.
  • AspectJ 애너테이션 스타일에서는 어드바이스에 설정한 AspectJ가 어드바이스 유형을 지정한다.
  • @Before 애너테이션은 대상 메서드를 실행하기전에 어드바이스를 실행해야 한다고 지정하며, @After는 대상 메서드를 실행한 후에 어드바이스를 실행해야 한다고 지정한다.
  • @Around는 대상 메서드를 실행하기 전후에 어드바이스를 실행해야 한다고 지정한다.

프록시 생성

  • 스프링 AOP를 사용할 때는 스프링의 ProxyFactoryBean(org.springframework.aop.framework)를 사용해 AOP 프록시를 명시적으로 사용할 수 있고, 스프링이 AOP 프록시를 자동으로 생성하게 만들 수도 있다.
  • AOP 프록시를 스프링 AOP가 자동으로 생성하는 경우 자동 프록시 생성이라고 한다.
  • AspectJ 애너테이션 스타일을 사용해 애스팩트를 생성하려면 스프링 aop 스키마의 <aspectj-autoproxy> 엘리먼트를 사용해 AspectJ 애너테이션 스타일을 사용 지원을 활성화할 필요가 있다.
  • 추가적으로 <aspectj-autoproxy>는 스프링 AOP 프레임워크가 자동으로 대상 객체에 대한 AOP 프록시를 생성하도록 지시한다.
    <aop:aspectj-autoproxy proxy-target-class="false" expose-proxy="true"/>
  • 스프링 AOP 프레임워크는 자바 SE나 CGLIB를 기반으로 프록시를 생성한다.
  • 대상 객체가 아무 인터페이스도 구현하지 않으면 스프링 AOP는 CGLIB 기반의 프록시를 생성한다.
  • 대상 객체가 하나 이상의 인터페이스를 구현한다면 스프링 AOP는 자바 SE 기반의 프록시를 생성한다.
  • <aspectj-autoproxy>의 proxy-target-class 속성은 대상 객체에 대한 프록시를 자바 SE 기반으로 할지 CGLIB 기반으로 할지 지정한다.
  • false면 스프링 AOP가 대상 객체가 하나 이상 인터페이스를 구현해서 자바 SE 기반의 프록시를 생성한다.
  • true면 스프링 AOP가 대상 객체가 하나 이상의 인터페이스를 구현하더라도 CGLIB 기반의 프로시를 생성한다.
  • expose-proxy 속성값은 AOP 프록시 자제를 대상 객체가 사용할 수 있게 제공할지 지정한다.
  • true면, 대상 객체 메서드가 AopContext의 currentProxy 정적 메서드를 호출해서 AOP 프록시에 접근할 수 있다.
  • 자바기반 설정은 @EnableAspectJAutoProxy를 설정한다.

expose-proxy 속성

  • 똑같은 은행 계좌가 존재하는지 검사하기 위한 createBankAccount 메서드가 isDuplicateAccount를 호출하도록 변경한 BankAccountServiceImpl 클래스


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import sample.spring.chapter11.bankapp.dao.BankAccountDao;
import sample.spring.chapter11.bankapp.domain.BankAccountDetails;
import sample.spring.chapter11.bankapp.exception.BankAccountAlreadyExistsException;

@Service(value = "bankAccountService")
public class BankAccountServiceImpl implements BankAccountService {
    private static Logger logger = LogManager
            .getLogger(BankAccountServiceImpl.class);
    @Autowired
    private BankAccountDao bankAccountDao;

    @Override
    public int createBankAccount(BankAccountDetails bankAccountDetails) {
        logger.info("createBankAccount method invoked");        
        if(!isDuplicateAccount(bankAccountDetails)) {
            return bankAccountDao.createBankAccount(bankAccountDetails);
        } else {
            throw new BankAccountAlreadyExistsException("Bank account already exists"); 
        }
    }

    public boolean isDuplicateAccount(BankAccountDetails bankAccountDetails) {
        //--check if the account already exists
        return false;
    }
}
  • LoggingAspect의 log 메서드는 createBankAccount 메서드가 isDuplicateAccount 메서드를 호출할 때 호출되지 않는다.
  • isDuplicateAccount 메서드가 LoggingAspect의 log 메서드에 설정한 @Before 포인트 컷과 매치되긴 하지만 LoggingAspect의 log 메서드는 호출되지 않는다.
  • 대상 객체가 자기 자신에 속한 메서드를 호출하는 경우 AOP 프록시가 가로채지 않기 때문이다.
  • 이 메서드 호출이 AOP 프록시 객체를 통해 전달되지 않기 때문에, 대상 메서드와 연관된 어떤 어드바이스도 실행되지 않는다.
  • 대상 객체에서 isDuplicateAccount 메서드로 가는 호출이 AOP 프록시를 통해 만들려면, createBankAccount 메서드에서 AOP 프록시를 얻어서 AOP 프록시 객체의 isDuplicateAccount를 호출해야 한다.
    @Override
    public int createBankAccount(BankAccountDetails bankAccountDetails) {
        logger.info("createBankAccount method invoked");
        //-- obtain the proxy and invoke the isDuplicateAccount method via proxy
        boolean isDuplicateAccount = ((BankAccountService)AopContext.currentProxy()).isDuplicateAccount(bankAccountDetails);
        if(!isDuplicateAccount) {
            return bankAccountDao.createBankAccount(bankAccountDetails);
        } else {
            throw new BankAccountAlreadyExistsException("Bank account already exists"); 
        }
    }
  • AopContext의 currentProxy를 호출하면 createBankAccount 메서드를 호출했던 AOP 프록시가 반환한다.
  • createBankAccount 메서드가 스프링 AOP 프레임워크를 통해 호출되지 않았거나, <aspectj-autoproxy>의 expose-proxy 속성이 false였다면, currentProxy 메서드 호출은 java.lang.IllgalStateException 예외를 던질 것이다.
  • AOP 프록시가 대상 객체와 똑같은 인터페이스를 구현하므로, currentProxy 메서드가 반환하는 AOP 프록시를 먼저 BankAccountService 타입으로 변환한 다음에 BankAccountService의 isDuplicateAccount 메서드를 호출한다.

포인트컷 식

  • 스프링 AOP를 사용할 때 포인트컷 식은 어드바이스를 적용할 조인포인트를 지정한다.
  • 스프링 AOP에서 조인 포인트는 항상 빈 메서드다. 필드, 생성자, public이 아닌 메서드, 스프링 빈이 아닌 객체 등에 어드바이스를 적용하려면 스프링 AOP 프레임워크 대신 AspectJ를 사용해야 한다.
  • AspectJ 애너테이션 스타일을 사용해 애스팩트를 개발할 때는 AspectJ의 @Pointcut 애너테이션과 포인트컷 식을 사용하거나 어드바이스 유형을 지정하는 AspectJ의 @Before, @After 등의 애너테이션을 사용한다.
  • 포인트컷 식에서는 execution, args, within, this와 같은 포인트컷 지정자를 사용해 어드바이스를 적용할 메서드를 찾는다.

@Pointcut

  • @Pointcut의 value 속성은 포인트 컷 식을 지정한다.
  • @Pointcut을 사용하려면 빈 메서드를 만들고, 메서드에 @Pointcut 설정한다.
  • 빈 메서드는 void를 반환해야 한다.
  • 하나의 애스팩트나 여러 애스팩트에 속한 여러 어드바이스에서 포인트컷 식을 공유할때 @Pointcut 애너테이션을 사용하면 유용하다.
@Aspect
@Component
@SuppressWarnings("unused")
public class LoggingAspect {
    private Logger logger = LogManager.getLogger(LoggingAspect.class);

    @Pointcut(value = "execution(* sample.spring.chapter11.bankapp.service.*Service.*(..))")
    private void invokeServiceMethods() {

    }

    @Before(value = "invokeServiceMethods()")
    public void log(JoinPoint joinPoint) {
        logger.info("Entering "
                + joinPoint.getTarget().getClass().getSimpleName() + "'s "
                + joinPoint.getSignature().getName());
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            logger.info("args[" + i + "] -->" + args[i]);
        }
    }
}
  • @Pointcut을 invokeServiceMethod메서드 설정한다.
  • @Before는 invokeServiceMethods 메서드를 참조한다.
  • log 메서드는 invokeServiceMethods 메서드에 설정한 @Pointcut이 지정하는 포인트컷 식과 매치되는 메서드에게 적용된다.

execution과 args 포인트컷 지정자

  • execution 포인트컷 지정자의 형식
    execution(<access-modifier-pattern> <return-type-pattern> <declaring-type-pattern> <method-name-pattern>(<method-param-pattern>) <throws-pattern>)
   1         2                    3                 4                   5
public FixedDepositDetails getFixedDeposit(int fixedDepositId) throw Exception

1: access-modifier-pattern
2: return-type-pattern
3: method-name-pattern
4: method-param-pattern
5: throws-pattern
  • 스프링 AOP 프레임워크는 어드바이스를 적용할 메서드를 찾기 위해 execution 식의 여러 부분을 메서드 선언의 각 부분과 매치시킨다.
  • <declaring-type-pattern>이 없는데, 이는 특정 타입이나 패키지에 포함된 메서드를 참조할 때만 <declaring-type-pattern>을 사용하기 때문이다.
  • args 포인트컷 지정자는 실행 시점에 대상 메서드가 받아들여야 하는 인수의 타입을 지정한다.
  • 포인트컷 식으로 실행시점에 java.util.List의 인스턴스 하나만 받는 메서드를 찾으려면, args 식을 args(java.util.List)처럼 지정해야 한다.
@pointcut(value = "execution(* createFixed*(..))")
  • *은 메서드의 반환 타입은 무엇이든 관계없다.
  • createFixed* 메서드 이름이 createFixed로 시작해야 한다.
  • (..) 메서드 인수가 무엇이든 관계없다.
@pointcut(value = "execution(* sample.MyService.*(..))")

@pointcut(value = "execution(* sample.MyService.*(..) throws *Exception)")

@pointcut(value = "execution(* sample.MyService.*(..)) && args(mypackage.someObject") // 메서드 실행 시점에 타입이 mypackage.someObject인 인수를 하나만 받는다.
  • execution과 args 포인트 컷 지정자를 조합해서 사용한다.
  • &&나 ||로 포인트컷 지정자를 조합해서 복잡한 포인트컷 식을 만들수 있다.

대상 메서드 인수를 어드바이스에 전달

  • 대상 메서드가 전달된 인수가 FixedDepositDetails의 인스턴스인 경우에만 호출되고, 메서드에 전달된 FixedDepositDetails 인스턴스를 log 메서드에서 사용할 수 있도록 변경
  • @Aspect @Component @SuppressWarnings("unused") public class LoggingAspect { private Logger logger = LogManager.getLogger(LoggingAspect.class); @Pointcut(value = "execution(* sample.spring.chapter11.bankapp.service.*Service.*(..)) && args(fixedDepositDetails)") private void invokeServiceMethods(FixedDepositDetails fixedDepositDetails) { } @Before(value = "invokeServiceMethods(fixedDepositDetails)") public void log(JoinPoint joinPoint, FixedDepositDetails fixedDepositDetails) { logger.info("Entering " + joinPoint.getTarget().getClass().getSimpleName() + "'s " + joinPoint.getSignature().getName()); Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { logger.info("args[" + i + "] -->" + args[i]); } } }
  • args 식은 대상 메서드에 전달된 FixedDepositDetails 인스턴스를 log 메서드에서 fixedDepositDetails 파라미터로 볼 수 있게 지정한다.
  • args 식이 FixedDepositDetails 객체 인스턴스를 log 메서드에 전달하기 때문에, log 메서드가 FixedDepositDetails 타입의 인수를 추가로 받도록 변경해야 한다.

bean 포인트컷 지정자

  • bean 포인트컷 지정자는 지정한 빈ID로 대상 메서드를 한정한다.
@pointcut(value = "bean(someBean)")
  • bean 포인트 컷 지정자는 이름이나 ID가 someBean으로 시작하는 빈에 들어 있는 메서드에게 어드바이스를 적용한다.

애너테이션 기반 포인트컷 지정자

@pointcut(value = "@annotation(org.springframework.cache.annotation.Cacheable)")
  • 이 포인트컷 식과 매칭되는 메서드는 스프링 Cacheable을 설정한다.
@pointcut(value = "@annotation(org.springframework.stereotype.Component)")
  • 이 포인트컷 식과 매칭되는 메서드는 스프링 @Component를 설정한 객체에 들어 있는 메서드다.

어드바이스 유형

  • before, after returning, after throwing, after, around

before : 어드바이스 대상 메서드가 실행되기 전에 실행된다. 예외를 던지지 않으면 대상 메서드가 항상 실해된다.
after returning: 어드바이스 대상 메서드가 반환된 다음에 실행된다. 대상 메서드가 예외를 발생시키면 after returning 어드바이스가 실행되지 않는다.
after throwing: 어드바이스는 대상 메서드가 예외를 던질때 실행된다. 대상 메서드가 던진 예외에 접근할 수 있다.
after : 어드바이스는 대상 메서드가 실행된 다음에 실행된다. 이때 대상 메서드가 정상적으로 끝났는지 예외를 던졌는지는 관계없다.
around: 어드바이스는 대상 메서드가 실행되기 전과 후에 실행된다. 대상 메서드의 호출 여부를 결정할 수 있다.


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import sample.spring.chapter11.bankapp.dao.FixedDepositDao;
import sample.spring.chapter11.bankapp.domain.FixedDepositDetails;

@Aspect
@Component
@SuppressWarnings("unused")
public class SampleAspect {
    private Logger logger = LogManager.getLogger(SampleAspect.class);

    @Pointcut(value = "execution(* sample.spring..BankAccountService.createBankAccount(..))")
    private void createBankAccountMethod() {

    }

    @Pointcut(value = "execution(* sample.spring..FixedDepositService.*(..))")
    private void exceptionMethods() {

    }

    @AfterReturning(value = "createBankAccountMethod()", returning = "aValue")
    public void afterReturningAdvice(JoinPoint joinPoint, int aValue) {
        logger.info("Value returned by " + joinPoint.getSignature().getName()
                + " method is " + aValue);
    }

    @AfterThrowing(value = "exceptionMethods()", throwing = "exception")
    public void afterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
        logger.info("Exception thrown by " + joinPoint.getSignature().getName()
                + " Exception type is : " + exception);
    }

    @After(value = "exceptionMethods() || createBankAccountMethod()")
    public void afterAdvice(JoinPoint joinPoint) {
        logger.info("After advice executed for "
                + joinPoint.getSignature().getName());
    }

    @Around(value = "execution(* sample.spring..*Service.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        Object obj = null;
        StopWatch watch = new StopWatch();
        watch.start();
        try {
            obj = pjp.proceed();
        } catch (Throwable throwable) {
            // -- perform any action that you want
        }
        watch.stop();
        logger.info(watch.prettyPrint());
        return obj;
    }
}

특별 인터페이스를 구현해서 어드바이스 만들기

  • 애너테이션을 사용하는 대신 스프링에 제공하는 특별 인터페이스를 사용해 여러 유형의 어드바이스를 만들 수 있다.
  • MethodBeforeAdvice 인터페이스를 구현하면 before 어드바이스를 만들수 있고, AfterReturningAdvice 인터페이스를 구현하면 afterReturning 어드바이스를 구현하는 식으로 여러 유형의 어드바이스를 만들 수 있다.
public class MyBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {}
}
  • MyBeforeAdvice 클래스에 MethodBeforeAdvice 인터페이스를 구현한다.
  • MethodBeforeAdvice 에는 대상 메서드를 호출하기 전에 실행하려는 로직이 집어 넣을수 있는 before 메서드가 정의되어 있다.
  • 특별 인터페이스를 구현해서 만들어지는 어드바이스를 스프링 aop 스키마의 config를 사용해 설정할 수 있다.

배워서 바로 쓰는 스프링프레임워크
애시시 사린, 제이 샤르마 지음
오현석 옮김

반응형

'JAVA > Spring' 카테고리의 다른 글

빈 라이프 사이클 관리  (0) 2023.11.02
검증과 데이터바인딩  (0) 2022.01.28
캐싱  (0) 2022.01.28
데이터베이스 연결  (0) 2022.01.05
빈과 빈 정의 커스텀  (0) 2021.12.26