본문 바로가기

Programing/Framework

[스프링] MVC - @RequestBody 객체의 Setter가 필요한가?

주의: 이 글은 jackson-databind 2.8.11.11 기준으로 작성되어 있습니다!
버전에 따라 실제 구현 내용은 바뀔 수 있으니 참고용으로 읽기를 바랍니다.

3줄요약

1. Immutable 객체는 좋은 습관이다. 하지만 절대적인 최선이란 없고 최선의 실천방법(Best Practice)는 상대적이다.
2. Request 객체에 Setter가 없어도 값은 필드 주입이 된다.
3. 필드 주입의 경우 리플렉션에 의한 처리가 되어 Setter가 있는 경우보다 오버헤드가 있을 수 있다.

코드리뷰

코드리뷰를 하다가 getter만 존재하는 @RequestBody 객체를 보았다. (HTTP POST 요청)
불변에 대한 장점들이 많이 알려져 있기에 VO나 DTO 같은 객체는 불변으로 만드는 편이다.

 

@Setter 가 없어도 해당 객체에는 값이 잘 들어오고 코드는 잘 동작한다.
하지만 명시적으로 객체에 setter를 붙여주는 경우와 없는 경우 어떤 차이로 동작하는지 궁금해졌다.

 

물론 여기에서 다루는 케이스는 HttpMessageConverter의 구현체로 MappingJackson2HttpMessageConverter을 사용하고 있다고 가정한 특수한 케이스이다. (spring-web 모듈은 이 구현체를 가지고 있다) 따라서 다른 Converter를 사용하고 있다면 다르게 동작할 수 있다는 점을 유의해야 한다.

은총알은 없다라는 말처럼 여기서는 하나의 케이스만 다룬다.

 

요청은 JSON 형태의 데이터이고 Spring MVC에 기본적으로 있는 Jackson 에 의해 데이터 바인드가 이루어진다.

 

@RequestBody 가 붙어있는 객체에 대한 처리는 RequestResponseBodyMethodProcessor 에서 처리가 된다.

resolveArgument 라는 부분이 메시지 컨버터를 이용해서 정해진 객체로 읽어온다.

resolveArgument

결국은 ObjectMapper

Jackson 2의 추상화 컨버터인 AbstractJackson2HttpMessageConverter 에서 ObjectMapper 의 readValue 메서드를 이용해서 TEXT 형태의 JSON을 객체로 만드는 작업을 수행한다.

 

Setter를 호출하기 위한 SettableBeanProperty 객체는 BeanPropertyMap의 find 를 통해 구해오는데,

Bean 형태의 프로퍼티는 BeanPropertyMap 에 _propsInOrder 라는 곳에 저장이 되는데

Setter가 있는 경우는 MethodProperty가 필드만 있는 경우에는 FieldProperty로 저장이 된다.

이것은 BeanDeserializerFactory 의해 buildBeanDeserializer → addBeanProps 에서 프로퍼티의 개수만큼 이터레이션을 돌아 프로퍼티 정의에 해당하는 SettableBeanProperty들을 추가하게 된다. LINK

