본문 바로가기
Spring

[AOP] xml을 @로 바꾸기 : 횡단관심_aop(2)

by amoomar 2022. 4. 5.
반응형

 

목차는 다음과 같다.

1. AOP_로깅_xml
   1) JoinPoint
   2) returnObj
   3) excepObj
   4) ProceedingJoinPoint
   5) 예제

2. @(어노테이션)으로 변경하기
   1) 변경
   2) Pointcut클래스 분리
   3) 예제 응용

 

 


 

1. AOP_로깅

 

1) Join Point

AOP를 로깅 외에 활용하기 위해서는 어드바이스(횡단관심,공통로직)를 잘 알아야하고, 또한 효율적으로 사용하려면, 핵심관심(비즈니스메서드, CRUD)을 알아야한다. 스프링에서는 JoinPoint라는 interface가 제공되는데, 이 클래스를 통해 핵심관심에 대한 정보를 추출해 낼 수 있다.

 

- JoinPoint를 인자로 두는 것 만으로 컨테이너에 의해 자동으로 초기화 되며, 이 클래스의 getStignature()메서드에서 메서드의 이름을 뽑아낼 수 있다.

- Object배열에서 getArgs()메서드로 해당 메서드에 사용된 인자를 확인할 수 있다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;

public class LogAdvice {

	public void printlog(JoinPoint jp) { //컨테이너가 new도 한다.
		//jp: 어떤 핵심관심이 호출되었는지에 대한 정보가 담겨있음
		String methodName=jp.getSignature().getName();//메서드 이름을 호출
		System.out.println("호출된 핵심관심"+methodName);
		
		Object[] args=jp.getArgs();//사용된 인자
        System.out.println("사용된인자");
		for(Object v:args) {
			System.out.println(v);
		}
	}
}

 

 

LogAdvice를 사용하기 위한 설정 코드는 다음과 같다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/>

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<aop:config>
		<!-- select류 메서드에 대해 aPointCut으로 설정 -->
		<aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="aPointCut"/>
		<aop:aspect ref="log"> <!-- 사용할 클래스의 객체 -->
			<!-- 해당 PointCut(지정된 메서드의 사용)을 기준으로 하여 설정한 동작 시점에, method를 수행 할 것이다. -->
			<aop:before method="printlog1" pointcut-ref="aPointCut"/>
		</aop:aspect>
	</aop:config>
	
</beans>

 

실행 결과는 아래와 같다.

실행된 비즈니스 메서드와 사용된 인자

 

 


 

2) returnObj

바인딩 개념을 통해 메서드로 인해 반환된 output을 확인할 수 있다. After류인 동작시점에대해 사용된다.

동적바인딩에 대해 우선 알아보자면 내용은 아래와 같다.

 

동적, 정적 바인딩의 개념

 

 

 

예시 코드

실제 저장된 객체에 대해서만 메서드가 실행된다. (동적 바인딩개념에 의해 다형성이 실현되었다고 표현한다.)

 

 

 

아래의 코드로 Join Point에 접근하여 사용된 메서드의 이름을 호출하고, returnObj에 접근하여 반환된 값을 확인할 수 있다.

이때, 반환값이 MemberVO로 캐스팅(형변환)이 가능한지 확인한 후 가능하다면 이후의 로직이 수행될 수 있도록 한다. 이때 캐스팅은 returnObj가 더 상위 개념이므로 다운캐스팅 한다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;

import com.test.app.member.MemberVO;

public class LogAdvice {

	//return에 대한 정보
	public void printlog(JoinPoint jp,Object returnObj) { //returnObj: 바인드변수->얘는 컨테이너에게 설정을 해주어야한다.
		
		//jp: 어떤 핵심관심이 호출되었는지에 대한 정보가 담겨있음
		String methodName=jp.getSignature().getName();//메서드 이름을 호출
		System.out.println("호출된 핵심관심: "+methodName);
		System.out.println("반환값: "+returnObj);

		// 바인딩 될 수 있니?/return이 MemberVO가 될 수 있니?
		if(returnObj instanceof MemberVO) {
			
			MemberVO vo= (MemberVO)returnObj;//다운캐스팅
			if(vo.getRole().equals("ADMIN")) {
				System.out.println("+++++ADMIN+++++");
			}else {
				System.out.println("일반 사용자 입장");
			}
			
		}else {
			System.out.println("알 수 없는 반환값입니다.");
		}
		
	}
}

 

 

