더 많은 도움을 드리기 위해

열심히 포스팅 중입니다!


지나가다 📢 광고 한 번 눌러주시면

더 좋은 글로 보답하겠습니다. 🥰

기술 면접 준비

트랜잭션 격리수준 - 기술 면접 준비

평비 - Giveloper 2025. 5. 13. 07:00

 

👋 

안녕하세요~ 평비입니다!

오늘은 트랜잭션 격리수준 개념에 대해서 나름(?) 자세하게 포스팅을 준비해봤습니다!

 

앞서 포스팅한 송금/자동이체 동시성 이슈 예제를 가지고 설명해보겠습니다.

 

 

🍼 왕초보잔고를 먼저 확인하고 송금하는 식으로 해결하면 된다고 했습니다. 다만, 위와 같이 잔고를 확인 한 후 송금을 하기 전에 이미 자동이체가 되버렸다면? 그러면 안되겠죠?

이는 🍼 왕초보가 트랜잭션을 언급하지 않았기 때문인데요. 잔고 확인과 송금이 하나의 트랜잭션으로 묶여 있다면 일단은 가능한 일입니다.

위의 예시대로라면, 두 트랜잭션이 충돌하는 상황인데요. 자동이체 트랜잭션이 시작할 때는 아무런 트랜잭션이 없으니 시작하는데 문제가 없었지만, 송금 트랜잭션이 시작할 때는 자동이체 트랜잭션이 이미 데이터베이스를 조회한 상태니까, 송금 트랜잭션이 데이터베이스를 조회할 수 있느냐? 가 문제입니다.

 

이 때, 데이터베이스의 트랜잭션 격리 수준이 어떻게 되느냐에 따라 달라집니다.

 


💡 트랜잭션 격리수준?

트랜잭션의 격리성(Isolation) : 트랜잭션은 다른 트랜잭션에게 영향을 주지 않도록 격리되어 수행되는 특성

 

여러 트랜잭션이 동시에 실행될 때 트랜잭션 간의 간섭을 어느 정도 허용할지를 정의하는 설정입니다. 격리 수준에 따라 성능과 데이터 일관성의 균형을 다르게 조절할 수 있습니다.

 

각 데이터베이스별 트랜잭션 격리 수준에는 다음과 같은 항목들이 있습니다.

 

데이터베이스 별 트랜잭션 격리수준

 

그러면, 이게 무엇인지 아래와 같은 예시로 하나하나 알아보겠습니다. 설명을 위해, 예제를 좀 수정해봤습니다.

@Transactional
public boolean 자동이체 (...) {
	잔고조회(...);
	이체(...);
	알림(...);
	return true;
}

@Transactional
public boolean 송금 (...) {
	잔고조회(...);
	이체(...);
	return true;
}

 

자동이체 기능에 알림 기능이 추가되었습니다. 이에 따라 자동이체는 잔고 조회, 송금, 알림 기능이 하나의 트랜잭션으로, 송금은 잔고 조회, 송금 기능이 하나의 트랜잭션이네요.

 


1. Read Uncommitted

커밋되지 않은 데이터를 읽을 수 있는 가장 낮은 격리 수준

commit이 되지 않는 데이터를 조회한다는 건 어떤 의미일까요?

Read Uncommitted

 

트랜잭션 2가 데이터베이스를 조회할 때, 트랜잭션 1은 아직 끝나지 않았습니다. 심지어, 30만원이라고 이미 업데이트를 한 상태에요. 다만, commit은 되지 않았죠.

데이터베이스의 트랜잭션 격리수준이 Read Uncommitted인 경우, 트랜잭션 2는 데이터베이스로부터 잔고가 30만원이라고 조회를 하게 됩니다.

 

🍼 왕초보 : 오, 너무 좋은데요? 그러면, 송금액이 30만원인 경우에는 문제 없이 되겠네요!

 

🚨 하지만... 만약 알림 기능에 문제가 있었다면?

알림 전송 중 실패가 되어, 자동이체 트랜잭션이 롤백되면? 송금은 정상적으로 진행됐다면?

잔고에 80만원이 있는 걸 보고 30만원 송금을 했는데, 갑자기 0원이 되어있다??? 그 날 콜센터는 난리가 났을 겁니다...

(그래서, 알림은 트랜잭션에서 분리하긴 합니다.)

 

이렇게, commit이 되지 않은 데이터를 조회하는 걸 Dirty read라고 합니다. 그래서, 대부분의 실무에서는 최소한 Read committed 이상을 사용합니다. 심지어 Oracle의 경우는 commit이 되지 않은 데이터를 조회하지 못하도록 Read Uncommitted 격리수준을 아예 지원도 안합니다.

 


2. Read committed

트랜잭션이 커밋되어 확정된 데이터만 읽는 것을 허용하는 격리 수준

Oracle DB에서 기본 값으로 설정하고 있는 격리 수준인데요. 이 경우는 문제가 없을까요?

 

아쉽게도, Non-repeatable read 문제가 있습니다. 이건 어떤 경우에 발생하는 문제일까요?

자, 송금 시 잔고가 꼬이는 걸 방지하기 위해서 송금 로직 마지막에 잔고 검증 기능을 넣었다고 합시다.

@Transactional
public boolean remit (...) {
	getBalance(...);
	transferMoney(...);
	checkMoney(...); // 검증 로직 추가됨
}

 

 

