2. form데이터 JSON 변환 : 커스텀 어노테이션 활용

한승록·2024년 9월 27일
0

API 활용

목록 보기
2/2
post-thumbnail

1. 개요

  • 외부 API를 활용하다 보면 스네이크 케이스카멜케이스가 내부 프로젝트와 충돌하다 보니 관리에 힘든 부분이 발생합니다.

  • 그렇다고 같은 키값을 객체를 나누어 내부에서 전달받을 경우에는 카멜케이스로 외부로 전달할때는 객체 매핑을 통해 스네이크 케이스로 나누는 방식은 유비보수 비용이 높게 책정 된다고 생각합니다.

  • 물론 모든게 비동기 방식으로 처리 되는 경우에는 파라미터 형태를 JSON형식을 가정하고 ObjectMapper 혹은 Jackson 라이브러리의 어노테이션을 활용하면 됩니다.

    하지만 페이지 이동을 포함하는 API 작동 시 FormData 형식으로 넘어오는 경우 매핑이 쉽지 않다는 점이 있습니다.
  • 위의 문제에 대한 해결방안에 앞서 요청에 대한 간단한 원리를 설명드리겠습니다.




2. 요청 구조 이해

1-1. HttpServletRequest

Http 통신에서 요청 되는 정보를 해당 객체에 매핑하여 서버로 전달하는 역할을 수행하게 됩니다.
  • 이때 RequestHeader, 파라미터, 쿠키 등의 정보를 전달하는 매개변수로 역할을 수행합니다.
  • 여기서 주목해야 하는 부분은 RequestHeader입니다.

1-2. RequestHeader

Header에는 통신방식, 요청 주소, 브라우저 및 애플리케이션 정보 등이 들어있습니다.
  • 해당 필드에서 주목해야 하는 부분은 Accept입니다.

    • text/plain
    • application/json
    • application/x-www-form-urlencoded
  • 개발자 도구를 통해 디버깅 할 경우 이와 같은 형태를 자주 보셨을 겁니다.

  • 이때 application/x-www-form-urlencoded 타입으로 FormData가 넘어오게 된다면 ObjectMapper를 활용한 직렬화 의 경우 Map이든 객체로 받든 String 자료형을 기반으로 하는 JSON형태로 변환이 필요하며 어노테이션 방식의 경우 활용이 힘들게 됩니다.

  • 물론 내부 통신의 경우 프론트에서 header를 변환시켜 넘기면 해결 가능한 부분이지만 외부 API의 경우는 제어가 어려운 상황에 빠지게 됩니다.

  • 그럼 백엔드에서 해당 통신을 JSON으로 변환시켜야 하는데 이때 HttpMessageConverter 클래스를 활용하게 됩니다.


1-3. HttpMessageConverter

해당 클래스는 인터페이스 형태로 구성되어 있으며 헤더의 Accept부분과 Body를 종합하여 처리 방식을 선택하게 됩니다.
  • 그렇기 때문에 외부 API 호출 결과 값이 application/x-www-form-urlencoded 해당 방식으로 넘어오게 된다면 @RequestBody, @ResponseBody 어노테이션을 활용한 객체 매핑에 어려움이 발생합니다.

  • 이와 같은 문제 해결을 위해 해당 인터페이스를 상속받는 클래스커스텀 어노테이션을 생성하여 활용할 수 있습니다.




3. 어노테이션 생성

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFormConvert {

}
  • @Target : 해당 어노테이션의 적용 범위를 선택
  • @Retention : 해당 어노테이션의 수명 주기를 선택




4. 클래스 생성

import com.certpia.attachuser.annotation.JsonFormConvert;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.util.Map;


@Slf4j
public class JsonFormMessageConverter<T> extends AbstractHttpMessageConverter<T> {

