아는 사람만 아는 Spring MVC에서의 JSON 마셜링의 비밀

라모스·2022년 12월 22일
3

삽질.log

목록 보기
11/12
post-custom-banner

REST API의 메시지는 JSON 형식을 사용한다. 이 형식은 특정 플랫폼에 의존하지 않는 범용적인 데이터 교환 포맷이라서, 클라이언트가 사용하는 프로그래밍 언어와 플랫폼에 상관 없이 자유롭게 데이터를 주고받을 수 있다.

Spring 프레임워크를 사용하는 백엔드 개발자는 Java 언어를 사용하여 REST API를 개발하게 되는데, 우리가 사용하는 이 Java 객체를 JSON 메시지로 변경하는 과정을 마셜링이라 하고, 이 반대의 과정을 언마셜링이라 한다.

Spring Boot는 JSON 메시지를 처리하는 Jackson 라이브러리를 포함하고 있어 별도의 설정 없이 바로 사용 가능하며, 마셜링과 언마셜링을 위한 어노테이션과 클래스를 제공한다.

그렇다면, 도대체 마셜링이 뭔지 그 원리를 간략하게 파악하고 이를 활용하여 REST API 개발 시, JSON 메시지를 좀 더 효과적으로 다룰 수 있는 방법은 무엇이 있는지에 대해 정리하고자 한다.

RestController와 마셜링

@RestController를 사용하면 @Controller@ResponseBody의 기능을 동시에 사용하게 된다. 이 애노테이션이 선언된 해당 컨트롤러는 스프링 빈으로 등록되며, 이 컨트롤러가 리턴하는 객체들은 JSON 메시지 형태로 마셜링된다.

@ResponseBody의 기능을 사용하기 때문에 Spring MVC의 View를 사용하지 않는다.
그 대신 컨트롤러의 핸들러 메서드가 리턴하는 객체는 Spring MVC 프레임워크 내부에 설정된 여러 개의 HttpMessageConverter 중 적절한 것을 골라 변환할 대상 객체의 클래스 타입과 MediaType에 따라 특정 HttpMessageConverter를 선택하여 이 객체로 마셜링한다. 결과적으로 이 마셜링 된 JSON 객체가 클라이언트에 전달되는 것이다.

Spring Boot는 HttpMessageConverter 객체들을 자동 설정하기 위해 HttpMessageConvertersAutoConfiguration을 제공한다. 자동 설정으로 구성된 객체들 중 MappingJackson2HttpMessageConverter 구현체가 JSON 메시지로 마셜링되거나 언마셜링을 담당하는 것이다.

기본 설정으로 구성된 Spring MVC는 마셜링/언마셜링에 Jackson 라이브러리를 사용한다. ObjectMapperMappingJackson2HttpMessageConverter의 속성으로 포함되어 이를 처리한다.

ObjectMapper의 마셜링 기본 규칙

ObjectMapper가 객체를 마셜링하는 기본 규칙은 다음과 같다.

  • Java 객체는 JSON 객체로 변환되고, 컬렉션 객체는 JSON 배열로 변환된다.
  • Java 객체의 멤버 변수도 위 방식대로 변환된다. 속성이 객체이면 JSON 객체로, 컬렉션 객체라면 JSON 배열로 변환된다.
  • 객체의 멤버 변수 이름이 JSON 속성 이름으로 사용된다. Jackson 라이브러리는 JSON 속성 이름을 커스터마이징 할 수 있는 어노테이션들을 제공한다.
  • 마셜링 대상이 되는 객체 속성은 객체 외부에서 접근할 수 있어야 한다. 즉, private 키워드로 선언된 속성은 getter를 통해 외부에서 접근 가능해야 한다.
  • Java Map 객체의 entry 객체는 JSON 객체로 변환되는데, entry의 키가 JSON 객체의 이름이 되고, entry 값이 JSON 객체 값으로 변환된다.

@JsonProperty, @JsonSerialize

@RestController로 구현한 REST API는 클라이언트에게 객체를 전달한다. 보통 DTO 객체를 반환하는데 JSON 객체로 마셜링할 때 효과적인 방법의 예시를 가져왔다.

@Getter
public class MemberResponse {
    
    @JsonProperty("id")
    @JsonSerialize(using=ToStringSerializer.class)
    private final Long memberId;
    
    // ...
}

JSON 객체로 마셜링하는 과정에서 memberId 속성 이름 대신 다른 속성 이름을 사용하고 싶다면 @JsonProperty를 사용하면 된다. 이 어노테이션의 속성 값이 JSON 객체의 속성 이름이 된다.

또한, 이 변수의 Long 값을 String 타입으로 변경하고자 한다면, 위와 같이 @JsonSerializeToStringSerializer.class를 사용하면 된다. ToStringSerializer.class는 Jackson 라이브러리에서 기본으로 제공하는 클래스다.

DTO 객체의 속성 이름과 JSON 객체의 속성 이름이 다르다면 클래스의 속성이름을 변경하지 말고, @JsonProperty를 활용하는 것이 좋다.

객체의 속성 값을 마셜링 과정에서 적절한 형태의 데이터로 변경할 땐 @JsonSerialize를 활용하도록 하자. 특히, Java 애플리케이션에선 Long 타입의 값을 마셜링할 땐 문자열로 변환하는 것이 좋은데 이를 세트로 꼭 기억해두자.

