REST API의 메시지는 JSON 형식을 사용한다. 이 형식은 특정 플랫폼에 의존하지 않는 범용적인 데이터 교환 포맷이라서, 클라이언트가 사용하는 프로그래밍 언어와 플랫폼에 상관 없이 자유롭게 데이터를 주고받을 수 있다.
Spring 프레임워크를 사용하는 백엔드 개발자는 Java 언어를 사용하여 REST API를 개발하게 되는데, 우리가 사용하는 이 Java 객체를 JSON 메시지로 변경하는 과정을 마셜링이라 하고, 이 반대의 과정을 언마셜링이라 한다.
Spring Boot는 JSON 메시지를 처리하는 Jackson 라이브러리를 포함하고 있어 별도의 설정 없이 바로 사용 가능하며, 마셜링과 언마셜링을 위한 어노테이션과 클래스를 제공한다.
그렇다면, 도대체 마셜링이 뭔지 그 원리를 간략하게 파악하고 이를 활용하여 REST API 개발 시, JSON 메시지를 좀 더 효과적으로 다룰 수 있는 방법은 무엇이 있는지에 대해 정리하고자 한다.
@RestController
를 사용하면 @Controller
와 @ResponseBody
의 기능을 동시에 사용하게 된다. 이 애노테이션이 선언된 해당 컨트롤러는 스프링 빈으로 등록되며, 이 컨트롤러가 리턴하는 객체들은 JSON 메시지 형태로 마셜링된다.
@ResponseBody
의 기능을 사용하기 때문에 Spring MVC의 View를 사용하지 않는다.
그 대신 컨트롤러의 핸들러 메서드가 리턴하는 객체는 Spring MVC 프레임워크 내부에 설정된 여러 개의 HttpMessageConverter
중 적절한 것을 골라 변환할 대상 객체의 클래스 타입과 MediaType
에 따라 특정 HttpMessageConverter
를 선택하여 이 객체로 마셜링한다. 결과적으로 이 마셜링 된 JSON 객체가 클라이언트에 전달되는 것이다.
Spring Boot는 HttpMessageConverter
객체들을 자동 설정하기 위해 HttpMessageConvertersAutoConfiguration
을 제공한다. 자동 설정으로 구성된 객체들 중 MappingJackson2HttpMessageConverter
구현체가 JSON 메시지로 마셜링되거나 언마셜링을 담당하는 것이다.
기본 설정으로 구성된 Spring MVC는 마셜링/언마셜링에 Jackson 라이브러리를 사용한다. ObjectMapper
가 MappingJackson2HttpMessageConverter
의 속성으로 포함되어 이를 처리한다.
ObjectMapper
가 객체를 마셜링하는 기본 규칙은 다음과 같다.
@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 타입으로 변경하고자 한다면, 위와 같이 @JsonSerialize
와 ToStringSerializer.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에서 숫자타입은 문자열 값으로 변경해서 전달하는 것이 바람직하다.
ByteArraySerializer
: byte[]
객체를 마셜링할 때 사용CalanderSerializer
: Calendar
객체를 마셜링할 때 사용DateSerializer
: Date
객체를 마셜링할 때 사용CollectionSerializer
: Collection
객체를 마셜링할 때 사용NumberSerializer
: Number
객체를 마셜링할 때 사용TimeZoneSerializer
: TimeZone
객체를 마셜링할 때 사용마셜링/언마셜링 과정에서 데이터를 변환 시, 복잡한 로직이 필요하거나 특별한 형태의 데이터로 변환하는 경우, JsonSerializer
와 JsonDeserializer
구현체를 사용하여 기능을 확장할 수 있다. Jackson 라이브러리는 위 두 클래스를 추상 클래스로 제공하여 기능을 확장할 수 있게 한다. ToStringSerializer
도 JsonSerializer
의 구현체이다.
활용한 예제를 살펴보자.
@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
를 위와 같이 상속받아 변환할 대상의 클래스 타입을 제네릭 타입으로 설정한다. 위 예제는 달러 표시를 위해 BigDecimal
의 setScale(2)
메서드를 사용하여 소숫점 아래를 두 자리로 설정한 뒤 문자열을 반환하도록 하였다.
@JsonFormat
어노테이션은 java.util.Date
나 java.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로 변경되는 식이 된다.
포스팅 감사합니다! 문제 해결에 큰 도움 되었습니다👍