이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › DB
저장과 지속성
커밋한 데이터는 서버가 꺼져도 남는가
DB는 데이터를 디스크의 파일에 저장하는 프로그램입니다. 자바 프로그램의 변수는 프로그램이 끝나면 사라지지만, COMMIT까지 끝낸 데이터는 DB 프로그램이 갑자기 꺼졌다가 다시 켜져도 남아 있어야 합니다. 이 성질을 지속성(durability)이라고 합니다. 코드에서 이 성질을 이루는 것은 두 가지입니다(DB에 따라 COMMIT한 내용을 바로 디스크에 쓰게 하는 설정도 더 필요합니다). 하나는 COMMIT으로, 묶음 안의 변경을 남기겠다고 확정하는 문장입니다. 다른 하나는 데이터를 두는 곳으로, 연결 주소가 jdbc:h2:file: 글자로 시작하면 디스크의 파일에, jdbc:h2:mem: 글자로 시작하면 메모리에만 둡니다. 이 실습은 COMMIT 줄을 지우거나 데이터를 두는 곳을 바꿔 가며 같은 INSERT를 실행하고, DB 프로그램을 정리 없이 끝낸 다음 다시 켜서 memo 테이블을 읽어 봅니다. (이 실습의 H2는 자바 프로그램 안에서 도는 DB여서 "DB 프로그램이 꺼진다"는 것은 그 자바 프로그램이 끝나는 것입니다. MySQL 같은 DB는 따로 도는 프로그램이라는 점만 다릅니다. DB가 이 약속을 어떻게 지키는지(로그와 버퍼)는 세부 동작 실습에서 다룹니다.)
실행으로 대조 · H2 2.4.240에서 네 가지 조합을 정리 없이 프로세스를 끝낸 뒤 다시 열어 실행해 대조, 지속성의 규칙은 MySQL과 H2 문서와 대조(전원 차단은 실행하지 않음)
이 실습에 나오는 개념
- 지속성(Durability)
- 지속성은 COMMIT이 성공한 트랜잭션의 변경이 전원 장애나 시스템 충돌이 나도 사라지지 않는 성질이며, ACID의 D에 해당한다. MySQL 문서는 지속성이 보통 디스크 저장소에 쓰는 일로 이루어지고, 쓰는 도중의 장애에 대비해 여분의 기록을 둔다고 설명한다. MySQL에서는 이것이 소프트웨어 설정(innodb_flush_log_at_trx_commit 등)과 하드웨어(저장 장치의 쓰기 버퍼, 운영체제의 fsync() 지원 등)가 함께 정하는 성질이어서, 설정에 따라 보장 정도가 달라진다. COMMIT하기 전의 변경은 이 약속의 대상이 아니다.
- 파일 DB와 메모리 DB
- 파일 DB는 데이터를 디스크의 파일에 저장하는 DB이고, 메모리 DB는 컴퓨터의 메모리에만 두는 DB이다. H2에서는 연결 주소로 고른다. jdbc:h2:file:경로로 시작하면(file: 은 생략할 수 있다) 파일에 저장하고, jdbc:h2:mem:이름으로 시작하면 메모리에 둔다. 파일 DB는 연결을 닫아도 남지만, H2 문서에 따르면 메모리 DB는 마지막 연결을 닫으면 내용이 사라지고, DB_CLOSE_DELAY=-1을 붙여도 프로그램(가상 머신)이 살아 있는 동안만 남는다. MySQL 용어집은 디스크에 저장하는 DB(disk-based)의 반대를 인메모리 DB라고 부른다.
- 커밋과 롤백
- 커밋(commit)은 트랜잭션 안에서 한 데이터 변경을 모두 DB에 확정해 반영하는 것이고, 롤백(rollback)은 그 변경을 모두 취소해 트랜잭션을 시작하기 전 상태로 되돌리는 것이다. 트랜잭션은 끝날 때 둘 중 하나로 끝나며, 이미 커밋한 변경은 롤백으로 되돌릴 수 없다. 스프링에서 @Transactional이 붙은 메서드는 정상으로 끝나면 스프링이 커밋하고, 예외로 끝나면 롤백 규칙을 보고 롤백한다. 기본 규칙에서는 RuntimeException과 Error가 메서드 밖으로 던져지면 롤백하고 체크 예외는 롤백하지 않는다. 트랜잭션이 없으면 JDBC 연결의 기본 설정인 자동 커밋에 따라 저장 문장마다 곧바로 커밋되므로, 뒤에서 예외가 나도 앞의 저장은 롤백할 수 없다.
- H2의 쓰기 지연(WRITE_DELAY)
- WRITE_DELAY는 H2에서 COMMIT과 그 내용을 파일에 쓰는 일 사이에 둘 수 있는 최대 지연이며 밀리초로 정한다. H2 문서에 따르면 기본값은 500이고, SET WRITE_DELAY 숫자 명령이나 연결 주소 끝의 ;WRITE_DELAY=0 꼴로 바꿀 수 있으며 설정은 데이터베이스에 남는다. H2는 기본으로 COMMIT한 내용을 파일에 쓰는 일을 미루기 때문에, H2 문서는 전원이 나가면 1초 남짓 동안 COMMIT한 내용을 잃을 수 있다고 적고 있다. 하드 디스크가 쓰기 명령(fsync)을 지키지 않는 경우도 많아, 어떤 설정이든 완전한 지속성은 장치에 달려 있다.
이렇게 바꿔 보세요
- 처음 설정으로 — 파일에 저장하는 DB에서 COMMIT까지 한 행은 DB 프로그램이 갑자기 꺼졌다 켜진 뒤에도 남아 있습니다.
- COMMIT을 지우면 — COMMIT하지 않은 행은 다시 켜면 없습니다. 테이블은 남아 있습니다.
- 메모리에만 두는 DB면 — COMMIT까지 했는데도 다시 켜면 테이블부터 없습니다.
- COMMIT도 없고 메모리에만 두면 — COMMIT이 있을 때와 같은 결과입니다. 이 DB는 COMMIT과 상관없이 통째로 사라집니다.
처음 설정으로 실행하면
- DB에 연결합니다. 주소의 jdbc:h2:file:./shop은 DB의 내용을 ./shop이라는 이름의 파일(H2는 shop.mv.db라는 파일로 만듭니다)에 저장하겠다는 뜻입니다. 끝에 붙인 WRITE_DELAY=0 설정은 COMMIT한 내용을 미루지 않고 바로 파일에 쓰라는 뜻입니다. H2는 기본 설정에서는 COMMIT 뒤 파일에 쓰는 일을 최대 0.5초 미루기 때문에(H2 문서), 갑자기 꺼지는 일을 실험하려고 바로 쓰게 했습니다.
- CREATE TABLE memo: DB가 id와 text 두 칸이 있는 memo 테이블을 만듭니다. 아직 BEGIN이 없으므로 이 문장은 실행되자마자 확정됩니다(자동 커밋). 확정된 내용은 디스크의 DB 파일에 기록됩니다.
- BEGIN: DB가 지금부터 COMMIT을 만날 때까지의 문장을 한 묶음(트랜잭션)으로 다루기 시작합니다. 자동 커밋이 꺼지므로 이 묶음 안의 변경은 COMMIT을 만나기 전까지 확정되지 않습니다.
- INSERT INTO memo: DB가 (1, 'hello') 행을 memo 테이블에 넣습니다. 묶음 안의 변경이라 아직 확정되지 않았습니다. 이 연결에서 memo 테이블을 읽으면 1행이 보입니다.
- COMMIT: DB가 묶음을 확정합니다. WRITE_DELAY=0 설정이 있으므로 확정한 변경을 곧바로 DB 파일에 씁니다. 이제 행 1개가 프로그램의 메모리에도, 디스크의 파일에도 있습니다. 자동 커밋은 다시 켜집니다.
- 이 코드에는 없는 일입니다. DB 프로그램이 갑자기 꺼집니다. 연결을 닫지도, 끝내는 절차를 거치지도 않고 프로그램이 곧바로 끝납니다(실험에서는 자바의 Runtime.halt 메서드로 끝냈습니다). 프로그램의 메모리에 있던 memo 테이블과 행은 모두 사라집니다. 디스크의 DB 파일은 그대로 남습니다. (전원이 나가는 경우가 아니라 프로그램만 끝나는 경우를 실험했습니다.)
- DB 프로그램이 다시 켜지고, 같은 주소로 연결합니다. ./shop 파일이 디스크에 남아 있으므로 DB는 그 파일을 열어 내용을 읽을 수 있게 됩니다. 꺼질 때 연결을 닫지 않았는데도 파일은 오류 없이 열렸습니다.
- SELECT * FROM memo: 결과는 1행 (1, hello)입니다. COMMIT으로 확정한 행이 디스크의 파일에 있었기 때문에, DB 프로그램이 갑자기 꺼졌다 켜진 뒤에도 남아 있습니다.
실행 결과
- COMMIT까지 끝낸 행은 DB 프로그램이 갑자기 꺼졌다가 다시 켜진 뒤에도 남아 있었습니다. 행이 디스크의 파일에 있었기 때문입니다. 프로그램이 끝나면 사라지는 메모리와 달리 디스크의 파일은 프로그램이 꺼져도 남고, COMMIT은 변경을 그 파일에 남기겠다고 확정하는 문장입니다.
- COMMIT한 변경이 꺼진 뒤에도 남는 이 성질을 지속성(durability)이라고 합니다. 이 실습에서는 COMMIT과 함께 파일에 쓰도록 주소에 WRITE_DELAY=0 설정을 붙였습니다. 이것을 빼고 H2 기본 설정으로 COMMIT 직후에 바로 끄면 다섯 번 모두 COMMIT한 행이 남지 않았고, 1.5초 기다렸다가 끄면 두 번 모두 남았습니다. DB가 이 약속을 지키는 방법(로그와 버퍼)은 세부 동작 실습에서 다룹니다.
코드
-- 파일에 저장하는 DB(jdbc:h2:file:./shop;WRITE_DELAY=0)에 연결해서
CREATE TABLE memo (id INT PRIMARY KEY, text VARCHAR(20));
-- 시작: 여기서부터를 한 묶음으로 다룬다
BEGIN;
INSERT INTO memo VALUES (1, 'hello');
-- 확정: 여기까지의 변경을 남기겠다고 DB에 알린다
COMMIT;
-- DB 프로그램이 갑자기 꺼졌다가 다시 켜진 뒤, 같은 주소로 연결해서
SELECT * FROM memo;
이 모형의 전제
H2 2.4.240(JDK 21.0.12.1)에서 실행해 대조했고, MySQL이 아니라 H2로 대조했습니다. 네 가지 조합을 모두, 화면의 SQL 전체(주석 줄 포함)를 글자 그대로 실행했습니다. 첫 줄 주석에 적힌 주소로 연결했고, 주석 줄은 뒤따르는 문장과 함께 DB에 보냈습니다. 먼저 새 자바 프로세스가 "DB 프로그램이 갑자기 꺼졌다가" 줄 앞까지의 문장을 한 문장씩 실행한 뒤 연결을 닫지도 종료 절차를 돌리지도 않고 Runtime.halt 메서드로 프로세스를 바로 끝냈고(종료 코드 7), 다시 새 자바 프로세스가 같은 주소로 연결해 그 줄 뒤의 SELECT를 실행했습니다. 파일 DB의 주소 ./shop은 조합마다 따로 만든 폴더 안의 파일입니다. 대조한 것은 결과 표의 세 항목, 곧 다시 켠 뒤 읽은 행(행 수와 값, 또는 오류의 종류), COMMIT 줄이 있는지, 폴더에 만들어진 파일입니다. 파일 DB는 꺼진 뒤 정리 없이 다시 열렸고, 네 조합 가운데 행이 남은 것은 파일 DB에서 COMMIT한 조합 하나뿐이었습니다. 메모리 DB는 폴더에 파일을 만들지 않았고, 다시 켠 연결에서 memo 테이블이 없다는 오류(H2 오류 코드 42104, 메시지에 this database is empty)가 났습니다. 오류의 원문은 Probe 출력에 있고 화면에는 종류만 한국어로 옮겼습니다. 단계 문장이 기대는 사실로 BEGIN 뒤 자동 커밋이 꺼지고 COMMIT 뒤 다시 켜지는 것, 꺼지기 직전 같은 연결에서 읽으면 1행이 보이는 것, CREATE TABLE이 BEGIN 앞에서 확정되어 COMMIT 없이 꺼진 뒤에도 테이블이 남는 것도 실행으로 확인했습니다. 화면의 코드에는 이 확인용 조회가 없고, memo 테이블의 열 이름 text는 H2에서 쓸 수 있었습니다. 중요한 점: 이 실습의 파일 DB 주소에는 ;WRITE_DELAY=0을 붙였습니다. H2 문서(commands 페이지의 SET WRITE_DELAY)는 COMMIT과 로그를 파일에 쓰는 사이의 최대 지연을 밀리초로 정하며 기본값이 500이라고 하고, 데이터베이스 주소에 ;WRITE_DELAY=0으로 붙일 수 있다고 합니다. 이 설정을 뺀 H2 기본 설정에서 파일 DB로 COMMIT 직후에 바로 끝내면, 다섯 번 모두 다시 켠 뒤 memo 테이블도 행도 없었고(DB 파일은 있으나 비어 있었음), COMMIT 뒤 1.5초를 기다렸다가 끝내면 두 번 모두 행이 남았습니다. 즉 이 실습의 "COMMIT하면 남는다"는 WRITE_DELAY=0 설정에서 관찰한 결과입니다. H2 문서(advanced 페이지의 Durability Problems)도 H2가 기본으로 COMMIT한 내용을 파일에 쓰는 일을 미루고, 전원이 나가면 1초 남짓 COMMIT한 내용을 잃을 수 있으며, 하드 디스크가 fsync를 지키지 않는 경우가 많아 완전한 지속성은 어렵다고 설명합니다. 그리고 이 실습은 전원을 끊은 것이 아니라 프로세스만 끝낸 것이라서, 프로세스가 끝나도 운영체제가 받아 둔 파일 쓰기는 남기 때문에 전원이 나가는 경우의 지속성은 실행으로 확인하지 못했습니다. 메모리 DB가 마지막 연결을 닫으면(또는 프로그램이 끝나면) 사라진다는 것은 H2 문서(features 페이지의 In-Memory Databases)를 따랐고, 이 실습에서는 프로세스가 끝난 경우를 실행으로 확인했습니다. MySQL 8.4 문서의 "InnoDB and the ACID Model" 페이지는 지속성이 MySQL 소프트웨어와 하드웨어 설정이 함께 정하는 성질이라 하며 innodb_flush_log_at_trx_commit, sync_binlog, fsync() 지원, 저장 장치의 쓰기 버퍼 같은 것을 관련 요소로 꼽고, 용어집의 ACID 항목은 commit이 성공하면 그 변경이 전원 장애나 시스템 충돌에도 안전하며 지속성은 보통 디스크에 쓰는 것을 뜻한다고 적고 있습니다. 이 문서들은 직접 열어 확인했지만 MySQL에서 같은 결과가 나오는지는 실행하지 않았습니다. 이 실습은 지속성의 겉모습, 곧 COMMIT과 저장 장소에 따라 꺼졌다 켜진 뒤 데이터가 남는지만 다룹니다. 로그와 버퍼 풀이 이것을 어떻게 지키는지는 세부 동작 실습 "Undo와 Redo 로그"와 "버퍼 풀과 페이지"에서, 트랜잭션 묶음 자체는 "트랜잭션"에서 다룹니다.
앞 실습: 행 잠금 · 다음 실습: IP 주소와 포트