본문 바로가기
개발/JAVA

[JAVA] Spring Controller를 직접 만들어보자 (5) - 리팩토링

by 상용최 2021. 1. 30.
반응형

소스주소 : github.com/sangyongchoi/spring-controller-copy

 

현재 우리는 아래와 같은 기능을 만들었다.

  • Handler Mapping
  • Parameter Binding
  • Method invoke 

하지만 누구나 다 느낄 수 있듯 현재 객체들의 책임분배가 올바르지 않다.

어떤 객체는 2~3개의 역할을 하고있다.

책임을 적절하게 분배하는 작업을 하도록 하겠다.

 

1. HandlerMapping 리팩토링

HanddlerMapping은 이름에 알맞게 Handler에 대한 정보만 가지고 매핑작업만 이루어져야 한다.

현재는 매핑작업 뿐만 아니라 파라미터 파싱, 메소드실행 까지한다.

 

현재 동작 방식 

1. 요청에 알맞는 Method 정보가 있는지 검사

2. 파라미터 파싱

3. 메소드 실행

 

변경할 방식

1. 요청에 알맞는 Method 정보가 있는지 검사

2. Return

 

HandlerMapping 클래스를 아래와 같이 변경해줍니다.

파라미터 파싱과 메소드실행작업이 사라지고 getHandler메소드가 생성됩니다.

public class HandlerMapping {

    Map<String, MethodInvoker> controller = new HashMap<>();
    Map<String, MethodInvoker> restController = new HashMap<>();

    public void init(){
        subDirList(MainApplication.class.getResource(".").getPath());
    }

