트랜잭션(Transaction)은 데이터베이스와 같은 시스템에서 여러 연산을 논리적으로 하나의 단위로 묶어서 처리하는 개념입니다. 트랜잭션은 여러 단계의 작업이 모두 성공적으로 수행되어야만 완료되는 원자성(Atomicity), 특정 작업이 완료된 상태에서만 다음 작업이 수행되는 일관성(Consistency), 작업이 완료된 결과를 시스템에 영구적으로 반영하는 지속성(Durability), 여러 사용자 간에 독립적으로 수행되는 독립성(Isolation)의 네 가지 특성을 가지고 있습니다.
간단하게 말하면, 트랜잭션은 데이터베이스에서 여러 작업을 하나의 논리적인 작업 단위로 묶어서 처리하는 것으로, 이 단위 내에서 모든 작업이 성공하면 모든 변경이 적용되고, 하나라도 실패하면 이전 상태로 롤백되는 개념입니다.
스프링에서 @Transactional
어노테이션을 사용하면, 해당 어노테이션이 붙은 메서드 또는 클래스의 모든 메서드는 하나의 트랜잭션으로 묶이게 됩니다. 이는 다음과 같은 상황에서 유용합니다.
즉, @Transactional
어노테이션을 사용하면 스프링이 트랜잭션을 시작, 커밋 또는 롤백하는 등의 작업을 대신해주어 개발자가 트랜잭션을 명시적으로 관리하는 부담을 줄여줍니다. 트랜잭션 설정이 적용되면 메서드 수행 도중 예외가 발생하면 트랜잭션이 롤백되고, 예외가 발생하지 않으면 트랜잭션이 커밋되어 데이터베이스에 변경사항이 반영됩니다.
@Transactional
은 스프링 프레임워크에서 제공하는 어노테이션 중 하나로, 트랜잭션 처리에 관련된 설정을 지원합니다. 이 어노테이션을 사용하면 메서드 또는 클래스에 트랜잭션을 적용할 수 있습니다.
@Transactional
어노테이션을 메서드에 적용하면 해당 메서드에서 수행되는 모든 작업이 하나의 트랜잭션으로 묶입니다. 메서드가 실행되면 트랜잭션이 시작되고, 메서드가 정상적으로 완료되면 트랜잭션이 커밋되고, 예외가 발생하면 롤백됩니다.
@Transactional
public void someTransactionalMethod() {
// 트랜잭션 범위 내에서의 작업
}
@Transactional
어노테이션을 메서드 레벨에서 사용하는 경우, 해당 메서드 내의 모든 작업이 하나의 트랜잭션으로 처리됩니다. 이것은 주로 특정 메서드에서 수행되는 작업들이 원자적으로 처리되어야 하는 경우에 유용합니다. 메서드 레벨에서 @Transactional
을 사용하는 방법에 대해 자세히 알아보겠습니다.
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Transactional
public void myTransactionalMethod() {
// 트랜잭션 범위 내에서의 작업
myRepository.save(new MyEntity("data1"));
myRepository.save(new MyEntity("data2"));
// 예외가 발생하지 않으면 모든 작업이 커밋됨
}
}
위의 예제에서 myTransactionalMethod
메서드는 @Transactional
어노테이션이 적용된 메서드입니다. 이 메서드 내에서 수행되는 모든 데이터베이스 작업은 하나의 트랜잭션으로 묶이게 됩니다.
여기서 중요한 점은 메서드가 끝날 때, 즉 메서드가 정상적으로 종료되면 스프링은 트랜잭션을 커밋하고, 메서드에서 예외가 발생하면 트랜잭션을 롤백합니다. 따라서, 위의 예제에서 myRepository.save(new MyEntity("data2"));
에서 예외가 발생하면 data1
은 저장되지만 data2
는 저장되지 않습니다.
@Transactional
어노테이션을 메서드 레벨에서 사용할 때, 여러 옵션을 설정할 수 있습니다. 몇 가지 주요한 옵션은 다음과 같습니다:
readOnly
: 트랜잭션이 읽기 전용인지 여부를 나타내며, 읽기 전용일 경우에는 커밋이 아닌 롤백으로 트랜잭션을 종료하여 성능을 최적화할 수 있습니다.
@Transactional(readOnly = true)
public void readOnlyMethod() {
// 읽기 전용 트랜잭션 내에서의 작업
}
propagation
: 트랜잭션 전파 동작을 나타냅니다. 메서드가 이미 실행 중인 트랜잭션에 참여할지, 새로운 트랜잭션을 시작할지를 결정합니다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewMethod() {
// 새로운 트랜잭션 내에서의 작업
}
isolation
: 격리 수준을 나타내며, 여러 트랜잭션이 동시에 실행될 때 어떻게 격리되는지를 정의합니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedMethod() {
// READ_COMMITTED 격리 수준의 트랜잭션 내에서의 작업
}
timeout
: 트랜잭션의 제한 시간을 나타내며, 설정된 시간 내에 트랜잭션이 완료되지 않으면 롤백됩니다.
@Transactional(timeout = 30)
public void timeoutMethod() {
// 트랜잭션 내에서의 작업
}
이렇게 @Transactional
어노테이션을 메서드 레벨에서 사용하면 해당 메서드 내의 모든 작업이 트랜잭션 범위에 속하게 되어, 트랜잭션의 원자성을 보장할 수 있습니다.
@Transactional
어노테이션을 클래스에 적용하면 해당 클래스의 모든 메서드에 트랜잭션 설정이 적용됩니다. 클래스 레벨에서 설정한 트랜잭션 속성은 메서드 레벨에서의 설정보다 우선시됩니다.
@Transactional
public class SomeTransactionalService {
public void method1() {
// 트랜잭션 범위 내에서의 작업
}
public void method2() {
// 트랜잭션 범위 내에서의 작업
}
}
@Transactional
어노테이션을 클래스 레벨에서 사용하는 경우, 해당 클래스의 모든 메서드에 트랜잭션 설정이 적용됩니다. 이는 주로 클래스 내의 모든 메서드가 하나의 트랜잭션으로 묶여야 하는 경우에 유용합니다. 클래스 레벨에서 @Transactional
을 사용하는 방법에 대해 자세히 설명하겠습니다.
예를 들어, 다음과 같이 클래스 레벨에서 @Transactional
어노테이션을 사용할 수 있습니다:
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
public void saveUser(User user) {
userRepository.save(user);
}
public void updateUser(User user) {
userRepository.save(user);
// 다른 비즈니스 로직 수행
}
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}
위의 예제에서 @Transactional
어노테이션이 UserService
클래스에 적용되어 있습니다. 이렇게 하면 getUserById
, saveUser
, updateUser
, deleteUser
메서드가 모두 UserService
클래스에 정의된 트랜잭션 설정을 따르게 됩니다.
클래스 레벨에서 @Transactional
을 사용할 때 주의할 점:
메서드 레벨의 설정 우선 순위: 클래스 레벨과 메서드 레벨에 모두 @Transactional
어노테이션이 존재할 경우, 메서드 레벨의 설정이 우선 순위를 가집니다. 메서드에 별도의 설정이 없는 경우에는 클래스 레벨의 설정이 적용됩니다.
@Service
@Transactional
public class UserService {
@Transactional(readOnly = true)
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
}
위의 예제에서 getUserById
메서드는 클래스 레벨의 @Transactional
설정을 따르지 않고, 메서드 레벨에서 명시한 readOnly = true
설정을 따르게 됩니다.
트랜잭션 속성 일괄 적용: 클래스 레벨에서 @Transactional
을 사용하면 해당 클래스 내의 모든 메서드에 동일한 트랜잭션 속성이 적용됩니다. 따라서, 특정 메서드에 대해 다르게 설정하려면 메서드 레벨에서 추가적인 @Transactional
어노테이션을 사용해야 합니다.
@Service
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public class UserService {
@Transactional(timeout = 30)
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
}
위의 예제에서 getUserById
메서드는 클래스 레벨의 트랜잭션 속성과는 다르게 timeout = 30
설정을 따르게 됩니다.
클래스 레벨에서 @Transactional
어노테이션을 사용하면 클래스 내의 모든 메서드가 하나의 트랜잭션으로 묶이게 되어 트랜잭션의 일관성과 효율성을 유지할 수 있습니다.
@Transactional
어노테이션은 다양한 속성을 제공하여 트랜잭션의 동작을 조절할 수 있습니다. 이러한 속성들은 트랜잭션의 격리 수준, 전파 동작, 읽기 전용 여부, 타임아웃 등을 설정하는 데 사용됩니다. 아래에서 주요한 @Transactional
속성들을 자세히 설명하겠습니다:
readOnly
false
이며, 읽기 전용일 경우 true
로 설정하면 성능 최적화를 위해 트랜잭션이 커밋되지 않고 종료됩니다.@Transactional(readOnly = true)
public void readOnlyMethod() {
// 읽기 전용 트랜잭션 내에서의 작업
}
isolation
Isolation.DEFAULT
로서 데이터베이스 기본 격리 수준을 따릅니다.@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedMethod() {
// READ_COMMITTED 격리 수준의 트랜잭션 내에서의 작업
}
Isolation
열거형에는 여러 격리 수준이 정의되어 있습니다. 예를 들어 Isolation.READ_COMMITTED
, Isolation.REPEATABLE_READ
, Isolation.SERIALIZABLE
등이 있습니다.
propagation
@Transactional(propagation = Propagation.REQUIRED)
public void requiredMethod() {
// 현재 트랜잭션 내에서의 작업
}
Propagation
열거형에는 다양한 전파 옵션이 정의되어 있습니다. 예를 들어 Propagation.REQUIRED
, Propagation.REQUIRES_NEW
, Propagation.NESTED
등이 있습니다.
timeout
@Transactional(timeout = 30)
public void timeoutMethod() {
// 트랜잭션 내에서의 작업
}
timeout
은 초 단위로 설정하며, 기본값은 -1로서 타임아웃이 없음을 나타냅니다.
rollbackFor
와 noRollbackFor
rollbackFor
는 롤백을 수행할 예외 클래스를 나타내고, noRollbackFor
는 롤백을 수행하지 않을 예외 클래스를 나타냅니다.@Transactional(rollbackFor = { CustomException.class, AnotherException.class },
noRollbackFor = { AllowedException.class })
public void customRollbackMethod() {
// 트랜잭션 내에서의 작업
}
위의 예제에서는 CustomException
과 AnotherException
이 발생하면 롤백을 수행하고, AllowedException
이 발생하면 롤백을 수행하지 않습니다.
readOnly
와 timeout
을 동시에 사용하는 예제:
@Transactional(readOnly = true, timeout = 30)
public void readOnlyWithTimeoutMethod() {
// 읽기 전용 트랜잭션 내에서의 작업
}
위의 예제에서는 읽기 전용 트랜잭션을 사용하며, 30초 동안 트랜잭션이 완료되지 않으면 롤백됩니다.
이러한 @Transactional
어노테이션의 속성들을 사용하여 트랜잭션의 동작을 조절하면, 특정 메서드나 클래스에서 필요에 맞게 트랜잭션을 설정할 수 있습니다.
@Transactional 속성에 대한 간단한 정리는 다음과 같습니다.
속성 | 타입 | 설명 |
---|---|---|
value | String | 사용할 트랜잭션 관리자 |
propagation | enum: Propagation | 선택적 전파 설정 |
isolation | enum: Isolation | 선택적 격리 수준 |
readOnly | boolean | 읽기/쓰기 vs 읽기 전용 트랜잭션 |
timeout | int (초) | 트랜잭션 타임 아웃 |
rollbackFor | Throwable 로부터 얻을 수 있는 Class 객체 배열 | 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열 |
rollbackForClassName | Throwable 로부터 얻을 수 있는 클래스 이름 배열 | 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열 |
noRollbackFor | Throwable 로부터 얻을 수 있는 Class 객체 배열 | 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열 |
noRollbackForClassName | Throwable 로부터 얻을 수 있는 클래스 이름 배열 | 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열 |
간단한 예제를 통해 @Transactional
어노테이션을 사용하는 방법을 설명하겠습니다. 이 예제에서는 간단한 서비스와 리포지토리 클래스를 사용하여 데이터베이스 트랜잭션을 다루겠습니다.
도메인 클래스 정의:
// Article.java
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// getters and setters
}
// ArticleRepository.java
public interface ArticleRepository extends JpaRepository<Article, Long> {
}
서비스 클래스 정의:
// ArticleService.java
@Service
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
@Transactional
public void saveArticleWithTransaction(Article article1, Article article2) {
articleRepository.save(article1);
// 예외를 발생시킬 수 있는 일부 비즈니스 로직
throw new RuntimeException("Simulating an error");
// 예외가 발생하면 두 저장이 모두 롤백됩니다.
// 예외가 발생하지 않으면 두 저장이 모두 커밋됩니다.
articleRepository.save(article2);
}
}
테스트 클래스 작성:
// ArticleServiceTest.java
@SpringBootTest
@Transactional
public class ArticleServiceTest {
@Autowired
private ArticleService articleService;
@Autowired
private ArticleRepository articleRepository;
@Test
public void testSaveArticleWithTransaction() {
Article article1 = new Article();
article1.setTitle("Article 1");
Article article2 = new Article();
article2.setTitle("Article 2");
try {
articleService.saveArticleWithTransaction(article1, article2);
} catch (RuntimeException e) {
// 예상되는 예외, 아무것도 하지 않음
}
// 데이터베이스에 article1과 article2가 모두 저장되지 않았는지 확인합니다.
List<Article> articlesInDatabase = articleRepository.findAll();
assertThat(articlesInDatabase).isEmpty();
}
}
위의 예제에서 ArticleService
클래스의 saveArticleWithTransaction
메서드에 @Transactional
어노테이션이 적용되어 있습니다. 이 메서드는 두 개의 Article 객체를 받아서 데이터베이스에 저장하고, 그 중 하나의 저장 과정에서 의도적으로 예외를 발생시켜 롤백을 시도하고 있습니다.
테스트 클래스에서는 @Transactional
어노테이션을 통해 해당 테스트 메서드가 트랜잭션 내에서 실행되도록 설정하고 있습니다. 그리고 articleService.saveArticleWithTransaction
메서드를 호출할 때 예외가 발생하므로, 트랜잭션 내의 모든 작업이 롤백되어 데이터베이스에는 아무런 변경이 없는지를 검증하고 있습니다.
이런식으로 @Transactional
어노테이션을 사용하면 트랜잭션의 범위에서 메서드의 실행이 관리되며, 예외가 발생하면 롤백이 이루어지고 예외가 발생하지 않으면 커밋이 이루어지게 됩니다.
선언적 트랜잭션(Declarative Transaction Management)은 트랜잭션의 관리를 코드 대신 설정으로 처리하는 방법을 의미합니다. 이는 주로 스프링과 같은 프레임워크에서 지원되는 기능 중 하나입니다.
선언적 트랜잭션은 개발자가 코드 내에서 명시적으로 트랜잭션을 시작, 커밋 또는 롤백을 작성하지 않고도 트랜잭션을 관리할 수 있게 해줍니다. 대신에 설정 파일이나 어노테이션을 통해 트랜잭션의 속성을 지정하고, 프레임워크가 이를 인식하여 자동으로 트랜잭션을 처리합니다.
주로 선언적 트랜잭션은 다음과 같은 방법으로 사용됩니다:
XML 기반 설정:
tx:advice
와 aop:config
를 사용하여 XML 기반으로 선언적 트랜잭션을 설정할 수 있습니다.<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 데이터베이스 설정 등 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/>
</aop:config>
</beans>
어노테이션 기반 설정:
@Transactional
어노테이션을 통해 선언적 트랜잭션을 설정할 수 있습니다.@Service
@Transactional
public class MyService {
// 메서드들
}
특별한 경우가 아니라면 어노테이션 기반 설정을 사용합니다.
따라서, 아래와 같은 특징이 있다
@Transactional
어노테이션이 적용된 메서드가 호출되면, 스프링은 새로운 트랜잭션을 시작합니다.@Transactional
어노테이션에 지정된 속성들에 따라 트랜잭션의 동작이 결정됩니다. 속성에는 readOnly
, isolation
, propagation
, timeout
등이 있으며, 이들은 트랜잭션의 특성을 정의합니다.트랜잭션은 Spring AOP를 통해 구현되어있습니다. 더 정확하게 말하면, 어노테이션 기반 AOP를 통해 구현되어 있습니다.
(import문을 보면 알 수 있습니다.)
import org.springframework.transaction.annotation.Transactional;
따라서, 아래와 같은 특징이 있습니다.
@Transactional
이 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성@Transactional
이 포함된 메서드가 호출될 경우, 트랜잭션을 시작하고 Commit or Rollback을 수행CheckedException
or 예외가 없을 때는 CommitUncheckedException
이 발생하면 Rollback스프링은 AOP(Aspect-Oriented Programming)을 활용하여 @Transactional
어노테이션을 프록시로 감싸는 방식으로 동작합니다. 이를 통해 @Transactional
어노테이션이 적용된 메서드 호출 시 트랜잭션 관리를 추가할 수 있습니다. 스프링이 제공하는 TransactionInterceptor
등의 AOP 기능을 통해 이러한 동작이 구현됩니다.
중요한 점은 @Transactional
어노테이션이 적용된 클래스나 메서드는 스프링의 트랜잭션 관리자에 의해 관리되며, 트랜잭션 속성을 명시하면 해당 속성에 따라 트랜잭션이 동작한다는 것입니다.
@Transactional
은 우선순위를 가지고 있습니다.
클래스 메서드에 선언된 트랜잭션의 우선순위가 가장 높고, 인터페이스에 선언된 트랜잭션의 우선순위가 가장 낮습니다.
클래스 메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스
따라서 공통적인 트랜잭션 규칙은 클래스에, 특별한 규칙은 메서드에 선언하는 식으로 구성할 수 있습니다.
또한, 인터페이스 보다는 클래스에 적용하는 것을 권고합니다.
트랜잭션의 모드
@Transactional
은 Proxy Mode와 AspectJ Mode가 있는데 Proxy Mode가 Default로 설정되어 있습니다.
Proxy Mode는 다음과 같은 경우 동작하지 않습니다.
public
메서드에 적용되어야한다.Protected, Private Method
에서는 선언되어도 에러가 발생하지는 않지만, 동작하지도 않는다.AspectJ Mode
를 고려해야한다.@Transactional
이 적용되지 않은 Public Method
에서 @Transactional
이 적용된 Public Method
를 호출할 경우, 트랜잭션이 동작하지 않습니다.