Project/배달 주문 관리 플랫폼

[Spring Boot] 배달 플랫폼 주문 도메인 설계: Repository Fixture를 활용한 통합 테스트 최적화와 보안 검증

Kr1 2026. 5. 17. 21:58
  • 시스템의 전체 흐름을 검증하는 통합 테스트(@SpringBootTest)는 무겁고 느릴 수밖에 없습니다. 이번 글에서는 거대한 E2E 테스트의 실행 속도를 최적화한 방법과, 앞서 구현했던 '실시간 권한 검증' 및 '5분 취소 룰'이 실제 환경에서 어떻게 방어되는지 코드로 증명하는 과정을 회고해 봅니다.

 

 

💻 실제 코드 (FullDomainIntegrationTest.java - 주요 시나리오 발췌)

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
public class FullDomainIntegrationTest {

    @Autowired private MockMvc mockMvc;
    @Autowired private OrderRepository orderRepository;
    // ... 의존성 주입 생략 ...

    // ---------------------------------------------------------
    // Helper Methods (Test Fixtures)
    // ---------------------------------------------------------

    /**
     * Repository를 통해 가게를 직접 생성함. (Given 데이터 최적화용)
     */
    private StoreEntity createStoreFixture(UserEntity owner, String storeName) {
        // 복잡한 연관관계(Area, Category)를 API 호출 없이 DB에 직접 주입함.
        // ... 생략 ...
        return storeRepository.save(StoreEntity.builder()
                .user(owner).name(storeName).minOrderPrice(15000).status(StoreStatus.OPEN)
                .openTime(LocalTime.of(10, 0)).closeTime(LocalTime.of(22, 0)).build());
    }

    /**
     * Repository를 통해 주문을 직접 생성함.
     */
    private OrderEntity createOrderFixture(UserEntity customer, StoreEntity store, AddressEntity address,
                                           OrderStatus status, int totalPrice) {
        return orderRepository.save(OrderEntity.builder()
                .userId(customer.getId()).storeId(store.getStoreId()).addressId(address.getId())
                .status(status).totalPrice(totalPrice).deliveryFee(3000).build());
    }

    // ---------------------------------------------------------
    // Test Methods
    // ---------------------------------------------------------

    @Test
    @DisplayName("실시간 권한 인가: CUSTOMER 권한을 MANAGER로 강제 변경 시 주문 생성 403 차단 검증")
    void changedCustomerRole_shouldBlockOrderCreationImmediately() throws Exception {
        // [Refactor] Given: 테스트 목적이 아닌 데이터는 Fixture로 빠르게 생성함.
        UserEntity owner = createUserFixture("owner_role@test.com", "Owner123!@#", "roleowner", UserRole.OWNER);
        String customerToken = signupAndGetToken("cust_role@test.com", "Cust123!@#", "rolecust", "CUSTOMER");
        UserEntity customerUser = getUserByEmail("cust_role@test.com");

        StoreEntity store = createStoreFixture(owner, "권한 차단 가게");
        MenuEntity menu = createMenuFixture(store, "일반 메뉴", 10000);
        AddressEntity address = createAddressFixture(customerUser, "자택", "서울시 강남구");

        // [핵심] 해킹 시뮬레이션: 정상 발급된 토큰을 가진 유저의 DB 상 권한을 강제로 변조함.
        ReflectionTestUtils.setField(customerUser, "userRole", UserRole.MANAGER);
        userRepository.saveAndFlush(customerUser);

        // When & Then: 토큰은 CUSTOMER지만 DB 권한은 MANAGER로 변한 고객이 주문 생성 시도 시 403 Forbidden
        // 차단됨을 검증함.
        Map<String, Object> req = Map.of("storeId", store.getStoreId(), "addressId", address.getId(),
                                         "orderItems", List.of(Map.of("menuId", menu.getMenuId(), "quantity", 1,
                                         "priceAtOrder", 10000)));

        mockMvc.perform(post("/api/v1/orders")
                .header("Authorization", customerToken) // 정상 토큰 삽입
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isForbidden()); // 서비스 계층의 validateUserRoleFromDB 로직이 제대로 작동함을
        // 증명함.
    }

