[Web] 서블릿 컨트롤러 설계

2022. 10. 23. 21:32·Programming/Web
728x90

컨트롤러를 구현하기 위해서는 컨트롤러가 처리해야 하는 주요 기능에 대해 먼저 이해해야 한다.

컨트롤러의 가장 기본적인 기능인 클라이언트 요청 처리, 입력값 핸들링, 뷰 이동에 대해 살펴보고 실제 서블릿에서 어떻게 구현하는지 살펴본다.

클라이언트 요청 처리

우선 클라이언트 요청은 단일 컨트롤러에서 처리할 것인지 개별 컨트롤러에서 처리할 것인지 결정해야 한다. 서블릿은 URL 요청을 GET, POST 등의 HTTP 메서드를 통해 처리하는 구조이기 때문에 여러 URL 패턴을 하나의 서블릿에서 처리할 수 있지만 URL에 따라 다른 처리를 구현할 수는 없다.

예를 들어 어떤 쇼핑몰에서 제품을 등록하는 기능과 삭제하는 기능이 필요하다고 가정했을 때 각각의 요청 URL은 다음과 같다.

  • 제품 등록 요청 URL : /shop/addProduct
  • 제품 삭제 요청 URL : /shop/delProduct

이때 두 URL 요청을 하나의 서블릿으로 처리하도록 URL 매핑 설정을 할 수 있으나, 두 요청 모두 GET 방식이라면 URL이 다르더라도 동일한 doGet() 메서드가 호출되기 때문에 어떤 요청이 호출된 것인지 구분할 수 없다. 따라서 각각의 URL 요청을 별도의 서블릿으로 구현해야 한다.

 

한 가지 예를 더 보면, 회원 관리 프로그램을 개발한다면 회원 가입, 승인, 수정, 탈퇴(삭제), 로그인 등의 기능이 필요하며 각각의 요청을 처리하기 위한 컨트롤러가 있어야 한다. 즉 다음과 같은 입력/요청 화면과 입력 데이터를 받아 이를 처리하는 컨트롤러, 처리된 결과를 보여주기 위한 화면이 필요하다.

  • 가입 양식 입력 화면      → 가입 처리 컨트롤러                   → 가입 완료 화면
  • 신규 회원 관리자 화면   → 신규 회원 가입 승인 컨트롤러  → 승인 완료 화면
  • 로그인 양식 입력 화면   → 로그인 처리 컨트롤러               → 메인 화면

이 경우 각각의 컨트롤러를 구현해야 하지만 같은 단위의 업무를 하나의 컨트롤러에서 처리하는 것이 구조적으로 관리가 쉬울 수 있다. 물론 하나의 컨트롤러에서 처리할 요청이 지나치게 많은 경우 오히려 코드 관리가 어려울 수 있으므로 주의한다.

GET 방식과 POST 방식
GET 메서드는 특정 리소스의 표시를 요청할 때 사용한다. GET을 사용하는 요청은 파라미터(Query String)를 통해 클라이언트에서 작성된 데이터를 서버로 전송하며, POST 메서드는 특정 서버 리소스(서블릿 등)에 데이터를 전송할 때 사용된다. GET 방식과 달리 웹 브라우저의 주소창에 데이터 부분은 보이지 않으며 데이터는 Request Body에 포함되어 전달된다.

사용자의 요청을 구분해 하나의 서블릿에서 처리하기 위해서는 다음의 두 가지 방법을 사용할 수 있다.

  • URL의 파라미터 이용
  • 프론트 컨트롤러 구현

URL의 파라미터 이용

URL의 파라미터를 이용하는 방식은 URL에 action과 같이 별도의 파라미터를 두어 요청을 구분하는 방법이다.

