본문 바로가기
Spring&Spring Boot

(Spring Boot) @Transactional 이란? / @Transactional에 대하여

by Developer RyanKim 2018. 10. 10.


@Transactional

관련글 
@Transactional 전파 유형: https://lion-king.tistory.com/47
@Transactional 격리 레벨: https://lion-king.tistory.com/48

StackOverflow 질문중 UserNameNotFoundException 발생시 delete method가 실행되지않는다는 질문에 답을 하였습니다.

https://stackoverflow.com/questions/52619924/how-to-execute-transaction-in-hibernate-while-throwing-exception/52620514#52620514


I have the following method in my transactional service layer implemented with Hibernate:

@Override
public void activateAccount(String username, String activationCode)
        throws UsernameNotFoundException, AccountAlreadyActiveException,
        IncorrectActivationCodeException {
    UserAccountEntity userAccount = userAccountRepository.findByUsername(username);
    if (userAccount == null) {
        throw new UsernameNotFoundException(String.format("User %s was not found", username));
    } else if (userAccount.isExpired()) {
        userAccountRepository.delete(userAccount); // 이부분!
        throw new UsernameNotFoundException(String.format("User %s was not found", username)); 
    } else if (userAccount.isActive()) {
        throw new AccountAlreadyActiveException(String.format(
                "User %s is already active", username));
    }
    if (!userAccount.getActivationCode().equals(activationCode)) {
        throw new IncorrectActivationCodeException();
    }
    userAccount.activate();
    userAccountRepository.save(userAccount);
}

As you can see, in else if (userAccount.isExpired()) block, I want to first delete userAccount and then throw an exception. But as it's throwing an exception, and exiting the method abruptly, the delete is not executed.

I wonder if there is any way to persist the delete action while throwing an exception.


로직을 보면 delete를 수행하고 Exception을 throw하는 것이 분명했습니다.



문제를 찾은 결과 UserNameNotFoundException이 발생하기 전에 

RunTimeException이 발생하였고, 발생 시 다시 rollback 된 것이었습니다.


하지만 왜 RunTimeException 발생시에 rollback이 되는 것일까요? 특별한 다른 설정이 되어있나 찾아도 찾을 수도 없었습니다. 




답은 @Transactional 에서 찾을 수 있었습니다.



https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html
https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
https://dzone.com/articles/how-does-spring-transactional

At a high level, Spring creates proxies for all the classes annotated with @Transactional – either on the class or on any of the methods.


High Level에서, Spring이 모든 클래스를 만들때 규칙에 @Transactionl이 추가되어있다고 합니다.


By default transaction is rolled back for unchecked exceptions (subclasses of RuntimeException and Error), and not rolled back for checked exceptions.

//default
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}


그리고 Transaction의 기본 설정은 확인 되지않은 Exception ( ex: RuntimeException) 발생시 RollBack 하도록 설정되어 있답니다!


Only CRUD methods (CrudRepository methods) are by default marked as transactional. If you are using custom query methods you should explicitly mark it with @Transactional annotation.


또한 CrudRepository가 기본으로 제공하는 Method에는 @Transactional이 적용되어 있다는 것!

따라서 위와같은 상황이 발생했던 것입니다.




@Transactional 사용없이 Transactional 한 동작 설정

원활한 이해를 돕기위해 @Transactional 어노테이션이 없을 때 어떻게 설정을 했었는지 잠깐 살펴보겠습니다.



JPA를 사용하지 않을때 db connection 설정은 아래와 같이하였습니다. 

그 과정에서 Transactional 하게 동작하도록 설정하기 위해서


con.setAutoCommit(false);

 커밋이 자동으로 되지 않도록 하고, 


con.commit();

Transaction이 발생하기 원하는 시점에 commit메소드를 호출하여 발생시키고,


