์˜ค๋Š˜ ์ •๋ฆฌํ•˜๊ณ  ๊ณต์œ ํ•˜๊ณ ์ž ํ•˜๋Š” ๋‚ด์šฉ์€ @PathVariable๋กœ ์ง€์ •๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ @RequestBody๊ฐ€ ์ง€์ •๋œ ์˜ค๋ธŒ์ ํŠธ ํ•„๋“œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ตฌํ˜„ ๋ฐฉ์•ˆ์ด๋‹ค. @PathVariable๋กœ ์ง€์ •๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ @ModelAttribute์— ์ฃผ์ž…๋˜๋„๋ก ServletModelAttributeMethodProcessor๊ฐ€ ๊ตฌํ˜„๋˜์–ด์žˆ๋Š” ๋ฐ˜๋ฉด์— @RequestBody์— ๋Œ€ํ•ด์„œ ๋ณ€ํ™˜์„ ๋‹ด๋‹นํ•˜๋Š” MappingJackson2HttpMessageConverter ์—์„œ๋Š” ObjectMapper ์— ์˜ํ•ด ๋ณ€ํ™˜๋งŒ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋˜์–ด์žˆ๋‹ค.

๋”ฐ๋ผ์„œ, ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” @PathVariable ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์ด @RequestBody ์˜ค๋ธŒ์ ํŠธ์— ์ฃผ์ž…๋˜์ง€ ์•Š๋Š”๋‹ค.

RequestBodyAdviceAdapter

ChatGPT ์—๊ฒŒ ํžŒํŠธ๋ฅผ ์–ป์–ด RequestBodyAdvice ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ RequestBody๋ฅผ ์ „ํ›„์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•„๋ƒˆ๋‹ค. ๋Œ€๋ถ€๋ถ„ @RestControllerAdvice์™€ ํ•จ๊ป˜ ๊ณตํ†ต ์˜ค๋ฅ˜์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ž‘์„ฑํ•ด์™”์„ํ…๋ฐ RequestBodyAdviceAdapter๋ฅผ ํ™•์žฅํ•˜์—ฌ ServletModelAttributeMethodProcessor ์—์„œ์˜ ๊ตฌํ˜„์ฒ˜๋Ÿผ @PathVariable๋กœ ์ง€์ •๋œ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ฐ’์„ RequestBody ์˜ค๋ธŒ์ ํŠธ์— ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ์‹œ๋„ํ•ด๋ณด์•˜๋‹ค.

