코다와 함께 성장합니다. ^.^

emptynote의 이미지

현재 코다 서버 비지니스 로직을 개선중입니다.

썸머노트 위지웍 HTML 에디터라 코다 관련 문서를 이거 이용하여 작성하면 좋겠다

생각하여 문서 서비스를 개발하게 되었습니다.

문서 서비스는 기본 게시판 제목 + 내용 에 파일명 정도 추가하여 '적용' 버튼 클릭시 지정한 파일명으로 '제목' + '내용' 을 바탕으로 템플릿에 넣어 최종 html 로 만들어 주는 서비스입니다.

그런데 이거 만들면서 기존 게시판 하나 더 추가해서 문서 관련 '파일명' 이라는 정보만 추가하고 했는데

아 설계를 잘못하여 DB 트랜재션으로 묶을 수가 없네요.

그래서 개선을 하고 있습니다.

개선된 소스를 보시면 아시겠지만 개발자가 작성할 비지니스 로직과 트랜재션을 완전히 분리해 놓았습니다.

무엇이 조립 가능한 콤포넌트 인가? 고민이 되더군요.

조립 가능한 콤포넌트는 트랜재션이 분리된 비지니스 로직이라는 결론에 도달하게 되었습니다.

그래서 그 방향에 맞추어서 비지니스 로직과 트랜재션을 분리를 하였습니다.

db 트랜재션을 완전히 개발자가 전적으로 맡아서 할 수 있는 방법도 제공할 생각입니다.

다행히도 코다 커뮤니티 개발 수준에서는 직접 트랜재션을 맡아 처리할게 없네요.

그런데 만들고 보니 프레임워크라면 무엇인가 강제적으로 해야 할것 같은데

제가 만든것은 개발자가 자유 의사로 2개중 하나를 선택하여 그것에 맞춘것 뿐이라서

이것을 프레임워크라고 말을 해야 하나 합니다.

뭐... 내맘대로 프레임워크라서 그런거라고 세상에 외치고 싶네요 ^^

=========== 신 코드

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      <a href="http://www.apache.org/licenses/LICENSE-2.0
" rel="nofollow">http://www.apache.org/licenses/LICENSE-2.0
</a> * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package kr.pe.codda.impl.task.server;
 
import static kr.pe.codda.jooq.tables.SbDocHistoryTb.SB_DOC_HISTORY_TB;
import static kr.pe.codda.jooq.tables.SbDocTb.SB_DOC_TB;
import static kr.pe.codda.jooq.tables.SbSeqTb.SB_SEQ_TB;
 
import java.sql.Timestamp;
 
import org.jooq.DSLContext;
import org.jooq.Record1;
import org.jooq.types.UInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import kr.pe.codda.common.exception.DynamicClassCallException;
import kr.pe.codda.common.exception.ServerTaskException;
import kr.pe.codda.common.message.AbstractMessage;
import kr.pe.codda.impl.message.DocumentWriteReq.DocumentWriteReq;
import kr.pe.codda.impl.message.DocumentWriteRes.DocumentWriteRes;
import kr.pe.codda.server.LoginManagerIF;
import kr.pe.codda.server.lib.DBTaskIF;
import kr.pe.codda.server.lib.DocumentStateType;
import kr.pe.codda.server.lib.PermissionType;
import kr.pe.codda.server.lib.RollbackServerTaskException;
import kr.pe.codda.server.lib.SequenceType;
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;
 
/**
 * 문서 쓰기 요청 처리 담당 서버 비지니스 로직
 * 
 * @author Won Jonghoon
 *
 */
public class DocumentWriteReqServerTask extends AbstractServerTask implements DBTaskIF<DocumentWriteReq, DocumentWriteRes> {
	private Logger log = LoggerFactory.getLogger(DocumentWriteReqServerTask.class);
 
