예시

@Entity()
export class Exercise extends Timestamps {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'enum', enum: BodyPart })
  bodyPart: BodyPart;

  @Column()
  exerciseName: string;

  @OneToMany(() => WorkoutLog, (workoutLog) => workoutLog.exercise)
  workoutLogs: WorkoutLog[];

  @OneToMany(() => RoutineToExercise, (routineToExercise) => routineToExercise.exercise)
  routineToExercises: RoutineToExercise[];

  constructor();
  constructor(params: { bodyPart: BodyPart; exerciseName: string });
  constructor(params?: { bodyPart: BodyPart; exerciseName: string }) {
    super();
    if (params) {
      this.exerciseName = params.exerciseName;
      this.bodyPart = params.bodyPart;
    }
  }
}

  constructor :  클래스에서 객체의 설계도 또는 청사진으로, 특정 타입의 객체를 생성하고 초기화하는데 사용됩니다.

  • constructor() 초기화할 속성이 없는 경우에 사용
  • constructor(params? : {})  매개변수가 선택적이라는 의미 (optional)
  • constructor(params : {}) 매개변수가 선택적이라는 의미 (optional)

위 코드에서 매개변수의 초기화가 꼭 필요하기 때문에 

constructor() 와 constructor(params? : {}) 는 필요없는 코드

 

  .....
  // 이 와 같이 수정
  constructor(params: { bodyPart: BodyPart; exerciseName: string }){
    super();
    if (params) {
      this.exerciseName = params.exerciseName;
      this.bodyPart = params.bodyPart;
    }
  }

constructor(params:{}) 에서 params를 선언했는데  왜 if(params) 가 필요할까??

( TypeORM 공식문서에서 보면)

When using an entity constructor its arguments must be optional. Since ORM creates instances of entity classes when loading from the database, therefore it is not aware of your constructor arguments.

 

ORM 은 앱을 실행시킬때 엔티티의 인스턴스를 생성한다. 그러면 초기화 값을 받지 못한 params 는 undefined 가 되어 버리기 때문에 앱 실행 중에 오류가 발생한다.

 

 

 

참고자료

TypeORM 공식문서

https://seungtaek-overflow.tistory.com/15

예시  email  검증

// 'class-validator' 패키지에서 필요한 클래스와 함수를 가져옵니다.
import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

// 이것은 특정 것을 검증하기 위한 데코레이터입니다. 여기서는 이메일을 검사하고 있습니다.
@ValidatorConstraint({ async: false })
class IsCustomEmailConstraint implements ValidatorConstraintInterface {
  // 이 'validate' 함수는 제공된 이메일이 유효한지 확인합니다.
  validate(email: string, args: ValidationArguments) {
    // 이메일이 유효하다고 간주되려면 일치해야 하는 패턴입니다.
    const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/;
    // 이메일이 패턴과 일치하지 않는지 확인
    if (!emailRegex.test(email)) {
      // 일치하지 않으면 '잘못된 형식'이라고 말합니다.
      args.constraints[0] = 'invalidFormat';
      return false;
    }

    // 이메일의 첫 부분(‘@’ 전)에 특수 문자가 있는지 검사합니다.
    const specialChars = /[!#$%&'*+/=?^_`{|}~-]/;
    const localPart = email.split('@')[0];

    // 이메일의 첫 부분의 첫 번째 또는 마지막 문자가 특수 문자인지 확인합니다.
    if (specialChars.test(localPart[0]) || specialChars.test(localPart[localPart.length - 1])) {
      args.constraints[0] = 'specialCharFirstOrLast';
      return false;
    }

    // 이메일의 첫 부분에서 두 특수 문자가 나란히 있는지 확인합니다.
    for (let i = 0; i < localPart.length - 1; i++) {
      if (specialChars.test(localPart[i]) && specialChars.test(localPart[i + 1])) {
        args.constraints[0] = 'consecutiveSpecialChars';
        return false;
      }
    }

    // 모든 검사를 통과하면, 이메일은 유효합니다.
    return true;
  }

  // 이 함수는 무엇이 잘못되었는지에 따라 기본 오류 메시지를 제공합니다.
  defaultMessage(args: ValidationArguments) {
    const failureReason = args.constraints[0];
    switch (failureReason) {
      case 'invalidFormat':
        return '이메일 형식이 잘못되었습니다. 표준 이메일 형식(예: user@example.com)을 따라야 합니다.';
      case 'specialCharFirstOrLast':
        return '이메일이 유효하지 않습니다. 이메일 주소의 첫 번째 또는 마지막 문자로 특수 문자가 올 수 없습니다.';
      case 'consecutiveSpecialChars':
        return '이메일이 유효하지 않습니다. 특수 문자가 연속해서 두 번 이상 나타날 수 없습니다.';
      default:
        return '이메일이 유효하지 않습니다.';
    }
  }
}

// 이 함수는 사용자 정의 이메일 검증기를 생성하기 위해 사용됩니다.
export function IsEmailCustom(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    // 이 코드는 클래스의 속성에 사용자 정의 이메일 검증기를 등록합니다.
    registerDecorator({
      target: object.constructor, 
      propertyName: propertyName, 
      options: validationOptions, 
      constraints: [],            
      validator: IsCustomEmailConstraint, 
    });
  };
}

 

