Test Isolation


μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κ°œλ°œμ— μžˆμ–΄μ„œ ν…ŒμŠ€νŠΈμ½”λ“œλŠ” 개발 생산성과 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 신뒰도 ν–₯상에 큰 도움을 μ€λ‹ˆλ‹€. 이런 ν…ŒμŠ€νŠΈμ½”λ“œλŠ” μˆœμ„œμ— 상관없이 λ…λ¦½μ μœΌλ‘œ μˆ˜ν–‰λ˜μ–΄μ•Ό λΉ„λ‘œμ†Œ μ‹ λ’°μ„± 있고 μ•ˆμ •μ μΌ 수 μžˆλŠ”λ°μš”. 아무리 ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό 꼼꼼히 μž‘μ„±ν•˜λ”λΌλ„ λ™μΌν•œ μž…λ ₯값에 λŒ€ν•΄ 항상 같은 κ²°κ³Όλ₯Ό 좜λ ₯ν•˜μ§€ μ•ŠλŠ” 비결정적 ν…ŒμŠ€νŠΈκ°€ λœλ‹€λ©΄ ν…ŒμŠ€νŠΈμ½”λ“œμ˜ 의미λ₯Ό 상싀할 수 μžˆμŠ΅λ‹ˆλ‹€. λ•Œλ¬Έμ— μΆ©λΆ„ν•œ ν…ŒμŠ€νŠΈ 격리λ₯Ό 톡해 μ•ˆμ •μ μΈ ν…ŒμŠ€νŠΈκ°€ μˆ˜ν–‰λ  수 μžˆλ„λ‘ ν•˜λŠ” 것이 맀우 μ€‘μš”ν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ 격리가 이루어지지 μ•ŠλŠ” 근본적인 원인은 각각의 ν…ŒμŠ€νŠΈκ°€ ν•˜λ‚˜μ˜ μžμ›μ„ κ³΅μœ ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. JUnit κ³Ό Spring 에선 @AfterEach 와 @Transactional 등을 톡해 ν…ŒμŠ€νŠΈ 격리가 κ°€λŠ₯ν•©λ‹ˆλ‹€. 뿐만 μ•„λ‹ˆλΌ Mock ν”„λ ˆμž„μ›Œν¬λ₯Ό ν™œμš©ν•˜μ—¬ ν…ŒμŠ€νŠΈ 격리에 μ‹ κ²½μ“°μ§€μ•Šκ³  계측 κ΅¬μ‘°μ—μ„œ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•  수 μžˆλŠ” 방법 μ—­μ‹œ μ‘΄μž¬ν•©λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 운영 ν™˜κ²½κ³Ό 같은 μ‘°κ±΄μ—μ„œ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜λŠ” 인수 ν…ŒμŠ€νŠΈμ˜ 경우 Mock ν”„λ ˆμž„μ›Œν¬λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  μ‹€μ œ DB λ₯Ό μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€. 즉, ν…ŒμŠ€νŠΈκ°€ 진행됨에 따라 DB μƒνƒœκ°€ μ§€μ†μ μœΌλ‘œ λ³€ν•΄ λ™μΌν•œ 초기 μƒνƒœλ₯Ό 보μž₯ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— μΆ©λΆ„ν•œ ν…ŒμŠ€νŠΈ 격리가 μ΄λ£¨μ–΄μ‘Œλ‹€κ³  λ³Ό 수 μ—†μŠ΅λ‹ˆλ‹€.

이번 글에선 인수 ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•  λ•Œ ν…ŒμŠ€νŠΈλ₯Ό κ²©λ¦¬ν•˜λŠ” λ‹€μ–‘ν•œ 방법에 λŒ€ν•΄ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

Acceptance Test Isolation


@Transactional

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Transactional
class AcceptanceTest {
}

