본문 바로가기
Programming/Spring Boot

AOP(Aspect Oriented Programming: 관점 지향 프로그래밍)

by yoon9i 2024. 7. 2.


      OOP (Object Oriented Programming: 객체 지향 프로그래밍)
12. AOP (Aspect Oriented Programming: 관점 지향 프로그래밍)
==> @Transactional 이 AOP 기술로 만들어짐. (성공: commit , 실패: rollback)
https://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/spring-framework-reference/core.html#aop

1> 개념

  브라우저 ----------> A서블릿 ----------> 서비스 ----------> DAO ----------> DB
                      (핵심기능:필수     (핵심기능          (핵심기능    
                          +                 +                 +
                      부수기능: 로깅)    부수기능: 로깅)     부수기능: 로깅)

  브라우저 ----------> B서블릿 ----------> 서비스 ----------> DAO ----------> DB
                      (핵심기능          (핵심기능          (핵심기능    
                          +                 +                 +
                      부수기능)    부수기능: 로깅)     부수기능: 로깅)


  - 각 layer 가 달라도 공통적으로 사용되는 코드들이 있음.
  ex> 로그처리

  - 최종적인 AOP 개념은 다음고 ㅏ같다.
    핵심기능과 부수기능(분리기능)

2> AOP 기술
가. AOP 원천기술
  - AspectJ (1995년)
  - 굉장히 무겁다.
    startup 시간이 많이 걸림.
  - target class 의 많은 이벤트가 발생시 AOP 적용될 수 있다.
    ex>
        변수값이 변경,
        생성자 호출,
        메서드 호출,
        ...

나. Spring AOP
  - 원천기술인 AspectJ 에서 일부분의 기술만 빌려와서 만듬.
  - Spring 기반의 AOP 프레임워크.
  - target class 에서 메서드 호출되는 이벤트에서만 AOP 가 적용됨.

3> 용어정리
가. Aspect
- 여러 빈에 공통적으로 사용되는 부수기능을 구현한 빈을 의미.
- @Aspect 어노테이션 사용


나. JoinPoint
http://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/spring-framework-reference/core.html#aop-pointcuts-examples
- 핵심기능에 Aspect 가 적용되는 시점을 의미.
  Spring AOP 에서는 메서드 호출되는 시점만을 의미한다.
- 이벤트로 적용됨.
  ex> 핵심기능에서 발생 가능한 이벤트 종류?

    # 핵심기능(타켓클래스: target class)
    @Service
    public class DeptServiceImpl {
      int num;

      public void setNum(int n) {} // 메서드 호출 이벤트
      public void getNum() {} // 메서드 호출 이벤트
      public DeptServiceImpl() {} // 생성자 호출 이벤트
    }

    # 부가기능
    @Aspect
    public class MyAspect {
      
      public void log_print() {
        System.out.println("로그출력");
      }
    }


다. PointCut
- JoinPoint 는 AOP 가 주입되는 시점인 메서드 호출시점을 의미하고
  PointCut 는 메서드들 중에서 어떤 메서드를 호출했을 때 주입할 것인지를
  알려주는 표현식이다.

- execution("public void getNum()")
  execution("public void get*()")
  execution("public * get*()")
  execution("public * get*(**)")


라. Advice
- JoinPoint 는 AOP 가 주입되는 시점인 메서드 호출시점을 의미하고
  PointCut 는 메서드들 중에서 어떤 메서드를 호출했을 때 주입할 것인지를
  알려주는 표현식이고
  Advice 는 호출된 메서드 전/후/성공/에러(전,후,성공,에러) 시점을 의미.

  전: @Before (Before Advice)
  - 아래 예시로는 getNum() 전에 삽입
  후: @After (After Advice)
  - 아래 예시로는 getNum() 후에 삽입
  성공: @AfterReturning (AfterReturning Advice)
  실패: @AfterThrowing (AfterThrowing Advice)
  전/후/성공/에러: @Around (Around Advice)

  ex>
      # 사용
      DeptServiceImpl service = ctx.getBean();

      // 전
      int n = service.getNum();
      // 후


