스프링 21강 - Book 게시판 CRUD 구현하기

voilà!·2022년 2월 3일
0

JSP 스프링

목록 보기
21/31

JSP->Controller->Service->Dao->xml->DB

서비스는 인터페이스. 스프링은 껍데기인 인터페이스를 Implement한 serviceImpl을 사용해서 현실화함

왜 인터페이스를 거쳐서 jsp->service->serviceImpl로 가야하는지?
-> 컨트롤러에서 ServiceImpl을 쓸 때, Service interface를 @autowired함
-> 스프링은 interface를 거쳐서 요청을 세분화하기 때문(확장의 여지를 남겨놓는다)

VO

BookVO.java

package kr.or.ddit;

public class BookVO {
	private int bookId;
	private String title;
	private String category;
	private int price;
	private String insertDate;
	
	public int getBookId() {
		return bookId;
	}
	public void setBookId(int bookId) {
		this.bookId = bookId;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getCategory() {
		return category;
	}
	public void setCategory(String category) {
		this.category = category;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}
	public String getInsertDate() {
		return insertDate;
	}
	public void setInsertDate(String insertDate) {
		this.insertDate = insertDate;
	}
	
	@Override
	public String toString() {
		return "BookVO [bookId=" + bookId + ", title=" + title + ", category=" + category + ", price=" + price
				+ ", insertDate=" + insertDate + "]";
	}
	
	
}

Controller

BookController.java

package kr.or.ddit;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

//컨트롤러 어노테이션(Annotation)
//어노테이션이 있는 클래스
//스프링프레임워크(디자인패턴 + 라이브러리 집합)가 웹 브라우저(크롬)의 요청(request)을 받아들이는 컨트롤러라고 인지해서
//자바 빈(java Bean)으로 등록해서 관리

@Controller
public class BookController {
	private static final Logger logger = LoggerFactory.getLogger(BookController.class);
	@Autowired
	BookService bookService;
	//Model : 컨트롤러가 반환할 데이터(VO객체, List, List<VO>, Map, List<Map>, JSON)를 담당
	//			반환? 1)forward(데이터 담기 O)	2) redirect(데이터 담기 X)
	
	//View : 화면 담당
	//localhost:5090/create
	
	//요청을 매핑한다
	@RequestMapping(value="/create", method=RequestMethod.GET)
	public ModelAndView create() {
		ModelAndView mav = new ModelAndView();
		//원래는 localhost:8090/WEB-INF/view/book/create.jsp
		
		//servlet-context.xml에서...
		//prefix : /WEB-INF/views/
		mav.setViewName("book/create");
		//suffix : .jsp
		return mav;
	}
	
	//HTTP파라미터 : 웹 브라우저(크롬)에서 서버(톰캣)로 전달하는 데이터
	//				제목, 분류, 가격.. 등이 파라미터로 넘어옴
	//?title=톰소여의모험&category=소설&price=10000
	//스프링은 HTTP파라미터를 자바 메소드의 파라미터로 변환해서 컨트롤러 메소드를 호출해줌
	//map으로 받을 때에는 RequestParam 어노테이션
	//{"title":"톰소여의 모험","category":"소설"...}
	//vo로 받을때에는 ModelAtrribute 어노테이션
	@RequestMapping(value="/create", method=RequestMethod.POST)
	public ModelAndView createPost(ModelAndView mav,
			@RequestParam Map<String, Object>map) { //@RequestParam 요청 파라미터를 받아주는 어노테이션
		logger.info("map : " + map.toString());
		
		BookVO bookVO = new BookVO();
		//result : insert된 book_id의 값(p.k)
		int result = bookService.insert(mapToVO(map));
		logger.info("result : " + result);
		
		if(result==0) { //insert가 안됨 -> 책 입력 화면으로 돌아가라
			//데이터를 담지 않고 단순히 되돌아감.. --> forward아닌 redirect!
			mav.setViewName("redirect:/create");
		}else { //insert 성공 -> 상세 페이지를 요청
			//?bookId=1 <-- 쿼리스트링 or 파라미터 목록
			mav.setViewName("redirect:/detail?bookId="+result);
		}
		
		return mav;
	}
	
	//20220127숙제
	//map -> BookVO로 변환(public BookVO mapToVo(map)..메서드로 처리)
	public BookVO mapToVO(Map<String, Object>map) {
		BookVO bookVO = new BookVO();
		//bookVO.setTitle(map.get("title")); map에서 title뽑아내면 Object형이기 때문에 에러(나머지도 마찬가지!)
		bookVO.setTitle((String)map.get("title"));//형변환
		bookVO.setCategory((String)map.get("category"));
		bookVO.setPrice(Integer.parseInt((String)map.get("price")));
		return bookVO;
	}
	
