NestJS 로 REST API를 구현하면서 404 exception 처리에 대한 의문이 들었다.
다음과 같은 상황에서,
@Controller('project')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Project> {
return await this.projectService.findOne(id);
}
}
@Injectable()
export class ProjectService {
constructor(
@InjectModel(Project)
private projectModel: typeof Project,
) {}
async findOne(id: string): Promise<Project> {
return await this.projectModel.findOne({
where: {
id,
},
});
}
}
id 에 해당하는 엔티티를 찾지 못한다면 컨트롤러는 404 상태코드를 반환해야 한다.
하지만 현재 사용중인 sequelize 는 위와 같은 상황에서 에러 없이 null을 반환하기 때문에 컨트롤러는 응답코드 200에 empty body를 반환한다.
NestJS에서 기본적으로 제공해주는 HTTP exception인 NotFoundException으로 처리할 수도 있지만,
async findOne(id: string): Promise<Project> {
const project = await this.projectModel.findOne({
where: {
id,
},
});
if (!project) {
throw new NotFoundException();
return project;
}
서비스에서 HTTP 관련 처리를 하는 부분이 마음에 걸렸다. 그러다가 이곳(https://github.com/nestjs/nest/issues/310)에서 관련 내용 찾았다.
대략 서비스에서 바로 http 에러를 던져도 상관은 없지만, 보통은 서비스에서는 서비스 관련 에러를 던지고 컨트롤러에서 HTTP exception을 던지는 편이 좋다는 내용이었다.
그래서 다음과 같이 서비스 에러를 작성하였다.
interface Exception {
name: string;
message: string;
}
class EntityNotFoundException implements Exception {
public readonly name = 'EntityNotFoundException';
public readonly message = 'entity not found';
}
이제 서비스에서 엔티티를 찾지 못했을 때 EntityNotFoundException을 던지고,
@Controller('project')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Project> {
try {
return await this.projectService.findOne(id);
} catch (error) {
if (error instanceof EntityNotFoundException) {
throw new NotFoundException();
} else {
throw error;
}
}
}
위와 같이 처리해 줄 수 있지만 NotFoundException이 발생할 수 있는 route 마다 반복적으로 처리해야 하기 때문에 번거롭다. NestJS 에서 제공하는 Interceptor를 활용하면 편하다.
Interceptor는 AOP 개념에 영향을 받았다. 꼭 필요하지만 어플리케이션의 주요 기능과 상관이 없는 기능(ex. logging, security ...)들을 주요기능의 구현과 분리할 수 있도록 한다.
다음과 같이 EntityNotFoundException이 발생하면 NotFoundException 으로 변환하도록 구현한다.
@Injectable()
export class NotFoundInterceptor implements NestInterceptor {
constructor(private errorMessage: string) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
if (error instanceof EntityNotFoundException) {
throw new NotFoundException(this.errorMessage);
} else {
throw error;
}
}),
);
}
}
NestInterceptor 의 intercept 함수의 두번째 인자 next는 route handler를 호출하고 RxJS의 Observable을 반환한다. 이를 통해 컨트롤러 호출 이전과 이후의 처리를 모두 할 수 있다.
이제 NotFoundException 이 발생할 수 있는 route에 interceptor를 적용하면 된다.
@Get(':id')
@UseInterceptors(new NotFoundInterceptor('project not found'))
async findOne(@Param('id') id: string): Promise<Project> {
return await this.projectService.findOne(id);
}
https://github.com/nestjs/nest/issues/310
https://docs.nestjs.com/interceptors
https://en.wikipedia.org/wiki/Aspect-oriented_programming
https://stackoverflow.com/questions/51112952/what-is-the-nestjs-error-handling-approach-business-logic-error-vs-http-error