마. weaving
- target object 와 aspect 연결의미.


4> 구현
가. 의존성 설정
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>

  ==> aspectjweaver-1.9.7.jar 다운로드 됨.

나. Aspect 작성
- 부가기능을 구현한 빈
- @Aspect 어노테이션 지정


다. Aspect 내에서 advice 와 pointcut 을 설정
- @Before(pointcut 설정)
  target object 의 필수기능인 메서드가 호출하기전에 위빙
  ex> @Before("execution(public * say*(..))")

- @After(pointcut 설정)
  target object 의 필수기능인 메서드가 호출 후에 위빙
  ex> @After("execution(public * say*(..))")

- @AfterReturning(pointcut=pointcut설정, returning=리턴값저장변수설정)
  https://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/spring-framework-reference/core.html#aop-advice-after-returning

  target object 의 필수기능인 메서드가 리턴한 값을 얻을 수 있다.
  ex>
      @AfterReturning(pointcut="execution(public * say*(..))", returning="xxx") // 핵심기능에 리턴값이 있을때 사용
      public void afterLogging(JoinPoint join, Object xxx) {...}

- @AfterThrowing(pointcut=pointcut설정, throwing=발생된예외저장변수설정)
  target object 의 필수기능인 메서드가 예외발생 되었을때

  ex>
      @AfterThrowing(pointcut="execution(public * say*(..))", throwing="ex")
    public void afterThrowingLogging(JoinPoint join, Exception ex) {...}

- @Around(pointcut설정)
  @Before + @After + @AfterReturning + @AfterThrowing 모두 포함하는 기능
  ex>
      public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        Object retVal = pjp.proceed();

        return retVal;
      }

package com.exam;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

import com.exam.service.TargetObjectBean;

@SpringBootApplication
public class Application implements CommandLineRunner{

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
	ApplicationContext ctx;

	@Override
	public void run(String... args) throws Exception {
		logger.info("logger:ApplicationContext:{}",ctx);
		
		try {
			TargetObjectBean tob = ctx.getBean("target", TargetObjectBean.class);
			logger.info("logger:타겟객체 sayEcho메서드호출:{}",tob.sayEcho("홍길동"));			
		} catch (Exception e) {
			logger.error("logger: 에러발생, {}", e.getMessage());
		}
	}

}
package com.exam.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

@Configuration // 설정한다는 의미로 @Configuration / @Component 도 가능
@Aspect
public class LoggingAspect {
	
	Logger logger = LoggerFactory.getLogger(getClass()); 
	
//	@Before("execution(public String sayEcho(..))") // sayEcho 호출되기 전에
//	public void beforeLogging() {
//		logger.info("logger:LoggingAspect:{}","Before Advice");
//	}
//	@Before("execution(public * say*(..))") // say 로 시작하는 모든 기능
//	public void beforeLogging(JoinPoint join) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}",join.getSignature().getName());
//		logger.info("logger:LoggingAspect:{}","Before Advice");
//	}
	
	//-------------------------------------------------------
	
//	@After("execution(public * say*(..))") // say 로 시작하는 모든 기능
//	public void afterLogging(JoinPoint join) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}",join.getSignature().getName());
//		logger.info("logger:LoggingAspect:{}","After Advice");
//	}
	
//	@AfterReturning(pointcut="execution(public * say*(..))", returning="xxx") // 핵심기능에 리턴값이 있을때 사용
//	public void afterReturningLogging(JoinPoint join, Object xxx) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}",join.getSignature().getName());
//		logger.info("logger:호출된 핵심기능 메서드의 리턴값:{}", xxx);
//		logger.info("logger:LoggingAspect:{}","AfterReturning Advice");
//	}
	
	//--------------------------------------------------------
	
