[스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (3)

2022. 12. 17. 13:11·공부/Spring Boot

- 사용자가 전달하는 데이터에 대한 검증을 진행해야 하는데, 따로 설정을 해주지 않는다면 NullPointerException 예외가 발생하여 서버는 500에러를 반환해줄 것이다.

- 사용자가 올바르지 않은 데이터를 통하여 요청한 것이기에 400에러를 보내야 하지만,

이를 구현하기 위해 하드 코딩이나 try-catch 구문을 사용하게 되면 유지보수와 가독성이 떨어지는 문제가 발생하였다.

- 이에 스프링 프레임워크에서 JSR-303이나 Validator 구현 클래스를 통하여 데이터를 검증 할 수 있다.

JSR-303을 사용한 데이터 검증

- Hibernate-validator 라이브러리를 사용하여 객체의 속성을 검증한다.

- 여러가지 검증 애너테이션을 사용할 수 있다.

  1. @NotNull : not null을 검사한다.

  2. @Pattern : 정규 패턴과 매칭되는지 검사한다.

  3. @Past : 과거의 날짜인지 검증한다.

  4. @Size : 배열, 맵, 컬렉션인 객체의 크기를 검증한다.

  5. @Min : 대상값이 min값 보다 크거나 같은지 검증한다.

  6. @Max : 대상값이 max값 보다 작거나 같은지 검증한다.

  7. @NotEmpty : 대상 값의 크기가 0보다 큰지 검증한다.

@Data
public class HotelRoomUpdateRequest {

    @NotNull(message = "roomType can't be null")
    private HotelRoomType roomType;

    @NotNull(message = "originalPrice can't be null")
    @Min(value = 0, message = "originalPrice must be larger than 0")
    private BigDecimal originalPrice;
}

 

- @Valid 애너테이션은 검증할 자바 빈 객체를 마킹하는 용도로 사용한다.

@PutMapping("/hotels/{hotelId}/rooms/{roomNumber}")
public ResponseEntity<HotelRoomIdResponse> updateHotelRoomByroomNumber(@PathVariable Long hotelId, @PathVariable String roomNumber,
                                                                       @Valid @RequestBody HotelRoomUpdateRequest hotelRoomUpdateRequest,
                                                                       BindingResult bindingResult) {

    if (bindingResult.hasErrors()) {
        FieldError fieldError = bindingResult.getFieldError();

        String errorMessage = new StringBuilder("validation error.")
                .append(" field : ").append(fieldError.getField())
                .append(", code : ").append(fieldError.getCode())
                .append(", message : ").append(fieldError.getDefaultMessage())
                .toString();

        log.warn(errorMessage);

        return ResponseEntity.badRequest().build();
    }

    log.info(hotelRoomUpdateRequest.toString());
    System.out.println(hotelRoomUpdateRequest.toString());
    HotelRoomIdResponse body = HotelRoomIdResponse.from(1_002_003_004L);

    return ResponseEntity.ok(body);
}

- @Valid로 지정한 RequestBody의 HotepRoomUpdateRequest 객체에서 이전에 설정한 검증 애너테이션의 정보를 토대로 검증을 진행한다.

- BindingResult 인수는 @Valid 객체의 검즘 결과를 포함한다.

- bindingReuslt.hasErrors() 메서드를 통해 검증 결과 중 틀린 것이 하나라도 있다면 에러 처리 로직으로 들어가도록 설정하고, 그 이외에는 정상적인 로직을 통해 응답하도록 설정한다.

 

Validator 인터페이스를 사용한 검증

- JSR-303에서 제공하는 애너테이션은 하나의 속성에 여러가지 애너테이션을 조합하여 검증 할 수 있지만, 여러 속성을 조합하여 검증 할 수는 없다.

- 예를들어, 호텔 예약 일정 검색 시 CheckInDate와 CheckOutDate를 받아온다고 했을 때,

CheckOutDate는 CheckInDate 보다 앞설 수 없다는 조건을 달고 싶지만 기존 애너테이션으로는 검증 할 수 없다. 그렇기에 Validator 인터페이스를 구현하여 이를 검증한다.

public class HotelRoomReserveValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return HotelRoomReserveRequest.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        HotelRoomReserveRequest request = HotelRoomReserveRequest.class.cast(target);

        if(Objects.isNull(request.getCheckInDate())) {
            errors.rejectValue("checkInDate", "NotNull", "checkInDate is Null");
            return;
        }
        if(Objects.isNull(request.getCheckOutDate())) {
            errors.rejectValue("checkOutDate", "NotNull", "checkOutDate is Null");
            return;
        }
        if(request.getCheckInDate().compareTo(request.getCheckOutDate()) >= 0) {
            errors.rejectValue("checkOutDate", "Constraint Error", "checkOutDate is earlier than checkInDate");
            return;
        }
    }
}

 

