2020년 12월 1일 화요일

웹 페이지에서 프로그레스 써클 띄우기

특정 웹 페이지 로딩시 프로그레서 바/프로그레스 써클 띄워야 할 경우가 있다면 dialog 태그와 img 태그와 로딩 이미지를 보여주는 gif 이미지를 적절히 조합하여 로딩 상태를 띄워줄수가 있다. 아래는 코드이다.

<button onclick="showProCircle()" style="padding: 5px;"> 프로그레스 써클 띄우기 </button><br/>
<h3> 5초 후에 자동으로 닫힙니다. </h3>

<div>
<!-- dialog 태그를 border-radius 속성을 이용해서 원형으로 만들고 gif 이미지도 동일하게 원형으로 만든다 -->
<dialog id="progressCircleDialog" style="width:200px; height:200px; padding:-5px; background-color:#ffffff; border: 0px solid black; border-radius: 50%;">
<img style="width: 100%; height: 100%; display: block; margin: 0px auto; padding: 0px; border-radius: 50%;" src="loading.gif">
</dialog>
</div>
<script>
function showProCircle(){
var dialog = document.getElementById("progressCircleDialog");
dialog.showModal();

setTimeout(function(){
dialog.close();
}, 5000);
}
</script>



2020년 10월 28일 수요일

dialog 태그를 이용한 모달 창 만들기 - alert 대체용 메시지 창 만들기

 웹의 view단을 작업할때 alert() 함수를 이용해서 사용자에게 필요한 알림을 제공하게 된다. 가장 간단하고 쉬운 방법이나 alert()을 사용할때 예상치 못한 곤란을 만날수도 있다. 아무튼 alert()과 같은 역할을 하면서 alert()을 대체할수 있는 dialog 태그에 대해 정리하고자 한다.

<html>
<head>
<title>alert을 대체할 dialog 태그</title>
<style>
#myMsgDialog {
width:40%; 
background-color: #f4ffef; 
border:1px solid black; 
border-radius: 7px;
}

#mButton {
padding: 7px 30px;
background-color: #66ccff;
color: white;
font-size: 15px;
border: 0;
outline: 0;
}
</style>
</head>
<body>
<button onclick="showMsg()">메시지 띄우기</button>
<div>
<!-- 아래 dialog 태그 영역이 메시지 창 -->
<dialog id="myMsgDialog">
<h3>여기는 메시지 내용입니다.</h3>
<input type="button" id="mButton" onclick="closeMsg()" value=" 확 인 " >
</dialog>
</div>

<script>
function showMsg(){
var dialog = document.getElementById("myMsgDialog");
dialog.showModal();
}

function closeMsg(){
var dialog = document.getElementById("myMsgDialog");
dialog.close();
}
</script>
</body>
</html>

코드가 그렇게 복잡하지는 않다. 아무튼 alert() 대체용으로 간단하면서 아주 유용한 기능이다. 아래는 실행 결과이다.



2020년 7월 16일 목요일

Oracle의 ROLLUP 함수의 개념

Oracle의 ROLLUP 함수의 개념

아래와 같은 테이블이 있다고 할때 부서별 연봉(SAL) 소계와 전체 SAL 총계를 도출해 내고자 할때 사용할수 있는 함수가 ROLLUP 함수이다.
한마디로 말하면 GROUP BY로 묶을 칼럼의 소계를 도출하는 함수이다.

DEPT      |  RANK  |  SAL
--------------------------------
기획부    |  부장   | 3800
기획부    |  부장   | 4000
기획부    |  차장   | 1800
마케팅부 |  부장   | 4000
마케팅부 |  차장   | 2000
마케팅부 |  과장   | 1500

SELECT DEPT, RANK, COUNT(RANK) "RANK_COUNT", SUM(SAL) FROM TEST GROUP BY ROLLUP(DEPT, RANK);

위 쿼리가 의미하는 바는 DEPT와 RANK라는 두 칼럼의 데이터가 같은 튜플들을(row) 하나로 묶어(grouping) SAL의 합을 도출하고
DEPT 칼럼의 데이터가 같은 튜플들을 하나로 묶어 SAL의 소계를 도출하고 최종적으로는 전체 튜플들 모두의 합(총계)를 도출하는 기능을 하는 것이 ROLLUP() 함수이다.

