티스토리 뷰

Spring

[Spring] 스프링 테스트 컨텍스트(Spring Test Context)와 트랜잭션 지원

망나니개발자 2021. 12. 28. 18:55
반응형

이번에는 테스트를 위한 테스트 컨텍스트 프레임워크(Test Context Framework)에 대해 정리해보도록 하겠습니다. 아래의 내용은 토비의 스프링을 참고하여 정리한 내용입니다.

 

 

1. 스프링 테스트 컨텍스트(Spring Test Context)


[ 테스트 컨텍스트(Test Context) ]

스프링은 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리하여 테스트에 적용해주는 테스트 프레임워크를 제공하는데, 이를 테스트 컨텍스트 프레임워크(Test Context Framework)라고 부른다.

테스트 컨텍스트 프레임워크는 실제 서버와 거의 동일한 구성으로 동작하는 애플리케이션 컨텍스트를 손쉽게 만들 수 있도록 도와준다. 여기서의 컨텍스트는 테스트에서 사용되는 애플리케이션 컨텍스트를 생성하고 관리해주는 객체를 의미한다. 하지만 테스트가 사용하는 애플리케이션 컨텍스트도 간단히 테스트 컨텍스트라고 하므로 문맥에 따라서 어떠한 의미인지 구분할 수 있어야 한다.

  • 테스트 컨텍스트 프레임워크: 테스트를 위한 애플리케이션 컨텍스트를 생성하고 관리하여 테스트에 적용해주는 테스트 프레임워크
  • 테스트 컨텍스트: 테스트에서 사용되는 애플리케이션 컨텍스트를 생성하고 관리하기 위한 컨텍스트
  • 애플리케이션 컨텍스트: 테스트를 실행하기 위한 애플리케이션 컨택스트

 

JUnit은 테스트 메소드를 실행할 때마다 매번 새로운 객체의 테스트 클래스를 만든다. 그래서 모든 테스트가 서로에게 영향을 주지 않으며 독립적으로 실행됨을 보장한다. 그런데 문제는 테스트가 독립적이기 위해서라면 매번 별개의 애플리케이션 컨텍스트(DI 컨테이너)를 만들어야 하는 것이다. 물론 스프링은 여러 개의 빈을 생성하고 의존관계를 주입하는 과정을 빠르게 처리하지만 다른 컴포넌트들까지 결합되는 경우라면 더욱 많은 시간이 요구된다. 대표적으로 JPA와 같은 ORM에서 처리하는 초기화 작업은 세션을 지원할 준비를 하고 쓰레드를 생성하는 등에 의해 오래 걸리며, 캐시를 사용하는 경우 캐시 연결을 위한 시간이 또 소요되기 때문이다. 만약 이런 초기화 작업이 테스트마다 반복되면 테스트 비용이 너무 커지게 된다.

그래서 스프링은 테스트가 사용하는 컨텍스트를 캐싱해서 여러 테스트가 1개의 (애플리케이션) 컨텍스트를 공유하는 방법을 제공한다. 동일한 컨텍스트 구성을 갖는 테스트끼리는 같은 (애플리케이션) 컨텍스트를 공유하는 것이다.

테스트 컨텍스트의 공유는 테스트 클래스 내의 메소드 사이에서만 가능한 것이 아니라 여러 테스트 클래스 사이에서도 가능하다. 만약 모든 테스트가 동일한 컨텍스트 구성을 갖는다면 테스트가 수천 개라고 하더라도 단 1개의 애플리케이션 컨텍스트를 통해서만 테스트가 실행되고, 이를 통해 테스트의 속도를 높일 수 있다.

다음의 그림은 모든 테스트를 통틀어 2가지 종류의 애플리케이션 컨텍스트 만이 존재하며, 여러 테스트가 이를 공유하는 그림이다.

 

물론 테스트 메소드의 개수만큼 Test 클래스의 객체가 반복적으로 만들어진다. 하지만 동일한 애플리케이션 컨텍스트를 공유하므로 비용이 상당히 줄어들게 된다. 물론 1개의 컨텍스트가 공유되므로 그 구성이나 내부를 함부로 변경하지 않도록 주의해야 한다.

 

 

