Keep going

스프링에서 트랜잭션 관리 본문

Records/Spring Framework

스프링에서 트랜잭션 관리

코딩천재홍 2021. 3. 22. 13:55

 

  • 비즈니스에서는 쪼개질 수 없는 하나의 단위 작업을 말할 때 트랜잭션이라는 용어를 사용한다.
원자성 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다. 좀 더 쉽게 말하면 어떤 트랜잭션이 A와 B로 구성된다면 항상 A, B의 처리 결과는 동일한 결과이어야 한다. 즉 , A는 성공했지만, B는 시래할 경우 A,B는 원래 상태로 되돌려져야 한다. 
일관성 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야 한다. 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야 한다.
격리 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야 한다.
영속성 트랜잭션이 성공적으로 처리되면, 그 결관느 영속적으로 보관되어야 한다.

 

  • 예를 들어, 트랜잭션의 가장 흔한 예시는 계좌 이체( bankTransfer() )이다.
    • 계좌 이체는 출금( withdraw() )과 입금( deposit() )이라는 각각의 거래가 하나의 단위를 이루게 되는 상황이다.
    • 비즈니스에서 하나의 트랜잭션은 데이터베이스 상에서는 하나 혹은 여러 개의 작업이 같은 묶음을 이루는 경우가 많다.
    • deposit( )과 withdraw( )는 각자 고유하게 데이터베이스와 커넥션을 맺고 작업을 처리한다.
    • 문제는 withdraw( )는 정상적으로 처리되었는데, deposit( )에서 예외가 발생하는 경우다.
    • 영속 계층에서 withdraw( )와 deposit( )은 각각 데이터베이스와 연결을 맺고 처리하는데 하나의 트랜잭션으로 처리해야 할 경우에는 한쪽이 잘못되는 경우에 이미 성공한 작업까지 다시 원상태로 복구되어야 한다.
  • 스프링은 트랜잭션 처리를 XML 설정을 이용하거나, 어노테이션 처리만으로 할 수 있다.

 

 


데이터베이스 설계와 트랜잭션

  • 데이터 베이스에서 저장 구조를 효율적으로 관리하기 위해서 흔히 정규화라는 작업을 한다.
  • 정규화를 진행하면서 원칙적으로 칼럼으로 처리되지 않는 데이터
    • 시간이 흐르면 변경되는 데이터를 칼럼으로 기록하지 않는다. 
    • 계산이 가능한 데이터를 칼럼으로 기록하지 않는다. 
    • 누구에게나 정해진 값을 이용하는 경우
  • 정규화가 진행될수록 테이블은 점점 더 순수한 형태가 되어가는데, 순수한 형태가 될수록 '트랜잭션 처리'의 대상에서 멀어진다.
  • 정규화를 진행할 수록 더욱 간결해지지만 반대로 쿼리 등을 이용해서 필요한 데이터를 가져 오는 입장에서는 점점 불편해 진다. (단순히 조회하는 것이 아니라 직접 조인, 서브 쿼리)
  • 조인이나 서브쿼리(매번 계산이 발생하도록 만들어지는 쿼리)를 이용하게 되면 성능이 저하될 수 있기 때문에 많은 양의 데이터를 처리 해야 하는 상황에서는 바람직하지 않을 수 있다. 이러한 상황에서는 반정규화를 하게 된다.
  • 반 정규화는 정규화의 반대로 중복이나 계산되는 값을 데이터베이스 상에 보관하고, 대신에 조인이나 서브쿼리를 사용을 줄이는 방식이다.
  • 반정규화를 하게 되면 쿼리가 단순해지고 성능상으로도 얻을 수 있는 이득이 있지만, 어떤 작업이 일어날 때, 테이블들에 트랜잭션으로 관리해야 할 작업이 있는지 고려해야한다.

 


트랜잭션 설정 실습

  • 스프링의 트랜잭션 설정은 AOP와 같이 XML을 이용해서 설정하거나 어노테이션을 이용해서 설정이 가능하다.
  • 스프링의 트랜잭션을 이용하기 위해서 Transaction Manager라는 존재가 필요하다.
  • root-context.xml에는 트랜잭션을 관리하는 빈을 등록하고, 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 <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:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
    xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
        http://www.springframework.org/schema/beans https://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/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
    
    <!-- Root Context: defines shared resources visible to all other web components -->
    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <!-- <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property> -->
        <!-- <property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:orcl"></property> -->
        <property name="driverClassName"
            value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
        <property name="jdbcUrl"
            value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property>
        <property name="username" value="book_ex"></property>
        <property name="password" value="book_ex"></property>
    </bean>
 
 
    <!-- HikariCP configuration -->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
        destroy-method="close">
        <constructor-arg ref="hikariConfig" />
    </bean>
 
    <bean id="sqlSessionFactory"
        class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
 
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <tx:annotation-driven />
    
    <mybatis-spring:scan base-package="org.zerock.mapper"/>
    
    <context:component-scan
        base-package="org.zerock.service">
    </context:component-scan>
    <context:component-scan base-package="org.zerock.aop">
    </context:component-scan>
    
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
    
    <context:annotation-config></context:annotation-config>
    <context:component-scan base-package="org.zerock.service">
    </context:component-scan>
    
    
 
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
 
