카테고리 없음

[Spring Boot 테스트] Gradle에서 필터 중복 + Mockito 설정 충돌로 인한 테스트 실패 해결기

책다니엘 2025. 5. 8. 17:31

📝 서론

Spring Security + JWT 기반 인증 구조를 가진 프로젝트에서 MockMvc를 활용한 단위 테스트를 작성하던 중
두 가지 독립적인 테스트가 서로 간섭하면서 테스트가 실패하는 현상을 겪었다.
Gradle CLI 환경에서만 발생하며, IDE에서는 정상 작동하는 케이스였다.


	@Nested
	@DisplayName("회원 탈퇴 테스트")
	class DeleteUserTest {

		@Test
		@DisplayName("회원 탈퇴 성공 (JWT 인증)")
		void deleteUserSuccess() throws Exception {
		    // given
		    Long userId = 1L;
		    String email = "user@example.com";
		    String token = jwtTokenProvider.createToken(userId, email);  // 실제 토큰 발급

		    com.example.moneytalk.domain.User mockUser = mockUser();

		    given(jwtTokenProvider.validateToken(anyString())).willReturn(true);
		    given(jwtTokenProvider.getUserId(anyString())).willReturn(userId);
		    given(userRepository.findById(userId)).willReturn(Optional.of(mockUser));

		    doAnswer(invocation -> {
		        System.out.println("🧪 deleteUser() 호출!");
		        return null;
		    }).when(userService).deleteUser(any());

		    // when & then
		    mockMvc.perform(delete("/api/users/me")
		            .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
		            .with(csrf()))
		        .andExpect(status().isNoContent());

		    verify(userService, times(1)).deleteUser(any());
		}

	}

❌ 문제 상황

회원 탈퇴 API 테스트에서 다음과 같은 코드가 작성되어 있었다:

verify(userService, times(1)).deleteUser(any());

 

하지만 Gradle로 실행하면 다음과 같은 오류가 발생했다:

Wanted 1 time:
But was 2 times:
-> at UserController.deleteUser(...)

🔍 원인 분석

로그를 통해 확인한 결과, 테스트 중에 JwtAuthenticationFilter가 두 번 작동하고 있었고,
결과적으로 컨트롤러의 deleteUser() 메서드가 두 번 실행된 것이 원인이었다.

이는 테스트에서 다음과 같은 구조로 필터를 수동 등록하면서 발생한 문제였다:

http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userRepository), ...);

 

테스트 클래스가 @SpringBootTest와 @TestConfiguration을 함께 사용할 경우,
Gradle CLI 환경에서는 Spring Context가 강하게 캐시되거나 두 번 초기화되는 상황이 발생할 수 있고,
이때 필터가 중복 등록되며 요청을 두 번 처리하게 된다.


✅ 회피 방법 (현실적 해결책)

이 문제를 구조적으로 해결하려면 다음과 같은 리팩토링이 필요하다:

  • SecurityFilterChain에서 필터 등록을 조건부로 구성하거나
  • 실제 애플리케이션 설정을 @Import해서 사용하는 방식으로 통합하거나
  • DirtiesContext로 컨텍스트를 매번 리셋

하지만 이렇게 하면 테스트 실행 속도가 느려지고, 테스트 클래스 전체가 무거워질 수 있다.

그래서 이번에는 현실적인 절충안으로, 아래와 같이 테스트를 회피적으로 수정했다:

verify(userService, atLeastOnce()).deleteUser(any());

 

또는

verify(userService, times(2)).deleteUser(any()); // 실제 2번 호출됨을 허용

 


2️⃣ 문제 2: NullPointerException 발생 (테스트 간 설정 충돌)


정상적으로 응답을 기대한 테스트에서 다음과 같은 에러가 발생했다:

java.lang.NullPointerException: 테스트용 NPE
at UserService.getMyInfo(...)


🔍 원인

// 이전 테스트에서 설정됨
given(userService.getMyInfo(any())).willThrow(new NullPointerException("테스트용 NPE"));


→ @MockBean으로 주입된 userService는 테스트 간 공유되는 싱글턴 Mock 객체이기 때문에,
다른 테스트에서 설정한 willThrow()가 계속 유지되어 있었던 것.


✅ 해결 방법: @AfterEach로 Mock 초기화

@AfterEach
void tearDown() {
    reset(userService, jwtTokenProvider, userRepository);
}


이렇게 하면 각 테스트가 끝날 때마다 모든 설정이 초기화되므로,
테스트 간 설정 충돌이 발생하지 않는다.

 


🧩 추가로 알게 된 교훈


항목 실전 교훈
@MockBean은 전역 공유됨 각 테스트가 given(...).willThrow() 등을 설정하면, 다른 테스트도 영향을 받는다
@DirtiesContext는 만능이 아님 Gradle에서는 여전히 컨텍스트 내부 bean의 상태가 유지될 수 있다
reset(mock...)은 빠르고 효과적 설정 충돌을 안전하게 막아준다
인증 필터 테스트는 헤더 기반으로 구성 .with(authenticatedUser(...))는 Jwt 기반 필터에서는 무의미함

 



✅ 결론

단위 테스트는 단순한 검증이 아니라 인증 흐름, 필터 체인, Mock 설정 관리까지 모두 컨트롤해야 한다.
이번 경험을 통해 필터 중복 문제, 인증 흐름 충돌, Mockito 설정 공유 이슈를 모두 해결했고,
테스트를 안정적으로 유지하는 방법에 대해 중요한 인사이트를 얻을 수 있었다.