관리 메뉴

거니의 velog

231117_SPRING 1 (3) 본문

대덕인재개발원_웹기반 애플리케이션

231117_SPRING 1 (3)

Unlimited00 2023. 11. 17. 08:32

[BookRetrieveController.java]

package kr.or.ddit.book.web;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookRetrieveController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/detail.do", method = RequestMethod.GET)
	public ModelAndView detail(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		// 데이터베이스에서 조회한 결과를 detailMap 변수에 담는다.
		Map<String, Object> detailMap = service.selectBook(map);
		// ModelAndView 객체 mav에 뷰로 전달할 데이터를 담는다.
		// book 이라는 키의 이름으로 쿼리의 결과를 담았다.
		mav.addObject("book", detailMap);
		// Book의 pk인 bookId도 mav 객체에 담는다.
		String bookId = map.get("bookId").toString();
		mav.addObject("bookId", bookId);
		// 응답으로 나갈 페이지 정보 설정
		mav.setViewName("book/detail");
		
		return mav;
	}
	
}

[IBookService.java]

package kr.or.ddit.book.service;

import java.util.Map;

public interface IBookService {
	
	public String insertBook(Map<String, Object> map);
	public Map<String, Object> selectBook(Map<String, Object> map);
	
}

[BookServiceImpl.java]

package kr.or.ddit.book.service;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import kr.or.ddit.book.dao.BookDAO;

/*
 * 일반적으로 서비스 레이어는 인터페이스와 클래스를 함께 사용한다.
 * 스프링은 직접 클래스를 생성하는 것을 지양하고 인터페이스를 통해 접근하는 것을 권장하는 프레임워크이다.
 */
@Service
public class BookServiceImpl implements IBookService {

	/*
	 * Service 클래스는 비즈니스 클래스가 위치하는 곳이다.
	 * 스프링 MVC 구조에서 서비스 클래스는 컨트롤러와 DAO를 연결하는 역할을 한다.
	 * 
	 * 어노테이션(@) Service는 스프링에 서비스 클래스임을 알려준다.
	 * 
	 * 데이터베이스 접근을 위해 BookDAO 인스턴스를 주입받는다.
	 * 클래스의 이름이 Impl로 끝나는 것은 implements의 약자로 관습에 따른다.
	 * Impl이 붙고 안붙고에 따라 클래스인지 인터페이스인지 구별하기 쉽다.
	 */
	@Inject
	private BookDAO dao;
	
	/**
	 * <p>책 등록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 등록할 책 데이터
	 * @return 성공시 책ID, 실패시 null
	 */
	@Override
	public String insertBook(Map<String, Object> map) {
		// status 변수에는 영향받은 행 수가 담긴다.
		// insert 구분은 입력이 성공하면 1, 실패하면 0을 리턴한다.
		int status = dao.insert(map);
		if(status == 1) {
			// 결과가 성공일 시, map 인스턴스에 book 테이블의 pk인 book_id가 담겨있다.
			return map.get("book_id").toString();
		}
		return null;
	}
	
	/**
	 * <p>책 상세보기</p>
	 * @since SampleSpringYse 1.0
	 * @author PC_23
	 * @param map 책 ID
	 * @return ID에 해당하는 책 정보
	 */
	@Override
	public Map<String, Object> selectBook(Map<String, Object> map) {
		// 서비스 내 detail 함수는 dao를 호출한 결과를 바로 리턴하는 일만 한다.
		return dao.selectBook(map);
	}

}

[BookDAO.java]

package kr.or.ddit.book.dao;

import java.util.Map;

import javax.inject.Inject;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

/*
 * 어노테이션(@) Repository는 데이터에 접근하는 클래스임을 명시한다.
 * 해당 어노테이션이 있는 클래스는 스프링이 데이터를 관리하는 클래스라고 인지하며
 * 자바 빈(Java Bean)으로 등록해서 관리한다.
 * 
 * SqlSessionTemplate 객체를 멤버 변수로 선언하는 이유는 mapper xml을 실행시키기 위해서이다.
 * 해당 객체 위에 @Inject 또는 @Autowired를 붙여서 sqlSessionTemplate 객체를 사용할 수 있도록 한다.
 * 이러한 형태를 '의존성 주입'이라고 한다. (필드 인젝션, Field Injection)
 * 
 * SqlSessionTemplate 객체는 new 키워드를 통해 직접 생성하지 않고, 의존성 주입(Dependency Injection - DI)을 통해 주입받는다.
 * 스프링은 미리 만들어 놓은 SqlSessionTemplate 타입 객체를 BookDAO 클래스 안에서 사용한다.
 * 해당 과정은 스프링에서 자동 실행되며 개발자가 직접 SqlSessionTemplate 객체를 생성하는 일 없이 곧바로 사용할 수 있다.
 * 
 * SqlSessionTemplate 객체는 root-context.xml에서 정의해둔 객체이기도 하고, 서버가 시작될 때 스프링은 미리 xml을 읽어 객체를 인스턴스화 해둔다.
 * 
 */
@Repository
public class BookDAO {
	
	/*
	 * 매퍼 xml을 실행시키기 위해서 SqlSessionTemplate 객체를 멤버 변수로 선언한다.
	 * @Inject를 붙여서 SqlSessionTemplate 객체를 사용할 수 있게 한다.
	 */
	@Inject
	private SqlSessionTemplate sqlSession;
	
	/*
	 * sqlSessionTemplate.insert()
	 * 1) 첫 번째 파라미터는 SQL Mapper의 id이다.
	 * 	book_SQL.xml에서 namespace로 설정한 'Book'과 insert 쿼리를 실행하기 위해 만든 insert 문의 id의 값 'insert'이다.
	 * 	mybatis는 네임스페이스 + id 조합으로 쿼리를 찾아서 실행한다.
	 * 2) 두 번째 파라미터는 쿼리에 전달할 데이터이다.
	 * 	mapper 내 insert 쿼리를 실행하기 위해 전달되어 지는 parameterType이 map이다.
	 * 
	 * 외부에서 Dao까지 map에 title, category, price가 담겨져서 온다.
	 * 그리고, useGeneratedKeys와 keyProperty의 설정 덕분에 book 테이블의 pk인 book_id 항목이 생긴다.
	 */
	public int insert(Map<String, Object> map) {
		/*
		 * useGeneratedKeys와 keyProperty 설정에 따라서 쿼리가 실행되고 나면 파라미터로 전달된 map 객체에 book 테이블의 PK인 book_id 항목이 생김
		 * 
		 * 기존 Map :::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000
		 * }
		 * 
		 * 쿼리 실행 후 Map:::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000, "book_id" : 1
		 * }
		 * 
		 * sqlSessionTemplate.insert()의 반환값은 쿼리의 영향을 받은 행 수(row count)이다.
		 * insert 쿼리의 경우 성공하면 1개의 행(row)이 생기므로 1을 리턴하고 실패하면 0을 리턴한다.
		 */
		return sqlSession.insert("Book.insert", map);
	}
	
	public Map<String, Object> selectBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate의 selectOne 메소드는 데이터를 한 개만 가져올 때 사용한다.
		 * 만약 쿼리 결과 행 수가 0개면 selectOne 메소드는 null을 반환하게 되고,
		 * 쿼리 결과가 여러 개면 TooManyResultException 예외를 던진다.
		 * 우리가 작성한 쿼리는 조건이 pk이고, pk는 무조건 행이 유일함을 보장하기 때문에 결과는 0개 아니면 1개이다.
		 */
		return sqlSession.selectOne("Book.selectBook", map);
	}
	
}

