[Spring] Spring Transaction

nimoh·2023년 3월 22일
0

Transaction

목록 보기
2/2
post-thumbnail

트랜잭션은 DB 기술이기 때문에 이를 Spring에서 구현할 수 있는 방법이 필요하다.
방법에는 몇 가지가 있는데 하나씩 알아보며 각각의 문제점을 알아보겠다.

Connection에 직접 접근하는 방법

스프링부트에서는 DataSource 인터페이스에서 getConnection()을 호출할 시에 HikariCP 커넥션 풀에서 커넥션을 꺼내온다.

사용법

	import javax.sql.DataSource;
	import java.sql.Connection;

	@Service
	public class ConnectionService {
		private final DataSource dataSource; // DataSource는 외부에서 주입받았다.

		public void accountTransfer(String fromId, String toId, int money) throws SQLException {
		        Connection con = dataSource.getConnection();
		        try {
		            con.setAutoCommit(false); // 트랜잭션 시작
		            //비즈니스 로직
		            bizLogic(con, fromId, toId, money);
		            con.commit();
		        } catch (Exception e) {
		            con.rollback(); // 실패 시 롤백
		            throw new IllegalStateException(e);
		        }finally {
		            release(con); // release를 위한 메서드가 따로 정의되어 있음
		        }
		}
        
    	private void bizLogic(Connection con, String fromId, String toId, int money){
			// 비즈니스 로직
		}
        
    	private void release(){
			// 커넥션을 반환하기 위한 코드
		}
    }

위 코드는 DataSource로 커넥션을 받아오고 커넥션을 수동커밋으로 전환(con.setAutoCommit(false))하면서 트랜잭션이 시작된다.

수동 커밋과 자동 커밋에 대한 내용은 트랜잭션의 기본 포스트에서 확인할 수 있다.

문제점

위 코드는 문제없이 트랜잭션을 수행하는 코드이다. 그래도 문제점을 꼽자면 트랜잭션 관련 코드가 반복될 수 있다. 좀 더 중요한 것은 해당 클래스(ConnectionService)의 Datasource와 Connection는 외부 라이브러리이다.

import javax.sql.DataSource;
import java.sql.Connection;

해당 클래스는 서비스 계층인데 서비스 계층은 외부 라이브러리 코드를 사용하는 것을 권장하지 않는다. 서비스 계층은 애플리케이션의 핵심 비즈니스 로직을 담당하기 때문에 웹 애플리케이션에서 가장 중요한 부분이기 때문이다. 또한, 외부 라이브러리를 사용하면 서비스 계층을 테스트하기 어렵다. 때문에 순수 자바코드로 작성하는 것이 좋다.

트랜잭션 매니저

스프링에는 트랜잭션을 지원해주는 PlatformTransactionManagerTransactionStatus가 있다. 이를 사용하면 위에서 사용한 Connection이나 Datasource에 의존하지 않아도 트랜잭션을 구현할 수 있다.

사용법

	
    @RequriedArgsConstructor
	@Service
	public class ConnectionService {
			
        private final PlatformTransactionManager transactionManager;
        
		public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        		// 트랜잭션 시작
		        TransactionStatus stauts = transactionManager.getTransaction(new DefaultTransactionDefinition());
		        try {
		            //비즈니스 로직
		            bizLogic(fromId, toId, money);
					transactionManager.commit(status);
		        } catch (Exception e) {
		            transactionManager.rollback(status); // 실패 시 롤백
		            throw new IllegalStateException(e);
		        }
		}
        
    	private void bizLogic(String fromId, String toId, int money){
			// 비즈니스 로직
		}
    }

앞서 말했듯이 PlatformTransactionManager는 스프링이 지원하기 때문에 외부 라이브러리에 의존하는 코드는 모두 사라졌다. 그리고 트랜잭션 매니저는 해당 트랜잭션이 커밋 혹은 롤백으로 종료되는 경우 자동으로 커넥션을 반환해준다. 따라서 코드를 보면 커넥션을 반환하는 코드가 삭제되었다.

문제점

PlatformTransactionManager을 사용한 경우 외부 라이브러리에 대한 의존을 제거함으로써 순수 자바코드가 되었으나 트랜잭션을 위한 코드가 중복되고 있다. 현재 위의 코드의 경우 실제 로직은 bizLogic() 밖에 없으며 나머지는 모두 트랜잭션 코드이다. 만약 다른 메서드에서 트랜잭션을 구현하려면 똑같은 코드를 또 중복 작성해야한다.

트랜잭션 템플릿

스프링은 트랜잭션 템플릿이라는 것을 제공한다. Template이라는 것은 어떤 '틀'을 말하는 것인데 비즈니스 로직만 쏙 넣을 수 있게끔 트랜잭션 틀을 만들어준다는 것이다. 코드로 알아보자

