Spring - AOP

2022. 7. 19. 16:17Spring

728x90

애플리케이션은 다양한 공통기능이 필요하다. 로깅과 같은 기본적인 기능부터 보안, 트랙잭션과 같은 기능에 이르기까지 애플리케이션 전반에 걸쳐 적용 되는 공통기능이 필요하다.

이런 공통기능들은 공통 관심 사항이라 한다. 공통 관심 사항들을 객체 지향 기법으로 여러 모듈에 효과적으로 적용하는 데 한계가 있어 AOP 라는 기법을 사용한다.

 

 

AOP란?

Aspect Oriented Programming, 문제를 바라보는 관점을 기준으로 프로그래밍 하는 기법을 말한다.

 


AOP를 사용하면, 핵심 로직을 구현한 코드에 공통 기능 관련 코드가 포함되어 있지 않기 때문에 적용해야 할 공통 기능 관련 코드가 변경 되더라도 핵심 로직을 구현한 코드를 변경할 필요가 없다.

 


AOP 용어

 

  • Advice : 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의
  • JoinPoint : Advice를 적용 가능한 지점
  • PointCut : Joinpoint의 부분 집합으로 실제로 Advice가 적용되는 지점
  • Weaving : Advice를 핵심 로직 코드에 적용하는 것
  • Aspect : 공통 관심 사항

 

 

Weaving 방식

 

런타임 시에 Weaving하기

 

  1. 소스 코드나 클래스 정보 자체를 변경하지 않음
  2. 프록시를 이용하여 AOP를 적용, 프록시를 이용하여 핵심 로직을 구현한 객체에 접근
  3. 메서드를 호출할 때만 Advice를 적용할 수 있기 때문에 필드 값 변경과 같은 JoinPoint에 대해서는 적용할 수 없음

 

프록시 기반의 AOP 적용과정

 

 


 

프록시를 통한 AOP

 

스프링은 Aspect의 적용 대상이 되는 객체에 대한 프록시를 만들어 제공하고 있으며 대상 객체를 사용하는 코드는 대상 객체에 직접 접근하기 보다는 프록시를 통해서 간접적으로 접근하게 된다.

 

인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되어 있지 않은 메서드에 대해서는 AOP가 적요되지 않는다.

 

execution 명시자

 

Advice를 적용할 메서드를 명시할 때 사용한다.

 

execution(리턴타입패턴 패키지경로패턴.클래스이름패턴.메서드이름패턴(파라미터패턴))

 

  • 각 패턴은 "*"를 이용하여 모든 값을 표현할 수 있다.
  • "."을 이용해서 0개 이상이라는 의미를 표현할 수 있다.
  • 클래스이름 뒤에 +를 사용하여 해당 클래스로부터 파생된 모든 자식클래스 선택
  • 인터페이스 이름 뒤에 +를 사용하면 해당 인터페이스를 구현한 모든 클래스 선택

 

 

Advice

스프링에서는 어드바이스의 동작 시점을 "before", "after", "after-returning", "after-throwing", "around" 다섯가지로 지정할 수 있다.

종류 설명
Before Advice 대상 객체의 메서드 호출 전에 공통 기능을 실행한다.
After Returning Advice 대상 객체의 메서드가 예외 없이 실행한 이후에 공통 기능을 실행한다.
After Throwing Advice 대상 객체의 메서드를 실행하는 도중에 예외가 발생한 경우에 공통기능을 실행한다.
try-catch 블럭에서의 catch 블럭에 해당
After Advice 대상 객체의 메서드를 실행하는 도중에 예외가 발생했는 지의 여부와 상관없이 메서드 실행 후 공통 기능을 실행한다. 
try-catch 블럭의 fianlly와 비슷하다.
Around Adice 대상 객체의 메서드 실행 전 후 또는 예외 발생 시점에 공통 기능을 실행하는 데 사용한다.

 

어드바이스 메서드의 동작시점 <aop:before>, <aop:after>, <aop:after-returning>, <aop:after-throwing>, <aop:around> 엘리먼트를 이용하여 지정한다.

 

 

실제 적용 사례)

 

package sonny.spring.web.common;

public class LogAdvice {
	
	public void printLog() {
		System.out.println("[로그] : 비즈니스 로직 수행 전 동작");
	}
}

 

	<bean id = "log" class ="sonny.spring.web.common.LogAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* sonny.spring.web..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref = "log">
			<aop:before method = "printLog" pointcut-ref = "allPointcut"/>
		</aop:aspect>
	</aop:config>

 

Acpect의 적용대상이 되는 객체의 id : log

객체의 메서드 이름 : pringLog

적용되는 지점 : allPointcut

 

allPointcut으로 설정한 포인트컷 메서드가 호출될 때 log라는 어드바이스 객체의 printLog() 메서드가 실행되고 이때 메서드 동작 시점이 <aop:before>이다.

 

 


 

횡단 관심에 해당하는 어드바이스 메서드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메서드의 정보가 필요하다. 스프링에서는 이런 다양한 정보들을 이용할 수 있도록 JoinPoint 인터페이스를 제공한다.

 

JoinPoint 메서드

메서드 설명
Signature getSignature() 클라이언트가 호출한 메서드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장
된 Signature 객체를 리턴
Object getTarget() 클라이언트가 호출한 비즈니스 메서드를 포함하는 비즈니스 객체 리턴
Object[] getArgs()  클라이언트가 메서드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴

 

