PHP로 사이트를 개발하다보면 특정 변수의 값을 브라우저가 아닌 브라우저 콘솔에 출력해서 확인해 보고 싶은 때가 있다.
그런데 만일 PHP 소스에서 다음과 같이 하면

echo "<h1>Hello world</h1>";

이건 현재 웹 페이지에 Hello world라는 문자열을 큰 글씨로 출력하게 된다. 이 말인즉은 사용자가 특정 웹 페이지로 이동할때 갑자기 저 문구가 뜬금없이 큰 글씨로 사용자 웹 브라우저에 보여지게 된다는 것이다.
즉 echo로 출력하게 되면 브라우저 콘솔이 아닌 브라우저 화면 자체에 출력이 된다.
그러면 일반 사용자에게는 보이지 않고 콘솔에 출력할려면 어떻게 해야 되는가?
다음과 같이 하면 된다.

echo '<script>';
echo 'console.log(“Hello world”)’;
echo '</script>';

그러면 이번에는 특정변수의 값을 출력할려면 어떻게 해야 하는가? 아래와 같이...

echo '<script>';
echo 'console.log("'.$sql_notice.'")';
echo '</script>';

그러면 이번에는 콘솔이 아닌 alert()을 이용해서 화면에 출력할려면? 아래와 같이...

echo '<script>';
echo 'alert("Yes, Mobile~");';
echo '</script>';

그러면 alert()에 특정 변수의 값을 출력할려면? 아래와 같이..

echo '<script>';
echo 'alert("isMobile : '.$isMobile .'");';
echo '</script>';

그러면 이번에는 콘솔에 특정 변수 하나의 값이 아닌 배열의 내용을 출력할려면?
다른 곳에서 퍼온 내용인데 아주 유용하다. 예를 들어서 DB에서 가져온 배열의 내용이 $result에 담겨 있을 때 이를 출력할려면 print_r($result)로 하면 화면 상에서 쉽게 확인이 되지만 실제 운영중인 사이트의 경우는 참으로 곤란해 진다. 이때 아래와 같이 하면 콘솔 상에서 배열에 담긴 많은 내용을 사용자 화면에 아무런 영향을 주지 않고 콘솔 상에서 쉽게 확인이 가능하다.

$result에 DB에서 가져온 값이 배열로 담겨 있다고 할때, 아래와 같이

//$result에 담긴 배열 값 콘솔에 출력하기
echo "<script>\r\n//<![CDATA[\r\nif(!console){var console={log:function(){}}}";
$arr = explode("\n", print_r($result, true));

foreach ($arr as $temp) {
  if (trim($temp)) {
      $temp = addslashes($temp);
      echo "console.log(\"{$temp}\");";
  }
}
echo "\r\n//]]>\r\n</script>";

위의 경우는 배열의 key-value중 value 값만 출력한 경우라면 이번에는 배열의 key-value 형태로 출력하는 경우를 보자.

//배열을 console에 출력하기($result에 배열 형태의 데이터가 있을 때 key-value 형태로 출력하기)
echo "<script>\r\n//<![CDATA[\r\nif(!console){var console={log:function(){}}}";
foreach ($result as $key => $line) {
        $key = addslashes($key);
        $line = addslashes($line);
        echo "console.log(\"${key} : {$line}\");";
}
echo "\r\n//]]>\r\n</script>";   
 

다른 PC의 이클립스에서 개발한 프로젝트를 새로운 PC의 이클립스로 import할 경우 원래 PC의 개발환경과 JDK, Tomcat... 등등 환경이 다른 관계로 인해 프로젝트에 빨간색 x 박스가 뜨는 경우가 허다하다.
이를경우 해법은 

