본문으로 건너뛰기

에러 처리 코어 모듈 구현 가이드

1. CoreErrorHandlingModule

1.1 개요

애플리케이션 전반의 에러 처리, 로깅, 모니터링을 담당하는 글로벌 모듈입니다.

1.2 프로젝트 구조

CoreErrorHandlingModule은 모든 서비스와 모듈에서 공통으로 사용되는 핵심 기능이므로 libs/ 디렉토리 아래에 위치합니다.

libs/
core/
error-handling/
src/
lib/
errors/
base-error.ts
domain-error.ts
system-error.ts
filters/
global-exception.filter.ts
services/
error-logger.service.ts
error-metrics.service.ts
error-alert.service.ts
core-error-handling.module.ts
index.ts
README.md
tsconfig.json

모든 에러 처리 관련 코드는 libs/core/error-handling 디렉토리에 구현하고, 필요한 서비스에서 import { CoreErrorHandlingModule } from '@libs/core/error-handling';와 같이 임포트하여 사용합니다. 이를 통해 모든 서비스에서 일관된 에러 처리, 로깅, 모니터링 방식을 보장합니다.

1.3 구현 사항

// libs/core/error-handling/src/lib/core-error-handling.module.ts
import { Module, Global } from '@nestjs/common';
import { ErrorLoggerService } from './services/error-logger.service';
import { ErrorMetricsService } from './services/error-metrics.service';
import { ErrorAlertService } from './services/error-alert.service';
import { GlobalExceptionFilter } from './filters/global-exception.filter';

@Global()
@Module({
providers: [
ErrorLoggerService,
ErrorMetricsService,
ErrorAlertService,
GlobalExceptionFilter,
],
exports: [
ErrorLoggerService,
ErrorMetricsService,
ErrorAlertService,
],
})
export class CoreErrorHandlingModule {}

1.4 기본 에러 클래스

// libs/core/error-handling/src/lib/errors/base-error.ts
export interface ErrorMetadata {
[key: string]: any;
}

export abstract class BaseError extends Error {
constructor(
public readonly code: number,
message: string,
public readonly statusCode: number = 500,
public readonly metadata: ErrorMetadata = {},
public readonly errorId: string = randomUUID()
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}

toJSON(): Record<string, any> {
return {
code: this.code,
message: this.message,
statusCode: this.statusCode,
metadata: this.metadata,
errorId: this.errorId
};
}
}

// libs/core/error-handling/src/lib/errors/domain-error.ts
export class DomainError extends BaseError {
constructor(
code: number,
message: string,
metadata?: ErrorMetadata
) {
super(code, message, 400, metadata);
}
}

// libs/core/error-handling/src/lib/errors/system-error.ts
export class SystemError extends BaseError {
constructor(
code: number,
message: string,
metadata?: ErrorMetadata
) {
super(code, message, 500, metadata);
}
}

// 공통 도메인 에러 (추가)
export class NotFoundError extends DomainError {
constructor(
message = '리소스를 찾을 수 없습니다.', // 기본 메시지
metadata?: ErrorMetadata
) {
super(404, message, metadata); // 404 상태 코드에 매핑, 내부 에러 코드는 임의로 404 사용
}
}

export class ForbiddenError extends DomainError {
constructor(
message = '접근 권한이 없습니다.', // 기본 메시지
metadata?: ErrorMetadata
) {
super(403, message, metadata); // 403 상태 코드에 매핑, 내부 에러 코드는 임의로 403 사용
}
}

2. 서비스 구현

2.1 에러 로깅 서비스

// libs/core/error-handling/src/lib/services/error-logger.service.ts
@Injectable()
export class ErrorLoggerService {
constructor(
private readonly logger: LoggerService,
private readonly logPublisher: LogPublisherService
) {}

logError(error: Error, context?: ErrorContext): void {
const errorData = this.formatErrorData(error, context);

// BigQuery의 error_logs 테이블에 저장
this.logPublisher.publish('error', errorData);

// 일반 로깅도 함께 수행
this.logger.error(error.message, error.stack, context);
}

private formatErrorData(error: Error, context?: ErrorContext) {
const baseErrorData = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
type: 'error_log'
};

if (error instanceof BaseError) {
return {
...baseErrorData,
code: error.code,
statusCode: error.statusCode,
metadata: error.metadata,
errorId: error.errorId,
...context
};
}

return {
...baseErrorData,
...context
};
}
}

