@Transactional, Proxy

김상진 ·2024년 12월 9일
0

CS

목록 보기
16/30

@Transactional, 프록시

JPA와 Spring에서 트랜잭션 처리를 위해 많이 사용하는 @Transactional과 프록시의 관계는 매우 밀접합니다. 이 글에서는 트랜잭션 처리 방식, 프록시의 역할, 그리고 주의해야 할 점에 대해 자세히 설명하겠습니다.


1. @Transactional이란?

@TransactionalSpring 프레임워크에서 제공하는 트랜잭션 관리를 위한 어노테이션입니다.
트랜잭션은 데이터의 일관성과 무결성을 보장하기 위해 하나의 작업 단위로 처리되는 개념입니다.
예를 들어 데이터베이스 연산에서 실패했을 경우 작업을 롤백하는 역할을 합니다.


트랜잭션의 기본 원리

  • 트랜잭션 시작 → 비즈니스 로직 실행 → 트랜잭션 종료
    • 정상 실행 시: commit
    • 예외 발생 시: rollback

Spring에서 @Transactional 동작 원리

Spring에서는 AOP (Aspect-Oriented Programming)프록시 패턴을 사용하여 트랜잭션을 관리합니다.
트랜잭션을 적용한 메서드는 프록시 객체를 통해 호출되며, 프록시 객체가 트랜잭션의 시작과 종료를 제어합니다.

작동 흐름

  1. 트랜잭션이 필요한 메서드 호출 → 프록시 객체가 대신 실행
  2. 프록시 객체는 트랜잭션 매니저에게 트랜잭션 시작을 요청 (begin)
  3. 비즈니스 로직 실행
  4. 메서드 실행이 정상 종료되면 commit, 예외 발생 시 rollback

@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");
        }
    }
}

실행 흐름 분석

  1. saveUser() 메서드 호출 시 프록시 객체가 대신 호출됩니다.
  2. 트랜잭션 시작: 프록시 객체가 트랜잭션을 시작합니다.
  3. userRepository.save() 실행: 데이터베이스 연산 수행
  4. 예외 발생 시: 프록시 객체가 트랜잭션을 rollback 합니다.
  5. 정상 실행 시: 프록시 객체가 트랜잭션을 commit 합니다.

2. 프록시 객체란?

프록시 객체실제 객체의 대리인 역할을 하는 객체입니다.
Spring의 @Transactional이 동작하기 위해 프록시 객체가 사용됩니다.


프록시 객체의 특징

  1. 프록시 객체는 진짜 객체 대신 동작하며, 메서드 실행 전후로 추가 작업(트랜잭션 시작/종료)을 수행합니다.
  2. 프록시는 리모컨처럼 동작하며, 진짜 객체를 감싸고 있습니다.
  3. 프록시 객체를 통해 메서드가 호출되기 때문에 트랜잭션 관리가 가능해집니다.

프록시 객체 코드 예시

@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(); // 예외 발생 시 롤백
        }
    }
}

3. 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");
    }
}

실행 결과

  1. outerMethod()는 프록시 객체를 통해 호출되므로 트랜잭션이 적용됩니다.
  2. 그러나 innerMethod()this를 사용해 직접 호출하므로 프록시를 거치지 않아 트랜잭션이 적용되지 않습니다.

해결 방법

자기 자신을 프록시 객체를 통해 호출하도록 해야 합니다.

1. 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");
    }
}

2. 별도의 서비스 클래스로 분리

내부 호출 문제를 피하기 위해 메서드를 다른 빈에 분리합니다.


4. Lazy Loading과 @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());
}

5. 결론

  • @Transactional프록시 객체를 통해 트랜잭션을 시작하고 종료합니다.
  • 자기 자신의 메서드를 호출할 때는 프록시를 거치지 않으므로 트랜잭션이 적용되지 않습니다.
  • Lazy Loading은 트랜잭션 범위 내에서만 동작하므로 트랜잭션이 종료되면 LazyInitializationException이 발생합니다.

출처 및 참고 자료

  1. Spring 공식 문서 - Transaction Management
  2. Hibernate User Guide
  3. Baeldung: Spring @Transactional
profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글

관련 채용 정보