jhs0129
프로그래밍
jhs0129
전체 방문자
오늘
어제
  • 분류 전체보기
    • 자격증
      • SQLD
      • 정보처리기사
    • 프로젝트
      • html csss js - todolist
      • JSP 방명록
      • 졸업작품
    • 공부기록
      • Java
      • Spring
      • Spring Security
      • Algorithm
      • JPA
      • DB
      • Servlet JSP
      • html
      • 기술공유
    • 잡다한 생각

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • EC2
  • cicd
  • 프로젝트
  • codedeploy
  • JPA
  • 스프링시큐리티
  • spring boot
  • github
  • NHN Cloud
  • 스프링
  • spring
  • rest docs
  • AWS
  • nhn cloud 강의
  • oAuth2
  • Spring Security
  • spring framework
  • 스프링 프레임워크
  • Spring Security Login
  • spring data jpa

최근 댓글

최근 글

티스토리

반응형
250x250
hELLO · Designed By 정상우.
jhs0129

프로그래밍

RequestBody Runtime시 동적 주입
공부기록/기술공유

RequestBody Runtime시 동적 주입

2023. 11. 15. 14:22
320x100
반응형

배경

여러개로 나뉘어있던 이력서 내부 값들을 저장하는 테이블들을 하나로 합치면서 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 새로 만들기

이 방법도 두가지가 있는데

  1. resolver를 두개 중복해서 사용
  2. 하지만 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;
}
  1. 새로운 resolver를 만들어 사용
  2. 결론만 말하자면 이 방법은 사용을 하지 않았는데 하고자 하는 것이 값(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);
        }
}
320x100
반응형

'공부기록 > 기술공유' 카테고리의 다른 글

스프링 부트 한무 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
    '공부기록/기술공유' 카테고리의 다른 글
    • 스프링 부트 한무 403 Error
    • ??? : 어딜 보시는 거죠 그건 제 잔상입니다만?!?
    • Redis 사용기
    • 이력서 저장 테이블 구조 재구성
    jhs0129
    jhs0129
    공부기록 남기기

    티스토리툴바