아래 Spring web.xml의 ContextLoaderListener의 환경설정 파일인 applicationContext.xml의 위치를 지정하는 코드에서 classpath:의 위치가 어디인가?

  <context-param>
  	<param-name>contextConfigLocation</param-name>
  	<param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  
  <listener>
  	<listener-class>
  		org.springframework.web.context.ContextLoaderListener
  	</listener-class>
  </listener>

만일 위 설정 내용에서 classpath:을 빼버리면 당연히 applicationContext.xml를 찾지 못한다는 에러가 발생한다. 아래와 같이

org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from class path resource [applicationContext.xml]; nested exception is java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be opened because it does not exist

그렇다면 저 classpath:의 위치는 어디를 가리킨단 말인가? 만일 프로젝트 이름이 BordWebDay4Class04라고 한다면 
이클립스의 프로젝트명에서 마우스 우측 클릭 ⇒ Build Path ⇒ Configure Build Path... ⇒ 상단 4개의 탭 중에서 Source 탭 선택 하면 아래와 같은 내용이 보일 것이다.


여기서 classpath:의 위치가 2곳 나타나 있다.
BordWebDay4Class04/src/main/java/ 
BordWebDay4Class04/src/main/resources/ 

따라서 applicationContext.xml를 위 두 경로 중 어느 한곳에 위치시키면 정상적으로 구동이 된다.
그런데 BordWebDay4Class04/src/main/java에는 당연히 java 소스코드를, BordWebDay4Class04/src/main/resources에는 스프링 설정 파일을 위치시킨다. 
따라서 applicationContext.xml를 BordWebDay4Class04/src/main/resources/에 위치시키면 된다.

그런데 만일 applicationContext.xml가 BordWebDay4Class04/src/main/resources/joe/ 아래에 설정 파일이 있다면 역시 아래 에러가 발생할 것이다.

java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be opened because it does not exist

해결책은 몇 가지 방법이 있는데 아래 방법 중 어느 하나를 적용하면 된다.
(1) classpath:에 joe라는 경로를 포함시키는 방법

  <context-param>
  	<param-name>contextConfigLocation</param-name>
  	<param-value>classpath:/joe/applicationContext.xml</param-value>
  </context-param>

(2) applicationContext.xml가 있는 BordWebDay4Class04/src/main/resources/joe/를 classpath에 등록하는 방법
이클립스의 프로젝트명에서 마우스 우측 클릭 ⇒ Build Path ⇒ Configure Build Path... ⇒ 상단 4개의 탭 중에서 Source 탭 선택 ⇒ 우측 "Add folder..." 버튼 클릭하여 BordWebDay4Class04/src/main/resources/joe/ 경로를 추가해 준다.

(3) 와일드 카드(**)를 이용해서 현재 classpath 하위의 모든 디렉토리를 포함하도록 설정

  <context-param>
  	<param-name>contextConfigLocation</param-name>
  	<param-value>classpath:/**/applicationContext.xml</param-value>
  </context-param>


위와 같이 설정하면 아래의 경우들이 모두 정상적으로 동작한다.

BordWebDay4Class04/src/main/resources/joe/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/yourjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/herjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/hisjob/applicationContext.xml

이 작업 후 Tomcat을 restart 하면 이제 정상적으로 구동이 될 것이다.

여기서 와일드카드가 하나일때인 /*/와 두개 일때인 /**/의 차이는

전자의 경우는(/*/의 경우는) 현재의 classpath: 디렉토리 하위에 있는 디렉토리들 중 첫번째 하위 디렉토리만 해당된다.
즉 applicationContext.xml가 classpath: 디렉토리 하위의 디렉토리들 중 어느 하위에 속해있든지 모두 인식이 된다는 뜻이다.

/joe/applicationContext.xml    (정상적으로 인식됨)
/kim/applicationContext.xml    (정상적으로 인식됨)
/kim/goo/applicationContext.xml   (인식 안됨)
/seo/qqq/applicationContext.xml   (인식 안됨)

후자의 경우는(/**/의 경우는) 현재의 classpath: 디렉토리 하위에 몇개의 하위 디렉토리들이 있어도 그 하위 모든 디렉토리들을 다 포함시킬수가 있다.
즉 applicationContext.xml가 classpath: 디렉토리 하위의 디렉토리들 중 어느 하위에 속해있든지 모두 인식이 된다는 뜻이다.

/joe/applicationContext.xml    (정상적으로 인식됨)
/kim/applicationContext.xml    (정상적으로 인식됨)
/kim/goo/applicationContext.xml   (정상적으로 인식됨)
/seo/qqq/applicationContext.xml   (정상적으로 인식됨)

아래는 Tomcat을 재구동했을 때 정상적으로 인식되었을 때의 로그이고

