디버거를 곁들인 @ModelAttribute 삽질기

Louie·2022년 10월 22일
0

프로젝트

목록 보기
5/6
  • 프로젝트를 진행하면서 @ModelAttribute를 통해 요청값을 입력받는 DTO를 만들었다.
@Getter
@Setter
public class RequestDTO {

    private ShareType type;
    private List<MultipartFile> images;
    private int price;

    public RequestDTO(ShareType type, List<MultipartFile> images, int price) {
        this.type = type;
        this.images = images;
        this.price = price;
    }
}
  • 이후에 요청값 검증을 위해서 아래와 같이 type, price 필드는 클라이언트가 입력하지 않으면 null 값이 할당되어 @NotNull 어노테이션을 통해 Bean Validation이 동작하도록 구현했다.
  • 이 과정에서 price 필드는 Primitive Type이기 때문에 null 값이 들어갈 수 없으므로 Wrapper ClassInteger로 타입을 변경해줬다.
@Getter
@Setter
public class RequestDTO {

    @NotNull(message = "요청한 쉐어정보 값이 비어있습니다.")
    private ShareType type;

    @NotEmpty(message = "요청한 쉐어정보 값이 비어있습니다.")
    private List<MultipartFile> images;

    @NotNull(message = "요청한 쉐어정보 값이 비어있습니다.")
    private Integer price;

    public RequestDTO(ShareType type, List<MultipartFile> images, int price) {
        this.type = type;
        this.images = images;
        this.price = price;
    }
}
  • 하지만 price 필드의 값을 넣지 않고 테스트를 실행해보니 아래와 같은 예외(TypeMismatchException)가 발생했다.

  • 에러 로그를 읽어보니 int 타입인 price 필드에 null 값을 할당할 수 없다는 뜻으로 이해했다.
  • 하지만 나는 @ModelAttribute의 동작 방식이 자바빈 프로퍼티 방식으로만 필드에 값을 할당해주는 걸로 알고 있었기 때문에 이 에러가 발생한게 조금 의아했다.
  • 왜냐하면 RequestDTO 클래스는 Lombok의 @Setter를 통해서 setPrice 메서드를 만들어 줄 것이고 setPrice 메서드의 매개변수 타입은 price 필드의 타입과 같은 Integer라고 생각했기 때문에 도대체 저 int 타입은 어디서 나온거지…? 라는 궁금증이 생겼다.
  • 일단 디버거를 실행하여 어느 부분에서 TypeMismatchException이 발생하는지 확인해봤다.
  • 스프링은 ModelAttributeMethodProcessor 클래스에서 createAttribute 메서드를 호출하고 BindException이 발생한 것을 확인할 수 있었다.
  • 이번에는 createAttribute 메서드의 시작 부분에 break point를 찍고 다시 요청해봤다.
  • createAttribute 메서드의 219번째 줄에서 getResolvableConstructor 메서드를 통해 ModelAttribute로 생성할 객체의 생성자를 찾아서 ctor 변수에 할당한다.
  • 위 사진과 같이 ModelAttribute로 생성할 객체의 생성자 정보를 담고 있는 변수 ctor의 정보를 디버거로 확인해봤다.
  • 어…? 3번째 파라메터의 타입이 int인 것을 확인할 수 있다.
  • RequestDTO의 price 필드 타입은 분명 Integer로 변경했는데 생각해보니까 생성자의 매개변수 타입은 따로 변경해주지 않아서 int 타입이었던 것이다.
  • 그렇다면 스프링이 알아서 null 값으로 매개변수를 채워준 것일까?
  • 맞다. 디버거로 조금 더 자세하게 둘러보니까 위 사진처럼 int 타입인 생성자의 매개변수 이름(price)와 동일한 파라메터를 클라이언트가 입력하지 않는다면 스프링이 null 값으로 매개변수를 넣어줘서 TypeMissMatchException이 발생한 것이다.
  • 이제 원인을 알아냈으니 아래와 같이 생성자의 priceInteger 타입으로 변경해줬다.
@Getter
@Setter
public class RequestDTO {

    @NotNull(message = "요청한 쉐어정보 값이 비어있습니다.")
    private ShareType type;

    @NotEmpty(message = "요청한 쉐어정보 값이 비어있습니다.")
    private List<MultipartFile> images;

    @NotNull(message = "요청한 쉐어정보 값이 비어있습니다.")
    private Integer price;

    public RequestDTO(ShareType type, List<MultipartFile> images, Integer price) {
        this.type = type;
        this.images = images;
        this.price = price;
    }
}

  • 이제 price 필드를 입력하지 않아도 TypeMismatchException이 발생하지 않고 예외 메시지를 보내줄 수 있다!

결론

  • 기본 생성자가 존재하지도 않는데 기본 생성자와 setter를 통해서 ModelAttribute가 동작한다고 생각했지만 스프링은 생성자의 매개변수 이름과 같은 파라메터 값을 ModelAttribute의 대상인 객체에게 할당해 주는 것을 알 수 있었다.
  • 그리고 매개변수명에 대한 파라메터 값이 존재하지 않으면 null값을 넣어주고 매개변수의 타입이 Primitive Type이여서 TypeMismatchException이 발생했던 것이다.

느낀점

  • 이번 기회에 여러 방식으로 DTO를 구현해서 어떤 상황에서 ModelAttribute로 객체에 값이 들어가는지 계속 확인해봤는데 @AllArgsConstructor만 있어도 필드에 값이 제대로 들어가는 경우도 있었고 기본 생성자의 접근 제어자와 setter의 유무에 따라서 필드에 값이 제대로 들어가지 않는 경우도 있었다.
  • 다음번에는 ModelAttribute의 동작 방식에 대해 제대로 학습해봐야겠다.
profile
백엔드 개발자를 준비하고 있는 Louie입니다.

0개의 댓글