haileyjpark

[소프트웨어 공학] 에러 및 로그 처리 방식 본문

소프트웨어 공학

[소프트웨어 공학] 에러 및 로그 처리 방식

개발하는 헤일리 2022. 10. 2. 15:19
반응형

팀에 배치되기 전 교육과정 커리큘럼을 진행하면서 아래와 같은 피드백을 받았었고, 피드백을 받았던 부분을 중심으로 에러와 로그 처리 방식에 대해 간단하게 알아보았다.

  • try-catch로 모든 레이어에서 처리해주는 것이 적절할지?
  • 로그는 어떻게 남기면 좋을지?

어느 시점에서 에러/예외처리를 할 것인가?

모든 레이어에서 try-catch문을 쓰지 않더라도, 에러가 발생하는 코드 다음에 있던 코드들은 실행되지 않고,

발생한 에러는 Promise.reject 처리되어 상위 컨텍스트에서 비동기 에러로 처리된다.

 

→ 서비스 레이어에서 상위 컨텍스트로 에러를 전파하기 위해 async 함수의 내부를 try-catch로 묶는 것은 불필요한 코드이다.

하지만, controller 레이어는 에러가 발생하거나 성공을 하는 것에 상관없이 클라이언트에게 정상적인 응답을 주어야 한다.

그래서 controller 레이어에서는 더이상 상위 컨텍스트로 에러를 전파하지 않고, 에러를 catch로 잡아서 에러 내용을 정리해 클라이언트에게 응답을 해주어야 한다.

 

즉, 어차피 서비스 레이어에서 try-catch를 사용해서 에러를 잡더라도 클라이언트에게 응답을 주기 위해 catch 블럭에서 상위로 error를 throw해야 한다.

이 때, try-catch문을 쓰지 않아도 상위로 에러가 전파되고, catch로 잡은 error에 대한 특정한 처리를 서비스 레이어에서 해줄 것이 아니라면 error를 잡아서 다시 던져주고 상위에서 또 받아서 다시 던져주는 것이 불필요하다.

 

또한, 하나의 메서드 콜 체인 안에서 같은 동일한 부분에 대한 예외 처리가 여러 메서드에서 이루어지고 있다면,

하나의 체인 안에는 하나만 남기는 것이 유지보수에 좋다.


어떻게 에러/예외처리를 할 것인가?

에러에는 Operational Error와 Programmer Error가 있다.

Operational Error는 런타임 문제를 나타내며, 응용 프로그램 자체에 버그가 있음을 의미하지 않고 적절하게 처리해주어야 한다. Programmer Error는 버그라고 표현되는 오류이며, 코드 자체의 문제를 나타낸다.

이렇게 두 가지로 에러를 분류해주어야 하는 이유는, 버그가 있는 경우에는 앱을 다시 시작해야하지만, user not found 에러의 경우에는 앱을 재시작 해야 할 필요가 없기 때문이다.

Operational Error

Operational Error는 적절한 수준으로 잘 핸들링 해주어야 하는데, 문자열로만 예외처리를 한다거나 임의로 정의한 타입으로 에러를 처리하게 되면 에러 처리 방식이 더 복잡해진다. 따라서 promise가 거부되었다거나, 예외로 처리하거나, 에러를 낸다던가 하는 경우의 수에 모두 내장된 Error 객체만 이용하는 것이 균일성을 향상시킨다.

 

일관된 Error객체를 사용하지 않을 경우, 어떤 에러의 형태가 반환될 지 불확실해져서 적절한 에러처리가 어렵고, 임의적인 타입으로 에러를 내게되면 stack 정보와 같은 중요한 에러 관련 정보가 손실될 수 있다.

 

Node.js에는 built-in Error인 ReferenceError, RangeError, TypeError, URIError, EvalError, SyntaxError 가 있다. 이를 제외하고도, Node.js의 base Error 객체를 extend해서 다음과 같이 커스텀 에러 객체를 만들어서 사용할 수 있다.

  • 예시
// 커스텀 에러 객체 생성
class CustomError extends Error {
  constructor(message) {
    super(message);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CustomError);
    }
    // this.name = this.constructor.name;
    this.message = message;
  }
}

// 에러 객체 throw
const createRental = async (data) => {
	if (!isValid.status) {
	    throw new CustomError('이 고객은 연체로 인해 현재 대출이 불가능합니다.');
	  }
	return createdSingleRental;
};

// 에러 발생시 처리
const errorHandler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.statusCode || err.status || 500;
    ctx.body = {
      message: err.message,
    };
    console.log(err.status, err.message);
  }
};

 