	/**
	 * 생성자
	 * @throws DynamicClassCallException 동적 호출 작업중 에러 발생시 던지는 예외
	 */
	public DocumentWriteReqServerTask() throws DynamicClassCallException {
		super();
	}
 
 
	@Override
	public void doTask(String projectName, LoginManagerIF personalLoginManager, ToLetterCarrier toLetterCarrier,
			AbstractMessage inputMessage) throws Exception {
 
		/*
		AbstractMessage outputMessage = doWork(ServerCommonStaticFinalVars.DEFAULT_DBCP_NAME,
				(DocumentWriteReq) inputMessage);
		toLetterCarrier.addSyncOutputMessage(outputMessage);
		*/
 
 
		AbstractMessage outputMessage = ServerDBUtil.doDBWork(ServerCommonStaticFinalVars.DEFAULT_DBCP_NAME, (DocumentWriteReq)inputMessage, this);
 
		toLetterCarrier.addSyncOutputMessage(outputMessage);
	}
 
	@Override
	public DocumentWriteRes doWork(final DSLContext dsl, final DocumentWriteReq documentWriteReq) throws Exception {
		/** FIXME! */
		log.info(documentWriteReq.toString());
 
		try {
			ValueChecker.checkValidUserID(documentWriteReq.getRequestedUserID());
			ValueChecker.checkValidIP(documentWriteReq.getIp());
			ValueChecker.checkValidFileName(documentWriteReq.getFileName());
			ValueChecker.checkValidSubject(documentWriteReq.getSubject());
			ValueChecker.checkValidContents(documentWriteReq.getContents());
		} catch (IllegalArgumentException e) {
			String errorMessage = e.getMessage();
			throw new ServerTaskException(errorMessage);
		}
 
		DocumentWriteRes documentWriteRes = new DocumentWriteRes();
 
		final Timestamp registeredDate = new java.sql.Timestamp(System.currentTimeMillis());
		final UInteger lastDocumentSequence = UInteger.valueOf(0);
 
 
			ServerDBUtil.checkUserAccessRights( dsl, log, "문서 조회 서비스", PermissionType.ADMIN,
					documentWriteReq.getRequestedUserID());
 
			/** 문서 쓰기와 수정을 위한 문서 번호 락 */
			Record1<UInteger> seqRecord = dsl.select(SB_SEQ_TB.SQ_VALUE).from(SB_SEQ_TB)
					.where(SB_SEQ_TB.SQ_ID.eq(SequenceType.DOCUMENT_NO.getSequenceID())).forUpdate().fetchOne();
 
			if (null == seqRecord) {
				String errorMessage = new StringBuilder("시퀀스 테이블에서 문서 번호 시퀀스[")
						.append(SequenceType.DOCUMENT_NO.getSequenceID()).append("]가 존재하지  않습니다").toString();
				throw new RollbackServerTaskException(errorMessage);
			}
 
			UInteger newDocumentNo = seqRecord.get(SB_SEQ_TB.SQ_VALUE);
 
			documentWriteRes.setDocumentNo(newDocumentNo.longValue());
 
			dsl.update(SB_SEQ_TB).set(SB_SEQ_TB.SQ_VALUE, SB_SEQ_TB.SQ_VALUE.add(1))
					.where(SB_SEQ_TB.SQ_ID.eq(SequenceType.DOCUMENT_NO.getSequenceID())).execute();
 
			dsl.insertInto(SB_DOC_TB).set(SB_DOC_TB.DOC_NO, newDocumentNo)
					.set(SB_DOC_TB.DOC_STATE, DocumentStateType.OK.getValue())
					.set(SB_DOC_TB.LAST_DOC_SQ, lastDocumentSequence).execute();
 
			dsl.insertInto(SB_DOC_HISTORY_TB).set(SB_DOC_HISTORY_TB.DOC_NO, newDocumentNo)
					.set(SB_DOC_HISTORY_TB.DOC_SQ, lastDocumentSequence)
					.set(SB_DOC_HISTORY_TB.FILE_NAME, documentWriteReq.getFileName())
					.set(SB_DOC_HISTORY_TB.SUBJECT, documentWriteReq.getSubject())
					.set(SB_DOC_HISTORY_TB.CONTENTS, documentWriteReq.getContents())
					.set(SB_DOC_HISTORY_TB.REG_DT, registeredDate).execute();
 
 
		return documentWriteRes;
	}
}

=========== 구 코드

