웹사이트 검색

스프링 트랜잭션 관리 예제 JDBC


Spring 트랜잭션 관리는 Spring 프레임워크에서 가장 널리 사용되고 중요한 기능 중 하나입니다. 트랜잭션 관리는 모든 엔터프라이즈 애플리케이션에서 사소한 작업입니다. 트랜잭션 관리를 위해 JDBC API를 사용하는 방법은 이미 배웠습니다. Spring은 트랜잭션 관리에 대한 광범위한 지원을 제공하고 개발자가 시스템 오류가 발생할 경우 데이터 무결성에 대해 걱정하기보다는 비즈니스 로직에 더 집중할 수 있도록 도와줍니다.

스프링 트랜잭션 관리

  1. 선언적 트랜잭션 관리 지원. 이 모델에서 Spring은 데이터 무결성을 제공하기 위해 트랜잭션 방식을 통해 AOP를 사용합니다. 이것은 선호되는 접근 방식이며 대부분의 경우에 작동합니다.
  2. JDBC, Hibernate, JPA, JDO, JTA 등과 같은 대부분의 트랜잭션 API를 지원합니다. 적절한 트랜잭션 관리자 구현 클래스를 사용하기만 하면 됩니다. 예를 들어 JDBC 트랜잭션 관리를 위한 org.springframework.jdbc.datasource.DriverManagerDataSource 및 Hibernate를 ORM 도구로 사용하는 경우 org.springframework.orm.hibernate3.HibernateTransactionManager를 예로 들 수 있습니다.
  3. TransactionTemplate 또는 PlatformTransactionManager 구현을 사용하여 프로그래밍 방식의 트랜잭션 관리를 지원합니다.

트랜잭션 관리자에서 원하는 대부분의 기능은 선언적 트랜잭션 관리에서 지원하므로 예제 프로젝트에 이 접근 방식을 사용합니다.

스프링 트랜잭션 관리 JDBC 예제

단일 트랜잭션에서 여러 테이블을 업데이트할 간단한 Spring JDBC 프로젝트를 생성합니다. 트랜잭션은 모든 JDBC 문이 성공적으로 실행될 때만 커밋해야 하며, 그렇지 않으면 데이터 불일치를 방지하기 위해 롤백해야 합니다. JDBC 트랜잭션 관리를 알고 있다면 연결에 대해 auto-commit을 false로 설정하고 모든 명령문의 결과에 따라 트랜잭션을 커밋하거나 롤백하면 쉽게 수행할 수 있다고 주장할 수 있습니다. 분명히 우리는 그것을 할 수 있지만 트랜잭션 관리를 위한 상용구 코드가 많이 생길 것입니다. 또한 우리가 트랜잭션 관리를 찾고 있는 모든 위치에 동일한 코드가 나타나 밀접하게 결합되고 유지 관리할 수 없는 코드를 생성합니다. Spring 선언적 트랜잭션 관리는 Aspect Oriented Programming을 사용하여 느슨한 결합을 달성하고 응용 프로그램에서 상용구 코드를 피함으로써 이러한 문제를 해결합니다. 간단한 예제를 통해 Spring이 어떻게 작동하는지 살펴보겠습니다. Spring 프로젝트로 이동하기 전에 사용할 데이터베이스 설정을 수행해 보겠습니다.

Spring 트랜잭션 관리 - 데이터베이스 설정

사용을 위해 두 개의 테이블을 생성하고 단일 트랜잭션에서 두 테이블을 모두 업데이트합니다.

