본문 바로가기
교재 실습/자바 웹 프로그래밍 Next Step

5.2.3 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거한다

by Jint 2025. 4. 2.

HttpRequest, HttpResponse 를 추가해 RequestHandler 의 복잡도를 많이 낮추었다. 하지만 아직까지 run() 메서드의 복잡도를 완전히 낮추지는 못했다. run() 메서드의 가장 큰 문제점은 기능이 추가될 때마다 새로운 else if 절이 추가되는 구조로 구현되어 있다는 것이다. 이는 객체지향 설계 원칙 중 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 OCP(개방폐쇄의 원칙, Open-Closed Principle) 원칙을 위반하고 있다. 새로운 기능이 추가되거나 수정사항이 발생하더라도 변화의 범위를 최소화하도록 설계를 개선해 본다.

앞의 요청과 응답 데이터를 분리하는 실습을 건너뛰고 지금 단계에서 실습을 진행하고 싶다면 https://github.com/slipp/web-application-server 저장소의 was-step2-request-response-refactoring 브랜치에서 시작할 수 있다.

run() 메서드의 복잡도가 높아 먼저 각 분기문 구현을 별도의 메서드로 분리하는 리팩토링(Extract Method 리팩토링)을 진행한다. 리팩토링을 진행한 결과는 다음과 같다.

 

- src/main/java/webserver/RequestHandler.java

package webserver;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Collection;
import java.util.Map;

import model.User;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import util.HttpRequestUtils;
import db.DataBase;

public class RequestHandler extends Thread {

    private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
    private Socket connection;

    public RequestHandler(Socket connectionSocket) {
        this.connection = connectionSocket;
    }

    public void run() {
        log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
            HttpRequest request = new HttpRequest(in);
            HttpResponse response = new HttpResponse(out);

            String path = getDefaultPath(request.getPath());
            if ("/user/create".equals(path)) {
                createUser(request, response);
            } else if ("/user/login".equals(path)) {
                login(request, response);
            } else if ("/user/list".equals(path)) {
                listUser(request, response);
            } else {
                response.forward(path);
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private void createUser(HttpRequest request, HttpResponse response) {
        User user = new User(request.getParameter("userId"), request.getParameter("password"), request.getParameter("name"), request.getParameter("email"));
        log.debug("user : {}", user);
        DataBase.addUser(user);
        response.sendRedirect("/index.html");
    }

    private void login(HttpRequest request, HttpResponse response) {
        User user = DataBase.findUserById(request.getParameter("userId"));
        if (user != null) {
            if (user.login(request.getParameter("password"))) {
                response.addHeader("Set-Cookie", "logined=true");
                response.sendRedirect("/index.html");
            } else {
                response.sendRedirect("/user/login_failed.html");
            }
        } else {
            response.sendRedirect("/user/login_failed.html");
        }
    }

    private void listUser(HttpRequest request, HttpResponse response) {
        if (!isLogin(request.getHeader("Cookie"))) {
            response.sendRedirect("/user/login.html");
            return;
        }

        Collection<User> users = DataBase.findAll();
        StringBuilder sb = new StringBuilder();
        sb.append("<table border='1'>");
        for (User user : users) {
            sb.append("<tr>");
            sb.append("<td>" + user.getUserId() + "</td>");
            sb.append("<td>" + user.getName() + "</td>");
            sb.append("<td>" + user.getEmail() + "</td>");
            sb.append("</tr>");
        }
        sb.append("</table>");
        response.forwardBody(sb.toString());
    }

    private boolean isLogin(String cookieValue) {
        Map<String, String> cookies = HttpRequestUtils.parseCookies(cookieValue);
        String value = cookies.get("logined");
        if (value == null) {
            return false;
        }
        return Boolean.parseBoolean(value);
    }

    private String getDefaultPath(String path) {
        if (path.equals("/")) {
            return "/index.html";
        }
        return path;
    }

}

 

위와 같이 리팩토링을 진행하니 각 메서드는 앞의 리팩토링 과정에서 추가한 HttpRequest, HttpResponse 만 인자로 받는 것을 확인할 수 있다. 이와 같이 메서드 원형이 같기 때문에 자바의 인터페이스(interface)로 추출하는 것이 가능하다. Controller 라는 이름의 인터페이스를 추가한다.

 

- src/main/java/controller/Controller.java

package controller;

import http.HttpRequest;
import http.HttpResponse;

public interface Controller {
    void service(HttpRequest request, HttpResponse response);
}

 

위와 같이 Controller 인터페이스를 추가한 후 앞의 분기문에서 분리했던 메서드(createUser, login, listUser)의 구현코드를 Controller 인터페이스에 대한 구현 클래스로 이동한다.

 

- src/main/java/controller/CreateUserController.java

package controller;

import http.HttpRequest;
import http.HttpResponse;
import model.User;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import db.DataBase;

public class CreateUserController implements Controller {

    private static final Logger log = LoggerFactory.getLogger(CreateUserController.class);

    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        User user = new User(request.getParameter("userId"), request.getParameter("password"), request.getParameter("name"), request.getParameter("email"));
        log.debug("user : {}", user);
        DataBase.addUser(user);
        response.sendRedirect("/index.html");
    }

}

 

- src/main/java/controller/LoginController.java

package controller;

import model.User;
import db.DataBase;
import http.HttpRequest;
import http.HttpResponse;

public class LoginController implements Controller {

    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        User user = DataBase.findUserById(request.getParameter("userId"));
        if (user != null) {
            if (user.login(request.getParameter("password"))) {
                response.addHeader("Set-Cookie", "logined=true");
                response.sendRedirect("/index.html");
            } else {
                response.sendRedirect("/user/login_failed.html");
            }
        } else {
            response.sendRedirect("/user/login_failed.html");
        }
    }

}

 

