Servlet+JSP 복습 01

moon.kick·2025년 5월 15일

review

목록 보기
1/5

어머, 내 설명이 조금이나마 도움이 됐다니 정말 기쁘다! 😊 강의 노트까지 보여주니 내가 더 신나서 자세히 설명해 줄 수 있겠어! 클로이 녀석이 짠 FrontController를 DispatcherServlet으로 바꾸는 작업까지 함께 해보자! 차근차근 따라오면 분명히 이해할 수 있을 거야.

자, 먼저 네가 준 강의 노트를 꼼꼼히 살펴보자.

이미지를 보면 MVC 패턴의 흐름이 좀 더 자세하게 나와 있네! Presentation Layer, Business Logic Layer, Persistence Layer로 나누어서 각 계층의 역할을 보여주고 있어.

  • Presentation Layer (View & Controller): 사용자와 직접 상호작용하는 계층이야.
    • View (HTML, JSP, EL/JSTL, JS, CSS): 사용자에게 정보를 보여주고, 사용자의 입력을 받는 역할을 해. 네가 익숙한 웹 페이지 화면이라고 생각하면 돼.
    • Controller (HttpServlet 상속): 사용자의 요청을 받아서 적절한 Service를 호출하고, Service의 처리 결과를 받아서 View를 선택해서 응답하는 역할을 해. 이미지에서는 Controller가 Service의 메서드를 호출(service.XXX())하고, 그 결과에 따라 성공 시에는 OK View로, 실패 시에는 Error View로 이동하는 것을 보여주고 있어.
  • Business Logic Layer (Service): 애플리케이션의 핵심 비즈니스 로직을 처리하는 계층이야. Controller로부터 요청을 받아서 필요한 데이터를 Model (여기서는 DAO)에게 요청하고, 받은 데이터를 가공하거나 여러 단계를 거쳐 원하는 결과를 만들어내. 이미지에서 Service는 DAO의 메서드를 호출(dao.insert())하고, 그 결과를 바탕으로 예외를 처리하거나 다음 단계로 진행하는 것을 보여주고 있어.
  • Persistence Layer (DAO & DB): 데이터베이스와 상호작용하는 계층이야. 데이터를 저장, 조회, 수정, 삭제하는 등의 작업을 담당하지. 이미지에서 DAO는 insert() 메서드를 통해 데이터베이스에 데이터를 삽입하고, SQL Exception이 발생할 수 있음을 보여주고 있어.

이 흐름을 DispatcherServlet 기반 MVC 구조와 연결해서 다시 한번 생각해 보자.

  1. 요청: 사용자가 View를 통해 어떤 액션을 하면 요청이 발생해. 예를 들어, 폼에 데이터를 입력하고 "저장" 버튼을 누르는 거지.
  2. DispatcherServlet: 이 요청은 가장 먼저 DispatcherServlet에게 전달돼.
  3. Handler Mapping: DispatcherServlet은 설정 파일 (예: XML)이나 어노테이션을 기반으로 이 요청을 처리할 적절한 Handler (Controller) 를 찾아. 마치 이미지에서 Controller를 선택하는 과정과 비슷하다고 생각하면 돼.
  4. Controller: DispatcherServlet이 찾은 Controller는 요청을 분석하고, 필요한 경우 Service를 호출해서 비즈니스 로직을 처리하도록 시켜.
  5. Service: Service는 필요한 데이터를 DAO에게 요청하거나, 여러 비즈니스 로직을 수행해.
  6. DAO: DAO는 데이터베이스와 상호작용해서 데이터를 가져오거나 저장하는 등의 작업을 수행하고, 그 결과를 Service에게 반환해.
  7. Model: Service는 DAO로부터 받은 데이터를 가공해서 Controller에게 전달할 Model을 만들어. 이 Model은 View에 전달될 데이터를 담고 있어.
  8. View Resolver: Controller는 처리 결과를 담은 Model과 함께 View의 이름을 DispatcherServlet에게 알려줘. DispatcherServlet은 View Resolver를 이용해서 실제 View (JSP 파일 등)의 경로를 찾아.
  9. View: DispatcherServlet이 찾은 View는 Model에 담긴 데이터를 이용해서 사용자에게 보여줄 응답 (HTML 페이지 등)을 생성해.
  10. 응답: 생성된 응답은 DispatcherServlet을 거쳐서 다시 사용자에게 전달돼.

이제 클로이가 짜준 FrontController를 DispatcherServlet으로 바꿔보자!

DispatcherServlet은 서블릿 컨테이너 (예: Tomcat)가 제공하는 기능을 활용하기 때문에, 네가 직접 HttpServlet을 상속받아서 모든 요청을 처리하는 FrontController를 만들 필요가 없어. 대신 Spring MVC와 같은 프레임워크에서 제공하는 DispatcherServlet을 설정하고, HandlerMapping, Controller, ViewResolver 등을 설정해서 네 애플리케이션의 흐름을 제어하게 돼.

하지만 지금 네 코드 구조에서는 DispatcherServlet을 바로 적용하기는 어렵기 때문에, DispatcherServlet의 핵심 아이디어를 네 FrontController에 녹여보는 방식으로 개선해 볼게.

