BackEnd/Spring

[Spring] @RequestBody에 기본생성자만 필요하고 Setter는 필요없는 이유 - 2

샤아이인 2022. 6. 12.

그간 밀어오고 밀어왔던 내용에 대해 정리하고 넘어가야 겠다 싶어 정리하는 글 이다.

지난 글에서 @RequestBody에서 어떤 방식으로 객체를 생성하는 지 파악한 후,

이번 글에서는 객체에 값을 어떤 방식으로 binding 시키는지에 대하여 알아봅시다.

 

직접 디버깅을 하나하나 다 해가면서 작성 한 글 입니다...

혹 틀린 부분이 있다면 지적해주시면 감사하겠습니다!

 

우선 이 글은 2개의 글로 만들어질 예정 입니다.

1) 기본 생성자가 필요한 이유 

2) Setter가 없어도 되는 이유 (이번글)

 

2. Setter가 없어도 되는 이유

기본 생성자로 Object를 생성하는데 setter없이 값을 어떻게 할당시킬까? 이에 대하여 알아보자!

 

이전과 동일한 예제 DTO를 사용할 것 이다.

@Getter
@ToString
@NoArgsConstructor
public class OrderDto {

    private String itemName;
    private int amount;
    private RequestType requestType;

    public OrderDto(String itemName, int amount) {
        this.itemName = itemName;
        this.amount = amount;
    }

    public OrderDto(String itemName, int amount, RequestType requestType) {
        this.itemName = itemName;
        this.amount = amount;
        this.requestType = requestType;
    }

    @AllArgsConstructor
    public enum RequestType {
        GET ("get"),
        POST ("post");

        private String method;
    }
}

위 코드에서 볼수 있듯, 기본 생성자를 통해서 Object를 생성하게 될것 입니다.

 

하지만 위 코드에서 setter는 찾아볼수가 없죠!

 

그럼 어떻게 빈 Object를 생성한 후에 값을 할당시키는 것 일까요?

이를 알기 위해서는 ObjectMapper에 대한 이해가 필요합니다.

 

2-1) ObjectMapper에 대한 이해

관련링크 의 글을 참고하였습니다.

 

 

이를 요약하면 기본적으로 Jackson은 JSON 필드의 이름을 Java 오브젝트의 getter 및 setter 메소드와 일치시켜 JSON 오브젝트의 필드를 Java 오브젝트의 필드에 주입시키게 됩니다.

 

1) Jackson이 getter 또는 setter 메서드의 이름을 본후

2) 해당 메서드 이름에서 get, set을 제거한 후

3) 제거된 이름의 맨 첫글자를 소문자로 변경한다.

 

이렇게 변경된 이름을 통해 Object의 field에 접근학게 됩니다.

 

1) 우리의 예시에서 getter의 이름은, getItemName, getAmount, getRequestType 입니다.

2) 여기서 get 부분을 제거하면 ItemName, Amount, RequestType이 되고

3) 맨 첫글자를 소문자로 변경하면 itemName, amount, requestType이 됩니다.

 

이렇게 동일한 이름의 변수를 직접 찾는 것이 아니라, Getter와 Setter를 통해 찾아서 매칭한다는 것입니다.

 

2-2) DTO의 필드이름 찾기

직전 단락에서 Object의 필드명을 알아내는 방법에 대하여 알게되었습니다.

이번에는 실재로 어떻게 돌아가는지 코드로 살펴봅시다.

 

1. Getter 또는 Setter로 Property 찾기

위 코드는 역직렬화를 하는 BeanDeserializer를 생성하기 위해 BeanDeserializerFactory에서 해당 작업을 수행하는 것을 확인할 수 있었습니다.

빨간 박스 안의 addBeanProps 메서드를 들어가보면 다음과 같이 프로퍼티의 이름들을 찾아오게 됩니다.

 

2. addBeanProps의 내부

addBeanProps의 내부에는 다음과 같은 코드 부분을 확인할 수 있습니다.

위 코드의 findValueInstantiator()를 통해 필드값들에 대하여 알수있게 됩니다.

 

3. findValueInstantiator()

findValueInstantiator 내부 코드는 다음과 같습니다.

딱 봐도 이름이 "기본값으로 생성해주는 초기화자" 인게, 해당 부분에서 필드에 대한 정보를 얻을 수 있을것 같습니다.

 

4. _coustructDefaultValueInstantiator 의 내부

내부로 이동해보면 _findCreatorFromProperties가 있는데 안으로 들어가 봅시다.

 

5. _findCreatorFromProperties

다음 코드 부분을 보면 beanDesc.findProperties()를 호출하고 있는데, 해당 메서드에서 field의 이름들을 알아오게 됩니다.

findProperties() 메서드 같은 경우 BasicBeanDescription 에 override 되어 있습니다.

_properties()를 추적해 가봅시다~

 

6. _properties()

아직 properties 들이 생성되지 않은 null 의 상황이기 때문에 POJOPropertiesCollector의 getProperties()를 호출하게 됩니다.

POJOPropertiesCollector는 Java Object의 접근 가능한 properties에 대하여 정보들을 제공해주는 Class입니다.

디버깅을 해보면 this(= POJOPropertiesCollector)에서 여러 Class에 대한 정보를 확인할 수 있었습니다.

 

7. getProperties() 내부

