728x90
300x250
[Spring-Framework] 35. Spring-JDBCTemplate - 트랜젝션 (어노테이션 X)


@Transactional이라는 어노테이션 기능 방식이 아닌 직접 구현으로 트랜젝션을 Spring JDBC에서 다루는 방법을 소개하려고 한다.


아마 이전의 Spring Framework의 34번 글을 실습해보았다면, 희안하게 insert 명령은 수행되어서 SQL Developer로 SELECT문으로 출력했을 때는 화면에 보이는데, Java에서 DB 질의로 SELECT문으로 조회한다면 동작이 되는 경우도 있고, 안 된 경우도 있었을 거 같다.


이 문제는 commit()을 했느냐 안 했느냐의 차이에서 발생한다.


1. [Spring-Framework] 34. Spring JDBCTemplate - 사용하기, 2020. 10. 9. 21:01

- https://yyman.tistory.com/1459


이 글의 개선 버전이라고 보면 되겠다.


- IDE: Eclipse 2020-06

- Spring Framework 4.2.4 RELEASES



1. 프로젝트 구성


다음 그림은 프로젝트를 구성하고 있는 구성도이다.



그림 1. 프로젝트 구성도


이전 글에서는 프로젝트 구성도까진 필요할까 고민했는데, 심플하게 적어주는 것이 더 나아보여서 핵심만 적었다.

지금 글에서는 Spring JDBC에서 트랜젝션을 제대로 사용하는 방법에 대해서 소개하려고 한다.



2. Project Factes, Build Paths, Java Compiler 버전 변경해주기


* Build Paths -> JRE Library 1.8로 변경, Junit 5 추가할 것

* Java Compiler -> Compiler complicance Level: 1.8 환경으로 바꿔줄 것

* Project Factes -> Java 버전 1.8로 변경할 것


- 오라클 JDBC는 WEB-INF/lib에 넣어서 해결해주면 된다.

  (Build Paths에 등록도 해줘야 함.)




3. pom.xml 설정하기



 <properties>
  <java-version>1.8</java-version>
  <org.springframework-version>4.2.4.RELEASE</org.springframework-version>
  <org.aspectj-version>1.6.10</org.aspectj-version>
  <org.slf4j-version>1.6.6</org.slf4j-version>
 </properties>
 <dependencies>


(중략)


  <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
  <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-jdbc</artifactId>
       <version>${org.springframework-version}</version>
  </dependency>

(중략)



  <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.21</version>
  </dependency>





4. AccountTbl.sql


-- Transaction 실습 DB (은행 - Account)
-- Oracle 11 - 자동번호 생성 테이블 정의
-- Table 생성 (FOODMENU_TBL)
-- NEW.ID (Table의 id를 가리킴)
CREATE TABLE account_tbl
(
    idx NUMBER PRIMARY KEY,
    name VARCHAR2(30),
    balance NUMBER,
    regidate TIMESTAMP
);

-- Sequence 정의
CREATE SEQUENCE account_sequence
START WITH 1
INCREMENT BY 1;

-- Trigger 생성
-- BEFORE INSERT on '테이블명'
CREATE OR REPLACE TRIGGER account_trigger
BEFORE INSERT
    ON account_tbl
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
SELECT account_sequence.nextval INTO :NEW.IDX FROM dual;
END;


파일명: AccountTbl.sql


[첨부(Attachments)]

AccountTbl.zip



5. MyDataSourceFactory.java (com.website.example.common)


package com.website.example.common;

import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import com.mysql.cj.jdbc.MysqlDataSource;

import oracle.jdbc.pool.OracleDataSource;

public class MyDataSourceFactory {
 
 private InputStream is = null;
    private Properties props = null;
 
 public MyDataSourceFactory()  {
  
        String resource = "db.properties";
        this.is = getClass().getClassLoader().getResourceAsStream(resource);
        this.props = new Properties();
       
 }
 
 public DataSource getMySQLDataSource() {
  
        MysqlDataSource mysqlDS = null;
       
        try {
         
            props.load(is);
            mysqlDS = new MysqlDataSource();
            mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));
            mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));
            mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));
           
        } catch (IOException e) {
            e.printStackTrace();
        }
       
        return mysqlDS;
       
    }
    
    public DataSource getOracleDataSource(){
     
        OracleDataSource oracleDS = null;
       
        try {
         
            props.load(is);
            oracleDS = new OracleDataSource();
            oracleDS.setURL(props.getProperty("ORACLE_DB_URL"));
            oracleDS.setUser(props.getProperty("ORACLE_DB_USERNAME"));
            oracleDS.setPassword(props.getProperty("ORACLE_DB_PASSWORD"));
           
           
        } catch (IOException e) {
         
            e.printStackTrace();
           
        } catch (SQLException e) {
         
            e.printStackTrace();
           
        }
       
        return oracleDS;
       
    }

}