    /* JSON 매핑 안된 속성 오류 무시 */
    private static final ObjectMapper objectMapper =
            new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    private static final FormHttpMessageConverter converter = new FormHttpMessageConverter();


    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.isAnnotationPresent(JsonFormConvert.class);
    }

    @NotNull
    @Override
    protected T readInternal(@NotNull Class<? extends T> clazz, @NotNull HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        Map<String, String> param = converter.read(null, inputMessage).toSingleValueMap();
        
        return objectMapper.convertValue(param, clazz);
    }

    @Override
    protected void writeInternal(@NotNull T t, @NotNull HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {

    }
}
  • HttpMessagConverter를 상속받은 구현 클래스인 AbstractHttpMessageConverter를 상속하여 필요한 메서드만 상속받도록 합니다.
  • supports 메서드에 위에서 생성한 어노테이션의 클래스를 clazz.isAnnotationPresent(JsonFormConvert.class) 로 등록합니다.
사실상 ObjectMapper가 여기서 생성되므로 해당 클래스에서 객체 매핑이 이루어져도 상관없다고 생각합니다. 다만 AbstractHttpMessageConverter 상속받아 JSON형태의 파싱역할만 수행하도록 하기 위하여 해당 과정은 구현하지 않았습니다.




5. Spring MVC 등록

1) Spring의 WebMvcConfigurer 구현

1-1) WebMvcConfigurer 인터페이스를 구현

  • 함으로써 Spring MVC의 기본 설정을 확장하거나 커스터마이징할 수 있습니다.

1-2) HTTP 메시지 변환기 확장 (extendMessageConverters)

  • extendMessageConverters(List<HttpMessageConverter<?>> converters) 메서드는 Spring MVC에서 요청과 응답을 처리할 때 사용하는 MessageConverter 목록을 확장하는 역할을 합니다.
  • 따라서 HttpMessageConverter의 커스텀 메시지 변환기를 추가하거나 기존 메시지 변환기를 수정할 수 있습니다.

1-3) JsonFormMessageConverter 커스텀 변환기 추가

  • converter 인스턴스 선언을 통해 특정 조건에서 application/x-www-form-urlencoded 데이터를 처리할 수 있는 메시지 변환기를 선언합니다.

1-4) MediaType 설정:

  • MediaType mediaType = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8);application/x-www-form-urlencoded 데이터를 UTF-8 인코딩 방식으로 처리할 수 있는 미디어 타입을 정의합니다.
  • 이 미디어 타입을 JsonFormMessageConverter에 설정하여, 해당 컨버터가 application/x-www-form-urlencoded 형식의 요청을 처리하도록 만듭니다.

1-5) 커스텀 변환기 목록에 추가

  • converters.add(converter);를 통해 커스텀 변환기 JsonFormMessageConverterSpring MVC의 메시지 변환기 목록에 추가합니다. 이를 통해 이 변환기가 등록되고 요청과 응답을 처리할 때 자동으로 이 변환기가 사용될 수 있게 됩니다.



2) 코드 구현

import com.certpia.attachuser.util.JsonFormMessageConverter;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.charset.StandardCharsets;
import java.util.List;

@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        JsonFormMessageConverter<?> converter = new JsonFormMessageConverter<>();
        MediaType mediaType = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8);
        converter.setSupportedMediaTypes(List.of(mediaType));
        converters.add(converter);
    }




6. 활용

1) 객체 생성

  • 해당 요청이 매핑될 객체에 어노테이션을 기입하면 적용이 완료됩니다.

1-1) @JsonNaming


import com.example.demo.annotation.JsonFormConvert;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@JsonFormConvert
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Member {
	private String name;
    private int age;
    private String userId;
    private String userPassword;
}

1-2) @JsonProperty


import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Member {

	private String name;
    private int age;
    
    @JsonProperty("user_id")
    private String userId;
    
    @JsonProperty("user_password")
    private String userPassword;
}



2) Controller

import org.springframework.stereotype.Controller;

@Controller
public class JoinController {
    
    @PostMappping("/join")
    public String join(@RequestBody Member member) {
   		system.out.println("객체 매핑 결과 확인 : ", member.toString());
    	return "success"
	}
}
profile
개발 학습

0개의 댓글