본문으로 건너뛰기

User 도메인 오류 관리 가이드 (CoreErrorHandlingModule 기반)

User 도메인에서 발생하는 모든 오류는 애플리케이션 전반의 CoreErrorHandlingModuleGlobalExceptionFilter를 통해 중앙 집중식으로 관리됩니다. 이 문서는 User 도메인 특화 오류를 정의하고 사용하는 방법에 대해 설명합니다.

참고: 전반적인 에러 처리 아키텍처, 로깅, 메트릭 수집 등은 에러 처리 코어 모듈 구현 가이드 문서를 참조하세요.

목차

  1. User 도메인 오류 코드
  2. 오류 코드 및 메시지 정의
  3. User 도메인 에러 클래스 정의
  4. 사용 예시

User 도메인 오류 코드

User 도메인의 오류 코드는 다음과 같은 체계로 구성됩니다 (7000번대 사용):

범위용도
7000-7009사용자 계정 관련 일반 오류 (조회, 생성 등)
7010-7019사용자 상태 관리 관련 오류
7020-7029사용자 프로필 관련 오류
7030-7039테스터 관리 관련 오류
7040-7049개인정보 보호 (GDPR 등) 관련 오류
7050-7059사용자 유형 관련 오류
7080-7089사용자 데이터 관리 관련 오류
7090-7099기타 User 도메인 오류

참고: 기존 숫자 코드를 사용하며, CoreErrorHandlingModuleBaseErrornumber 타입 코드를 지원합니다.

오류 코드 및 메시지 정의

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-handlingDomainError 또는 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)과 통합됩니다.
  • 생성자에서 고유 코드(UserErrorCodeRef enum 사용), 사용자 메시지, 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 도메인의 오류 처리는 CoreErrorHandlingModuleGlobalExceptionFilterBaseError를 기반으로 합니다. 각 도메인은 필요한 특정 오류 상황을 나타내는 커스텀 에러 클래스를 정의하고, 비즈니스 로직 내에서 이를 throw하여 사용합니다. 이를 통해 애플리케이션 전체적으로 일관되고 관리하기 쉬운 오류 처리 메커니즘을 구축할 수 있습니다.