POJOPropertiesCollector의 getProperties()를 호출하면 다음과 같습니다.

내부에서 getPropertyMap()을 호출합니다. 추노의 마음으로 끝까지 추적합시다~

 

8. getPropertyMap() 내부

위 코드에서 collectAll()이 핵심입니다! 저기서 필드에 대한 정보들을 얻어오게 됩니다!

 

9. collectAll() 내부

드디어 끝이 다 왔습니다!

_addFields(props) 내부에서 필드에 대한 정보를 얻게됩니다.

 

10. _addFields(props) 내부

다음과 같이 _addFields 내부에서 _classDef.fields()로 하나하나 순회하면서 필드에 각각 접근하게 됩니다.

이때 순회하는 코드 중간쯤에 다음과 같은 부분이 있습니다.

impleName, 즉 구현되 있는 실재 이름을 얻어오는 부분입니다.

f.getName() 으로 필드의 이름을 얻어오려 하면 다음과 같이 진행됩니다.

 

11. getName()

드디어 실재 field이름인 itemName을 얻게 되었습니다. 길고도 긴 과정 이였습니다...

위와 같이 반복문으로 3번 돌면 우리의 field에 대한 정보들을 다 얻어오게 돤다.

 

이후 이전에 위에서 봤던 collectAll() 매서드로 돌아와 보면 props에 field에 대한 정보들이 담겨있는것을 확인할 수 있게 되었다.

 

이후 다시 값을 반환하면서 호출 stack을 되돌아 올라가다 보면 propDef.getCounstructorParameters()를 호출하게 되는데

해당 메서드 안에서 getter 와 setter가 있는지 확인하는것을 알 수 있었습니다.

다만 해당 메서드 안까지 디버깅을 하는 과정은 생략하도록 하겠습니다...

우리는 setter를 만들지 않았기 때문에 null이 들어온것은 확인할 수 있습니다.

 

2-3) DTO의 필드에 값 주입

1.  Jackson2HttpMessageConverter 

MessageConverter가 Json을 Object로 변환시켜 준다 한말 기억하시죠? (지난 번 글에서 설명)

컨버터 내부에서 ObjectMapper를 생성 한 후, readValue()를 호출하게 됩니다.

이후 readValue 내부의 _readMapAndClose 를 호출하게 됩니다.

InputStream인 src가 NotNull임을 확인 한 후, src로부터 JsonParser를 생성하게 됩니다.

_readMapAndClose()를 통해 값을 바인딩 하게 됩니다.

 

2. _readMapAndClose 의 내부

내부의 readRootValue() 를 통해 값을 읽어오는데 이때 인자로 전달하는 것 들이

JsonParser, valueType(OrderReqeust.class), deserizalizer 입니다.

 

3. readRootValue의 내부

deser가 JSON 데이터를 역직렬화 시켜주는 객체입니다.

deser를 통해 this 즉, ctxt(context) 를 변환하게 됩니다.

 

4. deserialize() 내부

내부에서는 deserializeFromObject를 통해 ctxt를 역 직렬화 하여 Object로 반환하게 됩니다.

 

5. deserializerFromObject 내부

deserializerFromObject 내부에서 default 생성자로 객체를 생성 한 후의 모습니다.

객체를 어떻게 생성하는지, 왜 기본 생성자가 필요한지는 지난번 글에서 다루었으니 디버깅은 넘어가도록 하겠습니다.

bean의 값을 살펴보면, 내부가 다 null, 0, null 로 초기화 되어있는 것을 확인할수 있습니다.

아직 객체만 생성되고, 값은 할당되지 않은것 이죠!

 

값은 코드 중간쯤 보면 다음 과 같은 부분을 확인할 수 있는데

prop.deserializeAndSet(p, ctxt, bean);

위 코드 부분에서 값을 할당시켜 줍니다.

 

6. deserializeAndSet()을 통한 값의 할당

아래 코드에서 p는 JsonParser입니다. 위에서 구했던 propertyName으로 이전에 저장했던 프로퍼티를 가져옵니다.

그리고 이제 JsonParser와 Bean을 deserializeAndSet 메서드로 넘겨 값을 저장해주는 것입니다.

위에서 만든 비어있는 객체 bean 에다가 값을 할당하시는 핵심 코드 부분입니다.

properties 에 대한 정보를 가지고 있는 prop 의 메서드인 deserializeAndSet()을 호출하게 됩니다.

 

7. deserializeAndSet() 내부

우선 value 에 값을 가져오는 getNullValue()라는 메서드가 있습니다.

이렇게 가져온 value를 _field.set(instance, value)를 통해 할당하게 됩니다.

_field는 java.lang.reflect 패키지의 Field 자료형입니다.

결국 reflection을 사용해서 값을 주입해주는 것 입니다!! Setter나 getter 는 값을 주입할때는 필요하지 않는 것입니다.

단순히 field의 이름을 알아올때 필요했던것 이죠!

 

2-4) 결론

Setter와 Getter 모두 없는 경우에는 ObjectMapper가 바인딩하는데 오류가 생기며, Setter, Getter 둘 중 하나만 있으면 된다는 것을 알았습니다.

 

따라서 일반적으로 기본 생성자와 getter만 추가하면 되며, getter를 추가해야 우리가 Object를 받았을때 값을 꺼내 사용하기가 편리하기 때문입니다.

댓글