콘텐츠로 이동

CRUD ERP를 에이전트 ERP로 — 챗봇이 아니라 데이터 계층까지

전통 ERP는 사람이 화면을 알고 직접 조작해야 돌아갑니다. AI 에이전트 ERP의 목표는 **“의도를 자연어로 말하면 시스템이 알아서 처리”**하는 것입니다. 이 전환은 화면에 챗봇을 하나 붙이는 일이 아니라, 비즈니스 로직과 데이터 계층을 LLM이 안전하게 다룰 수 있는 형태로 다시 설계하는 일이었습니다. 이 글은 SL.AIMS 전환을 준비하며 정리한, 도메인에 관계없이 공통으로 적용되는 설계 원칙의 기록입니다. 코드 예시는 NestJS·Prisma 스타일이지만 개념은 스택과 무관합니다.

비즈니스 로직을 ‘도구(Tool)‘로 추상화한다

섹션 제목: “비즈니스 로직을 ‘도구(Tool)‘로 추상화한다”

가장 근본적인 차이는 비즈니스 로직의 위치입니다. 전통 ERP에서 로직의 진입점은 컨트롤러이고, 그 컨트롤러는 특정 화면이 호출해야만 실행됩니다. 즉 UI를 거치지 않으면 아무 일도 일어나지 않습니다.

// 전통 방식 — 사용자가 화면을 클릭해야만 실행됨
@Post('/leave-requests')
async submitLeave(@Body() dto: CreateLeaveDto) {
return this.leaveService.create(dto);
}

에이전트 ERP에서는 같은 로직을 **이름·종류·설명·핸들러를 가진 독립 ‘도구’**로 등록합니다. LLM은 도구의 설명(description)을 읽고 스스로 적절한 도구를 선택해 실행합니다. 화면이 없어도 됩니다.

// 에이전트 방식 — LLM이 description을 읽고 호출
defineTool({
name: 'hr_getLeaveBalance',
kind: 'QUERY',
description: '직원의 연차 잔여일수 조회',
handler: async ({ userId }) => hrService.getLeaveBalance(userId),
});

도구는 권한과 위험도로 분류합니다.

종류의미예시
QUERY읽기 전용 조회 — 부작용 없음잔여 연차 조회, 월간 근태 요약
COMMAND상태를 바꾸는 실행신청서 생성, 정정 요청 접수
HITL실행 전 사람 승인 필요승인/반려, 퇴직 처리, 안전사고 보고

자연어에서 DB까지 — 아키텍처 흐름

섹션 제목: “자연어에서 DB까지 — 아키텍처 흐름”

도구를 등록했다고 에이전트가 완성되는 건 아닙니다. 사용자의 자연어가 실제 DB 변경까지 도달하려면 다음 흐름이 모두 있어야 합니다.

자연어 입력 사용자 의도 LLM 라우팅 의도 → 도구 선택 도구 실행 Tool Registry HITL 검토 필요 시 승인 DB 반영 실제 변경
자연어가 실제 DB 변경까지 도달하는 다섯 단계. 되돌리기 어려운 행위는 반드시 HITL 게이트를 거칩니다.

전환을 두 계층으로 나눠 생각하면 우선순위가 명확해집니다.

① 인프라 계층 — 먼저 갖춰지는 것② 에이전트 동작 계층 — 진짜 어려운 것
내용도구 정의(ToolDef), 도구 레지스트리, HITL 구조, 권한 매핑LLM 연동, Agentic Loop(도구 선택→실행→결과→재추론), 멀티턴 대화 컨텍스트 관리
성격비교적 정적인 뼈대전환의 난이도와 가치의 대부분

HITL(Human-in-the-Loop)은 전통 ERP 결재와 방향이 반대입니다. 전통 결재는 사람이 처음부터 끝까지 판단합니다. HITL은 AI가 업무를 처리하다가 위험하거나 확신이 낮은 지점에서만 사람을 부릅니다.

전통 결재에이전트 HITL
주도권사람이 전 과정 주도AI가 주도, 사람은 게이트
흐름기안 → 결재선 지정 → 화면마다 이동하며 건건이 클릭 승인AI가 처리 중 필요를 감지 → “이 5건 승인할까요?” 카드로 위임 → 확인 후 자동 재개

