의문점 : class level 또는 method level에 @Transactional 어노테이션이 붙어있는 Service 클래스는 Transaction 관련 코드(트랜잭션 시작, 커밋 또는 롤백, 트랜잭션 종료)가 추가된 Proxy 객체를 Bean으로 등록해 Transaction 기능을 수행한다.
그렇다면 아래와 같이 Service 내에 DI 컨테이너에서 주입받은 두가지 방식의 Repository는 Proxy일까?
Proxy라면 무슨 부가기능을 위해 Proxy가 된 것일까?
1. Spring Framework의 @Repository
2. Spring Data Jpa의 JpaRepository<T, ID>
// Proxy 객체로 DI 컨테이너에 등록되는 Service class
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService implements ItemService {
private final ItemAnnotationRepository itemAnnotationRepository; // @Repository Annotation
private final ItemJpaRepository itemJpaRepository; // JpaRepository
...
}
// @Repository 어노테이션
@Repository
public class ItemAnnotationRepository {
...
}
// extends JpaRepository (Spring Data Jpa)
public interface ItemJpaRepository extends JpaRepository<Item, Long> {
}
테스트 : Service, @Repository 어노테이션이 붙은 AnnotationRepository (이후부터는 AnnotationRepository라고 부른다), JpaRepository에 대해 Proxy 여부를 테스트 해본다.
1. Service is Proxy?
@Test
void proxyService() {
log.info("ItemService class = {}", itemService.getClass());
log.info("ItemService - isProxy = {}", AopUtils.isAopProxy(itemService));
assertThat(AopUtils.isAopProxy(itemService)).isTrue();
}
// [LOG]
// ItemService class =
// class hello.itemservice.service.ItemServiceV2$$EnhancerBySpringCGLIB$$ef71197f
// ItemService - isProxy = true
Service의 경우, CGLIB 방식으로 Proxy가 생성됨을 확인하였다.
Service의 경우는 클래스 레벨에서 @Transactional 어노테이션이 있으므로, AOP에 의해 Transaction 관련 코드가 추가된 Proxy가 추가됨을 알고 있다.
2. AnnotationRepository is Proxy?
@Test
void proxyAnnotationRepository() {
log.info("AnnotationRepository class = {}", itemService.getAnnotationRepository().getClass());
log.info("AnnotationRepository - isProxy = {}",
AopUtils.isAopProxy(itemService.getAnnotationRepository()));
assertThat(AopUtils.isAopProxy(itemService.getAnnotationRepository())).isTrue();
}
// [LOG]
// AnnotationRepository class =
// class hello.itemservice.repository.v2.ItemAnnotationRepository$$EnhancerBySpringCGLIB$$888809c
// AnnotationRepository - isProxy = true
Spring Framework의 @Repository 어노테이션의 경우에도 Proxy가 생성됨을 확인하였다.
Proxy로 생성됨에 따라 추가되는 부가기능에 대해서는 하단 내용에서 알아본다.
3. JpaRepository is Proxy?
@Test
void proxyJpaRepository() {
log.info("JpaRepository class = {}", itemService.getJpaRepository().getClass());
log.info("JpaRepository - isProxy = {}",
AopUtils.isAopProxy(itemService.getJpaRepository()));
assertThat(AopUtils.isAopProxy(itemService.getJpaRepository())).isTrue();
}
// [LOG]
// JpaRepository class = class jdk.proxy2.$Proxy102
// JpaRepository - isProxy = true
Spring Data Jpa의 JpaRepository의 경우에도 마찬가지로 Proxy가 생성됨을 확인하였다.
Proxy로 생성됨에 따라 추가되는 부가기능에 대해서는 역시 하단 내용에서 알아본다.
테스트 결과 : @Transactional이 적용된 Service는 물론이고, AnnotationRepository, JpaRepository 모두 Proxy가 적용된다.
분석 :
1. Spring Framework의 @Repository 어노테이션이 Proxy로 생성되는 이유 (추가되는 부가기능)
Spring Framework의 @Repository 어노테이션이 Proxy로 생성되는 이유는 JPA와 관련된 예외 발생에 있다.
Repository 내부에서 JPA 기술과 같은 데이터 접근 기술을 사용하게 되면 예외 발생 시 JPA와 관련된 예외가 발생하게 된다.
이러한 예외가 Service 단까지 throw 될 시 Service 단에서 데이터 접근 기술인 JPA에 의존하게 된다.
(* Service 단은 외부 라이브러리, 기술에 종속적이지 않고 최대한 순수한 비즈니스 로직으로 구성되는 것이 좋다.)
Service 단의 외부 종속을 방지하기 위해서는 외부 기술에 종속적인 예외를 추상화된 스프링 예외로 변환해야 하는데,
Spring은 이에 대한 해결을 위해 @Repository 어노테이션이 붙은 클래스를 찾아
JPA 관련 예외를 추상화된 스프링 예외로 변환하는 부가기능을 수행하는 Proxy를 생성해주는 것이다.
해당 부가기능이 추가된 Proxy로 인해 외부 데이터 접근기술 관련 예외가 Repository 단에서 발생해도 Service 단에는 순수하게 추상화된 스프링 관련 예외로 변환되어 발생함으로써 Service 단은 외부 기술에 종속적이지 않게 된다.
2. Spring Data Jpa의 JpaRepository에서 Proxy로 생성되는 이유 (추가되는 부가기능)
JpaRepository는 구조와 동작을 생각하면 Proxy가 생성되는 것이 어찌보면 당연하다.
아래와 같이 interface에 아무런 default 코드도 없이 오직 JpaRepository 인터페이스를 상속받은 게 전부인데도
save, findAll과 같은 기능들이 정상적으로 동작하기 때문이다.
public interface ItemJpaRepository extends JpaRepository<Item, Long> {
}
Spring Data Jpa는 JpaRepository를 상속받은 interface를 찾아 이를 구현하는 구체 클래스를 Proxy로 생성한다.
이 때 생성되는 Proxy에서 save, findAll과 같은 기본적인 CRUD 메서드를 구현한다.
이로 인해 interface에 아무 코드가 없더라도 실제로 DI 컨테이너에는 기본적인 CRUD가 모두 구현된 Proxy 구체 클래스가 Bean으로 등록되어 올바르게 동작하는 것이다.
(+ Spring Data Jpa의 경우 Spring이 제공하는 컴포넌트이므로 위에서 설명한 예외 변환 기능도 Proxy에 모두 포함된다)
'Spring' 카테고리의 다른 글
외부 메서드, 내부 메서드에 대한 @Transactional 트랜잭션 적용 결과 테스트 (0) | 2023.06.22 |
---|---|
Jsoup 라이브러리를 통한 정적 페이지 크롤링 (0) | 2023.03.18 |
JWT를 사용한 로그인 및 Refresh Token을 활용한 로그인 상태 유지 (9) | 2022.07.12 |
username만 매칭되면 user 세션 생성 되는 문제 해결 - AuthenticationProvider를 통한 password 기반 인증 (0) | 2022.05.30 |
로그인 성공 시 이전 페이지로 이동 - Referer 헤더와 AuthenticationSuccessHandler extends (0) | 2022.05.30 |