본문 바로가기
교재 실습/자바 웹 개발 워크북

91. 프로퍼티를 이용한 객체 관리 (3)

by Jint 2022. 8. 11.

4. ContextLoaderListener 변경

ApplicationContext 클래스를 만든 이유는 페이지 컨트롤러나 DAO가 추가되더라도 ContextLoaderListener 클래스를 변경하지 않기 위함이다. 정말 그것이 가능한지 다음 실습을 통해 확인한다.

spms.listeners.ContextLoaderListener 클래스를 다음과 같이 변경한다.

package spms.listeners;

import javax.naming.InitialContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
//import javax.servlet.annotation.WebListener;

import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;

import spms.context.ApplicationContext;
import spms.controls.LogInController;
import spms.controls.LogOutController;
import spms.controls.MemberAddController;
import spms.controls.MemberDeleteController;
import spms.controls.MemberListController;
import spms.controls.MemberUpdateController;
import spms.dao.MySqlMemberDao;

//프로퍼티 파일 적용 : ApplicationContext 사용
@WebListener
public class ContextLoaderListener implements ServletContextListener {
	static ApplicationContext applicationContext;
	
	//ApplicationContext 객체 얻을 때 사용
	public static ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
		try {
			//DB 커넥션 객체 준비
			ServletContext sc = event.getServletContext();
			
			//InitialContext 객체 생성 - 톰캣 서버에서 자원 찾기 위함
			//InitialContext initialContext = new InitialContext();
			//lookup() 메서드로 JNDI 이름으로 등록된 서버 자원 찾을 수 있다.
			//lookup() 메서드 매개변수는 서버 자원의 JNDI 이름
			//DataSource ds = (DataSource)initialContext.lookup("java:comp/env/jdbc/studydb");
			
			//MemberDao 객체 준비하여 ServletContext에 보관
			//MemberDao memberDao = new MemberDao();
			//MySqlMemberDao memberDao = new MySqlMemberDao();
			
			//memberDao.setDataSource(ds); //DAO에 DataSource 객체 주입
			//sc.setAttribute("memberDao", memberDao); //memberDao 객체를 별도로 꺼내서 사용할 일 없기 때문에 ServletContext에 저장하지 않는다.
			
			//페이지 컨트롤러 객체를 ServletContext에 저장시, 서블릿 요청 URL을 키값으로 하여 저장
			/*sc.setAttribute("/auth/login.do", new LogInController().setMemberDao(memberDao));
			sc.setAttribute("/auth/logout.do", new LogOutController());
			sc.setAttribute("/member/list.do", new MemberListController().setMemberDao(memberDao));
			sc.setAttribute("/member/add.do", new MemberAddController().setMemberDao(memberDao));
			sc.setAttribute("/member/update.do", new MemberUpdateController().setMemberDao(memberDao));
			sc.setAttribute("/member/delete.do", new MemberDeleteController().setMemberDao(memberDao));*/
			
			//web.xml 파일로부터 프로퍼티 파일 이름과 경로 정보 읽어옴
			String propertiesPath = sc.getRealPath(sc.getInitParameter("contextConfigLocation"));
			applicationContext = new ApplicationContext(propertiesPath);
		} catch(Throwable e) {
			e.printStackTrace();
		}
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {}
}

이전 소스와 비교하면 매우 간결하다. 그리고 정말 중요한 것은 이제 더 이상 이 클래스를 변경할 필요가 없다는 것이다. 페이지 컨트롤러나 DAO 등을 추가할 때는 프로퍼티 파일에 그 클래스에 대한 정보를 한 줄 추가하면 자동으로 그 객체가 생성된다.

 

- 프로퍼티 파일의 경로

프로퍼티 파일의 이름과 경로 정보도 web.xml 파일로부터 읽어오게 처리하였다. ServletContext 객체의 getInitParameter() 메서드를 호출하여 web.xml에 설정된 매개변수 정보를 가져온다.

//web.xml 파일로부터 프로퍼티 파일 이름과 경로 정보 읽어옴
String propertiesPath = sc.getRealPath(sc.getInitParameter("contextConfigLocation"));

