본문 바로가기
우아한테크코스 4기/레벨2

[Spring] Spring Boot에서 트랜잭션 사용하기

by 나는후니 2022. 4. 20.

일반적으로 하나의 비즈니스는 한 트랜잭션 내부에서 다룹니다.
비즈니스로직이 실행되는 도중 어떠한 이유로 메서드 실행이 중단된다면 지금껏 처리한 모든 쿼리가 롤백되어야 합니다.

 

체스 미션을 예시로 간단히 설명해보겠습니다.

a2 포지션의 폰을 a4 포지션으로 옮기려면 a2 포지션은 blank로 a4 포지션은 폰으로 변경해야 합니다.

만약 위 비즈니스 로직이 다른 트랜잭션에서 작동하다 a2 포지션을 blank로 바꾼 후 에러가 발생하면 어떻게 될까요?
그렇다면 a2, a4 모두 blank를 갖게되고 폰은 어디론가 증발하게 될 것입니다.

이러한 문제를 해결하기 위해 주로 비즈니스 로직이 존재하는 Service 레이어에서 트랜잭션을 관리하게 됩니다.


트랜잭션을 완벽하게 이해하기 위해서는 dataSource가 connection을 어떻게 관리하는지, Spring Boot가 @Transactional 애노테이션을 어떻게 aop로 사용하는지 왜 그렇게 됐는지 등을 이해해야 하지만,

 

우선 모두 제쳐두고 한 번 기능만 구현해보겠습니다.

 

간단하게 Service와 Repository를 만들어 Spring Container에 올려둡니다.

예제는 트랜잭션을 가장 이해하기 쉬운 계좌이체로 설정하였습니다.

@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        memberRepository.update(toId, toMember.getMoney() + money);
    }
}
@RequiredArgsConstructor
@Repository
public class MemberRepository {

    private final DataSource dataSource;

    //.. 생략
}

먼저 가정해야 할 사항이 있습니다.

 

1. Spring이 제공하는 JdbcTemplate을 사용한다. (Connection을 TransactionSynchronizationManager로부터 가져오고 release 함)
2. 순수 Jdbc api를 사용한다면, DataSourceUtil에서 getConnection, releaseConnection을 사용한다. (TransactionSynchronizationManager로부터 커넥션 가져오기 위해)

위 내용을 유념하여 Repository를 생성합니다.
이제 DataSourceTransactionManager 를 Bean으로 생성하여 주입해보겠습니다.

@Configuration
public class TransactionConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource());
        return tm;
    }
}

트랜잭션 매니저 참고

spring:
  datasource:
    driver-class-name: org.h2.Driver
    jdbc-url: jdbc:h2:tcp://localhost/~/test
    username: sa
    password:

Spring Boot는 설정 property로 작성된 내용을 가져와 Bean으로 등록할 수 있다는 장점이 있습니다. (자바 코드에 직접 입력보다 보안에 좋습니다. 깃헙에 공개하지 않을 수 있음)
따라서 yml파일을 이용하여 DataSource를 하나 생성하고 이를 기반으로 TransactionManager를 만들어 @Bean으로 등록합니다.

 

이제 아래와 같이 테이블을 하나 생성해두고

간단한 테스트 코드를 하나 작성해보겠습니다.

@SpringBootTest
@ExtendWith(SpringExtension.class)
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Test
    @Transactional
    void accountTransfer() throws SQLException {
        memberService.accountTransfer("memberB", "memberA", 2000);
        assertThat(memberService.findById("memberB").getMoney()).isEqualTo(10000);
        assertThat(memberService.findById("memberA").getMoney()).isEqualTo(10000);
    }
}

테스트 코드가 잘 통과하는 것을 확인할 수 있습니다.

그리고 하나의 업데이트가 진행된 뒤 강제로 예외를 발생시켜 보겠습니다.

@Test
void accountTransferException() throws SQLException {
     assertThatThrownBy(() -> memberService.accountTransfer("memberA", "ex", 2000))
             .isInstanceOf(IllegalStateException.class);
     assertThat(memberService.findById("memberA").getMoney()).isEqualTo(8000);
}

트랜잭션이 제대로 적용되지 않았다면 memberA의 돈은 2000원 줄어야하지만 트랜잭션이 잘 적용되어있기 때문에 실행하고도 잘 rollback되는 것을 확인할 수 있습니다.

정리

Spring framework에서 지원하는 @Transactional 애노테이션은 내부적으로 엄청난 것을 숨기고 있는 애노테이션입니다. 간단한 설정만으로 이모든 걸 사용하게 해주는 갓프링에게 큰 감사를 보냅니다~!

 

+ 스프링 부트는 yml 설정만 있으면 트랜잭션과 데이터 소스를 따로 빈으로 등록하지 않아도 사용이 가능하다~~