애플리케이션에서 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도 이젠 필요없어 졌다.

MyDataSourceMyTansactionManager로 바뀐거긴한데...

이제 모든 Repository에서 커넥션을 파라미터로 받는 메소드들을 제거해주면 된다.
그리고 MyJdbcTemplate에서도 커넥션을 직접 받지 않고, DataSource에서 커넥션을 꺼내게 만들면 된다.

profile
개발 기록 아카이브

0개의 댓글