요청마다 DB 연결을 새로 맺던 코드를 커넥션 풀로 갈아치웠다
createConnection 콜백에서 mysql2 풀로
#MySQL #Node #백엔드 #리팩토링
어느 날 사이트가 통째로 먹통이 됐다. 로그를 보니 DB가 Too many connections 를 뱉고 있었다. 연결이 안 닫힌 채로 계속 쌓여서 한도를 넘긴 거다. 범인은 내가 짠 패턴이었다 — 요청이 들어올 때마다 DB 연결을 새로 만들고 있었으니까. 이 글에선 요청마다 연결을 새로 맺던 콜백 방식 을 커넥션 풀 로 바꾸면서 뭘 배웠는지 정리해본다. 기존 방식의 문제 예전 코드는 라우터마다 이 패턴이 반복됐다. app.post('/something', (req, res) = { const connection = mysql.createConnection(db_info); connection.connect(); connection.query(sql, params, (err, results) = { if (err) { connection.end(); return res.status(500).send('error'); } res.json(results); connection.end(); }); }); 문제가 여러 개였다. 1. 요청마다 새 연결 — TCP 연결을 맺고 끊는 비용이 매 요청마다 든다. 2. 커넥션 누수 — 에러 분기마다 end()를 빼먹으면 연결이 안 닫힌 채 쌓인다. 위에서 사이트가 죽은 게 딱 이거였다. 분기 하나에서 end를 빠뜨려서 연결이 줄줄 샜다. 3. 콜백 지옥 — 쿼리 안에 쿼리가 중첩되면 코드가 오른쪽으로 끝없이 밀려난다. 커넥션 풀로 바꾸기 해결은 mysql2/promise의 커넥션 풀 이었다. 풀은 미리 연결을 몇 개 만들어두고 돌려쓴다. 요청 오면 풀에서 하나 빌려 쓰고, 끝나면 자동 반납. 매번 새로 연결하지 않는다. 공용 풀을 파일 하나에 만들어두고 전 라우터가 공유하게 했다. // db/pool.js const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, connectionLimit: 10, // 최대 10개 연결을 돌려씀 waitForConnections: true, queueLimit: 0, }); module.exports = pool; 그러면 라우터 코드는 이렇게 짧아진다. connect도, end도 없다. 그냥 빌려 쓰고 알아서 반납된다. const pool = require('./pool'); app.post('/something', async (req, res) = { try { const [rows] = await pool.query(sql, params); res.json(rows); } catch (err) { res.status(500).send('error'); } }); async/await로 콜백 지옥도 정리 promise 기반으로 바꾸니까 콜백 중첩이 자연스럽게 사라졌다. 쿼리를 여러 번 날려야 할 때도 await로 위에서 아래로 쭉 읽힌다. 트랜잭션 필요한 데만 pool.getConnection()으로 연결 하나 잡아서 처리했다. 제일 큰 건 end()를 빼먹을 일 자체가 없어지니까 커넥션 누수 걱정이 통째로 사라진 거다. 실수할 여지를 구조적으로 없앤 셈이다. 그 뒤로 Too many connections는 다시 안 봤다. 정리하면 요청마다 연결을 새로 맺는 건 처음엔 문제없어 보이지만, 트래픽이 늘면 연결 비용이랑 누수가 발목을 잡는다. 커넥션 풀은 이걸 "미리 만들어두고 돌려쓴다"는 단순한 아이디어로 해결한다. 지금은 모든 프로젝트를 db/pool.js 하나 만들어두고 시작한다. 별거 아닌 것 같아도 이 패턴 하나로 성능이랑 안정성을 동시에 챙길 수 있어서 안 쓸 이유가 없다.