6.8) 트랜잭션 지원 테스트
선언적 트랜잭션과 트랜잭션 전파 속성
- 트랜잭션을 정의할 때 지정할 수 있는 트랜잭션 전파 속성은 매우 유용한 개념이다.
- REQUIRED로 전파 속성을 지정해줄 경우,
앞에서 진행 중인 트랜잭션이 있으면 참여하고 없으면 자동으로 새로운 트랜잭션을 시작해준다. - 스프링은 트랜잭션 전파 속성을 선언적으로 적용할 수 있는 기능을 제공한다.
UserService의 add()는 트랜잭션의 속성이 디폴트로 지정되어 있으므로 트랜잭션 전파 방식은 REQUIRED가 된다.
그러므로 독자적인 트랜잭션 단위가 될 수도 있고, 다른 트랜잭션의 일부로 참여할 수도 있다. - 이로 인해 만약 다른 메소드가 작업 중간에 사용자 등록을 할 필요가 있을 때
add() 메소드는 독자적인 트랜잭션을 시작하는 대신 실행되었던 메소드에서 시작된 트랜잭션의 일부로 참여하게 된다.
그러므로 매번 독자적인 트랜잭션을 만들지 않아 메소드의 코드 중복 문제와 실수로 인한 데이터 문제 발생을 막을 수 있다. - 그렇다고 트랜잭션 전파 속성이 개발자의 부주의나 게으름으로 인해 발생하는 불필요한 코드 중복을 막아주지는 못한다.
- REQUIRED로 전파 속성을 지정해줄 경우,

