반응형
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 |