2020년 3월 12일 목요일

Java DAO와 MyBatis Mapper xml의 쿼리문과의 매핑 원리

아래와 같은 Mapper xml의 쿼리 문이 있다고 할 경우,

<insert id="insertMember">
insert into tbl_member (userid, userpw, username, email) values
(#{userid}, #{userpw}, #{username}, #{email})
</insert>

VO 클래스는 다음과 같고,

public class MemberVO {
    private String userid; 
    private String userpw;
    private String username;
    private String email;
    private Date regdate;
    private Date updatedate;

    ... 이하 getter, setter는 생략 ...
}

DAO에서 Mapper xml에 있는 쿼리 실행을 다음과 같이 한다고 할때,

public void insertMember(MemberVO vo) {
    sqlSession.insert(namespace + ".insertMember", vo);
}

이럴때 sqlSession.insert(namespace + ".insertMember", vo)에서 vo 객체를 Mapper로 넘기면 #{userid}, #{userpw}, #{username}, #{email} 값들과 vo가 어떻게, 어떤 원리에 의해서 매핑이 되는가?

SqlSession 클래스의 insert() 메소드를 보면 다음과 같이 API 설명이 되어 있다.

int insert(String statement, Object parameter)
   ==>Execute an insert statement with the given parameter object. Any generated autoincrement
   ==>values or selectKey entries will modify the given parameter object properties.
   ==>Only the number of rows affected will be returned.
   Parameters:
     -. statement : Unique identifier matching the statement to execute.
     -. parameter : A parameter object to pass to the statement.
   Returns:
     -. int : The number of rows affected by the insert.

이 메소드의 첫 번째 매개인자인 statement는 이 statement의 문자열과 1:1로 대응되는 Mapper xml 파일에 있는 쿼리문을 지칭하게 된다. 이때 Mapper의 id값과 statement의 문자열이 동일한 쿼리문에 매핑된다.
위의 예에서는 Mapper xml 파일에서 id가 insertMember인 쿼리문을 실행하게 된다.

<insert id="insertMember">
insert into tbl_member (userid, userpw, username, email) values
(#{userid}, #{userpw}, #{username}, #{email})
</insert>


두 번째 매개인자인 parameter는 첫 번째 매개인자인 statement가 가리키는 Mapper 클래스의 쿼리문을 실행할 때 이 쿼리문에서 사용할 변수들에(#{userid}, #{userpw}, #{username}...) 넘겨줄 값을 담고 있다.
위이 예에서는 MemberVO 클래스의 객체인 vo가 되겠다.
이때 vo가 클래스가 가지고 있는 값들과 #{userid}, #{userpw}, #{username}, #{email} 이 변수들이 어떤 원칙에 의해서 연동되는가 하는 것이다.

이때 두 번째 매개인자인 parameter에는 다음과 같은 다양한 종류들이 가능한데

1) parameter가 하나이고 기본 자료형이나 문자열인 경우 값이 그대로 전달된다.

public MemberVO readMember(String userid) throws Exception {
    return (MemberVO)sqlSession.selectOne(namespace+".selectMember", userid);
}

Mapper xml에서는 userid와 동일한 mapper 변수와 1:1로 대응된다. 이 경우는 #{userid} = userid가 되는 것이다.

<select id="selectMember" resultType="org.zerock.domain.MemberVO">
select
*
from tbl_member
where userid = #{userid}
</select>

2) parameter가 클래스의 객체인 경우 해당 클래스의 getter 메소드에 대응되서 mapper 변수가 값을 획득한다.
위의 vo 객체의 경우인 #{userid}, #{userpw}, #{username}, #{email}는 각각 vo 객체의 getUserid(), getUserpw(), getUsername(), getEmail()을 통해서 이들 각각의 Mapper 변수들의 값을 할당받게 된다.
만일 MemberVO 클래스의 멤버 변수에 dayCalc는 존재하지 않지만 MemberVO 클래스에 getDayCalc()이라는 getter가 만들어져 있다면 Mapper 변수에 #{dayCalc}과 같이 표현하면 정상적으로 값을 가져올 수 있게된다.

3) parameter가 클래스의 객체는 아니나 Mapper로 넘겨야 할 파라미터가 2개 이상일 경우 Map에 담아서 넘기는데 이 경우의 1:1 대응 원칙은 다음과 같다.

public MemberVO readWithPW(String userid, String userpw) throws Exception {
    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("userid",  userid);
    paramMap.put("userpw", userpw);

    return sqlSession.selectOne(namespace+".readWithPW", paramMap);
}

<select id="readWithPW" resultType="org.zerock.domain.MemberVO">
select
*
from tbl_member
where userid = #{userid} and userpw = #{userpw}
</select>

이 경우는 Map 객체의 key값과 Mapper의 변수가 1:1로 대응되서 값이 전달된다.
즉 Map 객체의 key 이름과 Mapper xml의 변수 이름이 동일해야 한다.

