@Repository 예외 변환

yeahdy_:)·2024년 6월 15일

Spring

목록 보기
3/3

Spring AOP 의 트랜잭션에 대해 공부하며 객체가 프록시 생성을 하는지 안하는지를 테스트로 검증하고 있었다.

배경과 특이점은 아래와 같다.

배경

  1. Repository 역할을 하는 BankRepository 가 있다.
  2. Service 역할인 BankService가 있다.
    a. 이체를 하는 메소드인 accountTransfer()@Transactional이 선언되어 있다.
    b. accountTransfer()내부에서 BankRepository의 메소드를 호출한다.
//BankService.class
@RequiredArgsConstructor
@Service
public class BankService {
    private final BankRepository bankRepository;

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Bank fromBank = bankRepository.findById(fromId);
        Bank toBank = bankRepository.findById(toId);
        bankRepository.update(fromId, fromBank.getMoney() - money);
        bankRepository.update(toId, toBank.getMoney() + money);
    }
}

//BankRepository.class
@RequiredArgsConstructor
public class BankRepository {
    private final DataSource dataSource;

    public Bank findById(String bankId) throws SQLException{
        //bankId로 Bank 정보 가져오기
    }

    public void update(String bankId, int money) throws SQLException {
        //bankId로 Bank를 찾아서 money로 업데이트
    }
	// ...
}

특이점

  1. 테스트코드 실험1
    a. 스프링부트 테스트 내부 설정에서 BankRepository를 빈으로 등록한다. 이때 BankRepository@Repository가 선언되어 있지 않고, @TestConfiguration으로 테스트 설정으로 @Bean을 등록한다.
    b. BankRepository 가 AOP 프록시가 아닌지 검증한다.
    c. BankRepository 는 AOP 프록시가 아니기 때문에 검증에서 통과한다.

  2. 테스트코드 실험2
    a. 테스트 내부 설정에서 BankRepository를 빈으로 등록하지 않고, BankRepository@Repository선언을 통해 빈으로 등록한다.
    b. BankRepository 가 AOP 프록시가 아닌지 검증한다.
    c. BankRepository 는 AOP 프록시이기 때문에 검증에서 실패한다. → ✔️특이점!!!

위 실험에서 특이사항은 BankRepository@Repository선언 유무에 따라 프록시 생성이 달라진다. 따라서 @Repository 유무에 따른 프록시 생성 이유에 대해 알아보았다.

실험1_Repository를 빈으로 등록한 경우

@SpringBootTest
class BankServiceTest {
    @Autowired
    private BankRepository bankRepository;
    @Autowired
    private BankService bankService;

    @TestConfiguration 
    static class TestConfig {
        @Bean
        BankRepository bankRepository(){
            return new BankRepository(dataSource());
        }
    }

    @Test
    void aopCheck(){
        log.info("bankRepository class= {}", bankRepository.getClass());
        log.info("bankService class= {}",bankService.getClass());
        assertThat(AopUtils.isAopProxy(bankRepository)).isFalse(); //프록시아님 > 통과
        assertThat(AopUtils.isAopProxy(bankService)).isTrue(); //프록시맞음 > 통과
    }
}

BankRepository 를 테스트 설정에서 빈으로 등록했을 때, aopCheck() 검증을 하면 bankRepository는 AOP 프록시를 생성하지 않고, bankService는 AOP 프록시를 생성 해서 예상대로 테스트는 통과한다.

결과로그

INFO  s.jdbc.service.BankServiceTest --
      bankRepository class= class xxx.xxx.BankRepository
INFO  s.jdbc.service.BankServiceTest --
      bankService class= class xxx.xxx.BankService$$SpringCGLIB$$0

실험2_Repository에 @Repository를 선언한 경우

실험2는 아래와 같이 BankRepository클래스에 @Repository를 선언하고,
테스트 설정인 TestConfig에서 bankRepository의 빈 등록을 없앤 경우이다.

//BankRepository.class
@Repository	//추가★
public class BankRepository {
		// ...
}

//BankServiceTest.class
@SpringBootTest
class BankServiceTest {
    @Autowired
    private BankRepository bankRepository;
    @Autowired
    private BankService bankService;

    @TestConfiguration 
		static class TestConfig {
		    //BankRepository 빈등록 없앰★
		}

    @Test
    void aopCheck(){
        log.info("bankRepository class= {}", bankRepository.getClass());
        log.info("bankService class= {}",bankService.getClass());
        assertThat(AopUtils.isAopProxy(bankRepository)).isFalse(); //프록시맞음 > 실패
        assertThat(AopUtils.isAopProxy(bankService)).isTrue(); //프록시맞음 > 통과
    }
}

