ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [NestJS] Prisma 7과 JWT로 철벽 보안 로그인 구현하기 (feat. 드라이버 어댑터의 늪)
    프로젝트/개인프로젝트 2026. 3. 2. 16:27

    [NestJS] Prisma 7 Driver Adapter 도입 및 JWT 기반 인증 시스템 구축 (F-03)

    본 포스팅은 'Smart Diet Agent' 프로젝트의 인가(Authorization) 시스템 구축 과정에서 발생한 기술적 이슈와 의사결정 과정을 정리한 기록입니다.

    1. 개요

    • 작업 내용: JWT를 활용한 인증/인가 시스템 구축 및 Prisma 7 마이그레이션
    • 요구사항: F-03 (사용자 인가 관리)
    • 핵심 스택: NestJS, Prisma 7, Passport.js, PostgreSQL

    2. Architecture Decision Records (ADR)

    기능 구현에 앞서 기술적 부채를 최소화하기 위해 다음과 같은 설계 결정을 내렸습니다.

    ADR-001: Prisma 7 드라이버 어댑터 패턴 채택

    Prisma 7.4.1 버전의 Breaking Changes에 대응하기 위해 기존의 datasource 설정 방식을 폐기하고 드라이버 어댑터 패턴을 도입했습니다.

    • 결정: @prisma/adapter-pg 채택 및 prisma.config.ts를 통한 설정 일원화.
    • 이유: 런타임 DB 연결 계층의 유연성 확보 및 최신 보안 패치 적용.

    ADR-002: 인증 전략 수립

    • Password Hashing: bcrypt를 통한 키 스트레칭 및 솔팅 적용.
    • Token Strategy: Stateless 환경 구현을 위한 JWT(JSON Web Token) 채택.

    3. 본격적인 JWT 인증 시스템 구축

    3.1 필수 패키지 설치

    NestJS 환경에서 Passport 기반의 JWT 인증 체계를 구축하기 위해 필요한 핵심 라이브러리들을 설치했습니다.

    • @nestjs/jwt: JWT 생성 및 검증을 위한 NestJS 래퍼 모듈
    • @nestjs/passport, passport-jwt: 인증 미들웨어인 Passport 연동 및 JWT 전략 라이브러리
    • @nestjs/config: 환경 변수(.env) 관리를 위한 모듈
    # 관련 패키지 설치
    npm install @nestjs/jwt @nestjs/passport passport passport-jwt @nestjs/config
    npm install -D @types/passport-jwt

    3.2 JwtModule 비동기 설정

    보안을 위해 JWT_SECRET과 EXPIRES_IN 등의 설정값은 소스 코드에 하드코딩하지 않고 환경 변수(.env)에서 관리합니다. 이를 위해 ConfigService가 완전히 로드된 후 모듈을 초기화하도록 registerAsync 방식을 채택했습니다.

    // src/auth/auth.module.ts
    
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        // getOrThrow를 사용하여 런타임 시 필수 설정값 누락 방지
        secret: configService.getOrThrow<string>('JWT_SECRET'),
        signOptions: { 
          // ms 라이브러리 규격에 따른 만료 시간 설정 (ex: '3600s')
          expiresIn: configService.get<string>('JWT_EXPIRES_IN') as any 
        },
      }),
    })

    의존성 주입(DI)을 통해 ConfigService를 활용함으로써, 인증 모듈이 환경 설정 모듈에 유연하게 대응할 수 있도록 구조화했습니다.


    4. 주요 구현 상세

    4.1 JwtModule 비동기 설정

    ConfigService를 주입받아 환경 변수에서 시크릿 키를 로드하는 비동기 등록 방식을 사용했습니다.

    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.getOrThrow<string>('JWT_SECRET'),
        signOptions: { 
          expiresIn: configService.get<string>('JWT_EXPIRES_IN') as any 
        },
      }),
    })

    4.2 JwtStrategy 및 Guard 구현

    PassportStrategy를 상속받아 HTTP 요청 헤더의 Bearer 토큰을 추출 및 검증하는 로직을 구축했습니다.

    • 추출 및 검증: ExtractJwt.fromAuthHeaderAsBearerToken()을 통해 토큰을 확보하고, 설정된 secretOrKey로 서명의 유효성을 판단합니다.
    • 사용자 주입: validate 메서드 내에서 페이로드의 식별자(sub)를 이용해 DB 내 실존 사용자인지 검증한 후, 해당 유저 객체를 Request 객체에 자동으로 주입하여 후속 로직에서 활용할 수 있게 설계했습니다.

    5. Troubleshooting: TypeScript 엄격한 타입 체크 대응

    문제 상황

    ConfigService.get() 메서드를 사용하여 환경 변수를 로드할 때, 반환 타입이 string | undefined로 추론되면서 JwtModulePassport 옵션의 기대 타입(string)과 충돌하는 정적 컴파일 에러가 발생했습니다.

    원인 분석

    TypeScript의 strictNullChecks 옵션이 활성화된 환경에서, 런타임 시 환경 변수(.env) 누락으로 인한 undefined 참조 가능성을 컴파일 단계에서 차단하기 위한 제약 사항임을 확인했습니다.

    해결 방안

    1. getOrThrow<T>() 적용: get() 대신 getOrThrow()를 사용하여 환경 변수 미존재 시 런타임에서 즉시 예외를 발생시킴으로써, 반환 타입에서 undefined를 제거하고 타입 안정성을 확보했습니다.
    2. Explicit Type Casting: signOptionsexpiresIn 필드와 같이 라이브러리 간 인터페이스 타입이 상이한 경우, as any 또는 명시적 타입 캐스팅을 통해 타입 불일치 문제를 우회하여 해결했습니다.

    6. 결과 및 검증

    Swagger UI 연동

    @ApiBearerAuth() 데코레이터를 적용하여 Swagger 명세서 상에서 JWT 인증 테스트를 진행할 수 있는 환경을 구축했습니다. 전역 addBearerAuth() 설정을 통해 모든 보호된 엔드포인트에 대한 인증 인터페이스를 통일했습니다.

    인가(Authorization) 테스트

    JwtAuthGuard가 적용된 /auth/profile 엔드포인트를 대상으로 다음과 같이 테스트를 수행했습니다.

    • Case 1: 유효한 토큰 포함 시 200 OKreq.user 데이터 반환 확인.

    • Case 2: 토큰 누락 또는 유효하지 않은 토큰 전송 시 401 Unauthorized 정상 차단 확인.


    7. 결론 및 향후 계획

    인증 시스템의 기초 설계와 인가 가드(Guard) 구현을 완료함으로써 보안 계층의 안정성을 확보했습니다. 이를 기반으로 다음 단계인 F-04: 식단 기록(Meal CRUD) API 개발로 전환할 예정입니다.

    향후 식단 데이터의 소유권 검증(Ownership Validation) 로직을 Guard 계층에서 범용적으로 처리할지, 혹은 Service 계층에서 비즈니스 로직으로 포함할지에 대한 아키텍처 설계를 구체화할 계획입니다.

Designed by Tistory.