HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
Backend
2026. 3. 28.•
2

NestJS 아키텍처 가이드: 3-Layer부터 CQRS까지 실무 패턴 총정리

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

NestJS 아키텍처 가이드: 3-Layer부터 CQRS까지 실무 패턴 총정리

1. 문제의 배경

"NestJS를 쓰면 무조건 이 구조로 짜야 하나요?" NestJS는 강력한 모듈 시스템과 의존성 주입(DI) 덕분에 매우 자유로운 아키텍처 설계를 지원합니다. 하지만 초기 단계에서 프로젝트의 복잡도를 고려하지 않고 아키텍처를 선택하면 나중에 큰 비용을 치르게 됩니다.

작은 프로젝트에 과한 아키텍처를 적용하면 보일러플레이트 코드에 지치게 되고, 반대로 거대한 프로젝트를 단순한 구조로 짜면 비대해진 서비스(Fat Service)와 스파게티 코드를 마주하게 됩니다. 따라서 상황에 맞는 '정답'을 찾는 능력이 시니어 개발자에게는 필수적입니다.

2. 해결 방안 탐색

업계에서 NestJS와 함께 가장 많이 언급되는 3가지 주요 아키텍처를 비교해 보겠습니다.

  1. 3-Layer Architecture (전통적 방식): 가장 기본이 되는 구조로, 레이어별 관심사를 분리합니다.
  2. CQRS (Command Query Responsibility Segregation): 명령(쓰기)과 쿼리(읽기)를 완전히 분리하여 확장성을 극대화합니다.
  3. Hexagonal / Clean Architecture (도메인 중심): 외부 기술(DB, 외부 API)과 비즈니스 로직을 완벽히 분리하여 유연성을 챙깁니다.

3. 핵심 개념 및 아키텍처

1) 3-Layer Architecture (Controller-Service-Repository)

가장 표준적이며 NestJS가 기본적으로 권장하는 방식입니다.

  • Controller: 클라이언트의 요청을 받고 응답을 보냅니다. (입구)
  • Service: 비즈니스 로직을 처리합니다. (두뇌)
  • Repository/Entity: 데이터베이스에 접근합니다. (데이터)

2) CQRS (명령 및 조회 책임 분리)

상태를 변경하는 작업(Command)과 데이터를 조회하는 작업(Query)을 서로 다른 모델과 경로로 처리합니다.

  • Command: CreateUser, UpdatePost 등 상태 변경. (Side Effect 발생)
  • Query: GetUserList, GetPostById 등 데이터 조회. (Side Effect 없음)
  • Bus: 명령과 쿼리를 핸들러로 전달하는 매개체 역할을 합니다.

3) Hexagonal Architecture (Ports and Adapters)

도메인 로직을 핵심(Inside)에 두고, DB나 외부 라이브러리를 어댑터(Outside)로 취급합니다.

  • Domain: 순수한 비즈니스 규칙만 포함합니다.
  • Port: 외부와 통신하기 위한 인터페이스 정의입니다.
  • Adapter: 실제 DB 연동이나 외부 API 연출을 담당합니다.

4. 구현 및 트러블슈팅

1) 3-Layer 구현 예시

가장 익숙한 형태입니다. 단순하고 빠릅니다.

// users.service.ts @Injectable() export class UsersService { constructor(private usersRepository: UsersRepository) {} async signup(dto: SignupDto) { // 로직 처리... return this.usersRepository.save(dto); } }

2) CQRS 구현 예시

@nestjs/cqrs 라이브러리를 사용합니다. 로직이 명확히 쪼개집니다.

// create-user.command.ts (명령 정의) export class CreateUserCommand { constructor(public readonly email: string) {} } // create-user.handler.ts (처리 로직) @CommandHandler(CreateUserCommand) export class CreateUserHandler implements ICommandHandler<CreateUserCommand> { async execute(command: CreateUserCommand) { // 오직 유저 생성에만 집중! } }

5. 결과 및 Trade-off

아키텍처장점단점추천 상황
3-Layer낮은 학습 곡선, 빠른 개발 속도서비스가 너무 비대해짐(Fat Service)소규모 ~ 중규모 프로젝트, MVP
CQRS높은 확장성, 읽기/쓰기 성능 최적화 유리복잡도 증가, 코드량 급증대규모 트래픽, 복잡한 비즈니스 로직
Hexagonal인프라 변경(DB 교체 등)에 매우 강함추상화가 너무 심해 코드를 파악하기 힘듦장기 운영이 필요한 핵심 시스템

💡 실무에서의 전략

  • 시작은 3-Layer로: 처음부터 CQRS나 클린 아키텍처를 도입하면 오버엔지니어링이 될 확률이 높습니다.
  • 필요할 때 리팩토링: 특정 서비스 클래스가 1,000줄이 넘어가거나 조회 쿼리가 너무 복잡해질 때 해당 모듈만 CQRS로 전환하는 방식이 효율적입니다.

6. 마치며

아키텍처에 '절대 선'은 없습니다. "지금 우리 팀이 이 복잡도를 감당할 수 있는가?"와 "이 서비스가 얼마나 커질 것인가?"를 고민하는 것이 더 중요합니다.

NestJS는 어떤 옷을 입혀도 잘 소화하는 프레임워크입니다. 오늘 정리한 패턴들을 이해하고, 현재 진행 중인 프로젝트의 규모에 딱 맞는 아키텍처를 선택해 보세요. 도구보다 중요한 것은 그 도구를 다루는 개발자의 판단력입니다!

← 이전 글Supabase RLS 완벽 가이드: anon key가 털려도 내 DB가 안전한 이유
다음 글 →Prisma에는 임베디드 엔티티가 없다? 복합 객체를 다루는 최선의 방법