728x90
300x250
[Spring-Framework] 37. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java - AOP)


이번에 소개할 내용은 지난 36번 글에 이어서 AOP를 추가로 적용한 방법에 대해 소개하려고 한다.

어노테이션 방식의 트랜젝션 구축이 이해가 되지 않으면, 이전 글을 참고하면 도움이 된다.


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

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


IDE: Eclipse 2020-06

- Spring Framework 4.2.4 Releases

- Spring-JDBC

- Spring-TX

- Spring-Core

- AspeetJ Weaver

- AspectJ


DB: Oracle Databases 11g (Express Edition)



1. 프로젝트 구성도(비교)


AOP 작업을 추가한 프로젝트와 기존의 프로젝트의 구성을 비교해놓은 것이다.

이 글에서는 이전의 작성된 객체 파일을 그대로 복사 붙여넣기하여 AOP패키지에 적용하였다.


LogAdvisor.java 파일 말고는 추가 코드를 작성하진 않았다.

MainTestAOP.java도 복사, 붙여넣기해서 만든 것이다. 가리키고 있는 명칭만 변경하였다.



 

 



그림 1, 그림 2. 
(Spring 트랜젝션 + Spring JDBC) + AOP

그림 3, 그림 4. 이전 프로젝트
(Spring 트랜젝션 + Spring JDBC)



2. pom.xml 설정하기


((중략) - 추가된 사항)
    
  <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${org.springframework-version}</version>
  </dependency>

  
    
  <!-- AspectJ -->
  <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>${org.aspectj-version}</version>
  </dependency> 
  

  <!-- AspectJWeaver 추가 -->
  <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>${org.aspectj-version}</version>
  </dependency>
  





3. LogAdvisor.java(com.website.example.aop)


package com.website.example.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Service;


//관점, 서비스
@Aspect
@Service
public class LogAdvisor {


   // 2단계 - 전 단계 시야)
 
  @Before("execution(* com.website.example.aop.service.AccountServiceAOP.*(..))")
  // @Before("execution(public void sum())")
  // 반환값 없어도 무방
  
   public void logBefore() {
           System.out.println("전 단계");


   }

 
}


파일명: LogAdvisor.java


[첨부(Attachments)]

LogAdvisor.zip



4. AccountServiceAOP.java(com.website.example.aop.service) - 예(복사, 붙여넣기)


코드 자체를 복사 붙여넣기 한 것이다.

AOP를 왜 하는지 철학을 이해하라고 하나 예로 보여주는 것이다.



package com.website.example.aop.service;

import java.sql.SQLException;


import com.website.example.vo.AccountVO;


public interface AccountServiceAOP {

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




5. AccountServiceImplAOP.java(com.website.example.aop.service) - 예(복사, 붙여넣기)


하나 어노테이션 @Repository를 달아주었다. 그거 말고는 동일하다.



package com.website.example.aop.service;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

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


@Repository
@Transactional
public class AccountServiceImplAOP implements AccountServiceAOP{

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

 @Override
 @Transactional(propagation=Propagation.NEVER)
 public void accountCreate(AccountVO vo) throws SQLException {
  accountDAO.createAccount(vo); 
  System.out.println("create CurrentTransactionName: " + TransactionSynchronizationManager.getCurrentTransactionName());
 }
 
