Node.js로 대용량 데이터 다루기
Node.js 대용량 데이터 처리 과제
Node.js를 사용해 서버를 구축하는 경우, 비동기 이벤트 기반 아키텍처 덕분에 대규모 트래픽 처리가 용이하지만, 데이터가 방대해질수록 여러 가지 문제에 직면할 수 있다. 대용량 데이터와 높은 트래픽 상황에서는 메모리 부족, 응답 지연, CPU 부하 등의 문제가 발생할 수 있다. 이런 문제를 해결하기 위해 Node.js를 어떻게 최적화할 수 있을까?
스트리밍 데이터 처리
대용량 파일이나 데이터를 처리할 때는 모든 데이터를 한 번에 메모리에 올리는 대신, 스트림(Stream) 을 사용해 청크 단위로 처리하는 것이 좋다. Node.js의 스트림 모듈은 데이터를 작은 조각으로 나누어 처리하여 메모리 사용을 줄이고, 효율적인 데이터 처리를 가능하게 한다.
다음은 거대한 CSV파일을 읽는 예시 코드이다.
const fs = require("fs");
const readStream = fs.createReadStream("largefile.csv");
readStream.on("data", (chunk) => {
console.log("Reading a chunk of data:", chunk.length);
});
readStream.on("end", () => {
console.log("Finished reading the file");
});
readStream.on("error", (err) => {
console.error("Error while reading file:", err);
});
큰 파일을 한 번에 메모리에 로드하는 대신, 작은 청크 단위로 데이터를 읽고 처리함으로써 메모리 사용을 최소화할 수 있다.
data
: 이 이벤트는 파일에서 데이터를 읽을 때 발생한다. data 이벤트가 발생할 때마다 파일에서 읽은 데이터를 청크(Chunk) 라고 한다. 따라서 청크는 파일의 일부를 의미한다.
- 코드에서
chunk
는 파일의 일부 데이터를 의미하며,chunk.length
를 통해 각 청크의 길이를 확인한다.
end
: 이 이벤트는 파일을 모두 읽은 후에 발생한다. 즉, 파일의 모든 청크가 성공적으로 처리된 후에 end 이벤트가 발생하여 파일 읽기가 완료되었음을 알린다. 이를 통해 파일 읽기 작업이 정상적으로 완료되었는지 알 수 있다.
error
: 이 이벤트는 파일을 읽는 도중에 에러가 발생했을 때 호출된다.
이러한 스트리밍 방식은 네트워크 처리나 파일 읽기/쓰기에서 많이 사용되며 메모리 사용을 최소화하고 더 많은 양의 데이터를 효율적으로 처리할 수 있게 해준다.
데이터베이스 접근 최적화
대용량 데이터를 다룰 때는 데이터베이스 접근 또한 중요한 부분이다. Node.js에서는 커넥션 풀링(Connection Pooling) 을 통해 데이터베이스 연결을 효율적으로 관리할 수 있다.
커넥션 풀링
란, 데이터베이스 연결을 효율적으로 관리하기 위해 사용되는 기법이다. 일반적으로 애플리케이션이 데이터베이스와 통신하려면 연결이 필요하다. 매번 새로운 연결을 생성하는 것은 시간이 오래 걸리고 리소스를 많이 소모하기 때문에 비효율적이라고 할 수 있다.
이런 문제를 해결하기 위해 커넥션 풀링은 미리 일정 수의 데이터베이스 연결을 만들어 풀(pool) 에 저장해 놓는다. 애플리케이션이 데이터베이스 연결이 필요할 때마다 새로 연결을 생성하지 않고, 이 풀에서 연결을 가져와 재사용한다. 사용이 끝난 연결은 다시 풀에 반환되어 다른 요청에 사용된다. 이렇게 하면 연결 생성과 종료에 걸리는 시간을 줄일 수 있고, 데이터베이스에 가해지는 부하도 감소시킬 수 있다.
또한 쿼리 최적화도 성능 향상의 핵심 요소이다. 아래 코드는 커넥션 풀링을 사용해 데이터베이스에 효율적으로 접근하는 예시 코드이다.
const mysql = require("mysql2");
const pool = mysql.createPool({
host: "localhost",
user: "root", // DB계정명
password: "password", //비밀번호
database: "mydatabase", //데이터베이스명
waitForConnections: true,
connectionLimit: 10, // 커넥션 풀이 몇 개의 커넥션을 가질 지
queueLimit: 0,
});
pool.query("SELECT * FROM large_table WHERE condition = ?", ["value"], (err, results) => {
if (err) {
console.error("Database query error:", err);
return;
}
console.log("Query results:", results);
});
createPool
: 데이터베이스 연결을 관리하기 위해 풀을 생성한다.
connectionLimit
: 한 번에 몇 개의 연결을 유지할 것인지를 정한다.waitForConnections
: 풀이 가득 찼을 때 요청이 대기할지를 설정한다. true로 설정하면, 연결이 가능해질 때까지 기다리게 된다.pool.query()
: 쿼리를 실행할 때 매번 새로운 연결을 생성하는 것이 아니라 풀에서 연결을 가져와 실행한다. 작업이 끝나면 연결은 다시 풀로 반환되어 다른 요청에 사용된다.
캐싱을 통한 성능 향상
캐싱(Caching) 은 대용량 데이터를 처리할 때 서버 부하를 줄이는 효과적인 방법이다. Redis와 같은 인메모리 데이터베이스를 사용해 자주 요청되는 데이터를 캐싱하면 데이터베이스 접근 횟수를 줄여 성능을 향상시킬 수 있다. 특히, 정적 데이터나 자주 변경되지 않는 데이터는 캐싱을 통해 빠르게 제공할 수 있다.
캐싱의 원리
캐싱은 데이터를 미리 저장해 두는 일종의 “임시 저장소”라고 생각할 수 있다. 사용자가 특정 데이터를 요청하면, 서버는 먼저 캐시를 확인하여 해당 데이터가 있는지 확인한다. 캐시에 데이터가 있으면 데이터베이스를 다시 쿼리하지 않고도 빠르게 결화를 반환할 수 있다. 이를 캐시 히트(Cache Hit) 라고 한다.
만약, 캐시에 데이터가 없다면 데이터베이스나 원본 데이터 소스에 데이터를 가져와 사용자가 요청한 작업을 수행한 뒤, 그 결과를 캐시에 저장한다. 이를 캐시 미스(Chache Miss) 라고 하며, 이후에 동일한 데이터 요청이 있을 때는 캐시에서 빠르게 제공할 수 있다.
Redis를 이용한 캐싱
Redis는 대표적인 인메모리 데이터베이스로, 주로 캐싱을 위해 많이 사용된다. Redis는 데이터가 메모리에 저장되기 때문에 매우 빠른 속도로 데이터를 읽고 쓸 수 있다. 아래 코드는 Redis를 이용해 캐싱을 구현한 예시 코드이다.
const redis = require("redis");
const client = redis.createClient();
client.on("error", (err) => {
console.error("Redis client error:", err);
});
const getCachedData = (key, callback) => {
client.get(key, (err, data) => {
if (err) {
console.error("Error fetching from cache:", err);
return callback(err);
}
if (data) {
console.log("Cache hit:", key);
return callback(null, JSON.parse(data));
}
console.log("Cache miss:", key);
// 캐시에 데이터가 없을 때 데이터베이스에서 데이터를 가져온 후 캐시에 저장
// 예: db.getData(key, (err, dbData) => {...})
callback(null, null);
});
};
cient.get(key, value)
: Redis에서 특정 키(key)를 통해 데이터를 검색한다.
- 캐시 히트 : 요청한 키에 해당하는 데이터가 Redis에 있을 경우
data
로 반환한다. - 캐시 미스 : 요청한 데이터가 Redis에 없을 경우 데이터베이스에서 데이터를 가져온 후 Redis에 저장한다.
Leave a comment