Copilot을 사용한 설계 아키텍처
좋은 건축은 지속 가능한 프로젝트의 기초입니다. 이 단계에서, 우리는 Copilot을 사용하여 백엔드 che il 프런트엔드, 명확한 API 계약을 통해 두 사람이 통신하는 방법을 정의합니다. 아키텍처 그것은 단지 "파일을 어디에 둘 것인가"가 아니라 일련의 영향을 미치는 결정입니다. 프로젝트의 확장성, 테스트 가능성 및 유지 관리 가능성.
🏗️ 기본 아키텍처 원칙
- 우려사항 분리: 각 구성원은 단일한 책임을 집니다.
- 느슨한 커플링: 모듈은 구현이 아닌 추상화에 의존합니다.
- 높은 응집력: 관련 코드가 함께 들어갑니다.
- 테스트 가능성: 각 부품을 개별적으로 테스트할 수 있습니다.
- 유지 관리성: 코드는 이해 가능하고 편집 가능합니다.
시리즈 개요
"GitHub Copilot: AI 기반 개인 프로젝트" 시리즈의 세 번째 기사입니다.
| # | 기준 치수 | 상태 |
|---|---|---|
| 1 | 재단 – 파트너로서의 부조종사 | ✅ 완료됨 |
| 2 | 개념 및 요구사항 | ✅ 완료됨 |
| 3 | 백엔드 및 프런트엔드 아키텍처 | 📍 당신은 여기에 있습니다 |
| 4 | 코드의 구조 | ⏳ 다음 |
| 5 | 프롬프트 엔지니어링 및 MCP 에이전트 | ⏳ |
| 6 | 테스트 및 품질 | ⏳ |
| 7 | 선적 서류 비치 | ⏳ |
| 8 | 배포 및 DevOps | ⏳ |
| 9 | 진화와 유지 | ⏳ |
백엔드 아키텍처 선택
개인 프로젝트의 경우 다양한 아키텍처 패턴이 있습니다. 선택은 달려있습니다 도메인의 복잡성과 팀(당신!)의 기술에 따라 결정됩니다.
아키텍처 패턴 비교
| 패턴 | 복잡성 | 언제 사용하는가 | 찬성 | 에 맞서 |
|---|---|---|---|---|
| 간단한 MVC | 🟢 낮음 | CRUD 앱, 프로토타입 | 빠른 구현 | 확장이 잘 안됨 |
| 계층형/N계층 | 🟡 중간 | 표준 비즈니스 앱 | 명확한 분리 | 딱딱해질 수 있어요 |
| 클린 아키텍처 | 🔴 높음 | 복잡한 도메인 | 최대 테스트 가능성 | MVP를 위한 오버엔지니어링 |
| 육각형 | 🔴 높음 | 다양한 외부 통합 | 포트 및 어댑터 | 높은 상용구 |
💡 개인 프로젝트에 대한 조언
MVP의 경우 단순화된 계층형 아키텍처: 충분하다 구조는 유지 관리가 가능하지만 속도가 느려질 정도로 복잡하지는 않습니다. 프로젝트가 성장함에 따라 언제든지 Clean Architecture로 리팩터링할 수 있습니다.
백엔드: 계층형 아키텍처
Design a backend architecture for my project.
PROJECT: TaskFlow (time tracking + task management)
STACK: Node.js + Express + TypeScript + PostgreSQL
TEAM SIZE: Solo developer
EXPECTED SCALE: 1000 users, 100k requests/day
Requirements:
- Clean separation between layers
- Easy to test each layer independently
- Scalable for future features
- Simple enough for a solo developer to maintain
- Support for both REST API and future GraphQL
Include:
1. Layer diagram with responsibilities
2. Complete folder structure with explanations
3. Data flow example for a typical request
4. Error handling strategy across layers
5. Dependency injection approach
레이어 다이어그램
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Controllers │ │ Middleware │ │ DTOs (Validation) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ Responsabilità: HTTP handling, request/response transform │
├─────────────────────────────────────────────────────────────┤
│ BUSINESS LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Services │ │ Use Cases │ │ Domain Logic │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ Responsabilità: Business rules, orchestration, validation │
├─────────────────────────────────────────────────────────────┤
│ DATA ACCESS LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │Repositories │ │ Entities │ │ Query Builders │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ Responsabilità: Data persistence, query optimization │
├─────────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Database │ │External APIs│ │ File System │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ Responsabilità: Concrete implementations, external deps │
└─────────────────────────────────────────────────────────────┘
↑ DEPENDENCY DIRECTION ↑
(I layer superiori dipendono da quelli inferiori)
종속성 규칙
✅ 맞습니다
- 컨트롤러 → 서비스
- 서비스 → 저장소
- 저장소 → 데이터베이스
- 서비스 → 인터페이스(구현 아님)
❌ 틀렸어
- 컨트롤러 → 데이터베이스(레이어 건너뛰기)
- 저장소 → 컨트롤러(역방향)
- 서비스 → 빠른 요청(프레임워크에 연결)
- 컨트롤러의 비즈니스 로직
요청의 흐름
요청이 HTTP에서 지속성까지 모든 계층을 어떻게 통과하는지 살펴보겠습니다.
// ═══════════════════════════════════════════════════════════
// 1. ROUTE DEFINITION (Presentation Layer)
// ═══════════════════════════════════════════════════════════
// routes/task.routes.ts
import {{ '{' }} Router {{ '}' }} from 'express';
import {{ '{' }} TaskController {{ '}' }} from '../controllers/task.controller';
import {{ '{' }} authMiddleware {{ '}' }} from '../middleware/auth.middleware';
import {{ '{' }} validateDto {{ '}' }} from '../middleware/validation.middleware';
import {{ '{' }} CreateTaskDto {{ '}' }} from '../dto/create-task.dto';
const router = Router();
const controller = new TaskController();
router.post(
'/',
authMiddleware, // Verifica JWT
validateDto(CreateTaskDto), // Valida body
controller.create.bind(controller) // Gestisce richiesta
);
export default router;
// ═══════════════════════════════════════════════════════════
// 2. DTO WITH VALIDATION (Presentation Layer)
// ═══════════════════════════════════════════════════════════
// dto/create-task.dto.ts
import {{ '{' }} IsString, IsOptional, IsDateString, MinLength, MaxLength {{ '}' }} from 'class-validator';
export class CreateTaskDto {{ '{' }}
@IsString()
@MinLength(3, {{ '{' }} message: 'Title must be at least 3 characters' {{ '}' }})
@MaxLength(100)
title: string;
@IsOptional()
@IsString()
@MaxLength(1000)
description?: string;
@IsOptional()
@IsDateString()
dueDate?: string;
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// 3. CONTROLLER (Presentation Layer)
// ═══════════════════════════════════════════════════════════
// controllers/task.controller.ts
import {{ '{' }} Request, Response, NextFunction {{ '}' }} from 'express';
import {{ '{' }} TaskService {{ '}' }} from '../services/task.service';
import {{ '{' }} CreateTaskDto {{ '}' }} from '../dto/create-task.dto';
export class TaskController {{ '{' }}
private taskService: TaskService;
constructor() {{ '{' }}
this.taskService = new TaskService();
{{ '}' }}
async create(req: Request, res: Response, next: NextFunction) {{ '{' }}
try {{ '{' }}
const userId = req.user.id; // From auth middleware
const dto: CreateTaskDto = req.body;
const task = await this.taskService.createTask(userId, dto);
res.status(201).json({{ '{' }}
success: true,
data: task
{{ '}' }});
{{ '}' }} catch (error) {{ '{' }}
next(error); // Forward to error middleware
{{ '}' }}
{{ '}' }}
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// 4. SERVICE (Business Layer)
// ═══════════════════════════════════════════════════════════
// services/task.service.ts
import {{ '{' }} TaskRepository {{ '}' }} from '../repositories/task.repository';
import {{ '{' }} Task {{ '}' }} from '../entities/task.entity';
import {{ '{' }} CreateTaskDto {{ '}' }} from '../dto/create-task.dto';
import {{ '{' }} ValidationError {{ '}' }} from '../errors/validation.error';
export class TaskService {{ '{' }}
private taskRepo: TaskRepository;
constructor() {{ '{' }}
this.taskRepo = new TaskRepository();
{{ '}' }}
async createTask(userId: string, dto: CreateTaskDto): Promise<Task> {{ '{' }}
// Business rule: due date must be in future
if (dto.dueDate && new Date(dto.dueDate) < new Date()) {{ '{' }}
throw new ValidationError('Due date must be in the future');
{{ '}' }}
// Business rule: max 100 active tasks per user
const activeTasksCount = await this.taskRepo.countActiveByUser(userId);
if (activeTasksCount >= 100) {{ '{' }}
throw new ValidationError('Maximum 100 active tasks allowed');
{{ '}' }}
// Create entity
const task = new Task({{ '{' }}
userId,
title: dto.title,
description: dto.description,
dueDate: dto.dueDate ? new Date(dto.dueDate) : null,
status: 'TODO',
createdAt: new Date()
{{ '}' }});
// Persist and return
return this.taskRepo.save(task);
{{ '}' }}
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// 5. REPOSITORY (Data Access Layer)
// ═══════════════════════════════════════════════════════════
// repositories/task.repository.ts
import {{ '{' }} db {{ '}' }} from '../database/connection';
import {{ '{' }} Task {{ '}' }} from '../entities/task.entity';
export class TaskRepository {{ '{' }}
async save(task: Task): Promise<Task> {{ '{' }}
const result = await db.query(
`INSERT INTO tasks (id, user_id, title, description, due_date, status, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[task.id, task.userId, task.title, task.description,
task.dueDate, task.status, task.createdAt]
);
return Task.fromRow(result.rows[0]);
{{ '}' }}
async countActiveByUser(userId: string): Promise<number> {{ '{' }}
const result = await db.query(
`SELECT COUNT(*) FROM tasks
WHERE user_id = $1 AND status != 'DONE' AND deleted_at IS NULL`,
[userId]
);
return parseInt(result.rows[0].count);
{{ '}' }}
{{ '}' }}
일관된 오류 처리
잘 설계된 오류 처리 시스템은 디버깅과 UX를 단순화합니다.
// ═══════════════════════════════════════════════════════════
// errors/app-error.ts - Base Error Class
// ═══════════════════════════════════════════════════════════
export class AppError extends Error {{ '{' }}
public readonly statusCode: number;
public readonly code: string;
public readonly isOperational: boolean;
public readonly details?: unknown;
constructor(
message: string,
statusCode: number = 500,
code: string = 'INTERNAL_ERROR',
details?: unknown
) {{ '{' }}
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
this.details = details;
Error.captureStackTrace(this, this.constructor);
{{ '}' }}
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// errors/specific-errors.ts - Domain Specific Errors
// ═══════════════════════════════════════════════════════════
export class ValidationError extends AppError {{ '{' }}
constructor(message: string, details?: unknown) {{ '{' }}
super(message, 400, 'VALIDATION_ERROR', details);
{{ '}' }}
{{ '}' }}
export class NotFoundError extends AppError {{ '{' }}
constructor(resource: string, id?: string) {{ '{' }}
const message = id
? `${{ '{' }}resource{{ '}' }} with id '${{ '{' }}id{{ '}' }}' not found`
: `${{ '{' }}resource{{ '}' }} not found`;
super(message, 404, 'NOT_FOUND');
{{ '}' }}
{{ '}' }}
export class UnauthorizedError extends AppError {{ '{' }}
constructor(message: string = 'Unauthorized') {{ '{' }}
super(message, 401, 'UNAUTHORIZED');
{{ '}' }}
{{ '}' }}
export class ForbiddenError extends AppError {{ '{' }}
constructor(message: string = 'Forbidden') {{ '{' }}
super(message, 403, 'FORBIDDEN');
{{ '}' }}
{{ '}' }}
export class ConflictError extends AppError {{ '{' }}
constructor(message: string) {{ '{' }}
super(message, 409, 'CONFLICT');
{{ '}' }}
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// middleware/error.middleware.ts - Global Error Handler
// ═══════════════════════════════════════════════════════════
import {{ '{' }} Request, Response, NextFunction {{ '}' }} from 'express';
import {{ '{' }} AppError {{ '}' }} from '../errors/app-error';
import {{ '{' }} logger {{ '}' }} from '../utils/logger';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {{ '{' }}
// Handle known operational errors
if (err instanceof AppError) {{ '{' }}
logger.warn({{ '{' }}
code: err.code,
message: err.message,
path: req.path,
method: req.method
{{ '}' }});
return res.status(err.statusCode).json({{ '{' }}
success: false,
error: {{ '{' }}
code: err.code,
message: err.message,
...(err.details && {{ '{' }} details: err.details {{ '}' }})
{{ '}' }}
{{ '}' }});
{{ '}' }}
// Handle unexpected errors
logger.error({{ '{' }}
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
{{ '}' }});
// Don't leak internal errors in production
const message = process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: err.message;
return res.status(500).json({{ '{' }}
success: false,
error: {{ '{' }}
code: 'INTERNAL_ERROR',
message
{{ '}' }}
{{ '}' }});
{{ '}' }}
프런트엔드 아키텍처: 기능 기반
프런트엔드의 경우 기능 기반 구조가 구조보다 확장성이 더 좋습니다. 유형(구성요소, 서비스 등)별로 전통적입니다.
Design a frontend architecture for my project.
PROJECT: TaskFlow (time tracking + task management)
STACK: Angular 21 with standalone components
TEAM SIZE: Solo developer
Requirements:
- Feature-based folder structure
- Lazy loading per route for performance
- State management strategy (signals vs NgRx)
- API communication layer with interceptors
- Reusable component library (design system)
- Offline support consideration
Include:
1. Detailed folder structure with explanations
2. State management approach
3. Data flow diagram
4. Component communication patterns
5. API service architecture
프런트엔드 구조 비교
❌ 전통(유형별):
src/app/
├── components/ # 50+ files
│ ├── user-list.ts
│ ├── user-form.ts
│ ├── task-list.ts
│ ├── task-item.ts
│ └── ... (mescolati)
├── services/
│ └── ... (tutti insieme)
├── models/
│ └── ...
└── pipes/
└── ...
문제: 무엇이 무엇에 속하는지 이해하기 어려움
✅ 기능 기반:
src/app/
├── core/ # Singleton services
├── shared/ # Componenti riusabili
├── features/
│ ├── auth/ # Tutto di auth
│ │ ├── components/
│ │ ├── services/
│ │ └── auth.routes.ts
│ ├── tasks/ # Tutto di tasks
│ └── dashboard/ # Tutto di dashboard
└── app.routes.ts
장점: 독립적인 기능, 지연 로드 가능
상세한 프런트엔드 구조
src/
├── app/
│ ├── core/ # Singleton services (providedIn: 'root')
│ │ ├── services/
│ │ │ ├── api.service.ts # HTTP client wrapper
│ │ │ ├── auth.service.ts # Authentication state
│ │ │ ├── storage.service.ts # LocalStorage abstraction
│ │ │ └── toast.service.ts # Notifications
│ │ ├── guards/
│ │ │ ├── auth.guard.ts # Protected routes
│ │ │ └── guest.guard.ts # Redirect if logged in
│ │ ├── interceptors/
│ │ │ ├── auth.interceptor.ts # Add JWT to requests
│ │ │ ├── error.interceptor.ts # Global error handling
│ │ │ └── loading.interceptor.ts # Loading state
│ │ └── core.module.ts # Core providers
│ │
│ ├── shared/ # Reusable (import in features)
│ │ ├── components/
│ │ │ ├── button/
│ │ │ │ ├── button.component.ts
│ │ │ │ ├── button.component.html
│ │ │ │ └── button.component.scss
│ │ │ ├── modal/
│ │ │ ├── form-field/
│ │ │ ├── loading-spinner/
│ │ │ └── empty-state/
│ │ ├── directives/
│ │ │ ├── click-outside.directive.ts
│ │ │ └── autofocus.directive.ts
│ │ ├── pipes/
│ │ │ ├── time-ago.pipe.ts
│ │ │ └── duration.pipe.ts
│ │ └── index.ts # Barrel export
│ │
│ ├── features/ # Feature modules (lazy loaded)
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ │ ├── login-form/
│ │ │ │ └── register-form/
│ │ │ ├── pages/
│ │ │ │ ├── login.page.ts
│ │ │ │ └── register.page.ts
│ │ │ ├── services/
│ │ │ │ └── auth-api.service.ts
│ │ │ └── auth.routes.ts
│ │ │
│ │ ├── tasks/
│ │ │ ├── components/
│ │ │ │ ├── task-list/
│ │ │ │ ├── task-item/
│ │ │ │ ├── task-form/
│ │ │ │ └── task-timer/
│ │ │ ├── pages/
│ │ │ │ ├── tasks-list.page.ts
│ │ │ │ └── task-detail.page.ts
│ │ │ ├── services/
│ │ │ │ └── tasks-api.service.ts
│ │ │ ├── models/
│ │ │ │ └── task.model.ts
│ │ │ ├── state/
│ │ │ │ └── tasks.store.ts # Signals-based state
│ │ │ └── tasks.routes.ts
│ │ │
│ │ └── dashboard/
│ │ ├── components/
│ │ │ ├── stats-card/
│ │ │ └── recent-tasks/
│ │ ├── pages/
│ │ │ └── dashboard.page.ts
│ │ └── dashboard.routes.ts
│ │
│ ├── layouts/ # Layout components
│ │ ├── main-layout/ # Authenticated layout
│ │ │ ├── main-layout.component.ts
│ │ │ ├── header/
│ │ │ └── sidebar/
│ │ └── auth-layout/ # Guest layout
│ │
│ ├── app.component.ts
│ ├── app.routes.ts # Root routes with lazy loading
│ └── app.config.ts # App configuration
│
├── assets/
│ ├── images/
│ └── icons/
│
├── styles/
│ ├── _variables.scss # Design tokens
│ ├── _mixins.scss
│ └── global.scss
│
└── environments/
├── environment.ts
└── environment.prod.ts
API 계약: 백엔드 ⇔ 프런트엔드
명확한 API 계약은 프런트엔드와 백엔드 간의 통신 문제를 방지합니다. 구현하는 동안이 아니라 구현하기 전에 정의하십시오.
Design a complete API contract for my project.
PROJECT: TaskFlow
RESOURCES: Users, Tasks, TimeEntries, Reports
For each resource include:
1. All endpoints (REST conventions)
2. Request/Response schemas (TypeScript interfaces)
3. Standard error response format
4. Authentication requirements per endpoint
5. Pagination strategy for lists
6. Rate limiting considerations
Use OpenAPI/Swagger style descriptions.
Include examples for each endpoint.
표준 응답 형식
// ═══════════════════════════════════════════════════════════
// types/api-response.ts - Standard Response Wrappers
// ═══════════════════════════════════════════════════════════
// Success response for single item
interface ApiResponse<T> {{ '{' }}
success: true;
data: T;
{{ '}' }}
// Success response for lists with pagination
interface ApiListResponse<T> {{ '{' }}
success: true;
data: T[];
meta: {{ '{' }}
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
{{ '}' }};
{{ '}' }}
// Error response (always same structure)
interface ApiErrorResponse {{ '{' }}
success: false;
error: {{ '{' }}
code: string; // Machine-readable: 'VALIDATION_ERROR'
message: string; // Human-readable: 'Invalid input'
details?: unknown; // Optional: field-specific errors
{{ '}' }};
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// Example: Task Endpoints
// ═══════════════════════════════════════════════════════════
/**
* GET /api/v1/tasks
* List all tasks for authenticated user
*
* Query Parameters:
* - page: number (default: 1)
* - limit: number (default: 20, max: 100)
* - status: 'TODO' | 'IN_PROGRESS' | 'DONE' (optional filter)
* - sort: 'createdAt' | 'dueDate' | 'title' (default: 'createdAt')
* - order: 'asc' | 'desc' (default: 'desc')
*
* Response 200: ApiListResponse<Task>
* Response 401: ApiErrorResponse (not authenticated)
*/
/**
* POST /api/v1/tasks
* Create a new task
*
* Request Body: CreateTaskDto
* Response 201: ApiResponse<Task>
* Response 400: ApiErrorResponse (validation error)
* Response 401: ApiErrorResponse (not authenticated)
*/
/**
* GET /api/v1/tasks/:id
* Get single task by ID
*
* Response 200: ApiResponse<Task>
* Response 404: ApiErrorResponse (task not found)
* Response 403: ApiErrorResponse (not owner)
*/
/**
* PATCH /api/v1/tasks/:id
* Update task (partial update)
*
* Request Body: UpdateTaskDto (all fields optional)
* Response 200: ApiResponse<Task>
* Response 404: ApiErrorResponse
*/
/**
* DELETE /api/v1/tasks/:id
* Soft delete task
*
* Response 204: No content
* Response 404: ApiErrorResponse
*/
자세한 API 계약 예시
## Tasks API v1
Base URL: `https://api.taskflow.com/v1`
Authentication: Bearer token required for all endpoints
---
### List Tasks
`GET /tasks`
**Query Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| page | integer | 1 | Page number (1-indexed) |
| limit | integer | 20 | Items per page (max: 100) |
| status | string | - | Filter by status |
| search | string | - | Search in title/description |
**Example Request:**
```
GET /tasks?page=1&limit=10&status=TODO
Authorization: Bearer eyJhbGciOiJIUzI1...
```
**Response 200:**
```json
{{ '{' }}
"success": true,
"data": [
{{ '{' }}
"id": "task_abc123",
"title": "Complete API documentation",
"description": "Write OpenAPI specs",
"status": "TODO",
"dueDate": "2025-02-15T10:00:00Z",
"createdAt": "2025-01-30T12:00:00Z",
"updatedAt": "2025-01-30T12:00:00Z"
{{ '}' }}
],
"meta": {{ '{' }}
"page": 1,
"limit": 10,
"total": 45,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
{{ '}' }}
{{ '}' }}
```
**Error Responses:**
| Code | When |
|------|------|
| 401 | Missing or invalid token |
| 500 | Server error |
---
### Create Task
`POST /tasks`
**Request Body:**
```json
{{ '{' }}
"title": "Complete API documentation",
"description": "Write OpenAPI specs for all endpoints",
"dueDate": "2025-02-15T10:00:00Z"
{{ '}' }}
```
**Validation Rules:**
| Field | Rules |
|-------|-------|
| title | Required, 3-100 characters |
| description | Optional, max 1000 characters |
| dueDate | Optional, must be future date |
**Response 201:**
```json
{{ '{' }}
"success": true,
"data": {{ '{' }}
"id": "task_xyz789",
"title": "Complete API documentation",
"status": "TODO",
...
{{ '}' }}
{{ '}' }}
```
**Error Response 400:**
```json
{{ '{' }}
"success": false,
"error": {{ '{' }}
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{{ '{' }}
"field": "title",
"message": "Title must be at least 3 characters"
{{ '}' }},
{{ '{' }}
"field": "dueDate",
"message": "Due date must be in the future"
{{ '}' }}
]
{{ '}' }}
{{ '}' }}
```
API 버전 관리
향후 주요 변경 사항을 방지하려면 처음부터 버전 관리를 구현하세요.
😀 버전 관리 전략
| 전략 | Esempio | 찬성 | 에 맞서 |
|---|---|---|---|
| URL경로 ✅ | /api/v1/작업 | 명확하고 테스트하기 쉽습니다. | 더 긴 URL |
| 헤더 | 수락: application/vnd.api+json;v=1 | URL 정리 | 눈에 잘 띄지 않음 |
| 쿼리 매개변수 | /api/tasks?version=1 | 추가하기 쉬움 | 잊혀질 수도 있어 |
데이터베이스 스키마 디자인
데이터베이스 스키마는 요구 사항 및 정의된 엔터티에서 직접 파생됩니다.
-- ═══════════════════════════════════════════════════════════
-- PostgreSQL Schema for TaskFlow
-- ═══════════════════════════════════════════════════════════
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ───────────────────────────────────────────────────────────
-- Users Table
-- ───────────────────────────────────────────────────────────
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
avatar_url VARCHAR(500),
email_verified_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP -- Soft delete
);
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
-- ───────────────────────────────────────────────────────────
-- Tasks Table
-- ───────────────────────────────────────────────────────────
CREATE TYPE task_status AS ENUM ('TODO', 'IN_PROGRESS', 'DONE');
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL,
description TEXT,
status task_status NOT NULL DEFAULT 'TODO',
due_date TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP -- Soft delete
);
CREATE INDEX idx_tasks_user_status ON tasks(user_id, status)
WHERE deleted_at IS NULL;
CREATE INDEX idx_tasks_user_due ON tasks(user_id, due_date)
WHERE deleted_at IS NULL AND status != 'DONE';
-- ───────────────────────────────────────────────────────────
-- Time Entries Table
-- ───────────────────────────────────────────────────────────
CREATE TABLE time_entries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
started_at TIMESTAMP NOT NULL,
ended_at TIMESTAMP, -- NULL = timer running
duration_seconds INTEGER, -- Calculated on stop
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_time_entries_task ON time_entries(task_id);
CREATE INDEX idx_time_entries_user_date ON time_entries(user_id, started_at);
-- ───────────────────────────────────────────────────────────
-- Trigger: Auto-update updated_at
-- ───────────────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER tasks_updated_at
BEFORE UPDATE ON tasks
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
ADR(아키텍처 결정 기록)
나중에 참조할 수 있도록 중요한 아키텍처 결정을 문서화합니다. ADR은 다음을 설명합니다. perché, 뿐만 아니라 무엇.
# ADR-001: Scelta di PostgreSQL come Database
## Status
Accepted (2025-01-30)
## Context
Abbiamo bisogno di un database per persistere i dati dell'applicazione TaskFlow.
I requisiti chiave sono:
- Supporto per relazioni tra entità (users → tasks → time_entries)
- Query complesse per report e analytics
- Supporto per dati semi-strutturati (future metadata)
- Scalabilità verticale sufficiente per MVP (1000 users)
## Decision
Usiamo **PostgreSQL 16** come database primario.
## Alternatives Considered
### MongoDB
- ✅ Flessibilità schema
- ✅ Scaling orizzontale nativo
- ❌ Query JOIN complesse difficili
- ❌ Consistenza transazionale limitata
### MySQL
- ✅ Ampia adozione, familiare
- ❌ Supporto JSON meno maturo
- ❌ Meno feature avanzate (CTE, window functions)
### SQLite
- ✅ Zero setup, embedded
- ❌ Non adatto per produzione multi-istanza
- ❌ Limitazioni concurrency
## Consequences
### Positive
- Query SQL potenti e ottimizzate
- Supporto nativo per JSONB
- Transazioni ACID complete
- Ottimo tooling (Prisma, pgAdmin)
- Free tier su Supabase/Neon
### Negative
- Setup più complesso di SQLite
- Richiede managed service in produzione
### Risks
- Potrebbe essere overkill per MVP iniziale
- Migrazione costosa se cambieremo DB
## Notes
Rivalutare dopo 6 mesi di produzione se i pattern di accesso cambiano.
구현 전 체크리스트
✅ 아키텍처 체크리스트
| Item | 완전한 |
|---|---|
| 아키텍처 패턴 선택 및 문서화(ADR) | ☐ |
| 백엔드 폴더 구조 정의 | ☐ |
| 프런트엔드 폴더 구조가 정의됨 | ☐ |
| 문서화된 API 계약 | ☐ |
| 표준화된 응답 형식 | ☐ |
| 오류 처리 전략이 정의됨 | ☐ |
| 설계된 데이터베이스 스키마 | ☐ |
| 예약된 데이터베이스 인덱스 | ☐ |
| 문서화된 인증 흐름 | ☐ |
| API 버전 관리 구현 | ☐ |
결론
잘 설계된 아키텍처로 인해 개발이 더 원활하게 실행되고 코드가 더 원활하게 실행됩니다. 유지 관리 가능. Copilot은 옵션을 탐색하고 상용구를 생성하는 데 도움을 줄 수 있습니다. 결정을 문서화하지만 아키텍처 선택에는 이해가 필요합니다. 프로젝트의 도메인 및 특정 요구 사항. 아키텍처를 복사하지 마십시오 복잡함: 프로젝트에 적합한 레벨을 선택하세요.
🎯 기사의 핵심 사항
- 계층화된 아키텍처: Controller → Service → Repository 간 관심사 분리
- 기능 기반 프런트엔드: 파일 유형이 아닌 기능별로 정리
- 계약 우선 API: 구현하기 전에 계약을 정의하세요.
- 표준 응답: 성공과 오류에 대한 일관된 형식
- 오류 처리: 입력된 오류가 있는 중앙 집중식 시스템
- ADR: 중요한 아키텍처 결정을 문서화하세요.
📚 다음 기사
다음 기사에서 "코드 구조 및 구성" 명명 규칙, 구성 관리 등 구현 세부 사항을 살펴보겠습니다. 깨끗하고 유지 관리 가능한 코드베이스를 위한 배럴 내보내기 및 모범 사례입니다.







