-
공통 응답 처리와 예외 핸들링이커머스 devops/국비교육 2025. 11. 16. 11:57
일관된 API를 위한 공통 응답 객체 설계
- 예측 가능성 : 모든 API 응답이 동일한 구조를 가지므로 클라이언트 개발자가 결과를 예측하고 처리하기 쉬움
- 유연한 확장 : 공통 포맷 안에 서버 상태, 페이징 정보 등 추가적인 메타데이터를 담기 용이하다
ApiResponse<T> 설계 예시
- 모든 응답을 감싸는 제네릭 클래스 설계
- 성공 시에는 data 필드에, 실패 시에는 error 필드에 정보를 담는다
@Getter @Builder @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class ApiResponse<T> { Boolean result; Error error; T message; public static <T> ApiResponse<T> success() { return success(null); } public static <T> ApiResponse<T> success(T message) { return ApiResponse.<T>builder() .result(true) .message(message) .build(); } public static <T> ResponseEntity<ApiResponse<T>> error(String code, String errorMessage) { return ResponseEntity.ok(ApiResponse.<T>builder() .result(false) .error(Error.of(code, errorMessage)) .build()); } public static <T> ResponseEntity<ApiResponse<T>> badRequest(String code, String errorMessage) { return ResponseEntity.badRequest().body(ApiResponse.<T>builder() .result(false) .error(Error.of(code, errorMessage)) .build()); } public static <T> ResponseEntity<ApiResponse<T>> serverError(String code, String errorMessage) { return ResponseEntity.status(500).body(ApiResponse.<T>builder() .result(false) .error(Error.of(code, errorMessage)) .build()); } @JsonInclude(JsonInclude.Include.NON_NULL) public record Error(String errorCode, String errorMessage) { public static Error of(String errorCode, String errorMessage) { return new Error(errorCode, errorMessage); } } }// Controller 적용 예시 // 단일 상품 조회 @GetMapping("/{id}") public ApiResponse<ProductResponse> getById(@PathVariable Long id) { return ApiResponse.success(productService.getById(id)); } // 상품 삭제 @DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id) { productService.delete(id); return ApiResponse.success(); }예측 가능한 오류 처리를 위한 전역 예외 핸들링
- 애플리케이션의 모든 예외를 한 곳에서 중앙 처리하여 일관되고 정제된 오류 메시지를 클라이언트에게 반환한다
1. 커스텀 예외와 에러 코드 정의
- 애플리케이션에서 발생할 수 있는 비즈니스 예외 상황을 명확하게 정의한다
- ServiceExceptionCode Enum : 오류 코드와 메시지를 한 곳에서 관리한다
@Getter @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public enum ServiceExceptionCode { NOT_FOUND_PRODUCT("상품을 찾을 수 없습니다."), INSUFFICIENT_STOCK("상품의 재고가 부족합니다."); // ... 다른 예외 코드들 final String message; }- ServiceException 클래스 : 비즈니스 로직에서 발생하는 예외를 위한 커스텀 클래스
@Getter @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class ServiceException extends RuntimeException { String code; String message; public ServiceException(ServiceExceptionCode response) { super(response.getMessage()); this.code = response.name(); this.message = super.getMessage(); } @Override public String getMessage() { return message; } }2. @RestControllerAdvice 전역 예외 처리기 구현
- 모든 Controller에서 발생하는 예외를 가로채 처리하는 클래스를 만든다
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@Hidden @RestControllerAdvice public class GlobalExceptionHandler { private final String VALIDATE_ERROR = "VALIDATE_ERROR"; private final String SERVER_ERROR = "SERVER_ERROR"; @ExceptionHandler(ServiceException.class) public ResponseEntity<?> handleResponseException(ServiceException ex) { // 비즈니스 로직에서 직접 던진 커스텀 예외 처리 return ApiResponse.error(ex.getCode(), ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException ex) { // @Valid 또는 @Validated가 붙은 DTO 검증 실패 시 발생 AtomicReference<String> errors = new AtomicReference<>(""); ex.getBindingResult().getAllErrors().forEach(c -> errors.set(c.getDefaultMessage())); return ApiResponse.badRequest(VALIDATE_ERROR, String.valueOf(errors)); } @ExceptionHandler(BindException.class) public ResponseEntity<?> bindException(BindException ex) { // @RequestParam, @ModelAttribute 바인딩 시 검증 실패했을 때 // (JSON → DTO 변환이 아닌 폼데이터 / 쿼리파라미터 검증 실패) AtomicReference<String> errors = new AtomicReference<>(""); ex.getBindingResult().getAllErrors().forEach(c -> errors.set(c.getDefaultMessage())); return ApiResponse.badRequest(VALIDATE_ERROR, String.valueOf(errors)); } @ExceptionHandler(Exception.class) public ResponseEntity<?> serverException(Exception ex) { // 잡히지 않은 모든 예외의 마지막 방어 return ApiResponse.serverError(SERVER_ERROR, ex.getMessage()); } }cf. methodArgumentNotValidException & bindException
- 체크 예외 계열로 스프링 MVC가 컨트롤러 메서드 호출 전 던진다
- 명시적으로 잡지 않으면 스프링 기본 리졸버가 HTTP 400으로 변환한다
3. 서비스 로직에서 예외 발생
- 서비스 코드에서 try-catch 없이, 필요한 상황에 직접 정의한 예외를 발생시키면 된다
public ProductResponse getById(Long id) { Product product = productRepository.findById(id) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT)); return ProductResponse.builder() .id(product.getId()) .categoryId(product.getCategoryId()) .name(product.getName()) .description(product.getDescription()) .price(product.getPrice()) .stock(product.getStock()) .createdAt(product.getCreatedAt()) .build(); }{ "result": false, "data": null, "error": { "code": "NOT_FOUND_PRODUCT", "message": "상품을 찾을 수 없습니다." } }cf. 성공 응답 코드 상황별 반환
- 200 Ok : 요청이 성공적으로 처리 (주로 GET, PUT 응답)
- 201 Created : 리소스가 성공적으로 생성(POST 응답), 응답 헤더의 Location에 생성된 리소스의 URL 포함
- 204 No Content : 요청은 성공했지만 응답으로 보낼 데이터 없음 (주로 DELETE 성공 응답)
// 200 OK @GetMapping("/books/{id}") public ResponseEntity<ApiResponse<BookResponse>> getBook(@PathVariable Long id) { BookResponse book = bookService.find(id); return ResponseEntity.ok(ApiResponse.success(book)); } // 201 Created @PostMapping("/books") public ResponseEntity<ApiResponse<BookResponse>> create(@RequestBody @Valid BookRequest request) { BookResponse created = bookService.save(request); URI location = URI.create("/books/" + created.getId()); return ResponseEntity.created(location) .body(ApiResponse.success(created)); } // 204 No Content @DeleteMapping("/books/{id}") public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) { bookService.delete(id); return ResponseEntity.noContent().build(); }- 표준 REST 규약 준수
- 클라이언트 해석 명확
- Swagger/문서화 깔끔
- 불필요한 body 제거 : 204는 body 없음, 네트워크 비용 절약
- 테스트/예외처리 용이 : status code 기준으로 테스트케이스 분기 가능
728x90'이커머스 devops > 국비교육' 카테고리의 다른 글
QueryDSL 기본 문법 (0) 2025.11.19 영속성 컨텍스트와 트랜잭션 & QueryDSL (0) 2025.11.16 RESTful API 설계하기 (0) 2025.11.16 ORM 심화 및 데이터 로딩 전략 (0) 2025.11.15 프로젝트 기본 설정 (0) 2025.11.15