Auth 도메인 오류 관리 가이드 (CoreErrorHandlingModule 기반)
Auth 도메인에서 발생하는 모든 오류는 애플리케이션 전반의 CoreErrorHandlingModule과 GlobalExceptionFilter를 통해 중앙 집중식으로 관리됩니다. 이 문서는 Auth 도메인 특화 오류를 정의하고 사용하는 방법에 대해 설명합니다.
참고: 전반적인 에러 처리 아키텍처, 로깅, 메트릭 수집 등은 에러 처리 코어 모듈 구현 가이드 문서를 참조하세요.
목차
Auth 도메인 오류 코드
Auth 도메인의 오류 코드는 다음과 같은 체계로 구성됩니다:
| 범위 | 용도 |
|---|---|
| 2000-2008 | 서버 및 일반 오류 |
| 2009-2019 | 디바이스 및 앱 인증 관련 오류 |
| 2020-2039 | 사용자 인증 관련 오류 (로그인, 자격증명 등) |
| 2040-2049 | 속도 제한 및 보안 관련 오류 |
| 2050-2059 | 토큰 관련 오류 (생성, 검증, 만료 등) |
| 2060-2069 | 권한 및 역할 관련 오류 |
| 2070-2079 | Consent 관리 관련 오류 |
| 2080-2089 | 약관 관련 오류 |
| 2090-2099 | 기타 Auth 도메인 오류 (Access Code 포함) |
참고: 기존 숫자 코드를 사용하며, CoreErrorHandlingModule의 BaseError는 number 타입 코드를 지원합니다.
오류 코드 및 메시지 정의
일관된 오류 코드와 메시지를 관리하기 위해 enum과 매핑 객체를 사용할 수 있습니다. 이는 커스텀 에러 클래스를 정의할 때 참조용으로 활용됩니다.
1. 오류 코드 enum 정의 (참조용)
// 파일 경로: libs/feature/auth/src/lib/domain/errors/auth-error-codes.enum.ts
// 참고: 실제 에러 클래스에서는 숫자 코드를 직접 사용하므로, 이 enum은 선택 사항입니다.
export enum AuthErrorCodeRef {
// 서버 오류 (2000-2008)
SERVER_ERROR = 2000,
// 디바이스 및 앱 인증 오류 (2009-2019)
INVALID_DEVICE_ID = 2009,
CHALLENGE_VERIFICATION_FAILED = 2010,
DEVICE_BLACKLISTED = 2011,
INVALID_MFA_CODE = 2012,
EMAIL_NOT_VERIFIED = 2013,
VERIFICATION_CODE_EXPIRED = 2014,
INVALID_APP_TOKEN = 2015,
DEVICE_HASH_MISMATCH = 2016,
INVALID_VERIFICATION_CODE = 2017,
VERIFICATION_ATTEMPTS_EXCEEDED = 2018,
// 사용자 인증 오류 (2020-2039)
EMAIL_ALREADY_EXISTS = 2020,
WEAK_PASSWORD = 2021,
PASSWORD_HISTORY_MATCH = 2022,
INVALID_CREDENTIALS = 2023,
ACCOUNT_LOCKED = 2024,
PASSWORD_EXPIRED = 2025,
// 속도 제한 및 보안 관련 오류 (2040-2049)
RATE_LIMIT_EXCEEDED = 2040,
SUSPICIOUS_ACTIVITY = 2041,
// 토큰 관련 오류 (2050-2059)
INVALID_APP_TOKEN = 2050,
APP_TOKEN_EXPIRED = 2051,
REFRESH_TOKEN_INVALID = 2052,
TOKEN_REVOKED = 2053,
// 권한 관련 오류 (2060-2069)
PERMISSION_DENIED = 2060,
INSUFFICIENT_ROLE = 2061,
// Consent 관리 관련 오류 (2070-2079)
CONSENT_NOT_FOUND = 2070,
EXISTING_CONSENT_NOT_FOUND = 2071,
// 약관 관련 오류 (2080-2089)
TERMS_NOT_FOUND = 2080,
TERMS_ALREADY_EXISTS = 2081,
TERMS_VERSION_INVALID = 2082,
TERMS_ACCEPTANCE_REQUIRED = 2083,
// 기타 Auth 도메인 오류 (Access Code 포함) (2090-2099)
INVALID_ACCESS_CODE = 2090,
ACCESS_CODE_ALREADY_USED = 2091,
ACCESS_CODE_EXPIRED = 2092,
UNKNOWN_AUTH_ERROR = 2099,
}
2. 오류 메시지 매핑 (참조용)
// 파일 경로: libs/feature/auth/src/lib/domain/errors/auth-error-messages.ref.ts
// 참고: 실제 에러 클래스에서 메시지를 직접 정의하므로, 이 매핑은 참조용입니다.
import { AuthErrorCodeRef } from './auth-error-codes.enum'; // Enum 임포트 추가
export const AUTH_ERROR_MESSAGES_REF: Record<number, { message: string; detail: string }> = {
// 서버 오류
[AuthErrorCodeRef.SERVER_ERROR]: { message: 'SERVER_ERROR', detail: '서버 내부 오류' },
// 디바이스 및 앱 인증 오류
[AuthErrorCodeRef.INVALID_DEVICE_ID]: { message: 'INVALID_DEVICE_ID', detail: '디바이스 ID가 유효하지 않습니다' },
[AuthErrorCodeRef.CHALLENGE_VERIFICATION_FAILED]: { message: 'CHALLENGE_VERIFICATION_FAILED', detail: '챌린지 검증에 실패했습니다' },
[AuthErrorCodeRef.DEVICE_BLACKLISTED]: { message: 'DEVICE_BLACKLISTED', detail: '디바이스가 블랙리스트에 등록되어 있습니다' },
[AuthErrorCodeRef.INVALID_MFA_CODE]: { message: 'INVALID_MFA_CODE', detail: '다중 인증 코드가 유효하지 않습니다' },
[AuthErrorCodeRef.EMAIL_NOT_VERIFIED]: { message: 'EMAIL_NOT_VERIFIED', detail: '이메일이 인증되지 않았습니다' },
[AuthErrorCodeRef.VERIFICATION_CODE_EXPIRED]: { message: 'VERIFICATION_CODE_EXPIRED', detail: '인증 코드가 만료되었습니다' },
[AuthErrorCodeRef.INVALID_APP_TOKEN]: { message: 'INVALID_APP_TOKEN', detail: '앱 토큰이 유효하지 않거나 만료되었습니다' },
[AuthErrorCodeRef.DEVICE_HASH_MISMATCH]: { message: 'DEVICE_HASH_MISMATCH', detail: '디바이스 ID 해시가 계산된 값과 일치하지 않습니다' },
[AuthErrorCodeRef.INVALID_VERIFICATION_CODE]: { message: 'INVALID_VERIFICATION_CODE', detail: '인증 코드가 올바르지 않습니다' },
[AuthErrorCodeRef.VERIFICATION_ATTEMPTS_EXCEEDED]: { message: 'VERIFICATION_ATTEMPTS_EXCEEDED', detail: '인증 코드 확인 시도 횟수를 초과했습니다' },
// 사용자 인증 오류
[AuthErrorCodeRef.EMAIL_ALREADY_EXISTS]: { message: 'EMAIL_ALREADY_EXISTS', detail: '이미 등록된 이메일 주소입니다' },
[AuthErrorCodeRef.WEAK_PASSWORD]: { message: 'WEAK_PASSWORD', detail: '비밀번호가 보안 요구사항을 충족하지 않습니다' },
[AuthErrorCodeRef.PASSWORD_HISTORY_MATCH]: { message: 'PASSWORD_HISTORY_MATCH', detail: '최근에 사용한 비밀번호입니다' },
[AuthErrorCodeRef.INVALID_CREDENTIALS]: { message: 'INVALID_CREDENTIALS', detail: '이메일 또는 비밀번호가 올바르지 않습니다' },
[AuthErrorCodeRef.ACCOUNT_LOCKED]: { message: 'ACCOUNT_LOCKED', detail: '계정이 잠겨 있습니다' },
[AuthErrorCodeRef.PASSWORD_EXPIRED]: { message: 'PASSWORD_EXPIRED', detail: '비밀번호가 만료되었습니다. 비밀번호를 변경해주세요' },
// 속도 제한 및 보안 관련 오류
[AuthErrorCodeRef.RATE_LIMIT_EXCEEDED]: { message: 'RATE_LIMIT_EXCEEDED', detail: '요청 횟수가 제한을 초과했습니다' },
[AuthErrorCodeRef.SUSPICIOUS_ACTIVITY]: { message: 'SUSPICIOUS_ACTIVITY', detail: '의심스러운 활동이 감지되었습니다' },
// 토큰 관련 오류
[AuthErrorCodeRef.INVALID_APP_TOKEN]: { message: 'INVALID_APP_TOKEN', detail: '토큰이 유효하지 않습니다' },
[AuthErrorCodeRef.APP_TOKEN_EXPIRED]: { message: 'APP_TOKEN_EXPIRED', detail: '토큰이 만료되었습니다' },
[AuthErrorCodeRef.REFRESH_TOKEN_INVALID]: { message: 'REFRESH_TOKEN_INVALID', detail: '리프레시 토큰이 유효하지 않습니다' },
[AuthErrorCodeRef.TOKEN_REVOKED]: { message: 'TOKEN_REVOKED', detail: '토큰이 취소되었습니다' },
// 권한 관련 오류
[AuthErrorCodeRef.PERMISSION_DENIED]: { message: 'PERMISSION_DENIED', detail: '요청한 작업에 대한 권한이 없습니다' },
[AuthErrorCodeRef.INSUFFICIENT_ROLE]: { message: 'INSUFFICIENT_ROLE', detail: '요청한 작업을 수행하기 위한 역할이 부족합니다' },
// Consent 관리 오류 메시지
[AuthErrorCodeRef.CONSENT_NOT_FOUND]: { message: 'CONSENT_NOT_FOUND', detail: '요청한 동의 정보를 찾을 수 없습니다.' },
[AuthErrorCodeRef.EXISTING_CONSENT_NOT_FOUND]: { message: 'EXISTING_CONSENT_NOT_FOUND', detail: '기존 동의 정보를 찾을 수 없어 업데이트할 수 없습니다.' },
// 약관 관리 오류 메시지
[AuthErrorCodeRef.TERMS_NOT_FOUND]: { message: 'TERMS_NOT_FOUND', detail: '요청한 약관을 찾을 수 없습니다' },
[AuthErrorCodeRef.TERMS_ALREADY_EXISTS]: { message: 'TERMS_ALREADY_EXISTS', detail: '동일한 버전의 약관이 이미 존재합니다' },
[AuthErrorCodeRef.TERMS_VERSION_INVALID]: { message: 'TERMS_VERSION_INVALID', detail: '약관 버전이 유효하지 않습니다' },
[AuthErrorCodeRef.TERMS_ACCEPTANCE_REQUIRED]: { message: 'TERMS_ACCEPTANCE_REQUIRED', detail: '이 작업을 진행하려면 약관 동의가 필요합니다' },
// 기타 Auth 도메인 오류 (Access Code 포함)
[AuthErrorCodeRef.INVALID_ACCESS_CODE]: { message: 'INVALID_ACCESS_CODE', detail: '유효하지 않은 Access Code입니다.' },
[AuthErrorCodeRef.ACCESS_CODE_ALREADY_USED]: { message: 'ACCESS_CODE_ALREADY_USED', detail: '이미 사용된 Access Code입니다.' },
[AuthErrorCodeRef.ACCESS_CODE_EXPIRED]: { message: 'ACCESS_CODE_EXPIRED', detail: '만료된 Access Code입니다.' },
[AuthErrorCodeRef.UNKNOWN_AUTH_ERROR]: { message: 'UNKNOWN_AUTH_ERROR', detail: '알 수 없는 인증 오류가 발생했습니다' },
};
Auth 도메인 에러 클래스 정의
Auth 도메인에서 발생하는 특정한 오류 상황들은 @core/error-handling 의 DomainError 또는 SystemError를 상속받아 구체적인 에러 클래스로 정의합니다.
// 파일 경로: libs/feature/auth/src/lib/domain/errors/auth.errors.ts
import { DomainError, SystemError, ErrorMetadata } from '@core/error-handling';
import { AuthErrorCodeRef } from './auth-error-codes.enum'; // Enum 임포트
// --- 서버 오류 ---
export class ServerError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.SERVER_ERROR, '서버 내부 오류', { ...metadata, originalErrorMessage: originalError?.message });
}
}
// --- 디바이스 및 앱 인증 오류 ---
export class InvalidDeviceIdError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_DEVICE_ID, '디바이스 ID가 유효하지 않습니다', 400, metadata);
}
}
export class ChallengeVerificationFailedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.CHALLENGE_VERIFICATION_FAILED, '챌린지 검증에 실패했습니다', 401, metadata);
}
}
export class DeviceBlacklistedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.DEVICE_BLACKLISTED, '디바이스가 블랙리스트에 등록되어 있습니다', 403, metadata);
}
}
export class InvalidMfaCodeError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_MFA_CODE, '다중 인증 코드가 유효하지 않습니다', 401, metadata);
}
}
export class EmailNotVerifiedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.EMAIL_NOT_VERIFIED, '이메일이 인증되지 않았습니다', 403, metadata);
}
}
export class VerificationCodeExpiredError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.VERIFICATION_CODE_EXPIRED, '인증 코드가 만료되었습니다', 400, metadata);
}
}
export class InvalidAppTokenError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_APP_TOKEN, '앱 토큰이 유효하지 않거나 만료되었습니다', 401, metadata);
}
}
export class DeviceHashMismatchError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.DEVICE_HASH_MISMATCH, '디바이스 ID 해시가 계산된 값과 일치하지 않습니다', 400, metadata);
}
}
export class InvalidVerificationCodeError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_VERIFICATION_CODE, '인증 코드가 올바르지 않습니다', 400, metadata);
}
}
export class VerificationAttemptsExceededError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.VERIFICATION_ATTEMPTS_EXCEEDED, '인증 코드 확인 시도 횟수를 초과했습니다', 429, metadata);
}
}
// --- 사용자 인증 오류 ---
export class EmailAlreadyExistsError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.EMAIL_ALREADY_EXISTS, '이미 등록된 이메일 주소입니다', 409, metadata);
}
}
export class WeakPasswordError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.WEAK_PASSWORD, '비밀번호가 보안 요구사항을 충족하지 않습니다', 400, metadata);
}
}
export class PasswordHistoryMatchError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.PASSWORD_HISTORY_MATCH, '최근에 사용한 비밀번호입니다', 409, metadata);
}
}
export class InvalidCredentialsError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_CREDENTIALS, '이메일 또는 비밀번호가 올바르지 않습니다', 401, metadata);
}
}
export class AccountLockedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.ACCOUNT_LOCKED, '계정이 잠겨 있습니다', 403, metadata);
}
}
export class PasswordExpiredError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.PASSWORD_EXPIRED, '비밀번호가 만료되었습니다. 비밀번호를 변경해주세요', 403, metadata);
}
}
// --- 속도 제한 및 보안 관련 오류 ---
export class RateLimitExceededError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.RATE_LIMIT_EXCEEDED, '요청 횟수가 제한을 초과했습니다', 429, metadata);
}
}
export class SuspiciousActivityError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.SUSPICIOUS_ACTIVITY, '의심스러운 활동이 감지되었습니다', 403, metadata);
}
}
// --- 토큰 관련 오류 ---
export class InvalidTokenError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_APP_TOKEN, '토큰이 유효하지 않습니다', 401, metadata);
}
}
export class TokenExpiredError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.APP_TOKEN_EXPIRED, '토큰이 만료되었습니다', 401, metadata);
}
}
export class RefreshTokenInvalidError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.REFRESH_TOKEN_INVALID, '리프레시 토큰이 유효하지 않습니다', 401, metadata);
}
}
export class TokenRevokedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.TOKEN_REVOKED, '토큰이 취소되었습니다', 401, metadata);
}
}
// --- 권한 관련 오류 ---
export class PermissionDeniedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.PERMISSION_DENIED, '요청한 작업에 대한 권한이 없습니다', 403, metadata);
}
}
export class InsufficientRoleError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INSUFFICIENT_ROLE, '요청한 작업을 수행하기 위한 역할이 부족합니다', 403, metadata);
}
}
// --- Consent 관리 관련 오류 ---
export class ConsentNotFoundError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.CONSENT_NOT_FOUND, '요청한 동의 정보를 찾을 수 없습니다.', 404, metadata);
}
}
export class ExistingConsentNotFoundError extends DomainError {
constructor(metadata?: ErrorMetadata) {
// ConsentService.updateConsent에서 기존 동의를 못찾는 경우 발생. Bad Request로 처리.
super(AuthErrorCodeRef.EXISTING_CONSENT_NOT_FOUND, '기존 동의 정보를 찾을 수 없어 업데이트할 수 없습니다.', 400, metadata);
}
}
// --- 약관 관련 오류 ---
export class TermsNotFoundError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.TERMS_NOT_FOUND, '요청한 약관을 찾을 수 없습니다', 404, metadata);
}
}
export class TermsAlreadyExistsError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.TERMS_ALREADY_EXISTS, '동일한 버전의 약관이 이미 존재합니다', 409, metadata);
}
}
export class TermsVersionInvalidError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.TERMS_VERSION_INVALID, '약관 버전이 유효하지 않습니다', 400, metadata);
}
}
export class TermsAcceptanceRequiredError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.TERMS_ACCEPTANCE_REQUIRED, '이 작업을 진행하려면 약관 동의가 필요합니다', 403, metadata);
}
}
// --- 기타 오류 (Access Code 포함) ---
export class InvalidAccessCodeError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.INVALID_ACCESS_CODE, '유효하지 않은 Access Code입니다.', 400, metadata);
}
}
export class AccessCodeAlreadyUsedError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.ACCESS_CODE_ALREADY_USED, '이미 사용된 Access Code입니다.', 409, metadata);
}
}
export class AccessCodeExpiredError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.ACCESS_CODE_EXPIRED, '만료된 Access Code입니다.', 400, metadata); // 410 Gone도 고려 가능
}
}
// 이전에 정의했던 TokenGenerationError와 같은 특정 SystemError는 유지하거나
// 필요하다면 UNKNOWN_AUTH_ERROR 코드를 사용하는 일반적인 SystemError를 정의할 수 있습니다.
export class UnknownAuthError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(AuthErrorCodeRef.UNKNOWN_AUTH_ERROR, '알 수 없는 인증 오류가 발생했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
// TokenGenerationError를 위한 특정 코드가 필요하다면 enum에 추가하고 사용
// export class TokenGenerationError extends SystemError {
// constructor(originalError: Error, metadata?: ErrorMetadata) {
// super(AuthErrorCodeRef.TOKEN_GENERATION_FAILED, '토큰 생성 중 오류가 발생했습니다.', { ...metadata, originalErrorMessage: originalError.message });
// }
// }
주요 특징:
- 각 에러 클래스는 특정 오류 상황을 명확하게 나타냅니다.
DomainError또는SystemError를 상속받아 공통 에러 처리 메커니즘(GlobalExceptionFilter)과 통합됩니다.- 생성자에서 고유 코드(
AuthErrorCodeRefenum 사용), 사용자 메시지, HTTP 상태 코드(필요시 기본값 재정의), 메타데이터를 설정합니다.
사용 예시
정의된 에러 클래스는 서비스, 컨트롤러, 가드 등 필요한 로직 내에서 throw 키워드를 사용하여 발생시킵니다. 발생된 에러는 GlobalExceptionFilter에 의해 자동으로 처리됩니다.
서비스에서 사용 예시
// 파일 경로: libs/feature/auth/src/lib/services/auth.service.ts
import { Injectable } from '@nestjs/common';
import { ErrorLoggerService } from '@core/error-handling'; // 로깅 서비스 주입
import { InvalidCredentialsError, AccountLockedError } from '../domain/errors/auth.errors'; // 정의된 에러 임포트
// ... (UserRepository, PasswordService 등 다른 의존성)
@Injectable()
export class AuthService {
constructor(
private readonly errorLogger: ErrorLoggerService, // 에러 로깅을 위해 주입
private readonly userRepository: UserRepository,
private readonly passwordService: PasswordService
) {}
async login(loginDto: LoginDto): Promise<{ accessToken: string }> {
try {
const user = await this.userRepository.findByEmail(loginDto.email);
if (!user || !(await this.passwordService.compare(loginDto.password, user.passwordHash))) {
// --- 인증 실패 시 InvalidCredentialsError 발생 ---
throw new InvalidCredentialsError({ attemptEmail: loginDto.email });
}
if (user.isLocked) {
// --- 계정 잠금 시 AccountLockedError 발생 ---
throw new AccountLockedError({ userId: user.id });
}
// 로그인 성공: JWT 토큰 생성
const accessToken = this.generateJwtToken(user);
return { accessToken };
} catch (error) {
// 발생한 에러(InvalidCredentialsError, AccountLockedError 등)를 로깅
// GlobalExceptionFilter에서도 기본 로깅을 하지만, 여기서 추가 컨텍스트 로깅 가능
if (error instanceof DomainError || error instanceof SystemError) {
this.errorLogger.logError(error, { // BaseError를 상속한 경우에만 추가 로깅
context: 'AuthService.login',
data: { email: loginDto.email },
});
} else {
// 예상치 못한 일반 에러 로깅 (GlobalExceptionFilter가 처리하겠지만 여기서도 가능)
this.errorLogger.logError(error as Error, { context: 'AuthService.login - Unexpected' });
}
// --- 잡은 에러를 GlobalExceptionFilter가 처리하도록 다시 던짐 ---
throw error;
}
}
private generateJwtToken(user: any): string { /* ... */ return 'dummy-token'; }
}
컨트롤러에서 사용 예시 (직접 에러 처리 불필요)
컨트롤러는 일반적으로 서비스에서 던져진 에러를 그대로 전달하여 GlobalExceptionFilter가 처리하도록 둡니다.
// 파일 경로: apps/dta-wide-api/src/app/auth/controllers/terms-and-consents.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from '@feature/auth-core'; // 경로 수정 필요
import { LoginDto } from '@feature/auth-core'; // 경로 수정 필요
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK) // 성공 시 200 OK 명시
async login(@Body() loginDto: LoginDto) {
// AuthService.login() 내부에서 에러 발생 시 자동으로 GlobalExceptionFilter로 전달됨
// 컨트롤러 레벨에서 별도의 try-catch나 에러 처리가 필요 없음
const result = await this.authService.login(loginDto);
return result;
}
}
가드에서 사용 예시
가드에서도 특정 조건 미충족 시 정의된 커스텀 에러를 발생시킬 수 있습니다.
// 파일 경로: libs/feature/auth/src/lib/guards/role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionDeniedError } from '../domain/errors/auth.errors'; // 정의된 에러 임포트
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true; // 역할 요구사항 없으면 통과
}
const { user } = context.switchToHttp().getRequest();
const hasRole = () => requiredRoles.some((role) => user?.roles?.includes(role));
if (!user || !user.roles || !hasRole()) {
// --- 권한 부족 시 PermissionDeniedError 발생 ---
throw new PermissionDeniedError({ requiredRoles, userRoles: user?.roles });
}
return true;
}
}
결론
Auth 도메인의 오류 처리는 이제 CoreErrorHandlingModule의 GlobalExceptionFilter와 BaseError (및 DomainError, SystemError)를 기반으로 합니다. 각 도메인은 필요한 특정 오류 상황을 나타내는 커스텀 에러 클래스를 정의하고, 비즈니스 로직 내에서 이를 throw하여 사용합니다. 이를 통해 애플리케이션 전체적으로 일관되고 관리하기 쉬운 오류 처리 메커니즘을 구축할 수 있습니다.