톡합 ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λ“―μ΄ @Transactional 을 λΆ™μ—¬ 인수 ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•΄λ³΄λ©΄ 둀백이 μˆ˜ν–‰λ˜μ§€ μ•ŠλŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” 인수 ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•  λ•Œ @SpringBootTest 에 port λ₯Ό μ§€μ •ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜κΈ° λ•Œλ¬ΈμΈλ°μš”. port λ₯Ό μ§€μ •ν•˜μ—¬ μ„œλ²„λ₯Ό κ΅¬λ™ν•˜κ²Œ 되면 @Transactional 이 적용된 Test Thread μ™€λŠ” λ³„κ°œλ‘œ ꡬ동쀑인 μ„œλ²„κ°€ λ³„λ„μ˜ Thread μ—μ„œ Spring Container λ₯Ό μ‹€ν–‰μ‹œμΌœ νŠΈλžœμž­μ…˜μ„ μ»€λ°‹ν•˜κΈ° λ•Œλ¬Έμ— 둀백이 μˆ˜ν–‰λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

@DirtiesContext

@DirtiesContext
class AcceptanceTest {
}

기본적으둜 Spring 은 Context Caching 을 톡해 Bean 을 μž¬μ‚¬μš©ν•©λ‹ˆλ‹€. @DirtiesContext λŠ” 이런 Bean 듀에 λŒ€ν•΄ Dirties λ₯Ό ν™•μΈν•˜κ³  Context λ₯Ό μƒˆλ‘œ λ‘œλ“œν•©λ‹ˆλ‹€. 즉, Context κ°€ μž¬κ΅¬μ„±λ  λ•Œλ§ˆλ‹€ ν…Œμ΄λΈ”λ„ μž¬κ΅¬μ„±ν•˜μ—¬ 초기 μƒνƒœλ₯Ό 보μž₯ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

μ–΄λ…Έν…Œμ΄μ…˜ μΆ”κ°€λ§ŒμœΌλ‘œ ν…ŒμŠ€νŠΈ 격리가 이루어져 μ‚¬μš©μ΄ κ°„νŽΈν•˜μ§€λ§Œ 맀번 Context λ₯Ό μƒˆλ‘œ λ‘œλ“œν•˜κΈ° λ•Œλ¬Έμ— μ‹œκ°„μ΄ μ˜€λž˜κ±Έλ¦°λ‹€λŠ” 단점이 μ‘΄μž¬ν•˜μ—¬ 크게 μΆ”μ²œν•˜μ§€ μ•ŠλŠ” λ°©λ²•μž…λ‹ˆλ‹€.

Context λ₯Ό 맀번 μƒˆλ‘œ λ‘œλ“œν•  ν•„μš” 없이 DB 만 μ΄ˆκΈ°ν™” μ‹œμΌœμ£ΌλŠ” 것이 λͺ©μ μ΄κΈ° λ•Œλ¬Έμ— @DirtiesContext 보단 λ‹€λ₯Έ 방법이 더 쒋은 선택지일 수 μžˆμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ μˆ˜ν–‰ ν›„ 직접 μ‚­μ œ

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class AcceptanceTest {
 
    @Autowired
    private UserRepository userRepository;
 
    @BeforeEach
    void beforeEach() {
        if (RestAssured.port == UNDEFINED_PORT) {
            RestAssured.port = port;
        }
        userRepository.save(new User("quaritch"));
    }
 
    // ...
 
    @AfterEach
    void afterEach() {
        userRepository.deleteAll();
    }
}

