Java 웹 MVC 프레임워크: 요청 파라미터 바인딩 구현

이전 단계에서는 기본적인 웹 프레임워크의 토대를 마련했습니다. 이제 사용자 요청에 포함된 매개변수를 백엔드 컨트롤러 메서드로 어떻게 전달하고 매핑할지에 대해 살펴보겠습니다. 이는 웹 애플리케이션에서 사용자 입력을 처리하는 데 필수적인 기능입니다.

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;
    }
}

RequestDispatcherprocessRequest 메서드에서는 다음 단계를 수행합니다:

  1. 요청 URI에서 컨텍스트 경로를 제외한 실제 경로를 추출합니다.
  2. HandlerRegistry를 통해 해당 경로에 매핑된 컨트롤러와 메서드 정보를 찾습니다. (이 예시에서는 HandlerRegistryHandlerEntry가 이미 구현되어 있다고 가정합니다.)
  3. 리플렉션을 사용하여 컨트롤러 클래스와 메서드 객체를 얻습니다.
  4. 메서드의 매개변수 타입과 어노테이션을 검사합니다.
  5. @RequestParameter 어노테이션이 발견되면, 어노테이션에 지정된 이름으로 HTTP 요청 파라미터 값을 가져옵니다.
  6. 가져온 문자열 값을 메서드 매개변수의 실제 타입(String, Integer, Double 등)에 맞게 변환합니다.
  7. 컨트롤러 인스턴스를 생성하고, 준비된 매개변수 배열을 사용하여 메서드를 호출합니다.
  8. 메서드 실행 결과를 클라이언트에 응답합니다.

4. 테스트 결과

웹 서버(예: Tomcat)를 시작하고 브라우저에서 다음 URL을 입력합니다.

  • http://127.0.0.1:8080/MyMVC/message/custom?text=HelloCustomMessage
  • http://127.0.0.1:8080/MyMVC/product/order?item=Laptop&quantity=2&price=1200.50

서버 콘솔에는 다음과 같은 메시지가 출력될 것입니다:

수신된 메시지: HelloCustomMessage
Laptop 2개 주문, 개당 가격: 1200.5

태그: java MVC Servlet Annotation reflection

7월 14일 20:59에 게시됨