개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 19강. 스프링에서 트랜잭션 관리 (트랜잭션, ACID ...)

ee2ee2 2022. 5. 25. 15:12
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


트랜잭션(Transaction)이란?

: 한 번에 이루어지는 작업의 단위를 뜻함. (쪼갤 수 없는 하나의 단위 작업)

트랜잭션의 성격을 'ACID 원칙'으로 설명한다. 아래의 표를 참고해보자.

<ACID 원칙>


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

데이터베이스의 저장 구조를 효율적으로 관리하기 위해서는 '정규화' 작업을 한다. '정규화' 작업은 '중복된 데이터를 제거'해서 데이터 저장의 효율을 올리는 것을 목표로 한다. 정규화를 진행하면 1) 테이블을 늘어나고, 2) 각 테이블의 데이터 양은 줄어드는 것이 일반적이다.

정규화의 목적과 단계 별 정규화에 대해 상세히 알고 싶다면, 여기 글을 참고하면 좋다.


19.2 트랜잭션 설정 실습

스프링의 트랜잭션 설정은 AOP와 같이 XML을 이용하거나, 어노테이션을 이용해서 설정이 가능하다.

스프링의 트랙잭션을 이용하기 위해서는 Transaction Manager라는 존재가 필요하다.

pom.xml

: spring-jdbc, spring-tx,  mybatis, mybatis-spring, hikari 등 라이브러리 추가

        ..생략
        <!-- spring-jdbc/spring-tx :스프링에서 데이터베이스 처리와 트랜잭션 처리 -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		
		<dependency>
			<groupId>com.zaxxer</groupId>
			<artifactId>HikariCP</artifactId>
			<version>2.7.8</version>
		</dependency>
		
		<!-- MyBatis 관련 라이브러리 추가 시작-->
		<!-- mybatis/mybatis-spring : Mybatis와 spring 연동용 라이브러리 -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.4.6</version>
		</dependency>
		
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>1.3.2</version>
		</dependency>
		<!-- MyBatis 관련 라이브러리 추가 끝 -->
        
        <!-- SQL의 로그를 제대로 보기 위한 라이브러리 사용 -->
 		<!-- https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4 -->
		<dependency>
			<groupId>org.bgee.log4jdbc-log4j2</groupId>
			<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
			<version>1.16</version>
		</dependency>
        
        ..생략

 

root-context.xml

트랜잭션을 관리하는 빈(객체)을 등록하고, 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 <tx:annotation-driven/> 태그를 등록한다.

Namespaces에서 tx 체크

<?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:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	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 -->
	<context:annotation-config> </context:annotation-config>
	
	<!-- HikariCP의 bean 설정 -->
	<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
		<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
		<property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1522: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>
	
	<!-- 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 하기 위함 -->
	<tx:annotation-driven/>
	
	<mybatis-spring:scan base-package="org.zerock.mapper"/>

	<!-- Service 작성한 클래스를 스프링의 Bean으로 인식하기 위해 아래 패키지를 스캔하도록 추가 (@Service 어노테이션으로 구분) -->
	<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 예제 테이블 생성

간단히 2개의 테이블을 생성하고, 한 번에 두 개의 테이블에 insert해야 하는 상황을 테스트 해본다.

예제 테이블 생성하기

create table tbl_sample1 (col1 varchar2(500));
create table tbl_sample2 (col2 varchar2(50));

예제 테이블 생성 완료

org.zerock.mapper 패키지에 Sample1Mapper/Sample2Mapper 인터페이스 생성

Sample1Mapper / Sample2Mapper인터페이스

//각각 다른 파일에 작성된 내용입니다.

//Sample1Mapper 인터페이스 (Sample1Mapper.java)
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);
}


//Sample2Mapper 인터페이스 (Sample2Mapper.java)
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.2 비즈니스 계층과 트랜잭션 설정

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

SampleTxService 인터페이스

package org.zerock.service;

public interface SampleTxService {
	public void addData(String value);
}

SampleTxServiceImpl 클래스

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

 

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) {
		
		log.info("mapper1...............");
		mapper1.insertCol1(value);
		
		log.info("mapper2...............");
		mapper2.insertCol2(value);
		
		log.info("end...................");
	}

}

SampleTxServiceTests.java

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)
@Log4j
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
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 grey\\r\\n" + 
					"Look out on a summer's day~!\\r\\n";
		
		log.info(str.getBytes().length);
		
		service.addData(str);
	}
}

테스트 결과

테스트에 사용한 문자열을 82bytes 였으므로 tbl_sample1에서는 아래와 같이 정상 insert됐지만, tbl_sample2에서는 insert에 실패하였다.

SELECT 결과

500byte까지 insert 가능한 tbl_sample1 테이블
50byte까지 insert 가능한 tbl_sample2 테이블


19.2.3 Transactional 어노테이션

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

SampleTxServiceImpl 클래스의 addData()

	@Transactional
	@Override
	public void addData(String value) {
		
		log.info("mapper1...............");
		mapper1.insertCol1(value);
		
		log.info("mapper2...............");
		mapper2.insertCol2(value);
		
		log.info("end...................");
	}

테스트 결과

두 테이블 모두 아무 데이터가 들어가지 않았음을 확인할 수 있다. 즉, 트랜잭션 처리가 성공하였다.

tbl_sample1 테이블
tbl_sample2테이블


19.2.4 @Transactional 적용 순서

@Transactional의 경우 위와 같이 메소드에 적용도 가능하지만, 클래스나 인터페이스에 선언하는 것 역시 가능하다.

우선 순위는 아래와 같다.

  1. 메소드의 @Transactional 설정이 가장 우선시 된다.
  2. 클래스의 @Transactional 설정은 메소드보다 우선순위가 낮다.
  3. 인터페이스의 @Transactional 설정이 가낭 낮은 우선순위를 갖는다.

메소드 > 클래스 > 인터페이스

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