RDB vs Nosql
- 개발
- 2025. 12. 12. 16:22
크게 DB에는 2가지 종류가 존재합니다. SQL을 사용하는 RDB와 SQL도 사용하는 NoSql(NoSql이 왜 Nosql인지에 대해 sql을 안써서 noSql이다 아니다 Not only sql의 약자라 그렇다라는 말들이 존재합니다. 안 중요하다고 생각합니다.)
그렇다면, 어떻게 다를까요? 성능을 올리는 방식에는 크게 두가지가 있습니다. 수평적으로 늘리는 방식과 수직적으로 늘리는 방식이 있습니다. 간단히 설명하자면 수평적으로 성능을 올리는 방법은 갯수나 방법등을 늘리는 방법이고, 수직적으로 늘리는 방법은 성능을 올리는 방법입니다. 이에 해당하는 기술들은 Replication, 파티셔닝, 샤딩등이 존재한다고 합니다. 자세하게 설명하면 다른 방법들이긴하지만 말이죠. 그리고 개발을 하게 되면 SQL을 사용하여 DB에 데이터를 저장하게 할 수 있어야 합니다. 대표적으로 java는 JDBC를 통해 DB에 접근을 할 수 있죠. 또한 이것을 활용하여 라이브러리, 프레임워크등이 만들어졌습니다. 대표적으로 mybatis와 JPA가 존재합니다. mybatis는 SQL매퍼라 불리며, JPA는 ORM이라고 불려집니다. 그렇다면, 본견적으로 시작해보죠. 아마 주로 RDB와 NoSql 위주로 설명할 예정입니다.
RDB
RDB는 스키마를 활용하여 데이터 무결성을 지키는 것을 목적으로 사용이 되며, Nosql은 RDB보다 유연하게 동작합니다. 즉, 대규모 트래픽과 빠른 쓰기/조회를 최우선으로 할 때 선택합니다. 대표적으로 로그, 캐시, 세션, 실시간 분석 등이 있습니다. 그렇다면, RDB는 어떻게 무결성을 지킬 수 있으며, Nosql은 어째서 RDB보다 빠른 조회 나 쓰기를 할 수 있는 걸까요?
RDB부터 설명하면, ACID를 보장하기 때문입니다. ACID가 과연 무엇일까요?
ACID
각각 Atomic(원자성) Consistency(일관성) Isolation(격리성) Durability(지속성) 의 약자입니다.
이것들은 어떠한 정보를 설명하고 있습니다. 그것이 무엇일까요? 바로 트랜잭션입니다. 트랜잭션이 뭐길래 이렇게까지 하는걸까요?
트랜잭션은 한 번에 처리되어야 하는 작업 단위이라고 생각하시면 됩니다. 보통 트랜재션 요청 한번을 SQL 요청 한번이라 생각할 수 있습니다. 하지만 이는 아닙니다. 제가 트랜잭션은 한 번에 처리되어야 하는 작업 단위라고 했습니다. 그렇다는건 트랜재션하나에 수많은 SQL을 넣을 수 있다는 뜻이 되어집니다.
@Transactional
public void test() {
...SQL 1번
... SQL 2번
}
spring boot에서는 트랜젹션을 추상화해서 제공을 하고 있습니다. 다만 실제 DB트랜잭션과 스프링의 트랜잭션은 서로 다르다는 점만큼은 인지하고 있으면 좋을 거 같습니다. 지금은 이거까지 설명하기에는 너무 갈길이 깁니다.ㅜㅜ
그렇다면 트랜재션은 ACID를 어떻게 보장할 수 있을까요?
각각이 어떤 특성이 있는지 부터 알아보자.
Atomic
원자성이라는 뜻을 가지고 있고 트랜잭션은 모두 성공하거나 실패한다는 원칙입니다. SQL 요청이 한개인 경우에는 당연할거 같지만, 여러개라면 어떨까요? A쿼리는 성공했는데 B쿼리는 실패했다면? 어떻게 해야 할까요? 이 트랜잭션은 실패입니다. 실패는 하나라도 실패했다면, 실패이기 때문이죠. 그렇다면 이미 실행이 되어버린 SQL은 어떻게 보장할 수 있을까요?
그것의 핵심은 Undo Log에 있습니다. RDB는 데이터가 변경되기 전 상태를 Undo Log에 저장을 시켜놓습니다. 이렇게 저장하는 목적은 실패시 rollback하기위한 목적이 있습니다.
갑자기 드는 궁금증은 Undo Log는 어디에 저장이 되어질까요? 데이터를 어딘가에 저장을 시키려면 메모리를 통해 저장을 해야 한다고 생각이 들기 때문이죠. Undo Log는 디스크에 저장이 되어지고, 처리 과정은 메모리를 거친다고 합니다.
트랜잭션이 실행되는 동안 서버가 죽어도 변경 전 데이터를 복구(rollback)할 수 있게 하려고 디스크에 저장한다고 합니다.
그렇다면, 트랜젹션이 실행이 되는 도중에 DB서버가 죽어도 복구가 가능하다는 뜻인걸까요? 가능하다고 합니다.
또, 복구가 된 이후에 Undo Log정보는 어떻게 되어지는가?
Undo Log는 트랜잭션이 끝난 뒤, 더 이상 필요 없으면 해제(free)되거나 재사용됩니다.
영구히 남아있지 않게 됩니다.
즉, rollback이 끝난 순간 또는 commit으로 더 이상 undo 정보가 필요 없는 순간 Undo는 더 이상 유지할 가치가 없기 때문에 재사용되어집니다.
또한, Undo방식은 Mysql이나 오라클에 해당되는 얘기라고 합니다. 간단하게 정리해봤습니다.
| DBMS | Undo Log 존재 여부 | Undo 저장 위치 | 트랜잭션 종료 후 | Undo 처리 방식비고 |
| MySQL (InnoDB) | 있음 | Undo Tablespace (디스크) | Purge Thread가 불필요한 Undo 레코드를 정리하고 공간을 재사용 | 가장 교과서적인 Undo 구조 |
| Oracle DB | 있음 (Undo Segment) | Undo Tablespace (Undo Segment) | Automatic Undo Management가 만료(Expired)된 Undo를 재사용 |
Undo 구조가 매우 정교함 |
| PostgreSQL | Undo 파일 없음 (MVCC 버전이 Undo 역할) | 테이블의 튜플 버전 (Heap) | VACUUM이 이전 버전 튜플을 삭제(cleanup) | Undo Log 파일이 존재하지 않음 |
| SQL Server | 전통적 Undo 없음 (Transaction Log 1개로 Undo/Redo 수행) |
Transaction Log (디스크) | 로그 체인이 유지되다가 Checkpoint 시 정리 | Undo 개념이 파일 단 위로 존재하지 않음 |
특정 DB에만 Undo Log가 있는것은 맞지만 전체적인 방식은 유사하다고 합니다.
Consistency
일관성이라는 뜻을 가지고 있습니다. DB의 무결성 상태를 유지하는 특성을 가지고 있습니다. 무결성 상태? 무결성 상태는 어떤 상태를 말하는 걸까요? 무결성에는 총 3가지가 존재합니다. 참조 무결성, 객체 무결성, 도메인 무결성이 존재하죠. 참조는 FK 객체는 PK 도메인은 칼럼은 허용된 값만 가질 수 있다고 합니다. 그럼 의문이 드는 사실이 있습니다.
@Transactional
public void test() {
...SQL 1번
... SQL 2번
}
위에서 코드를 가져왔습니다. 가져온 이유는 자고로 트랜잭션은 SQL을 여러개 적용을 시킬 수 있다고 하였습니다. 하지만 일관성은 이거와 거리가 조금 멀어 보입니다. 테이블 만들때 PK, FK, 칼럼에 대한 허용을 미리 설정이 되어집니다. 즉, 테이블 하나하나 마다 적용이 되는데 이거와 트랜잭션은 크게 관련이 없어 보입니다. Consistency는 유일하게 트랜잭션이 깨뜨리면 안되는 조건이라고 합니다.
왜 ACID에 포함이 되었을까요? 그 이유는 데이터의 올바름을 보여주기 위한 목적으로 사용이 되어진다고 합니다.
그렇다면, DB는 무결성을 어떻게 지키는 걸까요? PK/FK/칼럼 등은 개발자가 직접 설정하는 것이 맞습니다. 그리고 그것을 검증하는 역할은 DB측에서 발생합니다. 어느 시점에 할까요?
바로 commit 직전에 실행이 되어진다고 합니다. 만약 어긋나게 되면 실패하게 되어집니다.
Isolation
사실 이것을 작성하려고 ACID를 말하고 있는지 모르겠습니다. 이것은 격리성을 말합니다. 격리성 어떤것을 말하고 있는걸까요?
어쩌면 이것을 설명하보면 LOCK도 설명할 수 있을런지도 모르겠습니다. 아무튼 본격적으로 시작하십시다.
일단 격리성이란것은 무엇일까
여러 트랜잭션이 동시에 실행되는 환경에서 서로의 중간 결과를 절대로 보지 못하게 만드는 속성입니다.
즉, 트랜잭션은 마치 혼자 실행되는 것처럼 보여야 합니다. 만약 다른 트랜잭션이 어떤 값을 읽거나 쓰고 있더라도
현재 트랜잭션은 절대로 그 중간 상태에 간섭받아서는 안 됩니다. (동시성은 물리적인 동시성이 아닌 논리적인 동시성입니다.)
그렇다면, 격리성은 왜 필요할까요? 격리성이 존재하지 않는 환경에서는 어떤 문제가 발생이 되어질까요?
총 4가지 이슈가 발생하게 되어집니다. 과연 어떤 이슈인지 면밀히 살펴봅시다.
1. dirty Read
정의는 다음과 같다고 합니다.
commit되지 않은(uncommitted) 데이터를 다른 트랜잭션이 읽어버리는 상황을 말합니다. 이는 잘못된 데이터를 읽을 수 있습니다. 즉, 읽기 전까지는 이 데이터가 올바른 데이터인지 잘못된 데이터인지 알 수 없다는 뜻이 되죠.
어떻게 보면 슈뢰딩거의 고양이와 굉장히 유사하다고 생각이 들어집니다. 이번에는 특별하게 직접 실험해보는 시간을 가져보겠습니다.
환경부터 세팅을 해봅시다. 그냥 동작하면 dirty read는 동작하지 않기 때문이죠 그 이유는 추후 설명할 예정입니다.
세션1
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
UPDATE brand SET name = 'update' WHERE id = 1;
첫 번째, 격리 레벨을 더티리드로 변경합니다. UNCOMMITTED 가 더티리드라고 하는 군요 왜냐하면 아직 커밋을 읽기 전이기 때문이라고 합니다. non - repead와 다른 레벨입니다.
현재 상태는 커밋을 하지 않았습니다.
세션2
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT name FROM brand where id = 1;
세션1
ROLLBACK;
두 번째, 다른 세션에서 조회를 시도합니다. 이렇게 하면 어떻게 될까요? 레벨을 보면 알 수 있듯이 커밋되지 않은 상태를 보여주게 됩니다.
이 상태에서 롤백하면 어떻게 될까요? 이 전값이 보일까요? 아니면 변경 후가 보여질까요?
일반적인 환경(READ COMMITTED 이상)에서는 절대 보일 수 없는 값입니다. Dirty Read에서는 예외적으로 이런 값이 읽힙니다.