http://abc.com/member?action=create 
http://abc.com/member?action=login 
  • member: 서블릿 URL 매핑값이다.
  • action: 요청을 구분하기 위한 파라미터다.
  • 요청에는 회원 정보, 로그인 아이디 등의 사용자 데이터가 추가로 포함된다.
  • GET, POST 방식이 모두 가능하다.
  • 컨트롤러에서는 action 값을 비교하여 별도의 메서드 구현 등의 방식으로 처리한다.

비교적 간단한 방법이지만 action 파라미터의 구조가 변경되며 관련된 HTML, JSP, 컨트롤러의 수정이 필요하다는 단점이 있다.

 

실제 구현은 다음과 같이 분기문으로 처리할 수 있다.

doGet(...) {
    String action = request.getParameter("action");
    switch(action) {
        case "create": createMember(); break;
        case "login": loginMember(); break;
        ...
    }
}

프론트 컨트롤러 구현

프론트 컨트롤러를 구현하는 방식은 모든 요청의 진입점이 되는 컨트롤러가 있고 여기에서 서브 컨트롤러를 호출하는 구조다. 좀 더 복잡한 구조를 체계적으로 처리할 수 있으며 프론트 컨트롤러 패턴으로 정립되어 있어 여러 구현에 응용되는 디자인 패턴이다.

프론트 컨트롤러를 구현하기 위해서는 우선 모든 요청을 하나로 모으는 방법이 필요하다. 일반적으로는 서블릿 매핑의 구조적인 특징을 활용하는데, 예를 들어 URL 요청을 특정 확장자 형식으로 끝나도록 설계하는 방식이 있다.

http://abc.com/member/create.do 
http://abc.com/member/login.do
  • /member: 회원 관리 웹 애플리케이션 콘텍스트 혹은 서비스 구분 경로다.
  • *.do: 서블릿 URL매핑값으로 모든 요청은 하나의 서블릿으로 호출된다.
  • 컨트롤러에서는 .do 앞의 요청 이름(create, login)으로 구분하여 별도의 메서드 혹은 서브클래스를 통해 실행한다.

요청에 대한 파라미터 없이 명확한 이름(위 예시의 create, login)으로 요청할 수 있으며, 요청에 대한 URL 관리가 필요없다는 장점이 있다. 반면 전체 시스템이 네이버와 같이 포털 형태로 회원관리, 블로그, 카페 등으로 세부 시스템이 분리되어 있는 경우 콘텍스트를 분리하는 것은 세션 관리 등에 부담이 갈 수 있다. 또한 단일 콘텍스트에 경로로 구분하는 경우 프론트 컨트롤러에서 모든 요청을 조건문과 메서드 구현만으로 처리하기에는 컨트롤러 클래스가 너무 비대해지는 문제도 발생할수 있다.

따라서 규모가 어느 수준 이상이 되면 경로에 따라 서브 컨트롤러로 포워딩하는 처리가 필요하다. 서브 컨트롤러를 구현하기 위해서는 먼저 다음과 같이 URL 요청을 분석해 사용자 요청을 구분하는 작업이 필요하다.

@WebServlet("*.do")
public class MemberController extends HttpServlet {
    ...
    doGet(...) {
        String uri = request.getRequestUri();
        String conPath = request.getContextPath();
        String command = uri.substring(conPath.length());

        switch(command) {
            case "create.do" : createMember(); break;
            case "login.do" : loginMember(); break;
            ...
        }
    }
}

이 구조에서는 action과 같은 파라미터는 없지만 메서드를 이용해 사용자 요청을 분리해서 처리한다. 이렇게 switch(혹은if) 문을 사용하는 구조는 기능 추가 또는 변경이 필요할 때 조건문도 함께 관리해야 하는 문제가 있다. 이러한 문제점은 Command 패턴을 사용하면 다음과 같이 switch(혹은if) 구조 없이 해당 요청에 맞는 특정 컨트롤러가 실행되도록 구현할 수 있다.

public void init(ServletConfig sc) throws ServletException {
    Map<String, SubController> contList = new HashMap<>();

    contList.put("/create.do", new MemberCreateController());
    contList.put("/login.do", new LoginController());
	...
}

