스프링 프레임워크는 강력한 트랜잭션 관리 기능을 제공한다.
트랜잭션 추상화: JDBC와 JPA의 차이
스프링의 트랜잭션 추상화는 서로 다른 데이터 접근 기술(JDBC, JPA 등)에서 동일한 방식으로 트랜잭션을 관리할 수 있게 해준다.
예를 들어, JDBC에서는 트랜잭션을 직접 제어해야 하지만, JPA에서는 EntityManager를 통해 트랜잭션을 제어한다.
이러한 차이는 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통해 해결된다.
JDBC 트랜잭션 예시
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); // 성공시 커밋
} catch (Exception e) {
con.rollback(); // 실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
JPA 트랜잭션 예시
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // 트랜잭션 시작
logic(em); // 비즈니스 로직
tx.commit(); // 커밋
} catch (Exception e) {
tx.rollback(); // 롤백
} finally {
em.close(); // 엔티티 매니저 종료
}
emf.close(); // 엔티티 매니저 팩토리 종료
}
스프링은 이러한 다양한 트랜잭션 관리 방식을 통일하여, 개발자는 @Transactional 애노테이션 하나로 간편하게 트랜잭션을 관리할 수 있다.
선언적 트랜잭션 관리와 AOP
스프링에서 트랜잭션을 관리하는 주요 방법은 선언적 트랜잭션 관리이다. 이는 @Transactional 애노테이션을 통해 구현된다. 선언적 트랜잭션 관리는 AOP를 기반으로 하며, 트랜잭션 처리 로직을 서비스 로직과 분리시켜준다. 프록시 객체가 트랜잭션을 시작하고, 서비스 로직을 호출한 후 트랜잭션을 종료하는 방식이다.
@Transactional
public void logic() {
// 비즈니스 로직
}
위와 같이 @Transactional을 적용하면, 스프링은 프록시 객체를 생성하여 트랜잭션을 관리한다.
프록시 객체는 비즈니스 로직 전에 트랜잭션을 시작하고, 이후 커밋 또는 롤백을 처리한다.
트랜잭션 적용 위치와 우선순위
스프링 트랜잭션에서 중요한 개념 중 하나는 트랜잭션 적용 위치에 따른 우선순위다. 더 구체적이고 자세한 설정이 높은 우선순위를 가진다. 예를 들어, 클래스 레벨보다 메소드 레벨의 설정이 우선하며, 메소드 레벨에서 세밀한 제어가 가능하다.
@Transactional(readOnly = true)
public class Service {
@Transactional(readOnly = false)
public void write() {
// 쓰기 로직
}
public void read() {
// 읽기 로직
}
}
위 코드에서 클래스 레벨의 @Transactional 설정은 기본적으로 읽기 전용 트랜잭션을 사용하지만, write() 메서드에는 쓰기 트랜잭션이 적용된다.
예외 처리와 트랜잭션 롤백/커밋
스프링 트랜잭션에서는 예외의 종류에 따라 롤백과 커밋을 자동으로 처리한다. 기본적으로 RuntimeException과 Error는 롤백을 트리거하고, Exception은 커밋을 트리거한다. 그러나 rollbackFor 속성을 사용하면 체크 예외도 롤백할 수 있다.
@Transactional(rollbackFor = Exception.class)
public void checkedException() throws Exception {
// 체크 예외 발생 시 롤백
}
이렇게 예외에 대해서 설정할 수 있는데, 구글에는 너무 잘못된 글들이 돌아다닌다.
그것을 갓백기선님이 집으신 영상이 있는데, 보고 오면 좋을 것 같다.
https://www.youtube.com/watch?v=_WkMhytqoCc
내부 호출과 프록시 문제
스프링 AOP 기반의 트랜잭션 관리에서 자주 겪는 문제 중 하나는 내부 메소드 호출이다. 내부 호출은 프록시를 거치지 않기 때문에, 트랜잭션이 적용되지 않는다. 이를 해결하려면 트랜잭션이 필요한 메소드를 별도의 빈으로 분리하거나 외부 호출을 사용해야 한다.
public class Service {
public void external() {
internal(); // 프록시를 거치지 않음, 트랜잭션 미적용
}
@Transactional
public void internal() {
// 트랜잭션이 필요함
}
}
위 코드에서 external() 메서드에서 internal() 메서드를 호출할 때 트랜잭션이 적용되지 않는 문제가 발생한다.
이를 피하기 위해 internal() 메서드를 별도의 클래스나 빈으로 분리해야 한다.
트랜잭션 옵션과 추가 기능들
스프링 트랜잭션은 다양한 옵션을 제공한다.
propagation, isolation, timeout, readOnly 등의 옵션을 사용하여 트랜잭션의 동작 방식을 세밀하게 제어할 수 있다.
- Propagation: 트랜잭션 전파 옵션으로, 트랜잭션이 호출되는 방식과 새로운 트랜잭션이 생성되는 방식을 결정한다.
- Isolation: 트랜잭션 격리 수준을 설정하며, 동시에 실행되는 트랜잭션 간의 데이터 무결성을 어떻게 유지할 것인지 결정한다.
- Timeout: 트랜잭션의 최대 실행 시간을 설정하여, 시간 초과 시 트랜잭션을 롤백시킨다.
- ReadOnly: 읽기 전용 트랜잭션을 설정하여, 성능 최적화를 꾀할 수 있다.
1. Propagation: 트랜잭션 전파 옵션
Propagation 옵션은 트랜잭션이 호출되는 방식과 새로운 트랜잭션이 생성되는 방식을 결정한다.
스프링은 여러 가지 전파 레벨을 제공한다. 각각의 주요 전파 레벨과 예시는 다음과 같다.
- REQUIRED (기본값): 현재 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작한다.
@Transactional(propagation = Propagation.REQUIRED)
public void requiredMethod() {
// 비즈니스 로직
}
예: 주문을 처리하는 과정에서 결제 트랜잭션이 이미 진행 중이면 그 트랜잭션을 사용하고, 그렇지 않으면 새 트랜잭션을 시작한다.
- REQUIRES_NEW: 현재 트랜잭션이 존재하더라도 새로운 트랜잭션을 시작한다. 기존 트랜잭션은 새 트랜잭션이 끝날 때까지 일시 중단된다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewMethod() {
// 비즈니스 로직
}
로깅이나 감사 데이터를 별도의 트랜잭션에서 처리할 때 사용한다. 원래 트랜잭션이 실패해도 requresNewMethod 에서 실행한 트랜잭션은 별도 처리한다. 이는 다음 글에서 더 자세히 다뤄보겠다.
이 외의 다른 옵션들은.. 잘 사용하지 않는다고 한다.
2. Isolation: 트랜잭션 격리 수준
Isolation 옵션은 트랜잭션 간에 데이터 무결성을 보장하기 위한 설정이다.
격리 수준이 높을수록 다른 트랜잭션과의 간섭을 막지만 성능 저하가 발생할 수 있다.
실제로는 격리수준을 올린다기 보다 분산락, 락같은 걸 더 많이 사용한다고 한다.
- READ_UNCOMMITTED: 커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedMethod() {
// 커밋되지 않은 데이터 읽기 가능
}
예: 매우 빠른 읽기 작업이 필요하지만 데이터의 무결성보다는 성능이 중요한 상황.
- READ_COMMITTED: 커밋된 데이터만 읽을 수 있습니다. 일반적인 설정이다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedMethod() {
// 커밋된 데이터만 읽기 가능
}
예: 커밋된 데이터만 읽어서 데이터의 일관성을 보장하지만, 일부 데이터는 다른 트랜잭션에서 변경될 수 있다.
- REPEATABLE_READ: 같은 트랜잭션 내에서는 읽은 데이터가 변하지 않도록 보장한다.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadMethod() {
// 동일 트랜잭션에서 동일한 데이터를 읽으면 항상 같은 값이 보장됨
}
하나의 트랜잭션 동안 여러 번 데이터를 읽을 때, 데이터가 변하지 않도록 하고 싶은 경우 사용된다.
- SERIALIZABLE: 가장 높은 수준의 격리, 트랜잭션이 순차적으로 실행되며, 동시 접근을 제한한다.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableMethod() {
// 동시성이 매우 제한됨
}
매우 높은 데이터 무결성을 요구하는 금융 시스템에서 사용되며, 동시성 처리 성능은 낮아질 수 있다.
3. Timeout: 트랜잭션 최대 실행 시간
Timeout 옵션은 트랜잭션이 실행될 수 있는 최대 시간을 초 단위로 설정한다.
설정된 시간 내에 트랜잭션이 완료되지 않으면 자동으로 롤백된다.
@Transactional(timeout = 5)
public void timeoutMethod() {
// 5초 안에 완료되지 않으면 트랜잭션 롤백
}
4. ReadOnly: 읽기 전용 트랜잭션
ReadOnly 옵션은 트랜잭션이 데이터 변경 없이 읽기 전용 작업을 수행할 때 사용된다.
이 옵션은 성능 최적화에 유용하며, JPA나 JDBC에서는 성능 최적화를 할 수 있다.
필자는 프로젝트할 때 항상 Service나 UseCase 클래스단에 기본으로 Transactional(readOnly = true)를 붙였는데 이부분이 잘못됏다는 것을 트랜잭션 전파를 학습하고 나서 알았다.
@Transactional(readOnly = true)
public void readOnlyMethod() {
// 데이터 조회만 수행, 변경 작업은 불가능
}
이러한 전파 옵션이 존재하며, 실제 @Transactional 을 붙였을 대, 어떤 작업이 일어나는지 자세히 알아보자.
트랜잭션 AOP 적용 전체 흐름
1. 클라이언트의 요청
사용자가 특정 비즈니스 로직을 호출하면, 이 호출은 스프링의 AOP 프록시를 거쳐 트랜잭션이 적용된다.
2. AOP 프록시 적용
Spring의 @Transactional과 DBMS의 트랜잭션은 엄연히 다르다. Spring의 트랜잭션은 비즈니스 로직 단위에서 트랜잭션을 관리하는 개념이고, DBMS는 단순히 트랜잭션을 실행하고 커밋 또는 롤백하는 역할을 한다.
Spring은 사용하는 데이터베이스가 MySQL인지, PostgreSQL인지 등을 어떻게 알 수 있을까? 정답은 TransactionManager에 있다.
Spring은 이를 DI(의존성 주입)를 활용하여 추상화한 PlatformTransactionManager를 통해 관리한다. 이를 통해 특정 데이터베이스에 종속되지 않고, 다양한 DBMS에서도 동일한 트랜잭션 관리 기능을 제공할 수 있다.
해당 메서드가 @Transactional 어노테이션이 적용된 경우, AOP 프록시가 동작하여 트랜잭션을 시작한다.
트랜잭션 관련 기능을 수행하기 위해 트랜잭션 매니저를 호출한다.
3. 트랜잭션 매니저 호출
AOP 프록시는 트랜잭션 매니저를 호출하여 트랜잭션을 시작한다.
트랜잭션 매니저는 transactionManager.getTransaction()을 호출하여 트랜잭션을 가져온다.
transactionManager는 인터페이스이며, 실제 사용되는 구현체는 DataSourceTransactionManager, JpaTransactionManager, HibernateTransactionManager 등 DBMS 환경에 따라 다르게 설정된다.
Spring이 특정 DBMS에 맞는 트랜잭션 매니저를 결정하는 과정에서 PlatformTransactionManager가 핵심 역할을 하며, 이를 통해 다양한 데이터베이스에서 동일한 방식으로 트랜잭션을 관리할 수 있다.
AOP 프록시는 트랜잭션 매니저(Transaction Manager)를 호출하여 트랜잭션을 시작한다.
트랜잭션 매니저는 transactionManager.getTransaction()을 호출하여 트랜잭션을 가져온다.
4. 트랜잭션 동기화 매니저 활용
트랜잭션 동기화 매니저(Transaction Synchronization Manager)가 현재 진행 중인 트랜잭션과 관련된 커넥션을 관리한다.
데이터베이스 커넥션을 가져와서 Persistence Context와 연결한다.
5. 데이터베이스 커넥션 확보
트랜잭션 매니저는 데이터베이스 커넥션을 가져오고, autoCommit을 false로 설정하여 수동 트랜잭션을 관리할 수 있도록 설정한다.
이 과정에서 기존의 커넥션이 없다면 새로운 커넥션을 생성하고, 필요하면 기존 커넥션을 재사용한다.
6. 커넥션 보관
트랜잭션 매니저는 확보한 커넥션을 트랜잭션 동기화 매니저(Transaction Synchronization Manager)를 통해 저장해 둔다.
이로 인해 이후의 데이터베이스 접근 로직에서 동일한 커넥션을 재사용할 수 있다.
7. 트랜잭션 동기화 매니저에서 커넥션 관리
트랜잭션 동기화 매니저는 확보한 커넥션을 트랜잭션과 동기화한다.
나중에 트랜잭션이 정상적으로 종료되거나 롤백될 때까지 커넥션을 유지하도록 설정한다.
8. 실제 서비스 로직 실행
이제 서비스 계층에서 실제 비즈니스 로직이 실행된다.
비즈니스 로직이 실행되면서 리포지토리(Repository) 계층을 통해 데이터베이스에 접근하게 된다.
9. 트랜잭션 동기화 커넥션 획득
리포지토리에서 데이터베이스에 접근할 때, 트랜잭션 동기화 매니저를 통해 동일한 커넥션을 획득하여 사용한다.
트랜잭션이 끝날 때까지 동일한 커넥션을 유지하며, 여러 개의 데이터베이스 작업이 하나의 트랜잭션 내에서 수행된다.
10. 트랜잭션 종료
비즈니스 로직이 정상적으로 실행되면, AOP 프록시가 트랜잭션을 종료한다.
트랜잭션 매니저는 commit() 또는 rollback()을 수행한다.
- 예외가 발생하지 않으면 commit()
- 예외가 발생하면 rollback()
11. 트랜잭션 완료 후 정리
트랜잭션이 끝나면, 트랜잭션 동기화 매니저는 보관 중이던 커넥션을 정리하고 반환한다.
이때, 커넥션 풀을 사용 중이라면 커넥션을 닫지 않고 풀(Pool)로 반환하여 재사용할 수 있도록 한다.
전체 요약
- 클라이언트의 요청이 들어오면 AOP 프록시가 트랜잭션을 시작한다.
- 트랜잭션 매니저가 트랜잭션을 관리하며, 필요한 경우 새 데이터베이스 커넥션을 생성한다.
- 트랜잭션 동기화 매니저를 통해 커넥션을 유지하고, 트랜잭션 범위 내에서 동일한 커넥션이 사용된다.
- 비즈니스 로직이 실행되면서 데이터베이스에 접근하게 되며, 트랜잭션 범위 내에서 쿼리가 실행된다.
- 서비스가 정상적으로 완료되면 트랜잭션이 커밋되고, 예외가 발생하면 롤백된다.
- 트랜잭션 종료 후 커넥션이 반환되면서 정리가 이루어진다.
다음 글에서는 스프링 트랜잭션의 핵심, 트랜잭션 전파에 대해 알아보겠다.
'Spring > Spring Transaction' 카테고리의 다른 글
Spring Transaction 2. 스프링 트랜잭션 전파와 활용 (0) | 2024.08.15 |
---|