롤백이 수행되면 실제 DB에는 변경이 반영되지 않습니다. 하지만 Dirty Read는 rollback되기 전의 변경 값을 다른 세션이 먼저 읽어버리는 현상을 말합니다. 즉, 읽은 값은 실제로 commit되지 않았기 때문에 나중에는 존재하지 않을 수도 있는 값입니다.
이 때문에 Dirty Read는 매우 위험한 읽기 방식으로 분류됩니다.
2. non-repeated Read
그 다음은 non repeated Read입니다. 똑같이 정의 부터 확인해봅시다. 직관적으로 보면 반복적으로 읽기가 불가능한 현상을 뜻합니다.
즉, 한 트랜잭션이 같은 행(row)을 두 번 조회했을 때, 그 사이에 다른 트랜잭션이 해당 행을 수정 후 commit 하면 첫 번째 조회 결과와 두 번째 조회 결과가 달라지는 현상입니다.
음 그러니까 같은 행을 조회했는데 첫 번째 조회 결과와 두 번째 조회 결과가 서로 상이한 현상을 말하고 있는 거 같습니다. 더티리드는 가짜 값을 읽는 것이 문제였고,non repeated Read는 값이 중간에 바뀌는 문제라고 합니다.
과연 사실일까요? 한번 실험해봅시다.
세션1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT name FROM brand WHERE id = 2;
같은 트랜잭션안에서 동작을 해야 하기때문에 요렇게 작성하였습니다.
세션2
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE brand SET name = 'updated' WHERE id = 2;
COMMIT;
이것을 업데이트를 하고
세션1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT name FROM brand WHERE id = 2;
SELECT name FROM brand WHERE id = 2;
이렇게 다시 조회하게 되면 어떻게 보일까? 배운대로라면 다르게 나오는게 맞는데 과연 그럴까?
순간적으로 1번 쿼리와 2번쿼리의 값이 달라졌다. 근데 이게 그렇게 까지 문제일까? 순간적으로 바뀌어서 사진은 못찍었지만 아무튼 바뀌었다. ㅜㅜ
순간적인 변화가 요구되는 환경에서는 이와 같은 격리 수준을 사용하는 것은 위험할 수 있다고 한다. 그렇지 않은 환경에서는 크게 상관없다고 한다. 특히나 정합성이 요구되는 환경이라면 더더욱 사용하면 안되는 격리 수준이라 생각이 든다. 특히나 요즘과 같은 대용량 트래픽이 기본값이 된 환경에서는 자제해야 할거 같다고 생각이 드네요.
3. phantom Read
팬텀 리드.. 유령처럼 읽혀지는 현상을 말한다고 합니다. 이거 역시 정의 부터 확인해봅시다.
같은 트랜잭션 내에서 동일한 조건으로 두 번 SELECT를 했을 때, 첫 번째 조회에는 없던 새로운 행이 두 번째 조회에서 나타나는 현상을 말합니다. 즉, non Repeatable Read가 행의 값이 바뀌는 문제라면 phantom Read는 행의 개수가 바뀌는 문제입니다.
일반적으로 Mysql환경에서는 재현이 불가능하다고 합니다. (참고로 postgreSql에서는 기본 환경에서도 가능하다고 합니다.)그 이유는 MySql환경에서는 MVCC와 갭락이 원천 차단하기 때문이라고 합니다. 그렇다면 진짜 불가능할까요? (이에 대한 설명은 추후에 진행하겠습니다.)
그렇지는 않는다고는 합니다. Mysql환경에서도 재현이 가능하게 만들 수 있다고 합니다.
방법은 격리 수준을 READ COMMITTED로 낮추는 방법 또는 SELECT 시 스냅샷을 읽지 않도록 우회하는 방법이라고 합니다. READ COMMITTED에서는 UPDATE로 인해 non-repeatable read가 발생할 수 있고, INSERT로 인해 phantom read가 발생할 수 있습니다. 둘은 구분되는 현상이지만, 실습 시 UPDATE/INSERT가 동시에 일어나면 겉으로는 비슷하게 보일 수 있어 원인 분석이 헷갈릴 수 있습니다.
저는 둘중 두번째 방법을 선택해서 실습해보겠습니다.
그 이유는 간단합니다. 첫 번째 방법(READ COMMITTED)은 애초에 락 전략 자체가 달라지기 때문에 UPDATE/INSERT가 섞여 이상현상이 모호하게 보여질 수 있습니다. 반면 두 번째 방법은 SELECT가 MVCC의 스냅샷(Consistent Read)을 사용하지 못하도록 우회하는 방식입니다. IGNORE INDEX를 사용하면 InnoDB가 스냅샷 기반 읽기가 아니라 '현재 버전(Current Read)'을 수행할 가능성이 높아지며, 그 상태에서는 INSERT된 새로운 행이 바로 보입니다. 이 덕분에 팬텀 리드를 실제와 매우 유사한 형태로 재현할 수 있습니다.
실습을 시작해보겠습니다.
이번엔 특이 하게 데이터를 먼저 세팅해보겠습니다.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
age INT,
name VARCHAR(50)
);
데이터를 2~3개 정도 넣어두고
INSERT INTO users(age, name) VALUES (25, 'A'), (30, 'B'), (35, 'C');
세션1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM users IGNORE INDEX (`PRIMARY`) WHERE age > 20;
MySQL의 REPEATABLE READ는 MVCC의 스냅샷 읽기 덕분에 팬텀 리드를 원천 차단합니다.
갭락은 SELECT FOR UPDATE 또는 UPDATE/DELETE 시에 작동합니다.
그래서 팬텀 리드를 재현하기 위해서는 갭락을 정면으로 제거하려고 하기보다, SELECT가 갭락을 사용하지 못하도록 우회하는 방식을 사용해야 한다고 합니다. 그게 바로 인덱스를 무시하여 풀스캔을 강제하는 방식(IGNORE INDEX) 입니다.
(자세한 설명은 제가 이해가 가지 않는 관계로 생략하겠습니다.)
세션2
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
INSERT INTO users(age, name) VALUES (40, 'PHANTOM');
COMMIT;
이걸 실행해보면 놀라운일이 발생하게 합니다.
정상적인 환경에서는 다음 처럼 보인다고 한다.