그리고 ApplicationContext 객체를 생성할 때 생성자의 매개변수로 넘겨준다.

applicationContext = new ApplicationContext(propertiesPath);

이렇게 생성한 ApplicationContext 객체는 프런트 컨트롤러에서 사용할 수 있게 ContextLoaderListener 클래스의 클래스 변수 'applicationContext'에 저장된다.

 

- getApplicationContext() 클래스 메서드

이 메서드는 ContextLoaderListener 클래스에서 만든 ApplicationContext 객체를 얻을 때 사용한다. 당장 프런트 컨트롤러에서 사용해야 한다. 클래스 이름만으로 호출할 수 있게 static으로 선언하였다.

//ApplicationContext 객체 얻을 때 사용
public static ApplicationContext getApplicationContext() {
    return applicationContext;
}

 

5. web.xml 파일에 프로퍼티 경로 정보 설정

ContextLoaderListener 클래스가 프로퍼티 파일을 찾을 수 있도록 web.xml 파일에 프로퍼티에 대한 파일 경로 정보를 설정한다.

다음과 같이 web.xml 파일에 컨텍스트 매개변수를 추가한다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xmlns="http://xmlns.jcp.org/xml/ns/javaee"
		 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
		 id="WebApp_ID"
		 version="3.1">
	<display-name>web06</display-name>

	<!-- 컨텍스트 초기화 파라미터 -->
	<!-- 프로퍼티 경로 정보 설정 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/application-context.properties</param-value>
	</context-param>

	<!-- 웹 애플리케이션에서 톰캣 서버의 자원 사용 -->
	<resource-ref>
		<res-ref-name>jdbc/studydb</res-ref-name>
		<res-type>javax.sql.DataSource</res-type>
		<res-auth>Container</res-auth>
	</resource-ref>
  
	<!-- 컨텍스트 초기화 파라미터 -->
	<!-- 
	<context-param>
		<param-name>driver</param-name>
		<param-value>com.mysql.jdbc.Driver</param-value>
	</context-param>
	<context-param>
		<param-name>url</param-name>
		<param-value>jdbc:mysql://localhost/studydb</param-value>
	</context-param>
	<context-param>
		<param-name>username</param-name>
		<param-value>study</param-value>
	</context-param>
	<context-param>
		<param-name>password</param-name>
		<param-value>study</param-value>
	</context-param>
	-->
	
	<!-- 필터 선언 -->
	
	<!-- 필터 URL 매핑 -->
	
	<!-- 리스너 선언 -->
	<!--  
	<listener>
		<listener-class>spms.listeners.ContextLoaderListener</listener-class>
	</listener> 
	 -->
		
	<!-- 서블릿 선언 -->
	<!-- 
	<servlet>
		<servlet-name>AppInitServlet</servlet-name>
		<servlet-class>spms.servlets.AppInitServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet> 
	 -->
	
	<!-- 서블릿을 URL과 연결 -->
  
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.jsp</welcome-file>
    <welcome-file>default.htm</welcome-file>
  </welcome-file-list>
</web-app>

 

6. DispatcherServlet 변경

이제 마지막으로 프런트 컨트롤러를 변경하는 것만 남았다.

spms.servlets.DispatcherServlet 클래스를 다음과 같이 변경한다.

package spms.servlets;

import java.io.IOException;
import java.util.HashMap;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import spms.bind.DataBinding;
import spms.bind.ServletRequestDataBinder;
import spms.context.ApplicationContext;
import spms.controls.Controller;
import spms.controls.LogInController;
import spms.controls.LogOutController;
import spms.controls.MemberAddController;
import spms.controls.MemberDeleteController;
import spms.controls.MemberListController;
import spms.controls.MemberUpdateController;
import spms.listeners.ContextLoaderListener;
import spms.vo.Member;

