아티클 목록과 개수를 일관되게 유지하려면 동일한 node-postgres 클라이언트에서 두 읽기를 하나의 REPEATABLE READ READ ONLY 트랜잭션으로 실행하세요. 이 튜토리얼에서는 그 패턴과 Mac에서 실행할 수 있는 두 세션 확인 방법을 보여줍니다.
아티클 목록과 개수가 서로 다를 수 있는 이유는 무엇인가요?
데스크톱 스크립트가 craft 토픽에 게시된 아티클을 가져온 다음 일치하는 아티클의 개수를 센다고 가정해 보세요. 두 읽기 사이에 다른 세션이 아티클의 토픽을 변경하면, 반환된 목록에는 아티클이 두 개 있지만 개수는 하나라고 나올 수 있습니다.
PostgreSQL의 기본 격리 수준인 READ COMMITTED에서는 각 일반 SELECT이 해당 문장이 시작될 때의 스냅샷을 사용합니다. 따라서 하나의 트랜잭션 안에서도 연속된 읽기가 서로 다른 커밋 데이터를 볼 수 있습니다. PostgreSQL: Transaction Isolation.
이 작업에서 원하는 결과는 아티클 행을 일관된 하나의 관점에서 보는 것입니다. 이후의 수정 사항은 다음 실행에서 반영될 수 있습니다. PostgreSQL의 REPEATABLE READ는 트랜잭션의 첫 번째 트랜잭션 제어 이외 문장이 설정한 스냅샷을 연속된 읽기에서 유지합니다. BEGIN를 실행하는 것만으로는 해당 스냅샷이 설정되지 않습니다. PostgreSQL: Transaction Isolation.
구현에는 두 가지 요구 사항이 있습니다. 읽기 전에 격리 수준을 선택하고, 관련된 모든 쿼리를 같은 클라이언트에서 실행해야 합니다. node-postgres는 트랜잭션에 단일 클라이언트를 사용해야 한다고 명시하며, 해당 문장에 pool.query()을 사용하지 말라고 경고합니다. node-postgres: Transactions.
Mac에서 간단한 아티클 테스트 데이터를 준비하기
실행 중인 로컬 PostgreSQL 서버, psql 명령, 그리고 npm이 포함된 지원 대상 Node.js 설치 환경을 사용하세요. 아래 명령은 기존 로컬 연결에서 postgres 데이터베이스를 사용할 수 있다고 가정합니다. 작업 중인 데이터베이스의 이름이 다르면 모든 명령에서 일관되게 바꾸세요. PostgreSQL 설치와 연결 설정은 사전 조건입니다.
Terminal을 열고 프로젝트 폴더를 만드세요. 문서화된 node-postgres 패키지는 pg입니다. node-postgres: Welcome.
mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres
psql 프롬프트에서 다음 설정을 한 번 실행하세요. 합성 아티클 행만 저장하는 전용 테이블을 만듭니다. 명시적 식별자를 사용하므로 이후 실습도 쉽게 따라갈 수 있습니다.
CREATE TABLE public.article_snapshot_demo (
id integer PRIMARY KEY,
title text NOT NULL,
topic text NOT NULL,
published boolean NOT NULL
);
INSERT INTO public.article_snapshot_demo
(id, title, topic, published)
VALUES
(1, 'Paper Shapes', 'craft', true),
(2, 'Color Studies', 'craft', true),
(3, 'Desk Layouts', 'design', true);
테이블이 이미 있으면 자동으로 삭제하거나 교체하지 말고, 중단한 뒤 이전 테스트 데이터를 확인하세요. 예제는 정확히 이 세 개의 초기 행을 가정합니다. Node.js 스크립트를 실행하기 전에 psql을 \q 상태로 두세요.
하나의 대여한 클라이언트로 목록과 개수 읽기
다음 전체 프로그램을 프로젝트 폴더의 read-articles.mjs 파일로 저장하세요. new Pool()은 위에서 내보낸 PGDATABASE 값을 비롯해 문서화된 PostgreSQL 연결 환경 변수를 사용합니다. node-postgres: Connecting.
프로그램은 클라이언트 하나를 가져오고, 트랜잭션 특성을 지정한 상태로 트랜잭션을 시작한 다음 두 읽기를 수행하고, 조합한 결과를 반환하기 전에 커밋합니다. PostgreSQL은 BEGIN에서 이러한 특성을 직접 지원하며, 트랜잭션은 COMMIT 또는 ROLLBACK로 끝납니다. PostgreSQL: BEGIN.
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ connectionTimeoutMillis: 5000 });
pool.on('error', (error) => {
console.error('Idle database client error:', error.message);
process.exitCode = 1;
});
async function readArticles(topic) {
const client = await pool.connect();
let began = false;
let discardClient = false;
try {
await client.query(
'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
);
began = true;
const articles = await client.query(
`SELECT id, title, topic
FROM public.article_snapshot_demo
WHERE topic = $1 AND published = true
ORDER BY id`,
[topic]
);
const count = await client.query(
`SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = $1 AND published = true`,
[topic]
);
await client.query('COMMIT');
began = false;
return {
topic,
articles: articles.rows,
total: count.rows[0].total
};
} catch (error) {
if (began) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
discardClient = true;
throw new AggregateError(
[error, rollbackError],
'Article read failed; rollback also failed'
);
}
} else {
discardClient = true;
}
throw error;
} finally {
client.release(discardClient);
}
}
try {
const result = await readArticles('craft');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error(error);
process.exitCode = 1;
} finally {
await pool.end();
}
같은 Terminal 세션에서 프로그램을 실행하세요.
node read-articles.mjs
테스트 데이터를 변경하지 않았다면 예상 결과에는 아티클 1과 2이 포함되고, total 값은 문자열 "2"입니다. 개수는 텍스트로 명시적으로 반환되므로 이 예제에서는 JavaScript 숫자로 변환할 필요가 없습니다. 두 쿼리 모두 같은 토픽과 게시 필터를 사용합니다. 격리 수준으로 서로 다른 조건자를 바로잡을 수는 없습니다.
정리 작업도 실용적인 패턴의 일부입니다. 읽기에 실패하면 롤백을 수행하고, 롤백에도 실패하면 클라이언트를 폐기합니다. node-postgres는 대여한 클라이언트를 폐기할 때 client.release(true)을, 스크립트의 풀을 종료할 때 pool.end()을 사용하도록 문서화합니다. node-postgres: Pool API.
이 코드는 독립 실행형 스크립트이므로 한 번 실행한 후 풀을 종료합니다. 기존 애플리케이션에서는 애플리케이션의 풀 수명 주기를 유지하면서 트랜잭션 함수를 그 안에 배치하세요. 이 읽기에 참여하는 모든 도우미 함수에는 대여한 client을 전달해야 합니다. 몰래 pool.query()를 호출하는 도우미 함수는 그 경계를 깨뜨립니다. node-postgres: Transactions.
Terminal 세션 두 개로 스냅샷 확인하기
빠른 스크립트는 다른 세션이 무언가를 변경하기 전에 끝날 수 있습니다. 관련 시점을 명확히 드러내려면 아래 순서를 의도적으로 지켜 실습하세요. 이 단계들은 이미 만든 테이블에서 세션별로 실행하는 SQL이며, 각각 독립적인 설정 프로그램은 아닙니다. 예상 데이터베이스 결과는 문서에 설명된 격리 동작을 따른 것이며, 실제 실행한 PostgreSQL 테스트 결과로 제시하는 것은 아닙니다.
Terminal 창 두 개를 열고 각각에서 psql -d postgres를 실행하세요. 읽기 세션을 시작하기 전에 세션 B에서 합성 테스트 데이터의 토픽을 복원하세요. 이 문장이 완료될 때까지 기다리세요.
-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;
세션 A에서 읽기 트랜잭션을 시작하고 목록을 가져오세요. 쿼리가 완료된 뒤 잠시 멈추세요.
-- Session A: run these statements, then pause.
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT id, title
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true
ORDER BY id;
목록에는 Paper Shapes와 Color Studies가 포함되어야 합니다. 이제 세션 B로 전환해 다음 독립 실행 문장으로 토픽을 변경하고 커밋하세요. 명시적 트랜잭션 블록 밖에서는 PostgreSQL이 성공한 문장을 자동으로 커밋합니다. PostgreSQL: BEGIN.
-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;
세션 A로 돌아가 관련 읽기를 마무리하세요.
-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;
COMMIT;
예상 개수는 2이며 세션 A의 앞선 목록과 일치합니다. 이후 Node.js 스크립트를 새로 실행하면 Paper Shapes만 반환되고 총 개수는 "1"이어야 합니다. 이전 결과와 새 결과는 각각 일관된 관점을 나타냅니다.
통제된 비교를 위해 세션 B의 초기 복원 단계를 포함해 실습 전체를 반복하되, 세션 A에서는 BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;로 시작하세요. 동일한 대기와 업데이트 순서를 유지하세요. 예상되는 첫 번째 목록에는 아티클 두 개가 포함되지만, 이후 개수는 1입니다. 이 비교에서는 트랜잭션 설정만 변경 조건으로 작용합니다.
관련 읽기에 적합한 경계 선택하기
예제를 적용할 때 다음 판단 기준을 사용하세요. 먼저 어떤 출력이 서로 일치해야 하는지 결정한 다음, 그 결과를 제공하는 가장 작은 데이터베이스 작업을 선택하세요.
| 읽기 작업 | 적합한 접근 방식 | 유지해야 할 조건 |
|---|---|---|
| 일반 쿼리 하나로 아티클 목록 하나 반환 | READ COMMITTED로 쿼리 하나 실행 가능 |
이후 쿼리에서 같은 스냅샷을 공유할 필요가 없습니다. |
| 목록과 별도 쿼리로 가져온 개수 반환 | 하나의 REPEATABLE READ READ ONLY 트랜잭션 |
클라이언트 하나를 사용하고 필터를 일치시키세요. |
| 일관된 결과를 위해 여러 배치를 가져오기 | 같은 트랜잭션 안에서 읽기 유지 | 새 트랜잭션은 새로운 관점을 설정합니다. |
| 별도의 데이터베이스 세션에서 읽기를 조정하기 | 스냅샷 명시적 내보내기 및 가져오기 | 별도 트랜잭션은 자동으로 스냅샷을 공유하지 않습니다. |
여러 세션의 경우 PostgreSQL은 pg_export_snapshot()과 SET TRANSACTION SNAPSHOT을 제공합니다. 내보내는 세션은 다른 트랜잭션이 스냅샷을 가져올 때까지 열린 상태로 유지해야 합니다. 가져오는 트랜잭션은 REPEATABLE READ 또는 SERIALIZABLE을 사용해야 하며, 첫 쿼리나 데이터 수정 문장을 실행하기 전에 스냅샷을 가져와야 합니다. Serializable 가져오기에 관한 추가 제한 사항은 명령어 참조 문서에 나와 있습니다. PostgreSQL: Snapshot Synchronization Functions, PostgreSQL: SET TRANSACTION.
이 데스크톱의 목록 및 개수 작업에서는 클라이언트 하나를 유지하면 이러한 추가 조정이 필요하지 않습니다. 사람이 다음 작업을 선택하기를 기다리기 전에 데이터베이스 읽기를 마치고 트랜잭션을 닫으세요.
이 격리 수준으로 해결되지 않는 문제는 무엇인가요?
PostgreSQL의 REPEATABLE READ는 팬텀 읽기를 방지하지만 직렬화 이상 현상은 여전히 발생할 수 있습니다. 따라서 안정적인 스냅샷이 있다고 해서 동시 워크플로가 하나씩 실행된 것처럼 동작했다는 뜻은 아닙니다. 또한 시퀀스 값은 고정되지 않습니다. 시퀀스는 트랜잭션과 관련해 특별한 동작을 합니다. PostgreSQL: Transaction Isolation.
READ ONLY는 특정 임시 테이블 작업과 같은 문서화된 예외를 제외하고 일반 테이블의 변경을 허용하지 않습니다. 이 설정만으로 안정적인 스냅샷이 선택되지는 않습니다. 읽기 전에 두 특성을 모두 설정하세요. 트랜잭션의 첫 쿼리나 데이터 수정 문장이 실행된 뒤에는 PostgreSQL에서 격리 수준을 변경할 수 없습니다. PostgreSQL: SET TRANSACTION.
보고서에 더 강한 직렬화 가능 의미 체계가 필요하면 PostgreSQL은 SERIALIZABLE READ ONLY DEFERRABLE을 지원합니다. 스냅샷을 가져오는 동안 대기할 수 있으며, 이후에는 직렬화 실패로 인한 취소 없이 실행됩니다. 이러한 절충은 여기서 설명한 일반적인 아티클 읽기 요구 사항과는 별개의 문제입니다. PostgreSQL: SET TRANSACTION.
아티클 목록과 개수에서는 구현 경계를 명확히 유지하세요. 클라이언트 하나를 가져오고, 필요한 특성을 지정해 시작한 뒤 관련 결과를 모두 읽고 커밋한 다음 클라이언트를 반환합니다. 같은 필터를 재사용하고 트랜잭션이 완료된 후에만 조합한 결과를 공개하세요. 그러면 다음 데스크톱 작업에서 사용할 아티클 데이터를 일관된 하나의 세트로 얻을 수 있습니다.
출처
- Transaction Isolation — PostgreSQL Global Development Group.
- BEGIN — PostgreSQL Global Development Group.
- SET TRANSACTION — PostgreSQL Global Development Group.
- System Administration Functions: Snapshot Synchronization Functions — PostgreSQL Global Development Group.
- Transactions — node-postgres.
- Pool API — node-postgres.
- Connecting — node-postgres.
- Welcome — node-postgres.