본문 바로가기

Spring

[Spring] JSON 매핑 오류와 Java, Kotlin에서의 DTO 구현

Spring Boot 환경에서 프로젝트를 진행하다 보면 컨트롤러와 DTO 간의 JSON 매핑이 실패하는 경우가 간혹 있습니다. 이 글에서는 매핑이 실패하는 이유와 Java, Kotlin에서 각각 DTO를 어떻게 구현하면 되는지 정리해보겠습니다.


 

기본 개념

Controller에서 DTO를 주고받을 때 JSON 직렬화역직렬화가 발생합니다:

  • 직렬화 (Serialization): Java 객체를 JSON 형식으로 변환합니다.
    • @ResponseBody 사용 시 발생하며, 서버에서 클라이언트로 데이터를 전송할 때 사용됩니다.
  • 역직렬화 (Deserialization): JSON 데이터를 Java 객체로 변환합니다.
    • @RequestBody 사용 시 발생하며, 클라이언트에서 서버로 데이터를 전송할 때 사용됩니다.

Spring Boot는 Jackson 라이브러리를 통해 직렬화와 역직렬화를 처리하며, 내부적으로 ObjectMapper가 리플렉션을 통해 필드에 접근합니다.

직렬화와 역직렬화 시 필드 접근 조건

  • 직렬화: getter가 필요하며, 기본 생성자는 필요하지 않습니다.
  • 역직렬화: getter 혹은 setter가 필요하며 기본 생성자가 필요합니다.

기본 생성자가 없는 경우 Jackson에 필드 매핑 방법을 알려주는 어노테이션을 사용해 오류를 방지할 수 있습니다.

  • @JsonProperty: JSON 프로퍼티와 필드, 메서드의 매핑을 지정합니다.
  • @JsonAutoDetect: 접근 수준을 설정해 Jackson이 접근할 수 있는 필드와 메서드를 지정합니다.
  • @JsonCreator: 생성자를 통해 역직렬화할 때 사용됩니다.

Java: 

성공 예시


/*
 * 1. 생성자가 정의되지 않은 경우
 *    컴파일러가 기본 생성자를 자동으로 추가하므로 성공
 */
@Getter
public class UserRequest {
    private String name;
}

/*
 * 2. 기본 생성자를 명시한 경우 성공
 */
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserRequest {
    private String name;
}

/*
 * 3. 기본 생성자가 없더라도 어노테이션을 통해 매핑을 설정하여 성공
 */
@Getter
public class UserRequest {
    private String name;

    @JsonCreator
    public UserRequest(String name) {
        this.name = name;
    }
}

실패 예시

// 매개변수가 있는 생성자만 있고 기본 생성자가 없어 매핑 실패
@Getter
@AllArgsConstructor
public class UserRequest {
    private String name;
}

 

위와 같은 실패를 방지하기 위해 @Data 어노테이션을 사용하여 구현하는 사례도 존재합니다.

record 타입을 사용한 DTO 구현

Java 14부터는 record 타입을 사용하여 DTO를 구현할 수 있습니다.

record 타입의 특징:

  • 자동 생성 메서드: getter, equals(), hashCode(), toString() 메서드를 자동으로 생성하여 반복적인 코드 작성을 줄여줍니다.
  • 불변성: 모든 필드가 final로 선언되어 객체의 상태를 외부에서 변경할 수 없습니다.
  • 간결한 코드: DTO 특성을 가진 클래스를 개발할 때 필요한 메서드를 자동으로 생성해 주어 반복적인 작업을 줄입니다.
public record UserRequest(String name) {}

 

이처럼 record 타입을 활용하면 DTO 구현의 효율성과 가독성을 높일 수 있습니다.


Kotlin: 

Kotlin에서는 기본 생성자를 명시하지 않아도 DTO가 올바르게 JSON 데이터를 매핑합니다.

data class UserRequest(val name: String)

 

Kotlin에서는 data class를 사용해 JSON 데이터를 매핑할 수 있습니다. jackson-module-kotlin 모듈을 의존성에 추가해주면 기본 생성자 없이도 Spring Boot에 내장된 Jackson이 Kotlin의 data class와 호환됩니다.

 compileOnly("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+")

 

이 모듈로 인해 역직렬화 시 필요한 필드를 주 생성자 프로퍼티에 자동으로 매핑하여 별도의 설정 없이 간편하게 사용할 수 있습니다.

일반 클래스와 data class의 차이

꼭 data class로만 DTO를 정의해야 JSON 매핑이 동작하는 것은 아닙니다. 일반 클래스도 주 생성자에 필드를 정의하고 있다면 JSON 매핑이 정상적으로 동작합니다. 일반 클래스와 달리 data class는 컴파일 시 final 클래스로 생성되어 불변성을 유지할 수 있습니다. 또한, data class는 데이터 저장을 목적으로 설계된 클래스이기 때문에 DTO로 사용하기에 더 적합합니다.


결론

  1. Spring 환경에서 DTO는 기본적으로 기본 생성자가 필요하며, 없을 경우 매핑 시 오류가 발생할 수 있습니다.
  2. Java 14 이상의 환경에서는 record를 적극 활용하여 불필요한 코드를 줄이고, 자동 생성 메서드를 통해 가독성을 높일 수 있습니다.
  3. Kotlin 프로젝트에서는 jackson-module-kotlin 모듈을 의존성에 추가하고 data class를 사용하여 간결하고 효율적으로 DTO를 구현할 수 있습니다.

References :