NestJS에서 MikroORM을 “실무처럼” 쓰기: RequestContext(AsyncLocalStorage)·Identity Map·Unit of Work를 한 번에 정리
TypeScript ORM을 운영 단계까지 끌고 가면, 단순 CRUD보다 요청 단위 상태 격리, flush()/트랜잭션 경계, 메모리/응답 불안정 같은 문제가 먼저 터집니다. MikroORM은 이를 위해 Identity Map, Unit of Work, 그리고 RequestContext(AsyncLocalStorage 기반)를 핵심 메커니즘으로 제공합니다.
이 글은 NestJS 환경에서 MikroORM을 쓸 때 가장 자주 헷갈리는 지점을 “운영 관점”에서 묶어 설명합니다. (본문의 사실/주장은 모두 공식 문서/공식 README 원문 근거만 사용했습니다.)
1) 왜 RequestContext가 핵심인가: “요청마다 Identity Map은 분리”
MikroORM은 Identity Map 패턴을 사용해, 같은 엔티티(같은 PK)를 한 요청 내에서 재조회해도 동일 인스턴스를 돌려줄 수 있고, 불필요한 SELECT를 줄일 수 있다고 설명합니다. 또한 Identity Map은 결과 캐시가 아니라, 한 요청(단일 작업 범위) 안에서 성능/일관성을 돕기 위한 것이라고 명시합니다.
그리고 “요청마다 고유한 Identity Map을 유지해야 한다”는 결론으로 이어집니다. 문서에는 이를 위해 EntityManager를 fork() 해서 요청마다 별도의 컨텍스트(Identity Map)를 갖게 하라고 안내합니다.
아키텍처/요청 흐름 도식

