-
Notifications
You must be signed in to change notification settings - Fork 1
기술 공유 ‐ 트랜잭션
일반적인 트랜잭션 설명 → 이상적인 상황에서의 설명이 많음
데이터 중심 어플리케이션 저자의 트랜잭션 설명 → 데이터베이스 복제와 파티셔닝 등 분산환경과 하드웨어, 네트워크 오류 등 여러 문제를 다 겪어본 저자의 분산 및 동시성 이슈를 고려한 설명
- 현실 세계의 데이터베이스는 많은 문제를 가질 수 있음
- SW / HW 문제
- 어플리케이션 종료
- 네트워크 문제
- 여러 클라이언트의 동시요청
- 부분 갱신된 비정상 데이터
- 경쟁조건 ….
- 문제의 단순화 ⇒ 트랜잭션
트랜잭션이란 애플리케이션에서 몇개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 것
- 개념적으로 한 트랜잭션 내의 모든 읽기와 쓰기는 하나의 연산으로 실행
- 트랜잭션은 전체가 성공(Commit) 하거나 실패(Abort, Rollback) 한다.
- 트랜잭션 실패시 안전하게 재시도 가능
- 트랜잭션은 자연법칙이아니라 앱의 프로그래밍 모델을 단순화하려는 목적으로 만들어짐
- 잠재적인 오류와 동시성 문제 무시 가능 → 안전성 보장(Safety Guaranatee)
- 모든 앱에서 트랜잭션이 필요하지는 않으며 때로는 트랜잭션적인 보장을 완화하거나 아예 쓰지 않는 것이 이득이다(성능을 향상 시키거나 가용성을 높일 수 있다.)
- 트랜잭션의 안정성 보장과 이와 관련된 비용을 정확히 이해해야한다. → 경쟁 조건과 격리 수준
더이상 쪼갤 수 없음을 의미한다.
- 예를 들어, 트랜잭션 하나에 쓰기 작업이 3개 있었다면 3개 중 일부만 성공하는 경우는 있을 수 없다.
- 전부 다 성공하거나 전부 다 실패하거나, 둘 중 하나만 가능하다.
- 오류가 생겼을 시 트랜잭션을 어보트(Abort, 롤백)하고 해당 트랜잭션의 모든 내용을 취소하는 능력으로 원자성 보다 **어보트 능력(Abortability)**이라 해도 어울린다.
- 일관성이란 단어는 여러 의미로 사용된다.
- 복제 일관성과 최종적 일관성
- 일관성 해싱
- 일관성 = 선형성
- 일반적인 ACID 맥락의 일관성은 데이터베이스가 좋은 상태에 있어야 한다는 것의 앱에 특화된 개념
- 일관성의 아이디어는 항상 진실이어야 하는 데이터에 관한 어떤 선언**(불변식(Invariant))**이 있다는 것
- 외래키나 유니크 등이 불변식에 속한다
- 다만 데이터 베이스는 불변식 위반의 잘못된 데이터를 쓰도록 막기 힘들다
- 일관성은 데이터베이스의 속성이 아닌 애플리케이션의 속성이다
- 대부분 동시에 여러 클라이언트에서 데이터베이스에 접속하면서 동시성 문제(경쟁 조건)에 맞닥뜨리게 됨
- 격리성은 동시에 실행되는 트랜잭션은 서로 격리된다는 것을 의미한다.
- 트랜잭션은 다른 트랜잭션을 방해할 수 없다
- 고전적으로는 직렬성이라는 용어 사용
- 트랜잭션이 순차적으로 실행되었을 때와 같은 결과를 보장
- 하지만 직렬성 격리는 성능저하가 일어나기에 잘 사용하지 않고 스냅샷 격리를 ****사용하기도 한다
- 지속성은 트랜잭션이 성공적으로 커밋 되었다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에 기록한 모든 데이터는 손실되지 않는다는 보장
- 하드웨어와 백업이 동시에 파괴되면 할수 있는 것이 없으므로 완벽한 지속성은 없음
여기서 말하는 객체는 테이블, 도큐먼트, 레코드 등을 말한다. 단일 객체 연산은 말 그대로 객체 1개에 대한 연산을, 다중 객체 연산은 객체 여러개에 대한 연산을 말한다. 예를 들어 SNS에서 회원가입 처리를 할 때 회원 테이블 하나만 업데이트한다면 단일 객체 연산을 한 것이고, 다른 테이블도 같이 업데이트를 한다면 다중 객체 연산을 한 것이다.
- 단일 객체 연산은 경량 트랜잭션으로 불리기도 한다.
- 갱신 손실 방지하므로 유용
- 다중 객체 트랜잭션
- 단일 객체로 처리한다면 오류처리가 훨씬 복잡해질 수 있고 동시성 문제도 생길 수 있다면 여러 객체를 한번에 트랜잭션하는 것이 좋다
- ACID를 보장하는 데이터베이스라면 보통 잘못된 트랜잭션 연산을 허용하지 않는다. 즉, abort한다.
- 반면 어느정도 잘못된 연산을 허용하는 경우를 'best effort를 한다'라고 표현한다.
- 데드락을 예로 들어보자.
- 데드락 자체가 절대 발생하지 않게 막는 데이터베이스는 문제의 원인이 되는 트랜잭션을 abort 시킬 것이다.
- 반면 데드락 발생을 막기 위해 best effort를 하는 데이터베이스는 데드락의 발생 자체를 막지는 않는다. 다만 데드락 발생을 감지해서 그 상황을 빠져나오는 방법을 제공할 것이다.
둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태.
- 더티 읽기
- 한 클라이언트가, 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 읽는 경우.
- 더티 쓰기
- 한 클라이언트가, 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 덮어쓰는 경우.
- 읽기 스큐(비반복 읽기)
- 비반복 읽기 nonrepeatbale read 라고도 부른다. 같은 읽기 요청을 2번 보냈을 때, 일시적으로 서로 다른 부분을 보게 되는 경우.
- 갱신 손실
- 두 클라이언트가 도잇에 read-modify-write 주기를 실행할 때, 한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 채 그 내용을 덮어써서 데이터가 손실되는 경우.
- 쓰기 스큐
- 트랜잭션이 무언가를 읽은 뒤 그 값을 기반으로 어떤 결정을 해서 그 결정을 DB에 쓴다. 그러나 쓰기를 실행하는 시점에서는 결정의 전제가 더이상 참이 아닌 경우.
- 팬텀 읽기
- 트랜잭션이 어떤 검색 조건에 부합하는 객체를 읽고 있을 때, 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 실행하는 경우.
- 완화된 격리
- 커밋 후 읽기
- 스냅샷 격리
- 직렬성
- 커밋 전의 변경사항도 읽을 수는 있음, 업데이트는 못함
- 더티 읽기 있음
- 더티 쓰기 없음
- 트랜잭션 전의 값을 읽게 함
- 쓰여진 모든 객체에 대해 과거에 커밋된 값과 현재 쓰기 잠금을 가지고 있는 트랜잭션에서 쓴 새로운 값 모두를 기억하여 쓰기 잠김을 가지고 있지 않은 트랜잭션들은 과거의 값을 읽게 함 => 질의마다 독립된 스냅숏 사용
- 더티 읽기 없음, 더티 쓰기 없음
- 갱신 손실 있음, 읽기 스큐 있음
- 트랜잭션 이전의 스냅샷을 읽게 함
- 트랜잭션이 시작할 당시 커밋된 상태였던 (과거) 데이터를 읽고, 갱신할 때는 값을 교체하지 않고 새 버전을 생성
- 체 트랜잭션에 대해 동일한 스냅숏 사용객체마다 커밋된 버전 여러 개를 유지해야하므로 다중 버전 동시성 제어 multi-version concurrency control, MVCC 기법을 구현
- 트랜잭션마다 ID를 할당, 삭제 요청 트랜잭션도 마찬가지트랜잭션 ID가 더 크거나 ID를 할당받을 시점에 진행중인 모든 트랜잭션이 쓴 데이터가 아닌 데이터를 읽음나중에 아무 트랜잭션도 접근하지 않는다는게 확실해지면 가비지 컬렉션 프로세스가 오래된 객체 버전을 삭제 -> 이때 색인 항목도 삭제됨
- 더티 읽기 없음, 더티 쓰기 없음 (쓰기 잠금 사용)
- 읽는 쪽에서 쓰는 쪽을 차단하지 않고, 반대의 경우도 마찬가지이므로 백업이나 분석 등 실행하는 데 오래 걸리며 읽기만 실행하는 질의에 요긴
- 스냅숏 격리를 구현한 데이터베이스마다 직렬성, 반복 읽기 등 다른 이름을 사용
- 구현방법이 여러가지
-
직렬 실행
트랜잭션이 짧고 단일 코어에서 처리가능한 양이면 단순해서 선택할만함
- 단일 스레드에서 순서대로 트랜잭션 실행
- 트랜잭션을 스토어드 프로시저 안에 캡슐화
- 파티셔닝 시 여러 파티션이 각각의 스레드에서 접근 가능하지만 여러 파티션에 접근해야할 경우 성능저하
-
2단계 잠금 (2 Phase Lock, 2PL)
표준적 방법이지만 성능이 나빠서 잘 안씀
- 두개의 트랜잭션이 동시에 같은 객체에 쓰려고 하면 잠금은 나중에 쓰는쪽이 진행하기 전에 먼저 쓰는 쪽에서 트랜잭션을 완료할 때 까지 기다리도록 보장
- 쓰기 실행 객체가 아니면 여러 트랜잭션에서 읽을 수 있지만 쓰기는 기다려야함
- 성능 이슈가 있음
- 비관적 동시성 제어 기법
- 상호배제와 비슷
- 서술 잠금, 색인 범위 잠금 등으로 팬텀 방지함
-
직렬성 스냅샷 격리 (SSI, Serializable snapshot isolation)
트랜잭션이 차단하지 않고 진행되도록 함 - 커밋할 때 직렬적이지 않다면 어보트
- 낙관적 동시성 제어 기법
- 뒤처진 전제에 기반한 결정
- 오래된 MVCC 읽기 감지
- 과거의 읽기에 영향을 미치는 쓰기 감지
읽기 전용 트랜잭션에서 읽을 수 있는 데이터는 격리 수준에 따라 달라진다.
트랜잭션 기술 제공
→ 트랜잭션 추상화(PlatformTransactionManager)
→ AOP 적용(@Transactional 어노테이션 사용)
-
1. 트랜잭션(Transaction) 동기화
- Spring은 트랜잭션 동기화(Transaction Synchronization) 기술을 제공하고 있다.
- 트랜잭션 동기화는 트랜잭션을 시작하기 위한 Connection 객체를 특별한 저장소에 보관해두고 필요할 때 꺼내쓸 수 있도록 하는 기술이다.
- 트랜잭션 동기화 저장소는 작업 쓰레드마다 Connection 객체를 독립적으로 관리하기 때문에, 멀티쓰레드 환경에서도 충돌이 발생할 여지가 없다. 그래서 다음과 같이 트랜잭션 동기화를 적용하게 된다.
// 동기화 시작 TransactionSynchronizeManager.initSynchronization(); Connection c = DataSourceUtils.getConnection(dataSource); // 작업 진행// 동기화 종료 DataSourceUtils.releaseConnection(c, dataSource); TransactionSynchronizeManager.unbindResource(dataSource); TransactionSynchronizeManager.clearSynchronization(
기술 종속적인 문제를 해결하기 위해 Spring은 트랜잭션 관리 부분을 추상화한 기술을 제공하고 있다.
-
2. 트랜잭션(Transaction) 추상화
Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용함으로써 애플리케이션에 각 기술마다(JDBC, JPA, Hibernate 등) 종속적인 코드를 이용하지 않고도 일관되게 트랜잭션을 처리할 수 있도록 해주고 있다.
Spring이 제공하는 트랜잭션 경계 설정을 위한 추상 인터페이스는 PlatformTransactionManager 이다. 예를 들어 만약 JDBC의 로컬 트랜잭션을 이용한다면 DataSourceTxManager를 이용하면 된다.
이제 우리는 사용하는 기술과 무관하게 PlatformTransactionManager를 통해 다음의 코드와 같이 트랜잭션을 공유하고, 커밋하고, 롤백할 수 있게 되었다.
public Object invoke(MethodInvoation invoation) throws Throwable { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { Object ret = invoation.proceed(); this.transactionManager.commit(status); return ret; } catch (Exception e) { this.transactionManager.rollback(status); throw e; } }
하지만 위와 같은 트랜잭션 관리 코드들이 비지니스 로직 코드와 결합되어 2가지 책임을 갖고 있다. Spring에서는 AOP를 이용해 이러한 트랜잭션 부분을 핵심 비지니스 로직과 분리하였다.
-
3. AOP를 이용한 트랜잭션(Transaction) 분리
예를 들어 다음과 같이 트랜잭션 코드와 비지니스 로직 코드가 복잡하게 얽혀있는 코드가 있다고 하자.
public void addUsers(List<User> userList) { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { userService. this.transactionManager.commit(status); } catch (Exception e) { this.transactionManager.rollback(status); throw e } }
위의 코드는 여러 책임을 가질 뿐만 아니라 서로 성격도 다르고 주고받는 것도 없으므로 분리하는 것이 적합하다.
하지만 위의 코드를 어떻게 분리할 것인지에 대한 고민을 해야 한다. 흔히 떠올릴 수 있는 방법으로는 내부 메소드로 추출하거나 DI로 합성을 이용해 해결하거나 상속을 이용할 수 있을 것이다.
하지만 위의 어떠한 방법을 이용하여도 트랜잭션을 담당하는 기술 코드를 완전히 분리시키는 것이 불가능하였다. 그래서 Spring에서는 마치 트랜잭션 코드와 같은 부가 기능 코드가 존재하지 않는 것 처럼 보이기 위해 해당 로직을 클래스 밖으로 빼내서 별도의 모듈로 만드는 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)를 고안 및 적용하게 되었고, 이를 적용한 트랜잭션 어노테이션(@Transactional)을 지원하게 되었다. 이를 적용하면 위와 같은 코드를 핵심 비지니스 로직만 다음과 같이 남길 수 있다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 사용할 트랜잭션 관리자
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
// 선택적 전파 설정
Propagation propagation() default Propagation.REQUIRED;
// 선택적 격리 수준
Isolation isolation() default Isolation.DEFAULT;
// 트랜잭션 타임 아웃
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
// 읽기/쓰기 vs 읽기 전용 트랜잭션
boolean readOnly() default false;
// 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열
Class<? extends Throwable>[] rollbackFor() default {};
// 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열
String[] rollbackForClassName() default {};
// 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열
Class<? extends Throwable>[] noRollbackFor() default {};
// 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열
String[] noRollbackForClassName() default {};
}
Spring의 DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다. 해당 4가지 속성은 트랜잭션을 세부적으로 이용할 수 있게 도와주며, @Transactional 어노테이션에도 공통적으로 적용할 수 있다.
- 트랜잭션 전파
- 격리수준
- 제한시간
- 읽기전용
-
DEFAULT
:데이터 베이스에서 설정된 기본 격리 수준을 따릅니다. -
READ_UNCOMMITED
: 트랜잭션이 아직 커밋되지 않은 데이터를 읽을 수 있습니다. -
READ_COMMITED
: Dirty Read 를 방지하기 위해 Commit 된 데이터만 읽을 수 있습니다. -
REPEATABLE READ
: 트랜잭션이 완료될 때까지 조회한 모든 데이터에 shared lock이 걸리므로 트랜잭션이 종료될 때까지 다른 트랜잭션은 그 영역에 해당하는 데이터를 수정할 수 없습니다. -
SERIALIZABLE
: 가장 엄격한 트랜잭션 격리수준으로, 완벽한 읽기 일관성 모드를 제공합니다. 이 격리 수준에서는 PHANTOM READ 상태가 발생하지 않지만 동시성 처리 성능이 급격히 떨어질 수있습니다.
-
REQUIRED
: 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작합니다. (디폴트 속성) -
SUPPORTS
: 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 처리합니다. -
REQUIRED_NEW
: 항상 새로운 트랜잭션을 시작합니다. 이미 진행중인 트랜잭션이 있다면 잠시 보류시킵니다. -
MANDATORY
: 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜색션을 시작하는 대신 예외를 발생시킵니다. 혼자서는 독립적으로 수행되면 안되는 경우에 사용됩니다. -
NOT_SUPPORTED
: 트랜잭션을 사용하지 않고 처리하도록 합니다. 이미 진행중인 트랜잭션이 있다면 잠시 보류시킵니다. -
NEVER
: 트랜잭션을 사용하지 않도록 강제시킵니다. 이미 진행중인 트랜잭션 또한 허용하지 않으며, 있다면 예외를 발생시킵니다. -
NESTED
: 이미 실행중인 트랜잭션이 있다면 중첩하여 트랜잭션을 진행합니다. 부모 트랜잭션은 중첩 트랜잭션에 영향을 주지만 중첩 트랜잭션은 부모 트랜잭션에 영향을 주지 않습니다.
-
readOnly
속성을 통해 트랜잭션을 읽기 전용으로 설정할 수 있다. -
JPA
의 경우, 해당 옵션을true
로 설정하게 되면 트랜잭션이 커밋되어도 영속성 컨텍스트를 플러시하지 않는다. 플러시할 때 수행되는 엔티티의 스냅샷 비교 로직이 수행되지 않으므로 성능을 향상 시킬 수있다.
- rollbackFor 옵션을 활용하여 CheckedException 또한 롤백 시킬 수 있다.
Isolation Level | Dirty Read | NonRepeatable Read | Phantom Read |
---|---|---|---|
READ_UNCOMMITTED | 가능 | 가능 | 가능 |
READ_COMMITTED | 불가능 | 가능 | 가능 |
REPEATABLE_READ | 불가능 | 불가능 | 가능 |
SERIALIZABLE | 불가능 | 불가능 | 불가능 |
격리 레벨(Isolation Level) | 설명 |
---|---|
DEFAULT | 연결되는 DB의 기본 격리 레벨을 따른다. 대부분의 관계형 DB의 deault Isolation Level은 READ_COMMITTED 이다. |
READ_UNCOMMITTED | 격리레벨중 가장 낮은 격리 레벨이다. 이 격리레벨은 다른 Commit되지 않은 트랜잭션에 의해 변경된 데이터를 볼 수 있기 때문에 거의 트랜잭션의 기능을 수행하지 않는다. |
READ_COMMITTED | 대개의 데이터베이스에서의 디폴트로 지원하는 격리 레벨이다. 이 격리 레벨은 다른 트랜잭션에 의해 Commit되지 않은 데이터는 다른 트랜잭션에서 볼 수 없도록 한다. 그러나 개발자들은 다른 트랜잭션에 의해 입력되거나 수정된 데이터는 조회할 수는 있다. |
REPEATABLE_READ | READ_COMMITED 보다는 다소 조금 더 엄격한 격리레벨이다. 이 격리레벨은 다른 트랜잭션이 새로운 데이터를 입력했다면, 새롭게 입력된 데이터를 조회할 수 있다는 것을 의미한다. |
SERIALIZABLE | 가장 많은 비용이 들지만 신뢰할만한 격리레벨을 제공하는 것이 가능하다. 이 격리레벨은 하나의 트랜잭션이 완료된 후에 다른 트랜잭션이 실행하는 것처럼 지원한다. |
데이터 중심 어플리케이션 설계
https://wikibook.co.kr/data-intensive-applications-ebook/
https://velog.io/@broccolism/동시성과-싸우기-트랜잭션-데이터-중심-애플리케이션-설계-7장
https://medium.com/@lsy.study/study-데이터-중심-애플리케이션-설계-7-트랜잭션-10b4b4c08efe
스프링 트랜잭션 참고
https://edu.nextstep.camp/s/tibBv57v/ls/wTCMcDAw
https://mangkyu.tistory.com/154