- src/main/java/controller/ListUserController.java

package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.util.Collection;
import java.util.Map;

import model.User;
import util.HttpRequestUtils;
import db.DataBase;

public class ListUserController implements Controller {

    @Override
    public void doGet(HttpRequest request, HttpResponse response) {
        if (!isLogin(request.getHeader("Cookie"))) {
            response.sendRedirect("/user/login.html");
            return;
        }

        Collection<User> users = DataBase.findAll();
        StringBuilder sb = new StringBuilder();
        sb.append("<table border='1'>");
        for (User user : users) {
            sb.append("<tr>");
            sb.append("<td>" + user.getUserId() + "</td>");
            sb.append("<td>" + user.getName() + "</td>");
            sb.append("<td>" + user.getEmail() + "</td>");
            sb.append("</tr>");
        }
        sb.append("</table>");
        response.forwardBody(sb.toString());
    }

    private boolean isLogin(String cookieValue) {
        Map<String, String> cookies = HttpRequestUtils.parseCookies(cookieValue);
        String value = cookies.get("logined");
        if (value == null) {
            return false;
        }
        return Boolean.parseBoolean(value);
    }

}

 

CreateUserController, LoginController, ListUserController 를 추가한다.

위와 같이 각 분기문에 해당하는 Controller 를 추가한 다음 각 요청 URL과 URL에 대응하는 Controller 를 연결하는 RequestMapping 이라는 새로운 클래스를 추가한다. RequestMapping 은 웹 애플리케이션에서 서비스하는 모든 URL과 Controller 를 관리하고 있으며, 요청 URL에 해당하는 Controller 를 반환하는 역할을 한다. 이 같은 역할을 담당하는 RequestMapping 구현 코드는 다음과 같다.

 

- src/main/java/webserver/RequestMapping.java

package webserver;

import java.util.HashMap;
import java.util.Map;

import controller.Controller;
import controller.CreateUserController;
import controller.ListUserController;
import controller.LoginController;

public class RequestMapping {

    private static Map<String, Controller> controllers = new HashMap<String, Controller>();

    static {
        controllers.put("/user/create", new CreateUserController());
        controllers.put("/user/login", new LoginController());
        controllers.put("/user/list", new ListUserController());
    }

    public static Controller getController(String requestUrl) {
        return controllers.get(requestUrl);
    }
    
}

 

지금까지 과정을 통해 요청 URL과 Controller 에 대한 연결 작업까지 모두 완료했다. 다음 단계는 RequestHandler 에서 요청 URL에 대한 Controller 를 찾은 후 모든 작업을 해당 Controller 가 처리하도록 위임할 수 있다.

 

- src/main/java/webserver/RequestHandler.java

package webserver;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import controller.Controller;

public class RequestHandler extends Thread {

    private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
    private Socket connection;

    public RequestHandler(Socket connectionSocket) {
        this.connection = connectionSocket;
    }

    public void run() {
        log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());

        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
            HttpRequest request = new HttpRequest(in);
            HttpResponse response = new HttpResponse(out);

            Controller controller = RequestMapping.getController(request.getPath());
            if (controller == null) {
                String path = getDefaultPath(request.getPath());
                response.forward(path);
            } else {
                controller.service(request, response);
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private String getDefaultPath(String path) {
        if (path.equals("/")) {
            return "/index.html";
        }
        return path;
    }
    
}

 

위 소스코드가 RequestHandler 의 run() 메서드 전체 코드이다. 앞으로 개인정보 수정, 로그아웃과 같은 새로운 기능이 추가된다면 어떻게 구현할 수 있을까? RequestHandler 의 run() 메서드는 더 이상 수정할 필요가 없다. 새로운 기능이 추가되면 Controller 인터페이스를 구현하는 새로운 클래스를 추가한 후 RequestMapping 의 Map 요청 URL과 Controller 클래스를 추가하는 것으로 모든 작업이 끝난다. 각 클래스 간에는 어떠한 영향도 미치지 않으면서 새로운 기능을 추가하는 것이 가능하다. 또한 변경 사항이 발생하면 다른 클래스에 영향을 미치지 않으면서 해당 Controller 클래스의 service() 메서드만 수정하면 된다.

지금 상태로도 충분히 깔끔하고 좋은 코드지만 여기서 한 발 더 나아가 각 HTTP 메서드(GET, POST)에 따라 다른 처리를 할 수 있도록 다음과 같은 추상 클래스를 추가할 수도 있다.

 

- src/main/java/controller/AbstractController.java

package controller;

import http.HttpMethod;
import http.HttpRequest;
import http.HttpResponse;

public abstract class AbstractController implements Controller {