	//book 상세보기
	//파라미터 목록(쿼리스트링) : bookId=1
	//{bookId,1}
	@RequestMapping(value="detail", method=RequestMethod.GET)
	public ModelAndView detail(@RequestParam Map<String,Object> map) {
		logger.info("map : " + map);
		
		BookVO bookVO = new BookVO();
		bookVO.setBookId(Integer.parseInt((String)map.get("bookId")));
		//{bookID:1,나머지는 null}
		logger.info("bookVO(before) : " + bookVO.toString());
	
		//상세보기 데이터 가져오기
		bookVO = this.bookService.detail(bookVO);
		
		//{bookId:1,나머지도 있음}
		logger.info("bookVO(after) : " + bookVO.toString());
		
		ModelAndView mav = new ModelAndView();
		//book/detail : 뷰의 경로
		//forwarding
		mav.addObject("data", bookVO); //데이터를 담음
		mav.setViewName("book/detail");
		
		return mav;
	}
	// /update?bookId=3
	// 쿼리스트링 : bookId=3
	// map : {bookId:3}
	// 책 수정화면 = 책 입력화면(jsp) + 책 상세 서비스 로직
	@RequestMapping(value = "/update", method=RequestMethod.GET)
	public ModelAndView update(@RequestParam Map<String, String> map) {
		logger.info("map : " + map.get("bookId"));
		
		//책 상세 서비스 로직 사용
		BookVO bookVO = new BookVO();
		bookVO.setBookId(Integer.parseInt(map.get("bookId")));
		bookVO = this.bookService.detail(bookVO);
		
		ModelAndView mav = new ModelAndView();
		//request.setAttribute("data", bookVO);
		mav.addObject("data", bookVO);
		//forwarding : setViewName()로 페이지 이동값 세팅
		mav.setViewName("book/update");
		
		return mav;
	}
	
	//책 수정 post
	@RequestMapping(value="/update", method=RequestMethod.POST)
	public ModelAndView updatePost(@ModelAttribute BookVO bookVO, ModelAndView mav) {
		logger.info("bookVO : " + bookVO.toString());
		//true : update성공, false : update실패
		boolean isUpdateSuccess = this.bookService.update(bookVO);
		
		if(isUpdateSuccess) { //성공
			//상세페이지로 이동
			mav.setViewName("redirect:/detail?bookId="+bookVO.getBookId());
		}else {//실패
			//책 수정화면으로 돌아가기
			//방법1)
			//mav.setViewName("redirect:/update?bookId="+bookVO.getBookId());
			//방법2)
			Map<String,String> map = new HashMap<String, String>();
			map.put("bookID", "" + bookVO.getBookId());
			mav = this.update(map);
		}
		return mav;
	}
	//책 목록 리스트 + 검색 기능 추가
	//http://localhost:8090/list?keyword=야호
	//map : {"keyword":"야호"} 이렇게 들어간다~
	@RequestMapping(value="/list", method=RequestMethod.GET)
	//public ModelAndView allList(ModelAndView mav, @RequestParam String keyword) { //방법 1. 야호를  String 매개변수로 받음
	public ModelAndView allList(ModelAndView mav, @RequestParam Map<String, Object> map) {//방법2. value를 Object로 해서 모든 형태의 데이터를 받을 수 있게 함
	//ModelAndView mav = new ModelAndView로 객체 생성안해도 파라미터에 ModelAndView mav 넣으면 만들어짐
		List<BookVO> allList = this.bookService.allList(map);
		//데이터를 VIEW(jsp)에 전달할 수 있도록 mav 객체에 add함
		mav.addObject("data",allList);
		//forwarding
		mav.setViewName("book/list"); //조립하기, servlet-context의 prefix에 /WEB-INF/views/ 마지막에 /붙으므로 book앞에 /안넣어도 됨
		return mav;
	}
	// <form method="post" action="/delete">
	//<input type="hidden" name="bookId" value="${data.bookId}" />
	@RequestMapping(value = "/delete", method = RequestMethod.POST)
	public ModelAndView deletePost(@RequestParam Map<String,Object> map, ModelAndView mav) {
		//map : {bookId=3} 처럼 키와 값으로 구성되어 있음
		boolean isDeleteSuccess = this.bookService.delete(map);
		
		if(isDeleteSuccess) { //성공
			//목록 화면으로  redirect(요청흐름을 이동시킴. 단, 데이터는 이동 못함)
			mav.setViewName("redirect:/list");
		}else { //실패
			//상세 화면으로 redirect
			mav.setViewName("redirect:/detail?bookId="+map.get("bookId").toString());
		}
		return mav;
	}
	 