DEPT    |  RANK  |  RANK_COUNT | SAL
------------------------------------------------------------------------------
기획부    |  부장  | 2                   | 7800 ==> 기획부, 부장의 합계
기획부    |  차장  | 1                   | 1800
기획부    |  (null) | 3                   | 9600 ==> 기획부 소계
마케팅부 |  과장  | 1                   | 1500
마케팅부 |  부장  | 1                   | 4000
마케팅부 |  차장  | 1                   | 2000
마케팅부 |  (null) | 3                   | 7500 ==> 마케팅부 소계
(null)        |  (null) | 6                   | 17100 ==> 전체 총계

2020년 6월 25일 목요일

PHP에서 잘못된 URL의 유입을 막는 방법

PHP에서 MVC 모델 형태로 웹 시스템을 개발하다보면 DB에 저장, 혹은 DB 정보를 수정하는 URL이 생성되는데 문제는 해당 URL을 막바로 웹브라우저 주소 창에 입력하게 될 경우 문제가 발생하게 된다.
DB에 insert하는 경우의 URL을 웹 브라우저에서 막바로 입력해서 막바로 접속할 경우 시스템을 정교하게 만들지 않을 경우 데이터가 없는 새로운 레코드가 생성되는 불상사가 발생하게 된다.

http://xxx.xxx.xxx/adm/reg/member ⇒ 회원가입 페이지(reg_member.php) ⇒ http://xxx.xxx.xxx/adm/save/member ⇒ DB에 회원 정보 insert(AdmModel.php -> insertMember())

여기서 사용자가 웹 브라우저 주소창에  http://xxx.xxx.xxx/adm/save/member 이 URL로 막바로 접속할 경우 AdmModel.php의 insertMember() 함수가 실행되어 DB에 입력 값이 없는 회원 레코드가 하나 생성되는 불상사가 발생한다는 것이다.
따라서 이런 경우를 막기 위해서는 아래와 같이 간단한 몇줄의 코드로 방어할수 있다.

        public function registeMember($regMbr){
            $prevPage = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_PATH);

            if($prevPage != '/adm/reg/member'){
                echo "<script>alert('허용되지 않는 잘못된 접근입니다.');</script>";
                return;
            }
        }

HTTP_REFERER 환경 변수는 이전 페이지의 URL 값을 담고 있는 환경변수이다. 따라서 정상적인 경우라면 /adm/save/member URL로 들어오기 전 URL은 /adm/reg/member 이어야 하는 것이다. 이 값이 아닌경우라면 차단하면 되는 것이다.
즉 브라우저에서 회원 정보를 등록하는 URL인 http://xxx.xxx.xxx/adm/save/member로 막바로 접속시 위의 $prevPage에는 아무 값이 없다. 따라서 그런 경우는 차단을 하면된다.

2020년 6월 10일 수요일

아파치 url rewrite 기능을 이용한 PHP에서 MVC 모델의 Controller 기능 구현하기

PHP에서 MVC 모델 방식에서 Controller 기능을 구현할려면 어떻게 해야 할까? 혹은 FrontController 기능을 구현할 경우 어떻게 해야 할 것인가?
php의 경우는 url 경로에 맞는 해당 경로에 php 소스가 1:1로 대응되도록 되어 있다.
즉 http://xxx.xxx.xxx/reserve/list.php일 경우 웹 Document Root 디렉토리 아래의 reserve 디렉토리 안에 list.php 소스가 있어야 되는 식이다.

문제는 이런 방식으로 동작하기 때문에 어느 특정 디렉토리의 특정 php 소스를 Controller 역할을 하게 할수가 없게된다. 달리말해서 어떤 형태의 url이 들어오더라도 무조건 특정 디렉토리의 특정 php 소스를 무조건 실행되게 할수가 없다는 것이다.
어떤 경우의 url 형태가 요청되더라도 항상 특정 php가 실행되어야 해당 php 소스가 Controller 역할을 수행할수 있는것 아니겠는가? 

또 url 경로에 맞는 디렉토리에 해당 소스가 있다는 것은 그 소스가 노출될 위험성을 안고 있는 보안상의 문제가 있게 된다.

PHP의 경우 이러한 난점을 해결하고 모든 url에 대해 특정 php로 redirect 시켜 해당 php가 FrontController 역할을 하도록 하기 위해 apache의 기술을 이용하면 Controller 기능을 구현할수 있다.
이때 사용하는 것이apache의 rewrite 모듈이다.