이렇게 발생하는 이유는 DB는 스냅샷을 읽어서 보여주기 때문이라고 합니다.
그렇다면 팬텀리드가 발생하는 경우에는 어떨까요?

값이 추가 되는게 이상하다는 생각을 할 수 있습니다. 왜냐하면 insert하고 commit을 하면 select해서 데이터가 보여주는게 정상이 아닐까 생각이 듭니다. 하지만 자세히 보시면 세션1에서 트랜잭션을 종료시키지 않고 세션2를 실행을 시켰습니다. 이렇게 되면 트랜잭션의 스냅샷이 고정되기 때문에 INSERT가 커밋되어도 안 보이게 됩니다. 하지만 이해를 위해 이렇게 작성하였습니다.) Mysql에서는 어떻게 repeateble read에서 팬텀리드가 발생하지 않는지 추후에 알아봅시다.
4. Lost Update
마지막 이상 현상입니다. 위에서는 읽기에 관련된 현상이었다면 이번에는 쓰기와 관련된 현상입니다. 자세히 알아봅시다.
업데이트가 유실되는 현상입니다. 다른 현상들은 읽기에 관련된 현상들이기 때문에 결과가 잘못되었지 하고 넘어가면 될 수도 있지만 이 현상은 굉장히 위험합니다. 이 문제는 앞서 반영된 UPDATE 결과가 나중 트랜잭션의 UPDATE에 의해 그대로 덮어쓰여 유실되는 문제로, 데이터 정합성 자체에 직접적인 손상이 발생합니다.
그래서 대부분의 DBMS들은 Lock 또는 MVCC를 통해 자동으로 이 현상을 방지하려고 합니다.
팬텀 리드와 마찬가지로 의도적으로 실험을 해야 결과를 볼 수 있다고는 한다. 하지만 유실되어지는 현상을 직접 테스트를 굳이 할 필요까지는 없을 거 같다.
격리 수준
| 격리 수준 (낮→높) | Dirty Read | Non-Repeatable Read | Phantom Read | Lost Update | 특징 요약 |
| READ UNCOMMITTED | ❌ 차단 못함 | ❌ 차단 못함 | ❌ 차단 못함 | ❌ 차단 못함 | 사실상 “격리 없음”. 임시 데이터도 읽음. |
| READ COMMITTED | ✅ 차단 | ❌ 차단 못함 | ❌ 차단 못함 | △ 일부 DB만 차단 (MySQL은 차단됨*) | 대부분 상용 DB의 기본값. 커밋된 데이터만 읽음. |
| REPEATABLE READ (MySQL 기본값) | ✅ 차단 | ✅ 차단 | MySQL: 사실상 차단 (Gap + Next-Key Lock + MVCC) |
✅ 차단 | 동일 트랜잭션 내에서는 스냅샷 고정. |
| SERIALIZABLE | ✅ 차단 | ✅ 차단 | ✅ 완전 차단 | ✅ 완전 차단 | 모든 트랜잭션을 직렬처럼 강제. 동시성 ↓ 비용 ↑ |
각각의 DB의 SERIALIZABLE 구현 방식
| DB | SERIALIZABLE 구현 방식 | 성능 | 비고 |
| PostgreSQL | SSI (락 거의 없음) | 👍 상위권 | 직렬화 고정밀 보장 |
| MySQL/InnoDB | Lock 기반 | 👎 동시성 낮음 | 기본값 불가능 |
| Oracle | Serializable Snapshot 아님 | 👎 비싼 비용 | 기업은 잘 안 씀 |
| MSSQL | Lock 기반 | 👎 병목 발생 | 기본 사용 비추 |
각 DB의 기본 격리 수준
| DBMS | 기본 Isolation Level | 구현 방식 특징 |
| MySQL (InnoDB) | REPEATABLE READ | MVCC + Next-Key Lock으로 팬텀 리드까지 차단(사실상 Snapshot 기반 직렬화에 가까움) |
| PostgreSQL | READ COMMITTED | MVCC 기반, 필요 시 SERIALIZABLE(SSI) 사용 가능 |
| Oracle | READ COMMITTED | Consistent Read(Undo 기반), Repeatable Read 없음 |
| SQL Server (MSSQL) | READ COMMITTED | Lock 기반(기본), 옵션으로 Snapshot Read 가능 |
| SQLite | SERIALIZABLE | 단일 writer 구조라 직렬화 비용이 낮음 |
드디어 격리성 파트가 종료하였습니다. 각 DB가 기본값을 왜 이렇게 지정했고 어떻게 구현했는지도 나중에 공부하면 재미있을거 같습니다. 지금은 글의 의도와 거리가 멀어 생략합니다.
Durability
이번 파트는 지속성입니다. 이 특성은 "커밋된 데이터는 어떤 장애가 와도 절대 사라지지 않는다"는 것을 보장합니다.
즉, 트랜잭션이 COMMIT 되는 순간부터, 그 데이터는 디스크에 영구히 저장되어야 하며 절대 유실되어서는 안 된다는 거죠.
여기서 포인트는 커밋된 데이터는 절대 롤백될 수 없다 입니다.
그렇다면 DB는 이 원칙을 어떻게 지킬 수 있을까요?
DB는 이 원칙을 지키기 위해 WAL(Write-Ahead Logging)방식을 사용합니다.
실제 데이터 파일을 수정하기 전에, 먼저 Redo Log에 "어떤 데이터를 어떻게 변경했는지"를 기록하고, 이 로그를 디스크에 flush한 뒤에야 COMMIT을 완료합니다. 만약 COMMIT 이후 서버가 죽더라도, 재기동 시 DB는 Redo Log를 다시 읽어 데이터 파일에 재적용(replay)함으로써 장애 직전 상태까지 복구할 수 있습니다. 이 때문에 "커밋된 트랜잭션은 절대 롤백될 수 없고, 장애로도 지워지지 않는다"는 지속성의 요구사항을 만족할 수 있게 됩니다.
이것을 정리하면 다음과 같은 과정을 거친다고 합니다.
- 트랜잭션 변경 내용을 REDO LOG에 먼저 기록한다 (디스크 flush).
- 데이터 자체는 버퍼 풀(메모리)에서 수정하고, 디스크는 나중에 반영한다.
- DB가 죽으면 REDO LOG를 읽어서 데이터 파일을 복구한다.
- 정상 상황에서는 거의 모든 읽기/쓰기를 메모리(버퍼 풀)에서 처리한다.
REDO LOG를 활용하면, 백업도 가능은 할거 같다는 생각이 문득 드는군요. 확인해보니 가능은 한데 REDO LOG는 차이를 기록해 놓은 로그파일이 때문에 직접하는것은 쉽지 않는다고 합니다.
정리하면,
- Atomicity = 묶음 실행
- Isolation = 동시성 보장
- Durability = 영속성
- Consistency = 정상 상태(무결성 규칙) 유지
그런데 한가지 의문점이 있습니다. 어째서 UNDO나 REDO같은 정보들은 전부 LOG일까요? 그 이유는 LOG가 최소비용으로 압축할 수 있기 때문입니다. 또한 바이너리 전용 포맷을 가지고 있어서 기계와 매우 친숙합니다. 그래서 차이만 기록해 놓는다고 합니다.
ACID를 학습하면서 격리성을 공부했었는데요. 격리성을 지키는 방법중에서 Lock과 MVCC가 있다고 학습했습니다. 본격적으로 LOCK과 MVCC에 대해 학습해 봅시다.
LOCK
락(Lock)은 트랜잭션의 격리성을 강제하기 위해 DB가 특정 자원을 독점하는 메커니즘입니다.
일반적으로 "무언가를 잠근다"고 표현하지만, DB가 잠그는 대상은 추상적인 개념이 아닙니다.
트랜잭션이 어떤 데이터를 읽거나 쓰는 순간, DB는 그 데이터가 다른 트랜잭션에 의해 동시에 변경되어 정합성이 깨지는 상황을 원천 차단하기 위해 락을 설정합니다.
여기서 자연스럽게 의문이 하나 생깁니다. DB는 실제로 무엇을 잠글까요? Row? Index? Page? 범위(Gap)? 테이블 전체?
정답은 상황에 따라 모두 가능하다입니다.
DB의 락은 단순히 "데이터를 잠근다" 수준이 아니라, 정합성을 보장할 수 있는 가장 작은 단위의 자원을 점유하는 방식으로 동작합니다.
핵심은 단 하나입니다. 특정 자원에 락이 걸리는 순간, 다른 트랜잭션은 해당 자원에 대한 읽기 또는 쓰기를 제한 받습니다.
이 기본 원칙 위에서 공유 락, 배타 락, 레코드 락, 갭 락, 넥스트키 락 같은 정교한 락 전략이 동작하게 됩니다.
본격적으로 하나씩 알아봅시다. 추가적으로 왜 필요한지 알아보고 실습까지 진행해봅시다.
공유 락
먼저 공유 락입니다. 공유락은 S-Lock이라고 불립니다. 이름만 들어보면 공유를 해서 뭐 어떻게 하는 거 같긴한데
사실은 읽기 전용 락입니다. 읽기 작업을 읽는 동안, 그 데이터를 다른 트랜잭션이 수정하지 못하도록 막는 락입니다.
즉, 여러 트랜잭션이 동시에 읽는 것은 허용하되, 읽는 도중에 수정은 절대로 허용하지 않습니다.
또한, 여러 트랜잭션이 동시에 "공유해서" 읽을 수 있기 때문에 공유락입니다.
왜 공유락이 필요할까요?
트랜잭션 A가 어떤 값을 읽고 있는 동안 트랜잭션 B가 그 값을 UPDATE가 가능하다면, 읽기-쓰기 충돌(Read-Write Conflict)이 발생하고 정합성이 깨진다. RDM는 이런 상황을 절대로 용납할 수 없다고 합니다. 그래서 읽는 동안 다른 트랜잭션의 변경을 막기 위해 공유 락을 걸게 됩니다.
공유락이 어디에 락을 걸까요?
현대적인 RDM는 행이 아닌 인덱스에 락을 건다고 합니다. 그렇게 거는 이유가 무엇일까요?
DB는 Row를 직접 찾을 수 없고, 인덱스를 통해서만 Row의 위치를 알 수 있기 때문입니다. 즉, 행을 잠그려면 먼저 인덱스 엔트리를 잠가야 합니다.(어차피 행에도 인덱스가 돌고 있음)
정확히 말하면, 행의 주소값을 이용하여 락을 건다고 생각하시면 될거 같습니다.
그렇다면 어떻게 사용할 수 있을까요?
세션1 - 공유락 걸기
START TRANSACTION;
SELECT *
FROM users
WHERE id = 1
LOCK IN SHARE MODE;
공유락은 위에서 확인했듯이 다른 트랜잭션에서 조회는 가능하지만 수정은 불가능하다고 했습니다.
과연 그럴까요?
세션2 - UPDATE 시도 (막혀야 정상)
START TRANSACTION;
UPDATE users
SET name = 'X'
WHERE id = 1;

