MyBatis - Model2 게시판 연동
프로젝트를 import 하였다.
자료실 기능과 답글 기능이 있는 이 프로젝트의 구조는 일전에 Model2 실습으로 만든 게시판과 동일하다.
pom.xml 에는 필요한 의존 라이브러리르 추가한다.
한번 잘 생성해놓으면 그 다음부터는 꾸준히 복사해서 사용할 수 있다.
db 접속에 필요한 내용은 resources > db.properties 에 들어있다.
이 곳에 등록된 계정으로 db용 테이블을 생성할 예정이다.
다음과 같은 구조로 테이블과 시퀀스를 생성했다.
DTO 프로퍼티는 테이블의 컬럼명과 같은 것으로 설정해야 일일히 매퍼에서 매핑을 잡아주지 않아도 되어서 편리하다.
cotroller 에서는 뷰 페이지의 값 전달 방식에 따라 doGet 또는 doPost를 실행하게 되는데,
두 메소드에는 doProcess를 실행하라는 공통 코드가 있다.
뷰 페이지에서 전달된 주소값에 따라 if문 중 해당하는 경우를 실행하는 것이다.
서비스 클래스는 Action 부모 인터페이스 하나를 생성해두고
그것을 상속받아 구현하는 형식으로 생성한다.
포워딩 방식과 경로를 설정하는 ActionForward도 생성한다.
여기까지는 기존에 게시판을 생성했던 방식과 비슷하다.
DAO를 생성해서 static 싱글톤 메소드를 생성하고, 세션을 공유하는 메소드를 생성한다.
환경설정 파일 mybatis-config.xml 을 읽어와 세션을 공유할 수 있도록 한다.
이 환경설정 파일에서 중요한 내용은 크게 3가지 이다.
- DTO 클래스에 대한 alias값
- db 연결에 필요한 property 값
- mapper 태그로 mapper 파일 불러오기
나중에는 이 매퍼 파일이 여러개가 될 수도 있다.
매퍼 파일에는 메소드를 실행할 때 필요한 SQL문이 들어있다.
내용은 다음과 같다.
각 SQL문은 각각의 id 값을 가지고 있고, 파라미터 타입이 지정되어 있어 해당 자료형의 값을 받는다.
qnd_board_write.jsp에서 글을 입력하고 submit 하면 Controller를 거쳐
서비스 클래스인 BoardAddAtion.java 로 넘어온다.
자료실 기능이 있으므로 MultipartRequest 객체를 생성해
첨부파일에 대한 내용을 처리한다.
이 때, 파일명은 업로드 할 때의 파일명과 실제 서버에 저장된 파일명 이 두가지가 있는데,
실제 서버에 저장된 파일명을 구해오게 된다.
DAO로 넘어가면 try catch 로 예외처리를 했던 이전과는 달리 throws로 예외처리를 controller 쪽으로 넘긴다.
controller
다시 DAO로 돌아와 코드를 보면, db 연동을 getSession 메소드를 호출하는 것으로 해결하고 있다.
이 getSession 메소드는 DAO 상단에 정의된 메소드로, 다음과 같은 내용이다.
getSession 메소드로 db 연결을 실행하면 insert SQL문을 실행한다.
이 insert SQL문은 mapper 파일에 정의되어 있다.
여러 곳으로 분산을 해두니 문서 하나에 들어가는 코드의 길이가 확실히 많이 줄었다.
board_file,jdbcType=VARCHAR 는 null 값을 허용하게 해준다.
(MyBatis는 null 값을 허용하지 않는다.)
이 코드를 뺀다면, 첨부파일을 업로드하지 않고 글을 작성했을 때 다음과 같은 예외가 발생하면서 글 작성이 되지 않는다.
org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='board_file', mode=IN, javaType=class java.lang.String, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting null for parameter #5 with JdbcType OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. Cause: java.sql.SQLException: 부적합한 열 유형: 1111 |
insert SQL문을 잘 수행했다면 다시 DAO로 돌아가 result 값을 리턴하면서 메소드가 종료된다.
그러면 다시 서비스 클래스로 돌아간다.
result 변수가 1을 반환한다면 콘솔창에 insert를 출력하고
이후 이동할 곳의 포워딩 방식과 경로를 설정한다.
글 목록을 불러오는 경로를 호출했기 때문에
controller로 돌아가 포워딩을 수행한다.
setRedirect가 true 값으로 설정되어 있기 때문에
dispatcher 방식으로 포워딩한 글목록을 출력한다.
중요한 변수
현재 페이지 page, 페이지 개수 limit, 총 데이터 개수 count,
글 목록에서 제일 처음으로 하는 일은 페이징 처리인데,
page = 현재 페이지
limit = 한 페이지에 출력할 페이지의 개수
이 것은 request 객체로 계속 다른 문서로 넘기게 된다.
이 변수에서 startRow, endRow가 파생되므로 이 변수들을 넘기는 것은 중요하다.
총 데이터 개수가 저장될 변수 count를 구하기 위해 dao 객체를 생성하고,
메소드를 호출한다.
dao의 getCount 메소드로 이동하면 다음과 같은 내용이 들어있다.
getSession으로 db를 연결하고, board_count로 그룹함수 SQL문을 실행하는데,
insert, update, delete와 달리 db에 값을 입력하고 종료되는 것이 아니라
select는 값을 돌려받아야 한다.
이 때, 실행한 SQL문의 검색 결과는 여러 개의 로우 데이터가 아닌
하나로 반환되기 때문에 selectOne으로 값을 구해오게 된다.
매퍼 파일의 board_count 는 다음과 같은 내용이다.
count 함수로 값을 구해오면 int형으로 값을 돌려주도록 되어 있다.
메소드의 모든 내용을 실행하면 result에 담긴 SQL문의 결과를 가지고 서비스 클래스로 다시 돌아간다.
이제 글 목록을 구해와 그것을 출력해야 하는데,
서브쿼리를 두 번 사용한 SQL문으로 잘라내기 하여 출력한다.
where 조건절의 첫번째 rnum 값이 startRow, 두번째 rnum 값이 endRow 이다.
페이지 당 출력 개수는 limit 이라는 변수 대신 숫자 10으로 고정 시켜놓았다.
부등호 <, > 를 인식하지 못하기 때문에 > >, < < 로 표기를 한다.
글 목록을 list 객체를 사용해 출력하지만 resultType을 list로 표기하지는 않는다.
이 때, select문으로 돌려받는 값이 여러개이므로 이번에는 selectOne 대신 selectList를 사용하게 된다.
글 목록을 구해오는 또 다른 방법으로는 map 객체를 활용한다.
map 객체의 key-value를 startRow-endRow로 설정한다.
메소드 호출도 map 객체를 매개변수로 받도록 한다.
메소드는 다음과 같이 작성한다.
매퍼 파일도 수정한다.
역시 resultType은 map 이 아닌 board 로 설정하지만, 파라미터 타입은 Map으로 설정한다.
구해온 값은 다음과 같은 과정을 통해 출력된다.
현재 페이지 값을 저장하는 변수 page 에서 startPage, endPage가 파생된다.
값을 모두 구했으면 request 객체를 통해 페이징 처리에 필요한 변수들을 공유 설정한다.
공유되는 값의 형태는 크게 3가지 이다.
1) int형 -> EL로 바로 출력 (page, listcount)
2) list -> forEach 태그의 items 값으로 설정하여 루프를 돌려 출력 (boardlist)
3) DTO 클래스
이후 포워딩 방식을 설정하고 이동할 페이지 경로를 설정한다.
경로를 controller가 아닌 뷰 페이지로 설정했기 때문에 qna_board_list 뷰 페이지로 바로 이동한다.
글 목록을 보여주는 뷰 페이지 qna_board_list.jsp 문서 상단에는
코어 라이브러리, 국제화 라이브러리를 불러온다.
서비스 클래스에서 공유 설정한 변수 listcount는 int형이기 때문에 EL로 바로 출력한다.
서비스 클래스의 기본 변수 page에서 파생되는 마지막 변수가 num 이다.
이 num은 화면에 출력되는 글 번호이다. (db의 board_num 과는 전혀 상관없는 값)
forEach를 통해 루프가 돌아갈 때마다 1씩 감소하게 되는데,
EL은 증감연산자 지원이 안되기 때문에 따로 감소 처리를 해주어야 한다.
forEach 태그의 items 값에는 boardlist가 전달되면서 루프가 돌아갈때마다 자료를 출력하게 된다.
변수 'b' 는 인덱스 값.
만약 글의 종류가 원문인 부모글이 아니라 파생된 답글일 경우,
댓글의 깊이만큼 여백을 추가해준다.
대댓글일 경우 여백이 더 많이 들어가게 된다. (re_lev 값이 클수록)
페이지와 블럭을 이동할 수 있도록 링크를 걸어서 추가했다.
상세 페이지에 들어가면 글의 내용을 구해오고 조회수 +1 작업을 수행하게 된다.
서비스 클래스 BoardDetailAction.java에서는
글번호인 board_num, 진입한 페이지 번호인 page 값을 가장 먼저 공유 받는다.
dao 객체를 생성해 db를 공유받아 조회수 +1 기능을 수행하는 updateCount 메소드를 실행한다.
글번호 board_num을 매개변수로 받아 해당 로우 데이터를 찾아내 그 중 readcount 컬럼의 값에 +1을 수행하게 된다.
돌려줄 값이 있는 select가 아닌 update SQL문으로 resultType은 설정하지 않아도 된다.
이후로 DTO 객체를 생성하여 상세 글 내용을 구해오기 위한 getContent 메소드를 실행한다.
이 getContent는 여러번 사용된다. (삭제, 수정폼)
한 개의 로우 데이터를 구해오는 것이기 때문에 selectOne으로 값을 받아온다.
selectOne의 두번째 매개변수 board_num은 SQL문의 where 조건절에 할당 된다.
이렇게 구해온 board 객체와 목록으로 이동할때 필요한 page 값은 request 객체로 공유한다.
공유하는 값에 따라 뷰 페이지에서 출력하는 형태가 달라진다.
객체인 board는 ${board.필드명}, 변수인 page는 ${page}
상세 페이지 하단의 버튼 중 '댓글'로 진입할 때에는 5가지 값을 전달한다
board_num, page, board_re_ref, board_re_lev, board_re_seq이다.
서비스 db연결이 필요할 때 거쳐야 하는 클래스다.
만약 이 값을 onClick 이벤트로 전달하지 않는다면 서비스 클래스-controller 를 통해
부모글에 대한 정보를 받아와 댓글 페이지로 전달해야 한다.
'댓글' 버튼을 누르면 controller로 이동한다.
이후 뷰 페이지인 qna_board_reply.jsp 에서는 다섯 가지의 값을 전달받는다.
일전에 model2 게시판을 만들때에는 re_rev, re_lev, re_seq는 서비스 클래스를 통해 전달받았지만
이렇게 값을 한꺼번에 받아서 움직이면 서비스 클래스를 통할 필요가 없다.
댓글을 작성하고 나면 넘어가는 값은 4개가 늘어 총 9개가 된다.
기존에 가지고 움직이던 값 5가지와 사용자가 입력한 글쓴이, 비밀번호, 제목, 내용 정보가 추가되어 9개.
서비스 클래스인 BoardReply.java로 이동하면 9가지 값들을 받아와 처리하게 된다.
이전에는 한꺼번에 값을 받아왔지만 이제는 mapper 파일로 SQL문을 분리했기 때문에 한꺼번에 처리할 수가 없다.
댓글 출력과 작성 관련 메소드, SQL문
하나의 메소드에서 seq +1 을 처리하고 insert SQL문을 처리할수가 없다.
각각의 메소드, SQL문으로 분리해서 처리해야만 한다.
작업을 마치고 이동할 페이지 경로를 설정하는데,
상세 페이지에 갈때에는 진입 페이지와 글 번호를,
글 목록에 갈때에는 진입 페이지값만 가지고 간다.
그러면 진입 페이지로 다시 돌아갈 수 있다.
이제 '글 수정'을 처리해보도록 한다.
BoardModifyAction.java 은 글수정폼으로 진입하는데 필요한 내용이 있는 서비스 클래스이다.
상세 페이지에서 board_num, page 값을 받아온다.
db 연결을 위한 DAO 객체를 생성하여 getContent에서 수정할 글의 상세 정보를 구해온다.
이 메소드의 매개변수로 request객체로 받아온 board_num 을 사용한다.
이렇게 구해온 정보는 board 객체에 저장하고
board 객체와 page를 공유설정한다.
그리고 수정폼으로 넘어간다.
board_num과 page는 hidden 으로 넘겨준다.
이 두가지 값과 사용자가 입력 양식에 입력한 글쓴이, 비밀번호, 제목, 내용 값 까지
총 6가지 값을 BoardModify.java 서비스 클래스에 넘긴다.
사용자가 입력한 정보를 update SQL문으로 db 정보를 수정하기 위해서는
입력한 비밀번호와 db내의 비밀번호가 일치해야 한다.
그렇기 때문에 글 정보를 구해와 그 중 비밀번호 컬럼의 정보와 일치하는지 비교하는 작업을 수행해야 한다.
board_num 값을 매개로 글의 정보를 구해오는 getContent 메소드를 호출한다.
이렇게 구해온 정보는 old 에 저장하는데,
조건문으로 비밀번호를 비교해 일치 여부에 따라 실행하는 내용이 달라진다.
글 수정을 완료하면 board_num 값과 page 값을 가지고 상세 페이지로 이동하도록 경로가 설정되어 있다.
이제 '글 삭제'를 처리해본다.
삭제 폼 진입은 DB 연동할 필요는 딱히 없어서 서비스 클래스를 통하지 않는다.
삭제 폼에 가져갈 값은 board_num, page 값이다.
이렇게 서비스 클래스를 거치지 않고 뷰 페이지로 바로 연결된다.
다음에 넘어갈 페이지에 board_num과 page를 공유해야 하기 때문에 hidden 설정을 한다.
이 페이지에서는 비밀번호 비교만 하기 때문에 가장 단촐한 구성이다.
사용자에게 입력받은 비밀번호와 board_num, page를 가지고 BoardDelete.java 서비스 클래스로 넘어간다.
넘겨받은 board_num 을 매개변수로 삭제할 글의 상세 정보를 구해온다.
구해온 상세정보에 담긴 비밀번호와 사용자에게 입력받은 비밀번호가 일치하면 delete 메소드를 실행한다.
이 메소드의 내용은 매퍼 파일의 delete SQL을 실행하는 것이다.
그러나 비밀번호가 맞지 않으면 alert창을 실행시키고 뒤로 한번 이동 시켜 다시 비밀번호를 입력하도록 하는데,
alert 창을 띄우기 위한 out 객체를 생성했고, 한글 인코딩을 위한 response 객체도 불러온다.
이 때, 파일을 첨부했다면 이 파일까지 같이 삭제를 해야 한다.
경로값 path를 매개로 하여 file 객체를 생성하여 경로안의 모든 파일 목록을 구해온다.
이 목록은 배열에 저장하는데, 루프를 돌려 모든 값을 꺼내 db에 저장된 첨부파일명과 대조를 하여
일치하는 값을 찾았을 때 이것을 삭제한다.