Rest Api는 리소스 위치를 URI로 하는 것을 권장
현재 프로젝트는 도서 대출은 유저가, 도서 등록은 관리자가 가능하며, 관리자 또한 유저이기도 하다. 따라서, 권한에 따라 컨트롤러를 선택하도록 할 수는 없다.
아래는 유저가 도서를 대출하는 URI이다. 리소스 위치, 그리고 컨트롤 URI까지 부착되어 괜찮다고 생각한다.
/books/{id}/rent
/books/{id}/unrent
그렇다면, 관리자 기능 URI는 어떻게 설계해야 할까?
/books/add
/admin/books/add
둘중에 나는 /admin/이 붙을 필요가 없고, rest api의 원칙에 맞지 않는다고 생각했다.(리소스 위치로 설계하는 것이 권장 사항)
현재 MVC1편의 Front-Controller v5를 개선하여 와일드카드를 붙여 하위 경로를 한 번에 처리할 수 있는 컨트롤러를 부착하여 한 번에 처리하는 것 까지는 개선해둔 상태이지만, Servlet Front-Controller에서 하위 컨트롤러 처리하기 이것으로는 부족하다.
각각 User와 Admin에서 처리하는 컨트롤러 클래스를 만들어 처리하고 싶다. 현재 FrontController는 이렇게 되어 있다. 따라서 한 uri에 하나의 컨트롤러만 부착이 가능하다(단, 하위 uri를 처리할 수 있도록 개선된 상태)
@WebServlet(name = "frontControllerServlet", urlPatterns = "/site/*")
public class FrontControllerServlet extends HttpServlet {
/* handler(Controller)가 매핑된 Map */
private final Map<String, Controller> handlerMappingMap = new HashMap<>();
...
/**
* 이 클래스가 생성되면서 handler(Controller)가 매핑된 Map, handlerAdapter가 들어있는 List가 초기화 됩니다.
*/
public FrontControllerServlet() {
initHandlerMappingMap();
initHandlerAdapters();
}
/**
* 컨트롤러 클래스를 URI에 맞게 등록해서 초기화 합니다.
*/
private void initHandlerMappingMap() {
handlerMappingMap.put("/site", new IndexController());
handlerMappingMap.put("/site/join", new JoinController());
handlerMappingMap.put("/site/login", new LoginController());
handlerMappingMap.put("/site/books/*", new UserBookController());
handlerMappingMap.put("/site/admin", new AdminPageController());
handlerMappingMap.put("/site/users/*", new AdminUsersController());
}
...
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* 핸들러 획득 */
Controller handler = getHandler(request);
/* 핸들러를 획득하지 못하면, 404 응답 */
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json");
JsonResponse errorResponse = new ErrorResponse(
HttpServletResponse.SC_NOT_FOUND,
"Not Found"
);
String result = mapper.writeValueAsString(errorResponse);
response.getWriter().write(result);
return;
}
/* 어댑터 획득 */
List<HandlerAdapter> handlerAdapters = getHandlerAdapter(handler);
for (HandlerAdapter adapter : handlerAdapters) {
/* 어댑터에서 handle 메서드 호출 */
ModelView mv = adapter.handle(request, response, handler);
/* ModelView 객체가 null로 넘어온 경우(redirect된 경우) service 종료 */
if (mv == null) {
continue;
}
/* ModelView 인스턴스에서 viewName(논리적 주소) 획득 */
String viewName = mv.getViewName();
/* 획득한 viewName으로 실제 뷰 위치 만들고 View 반환 */
View view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
}
/**
* @param request : HttpServletRequest 인스턴스
* @return : Controller 인스턴스를 핸들러로 반환
*/
private Controller getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
Controller handler = handlerMappingMap.get(requestURI);
if (handler == null) {
if (request.getRequestURI().startsWith("/site/books/")) {
//user books 컨트롤러
handler = handlerMappingMap.get("/site/books/*");
} else if (request.getRequestURI().startsWith("/site/users/")) {
//users 컨트롤러
handler = handlerMappingMap.get("/site/users/*");
}
}
return handler;
}
...
}
어떻게 개선하면 좋을까? 현재 ForwardController와 JsonResponseController를 각각 사용하기 위해 ModleView가 null인 경우 무시하도록 설계되어 있다. 때문에, 단순히 다중 컨트롤러를 등록할 수만 있게 해준다면, 충분히 쉽게 개선 가능하다고 보았다.
개선
방법은 단순하다. 기존에는 handlerMappingMap의 value가 Controller였지만, 이제 Controller를 담은 Set을 value로 하도록 수정했다.
@WebServlet(name = "frontControllerServlet", urlPatterns = "/site/*")
public class FrontControllerServlet extends HttpServlet {
/* handler(Controller)가 들어있는 Set이 매핑된 Map */
private final Map<String, Set<Controller>> handlerMappingMap = new HashMap<>();
...
/**
* 이 클래스가 생성되면서 handler(Controller)가 매핑된 Map, handlerAdapter가 들어있는 List가 초기화 됩니다.
*/
public FrontControllerServlet() {
initHandlerMappingMap();
initHandlerAdapters();
}
/**
* 컨트롤러 클래스를 URI에 맞게 등록해서 초기화 합니다.
* 다중 컨트롤러를 등록할 수 있습니다.
*/
private void initHandlerMappingMap() {
/* uri를 담은 리스트 */
List<String> uris = new ArrayList<>();
uris.add("/site");
uris.add("/site/login");
uris.add("/site/join");
uris.add("/site/books/*");
uris.add("/site/admin");
uris.add("/site/users/*");
/* uri 리스트를 돌면서 Set에 컨트롤러 add하기 */
for (String uri : uris) {
Set<Controller> handlerSet = new HashSet<>();
handlerMappingMap.put(uri, handlerSet);
switch (uri) {
case "/site" -> {
handlerSet.add(new IndexController());
}
case "/site/login" -> {
handlerSet.add(new LoginController());
}
case "/site/join" -> {
handlerSet.add(new JoinController());
}
case "/site/books/*" -> {
handlerSet.add(new UserBookController());
handlerSet.add(new AdminBookController());
}
case "/site/admin" -> {
handlerSet.add(new AdminPageController());
}
case "/site/users/*" -> {
handlerSet.add(new AdminUsersController());
}
}
}
}
...
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* handlerSet 획득 */
Set<Controller> handlerSet = getHandlerSet(request);
/* handlerSet을 획득하지 못하면, 404 응답 */
if (handlerSet == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json");
JsonResponse errorResponse = new ErrorResponse(
HttpServletResponse.SC_NOT_FOUND,
"Not Found"
);
String result = mapper.writeValueAsString(errorResponse);
response.getWriter().write(result);
return;
}
/* handlerSet을 돌며 어댑터 리스트 획득 */
for (Controller handler : handlerSet) {
List<HandlerAdapter> handlerAdapters = getHandlerAdapters(handler);
/* 등록된 adapter를 돌며 handle 호출 */
for (HandlerAdapter adapter : handlerAdapters) {
/* 어댑터에서 handle 메서드 호출 */
ModelView mv = adapter.handle(request, response, handler);
/* ModelView 객체가 null로 넘어온 경우(redirect된 경우) 다음으로 건너뜀 (무시) */
if (mv == null) {
continue;
}
/* ModelView 인스턴스에서 viewName(논리적 주소) 획득 */
String viewName = mv.getViewName();
/* 획득한 viewName으로 실제 뷰 위치 만들고 View 반환 */
View view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
}
}
/**
* URI에 맞는 handlerSet을 반환하는 메서드
* 와일드카드 (/*)가 붙은 uri는 특별히 처리
*
* @param request : HttpServletRequest 인스턴스
* @return : Controller 인스턴스가 담긴 Set을 반환
*/
private Set<Controller> getHandlerSet(HttpServletRequest request) {
String requestURI = request.getRequestURI();
Set<Controller> handlerSet = handlerMappingMap.get(requestURI);
if (handlerSet == null) {
if (request.getRequestURI().startsWith("/site/books/")) {
//books handlerSet
handlerSet = handlerMappingMap.get("/site/books/*");
} else if (request.getRequestURI().startsWith("/site/users/")) {
//users handlerSet
handlerSet = handlerMappingMap.get("/site/users/*");
}
}
return handlerSet;
}
...
}