본문 바로가기

Spring

@RequestBody로 Dto를 바인딩 할때 바인딩이 되지 않는 문제

상황

  • 회사에서 현재 업무 시 php -> Spring으로 플랫폼 전환을 진행하고 있으며 php Ajax요청을 Spring으로 전환하는
    업무였으며 php에서 똑같은 기능을 담당하는 코드들이 중복되는 현상을 발견해 Spring으로 Ajax 요청, 응답하도록
    기능을 전환하던 작업 중 발견

문제 상황

  • php에서 요청을 보내는 Ajax의 모든 본문 Key값들을 바꾸는 것은 개발 외적으로 시간 낭비가 많아 url 및 응답 변수만
    고치는 상황에서 php에서현상이 발견됐다.
  • Ajax로 요청보내는 본문의 응답 Key 값들이 모두 대문자로 요청을 보내는 상황에서 Spring에서 해당 key값으로 직렬화
    후 바인딩 하지 못하는 문제가 발생됐는데 그 현상은 하단의 사진과 같다.
@Data

public class TestDto{
	private String MEMBERID;
    private String MEMBERNAME;
    private String URL;
    private String MEMBERIMG;
}

  • 포스트맨에 요청할땐 하단처럼 본문을 구성하여 요청을 보냈다.
{
"MEMBERID" : "chulsu",
"MEMBERNAME" : "chulsu",
"URL" : "domain",
"MEMBERIMG" : "tmpImg"
}

 

포스트맨으로 상단과 같이 요청을 보내도 서버에서 로그를 확인하면 전부 바인딩 되지 않고 Null로만 찍히는 현상을 확인했다.

문제 원인

  • Spring에서 직렬화 역직렬화에서 사용하는 라이브러리에 대해서 먼저 알 필요가 있는데 Jackson 라이브러리를 사용해서 직렬화 및 바인딩을 진행할 때 생기는 복합적인 문제라고 할 수 있다.
  • 기본적으로 바인딩 시 dto의 키값과 요청본문의 Key값을 검증하여 해당 value값을 바인딩 하는 줄 알았지만
    Jackson 라이브러리는 Json 데이터를 getMethod의 이름으로 key값을 찾고 해당 내용을 직렬화 하기 때문에
    생기는 문제였다.
public class JacksonDto {
    private String Memname;

    public String getMemberName() {
        return Memname;
    }
    
    @Builder
    public class JacksonDto(String Memname){
    	this.Memname = Memname;
    }
}

JacksonDto jacksonDto = JacksonDto.builder().Memname("test").build();
String content = objectMapper.writeValueAsString(jacksonDto);

System.out.println(content);  // {"userName":"test"}
  • Jackson라이브러리는 이처럼 직렬화 시 get메서드의 이름으로 필드값을 직렬화 하기 때문에 개발자의 의도와는 다르게
    직렬화 혹은 null값이 나오는 현상이 발생되는 것이었다.

  •  Jackson라이브러리는 Json을 직렬화 할때 java beans 규약을 따르지만 상이한 차이점이 존재한다.
    • 1. 빈이 패키지화 되어 있어야 한다.
    • 2. 맴버 변수의 접근자는 private으로 지정한다.
    • 3. 맴버 변수에 접근하기 위한 Public 접근자인 getter/setter 메서드가 존재해야 한다.
    • 4. get메서드는 파라미터가 존재하지 않아야 한다.
    • 5. set 메서드는 반드시 하나 이상의 파라미터가 존재해야 한다.
  • 이러한 java beans 규약들은 프론트 / 백 각 진영간 분리하고 일관된 방식으로 자바 클래스를 개발하기 위해 만들어졌다.

Jackson의 직렬화 방식

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

Jackson 직렬화 방식 테스트

@Data
public class TestBindingDto {

    private String MEMBERID;
    private String MEMBERNAME;
    private String URL;
    private String MEMBERIMG;

    public String getMEMBERID(){
        return this.MEMBERID;
    }

    public String getMEMBERNAME(){
        return this.MEMBERNAME;
    }

    public String getURL(){
        return this.URL;
    }

    public String getMEMBERIMG(){
        return this.MEMBERIMG;
    }
}