DB λ₯Ό μ΄ˆκΈ°ν™”ν•˜λŠ” κ°€μž₯ 기본적인 방법은 맀 ν…ŒμŠ€νŠΈλ§ˆλ‹€ 초기 데이터λ₯Ό μ„ΈνŒ…ν•΄μ£Όκ³  ν…ŒμŠ€νŠΈκ°€ μ’…λ£Œλ  λ•Œ λ§ˆλ‹€ 데이터λ₯Ό μ œκ±°ν•΄μ£ΌλŠ” λ°©λ²•μž…λ‹ˆλ‹€. JUnit 의 @BeforeEach 와 @AfterEach λ₯Ό μ‚¬μš©ν•˜μ—¬ 데이터 생성 및 μ‚­μ œ μš”μ²­μ„ λ³΄λ‚΄λŠ” 방식을 생각해 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ 데이터가 적은 경우, κ°„λ‹¨ν•˜κ²Œ μˆ˜ν–‰ν•  수 μžˆλŠ” 방법이기 λ•Œλ¬Έμ— 많이 μ‚¬μš©λ˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ 생성할 데이터가 λ§Žμ„ 경우 μƒμ„±ν•œ 만큼 μ‚­μ œ μš”μ²­μ΄ μΆ”κ°€λ˜λ©° 연관관계 맀핑이 λ˜μ–΄μžˆμœΌλ©΄ 도메인 지식이 μ—†λŠ” μ‚¬λžŒμ΄ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•  λ•Œ μ—°κ΄€κ΄€κ³„λ‘œ μƒμ„±λ˜λŠ” μ—”ν‹°ν‹°λ₯Ό μΆ”μ ν•˜κΈ° μ–΄λ €μš°λ©° μ™Έλž˜ν‚€ λ“± μ œμ•½ 쑰건에 따라 μ‚­μ œκ°€ μ–΄λŸ¬μšΈ 수 μžˆλŠ” λΉ„νš¨μœ¨μ΄ μ‘΄μž¬ν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ μˆ˜ν–‰ ν›„ TRUNCATE 둜 ν…Œμ΄λΈ” μ΄ˆκΈ°ν™”

TRUNCATE λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•˜λ©΄ μ•žμ„œ μ†Œκ°œλœ 방법듀 보닀 훨씬 κ°„νŽΈν•˜κ²Œ DB λ₯Ό μ΄ˆκΈ°ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

API μš”μ²­μ„ 보낼 ν•„μš”λ„ μ—†κ³  DELETE 와 λ‹€λ₯΄κ²Œ SELECT κ°€ ν•„μš”ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— μƒλŒ€μ μœΌλ‘œ 적은 μ‹œκ°„μ— ν…Œμ΄λΈ”μ„ μ΄ˆκΈ°ν™”ν•  수 μžˆλŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Sql("truncate.sql")
class AcceptanceTest {
}

λ¨Όμ € @Sql μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 TRUNCATE 쿼리λ₯Ό μ‹€ν–‰ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ ν΄λž˜μŠ€κ°€ μ‹€ν–‰λ˜κΈ° 전에 @Sql 에 μ„ μ–Έλœ κ²½λ‘œμ— μžˆλŠ” SQL 이 λ¨Όμ € μ‹€ν–‰λ©λ‹ˆλ‹€. ν•΄λ‹Ή 파일 μ•ˆμ— λͺ¨λ“  ν…Œμ΄λΈ”μ— λŒ€ν•œ TRUNCATE λͺ…λ Ήμ–΄λ₯Ό μž‘μ„±ν•΄ λ†“μœΌλ©΄ 파일 ν•˜λ‚˜λ§ŒμœΌλ‘œ ν…ŒμŠ€νŠΈ 격리λ₯Ό 이뀄낼 수 μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μ—”ν‹°ν‹° ν˜Ήμ€ ν…Œμ΄λΈ”μ΄ 좔가될 λ•Œ λ§ˆλ‹€ νŒŒμΌμ„ 직접 μˆ˜μ •ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€λŠ” 단점이 μ‘΄μž¬ν•©λ‹ˆλ‹€.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class AcceptanceTest {
 
    @Autowired
    private EntityManager entityManager;
    
    @AfterEach
    void afterEach() {
        // μ“°κΈ° 지연 μ €μž₯μ†Œμ— 남은 쿼리 μ‹€ν–‰
        entityManager.flush();
        // ν…Œμ΄λΈ” 이름 μΆ”μΆœ
        List<String> tableNames = getTableNames();
        // μ°Έμ‘° 무결성 λΉ„ν™œμ„±ν™”
        executeQuery("SET REFERENTIAL_INTEGRITY FALSE");
        for (String tableName : tableNames) {
            // TRUNCATE μ‹€ν–‰
            executeQuery(String.format("TRUNCATE TABLE %s", tableName));
            // ID κ°’ 1둜 μ΄ˆκΈ°ν™”
            executeQuery(String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName));
        }
        // μ°Έμ‘° 무결성 μž¬ν™œμ„±ν™”
        executeQuery("SET REFERENTIAL_INTEGRITY TRUE");
    }
 
    private List<String> getTableNames() {
        return entityManager.getMetamodel()
                .getEntities()
                .stream()
                .map(e -> e.getJavaType()
                        .getAnnotation(Table.class)
                        .name()
                        .toUpperCase())
                .collect(Collectors.toList());
    }
 
    private void executeQuery(final String sql) {
        entityManager.createNativeQuery(sql)
                .executeUpdate();
    }
}

