[Section3] Spring MVC - 트랜잭션(Transaction)
🧑🏻💻 TIL(Today I Learned)
✔️ 트랜잭션(Transaction)
💡 트랜잭션(Transaction)?
➡️ 여러 개의 작업들을 하나의 그룹으로 묶어서 처리하는 단위
➡️ 물리적으로는 여러 개의 작업, 논리적으로는 하나의 작업으로 인식
예를 들어서 A의 5000을 B에게 계좌이체를 한다고 했을 때 A의 잔고는 5000원 감소, B의 잔고는 5000원 증가해야 함
- 위의 예시처럼 계좌이체라는 거래는 2가지 작업이 합쳐져서 하나의 작업으로 동작해야 함
- 만약 둘 중 하나가 문제가 생겨 실패한다면 심각한 문제가 발생한다.
➡️ 전부 성공하든가 전부 실패하든가(All or Nothing)의 둘 중 하나로만 처리되어야 함
: 이러한 트랜잭션 처리 방식은 애플리케이션에서 사용하는 데이터의 무결성을 보장하는 핵심적인 역할을 함
🔎 ACID 원칙 (트랜잭션의 특징)
➡️ 데이터베이스 내에서 일어나는 하나의 트랜잭션의 안전성을 보장하기 위해 필요한 것
1. 원자성(Atomicity)
➡️ 작업을 더 이상 쪼갤 수 없음을 의미
➡️ 논리적으로 하나의 작업으로만 인식해서 둘 다 성공하든가 둘 다 실패하든가 중에서 하나로만 처리되는 것이 보장되어야 함
2. 일관성(Consistency)
➡️ 트랜잭션이 에러없이 성공적으로 종료될 경우 비즈니스 로직에서 의도하는 대로 일관성 있게 저장되거나 변경되는 것을 의미
3. 격리성(Isolation)
➡️ 여러 개의 트랜잭션이 실행될 경우 다른 트랜잭션에 영향을 주지 않고 독립적으로 실행이 되어야 함을 의미
4. 지속성(Durablity)
➡️ 데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미
(참고 : https://reeeemind.tistory.com/87)
🔎 커밋(commit), 롤백(rollback)
- 커밋(commit)
- 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어, commit 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장됨
- commit 명령을 수행하지 않으면 작업 결과가 데이터베이스에 최종적으로 반영되지 않음
- commit 명령을 수행하면 하나의 트랜잭션 과정은 종료하게 됨
- 롤백(rollback)
- 작업 중 문제가 발생했을 때 트랜잭션 내에서 수행된 작업들을 취소함
- 트랜잭션 시작 이전의 상태로 되돌아감
💡Spring에서 트랜잭션 적용하기
➡️ 트랜잭션은 크게 로컬 트랜잭션과 분산 트랜잭션으로 구분할 수 있으며, Spring에서 사용되는 방식은 선언형 트랜잭션 방식과 프로그래밍 코드 베이스 트랜잭션 방식이 있음
➡️ 트랜잭션은 애플리케이션의 핵심 로직이 아닌 부가 기능이기 때문에 AOP의 적용 대상 중 하나라고 볼 수 있음
🔎 선언형 방식의 트랜잭션 적용
➡️ 선언형 방식으로 트랜잭션 적용하는 방법에는 두 가지가 있음
- 작성한 비즈니스 로직에 애너테이션을 추가하는 방법
- AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 코드를 감추는 방법
✔️ 만약 Spring Boot 를 사용하고 있지 않다면 아래와 같은 코드를 Spring Configuration에 추가해야 함
→ Spring Boot를 사용하고 있다면 트랜잭션 설정은 내부적으로 알아서 해줌
@Configuration
@EnableTransactionManagement
public class JpaConfig{
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(){
final LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource());
...
...
return em;
}
@Bean // 📍
public DataSource dataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
...
...
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(){
JpaTransactionManager transactionManager
= new JpaTransactionManager(); // 📍
transactionManager.setEntityManagerFactory(
entityManagerFactoryBean().getObject() );
return transactionManager;
}
}
- 트랜잭션은 기본적으로 데이터베이스와의 인터렉션 관계에 있기 때문에 데이터 커넥션 정보를 포함하고 있는 DataSource가 기본적으로 필요함
- Spring에서 기본적으로 트랜잭션은 PlatformTransacrionManager에 의해 관리되며 이 인터페이스를 구현하여 해당 데이터 액세스 기술에 맞게 유연하게 트랜잭션 기술을 적용할 수 있도록 추상화되어 있음
- 사용하는 액세스 기술이 JPA이기 때문에 구현 클래스인 JpaTransacrionManger 를 사용하고 있는 것을 알 수 있음
1. 작성한 비즈니스 로직에 애너테이션을 추가하는 방법
➡️ @Transactional 애너테이션을 트랜잭션이 필요한 영역에 붙여주면 됨
❓ 실습하던 중 애너테이션을 작성하고 있는데 Transaction에 관한 import 구문이 두 개가 존재함
import org.springframework.transaction.annotation.Transactional; // (1)
import javax.transaction.Transactional; // (2)
(1)번의 경우 : Spring Framework에 특화된 어노테이션으로, Spring 애플리케이션 내에서 트랜잭션 관리 기능을 제공
(2)번의 경우: JTA 표준 Java API로, 분산 트랜잭션을 관리하기 위한 Java EE 애플리케이션에서 활용됨
@Service
@Transactional
// 클래스 레벨에 추가하면 기본적으로 이 클래스에서 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션 적용됨
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
return memberRepository.save(member);
}
...
...
- 왜 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용될까?
: MemberRepository가 데이터베이스와 직접적인 연관이 있기 때문에
🏁 트랜잭션이 어떻게 적용되는지 확인하기 위한 로그 설정하기
➡️ JPA 로그 레벨 코드를 application.yml에 추가하기
➡️ DEBUG 레벨로 설정하면 JPA 내부에서 DEBUG 로그 레벨을 설정한 부분의 로그를 확인할 수 있음
➡️ 로그 레벨까지 설정했다면 애플리케이션을 실행시키고 포스트맨으로 postMember() 핸들러 메서드를 호출한 뒤 로그 확인하기
- MemberService의 createMember 가 호출되면서 새로운 트랜잭션을 생성하고 있음을 확인
- 트랜잭션에서 commit 이 일어나는 것을 알 수 있음 : Committing JPA transaction on EntityManager ~
- 트랜잭션 종료됨을 알 수 있음 : Not closing pre-bound JPA EntityManager after transaction
- JPA EntityManager가 종료됨을 알 수 있음 : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
✔️ rollback 동작 확인
➡️ 기존 코드를 아래와 같이 수정하고 애플리케이션 실행한 뒤 똑같이 포스트맨으로 postMembers() 핸들러 메서드 호출한 뒤 확인하기
public Member createMember(Member member) {
// 이미 등록된 이메일인지 확인
verifyExistsEmail(member.getEmail());
Member resultMember = memberRepository.save(member);
if(true) { // 메서드가 종료되기 전에 강제로 RuntimeException이 발생하도록 수정
throw new RuntimeException("Rollback test");
}
return resultMember;
}
(중간 로그 생략)
- rollback 잘 진행된 뒤 EntityManager가 닫히는 것까지 확인 가능
java.lang.RuntimeException 상속받는 언체크 예외는 @Transactional 애너테이션 추가해서 rollback 가능하지만
체크 예외의 경우 애너테이션만 추가한다고 해서 rollback이 되지 않음
➡️ 캐치(catch)해서 예외를 복구할지 회피할지 적절한 예외 전략을 고민하거나 별도의 예외 전략이 필요하지 않은 경우
@Transactional(rollbackFor = {SQLException.class, DataFormatException.class} ) 와 같이 예외를 직접 지정해주거나 감싸서 rollback이 동작하도록 할 수 있음
✔️ 메서드 레벨에서 @Transactional 적용
@Transactional(readOnly = true) // 읽기 전용 트랜잭션
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
- 트랜잭션 설정이 readOnly인 것을 알 수 있음
조회 메서드에 @Transactional(readOnly = true)로 설정하는 이유
: 기본적으로 JPA에서 commit 이 호출되면 영속성 컨텍스트가 flush됨 하지만 위와 같이 readOnly=true로 설정하면 JPA 내부적으로 영속성 컨텍스트를 flush 하지 않음 또한 변경 감지를 위한 스냅샷도 생성하지 않음
➡️ 전반적으로 불필요한 추가 동작을 줄일 수 있음
➡️ 조회 메서드에서 readOnly 속성을 true로 지정해서 JPA가 자체적으로 성능 최적화를 거치도록 하는 것이 좋음
✔️ 클래스 레벨과 메서드 레벨의 트랜잭션 적용 순서
- 클래스 레벨에만 @Transactional 애너테이션을 추가할 때
- 클래스 레벨의 @Transactional 애너테이션이 메서드에 일괄 적용
- 클래스 레벨과 메서드 레벨에 함께 적용된 경우
- 메서드 레벨의 @Transactional 애너테이션 적용
- 만약 메서드 레벨에 해당 애너테이션 없을 경우 클래스 레벨의 @Transactional 애너테이션 적용됨
🔎 여러 작업이 하나의 트랜잭션으로 묶이는 경우
➡️ 음료를 주문하면 주문한 개수만큼 스탬프가 찍힌다고 가정했을 때 주문 정보 저장을 위한 트랜잭션이 하나 시작되고, memberService에서 스탬프 업데이트를 위한 트랜잭션이 하나 더 시작될 것
이렇게 각각의 트랜잭션이 독립적으로 실행된다면 updateStamp() 동작에서 예외가 밸생한 경우 스탬프 도장은 찍히지도 않았는데 주문정보는 저장되는 이상한 경우 생김
@Service
@Transactional
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;
}
@Transactional
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
...
// 📍아래와 같이 애트리뷰트 지정하면 메서드 실행 시 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 사용
// 존재하지 않으면 새 트랜잭션을 생성하도록 해줌
@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);
}
...
...
}
- OrderService에서 createOrder() 메서드를 호출하면 트랜잭션이 하나 생성되며 createOrder() 메서드 내에서 updateMember() 메서드를 호출하면 현재 OrderService에서 진행 중인 트랜잭션에 참여함
- 트랜잭션이 하나로 묶여있기 때문에 MemberService의 updateStamp() 메서드 작업을 처리하는 도중에 예외가 발생해도 두 클래스에서 작업을 처리하는 메서드들이 모두 하나의 트랜잭션 경계 내에 있으므로 모두 rollback 됨
- 실제로 실행시켜봐도 OrderService에서 강제로 발생시킨 예외로 인해 rollback이 진행됨
→ h2 콘솔로 확인해 보면 주문 정보와 스탬프 정보가 반영되지 않았음을 확인할 수 있음
✔️ 트랜잭션 전파(Transaction Propagation)
➡️ 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때 어떻게 동작할 것인지 결정하는 방식
➡️ propagation 애트리뷰트를 통해서 설정할 수 있음
- Propagation.REQUIRED
: 일반적으로 가장 많이 사용되는 propagation 유형의 디폴트 값
: 진행 중인 트랜잭션이 없으면 새로 시작하고 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여 - Propagation.REQUIRES_NEW
: 이미 진행 중인 트랜잭션과 무관하게 새로운 트랜잭션 시작
: 기존에 진행 중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때까지 중지됨 - Propagation.MANDATORY
: 진행 중인 트랜잭션이 없으면 예외를 발생시킴 - Propagation.NOT_SUPPORTED
: 트랜잭션을 필요로 하지 않음을 의미
: 현재 진행 중인 트랜잭션이 있는 경우 해당 트랜잭션 일시적으로 중지
: 메서드가 실행되는 동안에는 트랜잭션을 시작하지 않으며 트랜잭션 컨텍스트에 접근하지 않음
: 메서드 내에서 수행되는 데이터베이스 작업은 트랜잭션없이 독립적으로 실행됨
: 즉, 특성 메서드를 트랜잭션 범위에서 벗어나게 하고자 할 때 사용함
✔️ 트랜잭션 격리 레벨(Isolation Level)
➡️ 트랜잭션은 다른 트랜잭션에 영향을 주지 않고 독립적으로 실행되어야 하는 격리성이 보장되어야 함
➡️ Spring은 이러한 격리성을 조정할 수 있는 옵션을 @Transactional 애너테이션의 isolation 애트리뷰트를 통해 제공함
- Isolation.DEFAULT
: 데이터베이스에서 제공하는 기본값 - Isolation.READ_UNCOMMITTED
: 다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용함 - Isolation.READ_COMMITED
: 다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용함 - Isolation.REPEATABLE_READ
: 트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 함 - Isolation.SERIALIZABLE
: 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 함
2. AOP 방식의 트랜잭션 적용
package com.codestates.config;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import java.util.HashMap;
import java.util.Map;
@Configuration // AOP 방식으로 트랜잭션을 적용하기 위한 Configuration 클래스 정의
public class TxConfig {
// TransactionManager : 마커 인터페이스, 단순히 트랜잭션을 처리하는 최상위 명세, 하위 인터페이스들을 구분하기 위한 용도
private final TransactionManager transactionManager;
public TxConfig(TransactionManager transactionManager) { // TransactionManager DI
this.transactionManager = transactionManager;
}
@Bean
// TransactionInterceptor : AOP 기반 트랜잭션 관리 인터셉터. 메서드 실행전후에 트랜잭션을 시작하고 커밋 또는 롤백하는 트랜잭션 관련 작업 수행
// 설정 파일에서 메서드 레벨 또는 클래스 레벨에 트랜잭션 애너테이션을 지정하면 해당 메서드를 감싸는 형태로 AOP 적용
public TransactionInterceptor txAdvice() { // 트랜잭션 매니저와 함께 작동
// 트랜잭션 속성을 결정하는데 사용되는 클래스, 메서드 이름을 기반으로 트랜잭션 속성을 매칭하여 적용할 수 있도록 도와줌
NameMatchTransactionAttributeSource txAttributeSource =
new NameMatchTransactionAttributeSource();
// 트랜잭션 속성을 정의하기 위한 클래스, 선언적 트랜잭션 관리 설정에서 사용, 격리 수준, 제한 시간, 읽기 전용 여부 지정 가능
RuleBasedTransactionAttribute txAttribute =
new RuleBasedTransactionAttribute();
txAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 트랜잭션 전파 설정
RuleBasedTransactionAttribute txFindAttribute =
new RuleBasedTransactionAttribute();
txFindAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txFindAttribute.setReadOnly(true); // 읽기 전용 여부 지정
// 트랜잭션을 적용할 메서드에 트랜잭션 애트리뷰트 매핑
Map<String, TransactionAttribute> txMethods = new HashMap<>();
txMethods.put("find*", txFindAttribute);
txMethods.put("*", txAttribute);
txAttributeSource.setNameMap(txMethods);
return new TransactionInterceptor(transactionManager, txAttributeSource);
}
@Bean
public Advisor txAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 포인트컷 지정 --> CoffeeServiece 클래스를 타깃 클래스로 지정함
pointcut.setExpression("excution(*com.codestates.coffee.service." +
"CoffeeService.*(..))");
// AOP에서 어떤 메서드에 어떤 어드바이스를 적용할지 결정하는 역할을 하는 클래스
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
'SEB_BE_45 > 공부 정리' 카테고리의 다른 글
[Section3] Spring MVC - Testing 2 (0) | 2023.07.02 |
---|---|
[Section3] Spring MVC - 테스팅(Testing) (0) | 2023.06.28 |
[Section3] Spring MVC - JPA 기반 액세스 계층 (0) | 2023.06.21 |
[Section3] Spring MVC - JDBC 기반 데이터 액세스 실습 (0) | 2023.06.21 |
[Section3] Spring MVC - 비즈니스 로직에 대한 예외처리 (0) | 2023.06.15 |