Search

JPA Event Listener 의존성 주입하기

태그
Java
Spring
분류
트러블슈팅
생성 일시
2024/02/04 12:28
프로젝트
Cabi

문제 상황

@Log4j2 @Component @NoArgsConstructor @AllArgsConstructor public class CqrsEventListener { private CqrsManager cqrsManager; @PostUpdate public void onPostUpdate(Object object) { ... } }
Java
복사
JPA Event Listener를 사용하기 위해 위와 같이 별도의 이벤트 리스너를 작성한 후, 실제 Update 쿼리 발생 시 동작하는지 테스트를 하려고 하니 NullPoint Exception이 발생하였다.

문제 원인

NPE가 발생하는 곳을 찍어보면, cqrsManager가 null 값을 가지고 있는 것을 확인 할 수 있다. cqrsManager에 의존성 주입이 이루어지지 않아 null 값을 가지고 있어, cqrsManager의 메서드를 호출하는 순간 NPE가 발생하게 된 것이다.
그럼 의존성 주입은 왜 안되는 걸까?
@Log4j2 @Component public class CqrsEventListener { private CqrsManager cqrsManager; public CqrsEventListener() { } @Autowired public CqrsEventListener(CqrsManager cqrsManager) { this.cqrsManager = cqrsManager; } ... }
Java
복사
이를 확인하기 위해 위처럼 CqrsEventListener와 CqrsManager의 @~ArgsConstructor 애노테이션을 삭제하고, 기본 생성자와 직접 생성자 주입 방식으로 의존성 주입을 만든 후 디버깅을 통해 확인해 보았다.
JPA Event Listener 내에 위처럼 기본 생성자가 아닌 다른 생성자가 있다면 기본 생성자가 필요하다며 오류를 보여주고,
이와 같이 의존성 주입 실패에 대한 컴파일 타임 에러가 발생한다.
JPA는 Class<T>.newInstance()로 Entity 클래스를 리플렉션을 통해 인스턴스를 만들기 때문에, 파라미터가 없는 기본 생성자가 반드시 필요하고 Hibernate는 이를 통해 프록시 객체를 생성할 수 있다.
정확한 이유는 찾지 못했지만 이러한 JPA Event Listener 또한 JPA에서 엔티티와 관련된 클래스로 간주하여 리플랙션의 Class<T>.newInstance()를 통해 인스턴스를 만들어, 인스턴스를 만들어 빈에 등록하는 과정에 생성자를 호출하기 때문에 기본 생성자가 필요한 것으로 추측된다.
이러한 이유로 기본 생성자를 추가해두면,
이처럼 애플리케이션 실행 시에 기본 생성자가 먼저 호출된 후, 이후에 CqrsManager를 주입받는 생성자는 호출되지 않아 생성자 주입 방식이 전혀 이루어지지 않는다. 때문에 cqrsManager는 null 값을 가지고 있는 것이다.

해결 과정

이를 해결하기 위해 인터넷을 찾아보니, 수정자(setter) 주입이나 스프링 이벤트 리스너를 통한 해결 등 여러 방법들을 통해 해결할 수 있음을 알게 되었다.
이런 여러 해결 방안 중 나는 수정자(setter) 주입을 통한 방식으로 해결하였다. 수정자 주입 방식은 여러 의존성 주입 방식 중 수정자 주입 방식을 적용하여, 인스턴스가 생성된 이후 수정자를 통해 의존성을 주입하는 방식이다.
@Configuration @RequiredArgsConstructor public class CqrsEventListenerConfig { private final CqrsEventListener cqrsEventListener; @Autowired public void init(CqrsManager cqrsManager) { cqrsEventListener.setCqrsManager(cqrsManager); } }
Java
복사
위처럼 의존성 주입해줄 수 있는 별도의 Configuration을 만들고, 해당 클래스 생성 이후 수정자 주입을 통해 JPA Event Listener 내에서 필요한 의존성을 주입한다.
@Log4j2 @Component public class CqrsEventListener { private static CqrsManager cqrsManager; // CqrsManager를 주입받는다.(Spring 주입 시점 차이로 인해 @Autowired 사용 불가) @Deprecated public void setCqrsManager(CqrsManager cqrsManager) { CqrsEventListener.cqrsManager = cqrsManager; } ... }
Java
복사
이처럼 static으로 선언하여 인스턴스 선언 시 마다 cqrsManager를 주입하여 사용하지 않도록 작성한다. 추가적으로 JavaDocs 주석과 @Deprecated 애노테이션을 통해 사용에 주의해야하며 가급적 사용하지 않을 것을 권고하였다.
이러한 해결 방법이 가능한 이유는 스프링 IoC에서 Bean과 Configuration을 등록하는 순서의 차이에 있다. Spring에서는 다음과 같은 순서로 실행된다.
1.
BeanFactoryPostProcessor: 빈 메타 정보를 수정하거나 추가할 수 있는 후처리기
2.
PropertySourcesPlaceholderConfigurer: 프로퍼티 파일을 활용하는 빈을 등록할 때 사용
3.
@Import: 설정 클래스 내의 @Component 애너테이션이 붙은 빈들을 생성
4.
DeferredImportSelector: @Import 어노테이션의 selectImports 메서드를 통해 등록된 빈을 생성
5.
@Configuration: 설정 클래스 내의 @Bean 애너테이션이 붙은 빈들을 생성
6.
@ComponentScan: 클래스 패스를 스캔하여 @Component 애너테이션이 붙은 빈을 생성
7.
ApplicationRunner: 애플리케이션 실행 후 실행되는 콜백 함수
리플렉션을 통해 Event Listener 클래스가 의존성 주입 없이 생성되고, @Configuration 클래스의 빈을 등록하는 과정에서 만들어진 Event Listener의 static 인자인 필드에 의존성 주입을 한다. 이렇게 의존성 주입된 클래스를 이후 @ComponentScan 시 빈으로 등록하여, 우리가 사용할 때는 의존성 주입 받은 EventListener를 사용할 수 있는 것이다.
사실 수정자 주입 자체가 잘못 사용하면 큰 문제로 이어지는 방식이다보니, 이에 대한 거부감은 조금 있었다. 하지만 스프링 이벤트를 사용하여 해결하는 방식은 사실상 JPA EventListener의 장점을 활용하지 못하는 것처럼 느껴졌다. 수정자 주입의 위험성에 대해 명확히 인지를 하고 있는 상태이고, 이와 관련된 경고문을 주석에 추가하여 두는 식으로 작성하여 최대한 조심해서 사용한다면 괜찮을 것 같아 이와 같이 해결하게 되었다.

참고