I T H

[스프링프로젝트연습 15] 스프링 프로젝트 구현 - 트랜잭션 처리 본문

Spring Basic

[스프링프로젝트연습 15] 스프링 프로젝트 구현 - 트랜잭션 처리

thdev 2024. 1. 22. 14:45
앞서 진행한 CRUD 작업에서는 트랜잭션을 적용하지 않은 상황으로 해당 문서에서는 트랜잭션 적용을 위해 필요한 라이브러리 주입 및 트랜잭션 처리, 기능 테스트를 진행하고자 한다.

 

 트랜잭션 : 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다. 유사한 시스템에서 상호작용의 단위이다. 특징에는 원자성, 일관성, 독립성, 지속성이 있다

 

- 커밋(Commit) : 하나의 트랜잭션이 성공적으로 끝났을 경우 수행이 끝났음을 알려주기 위한 연산으로 데이터베이스의 데이터를 입력하거나 수정 등의 작업이 실제로 반영되는 상태를 의미한다.

 

 롤백(Rollback) : 트랜잭션 처리 과정 중 오류 및 예외로 인해 수행이 완료되지 않은 경우 해당 일련의 과정을 취소시키기 위한 연산으로 입력 및 수정 등의 작업 과정이 실제 반영되지 않는다.

 

- AOP : AOP Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화 하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

 

- 기능 테스트의 경우 서비스 단에서 1개의 정상 쿼리를 통해 데이터를 insert 한 후 이어서 오류 발생 쿼리를 통해 데이터를 insert 한 경우 트랜잭션이 적용된 경우에는 앞서 insert 한 데이터가 롤백 되는 것을 확인하고, 트랜잭션이 적용되지 않은 경우에는 앞서 insert 한 데이터가 데이터베이스 테이블에 반영되는 것을 확인한다.

 

[ pom.xml 파일 수정 ]

 

- 이미 3장에서 추가한 경우라면 진행하지 않아도 된다.

- 트랜잭션 처리 및 AOP 적용을 위해 사용되는 라이브러리에 대한 의존성을 주입한다.

- 필요한 라이브러리는 아래 그림과 같다. (2)

- 데이터베이스 연동을 위해 의존성 주입한 경우 해당 라이브러리는 자동으로 다운이 되어 있을 것이다. 추가로 해주어야 하는 부분은 AOP와 관련한 라이브러리에 대한 의존성을 주입하여 준다.

<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-tx</artifactId>
		    <version>${org.springframework-version}</version>
		</dependency>
		
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-jdbc</artifactId>
		    <version>${org.springframework-version}</version>
		</dependency>

<!-- AspectJ (AOP) -->
		<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>

 

- 위 라이브러리를 이미 추가한 경우라면 패스해도 된다.

 

ㄱ. RootConfig.java 파일 수정  

- 트랜잭션 관리를 위해 사용되는 인터페이스를 Bean으로 추가하여 준다.

- 인터페이스는 PlatformTransactionManager

(참고) 스프링 트랜잭션 추상화의 핵심 인터페이스는 PlatformTransactionManager . 모든 스프링의 트랜잭션 기능과 코드는  인터페이스를 통해서 로우레벨의 트랜잭션 서비스를 이용할  있다. PlatformTransactionManager 인터페이스는 다음과 같이  개의 메소드를 갖고 있다.


TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

void commit(TransactionStatus status) throws TransactionException;

void rollback(Transaction status) throws TransactionException;

 

// 클래스 내부에 아래 내용 추가 
@Bean
	public PlatformTransactionManager transactionManager() {
		return new DataSourceTransactionManager(dataSource());
	}

 

 

ㄴ. TransactionConfig.java 파일 신규 생성

- 트랜잭션 처리를 위해 사용될 룰 관리를 설정하기 위한 클래스이다.

-  kr\co\values\init 패키지 아래에 신규 클래스 생성

- TransactionInterceptor : 트랜잭션 어드바이스로 사용하도록 스프링이 제공하는 객체

package kr.co.values.init;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

@Aspect // AOP를 통한 Global Transaction을 설정
// 트랜잭션을 건별 선언이 아닌 패키지(인터페이스) 단위로 설정하고자 할 때를 의미한다.

