❗️문제 상황
✉️ 오류 메시지
Exception in thread "main" java.lang.ClassCastException: class jdk.proxy2.$Proxy21 cannot be cast to class ver3_aop_annotation.Boy (jdk.proxy2.$Proxy21 is in module jdk.proxy2 of loader 'app'; ver3_aop_annotation.Boy is in unnamed module of loader 'app')
at ver3_aop_annotation.Main.main(Main.java:14)
📃 문제 코드
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(Beans.class);
Person boy = (Boy) context.getBean("boy"); // 예외 발생
boy.makeFood();
}
상황 설명
AspectJ와 Spring을 통해 AOP 실습을 진행하던 도중 @EnableAspectJAutoProxy를 통해 빈을 AOP 프록시로 만드는 과정 도중 예외가 발생하였음
[연관코드 들]
1. @Aspect 클래스(공통 관심사)
@Component
@Aspect // 다른 핵심관심사항들에게 공통적으로 적용할 공통 관심사항
public class CommonAspect {
@Pointcut("execution(* make*(..))")
public void pointcut() {}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName());
System.out.println(joinPoint.getTarget());
System.out.println("배가 고프다."); //before 핵심 관심사항 수행 전에 해야될 힐
}
@AfterReturning("pointcut()")
public void afterReturning() {
System.out.println("맛있게 먹는다."); // 핵심 관심사항이 정상적으로 종료된게 확인 됐을 때
}
@AfterThrowing("pointcut()")
public void afterThrowing() {
System.out.println("119를 부른다.");
}
@After("pointcut()")
public void after() {
System.out.println("설거지를 한다."); // 예외가 발생하던 안하던 반드시 수행해야 할 일
}
@Around("pointcut()")
// 위에 처럼 전후 다 적용하려는 경우 굳이 하나하나 떼서 만들지 않고 around를 사용하면 편함
public void around(ProceedingJoinPoint target) {
System.out.println("배가 고프다. in ard");
try {
target.proceed();
System.out.println("맛있게 먹는다. in ard");
} catch (Throwable e) {
System.out.println("119를 부른다. in ard");
} finally {
System.out.println("설거지를 한다. in ard");
}
}
}
2. AOP 대상 클래스(Boy - Person의 구현체)
public interface Person {
void makeFood() throws Exception;
}
//////
@Component
public class Boy implements Person{
@Override
public void makeFood() throws Exception {
System.out.println("타코를 만듭니다."); // 핵심 관심사항
if (new Random().nextBoolean()) {
throw new Exception("불났다.");
}
}
}
3. Configuration 클래스
@Configuration
@ComponentScan(basePackages = "ver3_aop_annotation")
@EnableAspectJAutoProxy
public class Beans {
}
💡 해결 방법
🤔 오류가 발생한 이유
Spring이 빈을 AOP 프록시로 생성하는 방식으로 인해 발생한 오류이다.
@EnableAspectJAutoProxy를 통해 AOP를 활성화하면 스프링은 프록시를 사용하여 빈을 생성한다.
이때, 대상 클래스가 인터페이스를 구현하고 있는지 여부에 따라 프록시의 종류가 달라진다.
대상 클래스가 인터페이스를 구현하는 경우:
- 스프링은 JDK 동적 프록시를 사용한다. 이 프록시는 인터페이스를 구현한 프록시 객체를 반환하며 이 프록시 객체는 인터페이스를 구현하는 별도의 익명클래스이다. 따라서 이 프록시 객체는 인터페이스 타입으로만 캐스팅할 수 있다.
대상 클래스가 인터페이스를 구현하지 않는 경우:
- 스프링은 CGLIB를 사용하여 대상 클래스의 서브클래스를 생성하는 프록시를 만든다. 이 프록시는 원래 클래스 타입으로 캐스팅할 수 있다.
다시 문제가 발생한 코드를 보자.
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(Beans.class);
Person boy = (Boy) context.getBean("boy"); // 예외 발생
boy.makeFood();
}
Boy는 Person의 구현체이다. 그렇기 때문에 JDK 동적 프록시를 사용하게 되고 이 경우 프록시 객체를 반환한다. 위에서 말했듯 프록시 객체는 인터페이스를 구현한 그 구현 클래스(예시에서의 Boy 클래스)자체가 아니라 별도의 익명 클래스이다. 그렇기 때문에 Boy로 캐스팅할 수 없는 것이다.
🛠️ 수정
💡 구현체인 Boy가 아닌 인터페이스인 Person으로 형변환을 해주었다.
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(Beans.class);
Person boy = (Person) context.getBean("boy"); // 수정!!!
boy.makeFood();
}
추가 정보
System.out.println(boy.getClass());
System.out.println(AopUtils.isAopProxy(boy));
실제로 JDK 동적 프록시가 적용된 것을 알 수 있다.
반면 인터페이스가 구현체가 아니라면?
@Component
public class Girl{
public void makeFood() throws Exception {
System.out.println("냉면을 만듭니다."); // 핵심 관심사항
if (new Random().nextBoolean()) { // 핵심 관심사항 수행 도중 만약 예외가 발생한다면?
throw new Exception("불났다.");
}
}
}
(원래는 구현체가 맞았지만) 테스트를 위해 인터페이스의 구현체가 아닌 클래스를 정의해봤다.
그리고 실행
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(Beans.class);
Girl girl = (Girl) context.getBean("girl");
System.out.println(girl.getClass());
System.out.println(AopUtils.isAopProxy(girl));
}
아까와는 다르게 CGLIB이 적용된 것을 알 수 있다.
참고