본문으둜 κ±΄λ„ˆλ›°κΈ°

🎁 MVC Argument Resolution

κ°œμš”β€‹

이 λ¬Έμ„œλŠ” HTTP μš”μ²­ 데이터λ₯Ό 컨트둀러 λ©”μ„œλ“œ λ§€κ°œλ³€μˆ˜μ— μžλ™μœΌλ‘œ λ°”μΈλ”©ν•˜λŠ” Sprout Framework의 인수 ν•΄κ²° μ‹œμŠ€ν…œμ— λŒ€ν•œ 기술적 뢄석을 μ œκ³΅ν•©λ‹ˆλ‹€. 이 μ‹œμŠ€ν…œμ€ μœ μ—°ν•˜κ³  ν™•μž₯ κ°€λŠ₯ν•œ ν•΄κ²°μž 체인을 톡해 μ›μ‹œ HTTP 데이터(경둜 λ³€μˆ˜, 헀더, 쿼리 λ§€κ°œλ³€μˆ˜, μš”μ²­ λ³Έλ¬Έ)λ₯Ό κ°•λ ₯ν•œ νƒ€μž…μ˜ λ©”μ„œλ“œ 인수둜 λ³€ν™˜ν•©λ‹ˆλ‹€.

μ•„ν‚€ν…μ²˜ κ°œμš”β€‹

인수 ν•΄κ²° 흐름​

HTTP μš”μ²­ 데이터 β†’ CompositeArgumentResolver β†’ νŠΉμ • ArgumentResolverλ“€
↓
컨트둀러 λ©”μ„œλ“œ λ§€κ°œλ³€μˆ˜ ← TypeConverter ← ν•΄κ²°λœ μΈμˆ˜λ“€

ꡬ성 μš”μ†Œ μƒν˜Έμž‘μš©β€‹

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HTTP μš”μ²­ │───→│ CompositeArgument │───→│ ArgumentResolverβ”‚
β”‚ - 경둜 λ³€μˆ˜ β”‚ β”‚ Resolver β”‚ β”‚ κ΅¬ν˜„μ²΄λ“€ β”‚
β”‚ - 헀더 β”‚ β”‚ (μ‘°μ •μž) β”‚ β”‚ β”‚
β”‚ - 쿼리 λ§€κ°œλ³€μˆ˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ - λ³Έλ¬Έ β”‚ ↓ ↓
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ νƒ€μž… λ³€ν™˜κΈ° │←───│ ν•΄κ²°λœ κ°’λ“€ β”‚
β”‚ (νƒ€μž… μ•ˆμ „μ„±) β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

핡심 ꡬ성 μš”μ†Œ 뢄석​

1. CompositeArgumentResolver: ν•΄κ²° μ‘°μ •μžβ€‹

μœ„μž„ μ „λž΅

CompositeArgumentResolverλŠ” μ—¬λŸ¬ νŠΉν™”λœ ν•΄κ²°μžλ“€μ„ μ‘°μ •ν•˜κΈ° μœ„ν•΄ 볡합 νŒ¨ν„΄μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

public Object[] resolveArguments(Method method, HttpRequest<?> request, 
Map<String, String> pathVariables) throws Exception {
Parameter[] params = method.getParameters();
Object[] args = new Object[params.length];

for (int i = 0; i < params.length; i++) {
Parameter p = params[i];

// 첫 번째 지원 ν•΄κ²°μž μ°ΎκΈ°
ArgumentResolver resolver = delegates.stream()
.filter(ar -> ar.supports(p))
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"No ArgumentResolver for parameter " + p));

args[i] = resolver.resolve(p, request, pathVariables);
}
return args;
}

섀계 νŒ¨ν„΄ 뢄석

  1. μ±…μž„ 연쇄 νŒ¨ν„΄: 각 ν•΄κ²°μžκ°€ λ§€κ°œλ³€μˆ˜λ₯Ό μ²˜λ¦¬ν•  수 μžˆλŠ”μ§€ 확인
  2. μ „λž΅ νŒ¨ν„΄: λ‹€μ–‘ν•œ λ§€κ°œλ³€μˆ˜ μœ ν˜•μ— λŒ€ν•œ μ„œλ‘œ λ‹€λ₯Έ ν•΄κ²° μ „λž΅
  3. ν…œν”Œλ¦Ώ λ©”μ„œλ“œ: νŠΉν™”λœ κ΅¬ν˜„κ³Ό ν•¨κ»˜ν•˜λŠ” 곡톡 ν•΄κ²° ν”„λ ˆμž„μ›Œν¬

