spring

[Spring] 단 한 줄로 트랜잭션 제어하기 - @Transactional의 강력한 힘

사이버나그네 2025. 3. 14. 17:53

[배경]

 개발자가 애플리케이션에서 작업의 연속성을 관리해야 하는 일은 쉽지 않습니다. 은행에서 송금을 하고 있는데, 돈만 빠져나가고 상대 계좌에 입금이 안 된다면 어떨까요? 이처럼 개발자가 트랜잭션 관리의 중요성을 간과한다면 예상치 못한 오류로 인해 심각한 문제를 일으킬 수 있습니다. Java에서 이러한 위험을 막아주는 강력한 도구가 바로 @Transactional입니다. @Transactional 어노테이션은 한 줄의 코드로 데이터베이스와의 상호작용에서 수동적인 오류 방지 부터 모든 것을 제어할 수 있는 힘을 제공합니다. 특히 여러 개의 데이터 조작 작업이 포함된 서비스에서는 트랜잭션을 적절히 적용해야 데이터의 정합성을 보장할 수 있습니다. @Transactional 애너테이션은 이러한 트랜잭션을 간편하게 관리할 수 있도록 도와줍니다. 이번 글에서는 @Transactional의 동작 원리, 주요 옵션, 내부적인 작동 방식, 그리고 사용 시 주의할 점까지 자세히 알아보겠습니다.


 

[내용]

1. @Transactional이란?

 @Transactional은 Spring 프레임워크에서 제공하는 어노테이션으로, 데이터베이스 작업에서 트랜잭션을 자동으로 관리해주는 역할을 합니다. 특히 여러 데이터베이스 작업이 하나의 논리적 작업으로 이루어져야 할 때, 오류 발생 시 작업을 취소하거나 롤백하여 데이터의 일관성을 유지할 수 있도록 합니다.

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUserName(Long userId, String newName) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new IllegalArgumentException("User not found"));
        user.setName(newName);
        userRepository.save(user); // 트랜잭션 내에서 실행
    }
}

 

 위 코드에서 updateUserName 메서드는 @Transactional 덕분에 트랜잭션 범위 안에서 실행됩니다. 만약 save 메서드에서 예외가 발생하면, 모든 변경 사항이 롤백됩니다.

 

2. @Transactional 동작 원리 

 

1) @Transactional이 적용된 메서드 실행 흐름

 

  • 트랜잭션 시작: 메서드가 실행되기 전에 트랜잭션을 시작
  • 비즈니스 로직 수행: 메서드 내부의 데이터 변경 작업 수행
  • 정상 종료 시 → commit() 실행
  • 예외 발생 시 → rollback() 실행

 

2) JDK Proxy vs CGLIB 방식으로 본 @Transactional 동작 원리

@Transactional은 스프링 프레임워크 를 통해 프록시 객체를 생성하여 사용됩니다. 스프링에서 사용하는 프록시 구현체는 JDK Proxy(Dynamic Proxy), CGLib 두 가지가 있습니다.

스프링에서 Target 객체를 직접 참조하지 않고, 프록시 객체를 사용하는 이유는, Aspect 클래스에서 제공하는 부가 기능을 사용하기 위해서입니다. Target 객체를 직접 참조하는 경우, 원하는 위치에서 직접 Aspect 클래스를 호출해야하기 때문에 유지보수가 어려워지기에 프록시 개체를 사용합니다.

 

출처 : https://sasca37.tistory.com/267

 

  • AOP 프록시 생성과정에서 Target 객체의 인터페이스 구현 여부에 따라 다음과 같이 나뉩니다.
  • JDK Dynamic Proxy : Target 클래스가 인터페이스 구현체일 경우 생성되며, 구현 클래스가 아닌 인터페이스를 프록시 객체로 구현해서 코드에 끼워넣는 방식입니다.
  • CGLib Proxy : 스프링에서 사용하는 디폴트 프록시 생성방식으로, Target 클래스를 프록시 객체로 생성하여 코드에 끼워넣는 방식입니다.

JDK 방식은 java.lang.Reflection 을 이용해서 동적으로 프록시를 생성해줍니다. 해당 방식의 단점은 AOP 적용을 위해 반드시 인터페이스를 구현해야된다는 점, 리플렉션은 private 접근이 가능하다는 점 때문에 스프링 부트에선 기본 방식으로 CGLib 방식을 채택하였습니다. (스프링 레거시는 JDK 기본 동작)

 

CGLib는 바이트 코드를 조작하여 프록시 객체를 생성합니다. 직접 원본 객체를 호출하지 않고 MethodInterceptor 와 같은 프록시와 원본 객체 사이에 인터셉터를 두어 메서드 호출을 조작할 수 있도록 도와줍니다.

 

 

 

출처 : https://sasca37.tistory.com/267

  • Target에 대한 호출이 오면, AOP 프록시가 인터셉터 체인을 통해 가로채온 후 Transaction Advisor에게 전달합니다.
  • Transaction Advisor는 트랜잭션을 생성합니다.
  • Custom Advisor가 있다면, 실행한 후 비즈니스 로직을 호출합니다.
  • Transaction Advisor는 커밋 또는 롤백 등의 트랜잭션 결과를 반환합니다.

 

 

 

