회원 저장 테스트를 하나씩 돌리면 둘 다 통과한다. 함께 돌리면 두 번째에서 duplicate: kim이 난다. 테스트가 서로 눈치를 보는 걸까? 두 테스트가 같은 메모리 저장소를 공유하고 있을 가능성부터 살펴보자.
메모리 리포지토리 테스트가 왜 서로 영향을 줄까?
초기 개발에서는 DB 대신 Map에 회원을 저장하는 구현을 만들 수 있다. 아래 예제의 store는 static이다. 따라서 new MemoryRepository()로 객체를 두 개 만들어도 저장 공간은 하나다.
import java.util.HashMap;
import java.util.Map;
class MemoryRepository {
private static final Map<String, Integer> store = new HashMap<>();
void save(String name) {
if (store.containsKey(name)) {
throw new IllegalStateException("duplicate: " + name);
}
store.put(name, store.size() + 1);
}
int count() { return store.size(); }
void clear() { store.clear(); }
}첫 테스트가 kim을 저장한 뒤 비우지 않으면, 다음 테스트는 새 객체로 시작해도 같은 이름을 본다. new는 객체를 새로 만들었지만 static 필드까지 새로 만들지는 않는다. 그림에서 두 테스트가 같은 상자를 가리키는 이유다.
새 객체를 만들었는데도 중복 오류가 날까?
같은 예제를 순서대로 실행하면 상태가 남는 지점을 볼 수 있다.
class Demo {
public static void main(String[] args) {
MemoryRepository first = new MemoryRepository();
first.save("kim");
System.out.println(first.count());
MemoryRepository second = new MemoryRepository();
try {
second.save("kim");
} catch (IllegalStateException error) {
System.out.println(error.getMessage());
}
first.clear();
second.save("kim");
System.out.println(second.count());
}
}1
duplicate: kim
1두 번째 save는 중복 예외를 던지고, 저장소를 비운 뒤에는 같은 이름을 다시 저장할 수 있다. JUnit Jupiter는 기본적으로 테스트 메서드마다 테스트 클래스 객체를 새로 만들지만, static 상태까지 초기화하지는 않는다. 그래서 '테스트 객체가 새것'과 '테스트 데이터가 새것'은 다른 말이다. JUnit의 테스트 인스턴스 수명 설명도 이 기본 동작을 명시한다.
@AfterEach로 무엇을 초기화해야 할까?
이처럼 공유 저장소를 유지해야 하는 테스트라면 각 테스트가 끝날 때 저장소를 비워, 다음 테스트가 앞선 데이터에 기대지 않게 할 수 있다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
class MemoryRepositoryTest {
private final MemoryRepository repository = new MemoryRepository();
@AfterEach
void tearDown() {
repository.clear();
}
@Test
void savesMember() {
repository.save("kim");
assertEquals(1, repository.count());
}
@Test
void alsoStartsEmpty() {
repository.save("kim");
assertEquals(1, repository.count());
}
}@AfterEach는 각 테스트 뒤에 실행되는 JUnit Jupiter 훅이다. 두 테스트 모두 kim을 저장하지만, 각 테스트가 끝난 뒤 clear()가 호출되면 다음 테스트는 빈 저장소에서 시작한다.
테스트 격리는 clear만으로 충분할까?
가능하다면 더 단순한 방법은 테스트마다 독립된 저장소 인스턴스를 만드는 것이다. store에서 static을 빼고 각 테스트가 새 MemoryRepository를 사용하면 clear()를 잊어서 생기는 간섭을 줄일 수 있다. 반대로 Spring 컨텍스트가 공유하는 빈이나 실제 DB를 사용하는 테스트라면, 무엇을 새로 만들고 무엇을 함께 쓰는지 확인해야 한다.
공유된 static Map을 여러 테스트가 동시에 읽고 지운다면 @AfterEach만으로 격리를 보장할 수 없다. 한 테스트의 정리가 다른 테스트의 데이터를 지울 수 있기 때문이다. 병렬 실행이나 실제 DB 테스트에는 저장소 분리·테스트 데이터 범위·트랜잭션 경계를 별도로 설계하자. 메모리 예제의 clear()를 운영 데이터 저장소에 그대로 적용해서는 안 된다.
핵심 요약
단독 테스트는 통과하고 전체 실행에서만 실패한다면, 먼저 테스트 사이에 남는 상태를 찾자. static 저장소는 새 객체를 만들어도 공유된다. @AfterEach는 공유된 테스트 데이터를 매번 정리하는 방법이지만, 가장 깔끔한 출발점은 테스트마다 독립된 저장소를 쓰는 것이다.

