Passport를 이용한 인증 구현
인증과 권한 부여를 구분하고 Passport 로컬 전략과 가드로 아이디·비밀번호 로그인 흐름을 구현합니다.
3장에서는 NestJS 애플리케이션의 데이터를 안전하게 관리하는 방법을 다뤘습니다.
4장에서는 웹 애플리케이션의 핵심 요소인 인증(Authentication)과 권한 부여(Authorization)를 다룹니다.
사용자 로그인, 개인정보 보호, 기능 접근 제어 등 현대 웹 서비스에서 반드시 필요한 주제입니다.
NestJS는 Node.js 생태계의 대표 인증 미들웨어인 Passport.js와 긴밀하게 통합되어, 다양한 인증 전략을 비교적 쉽게 구현할 수 있습니다.
이번 절에서는 Passport의 기본 개념을 짚고, NestJS에 적용해 사용자 인증을 구현하는 흐름을 살펴봅니다.
인증과 권한 부여란?
두 용어는 자주 혼용되지만, 명확히 다른 개념을 가지고 있습니다.
- 인증(Authentication): 당신이 누구인지 확인하는 과정입니다. 사용자가 주장하는 신원(ID)이 진짜인지 확인하는 절차입니다. 예를 들어, 아이디와 비밀번호를 통해 로그인하는 것이 인증의 대표적인 예입니다. (예: 로그인하셨군요, 환영합니다!)
- 권한 부여(Authorization): 당신이 무엇을 할 수 있는지 결정하는 과정입니다. 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 절차입니다. 예를 들어, 일반 사용자는 게시물을 읽을 수 있지만, 관리자만 게시물을 삭제할 수 있는 것이 권한 부여의 예입니다. (예: 관리자만 이 기능을 사용할 수 있습니다.)
이번 절에서는 주로 인증에 초점을 맞추고, 이 장의 RBAC 절에서 권한 부여를 더 자세히 다룹니다.
Passport.js란 무엇이며 왜 사용할까요?
Passport.js는 Node.js를 위한 강력하고 유연한 인증 미들웨어입니다.
웹 애플리케이션에 다양한 인증 전략(로컬 인증, JWT, OAuth, 소셜 로그인 등)을 쉽게 통합할 수 있도록 설계되었습니다.
Passport는 전략 실행과 성공·실패 전달을 공통 인터페이스로 묶어, 인증 어댑터를 라우팅 및 핵심 비즈니스 로직과 분리하도록 돕습니다.
Passport 사용의 주요 장점- 모듈화된 전략: Passport는 전략(Strategy)이라는 개념을 사용하여 다양한 인증 방식을 플러그인 형태로 제공합니다. 로컬(아이디/비밀번호), JWT, OAuth2 (Google, Facebook, GitHub 등), OpenID 등 수백 가지의 전략 중에서 필요한 것을 선택하여 적용할 수 있습니다.
- 유연하고 확장 가능: 특정 인증 방식에 얽매이지 않고, 필요에 따라 커스텀 전략을 만들거나 여러 전략을 조합하여 사용할 수 있습니다.
- NestJS와의 통합 용이성:
@nestjs/passport패키지가 Passport 전략을 Nest의 의존성 주입 및 Guard 구조에 연결합니다.
Passport를 이용한 로컬(Local) 인증 구현
Passport 인증 구현은 strategy 선택, validate() 반환값, guard 적용 위치, 실패 응답 기준으로 읽습니다.
Passport 로컬 인증 요청 흐름
guard·local 전략·Auth/Users 검증·역순으로 돌아오는 안전한 사용자·req.user·handler 또는 401의 실제 요청 순서이다.
Nest Passport · 현재 요청의 인증
로컬 검증 성공이 현재 요청의 req.user를 만듭니다
Local guard는 local 전략을 실행하고, 전략은 AuthService에 검증을 맡깁니다. 안전한 사용자만 현재 요청에 붙습니다. 자격 증명 실패는 401이며 저장소 장애 같은 실행 오류는 원래 예외로 전달합니다.
성공 경로
가드가 전략을 실행합니다.
LocalAuthGuard가 Passport의local전략을 시작하면, Passport Local 구현이 요청에서 기본 필드username·password를 읽어LocalStrategy.validate()에 전달합니다.검증을 위임합니다.
LocalStrategy.validate()는AuthService.validateUser()를 호출하고, 서비스는UsersService.findOne()으로 저장된 사용자 레코드를 조회합니다.안전한 사용자만 반환합니다. 자격 증명이 맞으면 민감 필드를 뺀 사용자가 전략과 가드로 돌아옵니다. 가드는 그 값을 현재 요청의
req.user에 넣고 라우트 핸들러를 통과시킵니다.
인증 실패와 실행 오류
사용자가 없거나 비밀번호가 다르면 AuthService는 null을 반환하고 전략은 UnauthorizedException을 던집니다. 가드는 HTTP 401로 요청을 끝내며 핸들러는 실행되지 않습니다.
저장소 장애 같은 예상하지 못한 오류는 잘못된 자격 증명으로 바꾸지 않습니다. 원래 오류를 전파해 Nest의 예외 처리와 관측 경계에서 다룹니다.
req.user는 이 요청의 인증 결과입니다. 세션이나 JWT처럼 다음 요청에도 사용할 로그인 상태는 이 흐름 뒤에서 별도로 구성합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 요청 파이프라인이 LocalAuthGuard를 호출하면 가드가 local 전략을 실행한다. LocalStrategy는 AuthService에 검증을 위임하고 AuthService는 UsersService에서 사용자를 찾는다. 성공하면 안전한 사용자가 역순으로 돌아와 가드가 req.user를 설정하고 핸들러를 실행하며, 실패하면 전략이 UnauthorizedException을 던져 401로 종료한다.
- 가드가 local 전략을 실행하고 전략은 AuthService로 자격 증명 검증을 위임한다. 서비스는 UsersService로 조회하여 비밀번호를 비교하고 성공한 안전한 사용자만 반환한다. 성공이 가드로 돌아오면 현재 요청의 req.user로 handler가 실행된다. 사용자 없음·불일치는 null과 401로 중단된다. 예상하지 못한 저장소 오류는 원래 예외로 전달한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | Passport 로컬 인증 요청 흐름 |
| 31 | 가드가 local 전략을 실행하고 전략은 AuthService로 자격 증명 검증을 위임한다. 서비스는 UsersService로 조회하여 비밀번호를 비교하고 성공한 안전한 사용자만 반환한다. 성공이 가드로 돌아오면 현재 요청의 req.user로 handler가 실행된다. 사용자 없음·불일치는 null과 401로 중단된다. 예상하지 못한 저장소 오류는 원래 예외로 전달한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| Guard와 전략 진입 LocalAuthGuard가 local 전략을 실행합니다. | Guard: LocalAuthGuard · 전략: local · handler: 아직 미실행 | 자격 증명 검증이 handler보다 먼저 시작됩니다. |
| 서비스로 검증 위임 LocalStrategy가 AuthService에 위임하고 UsersService에서 사용자를 찾습니다. | 조회: UsersService · 검증: AuthService · 전략: LocalStrategy | 조회 결과가 검증 결과로 돌아옵니다. |
| 사용자 전달 또는 거부 검증에 성공한 안전한 사용자만 req.user에 넣어 handler를 실행하고 사용자 없음·자격 증명 불일치는 401로 끝납니다. 예상하지 못한 저장소 오류는 원래 예외를 전파합니다. | 성공: 현재 req.user·handler 진입 · 사용자 없음·자격 증명 불일치: null→UnauthorizedException·401 · 예상하지 못한 저장소 오류: 원래 예외 전달, 401로 바꾸지 않음 · 세션 발급: 이 경로만으로 없음 | 정상 인증과 자격 증명 거절 및 기반 시설 예외를 별도 종료 경계로 유지합니다. |
Passport 로컬 인증 요청 흐름
guard·local 전략·Auth/Users 검증·역순으로 돌아오는 안전한 사용자·req.user·handler 또는 401의 실제 요청 순서이다.
Nest Passport · 현재 요청의 인증
로컬 검증 성공이 현재 요청의 req.user를 만듭니다
Local guard는 local 전략을 실행하고, 전략은 AuthService에 검증을 맡깁니다. 안전한 사용자만 현재 요청에 붙습니다. 자격 증명 실패는 401이며 저장소 장애 같은 실행 오류는 원래 예외로 전달합니다.
성공 경로
가드가 전략을 실행합니다.
LocalAuthGuard가 Passport의local전략을 시작하면, Passport Local 구현이 요청에서 기본 필드username·password를 읽어LocalStrategy.validate()에 전달합니다.검증을 위임합니다.
LocalStrategy.validate()는AuthService.validateUser()를 호출하고, 서비스는UsersService.findOne()으로 저장된 사용자 레코드를 조회합니다.안전한 사용자만 반환합니다. 자격 증명이 맞으면 민감 필드를 뺀 사용자가 전략과 가드로 돌아옵니다. 가드는 그 값을 현재 요청의
req.user에 넣고 라우트 핸들러를 통과시킵니다.
인증 실패와 실행 오류
사용자가 없거나 비밀번호가 다르면 AuthService는 null을 반환하고 전략은 UnauthorizedException을 던집니다. 가드는 HTTP 401로 요청을 끝내며 핸들러는 실행되지 않습니다.
저장소 장애 같은 예상하지 못한 오류는 잘못된 자격 증명으로 바꾸지 않습니다. 원래 오류를 전파해 Nest의 예외 처리와 관측 경계에서 다룹니다.
req.user는 이 요청의 인증 결과입니다. 세션이나 JWT처럼 다음 요청에도 사용할 로그인 상태는 이 흐름 뒤에서 별도로 구성합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 요청 파이프라인이 LocalAuthGuard를 호출하면 가드가 local 전략을 실행한다. LocalStrategy는 AuthService에 검증을 위임하고 AuthService는 UsersService에서 사용자를 찾는다. 성공하면 안전한 사용자가 역순으로 돌아와 가드가 req.user를 설정하고 핸들러를 실행하며, 실패하면 전략이 UnauthorizedException을 던져 401로 종료한다.
- 가드가 local 전략을 실행하고 전략은 AuthService로 자격 증명 검증을 위임한다. 서비스는 UsersService로 조회하여 비밀번호를 비교하고 성공한 안전한 사용자만 반환한다. 성공이 가드로 돌아오면 현재 요청의 req.user로 handler가 실행된다. 사용자 없음·불일치는 null과 401로 중단된다. 예상하지 못한 저장소 오류는 원래 예외로 전달한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | Passport 로컬 인증 요청 흐름 |
| 31 | 가드가 local 전략을 실행하고 전략은 AuthService로 자격 증명 검증을 위임한다. 서비스는 UsersService로 조회하여 비밀번호를 비교하고 성공한 안전한 사용자만 반환한다. 성공이 가드로 돌아오면 현재 요청의 req.user로 handler가 실행된다. 사용자 없음·불일치는 null과 401로 중단된다. 예상하지 못한 저장소 오류는 원래 예외로 전달한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| Guard와 전략 진입 LocalAuthGuard가 local 전략을 실행합니다. | Guard: LocalAuthGuard · 전략: local · handler: 아직 미실행 | 자격 증명 검증이 handler보다 먼저 시작됩니다. |
| 서비스로 검증 위임 LocalStrategy가 AuthService에 위임하고 UsersService에서 사용자를 찾습니다. | 조회: UsersService · 검증: AuthService · 전략: LocalStrategy | 조회 결과가 검증 결과로 돌아옵니다. |
| 사용자 전달 또는 거부 검증에 성공한 안전한 사용자만 req.user에 넣어 handler를 실행하고 사용자 없음·자격 증명 불일치는 401로 끝납니다. 예상하지 못한 저장소 오류는 원래 예외를 전파합니다. | 성공: 현재 req.user·handler 진입 · 사용자 없음·자격 증명 불일치: null→UnauthorizedException·401 · 예상하지 못한 저장소 오류: 원래 예외 전달, 401로 바꾸지 않음 · 세션 발급: 이 경로만으로 없음 | 정상 인증과 자격 증명 거절 및 기반 시설 예외를 별도 종료 경계로 유지합니다. |
가장 기본적인 인증 방식인 로컬 인증(아이디와 비밀번호 사용)을 NestJS와 Passport를 사용하여 구현해 보겠습니다.
단계 1: 필요한 패키지 설치npm install @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local사용자 정보를 관리하는 UsersModule, UsersService가 이미 있다고 가정합니다. UsersModule은 사용하는 UsersService를 providers에 등록하고 exports로 내보내야 뒤의 AuthModule에서 주입받을 수 있습니다.
아래 서비스는 3장의 DB CRUD와 별도로 인증 흐름을 읽기 위한 배열 예제입니다. findOne(username)과 userId·username·password·roles 계약을 사용하므로, 3장의 사용자 저장소를 연결하려면 먼저 조회 메서드와 반환 필드를 맞춰야 합니다. 같은 파일 이름이라는 이유로 두 서비스를 합치거나 기존 DB 모듈을 지우지 않습니다.
UsersService는 사용자 저장소를 감싸며, 여기서는 username으로 사용자 레코드를 조회하는 일만 맡습니다. 자격 증명이 맞는지 판단하는 인증 유스케이스는 뒤에서 AuthService에 둡니다.
import { Injectable } from '@nestjs/common';
// 실제 DB 연동 로직 대신 간단한 사용자 데이터 배열 사용
const users = [
{ userId: 1, username: 'testuser', password: 'password123', roles: ['user'] },
{ userId: 2, username: 'admin', password: 'adminpassword', roles: ['admin'] },
];
@Injectable()
export class UsersService {
async findOne(username: string): Promise<any | undefined> {
return users.find(user => user.username === username);
}
}위 배열과 평문 비밀번호는 인증 흐름만 확인하기 위한 예제입니다. 실제 서비스에서는 비밀번호 원문을 저장하거나 ===로 비교하지 말고, 검증된 비밀번호 해싱 라이브러리로 저장된 해시를 검증해야 합니다.
LocalStrategy)
Passport는 인증 로직을 전략으로 캡슐화합니다.
로컬 인증을 위한 LocalStrategy를 생성합니다.
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super(); // 기본 필드 이름: username, password
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}passport-local은 기본적으로 요청 본문의 username과 password 필드를 읽습니다. 이메일 같은 다른 이름을 쓰려면 super({ usernameField: 'email' })처럼 전략 옵션과 실제 요청 필드를 함께 바꿔야 합니다.
AuthModule) 생성
인증 관련 로직을 하나의 모듈로 묶습니다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { UsersModule } from '../users/users.module'; // UsersModule 임포트
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}AuthService: 사용자 조회 결과와 자격 증명을 검증하고 안전한 사용자 값을 만듭니다.LocalStrategy: Passport가 전달한 로컬 자격 증명을AuthService에 위임합니다.PassportModule: Passport 전략과 Nest Guard의 통합을 제공합니다.
AuthService) 작성
AuthService는 저장소 조회와 자격 증명 검증을 하나의 인증 유스케이스로 묶습니다. 검증 성공 값에서는 비밀번호처럼 요청 처리에 필요 없는 민감 필드를 제거합니다.
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
// 실제 JWT 발급 로직은 다음 절에서 다룰 예정입니다.
async login(user: any) {
return user;
}
}여기서도 평문 비교는 실행 흐름을 작게 보여 주기 위한 예제일 뿐입니다. 사용자 없음과 비밀번호 불일치를 같은 null 및 401 경계로 합치면 응답 내용만으로 계정 존재 여부를 구분하기 어려워집니다. 실제 서비스에서는 비밀번호 해시 검증 경로의 타이밍 차이와 로그인 시도 제한도 함께 점검해야 합니다. 반면 데이터베이스 장애처럼 예상하지 못한 오류까지 null로 바꾸면 안 됩니다. 그런 오류는 그대로 전파해 Nest의 예외 계층과 관측 시스템이 처리하게 합니다.
클라이언트의 로그인 요청을 처리하는 엔드포인트를 만듭니다.
전략 이름을 여러 라우트에 반복하지 않도록 AuthGuard('local')을 상속한 가드를 만들고 로그인 라우트에 적용합니다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}Nest의 Passport 가드는 기본적으로 세션을 사용하지 않습니다. 성공한 validate() 반환값을 현재 요청의 req.user에 넣는 것과 로그인 상태를 다음 요청까지 유지하는 것은 별도 결정입니다. JWT 방식이라면 login()에서 토큰을 발급하고 이후 요청의 JWT 전략이 토큰을 검증합니다. 서버 세션 방식이라면 세션 미들웨어와 Passport 세션 복원 미들웨어, PassportModule.register({ session: true }), 직렬화·역직렬화 구현이 추가로 필요합니다. 세션용 로그인 가드의 canActivate()에서는 먼저 await super.canActivate(context)로 request.user를 설정한 뒤 await super.logIn(request)을 호출해야 합니다.
AuthModule 임포트
현재 AppModule의 imports에 AuthModule을 추가합니다. 아래는 추가 위치를 보여 주는 발췌이며 기존 루트 모듈을 통째로 교체하는 코드가 아닙니다. 앞서 등록한 ConfigModule.forRoot({ isGlobal: true }), 선택한 DB 모듈과 다른 imports·controllers·providers는 유지하세요. 다음 절의 JWT 설정은 이 전역 ConfigService 등록을 사용합니다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [AuthModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}테스트해 보기
이 절의 요청 주소와 그림은 전역 접두사 없이 표기합니다. 앞 절의 main.ts에서 app.setGlobalPrefix('api')를 유지했다면 로그인 주소는 http://localhost:3000/api/auth/login입니다. 접두사 없는 별도 구성에서는 아래의 /auth/login을 사용합니다. 404라면 자격 증명보다 먼저 현재 main.ts의 접두사와 실제 포트를 확인하세요.
애플리케이션을 실행합니다.
npm run start:dev
Postman이나 curl을 사용하여 http://localhost:3000/auth/login으로 POST 요청을 보냅니다.
- Body는
x-www-form-urlencoded또는raw (JSON)형태로username과password를 포함해야 합니다.{ "username": "testuser", "password": "password123" } x-www-form-urlencoded를 사용하는 경우:username=testuserpassword=password123
성공 시: { "userId": 1, "username": "testuser", "roles": ["user"] }와 유사한 응답을 받게 됩니다 (AuthService의 login 메서드 반환 값).
실패 시 (존재하지 않는 사용자 또는 잘못된 비밀번호): 라우트 핸들러는 실행되지 않고 HTTP 401 Unauthorized 응답을 받습니다. 정확한 응답 본문은 Nest 버전, HTTP 어댑터, 전역 예외 필터 설정에 따라 달라질 수 있습니다.
현재 요청의 성공과 로그인 유지 구분하기
올바른 자격 증명으로 위 로그인이 성공했습니다. 그다음 요청에서 아무 쿠키나 토큰도 보내지 않았는데 “이 사용자는 방금 로그인했으니 req.user가 자동으로 있다”고 판단해도 될까요? 비밀번호가 틀린 요청에서는 AuthController.login()이 실행될까요?
가드가 만든 값의 범위로 풀이하기
현재 설정은 Passport 로그인 세션을 만들지 않습니다. 성공한 사용자는 해당 요청에만 붙으며, 이후 요청을 인증하려면 다음 절의 JWT 또는 별도 세션 복원 경로가 필요합니다. 비밀번호가 틀리면 전략이 401 예외를 던지고 가드에서 멈추므로 로그인 핸들러와 응답 유스케이스는 실행되지 않습니다. 저장소 장애는 잘못된 비밀번호와 구분해 원래 오류로 전달합니다.
마지막으로 로컬 인증을 구현할 때 각 파일이 맡는 책임과 실패 응답 경계를 한 번 더 점검해 보겠습니다.
Nest Passport · 역할과 로그인 상태
검증 책임을 나누고, 다음 요청의 상태는 세션·JWT로 이어갑니다
Guard·Strategy·AuthService·UsersService·Controller는 서로 다른 책임을 맡습니다. 현재 req.user 뒤에 세션 복원이나 JWT 발급·검증을 별도로 구성해야 다음 요청을 인증할 수 있습니다.
Guard
LocalAuthGuard가 local 전략을 실행하고 성공 결과를 현재 요청의 req.user에 넣습니다.Strategy
LocalStrategy가 요청 필드 계약을 설정하고 자격 증명 검증을 AuthService에 위임합니다.UnauthorizedException을 던집니다.Auth use case
AuthService가 사용자 레코드와 제출된 비밀번호를 검증하고 민감 필드를 제거합니다.User store
UsersService가 사용자 저장소를 조회하고 저장된 사용자 레코드를 반환합니다.Route
AuthController가 검증이 끝난 req.user로 로그인 응답 또는 토큰 발급 유스케이스를 호출합니다.서버 세션으로 이어가기
서버가 로그인 상태를 보관하고 브라우저는 세션 쿠키를 보냅니다. 세션 및 Passport 세션 복원 미들웨어, PassportModule.register()의 { session: true } 설정, 직렬화·역직렬화 구현이 필요합니다.
세션 로그인 가드는 먼저 super.canActivate()를 await하여 request.user를 설정한 뒤 super.logIn(request)으로 세션에 기록해야 합니다.
JWT로 이어가기
AuthService가 로컬 인증 성공 뒤 access token을 발급하고, 클라이언트는 보호 요청마다 Bearer token을 보냅니다.
JwtAuthGuard와 JWT 전략이 매 요청에서 서명·만료를 검증하고 새 req.user를 만듭니다. Passport 로그인 세션은 사용하지 않습니다.
세션과 JWT는 자격 증명 검증 뒤의 상태 유지 방식입니다. 둘 중 무엇을 택해도 비밀번호 검증 책임은 AuthService에, 사용자 조회 책임은 UsersService에 남습니다.
로컬 인증의 핵심은 AuthGuard가 전략을 실행하고, LocalStrategy가 자격 증명 검증을 AuthService에 위임하며, 성공한 안전한 사용자 값만 현재 요청의 req.user로 넘기는 경계를 분명히 두는 것입니다.
다음 절에서는 이 로그인 결과를 매 요청에서 다시 확인할 수 있도록 JWT(JSON Web Tokens) 기반 인증으로 확장합니다.