본문 바로가기
카테고리 없음

Express DB 관련 최적화

by 잘먹는 개발자 에단 2025. 7. 25.

1. 커넥션 풀을 매번 연결하지 말자. 

 

- 매 요청마다 데이터베이스에 새로 연결하고 끊는 작업 connect - query - close는 매우 비싼 비용을 소모한다. 사용자가 몰리게 되면 이는 서버와 데이터베이스에 엄청난 부하를 준다.

- 때문에 연결을 미리 만들어두고 풀 pool에 보관한다. 요청이 들어오면 풀에서 노는 연결을 하나 빌려쓰고, 작업이 끝나면 다시 풀에 반납하여 재사용한다. 

- 대부분의 db라이브러리 mysql2, pg 등 ...에서 풀 기능을 기본으로 제공한다. 

// 👎 나쁜 예: 매번 새로 연결
const mysql = require('mysql2');

app.get('/users/:id', (req, res) => {
  const connection = mysql.createConnection({ host: '...', user: '...', database: '...' }); // 비효율!
  connection.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, results) => {
    // ...
  });
  connection.end(); // 비효율!
});


// 👍 좋은 예: 커넥션 풀 사용
const mysql = require('mysql2/promise'); // promise 기반 라이브러리가 async/await와 잘 맞습니다.

// 1. 서버 시작 시 한 번만 풀을 생성합니다.
const pool = mysql.createPool({
  host: '...',
  user: '...',
  database: '...',
  waitForConnections: true,
  connectionLimit: 10, // 풀에 유지할 최대 커넥션 수
  queueLimit: 0
});

// 2. 요청 시 풀에서 커넥션을 빌려와 사용합니다.
app.get('/users/:id', async (req, res) => {
  try {
    const connection = await pool.getConnection(); // 풀에서 커넥션 획득
    const [rows] = await connection.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
    connection.release(); //  ❗️연결을 끊는게 아니라 풀에 반납(release)합니다.
    res.json(rows[0]);
  } catch (err) {
    // ...에러 처리
  }
});

- 중요한 것은 connection.end()가 아니라 connection.release()를 사용해서 풀에 꼭 반납해야한다. 

 

 

2. 레포지토리 패턴  "역할을 나누자"

 

- 코드가 복잡해질수록 가장 효과적인 아키텍처 패턴이다. 각 코드의 역할을 명확하게 분리한다. 

- 라우터 핸들러 ( (req,res)=>{} ) 안에 비즈니스 로직과 데이터베이스 접근 코드가 모두 섞여있으면 코드가 길어지고 재사용이 불가능하며, 테스트도 어려워진다. 때문에 코드를 3개의 계층으로 나눈다.

  ㄴ 1. Controller ( or Router ) : 오직 req와 res만 처리한다. 요청을 받고, 응답을 보내는 역할에만 집중한다.

  ㄴ 2. Service : 비즈니스 로직을 처리한다. "사용자를 탈퇴시킬 때, 게시글도 지우고 댓글도 비활성화 해야한다"와 같은 실제 서비스 규칙을 코드로 구현한다. 데이터베이스에 직접 접근하지 않고, 레포지토리를 호출한다.

  ㄴ 3. Repository ( or DAO ) : 데이터베이스 접근만 담당한다. 순수하게 데이터를 읽고 쓰는 CRUD 코드만 존재한다. 

 

* 폴더구조를 다음과 같이 적용한다.

/src
  ├── controllers/
  │   └── user.controller.js  # 1. 컨트롤러
  ├── services/
  │   └── user.service.js     # 2. 서비스
  └── repositories/
      └── user.repository.js  # 3. 리포지토리

 

함수형으로 짜면 ** 데이터 접근함수들이 담긴 객체를 반환하는 팩토리함수 ** 가 필요하다. 반환된 함수들은 클로저를 통해 외부에서 주입받은 의존성에 접근할 수 있다. this를 사용하지 않아도 상태를 공유할 수 있다. 

 

