본문으로 건너뛰기

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

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 에러 방지 기능 추가