2) RequestContext는 무엇을 해주나: “전역 EM을 호출해도 요청 전용 EM로 라우팅”
MikroORM 공식 문서(Identity Map and Request Context)에는, DI 컨테이너(예: NestJS)에서 Repository/EntityManager 인스턴스가 싱글턴처럼 주입되는 상황에서 요청 단위로 깨끗한 상태를 만들기 어렵다는 설명이 있습니다. 이를 해결하기 위해 RequestContext helper를 제공하며, 내부적으로 Node.js의 AsyncLocalStorage를 사용해 요청 컨텍스트를 격리한다고 명시합니다.
핵심은 이겁니다:
- 요청 시작 시점에
RequestContext.create(orm.em, next)가orm.em.fork()를 호출해 요청 전용 EntityManager를 async context에 붙입니다. - 이후 코드에서 전역
orm.em를 사용하더라도, 내부적으로em.getContext()가 async context의 fork를 우선 사용하도록 해 요청 범위로 자동 라우팅됩니다.
3) 실전 코드/설정 스니펫 (1): NestJS에서 @mikro-orm/nestjs 기본 구성
@mikro-orm/nestjs 공식 README는, 루트 모듈에 MikroOrmModule.forRoot()를 등록하고, 이후 EntityManager 및 MikroORM를 주입받아 사용할 수 있다고 안내합니다. 또한 특정 모듈 스코프에서 Repository를 등록하려면 forFeature()를 쓰라고 설명합니다.
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
@Module({
imports: [
MikroOrmModule.forRoot({
entities: ['../dist/entities'],
entitiesTs: ['../src/entities'],
dbName: 'my-db-name.sqlite3',
type: 'sqlite',
baseDir: __dirname,
}),
// feature module에서 Repository 등록
// MikroOrmModule.forFeature([Photo])
],
})
export class AppModule {}
4) Unit of Work/flush()를 운영에 맞게 이해하기: “flush()가 변경분을 트랜잭션으로 반영”
MikroORM의 Unit of Work 문서에는 다음이 명시되어 있습니다:
em.flush()시점에 Identity Map이 관리(managed) 중인 엔티티들에서 변경을 감지해 UPDATE/INSERT 등의 작업을 계산합니다.em.flush()는 (드라이버가 지원한다면) DB 트랜잭션 내부에서 계산된 변경분을 실행합니다. 문서에서 이를 “Implicit Transactions”로 설명합니다.- 트랜잭션 경계를 직접 제어하려면
em.transactional(cb)를 사용할 수 있다고 예시를 제공합니다.
5) 실전 코드/설정 스니펫 (2): HTTP 밖(큐/크론)에서도 RequestContext 만들기
MikroORM 공식 문서는 HTTP 미들웨어로 RequestContext를 여는 방식을 설명하면서, “미들웨어는 일반 HTTP 요청에서만 실행된다”는 한계를 함께 언급합니다. 그리고 큐 핸들러/스케줄러 같은 환경에서는 @CreateRequestContext() 데코레이터를 사용할 수 있다고 안내합니다(또한 v6 이전에는 이름이 @UseRequestContext()였다고 문서에 명시).
@mikro-orm/nestjs 공식 README 역시 같은 맥락으로, 큐/스케줄 작업에서 @CreateRequestContext() 사용 예시를 제공합니다.
import { Injectable } from '@nestjs/common';
import { MikroORM } from '@mikro-orm/core';
import { CreateRequestContext } from '@mikro-orm/core';
@Injectable()
export class JobsService {
constructor(private readonly orm: MikroORM) {}
@CreateRequestContext()
async runNightlyJob() {
// 이 메서드 내부는 별도의 request context(=forked EM)에서 실행됨
}
}
6) 의사결정 표: NestJS에서 “요청 단위 EM 격리”를 어떻게 구현할까
아래 선택지는 모두 공식 문서/README에 근거가 있는 것만 정리했습니다.
| 선택지 | 언제 쓰나 | 장점 | 주의/한계 |
|---|---|---|---|
| RequestContext 미들웨어(AsyncLocalStorage) | 일반적인 HTTP API | 전역 orm.em 호출도 요청 전용 컨텍스트로 자동 라우팅(문서에 내부 동작 설명 있음) |
문서에서 “요청 핸들러 직전, ORM 사용하는 커스텀 미들웨어보다 앞”에 등록 권장(등록 순서 주의) |
@CreateRequestContext() 데코레이터 |
큐/스케줄/크론 등 HTTP 밖 작업 | 미들웨어 없는 실행 경로에도 request context 생성 가능 | 문서에서 “top-level 메서드에만 사용, 중첩 사용 금지”를 명시 |
| Global Context 허용(allowGlobalContext / 환경변수) | 주로 테스트 등 특수 케이스 | 컨텍스트 체크를 완화(문서에 언급) | 문서에서 “전역 EM을 request context 없이 쓰는 것은 거의 항상 잘못”이라고 경고하며, v5부터 global identity map은 기본적으로 불가라고 명시 |
7) 장애 징후 → 원인 → 조치 요약표 (운영에서 자주 만나는 패턴)
아래는 공식 문서에 명시된 위험 시나리오(전역 Identity Map 공유로 인한 문제)와, 문서/README에서 제시한 해결책만 정리한 것입니다.
| 장애 징후 | 가능한 원인(공식 근거) | 조치(공식 근거) |
|---|---|---|
| 프로세스 메모리 사용량이 요청 처리량에 비례해 계속 증가 | 문서: 요청 간에 Identity Map을 공유하면 “요청 종료 후 clear할 수 없어” 점점 메모리 풋프린트가 커질 수 있음 | 문서: 요청마다 고유 Identity Map을 유지(=EntityManager fork + RequestContext 사용). 필요 시 요청 종료 후 정리(요청 컨텍스트 종료로 자연 정리) |
| 같은 엔드포인트인데 응답 형태가 이전 요청의 populate 상태에 영향을 받는 듯 불안정 | 문서: populate 여부 같은 상태가 Identity Map에 저장되어, 전역 공유 시 다른 엔드포인트 호출이 다음 응답을 오염시킬 수 있음 | 문서: “요청마다 고유 Identity Map”을 유지해야 함(RequestContext 권장) |
| RequestContext 없이 전역 EM 사용 시 이상한 버그/오염이 발생 | 문서: v5부터 global identity map은 불가(전역 EM을 request context 없이 사용하는 것은 거의 항상 잘못) | 문서: RequestContext 도입. 테스트 등 특수 상황이면 allowGlobalContext/환경변수로 완화 가능(단, 운영에선 권장되지 않음) |
| 프로세스 종료(SIGTERM 등) 후에도 DB 커넥션이 남아 리소스를 소모하는 듯함 | @mikro-orm/nestjs README: NestJS가 기본적으로 종료 시그널을 듣지 않으면 MikroORM shutdown 로직이 실행되지 않을 수 있음 | README(또한 NestJS 공식 문서 원문): 애플리케이션 부트스트랩에서 app.enableShutdownHooks() 호출 |
8) 체크리스트(요약)
- HTTP 요청 경로: RequestContext 미들웨어로 요청 단위 EM(fork) 확보
- HTTP 밖 경로(큐/크론):
@CreateRequestContext()사용 flush()를 트랜잭션 경계의 기본 단위로 보고, 필요한 경우em.transactional()로 명시적 경계 설정- 전역 컨텍스트 허용은 테스트 등 특수 경우로 제한(문서에서 전역 EM 무컨텍스트 사용을 경고)