▸Spring MVC/기본 문법

스프링 AOP 구현 (관점 지향 프로그래밍)

코데방 2020. 3. 10.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

[ AOP ]

  • Aspect Oriented Programming - 관점 지향 프로그래밍
  • 공통 기능과 핵심 기능을 분리시켜 공통 기능을 계속 재활용해 사용하는 방식

개념 자체는 기존 객체 지향 프로그래밍 또는 함수형 프로그래밍과 동일합니다. 공통 코드는 따로 만들어서 여기저기서 가져다 쓰자는거죠. 하지만 IOC의 개념이 적용되어 제어의 주체가 역전됩니다. 즉 공통 기능의 코드를 직접 개발자가 넣는게 아니라, 컨테이너 및 AOP 관련 라이브러리가 지정된 메소드를 실행 전에 가로 채서 공통 기능을 대신 구현해주는 방식입니다. 따라서 개발자가 공통 기능에 관련된 메소드를 직접 핵심 기능의 코드 내에 끼워넣지 않는 것이 핵심입니다. 핵심기능을 가진 클래스에서는 공통 기능(부가 기능)에 대한 코드가 전혀 존재하지 않게 됩니다.

 

 


 

 

[ AOP 관련 용어 정리 ]

  • Aspect : '공통 기능'을 모아둔 객체를 의미
  • Advice : Aspect 안에 있는 '공통 기능 각각의 로직'을 의미
  • JoinPoint : '공통 기능을 적용해야 하는 메소드'의 실행 시점을 의미
  • Pointcut : '공통 기능을 적용할 대상'을 의미(1개 또는 그 이상)
  • Weaving : 공통 기능을 적용 하는 '행위'를 의미

'Aspect'는 그 안의 로직에 관계 없이 "공통 기능"을 구현할 객체 자체를 의미합니다. 반면에 'Advice'는 Aspect 객체 내에서 실제 구현되는 각각의 로직을 의미합니다. 하나의 'Aspect'안에 여러 'Advice'가 존재할 수 있습니다.

 

'JoinPoint'는 메소드가 실행될 때 Advice가 실행될 시점을 의미합니다. 예를 들어 메소드 실행 전에 Advice를 적용한다면 JoinPoint도 메소드의 실행 전(Before)이 됩니다. 두 메소드가 조인되는 시점이라고 보면 됩니다. 'Pointcut'은 Advice가 적용될 대상의 범위를 의미합니다. 이 대상이 되는 빈(Bean)에서 메소드가 실행되면 중간에 가로채서 JointPoint에 Advice를 실행시켜 주는 구조입니다. 단순 범위로 지정할 수도 있고 다양한 표현식을 이용해 적용할 수도 있습니다.

 

'Weaving'은 그냥 "공통 기능을 적용 한다, AOP를 구현한다"라는 행위 자체를 의미합니다.

 

 

 

 


 

 

[ 스프링 AOP 라이브러리 ]

 

스프링에서 AOP를 구현하는 방법은 대표적으로 두 가지가 있습니다. 사실 두 방법을 한 번에 사용할 수 있도록 잘 설계되어 있습니다. 

 

 

1. 스프링 AOP

 

스프링 AOP(spring-aop)는 내부적으로 AspectJ 라이브러리를 사용해 AOP 기능을 구현합니다. 처음 Spring MVC 프로젝트를 생성하면 초기 'pom.xml' 파일에 아래와 같이 의존 설정이 되어있는 것을 확인할 수 있습니다.

		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

 

AspectJ는 대표적인 AOP 프레임워크이며, 인터페이스를 구현한 클래스에만 적용됩니다. 만약 인터페이스 구현 없는 순수 클래스의 경우 적용이 되지 않는 점에 유의해야 합니다.

 

그리고 만약 @Aspect 어노테이션을 사용해 Advice 클래스를 구현하고 Pointcut의 표현식을 사용하기 위해서는 추가로 aspectjweaver 라이브러리를 사용해야 합니다. 아래와 같이 의존 설정을 해주면 됩니다.

		<!-- AspectJ Weaver -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

 

 

 

2. Cglib 라이브러리 사용

 

