node.js 에러 핸들링 - 아키텍쳐의 빈 구멍 메꾸기

Seonkyu Kim·2020년 2월 5일
19
post-thumbnail

본 글은 Sam Quinn의 “Error handling - The missing piece of your node.js architecture” 글을 번역한 것입니다.
Error handling - The missing piece of your node.js architecture
How do you handle errors? And what about the server logs?

Introduction

배포를 하는 것은 개발 워크플로우를 완전히 바꾸어 놓을 것입니다. 삶에 스트레스가 증가하며, 유지∙보수를 위해 개발하는 시간은 없어질 것입니다. 이는 마치 육아를 하는 것과 비슷하죠.

기분을 나쁘게 하려는 의도는 아닙니다. 대신, 지난 날의 고통스러웠던 배포 경험들을 토대로 몇 가지 팁을 드리고자 합니다. 이 게시물이 유용하고, 시간 가는 줄 모르셨다면, @santypk4로 여러분의 생각을 알려주시기 바랍니다.

Table of contents

  • Error handling 🚧
  • 로그의 중요성 📝
  • 결론 🏗️
  • 예제 🔬

Error handling in Node.js 🚧

사용자가 마지막으로 버그를 신고한 적이 언제입니까? 그 에러가 치명적이었거나 사용하던 서비스가 계속 필요했을 것입니다. 하지만 대부분의 에러가 발생할 경우에 개발자들은 그것을 알아차릴 수 없습니다.

사용자들은 일반적으로 버그 제보를 하지 않습니다. 왜냐하면 지저분한 폼 양식에 너무나 많은 세부사항을 요구하지만 제대로 된 답변을 잘 받지 못하기 때문입니다.

제가 가장 최근에 마주한 버그는, 최신 AI를 기반으로 관심사에 맞는 컨텐츠를 자동으로 추천해주는 트위터의 새로운 SaaS 제품을 시험해보고 있었을 때입니다. 랜딩 페이지에서 즉시 제품을 구매했습니다. 하지만, 운이 좋게도, 앱이 완전히 멈추었고, 잘못 입력한 어떤 데이터 때문에 구매 요청이 완료되지 않았습니다. 트위터 개발자에게 연락을 했고, 답변은 절대 오지 않았습니다.

그 달의 마지막 날까지, 그들은 결코 작동하지 않았던 SaaS에 대해 구독 비용을 청구하고 싶어했습니다.

저는 아직도 이런 서비스에 관심이 많습니다.

이와 같은 버그를 만들지 말고, 사용자가 알아채기 전에 에러 로그를 꼭 남겨두세요.

에러를 처리하기 위한 안정적이고 신뢰할 수 있는 중앙 집중식 방법이 있어야 합니다.

이전 게시글의 3 계층 아키텍처를 사용하면서, 사용자의 검색 엔진이 작동하지 않기 시작했다고 해봅시다.

error handling 3-layer.jpg

여기서 중요한 점은 아래쪽 계층에서 에러를 처리하지 않고 controller 계층으로 throw하는 것입니다.

import UsefulError from '../utils/usefulError';
class UserService {
  constructor(
    private userSearchEngineService,
    private userThirdpartyService,
    private userDatabaseModel,
    private logger,
  )


  GetAll() {
    try {
      return this.userDatabaseModel.find();
    } catch(e) {
      throw new Error(`The database is dead!`, 503) 
    }
  }

  SearchUserByLocation(lat, long) {
    try {
      this.logger.silly('performing search...')
      return this.userSearchEngineService.searchByLocation(lat, long);
    } catch(e) {
      throw new Error(`The user search engine doesn't work!`, 503) 
    }
  }

  // Not related to something that happened to me 
  GetUsersFromThatThirdPartyServiceThatTheFounderMadeUsAssociateAndNeverWorkAndSeemLikeOurFault() {
    try {
      return this.userThirdpartyService.find();
    } catch(e) {
      this.logger.silly('We should call Pablo')
      throw new Error(`The thirdparty api doesnt work!`, 500) 
    }
  }
}

