1탄에서 다루었던 구현 방식에서 좀 더 자세하게 CRUD 기능 구현 코드를 살펴보려고 한다. 사용자 요청부터 순서대로 따라가면서 정리할 계획이다.
// insert.do로 GET 요청시에 입력 페이지를 리턴해줍니다.
@GetMapping("/insert.do")
public String getInsertPage(Model model) {
return "operate/flightLog/flightLogForm";
}
// insert.do로 POST 요청시에 처리과정
@PostMapping("/insert.do")
public String insertFLog(
@ModelAttribute("fLog") @Validated(InsertGroup.class) flightLogVO fLog
, Errors errors
, RedirectAttributes redirectAttributes
) {
String viewName = null;
// 에러가 없다면,
if (!errors.hasErrors()) {
// id(PK)값을 랜덤으로 생성
fLog.setFlId(UUID.randomUUID().toString());
// 일지 생성하고 결과 받기
boolean success = fLogService.createLog(fLog);
if (success) { // create 성공시에
redirectAttributes.addFlashAttribute("message", "일지작성이 완료되었습니다.");
// 성공메시지를 가지고, 생성된 일지 페이지로 리다이렉트 요청
viewName = "redirect:/operate/flightlog/view.do?what=" + fLog.getFlId();
} else { // 실패시에, 실패메시지, 기존의 입력값과 함께 입력폼으로 가기
redirectAttributes.addFlashAttribute("message", "일지작성에 실패하였습니다.");
viewName = "operate/flightLog/flightLogForm";
}
} else {
// 유효성 검사 실패시에도, 입력폼으로 이동
viewName = "operate/flightLog/flightLogForm";
}
return viewName;
}
@Override
public boolean createLog(flightLogVO log) {
processAtchFileGroup(log);
return fLogDao.insertLog(log) > 0;
}
// 첨부파일 처리 로직
private void processAtchFileGroup(flightLogVO fLog) {
// 사용자로부터 입력받은 파일 정보 가져오기
MultipartFile[] flFiles = fLog.getFlFiles();
// 파일이 없다면, 처리안함.
if (flFiles == null) return;
// 입력받은 파일들을 돌면서 AtchFileDetailVO로 convert 수행
List<AtchFileDetailVO> detailList = new ArrayList<>();
for (MultipartFile flFile : flFiles) {
if (flFile.isEmpty()) continue;
detailList.add(new AtchFileDetailVO(flFile));
}
// 변환된 데이터 리스트를 가지고 로직 수행
if (detailList.size() > 0) {
// 하나의 첨부파일 틀을 만들기
AtchFileVO fileGroup = new AtchFileVO();
// 파일에 실제 파일데이터들 넣어주기
fileGroup.setDetailList(detailList);
// 1. 첨부파일의 메타 데이터 저장
// 2. 첨부파일의 2진 데이터 저장
try {
// 이미 아이디가 들어있다는 것은, 수정하는 경우
if (StringUtils.isNotBlank(fLog.getAtchFileId())) {
// 운항일지 수정인경우 첨부파일 데이터 전부 삭제하고 다시 생성
atchService.removeAtchFileGroup(fLog.getAtchFileId(), atchPath);
}
// 운항일지 작성하기
fileGroup.setAtchFileId(UUID.randomUUID().toString().substring(0, 20));
atchService.createAtchFileGroup(fileGroup, atchPath);
fLog.setAtchFileId(fileGroup.getAtchFileId());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
위의 코드에서 파일데이터에 대한 정보를 저장하고 아래에서 실제로 DB에 첨부파일 데이터 저장 및 파일복사가 이루어진다.
@Override
public void createAtchFileGroup(AtchFileVO fileGroup, Resource saveRes) throws IOException {
// detailList 정보 꺼내와서
List<AtchFileDetailVO> detailList = fileGroup.getDetailList();
// 실제 파일 경로 저장하기
if (detailList != null) {
for (AtchFileDetailVO single : detailList) {
File saveFile = new File(saveRes.getFile(), single.getFiNm());
single.setFiCours(saveFile.getCanonicalPath());
}
}
// 각 파일에 대한 경로 저장하기
int cnt = atchDAO.insertAtchFileGroup(fileGroup);
// 성공적으로 DB에 저장된 이후에 실제로 첨부파일 저장하기
if (cnt > 1) {
for (AtchFileDetailVO single : detailList) {
File saveFile = new File(single.getFiCours());
single.getUploadFile().transferTo(saveFile);
}
}
}
순서대로 보자면, 입력받은 첨부파일에 대한 정보를 먼저 DB에 저장한 이후, 실제로 해당 파일들을 지정한 경로에 복사하는 과정을 거치게 된다.
<insert id="insertAtchFileGroup" parameterType="AtchFileVO">
INSERT ALL
INTO ATCH_FILE (ATCH_FILE_ID, CREAT_DT, USE_AT)
VALUES(#{atchFileId}, SYSDATE, 'Y')
<foreach collection="detailList" item="atch" index="idx">
INTO ATCH_FILE_DETAIL(
ATCH_FILE_ID
, FI_SN
, FI_COURS
, FI_NM
, FI_ORIGIN_NM
, FI_EXTSN
, FI_SIZE
, FI_MIME
) VALUES(
#{atchFileId,jdbcType=NUMERIC}
, #{idx,jdbcType=NUMERIC}+1
, #{atch.fiCours,jdbcType=VARCHAR}
, #{atch.fiNm,jdbcType=VARCHAR}
, #{atch.fiOriginNm,jdbcType=VARCHAR}
, #{atch.fiExtsn,jdbcType=VARCHAR}
, #{atch.fiSize,jdbcType=NUMERIC}
, #{atch.fiMime,jdbcType=VARCHAR}
)
</foreach>
SELECT * FROM DUAL
</insert>
단순히 첨부파일 아이디(atch_file_id)와 생성날짜 정보만 가지고 있는다.
첨부파일 아이디와 순번을 복합키로 갖는 테이블로, 실제 각각의 첨부파일에 대한 정보(파일명, 파일크기, 경로, 확장자 등)를 갖고 있는다.
atch_file : atch_file_detail = 1 : N의 관계를 가지게 된다.
이제 첨부파일이 필요한 테이블은 atch_file_id과 1:1 관계를 맺게 되고 결과적으로 여러개의 파일들을 저장할 수 있는 구조가 된다.
list.do 요청시에는
페이징 처리를 수행하고, view.do 요청시 특정 일지에 대한 정보 반환
@GetMapping("/list.do")
public String fLogList(@RequestParam(name = "page", required = false, defaultValue = "1") long currentPage,
Model model) {
PaginationInfo<flightLogVO> paging = new PaginationInfo<>();
paging.setCurrentPage(currentPage);
List<flightLogVO> fLogList = fLogService.retrieveList(paging);
paging.setDataList(fLogList);
model.addAttribute("paging", paging);
return "operate/flightLog/flightLogList";
}
@GetMapping("/view.do")
public String fLogView(@RequestParam(name = "what") String flId, Model model) {
flightLogVO fLog = fLogService.retrieveOne(flId);
model.addAttribute("fLog", fLog);
return "operate/flightLog/flightLogView";
}
@Override
public List<flightLogVO> retrieveList(PaginationInfo paging) {
long totalRecord = fLogDao.selectTotalRecord();
paging.setTotalRecord(totalRecord);
return fLogDao.selectList(paging);
}
<select id="selectList" parameterType="PaginationInfo" resultMap="flListMap">
SELECT B.*
FROM (
SELECT ROWNUM RNUM, A.*
FROM (
SELECT
FL_ID, FL_TITLE, FL_CONTENT, FL_CRT_TS, FL_UPD_TS, FL_WRITER,
EMP_NM
FROM FLIGHT_LOG INNER JOIN EMPLOYEE ON FL_WRITER = EMP_NO
ORDER BY FL_CRT_TS DESC
) A
) B
<![CDATA[
WHERE RNUM >= #{startRow} AND RNUM <= #{endRow}
]]>
</select>
게시판 사이트를 들어가보면, 게시물들이 나열되서 조회가 되고, 아래에서 1~n까지의 페이지들을 볼 수 있다. 이처럼 수많은 데이터들을 전부 다 가져오는 것이 아닌,
페이징 처리가 필요하다.
@GetMapping("/list.do")
public String fLogList(@RequestParam(name = "page", required = false, defaultValue = "1") long currentPage,
Model model) {
PaginationInfo<flightLogVO> paging = new PaginationInfo<>();
paging.setCurrentPage(currentPage);
List<flightLogVO> fLogList = fLogService.retrieveList(paging);
paging.setDataList(fLogList);
model.addAttribute("paging", paging);
return "operate/flightLog/flightLogList";
}
public void setCurrentPage(long currentPage) {
this.currentPage = currentPage;
this.endRow = currentPage * screenSize;
this.startRow = endRow - (screenSize - 1);
this.endPage = ((currentPage + blockSize - 1) / blockSize) * blockSize;
this.startPage = endPage - (blockSize - 1);
}
<select id="selectList" parameterType="PaginationInfo" resultMap="flListMap">
SELECT B.*
FROM (
SELECT ROWNUM RNUM, A.*
FROM (
SELECT
FL_ID, FL_TITLE, FL_CONTENT, FL_CRT_TS, FL_UPD_TS, FL_WRITER,
EMP_NM
FROM FLIGHT_LOG INNER JOIN EMPLOYEE ON FL_WRITER = EMP_NO
ORDER BY FL_CRT_TS DESC
) A
) B
<![CDATA[
WHERE RNUM >= #{startRow} AND RNUM <= #{endRow}
]]>
</select>
ROWNUM은 SELECT 데이터에
일련번호를 붙이는 것으로, 특정 집합에서 원하는 행만 가져오고 싶을 때 사용한다.TOP 3, 페이징 등
일반적으로 다음과 같이 사용할 수 있다.
SELECT * FROM EMPLOYEE E WHERE ROWNUM <= 10; // Top3 수행 가능
하지만, RNUM은 반환되는 결과의 임시 행번호이기 때문에 반드시 1부터 나와야 한다. 따라서 페이징이나 특정 행을 추출하고 싶다면, SELECT문으로 한번더 감싸서 사용할 수 있다.
SELECT ROWNUM, E.* FROM EMPLOYEE E WHERE ROWNUM BETWEEN 5 AND 10; // 불가
SELECT
A.*
FROM
(
SELECT ROWNUM RNUM, E.* FROM EMPLOYEE E
) A
WHERE RNUM BETWEEN 5 AND 10;
다만, 위처럼 ROWNUM은 예약어이기 때문에 그냥 사용하게 되면, 또 바깥 SELECT 기준으로 잡기 때문에 Alias를 이용하여 조건을 걸어줄 수 있다. 하나 문제점은 대부분 우리는 데이터를 가져올 때, 특정한 기준을 잡고 정렬하는 것이 필요하다. 일반적으로 게시물들을 가져올 때, 게시일 기준으로 정렬을 수행해서 가져와야 한다. ROWNUM은 ORDER BY절 이전에 행번호를 매기기 때문에 위와 같이 가져온다면, 엉뚱한 번호를 가져올 것이다.

따라서 SELECT로 한번 더 감싸서 사용할 필요가 있다.
SELECT
B.*
FROM
(
SELECT
ROWNUM RNUM, A.*
FROM
(
SELECT
EMP_NO, EMP_NM, EMP_BIR, EMP_GENDER
, EMP_EMAIL, EMP_ADDR, EMP_CONTACT
, EMP_STATUS, EMP_JOIN_DT, EMP_RESIGN_DT
FROM EMPLOYEE
ORDER BY EMP_JOIN_DT
) A
) B
WHERE RNUM BETWEEN 5 AND 10;
순서를 다시 정리해보자면
1. 먼저 정렬을 수행한 결과를 SELECT로 감싸기
2. ROWNUM을 별칭으로 바꾸고 SELECT로 감싸기
3. 가장 바깥 SELECT문에서 해당 별칭을 가지고 조건을 수행
