π 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;
}
μ€κ³ ν¨ν΄ λΆμ
- μ± μ μ°μ ν¨ν΄: κ° ν΄κ²°μκ° λ§€κ°λ³μλ₯Ό μ²λ¦¬ν μ μλμ§ νμΈ
- μ λ΅ ν¨ν΄: λ€μν λ§€κ°λ³μ μ νμ λν μλ‘ λ€λ₯Έ ν΄κ²° μ λ΅
- ν νλ¦Ώ λ©μλ: νΉνλ ꡬνκ³Ό ν¨κ»νλ κ³΅ν΅ ν΄κ²° νλ μμν¬
μ±λ₯ νΉμ±
- μκ° λ³΅μ‘λ: 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λ¨κ³ ν΄κ²° νλ‘ν μ½
- μ§μ νμΈ: ν΄κ²°μκ° λ§€κ°λ³μλ₯Ό μ²λ¦¬ν μ μλμ§ κ²°μ
- ν΄κ²°: μ€μ κ° μΆμΆ λ° λ³ν μν
μΈν°νμ΄μ€ μ€κ³ μ΄μ
- νμ₯μ±: μλ‘μ΄ ν΄κ²°μ μ ν μΆκ° μ©μ΄
- ν μ€νΈ κ°λ₯μ±: κ° ν΄κ²°μλ₯Ό λ 립μ μΌλ‘ λ¨μ ν μ€νΈ κ°λ₯
- κ΄μ¬μ¬ λΆλ¦¬: λͺ νν μ± μ κ²½κ³
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() + "]");
}
νμ μμ μ± κΈ°λ₯
- κΈ°λ³Έ νμ λ κ²μ¬: κΈ°λ³Έ νμ μ λ ν λΉ λ°©μ§
- λνΌ νμ μ²λ¦¬: κΈ°λ³Έ νμ κ³Ό λνΌ νμ λͺ¨λ μ§μ
- λͺ μμ νμ λ§€ν: μ§μλλ νμ μ λν λͺ νν λ³ν κ·μΉ
- μμ μ₯μΉ: μ§μλμ§ μλ λ³νμ λν μμΈ λ°μ
μ§μλλ λ³ν
μμ€ | λμ νμ | λ³ν λ©μλ |
---|---|---|
String | String | νλ± |
String | Long/long | Long.parseLong() |
String | Integer/int | Integer.parseInt() |
String | Boolean/boolean | Boolean.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;
- λͺ¨λ μμ²μμ 곡μ λλ μ μ ν΄κ²°μ λͺ©λ‘