public class BeanDeserializerFactory
    extends BasicDeserializerFactory
    implements java.io.Serializable // since 2.1
{

    protected void addBeanProps(DeserializationContext ctxt,
            BeanDescription beanDesc, BeanDeserializerBuilder builder)
            
        // ..
        List<BeanPropertyDefinition> propDefs = filterBeanProps(ctxt,
                beanDesc, builder, beanDesc.findProperties(), ignored);

        // ..
        for (BeanPropertyDefinition propDef : propDefs) {
            SettableBeanProperty prop = null;
            
            if (propDef.hasSetter()) {
                AnnotatedMethod setter = propDef.getSetter();
                JavaType propertyType = setter.getParameterType(0);
                prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
            } else if (propDef.hasField()) {
                AnnotatedField field = propDef.getField();
                JavaType propertyType = field.getType();
                prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
            } else {
                // NOTE: specifically getter, since field was already checked above
                AnnotatedMethod getter = propDef.getGetter();
                if (getter != null) {

 

Setter vs Field 차이점은?

획득과정의 차이

빈 프로퍼티 정의에서 생성자 파라미터가 아닌 경우에는 BeanPropertyDefinition 의 getNonConstructorMutator 를 통해 AnnotatedMember 를 획득한다.

public abstract class BeanPropertyDefinition
    implements Named
{
    // ..
    public AnnotatedMember getNonConstructorMutator() {
        AnnotatedMember m = getSetter();
        if (m == null) {
            m = getField();
        }
        return m;
    }

만약 setter가 존재하는 경우에는 바로 해당 AnnotatedMember 을 반환한다.

하지만 setter가 없는 경우에 getField 라는 fall-back 로직을 탄다.

 

POJOPropertyBuilder 의 도움을 받아 만들게 되는데 아래와 같이 루프를 하나 돌고 reflection 연산들이 들어간다.

public class POJOPropertyBuilder
    extends BeanPropertyDefinition
    implements Comparable<POJOPropertyBuilder>
{
    // ..
    @Override
    public AnnotatedField getField()
    {
        if (_fields == null) {
            return null;
        }
        // If multiple, verify that they do not conflict...
        AnnotatedField field = _fields.value;
        Linked<AnnotatedField> next = _fields.next;
        for (; next != null; next = next.next) {
            AnnotatedField nextField = next.value;
            Class<?> fieldClass = field.getDeclaringClass();
            Class<?> nextClass = nextField.getDeclaringClass();
            if (fieldClass != nextClass) {
                if (fieldClass.isAssignableFrom(nextClass)) { // next is more specific
                    field = nextField;
                    continue;
                }
                if (nextClass.isAssignableFrom(fieldClass)) { // getter more specific
                    continue;
                }
            }
            throw new IllegalArgumentException("Multiple fields representing property \""+getName()+"\": "
                    +field.getFullName()+" vs "+nextField.getFullName());
        }
        return field;
    }

 

수행과정의 차이

setter가 있는 경우 아래처럼 호출 스택을 가지게 된다. 즉, setter가 그냥 호출되고 만다.

MethodProperty 의 deserializeAndSet 에서 setter가 호출(invoke)된다.

반면 field의 경우는 좀 더 복잡하게 수행한다.

FieldAccessor 를 획득하여 set이 호출된다. 이 획득하는 과정 역시 리플렉션을 통해 이루어진다.

package java.lang.reflect;

public final class Field extends AccessibleObject implements Member {

    // ..
    
    public void set(Object obj, Object value)
        throws IllegalArgumentException, IllegalAccessException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        getFieldAccessor(obj).set(obj, value);
    }
    
    private FieldAccessor getFieldAccessor(Object obj)
        throws IllegalAccessException
    {
        boolean ov = override;
        FieldAccessor a = (ov) ? overrideFieldAccessor : fieldAccessor;
        return (a != null) ? a : acquireFieldAccessor(ov);
    }

    private FieldAccessor acquireFieldAccessor(boolean overrideFinalCheck) {
        // First check to see if one has been created yet, and take it
        // if so
        FieldAccessor tmp = null;
        if (root != null) tmp = root.getFieldAccessor(overrideFinalCheck);
        if (tmp != null) {
            if (overrideFinalCheck)
                overrideFieldAccessor = tmp;
            else
                fieldAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            tmp = reflectionFactory.newFieldAccessor(this, overrideFinalCheck);
            setFieldAccessor(tmp, overrideFinalCheck);
        }

        return tmp;
    }

그리고 UnsafeObjectFieldAccessorImpl 의 set이 호출되어 값이 변경된다.

class UnsafeObjectFieldAccessorImpl extends UnsafeFieldAccessorImpl {

    // ..
    public void set(Object obj, Object value)
        throws IllegalArgumentException, IllegalAccessException
    {
        ensureObj(obj);
        if (isFinal) {
            throwFinalFieldIllegalAccessException(value);
        }
        if (value != null) {
            if (!field.getType().isAssignableFrom(value.getClass())) {
                throwSetIllegalArgumentException(value);
            }
        }
        unsafe.putObject(obj, fieldOffset, value);
    }

그리고 호출스택은 아래와 같다.

호출스택의 깊이는 2단계가 더 적지만 Reflection 등의 많은 연산을 해야 하므로 성능상 손해를 볼 수 밖에 없다.

코드리뷰에 왜 Setter를 사용하지 않았나 어떤 trade-off 과정이 있었나 물어봐야겠다.

 

HttpMessageConverter의 타입들

스프링 5.3.6 이 CURRENT 인 시점에 5.1.8로 파악한 것이라 구현체는 더 있을 수 있다. 놓쳤을 가능성도 물론 있다.

org.springframework.http.converter 패키지의 타입

org.springframework.http.converter.json 하위 타입

생성자 값 주입  (HTTP GET 요청)

HTTP GET의 쿼리 파라미터의 경우라면 생성자를 통해 인자를 넣을 수 있다. 따라서 생성자로 객체를 생성하고 Setter를 없앨 수 있다.

이 경우에는 ModelAttributeMethodProcessor 에서 생성자 호출시 값을 넣어준다.

ServletModelAttributeMethodProcessor → ModelAttributeMethodProcessor → BeanUtils#instantiateClass → 생성자 호출

History

2021-04-26 이*욱 #41227

2021-05-11 류*희 #42349

 

같이보기