
Spring 을 사용하여 Scheduler API 를 구현하던 중, Service 에서 두개 이상의 repository 메서드 호출이 있을 경우 Transactional 을 사용였고 다음과 같은 피드백을 받았다.
읽기 요청 등 디비를 변경하지 않는 요청과 관련된 경우 Transactional 이 오히려 성능저하를 야기할 수 있다?
위와 같은 의문을 해결하기 위해 트랜잭션에 대해 알아보기로 했다.
@Transactional 애너테이션 정의@Transactional은 트랜잭션을 관리하기 위해 Spring 프레임워크에서 제공하는 애너테이션이다. 데이터의 일관성을 유지하고, 작업 도중 오류가 발생했을 때 트랜잭션을 롤백하여 DB 상태를 이전 상태로 복구할 수 있게 한다.
프록시 기반 AOP: @Transactional이 선언된 메서드는 Spring의 AOP 기능에 의해 프록시 객체가 생성된다. 프록시 객체는 실제 객체를 감싸며, 트랜잭션 시작, 커밋, 롤백 같은 트랜잭션 처리를 담당한다.
트랜잭션 경계 설정: 메서드가 호출되면 프록시가 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 커밋하여 변경 사항을 반영한다. 만약 메서드 실행 중 예외가 발생하면 트랜잭션을 롤백하여 작업을 취소한다.
전파 옵션: @Transactional의 propagation 옵션을 통해 트랜잭션 경계를 제어할 수 있다. 예를 들어, 이미 진행 중인 트랜잭션이 있을 경우 이를 재사용할지, 별도의 트랜잭션을 생성할지를 선택할 수 있다.
클래스 또는 메서드에 선언: @Transactional은 클래스나 메서드 레벨에 선언할 수 있다. 클래스에 선언할 경우, 해당 클래스의 모든 메서드가 트랜잭션 내에서 실행된다.
기본 속성:
@Transactional
public void someServiceMethod() {
// 트랜잭션 내에서 실행되는 로직
}
고급 설정:
@Transactional(
propagation = Propagation.REQUIRED, // 기본값: 이미 진행 중인 트랜잭션이 있으면 재사용
isolation = Isolation.DEFAULT, // 트랜잭션 격리 수준, 기본값은 데이터베이스 설정을 따름
timeout = 30, // 트랜잭션 최대 실행 시간(초)
rollbackFor = Exception.class // 특정 예외 발생 시 롤백 수행
)
public void someServiceMethod() {
// 트랜잭션 내에서 실행되는 로직
}
서비스에서 사용: 일반적으로 트랜잭션은 서비스 계층에 선언한다. 데이터베이스 작업을 포함한 비즈니스 로직이 서비스 계층에 위치하기 때문이다.
@Service
public class PostService {
@Transactional
public void createPost(Post post) {
postRepository.save(post);
}
}
컨트롤러에서의 사용 지양: 컨트롤러는 요청과 응답을 처리하는 역할을 담당하므로, 트랜잭션 관리가 필요한 경우 서비스 계층에서 트랜잭션을 적용하는 것이 바람직하다. 컨트롤러에 @Transactional을 사용하는 것은 데이터베이스 접근과 비즈니스 로직이 분리되지 않게 하여 유지보수와 확장성에 문제를 일으킬 수 있다.
트랜잭션을 사용하는 경우: 특정 배치 작업에서 다수의 데이터를 일괄로 업데이트할 때, 데이터 무결성이 중요한 경우 트랜잭션을 적용하여 작업 전체가 일관되게 수행되도록 한다. 예를 들어, 주문 처리 배치 작업에서 모든 주문 상태가 성공적으로 변경되어야 할 경우 트랜잭션이 필요하다.
트랜잭션을 사용하지 않는 경우: 각 작업이 독립적이고 다른 데이터에 영향을 주지 않는 경우(예: 단순 통계 데이터 집계 등), 트랜잭션을 사용하지 않고 개별 작업이 수행되게 하여 성능을 최적화할 수 있다. 실패한 작업은 추후 다시 시도하거나 로그를 통해 관리할 수 있다.
데이터의 일관성 보장 필요성: 데이터베이스에 여러 단계의 작업을 수행할 때 모든 작업이 원자성을 가져야 하는 경우 @Transactional을 사용한다.
롤백 조건: 특정 예외나 에러 발생 시 트랜잭션 전체를 취소해야 하는 경우 @Transactional을 적용하여 트랜잭션을 롤백할 수 있다.
성능: 트랜잭션 오버헤드를 고려하여 단일 조회 작업 등 트랜잭션이 불필요한 로직에는 @Transactional을 피한다.
동시성 제어: 트랜잭션 격리 수준을 통해 동시성 문제를 제어할 수 있다. 예를 들어, Isolation.READ_COMMITTED를 사용하여 특정 트랜잭션이 커밋한 데이터만 읽도록 제한할 수 있다.
트랜잭션 전파 옵션:
REQUIRED: 기본 옵션으로, 진행 중인 트랜잭션이 있으면 이를 사용하고, 없으면 새로 시작한다.REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며, 진행 중인 트랜잭션이 있으면 이를 일시 중지한다.트랜잭션 격리 수준: 격리 수준은 여러 트랜잭션 간의 데이터 접근 방법을 정의한다.
READ_UNCOMMITTED: 가장 낮은 격리 수준으로, 커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다.READ_COMMITTED: 커밋된 데이터만을 읽을 수 있다.REPEATABLE_READ: 트랜잭션 동안 같은 데이터를 여러 번 조회할 때 일관된 결과를 보장한다.SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 직렬화된 것처럼 동작하게 한다.