트랜잭션 전파에 대해 간단하게 정리해보려고 한다.
트랜잭션의 전파에 대해 알아보기 전에 우선 스프링의 트랜잭션에 대해 간단하게 정리하면 다음과 같다.
@Transaction?
@Transaction은 스프링의 AOP를 이용하여 개발자가 편하게 DB 관련 작업을 할 수 있도록 도와준다.
JPA, Mybatis 등을 사용하지 않을 때, 만약 Datasource를 이용해서 개발할 경우 getConnection(), close()를 하나하나 처리해줘야 한다.
대략 아래와 같은 코드다.
public class QueryExam {
private static final String QUERY = "select count(*) from ANIMAL";
private DataSource dataSource;
public QueryExam(DataSource dataSource) {
this.dataSource = dataSource;
}
public void query() {
Connection conn = null;
try {
conn = dataSource.getConnection();
try (Statement stmt = conn.createStatement(QUERY);
ResultSet rs = stmt.executeQuery()) {
rs.next();
return rs.getInt(1);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
}
}
}
}
보기만 해도 복잡한 코드다.
위 코드를 보면 getConnection(), close() 같은 로직은 모든 DB 접근 로직에서 공통으로 처리해야 하는 부분이다.
그래서 스프링에서는 이 부분을 AOP를 이용한 @Transaction을 이용해서 간단하게 개발 가능하도록 해주고 있다.
그리고 DB의 기본 개념인 ACID특징도 포함하고 있다.
- Atomicity(원자성) : 각 트랜잭션이 성공/실패하는 단일 단위로 처리되도록 보장한다.
- Consistency(일관성) : 모든 데이터는 규칙에 맞는 일관성을 보장한다. (제약조건, 트리거 등의 모든 규칙에 유효해야 한다)
- Isolation(고립성) : 트랜잭션은 다은 트랜잭션의 영향을 받지 않는다.
- Durability(지속성) : 트랜잭션이 성공하면 그 결과는 영구적으로 저장된다.
@Transactional 사용시 주의사항
- @Transactional을 클래스 또는 메서드 레벨에 명시하면 해당 메서드 호출시 지정된 트랜잭션이 작동하게 된다. 단, 조건이 있다. 해당 클래스의 Bean을 다른 클래스의 Bean에서 호출할 때만 @Transactional을 인지하고 작동하게 된다. (같은 빈 내에서 @Transactional이 명시된 다른 메서드를 호출해도 작동하지 않는다.) Spring Framework는 내부적으로 AOP를 통해 해당 어노테이션을 인지하여 프록시를 생성하여 트랜잭션을 자동 관리하기 때문이다. [관련 링크1] [관련 링크2]
트랜잭션 전파(propagation)
Spring에서 사용하는 어노테이션 '@Transactional'은 해당 메서드를 하나의 트랜잭션 안에서 진행할 수 있도록 만들어주는 역할을 한다.
이때 트랜잭션 내부에서 트랜잭션을 또 호출한다면 스프링에서는 어떻게 처리하고 있을까?
새로운 트랜잭션이 생성될 수도 있고, 이미 트랜잭션이 있다면 부모 트랜잭션에 합류할 수도 있을 것이다.
진행되고 있는 트랜잭션에서 다른 트랜잭션이 호출될 때 "어떻게 처리할지 정하는 것"을 '트랜잭션의 전파 설정'이라고 부른다.
전파 설정 옵션
트랜잭션의 전파 설정은 '@Transactional'의 옵션 'propagation'을 통해 설정할 수 있다. 각 옵션은 아래와 같다.
Propagation.REQUIRED (기본 값)
- 부모 트랜잭션이 존재한다면 부모 트랜잭션으로 합류한다.
- 부모 트랜잭션이 없다면 새로운 트랜잭션을 생성한다.
- 중간에 롤백이 발생한다면 모두 하나의 트랜잭션이기 때문에 진행사항이 모두 롤백된다.
@Transactional(propagation = Propagation.REQUIRED)
public void doSomething() { ... }
특정 메서드의 트랜잭션이 Propagation.REQUIRED로 설정되었을 때의 트랜잭션 동작은 다음과 같다.
기본적으로 해당 메서드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않았다면 트랜잭션를 새로 시작한다. (새로운 연결을 생성하고 실행한다.)
만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 로직을 실행한다. (동일한 연결 안에서 실행된다.)
예외가 발생하면 롤백이 되고 호출한 곳에도 롤백이 전파된다. 이러한 Propagation.REQUIRED 동작 방식을 원할 경우 기본값으로 설정되어 있기 때문에 생략해도 된다. [관련 링크]
만약, 해당 메서드가 호출한 곳와 별도의 쓰레드라면 어떤 동작이 일어날까? 답은 전파 레벨과 상관 없이 무조건 별도의 트랜잭션을 생성하여 해당 메서드를 실행한다. Spirng은 내부적으로 트랜잭션 정보를 ThreadLocal 변수에 저장하기 때문에 다른 쓰레드로 트랜잭션이 전파되지 않는다. [관련 링크1] [관련 링크2] [관련 링크3]
Propagation.REQUIRES_NEW
- 기존 트랜잭션이 존재하는 경우 트랜잭션을 잠시 보류시키고, 신규 트랜잭션을 생성하여 사용한다.
- 부모 트랙잭션에서 예외가 발생하더라도 자식 트랜잭션은 롤백되지 않는다.
- 그러나 자식 트랜잭션에서 예외가 발생한다면 부모 트랜잭션까지 롤백이 전파된다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething() { ... }
Propagation.REQUIRES_NEW로 설정되었을 때에는 매번 새로운 트랜잭션을 시작한다. (새로운 연결을 생성하고 실행한다.) 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면(기존의 연결이 존재한다면) 기존의 트랜잭션은 메서드가 종료할 때까지 잠시 대기 상태로 두고 자신의 트랜잭션을 실행한다. 새로운 트랜잭션 안에서 예외가 발생해도 호출한 곳에는 롤백이 전파되지 않는다. 즉, 2개의 트랜잭션은 완전히 독립적인 별개의 단위로 동작한다. [관련 링크1] [관련 링크2]
Propagation.MANDATORY
- 트랜잭션이 반드시 있어야 한다는 의미이다.
- 부모 트랜잭션이 존재하는 경우 해당 트랜잭션을 사용한다. 만약 부모 트랜잭션이 없다면 예외를 발생시킨다.
Propagation.NESTED
- 부모 트랜잭션이 존재한다면 중첩 트랜잭션을 생성한다.
- 중첩된 트랜잭션 내부에서 롤백 발생시 해당 중첩 트랜잭션의 시작 지점 까지만 롤백된다.
- 중첩 트랜잭션은 부모 트랜잭션이 커밋될 때 같이 커밋된다.
- 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성한다.
@Transactional(propagation = Propagation.NESTED)
public void doSomething() { ... }
Propagation.NESTED는 기본적으로 앞서 설명한 Propagation.REQUIRED와 동일하게 작동한다. 중요한 차이점은, SAVEPOINT를 지정한 시점까지 부분 롤백이 가능하다는 것이다. 유의할 점은, 데이터베이스가 SAVEPOINT 기능을 지원해야 사용이 가능하다. (대표적으로 Oracle이 해당한다.)
중요한 것은 Hibernate를 사용한다면 해당 전파 속성을 사용할 수 없으며, DataSourceTransactionManager를 직접 사용해야만 사용할 수 있다.
※ 참고
이와 관련된 자료는 다음과 같다.
https://stackoverflow.com/questions/37927208/nested-transaction-in-spring-app-with-jpa-postgres
https://stackoverflow.com/questions/33180558/does-hibernate-support-nested-transactions
다음 공식문서의 Nested 부분 -
https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html
Propagation.NEVER
- 부모 트랜잭션이 존재한다면 예외를 발생시킨다.
- 만약 부모 트랜잭션이 존재하는 경우 트랜잭션이 없는 상태로 처리를 수행한다.
Propagation.SUPPORTS
부모 트랜잭션이 존재하는 경우 해당 트랜잭션에 합류한다. 진행중인 부모 트랜잭션이 없다면 트랜잭션 없이 실행한다.
Propagation.NOT_SUPPORTED
부모 트랜잭션이 존재하는 경우 트랜잭션을 잠시 보류시킨다. 진행중인 부모 트랜잭션이 없다면 트랜잭션이 없는 상태로 처리를 수행한다.
예상 면접 질문 및 답변
Q. JPA Propagation 전파단계를 설명해주세요.
대기업면접에서 나왔던 질문으로 트랜잭션 고립단계와 같이 질문할 가능성이 있습니다.
JPA Propagation은 트랜잭션 동작 도중 다른 트랜잭션을 호출(실행)하는 상황에 선택할 수 있는 옵션입니다.
@Transactional의 propagation 속성을 통해 피호출 트랜잭션의 입장에서는 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있습니다.
REQUIRED(디폴트): 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성합니다.
이 외에도 종류가 REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORT, NEVER, NESTED 가 있지만 신입이 실제로 다뤄본 경험이 적기 때문에 REQUIRED(디폴트)값만 답변하는 것이 보통일 듯합니다.
참고