정보: Initializing Spring root WebApplicationContext
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Wed Jan 08 16:08:18 KST 2020]; root of context hierarchy

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from file [D:\MyProgramStudy\Spring\.metadata\.plugins\org.eclipse.wst.server.core\tmp2\wtpwebapps\BordWebDay4Class03\WEB-INF\classes\joe\myjob\herjob\applicationContext.xml]
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1593 ms

아래는 Tomcat을 재구동했을 때 인식되지 못했을 때의 로그이다.

정보: Initializing Spring root WebApplicationContext
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Wed Jan 08 16:15:38 KST 2020]; root of context hierarchy

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 357 ms

@ModelAttribute가 Controller 메소드의 매개변수로 선언된 Command 객체의 긴 이름을 짦은 이름으로 변경할때도 사용되지만(해당 포스트는 여기를 클릭),
Controller 클래스에 있는 특정 데이터를 View(.jsp 페이지)에서 사용할수 있도록 View로 넘기는 용도로도 사용되는 별스런 역할도 할수 있다.
일단 개념부터 정리를 해 보면...

어떤 컨트롤러 클래스 안에있는 특정 메소드에 @ModelAttribute 어노테이션이 붙어 있으면 해당 컨트롤러 클래스의 모든 @RequestMapping 어노테이션이 붙은 메소드가 호출될 때마다 그 메소드 호출 전에 @ModelAttribute가 붙은 메소드가 일단 먼저 호출되고 그 이후 @RequestMapping이 붙은 메소드가 호출되는데 이때 @ModelAttribute 메소드 실행 결과로 리턴되는 객체(데이터)는 자동으로 @RequestMapping 어노테이션이 붙은 메소드의 Model에 저장이되고 그 이후에 .jsp(View)에서 @ModelAttribute 메소드가 반환한 데이터를 사용할수 있다. 
놀라운 @ModelAttribute의 능력이랄까?

일단 코드에서 확인해 보자. 아래와 같은 컨트롤러 클래스가 있다.

@Controller
public class BoardController {
	... 전 략 ...
	
	//글 수정
	@RequestMapping("/updateBoard.do")
	public String updateBoard(BoardVO vo, BoardDAO bdDao) throws Exception {
		System.out.println("UpdateBoardController 글 수정 처리~");
		
		bdDao.updateBoard(vo);
		return "getBoardList.do";
	}
	
	//글 상세 조회
	@RequestMapping("/getBoard.do")
	public String getBoard(BoardVO vo, BoardDAO bdDao, Model model) throws Exception {
		System.out.println("GetBoardController 글 상세 조회 처리~");
		
		model.addAttribute("boardModel", bdDao.getBoard(vo)); 
		return "getBoard.jsp";
	}
	
	//글 목록 검색
	@RequestMapping("/getBoardList.do")
	public String getBoardList(@RequestParam(value="searchCondition", defaultValue="TITLE", required=true) String condition,
								@RequestParam(value="searchKeyword", defaultValue="", required=false) String keyWord,
								BoardVO vo, 
								BoardDAO bdDao, 
								Model model) throws Exception {
		System.out.println("$$$$$$$ GetBoardListController 글 목록 검색 처리~");
		
		model.addAttribute("boardListModel", bdDao.getBoardList(vo));
		return "getBoardList.jsp";
	}
	
	@ModelAttribute("myModelAttribute")
	public Map<String, String> joe() {
		System.out.println("▶▶▶▶▶▶▶ 여기는 joe()~~~ ▶▶▶▶▶▶▶");
		
		Map<String, String> infoMap = new HashMap<String, String>();
		
		infoMap.put("joe", "Web Developer");
		infoMap.put("kim", "Designer");
		infoMap.put("nana", "CEO of M&P");
		
		return infoMap;
	}
}

위의 BoardController 클래스안에는 @RequestMapping 어노테이션이 붙은 메소드가 3개가 있는데
-. public String updateBoard()
-. public String getBoard()
-. public String getBoardList()

이들 메소드가 호출될때마다 @ModelAttribute("myModelAttribute") 어노테이션이 붙은 아래 메소드가 먼저 호출된다.
public Map<String, String> joe()
그리고 joe() 메소드에서 반환하는 데이터(infoMap)가 클라이언트 요청으로 실행될 @RequestMapping이 붙은 메소드의 Model 객체에 자동으로 저장이 된다. 이렇게 저장된 데이터는 .jsp 페이지에서 사용할수 있게 되는데 이때 @ModelAttribute("myModelAttribute") 안에 지정한 문자열인 myModelAttribute가 객체 이름이 된다. 
역시 .jsp에서 어떻게 데이터에 접근하는지 코드에서 확인해 보자.

