티스토리 뷰

728x90

1. 우선 빌드 파일에 starter-web 대신 starter-webflux를 지정한다. 

  1-1 기본적으로 tomcat을 사용할 수 없게 되고, servlet에 관련된 내용은 전부 에러가 발생한다.

  1-2 파일 업로드에 사용한 multipart 같은 것도 servlet을 사용하므로 제대로 동작하지 않게 된다.

 

/*
 * This file was generated by the Gradle 'init' task.
 */

plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id "io.freefair.lombok" version "5.1.1"
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    // implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    implementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    implementation 'org.webjars:bootstrap:4.5.0'
    implementation 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

group = 'pe.pilseong'
version = '0.0.1-SNAPSHOT'
description = 'demo'
sourceCompatibility = '11'


tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

 

2. 실행하게 되면 tomcat 대신 netty를 사용하는 것을 볼 수 있다.

 

 

3. MockMvc를 사용한 테스트도 모두 실패하게 된다.

 

 

4. Thymeleaf를 사용할 때도 자동으로 template engine이 변경된다.

  4-1 아래의 소스는 VSCode에서 ctrl+shift+t 소스검색에서 찾은 ThymeleafWebMvcConfiguration.class 일부이다.

  4-2 즉 @ConditionalOnWebApplication(type = Type.REACTIVE) 이 부분이 어떤 라이브러리가 들어간지를 확인한다.

  4-3 reactive인 경우는 템플릿엔진을 사용하고 WebMvc의 경우는 ViewResolver를 사용함을 알 수 있다.

 

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebMvcConfiguration {

  @Bean
  @ConditionalOnEnabledResourceChain
  @ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class)
  FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
    FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
        new ResourceUrlEncodingFilter());
    registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
    return registration;
  }

  @Configuration(proxyBeanMethods = false)
  static class ThymeleafViewResolverConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "thymeleafViewResolver")
    ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
        SpringTemplateEngine templateEngine) {
      ThymeleafViewResolver resolver = new ThymeleafViewResolver();
      resolver.setTemplateEngine(templateEngine);
      resolver.setCharacterEncoding(properties.getEncoding().name());
      resolver.setContentType(
          appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
      resolver.setProducePartialOutputWhileProcessing(
          properties.getServlet().isProducePartialOutputWhileProcessing());
      resolver.setExcludedViewNames(properties.getExcludedViewNames());
      resolver.setViewNames(properties.getViewNames());
      // This resolver acts as a fallback resolver (e.g. like a
      // InternalResourceViewResolver) so it needs to have low precedence
      resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
      resolver.setCache(properties.isCache());
      return resolver;
    }

    private String appendCharset(MimeType type, String charset) {
      if (type.getCharset() != null) {
        return type.toString();
      }
      LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
      parameters.put("charset", charset);
      parameters.putAll(type.getParameters());
      return new MimeType(type, parameters).toString();
    }

  }

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafReactiveConfiguration {

  @Bean
  @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
  SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties,
      ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
    SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
    engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
    engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
    templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
    dialects.orderedStream().forEach(engine::addDialect);
    return engine;
  }

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebFluxConfiguration {

  @Bean
  @ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
  ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine,
      ThymeleafProperties properties) {
    ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
    resolver.setTemplateEngine(templateEngine);
    mapProperties(properties, resolver);
    mapReactiveProperties(properties.getReactive(), resolver);
    // This resolver acts as a fallback resolver (e.g. like a
    // InternalResourceViewResolver) so it needs to have low precedence
    resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
    return resolver;
  }

  private void mapProperties(ThymeleafProperties properties, ThymeleafReactiveViewResolver resolver) {
    PropertyMapper map = PropertyMapper.get();
    map.from(properties::getEncoding).to(resolver::setDefaultCharset);
    resolver.setExcludedViewNames(properties.getExcludedViewNames());
    resolver.setViewNames(properties.getViewNames());
  }

  private void mapReactiveProperties(Reactive properties, ThymeleafReactiveViewResolver resolver) {
    PropertyMapper map = PropertyMapper.get();
    map.from(properties::getMediaTypes).whenNonNull().to(resolver::setSupportedMediaTypes);
    map.from(properties::getMaxChunkSize).asInt(DataSize::toBytes).when((size) -> size > 0)
        .to(resolver::setResponseMaxChunkSizeBytes);
    map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames);
    map.from(properties::getChunkedModeViewNames).to(resolver::setChunkedModeViewNames);
  }

}

 