μ„±λŠ₯ νŠΉμ„±

  • μ‹œκ°„ λ³΅μž‘λ„: O(n * m) (n = λ§€κ°œλ³€μˆ˜ 수, m = 평균 ν™•μΈλœ ν•΄κ²°μž 수)
  • μ‘°κΈ° μ’…λ£Œ: 첫 번째 μΌμΉ˜ν•˜λŠ” ν•΄κ²°μžμ—μ„œ 쀑단
  • 캐싱 기회: 반볡 ν˜ΈμΆœμ„ μœ„ν•œ ν•΄κ²°μž λ§€ν•‘ μΊμ‹œ κ°€λŠ₯

였λ₯˜ 처리 μ „λž΅

.orElseThrow(() -> new IllegalStateException("No ArgumentResolver for parameter " + p));
  • λΉ λ₯Έ μ‹€νŒ¨ μ ‘κ·Ό: μ•Œ 수 μ—†λŠ” λ§€κ°œλ³€μˆ˜ μœ ν˜•μ€ μ¦‰μ‹œ μ‹€νŒ¨
  • 디버깅을 μœ„ν•œ λͺ…ν™•ν•œ 였λ₯˜ λ©”μ‹œμ§€
  • 뢀뢄적 ν•΄κ²° μ‹œλ„ μ—†μŒ

2. ArgumentResolver μΈν„°νŽ˜μ΄μŠ€: ν•΄κ²°μž 계약​

계약 μ •μ˜

public interface ArgumentResolver {
boolean supports(Parameter parameter);
Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception;
}

2단계 ν•΄κ²° ν”„λ‘œν† μ½œ

  1. 지원 확인: ν•΄κ²°μžκ°€ λ§€κ°œλ³€μˆ˜λ₯Ό μ²˜λ¦¬ν•  수 μžˆλŠ”μ§€ κ²°μ •
  2. ν•΄κ²°: μ‹€μ œ κ°’ μΆ”μΆœ 및 λ³€ν™˜ μˆ˜ν–‰

μΈν„°νŽ˜μ΄μŠ€ 섀계 이점

  • ν™•μž₯μ„±: μƒˆλ‘œμš΄ ν•΄κ²°μž μœ ν˜• μΆ”κ°€ 용이
  • ν…ŒμŠ€νŠΈ κ°€λŠ₯μ„±: 각 ν•΄κ²°μžλ₯Ό λ…λ¦½μ μœΌλ‘œ λ‹¨μœ„ ν…ŒμŠ€νŠΈ κ°€λŠ₯
  • 관심사 뢄리: λͺ…ν™•ν•œ μ±…μž„ 경계

3. TypeConverter: μ€‘μ•™ν™”λœ νƒ€μž… λ³€ν™˜β€‹

λ³€ν™˜ μ•Œκ³ λ¦¬μ¦˜

public static Object convert(String value, Class<?> targetType) {
if (value == null) {
if (targetType.isPrimitive()) {
throw new IllegalArgumentException(
"Null value cannot be assigned to primitive type: " + targetType.getName());
}
return null;
}

if (targetType.equals(String.class)) {
return value;
} else if (targetType.equals(Long.class) || targetType.equals(long.class)) {
return Long.parseLong(value);
} else if (targetType.equals(Integer.class) || targetType.equals(int.class)) {
return Integer.parseInt(value);
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
return Boolean.parseBoolean(value);
}

throw new IllegalArgumentException(
"Cannot convert String value [" + value + "] to target class [" + targetType.getName() + "]");
}

νƒ€μž… μ•ˆμ „μ„± κΈ°λŠ₯

  1. κΈ°λ³Έ νƒ€μž… 널 검사: κΈ°λ³Έ νƒ€μž…μ— 널 ν• λ‹Ή λ°©μ§€
  2. 래퍼 νƒ€μž… 처리: κΈ°λ³Έ νƒ€μž…κ³Ό 래퍼 νƒ€μž… λͺ¨λ‘ 지원
  3. λͺ…μ‹œμ  νƒ€μž… λ§€ν•‘: μ§€μ›λ˜λŠ” νƒ€μž…μ— λŒ€ν•œ λͺ…ν™•ν•œ λ³€ν™˜ κ·œμΉ™
  4. μ•ˆμ „ μž₯치: μ§€μ›λ˜μ§€ μ•ŠλŠ” λ³€ν™˜μ— λŒ€ν•œ μ˜ˆμ™Έ λ°œμƒ