PathVariableRequestBodyBinder
@RestControllerAdvice public class PathVariableRequestBodyBinder extends RequestBodyAdviceAdapter { private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @SuppressWarnings("unchecked") @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { Object object = super.afterBodyRead(body, inputMessage, methodParameter, targetType, converterType); Method method = methodParameter.getMethod(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); Map<String, ?> pathVariables = (Map<String, ?>) requestAttributes.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (method == null || pathVariables == null || pathVariables.isEmpty()) { return object; } Class<?> clazz = object.getClass(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); if (parameterNames != null && parameterNames.length > 0) { Parameter[] parameters = method.getParameters(); for (int index = 0; index < parameterNames.length; index++) { String parameterName = parameterNames[index]; Parameter parameter = parameters[index]; // NOTE: Set PathVariable into RequestBody. if (parameter.isAnnotationPresent(PathVariable.class) && pathVariables.containsKey(parameterName)) { try { Field field = clazz.getDeclaredField(parameterName); ReflectionUtils.makeAccessible(field); Object o = ReflectionUtils.getField(field, object); if (o == null) { ReflectionUtils.setField(field, object, pathVariables.get(parameterName)); } } catch (NoSuchFieldException ignored) { // ignored } } } } return object; } @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { Method method = methodParameter.getMethod(); if (method != null) { Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { if (parameter.isAnnotationPresent(PathVariable.class)) { return true; } } } return false; } }

์ปจํŠธ๋กค๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— @PathVariable ์ด ์กด์žฌํ•œ๋‹ค๋ฉด ์ฒ˜๋ฆฌํ•˜๋„๋ก supports ๊ฒฐ๊ณผ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. RequestBodyAdviceAdapter ์ž์ฒด๊ฐ€ RequestBody๋ฅผ ์ฒ˜๋ฆฌํ•จ์œผ๋กœ @PathVariable ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€๋งŒ ์ฒดํฌํ•ด๋„ ๋ฌด๋ฐฉํ•ด๋ณด์ธ๋‹ค. RequestBody๊ฐ€ ์˜ค๋ธŒ์ ํŠธ๋กœ ๋ณ€ํ™˜๋˜๊ณ ๋‚˜์„œ ์ฃผ์ž…์„ ์‹œ๋„ํ•˜๊ธฐ ์œ„ํ•ด์„œ afterBodyRead ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€๋Š”๋ฐ HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE๋กœ PathVariable ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„์„ ์ถ”๋ก ํ•˜๊ธฐ ์œ„ํ•˜์—ฌ DefaultParameterNameDiscoverer๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๊ทธ ์ด์œ ์—๋Š” @PathVariable ์ฒ˜๋Ÿผ ์ด๋ฆ„์„ ๋ณ„๋„๋กœ ์ง€์ •ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ arg0 ๊ณผ ๊ฐ™์ด ์ด๋ฆ„์ด ๋ถ€์—ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋‹ค.

@RequestBody๋กœ ๋ณ€ํ™˜๋˜์–ด์•ผํ•˜๋Š” ์˜ค๋ธŒ์ ํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์‹ค์ œ๋กœ ํ•„๋“œ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€์— ๋Œ€ํ•ด์„œ๋Š” ๋ฆฌํ”Œ๋ ‰์…˜ API์— ๋Œ€ํ•ด์„œ ๊นŠ์€ ์ดํ•ด๊ฐ€ ์—†๋Š” ๊ด€๊ณ„๋กœ ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์— ํฌํ•จ๋˜์–ด์žˆ๋Š” ReflectionUtils ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜์˜€๋‹ค. ์ž๋ฐ”์—์„œ ๋Œ€๋ถ€๋ถ„์˜ ์˜ค๋ธŒ์ ํŠธ ํ•„๋“œ์—๋Š” private ์ ‘๊ทผ ์ œ์–ด์ž๋ฅผ ์„ ์–ธํ•˜๋ฏ€๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ ์ดํ›„์— ํ•„๋“œ์— ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์— ํ•œํ•ด์„œ๋งŒ PathVariable์— ๋Œ€ํ•œ ๊ฐ’์„ ๋ฐ”์ธ๋”ฉํ•˜์˜€๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ์•„๋ž˜์˜ ์ด๋ฏธ์ง€์™€ ๊ฐ™์ด ์š”์ฒญ ๋ฐ์ดํ„ฐ์— ์•„์ด๋””๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์•„๋„ PathVariable์— ํ•ด๋‹น๋˜๋Š” 111 ๊ฐ’์ด ํฌํ•จ๋จ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

DefaultParameterNameDiscoverer

์Šคํ”„๋ง ๋ถ€ํŠธ 3.2 ๋ถ€ํ„ฐ๋Š” LocalVariableTableParameterNameDiscoverer๊ฐ€ ์•„๋‹Œ StandardReflectionParameterNameDiscoverer๊ฐ€ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ -parameters ์ปดํŒŒ์ผ ์˜ต์…˜์„ ํ™œ์„ฑํ•ด์•ผํ•œ๋‹ค. ์Šคํ”„๋ง ๋ถ€ํŠธ 2.7.18 ๋ฒ„์ „ ๊ธฐ์ค€์˜ DefaultParameterNameDiscoverer๋ฅผ ๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋˜์–ด์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

DefaultParameterNameDiscoverer
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { public DefaultParameterNameDiscoverer() { if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) { this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer()); } this.addDiscoverer(new StandardReflectionParameterNameDiscoverer()); this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer()); } }