[트러블슈팅] QueryDSL 검색 도입 및 MockMvc/트랜잭션 분리

Ahn·2026년 9월 3일

QueryDSL 검색 도입 및 MockMvc/트랜잭션 분리 트러블슈팅

날짜: 2026년 9월 3일

주제: Spring Data Web 페이징 직렬화 대응, @WebMvcTest 보안 컨텍스트 주입, REQUIRES_NEW 트랜잭션 분리 및 테스트 격리


1. MockMvc 슬라이스 테스트 시 401 Unauthorized 에러

문제 상황

TodoController의 검색 API를 검증하기 위해 @WebMvcTest(TodoController.class)를 작성하고 mockMvc.perform(get("/todos/search"))를 호출했으나, 200 OK 대신 401 Unauthorized 또는 403 Forbidden이 반환되며 테스트가 실패함.

원인

  • @WebMvcTest는 컨트롤러 레이어만 격리하여 로드하지만, SecurityConfigJwtAuthenticationFilter와 같은 웹 보안 필터 체인은 컴포넌트 스캔 대상에 포함됨.
  • 테스트 요청 시점에 별도의 인증 정보(SecurityContext)나 JWT 헤더를 넘기지 않아 필터 체인에서 인가되지 않은 사용자로 판단하고 요청을 차단함.

해결 방법

  • 테스트 클래스 레벨에 @WithMockUser를 적용하여 보안 컨텍스트에 가상의 인증 사용자를 미리 주입함.
@WebMvcTest(TodoController.class)
@WithMockUser // 가상 인증 객체 주입하여 Security Filter 통과
class TodoControllerTest {
    @Autowired
    private MockMvc mockMvc;
    ...
}

2. Spring Data Page JSON 응답 경로 불일치 (No value at JSON path)

문제 상황

컨트롤러 테스트에서 페이징 결과의 총 요소 수를 검증할 때 jsonPath("$.totalElements")를 조회했으나 No value at JSON path "$.totalElements" 에러 발생.

원인

  • 최신 Spring Data Web 환경 및 PagedModel 직렬화 설정에서는 페이징 메타데이터가 루트 레벨이 아닌 page 객체 하위로 묶여 반환됨.
  • JSON 직렬화 구조:
{
  "content": [...],
  "page": {
    "size": 10,
    "number": 0,
    "totalElements": 1,
    "totalPages": 1
  }
}

해결 방법

  • JsonPath 대상을 루트가 아닌 하위 객체 필드($.page.totalElements)로 수정함.
// 기존: .andExpect(jsonPath("$.totalElements").value(1))
// 수정 후:
.andExpect(jsonPath("$.page.totalElements").value(1));

3. QueryDSL 동적 조건 누락 임포트 및 페이징 카운트 최적화

문제 상황

  • 동적 쿼리 작성 시 BooleanExpression 및 문자열 유효성 검사를 위한 유틸리티 클래스가 임포트되지 않아 컴파일 에러 발생.
  • 데이터가 적거나 없는 경우에도 불필요하게 카운트 쿼리가 항상 실행되는 비효율 발생.

원인 및 해결 방법

  1. 임포트 정리: com.querydsl.core.types.dsl.BooleanExpressionorg.springframework.util.StringUtils 명시.
  2. 카운트 쿼리 지연 실행: new PageImpl<>()로 매번 카운트를 직접 날리지 않고, Spring Data의 PageableExecutionUtils.getPage()를 적용.
  • 첫 페이지에서 조회된 데이터 수가 페이지 사이즈보다 적거나, 마지막 페이지인 경우 카운트 쿼리 실행을 자체 생략하도록 최적화.
JPAQuery<Long> countQuery = queryFactory
        .select(todo.count())
        .from(todo)
        .where(
                titleContains(keyword),
                createdDateBetween(startDate, endDate),
                managerNicknameContains(nickname)
        );

return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);

4. Propagation.REQUIRES_NEW를 통한 로그 독립 트랜잭션 처리

문제 상황

매니저 등록 실패(예외 발생) 시 해당 트랜잭션이 롤백되면서, 요청 내역을 남기려던 로그 엔티티까지 함께 롤백되어 DB에 기록이 남지 않는 문제 발생.

원인

  • 기본 트랜잭션 전파 속성(REQUIRED)은 부모 트랜잭션에 참여하기 때문에, 매니저 등록 중 예외가 발생하면 롤백 마크가 찍혀 전체 작업이 함께 롤백됨.

해결 방법

  1. 물리적 트랜잭션 분리: 로그 저장을 담당하는 LogService를 별도 빈으로 분리하고, 메서드에 @Transactional(propagation = Propagation.REQUIRES_NEW) 선언.
  • AOP 내부 호출(self-invocation) 문제를 방지하기 위해 클래스를 분리하여 프록시가 정상 작동하도록 구성.
  1. 예외 격리 및 재전파: ManagerService에서 try-catch로 예외를 잡아 실패 로그를 커밋한 뒤, 원래 예외를 다시 던져(throw e) 본 비즈니스 트랜잭션은 안전하게 롤백시킴.
  2. 통합 테스트 작성 주의: 테스트 메서드 자체에 @Transactional을 붙이면 REQUIRES_NEW로 별도 커밋된 결과 검증이 꼬일 수 있으므로, 테스트에서는 @Transactional을 끄고 @AfterEach로 직접 DB 정리를 수행함.

기본 전파 옵션에만 의존하지 않고 트랜잭션 경계를 명확히 분리해야 데이터 유실 없이 감사 로그(Audit Log)를 남길 수 있음을 확인했습니다. 또한 슬라이스 테스트 작성 시 프레임워크 기본 설정(Spring Security, Spring Data Web 직렬화 규격)에 맞춘 세부 모킹과 경로 검증이 필수적임을 배웠습니다.

profile
dophamine

0개의 댓글