요청마다 DB 연결을 새로 맺던 코드를 커넥션 풀로 갈아치웠다
요청마다 DB 연결을 새로 맺다가 Too many connections로 사이트가 멈췄다. mysql2 커넥션 풀로 바꾼 코드와, 다시 세어보니 아직 남아 있던 createConnection까지 적었다.
#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(); }); }); 문제가 여러 개였다. 우선 요청마다 새 연결을 만드니까 TCP 연결을 맺고 끊는 비용이 매 요청마다 든다. MySQL 쪽에서도 연결 하나 받을 때마다 인증하고 세션을 만드는 일을 반복한다. 더 큰 문제는 커넥션 누수다. 에러 분기마다 end()를 빼먹으면 연결이 안 닫힌 채 쌓인다. 위에서 사이트가 죽은 게 딱 이거였다. 분기 하나에서 end를 빠뜨려서 연결이 계속 샜고, MySQL 기본 한도인 151개를 넘기는 순간 새 요청이 전부 거부됐다. 마지막은 콜백 지옥이다. 쿼리 안에 쿼리가 중첩되면 코드가 오른쪽으로 끝없이 밀려나고, 그 안에서 end()를 어디서 불러야 하는지 점점 헷갈린다. 누수가 생기는 이유가 결국 이 구조에 있었다. 커넥션 풀로 바꾸기 해결은 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; connectionLimit이 10이면 동시에 열리는 연결이 아무리 많아도 10개를 넘지 않는다. 11번째 요청은 waitForConnections 덕에 에러가 아니라 대기 상태가 됐다가 앞의 연결이 반납되면 이어받는다. 개인 서비스 트래픽이면 10개도 남는다. 그러면 라우터 코드는 이렇게 짧아진다. 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()으로 연결 하나를 잡아서 처리했는데, 이때는 finally에서 connection.release()를 꼭 불러줘야 한다. 풀에서 빌린 연결을 안 돌려주면 그것도 누수다. 제일 큰 건 end()를 빼먹을 일 자체가 없어지니까 커넥션 누수 걱정이 통째로 사라진 거다. 실수할 여지를 구조적으로 없앤 셈이다. 그 뒤로 Too many connections는 다시 안 봤다. 그렇게 안 하고 있었다 이 글을 다시 보다가 지금은 모든 프로젝트를 pool 만들어두고 시작한다고 써놓은 걸 봤다. 정말 그런가 싶어서 이 블로그 코드를 세어봤다. 2026년 9월 기준이다. $ grep -c createConnection db/*.js util/*.js db/board.js createConnection 17회 db/visit.js createConnection 7회 db/code.js createConnection 6회 db/user.js createConnection 3회 db/oauth.js createConnection 1회 util/seo.js createPool 1회 - 풀은 여기 하나뿐 나중에 만든 SEO 모듈에만 풀을 썼고, 정작 제일 자주 불리는 게시글 조회는 여전히 요청마다 연결을 새로 맺고 있었다. 글을 처음 쓸 때 셌던 것보다 오히려 한두 군데 늘어 있다. 그 사이에 방문자 통계와 GitHub 로그인을 붙이면서 기존 패턴을 그대로 복사한 거다. 글에 쓴 건 앞으로 그러겠다는 다짐이었지 이미 그러고 있다는 뜻은 아니었나 보다. 트래픽이 적어서 지금까지는 티가 안 났다. board.js가 쿼리 열일곱 군데를 들고 있어서, 한 번에 바꾸기보다 자주 불리는 조회부터 풀로 옮기고 나머지를 따라가게 할 생각이다. 옛날 코드에서 createConnection을 grep 해보면 자기가 어디까지 바꿨는지 금방 나오니, 풀로 옮겼다고 생각하는 프로젝트가 있다면 한 번 세어보길 추천한다.