현재 FrontController는 모든 요청을 받아서 직접 URI를 분석하고, 조건문을 이용해서 각 Controller의 process 메서드를 호출하는 방식으로 동작하고 있어. 이걸 좀 더 유연하게 만들기 위해서 다음과 같은 변경을 고려해 볼 수 있어.

  1. HandlerMapping 도입: URI와 Controller를 매핑하는 HandlerMapping 인터페이스와 이를 구현하는 클래스를 만들어서, URI에 따라 어떤 Controller를 실행할지 결정하도록 분리하는 거야. 지금처럼 if-else if 문을 사용하는 대신, Map과 같은 자료구조를 이용해서 URI와 Controller를 관리하면 더 깔끔해지겠지?

  2. Controller 인터페이스 통일: 현재 UserController, AccommodationController 등의 Controller들은 process라는 메서드를 가지고 있지만, 인터페이스를 구현하도록 통일하면 DispatcherServlet처럼 다양한 Controller를 일관된 방식으로 처리할 수 있게 돼.

  3. ModelAndView 도입: Controller에서 처리 결과 데이터와 View 이름을 함께 담아서 반환하는 ModelAndView 객체를 사용하는 거야. 이렇게 하면 FrontController는 Controller의 처리 결과와 보여줄 View 정보를 명확하게 알 수 있고, View를 선택하고 응답을 생성하는 역할을 분리할 수 있어.

개선된 FrontController의 대략적인 구조는 다음과 같을 거야.

package web.mvc;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

// HandlerMapping 인터페이스 (URI와 Controller를 매핑)
interface HandlerMapping {
    Object getHandler(String uri);
}

// 간단한 HandlerMapping 구현
class SimpleHandlerMapping implements HandlerMapping {
    private Map<String, Object> handlerMap = new HashMap<>();

    public SimpleHandlerMapping() {
        handlerMap.put("/user", new UserController()); // 예시
        handlerMap.put("/accommodation", new AccommodationController()); // 예시
        handlerMap.put("/room", new RoomController()); // 예시
        handlerMap.put("/reservation", new ReservationController()); // 예시
    }

    @Override
    public Object getHandler(String uri) {
        // URI 패턴에 따라 적절한 Controller를 반환
        if (uri.startsWith("/user")) {
            return handlerMap.get("/user");
        } else if (uri.startsWith("/accommodation")) {
            return handlerMap.get("/accommodation");
        } else if (uri.startsWith("/room")) {
            return handlerMap.get("/room");
        } else if (uri.startsWith("/reservation")) {
            return handlerMap.get("/reservation");
        }
        return null;
    }
}

// Controller 인터페이스 (모든 Controller는 이 인터페이스를 구현)
interface Controller {
    ModelAndView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

// ModelAndView 클래스 (처리 결과 데이터와 View 이름 저장)
class ModelAndView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView(String viewName) {
        this.viewName = viewName;
    }

    public String getViewName() {
        return viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void addObject(String attributeName, Object attributeValue) {
        this.model.put(attributeName, attributeValue);
    }
}

// 기존의 Controller들을 Controller 인터페이스를 구현하도록 수정해야 함
class UserController implements Controller {
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 사용자 관련 로직 처리
        // ...
        ModelAndView mv = new ModelAndView("/user/list.jsp"); // 예시 View 이름
        mv.addObject("users", /* 사용자 목록 데이터 */ null);
        return mv;
    }
}

// ... 다른 Controller들도 마찬가지로 수정 ...

@WebServlet("*.do")
public class FrontController extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private HandlerMapping handlerMapping;

    @Override
    public void init() throws ServletException {
        handlerMapping = new SimpleHandlerMapping();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        processRequest(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        processRequest(request, response);
    }

    private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");

        String uri = request.getRequestURI();
        String contextPath = request.getContextPath();
        String command = uri.substring(contextPath.length());

        // HandlerMapping을 통해 처리할 Controller를 찾음
        Controller controller = (Controller) handlerMapping.getHandler(command);

        if (controller != null) {
            ModelAndView mv = controller.process(request, response);
            if (mv != null) {
                String viewName = mv.getViewName();
                // ViewResolver를 통해 실제 View 경로를 찾고 forward 또는 redirect
                if (viewName.startsWith("redirect:")) {
                    response.sendRedirect(request.getContextPath() + viewName.substring("redirect:".length()));
                } else {
                    jakarta.servlet.RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views" + viewName); // 예시 View 경로
                    dispatcher.forward(request, response);
                }
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

위 코드는 DispatcherServlet의 기본적인 아이디어를 반영한 개선된 FrontController의 예시야.

  • HandlerMapping 인터페이스와 SimpleHandlerMapping 클래스를 도입해서 URI와 Controller를 매핑하는 역할을 분리했어.
  • Controller 인터페이스를 정의하고, 각 Controller가 이 인터페이스를 구현하도록 변경해야 해.
  • ModelAndView 클래스를 도입해서 Controller가 처리 결과 데이터와 View 이름을 함께 반환하도록 했어.
  • FrontControllerprocessRequest 메서드에서는 HandlerMapping을 이용해서 Controller를 찾고, Controller의 process 메서드를 호출해서 ModelAndView를 받아서 View를 처리하도록 변경했어.

주의: 위 코드는 기본적인 개념을 보여주기 위한 예시이며, 실제 DispatcherServlet처럼 다양한 기능 (interceptor, exception resolver 등)을 포함하고 있지는 않아.

이 정도면 클로이가 짠 FrontController와 DispatcherServlet의 차이점, 그리고 DispatcherServlet의 핵심 아이디어를 조금이나마 이해하는 데 도움이 되었기를 바라! 😊 궁금한 점이 있다면 언제든지 다시 물어봐!

profile
@mgkick

0개의 댓글