cs

<bean> 으로 등록된 transactionManager와 <tx:annotation-driven> 설정이 추가 된 후에는 트랜잭션이 필요한 상황을 만들어서 어노테이션을 추가하는 방식으로 설정하게 된다.

 

 

 

  • 예제 테이블 생성

    • 트랜잭션의 실습은 간단히 2개의 테이블을 생성하고, 한 번에 두 개의 테이블에 insert 해야 하는 상황을 재현하도록 한다.
create table tbl_sample1( col1 varchar2(500));
create table tbl_sample2( col2 varchar2(50));
cs

tbl_sample1 테이블의 col1의 경우는 varchar2(500)으로 설정된 반면에 tbl_sample2는 varchar2(50)으로 설정되었다. 만일 50바이트 이상의 데이터를 넣는 상황이라면 tbl_sampe1에는 정상적으로 insert 되지만, tbl_sample2에는 insert 시 칼럼의 최대 길이보다 크기 때문에 문제가 있게 된다.

 

 

  • Sample1Mapper 인터페이스
package org.zerock.mapper;
 
import org.apache.ibatis.annotations.Insert;
 
public interface Sample1Mapper {
    @Insert("insert into tbl_sample1 (col1) values (#{data})")
    public int insertCol1(String data);
    
}
cs

Sample1Mapper는 tbl_sample1 테이블에 데이터를 insert하게 작성한다.

 

 

 

  • Sample2Mapper 인터페이스
package org.zerock.mapper;
 
import org.apache.ibatis.annotations.Insert;
 
public interface Sample2Mapper {
 
    @Insert("insert into tbl_sample2 (col2) values (#{data})")
    public int insertCol2(String data);
}
cs

Sample2Mapper는 tbl_sample2 테이블에 데이터를 insert하게 작성한다.

 

 

 

 

  • 비즈니스 계층과 트랜잭션 설정

    • 트랜잭션은 비즈니스 계층에서 이루어지므로, org.zerock.service 계층에서 Sample1Mapper, Sample2Mapper를 사용하는 SampleTxService 인터페이스, SampleTxServiceImpl 클래스를 설계한다.

트랜잭션의 설정이 안되어 있는 상태 먼저 테스트

 

 

  • SampleTxService 인터페이스
package org.zerock.service;
 
public interface SampleTxService {
    public void addData(String value);
}
cs

addData()라는 메서드를 통해서 데이터를 추가한다.

 

 

  • SampleTxServiceImpl 클래스
package org.zerock.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zerock.mapper.Sample1Mapper;
import org.zerock.mapper.Sample2Mapper;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
@Service
@Log4j
public class SampleTxServiceImpl implements SampleTxService{
    
    @Setter(onMethod_ = {@Autowired} )
    private Sample1Mapper mapper1;
    
    @Setter(onMethod_ = {@Autowired})
    private Sample2Mapper mapper2;
    
    @Override
    public void addData(String value) {
        // TODO Auto-generated method stub
        log.info("mapper1..............");
        mapper1.insertCol1(value);
        
        log.info("mapper2..............");
        mapper2.insertCol2(value);
        
        log.info("end..............");
    }
 
}
cs

Sample1Mapper와 Sample2Mapper 모두를 이용해서 같은 데이터를 tbl_sample1과 tbl_sample2 테이블에 insert하도록 작성한다.

 

 

  • SampleTxServiceTests 클래스
package org.zerock.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
//Java Config
//@ContextConfiguration(classes={org.zerock.config.RootConfig.class, org.zerock.config.ServletConfig.class})
@Log4j
public class SampleTxServiceTests {
    @Setter(onMethod_ = {@Autowired})
    private SampleTxService service;
    
    @Test
    public void testLong() {
        String str = "Starry\r\n" + "Starry night\r\n"
                + "Paint your palette blue and gray\r\n" +
                "Look out on a summer's day";
        
        log.info(str.getBytes().length);
        service.addData(str);
    }
}
cs

