spring - 트랜잭션 기능 사용

오늘·2021년 6월 2일
0

웹 페이지 연습

목록 보기
24/35

트랜잭션(Transaction)

  • 여러개의 DML 명령문을 하나이 논리적인 작업 단위로 묶어서 관리하는 것
  • All 또는 Nothing 방식으로 작업을 처리함으로써 작업의 일관성을 유지
  • 웹 애플리케이션에서는 Service 클래스의 각 메소드가 애플리케이션의 단위 기능을 수행한다

묶어서 처리하는 단위 기능의 예

웹 애플리케이션에서 묶어 처리하는 단위기능의 예는 아래와 같다

  • 게시판 글 조회시 '해당 글을 조회'하는 기능과 '조회 수 갱신' 기능
  • 쇼핑몰에서 상품 주문 시 주문 상품을 '테이블에 기록'하는 기능과 주문자의 '포인트 갱신' 하는 기능
  • 은행에서 송금 시 '송금자의 잔고 갱신'과 '수신자의 잔고 갱신'

이렇게 거의 동시에 처리되어야하는 기능들을 묶어 처리한다.


스프링의 여러가지 트랜잭션 속성들

  1. propagation : 트랜잭션 전파 규칙 설명
  2. isolation : 츠랜잭션 격리 레벨 설정
  3. realOnly : 읽기 전용 여부를 설정
  4. rollbackFor : 트랜잭션을 롤백(rollback)할 때 예외 타입 설정
  5. norollbackFor : 트랜잭션을 롤백하지 않을 때 예외 타입 설정
  6. timeout : 트랜잭션의 타임아웃 시간 설정

propagation 속성이 가지는 값들

+
nested : 트랜 잭션 필요 / 진행 중인 트랜잭션이 있는 경우 기존 트랜잰션에 중첩된 트랜잭션에서 메소드 실행 / 트랜잭션이 없으면 새로운 트랜잭션 생성

isolation 속성이 가지는 값들


실습해보기 01

오라클에 사용할 테이블 만들기

create table cust_account (
    accountNo varchar2(20) primary key,
    cusName varchar2(50),
    balance number(20, 4)
);

-- 천만원씩 넣어놓기
insert into cust_account values('70-490-930', '홍길동', 100000000);
insert into cust_account values('70-490-912', '김유민', 100000000);

-- 커밋 필수... 반드시
commit;

-- 확인
select * from cust_account;

jdbc.properties

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@localhost:1521:XE
jdbc.username=System
jdbc.password=hb0317vd

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<listener>
      <listener-class>
         org.springframework.web.context.ContextLoaderListener
     </listener-class>
   </listener>
	<context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
          /WEB-INF/config/action-mybatis.xml
          /WEB-INF/config/action-service.xml
      </param-value>
   </context-param>
	
	
	<servlet>
		<servlet-name>action</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>action</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

</web-app>

action-servlet.xml

뷰 관련 빈과 각 url 요청명에 대해 호출될 메소드 설정

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.0.xsd">
	
		
	<bean id="viewResolver"  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
      <property name="prefix" value="/WEB-INF/views/account/" /> 
      <property name="suffix" value=".jsp"/>
   </bean>
   
   <bean id="accController" class="com.spring.account.AccountController">
      <property name="methodNameResolver">
         <ref local="methodResolver"/>
      </property>
      <!-- ref = "accService" : accService 빈을 여기에 넣어주겠다 -->
      <property name="accService" ref="accService"/>