2.2 에러 메트릭스 서비스

// libs/core/error-handling/src/lib/services/error-metrics.service.ts
@Injectable()
export class ErrorMetricsService {
private readonly errorCounter: Counter;

constructor() {
this.errorCounter = new Counter({
name: 'app_errors_total',
help: '발생한 에러의 총 개수',
labelNames: ['error_type', 'error_code']
});
}

recordError(error: Error): void {
if (error instanceof BaseError) {
this.errorCounter.inc({
error_type: error.constructor.name,
error_code: error.code
});
}
}
}

2.3 에러 알림 서비스

// libs/core/error-handling/src/lib/services/error-alert.service.ts
@Injectable()
export class ErrorAlertService {
private readonly criticalErrorPatterns = [
'DATABASE_CONNECTION_ERROR',
'PAYMENT_PROCESSING_ERROR',
];

async handleError(error: BaseError): Promise<void> {
if (this.isCriticalError(error)) {
await this.sendAlert({
level: 'critical',
title: `중요 에러 발생: ${error.code}`,
message: error.message,
metadata: error.metadata
});
}
}

private isCriticalError(error: BaseError): boolean {
return this.criticalErrorPatterns.includes(error.code.toString());
}
}

3. 필터 구현

3.1 글로벌 예외 필터

// libs/core/error-handling/src/lib/filters/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
Injectable,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BaseError } from '../errors/base-error';
import { ErrorCode } from '../enums/error-code.enum';

/**
* Standard validation error detail structure
*/
export interface ValidationErrorDetail {
field: string;
message: string;
}

/**
* Structure for validation message objects from class-validator or ValidationPipe
*/
interface ValidationMessageObject {
property?: string;
message?: string;
field?: string;
error?: string;
constraints?: Record<string, string>;
value?: unknown;
}

/**
* Type guard functions for safer type checking
*/
function isValidationMessageObject(value: unknown): value is ValidationMessageObject {
return typeof value === 'object' && value !== null;
}

function hasHttpExceptionResponse(value: unknown): value is { message?: unknown; error?: string } {
return typeof value === 'object' && value !== null;
}

@Catch()
@Injectable()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let errorResponse: Record<string, any> = {
code: ErrorCode.UNKNOWN_ERROR,
message: 'An unexpected error occurred',
detail: 'Internal Server Error',
};

// Handle different types of exceptions
if (exception instanceof BaseError) {
// Custom domain errors
statusCode = exception.statusCode;
errorResponse = {
code: exception.code,
message: exception.errorNameKey,
detail: exception.message,
};

// Include metadata if present
if (exception.metadata && typeof exception.metadata === 'object') {
errorResponse['metadata'] = exception.metadata;
}
} else if (exception instanceof HttpException) {
// NestJS built-in exceptions
statusCode = exception.getStatus();
const exceptionResponse = exception.getResponse();

// Check specifically for validation errors (BadRequest)
if (
statusCode === HttpStatus.BAD_REQUEST &&
hasHttpExceptionResponse(exceptionResponse) &&
Array.isArray(exceptionResponse.message)
) {
// Handle validation errors with enhanced processing
const errors = this.processValidationMessages(exceptionResponse.message);

errorResponse = {
code: ErrorCode.VALIDATION_ERROR,
message: exceptionResponse.error || 'Validation Failed',
detail: '입력 값이 유효하지 않습니다',
errors,
};
} else {
// Other HttpExceptions
errorResponse = {
code: errorResponse['code'],
message: errorResponse['message'],
detail: typeof exceptionResponse === 'string'
? exceptionResponse
: 'An error occurred',
};
}
} else if (exception instanceof Error) {
// Standard JS Error objects
errorResponse['detail'] = exception.message || 'Unknown error occurred';
}

// Enhanced logging with detailed context
const originalExceptionDetails = exception instanceof BaseError
? exception.toJSON()
: (exception instanceof Error ? { name: exception.name, message: exception.message } : undefined);

