감 잃지말고 개발하기

[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #4. 데이터 출력하기 (1) 본문

JSP/MVC

[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #4. 데이터 출력하기 (1)

persii 2023. 6. 12. 16:10

지난 포스팅에서 교보문고의 도서 상세 페이지를 참고해 사용자가 입력한 다중 카테고리 데이터를 서버에 저장하는 로직을 기록해 보았다.

 

이번 포스팅에서는 이번 "도서 사이트에서 다중 카테고리 구현하기" 프로젝트의 최종 목표-도서 상세 페이지에서 해당 도서의 다중 카테고리 데이터를 출력-에 해당하는 로직을 기록하고자 한다.

 


♠ "도서 사이트에서 다중 카테고리 구현" 서버 로직이 궁금하면 아래 포스팅을 참고하세요 ♠

 

2023.06.07 - [JSP/MVC] - [JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #3. 데이터 저장하기

 

[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #2. 데이터 저장하기

지난 포스팅에서 교보문고의 도서 상세 페이지를 참고해 클라 딴에서 다중 카테고리를 구현하는 로직을 기록해 보았다. 이번 포스팅에서는 사용자가 입력한 클라 딴 데이터를 서버에 저장하는

persimmon-ary-stepbystep.tistory.com


 

목표

♠ 다중 카테고리를 구현할 수 있다.

♠ 셀프 조인 및 이너 조인을 자유롭게 사용할 수 있다.

 

 

클라이언트 딴 구현 모습

구현할 도서 상세 페이지 모습은 아래와 같다(빨간 박스).

 

교보문고 사이트의 도서 상세 페이지와 마찬가지로 해당 도서의 카테고리 데이터는 아래 이미지처럼 두 곳에 출력되는데,

도서 정보 상단에 메인 분류 데이터를, 도서 상세 정보란에 메인 분류 + 서브 분류 데이터가 출력되도록 할 것이다.

 

도서 상세 페이지

 

 

로직 코드 및 실행 화면

다중 카테고리 구현에 초점을 맞추기 위해 기본적인 MVC 패턴의 흐름 과정은 생략하도록 하겠다.

1. Action 클래스(BookViewAction.java)

특정 도서의 상세 정보 보기 요청을 처리하는 클래스이다. 

 

☞ req.getParameter("b_id")

특정 도서 상세보기 페이지를 요청하면 해당 도서의 ID가 URL 파라미터로 넘어오는데, 해당 도서의 PK값이다.

 

service.getBookView(b_id);

Service 클래스의 getBookView() 메서드를 호출해 DB에 저장된 해당 도서의 정보를 가져올 것이다.

인자로 PK값을 넘겨준다.

 

package action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import svc.BookViewService;
import vo.ActionForward;
import vo.Book;

public class BookViewAction implements Action {

	@Override
	public ActionForward execute(HttpServletRequest req, 
                          	  	HttpServletResponse resp) throws Exception {

		/* URL로 건너온 파라미터 저장 */		
		int b_id = Integer.parseInt(req.getParameter("b_id"));
		
		/* DB 처리 */
		BookViewService service = new BookViewService();
		Book book = service.getBookView(b_id);
		
		System.out.println(" 도서 정보 : "+book.toString());
		
		/* 포워딩할 때 가져갈 정보 저장 
		 * 책 상품 정보를 request 영역에 속성으로 공유 
		 * .ok -> .jsp : Forward
		 * */
		req.setAttribute("book", book);
		ActionForward forward = new ActionForward();
		forward.setPath("/book/bookView.jsp");
		forward.setRedirect(false);

		return forward;
	}
}

 

2. Service 클래스(ManageBookRegistProService.java)

특정 도서의 상세 정보를 가져오는 비즈니스 클래스이다. 

 

☞ bookDAO.getBookView(int b_id);

b_id에 해당하는 책 정보를 리턴한다.

 

package svc;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

import dao.BookDAO;
import vo.Book;
import static db.JdbcUtil.*;

public class BookViewService {

	/** B_ID에 해당하는 책 정보를 리턴하는 메서드 
	 * @throws SQLException */
	public Book getBookView(int b_id) throws SQLException {
    
		Connection conn = getConnection();
		BookDAO bookDAO = BookDAO.getInstance();
		bookDAO.setConnection(conn);
		Book book = bookDAO.selectBook(b_id);
		close(conn);
		return book;
	}
}

 


DAO 클래스에서 b_id에 따른 도서 정보를 구하는 메서드를 작성하기에 앞서, 

도서 정보를 가져오기 위해 필요한 쿼리문을 먼저 정리하고 넘어가도록 하자.

 

※ 쿼리문 정리(DQL)   

1. 생각해보기

우리는 지금 결론적으로 총 3개의 테이블을 조인해서 필요한 데이터를 가져와야 한다.

 


♠ 테이블 관계가 궁금하면 아래 포스팅을 참고하세요 ♠

 

2023.06.12 - [JSP/MVC] - [JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #1. 구현 틀 잡기

 

[JSP][MVC][MySQL] 도서 사이트에서 다중 카테고리 구현하기 #1. 구현 틀 잡기

"도서 등록 페이지 카테고리 구현하기" 포스팅에서는 클라 딴 페이지에서 카테고리 1개를 구현하는 로직을 정리해 보았다(아래 포스팅 참고). 이번 포스팅에서는 이것을 조금 더 응용해 다중 카

persimmon-ary-stepbystep.tistory.com


 

☞ 생각 1.

도서 분류 코드 데이터와 관련해 book 테이블에는 메인 분류 코드가, booksubcatgy 테이블에는 서브 분류 코드가 저장되어 있다.

때문에 각각의 테이블에서 해당 코드(메인 및 서브 분류 코드)의 대 / 중분류 코드를 알기 위해선 bookcatgycode 테이블과 조인을 해야 한다.

 

 생각 2.

근데, 이 bookcatgycode 테이블의 데이터는 계층형의 구조를 가진다.

하나의 row에 소분류 코드와 코드명, 대 / 중분류 코드는 알 수 있지만 대 / 중분류 코드명은 알 수 없다.

때문에 특정 소분류 코드의 대 / 중분류 코드명을 알기 위해선 셀프 조인을 해야 한다.

 

2. 쿼리문

위 생각을 토대로 쿼리문을 작성해 보자.

 

 생각 1. 의 쿼리문 : 3개의 테이블을 이너 조인한다.

 

쿼리문 설명 

  1. 도서 아이디(b_id)가 128인 row만 가져올 것이다.
  2. book 테이블과 booksubcatgy 테이블을 조인해 도서 아이디가 128인 row를 가져온다.
  3. booksubcatgy 테이블과 bookcatgycode 테이블을 조인해 서브 분류 코드(bsc_bc_code)에 해당하는 row를 가져온다.
SELECT b.b_id, b.b_name, b.b_writer, b.b_translator, b.b_publisher, b.b_bc_code, 
		b.b_price, b.b_image, b.b_page, b.b_publish_date, b.b_content, b.b_readcount, 
        bc.bc_code, bc.bc_name, bc.bc_code_ref_md, bc.bc_code_ref_mn 
FROM jspbookshop.book b 
JOIN booksubcatgy bsc ON b.b_id = bsc.bsc_b_id
JOIN bookcatgycode bc ON bsc.bsc_bc_code = bc.bc_code
WHERE b.b_id = 128;

 

▼ 출력 결과


우선 b_id가 128인 도서의 book 테이블 정보만 뽑아보자.

해당 도서의 메인 분류 코드는 101101이다.

book 테이블

 

이젠 bsc_b_id가 128인 도서의 booksubcatgy 테이블 정보를 뽑아보자.

해당 도서의 서브 분류 코드는 2개로 각각 101107, 100109이다.

booksubcatgy 테이블

 

아래는 3개의 테이블을 조인한 결과이다.

서브 분류 코드의 대 / 중분류 코드가 잘 출력되고 있다.

3개의 테이블 조인


 

 생각 2. 의 쿼리문 : bookcatgycode 테이블을 2번 셀프 조인한다.

 

아래의 쿼리문으로 1번 셀프 조인해 보자.

 

SELECT s.bc_code_ref_mn "대분류 코드", 
       md.bc_code "중분류 코드", md.bc_name "중분류 코드명", 
       s.bc_code "소분류 코드", s.bc_name "소분류 코드명"
FROM bookcatgycode s 
JOIN bookcatgycode md ON s.bc_code_ref_md = md.bc_code;

 

▼ 출력 결과


중분류 코드명을 가져오기 위한 셀프조인을 해보았더니, 중분류 코드명은 제대로 잘 가져와지나 대분류 코드명은 가져올 수 없다.

깊이가 2번 있는 계층형 구조이므로 셀프 조인도 2번 해준다.

1번 셀프조인 결과


 

수정한 2번 셀프 조인 쿼리문이다.

 

SELECT mn.bc_code "대분류 코드", mn.bc_name "대분류 코드명", 
       md.bc_code "중분류 코드", md.bc_name "중분류 코드명", 
       s.bc_code "소분류 코드", s.bc_name "소분류 코드명"
FROM bookcatgycode s 
JOIN bookcatgycode md ON s.bc_code_ref_md = md.bc_code
JOIN bookcatgycode mn ON s.bc_code_ref_mn = mn.bc_code;

 

▼ 출력 결과


대분류 코드명 및 중분류 코드명이 잘 가져와진다.

2번 셀프조인 결과


 

3. 쿼리문 합치기

위의 두 쿼리문을 합쳐보자.

아래 쿼리문은 도서 아이디와 해당 도서의 서브 분류 코드를 가지고 도서 정보 및 서브 분류 코드에 해당하는 대 / 중 / 소분류 코드명을 조회한다.

 

SELECT 
	b.b_id, b.b_name, b.b_writer, b.b_translator, b.b_publisher, b.b_bc_code "메인분류 코드", 
	b.b_price, b.b_image, b.b_page, b.b_publish_date, b.b_content, b.b_readcount,
	s.bc_code "소분류 코드", s.bc_name "소분류 코드명",
    	md.bc_code "중분류 코드", md.bc_name "중분류 코드명", 
	mn.bc_code "대분류 코드", mn.bc_name "대분류 코드명"
FROM jspbookshop.book b
JOIN booksubcatgy bsc ON b.b_id = bsc.bsc_b_id 
JOIN bookcatgycode s ON bsc.bsc_bc_code = s.bc_code
JOIN bookcatgycode md ON s.bc_code_ref_md = md.bc_code
JOIN bookcatgycode mn ON s.bc_code_ref_mn = mn.bc_code
WHERE b.b_id = 128 
AND bsc.bsc_bc_code = 101107;

 

▼ 출력 결과


 

빨간 박스가 book 테이블 데이터,

파랑 박스가 bookcatgycode 테이블의 소분류 데이터,

주황 박스bookcatgycode 테이블의 중분류 데이터,

보라색 박스bookcatgycode 테이블의 대분류 데이터이다.

최종 쿼리문 결과


 

위의 최종 쿼리문을 사용할 것이다.


 

3. DAO 클래스(BookDAO.java)

커넥션 및 리소스를 이용해 DB와 직접적으로 데이터를 처리하는 클래스이다.

 

☞ 처리 흐름

  1. 해당 도서 아이디에 따른 서브 분류가 존재하는지 확인한다.
  2. 만약 서브 분류가 존재한다면 서브 분류 개수만큼 루프를 돌면서 도서 정보 및 서브 분류 정보를 가져온다.
  3. 만약 서브 분류가 존재하지 않는다면 도서 정보만 가져온다.

여기서 서브 분류가 존재하지 않을 때 도서 정보(book 테이블)만 가져오는 이유에 대해선 나중에 도서 상세 페이지 JS 코드에서 설명하도록 하겠다.

 

☞ 변수 설명

List<Integer> bookSubCatgyList

  • 도서 한 권의 서브 분류 코드를 List 타입으로 저장하는 객체이다.
  • 서브 분류 코드가 int형으로 저장되므로 wrapper Integer로 설정해 주었다. 

List<BookCatgyCode> bookCatgyList

  • 도서 한 권의 서브 분류의 정보를 List 타입으로 저장하는 객체이다.
  • 서브 분류의 정보와 함께 대 / 중분류 코드명을 저장할 것이므로 BookCatgyCode 클래스 타입으로 설정해 주었다.

 

3-1. 서브 분류 존재 확인

첫 단계, 서브 분류가 존재하는지 확인하는 try-catch문을 작성해 보자.

자원 객체를 중첩으로 사용할 것이므로 에러 추적을 위해 catch문을 넣어주었다.

 

PrepareStatement 자원과 ResultSet 자원으로 DB 처리 과정에서 에러를 만나지 않는다면 while문을 처리한 후 아래 if-else문을 처리하게 된다.

 

Book book = null;
BookCatgyCode bookCatgy = null;		
List<Integer> bookSubCatgyList = null;
List<BookCatgyCode> bookCatgyList = null;
String sql = "SELECT bsc_bc_code FROM jspbookshop.booksubcatgy WHERE bsc_b_id = ?";
		
try(PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setInt(1, b_id);
    
    try(ResultSet rs = pstmt.executeQuery()) {
    	book = new Book();
    	bookSubCatgyList = new ArrayList<>();
        while(rs.next()) {
            // 조회 레코드의 첫 번째 컬럼 값을 List로 저장
            bookSubCatgyList.add(rs.getInt(1));
        }
    } catch (Exception e) {
        System.err.println(" B.DAO : selectBook() rs 처리 도중 에러 : "+e);
    } 
    
    if(!bookSubCatgyList.isEmpty()) {
	// 해당 도서의 서브분류가 있는 경우
    
    } else {
    	// 해당 도서의 서브분류가 없는 경우
    }
} catch (SQLException e) {
    System.err.println(" B.DAO : selectBook() pstmt 처리 중 에러 : "+e);
}

 

3-2. 서브 분류가 있는 경우

두 번째 단계, 서브 분류가 존재하는 경우에 대해 작성해 보자. 

서브 분류가 있는 경우, 모든 데이터를 가져와야 한다. 아래 코드를 if문 안에 작성한다.

 

☞ for(int sub_code:bookSubCatgyList) { }

List형 저장되어 있는 객체를 루프를 돌면서 하나씩 접근해 모든 데이터를 가져온다. 

 

 bookCatgy = new BookCatgyCode(rs2.getInt(13), rs2.getString(14), rs2.getInt(15), rs2.getInt(17), rs2.getString(16),rs2.getString(18));

대 / 중 / 소분류 코드 및 코드명을 BookCatgyCode 타입의 변수에 저장한다.

BookCatgyCode() 생성자의 인자 순서에 맞게 칼럼 인덱스를 설정해 준다.

 

☞ bookCatgyList.add(bookCatgy);

row마다 저장된 bookCatgy 변수를 bookCatgyList에 저장한다.

 

☞ book.setBookSubCatgyList(bookCatgyList);

for문을 다 돈 후, bookCatgyList를 book 객체의 전역변수 값으로 설정한다.

 

bookCatgyList = new ArrayList<>();
sql = "SELECT "
        + "b.b_id, b.b_name, b.b_writer, b.b_translator, b.b_publisher, b.b_bc_code, "
        + "b.b_price, b.b_image, b.b_page, b.b_publish_date, b.b_content, b.b_readcount, "
        + "s.bc_code, s.bc_name, "
        + "md.bc_code, md.bc_name, "
        + "mn.bc_code, mn.bc_name "
    + "FROM jspbookshop.book b "
    + "JOIN jspbookshop.booksubcatgy bsc ON b.b_id = bsc.bsc_b_id "
    + "JOIN jspbookshop.bookcatgycode s ON bsc.bsc_bc_code = s.bc_code "
    + "JOIN jspbookshop.bookcatgycode md ON s.bc_code_ref_md = md.bc_code "
    + "JOIN jspbookshop.bookcatgycode mn ON s.bc_code_ref_mn = mn.bc_code "
    + "WHERE b.b_id = ? "
    + "AND bsc.bsc_bc_code = ?";

try(PreparedStatement pstmt2 = conn.prepareStatement(sql)) {
    pstmt2.setInt(1, b_id);

    for(int sub_code:bookSubCatgyList) {
        pstmt2.setInt(2, sub_code);
        try(ResultSet rs2 = pstmt2.executeQuery()) {
            if(rs2.next()) {
                book.setB_id(rs2.getInt(1));
                book.setB_name(rs2.getString(2));
                book.setB_writer(rs2.getString(3));
                book.setB_translator(rs2.getString(4));
                book.setB_publisher(rs2.getString(5));
                book.setB_bc_code(rs2.getInt(6));
                book.setB_price(rs2.getInt(7));
                book.setB_image(rs2.getString(8));
                book.setB_page(rs2.getInt(9));
                book.setB_publish_date(rs2.getString(10));
                book.setB_content(rs2.getString(11));
                book.setB_readcount(rs2.getInt(12));
                bookCatgy = new BookCatgyCode(rs2.getInt(13), 
                                              rs2.getString(14), 
                                              rs2.getInt(15), 
                                              rs2.getInt(17), 
                                              rs2.getString(16),
                                              rs2.getString(18));
            }
            bookCatgyList.add(bookCatgy);
        } 
    }
    book.setBookSubCatgyList(bookCatgyList);
} catch (Exception e) {
    System.err.println(" B.DAO : selectBook() pstmt2 처리 도중 에러 : "+e);
}

 

3-3. 서브 분류가 없는 경우

세 번째 단계, 서브 분류가 없는 경우에 대해 작성해 보자. 

서브 분류가 없는 경우, 도서 정보만 가져온다. 아래 코드를 else문 안에 작성한다.

 

sql = "SELECT * FROM jspbookshop.book WHERE b_id = ? ";
try(PreparedStatement pstmt2 = conn.prepareStatement(sql)) {
    pstmt2.setInt(1, b_id);
    try(ResultSet rs2 = pstmt2.executeQuery()) {
        if(rs2.next()) {
            book = new Book(rs2.getInt(1), 
                    rs2.getString(2), 
                    rs2.getString(3), 
                    rs2.getString(4), 
                    rs2.getString(5), 
                    rs2.getInt(6), 
                    rs2.getInt(7), 
                    rs2.getString(8), 
                    rs2.getInt(9), 
                    rs2.getString(10), 
                    rs2.getString(11), 
                    rs2.getInt(12),
                    null);
        }
    }
} catch (Exception e) {
    System.err.println(" B.DAO : selectBook() pstmt2 처리 도중 에러 : "+e);
}

 

▼ 전체 코드

/** B_ID에 따른 도서 정보를 구하는 메서드 */
public Book selectBook(int b_id) {

    Book book = null;
    BookCatgyCode bookCatgy = null;		
    List<Integer> bookSubCatgyList = null;
    List<BookCatgyCode> bookCatgyList = null;
    String sql = "SELECT bsc_bc_code FROM jspbookshop.booksubcatgy WHERE bsc_b_id = ?";

    try(PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setInt(1, b_id);

        try(ResultSet rs = pstmt.executeQuery()) {
            book = new Book();
            bookSubCatgyList = new ArrayList<>();

            while(rs.next()) {
                bookSubCatgyList.add(rs.getInt(1));
            }
        } catch (Exception e) {
            System.err.println(" B.DAO : selectBook() rs 처리 도중 에러 : "+e);
        }

        if(!bookSubCatgyList.isEmpty()) {
            // 해당 도서의 서브분류 코드가 있는 경우
            bookCatgyList = new ArrayList<>();
            sql = "SELECT "
                    + "b.b_id, b.b_name, b.b_writer, b.b_translator, b.b_publisher, b.b_bc_code, "
                    + "b.b_price, b.b_image, b.b_page, b.b_publish_date, b.b_content, b.b_readcount, "
                    + "s.bc_code, s.bc_name, "
                    + "md.bc_code, md.bc_name, "
                    + "mn.bc_code, mn.bc_name "
                + "FROM jspbookshop.book b "
                + "JOIN jspbookshop.booksubcatgy bsc ON b.b_id = bsc.bsc_b_id "
                + "JOIN jspbookshop.bookcatgycode s ON bsc.bsc_bc_code = s.bc_code "
                + "JOIN jspbookshop.bookcatgycode md ON s.bc_code_ref_md = md.bc_code "
                + "JOIN jspbookshop.bookcatgycode mn ON s.bc_code_ref_mn = mn.bc_code "
                + "WHERE b.b_id = ? "
                + "AND bsc.bsc_bc_code = ?";

            try(PreparedStatement pstmt2 = conn.prepareStatement(sql)) {
                pstmt2.setInt(1, b_id);

                for(int sub_code:bookSubCatgyList) {
                    pstmt2.setInt(2, sub_code);
                    try(ResultSet rs2 = pstmt2.executeQuery()) {
                        if(rs2.next()) {
                            book.setB_id(rs2.getInt(1));
                            book.setB_name(rs2.getString(2));
                            book.setB_writer(rs2.getString(3));
                            book.setB_translator(rs2.getString(4));
                            book.setB_publisher(rs2.getString(5));
                            book.setB_bc_code(rs2.getInt(6));
                            book.setB_price(rs2.getInt(7));
                            book.setB_image(rs2.getString(8));
                            book.setB_page(rs2.getInt(9));
                            book.setB_publish_date(rs2.getString(10));
                            book.setB_content(rs2.getString(11));
                            book.setB_readcount(rs2.getInt(12));
                            bookCatgy = new BookCatgyCode(rs2.getInt(13), 
                                                          rs2.getString(14), 
                                                          rs2.getInt(15), 
                                                          rs2.getInt(17), 
                                                          rs2.getString(16),
                                                          rs2.getString(18));
                        }
                        bookCatgyList.add(bookCatgy);
                    } 
                }
                book.setBookSubCatgyList(bookCatgyList);
            } catch (Exception e) {
                System.err.println(" B.DAO : selectBook() pstmt2 처리 도중 에러 : "+e);
            }
        } else {
            sql = "SELECT * FROM jspbookshop.book WHERE b_id = ? ";
            try(PreparedStatement pstmt2 = conn.prepareStatement(sql)) {
                pstmt2.setInt(1, b_id);
                try(ResultSet rs2 = pstmt2.executeQuery()) {
                    if(rs2.next()) {
                        book = new Book(rs2.getInt(1), 
                                rs2.getString(2), 
                                rs2.getString(3), 
                                rs2.getString(4), 
                                rs2.getString(5), 
                                rs2.getInt(6), 
                                rs2.getInt(7), 
                                rs2.getString(8), 
                                rs2.getInt(9), 
                                rs2.getString(10), 
                                rs2.getString(11), 
                                rs2.getInt(12),
                                null);
                    }
                }
            } catch (Exception e) {
                System.err.println(" B.DAO : selectBook() pstmt2 처리 도중 에러 : "+e);
            }
        } 
    } catch (SQLException e) {
        System.err.println(" B.DAO : selectBook() pstmt 처리 중 에러 : "+e);
    }

    return book;
}

 

 

서버 딴 확인

클라 딴(도서 상세 페이지) 구현은 일단 두고, 서버 딴에서 데이터가 잘 조회되는지 서버 콘솔로 확인해 보자.

경우 1. 서브 분류가 있는 경우

서브 분류 있는 경우

 

경우 2. 서브 분류가 없는 경우

서브 분류가 없는 경우, 변수 bookSubCatgyList은 null이다.

서브 분류가 없는 경우

 

 


 

 

 

 

도서 상세 페이지에서 해당 도서의 모든 정보를 가져오기 위한 서버 딴 로직이 완성되었다.

개인적으로 해당 프로젝트의 제일 중요한 부분이라고 생각한다.

머리 아프다 에휴

 

다음 포스팅에서는 마지막 부분, 클라 딴 로직을 구현해 보도록 하겠다.

 

 

끝.