	// 예외가 발생됬을때 위빙됨.
//	@AfterThrowing(pointcut="execution(public * say*(..))", throwing="ex")
//	public void afterThrowingLogging(JoinPoint join, Exception ex) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}",join.getSignature().getName());
//		logger.info("logger:호출된 핵심기능 메서드명에서 예외클래스 정보:{}", ex.getMessage());
//		logger.info("logger:LoggingAspect:{}","AfterThrowing Advice");
//	}
	
	//------------------------------------------------------------
	
	@Around("execution(public * say*(..))")
	public Object aroundLogging(ProceedingJoinPoint join) {
		
		logger.info("logger:@Before 역할:{}","before Advice"); // sayEcho 전에 출력됨.		
		Object retVal = null;
		try {
			retVal = join.proceed();
			logger.info("logger: retVal: {}", retVal);
		} catch (Throwable e) {
			logger.info("logger:@AfterThrowing 역할:{}","AfterThrowing Advice");
		} // 기준
		logger.info("logger:@After|@AfterReturning 역할:{}","After|AfterReturning Advice"); // sayEcho 후에 출려됨.
		
		return retVal;
	}
}
package com.exam.service;

import org.aspectj.lang.annotation.AfterThrowing;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

// 인터페이스 사용안함.
//@Service // 가능
@Component("target")
public class TargetObjectBean {
	
	// 핵심기능 
	public String sayEcho(String name) {
		System.out.println("sayEcho 호출");
		
		// @AfterThrowing 를 테스트해보기 위한 강제 예외발생
		int n = 10;
		int result = n/0;
		
		return "안녕하세요" + name;
	}
}
# application.properties
logging.level.org.springframework=info
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.18</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.exam</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>




5. AOP 패턴
가. Pointcut 정의한 빈 작성
  public class CommonPointcutConfig {
    
    @Pointcut("execution(public * say*(..))")
    public void businessService() {}
    
    @Pointcut("execution(public * aa*(..))")
    public void businessService2() {}
  }

나. Aspect 에서 빈의 메서드 호출해서 pointcut 적용

package com.exam;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

import com.exam.service.TargetObjectBean;



@SpringBootApplication
public class Application implements CommandLineRunner{

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
	
    Logger logger = LoggerFactory.getLogger(getClass());
	
    @Autowired
	ApplicationContext ctx;


	@Override
	public void run(String... args) throws Exception {
		logger.info("logger:ApplicationContext:{}",ctx);	
		
	  try {	
		  
		TargetObjectBean tob = ctx.getBean("target", TargetObjectBean.class);
		logger.info("logger:타겟객체 sayEcho메서드호출: {}",
				tob.sayEcho("홍길동"));	
		
	  }catch(Exception e) {
		logger.error("logger:에러발생, {}", e.getMessage());
	  }
	}
}
package com.exam.aop;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcutConfig {

	@Pointcut("execution(public * say*(..))")
	public void businessService() {}
	
	@Pointcut("execution(public * aa*(..))")
	public void businessService2() {}
}
package com.exam.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

@Configuration
@Aspect
public class LoggingAspect {
	
