아키텍처 패턴 — 클린 아키텍처와 의존성 주입
"규모가 커지면 main.js 하나에 모든 코드를 넣을 수 없다" — 계층을 분리하고 의존성을 관리해야 유지보수가 가능합니다.
계층 분리 아키텍처
src/
├── main/
│ ├── index.ts # 진입점 (앱 초기화)
│ ├── modules/ # 기능별 모듈
│ │ ├── window/
│ │ │ ├── WindowManager.ts
│ │ │ └── WindowFactory.ts
│ │ ├── file/
│ │ │ ├── FileService.ts
│ │ │ └── FileHandlers.ts # IPC 핸들러
│ │ └── update/
│ │ └── UpdateService.ts
│ ├── services/ # 인프라 서비스
│ │ ├── Database.ts
│ │ ├── Logger.ts
│ │ └── ConfigStore.ts
│ └── ipc/ # IPC 라우터
│ └── registerHandlers.ts
├── preload/
│ └── index.ts
└── renderer/
└── src/
서비스 레이어 패턴
// main/services/Database.ts
interface DatabasePort {
query<T>(sql: string, params?: unknown[]): T[];
run(sql: string, params?: unknown[]): { changes: number };
}
class SQLiteDatabase implements DatabasePort {
private db: BetterSqlite3.Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.db.pragma('journal_mode = WAL');
}
query<T>(sql: string, params: unknown[] = []): T[] {
return this.db.prepare(sql).all(...params) as T[];
}
run(sql: string, params: unknown[] = []) {
return this.db.prepare(sql).run(...params);
}
}
// main/modules/file/FileService.ts
class FileService {
constructor(
private db: DatabasePort,
private logger: Logger,
) {}
async getRecentFiles(limit = 10) {
this.logger.info('최근 파일 조회');
return this.db.query(
'SELECT * FROM recent_files ORDER BY opened_at DESC LIMIT ?',
[limit]
);
}
async openFile(filePath: string) {
const content = await fs.promises.readFile(filePath, 'utf-8');
this.db.run(
'INSERT OR REPLACE INTO recent_files (path, opened_at) VALUES (?, ?)',
[filePath, new Date().toISOString()]
);
this.logger.info(`파일 열기: ${filePath}`);
return content;
}
}
IPC 핸들러 분리
// main/modules/file/FileHandlers.ts
export function registerFileHandlers(fileService: FileService) {
ipcMain.handle('file:open', async (_event, filePath: string) => {
return fileService.openFile(filePath);
});
ipcMain.handle('file:recent', async () => {
return fileService.getRecentFiles();
});
}
// main/ipc/registerHandlers.ts
export function registerAllHandlers(container: Container) {
registerFileHandlers(container.get('fileService'));
registerNoteHandlers(container.get('noteService'));
registerSettingsHandlers(container.get('configStore'));
}
간단한 DI 컨테이너
// main/container.ts
class Container {
private instances = new Map<string, unknown>();
private factories = new Map<string, () => unknown>();
register(name: string, factory: () => unknown) {
this.factories.set(name, factory);
}
get<T>(name: string): T {
if (!this.instances.has(name)) {
const factory = this.factories.get(name);
if (!factory) throw new Error(`서비스를 찾을 수 없음: ${name}`);
this.instances.set(name, factory());
}
return this.instances.get(name) as T;
}
}
// 초기화
const container = new Container();
container.register('logger', () => new Logger());
container.register('database', () => new SQLiteDatabase(dbPath));
container.register('configStore', () => new ConfigStore());
container.register('fileService', () =>
new FileService(container.get('database'), container.get('logger'))
);
container.register('windowManager', () =>
new WindowManager(container.get('configStore'))
);
앱 초기화
// main/index.ts
import { app } from 'electron';
import { Container } from './container';
import { registerAllHandlers } from './ipc/registerHandlers';
async function bootstrap() {
const container = createContainer();
// IPC 핸들러 등록
registerAllHandlers(container);
// 윈도우 생성
const windowManager = container.get<WindowManager>('windowManager');
windowManager.createMainWindow();
}
app.whenReady().then(bootstrap);
면접 포인트 정리
- 메인 프로세스 코드를 서비스/핸들러/모듈로 계층 분리
- IPC 핸들러는 비즈니스 로직에서 분리하여 테스트 용이하게
- DI 컨테이너로 서비스 간 의존성을 관리
- 포트/어댑터 패턴으로 데이터베이스 등 인프라를 교체 가능하게
아키텍처 패턴을 다뤘으면, 다음은 Webview vs BrowserView를 비교해봅시다.
댓글 로딩 중...