본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev17

서비스 하나를 요청 스코프로 바꿨더니 앱 전체가 느려진 이유

  • #Engineering Note
  • #NestJS
  • #Performance

문제 발생

요청마다 다른 값을 담으려고 서비스 하나를 요청 스코프로 바꿨습니다.

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {}

그 뒤로 응답 시간이 눈에 띄게 느려졌고, 아무 관계 없어 보이는 서비스에서 상태가 요청마다 초기화된다는 제보가 들어왔습니다.

원인 분석

Nest의 기본 스코프는 싱글턴입니다. 인스턴스 하나를 애플리케이션 전체가 공유하고, 부트스트랩 때 만들어집니다. REQUEST는 요청마다 새로 만들고 끝나면 회수하며, TRANSIENT는 주입받는 쪽마다 각자의 인스턴스를 받습니다.

문제는 이 스코프가 한 클래스에만 머물지 않는다는 점입니다. 공식 문서가 그대로 말합니다 — REQUEST 스코프는 주입 체인을 타고 올라갑니다. 요청 스코프 provider에 의존하는 컨트롤러는 그 자신도 요청 스코프가 됩니다.

그래서 싱글턴이던 서비스가 요청 스코프 서비스를 하나 주입받는 순간 같이 요청 스코프가 됩니다. 그 서비스를 쓰던 다른 싱글턴도 연쇄적으로 따라갑니다. "상태가 매번 초기화된다"는 제보는 이 연쇄의 결과였습니다.

성능도 문서가 경고합니다 — 요청 스코프 provider를 쓰면 애플리케이션 성능에 영향이 있습니다. 평균 응답 시간이 느려집니다. 다만 문서는 잘 설계된 경우 지연 증가가 대략 5% 수준이라고도 덧붙입니다 — 즉 스코프 자체가 금지가 아니라, 어디까지 번지는지 알고 써야 한다는 뜻입니다.

해결 방안

  1. 번지는 범위를 먼저 그려봅니다. 요청 스코프로 바꾸기 전에 "이걸 주입받는 것이 무엇인지"를 따라가 봅니다. 컨트롤러까지 닿으면 그 컨트롤러 전체가 요청마다 새로 만들어집니다.

  2. 요청 데이터만 필요하면 스코프를 바꾸지 않습니다. 대부분은 인자로 넘기면 끝입니다.

@Injectable()
export class OrderService {
  create(userId: number, dto: CreateOrderDto) { ... }   // 싱글턴 유지
}
  1. 컨트롤러에서 꺼내 내려보냅니다. 컨트롤러는 어차피 요청마다 핸들러가 실행되므로 여기서 꺼내는 것이 자연스럽습니다.
@Post()
create(@Req() req: Request, @Body() dto: CreateOrderDto) {
  return this.orderService.create(req.user.id, dto);
}
  1. 정말 필요하면 그 클래스에만 좁게 겁니다. 여러 곳이 쓰는 공통 서비스에 걸면 애플리케이션 절반이 따라옵니다.
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {}
  1. 멀티테넌시라면 durable provider를 검토합니다. 문서가 이 용도로 제시하는 기능으로, 테넌트 ID처럼 공통 속성을 공유하는 요청들 사이에서 DI 하위 트리를 재사용합니다. 요청마다 트리를 통째로 다시 만들지 않습니다.
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantContext {}
  1. TRANSIENT는 "요청별"이 아닙니다. 주입받는 쪽마다 별개 인스턴스를 준다는 뜻이라, 같은 요청 안에서도 여러 개가 생깁니다. 요청 단위 상태를 여기에 담으면 서로 다른 인스턴스를 보게 됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.