독서/📚

[Next Step] 5장 웹 서버 리팩토링, 서블릿 컨테이너와 서블릿의 관계

leejinwoo1126 2023. 11. 16. 11:07
반응형

 

 


    실습 프로젝트 저장소

    실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함

     

    web-application-server (3 ~ 6장)

    https://github.com/slipp/web-application-server

     

    GitHub - slipp/web-application-server: 웹 애플리케이션 서버 실습을 위한 뼈대

    웹 애플리케이션 서버 실습을 위한 뼈대. Contribute to slipp/web-application-server development by creating an account on GitHub.

    github.com

     


     

    4장에서 구현한 HTTP 웹 서버를 리팩토링하면서 설계를 개선하는 경험을 해보자 

     

    https://dev-ljw1126.tistory.com/401

     

    [Next Step] 3~4장 HTTP 웹서버 구현을 통해 HTTP 이해하기(No Framework)

    로컬 개발 환경 구축 https://dev-ljw1126.tistory.com/294 [Next Step] 3.3 원격 서버에 배포 (p84) 정리 목차 요구사항 로컬 개발 환경에 설치한 HTTP 웹 서버를 물리적으로 떨어져 있는 원격 서버에 배포해 정상

    dev-ljw1126.tistory.com

     

    절차*

    ① 요청 데이터를 처리하는 로직을 별도의 클래스로 분리 : HttpRequest

    ② 응답 데이터를 처리하는 로직을 별도의 클래스로 분리 : HttpResponse

    ③ 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거 : Controller 인터페이스

     

     

    p166.

     객체 지향 설계에서 중요한 연습은 요구사항을 분석해 객체로 추상화 하는 부분이다. 눈으로 보이지 않는 비즈니스 로직의 요구사항을 추상화하는 작업은 생각보다 쉽지 않다. 이 장에서 다루고 있는 HTTP에 대한 추상화는 이미 표준화가 되어 있으며, 데이터를 눈으로 직접 확인할 수 있기 때문에 그나마 쉬울 수 있다. 따라서 객체지향 설계를 처음 연습할 때 요구사항이 명확하지 않은 애플리케이션을 개발하기보다 체스 게임, 지뢰 찾기 게임 등과 같이 이미 요구사항이 명확한 애플리케이션으로 연습할 것을 추천한다.

    5-1. 요청 처리 담당하는 HttpRequest 클래스

    Hint
    ① 클라언트 요청 데이터를 담고 있는 InputStream을 생성자로 받아 HTTP 메소드, URL, 헤더, 본문을 분리하는 작업함
    ② 헤더는 Map<String, String>에 저장해 관리하고 getHeader(”필드명”) 메서도를 통해 접근 가능하도록 구현함
    ③ GET과 POST 메소드에 따라 전달되는 인자를 MAP<String, String>에 저장해 관리하고 getParameter(”인자 명칭”) 메소드를 통해 접근 가능하도록 구현함

     

     

     

    테스트 코드를 기반으로 개발할 경우

    ① 클래스에 버그가 있는지를 빨리 찾아 구현할 수 있다

    ② 디버깅하기 쉽다 → 개발 생산성 향상

    ③ 리팩토링을 할 수 있다 → 테스트 코드가 이미 존재한다면 리팩토링 후 테스트 실행하기만 하면 쉽게 확인 가능

     

    HttpRequest 테스트 작성(p148)

    /src/test/resources 디렉토리에 HTTP_GET.txt, HTTP_POST.txt 생성

     

    ①. HTTP_GET.txt

    GET /user/create?userId=javajigi&password=password&name=JaeSung HTTP/1.1
    Host: localhost:8080
    Connection: keep-alive
    Accept: */*

     

    ②. HTTP_POST.txt

    POST /user/create HTTP/1.1
    Host: localhost:8080
    Connection: keep-alive
    Content-Length: 46
    Content-Type: application/x-www-form-urlencoded
    Accept: */*
    
    userId=javajigi&password=password&name=JaeSung

     

     

    HttpRequestTest 클래스

    package webserver;
    
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    class HttpRequestTest {
        private String testDirectory = "./src/test/resources/";
    
        @DisplayName("")
        @Test
        void fileResource() {
            ClassLoader classLoader = getClass().getClassLoader();
            File file = new File(classLoader.getResource("HTTP_GET.txt").getFile());
            assertThat(file).isNotNull();
    
            file = new File(testDirectory + "HTTP_GET.txt");
            assertThat(file).isNotNull();
        }
    
        @DisplayName("")
        @Test
        void requestGET() throws IOException {
            InputStream in = new FileInputStream(new File(testDirectory + "HTTP_GET.txt"));
            HttpRequest httpRequest = new HttpRequest(in);
    
            assertThat(httpRequest.getMethod()).isEqualTo("GET");
            assertThat(httpRequest.getPath()).isEqualTo("/user/create");
            assertThat(httpRequest.getHeader("Connection")).isEqualTo("keep-alive");
            assertThat(httpRequest.getParameter("userId")).isEqualTo("javajigi");
        }
    
        @DisplayName("")
        @Test
        void requestPOST() throws IOException {
            InputStream in = new FileInputStream(new File(testDirectory + "HTTP_POST.txt"));
            HttpRequest httpRequest = new HttpRequest(in);
    
            assertThat(httpRequest.getMethod()).isEqualTo("POST");
            assertThat(httpRequest.getPath()).isEqualTo("/user/create");
            assertThat(httpRequest.getHeader("Connection")).isEqualTo("keep-alive");
            assertThat(httpRequest.getParameter("userId")).isEqualTo("javajigi");
        }
    }

     

     

    HttpReqeust 클래스

    public class HttpRequest {
        private static final Logger log = ...
        
        private String method;
        private String path;
        private Map<String, String> headers = new HashMap<>();
        private Map<String, String> params = new HashMap<>();
        
        public HttpRequest(InputStream in) {
        	BufferReader br = new BufferReader(new IntputStreamReader(in, "UTF-8"));
            String line = br.readLine();
            
            if(line == null) return;
            
            processRequestLine(line);
            
            while(!"".equals(line = br.readLine())) {
                log.debug("{}", line);
                String[] tokens = line.split(":");
                headers.put(tokens[0].trim(), tokens[1].trim());
            }
        	
            if("POST".equals(method) {
               String body = IOUtils.readData(br, headers.get("Content-Length"));
               params = HttpRequestUtils.parseQueryString(body);
            }
        }
        	 
        private vodi processRequestLine(String requestLine) {
        	log.debug("{}", requestLine);
        	String[] tokens = requestLine.split(" ");
            method = tokens[0];
            
            if("POST".equals(method)) {
                path = tokens[1];
                return;
            }
            
            int idx = tokens[1].indexOf("?");
            if(idx == -1) { // POST 요청
                path = tokens[1];
                return;
            } else {
            	path = tokens[1].subString(0, idx);
                params = HttpRequestUtils.parseQueryString(tokens[1].subString(idx + 1));
            }
        }    
        
        public String getMethod() { return method; }
        public String getPath() { return path; }
        public String getHeader(String name) { return headers.get(name); }
        public String getParameter(String name) { return params.get(name); }
    }

     

     

    리팩토링① 

    HttpRequest 로직에서 요청라인(request line)을 처리하는 processRequestLine() 메소드의 복잡도가 높아 보인다. 신규 RequestLine 클래스를 추가하는 방식으로 리팩토링을 해보자.

     

     

    RequestLineTest 클래스

    import static org.assertj.core.api.Assertions.assertThat;
    
    class RequestLineTest {
        @DisplayName("")
        @Test
        void createMethod() {
            RequestLine requestLine = new RequestLine("GET /index.html HTTP/1.1");
            assertThat(requestLine.getMethod()).isEqualTo(HttpMethod.GET);
            assertThat(requestLine.getPath()).isEqualTo("/index.html");
    
            requestLine = new RequestLine("POST /index.html HTTP/1.1");
            assertThat(requestLine.getMethod()).isEqualTo(HttpMethod.POST);
            assertThat(requestLine.getPath()).isEqualTo("/index.html");
        }
    
        @DisplayName("")
        @Test
        void createPathAndParams() {
            RequestLine requestLine = new RequestLine("GET /user/create?userId=javajigi&password=password&name=JaeSung HTTP/1.1");
            assertThat(requestLine.getMethod()).isEqualTo(HttpMethod.GET);
            assertThat(requestLine.getPath()).isEqualTo("/user/create");
    
            Map<String, String> params = requestLine.getParams();
            assertThat(params.size()).isEqualTo(3);
        }
    }

     

     

    RequestLine 클래스

    public class RequestLine {
        private static final Logger log = ...
    
        private String method;
        private String path;
        private Map<String, String> params = new HashMap<>();
    
        public RequestLine(String requestLine) {
            log.debug("request line : {}", requestLine);
            String[] tokens = requestLine.split(" ");
            if(tokens.length != 3) {
                throw new IllegalArgumentException(requestLine + "이 형식에 맞지 않습니다.");
            }
    
            method = tokens[0];
    
            int idx= tokens[1].indexOf("?");
            if(idx == -1) {
                path = tokens[1];
            } else {
                path = tokens[1].substring(0, idx);
    
                String queryString = tokens[1].substring(idx + 1);
                params = HttpRequestUtils.parseQueryString(queryString);
            }
        }
    
        public String getMethod() {
            return method;
        }
    
        public String getPath() {
            return path;
        }
    
        public Map<String, String> getParams() {
            return params;
        }
    }

     

     

    HttpReqeust 클래스 변경

    HttpRequest에서 요청 라인(request line)을 처리하는 책임을 분리했지만 HttpRequest의 메소드 원형은 바뀌지 않았다

    public class HttpRequest {
        private static final Logger log = ...
        
        private Map<String, String> headers = new HashMap<>();
        private Map<String, String> params = new HashMap<>();
        private RequestLine requestLine;
        
        public HttpRequest(InputStream in) {
        	BufferReader br = new BufferReader(new IntputStreamReader(in, "UTF-8"));
            String line = br.readLine();
            
            if(line == null) return;
            
            requestLine = new RequestLine(line); // *
            
            while(!"".equals(line = br.readLine())) {
                log.debug("{}", line);
                String[] tokens = line.split(":");
                headers.put(tokens[0].trim(), tokens[1].trim());
            }
        	
            if("POST".equals(method)) {
               String body = IOUtils.readData(br, headers.get("Content-Length"));
               params = HttpRequestUtils.parseQueryString(body);
            } else {
               params = requestLine.getParams(); // *	
            }
        }
    
        public String getMethod() { return requestLine.getMethod(); } // *
        public String getPath() { return requestLine.getPath(); } // *
        public String getHeader(String name) { return headers.get(name); }
        public String getParameter(String name) { return params.get(name); }
    }

     

     

     

    리팩토링

    구현부에서 Http Method가 문자열로 하드코딩 되어 있다. enum으로 변경해보자

     

    enum HttpMethod

    public enum HttpMethod {
        GET, POST;
        
        public boolean isPost() {
        	return this == POST;
        }
    }

     

    RequestLine 클래스

    public class RequestLine {
        private static final Logger log = ...
    
        private HttpMethod method; // *
        private String path;
        private Map<String, String> params = new HashMap<>();
    
        public RequestLine(String requestLine) {
        	// ..
            
            method = HttpMethod.valueOf(tokens[0]);  // *
    
            int idx= tokens[1].indexOf("?");
            if(idx == -1) {
                path = tokens[1];
            } else {
                path = tokens[1].substring(0, idx);
    
                String queryString = tokens[1].substring(idx + 1);
                params = HttpRequestUtils.parseQueryString(queryString);
            }
        }
    
        public HttpMethod getMethod() { return method; }  // *
    
        // ..
    }

     

     

    HttpRequest 클래스 

    public class HttpRequest {
        private static final Logger log = ...
        
        private Map<String, String> headers = new HashMap<>();
        private Map<String, String> params = new HashMap<>();
        private RequestLine requestLine;
        
        public HttpRequest(InputStream in) {
        	// ..
        	
            if(getMethod().isPost()) { // *
               String body = IOUtils.readData(br, headers.get("Content-Length"));
               params = HttpRequestUtils.parseQueryString(body);
            } else {
               params = requestLine.getParams(); 	
            }
        }
    
        public HttpRequest getMethod() { return requestLine.getMethod(); } // *
        // ..
    }

     

     

    리팩토링 ③

    RequestHandler 클래스

    public class RequestHandler extends Thread {
         // 중략..
    		
         public void run() {
            try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
                HttpRequest request = new HttpRequest(in); //*
                String path = getDefaultPath(request.getPath());
    						
                DataOutputStream dos = new DataOutputStream(out);
                if(path.startsWith("/user/create")) {
                    User user = new User(request.getParameter("userId"),
                            request.getParameter("password"),
                            request.getParameter("name"),
                            request.getParameter("email"));
                    DataBase.addUser(user);
    
                    log.debug("User : {}", user);
                    response302Header(dos);
                }
                // 중략..
            } catch (IOException e) {
               log.error(e.getMessage());
            }
         }
         
         private boolean isLogin() {
            Map<String, String> cookies = HttpRequestUtils.parseCookies(cookieValue);
            String value = cookies.get("logined");
            if(value == null) {
               return false;
            }
            
            return Boolean.parseBoolean(value);
         }
         
         private StringgetDefaultPath(String path) {
            if("/".equals(path)) {
               return "/index.html";
            }
            
            return path;
         }
    }

     

     

    p163.

    프로그래밍 경험이 많지 않은데 객체의 책임을 분리하고 좋은 설계를 하기는 쉽지 않다. 객체 지향 설계를 잘하려면 많은 연습, 경험, 고민이 필요하다. 경험이 많지 않은 상태에서는 일단 새로운 객체를 추가했으면 객체를 최대한 활용하기 위해 노력해본다. *객체를 최대한 활용하는 연습을 하는 첫 번째는 객체에서 값을 꺼낸 후 로직을 구현하려고 하지 말고 값을 가지고 잇는 객체에 메시지를 보내 일을 시키도록 연습 해보자.

     

    p165.

    클라이언트 요청 데이터에 대한 처리(책임)를 모두 HttpRequest로 위임했기 때문에 RequestHandler는 요청 데이터를 처리하는 모든 로직을 제거할 수 있었다. RequestHandler는 HttpRequest가 제공하는 메소드를 이용해 필요한 데이터를 사용하기만 하면 된다. 이와 같이 클라이언트 요청을 HttpRequest라는 객체로 추상화해 구현함으로써 RequestHandler는 요청 데이터를 조작하는 부분을 제거할 수 있었다.

    5-2. 응답 처리 담당하는 HttpResponse 클래스

     

    Hint
    ① RequestHandler 클래스를 보면 응답 데이터 처리를 위한 많은 중복이 있다. 이 중복을 제거해 본다.
    ② 응답 헤더 정보를 Map<String, String>으로 관리한다.
    ③ 메서드를 아래와 같이 분리한다
    - forward() : 응답을 보낼 때 HTMl, CSS, JavaScript 파일을 직접 읽어 응답으로 보내는 메서드
    - sendRedirect() : 다른 URL로 리다이렉트

     

    HttpResponse 테스트 작성(p150)

    class HttpResponseTest {
        private String testDirectory = "./src/test/resources/";
    
        @DisplayName("")
        @Test
        void responseForward() throws IOException {
            //HTTP_Forward.txt 결과는 응답 body에 index.html이 포함되어 있어야 한다.
            HttpResponse response = new HttpResponse(createOutputStream("Http_Forward.txt")); // *해당 위치에 파일 생성함
            response.forward("/index.html");
        }
    
        @DisplayName("")
        @Test
        void responseRedirect() throws IOException {
            //HTTP_Redirect.txt 결과는 Location: index.html 정보가 포함되어 있어야 한다.
            HttpResponse response = new HttpResponse(createOutputStream("HTTP_Redirect.txt"));
            response.sendRedirect("/index.html");
        }
    
        @DisplayName("")
        @Test
        void responseCookies() throws IOException {
            //HTTP_Cookie.txt 결과는 응답 body에 index.html이 포함되어 있어야 한다.
            HttpResponse response = new HttpResponse(createOutputStream("HTTP_Cookies.txt"));
            response.addHeader("Set-Cookie", "logined= true");
            response.sendRedirect("/index.html");
        }
    
        private OutputStream createOutputStream(String fileName) throws FileNotFoundException {
            return new FileOutputStream(new File(testDirectory + fileName));
        }
    }

     

    참고.

    HttpResponseTest 실행시 지정된 경로에 아래와 같이 파일이 생성된다

     

    /src/test/resources/HTTP_Cookies.txt

    HTTP/1.1 302 Found
    Set-Cookie: logined= true
    Location: /index.html

     

    HttpResponse 클래스(p167)

    public class HttpResponse {
        private static final Logger log = ...
    
        private DataOutputStream dos;
        
        private Map<String, String> headers = new HashMap<>();
        
        public HttpResponse(OutputStream out) {
            this.dos = new DataOutputStream(out);
        }
    
        public void addHeader(String key, String value) {
            headers.put(key, value);
        }
    
        public void forward(String url) {
            try {
                byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
                if(url.endsWith(".css")) {
                    headers.put("Content-Type", "text/css");
                } else if(url.endsWith(".js")) {
                    headers.put("Content-Type", "application/javascript");
                } else if(url.endsWith(".html")) {
                    headers.put("Content-Type", "text/html;charset=utf-8");
                }
                headers.put("Content-Length", String.valueOf(body.length));
                response200Header();
                responseBody(body);
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
        public void forwardBody(String body) {
            byte[] contents = body.getBytes();
            headers.put("Content-Type", "text/html;charset=utf-8");
            headers.put("Content-Length", String.valueOf(contents.length));
            response200Header();
            responseBody(contents);
        }
    
        public void sendRedirect(String redirectUrl) {
            try {
                dos.writeBytes("HTTP/1.1 302 Found \r\n");
                processHeaders();
                dos.writeBytes("Location: " + redirectUrl + " \r\n");
                dos.writeBytes("\r\n");
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
        private void responseBody(byte[] body) {
            try {
                dos.write(body, 0, body.length);
                dos.writeBytes("\r\n");
                dos.flush();
            }catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
        private void response200Header() {
            try {
                dos.writeBytes("HTTP/1.1 200 OK \r\n");
                processHeaders();
                dos.writeBytes("\r\n");
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
        private void processHeaders() {
            try {
                for (String key : headers.keySet()) {
                    dos.writeBytes(key + ": " + headers.get(key) + " \r\n");
                }
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    }

     

     

    리팩토링 

    RequestHandler 클래스

    public class RequestHandler extends Thread {
    
        public void run() {
    
            try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
                HttpRequest request = new HttpRequest(in);
                HttpResponse response = new HttpResponse(out); //*
                String path = request.getPath();
    
                if(path.startsWith("/user/create") && !path.endsWith(".html")) {
                    // 중략..
                    response.sendRedirect("/index.html");
                } else if(path.startsWith("/user/login") && !path.endsWith(".html")) {
                    User user = DataBase.findUserById(request.getParameter("userId"));
                    if (user == null || !user.getPassword().equals(request.getParameter("password"))) {
                        response.addHeader("Set-Cookie", "logined=false");
                        response.forward("/user/login_failed.html");
                    } else {
                        response.addHeader("Set-Cookie", "logined=true");
                        response.sendRedirect("/index.html");
                    }
                } else if(path.startsWith("/user/list") && !path.endsWith(".html")) {
                    if (isLogin(request.getHeader("Cookie"))) {
                        // 중략..
    
                        response.forwardBody(sb.toString());
                    } else {
                        response.sendRedirect("/index.html");
                    }
                } else {
                    response.forward(path);
                }
    
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
    	 //중략..
    }

     

     

     응답 데이터 처리에 대한책임HttpResponse위임했더니 RequestHandler에서 응답 헤더와 본문 처리를 담당하던 모든 private 메소드를 제거할 수 있었다. 


    5-3. Controller 인터페이스와 AbstractControllor 추상 클래스

     

    Hint
    ① 각 요청과 응답에 대한 처리를 담당하는 부분을 추상화해 인터페이스로 만든다(아래 클래스 다이어그램 참고)
    ② RequestHandler의 각 분기문을 Controller 인터페이스를 구현하는 클래스를 만들어 분기한다
    ③ 이렇게 생성한 Controller 구현체를 Map<String, Controller>에 저장한다.
    (Map 키에 해당하는 String은 요청 URL, value에는 Controller 구현체)
    ④ 클라리언트 요청 URL에 해당하는 Controller를 찾아 service() 메소드를 호출한다
    ⑤ AbstractController 추상클래스를 추가해 service() 메서드에서 HttpMethod에 따라 doGet(), doPost() 호출하도록 한다

     

     

    p171

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

     

     

    클래스 다이어그램을 본 후 “AbstractController 의 service()에서 HttpMethod에 따라 분기를 하면 되지 않을까?” 생각을 하고 하니 쉽게 작성할 수 있었다

     

    interface Controller

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

     

     

    AbstractController 클래스

    public abstract class AbstractController implements Controller {
        @Override
        public void service(HttpRequest request, HttpResponse response) {
            HttpMethod httpMethod = request.getMethod();
            if(httpMethod.isPost()) {
                doPost(request, response);
            } else {
                doGet(request, response);
            }
        }
    
        protected void doGet(HttpRequest request, HttpResponse response) {}
        protected void doPost(HttpRequest request, HttpResponse response) {}
    }

    doGet(), doPost() 메서드를 추상 메서드로 할 경우 상속 받은 자식 클래스에서 모두 구현해줘야 하므로, 위와 같이 하여 오버라이딩 처리하도록 함

     

     

    CreateUserController 클래스 구현 

    public class CreateUserController extends AbstractController {
        private static final Logger log = LoggerFactory.getLogger(CreateUserController.class);
    
        @Override
        protected 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");
        }
    }

     

    나머지 ListUserControllerLoginController도 동일하게 구현하면 된다

     

     

    RequestHandler 리팩토링(최종)

    public class RequestHandler extends Thread {
        private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
    
        private Socket connection;
    
        private static final Map<String, Controller> controllerMap;
    
        static {
            controllerMap = new HashMap<>();
            controllerMap.put("/user/login", new LoginController());
            controllerMap.put("/user/list", new ListUserController());
            controllerMap.put("/user/create", new CreateUserController());
        }
    
        public RequestHandler(Socket connectionSocket) {
            this.connection = connectionSocket;
        }
    
        public void run() {
            try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
                HttpRequest request = new HttpRequest(in);
                HttpResponse response = new HttpResponse(out);
    
                Controller controller = 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 static Controller getController(String requestUrl) {
            return controllerMap.get(requestUrl);
        }
        
        private String getDefaultPath(String path) {
            if("/".equals(path)) {
                return "/index.html";
            }
    
            return path;
        }
    }
    반응형