일반적으로 에러를 처리할 때는 가능한 코드 중복을 피하기 위해 중앙 집중식으로 오류를 처리하는 것이 좋다. 이 중앙 집중식 에러 핸들링 컴포넌트는 심각한 오류일 경우 시스템 관리자에게 알림을 보내고, Sentry.io와 같은 모니터링 서비스로 이벤트를 전송하고, 이를 로깅하는 역할을 한다.

 

에러 핸들링 미들웨어를 사용하면 오류 유형을 구분하고 중앙집중식 에러 핸들링 컴포넌트로 보낼 수 있다.

에러 처리 미들웨어는 지금 사용하고 있는 Koa의 공식문서에 있는 에러 처리 미들웨어 사용법을 보고 적용해볼 수 있다.

  • 참고한 블로그의 예시
// 에러 처리 미들웨어에서 에러를 구분하고 에러 핸들링 컴포넌트로 보낸다.
app.use(async (err, req, res, next) => {
 if (!errorHandler.isTrustedError(err)) {
   next(err);
 }
 await errorHandler.handleError(err);
});

// 에러 핸들링 컴포넌트에서 에러에 대한 작업을 해준다.
class ErrorHandler {
 public async handleError(err): Promise<void> {
   await logger.error(
     'Error message from the centralized error-handling component',
     err,
   );
   await sendMailToAdminIfCritical();
   await sendEventsToSentry();
 }
 
 public isTrustedError(error) {
   if (error instanceof BaseError) {
     return error.isOperational;
   }
   return false;
 }
}
export const errorHandler = new ErrorHandler();

 

Programmer Error

Programmer Error는 메모리 누수 또는 높은 CPU사용량 등의 앱 문제를 일으킬 수 있기 때문에 앱을 중단하고 다시 시작하는 것이 좋다. Prometheus****,**** PM2 등의 모니터링 툴을 사용할 수 있는데, 이 부분은 나중에 다시 알아보기로 하겠다!

에러 코드

그렇다면 상수화한 에러 코드는 어떻게 만들면 좋을지 Facebook의 에러 코드 예시를 찾아보았다.

Facebook에서는 다음과 같이 숫자 코드와, 에러의 name을 설정하여 어떻게 대응하면 좋을지를 문서화하였다.

  • FaceBook의 예시Code or Type Name What To Do
Code or Type Name What To Do
OAuthException   If no subcode is present, the login status or access token has expired, been revoked, or is otherwise invalid. Get a new access token. If a subcode is present, see the subcode.
102

API Session If no subcode is present, the login status or access token has expired, been revoked, or is otherwise invalid. Get a new access token. If a subcode is present, see the subcode.
1 API Unknown Possibly a temporary issue due to downtime. Wait and retry the operation. If it occurs again, check that you are requesting an existing API.
2 API Service Temporary issue due to downtime. Wait and retry the operation.
3 API Method Capability or permissions issue. Make sure your app has the necessary capability or permissions to make this call.
4 API Too Many Calls Temporary issue due to throttling. Wait and retry the operation, or examine your API request volume.
17 API User Too Many Calls Temporary issue due to throttling. Wait and retry the operation, or examine your API request volume.
10 API Permission Denied Permission is either not granted or has been removed. Handle the missing permissions.
190 Access token has expired Get a new access token.
200-299 API Permission (Multiple values depending on permission) Permission is either not granted or has been removed. Handle the missing permissions.
341 Application limit reached Temporary issue due to downtime or throttling. Wait and retry the operation, or examine your API request volume.
368 Temporarily blocked for policies violations Wait and retry the operation.
506 Duplicate Post Duplicate posts cannot be published consecutively. Change the content of the post and try again.
1609005 Error Posting Link There was a problem scraping data from the provided link. Check the URL and try again.

이런 부분은 클라이언트와 서버가 잘 상의해서 결정한 후에 문서화를 하고 업데이트가 될 때마다 문서도 함께 업데이트를 하면 좋을 것 같다.(문서화에는 Swagger같은 자동화 툴을 사용하면 좋을 것 같다.)


로그는 어떻게 남기면 좋을까?

로그는 서비스 동작 상태를 파악하는 것과, 장애 파악 & 알람을 위해 수집한다.

서비스 파악의 관점에서는, 작성된 로그를 분석하면 서비스 지표의 확인, 트랜잭션, 성능 등 다양한 정보를 확인할 수 있다.

에러가 발생했을 때 로그를 수집하는 것도 매우 중요하다.

앱이 프로덕션 환경에서 실행될 때는 디버거를 연결해서 버그를 재생할 수 없기 때문에, 로그를 수집해두면 어디에서 어떤 에러가 발생했는지를 알고 디버깅을 할 수 있다. Winston이나 Pino같은 로깅 도구들을 사용하면, 오류의 가시성을 높이고 에러를 발견하고 이해하는 속도를 높여준다.

