ERP 연동을 해본 사람은 안다. 기술적으로 어려운 게 아니라, 사람이 어렵다는 걸. SAP팀은 '우리 데이터 포맷에 맞춰라'고 하고, 우리 팀은 'REST API 표준을 따라야 한다'고 하고. 3개월간의 기술 개발보다 6개월간의 회의가 더 힘들었던, ERP 연동의 현실을 적어본다.
연동이 필요한 이유
ERP와 그룹웨어 연동이 필요한 대표적인 시나리오:
- 조직/인사 동기화: ERP의 조직도, 직원 정보를 그룹웨어에 반영
- 결재 연동: 그룹웨어 결재 완료 시 ERP 전표 생성
- 근태 연동: 그룹웨어 출퇴근 데이터를 ERP 급여 시스템에 전달
- 비용 처리: 그룹웨어 경비 청구를 ERP 회계 시스템에 반영
아키텍처 설계
미들웨어 방식 vs 직접 연동
직접 연동의 문제점:
- 양쪽 시스템의 스키마 변경에 취약
- 장애 전파 위험
- 트랜잭션 관리 복잡
따라서 미들웨어 방식을 채택했습니다:
ERP System ←→ Integration Middleware ←→ Groupware
↓
Staging Database
(변환/검증/로깅)
배치 vs 실시간
데이터 특성에 따라 처리 방식을 구분합니다:
- 배치 처리: 조직/인사 동기화 (1일 1회), 근태 집계 (1일 1회)
- 실시간 처리: 결재 연동, 긴급 인사발령
핵심 설계 패턴
1. Staging 테이블 활용
-- 원본 데이터 수신
CREATE TABLE stg_employee_sync (
sync_id BIGINT PRIMARY KEY,
emp_no VARCHAR(20),
emp_name NVARCHAR(100),
dept_code VARCHAR(20),
position_code VARCHAR(20),
raw_data NVARCHAR(MAX), -- 원본 JSON 보관
status VARCHAR(20), -- RECEIVED, VALIDATED, PROCESSED, ERROR
error_message NVARCHAR(500),
created_at DATETIME2,
processed_at DATETIME2
);
Staging 테이블의 장점:
- 원본 데이터 보존으로 문제 추적 용이
- 검증 실패 시 재처리 가능
- 처리 이력 관리
2. 멱등성 보장
네트워크 장애로 재전송이 발생해도 중복 처리되지 않도록 합니다:
@Transactional
public void processEmployeeSync(EmployeeSyncDto dto) {
// 이미 처리된 건인지 확인
if (syncRepository.existsByTransactionId(dto.getTransactionId())) {
log.info("Already processed: {}", dto.getTransactionId());
return;
}
// 처리 로직
Employee emp = employeeMapper.toEntity(dto);
employeeRepository.upsert(emp);
// 처리 완료 기록
syncRepository.markAsProcessed(dto.getTransactionId());
}
3. 에러 처리 및 재시도
@Retryable(
value = {TransientDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public void syncToGroupware(EmployeeDto dto) {
groupwareClient.updateEmployee(dto);
}
@Recover
public void recoverSync(TransientDataAccessException e, EmployeeDto dto) {
// Dead Letter Queue에 저장
errorQueueService.enqueue(dto, e.getMessage());
alertService.notify("Employee sync failed: " + dto.getEmpNo());
}
데이터 변환 전략
코드 매핑 테이블
ERP와 그룹웨어의 코드 체계가 다른 경우:
CREATE TABLE code_mapping (
source_system VARCHAR(20),
source_code VARCHAR(50),
target_system VARCHAR(20),
target_code VARCHAR(50),
code_type VARCHAR(50), -- DEPT, POSITION, etc.
PRIMARY KEY (source_system, source_code, code_type)
);
데이터 검증
public class EmployeeValidator {
public ValidationResult validate(EmployeeSyncDto dto) {
List<String> errors = new ArrayList<>();
if (StringUtils.isBlank(dto.getEmpNo())) {
errors.add("사원번호 누락");
}
if (!deptRepository.existsByCode(dto.getDeptCode())) {
errors.add("존재하지 않는 부서코드: " + dto.getDeptCode());
}
// 퇴직일이 입사일보다 빠른 경우
if (dto.getResignDate() != null &&
dto.getResignDate().isBefore(dto.getJoinDate())) {
errors.add("퇴직일이 입사일보다 빠름");
}
return new ValidationResult(errors.isEmpty(), errors);
}
}
현장에서 느낀 것: 그룹사 ERP 연동 프로젝트에 참여했을 때, 기술적인 구현보다 부서 간 데이터 표준을 맞추는 과정이 훨씬 오래 걸렸습니다. 부서코드 체계가 시스템마다 달라서 매핑 작업에 시간이 많이 소요되었고, 결국 ERP 연동의 진짜 난이도는 코드가 아니라 사람과 프로세스에 있다는 것을 느꼈습니다.
운영 고려사항
모니터링
- 배치 작업 성공/실패 현황
- 처리 지연 시간
- 에러 발생 추이
- 데이터 불일치 건수
장애 대응
- Circuit Breaker 적용: 대상 시스템 장애 시 빠른 실패
- Fallback 전략: 캐시 데이터 활용 또는 수동 처리 안내
- 알림: 담당자 즉시 통보
ERP 연동, 결국 사람의 문제였다
시스템 연동은 기술적 구현보다 업무 흐름의 이해가 더 중요합니다. 현업 담당자와 긴밀하게 소통하고, 예외 상황에 대한 처리 방안을 사전에 정의하는 것이 성공의 핵심입니다.
Jaeseong
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글