예를들어 http://xxx.xxx.xx/getBoard.do로 요청이 들어오면 먼저 
public Map<String, String> joe()가 먼저 호출이 되고 그 이후
public String getBoard(BoardVO vo, BoardDAO bdDao, Model model)가 호출이 되는데 이때 joe() 메소드에서 생성된 infoMap 데이터가 getBoard()의 model 객체에 자동으로 저장이 되고 getBoard.jsp에서는 다음과 같이 데이테어 접근할수 있게 된다.

... 전 략 ...
	<h1>글 상세보기</h1>
	<a href="logout.do">로그아웃</a>
	<hr>
	
	구성원1 : ${ myModelAttribute.joe }<br/>
	구성원2 : ${ myModelAttribute.nana }<br/>
	<hr/>
... 후 략 ...

위 .jsp 페이지의 출력 결과는 다음과 같이 될 것이다.

구성원1 : Web Devloper 
구성원2 : CEO of  M&P 

혹은 다음과 같이도 할수 있다.

	<c:forEach items="${ myModelAttribute }" var="item">
		item.key : ${item.key }<br/>
		item.value : ${item.value }<br/><br/>
	</c:forEach>
	<hr/>


그러면 다음과 같은 결과가 나올 것이다.

item.key : joe
item.value : Web Developer

item.key : nana
item.value : CEO of M&P

item.key : kim
item.value : Designer

@ModelAttribute 어노테이션이 이런 용도로도 사용될수 있다는 점이다. 
그런데 @ModelAttribute는 이것 외에 또 있으니 @SessionAttribute와 연결되면 또 요술을 부린다.

ModelAndView의 setViewName() 메소드에 redirect: 사용하는 법

ModelAndview는 Model 정보(DB로 부터 획득한 데이터 정보)와 View 정보(이동할 페이지의 .jsp 파일 정보)를 같이 담아서 넘기는 클래스인데 ViewResolver와 같이 엮이게 되고 그 중에서 ModelAndview.setViewName()에서 redirect:가 붙을 경우와 그렇지 않을 경우 .jsp 파일을 찾는 개념에 약간의 헛갈림이 있을수 있다. 이점에 대해서 정리하고자 한다.

상황 1. InternalResourceViewResolver의 prefix에 설정된 경로와 suffix의 설정 값이 다음과 같고

	<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/board/"></property>
		<property name="suffix" value=".jsp"></property>
	</bean>


상황 2. Context path가 다음과 같을 때(request.getContextPath()에서 출력된 정보가 다음과 같을 때)

/BordWebDay3Class05

따라서 full url은 다음과 같이 될 것이다.
http://localhost/BordWebDay3Class05/

상황 3. /webapp/WEB-INF/board/ 아래에 다음의 .jsp 파일들이 있다.
getBoard.jsp
getBoardList.jsp

ModelAndView.setViewName()에서 redirect:가 붙지 않으면 무조건 InternalResourceViewResolver가 설정한 prefix와 suffix 정보가 적용된 .jsp 파일을 찾고, 
redirect:가 붙으면 InternalResourceViewResolver 설정 정보는 무시되고 Context path 위치에서 .jsp 파일을 찾는다.
예를들어서 

ModelAndView.setViewName("getBoardList");로 되어 있으면 
http://localhost/BordWebDay3Class05/WEB-INF/board/getBoardList.jsp를 찾게 되고
(물론 /WEB-INF/board/getBoardList.jsp는 브라우저에서 직접 호출할수는 없다. 왜냐하면 /WEB-INF/ 아래에 있는 .jsp 파일은 브라우저에서 직접 호출이 안되기 때문이다)

ModelAndView.setViewName("redirect:getBoardList.jsp");로 되어 있으면
http://localhost/BordWebDay3Class05/getBoardList.jsp를 찾게 된다.

redirect:가 붙어있을 경우는 InternalResourceViewResolver가 동작하지 않고 그 반대는 InternalResourceViewResolver에 설정된 prefix 경로(정보)와 suffix 정보가 합쳐져서 .jsp 파일을 찾는다는 점을 기억하도록 하자.

만일 web.xml의 서블릿 url-pattern이 아래와 같은 상황 일때,

  <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

ModelAndView.setViewName("getBoard.do");로의 View 설정의 경우는 ViewResolver가 동작하지 않도록 redirect:를 사용해야 한다. 그렇지 않으면 url이
/WEB-INF/board/xxx.do.jsp
와 같이 찾기 때문에 404 Not Found 에러가 발생하고 멈춰 서 버린다. 따라서 ModelAndView.setViewName("getBoard.do");와 같은 경우는 ViewResolver가 동작하지 않도록 redirect:를 사용해서 ModelAndView.setViewName("redirect:getBoard.do");와 같이 해야한다.

AOP(Aspect Oriented Programming) 용어 정리

