[2024.06.28 TIL] 내일배움캠프 52일차 (Nest.js 강의 시청, Nest.js 고급 기술, 인증, 인가, 커스텀 데코레이터, AOP, 가드, 캐싱)

My_Code·2024년 6월 28일
0

TIL

목록 보기
69/113
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 인증(Authentication)

  • 인증이란 특정 사용자가 인증된 사용자인지 확인하는 절차를 의미

  • Nest.js에서는 JWT를 통한 인증을 사용함


✏️ JWT 발급 구현하기

  • 사용자 뼈대 한 번에 구성하기
npm g resource user

  • user.entity.ts 코드 구현 (사용자 엔티티 정의)
import { IsString } from 'class-validator';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  DeleteDateColumn,
} from 'typeorm';

@Entity({
  name: 'users',
})
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @IsString()
  @Column('varchar', { length: 10, nullable: false })
  userId: string;

  @IsString()
  @Column('varchar', { length: 10, select: false, nullable: false })
  password: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt?: Date;
}

  • create-user.dto.ts 코드 구현 (회원가입에 주입될 인자 구성)
import { PickType } from '@nestjs/mapped-types';

import { User } from '../entities/user.entity';

export class CreateUserDto extends PickType(User, [
  'userId',
  'password',
] as const) {}

  • 사용자 회원가입, 로그인 Service에서 JWT 토큰을 제공하기 위한 @nestjs/jwt 설치
npm i @nestjs/jwt

  • user.module.ts 코드 구현
  • 다른 사용자 모듈에서 JWT를 사용하기 위해서 JwtModule를 imports
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';

import { User } from './entities/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [
    // 현재 모듈에서 사용할 엔티티를 설정
    TypeOrmModule.forFeature([User]),
    // JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        // .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
        secret: config.get<string>('JWT_SECRET_KEY'),
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

  • user.service.ts 코드 구현
  • 회원가입, 로그인 시 JWT 토큰이 반환되도록 수정
...

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
    private jwtService: JwtService, // JWT 토큰 생성을 위해 주입한 서비스
  ) {}

  // 로그인
  async login(loginUserDto: LoginUserDto) {
    const { userId, password } = loginUserDto;

    const user = await this.userRepository.findOne({
      where: { userId, deletedAt: null },
      select: ['id', 'password'],
    });

    if (_.isNil(user)) {
      throw new NotFoundException(`유저를 찾을 수 없습니다. ID: ${userId}`);
    }

    if (user.password !== password) {
      throw new UnauthorizedException(
        `유저의 비밀번호가 올바르지 않습니다. ID: ${userId}`,
      );
    }

    // 추가된 코드 - JWT 토큰 생성
    const payload = { id: user.id };
    const accessToken = await this.jwtService.signAsync(payload);
    return accessToken;
  }

  // 회원가입
  async create(createUserDto: CreateUserDto) {
    const existUser = await this.findOne(createUserDto.userId);
    if (!_.isNil(existUser)) {
      throw new ConflictException(
        `이미 가입된 ID입니다. ID: ${createUserDto.userId}`,
      );
    }

    const newUser = await this.userRepository.save(createUserDto);

    // 추가된 코드 - JWT 토큰 생성
    const payload = { id: newUser.id };
    const accessToken = await this.jwtService.signAsync(payload);
    return accessToken;
  }

  ...
}

✏️ JWT 검증 구현하기 (Access Token 인증 미들웨어)

  • 전역적으로 사용하기 위한 JWT 검증 미들웨어를 구현

  • 코드 자체는 기존에 Express에서 사용한 Access Token 인증 미들웨어와 아주 유사함

  • app.module.ts 코드 구현

  • JWT 검증 미들웨어를 전역적으로 사용하기 위해서 app.module에서 imports

import Joi from 'joi';

import {
  MiddlewareConsumer,
  Module,
  NestModule,
  RequestMethod,
} from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Post } from './post/entities/post.entity';
import { PostModule } from './post/post.module';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { AuthMiddleware } from './auth/auth.middleware';
import { CacheModule } from '@nestjs/cache-manager';

const typeOrmModuleOptions = {
  // useFactory는 동적 모듈의 속성을 설정하기 위해 사용
  // useFactory에서 ConfigService를 주입받아 환경변수(.env)로부터
  // 데이터베이스 설정값을 가져와서 TypeOrmModuleOptions 객체를 반환함
  useFactory: async (
    configService: ConfigService,
  ): Promise<TypeOrmModuleOptions> => ({
    type: 'mysql',
    host: configService.get('DB_HOST'),
    port: configService.get('DB_PORT'),
    username: configService.get('DB_USERNAME'),
    password: configService.get('DB_PASSWORD'),
    database: configService.get('DB_NAME'),
    entities: [Post, User],
    synchronize: configService.get('DB_SYNC'),
    logging: true,
  }),
  // useFactory에서 사용할 의존성을 주입받기 위해 사용
  inject: [ConfigService],
};

