[Servlet, JSP] 231214 / 웹 서비스, POST&PUT, 차시예고(1-3)

이슬기·2023년 12월 14일

servlet

목록 보기
7/8

웹 서비스 & 어제 복습

www.naver.com검색했을 때 발생되는 과정을 쭉 설명할 수 있어야 완전히 이해했다고 볼 수 있다.


XXXController의 req, res는 ActionServlet가 쥐고 있다.
ActionServlet이 톰캣으로부터 req, res를 받는다.
req, res 제공해주는 건 서블릿 엔진이다.
서블릿은 req, res를 하나만 가지고 있다. 즉 싱글톤.
실제로 ActionServletreq, res를 주입해주는 건 웹 서버의 컨테이너이다.

ActionServlet의 req, res를 XXXController가 가져가야 하는데 둘을 연결하는 게 Controller 인터페이스이다. XXXController는 Controller를 implements 하고 있기 때문이다.

  • 왜 클래스가 아닌 인터페이스인가?

    • 결합도를 낮추기 위해서이다.
      • 클래스의 의존 관계 때문이다. 독립성을 갖기 위해서이다. 의존적이면 같이 야근해야 한다...
  • Controller는 execute라는 추상 메소드를 갖고 있다.

    • 추상 메소드 : Override
    • 따라서 execute를 XXXController가 사용할 수 있는 것
  • gd2 -> ActionServlet(경유. 얘의 관여를 받는다)

  • upmu[0]url을 통해 온다.

    • url에는 /, : 이 있다.
      • String
    • 배열은 올라갈 때
  • HashMapBinder

    • ActionServlet과 BoardController 사이에서 공통 코드를 묶어 처리함
      req.getParameter("b_title")
      • req, " " : String
      • 따라서 HashMapBinder는 ActionServlet과 의존관계이다.
    • HashMapBinder 인스턴스화 하는 곳은 BoardController이다.
      • 따라서 BoardController에도 req가 필요하고 이것을 주는 것이 Controller의 execute()
      • 생성자에서 HashMapBinder를 받음
  • XXXLogic

    • 트랜잭션 처리(AOP)하기 위해 두었음
    • 트랜잭션 처리하기 전까지 기다림(Wait) -> 처리
    • Dao 메소드는 최소 2개 이상 필요하다 : 따라서 트랜잭션 처리 대상이다
      • 여기까지 와야 하나의 프로세스 처리가 완료 된 것이다 -> commit -> 트랜잭션 처리

게시글 삭제

board.xml

parameterType을 int로 변경하였다. 원시형 타입을 처리할 땐 value로 처리한다.

<delete id="boardDelete" parameterType="int">
		delete from board where b_no = #{value} <!-- 원시형 타입을 처리할 땐 value --> 	
</delete>

BoardDao.java

BoardDao에서 int b_no = Integer.parseInt(pMap.get("b_no").toString());를 추가했다.
board.xml에서 parameterType을 int로 변경했기 때문에 int로 처리한다.

public int boardDelete(Map<String, Object> pMap) {
		logger.info("boardDelete");
		int result = 0;
		sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
		SqlSession sqlSession = sqlSessionFactory.openSession();
		try {
			int b_no = Integer.parseInt(pMap.get("b_no").toString());
			result = sqlSession.delete("boardDelete", b_no);			
			sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
		} catch (Exception e) {
			logger.info(e.toString());
		}
		return result;
	}

BoardController, ActionServlet - 입력, 수정, 삭제 에러 발생 이유

👇BoardController

else if("boardDelete".equals(upmu[1])) {//delete
			logger.info("boardDelete");
			int result = 0;
			hmb.bind(pMap);
			result = bLogic.boardDelete(pMap);
			if(result == 1) { //글 등록 성공했을 때
				//pageMove[0] = redirect
				//pageMove[1] = /board/boardList.gd2
				//path 정보를 upmu가 가져가나? pageMove에 담기는가?
				path = "redirect:/board/boardList.gd2";
			}else {
				path = "redirect:/board/boardError.jsp";
			}
		}//////////////////end of boardDelete - 조건문 블록 하나하나가 메소드로 분할 될 것
		
		return path;

👇ActionServlet
res.sendRedirect("/"+path+".jsp");
-> res.sendRedirect(path);으로 변경