세션 1 — COMMIT
COMMIT;

커밋을 때리게 되니 체크 표시가 완료처리로 바뀌었습니다.

정상적으로 변경이 되었다는것을 확인 할 수 있었습니다.
배타 락
그렇다면, 베타락은 어떤 락을 말하는 걸까요? 공유락과 달리, 베타락은 쓰기뿐 아니라 읽기까지 모두 차단하는 가장 강력한 락입니다.
이름에서 드러나듯이, 베타락은 해당 자원을 완전히 독점(Exclusive)을 하게됩니다.

쉽게 말해, 독불장군처럼 자원을 혼자 점령하고 누구도 끼어들지 못하게 막는 파렴치한 놈입니다.
그래서 베타락은 락 중에서도 유독 강하고, 어떤 락과도 공존하지 못합니다.
그렇다면 베타락은 언제 필요할까요?
베타락은 "해당 자원에 대해 경쟁을 절대로 허용하면 안 되는 순간"에 사용하는 강력한 락입니다.
즉, 읽기조차 허용하면 안 되는 비즈니스 상황에서 사용됩니다.
대표적으로는 다음과 같은 경우입니다.
- 금융 시스템에서 특정 계좌 잔고를 갱신할 때
- 동일 자원에 대해 동시 쓰기가 발생하면 안 되는 경우
- 변경 중인 데이터를 노출하면 절대로 안 되는 민감 연산
하지만 이런 강력한 락은 현대 서비스에서는 거의 사용하지 않습니다.
왜일까요?
그 이유는 단순합니다.
베타락은 성능을 폭발적으로 떨어뜨리는 락이기 때문입니다.
베타락이 걸린 순간부터 그 행은 사실상 "전 우주에서 한 트랜잭션만 접근 가능한 독점 상태"가 되며, 이는 고트래픽 환경에서는 거의 자살행위입니다.
그렇다면 이렇게 강력한 락을 쓰지 않으면 Lost Update가 발생할 수도 있는데, 실제로는 왜 요즘 시스템에서 Lost Update 문제가 잘 발생하지 않을까요?
답은 MVCC의 등장입니다. (추후 설명 예정입니다.)
결과적으로:
- 강력 락을 개발자가 직접 걸 필요가 줄어들었고
- 베타락 없이도 Lost Update 문제를 충분히 예방할 수 있게 되었으며
- 서비스 성능과 확장성 측면에서도 훨씬 유리
하기 때문에, 베타락은 "정말 불가피한 극단적 상황"이 아닌 이상 거의 사용되지 않습니다.
그래도 실습은 해봐야겠죠??ㅎ
세션1 - 베타락 획득
START TRANSACTION;
SELECT *
FROM users
WHERE id = 1
FOR UPDATE;
세션2 - 수정 요청
START TRANSACTION;
UPDATE users
SET name = 'X'
WHERE id = 1;