@Configuration
@EnableAspectJAutoProxy
public class TransactionConfig {

	private static final Logger LOGGER = LoggerFactory.getLogger(TransactionConfig.class);
	private static final int TX_METHOD_TIMEOUT = 3; 
	private static final String AOP_POINTCUT_EXPRESSION = "execution(* kr.co.values..*ServiceImpl.*(..))";

	@Autowired
	private PlatformTransactionManager transactionManager;

	@Bean
	public TransactionInterceptor txAdvice() {
		TransactionInterceptor txAdvice = new TransactionInterceptor();
		
		// 트랜잭션의 경계를 설정할 때에는 Propagation, Isolation, Timeout, ReadOnly 의
		// 네 가지 트랜잭션 속성으로 트랜잭션마다의 설정을 지정할 수 있다.
		// 아래는 select, search 등의 접두사로 이루어진 메소드인 경우에는 ReadOnly 속성을 통해 트랜잭션을 읽기 전용으로만 설정 
		// 그 외 (insert, update 등) 는 롤백룰을 적용하여 트랜잭션 대상으로 지정
		Properties txAttributes = new Properties();
		List<RollbackRuleAttribute> rollbackRules = new ArrayList<RollbackRuleAttribute>();
		rollbackRules.add(new RollbackRuleAttribute(Exception.class));

		// 읽기 전용 
		DefaultTransactionAttribute readOnlyAttribute = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
		readOnlyAttribute.setReadOnly(true);
		readOnlyAttribute.setTimeout(TX_METHOD_TIMEOUT);

		// 룰 적용 (쓰기 전용)
		RuleBasedTransactionAttribute writeAttribute = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED, rollbackRules);
		writeAttribute.setTimeout(TX_METHOD_TIMEOUT);

		String readOnlyTransactionAttributesDefinition = readOnlyAttribute.toString();
		String writeTransactionAttributesDefinition = writeAttribute.toString();

		LOGGER.info("Read Only Attributes :: {}", readOnlyTransactionAttributesDefinition);
		LOGGER.info("Write Attributes :: {}", writeTransactionAttributesDefinition);