1) 현재 PC의 JDK 설치 경로 맞춰주기
프로젝트 위에서 마우슨 우측 클릭 ⇒ 팝업 메뉴에서 Build Path 선택 ⇒ Configure Build Path ⇒ Java Build Path 창의 Libraries 탭 선택 ⇒ JRE System Library 항목 선택 ⇒ 우측 Eidt… 버튼 클릭 ⇒ JRE System Library 창에서 3가지 radio 버튼 항목 중 “Workspace default JRE(Java SE …) 항목 클릭 ⇒ Finish ⇒ Apply

2) .jar 파일이 없다는 경우(maven dependency 에러) pom.xml에서 맞춰주거나 혹은 해당 .jar 파일을 다운 받아서 추가하기
webapp/WEB-INF/lib 폴더(lib 폴더 없으면 생성)에 원하는 .jar 파일 복사 ⇒ 프로젝트 위에서 마우스 우측 클릭 ⇒ 팝업 메뉴에서 Build Path 선택 ⇒ Configure Build Path ⇒ Java Build Path 창의 Libraries 탭 선택 ⇒ 우측 “Add JARs…” 버튼 클릭 ⇒ 새로운 창에서 복사했던 .jar가 있는 webapp/WEB-INF/lib/ 아래의 해당 .jar 선택 ⇒ OK ⇒ Apply

3) 아래와 같은 에러가 발생시에는 
Target runtime jre1.8.0_161 is not defined. MybatisProject Unknown Faceted Project Problem

프로젝트에 에러가 발생했을 때는 구체적으로 어디서 어떤 에러인지에 대한 정보를 이클립스가 제공해주는 데(예를들어 위와 같은 에러) 그것 볼려면 메뉴에서 Window - Show View - Problems 메뉴를 선택하면 에러에 대한 자세한 정보를 볼수 있다.
위와 같은 에러의 경우는

프로젝트 위에서 마우스 우측 클릭 ⇒ Properties ⇒ Java Build Path 창의 좌측 항목들 중 Project Facets 항목 선택 가운데 Project Facet 항목들 중 Java 항목 선택 ⇒ 현재 PC에 설치된 Java 버전과 동일한 버전이 선택됐는지 확인 ⇒ 우측 Details와 Runtimes 탭 중에서 Runtimes 탭 선택 ⇒ 현재 개발 PC에 설치되어 있는 Java 버전에 맞는 항목 선택

나의 경우는 jre1.8.0.202가 설치되어 있었는데 체크박스 체크된 jre 버전은 1.8.0.161이 선택되어 있어서 발생한 에러였다.

Controller 클래스 수행후 이동하게 될 .jsp를 찾는 일은 Spring 기반의 프로젝트에서 늘상하게 되는 일인데 @RequestMapping에 의해서 특정 메소드의 리턴 타입이 String 타입이 있는가하면 void 타입도 있다. 이들 각각의 경우에 이동하게 될 .jsp를 찾능 원리에 대해 정리하고자 한다.

아래와 같이 메소드의 리턴타입이 String 일 경우는

@Controller
public class SomeController {
	
	@RequestMapping("/product")
	public String doD(Model model) {
		
		return "productDetail"; 
	}
}

이 경우 이동하게 될 .jsp는 productDetail.jsp로 이동하게 된다. 

아래와 같이 메소드의 리턴 타입이 void 인경우

@RequestMapping(value = "/info", method=RequestMethod.GET)
public void show(@RequestParam("seq") int seq, @ModelAttribute("myMEM") MemberInfo info, Model model) {

}

이 경우는 리턴 타입이 void인데 이 메소드 실행후 어떻게 .jsp로 넘어가는가?
리턴 타입이 void 일 경우 .jsp 페이지로 넘어가는 규칙은 접근하는 url 경로에 해당하는 .jsp를 찾는다. 이 경우 RequestMapping이 지정한 경로인 /info에 근거해서 info.jsp를 찾아서 실행한다. 

