관리 메뉴

한다 공부

[Spring] Spring MVC 본문

Dev/Java

[Spring] Spring MVC

사과당근 2024. 4. 16. 13:52

스프링 MVC를 적용하기 전에, 태초에는 MODEL 1 아키텍처 구조와 MODEL 2 아키텍처 구조라는 것이 있었다.
 

~ Model 1 ~

model 1 아키텍처는 JSP와 JavaBeans만을 이용하는 웹 개발의 가장 단순한 구조이다.
Bean은 자바에서의 객체를 의미하며, JavaBeans는 DB연동에 사용되는 자바 객체들인 DAO와 VO 클래스를 의미한다.
여기서는 JSP가 Controller와 View의 기능을 모두 담당한다.
 

출처: 채규태 선생님

 
JSP는 HTML에 Java코드를 사용하게 할 수 있는 것이며, 자바코드는 <% %> 으로 감싸야한다.
이를 스크립틀릿 태그라고 한다.
스크립틀릿으로 자바 코드를 감싸지 않으면, 에러는 나지 않지만 자바 코드가 그냥 화면에 텍스트로 뿌려진다..
 
<%= %> 는 expression이라고 하는데, 안에서는 연산이 가능하기도 하며,
메소드를 호출해서 리턴된 값을 받아오기도 한다.
 
model 1을 사용하다 보면 유지 보수 과정에서 문제가 생긴다.
디자이너가 글 목록 화면을 만들고,
하드코딩 되어있는 임시 데이터를 개발자가 실제 DB에 있는 데이터로 바꾸었을 때,
화면의 수정사항이 생겼을 때는 누가 JSP를 수정해야 하는 걸까?
이 문제를 해결하기 위해 model 2가 생겼다.


~ Model 2 ~

model 2의 큰 특징은 Controller가 등장했다는 것이다.
여기서 Controller는 기존 JSP에 포함되어있던 자바 코드를 Servlet 클래스로 구현을 한다.
View는 JSP로 구현을 하고, Model은 DAO와 VO클래스로 구성되어 있다.


JSP에 있던 자바 코드들을 DispatcherServlet으로 옮겨서 분기처리를 한다면,
글 목록 검색 기능을 구현했을 때의 로직은 다음과 같다.

1. 클라이언트가 "/getBoardList.jsp" 요청을 보내면, DispatcherServlet가 요청을 받는다.
2. DispatcherServlet은 BoardDAO 객체를 통해 글 목록을 검색한다.
3. 검색된 글 목록을 HttpSession에 등록한다.
4. 이후 DispatcherServlet이 getBoardList.jsp 화면을 요청한다.
5. getBoardList.jsp는 세션에 저장된 글 목록을 꺼내서, 목록 화면을 구성한다.
6. 목록 화면이 응답으로 브라우저에 전송된다.
 
이때, 클라이언트가 getBoardList.jsp를 바로 호출한다면,
세션에 등록된 글 목록이 없어서 정상적으로 동작하지 않는다.
그렇기 때문에 .jsp가 아닌 .do를 호출해야 한다.
 

~ Forward & Redirect ~

페이지를 전환하는 데에는 크게 포워드 방식과 리다이렉트 방법이 있다.
만약 a.jsp에 대한 요청이 들어왔을 때, a.jsp로 이동 후 b.jsp로 이동하여 응답을 한다고 가정해보자.
 
포워드의 경우, 한 번의 요청과 한 번의 응답으로 클라이언트의 요청이 처리된다.
따라서 a.jsp를 호출하면 url에는 a.jsp가 뜨지만, 서버 안에서 b.jsp로 점프하여 실제로는 b.jsp를 응답한다.
요청과 응답이 한 번 뿐이기 때문에 빠르다는 장점이 있다.
하지만 b.jsp에서 오류가 나도, url에는 a.jsp가 명시되어있기 때문에 오류의 원인을 찾기 힘들다는 단점이 있다.
 
