ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RESTful API
    Backend 2025. 11. 21. 07:47
    RESTful API 핵심 5줄 요약
    1. URL은 리소스 (명사), 동사 금지
    2. 행위는 HTTP 메서드
    3. 생성은 POST / 조회는 GET / 수정은 PATCH / 삭제는 DELETE
    4. 응답 코드는 200·201·204·400·404·409 잘 쓰기
    5. 응답은 DTO로 통일하고 에러는 공통 구조
    리소스 네이밍 규칙

     

    REST API에서는 기능 기준이 아니라 리소스 기준으로 URL을 설계한다.
    URL에는 동사를 사용하지 않고 명사 복수형을 사용한다.

     

    주문 기능 → orders
    상점 기능 → stores
    메뉴 기능 → menus
    쿠폰 기능 → coupons
    라이더/배달기사 → riders

     

    예시

    • 주문 생성
      • POST /orders
      • Body: 주문 항목, 상점 ID, 주소 등
      • 응답: 생성된 주문 정보 + 201 Created
    • 주문 상세 조회 

    GET /orders/{orderId}

    • 주문 목록 조회 (필터 + 페이징)
    GET /orders?status=DELIVERED&from=2025-11-01&to=2025-11-30&page=0&size=20
    • 주문 상태 변경 (부분 업데이트)  

    PATCH /orders/{orderId}

    Body: { "status": "CANCELLED" }

    • 상점 목록 조회 (카테고리/정렬) 
    GET /stores?category=CHICKEN&sort=rating,desc&page=0&size=20
    • 상점의 메뉴 목록 조회
    GET /stores/{storeId}/menus
    • 쿠폰 적용 시 예상 결제 금액 조회
    POST /orders/price-preview       
    Body: 주문 항목 + 쿠폰 정보
    응답: { "originalPrice": 23000, "discountedPrice": 18000 }
    • 특정 상점의 리뷰 목록 조회

    GET /stores/{storeId}/reviews?page=0&size=10

    • 리뷰 작성
      • 주문 기준으로 리뷰 작성

    POST /orders/{orderId}/reviews

    • 관리자용 주문 검색
    GET /admin/orders?storeId=1&status=PAID&from=...&to=...&page=0&size=50

    → 전부 복수형 명사 + 쿼리 파라미터로 필터 + 동사 없는 URL = RESTful한 패턴.

     

    예외 처리 기준

     

    | IllegalArgumentException 사용하는 경우

    도메인 객체 내부에서 순수 자바 로직 검증 시 사용한다.

    즉, API와 무관한 순수 Java 예외 IllegalArgumentException을 사용한다.

    예시

    • Money 객체 생성 시 음수 금지
    • Quantity 객체 생성 시 0 이하 금지
    • Value Object 내부 유효성 검증
    public class Quantity {
    
        public Quantity(int value) {
            if (value <= 0) {
                throw new IllegalArgumentException("Quantity must be > 0");
            }
            this.value = value;
        }
    }

     

     

    | ApiException 사용하는 경우

    API 요청 검증이나 비즈니스 룰 위반 시 사용한다.

    예시

    • 존재하지 않는 주문 ID
    • 잘못된 주문 상태 변경
    • 잘못된 enum 값
    • 비즈니스 정책 위반
     
    throw new ApiException(ErrorCode.ORDER_NOT_FOUND);

     

    REST API에서는 에러 코드, 메시지, HTTP 상태를 함께 전달해야 한다.

    {
      "code": "ORDER_NOT_FOUND",
      "message": "존재하지 않는 주문입니다.",
      "status": 404
    }

     

    | 계층별 예외 

    도메인 계층
    → IllegalArgumentException 사용
    → HTTP와 무관한 순수 Java 검증

    서비스 계층
    → ApiException 사용
    → 비즈니스 로직 검증

    컨트롤러 계층
    → ApiException 사용
    → 클라이언트 응답 제어

     

    Validation 

    DTO 검증 → @Valid 사용

    DTO 내부 필드 검증에 사용한다.

    리스트 내부 객체까지 검증하려면 List에도 붙인다. 

     
    public ResponseEntity<?> createOrder(
            @Valid @RequestBody CreateOrderRequest request
    )
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public class CreateOrderRequest {
    
        @NotNull(message = "storeId는 필수 값입니다.")
        private Long storeId;
    
        @Size(min = 1, message = "주문 항목은 최소 1개 이상이어야 합니다.")
        @Valid
        private List<OrderItemRequest> orderItems;
    }

     

    메서드 파라미터 검증 → @Validated 사용

    @PathVariable, @RequestParam 검증, 서비스 메서드에서도 사용한다.

    @Validated는 반드시 클래스 레벨에 붙여야 동작한다.

    @RestController
    @Validated
    public class CouponController {
    
        @GetMapping("/coupons/{id}")
        public ResponseEntity<?> getCoupon(
                @PathVariable @Min(1) Long id
        ) {
            return null;
        }
    }

     

     

    | @Valid vs @Validated

    DTO 필드 검증 → @Valid 사용

    DTO 내부 리스트 검증 → @Valid 사용

    @PathVariable 검증 → @Validated 사용

    @RequestParam 검증 → @Validated 사용

    검증 그룹 사용 → @Validated 사용

    서비스 메서드 → @Validated 사용

     

    검증 그룹? 상황에 따라 검증 조건을 다르게 적용하는 기능이다.

        예시

        회원 생성 - 이메일 필수, 비밀번호 필수, ID 없음

     

        회원 수정 - 이메일 선택, 비밀번호 선택, ID 필수

    //검증그룹 만들기
    public interface CreateGroup {}
    public interface UpdateGroup {}
    
    public class UserRequest {
    
        @NotNull(groups = UpdateGroup.class)
        private Long id;
    
        @NotBlank(groups = CreateGroup.class)
        private String email;
    
        @NotBlank(groups = CreateGroup.class)
        private String password;
    }
    
    //컨트롤러에서 회원 등록,수정 
    @PostMapping
    public void createUser(
            @Validated(CreateGroup.class)
            @RequestBody UserRequest request
    ) {
    }
    
    @PatchMapping
    public void updateUser(
            @Validated(UpdateGroup.class)
            @RequestBody UserRequest request
    ) {
    }

     

    서비스 메서드 → @Validated 사용

    @Service
    @Validated   // 여기 
    public class OrderService {
    
        public void createOrder(@Valid OrderCreateCommand command) {
        }
    }
    
    public class OrderCreateCommand {
    
        @NotNull
        private Long storeId;
        
        @Min(1)
        private int quantity;
    }

     

    | @PathVariable

    @PathVariable은 URL 경로에 포함된 값을 변수로 추출한다.

    예시

    GET /orders/10

     

    @GetMapping("/orders/{orderId}")
    public OrderResponse getOrder(@PathVariable Long orderId) {
    }

     

    | @PathVariable vs @RequestParam

    @PathVariable
    → URL 경로 값
    → 리소스 식별에 사용

    /users/3
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id)

     

    @RequestParam
    → Query String 값
    → 검색 조건이나 필터에 사용

    /users?page=2&size=20
    @GetMapping("/users")
    public User list(@RequestParam int page,
                     @RequestParam int size)

     

    글로벌 예외 처리 
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(ApiException.class)
        public ResponseEntity<ErrorResponse> handleApiException(ApiException e) {
            ErrorCode error = e.getErrorCode();
            return ResponseEntity
                    .status(error.getStatus())
                    .body(new ErrorResponse(error.getCode(), error.getMessage()));
        }
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<ErrorResponse> handleValidException(MethodArgumentNotValidException ex) {
            String message = ex.getBindingResult()
                    .getFieldErrors()
                    .get(0)
                    .getDefaultMessage();
    
            ErrorResponse body = new ErrorResponse("INVALID_INPUT", message);
            return ResponseEntity.badRequest().body(body);
        }
    
        @ExceptionHandler(ConstraintViolationException.class)
        public ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
            String message = ex.getConstraintViolations()
                    .iterator()
                    .next()
                    .getMessage();
    
            ErrorResponse body = new ErrorResponse("INVALID_INPUT", message);
            return ResponseEntity.badRequest().body(body);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ErrorResponse> handleException(Exception ex) {
            ErrorResponse body =
                    new ErrorResponse("INTERNAL_SERVER_ERROR", "알 수 없는 오류가 발생했습니다.");
    
            return ResponseEntity.internalServerError().body(body);
        }
    }

     

    HTTP 에러 코드 정리
    코드   상황  예시
    400 Bad Request 요청 값 자체가 잘못된 경우 사용한다. 필수 값 누락
    유효성 실패
    잘못된 enum 값
    JSON 파싱 실패
    404 Not Found 요청한 리소스가 존재하지 않을 때 사용한다. 존재하지 않는 주문 조회
    삭제된 쿠폰 조회
    존재하지 않는 사용자 조회
    409 Conflict 리소스 상태 충돌 시 사용한다. 이미 취소된 주문 재취소
    이미 사용된 쿠폰 재사용
    중복 회원 가입
    상태 변경 충돌
    500 Internal Server Error 서버 내부 오류 발생 시 사용한다. 코드 버그
    NullPointerException
    DB 장애
    예상하지 못한 시스템 오류

     

Designed by Tistory.