이때 info.jsp를 찾을 때 기본적으로 webapp/ 아래에서 찾지만 이건 정확히 말하면 web.xml에서 DispatcherServlet이 로딩하는 서블릿 컨테이너의 설정 파일이 지정한 위치에서 찾는다.
예를들어서 web.xml의 DispatcherServlet의 설정 내용이 다음과 같을 경우

	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
	</servlet>

DispatcherServlet 클래스가 사용할 서블릿 컨터이너의 설정 파일은 아래 위치라는 뜻이고 
/webapp/WEB-INF/spring/appServlet/servlet-context.xml

/webapp/WEB-INF/spring/appServlet/servlet-context.xml의 내용은 다음과 같다고 한다면

	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

즉 prefix가 지정한 위치(webapp/WEB-INF/views/) 아래에서 suffix가 지정한 확장자를 붙여서 view 페이지(.jsp)를 찾는다. 결론적으로 webapp/WEB-INF/views/info.jsp를 찾는다는 뜻이다.




본 포스트는 네이버페이 연동하면서 진행했던 내용을 중심으로 정리하고자 한다.
다음과 같은 XML 데이터를 생성해서 응답하는 기능을 구현 한다고 가정해 보자.

<response>
   <item id="xxx">
      <name><![CDATA[ 상품명 ]]></name>
      <url>http://xxx.xxx.com/mainNew/.../xxx.php</url>
      <description><![CDATA[...상세설명...]]></description>
      <image>
         http://xxx.xxx.com/xxx_file/xxx.jpg
      </image>
      <thumb>
         http://xxx.xxx.com/xxx_file/xxx.jpg
      </thumb>
      <price>27000</price>
      <quantity>100</quantity>
      <category>
         <first id="xxx">대분류</first>
         <second id="xxx">중분류</second>
         <third id="">소분류</third>
      </category>
   </item>
</response>

이때 주의해야 할 사항 및 요령은
    
① 엘리먼트 값으로 한글이 들어가야 하는 부분에 대해 character encoding 처리를 잘 해줘야 한다. 
위의 경우는 name, description 엘리먼트에 한글 값이 설정되는 부분이다. 그리고 통상적으로 XML를 생성할 때 XML의 엘리먼트에 들어갈 값은 DB로부터 획득해서 설정하게 될 것이다. 이때 DB에 저장된 한글 데이터가 EUC-KR인지 UTF-8인지 잘 분별해서 엘리먼트에 값이 들어가도록 해야한다. 만일 DB에 저장된 데이터가 EUC-KR인데 XML 생성은 UTF-8로 한다면 다음과 같이 처리해야 한다. 그렇지 않으면 XML 생성시 Encoding error라는 XML parsing 에러가 발생되고 XML이 생성되지 않는다.

$nameFromDB = "여기에 DB로부터 가져온 name의 값이 들어있다고 하면";
$name = iconv("EUC-KR", "UTF-8", $nameFromDB);

$nameFromDB에 있는 EUC-KR 타입의 한글을 UTF-8로 변경해서 $name에 저장한다.

$descriptionFromDB = "여기에 DB로부터 가져온 description의 값이 들어있다고 하면";
$description = iconv("EUC-KR", "UTF-8", $descriptionFromDB);

② http url 정보가 들어가야 하는 엘리먼트에서(위의 경우 url, image, thumb) http url에 &가 포함되어 있을 경우 &라는 특수문자는 &문자 자체로 인식되지 않고 특수한 기능을 하는 문자로 취급되기 때문에 EntityRef: expecting ';'라는 에러 발생한다. 예를들어서 다음과 같은 경우이다.

http://xxx.xxx.com?id=1234&tid=3456
이를 경우 str_replace()함수를 이용해서 &를 &로 바꿔줘야 한다.

$itemUrl = str_replace("&", "&", $원본데이터);

③ XML을 생성중 다음과 같은 에러가 발생했다면 왜, 어디서, 어떤 이유때문에 에러가 발생했는지를 쉽게 파악할 수 있는 방법이 있다.
This page contains the following errors:
error on line 4 at column 19: xmlParseEntityRef: no name
Below is a rendering of the page up to the first error.