public void service(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
    String url = request.getRequestURI();
    String contextPath = request.getContextPath();
    String path = url.substring(contextPath.length());

    SubController subController = contList.get(path);
    subController.process(request, response);
}
  • 처리하고자 하는 요청 URL을 키로 해서 컨트롤러 클래스 인스턴스를 맵에 저장한다.
  • 사용자 요청을 처리할 때 경로 이름(명령)만 가지고도 처리할 컨트롤러 객체를 참조할 수 있다.
  • init()에서 요청에 따른 컨트롤러 객체를 생성한 후 맵에 추가한다.

이제 서브 컨트롤러를 운영하는 프론트 컨트롤러를 설계한다. 먼저 서브 컨트롤러의 규격을 정의한 인터페이스가 필요하다.

public interface SubController {
	void process(HttpServletRequest request, HttpServletResponse response);
}

다음으로 필요한 서브 컨트롤러를 구현한다. 다중 요청 처리 서블릿을 서브 컨트롤러 규격에 맞게 수정한 것이다.

public class MemberController implements SubController {
    void process(HttpServletRequest request, HttpServletResponse response) {
        ...
    }
}
  • 기본적으로는 단일 기능을 수행하며 서브 URL 경로를 추가로 구분해 메서드 단위로 여러 요청을 처리하도록 구현하는 것도 가능하다.

컨트롤러라는 것이 단순히 서블릿으로 요청을 처리하기만 하면 되는 것이 아니라 체계적인 구조로 이루어져야 한다는 개념을 확실히 가져야 한다.

스프링 프레임워크에서 프론트 컨트롤러 구현

스프링 프레임워크의 경우 애너테이션 방식으로 구현하기 때문에 클래스 생성 시 컨트롤러 클래스로 지정한 다음 메서드 단위로 URL 매핑이 가능해 훨씬 간편하면서 관리가 용이한 구조의 컨트롤러 운영이 가능하다.

@Controller
@RequestMapping("/member")
public class MemberController {
    @PostMapping("create")
    public void createMember() {
    ...
    }
}
  • http://abc.com/member/create URL로 로 POST 요청을 처리한다.
  • 같은 방법으로 BlogController, CafeController 등을 두어 운영할 수도 있다.

입력값 핸들링

서블릿에서 클라이언트의 입력값을 처리하려면 request.getParameter()를 이용해야 한다. 파라미터가 한두 개라면 문제없겠지만 회원 가입과 같이 여러 정보가 전달되는 경우 모든 값을 request.getParameter()로 받는 것은 문제가 된다. 또한DAO 클래스와 연동을 위해서는 입력값을 Member 객체로 만든 후에 전달해야 하므로 기본적으로 다음과 같은 코드 구현이 필요하다.

doGet(...) {
    Member m = new Member();
    m.setName(request.getParameter("name"));
    m.setTel(request.getParameter("tel"));
    ...
    dao.create(m);
}

JSP에서는 useBean 액션을 통해 입력값을 Member 객체로 쉽게 만들 수 있다. 하지만 서블릿에서는 그런 기능이 제공되지 않기 때문에 별도의 라이브러리를 사용해야 하는데, 대표적으로 Apache Commons BeanUtils가 쓰인다.

doGet(...) {
    Member m = new Member();
    BeanUtils.populate(m, request.getParameterMap());
    ...
    dao.create(m);
}

스프링 프레임워크에서의 입력값 핸들링

스프링 프레임워크에서는 기본적으로 메서드 파라미터 지정으로 자동 처리가 가능하다. 즉 create라는 URL 요청으로 전달되는 파라미터를 Member 클래스의 멤버 변수에 자동으로 전달해 인자로 제공한다.

@PostMapping("create")
public void createMember(Member m) {
	dao.create(m);
}

뷰 이동

