스프링 부트로 개발하는 MSA 컴포넌트 4 (책정리)

YongHyun·2023년 1월 25일
0
post-thumbnail

스프링 MVC를 이용한 REST-API 개발

스프링 MVC 프레임워크는 HTTP 프로토콜을 쉽게 사용할 수 있는 애너테이션들과 클래스들을 제공한다. 그래서 개발자는 HTTP프로토콜 위에서 동작하는 REST-API를 보다 쉽게 개발할 수 있다.
이 장에서는 REST-API를 개발하는 방법을 정리해 보았다.

REST-API : GET, DELETE 메서드 매핑

REST-API에서 GET 메서드는 서버에 데이터를 조회, DELETE 메서드는 삭제하는 용도로 쓰인다.
이 때 두 HTTP 메서드는 클라이언트 요청 메시지 바디에 데이터가 없는 공통점이 있기 때문에
URI에 리소스 데이터를 포함하거나 파라미터를 사용하여 키/값으로 구성된 데이터를 전송하는 방법 또는 HTTP 헤더에 데이터를 설정하는 총 세 가지 방법이 있다.

예시

호텔 정보를 조회하는 REST-API를 요청하는 과정

GET /hotels/{hotelId}/rooms/{roomNumber}?fromDate={yyyyMMdd}&toDate={yyyyMMdd}

  • hotelId : (필수) Long 타입이며, 호텔의 고유 아이디 값
  • roomNumber : (필수) String 타입이며, 호텔 리소스에 포함된 룸 중 고유한 아이디 값
  • fromDate, toDate : (선택) String 타입이며, yyyyMMdd 형식의 예약일

먼저 HotelController 클래스는 사용자 요청을 처리하기 위해서 스프링 빈으로 설정한다.

위의 코드처럼

@RestController나 @Controller 애너테이션이 정의된 클래스를 컨트롤러 클래스라고 한다.

@GetMapping 애너테이션이 정의된 메서드는 사용자 요청을 처리하는 핸들러 메서드가 된다. 이를 매핑 애너테이션이라고 한다.

이렇게 설정된 매핑 애너테이션은 스프링 MVC 프레임워크의 RequestMappingHandlerMapping 컴포넌트가 분석하여 사용자 요청과 핸들러를 매핑하는 정보를 관리한다.

참고로 RequestMappingHandlerMapping은 DispatcherServlet이 클라이언트 요청과 매칭되는 핸들러 메서드를 조회할 때 사용한다.

@PathVariable 애너테이션은 사용자가 요청한 REST-API의 URI에서 특정 위치에 있는 데이터를 얻기 위해서 사용한다.

'/hotels/{hotelId}/rooms/{roomNumber}' path를 예시로 들면 hotelId와 roomNumber 두 개의 값을 주입받으려면 두 개의 @PathVariable 애너테이션을 핸들러 메서드 인자에 설정하면 된다.

@RequestParam 애너테이션은 설정한 파라미터의 이름과 매칭되는 값을 핸들러 메서드의 인자에 주입하는 기능을 제공한다.

'/hotels/123/rooms/West-Wing-3928?fromDate=20201224&toDate=20201230'를 예시로 들면 fromDate나 toDate 같은 파라미터는 필수가 아닌 선택 값이기 때문에 required 속성을 false로 설정한다. 추가로 @DateTimeFormat 애너테이션을 같이 사용하여서 문자열 값을 날짜 객체로 변환시켜준다.

@RequestHeader 애너테이션은 HTTP 헤더를 사용하여 클라이언트에서 데이터를 받을 때 사용한다.

이렇게 클라이언트에서 요청한 REST-API는 다음과 같이 응답해준다.

호텔 객실을 삭제하는 REST-API를 요청하는 과정

DELETE /hotels/{hotelId}/rooms/{roomNumber}

  • hotelId : (필수) Long 타입이며, 호텔의 고유 아이디 값
  • roomNumber : (필수) String 타입이며, 호텔 리소스에 포함된 룸 중 고유한 아이디 값

@PathVariable 애너테이션을 사용하여 hotelId, roomNumber 데이터를 가져와서 콘솔에 출력 후 DeleteResultResponse 객체를 반환한다.

REST-API : 응답 메시지 처리

서버에서 클라이언트로 데이터를 전달할때는 별도의 DTO 클래스를 만들어 사용하는 것을 지양한다.