- Validator 인터페이스를 구현한 HotelRoomReserveValidator 객체를 생성한다.

- supports 메서드는 인자로 받는 객체가 검증할 객체와 같은 타입인지 검증한다.

- cast 메서드를 통해 target 인자를 HotelRoomReserveRequest 객체로 캐스팅한다.

- 그 후, 사용자 정의에 따른 error 조건과 그에 다른 rejectValue들을 설정하여 준다.

 

@RestController
@Slf4j
public class HotelRoomReserveController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.addValidators(new HotelRoomReserveValidator());
    }

    @PostMapping("/hotels/{hotelId}/rooms/{roomNumber}/reserve")
    public ResponseEntity<HotelRoomIdResponse> reserveHotelRoomByRoomNumber(@PathVariable Long hotelId, @PathVariable String roomNumber,
                                                                            @Valid @RequestBody HotelRoomReserveRequest hotelRoomReserveRequest,
                                                                            BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();

            String errorMessage = new StringBuilder("validation error.")
                    .append(" field : ").append(fieldError.getField())
                    .append(", code : ").append(fieldError.getCode())
                    .append(", message : ").append(fieldError.getDefaultMessage())
                    .toString();

            log.warn(errorMessage);

            return ResponseEntity.badRequest().build();
        }

        // Reserve Service 로직

        Long reservationId = IdGenerator.create();
        HotelRoomIdResponse body = HotelRoomIdResponse.from(reservationId);

        return ResponseEntity.ok(body);
    }
}

 

- @InitBinder의 addValidators 메서드를 통해 내가 구현한 HotelRoomReserveValidator를 등록한다.

- @Valid로 지정한 RequestBody의 HotelRoomReserveRequest 객체에서 등록한 Validator 인터페이스를 통하여 검증한다.

- 그 이후의 로직은 기존 JSR-303 에러처리 로직과 같다.

 

ExceptionHandler 예외 처리

- java에서 제공하는 예외는 java.lang.Exception 클래스를 상속받는 예외이고, 또 하나는 java.lang.RuntimeException을 상속받는 예외이다.

- Exception 클래스를 상속 받는 예외는, 처리하지 않으면 컴파일 에러가 발생하지만 RuntimeException 클래스를 상속 받는 예외들은 어플리케이션 실행 중 발생하는 예외들이다.

- 스프링에서 상속받는 대부분의 Exception들은 RuntimeException 클래스를 상속받는다.

- 이 책에서 다루는 런타임에러에 관한 처리로직은 다음과 같다.

[Http Client의 요청] -> [Dispatcher Servlet의 컨트롤러 매핑] => [Controller에서 서비스 로직 호출] -> [Service 로직 처리 중 예외 발생] -> [사용자가 정의한 ApiExceptionHandler에서 예외 처리] -> [예외처리 결과를 Dispatcher Servlet에 전달] -> [Dispatcher Servlet은 처리 결과를 Client에게 응답]

 

@RestControllerAdvice
@Slf4j
public class ApiExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ErrorResponse> handlerBadRequestException(BadRequestException ex) {
      log.error("Error message : {}", ex.getErrorMessage());