 @Override
 @Transactional
 public void accountTransfer(String sender, String receiver, int money) throws SQLException {
  
     int balance = accountDAO.getBalance(sender); // 보내는 사람 잔액 체크
     
        if(balance >= money){ // 보내는 돈이 잔액보다 많으면
     
         System.out.println("transfer CurrentTransactionName: " + TransactionSynchronizationManager.getCurrentTransactionName() );
         
   accountDAO.minus(sender, money);
   accountDAO.plus(receiver, money);
   
        } else{

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


}




6. RootConfigAOP.java(com.website.example.common) - 예(복사, 붙여넣기)



package com.website.example.common;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@Import({DBConfigAOP.class})
@ComponentScan(basePackages = {"com.website.example"})
@EnableAspectJAutoProxy
//@ComponentScan(basePackages = {"com.local.example.beans", "com.local.example.advisor"})
public class RootConfigAOP {


}




7. DBConfigAOP.java(com.website.example.common) - 예(복사, 붙여넣기)


package com.website.example.common;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.aop.service.AccountServiceAOP;
import com.website.example.aop.service.AccountServiceImplAOP;


@Configuration
@EnableTransactionManagement
public class DBConfigAOP {

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
   
    @Bean
    public DataSource dataSource() {
     
     DataSource dataSource = new MyDataSourceFactory().getOracleDataSource();
     
     /* Apache DBCP
     
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/testDb?useUnicode=true&characterEncoding=utf8");
        dataSource.setUsername("test");
        dataSource.setPassword("test123!@#");
       
        */
        return dataSource;
    }
   
    @Bean
    public AccountDAO accountDAOImpl() {
     
     AccountDAO dao = new AccountDAOImpl(dataSource());
     return dao;
    }
   
    @Bean
    public AccountServiceAOP accountServiceImplAOP() {
     
     AccountServiceAOP service = new AccountServiceImplAOP(dataSource());

     return service;
    }
   
}




8. MainTestAOP.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 org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.website.example.common.MyDataSourceFactory;
import com.website.example.common.RootConfigAOP;

import com.website.example.dao.AccountDAOImpl;
import com.website.example.aop.service.AccountServiceAOP;
import com.website.example.aop.service.AccountServiceImplAOP;

import com.website.example.vo.AccountVO;


class MainTestAOP {

 @Test
 void test() throws SQLException {
  
       @SuppressWarnings("resource")
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RootConfigAOP.class);

  
        AccountServiceAOP service = (AccountServiceAOP) applicationContext.getBean("accountServiceImplAOP");
  
  AccountVO vo = new AccountVO();
  
  // 1. 계정 생성
  vo.setName("홍길동2");
  vo.setBalance(10000);
  vo.setRegidate(Timestamp.valueOf("2020-01-20 11:05:20"));
  service.accountCreate(vo);
  
  // 2. 계정 생성
  vo.setName("홍길자2");
  vo.setBalance(0);
  vo.setRegidate(Timestamp.valueOf("2020-01-20 23:05:20"));
  service.accountCreate(vo);
  
  // 3. 거래 처리
  service.accountTransfer("홍길동", "홍길자", 500);
  
  
 }


}




* 맺음글(Conclusion)


AOP를 쉽고 간단하게 트랜젝션과 연결해서 사용하는 방법에 대해서 소개하였다.



반응형
728x90
300x250
[Spring-Framework] 36. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java 설정)


이번 글에서 소개할 내용은 스프링 프레임워크의 트랜젝션을 조금 더 다뤄보려고 한다.


어노테이션을 사용하는 데 있어서 방법이 두 가지가 있다.


- 1. Java Config 파일 구성 방식

- 2. (context.xml) XML 파일 구성 방식


* 참고로 스프링 프레임워크는 셋팅이 시작의 반이라고 보면 될 것 같다.


[작업 환경]

IDE: Eclipse 2020-06

Framework: Spring Framework 4.2.4 RELEASE

spring-jdbc

spring-core

spring-tx

mysql-connector-java (8.0.21)

oracle jdbc (WEB-INF/lib에 넣어줘야 함.)


OpenJDK 15 (프로젝트에 적용한 자바 버전은 1.8로 셋팅하였음.)


딱딱한 글도 중요하지만, 영상으로 이게 무엇인지 소개해주겠다. (작동 원리 시연)



영상 1. Spring Framework 4.2.4 RELEASES - Transactions (시연)





1. 프로젝트 구성


프로젝트 구성에 관한 것이다.



그림 1. 프로젝트 구성도



2. 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>


  (중략)

  
  <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${org.springframework-version}</version>
  </dependency>
  
