53 KiB
WACE ERP Backend Architecture - 상세 분석 문서
작성일: 2026-02-06
작성자: Backend Specialist
목적: WACE ERP 시스템 백엔드 전체 아키텍처 분석 및 워크플로우 문서화
📑 목차
- 전체 개요
- 디렉토리 구조
- 기술 스택
- 미들웨어 스택
- 인증/인가 시스템
- 멀티테넌시 구현
- API 라우트 전체 목록
- 비즈니스 도메인별 모듈
- 데이터베이스 접근 방식
- 외부 시스템 연동
- 배치/스케줄 처리
- 파일 처리
- 에러 핸들링
- 로깅 시스템
- 보안 및 권한 관리
- 성능 최적화
1. 전체 개요
1.1 프로젝트 정보
- 프로젝트명: WACE ERP Backend (Node.js)
- 언어: TypeScript (Strict Mode)
- 런타임: Node.js 20.10.0+
- 프레임워크: Express.js
- 데이터베이스: PostgreSQL (Raw Query 기반)
- 포트: 8080 (기본값)
1.2 아키텍처 특징
- Layered Architecture: Controller → Service → Database 3계층 구조
- Multi-tenancy: company_code 기반 완전한 데이터 격리
- JWT 인증: Stateless 토큰 기반 인증 시스템
- Raw Query: ORM 없이 PostgreSQL 직접 쿼리 (성능 최적화)
- Connection Pool: pg 라이브러리 기반 안정적인 연결 관리
- Type-Safe: TypeScript 타입 시스템 적극 활용
1.3 주요 기능
- 관리자 기능 (사용자/권한/메뉴 관리)
- 테이블/화면 메타데이터 관리 (동적 화면 생성)
- 플로우 관리 (워크플로우 엔진)
- 데이터플로우 다이어그램 (ERD/관계도)
- 외부 DB 연동 (PostgreSQL, MySQL, MSSQL, Oracle)
- 외부 REST API 연동
- 배치 자동 실행 (Cron 스케줄러)
- 메일 발송/수신
- 파일 업로드/다운로드
- 다국어 지원
- 대시보드/리포트
2. 디렉토리 구조
backend-node/
├── src/
│ ├── app.ts # Express 앱 진입점
│ ├── config/ # 환경 설정
│ │ ├── environment.ts # 환경변수 관리
│ │ └── multerConfig.ts # 파일 업로드 설정
│ ├── controllers/ # 컨트롤러 (70+ 파일)
│ │ ├── authController.ts
│ │ ├── adminController.ts
│ │ ├── tableManagementController.ts
│ │ ├── flowController.ts
│ │ ├── dataflowController.ts
│ │ ├── batchController.ts
│ │ └── ...
│ ├── services/ # 비즈니스 로직 (80+ 파일)
│ │ ├── authService.ts
│ │ ├── adminService.ts
│ │ ├── tableManagementService.ts
│ │ ├── flowExecutionService.ts
│ │ ├── batchSchedulerService.ts
│ │ └── ...
│ ├── routes/ # API 라우터 (70+ 파일)
│ │ ├── authRoutes.ts
│ │ ├── adminRoutes.ts
│ │ ├── tableManagementRoutes.ts
│ │ ├── flowRoutes.ts
│ │ └── ...
│ ├── middleware/ # 미들웨어 (4개)
│ │ ├── authMiddleware.ts # JWT 인증
│ │ ├── permissionMiddleware.ts # 권한 체크
│ │ ├── superAdminMiddleware.ts # 슈퍼관리자 전용
│ │ └── errorHandler.ts # 에러 핸들러
│ ├── database/ # DB 연결
│ │ ├── db.ts # PostgreSQL Pool 관리
│ │ ├── DatabaseConnectorFactory.ts # 외부 DB 연결
│ │ └── runMigration.ts # 마이그레이션
│ ├── types/ # TypeScript 타입 정의 (26개)
│ │ ├── auth.ts
│ │ ├── batchTypes.ts
│ │ ├── flow.ts
│ │ └── ...
│ ├── utils/ # 유틸리티 함수
│ │ ├── jwtUtils.ts # JWT 토큰 관리
│ │ ├── permissionUtils.ts # 권한 체크
│ │ ├── logger.ts # Winston 로거
│ │ ├── encryptUtil.ts # 암호화/복호화
│ │ ├── passwordEncryption.ts # 비밀번호 암호화
│ │ └── ...
│ └── interfaces/ # 인터페이스
│ └── DatabaseConnector.ts # DB 커넥터 인터페이스
├── scripts/ # 스크립트
│ ├── dev/ # 개발 환경 스크립트
│ └── prod/ # 운영 환경 스크립트
├── data/ # 정적 데이터 (JSON)
├── uploads/ # 업로드된 파일
├── logs/ # 로그 파일
├── package.json # NPM 의존성
├── tsconfig.json # TypeScript 설정
└── .env # 환경변수
3. 기술 스택
3.1 핵심 라이브러리
{
"dependencies": {
"express": "^4.18.2", // 웹 프레임워크
"pg": "^8.16.3", // PostgreSQL 클라이언트
"jsonwebtoken": "^9.0.2", // JWT 토큰
"bcryptjs": "^2.4.3", // 비밀번호 암호화
"dotenv": "^16.3.1", // 환경변수
"cors": "^2.8.5", // CORS 처리
"helmet": "^7.1.0", // 보안 헤더
"compression": "^1.7.4", // Gzip 압축
"express-rate-limit": "^7.1.5", // Rate Limiting
"winston": "^3.11.0", // 로깅
"multer": "^1.4.5-lts.1", // 파일 업로드
"node-cron": "^4.2.1", // Cron 스케줄러
"axios": "^1.11.0", // HTTP 클라이언트
"nodemailer": "^6.10.1", // 메일 발송
"imap": "^0.8.19", // 메일 수신
"mysql2": "^3.15.0", // MySQL 연결
"mssql": "^11.0.1", // MSSQL 연결
"oracledb": "^6.9.0", // Oracle 연결
"uuid": "^13.0.0", // UUID 생성
"joi": "^17.11.0" // 데이터 검증
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/pg": "^8.15.5",
"typescript": "^5.3.3",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"eslint": "^8.55.0"
}
}
3.2 데이터베이스 연결
- 메인 DB: PostgreSQL (pg 라이브러리)
- 외부 DB 지원: MySQL, MSSQL, Oracle, PostgreSQL
- Connection Pool: Min 2~5 / Max 10~20
- Timeout: Connection 30s / Query 60s
4. 미들웨어 스택
4.1 미들웨어 실행 순서 (app.ts)
// 1. 프로세스 레벨 예외 처리
process.on('unhandledRejection', ...)
process.on('uncaughtException', ...)
process.on('SIGTERM', ...)
process.on('SIGINT', ...)
// 2. 보안 미들웨어
app.use(helmet({
contentSecurityPolicy: { ... }, // CSP 설정
frameguard: { ... } // Iframe 보호
}))
// 3. 압축 미들웨어
app.use(compression())
// 4. Body Parser
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
// 5. 정적 파일 서빙 (/uploads)
app.use('/uploads', express.static(...))
// 6. CORS 설정
app.use(cors({
origin: [...], // 허용 도메인
credentials: true, // 쿠키 포함
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
}))
// 7. Rate Limiting (1분에 10000회)
app.use('/api/', limiter)
// 8. 토큰 자동 갱신 (1시간 이내 만료 시 갱신)
app.use('/api/', refreshTokenIfNeeded)
// 9. API 라우터 (70+개)
app.use('/api/auth', authRoutes)
app.use('/api/admin', adminRoutes)
// ...
// 10. 404 핸들러
app.use('*', notFoundHandler)
// 11. 에러 핸들러
app.use(errorHandler)
4.2 인증 미들웨어 체인
// 기본 인증
authenticateToken → Controller
// 관리자 권한
authenticateToken → requireAdmin → Controller
// 슈퍼관리자 권한
authenticateToken → requireSuperAdmin → Controller
// 회사 데이터 접근
authenticateToken → requireCompanyAccess → Controller
// DDL 실행 권한
authenticateToken → requireDDLPermission → Controller
5. 인증/인가 시스템
5.1 인증 플로우
┌──────────┐
│ 로그인 │
│ 요청 │
└────┬─────┘
│
▼
┌────────────────────┐
│ AuthController │
│ .login() │
└────┬───────────────┘
│
▼
┌────────────────────┐
│ AuthService │
│ .processLogin() │
└────┬───────────────┘
│
├─► 1. loginPwdCheck() → DB에서 비밀번호 검증
│ (마스터 패스워드: qlalfqjsgh11)
│
├─► 2. getPersonBeanFromSession() → 사용자 정보 조회
│ (user_info, dept_info, company_mng JOIN)
│
├─► 3. insertLoginAccessLog() → 로그인 이력 저장
│
└─► 4. JwtUtils.generateToken() → JWT 토큰 생성
(payload: userId, userName, companyCode, userType)
5.2 JWT 토큰 구조
// Payload
{
userId: "user123", // 사용자 ID
userName: "홍길동", // 사용자명
deptName: "개발팀", // 부서명
companyCode: "ILSHIN", // 회사 코드 (멀티테넌시 키)
companyName: "일신정공", // 회사명
userType: "COMPANY_ADMIN", // 권한 레벨
userTypeName: "회사관리자", // 권한명
iat: 1234567890, // 발급 시간
exp: 1234654290, // 만료 시간 (24시간)
iss: "PMS-System", // 발급자
aud: "PMS-Users" // 대상
}
5.3 권한 체계 (3단계)
// 1. SUPER_ADMIN (최고 관리자)
- company_code = "*"
- userType = "SUPER_ADMIN"
- 전체 회사 데이터 접근 가능
- DDL 실행 가능
- 회사 생성/삭제 가능
- 시스템 설정 변경 가능
// 2. COMPANY_ADMIN (회사 관리자)
- company_code = "ILSHIN" (특정 회사)
- userType = "COMPANY_ADMIN"
- 자기 회사 데이터만 접근
- 자기 회사 사용자 관리 가능
- 회사 설정 변경 가능
// 3. USER (일반 사용자)
- company_code = "ILSHIN"
- userType = "USER" | "GUEST" | "PARTNER"
- 자기 회사 데이터만 접근
- 읽기/쓰기 권한만
5.4 토큰 갱신 메커니즘
// refreshTokenIfNeeded 미들웨어
// 1. 토큰 만료까지 1시간 미만 남은 경우
// 2. 자동으로 새 토큰 발급
// 3. 응답 헤더에 "X-New-Token" 추가
// 4. 프론트엔드에서 자동으로 토큰 교체
6. 멀티테넌시 구현
6.1 핵심 원칙
// 🚨 절대 규칙: 모든 쿼리는 company_code 필터 필수
const companyCode = req.user!.companyCode;
if (companyCode === "*") {
// 슈퍼관리자: 모든 데이터 조회 가능
query = "SELECT * FROM table ORDER BY company_code";
} else {
// 일반 사용자: 자기 회사 데이터만
query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'";
params = [companyCode];
}
6.2 회사 데이터 격리 패턴
// ✅ 올바른 패턴
async function getDataList(req: AuthenticatedRequest) {
const companyCode = req.user!.companyCode; // JWT에서 추출
// 슈퍼관리자 체크
if (companyCode === "*") {
// 모든 회사 데이터 조회
return await query("SELECT * FROM data WHERE 1=1");
}
// 일반 사용자: 자기 회사 + 슈퍼관리자 데이터 제외
return await query(
"SELECT * FROM data WHERE company_code = $1 AND company_code != '*'",
[companyCode]
);
}
// ❌ 잘못된 패턴 (절대 금지!)
async function getDataList(req: AuthenticatedRequest) {
const companyCode = req.body.companyCode; // 클라이언트에서 받음 (위험!)
return await query("SELECT * FROM data WHERE company_code = $1", [companyCode]);
}
6.3 슈퍼관리자 숨김 규칙
-- 슈퍼관리자 사용자 (company_code = '*')는
-- 일반 회사 사용자에게 보이면 안 됨
-- ✅ 올바른 쿼리
SELECT * FROM user_info
WHERE company_code = $1
AND company_code != '*' -- 슈퍼관리자 숨김
-- ❌ 잘못된 쿼리
SELECT * FROM user_info
WHERE company_code = $1 -- 슈퍼관리자 노출 위험
6.4 회사 전환 기능 (SUPER_ADMIN 전용)
// POST /api/auth/switch-company
// WACE 관리자가 특정 회사로 컨텍스트 전환
{
companyCode: "ILSHIN" // 전환할 회사 코드
}
// 새로운 JWT 토큰 발급 (company_code만 변경)
// userType은 "SUPER_ADMIN" 유지
7. API 라우트 전체 목록
7.1 인증/관리자 기능
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/auth/login |
POST | 로그인 | 공개 |
/api/auth/logout |
POST | 로그아웃 | 인증 |
/api/auth/me |
GET | 현재 사용자 정보 | 인증 |
/api/auth/status |
GET | 인증 상태 확인 | 공개 |
/api/auth/refresh |
POST | 토큰 갱신 | 인증 |
/api/auth/signup |
POST | 회원가입 (공차중계) | 공개 |
/api/auth/switch-company |
POST | 회사 전환 | 슈퍼관리자 |
/api/admin/users |
GET | 사용자 목록 | 관리자 |
/api/admin/users |
POST | 사용자 생성 | 관리자 |
/api/admin/menus |
GET | 메뉴 목록 | 인증 |
/api/admin/web-types |
GET | 웹타입 표준 관리 | 인증 |
/api/admin/button-actions |
GET | 버튼 액션 표준 | 인증 |
/api/admin/component-standards |
GET | 컴포넌트 표준 | 인증 |
/api/admin/template-standards |
GET | 템플릿 표준 | 인증 |
/api/admin/reports |
GET | 리포트 관리 | 인증 |
7.2 테이블/화면 관리
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/table-management/tables |
GET | 테이블 목록 | 인증 |
/api/table-management/tables/:table/columns |
GET | 컬럼 목록 | 인증 |
/api/table-management/tables/:table/data |
POST | 데이터 조회 | 인증 |
/api/table-management/tables/:table/add |
POST | 데이터 추가 | 인증 |
/api/table-management/tables/:table/edit |
PUT | 데이터 수정 | 인증 |
/api/table-management/tables/:table/delete |
DELETE | 데이터 삭제 | 인증 |
/api/table-management/tables/:table/log |
POST | 로그 테이블 생성 | 관리자 |
/api/table-management/multi-table-save |
POST | 다중 테이블 저장 | 인증 |
/api/screen-management/screens |
GET | 화면 목록 | 인증 |
/api/screen-management/screens/:id |
GET | 화면 상세 | 인증 |
/api/screen-management/screens |
POST | 화면 생성 | 관리자 |
/api/screen-groups |
GET | 화면 그룹 관리 | 인증 |
/api/screen-files |
GET | 화면 파일 관리 | 인증 |
7.3 플로우 관리
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/flow/definitions |
GET | 플로우 정의 목록 | 인증 |
/api/flow/definitions |
POST | 플로우 생성 | 인증 |
/api/flow/definitions/:id/steps |
GET | 단계 목록 | 인증 |
/api/flow/definitions/:id/steps |
POST | 단계 생성 | 인증 |
/api/flow/connections/:flowId |
GET | 연결 목록 | 인증 |
/api/flow/connections |
POST | 연결 생성 | 인증 |
/api/flow/move |
POST | 데이터 이동 (단건) | 인증 |
/api/flow/move-batch |
POST | 데이터 이동 (다건) | 인증 |
/api/flow/:flowId/step/:stepId/count |
GET | 단계 데이터 개수 | 인증 |
/api/flow/:flowId/step/:stepId/list |
GET | 단계 데이터 목록 | 인증 |
/api/flow/audit/:flowId/:recordId |
GET | 오딧 로그 조회 | 인증 |
7.4 데이터플로우/다이어그램
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/dataflow/relationships |
GET | 관계 목록 | 인증 |
/api/dataflow/relationships |
POST | 관계 생성 | 인증 |
/api/dataflow-diagrams |
GET | 다이어그램 목록 | 인증 |
/api/dataflow-diagrams/:id |
GET | 다이어그램 상세 | 인증 |
/api/dataflow |
POST | 데이터플로우 실행 | 인증 |
7.5 외부 연동
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/external-db-connections |
GET | 외부 DB 연결 목록 | 인증 |
/api/external-db-connections |
POST | 외부 DB 연결 생성 | 관리자 |
/api/external-db-connections/:id/test |
POST | 연결 테스트 | 인증 |
/api/external-db-connections/:id/tables |
GET | 테이블 목록 | 인증 |
/api/external-rest-api-connections |
GET | REST API 연결 목록 | 인증 |
/api/external-rest-api-connections |
POST | REST API 연결 생성 | 관리자 |
/api/external-rest-api-connections/:id/test |
POST | API 테스트 | 인증 |
/api/multi-connection/query |
POST | 멀티 DB 쿼리 | 인증 |
7.6 배치 관리
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/batch-configs |
GET | 배치 설정 목록 | 인증 |
/api/batch-configs |
POST | 배치 설정 생성 | 관리자 |
/api/batch-configs/:id |
PUT | 배치 설정 수정 | 관리자 |
/api/batch-configs/:id |
DELETE | 배치 설정 삭제 | 관리자 |
/api/batch-management/:id/execute |
POST | 배치 즉시 실행 | 관리자 |
/api/batch-execution-logs |
GET | 실행 이력 | 인증 |
7.7 메일 시스템
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/mail/accounts |
GET | 계정 목록 | 인증 |
/api/mail/templates-file |
GET | 템플릿 목록 | 인증 |
/api/mail/send |
POST | 메일 발송 | 인증 |
/api/mail/sent |
GET | 발송 이력 | 인증 |
/api/mail/receive |
POST | 메일 수신 | 인증 |
7.8 파일 관리
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/files/upload |
POST | 파일 업로드 | 인증 |
/api/files/download/:id |
GET | 파일 다운로드 | 인증 |
/api/files |
GET | 파일 목록 | 인증 |
/api/files/:id |
DELETE | 파일 삭제 | 인증 |
/uploads/:filename |
GET | 정적 파일 서빙 | 공개 |
7.9 기타 기능
| 경로 | 메서드 | 기능 | 권한 |
|---|---|---|---|
/api/dashboards |
GET | 대시보드 데이터 | 인증 |
/api/common-codes |
GET | 공통코드 조회 | 인증 |
/api/multilang |
GET | 다국어 조회 | 인증 |
/api/company-management |
GET | 회사 목록 | 슈퍼관리자 |
/api/departments |
GET | 부서 목록 | 인증 |
/api/roles |
GET | 권한 그룹 관리 | 인증 |
/api/ddl |
POST | DDL 실행 | 슈퍼관리자 |
/api/open-api/weather |
GET | 날씨 정보 | 인증 |
/api/open-api/exchange |
GET | 환율 정보 | 인증 |
/api/digital-twin |
GET | 디지털 트윈 | 인증 |
/api/yard-layouts |
GET | 3D 필드 레이아웃 | 인증 |
/api/schedule |
POST | 스케줄 자동 생성 | 인증 |
/api/numbering-rules |
GET | 채번 규칙 관리 | 인증 |
/api/entity-search |
POST | 엔티티 검색 | 인증 |
/api/todos |
GET | To-Do 관리 | 인증 |
/api/bookings |
GET | 예약 요청 관리 | 인증 |
/api/risk-alerts |
GET | 리스크/알림 관리 | 인증 |
/health |
GET | 헬스 체크 | 공개 |
8. 비즈니스 도메인별 모듈
8.1 관리자 도메인 (Admin)
컨트롤러: adminController.ts
서비스: adminService.ts
주요 기능:
- 사용자 관리 (CRUD)
- 메뉴 관리 (트리 구조)
- 권한 그룹 관리
- 시스템 설정
- 사용자 이력 조회
핵심 로직:
// 사용자 목록 조회 (멀티테넌시 적용)
async getUserList(params) {
const companyCode = params.userCompanyCode;
// 슈퍼관리자: 모든 회사 사용자 조회
if (companyCode === "*") {
return await query("SELECT * FROM user_info WHERE 1=1");
}
// 일반 관리자: 자기 회사 사용자만 + 슈퍼관리자 숨김
return await query(
"SELECT * FROM user_info WHERE company_code = $1 AND company_code != '*'",
[companyCode]
);
}
8.2 테이블/화면 관리 도메인
컨트롤러: tableManagementController.ts, screenManagementController.ts
서비스: tableManagementService.ts, screenManagementService.ts
주요 기능:
- 테이블 메타데이터 관리 (컬럼 설정)
- 화면 정의 (JSON 기반 동적 화면)
- 화면 그룹 관리
- 테이블 로그 시스템
- 엔티티 관계 관리
핵심 로직:
// 테이블 데이터 조회 (페이징, 정렬, 필터)
async getTableData(tableName, filters, pagination) {
const companyCode = req.user!.companyCode;
// 동적 WHERE 절 생성
const whereClauses = [`company_code = '${companyCode}'`];
// 필터 조건 추가
filters.forEach(filter => {
whereClauses.push(`${filter.column} ${filter.operator} '${filter.value}'`);
});
// 페이징 + 정렬
const sql = `
SELECT * FROM ${tableName}
WHERE ${whereClauses.join(' AND ')}
ORDER BY ${pagination.sortBy} ${pagination.sortOrder}
LIMIT ${pagination.limit} OFFSET ${pagination.offset}
`;
return await query(sql);
}
8.3 플로우 도메인 (Flow)
컨트롤러: flowController.ts
서비스: flowExecutionService.ts, flowDefinitionService.ts
주요 기능:
- 플로우 정의 (워크플로우 설계)
- 단계(Step) 관리
- 단계 간 연결(Connection) 관리
- 데이터 이동 (단건/다건)
- 조건부 이동
- 오딧 로그
핵심 로직:
// 데이터 이동 (단계 간 전환)
async moveData(flowId, fromStepId, toStepId, recordIds) {
return await transaction(async (client) => {
// 1. 연결 조건 확인
const connection = await getConnection(fromStepId, toStepId);
// 2. 조건 평가 (있으면)
if (connection.condition) {
const isValid = await evaluateCondition(connection.condition, recordIds);
if (!isValid) throw new Error("조건 불충족");
}
// 3. 데이터 이동
await client.query(
`UPDATE ${connection.targetTable}
SET flow_step_id = $1, updated_at = now()
WHERE id = ANY($2) AND company_code = $3`,
[toStepId, recordIds, companyCode]
);
// 4. 오딧 로그 기록
await insertAuditLog(flowId, recordIds, fromStepId, toStepId);
});
}
8.4 데이터플로우 도메인 (Dataflow)
컨트롤러: dataflowController.ts, dataflowDiagramController.ts
서비스: dataflowService.ts, dataflowDiagramService.ts
주요 기능:
- 테이블 관계 정의 (ERD)
- 다이어그램 관리 (시각화)
- 관계 실행 (조인 쿼리 자동 생성)
- 관계 검증
핵심 로직:
// 테이블 관계 실행 (동적 조인)
async executeRelationship(relationshipId) {
const rel = await getRelationship(relationshipId);
// 동적 조인 쿼리 생성
const sql = `
SELECT
a.*,
b.*
FROM ${rel.fromTableName} a
${rel.relationshipType === '1:N' ? 'LEFT JOIN' : 'INNER JOIN'}
${rel.toTableName} b
ON a.${rel.fromColumnName} = b.${rel.toColumnName}
WHERE a.company_code = $1
`;
return await query(sql, [companyCode]);
}
8.5 배치 도메인 (Batch)
컨트롤러: batchController.ts, batchManagementController.ts
서비스: batchService.ts, batchSchedulerService.ts
주요 기능:
- 배치 설정 관리
- Cron 스케줄링 (node-cron)
- 외부 DB → 내부 DB 데이터 동기화
- 컬럼 매핑
- 실행 이력 관리
핵심 로직:
// 배치 스케줄러 초기화 (서버 시작 시)
async initializeScheduler() {
const activeBatches = await getBatchConfigs({ is_active: 'Y' });
for (const batch of activeBatches) {
// Cron 스케줄 등록
const task = cron.schedule(batch.cron_schedule, async () => {
await executeBatchConfig(batch);
}, {
timezone: "Asia/Seoul"
});
scheduledTasks.set(batch.id, task);
}
}
// 배치 실행
async executeBatchConfig(config) {
// 1. 소스 DB에서 데이터 가져오기
const sourceData = await getSourceData(config.source_connection);
// 2. 컬럼 매핑 적용
const mappedData = applyColumnMapping(sourceData, config.batch_mappings);
// 3. 타겟 DB에 INSERT/UPDATE
await upsertToTarget(mappedData, config.target_table);
// 4. 실행 로그 기록
await logExecution(config.id, sourceData.length);
}
8.6 외부 연동 도메인 (External)
컨트롤러: externalDbConnectionController.ts, externalRestApiConnectionController.ts
서비스: externalDbConnectionService.ts, dbConnectionManager.ts
주요 기능:
- 외부 DB 연결 설정 (PostgreSQL, MySQL, MSSQL, Oracle)
- Connection Pool 관리
- 연결 테스트
- 외부 REST API 연결 설정
- API 프록시
핵심 로직:
// 외부 DB 연결 (Factory 패턴)
class DatabaseConnectorFactory {
static getConnector(dbType: string, config: any) {
switch (dbType) {
case 'POSTGRESQL':
return new PostgreSQLConnector(config);
case 'MYSQL':
return new MySQLConnector(config);
case 'MSSQL':
return new MSSQLConnector(config);
case 'ORACLE':
return new OracleConnector(config);
default:
throw new Error(`지원하지 않는 DB 타입: ${dbType}`);
}
}
}
// 외부 DB 쿼리 실행
async executeExternalQuery(connectionId, sql) {
// 1. 연결 정보 조회 (암호화된 비밀번호 복호화)
const connection = await getConnection(connectionId);
connection.password = decrypt(connection.password);
// 2. 커넥터 생성
const connector = DatabaseConnectorFactory.getConnector(
connection.db_type,
connection
);
// 3. 연결 및 쿼리 실행
await connector.connect();
const result = await connector.query(sql);
await connector.disconnect();
return result;
}
8.7 메일 도메인 (Mail)
컨트롤러: mailSendSimpleController.ts, mailReceiveBasicController.ts
서비스: mailSendSimpleService.ts, mailReceiveBasicService.ts
주요 기능:
- 메일 계정 관리 (파일 기반)
- 메일 템플릿 관리
- 메일 발송 (nodemailer)
- 메일 수신 (IMAP)
- 발송 이력 관리
- 첨부파일 처리
핵심 로직:
// 메일 발송
async sendEmail(params) {
// 1. 계정 정보 조회
const account = await getMailAccount(params.accountId);
// 2. 템플릿 적용 (있으면)
let emailBody = params.body;
if (params.templateId) {
const template = await getTemplate(params.templateId);
emailBody = renderTemplate(template.content, params.variables);
}
// 3. nodemailer 전송
const transporter = nodemailer.createTransport({
host: account.smtp_host,
port: account.smtp_port,
secure: true,
auth: {
user: account.email,
pass: decrypt(account.password)
}
});
const result = await transporter.sendMail({
from: account.email,
to: params.to,
subject: params.subject,
html: emailBody,
attachments: params.attachments
});
// 4. 발송 이력 저장
await saveSentHistory(params, result);
}
8.8 파일 도메인 (File)
컨트롤러: fileController.ts, screenFileController.ts
서비스: fileSystemManager.ts
주요 기능:
- 파일 업로드 (multer)
- 파일 다운로드
- 파일 삭제
- 화면별 파일 관리
핵심 로직:
// 파일 업로드
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
const companyCode = req.user!.companyCode;
const dir = `uploads/${companyCode}`;
fs.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${uuidv4()}-${file.originalname}`;
cb(null, uniqueName);
}
}),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
fileFilter: (req, file, cb) => {
// 파일 타입 검증
const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx|xls|xlsx/;
const isValid = allowedTypes.test(file.mimetype);
cb(null, isValid);
}
});
// 파일 메타데이터 저장
async saveFileMetadata(file, userId, companyCode) {
return await query(
`INSERT INTO file_info (
file_name, file_path, file_size, mime_type,
company_code, created_by
) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
[file.originalname, file.path, file.size, file.mimetype, companyCode, userId]
);
}
8.9 대시보드 도메인 (Dashboard)
컨트롤러: DashboardController.ts
서비스: DashboardService.ts
주요 기능:
- 대시보드 설정 관리
- 위젯 관리
- 통계 데이터 조회
- 차트 데이터 생성
9. 데이터베이스 접근 방식
9.1 Connection Pool 설정
// database/db.ts
const pool = new Pool({
host: "localhost",
port: 5432,
database: "ilshin",
user: "postgres",
password: "postgres",
// Pool 설정
min: config.nodeEnv === "production" ? 5 : 2, // 최소 연결 수
max: config.nodeEnv === "production" ? 20 : 10, // 최대 연결 수
// Timeout 설정
connectionTimeoutMillis: 30000, // 연결 대기 30초
idleTimeoutMillis: 600000, // 유휴 연결 유지 10분
statement_timeout: 60000, // 쿼리 실행 60초
query_timeout: 60000,
// 연결 유지
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
// Application Name
application_name: "WACE-PLM-Backend"
});
9.2 쿼리 실행 패턴
// 1. 기본 쿼리 (다중 행)
const users = await query<User>(
'SELECT * FROM user_info WHERE company_code = $1',
[companyCode]
);
// 2. 단일 행 쿼리
const user = await queryOne<User>(
'SELECT * FROM user_info WHERE user_id = $1',
['user123']
);
// 3. 트랜잭션
const result = await transaction(async (client) => {
await client.query('INSERT INTO table1 (...) VALUES (...)', [...]);
await client.query('INSERT INTO table2 (...) VALUES (...)', [...]);
return { success: true };
});
9.3 Parameterized Query (SQL Injection 방지)
// ✅ 올바른 방법 (Parameterized Query)
const users = await query(
'SELECT * FROM user_info WHERE user_id = $1 AND dept_code = $2',
[userId, deptCode]
);
// ❌ 잘못된 방법 (SQL Injection 위험!)
const users = await query(
`SELECT * FROM user_info WHERE user_id = '${userId}'`
);
9.4 동적 쿼리 빌더 패턴
// utils/queryBuilder.ts
class QueryBuilder {
private table: string;
private whereClauses: string[] = [];
private params: any[] = [];
private paramIndex: number = 1;
constructor(table: string) {
this.table = table;
}
where(column: string, value: any) {
this.whereClauses.push(`${column} = $${this.paramIndex++}`);
this.params.push(value);
return this;
}
whereIn(column: string, values: any[]) {
this.whereClauses.push(`${column} = ANY($${this.paramIndex++})`);
this.params.push(values);
return this;
}
build() {
const where = this.whereClauses.length > 0
? `WHERE ${this.whereClauses.join(' AND ')}`
: '';
return {
sql: `SELECT * FROM ${this.table} ${where}`,
params: this.params
};
}
}
// 사용 예시
const { sql, params } = new QueryBuilder('user_info')
.where('company_code', companyCode)
.where('user_type', 'USER')
.whereIn('dept_code', ['D001', 'D002'])
.build();
const users = await query(sql, params);
10. 외부 시스템 연동
10.1 외부 DB 연결 아키텍처
┌─────────────────────┐
│ Backend Service │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ DatabaseConnector │
│ Factory │
└──────────┬──────────┘
│
┌─────┴─────┬─────────┬─────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
│PostgreSQL│ │ MySQL │ │ MSSQL │ │ Oracle │
│Connector│ │Connector│ │Connect│ │Connect │
└─────────┘ └─────────┘ └───────┘ └────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
│External │ │External │ │External│ │External│
│ PG DB │ │MySQL DB │ │MSSQL DB│ │Oracle │
└─────────┘ └─────────┘ └───────┘ └────────┘
10.2 외부 DB Connector 인터페이스
// interfaces/DatabaseConnector.ts
interface DatabaseConnector {
connect(): Promise<void>;
disconnect(): Promise<void>;
query(sql: string, params?: any[]): Promise<any[]>;
testConnection(): Promise<boolean>;
getTables(): Promise<string[]>;
getColumns(tableName: string): Promise<ColumnInfo[]>;
}
// 구현 예시: PostgreSQL
class PostgreSQLConnector implements DatabaseConnector {
private pool: Pool;
constructor(config: ExternalDbConnection) {
this.pool = new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.username,
password: decrypt(config.password),
ssl: config.use_ssl ? { rejectUnauthorized: false } : false
});
}
async connect() {
await this.pool.connect();
}
async query(sql: string, params?: any[]) {
const result = await this.pool.query(sql, params);
return result.rows;
}
async getTables() {
const result = await this.query(
`SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename`
);
return result.map(row => row.tablename);
}
}
10.3 외부 REST API 연동
// services/externalRestApiConnectionService.ts
async callExternalApi(connectionId: number, endpoint: string, options: any) {
// 1. 연결 정보 조회
const connection = await getApiConnection(connectionId);
// 2. 인증 헤더 생성
const headers: any = {
'Content-Type': 'application/json',
...connection.headers
};
if (connection.auth_type === 'BEARER') {
headers['Authorization'] = `Bearer ${decrypt(connection.auth_token)}`;
} else if (connection.auth_type === 'API_KEY') {
headers[connection.api_key_header] = decrypt(connection.api_key);
}
// 3. Axios 요청
const response = await axios({
method: options.method || 'GET',
url: `${connection.base_url}${endpoint}`,
headers,
data: options.body,
timeout: connection.timeout || 30000
});
return response.data;
}
11. 배치/스케줄 처리
11.1 배치 스케줄러 시스템
// services/batchSchedulerService.ts
class BatchSchedulerService {
private static scheduledTasks: Map<number, ScheduledTask> = new Map();
// 서버 시작 시 모든 활성 배치 스케줄링
static async initializeScheduler() {
const activeBatches = await getBatchConfigs({ is_active: 'Y' });
for (const batch of activeBatches) {
this.scheduleBatch(batch);
}
}
// 개별 배치 스케줄 등록
static scheduleBatch(config: any) {
if (!cron.validate(config.cron_schedule)) {
throw new Error(`Invalid cron: ${config.cron_schedule}`);
}
const task = cron.schedule(
config.cron_schedule,
async () => {
await this.executeBatchConfig(config);
},
{ timezone: "Asia/Seoul" }
);
this.scheduledTasks.set(config.id, task);
}
// 배치 실행
static async executeBatchConfig(config: any) {
const startTime = new Date();
try {
// 1. 소스 DB에서 데이터 조회
const sourceData = await this.getSourceData(config);
// 2. 컬럼 매핑 적용
const mappedData = this.applyMapping(sourceData, config.batch_mappings);
// 3. 타겟 DB에 저장
await this.upsertToTarget(mappedData, config);
// 4. 성공 로그
await BatchExecutionLogService.updateExecutionLog(executionLogId, {
execution_status: 'SUCCESS',
end_time: new Date(),
success_records: mappedData.length
});
} catch (error) {
// 5. 실패 로그
await BatchExecutionLogService.updateExecutionLog(executionLogId, {
execution_status: 'FAILURE',
end_time: new Date(),
error_message: error.message
});
}
}
}
11.2 Cron 표현식 예시
# 형식: 초 분 시 일 월 요일
# 매 시간 정각
0 * * * *
# 매일 새벽 2시
0 2 * * *
# 매주 월요일 오전 9시
0 9 * * 1
# 매월 1일 자정
0 0 1 * *
# 5분마다
*/5 * * * *
# 평일 오전 8시~오후 6시, 매 시간
0 8-18 * * 1-5
11.3 배치 실행 이력 관리
// services/batchExecutionLogService.ts
interface ExecutionLog {
id: number;
batch_config_id: number;
execution_status: 'RUNNING' | 'SUCCESS' | 'FAILURE';
start_time: Date;
end_time?: Date;
total_records: number;
success_records: number;
failure_records: number;
error_message?: string;
company_code: string;
}
// 실행 로그 저장
async createExecutionLog(data: Partial<ExecutionLog>) {
return await query(
`INSERT INTO batch_execution_logs (
batch_config_id, company_code, execution_status,
start_time, total_records
) VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[data.batch_config_id, data.company_code, data.execution_status,
data.start_time, data.total_records]
);
}
12. 파일 처리
12.1 파일 업로드 흐름
┌─────────────┐
│ Frontend │
└──────┬──────┘
│ (FormData)
▼
┌─────────────────────┐
│ Multer Middleware │ → 파일 저장 (uploads/COMPANY_CODE/)
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ FileController │ → 메타데이터 저장 (file_info 테이블)
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ Response │ → { fileId, fileName, filePath, fileSize }
└─────────────────────┘
12.2 Multer 설정
// config/multerConfig.ts
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const companyCode = req.user!.companyCode;
const uploadDir = path.join(process.cwd(), 'uploads', companyCode);
// 디렉토리 없으면 생성
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// 파일명 중복 방지: 타임스탬프 + UUID + 원본파일명
const timestamp = Date.now();
const uniqueId = uuidv4();
const extension = path.extname(file.originalname);
const basename = path.basename(file.originalname, extension);
const uniqueName = `${timestamp}-${uniqueId}-${basename}${extension}`;
cb(null, uniqueName);
}
});
export const upload = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB
},
fileFilter: (req, file, cb) => {
// 허용 확장자 검증
const allowedMimeTypes = [
'image/jpeg', 'image/png', 'image/gif',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('허용되지 않는 파일 형식입니다.'));
}
}
});
12.3 파일 다운로드
// controllers/fileController.ts
async downloadFile(req: AuthenticatedRequest, res: Response) {
const fileId = parseInt(req.params.id);
const companyCode = req.user!.companyCode;
// 1. 파일 정보 조회 (권한 체크)
const file = await queryOne(
`SELECT * FROM file_info
WHERE id = $1 AND company_code = $2`,
[fileId, companyCode]
);
if (!file) {
return res.status(404).json({ error: '파일을 찾을 수 없습니다.' });
}
// 2. 파일 존재 확인
if (!fs.existsSync(file.file_path)) {
return res.status(404).json({ error: '파일이 존재하지 않습니다.' });
}
// 3. 파일 다운로드
res.download(file.file_path, file.file_name);
}
12.4 파일 삭제 (논리 삭제)
async deleteFile(req: AuthenticatedRequest, res: Response) {
const fileId = parseInt(req.params.id);
const companyCode = req.user!.companyCode;
// 1. 논리 삭제 (is_active = 'N')
await query(
`UPDATE file_info
SET is_active = 'N', deleted_at = now(), deleted_by = $3
WHERE id = $1 AND company_code = $2`,
[fileId, companyCode, req.user!.userId]
);
// 2. 물리 파일은 삭제하지 않음 (복구 가능)
// 주기적으로 삭제된 지 30일 지난 파일만 물리 삭제
}
13. 에러 핸들링
13.1 에러 핸들러 미들웨어
// middleware/errorHandler.ts
export const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// 1. PostgreSQL 에러 처리
if (err.code) {
switch (err.code) {
case '23505': // unique_violation
error = new AppError('중복된 데이터가 존재합니다.', 400);
break;
case '23503': // foreign_key_violation
error = new AppError('참조 무결성 제약 조건 위반입니다.', 400);
break;
case '23502': // not_null_violation
error = new AppError('필수 입력값이 누락되었습니다.', 400);
break;
default:
error = new AppError(`데이터베이스 오류: ${err.message}`, 500);
}
}
// 2. JWT 에러 처리
if (err.name === 'JsonWebTokenError') {
error = new AppError('유효하지 않은 토큰입니다.', 401);
}
if (err.name === 'TokenExpiredError') {
error = new AppError('토큰이 만료되었습니다.', 401);
}
// 3. 에러 로깅
logger.error({
message: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip
});
// 4. 응답
const statusCode = error.statusCode || 500;
res.status(statusCode).json({
success: false,
error: {
message: error.message,
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
}
});
};
13.2 커스텀 에러 클래스
export class AppError extends Error {
public statusCode: number;
public isOperational: boolean;
constructor(message: string, statusCode: number = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 사용 예시
if (!user) {
throw new AppError('사용자를 찾을 수 없습니다.', 404);
}
13.3 프로세스 레벨 예외 처리
// app.ts
process.on('unhandledRejection', (reason, promise) => {
logger.error('⚠️ Unhandled Promise Rejection:', {
reason: reason?.message || reason,
stack: reason?.stack
});
// 프로세스 종료하지 않고 로깅만 수행
});
process.on('uncaughtException', (error) => {
logger.error('🔥 Uncaught Exception:', {
message: error.message,
stack: error.stack
});
// 예외 발생 후에도 서버 유지 (주의: 불안정할 수 있음)
});
process.on('SIGTERM', () => {
logger.info('📴 SIGTERM 시그널 수신, graceful shutdown 시작...');
// 연결 풀 정리, 진행 중인 요청 완료 대기
process.exit(0);
});
14. 로깅 시스템
14.1 Winston Logger 설정
// utils/logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
transports: [
// 파일 로그
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10485760,
maxFiles: 10
}),
// 콘솔 로그 (개발 환경)
...(process.env.NODE_ENV === 'development' ? [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
] : [])
]
});
export { logger };
14.2 로깅 레벨
// 로그 레벨 (우선순위 높음 → 낮음)
logger.error('에러 발생', { error }); // 0
logger.warn('경고 메시지'); // 1
logger.info('정보 메시지'); // 2
logger.http('HTTP 요청'); // 3
logger.verbose('상세 정보'); // 4
logger.debug('디버그 정보', { data }); // 5
logger.silly('매우 상세한 정보'); // 6
14.3 로깅 패턴
// 1. 인증 로그
logger.info(`인증 성공: ${userInfo.userId} (${req.ip})`);
logger.warn(`인증 실패: ${errorMessage} (${req.ip})`);
// 2. 쿼리 로그 (디버그 모드)
if (config.debug) {
logger.debug('쿼리 실행:', {
query: sql,
params,
rowCount: result.rowCount,
duration: `${duration}ms`
});
}
// 3. 에러 로그
logger.error('배치 실행 실패:', {
batchId: config.id,
error: error.message,
stack: error.stack
});
// 4. 비즈니스 로그
logger.info(`플로우 데이터 이동: ${fromStepId} → ${toStepId}`, {
flowId,
recordCount: recordIds.length
});
15. 보안 및 권한 관리
15.1 비밀번호 암호화
// utils/encryptUtil.ts
export class EncryptUtil {
// 비밀번호 해싱 (bcrypt)
static hash(password: string): string {
return bcrypt.hashSync(password, 12);
}
// 비밀번호 검증
static matches(plainPassword: string, hashedPassword: string): boolean {
return bcrypt.compareSync(plainPassword, hashedPassword);
}
}
// 사용 예시
const hashedPassword = EncryptUtil.hash('mypassword');
const isValid = EncryptUtil.matches('mypassword', hashedPassword);
15.2 민감 정보 암호화 (AES-256)
// utils/credentialEncryption.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-cbc';
const SECRET_KEY = process.env.ENCRYPTION_KEY || 'default-32-char-secret-key!!!!';
const IV_LENGTH = 16;
// 암호화
export function encrypt(text: string): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(SECRET_KEY), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
// 복호화
export function decrypt(encryptedText: string): string {
const parts = encryptedText.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encryptedData = parts[1];
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(SECRET_KEY), iv);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// 사용 예시: 외부 DB 비밀번호 저장
const encryptedPassword = encrypt('db_password');
await query(
'INSERT INTO external_db_connections (password) VALUES ($1)',
[encryptedPassword]
);
// 사용 시 복호화
const connection = await queryOne('SELECT * FROM external_db_connections WHERE id = $1', [id]);
const plainPassword = decrypt(connection.password);
15.3 SQL Injection 방지
// ✅ Parameterized Query (항상 사용)
const users = await query(
'SELECT * FROM user_info WHERE user_id = $1 AND company_code = $2',
[userId, companyCode]
);
// ❌ 문자열 연결 (절대 사용 금지!)
const users = await query(
`SELECT * FROM user_info WHERE user_id = '${userId}'`
);
15.4 Rate Limiting
// app.ts
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1분
max: 10000, // 최대 10000회 (개발: 완화, 운영: 100)
message: {
error: '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'
},
skip: (req) => {
// 헬스 체크는 Rate Limiting 제외
return req.path === '/health';
}
});
app.use('/api/', limiter);
15.5 CORS 설정
// config/environment.ts
const getCorsOrigin = () => {
// 개발 환경: 모든 origin 허용
if (process.env.NODE_ENV === 'development') {
return true;
}
// 운영 환경: 허용 도메인만
return [
'http://localhost:9771',
'http://39.117.244.52:5555',
'https://v1.vexplor.com',
'https://api.vexplor.com'
];
};
// app.ts
app.use(cors({
origin: getCorsOrigin(),
credentials: true, // 쿠키 포함
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
}));
16. 성능 최적화
16.1 Connection Pool 모니터링
// database/db.ts
// 5분마다 Pool 상태 체크
setInterval(() => {
if (pool) {
const status = {
totalCount: pool.totalCount, // 전체 연결 수
idleCount: pool.idleCount, // 유휴 연결 수
waitingCount: pool.waitingCount // 대기 중인 연결 수
};
// 대기 연결이 5개 이상이면 경고
if (status.waitingCount > 5) {
console.warn('⚠️ PostgreSQL 연결 풀 대기열 증가:', status);
}
}
}, 5 * 60 * 1000);
16.2 쿼리 실행 시간 로깅
// database/db.ts
export async function query(text: string, params?: any[]) {
const client = await pool.connect();
try {
const startTime = Date.now();
const result = await client.query(text, params);
const duration = Date.now() - startTime;
// 1초 이상 걸린 쿼리는 경고
if (duration > 1000) {
logger.warn('⚠️ 느린 쿼리 감지:', {
query: text,
params,
duration: `${duration}ms`
});
}
return result.rows;
} finally {
client.release();
}
}
16.3 캐싱 전략
// utils/cache.ts (Redis 기반)
import Redis from 'redis';
const redis = Redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
// 캐시 조회 (있으면 반환, 없으면 쿼리 후 캐싱)
export async function getCachedData(key: string, fetcher: () => Promise<any>, ttl: number = 300) {
// 1. 캐시 확인
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// 2. 캐시 미스 → DB 조회
const data = await fetcher();
// 3. 캐시 저장 (TTL: 5분)
await redis.setEx(key, ttl, JSON.stringify(data));
return data;
}
// 사용 예시: 메뉴 목록 캐싱
const menuList = await getCachedData(
`menu:${companyCode}:${userId}`,
() => AdminService.getUserMenuList(params),
600 // 10분 캐싱
);
16.4 압축 (Gzip)
// app.ts
import compression from 'compression';
app.use(compression({
level: 6, // 압축 레벨 (0~9)
threshold: 1024, // 1KB 이상만 압축
filter: (req, res) => {
// JSON 응답만 압축
return req.headers['x-no-compression'] ? false : compression.filter(req, res);
}
}));
🎯 핵심 요약
아키텍처 패턴
- Layered Architecture: Controller → Service → Database
- Multi-tenancy:
company_code기반 완전한 데이터 격리 - JWT 인증: Stateless 토큰 기반 (24시간 만료)
- Raw Query: ORM 없이 PostgreSQL 직접 쿼리
보안 원칙
- 모든 쿼리에 company_code 필터 필수
- JWT 토큰에서 company_code 추출 (클라이언트 신뢰 금지)
- Parameterized Query로 SQL Injection 방지
- 비밀번호 bcrypt, 민감정보 AES-256 암호화
- Rate Limiting으로 DDoS 방지
주요 도메인
- 관리자 (사용자/메뉴/권한)
- 테이블/화면 메타데이터
- 플로우 (워크플로우 엔진)
- 데이터플로우 (ERD/관계도)
- 외부 연동 (DB/REST API)
- 배치 (Cron 스케줄러)
- 메일 (발송/수신)
- 파일 (업로드/다운로드)
API 개수
- 총 70+ 라우트
- 인증/관리자: 15개
- 테이블/화면: 20개
- 플로우: 10개
- 외부 연동: 10개
- 배치: 6개
- 메일: 5개
- 파일: 4개
문서 버전: 1.0
마지막 업데이트: 2026-02-06