		// readonly 속성을 적용할 메소드 접두사를 적용 
		txAttributes.setProperty("retrieve*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("select*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("get*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("list*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("search*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("find*", readOnlyTransactionAttributesDefinition);
		txAttributes.setProperty("count*", readOnlyTransactionAttributesDefinition);

		// 그 외에는 트랜잭션 적용
		txAttributes.setProperty("*", writeTransactionAttributesDefinition);
		txAdvice.setTransactionAttributes(txAttributes);
		txAdvice.setTransactionManager(transactionManager);
		
		return txAdvice;

	}

	@Bean
	public Advisor txAdviceAdvisor() {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression(AOP_POINTCUT_EXPRESSION);

		// 위에서 선언한 트랜잭션 설정에 aop 적용 
		// AOP_POINTCUT_EXPRESSION 에 선언한 패키지 (인터페이스)에만 설정을 적용한다.
		return new DefaultPointcutAdvisor(pointcut, txAdvice());

	}

}

 

ㄷ. RootConfig.java 수정  어노테이션 추가

- 트랜잭션 설정 java (TransactionConfig.java) 파일 신규 생성이 완료된 이후 다시 RootConfig.java 설정 파일로 돌아와서 아래 내용을 추가한다.

- 상단에 어노테이션 추가

@Import({ TransactionConfig.class })
@EnableTransactionManagement

 

 

ㄹ.Service 패키지 생성 및 인터페이스 생성

- Insert , update 등의 비즈니스 로직에 트랜잭션 적용을 위해 사용될 서비스 패키지 및 인터페이스를 생성한다.

- 위에서 AOP를 통해 아래와 같이 설정하였으므로 패키지 아래의 ServiceImpl 이 포함된 클래스는 트랜잭션이 적용되도록 되어 있다. 따라서 클래스명 생성 시 주의해야 한다.

- 패키지 및 인터페이스, 인터페이스 구현클래스를 각각 생성한다.

 

[ 완성된 폴더 구성 ]

 

 

- MainService.java 인터페이스 생성 후 테스트용 메소드를 하나 설정한다.

package kr.co.values.service;

import java.util.Map;

public interface MainService {
	
	//TEST
	void saveTest(Map<String, Object> map);
}

 

-  위에서 만든 인터페이스를 구현할 클래스를 하나 생성하고 메소드의 내용을 상세 기술한다.

package kr.co.values.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.values.persistence.MainMapper;

@Service
public class MainServiceImpl implements MainService{
	
	@Autowired
	private MainMapper mainMapper;

	@Override
	public void saveTest(Map<String, Object> map) {

	// 아래는 트랜잭션 테스트를 해보기 위해 insert 2번을 실행한다. 

		map.put("userId", "test1");
		map.put("userPwd", "test1");
		map.put("userName", "test1");
		map.put("userAuth", "1");
		map.put("userTel", "test1");
		map.put("userEmail", "test1");
		
		mainMapper.addList(map);
		
		map.put("userId", "test2");
		map.put("userPwd", "test2");
		map.put("userName", "test2");
		map.put("userAuth", "112313123123"); // char 자리수 초과하여 에러를 발생
		map.put("userTel", "test2");
		map.put("userEmail", "test2");
		
		mainMapper.addList(map);
	}

	

}

 

ㅁ. 매퍼 메소드 및 쿼리 작성 (11장에서 만든 insert 쿼리 재사용)

- 11장에서 만든 아래 메소드를 그대로 사용할 것이므로 추가로 작성할 내용은 없다.

// 1. DATA INSERT
	public void addList(Map<String, Object> map);

 

ㅂ. 컨트롤러 클래스 수정  서비스 호출 위한 컨트롤러 메소드 생성

- MainController.java 파일에 service 인터페이스 호출을 위한 메소드를 하나 추가한다.

- 상단에 어노테이션을 포함하여 서비스 호출을 위한 변수를 지정한다.

@Autowired
	private MainService mainService; // 서비스 호출 변수 지정

… 중간 생략

// 트랜잭션 테스트
    @RequestMapping("/main/testTransaction.do")
    @ResponseBody
    public Map<String, Object> testTransaction(@RequestBody Map<String, Object> params) {
    	
    	System.out.println("트랜잭션 테스트");
    	
    	mainService.saveTest(params);
    	Map<String, Object> result = new HashMap<String, Object>();
    	result.put("result", true);
    	
        return result;
    }

 

ㅅ. 화면 수정  버튼 추가

-  앞서 백엔드 구현이 완료된 이후 화면에서 컨트롤러 호출을 위한 버튼을 하나 생성하고 해당 버튼에 이벤트를 적용한다.

-  home.jsp 파일에 버튼 추가 및  crud.js파일 내 스크립트를 추가한다.

<button id="btnTransaction">Transaction Test</button>

 

//5. Transaction Test
	$("#btnTransaction").on("click", function(){
		var obj = {};
		dataSaveAjax("/main/testTransaction.do", obj, "POST");
	})

 

[ 트랜잭션 기능 테스트 ]

-  화면에서 트랜잭션 테스트 버튼을 클릭하면 서비스 인터페이스 메소드가 호출되면서 insert 쿼리를 2번 실행할 것이다.

-  이 때 첫번째 쿼리는 오류 없이 동작하여 insert 쿼리가 수행이 될 것이고 두번째 쿼리는 컬럼 사이즈를 초과하는 값을 넣어 오류를 발생하였으므로 쿼리가 정상 실행이 되지 않을 것이다.

-  이 때, 트랜잭션이 정상적으로 적용된 경우라면 첫번째 쿼리 또한 커밋되지 않고 롤백되어 데이터베이스 테이블을 조회할 경우 입력된 데이터가 없어야 한다.

-  반대로 트랜잭션 설정을 제거 (아래 어노테이션 제거) 하고 위 과정을 반복해보면 첫번째 쿼리의 데이터는 테이블에 반영이 될 것이다. -> 트랜잭션이 설정되지 않았으므로 두번째 쿼리 성공 유무와 관계 없이 첫번째 쿼리는 커밋된다.

 

[결과화면]

-  트랜잭션이 잘 적용 되서 첫번째 insert의 경우 올바른 쿼리임에도 불구하고, 2번째 insert에서 쿼리에 에러가 있기에 2번의 insert 모두 데이터에 반영되지 않음을 알수 있다.