catch (SQLException e ) {
if (con != null) {
try {
System.err.print("Transaction is being rolled back");
con.rollback();
} catch(SQLException e) {

}
}

RollBack하기 원하는 시점에 rollback함수를 호출하여 Transaction을 rollback 함으로써 작업수행을 Transactional하게 설정하였습니다.


public class Example {
Connection con;

public void makeCon() throws SQLException {
String URL = "jdbc:oracle:thin:@amrood:1521:EMP";
Properties info = new Properties( );
info.put( "user", "username" );
info.put( "password", "password" );

con = DriverManager.getConnection(URL, info);

con.setAutoCommit(false);

String dbName = "ovnd";

PreparedStatement updateSales = null;
PreparedStatement updateTotal = null;

String updateString =
"update " + dbName + ".COFFEES " +
"set SALES = ? where COF_NAME = ?";

String updateStatement =
"update " + dbName + ".COFFEES " +
"set TOTAL = TOTAL + ? " +
"where COF_NAME = ?";

HashMap<String, Integer> salesForWeek = null;

try {
con.setAutoCommit(false);
updateSales = con.prepareStatement(updateString);

for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());
updateSales.executeUpdate();
updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate();
con.commit();
}
} catch (SQLException e ) {
if (con != null) {
try {
System.err.print("Transaction is being rolled back");
con.rollback();
} catch(SQLException e) {

}
}
} finally {
if (updateSales != null) {
updateSales.close();
}
if (updateTotal != null) {
updateTotal.close();
}
con.setAutoCommit(true);
}
}


하지만 JPA의 사용과 @Transactional 어노테이션으로 아래와 같이 쉽게 설정 할 수 있습니다.


@Transactional

여러 속성을 통해 전파유형, 특정 Exception 발생시 rollback 이나 반대 경우, Timeout, readOnlyFlag 등을 설정할 수 있습니다.

* 추가 : @Transactionl이 추가된 메소드가 클래스가 동작할 때에는 트랜잭션  기능이 적용된 프록시 객체가 생성되어 동작합니다.

기본적으로 method 시작시 하나의 Transaction을 생성하고, return 후 commit되도록 설정 되어 있습니다.


@Transactional(rollbackOn = RollBackException.class, dontRollbackOn = RuntimeException.class)
public void save(ExObj exObj) throws RollBackException {

ExObj res = rollBackRepository.save(exObj);
if(res == null) throw new RollBackException();
}


rollbackOn

: 특정 Exception 발생 시 rollback 하도록 설정

dontRollbackOn

: 특정 Exception 발생 시 rollback 하지 않도록 설정


사용 예시

회원가입 로직 구현 시, 회원정보와 회원상세정보를 저장하는데 회원정보와 상세정보 둘 모두 필수로 저장되어야 한다고 가정한다면

아래와 같이 사용할 수 있을 것입니다.


@Transactional(rollbackOn = RollBackException.class)
public void save(ExObj user, ExObj2 userDetails) throws RollBackException {

ExObj resUser = rollBackRepository.save(user);
if(resUser == null) throw new RollBackException();

ExObj2 resUserDetails = rollBackRepository.save(userDetails);
if(resUserDetails == null) throw new RollBackException();
}


회원 정보, 회원 상세정보를 DB에 저장하는 명령을 수행하고 둘 중 하나라도 실패하면 RollBackException을 발생 시키도록 작성하여

두 정보가 항상 같이 저장 되도록 할 수 있습니다.



주의할점!

https://stackoverflow.com/questions/39827054/spring-jpa-repository-transactionality


You should also be aware about consequences of marking repository interface methods instead of service methods. If you are using default transaction propagation configuration (Propagation.REQUIRED) then:

The transaction configuration at the repositories will be neglected then as the outer transaction configuration determines the actual one used.


트랜잭션을 어느 Layer에서 사용할지 신중히 고민하여 선택해야 합니다.

트랜잭션이 발생하기 직전인 DB와 직접 연관이 있는 Layer 또는 트랜잭션 전에 조건을 준 로직이 담긴 Service Layer에서 사용하는 것이 좋겠습니다.


* 추가 : @Transactional이 적용되는 우선 순위는
클래스 메소드 > 클래스 > 인터페이스 메소드 > 인터페이스 순입니다.



이상으로 포스팅을 마칩니다.

다음에 더 좋은 내용으로 만나요~

99746B455BE9272912 (450×90)

By RyanKim (Overnodes Devloper)



댓글