파일명: MyDataSourceFactory.java


[첨부(Attachments)]

MyDataSourceFactory.zip


AccountTbl.zip



6. AccountVO.java (com.website.example.vo)


package com.website.example.vo;

import java.sql.Timestamp;;


public class AccountVO {
 
     private int idx;
     private String name;
     private int balance;
     private Timestamp regidate;
 
     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 getBalance() {
          return balance;
     }

 

     public void setBalance(int balance) {
          this.balance = balance;
     }


     public Timestamp getRegidate() {
          return regidate;
     }
 
     public void setRegidate(Timestamp regidate) {
          this.regidate = regidate;
      }
   
}


파일명: AccountVO.java


[첨부(Attachments)]

AccountVO.zip

AccountTbl.zip



7. AccountDAO.java (com.website.example.dao)


package com.website.example.dao;


import java.sql.SQLException;

import com.website.example.vo.AccountVO;


public interface AccountDAO {
 
       void createAccount(AccountVO vo) throws SQLException;
       int getBalance(String name);
       void minus(String name, int money) throws SQLException;
       void plus(String name, int money) throws SQLException;
 
}


파일명: AccountDAO.java


[첨부(Attachments)]

AccountDAO.zip



8. AccountDAOImpl.java (com.website.example.dao)


package com.website.example.dao;

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

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.website.example.common.MyDataSourceFactory;
import com.website.example.vo.AccountVO;

public class AccountDAOImpl implements AccountDAO {


       // Spring Framework - JDBC
       private JdbcTemplate jdbcTemplate = null;
       private DataSource ds = null;
 
       private final String INSERT = "insert into account_tbl(name, balance, regidate) values(?, ?, ?)";
       private final String SELECT_BALANCE = "select * from account_tbl where name = ?";
       private final String UPDATE_MINUS = "update account_tbl set balance = (select balance from account_tbl where name = ?) - ? " +
          " where name = ?";
       private final String UPDATE_PLUS = "update account_tbl set balance = (select balance from account_tbl where name = ?) + ? " +
          " where name = ?";
 
 
        public AccountDAOImpl(DataSource ds) {
               this.jdbcTemplate = new JdbcTemplate(ds);
               this.ds = ds;
        }
 
         public void createAccount(AccountVO vo) throws SQLException {
  
                TransactionSynchronizationManager.initSynchronization(); // 동기화
                Connection c = DataSourceUtils.getConnection(ds); //커넥션을 생성

  
                // 데이터 저장소 바인딩(트랜젝션)
                c.setAutoCommit(false); //트랜잭션을 시작
  
                try{
   
                    this.jdbcTemplate.update(INSERT, vo.getName(), vo.getBalance(), vo.getRegidate());
                    c.commit(); // 커밋
       
               }
               catch(Exception e) {
                    c.rollback(); // 예외가 발생하면 롤백
       
               }finally {
      
                    DataSourceUtils.releaseConnection(c, ds); // 스프링 유틸리티를 이용해 DB커넥션을 닫는다.
                    TransactionSynchronizationManager.unbindResource(ds); // 동기화 작업을 종료하고
                    TransactionSynchronizationManager.clearSynchronization();     // 정리한다.
      
               }

  
 }
 
    public int getBalance(String name){
     
     Object[] args = {name};
     
     AccountVO vo = this.jdbcTemplate.queryForObject(SELECT_BALANCE, args, new AccountRowMapper());
       
        int result = vo.getBalance();
     
        return result;
    }
    
    public void minus(String name, int money) throws SQLException{

     // 예외 발생시키기
     if(true){
      throw new SQLException(); // 의도적 예외 발생
        }
     
        this.jdbcTemplate.update(UPDATE_MINUS, name, money, name);
       
    }
    
    public void plus(String name, int money) throws SQLException{
          
        this.jdbcTemplate.update(UPDATE_PLUS, name, money, name);
    }
 
}


파일명: AccountDAOImpl.java


[첨부(Attachments)]

AccountDAOImpl.zip


AccountDAO.zip



9. AccountRowMapper.java (com.website.example.dao)


package com.website.example.dao;


import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

