1. 문제 상황

안녕하세요, 최근 우리 팀은 주문 API의 성능 저하 문제에 직면했었고, 그 중심에는 예외 처리 로직이 있었습니다.

1.1 직면한 문제

  • 초당 3000건 이상의 주문 요청 처리 중 약 10%가 유효성 검증 실패
  • 예외 발생 시 평균 응답 시간 300ms → 800ms로 증가
  • 주문 실패 시 상세 로깅으로 인한 시스템 부하 증가
  • 반복적인 예외 처리 코드로 인한 유지보수 어려움

2. 기존 코드의 문제점 분석

2.1 기존 예외 처리 코드

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ApiError> handleOrderNotFoundException(OrderNotFoundException ex) {
        log.error("주문을 찾을 수 없습니다: {}", ex.getMessage(), ex);  // 전체 스택트레이스 로깅
        
        ApiError error = new ApiError(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    // 다른 예외들에 대해 비슷한 패턴 반복
}

2.2 성능 병목 지점

1. 과도한 로깅

  • 전체 스택트레이스 로깅으로 인한 I/O 부하
  • 로그 파일 크기 급증

2. 반복적인 객체 생성

  • 매 예외마다 새로운 ApiError 객체 생성
  • LocalDateTime 객체 생성

3. 예외 처리 메서드의 중복

  • 비슷한 패턴의 코드가 반복됨
  • 유지보수 비용 증가

3. 최적화 솔루션

3.1 예외 처리 구조 개선

@RestControllerAdvice
@Slf4j
public class OptimizedExceptionHandler {
    
    private final ErrorResponseCache errorResponseCache;
    private final MetricService metricService;
    
    public OptimizedExceptionHandler(ErrorResponseCache errorResponseCache, 
                                   MetricService metricService) {
        this.errorResponseCache = errorResponseCache;
        this.metricService = metricService;
    }

    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ApiError> handleOrderNotFoundException(OrderNotFoundException ex) {
        // 메트릭 수집
        metricService.incrementCounter("exception.order.notfound");
        
        // 로그 레벨 최적화
        if (log.isDebugEnabled()) {
            log.debug("주문을 찾을 수 없습니다: {}", ex.getMessage());
        }
        
        // 캐시된 에러 응답 사용
        return errorResponseCache.getErrorResponse(
            ExceptionType.ORDER_NOT_FOUND,
            ex.getMessage()
        );
    }
}

@Component
public class ErrorResponseCache {
    private final Map<ExceptionType, ApiError> cache = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        // 자주 사용되는 에러 응답을 미리 생성하여 캐시
        Arrays.stream(ExceptionType.values())
              .forEach(type -> cache.put(type, createApiError(type)));
    }
    
    public ResponseEntity<ApiError> getErrorResponse(ExceptionType type, String message) {
        ApiError error = cache.get(type).clone();  // 프로토타입 패턴 적용
        error.setMessage(message);
        return new ResponseEntity<>(error, type.getStatus());
    }
}

3.2 성능 최적화 포인트

1. 로깅 최적화

  • DEBUG 레벨 조건부 로깅
  • 필요한 정보만 선택적 로깅

2. 객체 생성 최소화

  • 에러 응답 객체 캐시 도입
  • 프로토타입 패턴 적용

3. 메트릭 수집 추가

  • 예외 발생 패턴 모니터링
  • 실시간 알람 설정 가능

4. 성능 개선 결과

4.1 응답 시간 개선

  • 예외 발생 시 평균 응답 시간: 800ms → 150ms
  • 99percentile 응답 시간: 1.2s → 300ms

4.2 시스템 리소스 사용량

  • CPU 사용률: 평균 75% → 45%
  • 메모리 사용량: 최대 4GB → 2.8GB
  • 로그 파일 크기: 일 10GB → 2GB

5. 모니터링 대시보드 구현

@RestController
@RequestMapping("/api/v1/metrics")
public class MetricController {
    private final MetricService metricService;
    
    @GetMapping("/exceptions")
    public ExceptionMetrics getExceptionMetrics() {
        return metricService.getExceptionMetrics();
    }
}

@Service
public class MetricService {
    private final MeterRegistry meterRegistry;
    
    public ExceptionMetrics getExceptionMetrics() {
        return ExceptionMetrics.builder()
            .orderNotFoundCount(getCount("exception.order.notfound"))
            .orderValidationFailCount(getCount("exception.order.validation"))
            .responseTimeP99(getP99("exception.response.time"))
            .build();
    }
}

6. 실제 적용 결과 및 교훈

1. 예외는 정말 '예외적인' 상황에서만 사용

  • 비즈니스 로직상 발생 가능한 상황은 정상 플로우로 처리

2. 로깅 전략의 중요성

  • 모든 것을 로깅하는 것이 아닌, 필요한 정보만 선택적으로 로깅
  • 로그 레벨 적절히 활용

3. 캐싱 전략 수립

  • 자주 사용되는 응답은 캐시
  • 불변 객체 활용

4. 모니터링의 중요성

  • 예외 발생 패턴 분석으로 선제적 대응 가능
  • 실시간 알람으로 빠른 대응

결론

이번 최적화를 통해 대규모 트래픽 환경에서의 예외 처리가 얼마나 중요한지 깨달았습니다.
단순히 예외를 처리하는 것을 넘어, 시스템 전반의 성능에 미치는 영향을 고려한 설계가 필요합니다.
향후에는 다음과 같은 부분을 추가로 개선할 계획입니다.

  • 예외 처리 자동화 도구 개발
  • ML 기반 예외 패턴 분석
  • 마이크로서비스 환경에서의 분산 예외 추적 시스템 구축
profile
그냥 코딩할래요 재미있어요

0개의 댓글