개발/Spring MVC

@RequestBody 직렬화와 역직렬화 눈으로 확인하기

Debin 2024. 5. 30.
반응형

자바 스프링에서 @RequestBody 직렬화, 역직렬화에 대해 알아보겠다.

 

Java에서 @RequestBody

필자는 요청 바디에 json 객체를 담아 요청했다.

따라서 헤더의 content-type은 application/json이다.

 

@RequestBody를 사용하면 RequestResponseBodyMethodProcessor가 HTTP 요청을 우리가 정의한 자바 객체로 변환해준다.

 

그럼 어떻게 HTTP 요청을 자바 객체로 변환해주는지 살펴보자.

 

실습 코드는 아래와 같다.

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ExampleController {

    @PostMapping("/json")
    public void json(@RequestBody Request request) {
        System.out.println("request = " + request);
    }

    static class Request {
        private String message;
        private int age;

        public Request(String message, int age) {
            this.message = message;
            this.age = age;
        }

        public String getMessage() {
            return message;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "Request{" +
                    "message='" + message + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

 

스프링은 별도로 설정을 하지 않는한 요청 바디에서 읽거나 쓰는 경우  MappingJackson2HttpMessageConverter를 사용한다.

해당 코드는 내부적으로 ObjectMapper를 활용한다.

 

객체를 생성하는 순서는 크게 다음과 같다.

 

1. 먼저 @RequestBody 애노테이션이 붙은 클래스 타입을 파악한다.

RequestResponseBodyMethodProcessor.resolveArgument가 파라미터로 받는 parameter에서 클래스를 파악할 수 있다.

이후 요청에서 객체를 만들 때 여기서 얻은 클래스 타입을 계속 활용한다. 

RequestResponseBodyMethodProcessor 클래스 내부 코드

2. 생성자가 존재하는 경우 BeanDeserializer 클래스에서 요청 바디에서 각 필드의 값을 역직렬화해 PropertyValueBuffer 객체에 저장한다.

BeanDeserializer 클래스 코드

 

이후 리플렉션 기술인 java.lang.reflect.Constructor을 사용해 요청 객체를 생성하고 이를 리턴한다.

 

StdValueInstantiator 내부 코드

3. Getter만 있는 경우에도 동일하게 BeanDeserializer 클래스를 사용하지만 deserializeFromObject라는 메서드를 사용한다.

빨간 상자를 친 부분에서 기본 생성자를 통해 요청 객체를 생성한다.

 

빨간 상자를 친 부분에서 기본 생성자를 통해 요청 객체를 생성한다.

 

 

이후에 필드들을 추출해 java.lang.reflect.Field 클래스의 set 메서드를 사용해 필드 값을 주입한다.

아래 빨간 상자를 친 메서드 내부에서 로직이 동작한다.

 

이렇게 직접 input을 읽어서 객체로 매핑하는 과정을 살펴보았다. 

 

여기까지는 요청 클래스에 관한 얘기이다.

응답 클래스의 경우는 생성자는 사용하지않는다. 응답 값을 직렬화할 때는 게터만 필요하다.

 

추가적으로 public 인스턴스 변수를 사용하면 게터를 사용하지 않아도 된다! 리플렉션이 문제 없이 적용되기 때문이다.

사실 인스턴스 변수를 public으로 작성할 일은 아예 없다고 본다.

따라서 결론은 Getter를 꼭 작성해야 한다는 것이다.

 

그럼 이번에는 코틀린의 경우를 살펴보겠다.

 

Kotlin에서 @RequestBody

예시 코드는 다음과 같다.

 

data class와 class 모두 동일하게 생각하면 된다.

코틀린은 변수를 작성하면 Getter, Setter가 컴파일러에 의해 작성된다.

import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class LabController {

    @PostMapping("/json")
    fun json(@RequestBody request: Request): Request {
        println("request = ${request}")
        return request
    }
}

//data class가 아닌 class도 동일
data class Request(
    val message: String,
    val age: Int
)

 

따라서 요청 역직렬화, 응답 직렬화에 문제가 없다.

코틀린 바이트 코드를 다시 자바로 디컴파일 하면 해당 코드를 찾을 수 있다.

 

 

참고로 아래와 같이 코드를 작성하면 getter이 자동으로 생성되지 않아서 요청의 역직렬화는 가능하지만 응답의 직렬화는 실패한다.

//단순 class도 동일
data class Request(
    private val message: String,
    private val age: Int
)

 

참고로 아래와 같이 직접 게터를 작성하면 응답의 직렬화도 통과한다.

 

data class Request(
    private val message: String,
    private val age: Int
) {
    fun getMessage() = this.message
    fun getAge() = this.age
}

 

 

이상으로 포스팅을 마칩니다. 감사합니다.

반응형

댓글