이 경우 웹 브라우저의 해당 페이지에서 마우스 우측 클릭하여 "페이지 소스 보기"(크롬의 경우)를 하면 아래와 같은 내용이 표시되는 4번 라인에서 url 엘리먼트에 특수문자들이 들어가 있다(아래의 경우는 에러를 의도적으로 발생시키기 위해 이 값들을 넣은 경우이다). 이를 경우 urlencode() 함수 등으로 특수 문자들을 변환해줘야 한다.

<?xml version="1.0" encoding="euc-kr"?><response>
<item id="itemid">
	<name><![CDATA[상품 명]]></name> 
	<url>http://"/:@&%=?.#"#$%=+/test.html</url> 
	<description><![CDATA[간지나는 아이템]]></description> 
	<image>http://localhost/test.jpg</image> 
	<thumb>http://localhost/test.jpg</thumb> 
	<price>1000</price> 
	<quantity>1</quantity>
	<category>
		<first id="MJ01">대분류</first>
		<second id="ML01">중분류</second>
		<third id="MN01">소분류</third> 
	</category>
</item> 
</response>

이제 본격적으로 PHP로 위의 예시와 같은 XML을 생성하는 코드를 작성해 보자.

<?php
    header('Content-Type: application/xml;charset=utf-8'); 
    echo ('<?xml version="1.0" encoding="utf-8"?>');
?>

<response>

<?php
    $dbData = "위의 XML예시에서 XML에 들어갈 item 요소가 여러개 일 경우의 데이터";	

    //아래와 같이 반복문을 통해 위의 예시의 XML에서 복수개의 item 항목을 가진 XML을 만들고자 할 경우
    for($i=0; $i<count($dbData); $i++) {
        $price = "DB로부터 가져온 가격 데이터";
        $quantity = 50;
        $itemUrl = "http://xxx.xxx.com/...";
        $imgUrl = "http://xxx.xxx.com/...";

        $nameFromDB = "여기에 DB로부터 가져온 name의 값이 들어있다고 하면";
        $name = iconv("EUC-KR", "UTF-8", $nameFromDB);

        $descriptionFromDB = "여기에 DB로부터 가져온 description의 값이 들어있다고 하면";
        $description = iconv("EUC-KR", "UTF-8", $descriptionFromDB);
?>

<item id="<?=$id?>">
	<name><![CDATA[<?=$name?>]]></name> 
	<url><?=$itemUrl?></url> 
	<description><![CDATA[<?=$description?>]]></description> 
	<image><?=$imgUrl?></image> 
	<thumb><?=$imgUrl?></thumb> 
	<price><?=$price?></price> 
	<quantity><?=$quantity?></quantity>
	<category>
		<first id="<?=$product[0][bdr_category_code1]?>">대분류</first>
		<second id="<?=$product[0][bdr_category_code2]?>">중분류</second>
		<third id="<?=$product[0][bdr_category_code3]?>">소분류</third>
	</category>
</item> 
<?php
	} //for
	//end while;
	echo('</response>');
?>

이렇게 하면 PHP에서 간단하게(?) XML을 생성해서 클라이언트들의 request에 응답할 수 있다. 
이상의 내용은 네이버페이 연동 개발을 하면서 진행했던 내용이었다.

통상적으로 전체적인 색상 변경을 한번에 처리하는 방법이 테마변경을 통해서 이뤄지지만 선택한 테마가 모든 면에서 내 입맛에 맞지 않을수도 있어서 마우스로 클릭한 단어만 특정 색상으로 변경한다든지 특정 태그를 선택했을 때 해당 태그의 쌍을 특정 색상으로 변경해서 그 태그의 범위가 어디서 어디까지 인지 등 특정 부분에 대해서만 가독성 향상을 위한 색상 변경할때 아래와 같은 방법을 이용하면 된다.