      return new ResponseEntity<>(new ErrorResponse(ex.getErrorMessage()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        log.error("Error message : {}", ex.getMessage());

        return new ResponseEntity<>(new ErrorResponse("System Error"), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
@Data
public class BadRequestException extends RuntimeException{
    private String errorMessage;

    public BadRequestException(String errorMessage) {
        super();
        this.errorMessage = errorMessage;
    }
}
@Service
@Slf4j
public class ReserveService {

    public Long reserveHotlRoom(Long hotelId, String roomNumber, LocalDate checkInDate, LocalDate checkOutDate) {

//        hotelRoomRepository.findByHotelIdAndRoomNumber(hotelId, roomNumber)
//                .orElseThrow(() -> {
//                    log.error("Invalid roomNumber. hotelId: {}, roomNumber: {}", hotelId, roomNumber);
//                    return new BadRequestException("Not existing roomNumber");
//                });
        return IdGenerator.create();
    }
}

 

 

- 위와 같이 Service 로직을 처리할 때 생기는 RuntimeException에 대하여 사용자가 생성한 BadRequestException을 통하여 처리하도록 지정하였다.

- BadRequestException은 java.lang의 RuntimeException을 상속한다.

- @RestControllerAdvice 애너테이션을 통하여 스프링 애플리케이션에서 발생하는 전체 예외 처리 메서드를 선언한다.

- @ExceptionHanlder를 통해 사용자가 구현한 Exception 객체를 등록하고, 이에 해당하는 응답객체를 DispatcherServlet에 응답할 수 있도록 설정해 준다.

 

미디어 컨텐츠 처리하기

- 애플리케이션 서버에서 이미지 등 미디어를 처리하기 위해서는 HttpMessageConverter을 사용하여 메세지를 변환하거나, 서블릿 객체인 javax.servlet.http.HttpServletResponse를 사용하여 직접 OutputStream을 다루는 방법이 있다.

1.  HttpMessageConverter 사용

@GetMapping("/hotels/{hotelId}/rooms/{roomNumber}/reservations/{reservationId}")
public ResponseEntity<byte[]> getInvoice(@PathVariable Long hotelId, @PathVariable String roomNumber, @PathVariable Long reservationId) throws IOException {
    String filePath = "pdf/hotel_invoice.pdf";
    try(InputStream inputStream = new ClassPathResource(filePath).getInputStream()) {
        byte[] bytes = StreamUtils.copyToByteArray(inputStream);

        return new ResponseEntity<>(bytes, HttpStatus.OK);
    } catch (Throwable th) {
        th.printStackTrace();
        throw new FileDownloadException("file download error");
    }
}

 

- filePath는 resource 폴더에 저장된 pdf 파일의 경로이다.

- pdf 파일을 byte 배열로 변경 후, ResponseEntity에 byte 배열을 담아 리턴한다.

 

2. 서블릿 객체 HttpServletResponse 사용

@GetMapping(value = "/hotels/{hotelId}/rooms/{roomNumber}/reservations/{reservationId}", produces = "application/pdf")
public void downloadInvoice(@PathVariable Long hotelId, @PathVariable String roomNumber, @PathVariable Long reservationId, HttpServletResponse response) {
    String filePath = "pdf/hotel_invoice.pdf";
    try(InputStream is = new ClassPathResource(filePath).getInputStream(); OutputStream os = response.getOutputStream();) {
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_PDF_VALUE);
        response.setHeader("Content-Disposition", "finename=hotel_invoice.pdf");

        StreamUtils.copy(is, os);
    } catch (Throwable th) {
        th.printStackTrace();
        throw new FileDownloadException("file download error");
    }
}

 

- @GetMapping 메서드 인자로 HttpServletResponse 인자를 선언하면, 런타임 과정에서 생성된 HttpServletResponse 객체를 주입한다.

- 이 객체를 이용하여, HttpSatus의 상태코드와 헤더를 설정 할 수 있다.

- response.getOutputStream()을 통해 pdf의 byte 형식으로 쓸 수 있다.

저작자표시 비영리 변경금지 (새창열림)

'공부 > Spring Boot' 카테고리의 다른 글

[스프링 부트 공부 일지] 4. 웹 어플리케이션 서버 구축하기 (2)  (0) 2022.12.22
[스프링 부트 공부 일지] 4. 웹 어플리케이션 서버 구축하기 (1)  (0) 2022.12.21
[스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (2)  (0) 2022.12.16
[스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (1)  (0) 2022.12.16
[스프링 부트 공부 일지] 2. 스프링 웹 MVC  (0) 2022.12.15
'공부/Spring Boot' 카테고리의 다른 글
  • [스프링 부트 공부 일지] 4. 웹 어플리케이션 서버 구축하기 (2)
  • [스프링 부트 공부 일지] 4. 웹 어플리케이션 서버 구축하기 (1)
  • [스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (2)
  • [스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (1)
뚝딱뚝딱2
뚝딱뚝딱2
  • 뚝딱뚝딱2
    개발도상국
    뚝딱뚝딱2
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 공부
        • Java
        • Spring Boot
        • LORA
      • Web
        • 인스타 클론 코딩
        • GPT 응답 API 서버
        • Spring Boot 예외 처리
        • 코테 준비용 서비스 만들기
      • DevOps
        • 쿠버네티스
        • 서버 만들기
      • 코딩테스트
        • 알고리즘
      • 교육
        • 스파르타코딩클럽 - 내일배움단
        • 혼자 공부하는 컴퓨터 구조 운영체제
      • 잡다한것
  • 블로그 메뉴

    • 홈
  • 링크

    • GITHUB
  • 공지사항

  • 인기 글

  • 태그

    Entity
    REST API
    mapstruct
    오블완
    스프링 부트
    티스토리챌린지
    인스타그램
    chat GPT
    OpenAI API
    예외
    react
    spring boot
    Java
    클러스터
    쿠버네티스
    스프링부트
    클론코딩
    리액트
    MSA
    백준
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
뚝딱뚝딱2
[스프링 부트 공부 일지] 3. 스프링에서 REST API 개발하기 (3)
상단으로

티스토리툴바