사용을 위한 설정 코드는 아래와 같다. returnObj는 JoinPoint와 다르게 컨테이너에서 자동 초기화되지 않으므로, 별도 설정을 해주어야한다. 그에 대한 내용은 returning에 담겨있다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/>

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<aop:config>
		<aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="aPointCut"/>
		<aop:aspect ref="log">
			<aop:after-returning method="printlog1" pointcut-ref="aPointCut" returning="returnObj"/><!-- returnObj -->
		</aop:aspect>
	</aop:config>
	
</beans>

 

 

결과는 아래와 같다. 호출된 메서드에 대한 로깅은 자른 상태로 캡처하였다.

로그인 성공(일반 사용자)

 

 

로그인 실패

 

 


 

 

3) excepObj

Exception이라는 바인드 변수를 통해 메서드 수행시 반환된 예외를 확인할 수 있다.

 

확인을 위한 코드는 아래와 같다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;


public class LogAdvice {

	//예외에 대한 정보
	public void printlog(JoinPoint jp, Exception excepObj) { //excepObj: 바인드변수->얘는 컨테이너에게 설정을 해주어야한다.
		
		String methodName=jp.getSignature().getName();//메서드 이름을 호출
		System.out.println("호출된 핵심관심:"+ methodName);
		System.out.println("반환된 예외"+excepObj);
		
	}
}

 

 

설정파일의 코드를 첨부하였다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/>

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<aop:config>
		<aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="aPointCut"/>
		<aop:aspect ref="log">
			<aop:after-throwing method="printlog1" pointcut-ref="aPointCut" throwing="excepObj"/><!-- excepObj -->
		</aop:aspect>
	</aop:config>
	
</beans>

 

 

콘솔 결과는 아래와 같다.

결과

 

 

 

 

 

추가로 각 발생 예외에 따라 출력에 분기를 둘 수 있다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;

public class AfterThrowingAdvice {
   public void ataLog(JoinPoint jp,Exception excepObj) {
      String methodName=jp.getSignature().getName();
      System.out.println("호출된 핵심관심: "+methodName);
      System.out.println("반환된 예외: "+excepObj);
      if(excepObj instanceof IllegalArgumentException) {
         System.out.println("실습을위해서 일부러 예외를 만든상황입니다.");
      }
      else if(excepObj instanceof NumberFormatException) { // 예외 확인후 추가가능
         System.out.println("타입이 올바르지않습니다.");
      }
      else if(excepObj instanceof Exception) {
         System.out.println("미확인 예외발생!!!");
      }
   }
}

 

분기처리 결과

 

 

 


 

4) ProceedingJoinPoint

around에 사용되는 로깅 중 스프링에서 지원해주는 StopWatch클래스를 활용해, 메서드가 수행되는데에 걸리는 시간을 확인해볼 수 있다.

 

사용 방식은 다음과 같다.

package com.test.app.common;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class AroundAdvice {
   public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
      StopWatch sw=new StopWatch();
      sw.start();
      Object obj=pjp.proceed(); // 수행할 핵심관심
      sw.stop();
      String methodName=pjp.getSignature().getName();
      System.out.println("호출된 핵심관심: "+methodName);
      System.out.println("걸린시간: "+sw.getTotalTimeMillis());      
      return obj;
   }
}

 

 

설정코드를 첨부하였다.

<?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:p="http://www.springframework.org/schema/p"
   xmlns:context="http://www.springframework.org/schema/context"
   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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

   <context:component-scan base-package="com.test.app" />
   <bean id="aa" class="com.test.app.common.AroundAdvice" /> 
   <aop:config>
      <aop:pointcut expression="execution(* com.test.app..*Impl.*(..))" id="aPointcut" />
      <aop:pointcut expression="execution(* com.test.app..*Impl.get*(..))" id="bPointcut" />
      
      <aop:aspect ref="aa">
         <aop:around method="aroundLog" pointcut-ref="aPointcut" />
      </aop:aspect>
   </aop:config>

