57일차(2)/Spring(21) : Transaction, DataAccessException 활용 예제
- table.sql에 새 테이블 생성한 내용추가
-- 상품테이블
CREATE TABLE shop(
num NUMBER PRIMARY KEY, --상품번호
name VARCHAR2(30), --상품이름
price NUMBER, --상품가격
remainCount NUMBER CHECK(remainCount >= 0) --재고갯수
);
-- 고객 계좌 테이블
CREATE TABLE client_account(
id VARCHAR2(30) PRIMARY KEY, -- 고객의 아이디
money NUMBER CHECK(money >= 0), -- 고객의 잔고
point NUMBER
);
-- 주문 테이블
CREATE TABLE client_order(
num NUMBER PRIMARY KEY, -- 주문번호
id VARCHAR2(30), -- 주문 고객의 아이디
code NUMBER, -- 주문한 상품의 번호
addr VARCHAR2(50) -- 배송 주소
);
-- 주문 테이블에 사용할 시퀀스
CREATE SEQUENCE client_order_seq;
-- sample 데이터
INSERT INTO shop (num,name,price,remainCount)
VALUES(1, '사과', 1000, 5);
INSERT INTO shop (num,name,price,remainCount)
VALUES(2, '바나나', 2000, 5);
INSERT INTO shop (num,name,price,remainCount)
VALUES(3, '귤', 3000, 5);
- transaction 예제
- 하나의 비즈니스 로직을 처리하기 위해서 여러 개의 변경사항이 일어날 수 있다.
- 상품구입, 상품재고관리, 계좌 연동, 배송 처리 등
- 쇼핑몰이라고 하면, 하나의 구매만으로도 여러개의 테이블에 변경사항이 일어날 수 있다.
- 이러한 과정 속에서 exception이 일어날 수 있다.
- 이런 경우, 작업하다가 예외가 발생하면 이전에 작업했던 것들을 모두 rollback 해야 한다.
- DB를 관리할 때 이런 transaction을 관리할 일이 많다.
- transaction 관리 자체는 어렵지 않다. 단 테스트하는 환경을 만드는것이 번거롭다.
- quantum DB에서 테이블을 이렇게 만들어두고, 샘플 데이터도 넣어주었다.
- com.sy.spring04.shop.dto / dao / service / controller 4개의 패키지 생성
ShopDto
package com.sy.spring04.shop.dto;
public class ShopDto {
private int num; //상품번호
private String name; //상품명
private int price; //가격
private int remainCount; //재고개수
private String id; //주문자 아이디
public ShopDto() {}
public ShopDto(int num, String name, int price, int remainCount, String id) {
super();
this.num = num;
this.name = name;
this.price = price;
this.remainCount = remainCount;
this.id = id;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getRemainCount() {
return remainCount;
}
public void setRemainCount(int remainCount) {
this.remainCount = remainCount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
OrderDto
package com.sy.spring04.shop.dto;
public class OrderDto {
private int num; //주문번호
private String id; //주묹자의 아이디
private int code; //상품번호
private String addr; //배송 주소
public OrderDto() {}
public OrderDto(int num, String id, int code, String addr) {
super();
this.num = num;
this.id = id;
this.code = code;
this.addr = addr;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
}
ShopDao 인터페이스
package com.sy.spring04.shop.dao;
import java.util.List;
import com.sy.spring04.shop.dto.ShopDto;
public interface ShopDao {
//상품목록을 리턴해주는 메소드
public List<ShopDto> getList();
//상품 재고를 감소시키는 메소드
public void minusCount(int num);
//잔고 감소 시키는 메소드
public void minusMoney(ShopDto dto);
//포인트를 증가 시키는 메소드
public void plusPoint(ShopDto dto);
//상품의 가격을 리턴해주는 메소드
public int getPrice(int num);
}
OrderDao 인터페이스
package com.sy.spring04.shop.dao;
import com.sy.spring04.shop.dto.OrderDto;
public interface OrderDao {
//배송정보를 추가하는 메소드
public void addOrder(OrderDto dto);
}
ShopDaoImpl
package com.sy.spring04.shop.dao;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.sy.spring04.shop.dto.ShopDto;
@Repository
public class ShopDaoImpl implements ShopDao {
@Autowired
private SqlSession session;
@Override
public List<ShopDto> getList() {
return session.selectList("shop.getList");
}
//재고의 갯수를 1 줄이기
@Override
public void minusCount(int num) {
session.update("shop.minusCount", num);
}
//계좌 잔고 줄이기
@Override
public void minusMoney(ShopDto dto) {
session.update("shop.minusMoney", dto);
}
//상품 구입 가격의 10% 를 포인트로 적립하는 메소드
@Override
public void plusPoint(ShopDto dto) {
session.update("shop.plusPoint", dto);
}
//상품 번호에 해당하는 상품의 가격을 리턴하는 메소드
@Override
public int getPrice(int num) {
// TODO Auto-generated method stub
return session.selectOne("shop.getPrice", num);
}
}
- @Repository 와 @Autowired session 작성
OrderDaoImpl 클래스
package com.sy.spring04.shop.dao;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.sy.spring04.shop.dto.OrderDto;
@Repository
public class OrderDaoImpl implements OrderDao {
@Autowired
private SqlSession session;
//상품 주문 정보를 저장하는 메소드
@Override
public void addOrder(OrderDto dto) {
session.insert("shop.addOrder", dto);
}
}
configuration.xml에 Dto정보 추가
ShopMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="shop">
<select id="getList" resultType="shopDto">
SELECT num,name,price,remainCount
FROM shop
ORDER BY num ASC
</select>
<update id="minusCount" parameterType="int">
UPDATE shop
SET remainCount=remainCount-1
WHERE num=#{num}
</update>
<update id="minusMoney" parameterType="shopDto">
UPDATE client_account
SET money=money-#{price}
WHERE id=#{id}
</update>
<update id="plusPoint" parameterType="shopDto">
UPDATE client_account
SET point=point + #{price}*0.1
WHERE id=#{id}
</update>
<select id="getPrice" parameterType="int" resultType="int">
SELECT price
FROM shop
WHERE num=#{num}
</select>
<insert id="addOrder" parameterType="orderDto">
INSERT INTO client_order
(num, id, code, addr)
VALUES(client_order_seq.NEXTVAL, #{id}, #{code}, #{addr})
</insert>
</mapper>
- plusPoint : 산 가격의 10%를 포인트로 부여하는 것
- addOrder : 주문내역에 data를 추가하는 것
- 하나의 구입이 일어나면 아래의 과정이 일어난다고 하자.
1) 재고개수 줄이기,
2) 고객 계좌의 금액 감소,
3) 포인트 증가하기,
4) 주문정보 추가 (orderDao)
ShopService 인터페이스
package com.sy.spring04.shop.service;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
public interface ShopService {
//상품 목록을 ModelAndView 객체에 담아주는 메소드
public void getList(ModelAndView mView);
//상품 주문 처리를 하는 메소드
public void buy(HttpServletRequest request, ModelAndView mView);
}
ShopServiceImpl 클래스
package com.sy.spring04.shop.service;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.ModelAndView;
import com.sy.spring04.exception.DeliveryException;
import com.sy.spring04.shop.dao.OrderDao;
import com.sy.spring04.shop.dao.ShopDao;
import com.sy.spring04.shop.dto.OrderDto;
import com.sy.spring04.shop.dto.ShopDto;
@Service
public class ShopServiceImpl implements ShopService {
@Autowired private ShopDao shopDao;
@Autowired private OrderDao orderDao;
@Override
public void getList(ModelAndView mView) {
//상품 목록
List<ShopDto> list=shopDao.getList();
//ModelAndView객체에 list라는 키 값으로 담는다.
mView.addObject("list", list);
}
/*
* - Spring 트랜잭션 설정 방법
* 1. pom.xml에 spring-tx dependency 추가
* 2. servlet-context.xml에 transaction 설정 추가
* 3. 트랜잭션을 관리할 서비스의 메소드에 @Transactional 어노테이션 붙이기
*
* - 프로그래머의 의도 하에서 트랜잭션에 영향을 주는 Exception을 발생시키는 방법
*
* DataAccessException 클래스를 상속받은 클래스를 정의하고
* 예) class MyException extends DataAccessException{ }
*
* throw new MyException("예외 메시지");
*
* 예외를 발생시킬 조건이라면 위와 같이 예외를 발생시켜서
* 트랜잭션이 관리되도록 한다.
*/
@Transactional
@Override
public void buy(HttpServletRequest request, ModelAndView mView) {
//구입자의 아이디
String id=(String)request.getSession().getAttribute("id");
//1. parameter로 전달되는 구입할 상품 번호
int num=Integer.parseInt(request.getParameter("num"));
//2. 상품의 가격을 얻어온다.
int price=shopDao.getPrice(num);
//3. 상품의 가격만큼 계좌 잔액을 줄인다.
ShopDto dto=new ShopDto();
dto.setId(id);
dto.setPrice(price);
shopDao.minusMoney(dto);
//4. 가격의 10%를 포인트로 적립한다.
shopDao.plusPoint(dto);
//5. 재고의 개수를 1 줄인다.
shopDao.minusCount(num);
//6. 주문 테이블(배송)에 정보를 추가한다.
OrderDto dto2=new OrderDto();
dto2.setId(id); //누가
dto2.setCode(num); //어떤 상품을
//클라이언트가 입력한 배송 주소라고 가정
String addr="제주도 역삼동";
//가상의 테스트
if(addr.contains("제주도")) {
throw new DeliveryException("제주도는 배송 불가 지역입니다.");
}
dto2.setAddr(addr);//어디로 배송할지
orderDao.addOrder(dto2);
}
}
- 이 service 클래스는 2개의 dao에 의존한다.
home.jsp 링크하나 추가
컨트롤러 추가
ShopController
package com.sy.spring04.shop.controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.sy.spring04.shop.service.ShopService;
@Controller
public class ShopController {
@Autowired private ShopService service;
@RequestMapping("/shop/list")
public ModelAndView list(ModelAndView mView) {
service.getList(mView);
mView.setViewName("shop/list");
return mView;
}
@RequestMapping("/shop/buy")
public ModelAndView buy(HttpServletRequest request, ModelAndView mView) {
service.buy(request, mView);
mView.setViewName("shop/buy");
return mView;
}
}
shop/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">
<title>/views/shop/list.jsp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<div class="container">
<c:choose>
<c:when test="${empty id }">
<p>
<a href="${pageContext.request.contextPath }/users/loginform">로그인</a>
<a href="${pageContext.request.contextPath }/users/signup_form">회원가입</a>
</p>
</c:when>
<c:otherwise>
<p>
<strong>${id }</strong> 님 로그인중...
</p>
</c:otherwise>
</c:choose>
<h1>상품 목록 입니다.</h1>
<div class="row">
<c:forEach var="tmp" items="${list }">
<div class="col">
<div class="card">
<img class="card-img-top" src="${pageContext.request.contextPath }/resources/upload/apple.jpg"/>
<div class="card-body">
<h3 class="card-title">${tmp.name }</h3>
<p class="card-text">
가격 : <strong>${tmp.price }</strong>원 <br/>
재고 : <strong>${tmp.remainCount }</strong>개
</p>
<a href="buy?num=${tmp.num }" class="card-link">구입하기</a>
</div>
</div>
</div>
</c:forEach>
</div>
</div>
</body>
</html>
- /resources/upload 폴더에 샘플 image 넣어주기
- 현재까지 만든 구매 페이지의 모습
- transaction을 관리하면서 구입을 해보기!
servlet-context.xml 설정 필요!
- 이 xml의 문서 구조는 상단에 정의된 것만 나온다.(지금은 aop,tx를 추가한 상태)
- 추가를 하고싶으면 하단의 Namespaces 탭을 클릭해보면 된다.
- Namespaces 탭에서 aop, tx를 체크해서 추가해주면
다시 Source로 돌아와서 보면 문서가 확장되어 있다!
- 이 내용을 추가해주기
- 트랜잭션을 사용하려면 저 DataSourceTransactionManager가 있어야 한다.
- 위에 보면 dataSource 가 있는데, 그것이 저 ref 자리로 들어온다.
- 하단의 aop설정도 추가해주기!
- 이제 어노테이션 @transactional 만 붙이면 트랜잭션으로 사용할 수 있다.
- 트랜잭션이 관리되면서 구입이 이루어지도록 처리할 예정!
- id를 사용해서 클라이언트 계좌에 돈을 집어넣는다.
- 계좌테이블에 아래 샘플데이터 추가
-- sample 계좌 데이터
INSERT INTO client_account
(id, money, point)
VALUES('banana',10000, 0);
* 처리해야 할 업무
- 사과 1개 구입하기 를 누르면?
→ 잔고에서 1000원 줄이기
→ 포인트 100원 쌓기
→ 재고 1 감소
→ 배송테이블에 배송정보 추가
- 이 하나하나의 업무를 할 때 바로 commit하는 것이 아니다.(기존 자료는 auto commit이었다)
- 이 과정에서 하나가 잘못되면 바로 전부 다 rollback 해야 한다.
- 임시반영했다가 한번에 모두 커밋하는 작업이 필요하다.
ShopController 작성
- 새 메소드추가
shopServiceImpl
@Override
public void buy(HttpServletRequest request, ModelAndView mView) {
//구입자의 아이디
String id=(String)request.getSession().getAttribute("id");
//1. parameter로 전달되는 구입할 상품 번호
int num=Integer.parseInt(request.getParameter("num"));
//2. 상품의 가격을 얻어온다.
int price=shopDao.getPrice(num);
//3. 상품의 가격만큼 계좌 잔액을 줄인다.
ShopDto dto=new ShopDto();
dto.setId(id);
dto.setPrice(price);
shopDao.minusMoney(dto);
//4. 가격의 10%를 포인트로 적립한다.
shopDao.plusPoint(dto);
//5. 재고의 개수를 1 줄인다.
shopDao.minusCount(num);
//6. 주문 테이블(배송)에 정보를 추가한다.
OrderDto dto2=new OrderDto();
dto2.setId(id); //누가
dto2.setCode(num); //어떤 상품을
//클라이언트가 입력한 배송 주소라고 가정
String addr="강남구 역삼동";
dto2.setAddr(addr);//어디로 배송할지
orderDao.addOrder(dto2);
}
- spring에서는 이 transaction 작업을 annotation으로 처리할 수 있다.
- 이 전체가 트랜잭션으로 관리해야하는 업무라면 주의해야 한다.
- 재고가 부족해서 여기서 예외가 발생한다면? 또는 52행에서 발송 불가능한 지역이라 예외가 발생한다면?
주문도 되지 않았는데 계좌 잔액이 줄거나 포인트가 늘어나면 안되지 않을까?
- 그래서 하나라도 실패하면 전체를 롤백해야 하는 상황이 발생할 수 있다.
- transaction은 커밋을 보류하고 있지만, 기본적으로는 작업은 자동 커밋된다. (auto commit)
- orderDaoImpl 에서 확인해보기
- session에 commit() 이라는 메소드가 있다.
- rollback() 이라는 메소드도 있다.
- 하지만 이 전체를 하나하나 관리한다면 번거롭다.
- 그래서 commit과 rollback의 관리를 spring framework에 맡긴다.
buy.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">
<title>/shop/buy.jsp</title>
</head>
<body>
<div class="container">
<h1>상품 구매 결과 페이지</h1>
<p><strong>${id }</strong> 님 상품을 정상적으로 주문 했습니다.</p>
<a href="list">상품 목록보기</a>
</div>
</body>
</html>
- 구입하면 숫자가 하나씩 줄어들도록 했다.
- 이 재고값이 0이 되면 예외가 발생한다.
- check 제약조건을 만들어놓았기 때문에 -1은 들어갈 수 없다. 0까지만 값이 들어갈 수 있음.
- 하지만 계좌에 들어가보면 금액이 줄어있다. 재고가 없는데도 돈이 빠져나갔다!
- 계좌 잔액, 포인트가 6번 주문된 상태가 되어있다.
UPDATE client_account
SET money=10000, point=0
WHERE id='banana';
--
UPDATE shop
SET remainCount=5;
- 위 sql문으로 값을 복구하고 다시 테스트 해보기!
[ Spring 트랜잭션 설정 방법 ]
1. pom.xml에 spring-tx dependency 추가
2. servlet-context.xml에 transaction 설정 추가
3. 트랜잭션을 관리할 서비스의 메소드에 @Transactional 어노테이션 붙이기
- 프로그래머의 의도 하에서 트랜잭션에 영향을 주는 Exception을 발생시키는 방법
- DataAccessException 클래스를 상속받은 클래스를 정의하고
ex) class MyException extends DataAccessException{ }
throw new MyException("예외 메시지");
- 예외를 발생시킬 조건이라면 위와 같이 예외를 발생시켜서 트랜잭션이 관리되도록 한다.
- pom.xml에 이미 넣어놓은 트랜잭션 라이브러리!
- 위에서 servlet-context에서 설정한 것
- tx:annotation-driven 에서 저 txManager 를 필요로 한다.
@Transactional
- 이렇게 붙여두기만 하면 이 과정에서 예외가 발생하는 내용이 있다면 전체가 롤백 된다.
- 다시 테스트해보면, 재고가 0일때는 아무리 여러번 눌러도 이 이상으로 계좌잔액이 줄거나 포인트가 늘지 않는다!
- 재고도 줄지 않고, DB의 다른 값도 영향을 미치지 않는다.
ExceptionController 에 메소드 추가
//DB관련 작업을 하다가 발생하는 모든 예외를 처리하는 컨트롤러
@ExceptionHandler(DataAccessException.class)
public ModelAndView dataAccess(DataAccessException dae) {
ModelAndView mView=new ModelAndView();
mView.addObject("exception", dae);
mView.setViewName("error/info");
return mView;
}
- 이 정보가 클라이언트에 출력되도록 한다.
- view page 를 만들면 이것도 컨트롤 가능!
- Spring 프레임워크는 DB 관련 작업중에 sql Exception이 발생하면
해당 예외를 잡아서 DataAccessException을 발생시켜 준다.
- 따라서 Exception Controller에서 DataAccessException을 컨트롤할 준비가 되어있다면
해당 컨트롤러에서 직접 응답할 수가 있다.
- DataAccessException 은 Transaction에 영향을 주는 Exception이다.
- Transaction의 예외 처리를 하고싶다면 이것을 상속받아서 만든 클래스로 Exception 을 만들어내면 된다.
- 이 예시가 바로 위의 내용을 의미한다.
- Exception 클래스를 하나 만든 다음에 상속받으면 된다.
- 이렇게 만들면 transaction에 영향을 미치는 Exception 이 된다.
DeliveryException 생성
package com.sy.spring04.exception;
import org.springframework.dao.DataAccessException;
/*
* 트랜잭션에 영향을 주는 예외 클래스 만들기
* - DataAccessException 클래스를 상속받아서 만든다.
*/
//배송 관련 예외를 발생시키고 싶을 때 사용할 클래스
public class DeliveryException extends DataAccessException{
public DeliveryException(String msg) {
super(msg);
}
}
- 상속하고, Add Constructor
- 부모가 디폴트 생성자가 없으므로 반드시 msg를 전달해 주어야 한다.
- 트랜잭션에 영향을 주는 예외클래스라는 것은 즉 이 예외가 발생하면 rollback시키라는 뜻!
service - 메소드에서 테스트
ex) 만약 제주도는 주문 불가하다면?
if(addr.contains("제주도")) {
throw new DeliveryException("제주도는 배송 불가 지역입니다.");
}
- 이렇게 작성하고, 샘플 주소에 '제주도' 라는 텍스트를 넣어준다.
- 예외가 발생하면 이렇게 뜬다.
- DeliveryException 은 DataAccessException 타입이기도 하다.
- 그래서 이것이 실행된다!
- 만약에 다른처리를 하고 싶다면? 새로 view page를 만들면 된다.
Exception Controller 수정
- info 라는 키워드로 정보를 담아준다.
delivery.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">
<title>/views/error/delivery.jsp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h3>배송관련 에러 입니다.</h3>
<p class="alert alert-danger">${exception.message } </p>
<p class="alert alert-info">
${info }
<a href="${pageContext.request.contextPath}/">인덱스로 돌아가기</a>
</p>
</div>
</body>
</html>
- 뷰페이지에 css cdn을 넣어서 info를 출력해준다.
- 이렇게 상황에 따라 예외를 발생시켜서,
적절한 정보를 담은 에러 페이지도 띄울 수 있다.
'국비교육(22-23)' 카테고리의 다른 글
58일차(2)/Spring Boot(3) : Spring web project 세팅 / Spring Boot 설정 작성법 (0) | 2022.12.29 |
---|---|
58일차(1)/Spring(22) : 갤러리 게시판 구현 (1) | 2022.12.29 |
57일차(1)/Spring(20) : 게시판 댓글 기능 코드 리뷰 (0) | 2022.12.27 |
56일차(3)/Spring(19) : 게시판 댓글 기능 구현 (0) | 2022.12.27 |
56일차(2)/Spring(18) : 예외 클래스, Exception Controller (0) | 2022.12.26 |