이상의 원리를 따라 MyBatis Mapper xml의 쿼리문과의 연동은 SqlSession 클래스가 알아서 매핑 작업을 처리해 준다.

2020년 3월 11일 수요일

MySQL DB dump가 복구되지 않을때

MySQL db dump로 DB 내용을 백업 받은 것을 다시 MySQL에 복구하고자할 때 복구가 되지 않는 경우가 있다.
MySQL db에 접속할 계정의 user id가 root라고한다면 아래와 같은 커맨더가 정상적으로 DB를 복구해야 한다.
(test.sql이 DB 내용을 dump로 백업 받은 파일이고 복구하고자 하는 DB가 testDB라고 할 경우. 아래는 맥, 리눅스에서의 명령어 예이다)

$ mysql -uroot -p testDb < /Users/Documents/test.sql

testDb를 열어서 보면 Empty인 경우가 있다.
이렇게 복구가 안되면 다음을 체크해 볼것

아래의 내용은 커맨드 라인에서가 아닌 phpmyAdmin을 이용해서 복구를 할 경우에 대한 내용이다.
phpinfo()를 실행해서 나온 결과에서 

upload_max_filesize ==> 이 값이 아마도 기본 값 2M로 되어 있을 것이다.
post_max_size ==> 이 값이 아마도 기본 값 8M로 되어 있을 것이다.

이럴경우 test.sql의 크기가 upload_max_filesize에서 설정한 2Mb보다 클 경우 복구가 안될 것이다.
이 경우 php.ini 파일을 열어서 upload_max_filesize의 값을 더 높게 설정해 주어야 한다.

맥이나 리눅스의 경우 /etc/php.ini가 해당 파일의 경로이다.
참고적으로 post_max_size의 값이 upload_max_filesize의 설정 값보다 더 높게 설정해야 한다는 것이다.

자세한 정보는 아래를 참조

1.16 I cannot upload big dump files (memory, HTTP or timeout problems).

Starting with version 2.7.0, the import engine has been re–written and these problems should not occur. If possible, upgrade your phpMyAdmin to the latest version to take advantage of the new import features.

The first things to check (or ask your host provider to check) are the values of max_execution_time, upload_max_filesize, memory_limit and post_max_size in the php.ini configuration file. All of these settings limit the maximum size of data that can be submitted and handled by PHP. Please note that post_max_size needs to be larger than upload_max_filesize. There exist several workarounds if your upload is too big or your hosting provider is unwilling to change the settings:

Look at the $cfg['UploadDir'] feature. This allows one to upload a file to the server via scp, FTP, or your favorite file transfer method. PhpMyAdmin is then able to import the files from the temporary directory. More information is available in the Configuration of this document.

Using a utility (such as BigDump) to split the files before uploading. We cannot support this or any third party applications, but are aware of users having success with it.

If you have shell (command line) access, use MySQL to import the files directly. You can do this by issuing the “source” command from within MySQL:

source filename.sql;

PHP에서 console.log()와 alert()으로 출력하기

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>";   

2020년 2월 20일 목요일

이클립스 프로젝트에 에러 표시(빨간색 x 박스) 해법

다른 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이 선택되어 있어서 발생한 에러였다.

2020년 2월 11일 화요일

Spring Controller 클래스에서 view(.jsp)로 이동하는 규칙

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를 찾는다는 뜻이다.

2020년 2월 6일 목요일

PHP로 XML 생성 방법 및 생성시 주의 사항

본 포스트는 네이버페이 연동하면서 진행했던 내용을 중심으로 정리하고자 한다.
다음과 같은 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()함수를 이용해서 &를 &amp;로 바꿔줘야 한다.

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

③ 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에 응답할 수 있다.
이상의 내용은 네이버페이 연동 개발을 하면서 진행했던 내용이었다.

2020년 1월 17일 금요일

STS 혹은 이클립스에서 JPA 항목이 보이지 않을때 추가하는 방법


JAP 프로젝트 생성시 Project Facets에서 JAP 항목이 보이지 않는 문제가 있다. 이는 중대한 이슈가 JAP와 관련해서 있기 때문에 이클립스 쪽에서 제거한 것으로 나와있다(자세한 건 여기를 참조)

Help 메뉴
 ⇒ Instal new software...
  ⇒ Work with: 항목에 http://download.eclipse.org/releases/oxygen를 입력 후 엔터
   ⇒ 아래 Name 항목 여럿 중에서
          "Web, XML, Java EE and OSGi Enterprise Development" 항목을 펼쳐서
           JPA 관련 모든 항목을 체크한다
           (이때 간단히 찾는 방법은 위의 "type filter text" 항목에 JAA라고 입력)
      ⇒ 이렇게 설치과정을 진행 후 이클립스 재구동

이렇게 하면 정상적으로 JPA 항목이 보일 것이다.

2020년 1월 8일 수요일

이클립스에서 코드의 단어, 태그 등에 대해 가독성 향상을 위한 색상 변경하기

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

테마 변경은 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 색상을 변경할 때

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

2020년 1월 7일 화요일