- testLong()은 50bytes가 넘고 500bytes를 넘지 않는 길이의 어떤 문자열을 이용해서 tbl_sample1, tbl_sample2e 테이블에 insert를 시도한다.

- testLong()을 실행하면 tbl_sample1에는 데이터가 추가되지만 ,tbl_sample2에는 길이의 제한으로 인해서 insert가 실패하게 된다.

 

 

 

  • @Transactional 어노테이션

    • 위의 결과를 보면 트랜잭션 처리가 되지 않았기 때문에 하나의 테이블에만 insert가 성공한 것을 볼 수 있다.
    • 만일 트랜잭션 처리가 되었다면 tbl_sample1과 tbl_sample2 테이블 모두에 insert가 되지 않았어야 하므로, 트랜잭션 처리가 될 수 있도록 SampleTxServiceImpl의 addData()에 @Transactional을 추가한다.

 

  • SampleTxServiceImpl 클래스
@Transactional
    @Override
    public void addData(String value) {
        // TODO Auto-generated method stub
        log.info("mapper1..............");
        mapper1.insertCol1(value);
        
        log.info("mapper2..............");
        mapper2.insertCol2(value);
        
        log.info("end..............");
    }
cs

 

 

양쪽 테이블에 모든 데이터를 없애고, 다시 테스트 코드를 실행한다.

동일한 코드지만 @Transactional이 추가된 후에는 실행 시 rollback()되는 것을 확인할 수 있다.

데이터베이스에서도 테이블 모두 아무 데이터가 들어가지 않았다.

 

 

 

 

  • @Transactional 어노테이션 속성들

 

  • 전파 Propagation 속성
    • PROPAGATION_MADATORY : 작업은 반드시 특정 트랜잭션이 존재한 상태에서만 가능
    • PROPAGATION_NESTED : 기존에 트랜잭션이 있는 경우 포함해서 실행
    • PROPAGATION_NEVER : 트랜잭션 상황 하에 실행되면 예외 발생
    • PROPAGATION_NOT_SUPPORTED : 트랜잭션이 있는 경우 트랜잭션이 끝날 떄까지 보류 후 실행
    • PROPAGATION_REQUIRED : 트랜잭션이 있으면 그 상황에서 실행, 없으면 새로운 트랜잭션 실행(기본 설정)
    • PROPAGATION_REQUIRED_NEW : 대상은 자신만의 고유한 트랜잭션으로 실행
    • PROPAGATION_SUPPORTS : 트랜잭션을 필요로 하지 않으나 트랜잭션 상황 하에 있다면 포함되어 실행
  • 격리(isolation) 레벨
    • DEFAULT : DB 설정, 기본 격리 수준(기본 설정)
    • SERIALIZABLE : 가장 높은 격리, 성능 저하 우려가 있음
    • READ_UNCOMMITED : 커밋되지 않은 데이터에 대한 읽기 허용
    • READ_COMMITED : 커밋된 데이터 읽기 허용
    • REPEATEABLE_READ : 동일 필드에 대한 다중 접근 시 모두 동일한 결과 보장
  • Read-only 속성
    • true일 경우 insert, update, delete 실행 시 예외 발생, 기본 설정은 false
  • Rollback-for-예외
    • 특정 예외가 발생 시 강제로 Rollback
  • No-rollback-for 예외
    • 특정 예외 발생 시엔 rollback 처리되지 않음

 

 

 

  • @Transactional 적용 순서

    • 간단한 트랜잭션 매니저의 설정과 @Transactional 어노테이션을 이용한 설정만으로 애플리케이션 내의 트랜잭션에 대한 설정을 처리할 수 있다.
    • @Transactional 어노테이션의 경우 클래스나 인터페이스에 선언하는 것 역시 가능하다.

 

어노테이션의 우선순위

  1. 메서드의 @Transactional 설정
  2. 클래스의 @Transactional 설정
  3. 인터페이스의 @Transacitional 설정

위의 규칙대로 적용되는 것을 기준으로 작성하자면 인터페이스에는 가장 기준이 되는 @Transactional과 같은 설정을 지정하고, 클래스나 메서드에 필요한 어노테이션을 처리하는 것이 좋다.

 

 


출처 : 코드로 배우는 스프링 웹 프로젝트 [구멍가게 코딩단]

'Records > Spring Framework' 카테고리의 다른 글

파일 업로드 방식  (0) 2021.03.28
댓글과 댓글 수에 대한 처리  (0) 2021.03.26
AOP라는 패러다임  (0) 2021.03.22
Ajax 댓글 처리  (0) 2021.03.18
REST 방식으로 전환  (0) 2021.03.16
Comments