  <!-- 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/org.springframework/spring-tx -->
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</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>




3. Build Path, Project Factes, Java compiler 설정하기


* Build Path -> JRE System Library 1.8, JUnit 5

* Java Compiler -> Compiler compliance level: 1.8

* Project Factes -> Java : 1.8


* 오라클 JDBC: WEB-INF/lib폴더에 ojdbc.jar 파일 넣어주기 




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




4. db.properties (/src/main/resources)


MYSQL_DB_URL=jdbc:mysql://localhost:3306/dbanme?useUnicode=true&characterEncoding=utf8
MYSQL_DB_USERNAME=
MYSQL_DB_PASSWORD=
ORACLE_DB_URL=jdbc:oracle:thin:@127.0.0.1:1521:xe
ORACLE_DB_USERNAME=HR
ORACLE_DB_PASSWORD=1234


파일명: db.properties


[첨부(Attachments)]

db.zip



5. 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




6. DBConfig.java (com.website.example.common)


package com.website.example.common;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.service.AccountService;
import com.website.example.service.AccountServiceImpl;

@Configuration
@EnableTransactionManagement

public class DBConfig {

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
   
    @Bean
    public DataSource dataSource() {
     
     DataSource dataSource = new MyDataSourceFactory().getOracleDataSource();
     
     /* Apache DBCP
     
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/testDb?useUnicode=true&characterEncoding=utf8");
        dataSource.setUsername("test");
        dataSource.setPassword("test123!@#");
       
        */
        return dataSource;
    }
   
    @Bean
    public AccountDAO accountDAOImpl() {
     
     AccountDAO dao = new AccountDAOImpl(dataSource());
     return dao;
    }
   
    @Bean
    public AccountService accountServiceImpl() {
     
     AccountService service = new AccountServiceImpl(dataSource());
     return service;
    }
   
}


파일명: DBConfig.java


[첨부(Attachments)]

DBConfig.zip




7. RootConfig.java (com.website.example.common)


package com.website.example.common;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@Import({DBConfig.class})
@ComponentScan(basePackages = {"com.website.example"})
//@ComponentScan(basePackages = {"com.local.example.beans", "com.local.example.advisor"})
public class RootConfig {


}


파일명: RootConfig.java


[첨부(Attachments)]

RootConfig.zip




8. 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




9. 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

MyDataSourceFactory.zip




10. 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 org.springframework.stereotype.Service;

import com.website.example.vo.AccountVO;

@Service
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





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


package com.website.example.dao;

import java.sql.SQLException;
import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
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 {
   
             this.jdbcTemplate.update(INSERT, vo.getName(), vo.getBalance(), vo.getRegidate());  
       }
 
       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{
     
              this.jdbcTemplate.update(UPDATE_MINUS, name, money, name);
       
       }
   
       public void plus(String name, int money) throws SQLException{

            /*
            // 예외 발생시키기
            if(true){
                throw new RuntimeException("런타임 에러");
            }
             */
     
            this.jdbcTemplate.update(UPDATE_PLUS, name, money, name);
       }
 
}


파일명: AccountDAOImpl.java


[첨부(Attachments)]

AccountDAOImpl.zip





12. 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




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



package com.website.example.service;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

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

@Repository
@Transactional
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); // 보내는 사람 잔액 체크
      
            if(balance >= money){ // 보내는 돈이 잔액보다 많으면
     
                 accountDAO.minus(sender, money);
                 accountDAO.plus(receiver, money);
              
        } else{

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


}


파일명: AccountServiceImpl.java


[첨부(Attachments)]

AccountServiceImpl.zip





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


package com.website.example.unit;


import java.sql.SQLException;

import java.sql.Timestamp;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.website.example.common.RootConfig;
import com.website.example.service.AccountService;
import com.website.example.vo.AccountVO;


class MainTest {

      
      @Test
      void test() throws SQLException {
  
           @SuppressWarnings("resource")
           ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RootConfig.class);
  
           AccountService service = (AccountService) applicationContext.getBean("accountServiceImpl");
  
           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




* 맺음글(Conclusion)


트랜젝션을 어노테이션 방식으로 Java Config 환경설정으로 구성하여 사용하는 방법에 대해서 소개하였다.


[Spring-Framework] 37. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java - AOP)

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



* 참고 자료(References)


1. Spring JDBC, https://velog.io/@koseungbin/Spring-JDBC, Accessed by 2020-10-10, Last Modified 2020-07-08.

2. Spring JDBC using Annotation based configuration, https://www.topjavatutorial.com/frameworks/spring/spring-jdbc/spring-jdbc-using-annotation-based-configuration/, Accessed by 2020-10-10, Last Modified 2016-02-14.

반응형
728x90
300x250