3. @Transactional의 주요 옵션

@Transactional은 다양한 속성을 제공해 트랜잭션 동작을 세밀하게 제어할 수 있습니다. 자주 쓰이는 몇 가지를 소개합니다.

 

1) propagation (전파 방식)

  • 트랜잭션의 전파 방식을 설정합니다. 기본값은 Propagation.REQUIRED입니다.

    • REQUIRED: 기존 트랜잭션이 있으면 참여하고, 없으면 새로 생성.
    • REQUIRES_NEW: 항상 새 트랜잭션을 생성.
    • NESTED: 중첩 트랜잭션을 생성 (DB가 지원해야 함).
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUserWithNewTransaction() {
    // 새로운 트랜잭션 시작
}

 

2) isolation (격리 수준)

  • 트랜잭션 간 데이터 간섭을 얼마나 허용할지 설정합니다. 기본값은 Isolation.DEFAULT (DB 설정을 따름).
    • READ_UNCOMMITTED: 더티 리드 허용.
    • READ_COMMITTED: 커밋된 데이터만 읽음.
    • REPEATABLE_READ: 반복 읽기 보장.

3) rollbackOn

  • 어떤 예외에서 롤백할지 지정합니다. 기본적으로 RuntimeException과 Error에서만 롤백됩니다.
  • 읽기 작업이 많다면 readOnly = true를 적극 활용하세요. JPA에서는 더티 체킹(dirty checking)을 생략해 성능이 향상됩니다.
@Transactional(rollbackOn = Exception.class)
public void riskyOperation() throws Exception {
    // 모든 Exception에서 롤백
}

4) readOnly

  • 읽기 전용 트랜잭션으로 설정합니다. 쓰기 작업이 없으면 성능 최적화에 유리합니다.
@Transactional(readOnly = true)
public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);
}

 

 

4. Spring에서의 Transaction Management의 동작 방식

import org.springframework.transaction.annotation.Transactional;

public class UserService {

    @Transactional
    public Long registerUser(User user) {
        // 예: DB에 사용자를 삽입하고 자동 생성된 ID를 반환
        return userDao.save(user);
    }
}

 

• Spring Configuration에 @EnableTransactionManagement 어노테이션을 붙입니다. (스프링 부트에서는 자동으로 해줍니다.)
• Spring configuration에서 트랜잭션 매니저를 지정합니다.

 

이렇게 작성 하면 다음과 같이 동작합니다.

public class UserService {
    private DataSource dataSource;

    public UserService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Long registerUser(User user) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            // 중략
            connection.commit();
            return 1L; // 예시로 반환 값 설정
        } catch (SQLException e) {
            e.printStackTrace();
            if (connection != null) {
                    connection.rollback();
            }
            return null;
        } finally {
            if (connection != null) {           
                    connection.close();              
            }
        }
    }
}

 

 스프링이 실제로 내가 작성한 자바 코드를 추가로 재작성할 수는 없습니다. (바이트 코드 위빙 같은 고급 기술은 여기서 무시합시다). 위에서 보여준 registerUser() 메소드를 호출하면 userDao.save(user)을 호출하는 것은 바뀌지 않는 사실입니다.

대신에 스프링은 IoC 컨테이너라는 장점을 활용합니다. 우리가 @Transactional을 사용하면 UserService를 초기화할 뿐만 아니라, UserServiceProxy 또한 초기화하는 것입니다. CGlib 라이브러리의 도움을 받아 만든 프록시를 사용하면 마치 실제 UserService 코드에 위에서 보여준 트랜잭션 코드를 추가하여 사용하는 것처럼 동작하게 됩니다.

위 다이어그램을 보면 Proxy는 한가지 일을 합니다. 우선 database의 connections/transactions 을 열거나 닫고, 실제 UserService에게 나머지 역할을 위임시키게 됩니다. 이런 과정을 좀더 구체적으로 보자면 다음과 같습니다.

  1. 만약 스프링이 어떤 Bean에 붙어 있는 @Transactional을 발견한다면, 해당 Bean의 동적 프록시를 만들게 됩니다.
  2. 프록시는 트랜잭션 매니저에 접근할 수 있으며, 트랜잭션이나 연결을 열고 닫도록 요청합니다.
  3. 트랜잭션 매니저는 JDBC 방식으로 연결을 관리할 뿐입니다.

[결론]

@Transactional은 Spring 개발에서 트랜잭션 관리를 단순화해주는 강력한 도구입니다. @Transactional을 활용하면 복잡한 트랜잭션 관리 로직을 간단하게 처리할 수 있습니다. 데이터베이스의 일관성을 유지하며, 예외 상황에서도 안정적인 처리가 가능합니다. 이를 통해 개발자는 비즈니스 로직에 집중할 수 있으며, 유지 보수성과 코드의 가독성을 크게 향상시킬 수 있습니다. 현대적인 애플리케이션에서 트랜잭션 관리는 필수적인 요소이므로, @Transactional의 원리를 이해하고 활용하는 것은 개발자에게 매우 중요한 역량이 될 것입니다.


[출처 및 참조]