@Module({
  imports: [
    // forRoot는 ConfigModule의 정적인(하드코딩된) 기초 설정을 위해 사용
    // 여기서는 Joi를 통한 유효성 검사 설정
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DB_HOST: Joi.string().required(),
        DB_PORT: Joi.number().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_NAME: Joi.string().required(),
        DB_SYNC: Joi.boolean().required(),
      }),
    }),
    // forRootAsync는 TypeOrmModule의 동적인 기초 설정을 위해 사용 (환경변수나 데이터베이스)
    TypeOrmModule.forRootAsync(typeOrmModuleOptions),
    // JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        secret: config.get<string>('JWT_SECRET_KEY'), // .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
      }),
      inject: [ConfigService],
    }),
    PostModule,
    UserModule,
  ],
  controllers: [AppController],
  // 모듈 내부에서 사용하기 위해서 인증 미들웨어를 providers에 추가
  providers: [AppService, AuthMiddleware],
})
// 미들웨어를 사용하는 모듈은 NestModule 인터페이스를 구현해야 함
export class AppModule implements NestModule {
  // 미들웨어를 구성하기 위한 configure 메서드
  configure(consumer: MiddlewareConsumer) {
    // 미들웨어 구성을 위한 consumer 객체
    consumer
      // 적용할 미들웨어를 선택
      .apply(AuthMiddleware)
      // user/check 경로를 GET 메서드 요청에 대해 AuthMiddleware를 적용시킴
      .forRoutes({ path: 'user/check', method: RequestMethod.GET });
  }
}

  • user.controller.ts 코드 구현
  • 각 기능들의 경로를 명시적으로 표시
  • checkUser 메서드를 통해서 req.user의 데이터를 확인
import { Body, Controller, Get, Post, Req } from '@nestjs/common';

import { CreateUserDto } from './dto/create-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { UserService } from './user.service';

// /user 주소를 사용
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // /user/login 주소로 요청이 들어왔을 때 userService의 login 메서드 실행
  @Post('/login')
  async login(@Body() loginUserDTO: LoginUserDto) {
    return await this.userService.login(loginUserDTO);
  }

  // /user/signup 주소로 요청이 들어왔을 때 userService의 create 메서드 실행
  @Post('/signup')
  async createUser(@Body() createUserDTO: CreateUserDto) {
    return await this.userService.create(createUserDTO);
  }

  // /user/check 주소로 요청이 들어왔을 때 userService의 checkUser 메서드 실행
  @Get('/check')
  checkUser(@Req() req: any) {
    const userPayload = req.user;
    return this.userService.checkUser(userPayload);
  }
}

✏️ 인가(Authorization)

  • 인가는 인증된 사용자가 특정 작업을 수행할 권리가 있는지 확인하는 절차를 의미

  • 예를 들면, 게시물 작성 및 수정, 삭제는 로그인된 사용자만 사용가능하기에 정상적으로 인증된 사용자인지 확인


✏️ 커스텀 데코레이터

  • 일반적인 데코레이터는 클래스나 함수와 같은 곳에 메타 데이터를 추가하는 방법을 제공하는 것

  • 즉, 코드에 추가적인 정보를 제공해서 실행 시점에 코드의 동작 방법이 제공됨

  • 커스텀 데코레이터는 우리가 원하는 대로 데코레이터의 동작을 구현해서 사용하는 것을 의미함

  • 게시판 프로젝트를 예로 들면 커스텀 데코레이터를 통해서 사용자의 정보를 추출하거나 사용자의 역할에 따라 접근을 제어할 수도 있음

  • 커스텀 데코레이터 코드 예시

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const UserInfo = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    if (request.user) {
      return request.user;
    }
    return null;
  }
);
  • data : 데코레이터의 인자로 전달될 수 있는 데이터

  • ctx : Nest.js의 실행 컨텍스트를 나타내며 현재 HTTP 요청과 관련된 모든 정보를 가지고 있음 (즉, HTTP 요청의 req.user에 접근할 수 있음)

  • ctx.switchToHttp().getRequest()라는 메소드를 통해서 req에 접근을 하는 것

  • ctx.switchToHttp().getResponse()라는 메소드를 통해서 res에 접근을 하는 것

  • 커스텀 데코레이터 적용 예시

import { Controller, Get } from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';

@Controller('user')
export class UserController {
  @Get()
  getProfile(@UserInfo() user: any) {
    return user;
  }
}

✏️ 가드(Guard)

  • Guard는 Nest.js에서 인가를 구현할 때 특정 라우트에 대한 접근 제어를 함

  • 인가를 구현할 때 커스텀 데코레이터와 마찬가지로 꼭 필요한 요소