5. Controller에서 block으로 처리한 것들을 풀어주어야 한다.

  5-1 reactive(mongo-reactive) 방식을 사용하면서 block을 통해 WebMvc를 사용하는 것이 가능한데,

    5-1-1 webflux를 사용하면 바로 아래처럼 에러가 발생한다.

 

  5-2 기존 코드

 

package pe.pilseong.recipe.controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.service.RecipeService;

@Slf4j
@RequiredArgsConstructor
@Controller
public class IndexController {

  private final RecipeService recipeService;
  
  @GetMapping({"", "/", "/index"})
  public String getIndexPage(Model model) {

    log.debug("getIndexPage in IndexController");
    List<Recipe> recipes = recipeService.getRecipes().collectList().block();
    model.addAttribute("recipes", recipes);

    return "main";
  }
}

 

  5-3 아래처럼 더 이상 block 같은 것은 사용할 필요가 없다.

 

package pe.pilseong.recipe.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.service.RecipeService;

@Slf4j
@RequiredArgsConstructor
@Controller
public class IndexController {

  private final RecipeService recipeService;
  
  @GetMapping({"", "/", "/index"})
  public String getIndexPage(Model model) {

    log.debug("getIndexPage in IndexController");
    model.addAttribute("recipes", recipeService.getRecipes().collectList());

    return "main";
  }
}

 

  5-4 Service 레이어의 구현코드도 변경이 필요하다.

    5-4-1 아래 코드의 deleteById는 void 대신 Mono<Void>가 반환되어야 한다. 구독시점을 클라이언트가 필요로 한다.

 

package pe.pilseong.recipe.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.RecipeCommand;
import pe.pilseong.recipe.converter.RecipeCommandToRecipe;
import pe.pilseong.recipe.converter.RecipeToRecipeCommand;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.exception.NotFoundException;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
@Slf4j
public class RecipeServiceImpl implements RecipeService {

  private final RecipeReactiveRepository recipeReactiveRepository;

  private final RecipeCommandToRecipe recipeConverter;
  private final RecipeToRecipeCommand commandConverter;

  @Override
  public Flux<Recipe> getRecipes() {
    log.debug("getRecipes in RecipeServiceImpl");
    return recipeReactiveRepository.findAll();
  }

  @Override
  public Mono<Recipe> findById(String id) {
    return recipeReactiveRepository.findById(id)
    .switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
  }

  @Override
  public Mono<Recipe> save(RecipeCommand command) {
    return recipeReactiveRepository.save(recipeConverter.convert(command));
  }

  @Override
  public Mono<RecipeCommand> findCommandById(String id) {
    return recipeReactiveRepository.findById(id)
    .map(commandConverter::convert)
    .switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
  }

  @Override
  public Mono<RecipeCommand> saveRecipeCommand(RecipeCommand command) {
    return recipeReactiveRepository.save(recipeConverter.convert(command))
      .map(commandConverter::convert);
  }

  @Override
  public void deleteById(String id) {
    recipeReactiveRepository.deleteById(id).block();
  }
}

 

    5-4-2 아래처럼 변경하였다.

 

package pe.pilseong.recipe.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.RecipeCommand;
import pe.pilseong.recipe.converter.RecipeCommandToRecipe;
import pe.pilseong.recipe.converter.RecipeToRecipeCommand;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.exception.NotFoundException;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
@Slf4j
public class RecipeServiceImpl implements RecipeService {

