본문 바로가기

프레임워크/Spring

[ Spring ] Transaction(트랜잭션)의 모든것

728x90

Transaction 이란?

 

Transaction은 데이터베이스의 상태를 변경시키는 작업의 단위이다.

 

데이터베이스에서 데이터를 다룰 때(데이터 추가, 갱신, 삭제 등) 처리하던 작업이 오류가 발생했을 때 모든 일련의 작업들을 원상태로 되돌린다. 모든 일련의 작업들이 성공해야만 최종적으로 데이터베이스에 데이터를 갱신, 삭제, 추가가 된다.

 

이러한 트랜잭션은 여러 상황에 따라 여러 개가 만들어진다. 하나의 트랜잭션에 Commit 되거나 Rollback 될 수 있다. 위의 말이 해당 말과 같다고 보면 된다.

 

예를 들어 보겠다.

어느 application에 충전 금액이 있고, 충전을 하게 되면 보너스 포인트를 제공한다 가정하자.

 

한 사용자가 금액을 10000원 충전하려 한다.(포인트는 100포인트) 해서 DB에 금액 10000원을 저장하고, 포인트 100을 저장하려 하는데 실패를 해버린다. 이런 상황이 오면 잘못된 작업 단위이기에 다시 저장을 수행해야 하는데

금액 10000원은 이미 저장이 완료되어서 포인트는 주어지지 않고 금액만 10000원이 +되어버린다.

이런 문제점 때문에 트랜잭션은 Commit, Rollback을 이용한다.

 

 

Spring에서 Transaction 사용하는 방법

 

Spring에서는 Transaction 처리를 지원하는데 프로그래밍적 트랜잭션 처리선언적 트랜잭션을 지원해준다.

그중 많이들 사용하고 편한 것이 어노테이션 @Transactional을 선언해 트랜잭션을 처리한다 @Transactional이 선언적 트랜잭션이라고 부른다.

 

1. 프로그래밍적 트랜잭션

- Spring에서 제공하는 프로그래밍적 트랜잭션은 두 가지가 있다.

TransactionTemplate

PlatformTransactionManager

(Spring 팀에서 프로그래밍적 트랜잭션 관리는 TransactionTemplate 사용을 권장한다.)

 

2. 선언적 트랜잭션

-클래스, 메서드 위에 @Transactional을 추가해서 관리한다.

 

프로그래밍적 트랜잭션 - TransactionTemplate

 

TransactionTemplate은 개발자가 Transaction을 시작, 종료 시점을 명시적으로 결정할 수 있도록 Spring에서 제공하는 하나의 방법이다.

public class PointTransaction {

	private PointDao pointDao;
    private TransactionTemplate transactionTemplate;
    
    public void setPointDao(PointDao pointDao) {
    	this.pointDao = pointDao;
    }
    
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
    	this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
    
    public void invoke() {
    	dolnternalTransaction();
    }
    
    public void dolnternalTransaction() throws Exception {
    	transactionTemplate.execute(new TransactionCallbackWithoutResult() { 
        	protected void doInTransactionWithoutResult(TransactionStatus status) { 
            	try { 
                	pointDao.plusPoint() 
                } catch (Exeption ex) { 
                	status.setRollbackOnly(); 
                } 
            } 
        });
    }
}

 

위의 예제는 transactionManager를 주입받고 Transaction 속성을 설정해주고 execute 메서드 내에서 로직을 실행하고 exception이 나면 파라미터로 제공된 TransactionStatus 객체의 setRollbackOnly() 메서드를 호출하여 트랜잭션을 롤백할 수 있다.

 

public class PointTransaction {

	private PointDao pointDao;
    private TransactionTemplate transactionTemplate;
    
    public void setPointDao(PointDao pointDao) {
    	this.pointDao = pointDao;
    }
    
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
    	this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
    
    public void invoke() {
    	dolnternalTransaction();
    }
    
    public void dolnternalTransaction() throws Exception {
    	transactionTemplate.execute(new TransactionCallback() { 
        	protected Object doInTransaction(TransactionStatus status) { 
            	try { 
                	pointDao.plusPoint() 
                } catch (Exeption ex) { 
                	status.setRollbackOnly(); 
                } 
                return 트랜잭션컨텐스트에서실행할메서드()
            } 
        });
    }
}

위의 예제는 TransactionCallbackWithoutResult 사용이 아닌 TransactionCallback을 사용 한 예제이다

만약 트랜잭션 콘테스트에서 실행할 로직이 있다면 작성된 메서드를 반환하면 된다.

 

프로그래밍적 트랜잭션 - PlatformTransactionManager 구현체 사용

 

트랜잭션 관리를 위해 PlatformTransactionManager 구현체를 직접 사용할 수 있다. PlatformTransactionManager 구현체를 Bean에 넘기기만 하면 된다.

