[LG U+ 유레카 3기]Spring Boot + JDBC -> MyBatis 전환 실습

2025. 11. 6. 12:10Java/Spring

❶ 상황 설명

이번 실습에서는 기존 JDBC 기반 DAO 구현(Impl) 프로젝트를 MyBatis로 전환했다. 핵심은

DAO 구현체(BookDaoImpl) 제거@Mapper 인터페이스 + XML 매퍼로 대체하고,

View는 webapp 하위의 index.htmlwebapp/WEB-INF/jsp 하위의  jsp/books.jsp를 사용했다.

 

프로젝트 생성할땐 Spring Strarter Project  에서  MyBatis Framework을 추가해야한다.
기존의 저는 JDBC API 추가된 실습이였는데  그걸  MyBatis로  바꾸는 실습이기에 
JDBC API 해제 해주고 MyBatis Framework을 추가했습니다.

❷ 디렉터리 구조

여기서 중요한점은 config와 mapper 폴더가 생기고 xml 파일들을 하나씩 추가해줘야한다!

✅ book-mapper.xml (MyBatis 매퍼)

<?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">


<mapper namespace="com.mycom.myapp.dao.BookDao">

	<select id="listBook" resultType="com.mycom.myapp.dto.BookDto">
		select bookid bookId,bookname bookName ,publisher,price from book;
	</select>
	
	<select id="detailBook" resultType="com.mycom.myapp.dto.BookDto">
		select bookid bookId,bookname bookName ,publisher,price 
		from book 
		where bookid = #{bookId};
	</select>
	
	<insert id = "insertBook" parameterType = "com.mycom.myapp.dto.BookDto">
		insert into book (    bookid,    bookname,   publisher,   price)
		          values ( #{bookId} , #{bookName},#{publisher},#{price});		         
	</insert>
	
	<update id ="updateBook" parameterType = "com.mycom.myapp.dto.BookDto">
		update book
		 	set bookname = #{bookName}
		 	   ,publisher = #{publisher}
		 	   ,price     = #{price}
		where bookid = #{bookId};	 	   
	</update>
	
	
	<delete id ="delteBooke" parameterType = "int">
		delete form book
		where bookid = #{bookId};
	</delete>
</mapper>

✅ mybatis-config.xml (매퍼 등록)

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <mappers>
    <mapper resource="mapper/book-mapper.xml"/>
  </mappers>
  
</configuration>

 

❸ 실행 흐름 (요청 → 응답)

  1. 사용자가 http://localhost:8080/ 접속 → webapp/WEB-INF/index.html 노출
  2. 메뉴에서 /books 클릭 → Controller 매핑으로 WEB-INF/jsp/books.jsp 렌더
  3. books.jsp의 JavaScript가 Ajax로 API 호출
    • GET /books/list → 목록
    • GET /books/detail/{bookId} → 상세
    • POST /books/insert → 등록
    • POST /books/update → 수정
    • GET /books/delete/{bookId} → 삭제
  4. Controller → Service → BookDao(@Mapper)book-mapper.xml SQL 실행 → DB

❹ 핵심 코드 

✅ index.html (메뉴)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>안녕하세요</h2>
<a href="/books">도서 관리</a>
<a href="#">회원 관리</a>
<a href="#">상품 관리</a>
</body>
</html>

✅ books.jsp (Ajax 기반 CRUD 화면)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>도서 관리</title>
</head>
<body>
    <h1>도서 관리</h1>
    <table>
        <thead>
            <tr><th>bookId</th><th>bookName</th><th>publisher</th><th>price</th></tr>
        </thead>
        <tbody id="bookTbody"></tbody>
    </table>
    <hr>

    <form>
        <input type="text" name="bookId" id="bookId"></input><br>
        <input type="text" name="bookName" id="bookName"></input><br>
        <input type="text" name="publisher" id="publisher"></input><br>
        <input type="text" name="price" id="price"></input><br> 
    </form>
    <hr>
    <button id="btnInsert">등록</button> <button id="btnUpdate">수정</button> <button id="btnDelete">삭제</button> <button id="btnClear">초기화</button>

    <script>
        window.onload = function(){
            listBook();
            document.querySelector("#btnClear").onclick = clearForm;
            document.querySelector("#btnInsert").onclick = insertBook;
            document.querySelector("#btnUpdate").onclick = updateBook;
            document.querySelector("#btnDelete").onclick = deleteBook;
        }

        async function listBook(){
            let url = '/books/list';
            let response = await fetch(url);
            let data = await response.json();
            makeListHtml(data);
        }

        function makeListHtml(list){
            let listHtml = ``;
            list.forEach(book => {
                listHtml += 
                    `<tr style="cursor:pointer" data-bookId=\${book.bookId}>
                        <td>\${book.bookId}</td>
                        <td>\${book.bookName}</td>
                        <td>\${book.publisher}</td>
                        <td>\${book.price}</td>
                    </tr>`;
            });
            document.querySelector("#bookTbody").innerHTML = listHtml;
            document.querySelectorAll("#bookTbody tr").forEach(tr => {
                tr.onclick = function(){
                    let bookId = this.getAttribute("data-bookId");
                    detailBook(bookId);
                }
            });
        }

        async function detailBook(bookId){
            let url = '/books/detail/' + bookId;
            let response = await fetch(url);
            let data = await response.json();
            document.querySelector("#bookId").value = data.bookId;
            document.querySelector("#bookName").value = data.bookName;
            document.querySelector("#publisher").value = data.publisher;
            document.querySelector("#price").value = data.price;
        }

        function clearForm(){
            document.querySelector("#bookId").value = "";
            document.querySelector("#bookName").value = "";
            document.querySelector("#publisher").value = "";
            document.querySelector("#price").value = "";
        }

        async function insertBook(){
            let book = {
                bookId: document.querySelector("#bookId").value,
                bookName: document.querySelector("#bookName").value,
                publisher: document.querySelector("#publisher").value,
                price: document.querySelector("#price").value
            };
            let urlParams = new URLSearchParams(book);
            let response = await fetch('/books/insert', { method: "post", body: urlParams });
            let data = await response.json();
            if (data.result == "success"){ alert("도서 등록 성공!"); listBook(); clearForm(); }
            else { alert("도서 등록 실패!"); }
        }

        async function updateBook(){
            let urlParams = new URLSearchParams({
                bookId: document.querySelector("#bookId").value,
                bookName: document.querySelector("#bookName").value,
                publisher: document.querySelector("#publisher").value,
                price: document.querySelector("#price").value
            });
            let response = await fetch('/books/update', { method: "post", body: urlParams });
            let data = await response.json();
            if (data.result == "success"){ alert("도서 수정 성공!"); listBook(); clearForm(); }
            else { alert("도서 수정 실패!"); }
        }

        async function deleteBook(){
            let bookId = document.querySelector("#bookId").value;
            let response = await fetch('/books/delete/' + bookId);
            let data = await response.json();
            if (data.result == "success"){ alert("도서 삭제 성공!"); listBook(); clearForm(); }
            else { alert("도서 삭제 실패!"); }
        }
    </script>
</body>
</html>

✅ pom.xml (의존성)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.7</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.mycom</groupId>
	<artifactId>SpringBootMVCDBMybatis</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBootMVCDBMybatis</name>
	<description>SpringBootMVCDBMybatis</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>3.0.5</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter-test</artifactId>
			<version>3.0.5</version>
			<scope>test</scope>
		</dependency>
		
<!--		<dependency>-->
<!--			<groupId>org.springframework.boot</groupId>-->
<!--			<artifactId>spring-boot-starter-jdbc</artifactId>-->
<!--		</dependency>-->
		
		<!-- jsp -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

✅ application.properties

# Mybatis
mybatis.config-location=classpath:/config/mybatis-config.xml

 

이 코드를 무조건 추가하고 알맞는 경로에 mybatis-cofing.xml 파일을 추가해야한다.

 

❺ 왜 이 실습을 하는가 (학습 관점의 깨달음)

  • “구현 세부를 빼고 계약에 집중” — JDBC의 보일러플레이트(연결/해제/예외)를 걷어내고, 인터페이스 시그니처 = 시스템 계약에만 집중한다. 유지보수·테스트가 쉬워진다.
  • SQL을 코드에서 분리 — SQL 변경은 XML만 바꾸면 된다. 릴리즈 리스크가 줄고 협업이 쉬워진다(백엔드/DBA 역할 분리).
  • 레이어 책임 분리 감각 — Controller/Service/DAO가 왜 나뉘는지 “경계의 의미”를 손으로 체득한다. 면접에서 설계 질문에 강해진다.
  • 웹 리소스 동작 원리webapp, WEB-INF, resources/static의 탐색 우선순위를 경험으로 익혀 배포 구조 문제를 스스로 해결할 수 있다.
  • JPA로의 다리 — MyBatis의 바인딩·트랜잭션·레이어링을 이해하면 JPA/Hibernate의 추상화도 자연스럽게 흡수된다.

❻ 반드시 알아야 하는 핵심

  • namespace = 인터페이스 FQN, id = 메서드명 — 이 계약이 깨지면 바로 매핑 실패다.
  • #{} 사용이 기본${}는 문자열 치환이라 인젝션 위험. 정렬 키/컬럼명 동적 치환 등 꼭 필요할 때만 엄격히 사용.
  • 파일 경로 감각 — 정적은 webapp 또는 resources/static, JSP는 WEB-INF/jsp. 404/Whitelabel이 나면 우선 경로·우선순위를 의심.