BackEnd/Spring

Spring_Transaction_국비_Day89

Leo.K 2022. 7. 8. 16:59
더보기

감시 지점을 지정하는 것 - PointCut, 지점의 정보를 가지고 있는 것 - JoinPoint

[ Spring Transaction ]

DML명령중에서 Select문을 제외한 나머지만 명령이 실행되고, 바로 DB에 저장되는 것이 아니고, Transaction Log에 저장되었다가, 커밋 또는 롤백이 된다. 이는 DML명령이 실행되다가 중간에 에러가 나서 앞에 실행한 명령은 수행되고, 에러 이후에 명령은 수행되지 않는 결과를 방지하기 위함이다. 이러한 성질을 All or Nothing 즉, 모든 DML이 커밋되거나, 하나라도 잘못되면 모두 취소하거나(roll back)이라고 한다.

가장 간단한 예를 들면 결제에서의 트랜잭션을 생각해볼 수 있다. 간단한 결제의 과정을 보면 사용자가 단가가 5000원인 특정 물품을 10개 주문(입고, insert) 했다고 생각해보자. 그런데 판매처에 물품의 재고가 8개라고 하면, 10개를 판매(출고)할 수 없는 상황이다. 이 경우 사용자의 요구사항에 따라 10개의 입고가 insert되었지만, 판매처는 재고에서 출고를 제외하고 나머지를 update로 갱신해주어야 하는데, 재고를 (8-10=)-2개로 갱신할 수 있는가? 안 된다. 여기에서 에러가 발생한다.

이 경우 insert(입고)는 진행이 되었지만, update는 에러가 발생한다. All or Nothing이라는 성질을 적용시켜서 트랜잭션 log에 있던 insert또한 update명령이 실패했기 때문에 rollback이 되어 취소가 된다.

이러한 과정이 없다면, insert는 그대로 이루어 졌기 때문에 사용자로부터는 10*5000=5만원을 받지만, 판매처는 제품의 재고가 부족하여 제품을 출고할 수 없다. 쉽게 말해 돈은 냈는데, 물건은 받지 못하는 상황이 벌어지는 것이다.  

 

오늘은 아래와 같은 이미지를 구현해보면서 입고&재고&출고를 통해서 트랜잭션이 무엇인지 확실하게 정리해보고자 한다. 

[ UI ]

[ DB설계 ]

한 가지 고려할 점이 있다면, 입고와 출고는 동일한 상품이 존재할 수 있지만, 재고 테이블에는 동일한 상품명이 존재할 수없기 때문에 unique속성을 주어야 한다. 쉽게 생각해보면 같은 상품에 대해서는 여러명의 고객이 주문할 수 있기 때문에, 상품명이 존재할 수 있지만, 재고 테이블에 하나의 상품을 두 개이상의 레코드로 나누어 저장하지는 않지 않는가? 하나의 레코드에 수량을 저장하는 것이 재고의 개념이다. 

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
--입고
create sequence seq_product_in_idx
 
create table product_in
(
   idx   int,              --일련번호
   name  varchar2(255),   --상품명 
   cnt   int,              --입고수량    
   regdate date           --입고일자
)
 
--기본키
alter table product_in
    add constraint pk_product_in_idx primary key(idx)
 
select * from product_in
    
    
    
    
--출고
create sequence seq_product_out_idx
 
create table product_out
(
   idx   int,--일련번호
   name  varchar2(255),   --상품명 
   cnt   int,              --출고수량    
   regdate date           --출고일자
)
--기본키
alter table product_out
    add constraint pk_product_out_idx primary key(idx)
 
select * from product_out
    
    
--입고&출고는 상품명이 중복되어도 되지만, 재고 상품은 한 상품에 대한 남은 수량을 저장하는 것이기 때문에 중복이 가능하지 않도록 제한조건을 설정한다. 
--재고
create sequence seq_product_remain_idx
 
create table product_remain
(
   idx   int,--일련번호
   name  varchar2(255),   --상품명 
   cnt   int,              --재고수량    
   regdate date          --재고일자
)
 
