Spring Framework 2024. 8. 19. 01:15

[Spring] Spring Web이 Request Parameter를 바인딩하는 과정, 내부 구현 분석하기 - 생성자, getter, setter

목차
  1. 요약
  2. 배경
  3. Spring Web이 Request Parameter를 바인딩하는 과정
  4. 1. HTTP 요청을 받았을 때, Spring Web은 사용자가 Controller에 정의한 메소드를 호출한다
  5. 2. Controller 메소드의 파라미터로 바인딩한다
  6. 2-1. 메소드 파라미터인 param DTO 클래스의 생성자를 호출해서 인스턴스화한다.
  7. 2-2. 바인딩 메소드를 호출한다
  8. 2-2-1. 바인딩 메소드에서 oldValue를 얻기 위해 getter를 호출한다
  9. 2-2-2. 바인딩 메소드에서 값을 할당하기 위해 setter를 호출한다
  10. 실험: 실제로 멤버가 존재해야 하나? 멤버 없이 getter, setter만 있으면 동작할까?
  11. 결론

요약

  • Spring Web은 HTTP Request Parameter(?property=vale)를 개발자가 정의한 클래스 객체로 바인딩하기 위해서 생성자, getter, setter 메소드를 호출한다

배경

Request Param 클래스를 만들다가 페이지네이션을 할 때 쓰는 속성 page와 size를 인터페이스화 해서 api에서 공통으로 관리하면 좋겠다는 생각을 했다.

/**
 * 목록 조회 API - 페이지네이션 기능을 사용할 때 쓰는 Request Param 속성.
 */
public interface PaginationRequestParam {
    /**
     * 페이지 번호
     */
    Integer getPage();

    /**
     * 페이지 당 데이터 수
     */
    Integer getSize();
}

그런데 자바 인터페이스는 속성은 선언하지 않고, public 메소드만 선언할 수 있었다. 그래서

  1. getter만 포함한 인터페이스를 만드는 것이 좋은 설계인가 하는 의문이 들었다. getter가 메소드이기는 하지만, 구체적인 속성을 인터페이스에서 지정하는 느낌이 들었기 때문이다.
  2. Spring Web 프레임워크에서 속성이 아닌 캡슐화된 getter와 setter를 호출한다면, 이 메소드를 인터페이스화할 수 있지 않을까 하는 생각이 들었다. Spring Web이 의존하고 있는 건 멤버가 아니라 getter와 setter라는 메소드 형식이기 때문이다.

2번의 전제인 'Spring Web은 속성이 아닌 getter와 setter를 호출한다'를 검증하기 위해 IntelliJ의 breakpoint 기능을 활용하여 내부 구현을 확인해 보았다

Spring Web이 Request Parameter를 바인딩하는 과정

1. HTTP 요청을 받았을 때, Spring Web은 사용자가 Controller에 정의한 메소드를 호출한다

HTTP 요청은 텍스트(String)형식이다. 형식이 정해져 있으니 파싱을 하면 형식에 맞게 속성이 정의되어 있는 Object일 것이다. HOST, Method(GET, POST)야 미리 정의되어 있기 때문에 클래스의 속성으로 만들어놓을 수 있다. 그러나 이 시점에서 Request Body나 Request Param은 어떤 형식인지 모른다. 단순히 Key-value의 list로 들고 있을 뿐이다. 그런데 개발자가 Controller에 정의한 메소드는 request DTO 클래스를 입력으로 받는다. 이를 변환해 주는 작업이 바인딩이고 springframework.web.bind가 수행한다.

2. Controller 메소드의 파라미터로 바인딩한다

2-1. 메소드 파라미터인 param DTO 클래스의 생성자를 호출해서 인스턴스화한다.

개발자가 정의한 Controller 메소드를 호출하려면 param 인자의 타입을 개발자가 정의한 타입으로 변환해서 호출해야 한다. 이 작업을 하기 위해서 먼저 하는 일이 생성자를 호출해서 인스턴스를 생성하는 것이다. 이 때는 param 멤버 값은 null인 상태이다.

2-2. 바인딩 메소드를 호출한다

2-2-1. 바인딩 메소드에서 oldValue를 얻기 위해 getter를 호출한다

바인딩 메소드는 값이 변경될 때 이벤트를 발생시키고, 타입 컨텍스트를 전달하기 위해서 oldValue를 저장한다. 이 때 oldValue를 얻기 위해서 개발자가 정의한 param DTO의 getter 메소드가 호출된다

2-2-2. 바인딩 메소드에서 값을 할당하기 위해 setter를 호출한다

setter를 HTTP request param에 있던 value를 개발자가 정의한 DTO의 멤버로 할당한다. 어떤 setter를 호출할 지는 request param의 key값을 입력으로 Java의 리플렉션 기능을 사용한다. key값과 같은 DTO의 멤버를 쓰는 setter를 찾아서 호출한다.

실험: 실제로 멤버가 존재해야 하나? 멤버 없이 getter, setter만 있으면 동작할까?

Spring Web이 의존하는 게 멤버가 아니라 캡슐화된 getter, setter라는 가정을 검증하기 위해서 클래스의 멤버 없이 getter, setter 메소드만 있을 때도 Spring Web이 Controller 메소드를 정상적으로 동작하는 지 실험해 보았다.

@Getter
@Setter
@Slf4j
public class HistoryGetOneRequestParam {
//    @Nullable
//    @PatternList(regexp = "^(runHabit)$", message = "'related' param 값이 유효하지 않습니다.")
//    private List<String> related;

