사이트를 운영할 때, 백엔드를 위한 파일을 따로 둡니다.
이때, 해당 파일을
node backend.js
위와 같은 명령어로 실행을 할 수 있습니다.
문제는 이런 명령어로 실행한 세션이 닫힌다고 가정할까요?
저는 Putty 를 통해서 EC2 에 접근하고, 그곳에서 백엔드 파일을 실행하는데
이렇게 실행한 백엔드 파일을 컨트롤+C로 끄거나, 세션을 닫으면 사이트의 백엔드 로직은 더 이상 작동하지 않습니다.
nohup node backend.js &
때문에 보통 이런 명령어를 실행합니다.
해당 명령어를 하면 세션이 꺼지더라도 지속해서 백엔드가 실행되게 됩니다.
찾아보면 이론은 그렇습니다.
그런데 문제가 발생합니다.
Database error: Error: Can't add new command when connection is in closed state
분명히 백엔드 파일은 실행을 했고, 사이트는 잘 작동을 하고 있던 와중에…
꼭 시간이 어느 정도 흐르면 위와 같은 에러와 함께 백엔드 로직 작동이 멈춥니다.
저는 그 이유를 알아보고 이것을 해결코자 합니다.
이 오류 메시지는 MySQL 데이터베이스 연결과 관련된 문제를 나타내고 있습니다.
구체적으로는 MySQL 연결이 닫힌 상태에서 명령을 실행하려고 할 때 발생하는 오류입니다.
GPT를 이용해서 디버깅을 해본 결과 이런 답변을 받았네요.
그러니까, 이 오류는 MySQL DB를 이용하기 때문에 그런 거였습니다.
그렇다면 지금 현재 저는 MySQL DB접근을 위해 어떤 방법을 취하고 있는지 보겠습니다.
const express = require('express');
const mysql = require('mysql2');
const app = express();
// MySQL 연결을 단일 연결로 설정 (잘못된 방식)
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'my_database'
});
// 잘못된 방식의 라우터 예시
app.get('/api/get-users', (req, res) => {
const query = "SELECT * FROM users";
// MySQL 쿼리를 요청할 때마다 단일 연결 사용
connection.query(query, (error, results) => {
if (error) {
console.error('Error fetching users:', error);
res.status(500).send("Error fetching users.");
return;
}
res.status(200).json(results);
});
});
// 서버 실행
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
이게 바로 제가 사용하던 방식이었습니다.
mysql.createConnection 을 통해서 연결을 위한 단일 설정을 취하고 있었지요.
그렇지만, 이 방식에는 다음과 같은 문제가 있었습니다.
mysql.createConnection()을 사용하여 하나의 MySQL 연결만 생성합니다. 이 연결은 일정 시간이 지나면 유휴 상태로 인해 MySQL 서버에서 끊길 가능성이 있습니다. 그 상태에서 새로운 쿼리를 보내면, “Can’t add new command when connection is in closed state” 오류가 발생할 수 있습니다.
매 요청마다 같은 단일 연결을 사용하고 있습니다. MySQL 연결이 끊긴 상태에서는 이 쿼리가 제대로 실행되지 않고, 연결을 재사용할 수 없는 상태가 됩니다. 연결 상태를 확인하지 않고 계속 사용하려고 하면 오류가 발생합니다.
한 번 끊긴 연결을 자동으로 복구하거나, 여러 요청을 효율적으로 처리할 수 있는 연결 풀(Connection Pool)을 사용하지 않음으로 인해 다수의 동시 요청을 처리할 때 비효율적입니다. 각 요청마다 연결을 확인하거나 새로운 연결을 맺어야 할 수도 있습니다.
const express = require('express');
const mysql = require('mysql2');
const app = express();
// MySQL 연결 풀 설정 (올바른 방식)
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'my_database',
waitForConnections: true,
connectionLimit: 10, // 동시에 허용되는 최대 연결 수
queueLimit: 0
});
// 올바른 방식의 라우터 예시
app.get('/api/get-users', (req, res) => {
const query = "SELECT * FROM users";
// MySQL 풀에서 연결을 가져와 쿼리 실행
pool.query(query, (error, results) => {
if (error) {
console.error('Error fetching users:', error);
res.status(500).send("Error fetching users.");
return;
}
res.status(200).json(results);
});
});
// 서버 실행
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
방금 위에서 문제가 있는 코드로 예시를 들었던 방식을 이렇게 고치면 해결이 됩니다.
const pool 을 만들고, 이를 이용해서 mysql DB 접근을 실행하는 방식인 겁니다.
고치긴 해야겠습니다.
그리고 고칠 때, 또 한 가지를 더 정리하려 합니다.
저는 하나의 파일 내부에 라우터를 잔뜩 생성했는데… 이 정리 방식은 가독성이 매우 좋지 않고 효율적이지 못합니다.
또한 유지 및 보수를 할 때, 작동하고 있던 백엔드를 종료했다가 다시 켜야하는 번거로움이 있어서 이걸 고칠 겁니다.
// 필요한 모듈들 불러오기
const express = require("express");
const cors = require("cors");
const session = require("express-session");
const path = require("path");
require("dotenv").config(); // .env 파일을 통해 환경 변수를 로드
// 라우터 모듈들 불러오기
const authRouter = require("./routes/auth");
const projectRouter = require("./routes/project");
const dataRouter = require("./routes/data");
// Express 앱 생성
const app = express();
const port = process.env.PORT || 5000; // 포트를 환경 변수로 설정, 기본값은 5000
// CORS 설정 (클라이언트와 서버 간의 통신을 허용)
app.use(cors());
// JSON 형태의 요청을 처리할 수 있도록 설정
app.use(express.json());
// 정적 파일 제공 (예: 이미지, 파일 등)
app.use("/data", express.static(path.join(__dirname, "data")));
// 세션 설정
app.use(
session({
secret: process.env.SESSION_SECRET || "your_secret_key", // 세션 암호화 키
resave: false,
saveUninitialized: false,
cookie: { secure: false, httpOnly: true }, // 쿠키 설정
})
);
// 라우터 설정
// /api/auth 경로로 authRouter에서 처리
app.use("/api/auth", authRouter);
// /api/project 경로로 projectRouter에서 처리
app.use("/api/project", projectRouter);
// /api/data 경로로 dataRouter에서 처리
app.use("/api/data", dataRouter);
// 서버 실행
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
바로 이런 식으로 고칠 것입니다.
backend.js 가 있는 파일 위치에 “routes” 라는 폴더를 만들고 그 폴더 내부에 여러 라우터 파일들을 만드는 거죠.
그리고 app.use 으로 처리를 하는 겁니다.
그렇다면 DB 정보를 처리하기 위해서 어떻게 하면 좋을까요?
// config/db.js
const mysql = require("mysql2");
// MySQL 연결 풀 설정
const pool = mysql.createPool({
host: process.env.DB_HOST, // 환경변수로부터 DB 호스트 정보 가져옴
user: process.env.DB_USER, // 환경변수로부터 DB 사용자명 가져옴
password: process.env.DB_PASS, // 환경변수로부터 DB 비밀번호 가져옴
database: process.env.DB_NAME, // 환경변수로부터 사용할 DB 이름 가져옴
waitForConnections: true, // 연결이 부족할 경우 대기하도록 설정
connectionLimit: 10, // 동시에 허용할 최대 연결 수 (여기서 10으로 설정)
queueLimit: 0 // 대기열 제한 없음 (무제한 대기)
});
// Promise 기반의 pool을 내보내기
module.exports = pool.promise();
저는 위와 같은 코드를 Backend.js 파일이 위치한 곳에 “config” 라는 폴더를 만들고 안에 db.js 를 생성하여 넣었습니다.
그럼 이제 우리는 기존에 connection.query 를 이용하던 라우터들을 다음과 같이 고치면 됩니다.
const express = require("express");
const router = express.Router();
const bcrypt = require("bcrypt");
const pool = require("../config/db"); // DB 연결 풀을 불러옴
// 로그인 라우터
router.post("/login", async (req, res) => {
const { id, password } = req.body;
try {
// DB에서 사용자 정보 조회
const [results] = await pool.query("SELECT * FROM table WHERE id = ?", [
id,
]);
if (results.length > 0) {
const user = results[0];
// 비밀번호 검증
const match = await bcrypt.compare(password, user.password);
if (match) {
res.status(200).send({
message: "로그인 성공",
user: { id: user.id, email: user.email },
});
} else {
res.status(401).json({ message: "Invalid credentials." });
}
} else {
res.status(404).json({ message: "Corresponding ID does not exist." });
}
} catch (error) {
res.status(500).send("로그인 처리 중 오류가 발생했습니다.");
}
});
module.exports = router;
포인트는 경로를 통해 db.js 를 불러오고, pool을 활용한다는 점입니다.
이제 훨씬 더 깔끔하게 라우터를 관리하며, DB 끊길 걱정은 없을 것 같네요.
추가적으로 기존에 사용하던 nohup node backend.js & 방식 말고 다른 방식으로 백엔드 관리를 합시다.
npm install pm2 -g
전역으로 pm2를 설치합니다.
pm2 start backend.js
위와 같이 pm2 로 특정 백엔드 파일을 실행 가능합니다.
pm2 list
다음의 방법으로 실해오디고 있는 파일들의 id 들을 확인 가능합니다.
pm2 logs
백엔드 디버깅을 위해 console.log 해놓은 것들은 이렇게 볼 수 있습니다.
pm2 save
pm2 startup
위 두 명령어를 차례대로 입력하여 지속적으로 pm2 실행이 가능합니다.
pm2 stop <프로세스 ID 또는 이름>
실행되던 특정 작업을 끄고 싶으면 위와 같이 실행하면 됩니다. pm2 list 를 통해서 확인 후 실행하면 되겠죠?
pm2 kill
아예 종료하고 싶으면 이렇게 pm2를 끄면 됩니다.
이제 조금 더 효율적으로 백엔드를 관리 가능하지 않을까 싶습니다.^^