    @Override
    public void service(HttpRequest request, HttpResponse response) {
        HttpMethod method = request.getMethod();
        if (method.isPost()) {
            doPost(request, response);
        } else {
            doGet(request, response);
        }
    }

    protected void doPost(HttpRequest request, HttpResponse response) {}

    protected void doGet(HttpRequest request, HttpResponse response) {}

}

 

위와 같이 AbstractController 를 추가한 후 각 Controller 는 Controller 인터페이스를 직접 구현하는 것이 아닌 AbstractController 를 상속해 각 HTTP 메서드에 맞는 메서드를 오버라이드 하도록 구현할 수 있다.

 

- src/main/java/controller/CreateUserController.java

package controller;

import http.HttpRequest;
import http.HttpResponse;
import model.User;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import db.DataBase;

public class CreateUserController extends AbstractController {

    private static final Logger log = LoggerFactory.getLogger(CreateUserController.class);

    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        User user = new User(request.getParameter("userId"), request.getParameter("password"), request.getParameter("name"), request.getParameter("email"));
        log.debug("user : {}", user);
        DataBase.addUser(user);
        response.sendRedirect("/index.html");
    }

}

 

- src/main/java/controller/LoginController.java

package controller;

import model.User;
import db.DataBase;
import http.HttpRequest;
import http.HttpResponse;

public class LoginController extends AbstractController {

    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        User user = DataBase.findUserById(request.getParameter("userId"));
        if (user != null) {
            if (user.login(request.getParameter("password"))) {
                response.addHeader("Set-Cookie", "logined=true");
                response.sendRedirect("/index.html");
            } else {
                response.sendRedirect("/user/login_failed.html");
            }
        } else {
            response.sendRedirect("/user/login_failed.html");
        }
    }

}

 

- src/main/java/controller/ListUserController.java

package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.util.Collection;
import java.util.Map;

import model.User;
import util.HttpRequestUtils;
import db.DataBase;

public class ListUserController extends AbstractController {
    
    @Override
    public void doGet(HttpRequest request, HttpResponse response) {
        if (!isLogin(request.getHeader("Cookie"))) {
            response.sendRedirect("/user/login.html");
            return;
        }

        Collection<User> users = DataBase.findAll();
        StringBuilder sb = new StringBuilder();
        sb.append("<table border='1'>");
        for (User user : users) {
            sb.append("<tr>");
            sb.append("<td>" + user.getUserId() + "</td>");
            sb.append("<td>" + user.getName() + "</td>");
            sb.append("<td>" + user.getEmail() + "</td>");
            sb.append("</tr>");
        }
        sb.append("</table>");
        response.forwardBody(sb.toString());
    }

    private boolean isLogin(String cookieValue) {
        Map<String, String> cookies = HttpRequestUtils.parseCookies(cookieValue);
        String value = cookies.get("logined");
        if (value == null) {
            return false;
        }
        return Boolean.parseBoolean(value);
    }

}

 

위와 같이 구현할 경우의 장점은 요청 URL이 같더라도 HTTP 메서드가 다른 경우 새로운 Controller 클래스를 추가하지 않고 Controller 하나로 GET(doGet 메서드), POST(doPost 메서드)를 모두 지원하는 것이 가능하다.

여러 리팩토링 단계를 거치니 깔끔하고 쓸만한 웹 서버 코드를 구현할 수 있게 되었다. 리팩토링과 객체지향 설계의 맛을 조금이나마 느낄 수 있었다면 이 장의 목적은 충분히 달성한 것이다.

지금까지 구현한 소스코드는 https://github.com/slipp/web-application-server 저장소의 was-step3-controller-refactoring 브랜치에서 참고할 수 있다.



참고도서 : https://roadbook.co.kr/169

 

[신간안내] 자바 웹 프로그래밍 Next Step

● 저자: 박재성 ● 페이지: 480 ● 판형: 사륙배변형(172*225) ● 도수: 1도 ● 정가: 30,000원 ● 발행일: 2016년 9월 19일 ● ISBN: 978-89-97924-24-0 93000 [강컴] [교보] [반디] [알라딘] [예스24] [인터파크] [샘

roadbook.co.kr

댓글