본문 바로가기
Spring

[Spring] @Transactional 기본 사용법

by Amy IT 2022. 8. 27.

 

목차

     

    트랜잭션 (Transaction)

    트랜잭션(Transaction)은 더 이상 쪼개질 수 없는 하나의 작업 단위을 의미합니다. 예를 들어 '계좌 이체'라는 행위를 따져보면, '출금'과 '입금'이라는 각각의 작업이 하나의 단위를 이루고 있습니다. 이때 출금은 정상적으로 처리되었는데 입금하는 과정에서 예외가 발생하는 경우를 생각해 볼 수 있습니다. 이미 계좌에서 돈이 빠져나갔는데 상대방의 계좌에 돈이 입금되지 않는다면 큰 문제가 될 것입니다. 이 때문에 출금과 입금을 하나의 트랜잭션으로 관리하여 문제가 발생한 경우 모든 작업을 rollback하는 것이 필요합니다. 이처럼 여러 작업을 진행하다가 문제가 생기면 모든 작업을 이전 상태로 rollback하기 위해 사용되는 것이 트랜잭션입니다.

     

    ACID 원칙

    원칙 설명
    Atomicity (원자성) 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다. 예를 들어 어떤 트랜잭션이 A와 B로 구성된다면 A와 B의 처리 결과는 항상 동일해야 한다.
    Consistency (일관성) 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야만 한다. 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야 한다.
    Isolation (격리) 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야 한다.
    Durability (영속성) 트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 한다.

     

     

    @Transactional 어노테이션

    Spring에서는 클래스나 인터페이스 또는 메소드에 부여할 수 있는 @Transactional이라는 어노테이션을 제공하고 있습니다. @Transactional 어노테이션이 붙으면 트랜잭션 관리 대상이 됩니다. 선언적 트랜잭션(declarative transaction) 방식이라고도 불리는데, 코드 외부에서 트랜잭션의 기능을 부여하고 속성을 지정할 수 있도록 하기 때문입니다. @Transactional 어노테이션이 지정된 곳에서 예외가 발생할 경우 하나의 트랜잭션 내 모든 작업을 rollback시켜 줍니다. 단, 해당 예외가 Unchecked Exception(Runtime Exception)이라면 자동으로 rollback되지만, Checked Exception은 rollback되지 않습니다. Checked Exception을 롤백시키기 위해서는 @Transactional의 rollbackFor 속성으로 해당 예외를 지정해 주어야 합니다.

     

     

    @Transactional을 사용하기 위한 환경설정

    dependency 추가

    spring-jdbc 라이브러리와 그외 DataSource 사용을 위한 DBCP2 라이브러리, 오라클 DBMS와 연동하기 위한 오라클 라이브러리, MyBatis 라이브러리 두 가지를 추가했습니다. 저는 spring-tx 라이브러리는 기본으로 설정되어 있어서 추가하지 않았으나, 기본으로 설정되어 있지 않은 분들은 추가해야 합니다.

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>4.3.22.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-dbcp2</artifactId>
        <version>2.5.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 -->
    <dependency>
        <groupId>com.jslsolucoes</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0.1.0</version>
    </dependency>
    <!-- 필요시 추가 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring-framework.version}</version>
    </dependency>

     

    properties 파일

    DB 연동을 위해 필요한 정보를 properties 파일로 저장해 줍니다. 저는 오라클의 scott 계정을 사용하기 위해 db.properties 파일을 다음과 같이 작성하였습니다.

    db.driver=oracle.jdbc.driver.OracleDriver
    db.url=jdbc:oracle:thin:@localhost:1521:xe
    db.username=scott
    db.password=tiger

     

    applicationContext.xml

    1. component-scan : 빈이 자동 등록 및 생성되도록 컴포넌트 스캔 대상 패키지를 지정합니다.
    2. properties 파일 등록 : DB 연동을 위한 정보를 저장해 놓은 properties 파일의 경로를 등록합니다.
    3. DataSource 등록 : BasicDataSource 빈을 등록하면서 properties 파일로부터 데이터를 가져와 프로퍼티로 설정해 줍니다. DataSource는 DB와 Connection을 맺고 일정량의 Connection을 미리 생성해서 저장소에 저장해 두었다가 필요시 제공하는 Connection Pooling 역할을 수행하는 객체입니다. 객체가 소멸될 때 close() 메소드를 호출하도록 destroy-method 속성을 설정합니다.
    4. TransactionManager 등록 : DataSourceTransactionManager를 등록하며 DataSource를 주입합니다. TransactionManager는 트랜잭션을 처리하는 객체이며, DataSourceTransactionManager는 JDBC 기반 라이브러리로 DB에 접근할 때 이용하는 TransactionManager입니다. 
    5. Transaction annotation 활성화 : 어노테이션을 기반으로 하는 트랜잭션 설정을 활성화합니다.
    <?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:context="http://www.springframework.org/schema/context"
    	xmlns:tx="http://www.springframework.org/schema/tx"
    	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-4.3.xsd
    		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
    
    <!-- 1. component-scan -->
    <context:component-scan base-package="com.*"></context:component-scan>
    <!-- 2. properties 파일 등록 -->
    <context:property-placeholder location="classpath:config/db.properties"/>
    <!-- 3. DataSource 등록 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    	<property name="driverClassName" value="${db.driver}"></property>
    	<property name="url" value="${db.url}"></property>
    	<property name="username" value="${db.username}"></property>
    	<property name="password" value="${db.password}"></property>
    </bean>
    
    <!-- Transaction 설정 -->
    <!-- 4. Transaction 처리용 TransactionManager 등록 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 5. Transaction annotation 활성화 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <!-- MyBatis 설정 -->
    <!-- 4. SqlSessionFactoryBean 등록 - mapper 지정, alias 지정 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    	<property name="dataSource" ref="dataSource"></property>
    	<property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    	<property name="typeAliasesPackage" value="com.dto"></property>
    </bean>
    <!-- 5. SqlSessionTemplate 등록 -->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    	<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"></constructor-arg>
    </bean>
    
    </beans>

     

    어노테이션을 사용하여 트랜잭션 처리를 하기 위한 환경설정이 완료되었습니다. 이제 필요한 메소드나 클래스 또는 인터페이스의 선언부에 @Transactional을 사용하여 트랜잭션 처리를 할 수 있습니다.

     

     

    실습

    실습을 위해 간단하게 주문하는 과정을 구현해 보겠습니다. 주문이라는 행위는 상품 수량 감소와 주문 추가라는 두 가지의 작업이 하나의 단위를 이루고 있습니다. 트랜잭션 처리를 하지 않았는데 이 과정에서 예외가 발생하면 상품 수량은 감소했으나 주문은 추가되지 않은 상황이 발생할 수 있습니다. 주문하는 과정을 하나의 트랜잭션으로 처리하면 이러한 문제를 방지할 수 있습니다.

     

    프로젝트 구조

     

    SQL

    create table test_product
    ( pcode  varchar2(10) primary key,
      pname varchar2(10),
      price number(10),
      quantity number(10) );
    
    create table test_order
    ( order_id number(10) primary key,
      pcode  varchar2(10) references test_product(pcode),
      quantity number(10) ); 
    create sequence test_order_seq;
    
    insert into test_product( pcode,pname,price,quantity) values ( 'p01','노트북',1000, 10 );
    insert into test_product( pcode,pname,price,quantity) values ( 'p02','iPhone',500, 5 );
    insert into test_product( pcode,pname,price,quantity) values ( 'p03','에어팟',300, 20 );
    commit;

     

    TestMain.java

    먼저 전체 상품 목록을 조회하고 주문하는 과정을 거친 후, 다시 전체 상품 목록을 조회하고 전체 주문 목록을 조회하도록 하고 있습니다. 주문 과정에서 예외가 발생한 경우 catch문을 실행해 '주문 실패' 메시지가 출력되도록 했습니다. 

    public class TestMain {
    	public static void main(String[] args) {
    		ApplicationContext ctx = new GenericXmlApplicationContext("classpath:config/applicationContext.xml");
    		ProductService service = ctx.getBean("productService", ProductService.class);
    		//전체 상품 목록
    		List<ProductDTO> prodlist = service.selectProduct();
    		for (ProductDTO dto : prodlist) {
    			System.out.println(dto);
    		}
    		//주문하기
    		System.out.println("p01상품 5개 주문하기");
    		try {
    			service.order("p01", 5);
    			System.out.println("주문 성공");
    		} catch (Exception e) {
    			System.out.println("주문 실패, 에러 발생하여 롤백처리");
    		}
    		//전체 상품 목록
    		prodlist = service.selectProduct();
    		for (ProductDTO dto : prodlist) {
    			System.out.println(dto);
    		}
    		//전체 주문 목록
    		List<OrderDTO> orderList = service.selectOrder();
    		for (OrderDTO dto : orderList) {
    			System.out.println(dto);
    		}
    		System.out.println("======= 프로그램 종료 =======");
    	}
    }

     

    ProductService.java

    Service 클래스에서 트랜잭션 처리가 필요한 메소드의 선언부에 @Transactional 어노테이션을 지정합니다. 이 메소드가 실행되는 과정에서 예외가 발생할 경우 TransactionManager가 예외를 감지하고 rollback하여 처리하게 됩니다.

    @Service
    public class ProductService {
    	@Autowired
    	private ProductDAO dao;
    	//전체 상품 목록
    	public List<ProductDTO> selectProduct() {
    		return dao.selectProduct();
    	}
    	//전체 주문 목록
    	public List<OrderDTO> selectOrder() {
    		return dao.selectOrder();
    	}
    	//주문하기
    	@Transactional
    	public void order(String pcode, int quantity) {
    		dao.order(pcode, quantity);
    	}
    }

     

    ProductDAO.java

    주문하는 과정은 상품 수량 감소와 주문 추가라는 두 가지 작업으로 이루어집니다. 주문 추가 과정에서 Mapper의 SQL id를 잘못 지정하여 예외를 발생시켜 보겠습니다. 상품 수량 감소는 정상적으로 이루어지기 때문에 'Product 레코드가 변경되었다'는 메시지가 출력되어야 합니다. 

    @Repository
    public class ProductDAO {
    	@Autowired
    	private SqlSessionTemplate session;
    	//전체 상품 목록
    	public List<ProductDTO> selectProduct() {
    		return session.selectList("ProductMapper.selectProduct");
    	}
    	//전체 주문 목록
    	public List<OrderDTO> selectOrder() {
    		return session.selectList("ProductMapper.selectOrder");
    	}
    	//주문하기
    	public void order(String pcode, int quantity) {
    		ProductDTO dto = new ProductDTO();
    		dto.setPcode(pcode);
    		dto.setQuantity(quantity);
    		//상품 수량 감소
    		int n = session.update("ProductMapper.updateProduct", dto);
    		System.out.println(n+"개의 Product 레코드 변경");
    		//주문 추가
    		int n2 = session.insert("ProductMapper.insertOrderxx", dto);
    		System.out.println(n2+"개의 Order 레코드 추가");
    	}
    }

     

    ProductMapper.xml

    <?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="ProductMapper">
    
    <select id="selectProduct" resultType="ProductDTO">
    	select * from test_product
    </select>
    
    <select id="selectOrder" resultType="OrderDTO">
    	select * from test_order
    </select>
    
    <update id="updateProduct" parameterType="ProductDTO">
    	update test_product
    	set quantity=quantity-${quantity}
    	where pcode=#{pcode}
    </update>
    
    <insert id="insertOrder" parameterType="ProductDTO">
    	insert into test_order 
    		(order_id, pcode, quantity)
    	values
    		(test_order_seq.nextval, #{pcode}, ${quantity})
    </insert>
    
    </mapper>

     

    실행 결과

    주문하는 과정에서 상품 수량 감소는 정상적으로 이루어져 'Product 레코드가 변경되었다'는 메시지가 출력되지만, 이후 주문을 추가할 때 예외가 발생하여 'Order 레코드가 추가되었다'는 메시지 출력 없이 Main의 catch문의 '주문 실패' 메시지가 출력된 것을 확인할 수 있습니다. 다시 전체 상품 목록을 조회했을 때 "p01" 상품의 수량이 변경되지 않은 것을 보면 트랜잭션 처리가 올바르게 된 것을 확인 수 있습니다.

     

    Mapper의 SQL id 지정을 제대로 하고 다시 테스트해 보겠습니다.

    "p01" 상품의 수량도 변경되고, 주문 테이블에 주문 추가도 정상적으로 이루어진 것을 확인할 수 있습니다.

     

    이상으로 @Transactional 어노테이션을 사용하기 위한 기본적인 환경설정과 간단한 예제를 살펴보았습니다.

     

     

    참고

     

    'Spring' 카테고리의 다른 글

    [Spring] @RequestMapping - 요청 주소 매핑하기  (0) 2022.09.01
    [Spring] 스프링 MVC의 기본 구조  (0) 2022.08.31
    [Spring] DB 연동 - MyBatis  (0) 2022.08.27
    [Spring] DB 연동 - JDBC  (0) 2022.08.25
    [Spring] SpEL 사용법  (0) 2022.08.22

    댓글