이 글에서는 내가 만든 TransactionManager를 적용하며 계층간 주고 받는 Connection을 제거할 것이다.
다음 코드에 TransactionManager를 적용할 것이다.
public class FoodOrderService {
...
public void order(long userId, FoodOrderRequests foodOrderRequests) {
Users user = usersRepository.findById(userId);
List<Foods> foods = foodsRepository.findAll(foodOrderRequests.foodIds());
List<FoodOrders> foodOrders = FoodOrders.from(foodOrderRequests, foods);
try(Connection conn = myDataSource.getConnection()) {
try {
conn.setAutoCommit(false);
Orders order = orderService.order(user, foodOrders);
orderRepository.save(conn, order);
usersRepository.updateBalance(conn, user.getId(), user.getBalance());
for (Foods food : foods)
foodsRepository.updateStock(conn, food.getId(), food.getStock());
conn.commit();
} catch (SQLException | RuntimeException e) {
conn.rollback();
throw e;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
먼저 테스트 코드를 추가해본다.
class FoodOrderServiceTest {
DBConfig mysqlConfig = new MySQLConfig();
MyDataSource myTxDataSource = new MyTransactionAwareDataSourceProxy(new DriverManagerDataSource(mysqlConfig));
OrderService orderService = new OrderService();
MyJdbcTemplate myJdbcTemplate = new MyJdbcTemplate();
OrderRepository orderRepository = new OrderRepository(myTxDataSource, myJdbcTemplate);
UsersRepository usersRepository = new UsersRepository(myTxDataSource, myJdbcTemplate);
FoodsRepository foodsRepository = new MockFoodRepository(myTxDataSource, myJdbcTemplate); // 예외 발생 객체
FoodOrderService foodOrderService = new FoodOrderService(
myTxDataSource, orderService,
orderRepository, foodsRepository, usersRepository,
new MyTransactionManager(myTxDataSource)
);
// 롤백 테스트를 위한 예외를 발생시키는 클래스
static class MockFoodRepository extends FoodsRepository {
public MockFoodRepository(MyDataSource myDataSource, MyJdbcTemplate myJdbcTemplate) {
super(myDataSource, myJdbcTemplate);
}
@Override
public void updateStock(long userId, int balance) {
throw new MockException("트랜잭션 테스트를 위한 예외");
}
static class MockException extends RuntimeException {
public MockException(String message) {
super(message);
}
}
}
@Test
@DisplayName("주문 트랜잭션 롤백 테스트")
void test() {
// given
Users users = new Users("김아무개", 5000);
usersRepository.save(users);
Foods foods1 = new Foods("떡볶이", 100, 10);
Foods foods2 = new Foods("짬뽕", 200, 5);
foodsRepository.save(foods1);
foodsRepository.save(foods2);
// when - then
assertThrows(MockFoodRepository.MockException.class,
() -> foodOrderService.order(
users.getId(),
new FoodOrderRequests(
List.of(
new FoodOrderRequest(foods1.getId(), 5),
new FoodOrderRequest(foods2.getId(), 3)
)
)
)
);
// then - 롤백 체크
assertEquals(5000, usersRepository.findById(users.getId()).getBalance());
assertEquals(10, foodsRepository.findById(foods1.getId()).getStock());
assertEquals(5, foodsRepository.findById(foods2.getId()).getStock());
}
}
테스트 코드를 작성할 때 주입을 직접하는게 참 쉽지않다.
Spring DI 주입이 없음 + Mockito 사용 X 환경이기 때문에 예외를 발생시키는 클래스를 추가하고 주입했다.
그리고 MyDataSource도 프록시로 감싸서 주입했다.
어느정도 타협을 해야할 때인 것 같다.
만약 SpringBoot와 Mockito를 쓴다면 insert 과정 중간에 예외가 터지도록 만들기 쉬웠을텐데 지금 환경에선 어렵다.
Spring 빈 관리가 없다보니 통합 테스트에서 힘든 부분이 한 둘이 아니다.
Spring 컨테이너 정도까지는 들어오는게 맞는 것 같다.
일단 트랜잭션 작업을 완성하고 다음 디펜던시를 추가해서 바꾸겠다./* Spring IoC 컨테이너 + Mockito 테스트를 위한 의존성 추가 */ // Spring IoC 컨테이너 implementation 'org.springframework:spring-context:6.1.5' // Spring Test (ApplicationContext 로딩, @ExtendWith 등) testImplementation 'org.springframework:spring-test:6.1.5' // Mockito testImplementation 'org.mockito:mockito-core:5.11.0' /* ****************************************************** */
public class FoodOrderService {
private final MyTransactionManager myTransactionManager;
...
public void order(long userId, FoodOrderRequests foodOrderRequests) {
Users user = usersRepository.findById(userId);
List<Foods> foods = foodsRepository.findAll(foodOrderRequests.foodIds());
List<FoodOrders> foodOrders = FoodOrders.from(foodOrderRequests, foods);
myTransactionManager.startTransaction();
try {
Orders order = orderService.order(user, foodOrders);
orderRepository.save(order);
usersRepository.updateBalance(user.getId(), user.getBalance());
for (Foods food : foods)
foodsRepository.updateStock(food.getId(), food.getStock());
myTransactionManager.commit();
} catch(RuntimeException e) {
myTransactionManager.rollback();
throw e;
}
}
}
이제 애플리케이션에선 Connection의 존재를 몰라도 잘 동작한다.
MyDataSource도 이젠 필요없어 졌다.
뭐 MyDataSource가 MyTansactionManager로 바뀐거긴한데...
이제 모든 Repository에서 커넥션을 파라미터로 받는 메소드들을 제거해주면 된다.
그리고 MyJdbcTemplate에서도 커넥션을 직접 받지 않고, DataSource에서 커넥션을 꺼내게 만들면 된다.