모든 서비스에 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
아직 댓글이 없습니다.