TIL

1_2.Spring MVC와 Controller 설계, 공통 에러

꿀승 2025. 1. 9. 11:35
728x90
반응형
SMALL

학습 내용

  1. CRUD 구조와 HTTP 메서드 이해
  2. 공통 응답 처리
  3. 공통 에러 처리

학습 정리

1. CRUD 구조와 HTTP 메서드 이해

  • CRUD 동작과 HTTP 메서드 매핑

    작업 HTTP 메서드 엔드포인트 예시
    조회 (Read) GET /api/products/{id}
    생성 (Create) POST /api/products
    수정 (Update) PUT /api/products/{id}
    삭제 (Delete) DELETE /api/products/{id}
  • 예제 코드

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/products")
    public class ProductController {
    
    private final ProductService productService;
    
    @GetMapping
    public ResponseEntity<List<Product>> findProduct() {
      List<Product> products = productService.findAll();
      return ResponseEntity.ok(products);
    }
    
    @GetMapping
    public ResponseEntity<Product> findProductById(@RequestParam("id") Long id) {
      Product product = productService.findById(id);
      return ResponseEntity.ok(product);
    }
    
    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
      Product newProduct = productService.create(product);
      return ResponseEntity.ok(newProduct);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody Product product) {
      Product updateProduct = productService.update(id, product);
      return ResponseEntity.ok(updateProduct);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<String> delete(@PathVariable Long id) {
      productService.delete(id);
      return ResponseEntity.ok("삭제 완료");
    }
    }

2.공통 응답 처리

  • ApiResponse 공통 응답 클래스

    @Getter
    public class ApiResponse<T> {
    
      private final Boolean result;
      private final Error error;
      private final T message;
    
      public ApiResponse(Boolean result, String error, String errorMessage, T message) {
        this.result = result;
        this.error = Error.builder()
            .errorCode(error)
            .errorMessage(errorMessage)
            .build();
        this.message = message;
      }
    
      public static <T> ApiResponse<T> success(T result) {
        return new ApiResponse<>(true, "", "", result);
      }
      # 반환 타입이 ResponseEntity 이유는 상태코드를 지정하기 위함
      # 예외처리가 발생하면 전역예외처리가 대신 반환 하여 아래 메서드들을 호출하여 응답
      public static <T> ResponseEntity<ApiResponse<T>> ResponseException(String code, String errorMessage) {
        return ResponseEntity.ok(new ApiResponse<>(false, code, errorMessage, null));
      }
      public static <T> ResponseEntity<ApiResponse<T>> ValidException(String code, String errorMessage) {
        return ResponseEntity.status(400).body(new ApiResponse<>(false, code, errorMessage, null));
      }
    
      public static <T> ResponseEntity<ApiResponse<T>> ServerException(String code, String errorMessage) {
        return ResponseEntity.status(500)
            .body(new ApiResponse<>(false, code, errorMessage, null));
      }
    
      @Getter
      public static class Error {
    
        private final String errorCode;
        private final String errorMessage;
    
        @Builder
        public Error(String errorCode, String errorMessage) {
          this.errorCode = errorCode;
          this.errorMessage = errorMessage;
        }
      }
    }
  • 공통 응답 사용방법

    @GetMapping
    public ApiResponse<List<Product>> findProduct() {
        List<Product> products = productService.findAll();
        return ApiResponse.Success(products);
    }
  • 기존 RespnoseEntity 응답결과와 비교

    #기존RespnseEntity 시
    [
        {
            결과값
        }
    ]
    
    #ApiResponse 공통응답 사용시
    {
        "result": true,
        "error": {
            "errorCode":"string",
            "errorMessage":"string"
        },
        "message":[
            {
                결과값
            }
        ]
    }

