AspectJ
포인트컷(@Pointcut)
포인트컷은
@Pointcut 어노테이션이 달린 메소드를 이용해 선언. 선택 로직은
@Pointcut 안에 포인터컷
표현식을 넣어서 정의한다. 메소드의 내부는 코드를 작성할 필요는 없다.
@Pointcut("execution(* hello(..))")
private void all() {
}
어드바이스(@Before, @AfterRetuning, @AfterThrowing, @After, @Around)
어드바이스도 포인터컷과 마찬가지로 어노테이션이 붙은 메소드를 이용해 정의한다. AspectJ에서는 다섯가지 종류의 어드바이스를 사용할 수 있다.
- @Around :
Methodlnterceptor
인터페이스 메소드와 비슷하다. 실제로 @Around 와 Methodlnterceptor
는 개발 방식이 다를뿐 같은 종류의 어드바이스를 개발할 때 사용한다. 프록시를 통해서 타깃 오브젝트의 메소드가 호출되는 전 과장을 모두 담을 수 있는 어드바이스다.
@Around("all()")
public Object printParametersAndReturnVal(ProceedingJoinPoint pjp) throws Throwable {
//...
Object ret =pjp.proceed();
...
return ret;
}
- @Before : 이름 그대로 타깃 오브젝트의 메소드가 실행되기 전에 사용되는 어드바이스다. @Before 어드바이스로는 타깃 오브젝트 메소드를 호출하는 방식을 제어할 수 없다. @Before를 적용해도 타깃 오브젝트 메소드 호출은 정상적으로 일어난다.
@Before("myPointcut()")
public void logJoinPoint(JoinPoint jp) {
System.out.println(jp.getSignature().getDeclaringTypeName());
System.out.println(jp.getSignature().getName());
for(Object arg: jp.getArgs()) {
System.out.println(arg);
}
}
- @AfterReturning : 타깃 오브젝트의 메소드가 실행을 마친뒤에 실행되는 어드바이스다. 단 예외가 발생하지 않고 정상적으로 종료한 경우에만 해당된다. 따라서 메소드에서 예외가 던져졌다면 이 어드바이스는 적용되지 않는다. 종료된 후에 호출되기 때문에 메소드의 리턴 값을 참조할 수 있다. 리턴 값을 참조할 때는 어노테이션의
returning
속성을 이용해서 리턴 값을 담을 파라미터 이름을 지정해야 한다. 리턴 값 자체를 바꿀 수는 없다. 리턴값을 변경 하려면 @Around
를 사용해야 된다. 하지만 리턴 값이 레퍼런스 타입이라면 참조하는 오브젝트를 조작할 수는 있다. 전달받은 파라미터의 타입을 구체적으로 지정해주면 리턴 값의 타입이 일치하는 경우에만 어드바이스가 실행된다.
@AfterReturning(pointcut="myPointcut()", returning="ret")
public void logReturnValue(Object ret) {
//...
}
- @AfterThrowing : 타깃 오브젝트의 메소드를 호출했을 때 예외가 발생하면 실행되는 어드바이스다. 속성중
throwing
을 이용해서 예외를 전달받을 메소드 파라미터 이름을 지정할 수 있다. throwing
으로 지정한 파라미터의 타입이 발생한 예외와 일치할 경우에만 어드바이스가 호출된다. 모든 예외를 다 전달받으려면 Throwable
로 파라미터 타입을 지정하면 된다.
@AfterThrowing(pointcut="daoLayer()" throwing="ex")
public void logDAException(DataAccessException ex) {
//...
}
- @After : 메소드 실행이 정상 종료 되었을 때와 예외가 발생했을 때 모두 실행되는 어드바이스다. 코드에서
finally
를 사용했을 때와 비슷한 용도라 생각하면 된다. 반드시 반환돼야 하는 리소스가 있거나 메소드 실행 결과를 항상 로그로 남겨야 하는 경우에 사용 할 수 있다. 하지만 리턴 값이나 예외를 직접 전달받을 수는 없다.
다음은 @target 포인트컷 표현식을 직접 사용한 어드바이스 코드를 살펴보자.
@Before("@target(com.epril.myproject.special.annotation.BatchJob)")
public void beforeBatch () {
//...
}
타깃 오브젝트에
@BatchJob
이라는 어노테이션이 붙은 것을 선정해서
@Before
어드바이스를 적용한 코드다.
args
,
target
,
this
,
@target
,
@within
,
@annotation
,
@args
등에서 타입을 직접 지정하려면 위와 같이 패키지 이름을 포함한 클래스 이름전체를 적어야 한다. 문자열로 구성된 포인트컷에 타입 이름을 모두 적는 건 매우 번거롭고 실수 하기 쉽다. 이런 경우 메소드 파라미터를 이용해서 타입정보를 자바 코드로 작정하면 훨씬 깔끔하다.
import com.epril.myproject.speCla1.annotation.BatchJob;
@Before("@target(bj)")
public void beforeBatch(BatchJob bj) {
//...
}
포인트컷 표현식 내의 파라미터는 포인트컷을 따로 정의할 때도 다음과 같이 사용할 수 있다.
@Pointcut("@target(bj)")
private void batchJob(BatchJob bj) {
}
이렇게 정의된 포인트컷을 다음과 같이 어드바이스에 적용하면 어드바이스 메소드가 파라미터로 지정된 정보를 제공받을 수 있다.
@Before("batchJob(bj)")
public void beforeBatch(BatchJob bj) {
//...
}
포인트컷 표현식 내의 파라미터 이름은 포인트컷 메소드 또는 어드바이스 메소드의 파라미터 이름과 일치 해야 한다. BatchJob 애노테이션에 속성값으로 여러 가지 정보를 지정할 수 있도록 되어 있다면, 어드바이스 메소드에서 어노테이션 오브젝트를 받아 이를 참조하거나 활용할 수 있다. 마찬가지로
args()
를 이용해 파라미터 값을 바인딩한다거나,
@annotation
을 이용해 메소드의 애노테이션을 파라미터로 받을 수도 있다.
@Configurable
IoC의 원리를 다시 생각해보자. IoC는 오브젝트의 생성과 관계설정, 사용, 제거까지의 모든 과정을 컨테이너에게 위임하는 방식을 사용하는, DI의 기반이 되는 프로그래밍모델이다. DI를 적용하려면 IoC도 역시 적용돼야 한다. 따라서 이 둘은 뗄 수 없고, 그래서 보통 이 둘을 합쳐서
IoC/DI
라고 부른다.
User
도메인 오브젝트다. 따라서 애플리케이션 코드를 비롯해서
@MVC
,
ORM
프레ㅐ임워크 등 다양한 곳에서 생성된다.
User
도메인 오브젝트 안에 비즈니스 로직을 넣기 위해서 DI를 통해
UserPolicyDao
와
EmailService
빈은 제공받아야 한다고 해보자. 이런 빈들을 DI 받아서 사용할 수 있다면 사용자 레벨이 업그레이드되는 로직을 User 오브젝트의 메소드에 넣을 수 있다.
public class User (
private UserPolicyDao userPolicyDao;
private EmailService emailService;
public void setUserPolicyDao(UserPolicyDao userPolicyDao) {
//...
}
public void setEmailService(EmailService emailService) {
//...
}
public void upgradeToNextLevel() {
UserPolicy userPolicy = userPolicyDao.get(PolicyType.UPGRADE);
emailService.sendMail(this.email , upgradeMessage);
}
}
그런데 이
User
클래스는 빈으로 등록돼서 만들어지고 DI를 받는 것이 아니라 직접 코드 내에서 생성하거나 ORM 프레임 워크 등에서 오브젝트가 만들어 진다.
User user = new User();
이렇게 만들어진 User 오브젝트의
UserPolicyDao
와
EmailService
필드에는
null
값이 들어 있을 것이다. 당연히
NullPointerException
이 발생한다.
이제
@Configurable
을 이용해 DI 애스펙트를 적용해보자. 아주 간단하다.
@Configurable
public class User {
//...
}
어드바이스 자동 DI를 수행할 때 이떤 방식을 사용할지 결정하는 일이다. 세가지가 있는데 살펴보자.
설정
@Configurable
이 붙은 클래스는 스프링의 빈이 아니고, 빈으로 등록될 필요도, 할수도 없다.
태그를 사용했다고 해서 빈으로 만들어지는 것은 아니다. 빈 오브젝트가 불필요하게 만들어지는일을 피하기 위해 abstract="true"
를 넣어서 추상 빈으로 등록하면 편이 좋다.
<bean class="springbook ... User" abstract="true" >
<property name="userPolicyDao" ref=" userPolicyDao " />
<property name="emailService" ref="emailService" />
</bean>
자동 와이어링
수정자 메소드를 준비해놨다면 자동와이어링 방식을 적용할 수도 있다. 이때는 다음과 같이 @Configurable
애노테이션의 autowire
속성에 자동와이어링 방식을 지정해주면 된다.
BY_NAME
대신 BY_TYPE
을 사용할 수도 있다.
@Configurable(autowire=Autowire.BY_NAME)
public class User {
//...
}
자동 와이어링을 사용할 때는 XML 설정이 필요 없다. 보통 도메인 오브젝트는 많은 수정자 메소드를 갖고 있기 마련이라서, 자칫 원하지 않는 정보가 주입될 수도 있고, 자동와이어링 작업에 불필요한 시간을 소모한다는 단점도 있다.
어노테이션 의존 관계 설정
어노테이션 방식의 의존관계를 설정하게 되어 있다면 @Autowired
나 @Resource
를 이용할 수도 있다. 필드 주입 방식을 이용해 DI하도록 User 클래스 코드를 작성했다면 수정자 메소드도 생략 가능하다. 도메인 오브젝트에 스프링 어노테이션을 사용하는 데 불만이 없다면 가장 단순하고 적용하기 쉬운 방법이다. 단 필드 주입 방식을 선택했다고 해도 단위 테스트를 위해서라면 수정자 메소드는 생략하지 않는게 좋다.
public class User {
@Autowired private UserPolicyDao userPolicyDao;
@Autowired private EmailService emailService;
}
User를 <bean>
으로 등록할 필요는 없지만, 어노테이션을 이용한 DI를 적용하기 위한 <context:annotation-config>
와 같은 설정은 반드시 필요하다.