@RestController
@RequestMapping("/test")
@Slf4j
public class testController {

    @PostMapping("/Attribute")
    public void testAttribute(@RequestBody TestBindingDto testBindingDto){

        log.info(testBindingDto.toString());
    }
}

이때 포스트맨으로는 

{
"memberid" : "chulsu",
"membername" : "chulsu",
"url" : "domain",
"memberimg" : "tmpImg"
}

상단의 형식대로 요청을 보내며 해당 요청 외 key의 대문자 형식 등이 다를때는 모두 null로만 출력된다.

  • 상단의 테스트를 보면 알 수 있듯 Jackson 라이브러리는 get메서드의 이름을 기준으로 직렬화시 key 바인딩을 시도하는데
    맨 앞 두글자가 모두 대문자인 get메서드이기 때문에 모두 소문자로 바꿔 인식해 요청 본문의 key를 모두 소문자 형식의
    key로 찾아 바인딩 하려고 시도하기 때문에 전부 소문자 외의 Key는 예외없이 null로 인식한다.
    • 상단의 테스트 시 필드 이름이 get메서드의 명명 규칙과 상관없이 진행되는건 @Data, @Value 어노테이션
      때문이며  이 어노테이션과 상관없이 get메서드를 만들거나 @Getter만 써서 진행하면 get메서드의 변경사항과
      필드 이름이 맞는지의 일치 여부를 판단하기 때문에 해당 사항을 잘 준수하여 진행해야 한다.
@NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode
public class TestBindingDto {

    private String memberId;
    private String memberName;
    private String URL;
    private String MEMBERIMG;

    public String getURL(){
        return this.URL;
    }

    public String getMemberID(){
        return memberId;
    }

    public String getMemberNAME(){
        return memberName;
    }


    public String getMemberIMG(){
        return MEMBERIMG;
    }
}

 

{
"memberID" : "chulsu",
"memberNAME" : "chulsu",
"url" : "domain",
"memberimg" : "mpImg"
}

  • 상단의 테스트를 보면 알 수 있듯 요청 본문의 key로 dto로 직렬화를 시도할 때 Jackson 라이브러리는 get메서드의
    이름을 기준으로 필드의 key와 매핑을 시도하려 하며 첫글자를 소문자로 바꾼 뒤 2번째까지 대문자로 이어지지 않기
    때문에 바인딩이 되는 모습을 확인할 수 있다.

해결 방안

  1. 직접 get메서드를 커스텀하여 메서드와 필드값을 일치시킨다면 Jackson라이브러리는 public으로 정의된 해당 메서드와
    필드를 보고 직렬화를 시도하기에 이런 문제에서 자유로울 수 있다.
  2. @JsonProperty()어노테이션을 사용하게 되면 Jackson라이브러리가 직렬화를 시도할 때 해당 속성으로 정의된 key값을
    보고 직렬화를 시도하기 때문에 java명명규칙에 상관없이 진행할 수 있다.
  3. @RequestBody로 Map 자료구조로 바인딩을 하게 된다면 요청 본문의 key, value를 그대로 map에 바인딩하며 이땐
    직렬화 당시 필드값 일치 여부를 검증하는 것이 아니기에 이 또한 해결책이 될 수 있다.

주의 사항

  • 테스트를 진행하며 확인했듯 @Data, @Value 어노테이션은 get메서드와 필드값이 다르다고 해도 개발자가 기대하는 것과
    달리 바인딩 하는 경우도 존재한다.
    url key로 요청을 받았을 때 URL 필드값에 바인딩 해주는 경우와 같다. 허니 이런 사항들을 주의하여 개발을 해야한다.
  • 필드값의 첫번째 글자가 대문자일 경우 요청을 받아 바인딩 하는 것이 대단히 어렵기 때문에 java 명명 규칙과
    Jackson라이브러리의 직렬화 방식을 생각하여 필드 이름과 get메서드의 이름을 정해 사용해야 한다.

참고 블로그
https://velog.io/@ssol_916/RequestBody%EB%A1%9C-%EB%B0%9B%EC%95%98%EB%8A%94%EB%8D%B0-null%EC%9D%B8-%EA%B2%BD%EC%9A%B0