    @Test
    @DisplayName("주문 5분 취소 룰: 5분이 경과된 주문 취소 요청 시 400 Bad Request 검증")
    void cancelOrderAfterFiveMinutes_shouldFail() throws Exception {
        // Given: 기초 데이터를 Fixture로 신속 생성함.
        UserEntity owner = createUserFixture("own_cancel@test.com", "Owner123!@#", "canowner", UserRole.OWNER);
        String customerToken = signupAndGetToken("cus_cancel@test.com", "Cust123!@#", "cancust", "CUSTOMER");
        UserEntity customer = getUserByEmail("cus_cancel@test.com");

        StoreEntity store = createStoreFixture(owner, "취소 불가 가게");
        AddressEntity address = createAddressFixture(customer, "자택", "서울시 강남구");

        // [핵심] 5분이 경과한 주문 시뮬레이션: Fixture로 주문 생성 후, Reflection으로 생성 시간을 강제 조작함.
        OrderEntity lateOrder = createOrderFixture(customer, store, address, OrderStatus.PENDING, 23000);
        ReflectionTestUtils.setField(lateOrder, "createdAt", LocalDateTime.now().minusMinutes(6));
        orderRepository.saveAndFlush(lateOrder);

        // When & Then: 취소 API 호출 시 비즈니스 룰에 의해 400 에러가 반환됨을 검증함.
        mockMvc.perform(patch("/api/v1/orders/" + lateOrder.getOrderId() + "/cancel")
                .header("Authorization", customerToken))
                .andExpect(status().isBadRequest());
    }
}

 

🤔 왜 이렇게 짰는가? (설계 의도와 이유)

Given 데이터 최적화를 위한 Repository Fixture 도입

  • 기존 통합 테스트는 '주문 취소' 하나를 검증하기 위해 회원가입, 가게 생성, 메뉴 생성 등 테스트 목적과 무관한 모든 사전 단계(Given)를 MockMvc API 호출로 처리했습니다. 이로 인해 불필요한 직렬화/역직렬화 오버헤드가 발생하여 테스트 실행 속도가 저하되었습니다.
  • 이를 개선하기 위해, 검증 대상이 아닌 사전 데이터는 Repository.save()를 통해 DB에 직접 주입하는 Fixture 방식의 헬퍼메서드(createStoreFixture 등)로 리팩토링했습니다. 결과적으로 테스트 코드의 가독성을 높이고, 대규모 통합 테스트 환경에서도 병목 없이 빠르게 검증을 마칠 수 있도록 테스트 구조를 개선했습니다.

실시간 인가(Authorization) 가드 로직 검증

  • 이전 서비스 계층에서 구현했던 validateUserRoleFromDB 로직이 실제 트랜잭션 환경에서 어떻게 동작하는지 꼼꼼하게검증했습니다. 정상적인 JWT 토큰을 발급받은 유저의 권한을 데이터베이스 상에서 강제로 변조하는 시나리오를 시뮬레이션하고, 이때 시스템이 토큰을 맹신하지 않고 DB를 조회하여 즉시 403 Forbidden 예외를 반환하는 흐름을 확인했습니다. 이를 통해 JWT의 Stateless 한계를 보완하기 위한 방어 코드가 정상적으로 동작함을 검증했습니다.

 

💡정리

  • 하이브리드 테스트 아키텍처 구축: 모든 과정을 API 호출로 처리하던 기존 방식의 성능 병목을 분석하고, 사전 준비 데이터는 Repository Fixture로, 검증 대상 로직은 MockMvc로 분리하여 통합 테스트의 실행 속도를 대폭 개선하는 테스트 가능한 구조를 구성했습니다.
  • 보안 취약점 시나리오 통합 검증: 토큰 발급 이후 실시간으로 권한이 강등되는 등의 엣지 케이스(Edge Case)를 시뮬레이션하고, 구축해 둔 다단계 권한 검증 구조가 비정상적인 접근을 사전에 차단하는 것을 실제 환경에서 안정적으로 검증했습니다.