	Logger logger = LoggerFactory.getLogger(getClass());
	
	
	@Before("com.exam.aop.CommonPointcutConfig.businessService()")
	public void commonPointcutConfigLogging(JoinPoint join) {
		logger.info("logger:호출된 핵심기능 메서드명:{}", join.getSignature().getName());	
		logger.info("logger:LoggingAspect:{}", "before Advice");	
	}
	
	
//	@Around("execution(public * say*(..))")
//	public Object aroundLogging(ProceedingJoinPoint join){
//
//		logger.info("logger:@Before 역할:{}", "before Advice");
//		Object retVal =null;
//		try {
//			retVal = join.proceed();
//			logger.info("logger:retVal :{}", retVal);
//			
//		} catch (Throwable e) {
//			logger.info("logger:@AfterThrowing 역할:{}", "AfterThrowing Advice");			
//		}
//		logger.info("logger:@After|@AfterReturning 역할:{}", "After|AfterReturning Advice");
//		
//		return retVal;
//	}
	
//	@AfterThrowing(pointcut="execution(public * say*(..))", throwing = "ex")
//	public void afterThrowingLogging(JoinPoint join, Exception ex) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}", join.getSignature().getName());	
//		logger.info("logger:호출된 핵심기능 메서드명에서 예외클래스 정보:{}", ex.getMessage());	
//		logger.info("logger:LoggingAspect:{}", "AfterThrowing Advice");	
//	}
	
//	@AfterReturning(pointcut="execution(public * say*(..))", returning = "xxx")
//	public void afterReturningLogging(JoinPoint join, Object xxx) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}", join.getSignature().getName());	
//		logger.info("logger:호출된 핵심기능 메서드명의 리턴값:{}", xxx);	
//		logger.info("logger:LoggingAspect:{}", "AfterReturning Advice");	
//	}
//	@After("execution(public * say*(..))")
//	public void afterLogging(JoinPoint join) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}", join.getSignature().getName());	
//		logger.info("logger:LoggingAspect:{}", "after Advice");	
//	}
	
//	@Before("execution(public * say*(..))")
//	public void beforeLogging(JoinPoint join) {
//		logger.info("logger:호출된 핵심기능 메서드명:{}", join.getSignature().getName());	
//		logger.info("logger:LoggingAspect:{}", "before Advice");	
//	}

}
package com.exam.service;

import org.springframework.stereotype.Component;

// 인터페이스 사용안함.

@Component("target")
public class TargetObjectBean {

	// 핵심기능
	public String sayEcho(String name) {
		System.out.println("sayEcho 호출");
		
		int n = 10;
		int result = n/2;
		
		return "안녕하세요"+ name;
	}
}
# application.properties
logging.level.org.springframework=info
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.18</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.exam</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

<AOP 커스텀어노테이션>

package com.exam;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

import com.exam.service.TargetObjectBean;



@SpringBootApplication
public class Application implements CommandLineRunner{

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
	
    Logger logger = LoggerFactory.getLogger(getClass());
	
    @Autowired
	ApplicationContext ctx;


	@Override
	public void run(String... args) throws Exception {
		logger.info("logger:ApplicationContext:{}",ctx);	
		
	  try {	
		  
		TargetObjectBean tob = ctx.getBean("target", TargetObjectBean.class);
		logger.info("logger:타겟객체 sayEcho메서드호출: {}",
				tob.sayEcho("홍길동"));	
		
	  }catch(Exception e) {
		logger.error("logger:에러발생, {}", e.getMessage());
	  }
	}
}
package com.exam.aop;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcutConfig {

	@Pointcut("execution(public * say*(..))")
	public void businessService() {}
	
	@Pointcut("execution(public * aa*(..))")
	public void businessService2() {}
	
	@Pointcut("@annotation(com.exam.aop.PerformanceTime)")
	public void performanceTimeAnnotation() {}
	
}
package com.exam.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceTime {}
package com.exam.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

@Configuration
@Aspect
public class PerformanceTimeAspect {
	
	Logger logger = LoggerFactory.getLogger(getClass());
	
	@Around("com.exam.aop.CommonPointcutConfig.performanceTimeAnnotation()")
	public Object performanceTime(ProceedingJoinPoint join) {
		
		Object retVal=null;
		long startTime = 0L;
		long endTime = 0L;
		try {
			startTime = System.currentTimeMillis();
			retVal = join.proceed();
			endTime = System.currentTimeMillis();
		} catch (Throwable e) {
			
		}
		
		System.out.println("총 작업시간 : " + (endTime - startTime));
		
		return retVal;
	}
}
package com.exam.service;

import org.springframework.stereotype.Component;

import com.exam.aop.PerformanceTime;

// 인터페이스 사용안함.

@Component("target")
public class TargetObjectBean {

	// 핵심기능
	@PerformanceTime
	public String sayEcho(String name) {
		System.out.println("sayEcho 호출");
		
		int n = 10;
		int result = n/2;
		
		return "안녕하세요"+ name;
	}
}
# application.properties
logging.level.org.springframework=info
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.18</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.exam</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>