대기 상태가 계속 지속이 되어진다는것을 알 수 있습니다. 그렇다면 조회는 어떻게 되어질까요?
세션2 - 조회 요청
SELECT *
FROM users
WHERE id = 1
FOR UPDATE;

그냥 SELECT하는 경우에는 스냅샷 읽기라서 실제 바뀌는 것은 아닙니다.
commit또는 rollback을 통해 즉시 해제가 가능합니다.
갭 락
MySQL은 팬텀 리드 상황을 갭락(Gap Lock)이라는 메커니즘을 통해 차단한다고 하였습니다.
그렇다면 갭락은 무엇일까요?
이름만 보면 "행과 행 사이의 틈(Gap)을 잠근다"라고 하는데, 직관적으로는 행 사이에 틈이라는 것이 존재하지 않는 것처럼 느껴집니다. 하지만 실제로는 존재합니다.
DB는 데이터를 저장할 때 레코드들이 정렬된 인덱스 구조(B+Tree) 안에 배치되는데, 이 인덱스 노드들은 각각 다음 레코드가 들어올 수 있는 위치(=Gap) 를 갖고 있습니다.
즉, 갭락은 특정 "행(Row)" 자체를 잠그는 것이 아니라,
그 행 앞뒤로 새로운 레코드가 삽입될 수 있는 공간(=Gap)을 잠그는 락입니다.
갭락의 본질은 단 하나입니다.
동일 조건으로 두 번 SELECT했을 때,
두 번째 SELECT에서 새로운 행이 끼어드는 것을 원천적으로 차단하는 것.
그래서 MySQL은 REPEATABLE READ에서 팬텀 리드를 막기 위해
실제로 존재하지 않는 레코드 사이의 "공간"까지 잠가버리는 방식을 사용합니다.
| 개념 | 의미 | 누가 사용하는가 |
| Row(행) | 테이블의 논리적 데이터 | 개발자, SQL 관점 |
| Record(레코드) | Row를 실제 저장한 물리적 구조체 | DB 엔진 내부, 락/인덱스/MVCC |
락을 걸리는데도 체감하지 못하는 이유는
일반적인 SELECT에는 락은 걸리지 않는다는것이 첫번째 이유고
두번째 이유는 MVCC 스냅샷을 읽기 때문에 잘 보이지 않는다고 합니다.
갭락이 데드락을 유발하는 이유
갭락은 ‘존재하지 않는 행’까지 잠가 락 범위를 비정상적으로 넓히기 때문에 다른 트랜잭션의 INSERT·UPDATE와 충돌하고,
서로가 서로의 gap을 기다리는 구조가 매우 쉽게 만들어진다.
즉,
- A가 특정 범위의 gap을 잡고
- B가 그 gap에 INSERT하려 하고
- 동시에 A도 B가 잡은 다른 레코드를 기다리면
→ 둘 다 서로를 기다리는 전형적인 데드락 패턴이 생성됩니다.
갭락 기반 데드락은 실무에서 가장 난이도 높은 데드락 유형으로 평가된다고 합니다.
MVCC
이제 마지막 MVCC를 알아보자. MVCC도 일부 RDB에서 팬텀리드를 방지하기 위해 사용되어지는 기술입니다. 그러면 MVCC는 과연 무엇일까요? Multi-Version Concurrency Control의 약자로 이름부터 동시성을 제어한다고 나와있습니다.
일부 RDB에서는 팬텀 리드를 막기 위해 사용된다고 하지만, 정확히 말하면 MVCC의 본질은 읽기와 쓰기를 분리하여 락 없이도 일관된 데이터를 읽게 만드는 것입니다.
그래서 MVCC가 존재하는 이유를 단순화하면 아래 한 문장으로 정리할 수 있습니다.:
읽기는 과거 스냅샷을 보고, 쓰기는 새로운 버전을 만든다. 둘을 절대로 충돌시키지 않는다.
또 한 가지 중요한 점은, 락이 필요한 SELECT (FOR UPDATE, LOCK IN SHARE MODE)는 스냅샷을 사용하지 않고 현재 버전을 읽습니다. 즉, 모든 SELECT가 스냅샷을 보는 것은 아니며, 일반 SELECT만 과거 스냅샷을 기반으로 동작한다.
이정도 학습하면 RDB에 대한 전반적인 학습은 종료한거 같습니다.
이제 Nosql에 대해 학습해봅시다.
Nosql
Nosql은 전통적인 RDB가 갖는 구조적 한계를 보완하기 위해 등장한 저장 기술입니다.
RDB가 강력한 정합성과 트랜잭션 처리 능력을 제공하는 대신, 확장성/유연성/대량 처리 측면에서 구조적으로 불리한 상황이 생기기 때문이죠.
대표적인 문제는 다음과 같습니다.
- 수평 확장이 어렵다(Scale-Out 한계)
- 스키마가 고정되어 변화에 취약하다
- 대량 쓰기·대량 읽기에서 병목이 쉽게 발생한다
- 초저지연(µs 수준) 요구에서는 RDB 구조가 너무 무겁다
이 때문에 실무에서는 다음과 같은 요구가 있을 때 RDB 대신 NoSQL이 선택됩니다.
✔ 엄청나게 빠른 조회가 필요할 때 → Redis, Aerospike
✔ 반정형/비정형 데이터를 저장해야 할 때 → MongoDB
✔ 수평 확장을 전제로 한 massive write 환경 → Cassandra
✔ 이벤트 로그/타임라인 같은 append-only 데이터 → DynamoDB
✔ 전세계 리전에 걸친 분산 저장이 기본 스펙일 때 → DynamoDB, Couchbase
즉, NoSQL은 "RDB를 대체하는 기술"이 아니라 특정 요구사항에서 RDB를 보조하거나 극단적 트래픽을 처리하기 위해 등장한 선택지입니다.
모든 NoSQL을 깊게 학습할 필요는 없습니다.
하지만 최소한 아래 두 가지는 반드시 판단할 수 있어야 합니다:
- 이 문제는 RDB로 해결 가능한가? 가능하면 RDB가 기본값.
- RDB가 병목이 되는 지점이 무엇인가? 그 병목을 해소하는 타입의 NoSQL을 골라야 한다.
이 관점만 잡아도 실무에서 NoSQL을 "언제, 왜" 선택하는지 정확히 판단할 수 있습니다.
CAP 이론
CAP이론은 분산 시스템에서 동시에 3개는 만족할 수 없다는 이론입니다. Nosql은 RDB와 달리 특수한 경우에 경우에 사용이 되어지는데 주로 분산 환경에서 사용이 되어집니다. Nosql은 RDB와 달리 종류도 무수히 많습니다. 그렇기 때문에 각 NoSQL 자체를 깊게 파는 것보다, 어떤 상황에서 어떤 특성을 가진 저장소를 선택해야 하는지를 판단하는 기준(CAP)이 훨씬 더 실무적이고 효율적입니다.
- C – Consistency(일관성)
모든 노드가 같은 데이터를 즉시 보여준다.
즉, "어디서 읽어도 같은 값". - A – Availability(가용성)
일부 노드가 죽어도 항상 응답한다.
즉, "읽기/쓰기가 언제나 가능". - P – Partition Tolerance(분할 내성)
네트워크가 쪼개져도(Partition) 시스템이 살아남는다.
즉, "노드 간 통신 실패 상황에서도 계속 동작".
일단 P는 분산환경에서는 무조건 가져가야 합니다. 그렇기 때문에 C또는 A를 버린다고 생각하시면 됩니다.
즉, CP혹은 AP만 존재한다고 생각해도 무방하다 생각합니다.
그렇다면 왜 CAP 모두 가져갈 수 없을까요?
그 이유는 CAP의 본질은 네트워크 분리(Partition)가 발생했을 때
→ Consistency(정합성) 과 Availability(가용성) 은 물리적으로 동시에 충족할 수 없다는 것입니다.
그 이유는
두 노드가 서로 통신할 수 없는 상황에서, 요청에 대해 '즉시 일관된 값'을 보장하려면
최소 한 쪽 노드는 응답을 포기해야 하기 때문이다.
즉, 네트워크가 끊긴 순간 시스템은 선택을 강요당한답니다.
CAP 이론은 네트워크가 분리되었을 때(Partition) 발생하는 극한 상황에 대한 이론입니다.
그러나 현실의 NoSQL 시스템은 항상 P 상황이 발생하는 것은 아니기 때문에,
C/A 중 하나를 항상 완전히 포기하는 방식으로 설계하지 않습니다.
CAP은 너무 단순화되어 있습니다: "장애(P) 상황일 때만 고려"가 되죠.
하지만 실무에서는 장애보다 평상시 성능(지연)과 일관성 충돌이 훨씬 더 자주 발생합니다.
→ PACELC 이론은 현실적인 분산 시스템 선택 기준을 제공한다.
- 지연이 더 중요한지
- 일관성이 더 중요한지
- 장애 시에 어떤 것을 우선할지
이 기준으로 NoSQL을 선택해야 한다.
간단하게 각 Nosql은 어디에 속하는지 확인하고 마치겠습니다.
| NoSQL 제품 | CAP 분류 | PACELC 분류 | 특징 요약 |
| Cassandra | AP | PA / EL | 가용성·지연 최우선. Eventually Consistent. 분산 확장 최강. |
| DynamoDB (AWS) | AP | PA / EL | Dynamo 설계 그대로. 고가용성 + 낮은 지연. 강한 일관성 옵션 제공하지만 기본은 EL. |
| Riak | AP | PA / EL | Dynamo 계열. 고가용성 중심. |
| MongoDB | CP (ReplicaSet 기본) | PC / EC | 기본적으로 강한 일관성. Secondary 읽기 옵션 사용 시 AP 비슷하게 전환 가능. |
| HBase | CP | PC / EC | Master 기반. 강한 일관성, 지연은 희생. 대규모 분석 계열에서 사용. |
| Redis (Cluster) | AP | PA / EL | 빠른 응답이 목표. 일관성보다 성능·가용성 우선. |
| Redis (Standalone) | CAP 모델에 속하지 않음 | PACELC 적용 X | 단일 노드이므로 CAP/PACELC 의미 없음. |
| Elasticsearch | AP | PA / EL | 검색 엔진 특성상 지연과 가용성 우선. 일관성은 Eventually. |
| Couchbase | AP | PA / EL | Eventually Consistent 기반. 매우 빠른 응답 속도. |
| Spanner (Google) | CP | PC / EC | TrueTime 기반 글로벌 일관성 제공. 지연 비용 감수. |
마무리
간략하게 RDB와 Nosql을 비교하는 시간을 가졌는데요. 실제로 작성해보니 비교보다는 각 특징에 대해 작성한 느낌이 듭니다.
격리성을 학습할때 생각보다 많은 내용을 학습을 했던거 같구 무엇보다 RDB도 메모리를 사용한다는 사실은 새롭게 안 사실이었습니다.
다음 파트는 Nosql의 확장에 대해 알아보고 RDB도 가능한지 학습해보는 시간을 가져보겠습니다.
'개발' 카테고리의 다른 글
| 스프링 시큐리티에는 보안 6요소를 어떻게 사용할까? (0) | 2025.11.12 |
|---|