[JSP] 27. Java 스타일의 트랜젝션 구현하기(은행 계좌)


트랜젝션 처리에 대해서 소개하려고 한다.

이 주제는 사실 몇 가지 키워드가 있다.


ACID(원자성, 일관성, 고립성, 지속성)을 만족해야 한다.


기본적인 DML만 조작하다가 갑자기 접근하게 되면, 이 내용은 조금 난해할 수도 있다.


1. 자료처리에 있어서 4가지를 모두 충족해야 한다.


원자성(Atomicity)
트랜젝션은 분해가 불가능한 최소의 단위인 하나의 원자처럼 동작한다는 의미. 

트랜젝션 내의 모든 연산들은 반드시 한꺼번에 완전하게 전체가 정상적으로 수행이 완료되거나 아니면 연산자체를 수행하지 않음.


일관성(Consistency)
트랜잭션 작업이 시작되지 전에 데이터베이스 상태가 일관된 상태였다면 트랜잭션 작업이 종료된 후에도 일관성 있는 데이터 베이스 상태를 유지해아한다.

예를 들어서 게시판에 글을 쓰는데 제목의 글자 제한이 255자라고 하자.

트랜잭션이 일어나면 이러한 조건을 만족해야하는 것이다. 만약 이를 위반하는 트랜잭션이 있다면 롤백 처리해야 한다.

(문제가 있는 작업이라면, 입력자체를 시키면 안 되는 것이다.)


고립성(Isolation)
트랜잭션 작업 수행 중에는 다른 트랜잭션에 영향을 주어서도 안 되고, 다른 트랜잭션들에 의해 간섭을 받아서도 안 된다는 것을 의미. 


다른 트랜잭션의 영향을 받게 되면 영향을 주는 트랜잭션에 의해 자신의 동작이 달라 질 수 있기 때문이다.

트랜젝션 자신은 고립된 상태에서 수행되어야 한다는 것을 의미. 즉 다수의 트랜잭션이 동시에 수행중인 상황에서 하나의 트랜잭션이 완료될 때까지는 현재 실행 중인 트랜잭션의 중간 수행결과를 다른 트랜잭션에서 보거나 참조 할 수 없다.


지속성(Durablility)
일련의 데이터 조작(트렌젝션 조작)을 완료 하고 완료 통지를 사용자가 받는 시점에서 그 조작이 영구적이 되어 그 결과를 잃지 않는 것을 나타낸다. 시스템이 정상일 때 뿐 아니라 데이터베이스나 OS의 이상 종료, 즉 시스템 장애도 견딜 수 있다는 것을 말한다.




2. "은행 계좌"라는 주제


은행 계좌 구현의 문제를 놓고 보면, 돈을 이체시켰는데 시스템 장애로 돈이 빠져나가고 이체가 되지 않아버리면 입금자는 돈을 잃어버리게 된다.

물론 트랜젝션만 가지고 은행 시스템이 구축된 것은 아닐 것이다.


이런 경우를 컴퓨터 프로그래밍으로 표현해보려고 한다.




3. JSP/Servlet으로 구현해도 되고, JUnit 태스트 도구로 접근해도 상관없다.


이 프로젝트를 자습하는 데 있어서, 자유롭게 원하는 형식에서 태스트해도 무방하다.

자바 기반이면, 스프링프레임워크에서도 가능하고, Dynamic Web Project를 생성해도 무방하고 Java Project로 따라해도 괜찮다는 이야기를 하고 소개하겠다.


[태스트 환경]

IDE: Eclipse 2020-06

DB: Oracle 11g XE(Express Edition)
- Maven Projects(Spring 포함)

- JUnit 5



4. 데이터베이스 설계


-- 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;


트리거 작성과 스퀀스 번호 생성에 대해서도 적혀져 있으니깐, 잘 사용해도 무방하다.


파일명: account_tbl.sql


[첨부(Attachments)]

AccountTbl.zip




5. 프로젝트 구성도


아래 그림은 프로젝트 구성도이다.

HomeController.java 등은 생략해도 된다.



그림 1, 그림 2. 프로젝트 구성도



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




6-1. db.properties (/src/main/resources)