  private final RecipeReactiveRepository recipeReactiveRepository;

  private final RecipeCommandToRecipe recipeConverter;
  private final RecipeToRecipeCommand commandConverter;

  @Override
  public Flux<Recipe> getRecipes() {
    log.debug("getRecipes in RecipeServiceImpl");
    return recipeReactiveRepository.findAll();
  }

  @Override
  public Mono<Recipe> findById(String id) {  
    return recipeReactiveRepository.findById(id)
    .switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
  }

  @Override
  public Mono<Recipe> save(RecipeCommand command) {
    return recipeReactiveRepository.save(recipeConverter.convert(command));
  }

  @Override
  public Mono<RecipeCommand> findCommandById(String id) {
    return recipeReactiveRepository.findById(id)
    .map(commandConverter::convert)
    .switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
  }

  @Override
  public Mono<RecipeCommand> saveRecipeCommand(RecipeCommand command) {
    return recipeReactiveRepository.save(recipeConverter.convert(command))
      .map(commandConverter::convert);
  }

  @Override
  public Mono<Void> deleteById(String id) {
    return recipeReactiveRepository.deleteById(id);
  }
}

 

6. validation도 수정해 주어야 한다.

  6-1 아래 코드는 WebMvc에서는 잘 돌아가던 코드인데 webflux로는 돌아가지 않는다. 

    6-1-1 아래의 에러를 보면 BindingResult는 검증 대상인 @ModelAttribute 인자 바로 뒤에 나와야 한다는 말이다.

    6-1-2 그런데, 이 코드는 WebMvc에서 문제가 없었고, 보면 알겠지만 에러가 말하는대로 바로 뒤에 지정되어 있다.

 

 

  @PostMapping("/")
  public String saveOrUpdate(@Valid @ModelAttribute("recipe") RecipeCommand command, BindingResult result, 
      Model model) {

    if (result.hasErrors()) {
      model.addAttribute("recipe", command);
      
      result.getAllErrors().forEach(error -> {
        System.out.println();
        log.debug(error.toString());
        System.out.println();
      });
      return VIEW_RECIPE_FORM;
    }

    RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();

    return "redirect:/recipe/" + savedCommand.getId() + "/show";
  }

 

    6-1-3 아래 코드는 BindingResult가 문제가 있으니 바로 InitBinder에서 받아오도록 수정한 코드이다.

      6-1-3-1 하지만 아래처럼 해도 돌아가지 않는다. 문제는 @Valid 이 놈도 정상적으로 실행되지 않는다.

      6-1-3-2 에러가 복잡한데 무슨 말인지는 모륵ㅆ다. 0번 인자가 문제가 있다는 것 같긴 하다.

 

 

 

  private WebDataBinder webDataBinder;

  @InitBinder
  public void initBinder(WebDataBinder webDataBinder) {
    this.webDataBinder = webDataBinder;
  }

  @PostMapping("/")
  public String saveOrUpdate(@Valid @ModelAttribute("recipe") RecipeCommand command, Model model) {

    webDataBinder.validate();
    BindingResult result = webDataBinder.getBindingResult();

    if (result.hasErrors()) {
      model.addAttribute("recipe", command);
      
      result.getAllErrors().forEach(error -> {
        System.out.println();
        log.debug(error.toString());
        System.out.println();
      });
      return VIEW_RECIPE_FORM;
    }

    RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();

    return "redirect:/recipe/" + savedCommand.getId() + "/show";
  }

 

    6-1-4 결국 마지막으로 @Valid를 제거하니 잘돌아간다. 코드를 보면 알겠지만 block()이 있어 실패할 때만 돌아간다.

 

  private WebDataBinder webDataBinder;

  @InitBinder
  public void initBinder(WebDataBinder webDataBinder) {
    this.webDataBinder = webDataBinder;
  }

  @PostMapping("/")
  public String saveOrUpdate(@ModelAttribute("recipe") RecipeCommand command, Model model) {

    webDataBinder.validate();
    BindingResult result = webDataBinder.getBindingResult();

    if (result.hasErrors()) {
      model.addAttribute("recipe", command);
      
      result.getAllErrors().forEach(error -> {
        System.out.println();
        log.debug(error.toString());
        System.out.println();
      });
      return VIEW_RECIPE_FORM;
    }

    RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();

    return "redirect:/recipe/" + savedCommand.getId() + "/show";
  }

 

 

