JPA와 Spring에서 트랜잭션 처리를 위해 많이 사용하는 @Transactional
과 프록시의 관계는 매우 밀접합니다. 이 글에서는 트랜잭션 처리 방식, 프록시의 역할, 그리고 주의해야 할 점에 대해 자세히 설명하겠습니다.
@Transactional
이란?@Transactional
은 Spring 프레임워크에서 제공하는 트랜잭션 관리를 위한 어노테이션입니다.
트랜잭션은 데이터의 일관성과 무결성을 보장하기 위해 하나의 작업 단위로 처리되는 개념입니다.
예를 들어 데이터베이스 연산에서 실패했을 경우 작업을 롤백하는 역할을 합니다.
@Transactional
동작 원리Spring에서는 AOP (Aspect-Oriented Programming)와 프록시 패턴을 사용하여 트랜잭션을 관리합니다.
트랜잭션을 적용한 메서드는 프록시 객체를 통해 호출되며, 프록시 객체가 트랜잭션의 시작과 종료를 제어합니다.
begin
) @Transactional
의 코드 예제@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void saveUser(User user) {
userRepository.save(user);
// 일부러 예외를 발생시켜 롤백 확인
if (user.getName().equals("error")) {
throw new RuntimeException("Rollback test");
}
}
}
saveUser()
메서드 호출 시 프록시 객체가 대신 호출됩니다. userRepository.save()
실행: 데이터베이스 연산 수행 프록시 객체는 실제 객체의 대리인 역할을 하는 객체입니다.
Spring의 @Transactional
이 동작하기 위해 프록시 객체가 사용됩니다.
@Transactional
public class UserService {
public void saveUser(User user) {
// 로직 실행
}
}
Spring은 위 클래스를 다음과 같은 구조로 변환합니다:
public class UserService$$Proxy {
private final UserService target;
public void saveUser(User user) {
try {
// 트랜잭션 시작
beginTransaction();
target.saveUser(user); // 진짜 객체 실행
commitTransaction(); // 트랜잭션 종료
} catch (Exception e) {
rollbackTransaction(); // 예외 발생 시 롤백
}
}
}
this
를 통한 메서드 호출 문제Spring의 @Transactional
은 프록시 객체를 통해 트랜잭션을 관리합니다.
그런데 같은 클래스의 메서드를 this
로 호출하면 프록시 객체를 거치지 않기 때문에 트랜잭션이 적용되지 않습니다.
@Service
public class UserService {
@Transactional
public void outerMethod() {
System.out.println("Outer method start");
this.innerMethod(); // this를 사용한 내부 호출(진짜 객체를 호출 함으로 @Transacional 적용 X)
System.out.println("Outer method end");
}
@Transactional
public void innerMethod() {
System.out.println("Inner method start");
}
}
outerMethod()
는 프록시 객체를 통해 호출되므로 트랜잭션이 적용됩니다. innerMethod()
는 this
를 사용해 직접 호출하므로 프록시를 거치지 않아 트랜잭션이 적용되지 않습니다.자기 자신을 프록시 객체를 통해 호출하도록 해야 합니다.
ApplicationContext
를 통해 호출@Service
public class UserService {
@Autowired
private ApplicationContext context;
@Transactional
public void outerMethod() {
System.out.println("Outer method start");
UserService self = context.getBean(UserService.class);
self.innerMethod(); // 프록시 객체를 통해 호출
System.out.println("Outer method end");
}
@Transactional
public void innerMethod() {
System.out.println("Inner method start");
}
}
내부 호출 문제를 피하기 위해 메서드를 다른 빈에 분리합니다.
@Transactional
의 관계Lazy Loading을 사용하면 연관된 엔티티가 프록시 객체로 초기화됩니다.
이 프록시 객체는 트랜잭션 범위 내에서만 사용할 수 있습니다.
@Transactional
public void fetchLazyEntity(Long parentId) {
Parent parent = parentRepository.findById(parentId).orElseThrow();
System.out.println("Parent retrieved");
// Lazy 로딩된 자식 엔티티에 접근
System.out.println("Number of children: " + parent.getChildren().size());
}
@Transactional을 사용하지 않으면 부모 엔터티를 조회하는 시점에 프록시 객체는 트랜잭션 범위 내에서만 초기화됩니다. 이때 데이터베이스 세션은 트랜잭션과 함께 시작되고 즉시 종료됩니다.
트랜잭션이 종료된 후 Lazy 로딩된 자식 엔터티에 접근하면 Hibernate는 이미 데이터베이스 세션을 닫았기 때문에 프록시 객체를 초기화하지 못하고 LazyInitializationException이 발생합니다.
public void fetchLazyEntity(Long parentId) {
Parent parent = parentRepository.findById(parentId).orElseThrow();
System.out.println("Parent retrieved");
// 트랜잭션 종료 후 Lazy 로딩 접근 → LazyInitializationException 발생
System.out.println("Number of children: " + parent.getChildren().size());
}
@Transactional
은 프록시 객체를 통해 트랜잭션을 시작하고 종료합니다. LazyInitializationException
이 발생합니다.