[스프링 MVC 1편] 4 - (5) 프론트 컨트롤러 - v5
* 스프링 입문 = window, 스프링 MVC 1편 = Mac 으로 진행합니다
* 진도 : 섹션4 - (6) ~ (7)
* : 자바 클래스명, : 코드, : 단축키
1. 프론트 컨트롤러_구현 5단계 (V3 지원)
- Controller를 v1 ~ v4 까지 만들어보았는데 컨트롤러를 섞어서 다양하게 써보자
지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다
예로 ControllerV3 , ControllerV4 는 다른 인터페이스라 호환이 불가능하다어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리하자
- 어댑터 패턴
서로 다른 인터페이스를 사용할 수 있도록 바꿔줌으로써 기존 코드를 재사용 - 핸들러란
이벤트가 발생했을 때, 해당 이벤트에 맞는 동작을 수행하는자 라고 생각하면 된다.
즉, 해당 URI에 맞는 동작을 수행하는 자 = 컨트롤러 라고 생각하면된다. 컨트롤러의 상위 개념이 핸들러이다 - 핸들러 어댑터
핸들러 매핑에서 리턴받은 핸들러 객체를 가지고 이에 맞는 어댑터를 찾는 과정
핸들러 어댑터는 컨트롤러에서 String으로 응답받든, Model로 응답받든, 무조건 Dispatcher Servlet에서 ModelAndView객체로 응답을 해줘야한다
1) 어댑터용 인터페이스
[ src - java - hello.servlet - web - frontcontroller .v5 - MyHandlerAdapter ]
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException;
}
boolean supports (Object handler)
- 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
- object는 객체 생성 중의 가장 상위 개념(모든 것 포함가능)
ModelView handle
- 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환
(반환하지 못하면 어댑터가 ModelView를 직접 생성해서라도 반환) - 어댑터를 통해서 실제 컨트롤러 호출 (이전에는 프론트 컨트롤러가 컨트롤러를 호출)
< 기존의 ControllerV3 인터페이스 코드 >
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
< 기존의 ControllerV4 인터페이스 코드 >
public interface ControllerV4 {
String process(Map<String, String> paramMap,Map<String, Object> model);
}
2) V3 지원하는 어댑터
[ src - java - hello .servlet - web - frontcontroller .v5 - adapter - ControllerV3HandlerAdapter ]
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private static 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;
}
}
- 상속 implements MyHandlerAdapter (option + enter)
- boolean supports / ModelView handle 2개가 구현된다
- boolean supports / ModelView handle 2개가 구현된다
- 핸들러 참 or 거짓 boolean supports~return(handler instanceof ControllerV3)
- ControllerV3가 넘어오면 참을 반환
- ControllerV3가 넘어오면 참을 반환
- 핸들러 처리 ModelView handle~(handler instanceof ControllerV3)
- ControllerV3 controller = ~ handler
handler를 컨트롤러 V3로 변환한다
- 파라미터네임 꺼내기 (그냥 FrontControllerV3 복사해도 된다)
- parammap 맵 생성
- request.getParameterNames() ~ 파라미터 다 꺼내기 : iterator 인터페이스형으로 바꿔서 반환
- key = paramName, value = request.getParameter(paramName)
★★ 너무 길어서 따로 메서드로 뽑는게 좋다!
paramMap부터 request.getParameterName 까지 드래그 후
option + command + m하고 parammapcreateParamMap 입력하기 ★★
- ControllerV3 controller = ~ handler
- 모델뷰 반환 controller.process(paramMap) (option + command + v
3) 프론트 컨트롤러
[ src - java - hello.servlet - web - frontcontroller. v5 - FrontControllerServletV5 ]
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if(handler==null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("adapter 를 찾을 수 없습니다. handler=" +handler);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
}
- 프론트 컨트롤러는 꼭 서블릿으로 작성한다!
1. 상속 extends HttpServlet
2. 서블릿 생성@WebServlet
3. 핸들러 매핑 맵 생성 private final Map <Stirng,Object>
- key = url, value = Object (어떠한 컨트롤러도 들어갈 수 있음)
4. 리스트 만들기 List<MyHandlerAdapter> ~ new ArrayList<>()
- 어댑터가 여러개 담겨 있기 때문에 만들어줌
5. 매핑 정보 public FrontControllerServiceV5()
- command + n 하고 constructor 클릭 → Select None 클릭
- 매핑정보를 담기!
- key 값이 요청되면 value를 실행하게끔 설정
★★ 너무 길어서 따로 메서드로 뽑는게 좋다!
경로 드래그 후 option + command + m하고 initHandlerMappingMap 입력하기 ★★
6. 리스트에 어댑터 클래스 넣기 handlerAdapters.add()
- ★★ 얘도 메서드로 따로 뽑기!
드래그 후 option + command + m하고 initHandlerAdapters 입력하기 ★★
7. 서비스 생성 protected service
8. URI
- 주소 가져오기 request.getRequestURI (option + command + v)
- 맵에서 URI 찾아 컨트롤러 반환 handlerMappingMap.get(requestURI)(option + command + v)
- ★★두 줄 드래그하고 option + command + m 해서 getHandler로 메서드로 따로 뽑기!★★
- 새로 생긴 메서드에서 반환 타입 변경
- private void getHandler →private Object getHandler 변경
- Object Handler = handlerMappingMap.get() → return handlerMappingMap.get() 변경
- 예외처리는 404 출력 SC_NOT_FOUND
9. 어댑터 for문 작성 handlerAdapters.iter (enter)
- ★★드래그, option + command + m 해서 getHandlerAdapter로 메서드로 따로 뽑기!★★
- MyHandlerAdapter adapter = getHandlerAdapter 로 변경
이때 MyHandlerAdapter는 앞서 만든 인터페이스다 - 새로 생긴 메서드에서 반환 타입 변경
- private void →private MyHandlerAdapter 변경
- a = adapter → return adapter 변경
- 새로 생긴 메서드에 예외 코드 추가
- throw new IllegalArgumentException ~
10. 모델 뷰 반환 ModelView mv = adpater handle ~
11. 뷰 설정 (프론트컨트롤러v3 랑 같음)
- new MyView("/WEB-INF/views/" + viewName + ".jsp") (option + command + v) 하고
★★ option + command + m 해서 viewResolver(viewName) 입력하기 ★★- 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 실제 물리 경로가 있는 MyView 객체를 반환 논리 뷰 이름: members
물리 뷰 경로: /WEB-INF/views/members.jsp
- 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 실제 물리 경로가 있는 MyView 객체를 반환 논리 뷰 이름: members
- 렌더 호출 view.render(model, request, respontse)
< 기존의 FrontControllerService4 클래스에서 수정한 코드 >
- 3번 과정 Map 형식이 ControllerV4 에서 Object로 바뀜
- 5번 과정 경로 /v5/v3 으로 변경
- 8번 과정 controllerMap에서 가져오던거 이제는 handlerMappingMap에서 가져오고 Object로 변경
- Map paramMap / model 2개 다 삭제
- String ViewName ~ 삭제
- createParamMap 메서드 삭제
@WebServlet(name = "frontControllerServiceV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServiceV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServiceV4() {
controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if(controller==null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private static 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;
}
}
< 실행 과정 >
1. 클라이언트 요청이 오면 프론트 컨트롤러가 핸들러 매핑 정보 뒤지기 (5번 과정)
2. 찾아와서 핸들러를 처리할 수 있는 핸들러 어댑터 찾기 (9번 과정)
3. 핸들 어댑터(=ControllerV3HandlerAdapter)를 호출 (9번 과정)
4. 핸들 호출 (ControllerV3HandlerAdapter 클래스 실행)
5. 모델뷰 반환 (10번 과정)
6. viewResolver 호출 (11번 과정)
7. MyView 반환 (11번 과정)
8. 렌더링 (11번 과정)
2. 프론트 컨트롤러_구현 5단계 (V4 지원)
1) V4 지원하는 어댑터
[ src - java - hello .servlet - web - frontcontroller .v5 - adapter - ControllerV3HandlerAdapter ]
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return(handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private static 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;
}
}
- 상속 implements MyHandlerAdapter (option + enter)
- boolean supports / ModelView handle 2개가 구현된다
- boolean supports / ModelView handle 2개가 구현된다
- 핸들러 참 or 거짓 boolean supports~return(handler instanceof ControllerV4)
- ControllerV4가 넘어오면 참을 반환
- ControllerV4가 넘어오면 참을 반환
- 핸들러 처리 ModelView handle~(handler instanceof ControllerV4)
- ControllerV4 controller = ~ handler
handler를 컨트롤러 V3로 변환한다
- 파라미터네임 꺼내기 (그냥 FrontControllerV4 복사해도 된다)
- parammap 맵 생성
- request.getParameterNames() ~ 파라미터 다 꺼내기 : iterator 인터페이스형으로 바꿔서 반환
- key = paramName, value = request.getParameter(paramName)
★★ 너무 길어서 따로 메서드로 뽑는게 좋다!
paramMap부터 request.getParameterName 까지 드래그 후
option + command + m하고 parammapcreateParamMap 입력하기 ★★
- ControllerV4 controller = ~ handler
- 인터페이스, 회원 등록/저장/목록 컨트롤러에서 사용한 맵 2개 등록
- Map<String, String> paraMap = new HashMap<>()
- HashMap<String, Object> model = new HashMap<>()
- Map<String, String> paraMap = new HashMap<>()
- 파라미터 받기 controller.process(paramMap) (option + command + v)
- 모델 뷰 생성 new ModelView(viewName) (option + command + v)
< 기존의 ControllerV3HandlerAdapter 클래스 코드 >
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private static 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;
}
}
2) 프론트 컨트롤러
[ src - java - hello.servlet - web - frontcontroller. v5 - FrontControllerServletV5 ]
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
//V4 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
//처리할 어댑터
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if(handler==null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("adapter 를 찾을 수 없습니다. handler=" +handler);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
}
- 프론트 컨트롤러는 꼭 서블릿으로 작성한다!
1. 상속 extends HttpServlet
2. 서블릿 생성@WebServlet
3. 핸들러 매핑 맵 생성 private final Map <Stirng,Object>
- key = url, value = Object (어떠한 컨트롤러도 들어갈 수 있음)
4. 리스트 만들기 List<MyHandlerAdapter> ~ new ArrayList<>()
- 어댑터가 여러개 담겨 있기 때문에 만들어줌
5. 매핑 정보 public FrontControllerServiceV5()
- command + n 하고 constructor 클릭 → Select None 클릭
- 매핑정보를 담기!
- key 값이 요청되면 value를 실행하게끔 설정
★★ 너무 길어서 따로 메서드로 뽑는게 좋다!
경로 드래그 후 option + command + m하고 initHandlerMappingMap 입력하기 ★★
6. 리스트에 어댑터 클래스 넣기 handlerAdapters.add()
- ★★ 얘도 메서드로 따로 뽑기!
드래그 후 option + command + m하고 initHandlerAdapters 입력하기 ★★
7. 서비스 생성 protected service
8. URI
- 주소 가져오기 request.getRequestURI (option + command + v)
- 맵에서 URI 찾아 컨트롤러 반환 handlerMappingMap.get(requestURI)(option + command + v)
- ★★두 줄 드래그하고 option + command + m 해서 getHandler로 메서드로 따로 뽑기!★★
- 새로 생긴 메서드에서 반환 타입 변경
- private void getHandler →private Object getHandler 변경
- Object Handler = handlerMappingMap.get() → return handlerMappingMap.get() 변경
- 예외처리는 404 출력 SC_NOT_FOUND
9. 어댑터 for문 작성 handlerAdapters.iter (enter)
- ★★드래그, option + command + m 해서 getHandlerAdapter로 메서드로 따로 뽑기!★★
- MyHandlerAdapter adapter = getHandlerAdapter 로 변경
이때 MyHandlerAdapter는 앞서 만든 인터페이스다 - 새로 생긴 메서드에서 반환 타입 변경
- private void →private MyHandlerAdapter 변경
- a = adapter → return adapter 변경
- 새로 생긴 메서드에 예외 코드 추가
- throw new IllegalArgumentException ~
- throw new IllegalArgumentException ~
10. 모델 뷰 반환 ModelView mv = adpater handle ~
11. 뷰 설정 (프론트컨트롤러v3 랑 같음)
- new MyView("/WEB-INF/views/" + viewName + ".jsp") (option + command + v) 하고
★★ option + command + m 해서 viewResolver(viewName) 입력하기 ★★- 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 실제 물리 경로가 있는 MyView 객체를 반환 논리 뷰 이름: members
물리 뷰 경로: /WEB-INF/views/members.jsp
- 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 실제 물리 경로가 있는 MyView 객체를 반환 논리 뷰 이름: members
- 렌더 호출 view.render(model, request, respontse)
< 기존의 FrontControllerService5(V3버전) 클래스 코드 >
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if(handler==null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("adapter 를 찾을 수 없습니다. handler=" +handler);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
}
< V4버전 실행 과정 >

