aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
1,070 lines (896 loc) • 25.6 kB
Markdown
# Backend 페르소나 Knowledge Base
## 서버 아키텍처 패턴
### Clean Architecture
```
┌─────────────────────────────────────┐
│ Controllers/Routes │
├─────────────────────────────────────┤
│ Use Cases │
├─────────────────────────────────────┤
│ Domain/Entities │
├─────────────────────────────────────┤
│ Data Access/Repositories │
└─────────────────────────────────────┘
```
```javascript
// Domain Entity
class User {
constructor(id, email, passwordHash) {
this.id = id;
this.email = email;
this.passwordHash = passwordHash;
}
static create(email, password) {
if (!this.isValidEmail(email)) {
throw new DomainError('Invalid email format');
}
if (!this.isStrongPassword(password)) {
throw new DomainError('Password does not meet requirements');
}
return new User(
generateId(),
email.toLowerCase(),
hashPassword(password)
);
}
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
static isStrongPassword(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
}
// Use Case
class RegisterUserUseCase {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
async execute(request) {
// 비즈니스 규칙 검증
const existingUser = await this.userRepository.findByEmail(request.email);
if (existingUser) {
throw new ConflictError('Email already registered');
}
// 도메인 객체 생성
const user = User.create(request.email, request.password);
// 영속성 계층에 저장
await this.userRepository.save(user);
// 부가 작업
await this.emailService.sendWelcomeEmail(user.email);
return {
id: user.id,
email: user.email
};
}
}
// Repository Interface
class UserRepository {
async save(user) {
throw new Error('Method not implemented');
}
async findByEmail(email) {
throw new Error('Method not implemented');
}
async findById(id) {
throw new Error('Method not implemented');
}
}
// Repository Implementation
class PostgresUserRepository extends UserRepository {
constructor(db) {
super();
this.db = db;
}
async save(user) {
const query = `
INSERT INTO users (id, email, password_hash, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
updated_at = NOW()
RETURNING *
`;
const result = await this.db.query(query, [
user.id,
user.email,
user.passwordHash
]);
return this.toDomainEntity(result.rows[0]);
}
async findByEmail(email) {
const query = 'SELECT * FROM users WHERE email = $1';
const result = await this.db.query(query, [email.toLowerCase()]);
if (result.rows.length === 0) return null;
return this.toDomainEntity(result.rows[0]);
}
toDomainEntity(row) {
return new User(row.id, row.email, row.password_hash);
}
}
```
### Event-Driven Architecture
```javascript
// Event Bus Implementation
class EventBus {
constructor() {
this.handlers = new Map();
this.middlewares = [];
}
subscribe(eventType, handler) {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, []);
}
this.handlers.get(eventType).push(handler);
// Return unsubscribe function
return () => {
const handlers = this.handlers.get(eventType);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
};
}
async publish(eventType, payload) {
const event = {
type: eventType,
payload,
timestamp: new Date().toISOString(),
id: generateId()
};
// Apply middlewares
for (const middleware of this.middlewares) {
await middleware(event);
}
// Execute handlers
const handlers = this.handlers.get(eventType) || [];
const promises = handlers.map(handler =>
this.executeHandler(handler, event)
);
await Promise.allSettled(promises);
}
async executeHandler(handler, event) {
try {
await handler(event);
} catch (error) {
console.error(`Handler error for ${event.type}:`, error);
// Could publish error event here
}
}
use(middleware) {
this.middlewares.push(middleware);
}
}
// Domain Events
class OrderCreatedEvent {
constructor(order) {
this.orderId = order.id;
this.userId = order.userId;
this.items = order.items;
this.total = order.total;
this.timestamp = new Date().toISOString();
}
}
// Event Handlers
class EmailNotificationHandler {
constructor(emailService, userRepository) {
this.emailService = emailService;
this.userRepository = userRepository;
}
async handleOrderCreated(event) {
const user = await this.userRepository.findById(event.payload.userId);
await this.emailService.send({
to: user.email,
subject: 'Order Confirmation',
template: 'order-confirmation',
data: {
orderNumber: event.payload.orderId,
items: event.payload.items,
total: event.payload.total
}
});
}
}
// Event Sourcing
class EventStore {
constructor(db) {
this.db = db;
}
async append(streamId, events) {
const query = `
INSERT INTO events (stream_id, event_type, payload, version, timestamp)
VALUES ($1, $2, $3, $4, $5)
`;
const client = await this.db.connect();
try {
await client.query('BEGIN');
for (const event of events) {
await client.query(query, [
streamId,
event.type,
JSON.stringify(event.payload),
event.version,
event.timestamp
]);
}
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getEvents(streamId, fromVersion = 0) {
const query = `
SELECT * FROM events
WHERE stream_id = $1 AND version > $2
ORDER BY version ASC
`;
const result = await this.db.query(query, [streamId, fromVersion]);
return result.rows.map(row => ({
type: row.event_type,
payload: row.payload,
version: row.version,
timestamp: row.timestamp
}));
}
}
```
## 데이터베이스 패턴
### Repository Pattern with Unit of Work
```javascript
// Unit of Work
class UnitOfWork {
constructor(db) {
this.db = db;
this.client = null;
this.repositories = new Map();
}
async begin() {
this.client = await this.db.connect();
await this.client.query('BEGIN');
}
async commit() {
if (!this.client) {
throw new Error('No transaction in progress');
}
await this.client.query('COMMIT');
this.client.release();
this.client = null;
}
async rollback() {
if (!this.client) {
throw new Error('No transaction in progress');
}
await this.client.query('ROLLBACK');
this.client.release();
this.client = null;
}
getRepository(name, RepositoryClass) {
if (!this.repositories.has(name)) {
this.repositories.set(
name,
new RepositoryClass(this.client || this.db)
);
}
return this.repositories.get(name);
}
}
// Usage
class OrderService {
constructor(unitOfWork) {
this.unitOfWork = unitOfWork;
}
async createOrder(orderData) {
await this.unitOfWork.begin();
try {
const userRepo = this.unitOfWork.getRepository('user', UserRepository);
const orderRepo = this.unitOfWork.getRepository('order', OrderRepository);
const inventoryRepo = this.unitOfWork.getRepository('inventory', InventoryRepository);
// Validate user
const user = await userRepo.findById(orderData.userId);
if (!user) throw new Error('User not found');
// Check inventory
for (const item of orderData.items) {
const available = await inventoryRepo.checkAvailability(
item.productId,
item.quantity
);
if (!available) {
throw new Error(`Insufficient inventory for ${item.productId}`);
}
}
// Create order
const order = await orderRepo.create(orderData);
// Update inventory
for (const item of orderData.items) {
await inventoryRepo.decrementStock(item.productId, item.quantity);
}
await this.unitOfWork.commit();
return order;
} catch (error) {
await this.unitOfWork.rollback();
throw error;
}
}
}
```
### Database Migration Management
```javascript
// Migration System
class MigrationRunner {
constructor(db, migrationsPath) {
this.db = db;
this.migrationsPath = migrationsPath;
}
async run() {
await this.ensureMigrationTable();
const appliedMigrations = await this.getAppliedMigrations();
const migrations = await this.loadMigrations();
for (const migration of migrations) {
if (!appliedMigrations.includes(migration.id)) {
await this.applyMigration(migration);
}
}
}
async ensureMigrationTable() {
await this.db.query(`
CREATE TABLE IF NOT EXISTS migrations (
id VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}
async getAppliedMigrations() {
const result = await this.db.query('SELECT id FROM migrations');
return result.rows.map(row => row.id);
}
async loadMigrations() {
const files = fs.readdirSync(this.migrationsPath);
const migrations = [];
for (const file of files) {
if (file.endsWith('.js')) {
const migration = require(path.join(this.migrationsPath, file));
migrations.push({
id: file,
up: migration.up,
down: migration.down
});
}
}
return migrations.sort((a, b) => a.id.localeCompare(b.id));
}
async applyMigration(migration) {
const client = await this.db.connect();
try {
await client.query('BEGIN');
console.log(`Applying migration: ${migration.id}`);
await migration.up(client);
await client.query(
'INSERT INTO migrations (id) VALUES ($1)',
[migration.id]
);
await client.query('COMMIT');
console.log(`Migration ${migration.id} applied successfully`);
} catch (error) {
await client.query('ROLLBACK');
console.error(`Failed to apply migration ${migration.id}:`, error);
throw error;
} finally {
client.release();
}
}
}
// Migration Example
// migrations/001_create_users_table.js
module.exports = {
async up(db) {
await db.query(`
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
`);
await db.query(`
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
`);
},
async down(db) {
await db.query('DROP TABLE IF EXISTS users');
}
};
```
## API 패턴
### GraphQL DataLoader Pattern
```javascript
// DataLoader for N+1 query prevention
class UserLoader {
constructor(userRepository) {
this.userRepository = userRepository;
this.loader = new DataLoader(
keys => this.batchLoadUsers(keys),
{ cache: true }
);
}
async batchLoadUsers(userIds) {
const users = await this.userRepository.findByIds(userIds);
// Map users to maintain order
const userMap = new Map();
users.forEach(user => userMap.set(user.id, user));
return userIds.map(id => userMap.get(id) || null);
}
async load(userId) {
return this.loader.load(userId);
}
async loadMany(userIds) {
return this.loader.loadMany(userIds);
}
clearCache(userId) {
this.loader.clear(userId);
}
}
// GraphQL Resolvers with DataLoader
const resolvers = {
Query: {
user: async (parent, { id }, { dataloaders }) => {
return dataloaders.user.load(id);
},
users: async (parent, args, { userRepository }) => {
return userRepository.findAll(args);
}
},
User: {
orders: async (user, args, { dataloaders }) => {
return dataloaders.ordersByUser.load(user.id);
},
profile: async (user, args, { dataloaders }) => {
return dataloaders.profile.load(user.id);
}
},
Order: {
user: async (order, args, { dataloaders }) => {
return dataloaders.user.load(order.userId);
},
items: async (order, args, { dataloaders }) => {
return dataloaders.orderItems.load(order.id);
}
}
};
// Context Factory
function createGraphQLContext(req) {
const repositories = {
user: new UserRepository(db),
order: new OrderRepository(db),
profile: new ProfileRepository(db)
};
const dataloaders = {
user: new UserLoader(repositories.user),
ordersByUser: new OrdersByUserLoader(repositories.order),
profile: new ProfileLoader(repositories.profile),
orderItems: new OrderItemsLoader(repositories.order)
};
return {
req,
repositories,
dataloaders,
user: req.user // From auth middleware
};
}
```
### REST API Versioning
```javascript
// API Version Management
class APIVersionManager {
constructor() {
this.versions = new Map();
}
register(version, routes) {
this.versions.set(version, routes);
}
middleware() {
return (req, res, next) => {
// Extract version from header or URL
const version = this.extractVersion(req);
if (!this.versions.has(version)) {
return res.status(400).json({
error: 'Unsupported API version'
});
}
req.apiVersion = version;
next();
};
}
extractVersion(req) {
// Priority: Header > URL > Default
if (req.headers['api-version']) {
return req.headers['api-version'];
}
const urlMatch = req.path.match(/^\/api\/v(\d+)/);
if (urlMatch) {
return `v${urlMatch[1]}`;
}
return 'v1'; // Default version
}
getRouter(version) {
return this.versions.get(version);
}
}
// Version-specific implementations
// v1/users.js
const v1UserRoutes = {
getUser: async (req, res) => {
const user = await userService.findById(req.params.id);
res.json({
id: user.id,
email: user.email,
name: `${user.firstName} ${user.lastName}`
});
}
};
// v2/users.js
const v2UserRoutes = {
getUser: async (req, res) => {
const user = await userService.findById(req.params.id);
res.json({
id: user.id,
email: user.email,
profile: {
firstName: user.firstName,
lastName: user.lastName,
displayName: user.displayName
},
preferences: user.preferences
});
}
};
```
## 성능 최적화
### Connection Pooling
```javascript
// Database Connection Pool
class DatabasePool {
constructor(config) {
this.config = config;
this.pool = new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
max: config.maxConnections || 20,
idleTimeoutMillis: config.idleTimeout || 30000,
connectionTimeoutMillis: config.connectionTimeout || 2000
});
// Connection lifecycle events
this.pool.on('connect', (client) => {
console.log('New client connected');
client.query('SET search_path TO public');
});
this.pool.on('error', (err, client) => {
console.error('Unexpected error on idle client', err);
});
}
async query(text, params) {
const start = Date.now();
try {
const result = await this.pool.query(text, params);
const duration = Date.now() - start;
// Log slow queries
if (duration > 1000) {
console.warn('Slow query detected:', {
query: text,
duration,
rows: result.rowCount
});
}
return result;
} catch (error) {
console.error('Query error:', error);
throw error;
}
}
async transaction(callback) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async end() {
await this.pool.end();
}
}
```
### Caching Strategies
```javascript
// Multi-layer Cache
class CacheManager {
constructor(memoryCache, redisCache) {
this.L1 = memoryCache; // In-memory cache
this.L2 = redisCache; // Redis cache
}
async get(key, options = {}) {
// Check L1 cache
const l1Value = this.L1.get(key);
if (l1Value !== undefined) {
return l1Value;
}
// Check L2 cache
const l2Value = await this.L2.get(key);
if (l2Value !== null) {
// Populate L1 cache
this.L1.set(key, l2Value, options.ttl);
return l2Value;
}
return null;
}
async set(key, value, options = {}) {
// Set in both caches
this.L1.set(key, value, options.ttl);
await this.L2.set(key, value, options.ttl || 3600);
}
async invalidate(pattern) {
// Clear from L1
this.L1.clear(pattern);
// Clear from L2
await this.L2.invalidate(pattern);
}
}
// Query Result Cache
class QueryCache {
constructor(cache, db) {
this.cache = cache;
this.db = db;
}
async query(sql, params, options = {}) {
const cacheKey = this.generateCacheKey(sql, params);
// Try cache first
const cached = await this.cache.get(cacheKey);
if (cached && !options.skipCache) {
return cached;
}
// Execute query
const result = await this.db.query(sql, params);
// Cache result
if (options.cache !== false) {
await this.cache.set(
cacheKey,
result.rows,
options.ttl || 300
);
}
return result.rows;
}
generateCacheKey(sql, params) {
const hash = crypto
.createHash('sha256')
.update(sql + JSON.stringify(params))
.digest('hex');
return `query:${hash}`;
}
async invalidatePattern(table) {
// Invalidate all queries related to a table
await this.cache.invalidate(`query:*${table}*`);
}
}
```
## 보안 구현
### OAuth 2.0 Server Implementation
```javascript
// OAuth 2.0 Authorization Server
class OAuth2Server {
constructor(clientStore, tokenStore, userStore) {
this.clientStore = clientStore;
this.tokenStore = tokenStore;
this.userStore = userStore;
}
// Authorization Code Grant
async authorize(request) {
const {
client_id,
redirect_uri,
response_type,
scope,
state
} = request.query;
// Validate client
const client = await this.clientStore.findById(client_id);
if (!client) {
throw new OAuth2Error('invalid_client');
}
// Validate redirect URI
if (!client.redirectUris.includes(redirect_uri)) {
throw new OAuth2Error('invalid_redirect_uri');
}
// Generate authorization code
const code = crypto.randomBytes(32).toString('hex');
await this.tokenStore.saveAuthCode({
code,
clientId: client_id,
userId: request.user.id,
redirectUri: redirect_uri,
scope: scope.split(' '),
expiresAt: Date.now() + 600000 // 10 minutes
});
// Redirect with code
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', code);
if (state) {
redirectUrl.searchParams.set('state', state);
}
return redirectUrl.toString();
}
// Token Exchange
async token(request) {
const { grant_type } = request.body;
switch (grant_type) {
case 'authorization_code':
return this.authorizationCodeGrant(request.body);
case 'refresh_token':
return this.refreshTokenGrant(request.body);
case 'client_credentials':
return this.clientCredentialsGrant(request.body);
default:
throw new OAuth2Error('unsupported_grant_type');
}
}
async authorizationCodeGrant(params) {
const { code, client_id, client_secret, redirect_uri } = params;
// Validate client credentials
const client = await this.clientStore.findById(client_id);
if (!client || client.secret !== client_secret) {
throw new OAuth2Error('invalid_client');
}
// Validate authorization code
const authCode = await this.tokenStore.getAuthCode(code);
if (!authCode || authCode.clientId !== client_id) {
throw new OAuth2Error('invalid_grant');
}
if (Date.now() > authCode.expiresAt) {
throw new OAuth2Error('invalid_grant', 'Code expired');
}
if (authCode.redirectUri !== redirect_uri) {
throw new OAuth2Error('invalid_grant');
}
// Generate tokens
const accessToken = this.generateAccessToken(
authCode.userId,
client_id,
authCode.scope
);
const refreshToken = crypto.randomBytes(32).toString('hex');
// Save tokens
await this.tokenStore.saveTokens({
accessToken: accessToken.token,
refreshToken,
userId: authCode.userId,
clientId: client_id,
scope: authCode.scope,
accessTokenExpiresAt: accessToken.expiresAt,
refreshTokenExpiresAt: Date.now() + 2592000000 // 30 days
});
// Revoke authorization code
await this.tokenStore.revokeAuthCode(code);
return {
access_token: accessToken.token,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: authCode.scope.join(' ')
};
}
generateAccessToken(userId, clientId, scope) {
const payload = {
sub: userId,
client: clientId,
scope,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
};
const token = jwt.sign(payload, process.env.JWT_SECRET);
return {
token,
expiresAt: Date.now() + 3600000
};
}
}
```
### API Key Management
```javascript
// API Key Service
class APIKeyService {
constructor(db, crypto) {
this.db = db;
this.crypto = crypto;
}
async generateAPIKey(userId, name, permissions = []) {
// Generate secure random key
const keyValue = this.generateSecureKey();
const keyHash = await this.hashAPIKey(keyValue);
// Store key metadata
const apiKey = await this.db.query(`
INSERT INTO api_keys
(user_id, name, key_hash, permissions, last_used_at)
VALUES ($1, $2, $3, $4, NULL)
RETURNING id, created_at
`, [userId, name, keyHash, JSON.stringify(permissions)]);
// Return key value only once
return {
id: apiKey.rows[0].id,
key: `sk_live_${keyValue}`,
name,
permissions,
createdAt: apiKey.rows[0].created_at
};
}
generateSecureKey() {
return crypto.randomBytes(32).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async hashAPIKey(key) {
return crypto
.createHash('sha256')
.update(key)
.digest('hex');
}
async validateAPIKey(keyValue) {
// Extract actual key
const key = keyValue.replace(/^sk_(live|test)_/, '');
const keyHash = await this.hashAPIKey(key);
// Find key in database
const result = await this.db.query(`
UPDATE api_keys
SET last_used_at = NOW(), usage_count = usage_count + 1
WHERE key_hash = $1 AND revoked_at IS NULL
RETURNING *
`, [keyHash]);
if (result.rows.length === 0) {
return null;
}
const apiKey = result.rows[0];
// Check rate limits
if (apiKey.rate_limit) {
const rateLimitExceeded = await this.checkRateLimit(
apiKey.id,
apiKey.rate_limit
);
if (rateLimitExceeded) {
throw new Error('Rate limit exceeded');
}
}
return {
id: apiKey.id,
userId: apiKey.user_id,
permissions: apiKey.permissions,
rateLimit: apiKey.rate_limit
};
}
async revokeAPIKey(keyId, userId) {
const result = await this.db.query(`
UPDATE api_keys
SET revoked_at = NOW()
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
RETURNING id
`, [keyId, userId]);
return result.rows.length > 0;
}
}
```