text/common

transaction에 대한 구구절절

hoonzii 2023. 7. 26. 12:37
반응형

transaction이 뭔지?

  • 수행 시 분할할 수 없는 작업의 단위
  • 작업 수행이 되던가(commit) / 아니면 해당 작업 전체가 취소 (rollback)
  • DB에서는 부분적으로 업데이트되어 부정합이 일어나는 걸 방지하기 위해 동작하는 작업의 최소 단위를 의미

 

acid 성질

데이터의 부정합을 막기 위해서는 아래의 ACID 라는걸 만족해야 한다

원자성(Atomicity)

  • 위에 적어 놨듯이 하나의 작업이 더 이상 쪼갤 수 없음을 의미
  • 하나의 작업이 모두 commit 되던가 / 모두 rollback 되어야 함
  • 예를 들어 100개의 행에 대한 update 작업을 하나의 transaction으로 처리할 때 99개의 작업 완료 + 1개의 작업 실패 시 100개 전부 update 작업 X (rollback)

일관성(Consistency)

  • 작업 시작 전과 작업 완료 후, 상태가 변하지 않고 일정함을 의미
  • transaction이 끝날 때 DB의 여러 제약 조건에 맞는 상태를 보장하는 성질

고립성(Isolation)

  • 작업시 해당 작업이 처리하는 데이터에 다른 작업이 접근 못하는 성질
  • 예를 들어 작업_1 이 값(ex. a=1 → a=2)을 바꾸는 작업 도중, 작업_2가 해당 값(a)을 읽는다면 작업_1이 완료되기 전 일 때는 바꾸기 이전의 값(a=1)이 읽혀야 함을 의미 (읽으려는 값을 격리, 고립)

지속성(Durability)

  • 작업의 결과가 영구적으로 저장되어야 함을 의미
  • 완료 처리가 된 작업의 경우, 시스템은 해당 작업의 결과를 책임지고 저장 및 관리

출처

트랜잭션 - 해시넷

MySQL :: MySQL 8.0 Reference Manual :: 15.2 InnoDB and the ACID Model

 

위 원칙을 완벽하게 지키게 되면 한번에 한 transaction만 처리되어야 할 것이고, 그럼 굉장히 느린 프로그램이 될 것이다. 그래서 이런 성질을 최대한 지키면서도 동시에 많은 양의 transaction을 처리하기 위해 isolation level 이란걸 나눠놨다.

 

고립레벨에 따라 transaction을 빡세게 하냐(동시성을 낮추고, ACID를 빡세게 지키느냐),
느슨하게 하냐(동시성을 높히고, ACID를 느슨하게 지키냐)를 정할 수 있다.

 

고립 레벨은 행(row) 또는 index 단위의 잠금을 이용해 수행되는데 아래부터 잠금에 대해 간단히 살펴보자.

말하기에 앞서 mysql(inno DB engine)에서 transaction에 대해서 예를 들어 설명할 예정이다.

(mysql의 Inno db engine의 경우 default engine으로 transaction을 지원하기 위한 여러 개념을 포함하고 있다)

 

lock

mysql은 transaction 작업 내 순차적 진행(직렬화)에 대해 row(행) 단위 잠금을 지원한다.

transaction이 진행 시 작업 중인 행에 대한 접근을 잠금 한다는 의미이고,

여러 잠금(row-level lock / record lock / gap lock) 등이 존재한다.

 

row-level lock (행 단위 잠금)

row-level lock에는 크게 두 가지, 공유 잠금(Shared lock)과배타적 잠금(Exclusive lock) 이 존재한다.

 

공유 잠금(Shared lock)

  • 행 읽기를 위한 잠금
  • transaction_1 (T1) 이 읽기 잠금을 획득한 경우, transaction_2 (T2)는 읽기 잠금은 획득할 수 있지만,
    배타적 잠금(쓰기를 위한 잠금) 은 할 수 없다. (당연하게도 읽기 중에 내용이 바뀌면 안 되기 때문에!)

배타적 잠금(Exclusive lock)

  • 행 쓰기를 위한 잠금
  • T1이 쓰기 잠금을 획득한 경우, T2의 경우 읽기, 쓰기 잠금을 획득할 수 없다.