두 번째 방식은 EntityManager λ‚˜ JdbcTemplate 을 Bean 으둜 μ£Όμž…λ°›μ•„ λͺ¨λ“  ν…Œμ΄λΈ” 이름을 μ‘°νšŒν•˜μ—¬ 각 ν…ŒμŠ€νŠΈκ°€ μ‹œμž‘λ  λ•Œ TRUNCATE 쿼리λ₯Ό μ‹€ν–‰ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

TRUNCATE λͺ…λ Ήμ–΄λŠ” μ™Έλž˜ν‚€ λ“±μœΌλ‘œ 인해 μ •μƒμ μœΌλ‘œ μ‹€ν–‰λ˜μ§€ μ•Šμ„ 수 있기 λ•Œλ¬Έμ— μ°Έμ‘° 무결성을 λΉ„ν™œμ„±ν™”ν•œ λ’€ μ‹€ν–‰ν•˜κ³  μΆ”κ°€μ μœΌλ‘œ AUTO_INCREMENT λ₯Ό μ‚¬μš©ν•œλ‹€λ©΄ ID 값을 μ΄ˆκΈ°ν™”ν•΄μ£ΌλŠ” μž‘μ—… μ—­μ‹œ 좔가해쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

ν•œ 번 λ§Œλ“€μ–΄ λ†“μœΌλ©΄ μ—”ν‹°ν‹°κ°€ μ–Όλ§ˆλ‚˜ μΆ”κ°€λ˜κ³  μ‚­μ œλ˜λŠ”μ§€μ™€ 관계없이 μžλ™μœΌλ‘œ ν…Œμ΄λΈ”μ„ μ‘°νšŒν•˜μ—¬ λͺ¨λ‘ μ΄ˆκΈ°ν™”ν•˜κΈ° λ•Œλ¬Έμ— κ°€μž₯ 효과적으둜 인수 ν…ŒμŠ€νŠΈλ₯Ό 격리할 수 μžˆλŠ” λ°©λ²•μž…λ‹ˆλ‹€.

ν•΄λ‹Ή 방법을 톡해 관리 포인트λ₯Ό 쀄이며 인수 ν…ŒμŠ€νŠΈ μ‹œ μΆ©λΆ„ν•œ 격리성을 ν™•λ³΄ν–ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ—¬μ „νžˆ ν…ŒμŠ€νŠΈ ν΄λž˜μŠ€κ°€ 좔가될 λ•Œλ§ˆλ‹€ ν•΄λ‹Ή μ½”λ“œλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— 쀑볡이 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό κ³ λ„ν™”ν•˜μ—¬ λ¬Έμ œμ μ„ ν•΄κ²°ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

Advancement


Spring 의 TestExecutionListener μ‚¬μš©

public class AcceptanceTestExecutionListener implements TestExecutionListener {
 
    private EntityManager entityManager;
 
    @Override
    public void afterTestMethod(final TestContext testContext) {
        this.entityManager = testContext.getApplicationContext()
                .getBean(EntityManager.class);
        List<String> tableNames = getTableNames();
        executeQuery("SET FOREIGN_KEY_CHECKS = 0");
        for (String tableName : tableNames) {
            executeQuery(String.format("TRUNCATE TABLE %s", tableName));
            executeQuery(String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName));
        }
        executeQuery("SET FOREIGN_KEY_CHECKS = 1");
    }
 
    // ...
}

