Spring & Springboot/올인원 스프링 프레임워크

전자 도서관 프로젝트 - 도서 대출 기능 구현

YJ_ma 2023. 12. 3. 18:38

도서 대출 처리하기

도서 상세 화면에서 <도서 대출> 버튼을 클릭하면 도서 대출 페이지로 넘어갈 수 있다. 다음은 도서 상세 화면(book_detail.jsp)의 도서 대출 코드이다.

<c:choose>
    <c:when test="${bookVo.b_rantal_able eq 0}">
        <a href="#none">대출중</a>
    </c:when>
    <c:when test="${bookVo.b_rantal_able eq 1}">
        <c:url value='/book/user/rentalBookConfirm' var='rental_url'>
            <c:param name='b_no' value='${bookVo.b_no}'/>
        </c:url>
        <a class="rental_book_button" href="${rental_url}">도서 대출</a>
    </c:when>
</c:choose>

 

<도서 대출>을 클릭하면 /book/user/rentalBookConfirm 요청이 발생하는데, 이때 매개변수로 도서를 구분할 수 있는 b_no와 함께 서버에 전달한다.

 

컨트롤러 기능 구현

클라이언트 요청을 처리하는 rentalBookConfirm()을 BookController에 다음과 같이 코딩한다.

// 도서 대출
@GetMapping("/rentalBookConfirm")
public String rentalBookConfirm(@RequestParam("b_no") int b_no, HttpSession session) {
    System.out.println("[UserBookController] rentalBookConfirm()");

    String nextPage = "user/book/rental_book_ok";

    UserMemberVo loginedUserMemberVo = (UserMemberVo) session.getAttribute("loginedUserMemberVo");

    if (loginedUserMemberVo == null)
        return "redirect:/user/member/loginForm";

    int result = bookService.rentalBookConfirm(b_no, loginedUserMemberVo.getU_m_no());

    if (result <= 0)
        nextPage = "user/book/rental_book_ng";

    return nextPage;
}

 

서비스 기능 구현

컨트롤러에서 호출하는 rentalBookConfirm()을 서비스(BookService)에 선언한다.

public int rentalBookConfirm(int b_no, int u_m_no) {
    System.out.println("[BookService] rentalBookConfirm()");

    int result = bookDao.insertRentalBook(b_no, u_m_no);

    if (result >= 0)
        bookDao.updateRentalBookAble(b_no);

    return result;
}

 

도서 대출 이력 테이블 생성

도서 대출 정보를 관리하는 tbl_rental_book 테이블 명세서를 참고해서 데이터베이스를 생성한다.

CREATE TABLE tbl_rental_book(
    rb_no            INT AUTO_INCREMENT,
    b_no             INT,
    u_m_no           INT,
    rb_start_date    DATETIME,
    rb_end_date      DATETIME DEFAULT '1000-01-01',
    rb_reg_date      DATETIME,
    rb_mod_date      DATETIME,
    PRIMARY KEY(rb_no)
);

 

VO 구현

com.office.library.book 패키지에 tbl_rental_book 테이블에 대한 VO를 RentalBookVo 이름으로 생성한다.

RentalBookVo는 tbl_rental_book 테이블의 데이터를 저장할 수 있는 클래스이다.

tbl_rental_book에는 대출 정보뿐만 아니라 도서 정보, 회원 정보가 조인될 수 있다.

따라서 모든 멤버 필드를 선언하고 @Getter와 @Setter를 이용해서 getter, setter 메서드를 생성한다.

package com.office.library.book;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RentalBookVo {

	int rb_no;
	String rb_start_date;
	String rb_end_date;
	String rb_reg_date;
	String rb_mod_date;

	int b_no;
	String b_thumbnail;
	String b_name;
	String b_author;
	String b_publisher;
	String b_publish_year;
	String b_isbn;
	String b_call_number;
	int b_rantal_able;
	String b_reg_date;
	String b_mod_date;

	int u_m_no;
	String u_m_id;
	String u_m_pw;
	String u_m_name;
	String u_m_gender;
	String u_m_mail;
	String u_m_phone;
	String u_m_reg_date;
	String u_m_mod_date;

}

 