컨트롤러에서 사용자 요청을 처리한 다음에는 적절한 뷰로 이동할 수 있어야 한다. 이때 뷰에서 보여줄 데이터를 포함해서 이동해야 하는 경우와 그렇지 않은 경우가 있다.

데이터를 포함하지 않는 경우

사용자 요청 처리 후 별도의 데이터를 포함하지 않는다면 해당 페이지로 리디렉션(Redirection)할 수 있다. JSP, 서블릿 모두 response.sendRedirect()을 사용하면 된다.

response.sendRedirect("main.jsp");

데이터를 포함하는 경우

데이터를 포함해서 이동하는 경우 request scope object에 속성으로 데이터를 넣은 후 원하는 페이지로 포워딩한다. 데이터 활용 목적에 따라 session 또는 application을 사용할 수도 있으며 여러 데이터를 포함하는 것도 가능하다.

doGet(...) {
    ...
    request.setAttribute("member", m);
    RequestDispatcher dispatcher = request.getRequestDispatcher("userInfo.jsp");
    dispatcher.forward(request, response);
}

JSP와 스프링 프레임워크에서의 뷰 이동

JSP에서 뷰 이동을 구현할 경우 다음과 같이 작성한다.

<%
    request.setAttribute("member", m);
    pageContext.forward("userInfo.jsp");
%>

 

스프링 프레임워크의 경우 인자로 전달된 Model 객체에 원하는 데이터를 저장하고 뷰 페이지 이름을 리턴하기만 하면 된다.

@GetMapping("info")
public String getMemberInfo(int id, Model model) {
    ...
    model.addAttribute("member", m);
    return "userInfo";
}
  • 리턴되는 문자열 값은 뷰 페이지의 이름이며 확장자는 생략된다.

서블릿, JSP, 스프링 프레임워크 모두 코드는 조금씩 다르지만 기본 개념과 구조는 유사하다는 것을 알 수 있다.

'Programming > Web' 카테고리의 다른 글

[Web] 관계형 데이터베이스(RDBMS)  (0) 2022.10.24
[Web] 데이터베이스(Database)  (0) 2022.10.24
[Web] MVC 패턴  (0) 2022.10.22
[Web] JSTL(JSP Standard Tag Library)  (0) 2022.10.22
[Web] 커스텀 태그와 EL  (0) 2022.10.22
'Programming/Web' 카테고리의 다른 글
  • [Web] 관계형 데이터베이스(RDBMS)
  • [Web] 데이터베이스(Database)
  • [Web] MVC 패턴
  • [Web] JSTL(JSP Standard Tag Library)
arajo
arajo
  • arajo
    아라 메모장
    arajo
  • 전체
    오늘
    어제
    • 분류 전체보기 (509)
      • Language (298)
        • HTML (55)
        • CSS (11)
        • JavaScript (70)
        • TypeScript (8)
        • Python (33)
        • Java (119)
        • C (0)
        • C# (2)
      • Programming (92)
        • Programming (14)
        • Web (51)
        • Apache (1)
        • MySQL (23)
        • AWS (3)
      • Framework | Library (26)
        • Framework | Library (3)
        • Vue.js (2)
        • React.js (5)
        • React Native (4)
        • Node.js (1)
        • Ajax (1)
        • Bootstrap (8)
        • Spring (1)
        • Flutter (1)
      • etc (2)
      • 휴식 (19)
        • 책 (13)
        • 일기 (5)
        • 게임 일기 (1)
      • A (71)
        • 공부 (18)
        • 기타 (6)
        • 일 (47)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    제어문
    react
    MySQL
    JavaScript
    event
    자바스크립트
    web
    array
    타입스크립트
    리액트
    Java
    Python
    TypeScript
    파이썬
    next.js
    객체
    object
    CSS
    변수
    HTML
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
arajo
[Web] 서블릿 컨트롤러 설계
상단으로

티스토리툴바