그리고 TransactionDefinition, TransactionStatus을 사용해서 롤백 커밋할 수 있다.

 

// PlatformTransactionManager을 Bean으로 만들었다고 가정

privat final PlatformTransactionManager transactionManager;

public void pointPlus() {
	DefaultTransactionDefinition def = new DefaultTransactionDefinition(); 
    def.setName("pointTransaction"); 
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 
    TransactionStatus status = transactionManager.getTransaction(def); 
    try {
    	pointDao.plusPoint();
    } catch (MyException ex) { 
    	transactionManager.rollback(status); 
        throw ex; 
    } 
	txManager.commit(status);
}

위의 예제처럼 PlatformTransactionManager을 빈으로 만들어 직접 구현체로 설정할 수 있다.

 

 

선언적 트랜잭션

 

클래스, 메서드 위에 @Transactional 선언해서 사용하면 트랜잭션 기능이 적용된 프록시 객체가 생성된다.

개발자가 직접 객체를 만들 필요 없이 선언만으로 관리를 편리하게 해 줘서 많은 개발자들이 사용을 하고 있다.

(Spring Boot 에는 선언 전 트랜잭션에 필요한 여러 설정이 이미 되어 있는 탓에 더 쉽게 사용 가능)

 

@Transactional 포함된 메서드가 호출될 경우 Spring은 해당 메서드에 대한 프록시(프록시패턴 디자인 패턴 중 하나)를 만드는데, PlatformTransactionManager를 사용해 트랜잭션을 시작하고, 정상 여부에 따라 커밋 또는 롤백을 알아서 해준다.

 

@Transactional
public void plusPoint() {
	pointRepository.updatePoint(10);
}

@Transactional 어노테이션을 plusPoint 메서드에 선언했다.

Spring이 plusPoint에 대한 프록시를 만들고 해당 로직이 잘 끝났으면 커밋 오류 나면 롤백을 시켜준다.

 

선언적 트랜잭션의 문제점

 

선언적 트랜잭션 @Transactional을 쓸 때 @Transactional을 쓴 메서드를 같은 객체 안에서 불러오게 되면 문제가 발생한다.

왜냐? 선언적 트랜잭션은 프록시 객체 이기 때문에 같은 내부에서 호출하게 되면 트랜잭션이 정상 작동을 하지 않게 되는 것이다.

객체 변경 감지는 트랜잭션이 커밋될 때 작동되는데, spring이 @Transactional 어노테이션을 선언한 메서드가 호출되기 전, transaction begin 코드를 사입하고, 실행된 후에 commit 코드를 삽입하여 변경 감지를 수행하게 유도한다.

spring에서 해당 방식들로 프록시 객체로 한번 더 감싸기 때문에 프록시 객체가 제공하는 메서드를 사용해야만 트랜잭션이 수행되는 것이다.

// 이처럼 같은 객체 내부에서 하게 되면 작동x
@Service
public class MemberService {

  private final MemberRepository memberRepository;

  public void memberInserts(List<Member> members) {
    members.forEach(it -> this.memberInsert(it));
  }
  
  @Transactional
  public void memberInsert(Member member) {
    memberRepository.save(member);
  }
}

///////////////////////////////

// 이처럼 내부가 아닌 외부에서 호출 하는것이다.
// 꼭 내부에서 해야겠다면 의존성 주입으로 프록시 객체를 가져와 사용 하는 방법도 있다
@Controller
public class MemberController {

	private final MemberService memberService;

	@RequestMapping("memberInsert")
	public Member memberInsert(Member member) {
    	memberService.memberInsert(Lists.newArray(member));
    }
}

@Service
public class MemberService implements MemberRepository {
  
  private final MemberRepository memberRepository;
  
  @Transactional
  public void memberInsert(List<Member> members) {
  	members.forEact(it -> memberRepository.save(it));
  }
}

 

프로그래밍적 트랜잭션 와 선언적 트랜잭션 둘 중 뭐가 좋을까?

 

프로젝트 성향마다 다르긴 하다만, 트랜잭션 작업이 많지 않은 프로젝트라면 프로그래밍적 트랜잭션이 효율적이다.

트랜잭션이 몇 개 안된다면 스프링이나 다른 프레임워크에서 트랜잭션 프록시를 원하지 않을 수 있다. 그럴 때 직접적으로 내가 코드를 구현하는 TransactionTemplate 처리 방식이 좋다.

 

만약 트랜잭션 작업이 많다면 선언적 트랜잭션이 관리에 용이하고 효율이 좋다.

 

 

   관련 글
 

저의 글을 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 보내세요.

저의 글이 조금이나마 도움이 되셨다면 로그인이 필요 없는 공감♥ 한번 꾸욱 눌러주세요 하하~

728x90