import com.website.example.vo.AccountVO;


public class AccountRowMapper implements RowMapper<AccountVO>  {


       @Override
        public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {
  
                 AccountVO vo = new AccountVO();

                 vo.setIdx(rs.getInt(1));
                 vo.setName(rs.getString(2));
                 vo.setBalance(rs.getInt(3));
                 vo.setRegidate(rs.getTimestamp(4));
  
                 return vo;
        }

}


파일명: AccountRowMapper.java


[첨부(Attachments)]

AccountRowMapper.zip




10. AccountService.java (com.website.example.service)


package com.website.example.service;

import java.sql.SQLException;

import com.website.example.vo.AccountVO;


public interface AccountService {

       void accountCreate(AccountVO vo) throws SQLException;
       void accountTransfer(String sender, String receiver, int money) throws SQLException;
 
}


파일명: AccountService.java


[첨부(Attachments)]

AccountService.zip




11. AccountServiceImpl.java (com.website.example.service)


package com.website.example.service;

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

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.vo.AccountVO;



public class AccountServiceImpl implements AccountService{

      private AccountDAO accountDAO;
      private DataSource ds = null;
 
      public AccountServiceImpl(DataSource ds) {
            this.accountDAO = new AccountDAOImpl(ds);
            this.ds = ds;
      }


      @Override
      public void accountCreate(AccountVO vo) throws SQLException {
            this.accountDAO.createAccount(vo);  
      }
 

      @Override
      public void accountTransfer(String sender, String receiver, int money) throws SQLException {
  
               int balance = accountDAO.getBalance(sender); // 보내는 사람 잔액 체크
               Connection conn = null;
     
               if(balance >= money){ // 보내는 돈이 잔액보다 많으면

                    TransactionSynchronizationManager.initSynchronization(); // 트랜잭션 동기화 작업 초기화

                    conn = ds.getConnection();
                    conn.setAutoCommit(false);
         
                    try {
                       accountDAO.minus(sender, money);
                       accountDAO.plus(receiver, money);
          
                       conn.commit(); // 성공
    
                   }catch(SQLException e) {
                        conn.rollback(); // 실패
                   }
                   finally {
                        // 커넥션 종료(Spring)
                       DataSourceUtils.releaseConnection(conn, this.ds);
          
                       // 동기화 작업을 종료하고 저장소를 비운다
                       TransactionSynchronizationManager.clearSynchronization();
                   }
           
             } else{

                   System.out.println("돈 없음");
                  //throw new NoMoneyException();
             }
  
       }


}


파일명: AccountServiceImpl.java


[첨부(Attachments)]

AccountServiceImpl.zip



12. MainTest.java (com.website.example.unit)


package com.website.example.unit;

import java.sql.SQLException;
import java.sql.Timestamp;

import javax.sql.DataSource;

import org.junit.jupiter.api.Test;

import com.website.example.common.MyDataSourceFactory;
import com.website.example.service.AccountService;
import com.website.example.service.AccountServiceImpl;
import com.website.example.vo.AccountVO;


class MainTest {


     @Test
     void test() throws SQLException {

            MyDataSourceFactory sourceFactory = new MyDataSourceFactory();
            DataSource ds = sourceFactory.getOracleDataSource();
  
            AccountService service = new AccountServiceImpl(ds);
            AccountVO vo = new AccountVO();
   
            /*
            // 1. 계정 생성
           vo.setName("홍길동");
           vo.setBalance(10000);
           vo.setRegidate(Timestamp.valueOf("2020-01-20 10:05:20"));
           service.accountCreate(vo);
  
           // 2. 계정 생성
           vo.setName("홍길자");
           vo.setBalance(0);
           vo.setRegidate(Timestamp.valueOf("2020-01-20 22:05:20"));
           service.accountCreate(vo);
            */
  
           // 3. 거래 처리
           service.accountTransfer("홍길동", "홍길자", 500);
  
  
      }

}


파일명: MainTest.java


[첨부(Attachments)]

MainTest.zip

AccountServiceImpl.zip




* 맺음글(Conclusion)


스프링 JDBC에서 트랜젝션 사용하는 방법에 대해서 어노테이션을 적용하지 않는 방법에 대해서 살펴보았다.


1. [Spring-Framework] 36. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java 설정), 2020-10-10

- https://yyman.tistory.com/1461

(이해를 돕기 위하여 실제 개발환경도구에서 트랜젝션 시연 영상도 포함해서 소개하였다.)


반응형

+ Recent posts