DTO(Data Transfer Object) : 계층 간 데이터 전송을 위해 사용되는 객체

엔티티 객체나 값 객체가 DTO의 기능을 공통으로 사용하게 되면 여러 애너테이션이 섞이게 되어서 클래스의 내부 코드는 복잡해진다.

'REST-API : GET, DELETE 메서드 매핑' 에서 사용한 코드에 있는 HotelRoomController에 getHotelRoomByPeriod 메서드를 살펴보면 리턴하는 객체가 HotelRoomResponse이다.
여기서 HotelRoomResponse 클래스가 DTO 역할을 한다.

스프링 MVC 프레임워크 내부에는 여러 개의 HttpMessageConverter 객체를 포함하고 있어서 변환할 대상 객체의 클래스 타입과 MediaType에 따라 특정 HttpMessageConverter가 선택되어 변환한다.

그 중에 MappingJackson2HttpMessageConverter 구현체가 JSON 메시지로 변환시켜주는데 Jackson 라이브러리의 ObjectMapper 객체가 MappingJackson2HttpMessageConverter의 속성으로 포함 되어 있다.

@JsonProperty :

클래스의 맴버 변수 이름을 JSON 객체의 속성 이름으로 지정할 때 사용한다.

@JsonSerialize :

객체의 속성 값을 변환하는 과정에서 적절한 형태의 데이터로 변경할 때 사용한다.

REST-API POST, PUT 매핑

POST, PUT 메서드는 각 각 서버에 데이터를 생성, 서버에 데이터를 수정하는 REST-API이다.
두 메서드는 클라이언트에서 요청 메시지를 전송할 때 JSON 메시지를 바디에 포함하여 전달한다.

예시

POST /hotels/{hotelId}/rooms

  • 객실 정보를 생성하기 위해 세 가지 정보를 포함한 JSON 메시지를 서버에 요청한다.
  • 객실 번호(roomNumber), 객실 타입(roomType), 기본 가격(originalPrice)을 바디에 포함시킨다.
{
	"roomNumber" : "West-Wing-3928",
    "roomType" : "double",
    "originalPrice" : "150.00"
}


클라이언트의 'POST /hotels/{hotelId}/rooms' 요청을 createHotelRoom() 메서드로 매핑하는 @PostMapping 애너테이션이 받고 응답 바디는 HotelRoomIdResponse를 body 객체를 사용하여 구성한다.

ResponseEntity 응답과 Pageable, Sort 클래스

REST-API를 개발할 때는 설계에 맞추어 응답 메시지의 상태 코드와 헤더, 바디 메시지를 설정해야 한다. 이때 ResponseEntity 클래스를 사용하면 응답 메시지의 값들을 쉽게 설정할 수 있다.

ResponseEntity의
HTTP 헤더는 LinkedMultiValueMap 객체를 생성하여 "X-CREATED-AT" 헤더와 값을 추가하였고
상태코드는 HttpStatus.OK 열거형 클래스를 사용하였다.

스프링 프레임워크에서는 Pageable, Sort 라는 클래스도 제공한다.

먼저 Pageable 클래스는 REST-API 중 GET 메서드를 사용할 때 여러 개의 객체를 리스트로 응답할때 유용하다. 즉, 페이지 번호와 한 페이지가 포함하는 객체 개수를 HTTP 파라미터로 전달하면
그 범위에 맞는 데이터를 조회하여 응답하는 기능을 페이징 혹은 페이지네이션이라고 한다.

예시

GET /hotels/{hotelId}/rooms/{roomNumber}/reservations

  • page : 페이지 번호이며 0부터 시작
  • size : 페이지당 포함할 예약 정보의 개수. 기본값 20
  • sort : 소팅 정보. 정렬 프로퍼티 이름과 방향 키워드를 사용하여 콤마(,)로 구분한다.
    - 방향 키워드로는 순차 정렬을 의미하는 ASC, 역순 정렬을 의미하는 DESC가 있다.

page, size, sort 는 스프링 프레임워크에서 미리 정의된 기본 파라미터 이름이다.

@RequestParam 애너테이션 없이도 스프링 프레임워크는 객체로 변환하여 핸들러 메서드의 인자로 주입한다. 이러한 기능을 웹 서포트라고 한다. 이 때 spring-boot-starter-data-jpa 의존성을 추가해야 사용할 수 있다.