7. ErrorHandling 코드도 수정이 필요하다.

  7-1 webflux에서는 ModelAndView를 사용할 수 없고 추가적인 에러도 처리해 주어야 한다.

 

package pe.pilseong.recipe.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.exception.NotFoundException;

@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {

  @ResponseStatus(code = HttpStatus.BAD_REQUEST)
  @ExceptionHandler(NumberFormatException.class)
  public ModelAndView handleNumberFormatException(Exception exception) {
    log.error("Handling Number Format Exception");
    log.error(exception.getMessage());

    ModelAndView mav = new ModelAndView();

    mav.setViewName("400error");
    mav.addObject("exception", exception);

    return mav;
  }

  
  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ExceptionHandler(NotFoundException.class)
  public ModelAndView handleNotFound(Exception exception) {
    log.error("Handling not found Exception");
    log.error(exception.getMessage());

    ModelAndView mav = new ModelAndView();

    mav.setViewName("404error");
    mav.addObject("exception", exception);

    return mav;
  }
}

 

  7-2 ModelAndView 대신 Model을 받아서 처리하면 된다.

    7-2-1 null이 반환되는 것을 처리하기 위한 TemplateInputException도 지정해 준다.

 

package pe.pilseong.recipe.controller;

import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.thymeleaf.exceptions.TemplateInputException;

import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.exception.NotFoundException;

@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {

  @ResponseStatus(code = HttpStatus.BAD_REQUEST)
  @ExceptionHandler(NumberFormatException.class)
  public String handleNumberFormatException(Exception exception, Model model) {
    log.error("Handling Number Format Exception");
    log.error(exception.getMessage());

    model.addAttribute("exception", exception);

    return "400error";
  }

  
  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ExceptionHandler({ NotFoundException.class, TemplateInputException.class })
  public String handleNotFound(Exception exception, Model model) {
    log.error("Handling not found Exception");
    log.error(exception.getMessage());

    model.addAttribute("exception", exception);

    return "404error";
  }
}


8. 저장관련 로직도 수정되어야 한다. block을 사용하지 않고 필요시 subscribe만 처리하여 사용한다.

  8-1 아래는 Service 구현 부분이다.

 

package pe.pilseong.recipe.service;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.IngredientCommand;
import pe.pilseong.recipe.converter.IngredientCommandToIngredient;
import pe.pilseong.recipe.converter.IngredientToIngredientCommand;
import pe.pilseong.recipe.domain.Ingredient;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import pe.pilseong.recipe.repository.reactive.UnitOfMeasureReactiveRepository;
import reactor.core.publisher.Mono;

@Slf4j
@Service
@RequiredArgsConstructor
public class IngredientServiceImpl implements IngredientService {

  private final RecipeReactiveRepository recipeReactiveRepository;

  private final IngredientToIngredientCommand ingredientCommandConverter;
  private final IngredientCommandToIngredient ingredientConverter;
  private final UnitOfMeasureReactiveRepository uomReactiveRepository;