	//주문하면 아래 메서드로 매핑
	@RequestMapping(value="/make", method=RequestMethod.GET)
	public ModelAndView make() {
		//라이더에 태워서 짬뽕 배송
		ModelAndView mav = new ModelAndView();
		//짬뽕 위치 알려주기
		mav.setViewName("book/make");
		//배송하기
		return mav;
	}
	//RequestMapping 어노테이션 : 웹 브라우저의 요청에 실행되는 자바 메소드를 지정해줌
	//method : 속성. http 요청 메소드를 의미함
	//			1) GET : 데이터를 변경하지 않는 경우 사용
	//			2) POST : 데이터가 변경될 경우 사용
	//웹 브라우저에 화면을 보여줄 뿐 데이터의 변경이 일어나지 않으므로 GET 메소드를 사용한 것임
	//jjajang() 메소드는 웹 브라우저에서 /jjajang 주소가 GET 방식으로 입력되었을 때 book/jjajang 경로의 뷰를 보여줌
	
	@RequestMapping(value="/jjajang", method=RequestMethod.GET)
	public ModelAndView jjajang(ModelAndView mav) { //라이더 생성
		// book/jjajang : 뷰의 경로
		mav.setViewName("book/jjajang");
		return mav;
	}
}

Dao

BookDao.java

package kr.or.ddit;

import java.util.List;
import java.util.Map;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

//매퍼 xml을 실행해주는 클래스.
//@Repository 어노테이션을 붙여서 이 클래스는 데이터에 접근하는 클래스라는 것을
//Spring에게 알려줌
//Spring이 데이터를 관리하는 클래스라고 인지해서 자바 빈(java bean)으로 등록해서 관리
@Repository
public class BookDao {
	//sqlSessionTemplate 사용
	/*
	 * new 키워드를 통해 직접 생성 안함.
	 * 의존성 주입(Dependency Injection - DI)을 통해 주입받음.
	 * 스프링은 미리 만들어놓은 sqlSessionTemplate 타입 객체를 BookDao 객체에 주입
	 * 이 과정은 자동으로 스프링에서 실행되며, 개발자가 직접 객체를 생성하지 않음(Inversion of Control - IoC) 
	 */
	
	@Autowired
	SqlSessionTemplate sqlSessionTemplate;
	
	public int insert(BookVO bookVO) {
		//book_SQL.xml파일에서
		//namespace="book"
		//id="insert"
		//book.inert : 매퍼 쿼리 명
		//bookVO : 두번째 인수.. 쿼리에 전달할 데이터(String, int, VO, Map 등이 올 수 있음)
		return this.sqlSessionTemplate.insert("book.insert",bookVO);
	}
	
	//책 상세보기
	public BookVO detail(BookVO bookVO) {
		//.selectOne 메소드 : 1행을 가져올 때 사용
		//결과 행 수가 0이면 null반환
		//결과 행 수가 2 이상일 때 TooManyResultxException 예외 발생
		//(namespace.id, 파라미터0)
		return sqlSessionTemplate.selectOne("book.detail",bookVO);
	}
	
	//책 수정하기
	public boolean update(BookVO bookVO) {
		//(namespace.id, 파라미터)
		//update 후에 영향받은 행의 수를 받음
		int result = this.sqlSessionTemplate.update("book.update",bookVO);
		//0보다 크다는 것은 update가 성공했다는 의미
		return result > 0;
	}
	//책 목록
	//map : {"keyword":"야호"}
	public List<BookVO> allList(Map<String, Object> map){
		//.selectList() 메소드는 결과 집합의 목록을 반환
		// List 타입으로 읽어들일 수 있음
		return this.sqlSessionTemplate.selectList("book.allList", map); 
	}
	
