Spring

@PathVariable 엔드포인트 매핑 원리

recent0 2024. 10. 12. 20:20

서론

최근에 회사일을 하며 파일 다운로드 기능에 대해 리팩터링이 진행되었는데, 기존에 @PathVariable로 수행되었던 API 엔드포인트가 변경되면서 정상적으로 동작하지 않던 문제를 확인했습니다. 이에 관해 @PathVariable에 어떤 로직이 숨어있는지 알아보고자 합니다.

문제 파악

문제가 발생한 코드 

  • 다음은 문제가 발생하기 이전 코드에 대한 예시입니다. fileName을 PathVariable 어노테이션을 사용해서 받아왔습니다.
@RequiredArgsConstructor
@RestController
public class DownloadController {

    private final DownloadService downloadService;

    @GetMapping("/before/file/{fileName}")
    public ResponseEntity<?> getFileByPathVariable(@PathVariable String fileName) {
        downloadService.download(fileName);
        return ResponseEntity.ok().build();
    }
}

 

  • 그리고 이후는 리팩터링 하면서 개발된 코드에 대한 예시입니다.
@RequiredArgsConstructor
@RestController
public class DownloadController {

    private final DownloadService downloadService;

    @GetMapping("/file/any/**")
    public ResponseEntity<?> getFileByRequest(HttpServletRequest request) {
        String requestURI = request.getRequestURI();

        downloadService.download(getFileName(requestURI));
        return ResponseEntity.ok().build();
    }
}

문제 원인 및 원리 파악

  • 여기서 큰 차이점은 엔드포인트에 와일드 카드, PathVariable 어노테이션 사용 유무입니다.
  • 그래서 저는 @Pathvariable 어노테이션을 사용하면, 추가적으로 디코딩 작업이 선행된다고 추측했습니다.

디코딩된 데이터 찾는 과정

그러면 HttpServletRequest가 아닌 @PathVariable을 사용했을 때 디코딩된 데이터를 찾는 과정에 대해 알아보겠습니다.

1.  DefaultPathContainer를 통해 데이터 파싱하기

  • 구분자를 통해서 URL에 있는 경로를 파싱 합니다.
  • Decode가 필요한 문자는 decodeAndParsePathSegment 메서드를 호출해 decode를 수행합니다.
URL 경로 파싱
  • 파싱한 데이터들은 HttpServeletRequest의 Map<String, Object> 타입으로 이루어진 attributes 필드에 저장됩니다.
파싱한 URL 경로 저장

2.  핸들러 매핑 과정에서 데이터 세팅

  • 이전에 HttpServeletRequest에 저장했던 attributes 필드에서 pathWithinApplication의 PathContainer를 획득합니다.
  • 수행할 핸들러를 찾아 경로에 맞는 데이터가 있는지 확인합니다. 아래 핸들러에서는 fileName이라는 경로에 맞는 데이터가 있는지 매치시켰습니다.
  • 매치되는 데이터가 있다면 해당 데이터를 URI_TEMPLATE_VARIABLES_ATTRIBUTE에 세팅합니다.
Path에서 데이터 매치 로직
HttpServletRequest Attributes에 데이터 세팅

3.  Arguments 획득

  • Controller에 Args로 넘길 데이터를 획득하기 위해서 아래 getMethodArgumentValues를 호출합니다.
  • 해당 데이터는 HttpServletRequest의 Attributes 필드 안에 uriTemplateVariable로 키를 가진 Value에서 획득 가능합니다.
Controller에 넘길 Args 획득 및 실행
HttpServeletRequest로부터 저장했던 데이터 획득

4. Controller 호출

  • 마지막으로 얻은 데이터와 호출할 컨트롤러를 넘겨 비즈니스 로직을 수행합니다.

HttpServletRequest는 로직을 안 타나요?

그러면 PathVariable이 아닌 HttpServletRequest는 위에서 알아본 로직을 안 타는지 확인을 해봤습니다.

HttpServletRequest 확인해 보기(와일드카드가 있는 API)

  • 직접 확인을 해봤을 때, API의 엔드포인트가 와일드카드로 이루어져있다 보니, 매칭되는 값이 없기 때문에 컨트롤러에 디코딩되는 값이 없음을 확인할 수 있습니다.

HttpServletRequest 확인해 보기(와일드카드 없는 API)

  • API의 엔드포인트를 와일드카드를 사용하지 않고, 직접 지정해서 테스트해봤을 때 정상적으로 데이터가 넘어가는 것을 확인할 수 있었습니다.

문제 해결 방법

  • 경로를 지정하고 싶지만, 범용적으로 사용하는 API이다 보니 하나하나 다시 바꾸는 것은 어려움이 있다고 생각했습니다.
  • URLDecoder를 활용해 마지막 경로에 있는 문자를 decode하는 방식으로 이를 해결했습니다.

결론

  • Request를 받을 때 경로에 있는 변수를 매핑하기 위해서는 정확한 변수명이 필요하다(와일드카드x)
  • PathVariable, HttpServletRequest로 엔드포인트의 변수명을 다루는 것은 차이점이 없다.

 
PathVariable에 특별한 로직이 있을 거라고 생각했는데, 자칫 알아보지 않았으면 잘못된 지식을 가질 뻔했습니다. 앞으로도 꼼꼼히 확인해 보는 습관을 가져야겠다고 다시 한번 다짐했습니다.
 
읽어주셔서 감사합니다.