제일 중요
동적
동적은 "코드가 실행하는 과정에서
자동으로 코드
가 추가" 의미를 가지고 있다.
관점 지향 프로그래밍이란 말을 들었을 때 이해가지 않을 것이다. 관점? 그게 뭐지? 횡단? 일단 개념을 먼저 아는 것보다는 쉬운 예시를 먼저 이야기하는 것이 좋을 거 같다.
그럼 중복 코드의 분리?
아래 3가지의 기준으로 코드들은 분리된다.
그럼 분리하는 이유가 무엇일까?
바로, 코드의 재사용성
과 유지보수
측면에서 유리하기 때문이다. 이것 때문에 우린 객체지향 언어를 사용한다.
AOP가 적용되지 않은, 다른 말로는 분리되지 않은 예시이다.
class Aop {
void aaa() {
System.out.println("[before]{");
System.out.println("aaa() is called.");
System.out.println("}[after]");
}
void aaa2() {
System.out.println("[before]{");
System.out.println("aaa2 is called.");
System.out.println("}[after]");
}
void bbb() {
System.out.println("[before]{");
System.out.println("bbb is called.");
System.out.println("}[after]");
}
public static void main(String[] args) {
Aop aop = new Aop();
aop.aaa();
System.out.println();
aop.aaa2();
System.out.println();
aop.bbb();
}
}
여기서 각 메서드 마다 중복되는 코드들이 있다. 우리는 이것을 부가기능(advice)로 부르고, 중복되지 않는 코드를 핵심기능(Target) 이라고 하겠다.
하나의 메서드에 2개의 관심사(Target, Advice) 있는 것도 알 수 있다.
관심사
- Target : 핵심기능
- Advice : 부가기능
void aaa() { System.out.println("[before]{"); // 변하지 않는 것, 공통코드, 관심사 Advice System.out.println("aaa() is called."); // 변하는 것, 관심사 Target System.out.println("}[after]"); // 변하지 않는 것, 공통코드, 관심사 Advice }
위 코드는 중복되어 있어 굉장히 불필요한 코드이다. 그리고 관심사가 분리되어 있지 않아 코드를 변경했을 때 변경해야하는 스코프가 넓어지는 단점도 가지고 있다.
그래서 AOP 를 적용해서 Target에 대한 클래스, Advice에 대한 클래스를 각각 생성하여 분리해보자.
Target의 MyClass 클래스
class MyClass {
void aaa() {
System.out.println("aaa is called");
}
void aaa2() {
System.out.println("aaa2 is called");
}
void bbb() {
System.out.println("bbb is called");
}
}
Advice의 MyAdvice 클래스
class MyAdvice {
void invoke(Method m, Object obj, Object... args) throws Exception {
System.out.println("[before]{"); // 변하지 않는 것, 공통코드, 관심사 Advice
m.invoke(obj, args); // aaa(), aaa2(), bbb() 호출
System.out.println("}[after]"); // 변하지 않는 것, 공통코드, 관심사 Advice
}
}
실행 메서드
main 메서드 안에 각 코드마다 설명을 주석으로 달았다. 참고하면 된다.
public static void main(String[] args) throws Exception {
MyAdvice myAdvice = new MyAdvice();
Class myClass = Class.forName("com.fastcampus.ch3.aop.MyClass");
Object obj = myClass.newInstance();
/**
* 1. Class myClass = Class.forName("aop.MyClass"); : Class myClass : MyClass 타입의 참조변수 생성
* 2. myClass.newInstance() : 생성된 MyClass 타입의 참조변수를 인스턴스화 시켜 "객체 생성"
* 3. Object obj = myClass.newInstance(); : 생성된 myClass 객체 -> Object 타입의 obj 변수에 할당
*/
for(Method m : myClass.getDeclaredMethods()){
/**
* for(대입 받을 변수 명 : 배열명)
* myClass.getDeclaredMethods() : Myclass 클래스의 메서드 배열 {aaa(), aaa2(), bbb()}
* Method m 에 차례대로 배열 메서드가 저장됨
*/
myAdvice.invoke(m, obj, null);
}
}
}
실행하게 되면, 아래와 같이 나온다. 우리가 수정해야할 부분은 Target에 대한 클래스만 수정해주면 되는 장점을 가지고 있다.
[before]{
aaa is called
}[after]
[before]{
aaa2 is called
}[after]
[before]{
bbb is called
}[after]
예를 들어 advice의 메서드 이름에서 a로 시작하는 대상만 advice 를 적용한다고 했을 때 어떻게 코드를 작성할까
Adivce의 MyAdvice 클래스
class MyAdvice {
Pattern p = Pattern.compile("a.*"); // a 로 시작하는 정규식
boolean matchers(Method m){
Matcher matcher = p.matcher(m.getName());
return matcher.matches();
}
void invoke(Method m, Object obj, Object... args) throws Exception {
if(matchers(m)) // a 로 시작하는 메서드의 이름이면, before 출력
System.out.println("[before]{");
m.invoke(obj, args);
if(matchers(m)) // a 로 시작하는 메서드의 이름이면, after 출력
System.out.println("}[after]");
}
}
정규식을 사용해서 a로 시작하는 Target 메서드 이름만 before, after 가 출력된다. bbb 메서드는 advice 와 함께 출력되지 않느다.
[before]{
aaa is called
}[after]
[before]{
aaa2 is called
}[after]
bbb is called
@Transactional 사용해서도 똑같은 결과를 낼 수 있따.
Transactional Default 속성
@Transactional 속성값이 정의되어 있지 않다면, Around(Before + After) Default 속성이다.
@Transactional 이 없는 join point 대상은 advice 동적으로 추가되지 않는 예시이다.
class MyAdvice {
void invoke(Method m, Object obj, Object... args) throws Exception {
if(m.getAnnotation(Transactional.class) != null) // 메서드에 @Transactional 있으면 true
System.out.println("[before]{");
m.invoke(obj, args); // aaa(), aaa2(), bbb() 호출
if(m.getAnnotation(Transactional.class) != null)
System.out.println("}[after]");
}
}
class MyClass {
@Transactional
void aaa() {
System.out.println("aaa is called");
}
@Transactional
void aaa2() {
System.out.println("aaa2 is called");
}
void bbb() {
System.out.println("bbb is called");
}
}
우리는 패턴에 의해서 join point 에 부가기능을 추가할지 안할지에 대한 과정을 AOP에서는 pointcut
라고 부른다. 그리고 보면 항상 Advice 사이에 Target 이 있는 것을 볼 수 있다. 이러한 구조도 AOP와 관련이 있기 때문이다.
이것을 먼저 생각해야한다.
코드를 자동으로 추가한다면, 어디에 추가될까?
맨 위에서 동적이란 내용에 대해서 코드가 실행 중 자동으로 추가 된다고 설명했다.
실행 중이라면 실행에 영향을 끼치지 않고 어디 부분에 Advice를 추가해야할까?
바로 핵심기능(Target) 메서드 기준 시작
과 끝 부분
이다.
중간에는 추가할 수가 없다. 중간에는 계속해서 변경해야만 하는 변하는 특징을 가진 핵심 기능(Target)이 위치하기 때문이다. 그러나 시작과 끝 부분은 항상 똑같다. 그래서 코드가 실행 중이더라도 동적으로 또는 자동으로 추가될 수 있다.
즉, 코드가 실행 중에 자동으로 추가해야하는 조건 안에서
- 변하는 성격을 가진 핵심 기능이 위치한 중간에는 넣을 수 없음
- 항상 똑같은 시작과 끝 부분은 변하지 않는 성격을 가져 실행 중이더라도 자동 추가 가능
어느정도 예시를 통해 감이 왔을 것이다. 그럼 AOP 관련 용어에 대해 설명해보겠다.
advice 가 추가될 객체
target에 동적으로 추가될 부가 기능(코드)
advice가 추가(join)될 대상 메서드
ex : 핵심 기능 Target의 메서드 aaa, aaa2, bbb
join point 들을 정의한 패턴
ex : a로 시작하는 Target 메서드만 부가 기능 추가
target에 advice가 동적으로 추가되어 생성된 객체(실행중)
target에 advice를 추가해서 proxy를 생성하는 것
여기서 proxy에서 실행중 이란 단어를 붙였는데, 그림을 통해 자세히 설명하겠다.
더하기
와 같은 역할어떻게 보면 AOP는 SQL Join과 같다.
DB 설계 시 정규화 작업을 진행한다.
정규화는 테이블 쪼개는 것이라고 생각하면 쉽다. 그런데 왜 가만히 있는 테이블을 왜 쪼개는가?
중복된 데이터들을 각 테이블 성격에 맞게 나누기 위해서이다.
쪼개진 테이블, 2개의 테이블로 join 쿼리문을 통해 가상 테이블을 생성한다. 이 가상 테이블은 데이터 베이스의 객체는 아니다. join 쿼리문 실행 시 마치 하나 인것처럼 가상 테이블 생성할 뿐이다.
AOP도 똑같다. advice, target 클래스들이 쪼개져 있지만 실행하면 SQL join 처럼 마치 하나의 클래스처럼 사용된다.
사용 예시로,
해당 예시들은 advice의 역할을 한다. 공통적으로 사용된다는 의미로, 쇼핑몰, 헬스케어, 자동화 공정 도메인의 산업에서도 공통적으로 위 예시들이 쓰인다.
Advice 설정은
사용된다.
여기서 @AfterReturning과 @AfterThrowing 는 try catch 문에 사용된다.
*
-> 모든 타입(반환타입), 모든 대상(패키지,클래스, 메서드)..
-> 개수 상관없음(매개변수 목록)pointcut 를 사용해 target 메서드를 특정 패턴에 의해 제외를 할 수 있다.
join point 메서드 중 add 이름을 가진 대상만 적용하면
LoggingAdvice 클래스(advice)
@Component
@Aspect
public class LoggingAdvice {
// pointcut - 부가기능이 적용될 메서드의 패턴
@Around("execution(* com.fastcampus.ch3.aop.MyMath.add(..))")
// ProceedingJoinPoint pjp : 메서드의 정보들이 들어 있음.
public Object methodCallLog(ProceedingJoinPoint pjp) throws Throwable {
// Before advice
long start = System.currentTimeMillis();
/**
* pjp.getSignature().getName() : 메서드 이름
* Arrays.toString(pjp.getArgs()) : 메서드의 매개변수들
*/
System.out.println("<<[start] " + pjp.getSignature().getName() + Arrays.toString(pjp.getArgs()));
Object result = pjp.proceed(); // target의 메서드 호출 반환 값, target 대상
// After advice
System.out.println("result = " + result);
System.out.println("[end]>> " + (System.currentTimeMillis() - start) + "ms");
/**
* return result 사용한 이유?
*
* 이 advice가 여러 개 적용될 수 있고,
* 그 다음 advice 메서드에게 호출 결과 넘겨줘야 할 때 필요
* 만약, Advice가 한 개만 적용된다면, void 사용(return result 사용 안함)하면 됨.
*/
return result;
}
}
MyMath 클래스(target)
@Component
public class MyMath {
public int add(int a, int b) {
int result = a + b;
return result;
}
public int add(int a, int b, int c) {
int result = a + b + c;
return result;
}
public int subtract(int a, int b) {
int result = a -b ;
return result;
}
public int multiply(int a, int b) {
int result = a * b;
return result;
}
}
main 실행
public class AopMain2 {
public static void main(String[] args) {
ApplicationContext ac = new GenericXmlApplicationContext("file:src/main/webapp/WEB-INF/spring/**/root-context_aop.xml");
MyMath mm = (MyMath) ac.getBean("myMath");
System.out.println("mn.add(3, 5) = " + mn.add(3, 5));
System.out.println("mn.add(1, 2, 3) = " + mn.add(1, 2, 3));
System.out.println("mn.multiply(3, 5) = " + mn.multiply(3, 5));
}
}
결과
<<[start] add[3, 5]
result = 8
[end]>> 9ms
mn.add(3, 5) = 8
mn.multiply(3, 5) = 15
<<[start] add[1, 2, 3]
result = 6
[end]>> 0ms
mn.add(1, 2, 3) = 6
return result
왜 사용하는 지 설명하면,
advice는 여러 개가 적용될 수 있다. 그래서 그 다음 advice 메서드에게 호출결과 넘겨줘야하기 때문에 return result
사용한 것이다.(추가로, 반환타입은 Object)
그럼 advice가 1개일 경우에는 반환 타입 void, return result 사용 안하면 된다.
advice 가 여러 개일 때 @Order(0), @Order(1), @Order(2)... 사용해서 advice 순서를 적용할 수 도 있다.
그리고 우리는 주입 방식으로 사용하기 때문에 아래 애너테이션은 꼭 넣어줘야 한다.
@Aspect
과 같은 기능 사용하려면 AOP 관련된 설정을 해줘야 한다.
<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.fastcampus.ch3.aop"/>
추가 자료
Before 영역
TransactionManager
- DAO의 각 메서드는 개별 Connection을 사용하는데, TransactionManager 설정해주면 같은 connection 으로 지정
TransactionManager Bean 등록
TransactionManager 를 직접 생성하지 않고 root-context.xml Bean 등록
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
여기서 DataSourceTransactionManager 는
스프링에서JDBC
및mybatis
등의 JDBC 기반 라이브러리로 데이터베이스로 접근하는 경우 사용하는 인터페이스이다.@Transactional
- Transaction 속성정의
- 지정된 예외 처리rollbackFor
- Transaction 경계 설정
- Transaction의 isolation level 지정
- @Transactional 사용하려면, root-context.xml 등록
<tx:annotation-driven/>
After 영역
Commit(성공), Rollback(지정 예외 발생시)
출처 : 스프링의 정석 : 남궁성과 끝까지 간다