package kr.pe.codda.impl.task.server;
 
import static kr.pe.codda.jooq.tables.SbBoardFilelistTb.SB_BOARD_FILELIST_TB;
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 java.sql.Timestamp;
import java.util.List;
 
import org.jooq.Record3;
import org.jooq.types.UByte;
import org.jooq.types.UInteger;
import org.jooq.types.UShort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import kr.pe.codda.common.etc.CommonStaticFinalVars;
import kr.pe.codda.common.exception.DynamicClassCallException;
import kr.pe.codda.common.exception.ServerTaskException;
import kr.pe.codda.common.message.AbstractMessage;
import kr.pe.codda.impl.message.BoardWriteReq.BoardWriteReq;
import kr.pe.codda.impl.message.BoardWriteRes.BoardWriteRes;
import kr.pe.codda.server.LoginManagerIF;
import kr.pe.codda.server.lib.BoardStateType;
import kr.pe.codda.server.lib.MemberActivityType;
import kr.pe.codda.server.lib.MemberRoleType;
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 BoardWriteReqServerTask extends AbstractServerTask {
	private Logger log = LoggerFactory.getLogger(AccountSearchProcessReqServerTask.class);
 
	public BoardWriteReqServerTask() throws DynamicClassCallException {
		super();
	}
 
	@Override
	public void doTask(String projectName, LoginManagerIF personalLoginManager, ToLetterCarrier toLetterCarrier,
			AbstractMessage inputMessage) throws Exception {
 
		AbstractMessage outputMessage = doWork(ServerCommonStaticFinalVars.DEFAULT_DBCP_NAME,
				(BoardWriteReq) inputMessage);
		toLetterCarrier.addSyncOutputMessage(outputMessage);
	}
 
	public BoardWriteRes doWork(String dbcpName, BoardWriteReq boardWriteReq) throws Exception {
		// FIXME!
		log.info(boardWriteReq.toString());
 
		try {
			ValueChecker.checkValidRequestedUserID(boardWriteReq.getRequestedUserID());
			ValueChecker.checkValidIP(boardWriteReq.getIp());
			ValueChecker.checkValidBoardID(boardWriteReq.getBoardID());		
			ValueChecker.checkValidBoardPasswordHashBase64(boardWriteReq.getPwdHashBase64());
			ValueChecker.checkValidSubject(boardWriteReq.getSubject());		
			ValueChecker.checkValidContents(boardWriteReq.getContents());
			ValueChecker.checkValidAttachedFilCount(boardWriteReq.getNewAttachedFileCnt());
		} catch (IllegalArgumentException e) {
			String errorMessage = e.getMessage();
			throw new ServerTaskException(errorMessage);
		}
 
		if (boardWriteReq.getNewAttachedFileCnt() > 0) {
			int newAttachedFileCnt = boardWriteReq.getNewAttachedFileCnt();
			List<BoardWriteReq.NewAttachedFile> newAttachedFileList = boardWriteReq.getNewAttachedFileList();
 
			for (int i = 0; i < newAttachedFileCnt; i++) {
				BoardWriteReq.NewAttachedFile newAttachedFile = newAttachedFileList.get(i);
				try {
					ValueChecker.checkValidFileName(newAttachedFile.getAttachedFileName());
				} catch (IllegalArgumentException e) {
					String errorMessage = new StringBuilder().append(i).append("번째 파일 이름 유효성 검사 에러 메시지::")
							.append(e.getMessage()).toString();
					throw new ServerTaskException(errorMessage);
				}
 
				if (newAttachedFile.getAttachedFileSize() <= 0) {
					String errorMessage = new StringBuilder().append(i).append("번째 파일[")
							.append(newAttachedFile.getAttachedFileName()).append("] 크기가 0보다 작거나 같습니다").toString();
					throw new ServerTaskException(errorMessage);
				}
			}
		}
 
		final UByte boardID = UByte.valueOf(boardWriteReq.getBoardID());
		final UByte nextAttachedFileSeq = UByte.valueOf(boardWriteReq.getNewAttachedFileCnt());
		final String boardPasswordHashBase64 = (null == boardWriteReq.getPwdHashBase64()) ? "" : boardWriteReq.getPwdHashBase64(); 
 
		final BoardWriteRes boardWriteRes = new BoardWriteRes();
 
		ServerDBUtil.execute(dbcpName, (conn, dsl) -> {
 
			/**
			 * '게시판 식별자 정보'(SB_BOARD_INFO_TB) 테이블에는 '다음 게시판 번호'가 있어 락을 건후 1 증가 시키고 가져온 값은 '게시판 번호'로 사용한다
			 */
			Record3<String, Byte, UInteger> boardInforRecord = dsl
					.select(SB_BOARD_INFO_TB.BOARD_NAME, SB_BOARD_INFO_TB.WRITE_PERMISSION_TYPE,
							SB_BOARD_INFO_TB.NEXT_BOARD_NO)
					.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 ServerTaskException(errorMessage);
			}
 
			String boardName = boardInforRecord.get(SB_BOARD_INFO_TB.BOARD_NAME);
			byte boardWritePermssionTypeValue = boardInforRecord.get(SB_BOARD_INFO_TB.WRITE_PERMISSION_TYPE);
			UInteger boardNo = boardInforRecord.get(SB_BOARD_INFO_TB.NEXT_BOARD_NO);
 
			if (boardNo.longValue() == CommonStaticFinalVars.UNSIGNED_INTEGER_MAX) {
				try {
					conn.rollback();
				} catch (Exception e) {
					log.warn("fail to rollback");
				}
 
				String errorMessage = new StringBuilder()
						.append(boardName)
						.append(" 게시판[").append(boardID.shortValue())
						.append("]은 최대 갯수까지 글이 등록되어 더 이상 글을 추가 할 수 없습니다").toString();
				throw new ServerTaskException(errorMessage);
			}
 
			PermissionType boardWritePermissionType = null;
 
			try {
				boardWritePermissionType = PermissionType.valueOf("본문글 쓰기", boardWritePermssionTypeValue);
			} catch (IllegalArgumentException e) {
				try {
					conn.rollback();
				} catch (Exception e1) {
					log.warn("fail to rollback");
				}
 
				String errorMessage = e.getMessage();
				throw new ServerTaskException(errorMessage);
			}
 
			MemberRoleType memberRoleTypeOfRequestedUserID = ServerDBUtil.checkUserAccessRights(dsl, log, "게시판 본문 글 등록 서비스", boardWritePermissionType, boardWriteReq.getRequestedUserID());
 
			if (MemberRoleType.GUEST.equals(memberRoleTypeOfRequestedUserID)) {
				if (boardPasswordHashBase64.isEmpty()) {
					try {
						conn.rollback();
					} catch (Exception e1) {
						log.warn("fail to rollback");
					}
 
					String errorMessage = "손님의 경우 반듯이 게시글에 대한 비밀번호를 입력해야 합니다";
					throw new ServerTaskException(errorMessage);
				}
			}
 
			dsl.update(SB_BOARD_INFO_TB)
			.set(SB_BOARD_INFO_TB.NEXT_BOARD_NO, SB_BOARD_INFO_TB.NEXT_BOARD_NO.add(1))
			.where(SB_BOARD_INFO_TB.BOARD_ID.eq(boardID))
			.execute();
 
			conn.commit();			
 
			int boardInsertCount = dsl.insertInto(SB_BOARD_TB).set(SB_BOARD_TB.BOARD_ID, boardID)
					.set(SB_BOARD_TB.BOARD_NO, boardNo).set(SB_BOARD_TB.GROUP_NO, boardNo)
					.set(SB_BOARD_TB.GROUP_SQ, UShort.valueOf(0)).set(SB_BOARD_TB.PARENT_NO, UInteger.valueOf(0L))
					.set(SB_BOARD_TB.DEPTH, UByte.valueOf(0)).set(SB_BOARD_TB.VIEW_CNT, Integer.valueOf(0))
					.set(SB_BOARD_TB.BOARD_ST, BoardStateType.OK.getValue())
					.set(SB_BOARD_TB.NEXT_ATTACHED_FILE_SQ, nextAttachedFileSeq)
					.set(SB_BOARD_TB.PWD_BASE64, (boardPasswordHashBase64.isEmpty() ? null : boardPasswordHashBase64))
					.execute();
 
			if (0 == boardInsertCount) {
				try {
					conn.rollback();
				} catch (Exception e) {
					log.warn("fail to rollback");
				}
				String errorMessage = "게시판 본문 글 등록이 실패하였습니다";
				throw new ServerTaskException(errorMessage);
			}
 
			Timestamp registeredDate = new java.sql.Timestamp(System.currentTimeMillis());
 
			int boardHistoryInsertCount = dsl.insertInto(SB_BOARD_HISTORY_TB)
					.set(SB_BOARD_HISTORY_TB.BOARD_ID, boardID).set(SB_BOARD_HISTORY_TB.BOARD_NO, boardNo)
					.set(SB_BOARD_HISTORY_TB.HISTORY_SQ, UByte.valueOf(0))
					.set(SB_BOARD_HISTORY_TB.SUBJECT, boardWriteReq.getSubject())
					.set(SB_BOARD_HISTORY_TB.CONTENTS, boardWriteReq.getContents())
					.set(SB_BOARD_HISTORY_TB.REGISTRANT_ID, boardWriteReq.getRequestedUserID())
					.set(SB_BOARD_HISTORY_TB.IP, boardWriteReq.getIp())
					.set(SB_BOARD_HISTORY_TB.REG_DT, registeredDate).execute();
 
			if (0 == boardHistoryInsertCount) {
				try {
					conn.rollback();
				} catch (Exception e) {
					log.warn("fail to rollback");
				}
				String errorMessage = "게시판 본문 글 내용을 저장하는데 실패하였습니다";
				throw new ServerTaskException(errorMessage);
			}
 
			if (boardWriteReq.getNewAttachedFileCnt() > 0) {
				int attachedFileListIndex = 0;
 
				for (BoardWriteReq.NewAttachedFile attachedFileForRequest : boardWriteReq.getNewAttachedFileList()) {
					int boardFileListInsertCount = dsl.insertInto(SB_BOARD_FILELIST_TB)
							.set(SB_BOARD_FILELIST_TB.BOARD_ID, boardID).set(SB_BOARD_FILELIST_TB.BOARD_NO, boardNo)
							.set(SB_BOARD_FILELIST_TB.ATTACHED_FILE_SQ, UByte.valueOf(attachedFileListIndex))
							.set(SB_BOARD_FILELIST_TB.ATTACHED_FNAME, attachedFileForRequest.getAttachedFileName())
							.set(SB_BOARD_FILELIST_TB.ATTACHED_FSIZE, attachedFileForRequest.getAttachedFileSize())
							.execute();
 
					if (0 == boardFileListInsertCount) {
						try {
							conn.rollback();
						} catch (Exception e) {
							log.warn("fail to rollback");
						}
						String errorMessage = "게시판 첨부 파일을  저장하는데 실패하였습니다";
						log.warn("게시판 첨부 파일 목록내 인덱스[{}]의 첨부 파일 이름을 저장하는데 실패하였습니다", attachedFileListIndex);
 
						throw new ServerTaskException(errorMessage);
					}
 
					attachedFileListIndex++;
				}
			}
 
			dsl.update(SB_BOARD_INFO_TB).set(SB_BOARD_INFO_TB.CNT, SB_BOARD_INFO_TB.CNT.add(1))
					.set(SB_BOARD_INFO_TB.TOTAL, SB_BOARD_INFO_TB.TOTAL.add(1))
					.where(SB_BOARD_INFO_TB.BOARD_ID.eq(boardID)).execute();
 
			conn.commit();
 
			ServerDBUtil.insertMemberActivityHistory(conn, dsl, log, boardWriteReq.getRequestedUserID(), 
					memberRoleTypeOfRequestedUserID, MemberActivityType.WRITE, boardID, boardNo, registeredDate);
 
			conn.commit();
 
			boardWriteRes.setBoardID(boardID.shortValue());
			boardWriteRes.setBoardNo(boardNo.longValue());
		});
 
		return boardWriteRes;
	}
}