Develog
코드스테이츠 52일차 본문
학습 목표
- 트랜잭션(Transaction)이 무엇인지 이해할 수 있다.
- 트랜잭션(Transaction) 경계가 무엇인지 이해할 수 있다.
- 트랜잭션이 적용되지 않은 애플리케이션에서 어떤 문제점이 발생할 수 있는지 이해할 수 있다.
- Spring에서 지원하는 트랜잭션 방식을 이해할 수 있다.
- 샘플 애플리케이션에 트랜잭션을 적용할 수 있다.
트랜잭션(Transaction)이란?
- 사례 1
회원이 커피 주문 앱으로 카페라떼 두 잔을 선택하고 결제 버튼을 누른 후, 주문이 진행되는 중에 네트워크 오류로 인해 결제를 완료하는데 실패했다고 치자
그런데, 회원이 주문한 주문이 정상적으로 데이터베이스에 등록이 되고, 주문한 커피 수 만큼의 스탬프가 찍혔다.
이 경우, 카페를 운영하는 쪽에서는 판매 수익을 얻지 못하는 손해를 볼 것이고, 회원은 공짜로 커피를 마시게 될 것이다.
- 사례 2
반대로 회원이 주문한 커피에 대한 결제는 완료되었는데, 데이터베이스에 저장하는 중에 에러가 발생해서 회원이 주문한 커피 주문 정보가 데이터베이스에 정상적으로 등록이 되지 않았다고 치자
이 경우, 회원은 커피도 마시지 못한 채 금전적인 손해만 볼 것이다.
- 사례 3
마지막으로 회원의 커피 주문은 정상적으로 데이터베이스에 등록이 되었는데, 주문한 커피 수 만큼의 스탬프 횟수를 데이터베이스에 업데이트 하는 중에 에러가 발생해서 커피 주문은 완료 되었지만 나중에 확인해보니 스탬프가 누적되지 않았다고 치자
이 경우, 회원은 커피는 마시지만 아까운 스탬프 횟수를 잃게 될 것이다.
위에서 살펴본 세 가지 사례의 공통점은 두 개의 작업들이 마치 하나의 그룹처럼 묶여서 처리되는 중에 둘 중 하나라도 처리에 실패할 경우 애플리케이션의 신뢰성이 깨지는 상황이 발생하고 있는 것이다.
이처럼 트랜잭션은 여러개의 작업들을 하나의 그룹으로 묶어서 처리하는 처리 단위인데, 앞에서 본 사례처럼 애플리케이션의 신뢰성이 깨지는 상황이 발생하면 트랜잭션이라고 부를 수 없다.
무조건 여러 개의 작업을 그룹으로 묶는다고해서 트랜잭션이라고 부를 수 있는게 아니라 물리적으로는 여러 개의 작업이지만 논리적으로는 마치 하나의 작업으로 인식해서 전부 성공하든가 전부 실패하든가(All or Nothing)의 둘 중 하나로만 처리되어야 트랜잭션의 의미를 가진다.
ACID 원칙
트랜잭션의 특징을 이야기할 때는 일반적으로 ACID라는 원칙을 이용한다.
- 원자성(Atomicity)
트랜잭션에서의 원자성이란 작업을 더이상 쪼갤 수 없음을 의미한다.
앞에서 살펴본 트랙잭션 상황 3을 예를 들면, 커피 주문 작업과 스탬프 횟수를 증가시키는 두 작업은 어떤 작업은 처리해도 되고, 어떤 작업은 처리하지 않아도 되는 식으로 쪼개서 처리할 수 없다.
논리적으로 하나의 작업으로 인식해서 둘 다 성공하든가 둘 다 실패하든가(All or Nothing) 중에서 하나로만 처리되는 것이 보장되어야 한다.
- 일관성(Consistency)
일관성은 트랜잭션이 에러없이 성공적으로 종료될 경우, 비즈니스 로직에서 의도하는대로 일관성있게 저장되거나 변경되는 것을 의미한다.
트랜잭션 상황 3의 예에서는 주문한 커피의 수 만큼, 스탬프 횟수가 증가한다는 비즈니스 로직에 맞게 저장되거나 변경되어야 한다.
즉, 회원이 3잔의 커피를 주문했으면 스탬프 횟수가 3 증가한 값으로 저장이 되어있을거라고 예상했는데, 3이 아닌 숫자로 증가한 값이 조회 된다면 일관성에 위배 되는 것이다.
- 격리성(Isolation)
격리성은 여러 개의 트랜잭션이 실행될 경우 각각 독립적으로 실행이 되어야 함을 의미한다.
예를 들어 우리가 컴퓨터에서 워드 작업을 하고 있고, 동시에 뮤직 플레이어로 음악을 듣고 있다면 우리 눈에는 보이지 않지만 CPU는 위 두 가지 프로세스를 아주 빠른 속도로 번갈아가면서 실행을 시키는 것이다.
이처럼 데이터베이스 역시 성능 향상을 목적으로 한 개 이상의 트랜잭션을 번갈아가면서 처리할 수 있는데, 이 경우 각 트랜잭션이 다른 트랜잭션에 영향을 주지 않고 독립적으로 실행이 되어야 한다는 것이 바로 격리성(Isolation)이다.
4. 지속성(Durability)
트랜잭션이 완료되면 그 결과는 지속되어야 한다는 의미이다.
즉, 지속성은 데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미이다.
트랜잭션 커밋(commit)과 롤백(rollback)
커밋(commit)
- 커밋(commit)은 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어로써 commit 명령을 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장된다.
- 만약 commit 명령을 수행하지 않으면 작업의 결과가 데이터베이스에 최종적으로 반영되지 않는다.
- commit 명령을 수행하면, 하나의 트랜젝션 과정은 종료하게된다.
롤백(rollback)
- 롤백(rollback)은 작업 중 문제가 발생했을 때, 트랜잭션 내에서 수행된 작업들을 취소한다.
- 따라서 트랜잭션 시작 이 전의 상태로 되돌아간다.
총정리
- 트랜잭션은 여러개의 작업들을 하나의 그룹으로 묶어서 처리하는 처리 단위를 의미한다.
- ACID 원칙
ㄴ원자성(Atomicity)
ㄴ트랜잭션에서의 원자성(Atomicity)이란 작업을 더이상 쪼갤 수 없음을 의미한다.
ㄴ따라서 논리적으로 하나의 작업으로 인식해서 둘 다 성공하든 둘 다 실패하든가(All or Nothing)의 둘 중 하나로만 처리되는 것이 보장된다.
ㄴ일관성(Consistency)
ㄴ일관성(Consistency)은 트랜잭션이 에러없이 성공적으로 종료될 경우, 비즈니스 로직에서 의도하는대로 일관성있게 저장되거나 변경되는 것을 의미한다.
ㄴ고립성(Isolation)
ㄴ고립성(Isolation)은 여러 개의 트랜잭션이 실행될 경우 각각 독립적으로 실행이 되어야 함을 의미한다.
ㄴ지속성(Durability)
ㄴ지속성(Durability)은 데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미한다.
- 커밋(commit)
ㄴ커밋(commit)은 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어로써 commit 명령을 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장된다.
ㄴ만약 commit 명령을 수행하지 않으면 작업의 결과가 데이터베이스에 최종적으로 반영되지 않는다.
ㄴcommit 명령을 수행하면, 하나의 트랜젝션 과정은 종료하게된다.
- 롤백(rollback)
ㄴ롤백(rollback)은 작업 중 문제가 발생했을 때, 트랜잭션 내에서 수행된 작업들을 취소한다.
ㄴ따라서 트랜잭션 시작 이 전의 상태로 되돌아간다.
- JPA 기술을 사용한 데이터베이스와의 인터랙션은 내부적으로는 JDBC API를 통해서 이루어진다.
학습 목표
- 선언형 트랜잭션 방식의 의미를 이해할 수 있다.
- 선언형 트랜잭션 방식을 샘플 애플리케이션에 적용할 수 있다.
- 로컬 트랜잭션과 분산 트랜잭션의 의미를 이해할 수 있다.
- 분산 트랜잭션을 샘플 애플리케이션에 적용할 수 있다.
선언형 방식의 트랜잭션 적용
Spring에서 선언형 방식으로 트랜잭션을 적용하는 방법은 크게 두 가지이다.
첫 번째는 우리가 작성한 비즈니스 로직에 애너테이션을 추가하는 방식이고, 또 하나는 AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 적용 코드 자체를 감추는 방식이다.
Spring Boot에서의 트랜잭션 설정
트랜잭션 관련 설정은 Spring Boot이 내부적으로 알아서 해주기 때문에 따로 우리가 설정해줄 필요는 없다.
애너테이션 방식의 트랜잭션 적용
Spring에서 트랜잭션을 적용하는 가장 간단한 방법은 @Transactional이라는 애너테이션을 트랜잭션이 필요한 영역에 추가해 주는 것이다.
@Transactional 애너테이션을 클래스 레벨에 추가하면 기본적으로 해당 클래스에서의 지정한 기능을 이용하는 모든 메서드에 트랜잭션이 적용된다.
다만 Exception, SQLException, DataFormatException 같은 체크 예외(checked exeption)는 @Transactional 애너테이션만 추가해서는 rollback이 되지 않는다.
체크 예외의 경우, 말 그대로 체크를 해야되는 예외이므로 캐치(catch)한 후에 해당 예외를 복구할지 회피할지 등의 적절한 예외 전략을 고민해 볼 필요가 있을 것이다.
만일 별도의 예외 전략을 짤 필요가 없다면 @Transactional(rollbackFor = {SQLException.class, DataFormatException.class})와 같이 해당 체크 예외를 직접 지정해주거나 언체크 예외(unchecked exception)로 감싸서 rollback이 동작하도록 할 수 있다.
✔ 메서드 레벨에 @Transactional 적용
@Service
@Transactional // (1)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// (2)
@Transactional(readOnly = true)
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
...
...
}
(1)과 같이 앞에서 이미 추가한 클래스 레벨 @Transactional 애너테이션 이외에 (2)와 같이 findMember() 메서드에 @Transactional(readOnly = true)를 추가했다.
이 경우, findMember() 메서드는 읽기 전용 트랜잭션이 적용된다.
조회 메서드에 @Transactional(readOnly = true) 로 설정하는 이유는?
findeMember()와 같은 조회 메서드에@Transactional(readOnly = true)로 설정해도 commit 절차를 진행하기는 하지만…
JPA에서 commit이 호출되면 영속성 컨텍스트가 flush된다.
그런데 @Transactional(readOnly = true) 로 설정하면 JPA 내부적으로 영속성 컨텍스트를 flush하지 않는다.
또한 읽기 전용 트랜잭션일 경우, 변경 감지를 위한 스냅샷 생성도 진행하지 않는다.
flush 처리를 하지 않고, 스냅샷도 생성하지 않으므로 불필요한 추가 동작을 줄일 수 있다.
즉, 조회 메서드에는 readonly 속성을 true로 지정해서 JPA가 자체적으로 성능 최적화 과정을 거치도록 하는것이 좋다.
✔ 클래스 레벨과 메서드 레벨의 트랜잭션 적용 순서
- 클래스 레벨에만 @Transactional이 적용된 경우
ㄴ클래스 레벨의 @Transactional 애너테이션이 메서드에 일괄 적용됩니다.
- 클래스 레벨과 메서드 레벨에 함께 적용된 경우
- 메서드 레벨의 @Transactional 애너테이션이 적용됩니다.
ㄴ만약 메서드 레벨에 @Transactional 애너테이션이 적용되지 않았을 경우, 클래스 레벨의 @Transactional 애너테이션이 적용된다.
여러 작업이 하나의 트랜잭션으로 묶이는 경우
주문한 커피 수 만큼의 스탬프 수를 업데이트 하는 작업(트랜잭션이 하나로 묶여있는)이 있다고 치자
OrderService 코드 일부
@Transactional // (1)
@Service
public class OrderService {
private final MemberService memberService;
private final OrderRepository orderRepository;
private final CoffeeService coffeeService;
public OrderService(MemberService memberService,
OrderRepository orderRepository,
CoffeeService coffeeService) {
this.memberService = memberService;
this.orderRepository = orderRepository;
this.coffeeService = coffeeService;
}
public Order createOrder(Order order) {
verifyOrder(order);
Order savedOrder = saveOrder(order);
updateStamp(savedOrder);
// (2)
throw new RuntimeException("rollback test");
// return savedOrder;
}
private void updateStamp(Order order) {
Member member = memberService.findMember(order.getMember().getMemberId());
int stampCount = calculateStampCount(order);
Stamp stamp = member.getStamp();
stamp.setStampCount(stamp.getStampCount() + stampCount);
member.setStamp(stamp);
memberService.updateMember(member);
}
private int calculateStampCount(Order order) {
return order.getOrderCoffees().stream()
.map(orderCoffee -> orderCoffee.getQuantity())
.mapToInt(quantity -> quantity)
.sum();
}
private Order saveOrder(Order order) {
return orderRepository.save(order);
}
...
...
}
(1)과 같이 클래스 레벨에 @Transactional 애너테이션이 추가 되었으므로 메서드 전체에 트랜잭션이 적용된다.
그리고 (2)에서 테스트를 위해 RuntimeException이 발생하도록 했다.
MemberService 코드 일부
@Transactional
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
...
// (1)
@Transactional(propagation = Propagation.REQUIRED)
public Member updateMember(Member member) {
Member findMember = findVerifiedMember(member.getMemberId());
Optional.ofNullable(member.getName())
.ifPresent(name -> findMember.setName(name));
Optional.ofNullable(member.getPhone())
.ifPresent(phone -> findMember.setPhone(phone));
Optional.ofNullable(member.getMemberStatus())
.ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));
return memberRepository.save(findMember);
}
@Transactional(readOnly = true)
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
...
...
}
(1)과 같이 @Transactional 애너테이션의 애트리뷰트로 propagation = Propagation.REQUIRED를 지정했다.
(1)과 같이 propagation 애트리뷰트의 값으로 Propagation.REQUIRED를 지정하면 메서드 실행 시, 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 존재하지 않으면 새 트랜잭션을 생성하도록 해준다.
따라서 OrderService에서 createOrder() 메서드를 호출하면 트랜잭션이 하나 생성되며, createOrder() 메서드내에서 updateMember() 메서드를 호출하면 현재 OrderService에서 진행 중인 트랜잭션에 참여한다.
트랜잭션 전파(Transaction Propagation)
트랜잭션 전파란 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때, 어떻게 동작할 것인지 결정하는 방식을 의미한다.
트랜잭션 전파는 propagation 애트리뷰트를 통해서 설정할 수 있으며, 대표적으로 아래와 같은 propagation 유형을 사용할 수 있다.
- Propagation.REQUIRED
우리가 앞에서 @Transactional 애너테이션의 propagation 애트리뷰트에 지정한 Propagation.REQUIRED 는 일반적으로 가장 많이 사용되는 propagation 유형의 디폴트 값이다.
진행 중인 트랜잭션이 없으면 새로 시작하고, 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여한다.
- Propagation.REQUIRES_NEW
이미 진행중인 트랜잭션과 무관하게 새로운 트랜잭션이 시작된다. 기존에 진행중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때까지 중지된다. - Propagation.MANDATORY
Propagation.REQUIRED는 진행 중인 트랜잭션이 없으면 새로운 트랜잭션이 시작되는 반면, Propagation.MANDATORY는 진행 중인 트랜잭션이 없으면 예외를 발생시킨다. - Propagation.*NOT_SUPPORTED*
트랜잭션을 필요로 하지 않음을 의미한다. 진행 중인 트랜잭션이 있으면 메서드 실행이 종료될 때 까지 진행중인 트랜잭션은 중지되며, 메서드 실행이 종료되면 트랜잭션을 계속 진행한다. - Propagation.*NEVER*
트랜잭션을 필요로 하지 않음을 의미하며, 진행 중인 트랜잭션이 존재할 경우에는 예외를 발생시킨다.
이처럼 Spring에서는 다양한 Propagation 유형을 지원하지만 작업별로 트랜잭션을 새로 생성해야 한다거나 특정 작업에는 트랜잭션을 적용하지 않는 등의 경우가 아니라면 @Transactional 애너테이션만 추가해도 무방하다고 생각하자
트랜잭션 격리 레벨(Isolation Level)
ACID 원칙에서 살펴보았다시피 트랜잭션은 다른 트랜잭션에 영향을 주지 않고, 독립적으로 실행되어야 하는 격리성이 보장되어야 하는데 Spring은 이러한 격리성을 조정할 수 있는 옵션을 @Transactional 애너테이션의 isolation 애트리뷰트를 통해 제공하고 있다.
- Isolation.DEFAULT
데이터베이스에서 제공하는 기본 값이다. - Isolation.READ_UNCOMMITTED
다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용한다. - Isolation.READ_COMMITTED
다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용한다. - Isolation.REPEATABLE_READ
트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 한다. - Isolation.SERIALIZABLE
동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 한다.
트랜잭션의 격리 레벨은 일반적으로 데이터베이스나 데이터소스에 설정된 격리 레벨을 따르는 것이 권장되므로, 이러한 격리 레벨이 있다라고 이해하고 넘어가자
AOP 방식의 트랜잭션 적용
Spring에서 대부분의 트랜잭션 적용은 @Transactional 애너테이션을 사용하는 방법만으로 간단하게 적용이 가능하다.
그런데 @Transactional 애너테이션 조차도 비즈니스 로직에 적용하지 않고, 트랜잭션을 적용하는 방법이 있다.
바로 AOP를 이용한 트랜잭션 적용이다.
// (1)
@Configuration
public class TxConfig {
private final TransactionManager transactionManager;
// (2)
public TxConfig(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Bean
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource txAttributeSource =
new NameMatchTransactionAttributeSource();
// (3)
RuleBasedTransactionAttribute txAttribute =
new RuleBasedTransactionAttribute();
txAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// (4)
RuleBasedTransactionAttribute txFindAttribute =
new RuleBasedTransactionAttribute();
txFindAttribute.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRED);
txFindAttribute.setReadOnly(true);
// (5)
Map<String, TransactionAttribute> txMethods = new HashMap<>();
txMethods.put("find*", txFindAttribute);
txMethods.put("*", txAttribute);
// (6)
txAttributeSource.setNameMap(txMethods);
// (7)
return new TransactionInterceptor(transactionManager, txAttributeSource);
}
@Bean
public Advisor txAdvisor() {
// (8)
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* com.codestates.coffee.service." +
"CoffeeService.*(..))");
return new DefaultPointcutAdvisor(pointcut, txAdvice()); // (9)
}
}
AOP 방식으로 트랜잭션을 적용하는 순서는 다음과 같다.
1. AOP 방식으로 트랜잭션을 적용하기 위한 Configuration 클래스 정의
(1)과 같이 @Configuration 애너테이션을 추가하며 Configuration 클래스를 정의한다.
2. TransactionManager DI
애플리케이션에 트랜잭션을 적용하기 위해서는 TransactionManager 객체가 필요하다.
(2)와 같이 TransactionManager 객체를 DI 받는다.
3. 트랜잭션 어드바이스용 TransactionInterceptor 빈 등록
Spring에서는 TransactionInterceptor를 이용해서 대상 클래스 또는 인터페이스에 트랜잭션 경계를 설정하고 트랜잭션을 적용할 수 있다.
- 트랜잭션 애트리뷰트 지정
트랜잭션 애트리뷰트는 메서드 이름 패턴에 따라 구분해서 적용 가능하기 때문에 (2), (3)과 같이 트랜잭션 애트리뷰트를 설정할 수 있다.
(3)은 조회 메서드를 제외한 공통 트랜잭션 애트리뷰트이고, (4)는 조회 메서드에 적용하기 위한 트랜잭션 애트리뷰트이다. - 트랜잭션을 적용할 메서드에 트랜잭션 애트리뷰트 매핑
설정한 트랜잭션 애트리뷰트는 (5)와 같이 Map에 추가하는데, Map의 key를 메서드 이름 패턴으로 지정해서 각각의 트랜잭션 애트리뷰트를 추가해주면 된다.
트랜잭션 애트리뷰트를 추가한 Map 객체를 (6)과 같이 txAttributeSource.setNameMap(txMethods)으로 넘겨준다. - TransactionInterceptor 객체 생성
(7)과 같이 TransactionInterceptor 의 생성자 파라미터로 transactionManager와 txAttributeSource를 전달한다.
4. Advisor 빈 등록
- 포인트 컷 지정
이제 트랜잭션 어드바이스인 TransactionInterceptor 를 타겟 클래스에 적용하기 위해 포인트 컷을 지정한다.
(8)과 같이 AspectJExpressionPointcut 객체를 생성한 후, 포인트 컷 표현식으로 CoffeeService 클래스를 타겟 클래스로 지정한다. - Advisor 객체 생성
마지막으로 (9)와 같이 DefaultPointcutAdvisor의 생성자 파라미터로 포인트컷과 어드바이스를 전달해준다.
총정리
- 트랜잭션 관련 설정은 Spring Boot이 내부적으로 알아서 해주기 때문에 개발자가 직접적으로 트랜잭션 설정해줄 필요가 없다.
- Spring에서는 일반적으로 애너테이션 방식( @Transactional )의 트랜잭션과 AOP 방식의 트랜잭션 적용 방식을 사용한다.
- 체크 예외(checked exeption)는 @Transactional 애너테이션만 추가해서는 rollback이 되지 않으며, @Transactional(rollbackFor = {SQLException.class, DataFormatException.class})와 같이 해당 체크 예외를 직접 지정해주거나 언체크 예외(unchecked exception)로 감싸야 rollback 기능을 적용할 수 있다.
- 트랜잭션 전파란 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때, 어떻게 동작할 것인지 결정하는 방식을 의미한다.
- @Transactional 애너테이션의 isolation 애트리뷰트를 통해 트랜잭션 격리 레벨을 지정할 수 있다.
'코드스테이츠' 카테고리의 다른 글
코드스테이츠 54일차 (0) | 2022.07.12 |
---|---|
코드스테이츠 53일차 (0) | 2022.07.11 |
코드스테이츠 51일차 (0) | 2022.07.07 |
코드스테이츠 50일차 (0) | 2022.07.06 |
코드스테이츠 48-49일차 (0) | 2022.07.04 |