ORM 오버헤드를 피하면서 node-postgres로 직접 데이터베이스 상호 작용하기
Ethan Miller
Product Engineer · Leapcell

소개
활기찬 JavaScript 백엔드 개발의 세계, 특히 Node.js에서는 데이터베이스와의 상호 작용이 기본적인 작업입니다. 오랫동안 객체-관계형 매퍼(ORM)는 객체 지향 인터페이스 뒤에 SQL을 추상화하여 데이터베이스 작업을 단순화하겠다고 약속하는 솔루션으로 사용되어 왔습니다. Sequelize 및 TypeORM과 같은 프레임워크는 훌륭한 이유로 매우 인기가 있습니다. 스키마 마이그레이션, 모델 연관 관계, 쿼리 빌더와 같은 강력한 기능을 제공합니다.
하지만 이러한 편리함에는 종종 대가가 따릅니다. ORM은 모든 프로젝트에 필요하지 않을 수 있는 복잡성, 상용구 및 성능 오버헤드 계층을 도입할 수 있습니다. 이는 중요한 질문을 제기합니다. ORM을 포기하고 데이터베이스와 직접 상호 작용하는 것이 더 나은 경우는 언제일까요? 이 문서에서는 PostgreSQL 사용자를 위한 매력적인 대안인 node-postgres(pg) 네이티브 라이브러리를 자세히 살펴보겠습니다. 일반적인 사용 사례의 경우 pg를 직접 사용하면 더 깨끗하고 성능이 뛰어나며 궁극적으로 더 유지 관리하기 쉬운 코드를 얻을 수 있으며, ORM은 종종 불필요한 추상화가 될 수 있다는 점을 살펴보겠습니다.
핵심 개념 이해
node-postgres의 실제 적용을 살펴보기 전에 이 논의에 필수적인 몇 가지 핵심 용어를 명확히 하겠습니다.
- SQL (Structured Query Language): 관계형 데이터베이스를 관리하고 조작하는 표준 언어입니다. 데이터베이스에 저장, 검색, 업데이트 또는 삭제할 데이터를 알려주는 방법입니다.
- 데이터베이스 드라이버/클라이언트: 애플리케이션이 특정 유형의 데이터베이스에 연결하고 상호 작용할 수 있도록 하는 소프트웨어 구성 요소입니다.
node-postgres(종종pg로 참조됨)는 Node.js의 공식 PostgreSQL 클라이언트입니다. PostgreSQL 서버로 SQL 쿼리를 보내고 결과를 받는 것을 용이하게 합니다. - ORM (Object-Relational Mapper): 객체 지향 프로그래밍 언어를 사용하여 호환되지 않는 유형 시스템 간의 데이터를 변환하는 프로그래밍 도구입니다. ORM은 본질적으로 데이터베이스 테이블을 클래스에, 테이블 행을 객체에 매핑하여 개발자가 원시 SQL을 작성하는 대신 선호하는 프로그래밍 언어의 객체를 사용하여 데이터베이스와 상호 작용할 수 있도록 합니다.
- 원시 SQL: ORM의 쿼리 빌더의 추상화 계층 없이 개발자가 직접 작성한 SQL 문입니다.
Node-Postgres만 있으면 되는 이유
node-postgres를 직접 사용하는 주요 이점은 제어 및 명확성입니다. 원시 SQL을 작성할 때 실행되는 쿼리에 대한 절대적인 제어권을 갖습니다. 이는 몇 가지 이점으로 이어집니다.
- 직접 통신: 데이터베이스의 네이티브 언어로 말하는 것입니다. ORM의 쿼리 생성기에서 발생하는 잠재적인 오해 또는 비효율성(특히 복잡한 쿼리의 경우)을 제거합니다.
- 성능 투명성: 쿼리 성능을 이해하고 최적화하기가 더 쉽습니다. 데이터베이스로 전송되는 SQL이 정확히 무엇인지 볼 수 있으므로 정확한 조정 및 디버깅이 가능합니다. ORM의 경우 생성된 SQL을 이해하려면 추가 로깅 또는 디버깅 도구가 필요한 경우가 많습니다.
- 추상화 오버헤드 감소: ORM은 일부에게는 도움이 되지만 다른 사람에게는 과할 수 있는 추상화 계층을 추가합니다. 이 추상화는 때때로 "N+1 쿼리 문제"를 유발하거나 최적이 아닌 SQL을 생성할 수 있으며, 이는 ORM의 추상화에서 "벗어나" 수정하는 방법을 이해해야 합니다.
pg를 사용하면 이 문제가 완전히 해결됩니다. - 작은 의존성 영향: ORM은 종종 많은 의존성을 가진 큰 라이브러리입니다.
pg는 상대적으로 가벼운 드라이버이므로node_modules디렉토리가 작아지고 빌드 시간이 단축될 수 있습니다. - 데이터베이스 기능 활용: SQL을 직접 작성하면 ORM 지원을 기다리거나 ORM의 쿼리 빌더에 억지로 끼워 넣으려고 하지 않고도 고급 데이터베이스별 기능(예: 공통 테이블 식, 창 함수, 사용자 정의 함수)을 쉽게 사용할 수 있습니다.
Node-Postgres 사용 방법
실제 예제를 통해 설명해 보겠습니다. 먼저 라이브러리를 설치해야 합니다.
npm install pg
다음으로 연결을 설정합니다. 단일 쿼리에는 Client를 사용하거나 여러 동시 연결을 관리하기 위한 Pool을 사용할 수 있습니다. 대부분의 애플리케이션에는 Pool이 권장됩니다.
연결 풀 설정
// db.js const { Pool } = require('pg'); const pool = new Pool({ user: 'your_user', host: 'localhost', database: 'your_database', password: 'your_password', port: 5432, }); pool.on('error', (err, client) => { console.error('유휴 클라이언트에서 예상치 못한 오류 발생', err); process.exit(-1); }); module.exports = { query: (text, params) => pool.query(text, params), getClient: () => pool.connect(), };
기본 CRUD 작업 수행
간단한 users 테이블을 상상해 보겠습니다.
CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
1. 데이터 삽입
const { query } = require('./db'); async function createUser(name, email) { const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING id, name, email'; const values = [name, email]; try { const res = await query(text, values); console.log('사용자 생성됨:', res.rows[0]); return res.rows[0]; } catch (err) { console.error('사용자 생성 오류:', err.stack); throw err; } } // 예제 사용 createUser('Alice Smith', 'alice@example.com');
$1, $2와 같은 플레이스홀더가 눈에 띕니다. pg는 SQL 주입 공격을 방지하는 데 중요한 매개변수화된 쿼리를 사용합니다.
2. 데이터 검색
const { query } = require('./db'); async function getUsers() { const text = 'SELECT id, name, email, created_at FROM users'; try { const res = await query(text); console.log('모든 사용자:', res.rows); return res.rows; } catch (err) { console.error('사용자 가져오기 오류:', err.stack); throw err; } } async function getUserById(id) { const text = 'SELECT id, name, email, created_at FROM users WHERE id = $1'; const values = [id]; try { const res = await query(text, values); console.log(`ID ${id}인 사용자:`, res.rows[0]); return res.rows[0]; } catch (err) { console.error(`ID ${id}인 사용자 가져오기 오류:`, err.stack); throw err; } } // 예제 사용 getUsers(); getUserById(1);
3. 데이터 업데이트
const { query } = require('./db'); async function updateUserEmail(id, newEmail) { const text = 'UPDATE users SET email = $1 WHERE id = $2 RETURNING id, name, email'; const values = [newEmail, id]; try { const res = await query(text, values); if (res.rows.length === 0) { console.log(`ID ${id}인 사용자 찾을 수 없음.`); return null; } console.log('사용자 업데이트됨:', res.rows[0]); return res.rows[0]; } catch (err) { console.error(`ID ${id}인 사용자 업데이트 오류:`, err.stack); throw err; } } // 예제 사용 updateUserEmail(1, 'alice.new@example.com');
4. 데이터 삭제
const { query } = require('./db'); async function deleteUser(id) { const text = 'DELETE FROM users WHERE id = $1 RETURNING id'; const values = [id]; try { const res = await query(text, values); if (res.rows.length === 0) { console.log(`ID ${id}인 사용자 찾을 수 없음.`); return null; } console.log(`ID ${res.rows[0].id}인 사용자 삭제됨.`); return res.rows[0].id; } catch (err) { console.error(`ID ${id}인 사용자 삭제 오류:`, err.stack); throw err; } } // 예제 사용 deleteUser(2);
트랜잭션
여러 데이터베이스 변경이 함께 성공하거나 실패해야 하는 작업의 경우 트랜잭션이 필수적입니다.
const { getClient } = require('./db'); async function transferFunds(fromAccountId, toAccountId, amount) { const client = await getClient(); try { await client.query('BEGIN'); // 송금자로부터 차감 await client.query( 'UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromAccountId] ); // 수신자에게 추가 await client.query( 'UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toAccountId] ); await client.query('COMMIT'); console.log(`계정 ${fromAccountId}에서 ${toAccountId}로 ${amount} 성공적으로 이체됨`); return true; } catch (err) { await client.query('ROLLBACK'); console.error('자금 이체 중 오류 발생, 트랜잭션 롤백:', err.stack); throw err; } finally { client.release(); // 클라이언트를 풀로 반환 } } // 가정상의 `accounts` 테이블: // CREATE TABLE accounts (id SERIAL PRIMARY KEY, balance NUMERIC); // INSERT INTO accounts (balance) VALUES (1000), (500); // 예제 사용 transferFunds(1, 2, 100);
애플리케이션 시나리오
중소 규모 애플리케이션, 데이터 모델에 집중하는 마이크로서비스, 성능 또는 세분화된 SQL 제어가 중요한 프로젝트는 node-postgres를 직접 사용하는 데 탁월한 후보입니다. 팀이 SQL에 익숙하다면 이 접근 방식은 데이터 집약적인 기능의 개발 속도를 높이는 경우가 많습니다.
결론
ORM은 특정 프로젝트의 개발 속도를 높일 수 있는 매력적인 추상화를 제공하지만, 만능 솔루션은 아닙니다. PostgreSQL과 상호 작용하는 많은 Node.js 애플리케이션의 경우 node-postgres 네이티브 라이브러리는 직접적이고 강력하며 종종 더 효율적인 대안을 제공합니다. pg를 사용하여 원시 SQL을 채택함으로써 개발자는 세분화된 제어, 투명한 성능 및 더 가벼운 의존성으로 더 견고하고 유지 관리하기 쉬운 데이터 상호 작용으로 이어질 수 있음을 입증합니다. 프로젝트가 데이터베이스와 직접 대화함으로써 번창할 수 있습니다.