</beans>

 

 

결과는 아래와 같다.

콘솔

 

 

 


 

 

5) 예제

아래의 조건을 만족하는 코드를 작성해보았다.

문제의 조건

 

 

 

로깅을 위한 메서드 코드

package com.test.app.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

import com.test.app.board.BoardVO;


public class LogAdvice {

	// 누구의 글인지 check
	public void printlog1(JoinPoint jp, Object returnObj) {
		
		if(returnObj instanceof BoardVO) {
			BoardVO vo=(BoardVO)returnObj;
			
			if(vo.getWriter().equals("관리자")) {
				System.out.println("공지글입니다.");
			} else {
				System.out.println("일반 사용자의 글 입니다.");
			}
		}else {
			System.out.println("알 수 없음 ???");
		}
	}
	
	// 메서드 수행에 걸린 시간 check
	public Object printlog2(ProceedingJoinPoint pjp) throws Throwable {
		StopWatch sw=new StopWatch();//스탑워치 사용
		sw.start();//시작
		Object obj=pjp.proceed(); // 수행할 핵심관심
		sw.stop();//정지

		String methodName=pjp.getSignature().getName();
		long time=sw.getTotalTimeMillis();
		System.out.println(methodName+"을 수행하는데 걸린 시간은 "+time+"ms 입니다.");
		return obj;
	}
	
}

 

 

그에 대한 설정 코드이다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/>

	<bean id="log" class="com.test.app.common.LogAdvice"/><!-- 사용될 클래스 new처리 -->
	
	<aop:config>
		<!-- serviceImpl과 연결되어있으므로, select류는 제외하고 반환값이 없을 수 있다(null)일 수 있다. -->
		<aop:pointcut expression="execution(* com.test.app..*Impl.get*(..))" id="aPointCut"/>
		<aop:aspect ref="log">
			<aop:after-returning method="printlog1" pointcut-ref="aPointCut" returning="returnObj"/>
			<aop:around method="printlog2" pointcut-ref="aPointCut"/>
		</aop:aspect>
	</aop:config>
	
</beans>

 

 

조건을 실행하기 위한 view를 작성하였다.

package com.test.app.board;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import com.test.app.member.MemberService;
import com.test.app.member.MemberVO;

public class Client { // 클라이언트,사용자,브라우저
	public static void main(String[] args) {
		AbstractApplicationContext factory=new GenericXmlApplicationContext("applicationContext.xml");
		
		BoardService bs=(BoardService)factory.getBean("boardService");
		BoardVO boardVO=new BoardVO();
		
		boardVO.setBid(1);
		boardVO = bs.getBoard(boardVO); //2번 글 상세보기
		
		factory.close();
	}
}

 

 

결과는 다음과 같았다.

 

수행 결과

 

 

 


 

2. @(어노테이션)으로 변경하기

 

1) 변경

 

① 초기 설정

<aop:aspectj-autoproxy>태그를 달아주는 것 만으로 초기 설정이 완료된다.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.test.app"/><!-- 어노테이션 관련 설정 -->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy><!-- aop어노테이션 초기설정 -->

</beans>

 


 

 

② @PointCut_참조메서드

어떤 메서드를 참조할 것인지를 설정한다. 이때 참조되는 메서드를 참조메서드라고 하며, 참조메서드는 식별을 목적으로 하기 때문에, 로직이 비어있는 것이 특징이다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.test.app.member.MemberVO;

public class BeforeAdvice {

	// pointCut설정
	@Pointcut("execution(* com.test.app..*Impl.*(..))")
	public void aPointCut() { } //참조메서드
	@Pointcut("execution(* com.test.app..*Impl.get*(..))")
	public void bPointCut() { } //참조메서드

}

 

