6장 AOP - 6.8 트랜잭션 지원 테스트

6 분 소요

6.8 트랜잭션 지원 테스트

6.8.1 선언적 트랜잭션과 트랜잭션 전파 속성

트랜잭션 전파 속성은 유용한 개념이다.

REQUIRED 전파 속성은 진행 중인 트랜잭션이 있으면 참여하고 없으면 새로운 트랜잭션을 시작해주는 속성이다.

이를 통해 비즈니스 로직상 코드가 중복되는 일을 피할 수 있고, 애플리케이션을 작은 기능 단위로 쪼개서 개발할 수도 있다.

트랜잭션 전파라는 기법을 사용했기 때문에 작은 메소드가 독자적인 트랜잭션 단위가 될 수도 있고, 다른 트랜잭션의 일부로 참여할 수도 있는 것이다.

AOP 를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션 declarative transaction 이라고 한다.

반대로 TransactionTemplate 나 개별 데이터 기술의 트랜잭션 API 를 사용해 직접 코드 안에서 사용하는 방법을 프로그램에 의한 트랜잭션 programmatic transaction 이라고 한다.

특별한 경우가 아니리ㅏ면 선언적 방식의 트랜잭션을 사용하는 것이 바람직하다.

6.8.2 트랜잭션 동기화와 테스트

트랜잭션의 자유로운 전파의 기술적 배경에는 AOP 와 스프링의 트랜잭션 추상화가 있다.

트랜잭션 매니저와 트랜잭션 동기화

트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화이다.

트랜잭션 매니저를 통해 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다.

트랜잭션 동기화 기술을 통해 시작된 트랜잭션 정보를 저장소에 보관했다가 DAO 에서 공유할 수가 있었다.

특히 트랜잭션 동기화 기술은 트랜잭션 전파를 위해 중요한 역할을 한다.

트랜잭션 동기화 기술 덕분에 진행중인 트랜잭션이 있는지 확인하고, 참여할 수 있게 만드는게 가능하다.

트랜잭션 매니저를 통해 트랜잭션을 제어하는 것이기 때문에 트랜잭션 매니저를 이용해 트랜잭션에 참여하는 것도 가능하다.

트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어

트랜잭션 매니저를 이용해 각자의 트랜잭션으로 동작하던 메소드를 하나로 통합할 수 있다.

트랜잭션을 시작하기 위해 트랜잭션 정의를 담은 오브젝트를 만들고 이를 트랜잭션에 제공하면서 새로운 트랜잭션을 요청한다.

@Test
public void transactionSync() {
    DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);

    userDao.deleteAll();
    userDao.add(user1);
    userDao.add(user2);
    
    transactionManager.commit(txStatus);
}

세 개의 메소드의 트랜잭션 속성이 모두 REQUIRED 이므로 이미 시작된 트랜잭션이 있으면 참여하게 된다.

트랜잭션 동기화 검증

트랜잭션 속성 중에서 읽기전용과 제한시간 등은 처음 트랜잭션이 시작할 때만 적용되고 그 이후에 참여하는 메소드의 속성은 무시된다.

스프링의 트랜잭션 추상화가 제공하는 트랜잭션 동기화 기술과 트랜잭션 전파 속성 덕분에 테스트도 트랜잭션으로 묶을 수 있다.

이런 방법은 선언적 트랜잭션이 적용된 서비스 메소드에만 적용되는 것이 아니고, JdbcTemplate 역시 트랜잭션이 시작된 것이 있으면 그 트랜잭션에 자동으로 참여하고, 없으면 트랜잭션 없이 자동커밋 모드로 JDBC 작업을 수행힌다.

테스트를 통해 트랜잭션이 롤백되는지도 확인할 수 있다.

@Test
public void transactionSync() {
    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0));
    
    DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition); // 트랜잭션 시작
    
    userService.add(users.get(0)); 
    userService.add(users.get(1));
    assertThat(userDao.getCount(), is(2));
    
    transactionManager.rollback(txStatus);
    
    assertThat(userDao.getCount(), is(0)); // add() 작업이 취소되고 트랜잭션 시작 이전의 상태로 돌아옴을 확인
}

테스트에서 트랜잭션을 시작하거나 조작할 수 있는 기능은 매우 유용하다.

롤백 테스트

롤백 테스트는 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트를 말한다.

롤백 테스트는 DB 작업이 포함된 테스트가 수행되어도 DB 에 영향을 주지 않기 때문에 장점이 많다.

어떤 경우에도 트랜잭션을 커밋하지 않기 때문에 테스트가 성공하든 실패하든 상관없고, 예외가 발생해도 괜찮다.

만약 고유한 테스트 데이터가 필요하다면 테스트 앞부분에서 그에 맞게 DB 를 초기화하고 테스트를 진행하면 된다.

테스트에서 트랜잭션을 제어할 수 있기 때문에 얻을 수 있는 가장 큰 유익이 있다면 롤백 테스트라고 할 수 있다.

6.8.3 테스트를 위한 트랜잭션 애노테이션

스프링의 컨텍스트 테스트 프레임워크는 애노테이션을 이용해 테스트를 편리하게 만들 수 있는 여러 가지 기능을 추가하게 해준다.

