본문 바로가기
스프링

[스프링] 프로그래밍 방식의 트랜잭션 관리 방법

by kdohyeon (김대니) 2023. 2. 3.
반응형

[스프링] @Transactional 에서는 트랜잭션과 스프링에서의 @Transactional 어노테이션에 대해 알아보았는데, 프로그래밍 방식의 트랜잭션도 함께 소개를 했었다. 이번에는 프로그래밍 방식의 트랜잭션에 집중해서 정리를 해보고자 한다.

Programmatic Transaction

트랜잭션을 관리하기 위해 흔히 사용하는 @Transactional 어노테이션을 활용하지 않고, 코드 내에서 개발자가 직접 트랜잭션 범위를 설정하며 관리하는 방식을 의미한다. 의견이 다양하겠지만 개인적으로는 개발자에게 트랜잭션 범위를 직접 설정할 수 있도록 하여 자율성을 추구하고 @Transactional 어노테이션으로는 구현하기 어려운 (예: 동일 클래스 내 여러 메소드 간 호출에서 트랜잭션을 각각 적용하고 싶은 경우) 경우도 편리하게 구현할 수 있다. 

여러 장단점이 있지만 트랜잭션 범위를 작은 단위로 관리하거나 @Transactional 으로 구현하기 어려울 때, 사용하면 좋다고 생각한다.

스프링 공식 문서에 따르면 스프링에서 프로그래밍 방식의 트랜잭션 관리는 두 가지 방법이 존재하지만 스프링 팀은 첫 번째 방법을 사용하길 권장한다.

  1. TransactionTemplate 을 활용
  2. PlatformTransactionManager 의 구현체를 직접 활용

스프링에서 트랜잭션 관리를 활성화하기

트랜잭션 관리를 활성화하기 위해서는 spring-tx 또는 spring-data 관련 의존성을 build.gradle 에 추가하면 자동으로 트랜잭션 관리가 활성화된다.

implementation("org.springframework:spring-tx")
// 또는 
implementation("org.springframework.data:spring-data-{...}")

의존성 추가 없이 진행하려면 @EnableTransactionManagement 어노테이션을 설정 (@Configuration) 클래스에 적용해야 한다.

@EnableTransactionManagement // 추가
@SpringBootApplication
public class ReviewApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReviewApiApplication.class, args);
    }
}

 

설정하기

별도의 설정 클래스를 만들고 (e.g., JpaConfig.java) TransactionTemplate 을 활용하여 읽기 전용과 쓰기 전용 TransactionTemplate Bean 을 설정한다.

@Configuration
public class JpaConfig {
    @Bean
    public TransactionTemplate readTransactionOperations(PlatformTransactionManager transactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setReadOnly(true);
        return transactionTemplate;
    }

    @Bean
    public TransactionTemplate writeTransactionOperations(PlatformTransactionManager transactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setReadOnly(false);
        return transactionTemplate;
    }
}

사용 방법

조회를 해야 할 땐 readTransactionOperations 빈을 주입받아 활용하고 생성, 삭제, 수정 등 에서는 writeTransactionOperations 를 주입받으면 된다.

transactionOperations.execute(...)

사용자를 조회하는 트랜잭션 메소드를 만들어보자. 

@Service
public class UserSearchService {
    private final UserPort userPort;
    private final UserDtoConverter userDtoConverter;
    private final TransactionOperations readTransactionOperations;
    
    public UserSearchService(UserPort userPort, 
    				UserDtoConverter userDtoConverter,
    				TransactionOperations readTransactionOperations) {
        this.userPort = userPort;
        this.userDtoConverter = userDtoConverter;
        this.readTransactionOperations = readTransactionOperations;
    }
    
    public UserDto findUserDtoByUserId(Long userId) {
        var user = readTransactionOperations.execute(status -> {
            return userPort.findByUserId(userId);
        });
        
        return userDtoConverter.convert(user);
    }
}

readTransactionOperations 빈을 주입받아서 .execute(...) 를 통해 트랜잭션 범위를 설정하고 그 결과값을 user 로 반환한다.

transactionOperations.executeWithoutResult(...)

만약 데이터 수정만 하고 그 결과값은 필요없을 땐 어떻게 하면 될까? 똑같이 .execute(...) 를 활용해도 되지만 결과값이 필요없는 경우에 대해서는 .executeWithoutResult(...) 를 활용하면 된다.

public void modify(Long userId, ModifyUserCommand command) {
    writeTransactionOperations.executeWithoutResult(status -> {
        var user = userPort.findByUserId(userId);
        user.modifyUserName(command.getUserName());
    });
}
반응형

댓글