fridge 프로젝트도 member 프로젝트와 같이 Spring Data Rest 를 사용하여 코드를 정리한다.
response 본문에 id 가 표시되도록 RepositoryRestConfig 클래스를 작성한다.
Fridge, Food 클래스 두 엔티티의 Id를 노출한다.
FridgeRepository, FoodRepository 인터페이스에 @RepositoryRestResource 애너테이션을 선언한다.
FridgeRepository의 findByUserId 메서드의 파라미터에 @Param("userId") 애너테이션을 선언한다.
findByUserId 메서드는 fridges/search/findByUserId GET URL 형태로 제공된다. @Param("userId") 를 선언하면 QueryString userId를 키로 사용한다.
예를들어 http://localhost:8080/fridges/search/findByUserId?userId=1004 처럼 호출하면 쿼리스트링으로 전달된 파라미터 UserID 값을 가진 Fridge 객체가 응답결과로 전달된다.
이제 fridge 프로젝트의 controller, service 등의 클래스가 전부 정리된다.
fridge 프로젝트의 API endpoint 가 변경됨에 따라 web 프로젝트도 코드를 수정한다.
FridgeClient 클래스는 Feign Client로 fridge 서버와 HTTP 통신을 한다. 해당 클래스는 다음과 같이 수정한다.
다른 메서드는 동일하지만 loadByUserId 메서드는 /fridges/search/findByUserId URL을 사용해서 Fridge 객체를 응답결과로 리턴받는다. 해당 URL은 쿼리스트링으로 userId 를 사용하므로 @RequestParam 애너테이션을 메서드 파라미터에 전달해서 userId 값을 쿼리스트링으로 호출한다.
loadFoodsByFridgeId 메서드는 새롭게 생성된 메서드인데, 기존의 API는 loadByUserId 를 호출해서 Fridge 객체를 리턴받으면 Fridge가 식품목록을 가지는 경우 배열로 응답결과에 식품목록이 같이 전달되었지만, Spring Data Rest를 사용함으로써 foods목록은 /fridges/{id}/foods GET URL에 매핑되어있다.
loadFoodsByFridgeId 메서드는 /fridges/{id}/foods GET URL을 호출하며, Resources<Food> 타입을 리턴한다. @PathVariable("id") int id값은 Fridge 식별자이다.
응답본문의 JSON 값은 다음과 같이 전달된다.
"_embedded" 와 같은 JSON 값이 존재하기 때문에 List<Food> 타입으로 리턴받을 수 없다. 그래서, HATEOAS의 Resources 클래스로 리턴받는다. 이를 위해서 별도의 Feign Decoder 설정을 해줘야한다.
Feign 설정을 위해 FeignConfig 클래스를 작성한다.
feignDecoder Bean 메서드는 feign.codec.Decoder 를 리턴하는데 디폴트 설정을 덮어쓴다. MessageConverter를 설정하는데 customObjectMapper 를 전달한다. customObjectMapper 메서드는 ObjectMapper 클래스를 생성해서 리턴한다. DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 값을 false로 설정한다.
말 그대로 역직렬화시 알수없는 속성값이 있으면 역직렬화에 실패하는 설정을 끈다.
그리고, JavaTimeModule 클래스는 JSR-310 스펙으로 java.time 패키지의 JSON 직렬화 관련 모듈이다.
JavaTimeModule 에 LocalDate의 JSON 직렬화, 역직렬화 클래스를 추가한다.
직렬화 클래스는 LocalDateSerializer, 역직렬화 클래스는 LocalDateDeserializer 이다. 내부에는 DateTimeFormatter의 "yyyy-MM-dd" 포맷형식으로 직렬화한다고 선언한다.
ObjectMapper에 모듈을 등록하는데 (registerModules), JavaTimeModule 과 Jackson2HalModule 을 등록한다. Jackson2HalModule 은 HATEOAS 의 형태(link, rel)에 맞춰서 직렬화해주는 모듈이다.
Application 클래스에 @EnableHypermediaSupport 애너테이션을 추가한다.
FridgeClient 의 loadFoodsByFridgeId 메서드의 리턴타입이 Resources<Food> 이기 때문에, 기존에 Fridge 클래스의 List<Food> foods 멤버 변수의 리턴타입을 Collection<Food> 로 수정한다.
FridgeController 클래스에서 foods 목록을 View에 전달하기 위해서 수정한다.
client.loadFoodsByFridgeId 메서드를 호출해서 리턴값을 resources 변수에 담고, fridge 변수에 setFoods로 전달한다. Resources 클래스로 감싸고 있는 Food 목록값은 getContent메서드로 리턴된다.
이전글: 냉장고 가계부 프로젝트 36
다음글: 냉장고 가계부 프로젝트 38
response 본문에 id 가 표시되도록 RepositoryRestConfig 클래스를 작성한다.
@Configuration public class RepositoryRestConfig extends RepositoryRestConfigurerAdapter { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.exposeIdsFor(Fridge.class, Food.class); } }
Fridge, Food 클래스 두 엔티티의 Id를 노출한다.
FridgeRepository, FoodRepository 인터페이스에 @RepositoryRestResource 애너테이션을 선언한다.
@RepositoryRestResource public interface FridgeRepository extends JpaRepository<Fridge, Integer> { Optional<Fridge> findByUserId(@Param("userId") Long userId); }
@RepositoryRestResource public interface FoodRepository extends JpaRepository<Food, Long> { }
FridgeRepository의 findByUserId 메서드의 파라미터에 @Param("userId") 애너테이션을 선언한다.
findByUserId 메서드는 fridges/search/findByUserId GET URL 형태로 제공된다. @Param("userId") 를 선언하면 QueryString userId를 키로 사용한다.
예를들어 http://localhost:8080/fridges/search/findByUserId?userId=1004 처럼 호출하면 쿼리스트링으로 전달된 파라미터 UserID 값을 가진 Fridge 객체가 응답결과로 전달된다.
이제 fridge 프로젝트의 controller, service 등의 클래스가 전부 정리된다.
fridge 프로젝트의 API endpoint 가 변경됨에 따라 web 프로젝트도 코드를 수정한다.
FridgeClient 클래스는 Feign Client로 fridge 서버와 HTTP 통신을 한다. 해당 클래스는 다음과 같이 수정한다.
@FeignClient(name="fridge-service", configuration=FeignConfig.class) public interface FridgeClient { @GetMapping("/fridges/search/findByUserId") Fridge loadByUserId(@RequestParam("userId") long userId) throws NotFoundException; @GetMapping("/fridges/{id}/foods") Resources<Food> loadFoodsByFridgeId(@PathVariable("id") int id); ... }
다른 메서드는 동일하지만 loadByUserId 메서드는 /fridges/search/findByUserId URL을 사용해서 Fridge 객체를 응답결과로 리턴받는다. 해당 URL은 쿼리스트링으로 userId 를 사용하므로 @RequestParam 애너테이션을 메서드 파라미터에 전달해서 userId 값을 쿼리스트링으로 호출한다.
loadFoodsByFridgeId 메서드는 새롭게 생성된 메서드인데, 기존의 API는 loadByUserId 를 호출해서 Fridge 객체를 리턴받으면 Fridge가 식품목록을 가지는 경우 배열로 응답결과에 식품목록이 같이 전달되었지만, Spring Data Rest를 사용함으로써 foods목록은 /fridges/{id}/foods GET URL에 매핑되어있다.
loadFoodsByFridgeId 메서드는 /fridges/{id}/foods GET URL을 호출하며, Resources<Food> 타입을 리턴한다. @PathVariable("id") int id값은 Fridge 식별자이다.
응답본문의 JSON 값은 다음과 같이 전달된다.
{ "_embedded": { "foods": [ { "id": 1, ... } ], "_links": { ... } } }
"_embedded" 와 같은 JSON 값이 존재하기 때문에 List<Food> 타입으로 리턴받을 수 없다. 그래서, HATEOAS의 Resources 클래스로 리턴받는다. 이를 위해서 별도의 Feign Decoder 설정을 해줘야한다.
Feign 설정을 위해 FeignConfig 클래스를 작성한다.
@Configuration public class FeignConfig { @Bean public Decoder feignDecoder() { HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper()); ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter); return new ResponseEntityDecoder(new SpringDecoder(objectFactory)); } public ObjectMapper customObjectMapper(){ ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer()); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer()); objectMapper.registerModules(new Jackson2HalModule(), javaTimeModule); return objectMapper; } private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static class LocalDateSerializer extends JsonSerializer<LocalDate> { @Override public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(value.format(FORMATTER)); } } private static class LocalDateDeserializer extends JsonDeserializer<LocalDate> { @Override public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { return LocalDate.parse(p.getValueAsString(), FORMATTER); } } }
feignDecoder Bean 메서드는 feign.codec.Decoder 를 리턴하는데 디폴트 설정을 덮어쓴다. MessageConverter를 설정하는데 customObjectMapper 를 전달한다. customObjectMapper 메서드는 ObjectMapper 클래스를 생성해서 리턴한다. DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 값을 false로 설정한다.
말 그대로 역직렬화시 알수없는 속성값이 있으면 역직렬화에 실패하는 설정을 끈다.
그리고, JavaTimeModule 클래스는 JSR-310 스펙으로 java.time 패키지의 JSON 직렬화 관련 모듈이다.
JavaTimeModule 에 LocalDate의 JSON 직렬화, 역직렬화 클래스를 추가한다.
직렬화 클래스는 LocalDateSerializer, 역직렬화 클래스는 LocalDateDeserializer 이다. 내부에는 DateTimeFormatter의 "yyyy-MM-dd" 포맷형식으로 직렬화한다고 선언한다.
ObjectMapper에 모듈을 등록하는데 (registerModules), JavaTimeModule 과 Jackson2HalModule 을 등록한다. Jackson2HalModule 은 HATEOAS 의 형태(link, rel)에 맞춰서 직렬화해주는 모듈이다.
Application 클래스에 @EnableHypermediaSupport 애너테이션을 추가한다.
@EnableHypermediaSupport(type=HypermediaType.HAL) @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class Application { @Bean public LayoutDialect layoutDialect() { return new LayoutDialect(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
FridgeClient 의 loadFoodsByFridgeId 메서드의 리턴타입이 Resources<Food> 이기 때문에, 기존에 Fridge 클래스의 List<Food> foods 멤버 변수의 리턴타입을 Collection<Food> 로 수정한다.
@NoArgsConstructor @Data public class Fridge { private Integer id; @NotNull @Size(min=2, max=15) private String nickname; private Collection<Food> foods; @NotNull private Long userId; @Builder public Fridge(Integer id, String nickname, Long userId) { this.id = id; this.nickname = nickname; this.userId = userId; } }
FridgeController 클래스에서 foods 목록을 View에 전달하기 위해서 수정한다.
@Controller @RequestMapping("/fridges") @RequiredArgsConstructor @Slf4j public class FridgeController { private final FridgeClient client; private static final String DEFAULT_NICKNAME = "myFridge"; @GetMapping("/me") public String myFridge(@AuthenticationPrincipal Member member, Model model) { Fridge fridge = null; try { fridge = client.loadByUserId(member.getId()); } catch(NotFoundException ex) { log.info(ex.getMessage()); fridge = client.generate(Fridge.builder() .nickname(DEFAULT_NICKNAME) .userId(member.getId()).build()); } Resources<Food> resources = client.loadFoodsByFridgeId(fridge.getId()); fridge.setFoods(resources.getContent()); model.addAttribute("fridge", fridge); return "fridges/fridge"; } }
client.loadFoodsByFridgeId 메서드를 호출해서 리턴값을 resources 변수에 담고, fridge 변수에 setFoods로 전달한다. Resources 클래스로 감싸고 있는 Food 목록값은 getContent메서드로 리턴된다.
이전글: 냉장고 가계부 프로젝트 36
다음글: 냉장고 가계부 프로젝트 38