MYSQL_DB_URL=jdbc:mysql://localhost:3306/dbanme?useUnicode=true&characterEncoding=utf8
MYSQL_DB_USERNAME=
MYSQL_DB_PASSWORD=
ORACLE_DB_URL=jdbc:oracle:thin:@127.0.0.1:1521:xe
ORACLE_DB_USERNAME=HR
ORACLE_DB_PASSWORD=1234


파일명: db.properties


[첨부(Attachments)]

db.zip




7. JDBCUtil.java(com.website.example.common)


이 프로젝트에서는 close()만 사용하였다. 예비 태스트 용도로 자바 표준의 SQL 방식도 두고 있다.


package com.website.example.common;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import javax.sql.DataSource;


public class JDBCUtil {
 
      private static final String driverName = "oracle.jdbc.driver.OracleDriver";
      private static final String jdbcUrl = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
      private static final String userId = "{사용자 계정명}";
      private static final String userPwd = "{비밀번호}";
 
 public static Connection getConnection() {


  try {
   
       Class.forName(driverName); 
        return DriverManager.getConnection(jdbcUrl, userId, userPwd);
   
  } catch (Exception e) {
        e.printStackTrace();
  }
  
  return null;
  
 }


 public static void close(PreparedStatement stmt, Connection conn) {
  
  if (stmt != null) {
   
   try {
    
    if (!stmt.isClosed())
     
       stmt.close();
    
   } catch (Exception e) {
    
        e.printStackTrace();
    
   } finally {
    
        stmt = null;
    
   }
   
  } // end of if
  
  if (conn != null) {
   
   try {
    
    if (!conn.isClosed())
         conn.close();
    
   } catch (Exception e) {
    
    e.printStackTrace();
    
   } finally {
    
    conn = null;
    
   }
   
  } // end of if
  
 }

 public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) {
  
  if (rs != null) {
   
   try {
    
    if (!rs.isClosed())
     rs.close();
    
   } catch (Exception e) {
    
    e.printStackTrace();
    
   } finally {
    
    rs = null;
    
   }
   
  } // end of if
  
  if (stmt != null) {
   
   try {
    
    if (!stmt.isClosed())
     stmt.close();
    
   } catch (Exception e) {
    
    e.printStackTrace();
    
   } finally {
    stmt = null;
   }
   
  } // end of if
  
  if (conn != null) {
   
   try {
    
    if (!conn.isClosed())
     conn.close();
    
   } catch (Exception e) {
    
    e.printStackTrace();
    
   } finally {
    
    conn = null;
    
   }
   
  } // end of if
    
 }

}



파일명: JDBCUtil.java


[첨부(Attachments)]

JDBCUtil.zip

AccountVO.zip




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