Spring 은 ν…ŒμŠ€νŠΈμ˜ νŠΉμ • μ‹œμ μ— κ°œμž…ν•  수 μžˆλŠ” λ¦¬μŠ€λ„ˆ μΈν„°νŽ˜μ΄μŠ€μΈ TestExecutionListener λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. afterTestExecution() λ˜λŠ” afterTestMethod() λ₯Ό 톡해 ν…ŒμŠ€νŠΈ μ‹€ν–‰ ν›„ κ°œμž…ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(value = {AcceptanceTestExecutionListener.class,}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface AcceptanceTest {
}

@TestExecutionListers λ₯Ό 톡해 μœ„μ™€ 같이 등둝해쀄 수 μžˆμŠ΅λ‹ˆλ‹€. λͺ¨λ“  ν…ŒμŠ€νŠΈ ν΄λž˜μŠ€μ— λΆ™μ΄λŠ” λ²ˆκ±°λ‘œμ›€μ„ 쀄이기 μœ„ν•΄ μ»€μŠ€ν…€ μ–΄λ…Έν…Œμ΄μ…˜μ„ μ„ μ–Έν•΄μ„œ μ‚¬μš©ν•΄μ£Όλ©΄ @AcceptanceTest λ§Œμ„ λΆ™μ—¬ κ°„νŽΈν•˜κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

JUnit5 의 Extension μ‚¬μš©

public class DatabaseCleanerExtension implements AfterEachCallback {
 
    private EntityManager entityManager;
 
    @Override
    public void afterEach(final ExtensionContext context) {
        this.entityManager = SpringExtension.getApplicationContext(context)
                .getBean(EntityManager.class);
        List<String> tableNames = getTableNames();
        executeQuery("SET FOREIGN_KEY_CHECKS = 0");
        for (String tableName : tableNames) {
            executeQuery(String.format("TRUNCATE TABLE %s", tableName));
            executeQuery(String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName));
        }
        executeQuery("SET FOREIGN_KEY_CHECKS = 1");
    }
 
    // ...
}

JUnit5 μ—­μ‹œ ν…ŒμŠ€νŠΈμ˜ νŠΉμ • μ‹œμ μ— κ°œμž…ν•  수 μžˆλŠ” Extension μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. Extension μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™•μž₯ν•œ μΈν„°νŽ˜μ΄μŠ€ 쀑 ν…ŒμŠ€νŠΈ μ‹€ν–‰ 직후에 κ°œμž…ν•  수 μžˆλŠ” AfterEachCallback μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜μ—¬ DB 의 초기 μƒνƒœλ₯Ό 보μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({DatabaseCleanerExtension.class})
public @interface AcceptanceTest {
}

@ExtendWith λ₯Ό 톡해 μœ„μ™€ 같이 등둝해쀄 수 μžˆμŠ΅λ‹ˆλ‹€. μ—­μ‹œ μ»€μŠ€ν…€ μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 κ°„νŽΈν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ„λ‘ λ§Œλ“€μ–΄μ€„ 수 μžˆμŠ΅λ‹ˆλ‹€.

DatabaseCleaner 생성

아직도 κ°œμ„ μ μ΄ λ³΄μž…λ‹ˆλ‹€. ν…ŒμŠ€νŠΈλ§ˆλ‹€ 이루고 싢은 것은 TRUNCATE 일 뿐인데 맀번 Context μ—μ„œ EntityManager λ₯Ό κ°€μ Έμ™€μ„œ ν…Œμ΄λΈ” 이름을 μΆ”μΆœν•˜λŠ” 과정이 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

Spring 을 μ’€ 더 Spring λ‹΅κ²Œ μ“Έ 수 μžˆλ„λ‘ DatabaseCleaner λΌλŠ” Component λ₯Ό λ§Œλ“€μ–΄ Bean 으둜 λ“±λ‘μ‹œμΌœμ€μ‹œλ‹€.