</bean>

   <bean  id="methodResolver" class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver" >
      <property  name="mappings" >
        <props>
        	<!-- /account/sendMoney.do로 요청 시 sendMoney 메소드 호출 -->
          <prop key="/account/sendMoney.do">sendMoney</prop>
       </props>
      </property>
   </bean>
   
   
   <bean id="urlMapping"
       class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
      <property name="mappings">
         <props>
         	<!-- /account/*.do 로 요청시 accController 빈 실행 -->
            <prop key="/account/*.do">accController</prop>
         </props>
      </property>
   </bean>
   
</beans>

action-mybatis.xml

스프링의 DataSourceTransactionManager클래스를 이용해 트랜잭션 처리빈을 생성
-> DataSource 속성에 dataSource 빈을 주입하여 데이터 베이스 연동시 트랜잭션을 적용한다
-> txManager 빈에 <tx:annotation-driven> 태그를 설정해 어노테이션을 적용할 수 있게 한다

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

	<bean id="propertyPlaceholderConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<value>/WEB-INF/config/jdbc.properties</value>
		</property>
	</bean>


	<bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
		<property name="driver" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
	</bean>

	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="mapperLocations"
			value="classpath:mybatis/mappers/*.xml" />
	</bean>

	<bean id="sqlSession"
		class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory"></constructor-arg>
	</bean>
	
	<bean id="accDAO" class="com.spring.account.AccountDAO">
		<property name="sqlSession" ref="sqlSession" />
	</bean>
	
	<!-- DataSourceTransactionManager 클래스를 이용해 dataSource 빈에 트랜잭션을 적용 -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- 애너테이션을 사용하여 트랜잭션을 적용할 것이기 때문에, txManager빈을 설정 -->
	<tx:annotation-driven transaction-manager="txManager"/>
	
</beans>

account.xml

sql문이 있는 곳!

<?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="mapper.account">
	<update id="updateBalance1" >
	<![CDATA[
		update cust_account
        set balance=balance-5000000
        where accountNo = '70-490-930'
    ]]>
	</update>
	
	<update id="updateBalance2">
	<![CDATA[
		update cust_account
        set balance=balance+5000000
        where
        accountNo ='70-490-912'
    ]]> 
	</update>
	
</mapper>

action-service.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">


	<bean id="accService" class="com.spring.account.AccountService">
		<property name="accDAO" ref="accDAO"/>
	</bean>
</beans>

MemberVO.java

package com.spring.account;

public class AccountVO {
	private String accountNo;
	private String custName;
	private int balance;
	
	public AccountVO() {
	}

	public AccountVO(String accountNo, String custName, int balance) {
		this.accountNo = accountNo;
		this.custName = custName;
		this.balance = balance;
	}

	public String getAccountNo() {
		return accountNo;
	}

	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}

	public String getCustName() {
		return custName;
	}

	public void setCustName(String custName) {
		this.custName = custName;
	}

	public int getBalance() {
		return balance;
	}

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

AccountController.java

  • 컨트롤러에서는 속성 accService에 빈을 주입하기 위해서 setter를 구현
  • /account/sendMoney.do 로 요청시 sendMoney() 메소드를 호출해 계좌 이체 작업을 수행한다
package com.spring.account;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

public class AccountController  extends MultiActionController  {
	private AccountService accService ;
	
	// setter 작성
	public void setAccService(AccountService accService){
		this.accService = accService;
	}
	
	public ModelAndView sendMoney(HttpServletRequest request, HttpServletResponse response) throws Exception {
		ModelAndView mav = new ModelAndView();
		
		// 금액 이체
		accService.sendMoney();
		mav.setViewName("result");
		
		return mav;
	}
}

AccountService.java

  • 서비스 클래스의 메소드는 단위 기능을 수행하기 때문에
    @Transactional 어노테이션을 서비스 클래스에 적용하여
    메소드 별로 트랜잭션을 적용
package com.spring.account;

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

// AccountService 믈래스의 모든 메소드에게 트랜잭션 적용
// REQUIRED : 진행중인 트랜잭션이 있는 경우 해당 트랜잭션 사용
//	    : 트랜젝션이 없으면 새로운 트랜잭션을 생성하고 그걸 디폴트(기본)으로 사용해라
@Transactional(propagation = Propagation.REQUIRED)
public class AccountService {

	private AccountDAO accDAO;
	// setter
	public void setAccDAO(AccountDAO accDAO) {
		this.accDAO = accDAO;
	}
	
	
	public void sendMoney() {
		accDAO.updateBalance1();
		accDAO.updateBalance2();
	}
}

AccountDAO.java

각 예금자 계좌를 갱신하는 메소드 구현

package com.spring.account;

import org.apache.ibatis.session.SqlSession;
import org.springframework.dao.DataAccessException;

public class AccountDAO {
	private SqlSession sqlSession;
	//setter
	public void setSqlSession(SqlSession sqlSession) {
		this.sqlSession = sqlSession;
	}
	
	
	// 첫번째 update문을 실행해 '홍길동'계좌에서 차감
	public void updateBalance1() throws DataAccessException {
		sqlSession.update("mapper.account.updateBalance1");
	}
	
	// 두번째 update문을 실행해 '김유민'계좌에서 차감
	public void updateBalance2() throws DataAccessException {
		sqlSession.update("mapper.account.updateBalance2");
	}
}

result.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" isELIgnored="false"  %>
 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>
<% request.setCharacterEncoding("UTF-8"); %>


<html>
<head>
<meta charset="UTF-8">
<title>결과창</title>
</head>
<body>
	<h1>송금이 완료되었습니다.</h1>
</body>
</html>

실행

실행하는데 계속 에러가 나서보니 서버 문제였다. 정확히는 안쓰는 애들까지 다 서버 실행에 연결해놔서 거기서 난 오류가 계속 이어지며 난리난 것. 그래서 Add and Remove 들어가서 쓰는 것만 남기고 다 정리했더니 나온다!

서버를 돌린 후 http://localhost:8700/pro25/account/sendMoney.do 를 주소창에 검색해주면 아래와 같이 jsp에 작성한 대로 문구가 출력된다

그리고 연결해놓은 오라클에 접속해 테이블을 확인하면 이런 식으로 나온다. 몇번 새로고침 해줬더니 지정해준만큼 (5000000원)
홍길동에게서는 빠져나가고
김유민에게는 들어간 모습을 확인할 수 있었다!

만약 트랜잭션을 적용하지 않았다면?

sql문에서 김유민의 계좌번호가 잘못 적혔다고 생각해보자. 당연히 실행시 오류가 난다. 하지만 트랜잭션을 적용한것과 적용하지 않은 경우 sql에서의 변화차이가 눈여겨볼 지점인데

  1. 만약 트랜잭션을 적용하지 않았다면 오라클에서 select * from cust_account; 을 실행시켜 잔고를 확인했을 때, 아래와 같이 맞게 작성한 홍길동의 경우는 실행되고, 틀리게 작성된 김유민의 잔고는 그대로인 것을 확인할 수 있다.

  2. 트랜잭션을 적용한 경우 오라클에서 동일하게 실행문을 돌려보면 이런 식으로 둘 다 변화없는 것을 확인할 수 있다.

-> 말 그대로 '묶어서' 처리하는 것이다. 묶은 둘 중 하나라도 오류가 있으면 실행문 자체를 무시해버린다

0개의 댓글