μ§€μ›λ˜λŠ” λ³€ν™˜

μ†ŒμŠ€λŒ€μƒ νƒ€μž…λ³€ν™˜ λ©”μ„œλ“œ
StringStringν•­λ“±
StringLong/longLong.parseLong()
StringInteger/intInteger.parseInt()
StringBoolean/booleanBoolean.parseBoolean()

ν•΄κ²°μž κ΅¬ν˜„ 뢄석​

1. PathVariableArgumentResolver​

지원 감지

public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(PathVariable.class);
}

ν•΄κ²° 둜직

public Object resolve(Parameter parameter, HttpRequest<?> request, 
Map<String, String> pathVariables) throws Exception {
PathVariable pathVariableAnnotation = parameter.getAnnotation(PathVariable.class);
String variableName = pathVariableAnnotation.value();

if (variableName.isEmpty()) {
variableName = parameter.getName(); // 섀정보닀 κ΄€λ‘€
}

String value = pathVariables.get(variableName);
if (value == null) {
throw new IllegalArgumentException(
"Path variable '" + variableName + "' not found in path.");
}

return TypeConverter.convert(value, parameter.getType());
}

μ£Όμš” κΈ°λŠ₯

  • 섀정보닀 κ΄€λ‘€: μ–΄λ…Έν…Œμ΄μ…˜ 값이 λΉ„μ–΄μžˆμœΌλ©΄ λ§€κ°œλ³€μˆ˜ 이름 μ‚¬μš©
  • μ—„κ²©ν•œ 검증: 경둜 λ³€μˆ˜κ°€ μ—†μœΌλ©΄ μ˜ˆμ™Έ λ°œμƒ
  • νƒ€μž… λ³€ν™˜: μ€‘μ•™ν™”λœ λ³€ν™˜κΈ°μ— μœ„μž„

2. RequestParamArgumentResolver​

ν–₯μƒλœ 지원 둜직

public Object resolve(Parameter parameter, HttpRequest<?> request, 
Map<String, String> pathVariables) throws Exception {
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
String paramName = requestParam.value().isEmpty() ?
parameter.getName() : requestParam.value();

String paramValue = request.getQueryParams().get(paramName);

if (paramValue == null) {
if (requestParam.required()) {
throw new IllegalArgumentException(
"Required request parameter '" + paramName + "' not found in request.");
}
}

return TypeConverter.convert(paramValue, parameter.getType());
}

κ³ κΈ‰ κΈ°λŠ₯

  • 선택적 λ§€κ°œλ³€μˆ˜: ν•„μˆ˜/선택적 ꡬ뢄 지원
  • 널 처리: λˆ„λ½λœ 선택적 λ§€κ°œλ³€μˆ˜μ˜ μš°μ•„ν•œ 처리
  • 검증 둜직: ν•„μˆ˜ λ§€κ°œλ³€μˆ˜ μ œμ•½ 쑰건 κ°•μ œ

3. HeaderArgumentResolver​

이쀑 λͺ¨λ“œ ν•΄κ²°

μ‹œμŠ€ν…œμ€ 두 개의 헀더 ν•΄κ²°μžλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

κ°œλ³„ 헀더 ν•΄κ²°

// HeaderArgumentResolver - νŠΉμ • 헀더 처리
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(Header.class) &&
!parameter.getAnnotation(Header.class).value().isEmpty();
}

λͺ¨λ“  헀더 ν•΄κ²°

// AllHeaderArgumentResolver - 헀더 λ§΅ μ£Όμž… 처리
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(Header.class) &&
parameter.getAnnotation(Header.class).value().isEmpty();
}

νƒ€μž… 기반 λ””μŠ€νŒ¨μΉ˜