어드바이스들중 Around 어드바이스만이  proceed() 메서드를 사용하기 때문에 ProceedingJoinPoint 를 매개변수로 사용한다. 나머지 어드바이스들은 JoinPoint를 사용한다. 

 

Signature 메소드

메서드 설명
String getName() 클라이언트가 호출한 메서드 이름을 리턴
String toLongString() 클라이언트가 호출한 메서드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함하여 리턴
String toShortString() 클라이언트가 메서드를 메서드 시그니처를 축약한 문자열로 리턴

 

Joinpoint 객체를 사용하기 위해서는 단지 JoinPoint 어드바이스 메서드 매개변수를 선언만 하면 된다.

 

 

사례1) AfterReturningAdvice 메소드

 

위 메소드는 비즈니스 메서드가 수행되고 나서 결과 데이터를 리턴할 때 동작하는 어드바이스이다.

위 메소드의 두번째 매개변수를 바인딩 변수라 한다.

public class AfterReturningAdvice {
	public void afterLog(JoinPoint joinPoint, Object returnObj) {
		String method = joinPoint.getSignature().getName();
		if(returnObj instanceof UserVO) {
			UserVO user = (UserVO) returnObj;
			if(user.getRole().equals("admin")) {
				System.out.println(user.getName() + "로그인(Admin)");
			}
		}
		System.out.println("[사후처리] : " + method + "() 메서드의 리턴값 : " + returnObj.toString());
	}
}

 

<context:component-scan base-package = "sonny.spring.web"></context:component-scan>
	
	<bean id = "afterReturning" class ="sonny.spring.web.common.AfterReturningAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* sonny.spring.web..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref = "afterReturning">
			<aop:after-returning method = "afterLog" pointcut-ref = "allPointcut" returning="returnObj"/>
		</aop:aspect>
	</aop:config>

 

사례2) AfterThrowingAdvice 메소드

위 메소드는 예외가 발생하는 시점에 동작한다.

따라서 예외처리 어드바이스를 설정할 때 사용한다.

public class AfterThrowingAdvice {
	
	public void exceptionLog(JoinPoint joinPoint, Exception e) {
		String method = joinPoint.getSignature().getName();
		System.out.println("[예외처리] : " + "() 메서드 수행 중 발생된 예외 메세지 : " + e.getMessage());
	}
}

 

<context:component-scan base-package = "sonny.spring.web"></context:component-scan>
	
	<bean id = "afterThrowing" class ="sonny.spring.web.common.AfterThrowingAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* sonny.spring.web..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref = "afterReturning">
			<aop:after-returning method = "afterThrowingAdvice" pointcut-ref = "allPointcut" throwing="e"/>
		</aop:aspect>
	</aop:config>

 

 

<aop:aspect> 엘리먼트 : 핵심관심에 해당하는 포인트컷 메서드횡단관심에 해당하는 어드바이스 메서드를 결합하기 위해 사용한다.

 

<aop:advisor> 엘리먼트 : 포인트컷과 어드바이스를 결합한다는 의미에서 애스펙트와 같은 기능이다.

하지만, <aop:advisor>는 <aop:aspect>와 다르게 객체의 id를 모르거나 메소드 이름을 확인할 수 없을 때 사용한다.(<aop:aspect>는 객체의 id와 메서드 이름을 알아야한다.)

 


어노테이션

 

 

만약, AfterReturning 메소드, AfterThrowing 메소드등을 같이 사용한다고 가정하자.

그렇다면 xml은 같은 의미의 코드를 반복하게 된다.

이러한 단점을 극복하기 위해서 어노테이션을 사용한다.

 

AfterReturning 메소드를 어노테이션을 활용하여 나타내면 아래와 같다.

 

	<context:component-scan base-package = "sonny.spring.web"></context:component-scan>
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Service
@Aspect
public class AfterReturningAdvice {
	
	
	@AfterReturning(pointcut = "PointcutCommon.getPointcut()", returning = "returnObj")
	public void afterLog(JoinPoint joinPoint, Object returnObj) {
		String method = joinPoint.getSignature().getName();
		if(returnObj instanceof UserVO) {
			UserVO user = (UserVO) returnObj;
			if(user.getRole().equals("admin")) {
				System.out.println(user.getName() + "로그인(Admin)");
			}
		}
		System.out.println("[사후처리] : " + method + "() 메서드의 리턴값 : " + returnObj.toString());
	}
}

<aop:aspectj-autoproxy> 는 AOP 관련 어노테이션을 인식하고 용도에 맞게 처리해준다.

 

xml 파일에서 <context:component-scan> 엘리먼트의 의미는 객체를 생성한다는 의미니다.

 

@Service는 @Component를 상속하는 어노테이션으로 어떤 객체를 생성해야 될 지 타겟을 구체적으로 정하는 역할을 한다.

 

@AfterReturning(pointcut = "PointcutCommon.getPointcut()", returning = "returnObj")의 의미는 <aop:aspect> 태그 이다.

 

 

 

 

이상입니다.

728x90

'Spring' 카테고리의 다른 글

Spring MVC 구조  (0) 2022.07.26
Spring - 트랜잭션 처리  (0) 2022.07.21
의존성 관리 -2  (0) 2022.07.15
의존성 관리-1  (0) 2022.07.14
스프링 XML 설정  (0) 2022.07.14