DI가 이론적인 부분에서 애를 먹었다면,
AOP에서 각 객체별로 공통의 관심사와 핵심 관심사를
나누는 일은 별로 어렵지 않은 개념이었지만,
구현하는 부분에서 알아야 할게 많았던거 같다.
그래서 이번엔 개념도 하긴 할테지만,
구현에 중점을 두고 블로깅하고자 한다.
AOP는
Aspect Oriented Programming의 약자로
관심사 기반 프로그래밍이라는 뜻이다.
이 관심사는
핵심 관심사, 공통 관심사로 나뉜다.
일단 관심사는
각 class 별로 구현된 기능이라고 볼 수 있다.그 중
공통으로 작성된 기능은
공통 관심사이고,그 외는
핵심 관심사라 볼 수 있다.
이렇게 관심사 별로 구분하여 설계하면,
공통 관심사를 기준으로 중복된 코드를 통일하여
코드의 간결성과 재사용성을 높이고,
OOP에 적합한 코드를 구현할 수 있다.
일단 추가 라이브러리 없이 사용할 땐,
class A implements InterFace{
// 핵심 관심사
@Overide
public void calc(){
...
}
...
}
class B implements InterFace{
// 핵심 관심사
@Overide
public void calc(){
...
}
...
}
class Proxy implements InterFace{
private InterFace delegator;
public Proxy(InterFace delegator) {
this.delegator = delegator;
}
@Overide
public void calc(){
delegator.calc() // 핵심 관심사 실행
... // 공통 관심사 부분
}
}
공통 관심사가 있는 클래스를 묶어
상위 인터페이스를 생성하고,
(공통 관심사 메서드명이 있는 인터페이스)
그 상위 인터페이스를 구현한 proxy 클래스를 생성
해당 클래스에는 생성자에서
공통 관심사로 묶어진 클래스를 받고,
핵심 관심사를 Override해서 주입받은 클래스의 핵심 관심사 메서드를 실행,
이후나 이전에 공통 관심사를 작성
사용할 때에는
묶인 클래스를 Proxy 클래스에 주입시키고
Proxy 클래스에서 Override한 핵심 관심사를 실행
일단 aspectj에 대해 사용하기 전에
개념을 숙지하자
Aspect
공통 관심사항에 대한 기능
Advice
공통 기능과 적용 시점을 정의
Weaving
advice를 핵심 관심사항에 적용
Joinpoint
advice가 적용되는 위치
(핵심 관심사항을 실행하는 위치)
Pointcut
joinpoint를 선정하는 방법
공통 관심사항을 작성할 class를 따로 생성,
Aspect, Pointcut,
Advice(Around, After, Before...) 등의 annotation을 사용해서 작성
@Aspect
public class exAspect {
@PointCut("execution(* *(..))")
private void aroundMethod() {}
@Around("aroundMethod()")
public Object ArdcommonAspect(ProceedingJoinPoint jp) thros Throwable {
try {
// before 부분
...
Object result = jp.proceed(); // 핵심 메서드 실행
...
// after returning 부분
return result;
} catch (Exception e) {
// after throwing 부분
...
throw e
} finally {
// after 부분
...
}
}
@Pointcut("execution(* *(..))")
private void beforeMethod() {}
@Before("beforeMethod()")
// 핵심 기능 이전에 실행
public void BefcommonAspect(JoinPoint jp) {
...
}
/* Pointcut의 execution을 advice에 작성하여
PointCut 생략 가능 */
@After("execution(* *(..))")
// 핵심 기능 이후 실행
...
@AfterReturning(value="execution(* *(..))", returning = returnVal)
public void AftcommonAspect(JoinPoint jp, Object returVal) {
/*
핵심 기능이 return 값이 있고,
return 값을 다루고자 할 때 사용
annotation안에 returning 키워드를 통해
return 값을 공통 메서드에서 파라미터로 받을 수 있음
*/
}
@AfterThrowing(value="execution(* *(..))", throwing = "e")
public void ThrowingAspect(JoinPoint jp, Exception e) {
/*
핵심 기능 실행 할 때 exception발생 시
eception 관련해서 다루고자 할 때 사용
annotation안에 throwing 키워드를 사용해
에러 메시지 컨트롤 가능
*/
}
}
around는 모든 시점에 대해 작성이 가능하다.
하지만 aspectj없이 aop를 할 때와 마찬가지로
핵심 관심사를 안에서 실행시켜줘야 한다.
핵심 관심사를 실행시키기 위해
다른 advice와 다른 ProceedingJoinPoint 클래스를 파라미터로 받고,
핵심 관심사의 결과를 return해야 한다.
Pointcut와 Advice시점이 같을 경우
이 Advice의 동작 순서는 보장되지 않는다고 한다.
그럴경우
@Aspect
@Order(1)
public class exAspect1{
...
}
@Aspect
@Order(2)
public class exAspect2{
...
}
위와 같이 Order(N) annotation으로 해결이 가능하다.
다만, class를 분리시켜줘야 한다!!
Pointcut에는 이때까지 사용한
execution 명시자 외에도 몇가지 더 존재한다.
(이 명시자를 PCD라고 한다)
작성 방법도 존재하며,
일반 적으로 많이 사용하는 와일드카드인 *
를 사용
메서드의 경우
print*
이렇게 작성시,
print로 시작하는 모든 메서드를 적용한다.
파라미터의 개수는 .
으로 표현하며 ..
일 때 0이상을 의미
가장 정교하게 joinpoint와 매칭 시킬 수 있으며
execution(`접근제어자` `반환 타입` `패키지/클래스.`메서드명`(`파라미터 타입` `파라미터 개수`)
위와 같이 표현이 가능하다.
패턴 내에 해당하는 모든 joinpoin를 매칭하며
within(`패키지`.`클래스`.`메서드명`)
위와 같이 표현이 가능하다.
물론 와일드카드 사용도 가능
spring framework에서 DI를 통해 생성한 bean으로
joinpoint를 매칭시키며
bean(`bean이름`)
위와 같이 표현한다.
모든 Advice에는 JoinPoint나 ProceedingJoinPoint를
파라미터로 받는다.
앞서 말했듯이 JoinPoint는
핵심 관심사 메서드를 의미하며
핵심 관심사 메서드를 공통 관심사에서 다루기 위해
파라미터로 받는다.
joinpoint.getSignature()
핵심 관심사 메소드의 return값, 이름, 매개변수 등의 정보가 저장된 Signature 객체
joinpoint.getTarget()
핵심 관심사 메소드를 가지고있는 객체
joinpoint.getArgs()
핵심 관심사 메소드에 전달된 매개변수 배열
Around advice에서만 사용되며,
Object result = joinpoint.proceed()
이를 통해 핵심 관심사 메소드 호출이 가능하고,
결과 값 저장 가능
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
class AppConfigurer{
...
}
DI를 생성하는 곳에서(Configuration annotation이 있는 클래스)
EnableAspectJAutoProxy annotation을 추가하면
Aspect annotation을 달고 있는 Proxy들을 찾고,
등록하여 Advice를 적용한다.
또한,
Spring에서 Bean으로 등록된 객체를 사용하기 위해서
Aspect class 또한 Bean으로 등록해 준다.
(Bean객체를 사용하지 않더라도 Bean등록은 권장한다고 한다.)
class A {
// 핵심 관심사 메서드 1
public void externalMethod(){
internalMethod();
// 공통 관심사 메서드 실행이 안됨...
...
}
// 핵심 관심사 메서드 2
public void internalMethod(){
...
}
}
과제를 하면서 깨달았는데,
핵심 메서드 안에서
다른 내부 메서드를 호출하면
(이 내부 메서드가 Pointcut에 해당하더라도)
AOP 적용이 안된다고한다.
@Component
class A {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
// 핵심 관심사 메서드 1
public void externalMethod(){
a.internalMethod();
// 공통 관심사 메서드 실행 성공!!
...
}
// 핵심 관심사 메서드 2
public void internalMethod(){
...
}
}
이는
autowired annotation을 이용해
자기 자신의 객체를 설정하고
객체의 내부 메서드를 호출하는 방식으로 해결이 가능하다.
이외에 Bean을 조회해서 사용하는 방법도 있다.
Aspect class를
bean 객체로 등록하기 위해 ComponentScan 방식을 사용해서
등록했었는데, 문제가 발생했다.
내가 작성하지 않은 메서드가 Pointcut에 포함되어
JoinPoint가 된 것...
다른 동기분들에게 물어보다 큰 이유는 알게 되었다.
바로 ComponentScan 방식과 수동으로 Bean을 등록하는 방식에
차이가 있다는 것...
ComponentScan 방식
Bean객체를 등록하기 위해 범위 안의 패키지를 다 순회한다.따라서 개발자가 직접 작성한(컨트롤이 가능한) 클래스에 사용
수동 등록 방식
개발자가 컨트롤이 불가능한 외부라이브러리 사용시 사용
이 차이 때문에 오류가 발생한건 알겠는데,
아직 명확하게 이유를 찾은건 아닌거 같다...
구현하는데 애좀 쓴거 같다
이해도 꽤 잘된거 같고...
그래도 아직 노력해서 안된것이 없었기 때문에
기분이 꽤 좋다