▸Spring MVC/기본 문법

스프링, Mybatis, MySQL_트랜잭션 처리 [4/5]

코데방 2020. 3. 22.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

[ 트랜잭션(TransAction) ]

  • 한번에 수행되어야 할 쿼리의 묶음(Set)
  • 모두 다 정상적으로 수행되지 못한다면, 아무것도 수행되지 않도록 하는 묶음 단위

트랜잭션은 '거래'라는 의미로 "실제로 돈을 받아야 통장에서 차감된다" 라는 의미입니다. 돈을 못받았는데 통장에서 차감되면 큰일나니까요. "모 아니면 도" 라는 말로 이해해도 될 것 같습니다.

 

이상하게도 Mabatis에는 DB 작업의 트랜잭션을 처리할 수 있는 기능을 넣어두지 않았습니다. 그냥 스프링의 힘을 빌려 처리하라고 되어 있습니다. 따라서 스프링의 기능을 빌려 몇 가지 방법으로 트랜젝션 처리를 할 수 있습니다.

 

트랜잭션을 처리하기에 앞서, 일단 MySQL의 경우 InnoDB 엔진을 사용해야 트랜잭션 기능을 사용할 수 있다고 합니다. 아래 코드를 입력하면 엔진 타입을 확인할 수 있습니다. 전 도커로 5.7버전을 받아 설치했는데 다행히 InnoDB로 되어있네요. 

 

select engine, support from information_schema.engines where support='DEFAULT';

 

만약 MyISAM 타입으로 되어 있다면 InnoDB로 바꿔줘야 한다고 합니다. MySQL을 잘 몰라서 이부분은 따로 검색을 하셔야할 것 같습니다. 아니면 저 같이 도커로 간단하게 다시 설치하는 방법도 있습니다. 카테고리에 Docker란에 보면 설치 방법이 나와있습니다.

 

 


 

 

커밋의 핵심은 DataSource 객체에서 생성해주는 Connection 객체에 있습니다. 프레임워크 없이 JAVA JDBC를 사용할 때는 Connection 객체의 setAutoCommit(flase) 메소드를 통해 오토 커밋을 해제하고 직접 커밋과 롤백을 수행해줬습니다.

 

하지만 Spring JDBC나 Mybitis에서는 DataSource 객체만 제공해주면 커넥션을 알아서 생성해 사용하므로 개발자가 직접 위와 같은 방법으로 오토커밋을 해제하고 커밋/롤백을 자유롭게 사용할 수가 없습니다. 따라서 스프링에서 트랜잭션을 관리해주는 객체에다가 DataSource 객체(Bean)를 넘긴 뒤, 해당 객체를 사용해 트랜잭션을 관리하면 됩니다.

 

구현하는 방법은 여러가지가 있지만 이 과정만 이해하면 크게 어려운 부분은 없습니다.

 

  • 스프링의 트랜잭션 관리 클래스 : DataSourceTransactionManager

 

 


 

 

 

[ '@Transactional' 어노테이션을 사용한 트랜잭션 처리 ]

 

가장 쉽고 직관적인 방법입니다. 장점은 트랜잭션 처리가 필요한 메소드에만 적용할 수 있다는 점이고, 단점은 메소드마다 개발자가 직접 어노테이션을 선언해줘야 한다는 점입니다. 개발자의 실수로 트랜잭션이 필요한 부분에 선언되지 않으면 큰 장애로 이어질 수 있다는 위험이 존재합니다. 

 

이 어노테이션을 메소드에 선언하면 메소드 내에서 실행된 쿼리를 트랜잭션으로 쌓아두다가 정상적으로 수행이 완료되면 커밋을 하고, 예외가 발생할 경우 모두 롤백를 시켜줍니다. 서블릿에서 직접 try-catch 문으로 커밋과 롤백을 수행해 주던 것을 대신 해준다고 보면 됩니다. 그 과정을 어노테이션 하나로 해결할 수 있으니 코드가 획기적으로 짧아지게 됩니다. 

 

 

1.  DataSourceTransactionManager 클래스를 Bean으로 등록

 

스프링의 핵심 기능들의 많은 부분은 스프링(IOC) 컨테이너에 있습니다. 트랜잭션 처리 또한 마찬가지입니다. 따라서 먼저 스프링의 트랜잭션을 관리하는 해당 클래스를 Bean으로 등록해줍니다. DB 리소스 관련이므로 루트 컨테이너가 참조하는 xml 파일에 설정해줍니다. DB 관련 Bean 설정이 있는 곳에 같이 해주면 됩니다.

	<!-- 트랜젝션 관리 객체 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

 

 

2. 트랜젝션 어노테이션  찾기 설정 추가

 

컨트롤러 클래스에 @Controller를 선언하면 알아서 Bean으로 만들어서 등록해주고 핸들러와 매핑을 해주는 것처럼, 스프링에서 어노테이션을 찾아서 뭔가 작업을 해주는 일은 컨테이너가 담당합니다. 그리고 트랜잭션  어노테이션을 검색해서 처리해주는 설정은 아래와 같습니다. 

 

"트랜잭션 어노테이션이 선언된 메소드는 transactionManager 라는 Bean 객체가 처리할 수 있도록 해준다" 라는 의미입니다. 파라미터로 실제 DB 연결에 사용한 dataSource 빈(Bean)을 DI 해주면 됩니다.

	<!-- 트랜젝션 관리 객체 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- @Transactional 어노테이션 처리 -->
	<tx:annotation-driven transaction-manager="transactionManager" />

 

 

3. '@Transactional' 어노테이션 선언

 