    public HistoryGetOneRequestParam() {
        log.info("HistoryGetOneRequestParam 생성자 호출");
    }

    @Nullable
    public List<String> getRelated() {
        return List.of("runHabit");
    }

    public void setRelated(@Nullable List<String> related) {
        log.info("HistoryGetOneRequestParam.setRelated 호출");
    }
}

멤버 related는 주석 처리하여 없는 코드로 만들었다. getter는 호출하면 그때그때 팩토리로 같은 리스트 생성, setter는 로그만 찍고 아무 동작도 안 하게 변경해 보았다.

테스트 결과: 정상 작동

테스트 결과 DTO에 멤버가 없어도 코드는 `related={"runHabit"}`일 때처럼 정상 작동했다. 

여기서 드는 의문점은 2-2-2에서 멤버에 값을 할당할 수 있는 setter를 호출한다고 했는데, 멤버도 없는데 어떻게 setter를 호출했는가는 것이다.

이는 `org.springframeworks.beans.PropertyDescriptorUtils`, `java.beans.PropertyDescriptor`의 코드에서 확인할 수 있었다

PropertyDescriptorUtils

Method[] var2 = beanClass.getMethods();
...

 Method method = var2[var4];
            String methodName = method.getName();
            boolean setter;
            byte nameIndex;
            if (methodName.startsWith("set") && method.getParameterCount() == 1) {
                setter = true;
                nameIndex = 3;
            } else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) {
                setter = false;
                nameIndex = 3;
            } else {
                if (!methodName.startsWith("is") || method.getParameterCount() != 0 || method.getReturnType() != Boolean.TYPE) {
                    continue;
                }

                setter = false;
                nameIndex = 2;
            }

            String propertyName = StringUtils.uncapitalizeAsProperty(methodName.substring(nameIndex));

PropertyDescriptor

if (writeMethodName == null) {
                writeMethodName = Introspector.SET_PREFIX + getBaseName();
            }

Spring framework는 클래스의 멤버를 get, set으로 시작하는 public method의 이름에서 추출한다. 따라서, 실제 멤버가 없어도 이름 형식이 같은 getter, setter가 존재(getXXX, setXXX)한다면 이를 호출할 수 있었던 것이다.

결론

같은 기능을 구현하기 위해 사용되는 `getter`와 `setter` 메소드는 인터페이스화해서 재사용해도 좋을 것 같다. Spring framework이 실제로 의존하는 것은 멤버가 아닌, 이름 형식이 일치하는 public 메소드 `getXXX`와 `setXXX`이기 때문이다. 실제로 멤버가 없어도 이들 메소드만 정의되어 있으면 정상 작동한다.

저작자표시 (새창열림)

'Spring Framework' 카테고리의 다른 글

[Spring] 내장 타입 변환 서비스 ConversionService, 모든 Enum Type에 String -> Enum 변환 적용 - ConverterFactory  (0) 2024.08.24
[MapStruct] List에서 요소 하나를 매핑할 함수 지정하는 법 - @IterableMapping  (0) 2024.08.24
[Spring][스크랩] Request Param의 타입으로 enum을 지정하는 법  (0) 2024.08.18
[JPA] deleteBy는 N개의 DELETE 쿼리로 실행된다  (0) 2024.08.16
[MapStruct] 필드 매핑할 때 다른 Mapper 클래스의 메소드 사용하는 법 - uses, @Named  (0) 2024.08.07
  1. 요약
  2. 배경
  3. Spring Web이 Request Parameter를 바인딩하는 과정
  4. 1. HTTP 요청을 받았을 때, Spring Web은 사용자가 Controller에 정의한 메소드를 호출한다
  5. 2. Controller 메소드의 파라미터로 바인딩한다
  6. 2-1. 메소드 파라미터인 param DTO 클래스의 생성자를 호출해서 인스턴스화한다.
  7. 2-2. 바인딩 메소드를 호출한다
  8. 2-2-1. 바인딩 메소드에서 oldValue를 얻기 위해 getter를 호출한다
  9. 2-2-2. 바인딩 메소드에서 값을 할당하기 위해 setter를 호출한다
  10. 실험: 실제로 멤버가 존재해야 하나? 멤버 없이 getter, setter만 있으면 동작할까?
  11. 결론
'Spring Framework' 카테고리의 다른 글
  • [Spring] 내장 타입 변환 서비스 ConversionService, 모든 Enum Type에 String -> Enum 변환 적용 - ConverterFactory
  • [MapStruct] List에서 요소 하나를 매핑할 함수 지정하는 법 - @IterableMapping
  • [Spring][스크랩] Request Param의 타입으로 enum을 지정하는 법
  • [JPA] deleteBy는 N개의 DELETE 쿼리로 실행된다
개발자 이우진
이우진 기술 블로그
  • All (86)
    • Spring Framework (20)
    • MSA (7)
      • Event Driven Architecture (3)
    • Java (3)
    • Flink (2)
    • Computer Science (9)
      • Object Oriented Programming (3)
    • Problem Solving (15)
    • Design Pattern (0)
    • React (4)
    • Javascript (2)
    • Web (3)
    • Tools & Environment (3)
    • C++ (2)
    • misc (5)
    • Essay (3)
      • 기술 회고 (5)
  • 홈
  • 태그
  • 관리자
  • 글쓰기
hELLO · Designed By 정상우.v4.2.2
[Spring] Spring Web이 Request Parameter를 바인딩하는 과정, 내부 구현 분석하기 - 생성자, getter, setter
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.