사용법

	
	@Service
	public class ConnectionService {
			
        private final TransactionTemplate txTemplate;
        
    	public ConnectionService(PlatformTransactionManager txManager){
        	this.txTemplate = new TransactionTemplate(txManager);
        }    
        
		public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        	txTemplate.executeWithoutResult((status)->{
            	try {
                	bizLogic(fromId, toId, money);
				} catch (SQLException e) {
                // 람다에서 체크 예외(SQLException)를 던질 수 없기때문에 언체크 예외(IllegalStateException)를 대신 던지며 실제 예외를 넣어줌
                	throw new IllegalStateException(e); 
                }
            })
		}
        
    	private void bizLogic(String fromId, String toId, int money){
			// 비즈니스 로직
		}
    }

트랜잭션 템플릿(TransactionTemplate)에 주입받은 트랜잭션 매니저를 넣어주었다.

 this.txTemplate = new TransactionTemplate(txManager);

자 어떤가? 트랜잭션매니저로 부터 트랜잭션을 가지고 오고 직접 커밋하고 예외 시 롤백하는 코드가 사라지고 람다식 하나만 남았다.
람다식 내부를 보면 예외처리만 하고 있는데, 예외 발생 시 트랜잭션 템플릿이 자동으로 롤백해주고 예외가 발생하지 않는 경우에는 커밋한 뒤 커넥션을 반환해준다. 매우 간단하고 편리하게 변경되었다.

문제점

사실 여기까지 오면 앞에서 정말 별 문제가 없다. 트랜잭션 템플릿과 트랜잭션 매니저 관련 의존관계를 설정해주는 것 말고는 정말 없다. 하지만 이것 역시 귀찮다면...?

트랜잭션 AOP

트랜잭션은 매우 중요하기 때문에 전세계적으로 수많은 개발자들이 사용해야한다. 그래서 스프링은 트랜잭션 템플릿을 AOP로 제공해준다. 트랜잭션 AOP가 무슨 말인지 모르더라도 코드를 보면 한 번쯤 봤던 코드일 것이다.

사용법

	
	@Service
	public class ConnectionService {

  		@Transactional
		public void accountTransfer(String fromId, String toId, int money) throws SQLException {
                	
                    bizLogic(fromId, toId, money);
                    
		}
        
    	private void bizLogic(String fromId, String toId, int money){
			// 비즈니스 로직
		}
    }

지금까지 열심히 코딩했던 트랜잭션 코드들이 모두 사라지고 달랑 @Transactional 애노테이션만 남아있다. 엄청나게 간단하다. @Transactional이 붙은 메서드는 트랜잭션을 수행해준다. 지금까지 그랬듯이 해당 애노테이션 역시 성공 시 커밋하고 실패 시 롤백하며 트랜잭션이 끝나면 자동으로 커넥션 풀에 커넥션을 반환해준다.

💡 @Transactional은 public 메서드에만 트랜잭션이 적용된다. 메서드에 붙어있다면 해당 메서드에 트랜잭션을 적용하지 않고, 클래스에 붙어있다면 해당 클래스의 public 메서드에만 트랜잭션을 적용한다.

문제점

트랜잭션을 사용하는 데에는 모든 문제점이 사라졌다. 그런데 이제와서 보니 순수 자바코드가 아니었다..!!! 범인은 바로 accountTransfer 메서드에서 던져진 SQLException 이다. SQLException은 체크예외이다. 체크예외의 경우에는 해당 메서드 혹은 클래스에서 체크예외가 발생한다는 것을 무조건 적시해줘야한다.
그렇지 않으면 컴파일 에러가 발생한다.

	public void occurException() throws SQLException {
    	throw new SQLException();
    }

그렇다면 던진 예외를 받는 모든 계층에서 throws SQLException을 해주어야 한다는 것이다. 이는 코드의 유지보수성을 저하시킨다.

해결법

우리는 앞서 서비스 계층에서 순수 자바코드를 사용하기로 했다. 체크 예외인 SQLException을 자바가 지원하는 언체크 예외로 변경해보자. 언체크 예외는 RuntimeException과 그 하위 예외들을 말한다.

	@Service
	public class ConnectionService {

  		@Transactional
		public void accountTransfer(String fromId, String toId, int money) {
                try{
                    bizLogic(fromId, toId, money);
                } catch (SQLException e){
                	throw new RuntimeException(e);
                }
		}
        
    	private void bizLogic(String fromId, String toId, int money){
			// 비즈니스 로직
		}
    }
	

SQLException 발생 시 RuntimeException으로 변경해서 던져주었다. 이제 SQL코드에도 의존하지 않고 throws 되물림 역시 사라졌다.

💡 위와 같이 예외를 변환해서 던져주는 경우 꼭 변환된 예외에 실제 발생한 예외를 넣어주자! 그렇지 않으면 나중에 예외가 발생했을 때 실제로 어떤 예외가 발생했는 지 추적하기 어렵다.

profile
부족함을 인정하는 순간이 성장의 시작점이다.

0개의 댓글