✅ @Transactional 기본 설정
- propagation(전파?) 속성은 PROPAGATION_REQUIRED 이다.
- 격리 수준은 ISOLATION_DEFAULT 이다.
- 읽고 쓰기가 가능하다.
- 타임아웃 시간은 트랜잭션을 관리하는 하부 시스템(ex. 데이터베이스 관리 시스템)에서 정의한 기본 타임아웃 시간으로 설정되거나, 설정값이 없을 경우 타임아웃이 적용되지 않는다.
- RuntimeException 또는 Error가 롤백을 유발하고, checked Exception은 유발하지 않는다.
(이는 DefaultTransactionAttribute에서 확인 가능하다.)
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
PROPAGATION_REQUIRED
물리적인 트랜잭션을 적용한다. 이 트랜잭션은 아직 트랜잭션이 없는 경우 현재 범위에 대해 로컬로 생성되거나, 더 큰 범위에 대해 이미 정의된 '외부' 트랜잭션에 참여한다.
기본적으로 참여하는 트랜잭션은 외부 범위의 트랜잭션 특성을 따르며 (만약 있을 경우) 로컬 격리 수준, 타임아웃 값 또는 읽기 전용 플래그를 무시한다. 즉, 외부 범위의 트랜잭션과 동일한 특성을 가진다.
(추가: 적혀있지 않은 noRollbackFor 속성은 전파되지 않는다. 즉, 외부 트랜잭션에서 noRollbackFor에 예외를 추가하고 내부 트랜잭션에서 해당 예외가 터져도 롤백 마크가 된다.)
내부 트랜잭션 범위가 롤백 전용 마커를 설정하는 경우 외부 트랜잭션은 롤백 자체를 결정하지 않았으므로 롤백(내부 트랜잭션 범위에 의해 자동으로 트리거됨)은 예상치 못한 현상이다. 따라서 UnexpectedRollbackException이 발생한다. 이는 트랜잭션 호출자가 커밋이 실제로 수행되지 않았는데 수행되었다고 오해할 수 없도록 하기 위한 예상되는 동작이다. 따라서 내부 트랜잭션(외부 호출자가 인식하지 못하는)이 자동으로 트랜잭션을 롤백 전용으로 표시하는 경우 외부 호출자는 여전히 커밋을 호출한다.
→ 이전 포스팅에서 예외 발생 원인을 알았다면, 이번에는 왜 'Unexpected'RollbackException이 발생하는지 알게 되었다.
✅ Spring의 AOP 그리고 Proxy
참고
- https://mangkyu.tistory.com/175
- https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
Spring AOP
Spring AOP는 대상 객체에 대한 프록시를 생성하기 위해 JDK 다이나믹 프록시 혹은 CGLIB를 지원한다. JDK 다이나믹 프록시는 JDK에 내장되어 있고 CGLIB는 오픈 소스 라이브러리로 존재한다. Spring에서는 Spring Core를 통해 CGLIB를 사용할 수 있다.
Spring Boot 2.0 부터 프록시 객체를 만들 시 기본으로 CGLIB로 생성된다.
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
// 생략
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class ClassProxyingConfiguration {
// 생략
}
}
Spring Boot 2.7.7 버전의 AopAutoConfiguration 클래스이다. 하위 클래스 ClassProxyingConfiguration에 붙은 @ConditionalOnProperty의 matchIfMissing 속성이 보인다. 'spring.aop'의 'proxy-target-class' property가 설정되지 않았다면 기본으로 true로 지정한다.
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {
}
그리고 해당 property가 true라면 CGLIB로 프록시를 생성하도록 설정되어 있다.
CGLIB는 JDK 다이나믹 프록시가 인터페이스를 이용해 프록시 객체를 만드는 것과 달리, 바이트 코드를 직접 조작해 상속을 이용하여 프록시 객체를 만든다.
그래서 @Transactional이 붙는 메서드는 접근 제어자를 private로 할 수 없구나... 💡
이 때 Spring Docs에서는 다음과 같은 주의를 준다.
- final 메소드는 CGLIB로 프록시를 적용할 수 없다. 런타임에서 생성되는 서브 클래스로 override될 수 없기 때문이다.
- Spring 4.0에서부터, CGLIB 프록시 객체가 Objenesis로 생성되기 때문에 생성자가 더 이상 두 번 호출되지 않는다. JVM이 생성자 우회를 허용하지 않는 경우에만 이전처럼 두 번 호출된다.
그래서 @Transactional을 붙이고 final을 선언할 수 없었던 거구나... 💡
✅ MySQL 아키텍처
참고: <Real MySQL 8.0> 1권
엔진 아키텍처
MySQL 서버는 크게 MySQL 엔진, 스토리지 엔진으로 구분된다.
스토리지 엔진
- InnoDB
- MyISAM
- Memory
스토리지 엔진은 위 기본 엔진 말고도 추가가 가능하며, 실제 데이터를 디스크 스토리지에 저장하거나 디스크 스토리지로부터 데이터를 읽어오는 부분을 전담한다. 즉 MySQL 엔진에서 작업을 정해주면 스토리지 엔진이 해당 작업을 처리하는 것이다.
스레딩 구조
MySQL 서버는 프로세스 기반이 아닌 스레드 기반으로 작동한다. 전통적인 스레드 모델은 커넥션 별로 포그라운드 스레드가 하나씩 생성되고 할당되는 모델로 MySQL 커뮤니티 에디션에서 사용된다.
Foreground 스레드 (클라이언트 스레드)
- 클라이언트가 작업을 마치고 커넥션을 종료하면 해당 스레드는 다시 스레드 캐시로 되돌아간다. 스레드 캐시에 유지할 수 있는 최대 스레드 개수는 thread_cache_size 시스템 변수로 정해져 있다.
- 데이터를 가져올 때는 데이터 버퍼나 캐시로 먼저 접근한 다음, 데이터가 없을 경우에만 직접 디스크 또는 인덱스 파일을 통해 가져온다. 이때 InnoDB 테이블은 MyISAM 테이블과 달리 데이터 버퍼나 캐시까지만 포그라운드 스레드가 처리하고 이후는 백그라운드 스레드를 통해 처리한다.
Background 스레드
- InnoDB에서는 쓰기 작업을 버퍼링해서 일괄 처리하기 때문에 쓰기 스레드(버퍼의 데이터를 디스크로 내려쓰는 작업을 처리)를 사용한다. 하지만 MyISAM에서는 기본적으로 쓰기 버퍼링 기능을 사용할 수 없어 하나의 포그라운드 스레드에서 다 처리한다.
InnoDB 스토리지 엔진
기본 키에 의한 클러스터링
MyISAM과 다르게 InnoDB에서는 기본적으로 기본 키를 기준으로 클러스터링 되어 저장된다. 즉 기본 키 순으로 디스크에 데이터가 저장되며, 모든 세컨더리 인덱스는 레코드의 주소(물리적 주소) 대신 기본 키 값을 논리적 주소로 사용한다.
외래 키 지원 📌
(MyISAM에서는 도대체 지원하는 게 뭐지?...)
- 외래 키를 거는 순간 부모 테이블과 자식 테이블 모두 해당 컬럼에 인덱스 생성이 필요하다.
- 변경 시에는 반드시 부모 혹은 자식 테이블에 데이터가 있는 지 체크하는 작업이 필요하다.
대표적으로 부모 테이블의 데이터를 삭제하는 경우가 있다. 부모 데이터를 삭제할 때 해당 데이터를 자식 테이블의 데이터가 참고하고 있다면 삭제에 실패하게 된다.
- 변경 시 부모 혹은 자식 테이블의 데이터를 체크하는 작업으로 lock이 전파되고, 이 때문에 데드락이 발생할 수 있다.
- foreign_key_checks 설정을 꺼 유효성 검증(외래 키 관계에 대한 체크 작업)을 하지 않을 수 있다. 이럴 경우 외래 키 관계의 부모 테이블에 대한 작업(ON DELETE CASCADE, ON UPDATE CASCADE)도 무시하게 된다.
MVCC(Muti Version Concurreny Control) 📌
잠금을 사용하지 않는 일관된 읽기를 제공하는 것이 목적이다. InnoDB는 메모리에 존재하는 언두 로그를 통해 이 기능을 구현한다.
만약 업데이트 쿼리를 실행하는 경우,
1. 언두 로그에 업데이트 전 값(변경되는 값만)을 저장한다.
2. 커밋 실행 여부와 관계 없이 버퍼 풀에 업데이트한 값을 저장하고, 이는 백그라운드 스레드에 의해 디스크 데이터 파일에 저장된다.
그리고 해당 쿼리가 커밋되지 않은 상황에서 다른 트랜잭션이 데이터를 조회하는 경우에는
- READ_UNCOMMITED: 커밋되지 않은 데이터를 읽을 수 있으므로 버퍼 풀에서 업데이트 된 데이터를 읽는다.
- READ_COMMITED 이상: 언두 로그에 있는 업데이트 전 데이터를 읽는다.
즉 InnoDB는 하나의 데이터를 대상으로 여러 버전을 저장해, 격리 수준에 맞추어 데이터를 보여준다.
이후 트랜잭션을 끝낼 때,
- 롤백: 언두 로그의 업데이트 전 데이터를 버퍼 풀로 복구한 후, 언두 로그의 데이터는 삭제한다.
- 커밋: 추가 작업이 없다. 다만 해당 언두 로그의 데이터를 필요로 하는 트랜잭션이 더 이상 없을 경우에 데이터를 삭제한다.
'개발일상 > TIL' 카테고리의 다른 글
[230916] MySQL InnoDB의 베타 락 (2) | 2023.09.17 |
---|---|
[230911] DTO와 도메인의 변환, 중첩 트랜잭션 (0) | 2023.09.14 |
[230910] JPA에서 부모가 자식을 제한해서 가지는 경우 (4) | 2023.09.10 |
[230907] Spring Boot 테스트의 롤백 (0) | 2023.09.08 |