AspectJ가 인터페이스를 구현한 클래스만 프록시를 생성해 사용할 수 있다면, Cglib 라이브러리는 인터페이스 구현과 관계 없이 해당 클래스의 프록시를 생성해 AOP를 구현해줍니다.

 

AspectJ와 달리 해당 클래스를 상속받아 AOP 내용을 추가한 새로운 클래스를 만들어 실행시켜주는 방식이기 때문에, 상속을 할 수 없도록 하는 final 클래스의 경우 적용이 되지 않는다는 점에 유의해야 합니다. 따라서 final 클래스는 인터페이스를 구현하도록 해 AspectJ에서 처리할 수 있도록 해줘야 합니다. 

 

Cglib 사용을 위해 아래와 같이 의존 설정을 해줍니다.

		<!-- Cglib -->
		<dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib</artifactId>
			<version>2.2</version>
		</dependency>

 

 

 

3. 스프링에서 AspectJ와 Cglib을 동시 사용

 

스프링에서는 두 라이브러리를 동시에 사용할 수 있도록 설계되어 있습니다. 즉 위에서 두 라이브러리를 모두 의존 설으로 등록해두었으므로 스프링에서 자동으로 가져다가 사용합니다. 인터페이스를 구현한 클래스는 AspectJ로, 인터페이스를 구현하지 않은 클래스는 Cglib을 사용하여 AOP를 구현해줍니다.

 

xml에서 Bean설정을 통해 AOP 구현 클래스를 등록할 수 있지만, 더 간단한 어노테이션 사용으로 정리하도록 하겠습니다. @Aspect 어노테이션 검색을 위해 아래와 같이 자동 검색 코드를 추가해줍니다. 보통 루트 컨테이너가 참조하는 설정 파일에 적용합니다. namespaces에서 aop에 체크를 해줘야 합니다.

	<!-- 자동으로 @Aspect 어노테이션이 있는 클래스를 찾음 -->
	<aop:aspectj-autoproxy />

 

만약 모든 클래스 프록시를 Cglib 방식으로만 사용하겠다고 하면 아래와 같이 설정해주면 됩니다. 

	<!-- 프록시 타겟을 클래스로 고정함 (Cglib만 사용하도록 함) -->
	<aop:aspectj-autoproxy proxy-target-class="true"/>

 

 


 

 

 

[ 스프링 AOP 구현 테스트 ] 

 

1. 프록시(Proxy) 역할의 Aspect 클래스 작성

 

공통 기능을 구현할 프록시 역할의 클래스를 작성합니다. 이 클래스는 빈(Bean)으로 만들어서 사용하기 때문에 @Component 어노테이션도 같이 붙여줘야 합니다. xml에서 기술해주는 방법도 있는데 어노테이션이 뭔가 더 쉽고 직관적인 것 같습니다.

 

먼저 공통 기능 전용 클래스에 "@Aspect" 어노테이션을 붙여줍니다. 그리고 각각의 Advice 메소드에 JoinPoint에 대한 어노테이션들을 붙여서 사용해주면 됩니다. 간단히 이 메소드가 언제 실행될지 여부를 결정해주는 것입니다.

 

  • @Around : 메소드 실행 전, 예외 발생 시, 후 등 원하는 곳에 공통 기능 삽입 가능
  • @Before : 메소드 실행 전
  • @After : 예외 발생 유무에 관계 없이 메소드 실행 후
  • @AfterReturning : 예외 없이 정상 완료 된 후
  • @AfterThrowing : 예외가 발생했을 때만

PointCut은 적용될 대상의 메소드 한 개나 패키지 전체 등 범위로 지정할 수 있습니다. 또한 aspectJ Weaver 라이브러리를 추가했다면 정규표현식 같이 여러 지정자를 이용해서 적용될 대상을 골라낼 수 있는데 너무 양이 많아 이번 글에서는 생략하겠습니다. 향후 실제 사용할 때 AOP 적용 대상(PointCut)에 대한 세부적인 분류가 필요할 때 찾아서 사용하면 될 것 같습니다. 예시 코드는 간단한 경로로 지정했습니다. within()은 단순히 패키지 범위를 지정하는 것이고, 표현식을 사용하려면 execution()을 사용하면 됩니다.

 

