여니의 프로그래밍 study/Spring & Spring Boot

[Spring] AOP 프로그래밍 (Chapter 7)

여니's 2023. 9. 3. 16:49

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문


<Chapter7. AOP 프로그래밍>

AOP (Aspect Oriented Programming) 

> 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서

재사용성을 높여주는 프로그래밍 기법이다. 

 

 

해당 기법은 핵심 기능과 공통 기능의 구현을 분리함으로써

핵심 기능을 구현한 코드의 수정 없이 

공통 기능을 적용할 수 있게 만들어준다.

 

 

(1) 계승을 구하기 위한 클래스 구현 

package chap07;

public class ImpeCalculator implements Calculator {

	@Override
	public long factorial(long num) {
		long result = 1;
		for (long i = 1; i <= num; i++) {
			result *= i;
		}
		return result;
	}

}

 

해당 클래스의 factorial 메서드 실행시간을 구하기 위해서는

아래와 같이 코드를 수정할 수 있다.

 

package chap07;

public class ImpeCalculator implements Calculator {

	@Override
	public long factorial(long num) {
    	long start=System.currentTimeMillis();
		long result = 1;
		for (long i = 1; i <= num; i++) {
			result *= i;
		}
        long end= System.currentTimeMillis();
        System.out.println("ImpeCalculator.factorial(%d) 실행시간 = %d\n",
        	num, (end-start));
		return result;
	}

}

 

해당 클래스만 수정하고 사용하는 것이면

문제가 되지 않지만

다른 클래스 내에 있는 메서드의 실행시간을 구하려면

지금 추가한 코드들을 똑같이 넣어줘야 한다.

그렇게 되면 중복코드를 삽입하게 되는 것이고

기존 소스를 여러번 수정하게 되는 셈이다.

 

이를 피할수 있는 건 프록시 객체를 이용하는 것이다.

package chap07;

public class ExeTimeCalculator implements Calculator {

	private Calculator delegate;

	public ExeTimeCalculator(Calculator delegate) {
        this.delegate = delegate;
    }

	@Override
	public long factorial(long num) {
		long start = System.nanoTime();
		long result = delegate.factorial(num);
		long end = System.nanoTime();
		System.out.printf("%s.factorial(%d) 실행 시간 = %d\n",
				delegate.getClass().getSimpleName(),
				num, (end - start));
		return result;
	}

}
package main;

import chap07.ImpeCalculator;
import chap07.RecCalculator;
import chap07.ExeTimeCalculator;

public class MainProxy {

	public static void main(String[] args) {
		ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
		System.out.println(ttCal1.factorial(20));

		ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
		System.out.println(ttCal2.factorial(20));
	}
}

 

생성자를 통해 Calculator 객체를 전달 받아서

delegate 필드에 할당하고

중복된 코드 없이

간단하게 메서드의 실행시간을 구할 수 있다. 

 

프록시란?

: 핵심 기능의 실행은 

다른 객체에 위임하고 부가적인 기능을 제공하는 객체

 

프록시는 핵심 기능을 구현하지 않는다는 게 특징이다. 

여러 객체에 공통으로 적용할 수 있는 기능을 구현한다. 

 

이렇게 핵심 기능 구현과 공통 기능 구현을 분리하는 것이

AOP의 핵심이다. 


AOP의 기본 개념은

핵심 기능에 공통 기능을 삽입하는 것이다. 

 

 

핵심 기능의 코드를 수정하지 않으면서

공통 기능의 구현을 추가하는 것이 AOP이다.

 

 

핵심 기능에 공통 기능을 삽입하는 방법 3가지

1. 컴파일 시점에 코드에 공통 기능을 삽입하는 법

2. 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 법

3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 법

 

 

스프링 AOP는 프록시 객체를 자동으로 생성하여

공통 기능을 추가할 수 있다. 

따라서 위 ExeTimeCalculator 클래스처럼

상위 타입의 인터페이스를 상속받은 프록시 클래스를

직접 구현할 필요가 없다

 

public class ExeTimeCalculator {

    public void measureTime(Runnable task) {
        long startTime = System.nanoTime();
        task.run();
        long endTime = System.nanoTime();
        System.out.println("Execution Time: " + (endTime - startTime) + " nanoseconds");
    }
}
public interface MyService {
    void doSomething();
}

public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // 실제 비즈니스 로직
    }
}

