Java/Spring, Spring Boot

@Transactional 사용법과 적용 범위 (feat. 트랜잭션 간단 설명)

열심히 사는 우진 2023. 4. 14. 09:46
반응형

 

 

 

@Transactional 애너테이션은 메서드(혹은 클래스, 인터페이스 등)를 트랜잭션 단위로 만듭니다.

여기서 트랜잭션이란, 데이터베이스에서 수행하는 작업의 단위로, 아래와 같은 4가지 특성을 가집니다.

  • 원자성 : 하나의 작업 단위이다. (모두 수행되던가, 전혀 수행되지 않던가)
  • 일관성 : 작업 처리 결과에 일관성이 있다. (트랜잭션 진행되는 동안에 발생하는 DB 변경은 무시한다.)
  • 독립성 : 둘 이상의 트랜잭션이 동시에 실행될 때, 다른 트랜잭션의 연산에 끼어들지 않는다.
  • 지속성 : 트랜잭션이 성공적으로 완료되면, 결과는 영구적으로 반영되어야 한다.

본 아티클에서는 간단하게 원자성을 기준으로 @Transactional 애너테이션을 사용해보겠습니다.

그리고 @Transactional의 적용 범위(하위 메서드, 상위 메서드)에 대해, Spring 내부의 동작 원리를 통해 간단하게 알아보겠습니다.

 

 

트랜잭션이 아닌 경우

아래의 간단한 예시 코드를 먼저 이해해야 합니다.

(Java의 문법과 Spring의 라이브러리, 그리고 약간의 의사코드를 포함하여 작성되었습니다.)

// Dao를 사용하는 Service
public class Service {
    privata final Dao dao;

    public saveResult(int userId) {
        this.dao.saveNameById("name", userId);
        this.dao.saveAgeById(40, userId);
    }
}
// Service가 사용하는 Dao
public class Dao {

    public void saveNameById(String name, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
    }

    public void saveAgeById(int age, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
    }
}

ServicesaveResult()DaosaveNameById()saveAgeById()라는 2개의 메서드를 호출합니다.

그를 통해 2개의 SQL문이 실행되고, DB에는 2번의 변경이 일어납니다.

 

이때, 아래와 같이 age를 저장하던 도중 예외가 발생한다고 가정하겠습니다.

public void saveAge(int age) {
    throw new RuntimeException("저장 실패");
    this.database.executeQuery("UPDATE user SET age = ? WHERE id = 1", age) // 입력 받은 age를 DB에 저장ㅇ하는 의사코드
}

이런 상황에서 Servicesave() 메서드가 호출된다면,

DB에서 해당 username까지만 저장되고, age는 저장되지 않을 것입니다.

 

예외가 발생한다면 대개의 경우,

name만 저장되는 상황보다는 nameage가 모두 저장되지 않는 상황이 바람직할 것입니다.

@Transactional 애너테이션으로 그것을 구현해보겠습니다.

 

 

트랜잭션을 사용하는 경우

public class Service {
    privata final Dao dao;

    @Transactional
    public saveResult(int userId) {
        this.dao.saveNameById("name", userId);
        this.dao.saveAgeById(40, userId);
    }
}

하나의 단위로 묶이기 원하는 메서드나 @Transactional 애너테이션을 붙입니다.

그럼 해당 메서드가 호출되어 수행이 완료되기 까지 발생하는 DB와의 모든 상호작용이 하나의 단위가 됩니다.

위에서 작성한 코드의 예를 들면, nameage를 저장하는 작업이 하나로 묶여서,

모두 성공하면 반영(commit)되고, 하나라도 실패하거나 중간에 문제가 발생하면 회수(rollback)됩니다.

 

 

nameage를 저장하는 Dao의 메서드 각각을 트랜잭션 단위로 만들고 싶다면,

아래와 같이 사용할 수 있습니다.

public class Dao {

    @Transactional
    public void saveNameById(String name, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
    }

    @Transactional
    public void saveAgeById(int age, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
    }
}

 

클래스나 인터페이스의 모든 메서드에 @Transactional을 붙여야 한다면,

아래와 같이 클래스나 인터페이스 상단에 1번 붙이는 것으로 대체할 수 있습니다.

@Transactional
public class Dao {

    public void saveNameById(String name, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
    }

    public void saveAgeById(int age, int userId) {
        // DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
        this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
    }
}

 

스프링 공식 문서에서는, 인터페이스에 주석을 추가하기보다, 구체적인 클래스나 메서드에 주석을 추가할 것을 권장합니다.

예상대로 동작하지 않을 수 있기 때문인데요, 자세한 사항은 공식 문서에서 설명하고 있습니다.

 

 

@Transactional의 적용 범위

@Transactional의 사용 시, 예상대로 동작하지 않을 수 있는 사례 한 가지를 소개하고자 합니다.

그것은 바로 내부 호출입니다.

public class Service {
    privata final Dao dao;


    public saveResult(int userId) {
        this.saveResultByDao(userId); // Dao를 사용하는 로직을 새로운 메서드로 분리
    }

    @Transactional
    public saveResultByDao(int userId) {
        this.dao.saveNameById("name", userId);
        this.dao.saveAgeById(40, userId);
    }
}

(상기 코드는 상위의 ControllerServicesaveResult() 메서드를 호출한다고 가정하겠습니다.)

위의 코드는, saveResult() 메서드 내부에서 Dao를 사용하여 DB를 수정하는 로직을 분리한 코드입니다.

nameage를 저장하는 행위가 하나의 트랜잭션으로 묶일 것 같지만, 그렇지 않습니다.

내부에서 호출하는 함수의 @Transational 애너테이션이 무시되기 때문입니다.

 

 

@Transactional은 프록시 모드에서 작동하고, 프록시를 통해 들어오는 외부 메서드 호출만 가로채기 때문입니다. 이를 정확하게 이해하려면 프록시 패턴을 사용하는 스프링 AOP를 이해해야 합니다. 본 아티클에서는 이정도로만 다루겠습니다.

(설정을 통해 모드를 변경하면, 내부 호출도 @Transactional을 적용할 수 있다고 합니다. 자세한 내용은 공식 문서를 참고해주세요.)

 

정리하자면, @Transactional은 프록시를 통해 들어오는 외부 메서드 호출만 가로채기 때문에 내부에서 호출되는 메서드에 추가된 @Transactional은 무시됩니다.

 

 


 

 

잘못된 내용이나 궁금하신 점을 댓글로 남겨주시면 감사하겠습니다.

감사합니다!

 

 

 

 

반응형