본문 바로가기

트러블슈팅

필드명으로 인해 Spring Request DTO 에 null 값이 들어가는 이유

문제 상황

Spring Request DTO의 파라미터로 아래와 같이 gTotal이라는 필드를 만들었다.

import java.math.BigDecimal;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class TestDTO{
    
	private DocsPackingGroupTotal gTotal;
    
    @Getter
    @Setter
    public static class DocsPackingGroupTotal{
		private BigDecimal netWeight;
		private BigDecimal grossWeight;
    }
}

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
public class TestController {
	
	private final RequestDtoTestService requestDtoTestService;

	@PostMapping
    public ResponseEntity<Void> test(@RequestBody TestDTO param) throws Exception {
		
		requestDtoTestService.insert(param);
		
        return ResponseEntity.ok().build();
    }
}

 

@Service
@RequiredArgsConstructor
public class RequestDtoTestService {

	public void test(TestDTO dto) {
		System.out.println(dto.toString());		
	}	
}

 

아래와 같이 Request를 보내보니 DTO에 null이 들어왔다.

[POST] http://localhost:8080/test
{
    "gTotal":{
        "netWeight": 123,
        "grossWeight": 456
    }
}

 

 

원인 (Jackson의 Property 명명 규칙)

 

이유는 Spring에서 JSON과 DTO를 매핑해주는 Jackson 때문이었다.

 

Jackson은 DTO의 필드명이 아니라 get메서드의 이름을 가지고 Json Key를 만든다. 

 

이때 Jackson이 Json 프로퍼티를 만드는 규칙은 아래와 같다.

  1. 맨 앞 두 글자가 모두 대문자인 경우 이어진 대문자를 모두 소문자로 변경한다.
  2. 나머지 모든 케이스에서는 맨 앞 글자만 소문자로 바꿔준다.

Lombok의 @Getter를 사용했기 때문에 gTotal의 get메서드는 아래와 같이 자동으로 만들어진다.

Lombok은 단순히 필드명의 맨 앞 글자만 대문자로 변경하고 get과 set 메서드를 만든다.

    public DocsPackingGroupTotal getGTotal() {
    	return this.gTotal;
    }

 

Jakson은 getGTotal을 가지고 JSON Key를 만드니까 JSON Key는 gTotal이 아니라 gtotal이 된다. 

Request의 JSON에서 gTotal에서 gtotal로 변경하면 값이 잘 들어간 것을 확인할 수 있다.

 

[POST] http://localhost:8080/test
{
    "gtotal":{
        "netWeight": 123,
        "grossWeight": 456
    }
}

 

해결방법

 

필드명을 변경하거나, @JsonProperty("Json 필드 이름")를 사용하면 된다.

 

Jakson 매핑을 프로퍼티가 아닌 멤버변수로 할 수 있기 위해 Jakson은 @JsonProperty 어노테이션 API를 제공한다.

@JsonProperty는 Jackson에게 JSON의 JSON Key를 @JsonProperty 어노테이션이 달린 Java 필드 이름에 매핑하도록 지시한다.

 

아래와 같이 @JsonProperty("Json 필드 이름")으로 어노테이션을 DTO 필드에 붙이면 된다. 

@Getter
@Setter
@NoArgsConstructor
public class TestDTO{
    
	@JsonProperty("gTotal") //JSON의 gTotal이 DTO의 gTotal과 매핑된다. 
	private DocsPackingGroupTotal gTotal;
	
    @Getter
    @Setter
    public static class DocsPackingGroupTotal{
		private BigDecimal netWeight;
		private BigDecimal grossWeight;
    }
    
}

 

