: a_jsp.java, b_jsp.java
: a_jsp.java - jsp파일은 두 갠데 서블릿은 한 개다
역할
👇a.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
123<br/>
<jsp:include page="b.jsp"/><!-- 액션태그 -->
<h3>456</h3>
</body>
</html>
👇b.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>b.jsp</title>
</head>
<body>
b.jsp 내용입니다.
</body>
</html>
👇출력

a.jsp url이지만 b.jsp 내용이 포함되어 있다.
<아래 파일 확인하는 경로>
C:\Users\SeulGi\Desktop\Developer\workspace_jsp.metadata.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\ROOT\org\apache\jsp\board

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<nav class="navbar navbar-expand-sm bg-light navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="">여기내GYM</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
<li class="nav-item">
<!--
확장자가 jsp이면 서블릿을 경유하지 않는다. - 목록에 보여줄 데이터가 없다?
조회버튼 -> /notice/noticeList.gd요청하자 - 오라클 서버를 경유함
확장자가 .gd이면 오라클 서버를 경유하니까 조회결과를 쥐고 있다.
쥔다 - request.setAttribute() - 화면 출력하기
-->
<a class="nav-link active" href="/notice/noticeList.gd">공지사항</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/board/boardList.jsp">게시판</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">회원관리</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">QnA게시판</a>
</li>
</ul>
</div>
</div>
</nav>
package com.util;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
public class MyBatisCommonFactory {
Logger logger = Logger.getLogger(MyBatisCommonFactory.class);
public static SqlSessionFactory sqlSessionFactory = null;
public static void init() {
try {
String resource = "com/mybatis/MapperConfig.xml";
Reader reader = null;
reader = Resources.getResourceAsReader(resource);
if(sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, "development");
}
} catch (Exception e) {
e.printStackTrace();
}
}/////////////end of init()
public static SqlSessionFactory getSqlSessionFactory() {
init();
return sqlSessionFactory;
}
}
이 설정 파일은 MyBatis를 사용하여 데이터베이스와 상호 작용하기 위한 매핑 및 환경 설정을 정의한다. MyBatis가 데이터베이스 연동 및 쿼리 수행을 할 때 이러한 설정을 참조하여 동작한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--
자바와 오라클 사이의 타입이 다르다 - 언어가 다르니까
타입이 다르다 : XXXVO패턴
jdbcType이 별도로 존재한다 : null에 대한 이슈가 존재함 - 태그 내에서 하나씩 별도로 처리하는 방법도 가능함
일괄처리 방법 : 아래 코드임
아래 코드가 없는 경우) 오라클 서버의 not null, null, 여부와 상관없이 발동됨
-->
<settings>
<setting name="jdbcTypeForNull" value="NULL" /> <!-- null값도 허용하겠다 -->
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="UNPOOLED">
<property name="driver" value="oracle.jdbc.driver.OracleDriver" />
<!-- <property name="driver" value="com.p6spy.engine.spy.P6SpyDriver"/> -->
<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl11" />
<property name="username" value="scott" />
<property name="password" value="tiger" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/mybatis/mapper/test.xml" />
<mapper resource="com/mybatis/mapper/notice.xml" />
<mapper resource="com/mybatis/mapper/board.xml" />
</mappers>
</configuration>
package com.example.demo.pojo2;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import lombok.extern.slf4j.Slf4j;
//Restful API란 무엇인가?
//전송방식 : 바이너리 - UI솔루션이 지원하는 모드 중 한 가지
@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);
String context = req.getContextPath();// /
logger.info(context);
String command = uri.substring(context.length()+1);//-> notice/noticeInsert.gd
logger.info(command);
//뒤에 의미없는 확장자 gd를 잘라내기
int end = command.lastIndexOf(".");//점이 있는 위치정보를 가져온다
logger.info(""+end);
command = command.substring(0,end);//-> notice/noticeInsert까지만 가져온다. .gd는 빼고서....
logger.info(command);//-> board/boardList or notice/noticeInsert or notice/noticeUpdate or notice/noticeDelete
String upmu[] = null;
String result = null; // -> redirect:/board/boardList.jsp, forward:/board/boardList.jsp
upmu = command.split("/");
for(String name:upmu) {
logger.info(name);
}
Controller controller = new BoardController();
if("board".equals(upmu[0])) {
logger.info("workname - board - execute 호출");
req.setAttribute("upmu",upmu);
result = controller.execute(req, res);
}
//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+".jsp"); // board/boardList.jsp
}// end of sendRedirect
else if("forward".equals(pageMove[0])) {
RequestDispatcher view = req.getRequestDispatcher("/"+path+".jsp");
view.forward(req, res);
}// end of forward
else {//콜론이 없는 경우에 실행되는 코드 : WEB-INF
path = pageMove[0]+"/"+pageMove[1]; //board/boardList
// /WEB-INF/jsp/board/boardList.jsp : 스프링에서는 ViewResolver가 해 줌
RequestDispatcher view = req.getRequestDispatcher("/WEB-INF/jsp/"+path+".jsp");
view.forward(req, res);
}//end of 배포 위치가 WEB-INF/ 아래인 경우
}////end of pageMove 배열이 null이 아닌 경우
}//////end of if
}////////end of doService
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//쿼리스트링, ?, 링크, header, 제한적임
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//body, 서버인터셉트 안 당함, 무조건 서버전달, 제한이 없음 - 바이너리 타입(첨부파일)
//POST 방식 : enctype="multipart/form-data" - 바이너리 전달 - 문자+숫자 -
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
}
package com.example.demo.pojo2;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface Controller {
public String execute(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException ;
}
XXXLogic 메소드를 호출할 때 BoardLogic클래스의 인스턴스화가 선행됨(DI지원)
여기는 POJO이므로 제어권을 개발자인 내가 가지니까 이른 인스턴스화 부분은 생략함
객체에 대한 라이프사이클 관리 책임이 개발자인 내게 있다
아쉬운 점
BoardController에서 메소드로 분할이 안 되었다는 점
대신 if문으로 처리하였다 - 별로다
Reflection API 깊은 고민 : ApplicationContext, BeanFactory 스프링 컨테이너
IoC 직접 구현해 보는 경험
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);
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);
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) { //글 등록 성공했을 때
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
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());
}
//첨부파일이 아닌 다른 정보들에 대해서도 담아준다
Enumeration<String> em = req.getParameterNames();
while(em.hasMoreElements()) {
//키값 꺼내기
String key = em.nextElement();//n_title, n_content, n_writer
pMap.put(key, req.getParameter(key));
}////////////// end of while
//첨부파일에 대한 처리
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("bs_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) {
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();
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
위 코드 중 아래 코드는 HttpServletRequest(BoardController)에서 파라미터 이름들을 가져와 Map<String, Object> pMap에 해당 파라미터들을 바인딩하는 작업을 수행한다.
1. pMap.clear()를 통해 기존에 pMap에 저장된 데이터를 초기화한다.
req.getParameterNames()를 호출하여 HTTP 요청에서 전달된 모든 파라미터의 이름들을 가져온다.
hasMoreElements()를 사용하여 가져온 요소들 중 처리해야 할 요소가 더 있는지 확인한다.
nextElement()를 호출하여 다음 파라미터의 이름을 가져온다. 가져온 이름은 String key에 할당된다.
req.getParameter(key)를 사용하여 해당 파라미터의 값을 가져와서 pMap에 해당 파라미터 이름(key)과 함께 저장한다.
이 과정을 모든 파라미터에 대해 반복한다.
마지막으로 logger.info(pMap.toString())를 통해 pMap의 내용을 로그에 출력한다.
이 코드는 HTTP 요청에서 받은 파라미터를 모두 pMap에 저장하는 역할을 한다. 이렇게 함으로써 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());
package com.example.demo.pojo2;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
//나는 Controller라는 인터페이스를 implements 하지 않아서 어떤 제약조건(추상 메소드(execute(req, res) - Tomcat이 제공해 줌.
// 단, 서블릿이어야 한다 - 그런데 서블릿 아니어도 스프링은 지원해줌)도 해당 안 됨
//나는 순수한 자바 클래스이다(순수성 - 원자성 : 다른 이종간의 결합에서 사용 가능한 상태라는 것이 장점).
public class BoardLogic {
Logger logger = Logger.getLogger(BoardLogic.class);
BoardDao bDao = new BoardDao();
public List<Map<String, Object>> boardList(Map<String, Object> pMap) {
logger.info("boardList");
//웹개발에서는 NullPointerException이 발동하면 화면 자체가 안 열림
//어떤 힌트를 보고 문제를 예측(추측)해서 하나씩 가능성을 제거해 나가는 과정을 통해 트러블슈팅을 완성함
//ArrayList로 화면은 출력된다
List<Map<String, Object>> bList = new ArrayList<>();//NullPointerException 방어코드로 ArrayList를 둔다
bList = bDao.boardList(pMap);
logger.info(bList);
return bList; //화면과 로직을 분리하자 -> POJO F/W를 설계해 본다
}
//아래 메소드는 트랜잭션 처리 대상이다
//업무적인 depth가 깊을수록 이런 상황이 발생된다
//AOP : 수평적인 관점으로 관계설정, 개입, 처리 가능하게 지원
//프레임워크의 한 종류로 공통된 관심사에 대해서 클래스의 어떤 지점을 접근하여 추가코드 없이 자동으로 일 처리를 가능하게 해주는 프로그래밍 기법
// <-> OOP : 상하관계
public int boardInsert(Map<String, Object> pMap) {
logger.info("boardInsert");
int result = 0;
result = bDao.boardInsert(pMap);
return result;
}
public int boardUpdate(Map<String, Object> pMap) {
logger.info("boardUpdate");
int result = 0;
result = bDao.boardUpdate(pMap);
return result;
}
public int boardDelete(Map<String, Object> pMap) {
logger.info("boardDelete");
int result = 0;
result = bDao.boardDelete(pMap);
return result;
}
}
package com.example.demo.pojo2;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.log4j.Logger;
import com.util.MyBatisCommonFactory;
/*
* 1-1에는 없는 XXXDao를 등판시켰다
* 왜냐하면 hibernate, myBatis 사용할 수도 있기 때문이다.
* 공통분모가 되는 클래스를 분리해서 설계하는 것이 좋다
* 그 밖에는 트랜잭션 처리를 일괄적(스프링 - 자동지원)으로 설정처리할 때도 유리함
* 트랜잭션 처리는 XXXLogic에서 여러가지 XXXDao클래스의 메소드를 호출할 수 있기 때문에 XXXLogic계층에서 AOP를 적용한 자동처리가 가능한 것이다.
*/
public class BoardDao {
Logger logger = Logger.getLogger(BoardDao.class);
SqlSessionFactory sqlSessionFactory = null;
public BoardDao() {
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
}
public List<Map<String, Object>> boardList(Map<String, Object> pMap) {
logger.info("boardList");
List<Map<String,Object>> bList = new ArrayList<>();
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
bList = sqlSession.selectList("boardList", pMap);
logger.info(bList.toString());
} catch (Exception e) {
logger.info(e.toString());
}
return bList;
}
public int boardInsert(Map<String, Object> pMap) {
logger.info("boardInsert");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardInsert", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
public int boardUpdate(Map<String, Object> pMap) {
logger.info("boardUpdate");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardUpdate", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
public int boardDelete(Map<String, Object> pMap) {
logger.info("boardDelete");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardDelete", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.mapper.boardMapper">
<resultMap id="bVO" type="com.vo.BoardVO">
</resultMap>
<select id="proc_boardList" parameterType="map" statementType="CALLABLE">
{ call proc_boardList(#{key, jdbcType=CURSOR, mode=OUT, javaType=java.sql.ResultSet, resultMap=bVO}) }
</select>
<insert id="boardInsert" parameterType="java.util.Map">
insert into board(b_no, b_title, b_content, b_writer, b_hit, b_date, b_file)
values(seq_board_no.nextval, #{b_title}, #{b_content}, #{b_writer}, 0, to_char(sysdate, 'YYYY-MM-DD HH24:MI:SS'), #{b_file})
</insert>
<update id="boardUpdate" parameterType="map">
update board
set b_title = #{b_title}
, b_content = #{b_content}
, b_writer = #{b_writer}
where b_no = #{b_no}
</update>
<delete id="boardDelete" parameterType="map">
delete from board where b_no = #{b_no}
</delete>
<select id="boardList" parameterType="map" resultType = "map">
select b_no, b_title, b_content, b_writer, b_hit, NVL(b_file, '없음') b_file from board
<where>
<if test="b_no!=null">AND b_no=#{b_no}</if>
<if test="gubun!=null">
<choose>
<when test='gubun.equals("b_title")'>
AND b_title LIKE '%'||#{keyword}||'%'
</when>
<when test='gubun.equals("b_content")'>
AND b_content LIKE '%'||#{keyword}||'%'
</when>
<when test='gubun.equals("b_writer")'>
AND b_writer LIKE '%'||#{keyword}||'%'
</when>
</choose>
</if>
</where>
order by b_no desc
</select>
</mapper>
package com.vo;
//JDBC API -> myBatis(ORM 매핑오픈소스 - if문 지원함, 동적쿼리를 지원함 - SQL문 그대로 사용) -> Hibernate(ORM 프레임워크 - SQL문 없다. 그런데 조회됨)
//JPA(DB연동 마지막 목표) : 좋다 나쁘다 문제 아님. 장단점이 분명하다. 단점) 튜닝 안 됨. 복잡한 계산식은 SQL문 사용이 유리함.
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
//왜 롬복을 사용하지?
//getter/setter메소드를 추가하지 않아도 괜찮다
//전변을 private으로 선언하였다 : 캡슐레이션(고유한 정보들을 외부에서 직접 수정하는 걸 막는다)
@Data //@Setter, @Getter
@NoArgsConstructor
public class BoardVO {
private int b_no =0;//
private String b_title =null;//
private String b_writer =null;//
private String b_content =null;//
private int b_hit =0;//
private String b_date =null;//
private String b_file =null;//
//Lombok에서 제공하는 @Builder를 사용하면 파라미터 갯수나 타입을 일일이 맞추지 않고도 자유롭게 사용 가능하다
//사용이란 생성자의 파라미터 값을 통한 전역변수들의 초기화 작업
@Builder
public BoardVO(int b_no, String b_title, String b_writer, String b_content, int b_hit, String b_date, String b_file) {
super(); //디폴트 생성자 호출 : 왜냐하면 파라미터 갖는 생성자가 하나라도 있으면 디폴트 생성자를 제공하지 않음
this.b_no = b_no;
this.b_title = b_title;
this.b_writer = b_writer;
this.b_content = b_content;
this.b_hit = b_hit;
this.b_date = b_date;
this.b_file = b_file;
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.util.*" %>
<%
List<Map<String, Object>> bList = (List)request.getAttribute("bList");
int size = 0;//총 레코드 수
if(bList != null){
size = bList.size(); //4 출력
}
//out.print(bList);
%>
<!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">
function boardList(){
location.href="/board/boardList.gd2"
}
</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++){
Map<String,Object> rmap = bList.get(i);
%>
<tr>
<td><%=rmap.get("B_NO") %></td>
<td><%=rmap.get("B_TITLE") %></td>
<td><%=rmap.get("B_FILE") %></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">
</ul>
</div>
<!-- [[ Bootstrap 페이징 처리 구간 ]] -->
<div class='board-footer'>
<button class="btn btn-warning" onclick="boardList()">
전체조회
</button>
<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.pj2">
<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>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판{WEB-INF}</title>
</head>
<body>
게시판
</body>
</html>
postman 소개 - 목업

1-1 : ActionForward
-> 1-2 : String으로 변경
jsp 페이지의 배포 위치가 두 군데이다.
WEB-INF는 url을 통해서는 페이지 접근이 불가함
보안상 해당 페이지 노출이 문제가 된다면 여기에 배포할 것
redirect나 forward 없이 바로 board/boardList가 온다
workname과 화면이름 빠져있다 -> 업무마다 변한다 -> 변수로 처리함
pageMove[]
👇webapp 아래에 있는 boarList.jsp로 이동하고자 할 때
path = "forward:board/boardList"; // webapp/board/boardList로 이동하고자 할 때

👇WEB-INF 아래에 있는 boardList.jsp로 이동하고자 할 때
path = "board/boardList"; // /WEB-INF/board/boardList로 이동하고자 할 때

데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다.
질의어(SQL)를 이용하여 데이터베이스를 접근 하는 것을 의미한다.
게시판을 예로 들어보자.
게시판 사용자는 게시글을 작성하고, 올리기 버튼을 누른다. 그 후에 다시 게시판에 돌아왔을때, 게시판은 자신의 글이 포함된 업데이트된 게시판을 보게 된다. 이러한 상황을 데이터베이스 작업으로 옮기면 사용자가 올리기 버튼을 눌렀을 시, Insert 문을 사용하여 사용자가 입력한 게시글의 데이터를 옮긴다. 그 후 게시판을 구성할 데이터를 다시 Select 하여 최신 정보로유지한다. 여기서 작업의 단위는 insert문과 select문 둘 다 합친것이다. 이러한 작업단위를 하나의 트랜잭션이라 한다. 관리자나 개발자가 하나의 트랜잭션 설계를 잘하는 것이 데이터를 다루는 것에 많은 이점이 있다.
데이터베이스 연산들의 논리적 단위이며 트랜잭션 내 모든 연산들이 정상적으로 완료되지 않으면 아무 것도 수행되지 않은 원래 상태로 복원되어야 한다.
예를들어 친구에게 인터넷 뱅킹으로 10,000원을 송금할경우 나의 계좌에서 10,000원을 줄이고 친구의 계좌에 10,000원을 증가시켜야 한다. 하지만 알 수 없는 오류로 인해 나의 계좌에서는 10,000원이 줄었지만 친구 계좌에는 10,000원이 증가되지 않는다면? 나의 10,000원은 그냥 공중으로 증발해버리는 사고가 발생한다.
이러한 경우가 생기지 않도록 중간에 오류가 발생하면 다시 처음부터 송금을 하도록 하는 것이 rollback이다.
위 송금 과정을 하나의 트랜잭션이라 볼 수 있으며 데이터베이스 연산들의 논리적 단위라 할 수 있다. 즉 한 번 질의가 실행되면 질의가 모두 수행되거나 모두 수행되지 않는 작업수행의 논리적 단위인 것이다.
대표적으로 선언적인 트랜잭션 관리 지원을 많이 사용하며 어노테이션 @Transactional을 활용하여 트랜잭션 처리를 한다. 단 몇줄의 코드만으로 다이나믹 프록시와 AOP라는 기술을 통해 크게는 인터페이스 단위에서 작게는 메서드까지 세분화하여 트랜잭션을 컨트롤 할 수 있다.
트랜잭션을 처리하다 보면 자주 발생하게 되는 상황이 있다.
바로 트랜잭션 동작 도중 다른 트랜잭션을 호출(실행)하는 상황이다.
피호출 트랜잭션의 입장에서는 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있다.
전자의 경우 중간에 오류가 발생하면 모든 트랜잭션이 롤백될 것이고, 후자의 경우 오류가 발생한 트랜잭션이 롤백 될 것이다.
이러한 트랜잭션 관련 설정은 @Transactional의 propagation 속성을 통해 지정할 수 있다.
propagation에 지정할 수 있는 값은 다양하다. 그것들을 간단히 정리하면 아래와 같다.
- REQUIRED: 현재 진행중인 트랜잭션이 있으면 그것을 사용하고, 없으면 생성한다. [DEFAULT 값]
- MANDATORY: 현재 진행중인 트랜잭션이 없으면 Exception 발생. 없으면 생성한다.
- REQUIRES_NEW: 항상 새로운 트랜잭션을 만듦 (트랜잭션을 분리)
- SUPPORTS: 현재 진행중인 트랜잭션이 있으면 그것을 사용. 없으면 그냥 진행.
- NOT_SUPPORTED: 현재 진행중인 트랜잭션이 있으면 그것을 미사용. 없으면 그냥 진행.
- NEVER: 현재 진행중인 트랜잭션이 있으면 Exception. 없으면 그냥 진행.
<트랜잭션 주의 사항>
트랜잭션은 꼭 필요한 최소한의 범위로 수행해야 한다. 왜냐하면 일반적으로 데이터베이스 커넥션은 갯수가 제한적이기 때문에 각 트랜잭션에서 커넥션을 소유하는 시간이 길어진다면, 그 이후에 사용 가능한 커넥션의 갯수가 줄어든다. 그러다 어느 순간 다른 트랜잭션이 수행될 때, 커넥션이 부족하여 커넥션을 받기 위해 기다리는 상황이 발생할 수 있다.
AOP란 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 한다. 여기서 Aspect(관점)이란 흩어진 관심사들을 하나로 모듈화 한 것을 의미한다.
객체 지항 프로그래밍(OOP)에서는 주요 관심사에 따라 클래스를 분할한다. 이 클래스들은 보통 SRP(Single Responsibility Principle)에 따라 하나의 책임만을 갖게 설계된다. 하지만 클래스를 설계하다보면 로깅, 보안, 트랜잭션 등 여러 클래스에서 공통적으로 사용하는 부가 기능들이 생긴다. 이들은 주요 비즈니스 로직은 아니지만, 반복적으로 여러 곳에서 쓰이는 데 이를 흩어진 관심사(Cross Cutting Concerns)라고 한다.
AOP 없이 흩어진 관심사를 처리하면 다음과 같은 문제가 발생한다.
따라서 흩어진 관심사를 별도의 클래스로 모듈화하여 위의 문제들을 해결하고, 결과적으로 OOP를 더욱 잘 지킬 수 있도록 도움을 주는 것이 AOP이다.
참고)
https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC
1-2에서는 기존에 없던 XXXDao를 추가하였다.
이는 Dao에서는 순수하게 DB의 List, Insert, Update, Delete를 하는 것만 남겨두는 곳이다. Logic에서는 Dao의 순수한 DB를 가져와 여러 쿼리문을 하나의 트랜잭션으로 묶어 처리하도록 하는 곳이다.
따라서 Dao 클래스를 설계하여 공통분모가 되는 부분만 Logic과 분리해서 설계했다.
또한 이후, 스프링에서 자동지원되는 일괄적 트랜잭션 처리를 할 때도 이렇게 분리를 해두면 유리하다.
트랜잭션 처리는 XXXLogic에서 여러가지 XXXDao클래스의 메소드를 호출할 수 있기 때문에 XXXLogic계층에서 AOP를 적용한 자동처리가 가능하기 때문이다.
👇BoardLogic 일부
//아래 메소드는 트랜잭션 처리 대상이다
//업무적인 depth가 깊을수록 이런 상황이 발생된다
//AOP : 수평적인 관점으로 관계설정, 개입, 처리 가능하게 지원
//프레임워크의 한 종류로 공통된 관심사에 대해서 클래스의 어떤 지점을 접근하여 추가코드 없이 자동으로 일 처리를 가능하게 해주는 프로그래밍 기법
// <-> OOP : 상하관계
public int boardInsert(Map<String, Object> pMap) {
logger.info("boardInsert");
int result = 0;
result = bDao.boardInsert(pMap);
return result;
}
👇BoardDao는 순수한 공통 부분만 있다.
public List<Map<String, Object>> boardList(Map<String, Object> pMap) {
logger.info("boardList");
List<Map<String,Object>> bList = new ArrayList<>();
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
bList = sqlSession.selectList("boardList", pMap);
logger.info(bList.toString());
} catch (Exception e) {
logger.info(e.toString());
}
return bList;
}
public int boardInsert(Map<String, Object> pMap) {
logger.info("boardInsert");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardInsert", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
public int boardUpdate(Map<String, Object> pMap) {
logger.info("boardUpdate");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardUpdate", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
public int boardDelete(Map<String, Object> pMap) {
logger.info("boardDelete");
int result = 0;
sqlSessionFactory = MyBatisCommonFactory.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
result = sqlSession.insert("boardDelete", pMap);
sqlSession.commit();//빼먹으면 물리적인테이블 반영안됨
} catch (Exception e) {
logger.info(e.toString());
}
return result;
}
}