테마 변경은 Help - Eclipse marketplaces...로 들어가서 Find 항목에 Theme로 검색해서 나오는 결과 중 Eclipse Color Theme 1.0.0을 보통 Install해서 사용하고 설치된 Theme을 적용할때는 Window - Preferences - General - Appearance - Color Theme으로 들어가서 원하는 테마를 적용하면 된다.

이렇게 지정된 테마에서 특정 요소만 색상 변경할때 아래를 참조

(1) 태그의 쌍을 쉽게 구분하고자 할때(.html, .jsp 파일에 적용)
상단의 메뉴 중 Window - Preferences - General - Editors - Text Editors - Annotations - Matching Tags 
⇒  태그의 쌍을 같은 색으로 변경해서 눈에 잘 띄게 하는 기능
예를들어서

<form> ... </form>

 두 개의 form 태그의 쌍을 어떤 색상으로 변경할지를 지정. 보틍 html이나 .jsp 파일에서 가독성 높이기 위한 방법이다. 이렇게 변경하면 form 태그의 범위가 어디서 어디까지 인지를 쉽게 구분할수 있다.

(2) 클릭한 특정 단어만 원하는 색상으로 변경하고자 할때(.java 파일에 적용)
상단의 메뉴 중 Window - Preferences - General - Editors - Text Editors - Annotations - Occurrence
⇒  .java 파일에서 특정 단어를 클릭했을 때 해당 단어와 동일한 단어의 색상을 모두 특정 색상으로 변경하고자 할때 사용

(3) 모든 태그의 색상을 원하는 색으로 변경하고자 할때(.html, .jsp 파일에 적용)
상단의 메뉴 중 Window - Preferences - Web - HTML Files - Editor - Systax Coloring - Tag Names 
⇒  .html 파일에서 모든 태그들의 foreground color 색상을 변경할 때

가독성 향상을 위해 이정도면 거의 만족스런 결과를 얻을 수 있을 것이다. 그 외의 기능들은 위의 경로로 들어가서 테스트해 보면 될 것이다.

@ModelAttribute가 하는 역할이 다양한데 
-. Command 객체의 이름을 변경하여 View에 넘기기 (해당 포스트는 여기를 참조)
-. Controller 클래스에 있는 데이터를 View로 넘기기 (해당 포스트는 여기를 참조)
-. @SessionAttribute가 세션에 저장한 데이터를 @ModelAttribute가 가져다 사용하기

이번 글에서는 이들 중에서 세번째에 대해서 다룬다.

@SessionAttribute("myData")의 의미
-. 특정 컨트롤러 클래스에 @SessionAttribute가 선언되어 있으면 
-. 해당 Controller 클래스의 특정 메소드에서 "myData"라는 이름으로 Model 객체에 저장되는 데이터가 있다면 그 데이터를 세션(HttpSession)에도 자동으로 저장하라는 어노테이션이다.
-. ex)

@Controller
@SessionAttributes("myData")
public class BoardController {
	
	@Autowired
	private BoardService boardService;

... 중 략 ...

	//글 상세 조회
	@RequestMapping("/getBoard.do")
	public String getBoard(BoardVO vo, Model model) throws Exception
	{
		System.out.println("GetBoardController 글 상세 조회 처리~");
		
		//아래와 같이 Model 객체에 myData라는 이름으로 저장하면
		//HttpSession에도 boardService.getBoard(vo)의 반환 데이터가
		//자동으로 저장이 된다.
		model.addAttribute("myData", boardService.getBoard(vo)); 
		
		return "getBoard.jsp";
	}

... 후 략 ...

} //class BoardController

