어떤 분을 위한 RPC 프레임워크 잡설

emptynote의 이미지

제가 처음 접한 프레임워크는 이원영씨의 JDF 입니다.

S00 증권 외주로 들어가서 처음 접하고 배운 프레임워크입니다.

이 JDF 프레임워크는 독자로 존재할 경우 별 볼일 없는듯하지만

RPC 와 결합하여 메시지 주도 개발을 할 경우 위력을 발휘합니다.

비지니스 서버 <-- tcp/udp --> 인증 서버 <------ tcp ----> 클라이언트

이것이 기본 모양새이고 이를 웹을 기준으로 바꾸면

비지니스 서버 <-- tcp/udp --> was(ex tomcat) <------ tcp ----> 사용자 PC 의 브라우저

가 됩니다.

서블릿 + jsp 형태의 MVC 모델에서

서블릿은 비지니스 로직을 담당하므로 비지니스 로직 메시지를 작성하여 요청후 결과 메시지를 얻어 오는 역활이 주요한 기능입니다.
JSP 는 view 를 담당하므로 결과 메시지를 바탕으로 화면 구성을 하는것이 주요한 기능입니다.

게식판 목록을 JDF 에서 작성하고자 한다면

web.xml 에 '게시판 목록' 담당 서블릿을 등록하고

사용자가 브라우저로 요청하면

서블릿은 '게시판 목록 요청' 메시지를 작성하여 '비지니스 로직' 서버로 던지고 결과 메시지를 받습니다.

이렇게 받은 결과 메시지는 jsp 로 전달되어 게시판 목록 화면이 완성되게 됩니다.

jsp 는 약속된 메시지를 바탕으로 화면을 구성할 수 있습니다.

즉 가짜로 결과 메시지를 받은것처럼하여 화면 작업을 진행할 수 있습니다.

다른 말로 비지니스 로직 개발자와 프론트 개발자는 약속된 메시지를 기반으로 병행하여 개발 작업을 할 수 있는 것 입니다.

JDF 프레임워크는 스프링과 달라서 비지니스 로직을 체계적으로 다루어는 부분이 없습니다.
제 주관적인 생각이지만 JDF 프레임워크가 RPC 를 전제로 개발을 했기때문에 비지니스 로직을 체계적 관리할 필요성이 없기때문이 아닌가합니다.

kldp 에서 어떤 분을 보니 처음 jsp 공부할때 생각이 나네요.

그 당시에 jsp 에서 모든것을 처리하였는데 그때 모습과 겹치네요.

그분한테 제가 아는 프레임워크이자 자작한 프레임워크를 보여 드리고 싶어 이 글을 작성하네요.

맘에 드는 RPC 개발 프레임워크가 있었다면 그것을 사용해서 더 나은 서비스를 만드는데 집중했을텐데,

아쉽게도 찾지 못해서 맨땅에 헤딩하면서 만들다 보니 오랜 시간이 걸렸네요.

아직도 관리자 사이트 완성을 못하고 있어

개인 프레임워크 개발를 이용한 커뮤니티 사이트를 오픈을 못하고 있지만

그래도 언제가 해뜰 날이 있겠지요.

>> 게시판 목록 요청 메시지

<?xml version="1.0" encoding="utf-8" ?>
<!--
	item type : (unsigned) byte, (unsigned) short, (unsigned) integer, long,
					fixed length string, 
					ub pascal string, us pascal string, si pascal string, 
					fixed length byte[], ub variable length byte[], 
					us variable length byte[], si variable length byte[]
					java sql date, java sql timestamp, boolean
	array counter type : reference 변수참조, direct 직접입력
	direction : FROM_NONE_TO_NONE, FROM_SERVER_TO_CLINET, FROM_CLIENT_TO_SERVER, FROM_ALL_TO_ALL
	(1) FROM_NONE_TO_NONE : 메시지는 서버에서 클라이언트로 혹은 클라이언트에서 서버로 양쪽 모두에서 전송되지 않는다.
	(2) FROM_SERVER_TO_CLINET : 메시지는 서버에서 클라이언트로만 전송된다.
	(3) FROM_CLIENT_TO_SERVER : 메시지는 클라이언트에서 서버로만 전송된다.
	(4) FROM_ALL_TO_ALL : 메시지는 서버에서 클라이언트로도 혹은 클라이언트에서 서버로 양쪽 모두에서 전송된다.
-->
<message>
<messageID>BoardInfoListReq</messageID>
<direction>FROM_CLIENT_TO_SERVER</direction>
<desc>게시판 정보 목록 요청 메시지</desc>
<singleitem name="requestedUserID" type="ub pascal string" />
</message>

>> 게시판 목록 응답 메시지

<?xml version="1.0" encoding="utf-8" ?>
<!--
	item type : (unsigned) byte, (unsigned) short, (unsigned) integer, long,
					fixed length string, 
					ub pascal string, us pascal string, si pascal string, 
					fixed length byte[], ub variable length byte[], 
					us variable length byte[], si variable length byte[]
					java sql date, java sql timestamp, boolean
	array counter type : reference 변수참조, direct 직접입력
	direction : FROM_NONE_TO_NONE, FROM_SERVER_TO_CLINET, FROM_CLIENT_TO_SERVER, FROM_ALL_TO_ALL
	(1) FROM_NONE_TO_NONE : 메시지는 서버에서 클라이언트로 혹은 클라이언트에서 서버로 양쪽 모두에서 전송되지 않는다.
	(2) FROM_SERVER_TO_CLINET : 메시지는 서버에서 클라이언트로만 전송된다.
	(3) FROM_CLIENT_TO_SERVER : 메시지는 클라이언트에서 서버로만 전송된다.
	(4) FROM_ALL_TO_ALL : 메시지는 서버에서 클라이언트로도 혹은 클라이언트에서 서버로 양쪽 모두에서 전송된다.