이제 트랜잭션이 필요한 메소드에 어노테이션을 선언해주기만 하면 됩니다. 아래와 같이 insert를 두 번 해주는 메소드를 하나 만든 뒤, userId2의 매개변수를 null 값으로 줘 두 번째 insert에서 예외가 발생되도록 해봅니다. userId는 null 값이 허용되지 않는 컬럼으로 설정했습니다.

 

	@Test
	public void insertTransaction() {
		
		UserInfoDAO dao = (UserInfoDAO) ctx.getBean("userInfoDAO");
		dao.insert("coconut", "12345", null, "123123");
	}
package hs.spring.hsweb.user.db;

import javax.annotation.Resource;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository("userInfoDAO")
public class UserInfoDAO {

	// insert
	@Transactional
	public void insert(String userId, String userPw, String userId2, String userPw2) {
		
		UserInfoVO vo1 = new UserInfoVO();
		vo1.setUser_id(userId);
		vo1.setUser_pw(userPw);
		
		UserInfoVO vo2 = new UserInfoVO();
		vo2.setUser_id(userId2);
		vo2.setUser_pw(userPw2);
		mapper.insert(vo1);
		mapper.insert(vo2);
	}
}

 

 

어노테이션을 사용하지 않았을 경우 첫번 째 쿼리만 정상적으로 적용돼 'coconut'이라는 값이 삽입되었습니다.

 

 

 

이번엔 어노테이션을 선언한 뒤 실행해보면 같은 예외가 발생하지만 아무 값도 DB에 삽입(커밋)되지 않은 것을 확인할 수 있습니다. 메소드 실행 중간에 예외가 발생했기 때문에 자동 롤백이 수행됐기 때문입니다.

 

 

 

 


 

 

 

[ AOP를 이용한 자동 트랜잭션 처리 ]

 

어노테이션을 일일이 붙여주는 것이 싫다면, AOP 기능을 사용해 특정 메소드가 실행될 때 자동으로 트랜잭션 처리가 될 수 있도록 해줄 수 있습니다. AOP에 대한 개념과 사용 방법은 아래 글을 참조하시면 됩니다.

 

[Spring MVC/- 기본 문법] - 스프링 AOP 구현 (관점 지향 프로그래밍)

 

 

1. DataSourceTransactionManager 클래스를 Bean으로 등록

 

이 클래스가 DataSource 객체를 가지고 트랜잭션 관리를 해주는 핵심이기 때문에, 역시 이 방법에서도 위와 동일하게 Bean으로 등록해줍니다. 대신 어노테이션을 사용하는 방법이 아니므로 "tx:annotation-driven" 설정은 불필요합니다.

	<!-- 트랜젝션 관리 객체 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

 

 

 

2. AOP 설정

 

스프링에서 제공하는 트랜잭션을 위한 AOP 설정입니다. 'tx'는 Transaction을 의미합니다. AOP 클래스를 이용해 작성할 수 있지만 xml에 설정에주는 것이 훨씬 간결하고 편리합니다. 외워서 쓰기도 힘드니 초기 셋팅할 때 찾아서 붙여넣고 사용하면 될 것 같습니다. 코드 자체는 직관적이어서 이해가 어렵진 않습니다.

	<!-- 트랜젝션 관리 객체 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!-- 트랜잭션 AOP 설정 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<tx:method name="insert*" rollback-for="Exception" />
			<tx:method name="update*" rollback-for="Exception" />
			<tx:method name="delete*" rollback-for="Exception" />
		</tx:attributes>
	</tx:advice>
	
	<aop:config>
		<aop:pointcut id="transactionPointcut" 
				expression="within(hs.spring.hsweb..*)" />
		
		<aop:advisor id="transactionAdvisor" 
				pointcut-ref="transactionPointcut" advice-ref="txAdvice" />
	</aop:config>

 

 

트랜잭션을 다루는 AOP는 <aop:advisor>를 사용합니다. 위의 advice 설정을 참조해 AOP를 적용하겠다는 의미입니다. 그리고 메소드의 이름으로 AOP를 적용하기 때문에 트랜잭션을 처리할 메소드 이름을 모두 잘 통일해서 사용해야 한다는 점에 주의하면 됩니다. 테스트 결과는 위와 동일하니 생략하도록 하겠습니다.

 

 

 


 

 

※ 트랜젝션 사용시 주의사항

 

트랜젝션이 걸려 있더라도 DB작업에서 예외 발생 시 로그를 찍기 위해 try-catch문을 사용할 경우가 있습니다. 트랜젝션은 해당 메소드에 예외가 발생할 경우 롤백이 작동하므로, 그 안에서 예외를 잡아버리면 트랜젝션이 걸린 메소드 자체는 예외가 발생하지 않기 때문에 롤백이 진행되지 않습니다.

 

따라서 트랜젝션이 걸린 메소드 안에서 try-catch를 사용해 예외를 잡는다면, 롤백을 위해 catch 구문 안에서 다시 예외를 throw 해줘야 합니다. 기본 설정으로는 RuntimeException을 상속받는 예외만 롤백된다는 점도 주의해야 합니다. 별도 설정 없이 그냥 최상위 Exception 객체로 예외를 받아 그대로 던져주면 롤백이 되지 않습니다.

 

아래는 예시코드입니다. 물론 아래 코드에서는 쿼리가 한 개라 굳이 롤백을 할 필요는 없지만 두 개 이상 CRUD 쿼리가 있다면 꼭 예외를 다시 던져줘야 합니다.

 

		try {
			// bGood +1 업데이트
			mapper.updateBGood(boardBGoodWorkingInfo);

		} catch (DataAccessException e) {
			e.printStackTrace();
			// 롤백을 위해 예외를 다시 던져줌
			throw e;
		}

 

728x90

댓글

💲 추천 글