개발 이론/Spring

[Spring] @ModelAttribute와 @RequestBody에 대해

dal_been 2023. 11. 9. 02:15
728x90

컨트롤러로 들어오는 데이터 객체를 매핑하는 어노테이션으로 @ModleAttribute와 @RequestBody가 있다.

@ModelAttribute는 여러개의@RequestParam을 하나의 객체로 매핑할때 쓰이고

@RequestBOdy는 Json형태로 들어오는 body를 객체로 매핑할때 쓰인다.

 

스프링은 이 두 어노테이션을 통해 클라이언트가 보내주는 데이터를 객체로 역직렬화 할 수 있다

 

다만 데이터를  객체로 역직렬화해주는데는 필요한 규칙들이 있다. 이 규칙을 지키지 않으면 정상적으로 객체 매핑이 될 수 없다

 

빈 생성자가 있어야한다

 

두 어노테이션모두 역직렬화 과정에서 기본적으로 아무 필드도 할당하지 않는 빈 생성자를 호출해서 객체를 생성한다.

물론 빈 생성자만 있다고 되는 것은 아니다. 빈 생성자를 만든후 각각의 필드에 클라이언트가 보낸 데이터를 넣는 작업이 필요하다

이과정은 두 어노테이션이 다르게 작동한다

 

@ModelAttribute

  • @ModelAttribute의 역직렬화는 기본적으로 전통적인 자바 빈즈 패턴을 따르다
  • 자바빈즈 패턴 : 매개변수 없는 생성자를 만든두 세터 메서드로 매개변수 값을 설정
  • 스프링은 빈 생성자를 호출한뒤 세터 메서드를 통해 데이터를 바인딩해준다
  • 따라서 빈생성자 + Setter 메서드 필요

하지만 setter호출을 빼먹는 경우 같은 상황이 있다. 그렇다면 그냥 생성자를 통해 바로 할당할 수 없을까??

 

있다! AllArgsConstructor가 잇다면 세터없이도 역직렬화가 가능하다. 단. setter를 없애거라면 빈생성자도 없애야한다

 

 

protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
         WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {

    if (ctor.getParameterCount() == 0) {

        return BeanUtils.instantiateClass(ctor);
    }

 
    String[] paramNames = BeanUtils.getParameterNames(ctor);
    Class<?>[] paramTypes = ctor.getParameterTypes();
    Object[] args = new Object[paramTypes.length];
    WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
    String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
    String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
    boolean bindingFailure = false;
    Set<String> failedParams = new HashSet<>(4);

    ...
}

 

위의 코드를 보면 역직렬화를 담당하는 ModelAttributeMethodProcessor 구현체인데 빈 생성자가 존재하면 자바 빈즈 패턴을 이용하고 빈생성자가 없다면 모든 필드를 할당하는 생성자를 이용해 객체를 생성한다

 

따라서 정리하자면 빈생성자+setter를 만들어주거나 모든 필드를 매개변수로 받는 생성자 하나만 만들자


 

@RequestBody

 

@ModelAttribute는 스프링 내부에서 구현체가 역직렬화를 해주었다면

@RequestBody는 스프링 내장된 외부 라이브러리 Jackson이 역직렬활르 해준다

또한 Jackson은 리플렉션을 이용하여 데이터를 바인딩 해준다.

 

그래서 @RequestBody의 규칙은

- 빈생성자가 꼭 있어야한다. 모든 필드를 받는 생성자가 있더라도

- 생성자가 private이라도 상관 없다

- Jackson이 객체의 property를 알 수 있어야한다

 

그렇다면 Jackson은 어떻게 데이터를 바인딩할까??

 

무조건 빈생성자를 호출한다(빈생성자가 정의되지 않았다면 역직렬화에 실패하고, private생성자여도 리플렉션을 사용하기 때문에 상관없다)

→ Jackson은 해당 필드들을 알 수 있어야하기 때문에 필드가 public(비추) 이거나 getter나 setter가 있어야한다

  (만약 세개다 없다면 예외가 발생한다)

→ 그래서 좋은 방법은 dto 사용에 getter가 필요하니 getter만 만들어주는 것이다.

    (만약 getter를 만들고 싶지 않다면 필드에 @JsonProperty를 선언해주거나 클래스에 @JsonAutoDetect를 붙여주면 Jackson이 직접 필드에 접근할 수 있다)

 

 

 


Reflection??

- Reflection은 구체적인 클래스를 몰라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있는 자바 API

 

일단 클래스를 하나 만들어보자

 

public class Bus {
    private final String name;
    private int position;

    public Bus(String name, int position) {
        this.name = name;
        this.position = position;
    }

    public void move() {
        this.position++;
    }

    public int getPosition() {
        return position;
    }
}

 

public static void main(String[] args) {
    Object obj = new BUS("720-2", 0);
}

 

하면 obj에서 move라는 메서드를 사용할 수 잇나?? 

 

불가능하다!

WHY? 컴파일 타임에 타입이 결정되기때문이다. -> 컴파일 타임에 Object로 타입이 결정되었기때문에 Object 클래스의 인스턴스 변수와 메서드만 사용가능하다

 

따라서 컴파일러가 있는 자바는 구체적인 클래스를 모르면 해당 클래스의 정보에 접근할 수 없다

 

근데 이를 가능하게 해주는 것이 Reflection API이다

위의 예제를 RefletionAPI를 이용해보자

public static void main(String[] args) throws Exception {
    Object obj = new Bus("foo", 0);
    Class busClass = Bus.class;
    Method move = busClass.getMethod("move");

    // move 메서드 실행, invoke(메서드를 실행시킬 객체, 해당 메서드에 넘길 인자)
    move.invoke(obj, null);

    Method getPosition = busClass.getMethod("getPosition");
    int position = (int)getPosition.invoke(obj, null);
    System.out.println(position);
    // 출력 결과: 1
}

 

 

이것이 어떻게 가능할까??

자바에서는 JVM이 실행되면 사용자 작성 자바 코드가 컴파일러를 거쳐 바이트코드로 전환되어 static영역에 저장된다

이 정보를 Reflection API가 활용한다. 그래서 클래스 이름만 알고 있다면 static영역에서 찾아 정보를 가져올 수 있다

 

 

다만 우리가 작성한 코드에서는 거의 사용하지 않는다

그이유는 컴파일 타임이 아닌 런타임에 동적으로 타입을 분석하고 정보를 가져옴으로 JVM최적화를 할 수없다. 또한 직접 접근할 수 없는 private 인스턴스 변수, 메서드에 접근하기 때문에 추상화가 깨진다

 

결론적으로 Reflection는 라이브러리나 프레임워크에 많이 사용된다.

사용자가 어떤 클래스를 만들지 모르기 때문에 동적으로 해결하기위해 Reflection을 사용한다

 

예를 들자면 앞서 얘기한 Jackson라이브러리도 있지만 Spring Framework에서도 BeanFactory가 리플렉션을 사용한다

애플리케이션이 실행한후 런타임에 객체가 호출될때 동적으로 Bean인스턴스를 생성하는데 이때 BeanFactory는 리플렉션을 사용한다

 

또한 Spring Data Jpa에서도 Entity에 기본생성자가 필요한 이유도 동적으로 객체 생성시 리플렉션을 이용하기 때문이다.

리플렉션이 가져올 수 없는 정보가 생성자의 인자 정보이다. 그래서 기본생성자가 반드시 있어야 리플렉션을 사용할 수 있다