IT 고찰/일상에서만난IT오류

[Spring] CGLIB 프록시의 호출 flow와 field Null 이슈...

MoonTheKid 2024. 12. 15. 21:39

AOP(Aspect - Oriented Programming)

 

핵심 비즈니스 로직을 분리시키면서 동시에 반복되는 동작에 대한 코드 재사용성과 가독성을 높여주는 프로그래밍 방식이다.

 

EX. 엔터프라이즈 애플리케이션에서 반복되는 로직들(로깅, 트랜잭션, 필터 처리 등등..)을 "비즈니스 로직"과 깔끔하게 분리해 준다.

 

Spring 프레임워크에서는 비즈니스 로직 외에 중요한 애플리케이션 기능들 (로깅, 보안 처리, 트랜잭션 관리) 등등을

 

AOP 기법으로 관리하며, 그 중심에는 프록시 패턴이 존재한다.

Proxy Pattern

 

 

디자인 패턴의 3개의 유형 중 하나인 구조(Structural) 유형에 속하는 패턴이다.

 

특정 객체에 대한 접근을 제어하는 과정에서 해당 객체를 래핑 하는 객체(Proxy)를 사용하는 방식으로,

 

클라이언트는 프록시 객체를 바라보게 된다.

 

프록시 객체가 특정 객체를 감싸고 있다보니, 프록시 객체에서 특정 메서드 호출 전, 후로 특정 기능을 동작하게 할 수 있다.

 

(디자인 패턴은 인터페이스 기반을 추천하지만, CGLIB에서는 구체 클래스를 상속하는 방식을 사용한다.(메서드 오버라이딩))

 

 

Spring에서의 프록시

 

Spring에서는

 

1. JDK의 dynamic Proxy의 인터페이스 기반(인터페이스 구현)의 프록시 생성 방식

 

2. CGLIB(Code Genartion Library) 기반의 실제 클래스 기반의 프록시 생성 방식(extends : 클래스 상속)

 

이 존재한다. 

 

Transactional 애너테이션

 

스프링 프레임워크를 사용하면 DB 레이어에 트랜잭션을 관리하기 위해 사용하는 애너테이션이 존재한다.

 

바로 Transactional 애너테이션인데, 해당 애너테이션으로 메서드나 클래스에 지정하게 되면,

 

해당 클래스를 래핑하는 프록시 객체가 생성된다.

 

프록시 객체는 특정 메서드 호출 전에 트랜잭션을 열고,

 

> 호출 과정에서 예외가 발생하면 롤백을,

 

> 예외가 발생하지 않으면 커밋을

 

수행한다.

 

트랜잭션 관리와 비즈니스 로직을 보기 좋게 구분할 수 있게 되는 것이다.

 

왜 서비스 빈에서 Field가 Null이라는 NPE가 발생했을까?

DB 레이어를 추상화한 JPA 인터페이스 repo를 의존하고 있는데, 해당 서비스 빈에서 repo가 null이라는 예외가 발생했다.

 

결론부터 말하면 repo가 null로 처리된(NPE가 발생한) 메서드가 final로 선언된 게 원인이었다.

 

CGLIB 기반으로 생성된 서브 클래스는 부모의 final 메서드를 오버라이딩할 수 없다. 

(final로 지정된 클래스는 확장할 수 없고, final 지정된 메서드는 오버라이딩이 불가능하다.)

 

프록시 객체에서는 해당 메서드 호출을 처리할 수 없고, 상속된 부모 클래스의 final method를 호출하게 된다.

final이 지정되지 않은 메서드들이 methodCache에 등록된 것을 볼 수 있다. [advised 객체]


CGLIB가 프록시 객체를 생성할 때  프록시가 호출할 콜백 메서드를 관리하는 참조를 가진다. (CGLIBCALLBACk 필드)

 

해당 필드에는 프록시 클래스의 메서드 호출을 가로채어 AOP 로직을 처리하는 역할을 담당한다.

 

Advised 필드

 

프록시 생성 시 사용하는 객체이다. AOP에 대한 설정들을 관리한다. 

 

Advice와 interceptor 목록을 포함하고 있으며, 어드바이스를 메서드 실행 전후로 호출하여 AOP 처리를 진행한다.

 

위의 목록에서 deleteAllBy 메서드가 methodCache에 등록되어 있지 않은데, final로 선언했기 때문이다.

 

