-
컨트롤러(핸들러)가 조회되는 과정개발/Spring 2023. 6. 11. 13:20
스프링에서 컨트롤러가 호출되는 방법은 언뜻 보면 간단해 보인다.
개발자는 RequestMapping으로 url 맵핑만 해주면 당연한 듯 동작하니까.
MVC 내부 구조는 꽤 복잡하게 되어 있는데, 유연하게 동작하기 위한 구조를 가진다.
구조적 고민 없이 서블릿만으로 간단하게 컨트롤러를 만들 수는 있다.
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") public class MvcMemberSaveServlet extends HttpServlet { private MemberRepository memberRepository = MemberRepository.getInstance(); @Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String username = req.getParameter("username"); int age = Integer.parseInt(req.getParameter("age")); Member member = new Member(username, age); memberRepository.save(member); req.setAttribute("member", member); String viewPath = "/WEB-INF/views/save-result.jsp"; RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath); dispatcher.forward(req, res); } }
중복되는 코드가 많고 구조화가 되어 있지 않아 컨트롤러가 많아질수록 유지보수가 어려워질 것이다.
예를 들어 이런 컨트롤러가 100개가 있을 때, view의 폴더 구조가 바뀌어야 하는 상황이 생긴다면
수정 과정에서 휴먼 에러가 생길 소지가 높다. 시간도 오래 걸리므로 여러모로 비효율적이다.
각 컨트롤러를 서블릿으로 만들지 않고(서블릿 종속성 제거)
모델과 뷰를 분리하면 다음과 같은 구조가 나올 수 있다.
@Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String uri = req.getRequestURI(); ControllerV3 controller = controllerMap.get(uri); if (null == controller) { res.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } Map<String, String> paramMap = createParamMap(req); ModelView modelView = controller.process(paramMap); String viewName = modelView.getViewName(); MyView view = viewResolver(viewName); view.render(modelView.getModel(), req, res); }
서블릿으로 만든 프론트 컨틀로러를 하나 두고 실제 호출되어야 하는 적절한 컨트롤러를 찾아 호출한다.
각 컨트롤러는 ControllerV3 인터페이스의 구현체다.
process는 저장소에 데이터를 저장하고 ModelView를 반환한다.
Model은 맵으로 데이터를 저장한 자료구조이며 View는 jsp 파일의 논리 경로를 가지고 있다(e.g. save).
논리 경로는 viewResolver를 통해 실제 물리 경로가 된다. (e.g. /WEB-INF/views/save.jsp)
여기서 컨트롤러는 다른 방식으로 구현될 수도 있을 것이다.
ModelView 클래스를 사용하지 않고 바로 viewName을 문자열로 반환하게 하고
model은 외부에서 주입시키는 방법을 생각해볼 수 있다.
@Override public String process(Map<String, String> paramMap, Map<String, Object> model) { String username = paramMap.get("username"); int age = Integer.parseInt(paramMap.get("age")); Member member = new Member(username, age); memberRepository.save(member); model.put("member", member); return "save-result"; }
스프링은 한 가지 방법만 강제하지 않고 상황에 따라 개발자가 선택할 수 있는 다양한 방법을 제공한다.
Bean을 등록할 때 XML을 이용할 수 있고, 애노테이션을 이용한다면 @Bean을 사용해 직접 등록할 수도 있다.
@Component 사용해 컴포넌트 스캔을 통해 자동으로 등록할 수 있다.
여기서도 여러 컨트롤러를 사용할 수 있도록 어댑터 패턴을 사용해보자.
컨트롤러 대신에 핸들러라는 용어를 사용할 건데 스프링에서도 그렇게 사용되고 있다.
url에 맵핑된 핸들러를 찾은 뒤, 핸들러를 직접 호출하지 않고, 이 핸들러를 실행해 줄 수 있는 어댑터를 조회한다.
맞는 어댑터가 있으면 해당 어댑터의 특정 함수로 핸들러를 넘길 것이다.
어댑터 패턴을 통해 이 문제를 해결하는 순서는 다음과 같다.
1. 맵핑된 핸들러 중에서 알맞는 핸들러 조회
2. 핸들러를 통해 알맞는 핸들러 어댑터 조회
3. 핸들러 어댑터의 handle 함수를 실행
구조가 조금 복잡해지는 이유는 다음 코드를 보고 알아보자.
@Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { Object handler = getHandler(req); if (null == handler) { res.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } MyHandlerAdapter handlerAdapter = getHandlerAdapter(handler); ModelView modelView = handlerAdapter.handle(req, res, handler); String viewName = modelView.getViewName(); MyView view = viewResolver(viewName); view.render(modelView.getModel(), req, res); }
최상단에서 유지되어야 하는 코드다.
위에서 언급했듯 어떤 컨트롤러는 modelView를 반환하지만 어떤 컨트롤러는 viewName을 문자열로 반환한다.
중간에서 중재해주지 않으면 viewName 문자열을 반환하는 컨트롤러는 영원히 사용할 수 없을 것이다.
modelView를 반환하지 않는 모든 컨트롤러는 사용할 수 없게 된다.
하지만 인터페이스를 하나 더 두고 handle 함수를 구현하도록 해보자.
@Override public ModelView handle(HttpServletRequest req, HttpServletResponse res, Object handler) throws ServletException, IOException { ControllerV4 controller = (ControllerV4) handler; Map<String, String> paramMap = createParamMap(req); HashMap<String, Object> model = new HashMap<>(); String viewName = controller.process(paramMap, model); ModelView modelView = new ModelView(viewName); modelView.setModel(model); return modelView; }
handle 함수를 구현할 때, viewName을 통해 modelView 객체를 만들면 문제가 해결된다.
복잡한 일련의 과정들을 인터페이스를 이용해 추상화 수준을 한 단계 더 높임으로서 단순화했다.
미치오 카쿠가 하이퍼스페이스라는 책에서,
입자 물리학 이론들을 통합할 때 차원을 높이면 간단하게 해결된다고 자주 언급한다.
handle이라는 인터페이스로 중개하는 만큼 추상화 수준이 올라가고 코드가 복잡해지는 단점이 있지만
세부적인 과정이 어떻든 간에 ModelView를 반환하도록 함으로써 세부적인 문제들을 해결할 수 있다.
'개발 > Spring' 카테고리의 다른 글
세션 인증 때 커스텀 애노테이션 적용하기 (0) 2023.06.26 Validation에서 에러 문구 설정 (0) 2023.06.20 빈 스코프(Singleton, Prototype) (1) 2023.05.16 @Configuration (0) 2023.05.10 [AOP] before의 우선순위 (0) 2023.04.30