기존 서비스는 대부분 **“사람 한 명이, 한 번에, 한 작업”**을 전제로 만들어졌습니다. AI 에이전트는 이 전제를 깹니다 — 병렬로, 여러 단계를, 자동 재시도하며 호출합니다. 그래서 다음 다섯 가지를 보강해야 합니다.

원칙 1 — 동시성: Race Condition 대비

섹션 제목: “원칙 1 — 동시성: Race Condition 대비”

“대기 10건 한꺼번에 처리해줘” → 같은 도구가 동시에 10번 실행됩니다. 행 잠금(row lock)이나 낙관적 락(version 컬럼) 같은 동시성 제어가 없으면 데이터가 깨집니다.

원칙 2 — 트랜잭션: Saga / 보상 트랜잭션

섹션 제목: “원칙 2 — 트랜잭션: Saga / 보상 트랜잭션”

AI는 여러 서비스를 순서대로 호출해 업무를 완성합니다. 예: 직원 생성 → 부서 배정 → 권한 부여. 마지막 단계가 실패하면 **“부서는 있는데 권한 없는 반쪽 직원”**이 남습니다. 다단계 작업에는 실패 시 이전 단계를 되돌리는 보상 트랜잭션(Saga)이 필요합니다.

응답이 늦으면 AI는 “실패했나?” 하고 같은 작업을 자동 재시도합니다. 멱등성이 없으면 신청서가 두 건 생깁니다. 모든 COMMAND·HITL 도구는 같은 키로 두 번 불려도 한 번만 적용되어야 합니다.

// 멱등성 키로 중복 생성 방지 (upsert)
async createLeaveRequest(dto, idempotencyKey: string) {
return prisma.leaveRequest.upsert({
where: { idempotencyKey },
create: { ...dto, idempotencyKey },
update: {}, // 이미 있으면 그대로 반환
});
}

DB에 idempotency_key UNIQUE 컬럼 하나만 추가하면 됩니다.

원칙 4 — 응답 포맷: LLM 전용 경량 DTO 분리

섹션 제목: “원칙 4 — 응답 포맷: LLM 전용 경량 DTO 분리”

화면 렌더링용 응답에는 LLM에 불필요한 필드(사진, 계좌, 이력 등)가 가득합니다. 이를 그대로 컨텍스트에 넣으면 토큰을 소모하고 정작 중요한 정보가 묻힙니다. UI용 DTO와 LLM용 DTO를 분리합니다.

// UI용 (기존 그대로)
class EmployeeDetailDto { /* 50+ 필드 */ }
// LLM용 경량 컨텍스트 — 4개면 충분
class EmployeeContextDto {
id: string; name: string;
department: string; leaveBalance: number;
}

원칙 5 — 이벤트: 능동적 반응을 위한 스트림

섹션 제목: “원칙 5 — 이벤트: 능동적 반응을 위한 스트림”

전통 ERP는 요청-응답(Request-Response)만 존재합니다. 하지만 “재고가 안전재고 이하로 떨어지면 자동 발주 제안” 같은 능동 동작은 시스템 변화를 감지할 채널이 있어야 가능합니다. 핵심 상태 변화를 도메인 이벤트(StockBelowSafeLevel, LeaveBalanceChanged 등)로 발행합니다. 이벤트가 없으면 AI는 폴링밖에 못 합니다.

재설계가 필요하다고 해서 모든 걸 갈아엎으라는 건 아닙니다. 다음은 그대로 활용하는 편이 낫습니다.

  • 비즈니스 로직 검증 규칙 — “재고 음수 금지”, “마감 전표 수정 금지” 같은 불변식은 AI가 도구로 호출해도 그대로 적용되어야 합니다.
  • DB 스키마의 핵심 구조 — 데이터 모델 자체는 대개 문제없습니다. 감사·학습 컬럼은 더하는 것이지 갈아엎는 게 아닙니다.
  • 기본 CRUD 동작 — “1건 조회” 같은 단순 작업은 기존 서비스로 충분합니다.
  • 도메인 지식 — 업무 규칙이 녹아든 기존 코드는 가장 값진 자산입니다.