-->
<message>
<messageID>BoardInfoListRes</messageID>
<direction>FROM_SERVER_TO_CLINET</direction>
<desc>게시판 정보 목록 응답 메시지</desc>
<singleitem name="cnt" type="integer" />
<array name="boardInfo" cnttype="reference" cntvalue="cnt">
	<singleitem name="boardID" type="unsigned byte" />
	<singleitem name="boardName" type="ub pascal string" />
	<singleitem name="boardListType" type="byte" />
	<singleitem name="boardReplyPolicyType" type="byte" />
	<singleitem name="boardWritePermissionType" type="byte" />
	<singleitem name="boardReplyPermissionType" type="byte" />
	<singleitem name="cnt" type="long" />
	<singleitem name="total" type="long" />
	<singleitem name="nextBoardNo" type="unsigned integer" />
</array>
</message>

>> web.xml

	<!-- 게시판 목록 -->
	<servlet>
		<servlet-name>BoardList</servlet-name>
		<servlet-class>kr.pe.codda.servlet.user.BoardListSvl</servlet-class>
		<init-param>
			<param-name>menuGroupURL</param-name>
			<param-value>/servlet/BoardList</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>BoardList</servlet-name>
		<url-pattern>/servlet/BoardList</url-pattern>
	</servlet-mapping>	

>> 게시판 목록 서블릿은

package kr.pe.codda.servlet.user;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import kr.pe.codda.client.AnyProjectConnectionPoolIF;
import kr.pe.codda.client.ConnectionPoolManager;
import kr.pe.codda.common.message.AbstractMessage;
import kr.pe.codda.impl.classloader.ClientMessageCodecManger;
import kr.pe.codda.impl.message.BoardListReq.BoardListReq;
import kr.pe.codda.impl.message.BoardListRes.BoardListRes;
import kr.pe.codda.impl.message.MessageResultRes.MessageResultRes;
import kr.pe.codda.weblib.common.ValueChecker;
import kr.pe.codda.weblib.common.WebCommonStaticFinalVars;
import kr.pe.codda.weblib.jdf.AbstractServlet;
 
@SuppressWarnings("serial")
public class BoardListSvl extends AbstractServlet {
 
	@Override
	protected void performTask(HttpServletRequest req, HttpServletResponse res)
			throws Exception {
 
		/**************** 파라미터 시작 *******************/
		String paramBoardID = req.getParameter("boardID");
		String paramPageNo = req.getParameter("pageNo");
		/**************** 파라미터 종료 *******************/
 
		short boardID = -1;
		try {
			boardID = ValueChecker.checkValidBoardID(paramBoardID);
		} catch(IllegalArgumentException e) {
			String errorMessage = e.getMessage();
			String debugMessage = null;
			printErrorMessagePage(req, res, errorMessage, debugMessage);
			return;
		}		
 
		int pageNo = -1;
		try {
			pageNo = ValueChecker.checkValidPageNoAndPageSize(paramPageNo, WebCommonStaticFinalVars.WEBSITE_BOARD_LIST_SIZE_PER_PAGE);
		} catch(IllegalArgumentException e) {
			String errorMessage = e.getMessage();
			String debugMessage = null;
			printErrorMessagePage(req, res, errorMessage, debugMessage);
			return;
		}
 
		// AccessedUserInformation accessedUserformation = getAccessedUserInformation(req);
 
		BoardListReq boardListReq = new BoardListReq();
		boardListReq.setRequestedUserID(getAccessedUserInformationFromSession(req).getUserID());
		boardListReq.setBoardID(boardID);
		boardListReq.setPageNo(pageNo);
		boardListReq.setPageSize(WebCommonStaticFinalVars.WEBSITE_BOARD_LIST_SIZE_PER_PAGE);
 
		AnyProjectConnectionPoolIF mainProjectConnectionPool = ConnectionPoolManager.getInstance().getMainProjectConnectionPool();
		AbstractMessage outputMessage = mainProjectConnectionPool.sendSyncInputMessage(ClientMessageCodecManger.getInstance(), boardListReq);
 
		if (!(outputMessage instanceof BoardListRes)) {
			if (outputMessage instanceof MessageResultRes) {
				MessageResultRes messageResultRes = (MessageResultRes)outputMessage;
				String errorMessage = "게시판 목록 조회가 실패하였습니다";
				String debugMessage = messageResultRes.toString();
				printErrorMessagePage(req, res, errorMessage, debugMessage);	
				return;
			} else {
				String errorMessage = "게시판 목록 조회가 실패했습니다";
				String debugMessage = new StringBuilder("입력 메시지[")
						.append(boardListReq.getMessageID())
						.append("]에 대한 비 정상 출력 메시지[")
						.append(outputMessage.toString())
						.append("] 도착").toString();
 
				log.error(debugMessage);
 
				printErrorMessagePage(req, res, errorMessage, debugMessage);
				return;
			}
		} 
 
		BoardListRes boardListRes = (BoardListRes)outputMessage;
		req.setAttribute("boardListRes", boardListRes);
		printJspPage(req, res, "/jsp/community/BoardList.jsp");
	}
}

>>> 게시판 목록 jsp

<%@page import="kr.pe.codda.common.etc.CommonStaticFinalVars"%><%
%><%@page import="kr.pe.codda.weblib.common.AccessedUserInformation"%><%
%><%@page import="kr.pe.codda.weblib.common.PermissionType"%><%	
%><%@page import="kr.pe.codda.weblib.common.BoardListType"%><%	
%><%@page import="java.util.List"%><%
%><%@page import="kr.pe.codda.weblib.htmlstring.StringEscapeActorUtil.STRING_REPLACEMENT_ACTOR_TYPE"%><%
%><%@page import="kr.pe.codda.weblib.htmlstring.StringEscapeActorUtil"%><%
%><%@ page import="kr.pe.codda.weblib.common.WebCommonStaticFinalVars" %><%	
%><%@ page import="kr.pe.codda.impl.message.BoardListRes.BoardListRes" %><%
%><%@ page extends="kr.pe.codda.weblib.jdf.AbstractUserJSP" language="java" session="true" autoFlush="true" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%
%><jsp:useBean id="boardListRes" class="kr.pe.codda.impl.message.BoardListRes.BoardListRes" scope="request" /><%//String boardListResJsonString = new Gson().toJson(boardListRes);
 
	/* {
	String requestUserID = "guest";
	boardListRes.setBoardID(BoardType.FREE.getBoardID());
	boardListRes.setPageNo(1);
	boardListRes.setPageSize(20);
 
	boardListRes.setTotal(5);
 
	List<BoardListRes.Board> boardList = new ArrayList<BoardListRes.Board>();
	{		
		for (long boardNo=5; boardNo >= 1; boardNo--) {
	BoardListRes.Board board = new BoardListRes.Board();
	board.setBoardNo(boardNo);
	board.setGroupNo(boardNo);
	board.setGroupSeq(0);
	board.setParentNo(0);
	board.setDepth((short)0);
	board.setWriterID("test01");
	board.setViewCount(0);
	board.setBoardSate(BoardStateType.OK.getValue());
	// yyyy-mm-dd hh:mm:ss
	board.setRegisteredDate(Timestamp.valueOf("2018-09-22 13:00:01"));
	board.setNickname("테스트아이디01");
	board.setVotes(0);
	board.setSubject("게시글"+boardNo);
	board.setLastModifiedDate(board.getRegisteredDate());
 
	boardList.add(board);
		}
 
	}
 
 
	boardListRes.setCnt(boardList.size());
	boardListRes.setBoardList(boardList);
	} */	
 
 
	AccessedUserInformation accessedUserformation = getAccessedUserInformationFromSession(request);
 
	BoardListType boardListType = BoardListType.valueOf(boardListRes.getBoardListType());		
	PermissionType boardWritePermissionType = PermissionType.valueOf(boardListRes.getBoardWritePermissionType());%><!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%=WebCommonStaticFinalVars.USER_WEBSITE_TITLE%></title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/bootstrap/3.3.7/css/bootstrap.css">
