06. Guard와 유저 인증

작성일:2024.08.01|조회수:0

06. Guard와 유저 인증

Guards

Nest.js의 가드(Guard)는 요청이 특정 조건을 만족하는지 판단하여, 해당 요청을 실제로 처리할지 여부를 결정하는 역할을 한다. 주로 인증(Authentication)과 인가(Authorization), 혹은 그 외의 사전 조건 검증을 구현하는 데 사용된다.

가드는 미들웨어와 유사한 역할을 하지만, 실행 컨텍스트(ExecutionContext) 에 접근할 수 있다는 점에서 더 강력하다. 이를 통해 현재 요청이 어떤 핸들러, 어떤 컨트롤러에서 실행되는지에 대한 정보를 함께 고려할 수 있으며, 그만큼 세밀한 제어가 가능해진다.

가드는 적용 위치에 따라 글로벌 가드, 컨트롤러 레벨 가드, 핸들러 레벨 가드로 나뉜다.

JS
// 글로벌 가드
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
TS
// 컨트롤러 가드
@Controller('')
@UseGuards(AuthGuard)
export class Controller {
  @Get()
  findAll() {}
}
TS
// 핸들러 가드
@Get()
@UseGuards(AuthGuard)
findAll() {}

커스텀 가드를 사용한 유저 인증

Nest.js에서 커스텀 가드는 CanActivate 인터페이스를 구현한다. 핵심 메서드인 canActivateExecutionContext를 인자로 받으며, 이를 통해 요청과 응답 객체에 접근할 수 있다.

HTTP 기반 애플리케이션의 경우 switchToHttp()를 사용해 Request, Response 객체를 가져오는 것이 일반적이다.

TS
import { Request } from 'express';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }

  private validateRequest(request: Request): boolean {
    const accessToken = request.cookies['accessToken'];

    if (!accessToken) {
      throw new UnauthorizedException('로그인이 필요합니다');
    }

    try {
      request.user = this.authService.verify(accessToken, 'access');
      return true;
    } catch {
      throw new UnauthorizedException('로그인이 필요합니다');
    }
  }
}

canActivateboolean을 반환하거나 예외를 던진다. false를 반환하면 Nest.js는 기본적으로 403 Forbidden 응답을 반환하지만, 인증 로직에서는 보통 명시적으로 예외를 던지는 방식을 사용한다.

이 구현에서 중요한 부분은 authService.verify의 결과를 request.user에 주입하는 지점이다. 이를 통해 이후 컨트롤러나 인터셉터, 파이프 등에서 별도의 인증 처리 없이 사용자 정보를 재사용할 수 있다.

Guard 이후의 인가 처리

가드는 “로그인 여부”를 보장하는 역할에 집중하고, 실제 권한 검증은 컨트롤러나 서비스 레이어에서 처리하는 편이 명확하다.

아래 예제에서는 인증된 유저가 실제로 리소스의 소유자인지를 확인하는 인가 로직을 컨트롤러에서 수행한다.

TS
import { User } from '@prisma/client';

@Controller('report')
export class ReportController {
  constructor(private reportService: ReportService) {}

  @Delete(':reportId')
  @UseGuards(AuthGuard)
  async deleteReport(
    @Param('reportId') reportId: string,
    @Req() req: Request,
  ) {
    const { id } = req.user as User;

    const isOwner = await this.reportService.checkIsOwner(reportId, id);

    if (!isOwner) {
      throw new ForbiddenException('권한이 없습니다.');
    }

    return this.reportService.deleteReport(reportId);
  }
}

이처럼 인증은 Guard, 인가(권한 판단)는 비즈니스 로직으로 분리하는 구조가 읽기에도, 유지보수 측면에서도 명확하다.

Passport와 유저 인증

@nestjs/passport 패키지를 사용하면 지금까지 직접 구현한 커스텀 AuthGuard를 훨씬 적은 코드로 대체할 수 있다. 특별히 복잡한 인증 로직이 필요하지 않다면, 실무에서도 Passport 기반 가드를 사용하는 경우가 많다.

PY
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  controllers: [AuthController],
  providers: [AuthService, AuthRepository, JwtStrategy],
  imports: [
    JwtModule.register({}),
    PassportModule.register({ defaultStrategy: 'jwt' }),
  ],
  exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}

PassportModule 역시 동적 모듈이기 때문에 register 메서드를 사용하며, { defaultStrategy: 'jwt' } 옵션을 통해 기본 전략을 지정한다.

JwtStrategy

JwtStrategy는 JWT 검증 로직을 정의하는 클래스이며, PassportStrategy를 상속받아 구현한다. 개념적으로 보면 Guard를 생성하기 위한 설정 클래스에 가깝다.

TS
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @Inject(jwtConfig.KEY) private jwt: ConfigType<typeof jwtConfig>,
    private authRepository: AuthRepository,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (request) =>
          request?.cookies?.['accessToken'] ?? '',
      ]),
      secretOrKey: jwt.accessSecret,
    });
  }

  async validate(payload: any) {
    const user = await this.authRepository.findUserByEmail(payload.email);

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}

내 경우에는 토큰을 쿠키로 전달하고 있기 때문에 jwtFromRequest를 커스텀 extractor로 구현했다. 만약 Authorization 헤더를 사용하는 방식이라면 ExtractJwt.fromAuthHeaderAsBearerToken()을 사용해 훨씬 간결하게 작성할 수 있다.

Passport를 쓸 것인가?

여러 방법을 검토한 끝에 @nestjs/passport 패키지를 알게 되었지만, 나는 여전히 커스텀 가드를 직접 만들어 사용하는 쪽을 선호한다. 가장 큰 이유는 JwtStrategy 하나를 만들기 위해 @nestjs/passport, passport-jwt 등 여러 패키지를 추가로 설치해야 하는 점이 마음에 들지 않는다. 누군가에게는 사소한 문제일 수 있지만, 가능한 한 의존성을 최소화하고 싶은 나에게는 중요한 판단 기준이다.

또한 커스텀 가드는 내가 원하는 방식으로 로직을 자유롭게 구성할 수 있다. 반면 JwtStrategy는 실제로 하는 일에 비해 설정해야 할 요소가 많고, 추상화 레벨이 다소 높게 느껴진다. 그래서 이 글에서 Passport를 소개하긴 했지만, 실제 프로젝트에서는 커스텀 가드를 만들어 사용하는 방향을 유지할 생각이다.

더 읽어보기

  • 2025.05.21

    14. Redis를 사용한 세션 관리 및 캐싱

    새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…

  • 2025.01.30

    비즈니스 로직은 어디에 있어야 할까

    Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…

  • 2024.12.07

    13. Jest 테스트 구성

    이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…

  • 2024.11.14

    12. GraphQL로 요청 처리

    GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP…

  • 2026.04.11

    Trie 자료구조

    문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…

  • 2026.03.19

    Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까

    웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…

댓글

댓글을 불러오는 중...