	//책 삭제하기
	public boolean delete(Map<String,Object> map) {
		//RDBMS(Relational DataBase Manegement System)에서 (테이블을 relation이라고도 함!)
		//delete 구문은 update 구문처럼 where 조건에 일치하는
		//모든 행을 삭제하므로 영향을 받은 행의 수는 0 혹은 1 이상이 됨
		int result = this.sqlSessionTemplate.delete("book.delete", map);
		//result > 0는 삭제가 잘 되었다는 의미..
		return result > 0;
	}
}

Service

BookService.java

package kr.or.ddit;

import java.util.List;
import java.util.Map;

/*
 * 서비스 클래스는 비즈니스 클래스(기능)가 위치하는 곳임.
 * 스프링 MVC 구조에서 Controller - Service - DAO
 * 컨트롤러와 DAO를 연결하는 역할도 함
 * 
 * 스프링은 직접 클래스를 생성하는 것을 지양(안함)하고,
 * 인터페이스를 통해 접근하는 것을 권장하는 프레임워크.
 */
public interface BookService {
	//메소드 시그니쳐 처리
	//book 테이블로 insert
	public int insert(BookVO bookVO);
	//book 상세보기
	BookVO detail(BookVO bookVO);
	//book 수정하기
	boolean update(BookVO bookVO);
	//book 목록보기
	public List<BookVO> allList(Map<String, Object> map);
	//책 삭제하기
	boolean delete(Map<String, Object> map);
}

BookServiceImpl.java

package kr.or.ddit;

import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

//@Service : 어노테이션.. 스프링에게 이 클래스는 서비스 클래스임을 알려줌
//스프링이 자바 빈(java bean)으로 등록하여 관리
@Service
public class BookServiceImpl implements BookService {
	private static final Logger logger = LoggerFactory.getLogger(BookServiceImpl.class);
	//이미 자바빈으로 관리되고 있는 BookDao 객체를 @Autowired로 가져다 쓴다
	//IoC(제어의 역전) : 객체의 생성주기를 개발자가 하는 것이 아니라 스프링 프레임워크가 알아서 함
	//DI(의존성 주입)
	@Autowired
	BookDao bookDao;
	
	//부모 객체의 메소드를 재정의
	@Override
	public int insert(BookVO bookVO) {
		logger.info("bookVO : " + bookVO.toString());
		int affectRowcount = this.bookDao.insert(bookVO); //인서트된 행의 수를 int로 받음
		
		if(affectRowcount == 1) { //입력이 성공
			//xml에서 selectkey에서 세팅된 그 값(max(book_id)+1)
			return bookVO.getBookId();
		}
		//입력 실패
		return 0;
	}
	//책 상세보기
	@Override
	public BookVO detail(BookVO bookVO) {
		return this.bookDao.detail(bookVO);
	}
	
	//책 수정하기
	@Override
	public boolean update(BookVO bookVO) {
		return this.bookDao.update(bookVO);
	}
	
	//책 목록
	@Override
	public List<BookVO> allList(Map<String, Object> map) {
		return this.bookDao.allList(map);
	}
	
	//책 삭제하기
	@Override
	public boolean delete(Map<String,Object> map) {
		return this.bookDao.delete(map);
	}
	
}

JSP

create.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
<!DOCTYPE html>
<html>
<head>
<title>책 생성하기</title>
</head>
<body>
	<h1>책 생성하기</h1>
	<form method="post" action="/create">
		<p>제목 : <input type="text" name="title"></p>
		<p>카테고리 : <input type="text" name="category"></p>
		<p>가격 : <input type="text" name="price"></p>
		<p><input type="submit" value="저장"></p>
		
