개요
ContextLoaderListener.java
@WebListener
public class ContextLoaderListener implements ServletContextListener {
private static final Logger LOG = Logger.getGlobal();
@Override
public void contextInitialized(ServletContextEvent event) {
try {
ServletContext sc = event.getServletContext();
InitialContext initialContext = new InitialContext();
DataSource ds = (DataSource) initialContext.lookup(
"java:comp/env/jdbc/crudboard_db"
);
MySqlFeedDao feedDao = new MySqlFeedDao();
MySqlUserDao userDao = new MySqlUserDao();
feedDao.setDataSource(ds);
userDao.setDataSource(ds);
sc.setAttribute("/auth/login.do", new LogInController().setUserDao(userDao));
sc.setAttribute("/auth/logout.do", new LogOutController());
sc.setAttribute("/auth/join.do", new JoinController().setUserDao(userDao));
sc.setAttribute("/feed/main.do", new FeedListController().setFeedDao(feedDao));
sc.setAttribute("/feed/content.do", new FeedContentController().setFeedDao(feedDao));
sc.setAttribute("/feed/add.do", new FeedAddController().setFeedDao(feedDao));
sc.setAttribute("/feed/edit.do", new FeedEditController().setFeedDao(feedDao));
sc.setAttribute("/feed/delete.do", new FeedDeleteController().setFeedDao(feedDao));
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {}
}
다음은 기존에 작성했던 ContextLoaderListener 클래스이다. 서블릿이 시작하면 바로 실행되어 서블릿 컨테이너의 작동에 필요한 객체들을 선언하고, 저장해둔다. 많은 경우, 웹 서비스가 커지면서 필요한 객체(DAO, 컨트롤러, 데이터소스 등등)는 계속 늘어난다. 그럴때마다 contextLoaderListener를 수정해주어야 한다. 이는 굉장히 비효율적인 작업이다.
ContextLoaderListner가 하는 객체 생성 및 의존성 주입 과정을 자동화하여 처리해보자.
프로퍼티 파일(.properties)과 Properties 객체
자동화 처리를 위해 프로퍼티 파일과 Properties 객체를 이용할 것이다. 프로퍼티 파일은 'key=value' 형태로 key-value 값을 정리해놓은 텍스트 파일이다. '.properties' 형식을 사용한다. Properties 객체는 프로퍼티 파일의 담긴 데이터를 자바 코드로 가져오는 역할을 한다.
my-properties.properties
myKey=myValue
Properties props = new Properties();
props.load(new FileReader("my-properties.properties"));
for (Object item : props.keySet()) {
String key = (String) item; // key = "myKey"
String value = (String) props.getProperty(key); // value = "myValue"
}
실제 프로퍼티 파일과 Properties 객체 사용 예시이다. Properties 객체의 내부 메서드인 load를 통해 프로퍼티 파일을 불러올 수 있고, keySet() 메서드를 통해 key 값을 set 형태로 가져와 getProperty() 메서드를 통해 value 값을 받을 수 있다.
프로퍼티 파일 작성
application-context.properties
ctx.dataSource=java:comp/env/jdbc/crudboard_db
feedDao=crud_board.dao.MySqlFeedDao
userDao=crud_board.dao.MySqlUserDao
/auth/login.do=crud_board.controllers.LogInController
/auth/logout.do=crud_board.controllers.LogOutController
/auth/join.do=crud_board.controllers.JoinController
/feed/main.do=crud_board.controllers.FeedListController
/feed/add.do=crud_board.controllers.FeedAddController
/feed/content.do=crud_board.controllers.FeedContentController
/feed/edit.do=crud_board.controllers.FeedEditController
/feed/delete.do=crud_board.controllers.FeedDeleteController
기존의 ContextLoaderListener에서 생성하던 객체들을 전부 프로퍼티 파일로 옮겨 놓았다.
먼저, dataSource의 경우, 직접 생성하는 것이 아니라 InitialContext를 통해 톰캣 서버에서 얻어오는 객체이다. 이러한 외부로부터 가져오는 개체의 경우 key 앞에 "ctx."을 넣어주었다.
DAO는 dao의 변수명을 key로 DAO 클래스의 경로를 value로 갖는다.
페이지 컨틀롤러는 각 페이지에 대응하는 서블릿 경로를 key로 가지고 컨트롤러 클래스의 경로를 value로 가진다.
ApplicationContext
ApplicationContext 클래스는 프로퍼티 파일을 로드받아 실제로 객체를 생성 및 저장하는 클래스이다. 구체적을 ApplicationContext가 하는 역할은 다음과 같다.
- 프로퍼티 파일을 로드받아 객체를 생성해 저장 = prepareObjects(Properties props) : void
- 저장한 객체들에 의존성 주입 = injectDependency() : void
- 다른 클래스에게 저장한 객체 전달 = getBean(String key) : Object
- getBean(String key) : Object
public class ApplicationContext {
Hashtable<String, Object> objTable = new Hashtable<>();
public Object getBean(String key) {
return objTable.get(key);
}
}
먼저 getBean() 메서드를 살펴보자. ApplicationContext 클래스는 생성한 객체를 HashTable 형태로 저장해둔다. 그 후 getBean(String key)는 메개변수로 key값을 받고 objTable에서 해당 key를 가진 객체를 리턴한다.
- prepareObjects(Properties props) : void
public class ApplicationContext {
Hashtable<String, Object> objTable = new Hashtable<>();
private void prepareObjects(Properties props) throws Exception {
Context ctx = new InitialContext();
String key = null;
String value = null;
for (Object item : props.keySet()) {
key = (String) item;
value = props.getProperty(key);
if (key.startsWith("ctx.")) {
objTable.put(key, ctx.lookup(value));
} else {
objTable.put(key, Class.forName(value).getDeclaredConstructor().newInstance());
}
}
}
}
prepareObjects() 메서드는 프로터피 객체로부터 값을 읽어와 객체를 생성한다. 이때 key가 "ctx."으로 시작할 경우, 외부로 부터 가져와야하는 객체이므로 InitialContext.lookup 메서드를 통해 객체를 가져온다. 그 외에는 직접 인스턴스를 생성한다. 그 후, 생성된 객체는 objTable(HashTable)에 저장한다.
- injectDependency() : void
public class ApplicationContext {
Hashtable<String, Object> objTable = new Hashtable<>();
private void injectDependency() throws Exception {
for (String key : objTable.keySet()) {
if (!key.startsWith("ctx.")) {
callSetter(objTable.get(key));
}
}
}
private void callSetter(Object obj) throws Exception {
Object dependency = null;
for (Method m : obj.getClass().getMethods()) {
if (m.getName().startsWith("set")) {
dependency = findObjectByType(m.getParameterTypes()[0]);
if (dependency != null) {
m.invoke(obj, dependency);
}
}
}
}
private Object findObjectByType(Class<?> type) {
for (Object obj : objTable.values()) {
if (type.isInstance(obj)) {
return obj;
}
}
return null;
}
}
injectDependency() 메서드는 생성한 객체에 의존성 주입을 해주는 메서드이다. 이를 위해 각 객체별로 setter 메서드를 호출해야하는데 callSetter 메서드를 통해 해당 작업을 수행한다. 메서드 검색 및 호출은 Reflection API의 Method 객체를 통해 처리한다.
findObjectByType 메서드는 callSetter 실행 과정에서 setter 하려는 값(또는 객체)가 objTable에 존재하는 조사하여, 존재할 경우 setter 메서드의 매게변수로 가져온다.
ApplicationContext.java
public class ApplicationContext {
Hashtable<String, Object> objTable = new Hashtable<>();
public Object getBean(String key) {
return objTable.get(key);
}
public ApplicationContext(String propertiesPath) throws Exception {
Properties props = new Properties();
props.load(new FileReader(propertiesPath));
prepareObjects(props);
injectDependency();
}
private void prepareObjects(Properties props) throws Exception {
Context ctx = new InitialContext();
String key = null;
String value = null;
for (Object item : props.keySet()) {
key = (String) item;
value = props.getProperty(key);
if (key.startsWith("ctx.")) {
objTable.put(key, ctx.lookup(value));
} else {
objTable.put(key, Class.forName(value).getDeclaredConstructor().newInstance());
}
}
}
private void injectDependency() throws Exception {
for (String key : objTable.keySet()) {
if (!key.startsWith("ctx.")) {
callSetter(objTable.get(key));
}
}
}
private void callSetter(Object obj) throws Exception {
Object dependency = null;
for (Method m : obj.getClass().getMethods()) {
if (m.getName().startsWith("set")) {
dependency = findObjectByType(m.getParameterTypes()[0]);
if (dependency != null) {
m.invoke(obj, dependency);
}
}
}
}
private Object findObjectByType(Class<?> type) {
for (Object obj : objTable.values()) {
if (type.isInstance(obj)) {
return obj;
}
}
return null;
}
}
다음은 ApplicationContext 클래스의 전문이다. 객체를 생성하는 prepareObjects 메서드와 injectDependency 메서드는 클래스가 생성되는 생성자 부분에서 호출된다.
ContextLoaderListener 수정
객체의 생성 및 의존성 주입을 자동화함으로써 ContextLoaderListner의 객체 선언 및 생성 파트는 삭제된다. 대신, web.xml로 부터 프로퍼티 파일 경로를 받아와 ApplicationContext를 생성한다.
@WebListener
public class ContextLoaderListener implements ServletContextListener {
private static ApplicationContext applicationContext;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void contextInitialized(ServletContextEvent event) {
try {
ServletContext sc = event.getServletContext();
String propertiesPath = sc.getRealPath(sc.getInitParameter("contextConfigLocation"));
applicationContext = new ApplicationContext(propertiesPath);
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {}
}
보다시피 객체를 직접 생성하는 코드는 전부 사라진것을 볼 수 있다. 그리고 ApplicationContext의 경우, 다른 객체에서도 사용할 것이므로 static 객체로 선언해주었다. 또한 ApplicationContext의 getter 메서드 역시 static 메서드로 선언해주었다.
프런트 컨트롤러(DispatcherServlet) 수정
기존의 프런트 컨트롤러는 ServletContext로부터 객체를 받아왔다. 해당 파트는 ApplicationContext.getBean() 메서드로 수정되었다.
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html; charset=UTF-8");
request.setCharacterEncoding("UTF-8");
HttpSession session = request.getSession();
String servletPath = request.getServletPath();
try {
HashMap<String, Object> model = new HashMap<>();
model.put("session", session);
ApplicationContext ctx = ContextLoaderListener.getApplicationContext();
Controller pageController = (Controller) ctx.getBean(servletPath);
if (pageController instanceof DataBinding) {
prepareRequestData(request, model, (DataBinding) pageController);
}
String viewUrl = "";
if (pageController != null) {
viewUrl = pageController.execute(model);
} else {
viewUrl = "error.jsp";
}
for (String key : model.keySet()) {
request.setAttribute(key, model.get(key));
}
if (viewUrl.startsWith("redirect:")) {
response.sendRedirect(viewUrl.substring(9));
} else {
RequestDispatcher rd = request.getRequestDispatcher(viewUrl);
rd.include(request, response);
}
} catch (Exception e) {
throw new ServletException(e);
}
}
}
'프로젝트 이야기 > CRUD 미니 게시판' 카테고리의 다른 글
220327 (6) : 댓글 기능 구현 (0) | 2022.03.27 |
---|---|
220323 [bug] : 줄바뀜이 읽기/수정 폼에서 적용이 안되는 문제 (0) | 2022.03.23 |
220319 (4) : 로그인/회원가입 입력폼 예외처리 (0) | 2022.03.19 |
220311 (3) : 인스턴스 생성 자동화하기 (feat. Reflection API) (0) | 2022.03.12 |
220310 (2) : 유저 기능 구현 (0) | 2022.03.10 |