this.logger.error(
`[${request.method} ${request.url}] Error ${errorResponse['code']}: ${errorResponse['message']}`,
{
stack: exception instanceof Error ? exception.stack : undefined,
errorResponse,
originalException: originalExceptionDetails,
}
);

response.status(statusCode).json(errorResponse);
}

/**
* Safely processes validation message arrays from various sources
* Supports both Global ValidationPipe and individual ValidationPipe formats
*/
private processValidationMessages(messages: unknown[]): ValidationErrorDetail[] {
if (!Array.isArray(messages)) {
this.logger.warn('Expected array of validation messages, got:', typeof messages);
return [];
}

return messages
.map((msg, index) => {
try {
return this.processValidationMessage(msg);
} catch (error) {
this.logger.warn(
`Failed to process validation message at index ${index}:`,
{ message: msg, error }
);
return {
field: 'unknown',
message: 'Validation error',
};
}
})
.filter((error): error is ValidationErrorDetail =>
error !== null
);
}

/**
* Processes a single validation message from ValidationPipe
* Handles both object and string formats
*/
private processValidationMessage(msg: unknown): ValidationErrorDetail | null {
if (!msg) {
return null;
}

// Handle object-based validation messages
if (isValidationMessageObject(msg)) {
if (msg.property && msg.message) {
return {
field: String(msg.property),
message: String(msg.message),
};
}

// Handle class-validator constraints format
if (msg.property && msg.constraints) {
const constraintMessages = Object.values(msg.constraints);
return {
field: String(msg.property),
message: constraintMessages.join(', '),
};
}
}

// Handle string-based validation messages
if (typeof msg === 'string') {
return {
field: this.extractFieldFromMessage(msg),
message: msg,
};
}

// Safe fallback
return {
field: 'unknown',
message: String(msg),
};
}

/**
* Extracts field name from string validation messages
* Supports various patterns like "email must be valid", "user.name is required"
*/
private extractFieldFromMessage(message: string): string {
if (!message || typeof message !== 'string') {
return 'unknown';
}

try {
const patterns = [
/^(\w+(?:\.\w+)*(?:\[\d+\])*) /, // nested.field[0] format
/^(\w+) /, // basic field format
/property\s+"([^"]+)"/, // property "fieldName" format
/(\w+(?:\.\w+)*)\s+(?:must|should|is)/i, // flexible pattern
];

for (const pattern of patterns) {
const match = message.match(pattern);
if (match && match[1]) {
return match[1];
}
}

return 'unknown';
} catch (error) {
this.logger.warn('Error extracting field from message:', { message, error });
return 'unknown';
}
}
}

3.2 새로운 타입 안전성 기능 (v0.47.0+)

ValidationErrorDetail 인터페이스

Validation 오류 시 클라이언트에게 반환되는 필드별 상세 정보:

export interface ValidationErrorDetail {
field: string; // 오류 발생 필드명
message: string; // 구체적인 오류 메시지
}

지원하는 Validation 형태

1. NestJS ValidationPipe 객체 형태 (main.ts에서 설정)

{
"property": "email",
"message": "must be an email",
"value": "invalid-email"
}

2. class-validator constraints 형태

{
"property": "accessCode",
"constraints": {
"isLength": "must be exactly 16 characters",
"matches": "must be alphanumeric"
},
"value": "invalid"
}

3. 문자열 validation 메시지

"email must be an email"
"user.profile.name should not be empty"

타입 안전성 개선사항

이전 (any 타입 사용):

const msgObj = msg as any; // 타입 안전하지 않음
if (msgObj.property && msgObj.message) { ... }

개선 (타입 가드 사용):

function isValidationMessageObject(value: unknown): value is ValidationMessageObject {
return typeof value === 'object' && value !== null;
}

if (isValidationMessageObject(msg)) {
if (msg.property && msg.message) { ... } // 타입 안전
}

3.3 실제 사용 예시

ValidationPipe 설정과 GlobalExceptionFilter 연동

main.ts에서의 ValidationPipe 설정:

