User 도메인 오류 관리 가이드 (CoreErrorHandlingModule 기반)
User 도메인에서 발생하는 모든 오류는 애플리케이션 전반의 CoreErrorHandlingModule과 GlobalExceptionFilter를 통해 중앙 집중식으로 관리됩니다. 이 문서는 User 도메인 특화 오류를 정의하고 사용하는 방법에 대해 설명합니다.
참고: 전반적인 에러 처리 아키텍처, 로깅, 메트릭 수집 등은 에러 처리 코어 모듈 구현 가이드 문서를 참조하세요.
목차
User 도메인 오류 코드
User 도메인의 오류 코드는 다음과 같은 체계로 구성됩니다 (7000번대 사용):
| 범위 | 용도 |
|---|---|
| 7000-7009 | 사용자 계정 관련 일반 오류 (조회, 생성 등) |
| 7010-7019 | 사용자 상태 관리 관련 오류 |
| 7020-7029 | 사용자 프로필 관련 오류 |
| 7030-7039 | 테스터 관리 관련 오류 |
| 7040-7049 | 개인정보 보호 (GDPR 등) 관련 오류 |
| 7050-7059 | 사용자 유형 관련 오류 |
| 7080-7089 | 사용자 데이터 관리 관련 오류 |
| 7090-7099 | 기타 User 도메인 오류 |
참고: 기존 숫자 코드를 사용하며, CoreErrorHandlingModule의 BaseError는 number 타입 코드를 지원합니다.
오류 코드 및 메시지 정의
1. 오류 코드 enum 정의 (참조용)
// 파일 경로: libs/feature/user/src/lib/domain/errors/user-error-codes.enum.ts
// 참고: 실제 에러 클래스에서는 숫자 코드를 직접 사용하므로, 이 enum은 선택 사항입니다.
export enum UserErrorCodeRef { // 실제 프로젝트의 UserErrorCode enum을 따르거나, 문서용 참조 enum으로 활용
// 계정 관련 오류 (7000-7009)
INVALID_INPUT_DATA = 7001,
USER_EMAIL_ALREADY_EXISTS = 7002,
USER_NOT_FOUND = 7003,
USER_PROFILE_NOT_FOUND = 7004, // 프로필 조회 시 사용 가능
SERVICE_ACCOUNT_CREATION_FAILED = 7005,
USER_EMAIL_NOT_FOUND = 7007, // 이메일로 사용자 찾기 실패
// 상태 관리 오류 (7010-7019)
INVALID_STATUS_TRANSITION = 7011,
CANNOT_DEACTIVATE_SELF = 7012, // 관리자가 자신을 비활성화하려는 경우 등
// 프로필 관련 오류 (7020-7029)
INVALID_LANGUAGE_CODE = 7021,
INVALID_TIMEZONE_FORMAT = 7022,
// 테스터 관리 오류 (7030-7039)
USER_ALREADY_TESTER = 7031,
USER_NOT_TESTER = 7032,
// GDPR 관련 오류 (7040-7049)
DATA_ACCESS_REQUEST_FAILED = 7041,
DATA_DELETION_REQUEST_FAILED = 7042,
// 사용자 유형 관련 오류 (7050-7059)
INVALID_USER_TYPE_FOR_OPERATION = 7051,
// 데이터 관리 오류 (7080-7089)
USER_DATA_DELETION_FAILED = 7080,
// 기타 오류 (7090-7099)
UNKNOWN_USER_ERROR = 7099,
}
2. 오류 메시지 매핑 (참조용)
// 파일 경로: libs/feature/user/src/lib/domain/errors/user-error-messages.ref.ts
// 참고: 실제 에러 클래스에서 메시지를 직접 정의하므로, 이 매핑은 참조용입니다.
import { UserErrorCodeRef } from './user-error-codes.enum'; // 위 정의된 UserErrorCodeRef 사용 가정
export const USER_ERROR_MESSAGES_REF: Record<number, { message: string; detail: string }> = {
// 계정 관련 오류
[UserErrorCodeRef.INVALID_INPUT_DATA]: { message: 'INVALID_INPUT_DATA', detail: '요청 데이터가 유효하지 않습니다.' },
[UserErrorCodeRef.USER_EMAIL_ALREADY_EXISTS]: { message: 'USER_EMAIL_ALREADY_EXISTS', detail: '이미 사용 중인 이메일입니다.' },
[UserErrorCodeRef.USER_NOT_FOUND]: { message: 'USER_NOT_FOUND', detail: '사용자를 찾을 수 없습니다.' },
[UserErrorCodeRef.USER_PROFILE_NOT_FOUND]: { message: 'USER_PROFILE_NOT_FOUND', detail: '사용자 프로필을 찾을 수 없습니다.' },
[UserErrorCodeRef.SERVICE_ACCOUNT_CREATION_FAILED]: { message: 'SERVICE_ACCOUNT_CREATION_FAILED', detail: '서비스 계정 생성에 실패했습니다.' },
[UserErrorCodeRef.USER_EMAIL_NOT_FOUND]: { message: 'USER_EMAIL_NOT_FOUND', detail: '해당 이메일의 사용자를 찾을 수 없습니다.' },
// 상태 관리 오류
[UserErrorCodeRef.INVALID_STATUS_TRANSITION]: { message: 'INVALID_STATUS_TRANSITION', detail: '현재 사용자 상태에서 요청한 작업으로 변경할 수 없습니다.' },
[UserErrorCodeRef.CANNOT_DEACTIVATE_SELF]: { message: 'CANNOT_DEACTIVATE_SELF', detail: '자기 자신을 비활성화할 수 없습니다.' },
// 프로필 관련 오류
[UserErrorCodeRef.INVALID_LANGUAGE_CODE]: { message: 'INVALID_LANGUAGE_CODE', detail: '유효하지 않은 언어 코드입니다.' },
[UserErrorCodeRef.INVALID_TIMEZONE_FORMAT]: { message: 'INVALID_TIMEZONE_FORMAT', detail: '유효하지 않은 타임존 형식입니다.' },
// 테스터 관리 오류
[UserErrorCodeRef.USER_ALREADY_TESTER]: { message: 'USER_ALREADY_TESTER', detail: '사용자가 이미 테스터로 지정되어 있습니다.' },
[UserErrorCodeRef.USER_NOT_TESTER]: { message: 'USER_NOT_TESTER', detail: '사용자가 테스터로 지정되어 있지 않습니다.' },
// GDPR 관련 오류
[UserErrorCodeRef.DATA_ACCESS_REQUEST_FAILED]: { message: 'DATA_ACCESS_REQUEST_FAILED', detail: '데이터 접근 요청 처리에 실패했습니다.' },
[UserErrorCodeRef.DATA_DELETION_REQUEST_FAILED]: { message: 'DATA_DELETION_REQUEST_FAILED', detail: '데이터 삭제 요청 처리에 실패했습니다.' },
// 사용자 유형 관련 오류
[UserErrorCodeRef.INVALID_USER_TYPE_FOR_OPERATION]: { message: 'INVALID_USER_TYPE_FOR_OPERATION', detail: '해당 사용자 유형으로는 이 작업을 수행할 수 없습니다.' },
// 데이터 관리 오류
[UserErrorCodeRef.USER_DATA_DELETION_FAILED]: { message: 'USER_DATA_DELETION_FAILED', detail: '사용자 데이터 완전 삭제 중 오류가 발생했습니다.' },
// 기타 오류
[UserErrorCodeRef.UNKNOWN_USER_ERROR]: { message: 'UNKNOWN_USER_ERROR', detail: '알 수 없는 사용자 관련 오류가 발생했습니다.' },
};
User 도메인 에러 클래스 정의
User 도메인에서 발생하는 특정한 오류 상황들은 @core/error-handling 의 DomainError 또는 SystemError를 상속받아 구체적인 에러 클래스로 정의합니다.
// 파일 경로: libs/feature/user/src/lib/domain/errors/user.errors.ts
import { DomainError, SystemError, ErrorMetadata } from '@core/error-handling';
import { UserErrorCodeRef } from './user-error-codes.enum'; // 문서의 UserErrorCodeRef enum 임포트 가정
// --- 계정 관련 오류 --- (7000-7009)
export class InvalidUserInputError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.INVALID_INPUT_DATA, '요청 데이터가 유효하지 않습니다.', 400, metadata);
}
}
export class UserEmailAlreadyExistsError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_EMAIL_ALREADY_EXISTS, '이미 사용 중인 이메일입니다.', 409, metadata);
}
}
export class UserNotFoundError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_NOT_FOUND, '사용자를 찾을 수 없습니다.', 404, metadata);
}
}
export class UserEmailNotFoundError extends DomainError {
constructor(email: string, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_EMAIL_NOT_FOUND, `해당 이메일(${email})의 사용자를 찾을 수 없습니다.`, 404, { ...metadata, email });
}
}
export class UserProfileNotFoundError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_PROFILE_NOT_FOUND, '사용자 프로필을 찾을 수 없습니다.', 404, metadata);
}
}
export class ServiceAccountCreationError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
// SystemError의 경우 HTTP 상태 코드는 기본적으로 500이거나, BaseError에서 지정한 값을 따릅니다.
super(UserErrorCodeRef.SERVICE_ACCOUNT_CREATION_FAILED, '서비스 계정 생성에 실패했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
// --- 상태 관리 오류 --- (7010-7019)
export class InvalidStatusTransitionError extends DomainError {
constructor(fromStatus: string, toStatus: string, metadata?: ErrorMetadata) {
super(
UserErrorCodeRef.INVALID_STATUS_TRANSITION,
`현재 상태(${fromStatus})에서 대상 상태(${toStatus})로 변경할 수 없습니다.`,
409, // Conflict
{ ...metadata, fromStatus, toStatus }
);
}
}
export class CannotDeactivateSelfError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.CANNOT_DEACTIVATE_SELF, '자기 자신을 비활성화할 수 없습니다.', 403, metadata);
}
}
// --- 프로필 관련 오류 --- (7020-7029)
export class InvalidLanguageCodeError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.INVALID_LANGUAGE_CODE, '유효하지 않은 언어 코드입니다.', 400, metadata);
}
}
export class InvalidTimezoneFormatError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.INVALID_TIMEZONE_FORMAT, '유효하지 않은 타임존 형식입니다.', 400, metadata);
}
}
// --- 테스터 관리 오류 --- (7030-7039)
export class UserAlreadyTesterError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_ALREADY_TESTER, '사용자가 이미 테스터로 지정되어 있습니다.', 400, metadata);
}
}
export class UserNotTesterError extends DomainError {
constructor(metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_NOT_TESTER, '사용자가 테스터로 지정되어 있지 않습니다.', 400, metadata);
}
}
// --- GDPR 관련 오류 --- (7040-7049)
export class DataAccessRequestError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.DATA_ACCESS_REQUEST_FAILED, '데이터 접근 요청 처리에 실패했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
export class DataDeletionRequestError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.DATA_DELETION_REQUEST_FAILED, '데이터 삭제 요청 처리에 실패했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
// --- 사용자 유형 관련 오류 --- (7050-7059)
export class InvalidUserTypeForOperationError extends DomainError {
constructor(operation: string, userType: string, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.INVALID_USER_TYPE_FOR_OPERATION, `사용자 유형(${userType})은(는) ${operation} 작업을 수행할 수 없습니다.`, 403, { ...metadata, operation, userType });
}
}
// --- 데이터 관리 오류 (7080-7089) ---
export class UserDataDeletionError extends SystemError {
// 클래스명 변경 UserDataDeletionFailed -> UserDataDeletionError
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.USER_DATA_DELETION_FAILED, '사용자 데이터 완전 삭제 중 오류가 발생했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
// --- 기타 오류 --- (7090-7099)
export class UnknownUserError extends SystemError {
constructor(originalError?: Error, metadata?: ErrorMetadata) {
super(UserErrorCodeRef.UNKNOWN_USER_ERROR, '알 수 없는 사용자 관련 오류가 발생했습니다.', { ...metadata, originalErrorMessage: originalError?.message });
}
}
주요 특징:
- 각 에러 클래스는 User 도메인의 특정 오류 상황을 명확하게 나타냅니다.
DomainError또는SystemError를 상속받아 공통 에러 처리 메커니즘(GlobalExceptionFilter)과 통합됩니다.- 생성자에서 고유 코드(
UserErrorCodeRefenum 사용), 사용자 메시지, HTTP 상태 코드(필요시 기본값 재정의), 메타데이터를 설정합니다. - 상태 전이 오류(
InvalidStatusTransitionError)처럼 컨텍스트 정보를 포함하여 더 상세한 오류 메시지를 제공할 수 있습니다.
사용 예시
정의된 에러 클래스는 UserService, UserProfileService 등 필요한 로직 내에서 throw 키워드를 사용하여 발생시킵니다. 발생된 에러는 GlobalExceptionFilter에 의해 자동으로 처리됩니다.
UserService에서 사용 예시
// 파일 경로: libs/feature/user/src/lib/domain/services/user.service.ts
import { Injectable } from '@nestjs/common';
import { ErrorLoggerService } from '@core/error-handling'; // 로깅 서비스 주입
import {
UserNotFoundError,
UserEmailAlreadyExistsError,
InvalidStatusTransitionError,
// PermissionDeniedError, // Auth 에러 재사용 또는 User 전용 정의 필요. 이 예시에서는 주석 처리
} from '../errors/user.errors'; // 정의된 에러 임포트
import { UserRepository } from '../repositories/user.repository';
import { User } from '../entities/user.entity';
// ... 다른 import ...
@Injectable()
export class UserService {
constructor(private readonly errorLogger: ErrorLoggerService, private readonly userRepository: UserRepository) {}
async getUserById(userId: string, requestingUserId: string): Promise<User> {
const user = await this.userRepository.findById(userId);
if (!user) {
// --- 사용자를 찾을 수 없을 때 UserNotFoundError 발생 ---
throw new UserNotFoundError({ userId });
}
// const isAdmin = false; // 실제 권한 확인 로직
// if (user.id !== requestingUserId && !isAdmin) {
// // --- 권한 없을 때 PermissionDeniedError 발생 (Auth 또는 공통 에러 사용 고려) ---
// throw new PermissionDeniedError({ requestingUserId, targetUserId: userId });
// }
return user;
}
async createUser(data: CreateUserDto): Promise<User> {
const existingUser = await this.userRepository.findByEmail(data.email);
if (existingUser) {
// --- 이메일 중복 시 UserEmailAlreadyExistsError 발생 ---
throw new UserEmailAlreadyExistsError({ email: data.email });
}
// ... (생성 로직)
const newUser = await this.userRepository.create(/* ... */);
return newUser;
}
async activateUser(userId: string, reason: string): Promise<User> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new UserNotFoundError({ userId });
}
const originalStatus = user.status;
try {
user.activate(); // 엔티티 내 상태 변경 로직 호출
} catch (error) {
// --- 상태 전이 실패 시 InvalidStatusTransitionError 발생 ---
throw new InvalidStatusTransitionError(originalStatus, UserStatus.ACTIVE, { reason: error.message });
}
// ... (업데이트 로직)
const updatedUser = await this.userRepository.update(user);
// TODO: 이벤트 발행 (UserActivatedEvent)
return updatedUser;
}
// ... 기타 메서드에서 필요한 에러 throw ...
}
컨트롤러 및 가드에서의 사용
UserController나 관련 가드에서는 일반적으로 서비스에서 발생한 에러를 그대로 전달하여 GlobalExceptionFilter가 처리하도록 합니다. 특정 권한 검사 등 가드 로직에서 직접 에러를 발생시켜야 하는 경우, User 도메인 또는 Auth 도메인의 적절한 에러 클래스(예: UserNotFoundError 또는 공통 PermissionDeniedError 등)를 사용합니다.
결론
User 도메인의 오류 처리는 CoreErrorHandlingModule의 GlobalExceptionFilter와 BaseError를 기반으로 합니다. 각 도메인은 필요한 특정 오류 상황을 나타내는 커스텀 에러 클래스를 정의하고, 비즈니스 로직 내에서 이를 throw하여 사용합니다. 이를 통해 애플리케이션 전체적으로 일관되고 관리하기 쉬운 오류 처리 메커니즘을 구축할 수 있습니다.