CREATE TABLE `Customer` (
  `id` int(11) unsigned NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Address` (
  `id` int(11) unsigned NOT NULL,
  `address` varchar(20) DEFAULT NULL,
  `country` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Spring 트랜잭션 관리 - Maven 종속성

JDBC API를 사용하고 있으므로 애플리케이션에 spring-jdbc 종속성을 포함해야 합니다. 또한 mysql 데이터베이스에 연결하려면 MySQL 데이터베이스 드라이버가 필요하므로 mysql-connector-java 종속성도 포함합니다. spring-tx 아티팩트는 트랜잭션 관리 종속성을 제공하며 일반적으로 STS에 의해 자동으로 포함되지만 그렇지 않은 경우 포함해야 합니다. 로깅 및 단위 테스트에 대한 다른 종속성을 볼 수 있지만 사용하지 않을 것입니다. 최종 pom.xml 파일은 아래 코드와 같습니다.

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.springframework.samples</groupId>
	<artifactId>SpringJDBCTransactionManagement</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>

		<!-- Generic properties -->
		<java.version>1.7</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

		<!-- Spring -->
		<spring-framework.version>4.0.2.RELEASE</spring-framework.version>

		<!-- Logging -->
		<logback.version>1.0.13</logback.version>
		<slf4j.version>1.7.5</slf4j.version>

		<!-- Test -->
		<junit.version>4.11</junit.version>

	</properties>

	<dependencies>
		<!-- Spring and Transactions -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>

		<!-- Spring JDBC and MySQL Driver -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.0.5</version>
		</dependency>

		<!-- Logging with SLF4J & LogBack -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>${logback.version}</version>
			<scope>runtime</scope>
		</dependency>

		<!-- Test Artifacts -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring-framework.version}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>
</project>

오늘 현재 Spring 버전을 최신 버전으로 업데이트했습니다. MySQL 데이터베이스 드라이버가 mysql 설치와 호환되는지 확인하십시오.

Spring 트랜잭션 관리 - 모델 클래스

테이블에 매핑할 두 개의 Java Bean인 고객 및 주소를 생성합니다.

package com.journaldev.spring.jdbc.model;

public class Address {

	private int id;
	private String address;
	private String country;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
	public String getCountry() {
		return country;
	}
	public void setCountry(String country) {
		this.country = country;
	}
	
}
package com.journaldev.spring.jdbc.model;

public class Customer {

	private int id;
	private String name;
	private Address address;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Address getAddress() {
		return address;
	}
	public void setAddress(Address address) {
		this.address = address;
	}
	
}

Customer bean에는 변수 중 하나로 Address가 있습니다. 고객을 위한 DAO를 구현할 때 고객 및 주소 테이블 모두에 대한 데이터를 얻고 이 테이블에 대해 두 개의 개별 삽입 쿼리를 실행할 것이므로 데이터 불일치를 피하기 위해 트랜잭션 관리가 필요한 이유입니다.

스프링 트랜잭션 관리 - DAO 구현

고객 빈에 대한 DAO를 구현해 보겠습니다. 단순화를 위해 고객 및 주소 테이블 모두에 레코드를 삽입하는 한 가지 방법만 있습니다.

package com.journaldev.spring.jdbc.dao;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerDAO {

	public void create(Customer customer);
}
package com.journaldev.spring.jdbc.dao;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import com.journaldev.spring.jdbc.model.Customer;

public class CustomerDAOImpl implements CustomerDAO {

	private DataSource dataSource;

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	@Override
	public void create(Customer customer) {
		String queryCustomer = "insert into Customer (id, name) values (?,?)";
		String queryAddress = "insert into Address (id, address,country) values (?,?,?)";

		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

		jdbcTemplate.update(queryCustomer, new Object[] { customer.getId(),
				customer.getName() });
		System.out.println("Inserted into Customer Table Successfully");
		jdbcTemplate.update(queryAddress, new Object[] { customer.getId(),
				customer.getAddress().getAddress(),
				customer.getAddress().getCountry() });
		System.out.println("Inserted into Address Table Successfully");
	}

}

CustomerDAO 구현은 트랜잭션 관리를 처리하지 않습니다. 이런 식으로 우리는 때때로 제3자로부터 DAO 구현을 받고 이러한 클래스를 제어할 수 없기 때문에 관심사를 분리하고 있습니다.

Spring 선언적 트랜잭션 관리 - 서비스

CustomerDAO 구현을 사용하고 단일 방법으로 고객 및 주소 테이블에 레코드를 삽입할 때 트랜잭션 관리를 제공하는 고객 서비스를 생성해 봅시다.

package com.journaldev.spring.jdbc.service;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerManager {

	public void createCustomer(Customer cust);
}
package com.journaldev.spring.jdbc.service;

import org.springframework.transaction.annotation.Transactional;

import com.journaldev.spring.jdbc.dao.CustomerDAO;
import com.journaldev.spring.jdbc.model.Customer;

public class CustomerManagerImpl implements CustomerManager {

	private CustomerDAO customerDAO;

	public void setCustomerDAO(CustomerDAO customerDAO) {
		this.customerDAO = customerDAO;
	}

	@Override
	@Transactional
	public void createCustomer(Customer cust) {
		customerDAO.create(cust);
	}

}

CustomerManager 구현을 보면 CustomerDAO 구현을 사용하여 고객을 생성하지만 createCustomer() 메서드에 @Transactional 주석을 추가하여 선언적 트랜잭션 관리를 제공합니다. 이것이 Spring 트랜잭션 관리의 이점을 얻기 위해 코드에서 수행해야 하는 전부입니다. Java 주석 자습서. 남은 부분은 스프링 트랜잭션 관리 예제가 작동하도록 스프링 빈을 연결하는 것입니다.

스프링 트랜잭션 관리 - 빈 구성

이름이 \spring.xml\인 Spring Bean 구성 파일을 만듭니다. 테스트 프로그램에서 이를 사용하여 Spring Bean을 연결하고 JDBC 프로그램을 실행하여 트랜잭션 관리를 테스트합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
	xmlns:tx="https://www.springframework.org/schema/tx"
	xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.0.xsd
		https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

	<!-- Enable Annotation based Declarative Transaction Management -->
	<tx:annotation-driven proxy-target-class="true"
		transaction-manager="transactionManager" />

	<!-- Creating TransactionManager Bean, since JDBC we are creating of type 
		DataSourceTransactionManager -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- MySQL DB DataSource -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">

		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
		<property name="username" value="pankaj" />
		<property name="password" value="pankaj123" />
	</bean>

	<bean id="customerDAO" class="com.journaldev.spring.jdbc.dao.CustomerDAOImpl">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<bean id="customerManager" class="com.journaldev.spring.jdbc.service.CustomerManagerImpl">
		<property name="customerDAO" ref="customerDAO"></property>
	</bean>

</beans>

Spring Bean 구성 파일에서 주목해야 할 중요한 사항은 다음과 같습니다.

  • tx:annotation-driven 요소는 주석 기반 트랜잭션 관리 구성을 사용하고 있음을 Spring 컨텍스트에 알리는 데 사용됩니다. transaction-manager 속성은 트랜잭션 관리자 빈 이름을 제공하는 데 사용됩니다. transaction-manager 기본값은 transactionManager이지만 혼동을 피하기 위해 여전히 사용하고 있습니다. 프록시 대상 클래스 속성은 클래스 기반 프록시를 사용하도록 Spring 컨텍스트에 알리는 데 사용되며, 속성이 없으면 Exception in thread "main\ org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean과 같은 메시지와 함께 런타임 예외가 발생합니다. 명명된 'customerManager'는 [com.journaldev.spring.jdbc.service.CustomerManagerImpl] 유형이어야 하지만 실제로는 [com.sun.proxy.$Proxy6] 유형이었습니다.
  • JDBC를 사용하고 있기 때문에 org.springframework.jdbc.datasource.DataSourceTransactionManager 유형의 transactionManager 빈을 생성하고 있습니다. 이는 매우 중요하며 트랜잭션 API 사용에 따라 적절한 트랜잭션 관리자 구현 클래스를 사용해야 합니다.
  • dataSource 빈은 DataSource 개체를 생성하는 데 사용되며 우리는 driverClassName, url, 사용자 이름 및 암호와 같은 데이터베이스 구성 속성을 제공해야 합니다. 로컬 설정에 따라 이 값을 변경하세요.
  • 우리는 dataSourcecustomerDAO 빈에 주입하고 있습니다. 마찬가지로 customerDAO 빈을 customerManager 빈 정의에 주입합니다.

설정이 준비되었습니다. 트랜잭션 관리 구현을 테스트하기 위한 간단한 테스트 클래스를 만들어 보겠습니다.

package com.journaldev.spring.jdbc.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.journaldev.spring.jdbc.model.Address;
import com.journaldev.spring.jdbc.model.Customer;
import com.journaldev.spring.jdbc.service.CustomerManager;
import com.journaldev.spring.jdbc.service.CustomerManagerImpl;

public class TransactionManagerMain {

	public static void main(String[] args) {
		ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
				"spring.xml");

		CustomerManager customerManager = ctx.getBean("customerManager",
				CustomerManagerImpl.class);

		Customer cust = createDummyCustomer();
		customerManager.createCustomer(cust);

		ctx.close();
	}

	private static Customer createDummyCustomer() {
		Customer customer = new Customer();
		customer.setId(2);
		customer.setName("Pankaj");
		Address address = new Address();
		address.setId(2);
		address.setCountry("India");
		// setting value more than 20 chars, so that SQLException occurs
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

주소 테이블에 데이터를 삽입하는 동안 예외가 발생하도록 명시적으로 주소 열 값을 너무 길게 설정하고 있습니다. 이제 테스트 프로그램을 실행하면 다음과 같은 결과가 나타납니다.

Mar 29, 2014 7:59:32 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3fa99295: startup date [Sat Mar 29 19:59:32 PDT 2014]; root of context hierarchy
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Inserted into Customer Table Successfully
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.support.SQLErrorCodesFactory <init>
INFO: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into Address (id, address,country) values (?,?,?)]; Data truncation: Data too long for column 'address' at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
	at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:100)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:907)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:968)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:978)
	at com.journaldev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$$84f71441.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
	at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
	at com.journaldev.spring.jdbc.main.TransactionManagerMain.main(TransactionManagerMain.java:20)
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
	at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
	at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
	at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
	at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:914)
	at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:907)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642)
	... 16 more

로그 메시지에는 데이터가 고객 테이블에 성공적으로 삽입되었다고 나와 있지만 MySQL 데이터베이스 드라이버에 의해 발생한 예외는 값이 주소 열에 비해 너무 길다는 것을 분명히 나타냅니다. 이제 Customer 테이블을 확인하면 트랜잭션이 완전히 롤백되었음을 의미하는 행을 찾을 수 없습니다. 트랜잭션 관리 마법이 발생하는 위치가 궁금하다면 로그를 주의 깊게 살펴보고 Spring 프레임워크에서 생성된 AOP 및 프록시 클래스를 확인하십시오. Spring 프레임워크는 CustomerManagerImpl에 대한 프록시 클래스를 생성하고 메서드가 성공적으로 반환되는 경우에만 트랜잭션을 커밋하기 위해 Around 어드바이스를 사용합니다. 예외가 있으면 전체 트랜잭션을 롤백하는 것입니다. Aspect Oriented Programming 모델에 대해 자세히 알아보려면 Spring AOP 예제를 읽어 보시기 바랍니다. 여기까지가 Spring Transaction Management 예제의 전부입니다. 아래 링크에서 샘플 프로젝트를 다운로드하고 이를 가지고 놀면서 자세히 알아보세요.

Spring JDBC 트랜잭션 관리 프로젝트 다운로드