- 스프링은 선언적 트랜잭션과 프로그램에 의한 트랜잭션을 모두 지원하고 있다.
- AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션이라 한다.
- 반대로 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법은 프로그램에 의한 트랜잭션이라 한다.
- 스프링은 복잡한 컴포넌트가 아닌 평범한 자바 클래스로 만든 오브젝트에도 선언적 트랜잭션을 적용할 수 있다.
또한 트랜잭션 추상화를 함께 제공하기 때문에 특정 트랜잭션 기술과 종속되지도 않는다.
트랜잭션 동기화와 테스트
- 이렇게 트랜잭션의 자유로운 전파와 그로 인한 유연한 개발이 가능할 수 있었던 기술적인 배경에는
AOP와 스프링의 트랜잭션 추상화가 있다.- AOP 덕분에 프록시를 이용한 트랜잭션 부가기능을 간단하게 애플리케이션 전반에 적용할 수 있었다.
- 또 한 가지 중요한 기술적인 기반은 스프링의 트랜잭션 추상화다.
데이터 액세스 기술에 상관없이, 또 트랜잭션 기술에 상관없이 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶어서
추상 레벨에서 관리하게 해주는 트랜잭션 추상화가 없었다면
AOP를 통한 선언적 트랜잭션이나 트랜잭션 전파 등은 불가능했을 것이다.
- 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.
- PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 통해
구체적인 트랜잭션의 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다. - 또한 트랜잭션 동기화 기술이 있었기에 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유할 수 있었다.
- 트랜잭션 동기화 기술은 트랜잭션 전파를 위해서 진행 중인 트랜잭션이 있는지 확인하고,
트랜잭션 전파 속성에 따라서 이에 참여할 수 있도록 만들어준다. - 지금은 모든 트랜잭션을 선언적으로 AOP로 적용하고 있지만,
필요하다면 프로그램에 의한 트랜잭션 방식을 사용할 수도 있다.
물론 특별한 이유가 없다면 트랜잭션 매니저를 직접 이용하는 코드를 작성할 필요가 없다. - 그런데 특별한 이유가 있다면
트랜잭션 매니저를 이용해 트랜잭션에 참여하거나 트랜잭션을 제어하는 방법을 사용할 수도 있다.
- PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 통해
- 지금까지 진행했던 특별하고 독특한 작업은 모두 테스트에서 일어났다.
- 스프링의 테스트 컨텍스트를 이용한 테스트에서는 @Autowired를 이용해
애플리케이션 컨텍스트에 등록된 빈을 가져와 테스트 목적으로 활용하였으므로
당연히 트랜잭션 매니저 빈도 가져와 테스트에서 사용할 수 있다. - 간단한 테스트 메소드를 추가하여 트랜잭션 매니저를 참조하는지 테스트해보자.
UserService의 모든 메소드에는 트랜잭션을 적용했으므로
테스트에서 각 메소드를 실행시킬 때는 기존에 진행 중인 트랜잭션이 없고
트랜잭션 전파 속성은 REQUIED이니 새로운 트랜잭션이 시작된다.
그리고 그 메소드를 정상적으로 종료하는 순간 트랜잭션은 커밋되면서 종료될 것이다.
- 스프링의 테스트 컨텍스트를 이용한 테스트에서는 @Autowired를 이용해
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 매니저를 참조하는 테스트
@Test
public void transactionSync() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
- 그렇다면 이 테스트 메소드에서 만들어지는 트랜잭션을 하나로 통합하여
모든 메소드가 하나의 트랜잭션 안에서 동작할 수는 없을까?- 메소드들의 트랜잭션 전파 속성이 모두 REQUIRED이므로 이 메소드들이 호출되기 전에 트랜잭션이 시작되게만 하면 된다.
- 그러므로 테스트에서 트랜잭션 매니저를 이용해 트랜잭션을 시작시키고
이를 동기화해주어 테스트도 트랜잭션 동기화에 참여하도록 하면 된다. - 트랜잭션을 시작하기 위해서는 먼저 트랜잭션 정의를 담은 오브젝트를 만들고
이를 트랜잭션 매니저에 제공하면서 새로운 트랜잭션을 요청하면 된다. - 이로 인해 메소드들은 이미 시작된 트랜잭션이 있으면 참여하고 새로운 트랜잭션을 만들지 않는다.
- 하지만 정말 이 메소드가 테스트 코드 내에서 시작된 트랜잭션에 참여하고 있는지는 알 수 없으므로
트랜잭션의 속성을 강제로 읽기전용으로 변경하고 테스트를 다시 해보면
읽기전용 트랜잭션에 대해 쓰기 작업을 했을 때의 예외가 발생한다.
이를 통해, 테스트 코드 내에서 시작한 트랜잭션에 메소드들이 참여하고 있다는 확신을 얻을 수 있다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 매니저를 참조하는 테스트
@Test
public void transactionSync() {
// 트랜잭션 매니저를 이용해 트랜잭션을 미리 시작하게 한다.
// 트랜잭션 정의는 기본 값을 사용한다.
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
// 읽기전용 트랜잭션으로 정의한다.
txDefinition.setReadOnly(true);
// 트랜잭션 매니저에게 트랜잭션을 요청한다.
// 기존에 시작된 트랜잭션이 없으니 새로운 트랜잭션을 시작시키고 트랜잭션 정보를 돌려준다.
// 동시에 만들어진 트랜잭션을 다른 곳에서도 사용할 수 있도록 동기화한다.
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
// 앞에서 만들어진 트랜잭션에 모두 참여한다.
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
// 앞에서 시작한 트랜잭션을 커밋한다.
transactionManager.commit(txStatus);
}
}
- 이런 방법은 선언적 트랜잭션이 적용된 서비스 메소드에만 적용되는 것이 아니다.
- JdbcTemplate과 같이 스프링이 제공하는 데이터 액세스 추상화를 적용한 DAO에도 동일한 영향을 미친다.
- JdbcTemplate은 개념은 조금 다르지만 트랜잭션 전파 속성이 REQUIRED인 것처럼
트랜잭션이 시작된 것이 있으면 그 트랜잭션에 자동으로 참여하고,
없으면 트랜잭션 없이 자동커밋 모드로 JDBC 작업을 수행한다.
그러므로 트랜잭션 매니저를 통한 커밋을 하지 않아도 동일한 결과를 얻을 수 있다.
- 트랜잭션이라면 당연히 롤백도 가능해야 한다.
- 테스트를 만들면 전체 트랜잭션이 한꺼번에 롤백되는지도 확인할 수 있다.
- 이렇게 테스트 코드에서 미리 트랜잭션을 시작해놓으면 직접 호출하는 DAO 메소드도 하나의 트랜잭션으로 묶을 수 있으며
트랜잭션의 결과나 상태를 조작하면서 테스트하는 것도 가능하다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 롤백 테스트
@Test
public void transactionRollback() {
// 트랜잭션을 롤백했을 때 돌아갈 초기 상태를 만들기 위해 트랜잭션 시작 전에 초기화를 해둔다.
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));
// userDao의 getCount() 메소드도 같은 트랜잭션에서 동작한다.
assertThat(userDao.getCount(), is(2));
// 강제로 롤백한다. 트랜잭션 시작 전 상태로 돌아가야 한다.
transactionManager.rollback(txStatus);
// add()의 작업이 취소되고 트랜잭션 시작 이전의 상태임을 확인할 수 있다.
assertThat(userDao.getCount(), is(0));
}
}
- 테스트 코드로 트랜잭션을 제어해서 적용할 수 있는 테스트 기법인 롤백 테스트가 있다.
- 테스트 내 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해거리는 테스트를 말한다.
- 롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고
테스트를 시작하기 전 상태로 만들어주기 때문에 DB에 영향을 주지 않는 장점이 많다.
어떤 경우에도 트랜잭션을 커밋하지 않기 때문에 테스트가 성공하든 실패하든 상관없으며 예외가 발생해도 괜찮다. - 그러므로 전체 테스트를 수행하기 전 여러 테스트에서 공통적으로 필요한 사용자 정보를 테스트 데이터로 넣어뒀다면,
롤백 테스트 덕분에 매 테스트마다 처음과 동일한 테스트 데이터로 테스트를 수행할 수 있다.
테스트에 따라 고유한 테스트 데이터가 필요하다면 테스트 앞부분에서 그에 맞게 DB를 초기화하고 테스트를 하면 된다. - 롤백 테스트는 심지어 여러 개발자가 하나의 공용 테스트용 DB를 사용할 수 있게도 해준다.
적절한 격리수준만 보장해주면 동시에 여러 개의 테스트가 진행돼도 상관없다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 롤백 테스트
@Test
public void rollbackTest() {
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
// 테스트 안의 모든 작업을 하나의 트랜잭션으로 통합한다.
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
} finally {
// 테스트 결과가 어떻든 상관없이 테스트가 끝나면 무조건 롤백한다.
// 테스트 중에 발생했던 DB의 변경사항은 모두 이전 상태로 복구된다.
transactionManager.rollback(txStatus);
}
}
}
테스트를 위한 트랜잭션 애노테이션
- @Transactionl 애노테이션을 통해 트랜잭션을 적용해주는 것을 테스트 클래스와 메소드에도 적용할 수 있다.
- 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다.
이를 이용하면 테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶어줄 수 있다.
그러므로 트랜잭션 매니저와 번거로운 코드를 사용하는 대신 애노테이션만으로 트랜잭션이 적용된 테스트를 만들 수 있다. - 물론 테스트에서 사용하는 @Transactional은 기본적인 동작방식과 속성은 UserService 등에 적용한 것과 동일하지만
AOP를 위한 것이 아니라 단지 컨텍스트 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 쓰일 뿐이다. - 이를 사용하면 테스트 메소드 안에서 실행되는 전파 속성이 REQUIRED인 메소드들이
테스트 메소드의 트랜잭션에 참여해서 하나의 트랜잭션으로 실행된다. - 앞에서 해봤던 것처럼 테스트의 트랜잭션을 읽기전용으로 바꾸면 동일한 예외가 발생한다.
- 또한 @Transactional은 테스트 클래스 레벨에 부여하고 각 메소드에도 지정할 수 있다.
이 경우 메소드의 트랜잭션 속성이 클래스의 속성보다 우선한다.
- 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 애노테이션 테스트
@Test
@Transactional
// @Transactional(readOnly = true)
public void transactionAnnotation() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
- 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백되므로
@Transactional이 부여된 테스트를 실행할 경우 DB의 테이블에 아무런 데이터가 남아 있지 않음을 확인할 수 있다.- 만약 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고 싶기는 하지만
강제 롤백을 원하지 않을 경우 @Rollback 애노테이션을 이용하면 된다. - @Transactional은 기본적으로 테스트에서 사용할 용도로 만든 게 아니기 때문에 롤백 테스트에 관한 설정을 담을 수 없다.
따라서 롤백 기능을 제어하려면 별도의 애노테이션을 사용해야 한다. - 테스트 메소드에 이를 설정해주면 테스트 전체에 걸쳐 하나의 트랜잭션이 만들어지고
예외가 발생하지 않는 한 트랜잭션은 커밋된다. - 하지만 @Rollback 애노테이션은 메소드 레벨에만 적용할 수 있다.
만약 테스트 클래스의 모든 메소드에 트랜잭션을 적용하면서 모든 트랜잭션이 롤백되지 않고 커밋되게 하려면
무식하게 모든 메소드에 @Rollback(false)를 적용하기 보다는
클래스 레벨에 @TransactionConfiguration 애노테이션을 이용하면 롤백에 대한 공통 속성을 지정할 수 있다.
(스프링 4.2부터는 @TransactionConfiguration이 사라지고 @Rollback을 클래스 레벨에서 사용할 수 있다.)
- 만약 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고 싶기는 하지만
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 애노테이션 테스트
@Test
@Transactional
// @Transactional(readOnly = true)
@Rollback(value = false)
public void transactionAnnotation() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 롤백 여부에 대한 기본 설정과 트랜잭션 매니저 빈을 지정하는데 사용할 수 있다.
// 디폴트 트랜잭션 매니저 아이디는 관례를 따라서 transactionManager로 되어 있다.
// 하지만 이는 더 이상 사용되지 않으므로 클래스 수준에서 @Rollback 애노테이션을 사용하도록 함
// @TransactionConfiguration(defaultRollback=false)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
// add() 메소드의 테스트
@Test
// 메소드에서 디폴트 설정과 그 밖의 롤백 방법으로 재설정할 수 있다.
@Rollback
public void add() {
userDao.deleteAll();
// GOLD 레벨이 이미 지정된 User라면 레벨을 초기화하지 않아야 한다.
User userWithLevel = users.get(4);
// 레벨이 비어 있는 사용자로 수정한다.
// 로직에 따라 등록 중에 BASIC 레벨이 설정되어야 한다.
User userWithoutLevel = users.get(0);
userWithoutLevel.setLevel(null);
userService.add(userWithLevel);
userService.add(userWithoutLevel);
// DB에 저장된 결과를 가져와 확인한다.
User userWithLevelRead = userDao.get(userWithLevel.getId());
User userWithoutLevelRead = userDao.get(userWithoutLevel.getId());
assertThat(userWithLevelRead.getLevel(), is(userWithLevel.getLevel()));
assertThat(userWithoutLevelRead.getLevel(), is(userWithoutLevel.getLevel()));
}
...
}
- 테스트 클래스에 @Transactional을 지정했을 때 굳이 트랜잭션이 필요 없는 메소드는 어떻게 해야 할까?
- 필요하지도 않은 트랜잭션이 만들어지는 것이 꺼림칙하거나 트랜잭션이 적용되면 안 되는 경우에는
해당 메소드에만 테스트 메소드에 의한 트랜잭션이 시작되지 않도록 만들어줄 수 있다. - @NotTransactional을 테스트 메소드에 부여하면
클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행한다.
물론 테스트 안에서 호출하는 메소드에서 트랜잭션을 사용하는데는 영향을 주지 않는다. - 또는 트랜잭션 테스트와 비 트랜잭션 테스트를 아예 클래스를 구분해서 만들도록 권장하거나
@Transactional의 트랜잭션 NEVER 전파 속성을 지정해 트랜잭션이 시작되지 않도록 할 수 있다.
- 필요하지도 않은 트랜잭션이 만들어지는 것이 꺼림칙하거나 트랜잭션이 적용되면 안 되는 경우에는
@Transactional(propagation=Propagation.NEVER)
- 위처럼 테스트 내에서 트랜잭션을 제어할 수 있는 네 가지 애노테이션을 잘 활용하면
DB가 사용되는 통합 테스트를 만들 때 매우 편리하다.- 일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와,
DB 같은 외부의 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는 게 좋다.
이후 DB가 사용되는 통합 테스트에 기본적으로 클래스 레벨에 @Transactional을 부여해주면 된다.
또한 DB가 사용되는 통합 테스트는 가능한 한 롤백 테스트로 만드는 게 좋다. - 테스트는 어떤 경우에도 서로 의존하면 안 된다.
테스트가 진행되는 순서나 앞의 테스트의 성공 여부에 따라서 다음 테스트의 결과가 달라지는 테스트를 만들면 안 된다. - 트랜잭션을 지원하는 롤백 테스트는 매우 유용한 도구가 돼줄 것이다.
- 일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와,
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (1) (0) | 2024.01.24 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (0) (0) | 2024.01.23 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (7) (0) | 2024.01.08 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (6) (0) | 2024.01.03 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (5) (0) | 2023.12.30 |
6.8) 트랜잭션 지원 테스트
선언적 트랜잭션과 트랜잭션 전파 속성
- 트랜잭션을 정의할 때 지정할 수 있는 트랜잭션 전파 속성은 매우 유용한 개념이다.
- REQUIRED로 전파 속성을 지정해줄 경우,
앞에서 진행 중인 트랜잭션이 있으면 참여하고 없으면 자동으로 새로운 트랜잭션을 시작해준다. - 스프링은 트랜잭션 전파 속성을 선언적으로 적용할 수 있는 기능을 제공한다.
UserService의 add()는 트랜잭션의 속성이 디폴트로 지정되어 있으므로 트랜잭션 전파 방식은 REQUIRED가 된다.
그러므로 독자적인 트랜잭션 단위가 될 수도 있고, 다른 트랜잭션의 일부로 참여할 수도 있다. - 이로 인해 만약 다른 메소드가 작업 중간에 사용자 등록을 할 필요가 있을 때
add() 메소드는 독자적인 트랜잭션을 시작하는 대신 실행되었던 메소드에서 시작된 트랜잭션의 일부로 참여하게 된다.
그러므로 매번 독자적인 트랜잭션을 만들지 않아 메소드의 코드 중복 문제와 실수로 인한 데이터 문제 발생을 막을 수 있다. - 그렇다고 트랜잭션 전파 속성이 개발자의 부주의나 게으름으로 인해 발생하는 불필요한 코드 중복을 막아주지는 못한다.
- REQUIRED로 전파 속성을 지정해줄 경우,