--기본키
alter table product_remain
    add constraint pk_product_remain_idx primary key(idx)
    
--제한조건
alter table product_remain
    add constraint unique_product_remain_name unique(name)
    
select * from product_remain
 
 
cs

 

[ ProductVo ]

세 개의 테이블의 컬럼 구성과 컬럼명이 동일하기 때문에 vo와 dao인터페이스는 하나만 만들고, 인터페이스를 구현하는 클래스만을 3가지로 나누어서 구현한다.

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package vo;
 
public class ProductVo {
    int     idx;            //인덱스
    String     name;            //상품명
    int     cnt;            //수량
    String     regdate;        //날자
    
    
    public int getIdx() {
        return idx;
    }
    public void setIdx(int idx) {
        this.idx = idx;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getCnt() {
        return cnt;
    }
    public void setCnt(int cnt) {
        this.cnt = cnt;
    }
    public String getRegdate() {
        return regdate;
    }
    public void setRegdate(String regdate) {
        this.regdate = regdate;
    }
    
}
 
cs

 

[ Product_Remain_DaoImpl ]

재고 테이블에 대한 예시이다. 재고 테이블만 특별히 SelectOne메소드가 있기 때문에 예시로 가져왔다. 나머지 출고와 입고 테이블에 대한 구현 클래스는 remain -> out or in으로만 수정하고 selectOne메소드는 삭제한다. 나머지는 같다. 트랜잭션 처리를 위해서 select를 제외한 DML명령은 throws Exception을 사용해서 예외 처리를 던져줘야 한다.

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package dao;
 
import java.util.List;
 
import org.apache.ibatis.session.SqlSession;
 
import vo.ProductVo;
 
public class Product_in_DaoImpl implements ProductDao{
    
    SqlSession sqlSession;
    
    //Setter Injection
    public void setSqlSession(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }
 
    
    @Override
    public List<ProductVo> list() {
        // TODO Auto-generated method stub
        return sqlSession.selectList("product_in.product_in_list");
    }

@Override
public ProductVo selectOne(String name) {
// TODO Auto-generated method stub
return sqlSession.selectOne("product_remain.product_remain_one", name);
}
 
    @Override
    public int insert(ProductVo vo) throws Exception {
        // TODO Auto-generated method stub
        return sqlSession.insert("product_in.product_in_insert", vo);
    }
 
    @Override
    public int update(ProductVo vo) throws Exception {
        // TODO Auto-generated method stub
        return sqlSession.update("product_in.product_in_update", vo);
    }
 
    @Override
    public int delete(int idx) throws Exception {
        // TODO Auto-generated method stub
        return sqlSession.delete("product_in.product_in_delete", idx);
    }
 
}
 
cs

 

[ mapper파일 product_remain.xml ]

매퍼파일 또한 remain -> out or in으로 수정해주고, selectOne만 삭제하면 나머지 내용은 동일하다.

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?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="product_in">
 
    <!-- 전체 조회 -->
    <select id="product_in_list" resultType="vo.ProductVo">
        select * from product_in order by idx
    </select>
    
<!-- 상품명 조회 -->
<select id="product_remain_one" parameterType="java.lang.String" resultType="vo.ProductVo">
select * from product_remain where name=#{name}
</select>

    <!-- 추가 -->
    <insert id="product_in_insert" parameterType="vo.ProductVo">
        insert into product_in values(
            seq_product_in_idx.nextVal,
            #{name},
            #{cnt},
            sysdate
        )
    </insert>
    
    <!-- 수정 -->
    <update id="product_in_update" parameterType="vo.ProductVo">
        update product_in
            set name=#{name}, cnt=#{cnt}, regdate=sysdate
        where idx=#{idx}
    </update>
    