[book_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<!--  
	[참고 사이트] 마이바티스
	- https://mybatis.org/mybatis-3/ko/getting-started.html
	
	마이바티스는 무엇인가요?
	- 마이바티스는 개발자가 지정한 SQL, 저장 프로시저 그리고 몇 가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크입니다.
		마이바티스는 JDBC로 처리하는 상당 부분의 코드와 파라미터 설정 및 결과 매핑을 대신해준다.
		마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO를 설정해서
		매핑하기 위해 xml과 어노테이션을 사용할 수 있습니다.
		
		*** POJO란?
		- POJO(Plain Old Java Object)란, 순수한 자바 객체를 말한다.
		
			그리고, 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트
			객체지향원리를 기반으로 설계된 프로젝트(getter/setter를 이용한 VO들)
			
	namespace 항목은 쿼리가 여러 개일 때 이름 공간(namespace)을 분리하는 역할을 한다.
	쿼리 xml 파일은 보통 여러 개 생성되기 때문에 이를 구별하는 용도로 사용한다.
-->
<mapper namespace="Book">

	<!--  
		데이터 입력 sql 쿼리 작성방법
		insert into 테이블명(컬럼1, 컬럼2 ...) values (값1, 값2 ...)
		id항목은 namespace 안에서 쿼리를 구분하는 유일한 식별자 역할을 한다.
		parameterType은 쿼리에 적용할 파라미터 타입(현재 Map타입 사용)
		useGeneratedKeys와 keyProperty는 하나의 쌍으로 작성된다.
		useGeneratedKeys가 true로 설정되면 mybatis는 insert 쿼리 실행 후 생성된 pk를 파라미터 객체의 keyProperty 속성에 넣어준다.
		useGeneratedKeys : 시퀀스로 자동 증가된 번호값을 가져올 것인지에 대한 여부 설정
		keyProperty : 여부 true 설정 시, 어떤 필드 값으로 받을 것인지 설정
		
		mybatis는 쿼리를 실행할 때 파라미터를 치환하는데 #{title}은 파라미터로 입력된 키를 값으로 치환한다.
	-->
	<insert id="insert" parameterType="hashMap" useGeneratedKeys="true">
		<selectKey keyProperty="book_id" resultType="int" order="BEFORE">
			select seq_book.nextval from dual
		</selectKey>
		insert into book(
			book_id, title, category, price
		) values (
			#{book_id}, #{title}, #{category}, #{price}
		)
	</insert>
	
	<!--  
		데이터 조회 sql 쿼리 작성 방법
		
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = 1
		 
		resultType은 select 쿼리가 실행된 후 반환값을 담을 컨테이너 타입을 말한다.
	-->
	<select id="selectBook" parameterType="hashMap" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = #{bookId}
	</select>

</mapper>

[detail.jsp]

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 상세</title>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 상세</h2>
        </div>
    </div>
    <div class="container">
        <div class="row">
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">제목</label>
                    </div>
                    <div class="col-md-10">${book.title }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">카테고리</label>
                    </div>
                    <div class="col-md-10">${book.category }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">가격</label>
                    </div>
                    <div class="col-md-10">${book.price }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">입력일</label>
                    </div>
                    <div class="col-md-10">${book.insertDate }</div>
                </div>
            </div>
            <a href="/book/update.do?bookId=${bookId }" class="btn btn-info">수정</a>
            <a href="/book/list.do" class="btn btn-primary">목록</a>
            <form action="/book/delete.do" method="post" id="delForm" name="delForm">
            	<input type="hidden" name="bookId" value="${bookId }" />
	            <input type="button" class="btn btn-danger" id="delBtn" value="삭제" />
            </form>
        </div>
    </div>
</body>

</html>

- http://localhost/book/form.do

테이블 컬럼명이 대문자이다...
테이블의 컬럼명이 대문자로 리턴되어 각각의 map 안의 키로 존재하게 되므로 값을 뿌릴 때 미스매치(jsp는 소문자로 했으므로)가 된다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 상세</title>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 상세</h2>
        </div>
    </div>
    <div class="container">
        <div class="row">
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">제목</label>
                    </div>
                    <div class="col-md-10">${book.TITLE }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">카테고리</label>
                    </div>
                    <div class="col-md-10">${book.CATEGORY }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">가격</label>
                    </div>
                    <div class="col-md-10">${book.PRICE }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">입력일</label>
                    </div>
                    <div class="col-md-10">${book.INSERT_DATE }</div>
                </div>
            </div>
            <a href="/book/update.do?bookId=${bookId }" class="btn btn-info">수정</a>
            <a href="/book/list.do" class="btn btn-primary">목록</a>
            <form action="/book/delete.do" method="post" id="delForm" name="delForm">
            	<input type="hidden" name="bookId" value="${bookId }" />
	            <input type="button" class="btn btn-danger" id="delBtn" value="삭제" />
            </form>
        </div>
    </div>
</body>

</html>

- http://localhost/book/detail.do?bookId=8


[BookModifyController.java]

package kr.or.ddit.book.web;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookModifyController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/update.do", method = RequestMethod.GET)
	public ModelAndView updateForm(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		Map<String, Object> detailMap = service.selectBook(map);
		
		mav.addObject("book", detailMap);
		mav.setViewName("book/update");
		
		return mav;
	}
	
}

[update.jsp]

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 수정하기</title>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 수정하기</h2>
        </div>
    </div>
    <div class="container">
        <form action="" method="post">
            <div class="row">
                <div class="col-md-12 mb-2">
                    <div class="row">
                        <div class="col-md-2">
                            <label for="title" class="col-form-label">제목</label>
                        </div>
                        <div class="col-md-10">
                            <input type="text" class="form-control" name="title" id="title" value="${book.TITLE }" />
                        </div>
                    </div>
                </div>
                <div class="col-md-12 mb-2">
                    <div class="row">
                        <div class="col-md-2">
                            <label for="category" class="col-form-label">카테고리</label>
                        </div>
                        <div class="col-md-10">
                            <input type="text" class="form-control" name="category" id="category" value="${book.CATEGORY }" />
                        </div>
                    </div>
                </div>
                <div class="col-md-12 mb-2">
                    <div class="row">
                        <div class="col-md-2">
                            <label for="price" class="col-form-label">가격</label>
                        </div>
                        <div class="col-md-10">
                            <input type="text" class="form-control" name="price" id="price" value="${book.PRICE }" />
                        </div>
                    </div>
                </div>
                <button type="submit" class="btn btn-info">수정</button>
                <a href="/book/list.do" class="btn btn-primary">목록</a>
            </div>
        </form>
    </div>
</body>

</html>

- http://localhost/book/update.do?bookId=8


[BookModifyController.java]

package kr.or.ddit.book.web;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookModifyController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/update.do", method = RequestMethod.GET)
	public ModelAndView updateForm(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		Map<String, Object> detailMap = service.selectBook(map);
		
		mav.addObject("book", detailMap);
		mav.setViewName("book/update");
		
		return mav;
	}
	
	@RequestMapping(value = "/update.do", method = RequestMethod.POST)
	public ModelAndView updateBook(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		boolean result = service.updateBook(map);
		if(result) {
			// 업데이트가 정상적으로 동작하여 데이터가 갱신되었을 때 확인을 위해 상세페이지로 이동합니다.
			String bookId = map.get("bookId").toString();
			mav.setViewName("redirect:/book/detail.do?bookId=" + bookId);
		}else {
			// 갱신이 되지 않았을 때 GET 메소드로 redirect 하는 방법도 있지만, 상세보기 화면을 바로 보여줄 수도 있습니다.
			mav = updateForm(map);
		}
		
		return mav;
	}
	
}

[IBookService.java]

package kr.or.ddit.book.service;

import java.util.Map;

public interface IBookService {
	
	public String insertBook(Map<String, Object> map);
	public Map<String, Object> selectBook(Map<String, Object> map);
	public boolean updateBook(Map<String, Object> map);
	
}

[BookServiceImpl.java]

package kr.or.ddit.book.service;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import kr.or.ddit.book.dao.BookDAO;

/*
 * 일반적으로 서비스 레이어는 인터페이스와 클래스를 함께 사용한다.
 * 스프링은 직접 클래스를 생성하는 것을 지양하고 인터페이스를 통해 접근하는 것을 권장하는 프레임워크이다.
 */
@Service
public class BookServiceImpl implements IBookService {

	/*
	 * Service 클래스는 비즈니스 클래스가 위치하는 곳이다.
	 * 스프링 MVC 구조에서 서비스 클래스는 컨트롤러와 DAO를 연결하는 역할을 한다.
	 * 
	 * 어노테이션(@) Service는 스프링에 서비스 클래스임을 알려준다.
	 * 
	 * 데이터베이스 접근을 위해 BookDAO 인스턴스를 주입받는다.
	 * 클래스의 이름이 Impl로 끝나는 것은 implements의 약자로 관습에 따른다.
	 * Impl이 붙고 안붙고에 따라 클래스인지 인터페이스인지 구별하기 쉽다.
	 */
	@Inject
	private BookDAO dao;
	
	/**
	 * <p>책 등록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 등록할 책 데이터
	 * @return 성공시 책ID, 실패시 null
	 */
	@Override
	public String insertBook(Map<String, Object> map) {
		// status 변수에는 영향받은 행 수가 담긴다.
		// insert 구분은 입력이 성공하면 1, 실패하면 0을 리턴한다.
		int status = dao.insert(map);
		if(status == 1) {
			// 결과가 성공일 시, map 인스턴스에 book 테이블의 pk인 book_id가 담겨있다.
			return map.get("book_id").toString();
		}
		return null;
	}
	
	/**
	 * <p>책 상세보기</p>
	 * @since SampleSpringYse 1.0
	 * @author PC_23
	 * @param map 책 ID
	 * @return ID에 해당하는 책 정보
	 */
	@Override
	public Map<String, Object> selectBook(Map<String, Object> map) {
		// 서비스 내 detail 함수는 dao를 호출한 결과를 바로 리턴하는 일만 한다.
		return dao.selectBook(map);
	}
	
	/**
	 * <p>책 수정</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean updateBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.updateBook(map);
		return status == 1;
	}

}

[BookDAO.java]

package kr.or.ddit.book.dao;

import java.util.Map;

import javax.inject.Inject;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

/*
 * 어노테이션(@) Repository는 데이터에 접근하는 클래스임을 명시한다.
 * 해당 어노테이션이 있는 클래스는 스프링이 데이터를 관리하는 클래스라고 인지하며
 * 자바 빈(Java Bean)으로 등록해서 관리한다.
 * 
 * SqlSessionTemplate 객체를 멤버 변수로 선언하는 이유는 mapper xml을 실행시키기 위해서이다.
 * 해당 객체 위에 @Inject 또는 @Autowired를 붙여서 sqlSessionTemplate 객체를 사용할 수 있도록 한다.
 * 이러한 형태를 '의존성 주입'이라고 한다. (필드 인젝션, Field Injection)
 * 
 * SqlSessionTemplate 객체는 new 키워드를 통해 직접 생성하지 않고, 의존성 주입(Dependency Injection - DI)을 통해 주입받는다.
 * 스프링은 미리 만들어 놓은 SqlSessionTemplate 타입 객체를 BookDAO 클래스 안에서 사용한다.
 * 해당 과정은 스프링에서 자동 실행되며 개발자가 직접 SqlSessionTemplate 객체를 생성하는 일 없이 곧바로 사용할 수 있다.
 * 
 * SqlSessionTemplate 객체는 root-context.xml에서 정의해둔 객체이기도 하고, 서버가 시작될 때 스프링은 미리 xml을 읽어 객체를 인스턴스화 해둔다.
 * 
 */
@Repository
public class BookDAO {
	
	/*
	 * 매퍼 xml을 실행시키기 위해서 SqlSessionTemplate 객체를 멤버 변수로 선언한다.
	 * @Inject를 붙여서 SqlSessionTemplate 객체를 사용할 수 있게 한다.
	 */
	@Inject
	private SqlSessionTemplate sqlSession;
	
