Node.js 데이터베이스 연동 (MySQL/MariaDB)
Node.js 환경에서 mysql2 라이브러리를 사용하여 MySQL 또는 MariaDB 데이터베이스에 연결하고, 기본적인 CRUD 작업을 수행하는 방법을 학습합니다.
Node.js 데이터베이스 연동 (MySQL/MariaDB)
대부분의 웹 애플리케이션은 데이터를 영구적으로 저장하고 관리하기 위해 데이터베이스를 사용합니다. Node.js는 다양한 데이터베이스와 연동할 수 있는 라이브러리들을 지원하며, 그 중에서도 MySQL 및 MariaDB는 가장 널리 사용되는 관계형 데이터베이스 관리 시스템(RDBMS)입니다.
이번 시간에는 mysql2
라이브러리를 사용하여 Node.js 애플리케이션에서 MariaDB 데이터베이스에 접속하고, 기본적인 데이터 처리 작업인 CRUD(Create, Read, Update, Delete) 를 async/await
구문을 통해 구현하는 방법을 학습합니다.
1. mysql2
라이브러리 설치
Node.js에서 MySQL/MariaDB와 통신하기 위해서는 전용 드라이버가 필요합니다. mysql
라이브러리도 있지만, mysql2
는 Promise를 지원하여 async/await
와 함께 사용하기 편리하고 성능이 개선된 버전이므로 mysql2
를 사용하는 것을 권장합니다.
1
$ yarn add mysql2
2. 데이터베이스 접속 설정
애플리케이션에서 데이터베이스에 접속하려면 접속 정보를 설정해야 합니다.
데이터베이스 접속 정보는 보안을 위해서 설정파일에 명시하는 것이 좋습니다. 명시해야 하는 정보는 다음과 같습니다.
- Host: 데이터베이스 서버의 주소 (일반적으로
localhost
) - Port: 데이터베이스 서버의 포트 번호 (MariaDB 기본값은
3306
, 예제에서는9090
) - User: 데이터베이스 사용자 계정
- Pass: 사용자 계정의 비밀번호
- Database Name: 접속할 데이터베이스의 이름
- Connection Limit: 커넥션 풀의 최대 연결 수 (예:
10
)
/.env
파일에 다음과 같이 접속 정보를 설정합니다.
# ... 이전 내용 생략 ...
# 데이터베이스 접속 정보
DB_HOST=localhost
DB_PORT=9090
DB_USER=myschool
DB_PASS=1234
DB_NAME=myschool
DB_CONNECTION_LIMIT=10
3. 데이터베이스 커넥션 풀(Connection Pool)
사용자의 요청이 있을 때마다 데이터베이스에 새로 연결하고 해제하는 작업은 많은 비용을 발생시킵니다. 이를 방지하기 위해 커넥션 풀(Connection Pool) 방식을 사용합니다.
커넥션 풀은 미리 일정 개수의 데이터베이스 연결(Connection)을 만들어 두고, 필요할 때마다 가져다 쓴 뒤 다시 반납하는 방식입니다. 이를 통해 애플리케이션의 성능을 크게 향상시킬 수 있습니다.
mysql2
라이브러리는 커넥션 풀 기능을 쉽게 구현할 수 있도록 promise()
메서드를 제공합니다.
DBHelper 클래스 API 명세
구현할 DBHelper
클래스의 주요 기능과 메서드를 다음 표와 같이 정의합니다:
구분 | 메서드/속성 | 타입 | 매개변수 | 반환값 | 설명 |
---|---|---|---|---|---|
속성 | connected | getter | - | boolean | 데이터베이스 연결 상태 확인 |
연결 관리 | connect() | async method | - | Promise<void> | 커넥션 풀 생성 및 데이터베이스 연결 |
close() | async method | - | Promise<void> | 커넥션 풀 종료 및 연결 해제 | |
쿼리 실행 | query() | async method | sql: string params?: array | Promise<array\|object> | SQL 쿼리 실행 및 결과 반환 |
트랜잭션 | transaction() | async method | callback: function | Promise<any> | 트랜잭션 내에서 안전한 쿼리 실행 |
주요 특징
- 싱글톤 패턴: 애플리케이션 전체에서 하나의 인스턴스만 유지
- 클래스 기반: 상태 관리와 메서드 체계화를 통한 명확한 구조
- 환경변수 지원:
.env
파일을 통한 안전한 설정 관리 - 에러 처리: 상세한 로깅과 예외 처리
- 성능 최적화: 커넥션 풀을 통한 효율적인 DB 연결 관리
실습: 데이터베이스 접속 모듈 생성 (/helpers/DBHelper.js
)
반복적인 데이터베이스 접속 코드를 줄이고 재사용성을 높이기 위해, 향상된 DBHelper
모듈을 생성합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// 필요한 라이브러리 import
import dotenv from 'dotenv'; // 환경변수 관리를 위한 라이브러리
import mysql from 'mysql2/promise'; // MySQL/MariaDB 연결을 위한 Promise 기반 드라이버
import logger from './LogHelper.js'; // 사용자 정의 로깅 헬퍼
// .env 파일의 환경변수를 process.env에 로드
// 이 함수 호출 후 process.env.DB_HOST, process.env.DB_PORT 등을 사용할 수 있음
dotenv.config();
class DBHelper {
// 싱글톤 객체
static #current = null;
// DBHelper 인스턴스
#pool = null;
#isConnected = false;
/**
* 싱글톤 객체를 생성하여 리턴한다.
* @returns {MybatisHelper}
*/
static getInstance() {
if (DBHelper.#current === null) {
DBHelper.#current = new DBHelper();
}
return DBHelper.#current;
}
/**
* 데이터베이스 연결 상태를 확인하는 getter 속성
* @returns {boolean} 연결되어 있으면 true, 아니면 false
*/
get connected() {
return this.#isConnected && this.#pool !== null;
}
/**
* 커넥션 풀을 생성하고 데이터베이스에 접속
*
* 커넥션 풀의 장점:
* - 연결 재사용으로 성능 향상
* - 동시 접속 수 제어로 서버 안정성 확보
* - 자동 연결 관리로 개발 편의성 증대
*/
async connect() {
// 이미 연결되어 있다면 재연결하지 않음 (중복 연결 방지)
if (this.connected) {
logger.debug('Database already connected');
return;
}
logger.debug('----------- DATABASE Connect -----------');
try {
// 커넥션 풀 생성 - 환경변수에서 설정값 읽어옴
this.#pool = mysql.createPool({
host: process.env.DB_HOST, // 데이터베이스 서버 주소
port: parseInt(process.env.DB_PORT) || 3306, // 포트 번호 (기본값: 3306)
user: process.env.DB_USER, // 사용자 계정
password: process.env.DB_PASS, // 비밀번호
database: process.env.DB_NAME, // 데이터베이스 이름
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT) || 10, // 최대 연결 수 (기본값: 10)
waitForConnections: true, // 연결 대기 여부
queueLimit: 0 // 대기 큐 제한 (0 = 무제한)
});
// 연결 테스트 - 실제 데이터베이스에 ping을 보내 연결 확인
const connection = await this.#pool.getConnection(); // 풀에서 연결 객체 가져오기
await connection.ping(); // 연결 상태 확인
connection.release(); // 연결 객체를 풀에 반납
// 연결 성공 상태 업데이트
this.#isConnected = true;
logger.info('Database connected successfully');
} catch (error) {
// 연결 실패 시 에러 처리
logger.error('Database connection failed:', error);
this.#isConnected = false;
throw error; // 상위 레벨로 에러 전파
}
}
/**
* 커넥션 풀을 종료하고 모든 연결을 해제
* 애플리케이션 종료 시 반드시 호출해야 함
*/
async close() {
if (this.#pool) {
try {
await this.#pool.end(); // 모든 연결 종료 대기
this.#pool = null; // 풀 객체 초기화
this.#isConnected = false; // 연결 상태 플래그 업데이트
logger.info('Database connection closed');
} catch (error) {
logger.error('Error closing database connection:', error);
}
}
}
/**
* SQL문을 실행하고 결과를 반환
*
* @param {string} sql - 실행할 SQL문 (? 플레이스홀더 사용 가능)
* @param {array} params - SQL문의 ? 자리에 들어갈 파라미터 배열
* @returns {Promise<array|object>} SELECT 결과는 배열, INSERT/UPDATE/DELETE는 OkPacket 객체
*/
async query(sql, params = []) {
// 연결 상태 확인
if (!this.connected) {
throw new Error('Database not connected. Call connect() first.');
}
let dbcon = null; // 커넥션 객체
let result = null; // 쿼리 실행 결과
try {
// 커넥션 풀에서 사용 가능한 연결 객체 임대
dbcon = await this.#pool.getConnection();
// 성능 측정을 위한 시작 시간 기록
const startTime = Date.now();
logger.debug(`Executing SQL: ${sql.trim()}`);
// SQL 실행 - 파라미터 바인딩으로 SQL Injection 방지
const [rows] = await dbcon.query(sql, params);
// 실행 시간 계산 및 로깅
const executionTime = Date.now() - startTime;
logger.debug(`SQL executed in ${executionTime}ms`);
// 결과 타입에 따른 처리 분기
// OkPacket: INSERT, UPDATE, DELETE 작업의 결과 객체
if (rows.constructor.name === 'OkPacket') {
result = {
fieldCount: rows.fieldCount, // 필드 개수
affectedRows: rows.affectedRows, // 영향받은 행 수
insertId: rows.insertId, // INSERT 시 생성된 ID
serverStatus: rows.serverStatus, // 서버 상태
warningCount: rows.warningCount, // 경고 개수
message: rows.message, // 서버 메시지
protocol41: rows.protocol41, // 프로토콜 버전
changedRows: rows.changedRows, // 실제 변경된 행 수
};
} else {
// SELECT 결과는 배열 형태로 반환
result = rows;
}
} catch (err) {
// SQL 실행 에러 로깅 및 재발생
logger.error('SQL execution error:', { sql, params, error: err.message });
throw err;
} finally {
// 연결 객체를 반드시 풀에 반납 (메모리 누수 방지)
if (dbcon) {
dbcon.release();
}
}
return result;
}
/**
* 트랜잭션 내에서 여러 쿼리를 안전하게 실행
*
* 트랜잭션의 ACID 특성:
* - Atomicity(원자성): 모든 작업이 성공하거나 모두 실패
* - Consistency(일관성): 데이터베이스 제약조건 유지
* - Isolation(격리성): 동시 실행되는 트랜잭션 간 간섭 방지
* - Durability(지속성): 커밋된 데이터는 영구적으로 저장
*
* @param {function} callback - 트랜잭션 내에서 실행할 함수
* @returns {Promise<any>} 콜백 함수의 반환값
*/
async transaction(callback) {
// 연결 상태 확인
if (!this.connected) {
throw new Error('Database not connected. Call connect() first.');
}
let dbcon = null;
try {
// 트랜잭션 전용 연결 객체 획득
dbcon = await this.#pool.getConnection();
// 트랜잭션 시작
await dbcon.beginTransaction();
logger.debug('Transaction started');
// 사용자 정의 쿼리들 실행
const result = await callback(dbcon);
// 모든 작업이 성공하면 커밋
await dbcon.commit();
logger.debug('Transaction committed');
return result;
} catch (error) {
// 에러 발생 시 롤백 (모든 변경사항 취소)
if (dbcon) {
await dbcon.rollback();
logger.debug('Transaction rolled back');
}
logger.error('Transaction error:', error);
throw error;
} finally {
// 트랜잭션 전용 연결 객체 반납
if (dbcon) {
dbcon.release();
}
}
}
}
// 싱글톤 패턴 구현 - 애플리케이션 전체에서 하나의 인스턴스만 사용
export default DBHelper.getInstance();
4. CRUD 구현하기
이제 DBHelper
모듈을 사용하여 myschool
데이터베이스의 departments
테이블에 대한 CRUD 작업을 수행하는 예제를 만들어 보겠습니다.
1) DBHelper 기본 사용법 및 조회 (SELECT)
DBHelper
의 기본 사용법과 데이터 조회 기능을 학습합니다. 연결 상태 확인, 기본 쿼리, 파라미터 쿼리, 트랜잭션을 포함합니다.
실습: /07-Database/01_select_example.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import dbHelper from '../helpers/DBHelper.js';
(async () => {
try {
// 1. 데이터베이스 연결
console.log('1. 데이터베이스 연결...');
await dbHelper.connect();
// 2. 연결 상태 확인
console.log(`2. 연결 상태: ${dbHelper.connected ? '연결됨' : '연결 안됨'}`);
// 3. 기본 쿼리 실행
console.log('\n3. 기본 쿼리 실행...');
const sql = "SELECT id, dname, loc FROM departments LIMIT 3";
const result = await dbHelper.query(sql);
console.log('결과:', result);
// 4. 파라미터를 사용한 쿼리
console.log('\n4. 파라미터 쿼리 실행...');
const paramSql = "SELECT * FROM departments WHERE id = ?";
const paramResult = await dbHelper.query(paramSql, [101]);
console.log('파라미터 결과:', paramResult);
// 5. 트랜잭션 예제
console.log('\n5. 트랜잭션 예제...');
const transactionResult = await dbHelper.transaction(async (connection) => {
// 트랜잭션 내에서 여러 쿼리 실행
const [rows1] = await connection.query("SELECT COUNT(*) as count FROM departments");
const [rows2] = await connection.query("SELECT COUNT(*) as count FROM students");
return {
departments: rows1[0].count,
students: rows2[0].count
};
});
console.log('트랜잭션 결과:', transactionResult);
} catch (e) {
console.error("데이터베이스 작업에 실패했습니다:");
console.error(e.message);
} finally {
// DB 접속 해제
console.log('\n6. 데이터베이스 연결 종료...');
await dbHelper.close();
console.log('연결이 종료되었습니다.');
}
})();
2) 데이터 추가 (INSERT)
INSERT
문을 사용하여 새로운 데이터를 추가합니다. 단일 레코드 삽입과 배치 삽입, 트랜잭션을 활용한 안전한 삽입을 다룹니다.
실습: /07-Database/02_insert_example.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import dbHelper from '../helpers/DBHelper.js';
(async () => {
try {
// 데이터베이스 연결
await dbHelper.connect();
console.log('데이터베이스에 연결되었습니다.\n');
// 1. 단일 레코드 INSERT
console.log('1. 단일 학과 정보 INSERT...');
const insertSql = `
INSERT INTO departments (dname, loc, phone, email, established, homepage)
VALUES (?, ?, ?, ?, ?, ?)
`;
const newDeptData = [
'데이터사이언스학과',
'IT관',
'051-999-8888',
'ds@myschool.ac.kr',
2023,
'http://ds.myschool.ac.kr'
];
const insertResult = await dbHelper.query(insertSql, newDeptData);
console.log('INSERT 결과:', insertResult);
console.log(`새로 생성된 레코드 ID: ${insertResult.insertId}`);
console.log(`영향받은 행 수: ${insertResult.affectedRows}\n`);
// 2. 배치 INSERT (여러 레코드 동시 삽입)
console.log('2. 여러 학과 정보 배치 INSERT...');
const batchInsertSql = `
INSERT INTO departments (dname, loc, phone, email, established, homepage)
VALUES
(?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?)
`;
const batchData = [
'융합소프트웨어학과', 'IT관', '051-999-8801', 'sw@myschool.ac.kr', 2024, 'http://sw.myschool.ac.kr',
'사이버보안학과', 'IT관', '051-999-8802', 'security@myschool.ac.kr', 2024, 'http://security.myschool.ac.kr',
'게임개발학과', 'IT관', '051-999-8803', 'game@myschool.ac.kr', 2024, 'http://game.myschool.ac.kr'
];
const batchResult = await dbHelper.query(batchInsertSql, batchData);
console.log('배치 INSERT 결과:', batchResult);
console.log(`영향받은 행 수: ${batchResult.affectedRows}\n`);
// 3. 트랜잭션을 사용한 안전한 INSERT
console.log('3. 트랜잭션을 사용한 안전한 INSERT...');
const transactionResult = await dbHelper.transaction(async (connection) => {
// 첫 번째 학과 삽입
const [result1] = await connection.query(
'INSERT INTO departments (dname, loc, phone, email, established, homepage) VALUES (?, ?, ?, ?, ?, ?)',
['블록체인학과', 'IT관', '051-999-8804', 'blockchain@myschool.ac.kr', 2024, 'http://blockchain.myschool.ac.kr']
);
// 두 번째 학과 삽입
const [result2] = await connection.query(
'INSERT INTO departments (dname, loc, phone, email, established, homepage) VALUES (?, ?, ?, ?, ?, ?)',
['IoT학과', 'IT관', '051-999-8805', 'iot@myschool.ac.kr', 2024, 'http://iot.myschool.ac.kr']
);
return {
first: { insertId: result1.insertId, affectedRows: result1.affectedRows },
second: { insertId: result2.insertId, affectedRows: result2.affectedRows }
};
});
console.log('트랜잭션 INSERT 결과:', transactionResult);
console.log('모든 INSERT 작업이 안전하게 완료되었습니다.\n');
// 4. 삽입된 데이터 확인
console.log('4. 삽입된 데이터 확인...');
const selectSql = 'SELECT id, dname, loc, established FROM departments WHERE id >= 600 ORDER BY id';
const insertedData = await dbHelper.query(selectSql);
console.log('새로 삽입된 학과 정보:');
insertedData.forEach(dept => {
console.log(`- ID: ${dept.id}, 학과명: ${dept.dname}, 위치: ${dept.loc}, 설립연도: ${dept.established}`);
});
} catch (error) {
console.error('INSERT 작업 중 오류 발생:', error.message);
// SQL 중복 키 에러 처리
if (error.code === 'ER_DUP_ENTRY') {
console.error('중복된 키 값입니다. 이미 존재하는 ID를 사용했습니다.');
}
} finally {
await dbHelper.close();
console.log('\n데이터베이스 연결이 종료되었습니다.');
}
})();
3) 데이터 수정 (UPDATE)
UPDATE
문을 사용하여 기존 데이터를 수정합니다. 단일 레코드 수정, 조건부 다중 레코드 수정, 트랜잭션을 활용한 안전한 수정을 다룹니다.
실습: /07-Database/03_update_example.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import dbHelper from '../helpers/DBHelper.js';
(async () => {
try {
// 데이터베이스 연결
await dbHelper.connect();
console.log('데이터베이스에 연결되었습니다.\n');
// 1. 단일 레코드 UPDATE
console.log('1. 특정 학과 정보 UPDATE...');
// 먼저 현재 데이터 확인
const beforeUpdateSql = 'SELECT id, dname, loc, phone FROM departments WHERE id = ?';
const beforeData = await dbHelper.query(beforeUpdateSql, [101]);
console.log('수정 전 데이터:', beforeData[0]);
// 업데이트 실행
const updateSql = `
UPDATE departments
SET loc = ?, phone = ?, homepage = ?
WHERE id = ?
`;
const updateData = ['신공학관', '051-123-9999', 'http://newcs.myschool.ac.kr', 101];
const updateResult = await dbHelper.query(updateSql, updateData);
console.log('UPDATE 결과:', updateResult);
console.log(`영향받은 행 수: ${updateResult.affectedRows}\n`);
// 수정 후 데이터 확인
const afterData = await dbHelper.query(beforeUpdateSql, [101]);
console.log('수정 후 데이터:', afterData[0]);
console.log();
// 2. 조건부 다중 레코드 UPDATE
console.log('2. 조건부 다중 레코드 UPDATE...');
// 공학관에 있는 모든 학과의 전화번호 업데이트
const multiUpdateSql = `
UPDATE departments
SET phone = CONCAT('051-555-', LPAD(id, 4, '0'))
WHERE loc LIKE '%공학관%'
`;
const multiUpdateResult = await dbHelper.query(multiUpdateSql);
console.log('다중 UPDATE 결과:', multiUpdateResult);
console.log(`영향받은 행 수: ${multiUpdateResult.affectedRows}\n`);
// 업데이트된 데이터 확인
const updatedDepts = await dbHelper.query(
"SELECT id, dname, loc, phone FROM departments WHERE loc LIKE '%공학관%' ORDER BY id"
);
console.log('공학관 학과들의 업데이트된 전화번호:');
updatedDepts.forEach(dept => {
console.log(`- ${dept.dname}: ${dept.phone}`);
});
console.log();
// 3. 트랜잭션을 사용한 안전한 UPDATE
console.log('3. 트랜잭션을 사용한 관련 데이터 일괄 UPDATE...');
const transactionResult = await dbHelper.transaction(async (connection) => {
// 특정 학과의 정보를 일괄 업데이트
const [result1] = await connection.query(
'UPDATE departments SET email = ? WHERE id = ?',
['newemail@myschool.ac.kr', 102]
);
const [result2] = await connection.query(
'UPDATE departments SET homepage = ? WHERE id = ?',
['http://newsw.myschool.ac.kr', 102]
);
const [result3] = await connection.query(
'UPDATE departments SET established = ? WHERE id = ?',
[1995, 102]
);
return {
emailUpdate: result1.affectedRows,
homepageUpdate: result2.affectedRows,
establishedUpdate: result3.affectedRows
};
});
console.log('트랜잭션 UPDATE 결과:', transactionResult);
console.log('모든 관련 데이터가 안전하게 업데이트되었습니다.\n');
// 4. 조건부 UPDATE (존재하는 경우만)
console.log('4. 조건부 UPDATE (존재하는 경우만)...');
const conditionalUpdateSql = `
UPDATE departments
SET loc = CASE
WHEN established < 2000 THEN CONCAT(loc, ' (구관)')
ELSE loc
END
WHERE established IS NOT NULL
`;
const conditionalResult = await dbHelper.query(conditionalUpdateSql);
console.log('조건부 UPDATE 결과:', conditionalResult);
console.log(`영향받은 행 수: ${conditionalResult.affectedRows}\n`);
// 5. 최종 결과 확인
console.log('5. 최종 업데이트 결과 확인...');
const finalData = await dbHelper.query(
'SELECT id, dname, loc, phone, email, established FROM departments WHERE id IN (101, 102) ORDER BY id'
);
console.log('최종 업데이트된 데이터:');
finalData.forEach(dept => {
console.log(`ID: ${dept.id}`);
console.log(` 학과명: ${dept.dname}`);
console.log(` 위치: ${dept.loc}`);
console.log(` 전화: ${dept.phone}`);
console.log(` 이메일: ${dept.email}`);
console.log(` 설립연도: ${dept.established}`);
console.log('---');
});
} catch (error) {
console.error('UPDATE 작업 중 오류 발생:', error.message);
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.error('존재하지 않는 컬럼을 참조했습니다.');
}
} finally {
await dbHelper.close();
console.log('\n데이터베이스 연결이 종료되었습니다.');
}
})();
4) 데이터 삭제 (DELETE)
DELETE
문을 사용하여 데이터를 삭제합니다. 단일 레코드 삭제, 조건부 다중 삭제, 안전한 삭제 방법을 다룹니다.
실습: /07-Database/04_delete_example.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import dbHelper from '../helpers/DBHelper.js';
(async () => {
try {
// 데이터베이스 연결
await dbHelper.connect();
console.log('데이터베이스에 연결되었습니다.\n');
// 0. 삭제할 테스트 데이터 먼저 생성
console.log('0. 테스트용 데이터 생성...');
const testDataSql = `
INSERT INTO departments (id, dname, loc, phone, email, established, homepage)
VALUES
(700, '테스트학과1', '테스트관', '051-000-0001', 'test1@test.com', 2024, 'http://test1.com'),
(701, '테스트학과2', '테스트관', '051-000-0002', 'test2@test.com', 2024, 'http://test2.com'),
(702, '테스트학과3', '테스트관', '051-000-0003', 'test3@test.com', 2024, 'http://test3.com'),
(703, '임시학과', '임시관', '051-000-0004', 'temp@test.com', 2024, 'http://temp.com')
ON DUPLICATE KEY UPDATE dname = VALUES(dname)
`;
await dbHelper.query(testDataSql);
console.log('테스트 데이터가 생성되었습니다.\n');
// 생성된 데이터 확인
const createdData = await dbHelper.query(
'SELECT id, dname, loc FROM departments WHERE id >= 700 ORDER BY id'
);
console.log('생성된 테스트 데이터:');
createdData.forEach(dept => {
console.log(`- ID: ${dept.id}, 학과명: ${dept.dname}, 위치: ${dept.loc}`);
});
console.log();
// 1. 단일 레코드 DELETE
console.log('1. 특정 학과 DELETE...');
const deleteSql = 'DELETE FROM departments WHERE id = ?';
const deleteResult = await dbHelper.query(deleteSql, [700]);
console.log('DELETE 결과:', deleteResult);
console.log(`삭제된 행 수: ${deleteResult.affectedRows}\n`);
// 삭제 확인
const checkDeleted = await dbHelper.query(
'SELECT COUNT(*) as count FROM departments WHERE id = ?', [700]
);
console.log(`ID 700 레코드 존재 여부: ${checkDeleted[0].count > 0 ? '존재함' : '삭제됨'}\n`);
// 2. 조건부 다중 레코드 DELETE
console.log('2. 조건부 다중 레코드 DELETE...');
const multiDeleteSql = 'DELETE FROM departments WHERE loc = ? AND established >= ?';
const multiDeleteResult = await dbHelper.query(multiDeleteSql, ['테스트관', 2024]);
console.log('다중 DELETE 결과:', multiDeleteResult);
console.log(`삭제된 행 수: ${multiDeleteResult.affectedRows}\n`);
// 3. 트랜잭션을 사용한 안전한 DELETE
console.log('3. 트랜잭션을 사용한 안전한 DELETE...');
// 먼저 새로운 테스트 데이터 생성
const newTestDataSql = `
INSERT INTO departments (id, dname, loc, phone, email, established, homepage)
VALUES
(704, '삭제예정1', '임시관', '051-000-0005', 'del1@test.com', 2024, 'http://del1.com'),
(705, '삭제예정2', '임시관', '051-000-0006', 'del2@test.com', 2024, 'http://del2.com')
ON DUPLICATE KEY UPDATE dname = VALUES(dname)
`;
await dbHelper.query(newTestDataSql);
const transactionResult = await dbHelper.transaction(async (connection) => {
// 관련된 여러 테이블에서 데이터를 순서대로 삭제
// (실제로는 외래키 제약이 있다면 순서가 중요)
// 첫 번째 레코드 삭제
const [result1] = await connection.query(
'DELETE FROM departments WHERE id = ?', [704]
);
// 두 번째 레코드 삭제
const [result2] = await connection.query(
'DELETE FROM departments WHERE id = ?', [705]
);
// 관련 데이터가 모두 삭제되었는지 확인
const [checkResult] = await connection.query(
'SELECT COUNT(*) as count FROM departments WHERE id IN (704, 705)'
);
return {
first: result1.affectedRows,
second: result2.affectedRows,
remainingCount: checkResult[0].count
};
});
console.log('트랜잭션 DELETE 결과:', transactionResult);
console.log('관련 데이터가 안전하게 삭제되었습니다.\n');
// 4. 조건부 DELETE (존재하는 경우만)
console.log('4. 조건부 DELETE with LIMIT...');
// 가장 최근에 생성된 임시 데이터 중 일부만 삭제
const limitDeleteSql = `
DELETE FROM departments
WHERE dname LIKE '%임시%'
ORDER BY id DESC
LIMIT 1
`;
const limitDeleteResult = await dbHelper.query(limitDeleteSql);
console.log('제한적 DELETE 결과:', limitDeleteResult);
console.log(`삭제된 행 수: ${limitDeleteResult.affectedRows}\n`);
// 5. 복합 조건 DELETE
console.log('5. 복합 조건 DELETE...');
const complexDeleteSql = `
DELETE FROM departments
WHERE (established > 2023 OR loc LIKE '%테스트%')
AND id NOT IN (101, 102, 103)
`;
const complexDeleteResult = await dbHelper.query(complexDeleteSql);
console.log('복합 조건 DELETE 결과:', complexDeleteResult);
console.log(`삭제된 행 수: ${complexDeleteResult.affectedRows}\n`);
// 6. 삭제 전후 비교
console.log('6. 최종 데이터 상태 확인...');
const finalData = await dbHelper.query(
'SELECT COUNT(*) as total_count FROM departments'
);
console.log(`전체 학과 수: ${finalData[0].total_count}`);
const remainingTestData = await dbHelper.query(
'SELECT id, dname, loc FROM departments WHERE id >= 700 ORDER BY id'
);
if (remainingTestData.length > 0) {
console.log('남은 테스트 데이터:');
remainingTestData.forEach(dept => {
console.log(`- ID: ${dept.id}, 학과명: ${dept.dname}, 위치: ${dept.loc}`);
});
} else {
console.log('모든 테스트 데이터가 삭제되었습니다.');
}
} catch (error) {
console.error('DELETE 작업 중 오류 발생:', error.message);
if (error.code === 'ER_ROW_IS_REFERENCED_2') {
console.error('외래키 제약으로 인해 삭제할 수 없습니다. 관련 데이터를 먼저 삭제하세요.');
}
} finally {
await dbHelper.close();
console.log('\n데이터베이스 연결이 종료되었습니다.');
}
})();
5. 주요 학습 포인트
DBHelper 클래스의 핵심 특징
- 싱글톤 패턴: 애플리케이션 전체에서 하나의 DB 연결 인스턴스만 유지
- 클래스 기반 구조: 상태 관리와 메서드 체계화로 더 나은 코드 구조
- async/await 지원: 모든 DB 작업이 비동기로 처리되어 성능 향상
- 트랜잭션 지원: 여러 쿼리를 하나의 단위로 안전하게 처리
- 환경변수 활용:
.env
파일을 통한 안전한 설정 관리