record lock

행에 걸리는 잠금이 아니라 DB의 index record에 걸리는 잠금이다.

SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

업데이트를 위한 table t에 c1값을 읽을 때 다른 transaction이 해당 값에 접근하지 못하게
lock (이경우엔 배타적 잠금, Exclusive lock)을 거는 것을 말한다.

만약 다른 trasaction이 t.c1 = 10인 조건의 값을 (삽입, 수정, 삭제) 시도할 때 막게 된다.

 

gap lock

record lock과 동일하게 index에 lock을 건다. gap이란 건 index “사이”를 의미한다.

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

예를 들어 위 작업이 수행 중이라면 10≤ t.c1 ≤ 20 인 index lock이 걸린다.

 

아래와 같은 연산은 index에 lock이 걸려 있기에 위 작업이 commit 되거나 rollback 이후에 동작하게 된다.

DELETE FROM t WHERE c1=15

위 lock은 작업이 완료되고 commit이나 rollback 시 풀리게 된다.

 

isolation level

고립 레벨에 따라 잠금을 이용해(혹은 이용 x) transaction의 강도를 조절해 동시성과의 tradeoff를 맞춰낸다.

 

만약 T1이 row의 값을 업데이트하는 중, T2가 해당 값을 읽는다면?

아니면 T1이 transaction 내 특정 row를 여러 번 읽는 와중에 T2가 해당 row를 업데이트한다면?

예를 들어보자

T1 start ----[find a=1]---------------------------------[find a=2?]------ end
T2 start ----[find a=1]----------[change a=1->2]--------[find b=2]------- end
// T1의 경우, isolation 위배
// transaction 작업내 동일 값에 대한 보장 필요

T1의 경우 a라는 값을 읽을 때 작업 내에서 동일한 값에 대해 보장받기 위해서는

row별로 고립되어 읽기, 쓰기가 가능해야 함

위와 같이 transaction 작업 중 서로 갱신과 읽기가 병렬로 수행되기 때문에 고립이 필요!

 

그래서 나온 isolation level은 크게 4가지가 존재

  • read uncommitted
  • read committed
  • repeatable read (mysql default)
  • serializable

read uncommitted

  • transaction 작업 시 commit 이 되지 않더라도 그 값을 읽을 수 있는 레벨
T1 start ----[insert a=1]----[insert b=1]----------- end
T2 start ---------------------------------[read b]-- end

위 예시를 보면 T1의 transaction이 end 되기 전 T2의 작업 중 값을 조회해 가져가는 것을 볼 수 있다.

commit이 안된 데이터를 읽는걸 dirty read라고 한다. 커밋되지 않더라도 읽을 수 있기에 실전에서 쓰이는지는 알 수 없지만, 읽고 쓰는데 서로 lock을 걸지 않아 동시성은 향상된다.

 

read committed

위 dirty read를 보완한 레벨로 commit 된 데이터만 읽게 하는 레벨

dirty read를 막게끔 transaction이 끝나기 전 데이터는 undo log에서 읽어오기 때문에 가능

[a=2,b=1] T1 start -----[change a=2>1]-----end
		undo log -------------[prev a=2] ------end
          T2 start ------[read a=2]--------end

commit 이전의 값은 mysql의 undo log에 기록되고, transaction의 작업이 끝나기 전까지 남아있다.

T2가 동일한 값을 읽으려 할 때, 바뀐 값(a=1)에 접근하는 것이 아닌 undo log에 써진 이전 값(a=2)에 접근하게 된다.

 

그러나… 치명적인 문제가 있다.

[a=2,b=1] T1 start -----[change a=2>1]------[change b=1>2]--end
          T2 start ----------[read b=1]------------------------[read b=2?]---end

T1은 미리 들어있던 값 a, b의 데이터를 변경하고 commit 했고,

T2는 T1의 commit이 끝난 후 데이터를 읽고 있다. commit이 끝났기 때문에
b의 경우, undo log값이 아닌 변경된 값을 읽어 오고 transaction 작업 내 읽어온 값이 서로 다른 걸 확인할 수 있다.

 