<!-- jQuery library -->
<script src="/jquery/3.3.1/jquery.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="/bootstrap/3.3.7/js/bootstrap.min.js"></script>
 
<script type="text/javascript" src="/js/jsbn/jsbn.js"></script>
<script type="text/javascript" src="/js/jsbn/jsbn2.js"></script>
<script type="text/javascript" src="/js/jsbn/prng4.js"></script>
<script type="text/javascript" src="/js/jsbn/rng.js"></script>
<script type="text/javascript" src="/js/jsbn/rsa.js"></script>
<script type="text/javascript" src="/js/jsbn/rsa2.js"></script>
<script type="text/javascript" src="/js/cryptoJS/rollups/sha256.js"></script>
<script type="text/javascript" src="/js/cryptoJS/rollups/aes.js"></script>
<script type="text/javascript" src="/js/cryptoJS/components/core-min.js"></script>
<script type="text/javascript" src="/js/cryptoJS/components/cipher-core-min.js"></script>
 
<script src="/js/common.js"></script>
<script type='text/javascript'>
	function buildPrivateKey() {
		var privateKey = CryptoJS.lib.WordArray.random(<%= WebCommonStaticFinalVars.WEBSITE_PRIVATEKEY_SIZE %>);	
		return privateKey;
	}
 
	function putNewPrivateKeyToSessionStorage() {
		var newPrivateKey = buildPrivateKey();
		var newPrivateKeyBase64 = CryptoJS.enc.Base64.stringify(newPrivateKey);
 
		sessionStorage.setItem('<%= WebCommonStaticFinalVars.SESSIONSTORAGE_KEY_NAME_OF_PRIVATEKEY %>', newPrivateKeyBase64);
 
		return newPrivateKeyBase64;
	}
 
	function getPrivateKeyFromSessionStorage() {
		var privateKeyBase64 = sessionStorage.getItem('<%=WebCommonStaticFinalVars.SESSIONSTORAGE_KEY_NAME_OF_PRIVATEKEY%>');
 
		if (null == privateKeyBase64) {			
			privateKeyBase64 = putNewPrivateKeyToSessionStorage();
		}
 
		var privateKey = null;
		try {
			privateKey = CryptoJS.enc.Base64.parse(privateKeyBase64);
		} catch(err) {
			console.log(err);
			throw err;
		}
 
		return privateKey;
	}
 
	function getSessionkeyBase64FromSessionStorage() {
		var privateKeyBase64 = sessionStorage.getItem('<%=WebCommonStaticFinalVars.SESSIONSTORAGE_KEY_NAME_OF_PRIVATEKEY%>');
 
		if (null == privateKeyBase64) {
			privateKeyBase64 = putNewPrivateKeyToSessionStorage();
		}
 
 
		var rsa = new RSAKey();	
		rsa.setPublic("<%= getModulusHexString(request) %>", "10001");
 
		var sessionKeyHex = rsa.encrypt(privateKeyBase64);
		var sessionKey = CryptoJS.enc.Hex.parse(sessionKeyHex);
		return CryptoJS.enc.Base64.stringify(sessionKey);
	}
 
	function buildIV() {
		var iv = CryptoJS.lib.WordArray.random(<%= WebCommonStaticFinalVars.WEBSITE_IV_SIZE %>);
		return iv;
	}
 
	function writeBoard() {		
		var f = document.writeInputFrm;		
 
		if ('' == f.subject.value) {
			alert("제목을 넣어 주세요.");
			f.subject.focus();
			return;
		}
 
		if ('' == f.contents.value) {
			alert("내용을 넣어 주세요.");
			f.contents.focus();
			return;
		}
 
		if (f.pwd != undefined) {
			try {
				checkValidPwd('게시글', f.pwd.value);
			} catch(err) {
				alert(err);
				f.pwd.focus();
				return;
			}
 
			try {
				checkValidPwdConfirm('게시글', f.pwd.value, f.pwdConfirm.value);
			} catch(err) {
				alert(err);
				f.pwd.focus();
				return;
			}
		}		
 
		var symmetricKeyObj = CryptoJS.<%= WebCommonStaticFinalVars.WEBSITE_JAVASCRIPT_SYMMETRIC_KEY_ALGORITHM_NAME %>;		
		var privateKey = getPrivateKeyFromSessionStorage();
		var iv = buildIV();
 
		var g = document.writeProcessFrm;	
 
		var newFileListDivNode = document.getElementById('newAttachedFileList');
		var uploadFileCnt = newFileListDivNode.childNodes.length;	
 
		if (uploadFileCnt > _ATTACHED_FILE_MAX_COUNT) {
			alert("업로드 할 수 있는 파일 갯수는 최대["+_ATTACHED_FILE_MAX_COUNT+"] 까지 입니다.");
			return;
		}
 
		for (var i=0; i < newFileListDivNode.childNodes.length; i++) {				
			var fileInput = newFileListDivNode.childNodes[i].childNodes[0].childNodes[0];
 
			if (1 == newFileListDivNode.childNodes.length) {
				if (g.newAttachedFile.value == '') {
					alert("첨부 파일을 선택하세요");
					return;
				}
			} else {
				if (g.newAttachedFile[i].value == '') {
					alert(fileInput.getAttribute("title")+"을 선택하세요");
					return;
				}
			}			
		}
 
		g.<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY %>.value = getSessionkeyBase64FromSessionStorage();	
		g.<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY_IV %>.value = CryptoJS.enc.Base64.stringify(iv);
 
		g.subject.value = f.subject.value;
		g.contents.value = f.contents.value;
 
		if (f.pwd != undefined) {
			var symmetricKeyObj = CryptoJS.AES;		
			g.pwd.value = symmetricKeyObj.encrypt(f.pwd.value, getPrivateKeyFromSessionStorage(), { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv });
		}
 
		g.submit()
	}
 
	function callBackForBoardWriteProcess(boardWriteResObj) {
		alert("게시글 작성이 완료되었습니다");		
		goListPage(1);
	}	
 
 
	function showWriteEditScreen() {	
		var writePartObj = document.getElementById('editScreenOfBoard0');
		writePartObj.style.display = "block";
 
		var f = document.writeInputFrm;	
		f.reset();
 
		var newFileListDivNode = document.getElementById('newAttachedFileList');
 
		while(newFileListDivNode.hasChildNodes()) {
			newFileListDivNode.removeChild(newFileListDivNode.firstChild);
		}
	}
 
	function hideWriteEditScreen() {	
		var writePartObj = document.getElementById('editScreenOfBoard0');
		writePartObj.style.display = "none";
	}
 
 
	function goDetailPage(boardID, boardNo, isTreeTypeList) {
		var detailPageURL = "/servlet/BoardDetail?boardID="+boardID+"&boardNo="+boardNo;
 
		if (isTreeTypeList) {
			window.open(detailPageURL, "", "width=800,height=600");
		}  else {
			document.location.href = detailPageURL;
		}		
	}
 
 
	function goListPage(pageNo) {
		var iv = buildIV();
 
		var g = document.listwriteInputFrm;
 
		g.<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY %>.value = getSessionkeyBase64FromSessionStorage();		
		g.<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY_IV %>.value = CryptoJS.enc.Base64.stringify(iv);
 
 
		g.pageNo.value = pageNo;		
		g.submit();
	}
 
	function addNewAttachedFile(f) {		
		var prefixOfNewChildDiv = 'newAttachedFileRowDiv';		
 
		var newFileListDivNode = document.getElementById('newAttachedFileList');		
		var oldFileListDivNode = document.getElementById('oldAttachedFileList');
 
		var uploadFileCnt;
 
		if (oldFileListDivNode == undefined) {
			uploadFileCnt = newFileListDivNode.childNodes.length;
		} else {
			uploadFileCnt = oldFileListDivNode.childNodes.length + newFileListDivNode.childNodes.length;
		}
 
		if (uploadFileCnt >= _ATTACHED_FILE_MAX_COUNT) {
			alert("업로드 할 수 있는 파일 갯수는 최대["+_ATTACHED_FILE_MAX_COUNT+"] 까지 입니다.");
			return;
		}
 
		var newAttachedFileRowSeq = parseInt(f.newAttachedFileRowSeq.value, 10);
 
		var attachedFileRowDivNode = makeNewAttachedFileRowDiv(prefixOfNewChildDiv+newAttachedFileRowSeq);
 
		newFileListDivNode.appendChild(attachedFileRowDivNode);
 
		newAttachedFileRowSeq++;
		f.newAttachedFileRowSeq.value = newAttachedFileRowSeq;
	}
 
	function makeNewAttachedFileRowDiv(attachedFileRowDivID) {
		var attachedFileRowDivNode = document.createElement("div");
		attachedFileRowDivNode.setAttribute("class", "row");
		attachedFileRowDivNode.setAttribute("id", attachedFileRowDivID);		
 
		var attachedFileNode  = document.createElement("INPUT");
		attachedFileNode .setAttribute("type", "file");
		attachedFileNode .setAttribute("class", "form-control");
		attachedFileNode .setAttribute("title", "첨부파일('"+attachedFileRowDivID+"')");
		attachedFileNode .setAttribute("name", "newAttachedFile");
 
		var attachedFileColDivNode = document.createElement("div");
		attachedFileColDivNode.setAttribute("class", "col-sm-10");
 
		attachedFileColDivNode.appendChild(attachedFileNode);
 
		var deleteButtonNode = document.createElement("INPUT");
		deleteButtonNode.setAttribute("type", "button");
		deleteButtonNode.setAttribute("value", "삭제");
		deleteButtonNode.setAttribute("title", "첨부파일('"+attachedFileRowDivID+"') 삭제");
		deleteButtonNode.setAttribute("onclick", "removeNewAttachFile('"+attachedFileRowDivID+"')");
 
		var buttonColDivNode = document.createElement("div");
		buttonColDivNode.setAttribute("class", "col-*-*");
 
		buttonColDivNode.appendChild(deleteButtonNode);
 
		attachedFileRowDivNode.appendChild(attachedFileColDivNode);
		attachedFileRowDivNode.appendChild(buttonColDivNode);
 
		return attachedFileRowDivNode;
	}
 
	function removeNewAttachFile(selectedDivID) {
		var newFileListDivNode = document.getElementById('newAttachedFileList');		
		var selectedDivNode = document.getElementById(selectedDivID);
		newFileListDivNode.removeChild(selectedDivNode);
	}
 
	function clickHiddenFrameButton(thisObj) {		
		var hiddenFrameObj = document.getElementById("hiddenFrame");
 
		if (hiddenFrameObj.style.display == 'none') {
			thisObj.innerText = "Hide Hidden Frame";
			hiddenFrameObj.style.display = "block";			
		} else {
			thisObj.innerText = "Show Hidden Frame";
			hiddenFrameObj.style.display = "none";
		}
	}	
 
	function init() {
		if(typeof(sessionStorage) == "undefined") {
		    alert("Sorry! No HTML5 sessionStorage support..");
		    document.location.href = "/";
		    return;
		}
 
 
		expandTextarea('contentsInWritePart');
	}
 
	window.onload = init;
