stack-analysis
CHAPTER 31 / 90
읽기 약 2분
FUNCTION
Express 프로덕션 아키텍처: 3레이어
핵심 개념
Controller·Service·Repository — 테스트 가능한 구조 + 의존성 주입.
본문
3레이어 아키텍처
src/
├── controllers/ # HTTP 요청/응답 변환
├── services/ # 비즈니스 로직
├── repositories/ # 데이터 액세스
├── middlewares/ # 인증·로깅·에러
├── routes/ # URL → controller 매핑
└── utils/Controller (얇게)
// controllers/post.controller.ts
import { Request, Response, NextFunction } from 'express';
import { PostService } from '@/services/post.service';
export class PostController {
constructor(private postService: PostService) {}
list = async (req: Request, res: Response, next: NextFunction) => {
try {
const { page = 1, limit = 20 } = req.query;
const result = await this.postService.list({
page: Number(page), limit: Number(limit),
});
res.json(result);
} catch (err) { next(err); }
};
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.user!.id; // 인증 미들웨어가 주입
const post = await this.postService.create(userId, req.body);
res.status(201).json(post);
} catch (err) { next(err); }
};
}Service (비즈니스 로직)
// services/post.service.ts
import { z } from 'zod';
import { PostRepository } from '@/repositories/post.repository';
import { ForbiddenError, ValidationError } from '@/utils/errors';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).max(10),
});
export class PostService {
constructor(private repo: PostRepository) {}
async list({ page, limit }: { page: number; limit: number }) {
const offset = (page - 1) * limit;
const [items, total] = await Promise.all([
this.repo.findMany({ offset, limit }),
this.repo.count(),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
}
async create(userId: string, data: unknown) {
const parsed = CreatePostSchema.safeParse(data);
if (!parsed.success) throw new ValidationError(parsed.error);
return this.repo.create({ ...parsed.data, authorId: userId });
}
async update(userId: string, postId: string, data: unknown) {
const post = await this.repo.findById(postId);
if (!post) throw new NotFoundError('Post');
if (post.authorId !== userId) throw new ForbiddenError();
return this.repo.update(postId, data);
}
}Repository (데이터 액세스)
// repositories/post.repository.ts
import { PrismaClient } from '@prisma/client';
export class PostRepository {
constructor(private prisma: PrismaClient) {}
findMany({ offset, limit }: { offset: number; limit: number }) {
return this.prisma.post.findMany({
skip: offset, take: limit,
orderBy: { createdAt: 'desc' },
include: { author: { select: { id: true, name: true } } },
});
}
count() { return this.prisma.post.count(); }
findById(id: string) { return this.prisma.post.findUnique({ where: { id } }); }
create(data: any) { return this.prisma.post.create({ data }); }
update(id: string, data: any) { return this.prisma.post.update({ where: { id }, data }); }
}DI 컨테이너 (간단 버전)
// container.ts
import { PrismaClient } from '@prisma/client';
import { PostRepository } from './repositories/post.repository';
import { PostService } from './services/post.service';
import { PostController } from './controllers/post.controller';
const prisma = new PrismaClient();
const postRepo = new PostRepository(prisma);
const postService = new PostService(postRepo);
export const postController = new PostController(postService);
// routes/post.routes.ts
import { Router } from 'express';
import { postController } from '@/container';
import { authenticate } from '@/middlewares/auth';
const router = Router();
router.get('/', postController.list);
router.post('/', authenticate, postController.create);
export default router;다음 챕터
CH.32 "인증 시스템: Passport + JWT + OAuth2".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 Express 아키텍처 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
Express 아키텍처 관련 인기 라이브러리/패턴 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 Express 아키텍처 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 백엔드 시장의 Express 아키텍처 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
Express 프로덕션 아키텍처: 3레이어는 이 3가지만 확실히 잡으세요
1.3레이어 = Controller(얇게) → Service(로직) → Repository(데이터) — 테스트 가능
2.DI(의존성 주입)로 Mock 교체 가능 — 단위 테스트 표준
3.Controller는 try/catch + next(err) — 에러 미들웨어가 처리
공유하기
진행도 31 / 90