Back-end/TroubleShooting

[Spring Boot] ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ…Œ์ŠคํŠธ์—์„œ @Transactional ์‚ฌ์šฉ ์‹œ ์‹คํŒจํ•˜๋Š” ์ด์œ 

์„œ์ฑ„๋ฆฌ 2024. 6. 19. 22:42

๐ŸŒฑ ๋ฌธ์ œ ์ƒํ™ฉ

๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ…Œ์ŠคํŠธ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•ด @Transactional์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ @BeforeEach๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐํ™”ํ•œ ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์•ˆ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค.

@Transactional
@SpringBootTest
class OrderUseCaseTest {

    @Autowired
    private OrderUseCase orderUseCase;

    // ...

    @BeforeEach
    void initData() {
        userService.create(user);
        productService.create(product);
        bidService.create(bid);
    }

    @Test
    @DisplayName("ํŒ๋งค ์ž…์ฐฐ์— ๋Œ€ํ•œ ๊ตฌ๋งค ์ž…์ฐฐ๊ณผ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•œ๋‹ค")
    void succeed_to_create_order() throws InterruptedException {
        int threadCount = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger failCount = new AtomicInteger();

        OrderRequestDto request = new OrderRequestDto(product.getId(), bid.getPrice());

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                try {
                    orderUseCase.order(user, request);
                    successCount.getAndIncrement();
                } catch (NotFoundException e) {
                    failCount.getAndIncrement();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        executorService.shutdown();

        assertAll(
            () -> assertThat(successCount.get()).isEqualTo(1),
            () -> assertThat(failCount.get()).isEqualTo(9)
        );
    }

    // ๊ฐ์ฒด ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’ ์ƒ๋žต
    User user = User.builder().build();
    Product product = Product.builder().build();
    Bid bid = Bid.builder().build();
}

์‹ค์ œ๋กœ order ๋ฉ”์„œ๋“œ์— findAll()์„ ํ†ตํ•ด ์กฐํšŒํ•œ ์ „์ฒด Bid ๋ฆฌ์ŠคํŠธ ์‚ฌ์ด์ฆˆ๋ฅผ ๋กœ๊ทธ๋กœ ์ถœ๋ ฅํ•  ๊ฒฝ์šฐ 0์ด ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

@Transactional ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ญ์ œํ•  ๊ฒฝ์šฐ ์ •์ƒ์ ์œผ๋กœ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง€๋Š”๋ฐ ์ด๋Ÿฐ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ?

 

๐ŸŒฑ ๋ฌธ์ œ ๋ฐœ์ƒ ์ด์œ  - Transaction๊ณผ EntityManager

EntityManager๋Š” ์Šค๋ ˆ๋“œ๋งˆ๋‹ค, ํŠธ๋žœ์žญ์…˜์ด ์‹œ์ž‘๋  ๋•Œ ์ƒ์„ฑ๋˜๊ณ  ํŠธ๋žœ์žญ์…˜์ด ๋๋‚  ๋•Œ ์†Œ๋ฉธ๋œ๋‹ค. ๋”ฐ๋ผ์„œ EntityManager๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋„ ์Šค๋ ˆ๋“œ๋งˆ๋‹ค ๋‹ค๋ฅด๋ฉฐ ์ด๋Š” 1์ฐจ ์บ์‹œ๋„ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

 

[JPA] ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ๋™์ž‘์›๋ฆฌ์™€ ์ด์ 

๐Ÿคญ ๋ฏธ๋ฆฌ ์ฝ๊ณ  ์˜ค๋ฉด ์ข‹์€ ๊ธ€ [JPA] JPA, Hibernate, Spring Data JPA์— ๋Œ€ํ•œ ์ด๋Ÿฐ์ €๋Ÿฐ.. ์ •๋ฆฌโ˜๏ธ JPA ์ธํ„ฐํŽ˜์ด์Šค ๐Ÿซง EntityManagerFactory JPA ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ EntityManager ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ ์—ญํ•  (์—ฌ๋Ÿฌ E

chaewsscode.tistory.com

 

์œ„์˜ succeed_to_create_order() ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•˜๊ณ , orderUseCase.order() ๋ฉ”์„œ๋“œ๋Š” ExecutorService๊ฐ€ ์ƒ์„ฑํ•œ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— main ์Šค๋ ˆ๋“œ์˜ EntityManager์™€ executorService๊ฐ€ ์‹คํ–‰ํ•˜๋Š” ์Šค๋ ˆ๋“œ์˜ EntityManager๋Š” ์„œ๋กœ ๋‹ค๋ฅธ 1์ฐจ ์บ์‹œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ executorService์—์„œ๋Š” main ์Šค๋ ˆ๋“œ์—์„œ ์ƒ์„ฑํ–ˆ๋˜ DB ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์—†๋‹ค.

 

ํ•˜์ง€๋งŒ @Transactional ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ญ์ œํ•  ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์˜ ์ดˆ๊ธฐํ™”๊ฐ€ ๋˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ฒŒ ๋œ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‚ด๊ฐ€ ์ฐพ์€ ํ•ด๊ฒฐ๋ฐฉ์•ˆ์€ ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

 

๐ŸŒฑ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

1. @SQL ์‚ฌ์šฉ

@Sql ์–ด๋…ธํ…Œ์ด์…˜๊ณผ SqlConfig ์„ค์ •์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ ์ „ ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ SQL ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ DB ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•ด ์ฃผ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

@Test
@Sql(scripts = "/order-test-data.sql",
    config = @SqlConfig(transactionMode = TransactionMode.ISOLATED))
void succeed_to_create_order() throws InterruptedException {
    ...
}

@Sql์„ ํ†ตํ•ด ๋‚ ์•„๊ฐ„ ์ฟผ๋ฆฌ๋Š” ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์‹คํ–‰๋˜๊ณ  ๋ฐ”๋กœ ์ปค๋ฐ‹๋˜๋ฏ€๋กœ, ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ๋‹ค๋ฅธ EntityManager๋กœ ์‹คํ–‰๋˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฐ’์ด ์กด์žฌํ•ด ์•ž์„  ๋ฌธ์ œ์—†์ด ํ…Œ์ŠคํŠธ ์ง„ํ–‰์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ user, bid ๋“ฑ์˜ ํ•„๋“œ๋“ค์˜ ๊ฐ’์„ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•ด findByยทยทยท ๋ฉ”์„œ๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์žฌ์‚ฌ์šฉ์„ฑ์ด ์ €ํ•˜๋˜๊ณ  ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

 

๋˜ํ•œ jojoldu๋‹˜์˜ ๋ธ”๋กœ๊ทธ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”์— @Transactional ์‚ฌ์šฉ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ์ ์— ๋Œ€ํ•œ ๊ธ€์„ ์ฝ๊ณ  ์ƒ๊ฐํ•ด ๋ณธ ํ›„, @Transactional ๋ณด๋‹ค๋Š” @AfterEach๋ฅผ ํ†ตํ•ด ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฒฐ์ •ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”์— @Transactional ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ์ƒ๊ฐ

์–ผ๋งˆ ์ „์— 2๊ฐœ์˜ ํ•ซํ•œ ์ปจํ…์ธ ๊ฐ€ ๊ณต์œ ๋˜์—ˆ๋‹ค. ์กด๊ฒฝํ•˜๋Š” ์žฌ๋ฏผ๋‹˜์˜ ์œ ํŠœ๋ธŒ - ํ…Œ์ŠคํŠธ์—์„œ @Transactional ์„ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ? ์กด๊ฒฝํ•˜๋Š” ํ† ๋น„๋‹˜์˜ ํŽ˜์ด์Šค๋ถ 2๊ฐœ์˜ ์ปจํ…์ธ ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”์— @Transa

jojoldu.tistory.com

 

2. JUnit5 Extension ์‚ฌ์šฉ

JUnit5 Extention์ด๋ž€ JUnit5์—์„œ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ฐ๊ฐ์˜ ํ™˜๊ฒฝ์— ๋Œ€ํ•ด ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค.

  • BeforeAllCallback: @BeforeAll ์‹คํ–‰ ์ „์— ์‹คํ–‰๋œ๋‹ค. (๊ฐ€์žฅ ๋จผ์ € ์‹คํ–‰)
  • BeforeEachCallback: @BeforeEach ์‹คํ–‰ ์ „์— ์‹คํ–‰๋œ๋‹ค.
  • BeforeTestExecutionCallback: ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ง์ „์— ์‹คํ–‰๋œ๋‹ค. (@BeforeEach ํ›„์— ์‹คํ–‰)
  • AfterTestExecutionCallback: ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์ข…๋ฃŒ๋œ ํ›„ ์‹คํ–‰๋œ๋‹ค. (@AfterEach ์ „์— ์‹คํ–‰)
  • AfterEachCallback : @AfterEach ์‹คํ–‰ ์ดํ›„์— ์‹คํ–‰๋œ๋‹ค.
  • AfterAllCallback : @AfterAll ์‹คํ–‰ ์ดํ›„์— ์‹คํ–‰๋œ๋‹ค. (๊ฐ€์žฅ ๋‚˜์ค‘์— ์‹คํ–‰)

 

 

ํ…Œ์ŠคํŠธ ํŒจํ‚ค์ง€์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ชจ๋“  ํ…Œ์ด๋ธ”์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•˜์—ฌ Bean์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

@Component
public class DatabaseCleaner {

    private final List<String> tableNames = new ArrayList<>();

    @PersistenceContext
    private EntityManager entityManager;

    @PostConstruct
    @SuppressWarnings("unchecked")
    private void findDatabaseTableNames() {
        List<Object[]> tableInfos = entityManager.createNativeQuery("SHOW TABLES").getResultList();
        for (Object[] tableInfo : tableInfos) {
            String tableName = (String) tableInfo[0];
            tableNames.add(tableName);
        }
    }

    private void truncate() {
        entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS %d", 0)).executeUpdate();
        for (String tableName : tableNames) {
            entityManager.createNativeQuery(String.format("TRUNCATE TABLE %s", tableName)).executeUpdate();
        }
        entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS %d", 1)).executeUpdate();
    }

    @Transactional
    public void clear() {
        entityManager.clear();
        truncate();
    }
}

์ด ํด๋ž˜์Šค์—์„œ๋Š” ๋นˆ์ด ์ƒ์„ฑ๋œ ํ›„ findDatabaseTableNames ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ชจ๋“  ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ tableNames ๋ฆฌ์ŠคํŠธ์— ์ €์žฅํ•œ๋‹ค.

์ดํ›„ DatabaseCleaner์˜ clear() ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด, Entity Manager๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ , truncate ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ํ…Œ์ด๋ธ”๋“ค์„ truncate ํ•œ๋‹ค. (์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์„ ์ผ์‹œ์ ์œผ๋กœ ํ•ด์ œํ•ด ๋ชจ๋“  ํ…Œ์ด๋ธ”์„ truncate ํ•œ ํ›„ ๋‹ค์‹œ ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์„ ํ™œ์„ฑํ™”ํ•จ)

 

 

๊ทธ๋ฆฌ๊ณ  JUnit5 Extension์„ ์‚ฌ์šฉํ•ด ๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋œ ํ›„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ •๋ฆฌํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•œ๋‹ค.

public class DatabaseClearExtension implements AfterEachCallback {

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        DatabaseCleaner databaseCleaner = getDataCleaner(context);
        databaseCleaner.clear();
    }

    private DatabaseCleaner getDataCleaner(ExtensionContext extensionContext) {
        return SpringExtension.getApplicationContext(extensionContext)
            .getBean(DatabaseCleaner.class);
    }
}

 

ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— @ExtensionWith(ยทยทยท) ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

@ExtendWith(DatabaseClearExtension.class)
@SpringBootTest
class OrderUseCaseTest {

    @Autowired
    private OrderUseCase orderUseCase;

    // ...

    @BeforeEach
    void initData() {
        userService.create(user);
        productService.create(product);
        bidService.create(bid);
    }

    @Test
    @DisplayName("ํŒ๋งค ์ž…์ฐฐ์— ๋Œ€ํ•œ ๊ตฌ๋งค ์ž…์ฐฐ๊ณผ ์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•œ๋‹ค")
    void succeed_to_create_order() throws InterruptedException {
        ...
    }

    User user;
    Product product;
    Bid bid;
}

 

 

 

๐Ÿ“š ์ฐธ๊ณ 

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”์— @Transactional ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ์ƒ๊ฐ

[Spring] AOP์™€ @Transactional์˜ ๋™์ž‘ ์›๋ฆฌ

[JPA] @Transactional ๊ณผ ๋™์‹œ์„ฑ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ - ๋ฉ€ํ‹ฐ์“ฐ๋ ˆ๋“œ ํ™˜๊ฒฝ์˜ ํŠธ๋žœ์žญ์…˜

์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ์—์„œ @Transactional์„ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ์ด์œ 

ํ…Œ์ŠคํŠธ๋ณ„๋กœ DB ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