함수형 기반의 핸들러
[코드 17.1]
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
[참고]
HandlerFunction 은 RouterFunction 을 통해 요청이 라우팅된 이후에 동작함
@RequestMapping
어노테이션과 동일한 역할[코드 17.2]
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
Optional<HandlerFunction<T>> route(ServerRequest request);
...
}
[코드 17.3]
@Configuration("bookRouterV1")
public class BookRouter {
@Bean
public RouterFunction<?> routeBookV1(BookHandler handler) {
return route()
.POST("/v1/books", handler::createBook)
.PATCH("/v1/books/{book-id}", handler::updateBook)
.GET("/v1/books", handler::getBooks)
.GET("/v1/books/{book-id}", handler::getBook)
.build();
}
}
[코드 17.4]
@Component("bookHandlerV1")
public class BookHandler {
private final BookMapper mapper;
public BookHandler(BookMapper mapper) {
this.mapper = mapper;
}
public Mono<ServerResponse> createBook(ServerRequest request) {
return request.bodyToMono(BookDto.Post.class)
.map(post -> mapper.bookPostToBook(post))
.flatMap(book ->
ServerResponse
.created(URI.create("/v1/books/" + book.getBookId()))
.build());
}
public Mono<ServerResponse> getBook(ServerRequest request) {
long bookId = Long.valueOf(request.pathVariable("book-id"));
Book book =
new Book(bookId,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now());
return ServerResponse
.ok()
.bodyValue(mapper.bookToResponse(book))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> updateBook(ServerRequest request) {
final long bookId = Long.valueOf(request.pathVariable("book-id"));
return request
.bodyToMono(BookDto.Patch.class)
.map(patch -> {
patch.setBookId(bookId);
return mapper.bookPatchToBook(patch);
})
.flatMap(book -> ServerResponse.ok()
.bodyValue(mapper.bookToResponse(book)));
}
public Mono<ServerResponse> getBooks(ServerRequest request) {
List<Book> books = List.of(
new Book(1L,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now()),
new Book(2L,
"Kotlin 고급",
"Advanced Kotlin",
"Kevin",
"222-22-2222-222-2",
"Kotlin 중급 프로그래밍 마스터",
"2022-05-22",
LocalDateTime.now(),
LocalDateTime.now())
);
return ServerResponse
.ok()
.bodyValue(mapper.booksToResponse(books));
}
}
[코드 17.5]
도서 정보 저장을 위한 커스텀 밸리데이터
@Component("bookValidatorV2")
public class BookValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return BookDto.Post.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
BookDto.Post post = (BookDto.Post) target;
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "titleKorean", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "titleEnglish", "field.required");
}
}
[코드 17.6]
@Slf4j
@Component("bookHandlerV2")
public class BookHandler {
private final BookMapper mapper;
private final BookValidator validator;
public BookHandler(BookMapper mapper, BookValidator validator) {
this.mapper = mapper;
this.validator = validator;
}
public Mono<ServerResponse> createBook(ServerRequest request) {
return request.bodyToMono(BookDto.Post.class)
.doOnNext(post -> this.validate(post))
.map(post -> mapper.bookPostToBook(post))
.flatMap(book ->
ServerResponse
.created(URI.create("/v2/books/" + book.getBookId()))
.build());
}
public Mono<ServerResponse> patchBook(ServerRequest request) {
final long bookId = Long.valueOf(request.pathVariable("book-id"));
return request
.bodyToMono(BookDto.Patch.class)
.map(patch -> {
patch.setBookId(bookId);
return mapper.bookPatchToBook(patch);
})
.flatMap(book -> ServerResponse.ok()
.bodyValue(mapper.bookToResponse(book)));
}
public Mono<ServerResponse> getBook(ServerRequest request) {
long bookId = Long.valueOf(request.pathVariable("book-id"));
Book book =
new Book(bookId,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now());
return ServerResponse
.ok()
.bodyValue(mapper.bookToResponse(book))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> getBooks(ServerRequest request) {
List<Book> books = List.of(
new Book(1L,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now()),
new Book(2L,
"Kotlin 고급",
"Advanced Kotlin",
"Kevin",
"222-22-2222-222-2",
"Kotlin 중급 프로그래밍 마스터",
"2022-05-22",
LocalDateTime.now(),
LocalDateTime.now())
);
return ServerResponse
.ok()
.bodyValue(mapper.booksToResponse(books));
}
private void validate(BookDto.Post post) {
Errors errors = new BeanPropertyBindingResult(post, BookDto.class.getName());
validator.validate(post, errors);
if (errors.hasErrors()) {
log.error(errors.getAllErrors().toString());
throw new ServerWebInputException(errors.toString());
}
}
}
위의 코드에서는 커스텀 밸리데이터를 이용하여 유효성을 검증했으나, 사실 이는 좋지 않은 방법임 (비즈니스 로직이 들어가 있음)
따라서 스프링에서 제공하는 표준 Bean validation 인터페이스로 유효성 검증하는것을 추천
[코드 17.7]
@Slf4j
@Component("bookValidatorV3")
public class BookValidator<T> {
private final Validator validator;
public BookValidator(@Qualifier("springValidator") Validator validator) {
this.validator = validator;
}
public void validate(T body) {
Errors errors =
new BeanPropertyBindingResult(body, body.getClass().getName());
this.validator.validate(body, errors);
if (!errors.getAllErrors().isEmpty()) {
onValidationErrors(errors);
}
}
private void onValidationErrors(Errors errors) {
log.error(errors.getAllErrors().toString());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errors.getAllErrors()
.toString());
}
}
[코드 17.8]
@Slf4j
@Component("bookValidatorV4")
public class BookValidator<T> {
private final Validator validator;
public BookValidator(@Qualifier("javaxValidator") Validator validator) {
this.validator = validator;
}
public void validate(T body) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(body);
if (!constraintViolations.isEmpty()) {
onValidationErrors(constraintViolations);
}
}
private void onValidationErrors(Set<ConstraintViolation<T>> constraintViolations) {
log.error(constraintViolations.toString());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
constraintViolations.toString());
}
}
[코드 17.9]
@Slf4j
@Component("bookHandlerV4")
public class BookHandler {
private final BookMapper mapper;
private final BookValidator validator;
public BookHandler(BookMapper mapper, BookValidator validator) {
this.mapper = mapper;
this.validator = validator;
}
public Mono<ServerResponse> createBook(ServerRequest request) {
return request.bodyToMono(BookDto.Post.class)
.doOnNext(post -> validator.validate(post))
.map(post -> mapper.bookPostToBook(post))
.flatMap(book -> ServerResponse
.created(URI.create("/v4/books/" + book.getBookId()))
.build());
}
public Mono<ServerResponse> updateBook(ServerRequest request) {
final long bookId = Long.valueOf(request.pathVariable("book-id"));
return request
.bodyToMono(BookDto.Patch.class)
.doOnNext(patch -> validator.validate(patch))
.map(patch -> {
patch.setBookId(bookId);
return mapper.bookPatchToBook(patch);
})
.flatMap(book -> ServerResponse.ok()
.bodyValue(mapper.bookToResponse(book)));
}
public Mono<ServerResponse> getBook(ServerRequest request) {
long bookId = Long.valueOf(request.pathVariable("book-id"));
Book book =
new Book(bookId,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now());
return ServerResponse
.ok()
.bodyValue(mapper.bookToResponse(book))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> getBooks(ServerRequest request) {
List<Book> books = List.of(
new Book(1L,
"Java 고급",
"Advanced Java",
"Kevin",
"111-11-1111-111-1",
"Java 중급 프로그래밍 마스터",
"2022-03-22",
LocalDateTime.now(),
LocalDateTime.now()),
new Book(2L,
"Kotlin 고급",
"Advanced Kotlin",
"Kevin",
"222-22-2222-222-2",
"Kotlin 중급 프로그래밍 마스터",
"2022-05-22",
LocalDateTime.now(),
LocalDateTime.now())
);
return ServerResponse
.ok()
.bodyValue(mapper.booksToResponse(books));
}
}