apache의 rewrite 모듈은 apache의 모듈의 한 종류로 서버로 접속해 오는 request를 정해진 규칙을 통해 특정 경로의 파일로 redirect 시키는 역할을 하는 모듈이다.
아래의 절차를 따라 진행하면 된다(Ubuntu 16.04를 기준으로 작성된 글이다).

(1) Apache rewrite 모듈 활성화
아래 명령어를 통해 apache rewrite 모듈을 활성화 한다.

# a2enmod rewrite

(2) Apache 설정에서 rewrite 기능을 사용가능하도록 설정 변경
아래의 경로에 있는 apache2.conf 파일을 열어 설정을 변경한다.

# vi /etc/apache2/apache2.conf
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

위의 AllowOverride None을 AllowOverride All로 변경

(3) .htaccess 파일에서 규칙 생성
웹 Document Root 디렉토리에 .htaccess 파일을 생성한다. apache2의 웹 document root가 어디인지 확인할려면 Ubuntu 16.04의 경우 /etc/apache2/sites-available/000-default.conf 파일을 열면 아래와 같은 내용이 있을 것이다.

DocumentRoot /var/www/html

따라서 .htaccess 파일을 /var/www/html/ 에 생성해서 아래의 내용을 입력한다.

RewriteEngine On
RewriteBase /
RewriteRule ^([^.?]+)$ /ebook3/index.php

.htaccess 파일을 작성하는 규칙은 복잡한 많은 내용이 있지만 여기서는 최소한으로 간략히 표현 경우이다.
위의 설정 내용의  /ebook3/index.php에서 /가 의미하는 것은 어느 경로 위치를 의미하는가 하는 것이다. 즉 여기서 /의 위치는 어디이냐 하는 것이다. 
여기서 /ebook3/index.php의 위치는 Web DocumentRoot의 경로 위치인 /var/www/html/ebook3/index.php를 의미한다. 따라서 /가 의미하는 것은 Web DocumentRoot의 위치를 의미한다.

위의 내용은 어떤 형태의 url로 접근을 하더라도 무조건 /ebook3/index.php를 redirect 시키라는 규칙이다.
만일
http://xxx.xxx.xx/reserve/list
http://xxx.xxx.xx/reserve/view
http://xxx.xxx.xx/reserve/list/json
...
과 같이 url이 reserve로 시작하는 모든 경우 /ebook3/index.php로 이동시키고 싶다면 RewriteRule을 다음과 같이 하면 될 것이다.

RewriteRule ^reserve /ebook3/index.php

(4) apache2  재시작
이상의 모든 설정 사항이 적용되도록 아파치 재 구동

# service apache2 restart

rewrite 모듈이 정상적으로 구동중인지 확인할려면 phpinfo() 를 통해 나오는 페이지에서 "Loaded Modules" 항목을 보면 mod_rewrite라는 항목이 있으면 해당 모듈이 정상 동작하고 있는 것이다.

이상의 작업이 완료되면 이제 PHP에서 MVC 모델을 적용하되 FrontController 역할을 index.php로 할수 있게된다. 즉 접속해 오는 모든 url을 무조건 index.php로(혹은 index.php가 아니어도 상관없음) redirect 시키고 index.php에서 url을 분석해서 각각의 url에 맞는 기능으로 분기시키면 이제 php에서도 Java의 Controller 역할을 수행하는 것이 가능하게 된다.
아래는 index.php가 FrontController(혹은 Controller) 역할을 하도록 간단하게 코드의 뼈대만 작성하면 이런식이 될 것이다.

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

switch($path)
{
case '/reserve/list' :
   //원하는 기능
   bareak;

case '/reserve/view' :
   //원하는 기능
   break;

case '/reserve/insert' :
   //원하는 기능
   bteak;

case '/reserve/list/json' :
   //원하는 기능
   bteak;

default :
   //에러 페이지
   break;
}

2020년 5월 11일 월요일

오라클에서 Primary Key 변경하기

오라클에서 다음과 같은 상황에 대해 DB를 어떻게 변경하는지에 대해 포스팅하고자 한다.