app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
exceptionFactory: (errors) => {
const result = errors.map((error) => ({
property: error.property,
message: error.constraints
? error.constraints[Object.keys(error.constraints)[0]]
: 'Invalid value',
value: error.value,
}));
return new BadRequestException(result);
},
})
);

GlobalExceptionFilter에서 자동 처리:

  • ValidationPipe가 생성한 객체 배열이 자동으로 ValidationErrorDetail[]로 변환됨
  • 타입 안전성으로 런타임 에러 (503 에러) 방지
  • 다양한 validation 형태 모두 지원

클라이언트 응답 예시

WCS URI validation 오류 (실제 로그 케이스):

{
"code": 1001,
"message": "VALIDATION_ERROR",
"detail": "Input validation failed",
"errors": [
{
"field": "content",
"message": "Invalid WCS URI format. Expected: wcs://[language]/[version]/[path]"
}
]
}

여러 필드 validation 오류:

{
"code": 1001,
"message": "VALIDATION_ERROR",
"detail": "Input validation failed",
"errors": [
{
"field": "email",
"message": "must be an email"
},
{
"field": "accessCode",
"message": "must be exactly 16 characters, must be alphanumeric"
},
{
"field": "user.profile.name",
"message": "should not be empty"
}
]
}

4. 도메인에서의 사용

4.1 도메인별 에러 정의

// libs/feature/auth/src/lib/errors/auth.errors.ts
import { DomainError } from '@core/error-handling';

export class InvalidCredentialsError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(
2023,
'이메일 또는 비밀번호가 올바르지 않습니다',
metadata
);
}
}

4.2 서비스에서의 사용

// libs/feature/auth/src/lib/services/auth.service.ts
import { Injectable } from '@nestjs/common';
import { ErrorLoggerService } from '@core/error-handling';
import { InvalidCredentialsError } from '../errors/auth.errors'; // 4.1에서 정의한 에러 임포트
// ... (다른 필요한 import들)

@Injectable()
export class AuthService {
constructor(
private readonly errorLogger: ErrorLoggerService
// ... (다른 필요한 서비스 주입)
) {}

async login(loginDto: LoginDto): Promise<any> { // 실제 반환 타입 명시 필요
try {
// --- 비즈니스 로직 시작 ---
const user = await this.findUserByEmail(loginDto.email); // 사용자 조회

if (!user || !(await this.verifyPassword(loginDto.password, user.passwordHash))) {
// <<--- 여기서 정의된 커스텀 에러를 throw 합니다.
throw new InvalidCredentialsError({ attemptEmail: loginDto.email });
}

// 로그인 성공 처리 (예: 토큰 생성)
const token = this.generateJwtToken(user);
return { accessToken: token };
// --- 비즈니스 로직 끝 ---

} catch (error) {
// InvalidCredentialsError 또는 다른 에러가 여기서 잡힘
this.errorLogger.logError(error, {
context: 'AuthService.login',
data: { email: loginDto.email } // 로깅 시 필요한 컨텍스트 추가
});
// 잡은 에러를 GlobalExceptionFilter가 처리하도록 다시 던짐
throw error;
}
}
...
}

5. 모듈 통합

5.1 App 모듈에서의 통합

// apps/dta-wide-api/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { CoreConfigModule } from '@core/config';
import { CoreDatabaseModule } from '@core/database';
import { CoreCommonModule } from '@core/common';
import { CoreErrorHandlingModule } from '@core/error-handling';

@Module({
imports: [
CoreConfigModule,
CoreDatabaseModule,
CoreCommonModule,
CoreErrorHandlingModule,
],
})
export class AppModule {}

변경 이력

버전날짜작성자변경 내용
0.1.02025-03-24bok@weltcorp.com최초 작성
0.2.02025-03-26bok@weltcorp.comBigQuery 로그 저장 통합
0.3.02025-03-30bok@weltcorp.com에러 처리 모듈 위치 libs/ 경로로 명시
0.47.02025-09-09jeff@weltcorp.comGlobalExceptionFilter 타입 안전성 개선, ValidationErrorDetail 인터페이스 추가, validation 처리 로직 고도화, 503 에러 방지 기능 추가