본문 바로가기
카테고리 없음

트렐로 프로젝트 - 멤버 가드를 통해 인가 처리하기

by 엔 터 2024. 11. 12.

가드 알아보기

Express에서는 인증∙인가를 미들웨어로 처리하지만, NestJS에서는 Guard라는 개념을 사용한다. NestJS에서 Guard를 사용하는 이유는 다음과 같습니다:

  1. 역할 분리: Guard는 인증과 인가에 초점을 맞춘 단순한 흐름을 제공한다.
  2. 의존성 주입 및 테스트 용이성: Guard는 클래스 기반으로 작성되어 DI(Dependency Injection)를 활용할 수 있으며, 테스트가 용이한다.
  3. 컨텍스트 접근: Guard는 ExecutionContext를 통해 요청이 어느 컨트롤러의 어느 핸들러에 도달했는지 파악하여 더 세밀한 인가 처리가 가능한다.

자세한 내용은 가드 공식 문서를 참고.
https://docs.nestjs.com/guards

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

개요

이번 프로젝트에서는 Trello 클론을 개발하며, 워크스페이스, 보드, 리스트, 카드, 댓글, 파일, 체크리스트 등에 대한 접근 권한을 설정해야 했습니다. 이를 위해 로그인한 유저가 해당 워크스페이스의 멤버인지 확인하는 과정을 Guard로 구현했다.

MemberGuard 설계

모든 요청에서 JWT 인증을 수행하고, 이후 유저가 요청한 워크스페이스에 속한 멤버인지를 확인하도록 Guard에서 처리했다. 이를 통해 유효한 멤버만 리소스에 접근할 수 있도록 제한했다.

import ...

@Injectable()
export class MemberGuard extends AuthGuard('jwt') implements CanActivate {
  constructor(
    @InjectRepository(MemberEntity)
    private readonly memberRepository: Repository<MemberEntity>,
    @InjectRepository(WorkspaceEntity)
    private readonly workspaceRepository: Repository<WorkspaceEntity>,
    // ... 생략된 의존성 주입
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // JWT 인증 수행
    const authenticated = await super.canActivate(context);
    if (!authenticated) {
      return false;
    }

    const request = context.switchToHttp().getRequest();
    const userId = request.user?.id;
    const reqWorkspaceId = request.params.workspaceId || request.body.workspaceId;
    if (!userId) {
      return false;
    }

    let workspaceId: number;
    if (reqWorkspaceId) {
      const workspace = await this.workspaceRepository.findOne({ where: { id: reqWorkspaceId } });
      if (!workspace) throw new NotFoundException('workspace not found');
      workspaceId = workspace.id;
    }
    // ... 생략된 조건 처리

    // 사용자(member)가 해당 workspaceId의 멤버인지 확인
    const member = await this.memberRepository.findOne({
      where: {
        user: { id: userId },
        workspace: { id: workspaceId },
      },
    });

    return !!member;
  }
}

엔티티 의존성 주입 문제

각 엔티티(워크스페이스, 보드, 리스트, 카드, 댓글 등)에 대한 의존성을 모두 주입해야 했다.
Guard가 각 리소스에 대한 정보를 확인하기 위해 필요했지만, 컨트롤러나 서비스에서 사용하지 않는 엔티티에도 의존성을 주입해야 하는 점이 문제점이다.

import ...

@Module({
  imports: [
    TypeOrmModule.forFeature([
      BoardEntity,
      WorkspaceEntity,
      ListEntity,
      CardEntity,
      MemberEntity,
      CommentEntity,
      ChecklistEntity,
      ItemEntity,
      FileEntity,
    ]),
  ],
  controllers: [BoardController],
  providers: [BoardService],
})
export class BoardModule {}

가드에서 모든 엔티티에 대한 권한을 확인하려면 각 엔티티에 접근해야 했기에 의존성 주입이 불가피했다.
더 효율적으로 구현하려면 가드를 리소스별로 나누어야 했지만,
이는 관리가 어려워질 수 있어 MemberGuard 하나에서 모든 리소스를 확인하도록 설계했다.

리팩토링 계획

현재 방식은 기능적으로 잘 동작하지만, 더 효율적이고 깔끔한 방법으로 리팩토링할 필요가 있다.
특히 필요한 엔티티만 주입하는 방식으로 개선할 방법을 고민 중입니다. 이후 모듈 분리의존성 주입 구조 최적화 등을 통해 구조를 개선할 계획이다.

또한, 실제 Trello와 같은 서비스에서는 인가 처리가 어떻게 이루어지는지클라이언트와의 처리 방식도 궁금한 점이 있다.
클라이언트가 자신이 속한 워크스페이스만 접근할 수 있도록 되어 있지만, 클라이언트 와의 연동을 더 고려해 볼 계획에 있다.