✏️ AOP(Aspect-Oriented Programming)

  • 여러 모듈에서 공통적으로 수행되는 로직을 중앙에서 관리하도록 하는 프로그래밍 기법

  • 이를 통해 코드의 중복을 줄이고 유지보수가 용이하게 구현이 가능함

  • 그래서 로깅이나 인증, 에러 처리와 같은 기능들을 AOP 방법으로 구현함


✏️ 인터셉터

  • 특정 작업을 수행하기 전에 추가 로직을 실행시키기 위한 AOP의 핵심 요소

✏️ 캐싱

  • 자주 변하지 않는 데이터가 지속적으로 요청되는 경우에 매번 서버에 접근하지 않고 사용자 메모리에 보관했다가 다시 요청이 오면 메모리에서 그 데이터를 꺼내주는 방법

  • cache-manager 설치 명령어

npm i @nestjs/cache-manager cache-manager

  • app.module.ts 코드 구현
  • 생각보다 쉽게 사용이 가능하고 지금은 캐시 데이터를 인메모리 형태로 사용함
...

@Module({
  imports: [
    // forRoot는 ConfigModule의 정적인(하드코딩된) 기초 설정을 위해 사용
    // 여기서는 Joi를 통한 유효성 검사 설정
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DB_HOST: Joi.string().required(),
        DB_PORT: Joi.number().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_NAME: Joi.string().required(),
        DB_SYNC: Joi.boolean().required(),
      }),
    }),
    // forRootAsync는 TypeOrmModule의 동적인 기초 설정을 위해 사용 (환경변수나 데이터베이스)
    TypeOrmModule.forRootAsync(typeOrmModuleOptions),
    // JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        secret: config.get<string>('JWT_SECRET_KEY'), // .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
      }),
      inject: [ConfigService],
    }),
    // 현재 모듈에서 사용할 캐시모듈을 imports
    CacheModule.register({
      ttl: 60000, // 데이터 캐싱 시간(밀리 초 단위, 1000 = 1초)
      max: 100, // 최대 캐싱 개수
      isGlobal: true,
    }),
    PostModule,
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService, AuthMiddleware],
})

...

  • post.service.ts 코드 구현
  • 캐싱 사용 예제로 게시물 목록 조회에 사용할 수 있음
  • 매번 데이터베이스에서 같은 정보를 가져오는 것이 리소스 낭비이기 때문에 캐시 메모리에 데이터를 저장했다가 다시 호출되면 데이터베이스에 접근하지 않고 캐시 메모리에서 바로 반환함
...

import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';

@Injectable()
export class PostService {
  constructor(
    // @InjectRepository는 어떤 엔티티(테이블)을 주입해서 사용할지 정의하는 데코레이터
    @InjectRepository(Post) private postRepository: Repository<Post>,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async create(createPostDto: CreatePostDto) {
    return (await this.postRepository.save(createPostDto)).id;
  }

  async findAll() {
    const cachedArticles = await this.cacheManager.get('articles');
    // 캐싱이 되어 있으면 캐싱에 있는 articles 데이터 반환
    if (!_.isNil(cachedArticles)) {
      return cachedArticles;
    }

    const articles = await this.postRepository.find({
      where: { deletedAt: null },
      select: ['id', 'title', 'updatedAt'],
    });

    // 캐싱된 데이터가 없으면 캐시에 articles 데이터 추가
    await this.cacheManager.set('articles', articles);
    return articles;
  }

  
  ...
}


📌 Tomorrow's Goal

✏️ 개인과제 기본 설계 및 코드 구현

  • Nest.js 프로젝트 생성, 깃허브 연결 등과 같은 기본적인 프로젝트 세팅을 진행할 예정

  • 그리고 프로젝트에 대한 ERD와 API 명세서도 구상해서 작성할 예정

  • 기본적인 설계 뿐만 아니라 세팅한 것들이 잘 돌아가는지 웹 서버를 구동할 예정

  • 가능하다면 추가적인 몇가지 API도 구현할 예정

  • 일단 주말 안으로 진행할 예정



📌 Today's Goal I Done

✔️ Nest.js 강의 시청

  • 5주차 강의를 통해서 인증, 인가에 대한 보안적인 측면에 대해 학습함

  • 여전히 Nest.js에서 지원해주는 기능들이 많지만 활용하기는 어려운 것 같음

  • 특히 관심이 있는 내용은 캐싱에 대한 내용임

  • 캐싱을 통해서 성능 향상을 도모할 수 있고 Redis와 같은 데이터베이스를 사용할 수 있으니 조금 더 공부하면 재밌는 구현을 할 수 있을 것 같음

  • 하지만 그와 같은 구현을 하기 위해서는 Nest.js와 조금 더 많이 친해질 필요가 있음


profile
조금씩 정리하자!!!

0개의 댓글