</script>
</head>
<body>
<div class="header">
	<div class="container">
<%= getMenuNavbarString(request) %>
	</div>
</div>
 
<form name=listwriteInputFrm method="post" action="/servlet/BoardList">
<input type="hidden" name="boardID" value="<%=boardListRes.getBoardID()%>" />
<input type="hidden" name="pageNo" />
<input type="hidden" name="<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY %>" />
<input type="hidden" name="<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY_IV %>" />
</form>
<div class="content">
	<div class="container">
		<div class="panel panel-default">
			<div class="panel-heading"><h4><%= boardListRes.getBoardName() %> 게시판</h4></div>
			<div class="panel-body">
				<div class="btn-group"><%
	if (PermissionType.MEMBER.equals(boardWritePermissionType)) {
		/** 본문 작성 권한이 회원인 경우 */
		if (accessedUserformation.isLoginedIn()) {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("					");
			out.write("<button type=\"button\" class=\"btn btn-primary btn-sm\" onClick=\"showWriteEditScreen();\">글 작성하기</button>");
		}
	} else if (PermissionType.ADMIN.equals(boardWritePermissionType)) {
		/** 본문 작성 권한이 관리자인 경우 */
		if (accessedUserformation.isAdmin()) {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("					");
			out.write("<button type=\"button\" class=\"btn btn-primary btn-sm\" onClick=\"showWriteEditScreen();\">글 작성하기</button>");
		}
	} else if (PermissionType.GUEST.equals(boardWritePermissionType)) {
		/** 본문 작성 권한이 손님인 경우 */
		out.write(CommonStaticFinalVars.NEWLINE);
		out.write("					");
		out.write("<button type=\"button\" class=\"btn btn-primary btn-sm\" onClick=\"showWriteEditScreen();\">글 작성하기</button>");
	}
