bkdragon's log

Transactional 원리 본문

Java Spring

Transactional 원리

bkdragon 2024. 9. 24. 12:17

@Transactional 은 굉장히 선언적으로 사용할 수 있다. 여기서 선언적이라는 말은 사용자가 정확한 원리를 알 필요 없이 추상화 되어있고 내부적으로 처리된다는 뜻이다.

아래는 유저를 추가하고 로그를 남기는 메서드이다.

 @Transactional
    public void createUserAndLog(String userName) {
        // 유저 생성
        User user = new User();
        user.setName(userName);
        userRepository.save(user);  // User 저장 (DB에 INSERT 실행됨)

        // 로그 기록
        Log log = new Log();
        log.setMessage("User " + userName + " created.");
        logRepository.save(log);  // 로그 저장 (DB에 INSERT 실행됨)
    }

이렇게 간단하게 Transaction 을 구현할 수 있다.

@Transactional을 사용하지 않으면 어떻게 될까? 직접 TransactionManager를 통해서 구현해야한다.

// 트랜잭션 매니저 주입
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createUserAndLog(String userName) {
        // 트랜잭션 정의 (기본 트랜잭션 정의 사용)
        TransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def); // 트랜잭션 시작

        try {
            // 유저 생성
            User user = new User();
            user.setName(userName);
            userRepository.save(user);  // User 저장 (DB에 INSERT 실행됨)

            // 로그 기록
            Log log = new Log();
            log.setMessage("User " + userName + " created.");
            logRepository.save(log);  // 로그 저장 (DB에 INSERT 실행됨)

            // 트랜잭션 커밋
            transactionManager.commit(status);
        } catch (Exception e) {
            // 예외 발생 시 트랜잭션 롤백
            transactionManager.rollback(status);
            throw e;  // 예외 재전파
        }
    }

이 코드도 많이 어려워보지 않고 나쁘지 않다고 생각할 수 있지만 트랜잭션이 필요할 때마다 반복해야하고 메서드가 비지니스 로직 이외의 로직이 포함이 되기 때문에 좋지않다.

그렇다면 @Transactional 은 어떻게 위의 코드를 아래처럼 동작하게 할까? 해답은 Spring AOP 기반의 프록시 객체이다.

@Transactional 이 적용된 메서드가 호출될 때, Spring은 해당 클래스의 프록시 객체를 생성한다. 프록시 객체는 원본 객체에 접근을 제어하고 메서드 호출 전후로 동작을 제어할 수 있다. 아래는 프록시 패턴의 간단한 예제이다.

// Subject (인터페이스)
public interface Service {
    void performTask();
}

// RealSubject (실제 객체)
public class RealService implements Service {
    @Override
    public void performTask() {
        System.out.println("실제 서비스 로직 실행");
    }
}

// Proxy (프록시 객체)
public class ServiceProxy implements Service {
    private final Service realService;

    public ServiceProxy(Service realService) {
        this.realService = realService;
    }

    @Override
    public void performTask() {
        System.out.println("프록시: 실제 서비스 호출 전 추가 작업");
        realService.performTask();  // 실제 객체 호출
        System.out.println("프록시: 실제 서비스 호출 후 추가 작업");
    }
}

(딱 보니 비지니스 로직 전후로 트랜잭션을 열고 닫는 로직이 실행될 것 같지 않은가?)

Spring은 두가지 방식으로 프록시 객체를 생성한다. JDK Dynamic Proxy(interface 기반), CGLIB(Code Generation Library, Class 기반)

Spring은 기본적으로 인터페이스가 있으면 JDK Dynamic Proxy를 사용하고, 없으면 CGLIB를 사용한다. 그러나 Spring Boot 2.0부터는 기본적으로 CGLIB를 사용하도록 설정되어 있다.

우선 JDK Dynamic Proxy는 인터페이스 기반이여서 인터페이스가 있을 때 실행되는 방식이다.

public interface UserService {
    void createUserAndLog(String userName);
}

이제 이 인터페이스를 구현하는 프록시 객체를 만들텐데 만약 메서드가 여러개면 모든 메서드에 중복되게 트랜잭션 로직을 추가해줘야 하지만...

InvocationHandler 인터페이스를 활용하면 중복을 제거하면서 트랜잭션 로직을 추가할 수 있다.

public interface InvocationHandler extends Callback {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

InvocationHandler 구현체 (실제론 자동 생성)

public class TransactionalProxyHandler implements InvocationHandler {