- 1. user.repository.js (데이터접근)  ( 팩토리 함수 : createUserRepository )

const dbpool = require('../db'); // 커넥션 풀 가져오기

// '팩토리 함수' : 레포지토리 함수 객체를 생성해서 반환한다. 
const createUserRepository = pool => {
	const findById = aysnc(id) => {
    	const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
        return rows[0];
    };
    
    const create = async (userInfo) => {
    	// 사용자 생성 쿼리
    };
    
    // 레포지토리의 실제 내용물인 함수들의 객체를 반환
    return {
    	findById,
        create
    };
};


// 팩토리 함수를 실행하여 실제 사용할 레포지토리 모듈을 생성하고 내부낸다.
const userRepository = createUserRepository(dbpool);

module.exports = useRepository;

 

2. user.service.js ( 비즈니스 로직 )

const userRepository = require('../repositories/user.repository');

const getUserInfo = async(id)=>{
	const user = await userRepository.findById(id);
    
    if(!user){
    	throw new Error('사용자를 찾을 수 없습니다');
    }
    return { id : user.id, name : user.name };
}

// 서비스 함수들을 객체로 묶어서 내보낸다.
module.exports = {
	getUserInfo
}

 

3. user.controller.js ( 요청/응답 처리 )

- 서비스 계층과 마찬가지로 코드가 거의 동일하다.

- ** asyncHandler는 express 5 버전부터 내장되어있다. 설명은 하단에

const userService = require('../services/user.service');
const asyncHandler = require('../utils/async-handler');

const getUser = asyncHandler(async(req, res) =>{
	const user = await userService.getUserInfo(req.params.id);
    res.json(user);
});

module.exports = {
	getUser
}

 

 

 

 

********

1. asyncHandler

 

- Express에서 비동기 async/await 로직의 에러를 쉽게 처리하기 위한 헬퍼 함수이다. 쉽게말해서 라우터마다 반복적으로 써야하는 try...catch 구문을 없애주는 역할을 한다. 

 

- 왜 필요한가? Express의 비동기 라우터 핸들러 안에서 에러가 발생하면, 그 에러는 자동으로 Express의 중앙 에러 처리기로 넘어가지 않는다. 그래서 개발자가 직접 에러를 잡아서 넘겨줘야 한다.

 

- 만약에 asyncHandler가 없으면 모든 비동기 함수마다 try..catch를 쓰고, catch 블록 안에서 next(error)를 직접 호출해야한다. 

 

- asyncHandler는 이런 반복작업을 대신해주는 **함수를 반환하는 함수 ( 고차함수 ) **이다.

 

- 구현

asyncHandler는 비동기 로직이 담긴 함수 requestHandler를 인자로 받는다. 그리고 그 함수를 실행한 뒤에 에러가 발생하면 .catch(next)를 통해 알아서 에러를 그 다음 미들웨어로 넘겨주는 새로운 함수를 반환한다. 

 

 

asyncHandler의 구현 ( express 5버전부터는 내장되어있음 ) 

// asyncHandler의 실제 코드 (매우 간단합니다)
const asyncHandler = (requestHandler) => {
  return (req, res, next) => {
    // requestHandler가 반환하는 프로미스(Promise)에 .catch(next)를 붙여서 에러를 잡습니다.
    Promise.resolve(requestHandler(req, res, next)).catch(next);
  }
}

그러면 이제 라우터에서는 try...catch를 완전히 제거하고 비즈니스 로직에만 집중할 수 있다. 

 

- Express 5 이상 버전의 코드 

// 아무런 래퍼(wrapper) 함수 없이 async/await만 쓰면 됩니다.
app.get('/users/:id', async (req, res, next) => {
  const user = await userService.findUser(req.params.id);
  if (!user) {
    const error = new Error('사용자를 찾지 못했습니다.');
    error.status = 404;
    throw error; // 에러를 던지면 Express 5가 자동으로 처리합니다.
  }
  res.json(user);
});