2015년 7월 30일 목요일

신입SW인력을 위한 실전 자바(Java) 스프링(Spring) 동영상과정 22~24_Transaction[트랜젝션]

이 게시물은
http://www.wiz.center/252
http://www.wiz.center/253
http://www.wiz.center/254

해당 링크를 청취하고 작성한 글입니다.

본인 이해도 확인을 위해서 작성한 것이니
스프링에 대해서 알고 있다면 쑥 훝어보고 끝내시고

좀 깊게 알고 싶다면 위의 링크부터 시작해서
총 30개 강의로 이루어진 스프링 과정을 청취하세요

1. 트랜젝션 개념
    하나의 작업이 완료되는 단위
     하나의 작업이 db로 입력 2번 이상 진행되는 경우
     끝까지 정상 진행되는 경우에는 모두 입력
     순서 진행중 하나라도 오류, 조건이 맞지 않는 경우에는
     처음부터 끝까지 작업을 되돌린다.
    이런 연결된 업무를 하나의 트랜젝션 이라고 한다.


-------------------------------
예제 1. 트랜잭션 기능 확인
http://tip.daum.net/question/60526161
--mysql 에 제약조건 넣은 방법은 위 글 참고
--   결과적으로 mysql 에 제약조건을 넣어도
--   적용되지 않아서 oracle 처럼 db에서 걸기보다는
--   java단이나 그 이하단에서 걸어줘야 겠습니다.


작성1 table 생성
card, ticket 두개 테이블을 생성한다.

CREATE TABLE IF NOT EXISTS `card` (
  `consumerid` varchar(100) NOT NULL,
  `amount` int(4) not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `ticket` (
  `consumerid` varchar(100) NOT NULL,
  `countnum` int(4) not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


작성2. 상단 252번 강의에서  22-1예제를 import
          및 servlet.xml 의 oracle 컨넥션 풀을 아래와 같이
          mysql용으로 변경

 
  
  
  
  
 


작성3. dao 단에서 강제로 입력이 안되도록 설정한다.

  template.update(new PreparedStatementCreator() {
   
   @Override
   public PreparedStatement createPreparedStatement(Connection con)
     throws SQLException {
    PreparedStatement pstmt = null;
    if(Integer.parseInt(dto.getAmount()) < 5){
     String query = "insert into ticket (consumerId, countnum) values (?, ?)";
     pstmt = con.prepareStatement(query);
     pstmt.setString(1, dto.getConsumerId());
     pstmt.setString(2, dto.getAmount());
    }
    return pstmt;
   }
  });


결과 )  
 - kim
3 입력시 정상 입력됨
 - park
5 입력시 500에러가 발생하였음
   + card 는 정상 입력되었으나 ticket 은 입력되지 않음
여기서 card도 입력되지 않았어야 함
   (트랜젝션이 적용시 이부분을 같이 진행 가능함)

2. 트랜젝션 사용방법
-------------------------------
예제 2 그대로 사용

작성1  servlet.xml 에 다음 bean 을 추가


 


 
  
  
 


<!-- dao는 기존에 property 중 datasource를 삭제하고
       transactionManager를 받도록 설정한다 -->


작성2  DAO에 PlatformTransactionManager  객체를 추가
         byeTicket 함수를 아래와 같이 수정

public void buyTicket(final TicketDto dto) {
  System.out.println("buyTicket()");
  System.out.println("dto.getConsumerId() : " + dto.getConsumerId());
  System.out.println("dto.getAmount() : " + dto.getAmount());
  
  TransactionDefinition definition = new DefaultTransactionDefinition();
  TransactionStatus status = transactionManager.getTransaction(definition);
  //기본 클래스인 definition 클래스를 설정
                // 이후 좀더 간단한 구문으로 수정 예정
  try{
  template.update(new PreparedStatementCreator() {
   
   @Override
   public PreparedStatement createPreparedStatement(Connection con)
     throws SQLException {
    String query = "insert into card (consumerId, amount) values (?, ?)";
    PreparedStatement pstmt = con.prepareStatement(query);
    pstmt.setString(1, dto.getConsumerId());
    pstmt.setString(2, dto.getAmount());
    
    return pstmt;
   }
  });
  
  template.update(new PreparedStatementCreator() {
   
   @Override
   public PreparedStatement createPreparedStatement(Connection con)
     throws SQLException {
    PreparedStatement pstmt = null;
    if(Integer.parseInt(dto.getAmount()) < 5){
     String query = "insert into ticket (consumerId, countnum) values (?, ?)";
     pstmt = con.prepareStatement(query);
     pstmt.setString(1, dto.getConsumerId());
     pstmt.setString(2, dto.getAmount());
    }
    return pstmt;
   }
  });
  transactionManager.commit(status);
  
  }catch(Exception e){
   transactionManager.rollback(status);
                        System.out.println("this data rollback!! ");
  }
 }


결과 ) 테스트 시 5이상의 숫자를 넣어도 에러가
         나지 않고 정상적으로 진행되는것 처럼 보이지만
      실제 db에는 입력되지 않고 (card, ticket 둘다)
       rollback 함수가 실행된걸 확인 할 수 있었다.
     // oracle과 틀린점 -> oracle에서는 db에서 에러를 냈기 때문에
     // 콘솔에 에러가 찍힘

