`Spring Transaction Management` 예제 JDBC

Spring Transaction Management은 Spring 프레임워크의 가장 널리 사용되는 중요한 기능 중 하나입니다. 트랜잭션 관리는 기업 애플리케이션에서 일반적인 작업입니다. 우리는 이미 트랜잭션 관리를 위한 JDBC API를 사용하는 방법을 배웠습니다. Spring은 트랜잭션 관리에 대한 포괄적인 지원을 제공하며, 시스템 장애 발생 시 데이터 무결성에 대해 걱정할 필요 없이 개발자가 비즈니스 로직에 더 집중할 수 있도록 도와줍니다.

Spring Transaction Management

Spring Transaction Management을 사용하는 몇 가지 이점은 다음과 같습니다:

  1. 선언적 트랜잭션 관리를 지원합니다. 이 모델에서 Spring은 트랜잭션 메서드 위에 AOP를 사용하여 데이터 무결성을 제공합니다. 이는 권장되는 접근 방식으로 대부분의 경우에 작동합니다.
  2. JDBC, Hibernate, JPA, JDO, JTA 등과 같은 대부분의 트랜잭션 API를 지원합니다. 우리가 해야 할 일은 적절한 트랜잭션 매니저 구현 클래스를 사용하는 것뿐입니다. 예를 들어, JDBC 트랜잭션 관리에는 org.springframework.jdbc.datasource.DriverManagerDataSource를 사용하고, ORM 도구로 Hibernate를 사용한다면 org.springframework.orm.hibernate3.HibernateTransactionManager를 사용합니다.
  3. TransactionTemplate 또는 PlatformTransactionManager 구현을 사용하여 프로그래밍 방식의 트랜잭션 관리를 지원합니다.

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

Spring 트랜잭션 관리 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;

우리는 여기서 주소 id 열에서 고객 id 열로 외래 키 관계를 정의할 수 있습니다. 그러나 간단함을 위해 여기에는 제약 조건이 정의되어 있지 않습니다. 우리의 데이터베이스 설정은 스프링 트랜잭션 관리 프로젝트에 대비되어 있으며, Spring Tool Suite에서 간단한 Spring Maven 프로젝트를 생성해 봅시다. 최종 프로젝트 구조는 아래 이미지와 같습니다. 이제 각 부분을 하나씩 살펴보면서, 함께하면 JDBC를 사용한 간단한 spring 트랜잭션 관리 예제를 제공할 것입니다.

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>

I have updated the Spring versions to the latest one as of today. Make sure MySQL database driver is compatible with your mysql installation.

스프링 트랜잭션 관리 – 모델 클래스

우리는 두 개의 자바 빈, Customer와 Address를 생성할 것입니다. 이 빈들은 테이블에 매핑됩니다.

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 빈이 Address를 변수로 가지고 있음에 주목하세요. Customer의 DAO를 구현할 때, 우리는 고객과 주소 테이블의 데이터를 가져와서 이 두 테이블에 대해 별도의 삽입 쿼리를 실행할 것이므로 데이터 일관성을 위해 트랜잭션 관리가 필요합니다.

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

간단하게 고객 및 주소 테이블에 레코드를 삽입하는 메서드만 가지는 Customer 빈의 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 구현에서는 트랜잭션 관리를 신경쓰지 않습니다. 이렇게 함으로써 우리는 관심사의 분리를 달성하게 됩니다. 왜냐하면 때로는 DAO 구현체를 제 3자로부터 얻고 이러한 클래스들에 대해 제어권을 가지지 못하기 때문입니다.

봄 선언적 트랜잭션 관리 – 서비스

우리는 고객 및 주소 테이블에 레코드를 삽입할 때 트랜잭션 관리를 제공하는 CustomerDAO 구현을 사용하는 Customer 서비스를 만들어보겠습니다.

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 구현을 살펴보면, createCustomer() 메서드에 @Transactional 주석을 달아 선언적 트랜잭션 관리를 제공하고 있음을 알 수 있습니다. 이렇게만 코드에 추가하면 스프링 트랜잭션 관리의 이점을 얻을 수 있습니다. @Transactional 주석은 메서드뿐만 아니라 전체 클래스에도 적용할 수 있습니다. 모든 메서드에 트랜잭션 관리 기능을 적용하려면 해당 주석을 클래스에 달아야 합니다. 자세한 내용은 Java Annotations Tutorial에서 어노테이션에 대해 더 알아볼 수 있습니다. 남은 부분은 스프링 빈을 연결하여 스프링 트랜잭션 관리 예제를 작동시키는 것입니다.

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

“spring.xml”라는 이름의 스프링 빈 구성 파일을 생성하십시오. 이를 테스트 프로그램에서 사용하여 스프링 빈을 연결하고 트랜잭션 관리를 테스트하는 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>

스프링 빈 구성 파일에서 주의해야 할 중요한 사항은 다음과 같습니다:

  • tx:annotation-driven 요소는 주석 기반의 트랜잭션 관리 구성을 사용하는 것을 Spring 컨텍스트에 알려주는 데 사용됩니다. transaction-manager 속성은 트랜잭션 관리자 빈 이름을 제공하는 데 사용됩니다. transaction-manager의 기본값은 transactionManager이지만 혼동을 피하기 위해 여전히 사용하고 있습니다. proxy-target-class 속성은 Spring 컨텍스트에 클래스 기반 프록시를 사용하도록 지시하는 데 사용됩니다. 이 속성이 없으면 다음과 같은 메시지와 함께 런타임 예외가 발생합니다. Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.journaldev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
  • JDBC를 사용하므로 org.springframework.jdbc.datasource.DataSourceTransactionManager 타입의 transactionManager 빈을 생성합니다. 이는 매우 중요하며 트랜잭션 API 사용에 따라 적절한 트랜잭션 관리자 구현 클래스를 사용해야 합니다.
  • dataSource 빈은 DataSource 객체를 생성하는 데 사용되며, driverClassName, url, username 및 password와 같은 데이터베이스 구성 속성을 제공해야 합니다. 이 값을 로컬 설정에 맞게 변경하십시오.
  • 우리는 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");
		// 값이 20자 이상인지 설정하여 SQLException이 발생하도록 합니다.
		address.setAddress("Albany Dr, San Jose, CA 95129");
		customer.setAddress(address);
		return customer;
	}

}

주소 열의 값을 너무 길게 설정하여 데이터를 Address 테이블에 삽입하는 동안 예외가 발생하도록합니다. 이제 테스트 프로그램을 실행하면 다음과 같은 출력이 나옵니다.

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 데이터베이스 드라이버에서는 주소 열에 값이 너무 길다는 예외가 발생합니다. 이제 고객 테이블을 확인해보면 어떤 행도 없다는 것을 알 수 있습니다. 이는 트랜잭션이 완전히 롤백되었음을 의미합니다. 트랜잭션 관리 마법이 어디에서 이루어지는지 궁금하다면 로그를 주의깊게 살펴보세요. Spring 프레임워크가 생성한 AOP와 프록시 클래스를 확인하세요. Spring 프레임워크는 CustomerManagerImpl에 대해 프록시 클래스를 생성하기 위해 Around 어드바이스를 사용하며, 메서드가 성공적으로 반환되면 트랜잭션을 커밋합니다. 예외가 발생하면 전체 트랜잭션을 롤백합니다. 더 많이 알고 싶다면 Spring AOP 예제를 읽어보시기 바랍니다. 이것으로 Spring 트랜잭션 관리 예제에 대한 설명이 끝났습니다. 아래 링크에서 샘플 프로젝트를 다운로드하여 더 많이 배우고 실험해보세요.

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

Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example