또 다른 예를 보자.

T1 start ----[insert Table a=1]------[insert Table b=1]--end
T2 start ----------------------[get Table.count(=0)]--------[get Table.count(=2)]---end

역시 위와 같은 흐름에서 문제가 된다.

T1은 Table에 a=1, b=1이라는 값을 집어넣는다.

T2는 연산 내 Table row 개수를 이용해 작업 중, T1의 작업이 commit 전/T1의 작업이 commit 된 다음 읽는 row 개수가 달라진다.

즉, read committed 레벨에서는 작업 도중에 값이 다르게 읽혀 올 수 있는 것이다.

 

repeatable read

transaction 내 select 작업 시 해당 select로 읽어온 데이터를 snapshot을 보관해 놓고, 작업 도중 다시 select 발생 시 다시 읽어오는 게 아닌 snapshot을 참고한다. (Consistent Nonlocking Reads)

*범위 조회 건에 대해 gap lock을 걸기에 중간에 다른 트랜잭션의 연산은 다른 snap을 보게 된다.

 

serializable

기본적으로는 repeatable read와 동일하지만 select query에 for share가 적용된다.
즉, 읽기 연산마다 공유 잠금이 걸리게 되는데

(A-1) SELECT state FROM account WHERE id = 1;
(B-1) SELECT state FROM account WHERE id = 1;
(B-2) UPDATE account SET state = ‘rich’, money = money * 1000 WHERE id = 1;
(B-3) COMMIT;
(A-2) UPDATE account SET state = ‘rich’, money = money * 1000 WHERE id = 1;
(A-3) COMMIT;

A-1, B-1 연산 시 모두에게 shared lock 이 걸리고,
B-2의 연산은 A-1 Lock이 풀리기를, A-2의 연산은 B-1의 Lock이 풀리기를 기다리다 데드락 상태에 빠지게 된다.
결국 timeout으로 변경되지 않고 연산은 끝나고 money 값은 변하지 않게 된다. (무결성 보장)

 

안전하지만 동작하지 않는 로직 완성이다.

대략적인 것들만 적었으니 더 자세한 Lock, isolation level에 대해서는 아래의 블로그를 참고바람…

 

출처 Lock으로 이해하는 Transaction의 Isolation Level

 

undo log

실행 취소 로그.
레코드의 집합으로 트랜잭션 실행 후 Rollback 시 Undo Log를 참조해 이전 데이터로 복구할 수 있도록 로깅해놓은 영역

 

작업 수행 중에 수정된 페이지들이 버퍼 관리자의 버퍼 교체 알고리즘에 따라서 디스크에 출력될 수 있다. 버퍼 교체는 전적으로 버퍼의 상태에 따라 결정되며, 일관성 관점에서 봤을 때는 임의의 방식으로 일어나게 된다. 즉 아직 완료되지 않은 트랜잭션이 수정한 페이지들도 디스크에 출력될 수 있으므로, 만약 해당 트랜잭션이 어떤 이유든 정상적으로 종료될 수 없게 되면 트랜잭션이 변경한 페이지들은 원상 복구되어야 한다. 이러한 복구를 UNDO라고 한다.

 

출처 Mysql Redo / Undo Log

 

mvcc (multi version concurrency control)

동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법 중 하나를 일컫는다.

위 isolation level의 예시들을 봤듯이 repeatable read에서 snapshot을 저장해 한 트랜잭션이 동작중 다른 트랜잭션과 별개로 이전에 읽던 값들을 문제없이 읽게 해주는 방법으로 이 snapshot의 변경사항이 적용되기 전까지
다른 트랜잭션은 이 snapshot값에 접근할 수 없다.

 

그렇기에 트랜잭션별로 snapshot이 존재한다. 이 값에 업데이트를 하면 이전 기록은 undo log에 남겨지고

해당 트랜잭션의 commit 혹은 rollback 이후 비로소 해당 snapshot이 지워지게 된다.

 

예를 들어보자

create table user(
	`seq` int NOT NULL,
	`name` varchar(10) NOT NULL,
	`district` varchar(10) NULL,
	PRIMARY KEY(seq)
)