리다이렉트의 경우, 두 번의 요청과 두 번의 응답이 필요하다.
왜냐하면 a.jsp를 호출했을 때, 클라이언트는 a.jsp에 대한 응답을 받기 때문이다.
그리고 해당 응답을 바탕으로, 클라이언트는 b.jsp를 호출하라는 새로운 요청을 서버에 보내고, 서버는 b.jsp를 응답한다.
그래서 총 두 번의 요청과 두 번의 응답이 필요한 것이다.
리다이렉트는 요청과 응답이 많이 들어가므로 속도가 느리다는 단점이 있다.
 

~ EL/JSTL ~

JSP 코드에서 자바 코드를 없애기 위해 Model 2를 도입했지만,
실제로는 세션에 등록된 결과를 JSP가 꺼내오기 위한 session.getAttribute 등 일부 자바 코드가 남아있다.
이러한 자바 코드 조차 없애고 싶다면 EL과 JSTL을 이용하면 된다.
 
EL이란, Expression Language로
request, session, application 과 같은 내장 객체에 등록된 데이터에 접근하는 표현 언어를 뜻한다.
JSP에서는 ${} 와 같은 기호를 통해 사용할 수 있다.
실제로 EL이 JSP 에 삽입되면,
request에 해당 데이터가 있는지 확인한 뒤, session을 확인하고, application을 확인한다.
이후 데이터가 있으면 화면에 해당 데이터를 표시하고, 데이터가 없으면 보여지지 않는다.
 
JSTL이란, JSP Standard Tag Library로 jsp에서 if, for문과 같은 자바 코드를 태그 형식으로 이용할 수 있게 하는 것이다.
 
여담으로, EL은 라이브러리가 이미 톰캣에 포함되어 있으므로 별도 라이브러리가 필요하지 않고,
JSTL을 사용하기 위해서는 두 개의 jar파일이 필요하다. (jstl.jar와 standard.jar)


~ Spring MVC ~

앞선 모델들을 보면, MVC 아키텍처를 적용했지만 DispatcherServlet이 너무 복잡해진다는 단점이 있다.
왜냐하면 거의 모든 비즈니스 로직을 전부 DispatcherServlet에 넣었기 때문이다!
 
우리는 스프링에서 제공하는 DispatcherServlet을 사용하고자 할 것이며,
DispatcherServlet 하나로 모든 로직을 처리하는 것이 아니라, 여러 클래스로 분산시키고자 한다.
 
그 결과는 아래와 같다.

login시도를 한다고 가정해보자.
쉽게 설명하자면, DispatcherServlet는 login.do 요청을 받는 창구 역할이다.
그리고 XmlWebApplicationContext라는 스프링 컨테이터를 생성하는 역할을 한다.
이 때 메모리에 두개의 객체가 생성되는데, 바로 HandleMapping과 id가 login인 객체이다.
 
중요한 것은 HandlerMapping이다.
HandlerMapping이 Properties 타입의 컬렉션을 탐색한 뒤, 맞는 컨트롤러를 매핑한다.
(앞선 포스팅을 복습하자면, Properties는 Map과 비슷한 구조이며 숫자와 문자열을 저장할 수 있다.
Properties 타입의 컬렉션에는 id와 객체가 매핑되어 있는데,
login.do 요청이 들어올 경우 id가 login인 컨트롤러를 동작할 수 있도록 매핑한 것이다.)
 
이후 DispatcherServlet이 해당 컨트롤러를 실행하고, DAO에서 데이터를 select 한 뒤 리턴한다.
 
ModelAndView는 어떤 화면으로 갈지 세팅한 뒤 foward 하는 역할을 한다.


추가적으로, 검색 기능을 구현할 때 겸색 결과는 세션이 아닌 request에 저장하는 것이 좋다.
왜냐하면, request와 response는 요청이 들어올 때 생성되고,
브라우저로 응답 메세지가 전달되는 순간 사라지는 일회성이기 때문이다.
이러한 request 정보를 servlet에 전달하기 위해서는 HttpServletRequest 객체를 사용한다.
스프링에서 제공하는 ModelAndView 객체를 사용하면  HttpServletRequest 객체에 검색 결과를 저장할 수 있다.
 