if (parameter.getType().equals(Map.class)) {
if (headerName.isBlank()) {
return request.getHeaders(); // λͺ¨λ“  헀더 λ°˜ν™˜
} else {
throw new IllegalArgumentException(
"Cannot bind specific header '" + headerName +
"' to a Map parameter. Use Map<String, String> without @Header for all headers.");
}
}

4. RequestBodyArgumentResolver​

JSON 역직렬화 톡합

@Component
public class RequestBodyArgumentResolver implements ArgumentResolver {
private final ObjectMapper objectMapper = new ObjectMapper();

public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
String rawBody = (String) request.getBody();

if (rawBody == null || rawBody.isBlank()) {
return null;
}

try {
return objectMapper.readValue(rawBody.trim(), parameter.getType());
} catch (Exception e) {
throw new BadRequestException(
"Failed to parse request body as JSON or convert to '" +
parameter.getType().getName() + "'. Check JSON format and target type. Cause: " +
e.getMessage(), ResponseCode.BAD_REQUEST, e);
}
}
}

κ³ κΈ‰ κΈ°λŠ₯

  • Jackson 톡합: JSON 역직렬화λ₯Ό μœ„ν•œ ObjectMapper μ‚¬μš©
  • μ œλ„€λ¦­ νƒ€μž… 지원: λͺ¨λ“  클래슀둜 역직렬화 κ°€λŠ₯
  • 였λ₯˜ μ»¨ν…μŠ€νŠΈ: μƒμ„Έν•œ 였λ₯˜ 정보 제곡
  • HTTP μƒνƒœ λ§€ν•‘: νŒŒμ‹± 였λ₯˜λ₯Ό μ μ ˆν•œ HTTP 응닡에 λ§€ν•‘

νƒ€μž… μ‹œμŠ€ν…œ 톡합​

Java λ¦¬ν”Œλ ‰μ…˜ 톡합​

λ§€κ°œλ³€μˆ˜ λ‚΄μ„±

Parameter[] params = method.getParameters();
// 각 ParameterλŠ” λ‹€μŒμ„ 제곡:
// - parameter.getType() - νƒ€μž… 확인을 μœ„ν•œ Class<?>
// - parameter.getName() - 섀정보닀 κ΄€λ‘€λ₯Ό μœ„ν•œ String
// - parameter.getAnnotations() - 메타데이터λ₯Ό μœ„ν•œ Annotation[]
// - parameter.isAnnotationPresent(Class) - λΉ λ₯Έ 확인을 μœ„ν•œ boolean

νƒ€μž… μ†Œκ±° 처리

μ‹œμŠ€ν…œμ€ ν˜„μž¬ κΈ°λ³Έ νƒ€μž…μ„ μ²˜λ¦¬ν•˜μ§€λ§Œ μ œλ„€λ¦­ νƒ€μž…μ—λŠ” μ œν•œμ΄ μžˆμŠ΅λ‹ˆλ‹€.

// μž‘λ™ν•¨
public void method(@RequestBody User user)

// μ œν•œμ  - μ œλ„€λ¦­ νƒ€μž… 정보 손싀
public void method(@RequestBody List<User> users)

잠재적 κ°œμ„ 

// μ œλ„€λ¦­ 지원을 μœ„ν•΄ ParameterizedType μ‚¬μš© κ°€λŠ₯
if (parameter.getParameterizedType() instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) parameter.getParameterizedType();
Type[] actualTypeArguments = pType.getActualTypeArguments();
// List<User>, Map<String, Object> λ“± 처리
}

μ„±λŠ₯ 뢄석​

ν•΄κ²° λ³΅μž‘λ„β€‹

μš”μ²­λ‹Ή ν•΄κ²°

  • μ‹œκ°„: O(p * r) (p = λ§€κ°œλ³€μˆ˜, r = 평균 확인할 ν•΄κ²°μž 수)
  • 곡간: O(p) (인수 λ°°μ—΄ ν• λ‹Ή)
  • μ΅œμ ν™”: ν•΄κ²°μž 캐싱 κ΅¬ν˜„ κ°€λŠ₯

νƒ€μž… λ³€ν™˜ μ˜€λ²„ν—€λ“œ

  • κΈ°λ³Έ νƒ€μž… λ³€ν™˜: O(1)
  • λ¬Έμžμ—΄ μ—°μ‚°: O(1)
  • JSON 역직렬화: O(json_크기)