	/*
	 * sqlSessionTemplate.insert()
	 * 1) 첫 번째 파라미터는 SQL Mapper의 id이다.
	 * 	book_SQL.xml에서 namespace로 설정한 'Book'과 insert 쿼리를 실행하기 위해 만든 insert 문의 id의 값 'insert'이다.
	 * 	mybatis는 네임스페이스 + id 조합으로 쿼리를 찾아서 실행한다.
	 * 2) 두 번째 파라미터는 쿼리에 전달할 데이터이다.
	 * 	mapper 내 insert 쿼리를 실행하기 위해 전달되어 지는 parameterType이 map이다.
	 * 
	 * 외부에서 Dao까지 map에 title, category, price가 담겨져서 온다.
	 * 그리고, useGeneratedKeys와 keyProperty의 설정 덕분에 book 테이블의 pk인 book_id 항목이 생긴다.
	 */
	public int insert(Map<String, Object> map) {
		/*
		 * useGeneratedKeys와 keyProperty 설정에 따라서 쿼리가 실행되고 나면 파라미터로 전달된 map 객체에 book 테이블의 PK인 book_id 항목이 생김
		 * 
		 * 기존 Map :::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000
		 * }
		 * 
		 * 쿼리 실행 후 Map:::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000, "book_id" : 1
		 * }
		 * 
		 * sqlSessionTemplate.insert()의 반환값은 쿼리의 영향을 받은 행 수(row count)이다.
		 * insert 쿼리의 경우 성공하면 1개의 행(row)이 생기므로 1을 리턴하고 실패하면 0을 리턴한다.
		 */
		return sqlSession.insert("Book.insert", map);
	}
	
	public Map<String, Object> selectBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate의 selectOne 메소드는 데이터를 한 개만 가져올 때 사용한다.
		 * 만약 쿼리 결과 행 수가 0개면 selectOne 메소드는 null을 반환하게 되고,
		 * 쿼리 결과가 여러 개면 TooManyResultException 예외를 던진다.
		 * 우리가 작성한 쿼리는 조건이 pk이고, pk는 무조건 행이 유일함을 보장하기 때문에 결과는 0개 아니면 1개이다.
		 */
		return sqlSession.selectOne("Book.selectBook", map);
	}
	
	public int updateBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 update 메소드를 insert 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.update("Book.updateBook", map);
	}
	
}

[book_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<!--  
	[참고 사이트] 마이바티스
	- https://mybatis.org/mybatis-3/ko/getting-started.html
	
	마이바티스는 무엇인가요?
	- 마이바티스는 개발자가 지정한 SQL, 저장 프로시저 그리고 몇 가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크입니다.
		마이바티스는 JDBC로 처리하는 상당 부분의 코드와 파라미터 설정 및 결과 매핑을 대신해준다.
		마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO를 설정해서
		매핑하기 위해 xml과 어노테이션을 사용할 수 있습니다.
		
		*** POJO란?
		- POJO(Plain Old Java Object)란, 순수한 자바 객체를 말한다.
		
			그리고, 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트
			객체지향원리를 기반으로 설계된 프로젝트(getter/setter를 이용한 VO들)
			
	namespace 항목은 쿼리가 여러 개일 때 이름 공간(namespace)을 분리하는 역할을 한다.
	쿼리 xml 파일은 보통 여러 개 생성되기 때문에 이를 구별하는 용도로 사용한다.
-->
<mapper namespace="Book">

	<!--  
		데이터 입력 sql 쿼리 작성방법
		insert into 테이블명(컬럼1, 컬럼2 ...) values (값1, 값2 ...)
		id항목은 namespace 안에서 쿼리를 구분하는 유일한 식별자 역할을 한다.
		parameterType은 쿼리에 적용할 파라미터 타입(현재 Map타입 사용)
		useGeneratedKeys와 keyProperty는 하나의 쌍으로 작성된다.
		useGeneratedKeys가 true로 설정되면 mybatis는 insert 쿼리 실행 후 생성된 pk를 파라미터 객체의 keyProperty 속성에 넣어준다.
		useGeneratedKeys : 시퀀스로 자동 증가된 번호값을 가져올 것인지에 대한 여부 설정
		keyProperty : 여부 true 설정 시, 어떤 필드 값으로 받을 것인지 설정
		
		mybatis는 쿼리를 실행할 때 파라미터를 치환하는데 #{title}은 파라미터로 입력된 키를 값으로 치환한다.
	-->
	<insert id="insert" parameterType="hashMap" useGeneratedKeys="true">
		<selectKey keyProperty="book_id" resultType="int" order="BEFORE">
			select seq_book.nextval from dual
		</selectKey>
		insert into book(
			book_id, title, category, price
		) values (
			#{book_id}, #{title}, #{category}, #{price}
		)
	</insert>
	
	<!--  
		데이터 조회 sql 쿼리 작성 방법
		
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = 1
		 
		resultType은 select 쿼리가 실행된 후 반환값을 담을 컨테이너 타입을 말한다.
	-->
	<select id="selectBook" parameterType="hashMap" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = #{bookId}
	</select>
	
	<update id="updateBook" parameterType="hashMap">
		update book
		set
			title = #{title},
			category = #{category},
			price = #{price}
		where book_id = #{bookId}
	</update>

</mapper>

- http://localhost/book/update.do?bookId=8

- http://localhost/book/detail.do?bookId=8


-----------------------------------------------
	스프링 MVC 하루만에 배우기
	::: 2023.11.17
-----------------------------------------------

4. 책 입력 만들기

	4-1. 책 입력 기능 쿼리 생성
		- resources source package 아래에 sqlmap 폴더 생성
		- book_SQL.xml 생성
		- 책 입력 기능 쿼리 작성
			> <insert id="insert" parametertype="hashMap" useGeneratedKeys="true"></insert>
		
	4-2. 책 입력 DAO 생성
		- class BookDAO
		- @Repository 어노테이션으로 데이터를 취급하는 빈이라고 알린다.
		
	4-3. 책 입력 service 인터페이스 메소드 생성
		- interface IBookService
		- public String insertBook(Map<String, Object> map);
		
	4-4. 책 입력 service 클래스 생성
		- IBookService interface를 참조받은 구현체 클래스
		- public String insertBook(Map<String, Object> map);
		- @Service 어노테이션으로 비즈니스 로직을 담당하는 빈이라고 알린다.
		
	4-5. 책 입력 컨트롤러 메소드 생성
		- public ModelAndView insertBook(@RequestParam Map<String, Object> map);
		- @Controller 어노테이션으로 컨트롤러를 담당하는 빈이라고 알린다.
		
	4-6. 책 입력 화면을 출력하고 책 등록 확인하기
		- 결과 404에러 발생(정상적인 에러)
			> 책 등록 후, 상세보기 화면으로 이동하는데 상세보기가 현 시점에는 만들어지지 않았으므로 404에러 발생
			
5. 책 상세보기 만들기

	5-1. 책 상세보기 기능 쿼리 생성
		- 책 상세 기능 쿼리 작성
		- <select id="selectBook" parameterType="hashMap" resultType="hashMap"></select>
		
	5-2. 책 상세보기 DAO 메소드 생성
		- public Map<String, Object> selectBook(Map<String, Object> map);
		
	5-3. 책 상세보기 service 인터페이스 메소드 생성
		- public Map<String, Object> selectBook(Map<String, Object> map);
		
	5-4. 책 상세보기 service 클래스 메소드 생성
		- public Map<String, Object> selectBook(Map<String, Object> map);
		
	5-5. 책 상세보기 컨트롤러 메소드 생성
		- public ModelAndView detail(@RequestParam Map<String, Object> map);
		
	5-6. 책 상세화면 생성
		- view/book/detail.jsp
		
	5-7. 책 상세보기 화면을 출력하고 책 상세보기 확인(데이터 확인)
	
6. 책 수정 만들기

	6-1. 책 수정 화면 컨트롤러 메소드 생성
		- public ModelAndView updateForm(@RequestParam Map<String, Object> map)
		
	6-2. 책 수정 화면 뷰 생성
		- view/book/update.jsp
		
	6-3. 책 수정 화면 확인하기
	
	6-4. 책 수정 기능 쿼리 생성
		- 책 수정 기능 쿼리 작성
		- <update id="updateBook" parameterType="hashMap"></update>
		
	6-5. 책 수정 기능 DAO 메소드 생성
		- public int updateBook(Map<String, Object> map)
		
	6-6. 책 수정 기능 service 인터페이스 메소드 생성
		- public boolean updateBook(Map<String, Object> map)
		
	6-7. 책 수정 기능 service 클래스 메소드 생성
		- public boolean updateBook(Map<String, Object> map)
		
	6-8. 책 수정 컨트롤러 메소드 생성
		- public ModelAndView updateBook(@RequestParam Map<String, Object> map)
		
	6-9. 책 수정 기능 확인하기
		- 책 수정 기능 > 책 상세보기 확인

[detail.jsp]

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 상세</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 상세</h2>
        </div>
    </div>
    <div class="container">
        <div class="row">
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">제목</label>
                    </div>
                    <div class="col-md-10">${book.TITLE }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">카테고리</label>
                    </div>
                    <div class="col-md-10">${book.CATEGORY }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">가격</label>
                    </div>
                    <div class="col-md-10">${book.PRICE }</div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-2">
                        <label for="title" class="col-form-label">입력일</label>
                    </div>
                    <div class="col-md-10">${book.INSERT_DATE }</div>
                </div>
            </div>
            <a href="/book/update.do?bookId=${bookId }" class="btn btn-info">수정</a>
            <a href="/book/list.do" class="btn btn-primary">목록</a>
            <form action="/book/delete.do" method="post" id="delForm" name="delForm">
            	<input type="hidden" name="bookId" value="${bookId }" />
	            <input type="button" class="btn btn-danger" id="delBtn" value="삭제" />
            </form>
        </div>
    </div>
</body>
<script type="text/javascript">
	$(function(){
		var delBtn = $("#delBtn");
		var delForm = $("#delForm");
		
		// 삭제 버튼 클릭시
		delBtn.on("click", function(){
			if(confirm("정말로 삭제하시겠습니까?")){
				delForm.submit();
			}else {
				delForm.reset();
			}
		});
	});
</script>
</html>

[BookModifyController.java]

package kr.or.ddit.book.web;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookModifyController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/update.do", method = RequestMethod.GET)
	public ModelAndView updateForm(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		Map<String, Object> detailMap = service.selectBook(map);
		
		mav.addObject("book", detailMap);
		mav.setViewName("book/update");
		
		return mav;
	}
	
	@RequestMapping(value = "/update.do", method = RequestMethod.POST)
	public ModelAndView updateBook(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		boolean result = service.updateBook(map);
		if(result) {
			// 업데이트가 정상적으로 동작하여 데이터가 갱신되었을 때 확인을 위해 상세페이지로 이동합니다.
			String bookId = map.get("bookId").toString();
			mav.setViewName("redirect:/book/detail.do?bookId=" + bookId);
		}else {
			// 갱신이 되지 않았을 때 GET 메소드로 redirect 하는 방법도 있지만, 상세보기 화면을 바로 보여줄 수도 있습니다.
			mav = updateForm(map);
		}
		
		return mav;
	}
	
	@RequestMapping(value = "/delete.do", method = RequestMethod.POST)
	public ModelAndView deleteBook(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		// 삭제가 성공했는지 확인한다.
		boolean result = service.removeBook(map);
		if(result) {
			// 삭제가 성공했으면 상세 페이지가 없으므로 목록으로 redirect 한다.
			mav.setViewName("redirect:/book/list.do");
		}else {
			// 삭제가 실패했으면 다시 상세 페이지로 이동한다.
			String bookId = map.get("bookId").toString();
			mav.setViewName("redirect:/book/detail.do?bookId="+bookId);
		}
		
		return mav;
	}
	
}