    <!-- 삭제 -->
    <delete id="product_in_delete" parameterType="int">
        delete from product_in where idx=#{idx}
    </delete>
</mapper>
cs

 

[ ProductServlceImpl ]

91행과 100행을 보자. 환경설정 파일에서 rollback-for는 롤백이 일어나는 조건인데 그것이 바로 Exception이라고 했다. 따라서 출고해야 하는데 재고에는 없는 상품인 경우, 출고하는 상품이 재고에 있는 상품보다 더 많은 경우 -> 예외를 던져서 롤백을 시켜버린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package service;
 
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import dao.ProductDao;
import dao.Product_Remain_DaoImpl;
import vo.ProductVo;
 
public class ProductServiceImpl implements ProductService {
    
    ProductDao product_in_dao;
    ProductDao product_out_dao;
    ProductDao product_remain_dao;
    
    //여러개의 객체로부터 주입을 받는 경우에는 생성자 주입이 효율적이다.
    public ProductServiceImpl(ProductDao product_in_dao, ProductDao product_out_dao, ProductDao product_remain_dao) {
        super();
        this.product_in_dao = product_in_dao;
        this.product_out_dao = product_out_dao;
        this.product_remain_dao = product_remain_dao;
    }
 
    @Override
    public Map selectList() {
        // TODO Auto-generated method stub
        //입고목록 전체 조회
        List<ProductVo> in_list  = product_in_dao.list();
        
        //출고목록 전체 조회
        List<ProductVo> out_list = product_out_dao.list();
        
        //재고목록 전체 조회
        List<ProductVo> remain_list = product_remain_dao.list();
        
        
        //map으로데이터 취합하기 
        Map map = new HashMap();
        
        map.put("in_list", in_list);
        map.put("out_list", out_list);
        map.put("remain_list", remain_list);
        
        return map;
    }
 
    @Override
    public int insert_in(ProductVo vo) throws Exception {
        // TODO Auto-generated method stub
        int res = 0;
        
        //1. 입고테이블에 등록
        res = product_in_dao.insert(vo);
        
        //2. 현재 입고된 상품이 재고테이블에 존재하는지 여부를 확인한다. 
        ProductVo remainVo = product_remain_dao.selectOne(vo.getName());
        
        if(remainVo == null) {//재고 테이블에 등록이 안 된 경우
            
            //입고정보와 동일한 상품내역을 재고에도 등록
            res = product_remain_dao.insert(vo);
            
        }else {//재고 테이블에 등록이 된 경우
            
            // 재고 수량 = 이전 재고량 + 입고 수량
            int cnt = remainVo.getCnt() + vo.getCnt();
            
            // 수정된 수량을 다시 재고 vo넣기
            remainVo.setCnt(cnt);
            
            res = product_remain_dao.update(remainVo);
        }
        
        return res;
    }
 