3. TransactionTemplate  을 활용하여 Transaction 을
     좀더 간단하게 사용할 수 있다.

-------------------------------
예제 1 기존 수정하던 예제


작성1 DAO 수정
        객체는 TransactionTemplate  로 생성하고
        하단 try catch 부분을 삭제하고
     
         객체명.execute(new TransactionCallbackWithoutResult(){
          .....
          }
         .... 안에 기존에 작성한 template
          파일을 통째로 집어넣는다.
package com.javalec.spring_pjt_ex.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.javalec.spring_pjt_ex.dto.TicketDto;

public class TicketDao {

 JdbcTemplate template;
 
// PlatformTransactionManager transactionManager;
// 
// 
// public void setTransactionManager(PlatformTransactionManager transactionManager) {
//  this.transactionManager = transactionManager;
// }
 
 TransactionTemplate transactionTemplate;

 public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
  this.transactionTemplate = transactionTemplate;
 }

 public void setTemplate(JdbcTemplate template) {
  this.template = template;
 }
 
 public TicketDao() {
  System.out.println(template);
 }
 
 public void buyTicket(final TicketDto dto) {
  System.out.println("buyTicket()");
  System.out.println("dto.getConsumerId() : " + dto.getConsumerId());
  System.out.println("dto.getAmount() : " + dto.getAmount());
  
  transactionTemplate.execute(new TransactionCallbackWithoutResult() {
   
   @Override
   protected void doInTransactionWithoutResult(TransactionStatus arg0) {
    template.update(new PreparedStatementCreator() {
     
     @Override
     public PreparedStatement createPreparedStatement(Connection con)
       throws SQLException {
      String query = "insert into card (consumerId, amount) values (?, ?)";
      PreparedStatement pstmt = con.prepareStatement(query);
      pstmt.setString(1, dto.getConsumerId());
      pstmt.setString(2, dto.getAmount());
      
      return pstmt;
     }
    });
    
    template.update(new PreparedStatementCreator() {
     
     @Override
     public PreparedStatement createPreparedStatement(Connection con)
       throws SQLException {
      PreparedStatement pstmt = null;
      if(Integer.parseInt(dto.getAmount()) < 5){
       String query = "insert into ticket (consumerId, countnum) values (?, ?)";
       pstmt = con.prepareStatement(query);
       pstmt.setString(1, dto.getConsumerId());
       pstmt.setString(2, dto.getAmount());
      }
      return pstmt;
     }
    });
   }
  });
  
//  TransactionDefinition definition = new DefaultTransactionDefinition();
//  TransactionStatus status = transactionManager.getTransaction(definition);
  
//  try{
//  
//  
//  
//  transactionManager.commit(status);
//  
//  }catch(Exception e){
//   transactionManager.rollback(status);
//   System.out.println("this data rollback!! ");
//  }
 }
}


작성2 servlet-context.xml 을 아래와 같이 수정한다.

 
  
 
 
 
  
 
 
 
  
  
 


결과 ) 이전 동작과 같이 정상 동작한다.

----------------------------------------------------------