스프링 AOP를 사용하여 메서드 실행시간을 측정하려면

아래와 같이 설정하면 된다. 

<aop:config>
    <aop:aspect ref="exeTimeAspect">
        <aop:around method="measureTime" pointcut="execution(* com.example.MyService.*(..))"/>
    </aop:aspect>
</aop:config>

 

exeTimeAspect는 실행시간을 측정하는 공통 기능을 가진 빈(Bean)

스프링 AOP는 이 설정을 바탕으로

MyService 인터페이스를 구현한 MyServiceImpl 클래스의 메서드 호출 시에

자동으로 프록시를 생성하고

프록시가 메서드 실행 시간을 측정하는 코드를 추가해준다. 

 

AOP 설정을 통해 공통 기능을 적용할 메서드를 선택하고

프록시 객체가 자동으로 해당 메서드에 공통 기능을 추가해준다. 

 


AOP 주요용어

용어 의미
Aspect 여러 객체에 공통으로 적용되는 기능으로 트랜잭션이나 보안 등이 좋은 예시임
Advice 언제 공통 관심 기능을 핵심 로직에 적용할지를 정의하는 용어
Joinpoint Advice를 적용 가능한 지점을 의미함
Pointcut Joinpoint의 부분집합으로서 실제 Advice가 적용되는 Joinpoint를 나타냄
Weaving Advice를 핵심 로직 코드에 적용하는 것을 의미함

 


스프링 AOP 구현

(1) AOP를 이용하여 공통 기능을 구현하고 적용하는 방법 절차

1. Aspect로 사용할 클래스에 @Aspect 어노테이션을 붙인다.

2. @Pointcut 어노테이션으로 공통 기능을 적용할 Pointcut을 정의함

3. 공통 기능을 구현한 메서드에 @Around 어노테이션을 적용함

 

개발자가 해야하는 일

공통 기능을 제공하는 Aspect 구현 클래스 만들기 ->

자바 설정을 이용하여 어디에 적용할지 설정하기 

 

package aspect;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;

@Aspect
// @Order(1)
public class ExeTimeAspect {

	@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
	}

	@Around("publicTarget()")
	public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.nanoTime();
		try {
			Object result = joinPoint.proceed();
			return result;
		} finally {
			long finish = System.nanoTime();
			Signature sig = joinPoint.getSignature();
			System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
					joinPoint.getTarget().getClass().getSimpleName(),
					sig.getName(), Arrays.toString(joinPoint.getArgs()),
					(finish - start));
		}
	}

}

 

@Aspect 어노테이션을 적용한 클래스는

Advice와 Pointcut을 함께 제공한다. 

 

@Pointcut은 공통 기능을 적용할 대상을 설정함

chap07 패키지와 그 하위 패키지에 위치한 타입의 public 메서드를 

Pointcut으로 적용한다는 의미

 

@Around 어노테이션은 Around Advice를 설정함.

대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에

공통 기능을 실행하는데 사용함 (Around Advice)

 

publicTarget() 메서드에 정의한 Pointcut에 

공통 기능을 적용한다는 것을 의미함

 

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}

	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}

}

 

@Aspect 어노테이션을 붙인 클래스를

공통 기능으로 적용하려면

@EnableAspectJAutoProxy 어노테이션을

설정 클래스에 붙여줘야 한다. 

해당 어노테이션을 붙이면

스프링은 @Aspect 어노테이션이 붙은 빈 객체를 찾아서

빈 객체의 @Pointcut 설정과 @Around  설정을 사용함

 

 

해당 어노테이션은 

프록시 생성과 관련된 AnnotaionAwareAspectJAutoProxyCreator 객체를 빈으로 등록함

@Enable 류의 어노테이션은 복잡한 스프링 설정을 대신해준다. 

 

 

빈 객체가 인터페이스를 상속하면

인터페이스를 이용해서 프록시를 생성한다

즉 프록시는 상속받은 인터페이스 타입으로 생성된다. 

 

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AppCtx{

 

인터페이스가 아닌 클래스를 이용해서

프록시를 생성하고 싶다면

proxyTargetClass 속성을 true로 지정하면 된다. 

 

 


Advice 적용 순서

@Order 어노테이션을 클래스에 붙이면

지정한 값에 따라 적용 순서를 결정한다

@Aspect
@Order(1)
public class ExeTimeAspect {

}

@Aspect
@Order(2)
public class CacheAspect {

}