MVC - PathPatternParser 동작 이해

this-is-spear·2023년 3월 4일
2

스프링 기초부터

목록 보기
3/3

Intro

요약

  • Spring 경로 분석 방법을 학습했습니다.
  • AntPathMatcher, PathPatternParser 차이점을 정리했습니다.
  • PathPatternParser의 동작을 학습했습니다.
  • Spring Security에서 필터링을 위한 경로 Parser인 AntPathRequestMatcher도 정리했습니다.

학습하게 된 계기

인터셉터에서 addPathPattern 메서드로 URI 필터링을 제공하는데, Http Method로도 필터링하고 싶었습니다. 추가적인 필터링 진행하기 위해서 Spring Security의 AntRequestMatcher를 사용했는데, 다음과 같은 문제가 생겼습니다.

  • Spring Security와 는 다른 목적으로 사용된 문제
  • 인터셉터와 Spring Security간 결합력이 생기는 문제

이러한 문제들을 해결하기 위해 변경할 방법을 모색하던 중 PathPatternParser의 존재를 확인했고, 이 방법을 학습했습니다.

Spring Path Matching

Path Matching Using String

컨트롤러 매핑과 비교할 수 있도록 하려면 URI를 디코딩해야 하는데, 경로 구조를 변경할 가능성이 있기 때문에 다시 바람직하지 않습니다.

boolean matches = "/users/register".equals("/users/register")

요청 URI를 바로 디코딩해 일치하는지 확인할 때 발생하는 문제

  • 기존 경로 구조가 변경될 가능성이 있다.
  • 예측하지 않은 문자열을 필터링 기능을 제공할 수 없다.
  • Servlet간 URL을 공유할 수 있는 공간을 제공할 수 없다.

이러한 이유로 URL 디코딩을 위해 Spring 1.0에서 AntPathMatcher 클래스가 탄생고 Spring 6.0 이후 PathPatternParser로 변경되어 더 많은 기능들을 제공하고 있습니다.

AntPathMatcher

AntPathMatcher는 Spring 1.0부터 제공된 경로 분석 유틸 클래스로 Ant style path pattern을 기반으로 URL을 일치 여부를 확인합니다.

  @Test
  void testAntPathMatcher() {
      AntPathMatcher antPathMatcher = new AntPathMatcher();
      antPathMatcher.match("/users/register", "users/register");
  }

Ant Style

Ant 스타일 패턴은 특정 구조를 가진 문자열을 일치시키는 데 사용되며 패턴에는 입력 문자열의 일부와 일치할 수 있는 와일드카드 및 자리 표시자가 포함될 수 있습니다.