final로 선언된 메서드는 어디서 호출되는가?

 

서브클래스

서브클래스를 참조로 호출한 메서드가 오버라이딩된 메서드인 경우 서브클래스의 오버라이딩된 메서드로 호출된다.

 

메서드 참조가 가능한데 서브 클래스에 없다면, 부모 클래스에 있는 메서드 호출을 탐색하게 된다.

 

그렇게 부모 클래스의 final 메서드가 호출되는 것이다.

 

어? 그러면 원본 클래스는 빈으로 등록되어 있어서 의존성 주입이 되어 있는 거 아니야? 라고 생각할 수 있다.

 

그러면 주입된 repo 기반으로 호출할 수 있는거 아닌가?..라고 말이다.

 

신기하게도 프록시 객체를 사용할 때  AOP 관련 로직에서 부모 클래스의 메서드(super().~~)를 호출하는 게 아니라고 한다.

 

프록시 객체 내에 부모 클래스를 참조(refer)로 가지고, 해당 부모 클래스 빈이 의존성 주입(DI)된다고 한다.

 

프록시 객체에서 호출되는 비즈니스 로직은 참조로 가지는 부모 클래스 인스턴스에서 호출되는 것이다.

(그래서 디버깅해보면 실제 호출되는 메서드는 CGLIB 기반의 빈이 아니라 스프링에서 관리되는 빈(POJO)임을 알 수 있다.)

 

AOP 처리와 분리되어 호출되는 비즈니스 로직이 super() 형식으로 호출되는 게 아닌,

 

의존성이 주입된 부모 클래스를 참조하는 인스턴스의 메서드를 호출하는 것이다. (서비스 빈)

 

[자세한 내용은 참고자료.. 총총]

 

프록시 객체의 원본 클래스는 빈으로 생성되는 것은 맞으나, 프록시 객체 자체의 부모 클래스가 빈으로 취급되는 것은 아니다.

 

프록시 객체만 Spring에서 관리할 빈이되는 것이다.  (프록시 객체의 부모를 참조하는게 Spring에서 관리하는 빈이 될 수 없는 것)

 

거기에 프록시 객체가 가진 필드에서 의존성 주입이 안 되는 필드는 null로 처리되다 보니 repo가 null이 되는 것이다.

 

부모 클래스의 final 메서드를 호출할 때 repo가 null이라는 것을 알게 되면서 NPE가 발생하는 것이다. 

[CGLIB 프록시 객체에서 호출한 메서드가 부모 클래스에서 호출되는지 여부를 확인하기가 어려워서 해맸다...]

 

후기...

Transactional 애너테이션이 클래스에 지정된 것 + Copilot이 만들어준 final method에서 발생한 이슈.

 

이슈의 원인은 해결했지만 왜 발생한 건지 이해하고 싶어 해당 글로 정리해 봤다.

 

Spring의 CGLIB 기반의 프록시 객체가 꽤나 복잡한 로직으로 관리되고 있음을 알게 되었다.

[바이트 코드 기반으로 생성되는 것은 서브 클래스이고, 실제 AOP로 동작시키는 과정은 합성 (DI로 주입된 빈)으로 관리된다는....]

 

오버라이딩 개념, CGLIB라는 개념, 프록시의 호출은 위임을 기반으로 진행된다는 것,

 

프록시 객체로 생성되는 빈의 부모는 Spring에 등록된 빈이 아니라는 것....

 

파고 들어가면서 재밌는 내용들을 배웠다. [개별적으로는 이해하는 기능들인데.. 복잡하게 얽혀있으니 이해하기가 쉽지 않았다.]

 

틀리거나 잘못된 정보가 적혀있다면 과감하게 지적해 주시기를 바라며 글을 마쳐본다.

 

 

 

참고자료

1. stackoverflow : Spring AOP CGLIB Proxy's field is null

 

Spring AOP CGLIB proxy's field is null

Description Using the vlcj component, the custom component appears as a result of the AOP proxy object null. MediaList Class public class MediaList { private libvlc_media_list_t mediaListIns...

stackoverflow.com

2. stackoverflow : Spring singleton bean fields are not populated

 

spring singleton bean fields are not populated

I need a service (singleton fits) with some internal fields, like a list of pending threads (yes everything is written to be thread safe) the problem is that if I @autowire this bean, fields appear...

stackoverflow.com

 

반응형