    @Override
    public int insert_out(ProductVo vo) throws Exception {
        // TODO Auto-generated method stub
        int res = 0;
        
        // 1.출고등록
        res = product_out_dao.insert(vo);
        
        // 2.재고테이블에서 출고된 상품 정보 얻어오기 
        ProductVo remainVo = product_remain_dao.selectOne(vo.getName());
        
        if(remainVo == null) {//재고 테이블에 출고상품이 없는 경우
            
            throw new Exception("remain_not");
            
        }else {//재고 상품이 존재한다. 
            
            //재고수량 = 기존 재고 수량 - 출고 수량
            int cnt = remainVo.getCnt() - vo.getCnt();
            
            if( cnt < 0) {//재고수량 부족
                
                throw new Exception("remain_lack");
            }
            
            //재고처리 
            remainVo.setCnt(cnt);
            product_remain_dao.update(remainVo);
        }
        
        return res;
    }
 
}
 
cs

 

[ Dependency Injection ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- sqlSession -> dao -->
<!-- 입고 -->
<bean id="product_in_dao" class="dao.Product_In_DaoImpl">
    <property name="sqlSession" ref="sqlSession"></property>
</bean>
 
<!-- 출고 -->
<bean id="product_out_dao" class="dao.Product_Out_DaoImpl">
    <property name="sqlSession" ref="sqlSession"></property>
</bean>
 
<!-- 재고 -->
<bean id="product_remain_dao" class="dao.Product_Remain_DaoImpl">
    <property name="sqlSession" ref="sqlSession"></property>
</bean>
 
<!-- dao -> service -->
<!-- Injection순서가 중요하다! in -> out -> remain -->
<bean id="product_service" class="service.ProductServiceImpl">
    <constructor-arg ref="product_in_dao"/>
    <constructor-arg ref="product_out_dao"/>
    <constructor-arg ref="product_remain_dao"/>
</bean>
 
<!-- service -> controller -->
<beans:bean class="controller.ProductController">
    <beans:property name="product_service" ref="product_service"></beans:property>
</beans:bean>
cs

 

[ ProductController ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package controller;
 
import java.util.Map;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
 
import service.ProductService;
import vo.ProductVo;
 
@Controller
@RequestMapping("/product/")
public class ProductController {
    
    ProductService product_service;
    
    public void setProduct_service(ProductService product_service) {
        this.product_service = product_service;
    }
 
    @RequestMapping("list.do")
    public String list(Model model) {
        
        Map map = product_service.selectList();
        
        model.addAttribute("map", map);
        
        return "product/product_list";
    }
    
    // /product/insert_in.do?name=TV&cnt=100
    @RequestMapping("insert_in.do")
    public String insert_in(ProductVo vo) {
        
        try {
            int res = product_service.insert_in(vo);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            
        }
        
        return "redirect:list.do";
    }
    
    // /product/insert_in.do?name=TV&cnt=100
    @RequestMapping("insert_out.do")
    public String insert_out(ProductVo vo) {
        
        try {
            int res = product_service.insert_out(vo);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            
        }
        
        return "redirect:list.do";
    }
}
 
cs

 

[ 트랜잭션 환경 설정하기 & AOP ]

아래 파일 context-1-1-transaction.xml을 추가한다. 이때 중요한 것은 초기화 순서인데, 트랜잭션은 datasource로부터 의존성을 주입받아야 하기 때문에 아래 이미지와 같이 순서를 조정해야 한다. 

[ context-1-1-transaction.xml ]

아래 코드를 심도 있게 분석해보자. 포인트컷으로 지정한 모든 메소드에서 Exception이 발생하면, ProductService에 정의된 모든 DML명령들은 트랜잭션 로그에서 대기하고 있다가 커밋되어 DB로 들어가는 것이 아니라 롤백이 되어 모두 취소된다. 그렇기에 개발자는 트랜잭션이 발생하는 경우의 수를 알고 있다가 그 경우의 수에서 일부러 익셉션을 던져준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Transaction -->    
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="ds"/>
</bean>
 
 
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"  
                   rollback-for="Exception"/><!-- pointcut을 지정한 모든 메소드에서 Exception이 발생하면 rollback해라 rollback-for:롤백 조건 -->
    </tx:attributes>
</tx:advice>
   
<aop:config>
    <aop:pointcut id="requiredTx" expression="execution( * service.ProductService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>
cs

 

[ beans graph ]

 

[ 핵심 ]

ProductController에서 함수를 실행하면 ProductServiceImpl이 재정의한 메소드를 실행하다가 예외가 발생하면 예외를 던진다. 이 예외는 ProductServiceImpl을 호출한 ProductController이 받게되고 여기에서 반드시 예외처리를 해주어야 한다.

  1. pointcut으로 지정한 지점을 통과하면 txAdvice에게 joinpoint를 통보한다. 
  2. Exception이 발생한 경우에만 txManager에게 통보한다. 
  3. txManager가 ds에게 롤백을 해야 한다는 명령을 통보한다. 
  4. 트랜잭션 로그에 있던 DML명령들을 커밋이 아닌 롤백한다.

'BackEnd > Spring' 카테고리의 다른 글

Spring기초_Day6_웹 개발 모델  (0) 2022.07.11
Spring_Day5_SpringLifeCycle  (0) 2022.07.11
Spring_AOP_국비_Day87  (0) 2022.07.06
Spring_국비_Day86  (0) 2022.07.05
SpringFileUpload_국비_Day86  (0) 2022.07.04