λ©”λͺ¨λ¦¬ μ‚¬μš© νŒ¨ν„΄β€‹

ν•΄κ²°μž 체인

private final List<ArgumentResolver> delegates;
  • λͺ¨λ“  μš”μ²­μ—μ„œ κ³΅μœ λ˜λŠ” 정적 ν•΄κ²°μž λͺ©λ‘
  • μš”μ²­λ‹Ή ν•΄κ²°μž ν• λ‹Ή μ—†μŒ

인수 λ°°μ—΄

Object[] args = new Object[params.length];
  • λ©”μ„œλ“œ ν˜ΈμΆœλ‹Ή μž„μ‹œ λ°°μ—΄
  • λ©”μ„œλ“œ μ‹œκ·Έλ‹ˆμ²˜λ‘œ 크기 κ²°μ •

μ΅œμ ν™” κΈ°νšŒβ€‹

ν•΄κ²°μž λ§€ν•‘ μΊμ‹œ

// 잠재적 κ°œμ„ 
private final Map<Parameter, ArgumentResolver> resolverCache = new ConcurrentHashMap<>();

public Object[] resolveArguments(...) {
// λ§€κ°œλ³€μˆ˜λ‹Ή ν•΄κ²°μž λ§€ν•‘ μΊμ‹œ
ArgumentResolver resolver = resolverCache.computeIfAbsent(p,
param -> delegates.stream().filter(ar -> ar.supports(param)).findFirst().orElse(null));
}

였λ₯˜ 처리 μ „λž΅β€‹

μ˜ˆμ™Έ 계측​

ν•΄κ²° μ‹€νŒ¨

  1. IllegalStateException: λ§€κ°œλ³€μˆ˜μ— λŒ€ν•œ ν•΄κ²°μžλ₯Ό 찾을 수 μ—†μŒ
  2. IllegalArgumentException: λ§€κ°œλ³€μˆ˜ 검증 μ‹€νŒ¨
  3. BadRequestException: ν΄λΌμ΄μ–ΈνŠΈ 데이터 ν˜•μ‹ 였λ₯˜

였λ₯˜ μ»¨ν…μŠ€νŠΈ 보쑴

throw new BadRequestException(
"Failed to parse request body as JSON or convert to '" +
parameter.getType().getName() + "'. Check JSON format and target type. Cause: " +
e.getMessage(), ResponseCode.BAD_REQUEST, e);

볡ꡬ μ „λž΅

  • λΉ λ₯Έ μ‹€νŒ¨: 첫 번째 였λ₯˜μ—μ„œ ν•΄κ²° 쀑단
  • 였λ₯˜ μ „νŒŒ: 원본 μ˜ˆμ™Έ 원인 보쑴
  • HTTP λ§€ν•‘: λ‚΄λΆ€ 였λ₯˜λ₯Ό μ μ ˆν•œ HTTP μƒνƒœ μ½”λ“œλ‘œ λ§€ν•‘

ν™•μž₯μ„± 뢄석​

μƒˆλ‘œμš΄ ν•΄κ²°μž 좔가​

κ΅¬ν˜„ μš”κ΅¬μ‚¬ν•­

  1. ArgumentResolver μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„
  2. μžλ™ 등둝을 μœ„ν•œ @Component μ–΄λ…Έν…Œμ΄μ…˜ μΆ”κ°€
  3. λͺ…ν™•ν•œ 지원 κΈ°μ€€ μ •μ˜
  4. μ μ ˆν•œ νƒ€μž… λ³€ν™˜ 처리

μ‚¬μš©μž μ •μ˜ ν•΄κ²°μž 예제

@Component
public class SessionArgumentResolver implements ArgumentResolver {
@Override
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(SessionAttribute.class);
}

@Override
public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
// μ‚¬μš©μž μ •μ˜ μ„Έμ…˜ 처리 둜직
SessionAttribute annotation = parameter.getAnnotation(SessionAttribute.class);
return sessionManager.getAttribute(annotation.value());
}
}

νƒ€μž… λ³€ν™˜κΈ° ν™•μž₯​

ν˜„μž¬ μ œν•œμ‚¬ν•­

  • κ³ μ •λœ 지원 νƒ€μž… μ§‘ν•©
  • μ‚¬μš©μž μ •μ˜ λ³€ν™˜κΈ° 등둝 μ—†μŒ
  • λ³΅μž‘ν•œ 객체 λ³€ν™˜ μ—†μŒ