    private void subDirList(String source) {
        File dir = new File(source);
        File[] fileList = dir.listFiles();

        if (fileList != null) {
            try {
                for (File file : fileList) {
                    if (file.isDirectory()) {
                        subDirList(file.getCanonicalPath());
                    } else if (file.isFile()) {
                        String path = file.getPath();

                        if (path.endsWith(".class")) {
                            int classes = path.lastIndexOf("classes");
                            String substring = path.substring(classes + 8);
                            String className = substring.split(".class")[0].replace("\\", ".");
                            Class<?> aClass = Class.forName(className);
                            if (aClass.isAnnotationPresent(Controller.class)) {
                                Method[] methods = aClass.getMethods();
                                Arrays.stream(methods)
                                        .forEach(m -> addPageHandler(aClass, m));
                            } else if (aClass.isAnnotationPresent(RestController.class)) {
                                Method[] methods = aClass.getMethods();
                                Arrays.stream(methods)
                                        .forEach(m -> addRestHandler(aClass, m));
                            }
                        }
                    }
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    private void addPageHandler(Class<?> aClass, Method m) {
        if (m.isAnnotationPresent(PostMapping.class)) {
            addPostHandler(aClass, m, RequestType.PAGE);
        } else if (m.isAnnotationPresent(GetMapping.class)) {
            addGetHandler(aClass, m, RequestType.PAGE);
        }
    }

    private void addRestHandler(Class<?> aClass, Method m) {
        if (m.isAnnotationPresent(PostMapping.class)) {
            addPostHandler(aClass, m, RequestType.POST);
        } else if (m.isAnnotationPresent(GetMapping.class)) {
            addGetHandler(aClass, m, RequestType.GET);
        }
    }

    private void addPostHandler(Class<?> aClass, Method m, RequestType requestType){
        PostMapping declaredAnnotation = m.getDeclaredAnnotation(PostMapping.class);
        String value = declaredAnnotation.value();
        addHandler(aClass, m, requestType, value);
    }

    private void addGetHandler(Class<?> aClass, Method m, RequestType requestType){
        GetMapping declaredAnnotation = m.getDeclaredAnnotation(GetMapping.class);
        String value = declaredAnnotation.value();
        addHandler(aClass, m, requestType, value);
    }

    private void addHandler(Class<?> aClass, Method m, RequestType requestType, String value){
        MethodInvoker methodInvoker = new MethodInvoker(aClass, m, requestType);

        if(RequestType.PAGE.equals(requestType)){
            controller.put(value, methodInvoker);
        }else{
            restController.put(value, methodInvoker);
        }
    }

    public MethodInvoker getHandler(HttpServletRequest request) throws ServiceNotFoundException {
        String requestURI = request.getRequestURI();
        if (isApiRequest(requestURI)) {
            return restController.get(requestURI);
        } else if (isPageRequest(requestURI)) {
            return controller.get(requestURI);
        } else {
            throw new ServiceNotFoundException("");
        }
    }

    public boolean isPageRequest(String requestURI) {
        return controller.containsKey(requestURI);
    }

    public boolean isApiRequest(String requestURI) {
        return restController.containsKey(requestURI);
    }
}

 

그렇다면 파라미터 파싱을 위한 객체를 만들어야 합니다.

Request는 여러 방식으로 들어오기 때문에 추상클래스 혹은 인터페이스를 이용하여 만드는것이 합리적입니다.

파라미터 파싱작업은 공통작업을 가지고 있기때문에 추상클래스가 알맞다고 생각하여서 추상클래스로 생성 하였습니다.

 

현재는 파라미터를 1개만 받을 수 있도록 만들었습니다. 

getParamter는 공통작업을 하도록 하고

getInputData 메소드를 통하여 요청방식에따라 데이터를 가져왔습니다 (=parse)

public abstract class RequestResolver {
    public Object[] getParameter(HttpServletRequest request, Method method) throws IOException{
        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return new Object[0];
        }

        String inputData = getInputData(request);
        Object[] result = new Object[1];
        Parameter parameter = parameters[0];
        result[0] = CommonUtil.objectMapper.readValue(inputData, parameter.getType());

        return result;
    }

    protected abstract String getInputData(HttpServletRequest request) throws IOException;
}

 

application/json 방식으로 들어온다면 아래와 같이 JsonResolver를 이용하여 데이터를 파싱합니다.

public class JsonRequestResolver extends RequestResolver{

    @Override
    protected String getInputData(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();

        try(InputStream inputStream = request.getInputStream()) {
            if (inputStream != null) {
                try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
                    char[] charBuffer = new char[128];
                    int bytesRead = -1;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                        stringBuilder.append(charBuffer, 0, bytesRead);
                    }
                }
            }
        }

        return stringBuilder.toString();
    }
}

Form 방식으로 들어온다면 아래와 같이 FormResolver를 이용하여 데이터를 파싱합니다.

public class FormRequestResolver extends RequestResolver{
    @Override
    protected String getInputData(HttpServletRequest request) throws IOException {
        final Enumeration<String> parameterNames = request.getParameterNames();
        StringBuilder sb = new StringBuilder();

        sb.append("{");
        while (parameterNames.hasMoreElements()) {
            final String s = parameterNames.nextElement();
            sb.append("\"").append(s).append("\" : \"").append(request.getParameter(s)).append("\",");
        }
        sb.replace(sb.length() - 1, sb.length(), "");
        sb.append("}");

        return sb.toString();
    }
}

 

여러 Resolver를 만들었으니 요청에 알맞는 Resolver를 가져오기 위해 ResolverFactory를 생성합니다.

Resolver들은 Thread-safe 하기때문에 static 변수로 생성하여 메모리를 절약합니다.

아래에서 DefaultRequestResolver를 만드시는것은 자유입니다.

저는 "{}" 빈값으로 return하는 메소드를 만들었습니다.

빈값으로 파라미터 바인딩을 시킬지 들어오지 않는다면 Exception을 발생시킬 것인지에 따라 만드시면 될 것 같습니다.

 

 

 

 

public class RequestResolverFactory {

    private static final DefaultRequestResolver defaultRequestResolver = new DefaultRequestResolver();
    private static final JsonRequestResolver jsonRequestResolver = new JsonRequestResolver();
    private static final FormRequestResolver formRequestResolver = new FormRequestResolver();

    public static RequestResolver getResolver(HttpServletRequest request) {
        String contentType = request.getContentType();

        if (contentType == null) {
            return defaultRequestResolver;
        } else if (contentType.contains("application/json")) {
            return jsonRequestResolver;
        } else if(contentType.contains("application/x-www-form-urlencoded")){
            return formRequestResolver;
        }

        return defaultRequestResolver;
    }
}

 

이제 파라미터 파싱(바인딩)부분과 메소드 실행부분을 HandlerMapping 객체에서 분리했습니다.

하지만 아직 ControllerFilter의 책임은 분리하지 못했습니다.

ControllerFilter의 책임을 분리하도록 하겠습니다.

현재 ControllerFilter는 Response를 직접 세팅하고 있습니다.

이는 올바르지 않은 책임을 가지고 있는 것입니다.

 

Response를 세팅하는 부분을 따로 분리하도록 하겠습니다.

Response를 세팅하는 부분은 따로 공통작업이 (아직은)없기에 Interface로 생성 하였습니다.

public interface ResponseResolver {
    void resolve(HttpServletRequest request, ServletResponse response, Object viewName) throws ServletException, IOException;
}

Rest 형식일 때는 json 타입으로 돌려줘야 하기에 JsonResponseResoler를 생성 하였습니다.

public class JsonResponseResolver implements ResponseResolver{