read committed 격리 수준이 설정되어 있으니까, 트랜잭션 2가 시작되어 조회하는 시점에는 트랜잭션 1이 commit 안 된 시점이니까 80만원이 조회 될 거에요.

그렇다면... 과연, 트랜잭션 2의 마지막 시점에는 얼마가 조회될까요? 트랜잭션 1은 commit이 되었으니까, dirty read도 아니에요.

트랜잭션 2는 처음에 조회할 때 80만원이었고, 10만원 송금을 했으니까 마지막 조회 시, 70만원을 기대할 거에요.

하지만, 결과는 20만원이 조회되겠죠. (50만원 이체 + 10만원 송금 = 60만원 차감)

조회를 했는데, 같은 값이 반복되지 않는다는 거에요. 이 문제가 Non-repeatable read입니다.

 

여기서 잠깐!
🐣 초보: 트랜잭션 2에서 10만원 송금했지만, commit이 안된 경우니까 2번째 조회에서도 70만원을 기대할 게 아니라, 80만원이 조회되는 거 아닌가요?

헷갈릴 포인트를 정확히 짚으셨습니다. 하지만, 트랜잭션의 일관성(Consistency) 규칙에 따라, 내 트랜잭션 안에서 DML이 이뤄진 데이터는 즉시 내가 읽을 수 있습니다. 이걸 Read your own writes 라고 해요.
쉽게 생각해서 트랜잭션 1이 없다고 했을 때, 처음 조회 시 80만원, 내 트랜잭션 안에서 발생한 수정에 대해서는 커밋이 되지 않았더라도 같은 트랜잭션 안에서는 반영이 된 값으로 조회가 됩니다.

🚨 만약, 트랜잭션 2에서 만약 10만원이 아닌 50만원을 송금한다면?
🐣 초보: -20만원...? 그럼, 문제되는 거 아닌가요?

문제가 맞습니다. 데이터 베이스는 기본적으로 잔고가 마이너스가 되지 않도록 보장해주지는 않습니다. 비즈니스 로직 수준에서 막아야 합니다.
트랜잭션 격리 수준으로 모든 읽기/쓰기 충돌, 팬텀 리드, 갱신 손실 등을 막기 위해서는 Serializable이 유일한데, 성능 문제가 있어서, 실무에서는 거의 사용하지 않아요.

 

 


3. Repeatable read

트랜잭션이 시작된 시점에 읽은 데이터는 그 트랜잭션이 끝날 때까지 절대 바뀌지 않는 격리 수준

 

이게 어떻게 가능할까? 정답은 Snapshot!

다른 트랜잭션이 어떤 장난을 쳤든, 처음 조회 시 결과를 Snapshot 해놓고, 이후 조회 시에도 이 snapshot 결과가 쓰이기 때문에 조회 결과는 반드시 repeatable하다.

 

그러면, 이 경우는 문제가 없을까?

이 예제에 대해서 조회는 문제가 없습니다. (다른 예제라면 조회 문제가 있다... Phantom read)

Read committed의 경우처럼 잔고가 -20만원으로 조회되는 경우는 없습니다.

Repeatable read 격리 수준에서는 트랜잭션 1에서 무슨 일이 일어나든, 트랜잭션 2는 1번째 조회의 snapshot만 보기 때문에, 2번째 조회에서는 트랜잭션 2 내의 DML만 반영하여, 30만원이 조회됩니다.

다만, commit을 해버리면... 트랜잭션 1의 갱신을 덮어써버리는 문제가 발생한다. 즉, 여전히 갱신 손실이 발생합니다.

 

그냥... 아예 시작도 못하게 막아버리는 방법은 없을까?


4. Serializable

트랜잭션을 무조건 순차적으로 진행시키는 가장 고수준의 격리수준

Serializable 격리수준이 도입되면, 트랜잭션은 항상 순차적으로 진행된다. 트랜잭션이 끼어들 수 없으니 데이터의 부정합 문제는 발생하지 않으나, 동시 처리가 불가능하다.

 

자동이체가 진행되는 동안에는 송금이 불가능하다! 아주 단순하고 문제도 명확하다.

병렬성이 떨어진다. 계좌가 1개라면, 동시에 처리할 수 있는 이체는 1개만 처리할 수 있다는 문제가 있습니다!

 

 


자, 그러면 처음으로 돌아가봅시다.

 

송금 트랜잭션이 시작할 때는 자동이체 트랜잭션이 이미 데이터베이스를 조회한 상태니까, 송금 트랜잭션이 데이터베이스를 조회할 수 있느냐?

 

🍼 왕초보: 격리 수준이 Serializable만 아니라면, 가능합니다!

 

위 질문에 대해 우리 🍼 왕초보는 트랜잭션, 격리수준에 대해서 답변할 수 있는 🐣 초보가 되었습니다!

 
 

 

👏

자, 이렇게 트랜잭션 격리수준 개념에 대해서 다뤄봤습니다!
저도 이 포스팅을 작성하면서, 좀 더 상세하게 공부를 하게 된 것 같은데요...!

트랜잭션 격리 수준에 이어, 다음 포스팅으로는 답변에서도 계속 언급되었던 트랜잭션 락 개념에 대해서 준비해보겠습니다.
 
평비의 이 평범한 글이 여러분에게 비범한 도움이 되었으면 좋겠습니다 👍