일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- java enum
- fcm성능비교
- 스프링요청반응
- 옵티마이저
- 불변객체
- DispatcherServlet
- 중첩클래스
- multipart바인딩
- 래퍼클래스
- 왜불변객체인가
- fcm데이터구조
- httpservlet기술
- HTTP프로토콜
- 옵티마저
- 데이터베이스파서
- 디스패처서블릿
- rest api
- fcmv1
- biblecash
- Wrapper class
- 공유기작동방식
- multiparfile데이터
- equals
- rest api 검증
- HttpServlet
- 프로세스 생성
- 동등성동일성
- 동기비동기블로킹논블로킹
- 검증 실패 예외처리
- 클라이언트요청반응
- Today
- Total
개발은 아름다워
[ Spring ] 고대의 서블릿을 찾아서(8) - 어댑터패턴과 트레이드 오프 본문
서블릿 -> 템플릿 엔진 -> MVC 패턴 -> FrontController 까지 왔다. 특히 FrontController를 직접 만들면서 HTTP 요청을 처리할 수 있는 다양한 버전의 Controller를 만들었다. 그렇다면 하나의 FrontController에서 다양한 버전의 Controller 들을 사용할 수 없을까?
어댑터 패턴 등장
다양한 버전의 Controller들은 반환 값이 다 다르다. 반환 값이 다 다르면 FrontController에서는 Controller 버전에 따라 로직을 따로 작성해야하는 번거로움이 생긴다. 따라서 Controller가 반환하는 값을 공통으로 만들어 줄 무언가가 필요한데 그것이 바로 어댑터이다. 아이폰을 충전하든, 맥북을 충전하든 결국 220v 소위 말하는 돼지코가 어댑터랑 연결되어야 충전기가 전기를 받아서 아이폰이든 맥북을 충전할 수 있는 것이다. 돼지코라는 규격이 FrontController에서 공통으로 처리할 수 있는 규격이라고 할 수 있고, 아이폰,맥북에 맞는 충전기 자체를 어댑터라고 할 수 있다.
어댑터 패턴을 추가한 FrontController
@WebServlet(name = "frontControllerServletV5",urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet{
private final Map<String,Object> handelMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5(){
initHandlerMappingMap();
initHadlerAdpters();
}
private void initHandlerMappingMap(){
// v2 컨트롤러를 handleMappingMap에 추가
handelMappingMap.put("/front-controller/v5/v2/members/new-form",new MemberFormControllerV2());
handelMappingMap.put("/front-controller/v5/v2/members/save",new MemberSaveControllerV2());
handelMappingMap.put("/front-controller/v5/v2/members", new MemberListControllerV2());
// v3 컨트롤러를 handleMappingMap에 추가
handelMappingMap.put("/front-controller/v5/v3/members/new-form",new MemberFormControllerV3());
handelMappingMap.put("/front-controller/v5/v3/members/save",new MemberSaveControllerV3());
handelMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
// v4 컨트롤러들을 handleMappingMap에 추가
handelMappingMap.put("/front-controller/v5/v4/members/new-form",new MemberFormControllerV4());
handelMappingMap.put("/front-controller/v5/v4/members/save",new MemberSaveControllerV4());
handelMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHadlerAdpters(){
handlerAdapters.add(new ControllerV2HandlerAdapter());
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV2.service");
// 1. 요청에 맞는 handler 찾기
Object handler = getHandler(request);
if(handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 2. 컨트롤러(handler)을 실행시킬 수 있는 어댑터 가져오기
MyHandlerAdapter adapter = getHandlerAdapter(handler);
// 3. 어댑터 handle을 실행하면 컨트롤러 process가 실행되고,
// 컨트롤러 실행 후 데이터와 viewName이 담긴ModelView 객체를 반환
ModelView mv = adapter.hadnle(request, response, handler);
// 4. viewResolver로 논리이름의 viewName을 물리이름의 viewPath로 바꾼 후 viewPath가 담긴 MyView객체 반환
MyView view = viewResolver(mv.getViewName());
// 5. jsp 서블릿에 데이터를 포워딩함.
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handelMappingMap.get(requestURI); // key로 handleMappingMap에 컨트롤러 가져옴
}
private MyHandlerAdapter getHandlerAdapter(Object handler){
for(MyHandlerAdapter adapter:handlerAdapters){
if(adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/"+viewName+".jsp");
}
}
- handlerMappingMAp
FrontController의 handlerMappingMap에 키에는 요청 URL, 값에은 요청 URL에 해당하는 Controller가 저장된다. '요청 URL이 왔을때 바로 컨트롤러를 실행시키면 되지 않을까?' 하는 생각이 들 수 있다. 그러나 그렇게 되면 FrontController에는 Controller 버전에 해당하는 로직들을 추가해야한다. 그렇게 되면 코드가 매우 복잡해질 것이다. 따라서 어댑터라는 것을 만들어서 어댑터가 controller를 실행시키도록 만들면 되는 것이다.
2.handlerAdapters
private void initHadlerAdpters(){
handlerAdapters.add(new ControllerV2HandlerAdapter());
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
각 버전의 맞는 controller를 실행시킬 수 있는 adpter들이 들어있다. getHandlerAdapter를 통해서 어댑터들 중에 controller를 실행시킬 수 있는 adapter를 찾을 수 있다.
어댑터들은 어떻게 만들면 될까? controller 실행 후 반환 값이 다르지만, 어댑터안에서 controller가 실행 후에는 동일한 반환 값이 되도록 만들어야한다. 따라서 adapter의 handle에서 controller를 실행 후 반환 값이 동일하도록 추상화 시켜야한다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView hadnle(HttpServletRequest request,
HttpServletResponse response,Object handler) throws ServletException,IOException;
}
public class ControllerV2HandlerAdapter implements MyHandlerAdapter{
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV2);
}
@Override
public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException {
ControllerV2 controller = (ControllerV2) handler;
MyView view = controller.process(request,response);
String viewPath = view.getViewPath();
// 시작 인덱스
int startIndex = viewPath.lastIndexOf("/") + 1;
int endIndex = viewPath.lastIndexOf(".");
// 논리 부분만 추출
String viewName = viewPath.substring(startIndex, endIndex);
ModelView mv = new ModelView(viewName);
return mv;
}
}
public class ControllerV3HandlerAdapter implements MyHandlerAdapter{
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String,String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String,String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
public class ControllerV4HandlerAdapter implements MyHandlerAdapter{
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String,String> paramMap = createParamMap(request);
Map<String,Object> model = new HashMap<>();
String viewName = controller.process(paramMap,model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String,String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
어댑터 패턴이 무조건 좋을까?? 여기서 중요한 점은 트레이드 오프가 발생한다는 것이다.
어디서 트레이드오프가 발생 할 수 있을까? 바로 ControllerV2 이다.
public interface ControllerV2 {
MyView process(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException;
}
ControllerV2의 반환 타입은 MyView이며 viewPath 물리 경로를 MyView 객체안에 넣어서 반환한다. 하지만 adapter의 반환 타입은 ModelView로 통일하였다. 따라서 어댑터 안에서 controller를 실행 후 viewPath에서 논리 이름인 viewName을 추출하는 코드 부분을 추가해야 한다.
public class ControllerV2HandlerAdapter implements MyHandlerAdapter{
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV2);
}
@Override
public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException {
ControllerV2 controller = (ControllerV2) handler;
MyView view = controller.process(request,response);
String viewPath = view.getViewPath();
// 시작 인덱스
int startIndex = viewPath.lastIndexOf("/") + 1;
int endIndex = viewPath.lastIndexOf(".");
// 논리 부분만 추출
String viewName = viewPath.substring(startIndex, endIndex);
ModelView mv = new ModelView(viewName);
return mv;
}
}
어댑터 패턴을 써서 여러 버전의 contoller를 쓸 수 있다는 장점이 있지만 controllerV2와 같이 viewName을 추출해야하는 추가해야 한다면 오히려 비용이 증가가 될 수 있다. 따라서 어댑터패턴을 위해 contollerV2 추가 시 비용이 더 들것이 예상된다면 controllerV2를 굳이 추가하지 않을 것이다.
정리
고대의 서블릿부터 frontController 어댑터패턴까지 공부하면서 어떻게 불편함을 개선했는지 알게되었다. 특히 이전에는 불편함만 개선하는게 목적이였다면, 이번에는 추가하는게 나은 선택인지 아닌지 생각해볼 시간이 되었다. 모든 것에는 트레이드 오프가 있다. 트레이드 오프를 생각하며 변경이나 추가가 맞을지, 아니면 그대로 두는게 맞을지 등을 생각해볼 수 있는 시간이였다.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/
'스프링' 카테고리의 다른 글
[ Spring ] HTTP API를 가능하게 해주는 메세지 컨버터 (0) | 2024.10.14 |
---|---|
[ Spring ] 스프링에서 응답 데이터 만드는 방식 (0) | 2024.10.14 |
[ Spring ] 고대의 서블릿을 찾아서(7) - ModelView를 없애고 더욱 단순하게 (0) | 2024.10.14 |
[ Spring ] 고대의 서블릿을 찾아서(6) - ModelView의 등판 (1) | 2024.10.14 |
[ Spring ] 고대의 서블릿을 찾아서(5) FrontController에 View 추가 (0) | 2024.10.14 |