작심육일러의 스프링 시작하기(6)-2

서은경·2022년 8월 3일
0

Spring

목록 보기
10/43

프록시 생성 방식

public class MainAspect {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

        // calculator 빈의 실제 타입은 Calculator를 상속한 프록시 타입이므로 RecCalculator로 받으면 타입 변환이 불가하여 익셉션 발생
        Calculator cal = ctx.getBean(Calculator.class);
        long fiveFact = cal.factorial(5);
        System.out.println("cal.factorial(5) = " + fiveFact);
        System.out.println(cal.getClass().getName());
        ctx.close();
    }
}

실행코드에서 calculator 빈을 받아오는 소스를
이렇게 바꾸면 어떻게 될까?

RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);

RecCalculator 타입을 리턴하니까 아무 문제가 없지 않을까?

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

정답은 ❌ 이다

[ERROR] [system.err] 
Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException: 
Bean named 'calculator' is expected to be of type 'chap07.RecCalculator' but was actually of type 'jdk.proxy2.$Proxy18'

스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체(RecCalculator)가 인터페이스(Calculator)를 상속하면 인터페이스를 이용해서 프록시를 생성한다.

따라서 빈의 실제 타입이 RecCalculator라고 하더라도 "calculator" 이름에 해당하는 빈 객체의 타입은 Calculator 인터페이스를 상속받은 프록시 타입이 된다.

즉 에러메시지를 다시 보면 스프링이 런타임에 생성한 프록시 객체인 $Proxy18은 Calculator 인터페이스를 상속받았는데 getBean() 메서드에 사용한 타입은 RecCalculator라 다르다고 익셉션이 발생한 것 !

빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용하여 프록시를 생성하고 싶다면

package config;

import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
// @Aspect 어노테이션을 붙인 클래스를 공통 기능으로 적용하기 위해 붙여줌
// 이 어노테이션을 추가하면 스프링은 @Aspect 어노테이션이 붙은 빈 객체를 찾아서 빈 객체의 @Pointcut 설정과 @Around 설정 사용
// 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면
// proxyTargetClass 속성을 true로 지정하면 됨
@EnableAspectJAutoProxy(proxyTargetClass = true)

public class AppCtx {

    @Bean
    public ExeTimeAspect exeTimeAspect() {
        return new ExeTimeAspect();
    }

    @Bean
    // AOP 적용 시 RecCalculator가 상속받은 Calculator 인터페이스를 이용해서 프록시 생성
    public Calculator calculator() {
        return new RecCalculator();
    }

}

@EnableAspectJAutoProxy 어노테이션에 (proxyTargetClass = true) 옵션을 넣어주면 된다. 속성을 true로 지정하면 인터페이스가 아닌 자바 클래스를 상속받아 프록시를 생성한다.

🙋‍♀️도대체 순서가 어떻게 되나요?
💡 1.런타임에 프록시 객체가 생성 -> 2.설정파일을 읽어 aspect를 구현한 클래스 탐색 -> 3.프록시 대상 객체가 호출될 때 weaving이 일어나 공통 기능이 적용 -> 4.aspect 클래스에 proceed()를 통해 비즈니스 메서드를 호출하고 결과를 리턴

너무 나만 이해한 답변같지만 다음에 말 정리가 잘 됐을 때 다시 수정해야겠다 ..

execution 명시자 표현식

execution 명시자는 Advice를 적용할 메서드를 지정할 때 사용
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))

  • 수식어 패턴
    생략 가능하며 public, protected 등이 온다. 사실상 public 만 올 수 있음
  • 리턴타입패턴
    리턴 타입 명시
  • 클래스이름패턴/메서드이름패턴
    클래스 이름 및 메서드 이름을 패턴으로 명시
  • 파라미턴패턴
    매칭될 파라미터에 대해 명시

Advice 적용 순서

한 Product에 여러 advice 적용 가능하며, 어떤 aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있다

적용순서가 중요하다면 @Order 어노테이션을 통해 적용 순서를 지정해주면 됨

@Aspect
@Order(1)
public class ExeTimeAspect {
	...
}

@Aspect
@Order(2)
public class CacheAspect {
	...
}

@Around의 Pointcut 설정과 @Pointcut 재사용

@Pointcut 어노테이션이 아닌 @Around 어노테이션에 execution 명시자를 직접 지정할 수도 있다.


@Aspect
public class CacheAspect {
	
    @Around("execution(public * chap07..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Trowable {
    	...
    }
}

만약 같은 Pointcut을 여러 Advice가 함께 사용한다면 공통 Pointcut을 재사용할 수도 있다.

@Aspect
public class ExeTimeAspect {

	@Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }
    
    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Trowable {
    	...
    }
}

이 코드에서 @Around는 publicTarget() 메서드에 설정한 Pointcut을 사용한다.

공통으로 사용하는 Pointcut이 있다면 별도 클래스에 포인트 컷을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성하면 Pointcut 관리가 편해진다.


public class CommonPointcut {

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

@Aspect
public class CacheAspect{
	
    @Around(CommonPointcut.commonTarget())
}

위처럼 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 @Around 어노테이션에서 사용하면 된다.

4개의 댓글

comment-user-thumbnail
2022년 8월 9일

메섣에 -> 오타가 있네요.

1개의 답글
comment-user-thumbnail
2022년 8월 9일

코인트 컷 -> 오타가 또 있네요..

1개의 답글