Controller controller = new BoardController();
		if("board".equals(upmu[0])) {
			logger.info("workname - board - execute 호출");
			req.setAttribute("upmu",upmu);
			result = controller.execute(req, res);//여기서 BoardController의 path값이 담김
		}
		//BoardController를 경유한 다음 리턴 값으로 문자열을 받았다
		//이 문자열을 잘라서 pageMove에 담아준다
		// 1. 널 체크하기
		// 2. 문자열 배열을 선언할 것
		// 3. 콜론이 포함되어 있나?
		// 4. 콜론이 없는 경우도 처리할 것
		// 1) redirect 인 경우 : webapp - "redirect:/board/boardList.jsp"
		// 2) forward 인 경우 : webapp - "forward:/board/boardList.jsp"
		// 3)/WEB-INF/jsp/ - "/board/boardList.jsp"
		if(result != null) {
			String pageMove[] = null;
			if(result.contains(":")) {
				logger.info(": 이 포함되어 있어요");
				//-> redirect:
				pageMove = result.split(":"); //[0]=redirect or forward [1]=board/boardList
				logger.info(pageMove);
			}// end of 콜론이 있는 경우
			//콜론이 없는 경우
			else {
				pageMove = result.split("/"); //[0]=board [1]=boardList
				logger.info(pageMove);
			}// end of 콜론이 없는 경우
			logger.info(pageMove[0] + ", " + pageMove[1]);
			if(pageMove != null) {
				//치환을 한 번 더 함
				String path = pageMove[1]; // board/boardList
				if("redirect".equals(pageMove[0])) {
					res.sendRedirect(path); // board/boardList.jsp
					//forward 처리 시와 동일한 컨벤션을 적용하기 위해 접두어와 접미어를 붙이는 과정에서 오류 발동함.
					//현재 구조 상 입력|수정|삭제 모두 처리 성공 시 목록 페이지로 응답이 나가도록 설계 되어 있다는 것을 간과함.
					//res.sendRedirect("/"+path+".jsp"); // board/boardList.gd2.jsp
				}// end of sendRedirect
  • 입력을 처리할 때
    insert(action) -> "redirect:/board/boardList.gd2" -> path -> ActionServlet -> pageMove[0]=redirect, pageMove[1]=/board/boardList.gd2 -> split("/") => board앞에서 / 있기 때문에 3개로 쪼개짐

postman : POST, PUT

POST일 때는 Body - x-www-form-urlencoded에 key와 value를 넣으면 정상적으로 처리되지만
PUT일 때는 정상 처리되지 않는다. PUT에서는 params에 key와 value를 넣어야 한다.

BoardList(webapp)


검색 버튼을 누른 후(BoardSeach()호출),


콘솔 창이 다 사라진 이유
: forward. url은 그대로지만 화면은 바뀌었기 때문이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.util.*, com.util.BSPageBar"%>
 <% 
	List<Map<String, Object>> bList = (List)request.getAttribute("bList"); 
 	int size = 0;//총 레코드 수
 	if(bList != null){
 		size = bList.size(); //4 출력
 	}
 	//out.print(bList);
 	
 	//한 페이지에 몇 개씩 뿌릴거야?
 	//1(26, 25, 24, 23, 22) 2(21, 20, 19, 18, 17) 3(16, 15, 14, 13, 12) > 4(11, 10, 9, 8, 7) 5(6, 5, 4, 3, 2) 6(1)
 	int numPerPage = 3;		
 	int nowPage = 0;
 	//Servlet이 아님으로 request 다 적어야 함
 	if(request.getParameter("nowPage") != null){
 		nowPage = Integer.parseInt(request.getParameter("nowPage"));
 	}
 %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판(webapp)</title>
<!--  
WAS란?
TOMCAT(웹서버{apache사용함 : 정적페이지}+웹컨테이너 - jsp : api.jar - 서블릿 변환, servlet : api.jar - 컴파일)

1) 액션태그 : a_jsp.java, b_jsp.java
: 파일이 두 개로 생성됨. 처리 결과가 a.jsp에 반영됨. 제어권은 a.jsp에게 있다
: 주소가 바뀌지 않는다(a->b->a로 다시 돌아오기 때문이다). 요청이 계속 유지되는 것으로 판단함
2) include directive 방식 : a_jsp.java - jsp파일은 두 갠데 서블릿은 한 개다
역할
: 인증에 관련된 코드는 서비스 개시 거의 직전에 추가함
: 보통 보안과 관련된 업체는 별도로 선정하고 요구사항을 수렴하는 시스템이어야 함
-->
	<%@include file="/common/bootstrap_common.jsp" %>
	<link rel="stylesheet" href="/css/board.css">
	<script type="text/javascript">
		const searchEnter = (event) => {
			console.log('searchEnter')
			console.log(window.event.keyCode); //13
			if(window.event.keyCode == 13){
				boardSearch(); //재사용성				
			}			
		}///end of searchEnter
		
		const boardSearch = () => {
			console.log('boardSearch');
			const gubun = document.querySelector("#gubun").value;
			const keyword = document.querySelector("#keyword").value;
			console.log(`${gubun} , ${keyword}`);
			location.href="/board/boardList.gd2?gubun="+gubun+"&keyword="+keyword;	
		}///end of boardSearch
		
		const boardList = () => {
			location.href="/board/boardList.gd2"
		}
		
		const boardInsert = () => {
			//<input type=text name=b_title/>
			//<input type=text name=b_content/>
			//<input type=text name=b_writer/>
			//Enumeration<String> em = req.getParameterNames();
			document.querySelector("#f_board").submit(); //form태그에 묶인 컴퍼넌트 값들이 전송됨
		}
		
		const fileDown = (b_file) => { //b_file=바나나우유.jpg
			location.href="downLoad.jsp?b_file="+b_file;
		}
	</script>
