코딩딩/NestJS

Nest 레전드 버그 발생

전낙타 2024. 12. 8. 13:17

NestJS에서 정적 경로와 동적 경로 충돌 문제 해결하기

NestJS로 프로젝트를 진행하면서 REST API 경로를 설계하던 중 예상치 못한 문제가 발생했다. mentor-registration과 :mentorId 경로가 충돌하면서, GET /api/mentors/mentor-registration 요청이 동적 경로로 매핑되어 버리는 현상이었다. 분명히 컨트롤러에 mentor-registration 경로를 명시했는데, 왜 이런 일이 발생한 걸까?


문제 상황: 정적 경로와 동적 경로의 충돌

아래와 같은 코드가 문제였다.

@Get(':mentorId')
async getMentorProfile(@Param('mentorId') mentorId: string) {
  return this.mentorService.getMentorProfile(mentorId);
}

@Get('mentor-registration')
@Roles(Role.ADMIN)
async getMentorProfilesForAdmin(
  @Query() getMentorAcceptReqDto: GetMentorAcceptReqDto,
) {
  return this.mentorService.getMentorProfilesForAdmin(getMentorAcceptReqDto);
}

NestJS는 라우트 경로를 처리할 때 동적 경로(:mentorId)를 정적 경로(mentor-registration)보다 우선적으로 매칭한다. 결과적으로 GET /api/mentors/mentor-registration 요청도 동적 경로로 처리되면서 HTTP 500 에러가 발생했다. 요청이 잘못된 컨트롤러로 라우팅된 것이다.


해결 방법

이 문제를 해결하기 위해 몇 가지 방법을 적용해 봤다.


1. 라우트 정의 순서 변경

NestJS는 경로를 위에서 아래로 매칭하므로, 정적 경로를 동적 경로보다 위에 배치하면 된다.

@Get('mentor-registration') // 정적 경로를 먼저 정의
@Roles(Role.ADMIN)
async getMentorProfilesForAdmin(
  @Query() getMentorAcceptReqDto: GetMentorAcceptReqDto,
) {
  return this.mentorService.getMentorProfilesForAdmin(getMentorAcceptReqDto);
}

@Get(':mentorId') // 동적 경로를 나중에 정의
async getMentorProfile(@Param('mentorId') mentorId: string) {
  return this.mentorService.getMentorProfile(mentorId);
}

이렇게 경로를 재배치한 뒤에는 mentor-registration 요청이 올바른 컨트롤러로 매핑되었다.


2. 동적 경로에 정규식 추가

mentorId가 특정 형식(예: 숫자)일 경우에만 동적 경로로 매칭되도록 정규식을 추가했다. 이렇게 하면 mentor-registration처럼 문자열로 된 경로와 동적 경로의 충돌을 방지할 수 있다.

@Get(':mentorId(\\d+)') // mentorId가 숫자인 경우에만 매칭
async getMentorProfile(@Param('mentorId') mentorId: string) {
  return this.mentorService.getMentorProfile(mentorId);
}

3. 경로를 별도의 컨트롤러로 분리

정적 경로와 동적 경로를 분리된 컨트롤러에서 관리하면 구조가 더 명확해지고 충돌도 방지할 수 있다.

@Controller('api/mentors/admin') // Admin 관련 경로를 별도 컨트롤러로 분리
export class MentorAdminController {
  @Get('mentor-registration')
  async getMentorProfilesForAdmin(
    @Query() getMentorAcceptReqDto: GetMentorAcceptReqDto,
  ) {
    return this.mentorService.getMentorProfilesForAdmin(getMentorAcceptReqDto);
  }
}

@Controller('api/mentors') // 일반 Mentor 관련 경로
export class MentorController {
  @Get(':mentorId')
  async getMentorProfile(@Param('mentorId') mentorId: string) {
    return this.mentorService.getMentorProfile(mentorId);
  }
}

 

결론

NestJS로 REST API를 설계하면서 가장 불만족스러웠던 부분은, Spring에 비해 세세한 부분까지 직접 신경 써야 한다는 점이다. 정적 경로와 동적 경로의 충돌은 NestJS의 설계 철학 상 개발자가 직접 해결해야 하는 문제로 여겨지지만, 이는 생산성을 저하시킬 뿐만 아니라 개발 경험에서도 큰 차이를 만들어낸다.

 

Spring에서는 이런 문제가 애초에 발생하지 않는다. Spring MVC는 정적 경로와 동적 경로의 우선순위를 명확히 처리하고, 이를 별도로 관리할 필요가 없다. 프레임워크가 개발자를 대신해 이러한 세부 사항을 처리해 준다는 점에서, NestJS는 이 부분에서 분명히 뒤처진다.

 

살짝 정떨어지는중