[ 테스트 컨텍스트의 상속과 @DirtiesContext ]

Junit은 테스트 클래스가 특정 클래스를 상속하지 않도록 하고 있는데, 이를 통해 테스트 클래스를 구성할 때 상속 구조를 활용할 수 있다. 만약 부모와 자식 클래스에 모두 테스트 컨택스트를 지정하는 @ContextConfiguration이 있다면 자식 클래스의 컨텍스트는 부모 클래스에 정의된 것 까지 포함하여 구성된다. 만약 부모 클래스의 컨텍스트 설정을 무시하고 싶다면 inheritLocations를 false로 바꾸어주면 된다.

테스트는 그 실행 순서와 환경에 영향을 받지 말아야 한다. 예를 들어 A 테스트가 진행되고 B 테스트를 하면 성공하지만 그 반대로 진행이 되면 실패한다던가 하는 테스트는 잘못된 테스트이다. 또한 모든 테스트는 독립적으로 동작해야 하고 일정해야 한다. JUnit은 이러한 부분을 보장하기 위해 테스트 객체를 매번 새로 만드는 것이다. 물론 테스트 컨텍스트까지 독립적으로 제공한다면 좋겠지만 이는 비용이 너무 크므로 테스트 컨텍스트를 공유하고 있는 것이고, 그러므로 테스트 컨텍스트를 다룰 때에는 매우 신중해야 한다.

만약 어쩔수없이 컨텍스트의 빈을 조작하고 수정해야 한다면 @DirtiesContext 어노테이션을 붙여서 수정된 컨텍스트를 공유하지 않고 제거하도록 해주면 된다. @DirtiesContext가 붙은 컨텍스트의 테스트를 실행하고 다음 테스트를 실행해야 한다면 새로운 컨텍스트가 만들어지게 된다. 이를 통해 우리는 다른 테스트에 영향을 주지 않고 안전하게 컨텍스트를 조장하는 테스트를 작성할 수 있다.

 

 

 

 

2. 테스트에서의 트랜잭션 지원


[ 테스트의 트랜잭션 지원 필요성 ]

스프링 테스트 컨텍스트 프레임워크를 사용해 테스트를 위한 컨텍스트를 만든다면 데이터베이스 계층을 위한 Repository 계층(DAO)을 테스트하는 것은 간단하다. 테스트용 설정 파일에 테스트를 위한 DB를 지정한 DataSource를 등록해주면 되고, SpringBoot와 JPA를 이용하고 있다면 @DataJpaTest 어노테이션만 붙여주면 되기 때문이다. 그런데 DB가 사용되는 통합 테스트에서는 트랜잭션의 지원이 필요한 경우가 있다.

  1. 레포지토리 계층의 테스트
  2. 데이터베이스의 일관성

 

 

레포지토리 계층의 테스트

만약 트랜잭션 경계로 설정해 둔 서비스 계층의 빈을 테스트하는 경우라면 서비스 계층의 트랜잭션이 적용되어 테스트가 진행될 것이다. 일반적으로 서비스 계층에 트랜잭션 어노테이션이 붙어있어 상관이 없지만 레포지토리 계층을 테스트할 경우도 있을 수 있다.

JDBC를 기반으로 하는 JdbcTemplate과 같은 기술은 시작된 트랜잭션이 없으면 직접 트랜잭션을 시작해줘서 문제가 없다. 하지만 JPA나 하이버네이트 등은 트랜잭션이 시작되지 않은 채로 사용하면 예외가 발생한다. 그렇다고 Repository 계층(DAO)까지 트랜잭션을 모두 붙여주는 것은 좋지 못하다. 이러한 경우라면 테스트에서도 트랜잭션의 지원이 필요하다.

 

 

데이터베이스의 일관성