</head>
<body>
<!-- header start -->
	<%@include file="/include/gym_header.jsp"%>
	<!-- header end    -->
	<!-- body start    -->
	<div class="container">
		<div class="page-header">
			<h2>게시판 <small>게시글목록</small></h2>
			<hr />
		</div>
		<!-- 검색기 시작 -->
		<div class="row">
			<div class="col-3">
		    	<select id="gubun" class="form-select" aria-label="분류선택">
		      		<option value="none">분류선택</option>
		      		<option value="b_title">제목</option>
		      		<option value="b_writer">작성자</option>
		      		<option value="b_content">내용</option>
		    	</select>			
		  	</div>
			<div class="col-6">
		 		<input type="text" id="keyword" class="form-control" placeholder="검색어를 입력하세요" 
		           aria-label="검색어를 입력하세요" aria-describedby="btn_search" onkeyup="searchEnter()"/>
			</div>
			<div class="col-3">
		 		<button id="btn_search" class="btn btn-danger" onclick="boardSearch()">검색</button>
		 	</div>
		</div>		
		<!-- 검색기 끝 -->
		
		<!-- 회원목록 시작 -->
		<div class='board-list'>
			<table class="table table-hover">
		    	<thead>
		      		<tr>
		        		<th width="10%">#</th>
		        		<th width="40%">제목</th>
		        		<th width="20%">첨부파일</th>
		        		<th width="15%">작성자</th>
		        		<th width="15%">조회수</th>
		      		</tr>
		    	</thead>
		    	<tbody>	    
					<%
						//스크립틀릿 - 지변이다, 메소드 선언불가, 생성자 선언불가, 실행문
						//n건을 조회하는 경우이지만 resultType에는 map이나 vo패턴을 주는게 맞다
						//주의사항 - 자동으로 키값을 생성함 - 디폴트가 대문자이다
						//myBatis연동시 resultType=map{한행}으로 줌 -> selectList("noticeList", pMap)
						//for(int i=0;i<size;i++){ //페이징 처리 이전 코드
						for(int i=nowPage*numPerPage; i<(nowPage*numPerPage)+numPerPage; i++){
							if(size == i) break;
							Map<String,Object> rmap = bList.get(i);
					%>					
										<tr>
											<td><%=rmap.get("B_NO") %></td>
											<td><%=rmap.get("B_TITLE") %></td>
											<td>
											<!-- 자바스크립트에서는 값 앞 뒤에 싱글 또는 더블 쿼테이션을 붙이지 않으면 변수 취급 받음 
													XXXX is undefined 에러 보게 된다
											-->
											<a href="javascript:fileDown('<%= rmap.get("B_FILE") %>')"><%=rmap.get("B_FILE") %></a>
											</td>
											<td><%=rmap.get("B_WRITER") %></td>
											<td><%=rmap.get("B_HIT") %></td>
										</tr>					
					<%
						}
					%>  		
		    	</tbody>
			</table> 	
<hr />  
<!-- [[ Bootstrap 페이징 처리  구간  ]] -->
	<div style="display:flex;justify-content:center;">
	<ul class="pagination">
<% 
	String pagePath = "boardList.gd2";
	BSPageBar bspb = new BSPageBar(numPerPage, size, nowPage, pagePath);
	out.print(bspb.getPageBar());
%>
	</ul>
	</div>
<!-- [[ Bootstrap 페이징 처리  구간  ]] -->		  
		  	<div class='board-footer'>
		    	<button class="btn btn-warning" onclick="boardList()">
		      		전체조회
		    	</button>&nbsp; 
			    <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#boardForm">
			    글쓰기
			    </button>
		    </div>
		</div>		
		<!-- 회원목록   끝  -->		
		
	</div>
	<!-- body end       -->
	<!-- footer start -->
	<%@include file="/include/gym_footer.jsp"%>
	
		<!-- ========================== [[ 게시판 Modal ]] ========================== -->
	<div class="modal" id="boardForm">
	  <div class="modal-dialog modal-dialog-centered">
	    <div class="modal-content">
	
	      <!-- Modal Header -->
	      <div class="modal-header">
	        <h4 class="modal-title">게시판</h4>
	        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
	      </div>
	      <!-- Modal body -->
	      <div class="modal-body">
	      	<!-- <form id="f_board" method="get" action="./boardInsert.pj2"> -->
	      	<form id="f_board" method="post" enctype="multipart/form-data" action="./boardInsert.gd2">
	      	  <input type="hidden" name="method" value="boardInsert">
	          <div class="form-floating mb-3 mt-3">
	            <input type="text"  class="form-control" id="b_title" name="b_title" placeholder="Enter 제목" />
	            <label for="b_title">제목</label>
	          </div>	      	
	          <div class="form-floating mb-3 mt-3">
	            <input type="text"  class="form-control" id="b_writer" name="b_writer" placeholder="Enter 작성자" />
	            <label for="b_writer">작성자</label>
	          </div>	      	
	          <div class="form-floating mb-3 mt-3">
	            <textarea rows="5" class="form-control h-25" aria-label="With textarea" id="b_content" name="b_content"></textarea>
			  </div>	
		      <div class="input-group mb-3">
				  <input type="file" class="form-control" id="b_file" name="b_file">
				  <label class="input-group-text" for="b_file">Upload</label>
			  </div>      	
	      	</form>
	      </div>	
	      <div class="modal-footer">
	        <input type="button" class="btn btn-warning" data-bs-dismiss="modal" onclick="boardInsert()"  value="저장">
	        <input type="button" class="btn btn-danger" data-bs-dismiss="modal" value="닫기">
	      </div>
	
	    </div>
	  </div>
	</div>
    <!-- ========================== [[ 게시판 Modal ]] ========================== -->
    
</body>
</html>

BSPageBar.java

package com.util;

import org.apache.log4j.Logger;
//			1 2 3 >			< 4 5 6>		< 7 8
public class BSPageBar {
	Logger logger = Logger.getLogger(BSPageBar.class);
	//전체레코드 갯수
	private int totalRecord;//list.size():47row
	//페이지당 레코드 수
	private int numPerPage;// 10개씩이다
	//블럭당 디폴트 페이지 수 - 여기서는 일단 2개로 정함.
	private int pagePerBlock=3;
	//총페이지 수
	private int totalPage;
	//총블럭 수
	private int totalBlock;
	//현재 내가 바라보는 페이지 수
	private int nowPage;
	//현재 내가 바라보는 블럭 수
	private int nowBlock;
	//적용할 페이지 이름
	private String pagePath;
	