INSERT INTO user(seq, name, district) VALUES (1, 'hoonzi', 'seoul');

user 테이블을 새로 만들고, 값을 집어넣었다.

이때 상태는

inno DB 버퍼풀 : user(1, 'hoonzi', 'seoul')
undo log :
disk : user(1, 'hoonzi', 'seoul')

이 상황에서 아래와 같이 연산한다고 하자.

(A-1) UPDATE user SET district='cheonAn' WHERE seq = 1;
(B-1) SELECT district FROM user WHERE seq = 1;
(A-2) COMMIT;

A-1의 연산이 끝나기 전 B-1이 seq=1에 접근해 값을 읽으려 한다.

A-1이 끝난 직후 commit 여부와는 관계없이 버퍼풀은 값 변경이 일어나고,

이전 값은 undo log에 저장된다. 이때 disk는 백그라운드 스레드에 의해 값이 변경되는데, 변경 시점에 대해서는 현재 작업과 무관하기 때문에 사용자는 알 수 없다.

inno DB 버퍼풀 : user(1, 'hoonzi', 'cheonAn')
undo log : user(1, 'hoonzi', 'seoul')
disk : user(1, 'hoonzi', '?')

B-1의 작업 시 commit이 안된 데이터를 읽지 않고 undo log에 적힌 값(district=”seoul”)을 읽는다.

이후 A-2 commit 후엔 undo log의 값을 지우고 현재 값으로 적용하게 된다.

 

간략하게 특징을 정리하자면

  • 각 트랜잭션 별로 여러 버전이 존재하기에 잠금을 하지 않고 빠르게 동작 가능
  • 그러나, 트랜잭션이 완료되기 전까지 snapshot이 존재하므로 해당 정보 저장 및 관리 등 부가적인 작업에 대한 오버헤드 발생 가능

출처 [Database] MVCC(다중 버전 동시성 제어)란?

 

JAVA에서 transaction 

jdbc

java는 jdbc 라이브러리를 통해 DB와 커넥션을 맺고 쿼리문을 실행시킨다.

물론 commit과 rollback도 지원한다. 가장 기초적인 java jdbc 예제를 보자

String driverName = "com.mysql.jdbc.Driver";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
    Class.forName(driverName);
    connection = DriverManager.getConnection(server_url, server_id, server_pw);
    connection.setAutoCommit(false); 
// insert, delete, update 등 연산 수행시 트랜잭션 보장을 위해

    // 트랜잭션 내 작업

    connection.commit(); 
// commit시 해당 작업이 완료됌

    resultSet.close();
    preparedStatement.close();
    connection.close(); // 작업 완료시 해당 커넥션을 닫아주기
} catch (SQLException | ClassNotFoundException e) {
    e.printStackTrace();
}

먼저 DB 서버와 연결을 맺고

Class.forName(driverName);
connection = DriverManager.getConnection(server_url, server_id, server_pw);

작업이 완료되지 않았을 때 중간에 commit 되지 않도록 autocommit을 막아준다.

connection.setAutoCommit(false);

작업이 완료된 뒤, commit 처리와 맺고 있던 connection을 반환해 준다.

connection.commit(); 
// commit시 해당 작업이 완료됌

resultSet.close();
preparedStatement.close();
connection.close(); // 작업 완료시 해당 커넥션을 닫아주기

직접 프로젝트에서 수행해 보려면 자세한 건 아래의 링크를 통해….

출처 mysql-connector-java 사용 및 다운로드 방법

 

내가 사용하는 Spring boot에서는 @Transactional 어노테이션을 붙이면 trasaction 작업을 지정할 수 있게 되는데

public class UserService {

    @Transactional
    public Long registerUser(User user) {
       // execute some SQL that e.g.
        // inserts the user into the db and retrieves the autogenerated id
        // userDao.save(user);
        return id;
    }
}

해당 어노테이션이 붙어 있는 함수를 아래와 같이 변경해 준다.

public class UserService {

    public Long registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)

            // execute some SQL that e.g.
            // inserts the user into the db and retrieves the autogenerated id
            // userDao.save(user); <(2)

            connection.commit(); // (1)
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}