@ReqeustParam 애너테이션 없이 page, size, sort 파라미터의 값을 매핑한 Pageable 객체를 주입한다.
'localhost:18080/hotels/123456/rooms/west-wings-2012/reservations?page=1&size=50&sort=reservationId,desc&sort=reservationDate,desc!'
를 호출하면 다음과 같이 출력된다.

REST-API 검증과 예외 처리

견고한 REST-API를 만들기 위해서는 클라이언트의 요청 값을 검증해야 한다.
검증하는 방법은 요청 데이터를 자체의 포맷이나 무결성을 검증하는 방법
데이터 저장소에 데이터를 조회하여 데이터 유무를 검증하는 방법으로 나눌 수 있다.

하지만 사용자 요청을 검증하는 행위는 사용자 요청을 받아 서비스 클래스로 전달하는 컨트롤러 역할이 알맞다. 다음은 컨트롤러 클래스에서 사용자 요청을 검증하는 방법이다.

  • JSR-303 스펙에서 제공하는 애너테이션을 사용하여 검증하는 방법
  • Validator 구현 클래스와 @initBinder를 사용하여 복합적으로 검증하는 방법

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

Java EE와 Java SE에서 자바 빈을 자동으로 검증할 수 있는 스펙이다.
JSR-303을 구현한 구현체로는 Hibernate-validator 라이브러리를 사용하면 된다.

자바 빈이 되기 위해서는 Getter 메서드를 제공해야 하는데 위의 코드에서는 Lombok의 @Getter 애너테이션을 정의하였다.

  • roomType 속성을 null 체크를 검증하고 검증 실패시 에러 메시지를 표시하기 위해서@NotNull 애너테이션을 정의하고 message 속성을 선언하였다.
  • @Min 애너테이션을 정의하여 originalPrice 속성의 최솟값은 0으로 설정하였다.

다양한 JSR-303 검증 애너테이션

  • @NotNull : not null을 검증한다. empty string은 검사하지 못한다.
  • @Pattern : regular expression과 매칭되는지 검사한다.
  • @Past : 과거의 날짜인지 검증한다.
  • @Size : 검증 대상이 배열, 맵, 컬렉션인 객체의 크기를 검증한다.
  • @Min : 대상 값이 @min 값보다 크거나 같아야 한다.
  • @Max : 대상 값이 @max 값보가 작거가 같아야 한다.
  • @NotEmpty : 대상 값의 크기가 0보다 커야 한다.
  • @NotBlank : 대상 값을 트림(trim)한 후 그 크기가 0보다 커야 한다.
  • @Email : 대상 값이 이메일 형식인지 아닌지 정규 표현식으로 검증할 수 있다.
  • @Length : 문자열의 길이가 min과 max 사이인지 검증한다.

@Valid 애너테이션과 예제

@Valid 애너테이션은 검증할 자바 빈 객체를 마킹하는 용도로 사용한다.
@Min, @NotNull 애너테이션만 선언하면 동작하지 않고, 대상 객체에 @Valid 애너테이션을 정의해야 한다.

@Valid 애너테이션이 표시된 HotelRoomUpdateRequest 인자를 검사한다.
BindingResult bindingresult 인수는 HotelRoomUpdateRequest의 검증 결과를 포함한다. 이 때 BindingResult 인자는 반드시 검증 대상 객체 바로 다음 자리에 위치해야 한다.


다음과 같이 BindingResult 객체는 검증 결과 어떤 에러가 발생했는지, 어느 부분에서 발생했는지, 발생한 건수 등 전반적인 결과들을 포함한다.

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

JSR-303에서 제공하는 애너테이션은 하나의 속성에 여러 애너테이션을 조합하는데
이 검증 조건이 점점 복잡해지면 검증하기가 쉽지 않다.

이럴 때는 Validator 인터페이스를 이용하는 방법이 있다.

Validator 구현체와 검증 대상 클래스를 일대일로 개발한다.
예시)

그리고 Validator 구현 클래스(HotelRoomReserveValidator)를 연결시키기 위해서
컨트롤러 클래스에 다음과 같이 코드를 작성한다.

@InitBinder 애너테이션:
WebDataBinder는 스프링 MVC 프레임워크에서 사용자 요청과 자바 빈 객체를 바인딩할 수 있는 클래스이다. @InitBinder 애너테이션을 선언하면 스프링 MVC 프레임워크가 WebDataBinder 객체를 주입한다. 주입된 WebDataBinder 객체에 addValidators() 메서드를 사용하여 객체를 넘겨준다.

