낙서장이자 오답 노트이자 컨닝 페이퍼

패스트캠퍼스 환급챌린지 26일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문

NestJS

패스트캠퍼스 환급챌린지 26일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기

NangIn 2025. 4. 26. 20:33

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

강의 내용 정리   

 

User Module 작업하기

 

인증 구현의 시작: 회원가입 준비

  • 인증 기능은 복잡하므로 모듈을 분리하여 개발
    • User 모듈 → 사용자 정보 관리
    • Auth 모듈 → 인증 전담 (재사용성 고려)

 

Nest CLI로 모듈 및 리소스 생성

  • Auth 모듈 생성
    • nest g resource auth
      
    • REST API 선택, CRUD 엔드포인트는 생성 안 함
  • User 모듈 생성
    • nest g resource user
      
    • REST API 선택, CRUD 엔드포인트는 생성함

 

User Entity 구성

  • BaseTable 상속 (공통 컬럼 포함)
  • 주요 컬럼 정의:
    • export enum Role {
        ADMIN = 'admin',
        PAID_USER = 'paid_user',
        USER = 'user',
      }
      
      @Entity('users')
      export class User extends BaseEntity {
        @PrimaryGeneratedColumn()
        id: number;
      
        @Column({ unique: true })
        email: string;
      
        @Column()
        password: string;
      
        @Column({ enum: Role, default: Role.USER })
        role: Role;
      }
      

 

User 서비스 및 CRUD 구현

  • UserModule에 import
    • // user.module.ts
      @Module({
        imports: [TypeOrmModule.forFeature([User])],
        controllers: [UserController],
        providers: [UserService],
      })
      
      // app.module.ts
      entities: [Movie, MovieDetail, Director, Genre, User],
      
  • UserService에 Repository 주입:
    • constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>
      ) {}
      
  • CRUD 기능 순서대로 구현:
    • 전체 조회:
      • findAll() {
          return this.userRepository.find();
        }
        
    • 단일 조회:
      • async findOne(id: number) {
          const user = await this.userRepository.findOne({ where: { id } });
          if (!user) throw new NotFoundException('존재하지 않는 사용자입니다.');
          return user;
        }
        
    • 생성:
      • create(createUserDto: CreateUserDto) {
          return this.userRepository.save(createUserDto);
        }
        
    • 수정:
      • async update(id: number, updateUserDto: UpdateUserDto) {
            const user = await this.userRepository.findOne({ where: { id } });
            if (!user) throw new NotFoundException('존재하지 않는 사용자입니다.');
            await this.userRepository.update({ id }, updateUserDto);
            return await this.userRepository.findOne({ where: { id } });
        }
        
    • 삭제:
      • remove(id: number) {
          return this.userRepository.delete(id);
        }
        

 

Controller 설정 및 Pipe 적용

  • @Param('id', ParseIntPipe)로 라우트 파라미터 타입 검증 적용
  • 기본적인 RESTful 라우트 생성 완료

 

Auth Module 회원가입 작업하기

 

인증 시작 및 구조 설계

  • 목표: 인증 시스템 구축의 첫걸음으로 회원가입 기능부터 구현
  • 구성 모듈
    • User 모듈: 사용자 관리 담당 (CRUD)
    • Auth 모듈: 인증 담당 (회원가입, 로그인 등)

 

User 엔티티 정의

  • 기본 필드
    • @Entity('users')
      export class User extends BaseEntity {
        @PrimaryGeneratedColumn()
        id: number;
      
        @Column({ unique: true })
        email: string;
      
        @Column()
        @Exclude({ toPlainOnly: true })
        password: string;
      
        @Column({ enum: Role, default: Role.USER })
        role: Role;
      }
      
      • id: 기본 PK
      • email: @Column({ unique: true })
      • password: @Exclude({ toPlainOnly: true }) → 응답 시 숨김
      • role: enum 기반 권한 관리 (Admin, PaidUser, User)
        • 기본값: User
  • 직렬화 설정
    • @Exclude() 데코레이터 → 비밀번호, createdAt, updatedAt, version 숨김
    • 컨트롤러에 @UseInterceptors(ClassSerializerInterceptor) 설정 필요

 

AuthController - 회원가입 API 구현

  • 라우팅
    • POST /auth/register: 회원가입 요청을 처리
  • 요청 형식
    • 헤더: Authorization: Basic <base64-encoded-email:password>
  • 모듈 등록
    • @Module({
        imports: [TypeOrmModule.forFeature([User])],
        controllers: [AuthController],
        providers: [AuthService],
      })
      
  • 컨트롤러 구조
    • @Post('register')
      registerUser(@Headers('authorization') token: string) {
        return this.authService.register(token);
      }
      
  • 서비스 등록
    •   constructor(
          @InjectRepository(User)
          private readonly userRepository: Repository<User>,
        ) {}
      

 

