매주 약 18만 개의 숫자 조합을 생성하고 DB에 저장해야 하는 업무가 있다.
Kotlin으로 짜인 앱에서 숫자 조합을 생성 요청을 보내면 서버에 있는 PHP 코드가 해당 로직을 확인한 뒤 숫자 조합을 생성하고 DB에 저장하게 된다.
하지만 요구사항에 실시간으로 스크래핑한 후 몇 가지 알고리즘 조건을 지켜야 했기에 18만 개의 조합을 모두 DB에 저장되기까지 약 4시간 정도의 긴 시간이 걸렸다.
앞으로 계속 사용해야 할 코드이기 때문에 Javascript로 컨버팅하기로 했다.
1. cheerio
https://cheerio.js.org/docs/intro
제일 먼저 웹 스크래핑을 위한 라이브러리를 찾아보았다. 그 중 Node 환경에서 사용 가능한 cheerio 라이브러리를 확인했고, url에서 HTML을 가져와 특정 태그의 ID 값으로 value를 가져오는 방식으로 스크래핑했다.
const cheerio = require('cheerio');
const html = '<div class="content"><h1>Hello, Cheerio!</h1><p>This is an example HTML document.</p></div>';
const $ = cheerio.load(html);
const contentText = $('.content').text();
console.log(contentText);
// Hello, Cheerio!This is an example HTML document.
위의 예시에선 html에 직접 하드코딩 하였지만 실제론 아래 방식으로 url을 통해 가져오게 된다.
// async
const response = await axios.get(url);
const html = response.data;
const $ = cheerio.load(html);
https://github.com/cheeriojs/cheerio
2. mysql2
mysql은 Node에서 RDB중 하나인 MYSql과 연결 및 쿼리를 주고받을 수 있는 라이브러리인데, mysql에서 부족한 성능을 향상하기 위해 기본적으로 비동기 기능을 추가하고 Promise 문법을 사용 가능하게 한 다음 버전이 mysql2이다.
mysql2가 나온지 10년이 되어가기 때문에 mysql 라이브러리를 사용해 본 적이 없기 때문에 직접 비교는 불가능할 것 같다.
https://www.npmjs.com/package/mysql2
import mysql from 'mysql2/promise';
try {
// DB 연결을 위해 생성
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'test',
});
// prepare와 query를 호출
const [results, fields] = await connection.execute(
'SELECT * FROM `table` WHERE `name` = ? AND `age` > ?',
['Rick C-137', 53]
);
console.log(results);
console.log(fields);
} catch (err) {
console.log(err);
}
mysql2 공식 문서에 있는 Promise 사용 예제
3. pool
connection pool을 이해하기 위해 아래 글을 참고하였다.
https://velog.io/@isntkyu/Nodejs-mysql-vs-mysql2-Connection-Pool
Connection pools help reduce the time spent connecting to the MySQL server by reusing a previous connection, leaving them open instead of closing when you are done with them.
This improves the latency of queries as you avoid all of the overhead that comes with establishing a new connection.
연결 풀은 이전 연결을 재사용하여 MySQL 서버에 연결하는 데 소요되는 시간을 줄이는 데 도움이 되며, 작업이 끝났을 때 연결을 닫는 대신 열어 두는 데 도움이 됩니다. 이렇게 하면 새 연결을 설정할 때 발생하는 모든 오버헤드를 피할 수 있으므로 쿼리 대기 시간이 향상됩니다.
https://sidorares.github.io/node-mysql2/docs#using-connection-pools
const mysql = require('mysql2/promise');
// Connection pool 설정
const pool = mysql.createPool({
host: 'localhost', // 데이터베이스 호스트
user: 'yourusername', // 데이터베이스 사용자 이름
database: 'yourdatabase', // 사용할 데이터베이스 이름
waitForConnections: true,
connectionLimit: 10, // 최대 연결 수
queueLimit: 0
});
async function queryDatabase() {
let conn;
try {
// Pool로부터 연결을 가져옵니다.
conn = await pool.getConnection();
// 쿼리 실행
const [rows, fields] = await conn.query('SELECT * FROM yourtable');
// 결과 출력
console.log(rows);
} catch (err) {
console.error('데이터베이스 쿼리 중 오류 발생:', err);
} finally {
// 연결을 pool로 반환
if (conn) conn.release();
}
}
connection pool을 사용하면 DB에 대한 연결을 만들고 해제하는 번거로움을 줄일 수 있고 동시에 여러 연결을 효율적으로 관리할 수 있기에 성능을 향상할 수 있다.
동일한 DB에 여러 요청이 발생할 경우 생길 수 있는 문제를 해결하기 위한 기능인 것 같다.
connection pool 사용 장점
- 성능 향상 : DB와 연결을 재사용할 수 있기 때문에 매번 연결을 생성하고 해제하지 않아도 됨
- 동시성 관리 : 여러 클라이언트 요청에 대해 동시에 관리 가능
- 자원 관리 : 사용이 완료된 연결을 다시 pool에 반환하여 자원 최적으로 활용
- 대기 시간 최소화 : 이미 생성된 연결을 사용하기 때문에 요청이 처리될 때까지 새로운 연결을 기다리지 않아도 됨
- 안정성 향상 : 연결 오류를 자동으로 처리하고 새로운 연결 생성
- 성능 튜닝 : 최대 연결 수, 대기열 크기 등 설정 가능
4. 결과
PHP 코드를 Node.js로 컨버팅/리펙터링 후 데이터를 실제로 출력해 봤을 때 생각보다 더 많은 성능 개선을 느낄 수 있었다.
기존에 약 40분 ~ 60분 정도 걸리던 작업이 약 2분 ~ 4분으로 단축되었다. 아마 스크래핑 과정에선 큰 차이가 없고 Node와 MYSql 연결 시 사용했던 connection pool 그리고 Node의 논 블로킹 IO 비동기 방식이 큰 차이를 가져온 것 같다.
DB에 연결 후 데이터를 주고 받을 때 조건에 맞는 알고리즘 유효성 확인 작업이 끊기지 않고 계속 처리되었기에 많은 시간을 단축할 수 있었다.
https://nodejs.org/en/learn/asynchronous-work/overview-of-blocking-vs-non-blocking