검증 대상 인자 앞에 @Valid 애너테이션을 정의하면
Validator 구현 클래스(HotelRoomReserveValidator)가 검증할 결과를 BindingResult 객체에 포함시킨다.

@ControllerAdvic와 @ExceptionHandler 예외 처리

자바에서는 예외를 두 가지로 나눌 수 있는데 Checked Exception 과 Unchecked Exception이다.

Checked Exception은 java.lang.Exception 클래스를 상속받는 클래스로 try-catch 구문으로 감싸거나 메서드 시그니처에 throws 키워드를 사용해야 한다.
또한 Checked Exception을 예외 처리 하지 않으면 컴파일 에러가 발생한다.

Unchecked Exception은 java.lang.RuntimeException을 상속받는 Exception 클래스로 예외 처리를 하지 않아도 컴파일 에러가 발생하지는 않는다.
하지만 스레드 호출 스택 기준으로 상위 객체의 메서드로 올라가기 때문에 결국에는 예외 처리를 해줘야 한다.

스프링 프레임워크에서 사용하는 대부분의 Exception 클래스는 RuntimeException을 상속받는 Unchecked Exception이다.
그래서 스프링 프레임워크에서는 다음과 같은 예외 처리 기능을 제공한다.

  • @ExceptionHandler 애너테이션 : 예외를 처리할 수 있는 메서드를 지정하는데 사용
  • @ControllerAdvice 애너테이션 : 스프링 애플리케이션 전체에서 예외 처리 메서드를 선언할 수 있는 특수한 스프링 빈
  • @RestControllerAdvice 애너테이션 : @ControllerAdvice와 @ResponseEntity 기능을 합친 애너테이션이다.

@ExceptionHandler와 @RestControllerAdvice를 이용한 애플리케이션 전체 예외 처리 메커니즘은 견고한 시스템을 만드는데 반드시 필요하다.
이와 별도로 예외 처리와 핵심 비즈니스 로직을 분리할 수 있어 개발자는 도메인 개발에 집중할 수 있다.

미디어 컨텐츠 내려받기

이미지 파일을 내려받는 두 가지 방법이 있는데
첫 번째는 HttpMessageConverter를 사용하여 메시지를 변환하는 방법이고
두 번째는 javax.servlet.http.HttpServletResponse를 사용하여 직접 OutputStream을 다루는 방법이다.

먼저 HttpMessageConverter를 사용하기 위해서는 컨트롤러 클래스에 핸들러 메서드를 다음과 같이 설정해야 한다.

  • @ResponseEntity 혹은 그와 같은 기능을 하는 @RestController를 설정
  • 메서드 시그니처의 응답 객체는 byte[] 혹은 ResponseEntity<byte[]> 형태
    파일이나 이미지 같은 데이터를 HTTP 프로토콜을 사용하여 전달할 때, 응답 메시지 바디는 8비트 바이너리로 구성된다.

    하지만 이렇게 InputStream을 바이트 배열로 변경하는 것은 파일 크기가 커지면 메모리 사용량도 비례하여 증가하고 그만큼 GC 빈도수가 증가하여 성능이 하락된다.

다음은 javax.servlet.http.HttpServletResponse를 이용하는 방법이다.
HttpServletRequest, HttpServletResponse 객체를 컨트롤러 클래스의 핸들러 메서드에 인자로 선언하면 스프링 프레임워크는 인자를 확인하고 HttpServletRequest, HttpServletResponse 객체를 주입한다.

HttpServletResponse 내부에는 OutputStream 객체가 HttpServletRequest 내부에는 InputStream 객체가 포함되어 있기 때문에 StreamUtils.copy() 메서드를 사용하여 내부 버퍼만큼 데이터를 읽고 쓰는 과정을 반복한다.

이때 내부 버퍼는 바이트 배열이며 크기가 고정되어 있어 크기가 큰 파일을 서비스해도 JVM 메모리를 효율적으로 사용할 수 있다.

StreamUtils : 스프링 프레임워크에서 제공하는 IO 스트림을 다룰 수 있는 클래스이다.

profile
백엔드 개발자 되기 위한 여정

0개의 댓글