트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.
즉, 트랜잭션은 데이터베이스 작업이 원자적으로 수행되도록 보장하며, 모든 작업이 성공해야만 변경 사항이 커밋되고, 하나라도 실패하면 모든 변경 사항이 롤백된다.
트랜잭션은 다음과 같은 패턴이 반복된다.
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromId, toId, money); //비즈니스 로직
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비즈니스 로직 뿐이다. 이럴 때 템플릿 콜백 패턴을 활용해서 반복 문제를 해결할 수 있는데, 스프링에서는 TransactionTemplate이라는 기능을 제공한다.
txTemplate.executeWithoutResult((status) -> {
try {
bizLogic(fromId, toId, money); //비즈니스 로직
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
트랜잭션 템플릿 덕분에 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 모두 제거되었다. 하지만 아직 서비스 계층에 순수한 비즈니스 로직만 남기지는 못했다.
@Transactional을 사용하면 스프링이 AOP를 사용해서 트랜잭션을 편리하게 처리해준다. 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다. 트랜잭션 프록시 덕분에 서비스 계층에는 순수한 비즈니즈 로직만 남길 수 있다.


@Transactional 애노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리라 한다.선언적 트랜잭션 관리가 프로그래밍 방식에 비해서 훨씬 간편하고 실용적이기 때문에 실무에서는 대부분 선언적 트랜잭션 관리를 사용한다. 개발자는 트랜잭션이 필요한 곳에 @Transactional 애노테이션 하나만 추가하면 된다. 나머지는 스프링 트랜잭션 AOP가 자동으로 처리해준다.
트랜잭션은 여러 데이터베이스 작업을 하나의 논리적 단위로 묶어야 할 때 사용한다. JPA를 사용한다면 단일 작업에 대해서는 이미 @Transactional이 선언되어 있기 때문에 직접 선언할 필요가 없다.
가령 JpaRepository의 구현체의 save메서드를 보면 이미 @Transactional이 선언되어 있다.
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
테스트를 진행하기 위해 TestService를 작성한다. Member 엔티티의 email 컬럼은 유니크 제약 조건으로 설정하여, 동일한 이메일로 저장할 경우 RuntimeException예외가 발생한다.
@Service
@RequiredArgsConstructor
public class TestService {
private final MemberRepository memberRepository;
/**
* @Transactional으로 transactionalTest는 하나의 트랜잭션 내에서 실행되며,
* saveFailure에서 예외가 발생되어 이전 데이터도 롤백되어 데이터베이스에 반영되지 않는다.
*/
@Transactional
public void transactionalTest() {
saveSuccess();
saveFailure();
}
/**
* saveSuccess와 saveFailure는 별개의 트랜잭션으로 동작하며,
* saveFailure에서 예외가 발생되어도
* saveSuccess에서 저장한 데이터는 데이터베이스에 정상적으로 커밋된다.
*/
public void noTransactionalTest() {
saveSuccess();
saveFailure();
}
private void saveSuccess() {
Member member = Member.builder()
.email("test@email.com")
.build();
memberRepository.save(member);
}
private void saveFailure() {
Member member = Member.builder()
.email("test@email.com")
.build();
memberRepository.save(member);
}
}
테스트 코드를 작성하여 결과를 확인한다.
@SpringBootTest
class TestServiceTest {
@Autowired
private TestService testService;
@Autowired
private MemberRepository memberRepository;
@BeforeEach
void setUp() {
memberRepository.deleteAll();
}
@Test
@DisplayName("@Transactional 선언 시, 예외 발생 시 데이터가 롤백됨")
void transactionalTest() {
//given
long initialCount = memberRepository.count();
//when
assertThrows(RuntimeException.class, testService::transactionalTest);
//then
long finalCount = memberRepository.count();
assertEquals(initialCount, finalCount);
}
@Test
@DisplayName("@Transactional 선언하지 않을 시, 예외 발생 시 데이터가 롤백되지 않음")
void noTransactionalTest() {
//given
long initialCount = memberRepository.count();
//when
assertThrows(RuntimeException.class, testService::noTransactionalTest);
//then
long finalCount = memberRepository.count();
assertNotEquals(initialCount, finalCount);
}
}
테스트가 모두 성공하는 것을 확인할 수 있다.

트랜잭션은 기본적으로 읽기, 쓰기가 모두 가능한 트랜잭션이 생성된다. readOnly=true옵션을 사용하면 읽기 전용 트랜잭션이 생성되며 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다.
JPA에서는 다양한 최적화가 발생한다.