이제 custom error class를 만들고 properties를 추가해줍시다.

class UsefulError extends Error {
  constructor(name, httpStatusCode = 500, context, ...params) {
    // Pass remaining arguments (including vendor specific ones) to parent constructor
    super(...params);

    // Maintains proper stack trace for where our error was thrown (only available on V8)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, UsefulError );
    }

    this.name = 'name';
    this.httpStatusCode = httpStatusCode;
    this.context = context; 
    this.date = new Date();
  }
}

에러로 사용자들을 당황하게 하지 마십시오. 사용자들에게 왜 요청이 실패했는지 솔직하게 말해주어 다른 행동을 취할 수 있게 해주십시오.

좋은 에러 메세지는 다음과 같습니다:

검색 엔진이 지금은 작동하지 않지만, 프로필 화면은 볼 수 있습니다.

import Logger from '../logger';
import UserService from '../services/user';

export default (app) => {
  app.get('/user/search-location', (req, res, next) => {
    try {
      const { lat, lng } = req.query;
      Logger.silly('Invoking user service to search by location')
      const users = UserService.SearchUserByLocation(lat, lng);
      return res.json(users).status(200);
    } catch(e) {
      Logger.warn('We fail!')
      return next(e);
    }
  })
}

controller 계층에서는 에러를 다음 express middleware로 넘겨주고, error handler로 최종적으로 모아줍시다.

import Logger from '../logger';
export default (err, req, res, next) => {
  Logger.error('Error %o', err);
  return res.json(err).status(err.httpStatusCode || 500);
}

이제는 다음 주제로 넘어가봅시다.

로그의 중요성 📝

모든 것을 console.logs로 채운 서버를 만드신 적이 있으십니까?

✋물론이죠.

그렇다면, 아무것도 log를 남기지 않는 서버를 만드신 적이 있으십니까?

✋이것도 물론이죠. 모든 것을 로그로 남기는 것보다 더 최악이었습니다.

이제부터 할 것은 두 개의 접근을 적당히 섞는 것입니다.

모든 것을 log로 남기지만, 모든 것이 log로 출력되지는 않을 것입니다😉

어떤 행동이 시작되려고 할 때, 어떤 행동이 시작되었을 때, 그리고 그 결과와 발생한 에러 모두를 log 기록해야 합니다.

error handling log.jpg

물론 로그들은 모두 다른 레벨을 갖고 있습니다.

error handling diff error level.jpg

앱을 배포했다면, 그리고 더 많은 정보들을 원한다면 환경변수를 통해 로그 레벨을 바꿔주면 됩니다.

error handling change level.jpg

console.log에서 winston으로의 migration 예시를 보고 싶으시다면 견고한 node.js 프로젝트의 다음 PR을 참고하시면 됩니다.

winston의 가장 좋은 점은 'transport' 계층을 정할 수 있어, 에러를 원하는 곳에서 확인할 수 있다는 점입니다.

저는 단순이 console에서 에러를 확인하게 해놨지만, 매우 쉽게 Sentry 혹은 Rollbar, 아니면 그 어떤 곳이든지 쉽게 플러그인을 통해 확인할 수 있습니다.

역시 어댑터 패턴은 최고입니다.

결론 🏗️

로그 파일이 넘쳐나는 console.log를 사용하는 대신, Winston과 같은 logger 라이브러리를 사용하고 단계에 따라 로그 레벨을 나눕십시오.

중앙 집중식 error handling 방식을 고민해보십시오. express에서는 middleware를 사용할 수 있습니다. 단지 에러들을 서버의 한 곳으로 전달해주면 됩니다.

이 짧은 포스트가 도움이 되셨기를 바랍니다. node.js의 더 많은 팁들을 원하신다면 견고한 node.js 프로젝트 설계하기를 꼭 확인해보시기 바랍니다.

예제 🔬

profile
hopsprings2ternal@gmail.com

1개의 댓글

comment-user-thumbnail
2020년 9월 13일

너무 잘 보고 갑니다.
감사합니다.

답글 달기