핵심 비즈니스 로직 외에 비즈니스 메소드마다 필요한 로깅, 예외, 트랜잭션 처리같이 매번 반복되는 코드를 효율적으로 관리하기 위해서 관심분리(Separation of Concerns)를 통해 정리하는 것
메소드마다 공통으로 등장하는 로깅, 예외, 트랜잭션 처리 같은 코드
사용자의 요청에 따라 실제로 수행되는 핵심 비즈니스 로직
AOP는 이 두 횡단 관심, 핵심 관심을 분리해 더욱 간결하고 응집도 높은 코드를 유지한다.
클래스 OO의 모든 메서드가 로그 출력 기능을 필요로 한다고 가정하자.
또한, 이 기능은 다른 클래스에서도 자주 사용될 수 있다.
재사용을 위해 OO 클래스 내부에 구현하지 않고, A 클래스의 print() 메서드로 만들었다.
print() 메서드를 호출public class OO {
private A a;
public OO() {
a = new A();
}
public void method1() {
a.print();
}
public void method2() {
a.print();
}
}
OO 클래스에서만 특화된 기능을 추가하거나,
공통 기능인 print() 메서드 이름을 printLog()로 변경하려고 하면,
아직도 전체 코드를 수정해야 하는 문제가 생기는데 AOP는 이것을 해결한다.
AOP를 적용하기 위해 필요한 라이브러리로, aspectj와 aspectjweaver를 추가한다.
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${사용할 버전정보}</version>
</dependency>
<!-- Aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectjweaver-version}</version>
</dependency>
AspectJ Runtime
역할: AspectJ 기반 코드를 실행할 때 필요한 클래스들
@Aspect 같은 어노테이션, JoinPoint, Advice 인터페이스 등 코드를 실행하는 데 필요한 기본 런타임, 주로 Spring AOP와 함께 기본적으로 Advice를 처리할 때 필요
역할: 실제 바이트코드에 Aspect를 적용하는 도구
Spring AOP에서 <aop:config>와 같은 AspectJ 스타일 포인트컷을 적용할 때 필요
런타임에 클래스의 바이트코드를 조작해서 Advice를 연결
클라이언트가 호출하는 모든 비즈니스 메서드 (서비스 구현체의 메서드)
포인트컷 대상, 포인트컷 후보 (Joinpoint -> Pointcut)
필터링된 조인포인트, 어디서 AOP를 적용할지 결정
예를 들어 트랜잭션 처리 횡단 관심 기능은 등록, 수정, 삭제 외의 '검색 기능'에는 필요하지 않는데, 이처럼 원하는 특정 메서드에만 횡단관심에 해당하는 공통 기능을 수행시키기 위해 포인트컷을 이용한다.
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.spring.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.spring.biz..*Impl.get*(..))"/>
...
</aop:config>
allPointcut은 Impl 클래스의 모든 메서드를 포함하고, getPointcut은 Impl 클래스의 메서드 중 get메서드만 포함한다.
<aop:pointcut/>포인트컷 엘리먼트
id 포인트컷 식별 문자열 속성
expression 필터링되는 메서드를 결정하는 표현식, 아래와 같은 형식으로 선언한다.
<리턴타입> <패키지경로><클래스명>.<메소드명(매개변수)>
| 심볼 | 위치/대상 | 의미 | 예시 |
|---|---|---|---|
* | 패키지 이름 | 한 단계 하위 패키지 이름 매칭 | com.example.* → service, repository, util |
* | 클래스/인터페이스 이름 | 이름 패턴 | *Service → 이름이 Service로 끝나는 클래스 |
* | 메서드 이름 | 이름 패턴 | get* → get으로 시작하는 메서드 |
* | 반환 타입 | 모든 타입 | * → 모든 리턴 타입 허용 |
.. | 패키지 경로 | 0개 이상의 하위 패키지 포함 | com.example.. → com.example.service, com.example.service.user 등 |
.. | 메서드 파라미터 | 0개 이상의 파라미터 포함 | (String, ..) → 첫 파라미터 String, 나머지 0개 이상 |
+ | 클래스/인터페이스 타입 | 자신 + 모든 하위 타입 포함 (자식 클래스/구현체) | MyService+ → MyService + 모든 구현체/자식 클래스 |
! | 타입/클래스/메서드 | 제외 | execution(* !com.example..MyService.*(..)) → MyService 제외 |
횡단 관심, 공통 코드 자체. 즉, AOP에서 무엇을 할지에 해당한다.
public class LogAdvice {
public void printLog() { // Advice (공통 로직 메서드)
System.out.println("[공통] 비즈니스 로직 수행 전 동작");
}
}
스프링 설정 파일에서 어드바이스 타입으로 어드바이스가 언제 동작할지 결정한다.
<aop:config>
...
<aop:aspect ref="log">
<aop:[AdviceType] pointcut-ref="allPointcut" method="printLog"/>
</apo:aspect>
...
</aop:config>
비즈니스 메서드 실행 전 동작
비즈니스 메서드가 실행된 후 무조건 실행 (finally)
비즈니스 메서드가 성공적으로 리턴되면 동작
비즈니스 메서드 실행 중 예외가 발생하면 동작 (catch)
메서드 호출 자체를 가로채 비즈니스 메서드 실행(ProceedingJoinPoint.proceed()) 전후에 동작
포인트컷으로 지정한 핵심 관심 메서드가 호출될 때 어드바이스 메서드가 삽입되는 과정
위빙을 통해 비즈니스 메서드를 수정하지 않고 횡단관심에 해당하는 기능을 추가하거나 변경할 수 있다.
스프링에서는 런타임 위빙 방식만 지원한다.
Pointcut + Advice
포인트컷으로 지정된 메서드가 호출될 때 어떤 어드바이스 메서드가 언제 삽입되는지 결정
Aspect에 따라 Weaving이 처리된다.
public class LogAdvice { // Aspect (공통 로직을 담는 클래스)
public void printLog() { // Advice (공통 로직 메서드)
System.out.println("[공통] 비즈니스 로직 수행 전 동작");
}
}
아래 설정에 따르면 어플리케이션이 실행되며 여러 조인포인트 중
getPointcut으로 지정된 get~() 메서드가 호출되면
메서드 실행 전(before)에
log 객체인 LogAdvice클래스의
printLog() 메서드가 실행된다.
<bean id="log" class="com.spring.biz.common.LogAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.spring.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.spring.biz..*Impl.get*(..))"/>
<!-- Aspect -->
<aop:aspect ref="log">
<aop:before pointcut-ref="getPointcut" method="printLog"/>
</apo:aspect>
</aop:config>
<aop:aspect> 대신 <aop:advisor> 엘리먼트를 사용해 설정하는 경우도 있는데,
이미 만들어진 Advice 객체를 Pointcut과 묶어줄 때 사용한다.
아래와 같이 주로 스프링 컨테이너가 <tx:advice/>설정을 참조해 자동으로 advice를 생성하는 트랜잭션 설정에서 사용한다.
<!-- Transaction 관리자 객체 컨테이너에 등록 -->
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"></property>
</bean>
<!-- Transaction advice 객체 컨테이너에 등록 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- 트랜잭션을 적용할 메서드 지정 -->
<!--get으로 시작하는 메서드는 읽기 전용으로 트랜잭션 관리대상에서 제외-->
<tx:method name="get*" read-only="true"/>
<!--나머지 메서드를 트랜잭션 관리에 포함(동일한 메서드가 여러 패턴에 매칭될 수 있다면 위에서부터 먼저 매칭된 규칙이 적용)-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.spring.biz..*Impl.*(..))"/>
<!-- 어드바이스 객체와 메서드의 이름이 필요한 aop:aspect 대신 aop:advisor 사용-->
<aop:advisor pointcut-ref="allPointcut" advice-ref="txAdvice"/>
</aop:config>
이 때 트랜잭션 어드바이스는 Advice Type이 around로 정해져 있고
Spring이 알아서 트랜잭션 시작/커밋/롤백 시점을 around advice 내부에서 처리한다.
name 트랜잭션이 적용될 메서드 이름
read-only 읽기전용여부 (기본 false)
no-rollback-for 트랜잭션을 롤백하지 않을 예외 지정
rollback-for 트랜잭션을 롤백할 예외 지정
어노테이션 기반으로 트랜잭션을 적용할 때 사용한다.
rollbackFor 트랜잭션을 롤백시키는 예외 클래스를 지정
isolation 트랜잭션 격리 수준 지정
transactionManager 트랜잭션 매니저를 지정
...
<!--하나의 트랜잭션 매니저만을 사용할 때 :
<tx:annotation-driven transaction-manager="txManager">
Service구현체에서 transactionManager를 지정하지 않는다.
-->
<!--여러 개의 트랜잭션 매니저를 사용할 때-->
<tx:annotation-driven/>
<bean id="txManager1" class="org.springframework...">
</bean>
<bean id="txManager2" class="org.springframework...">
</bean>
@Service
public class ServiceImpl implements SomeService {
@Transactional(transactionManager="txManager2")
}
@Transactional 어노테이션을 메서드에 선언하면 해당 메서드는 트랜잭션 안에서 실행된다.
(해당 메서드를 트랜잭션 Advice가 동작되는 포인트컷에 포함시킨다.)
어드바이스 메서드가 호출될 때 실행되는 비즈니스 메서드(JoinPoint)에 대한 정보를 제공하는 인터페이스
어드바이스가 실행되는 시점의 메서드, 클래스, 패키지, 전달된 인자 등 실행 컨텍스트를 확인하고 활용할 수 있다.
클라이언트가 비즈니스 메서드를 호출하면, 스프링은 런타임에 JoinPoint 객체를 생성하고, 호출 정보를 담아 어드바이스 메서드의 매개변수로 전달한다.
| 메서드 시그니처 | 소속 클래스 | 설명 | 비고 |
|---|---|---|---|
Signature JoinPoint.getSignature() | JoinPoint | 호출된 메서드에 대한 정보를 반환 | Signature 객체에서 메서드 이름, 선언 클래스, 반환 타입 등을 확인 가능 |
String Signature.getName() | Signature | 호출된 메서드 이름 반환 | getSignature().getName() |
Class<?> Signature.getDeclaringType() | Signature | 메서드를 선언한 클래스 타입 반환 | getSignature().getDeclaringType() |
String Signature.toShortString() | Signature | 메서드 이름과 매개변수 타입을 간략히 문자열로 반환 | MemberService.getMember(Long) |
String Signature.toLongString() | Signature | 메서드 이름, 매개변수, 반환 타입 등을 포함해 상세 문자열로 반환 | public Member com.example.service.MemberService.getMember(Long) |
Object JoinPoint.getTarget() | JoinPoint | 실제 비즈니스 객체(원본 객체) 반환 | 프록시가 아닌 실제 객체 |
Object JoinPoint.getThis() | JoinPoint | 프록시 객체 반환 | 현재 어드바이스가 적용된 객체 |
Object[] JoinPoint.getArgs() | JoinPoint | 메서드 호출 시 전달된 인자 배열 반환 | 배열 형태로 인자 확인 가능 |
참고 : Spring AOP는 proxy-based AOP이다.
getThis()의 프록시는 비즈니스 로직의 소스를 수정하지 않고 Aspect기능을 적용하기 위한 일종의 래퍼 객체이다. 컨테이너 구동 시점에 미리 생성된다. (Eager instantiation)
JoinPoint 인터페이스를 상속한 인터페이스
Around 어드바이스에서만 사용하며 실제 비즈니스 메서드 실행을 제어하는 proceed() 메서드를 제공한다.
바인드 변수란, 어드바이스 메서드에서 메서드 실행 결과나 예외 객체를 전달받기 위해 지정하는 변수이다.
AfterReturning과 AfterThrowing에서만 사용한다.
@AfterReturning(pointcut="execution(* com.example..*Service.*(..))", returning="result")
public void logResult(JoinPoint jp, Object result) {
System.out.println("메서드 " + jp.getSignature().getName() + " 반환값: " + result);
}
@AfterThrowing(pointcut="execution(* com.example..*Service.*(..))", throwing="ex")
public void logException(JoinPoint jp, Exception ex) {
System.out.println("메서드 " + jp.getSignature().getName() + " 예외: " + ex.getMessage());
}
| 어드바이스 타입 | XML 설정 | 어노테이션 설정 | 바인드 변수 설명 |
|---|---|---|---|
| AfterReturning | <aop:after-returning returning="변수명" ...> | @AfterReturning(returning="변수명") | 메서드 실행 결과를 어드바이스 매개변수에 바인딩 |
| AfterThrowing | <aop:after-throwing throwing="변수명" ...> | @AfterThrowing(throwing="변수명") | 예외 객체를 어드바이스 매개변수에 바인딩 |
스프링 설정파일에 <aop:aspectj-autoproxy> 엘리먼트를 선언하면 스프링 컨테이너가 AOP 관련 어노테이션을 인식하고 처리해준다.
<beans xmlns="" ...>
<!--어드바이스 클래스에 AOP 어노테이션을 작성한다.
컨테이너가 어드바이스 객체를 찾을 수 있게한다. (@Service 사용 또는 bean 등록)-->
<!-- @ComponentScan(basePackages = "...") -->
<context:component-scan base-package="..."></context:component-scan>
<!-- AOP 관련 어노테이션 사용 설정-->
<!-- @EnableAspectJAutoProxy -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
어노테이션 설정으로 포인트 컷을 선언할 때 사용하며 하나의 클래스 안에 여러 개의 포인트컷을 선언할 수 있다.
구현로직 없이 몸체가 비어있는 참조 메서드를 사용해 포인트컷을 식별하는 이름으로 사용한다.
어드바이스 클래스마다 포인트컷이 반복 선언되는 문제를 해결하고자 외부에 독립된 클래스로 따로 설정한다.
import org.aspectj.lang.annotation.Pointcut;
@Service
public class CommonPointcuts {
@Pointcut("execution(* com.spring.biz..*Impl.*(..))")
public void allPointcut(){}
@Pointcut("execution(* com.spring.biz..*Impl.get*(..))")
public void getPointcut(){}
}
해당 클래스는 Aspect니까 내부 어노테이션(@Before, @After...)을 AOP Advice로 등록하라는 표시
횡단 관심에 해당하는 어드바이스 메서드가 구현되어있는 어드바이스 클래스에 메서드가 동작할 포인트컷을 참조 메서드의 이름으로 지정해준다.
@Aspect
@Service
public class LogAdvice {
// 문자열로 참조 메서드의 괄호까지 꼭 써준다.
@Before("CommonPointcuts.allPointcut()")
public void printLog() {
System.out.println("[Before] 비즈니스 로직 수행 전");
}
@AfterReturning(pointcut="CommonPointcuts.getPointcut()", returning="returnObj")
public void afterLog(JoinPoint jp, Object returnObj) {
System.out.println("[AfterReturning] " + jp.getSignature().getName()) +"() 메소드 리턴값: " + returnObj.toString());
}
@AfterThrowing(pointcut="CommonPointcuts.allPointcut()", throwing="exeptObj")
public void exceptionLog(JoinPoint jp, Exception exeptObj) {
System.out.println("[AfterThrowing]" + jp.getSignature().getName() + "() 메서드 수행 중 예외발생! ");
if(exceptObj instanceof someException) {System.out.println("어떤어떤 문제가 발생했습니다.")}
}
@After("CommonPointcuts.allPointcut()")
public void finallyLog() {
System.out.println("[After] 비즈니스 로직 수행 후 무조건 동작");
}
@Around("CommonPointcuts.allPointcut()")
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("[Around] 비즈니스 로직 수행 전");
Object obj = pjp.proceed();
System.out.println("[Around] 비즈니스 로직 수행 후");
return obj;
}
}
@EnableAspectJAutoProxy 또는 <aop:aspectj-autoproxy/> 확인
애플리케이션 컨텍스트에서 @Aspect가 붙은 빈만 검색
@Aspect 클래스 안의 @Pointcut, @Before, @After... 메서드 파싱
AOP 프록시 생성 및 Advice 연결