[IBookService.java]

package kr.or.ddit.book.service;

import java.util.Map;

public interface IBookService {
	
	public String insertBook(Map<String, Object> map);
	public Map<String, Object> selectBook(Map<String, Object> map);
	public boolean updateBook(Map<String, Object> map);
	public boolean removeBook(Map<String, Object> map);
	
}

[BookServiceImpl.java]

package kr.or.ddit.book.service;

import java.util.Map;

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import kr.or.ddit.book.dao.BookDAO;

/*
 * 일반적으로 서비스 레이어는 인터페이스와 클래스를 함께 사용한다.
 * 스프링은 직접 클래스를 생성하는 것을 지양하고 인터페이스를 통해 접근하는 것을 권장하는 프레임워크이다.
 */
@Service
public class BookServiceImpl implements IBookService {

	/*
	 * Service 클래스는 비즈니스 클래스가 위치하는 곳이다.
	 * 스프링 MVC 구조에서 서비스 클래스는 컨트롤러와 DAO를 연결하는 역할을 한다.
	 * 
	 * 어노테이션(@) Service는 스프링에 서비스 클래스임을 알려준다.
	 * 
	 * 데이터베이스 접근을 위해 BookDAO 인스턴스를 주입받는다.
	 * 클래스의 이름이 Impl로 끝나는 것은 implements의 약자로 관습에 따른다.
	 * Impl이 붙고 안붙고에 따라 클래스인지 인터페이스인지 구별하기 쉽다.
	 */
	@Inject
	private BookDAO dao;
	
	/**
	 * <p>책 등록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 등록할 책 데이터
	 * @return 성공시 책ID, 실패시 null
	 */
	@Override
	public String insertBook(Map<String, Object> map) {
		// status 변수에는 영향받은 행 수가 담긴다.
		// insert 구분은 입력이 성공하면 1, 실패하면 0을 리턴한다.
		int status = dao.insert(map);
		if(status == 1) {
			// 결과가 성공일 시, map 인스턴스에 book 테이블의 pk인 book_id가 담겨있다.
			return map.get("book_id").toString();
		}
		return null;
	}
	
	/**
	 * <p>책 상세보기</p>
	 * @since SampleSpringYse 1.0
	 * @author PC_23
	 * @param map 책 ID
	 * @return ID에 해당하는 책 정보
	 */
	@Override
	public Map<String, Object> selectBook(Map<String, Object> map) {
		// 서비스 내 detail 함수는 dao를 호출한 결과를 바로 리턴하는 일만 한다.
		return dao.selectBook(map);
	}
	
	/**
	 * <p>책 수정</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean updateBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.updateBook(map);
		return status == 1;
	}
	
	/**
	 * <p>책 삭제</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean removeBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.removeBook(map);
		return status == 1;
	}

}

[BookDAO.java]

package kr.or.ddit.book.dao;

import java.util.Map;

import javax.inject.Inject;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

/*
 * 어노테이션(@) Repository는 데이터에 접근하는 클래스임을 명시한다.
 * 해당 어노테이션이 있는 클래스는 스프링이 데이터를 관리하는 클래스라고 인지하며
 * 자바 빈(Java Bean)으로 등록해서 관리한다.
 * 
 * SqlSessionTemplate 객체를 멤버 변수로 선언하는 이유는 mapper xml을 실행시키기 위해서이다.
 * 해당 객체 위에 @Inject 또는 @Autowired를 붙여서 sqlSessionTemplate 객체를 사용할 수 있도록 한다.
 * 이러한 형태를 '의존성 주입'이라고 한다. (필드 인젝션, Field Injection)
 * 
 * SqlSessionTemplate 객체는 new 키워드를 통해 직접 생성하지 않고, 의존성 주입(Dependency Injection - DI)을 통해 주입받는다.
 * 스프링은 미리 만들어 놓은 SqlSessionTemplate 타입 객체를 BookDAO 클래스 안에서 사용한다.
 * 해당 과정은 스프링에서 자동 실행되며 개발자가 직접 SqlSessionTemplate 객체를 생성하는 일 없이 곧바로 사용할 수 있다.
 * 
 * SqlSessionTemplate 객체는 root-context.xml에서 정의해둔 객체이기도 하고, 서버가 시작될 때 스프링은 미리 xml을 읽어 객체를 인스턴스화 해둔다.
 * 
 */
@Repository
public class BookDAO {
	
	/*
	 * 매퍼 xml을 실행시키기 위해서 SqlSessionTemplate 객체를 멤버 변수로 선언한다.
	 * @Inject를 붙여서 SqlSessionTemplate 객체를 사용할 수 있게 한다.
	 */
	@Inject
	private SqlSessionTemplate sqlSession;
	
	/*
	 * sqlSessionTemplate.insert()
	 * 1) 첫 번째 파라미터는 SQL Mapper의 id이다.
	 * 	book_SQL.xml에서 namespace로 설정한 'Book'과 insert 쿼리를 실행하기 위해 만든 insert 문의 id의 값 'insert'이다.
	 * 	mybatis는 네임스페이스 + id 조합으로 쿼리를 찾아서 실행한다.
	 * 2) 두 번째 파라미터는 쿼리에 전달할 데이터이다.
	 * 	mapper 내 insert 쿼리를 실행하기 위해 전달되어 지는 parameterType이 map이다.
	 * 
	 * 외부에서 Dao까지 map에 title, category, price가 담겨져서 온다.
	 * 그리고, useGeneratedKeys와 keyProperty의 설정 덕분에 book 테이블의 pk인 book_id 항목이 생긴다.
	 */
	public int insert(Map<String, Object> map) {
		/*
		 * useGeneratedKeys와 keyProperty 설정에 따라서 쿼리가 실행되고 나면 파라미터로 전달된 map 객체에 book 테이블의 PK인 book_id 항목이 생김
		 * 
		 * 기존 Map :::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000
		 * }
		 * 
		 * 쿼리 실행 후 Map:::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000, "book_id" : 1
		 * }
		 * 
		 * sqlSessionTemplate.insert()의 반환값은 쿼리의 영향을 받은 행 수(row count)이다.
		 * insert 쿼리의 경우 성공하면 1개의 행(row)이 생기므로 1을 리턴하고 실패하면 0을 리턴한다.
		 */
		return sqlSession.insert("Book.insert", map);
	}
	
	public Map<String, Object> selectBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate의 selectOne 메소드는 데이터를 한 개만 가져올 때 사용한다.
		 * 만약 쿼리 결과 행 수가 0개면 selectOne 메소드는 null을 반환하게 되고,
		 * 쿼리 결과가 여러 개면 TooManyResultException 예외를 던진다.
		 * 우리가 작성한 쿼리는 조건이 pk이고, pk는 무조건 행이 유일함을 보장하기 때문에 결과는 0개 아니면 1개이다.
		 */
		return sqlSession.selectOne("Book.selectBook", map);
	}
	
	public int updateBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 update 메소드는 insert 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.update("Book.updateBook", map);
	}
	
	public int removeBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 delete 메소드는 update 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.delete("Book.removeBook", map);
	}
	
}