위 보강 작업은 런칭 전 설계 단계에서 하면 코드와 테스트 비용만 듭니다. 운영 데이터가 쌓인 뒤에 하면 이야기가 완전히 달라집니다.

런칭 전 수정런칭 후 수정
치르는 비용코드 변경 + 테스트 재작성 — 끝코드 변경 + 테스트 재작성 + DB 마이그레이션 + 다운타임 계획 + 데이터 정합성 검증 + 롤백 위험 + 레거시 연동 깨짐

감사 추적 — 누가·언제·무엇을

섹션 제목: “감사 추적 — 누가·언제·무엇을”

대부분의 스키마에 createdAt·updatedAt(시각)은 있습니다. 하지만 “누가” 했는지(createdBy·updatedBy)는 누락된 경우가 많습니다. AI가 행위자로 끼어드는 순간 이 기록은 더더욱 중요해집니다 — 이 변경이 사람의 것인지 AI의 것인지 구분해야 하기 때문입니다.

// 모든 비즈니스 테이블 공통 감사 필드
createdBy String? // 생성자 (사람 or 에이전트 ID)
updatedBy String? // 최종 수정자
deletedAt DateTime? // 소프트 삭제 — 복구 가능하게
deletedBy String? // 삭제자

에이전트가 시간이 갈수록 똑똑해지려면, 매 결정을 학습 가능한 형태로 기록해야 합니다. 메커니즘은 단순합니다 — ① 기록(누가 어떤 상황에서 어떤 결정을) → ② 패턴 분석(누적 후 행동 패턴 추출) → ③ AI 제안(패턴 기반 예측·추천) → ④ 자율화(확신도 높고 거부율 낮으면 자동)의 사이클입니다.

// 레이어 A — 모든 비즈니스 테이블 공통 학습 필드
aiSuggested Boolean @default(false)
aiConfidence Float? // 0.0~1.0
aiDecisionId String? // 결정 로그 참조
humanOverridden Boolean @default(false)
humanOverrideReason String?
// 결과 피드백 (일정 시간 후 기록)
outcomeScore Float? // -1.0~+1.0
outcomeEvaluatedAt DateTime?
// 레이어 B — 행동 패턴 테이블(신규)
model UserBehaviorPattern {
id String @id
employeeId String
actionType String // "APPROVE_LEAVE"
contextHash String // 유사 상황 식별
frequency Int // 관찰 횟수
successRate Float? // AI 제안 수락률
}

이 학습 구조를 컬럼·테이블·엔진 단위로 파고든 청사진은 〈자가학습 설계 청사진〉에, 왜 기록 구조를 학습 엔진보다 먼저 박아야 하는지는 〈에이전트 자가학습〉에 있습니다.

  1. 도구 추상화 계층 — 비즈니스 기능을 name·kind·description·handler를 가진 ToolDef로 등록
  2. 도구 분류 — QUERY / COMMAND / HITL로 위험도에 따라 구분
  3. Agentic Loop & 대화 컨텍스트 — 도구 카탈로그에서 멈추지 말 것(전환 가치의 핵심)
  4. HITL 게이트 — 되돌릴 수 없는 행위에 승인 + 만료 시간
  5. 멱등성 키 — 모든 COMMAND/HITL 도구에 추가
  6. 동시성 제어 & Saga — 병렬 호출과 다단계 실패 대비
  7. LLM 전용 경량 DTO — UI 응답과 분리
  8. 도메인 이벤트 스트림 — 능동 반응 채널 확보
  9. 감사 필드createdBy·updatedBy·deletedAt·deletedBy 자동 주입 + 소프트 삭제
  10. AI 학습 필드aiSuggested·aiConfidence·humanOverridden·outcomeScore
  11. 타이밍 — 운영 데이터가 쌓이기 전, 런칭 전에 적용

이 글은 SL.AIMS를 만들며 겪은 현장 회고 중 하나입니다. 전체 그림은 〈사례연구: SL.AIMS〉에 있습니다.