[LG U+ 유레카 3기] JSP + Servlet + Ajax로 도서 관리 화면 만들기

2025. 10. 31. 12:14Java/JSP


이 포스트는 JSP + Servlet + Tomcat + Ajax(fetch)로 도서 목록을 JSON으로 받아와 테이블에 출력하고,  동일한 패턴으로 등록/수정/삭제(POST)까지 확장할 준비를 마쳤다.

❶ 프로젝트 구성 & 기본 설정

  • 프로젝트: Dynamic Web Project BookManagerAjax1
  • 서버: Tomcat 10.x (Jakarta)
  • 패키지: dao.BookDao, dto.BookDto, servlet.BookServlet
  • 뷰(View): webapp/books.jsp
  • 라이브러리: gson (JSON 직렬화), JDBC 드라이버(Tomcat lib)

❷ URL 설계 & 서블릿 라우팅

요청을 /books/*로 모으고, request.getRequestURI()에서 contextPath를 뺀 나머지로 분기한다.

Dao,Dto,DBManager 파일은 저번 실습에 이어서 진행하였다.

package servlet;
import java.io.IOException;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import dao.BookDao;
import dto.BookDto;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// Ajax 를 이용한 데이터 서비스
// JSP forwarding X, JSON 데이터 응답
@WebServlet("/books/*")
public class BookServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // content-type 을 application/json, utf-8 encoding 처리
        response.setContentType("application/json; utf-8");
        String job = request.getRequestURI().substring(request.getContextPath().length());
        switch(job) {
            case "/books/list" : listBook(request, response); break;
            case "/books/detail" : detailBook(request, response); break;
            case "/books/insert" : insertBook(request, response); break;
            case "/books/update" : updateBook(request, response); break;
            case "/books/delete" : deleteBook(request, response); break;
        }
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
    private void listBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        BookDao bookDao = new BookDao();
        List<BookDto> bookList = bookDao.listBook(); // model 준비
        System.out.println(bookList);
        // 아래 jsp 포워딩 대신 List<BookDto> 를 json 으로 응답
        // Java Object -> json 문자열 변경 Library
//      request.setAttribute("bookList", bookList);
//      request.getRequestDispatcher("/bookList.jsp").forward(request, response); // jsp 포워딩
        Gson gson = new Gson();
        String jsonStr = gson.toJson(bookList); // List<BookDto> -> JSON 문자열로 변경
        System.out.println(jsonStr);
        response.getWriter().write(jsonStr); // JSON 문자열을 응답 
    }
    
    private void detailBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        BookDao bookDao = new BookDao();
        // bookId parameter
        int bookId = Integer.parseInt(request.getParameter("bookId"));
        BookDto bookDto = bookDao.detailBook(bookId); // model 준비
        System.out.println(bookDto);
        Gson gson = new Gson();
        String jsonStr = gson.toJson(bookDto);
        System.out.println(jsonStr);
        response.getWriter().write(jsonStr);
    }
    
    private void insertBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        BookDao bookDao = new BookDao();
        // bookId, bookName, publisher, price parameter => BookDto
        int bookId = Integer.parseInt(request.getParameter("bookId"));
        String bookName = request.getParameter("bookName");
        String publisher = request.getParameter("publisher");
        int price = Integer.parseInt(request.getParameter("price"));
        
        BookDto bookDto = new BookDto(bookId, bookName, publisher, price);
        int ret = bookDao.insertBook(bookDto); // model 준비
        System.out.println(ret);
        
        Gson gson = new Gson();
        JsonObject jsonObject = new JsonObject();
        // 작업의 성공, 실패를 json 으로 응답
        if( ret == 1 ) {
            jsonObject.addProperty("result", "success");
        }else {
            jsonObject.addProperty("result", "fail");
        }
        
        String jsonStr = gson.toJson(jsonObject);
        System.out.println(jsonStr);
        response.getWriter().write(jsonStr);
    }
    
    private void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        BookDao bookDao = new BookDao();
        // bookId, bookName, publisher, price parameter => BookDto
        int bookId = Integer.parseInt(request.getParameter("bookId"));
        String bookName = request.getParameter("bookName");
        String publisher = request.getParameter("publisher");
        int price = Integer.parseInt(request.getParameter("price"));
        
        BookDto bookDto = new BookDto(bookId, bookName, publisher, price);
        int ret = bookDao.updateBook(bookDto); // model 준비
        System.out.println(ret);
        Gson gson = new Gson();
        JsonObject jsonObject = new JsonObject();
        // 작업의 성공, 실패를 json 으로 응답
        if( ret == 1 ) {
            jsonObject.addProperty("result", "success");
        }else {
            jsonObject.addProperty("result", "fail");
        }
        
        String jsonStr = gson.toJson(jsonObject);
        System.out.println(jsonStr);
        response.getWriter().write(jsonStr);        
    }   
    
    private void deleteBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        BookDao bookDao = new BookDao();
        // bookId parameter
        int bookId = Integer.parseInt(request.getParameter("bookId"));
        
        int ret = bookDao.deleteBook(bookId); // model 준비
        System.out.println(ret);
        Gson gson = new Gson();
        JsonObject jsonObject = new JsonObject();
        // 작업의 성공, 실패를 json 으로 응답
        if( ret == 1 ) {
            jsonObject.addProperty("result", "success");
        }else {
            jsonObject.addProperty("result", "fail");
        }
        
        String jsonStr = gson.toJson(jsonObject);
        System.out.println(jsonStr);
        response.getWriter().write(jsonStr);
    }   
}

❸ JSP (books.jsp) — Ajax로 목록 받아 테이블 렌더링

페이지 로드 시 fetch('/BookManagerAjax1/books/list')로 JSON을 받아 테이블을 구성한다. EL 충돌을 피하기 위해 JS 템플릿 리터럴 내 ${...}\${...}로 이스케이프하거나, 아래처럼 행 클릭 이벤트를 JS에서 바인딩한다(권장).

<%@ 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" placeholder="도서ID"><br>
        <input type="text" name="bookName" id="bookName" placeholder="도서명"><br>
        <input type="text" name="publisher" id="publisher" placeholder="출판사"><br>
        <input type="text" name="price" id="price" placeholder="가격"><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.getElementById("btnInsert").addEventListener("click", insertBook);
    document.getElementById("btnUpdate").addEventListener("click", updateBook);
    document.getElementById("btnDelete").addEventListener("click", deleteBook);
    document.getElementById("btnClear").addEventListener("click", clearForm);
}


//도서 목록 불러오기
async function listBook(){
    let url = '/BookManagerAjax1/books/list';
    let response = await fetch(url);
    let data = await response.json();
    console.log("도서 목록:", data);
    makeListHtml(data);
}

//목록 HTML 구성
function makeListHtml(list){
    let listHtml = ``;
    list.forEach(book => {
        listHtml += `
            <tr 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 insertBook() {
    let urlParams = new URLSearchParams({
        bookId: document.querySelector("#bookId").value,
        bookName: document.querySelector("#bookName").value,
        publisher: document.querySelector("#publisher").value,
        price: document.querySelector("#price").value
    });

    await fetch('/BookManagerAjax1/books/insert', {
        method: 'POST',
        body: urlParams
    });

    clearForm();
    listBook();
}

//도서 상세조회
async function detailBook(bookId){
    let url = '/BookManagerAjax1/books/detail?bookId=' + bookId; // 오타 수정: Bodok → Book
    let response = await fetch(url);
    let data = await response.json();
    console.log("도서 상세:", data);

    // 폼에 데이터 채워넣기
    document.getElementById("bookId").value = data.bookId;
    document.getElementById("bookName").value = data.bookName;
    document.getElementById("publisher").value = data.publisher;
    document.getElementById("price").value = data.price;
}

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 fetchOptions = {
        method: "post",
        body: urlParams
    };

    let url = '/BookManagerAjax1/books/update';
    let response = await fetch(url, fetchOptions); // 비동기 요청
    let data = await response.json();

    console.log(data);

    if (data.result == "success") {
        alert("도서 수정 성공!");
        listBook();   // 도서 목록 갱신
        clearForm();  // 입력 폼 초기화
    } else {
        alert("도서 수정 실패!");
    }
}

async function deleteBook(){
    let bookId = document.querySelector("#bookId").value;
    let url = '/BookManagerAjax1/books/delete?bookId=' + bookId;
    let response = await fetch(url); // 비동기 요청
    let data = await response.json();

    console.log(data);

    if (data.result == "success") {
        alert("도서 삭제 성공!");
        listBook();   // 도서 목록 갱신
        clearForm();  // 입력 폼 초기화
    } else {
        alert("도서 삭제 실패!");
    }
}



//폼 초기화 함수
function clearForm() {
    document.getElementById("bookId").value = "";
    document.getElementById("bookName").value = "";
    document.getElementById("publisher").value = "";
    document.getElementById("price").value = "";
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>홈</title>
</head>
<body>
    <h1>안녕하세요, 메뉴를 클릭해 주세요!</h1>
    <a href="/BookManagerAjax1/books.jsp">도서 관리</a>
    <a href="#">회원 관리</a>
    <a href="#">거래처 관리</a>
</body>
</html>

❹ 디버깅 포인트 & 트러블슈팅

  • JSON 파싱 오류(Unexpected token '<'): JSON 대신 HTML이 응답될 때 발생. response.json() 호출 전 Status 200/Content-Type: application/json 확인.
  • JSP EL 충돌: JS 템플릿의 ${...}가 JSP EL로 해석됨. 해결: \${...}로 이스케이프 또는 행 클릭 이벤트를 JS에서 바인딩해 값 전달 회피.
  • onclick 인자 부족 → undefined: rowClick(id)만 넘기면서 함수 시그니처가 4개 인자를 요구해서 undefined 출력됨. 해결: JS에서 addEventListener로 셀 텍스트를 직접 읽어 채움.
  • 소스 폴더 문제: 실수로 src/main/java가 빠져 빌드/배포 누락. 해결: Project Properties > Java Build Path > Add Foldersrc 또는 src/main/java를 Source로 복원.

❺ 현재 상태 체크리스트

기능 상태 비고
도서 목록 조회 (GET /books/list) 완료 Gson으로 JSON 응답
테이블 렌더링 완료 innerHTML로 동적 구성
행 클릭 → 입력창 채움 완료 JS 이벤트 바인딩
등록/수정/삭제 버튼 준비 fetch(POST)로 연결 예정

❻ 개념 정리 (한 줄 실무 관점)

  • Gson: Java 객체 ↔ JSON 직렬화. response.setContentType("application/json; charset=UTF-8") 필수.
  • fetch: 비동기 통신. await response.json() 전에 응답 상태/헤더 점검.
  • 컨텍스트 경로: /${context}/... 실제 배포명과 일치시켜야 404 회피.
  • JSP EL vs JS 템플릿: 충돌 시 \${...} 또는 DOM 이벤트 바인딩.
  • MVC 분리: Servlet(Controller) ↔ DAO(Model) ↔ JSP(View) 역할 분명히.

❼ 교훈 / 핵심 요약

  • URL과 컨텍스트 경로 정합성이 Ajax 404를 대부분 좌우한다.
  • JSP에서는 EL과 JS 템플릿의 충돌을 항상 의식할 것.
  • 이벤트는 인라인 onclick보다 JS addEventListener가 안전하고 유지보수성이 높다.