%>
					<button type="button" class="btn btn-primary btn-sm" onClick="clickHiddenFrameButton(this);">Show Hidden Frame</button>
				</div>			 
				<div id="resultMessage"></div>
				<br>
				<div id="listPartView">
					<div class="row">
						<div class="col-sm-1" style="background-color:lavender;">번호</div>
						<div class="col-sm-3" style="background-color:lavender;">제목</div>
						<div class="col-sm-2" style="background-color:lavender;">작성자</div>
						<div class="col-sm-1" style="background-color:lavender;">조회수</div>
						<div class="col-sm-1" style="background-color:lavender;">추천수</div>
						<div class="col-sm-2" style="background-color:lavender;">최초 작성일</div>
						<div class="col-sm-2" style="background-color:lavender;">마지막 수정일</div>
					</div><%
	List<BoardListRes.Board> boardList = boardListRes.getBoardList();
	if (null == boardList || boardList.isEmpty()) {
%>
					<div class="row">
						<div class="col-sm-12" align="center">조회 결과가 없습니다</div>
					</div><%
	} else {
		for (BoardListRes.Board board : boardList) {
			int depth = board.getDepth();
%>
					<div class="row">
						<div class="col-sm-1"><%=board.getBoardNo()%></div>
						<div class="col-sm-3"><%
			if (depth > 0) {
				for (int i=0; i < depth; i++) {
					out.print("&nbsp;&nbsp;&nbsp;&nbsp;");
				}
				out.print("ㄴ");
			}
%><a href="#" onClick="goDetailPage(<%=boardListRes.getBoardID()%>, <%=board.getBoardNo()%>, <%=BoardListType.TREE.equals(boardListType)%>)"><%=StringEscapeActorUtil.replace(board.getSubject(), STRING_REPLACEMENT_ACTOR_TYPE.ESCAPEHTML4)%></a></div>
						<div class="col-sm-2"><%=StringEscapeActorUtil.replace(board.getWriterNickname(), STRING_REPLACEMENT_ACTOR_TYPE.ESCAPEHTML4)%></div>
						<div class="col-sm-1"><%=board.getViewCount()%></div>
						<div class="col-sm-1"><%=board.getVotes()%></div>
						<div class="col-sm-2"><%=board.getRegisteredDate()%></div>
						<div class="col-sm-2"><%=board.getLastModifiedDate()%></div>
					</div><%
		}
	}
 
	if (boardListRes.getTotal() > 1) {
		final int pageNo = boardListRes.getPageNo();
		final int pageSize = boardListRes.getPageSize();
 
		// long pageNo = boardListRes.getPageOffset() / boardListRes.getPageLength() + 1;
 
		long startPageNo = 1 + WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE*(long)((pageNo - 1) / WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE);
		long endPageNo = Math.min(startPageNo + WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE, 
		(boardListRes.getTotal() + pageSize - 1) / pageSize);
 
 
		out.write(CommonStaticFinalVars.NEWLINE);
		out.write("					");
		out.write("<ul class=\"pagination pagination-sm\">");
 
		if (startPageNo > 1) {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("						");
			out.write("<li class=\"previous\"><a href=\"#\" onClick=\"goListPage(");
			out.write(String.valueOf(startPageNo-1));
			out.write(")\">이전</a></li>");
		} else {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("						");
			out.write("<li class=\"disabled previous\"><a href=\"#\">이전</a></li>");
		}
 
		for (int i=0; i < WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE; i++) {
			long workingPageNo = startPageNo + i;
			if (workingPageNo > endPageNo) break;
 
			if (workingPageNo == pageNo) {
				out.write(CommonStaticFinalVars.NEWLINE);
				out.write("						");
				out.write("<li class=\"active\"><a href=\"#\">");
				out.write(String.valueOf(workingPageNo));
				out.write("</a></li>");
			} else {
				out.write(CommonStaticFinalVars.NEWLINE);
				out.write("						");
				out.write("<li><a href=\"#\" onClick=\"goListPage(");
				out.write(String.valueOf(workingPageNo));
				out.write(")\">");
				out.write(String.valueOf(workingPageNo));
				out.write("</a></li>");
			}
		}
 
		if (startPageNo+WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE <= endPageNo) {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("						");
			out.write("<li class=\"next\"><a href=\"#\" onClick=\"goListPage(");
			out.write(String.valueOf(startPageNo+WebCommonStaticFinalVars.WEBSITE_BOARD_PAGE_LIST_SIZE));
			out.write(")\">다음</a></li>");
		} else {
			out.write(CommonStaticFinalVars.NEWLINE);
			out.write("						");
			out.write("<li class=\"disabled next\"><a href=\"#\">다음</a></li>");
		}
 
		out.write(CommonStaticFinalVars.NEWLINE);
		out.write("					");
		out.write("</ul>");
	}
