본문 바로가기
Spring

RequestMapping("/") vs RequestMapping("")

by e-pd 2021. 10. 7.

발단은 이렇다. 톡방에 RequestMapping의 질문이 올라왔다.

크게 고민해본적이 없어 갑자기 호기심이 생겼다.

 

@RestController
public class HelloController {

    @GetMapping("/")
    public String foo() {
        return "absolute";
    }

    @GetMapping("")
    public String foo2() {
        return "empty";
    }
}

Controller를 생성했다.

http://localhost:8080으로 접근하면 어떤 것이 실행될까?

 

특별히 /를 붙이지 않았지만 @GetMapping("/")으로 접근이 되었다.

 

@RestController
public class HelloController {

//    @GetMapping("/")
//    public String foo() {
//        return "absolute";
//    }

    @GetMapping("")
    public String foo2() {
        return "empty";
    }
}

/ 으로 접근하는 메서드를 제거하면 접속이 어떻게 될까?

 

재미있게도 ""으로 맵핑된 경로로 접근되었다. 

 

여기서 추론할 수 있는 것은 경로에 빈값이 입력되면 

맵핑된 "/"값이 있으면 우선적으로 접근하고 없다면 빈값으로 접근한다는 것이다.

 

 

그렇다면 / 뒤에 값이 붙을 때는 어떻게 될까?

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String foo() {
        return "absolute";
    }

    @GetMapping("hello")
    public String foo2() {
        return "empty";
    }
}

스프링 애플리케이션을 실행해보면 보기좋게 실패한다.

원인을 살펴보면 이미 등록된 Bean으로 인해 실패했다는 메시지가 보인다.

 

Mapping에서 오류가 났기때문에 HanlderMapping에서 부터 단서를 찾아나간다.

 

핸들러 맵핑은 리퀘스트와 핸들되는 객체간의 맵핑을 정의한다.

핸들러 매서드에서는 등록할 bean 후보중에서 등록처리를 하게 된다.

 

	protected void initHandlerMethods() {
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

 

후보bean이 확인되면 HandlerMethod를 찾아내는 작업을 하게된다.

 

protected void detectHandlerMethods(Object handler) {
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
			else if (mappingsLogger.isDebugEnabled()) {
				mappingsLogger.debug(formatMappings(userType, methods));
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}

 

자세히 내용을 보면 Map 자료구조에 선택된 메서드들을 담고 있는 것을 볼 수 있다.

 

위에서 7번째줄의 MethodIntrospector.selectMethods 

JavaDoc을 보면 다음과 같은 설명이 써있다.

 

 

Select methods on the given target type based on the lookup of associated metadata.
Callers define methods of interest through the MethodIntrospector.MetadataLookup parameter, allowing to collect the associated metadata into the result map.

메타데이터의 기반하여 주어진 목표 클래스의 메서드를 선택한다. MethodIntrospector.MetadataLookup의 파라미터를 통해 메서드를 정의하고 관련된 정보를 맵에 담는다.

 

	public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
		final Map<Method, T> methodMap = new LinkedHashMap<>();
		Set<Class<?>> handlerTypes = new LinkedHashSet<>();
		Class<?> specificHandlerType = null;

		if (!Proxy.isProxyClass(targetType)) {
			specificHandlerType = ClassUtils.getUserClass(targetType);
			handlerTypes.add(specificHandlerType);
		}
		handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));

		for (Class<?> currentHandlerType : handlerTypes) {
			final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);

			ReflectionUtils.doWithMethods(currentHandlerType, method -> {
				Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
				T result = metadataLookup.inspect(specificMethod);
				if (result != null) {
					Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
					if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
						methodMap.put(specificMethod, result);
					}
				}
			}, ReflectionUtils.USER_DECLARED_METHODS);
		}

		return methodMap;
	}

 

for 문 부분은 리플랙션을 통한 method 확인작업이다. 어떤 원리로 작동되는지 참고 유튜브 링크를 걸어둔다. (리플랙션의 활용이 궁금하다면 시리즈를 다 보기를 추천한다)