	</form>
</body>
</html>

detail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- 
BookController에서 mav 객체에 data라는 이름으로 select검색 결과를 넣었으므로
	mav.addObject("data",bookVO)
달러{data.title} 형식으로 사용하면 됨
 -->
<!DOCTYPE html>
<html>
<head>
<title>책 상세</title>
</head>
<body>
<h1>책 상세</h1>
<p>제목 : ${data.title}</p>
<p>카테고리 : ${data.category}</p>
<p>가격 : ${data.price}</p>
<p>입력일 : ${data.insertDate}</p>
<p><a href="/update?bookId=${data.bookId}">수정</a></p>
<p><a href="/list">목록으로</a></p>
<form method="post" action="/delete">
	<input type="hidden" name="bookId" value="${data.bookId}" />
	<input type="submit" value="삭제"/>
</form>
</body>
</html>

update.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- 
BookController에서 mav 객체에 data라는 이름으로 select검색 결과를 넣었으므로
	mav.addObject("data",bookVO)
달러{data.title} 형식으로 사용하면 됨
 -->
<!DOCTYPE html>
<html>
<head>
<title>책 수정하기</title>
</head>
<body>
<h1>책 수정</h1>
<!-- action이 없으면../update를 요청 단, method는 post -->
<form method="post">
	<input type="hidden" name="bookId" value="${data.bookId}" />
	<p>제목 : <input type="text" name="title" value="${data.title}" required /></p>
	<p>카테고리 : <input type="text" name="category" value="${data.category}" required /></p>
	<p>가격 : <input type="text" name="price" value="${data.price}" /></p>
	<p><input type="submit" value="저장" />&nbsp;
	<input type="button" value="취소" onclick="javascript:location.href='/detail?bookId=${data.bookId}'"/></p>
</form>
</body>
</html>

list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 

<!DOCTYPE html>
<html>
<head>
<title>책 목록</title>
<script type="text/javascript">
function fn_create(){
	location.href="/create";
}

</script>
</head>
<body>
<h1>책 목록</h1>
<p>
	<!--
		form 태그의 기본 HTTP 메소드는 GET
		action 속성을 생략하면 현재 url(/list)을 요청 
	 -->
	 <!-- <form method="get" action="/list"> -->
	<form>
		<input type="text" name="keyword" value="${param.keyword}" />
		<input type="submit" value="검색" />
	</form>
</p>
<table border="1">
	<thead>
		<tr>
			<th>제목</th>
			<th>카테고리</th>
			<th>가격</th>
		</tr>
	</thead>
	<tbody>
	<!-- data 객체의 타입 : List<BookVO> -->
	<c:forEach var="list" items="${data}">
		<tr>
			<td><a href="/detail?bookId=${list.bookId}">${list.title}</a></td>
			<td>${list.category}</td>
			<td>${list.price}</td>
		</tr>	
</c:forEach>	
	</tbody>
</table>
<br>
<input type="button" value="책 입력" onclick="fn_create();" />
</body>
</html>

xml

book_SQL.xml

<?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="book">
	<!-- * selectKey? 
	일련번호 처리시 많이 사용
	마이바티스는 쿼리 실행 시 파라미터를 치환해줌
	selectKey 전 : {"title":"검은태양","category":"드라마","price":10000}
	selectKey 후 : {"bookId":1, "title":"검은태양","category":"드라마","price":10000}
	
	-->
	<insert id="insert" parameterType="kr.or.ddit.BookVO">
		<!-- 중요!! -->
		<selectKey order="BEFORE" keyProperty="bookId" resultType="integer">
			SELECT NVL(MAX(BOOK_ID),0)+1 FROM BOOK
		</selectKey>
		INSERT INTO BOOK(BOOK_ID,TITLE,CATEGORY,PRICE,INSERT_DATE)
		VALUES(#{bookId},#{title},#{category},#{price},SYSDATE)
	</insert>
	
	<!-- 책 상세보기 -->
	<!-- 1:다 관계가 있다면  resultType을 사용하자-->
	<select id="detail" parameterType="bookVO" resultType="bookVO">
		SELECT BOOK_ID,TITLE,CATEGORY,PRICE,INSERT_DATE
		FROM BOOK
		WHERE BOOK_ID = #{bookId}
	</select>
	
	<!-- 책 수정하기, update태그는 UPDATE 쿼리를 실행하기 위한 마이바티스 태그 -->
	<update id="update" parameterType="bookVO">
		UPDATE  BOOK
		SET     TITLE = #{title}, CATEGORY = #{category}, PRICE = #{price}
		WHERE   BOOK_ID = #{bookId}
	</update>
	
	<!-- 책 목록보기 -->
	<!-- 
		map : {keyword=야호} 로 들어오면
		아래 쿼리의 keyword가 야호로 교체됨
		
		WHERE 1 = 1은 관습적인 구문. 1 = 1은 항상 TRUE이고 
		- 조건이 2개 이상일 경우 처음 시작하는 조건은 WHERE로 시작하고 두번째로 시작하는 조건은 AND 여야 함
		- 매번 첫번째 조건인지 검사하는 것은 번거롭기 때문에 무조건 WHERE 1 = 1을 써둔 후 나머지 조건을 AND로 이어붙임
		- 동적쿼리(<if test~~~) : 쿼리의 내용이 파라미터가 아니라 마이바티스의 규칙에 의해 변경되는 것
	 -->
	<select id="allList" parameterType="hashMap" resultType="bookVO">
		SELECT BOOK_ID,TITLE,CATEGORY,PRICE,INSERT_DATE
		FROM BOOK
		WHERE 1 = 1
		<if test="keyword!=null and keyword!=''">
			AND (TITLE	  LIKE '%' || #{keyword} || '%' OR
				 CATEGORY LIKE '%' || #{keyword} || '%')
		</if>
		ORDER BY BOOK_ID DESC
	</select>
	
	<!-- 책 삭제, detail.jsp에서 작성한 name="bookId" -->
	<delete id="delete" parameterType="hashMap">
			DELETE FROM BOOK
			WHERE BOOK_ID = #{bookId}
	</delete>
</mapper>

0개의 댓글