S/W에서 클래스간의(객체간의) 결합도가 높을 경우 그들 중 어느 한 클래스(객체)에 수정이 발생하면 해당 클래스(객체)가 사용된 모든 소스 코드를 수정해야 하고 이러한 수정은 S/W 유지 보수의 측면에서 뜻하지 않은 문제를 발생시키거나 유지 보수를 복잡하고 어렵게 만드는 요인이 된다. 따라서 가능한 객체들간의 결합도를 낮추는 방향으로 가야 한다.

Spring에서 결합도를 낮추는 기법이 의존성 주입(IoC)과 AOP가 있는데 AOP의 용어를 명확히 핵심적으로 정리하고자 한다.
IoC와 AOP는 개발자가 소스코드에서 해 주지 않아도 Spring 컨테이너가 알아서 처리해 주므로 인해 소스코드 수정을 하지 않아도 된다는 개념이 핵심이다. 그렇게 하도록 하기 위해 필요한 것이 Spring 설정 파일에서의 설정을 통해서 Spring 컨테이너가 처리하도록 하는 식이다.


▶ 횡단 관심(Crosscutting Concerns)
비지니스 메소드마다 공통으로 등장하는 코드를 의미(예외, 로깅, 트랜잭션같은 코드).

▶ 핵심관심(Core Concerns)
핵심 비지니스 로직을 의미.

▶ Joinpoint
모든 비지니스 메소드을 의미

▶ Pointcut
모든 비지니스 메소드들 중에서 횡단 관심 코드를 수행하기 원하는 "특정 비지니스 메소드"를 의미

▶ Advice 
횡단관심에 해당하는 코드를 담고 있는 메소드를 의미

▶ Aspect
Pointcut과 Advice의 결합(어떤 Pointcut 메소드에 대해 어떤 Advice 메소드를 실행할지를 정의)

▶ Weaving 
Advice가 삽입되는 과정

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
비지니스 로직 메소드                            횡단관심 메소드
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Joinpoint                                             Advice
Pointcut
                                Weaving
                                 Aspect
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 


Spring(혹은 전자정부프레임워크)에서 maven build가 정상적으로 수행된 후에 『Run As - Java Application』으로 해당 클래스를 실행했을 때 다음과 같은 에러가 발생했다면 무엇이 문제라는 것일까?


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'messageBean' defined in class path resource [context-hello.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.joe.MessageBean]: Specified class is an interface

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1105)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1050)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)

at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)

at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)

at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)

at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)

at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)

at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)

at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)

at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

at com.joe.HelloApp.main(HelloApp.java:10)

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.joe.MessageBean]: Specified class is an interface

at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:68)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1098)

... 13 more


위의 에러메시지에서 몇 가지 단서들을 확인할수 있는데 

 -. context-hello.xml에 있는 messageBean이라는 이름의 bean 객체를 생성하지 못했다는 것이고

    Error creating bean with name 'messageBean' defined in class path resource [context-hello.xml]

 -. bean을 생성하기 위해 지정된 클래스가 interface이기 때문에 bean(객체)를 생성할수 없다

    Failed to instantiate [com.joe.MessageBean]: Specified class is an interface


참고로 context-hello.xml 파일의 내용은 다음과 같다. 


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">


<bean name="messageBean" class="com.joe.MessageBean" />


</beans>


그러면 bean을 생성할 대상이 되는 클래스인 com.joe.MessageBean를 보면 다음과 같다.


package com.joe;


public interface MessageBean {

public void sayHello(String name);

}


따라서 결국은 문제가 된 것은 context-hello.xml에서 bean 생성 할 대상으로 지정한 클래스가 interface여서 발생한 에러였다. Java는 근본적으로 interface를 막바로 객체(bean) 생성할수가 없다. 만일 객체를 생성할수 있다고 한들 그 객체가 행할수 있는 기능(메소드) 자체가 정의되어 있지 않기 때문에, 위에서 보듯이 sayHello()라는 메소드가 무엇을 행할지 내용이 없다. 이것이 interface이다. 


따라서 interface는 객체를 막바로 생성할수가 없고 생성할수 있다고 해도 그 객체(bean)이 아무런 동작도 할것이 없는것이다.

따라서 interface를 구현한(implements) 클래스를 객체로(bean)으로 생성하도록 context-hello.xml의 내용을 변경해 주어야 한다.


여기서 MessageBean 인터페이스를 구현한 하위 클래스는 다음과 같다.


package com.joe;


public class MessageBeanEng implements MessageBean {

@Override
public void sayHello(String name){

System.out.println("Hello, "+name);

}

}


따라서 context-hello.xml의 내용을 바꾸어 주어야하는데 interface인 MessageBean을 그 구현 클래스인 MessageBeanEng로 변경해 주어야 하는 것이다.


<bean name="messageBean" class="com.joe.MessageBean" />


를 아래와 같이 바꾸어 주어야 한다.


<bean name="messageBean" class="com.joe.MessageBeanEng" />


+ Recent posts