오늘은 Spring Boot에서 가장 중요한 어노테이션 중 하나인 @Transactional에 대해 자세히 알아보도록 하겠습니다.
@Transactional은 Spring Framework에서 제공하는 선언적 트랜잭션 관리를 위한 어노테이션입니다. 데이터베이스 작업을 하나의 트랜잭션으로 처리하고자 할 때 사용됩니다.
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 데이터베이스 작업 수행
}
}
트랜잭션은 ACID 속성을 가집니다:
Spring은 AOP(Aspect-Oriented Programming)를 사용하여 @Transactional을 구현합니다:
// 내부 동작 예시
public class UserServiceProxy extends UserService {
public void createUser(User user) {
TransactionStatus status = null;
try {
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
super.createUser(user); // 실제 메서드 호출
transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
throw e;
}
}
}
@Transactional은 다양한 옵션을 제공합니다:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = -1,
readOnly = false,
rollbackFor = Exception.class,
noRollbackFor = {IllegalStateException.class}
)
각각의 전파 옵션을 실제 예시와 함께 자세히 설명해드리겠습니다:
@Transactional
public void parentMethod() { // 트랜잭션 시작
childMethod(); // 부모의 트랜잭션을 그대로 사용
}
@Transactional
public void childMethod() { // 새로운 트랜잭션 생성하지 않음
// 부모 트랜잭션이 있으면 그걸 사용
// 없으면 새로 생성
}
@Transactional
public void parentMethod() { // 트랜잭션 A 시작
childMethod(); // 트랜잭션 B 시작 (완전히 새로운 트랜잭션)
// childMethod가 실패해도 parentMethod는 커밋될 수 있음
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() { // 무조건 새로운 트랜잭션 생성
// 부모 트랜잭션과 완전히 독립적으로 동작
}
@Transactional(propagation = Propagation.SUPPORTS)
public void someMethod() {
// 트랜잭션이 있는 곳에서 호출되면 -> 해당 트랜잭션 사용
// 트랜잭션이 없는 곳에서 호출되면 -> 트랜잭션 없이 실행
}
@Transactional
public void parentMethod() {
mandatoryMethod(); // OK - 부모 트랜잭션이 있어서 실행 가능
}
public void nonTransactionalMethod() {
mandatoryMethod(); // 에러 발생! - 트랜잭션이 없어서 실행 불가
}
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryMethod() {
// 반드시 다른 트랜잭션 내부에서 호출되어야 함
// 단독으로 호출되면 예외 발생
}
@Transactional
public void parentMethod() {
neverMethod(); // 에러 발생! - 트랜잭션이 있는 상태에서 호출 불가
}
public void nonTransactionalMethod() {
neverMethod(); // OK - 트랜잭션이 없는 상태에서 호출
}
@Transactional(propagation = Propagation.NEVER)
public void neverMethod() {
// 트랜잭션이 있으면 예외 발생
// 트랜잭션이 없어야만 실행 가능
}
@Transactional
public void parentMethod() {
notSupportedMethod(); // 호출되는 동안 트랜잭션 잠시 중단
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedMethod() {
// 기존 트랜잭션을 일시 중단하고
// 트랜잭션 없이 실행
// 메서드 완료 후 기존 트랜잭션 재개
}
실무에서 주로 사용되는 옵션은:
나머지 옵션들은 특수한 경우에만 사용됩니다. MANDATORY, NEVER, NOT_SUPPORTED는 상대적으로 덜 사용되는 옵션들입니다.
기본적으로 @Transactional은 Runtime Exception과 Error가 발생했을 때만 롤백합니다. Checked Exception은 롤백하지 않습니다.
@Service
public class UserService {
// RuntimeException 발생시 롤백
@Transactional
public void createUser1() {
throw new RuntimeException(); // 롤백됨
throw new Exception(); // 롤백 안됨
}
// Exception이 발생하면 무조건 롤백
@Transactional(rollbackFor = Exception.class)
public void createUser2() {
throw new Exception(); // 롤백됨
}
// NullPointerException이 발생해도 롤백하지 않음
@Transactional(noRollbackFor = NullPointerException.class)
public void createUser3() {
throw new NullPointerException(); // 롤백 안됨
}
// 여러 예외 지정 가능
@Transactional(
rollbackFor = {SQLException.class, IOException.class},
noRollbackFor = {IllegalStateException.class}
)
public void createUser4() {
// SQLException, IOException -> 롤백
// IllegalStateException -> 롤백 안함
// 다른 RuntimeException -> 롤백
}
}
트랜잭션 수행 시간을 제한합니다. 단위는 초이며, -1은 no timeout을 의미합니다.
읽기 전용 트랜잭션임을 명시합니다. 성능 최적화와 실수 방지를 위해 사용됩니다.
컨트롤러에서 @Transactional을 사용하는 것은 가능하지만 권장되지 않습니다:
@Controller
public class UserController {
@Autowired
private UserService userService;
@Transactional // 동작은 하지만 권장되지 않음
public ResponseEntity<String> createUser() {
userService.saveUser(); // DB 작업 1
userService.saveUserLog(); // DB 작업 2
return ResponseEntity.ok("Success");
}
}
문제점:
1. 트랜잭션 범위가 너무 넓어짐
2. 프레젠테이션 계층과 비즈니스 로직의 분리 원칙 위배
3. 예외 처리가 복잡해짐
비즈니스 로직을 담당하는 서비스 레이어에서 트랜잭션을 관리하는 것이 가장 적절합니다:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser() {
// 비즈니스 로직 + DB 작업이 하나의 트랜잭션으로 처리됨
userRepository.save(user);
userRepository.saveLog(log);
}
}
장점:
1. 비즈니스 로직 단위로 트랜잭션 관리 가능
2. 실제 DB 작업과 가까운 위치에서 트랜잭션 관리
3. 명확한 예외 처리
레포지토리 레벨의 @Transactional은 주의해서 사용해야 합니다:
@Repository
public class UserRepository {
@Transactional // 주의 필요!
public void saveWithTransaction(User user) {
// DB 작업
}
}
주의사항:
1. JpaRepository의 기본 메서드들은 이미 @Transactional이 적용되어 있음
2. 트랜잭션 범위가 너무 작아질 수 있음
3. 비즈니스 로직 단위의 트랜잭션 관리가 어려움
서비스와 레포지토리에 @Transactional이 중복으로 있는 경우를 살펴보겠습니다:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional // 외부 트랜잭션
public void createUser() {
userRepository.saveWithTransaction(new User());
}
}
@Repository
public class UserRepository {
@Transactional // 내부 트랜잭션
public void saveWithTransaction(User user) {
// DB 작업
}
}
이 경우 트랜잭션 전파 속성(Propagation)에 따라 동작이 달라집니다:
Propagation.REQUIRED를 사용할 경우@Repository
public class UserRepository {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveWithTransaction(User user) {
// 새로운 트랜잭션 시작
}
}
// Case 1: REQUIRED (기본값)
@Transactional (Service) // 트랜잭션 시작
@Transactional (Repository) // 같은 트랜잭션 사용
// DB 작업
// Repository 트랜잭션 종료
// Service 트랜잭션 종료 (실제 커밋/롤백 발생)
// Case 2: REQUIRES_NEW
@Transactional (Service) // 외부 트랜잭션 시작
@Transactional(REQUIRES_NEW) // 새로운 트랜잭션 시작
// DB 작업
// 내부 트랜잭션 종료 (커밋/롤백)
// 외부 트랜잭션 종료 (커밋/롤백)
권장사항:
1. 일반적으로 중복 @Transactional은 피하는 것이 좋습니다
2. 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 관리하는 것이 바람직
3. 특별한 이유가 없다면 레포지토리의 @Transactional은 제거하는 것이 좋음
4. JpaRepository의 기본 메서드들은 이미 트랜잭션이 적용되어 있으므로 추가로 설정할 필요 없음
실제로는 중복으로 설정하더라도 큰 문제는 발생하지 않지만, 코드의 명확성과 유지보수를 위해 서비스 계층에만 @Transactional을 사용하는 것이 좋습니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// @Transactional 없음
public User findUser(Long id) {
return userRepository.findById(id);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional(readOnly = true)
public UserDTO findUserWithDetails(Long id) {
User user = userRepository.findById(id);
List<Order> orders = orderRepository.findByUserId(id);
return new UserDTO(user, orders); // 데이터 일관성 보장
}
}
JPA에서 @Transactional의 동작을 이해하기 위해서는 영속성 컨텍스트의 핵심 개념인 스냅샷과 플러시를 이해해야 합니다.
@Transactional
public void updateUser(Long id) {
User user = userRepository.findById(id); // 스냅샷 생성
// 최초 상태: name="John", age=20
user.setName("Mike"); // 엔티티 변경
// 트랜잭션 종료시 스냅샷과 비교해서
// name이 "John"에서 "Mike"로 변경된 것을 감지
} // UPDATE 쿼리 실행
@Transactional
public void process() {
User user = new User("John");
userRepository.save(user); // 영속성 컨텍스트에만 존재
// 1. 강제 플러시
entityManager.flush();
// 2. 자동 플러시
List<User> users = userRepository.findAll(); // JPQL 실행시
// 3. 트랜잭션 커밋시 자동 플러시
}
플러시가 발생하는 시점:
1. 트랜잭션 커밋시
2. JPQL 쿼리 실행시
3. 강제로 flush() 호출시
@Service
public class UserService {
@Transactional // 일반 트랜잭션
public void updateUser(Long id) {
User user = userRepository.findById(id);
// 1. 스냅샷 생성 (메모리 사용)
user.setName("Mike");
// 2. 변경 감지 수행
// 3. 플러시 발생 (UPDATE 쿼리 실행)
}
@Transactional(readOnly = true) // 읽기 전용
public User findUser(Long id) {
return userRepository.findById(id);
// 1. 스냅샷 생성 안함 (메모리 절약)
// 2. 변경 감지 안함
// 3. 플러시 호출 안함
}
}
readOnly = true의 장점:
1. 스냅샷을 만들지 않아 메모리 사용량 감소
2. 변경 감지(Dirty Checking)를 하지 않아 CPU 사용량 감소
3. 불필요한 플러시 호출 방지로 성능 향상
4. DB에 읽기 전용임을 알려줘서 DB 내부적으로 최적화 가능
또한 @Transactional(readOnly = true)를 사용할 때의 제약사항과 특징은 아래와 같습니다:
@Service
public class UserService {
@Transactional(readOnly = true)
public void someMethod() {
// 1. 엔티티 수정 시도 시 예외 발생
user.setName("새이름"); // 예외 발생!
// 2. save 메서드 호출 시 예외 발생
userRepository.save(user); // 예외 발생!
// 3. @Modifying 쿼리 실행 시 예외 발생
userRepository.updateUsername(1L, "새이름"); // 예외 발생!
// 4. JPA의 dirty checking 비활성화
User user = userRepository.findById(1L);
user.setName("새이름"); // 변경 감지 자체가 동작하지 않음
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query("UPDATE User u SET u.username = :username WHERE u.id = :id")
void updateUsername(Long id, String username);
}
readOnly = true 설정 시 발생하는 제약:
엔티티 변경 감지(Dirty Checking) 비활성화
INSERT, UPDATE, DELETE 쿼리 실행 불가
flush 모드가 NEVER로 설정
데이터베이스 최적화
실수 예방 효과:
@Service
public class UserService {
@Transactional(readOnly = true)
public UserDTO getUserInfo(Long id) {
User user = userRepository.findById(id);
// 실수로 데이터를 변경하려 해도 예외가 발생하거나 무시됨
user.setLastAccessDate(LocalDateTime.now()); // 이런 실수를 방지
return new UserDTO(user);
}
}
이러한 제약사항들 덕분에:
1. 읽기 전용 메서드에서 실수로 데이터를 수정하는 것을 방지
2. 코드 리뷰 시 메서드의 의도가 명확히 드러남 (조회 전용)
3. JPA/Hibernate의 불필요한 처리가 줄어 성능 향상
@Transactional은 Spring AOP를 기반으로 동작하기 때문에 프록시 내부 호출시 트랜잭션이 적용되지 않는 문제가 발생할 수 있습니다.
@Service
public class UserService {
@Transactional
public void createUser(User user) { // 외부에서 호출 시 트랜잭션 적용됨
userRepository.save(user);
}
public void processUser(User user) {
createUser(user); // 내부 호출 시 트랜잭션 적용 안됨!
// 다른 로직들...
}
}
Spring AOP의 동작 방식 때문에 발생합니다:
1. Spring은 @Transactional이 적용된 클래스의 프록시 객체를 생성
2. 외부에서 호출할 때만 이 프록시를 통해 호출됨
3. 내부 메서드 호출은 프록시를 거치지 않고 직접 호출되어 트랜잭션 적용 안됨
프록시의 실제 동작을 보면 다음과 같습니다:
// Spring이 생성하는 프록시 클래스 (개념적인 예시)
class UserServiceProxy extends UserService {
UserService target; // 실제 UserService 객체
@Override
public void createUser(User user) {
// 트랜잭션 시작
try {
target.createUser(user);
// 트랜잭션 커밋
} catch (Exception e) {
// 트랜잭션 롤백
}
}
@Override
public void processUser(User user) {
// 프록시 없이 직접 호출됨
target.processUser(user);
}
}
@Service
public class UserService {
@Autowired
private UserService self; // 프록시 객체 주입
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
public void processUser(User user) {
self.createUser(user); // 프록시를 통한 호출
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
}
@Service
public class UserProcessService {
@Autowired
private UserService userService;
public void processUser(User user) {
userService.createUser(user); // 다른 클래스를 통한 호출
}
}
@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
public void processUser(User user) {
((UserService) AopContext.currentProxy())
.createUser(user); // 현재 프록시를 통한 호출
}
}
자기 자신 주입
메서드 분리 (권장)
AopContext 사용
적절한 범위 설정
readOnly 속성 활용
예외 처리 전략
전파 옵션 이해
@Transactional은 Spring Boot에서 데이터 무결성을 보장하는 핵심적인 기능입니다. 올바른 사용을 위해서는 각 속성의 의미와 동작 방식을 잘 이해하고, 적절한 상황에 맞게 사용하는 것이 중요합니다.