1일 1개념정리 24.08.09.금 ~
큰 결정에 큰 동기가 따르지 않을 때도 있다. 하지만 큰 결심이 따라야 이뤄낼 수 있다.
무조건 무조건 1일 1개의 개념 정리하기 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#12. @Transactional
Spring에 대해 글을 자주 쓰고있는데, 참 많은 기능들이 함축적으로 들어가있다는 생각이 든다. 특히나 JDBC에서 발전해온 과정을 공부하며 데이터베이스를 관리하기가 편해졌구나 싶다. 이런 과정에서 연관되는 것이 바로 @Transactional이다.
Transaction은 하나의 거래라고 할 수 있고, DB 작업할 때 데이터를 입력하고, 업데이트하고, 저장하는 등 일련의 과정을 의미한다. 즉, 데이터 일관성을 유지하기 위한 하나의 작업단위이다. 이게 "작업단위"라서 나중에 오류가 발생했을 때 작업 이전의 상황으로 롤백할 수 있다.
Spring의 @Transactional 어노테이션에선 "선언적 DB 트랜잭션 관리"기능을 제공한다. 선언적으로 관리한다는 건 이 부분이 트랜잭션이라고 선언한다는 것이다. 즉, "트랜잭션을 코드로 직접 제어하지 않고, 어노테이션을 통해 그 경계를 지정하는 방식"을 말한다. 더 자세히는 자바 코드로 트랜잭션의 시작, 종료, 커밋, 롤백 등을 직접적으로 처리하지 않고, 메소드나 클래스에 @Transactional 어노테이션을 사용하여 트랜잭션의 범위를 선언하는 방식이다.
@Transactional
@Transactional이 적용된 메소드가 호출되면, 스프링은 자동으로 트랜잭션을 시작한다. 메소드가 성공적으로 완료되면 트랜잭션을 커밋하고, 예외가 발생하여 메소드가 비정상적으로 종료되면 트랜잭션을 롤백한다. 이 과정에서 트랜잭션의 일관성을 유지할 수 있어서 일부 작업만 DB에 반영되는 상황을 방지할 수 있다. (DB의 원자성 보장)
트랜잭션을 직접 관리하면 어떻게 될까 ??
public void processOrder(Order order) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// Order 저장 로직
saveOrder(conn, order);
// 재고 감소 로직
updateInventory(conn, order);
conn.commit();
} catch (SQLException ex) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
throw new RuntimeException("Order processing failed", ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
이처럼 트랜잭션 관리 로직이 비즈니스 로직과 합쳐져있어서 보기가 불편하고, 예외처리도 해줘야하고 connection 관리도 직접 해줘야한다. 그러나 @Transactional을 쓰면 아래와 같이 변한다.
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Order 저장 로직
saveOrder(order);
// 재고 감소 로직
updateInventory(order);
}
}
@Transactional을 사용하면 코드가 훨씬 간결해지고, 트랜잭션 관리를 메소드 수준에서 할 수 있다. 선언적으로 트랜잭션을 관리하면 이처럼 복잡했던 트랜잭션을 단순화할 수 있고, DB 일관성 유지에도 도움을 준다.
@Transactional 특징
1. 선언적 트랜잭션 관리
- 트랜잭션의 경계를 메소드 또는 클래스 레벨에서 선언 가능
- 메소드 호출시 트랜잭션 시작하고 메소드 종료되면 트랜잭션 커밋, 예외 발생시 롤백 등을 자동으로 관리함.
2. 트랜잭션 전파 (Propagation)
- 일단 전파가 뭔소리일까 ?? 트랜잭션이 메소드 호출 간에 어떻게 전파되는지를 결정하는 개념이다. 기존 트랜잭션이 존재할 때 새로운 트랜잭션을 시작할지, 기존 트랜잭션에 참여할지 등을 설정해야한다.
코드로 살펴보기에 앞서, 트랜잭션 전파 옵션부터 살펴보자. 전파 옵션은 아래 코드처럼 직접 설정할 수 있다.
- REQUIRED : 전파의 기본값이다. 기존 트랜잭션이 있으면 참여하고, 없으면 새로 시작
- REQUIRES_NEW : 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션은 일시 중지
- MANDATORY : 반드시 기존 트랜잭션이 있어야 하며, 없으면 예외 발생시킴
- SUPPORTS : 기존 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행
- NOT_SUPPORTED : 트랜잭션을 사용하지 않고 실행
- NEVER : 트랜잭션이 있으면 예외를 발생시킴
- NESTED : 현재 트랜잭션 내에 중첩 트랜잭션을 시작
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Transactional(propagation = Propagation.REQUIRED)
public void processOrder(Order order) {
orderRepository.save(order); // 현재 트랜잭션이 적용됨
// 중간에 다른 트랜잭션이 호출됨
inventoryService.updateInventory(order); // 새로운 트랜잭션이 시작됨 (REQUIRES_NEW)
}
}
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateInventory(Order order) {
inventoryRepository.updateInventory(order); // 새로운 트랜잭션이 적용됨
}
}
이런 코드가 있을 때 흐름이 다음과 같다.
- OrderService에 있는 ProcessOrder가 호출되면 트랜잭션이 시작된다. (REQUIRED 전파)
- 그리고 updateInventory 메소드가 호출되는데 이 메소드는 새로운 트랜잭션을 시작한다. (REQUIRES_NEW 전파라서 기존 트랜잭션 중지하고 항상 새로운 트랜잭션을 실행함.)
- updateInventory의 트랜잭션이 실패하거나 커밋되더라도, processOrder의 트랜잭션에는 영향을 미치지 않는다. 즉, 두 트랜잭션은 독립적으로 처리된다.
- 만약 processOrder 트랜잭션이 실패하면, processOrder 내의 모든 작업이 롤백된다. 하지만 updateInventory의 작업은 별도로 처리되어 커밋되거나 롤백된다.
3. 트랜잭션 격리 (Isolation)
- READ_UNCOMMITTED : 다른 트랜잭션이 커밋하지 않은 데이터까지 읽을 수 있다.
- READ_COMMITTED : 커밋된 데이터만 읽을 수 있다.
- REPEATABLE_READ : 트랜잭션 내에서 동일한 데이터를 반복적으로 읽을 때 데이터 변경이 없음을 보장함
- SERIALIZABLE : 가장 높은 격리 수준으로, 트랜잭션 간의 상호 작용을 완전히 차단함
4. 자동 롤백 (주의사항)
- 일단 이걸 구분하자. Runtime Exception은 unchecked Exception라고 하고 IOException, SQLException은 checked Exception라고 한다.
- 기본적으로 RuntimeException( unchecked )이 발생하면 트랜잭션을 자동으로 롤백한다.
- 근데 checkedException은 자동롤백 해주지 않는다. 물론 옵션을 달아주면 롤백 기능 추가할 수 있다.
5. 트랜잭션은 public에만 사용
- 외부에서 접근 가능한 인터페이스를 제공해야 하므로 트랜잭션을 메소드에 적용하려면 반드시 public으로 해야한다. 만약에 해당 메소드가 private이나 protected면 프록시 객체가 생성될 때 여기에 접근이 불가능하므로 트랜잭션 관리도 불가능하다.
@Transactional 주의사항
위 내용만 봐도 알 수 있듯이, @Transactional은 매우 유용하다. 근데 모르고 쓰면 큰코다친다. 한줄요약하면, 위에서 언급한 특징들을 잘 알고 써야한다는 것이다. 경계, 전파, 격리 등.
1. public만 적용
아까 말했듯 트랜잭션은 public에서만 가능하고, 다른 곳에서는 반영되지 않는다.
2. "같은 클래스"에선 호출하는 메소드도 @Transactional이어야함. ★ ★ ★ ★ ★
트랜잭션이 선언된 메소드를 호출할 거면 호출하는 메소드쪽에도 @Transactional을 달아줘야 트랜잭션이 적용된다.
@Service
public class MyService {
@Transactional
public void A() {
}
public void B() {
A();
}
}
여기서 A는 트랜잭션 달려있고, B는 없다. 근데 B에서 A를 호출하면 트랜잭션이 적용될 때도 있고, 아닐 때도 있다.
상황1) 같은 클래스 내에서 호출되는 경우
위 코드처럼 같은 클래스에서 호출하게 되면 A에 달린 @Transactional이 적용되지 않는다. 그 이유는 스프링의 AOP 프록시가 같은 클래스 내부에서의 메소드 호출은 가로채지 않기 때문이다.
- 프록시 기반 AOP : 스프링은 트랜잭션을 처리하기 위해 프록시를 사용한다. 프록시는 @Transactional이 적용된 메소드를 호출할 때 트랜잭션을 시작하고, 메소드가 끝나면 커밋 또는 롤백한다.
- 근데 같은 클래스에서 호출하면 메소드 호출이 프록시를 통하지 않고 직접 메소드를 호출한다. 그렇기 때문에 B에서 트랜잭션이 있는 A를 호출하면 @Transactional이 A에 적용되지 않는 문제가 생긴다.
상황2) 다른 클래스에서 호출되는 경우
그럼 다른 클래스에선 어떨까 ?
@Component
public class AnotherClass {
@Autowired
private MyService myService; // 아까 A, B 메소드 가지고 있던 클래스
public void callTransactionalMethod() {
myService.A(); // 이 호출은 트랜잭션이 유지됨
}
}
이 클래스는 아까 썼던 MyService를 주입받아 transactionalMethod를 호출한다. 이 호출은 프록시를 통해 이루어지므로, A의 @Transactional 설정이 적용된다. 그렇기에 우린 같은 클래스에서 호출되는지, 그리고 두 메소드에 @Transactional이 붙어있는지를 면밀하게 살펴볼 필요가 있다.
3. 서로 다른 클래스에서 호출해야 다른 트랜잭션 생성
하나의 클래스에서 A메소드가 B메소드를 호출했다면, 그리고 둘 다 @Transactional이 있다고 해도 트랜잭션은 1개만 만들어진다. 새로운 트랜잭션을 만들고싶다면 이를 분리해서 서로 다른 클래스에서 메소드를 호출해줘야한다.
4. @Transactional 우선순위
(우선순위 낮음) 인터페이스 - 클래스 - 메소드 (우선순위 높음)
- 메소드 레벨 : 메소드에 @Transactional이 적용되면 이 설정이 우선적으로 적용된다. 즉 클래스나 인터페이스에 설정된 @Transactional은 오버라이드된다. 즉, 메소드에 적용된 트랜잭션 옵션에 따라 적용된다는 것이다.
- 클래스 레벨 : 클래스 레벨에서 @Transactional이 적용된 경우, 클래스에 있는 모든 public 메소드에 트랜잭션이 적용된다. 하지만 메소드가 별도로 @Transactional을 가지고 있으면 메소드가 먼저 적용된다.
- 인터페이스 레벨 : 인터페이스에 @Transactional이 적용되면 인터페이스의 메소드에 대해 트랜잭션을 적용할 수 있지만, 실제로는 클래스에 적용된 설정이 우선시된다. 즉, 인터페이스에 설정된 트랜잭션은 클래스에서 별도로 재정의된 경우 무시된다.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(propagation = Propagation.REQUIRED, readOnly = false) // 클래스 레벨
public class MyService {
@Transactional(readOnly = true)
public void readOnlyMethod() {
// 이 메소드는 읽기가능 트랜잭션이 적용됨
}
public void regularMethod() {
// 이 메소드는 따로 설정한 게 없으니까 클래스 레벨의 트랜잭션(읽기불가능)적용된다.
}
}
나중에 추가하기) AOP 프록시가 가로챈다는 내용 , @Transactional 작동방식
'1일 1개념정리 (24년 8월~12월) > Spring' 카테고리의 다른 글
1일1개 (15) - 콩 너는 죽었다 (0) | 2024.08.24 |
---|---|
1일1개 (14) - ArgsConstructor (0) | 2024.08.22 |
1일1개 (11) - JDBC 발전 과정 (0) | 2024.08.19 |
1일1개 (10) - JDBC (0) | 2024.08.18 |
1일1개 (8) - Spring 왜 쓸까 ? (0) | 2024.08.16 |