CoreERP 출고(Outbound) 로직 구현 기록

최병현·2026년 2월 21일

coreerp project

목록 보기
11/44
post-thumbnail

이번 단계에서는 CoreERP 재고 시스템의 또 다른 핵심 축인 출고(Outbound Confirm) 로직을 구현하였다. 입고 확정을 통해 재고가 증가하는 경로를 완성했다면, 이번 단계에서는 재고가 감소하는 유일한 공식 경로를 완성하는 것이 목표였다.


1. 이번 단계의 목적

ERP 시스템에서 재고는 단순 숫자가 아니라, 트랜잭션 기반으로 움직이는 자산이다. 출고는 재고 감소를 발생시키는 공식 업무 행위이며, 다음 조건을 반드시 만족해야 한다.

  • 출고 헤더(Outbound) 저장
  • 출고 라인(OutboundLine) 저장
  • Inventory 현재 수량 감소
  • StockTx 원장 기록 생성 (음수 delta)
  • 모든 작업을 하나의 트랜잭션으로 처리

이 조건이 충족되어야 데이터 무결성이 유지된다.


2. 설계 구조

① Outbound (헤더)

  • outbound_no (UNIQUE)
  • warehouse_id
  • outbound_date
  • outbound_type
  • customer_name
  • manager
  • memo

② OutboundLine (라인)

  • outbound_id
  • item_id
  • qty
  • memo

헤더는 업무 단위, 라인은 실제 품목 단위 트랜잭션으로 분리하였다.


3. 재고 감소 로직 (Inventory)

출고 시 Inventory는 다음 조건을 만족해야 한다.

  • currentQty - 출고수량 >= 0
  • 음수가 되면 예외 발생
  • lastOutboundAt 갱신

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 내부에 고정하였다.


4. 서비스 로직 (@Transactional)

출고 확정은 다음 작업을 하나의 트랜잭션으로 묶는다.

  • Outbound 저장
  • OutboundLine 저장
  • Inventory 감소
  • StockTx 기록

OutboundService 핵심 코드

@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());
}

5. 원장(StockTx) 기록 전략

출고 시 원장에는 다음 값이 기록된다.

  • tx_type = OUTBOUND
  • qty_delta = 음수
  • balance_after = 작업 직후 재고량
  • ref_type = OUTBOUND
  • ref_id = outbound_id

특히 balance_after를 추가함으로써, 해당 시점 재고를 즉시 확인 가능하도록 설계하였다. 이는 실무에서 사고 분석 및 감사 대응에 매우 중요한 설계 요소다.


6. 예외 처리 기준

  • 존재하지 않는 창고 → 400
  • 존재하지 않는 품목 → 400
  • 재고 부족 → 409
  • 라인 없음 → 400

출고는 재고 감소이므로, 재고 부족 상황은 반드시 예외 처리한다.


7. 현재 시스템 상태

  • 입고 확정 → 재고 증가
  • 출고 확정 → 재고 감소
  • 조정(ADJUST) → 수동 변경
  • 모든 재고 변동은 StockTx에 기록

이 시점부터 CoreERP는 단순 CRUD 프로젝트가 아니라, 트랜잭션 기반 재고 관리 시스템의 구조를 갖추었다.


8. 이번 단계의 의미

재고 증가와 감소의 공식 경로가 완성되면서, Inventory는 단순 숫자 테이블이 아니라 “업무 트랜잭션의 결과값”이 되었다.

앞으로 발주(PurchaseOrder), 창고이동(Transfer), 안전재고 경고, 장기재고 분석 등을 붙이더라도 이 구조를 기반으로 확장하면 된다.


9. 다음 단계

  • StockTx 이력 조회 API 구현
  • 재고 이력 페이지와 연결
  • 작업자(created_by) 확장
  • Reason Code 체계 설계

10. 정리

출고 확정 로직을 통해 재고 감소 경로와 원장 기록을 완성하였다.

이제 CoreERP 재고 엔진은 증가/감소/조정의 3가지 공식 트랜잭션을 모두 갖추었다. 다음 단계는 이 데이터를 “조회하고 분석하는 기능”을 확장하는 것이다.

profile
Develop

0개의 댓글