개발자 노트

간소화한 Transaction 본문

Web

간소화한 Transaction

jurogrammer 2022. 7. 15. 23:27

예제 코드

https://github.com/jurogrammer/dtrans

 

상황

트랜잭션을 분리해서 작업하고 싶은데, 이 때문에 서비스 클래스를 생성하기엔 번잡할 때

코드

@Service
@RequiredArgsConstructor
public class SaveServiceVer1 {

    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    @Transactional
    public void save() {

        userRepository.save(new User("홍길동"));

        // 만약 order 저장시 에러가 발생 경우에도, user는 그대로 저장하고 싶다면?
        saveOrder();
    }

    public void saveOrder() {
        orderRepository.save(new Order());
        throw new RuntimeException("order 저장시 에러 발생");
    }
}

 

아이디어

saveOrder에서 에러가 발생하여 order는 롤백이 되더라도, User저장은 그대로 수행하도록 해야 합니다.

따라서 order를 저장하는 별도의 트랜잭션을 생성합니다.

잘못된 방법

별도의 트랜잭션을 생성할 의도로 동일 클래스 내 메서드에 RequiredNew 옵션을 선언한 후, 해당 메서드를 호출합니다.

@Service
@RequiredArgsConstructor
public class BadSaveService {

    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    @Transactional
    public void save() {
        userRepository.save(new User("홍길동"));

        try {
            saveOrder();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder() {
        orderRepository.save(new Order());
        throw new RuntimeException("order 저장시 에러 발생");
    }
}

하지만, 다음 테스트 코드에서 보시다시피 User 뿐만 아니라 에러가 발생한 Order를 저장하는 트랜잭션 또한 롤백없이 모두 정상적으로 저장됩니다.

@SpringBootTest
class BadSaveServiceTest {
    @Autowired
    BadSaveService badSaveService;

    @Autowired
    UserRepository userRepository;
    @Autowired
    OrderRepository orderRepository;

    // repository interface 의 기본 전략은 save시 트랜잭션 열고 commit
    @Test
    @DisplayName("user 1개 order 1개를 저장하면, 1개, 1개가 조회된다.")
    void test() {
        try {
            badSaveService.save();
        } catch (Exception e) {
            e.printStackTrace();
        }

        List<User> users = userRepository.findAll();
        List<Order> orders = orderRepository.findAll();
        int userSize = users.size();
        int orderSize = orders.size();

        assertThat(userSize).isEqualTo(1);
        assertThat(orderSize).isEqualTo(1);

        removeAll();
    }

    private void removeAll() {
        userRepository.deleteAll();
        orderRepository.deleteAll();
    }
}

원인

@Transactional 어노테이션을 선언할 경우, 스프링에선 CGLIB를 이용한 AOP를 통해 Transaction 처리를 실행해주는 로직이 있는 SubClass를 생성합니다. 그리고 스프링은 sub class의 인스턴스를 @Autowired 선언된 필드에 주입을 해주죠. 

하지만, 위와 같이 super class의 메서드를 호출할 경우 transaction 처리 로직이 없기 때문에 repository의 save의 기본 트랜잭션 처리를 따릅니다. 즉, save전 새로운 트랜잭션 생성 그리고 save 후 commit 입니다.

따라서, order 저장 후 익셉션이 발생했더라도 order를 저장하는 repository의 트랜잭션은 커밋되었기 때문에 롤백은 발생하지 않습니다.

콜스택에서 보시다시피, save 메서드는 cglib로 생성된 Subclass의 save 메서드가 실행된 다음에, TransactionInterceptor같은 트랜잭션 처리 로직이 수행되고 나서야 super class인 BadSaveService(위에서 2번째)가 실행되는 모습입니다.

saveOrder이라는 메서드는 인터셉터의 실행없이 곧바로 super class인 BadSaveService의 method가 실행됩니다.

방법 1 새로운 서비스 선언

새로운 서비스를 선언해서 주입받는 방식으로 이를 해결할 수 있습니다. 다음처럼요

@Service
@RequiredArgsConstructor
public class SaveServiceVer2 {

    private final UserRepository userRepository;
    private final OrderService orderService;

    @Transactional
    public void save() {
        userRepository.save(new User("홍길동"));

        try {
            orderService.save(new Order());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(Order order) {
        orderRepository.save(order);
        throw new RuntimeException("order 저장시 에러 발생");
    }
}

하지만, 새로운 서비스까지 선언하여 주입을 받아야 하나 싶습니다. 

거추장스럽습니다.

루비 온 레일즈 코드와 비교하면요.

착안

루비 온 레일즈의 트랜잭션

Account.transaction do
  balance.save!
  account.save!
end

얼마나 간결하나요?

1. Account 테이블에 대하여 트랜잭션을 생성하겠다.
2. balance와 account를 저장하는 operation을 수행하겠다.

새로운 클래스를 생성하지 않고도 충분히 가능합니다. 루비에서 위와 같은 문법을 블록문이라고 하는데, 사실 callback function을 선언해주는 것과 다름이 없습니다.

transaction을 새로 생성해주는 메서드에 User와 Order를 저장하는 callback function을 전달합니다. 그리고 해당 메서드 내에서 callback function을 lazy evaluation해주면 됩니다.


따라서 다음과 같이 코드를 작성해볼 수 있습니다.

방법2

import jurogrammer.dtrans.entity.Order;
import jurogrammer.dtrans.entity.User;
import jurogrammer.dtrans.repository.OrderRepository;
import jurogrammer.dtrans.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class SaveServiceVer3 {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final TransactionHelper transactionHelper;

    @Transactional
    public void save() {
        userRepository.save(new User("홍길동"));
        try {
            transactionHelper.requiredNew(() -> {
                orderRepository.save(new Order());
                throw new RuntimeException("order 저장시 에러 발생");
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class TransactionHelper {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requiredNew(TransactionTask transactionTask) {
        transactionTask.apply();
    }
}
@FunctionalInterface
public interface TransactionTask {

    void apply();
}

1. functionalInterface를 선언하여 function을 받을 수 있도록 합니다.
2. TransactionHelper 클래스를 선언하여 새로 생성된 Transaction 맥락에서 function을 call 합니다.
3. save 로직에선 lambda expression을 이용하여 function을 주입해줍니다.

 

아니! 결국 TransactionHelper라는 클래스를 생성했네! 라고 말하실 수 있습니다. 하지만

방법1의 OrderService의 경우엔 concrete한 Order에 관련된 로직을 사용하기 때문에 재사용 가능성이 떨어지지만,
방법2의 경우는 OrderService 로직 뿐만이 아니라 임의의 로직에 대하여 새로운 트랜잭션 맥락에서 로직을 수행 할 수 있습니다. 즉, 새로운 트랜잭션을 생성하는 목적 때문에 앞으로 클래스를 새로 정의하지 않아도 된다는 것이죠.

 

 

아니 근데... 이미 있네?

아... 근데 좀 찾아보니 이미 비슷한 녀석이 스프링에 있었습니다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionTemplate.html

TransactionTemplate라는 녀석이 있네요.

하하!

 

반응형
Comments