DataSourceFactory이다.



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;
 
 public MyDataSourceFactory()  {
  
        String resource = "db.properties";
        is = getClass().getClassLoader().getResourceAsStream(resource);
 }
 
 public DataSource getMySQLDataSource() {
  
        Properties props = new Properties();
       
        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(){
     
        Properties props = new Properties();
        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


JDBCUtil.zip

AccountVO.zip



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


AccountDAO에 대한 명세이다. DAO는 Data Access Object의 약자이다.


package com.website.example.dao;

import java.sql.SQLException;

import com.website.example.vo.AccountVO;

public interface AccountDAO {

 // 계좌 생성
 public void create(AccountVO vo) throws SQLException;
 
 // 잔액 조회
 public int getBalance(String name) throws SQLException;
 
 // 플러스 계좌
 public void plus(String name, int money) throws SQLException;
 
 // 마이너스 계좌
 public void minus(String name, int money) throws SQLException;
 
 // 거래
 public void transfer(String sender, String receiver, int money) throws SQLException;
 
}


파일명: AccountDAO.java


[첨부(Attachments)]

AccountDAO.zip


JDBCUtil.zip

AccountVO.zip



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


AccountDAO에 대한 명세이다. DAO는 Data Access Object의 약자이다.


package com.website.example.dao;

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

import javax.sql.DataSource;

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

// Transaction - 자바
public class AccountDAOImpl implements AccountDAO {


 private final String CREATE_ACCOUNT = "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 = ?";
 
 private DataSource ds = null;
 
 public AccountDAOImpl(DataSource ds) {
       this.ds = ds;
 }
 
 // 단일 쿼리에서의 트랜젝션 방법
 @Override
 public void create(AccountVO vo) throws SQLException {


       Connection conn = ds.getConnection();
       PreparedStatement pstmt = null;
  
  try {
   conn.setAutoCommit(false); // 트랜젝션 시작
   
   pstmt = conn.prepareStatement(CREATE_ACCOUNT);
   pstmt.setString(1, vo.getName());
   pstmt.setInt(2, vo.getBalance());
   pstmt.setTimestamp(3, vo.getRegidate());
   
   pstmt.executeUpdate();
   
   conn.commit();
   
  }catch(SQLException e) {
   
   conn.rollback();
   e.getMessage();
   
  }finally {
   JDBCUtil.close(pstmt, conn);
  }
  
 }

 @Override
 public int getBalance(String name) throws SQLException {
  
  Connection conn = ds.getConnection();
  PreparedStatement pstmt = null;
  ResultSet rs = null;
  
  int result = 0;
  
  try {
        conn.setAutoCommit(false); // 트랜젝션 시작
        pstmt = conn.prepareStatement(SELECT_BALANCE);
   
   pstmt.setString(1, name);
   
   rs = pstmt.executeQuery();
   
   if ( rs.next() ) {
         result = rs.getInt(3);
   }
   
   conn.commit();
   
  }catch(SQLException e) {
   
   conn.rollback();
   e.getMessage();
   
  }finally {
   JDBCUtil.close(pstmt, conn);
  }
  
  return result;
 }

 @Override
 public void plus(String name, int money) throws SQLException {


      Connection conn = ds.getConnection();
      PreparedStatement pstmt = null;
  
  try {
   
   conn.setAutoCommit(false);
   
   // plus, minus 다 확인 후에 commit처리 해야 함.
   // 그래서 지금 바로 트랜젝션을 구현하면 안 됨
   pstmt = conn.prepareStatement(UPDATE_PLUS);
   pstmt.setString(1, name);
   pstmt.setInt(2, money);
   pstmt.setString(3, name);
   
   pstmt.executeUpdate();
      System.out.println(money);

   conn.commit();
   
  }catch(SQLException e) {
   System.out.println(e.getMessage());
   conn.rollback();
   
  }finally {
   JDBCUtil.close(pstmt, conn);
  }
  
 }

 @Override
 public void minus(String name, int money) throws SQLException {
  

       Connection conn = ds.getConnection();
       PreparedStatement pstmt = null;
  
  try {
   
   conn.setAutoCommit(false);

      // 예외 발생시키기(트랜젝션 의도적 발생)
      if(true){
             throw new SQLException(); // 의도적 예외 발생
       }
        
   // plus, minus 다 확인 후에 commit처리 해야 함.
   // 그래서 지금 바로 트랜젝션을 구현하면 안 됨
   pstmt = conn.prepareStatement(UPDATE_MINUS);
   pstmt.setString(1, name);
   pstmt.setInt(2, money);
   pstmt.setString(3, name);
   
   pstmt.executeUpdate();
   
   conn.commit();
   
  }catch(SQLException e) {
   
        conn.rollback();
        System.out.println(e.getMessage());
    
  }finally {
        JDBCUtil.close(pstmt, conn);
  }
  
  
 }

 @Override
 public void transfer(String sender, String receiver, int money) throws SQLException {

  Connection conn = ds.getConnection();
  PreparedStatement pstmt = null;
  
  try {
   
   conn.setAutoCommit(false);

   /*
      // 예외 발생시키기(트랜젝션 의도적 발생)
      if(true){
       throw new SQLException(); // 의도적 예외 발생
         }
         */
        
   // plus, minus 다 확인 후에 commit처리 해야 함.
   // 그래서 지금 바로 트랜젝션을 구현하면 안 됨
   pstmt = conn.prepareStatement(UPDATE_MINUS);
   
   // 주는 분
   pstmt.setString(1, sender);
   pstmt.setInt(2, money);
   pstmt.setString(3, sender);
   
   pstmt.executeUpdate();
   
   // 받는 분
   pstmt = conn.prepareStatement(UPDATE_PLUS);
   pstmt.setString(1, receiver);
   pstmt.setInt(2, money);
   pstmt.setString(3, receiver);
   
   pstmt.executeUpdate();
   
        conn.commit();
   
  }catch(SQLException e) {
   
        conn.rollback();
        System.out.println(e.getMessage());
   
  }finally {
       JDBCUtil.close(pstmt, conn);
  }
  
 }

}


파일명: AccountDAOImpl.java


[첨부(Attachments)]

AccountDAOImpl.zip



JDBCUtil.zip

AccountVO.zip



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


AccountService에 대한 명세이다.

서비스에 대한 정의이다. DB를 정의하는 영역은 아니다.

하지만, Spring Framework로 구현할 때, Spring JDBC 등을 적용하여 트랜젝션 동기화 기능을 구현부에 구현하면, Connection을 사용할 수도 있다.

질의문을 정의하는 영역은 아니다.


package com.website.example.service;

import java.sql.SQLException;

import com.website.example.vo.AccountVO;


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


파일명: AccountService.java


[첨부(Attachments)]

AccountService.zip



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


AccountServiceImpl.java는 AccountService의 구현부이다.

인터페이스의 역할을 정의해보면, 하나의 통로와 같은 역할을 한다고 볼 수 있다.


package com.website.example.service;

import java.sql.SQLException;

import javax.sql.DataSource;

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) {
  
        accountDAO = new AccountDAOImpl(ds);
        this.ds = ds;
 }

 @Override
 public void accountCreate(AccountVO vo) throws SQLException {
  
        accountDAO.create(vo);
  
 }
 
    public void accountTransfer(String sender, String receiver, int money) throws SQLException{
       
     int balance = accountDAO.getBalance(sender); // 보내는 사람 잔액 체크
     
        if(balance >= money){ // 보내는 돈이 잔액보다 많으면
 
                   accountDAO.transfer(sender, receiver, money);
         
        } else{
                   System.out.println("돈 없음");
                   //throw new NoMoneyException();
        }
       
    }

 
}


