이끌든지 따르든지 비키든지

Framework/Spring

[Spring & JPA] EntityListener에 Bean 주입하기

SeongHo5 2025. 2. 10. 20:41

JPA의 EntityListener는 엔티티의 라이프사이클 이벤트(pre-persist, post-update 등)에 대해 특정 작업을 수행할 수 있도록 도와줍니다.

 

그런데, 일반적으로 JPA EntityListener는 Spring 컨테이너의 관리 대상이 아니므로, Spring Bean을 직접 주입받기 힘든 구조입니다.

 

때문에 다음과 같은 기법들을 통해 간접적으로 Bean을 주입하는 방법을 생각할 수 있습니다.

 

 

방법1: @Configurable 사용하기


 

@Configurable 어노테이션은 AspectJ와 같은 AOP와 함께 사용되어, JPA가 직접 생성한 객체에도 Spring의 의존성 주입을 적용할 수 있도록 해줍니다.

 

 

1. Bean을 주입할 클래스에 @Configurable 적용

@Configurable
public class MyEntityListener {

    @Autowired
    private FooService fooService;
    
    
    @PrePersist
    public void prePersist(MyEntity myEntity) {
       fooService.doSomething();
    }
    
    
    @PostPersist
    public void postPersist(MyEntity myEntity) {
        fooService.doOtherThing();
    }
}

 

 

2. AspectJ LoadTimeWeaving 사용 설정

 

AspectJ 의존성을 추가하고, Main 클래스나 설정 클래스에서 LTW(LoadTimeWeaving)을 활성화합니다.

 

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {  

}

 

 

  • 장점: 리스너에 필요한 Bean을 자동으로 주입할 수 있음
  • 단점:
    • AspectJ Weaving 설정이 필요함
      • aspectjweaver.jar를 javaagent로 추가해야 하며, META-INF/aop.xml 설정도 필요
      • @EnableLoadTimeWeaving을 활성화해야 하며, 일반적인 Spring Bean과 비교해 설정이 복잡함
    •  Java 17부터 보안 정책 강화 (JEP 396 등)으로 LTW가 기본적으로 동작하지 않음
      • Java 16에서 적용된 JEP 396로 인해, LTW가 JDK 내부 API 접근 시 충돌 발생
      • Java 17+에서는 JEP 403 (Strong Encapsulation) 등 추가 보안 제한이 적용됨

 

 

 

※ 참고: AspectJ 1.9.7 Release Notes에 weaving 관련 내용

 

aspectj/docs/release/README-1.9.7.adoc at master · eclipse-aspectj/aspectj

Contribute to eclipse-aspectj/aspectj development by creating an account on GitHub.

github.com

 

 

 

방법2: ApplicationContext 인터페이스 활용


 

두 번째 방법은 ApplicationContextAware 인터페이스를 구현하여,

Spring의 ApplicationContext로부터 필요한 Bean을 직접 조회하는 방법입니다.

 

아래는 특정 엔티티에 대응하는 JpaRepository를 주입받기 위한 추상 클래스 예시입니다.

 

public abstract class AbstractJpaRepositoryAware<T extends JpaRepository<?, ?>> implements ApplicationContextAware {

  protected T repository;

  /**
   * 사용할 {@link JpaRepository} 타입을 반환한다.
   *
   * @return 주입받을 {@link JpaRepository} 타입
   * @implNote 구현체에서는 해당 메서드를 구현하여 사용할 {@link JpaRepository} 타입을 반환해야 한다.
   */
  protected abstract Class<T> repositoryType();

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.repository = applicationContext.getBean(this.repositoryType());
  }
}


// 엔티티 변경 이력에 대한 로그를 저장하는 구현체 예시
// JpaRepository<MyEntity, Long>를 상속하는 인터페이스(e.g., MyEntityRepository extends JpaRepository<MyEntity, Long>) 가 있다면,
public class MyEntityAuditingListener extends AbstractJpaRepositoryAware<MyEntityRepository> {

    @Override
    protected Class<MyEntityRepository> repositoryType() {
        return MyEntityRepository.class;
    }

    @PostPersist
    public void postPersist(MyEntity myEntity) {
       repository.save(LogType.POST_PERSIST, myEntity);
    }
    
    
    @PostUpdate
    public void postUpdate(MyEntity myEntity) {
        repository.save(LogType.POST_UPDATE, myEntity);
    }

}

 

 

이 클래스를 상속한 구체 클래스는 repositoryType() 메서드를 구현해 특정 Repository를 주입받아 사용할 수 있습니다.

 

 

  • 장점: 명시적으로 어떤 Repository를 사용할지 정의할 수 있음
  • 단점: 매 엔티티(또는 리스너)마다 별도의 추상 클래스 상속 및 구현이 필요