4. TransactionTemplate  전파 속성 설정하기
         2개이상의 트랜젝션이 동작하고 있을때
          새로 들어가는 트랜젝션이 기존에 동작하고 있는
          트랜젝션과의 관계를 설정하는 기능입니다.
    먼저 속성에 대해 알아볼수 있는 예제 수정을 진행하고
      기능을 하나하나 알아보는 형태로 진행

 예제) 이전 예제에 트랜젝션을 추가하여
          2개 이상의 트랜젝션이 돌아가도록 수정한다.
작성1. servlet-context.xml 에 transactionTemplate를 2개 추가하여
         각각 dao, command(service) 에 할당


 

 
 
   
    

 
   
    


  
  
 
 

  
  



작성2. 패키지 추가com.javalec.spring_pjt_ex.command
         서비스를 담당할 패키지를 추가하고 그 밑에 인터페이스와
         구현체를 서술한다.
인터페이스 : ITicketCom

package com.javalec.spring_pjt_ex.command;

import com.javalec.spring_pjt_ex.dto.TicketDto;

public interface ITicketCom {
 public void execute (TicketDto ticketdto);
}

구현체 : TicketCom implements ITicketCom

package com.javalec.spring_pjt_ex.command;

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.javalec.spring_pjt_ex.dao.TicketDao;
import com.javalec.spring_pjt_ex.dto.TicketDto;

public class TicketCom implements ITicketCom {
 private TicketDao ticketDao;
 private TransactionTemplate transactionTemplate2;
 
 
 public void setTicketDao(TicketDao ticketDao) {
  this.ticketDao = ticketDao;
 }

 public void setTransactionTemplate2(TransactionTemplate transactionTemplate2) {
  this.transactionTemplate2 = transactionTemplate2;
 }

 @Override
 public void execute(final TicketDto ticketDto) {
  transactionTemplate2.execute(new TransactionCallbackWithoutResult() {
   
   
   @Override
   protected void doInTransactionWithoutResult(TransactionStatus arg0) {
    ticketDto.setAmount("1");
    ticketDao.buyTicket(ticketDto);
    ticketDto.setAmount("5");
    ticketDao.buyTicket(ticketDto); 
   }
  });
 }
}


작성3. controller 에서 변수 추가 및 호출을 dto로 하지 않고
         서비스를 거쳐서 진행되도록 변경한다.


.....
// private TicketDao dao;
 
 private ITicketCom ticketCom;
 
 @Autowired
 public void setTicketCom(ITicketCom ticketCom) {
  this.ticketCom = ticketCom;
 }
 
// @Autowired
// public void setDao(TicketDao dao) {
//  this.dao = dao;
// }
.......
 @RequestMapping("/buy_ticket_card")
 public String buy_ticket_card(TicketDto ticketDto, Model model) {
..................
  //dao.buyTicket(ticketDto);
  ticketCom.execute(ticketDto);
..................


실행시 값을 몇을 넣든 1,5 두번 실행될텐데 여기서는
    끝에 에러가 났겠지만 1을 넣어서 시작한 값부터
     끝까지 다 rollback 해버렸다.

이는 설정 파일의 
<beans:property name="propagationBehavior" value="0"></beans:property> 
옵션과 관계가 있다.  0은 전체 의존처리 이기 때문에
   해당 옵션과 맞춰 에러가 날 경우 전체 rollback 처리함.


이후 service(...com) 에서 작성한 트랜젝션과
       dto 안에 설정된 트랜젝션이 따로 동작하는걸로
       설정되어 있는데
       (트랜젝션1 은 DAO 에서 호출하고
        트랜젝션2 는 service(...com) 에서 사용하고 있음)
       이 둘 관계를 관찰하면서 각 속성을 알아보겠음

db 입력순서는

①card 이름 1
②ticket 이름 1
③card 이름 5
④ticket 이름 5


 1) PROPAGATION_REQUIRED(0)
    : 전체 의존 처리
작성1: servlet-context.xml 수정

 
  
  
 
 
 
  
  
 


     예제에서는 5에서 에러 났지만 1로 셋팅된 이전까지 취소됨
     DB에 한건도 입력되지 않았음


 2) PROPAGATION_SUPPORTS(1)
    : 기존 트랜젝션에 의존

 
  
  
 
 
 
  
  
 


     예제에서는 5에서 에러 났지만 1로 셋팅된 이전까지 취소됨
     서비스 객체를 1로 셋팅했지만 전체가 걸려있기 때문에
     1이나 0이나 별반 차이가 없었음.
   
     DB에 한건도 입력되지 않았음