	private String pagination;
	//페이지 네비게이션 초기화
	/*
	 * 화면에서 받아와야 하는 정보에는 어떤 것들이 있을까?
	 * 페이지에 뿌려질 로우의 수 numberPerPage
	 * 전체 레코드 수 totalRecord
	 * 현재 내가 바라보는 페이지 번호 nowPage
	 * 내가 처리해야할 페이지 이름 pagePath
	 * 
	 * 공식을 세우는데 필요한 인자는 누구?
	 * 
	 * 세워진 공식들은 어디에서 적용하면 되는 거지?
	 * 
	 * 화면에 내보내 져야 하는 언어는 html 아님 자바 중에서 ?????
	 * html
	 * 내보내지는 정보는 어디에 담으면 될까?
	 * 
	 */
	public BSPageBar(int numPerPage, int totalRecord, int nowPage, String pagePath) { //공지사항 페이징 처리인가 아님 게시판 페이징 처리인가? - String pagePath 이  부분이 페이지에 따라 들어오는 값이 달라진다. 따라서 변수 처리한 것(ex) /notic/noticeList.gd?1=1")
		this.numPerPage = numPerPage;
		this.totalRecord = totalRecord;
		this.nowPage = nowPage;
		this.pagePath = pagePath;
		
		this.totalPage = 
				(int)Math.ceil((double)this.totalRecord/this.numPerPage);// 47.0/10=> 4.7 4.1->5page 4.2->5page
		this.totalBlock= 
				(int)Math.ceil((double)this.totalPage/this.pagePerBlock);//5.0/2=> 2.5-> 3장
		//현재 내가바라보는 페이지 : (int)((double)4-1/2)
		this.nowBlock = (int)((double)this.nowPage/this.pagePerBlock);
	}
	//setter메소드 선언
	public void setPageBar() {
		StringBuilder pageLink = new StringBuilder();
		//전체 레코드 수가 0보다 클때 처리하기
		if(totalRecord>0) {
			//nowBlock이 0보다 클때 처리
			//이전 페이지로 이동 해야 하므로 페이지 번호에 a태그를 붙여야 하고
			//pagePath뒤에 이동할 페이지 번호를 붙여서 호출 해야함.
			if(nowBlock > 0 ) {                                    //(1-1)*2+(2-1)=1
				pageLink.append("<li class='page-item'>");
				pageLink.append("<a class='page-link' href='"+pagePath+"?nowPage="+((nowBlock-1)*pagePerBlock+(pagePerBlock-1))+"'>");
				pageLink.append("<span aria-hidden='true'>&laquo;</span>");
				pageLink.append("</a>");
				pageLink.append("</li>");
			}
			for(int i=0;i<pagePerBlock;i++) {
				//현재 내가 보고 있는 페이지 블록 일때와
				if(nowBlock*pagePerBlock+i==nowPage) {
					pageLink.append("<a class='page-link'>"+(nowBlock*pagePerBlock+i+1)+"</a>");
				}
				//그렇지 않을 때를 나누어 처리해야 함.
				else {
					pageLink.append("<a class='page-link' href='"+pagePath+"?nowPage="+((nowBlock*pagePerBlock)+i)+"'>"+((nowBlock*pagePerBlock)+i+1)+"</a>");
					
				}
				//모든 경우에 pagePerBlock만큼 반복되지 않으므로 break처리해야 함.
				//주의할 것.
				if((nowBlock*pagePerBlock)+i+1==totalPage) break;
			}
			//현재 블록에서 다음 블록이 존재할 경우 이미지 추가하고 페이지 이동할 수 있도록
			//a태그 활용하여 링크 처리하기
			if(totalBlock > nowBlock+1) {
				pageLink.append("<li class='page-item'>");
				pageLink.append("<a class='page-link' aria-label='Next' href='"+pagePath+"?nowPage="+((nowBlock+1)*pagePerBlock)+"'>");
				pageLink.append("<span aria-hidden=\"true\">&raquo;</span>");
				pageLink.append("</a>");	
				pageLink.append("</li>");
			}
		}
		logger.info("pageLink.toString():"+pageLink.toString());
		pagination = pageLink.toString();
	}
	//getter메소드 선언
	public String getPageBar() {
		this.setPageBar();
		return pagination;
	}
}

위 코드 중 아래 코드는 페이지 당 항목 수, 총 레코드 수, 현재 페이지 및 페이지 경로에 따라 페이지 바(페이징 처리를 위한 바)를 설정한다.

  1. numPerPage: 페이지당 항목 수
  2. totalRecord: 총 레코드 수
  3. nowPage: 현재 페이지 번호
  4. pagePath: 각 페이지의 경로(ex: /notice/noticeList.gd?1=1)

이들 값에 따라 다음과 같은 작업이 수행된다:

  1. totalPage: 전체 페이지 수. totalRecord를 numPerPage로 나눈 값을 올림하여 구한다. 이는 총 레코드를 페이지당 항목 수로 나눈 후 소수점을 올림한 것으로, 예를 들어 47개의 레코드를 10개씩 보여줄 때 4.7가 되어야 하지만 올림하여 5페이지가 된다.
  2. totalBlock: 전체 블록 수. totalPage를 pagePerBlock으로 나눈 값을 올림하여 구한다. 블록은 페이지 그룹 단위이며, 페이지 바의 블록 수를 결정한다.
  3. nowBlock: 현재 보고 있는 블록 번호. nowPage를 pagePerBlock으로 나눈 값을 정수로 변환하여 구한다. 현재 페이지가 속한 블록을 알려준다.

