배경
여러개로 나뉘어있던 이력서 내부 값들을 저장하는 테이블들을 하나로 합치면서 Service, Repository가 하나로 합쳐지게 되었다
테이블 구조 변경 관련
이로인해 API들도 Request를 제외한 모든게 동일하게 되었다
(기존에는 구조는 같았지만 접근해야 할 Service, Repository도 모두 달랐다)
변경 후 Controller
@PostMapping("/{resumeId}/activities")
public IdResponse createActivity(
@PathVariable Long resumeId,
@RequestBody ActivityRequestDto request
) {
return new IdResponse(componentService.create(request.toEntity().of(resumeId), ACTIVITY));
}
@PostMapping("/{resumeId}/foreign-languages")
public IdResponse createForeignLanguage(
@PathVariable Long resumeId,
@RequestBody ForeignLanguageRequestDto request
) {
return new IdResponse(componentService.create(request.toEntity().of(resumeId), FOREIGN_LANGUAGE));
}
기존 Table을 변경한 이유에는 너무 화면에 맞춰진 Table이 나와서 변경에 유연하지 못한 구조인 것도 있지만 API를 찍어내듯 만들어 지는 문제도 한 몫을 했기에 이 부분도 같이 변경하고자 한다
대안
현재 각 Controller에서 따로 가지고 있는 Request들을 하나의 추상 클래스(toEntity() 메소드를 가지고 있는)를 상속받도록 변경
ArgumentResolver 새로 만들기
이 방법도 두가지가 있는데
- resolver를 두개 중복해서 사용
- 하지만 ArgumentResolver를 호출 할 때
supportsParameter
를 일치하는 resolver만을 호출하기 때문에 두개의 resolver를 만드는 것은 불가능 하다
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
- 새로운 resolver를 만들어 사용
- 결론만 말하자면 이 방법은 사용을 하지 않았는데 하고자 하는 것이 값(Json)을 후처리(구체화 클래스로 변환)을 하는 것인데 새로 만든 다는 것은 후처리를 하는 것보다는 새로운 방식으로 값을 주입한다라는 의미가 강하게 느껴졌고
@RequestBody
를 사용하지 않아 의미를 없애고 싶지 않았다
RequestBodyAdvice 적용
이제 그럼 후처리를 어떻게 할까?
Spring에서는 RequestBodyAdvice
라는 인터페이스를 제공하고 있다
@RestControllerAdvice // bean 등록 시키기
@RequiredArgsConstructor
public class ComponentAdvice implements RequestBodyAdvice {
private static final String TYPE = "type";
private final ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.getParameterType().equals(ComponentCreateRequest.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return BlockType.of(getTypeFromPathVariable()).getClassType().cast(body);
}
private String getTypeFromPathVariable() {
ServletRequestAttributes request = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Map<String, String> attributes = (Map<String, String>) request.getRequest().getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
return attributes.get(TYPE);
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
내가 원하는 것은 body를 읽고 특정 구체 클래스로 변환을 하는 것이기 때문에 afterBodyRead()
를 사용했다
문제점
이렇게만 하면 다 될 줄 알았지만 역시 한번에 되는 건 없었다…
ObjectMapper를 통해 Json을 읽어올 때 변환 타입으로는 구체 클래스가 아닌 추상 클래스가 들어가졌고 객체 생성에 실패가 되었다
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = ActivityCreateRequest.class, name = "activities")
})
public abstract class ComponentCreateRequest {
public abstract Converter toEntity();
}
------------------------------------------------------------------------------------
@Getter @Setter
@NoArgsConstructor
@JsonTypeName("activities")
public class ActivityCreateRequest extends ComponentCreateRequest {
@Override
public Activity toEntity() {
return new Activity();
}
}
Jackson 라이브러리에서는 하위 타입을 명시할 수 있도록 도와 주는 것이 있는데 이를 이용해서 구체 클래스들을 구분할 수 있게 해주었다
하지만 또하나 문제가 있었는데 API 스펙이 변경이 되어야 한다는 점이었다
위 JsonTypeInfo에서 property로 지정한 type 필드가 request 시점에 같이 들어와야 했었고 내부 구조를 변경하는 것 때문에 프론트까지 영향을 주고 싶지는 않았다
하지만 우리에게는 beforeBodyRead
메소드가 존재했다 (하하하..하…)
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
Map<String, Object> result = objectMapper.readValue(inputMessage.getBody(), HashMap.class);
// 이 부분에서 type 필드의 값을 넣어주었고
result.put(TYPE, getTypeFromPathVariable());
// 새로운 body값을 가진 inputMessage를 반환해 주었다
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
String inputString = objectMapper.writeValueAsString(result);
return new ByteArrayInputStream(inputString.getBytes(UTF_8));
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
위와 같이 코드를 수정해주었고 하나의 API에서 런타임 시점에 동적으로 구체 클래스를 받을 수 있게 되었다
추가
RequestBodyAdvice 동작 원리
일단 기본적으로 @RequestBody
를 지원하는 resolver는 RequestResponseBodyMethodProcessor
인데 해당 객체에서 resolveArgument → readWithMessageConverters → AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters
로 들어가보면 advice를 호출하여 사용한다
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
// 여기부터
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
// 여기까지
break;
}
}
}
RequestBodyAdvice 등록
@ControllerAdvice
가 적용된 bean들을 찾아와서 등록
private void initControllerAdviceCache() {
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}
'공부기록 > 기술공유' 카테고리의 다른 글
스프링 부트 한무 403 Error (0) | 2023.11.15 |
---|---|
??? : 어딜 보시는 거죠 그건 제 잔상입니다만?!? (0) | 2023.11.13 |
Redis 사용기 (2) | 2023.11.12 |
이력서 저장 테이블 구조 재구성 (0) | 2023.11.10 |
Git Branch Linear (0) | 2023.11.06 |