%>				
				</div>
				<div id="editScreenOfBoard0" style="display:none">
					<form name="writeInputFrm" enctype="multipart/form-data" method="post" action="/servlet/BoardWriteProcess" onsubmit="return false;">
						<input type="hidden" name="newAttachedFileRowSeq" value="0" />
						<div class="form-group">
							<label for="subject">제목</label>
							<input type="text" name="subject" class="form-control" placeholder="Enter subject" />
							<label for="content">내용</label>
							<textarea name="contents" id="contentsInWritePart" class="form-control" placeholder="Enter contents" rows="5"></textarea><%
								if (! accessedUserformation.isLoginedIn()) {
							%>
							<label for="content">게시글 비밀번호</label>
							<input type="password" class="form-control" placeholder="Enter password" name="pwd">
							<label for="content">비밀번호 확인</label>
							<input type="password" class="form-control" placeholder="Enter password" name="pwdConfirm"><%
								}
							%>
 
						</div>
					</form>
					<div class="btn-group">
						<input type="button" class="btn btn-default" onClick="writeBoard();" value="저장" />					
						<input type="button" class="btn btn-default" onClick="addNewAttachedFile(document.writeInputFrm);" value="첨부 파일 추가" />
						<input type="button" class="btn btn-default" onClick="hideWriteEditScreen();" value="닫기" />
					</div>
					<form name="writeProcessFrm" enctype="multipart/form-data" method="post" target="hiddenFrame" action="/servlet/BoardWriteProcess">
						<div class="form-group">
							<input type="hidden" name="boardID" value="<%=boardListRes.getBoardID()%>" />
							<input type="hidden" name="subject" />
							<input type="hidden" name="contents" /><%
	if (! accessedUserformation.isLoginedIn()) {
		out.write(CommonStaticFinalVars.NEWLINE);
		out.write("							");
		out.write("<input type=\"hidden\" name=\"pwd\" />");
	}
%>
							<input type="hidden" name="<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY %>" />
							<input type="hidden" name="<%= WebCommonStaticFinalVars.PARAMETER_KEY_NAME_OF_SESSION_KEY_IV %>" />					
							<!-- 주의점 div 시작 태그와 종료 태그 사이에는 공백을 포함한 어떠한 것도 넣지 말것, 자식 노드로 인식됨 -->
							<div id="newAttachedFileList"></div>
						</div>
					</form>				
				</div>
				<iframe id="hiddenFrame" name="hiddenFrame" style="display:none;"></iframe>		
			</div>			
		</div>
	</div>
</div>
</body>
</html>

>>> 게시판 목록 비지니스 로직

package kr.pe.codda.impl.task.server;
 
import static kr.pe.codda.jooq.tables.SbBoardHistoryTb.SB_BOARD_HISTORY_TB;
import static kr.pe.codda.jooq.tables.SbBoardInfoTb.SB_BOARD_INFO_TB;
import static kr.pe.codda.jooq.tables.SbBoardTb.SB_BOARD_TB;
import static kr.pe.codda.jooq.tables.SbBoardVoteTb.SB_BOARD_VOTE_TB;
import static kr.pe.codda.jooq.tables.SbMemberTb.SB_MEMBER_TB;
 
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashSet;
 
import org.jooq.Record;
import org.jooq.Record1;
import org.jooq.Record13;
import org.jooq.Record3;
import org.jooq.Record4;
import org.jooq.Result;
import org.jooq.Table;
import org.jooq.types.UByte;
import org.jooq.types.UInteger;
import org.jooq.types.UShort;
 
import kr.pe.codda.common.exception.DynamicClassCallException;
import kr.pe.codda.common.exception.ServerServiceException;
import kr.pe.codda.common.message.AbstractMessage;
import kr.pe.codda.impl.message.BoardListReq.BoardListReq;
import kr.pe.codda.impl.message.BoardListRes.BoardListRes;
import kr.pe.codda.impl.message.MessageResultRes.MessageResultRes;
import kr.pe.codda.jooq.tables.SbBoardHistoryTb;
import kr.pe.codda.jooq.tables.SbBoardTb;
import kr.pe.codda.server.PersonalLoginManagerIF;
import kr.pe.codda.server.lib.BoardListType;
import kr.pe.codda.server.lib.BoardStateType;
import kr.pe.codda.server.lib.PermissionType;
import kr.pe.codda.server.lib.ServerCommonStaticFinalVars;
import kr.pe.codda.server.lib.ServerDBUtil;
import kr.pe.codda.server.lib.ValueChecker;
import kr.pe.codda.server.task.AbstractServerTask;
import kr.pe.codda.server.task.ToLetterCarrier;
 
public class BoardListReqServerTask extends AbstractServerTask {
 
	public BoardListReqServerTask() throws DynamicClassCallException {
		super();
	}
 
	private void sendErrorOutputMessage(String errorMessage, ToLetterCarrier toLetterCarrier,
			AbstractMessage inputMessage) throws InterruptedException {
		log.warn("{}, inObj={}", errorMessage, inputMessage.toString());
 
		MessageResultRes messageResultRes = new MessageResultRes();
		messageResultRes.setTaskMessageID(inputMessage.getMessageID());
		messageResultRes.setIsSuccess(false);
		messageResultRes.setResultMessage(errorMessage);
		toLetterCarrier.addSyncOutputMessage(messageResultRes);
	}
 