[POST] http://localhost:8080/test
{
    "gTotal":{
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

 

 

여기서 주의할 점은 @JsonProperty("Json 필드 이름")으로 사용해야지, @JsonProeprty만 붙이면 안된다. 

@JsonProperty만 붙이면 조회 시 gTotal과 gtotal 두개가 나오게 된다. 

왜냐하면 @JsonProperty를 붙여준 gTotal도 getGTotal도 모두 JSON Key로 Jackson이 만들기 때문이다. 

 

아래와 같이 @JsonProperty만 붙이고 요청을 보내보면 gTotal과 gtotal이 모두 응답으로 오는 것을 확인할 수 있다.

 

@Getter
@Setter
@NoArgsConstructor
public class TestDTO{
    
	@JsonProperty
	private DocsPackingGroupTotal gTotal;
	
    @Getter
    @Setter
    public static class DocsPackingGroupTotal{
		private BigDecimal netWeight;
		private BigDecimal grossWeight;
    }
    
}

 

@Service
@RequiredArgsConstructor
public class RequestDtoTestService {

	public TestDTO test(TestDTO dto) {		
		System.out.println(dto.toString());				
		return dto;
	}	
}

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
public class TestController {
	
	private final RequestDtoTestService requestDtoTestService;
	
	@PostMapping
    public ResponseEntity<TestDTO> test(@RequestBody TestDTO param) throws Exception {
        return ResponseEntity.ok().body(requestDtoTestService.test(param));
    }
}

 

Request

[POST] http://localhost:8080/test
{
    "gTotal":{
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

Response

{
    "gTotal": {
        "netWeight": 9999,
        "grossWeight": 10000
    },
    "gtotal": {
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

@JsonIgnore

참고로, Jackson은 필드명이 아닌 get메서드의 이름을 가지고 JSON Key를 만드므로, 아래와 같이 필드명 없이 get 메서드만 존재해도 ReseponseBody에 나올 수 있다.

@Getter
@Setter
@NoArgsConstructor
public class TestDTO{
    
	@JsonProperty("gTotal") 
	private DocsPackingGroupTotal gTotal;
	
    @Getter
    @Setter
    public static class DocsPackingGroupTotal{
		private BigDecimal netWeight;
		private BigDecimal grossWeight;
    }
    
    // get 메서드만 존재
    public List<String> getRequiredFields(){
    	List<String> requiredFields = new ArrayList<>();
    	requiredFields.add("Required Field 1");
    	requiredFields.add("Required Field 2");
    	
    	return requiredFields;    	
    }    
}

 

Request

[POST] http://localhost:8080/test
{
    "gTotal":{
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

Response

{
    "requiredFields": [
        "Required Field 1",
        "Required Field 2"
    ],
    "gTotal": {
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

만약 get메서드가 ResponseBody에 나오는 것을 원치 않는다면, get메서드 위에 @JsonIgnore을 붙여주면 된다.

@Getter
@Setter
@NoArgsConstructor
public class TestDTO{
    
	@JsonProperty("gTotal") 
	private DocsPackingGroupTotal gTotal;
	
    @Getter
    @Setter
    public static class DocsPackingGroupTotal{
		private BigDecimal netWeight;
		private BigDecimal grossWeight;
    }
    
    //@JsonIgnore을 붙이면 Jackson이 JSON key만들 때 무시한다.
    @JsonIgnore
    public List<String> getRequiredFields(){
    	List<String> requiredFields = new ArrayList<>();
    	requiredFields.add("Required Field 1");
    	requiredFields.add("Required Field 2");
    	
    	return requiredFields;    	
    }    
}

 

그럼 아래와 같이 Response에 requiredFields가 포함되지 않는다.

Response

{
    "gTotal": {
        "netWeight": 9999,
        "grossWeight": 10000
    }
}

 

참조

https://bcp0109.tistory.com/309

 

Spring Request DTO 에 null 값이 들어가는 이유 (Jackson, Lombok)

Overview Spring Boot 로 REST API 를 테스트 하다가 이상한 이슈에 직면했습니다. 클라이언트에서 @RequestBody 로 요청을 받기 위한 DTO 클래스를 만들고 값을 입력 받았는데 null 값이 입력되는 겁니다. 처

bcp0109.tistory.com