상황 : 
-. 테이블을 새로 생성할수 있는 상황이 아닌 현재 이미 사용하고 있어 데이터가 적재되어 있는 테이블의 PK 변경 및 새로운 칼럼 추가하고자 하는 상황이다.
-. 현재 3개의 PK가 잡혀있는데 이 3개의 PK를 삭제하고 새로 추가하는 칼럼을 PK로 잡고자 한다.
-. PK로 잡히게 될 새로 추가하는 칼럼은 auto increment가 되는 칼럼이다.
-. 테이블 이름은 TB_MEMBER로 가정한다.
-. 새로운 PK로 사용하게 될 새롭게 생성하게 될 칼럼은 RSV_SEQ로 가정한다.

이상의 상황에서 다음 단계를 밟아 원하는 DB를 새롭게 구성할수 있다.

(1) 현재 사용하고 있는 3개의 PK를 삭제한다. TB_MEMBER라는 테이블에서 현재 사용하고 있는 PK들이 삭제된다. PK라는 속성이 제거되는것이지 해당 칼럼 자체가 삭제되는 것은 아니다.

ALTER TABLE TB_MEMBER DROP PRIMARY KEY; 

(2) 새로운 PK로 사용할 칼럼을 추가한다. 새로 추가될 칼럼인 RSV_SEQ를 추후 PK로 사용하게 될 것이고 이 칼럼은 auto increment되는 값을 갖게 될 칼럼이다. 그러나 지금의 시점에서는 단지 새로운 칼럼을 하나 추가하는 것 뿐이다. 

ALTER TABLE TB_MEMBER ADD(RSV_SEQ NUMBER); 

(3) PK로 사용하게 될 새로 추가할 칼럼의 검색 속도를 높이기 위해 index를 생성한다.

CREATE UNIQUE INDEX TB_MEMBER_PK ON TB_MEMBER(RSV_SEQ); 

(4) 새로 추가하게 될 PK 칼럼이 auto increment이므로 이에 대한 sequence를 생성한다. 오라클은 MySQL과 달리 해당 칼럼에 직접 auto_increment 속성을 지정할수가 없어서 별도의 시퀀서를 만들어 사용해야 한다. sequence의 이름은 TB_MEMBER_SEQ이고 1부터 1씩 자동 증가하는 방식의 sequence이다.

CREATE SEQUENCE TB_MEMBER_SEQ START WITH 1 INCREMENT BY 1; 

(5) 이상의 과정까지에서 PK로 사용하게 될 새로운 칼럼에는 아래와 같이 모두 null이 들어 있을 것이다.
---------------------------------
rsv_seq | aaa | bbb | ccc ...
---------------------------------
 null      | ...    | ...    | ...
---------------------------------
 null      | ...      | ...    | ...
---------------------------------
 null      | ...     | ...    | ...
---------------------------------
...
새로 생성한 칼럼인 RSV_SEQ에 대해 앞에서 생성한 sequence를 이용해서 auto increment된 값을 RSV_SEQ 칼럼에 update해 줘야 한다. 아래의 쿼리에 의해 기존 테이블의 각 row의 RSV_SEQ의 값은 1부터 시작해서 1씩 증가된 값들이 비로소 저장되게 된다.

UPDATE TB_MEMBER SET RSV_SEQ=TB_MEMBER_SEQ.nextval; 

---------------------------------
rsv_seq | aaa | bbb | ccc ...
---------------------------------
 1         | ...    | ...     | ...
---------------------------------
 2         | ...   | ...     | ...
---------------------------------
 3         | ...   | ...     | ...
---------------------------------
...
(6) 이제 비로소 새로 생성한 칼럼을 PK로 설정할수 있다. 아래와 같이 

ALTER TABLE TB_MEMBER ADD CONSTRAINT TB_MEMBER_PK PRIMARY KEY(RSV_SEQ); 

테스트 단계이거나 개발 단계에서 기존 적재되어 있는 데이터를 모두 날려 버리고 테이블을 새로 생성할수 있다면 굳이 위와 같은 다소 복잡한 과정 필요없이 그냥

drop table ...
create table ...로 간단히 변경된 내용이 적용된 DDL로 원하는 테이블을 구성할수 있을것이나 그러나 이미 사용하고 있는 경우나 기존 있는 데이터를 유지해야할 경우 오라클에서는 위와 같은 단계를 밟아서 처리를 해야 한다.

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