REST API는 웹 브라우저와 같은 클라이언트도 사용 가능하다. 클라이언트의 경우 XHR(XmlHttpRequest) 객체를 사용하여 REST API를 호출하고 그 결과를 브라우저 화면에 렌더링한다. JavaScript의 숫자는 32bit 정수(interger)인 반면, Java의 Long이 표현할 수 있는 숫자는 64bit이다.

32bit를 넘는 Long 데이터 숫자가 JavaScript에 전달되면 오버플로우가 발생해 정확한 숫자를 처리할 수 없다. 이를 해결하기 위해 REST API에서 숫자타입은 문자열 값으로 변경해서 전달하는 것이 바람직하다.

Jackson 라이브러리에서 제공하는 Serializer 구현체들

  • ByteArraySerializer : byte[] 객체를 마셜링할 때 사용
  • CalanderSerializer : Calendar 객체를 마셜링할 때 사용
  • DateSerializer : Date 객체를 마셜링할 때 사용
  • CollectionSerializer : Collection 객체를 마셜링할 때 사용
  • NumberSerializer : Number 객체를 마셜링할 때 사용
  • TimeZoneSerializer : TimeZone 객체를 마셜링할 때 사용

JsonSerializer, JsonDeserializer

마셜링/언마셜링 과정에서 데이터를 변환 시, 복잡한 로직이 필요하거나 특별한 형태의 데이터로 변환하는 경우, JsonSerializerJsonDeserializer 구현체를 사용하여 기능을 확장할 수 있다. Jackson 라이브러리는 위 두 클래스를 추상 클래스로 제공하여 기능을 확장할 수 있게 한다. ToStringSerializerJsonSerializer의 구현체이다.

활용한 예제를 살펴보자.

@Getter
public class HotelRoomResponse {

	@JsonSerializer(using=ToDollarStringSerializer.class)
    private final BigDecimal originalPrice;
}
public class ToDollarStringSerializer extends JsonSerializer<BigDecimal> {
    
    @Override
    public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        gen.writeString(value.setScale(2).toString());
    }
}

JsonSerializer를 위와 같이 상속받아 변환할 대상의 클래스 타입을 제네릭 타입으로 설정한다. 위 예제는 달러 표시를 위해 BigDecimalsetScale(2) 메서드를 사용하여 소숫점 아래를 두 자리로 설정한 뒤 문자열을 반환하도록 하였다.

@JsonFormat

@JsonFormat 어노테이션은 java.util.Datejava.util.Calendar 객체를 사용자가 원하는 포맷으로 변경하는 역할을 한다.

@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")

위 처럼 shape와 pattern을 설정하여 사용하면 된다. Shape.String의 경우 마셜링 과정에서 해당 데이터를 String 타입으로 변경한다는 의미이다.

LocalDateTime을 포맷팅 하고 싶다면, 다음 글을 참고하도록 하자.
// 기억보단 기록을(향로님 블로그) - SpringBoot에서 날짜 타입 JSON 변환에 대한 오해 풀기

열거형 클래스 변환

enum 클래스는 마셜링할 때 enum 상수의 이름 그대로 변경된다. 마셜링 과정에서 상수를 변경할 때 toString() 메서드를 사용하고, 이 메서드가 enum 상수 이름을 리턴하기 때문이다.

따라서 응답 값과 enum 상수 값은 따로 분리하면 좋다. 다시 말해 SINGLE("single")과 같이 말이다.
만약 DTO 클래스 내부에서 switch문이나 if문을 통해 응답 값을 변경해야 하는 경우, @JsonValue 어노테이션을 사용하면 편리하게 변환할 수 있다.

public enum HotelRoomType {
    SINGLE("single"),
    DOUBLE("double"),
    TRIPLE("triple"),
    QUAD("quad");
    
    private static final Map<String, HotelRoomType> valueMap = 
        Arrays.stream(HotelRoomType.values())
        	  .collect(Collectors.toMap(
              	  HotelRoomType::getValue,
                  Function.identity()
              ));
              
    
    private final String value;
    
    HotelRoomType(String value) {
        this.value = value;
    }
    
    @JsonCreator
    public static HotelRoomType fromValue(String value) {
        return Optional.ofNullable(value)
                .map(valueMap::get)
                .orElseThrow(() -> new IllegalArgumentException("value is not valid"));
    }
    
    @JsonValue
    public String getValue() {
        return this.value;
    }
}

@JsonCreator은 언마셜링 과정에서 값 변환에 사용되는 메서드를 지정하는 어노테이션이다. fromValue()valueMap의 키와 일치하는 enum 상수를 가져와 응답한다.

@JsonValue는 마셜링 과정에서 enum을 문자열 변환하는 데 사용하는 toString()을 대신할 메서드를 설정하는 데 사용된다. 이를 통해 JSON의 속성 값으로 SINGLE이 아닌 single로 변경되는 식이 된다.

References

profile
Step by step goes a long way.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 1월 26일

포스팅 감사합니다! 문제 해결에 큰 도움 되었습니다👍

1개의 답글