[book_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<!--  
	[참고 사이트] 마이바티스
	- https://mybatis.org/mybatis-3/ko/getting-started.html
	
	마이바티스는 무엇인가요?
	- 마이바티스는 개발자가 지정한 SQL, 저장 프로시저 그리고 몇 가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크입니다.
		마이바티스는 JDBC로 처리하는 상당 부분의 코드와 파라미터 설정 및 결과 매핑을 대신해준다.
		마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO를 설정해서
		매핑하기 위해 xml과 어노테이션을 사용할 수 있습니다.
		
		*** POJO란?
		- POJO(Plain Old Java Object)란, 순수한 자바 객체를 말한다.
		
			그리고, 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트
			객체지향원리를 기반으로 설계된 프로젝트(getter/setter를 이용한 VO들)
			
	namespace 항목은 쿼리가 여러 개일 때 이름 공간(namespace)을 분리하는 역할을 한다.
	쿼리 xml 파일은 보통 여러 개 생성되기 때문에 이를 구별하는 용도로 사용한다.
-->
<mapper namespace="Book">

	<!--  
		데이터 입력 sql 쿼리 작성방법
		insert into 테이블명(컬럼1, 컬럼2 ...) values (값1, 값2 ...)
		id항목은 namespace 안에서 쿼리를 구분하는 유일한 식별자 역할을 한다.
		parameterType은 쿼리에 적용할 파라미터 타입(현재 Map타입 사용)
		useGeneratedKeys와 keyProperty는 하나의 쌍으로 작성된다.
		useGeneratedKeys가 true로 설정되면 mybatis는 insert 쿼리 실행 후 생성된 pk를 파라미터 객체의 keyProperty 속성에 넣어준다.
		useGeneratedKeys : 시퀀스로 자동 증가된 번호값을 가져올 것인지에 대한 여부 설정
		keyProperty : 여부 true 설정 시, 어떤 필드 값으로 받을 것인지 설정
		
		mybatis는 쿼리를 실행할 때 파라미터를 치환하는데 #{title}은 파라미터로 입력된 키를 값으로 치환한다.
	-->
	<insert id="insert" parameterType="hashMap" useGeneratedKeys="true">
		<selectKey keyProperty="book_id" resultType="int" order="BEFORE">
			select seq_book.nextval from dual
		</selectKey>
		insert into book(
			book_id, title, category, price
		) values (
			#{book_id}, #{title}, #{category}, #{price}
		)
	</insert>
	
	<!--  
		데이터 조회 sql 쿼리 작성 방법
		
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = 1
		 
		resultType은 select 쿼리가 실행된 후 반환값을 담을 컨테이너 타입을 말한다.
	-->
	<select id="selectBook" parameterType="hashMap" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = #{bookId}
	</select>
	
	<update id="updateBook" parameterType="hashMap">
		update book
		set
			title = #{title},
			category = #{category},
			price = #{price}
		where book_id = #{bookId}
	</update>
	
	<delete id="removeBook" parameterType="hashMap">
		delete from book 
		where book_id = #{bookId}
	</delete>

</mapper>

- http://localhost/book/detail.do?bookId=8

8번 게시글이 DB로부터 삭제된 모습을 볼 수 있다.


[BookRetrieveController.java]

package kr.or.ddit.book.web;

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

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookRetrieveController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/detail.do", method = RequestMethod.GET)
	public ModelAndView detail(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		// 데이터베이스에서 조회한 결과를 detailMap 변수에 담는다.
		Map<String, Object> detailMap = service.selectBook(map);
		// ModelAndView 객체 mav에 뷰로 전달할 데이터를 담는다.
		// book 이라는 키의 이름으로 쿼리의 결과를 담았다.
		mav.addObject("book", detailMap);
		// Book의 pk인 bookId도 mav 객체에 담는다.
		String bookId = map.get("bookId").toString();
		mav.addObject("bookId", bookId);
		// 응답으로 나갈 페이지 정보 설정
		mav.setViewName("book/detail");
		
		return mav;
	}
	
	@RequestMapping(value = "/list.do", method = RequestMethod.GET)
	public ModelAndView list() {
		ModelAndView mav = new ModelAndView();
		
		List<Map<String, Object>> list = service.selectBookList();
		
		mav.addObject("bookList", list);
		mav.setViewName("book/list");
		
		return mav;
	}
	
}

[IBookService.java]

package kr.or.ddit.book.service;

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

public interface IBookService {
	
	public String insertBook(Map<String, Object> map);
	public Map<String, Object> selectBook(Map<String, Object> map);
	public boolean updateBook(Map<String, Object> map);
	public boolean removeBook(Map<String, Object> map);
	public List<Map<String, Object>> selectBookList();
	
}

[BookServiceImpl.java]

package kr.or.ddit.book.service;

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

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import kr.or.ddit.book.dao.BookDAO;

/*
 * 일반적으로 서비스 레이어는 인터페이스와 클래스를 함께 사용한다.
 * 스프링은 직접 클래스를 생성하는 것을 지양하고 인터페이스를 통해 접근하는 것을 권장하는 프레임워크이다.
 */
@Service
public class BookServiceImpl implements IBookService {

	/*
	 * Service 클래스는 비즈니스 클래스가 위치하는 곳이다.
	 * 스프링 MVC 구조에서 서비스 클래스는 컨트롤러와 DAO를 연결하는 역할을 한다.
	 * 
	 * 어노테이션(@) Service는 스프링에 서비스 클래스임을 알려준다.
	 * 
	 * 데이터베이스 접근을 위해 BookDAO 인스턴스를 주입받는다.
	 * 클래스의 이름이 Impl로 끝나는 것은 implements의 약자로 관습에 따른다.
	 * Impl이 붙고 안붙고에 따라 클래스인지 인터페이스인지 구별하기 쉽다.
	 */
	@Inject
	private BookDAO dao;
	
	/**
	 * <p>책 등록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 등록할 책 데이터
	 * @return 성공시 책ID, 실패시 null
	 */
	@Override
	public String insertBook(Map<String, Object> map) {
		// status 변수에는 영향받은 행 수가 담긴다.
		// insert 구분은 입력이 성공하면 1, 실패하면 0을 리턴한다.
		int status = dao.insert(map);
		if(status == 1) {
			// 결과가 성공일 시, map 인스턴스에 book 테이블의 pk인 book_id가 담겨있다.
			return map.get("book_id").toString();
		}
		return null;
	}
	
	/**
	 * <p>책 상세보기</p>
	 * @since SampleSpringYse 1.0
	 * @author PC_23
	 * @param map 책 ID
	 * @return ID에 해당하는 책 정보
	 */
	@Override
	public Map<String, Object> selectBook(Map<String, Object> map) {
		// 서비스 내 detail 함수는 dao를 호출한 결과를 바로 리턴하는 일만 한다.
		return dao.selectBook(map);
	}
	
	/**
	 * <p>책 수정</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean updateBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.updateBook(map);
		return status == 1;
	}
	
	/**
	 * <p>책 삭제</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean removeBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.removeBook(map);
		return status == 1;
	}
	
	/**
	 * <p>책 목록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param 현재는 없음
	 * @return 성공 List(책), 실패 null
	 */
	@Override
	public List<Map<String, Object>> selectBookList() {
		return dao.selectBookList();
	}

}

[BookDAO.java]

package kr.or.ddit.book.dao;

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

import javax.inject.Inject;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

/*
 * 어노테이션(@) Repository는 데이터에 접근하는 클래스임을 명시한다.
 * 해당 어노테이션이 있는 클래스는 스프링이 데이터를 관리하는 클래스라고 인지하며
 * 자바 빈(Java Bean)으로 등록해서 관리한다.
 * 
 * SqlSessionTemplate 객체를 멤버 변수로 선언하는 이유는 mapper xml을 실행시키기 위해서이다.
 * 해당 객체 위에 @Inject 또는 @Autowired를 붙여서 sqlSessionTemplate 객체를 사용할 수 있도록 한다.
 * 이러한 형태를 '의존성 주입'이라고 한다. (필드 인젝션, Field Injection)
 * 
 * SqlSessionTemplate 객체는 new 키워드를 통해 직접 생성하지 않고, 의존성 주입(Dependency Injection - DI)을 통해 주입받는다.
 * 스프링은 미리 만들어 놓은 SqlSessionTemplate 타입 객체를 BookDAO 클래스 안에서 사용한다.
 * 해당 과정은 스프링에서 자동 실행되며 개발자가 직접 SqlSessionTemplate 객체를 생성하는 일 없이 곧바로 사용할 수 있다.
 * 
 * SqlSessionTemplate 객체는 root-context.xml에서 정의해둔 객체이기도 하고, 서버가 시작될 때 스프링은 미리 xml을 읽어 객체를 인스턴스화 해둔다.
 * 
 */
@Repository
public class BookDAO {
	
	/*
	 * 매퍼 xml을 실행시키기 위해서 SqlSessionTemplate 객체를 멤버 변수로 선언한다.
	 * @Inject를 붙여서 SqlSessionTemplate 객체를 사용할 수 있게 한다.
	 */
	@Inject
	private SqlSessionTemplate sqlSession;
	
	/*
	 * sqlSessionTemplate.insert()
	 * 1) 첫 번째 파라미터는 SQL Mapper의 id이다.
	 * 	book_SQL.xml에서 namespace로 설정한 'Book'과 insert 쿼리를 실행하기 위해 만든 insert 문의 id의 값 'insert'이다.
	 * 	mybatis는 네임스페이스 + id 조합으로 쿼리를 찾아서 실행한다.
	 * 2) 두 번째 파라미터는 쿼리에 전달할 데이터이다.
	 * 	mapper 내 insert 쿼리를 실행하기 위해 전달되어 지는 parameterType이 map이다.
	 * 
	 * 외부에서 Dao까지 map에 title, category, price가 담겨져서 온다.
	 * 그리고, useGeneratedKeys와 keyProperty의 설정 덕분에 book 테이블의 pk인 book_id 항목이 생긴다.
	 */
	public int insert(Map<String, Object> map) {
		/*
		 * useGeneratedKeys와 keyProperty 설정에 따라서 쿼리가 실행되고 나면 파라미터로 전달된 map 객체에 book 테이블의 PK인 book_id 항목이 생김
		 * 
		 * 기존 Map :::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000
		 * }
		 * 
		 * 쿼리 실행 후 Map:::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000, "book_id" : 1
		 * }
		 * 
		 * sqlSessionTemplate.insert()의 반환값은 쿼리의 영향을 받은 행 수(row count)이다.
		 * insert 쿼리의 경우 성공하면 1개의 행(row)이 생기므로 1을 리턴하고 실패하면 0을 리턴한다.
		 */
		return sqlSession.insert("Book.insert", map);
	}
	