1. 클라이언트 요청: http://localhost:8080/front-controller/v5/v4/members/new-form
2. [FrontControllerServletV5] - initHandleMappingMap (5번 과정)
매핑정보 찾아서 MemberFormControllerV4 반환
3. [FrontControllerServletV5] - MyHandlerAdapter adater = getHandlerAdapter(handler) (9번 과정)
MemberFormControllerV4핸들러를 처리할 수 있는 핸들러 어댑터 찾기
4. [FrontControllerServletV5] - initHandlerAdapters (6번 과정)
핸들러 어댑터 목록보기
list에는 ControllerV3HandlerAdapter, ControllerV4HandlerAdapter 가 들어가 있다
5. [FrontControllerServletV5] - MyHandlerAdapter adater - adapter.supports (9번 과정)
[ControllerV3HandlerAdapter/ControllerV4HandlerAdapter] - public boolean supports
어댑터들의 boolean supports 메서드 호출하고 해당하는 어댑터를 반환
- ControllerV3HandlerAdapter에서는 instanceof ControllerV3이라 false
- ControllerV4HandlerAdapter에서는 instanceof ControllerV4라 반환 ok
6. [FrontControllerServletV5] - ModelView mv = adapter.handle (10번 과정)
[ControllerV4HandlerAdapter] - public ModelView handle
해당 어댑터의 핸들 메서드 호출
7. [ControllerV4HandlerAdapter] - public ModelView handle
ControllerV4로 캐스팅하고 이에 맞춰서 paramMap, model 맵 2개를 넘겨 viewName 반환
viewName를 이용해 ModelView mv를 만들고 모델뷰 반환한다
8. [FrontControllerServletV5] viewResolver 호출 (11번 과정)
9. [FrontControllerServletV5] MyView 반환 (11번 과정)
10. [FrontControllerServletV5] 렌더링 (11번 과정)
[출처] 김영한 강사님 인프런 스프링 mvc1
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원
www.inflearn.com