ValidatorConstraintInterface

  커스텀 validation 로직을 제공하기 위해 꼭 구현해야하는 인터페이스 

→ 자신이 만든 custom valdators (검증자) 에 있는 custion validation(검증) 규칙을 제공하기 위해 꼭 구현해야하는 interface

→ implements 를 이용하여 해당 속성을 다른 클래스에 주입

 

ValidationArguments

→ 유효성 검사 과정에 대한 정보를 제공하기 위해 사용

→ validation 규칙을 확인하는 과정 중에 보내고 싶은 메세지를 저장

추가 코드 설명 : IsEmailCustom

IsEmailCustom 함수는 이메일 주소의 유효성을 검사하는 사용자 정의 검증기를 적용하기 위해 사용됩니다. 이 함수는 다음과 같이 구성되어 있습니다:

  1. 함수 인자: validationOptions - 이것은 선택적 인자로, 검증기의 동작을 설정할 수 있는 옵션을 제공합니다. 예를 들어, 특정 메시지를 표시하거나, 검증 실패 시 행동을 정의할 수 있습니다.
  2. 반환되는 함수: 이 함수는 object (대상 객체)와 propertyName (속성 이름)을 인자로 받습니다. 이 두 인자를 사용하여 특정 클래스의 특정 속성에 검증기를 적용합니다.
  3. registerDecorator 함수: 이 내장 함수는 실제로 검증기를 해당 속성에 연결하는 역할을 합니다. 여기서는 몇 가지 주요 옵션을 설정합니다:
    • target: 검증기를 적용할 클래스의 생성자입니다. 즉, 검증기가 어떤 클래스의 어떤 속성에 적용될지 정의합니다.
      • object.constructor는 JavaScript에서 매우 중요한 개념 중 하나, object.constructor는 해당 객체의 생성자 함수를 참조합니다. 생성자 함수는 객체를 생성하고 초기화하는 데 사용되는 특별한 메소드입니다.
    • propertyName: 검증기가 적용될 속성의 이름입니다.
    • options: 검증 시 적용할 추가 옵션들입니다. (예:  에러메세지)
    • constraints: 검증기에 전달할 추가적인 제약 조건을 배열 형태로 제공합니다.
    • validator: 실제로 이메일 유효성을 검사할 로직을 포함하고 있는 파일을 지정합니다. : IsCustomEmailConstraint 클래스

object.constructor의 역할

  • registerDecorator에서 object.constructor를 사용하는 이유는, 해당 데코레이터를 적용할 "클래스"를 정확하게 지정하기 위함입니다. 클래스의 인스턴스(즉, 객체)가 제공될 때, object.constructor를 통해 이 객체가 어떤 클래스에서 생성되었는지를 알 수 있습니다. 이 정보를 사용하여 클래스 레벨에서 속성이나 메소드에 특정 데코레이터를 연결할 수 있습니다.
  • 예시
class Person {
    constructor(name) {
        this.name = name;
    }
}

let joey = new Person("joey");

joey.constructor // Person class 의 구조가 출력됩니다.

데코레이터 사용 예시 

export class SignUpRequestDto {
  @IsEmailCustom()
  email: string;
}

 @IsEmailCustom() 은 자동으로 registerDecorator를 호출하여, SignUpRequestDto 클래스의 email 속성에 유효성 검사기를 적용합니다. 즉, target 파라메터에 SignUpRequestDto 클래스가 할당되어 SignUpRequestDto 구조를 파악하고, propertyName 에 할당된 email에 유효성 검사기인 "IsCustomEmailConstraint" 이 적용 되어집니다.

 

 

참고자료

- https://medium.com/@admin_1497/nestjs%EC%97%90%EC%84%9C-custom-validator-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-ffd94c5447f2#26a5

- class-validator 라이브러리

 

F.I.R.S.T 원칙은 단위 테스트가 가져야 할 특성과 원칙에 관해서 이야기하고 있습니다.

Fast

단위 테스트는 빨라야 한다.

- 빠른 실행은 개발 과정에서 테스트를 자주 수행할 수 있게 하고, 즉각적인 피드백을 제공하여 개발 속도를 향상시킵니다.

Isolated

단위 테스트는 외부 요인에 종속적이지 않고 독립적으로 실행되어야 한다.

- 하나의 테스트 당 하나의 기능만을 테스트해야 문제의 원인을 좁혀가는데 도움이 됩니다.

Repeatable

단위 테스트는 실행할 때마다 같은 결과를 만들어야 한다.

- 테스트가 외부 환경이나 상태에 의존하지 않고 일관된 결과를 보장하도록 함으로써, 신뢰할 수 있는 테스트 환경을 조성해야합니다.

Self-validating

단위 테스트는 스스로 테스트를 통과했는지 아닌지 판단할 수 있어야 한다.

