관리 메뉴

거니의 velog

(17) 스프링 트랜잭션 기능 사용하기 2 본문

Java_Spring Framework part1

(17) 스프링 트랜잭션 기능 사용하기 2

Unlimited00 2023. 11. 11. 14:47

4. 스프링 트랜잭션 기능 적용해 계좌 이체 실습하기

* 이번에는 계좌 이체 기능을 스프링의 트랜잭션 기능을 적용하여 실습해 보자.

* 먼저 SQL Developer 로 예금자 계좌 정보를 저장하는 테이블을 생성한다. 그리고 예금자의 계좌 정보를 다음과 같이 추가한다.

create table cust_account(
    accountNo varchar2(20) primary key, -- 계좌 번호
    custName varchar2(50), -- 예금자
    balance number(20,4) -- 계좌 잔고
);

insert into cust_account(accountNo, custName, balance) 
values('70-490-930', '홍길동', 10000000);
insert into cust_account(accountNo, custName, balance) 
values('70-490-911', '김유신', 10000000); -- 홍길동과 김유신의 계좌 정보를 생성한다.

commit; -- insert 문 실행 후 반드시 커밋을 해야 한다.

select * from cust_account;

* 다음은 계좌 정보를 조회한 결과이다. 모든 예금자의 계좌 잔고는 10,000,000 원이다.

테이블에 저장된 예금자들의 잔고 현황


(1) 트랜잭션 관련 XML 파일 설정하기

1. 스프링과 연동해 트랜잭션 기능을 구현하는 데 필요한 XML 파일들을 다음과 같이 준비한다. web.xml은 이전의 것을 복사해 붙여 넣는다.