앞서 살펴본 jdbc 기초 예제와 동일하게 동작하게끔 변경된다.

 

출처 

Transaction 동작 원리, JDBC 기본에 충실한 과정(JPA, Hibernate, Spring's @Transactional)

@Transactional 동작 원리

 

transaction propagation

@Transactional 어노테이션은 어노테이션을 붙인 것만으로도

해당 메서드를 하나의 트랜잭션처럼 작업화 할 수 있게 되는데

예를 들어

@Transactional
public void transaction_X() {
	this.insertA();
	this.updateB();
}

@Transactional
public void insertA() {
}

@Transactional
public void updateB() {
}

아래와 같이 어노테이션이 각각 붙은 작업에 대해 transaction_X에서

하나의 transaction으로 묶어 처리 가능하다.

이런 걸 논리적 트랜잭션이라고 부른다.

 

물리적 vs 논리적

물리적 트랜잭션 : 실제 JDBC 트랜잭션 작업

논리적 트랜잭션 : 중첩된 @Transactional 작업을 갖는 메서드

 

논리적 트랜잭션의 경우, 하나의 메서드 내 여러 다른 @Transactional 메서드를

하나의 작업화 하기 위해 transaction을 전파하는데 ( transaction Propagation)

전파 레벨에 따른 동작의 차이가 있다.

@Transactional(propagation = Propagation.REQUIRED)
  • Propagation.REQUIRED
    • default 값이기 때문에 생략가능
    • 부모 트랜잭션 내에서 실행하며, 부모 트랜잭션이 없을 경우 새로운 트랜잭션 생성
    • 해당 메서드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않다면 트랜잭션을 새로 시작한다.(새로운 연결을 생성하고 실행한다.)
    • 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 로직을 실행한다.(동일한 연결 안에서 실행됨)
    • 예외가 발생하면 롤백이 되고 호출한 곳에도 롤백이 전파된다

여러 다른 옵션이 존재하지만 나는 아직까지는 default로만 사용했기에
다른 옵션의 경우 아래의 블로그를 참고하면 좋을 듯하다.

 

출처 

[Spring] @Transactional - 1 전파 레벨(propagation)

[Spring] Transaction 동작 원리 (@Transactional 원리)

 

직접 connection 객체를 관리하며 transaction 열고 닫고를

어노테이션으로 추가만으로 할 수 있어 편리 하지만 주의해야 할 점이 있다.

“트랜잭션은 길어질수록 오버헤드가 발생한다”

springboot에서 jpa를 쓸 땐, 엔티티 객체를 영속성 컨텍스트를 통해 관리하는데

사용자 요청이 들어올 때마다 entityManager를 통해 connection을 사용한다.

 

connection 사용 시 커넥션 풀에서 연결된 connection을 할당받아 사용할 때

작업이 완료되기 전까진 해당 connection을 점유하게 된다.

이때 불필요하게 오래 connection을 잡고 있으면 다른 entityManager가 connection을 쓰기 위해

기다릴 가능성이 높아진다. 또한 그 작업이 시작하는 순간 하나의 영속성 컨텍스트가 형성되고,

이 영속성 컨텍스트는 해당 트랜잭션이 끝날 때까지 유지된다.

당연히 그 기간 동안 DB connection 역시 붙잡고 있어야 한다.

 

게다가 transaction이 완료되지 않고 시간이 지날수록 DB 관점에서는 앞서 살펴본 대로

다른 transaction 작업에 방해되지 않게 undo log에 해당 버전을 관리하는데

해당 작업부를 많이 조회할수록 이전 작업에 대한 log는 점점 쌓여간다.

되도록이면 transaction은 얼른 필요한 부분만 처리하고 connection을 닫아주는 게 프로그램과 서버입장에서 좋을 것이다.

 

출처 @Transactional은 만능이 아닙니다 - 1: Transaction의 개념과 트레이드오프

 

분산 트랜잭션

간단하게 transaction에 대해서 살펴봤는데, java에서 connection, 혹은 db client (mysql의 경우) 데이터 베이스 단위로 connection을 맺고, 해당 DB에 한 table 혹은 여러 table 간의 transaction을 보장해 준다.

 

그렇다면 여러 DB 혹은 다른 벤더사(예를 들어 oracle, mysql)의 DB 간의 transaction 보장은 어떻게 이루어 질까?

 

spring에는 다중 데이터베이스를 묶어주는 ChainedTransactionManager 가 존재한다.

여러 개의 transaction manager를 하나로 묶어서 동작하게 되는데 순차적으로 start - commit을 수행하게 된다.

중요한 건 마치 하나처럼 움직이게 하는 거지 하나가 아니라는 점이다. (완벽한 트랜잭션이 아니다!!)

 

Tx1 이 start 후 Tx2가 start → commit 되었다고 했을 때

만약 모종의 문제로 Tx1의 commit 동작이 완료되지 않는다면 Tx2의 commit은 완료된 상태이기 때문에

여러 데이터 베이스 간의 정합성이 깨지게 된다.

( 내가 돈을 송금하고(tx2), 상대방이 돈을 받지 못하는 결과(tx1 동작 실패)의 사태가 일어날 수도 있다는 얘기)

 

이 chainedTransactionManger 사용 시 중요한 점은 가장 오래 걸리고,
에러가 날 확률이 높은 transaction을 가장 나중에 선언해야 한다는 점이다. (위 그림에서는 Tx3에 선언해야 한다.)

tx3가 에러가 날 경우, tx2, tx1은 rollback이 가능하기 때문에 그렇다.

 

출처

ChainedTransactionManager 데이터소스 트랜잭션 연결하기

Springboot JPA Rollback Distributed Transaction with multi databases

 

saga pattern

msa의 경우, 각 서비스나 api에 관련된 DB들이 서로 다른 서버에 흩어져 있는 경우가 많은데 이 역시 transaction의 정합성을 깨뜨리기 쉽다. 그렇기에 chainedTransactionManager 이외에도 여러 방법을 사용하게 되는데

 

대표적으로 saga pattern이 있다.

saga pattern은 순차적으로 진행되는 로컬 트랜잭션의 모음으로

분산 트랜잭션 사용해야 하는 msa 서비스에서 동작하게끔 구성된다.

각 서버 혹은 서비스별로 로컬 트랜잭션이 존재할 때

  1. 트랜잭션은 작업 완료 시 메시지 혹은 이벤트를 보내고 다음 작업이 순차적으로 진행
  2. 순차적 진행하다가, 중간에 뻑났다? 이전 트랜잭션 작업은 롤백 대신 이전 값으로 되돌리는 ”보상 트랜잭션”을 수행

하는 방식이다.

출처 Saga 패턴 - Azure Design Patterns

 

이러한 saga pattern 은 크게 두 가지 종류의 구현으로 나뉜다.

 

1. 코레오그래피(Choreography)
⇒ 의사 결정과 순서화를 사가 참여자에게 맡긴다. 사가 참여자는 이벤트 교환 방식으로 통신한다

사가 참여자들끼리 이벤트를 수신하게 되면 트랜잭션을 처리한다.
이렇게 구현되면 서로에 대한 결합성은 낮아지게 된다. (단순히 이벤트 수신, 송신 정도만 처리하면 되니?)

문제는 이벤트 수신/송신 작업이 트랜잭션의 일부가 되어야 하고, 각 이벤트에 대해 변경되어야 할 자신의 데이터를 명확히 알 수 있어야 한다. 즉, 실패이벤트 수신 시 어떤 작업이 이뤄져야 하는지 참여자 본인이 알고 있어야 한다.

 

장점

  • 이해하기 쉽고 간단하다. (서비스는 단순히 이벤트 발행 및 구독)
  • 구축하기 쉽다.

단점

  • 어떤 서비스가 어떤 이벤트를 수신하는지 추측하기 어렵다.
  • 트랜잭션이 많은 서비스를 거쳐야 할 때 상태를 인지하기 어렵다.
  • 모든 서비스는 호출되는 각 서비스의 이벤트를 리슨해야 한다.
  • 서비스 간 순환 의존성, 참여자 간 결합도 급증에 대한 경계(모든 서비스끼리 구독해야 함)

2. 오케스트레이션(orchestration)
⇒ 사가 편성 로직을 사가 오케스트레이터에 중앙화한다. 사가 오케스트레이터는 사가 참여자에게 커맨드 메세지를 보내 수행할 작업을 지시한다. (아. 아. 행정실에서 전파합니다. XXX수병님은 당직실로. 이상)

코레오그레피와 반대로 오케스트레이터는 참여자들에게 메시지(명령)를 보내고 수신(응답)한 참여자들은
작업완료 시 완료 메시지를 오케스트레이터에 보내는 형식으로 동작한다.
완료 메시지를 받은 오케스트레이터는 다음 작업을 선별 후 위 과정을 반복한다.

 

장점

  • 서비스 간의 종속성이 없고 Orchestrator가 호출하기 때문에 분산트랜잭션의 중앙 집중화가 된다.
  • 서비스의 복잡성이 줄어든다.
  • 구현 및 테스트가 쉽다.
  • 롤백을 쉽게 관리할 수 있다.

단점

  • 모든 트랜잭션을 Orchestrator가 관리하기 때문에 로직이 복잡해질 수 있다.
  • Orchestrator라는 추가 서비스가 들어가고 이를 관리하면서 인프라의 복잡성이 증가된다.

출처

마이크로 서비스 패턴 #4 트랜잭션 관리

4장. Saga를 이용한 트랜잭션 관리

SAGA pattern을 이용한 분산 트랜잭션 구현하기

 

 

위 정보들을 찾다가 발견한 좋은 기사를 하나 덧붙인가.

토스뱅크는 어떻게 코어뱅킹을 MSA로 바꿨나 - Byline Network

(위 트랜잭션에 대한 구구절절을 읽고 이 글을 읽으면 단어와 구조가 아주아주 살짝 이해가 된다.)

 

토스뱅크는 코어뱅킹 아키텍처를 모바일과 대량 트래픽에 특화된 마이크로 서비스 아키텍처(MSA)로 전환

토스뱅크는 기술 스택을 바꿨다.

  • 쿠버네티스 위에 스프링 부트, 코틀린, JPA 등을 기반으로 개발
  • 비동기 메시지 처리와 캐싱은 카프카와 레디스를 채택

⇒ saga pattern

 

도메인 단위로 서비스를 구분

  • 이자지급을 위해 고객 금리조회, 이자의 회계처리를 위한 회계정보 등으로 구분
  • 기존 이자 받기 요청은 과거 고객 정보 조회를 거쳐 금리조회와 이자계산, 이자 송금, 회계처리를 한 개의 트랜잭션으로 처리
  • 새로운 코어뱅킹 아키텍처에서는 트랜잭션으로 묶지 않아도 되는 도메인은 별도의 마이크로 서버를 구성.
  • 각 서버의 API 호출을 통해 비즈니스 의존성을 느슨하게 가져가도록 구성

⇒ api라고 하는 거 보면 orchestration?

 

잔액조회의 동시성, 중복 문제

  • 계좌 단위 현재 잔액 데이터에 대해서만 고유한 로우 락킹(row locking)이 걸리도록 개발 ⇒ 동시성을 보장
  • 동시성이 발생했을 때 거래를 끝날 때까지 기다릴 수 있도록 재시도할 수 있는 로직과 타임아웃을 적용해, 고객관점에서 락(Lock)이 걸렸는지 모르게 구현

카프카를 활용한 비동기 트랜잭션 구현

  • 지금 이자 받기 트랜잭션에서 분리할 수 있는 테이블은 카프카를 이용해 트랜잭션에서 분리
  • 분리 기준은 고객의 잔액과 통장 데이터 관점에서 DB 쓰기 지연이 발생할 때, 실시간으로 문제가 발생하는지 접근
  • 기존 80회의 DML이 이뤄지던 지금 이자 받기 트랜잭션을 50회의 DML로 줄이는 개선

이렇게 트랜잭션에 대해 간략하게 살펴봤다. 다음엔 saga pattern을 한번 구현해 보는 걸로? (과연?)

반응형