파일명: AccountServiceImpl.java


[첨부(Attachments)]

AccountServiceImpl.zip




13. TestMain.java (com.website.example.unit)


JUnit의 태스트 영역이다.


package com.website.example.unit;

import static org.junit.jupiter.api.Assertions.*;

import java.sql.SQLException;

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 TestMain {


       @Test
       void test() throws SQLException {
   
                 DataSource ds = new MyDataSourceFactory().getOracleDataSource();
                 AccountService service = new AccountServiceImpl(ds);
  
  // 1. 계정 생성 (계정이 없는 경우라면, 주석 풀고 작업해보면 됨.
  /*
  AccountVO vo = new AccountVO();
  vo.setName("홍길동");
  vo.setBalance(10000);
  vo.setRegidate(Timestamp.valueOf("2020-10-01 10:30:00"));
  
  service.accountCreate(vo);

  vo.setName("홍길자");
  vo.setBalance(0);
  vo.setRegidate(Timestamp.valueOf("2020-10-04 23:20:00"));

  service.accountCreate(vo);
  */
  
  // 2. 금전 거래
  service.accountTransfer("홍길동", "홍길자", 500);
  
 }

}


파일명: TestMain.java


[첨부(Attachments)]

TestMain.zip




14. 태스트 해보기 - 결과


코드로 구현하여 태스트를 해봐도 좋을 듯 싶다.



그림 3. 태스트 하기



그림 4. 태스트 하기


트랜젝션을 한번 해보면, 무슨 일이 일어나는지 구경을 조금해보면 좋을 듯 싶다.




15. 맺음글(Conclusion)


setAutoCommit(), conn.commit(), conn.rollback() 사소해보이는 이 코드가 반응하는 것을 관찰해보면, 트랜젝션이 중요하다는 사실을 알 수 있다.



* 참고자료(References)


1. JDBC 트랜젝션 동기화, https://joont.tistory.com/158, Accessed by 2020-10-09, Last Modified 2016-07-11.

-> 추천(50점): 정말 잘 설명해주고 있다.

2. Data Access - Transaction, https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#transaction, Accessed by 2020-10-09, Last Modified 2020-09-15.


3. 

반응형

+ Recent posts