테스트는 어떤 순서로 실행되든 항상 결과가 동일해야 한다. 그런데 만약 데이터베이스가 포함되는 통합 테스트라면 어떤 테스트의 수정이나 조회 등의 결과가 다른 테스트에 영향을 받을 수 있게 된다. 예를 들어 어떤 데이터를 조회하는 테스트와 어떤 데이터를 추가해야 하는 테스트를 작성한 경우, 데이터를 조회했을 때 없어야 하는데 추가하는 테스트가 먼저 실행되어 데이터가 있는 경우가 발생하면 테스트는 일정하지 못하게 된다. 그렇다고 테스트마다 DB를 초기화 하는 것은 매우 부담스럽다.

 

 

Spring은 이러한 문제를 해결하기 위해 테스트에서 진행되는 모든 DB 작업을 하나의 트랜잭션으로 묶어서 진행하고, 테스트를 마칠 대 트랜잭션을 모두 롤백할 수 있도록 지원하고 있다. 테스트가 끝나면 트랜잭션을 롤백시키므로 DB는 테스트를 실행하기 전의 상태로 돌아가게 된다. 따라서 어차피 DB는 롤백될 것이므로 어떤 식으로든 DB를 조작하여도 테스트를 독립적으로 실행할 수 있게 된다.

(참고로 AUTO_INCREMENT로 증가된 값을 롤백되지 않는다.)

 

 

[ 테스트에서의 트랜잭션 ]

@Transactional을 이용한 AOP 기반의 트랜잭션은 컨텍스트에 등록된 빈들을 기반으로 동작한다. 그래서 Junit이 객체를 생성하고 관리하는 테스트 클래스의 객체는 AOP 적용대상이 될 수 없다. 하지만 스프링의 테스트 컨텍스트 프레임워크는 마치 AOP를 적용한 것과 유사한 방식으로 트랜잭션 기능을 테스트 메소드에 적용할 수 있게 해준다.

테스트에서의 @Transactional은 AOP에 의해 동작하는 것이 아니다. 그래서 설정에 애노테이션 방식의 트랜잭션을 위한 설정을 할 필요는 없다. 대신 물론 트랜잭션 매니저는 빈으로 등록이 되어 있어야 한다. 테스트에 @Transactional을 붙여주면 transactionManager라는 이름의 빈을 가져와 트랜잭션 제어에 사용한다. 만약 @Transactional이 붙은 테스트에 @Transactional이 붙은 서비스 계층의 빈을 호출한다면 테스트에 있는 @Transactional의 트랜잭션에 서비스 빈이 참여하게 된다. 이는 트랜잭션 전파 방식을 적용받기 때문이다.

테스트에서 사용가능한 트랜잭션 어노테이션으로는 다음과 같은 것들이 있다.

  • @Rollback
  • @BeforeTransaction, @AfterTransaction

 

 

@Rollback 어노테이션

테스트의 @Transactional은 강제롤백 옵션이 설정된 트랜잭션으로 만들어지는데, 테스트에서는 TransactionStatus의 setRollbackOnly()가 호출되기 때문이다. 물론 때로는 트랜잭션을 일부로 커밋하고 싶을 수도 있는데, 이러한 경우에는 @Rollback 어노테이션을 추가하고 false로 설정하면 롤백이 적용되지 않도록 할 수 있다. 그러면 해당 메소드에서 시작된 트랜잭션은 롤백을 일으키는 예외가 발생하지 않는 이상 커밋된다.

 

 

@BeforeTransaction, @AfterTransaction

@Transactional이 붙어있는 경우 @BeforeEach 등도 트랜잭션 안에서 실행이 되므로 자유롭게 트랜잭션이 필요한 작업을 처리할 수 있다. 그런데 트랜잭션가 시작되기 전이나 트랜잭션이 종류된 후에 해야할 작업이 있을 수 있다. 이런 경우에는 스프링이 제공하는 @BeforeTransaction이나 @AfterTransaction을 이용하면 된다. 만약 트랜잭션 매니저의 이름이 다르다면 @TransactionConfiguration을 이용해 트랜잭션 매니저 빈의 이름을 지정할 수 있다. 추가로 @TransactionConfiguration의 defaultRollback 속성을 이용하면 모든 테스트 메소드의 트랜잭션에 적용됐던 강제 롤백 기능을 해제할 수 있다.

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함