Programmer's Progress

스프링 프레임워크 트랜잭션 기능 본문

Servlet + JSP/Self Learning

스프링 프레임워크 트랜잭션 기능

Blanc et Noir 2021. 12. 31. 13:42

본격적으로 백엔드 개발을 공부하기 시작하면서 상당히 많은 SQL문을 작성했었다.

가령 회원의 목록을 질의하는 것부터, 회원 추가, 삭제, 수정, 계층형 게시판, 답글 등등

심지어는 3중 SELECT문을 활용하기도 했었다.

 

 

 

그 외에도 여러 SQL문을 한 번에 수행해야 할 때도 있었다. 가령, 게시판에 새로운 글을 추가할 때에는 반드시

현존하는 게시글중에서 가장 큰 번호를 가진 게시글의 번호보다+1된 값을 새 게시글의 번호로 사용해야 할 때가 있었다.

그런데 만약 우연하게도 동시에 두 사용자가 게시글 작성을 했다면 어떻게 될까?

 

 

 

물론 단일 SQL 질의에 대해서는 DBMS가 동시성제어를 통해 마치 OS에서 프로세스 간의 공유 자원을

동기화하기 위해 세마포어나 락을 사용하듯, DBMS도 마찬가지로 ACID의 원리 중 하나인 독립성

즉, 어떤 작업이 수행될때는 마치 그 시점에는 자기 자신만 수행되는 것처럼 여겨지도록 해야 할 것이다.

그렇게 해야만 공유 데이터에 대해서 서로 같은 값으로 알 수 있도록 보장할 수 있다.

 

 

 

 

 

예를 들어 은행을 생각해보자.

현재 은행에는 입금과 출금기능밖에는 존재하지 않는다.

이러한 입금과 출금은 아래와 같은 단일 SQL문으로 구현할 수 있을 것이다.

UPDATE ACCOUNT
SET BALANCE = BALANCE + AMOUNT
WHERE ACCOUNTNO = '입금계좌'
UPDATE ACCOUNT
SET BALANCE = BALANCE - AMOUNT
WHERE ACCOUNTNO = '출금계좌'

 

 

 

그런데 시중의 은행은 송금기능도 제공한다.

이러한 송금기능은 어떻게 구현할 수 있을까?

바로 앞서 구현한 입금과 출금 기능을 동시에 사용하면 구현 가능하다.

 

 

1 - A의 계좌에서 잔고를 AMOUNT만큼 감소시키고

2 - B의 계좌에서 잔고를 AMOUNT만큼 증가시키는 작업을 진행하면 된다.

 

 

 

 

DBMS에서 제공하는 원자성은 1, 2와 같은 하나의 SQL문 단위로 적용된다.

그런데, 1과 2를 조합하여 송금 기능을 구현했을 때

만약 1의 기능 수행 후, 2의 기능 수행 시 오류가 발생하여 DBMS의 원자성에 의해

처리가 완료되지 않았다고 생각해보자.

 

 

 

오류 발생을 일부러 일으키고자 편의상 B의 잔고를 증가시키고

A의 잔고를 감소시키는 순서로 작업 순서를 변경했다.

각 고객의 잔고는 반드시 0 이상이어야 한다는 CHECK 제약조건이 있기 때문이다.

 

 

 

 

 

1 -  B의 계좌에서 잔고를 1000000 증가시킨다.

성공적으로 수행된다.

 

2 - A의 계좌에서 잔고를 1000000 감소시킨다.

잔고가 0보다 작은 음수가 되므로 오류가 발생하여, commit 되지 않는다.

 

 

3 - 결과적으로 B의 계좌 잔고는 1000000만큼 증가했지만

A의 계좌 잔고는 그대로다.

 

 

즉, 돈이 복사가 된다 (?)

 

 

 

 

 

 

 

각 고객들의 초기 계좌 상태, 모두 500000원을 보유하고 있다.

 

이 상태에서 송금을 시도해보자.

 

 

 

 

qweqweqwe계좌에서 asdasdasd계좌로 1000000원을 송금한다.

 

 

 

 

 

 

당연하게도 CHECK제약조건에 의한 오류가 발생한다. 잔고가 음수가 되기 때문이다.

당연하게도 오류가 발생한다.

qweqweqwe계좌에는 50만 원밖에 없는데 그 이상인 100만 원을 송금하려 했기 때문이다.

이렇게 송금에 실패하면 당연히 asdasdasd계좌의 잔고는 그대로 여야 한다

 

 

 

 

 

그런데 돈이 복사가 된다 (?)

그런데 돈이 복사가 되었다.

 

 

 

 

 

MVC패턴으로 구성된 서비스의 컨트롤러 소스코드 일부를 보자.

	public ModelAndView transfer(HttpServletRequest request, HttpServletResponse response) throws Exception{
		request.setCharacterEncoding("utf-8");
		response.setContentType("text/html;charset=utf-8");
		ModelAndView mav = new ModelAndView("redirect:/account/list.do");
		
		String sender = request.getParameter("sender");
		String receiver = request.getParameter("receiver");
		int amount = Integer.parseInt(request.getParameter("amount"));
		accountService.transfer(sender,receiver,amount);
		return mav;
	}

