Node.js 콜백 지옥 탈출기 - Promise 패턴 적용

문제 상황

담당하고 있는 Node.js 6 기반 API 서버의 사용자 인증 로직이 콜백 중첩으로 인해 유지보수가 어려웠다. 데이터베이스 조회, 외부 API 호출, 세션 저장이 순차적으로 이루어지면서 들여쓰기가 5단계까지 깊어진 상태였다.

app.post('/login', (req, res) => {
  db.findUser(req.body.email, (err, user) => {
    if (err) return res.status(500).send(err);
    validatePassword(user, req.body.password, (err, valid) => {
      if (err) return res.status(500).send(err);
      if (!valid) return res.status(401).send('Invalid');
      fetchUserProfile(user.id, (err, profile) => {
        if (err) return res.status(500).send(err);
        saveSession(user.id, (err, session) => {
          if (err) return res.status(500).send(err);
          res.json({ user, profile, session });
        });
      });
    });
  });
});

Promise 패턴 적용

Node.js 8이 곧 출시될 예정이지만, 현재 환경에서는 Bluebird를 사용해 콜백 기반 함수들을 Promise로 변환했다.

const Promise = require('bluebird');
const db = Promise.promisifyAll(require('./db'));

app.post('/login', (req, res) => {
  let userData;
  db.findUserAsync(req.body.email)
    .then(user => {
      userData = user;
      return validatePasswordAsync(user, req.body.password);
    })
    .then(valid => {
      if (!valid) throw new Error('Invalid password');
      return fetchUserProfileAsync(userData.id);
    })
    .then(profile => {
      return saveSessionAsync(userData.id)
        .then(session => ({ user: userData, profile, session }));
    })
    .then(result => res.json(result))
    .catch(err => {
      if (err.message === 'Invalid password') {
        return res.status(401).send(err.message);
      }
      res.status(500).send(err);
    });
});

결과

에러 핸들링을 catch 블록 하나로 통합할 수 있었고, 비즈니스 로직의 흐름이 명확해졌다. 팀원들과 코드 리뷰에서 긍정적인 반응을 얻어 다른 API 엔드포인트에도 순차적으로 적용할 예정이다.

ES2017의 async/await가 표준화되면 더 개선할 수 있을 것 같다.