News

서비스마다 try-catch 금지 — Global Handler #shorts #SAP #CAP

▶ YouTube에서 보기

모든 서비스에 try-catch를 반복하는 문제

CAP for Java 애플리케이션에서 각 서비스 핸들러마다 try-catch 블록을 반복 작성하는 패턴은 코드 중복과 일관성 문제를 유발합니다. 서비스가 20개면 예외 처리 로직도 20곳에 분산됩니다. Spring의 @ControllerAdvice와 CAP의 이벤트 핸들러를 결합해 전역 예외 처리기를 구성하면 이 문제를 해결할 수 있습니다.

반복 try-catch — 잘못된 패턴

// 각 서비스 핸들러마다 동일한 예외 처리 반복
@Component
@ServiceName("OrdersService")
public class OrdersServiceHandler implements EventHandler {

    @On(event = CqnService.EVENT_READ, entity = Orders_.CDS_NAME)
    public void readOrders(CdsReadEventContext ctx) {
        try {
            // 비즈니스 로직
            var result = fetchOrders(ctx);
            ctx.setResult(result);
        } catch (DataAccessException e) {
            throw new ServiceException(ErrorStatuses.INTERNAL_SERVER_ERROR,
                "DB 조회 오류: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            throw new ServiceException(ErrorStatuses.BAD_REQUEST,
                "잘못된 요청: " + e.getMessage());
        }
        // 모든 핸들러마다 이 패턴 반복
    }
}

전역 예외 처리기 — Spring @ControllerAdvice 활용

// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // Spring Data 예외를 CAP ServiceException으로 변환
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<Object> handleDataAccessException(
            DataAccessException ex, WebRequest request) {

        log.error("DB 접근 오류: {}", ex.getMessage(), ex);
        var problem = Problem.builder()
            .withStatus(Status.INTERNAL_SERVER_ERROR)
            .withTitle("데이터베이스 오류")
            .withDetail("데이터 처리 중 오류가 발생했습니다.")
            .build();
        return handleExceptionInternal(ex, problem,
            new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
    }

    // 입력값 검증 오류
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Object> handleValidationException(
            ConstraintViolationException ex, WebRequest request) {

        String violations = ex.getConstraintViolations().stream()
            .map(v -> v.getPropertyPath() + ": " + v.getMessage())
            .collect(Collectors.joining(", "));

        log.warn("입력 검증 실패: {}", violations);
        var problem = Problem.builder()
            .withStatus(Status.BAD_REQUEST)
            .withTitle("입력값 오류")
            .withDetail(violations)
            .build();
        return handleExceptionInternal(ex, problem,
            new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
    }

    // CAP ServiceException은 그대로 통과
    @ExceptionHandler(ServiceException.class)
    public ResponseEntity<Object> handleServiceException(
            ServiceException ex, WebRequest request) {

        log.warn("비즈니스 오류: {}", ex.getMessage());
        // CAP이 알아서 OData 에러 응답으로 변환
        throw ex;
    }

    // 예상치 못한 모든 예외
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleGenericException(
            Exception ex, WebRequest request) {

        log.error("예상치 못한 오류: {}", ex.getMessage(), ex);
        var problem = Problem.builder()
            .withStatus(Status.INTERNAL_SERVER_ERROR)
            .withTitle("서버 오류")
            .withDetail("요청을 처리할 수 없습니다. 잠시 후 다시 시도하세요.")
            .build();
        return handleExceptionInternal(ex, problem,
            new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}

CAP 이벤트 핸들러 전역 Before 처리

// CAP 이벤트 레벨 전역 핸들러
@Component
public class GlobalBeforeHandler implements EventHandler {

    @Before(event = "*", service = "*")
    public void beforeAllEvents(EventContext ctx) {
        // 모든 서비스, 모든 이벤트 전에 실행
        String tenant = ctx.getCdsRuntime().getProvidedBy()
            .map(p -> p.getTenant()).orElse("unknown");

        // MDC로 로그에 컨텍스트 추가
        MDC.put("tenant", tenant);
        MDC.put("event", ctx.getEvent());
        MDC.put("entity", ctx.getTarget().getQualifiedName());
    }

    @After(event = "*", service = "*")
    public void afterAllEvents(EventContext ctx) {
        MDC.clear();
    }
}

사용자 정의 비즈니스 예외 클래스

// 도메인별 예외 클래스
public class InsufficientStockException extends ServiceException {

    public InsufficientStockException(String productId, int requested, int available) {
        super(ErrorStatuses.CONFLICT,
            String.format("상품 %s 재고 부족: 요청 %d개, 재고 %d개",
                productId, requested, available));
    }
}

public class DuplicateOrderException extends ServiceException {

    public DuplicateOrderException(String orderId) {
        super(ErrorStatuses.CONFLICT,
            String.format("주문 %s가 이미 존재합니다.", orderId));
    }
}

// 서비스 핸들러에서 사용
@On(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void createOrder(CdsCreateEventContext ctx) {
    var order = ctx.getCqn().entries().get(0);
    String orderId = order.get("OrderId").toString();

    if (orderExists(orderId)) {
        throw new DuplicateOrderException(orderId);  // 전역 핸들러가 처리
    }

    // 재고 확인
    int available = getStock(order.get("ProductId").toString());
    int requested = ((Number) order.get("Quantity")).intValue();
    if (available < requested) {
        throw new InsufficientStockException(
            order.get("ProductId").toString(), requested, available
        );
    }
    // 비즈니스 로직만 집중
}

핵심 정리

  • @ControllerAdvice로 예외 타입별 처리 로직을 한 곳에 집중
  • 도메인별 예외 클래스로 의미 있는 오류 메시지 제공
  • 서비스 핸들러는 비즈니스 로직만 담당, 예외 처리는 위임
  • CAP @Before/@After 와일드카드로 공통 처리(로깅, MDC) 구현

공식 문서

CAP Java 예외 처리는 cap.cloud.sap/docs/java/event-handlers/exceptions를 참고하세요.

댓글 0

아직 댓글이 없습니다.