@SuppressWarnings("serial")
@WebServlet("*.do") //프런트 컨트롤러의 배치
public class DispatcherServlet extends HttpServlet {//서블릿이기 때문에 HttpServlet 상속
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html; charset=UTF-8");
		request.setCharacterEncoding("UTF-8");
		String servletPath = request.getServletPath(); //서블릿 경로 추출
		try {
			//ServletContext 객체 준비
			//ServletContext sc = this.getServletContext();
			//ApplicationContext 객체 준비
			ApplicationContext ctx = ContextLoaderListener.getApplicationContext();
			
			//페이지 컨트롤러에게 전달할 Map 객체 준비
			HashMap<String,Object> model = new HashMap<String,Object>();
			//model.put("memberDao", sc.getAttribute("memberDao")); //memberDao 객체는 더이상 Map 객체에 담을 필요 없음.
			model.put("session", request.getSession()); //로그인 및 로그아웃 페이지 컨트롤러에서 사용할 세션 객체
			
			//String pageControllerPath = null;
			//인터페이스 타입의 참조 변수 선언 - Controller의 구현체 클래스들 객체 주소 저장 위함
			//Controller pageController = null;
			//ServletContext 객체에서 페이지 컨트롤러 꺼낼 때 서블릿 URL 사용
			//Controller pageController = (Controller)sc.getAttribute(servletPath);
			//ApplicationContext 객체에서 페이지 컨트롤러 꺼낼 때 서블릿 URL 사용
			Controller pageController = (Controller)ctx.getBean(servletPath);
			//찾지 못한다면 오류 발생시킴
			if(pageController == null) {
				throw new Exception("요청한 서비스를 찾을 수 없습니다.");
			}
			
			//DataBinding 구현 여부 검사하여, 해당 인터페이스를 구현한 경우에만 호출
			if(pageController instanceof DataBinding) {
				prepareRequestData(request, model, (DataBinding)pageController);
			}
			
			...
	}
}

이전에는 페이지 컨트롤러가 ServletContext 객체에 저장되었기 때문에 이 객체를 준비해야 했지만, ApplicationContext 객체를 도입하면서 필요가 없어졌다. 그래서 제거하였다.

//ServletContext 객체 준비
//ServletContext sc = this.getServletContext();
//ApplicationContext 객체 준비
ApplicationContext ctx = ContextLoaderListener.getApplicationContext();

대신 ContextLoaderListener 클래스의 getApplicationContext() 메서드를 호출하여 ApplicationContext 객체를 꺼낸다.

페이지 컨트롤러를 찾을 때도 ServletContext 객체에서 찾지 않기 때문에 해당 코드를 제거했다.

//ServletContext 객체에서 페이지 컨트롤러 꺼낼 때 서블릿 URL 사용
//Controller pageController = (Controller)sc.getAttribute(servletPath);
//ApplicationContext 객체에서 페이지 컨트롤러 꺼낼 때 서블릿 URL 사용
Controller pageController = (Controller)ctx.getBean(servletPath);

대신 ApplicationContext 객체의 getBean() 메서드를 호출하여 페이지 컨트롤러를 찾는다. 만약 찾지 못한다면 오류를 발생시킨다.

//찾지 못한다면 오류 발생시킴
if(pageController == null) {
    throw new Exception("요청한 서비스를 찾을 수 없습니다.");
}

 

앞에서 생각한 시나리오대로 잘 동작하는지 테스트한다. 톰캣 서버를 재시작하고, 모든 기능을 테스트한다. 이전과 동일하게 잘 동작한다.

 

만약 오류가 있다면, 누구에게 물어보지 말고 자신이 해결한다. 아무리 해도 안 되겠으면 처음부터 다시 코딩한다. 그것이 프로그래밍 실력을 가장 빠르게 향상시키는 방법이다. 그리고 절대로 눈 코딩하지 않는다. 그냥 쳐다본다고 실력이 늘지 않는다. 자신이 타이핑한 라인 수만큼 실력이 늘 것이다.

 

참고도서 : https://freelec.co.kr/book/1674/

 

[열혈강의] 자바 웹 개발 워크북

[열혈강의] 자바 웹 개발 워크북

freelec.co.kr

댓글