이 코드는 페이징 처리를 위한 각종 변수들을 초기화하고, 총 페이지 수 및 블록 수를 계산하여 페이지 바를 생성하는 데 사용된다. 페이지 바는 사용자가 특정 페이지로 이동하고 페이지 간 이동을 가능하게 하는 UI 요소이다.

	public BSPageBar(int numPerPage, int totalRecord, int nowPage, String pagePath) { //공지사항 페이징 처리인가 아님 게시판 페이징 처리인가? - String pagePath 이  부분이 페이지에 따라 들어오는 값이 달라진다. 따라서 변수 처리한 것(ex) /notic/noticeList.gd?1=1")
		this.numPerPage = numPerPage;
		this.totalRecord = totalRecord;
		this.nowPage = nowPage;
		this.pagePath = pagePath;
		
		this.totalPage = 
				(int)Math.ceil((double)this.totalRecord/this.numPerPage);// 47.0/10=> 4.7 4.1->5page 4.2->5page
		this.totalBlock= 
				(int)Math.ceil((double)this.totalPage/this.pagePerBlock);//5.0/2=> 2.5-> 3장
		//현재 내가바라보는 페이지 : (int)((double)4-1/2)
		this.nowBlock = (int)((double)this.nowPage/this.pagePerBlock);
	}

아래 코드는 페이지 링크를 생성하는데 사용된다. pagePath 변수에는 각 페이지의 경로(ex: /notice/noticeList.gd?1=1)가 들어있으며, nowPage 변수를 통해 현재 페이지의 링크를 생성한다.

해당 코드는 <a> 태그를 사용하여 페이지 이동 링크를 만드는데, href 속성에 현재 페이지를 기반으로 다음 페이지로 이동하는 링크를 생성한다. 이를 위해 nowBlock와 pagePerBlock 변수를 이용하여 다음 블록의 첫 번째 페이지로 이동하는 링크를 만들고 있다.

pageLink.append("<a class='page-link' href='"+pagePath+"?nowPage="+((nowBlock-1)*pagePerBlock+(pagePerBlock-1))+"'>");

예를 들어, 현재 블록이 2번 블록이며 pagePerBlock이 10이고 nowPage가 15일 경우, 아래와 같은 링크가 생성된다.

<a class='page-link' href='/notice/noticeList.gd?nowPage=19'>

이는 사용자가 클릭하면 다음 블록의 첫 번째 페이지로 이동하는 링크를 생성한다. 페이지 이동 로직에 따라 현재 페이지의 위치를 계산하여 다음 페이지로 이동하는 링크를 생성한다.

BSPageBar -> ActionServlet으로 이동하는 이유

BSPageBar의 코드에서 아래 코드가 진행되면

logger.info("pageLink.toString():"+pageLink.toString());

아래와 같이 콘솔에 찍히고

[2023-12-14 16:16:39,705] [INFO] (BSPageBar.java:94) - pageLink.toString():<a class='page-link' href='boardList.gd2?nowPage=0'>1</a><a class='page-link'>2</a><a class='page-link' href='boardList.gd2?nowPage=2'>3</a><li class='page-item'><a class='page-link' aria-label='Next' href='boardList.gd2?nowPage=3'><span aria-hidden="true">&raquo;</span></a></li>

이후 바로 ActionSerlvet으로 이동하여

@SuppressWarnings("serial")
//@Slf4j : Logger를 쓰지 않고도 사용할 수 있다
@WebServlet("*.gd2")
public class ActionServlet extends HttpServlet {
	Logger logger = Logger.getLogger(ActionServlet.class);
	