DAO 기능 구현

도서 대출 정보를 추가하기 위해서 insertRentalBook()을 BookDao에 선언한다. 그리고 대출된 도서를 다른 사용자가 대출 할 수 없도록 대출 도서의 대출 가능 컬럼 값을 0으로 변경하기 위한 updateRentalBookAble()도 선언한다.

public int insertRentalBook(int b_no, int u_m_no) {
    System.out.println("[BookDao] insertRentalBook()");

    String sql = "INSERT INTO tbl_rental_book(b_no, u_m_no, rb_start_date, rb_reg_date, rb_mod_date) "
            + "VALUES(?, ?, NOW(), NOW(), NOW())";

    int result = -1;

    try {
        result = jdbcTemplate.update(sql, b_no, u_m_no);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}

public void updateRentalBookAble(int b_no) {
    System.out.println("[BookDao] updateRentalBookAble()");

    String sql = "UPDATE tbl_book "
            + "SET b_rantal_able = 0 "
            + "WHERE b_no = ?";

    try {
        jdbcTemplate.update(sql, b_no);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

이제 프로젝트를 실행하고 원하는 도서를 검색해서 대출해본다. 우선 'user1'계정으로 로그인하고 '난생처음 컴퓨팅 사고 with 파이썬' 도서를 검색해 도서 상세 화면으로 이동한다. 상세 화면에서 <도서 대출> 버튼을 클릭해 대출을 시도한다. 만약 도서 대출이 성공하면 RENTAL BOOK SUCCESS!! 창이 뜬다. 이후 도서 대출을 성공하면 대출한 도서를 다시 조회했을 때 대출 가능에 X 표시가 되어있는지 확인한다.

 

tbl_rental_book 테이블에 대출 정보가 추가된 것을 확인할 수 있다.

 

마지막으로 tbl_book 테이블에 대출 도서의 b_rental_able 컬럼 값이 0(대출 불가)으로 업데이트된 것을 확인할 수 있다.

 

 

인터셉터

도서를 대출 했으니 다음으로 대출 목록 조회 기능을 구현해본다. 

 

HandlerInterceptor 인터페이스

도서를 대출하기 위해서는 BookController의 rentalBookConfirm()을 이용했다. rentalBookConfirm()에서는 session을 이용해서 만약 사용자가 로그인하지 않았다면 로그인 화면으로 리다이렉트 한다.

// 도서 대출
@GetMapping("/rentalBookConfirm")
public String rentalBookConfirm(@RequestParam("b_no") int b_no, HttpSession session) {
    System.out.println("[UserBookController] rentalBookConfirm()");

    String nextPage = "user/book/rental_book_ok";

    UserMemberVo loginedUserMemberVo = (UserMemberVo) session.getAttribute("loginedUserMemberVo");

    if (loginedUserMemberVo == null)
        return "redirect:/user/member/loginForm";

   ..생략..
}

 

리다이렉트해야할 상황이 많아져 코드 여기저기에 redirect 관련 코드를 삽입하면 동일한 코드의 반복과 코드 유지 보수 효율성이 떨어질 수 있다. 

ex, 사용자의 계정 수정 및 로그아웃, 도서 대출 및 목록 조회 페이지 등과 같은 웹 서비스의 경우 회원인증(로그인)이 안 된 상태로 클라이언트가 해당 페이지에 접근(요청)을 시도하면 서버는 로그인 또는 회원가입 등의 페이지로 유도(응답)해야 한다.

이것을 처리하기 위해 컨트롤러 각각에 세션과 리다이렉트 관련 코드를 삽입해야 하는 불편함이 발생하게 된다. 또한 프로그램 유지 보수할 때 모든 컨트롤러를 살펴봐야 하는 불편함이 따른다.

 

이러한 점을 보완하기 위해 스프링 MVC에서는 HandlerInterceptor 인터페이스를 제공하고 있다.

HandlerInterceptor 인터페이스는 3개의 메서드를 선언하고 있다.

메서드 기능
preHandle() 클라이언트의 요청이 컨트롤러에 전달하기 전에 호출되고, boolean을 반환한다.
매개변수로 handler(컨트롤러)를 받으며 true를 반환하면 handler가 실행되고, 
false를 반환하면 handler는 실행되지 않는다.
postHandle() 클라이언트 요청이 컨트롤러에서 실행된 후 호출되고, 이때 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
afterCompletion() 클라이언트 요청이 컨트롤러에서 실행되고, 뷰를 통해서 응답이 완료된 후 호출된다. 만약 뷰를 생성할 때 예외가 발생해도 afterCompletion은 호출된다.

 

· 일반적으로 많이 사용되는 preHandle()은 회원인증(로그인) 상태를 구분한 후 회원인증이 완료된 사용자한테만 웹 서비스를 제공하는 등의 작업을 한다.

즉, 회원인증이 안된 사용자한테는 handler(컨트롤러)의 실행을 막고, preHandle() 내부에서 다른 페이지로 유도하는 등의 작업을 한다.

· postHandle() handler가 실행되고 뷰가 실행되기 전에 호출된다.

· afterCompletion()handler 및 뷰의 실행이 완료된 후 호출되는 메서드이다.

매개변수로 받는 Exception 객체를 이용해서 뷰의 예외를 받는다. 뷰에서 예외가 발생하지 않으면 null이 전달된다.

 

HandlerInterceptor 사용하기

HandlerInterceptor를 사용하기 위해서는 클래스에서 HandlerInterceptor 인터페이스를 구현해야 한다.

스프링에서는 HandlerInterceptor를 구현한 HandlerInterceptorAdaptor 클래스를 제공하고, 개발자는 HandlerInterceptorAdaptor를 상속해서 필요한 메서드만 재정의하면 된다.

 

이제 기존의 리다이렉트 대신 인터셉터 클래스를 생성해서 로그인 인증 처리를 해본다.

우선 com.office.library.user.member에서 UserMemberLoginInterceptor 클래스를 생성하고 HandlerInterceptorAdaptor 클래스를 상속하도록 설정한다.

 

UserMemberLoginInterceptor를 다음과 같이 코딩한다.

· UserMemberLoginInterceptor는 인터셉터 기능을 구현하기 위해서 HandlerInterceptor를 구현한 HandlerInterceptorAdaptor 클래스를 상속하고 있다.

package com.office.library.user.member;

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

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class UserMemberLoginInterceptor extends HandlerInterceptorAdapter {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{

		HttpSession session = request.getSession(false);
		if (session != null) {
			Object object = session.getAttribute("loginedUserMemberVo");
			if (object != null)
				return true;
		}

		response.sendRedirect(request.getContextPath() + "/user/member/loginForm");

		return false;
	}
}

 

인터셉터 클래스를 생성했으니 인터셉터 클래스가 사용자의 어떤 요청 시에 작동해야 하는지 언터셉터를 설정해야한다.

인터셉터 설정이란?

: 사용자의 특정 요청이 발생했을 때 인터셉터 클래스(UserMemberLoginInterceptor)가 작동하도록 설정하는 것이다.

servlet-context.xml에 <interceptors>를 다음과 같이 추가한다.

<interceptors>
    <interceptor>
        <mapping path="/book/user/rentalBookConfirm"/>
        <beans:bean class="com.office.library.user.member.UserMemberLoginInterceptor"/>
    </interceptor>
</interceptors>

 

· 인터셉터를 설정하기 위해서는 <interceptors>태그를 사용한다.

· <interceptor> 태그는 인터셉터 항목을 설정한다.

· <mapping>을 이용해서 사용자의 요청을 매핑하고 있는데, 여기서는 사용자의 요청이 /book/user/rentalBookConfirm인 경우 인터셉터 클래스가 동작한다.

· 사용자의 요청을 매핑했으면 다음으로 인터셉터에 사용되는 인터셉터 클래스를 설정해야한다. <beans:bean>을 통해 전체 이름을 지정한다.

 

로그인에 대한 인터셉터가 모두 구현됐으므로 BookController의 rentalBookConfirm()에서 더 이상 로그인 상태를 확인할 필요가 없다. 따라서 다음과 같이 수정한다.

// 도서 대출
@GetMapping("/rentalBookConfirm")
public String rentalBookConfirm(@RequestParam("b_no") int b_no, HttpSession session) {
    System.out.println("[UserBookController] rentalBookConfirm()");

    String nextPage = "user/book/rental_book_ok";

    UserMemberVo loginedUserMemberVo = (UserMemberVo) session.getAttribute("loginedUserMemberVo");

//		if (loginedUserMemberVo == null)
//			return "redirect:/user/member/loginForm";

    int result = bookService.rentalBookConfirm(b_no, loginedUserMemberVo.getU_m_no());

    if (result <= 0)
        nextPage = "user/book/rental_book_ng";

    return nextPage;
}

  

프로젝트를 재시작한 후 로그인하지 않고 도서를 대출했을 때 로그인 화면으로 이동하는지 확인한다.

 

 

도서 대출 외에도 회원 정보 수정도 인터셉터의 대상이다. 따라서 servlet-context.xml에 추가하고, 메서드에서 로그인을 확인하는 코드도 제거한다.

<interceptors>
    <interceptor>
        <mapping path="/book/user/rentalBookConfirm"/>
        <mapping path="/user/member/modifyAccountForm"/>
        <mapping path="/user/member/modifyAccountConfirm"/>
        <beans:bean class="com.office.library.user.member.UserMemberLoginInterceptor"/>
    </interceptor>
</interceptors>
// 회원 정보 수정
@GetMapping("/modifyAccountForm")
public String modifyAccountForm(HttpSession session) {
    System.out.println("[UserMemberController] modifyAccountForm()");

    String nextPage = "user/member/modify_account_form";

//		UserMemberVo loginedUserMemberVo = (UserMemberVo) session.getAttribute("loginedUserMemberVo");
//
//		if (loginedUserMemberVo == null)
//			nextPage = "redirect:/user/member/loginForm";

    return nextPage;
}

 

브라우저에 http://localhost:8090/library/user/member/modifyAccountForm을 입력해서 로그인하지 않고 회원 정보 수정을 요청했을 때 로그인 화면으로 이동하는지 확인한다. 

 

대출 중인 도서 목록 확인하기

'나의 책장' 메뉴는 nav.jsp에 있다. 다음은 nav.jsp에서 '나의책장' 메뉴에 해당하는 링크이다.

<li><a href="<c:url value='/book/user/enterBookshelf' />">나의책장</a></li>

 

 

컨트롤러 기능 구현

클라이언트의 요청을 처리할 수 있는 메서드를 BookController.java에 선언한다.

// 나의 책장
@GetMapping("/enterBookshelf")
public String enterBookshelf(HttpSession session, Model model) {
    System.out.println("[UserBookController] enterBookshelf()");

    String nextPage = "user/book/bookshelf";

    UserMemberVo loginedUserMemberVo = (UserMemberVo) session.getAttribute("loginedUserMemberVo");

    List<RentalBookVo> rentalBookVos = bookService.enterBookshelf(loginedUserMemberVo.getU_m_no());

    model.addAttribute("rentalBookVos", rentalBookVos);

    return nextPage;
}

 

서비스 기능 구현

컨트롤러에서 호출하는 enterBookshelf()를 서비스(BookService)에 선언한다.

public List<RentalBookVo> enterBookshelf(int u_m_no){
    System.out.println("[BookService] enterBookshelf()");

    return bookDao.selectRentalBooks(u_m_no);
}

 

DAO 기능 구현

데이터베이스에서 대출 도서를 검색하는 selectRentalBooks()를 BookDao에 선언한다.

public List<RentalBookVo> selectRentalBooks(int u_m_no){
    System.out.println("[BookDao] selectRentalBooks()");

    String sql = "SELECT * FROM tbl_rental_book rb "
            + "JOIN tbl_book b "
            + "ON rb.b_no = b.b_no "
            + "JOIN tbl_user_member um "
            + "ON rb.u_m_no = um.u_m_no "
            + "WHERE rb.u_m_no = ? AND rb.rb_end_date = '1000-01-01'";

    List<RentalBookVo> rentalBookVos = new ArrayList<RentalBookVo>();

    try {
        rentalBookVos = jdbcTemplate.query(sql, new RowMapper<RentalBookVo>() {
            @Override
            public RentalBookVo mapRow(ResultSet rs, int rowNum) throws SQLException {
                RentalBookVo rentalBookVo = new RentalBookVo();

                rentalBookVo.setRb_no(rs.getInt("rb_no"));
                rentalBookVo.setB_no(rs.getInt("b_no"));
                rentalBookVo.setU_m_no(rs.getInt("u_m_no"));
                rentalBookVo.setRb_start_date(rs.getString("rb_start_date"));
                rentalBookVo.setRb_end_date(rs.getString("rb_end_date"));
                rentalBookVo.setRb_reg_date(rs.getString("rb_reg_date"));
                rentalBookVo.setRb_mod_date(rs.getString("rb_mod_date"));

                rentalBookVo.setB_thumbnail(rs.getString("b_thumbnail"));
                rentalBookVo.setB_name(rs.getString("b_name"));
                rentalBookVo.setB_author(rs.getString("b_author"));
                rentalBookVo.setB_publisher(rs.getString("b_publisher"));
                rentalBookVo.setB_publish_year(rs.getString("b_publish_year"));
                rentalBookVo.setB_isbn(rs.getString("b_isbn"));
                rentalBookVo.setB_call_number(rs.getString("b_call_number"));
                rentalBookVo.setB_rantal_able(rs.getInt("b_rantal_able"));
                rentalBookVo.setB_reg_date(rs.getString("b_reg_date"));

                rentalBookVo.setU_m_id(rs.getString("u_m_id"));
                rentalBookVo.setU_m_pw(rs.getString("u_m_pw"));
                rentalBookVo.setU_m_name(rs.getString("u_m_name"));
                rentalBookVo.setU_m_gender(rs.getString("u_m_gender"));
                rentalBookVo.setU_m_mail(rs.getString("u_m_mail"));
                rentalBookVo.setU_m_phone(rs.getString("u_m_phone"));
                rentalBookVo.setU_m_reg_date(rs.getString("u_m_reg_date"));
                rentalBookVo.setU_m_mod_date(rs.getString("u_m_mod_date"));

                return rentalBookVo;
            }
        }, u_m_no);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return rentalBookVos;
}

 

인터셉터 추가

'나의책장'은 로그인 상태에서 이용할 수 있다. 따라서 servlet-context.xml에 인터셉터에 추가한다.

<interceptors>
    <interceptor>
        <mapping path="/book/user/rentalBookConfirm"/>
        <mapping path="/book/user/enterBookshelf"/>
        <mapping path="/user/member/modifyAccountForm"/>
        <mapping path="/user/member/modifyAccountConfirm"/>
        <beans:bean class="com.office.library.user.member.UserMemberLoginInterceptor"/>
    </interceptor>
</interceptors>