로그에서 중요한 세 가지 파트

로그의 소스

  • MSA 서버의 경우에는, service name, zone, hostname같은 정보들과 로그의 소스들이 중요해진다. Filebeat같은 log shipper들을 사용하면, 여러 대의 서버에서 발생하는 로그들의 메타데이터를 중앙 집중 로그 시스템으로 보내준다.

 

타임스탬프

  • 로그가 생성된 시점은 디버깅 시에 가장 중요한 부분이다. 모든 로그는 타임스탬프가 있어야 하고, 그것으로 sorting이나 filter가 가능해야 한다.

 

레벨과 내용

  • 에러를 찾기 위해 로그를 확인하는데 로그가 불충분한 정보를 제공하고 있어서 이해를 위해 다시 코드로 돌아가야 한다면, 불필요한 워크로드가 증가하고 빠르게 해결하는 것에 방해가 될 수 있다.
  • 에러의 내용은, The operation failed! 보다는 Failed to create user, as the user id already exist 등으로 자세하게 적어주는 것이 좋다.
  • 레벨의 경우, 로그를 개발자가 확인하는지 프로덕트 매니저가 확인하는지에 따라 (확인 용도에 따라) 확인해야 할 로그의 양과 수준이 다르기 때문에, 대상에 따라 런타임에 로그 수준을 전환하고 적절한 로그만 가져올 수 있도록 해야한다.
  • 일반적으로 아래와 같이 로그의 레벨을 나눌 수 있다. 
    • INFO : 중요한 메세지들, 한 작업이 완료되는 시기를 설명하는 이벤트 메세지이다. 진행상황에 대한 정보 로그를 나타낸다.
    • DEBUG : 개발자를 위한 레벨로, 어떤 함수가 호출되고 어떤 매개변수가 전달되었는지 등의 정보를 기록하는 것과 유사하다. 정확한 문제를 찾고 디버깅하는 데에 도움이 된다.
    • TRACE : DEBUG 레벨보다 더 세분화된 정보를 제공한다.
    • WARN : 이 레벨의 로그는 지속하기 위해 앱을 차단하지 않는다. 문제가 있고, 해결방법이 사용될 때 경고를 제공한다. (예: 잘못된 사용자 입력, 재시도 등) 관리자는 향후 이런 경고를 수정해야 한다.
    • ERROR : 종료까지는 가지 않지만 의도하지 않은 문제가 발생해서 우선적으로 조사해야하는 레벨이다. (예: DB가 다운됨, 다른 MSA와의 커뮤니케이션 실패, 필수 Input이 undefined) 주요 대상은 시스템 운영자 또는 모니터링 시스템이다.
    • FATAL : 응용 프로그램이 중단될 수 있는 아주 심각한 에러 이벤트의 레벨이다.

로그 작성 시 주의사항

  • 로그 파일/DB 생명 주기 & 저장소 용량
  • 개인정보
  • 시스템 주요 정보 (시스템 보안, 계정 정보)
  • 성능에 미치는 영향

 

로그가 저장되는 저장소의 용량과, 파일 등 언제 삭제할 것인지에 대한 계획을 명확하게 수립하고 운영해야 디스크 용량 부족과 같은 갑작스러운 장애를 방지할 수 있다. 개인정보와 시스템 계정 등의 민감한 정보는 로그에 노출될 시 보안적 취약점을 가지기 때문에 주의해야한다.

 

또한, 앱의 로그 작성 빈도가 높으면 앱의 성능에 직접적인 영향을 미칠 수 있다.

DEBUG나 INFO는 로그의 95% 이상에 기여한다고 하므로, ERROR 및 WARN 수준만 활성화하는 것이 좋다.

문제가 발생했을 때 파악을 위해 DEBUG 레벨로 변경한 후, 문제가 해결되면 다시 ERROR 레벨로 변경하는 것이 좋다.


마치며

에러를 어느 레이어에서 어떻게 처리해주어야 할 지, 왜 그렇게 해야하는지를 알아보면서 에러의 종류와 처리해야하는 방법에 대해서 알게 되었다.

Programmer Error를 핸들링하기 위해 모니터링 툴을 이용하여 앱을 재시작하도록 만드는 상황은 어떤 상황들이 있는지 / 어떻게 대응해야 할 지도 궁금해졌고, 수집한 log들을 모니터링하는 방법과 winston/pino의 차이점도 다음에 더 자세히 알아보면 좋을 것 같다.

 

참고자료

[JAVA] 자바 예외/에러 처리 잘하기

async 함수와 try-catch

How to Build a Node.js Error-handling System

Node.js Error Handling Made Easy: Best Practices On Just About Everything You Need to Know

Logging: Best Practices for Node.JS Applications