앞선 글에서 우리는 Spring 기반 웹 애플리케이션에서 자바 객체가 JSON으로 직렬화되는 과정을 살펴보았습니다. 이번에는 그 반대 과정, 즉 JSON 데이터가 자바 객체로 역직렬화되는 과정을 자세히 알아보겠습니다. 이 과정 역시 주로 Jackson 라이브러리에 의해 처리되며, Spring Framework와 긴밀히 통합되어 있습니다.
역직렬화 과정
1. 요청 수신 및 컨트롤러 매핑
클라이언트로부터 JSON 데이터를 포함한 HTTP 요청이 서버에 도착하면 다음과 같은 과정을 거칩니다:
- 클라이언트는 일반적으로 Content-Type 헤더에 "application/json"을 포함하여 JSON 데이터를 전송합니다.
- 서버의 서블릿 컨테이너(예: Tomcat)가 요청을 받아 Spring의 DispatcherServlet에 전달합니다.
- 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를 사용하여 처리합니다.
이 프로세서는 다음 작업을 수행합니다:
- @RequestBody 어노테이션 확인
- 파라미터의 타입 분석
- Content-Type 확인
3. HttpMessageConverter 선택
Spring은 등록된 여러 HttpMessageConverter 중에서 적절한 컨버터를 선택합니다. JSON 요청의 경우 일반적으로 MappingJackson2HttpMessageConverter가 선택됩니다.
선택 과정은 다음과 같습니다:
- 등록된 HttpMessageConverter 순회
- 각 컨버터의 canRead(Class<?>, MediaType) 메소드 호출
- MediaType 일치 여부 확인
- 조건을 만족하는 첫 번째 컨버터 선택
4. ObjectMapper 사용
선택된 MappingJackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 실제 역직렬화를 수행합니다.
주요 과정은 다음과 같습니다:
- ObjectMapper의 readValue() 메소드 호출
- JsonParser를 사용한 JSON 입력 처리
- DeserializationContext 설정
- JavaType 객체 생성 및 적절한 JsonDeserializer 선택
- BeanDeserializer를 통한 객체 속성 설정
```java
ObjectMapper objectMapper = new ObjectMapper();
User user = objectMapper.readValue(jsonString, User.class);
```
이는 ObjectMapper를 직접 사용하는 예제입니다. Spring에서는 이 과정이 내부적으로 처리됩니다.
5. JSON 파싱 및 객체 생성
Jackson은 JSON 데이터를 파싱하여 Java 객체를 생성합니다. 이 과정에서 다음과 같은 작업이 수행됩니다:
- JSON 토큰 읽기
- 객체의 기본 생성자 호출 또는 @JsonCreator 어노테이션이 붙은 생성자 사용
- JSON 필드와 Java 객체의 속성 매핑
- 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 역직렬화 과정에서 개발자들이 자주 마주치는 문제와 그 해결책을 살펴보겠습니다:
- 생성자와 setter 메소드
Jackson은 기본적으로 기본 생성자 또는 setter 메소드를 사용하여 객체를 생성하고 값을 설정합니다. 이들이 없으면 역직렬화에 실패할 수 있습니다.
해결책:
- 기본 생성자 추가
- @JsonCreator와 @JsonProperty 사용
- Jackson의 visibility 설정 변경
- 알 수 없는 속성 처리
JSON에 Java 클래스에 정의되지 않은 필드가 있을 경우 UnrecognizedPropertyException이 발생할 수 있습니다.
해결책:
- @JsonIgnoreProperties(ignoreUnknown = true) 사용
- ObjectMapper 설정 변경
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
// ...
}
- 다형성 객체 처리
추상 클래스나 인터페이스의 구현체를 역직렬화할 때 타입 정보가 필요할 수 있습니다.
해결책:
- @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 { }
- 순환 참조
양방향 관계가 있는 객체를 역직렬화할 때 순환 참조로 인한 문제가 발생할 수 있습니다.
해결책:
- @JsonManagedReference와 @JsonBackReference 사용
- DTO 패턴 적용
- 날짜/시간 형식
날짜와 시간 데이터의 형식이 일치하지 않으면 파싱 오류가 발생할 수 있습니다.
해결책:
- @JsonFormat 사용
- 커스텀 Deserializer 구현
public class Event {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date eventDate;
}
- 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의 버전 관리도 용이해집니다.
참고 자료:
'JAVA > SPRING' 카테고리의 다른 글
Spring과 Jackson: JSON 직렬화의 내부 동작 원리 (1) | 2024.09.20 |
---|---|
@Transactional(readOnly = true)를 항상 써야할까? (0) | 2024.08.03 |