-테스트 결과를 간단하게 '성공' 또는 '실패'로 판단할 수 있게 하여, 결과 해석에 대한 모호성을 제거하고 자동화된 테스트 실행이 가능하게 합니다.

Timely

단위 테스트는 프로덕션 코드가 테스트에 성공하기 전에 구현되어야 한다. 

- 개발 초기에 문제를 발견하고 해결함으로써, 나중에 발생할 수 있는 복잡한 버그와 관련 비용을 줄일 수 있습니다.

 

 

 

 

참고자료

https://techblog.woowahan.com/17404/

https://velog.io/@sdb016/%EC%A2%8B%EC%9D%80-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-FIRST%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC

console.log()

의존성이 없으며 사용하기가 간단하다. 하지만

- 로깅을 켜고 끄는 기능이 없다.

- 별도의 설정없이는 로그레벨을 제공하기 어렵다.

 

Logging : 애플리케이션에서 발생하는 이벤트, 오류, 경고 등을 기록하는 프로세스

- 로그레벨 : 

  • DEBUG:디버깅 목적으로 시스템에 대한 자세한 정보를 나타냄
  • INFO: 일반적인 정보 메시지로, 애플리케이션의 정상 동작을 나타냄.
  • WARN: 경고 메시지로, 잠재적인 문제를 나타냄.
  • ERROR: 오류 메시지로, 애플리케이션의 일부 기능이 실패했음을 나타냄.
  • FATAL: 치명적인 오류로, 애플리케이션이 중단되어야 할 정도로 심각한 문제.

- 사용하는 이유

  1. 디버깅 : 애플리케이션에서 발생하는 문제를 식별하고 해결하기 위해 로그를 사용
  2. 성능모니터링 :  응답시간, 메모리 사용량, 데이터베이스 쿼리 등을 로그로 기록하여 애플리케이션의 성능을 최적화 하기위해 사용
  3. 보안 : 악성 공격, 인증 실패, 권한 부여 문제 등을 로그로 기록하여 보안 위협을 예방하기 위해 사용
  4. 운영  모니터링 : 애플리케이션의 실행 흐름, 작업 완료 여부 등을 추적할 수 있고, 이를 통해 애플리케이션 운영에 필요한 조치를 취하기 위해 사용

사용할 로그 레벨별 메세지 서비스 작성 및 모듈작성

logger.service.ts

import { ConsoleLogger, Injectable } from '@nestjs/common';

@Injectable()
export class LoggerService extends ConsoleLogger {

  log(message: any, ...optionalParams: any[]) {
    super.log(`${message}`, ...optionalParams);
  }

  error(message: any, ...optionalParams: any[]) {
    super.error(` ${message}`, ...optionalParams);
  }

  warn(message: any, ...optionalParams: any[]) {
    super.warn(` ${message}`, ...optionalParams);
  }

  debug(message: any, ...optionalParams: any[]) {
    super.debug(`${message}`, ...optionalParams);
  }
}

 

logger.module.ts

import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}

 

메인에 설정

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { ValidationPipe } from '@nestjs/common';
import { initializeTransactionalContext } from 'typeorm-transactional';
// 추가된 코드
import { LoggerService } from './common/Logger/logger.service';

async function bootstrap() {
  initializeTransactionalContext();
  const app = await NestFactory.create(AppModule, { bufferLogs: true });// 로그설정
  app.useLogger(app.get(LoggerService));// 서비스 설정
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );
  const configService = app.get(ConfigService);
  const port = configService.get<string>('PORT') as string;
  const hostIP = configService.get<string>('HOST_IP') as string;
  await app.listen(port, hostIP);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

 

 

app api 에 적용

app.controller.ts

import ....

@Controller('routines')
export class RoutineController {
  // 로그 헤더에 표시됨 -> [AppController] (message)
  // 해당 컨트롤러에 맞게 변경
  private readonly logger = new Logger(AppController.name); 
  constructor(private readonly routineService: RoutineService) {}


  @Get()
  @UseGuards(JwtAuthGuard)
  getRoutine(@Body() getRoutineRequest: GetRoutineRequestDto, @Request() req: any) {
    this.logger.log(`start getRoutine DTO : ${getRoutineRequest.name}`); // 로그 생성
    return this.routineService.getRoutineByName(getRoutineRequest, req.user);
  }
  ...
}

 

app.service.ts

import ...

@Injectable()
export class AppService {
  // 해당 서비스에 맞게 변경 ex)PostService.name
  private readonly logger = new Logger(AppService.name);
  constructor(
  	....
  ) {}


  async getRoutineByName(getRoutineRequest: GetRoutineRequestDto, user: User) {
    const { name } = getRoutineRequest;
    const routines = await this.routineRepository.findOne({where:{name}});
    if(!routines){
       this.logger.log(`Routines using name:'${name}' can not find`);
       throw new BadRequestException(`Routines using name:'${name}' can not find`);
    }
   
    this.logger.log(`found routines using ${name}`);
    return routines;
  }

}

 

 

참고자료

https://docs.nestjs.com/techniques/logger

+ Recent posts