위 코드로 aopCheck() 를 검증하면 bankRepository도 AOP 프록시를 생성하고, bankService도 AOP 프록시를 생성하지만, “bankRepository는 AOP 프록시가 아니다” 를 검증했기 때문에 테스트는 실패한다.

결과로그

INFO  s.jdbc.service.BankServiceTest --
      bankRepository class= class xxx.xxx.BankRepository$$SpringCGLIB$$0
INFO  s.jdbc.service.BankServiceTest --
      bankService class= class xxx.xxx.BankService$$SpringCGLIB$$0

출력한 로그를 확인 해 보니 bankRepository는 SpringCGLIB 프록시를 생성하고 있었기 때문이다.

궁금한 점은 BankRepository에는 트랜잭션과 관련한 무언가를 선언 한 적이 없는데 왜 @Repository를 선언을 했더니 프록시가 생성된걸까?

여기서 중요한건 Service와 상관없이 Repository클래스만 따로 @Repository 선언 유무에 대해 테스트 했을 때도 @Repository 를 선언하면 프록시가 생성되고, 선언하지 않으면 프록시가 생성되지 않았다.

따라서 @Repository가 AOP 프록시를 생성한다고 생각해서 @Repository의 역할과 기능에 대해 찾아보았다.



@Repository 예외변환

이유는 @Repository 어노테이션이 선언된 클래스는 Spring에 의해 프록시 객체로 생성되기 때문이다. @Repository 어노테이션을 선언하면 Spring은 해당 클래스를 데이터 접근 로직인 DAO(Data Access Object)로 인식하는데, 예외 변환 기능을 적용하고 AOP 프록시를 생성한다.

그럼 예외 변환은 뭐고, 적용하면 어떤 장점이있을까?
그리고 @Repository와 프록시 생성은 어떤 관계일까?


예외변환 (exception translation)

예외 변환은 데이터 접근 중에 발생할 수 있는 예외를 Spring의 자체 예외 클래스인  DataAccessException계층 구조의 예외로 변환하는 역할을 한다.

그럼 예외 변환은 어떤 상황에 필요할까?

예를 들어, Reposiotry 에서 JDBC 데이터 접근 기술를 사용하고 있을 경우 예외가 발생하면 JDBC의 예외가 발생하고 Reposiotry 클래스의 내부 코드에도 JDBC 예외를 명시하게 된다.

그런데 고도화를 하면서 JDBC에서 JPA로 변경하게 되었다. 이때 Repository에서 예외가 발생하면 JPA의 예외가 발생하게 되고 기존에 JDBC 예외로 적어 둔 코드를 JPA 예외로 모두 수정해야 한다.

→ 따라서 특정 데이터 접근 기술에 대한 예외가 결합되어 있어 코드가 특정 데이터 접근 기술에 종속되는 문제가 발생한다.

위 문제를 해결하기 위해 Spring Framework 에서는 DataAccessException을 통해 예외를 추상화 해서 본래에 발생하는 예외를 래핑하고 Spring 자체 예외 클래스로 일관된 예외를 처리할 수 있는 기능을 제공한다.
이렇게 되면 기존의 예외 정보는 보존되고 특정 데이터 접근 기술에 종속적이지 않게 된다.

예외 변환은 @Repository 선언을 통해 간단히 기능을 적용할 수 있다.

참고: Spring 예외변환 공식문서

DataAccessException 계층 구조의 하위 집합

즉, @Repository를 선언하면 예외 변환 기능이 적용이 되고, 예외변환 기능을 사용하면 데이터 접근 기술을 변경해도 Spring의 자체 예외인 DataAccessException의 하위 계층들로 예외를 변환 해 주기 때문에 특정 데이터 접근 기술에 의존하지 않게 된다.



그럼 다시 본론으로 돌아와서 @Repository와 프록시 생성은 어떤 관계가 있는걸까?

먼저 @Repository의 역할이 무엇인지 다시 생각 해 보았다.
김영한님 Spring DB 강의에서 Repository는 계속해서 여러 데이터 접근 기술과 여러 DB Driver, 커넥션, 트랜잭션 매니저 등에 의존적이지 않도록 추상화 하며 발전시키고 있었다.
@Repository의 예외 변환도 마찬가지로 예외를 추상화하면서 여러 데이터 접근 기술을 사용할 수 있도록 제공한다.

@Repository의 역할에 대한 좋은 글을 Bealdung 블로그에서 발견해서 문장을 가져왔다.

@Repository’s job is to catch persistence-specific exceptions and re-throw them as one of Spring’s unified unchecked exceptions.

@Repository의 역할은 특정 퍼시스턴스 예외를 잡아내고 이를 Spring의 통합된 언체크 예외로 다시 throw 하는 것입니다.

