NestJS에서 레이어별 책임 분리 - 컨트롤러와 서비스의 역할
01 Sep 2025 | nestjs배경
지금까지 컨트롤러는 API 엔드포인트를 제공하고 서비스와 연결하는 통로 역할만 한다고 생각했고, 에러 처리는 주로 서비스 레이어에서 해왔다.
그런데 팀장님의 코드를 보니 서비스에서는 null 반환이나 일부 에러만 처리하고, 컨트롤러에서 HTTP 상태 코드와 함께 상세한 에러 처리를 하는 패턴을 발견했다.
질문
이런 설계 철학의 배경과 장점이 무엇인지 궁금해서 여쭤보게 되었다.
답변 요약
핵심 원칙: 각 레이어의 책임 분리
- 컨트롤러: 통신 수단에 종속된 것들에 대한 책임 (HTTP, GraphQL, WebSocket 등)
- 서비스: 통신 수단과 무관한 비즈니스 로직에 대한 책임
- 레포지토리: DB 접근 등 외부 인프라와 데이터를 주고받는 책임
왜 이렇게 분리하는가?
- 재사용성: 동일한 서비스를 HTTP API, GraphQL, WebSocket 등 다양한 방식의 요청에 동일한 서비스레이어 제공 가능
- 테스트 용이성: 서비스 레이어는 HTTP와 무관하게 순수 비즈니스 로직만 테스트할 수 있고, 컨트롤러는 e2e 테스트시에 오히려 서비스 클래스를 뽑아버리고, 컨트롤러가 적절하게 에러를 응답으로 매핑하는지만 확인해볼 수 있게된다.
- 관심사의 분리: “서비스는 HTTP를 몰라야 함” - 각 레이어가 자신의 책임에만 집중
실제 코드 예시
회사 코드라 많은 부분을 생략했지만, 핵심은 service에서 master를 찾지 못했을 때 return null을 하고, 컨트롤러에서 HTTP 에러 처리를 하는 패턴에 주목하면 된다.
// controller.ts
@Get(':id')
async getMasterDetail(@Param('id') id: string): Promise<MasterDetailDto> {
try {
const master = await this.productMastersService.getMasterDetail(id);
if (!master) {
throw new HttpException('Master not found', HttpStatus.NOT_FOUND);
}
return master;
} catch (error) {
if (error.message === 'Master not found' || error.status === HttpStatus.NOT_FOUND) {
throw new HttpException('Master not found', HttpStatus.NOT_FOUND);
}
throw new HttpException('Failed to get master detail', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// service.ts
async getMasterDetail(masterId: string, tx?: DbTransaction): Promise<MasterDetailDto | null> {
const client = this.getClient(tx);
const master = await this.getMasterById(masterId, tx);
if (!master) {
return null;
}
const optionGroups = await client...
const optionGroupsWithValues: any[] = [];
for (const group of optionGroups) {
....
}
const variants = await client
.select()
.from(productVariants)
.where(eq(productVariants.masterId, masterId))
.orderBy(productVariants.displayOrder);
const channelProducts = [];
return {
...master,
optionGroups: optionGroupsWithValues,
variants: variants.map(v => ({ ...v, optionValues: [] })),
channelProducts
};
}
깨달음
"서비스는 HTTP를 몰라야 한다"
는 원칙이 핵심이었다. 서비스 레이어를 순수하게 유지하면 테스트도 쉬워지고, 다양한 전송 방식으로 확장하기도 용이하다.
앞으로의 계획
- 새로 작성하는 코드는 이 원칙을 적용
- 기존 코드는 점진적으로 리팩토링
참고 자료
NestJS 공식 문서를 다시 찾아보니, 컨트롤러에서 HTTP 상태 코드와 함께 에러를 처리하는 예제들이 있었다.
처음 문서를 읽을 때는 단순히 예제 코드라서 간단하게 컨트롤러에서 처리한 거라고 생각하고 넘어갔었다. 지금 생각해보니 공식 문서도 레이어별 책임 분리 원칙을 따르고 있었던 것 같다. 내가 그때는 이 설계 원칙의 중요성을 제대로 이해하지 못해서 놓쳤던 부분인듯 싶다.