	protected void doService(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		String  uri = req.getRequestURI(); // => /notice/noticeInsert.gd?n_title=a&n_content=b
		logger.info(uri);

logger.info(uri);를 통해 아래 콘솔창이 찍힌다.

[2023-12-14 16:16:40,640] [INFO] (ActionServlet.java:25) - /board/boardList.gd2

즉, BSPageBar에서 콘솔창이 찍히고 바로 ActionServlet 콘솔창이 찍히는 것이다.
그 이유는 콘솔에 출력된 pageLink.toString()의 결과에는 boardList.gd2라는 URL이 있다. 이는 페이지 링크를 구성하는 과정에서 href 속성에 해당 URL이 설정되어 있기 때문에 이 링크를 클릭하면 boardList.gd2에 대한 요청이 발생한다.
이 요청이 .gd2로 끝나기 때문에 서블릿 컨테이너는 이 요청을 ActionServlet에 매핑된 URL 패턴에 따라 처리하게 된다. 따라서 ActionServlet에서 이 요청을 처리하고 콘솔에 해당 URL이 찍히게 된다.

BoardController

//POST방식은 매번 동일한 url을 요청하더라도 무조건 서버를 경유함
GET방식은 같은 url이면 인터셉트 당함(302번)

hmb.multiBind(pMap);//첨부파일이 포함된 POST방식의 처리일때만 - get방식은 무조건 첨부파일 안 됨

HashMapBinder에

multi = new MultipartRequest(req, realFolder, maxSize, encType, new DefaultFileRenamePolicy());

있기 때문에 multiBind() 가능하다.

package com.example.demo.pojo2;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.example.demo.pojo1.ActionForward;

import com.example.demo.pojo1.NoticeController;
import com.example.demo.pojo1.NoticeLogic;
/*
 * upmu[] : 내려갈 때 -> ActionServlet -> BoardController로 연결될 때
 * -> 개선점(1-3버전) -> spring -> XXXHandlerMapping -> BoardController 클래스에서부터 메소드를 쪼갤 수는 없나?(현재는 if문으로 되어 있어 가독성, 재사용성 떨어짐)
 * pageMove[] : 올라올 때
 * 
 * XXXLogic 메소드를 호출할 때 BoardLogic클래스의 인스턴스화가 선행됨(DI지원)
 * 여기는 POJO이므로 제어권을 개발자인 내가 가지니까 이른 인스턴스화 부분은 생략함
 * 객체에 대한 라이프사이클 관리 책임이 개발자인 내게 있다
 * 
 * 아쉬운 점
 * BoardController에서 메소드로 분할이 안 되었다는 점
 * 대신 if문으로 처리하였다 - 별로다
 * Reflection API 깊은 고민 : ApplicationContext, BeanFactory 스프링 컨테이너
 * IoC 직접 구현해 보는 경험 : 시니어
 */
//@Controller : 스프링에서는 클래스 사이의 결합도를 낮추기 위해 상속(결합도가 높아지니까)을 포기하였다
//@RequestMapping(/notice/*) : 2번째 URL 매핑 방법
import com.google.gson.Gson;
import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;
import com.util.HashMapBinder;
public class BoardController implements Controller {
	Logger logger = Logger.getLogger(BoardController.class);
	BoardLogic bLogic = new BoardLogic();//이른
	//@GetMapping("noticeList.gd") : 객체 주입 받으려면 ApplicationContext로부터 빈 관리를 받을 때만 사용 가능함 - req, res 주입해주기 때문에
	//public String noticeList(HttpServletRequest req, HttpServletResponse res) {}
	@Override
	public String execute(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		String upmu[] = (String[])req.getAttribute("upmu");
		String path = null;
		Map<String, Object> pMap = new HashMap<>();
		HashMapBinder hmb = new HashMapBinder(req); //주입됨
		//전체조회일 때 : select / n건 - List<Map | VO> - list.jsp
		//상세보기와 응답페이지 이름이 달라서 메소드를 분리한다
		// 1) 배포위치가 WEB-INF 일 때 : /WEB-INF/jsp/(workname)/메소드이름 or upmu[1].jsp
		// 2) 배포위치가 webapp 일 때	
		if("boardList".equals(upmu[1])) {//select : 1-3버전에서는 이 장면을 메소드 단위로 변경하고 싶다. (req, res) 넘겨 받을 수 있어야 한다 <- 이 문제를 해결해야함
			logger.info("boardList");
			List<Map<String ,Object>> bList = null;//nList.size()가 n개 
			// NoticeLogic의 메소드 호출 - 객체주입 - 내가(책임) 아님 스프링(제어역전)
			hmb.bind(pMap);  
			bList = bLogic.boardList(pMap);
			logger.info(bList);
			//원본에다가 담아 두자
			req.setAttribute("bList", bList);
			//pageMove[0]=forward, pageMove[1]=/board/boardLIst
			path = "forward:board/boardList"; // url은 안 바뀐다. 화면은 바뀐다. 파일은 .jsp이다. 
			//path = "redirect:board/boardList"; // webapp/board/boardList로 이동하고자 할 때 //url 바뀐다(gd2->jsp로). 화면도 바뀐다.
			//path = "board/boardList"; // /WEB-INF/board/boardList로 이동하고자 할 때
		}//end of 목록조회		
		//상세조회일 때 : select / 1건 - Map or VO - read.jsp
		else if("boardDetail".equals(upmu[1])) {
				logger.info("boardDetail");
				List<Map<String ,Object>> bList = null;//nList.size()=1
				// NoticeLogic의 메소드 호출 - 객체주입 - 내가(책임) 아님 스프링(제어역전)
				//select * from notice where n_no=5;
				hmb.bind(pMap);
				bList = bLogic.boardList(pMap);
				//원본에다가 담아 두자
				req.setAttribute("bList", bList);
				path = "forward:/board/boardDetail.jsp";
		}
		//공통분모 : 반환값이 int이다. commit과 rollback 대상이다
		//입력 | 수정 | 삭제인 경우 모두 1이라면 어느 페이지로 이동할까? - 목록(select: /board/boardList.gd2 - 이 뒤에서 forward 처리해야 함)을 보여주세요
		//등록일 때 : post 방식 - insert : 1(수정성공) or 0(수정 안 됨)
		else if("boardInsert".equals(upmu[1])) {//insert
			logger.info("boardInsert");
			int result = 0;
			//hmb.bind(pMap);
			//POST방식은 매번 동일한 url을 요청하더라도 무조건 서버를 경유함
			//GET방식은 같은 url이면 인터셉트 당함(302번)
			hmb.multiBind(pMap);//첨부파일이 포함된 POST방식의 처리일때만 - get방식은 무조건 첨부파일 안 됨
			result = bLogic.boardInsert(pMap);
			if(result == 1) { //글 등록 성공했을 때
				path = "redirect:/board/boardList.gd2"; //jsp --(redirect)--> boardInsert.gd2 --(redirect)--> boardList.gd2 --(forward)--> jsp
			}else {
				path = "redirect:/board/boardError.jsp";
			}
		}/////////////////end of boardInsert
		
		//수정일 때 : get, put 방식(모든 방식을 doService로 묶었기 때문에 큰 의미는 없다) - Restful 상징성을 표현함 - update : 1(수정성공) or 0(수정 안 됨)
		else if("boardUpdate".equals(upmu[1])) {//update
			logger.info("boardUpdate");
			int result = 0;
			hmb.bind(pMap); //pMap.get(b_no) = 5
			logger.info(pMap);
			result = bLogic.boardUpdate(pMap);
			if(result == 1) { //글 등록 성공했을 때
				path = "redirect:/board/boardList.gd2";
			}else {
				path = "redirect:/board/boardError.jsp";
			}
		}/////////////////end of boardUpdate
		
		//삭제일 때 : delete 방식 - 스프링 수업 땐 분리해서 할 것임 - delete : 1(수정성공) or 0(수정 안 됨)
		else if("boardDelete".equals(upmu[1])) {//delete
			logger.info("boardDelete");
			int result = 0;
			hmb.bind(pMap);
			result = bLogic.boardDelete(pMap);
			if(result == 1) { //글 등록 성공했을 때
				//pageMove[0] = redirect
				//pageMove[1] = /board/boardList.gd2
				//path 정보를 upmu가 가져가나? pageMove에 담기는가?
				path = "redirect:/board/boardList.gd2";
			}else {
				path = "redirect:/board/boardError.jsp";
			}
		}//////////////////end of boardDelete - 조건문 블록 하나하나가 메소드로 분할 될 것
		
		return path;
		//return "redirect:/notice/noticeList.jsp"; //webapp
		//return "forward:/notice/noticeList.jsp"; //webapp - 요청이 유지되는 것으로 판단해서 서블릿이 쥐고 있는 값을 jsp에서도 사용할 수 있다.
		//return "/notice/noticeList"; //WEB-INF/jsp/notice 아래
	}//////////////end of execute
}/////////////////end of BoardController

HashMapBinder.java

package com.util;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;

import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

import java.io.File;
import java.util.Enumeration;

public class HashMapBinder {
	Logger logger = Logger.getLogger(HashMapBinder.class);
	HttpServletRequest req = null;
	MultipartRequest multi = null;
	String realFolder = "C:\\Users\\SeulGi\\Desktop\\Developer\\workspace_jsp\\nae2Gym\\src\\main\\webapp\\pds";
	String encType = "utf-8";
	int maxSize = 5*1024*1024;
	public HashMapBinder(HttpServletRequest req) {
		this.req = req;
	}
	//첨부파일이 있는 POST방식일 때 사용하는 메소드
	//파라미터에 있는 주소번지는 어디서 결정되나요? - 이 공통코드를 사용하는 클래스에서 주입받는다
	public void multiBind(Map<String, Object> pMap) {
		pMap.clear(); //기존에 들어있는 정보는 비운다 : 초기화와 연관된 행동
		try {
			//첨부파일 업로드 되는 지점
			multi = new MultipartRequest(req, realFolder, maxSize, encType, new DefaultFileRenamePolicy());
		} catch (Exception e) {
			logger.info(e.toString());
		}
		//첨부파일이 아닌 다른 정보들에 대해서도 담아준다 - enctype = multipart/form-data일 때
		Enumeration<String> em = multi.getParameterNames(); //req로는 가져오지 못함으로 multi로 바꿔야 함
		while(em.hasMoreElements()) {
			//키값 꺼내기
			String key = em.nextElement();//n_title, n_content, n_writer
			pMap.put(key, multi.getParameter(key));
		}////////////// end of while
		logger.info(pMap.toString());
		
		Enumeration<String> files = multi.getFileNames();
		String fullPath = null; //파일 정보에 대한 전체경로
		String filename = null; //파일이름
		//첨부파일이 있다면?
		if(files != null) {
			//File : 파일 이름을 클래스로 정의하는 객체. 파일 객체가 생성되었다고 해서 그 안에 내용까지 포함되진 않음
			//파일 크기를 계산해주는 메소드 지원함
			File file = null;
			while(files.hasMoreElements()) {
				String fname = files.nextElement();
				filename = multi.getFilesystemName(fname);
				pMap.put("b_file", filename);//avartar.png
				//File객체 생성하기
				file = new File(realFolder+"\\"+filename);
			}
		}///////////end of if	
	}/////////////end of multiBind
	//메소드 설계시 리턴타입이 아닌 파라미터 자리를 통해서 값을 전달하는 방법 소개
	//사용자가 입력한 값을 담아 맵이 외부 클래스에서 인스턴스화 되어 넘어오니까
	//초기화 처리 후 사용함
	/*****************************************************************
	 * 
	 * @param pMap -  필요한 클래스 주입 - 선언자리이지 생성자리 아님
	 *****************************************************************/
	public void bind(Map<String,Object> pMap) { //BoardController에서 주입해준다
		pMap.clear();
		//<input type="text" name="n_title">
		//<input type="text" name="n_content">
		//<input type="text" name="n_writer">
		Enumeration<String> em = req.getParameterNames(); //req.getParameterNames(); 얘만 변화함. 파라미터에 있는 pMap에 주입됨
		//logger.info(em.hasMoreElements()); //true여야 반복문 처리됨
		while(em.hasMoreElements()) {
			//키값 꺼내기
			String key = em.nextElement();//n_title, n_content, n_writer
			pMap.put(key, req.getParameter(key));
		}////////////// end of while
		logger.info(pMap.toString());
	}///////////////// end of bind
}/////////////////// end of HashMapBinder

HashMapBinder로 이동하여 로그를 찍었을 때, {nowPage=0} 찍히는 이유

[2023-12-14 16:16:40,642] [INFO] (HashMapBinder.java:79)  - {nowPage=0}

위 BSPageBar에서 nowPage를 가져와 ActionServlet에서 req에 저장하게 되고 BoardController에서 HashMapBinder에 req를 주입하게 된다.

Map<String, Object> pMap = new HashMap<>();
HashMapBinder hmb = new HashMapBinder(req);

이후 BoardController에서 hmb의 bind에 pMap을 주입하고

hmb.bind(pMap);

HashMapBinder의 bind 메소드에 pMap이 주입된다. 결국 nowPage가 -> req -> pMap으로 차례대로 주입된 것이기 때문에 로그에 {nowPage=0}이 출력된다.

public void bind(Map<String,Object> pMap) { //BoardController에서 주입해준다
		pMap.clear();
		//<input type="text" name="n_title">
		//<input type="text" name="n_content">
		//<input type="text" name="n_writer">
		Enumeration<String> em = req.getParameterNames(); //req.getParameterNames(); 얘만 변화함. 파라미터에 있는 pMap에 주입됨
		//logger.info(em.hasMoreElements()); //true여야 반복문 처리됨
		while(em.hasMoreElements()) {
			//키값 꺼내기
			String key = em.nextElement();//n_title, n_content, n_writer
			pMap.put(key, req.getParameter(key));
		}////////////// end of while
		logger.info(pMap.toString());
	}///////////////// end of bind
}/////////////////// end of HashMapBinder

downLoad.jsp

브라우저는 자기가 아는 mime type은 실행시키지만 모르는 mime type은 다운로드 처리한다.

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ page import="java.io.*,java.net.*" %>    
<%
//사용자 화면으로 부터 파일명을 받아서 저장함.
//<a href="./downLoad.jsp?b_file=avatar24.png">첨부파일이름</a>
	String b_file = request.getParameter("b_file");
	String fname = b_file;
	//out.print("b_file: 8->euc"+b_file);		
	//out.print("<br>");		
	String filePath = "C:\\Users\\SeulGi\\Desktop\\Developer\\workspace_jsp\\nae2Gym\\src\\main\\webapp\\pds"; // 절대경로.	
	//가져온 파일이름을 객체화 시켜줌. - 파일이 있는 물리적인 경로가 필요함.
	File file = new File(filePath,b_file.trim());
 	String mimeType = getServletContext().getMimeType(file.toString());
 	//mimeType이 브라우저가 인식할 수 없는 값이면 브라우저는 무조건 다운로드 처리하도록 설계되어 있음.
 	//마임타입이 부재인 경우 브라우저가 모르는 마임타입으로 변경하여 줌.
	if(mimeType == null){
		//그래서 다운로드 화면이 처리되도록 강제로 유도해줌.
		//아래코드는 마임타입에 따라 다운로드 되지 않고 브라우저에서 페이지를 처리하는 경우 방지
		//알 수 없는 마임타입은 브라우저가 모두 다운로드 처리한다.
		response.setContentType("application/octet-stream");
	}
	String downName = null;
	//첨부파일 이름에 대한 한글처리 코드 적용
	if(request.getHeader("user-agent").indexOf("MSIE")==-1){
		downName = new String(b_file.getBytes("UTF-8"),"8859_1");
	}else{
		downName = new String(b_file.getBytes("EUC-KR"),"8859_1");
	}
	//응답헤더에 한글처리한 이름 정보를 담아줌.
   	response.setHeader("Content-Disposition", "attachment;filename="+downName);
 	//해당 파일을 읽어오는 객체 생성해 줌. - 이 때 파일명을 객체화 한 주소번지가 필요함. 
 	FileInputStream fis = new FileInputStream(file);
 	//서블릿 출력객체를 생성해줌.
 	try{
 		out.clear();
 		out = pageContext.pushBody();
 	}catch(Exception e){
 		
 	}
	ServletOutputStream sos = response.getOutputStream();
	try{
		byte b[] = new byte[1024*10];
		int data = 0;
		//읽어들인 파일내용이 없어질 때까지 계속해서 읽어서 써줌.
		while((data=(fis.read(b,0,b.length)))!=-1){
			sos.write(b,0,data);
		}
		//출력 버퍼를 내보냄.
		sos.flush();		
	}catch(Exception e){
		out.print(e.toString());
	}finally{
		//사용한 자원은 반납해줌.
		if(sos != null) sos.close();
		if(fis != null) fis.close();
	}
%>  
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>다운로드</title>
</head>
<body>
</body>
</html>

차시 예고(1-3)

핸들러매핑, 뷰 리졸브, 모델앤뷰