	public Map<String, Object> selectBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate의 selectOne 메소드는 데이터를 한 개만 가져올 때 사용한다.
		 * 만약 쿼리 결과 행 수가 0개면 selectOne 메소드는 null을 반환하게 되고,
		 * 쿼리 결과가 여러 개면 TooManyResultException 예외를 던진다.
		 * 우리가 작성한 쿼리는 조건이 pk이고, pk는 무조건 행이 유일함을 보장하기 때문에 결과는 0개 아니면 1개이다.
		 */
		return sqlSession.selectOne("Book.selectBook", map);
	}
	
	public int updateBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 update 메소드는 insert 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.update("Book.updateBook", map);
	}
	
	public int removeBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 delete 메소드는 update 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.delete("Book.removeBook", map);
	}
	
	public List<Map<String, Object>> selectBookList() {
		return sqlSession.selectList("Book.selectBookList");
	}
	
}

[book_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<!--  
	[참고 사이트] 마이바티스
	- https://mybatis.org/mybatis-3/ko/getting-started.html
	
	마이바티스는 무엇인가요?
	- 마이바티스는 개발자가 지정한 SQL, 저장 프로시저 그리고 몇 가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크입니다.
		마이바티스는 JDBC로 처리하는 상당 부분의 코드와 파라미터 설정 및 결과 매핑을 대신해준다.
		마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO를 설정해서
		매핑하기 위해 xml과 어노테이션을 사용할 수 있습니다.
		
		*** POJO란?
		- POJO(Plain Old Java Object)란, 순수한 자바 객체를 말한다.
		
			그리고, 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트
			객체지향원리를 기반으로 설계된 프로젝트(getter/setter를 이용한 VO들)
			
	namespace 항목은 쿼리가 여러 개일 때 이름 공간(namespace)을 분리하는 역할을 한다.
	쿼리 xml 파일은 보통 여러 개 생성되기 때문에 이를 구별하는 용도로 사용한다.
-->
<mapper namespace="Book">

	<!--  
		데이터 입력 sql 쿼리 작성방법
		insert into 테이블명(컬럼1, 컬럼2 ...) values (값1, 값2 ...)
		id항목은 namespace 안에서 쿼리를 구분하는 유일한 식별자 역할을 한다.
		parameterType은 쿼리에 적용할 파라미터 타입(현재 Map타입 사용)
		useGeneratedKeys와 keyProperty는 하나의 쌍으로 작성된다.
		useGeneratedKeys가 true로 설정되면 mybatis는 insert 쿼리 실행 후 생성된 pk를 파라미터 객체의 keyProperty 속성에 넣어준다.
		useGeneratedKeys : 시퀀스로 자동 증가된 번호값을 가져올 것인지에 대한 여부 설정
		keyProperty : 여부 true 설정 시, 어떤 필드 값으로 받을 것인지 설정
		
		mybatis는 쿼리를 실행할 때 파라미터를 치환하는데 #{title}은 파라미터로 입력된 키를 값으로 치환한다.
	-->
	<insert id="insert" parameterType="hashMap" useGeneratedKeys="true">
		<selectKey keyProperty="book_id" resultType="int" order="BEFORE">
			select seq_book.nextval from dual
		</selectKey>
		insert into book(
			book_id, title, category, price
		) values (
			#{book_id}, #{title}, #{category}, #{price}
		)
	</insert>
	
	<!--  
		데이터 조회 sql 쿼리 작성 방법
		
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = 1
		 
		resultType은 select 쿼리가 실행된 후 반환값을 담을 컨테이너 타입을 말한다.
	-->
	<select id="selectBook" parameterType="hashMap" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = #{bookId}
	</select>
	
	<update id="updateBook" parameterType="hashMap">
		update book
		set
			title = #{title},
			category = #{category},
			price = #{price}
		where book_id = #{bookId}
	</update>
	
	<delete id="removeBook" parameterType="hashMap">
		delete from book 
		where book_id = #{bookId}
	</delete>
	
	<select id="selectBookList" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 order by insert_date desc
	</select>

</mapper>

[list.jsp]

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

<head>
    <meta charset="UTF-8">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 목록</title>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 목록</h2>
        </div>
    </div>
    <div class="container">
        <div class="row">
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-7"></div>
                    <div class="col-md-5">
                        <form>
                            <div class="row">
                                <div class="col-md-10">
                                    <input type="text" placeholder="검색" class="form-control" name="keyword" value="" />
                                </div>
                                <div class="col-md-2">
                                    <input type="submit" class="btn btn-secondary" value="검색" />
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <table class="table table-hover table-secondary">
                        <caption class="text-center">BOOK LIST</caption>
                        <thead class="table-dark">
                            <tr>
                                <th>제목</th>
                                <th>카테고리</th>
                                <th>가격</th>
                            </tr>
                        </thead>
                        <tbody>
                        	<c:choose>
                        		<c:when test="${empty bookList }">
		                            <tr>
		                                <td colspan="3">조회하실 책이 존재하지 않습니다.</td>
		                            </tr>
                        		</c:when>
                        		<c:otherwise>
                        			<c:forEach items="${bookList }" var="book">
			                            <tr>
			                                <td>
			                                	<a href="/book/detail.do?bookId=${book.BOOK_ID }">${book.TITLE }</a>
			                                </td>
			                                <td>${book.CATEGORY }</td>
			                                <td>${book.PRICE }</td>
			                            </tr>
                        			</c:forEach>
                        		</c:otherwise>
                        	</c:choose>
                        </tbody>
                    </table>
                </div>
            </div>
            <a href="/book/form.do" class="btn btn-primary">등록</a>
        </div>
    </div>
</body>

</html>

- http://localhost/book/list.do


[BookRetrieveController.java]

package kr.or.ddit.book.web;

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

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
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;

import kr.or.ddit.book.service.IBookService;

@Controller
@RequestMapping("/book")
public class BookRetrieveController {

	@Inject
	private IBookService service;
	
	@RequestMapping(value = "/detail.do", method = RequestMethod.GET)
	public ModelAndView detail(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		// 데이터베이스에서 조회한 결과를 detailMap 변수에 담는다.
		Map<String, Object> detailMap = service.selectBook(map);
		// ModelAndView 객체 mav에 뷰로 전달할 데이터를 담는다.
		// book 이라는 키의 이름으로 쿼리의 결과를 담았다.
		mav.addObject("book", detailMap);
		// Book의 pk인 bookId도 mav 객체에 담는다.
		String bookId = map.get("bookId").toString();
		mav.addObject("bookId", bookId);
		// 응답으로 나갈 페이지 정보 설정
		mav.setViewName("book/detail");
		
		return mav;
	}
	
	@RequestMapping(value = "/list.do", method = RequestMethod.GET)
	public ModelAndView list(@RequestParam Map<String, Object> map) {
		ModelAndView mav = new ModelAndView();
		
		List<Map<String, Object>> list = service.selectBookList(map);
		
		mav.addObject("bookList", list);
		
		// 검색 기능 추가
		// 목록 페이지에서는 ketword가 http 파라미터가 있을 수도 있고, 없을 수도 있다.
		if(map.containsKey("keyword")) {
			// 파라미터가 있다면 뷰에 keyword를 전달한다.
			mav.addObject("keyword", map.get("keyword"));
		}
		
		mav.setViewName("book/list");
		
		return mav;
	}
	
}

[IBookService.java]

package kr.or.ddit.book.service;

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

public interface IBookService {
	
	public String insertBook(Map<String, Object> map);
	public Map<String, Object> selectBook(Map<String, Object> map);
	public boolean updateBook(Map<String, Object> map);
	public boolean removeBook(Map<String, Object> map);
	public List<Map<String, Object>> selectBookList(Map<String, Object> map);
	
}

[BookServiceImpl.java]

package kr.or.ddit.book.service;

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

import javax.inject.Inject;

import org.springframework.stereotype.Service;

import kr.or.ddit.book.dao.BookDAO;

/*
 * 일반적으로 서비스 레이어는 인터페이스와 클래스를 함께 사용한다.
 * 스프링은 직접 클래스를 생성하는 것을 지양하고 인터페이스를 통해 접근하는 것을 권장하는 프레임워크이다.
 */
@Service
public class BookServiceImpl implements IBookService {

	/*
	 * Service 클래스는 비즈니스 클래스가 위치하는 곳이다.
	 * 스프링 MVC 구조에서 서비스 클래스는 컨트롤러와 DAO를 연결하는 역할을 한다.
	 * 
	 * 어노테이션(@) Service는 스프링에 서비스 클래스임을 알려준다.
	 * 
	 * 데이터베이스 접근을 위해 BookDAO 인스턴스를 주입받는다.
	 * 클래스의 이름이 Impl로 끝나는 것은 implements의 약자로 관습에 따른다.
	 * Impl이 붙고 안붙고에 따라 클래스인지 인터페이스인지 구별하기 쉽다.
	 */
	@Inject
	private BookDAO dao;
	
	/**
	 * <p>책 등록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 등록할 책 데이터
	 * @return 성공시 책ID, 실패시 null
	 */
	@Override
	public String insertBook(Map<String, Object> map) {
		// status 변수에는 영향받은 행 수가 담긴다.
		// insert 구분은 입력이 성공하면 1, 실패하면 0을 리턴한다.
		int status = dao.insert(map);
		if(status == 1) {
			// 결과가 성공일 시, map 인스턴스에 book 테이블의 pk인 book_id가 담겨있다.
			return map.get("book_id").toString();
		}
		return null;
	}
	
	/**
	 * <p>책 상세보기</p>
	 * @since SampleSpringYse 1.0
	 * @author PC_23
	 * @param map 책 ID
	 * @return ID에 해당하는 책 정보
	 */
	@Override
	public Map<String, Object> selectBook(Map<String, Object> map) {
		// 서비스 내 detail 함수는 dao를 호출한 결과를 바로 리턴하는 일만 한다.
		return dao.selectBook(map);
	}
	
	/**
	 * <p>책 수정</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean updateBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.updateBook(map);
		return status == 1;
	}
	
	/**
	 * <p>책 삭제</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param map 책 ID
	 * @return 성공 1(true), 실패 0(false)	
	 */
	@Override
	public boolean removeBook(Map<String, Object> map) {
		// 수정의 경우 입력과는 다르게 PK를 가져오거나 하는 절차가 필요 없으므로 행이 정상적을 영향 받았는지만 검사하면 된다.
		int status = dao.removeBook(map);
		return status == 1;
	}
	
	/**
	 * <p>책 목록</p>
	 * @since SampleSpringYse
	 * @author PC_23
	 * @param 현재는 없음
	 * @return 성공 List(책), 실패 null
	 */
	@Override
	public List<Map<String, Object>> selectBookList(Map<String, Object> map) {
		return dao.selectBookList(map);
	}

}

[BookDAO.java]

package kr.or.ddit.book.dao;

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

import javax.inject.Inject;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

/*
 * 어노테이션(@) Repository는 데이터에 접근하는 클래스임을 명시한다.
 * 해당 어노테이션이 있는 클래스는 스프링이 데이터를 관리하는 클래스라고 인지하며
 * 자바 빈(Java Bean)으로 등록해서 관리한다.
 * 
 * SqlSessionTemplate 객체를 멤버 변수로 선언하는 이유는 mapper xml을 실행시키기 위해서이다.
 * 해당 객체 위에 @Inject 또는 @Autowired를 붙여서 sqlSessionTemplate 객체를 사용할 수 있도록 한다.
 * 이러한 형태를 '의존성 주입'이라고 한다. (필드 인젝션, Field Injection)
 * 
 * SqlSessionTemplate 객체는 new 키워드를 통해 직접 생성하지 않고, 의존성 주입(Dependency Injection - DI)을 통해 주입받는다.
 * 스프링은 미리 만들어 놓은 SqlSessionTemplate 타입 객체를 BookDAO 클래스 안에서 사용한다.
 * 해당 과정은 스프링에서 자동 실행되며 개발자가 직접 SqlSessionTemplate 객체를 생성하는 일 없이 곧바로 사용할 수 있다.
 * 
 * SqlSessionTemplate 객체는 root-context.xml에서 정의해둔 객체이기도 하고, 서버가 시작될 때 스프링은 미리 xml을 읽어 객체를 인스턴스화 해둔다.
 * 
 */
@Repository
public class BookDAO {
	
	/*
	 * 매퍼 xml을 실행시키기 위해서 SqlSessionTemplate 객체를 멤버 변수로 선언한다.
	 * @Inject를 붙여서 SqlSessionTemplate 객체를 사용할 수 있게 한다.
	 */
	@Inject
	private SqlSessionTemplate sqlSession;
	
	/*
	 * sqlSessionTemplate.insert()
	 * 1) 첫 번째 파라미터는 SQL Mapper의 id이다.
	 * 	book_SQL.xml에서 namespace로 설정한 'Book'과 insert 쿼리를 실행하기 위해 만든 insert 문의 id의 값 'insert'이다.
	 * 	mybatis는 네임스페이스 + id 조합으로 쿼리를 찾아서 실행한다.
	 * 2) 두 번째 파라미터는 쿼리에 전달할 데이터이다.
	 * 	mapper 내 insert 쿼리를 실행하기 위해 전달되어 지는 parameterType이 map이다.
	 * 
	 * 외부에서 Dao까지 map에 title, category, price가 담겨져서 온다.
	 * 그리고, useGeneratedKeys와 keyProperty의 설정 덕분에 book 테이블의 pk인 book_id 항목이 생긴다.
	 */
	public int insert(Map<String, Object> map) {
		/*
		 * useGeneratedKeys와 keyProperty 설정에 따라서 쿼리가 실행되고 나면 파라미터로 전달된 map 객체에 book 테이블의 PK인 book_id 항목이 생김
		 * 
		 * 기존 Map :::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000
		 * }
		 * 
		 * 쿼리 실행 후 Map:::
		 * {
		 * 	"title" : "제목", "category" : "카테고리", "price" : 1000, "book_id" : 1
		 * }
		 * 
		 * sqlSessionTemplate.insert()의 반환값은 쿼리의 영향을 받은 행 수(row count)이다.
		 * insert 쿼리의 경우 성공하면 1개의 행(row)이 생기므로 1을 리턴하고 실패하면 0을 리턴한다.
		 */
		return sqlSession.insert("Book.insert", map);
	}
	
	public Map<String, Object> selectBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate의 selectOne 메소드는 데이터를 한 개만 가져올 때 사용한다.
		 * 만약 쿼리 결과 행 수가 0개면 selectOne 메소드는 null을 반환하게 되고,
		 * 쿼리 결과가 여러 개면 TooManyResultException 예외를 던진다.
		 * 우리가 작성한 쿼리는 조건이 pk이고, pk는 무조건 행이 유일함을 보장하기 때문에 결과는 0개 아니면 1개이다.
		 */
		return sqlSession.selectOne("Book.selectBook", map);
	}
	
	public int updateBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 update 메소드는 insert 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.update("Book.updateBook", map);
	}
	
	public int removeBook(Map<String, Object> map) {
		/*
		 * sqlSessionTemplate 객체의 delete 메소드는 update 메소드와 사용법이 동일하다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이며 반환값은 영향받은 행 수 이다.
		 */
		return sqlSession.delete("Book.removeBook", map);
	}
	
	public List<Map<String, Object>> selectBookList(Map<String, Object> map) {
		/*
		 * 쿼리 결과를 목록으로 받기 위해서는 sqlSessionTemplate.selectList를 사용할 수 있다.
		 * 첫 번째 파라미터는 쿼리 ID, 두 번째 파라미터는 쿼리 파라미터이다.
		 * 리턴타입을 List로 설정한 건, selectList 메소드의 결과가 집합 목록을 반환하기 때문이다.
		 */
		return sqlSession.selectList("Book.selectBookList", map);
	}
	
}

