테스트 간 데이터 격리는 어떻게 보장해야 할까요? #33
Replies: 3 comments
-
|
저는 테스트 간 데이터 격리의 본질이 격리가 깨지면 테스트가 실행 순서에 의존하게 되고, 그래서 저는 격리를 "테스트가 끝나고 청소하는 문제"가 아니라, 1. 데이터 격리가 깨지면 무슨 일이 일어나는가저는 이 문제의 심각성을 먼저 짚고 싶습니다. 격리가 깨진 테스트는 가장 디버깅하기 어려운 실패를 만들기 때문입니다. @Test
void 예약_목록_조회시_2건이_나온다() {
예약_2건_저장();
assertThat(repository.findAll()).hasSize(2); // 혼자 돌리면 통과
}이 테스트는 혼자 실행하면 통과합니다. 하지만 다른 테스트가 먼저 예약을 1건 저장하고 정리하지 않았다면, 전체 실행 시 이때 나타나는 증상들이 격리 문제의 특징입니다.
이전 토론들에서 강조했던 리팩터링 내성과 신뢰성이 모두 무너지는 지점입니다. 테스트가 실패해도 "진짜 버그인가, 격리 문제인가"를 의심하게 되면, 테스트 전체에 대한 신뢰가 무너집니다. 그래서 저는 격리를 "있으면 좋은 것"이 아니라 테스트가 신뢰를 가지기 위한 전제 조건이라고 생각합니다. 2. 격리의 두 가지 의미 — 상태 격리와 순서 독립저는 "격리"라는 말이 사실 두 가지를 담고 있다고 생각합니다. (a) 상태 격리한 테스트가 만든 데이터가 다른 테스트에 보이지 않는다. (b) 순서 독립테스트가 어떤 순서로 실행되든 같은 결과를 낸다. 이 둘은 연결되어 있습니다. 상태가 제대로 격리되면 순서에 독립적이 되고, 순서에 의존한다는 건 상태 격리가 깨졌다는 신호입니다. 핵심 원칙은 각 테스트는 알려진 초기 상태에서 시작해야 한다는 것입니다. 이전 테스트가 무엇을 남겼는지 몰라도, 내 테스트는 항상 동일한 출발점을 가져야 합니다. 이걸 보장하는 방법이 아래에서 다룰 여러 전략들입니다. 3. 방법 1 — @transactional 롤백가장 흔한 방법입니다. 테스트 메서드에 @SpringBootTest
@Transactional
class ReservationServiceTest {
@Test
void 예약을_생성한다() {
reservationService.create(요청);
// 테스트 종료 시 자동 롤백 → DB에 흔적이 남지 않음
}
}장점
함정 — 이게 핵심입니다저는 테스트의 함정 (a) 실제 커밋 동작을 검증하지 못한다테스트가 롤백되므로, 실제 운영에서 커밋될 때 일어나는 일을 못 봅니다. 예를 들어 커밋 시점에 동작하는 제약 조건, 함정 (b) 트랜잭션 전파를 검증할 수 없다테스트 전체가 하나의 트랜잭션으로 묶이므로, Service의 트랜잭션 경계가 의도대로 동작하는지 검증하기 어렵습니다. 이번 미션처럼 "예약 취소 → 첫 대기 승격"을 한 트랜잭션으로 묶었을 때, 중간 실패 시 롤백되는지를 테스트 함정 (c) 동시성 테스트에서 무력하다여러 스레드로 동시 요청을 보내는 테스트는, 테스트 스레드의 트랜잭션과 별개로 동작하므로 그래서 저는 단순 CRUD나 도메인 검증에는 4. 방법 2 — 매번 데이터 초기화 (truncate / @SQL)테스트 전후로 테이블을 비우거나 초기 데이터를 다시 세팅하는 방법입니다. @SpringBootTest
class ReservationIntegrationTest {
@BeforeEach
void cleanUp() {
// 모든 테이블 truncate
jdbcTemplate.execute("TRUNCATE TABLE reservation");
jdbcTemplate.execute("ALTER TABLE reservation AUTO_INCREMENT = 1");
}
}또는 @Sql(scripts = "/cleanup.sql", executionPhase = BEFORE_TEST_METHOD)장점
함정함정 (a) 느리다매번 실제 삭제·재삽입이 일어나므로 롤백보다 느립니다. 테스트가 많아질수록 누적됩니다. 함정 (b) 외래키 순서 문제테이블을 truncate할 때 외래키 제약 때문에 순서가 중요합니다. 자식 테이블부터 지워야 하고, 안 그러면 제약 위반이 납니다. 또는 외래키 체크를 잠시 끄는 처리가 필요합니다. 함정 (c) AUTO_INCREMENT 초기화 누락테이블만 비우고 AUTO_INCREMENT를 리셋하지 않으면, ID가 1이 아니라 이어집니다. "생성된 예약의 ID는 1이다" 같은 테스트가 깨집니다. 저는 이 함정을 직접 실험해보면 격리의 의미를 가장 명확하게 체감할 수 있다고 생각합니다. 5. 방법 3 — 각 테스트가 자기 데이터를 명시적으로 만든다전역 초기 데이터에 의존하지 않고, 각 테스트가 필요한 데이터를 그 안에서 직접 생성하는 방식입니다. @Test
void 본인_예약만_조회된다() {
Member 모카 = memberRepository.save(회원("moca"));
Member 손손 = memberRepository.save(회원("songsong"));
reservationRepository.save(예약(모카));
reservationRepository.save(예약(손손));
assertThat(reservationService.findByMember(모카.getId())).hasSize(1);
}장점
함정함정 (a) "특정 개수"에 의존하는 검증이 위험하다
그래서 가능하면 함정 (b) 여전히 cleanup이 필요할 수 있다데이터를 명시적으로 만들어도, 테스트가 끝난 뒤 정리하지 않으면 다음 테스트의 그래서 이 방법도 보통 롤백이나 truncate와 함께 씁니다. 6. 격리 단위를 어디까지 가져갈 것인가저는 격리를 고민할 때 어느 범위까지 격리할 것인가도 함께 생각해야 한다고 봅니다. 메서드 단위 격리각 클래스 단위 픽스처 공유
저는 변경(쓰기)이 있는 테스트는 반드시 메서드 단위로 격리하고, 읽기 전용이고 절대 안 바뀌는 데이터(예: 고정된 코드성 데이터)만 클래스 단위로 공유하는 편이 좋다고 생각합니다. 쓰기 테스트를 픽스처 공유로 묶으면 거의 반드시 격리 사고가 납니다. 7. 실험으로 확인해볼 것저는 격리야말로 직접 실험으로 깨봐야 본질이 보인다고 생각합니다. 이전 격리 수준 토론에서 "이론을 현실에 대입하는 건 실험뿐"이라고 했던 것과 같은 맥락입니다. 해볼 만한 실험들: 실험 1. 일부러 격리를 깨본다
실험 2. AUTO_INCREMENT 함정 재현
실험 3. @transactional 롤백이 동시성 테스트에서 무력한 것 확인
이런 실험은 "왜 격리가 필요한가"를 말로 아는 것과, "격리가 깨지면 이렇게 된다"를 눈으로 본 것의 차이를 만들어준다고 생각합니다. 8. 미션에서의 제 선택이번 예약 대기 미션 기준으로 제가 격리를 다루는 방식입니다. 일반 통합 테스트 (CRUD, 유스케이스 흐름)
트랜잭션 경계 검증 (예약 취소 → 대기 승격의 원자성)
동시성 테스트 (동시 대기 신청, 동시 예약)
검증 스타일
9. 정리저는 테스트 데이터 격리를 이렇게 정리할 수 있을 것 같습니다.
핵심은 "어느 한 방법이 정답"이 아니라, 검증 대상에 따라 방법을 나누는 것입니다. 단순 검증은 롤백으로 빠르게, 트랜잭션 경계와 동시성은 실제 cleanup으로 — 이렇게 나누는 기준은 결국 이 테스트에서 롤백이 검증 대상을 가려버리는가라고 생각합니다. 그리고 무엇보다, 격리가 깨졌을 때의 증상(순서 의존, 혼자는 통과·전체는 실패)을 한 번 직접 겪어보는 것이 격리를 진짜로 이해하는 가장 빠른 길이라고 봅니다. |
Beta Was this translation helpful? Give feedback.
-
테스트 격리란테스트 격리각 테스트가 다른 테스트의 상태에 영향을 주지도, 받지도 않게 만드는 것이다.
테스트 격리의 대상은 3가지가 존재한다.
왜 테스트 격리가 필요할까?왜 테스트 격리가 필요할까?테스트가 격리가 안되어 있다면 생기는 문제점들이 존재한다. 1. 순서 의존 order DependencyA → B라면 통과하고 , B → A라면 실패하는 상황이라면, 2. 누적 오염예약 생성에 대한 테스트 데이터가 남아있는 상황이라면 3. 유령 통과어제는 되던 테스트가 오늘 안된다는 상황이 발생하게 된다.
어떻게 테스트 격리를 해야할까?어떻게 테스트 격리를 해야할까?1. 트랜잭션 롤백 (@transactional on test)테스트 메서드 전체를 하나의 트랜잭션으로 감싸고, 단점:
2. @DirtyContext테스트 후 ApplicationContext 자체를 버리고 새로 띄운다. 단점:
3. 테이블 초기화 (truncate / deleteAll)@beforeeach나 @SQL로 실제 DB를 비운다. 실제 commit이 일어나기 때문에 운영 동작에 가장 가깝고 롤백의 단점을 보완할 수 있다. 단점:
4. 고유 데이터 사용하기테스트마다 충돌이 안나는 키를 써서 애초에 부딪히지 않게 한다. 단점:
상황에 따라 어떻게 테스트 격리를 보장해야할까?상황에 따라 어떻게 테스트 격리를 보장해야할까?격리를 해야하는 방법
격리에 주의해야하는 점:
정리정리테스트 격리란 테스트끼리 서로 영향을 주고 받게 된다면 위아 같이 같은 문제가 존재하게 된다.
이런 문제점들을 해결하기 위해서 테스트끼리 서로 격리를 하게 되는데,
이 5개의 질문들을 다 통과 한다면 격리가 되었다고 봐도 좋고, |
Beta Was this translation helpful? Give feedback.
-
테스트 실행 컨텍스트 격리가 필요한 이유테스트 격리는 테스트가 서로 영향을 주지 않도록 구성하는 것을 의미합니다. 즉, 어떤 순서로 테스트를 실행하던지 항상 같은 결과가 나오도록 보장하기 위해서는 테스트 격리가 필요합니다. 테스트의 실행 순서가 변경되었을 때, 결과가 변동될 수 있는 가장 보편적인 이유는 테스트 실행이후 데이터의 변화 때문인데, 앞선 테스트의 실행 이후 데이터의 변화에 따라 테스트의 결과가 영향을 받을수 있기 때문입니다. 예를 들어, 회원 생성 API 테스트를 실행하고 회원 조회 API 테스트를 실행할 때, 회원조회 테스트가 현재 존재하는 데이터의 개수로 검증 결과가 판단된다면, 이 테스트는 앞선 생성 테스트 때문에 실패하게 됩니다. 그렇기 때문에 각 테스트는 앞선 컨텍스트에 영향을 받으면 안되며 격리되어 독립적으로 실행되도록 보장해야합니다. 테스트 격리를 위한 도구들테스트 격리를 위해 사용할 수 있는 방법은 대표적으로. 1.
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
테스트를 작성하다 보면 각 테스트가 독립적으로 실행되어야 한다는 이야기를 자주 듣습니다.
하지만 실제로는 이전 테스트에서 저장한 데이터가 다음 테스트에 영향을 주거나,
테스트 실행 순서에 따라 성공/실패가 달라지는 문제가 생길 수 있습니다.
예를 들어 예약 생성 테스트에서 저장한 데이터가 남아 있다면,
다음 테스트의 중복 예약 검증이나 전체 조회 결과에 영향을 줄 수 있습니다.
이를 막기 위해 여러 방법을 사용할 수 있을 것 같습니다.
각 방식은 테스트 격리에는 도움이 되지만,
속도, 유지보수성, 테스트 신뢰도 측면에서 서로 다른 트레이드오프가 있을 것 같습니다.
그렇다면 테스트 간 데이터 격리는 어떤 방식으로 보장하는 것이 좋을까요?
그리고 테스트가 서로 독립적이라는 것을 어떤 기준으로 판단할 수 있을까요?
Beta Was this translation helpful? Give feedback.
All reactions