앞에서는 클라이언트의 요청 처리를 서블릿 홀로 담당하는 All-in-One 방식에 대해 살펴보았다.
All-in-One 방식은 규모가 크거나 업무 변경이 많은 경우에는 유지보수가 어려워 운영 비용이 증가하게 된다. 시스템 변경이 잦은 상황에서 유지보수를 보다 쉽게하려면, 중복 코드의 작성을 최소화하고, 코드 변경이 쉬워야 한다. 이를 위해서는 객체 지향의 특성을 활용하여 좀 더 역할을 세분화하고 역할 간 의존성을 최소화하여야 한다. 그리하여 제안된 것이 MVC(Model-View-Controller) 아키텍처이다.
MVC 구조에서는 클라이언트의 요청 처리를 서블릿 혼자서 담당하지 않고 세 개의 컴포넌트가 나누어 처리한다.
컨트롤러 컴포넌트의 역할은, 클라이언트의 요청을 받았을 때 그 요청에 대해 실제 업무를 수행하는 모델 컴포넌트를 호출하거나, 클라이언트가 보낸 데이터가 있다면 모델을 호출할 때 전달하기 쉽게 데이터를 가공하는 일을 한다. 그리고 모델이 업무 수행을 완료하면 그 결과를 가지고 화면을 생성하도록 뷰에게 전달한다.
모델 컴포넌트의 역할은 데이터 저장소와 연동하여 사용자가 입력한 데이터나 사용자에게 출력할 데이터를 다루는 일을 한다. 특히 여러 개의 데이터 변경 작업(추가, 변경, 삭제)을 하나의 작업으로 묶은 트랜잭션을 다루는 일도 한다.
뷰 컴포넌트의 역할은 모델이 처리한 데이터나 그 작업 결과를 가지고 사용자에게 출력할 화면을 만든다.
이렇게 역할을 나누어 작업을 처리하는 방식은 객체지향 프로그래밍의 특징이다. 가능한 역할을 작은 단위로 나누면 다른 프로젝트에서 재사용할 가능성이 높아진다. (모델 컴포넌트가 작업한 결과를 다양한 뷰 컴포넌트를 통하여 출력이 가능하다.) 그리고 자바 개발자는 컨트롤러와 모델 개발에 전념하고, JSP 개발자나 HTML 개발자는 뷰 개발에만 전념할 수 있기 때문에, 업무집중도가 높아져서 개발 속도가 빨라진다.
우선 뷰 컴포넌트를 살펴보면,
기존에 서블릿이 하던 작업 중에서 화면 출력과 관련된 기능은 JSP 를 사용하여 처리한다. JSP 엔진이 자바 출력문을 만들기 때문에 개발자가 자바로 출력문을 작성할 필요가 없이, 보다 쉽게 HTML 화면을 만들고 클라이언트로 출력한다.
JSP 라이프 사이클
1. JSP URL 호출(서블릿 컨테이너는 JSP 파일에 대응하는 자바 서블릿을 찾아서 실행)
2. (만약 JSP에 대응하는 서블릿이 없거나 JSP 파일이 변경되었다면,) JSP 엔진을 통해 JSP 파일(.jsp)을 해석하여 서블릿 자바 소스(.java)를 생성
3. 서블릿 자바 소스는 자바 컴파일러를 통해 서블릿 클래스 파일(.class)로 컴파일
4. 클래스 로드
5. 인스턴스 생성
6. jspInit 메서드 호출
7. _jspService 메서드 호출
8. jspDestory 메서드 호출
따라서, 해당 JSP 페이지가 이미 컴파일되어 있고, 클래스가 로드되어 있고, JSP 파일이 변경되지 않았다면, 가장 많은 시간이 소요되는 2~4 프로세스는 생략된다.
추가적으로, 서버의 종류에 따라서 서버가 기동될 때 컴파일을 미리 수행하는 Pre-compile 옵션이 있다.
(서버를 기동할 때마다 컴파일로 인한 응답 지연현상이 발생하므로, 개발 시에는 개발생산성이 낮아질 수 있다.)
JSP에 대해서 좀더 자세히 알아보면,
JSP 페이지는 출력문을 표현하는 템플릿 데이터와 지시자 <%@ %>, 선언문 <%! %>, 스크립트릿 <% %>, 표현식 <%= %> 등으로 구성된다. JSP 페이지는 그 자체로 실행되지 않고 서블릿을 생성하기 위한 소스로 사용된다. JSP 엔진은 JSP 파일을 참고하여 서블릿을 생성한다.
(JSP 엔진은 JSP 파일로부터 서블릿 클래스를 생성할 때 HttpJspPage 인터페이스를 구현한 클래스를 만든다. 톰캣 서버의 JSP 엔진은 서블릿 소스를 생성할 때 슈퍼 클래스로서 HttpJspBase를 사용하는데, HttpJspBase는 HttpServlet을 상속받았기 때문에 이 클래스의 자식 클래스도 당연히 서블릿 클래스가 된다.)
서블릿 파일을 만들 때 템플릿 데이터(HTML, 자바스크립트, 스타일시트, JSON 형식문자열, XML, 일반 텍스트 등)는 자바 출력문으로 바뀌고, 기타 JSP 전용 태그는 특정 자바 코드로 바뀐다.
1. [JSP 전용 태그] 지시자
- <%@ 지시자 속성="값" 속성="값" ...%>
- page :JSP 페이지와 관련된 속성을 정의할 떄 사용하는 태그
- language 속성
- contentType 속성
- pageEncoding 속성
- taglib
-include2. [JSP 전용 태그] 스크립트릿
- <% 자바 코드 %>
3. JSP 내장 객체
- 별도의 선언 없이 사용하는 자바 객체
- _jspService() 메서드에 선언된 request, response, pageContext, session, application, config, out, page, exception
4. [JSP 전용 태그] 선언문
- <%! 멤버변수 및 메서드 선언 %>
- 서블릿 클래스의 변수나 메서드를 선언할 떄 사용하는 태그 (_jspService() 밖의 클래스 블록 안에)
5. [JSP 전용 태그] 표현식
- <%= 결과를 반환하는 자바 표현식 %>
서블릿에서 JSP 페이지로 요청을 위임하는 방법으로는 forward와 include가 있다.
1. forward : exception 처리 등 다른 페이지로 포워딩
- <jsp:forward>
2. include : (header, footer, nav 등..) 다른 페이지를 인클루딩
- <jsp:include page="/Tail.jsp" />
그리고 데이터베이스에서 가져온 정보를 JSP 페이지에 전달하려면 그 정보를 담을 객체가 필요하다. (값 객체(VO) = 데이터 수송 객체(DTO))
ServletRequest는 클라이언트의 요청을 다루는 기능외에 어떤 값을 보관하는 보관소 기능도 있어, 이를 통해 서블릿에서 JSP로 데이터를 전달할 수 있다.
여기서 잠깐 데이터 보관소에 대해 살펴보자.
서블릿 및 JSP 에서 제공하는 보관소는 4가지가 있다.
우선, ServletContext는 웹 애플리케이션이 시작될 때 생성되어 종료될 때까지 유지되는 객체로, 이 객체에 값을 보관하면 웹 애플리케이션이 종료될 때까지 모든 서블릿(및 JSP)이 이용할 수 있다. DB 커넥션 객체 등이 여기에 활용된다. 이 서블릿 객체는 클라이언트에서 호출하는 것이 아니므로, service()나 doGet() 등이 아니라, init()이나 destroy() 등을 오버라이딩한다. 그리고 DD파일에 <load-on-startup>태그를 지정하면, 해당 서블릿은 웹 애플리케이션이 시작될 때 자동으로 생성된다. (클라이언트에서 실행을 요청해서는 안되므로, <servlet-mapping> 태그는 만들지 않는다.)
두번째로, HttpSession은 클라이언트의 최초 요청 시 생성되어 브라우저를 닫을 때까지 유지되며, 보통 로그인, 로그인 정보 사용, 로그아웃(invalidate) 등에서 활용된다.
HttpSession의 활용 - 로그인 정보 사용 (vo 생성 및 httpsession정보에 등록)
세번째로, ServletRequest는 클라이언트의 요청이 들어올 때 생성되어, 클라이언트에게 응답할 때까지 유지된다.
마지막으로, JspContext는 JSP 페이지가 실행되는 동안만 유지되며, JSP 로컬변수와의 차이는 <jsp:include page="/Tail.jsp" />와 같은 태그 핸들러에게 데이터를 전달할 때 사용될 수 있다는 것이다.
객체를 공유할 경우 그 유효 범위를 따져서 적절한 보관소를 선택해야 한다.
JSP 페이지를 작성할 때, 가능한 자바 코드의 삽입을 최소화하는 것이 유지보수에 좋다. JSP 전용 태그 중에서 액션은 자바 객체를 생성하거나 request 객체에서 값을 꺼내는 작업을 쉽게 처리할 수 있다. (비즈니스로직과 프리젠테이션 로직을 나눠서 개발이 가능해진다.) 그러나 이런 부분들도 최근에 들어서는 JSP 액션태그보다는 오히려 HTML, CSS, Javascript 기술이 더 요구되고 있다. JSP 전용태그를 많이 쓸수록 JSP 기술에 더 종속되기 때문이다.
JSP 액션 태그 - <jsp:useBean>
- application, session, request, page 보관소에 저장된 자바 객체를 꺼낼 수 있다.
- <jsp:useBean id="꺼낼 객체의 이름" scope="page|request|session|application"class="클래스명" type="타입명" />- scope : 기존의 객체를 조회하거나 새로 만든 객체를 저장할 보관소를 지정(기본은 page)
- page :JspContext - request :ServletRequest - session :HttpSession - application :ServletContext
EL표기란, 콤마(.)와 대괄호([])를 사용하여 자바 빈의 프로퍼티나 맵, 리스트, 배열의 값을 보다 쉽게 꺼내게 해주는 기술이다. EL 표기를 사용하면 액션보다 더 쉬운 방법으로 보관소에 저장된 객체에 접근하여 값을 꺼낼 수 있다.
EL 표기법
-${표현식} : 즉시 적용
-#{표현식} : 지연적용 (사용자가 입력한 값을 객체의 프로퍼티에 담는 용도)-EL도 usebean처럼 네 군데 보관소에서 값을 꺼낼 수 있다. (다만, 객체는 생성할 수 없다)
- pageScope, requestScope, sessionScope, applilcationScope
-${ 객체보관소.객체이름.프로퍼티 }
EL을 사용하면 객체를 꺼내고자 <jsp:useBean> 액션태그를 사용할 필요가 없다. 또한 <%=member.getNo()%>와 같은 표현식 태그보다도 훨씬 간결하게 표현할 수 있다.
여기에 JSTL 확장 태그를 사용하면 JSP 페이지에서 자바 코드를 작성하지 않고 대부분의 출력 작업을 처리할 수 있다(반복문 등). 이렇게 JSTL과 EL 표기를 사용하여 자바 코드의 작성을 최소화한다면, 웹 디자이너나 웹 퍼블리셔와 협업하기가 쉬워진다.
다음으로 모델 컴포넌트를 만들기 위해서는,
서블릿으로부터 데이터 처리로직을 분리하여 DAO를 정의하여야 한다. DAO는 데이터베이스나 파일, 메모리 등을 이용하여 애플리케이션 데이터를 생성, 조회, 변경, 삭제하는 역할을 수행한다. DAO를 도입할 경우 데이터 처리 부분을 공유함으로써 중복 코드를 줄일 수 있고, 데이터 구조에 변경이 발생하면 그 변경사항을 적용하기 쉽다. 또한, 다른 프로젝트에서 재사용하기가 쉽다.
MemberDao에서는 ServletContext에 접근할 수 없기 때문에, ServletContext에 보관된 DB Connection 객체를 꺼낼 수 없다.
따라서 selectList()가 호출되기 전에 Connection 객체가 먼저 설정되어야 한다.
이렇게 작업에 필요한 객체를 외부로부터 주입받는 것을 의존성 주입(DI)라고 한다.
그런데 DAO의 경우처럼 여러 서블릿이 사용하는 객체는, 서로 공유하는 것이 메모리 관리나 실행속도 측면에서 좋다. 요청에 따라 객체를 만들게 되면 Garbage가 생성되고, 실행 시간이 길어지기 때문이다. DAO를 공유하려면 ServletContext(application)에 저장하는 것도 좋으나, 더 좋은 방법은 이벤트를 이용하는 것이다.
서블릿 컨테이너는 웹 애플리케이션의 상태를 모니터링할 수 있도록 웹 애플리케이션의 시작에서 종료까지 주요한 사건에 대해 알림기능을 제공한다. 이런 알림 기능을 이용하려면 규칙에 따라 ServletContextListener의 구현체를 만들고, DD파일(web.xml)에 등록하면 된다.
DAO를 사용하면서 주의할 부분은 없을까?
DAO를 이용하여 데이터를 다루는 부분에서 성능상 가장 느려질 수 있는 부분은 DB Connection 객체를 얻는 부분이다. 왜냐하면 같은 장비에 DB가 구성되어 있다고 하더라도, DB와 WAS 사이에는 통신을 해야하기 때문이다. (다른장비에 있다면 당연히 더 소요된다.) 사용자가 갑자기 증가하면 Connection 객체를 얻기 위한 시간이 더더욱 증가하게 된다.
이를 개선하기 위해서는 연결 객체를 생성하고 사용한 후, 버리지 않고 보관해두었다가 다시 사용하는 방식(pooling)을 사용하면 되며, 이 때 DB Connection Pool을 사용하게 된다. (현재 모든 WAS에서 Connection Pool을 제공하고, DataSource를 사용하여 JNDI로 호출하여 쓸 수 있다.)
DataSource와 JNDI
1. javax.sql 확장 패키지
-DriverManger를 대체할 수 있는 Datasource 인터페이스 제공
-Connection 및 Statement 객체의 풀링
- 분산트랜잭션 처리
-Rowsets의 지원
2. DataSource
-Datasource는 서버에서 관리하기 때문에 데이터베이스나 JDBC드라이버가 변경되더라도 애플리케이션을 바꿀 필요가 없다.
-Connection과 Statement 객체를 풀링할 수 있으며, 분산 트랜잭션을 다룰 수 있다.
-Datasouce는 커넥션 객체를 반납하는게 아니라 close를 한다?
Datasource는 DriverManager가 생성한 커넥션을 리턴하는 것이 아니라, 대행 객체(PoolableConnection 객체)를 리턴한다.
이 대행객체에는 진짜 커넥션을 가리키는 참조변수(_conn)와 커넥션풀을 가리키는 참조변수(_pool)이 들어 있다.
따라서 DataSource가 만들어 준 커넥션 대행 객체에 대해 close()를 호출하면, 커넥션 대행 객체는 진짜 커넥션 객체를 커넥션풀에 반납한다.
-JNDI(JavaNamingandDirectoryInterfaceAPI): 이 API를 사용하여 서버의 자원을 찾을 수 있음("java:comp/env/jdbc/studydb")
이제 MVC 아키텍처의 컴포넌트 중 컨트롤러만 남았다.
컨트롤러를 만들다보면 요청 데이터를 처리하는 코드나, 모델과 뷰를 제어하는 코드가 중복되는 경우가 있다. 이런 부분은 컨트롤러를, 프런트 컨트롤러와 페이지 컨트롤러로 나누어 해결할 수 있다.
프런트 컨트롤러는 이름에서 알 수 있듯이 제일 앞에서 클라이언트의 요청을 받는 서블릿이다. 기존에 컨트롤러들이 하던 일 중에서 공통 작업을 처리한다. 페이지 컨트롤러는 특정 요청을 위한 작업을 수행한다. 즉 JSP가 사용할 데이터를 준비한다거나 데이터 처리가 끝났을 때 어떤 JSP를 실행해야 하는지 프런트 컨트롤러에게 알려주는 일을 한다.
1. 프런트 컨트롤러 패턴
- 프런트 컨트롤러는 VO 객체의 준비, 뷰 컴포넌트로의 위임, 오류 처리 등과 같은 공통작업을 담당하고, 페이지 컨트롤러는 이름 그대로 요청한 페이지만을 위한 작업을 수행
2. 프런트 컨트롤러 만들기
- doGet, doPost가 아니라 service를 오버라이딩하는 이유
(GET, POST 뿐만아니라 다양한 요청방식에도 대응하기 위함)
- 요청 URL에서 서블릿 경로 알아내기 : getServletPath()
- 조건문을 활용하여 페이지 컨트롤러로 위임 (include)
- 요청 매개변수의 값을 꺼내서 VO 객체에 담고, "member"라는 키로 ServletRequest에 보관
- 페이지 컨트롤러의 실행이 끝나면, 화면 출력을 위해 ServletRequest에 보관된 뷰 URL로 실행을 위임.
단, 뷰 URL이 "redirect:"로 시작한다면, 인클루딩하는 대신에 sendRedirect()를 호출
- 오류 처리 (페이지 컨트롤러를 작성할 때는 오류 처리 코드를 넣을 필요가 없음)
-@WebServlet 애노테이션을 사용하여 프런트 컨트롤러의 배치 URL을 "*.do"로 지정
(즉, 클라이언트 요청중에서 서블릿 경로 이름이 .do로 끝나는 경우는 DispatcherServlet이 처리하겠다는 의미)
그런데 페이지 컨트롤러를 서블릿으로 만들 경우, 기능이 추가되어 페이지 컨트롤러를 추가할 때마다 배치기술서 파일(web.xml)에 등록해야 한다. 페이지 컨트롤러를 서블릿이 아닌 일반 클래스로 만들면 web.xml 파일에 등록할 필요가 없어 유지보수가 쉬워진다. 일반 클래스로 전환 후에는 프런트 컨트롤러에서 작업 위임시에 포워딩이나 인클루딩 대신 메서드를 호출하면 된다.
이 때, 프런트 컨트롤러가 페이지 컨트롤러를 일관성 있게 사용하려면 호출 규칙을 정의해야 하며, 여기서 인터페이스 문법을 활용할 수 있다.
즉, 프런트 컨트롤러와 페이지 컨트롤러 사이의 호출 규칙을 인터페이스로 정의해놓고, 이를 구현하는 방식으로 페이지 컨트롤러를 생성할 수 있다. 이렇게 하면 페이지 컨트롤러를 만들더라도 더 이상 web.xml 파일에 등록할 필요가 없다.
(또한, 프런트 컨트롤러와 페이지 컨트롤러 사이에 데이터를 주고받기 위해 Map객체를 사용하여 페이지 컨트롤러가 Servlet API를 직접 사용하지 않도록 하여야 한다.)
하지만 이 방식의 경우에는 페이지 컨트롤러를 추가할 때마다 매번 프런트 컨트롤러에 이에 대한 내용(조건문)을 추가해야 한다. 물론 이를 자동화하기 위해서 ContextLoaderListener에서 페이지 컨트롤러를 준비하는 방식으로 해결할 수 있으나, DAO를 추가하는 경우에 ContextLoaderListener 클래스에 코드를 추가해주어야 한다. 결국, 이를 해결하기 위해서 프로퍼티를 이용해 객체를 관리하고, 리플렉션 API를 사용하여 인스턴스를 자동 생성하고 메서드를 자동으로 호출하여 해결할 수 있다.
프로퍼티 파일 작성
- properties는 '이름=값'형태로 된 파일을 다룰 때 사용하는 클래스
- 톰캣 서버에서 제공하는 객체 : jndi.{객체이름}={JNDI 이름}
- 일반 객체 : {객체이름} = {패키지 이름을 포함한 클래스 이름}
그런데, 객체 관리 기법으로 프로퍼티보다 애노테이션이 더 편리하다(ㅡㅅㅡ). 애노테이션을 정의해놓고, 이를 DAO나 페이지 컨트롤러에 적용하고, 웹 애플리케이션이 시작될 때 이들 클래스로부터 애노테이션 값을 추출하여 객체를 자동 생성하면 된다.
(솔직히 리플랙션 API, 프로퍼티를 이용한 객체 관리, 애노테이션을 이용한 객체 관리, 그리고 IoC 컨테이너 등 책의 후반부는 확실히 이해가 되지 않는 듯하다. 추가적인 학습 후에 내용을 보강할 계획이다.)
MVC 아키텍처를 적용하고 보니 All-in-One 방식일 때의 서블릿보다 코드 가독성이 좋아졌음을 알 수 있다. 그런데 DB와 연결하는 부분의 코드는 좀 더 깔끔하게 할 수 없을까?
퍼시스턴스 프레임워크를 사용하여 JDBC 프로그래밍으로부터 해방될 수 있다. 퍼시스턴스 프레임워크에는 SQL문장으로 직접 DB 데이터를 다루는 ‘SQL 맵퍼’와 자바 객체를 통해 간접적으로 DB 데이터를 다루는 ‘객체관계맵퍼(ORM)’이 있다. SQL 맵퍼의 대표주자로 ‘mybatis’가 있고, ORM의 대표주자로 ‘hibernate와 TopLink’가 있다.
ORM은 프레임워크에서 제공하는 API와 전용 객체 질의어를 사용하여 데이터를 다룬다. 그런데 ORM을 사용하려면 데이터베이스 정규화가 잘 되어 있어야 한다.(그래야 객체와 연결이 쉽다). 그리고 프로젝트가 유지보수 단계에 들어가면 잦은 기능 변경과 추가로 DB 구조가 흐트러져 결국 개발 비용이 증가하게 된다. 또한, ORM을 사용할 경우 DB 특징에 맞추어 최적화할 수 없다. 이에 ‘열혈강의 웹 개발 워크북’에서는 SQL 맵퍼를 추천한다.
mybatis는 SQL 맵퍼로서 JDBC 코드를 캡슐화하여 데이터베이스와의 연동을 쉽게 해둔다. 즉 자바코드에서 SQL을 분리함으로써 개발자가 좀 더 SQL 작성에 집중할 수 있게 해주고, JDBC API를 사용하여 DB와 연동하는 부분을 mybatis가 대신해줌으로써 단조롭고 번거로운 JDBC 프로그래밍으로부터 개발자를 해방시켜준다.
mybatis를 사용한다면 더이상 DataSource가 필요 없다. 대신 SqlSessionFactory 객체가 필요하다. 이 객체를 이용하여 Sqlsession 객체를 통해 SQL문을 실행하여 DAO 클래스를 간결하게 유지할 수 있다. 특히 mybatis의 동적 SQL 기능은 하나의 SQL문으로 여러 상황을 처리할 수 있어 편리하다.
SQL 맵퍼 파일 (DAO로부터 SQL문을 분리하자!)
1. SQL 맵퍼 파일 작성
-XML 선언, <mapper> 태그의 namespace 속성(자바의 패키지처럼 SQL문을 묶는 용도) 작성
- <select>, <insert>, <update>, <delete> 엘리먼트
-SQL문을 구분하기 위한 id
- resultType (결과를 담을 객체를 지정)
- resultMap (칼럼이름과 셋터 이름 불일치 문제 해결)
2. mybatis의 SELECT 결과 캐싱
-SELECT를 실행할때마다 결과 레코드에 대해 매번 객체를 생성한다면, 속도도 느리고 메모리도 낭비되므로 캐싱해두었다가 다음 SELECT를 실행할 때 재사용한다.
- <resultMap>의 <id>를 식별자로 사용하면, 캐싱된 객체를 빨리 찾을 수 있다.
3. SQL 문의 입력 매개변수 처리
-JDBC 의 입력 매개변수는 ? 였으나, mybatis 입력 매개변수는 #{프로퍼티명}이다.
mybatis는 내부 실행 상황을 쉽게 모니터링할 수 있도록 로그 출력 기능을 제공한다. SQL문이 실행되는 과정을 보고 싶거나 동적으로 생성된 SQL 문을 확인하고 싶다면 mybatis의 로그 출력기능을 켜면 된다.