[book_SQL.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<!--  
	[참고 사이트] 마이바티스
	- https://mybatis.org/mybatis-3/ko/getting-started.html
	
	마이바티스는 무엇인가요?
	- 마이바티스는 개발자가 지정한 SQL, 저장 프로시저 그리고 몇 가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크입니다.
		마이바티스는 JDBC로 처리하는 상당 부분의 코드와 파라미터 설정 및 결과 매핑을 대신해준다.
		마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO를 설정해서
		매핑하기 위해 xml과 어노테이션을 사용할 수 있습니다.
		
		*** POJO란?
		- POJO(Plain Old Java Object)란, 순수한 자바 객체를 말한다.
		
			그리고, 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트
			객체지향원리를 기반으로 설계된 프로젝트(getter/setter를 이용한 VO들)
			
	namespace 항목은 쿼리가 여러 개일 때 이름 공간(namespace)을 분리하는 역할을 한다.
	쿼리 xml 파일은 보통 여러 개 생성되기 때문에 이를 구별하는 용도로 사용한다.
-->
<mapper namespace="Book">

	<!--  
		데이터 입력 sql 쿼리 작성방법
		insert into 테이블명(컬럼1, 컬럼2 ...) values (값1, 값2 ...)
		id항목은 namespace 안에서 쿼리를 구분하는 유일한 식별자 역할을 한다.
		parameterType은 쿼리에 적용할 파라미터 타입(현재 Map타입 사용)
		useGeneratedKeys와 keyProperty는 하나의 쌍으로 작성된다.
		useGeneratedKeys가 true로 설정되면 mybatis는 insert 쿼리 실행 후 생성된 pk를 파라미터 객체의 keyProperty 속성에 넣어준다.
		useGeneratedKeys : 시퀀스로 자동 증가된 번호값을 가져올 것인지에 대한 여부 설정
		keyProperty : 여부 true 설정 시, 어떤 필드 값으로 받을 것인지 설정
		
		mybatis는 쿼리를 실행할 때 파라미터를 치환하는데 #{title}은 파라미터로 입력된 키를 값으로 치환한다.
	-->
	<insert id="insert" parameterType="hashMap" useGeneratedKeys="true">
		<selectKey keyProperty="book_id" resultType="int" order="BEFORE">
			select seq_book.nextval from dual
		</selectKey>
		insert into book(
			book_id, title, category, price
		) values (
			#{book_id}, #{title}, #{category}, #{price}
		)
	</insert>
	
	<!--  
		데이터 조회 sql 쿼리 작성 방법
		
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = 1
		 
		resultType은 select 쿼리가 실행된 후 반환값을 담을 컨테이너 타입을 말한다.
	-->
	<select id="selectBook" parameterType="hashMap" resultType="hashMap">
		select 
			book_id, title, category, price, insert_date 
		  from book 
		 where book_id = #{bookId}
	</select>
	
	<update id="updateBook" parameterType="hashMap">
		update book
		set
			title = #{title},
			category = #{category},
			price = #{price}
		where book_id = #{bookId}
	</update>
	
	<delete id="removeBook" parameterType="hashMap">
		delete from book 
		where book_id = #{bookId}
	</delete>
	
	<!--  
		책 정렬을 위해서 order by 구문을 사용한다.
		작은 순서부터 정렬하기 위해서는 asc
		큰 순서로 정렬하기 위해서는 desc
		
		where 1=1은 관습적인 구문이다.
		1=1은 늘 참이기 때문에 where 절을 나타낼 때 사용한다.
		조건이 2개 이상일 경우 처음에 시작하는 조건은 where 절로 시작하고 두 번째로 시작하는 조건은 and이어야 한다.
		
		마이바티스 if문은 조건을 나타낸다.
		test 조건 규칙을 나타낸다.
		키워드가 있는 경우, if문의 쿼리들이 동작한다. 이 때 쿼리의 내용이 파라미터가 아니라
		마이바티스 규칙에 의해서 변경되는 것을 동적 쿼리라고 한다.
		
		sql 쿼리 조건에서 포함을 나타내는 구문은 like 절이다.
		- title like '검색어%'일 경우는 검색어로 시작한다는 뜻이다.
		- title like '%검색어'일 경우는 검색어로 끝난다는 뜻이다.
		- title like '%검색어%'일 경우는 검색어를 포함한다는 뜻이다.
	-->
	<select id="selectBookList" resultType="hashMap">
		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 insert_date desc
	</select>

</mapper>

[list.jsp]

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

<head>
    <meta charset="UTF-8">
    <link href="${pageContext.request.contextPath}/resources/css/bootstrap.min.css" rel="stylesheet" />
    <title>책 목록</title>
</head>

<body>
    <div class="jumbotron">
        <div class="container">
            <h2 class="display-4">책 목록</h2>
        </div>
    </div>
    <div class="container">
        <div class="row">
            <div class="col-md-12 mb-2">
                <div class="row">
                    <div class="col-md-7"></div>
                    <div class="col-md-5">
                        <form>
                            <div class="row">
                                <div class="col-md-10">
                                    <input type="text" placeholder="검색" class="form-control" name="keyword" value="${keyword }" />
                                </div>
                                <div class="col-md-2">
                                    <input type="submit" class="btn btn-secondary" value="검색" />
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
            <div class="col-md-12 mb-2">
                <div class="row">
                    <table class="table table-hover table-secondary">
                        <caption class="text-center">BOOK LIST</caption>
                        <thead class="table-dark">
                            <tr>
                                <th>제목</th>
                                <th>카테고리</th>
                                <th>가격</th>
                            </tr>
                        </thead>
                        <tbody>
                        	<c:choose>
                        		<c:when test="${empty bookList }">
		                            <tr>
		                                <td colspan="3">조회하실 책이 존재하지 않습니다.</td>
		                            </tr>
                        		</c:when>
                        		<c:otherwise>
                        			<c:forEach items="${bookList }" var="book">
			                            <tr>
			                                <td>
			                                	<a href="/book/detail.do?bookId=${book.BOOK_ID }">${book.TITLE }</a>
			                                </td>
			                                <td>${book.CATEGORY }</td>
			                                <td>${book.PRICE }</td>
			                            </tr>
                        			</c:forEach>
                        		</c:otherwise>
                        	</c:choose>
                        </tbody>
                    </table>
                </div>
            </div>
            <a href="/book/form.do" class="btn btn-primary">등록</a>
        </div>
    </div>
</body>

</html>

- http://localhost/book/list.do


- http://localhost/



[MainController.java]

package kr.or.ddit.main.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class MainController {

	@RequestMapping(value = { "/", "/main.do" }, method = RequestMethod.GET)
	public String main() {
		return "main";
	}
	
}

[main.jsp]

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

<head>
    <link href="${pageContext.request.contextPath }/resources/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="${pageContext.request.contextPath }/resources/css/headers.css">
    <title>메인화면</title>
</head>
<style>
    .bi {
        vertical-align: -.125em;
        fill: currentColor;
    }
</style>

<body>
    <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
        <symbol id="bootstrap" viewBox="0 0 118 94">
            <title>Bootstrap</title>
            <path fill-rule="evenodd" clip-rule="evenodd" d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"></path>
        </symbol>
        <symbol id="home" viewBox="0 0 16 16">
            <path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z" />
        </symbol>
        <symbol id="grid" viewBox="0 0 16 16">
            <path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
        </symbol>
    </svg>
    <main>
        <header>
            <div class="px-3 py-2 text-bg-dark">
                <div class="container">
                    <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
                        <a href="/" class="d-flex align-items-center my-2 my-lg-0 me-lg-auto text-white text-decoration-none">
                            <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
                                <use xlink:href="#bootstrap" />
                            </svg>
                        </a>
                        <ul class="nav col-12 col-lg-auto my-2 justify-content-center my-md-0 text-small">
                            <li>
                                <a href="/main.do" class="nav-link text-white">
                                    <svg class="bi d-block mx-auto mb-1" width="24" height="24">
                                        <use xlink:href="#home" />
                                    </svg> Home
                                </a>
                            </li>
                            <li>
                                <a href="/board/list.do" class="nav-link text-white">
                                    <svg class="bi d-block mx-auto mb-1" width="24" height="24">
                                        <use xlink:href="#grid" />
                                    </svg> 일반게시판
                                </a>
                            </li>
                            <li>
                                <a href="/notice/list.do" class="nav-link text-white">
                                    <svg class="bi d-block mx-auto mb-1" width="24" height="24">
                                        <use xlink:href="#grid" />
                                    </svg> 공지사항게시판
                                </a>
                            </li>
                            <li>
                                <a href="/free/list.do" class="nav-link text-white">
                                    <svg class="bi d-block mx-auto mb-1" width="24" height="24">
                                        <use xlink:href="#grid" />
                                    </svg> 자유게시판
                                </a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </header>

        <div class="position-relative overflow-hidden p-3 p-md-8 m-md-8 text-center bg-white">
            <div class="col-md-8 p-lg-8 mx-auto my-5">
                <h1 class="display-4 fw-normal">게시판 작성 연습</h1>
                <p class="lead fw-normal">Spring framework를 이용한 게시판 CRUD Example</p>
            </div>
            <div class="product-device shadow-sm d-none d-md-block"></div>
            <div class="product-device product-device-2 shadow-sm d-none d-md-block"></div>
        </div>

        <div class="container">
            <div class="row">
                <div class="col-md-6">
                    <div align="left">
                        <h5>일반 게시판</h5>
                    </div>
                    <div align="right">
                        <span class="badge bg-dark-subtle border border-dark-subtle text-dark-emphasis rounded-pill">전체 0건</span>
                    </div>
                    <form action="" method="post">
                        <div style="padding-top: 50px">
                            <table class="table">
                                <tr class="table-dark">
                                    <th>번호</th>
                                    <th>제목</th>
                                    <th>작성일</th>
                                </tr>
                                <tr>
                                    <td colspan="3">조회하신 게시글이 존재하지 않습니다.</td>
                                </tr>

                                <tr>
                                    <td></td>
                                    <td></td>
                                    <td></td>
                                </tr>
                            </table>
                        </div>
                    </form>
                    <a href="" class="btn btn-outline-primary">&laquo;더보기</a>
                </div>
                <div class="col-md-6">
                    <div align="left">
                        <h5>공지사항</h5>
                    </div>
                    <div align="right">
                        <span class="badge bg-dark-subtle border border-dark-subtle text-dark-emphasis rounded-pill">전체 0건</span>
                    </div>
                    <form action="" method="post">
                        <div style="padding-top: 50px">
                            <table class="table">
                                <tr class="table-dark">
                                    <th>번호</th>
                                    <th>제목</th>
                                    <th>작성일</th>
                                </tr>
                                <tr>
                                    <td colspan="3">조회하신 게시글이 존재하지 않습니다.</td>
                                </tr>

                                <tr>
                                    <td></td>
                                    <td></td>
                                    <td></td>
                                </tr>
                            </table>
                        </div>
                    </form>
                    <a href="" class="btn btn-outline-primary">&laquo;더보기</a>
                </div>
            </div>
            <br />
            <div class="row">
                <div class="col-md-6">
                    <div align="left">
                        <h5>자유 게시판</h5>
                    </div>
                    <div align="right">
                        <span class="badge bg-dark-subtle border border-dark-subtle text-dark-emphasis rounded-pill">전체 0건</span>
                    </div>
                    <form action="" method="post">
                        <div style="padding-top: 50px">
                            <table class="table">
                                <tr class="table-dark">
                                    <th>번호</th>
                                    <th>제목</th>
                                    <th>작성일</th>
                                </tr>
                                <tr>
                                    <td colspan="3">조회하신 게시글이 존재하지 않습니다.</td>
                                </tr>

                                <tr>
                                    <td></td>
                                    <td></td>
                                    <td></td>
                                </tr>
                            </table>
                        </div>
                    </form>
                    <a href="" class="btn btn-outline-primary">&laquo;더보기</a>
                </div>
                <div class="col-md-6"></div>
            </div>
            <br />
        </div>
    </main>
</body>

</html>

- http://localhost/

- http://localhost/main.do


 

'대덕인재개발원_웹기반 애플리케이션' 카테고리의 다른 글

231121_SPRING 1 (5-1)  (1) 2023.11.21
231120_SPRING 1 (4)  (0) 2023.11.20
231116_SPRING 1 (2)  (0) 2023.11.16
231115_SPRING 1 (1)  (0) 2023.11.15
231114_JSP 개론 14  (1) 2023.11.14