Framework/Spring

Spring Transaction Propagation을 예제를 통해 알아보자

호형 2020. 5. 21. 14:04

spring에서 transaction propagation 은 전파옵션을 뜻한다. 전파옵션이라는 것은 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값 정도로 생각하면 된다. 즉 큰 트랜잭션의 흐름을 컨트롤 하는 옵션이라는 것이다. 

 

Transaction Propagation의 종류는 다음과 같다. 

· REQUIRED (default) : 이미 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다

· REQUIRES_NEW : 항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.

· SUPPORTS : 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션없이 진행한다.

· NESTED : 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다. 메인 트랜잭션이 롤백되면 중첩된 로그 트랜잭션도 같이 롤백되지만, 반대로 중첩된 로그 트랜잭션이 롤백돼도 메인 작업에 이상이 없다면 메인 트랜잭션은 정상적으로 커밋된다.

· MANDATORY : REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다. 반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.

· NOT_SUPPORTED : 트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.

· NEVER : 트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안된다 있다면 예외를 발생시킨다.

 

이중 프로젝트 수행중에 많이 쓰이는 녀석들은 REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS 이다.

 

이들에 대해 상황별 예제를 통해 이해를 하도록 하겠다. 

 

(테스트 사전 준비로는 commonDao의 insert는 트랜잭션을 선언하지 않았고 이는 곧 별도의 메소드로 분리하지 않은 것이라고 이해하면 된다. 또한 테스트를 위해 insertEmployee() 에서 try catch로 묶었다. 여기서 처리해주지 않으면 부모(insertEmployeeAll()) 에도 영향을 미쳐 부모의 결과에도 영향을 주게 된다. )

 


REQUIRED

 

1. 자식이 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
}

@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
        throw new RuntimeException("자식이 문제");
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

▶ 부모(insertEmployeeAll())는 멀쩡한데 자식(insertEmployee()) 이 문제가 생겼을 경우다. REQUIRED 의 핵심은 의리! 즉 연결되는 모든 트랜잭션은 한 통이라는것이다. 자식이 문제가 생기는 경우 부모가 다 정상수행을 했다 하더라도 모두 롤백이 된다. 

 

 

2. 부모가 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
    throw new RuntimeException("부모가 문제");
}

@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

자식(insertEmployee())은 멀쩡한데 부모(insertEmployeeAll()) 가 문제가 생겼을 경우다. REQUIRED 의 핵심은 의리! 즉 위의 case처럼 모두 롤백이 된다. 

 


REQUIRES_NEW

 

1. 자식이 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
}

@Transactional(propagation=Propagation.REQUIRES_NEW, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
        throw new RuntimeException("자식이 문제");
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

▶ REQUIRED 예제랑 거의 같아서 뭐가 다른지 헷갈릴수도 있는데 insertEmployee() propagation을 보면 차이가 있다. 마찬가지로 부모(insertEmployeeAll())는 멀쩡한데 자식(insertEmployee()) 이 문제가 생겼을 경우다. REQUIRES_NEW 의 핵심은 각개전투! 각자 알아서 논다고 생각하면 된다. 자식에 저 옵션이 붙는 순간 버린자식이라고 보면 된다. 따라서 부모와 자식간의 영향은 없다. 부모가 잘 되었고 자식이 안되었다면 부모는 커밋, 자식은 롤백이 된다고 보면 된다. 

(조던, 우즈 커밋, 타이슨 롤백)

 

 

2. 부모가 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
    throw new RuntimeException("부모가 문제");
}

@Transactional(propagation=Propagation.REQUIRES_NEW, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

자식(insertEmployee())은 멀쩡한데 부모(insertEmployeeAll()) 가 문제가 생겼을 경우다.  REQUIRES_NEW 의 핵심은 각개전투! 부모만 롤백이 되고 자식은 커밋이 된다. 

(조던, 우즈 롤백, 타이슨 커밋)

 


NESTED

 

1. 자식이 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
}

@Transactional(propagation=Propagation.NESTED, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
        throw new RuntimeException("자식이 문제");
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

▶ 부모(insertEmployeeAll())는 멀쩡한데 자식(insertEmployee()) 이 문제가 생겼을 경우다. NESTED 의 핵심은 체크포인트! 롤플레잉 게임에서 저장하듯이 NESTED 호출 직전까지 일단 저장을 해놓는다. 자식이 문제라 롤백이 되도 다시 체크포인트까지만 롤백이 된다. 따라서 부모는 커밋, 자식은 롤백 처리가 된다.  

(조던, 우즈 커밋, 타이슨 롤백)

 

 

2. 부모가 문제인 경우

@Transactional(rollbackFor=Exception.class)
public void insertEmployeeAll() { //부모
    commonDao.insert("employee.insertEmployee", new EmployeeVO("조던"));
    insertEmployee(new EmployeeVO("타이슨"));
    commonDao.insert("employee.insertEmployee", new EmployeeVO("우즈"));
    throw new RuntimeException("부모가 문제");
}

@Transactional(propagation=Propagation.NESTED, rollbackFor=Exception.class)
public void insertEmployee(EmployeeVO employeeVO) { //자식
    try {
        commonDao.insert("employee.insertEmployee", employeeVO);
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

 자식(insertEmployee())은 멀쩡한데 부모(insertEmployeeAll()) 가 문제가 생겼을 경우다.  NESTED 의 핵심은 체크포인트! NESTED 수행 후 문제가 없다면 NESTED에 속한 내용은 부모 트랜잭션의 일부가 된다. 따라서 하나의 트랜잭션으로 묶인 상태에서 부모 트랜잭션에서 문제가 생겼으니 모두 롤백이 된다. 

 


SUPPORTS

 

이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션없이 진행하므로 이건 트랜잭션이 별로 필요가 없는 조회 메소드 같은 경우에 기본적으로 걸어 놓는다. 조회 메소드 같은 경우는 propagation="SUPPORTS" 와 read-only="true" 를 넣어서 트랜잭션을 태우는게 성능적으로 유리하다. 설정은 다음과 같이 하면 된다.

 

<tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
    <tx:method name="create*" rollback-for="Exception" />
    <tx:method name="insert*" rollback-for="Exception" />
    <tx:method name="update*" rollback-for="Exception" />
    <tx:method name="delete*" rollback-for="Exception" />
    <tx:method name="save*" rollback-for="Exception" />
    <tx:method name="logging*" rollback-for="Exception" propagation="REQUIRES_NEW"/>
    <tx:method name="*" read-only="true" propagation="SUPPORTS"/>
  </tx:attributes>
</tx:advice>

이상 트랜잭션의 propagation 설명을 마친다.