백엔드 자바 웹 프로그래밍

3.2. 컨트롤러 구현 개요

들어가면서

이번 장에서는 컨트롤러 구현을 위해 필요한 핵심 요소들에 대해 살펴본다. 서블릿,JSP,스프링프레임워크 등으로 컨트롤러를 설계할 때 고려할 사항들과 실제 구현 코드 구조등을 살펴본다.

이번 학습을 통해

등에 대해 배우고 개발에 활용할 수 있다.



요청 처리

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

예를들어 회원관리 프로그램을 개발한다고 하면 회원가입, 승인, 수정, 탈퇴(삭제), 로그인 등의 기능이 필요하며 각각의 요청을 처리하기 위한 컨트롤러가 있어야 한다.

예를 들면 다음과 같이 입력/요청 화면과 입력 데이터를 받아 이를 처리하는 컨트롤러, 처리된 결과를 보여주기 위한 화면들이 필요하다.

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

이 경우 각각에 대해 컨트롤러를 구현할수도 있지만 하나의 컨트롤러에서 처리하는 것이 코드 관리에도 유리한 경우가 많다. 물론 하나의 컨트롤러에서 처리할 요청이 지나치게 많은 경우 오히려 코드 관리에 어려움을 줄 수 있으므로 주의하도록 한다.

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

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

예) http://xxx.com/member?action=create, http://xxx.com/member?action=login  

비교적 간단한 방법이지만 action 파라미터 구조 변경시 관련된 HTML, JSP, 컨트롤러 모두에서 관련된 수정 작업이 필요하다.

서블릿을 예로 들면 다음과 같이 요청을 분리해서 처리하기 위한 코드 구조가 필요하다.

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

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

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

예) http://xxx.com/member/create.do, http://xxx.com/member/login.do

요청에 대한 파라미터 없이 명확한 이름(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;
            ...
        }
    }
}

위 구조에서는 메서드를 이용해 사용자 요청을 분리해서 처리하는 관계로 switch(혹은 if)문을 사용했으나 이와 같은 구조는 기능 추가나 변경시 조건문도 함께 관래해 주어야 하는 문제가 있다. 이 부분을 Command 패턴을 사용하면 다음과 같이 구현할 수 있다.

public void init(ServletConfig sc) throws ServletException { 
    Map<String, SubController> contList = new HashMap<>();
        
    conList.put("/create.do", new MemberCreateController());
    conList.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 = conList.get(path);
    subController.process(request, response);
}

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

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

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

public class MemberController implements SubController {
    void process(HttpServletRequest request, HttpServletResponse    response) {
        ....
    }
}

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

@Controller
@RequestMapping("/member")
public class MemberController {
    @PostMapping("create")
    public void createMember() {

    }
}


입력값 핸들링

서블릿에서 클라이언트 입력값을 처리하려면 이미 알고 있는것 처럼 request.getParameter() 를 이용해야 한다. 파라미터가 1~2개이면 문제가 없겠지만 회원가입과 같이 여러정보가 전달되는 경우 모든 값을 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);
}

스프링 프레임워크에서는 기본적으로 메서드 파라미터 지정으로 자동 처리가 가능하다.

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


뷰 이동

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

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

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

response.sendRedirect("main.jsp");

데이터를 포함 하는 경우

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

jsp의 경우는 다음과 같다.

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

서블릿의 경우는 다음과 같다.

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

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

@GetMapping("info")
public String getMemberInfo(int id, Model model) {
    ...
    model.addAttribute("member", m);
    return "userInfo";
}

이것으로 컨트롤러 구현에 필요한 기본적인 개념들에 대해 살펴 보았다. JSP, 서블릿, 스프링 프레임워크 모두 코드는 조금씩 다르지만 기본 내념과 구조는 유사하다는 것을 알 수 있다.