여기서 말하는 특정 퍼시스턴스 예외(persistence-specific exceptions)란 DB와의 상호작용 중에 발생하는 예외를 말한다. 예를 들어 SQLException, DataIntegrityViolationException, DuplicateKeyException 등의 퍼시스턴스 예외를 Spring의 DataAccessException계층으로 변환 해 예외 처리를 일관되게 할 수 있도록 도와주는 것이다.

따라서 레포지토리의 주요 역할은 예외 변환이라고 말하고 있다.


그럼 프록시 생성과 어떤 관련이 있을까?

@Repository가 선언된 클래스는 예외변환을 하는 과정에서 프록시 객체로 래핑되는데, 여기서 생각 해 봐야할 점은 프록시는 어떤 역할을 하기 때문에 프록시로 래핑하는 것일까? 이다.

해당 답변은 Stack Overflow를 참고했다. 아래는 스택 오버플로우의 답변을 번역하였다.

Spring이 Bean 클래스를 프록시로 래핑해야 하는 이유는 무엇일까요? 런타임 때 클래스에 추가적인 로직을 삽입해야 하기 때문입니다. 이렇게 하면 개발자가 직접 작성하거나 긴 XML 설명자 파일을 구성할 필요가 없습니다.
@Transactional 어노테이션이 Bean 메서드에 마법 같은 트랜잭션 관리를 추가하는 것처럼, @Repository 어노테이션도 무언가를 추가해야 합니다. 이 사실은 어노테이션을 추가하면 Bean이 프록시로 감싸진다는 사실로 알 수 있습니다.

즉, 이 기능(예외 변환)은 런타임에 추가됩니다. 따라서 프록시가 필요합니다.

위 답변을 예외 변환과 관련지어 생각했을 때 해석 해 보면 아래와 같다.

프록시는 프로그램이 실행되는 동안에 클래스의 원래 기능 외에 추가적인 기능을 동적으로 삽입하는 기능을 가지고 있다. 이때 Spring은 런타임 시에 로직을 추가하는 것을 프록시를 통해 구현한다.
따라서 @Repository를 사용하면 런타임 시에 Spring은 데이터 접근 계층에서 발생하는 특정 예외를 Spring의 DataAccessException으로 변환하는 로직을 추가하기 때문에 프록시로 래핑해서 예외변환을 수행
하는 것이다.

(참고로 “마법 같은 트랜잭션 관리를 추가” 한다는 의미는 @Transactional을 사용하면 Spring은 해당 메서드 호출 전후에 트랜잭션을 시작하고 종료하는 로직을 자동으로 추가하는데 이때 프록시가 해당 역할을 한다.)

그래서 @Repository가 선언된 클래스는 Spring AOP를 통해 프록시로 감싸져서 예외 변환 기능이 적용되고 이로 인해 기본적으로 프록시가 생성되는 것이였다.



참고

DataAccessException계층 예외로 일관된 예외를 발생하게 하기 위해선 PersistenceExceptionTranslationPostProcessor를 빈을 등록시켜야 하는데, Springboot 에서 해당 빈을 자동으로 등록 해 준다.

아래의 Spring 공식문서 PersistenceExceptionTranslationPostProcessor 내용을 번역하면

Spring의 @org.springframework.stereotype.Repository 어노테이션이 붙은 모든 빈에 대해 자동으로 영속성 예외 변환을 적용하는 빈 후처리기입니다.

원래의 리소스 예외를 Spring의 org.springframework.dao.DataAccessException 계층구조로 변환합니다. org.springframework.dao.support.PersistenceExceptionTranslator 인터페이스를 구현하는 빈을 자동으로 감지하고, 이들에게 후보 예외를 변환하도록 요청합니다.

PersistenceExceptionTranslationPostProcessorPersistenceExceptionTranslator인터페이스를 구현하고 있다.



결론

@Repository 어노테이션이 선언된 클래스는 Spring이 자동변환 기능을 적용 시키는데, 이를 적용하기 위해 Spring AOP를 통해 프록시로 감싸지고 예외 변환 기능이 적용되기 때문에 프록시가 생성된다.

단순히 @Repository는 데이터 접근 로직을 작성하고 DAO 역할만 한다고 생각했는데, 예외 변환 이라는 기능에 대해 자세하게 몰랐었다. @Repository 의 역할을 다시 생각해보며, 프록시는 왜 적용되야 하는지에 대해 생각할 수 있는 기회가 되었다.

profile
기억하기 위해 기록하고 있습니다. 포스트 중 잘못된 정보가 있다면 코멘트 남겨주세요🐰

0개의 댓글