https://youtu.be/P5fPc2tjOko

 

중요한 것은 ClassUtils.getMostSpecificMethod에 의해 메서드가 특정된다. 

이때 추출된 method 정보에 delaredAnnotations 정보도 나온다. 여기서 value=> memberValues을 확인하면

내가 컨트롤러에 선언했던 path의 value값이 적혀있다.

 

이 정보를 가지고 이제 result 정보를 만들어낸다. 

result정보에 patternsCondition으로 값이 담겨있다.

 

patternsCondition 어디서 왔을까?

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.html

 

PatternsRequestCondition (Spring Framework 5.3.10 API)

Return the discrete items a request condition is composed of. For example URL patterns, HTTP request methods, param expressions, etc.

docs.spring.io

재미있는 사실을 발견했다.

javadoc 설명을 보니

PatternsRequestCondition(String... patterns)

Constructor with URL patterns which are prepended with "/" if necessary.

 

여기서 URL 패턴에 "/"를 붙인다는 것이다.

 

private static SortedSet<PathPattern> parse(PathPatternParser parser, String... patterns) {
		if (patterns.length == 0 || (patterns.length == 1 && !StringUtils.hasText(patterns[0]))) {
			return EMPTY_PATH_PATTERN;
		}
		SortedSet<PathPattern> result = new TreeSet<>();
		for (String path : patterns) {
			if (StringUtils.hasText(path) && !path.startsWith("/")) {
				path = "/" + path;
			}
			result.add(parser.parse(path));
		}
		return result;
	}

 

그럼 결론적으로 메서드가 담긴 Map은 어떻게 됐을까?

 

patterns conditions을 통해 같은 method로 담기게 되었다.

 

여기서 메서드들은 registerHandlerMethod 를 통해 등록이 된다.

 

하지만 핸들러 메서드를 검증하는 validateMethodMapping에서 중복된 메서드를 발견하게 되고 

예외상태 에러를 던지게 된다.

 

그래서 path 중복시 이러한 메시지를 개발자가 보게되는 것이다.

 

 

다시 맨처음 질문으로 돌아가자.

"/"와 ""은 왜 충돌이 안났을까?

실제로는 다르게 method가 들어갔기때문이다.

 

 

그러면 둘다 작동은 되니 prefix는 안붙여도 되는걸까?

 

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-config

 

Web on Servlet Stack

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more com

docs.spring.io

문서를 확인하면 / 기본 설정으로 되어있는 것을 확인할 수 있다.

 

 

 

 

p.s. 덤

뒤에 슬래시가 붙는 경우에도 잘되는 이유는 뭘까?

https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/set-use-trailing-slash-match.html

 

Spring MVC - URI matching, using setUseTrailingSlashMatch() method of RequestMappingHandlerMapping

Spring MVC - URI matching, using setUseTrailingSlashMatch() method of RequestMappingHandlerMapping [Last Updated: Mar 7, 2018]

www.logicbig.com

Whether to match to URLs irrespective of the presence of a trailing slash. If enabled a method mapped to "/users" also matches to "/users/".
The default value is true.

주석을 보면 해당 기능을 통해 뒷부분의 /도 앞과 매칭시키는 것을 볼 수 있고 기본 값은 true로 설정되어있다.

  
    public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
		this.useTrailingSlashMatch = useTrailingSlashMatch;
		if (getPatternParser() != null) {
			getPatternParser().setMatchOptionalTrailingSeparator(useTrailingSlashMatch);
		}
	}

 

 

덤2. 스프링 시큐리티에서 보안이슈로 // 더블 슬래쉬의 경우 /로 변경한다.

Using a Slash Character in Spring URLs | Baeldung

 

'Spring' 카테고리의 다른 글

Propagation 주석  (0) 2021.08.16
인터파크 API 사용하기  (0) 2021.07.26
@NotNull @NotEmpty @NotBlank  (0) 2021.01.16
Parameterized Test를 이용해서 여러 값 검증하기  (0) 2021.01.16
Component Scan  (0) 2020.08.15