앞선 글에서 우리는 Spring 기반 웹 애플리케이션에서 자바 객체가 JSON으로 직렬화되는 과정을 살펴보았습니다. 이번에는 그 반대 과정, 즉 JSON 데이터가 자바 객체로 역직렬화되는 과정을 자세히 알아보겠습니다. 이 과정 역시 주로 Jackson 라이브러리에 의해 처리되며, Spring Framework와 긴밀히 통합되어 있습니다.

역직렬화 과정

1. 요청 수신 및 컨트롤러 매핑

클라이언트로부터 JSON 데이터를 포함한 HTTP 요청이 서버에 도착하면 다음과 같은 과정을 거칩니다:

  1. 클라이언트는 일반적으로 Content-Type 헤더에 "application/json"을 포함하여 JSON 데이터를 전송합니다.
  2. 서버의 서블릿 컨테이너(예: Tomcat)가 요청을 받아 Spring의 DispatcherServlet에 전달합니다.
  3. DispatcherServlet은 HandlerMapping을 통해 적절한 컨트롤러 메소드를 찾습니다.
@RestController  
public class UserController {  
    @PostMapping("/user")  
    public User createUser(@RequestBody User user) {  
        return userService.save(user);  
    }  
}  

이 예제에서 `/user` 경로로 POST 요청이 오면, `createUser` 메소드가 실행됩니다.

2. @RequestBody 처리

Spring MVC는 메소드 파라미터에 @RequestBody 어노테이션이 있는 경우 RequestResponseBodyMethodProcessor를 사용하여 처리합니다.

이 프로세서는 다음 작업을 수행합니다:

  1. @RequestBody 어노테이션 확인
  2. 파라미터의 타입 분석
  3. Content-Type 확인

3. HttpMessageConverter 선택

Spring은 등록된 여러 HttpMessageConverter 중에서 적절한 컨버터를 선택합니다. JSON 요청의 경우 일반적으로 MappingJackson2HttpMessageConverter가 선택됩니다.

선택 과정은 다음과 같습니다:

  1. 등록된 HttpMessageConverter 순회
  2. 각 컨버터의 canRead(Class<?>, MediaType) 메소드 호출
  3. MediaType 일치 여부 확인
  4. 조건을 만족하는 첫 번째 컨버터 선택

4. ObjectMapper 사용

선택된 MappingJackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 실제 역직렬화를 수행합니다.

주요 과정은 다음과 같습니다:

  1. ObjectMapper의 readValue() 메소드 호출
  2. JsonParser를 사용한 JSON 입력 처리
  3. DeserializationContext 설정
  4. JavaType 객체 생성 및 적절한 JsonDeserializer 선택
  5. BeanDeserializer를 통한 객체 속성 설정

```java
ObjectMapper objectMapper = new ObjectMapper();
User user = objectMapper.readValue(jsonString, User.class);
```

이는 ObjectMapper를 직접 사용하는 예제입니다. Spring에서는 이 과정이 내부적으로 처리됩니다.

5. JSON 파싱 및 객체 생성

Jackson은 JSON 데이터를 파싱하여 Java 객체를 생성합니다. 이 과정에서 다음과 같은 작업이 수행됩니다:

  1. JSON 토큰 읽기
  2. 객체의 기본 생성자 호출 또는 @JsonCreator 어노테이션이 붙은 생성자 사용
  3. JSON 필드와 Java 객체의 속성 매핑
  4. setter 메소드 호출 또는 필드에 직접 값 설정
public class User {  
    private String name;  
    private int age;  

    @JsonCreator  
    public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {  
        this.name = name;  
        this.age = age;  
    }  

    // getters and setters  
}  

이 예제에서 Jackson은 @JsonCreator 어노테이션이 붙은 생성자를 사용하여 객체를 생성합니다.

6. 타입 변환

JSON의 데이터 타입과 Java 객체의 필드 타입이 다른 경우, Jackson은 적절한 타입 변환을 수행합니다.

  • 문자열 → 날짜
  • 숫자 → Enum
  • 문자열 → UUID 등
public class Event {  
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    private Date eventDate;  

    @JsonDeserialize(using = CustomEnumDeserializer.class)  
    private EventType type;  

    // getters and setters  
}  