  1. ActionForward -> String -> Object(ModelAndView 클래스 모방하기 - 스프링에서 지원하는 Model)

  2. FrontMVC(.gd) -> ActionServlet(.gd2) -> ActionSupport(*.gd3)

  3. ActionServlet에서 BoardController로 넘어갈 때 메소드 설계가 안 됨(if문으로 반복 처리)
    -> HandlerMapping 설계해 본다 (if문을 가져가고 QnAController에서는 메소드마다 request 객체와 response 객체를 사용할 수 있도록 구조를 변경해 본다.)

  • 메소드 : 스프링에서 SimpleURLHandlerMapping
  1. 응답처리
    : 요청을 받아주는 클래스와 응답이 나가는 페이지를 분리해 본다.
    : if문으로 했던 부분인데 개선해 본다.
    <1-2>
    pageMove[0] = redirect
    pageMove[1] = /board/boardList.gd2
    <1-3>
    ViewResolver 클래스로 설계해 본다. -> 스프링의 InternalResourceViewResolver
  • +React(UI솔루션이나 라이브러리 지원 목적) 해보고 싶다
    • JSON 형식으로 반환하는 로직을 추가해 본다.
      -> @ResponseBody 모방해 보기 - Spring4.0지원 -> Spring5.0이후 @RestController

0개의 댓글