Keep going
스프링에서 트랜잭션 관리 본문
- 비즈니스에서는 쪼개질 수 없는 하나의 단위 작업을 말할 때 트랜잭션이라는 용어를 사용한다.
원자성 | 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다. 좀 더 쉽게 말하면 어떤 트랜잭션이 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 어노테이션의 경우 클래스나 인터페이스에 선언하는 것 역시 가능하다.
어노테이션의 우선순위
- 메서드의 @Transactional 설정
- 클래스의 @Transactional 설정
- 인터페이스의 @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 |