  @Override
  public Mono<IngredientCommand> findByRecipeIdAndIngredientId(final String recipeId, final String ingredientId) {

    log.debug("\nfindByRecipeIdAndIngredientId in IngredientServiceImpl");

    return recipeReactiveRepository.findById(recipeId).
        flatMapIterable(Recipe::getIngredients)
        .filter(ingredient -> 
            ingredient.getId().equalsIgnoreCase(ingredientId)).single().map(ingredient -> {
              IngredientCommand command = ingredientCommandConverter.convert(ingredient);
              command.setRecipeId(recipeId);
              return command;
            });
  }

  @Override
  public Mono<IngredientCommand> saveIngredientCommand(IngredientCommand command) {
    Objects.requireNonNull(command);
    AtomicReference<String> ingredientId = new AtomicReference<>();
    AtomicReference<String> recipeId = new AtomicReference<>();

    log.debug("saveIngredientCommand in IngredientServiceImpl with command :: " + command.toString());

    return recipeReactiveRepository.findById(command.getRecipeId())
    .map(recipe -> {
      recipeId.set(recipe.getId());
      recipe.getIngredients().stream()
          .filter(ingredient -> ingredient.getId().equalsIgnoreCase(command.getId()))
          .findFirst()
          // update ingredient
          .map(ingredient -> {
            ingredientId.set(command.getId());
            ingredient.setDescription(command.getDescription());
            ingredient.setAmount(command.getAmount());
            return recipe;
          })
          // add new ingredient
          .orElseGet(() -> {
            Ingredient newIngredient = ingredientConverter.convert(command);
            ingredientId.set(Objects.requireNonNull(newIngredient).getId());

            uomReactiveRepository.findById(command.getUom().getId())
              .flatMap(unitOfMeasure -> {
                newIngredient.setUom(unitOfMeasure);
                return Mono.just(unitOfMeasure);
              })
              .subscribe();

            recipe.addIngredient(newIngredient);
            return recipe;
          });
      return recipe;
    })
    .flatMap(recipe -> recipeReactiveRepository.save(recipe).then(Mono.just(recipe)))
        .flatMapIterable(Recipe::getIngredients)
        .filter(savedIngredient -> savedIngredient.getId().equalsIgnoreCase(ingredientId.get()))
        .flatMap(savedIngredient -> {
          IngredientCommand ingredientCommand = ingredientCommandConverter.convert(savedIngredient);
          return Mono.just(ingredientCommand);
        }).single();
  }

  @Override
  public Mono<Void> deleteByRecipeIdAndIngredientId(final String recipeId, final String ingredientId) {

    log.debug("deleteByRecipeIdAnd in IngredientServiceImpl with recipe Id :: " + recipeId + " " + ingredientId);

    return recipeReactiveRepository.findById(recipeId)
      .map(recipe -> {
        log.debug(recipe.getIngredients().toString());
        boolean removeIf = recipe.getIngredients().removeIf(ingredient -> ingredient.getId().equals(ingredientId));

        if (removeIf) {
          log.debug("Some ingredient deleted");
          recipeReactiveRepository.save(recipe).subscribe();
        }
        return Mono.empty();
      })
      .then();
  }
}

 

  8-2 위의 service를 사용하는 controller 이다.

    8-2-1 저장과 삭제 부분만 참고하면 된다.

    8-2-2 Reactive는 워낙 방법이 많아서 나중에 좀 더 효율적인 코드가 생각나면 적어놓겠다.

    8-2-3 webflux에서 thymeleaf를 사용할 때 view name을 바로전에 저장한 객체의 id로  만들어야 하는 경우가 있다.

      8-2-3-1 아무리 생각해도 안되어 그냥 포기했다. 나중에 찾으면 붙여 놓을 예정이다.

      8-2-3-2 조금 우회하여 저장한 객체를 세부정보를 바로 보여주는 것이 아니라 리스트를 보여주는 것으로 대체했다.

 

package pe.pilseong.recipe.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.IngredientCommand;
import pe.pilseong.recipe.command.UnitOfMeasureCommand;
import pe.pilseong.recipe.service.IngredientService;
import pe.pilseong.recipe.service.RecipeService;
import pe.pilseong.recipe.service.UnitOfMeasureService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
@Controller
@RequiredArgsConstructor
public class IngredientController {
  
