[AOP]Transaction 설정
데이터 작업시에는 반드시 필요한 트랜잭션 처리가 되어야 합니다.
오늘 포스팅할 내용은 스프링에서 트랜잭션 설정하는 방법입니다. 참고로 Spring + MyBatis 설정은 되어있는 상황입니다.
테스트 환경
- JDK 1.8
- Spring 4.3.14
- Tomcat 9.0
- MariaDB
트랜잭션(Transaction)
트랜잭션의 핵심은 작업단위이다. 하나의 트랜잭션은 하나의 작업단위로 처리되어야 하는데, 쉽게 얘기하자면 추가(Insert)와 삭제(Delete)라는 작업이 하나의 작업으로 묶여있다면 두 작업은 모두 성공하거나 실패되어야 한다는 것이다.
작업이 정상적으로 종료되었다면 영구적으로 저장(Commit)되어야 하고 잘못되었다면 되돌려야 한다(Rollback)
Java jdbc를 사용할 때는 커넥션 객체의 setAutoCommit(false) 메소드를 통해 오토 커밋을 해제하고 직접 커밋과 롤백을 수행할 수 있었다. 하지만 Spring jdbc나 MyBatis에서는 커넥션을 자동 생성/커밋이 되기 때문에 커밋/롤백이 자유롭지 않다. 따라서 스프링에서 트랜잭션을 관리해주는 기능 구현이 필요하다.
jdbc를 이용해 DB 연동을 했을 때 트랜잭션 관리는 DataSourceTransactionManager 클래스를 사용한다. DataSourceTransactionManager 클래스는 Connection의 트랜잭션 API를 이용해서 트랜잭션을 관리해주는 트랜잭션 매니저다. 개발 환경에 따라 트랜잭션을 관리하는 방법은 다르지만 본 포스팅에서는 선언적 트랜잭션 관리로서 두 가지 케이스를 테스트했다.
선언적 트랜잭션
- @Transactional 어노테이션을 사용한 트랜잭션 관리
- AOP 기반 트랜잭션 관리
각각의 관리 방법 및 세부 내용은 아래 예제를 진행하면서 작성했다.
테스트를 위한 테이블과 페이지를 만들었다.(Controller, Service, VO, Mapper 포함)
Oracle을 사용한다면 상관없지만 MariaDB(MySQL 포함)를 사용하는 경우 아래와 같이 InnoDB 엔진을 사용해야 트랜잭션 기능을 사용할 수 있다고 한다.
* InnoDB 엔진은 MySQL 5.5 이후 기본적으로 사용되었다고 한다. 만약 엔진이 MyISAM이면 InnoDB로 변환하는 작업이 필요하다.
ex01.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>Spring Transaction Example</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
function testTransaction() {
var params = {
testno : $("#testno").val(),
testname : $("#testname").val()
};
$.ajax({
type : "POST",
async : false,
url : "/transaction/testTransaction.do",
cache : false,
dataType : "json",
data : JSON.stringify(params),
contentType: "application/json; charset=utf-8",
success: function(data) {
var msg = data.msg;
$("#result").html(msg);
},
error : function(request, status, error) {
alert("code : " + request.status + "\n" + "message : " + request.responseText + "\n" + "error : " + error);
}
});
}
$(document).ready(function() {
});
</script>
</head>
<body>
<form id="testForm">
<div class="container">
<div class="row border-bottom mb-3">
<div class="col-12">
<p class="h1">1. 선언적 트랜잭션</p>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">TESTNO</span>
<input type="text" class="form-control" id="testno" name="testno">
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">TESTNAME</span>
<input type="text" class="form-control" id="testname" name="testname">
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<button type="button" class="btn btn-primary mb-3" onclick="testTransaction()">전송</button>
</div>
</div>
<div class="row">
<div class="col-auto">
결과 : <span id="result"></span>
</div>
</div>
</div>
<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>
데이터 매핑은 Gson을 이용하였다.
Gson은 Json(Key : Value 형태) 문자열을 VO(DTO)에 선언한 변수와 자동 매핑해주는 자바 라이브러리이다.
pom.xml
<!-- Gson : JSON - VO 매핑 -->
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.4</version>
</dependency>
TransactionVO.java
package com.study.transaction.vo;
public class TransactionVO {
private int testno;
private String testname;
public int getTestno() {
return testno;
}
public void setTestno(int testno) {
this.testno = testno;
}
public String getTestname() {
return testname;
}
public void setTestname(String testname) {
this.testname = testname;
}
}
exMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="transaction">
<insert id="insertData" parameterType="transactionVO">
INSERT INTO PM_TEST(
TESTNO
, TESTNAME
)
VALUES(
#{testno}
, #{testname}
)
</insert>
<insert id="insertNull">
INSERT INTO PM_TEST(
TESTNO
, TESTNAME
)
VALUES(
99
, null
)
</insert>
</mapper>
PM_TEST 테이블에 TESTNAME 필드는 Not Null이기 때문에 insertNull 쿼리는 반드시 에러가 나도록 되어있다.
TransactionDAO.java
package com.study.transaction.dao;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.study.transaction.vo.TransactionVO;
@Repository("TransactionDAO")
public class TransactionDAO {
@Autowired
private SqlSession sqlSession;
public void insertData(TransactionVO transactionVO) {
sqlSession.insert("transaction.insertData", transactionVO);
}
public void insertNull() {
sqlSession.insert("transaction.insertNull");
}
}
TransactionService.java
package com.study.transaction.service;
import com.study.transaction.vo.TransactionVO;
public interface TransactionService {
public void insertEx01(TransactionVO transactionVO) throws Exception;
}
TransactionServiceImpl.java
package com.study.transaction.service.impl;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.study.transaction.dao.TransactionDAO;
import com.study.transaction.service.TransactionService;
import com.study.transaction.vo.TransactionVO;
@Service("transaction.TransactionService")
public class TransactionServiceImpl implements TransactionService {
@Inject
TransactionDAO transactionDAO;
@Override
public void insertEx01(TransactionVO transactionVO) throws Exception {
transactionDAO.insertData(transactionVO); // Success
transactionDAO.insertNull(); // Error! Roll Back
}
}
TransactionService 인터페이스의 실제 구현체인 TransactionServiceImpl 클래스에서 insertEx01 메소드로 insertData와 insertNull을 하나의 작업단위로 묶어 처리했다. 여기서 insertNull 실행시 에러가 나면 insertData가 롤백되는지가 테스트의 핵심이다.
TransactionController.java
@RequestMapping(value = "/transaction/testTransaction.do", method = { RequestMethod.POST })
public @ResponseBody JSONObject testTransaction(@RequestBody String jsonParam) throws Exception {
Gson gson = new Gson();
JSONObject result = new JSONObject();
TransactionVO transactionVO = new TransactionVO();
try {
transactionVO = gson.fromJson(jsonParam, TransactionVO.class);
transactionService.insertEx01(transactionVO);
result.put("msg", "SUCCESS");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
//throw new CustomSqlException("ERROR");
result.put("msg", "ERROR");
}
return result;
}
여기까지 진행 후 테스트해보면 에러가 났음에도 데이터가 저장되는 것을 확인할 수 있다.
이제 첫번째 방법을 이용해 트랜잭션 관리를 해보자.
1. @Transactional 어노테이션을 사용한 트랜잭션 관리
1) Namespace 등록
servlet-context-.xml(component-scan이 있는 곳)에서 beans xmlns에 tx 항목을 추가하고 schemaLocation을 등록한다.
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
2) DataSourceTransactionManager Bean 생성
DataSourceTransactionManager 클래스를 Bean으로 등록하고 DriverManagerDataSource 클래스를 통해 생성된 Bean 객체(dataSource : root-context.xml 참고)를 의존 주입 시킨다. 본인은 테스트 과정에서 스프링 jdbc 모듈에 있는 DriverManagerDataSource 클래스를 이용했지만 각자 환경에 맞는 DataSource를 의존 주입하면 된다. 즉, 트랜잭션을 적용할 DAO가 사용하는 DataSource를 말한다.
annotation-driven을 등록하지 않으면 @Transactional 어노테이션을 인식하지 못한다.
<!-- 트랜젝션 관리 -->
<beans:bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
<!-- 어노테이션에 기반한 트랜잭션 활성화 -->
<tx:annotation-driven />
root-context.xml
<context:property-placeholder location="/config/config.properties" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${spring.datasource.driverClassname}" />
<property name="url" value="${spring.datasource.url}" />
<property name="username" value="${spring.datasource.username}" />
<property name="password" value="${spring.datasource.password}" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/mybatis-config.xml" />
<property name="mapperLocations" value="classpath:/mapper/**/*Mapper.xml" />
</bean>
<!-- MyBatis - Spring 연동 모듈 -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
3) @Transactional 어노테이션을 선언한다.
@Transactional 어노테이션은 클래스, 메소드 단위에 선언이 가능하다. 클래스에 선언하게 되면 모든 메소드에 적용되고 특정 메소드에 선언시 선언된 메소드에서만 적용된다. 따라서 로직 흐름에 맞춰 알맞게 사용하는 것이 중요하다.
우선 순위는 클래스의 메소드 > 클래스 > 인터페이스의 메소드 > 인터페이스 순이다.
TransactionServiceImpl.java
package com.study.transaction.service.impl;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.study.transaction.dao.TransactionDAO;
import com.study.transaction.service.TransactionService;
import com.study.transaction.vo.TransactionVO;
@Service("transaction.TransactionService")
public class TransactionServiceImpl implements TransactionService {
@Inject
TransactionDAO transactionDAO;
@Transactional
@Override
public void insertEx01(TransactionVO transactionVO) throws Exception {
transactionDAO.insertData(transactionVO); // Success
transactionDAO.insertNull(); // Error! Roll back
}
}
4) 테스트
다시 테스트를 해보면 에러가 발생되어 롤백처리 된 것을 확인할 수 있다.
2. AOP 기반 트랜잭션 관리
앞서 살펴본 @Transactional 어노테이션은 필요한 메소드 또는 클래스에 선언을 해서 사용했었다. 그렇다보니 매번 선언을 해야하는 불편함이 생긴다. AOP 기반의 트랜잭션 관리는 이러한 불편함을 덜어준다.
AOP(Aspect Oriented Programming)
AOP는 관점 지향 프로그래밍이다. OOP(객체 지향 프로그래밍)에 기초를 두는 프로그래밍으로 하나의 애플리케이션을 관점이라는 논리적인 단위로 분리해 관리하는 개념이다. 로깅, 트랜잭션, 보안 등 다양한 곳에서 사용된다.
(AOP에 관해서는 추후에 자세하게 다뤄볼 예정입니다)
이제 두번째 방법을 이용해 트랜잭션을 관리해보자.
1) AspectJ의 pointcut 표현식 사용을 위해 aspectjweaver 라이브러리를 추가한다. (maven 사용시 aspectjrt는 기본으로 설치되어 있음)
pom.xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
2) Namespace 등록
servlet-context-.xml에서 beans xmlns에 aop 항목을 추가하고 schemaLocation을 등록한다.
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
3) 트랜잭션 AOP 설정
트랜잭션을 위한 태그는 "tx" 라는 네임스페이스를 갖는다.
트랜잭션에 사용될 Advisor를 생성한다. <tx:advise> 태그를 이용해서 특정 메소드에 어떻게 적용되는지에 대한 정책을 정할 수 있다. id는 Advisor의 고유 id이며, transaction-manager에는 트랜잭션 관리 객체를 지정한다. 트랜잭션 관리 객체는 위에서 Bean 객체로 생성된 DataSourceTransactionManager 클래스이다.
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes /> : 태그의 자식 태그를 선언해 트랜잭션을 관리할 메소드 및 속성을 지정할 수 있다.
<tx:method />
- name : 트랜잭션이 적용될 메소드명 명시. * 입력시 모든 메소드 적용
- rollback-for : 트랜잭션을 롤백할 예외 타입 명시
이외에도 다양한 속성이 있는데 나중에 심화 과정을 해보면서 추가해야겠다.
<tx:attributes>
<tx:method name="*" rollback-for="Exception" />
</tx:attributes>
Advisor가 준비되면 트랜잭션이 필요한 지점에서 실행될 수 있도록 설정한다. 이 때 AspectJ의 pointcut 표현식을 이용한다. pointcut 표현식은 예제 주석을 참고하고 아래와 같이 사용할 수 있다.
<aop:config>
<!--
execution(* com.study.transaction.service.impl.*Impl.*(..))
[리턴타입 선언] - * : 모든 리턴 타입 포함
[패키지경로 선언] - com.study.transaction.service.impl : 패키지 경로
[클래스명 선언] - *Impl : 패키지내에 Impl로 끝나는 모든 클래스
[메소드명(매개변수) 선언] - *(..) : 모든 메소드 선택
-->
<aop:pointcut id="requiredTx" expression="execution(* com.study.transaction.service.impl.*Impl.*(..))"/>
<!-- 정의된 pointcut과 transactionAdvice를 연결 -->
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="requiredTx" />
</aop:config>
pointcut 표현식
- execution : 가장 상세한 포인트컷을 지정할 수 있다. 리턴타입, 패키지경로, 클래스명, 메소드명(매개변수)까지.
- within : 패턴내에 해당하는 모든 것들을 지정한다.
- bean : 생성된 bean으로 지정한다.
1. execution(public(생략 가능) 리턴타입 패키지경로.클래스명.메소드명(매개변수))
-리턴타입
- * : 모든 리턴 타입
- void : 리턴 타입이 없음
- !void : 리턴 타입이 있음
-패키지경로
- com.study.transaction.service.impl : com.study.transaction.service.impl 까지의 패키지 경로
- com.study.transaction.service.impl.. : com.study.transaction.service.impl로 시작하는 모든 패키지
- com.study.transaction.service..impl : com.study.transaction.service 패키지에서 이름이 impl로 끝나는 패키지
-클래스명
- *Impl : 클래스명이 Impl로 끝나는 클래스
- TransactionServiceImpl : TransactionServiceImpl 클래스
- TransactionService+ : TransactionService 클래스로 파생된 모든 자식 클래스 또는 인터페이스가 구현된 모든 클래스
-메소드명(매개변수)
- *(..) : 모든 메소드
- insert*(..) : insert로 시작하는 모든 메소드
- *(int) : 모든 메소드 중 int 타입 인자값을 갖는 메소드
2. within(패키지경로)
- com.study.transaction.service : com.study.transaction.service 패키지에 있는 모든 메소드
- com.study.transaction..* : com.study.transaction 패키지 및 하위 패키지에 있는 모든 메소드
3.bean(빈 이름)
- bean(testTransaction) : 이름이 testTransaction인 빈의 메소드
- bean(*test) : 이름이 test로 끝나는 빈의 메소드
4) 테스트
이제 기존에 있던 @Transactional 어노테이션을 지우고 테스트해보자.
TransactionServiceImpl.java
package com.study.transaction.service.impl;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.study.transaction.dao.TransactionDAO;
import com.study.transaction.service.TransactionService;
import com.study.transaction.vo.TransactionVO;
@Service("transaction.TransactionService")
public class TransactionServiceImpl implements TransactionService {
@Inject
TransactionDAO transactionDAO;
@Override
public void insertEx01(TransactionVO transactionVO) throws Exception {
transactionDAO.insertData(transactionVO); // Success
transactionDAO.insertNull(); // Error! Roll back
}
}
동일하게 롤백되는 것을 확인할 수 있다.
참고자료
전체 소스는 GitHub에서 확인하실 수 있습니다.
+피드백은 언제나 환영입니다 :)