UNPKG

aiwf

Version:

AI Workflow Framework for Claude Code with multi-language support (Korean/English)

766 lines (657 loc) 17.9 kB
# Backend 페르소나 Best Practices ## 개요 Backend 페르소나는 서버 사이드 로직, 데이터베이스 설계, API 개발에 중점을 둡니다. 확장성, 성능, 데이터 무결성을 최우선으로 고려합니다. ## 핵심 원칙 ### 1. API 설계 원칙 - RESTful 원칙 준수 - GraphQL 스키마 최적화 - API 버전 관리 - 일관된 응답 형식 ### 2. 데이터베이스 설계 ```sql -- 정규화와 성능의 균형 CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 인덱스 전략 CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_created_at ON users(created_at DESC); -- 파티셔닝 (대용량 테이블) CREATE TABLE orders ( id BIGSERIAL, user_id UUID REFERENCES users(id), order_date DATE NOT NULL, total_amount DECIMAL(10,2), PRIMARY KEY (id, order_date) ) PARTITION BY RANGE (order_date); ``` ### 3. 마이크로서비스 아키텍처 ```javascript // 서비스 간 통신 패턴 class OrderService { constructor(eventBus, userService, inventoryService) { this.eventBus = eventBus; this.userService = userService; this.inventoryService = inventoryService; } async createOrder(orderData) { // Saga 패턴으로 분산 트랜잭션 처리 const saga = new OrderSaga(); try { // 1. 사용자 검증 const user = await this.userService.validateUser(orderData.userId); saga.addCompensation(() => this.userService.rollback(user)); // 2. 재고 확인 및 예약 const reservation = await this.inventoryService.reserve(orderData.items); saga.addCompensation(() => this.inventoryService.release(reservation)); // 3. 주문 생성 const order = await this.createOrderRecord(orderData); saga.addCompensation(() => this.deleteOrder(order.id)); // 4. 이벤트 발행 await this.eventBus.publish('order.created', { orderId: order.id, userId: user.id, items: orderData.items }); return order; } catch (error) { await saga.compensate(); throw error; } } } ``` ## API 개발 Best Practices ### RESTful API 설계 ```javascript // Express.js 라우터 class UserController { // GET /api/v1/users?page=1&limit=20&sort=created_at:desc async getUsers(req, res) { const { page = 1, limit = 20, sort, filter } = req.query; try { const users = await this.userService.findAll({ pagination: { page, limit }, sorting: this.parseSortParam(sort), filters: this.parseFilterParam(filter) }); res.json({ success: true, data: users, meta: { page: parseInt(page), limit: parseInt(limit), total: users.total, totalPages: Math.ceil(users.total / limit) } }); } catch (error) { this.handleError(error, res); } } // POST /api/v1/users async createUser(req, res) { try { // 입력 검증 const validatedData = await this.validator.validate(req.body, UserSchema); // 비즈니스 로직 const user = await this.userService.create(validatedData); // 201 Created with Location header res.status(201) .location(`/api/v1/users/${user.id}`) .json({ success: true, data: user }); } catch (error) { this.handleError(error, res); } } // Error handling handleError(error, res) { if (error instanceof ValidationError) { return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: error.message, details: error.details } }); } if (error instanceof NotFoundError) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: error.message } }); } // 500 Internal Server Error console.error('Unexpected error:', error); res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } }); } } ``` ### GraphQL 스키마 설계 ```graphql # Schema Definition type User { id: ID! email: String! profile: Profile! orders( first: Int = 10 after: String filter: OrderFilter ): OrderConnection! createdAt: DateTime! updatedAt: DateTime! } type Profile { firstName: String! lastName: String! avatar: String preferences: UserPreferences! } # Pagination using Relay specification type OrderConnection { edges: [OrderEdge!]! pageInfo: PageInfo! totalCount: Int! } type OrderEdge { node: Order! cursor: String! } # Input types input CreateUserInput { email: String! password: String! profile: ProfileInput! } # Queries type Query { user(id: ID!): User users( first: Int after: String filter: UserFilter orderBy: UserOrderBy ): UserConnection! me: User @auth } # Mutations type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! deleteUser(id: ID!): DeleteUserPayload! } # Subscriptions type Subscription { userUpdated(userId: ID!): User! orderCreated(userId: ID!): Order! } ``` ## 데이터베이스 최적화 ### 쿼리 최적화 ```javascript // Query Builder Pattern class UserRepository { constructor(db) { this.db = db; } // N+1 문제 방지 async findWithOrders(userId) { const query = ` SELECT u.id, u.email, u.created_at, o.id as order_id, o.total_amount, o.order_date, oi.product_id, oi.quantity, oi.price FROM users u LEFT JOIN orders o ON u.id = o.user_id LEFT JOIN order_items oi ON o.id = oi.order_id WHERE u.id = $1 ORDER BY o.order_date DESC `; const result = await this.db.query(query, [userId]); return this.mapResultToUser(result.rows); } // Batch loading async findByIds(userIds) { if (userIds.length === 0) return []; const query = ` SELECT * FROM users WHERE id = ANY($1::uuid[]) `; const result = await this.db.query(query, [userIds]); // 순서 보장 const userMap = new Map( result.rows.map(row => [row.id, row]) ); return userIds.map(id => userMap.get(id)).filter(Boolean); } // 동적 쿼리 빌더 async search(criteria) { let query = 'SELECT * FROM users WHERE 1=1'; const params = []; let paramIndex = 1; if (criteria.email) { query += ` AND email ILIKE $${paramIndex}`; params.push(`%${criteria.email}%`); paramIndex++; } if (criteria.createdAfter) { query += ` AND created_at >= $${paramIndex}`; params.push(criteria.createdAfter); paramIndex++; } if (criteria.sortBy) { const validColumns = ['email', 'created_at']; if (validColumns.includes(criteria.sortBy)) { query += ` ORDER BY ${criteria.sortBy} ${ criteria.sortOrder === 'desc' ? 'DESC' : 'ASC' }`; } } query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; params.push(criteria.limit || 20, criteria.offset || 0); return this.db.query(query, params); } } ``` ### 캐싱 전략 ```javascript // Redis 캐싱 레이어 class CacheService { constructor(redis, ttl = 3600) { this.redis = redis; this.ttl = ttl; } // Cache-Aside Pattern async get(key, fetchFn) { // 1. 캐시 확인 const cached = await this.redis.get(key); if (cached) { return JSON.parse(cached); } // 2. 캐시 미스 시 데이터 조회 const data = await fetchFn(); // 3. 캐시 저장 await this.redis.setex( key, this.ttl, JSON.stringify(data) ); return data; } // Write-Through Pattern async set(key, data, ttl = this.ttl) { await this.redis.setex( key, ttl, JSON.stringify(data) ); return data; } // Cache Invalidation async invalidate(pattern) { const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } } // Distributed Lock (분산 환경) async acquireLock(lockKey, ttl = 10) { const identifier = uuidv4(); const result = await this.redis.set( `lock:${lockKey}`, identifier, 'NX', 'EX', ttl ); return result === 'OK' ? identifier : null; } async releaseLock(lockKey, identifier) { const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; return this.redis.eval( script, 1, `lock:${lockKey}`, identifier ); } } ``` ## 메시지 큐와 이벤트 처리 ### RabbitMQ 패턴 ```javascript // Publisher/Subscriber Pattern class EventPublisher { constructor(channel) { this.channel = channel; } async publish(exchange, routingKey, message) { const messageBuffer = Buffer.from(JSON.stringify({ ...message, timestamp: new Date().toISOString(), messageId: uuidv4() })); await this.channel.publish( exchange, routingKey, messageBuffer, { persistent: true, contentType: 'application/json', headers: { 'x-retry-count': 0 } } ); } } // Consumer with Error Handling class EventConsumer { constructor(channel, options = {}) { this.channel = channel; this.maxRetries = options.maxRetries || 3; } async consume(queue, handler) { await this.channel.consume(queue, async (msg) => { if (!msg) return; try { const content = JSON.parse(msg.content.toString()); await handler(content); // Acknowledge message this.channel.ack(msg); } catch (error) { console.error('Error processing message:', error); // Retry logic const retryCount = (msg.properties.headers['x-retry-count'] || 0) + 1; if (retryCount <= this.maxRetries) { // Requeue with delay setTimeout(() => { this.channel.sendToQueue( queue, msg.content, { ...msg.properties, headers: { ...msg.properties.headers, 'x-retry-count': retryCount } } ); }, Math.pow(2, retryCount) * 1000); // Exponential backoff } else { // Send to DLQ await this.sendToDeadLetter(msg, error); } // Reject without requeue this.channel.nack(msg, false, false); } }); } async sendToDeadLetter(msg, error) { const dlq = `${msg.fields.routingKey}.dlq`; await this.channel.sendToQueue( dlq, msg.content, { headers: { 'x-death-reason': error.message, 'x-original-queue': msg.fields.routingKey } } ); } } ``` ## 보안 구현 ### 인증/인가 미들웨어 ```javascript // JWT Authentication class AuthMiddleware { constructor(jwtService, userService) { this.jwtService = jwtService; this.userService = userService; } authenticate() { return async (req, res, next) => { try { const token = this.extractToken(req); if (!token) { return res.status(401).json({ error: 'No authentication token provided' }); } const payload = await this.jwtService.verify(token); // Token blacklist check if (await this.isTokenBlacklisted(payload.jti)) { return res.status(401).json({ error: 'Token has been revoked' }); } req.user = await this.userService.findById(payload.sub); next(); } catch (error) { if (error.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token has expired' }); } return res.status(401).json({ error: 'Invalid authentication token' }); } }; } authorize(...roles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Authentication required' }); } if (roles.length > 0 && !roles.includes(req.user.role)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; } extractToken(req) { const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } return req.cookies?.accessToken; } } ``` ## 테스팅 전략 ### 단위 테스트 ```javascript describe('UserService', () => { let userService; let userRepository; let emailService; beforeEach(() => { userRepository = { findByEmail: jest.fn(), create: jest.fn(), update: jest.fn() }; emailService = { sendWelcomeEmail: jest.fn() }; userService = new UserService(userRepository, emailService); }); describe('createUser', () => { it('should create user with hashed password', async () => { const userData = { email: 'test@example.com', password: 'password123' }; userRepository.findByEmail.mockResolvedValue(null); userRepository.create.mockResolvedValue({ id: 'user-123', ...userData, password: 'hashed-password' }); const user = await userService.createUser(userData); expect(userRepository.findByEmail).toHaveBeenCalledWith(userData.email); expect(userRepository.create).toHaveBeenCalledWith({ email: userData.email, password: expect.stringMatching(/^\$2[aby]\$/) }); expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(userData.email); expect(user).not.toHaveProperty('password'); }); it('should throw error if email already exists', async () => { userRepository.findByEmail.mockResolvedValue({ id: 'existing-user' }); await expect( userService.createUser({ email: 'test@example.com', password: 'pass' }) ).rejects.toThrow('Email already registered'); }); }); }); ``` ### 통합 테스트 ```javascript describe('API Integration Tests', () => { let app; let db; beforeAll(async () => { // Test database setup db = await createTestDatabase(); app = createApp(db); }); afterAll(async () => { await db.close(); }); describe('POST /api/users', () => { it('should create user and return 201', async () => { const response = await request(app) .post('/api/users') .send({ email: 'newuser@example.com', password: 'SecurePass123!', profile: { firstName: 'John', lastName: 'Doe' } }); expect(response.status).toBe(201); expect(response.headers.location).toMatch(/\/api\/users\/[\w-]+/); expect(response.body).toMatchObject({ success: true, data: { email: 'newuser@example.com', profile: { firstName: 'John', lastName: 'Doe' } } }); // Verify database const user = await db.query( 'SELECT * FROM users WHERE email = $1', ['newuser@example.com'] ); expect(user.rows).toHaveLength(1); }); }); }); ``` ## 모니터링과 로깅 ### 구조화된 로깅 ```javascript class Logger { constructor(service) { this.service = service; this.winston = winston.createLogger({ format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service }, transports: [ new winston.transports.Console({ format: winston.format.simple() }), new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); } log(level, message, meta = {}) { this.winston.log({ level, message, ...meta, timestamp: new Date().toISOString() }); } // Request logging middleware requestLogger() { return (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; this.log('info', 'HTTP Request', { method: req.method, path: req.path, statusCode: res.statusCode, duration, ip: req.ip, userAgent: req.get('user-agent'), userId: req.user?.id }); }); next(); }; } } ``` ## 배포 전 체크리스트 ### 성능 - [ ] 데이터베이스 인덱스 최적화 - [ ] N+1 쿼리 제거 - [ ] 캐싱 전략 구현 - [ ] 비동기 처리 적용 ### 보안 - [ ] 환경 변수로 민감 정보 관리 - [ ] SQL Injection 방지 - [ ] Rate Limiting 설정 - [ ] CORS 정책 구성 ### 안정성 - [ ] 에러 처리 및 복구 메커니즘 - [ ] 서킷 브레이커 패턴 - [ ] 헬스체크 엔드포인트 - [ ] 우아한 종료(Graceful Shutdown)