@ContextConfiguration 을 클래스에 부여하면 테스트를 실행하기 전에 스프링 컨테이너를 초기화하고, @Autowired 애노테이션이 붙은 필드를 통해 테스트에 필요한 빈에 자유롭게 접근할 수 있다.

@Transactional

테스트의 @Transactional 은 앞에서 테스트 메소드의 코드를 이용해 트랜잭션을 만들어 적용했던 것과 동일한 결과를 가져온다.

테스트에서 사용하는 @Transactional 은 AOP 를 위한 것이 아니고, 단지 컨텍스트 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 쓰일 뿐이다.

@Rollback

테스트에 사용하는 @Transactional 은 애플리케이션의 클래스에 적용할 때와 디폴트 속성은 동일하지만 테스트가 끝나면 자동으로 롤백된다는 중요한 차이점이 있다.

테스트에 적용된 @Transactional 은 기본적으로 트랜잭션을 강제 롤백시키도록 되어있다.

만약 테스트 메소드 안에서 진행되는 작업의 강제 롤백을 원하지 않을 때는 @Rollback(false) 라고 해주면 된다.

@TransactionConfiguration

@Transaction 은 테스트 클래스에 넣어서 모든 테스트 메소드에 일괄 적용할 수 있지만 @Rollback 애노테이션은 메소드 레벨에서만 적용할 수 있다.

테스트 클래스의 모든 메소드에 트랜잭션이 롤백되지 않고 커밋되게 하려면 클래스 레벨에 부여할 수 있는 @TransactionConfiguration 애노테이션을 사용하면 된다.

다음과 같이 디폴트 롤백 속성을 false 로 해두고, 테스트 메소드 중에서 일부만 롤백을 적용하고 싶으면 메소드에 @Rollback 을 부여해주면 된다.

@TransactionConfiguration(defaultRollback = false)
public class UserServiceTest {
    @Test
    @Rollback
    public void add() throws SQLException {
        // ...
    }
}

NotTransactional과 Propagation.NEVER

테스트 클래스에 @Transaction 을 지정하면 모든 메소드에 @Transactional 이 적용된다.

이 경우 굳이 트랜잭션이 필요없는 메소드는 @NotTransactional 을 붙여주면 된다.

하지만 @NotTransactional 은 스프링 3.0 에서 제거 대상이 되었기 때문에 @Transactional 의 트랜잭션 전파 속성을 사용하는 방법을 검토할 수 있다.

@Transactional(propagation = Propagation.NEVER)

Propagation.NEVER 를 사용하면 트랜잭션을 시작하지 않고, 이미 진행 중인 트랜잭션이 있으면 참여하지 않게 된다.

효과적인 DB 테스트

일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와 DB 같은 외부 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는 것이 좋다.

DB 가 사용되는 통합 테스트를 별도의 클래스로 만들어둔다면 기본적으로 클래스 레벨에 @Transactional 을 부여해준다.

DB 가 사용되는 통합 테스트는 가능한 한 롤백 테스트로 만드는 것이 좋다.

애플리케이션의 테스트에서 공통적으로 이용할 수 있는 테스트 DB 를 셋업해주고, 각 테스트는 자신이 필요한 테스트 데이터를 보충해서 테스트를 진행하도록 한다.

테스트는 어떤 경우에도 서로 의존하면 안된다는 점을 기억하자.

6.9 정리

  • 트랜잭션 경계설정 코드를 분리해서 별도의 클래스로 만들고 비즈니스 로직 클래스와 동일한 인터페이스를 구현하면 DI 의 확장 기능을 이용해 클라이언트의 변경 없이도 깔끔하게 분리된 트랜잭션 부가기능을 만들 수 있다.
  • 트랜잭션처럼 환경과 외부 리소스에 영향을 받는 코드를 분리하면 비즈니스 로직에만 충실한 테스트를 만들 수 있다.
  • 목 오브젝트를 활용하면 의존관계 속에 있는 오브젝트도 손쉽게 고립된 테스트로 만들 수 있다.
  • DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다.
  • 번거로운 프록시 클래스 작성은 JDK의 다이내믹 프록시를 사용하면 간단하게 만들 수 있다.
  • 다이내믹 프록시는 스태틱 팩토리 메소드를 사용하기 때문에 빈으로 등록하기 번거롭다. 따라서 팩토리 빈으로 만들어야 한다. 스프링은 자동 프록시 생성 기술에 대한 추상화 서비스를 제공하는 프록시 팩토리 빈을 제공한다.
  • 프록시 팩토리 빈의 설정이 반복되는 문제를 해결하기 위해 자동 프록시 생성기와 포인트컷을 활용할 수 있다. 자동 프록시 생성기는 부가기능이 담긴 어드바이스를 제공하는 프록시를 스프링 컨테이너 초기화 시점에 자동으로 만들어준다.
  • 포인트컷은 Aspect] 포인트컷 표현식을 사용해서 작성하면 편리하다
  • AOP는 OOP만으로는 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.
  • 스프링은 자주 사용되는 AOP 설정과 트랜잭션 속성을 지정하는 데 사용할 수 있는 전용 태그를 제공한다.
  • AOP를 이용해 트랜잭션 속성을 지정하는 방법에는 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 6Transactional 애노테이션을 사용하는 방법이 있다.
  • @Transactional을 이용한 트랜잭션 속성을 테스트에 적용하면 손쉽게 DB를 사용하는 코드의 테스트를 만들 수 있다.