    private static final String CONTENT_TYPE = "application/json";

    @Override
    public void resolve(HttpServletRequest request, ServletResponse response, Object result) throws ServletException, IOException {
        response.setContentType(JsonResponseResolver.CONTENT_TYPE);

        try (PrintWriter writer = response.getWriter()) {
            writer.write("{ \"result\": \"" + result.toString() + "\"}");
            writer.flush();
        }
    }
}

리턴해줘야 하는 값이 페이지라면 text/html 형식으로 돌려줘야하기에 ViewResponseResolver를 생성 하였습니다.

public class ViewResponseResolver implements ResponseResolver{

    private static final String CONTENT_TYPE = "text/html";

    @Override
    public void resolve(HttpServletRequest request, ServletResponse response, Object viewName) throws ServletException, IOException {
        response.setContentType(ViewResponseResolver.CONTENT_TYPE);
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName.toString());
        requestDispatcher.forward(request, response);
    }
}

Request와 마찬가지로 어떤 ResonseResolver를 사용할지 결정해야 하기에 Factory 객체를 하나 만들도록 하겠습니다.

public class ResponseResolverFactory {

    private static final ViewResponseResolver viewResponseResolver = new ViewResponseResolver();
    private static final JsonResponseResolver jsonResponseResolver = new JsonResponseResolver();

    public static ResponseResolver getResponseResolver(MethodInvoker methodInvoker){
        if (RequestType.PAGE.equals(methodInvoker.getRequestType())) {
            return viewResponseResolver;
        } else {
            return jsonResponseResolver;
        }
    }
}

 

 

 

 

메소드별로 ResponseResolver를 결정하기 위해 MethodInvoker 필드를 아래와 같이 변경합니다.

MethodType은 이름과 용도가 알맞지 않습니다. 

MethodType => RequestType으로 객체이름을 변경합니다.

RequestType은 어떤걸 요청하는지 판단하는 타입입니다.

Page라면 View를 요청하고 POST나 GET이면 JSON타입의 데이터를 요청한다고 정했습니다.

public class MethodInvoker {

    private Class<?> aClass;
    private Method method;
    private RequestType requestType;

    public MethodInvoker(Class<?> aClass, Method method, RequestType requestType) {
        this.aClass = aClass;
        this.method = method;
        this.requestType = requestType;
    }

    public Object invoke(Object[] parameter) throws InvocationTargetException, IllegalAccessException, InstantiationException {
        try{
            if(parameter.length == 0) {
                return method.invoke(aClass.newInstance());
            }else{
                return method.invoke(aClass.newInstance(), parameter);
            }
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
            e.printStackTrace();
            throw e;
        }
    }

    public Method getMethod() {
        return method;
    }

    public RequestType getRequestType() {
        return requestType;
    }
}

 

위와 같이 변경을 하게되면 ControllerFilter는 아래와 같이 변경되게 됩니다.

 

1. 지원하는 메소드인지 검사

2. 요청에 알맞는 Handler를 가져옴

3. 요청에 알맞는 RequestResolver를 가져옴

4. 핸들러를 사용하여 메소드 실행

5. ResponseResolver를 가져옴

6. ResponseResolver를 이용하여 Response값 세팅

public class ControllerFilter implements Filter {

    HandlerMapping handlerMapping = new HandlerMapping();

    @Override
    public void init(FilterConfig filterConfig) {
        handlerMapping.init();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String method = request.getMethod();

        if(isAccessible(method)) {
            try {
                final MethodInvoker handler = handlerMapping.getHandler(request);
                RequestResolver resolver = RequestResolverFactory.getResolver(request);
                final Object invoke = handler.invoke(resolver.getParameter(request, handler.getMethod()));
                final ResponseResolver responseResolver = ResponseResolverFactory.getResponseResolver(handler);
                responseResolver.resolve(request, servletResponse, invoke);
            } catch (ServiceNotFoundException e) {
                throw new ServletException("존재하지 않는 URI입니다.");
            } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
                throw new IOException("처리하던 중 오류가 발생했습니다." + e.getMessage());
            }
        }else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }
    
    private boolean isAccessible(String method){
        return "GET".equals(method) || "POST".equals(method);
    }

}

 

 

 

이로써 Spring Controller와 비슷한 역할을 하는 Filter를 만들게 되었습니다.

  • 매핑
  • 파라미터 바인딩
  • Return

파라미터 바인딩은 추가로 고도화를 하거나 입맛에 맞게 RequestResolver 부분을 변경하면 됩니다.

반응형

댓글