서비스에서 객체 호출시 트랜젝션을 적용하지 않고 진행

@Override
 public void execute(final TicketDto ticketDto) {
  ticketDto.setAmount("1");
  ticketDao.buyTicket(ticketDto);
  ticketDto.setAmount("5");
  ticketDao.buyTicket(ticketDto); 
  
  
//  transactionTemplate2.execute(new TransactionCallbackWithoutResult() {
//   
//   
//   @Override
//   protected void doInTransactionWithoutResult(TransactionStatus arg0) {
//    ticketDto.setAmount("1");
//    ticketDao.buyTicket(ticketDto);
//    ticketDto.setAmount("5");
//    ticketDao.buyTicket(ticketDto); 
//   }
//  });
 }


     DB에 ③ 까지 입력됨
      하나의 DB 쿼리 실행이 하나의 트랜젝션으로 처리됨


 3) PROPAGATION_MANDATORY(2)
    :  트랜젝션에 꼭 포함 (트랜젝션 있는데서 호출해야 됨)


  
  
 
 
 
  
  
 


     실행시 DB 까지 진행되기 전에 에러처리됨
 Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

        트랜젝션이 걸리지 않은 서비스에서 호출하기 때문에
        이렇게 뜸

@Override
 public void execute(final TicketDto ticketDto) {
//  ticketDto.setAmount("1");
//  ticketDao.buyTicket(ticketDto);
//  ticketDto.setAmount("5");
//  ticketDao.buyTicket(ticketDto); 
  
  
  transactionTemplate2.execute(new TransactionCallbackWithoutResult() {
   
   
   @Override
   protected void doInTransactionWithoutResult(TransactionStatus arg0) {
    ticketDto.setAmount("1");
    ticketDao.buyTicket(ticketDto);
    ticketDto.setAmount("5");
    ticketDao.buyTicket(ticketDto); 
   }
  });
 }  

   
     처음부터 적용되지 않았지만
       에러가 다른 의미임
       oracle 엔진일 경우 제약 조건을 통해서 db 에러로 날릴수 있고
       mysql 의 경우 java 단에서 5이상은 입력하지 않아서
       dbms 에서 필수 인자가 없다는 메시지를 날릴 것임.


 4) PROPAGATION_REQUIRES_NEW(3)
    : 각각의 처리

 
  
  
 
 
 
  
  
 


서비스에서는 트랜젝션을 걸고 진행시

 @Override
 public void execute(final TicketDto ticketDto) {
//  ticketDto.setAmount("1");
//  ticketDao.buyTicket(ticketDto);
//  ticketDto.setAmount("5");
//  ticketDao.buyTicket(ticketDto); 
  
  
  transactionTemplate2.execute(new TransactionCallbackWithoutResult() {
   
   
   @Override
   protected void doInTransactionWithoutResult(TransactionStatus arg0) {
    ticketDto.setAmount("1");
    ticketDao.buyTicket(ticketDto);
    ticketDto.setAmount("5");
    ticketDao.buyTicket(ticketDto); 
   }
  });
 }


역시 에러는 뜨고  DB에 ② 까지 입력됨 5번 티켓에서걸리면
       해당 트랜젝션 5번 card까지 rollback 함

 5) PROPAGATION_NOT_SUPPORTED(4)
    :  트랜젝션이 없는것과 동일 (정의는 하지만 쓰지 않는다.)
     생략 트랜젝션 적용 전 진행했던 이전 예제를 참고바람

 6) PROPAGATION_NAVER(5) -- 3) 항목처럼 테스트 시 반대로 뜸
    : 트랜젝션에 절대 미 포함(트랜젝션 있는데서 호출시 에러 발생)

 
  
  
 
 
 
  
  
 


서비스(dao 호출클래스) 에서 트랜젝션을 걸고 실행시

Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

서비스 에서 트랜젝션을 걸지 않고 실행시
   500에러를 떨구지만
    DB에 ③ 까지 입력됨 마지막 하나에서
         에러나고 그 부분만 rollback됨

댓글 없음:

댓글 쓰기