
이번 단계에서는 CoreERP 재고 시스템의 또 다른 핵심 축인 출고(Outbound Confirm) 로직을 구현하였다. 입고 확정을 통해 재고가 증가하는 경로를 완성했다면, 이번 단계에서는 재고가 감소하는 유일한 공식 경로를 완성하는 것이 목표였다.
ERP 시스템에서 재고는 단순 숫자가 아니라, 트랜잭션 기반으로 움직이는 자산이다. 출고는 재고 감소를 발생시키는 공식 업무 행위이며, 다음 조건을 반드시 만족해야 한다.
이 조건이 충족되어야 데이터 무결성이 유지된다.
헤더는 업무 단위, 라인은 실제 품목 단위 트랜잭션으로 분리하였다.
출고 시 Inventory는 다음 조건을 만족해야 한다.
public void applyOutbound(int qty) {
if (qty <= 0) {
throw new IllegalArgumentException("출고 수량은 1 이상이어야 합니다.");
}
int next = this.currentQty - qty;
if (next < 0) {
throw new IllegalStateException("재고가 부족합니다.");
}
this.currentQty = next;
this.lastOutboundAt = LocalDateTime.now();
}
Setter를 직접 열지 않고 도메인 메서드로 제어함으로써, 재고 감소 규칙을 Inventory 내부에 고정하였다.
출고 확정은 다음 작업을 하나의 트랜잭션으로 묶는다.
@Transactional
public OutboundConfirmResponse confirm(OutboundConfirmRequest req) {
Warehouse warehouse = warehouseRepository.findById(req.warehouseId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 창고입니다."));
Outbound outbound = outboundRepository.save(
Outbound.builder()
.outboundNo(generateOutboundNo())
.warehouse(warehouse)
.outboundDate(LocalDate.now())
.build()
);
for (OutboundLineRequest line : req.lines()) {
Item item = itemRepository.findById(line.itemId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 품목입니다."));
outboundLineRepository.save(
OutboundLine.builder()
.outbound(outbound)
.item(item)
.qty(line.qty())
.build()
);
Inventory inventory = inventoryRepository
.findByItem_ItemIdAndWarehouse_WarehouseId(item.getItemId(), warehouse.getWarehouseId())
.orElseThrow(() -> new IllegalStateException("재고가 존재하지 않습니다."));
inventory.applyOutbound(line.qty());
inventoryRepository.save(inventory);
stockTxRepository.save(
StockTx.builder()
.item(item)
.warehouse(warehouse)
.txType(TxType.OUTBOUND)
.qtyDelta(-line.qty())
.balanceAfter(inventory.getCurrentQty())
.refType("OUTBOUND")
.refId(outbound.getOutboundId())
.build()
);
}
return new OutboundConfirmResponse(outbound.getOutboundId(), outbound.getOutboundNo(), req.lines().size());
}
출고 시 원장에는 다음 값이 기록된다.
특히 balance_after를 추가함으로써, 해당 시점 재고를 즉시 확인 가능하도록 설계하였다. 이는 실무에서 사고 분석 및 감사 대응에 매우 중요한 설계 요소다.
출고는 재고 감소이므로, 재고 부족 상황은 반드시 예외 처리한다.
이 시점부터 CoreERP는 단순 CRUD 프로젝트가 아니라, 트랜잭션 기반 재고 관리 시스템의 구조를 갖추었다.
재고 증가와 감소의 공식 경로가 완성되면서, Inventory는 단순 숫자 테이블이 아니라 “업무 트랜잭션의 결과값”이 되었다.
앞으로 발주(PurchaseOrder), 창고이동(Transfer), 안전재고 경고, 장기재고 분석 등을 붙이더라도 이 구조를 기반으로 확장하면 된다.
출고 확정 로직을 통해 재고 감소 경로와 원장 기록을 완성하였다.
이제 CoreERP 재고 엔진은 증가/감소/조정의 3가지 공식 트랜잭션을 모두 갖추었다. 다음 단계는 이 데이터를 “조회하고 분석하는 기능”을 확장하는 것이다.