이 예제에서 `eventDate`는 지정된 패턴에 따라 문자열에서 Date 객체로 변환되며, `type`은 커스텀 Deserializer를 통해 변환됩니다.

7. 유효성 검증

Spring의 Validator 또는 Bean Validation(예: @Valid)을 사용하여 생성된 객체의 유효성을 검증할 수 있습니다.

@PostMapping("/user")  
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {  
    // ...  
}  

이 예제에서 @Valid 어노테이션은 User 객체의 유효성 검증을 트리거합니다.

개발자가 주의해야 할 사항

JSON 역직렬화 과정에서 개발자들이 자주 마주치는 문제와 그 해결책을 살펴보겠습니다:

  1. 생성자와 setter 메소드

Jackson은 기본적으로 기본 생성자 또는 setter 메소드를 사용하여 객체를 생성하고 값을 설정합니다. 이들이 없으면 역직렬화에 실패할 수 있습니다.

해결책:

  • 기본 생성자 추가
  • @JsonCreator와 @JsonProperty 사용
  • Jackson의 visibility 설정 변경
  1. 알 수 없는 속성 처리

JSON에 Java 클래스에 정의되지 않은 필드가 있을 경우 UnrecognizedPropertyException이 발생할 수 있습니다.

해결책:

  • @JsonIgnoreProperties(ignoreUnknown = true) 사용
  • ObjectMapper 설정 변경
@JsonIgnoreProperties(ignoreUnknown = true)  
public class User {  
    // ...  
}  
  1. 다형성 객체 처리

추상 클래스나 인터페이스의 구현체를 역직렬화할 때 타입 정보가 필요할 수 있습니다.

해결책:

  • @JsonTypeInfo와 @JsonSubTypes 사용
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")  
@JsonSubTypes({  
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),  
    @JsonSubTypes.Type(value = Cat.class, name = "cat")  
})  
public abstract class Animal { }  
  1. 순환 참조

양방향 관계가 있는 객체를 역직렬화할 때 순환 참조로 인한 문제가 발생할 수 있습니다.

해결책:

  • @JsonManagedReference와 @JsonBackReference 사용
  • DTO 패턴 적용
  1. 날짜/시간 형식

날짜와 시간 데이터의 형식이 일치하지 않으면 파싱 오류가 발생할 수 있습니다.

해결책:

  • @JsonFormat 사용
  • 커스텀 Deserializer 구현
public class Event {  
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    private Date eventDate;  
}  
  1. Immutable 객체 처리

final 필드를 가진 Immutable 객체의 경우 일반적인 역직렬화 방식으로는 처리가 어려울 수 있습니다.

해결책:

  • @JsonCreator와 @JsonProperty 조합 사용
  • Builder 패턴과 함께 @JsonDeserialize 사용
@JsonDeserialize(builder = ImmutableUser.Builder.class)  
public class ImmutableUser {  
    private final String name;  
    private final int age;  

    private ImmutableUser(Builder builder) {  
        this.name = builder.name;  
        this.age = builder.age;  
    }  

    public static class Builder {  
        private String name;  
        private int age;  

        @JsonProperty("name")  
        public Builder withName(String name) {  
            this.name = name;  
            return this;  
        }  

        @JsonProperty("age")  
        public Builder withAge(int age) {  
            this.age = age;  
            return this;  
        }  

        public ImmutableUser build() {  
            return new ImmutableUser(this);  
        }  
    }  
}  

결론

Spring과 Jackson의 조합은 JSON 데이터의 역직렬화 과정을 매우 유연하고 강력하게 만듭니다. 그러나 이 과정을 깊이 이해하고 있으면, 복잡한 데이터 구조를 다룰 때 발생할 수 있는 다양한 문제들을 효과적으로 해결할 수 있습니다.

특히 도메인 모델이 복잡한 경우, API 계층에서는 DTO 패턴을 사용하여 역직렬화를 단순화하고, 도메인 객체와 API 요청/응답을 명확히 분리하는 것이 좋습니다. 이를 통해 역직렬화 관련 문제를 더 쉽게 관리하고, API의 버전 관리도 용이해집니다.

참고 자료:

  1. Spring Framework Documentation
  2. Jackson Project Documentation
  3. Java Reflection API Documentation
  4. HTTP Specification (RFC 7231)

+ Recent posts