AuthService - 회원가입 로직 구현

  1. Basic 토큰 파싱
    •   parseBasicToken(rawToken: string) {
          // 1) 토큰을 ' ' 기준으로 스플릿한 후 토큰 값만 추출하기
          // ['Basic', 'token']
          const basicSplit = rawToken.split(' ');
      
          if (basicSplit.length !== 2) {
            throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
          }
      
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          const [_, token] = basicSplit;
      
          // 2) 추출한 토큰을 base64 디코딩해서 이메일과 비밀번호로 나눈다.
          const decoded = Buffer.from(token, 'base64').toString('utf-8');
      
          // "email:password"
          const tokenSplit = decoded.split(':');
      
          if (tokenSplit.length !== 2) {
            throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
          }
      
          const [email, password] = tokenSplit;
      
          return { email, password };
        }
      
  2. 이메일 중복 검사
    • const user = await this.userRepository.findOne({ where: { email } });
      if (user) throw new BadRequestException('이미 존재하는 이메일입니다.');
      
  3. 비밀번호 해싱 처리
    • bcrypt 설치
      • pnpm i bcrypt 
        pnpm i -D @types/bcrypt
        
    • bcrypt 사용: hash(password, saltRounds)
      • const hash = await bcrypt.hash(
          password,
          this.configService.get('HASH_ROUNDS') as number,
        );
        
        await this.userRepository.save({ email, password: hash });
        
        return this.userRepository.findOne({ where: { email } });
        
        • 해싱된 비밀번호로 저장
        • saltRounds는 .env 또는 config 모듈에서 관리 (기본값 10)
          • HASH_ROUNDS=10
            
          • // app.module.ts
            validationSchema: Joi.object({
              // ...
              HASH_ROUNDS: Joi.number().required(),
            }),
            
            // auth.service.ts
            constructor(
              // ...
              private readonly configService: ConfigService,
            ) {}
            
  4. 사용자 등록 및 반환
    • 해싱된 비밀번호로 DB 저장
    • 저장된 사용자 조회 후 반환 (단, password는 직렬화에서 제외됨)

 

회원가입 테스트 (Postman)

  • Base64 수동 인코딩: email:password → Authorization: Basic xxxxxxx
  • Postman 내장 기능 사용: Authorization 탭 → Basic Auth 선택 → 자동 인코딩
  • 요청 성공 시 사용자 정보 반환 (비밀번호 등은 노출되지 않음)

 

학습 후기         

 

이번 학습을 통해 인증 시스템을 처음부터 직접 구축하는 과정이 왜 이렇게 세밀하게 설계되어야 하는지를 뼈저리게 느꼈습니다. 특히 사용자 모듈과 인증 모듈을 분리하여 개발하는 설계 방식을 통해, 모듈화된 구조가 얼마나 프로젝트 유지보수성과 확장성에 중요한 역할을 하는지 체감할 수 있었습니다. 앞으로 다양한 인증 방식이 추가될 수 있다는 점을 고려하여 처음부터 인증 기능을 분리하는 것이 현명한 판단임을 깨달았습니다.

 

User 엔티티를 설계할 때 단순히 email과 password만 저장하는 것이 아니라, Role을 명시적으로 구분하여 관리하는 방식이 굉장히 인상 깊었습니다. 이 작은 설계가 나중에 사용자 권한을 다루거나, 관리자와 일반 사용자를 구분하는 복잡한 로직에서 큰 역할을 하게 된다는 사실을 배웠습니다. 또한 패스워드에 @Exclude 데코레이터를 적용하고 직렬화 처리를 통해 응답 데이터에서 민감 정보를 안전하게 숨기는 과정을 보면서, 보안은 단순히 데이터베이스 저장뿐만 아니라 응답 레이어에서도 고려해야 한다는 점을 알게 되었습니다.

 

Auth 모듈을 통해 회원가입 로직을 구현하면서 Basic Auth 방식을 사용하는 것이 꽤나 깔끔한 패턴이라는 것을 알게 되었습니다. Base64 인코딩된 이메일과 비밀번호를 받아 서버에서 디코딩하고 검증하는 흐름은 직접 구현해보니 단순하지만 강력한 방식이라는 느낌을 받았습니다. 다만 Base64 인코딩은 암호화가 아니라 단순 인코딩일 뿐이므로 반드시 HTTPS로 통신해야 실제 보안이 확보된다는 점도 함께 인지하게 되었습니다.

 

비밀번호를 bcrypt를 사용해 해싱하는 부분도 매우 중요한 학습이었습니다. 해싱 과정을 직접 적용해보니, 단순한 저장이 아니라 보안성을 높이는 핵심 프로세스임을 실감할 수 있었습니다. 특히 saltRounds를 통해 해싱 속도를 조절하고, 이 값을 환경변수로 관리하는 패턴을 통해 실무에서는 모든 하드코딩을 피하고 설정 가능하게 만들어야 한다는 좋은 습관도 함께 배웠습니다.

 

전체적인 작업 흐름을 따라가면서 느낀 점은, 하나의 회원가입 기능을 완성하기 위해 수많은 작은 고려사항들이 존재한다는 것이었습니다. 그동안 당연하게 생각했던 로그인/회원가입 기능들이 사실은 보안, 설계, 확장성 등 다양한 측면을 모두 고려한 결과물이라는 사실을 새삼 깨닫게 되었습니다. 이번 과정을 통해 단순히 코드만 따라치는 것이 아니라, 왜 이렇게 설계하는지까지 이해하는 것이 얼마나 중요한지 깊이 체감했습니다. 앞으로도 비슷한 기능을 구현할 때 이번에 배운 설계 원칙과 보안 개념을 반드시 적용하겠다는 다짐을 하게 되었습니다.

 

학습 인증샷       

                       

수강 인증 사진
학습 인증 샷
공부 시작 시간
공부 종료 시간

https://abit.ly/lisbva