위 코드에서와 같이 컨트롤러 클래스인 BoardController 클래스에 @SessionAttributes("myData")어노테이션이 적용되어 있으면 이 컨트롤러 클래스의 어느 메소드에서  Model에 "myData"라는 이름으로 데이터를 저장하면(여기서는 boardService.getBoard(vo)를 저장) 그 데이터를 세션(HttpSession)에도 자동으로 저장하게된다.
그러면 이렇게 세션에 저장된 데이터를 누가 어디서 어떤식으로 꺼내서 사용하는가?
그 역할 하는 것이 다름아닌 @ModelAttribute이다. 아래 코드와 같이

	//글 수정
	@RequestMapping("/updateBoard.do")
	public String updateBoard(@ModelAttribute("myData") BoardVO vo) throws Exception
	{
		System.out.println("UpdateBoardController 글 수정 처리~");
		System.out.println("▶ vo.getSeq(): "+vo.getSeq());
		System.out.println("▶ vo.getTitle(): "+vo.getTitle());
		System.out.println("▶ vo.getWriter(): "+vo.getWriter());
		System.out.println("▶ vo.getContent(): "+vo.getContent());
		System.out.println("▶ vo.getRegDate(): "+vo.getRegDate());
		System.out.println("▶ vo.getCnt(): "+vo.getCnt());
		
		boardService.updateBoard(vo);
		return "getBoardList.do";
	}

위의 updateBoard()메소드에 @ModelAttribute("myData") BoardVO vo가 내부적으로 동작하는 내용은
① 세션에 myData라는 이름으로 저장된 데이터가 있는지 확인하여(세션에는 key-value 형태로 저장된다), 데이터가 있으면 그 데이터를 세션에서 꺼내서 @ModelAttribute가 붙은 매개변수에(여기서는 BoardVO 객체인 vo에) 자동으로 저장한다. 세션에 해당 데이터가 없다면 당연히 이 과정을 일어나지 않을 것이다.

그런데 여기서 위의 updateBoard(@ModelAttribute("myData") BoardVO vo) 메소드는 게시글 수정시 호출되는 메소드이다.
따라서 게시글 수정은 기본적으로 form 형태로 이전 글 내용이 보여지고 여기서 수정하고자 하는 항목을 수정후 해당 form을 commit하는 방식이 될것이다(아래 form).
이러한 경우 Command 객체 개념이 BoardVO vo에 적용이된다. 이때 updateBoard() 메소드에는 세션에 저장된 데이터가 BoardVO vo에 저장이 되면서 동시에 form으로부터 넘어온 데이터가 Command 객체인 BoardVO vo에 또한 저장이된다. 이런 상황에서 세션 데이터와 form에서 넘어온 데이터가 어떤 식으로 Command 객체인 BoardVO vo에 저장이 되는가 하는 것이다.

Command 객체란
Controller 클래스(위에서는 BoardController)의 메소드에 매개변수로 VO 객체가 선언되어 있을때(위에서는 BoardVO vo) 이를 Command 객체라고 한다. 위에서는 updateBoard() 메소드의 BoardVO vo가 Command 객체이다. Spring 컨테이너는 이러한 Command 객체에 대해서는 내부적으로 다음과 같은 특별한 처리를 자동으로 해준다. 
-. Command 객체인 BoardVO vo 객체를 자동으로 생성하고
-. form 태그에서 입력한 값들을 추출하여 BoardVO 객체인 vo에 저장한다. 이때 BoardVO 클래스의 setter 메소드들이 호출된다.
여기서 중요한 것은 form 태그의 name 속성에 지정한 이름과 BoardVO의 멤버 변수 이름이 동일해야 한다. 
아래의 form의 
name="title"
name="content"과 VO의 멤버 변수 이름이 동일해야 한다.

	<form action="updateBoard.do" method="post">
		<table border="1" cellpadding="0" cellspacing="0">
			<tr>
				<td bgcolor="orange" width="70">제목</td>
				<td align="left"><input type="text" name="title" value="${ joe_Board.title}" /></td>
			</tr>
			<tr>
				<td bgcolor="orange">작성자</td>
				<td align="left">${ boardModel.writer}</td>
			</tr>
			<tr>
				<td bgcolor="orange">내용</td>
				<td align="left"><textarea name="content" cols="40" rows="10">${joe_Board.content}</textarea></td>
			</tr>
			... 후 략 ...
		</table>
	</form>

