JPA의 변경 내용 (저장/수정/삭제)은 트랜잭션이 있을 때만 데이터베이스에 안전하게 반영된다.
JPA에서는 서비스 계층에서 트랜잭션을 시작하며, Spring은 @Transactional 어노테이션을 사용해 트랜잭션을 자동으로 관리한다.
기본적으로 트랜잭션을 시작한 메서드가 성공적으로 종료될 때 Commit된다. Spring 환경에서는 @Transactional 어노테이션이 붙은 메서드가 예외 없이 실행을 마치는 시점에 Commit이 발생한다.
@Transactional트랜잭션 관리를 지원하는 선언적 어노테이션이다. 메서드/클래스에 붙이면, 해당 범위 내의 모든 데이터베이스 작업을 트랜잭션 단위로 처리한다. 모든 작업이 성공하면 Commit, 중간에 예외가 발생하면 자동으로 Rollback 처리된다.
| 속성 | 기본값 | 설명 |
|---|---|---|
propagation | REQUIRED | 트랜잭션 전파 방식 (REQUIRED, REQUIRES_NEW, etc.) |
isolation | DEFAULT | 트랜잭션 격리 수준 (READ_COMMITTED, REPEATABLE_READ, etc.) |
readOnly | false | 읽기 전용 트랜잭션으로 설정 (삽입/수정/삭제 없이 조회만 하는 경우 성능 향상) |
timeout | -1 | 트랜잭션 제한 시간 (초), 초과 시 Rollback |
rollbackFor | {} | 지정한 예외 발생 시에만 Rollback (Default는 RuntimeException, Error 발생 시 Rollback) |
noRollbackFor | {} | 지정한 예외 발생 시에는 Rollback하지 않음 |
DB 변경이 있는 오퍼레이션을 수행하는 서비스 메서드에는 @Transactional을 사용하는 것을 권장한다. 그러나 남용하는 경우, 트랜잭션 범위가 넓어져 DB lock, 동시성 저하 등 성능 문제가 생길 수 있다.
@Transactional(readOnly = true)를 사용하거나 사용하지 않는다.트랜잭션이 이미 존재할 때, 현재 실행 중인 메서드에서 트랜잭션을 어떻게 처리할지 결정한다.
| 옵션명 | 설명 | 사용 예시 |
|---|---|---|
REQUIRED (default) | 기존 트랜잭션이 있으면 참여하고, 없으면 새로 생성한다. | 대부분의 서비스 로직 |
REQUIRES_NEW | 항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션은 일시중단한다. | 로그 기록, 감사 |
SUPPORTS | 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행한다. | 조회성 로직 |
MANDATORY | 반드시 트랜잭션 내에서만 실행하고, 없으면 예외가 발생한다. | 보안 로직 |
NOT_SUPPORTED | 트랜잭션이 있으면 일시 중단하고, 트랜잭션 없이 실행한다. | 외부 시스템 연동, 대용량 데이터 연동 |
NEVER | 트랜잭션 없이만 실행하고, 트랜잭션이 있으면 예외가 발생한다. | 트랜잭션과 같이 동작하면 안 되는 로직 |
NESTED | 기존 트랜잭션이 있으면 중첩 (새 savepoint)하고, 없으면 새 트랜잭션을 생성한다. | 부분 롤백 지원이 필요한 경우 |
하나의 클래스 내에서 @Transactional이 붙은 다른 메서드를 호출하면, 프록시를 거치지 않고 실제 객체의 내부 메서드를 직접 호출하기 때문에 트랜잭션 전파 설정이 적용되지 않는다.
@Service
public class UserService {
@Transactional // (propagation = Propagation.REQUIRED) Default
public void outerMethod() {
// DB 작업 1
innerMethod(); // 내부 호출 (프록시 X)
}
@Transactional (propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
// DB 작업 2
}
}
outerMethod()가 Transaction A를 시작하고, innerMethod()는 프록시를 거치지 않아 REQUIRES_NEW가 적용되지 않아 Transaction A 안에서 그대로 실행된다. 결국, 두 작업이 하나의 트랜잭션에서 Commit/Rollback된다.
이를 해결하기 위해서, 트랜잭션 단위가 다른 메서드는 별도의 클래스로 분리하여 주입받아 사용한다.
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional // (propagation = Propagation.REQUIRED)
public void outerMethod() {
// DB 작업 1
innerService.innerMethod();
// DB 작업 3
}
}
@Service
public class InnerService {
@Transactional // (propagation = Propagation.REQUIRED)
public void innerMethod() {
// DB 작업 2
// 만약 여기서 예외가 발생하여 롤백 되면,
// outerMethod의 'DB 작업 1'까지 모두 롤백 된다.
}
}
@Transactional은 기본적으로 RuntimeException (Unchecked Exception)과 Error가 발생했을 때만 Rollback한다. 따라서 Checked Exception이 발생하면 기본 정책 상 Rollback이 아니라 Commit이 된다. 또한try-catch로 해당 예외를 삼켜도 정상 리턴으로 간주되므로, Commit되어 데이터 정합성 문제가 발생할 수 있다. 따라서 모든 예외에 대해 Rollback하고 싶다면 rollbackFor 속성을 사용한다.
여러 사용자가 동시에 데이터에 접근하고 수정할 때 발생할 수 있는 데이터 불일치 문제를 방지하기 위한 제어로, JPA는 낙관적 Lock과 비관적 Lock을 제공한다.
트랜잭션 충돌이 자주 발생하지 않을 것이라고 가정하는 방식이다. DB 레코드 자체에 lock을 걸지 않고, @Version을 사용하여 데이터의 변경 여부를 확인한다.
@Entity
public class Article {
@Id
@GeneratedValue
private Long id;
private String content;
@Version
private Long version;
}
엔터티에 @Version 어노테이션이 붙은 필드 (주로 숫자 타입)을 추가하여, 데이터를 조회할 때 함께 가져온다. 데이터를 수정하고 트랜잭션을 커밋할 때, 현재 데이터베이스의 버전과 조회했던 시점의 버전을 비교한다. 버전이 일치하면 데이터를 수정하고 버전을 1 증가시키고, 일치하지 않으면 (다른 트랜잭션이 먼저 수정한 경우), ObjectOptimisticLockingFailureException을 발생시키고 수정을 실패 처리한다.
트랜잭션 충돌이 자주 발생할 것이라고 가정한다. 데이터에 접근 시점부터 데이터베이스 레코드에 직접 lock을 걸고, 다른 트랜잭션은 해당 lock이 해제될 때까지 접근할 수 없다.
PESSIMISTIC_WRITE: 데이터를 수정하기 위해 설정한다. 다른 트랜잭션은 해당 데이터를 읽거나 쓸 수 없다.PESSIMISTIC_READ: 다른 트랜잭션이 해당 데이터를 수정하는 것을 방지한다. DB에 따라 읽기는 허용될 수 있다.