  /**
   *
   */
  private static final String RECIPE_INGREDIENT_INGREDIENT_FORM = 
    "recipe/ingredient/ingredientForm";
    
  private final RecipeService recipeService;
  private final IngredientService ingredientService;
  private final UnitOfMeasureService uomService;

  private WebDataBinder webDataBinder;

  @InitBinder("ingredient")
  public void initBinder(WebDataBinder webDataBinder) {
    this.webDataBinder = webDataBinder;
  }

  @ModelAttribute("uomList")
  public Flux<UnitOfMeasureCommand> populateUoms() {
    return uomService.listAllUoms();
  }

  @GetMapping("/recipe/{id}/ingredients")
  public String listIngredients(@PathVariable("id") String id, Model model) {
    log.debug("listIngredients in IngredientController");

    model.addAttribute("recipe", recipeService.findCommandById(id));

    return "recipe/ingredient/list";
  }

  @GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/show")
  public String showIngredient(
    @PathVariable("recipeId") String recipeId, 
    @PathVariable("ingredientId") String ingredientId, Model model) {

    model.addAttribute("ingredient", 
        ingredientService.findByRecipeIdAndIngredientId(recipeId, ingredientId));
        
    return "recipe/ingredient/show";
  }

  @GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/update")
  public String showUpdateRecipeIngredient(
    @PathVariable("recipeId") String recipeId,
    @PathVariable("ingredientId") String ingredientId,
    Model model
  ) {

    model.addAttribute("ingredient", 
          ingredientService.findByRecipeIdAndIngredientId(recipeId, ingredientId));

    return RECIPE_INGREDIENT_INGREDIENT_FORM;
  }

  @PostMapping("/recipe/{recipeId}/ingredient")
  public String saveIngredientCommand(
    @PathVariable("recipeId") String recipeId, 
    @ModelAttribute("ingredient") IngredientCommand ingredientCommand,
    Model model) {
      
    webDataBinder.validate();
    BindingResult results = webDataBinder.getBindingResult();
    
    log.debug("\nsaveIngredientCommand in IngredientController\n");
    log.debug("\results in IngredientController\n" + results.toString());

    ingredientCommand.setRecipeId(recipeId);

    if (results.hasErrors()) {
      model.addAttribute("ingredient", ingredientCommand);

      log.debug("saveIngredientCommand error occurred in IngredientController\n");
      results.getAllErrors().forEach(error-> log.debug(error.toString()));
      return RECIPE_INGREDIENT_INGREDIENT_FORM;
    }

    log.debug("saveIngredientCommand passed in IngredientController\n");

    Mono<IngredientCommand> saveIngredientCommand = 
            ingredientService.saveIngredientCommand(ingredientCommand);
            
    model.addAttribute("ingredient", saveIngredientCommand);

    return "redirect:/recipe/{recipeId}/ingredients";
  }

  @GetMapping("/recipe/{recipeId}/ingredient/new")
  public String showNewIngredientForm(@PathVariable("recipeId") String recipeId, Model model) {

    IngredientCommand ingredientCommand = new IngredientCommand();
    // make them used inside template
    ingredientCommand.setRecipeId(recipeId);
    ingredientCommand.setUom(new UnitOfMeasureCommand());

    model.addAttribute("ingredient", ingredientCommand);

    return RECIPE_INGREDIENT_INGREDIENT_FORM;
  }

  @GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/delete")
  public String deleteIngredient(@PathVariable("recipeId") String recipeId, 
      @PathVariable("ingredientId") String ingredientId) {

    ingredientService.deleteByRecipeIdAndIngredientId(recipeId, ingredientId).subscribe();

    return "redirect:/recipe/" + recipeId + "/ingredients";
  }
}
728x90
댓글