본문 바로가기

개발 공부/Spring

코드로 배우는 스프링 웹 프로젝트-19.스프링에서 트랜잭션 관리

코드로 배우는 스프링 웹 프로젝트-19.스프링에서 트랜잭션 관리

코드로 배우는 스프링 웹 프로젝트 - 개정판

2019년 7월 10일 인쇄판

 

Part5. AOP와 트랜잭션

 

Chapter19.스프링에서 트랜잭션 관리

 

비즈니스에서 쪼개질 수 없는 하나의 작업 단위로 트랜잭션이라는 용어를 사용한다.

'한 번에 이루어지는 작업의 단위'를 트랜잭션으로 간주한다.

 

ACID 원칙)

원자성Atomicity : 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 함. 어떤 트랜잭션이 A와 B로 구성된다면 항상 A, B의 처리 결과는 동일한 결과여야 함. 즉 A는 성공하고 B는 실패할 경우 둘다 원래 상태로 되돌려져야 한다.

일관성 Consistency : 트랜잭션이 성공했다면 데이터베이스의 모든 데이터가 일관성을 유지해야 함

격리 isolation : 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야 한다

영속성 Durability : 트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 한다.

 

 

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

 

데이터베이스의 저장 구조를 효율적으로 관리하기 위해 흔히 정규화 작업을 한다.

정규화의 가장 기본은 중복 데이터를 제거해 데이터 저장의 효율을 올리자는 것이다. 일반적으로 테이블은 늘어나고 각 테이블의 데이터 양은 줄어든다.

정규화를 진행하며 원칙적으로 칼럼으로 처리되는 데이터는 다음과 같다.

 

- 시간이 흐르면 변경되는 데이터를 칼럼으로 기록하지 않는다 : 대표적으로 사용자의 생년월일은 데이터베이스에 기록하지만 현재 나이는 칼럼으로 유지하지 않는다.

- 계산이 가능한 데이터를 칼럼으로 기록하지 않는다 : 주문과 주문 상세가 별도의 테이블로 분리되어 있다면 사용자가 한 번에 몇 개의 상품을 주문했는지 등은 칼럼으로 기록하지 않는다.

- 누구에게나 정해진 값은 데이터베이에서 취급하지 않는다.

 

정규화가 잘 되었거나 위 같은 규칙이 반영된 디비 설계에선 트랜잭션이 많이 일어나지 않는다. 정규화가 진행될수록 테이블은 점점 더 순수한 형태가 되어가는데 순수 형태가 될수록 트랜잭션 처리의 대상에선 멀어진다.

정규화를 진행할수록 테이블은 간결해지지만 반대로 쿼리를 이용해 필요한 데이터를 가져오는 입장에선 불편해진다. 직접 조인이나 서브쿼리를 처리해야 하기 때문이다.

 

조인이나 서브쿼리를 사용하면 성능 이슈가 발생한다. 매번 계산이 발생하도록 만들어지는 쿼리의 경우 성능이 저하되므로 많은 양의 데이터를 처리해야 하는 상황에선 바람직하지 않다. 이런 상황에서 반정규화를 하게 된다. 중복이나 계산값을 디비 상에 보관하고 대신에 조인이나 서브쿼리를 줄이는 방식이다.

 

예를 들어 게시글과 댓글의 table이 분리되어 있는 경우 게시물의 목록 페이지에 일반적으로 댓글 숫자도 같이 표현할 때가 있다. 댓글 숫자를 칼럼으로 처리하면 성능상으로 좀 더 이득을 볼 수 있다.

 

 

19.2 트랜잭션 설정 실습

 

Transaction Manager가 필요하다.

spring-jdbc, spring-tx, mybatis, mybatis-spring, hikari 등을 ex04의 pom.xml에 추가.

 

root-context.xml Namespace에서 tx 추가

 

어노테이션 기반 트랜잭션 설정이 되도록 tx:annotation-driven 태그 등록.

이 때 mybatis-spring도 함께 체크.

 

<?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:orcl"></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>

bean으로 등록된 transactionManager와 tx:annotation-driven설정이 추가

 

19.2.1 JAVA 설정을 이용한 트랜잭션 설정

 

@Configuration
@ComponentScan(basePackages={"org.zerock.service"})
@ComponentScan(basePackages="org.zerock.aop")
@EnableAspectJAutoProxy

@EnableTransactionManagement

@MapperScan(basePackages={"org.zerock.mapper"})
public class RootConfig {
	@Bean
    public DataSource dataSource() {
    ...생략...
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
    	SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource());
        return (SqlSessionFactory) sqlSessionFactory.getObject();
    }
    
    @Bean
    public DataSourceTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

@EnableTransactionManagement 설정은 aspectj-autoproxy에 대한 설정이 되고 txManager는 bean 설정을 대신한다.

 

 

19.2.2 예제 테이블 생성.

 

create table tbl_sample1(col1 varchar2(500));

create table tbl_sample2(col2 varchar2(50));

 

org.zerock.mapper 패키지에 Sample1Mapper와 Sample2Mapper 인터페이스 추가.

 

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);
	
}
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);
}

 

 

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

 

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

package org.zerock.service;

public interface SampleTxService {
	public void addData(String value);
}
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..............");
	}

}

SampleTxService는 addData 메서드로 데이터를 추가한다. SampleTxServiceImpl 클래스는 Sample1Mapper와 Sample2Mapper 모두를 이용해 같은 데이터를 두 개의 테이블에 insert한다.

 

 

실행이 되지 않는다면 ojdbc 추가.

위 사항도 추가..^^;

 

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);
	}
}

 

 

tbl_sample1에는 데이터 추가가 되지만 2에는 길이 제한으로 실패한다.

 

 

 

19.2.4 @Transactional 어노테이션

 

위처럼 트랜잭션 처리가 되지 않았으므로 하나의 테이블에만 insert가 성공한다.

만일 트랜잭션 처리가 되었다면 두 테이블 모두에 insert가 되지 않았어야 한다.

 

SampleTxServiceImpl에 @Transactional 추가.

 

	@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..............");
	}

sample table의 모든 데이터를 지운 뒤 다시 테스트 코드 실행.

 

롤백이 실행되는 것을 확인할 수 있다.

 

 

19.2.5 @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 처리되지 않음

 

 

19.2.6 @Transactional 적용 순서

 

간단한 트랜잭션 매니저 설정과 @Transactional 어노테이션 설정만으로 애플리케이션 내외 트랜잭션에 대한 설정을 처리할 수 있다.

 

클래스나 인터페이스에도 선언 가능.

 

- 메서드의 @Transactional 설정이 가장 우선시 됨

- 클래스의 설정은 메서드보다 우선순위가 낮다

- 인터페이스의 설정이 가장 낮은 순위.