먼저 메소드 실행 전과 후에 Advice가 실행되도록 한 코드입니다. Aspect는 빈(Bean) 객체로 사용해야하기 때문에, 직접 클래스를 xml에 등록해주거나 @Component 어노테이션을 붙여서 자동으로 Bean 객체로 생성될 수 있도록 해줍니다. 패키지 내 하위 패키지도 포함되도록 within("패키지..*")을 사용했습니다. 표현식은 별도로 다루도록 하겠습니다.

package hs.spring.hsweb.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Aop {

	// 시작 전에 실행될 AOP 메소드
	@Before("within(hs.spring.hsweb..*)")
	public void beforeMethod() {
		System.out.println("메소드 시작전~!");
	}

	// 종료 후에 실행될 AOP 메소드
	@After("within(hs.spring.hsweb..*)")
	public void afterMethod() {
		System.out.println("메소드 종료~!");
	}
}

 

 

 

2. AOP 적용 테스트

 

위와 같이 클래스를 작성하고 패키지 내 아무 클래스의 메소드나 실행해봅니다. 만약 aspectjweaver 라이브러리를 추가하지 않았다면 아래와 같은 예외가 발생합니다. 

 

 

 

aspectjweaver 라이브러리를 의존 설정 한 뒤 실행해보겠습니다. 이번엔 아래와 같은 예외가 발생하며 메세지가 나옵니다. 현재 실행시킨 메소드의 클래스는 인터페이스를 구현하지 않은 객체이기 때문에 프록시 타겟이 될 수 없다는 의미입니다. 친절하게 Cglib 라이브러리를 추가하라는 메세지가 나옵니다.

Cannot proxy target class because CGLIB2 is not available.
Add CGLIB to the class path or specify proxy interfaces.

 

 

마지막으로 Cglib 라이브러리를 의존설정 한 뒤 실행해봅니다. 정상적으로 AOP가 적용되었습니다. 

 

 

 

 


 

 

 

이번엔 Around 방식입니다. 실제 가로채온 핵심 기능의 메소드가 실행되는 시점은 proceed() 메소드가 실행되는 시점이기 때문에 전, 후, 예외 발생 등 마음대로 넣을 수 있습니다.

 

	@Around("within(hs.spring.hsweb..*)")
	public void around(ProceedingJoinPoint jpt) throws Throwable {

		try {
			System.out.println("실행 전~!");

			// 핵심기능 메소드 수행
			jpt.proceed();

		} catch (Exception E) {
			System.out.println("예외 발생했을 경우");
		} finally {
			System.out.println("실행 완료~!");
		}
	}

 

 

 

만약 가로챈 원래의 메소드에 리턴 타입이 있다면, around 메소드에서 해당 객체를 받아서 리턴시켜주면 됩니다. 아래와 같이 작성합니다. 예시 코드에서는 그냥 within()으로 전부 적용했지만, execution()을 이용한 표현식을 쓰면 리턴 타입만 구분해서 적용될 수 있도록 Pointcut을 지정할 수 있습니다.

	@Around("within(hs.spring.hsweb..*)")
	public UserInfoVO around(ProceedingJoinPoint jpt) throws Throwable {

		UserInfoVO vo = null;
		try {
			System.out.println("실행 전~!");

			// 핵심기능 메소드 수행
			vo = (UserInfoVO) jpt.proceed();

		} catch (Exception E) {
			System.out.println("예외 발생했을 경우");
		} finally {
			System.out.println("실행 완료~!");
		}
		
		return vo;
	}

 

 


 

 

아래는 특정 패키지에서 실행되는 메소드를 모두 출력하는 AOP 구현 예시입니다.

 

package hs.spring.hsweb.config.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class SpringFrameworkAop {

	@After("within(hs.spring.hsweb..*)")
	public void SpringAop(JoinPoint jpt)  {

		Signature sig = jpt.getSignature();
		System.out.println("실행 타입 : " + sig.getDeclaringType());
		System.out.println("실행 메소드 : " + sig.getName());
	}
}
728x90

댓글

💲 추천 글