๐ฑ ๋ฌธ์ ์ํฉ
๋ฉํฐ์ค๋ ๋ ํ ์คํธ์์ ํ ์คํธ ๋ฐ์ดํฐ ์ด๊ธฐํ๋ฅผ ์ํด @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์ฐจ ์บ์๋ ๋ค๋ฅด๋ค๋ ๊ฒ์ ์๋ฏธํ๋ค.
์์ 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๋ฅผ ํตํด ๋ช ์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ด๊ธฐํํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๊ฒฐ์ ํ๊ฒ ๋์๋ค.
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 ๊ณผ ๋์์ฑ
ํ ์คํธ ์ฝ๋ - ๋ฉํฐ์ฐ๋ ๋ ํ๊ฒฝ์ ํธ๋์ญ์
ํ ์คํธ๋ณ๋ก DB ์ด๊ธฐํํ๊ธฐ