    private final Object target;  // 실제 서비스 객체
    private final PlatformTransactionManager transactionManager;

    public TransactionalProxyHandler(Object target, PlatformTransactionManager transactionManager) {
        this.target = target;
        this.transactionManager = transactionManager;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());  // 트랜잭션 시작
        try {
            Object result = method.invoke(target, args);  // 실제 메서드 호출
            transactionManager.commit(status);  // 트랜잭션 커밋
            return result;
        } catch (Exception e) {
            transactionManager.rollback(status);  // 예외 발생 시 트랜잭션 롤백
            throw e;
        }
    }
}

메서드 마다 추가하는 것이 아니라 메서드가 실행되기 전에 invoke 를 실행하는 방식이다.

여기서 Method 객체는 Reflection API라고 구체적인 클래스 타입을 알지 못해도 런타임에서 클래스의 정보에 접근할 수 있게 해주는 자바 API 이다. (느리다.)

UserService realService = new UserServiceImpl();  // 실제 서비스 객체
PlatformTransactionManager txManager = // 트랜잭션 매니저

UserService proxyService = (UserService) Proxy.newProxyInstance(
        realService.getClass().getClassLoader(),
        new Class[] { UserService.class },  // 프록시가 구현할 인터페이스
        new TransactionalProxyHandler(realService, txManager)  // 트랜잭션 관리
);

Proxy.newProxyInstance 를 통해서 프록시 객체를 생성하는데 이때 세번째 인자가 InvocationHandler이다. 이제 UserService 프록시 객체의 메서드를 실행하면 invoke가 실행이 되는것이다.

CGLIB는 상속하고 메서드를 오버라이딩해서 프록시를 구현한다.

public interface MethodInterceptor extends Callback {
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

CGLIB를 사용한 프록시 구현은 다음과 같다

public class TransactionalMethodInterceptor implements MethodInterceptor {
    private final Object target;
    private final PlatformTransactionManager transactionManager;

    public TransactionalMethodInterceptor(Object target, PlatformTransactionManager transactionManager) {
        this.target = target;
        this.transactionManager = transactionManager;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object result = proxy.invoke(target, args);
            transactionManager.commit(status);
            return result;
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

CGLIB를 사용하여 프록시 객체를 생성하는 방법:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new TransactionalMethodInterceptor(new UserServiceImpl(), txManager));
UserService proxyService = (UserService) enhancer.create();

여기서 Enhancer는 CGLIB의 핵심 클래스로, 동적으로 프록시 객체를 생성하는 역할을 한다. Enhancer의 주요 메서드와 기능은 다음과 같다.

  1. setSuperclass(Class): 프록시할 클래스를 지정한다. 이 클래스를 상속받아 프록시 객체를 생성한다.
  2. setCallback(Callback): 프록시 객체의 메서드 호출을 가로채고 처리할 콜백을 설정한다. 여기서는 MethodInterceptor를 구현한 TransactionalMethodInterceptor를 사용한다.
  3. create(): 설정된 정보를 바탕으로 실제 프록시 객체를 생성하고 반환한다.

Enhancer는 지정된 클래스의 바이트코드를 조작하여 새로운 서브클래스를 동적으로 생성한다. 이 서브클래스는 원본 클래스의 모든 메서드를 오버라이드하며, 각 메서드에서 설정된 Callback (여기서는 MethodInterceptor)을 호출한다.

CGLIB는 final 클래스나 메서드에는 적용할 수 없다는 제한이 있다.

간단하게 JDBC 내부 동작도 살펴보자. Transaction Manager로 트랜잭션을 시작하면 내부적으로 아래와 같은 코드가 실행된다.

  1. 트랜잭션 시작:
  2. connection.setAutoCommit(false);
  3. 트랜잭션 커밋:
  4. connection.commit(); connection.setAutoCommit(true);
  5. 트랜잭션 롤백:
  6. connection.rollback(); connection.setAutoCommit(true);

이런 메커니즘 덕분에 개발자는 비즈니스 로직에만 집중할 수 있다.

'Java Spring' 카테고리의 다른 글

웹 기술의 발전과정  (0) 2024.11.12
Specification 과 Criteria API  (0) 2024.11.08
JPA와 하이버네이트  (2) 2024.10.24
JDBC 부터 Spring Data JPA 까지  (0) 2024.09.20