애플리케이션에서 DB 종속적인 트랜잭션 Connection 코드 제거

Jang990·2026년 2월 3일

기존 코드

이 글에서는 내가 만든 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'
	/* ****************************************************** */

TransactionManger 적용하기

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에서 커넥션을 꺼내게 만들면 된다.

profile
개발 기록 아카이브

0개의 댓글