위와 같이 accountService 객체의 transfer메서드를 호출하고 있다.

 

 

 

 

 

 

아래는 accountService객체의 클래스, AccountService의 소스코드 일부다.

	public void transfer(String senderNO, String receiverNO, int amount) {
		accountDAO.deposit(receiverNO, amount);
		accountDAO.withdraw(senderNO, amount);	
	}

accountDAO 객체의 입, 출금 메소드를 각각 호출하여 송금을 구현하는 것을 알 수 있다.

 

 

 

 

 

 

아래는 AccountDAO클래스의 소스코드 일부다.

	@Override
	public void deposit(String accountNO, int amount) {
		HashMap map = new HashMap();
		map.put("accountNO", accountNO);
		map.put("amount", amount);
		sqlSession.update("AccountMapper.deposit", map);
	}
	@Override
	public void withdraw(String accountNO, int amount) {
		HashMap map = new HashMap();
		map.put("accountNO", accountNO);
		map.put("amount", amount);
		sqlSession.update("AccountMapper.withdraw", map);
	}

마이 바티스 프레임워크를 이용해 작업을 수행한다.

 

 

 

각각의 독립적인 SQL문을 수행할 때는 DBMS가 원자성을 보장해주지만

그러한 SQL문을 여럿 조합하여 기능을 구현했을 때 그때의 원자성을 보장하지는 않는다.

이것을 스프링 프레임워크에서 제공하는 트랜잭션 기능을 이용하면 보장할 수 있게 된다.

 

 

 

 

 

 

<?xml version="1.0" encoding="UTF-8"?>
<beans
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://www.springframework.org/schema/beans">
	
	<bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<value>/WEB-INF/config/jdbc.properties</value>
		</property>
	</bean>
	
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	<tx:annotation-driven transaction-manager="txManager"/>
	
	<bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
		<property name="driver" value="${jdbc.driver}"/>
		<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="configLocation" value="classpath:mybatis/config/config.xml"/>
		<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>
	</bean>
	
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory"/>
	</bean>
	
	<bean id="accountDAO" class="dao.AccountDAOImpl">
		<property name="sqlSession" ref="sqlSession"/>
	</bean>
	
	<bean id="accountService" class="service.AccountServiceImpl">
		<property name="accountDAO" ref="accountDAO"/>
	</bean>
	
</beans>

마이 바티스 프레임워크와 스프링 프레임워크를 연동하기 위한 XML 파일의 내용이다.

 

 

 

	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	<tx:annotation-driven transaction-manager="txManager"/>

이 부분이 바로 트랜잭션 매니저 빈을 생성하고, 설정하는 코드다.

 

 

 

package service;

import java.util.List;

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

import dao.AccountDAO;

@Transactional(propagation=Propagation.REQUIRED)
public class AccountServiceImpl implements AccountService {

	AccountDAO accountDAO;
	public void setAccountDAO(AccountDAO accountDAO) {
		this.accountDAO = accountDAO;
	}
	@Override
	public void transfer(String senderNO, String receiverNO, int amount) {
		accountDAO.deposit(receiverNO, amount);
		accountDAO.withdraw(senderNO, amount);	
	}
	@Override
	public void deposit(String accountNO, int amount) {
		accountDAO.deposit(accountNO, amount);		
	}
	@Override
	public void withdraw(String accountNO, int amount) {
		accountDAO.withdraw(accountNO, amount);		
	}
	@Override
	public List list() {
		return accountDAO.list();
	}
	@Override
	public List search(String accountNO) {
		return accountDAO.search(accountNO);
	}

}

서비스 클래스에서는 어노테이션을 이용해 트랜잭션을 설정한다.

 

 

 

 

 

이제 제대로 트랜잭션 기능이 적용되었는지 확인해보자.

똑같이 100만원을 송금한다.

 

.

 

 

여전히 오류가 발생한다. 이는 CHECK 제약조건에 의한 것이다

 

여전히 qweqweqwe계좌에는 50만 원 밖에 없으므로, 100만 원을 송금했다가는 잔고가 음수가 된다.

즉, CHECK제약조건을 위반했기에 오류가 발생한다.

 

 

 

 

 

그러나 잔고는 그대로다.

잔고에는 변함이 없다.

 

 

1 - asdasdasd 계좌의 잔고를 1000000 증가시킨다.

 

2 - qweqweqwe 계좌의 잔고를 1000000 감소시킨다.

CHECK제약조건 위반으로 오류가 발생한다.

 

3 - 결과적으로 이전에 수행했던 asdasdasd 계좌의 잔고를 롤백시킨다.

 

 

 

이처럼 DAO클래스에서는 하나의 독립된 SQL문을 수행하기에 DBMS가 원자성을 보장해주지만

Service 클래스의 경우 그러한 독립된 SQL을 조합하여 작업을 수행하기에 원자성 보장이 되지 않는다.

이를 스프링 프레임워크에서 제공하는 트랜잭션 기능을 사용하면 원자성을 보장할 수 있음을 배웠다.

Comments