세션에 검색 결과를 저장하면 좋지 않은 이유로는,
세션은 브라우저 하나 당 서버 메모리에 하나씩 생성되기 때문이다.
만약 검색 결과를 세션에 저장한다면, 메모리를 많이 차지하게 되어 속도가 느려진다.
세션에는 로그인에 성공한 id와, 권한(role) 정도만 저장하는 것이 좋다.
 

~ ViewResolver ~

위에서도 이야기 했지만, 사용자로 하여금 다이렉트로 jsp 파일을 요청하지 못하게 해야한다.
do를 호출에서 데이터에 접근하고 select 하고 jsp에 담아서 리턴을 해야 원하는 결과값이 보이지, 바로 jsp만 호출하면 뭐가 보일쏘냐
사용자는 jsp가 아닌 do를 호출해야한다.
ViewResolver를 사용하면 이처럼 직접적인 jsp 호출을 차단할 수 있다.
 
또한, web.xml과 같이 많은 정보가 있는 중요 파일은 숨겨야한다.
톰캣을 포함한 모든 서버는 /WEB-INF/ 폴더를 없는 취급 하기 때문에,
해당 폴더안에 있는 파일들은 절대 브라우저에서 접근할 수 없다.
따라서 해당 폴더에 web.xml을 담으면 된다.
직접적으로 호출하면 안되는 jsp와 같은 파일도 /WEB-INF/ 에 담으면 된다.
 
그리고 InternalResourceViewResolver를 사용하면
스프링 컨테이너는 WEB-INF 폴더에 있는 jsp 파일을 view 화면으로 사용할 수 있게 된다.
그리고 접두사에 /WEB-INF/ 를 붙여서 jsp 파일의 경로를 바꿀 수 있다.
접두사를 예외적으로 빼고 싶다면,
view의 이름 앞에 foward: 나 redirect: 를 붙여 해당 view에 대해 ViewResolver를 작동하지 않게 할 수 있다.


~ Annotation 기반 Spring MVC ~

이전 포스팅에서 IoC, AOP를 공부할 때, 어노테이션을 사용함으로써 xml 설정을 간소화 시킬 수 있었다.
이처럼 스프링 MVC 또한 어노테이션을 사용해 xml을 간소화 시킬 수 있다!
이를 위해, xml에 <context:component-scan> 엘리먼트를 추가하여 컴포넌트 스캔 대상을 지정해야한다.
 
더불어 어노테이션을 사용하면 컨트롤러가 가진
사용자 입력 정보 추출, DB 연동 처리, 화면 이동 코드를 단 한 줄로 변경할 수 있다. 
 
이전에 사용한 ModelAndView를 뜯어보면,
사용하지도 않는 request, response 매개변수를 필수로 받았어야 했고,
Controller를 implements 했어야했다.
이를 다 없애고 독립된 컨트롤러 객체를 만든 다음에, (POJO 스타일로!)
위에 @Controller를 붙이면서 컨트롤러 객체 생성하고 메모리에 띄울 수 있다.
 
그리고 @RequestMapping을 붙이면서 컨트롤러의 메소드를 실행시킬 수 있다.
@RequestMapping은 HandlerMapping을 대신한다.
 
커맨드 객체는, 컨트롤러에서 매개변수로 받은 VO 객체를 의미한다.
이처럼 컨트롤러에서 매개변수로 VO 객체를 받을 경우,
스프링 컨테이너가 사용자가 입력한 정보를 추출해주며,
VO 객체를 생성하기도 하며,  내부적으로 Setter 메소드를 실행하여 값을 설정해준다.
(즉, 컨테이너가 Setter Injection을 대신 해주는 것이다.)
 

~ JSP에서 Command 객체 사용하기 ~

이러쿵 저러쿵 설명이 길었지만, 어노테이션을 사용해서 xml 설정을 간소화 시킬 뿐만 아니라,
컨트롤러의 많은 코드를 싹 없앨 수 있다는 것이다 ~
 
그리고, 앞서 이야기 한 커맨드 객체를 JSP에서 가져다 쓸 수 있다.
컨트롤러에서 매개변수를 커맨드 객체로 받을 경우, 이를 EL 표현식을 사용하여 JSP를 사용할 수 있다.
만약 컨트롤러에서 받은 매개변수가 public String Controller (UserVO vo) 와 같이 VO 객체라면,
JSP에서는 ${userVO.id} 와 같은 표기법으로 해당 객체를 사용할 수 있는 것이다.
 