잠재적 κ°œμ„ 

public interface TypeConverter {
boolean supports(Class<?> sourceType, Class<?> targetType);
Object convert(Object source, Class<?> targetType);
}

// λ³€ν™˜κΈ° λ ˆμ§€μŠ€νŠΈλ¦¬
private final List<TypeConverter> converters;

IoC μ»¨ν…Œμ΄λ„ˆμ™€μ˜ 톡합​

μžλ™ ν•΄κ²°μž λ°œκ²¬β€‹

μ»΄ν¬λ„ŒνŠΈ μŠ€μΊ”

@Component
public class PathVariableArgumentResolver implements ArgumentResolver

μ˜μ‘΄μ„± μ£Όμž…

public CompositeArgumentResolver(List<ArgumentResolver> delegates) {
this.delegates = delegates;
}

IoC 이점

  • μžλ™ ν•΄κ²°μž 등둝
  • @Orderλ₯Ό ν†΅ν•œ μˆœμ„œν™”λœ ν•΄κ²°μž μ£Όμž…
  • λͺ¨μ˜ ν•΄κ²°μžλ‘œ μ‰¬μš΄ ν…ŒμŠ€νŠΈ

ν•΄κ²°μž μˆœμ„œβ€‹

ν˜„μž¬ λ™μž‘

  • IoC μ»¨ν…Œμ΄λ„ˆ 빈 생성 μˆœμ„œλ‘œ κ²°μ •λ˜λŠ” μˆœμ„œ
  • λͺ…μ‹œμ  μš°μ„ μˆœμœ„ 처리 μ—†μŒ
  • @Order μ–΄λ…Έν…Œμ΄μ…˜ μ§€μ›μœΌλ‘œ 이읡을 얻을 수 있음

잠재적 κ°œμ„ 

@Component
@Order(100)
public class PathVariableArgumentResolver implements ArgumentResolver

λ³΄μ•ˆ 고렀사항​

μž…λ ₯ 검증​

ν˜„μž¬ μƒνƒœ

  • λ³€ν™˜μ„ ν†΅ν•œ κΈ°λ³Έ νƒ€μž… 검증
  • μž…λ ₯ μ •ν™” μ—†μŒ
  • μž…λ ₯ 데이터 크기 μ œν•œ μ—†μŒ

λ³΄μ•ˆ 격차

  1. JSON 폭탄 보호: JSON νŒŒμ‹± 깊이/크기 μ œν•œ μ—†μŒ
  2. 경둜 λ³€μˆ˜ 검증: 경둜 λ³€μˆ˜μ— λŒ€ν•œ μ •κ·œμ‹ 검증 μ—†μŒ
  3. 헀더 μΈμ μ…˜: 헀더 λ‚΄μš© 검증 μ—†μŒ

κ°œμ„  고렀사항

// 크기 μ œν•œ
public Object resolve(Parameter parameter, HttpRequest<?> request, ...) {
String rawBody = (String) request.getBody();

if (rawBody != null && rawBody.length() > MAX_BODY_SIZE) {
throw new PayloadTooLargeException();
}

// λ³΄μ•ˆ μ„€μ •μœΌλ‘œ ObjectMapper ꡬ성
objectMapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
}

νƒ€μž… μ•ˆμ „μ„± κ°•μ œβ€‹

κΈ°λ³Έ νƒ€μž… 보호

if (targetType.isPrimitive() && value == null) {
throw new IllegalArgumentException("Null value cannot be assigned to primitive type");
}

클래슀 λ‘œλ”© λ³΄μ•ˆ

  • λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•˜μ§€λ§Œ μž„μ˜ 클래슀 μΈμŠ€ν„΄μŠ€ν™”λŠ” ν—ˆμš©ν•˜μ§€ μ•ŠμŒ
  • μ•Œλ €μ§„ μ•ˆμ „ν•œ νƒ€μž…μœΌλ‘œ μ œν•œλœ νƒ€μž… λ³€ν™˜
  • λ³΄μ•ˆ μ„€μ •μœΌλ‘œ ꡬ성할 수 μžˆλŠ” Jackson 역직렬화 ꡬ성