3. 공통 에러 처리

  • 커스텀 예외 클래스

    @Getter
    public class ServiceException extends RuntimeException {
    
      private String code;
      private String message;
    
      public ServiceException() {
      }
    
      public ServiceException(ServiceExceptionCode response) {
        super(response.getMessage());
        this.code = response.getCode();
        this.message = super.getMessage();
      }
    
      @Override
      public String getMessage() {
        return message;
      }
    }
    
  • 에러코드 정의 Enum

    @Getter
    public enum ServiceExceptionCode {
    
      NOT_FOUND_USERS("NOT_FOUND_USERS", "사용자를 찾을 수 없습니다"),
      NOT_FOUND_PRODUCT("NOT_FOUND_PRODUCT", "상품을 찾을 수 없습니다"),
      NOT_FOUND_CATEGORY("NOT_FOUND_CATEGORY", "카테고리를 찾을 수 없습니다"),
      NOT_FOUND_ORDER("NOT_FOUND_ORDER", "주문내역을 찾을 수 없습니다"),
      ORDER_NOT_MODIFIABLE("ORDER_NOT_MODIFIABLE", "주문상태변경 불가합니다."),
      NOT_FOUND_REFUND("NOT_FOUND_REFUND", "환불내역을 찾을 수 없습니다"),
      REFUND_NOT_MODIFIABLE("REFUND_NOT_MODIFIABLE", "환불상태변경 불가합니다."),
      OUT_OF_STOCK_PRODUCT("OUT_OF_STOCK_PRODUCT", "재고가 부족합니다."),
      CATEGORY_DELETE_CONFLICT("CATEGORY_DELETE_CONFLICT", "해당 카테고리는 제품이 등록되어 있어 삭제가 불가능합니다."),
      ;
    
      private final String code;
      private final String message;
    
      ServiceExceptionCode(String code, String message) {
        this.code = code;
        this.message = message;
      }
    
      @Override
      public String toString() {
        return "code : " + code + ", message :" + message;
      }
    }
  • 서비스 내 커스텀 예외 사용방법

    private Product findProductById(Long id) {
     return productRepository.findById(id)
          .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT));
    }
    #예외 발생시 결과값
    {
      "reuslt": false,
      "errorCode": "NOT_FOUND_PRODUCT",
      "errorMessage": "Product를 찾을 수 없습니다.",
      "message": null
    }
  • Global Exception Handler

    @Hidden
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
      @ExceptionHandler(value = ServiceException.class)
      public ResponseEntity<?> handleResponseException(ServiceException ex) {
        return ApiResponse.ResponseException(ex.getCode(), ex.getMessage());
      }
    
      @ExceptionHandler(MethodArgumentNotValidException.class)
      public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException ex) {
        AtomicReference<String> errors = new AtomicReference<>("");
        ex.getBindingResult().getAllErrors().forEach(c -> errors.set(c.getDefaultMessage()));
    
        return ApiResponse.ValidException("VALIDATE_ERROR", String.valueOf(errors));
      }
    
      @ExceptionHandler(BindException.class)
      public ResponseEntity<?> bindException(BindException ex) {
        AtomicReference<String> errors = new AtomicReference<>("");
        ex.getBindingResult().getAllErrors().forEach(c -> errors.set(c.getDefaultMessage()));
    
        return ApiResponse.ValidException("VALIDATE_ERROR", String.valueOf(errors));
      }
    
      @ExceptionHandler(value = Exception.class)
      public ResponseEntity<?> handleException(Exception exception) {
        return ApiResponse.ServerException("SERVER_ERROR", exception.getMessage());
      }
    }
    
    • @RestControllerAdvice : @RestController에서 발생하는 예외를 전역적으로 처리
    • @ExceptionHandler 특정 예외가 발생할 때 이를 처리하기 위한 메서드 정의
    • 동작 흐름
      1. 클라이언트 REST API 요청
      2. 컨트롤러 호출
      3. 서비스에서 예외 발생
      4. Spring의 DispatcherServlet 예외 감지
      5. 예외 처리 핸들러 탐색
        • 컨트롤러 내의 @ExceptionHandler 탐색
        • 전역예외 처리 핸들러(@RestControllerAdvice) 탐색 해당 @ExceptionHandler 메서드 호출
        • 위에가 다 없으면 기본 예외처리
      6. @ExceptionHandler 메서드 실행
      7. ApiResponse의 해당 결과 값으로 응답
728x90
반응형
LIST