여기서, 스프링 컨테이너가 생각하는 커맨드 객체의 이름은, 클래스 이름의 첫 글자를 소문자로 변경한 이름이 된다.
그래서 UserVO 클래스를 매개변수로 받았을 경우, 커맨드 객체의 이름이 userVO가 되는 것이다.
 
하지만 JSP에서 ${userVO.id} 하면 가독성이 떨어진달라
${user.id} 처럼 사용하고 싶다면 어떻게 해야할까?
커맨드 객체의 이름을 별도로 지정해줘야한다.
그 방법으로는 @ModelAttribute를 사용하는 방법이 있다.
컨트롤러에서 public String Controller (@ModelAttribute("user") UserVO vo) { } 와 같이 지정한다면
JSP 에서 ${user.id} 로 데이터를 접근할 수 있다.


~ Listener ~

슬슬 뇌가 아파온다. 하지만 거의 다 왔다.
처음부터 생각해보면, 클라이언트가 요청을 했을 때 DispatcherServlet이 로딩되고 스프링 컨테이너가 생성된다
이 컨테이너는 컨트롤러만 메모리에 띄운다.
하지만, 컨트롤러는 DAO와 ServiceImpl을 참조해야한다.
아직 만들어지지 않은 DAO와 ServiceImpl을 어떻게 참조할까?
 
누군가 서블릿보다 먼저 xml을 로딩하여 필요한 객체를 생성해두면,
DAO와 Service Impl을 메모리에 있을 것이다.
그러면 컨트롤러는 DAO와 ServiceImpl을 참조할 수 있게 된다.
이 누군가가 누구일까?
바로 리스너이다.
 
리스너는 서버만 껐다 키면 무조건 pre-loading이 된다.
그래서 pre-loading으로 먼저 뜬 리스너가, DAO와 ServiceImpl을 띄워두면
컨테이너가 나중에 뜨더라고 참조할 수 있는 DAO와 ServiceImpl이 뜨기 때문에 문제가 생기지 않는다.
 
xml -> listener -> Spring Container (DAO, ServiceImple)
xml -> Servlet Container -> Spring Container (Controller)
 
즉, DAO와 ServiceImple이 담긴 스프링 컨테이너 하나와, Controller가 담긴 스프링 컨테이너 하나,
총 스프링 컨테이너가 2개 필요하다. (컨트롤러는 클래스에 불과하므로 new 2번 하면 됨)
그리고 Servlet은 Controller를 생성할 1개의 컨테이너가 필요하다.
 
어쨌든 web.xml에 ContextLoaderListener를 등록하면
서버 재구동 시 리스너는 pre-loading 되고, 비즈니스 컴포넌트 객체들이 생성되는 것을 확인할 수 있다.


~ 총정리 ~

서버를 구동했을 때 일어나는 상황에 대해 알아보자.

1. 서버를 처음 구동하면 wer.xml 파일을 로딩하여 서블릿 컨테이너가 구동된다.
2. 서블릿 컨테이너는 web.xml에 등록된 ContextLoaderListener 객체를 생성한다.
이때 ContextLoaderListener 는 pre-loading되어 business-layer.xml파일을 로딩하고,
해당 파일에 등록된 스프링 컨테이너를 구동한다. 이 컨테이너를 루트 컨테이너라고 한다.
이때, ServiceImple 과 DAO 객체가 메모리에 생성된다.
3. 사용자가 브라우저를 통해 .do 요청을 보내면,
서블릿 컨테이너는 lazy-loading되어 DispatcherServlet 객체를 생성한다.
DispatcherServlet 는 /WEB-INF/아래에 있는 presentation-layer.xml 파일을 로딩하여 두 번째 스프링 컨테이너를 구동한다.
그리고 두 번째 스프링 컨테이너가 Controller 객체를 메모리에 생성한다.
이 Controller 객체는 미리 메모리에 생성된 ServiceImpl 과 DAO 객체를 참조하여 비즈니스 로직을 수행한다.