ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot - Jackson 라이브러리를 이용한 데이터 바인딩(2)
    Framework & Library/Spring Boot 2023. 2. 10. 13:51

    Jackson 라이브러리를 이용한 데이터 바인딩(2)

    지난 게시글에서는 getter(), setter() 메서드의 유무 또는 형태에 따라 직렬화, 역직렬화가 어떻게 이루어지는지 알아보았다. 이번 게시글에서는 생성자에서 데이터 바인딩이 어떻게 이루어지는지 여러 가지 상황을 통해 알아보겠다.

     

    1. 기본적인 형태
    @Getter
    @Stter
    public class Member {
        private Long id;
        private String name;
        private String address;
        private String email;
        
        public Member() {
            this.id = 0L;
            this.name = "No Name";
            this.address = "No Address";
            this.email = "No Email";
        }
        
        public Member(Lond id, String name, String address, String email) {
            this.id = id;
            this.name = name;
            this.address = address;
            this.email = email;
        }
    }

    위 예제 코드는 기본 생성자, 모든 필드를 인자로 담은 생성자, getter(), setter() 메서드 모두 존재하는 형태이다. 여기서는 당연히 직렬화 및 역직렬화 둘 다 잘 이루어질 것이다. 위와 같은 기본적인 형태보다 좀 더 특수한 형태를 한 번 살펴보겠다.

     

    2. 기본 생성자만 없는 경우

    2-1. ObjectMapper를 통해 JSON 데이터를 역직렬화하는 경우

    @Getter
    @Stter
    public class Member {
        private Long id;
        private String name;
        private String address;
        private String email;
        
        public Member(Lond id, String name, String address, String email) {
            this.id = id;
            this.name = name;
            this.address = address;
            this.email = email;
        }
    }

    위 Member 클래스는 기본 생성자만 없는 형태의 클래스이다.

     

    @Test
    @DisplayName("기본 생성자만 없는 경우 - ObjectMapper")
    public void test01() throws JsonProcessingException {
        Member member = mapper.readValue(json.toString, Member.class);   // JSON 데이터 -> POJO 객체
        logger.info(member.toString);
    }

    기본 생성자만 없는 경우 역직렬화가 잘 이루어지는지 테스트를 해보았다. 위와 같이 테스트를 진행할 경우 InvalidDefinitionException 에러가 발생하면서 테스트를 실패하게 된다.

     

    테스트가 실패한 이유는 다음과 같다. 비록, Member 클래스는 모든 필드를 포함하는 생성자를 가지고 있지만, ObjectMapper는 해당 생성자를 사용하여 바인딩하라는 지시를 받지 않는 이상 기본 생성자를 통해 객체를 생성하고, setter() 메서드를 통해 데이터를 주입한다. 이러한 이유 때문에, 위 예제에서는 생성자가 없다는 내용과 함께 에러를 던지게 되는 것이다.

     

    2-2. API Request를 통해 JSON 데이터를 역직렬화하는 경우

    @PostMapping("/api/member")
    public Member insertMember(@RequestBody Member member) {
        logger.info(member.toString());
        return member;
    }

    기본 생성자가 없는 경우 ObjectMapper를 통해 JSON을 역직렬화할 때는 에러를 발생시키는 것을 확인했다. 하지만, 위와 같이 Spring Boot Application 내 API Request를 통해 JSON을 역직렬화하는 경우에는 다른 결과가 나온다.

     

    @Test
    @DisplayName("기본 생성자만 없는 경우 - Request API")
    public void test02() throws Exception {
        mvc.perform(post("/api/member")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(requestJson.toString()))
            .andExpect(status().isOk())
            .andDo(print());
    }

    위 테스트를 실행하면 성공하는 것을 확인할 수 있다. ObjectMapper에서 JSON 데이터를 POJO 객체로 역직렬화했을 때는 분명 기본 생성자가 없어서 에러가 발생했는데, @RequestBody 애너테이션을 통해 받아온 데이터는 어떻게 성공적으로 바인딩이 되는 것일까?

     

    2-3. jackson-module-parameter-names 모듈을 통해 JSON 데이터를 역직렬화하는 경우

    @Test
    @DisplayName("기본 생성자만 없는 경우 - jackson-module-parameter")
    public void test01() throws JsonProcessingException {
        mapper.registerModule(new ParameterNamesModule());   // jackson-module-parameter-names 모듈 등록
        Member member = mapper.readValue(json.toString, Member.class);   // JSON 데이터 -> POJO 객체
        logger.info(member.toString);
        
        assertEquals(member.getName(), "홍길동");
        assertEquals(member.getAddress(), "홍길동의 주소");
    }

    위 예제는 ObjectMapper를 통해서 JSON 데이터를 역직렬화한 테스트를 조금 수정한 것이다. ObjectMapper에 jackson-module-parameter-names 모듈을 등록하고 역직렬화를 다시 한번 진행해 보았다.

    위 테스트는 성공적으로 통과하는 것을 확인할 수 있으며, jackson-module-parameter-names는 이 테스트를 통해 알 수 있듯이, 기본 생성자가 없어도 역직렬화를 수행하게끔 도와주는 모듈이라고 할 수 있다.

    jackson-module-parameter-names 모듈은 기본생성자가 없는 경우 id, name, address, email과 같은 필드에 데이터를 넣을 수 있는 다른 루트를 찾게 되는데, 인자가 있는 생성자가 바로 그 대상이 된다. 

    id, name, address, email을 인자로 가진 생성자가 있으면 해당 생성자를 통해 역직렬화를 진행하게 된다. 따라서, 기본생성자가 존재하지 않아도 다른 생성자에 역할을 위임해서 데이터 바인딩을 진행한 것이다.

     

    그렇다면, Spring Boot 상에서 진행했던 mockMvc 테스트는 왜 통과가 된 것일까?

     

     

    컨트롤러에 작성한 Post Method를 디버깅해 보면 위와 같은 내용을 확인해 볼 수 있었다.

    Spring Boot는 기본적으로 Jackson 라이브러리를 가지고 있다. 역직렬화 또는 직렬화를 진행할 때 ObjectMapper를 사용하게 되며, ObjectMapper에 등록된 모듈이 jackson-module-parameter-names인 것이다.

     

    3. 인자가 한 개뿐인 생성자만을 가진 경우
    public class Member {
        private String name;
        
        public Member(String name) {
            this.name = name;
        }
    }

    기본 생성자는 없는데, 인자가 있는 생성자만 있는 클래스에 대해서 jackson-module-parameter-names 모듈이 있으면 역직렬화가 가능하다는 것을 앞서서 확인할 수 있었다.

    하지만, 인자가 한 개뿐인 생성자에 대해서는 이야기가 달라진다. 위 예제 코드와 같이 인자가 한 개인 생성자만 있는 클래스를 ObjectMapper를 통해 역직렬화를 시도해 보면 에러가 발생한다. 심지어 jackson-module-parameter-names 모듈을 ObjectMapper에 등록하고 시도를 해도 똑같이 실패하게 된다.

     

    @Test
    @DisplayName("인자가 한 개뿐인 생성자만을 가진 경우")
    public void test02() throws Exception {
        mvc.perform(post("/api/member")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(requestJson.toString()))
            .andExpect(status().isBadRequest())
            .andExcept(result -> 
                    assertTrue(result.getResolvedException() instanceof HttpMessageNotReadableException)
            )
            .andDo(print());
    }

    인자가 한 개뿐인 생성자에 대해서는 컨트롤러를 통한 mockMvc 테스트에서도 똑같이 실패 결과를 반환한다. 이 경우 HttpMessageNotReadableException이 발생하게 되는데, JSON으로 들어온 요청 데이터를 제대로 역직렬화하지 못해 발생한 에러이다.

     

    public class Member {
        private String name;
        
        @JsonCreator
        public Member(String name) {
            this.name = name;
        }
    }

    인자가 하나인 생성자를 클래스를 역직렬화하기 위해서는 위 예제 코드와 같이 해당 생성자에 @JsonCreator를 지정해 준다.

     

     

    @JsonCreator는 위 내용에서 나와있는 것과 같이, 지정해 준 생성자를 통해 역직렬화를 진행하겠다고 알려주는 역할이다.

     

    @Test
    @DisplayName(" ")
    public void test02() throws JsonProcessingException {
        mapper.registerModule(new ParameterNamesModule());   // jackson-module-parameter-names 모듈 등록
        
        Member member = mapper.readValue(json.toString, Member.class);   // JSON 데이터 -> POJO 객체
        
        logger.info(member.toString);
        assertEquals(member.getName(), "홍길동");
    }

    @JsonCreator 애너테이션을 Member 클래스의 생성자에 지정한 후 위 테스트를 실행하면 정상적으로 통과되는 것을 확인할 수 있다. 보통 @JsonCreator@JsonProperty와 함께 쓰이는데 생성자의 인자가 한 개뿐이라 따로 지정할 필요는 없다.

     

    결론적으로, 인자가 한 개뿐인 생성자만을 가진 클래스는 많은 제약사항과 함께 역직렬화 시 에러가 발생하기 쉽다는 취약점이 있다. 결국, 요청 DTO에서 기본 생성자를 지정하는 것이 매우 중요하다.

     

    4. 인자가 있는 생성자와 setter() 메서드로 구성된 경우
    public class Member {
        private String name;
        
        private String address;
        
        private String email;
        
        public Member(String name, String address) {
            this.name = name;
            this.address = address;
        }
        
        public void setEmail(String email) {
            this.email = email;
        }
    
    }

    위 예제 코드는 name, address 필드에 대해서는 생성자를 통해 값을 주입받고, email 필드에 대해서는 setter() 메서드를 통해 값을 주입받는다.

     

    @Test
    @DisplayName("인자가 있는 생성자와 setter() 메서드로 구성된 경우")
    public void test03 throws JsonProcessingException {
        mapper.registerModule(new ParameterNamesModule());   // jackson-module-parameter-names 모듈 등록
        Member member = mapper.readValue(json.toString, Member.class);   // JSON 데이터 -> POJO 객체
        logger.info(member.toString);
    }

    위 테스트를 실행하면 정상적으로 통과되는 것을 확인할 수 있다. jackson-module-parameter-names 모듈을 등록한 ObjectMapper에서는 생성자의 인자를 토대로 JSON 데이터를 받아들이고, 생성자 인자에 지정되지 않은 JSON 데이터가 있을 경우 해당 필드의 setter() 메서드를 활용하게 된다.


    출처

    https://beaniejoy.tistory.com/76

     

    728x90

    댓글

Designed by Tistory.