Spring MVCμ™€μ˜ 비ꡐ​

μœ μ‚¬μ β€‹

  • μ–΄λ…Έν…Œμ΄μ…˜ 기반 λ§€κ°œλ³€μˆ˜ 바인딩
  • ν™•μž₯ κ°€λŠ₯ν•œ ν•΄κ²°μž 체인 μ•„ν‚€ν…μ²˜
  • νƒ€μž… λ³€ν™˜ μ‹œμŠ€ν…œ
  • 일반적인 HTTP λ§€κ°œλ³€μˆ˜ μœ ν˜• 지원

차이점​

λ‹¨μˆœν™”λœ νƒ€μž… μ‹œμŠ€ν…œ

  • Spring: κ΄‘λ²”μœ„ν•œ νƒ€μž… 지원을 κ°€μ§„ λ³΅μž‘ν•œ ConversionService
  • Sprout: κΈ°λ³Έ νƒ€μž…μ„ κ°€μ§„ κ°„λ‹¨ν•œ TypeConverter

ν•΄κ²°μž 발견

  • Spring: μˆœμ„œν™”λ₯Ό κ°€μ§„ λ³΅μž‘ν•œ HandlerMethodArgumentResolverComposite
  • Sprout: κ°„λ‹¨ν•œ λͺ©λ‘ 기반 반볡

였λ₯˜ 처리

  • Spring: μ •κ΅ν•œ MethodArgumentResolutionException 계측
  • Sprout: HTTP μƒνƒœ 맀핑을 κ°€μ§„ κΈ°λ³Έ μ˜ˆμ™Έ μœ ν˜•

μ„±λŠ₯

  • Spring: 캐싱과 미리 κ³„μ‚°λœ ν•΄κ²°μž λ§€ν•‘μœΌλ‘œ μ΅œμ ν™”
  • Sprout: ν•΄κ²°μž λͺ©λ‘μ„ ν†΅ν•œ μ„ ν˜• 검색 (μ΅œμ ν™” μ—¬μ§€κ°€ μžˆμŠ΅λ‹ˆλ‹€)

Sprout의 argument resolution은 HTTP μš”μ²­ 데이터 바인딩을 μ„±κ³΅μ μœΌλ‘œ μΆ”μƒν™”ν•˜λŠ” 잘 μ„€κ³„λ˜κ³  ν™•μž₯ κ°€λŠ₯ν•œ μ•„ν‚€ν…μ²˜λ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€. 이 μ‹œμŠ€ν…œμ€ ꡐ윑적 λͺ©μ μ— μ ν•©ν•œ λ‹¨μˆœν•¨μ„ μœ μ§€ν•˜λ©΄μ„œ κ²€μ¦λœ 섀계 νŒ¨ν„΄(볡합, μ „λž΅, μ±…μž„ 연쇄)을 ν™œμš©ν•©λ‹ˆλ‹€.

강점

  • λͺ…ν™•ν•œ 관심사 뢄리
  • ν™•μž₯ κ°€λŠ₯ν•œ ν•΄κ²°μž μ•„ν‚€ν…μ²˜
  • νƒ€μž… μ•ˆμ „ν•œ λ§€κ°œλ³€μˆ˜ 바인딩
  • HTTP μƒνƒœ 맀핑을 κ°€μ§„ 쒋은 였λ₯˜ 처리

κ°œμ„  μ˜μ—­

  • ν•΄κ²°μž 캐싱을 ν†΅ν•œ μ„±λŠ₯ μ΅œμ ν™”
  • ν–₯μƒλœ νƒ€μž… λ³€ν™˜ μ‹œμŠ€ν…œ
  • μž…λ ₯ 검증을 μœ„ν•œ λ³΄μ•ˆ κ°•ν™”
  • μ œλ„€λ¦­ νƒ€μž…κ³Ό λ³΅μž‘ν•œ 객체 지원

κ°œμ„  μ˜μ—­μ— λŒ€ν•œ 좔가적 이슈 λ°œν–‰κ³Ό κΈ°μ—¬λŠ” Sprout ν”„λ ˆμž„μ›Œν¬μ˜ 지속적인 λ°œμ „μ— κΈ°μ—¬ν•  κ²ƒμž…λ‹ˆλ‹€.