@PointCut내부에 어떤 표현식을 가진 메서드를 참조할 것인지 설정한다. 이는 xml파일을 활용했을때와 설정 방식이 같다.

 


 

③ @동작시점_Before, After, Around,.....

횡단관심에 대한 동작 시점을 설정하는 어노테이션으로, 원하는 시점에 따라 그에 맞는 어노테이션을 활용할 수 있다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.test.app.member.MemberVO;

public class BeforeAdvice {

	// pointCut설정
	@Pointcut("execution(* com.test.app..*Impl.*(..))")
	public void aPointCut() { } //참조메서드
	@Pointcut("execution(* com.test.app..*Impl.get*(..))")
	public void bPointCut() { } //참조메서드
	
	// 동작 시점 설정(before)
	@Before("aPointCut()") 
	public void beforeLog() { // 어노테이션의 활용으로, 메서드명이 바뀌어도 따로 수정할 사항이 없게 된다.
		System.out.println("로그");
	}
}

 

 


 

 

④ @Aspect_결합

위의 코드에서 해당 클래스의 초기화와 pointCut + 공통로직(결합)을 진행한다.

package com.test.app.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.test.app.member.MemberVO;

@Service// 초기화 진행
@Aspect // 포인트컷과 어드바이스를 결합해주는 어노테이션
public class BeforeAdvice {

	// pointCut설정
	@Pointcut("execution(* com.test.app..*Impl.*(..))")
	public void aPointCut() { } //참조메서드
	@Pointcut("execution(* com.test.app..*Impl.get*(..))")
	public void bPointCut() { } //참조메서드
	
	// 동작 시점 설정(before)
	@Before("aPointCut()") 
	public void beforeLog() { // 어노테이션의 활용으로, 메서드명이 바뀌어도 따로 수정할 사항이 없게 된다.
		System.out.println("로그");
	}
}

 

 


 

 

2) Pointcut클래스 분리

횡단관심에 대한 클래스가 증가하게 됨에 따라 공통적으로 사용하게 될 PointCut에 대한 내용이 계속적으로 반복될 것을 고려하려, 반복되는 코드의 사용을 감소시켜 유지보수를 용이하게 할 목적으로 PointCut을 관리하는 클래스를 별도 생성한다.

package com.test.app.common;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

// pointCut모음
@Aspect
public class PointcutCommon {

	@Pointcut("execution(* com.test.app..*Impl.*(..))")
	public void aPointCut() { } //참조메서드
	@Pointcut("execution(* com.test.app..*Impl.get*(..))")
	public void bPointCut() { } //참조메서드

}

 

이때, @Aspect은 해당 어노테이션이 붙은 부분에 대해서만 결합을 진행하기 때문에 Pointcut클래스에도 어노테이션을 붙여준다.

 

 


 

3) 예제 응용

기존 예제 @로 설정 변경하기

package com.test.app.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;

import com.test.app.board.BoardVO;

@Service
@Aspect
public class LogAdvice {

	@AfterReturning(pointcut = "PointcutCommon.bPointCut()", returning = "returnObj")
	public void printlog1(JoinPoint jp, Object returnObj) {
		
		if(returnObj instanceof BoardVO) {
			BoardVO vo=(BoardVO)returnObj;
			
			if(vo.getWriter().equals("관리자")) {
				System.out.println("공지글입니다.");
			} else {
				System.out.println("일반 사용자의 글 입니다.");
			}
		}else {
			System.out.println("알 수 없음 ???");
		}
	}
	
	@Around("PointcutCommon.bPointCut()")
	public Object printlog2(ProceedingJoinPoint pjp) throws Throwable {
		StopWatch sw=new StopWatch();//스탑워치 사용
		sw.start();//시작
		Object obj=pjp.proceed(); // 수행할 핵심관심
		sw.stop();//정지

		String methodName=pjp.getSignature().getName();
		long time=sw.getTotalTimeMillis();
		System.out.println(methodName+"을 수행하는데 걸린 시간은 "+time+"ms 입니다.");
		return obj;
	}
	
}

 


 

반응형