
대규모 마이크로커머스 플랫폼에서 90% 이상의 테스트 커버리지를 달성했음에도 불구하고, 프로덕션 환경에서는 여전히 예기치 않은 버그와 장애가 발생했습니다.
특히, 주문 처리 과정에서 발생하는 데이터 검증 오류가 주요한 문제였습니다. 이로 인해 고객의 불만과 시스템의 신뢰성에 심각한 영향을 미쳤습니다.
테스트 커버리지를 높였음에도 불구하고 프로덕션 환경에서 오류가 지속적으로 발생하는 현상은 단순한 커버리지의 한계를 보여줍니다.
커버리지가 높은 테스트라도 실제 비즈니스 로직과 요구 사항을 제대로 반영하지 않으면, 시스템에서 예기치 않은 장애가 발생할 수 있습니다.

단순 라인 커버리지에만 집중한 테스트가 문제였습니다.
예를 들어, OrderService 클래스에서 단순히 null 체크만 하며 중요한 비즈니스 검증을 놓쳤습니다.
@Service
public class OrderService {
public OrderResult processOrder(OrderRequest request) {
// 단순 라인 커버리지를 위한 테스트만 존재
if (request == null) {
throw new IllegalArgumentException("Order request cannot be null");
}
// 실제 중요한 비즈니스 검증 누락
return processOrderInternal(request);
}
}
이와 같이, 비즈니스 로직의 핵심을 검증하지 않고, 코드 라인을 테스트하는 데만 집중하다 보면 실제 오류를 발견할 수 없는 경우가 발생할 수 있습니다.
주문 생성 시 컨트롤러와 서비스 계층에서 중복 검증을 하고 있지만, 중요한 검증은 빠져 있었습니다.
예를 들어, OrderController에서는 주문 금액만 검증하지만, 실제로는 재고 검증이나 사용자 상태 검증 같은 추가적인 중요한 검증이 누락되었습니다.
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
if (request.getAmount() <= 0) {
throw new BadRequestException("Invalid amount");
}
return orderService.createOrder(request);
}
}
public class OrderRequest extends SelfValidating<OrderRequest> {
@NotNull
private final String orderId;
@NotNull
@Min(value = 1000)
@Max(value = 10000000)
private final Long amount;
@NotNull
@Size(min = 1, max = 50)
private final List<OrderLineItem> items;
public OrderRequest(String orderId, Long amount, List<OrderLineItem> items) {
this.orderId = orderId;
this.amount = amount;
this.items = items;
validateSelf(); // 구문 검증
}
}
public abstract class SelfValidating<T> {
private final Validator validator;
protected SelfValidating() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validateSelf() {
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
@DomainService
public class OrderValidator {
private final InventoryService inventoryService;
private final UserService userService;
public void validateOrder(Order order) {
validateInventoryAvailability(order);
validateUserEligibility(order);
validateOrderAmount(order);
}
private void validateInventoryAvailability(Order order) {
for (OrderLineItem item : order.getItems()) {
InventoryStatus status = inventoryService.checkAvailability(item.getProductId(), item.getQuantity());
if (!status.isAvailable()) {
throw new OrderValidationException("Product is out of stock");
}
}
}
private void validateUserEligibility(Order order) {
UserStatus userStatus = userService.getUserStatus(order.getUserId());
if (userStatus.hasOutstandingPayments()) {
throw new OrderValidationException("User has outstanding payments");
}
if (!userStatus.isEligibleForAmount(order.getTotalAmount())) {
throw new OrderValidationException("Order amount exceeds user's limit");
}
}
private void validateOrderAmount(Order order) {
Money totalAmount = order.calculateTotalAmount();
if (totalAmount.isGreaterThan(Money.of(10000000))) {
throw new OrderValidationException("Order amount exceeds maximum limit");
}
}
}
@SpringBootTest
class OrderValidationTest {
@Autowired
private OrderValidator orderValidator;
@MockBean
private InventoryService inventoryService;
@MockBean
private UserService userService;
@Test
@DisplayName("재고가 부족한 경우 주문 검증 실패")
void validateOrder_InsufficientInventory_ThrowsException() {
// Given
Order order = createSampleOrder();
when(inventoryService.checkAvailability(any(), any()))
.thenReturn(InventoryStatus.outOfStock());
// When & Then
assertThrows(OrderValidationException.class, () -> orderValidator.validateOrder(order));
}
@Test
@DisplayName("사용자가 주문 한도를 초과한 경우 검증 실패")
void validateOrder_ExceedsUserLimit_ThrowsException() {
// Given
Order order = createLargeOrder();
when(inventoryService.checkAvailability(any(), any()))
.thenReturn(InventoryStatus.available());
when(userService.getUserStatus(any()))
.thenReturn(UserStatus.withLimit(Money.of(1000000)));
// When & Then
assertThrows(OrderValidationException.class, () -> orderValidator.validateOrder(order));
}
}
1. 검증 실패로 인한 장애 감소
2. 성능 영향
3. 코드 품질
점진적 도입: 신규 주문 API에 먼저 적용하고, 2주간의 모니터링 후 전체 시스템으로 확장했습니다.
레거시 시스템은 4주에 걸쳐 점진적으로 마이그레이션 했습니다.
팀 문화 변화: 테스트에서 단순한 라인 커버리지 대신 실제 비즈니스 요구를 반영하는 시나리오 기반 테스트를 강화했습니다.
코드 리뷰 시 검증 로직을 집중적으로 검토하고, 주간 장애 리뷰를 통해 검증 규칙을 지속적으로 개선했습니다.