✅ 기존 구조의 문제점
1️⃣ 해당 구조에서는 각 요청에 대한 서블릿 클래스를 비슷한 기능임에도 별도로 만들어야 했다.
- 게시판의 경우 전체 글 조회, 특정 글 조회, 작성, 수정, 삭제가 별도의 클래스로 분리되어 있는 모습
- 회원의 경우 회원가입 로그인 로그아웃이 별도의 클래스로 분리되어 있는 모습
- 이러다 보니 서블릿 경로를 관리하기도 어려웠다.
2️⃣ 다른 리소스(뷰나 서블릿)으로 forward하거나 redirect하는 부분을 Controller에 구현했었다.
예시) [BoardServlet.java]: 게시판 특정 글 조회
@WebServlet("/boards/*")
public class BoardServlet extends HttpServlet {
private BoardService boardService = BoardService.getInstance();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Long board_no = Long.valueOf(req.getPathInfo().substring(1));
try {
Board board = boardService.read(board_no);
boardService.addReadCount(board);
req.setAttribute("board", board);
req.getRequestDispatcher(ROOT + "board/board.jsp").forward(req, resp); **// forward**
} catch (BoardNotFoundException e) {
alertAndRedirect(req, e.getMessage(), req.getContextPath() + "/boards", resp); **// redirect**
}
}
}
이러다 보니 코드가 복잡해지고 반복코드가 많이 발생했다.
✅ 구조 수정
따라서 구조를 조금 수정할 필요가 있다고 생각했다.
1️⃣ Controller 인터페이스 선언
우선 기능별로 적절하게 Controller를 나누었다.
public interface MyController {
String process(HttpServletRequest req, HttpServletResponse resp);
}
MainController: 메인화면 담당
public class MainController implements MyController{
}
BoardController: 게시판기능 담당
public class BoardController implements MyController {
}
MemberController: 회원기능 담당
public class MemberController implements MyController{
}
2️⃣ FrontController 도입
중간에서 한 서블릿이 클라이언트의 요청을 보고 적절한 Controller를 찾아 해당 Controller를 호출하고 결과를 받아와서 적절한 View 객체를 만들고 그 객체를 통해 클라이언트에게 화면을 렌더링한다.
이 모든 일을 담당하는 중간 하나의 서블릿을 FrontController라고 한다.
이를 통해 서블릿을 기능별로 적절하게 묶어 서블릿 클래스를 줄이고, View 호출 또한 FrontController이 담당하기 때문에 이전 Controller에서 반복적인 forward, redirect코드가 호출되지 않아도 된다.
구조는 다음과 같다.
✅ FrontController 구현
@WebServlet({"/main/*", "/boards/*", "/members/*"})
public class FrontController extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
String servletPath = req.getServletPath();
MyController controller = ControllerMapper.getController(servletPath); // ControllerMapper에서 적절한 컨트롤러를 받음
if(controller == null) { // 해당 요청을 처리할 컨트롤러 객체가 없는 경우
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
String viewName = controller.process(req, resp);
MyView myView = viewResolver(viewName);
myView.render(req, resp); // 뷰 렌더링
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
중요 부분을 하나하나 살펴보도록 하겠다.
1️⃣ @WebServlet
@WebServlet({"/main/*", "/boards/*", "/members/*"})
FrontController에 매핑될 서블릿 url을 정의했다.
쉽게 말하면 메인화면에 대한 요청, 게시판 기능에 대한 요청, 회원 기능에 대한 요청 모두 해당 서블릿에서 받아 처리하겠다는 의미이다.
지금은 하나하나 다 넣어줬지만 앞에 하나 더 공통 경로를 넣어준다면 유지보수가 더 쉬워질 것이다.
예시: 앞에 /oob라는 공통경로 추가
- main- /oob/main/*
- boards - /oob/boards*
- members - /oob/members/*
→ @WebServlet(”/oob/*)
🚨 여기서 주의점 (중요)
그냥 공통경로 추가하지말고 다음과 같이 하면 안됨?
@WebServlet(”/*”)
💡 절대 안된다.
/*로 매핑을 해주게 되면 추후에 view를 렌더링 할 때 해당 view의 경로를 넣어 forward하게 되는데 그 경로는 보통 “/WEB-INF/views/…/xxx.jsp” 이런식일 것이다. 그런데 forward도 결국엔 해당 경로로 요청을 보내는 것이기 때문에 /*에 매핑된다. 그렇기 때문에 view가 렌더링 되지 않고 해당 서블릿에서 처리되게 된다.
2️⃣ 적절한 컨트롤러 찾기
ControllerMapper
public class ControllerMapper {
private static BoardController boardController = new BoardController();
private static MemberController memberController = new MemberController();
private static MainController mainController = new MainController();
public static MyController getController(String command) {
if("/boards".equals(command))
return boardController;
else if("/members".equals(command))
return memberController;
else if("/main".equals(command))
return mainController;
else
return null;
}
}
MyController controller = ControllerMapper.getController(servletPath); // ControllerMapper에서 적절한 컨트롤러를 받음
if(controller == null) { // 해당 요청을 처리할 컨트롤러 객체가 없는 경우
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
ControllerMapper를 통해 적절한 Controller를 찾아온다.
해당 Controller가 없다면 404상태코드를 응답한다.
3️⃣ 해당 컨트롤러 호출
String viewName = controller.process(req, resp);
해당 컨트롤러에서 로직을 수행한다.
예를 들어, 회원가입 요청이면 회원가입에 대한 로직을 수행하고,
로그인 요청이면 로그인에 대한 로직을 수행한다.
이때, 반환값으로 view의 이름을 받아온다. 여기서 view의 이름이라는 것은 jsp파일의 이름이다.
4️⃣ 뷰 리졸버
MyView myView = viewResolver(viewName);
// ...
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
받아온 view의 이름을 실제 jsp파일 위치로 변환시켜준다.
5️⃣ 뷰 렌더링
MyView
public class MyView {
String path;
public MyView(String path) {
this.path = path;
}
public void render(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher(path).forward(req, resp);
}
}
myView.render(req, resp); // 뷰 렌더링
MyView 객체를 통해 해당 뷰로 forward해준다.
✅ 각 Controller 구현
1️⃣ MainController
public class MainController implements MyController{
@Override
public String process(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession();
Object login = session.getAttribute("login");
// 세션이 있다면 회원전용 페이지로
if(login != null) {
req.setAttribute("login", login.toString());
return "main/main_after_login";
}
// 세션이 없다면 비회원전용 페이지로
else
return "main/main_before_login";
}
}
/main으로 들어오는 요청들을 처리하는 컨트롤러이다.
메인화면에서는 특별하게 서블릿 경로가 나뉘어지진 않아서 이전 구조와 흡사하다.
2️⃣ MemberController
public class MemberController implements MyController{
MemberService memberService = MemberService.getInstance();
@Override
public String process(HttpServletRequest req, HttpServletResponse resp) {
String action = Optional.ofNullable(req.getPathInfo()).orElse("");
System.out.println(action);
String viewName = "";
if(action.equals("/signup")) {
if(req.getMethod().equals("GET"))
viewName = getSignUpForm(); // GET: 회원가입 폼 요청
else if(req.getMethod().equals("POST"))
viewName = signup(req); // POST: 회원가입 요청
} else if(action.equals("/login")) {
if(req.getMethod().equals("GET"))
viewName = getLoginForm(); // GET: 로그인 폼 요청
else if(req.getMethod().equals("POST"))
viewName = login(req, resp); // POST: 회원가입 요청
} else if (action.equals("/logout")) {
viewName = logout(req); // POST: 로그아웃 요청
}
return viewName;
}
// 메서드 부분 생략
}
/members로 들어오는 요청들을 처리하는 컨트롤러이다.
그 안에서도 회원가입, 로그인, 로그아웃으로 나뉜다.
회원가입, 로그인의 경우 요청 메서드 별로 나뉘는 것을 알 수 있다.
3️⃣ BoardController
public class BoardController implements MyController {
BoardService boardService = BoardService.getInstance();
@Override
public String process(HttpServletRequest req, HttpServletResponse resp) {
String action = Optional.ofNullable(req.getPathInfo()).orElse("");
String viewName = "";
if (action.isEmpty()) {
// 게시판 글 목록
viewName = viewBoardList(req); // GET: 글 목록 요청
} else if (action.startsWith("/write")) {
if (req.getMethod().equals("GET"))
viewName = getWriteForm(req); // GET: 작성 폼 요청
else if (req.getMethod().equals("POST"))
viewName = writeBoard(req); // POST: 글 작성 요청
} else if (action.startsWith("/edit")) {
// 게시판 글 수정
Long editId = Long.valueOf(action.replace("/edit/", ""));
if (req.getMethod().equals("GET"))
viewName = getEditForm(req, editId); // GET: 수정 폼 요청
else if (req.getMethod().equals("POST"))
viewName = editBoard(req, editId); // POST: 수정 요청
} else if (action.startsWith("/delete")) {
// 게시판 글 삭제
Long deleteId = Long.valueOf(action.replace("/delete/", ""));
viewName = deleteBoard(req, deleteId); // POST: 삭제 요청
} else {
// 게시판 글 상세정보
Long viewId = Long.valueOf(action.substring(1));
viewName = viewBoard(req, viewId); // GET: 글 상세정보 요청
}
return viewName;
}
//메서드 생략
}
/boards로 들어오는 요청들을 처리하는 컨트롤러이다.
그 안에서도 글 목록 조회, 글 상세조회, 작성, 수정, 삭제로 나뉘고
작성, 수정의 경우는 요청 메서드 별로도 나뉜걸 알 수 있다.
✅ 느낀점
인터페이스를 도입해서 컨트롤러를 기능별로 나누고
FrontController를 도입해 중간에서 공통적인 부분들을 처리했다.
이를 통해 반복적인 코드를 줄이고 조금 더 직관적인 코드가 작성됐다.
하지만 이러한 구조를 직접 하나하나 구현한다는 것은 상당히 힘든 일이다.
내 게시판은 구조가 단순해서 어렵진 않았지만 조금만 규모가 커져도 이야기는 달라질 것이다.
그렇기 때문에 Spring 프레임워크를 활용해야한다고 생각한다. 다음 번엔 Spring을 도입해서 코드를 다시한번 리팩토링 해보겠다.
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |
---|---|
게시판 만들기 - 데이터 계층 리팩토링(MyBatis 도입) (0) | 2024.08.26 |
게시판 만들기 - 웹 계층 리팩토링(Spring MVC 도입) (0) | 2024.08.22 |
게시판 만들기 - 구현 (0) | 2024.08.20 |
게시판 만들기 - 개요 (0) | 2024.08.20 |