- 스프링은 선언적 트랜잭션과 프로그램에 의한 트랜잭션을 모두 지원하고 있다.
- AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션이라 한다.
- 반대로 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법은 프로그램에 의한 트랜잭션이라 한다.
- 스프링은 복잡한 컴포넌트가 아닌 평범한 자바 클래스로 만든 오브젝트에도 선언적 트랜잭션을 적용할 수 있다.
또한 트랜잭션 추상화를 함께 제공하기 때문에 특정 트랜잭션 기술과 종속되지도 않는다.
트랜잭션 동기화와 테스트
- 이렇게 트랜잭션의 자유로운 전파와 그로 인한 유연한 개발이 가능할 수 있었던 기술적인 배경에는
AOP와 스프링의 트랜잭션 추상화가 있다.- AOP 덕분에 프록시를 이용한 트랜잭션 부가기능을 간단하게 애플리케이션 전반에 적용할 수 있었다.
- 또 한 가지 중요한 기술적인 기반은 스프링의 트랜잭션 추상화다.
데이터 액세스 기술에 상관없이, 또 트랜잭션 기술에 상관없이 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶어서
추상 레벨에서 관리하게 해주는 트랜잭션 추상화가 없었다면
AOP를 통한 선언적 트랜잭션이나 트랜잭션 전파 등은 불가능했을 것이다.
- 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.
- PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 통해
구체적인 트랜잭션의 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다. - 또한 트랜잭션 동기화 기술이 있었기에 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유할 수 있었다.
- 트랜잭션 동기화 기술은 트랜잭션 전파를 위해서 진행 중인 트랜잭션이 있는지 확인하고,
트랜잭션 전파 속성에 따라서 이에 참여할 수 있도록 만들어준다. - 지금은 모든 트랜잭션을 선언적으로 AOP로 적용하고 있지만,
필요하다면 프로그램에 의한 트랜잭션 방식을 사용할 수도 있다.
물론 특별한 이유가 없다면 트랜잭션 매니저를 직접 이용하는 코드를 작성할 필요가 없다. - 그런데 특별한 이유가 있다면
트랜잭션 매니저를 이용해 트랜잭션에 참여하거나 트랜잭션을 제어하는 방법을 사용할 수도 있다.
- PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 통해
- 지금까지 진행했던 특별하고 독특한 작업은 모두 테스트에서 일어났다.
- 스프링의 테스트 컨텍스트를 이용한 테스트에서는 @Autowired를 이용해
애플리케이션 컨텍스트에 등록된 빈을 가져와 테스트 목적으로 활용하였으므로
당연히 트랜잭션 매니저 빈도 가져와 테스트에서 사용할 수 있다. - 간단한 테스트 메소드를 추가하여 트랜잭션 매니저를 참조하는지 테스트해보자.
UserService의 모든 메소드에는 트랜잭션을 적용했으므로
테스트에서 각 메소드를 실행시킬 때는 기존에 진행 중인 트랜잭션이 없고
트랜잭션 전파 속성은 REQUIED이니 새로운 트랜잭션이 시작된다.
그리고 그 메소드를 정상적으로 종료하는 순간 트랜잭션은 커밋되면서 종료될 것이다.
- 스프링의 테스트 컨텍스트를 이용한 테스트에서는 @Autowired를 이용해
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 매니저를 참조하는 테스트
@Test
public void transactionSync() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
- 그렇다면 이 테스트 메소드에서 만들어지는 트랜잭션을 하나로 통합하여
모든 메소드가 하나의 트랜잭션 안에서 동작할 수는 없을까?- 메소드들의 트랜잭션 전파 속성이 모두 REQUIRED이므로 이 메소드들이 호출되기 전에 트랜잭션이 시작되게만 하면 된다.
- 그러므로 테스트에서 트랜잭션 매니저를 이용해 트랜잭션을 시작시키고
이를 동기화해주어 테스트도 트랜잭션 동기화에 참여하도록 하면 된다. - 트랜잭션을 시작하기 위해서는 먼저 트랜잭션 정의를 담은 오브젝트를 만들고
이를 트랜잭션 매니저에 제공하면서 새로운 트랜잭션을 요청하면 된다. - 이로 인해 메소드들은 이미 시작된 트랜잭션이 있으면 참여하고 새로운 트랜잭션을 만들지 않는다.
- 하지만 정말 이 메소드가 테스트 코드 내에서 시작된 트랜잭션에 참여하고 있는지는 알 수 없으므로
트랜잭션의 속성을 강제로 읽기전용으로 변경하고 테스트를 다시 해보면
읽기전용 트랜잭션에 대해 쓰기 작업을 했을 때의 예외가 발생한다.
이를 통해, 테스트 코드 내에서 시작한 트랜잭션에 메소드들이 참여하고 있다는 확신을 얻을 수 있다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 매니저를 참조하는 테스트
@Test
public void transactionSync() {
// 트랜잭션 매니저를 이용해 트랜잭션을 미리 시작하게 한다.
// 트랜잭션 정의는 기본 값을 사용한다.
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
// 읽기전용 트랜잭션으로 정의한다.
txDefinition.setReadOnly(true);
// 트랜잭션 매니저에게 트랜잭션을 요청한다.
// 기존에 시작된 트랜잭션이 없으니 새로운 트랜잭션을 시작시키고 트랜잭션 정보를 돌려준다.
// 동시에 만들어진 트랜잭션을 다른 곳에서도 사용할 수 있도록 동기화한다.
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
// 앞에서 만들어진 트랜잭션에 모두 참여한다.
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
// 앞에서 시작한 트랜잭션을 커밋한다.
transactionManager.commit(txStatus);
}
}
- 이런 방법은 선언적 트랜잭션이 적용된 서비스 메소드에만 적용되는 것이 아니다.
- JdbcTemplate과 같이 스프링이 제공하는 데이터 액세스 추상화를 적용한 DAO에도 동일한 영향을 미친다.
- JdbcTemplate은 개념은 조금 다르지만 트랜잭션 전파 속성이 REQUIRED인 것처럼
트랜잭션이 시작된 것이 있으면 그 트랜잭션에 자동으로 참여하고,
없으면 트랜잭션 없이 자동커밋 모드로 JDBC 작업을 수행한다.
그러므로 트랜잭션 매니저를 통한 커밋을 하지 않아도 동일한 결과를 얻을 수 있다.
- 트랜잭션이라면 당연히 롤백도 가능해야 한다.
- 테스트를 만들면 전체 트랜잭션이 한꺼번에 롤백되는지도 확인할 수 있다.
- 이렇게 테스트 코드에서 미리 트랜잭션을 시작해놓으면 직접 호출하는 DAO 메소드도 하나의 트랜잭션으로 묶을 수 있으며
트랜잭션의 결과나 상태를 조작하면서 테스트하는 것도 가능하다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 롤백 테스트
@Test
public void transactionRollback() {
// 트랜잭션을 롤백했을 때 돌아갈 초기 상태를 만들기 위해 트랜잭션 시작 전에 초기화를 해둔다.
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));
// userDao의 getCount() 메소드도 같은 트랜잭션에서 동작한다.
assertThat(userDao.getCount(), is(2));
// 강제로 롤백한다. 트랜잭션 시작 전 상태로 돌아가야 한다.
transactionManager.rollback(txStatus);
// add()의 작업이 취소되고 트랜잭션 시작 이전의 상태임을 확인할 수 있다.
assertThat(userDao.getCount(), is(0));
}
}
- 테스트 코드로 트랜잭션을 제어해서 적용할 수 있는 테스트 기법인 롤백 테스트가 있다.
- 테스트 내 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해거리는 테스트를 말한다.
- 롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고
테스트를 시작하기 전 상태로 만들어주기 때문에 DB에 영향을 주지 않는 장점이 많다.
어떤 경우에도 트랜잭션을 커밋하지 않기 때문에 테스트가 성공하든 실패하든 상관없으며 예외가 발생해도 괜찮다. - 그러므로 전체 테스트를 수행하기 전 여러 테스트에서 공통적으로 필요한 사용자 정보를 테스트 데이터로 넣어뒀다면,
롤백 테스트 덕분에 매 테스트마다 처음과 동일한 테스트 데이터로 테스트를 수행할 수 있다.
테스트에 따라 고유한 테스트 데이터가 필요하다면 테스트 앞부분에서 그에 맞게 DB를 초기화하고 테스트를 하면 된다. - 롤백 테스트는 심지어 여러 개발자가 하나의 공용 테스트용 DB를 사용할 수 있게도 해준다.
적절한 격리수준만 보장해주면 동시에 여러 개의 테스트가 진행돼도 상관없다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 롤백 테스트
@Test
public void rollbackTest() {
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
// 테스트 안의 모든 작업을 하나의 트랜잭션으로 통합한다.
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
} finally {
// 테스트 결과가 어떻든 상관없이 테스트가 끝나면 무조건 롤백한다.
// 테스트 중에 발생했던 DB의 변경사항은 모두 이전 상태로 복구된다.
transactionManager.rollback(txStatus);
}
}
}
테스트를 위한 트랜잭션 애노테이션
- @Transactionl 애노테이션을 통해 트랜잭션을 적용해주는 것을 테스트 클래스와 메소드에도 적용할 수 있다.
- 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다.
이를 이용하면 테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶어줄 수 있다.
그러므로 트랜잭션 매니저와 번거로운 코드를 사용하는 대신 애노테이션만으로 트랜잭션이 적용된 테스트를 만들 수 있다. - 물론 테스트에서 사용하는 @Transactional은 기본적인 동작방식과 속성은 UserService 등에 적용한 것과 동일하지만
AOP를 위한 것이 아니라 단지 컨텍스트 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 쓰일 뿐이다. - 이를 사용하면 테스트 메소드 안에서 실행되는 전파 속성이 REQUIRED인 메소드들이
테스트 메소드의 트랜잭션에 참여해서 하나의 트랜잭션으로 실행된다. - 앞에서 해봤던 것처럼 테스트의 트랜잭션을 읽기전용으로 바꾸면 동일한 예외가 발생한다.
- 또한 @Transactional은 테스트 클래스 레벨에 부여하고 각 메소드에도 지정할 수 있다.
이 경우 메소드의 트랜잭션 속성이 클래스의 속성보다 우선한다.
- 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 애노테이션 테스트
@Test
@Transactional
// @Transactional(readOnly = true)
public void transactionAnnotation() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
- 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백되므로
@Transactional이 부여된 테스트를 실행할 경우 DB의 테이블에 아무런 데이터가 남아 있지 않음을 확인할 수 있다.- 만약 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고 싶기는 하지만
강제 롤백을 원하지 않을 경우 @Rollback 애노테이션을 이용하면 된다. - @Transactional은 기본적으로 테스트에서 사용할 용도로 만든 게 아니기 때문에 롤백 테스트에 관한 설정을 담을 수 없다.
따라서 롤백 기능을 제어하려면 별도의 애노테이션을 사용해야 한다. - 테스트 메소드에 이를 설정해주면 테스트 전체에 걸쳐 하나의 트랜잭션이 만들어지고
예외가 발생하지 않는 한 트랜잭션은 커밋된다. - 하지만 @Rollback 애노테이션은 메소드 레벨에만 적용할 수 있다.
만약 테스트 클래스의 모든 메소드에 트랜잭션을 적용하면서 모든 트랜잭션이 롤백되지 않고 커밋되게 하려면
무식하게 모든 메소드에 @Rollback(false)를 적용하기 보다는
클래스 레벨에 @TransactionConfiguration 애노테이션을 이용하면 롤백에 대한 공통 속성을 지정할 수 있다.
(스프링 4.2부터는 @TransactionConfiguration이 사라지고 @Rollback을 클래스 레벨에서 사용할 수 있다.)
- 만약 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고 싶기는 하지만
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
@Autowired
PlatformTransactionManager transactionManager;
...
// 트랜잭션 애노테이션 테스트
@Test
@Transactional
// @Transactional(readOnly = true)
@Rollback(value = false)
public void transactionAnnotation() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
}
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 롤백 여부에 대한 기본 설정과 트랜잭션 매니저 빈을 지정하는데 사용할 수 있다.
// 디폴트 트랜잭션 매니저 아이디는 관례를 따라서 transactionManager로 되어 있다.
// 하지만 이는 더 이상 사용되지 않으므로 클래스 수준에서 @Rollback 애노테이션을 사용하도록 함
// @TransactionConfiguration(defaultRollback=false)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
// add() 메소드의 테스트
@Test
// 메소드에서 디폴트 설정과 그 밖의 롤백 방법으로 재설정할 수 있다.
@Rollback
public void add() {
userDao.deleteAll();
// GOLD 레벨이 이미 지정된 User라면 레벨을 초기화하지 않아야 한다.
User userWithLevel = users.get(4);
// 레벨이 비어 있는 사용자로 수정한다.
// 로직에 따라 등록 중에 BASIC 레벨이 설정되어야 한다.
User userWithoutLevel = users.get(0);
userWithoutLevel.setLevel(null);
userService.add(userWithLevel);
userService.add(userWithoutLevel);
// DB에 저장된 결과를 가져와 확인한다.
User userWithLevelRead = userDao.get(userWithLevel.getId());
User userWithoutLevelRead = userDao.get(userWithoutLevel.getId());
assertThat(userWithLevelRead.getLevel(), is(userWithLevel.getLevel()));
assertThat(userWithoutLevelRead.getLevel(), is(userWithoutLevel.getLevel()));
}
...
}
- 테스트 클래스에 @Transactional을 지정했을 때 굳이 트랜잭션이 필요 없는 메소드는 어떻게 해야 할까?
- 필요하지도 않은 트랜잭션이 만들어지는 것이 꺼림칙하거나 트랜잭션이 적용되면 안 되는 경우에는
해당 메소드에만 테스트 메소드에 의한 트랜잭션이 시작되지 않도록 만들어줄 수 있다. - @NotTransactional을 테스트 메소드에 부여하면
클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행한다.
물론 테스트 안에서 호출하는 메소드에서 트랜잭션을 사용하는데는 영향을 주지 않는다. - 또는 트랜잭션 테스트와 비 트랜잭션 테스트를 아예 클래스를 구분해서 만들도록 권장하거나
@Transactional의 트랜잭션 NEVER 전파 속성을 지정해 트랜잭션이 시작되지 않도록 할 수 있다.
- 필요하지도 않은 트랜잭션이 만들어지는 것이 꺼림칙하거나 트랜잭션이 적용되면 안 되는 경우에는
@Transactional(propagation=Propagation.NEVER)
- 위처럼 테스트 내에서 트랜잭션을 제어할 수 있는 네 가지 애노테이션을 잘 활용하면
DB가 사용되는 통합 테스트를 만들 때 매우 편리하다.- 일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와,
DB 같은 외부의 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는 게 좋다.
이후 DB가 사용되는 통합 테스트에 기본적으로 클래스 레벨에 @Transactional을 부여해주면 된다.
또한 DB가 사용되는 통합 테스트는 가능한 한 롤백 테스트로 만드는 게 좋다. - 테스트는 어떤 경우에도 서로 의존하면 안 된다.
테스트가 진행되는 순서나 앞의 테스트의 성공 여부에 따라서 다음 테스트의 결과가 달라지는 테스트를 만들면 안 된다. - 트랜잭션을 지원하는 롤백 테스트는 매우 유용한 도구가 돼줄 것이다.
- 일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와,
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (1) (0) | 2024.01.24 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (0) (0) | 2024.01.23 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (7) (0) | 2024.01.08 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (6) (0) | 2024.01.03 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (5) (0) | 2023.12.30 |