	@Override
	public void doTask(String projectName, PersonalLoginManagerIF personalLoginManager, ToLetterCarrier toLetterCarrier,
			AbstractMessage inputMessage) throws Exception {
		try {
			AbstractMessage outputMessage = doWork(ServerCommonStaticFinalVars.DEFAULT_DBCP_NAME,
					(BoardListReq) inputMessage);
			toLetterCarrier.addSyncOutputMessage(outputMessage);
		} catch (ServerServiceException e) {
			String errorMessage = e.getMessage();
			log.warn("errmsg=={}, inObj={}", errorMessage, inputMessage.toString());
 
			sendErrorOutputMessage(errorMessage, toLetterCarrier, inputMessage);
			return;
		} catch (Exception e) {
			String errorMessage = new StringBuilder().append("unknwon errmsg=").append(e.getMessage())
					.append(", inObj=").append(inputMessage.toString()).toString();
 
			log.warn(errorMessage, e);
 
			sendErrorOutputMessage("게시글 목록 조회가 실패하였습니다", toLetterCarrier, inputMessage);
			return;
		}
	}
 
	public BoardListRes doWork(String dbcpName, BoardListReq boardListReq) throws Exception {
		// FIXME!
		log.info(boardListReq.toString());
 
		try {
			ValueChecker.checkValidBoardID(boardListReq.getBoardID());		
			ValueChecker.checkValidPageNoAndPageSize(boardListReq.getPageNo(), boardListReq.getPageSize());
		} catch (IllegalArgumentException e) {
			String errorMessage = e.getMessage();
			throw new ServerServiceException(errorMessage);
		}
 
		final UByte boardID = UByte.valueOf(boardListReq.getBoardID());		
 
		final int pageNo = boardListReq.getPageNo();
		final int pageSize = boardListReq.getPageSize();
		final int offset = (pageNo - 1) * pageSize;		
 
		final java.util.List<BoardListRes.Board> boardList = new ArrayList<BoardListRes.Board>();
 
		final BoardListRes boardListRes = new BoardListRes();
 
		ServerDBUtil.execute(dbcpName, (conn, create) -> {
 
			ServerDBUtil.checkUserAccessRights(conn, create, log, "게시글 목록 조회 서비스", PermissionType.GUEST, boardListReq.getRequestedUserID());
 
			Record3<String, Byte, Byte> boardInforRecord = create
					.select(SB_BOARD_INFO_TB.BOARD_NAME, SB_BOARD_INFO_TB.LIST_TYPE, SB_BOARD_INFO_TB.WRITE_PERMISSION_TYPE)
					.from(SB_BOARD_INFO_TB).where(SB_BOARD_INFO_TB.BOARD_ID.eq(boardID)).forUpdate().fetchOne();
 
			if (null == boardInforRecord) {
				try {
					conn.rollback();
				} catch (Exception e) {
					log.warn("fail to rollback");
				}
 
				String errorMessage = new StringBuilder("입력 받은 게시판 식별자[").append(boardID.shortValue())
						.append("]가 게시판 정보 테이블에 존재하지  않습니다").toString();
				throw new ServerServiceException(errorMessage);
			}
 
 
			String boardName = boardInforRecord.get(SB_BOARD_INFO_TB.BOARD_NAME);
			byte boardListTypeValue = boardInforRecord.get(SB_BOARD_INFO_TB.LIST_TYPE);
			byte boardWritePermissionTypeValue = boardInforRecord.get(SB_BOARD_INFO_TB.WRITE_PERMISSION_TYPE);
			// byte boardReplyPolicyTypeValue = boardInforRecord.get(SB_BOARD_INFO_TB.REPLY_POLICY_TYPE);
 
 
			BoardListType boardListType = null;
			try {
				boardListType = BoardListType.valueOf(boardListTypeValue);
 
				PermissionType.valueOf("본문 쓰기", boardWritePermissionTypeValue);
			} catch (IllegalArgumentException e) {
				try {
					conn.rollback();
				} catch (Exception e1) {
					log.warn("fail to rollback");
				}
 
				String errorMessage = e.getMessage();
				throw new ServerServiceException(errorMessage);
			}			
 
 
			int total = create.select(SB_BOARD_INFO_TB.CNT).from(SB_BOARD_INFO_TB)
					.where(SB_BOARD_INFO_TB.BOARD_ID.eq(boardID)).fetchOne(0, Integer.class);
 
			SbBoardTb a = SB_BOARD_TB.as("a");
			SbBoardHistoryTb b = SB_BOARD_HISTORY_TB.as("b");
			SbBoardHistoryTb c = SB_BOARD_HISTORY_TB.as("c");			
 
			HashSet<UInteger> boardNoSet = new HashSet<UInteger>(); 
 
			if (BoardListType.TREE.equals(boardListType)) {
				Table<Record4<UByte, UInteger, UShort, Byte>> d = create.select(a.BOARD_ID, a.GROUP_NO, a.GROUP_SQ, a.BOARD_ST)
						.from(a.forceIndex("sb_board_idx1"))
						.where(a.BOARD_ID.eq(boardID))
						.and(a.BOARD_ST.eq(BoardStateType.OK.getValue()))
						.orderBy(a.GROUP_NO.desc(), a.GROUP_SQ.desc())
						.offset(offset).limit(pageSize).asTable("b");
 
				Result<Record1<UInteger>> boardResult = create
						.select(a.BOARD_NO)
						.from(a).innerJoin(d).on(a.BOARD_ID.eq(d.field(SB_BOARD_TB.BOARD_ID)))						
						.and(a.GROUP_NO.eq(d.field(SB_BOARD_TB.GROUP_NO)))
						.and(a.GROUP_SQ.eq(d.field(SB_BOARD_TB.GROUP_SQ)))
						.and(a.BOARD_ST.eq(d.field(SB_BOARD_TB.BOARD_ST))).fetch();
 
				for (Record1<UInteger> boardRecord : boardResult) {
					UInteger boardNo = boardRecord.get(SB_BOARD_TB.BOARD_NO);
					boardNoSet.add(boardNo);
				}
 
 
			} else {
				Table<Record4<UByte, UInteger, UInteger, Byte>> d = create.select(a.BOARD_ID, a.PARENT_NO, a.BOARD_NO, a.BOARD_ST)
						.from(a.forceIndex("sb_board_idx2"))
						.where(a.BOARD_ID.eq(boardID))
						.and(a.PARENT_NO.eq(UInteger.valueOf(0)))						
						.and(a.BOARD_ST.eq(BoardStateType.OK.getValue()))						
						.orderBy(a.BOARD_NO.desc())
						.offset(offset).limit(pageSize).asTable("b");
 
				Result<Record1<UInteger>> boardResult = create
						.select(a.BOARD_NO)
						.from(a).innerJoin(d).on(a.BOARD_ID.eq(d.field(SB_BOARD_TB.BOARD_ID)))
						.and(a.PARENT_NO.eq(d.field(SB_BOARD_TB.PARENT_NO)))
						.and(a.BOARD_NO.eq(d.field(SB_BOARD_TB.BOARD_NO)))
						.and(a.BOARD_ST.eq(d.field(SB_BOARD_TB.BOARD_ST))).fetch();
 
				for (Record1<UInteger> boardRecord : boardResult) {
					UInteger boardNo = boardRecord.get(SB_BOARD_TB.BOARD_NO);
					boardNoSet.add(boardNo);
				}
 
			}
 
			if (! boardNoSet.isEmpty()) {
				Result<Record13<UInteger, UInteger, UShort, UInteger, UByte, Integer, Byte, Object, String, Timestamp, String, Object, Timestamp>> boardResult = null;
				boardResult = create
						.select(a.field(SB_BOARD_TB.BOARD_NO), a.field(SB_BOARD_TB.GROUP_NO),
								a.field(SB_BOARD_TB.GROUP_SQ), a.field(SB_BOARD_TB.PARENT_NO),
								a.field(SB_BOARD_TB.DEPTH), a.field(SB_BOARD_TB.VIEW_CNT),
								a.field(SB_BOARD_TB.BOARD_ST),
								create.selectCount().from(SB_BOARD_VOTE_TB)
										.where(SB_BOARD_VOTE_TB.BOARD_ID.eq(a.field(SB_BOARD_TB.BOARD_ID)))
										.and(SB_BOARD_VOTE_TB.BOARD_NO.eq(a.field(SB_BOARD_TB.BOARD_NO)))
										.asField("votes"),
								b.SUBJECT, b.REG_DT.as("last_mod_date"), c.REGISTRANT_ID,
								create.select(SB_MEMBER_TB.NICKNAME).from(SB_MEMBER_TB)
										.where(SB_MEMBER_TB.USER_ID.eq(c.REGISTRANT_ID))
										.asField(SB_MEMBER_TB.NICKNAME.getName()),
								c.REG_DT.as("first_reg_date"))
						.from(a)
						.innerJoin(c).on(c.BOARD_ID.eq(a.field(SB_BOARD_TB.BOARD_ID)))
							.and(c.BOARD_NO.eq(a.field(SB_BOARD_TB.BOARD_NO)))
							.and(c.HISTORY_SQ.eq(UByte.valueOf(0)))
						.innerJoin(b).on(b.BOARD_ID.eq(a.field(SB_BOARD_TB.BOARD_ID)))
							.and(b.BOARD_NO.eq(a.field(SB_BOARD_TB.BOARD_NO)))
							.and(b.HISTORY_SQ.eq(create.select(b.HISTORY_SQ.max()).from(b)
								.where(b.BOARD_ID.eq(a.field(SB_BOARD_TB.BOARD_ID)))
								.and(b.BOARD_NO.eq(a.field(SB_BOARD_TB.BOARD_NO)))))
						.where(a.BOARD_ID.eq(boardID))
						.and(a.BOARD_NO.in(boardNoSet))
						.orderBy(a.field(SB_BOARD_TB.GROUP_NO).desc(), a.field(SB_BOARD_TB.GROUP_SQ).desc())
						.fetch();			
 
				for (Record boardRecord : boardResult) {
					UInteger boardNo = boardRecord.getValue(SB_BOARD_TB.BOARD_NO);
					UInteger groupNo = boardRecord.getValue(SB_BOARD_TB.GROUP_NO);
					UShort groupSequence = boardRecord.getValue(SB_BOARD_TB.GROUP_SQ);
					UInteger parentNo = boardRecord.getValue(SB_BOARD_TB.PARENT_NO);
					UByte depth = boardRecord.getValue(SB_BOARD_TB.DEPTH);
					int viewCount = boardRecord.getValue(SB_BOARD_TB.VIEW_CNT);
					byte boardStateValue = boardRecord.getValue(SB_BOARD_TB.BOARD_ST);
					int votes = boardRecord.getValue("votes", Integer.class);
					String subject = boardRecord.getValue(SB_BOARD_HISTORY_TB.SUBJECT);
					Timestamp lastModifiedDate = boardRecord.getValue("last_mod_date", Timestamp.class);
					String firstWriterID = boardRecord.getValue(SB_BOARD_HISTORY_TB.REGISTRANT_ID);
					String firstWriterNickName = boardRecord.getValue(SB_MEMBER_TB.NICKNAME);
					Timestamp firstRegisteredDate = boardRecord.getValue("first_reg_date", Timestamp.class);
 
					if (null == subject) {
						subject = "";
					}
 
					BoardListRes.Board board = new BoardListRes.Board();
					board.setBoardNo(boardNo.longValue());
					board.setGroupNo(groupNo.longValue());
					board.setGroupSeq(groupSequence.intValue());
					board.setParentNo(parentNo.longValue());
					board.setDepth(depth.shortValue());
					board.setWriterID(firstWriterID);
					board.setViewCount(viewCount);
					board.setBoardSate(boardStateValue);
					board.setRegisteredDate(firstRegisteredDate);
					board.setWriterNickname(firstWriterNickName);
					board.setVotes(votes);
					board.setSubject(subject);
					board.setLastModifiedDate(lastModifiedDate);
 
					// log.info(board.toString());
					boardList.add(board);
				}
			}			
 
			conn.commit();
 
			boardListRes.setBoardID(boardID.shortValue());
			boardListRes.setBoardName(boardName);
			boardListRes.setBoardListType(boardListTypeValue);	
			boardListRes.setBoardWritePermissionType(boardWritePermissionTypeValue);
			boardListRes.setPageNo(pageNo);
			boardListRes.setPageSize(pageSize);
			boardListRes.setTotal(total);
			boardListRes.setCnt(boardList.size());
			boardListRes.setBoardList(boardList);
		});
 
		return boardListRes;
	}
}


------
"JDF 개발 선언문- 아주 오래 전 자바는 말이지... "
https://okky.kr/article/99411