2. 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>
        <property name="accService" ref="accService" /> <!-- accService 빈을 주입한다. -->
    </bean>

    <bean id="methodResolver" class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
        <property name="mappings">
            <props>
                <prop key="/account/sendMoney.do">sendMoney</prop> <!-- /account/sendMoney.do 요청 시 sendMoney 메서드를 호출한다. -->
            </props>
        </property>
    </bean>

    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/account/*.do">accController</prop> <!-- /account/*.do로 요청 시 accController 빈을 실행한다. -->
            </props>
        </property>
    </bean>

</beans>

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

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean> <!-- DataSourceTransactionManager 클래스를 이용해 dataSource 빈에 트랜잭션을 적용한다. -->

    <tx:annotation-driven transaction-manager="txManager" /> <!-- 애너테이션을 사용하여 트랜잭션을 적용하기 위해 txManager 빈을 설정한다. -->

</beans>

4. action-service.xml에서는 AccountService의 accDAO 속성에 accDAO 빈을 주입하도록 구현한다.

<?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> <!-- accService 빈의 속성에 accDAO 빈을 주입한다. -->
    
</beans>

(2) 마이바티스 관련 XML 파일 설정하기

* 이번에는 계좌 이체 기능을 SQL문으로 구현한 매퍼 파일을 설정해 보자.

1. 다음과 같이 매퍼 파일인 account.xml을 준비한다.

2. 매퍼 파일에서는 두 개의 update 문으로 두 명의 계좌 잔고를 갱신한다.

<?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"> <!-- 잔고를 5000000원 감액한다. -->
        <![CDATA[
         update cust_account
         set balance=balance-5000000
         where
           accountNo = '70-490-930'
       ]]>
    </update>

    <update id="updateBalance2"> <!-- 잔고를 5000000원 증액한다. -->
        <![CDATA[
         update cust_account
         set balance=balance+5000000
         where
          accountNo ='70-490-911'
       ]]>
    </update>

</mapper>

(3) 트랜잭션 관련 자바 클래스와 JSP 파일 구현하기

1. 계좌 이체 기능에 필요한 자바 파일과 JSP 파일들을 다음과 같이 준비한다.

2. 컨트롤러에서는 속성 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 ; 
	public void setAccService(AccountService accService){
		this.accService = accService;
	} // 속성 accService에 빈을 주입하기 위해 setter를 구현한다.
	
	public ModelAndView sendMoney(HttpServletRequest request, HttpServletResponse response) throws Exception {
		ModelAndView mav=new ModelAndView();
		accService.sendMoney(); // 금액을 이체한다.
		mav.setViewName("result");
		return mav;
	}
	   
}

3. AccountService 클래스를 다음과 같이 작성한다. 서비스 클래스의 메서드는 단위 기능을 수행하므로 @Transactional 애너테이션을 서비스 클래스에 적용해 메서드별로 트랜잭션을 적용한다.

package com.spring.account;

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

// @Transactional을 이용해 AccountService 클래스의 모든 메서드에 트랜잭션을 적용한다.
@Transactional(propagation=Propagation.REQUIRED)
public class AccountService {
	
	private AccountDAO accDAO;

	public void setAccDAO(AccountDAO accDAO) {
		this.accDAO = accDAO;
	} // 속성 accDAO에 빈을 주입하기 위해 setter를 구현한다.

	public void sendMoney() throws Exception {
		accDAO.updateBalance1();
		accDAO.updateBalance2();
	} // sendMoney() 메서드 호출 시 accDAO의 두 개의 SQL문을 실행한다.
	
}

4. AccountDAO 클래스에서는 각 예금자 계좌를 갱신하는 메서드를 구현한다.

package com.spring.account;

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

public class AccountDAO {
	
	private SqlSession sqlSession;

	public void setSqlSession(SqlSession sqlSession) {
		this.sqlSession = sqlSession;
	} // 속성 sqlSession에 빈을 주입하기 위해 setter를 구현한다.

	public void updateBalance1() throws DataAccessException {
		sqlSession.update("mapper.account.updateBalance1");
	} // 첫 번째 update 문을 실행해 홍길동 계좌에서 5000000원을 차감한다.
	
	public void updateBalance2() throws DataAccessException {
		sqlSession.update("mapper.account.updateBalance2");
	} // 두 번째 update 문을 실행해 김유신 계좌에 5000000원을 증액한다.

}

5. 트랜잭션을 적용하지 않은 경우와 적용한 경우의 실행 결과를 각각 확인해 보자. 먼저 다음의 주소로 요청하여 정상적으로 계좌 이체가 이루어진 경우의 결과를 확인한다.

- http://localhost:8090/pro25/account/sendMoney.do

6. SQL Developer로 조회하면 홍길동의 계좌에서 김유신의 계좌로 5,000,000 만원이 이체된 것을 확인할 수 있다.

7. 이번에는 트랜잭션을 적용하지 않은 경우의 실행 결과를 보자. AccountService.java에서 다음 부분을 주석 처리한다.

/*@Transactional(propagation=Propagation.REQUIRED)*/
public class AccountService {

8. 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-911
       ]]>
    </update>
    <!-- 계좌 번호 양쪽 작은 따옴표(')를 삭제하여 오류를 발생시킨다. -->

</mapper>

9. SQL Developer에서 예금자들의 잔고를 원래대로 되돌린 후, 즉 10,000,000 만원으로 갱신한 후 브라우저에서 다음의 주소로 요청하면 다음과 같은 오류가 발생한다.

UPDATE cust_account
SET
    balance = 10000000
WHERE
    accountno = '70-490-930';
    
UPDATE cust_account
SET
    balance = 10000000
WHERE
    accountno = '70-490-911';

commit;

select * from cust_account;

- http://localhost:8090/pro25/account/sendMoney.do

10. SQL Developer로 각 계좌 잔고를 조회해 보면 홍길동의 잔고는 5,000,000원이 감소했으나, 김유신의 잔고는 10,000,000원 그대로인 것을 확인할 수 있다.

11. 트랜잭션을 적용한 후 브라우저에서 요청한 결과를 확인하기에 앞서 원래대로 주석을 해제한다. 그리고 SQL Developer로 다시 예금자들의 잔고를 10,000,000원으로 변경한다.

@Transactional(propagation=Propagation.REQUIRED)
public class AccountService {
UPDATE cust_account
SET
    balance = 10000000
WHERE
    accountno = '70-490-930';
    
UPDATE cust_account
SET
    balance = 10000000
WHERE
    accountno = '70-490-911';

commit;

select * from cust_account;

12. 다음의 주소로 요청하면 또 다시 오류가 발생한다.

- http://localhost:8090/pro25/account/sendMoney.do

13. SQL Developer로 각 계좌 잔고를 조회한다. 이번에는 트랜잭션이 적용되었으므로 김유신의 잔고는 물론이고 오류가 발생하지 않은 홍길동의 잔고도 원래의 금액으로 롤백이 된다.

* 대부분의 애플리케이션에서는 이처럼 Service 클래스에 트랜잭션을 적용한다. 사실 우리가 이전에 구현한 회원 기능 프로그램에서 MemberServiceImpl 클래스에도 트랜잭션 애너테이션을 적용했었다.

@Transactional(propagation=Propagation.REQUIRED)
public class MemberServiceImpl  implements MemberService{
	...
}

* 지금까지 애너테이션을 이용해 기본적인 트랜잭션 기능을 알아봤다. 더 세부적인 스프링 트랜잭션 기능은 전문적인 스프링 서적을 참고하길 바란다.