🌿Spring AOP
- Aspect Oriented Programming
관점 지향 프로그래밍(AOP)은 애플리케이션의 핵심 비즈니스 로직에서 분리된 관심사(Concern)를 모듈화 하여 코드의 가독성과 유지보수성을 향상하는 기술이다.
관점(관심사)은 업무 구현 시 주 업무가 아닌 나머지 보조 업무를 의미한다.
주 업무와 보조 업무
게시판 글쓰기를 예로 들어 주 업무와 보조 업무를 구분해 보도록 하자.
1. 기존 방식
- add.do
a. 글쓰기 (주 업무)
b. 권한 체크 (보조 업무)
기존 방식은 주 업무와 보조 업무를 같은 곳(한 파일)에서 구현한다.
단점: 코드 관리가 불편하다. 어디부터 어디까지가 주 업무이고 보조 업무인지 알기 어렵다.
2. AOP 방식
- add.do
주 업무와 보조 업무를 물리적으로 분리시켜서 따로 구현한다.
스프링을 통해서 주 업무와 보조 업무를 다시 결합 후 동작한다. 결과적으로 코드 관리가 훨씬 용이하며, 코드를 분리하기 때문에 재사용이 가능해진다.
이걸 스프링이 하기 때문에 Spring AOP라고 부르는 것이다.
Spring AOP 용어
Core Concern
- 비즈니스 코드, 주 업무
Cross-cutting Concern
- 보조 업무
그림과 같이 주 업무 사이에 보조업무가 사이에 끼어들듯이 끊어서 들어온다고 해서 Cross-cutting Concern이라고 부른다.
Target
- 비즈니스 업무를 소유한 객체
Proxy
- Target을 감싸는 대리 객체
JointPoint
- Target이 가진 주 업무(메서드)
Advice가 적용될 수 있는 특정한 실행 지점을 나타난다.
메서드 호출, 객체 생성과 같은 지점들이 조인포인트이다.
Pointcut
- 보조 업무와 특정 JointPoint를 연결하는 작업
Advice가 적용될 메서드의 위치를 지정한다.
특정 메서드 호출, 객체 생성, 필드 접근 등이 될 수 있다.
Aspect
- 보조 업무를 구현하는 객체
Advice
- 주 업무가 시행되는 어느 시점에 보조 업무를 실행할지 결정하는 작업
a. Before Advice
b. After Advice
c. Around Advice
d. After-returning Advice
e. After-throwing Advice
Aspect에서 어떤 특정한 시점에서 실행될 코드를 나타낸다.
🌿Spring AOP 구현
- com.test.spring.aop1 패키지
- Main.java: 메인
- Memo.java (I): 메인 업무 객체(인터페이스)
- MemoImpl.java (C): 메인 업무 객체(클래스)
- Logger.java (C): 보조 업무 객체(클래스)
- memo.xml: 스프링 설정 파일
메모장을 만들면서 Spring AOP에 대해 알아보도록 하자.
Memo.java에서 먼저 인터페이스를 구성해 보도록 한다.
인터페이스 생성
인터페이스를 만드는 게 AOP에서 반드시 해야 하는 업무는 아니지만, 하나의 계층에서 또 다른 계층으로 참조를 할 때 다음과 같은 작업을 한다.
Controller <-> DAO, Controller <-> View와 같이 계층과 계층 간의 연결을 할 때 클래스를 만들어서 인스턴스화시키는 게 아니라 반드시 인스턴스를 상속받고, 참조변수를 인스턴스로 만든다.
IBoardDAO dao = new BoardDAO();
dao.add(dto)
서로의 역할을 고정하기 위해서는 인터페이스를 만드는 게 보편적인 관례이다.
하나의 클래스에서 다른 클래스로 접근하기 위해서 인스턴스를 만들고 호출하는 과정에서는 dao에 대한 부모 인터페이스를 만들고, 참조 변수를 인터페이스로 만든다.
Memo.java (I): 인터페이스 설계
package com.test.spring.aop1;
public interface Memo {
//메모 쓰기
void add(String memo);
//메모 읽기
String read(int seq);
//메모 수정
boolean edit(int seq, String memo);
//메모 삭제
boolean del(int seq);
}
구현할 기능을 추상 메서드 add, read, edit, del을 미리 만들어서 인터페이스 설계를 마쳤다.
꼭 인터페이스가 필요하다기보다도 이렇게 설계 작업을 먼저 하면 구현할 코드가 훨씬 명확해진다.
MemoImpl.java (C): 메모장 주 업무 구현
package com.test.spring.aop1;
public class MemoImpl implements Memo {
@Override
public void add(String memo) {
System.out.println("메모 쓰기: " + memo);
}
@Override
public String read(int seq) {
return "메모입니다.";
}
@Override
public boolean edit(int seq, String memo) {
System.out.println("메모 수정: " + memo);
return true;
}
@Override
public boolean del(int seq) {
System.out.println("메모 삭제: " + seq);
return true;
}
}
메모장 기능을 구현했다는 가정 하에 위와 같이 작업을 완료했다.
이제 CRUD가 잘 구현이 되었는지 Main 메서드에서 테스트한다.
Main.java: 구현 테스트
package com.test.spring.aop1;
public class Main {
public static void main(String[] args) {
//주 업무 객체
Memo memo = new MemoImpl();
//메모 쓰기
memo.add("Spring AOP");
//메모 읽기
String txt = memo.read(5);
System.out.println(txt);
//메모 수정
memo.edit(5, "수정합니다.");
//메모 삭제
memo.del(5);
}
}
이렇게 메모장 구현이 끝났는데, 지금 했던 업무에 보조적인 성격으로 코드 주문이 새롭게 들어왔다고 하자.
예로 들어 이 4가지 주 업무를 할 때마다 업무가 언제 발생해야 하는지 기록을 해야 하는 보조 업무가 추가된 것이다.
MemoImpl.java (C): 메모장 보조 업무 구현?
public class MemoImpl implements Memo {
@Override
public void add(String memo) {
System.out.println("메모 쓰기: " + memo);
//로그 기록
Calendar now = Calendar.getInstance();
System.out.printf("[LOG][%tF %tT] 로그를 기록합니다.\n", now, now);
}
}
주 업무를 보조 업무와 함께 나타나게 하면 구현에는 무리가 없지만 뒤죽박죽 엉키게 된다.
이때 AOP를 사용할 수 있다. AOP는 주 업무와 보조 업무를 분리해서 구현하는 게 모토이다.
Logger.java (C): 메모장 보조 업무 구현
package com.test.spring.aop1;
import java.util.Calendar;
//보조 업무 객체
public class Logger {
//보조 업무 구현
public void log() {
Calendar now = Calendar.getInstance();
System.out.printf("[LOG][%tF %tT] 로그를 기록합니다.\n", now, now);
}
}
보조 업무 객체를 만들어서 모듈화를 시켰다.
이제 주 업무와 보조 업무를 결합해야 한다. 이 업무는 xml 파일에서 한다.
memo.xml: 주 업무와 보조 업무 결합
xml에서의 Namespace(태그의 집합)는 Java로 치면 Pachage(클래스의 집합)이다.
태그의 집합으로 모아놓고 이름을 정해둔 것을 Namespace라고 한다.
기본적으로 beans만 되어 있는데, aop코드가 필요하므로 aop를 추가해 주어야 한다.
xmlns:aop="http://www.springframework.org/schema/aop"
<aop:태그명></aop:태그명>
Namespace 하나당 태그가 잔뜩 들어있다. 어느 네임스페이스에 들어 있는 태그인지 구분해서 사용해야 하므로, 태그명 앞에 별칭을 붙여서 사용하면 된다.
<c:if></c:if>와 같이 다른 클래스와 구분하기 위한 식별자라고 생각하면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!--
Spring AOP
주 업무 객체: MemoImpl
보조 업무 객체: Logger
MemoImpl + Logger 결합
-->
<bean id="memo" class="com.test.spring.aop1.MemoImpl"></bean>
</beans>
스프링이 특정 객체(클래스)를 인지하려면 반드시 <bean>으로 선언해야 한다.
인식을 못 하면 Spring의 혜택을 받을 수 없는데, 이 방법이 아니면 절대로 클래스를 인식할 수 없다.
aop:config에서 스프링이 주 업무와 보조 업무를 인식할 수 있도록 역할을 지정한다.
먼저 aop:aspect가 보조 업무라는 것을 지정한다. 기존에 만든 Logger를 ref로 연결해 주었다.
그리고 스프링이 주 업무의 어떤 메서드와 결합해야 하는지 알게 해야 한다. 이런 일련의 과정을 PointCut을 지정한다고 하며, AspectJ 표현식을 사용한다.
🍃AspectJ 표현식
- execution() 지시자
- within() 지시자
이는 메서드를 검색하는 도구이다.
보통 execution() 지시자를 사용한다.
execution()
execution([접근지정자] 반환형 [클래스].메서드(인자))
execution(public void com.test.spring.aop1.MemoImpl.add(String))
execution는 자바의 메서드 선언부와 비슷하게 작성한다.
와일드카드
- * : 와일드카드 (접근지정자, 반환형, 패키지, 클래스, 메서드)
- .. : 와일드카드 (인자)
PointCut 생성
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!--
Spring AOP
주 업무 객체: MemoImpl
보조 업무 객체: Logger
MemoImpl + Logger 결합
-->
<!-- 주 업무 객체 -->
<bean id="memo" class="com.test.spring.aop1.MemoImpl"></bean>
<!-- 보조 업무 객체 -->
<bean id="Logger" class="com.test.spring.aop1.Logger"></bean>
<!--주 업무 객체 + 보조 업무 객체 결합 :: AOP -->
<aop:config>
<!-- 보조 업무를 담당할 객체를 지정: 역할 지정 -->
<aop:aspect id="LoggerAspect" ref="Logger">
<!--
PointCut 지정하기
- 주 업무 객체 지정 > 메서드 찾기
MemoImpl.add 메서드
MemoImpl.read 메서드
AspectJ 표현식 사용
- execution() 지시자
- within() 지시자
-->
<aop:pointcut expression="execution(public void com.test.spring.aop1.MemoImpl.add(String))" id="p1"/>
<!--
보조 업무 객체(Logger) + 포인트컷(MemoIml.add)
- 결합 > 위빙(Weaving) > 5종류의 Advice 중 하나 구현
-->
<aop:after method="log" pointcut-ref="p1"/>
</aop:aspect>
</aop:config>
</beans>
포인크컷을 생성할 때 인자는 변수명은 빼고 자료형을 적는다.
결합하는 것을 위빙이라고 한다. 이때 5종류의 Advice 중에 하나를 구현한다.
메서드가 동시에 실행될 수는 없으므로 after가 주 업무를 실행한 다음에 보조 업무를 실행해 달라는 의미로 사용된다.
하지만 실행을 해도 제대로 출력되지 않는다. 이는 우리가 직접 객체를 만들었기 때문이다.
스프링이 직접 객체를 만들게 해야 스프링의 여러 가지 혜택을 적용받을 수 있다.
pom.xml
<!-- AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
AOP를 사용하려면 jar파일을 몇 개 가져오도록 pom.xml에 추가해야 한다.
Main.java: 객체를 스프링이 만들도록 위임
package com.test.spring.aop1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
//주 업무 객체
//Memo memo = new MemoImpl();
ApplicationContext context = new ClassPathXmlApplicationContext("com/test/spring/aop1/memo.xml");
Memo memo = (Memo)context.getBean("memo"); //= new MemoImpl();
//메모 쓰기
memo.add("Spring AOP");
//메모 읽기
String txt = memo.read(5);
System.out.println(txt);
//메모 수정
memo.edit(5, "수정합니다.");
//메모 삭제
memo.del(5);
}
}
코드를 분리하기 때문에 <aop:after method="log" pointcut-ref="p1"/>를 지우면 로그가 발생하지 않는다.
<aop:before method="log" pointcut-ref="p1"/>
Advice를 어떤 것으로 하느냐에 따라서 주 업무와 보조 업무의 실행 순서를 바꿀 수 있다.
memo.xml: 다대다 관계 형성
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!--
Spring AOP
주 업무 객체: MemoImpl
보조 업무 객체: Logger
MemoImpl + Logger 결합
-->
<!-- 주 업무 객체 -->
<bean id="memo" class="com.test.spring.aop1.MemoImpl"></bean>
<!-- 보조 업무 객체 -->
<bean id="Logger" class="com.test.spring.aop1.Logger"></bean>
<!--주 업무 객체 + 보조 업무 객체 결합 :: AOP -->
<aop:config>
<!-- 보조 업무를 담당할 객체를 지정: 역할 지정 -->
<aop:aspect id="LoggerAspect" ref="Logger">
<!-- PointCut 지정하기 -->
<aop:pointcut expression="execution(public void com.test.spring.aop1.MemoImpl.add(String))" id="p1"/>
<aop:pointcut expression="execution(public String com.test.spring.aop1.MemoImpl.read(int))" id="p2"/>
<aop:pointcut expression="execution(public String com.test.spring.aop1.MemoImpl.read(int)) ||
execution(public void com.test.spring.aop1.MemoImpl.add(String))" id="p3"/>
<aop:pointcut expression="execution(public void add(String))" id="p4"/>
<aop:pointcut expression="execution(* com.test.spring.aop1.MemoImpl.*(..))" id="p5"/>
<!-- 위빙(Weaving) -->
<!-- <aop:after method="log" pointcut-ref="p1"/> -->
<!-- <aop:before method="log" pointcut-ref="p1"/> -->
<!-- <aop:after method="log" pointcut-ref="p1"/> -->
<!-- <aop:after method="log" pointcut-ref="p2"/> -->
<!-- <aop:after method="log" pointcut-ref="p3"/> -->
<!-- <aop:after method="log" pointcut-ref="p4"/> -->
<aop:after method="log" pointcut-ref="p5"/>
</aop:aspect>
</aop:config>
</beans>
글 쓰기를 할 때 뿐만 아니라 다른 업무를 할 때에도 보조 업무가 실행되도록 했다.
Or 연산자를 사용하여 하나의 포인트컷에서 여러 개의 메서드를 찾을 수 있다. 복잡하긴 하지만 같은 업무이므로 한 번에 찾아서 사용하는 편이다.
패키지명 생략
<aop:pointcut expression="execution(public void add(String))" id="p4"/>
보조 업무와 같은 패키지인 경우에 한해서 패키지명을 생략할 수 있다.
와일드카드 사용
<aop:pointcut expression="execution(* com.test.spring.aop1.MemoImpl.*(..))" id="p5"/>
와일드카드 '*'를 사용하여 모든 메서드를 선택할 수 있다.
인자 리스트에서의 와일드카드는 '..'를 사용한다.
이름을 'a*'라고 한다면 add, addAll이 있는 경우를 모두 찾으며, '*a'라고 하면 a로 끝나는 메서드를 찾고, 'a*a'라고 하면 a로 시작해서 a로 끝나는 메서드를 찾는다.
around
package com.test.spring.aop1;
import java.util.Calendar;
import org.aspectj.lang.ProceedingJoinPoint;
//보조 업무 객체
public class Logger {
//보조 업무 구현
public void log() {
Calendar now = Calendar.getInstance();
System.out.printf("[LOG][%tF %tT] 로그를 기록합니다.\n", now, now);
}
public void time(ProceedingJoinPoint jp) {
//주업무를 실행하는 소요시간
long begin = System.nanoTime();
System.out.println("[LOG] 기록을 시작합니다.");
//주업무 실행
//- 글쓰기 > 주 업무 객체의 가상 객체 참조 (프록시 객체)
try {
jp.proceed(); //memo.add(), memo.read(), memo.edit
} catch (Throwable e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("[LOG] 기록을 종료합니다.");
System.out.printf("[LOG] 소요 시간 %,dns\n", end - begin);
}
}
<aop:around method="time" pointcut-ref="p1"/>
주업무를 감싸듯이 주업무의 전과 후에 개입된다고 해서 around-advise라고 부른다.
반드시 중간에 주업무가 끼어들어야 하는데, 대리자를 대신 넣는 형태로 구현하게 된다.
after-returning
public void history(String memo) {
System.out.println("[LOG] 읽기 기록 > " + memo);
}
<aop:after-returning method="history" pointcut-ref="p2" returning="memo"/>
returning에 history 메서드에 전달할 매개변수의 이름을 적는다.
주 업무를 실행하고 난 다음에 리턴값이 있는데, 이를 보조 업무로 넘길 때 사용하는 어드바이스 형태이다.
history 메서드의 매개변수로 전달하여 로그에 전달할 수 있다.
after-throwing
//메모 읽기
try {
String txt = memo.read(11);
System.out.println(txt);
} catch (Exception e) {
e.printStackTrace();
}
//메모 읽기
String read(int seq) throws Exception;
@Override
public String read(int seq) throws Exception {
if (seq < 10) {
System.out.println("메모 읽기");
} else {
throw new Exception("존재하지 않는 메모");
}
return "메모입니다.";
}
메모 읽기를 예외 미루기로 하고, seq < 10이 아닌 경우에 존재하지 않는 메모로 예외를 발생시켰다.
public void check(Exception e) {
System.out.println("[LOG] 예외 발생: " + e.getMessage());
//관리자 연락처
}
<aop:after-throwing method="check" pointcut-ref="p2" throwing="e"/>
보통 이 부분에 관리자 연락처를 남긴다.
try-catch는 에러가 나는 것을 어느 정도 수습을 하지만, after-throwing는 로그를 호출하는 기능만을 한다.