VO의 멤버 변수 이름이 form 태그의 name 속성에서 지정한 이름과 동일해야 한다. 아래와 같이

public class BoardVO {
	private String title;
	private String content;

	public String getTitle() { return title; }
	public void setTitle(String title) { this.title = title; }
	public String getContent() { return content; }
	public void setContent(String content) { this.content = content; }
... 후 략 ...
}

② 따라서 세션에 myData라는 이름으로 저장된 데이터를 BoardVO 객체에 할당 이후에 사용자가 게시글 수정한 정보(form 태그 안에 있는 데이터)가 Command 객체인 BoardVO 객체에 덮어쓰기로 할당이 된다.

이상이 @ModelAttribute가 @SessionAttribute와 연동할때 내부적으로 데이터가 어떻게 처리되는지를 살펴보았다.
스프링은 많은 것을 내부에서 처리하는 자동화 메커니즘을 가지고 있기 때문에 개발자는 이러한 자동화 메커니즘을 잘 숙지하고 익숙해 있어야 한다.

아래 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와 연결되면 또 요술을 부린다.

아래와 같은 컨트롤러 클래스가 있을 경우 return "getBoardList.do" 혹은 
return "getBoardList.jsp"가 갖는 의미가 무엇인가?

@Controller
public class InsertBoardController {

	@RequestMapping(value="/insertBoard.do")
	public String insertBoard(BoardVO vo, BoardDAO bdDao) {
		System.out.println("InsertBoardController 글 등록 처리~");
		
		bdDao.insertBoard(vo);
		
		return "getBoardList.do";  ① 
//		return "getBoardList.jsp"; ②
	}
}

MVC에서 Controller는 기본적으로 Model에 대한 처리(DB로 부터 적정 정보 획득)와 이후 이동할 페이지 정보(View 정보)를
return 하는 역할이 Controller가 하는 역할이다.

따라서 return "getBoardList.do"가 갖는 의미도 Model에 대한 처리(bdDao.insertBoard(vo);)와 이후 이동할 페이지 정보 
즉 View 정보를 반환하는 역할의 의미가 return "getBoardList.do";인 것이다. 

따라서 Context Path가 member라고 한다면 return "getBoardList.do";의 실행은 http://localhost/member/getBoardList.do와 같은 url 형태의 request 요청이 발생하게 하는 역할이 return "getBoardList.do";의 의미이다.

return "getBoardList.jsp";의 경우는 /member/getBoardList.jsp를 막바로 호출하는 형태라고 한다면 return "getBoardList.do";의 경우는 getBoardList.do에 해당하는 특정 컨트롤러의 특정 메소드 실행후 그 특정 메소드가 지정하는 .jsp 페이지로의 이동을 의미하게 된다.

참고적으로 Controller 메소드가 return 하는 View 정보는 기본적으로 포워딩 방식으로 동작한다. 
따라서 사용자의 브라우저 주소줄에 
http://xxx.xxx.xx/member/insertBoard.do로 요청에 대해서 
return "getBoardList.do"; 해도 
사용자의 브라우저 주소줄은 http://xxx.xxx.xx/member/insertBoard.do로 변함이 없다. 
만일 포워딩이 아니라 리다이렉트 방식으로 동작하게 할려면 
return "redirect:getBoardList.do";와 같이 해야 한다.  
그러면 사용자의 브라우저 주소줄은 http://xxx.xxx.xx/member/getBoardList.do와 같이 변경되어 나타날 것이다.

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");와 같이 해야한다.

+ Recent posts