이전 단계에서는 기본적인 웹 프레임워크의 토대를 마련했습니다. 이제 사용자 요청에 포함된 매개변수를 백엔드 컨트롤러 메서드로 어떻게 전달하고 매핑할지에 대해 살펴보겠습니다. 이는 웹 애플리케이션에서 사용자 입력을 처리하는 데 필수적인 기능입니다.
1. 매개변수 매핑을 위한 사용자 정의 어노테이션 정의
메서드 매개변수에 프론트엔드 파라미터 이름을 지정하기 위한 새로운 어노테이션 RequestParameter를 생성합니다.
package dev.custom.framework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* HTTP 요청 파라미터를 메서드 인자로 매핑하기 위한 어노테이션
*/
@Target(ElementType.PARAMETER) // 메서드의 매개변수에 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
@Documented
public @interface RequestParameter {
/**
* 매핑할 요청 파라미터의 이름을 지정합니다.
* @return 요청 파라미터 이름
*/
String name() default "";
}
2. 컨트롤러 메서드에 파라미터 바인딩 적용
새로운 어노테이션을 사용하여 요청 매개변수를 받는 컨트롤러 메서드를 구현합니다. 다음은 MessageController의 예시입니다.
package dev.custom.framework.controller;
import dev.custom.framework.annotation.RequestParameter;
import dev.custom.framework.annotation.WebMapping;
@WebMapping(path="/message")
public class MessageController {
@WebMapping(path="/hello")
public String displayHello() {
System.out.println("Hello World!");
return "Hello";
}
@WebMapping(path="/custom")
public String showCustomMessage(@RequestParameter(name="text") String messageContent) {
System.out.println("수신된 메시지: " + messageContent);
return messageContent;
}
}
위 showCustomMessage 메서드의 String messageContent 매개변수에 @RequestParameter(name="text") 어노테이션을 붙여, 프론트엔드에서 'text'라는 이름으로 전달되는 파라미터가 해당 매개변수에 바인딩되도록 합니다.
또 다른 예시로, 여러 타입의 파라미터를 받는 ProductController를 살펴보겠습니다.
package dev.custom.framework.controller;
import dev.custom.framework.annotation.RequestParameter;
import dev.custom.framework.annotation.WebMapping;
@WebMapping(path="/product")
public class ProductController {
@WebMapping(path="/list")
public String listProducts() {
System.out.println("상품 목록을 표시합니다.");
return "Products List";
}
@WebMapping(path="/order")
public String placeOrder(
@RequestParameter(name="item") String itemName,
@RequestParameter(name="quantity") Integer amount,
@RequestParameter(name="price") Double unitPrice) {
System.out.println(itemName + " " + amount + "개 주문, 개당 가격: " + unitPrice);
return itemName + " ordered";
}
}
placeOrder 메서드에서는 itemName, amount, unitPrice 세 가지 매개변수를 @RequestParameter 어노테이션을 이용해 바인딩합니다.
3. 요청 디스패처 (Dispatcher) 로직 수정
이제 HTTP 요청을 처리하는 서블릿 로직을 수정하여, 요청 파라미터를 컨트롤러 메서드의 인자로 정확하게 전달하는 기능을 구현해야 합니다.
package dev.custom.framework.servlet;
import dev.custom.framework.annotation.RequestParameter;
import dev.custom.framework.core.HandlerEntry; // HandlerEntry는 URL, 컨트롤러 클래스명, 메서드명을 포함한다고 가정
import dev.custom.framework.core.HandlerRegistry; // HandlerRegistry는 매핑된 핸들러 정보를 제공한다고 가정
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RequestDispatcher extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* GET 및 POST 요청을 처리합니다.
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
processRequest(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
processRequest(req, resp);
}
private void processRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
String contextPath = req.getServletContext().getContextPath();
String pathWithoutContext = requestURI.substring(contextPath.length());
try {
// HandlerRegistry에서 매핑된 핸들러 엔트리를 찾습니다.
// HandlerRegistry.getHandlerEntry(pathWithoutContext)는 HandlerEntry 객체를 반환한다고 가정
for (HandlerEntry entry : HandlerRegistry.getAllHandlers()) {
if (entry.getPath().equals(pathWithoutContext)) {
Class<?> controllerClass = Class.forName(entry.getControllerClassName());
Method targetMethod = null;
for (Method method : controllerClass.getMethods()) {
if (method.getName().equals(entry.getMethodName())) {
targetMethod = method;
break;
}
}
if (targetMethod == null) {
throw new NoSuchMethodException("매핑된 메서드를 찾을 수 없습니다: " + entry.getMethodName());
}
// 메서드의 매개변수 타입 및 어노테이션 정보를 가져옵니다.
Class<?>[] paramTypes = targetMethod.getParameterTypes();
Annotation[][] paramAnnotations = targetMethod.getParameterAnnotations();
Object[] paramValues = new Object[paramTypes.length];
for (int i = 0; i < paramAnnotations.length; i++) {
for (Annotation anno : paramAnnotations[i]) {
if (anno instanceof RequestParameter) {
RequestParameter requestParam = (RequestParameter) anno;
String paramName = requestParam.name();
String paramValueStr = req.getParameter(paramName);
if (paramValueStr != null) {
// 매개변수 타입에 따라 값 변환
paramValues[i] = convertParameter(paramTypes[i], paramValueStr);
} else {
// 파라미터가 제공되지 않았을 경우, 기본값 또는 null 처리
paramValues[i] = null; // 필요시 타입에 맞는 기본값 설정
}
break; // 하나의 매개변수에는 하나의 RequestParameter 어노테이션만 있다고 가정
}
}
}
// 컨트롤러 인스턴스를 생성하고 메서드를 호출합니다.
Object controllerInstance = controllerClass.newInstance();
Object result = targetMethod.invoke(controllerInstance, paramValues);
// 여기서는 단순히 결과를 출력하지만, 실제로는 뷰 리졸버 등으로 연결됩니다.
if (result != null) {
resp.getWriter().write(result.toString());
}
return; // 요청 처리 완료
}
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "매핑된 URL을 찾을 수 없습니다: " + pathWithoutContext);
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
IllegalAccessException | InvocationTargetException e) {
System.err.println("요청 처리 중 오류 발생: " + e.getMessage());
e.printStackTrace();
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "내부 서버 오류");
} catch (NumberFormatException e) {
System.err.println("파라미터 타입 변환 오류: " + e.getMessage());
e.printStackTrace();
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 파라미터 형식");
}
}
/**
* 문자열 파라미터 값을 대상 타입으로 변환합니다.
*/
private Object convertParameter(Class<?> targetType, String value) {
if (targetType.equals(String.class)) {
return value;
} else if (targetType.equals(Integer.class) || targetType.equals(int.class)) {
return Integer.valueOf(value);
} else if (targetType.equals(Long.class) || targetType.equals(long.class)) {
return Long.valueOf(value);
} else if (targetType.equals(Double.class) || targetType.equals(double.class)) {
return Double.valueOf(value);
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
return Boolean.valueOf(value);
}
// 다른 타입에 대한 처리는 이곳에 추가
System.err.println("지원하지 않는 파라미터 타입: " + targetType.getName());
return null;
}
}
위 RequestDispatcher의 processRequest 메서드에서는 다음 단계를 수행합니다:
- 요청 URI에서 컨텍스트 경로를 제외한 실제 경로를 추출합니다.
HandlerRegistry를 통해 해당 경로에 매핑된 컨트롤러와 메서드 정보를 찾습니다. (이 예시에서는HandlerRegistry와HandlerEntry가 이미 구현되어 있다고 가정합니다.)- 리플렉션을 사용하여 컨트롤러 클래스와 메서드 객체를 얻습니다.
- 메서드의 매개변수 타입과 어노테이션을 검사합니다.
@RequestParameter어노테이션이 발견되면, 어노테이션에 지정된 이름으로 HTTP 요청 파라미터 값을 가져옵니다.- 가져온 문자열 값을 메서드 매개변수의 실제 타입(
String,Integer,Double등)에 맞게 변환합니다. - 컨트롤러 인스턴스를 생성하고, 준비된 매개변수 배열을 사용하여 메서드를 호출합니다.
- 메서드 실행 결과를 클라이언트에 응답합니다.
4. 테스트 결과
웹 서버(예: Tomcat)를 시작하고 브라우저에서 다음 URL을 입력합니다.
http://127.0.0.1:8080/MyMVC/message/custom?text=HelloCustomMessagehttp://127.0.0.1:8080/MyMVC/product/order?item=Laptop&quantity=2&price=1200.50
서버 콘솔에는 다음과 같은 메시지가 출력될 것입니다:
수신된 메시지: HelloCustomMessage
Laptop 2개 주문, 개당 가격: 1200.5