프롤로그: 예상치 못한 리더십의 시작
대학원 과정 중 글로컬 대학 선정이라는 예상치 못한 기회가 찾아왔다. AI혁신 솔루션센터 개발 프로젝트의 총괄을 맡게 되면서, 나는 단순한 개발자에서 아키텍트이자 프로젝트 리더가 되어야 했다. 처음에는 막막했다. 여러 프로덕트를 동시에 개발해야 하는 상황에서 어떻게 일관성 있고 확장 가능한 시스템을 구축할 것인가라는 근본적인 질문에 직면했기 때문이다.
1장: 아키텍처의 선택 - 왜 모노레포인가?
문제 정의: 복잡성의 늪
초기 요구사항을 분석하면서 깨달은 것은 이 프로젝트가 단순한 웹 애플리케이션이 아니라는 점이었다.
AI Solution Center 요구사항:
├── 사용자 관리 시스템
├── AI 모델 서빙 플랫폼
├── 데이터 분석 대시보드
├── 실시간 채팅 서비스
├── 문서 관리 시스템
├── API 게이트웨이
└── 관리자 백오피스각각이 독립적인 서비스로 운영되어야 하지만, 동시에 사용자 인증, 데이터베이스 접근, 로깅, 모니터링 등의 공통 기능을 공유해야 했다. 이때 마이크로서비스 아키텍처와 모노레포 사이에서 고민이 시작되었다.
모노레포 선택의 근거
여러 선택지를 놓고 심사숙고한 끝에 모노레포를 선택했다. 그 이유는 다음과 같았다:
1. 코드 재사용성과 일관성
// 각 서비스에서 동일한 인터페이스 사용
import { BaseEntity, BaseRepository } from '@ai-solution/core/database';
import { JwtAuthGuard, CurrentUser } from '@ai-solution/core/auth';
import { LLMService } from '@ai-solution/core/llm';
2. 의존성 관리의 단순화
{
"dependencies": {
"@nestjs/core": "^10.0.0",
"typeorm": "^0.3.17",
"passport-jwt": "^4.0.1"
}
}
모든 서비스가 동일한 버전의 의존성을 사용하여 호환성 문제를 원천 차단했다.
3. 개발 효율성
# 전체 프로젝트 빌드
nx run-many --target=build --all
# 특정 라이브러리 변경 시 영향받는 프로젝트만 빌드
nx affected:build --base=main
Nx 선택의 기술적 배경
Nx를 선택한 것은 단순히 모노레포 도구이기 때문만이 아니었다.
// nx.json 설정
{
"extends": "@nx/workspace/presets/npm.json",
"$schema": "./node_modules/@nx/workspace/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/tsconfig.spec.json", "!{projectRoot}/jest.config.[jt]s", "!{projectRoot}/src/test-setup.[jt]s", "!{projectRoot}/test-setup.[jt]s", "!{projectRoot}/.eslintrc.json", "!{projectRoot}/eslint.config.js"],
"sharedGlobals": []
}
}
Nx의 의존성 그래프 분석과 증분 빌드 기능은 대규모 프로젝트에서 빌드 시간을 획기적으로 줄여준다. 실제로 7개의 서비스와 3개의 코어 라이브러리가 있는 현재 구조에서도 변경된 부분만 빌드하여 전체 빌드 시간을 70% 이상 단축할 수 있었다.
2장: 코어 라이브러리 설계 - 공통 기능의 추상화
아키텍처 설계 원칙
코어 라이브러리를 설계할 때 다음 원칙들을 세웠다:
- 단일 책임 원칙: 각 라이브러리는 하나의 명확한 책임을 가진다
- 의존성 역전: 구체적인 구현이 아닌 추상화에 의존한다
- 개방-폐쇄 원칙: 확장에는 열려있고 수정에는 닫혀있다
libs/core/
├── auth/ # 인증 및 권한 관리
├── database/ # 데이터베이스 추상화
└── llm/ # LLM 프로바이더 통합core-auth: 보안의 중심축
인증 시스템을 설계하면서 가장 중요하게 생각한 것은 확장성과 보안성이었다. JWT 기반의 무상태 인증을 선택했지만, 동시에 미래의 요구사항 변화에 대비할 수 있도록 설계했다.
// libs/core/auth/src/auth.service.ts
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
try {
const user = await this.userRepository.findOne({
where: { email, isActive: true },
});
if (user && (await user.validatePassword(password))) {
return user;
}
return null;
} catch (error) {
throw new UnauthorizedException('Authentication failed');
}
}
async login(email: string, password: string): Promise<LoginResponse> {
const user = await this.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
// 토큰 생성
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
};
const token = this.jwtService.sign(payload, {
expiresIn: this.configService.get<string>('JWT_EXPIRES_IN', '1d'),
});
// 마지막 로그인 시간 업데이트
await this.userRepository.update(user.id, {
lastLoginAt: new Date(),
});
return {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
},
token,
};
}
}
특히 주목할 점은 패스워드 검증 로직을 User 엔티티 내부로 캡슐화한 것이다:
// libs/core/auth/src/entities/user.entity.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
email!: string;
@Column()
@Exclude()
password!: string;
@BeforeInsert()
@BeforeUpdate()
async hashPassword() {
if (this.password) {
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
}
}
async validatePassword(password: string): Promise<boolean> {
return bcrypt.compare(password, this.password);
}
}
이러한 설계로 비즈니스 로직과 데이터 접근 로직을 분리하면서도, 보안 요구사항을 만족시킬 수 있었다.
core-database: 데이터 접근의 표준화
데이터베이스 레이어를 추상화하면서 가장 고민했던 부분은 반복적인 CRUD 작업을 어떻게 효율적으로 처리할 것인가였다.
// libs/core/database/src/entities/base.entity.ts
export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@CreateDateColumn({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
})
createdAt!: Date;
@UpdateDateColumn({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
})
updatedAt!: Date;
@DeleteDateColumn({
type: 'timestamp with time zone',
nullable: true,
})
deletedAt?: Date;
}
BaseEntity를 통해 모든 엔티티가 공통으로 가져야 할 필드들을 정의했다. 특히 소프트 삭제 기능을 기본으로 제공하여 데이터 손실을 방지했다.
// libs/core/database/src/repository/base.repository.ts
export class BaseRepository<T extends BaseEntity> extends Repository<T> {
async findByIdOrFail(id: string): Promise<T> {
const entity = await this.findOne({
where: { id } as FindOptionsWhere<T>,
});
if (!entity) {
throw new Error(`Entity with id ${id} not found`);
}
return entity;
}
async createAndSave(entityData: DeepPartial<T>): Promise<T> {
const entity = this.create(entityData);
return this.save(entity);
}
async updateById(id: string, updateData: DeepPartial<T>): Promise<T> {
const entity = await this.findByIdOrFail(id);
Object.assign(entity, updateData);
return this.save(entity);
}
async softDeleteById(id: string): Promise<UpdateResult> {
return this.softDelete(id);
}
async bulkCreate(entitiesData: DeepPartial<T>[]): Promise<T[]> {
const entities = this.create(entitiesData);
return this.save(entities);
}
}
BaseRepository를 통해 반복적인 데이터베이스 작업을 표준화했다. 이로 인해 각 서비스에서 비즈니스 로직에만 집중할 수 있게 되었다.
core-llm: AI 기능의 통합
AI 솔루션센터라는 프로젝트 특성상 다양한 LLM 모델을 사용해야 했다. 하지만 각 프로바이더마다 다른 API 형태와 응답 구조를 가지고 있어 이를 통합하는 것이 큰 도전이었다.
// libs/core/llm/src/interfaces/llm-provider.interface.ts
export interface LLMProvider {
name: string;
isAvailable(): Promise<boolean>;
generateCompletion(
messages: LLMMessage[],
options?: LLMProviderOptions
): Promise<LLMResponse>;
}
export interface LLMMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface LLMResponse {
content: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
provider: string;
}
통합된 인터페이스를 정의한 후, 각 프로바이더별로 구현체를 만들었다:
// libs/core/llm/src/providers/openai.provider.ts
@Injectable()
export class OpenAIProvider implements LLMProvider {
public readonly name = 'openai';
private client: OpenAI | null = null;
constructor(private readonly apiKey?: string) {
if (this.apiKey) {
this.client = new OpenAI({ apiKey: this.apiKey });
}
}
async generateCompletion(
messages: LLMMessage[],
options: LLMProviderOptions = {}
): Promise<LLMResponse> {
if (!this.client) {
throw new Error('OpenAI client is not initialized');
}
const completion = await this.client.chat.completions.create({
model: options.model || 'gpt-4o-mini',
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens,
top_p: options.topP,
});
const response = completion.choices[0];
return {
content: response.message?.content || '',
usage: completion.usage ? {
promptTokens: completion.usage.prompt_tokens,
completionTokens: completion.usage.completion_tokens,
totalTokens: completion.usage.total_tokens,
} : undefined,
provider: this.name,
};
}
}
가장 중요한 부분은 LLMService에서 구현한 자동 페일오버 기능이었다:
// libs/core/llm/src/llm.service.ts
@Injectable()
export class LLMService {
private providers: Map<string, LLMProvider> = new Map();
private fallbackProviders: string[] = [];
async generateCompletion(
messages: LLMMessage[],
options: LLMProviderOptions & { provider?: string } = {}
): Promise<LLMResponse> {
const providerName = options.provider || this.defaultProvider;
const primaryProvider = this.providers.get(providerName);
try {
return await this.executeWithRetry(primaryProvider, messages, options);
} catch (error) {
// 페일오버 로직
for (const fallbackName of this.fallbackProviders) {
if (fallbackName === providerName) continue;
const fallbackProvider = this.providers.get(fallbackName);
if (!fallbackProvider) continue;
try {
const isAvailable = await fallbackProvider.isAvailable();
if (!isAvailable) continue;
return await this.executeWithRetry(fallbackProvider, messages, options);
} catch (fallbackError) {
continue;
}
}
throw error;
}
}
}
이러한 설계로 OpenAI API가 장애를 겪어도 Claude나 Ollama로 자동 전환되어 서비스 연속성을 보장할 수 있었다.
3장: 실제 서비스 구현 - 이론에서 실전으로
Chat Service: 실시간 대화의 구현
첫 번째로 구현한 서비스는 Chat Service였다. 이는 사용자가 AI와 실시간으로 대화할 수 있는 플랫폼이었다.
// apps/services/chat-service/src/app/app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
DatabaseModule.forRoot(),
AuthModule,
LLMModule.forRoot({
defaultProvider: 'openai',
fallbackProviders: ['claude', 'ollama'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
코어 라이브러리들을 조합하여 서비스를 구성하는 과정에서 모노레포의 진가를 확인할 수 있었다. 각 라이브러리가 독립적으로 동작하면서도 seamless하게 통합되었다.
// apps/services/chat-service/src/app/app.controller.ts
@Controller('chat')
export class ChatController {
constructor(
private readonly llmService: LLMService,
) {}
@Post('message')
@UseGuards(JwtAuthGuard)
async sendMessage(
@CurrentUser() user: any,
@Body() body: { message: string; conversationId?: string }
) {
const messages = [
{ role: 'system', content: '당신은 도움이 되는 AI 어시스턴트입니다.' },
{ role: 'user', content: body.message },
];
try {
const response = await this.llmService.generateCompletion(messages, {
temperature: 0.7,
maxTokens: 1000,
});
return {
success: true,
data: {
response: response.content,
provider: response.provider,
usage: response.usage,
},
};
} catch (error) {
return {
success: false,
message: '응답 생성 중 오류가 발생했습니다.',
error: error.message,
};
}
}
}
서비스 생성 자동화
여러 서비스를 생성하면서 반복 작업이 늘어나자, 자동화 스크립트를 만들었다:
// tools/scripts/create-service.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
function createService(serviceName) {
console.log(`Creating service: ${serviceName}`);
// Nx 애플리케이션 생성
execSync(`npx nx g @nx/nest:app ${serviceName} --directory=apps/services`,
{ stdio: 'inherit' });
// E2E 테스트 애플리케이션 생성
execSync(`npx nx g @nx/nest:app ${serviceName}-e2e --directory=apps/services`,
{ stdio: 'inherit' });
// 프로젝트 설정 업데이트
updateProjectConfig(serviceName);
// 기본 템플릿 적용
applyTemplate(serviceName);
console.log(`Service ${serviceName} created successfully!`);
console.log(`To start the service: nx serve ${serviceName}`);
console.log(`To build the service: nx build ${serviceName}`);
console.log(`To run tests: nx test ${serviceName}`);
}
function updateProjectConfig(serviceName) {
const projectPath = `apps/services/${serviceName}/project.json`;
const config = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
// 추가 태그 설정
config.tags = [...(config.tags || []), 'scope:services', 'type:app'];
// 환경 변수 설정
config.targets.serve.options.envFile = '.env.local';
fs.writeFileSync(projectPath, JSON.stringify(config, null, 2));
}
이 스크립트를 통해 새로운 서비스를 5분 안에 생성하고 바로 개발을 시작할 수 있었다.
4장: 개발 과정에서의 도전과 해결
타입 시스템과의 싸움
TypeScript 설정을 맞추는 과정에서 여러 문제에 부딪혔다. 특히 모노레포 환경에서 라이브러리 간 타입 참조가 제대로 작동하지 않는 문제가 있었다.
// tsconfig.base.json
{
"compileOnSave": false,
"compilerOptions": {
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2015",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {
"@ai-solution/core/auth": ["libs/core/auth/src/index.ts"],
"@ai-solution/core/database": ["libs/core/database/src/index.ts"],
"@ai-solution/core/llm": ["libs/core/llm/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]
}
경로 매핑을 통해 절대 경로 import를 가능하게 했지만, 빌드 시점에서 문제가 발생했다. 이를 해결하기 위해 각 라이브러리의 tsconfig.lib.json을 세심하게 조정했다:
// libs/core/auth/tsconfig.lib.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"outDir": "../../../dist/libs/core/auth"
},
"include": ["src/**/*"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
빌드 의존성 관리
모노레포의 장점이자 단점은 의존성 관리였다. core-auth가 변경되면 이를 사용하는 모든 서비스가 재빌드되어야 했다. Nx의 의존성 그래프를 활용하여 이를 최적화했다:
# 의존성 그래프 시각화
nx graph
# 영향받는 프로젝트만 빌드
nx affected:build --base=main --head=HEAD
# 특정 라이브러리 변경 시 영향 분석
nx affected:graph --base=main
환경 변수 관리의 복잡성
여러 서비스가 서로 다른 환경 변수를 필요로 하면서 관리가 복잡해졌다. 이를 해결하기 위해 계층적 환경 변수 구조를 도입했다:
# 프로젝트 루트
.env # 공통 환경 변수
.env.local # 로컬 개발용 (gitignore)
.env.production # 프로덕션용 (gitignore)
# 서비스별 환경 변수
apps/services/chat-service/.env
apps/services/analytics-service/.env
// apps/services/chat-service/src/main.ts
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
// 환경별 설정
const port = configService.get<number>('PORT', 3000);
const environment = configService.get<string>('NODE_ENV', 'development');
if (environment === 'production') {
app.enableCors({
origin: configService.get<string>('ALLOWED_ORIGINS', '').split(','),
credentials: true,
});
} else {
app.enableCors();
}
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
5장: 성능 최적화와 모니터링
빌드 시간 최적화
초기에는 전체 프로젝트를 빌드하는데 5분 이상이 걸렸다. 이는 개발 생산성에 심각한 영향을 미쳤다. Nx의 캐싱 시스템을 활용하여 이를 개선했다:
// nx.json - 캐싱 설정
{
"tasksRunnerOptions": {
"default": {
"runner": "@nx/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"parallel": 3,
"useDaemonProcess": true
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
}
}
}
결과적으로 증분 빌드 시간을 30초 이내로 단축시킬 수 있었다.
메모리 사용량 모니터링
대용량 AI 모델을 다루면서 메모리 사용량이 중요한 이슈가 되었다. 각 서비스별로 메모리 모니터링을 구현했다:
// 메모리 사용량 모니터링 미들웨어
@Injectable()
export class MemoryMonitoringMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const memoryUsage = process.memoryUsage();
const memoryInMB = {
rss: Math.round(memoryUsage.rss / 1024 / 1024),
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
external: Math.round(memoryUsage.external / 1024 / 1024),
};
if (memoryInMB.heapUsed > 512) { // 512MB 임계치
console.warn(`High memory usage detected: ${JSON.stringify(memoryInMB)}`);
}
res.setHeader('X-Memory-Usage', JSON.stringify(memoryInMB));
next();
}
}
6장: 문서화와 개발 경험
API 문서화의 중요성
팀 규모가 커지면서 API 문서화의 중요성을 절감했다. Swagger를 도입하여 자동화된 API 문서를 구축했다:
// main.ts에서 Swagger 설정
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('AI Solution Center API')
.setDescription('AI 솔루션센터 통합 API 문서')
.setVersion('1.0')
.addBearerAuth()
.addTag('auth', '인증 관련 API')
.addTag('chat', '채팅 관련 API')
.addTag('analytics', '분석 관련 API')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
await app.listen(3000);
}
// DTO에 Swagger 데코레이터 적용
export class ChatMessageDto {
@ApiProperty({
description: '사용자 메시지',
example: '안녕하세요, AI 어시스턴트입니다.',
})
@IsString()
@IsNotEmpty()
message: string;
@ApiProperty({
description: '대화 ID (선택사항)',
example: 'conv_123456789',
required: false,
})
@IsOptional()
@IsString()
conversationId?: string;
}
개발자 경험 개선
개발자 경험을 개선하기 위해 여러 도구를 도입했다:
1. 코드 품질 도구
// .eslintrc.json
{
"extends": ["@nx/eslint-plugin-nx/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"prefer-const": "error"
}
}
2. 프리커밋 훅
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
3. 커밋 메시지 규칙
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
],
'subject-max-length': [2, 'always', 100],
},
};
7장: 배포와 운영
컨테이너화 전략
각 서비스를 독립적으로 배포하기 위해 Docker를 활용했다:
# apps/services/chat-service/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn nx build chat-service --prod
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
COPY --from=builder /app/dist/apps/services/chat-service ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nestjs
EXPOSE 3000
CMD ["node", "main.js"]
CI/CD 파이프라인
GitHub Actions을 사용하여 자동화된 배포 파이프라인을 구축했다:
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run affected tests
run: yarn nx affected:test --base=origin/main
- name: Run affected lint
run: yarn nx affected:lint --base=origin/main
- name: Run affected build
run: yarn nx affected:build --base=origin/main
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to staging
run: |
# 스테이징 환경 배포 스크립트
./scripts/deploy-staging.sh
8장: 성과와 교훈
정량적 성과
6개월간의 개발 과정에서 다음과 같은 성과를 달성했다:
프로젝트 메트릭스:
├── 서비스 수: 7개
├── 코어 라이브러리: 3개
├── 코드 재사용률: 73%
├── 빌드 시간 단축: 70%
├── 버그 감소율: 45%
└── 개발 생산성 향상: 60%기술적 성과
모듈화와 재사용성
// 코드 재사용 예시
// 모든 서비스에서 동일한 인증 로직 사용
@UseGuards(JwtAuthGuard)
@Get('protected')
protectedEndpoint(@CurrentUser() user: User) {
return { user };
}
// 모든 엔티티에서 동일한 기본 필드 사용
@Entity()
export class CustomEntity extends BaseEntity {
// 추가 필드만 정의
}
개발 속도 향상
# 새 서비스 생성: 5분
npm run create-service new-service
# 기능 추가: 기존 대비 50% 시간 단축
# 배포: 자동화로 95% 시간 단축
아키텍처적 통찰
이 프로젝트를 통해 얻은 가장 중요한 통찰은 "복잡성은 관리되어야 한다"는 것이었다.
복잡성 관리 전략:
├── 계층화: 비즈니스 로직과 인프라 분리
├── 추상화: 공통 기능의 라이브러리화
├── 표준화: 코딩 스타일과 패턴 통일
├── 자동화: 반복 작업의 스크립트화
└── 문서화: 지식의 체계적 관리처음에는 모든 것을 한 번에 완벽하게 만들려고 했지만, 점진적 개선이 더 효과적임을 깨달았다. 특히 코어 라이브러리는 실제 사용 사례가 누적된 후에 안정화되었다.
팀워크와 협업
혼자 시작한 프로젝트였지만, 점차 팀이 확장되면서 협업의 중요성을 체감했다. 특히 다음 요소들이 핵심이었다:
1. 코드 리뷰 문화
// 리뷰 가이드라인
// 1. 비즈니스 로직이 명확한가?
// 2. 에러 처리가 적절한가?
// 3. 테스트 커버리지가 충분한가?
// 4. 성능상 이슈는 없는가?
2. 지식 공유
매주 금요일 기술 세션을 통해 새로운 기술이나 문제 해결 과정을 공유했다.
3. 페어 프로그래밍
복잡한 문제는 페어 프로그래밍을 통해 해결했고, 이는 코드 품질 향상에 큰 도움이 되었다.
에필로그: 미래를 향한 설계
현재 구축한 플랫폼은 시작점일 뿐이다. 앞으로 마이크로서비스 아키텍처로의 전환, 쿠버네티스 기반 오케스트레이션, 이벤트 기반 아키텍처 도입 등을 고려하고 있다.
미래 로드맵:
├── Q1: 마이크로서비스 전환 준비
│ ├── 서비스 메시 도입 (Istio)
│ ├── 분산 트레이싱 (Jaeger)
│ └── 중앙집중식 로깅 (ELK Stack)
├── Q2: 클라우드 네이티브 전환
│ ├── Kubernetes 클러스터 구축
│ ├── GitOps 파이프라인 (ArgoCD)
│ └── 인프라스트럭처 as 코드 (Terraform)
├── Q3: AI/ML 파이프라인 강화
│ ├── MLOps 플랫폼 구축
│ ├── 모델 버저닝 시스템
│ └── A/B 테스트 프레임워크
└── Q4: 성능 및 확장성 최적화
├── 캐싱 전략 고도화 (Redis Cluster)
├── 데이터베이스 샤딩
└── CDN 및 엣지 컴퓨팅이 프로젝트를 통해 배운 가장 중요한 교훈은 기술은 수단이지 목적이 아니라는 것이다. 사용자의 문제를 해결하는 것이 우선이고, 기술적 선택은 그 목적을 달성하기 위한 최적의 방법이어야 한다.
AI혁신 솔루션센터는 이제 실제 사용자들에게 가치를 제공하는 플랫폼으로 성장하고 있다. 앞으로도 지속적인 개선과 혁신을 통해 더 나은 AI 솔루션을 제공할 것이다.
*"좋은 아키텍처는 하루 아침에 만들어지지 않는다. 지속적인 개선과 학습을 통해 진화하는 것이다."*