@SessionAttribute와 @ModelAttribute가 연동될때의 동작 원리와 Command 객체의 동작원리

@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 객체에 key-value쌍으로 저장만 해 놓으면
//.jsp 페이지에서 key 문자열로 저장된 데이터에 접근이 가능하다.
//Model 객체를 넘기는 작업은 스프링 컨테이너가 알아서 처리한다.
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와 연동할때 내부적으로 데이터가 어떻게 처리되는지를 살펴보았다.
스프링은 많은 것을 내부에서 처리하는 자동화 메커니즘을 가지고 있기 때문에 개발자는 이러한 자동화 메커니즘을 잘 숙지하고 익숙해 있어야 한다.

2020년 1월 1일 수요일

Spring의 classpath:의 경로 위치

아래 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


2019년 12월 25일 수요일

@ModelAttribute 어노테이션을 이용한 Controller에서 View로의 데이터 전달

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

2019년 12월 22일 일요일

컨트롤러 클래스에서의 return이 갖는 의미

아래와 같은 컨트롤러 클래스가 있을 경우 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와 같이 변경되어 나타날 것이다.

2019년 12월 20일 금요일

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

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

2019년 12월 10일 화요일

AOP around의 proceed() 메소드 동작에 대한 개념 정리

AOP around의 proceed() 메소드 동작에 대한 개념 정리 

AOP에서 Advice 메소드의 동작 시점(동작 방식)에는  
ㆍbefore : 비지니스 메소드 실행 전에 Advice 메소드 실행

ㆍafter-returning : 비지니스 메소드가 성공적으로 리턴되면 Advice 메소드 동작. 즉 비지니스 메소드가 성공적으로 실행되었을 경우에만 Advice 메소드 동작 

ㆍafter-throwing: 비지니스 메소드 실행중 예외가 발생할 경우 Advice 메소드 실행. 즉 비지니스 메소드가 실행에 실패했을 경우에만 Advice 메소드 실행 

ㆍafter : 비지니스 메소드의 성공 실패와 상관없이 비지니스 메소드 실행 후 무조건 Advice 메소드 동작 

ㆍaround : 비지니스 메소드 실행 전과 실행 후 Advice 메소드 동작하는 형태 

이렇게 5가지가 있는데 이 중에서 around의 proceed() 메소드의 기능에 대해서 정리하고자 한다.  

around 어드바이스의 경우는 클라이언트 호출을 가로챈다. 만일 around 어드바이스 메소드에서 막바로 return을 해 버리면 비지니스 메소드 자체가 실행이 안된다.  
따라서 around Advice 메소드에서 비지니스 메소드 호출에 대한 책임을 감당해야 한다. 즉 around Advice가 비지니스 호출을 가로챘기 때문에 around Advice에서 비지니스 호출을 해 주지 않으면 비지니스 메소드는 실행 될 길이 없다. 그런데 그렇게 할려면 비지니스 메소드에 대한 정보를 around Advice 메소드가 가지고 있어야 하는데 그 정보를 Srping 컨테이너가 around Advice 메소드로 넘겨준다. 그게 ProceedingJoinPoint 객체이다. 

around Advice 메소드는 다음과 같은 형태를 띤다. 여기서 비지니스 메소드로 진행하도록(proceed) 하는 메소드가 proceed() 메소드이다.  
이 메소드를 실행하기 전에 비지니스 메소드 호출 전에 처리할 코드를 수행하도록 하면 된다.  
proceed()를 기준으로 비지니스 메소드 수행 전과 후가 나뉘어 진다. 즉 proceed()가 호출 되기 전에는 
비지니스 메소드 호출 전이고 proceed()가 호출된 후에는 비지니스 메소드 호출 후라고 생각하면 된다. 

Object org.aspectj.lang.ProceedingJoinPoint.proceed() throws Throwable 

그런데 여기서 proceed() 메소드가 반환하는 값이 있는데 Object이다. 여기에 무엇이 담겨 있단 말인가? 
여기에는 비지니스 메소드가 실행한 후의 결과 값들이 담겨 있게 된다. 
예를 들어 비지니스 메소드의 기능이 select 기능이라고 한다면 그 결과 값(보통은 VO 형태에 담기고 이 VO가 Object에 담기게 된다)이 Object에 담기게 되고 insert와 같이 return 되는 값이 없을 경우에는 Object에는 null이 담기게 된다. 

public class AroundAdvice { 
    public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable { 
        String method = pjp.getSignature().getName(); 
        Object returnObj = pjp.proceed(); 

        if(returnObj != null) { 
              //아래에서 다음과 같은 내용 출력. 아래 UserVO는 
              //DB로부터 select 해서 return 된 값이다. 
              //UserVO [id=test, password=null, name=관리자, role=Admin] 
              System.out.println("returnObj.toString() : "+returnObj.toString());  
        } 

        return returnObj; 
    } 
} 

2019년 12월 9일 월요일

AOP(Aspect Oriented Programming) 용어 정리

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
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━