@Component
public class DatabaseCleaner {
 
    @Autowired
    private EntityManager entityManager;
 
    private List<String> tableNames;
 
    @PostConstruct
    public void afterPropertiesSet() {
        this.tableNames = entityManager.getMetamodel()
                .getEntities()
                .stream()
                .map(e -> e.getJavaType()
                        .getAnnotation(Table.class)
                        .name()
                        .toUpperCase())
                .collect(Collectors.toList());
    }
 
    @Transactional
    public void truncateTables() {
        entityManager.flush();
        executeQuery("SET REFERENTIAL_INTEGRITY FALSE");
        for (String tableName : tableNames) {
            executeQuery(String.format("TRUNCATE TABLE %s", tableName));
            executeQuery(String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName));
        }
        executeQuery("SET REFERENTIAL_INTEGRITY TRUE");
    }
 
    private void executeQuery(final String sql) {
        entityManager.createNativeQuery(sql)
                .executeUpdate();
    }
}

DatabaseCleaner λΌλŠ” Component λ₯Ό μƒμ„±ν•¨μœΌλ‘œμ¨ Spring 의 DI λ₯Ό ν™œμš©ν•˜μ—¬ EntityManager λ₯Ό κ°€μ Έμ˜¬ 수 있게 λ˜μ—ˆκ³  @PostConstruct λ₯Ό 톡해 Bean 생성 μ‹œ ν…Œμ΄λΈ” 이름을 ν•œ 번만 μΆ”μΆœν•˜μ—¬ μ‚¬μš©ν•  수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, @Transactional 을 μΆ”κ°€ν•˜μ—¬ TRUNCATE 처리의 μ›μžμ„± μ—­μ‹œ 보μž₯ν•  수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

public class DatabaseCleanerExtension implements AfterEachCallback {
 
    @Override
    public void afterEach(final ExtensionContext context) {
        DatabaseCleaner databaseCleaner = SpringExtension.getApplicationContext(context)
                .getBean(DatabaseCleaner.class);
        databaseCleaner.truncateTables();
    }
}

이제 DatabaseCleaner 만 κ°€μ Έμ™€μ„œ λ©”μ„œλ“œ ν•˜λ‚˜λ§Œ μ‹€ν–‰ν•˜λŠ” κ²ƒμœΌλ‘œ 인수 ν…ŒμŠ€νŠΈ 격리가 μ™„μ„±λ©λ‹ˆλ‹€.

마무리


인수 ν…ŒμŠ€νŠΈλŠ” μ‹€μ œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ„œλΉ„μŠ€ λ˜λŠ” 것과 κ°€μž₯ λΉ„μŠ·ν™˜ ν™˜κ²½μ—μ„œ μ΄λ£¨μ§€λŠ” ν…ŒμŠ€νŠΈμΈ 만큼 μ•ˆμ •μ„±μžˆλŠ” ν…ŒμŠ€νŠΈ μ½”λ“œ ꡬ좕이 맀우 μ€‘μš”ν•©λ‹ˆλ‹€. @DirtiesContext λΆ€ν„° TRUNCATE 쿼리 μ‚¬μš©, Spring 의 TestExecutionListener 와 JUnit5 의 Extension κΉŒμ§€ λ‹€μ–‘ν•œ 방법을 톡해 인수 ν…ŒμŠ€νŠΈλ₯Ό κ²©λ¦¬ν•˜λŠ” 방법에 λŒ€ν•΄ μ•Œμ•„λ³΄μ•˜μŠ΅λ‹ˆλ‹€. TestExecutionListener 와 Extension 의 차이에 λŒ€ν•΄ μΆ”κ°€μ μœΌλ‘œ ν•™μŠ΅ν•˜μ—¬ 어떀점이 λ‹€λ₯΄κ³  μ–΄λ–€ 상황에 μ‚¬μš©ν•˜λŠ” 것이 쒋은지 κΉŒμ§€ μ•Œμ•„λ³΄λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.

References