Ant 스타일 패턴에 사용되는 일반적인 와일드카드 문자는 별표(*)로 빈 문자열을 포함한 모든 문자열과 일치합니다. ?와 같은 단일 와일드카드 문자를 일치시키는 데 사용할 수도 있습니다.

  	PATH : /users/**
    is equal to
    URI : /users/
    URI : /users/123
    URI : /users/123/profile
    URI : /users/123/profile/edit

PathPatternParser

Spring 5.3 이상에서 제공되는 경로 분석 유틸 클래스이며, PathPattern 문자열을 분석하고 URL 일치 여부를 확인할 수 있는 PathPattern을 만들게 됩니다.

  @Test
  void testPathPatternParser() {
      PathPatternParser pathPatternParser = new PathPatternParser();
      PathContainer parsePath = PathContainer.parsePath("/users/register");

      PathPattern pattern = pathPatternParser.parse("/users/register");

      boolean matches = pattern.matches(parsePath);
      assertThat(matches).isTrue();
  }

AntPathMatcher에서 PathPatternParser로 변경된 이유

PathElement 덕분에 wildcard 패턴과 caturing variant에 제약이 없고, PathContainer 덕분에 디코딩된 예약 문자가 경로 구조를 변경할 위험 없이 한 번에 하나의 경로 세그먼트를 일치시킬 수 있습니다.

  • 기능의 다양성 - org.springframework.web.util.pattern.PathPattern 설명
    • AntPathMatcher** 패턴을 제일 마지막에만 사용할 수 있다.
    • AntPathMatcher{*spring}와 같은 capturing variant도 마지막에만 사용할 수 있다.
  • 경로가 변경될 위험성 - spring docs
    • Unlike AntPathMatcher which needs either the lookup path decoded or the controller mapping encoded, a parsed PathPattern matches to a parsed representation of the path called RequestPath, one path segment at a time.
    • This allows decoding and sanitizing path segment values individually without the risk of altering the structure of the path.

즉, PathPatternParser로 동작한다면 경로 분석을 위해 요청 URI는 PathContainer에 의해 래핑됩니다. PathContainer에서는 path parameter가 제거된 상태로 관리가 됩니다. 그 후, PathPattern과 비교하는데, seperator(/)로 인해 PathElement 단위로 쪼개져 비교합니다.

위 내용을 이해하기 위해 동작을 정리했습니다.

PathPatternParser

PathPatternParser 동작을 알기 전 알아야 할 클래스들

PathPatternParser의 동작을 이해하기 위해 클래스 동작을 쉽게 정리했습니다.

PathContainer

요청 URI가 들어오게 된다면 경로를 PathContainer로 래핑합니다. parsePath 메서드는 경로 파라미터를 제거한 상태에서 디코딩된 형태로 내용을 표시합니다.

PathContainer#parsePath

static PathContainer parsePath(String path) {
    return DefaultPathContainer.createFromUrlPath(path, Options.HTTP_PATH);
}

PathPattern

구문 분석된 경로 패턴을 나타냅니다. 빠른 연산을 위해 PathElement 체이닝으로 비교해 계산된 결과를 누적합니다.

PathPattern#matches

public class PathPattern implements Comparable<PathPattern> {
  
  private final PathElement head;
  
  //...
  
  public boolean matches(PathContainer pathContainer) {
      // ...
      MatchingContext matchingContext = new MatchingContext(pathContainer, false);
      return this.head.matches(0, matchingContext);
  }
}

PathElement

PathPatternPathContainer와 비교할 때, seperator를 기준으로 PathElement로 나뉘어져 비교합니다. 나뉠 때, PathElement에 맞게 비교됩니다.

아래는 Elemet 클래스의 종류입니다.

  • Capture*PathElement
  • LiteralPathElement
  • RegexPathElement
  • SeperatorPathElement
  • SingleCharWildcardedPathElement
  • WildCard*PathElement

/user/{id} 으로 요청이 온다면 /SeparatorPathElement에서 userLiteralPathElement{id}CaptureVariablePathElement 에서 분석됩니다.

PathPatternParser 동작을 알아보자

PathPatternParser

PathPatternParser는 다량의 요청에 대해 지속적인 경로 비교를 효율적으로 도와주는 클래스입니다.

PathPatternParser#parse

경로 상 문자열을 한 번에 한 문자씩 처리해 seperator를 중심으로 PathElement로 나누고 각 단계에서 구조를 확인합니다.

public PathPattern parse(String pathPattern) throws PatternParseException {
    return new InternalPathPatternParser(this).parse(pathPattern);
}

이 때, InternalPathPatternParser를 호출해서 사용하게 되는데, 그 이유는 PathPatternParser가 thread-safe하지 않기 때문입니다.

InternalPathPatternParser#parse

경로 상 문자열을 한 번에 한 문자씩 처리해 seperator를 중심으로 PathElement로 나누고 각 단계에서 구조를 확인합니다. 전체적인 동작 과정을 아래와 같습니다.

  1. 파싱을 위해 클래스를 초기화한다.
  2. 문자열을 파싱한다.
  3. PathPattern를 반환한다.
public PathPattern parse(String pathPattern) throws PatternParseException {
  	    // 1. 초기화 과정 
  
        // 2. 문자열 파싱 
		while (this.pos < this.pathPatternLength) {
			// 문자열 한 글자씩 검사 로직
			this.pos++;
		}
  
        // ...

        return new PathPattern(pathPattern, this.parser, this.headPE);
	}

PathElement로 나누는 과정은 2 번째 단계에서 진행합니다. sperator를 만나게 되면 방금까지 모은 문자열을 PathElement로 만듭니다.

public PathPattern parse(String pathPattern) throws PatternParseException {
    // 1. 초기화 과정

    // 2. 문자열 파싱 
    while (this.pos < this.pathPatternLength) {
        // ...

        if (ch == separator) {
            if (this.pathElementStart != -1) {
                pushPathElement(createPathElement());
            }
            if (peekDoubleWildcard()) {
                pushPathElement(new WildcardTheRestPathElement(this.pos, separator));
                this.pos += 2;
            }
            else {
                pushPathElement(new SeparatorPathElement(this.pos, separator));
            }
        }
    }
    // ...
}

앞서 빠른 연산을 위해 PathElement 체이닝으로 비교해 계산된 결과를 누적한다고 말씀드렸는데, pushPathElement 과정으로 동작할 때 체이닝하는 것을 볼 수 있었습니다.

InternalPathPatternParser#pushPathElement

새로은 PathElement가 오게 되면 이중 연결 리스트로 체이닝하게 됩니다.

  private void pushPathElement(PathElement newPathElement) {
      if (newPathElement instanceof CaptureTheRestPathElement) {
          //...
      }
      else {
          if (this.headPE == null) {
              // ...
          }
          else if (this.currentPE != null) {
              this.currentPE.next = newPathElement;
              newPathElement.prev = this.currentPE;
              this.currentPE = newPathElement;
          }
      }
  }

체이닝된 PathElementPathPattern에 의해 사용될 때, SeparatorPathElement#matches에서 다음 원소로 이동할 수 있도록 도와줍니다.

SeparatorPathElement#matches

  @Override
  public boolean matches(int pathIndex, MatchingContext matchingContext) {
      if (pathIndex < matchingContext.pathLength && matchingContext.isSeparator(pathIndex)) {
          if (isNoMorePattern()) {
              //...
          }
          else {
              // 다음 PathElement 호출
              return (this.next != null && this.next.matches(pathIndex, matchingContext));
          }
      }
      return false;
  }

번외 : Spring Security에서 AntPathRequestMatcher

Spring Security의 SpringAntMatcherAntPathMatcher를 컴포지션해 사용하며, AntPathRequestMatcher의 내부 정적 클래스로 선언하고 있습니다.

public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor {
  // 
  private static final class SpringAntMatcher implements Matcher {
      private final AntPathMatcher antMatcher;
      private final String pattern;

      @Override
      public boolean matches(String path) {
          return this.antMatcher.match(this.pattern, path);
      }
      // ...
  }
}

조금 다른 점은 AntPathRequestMatcher는 HttpMethod 비교를 지원하고 있습니다.

public static AntPathRequestMatcher antMatcher(HttpMethod method, String pattern) {
    //...
    return new AntPathRequestMatcher(pattern, method.name());
}

추가로 request를 인자로 받기 때문에 사용자 입장에서 편리하게 사용할 수 있습니다.

@Override
public boolean matches(HttpServletRequest request) {
    // ...
    return this.matcher.matches(url);
}

Outro

요약

  • PathPatternParser는 wildcard 패턴과 caturing variant에 제약이 없고, 경로 구조를 변경할 위험이 없습니다.
  • Spring MVC에서 Method에 대한 필터링을 제공하고 있지 않습니다.
  • Spring Security에서 제공하는 AntPathRequestMatcher를 활용하는 것도 방법입니다.

마무리

Method 필터링을 사용해야 한다면 PathPatternParser를 컴포지션해야 한다는 것을 알 수 있었습니다.

profile
익숙함을 경계하자

0개의 댓글