ABAP

아직도 T001L 직접 조회해? 이제 CDS 뷰로 #shorts #SAP #ABAP

▶ YouTube에서 보기

저장 위치(Storage Location)란 — 물류 구조에서의 역할

SAP 물류 모듈에서 저장 위치(Storage Location)는 공장(Plant) 하위에 존재하는 재고 관리의 최소 단위입니다. 하나의 Plant 안에 여러 개의 Storage Location이 존재할 수 있으며, 각 Storage Location은 물리적인 창고 구역, 라인 사이드 버퍼, 검사 대기 구역 등 실제 자재가 위치하는 공간을 논리적으로 표현합니다. 예를 들어 부산 공장(Plant 1710) 하위에 완제품 창고(0001), 원자재 창고(0002), QM 검사 대기(0088)와 같은 방식으로 구성됩니다.

이 글에서 다룰 핵심 포인트는 다음과 같습니다.

  • Storage Location의 조직 구조상 위치와 데이터 모델 이해
  • T001L 테이블 직접 조회 방식의 한계와 위험성
  • 표준 CDS 뷰 I_StorageLocation의 구조와 실무 활용
  • MARA, MARC 등 자재 마스터와의 JOIN 패턴
  • 권한/언어/텍스트 필드 처리와 흔한 실수 회피 방법

이 글의 전제 — 알아두면 좋은 배경 지식

이 글은 ABAP CDS 기본 문법(@AbapCatalog, define view entity, association)과 SAP MM 조직 구조(Plant, Storage Location, Company Code)에 대한 기초적인 이해를 전제로 합니다. 또한 SE11로 T001L 테이블 구조를 한 번쯤 열어본 경험이 있으면 흐름을 빠르게 따라올 수 있습니다. VDM(Virtual Data Model)과 Basic/Composite/Consumption 뷰의 계층 구분을 알고 있으면 이해가 훨씬 수월합니다.

실습 환경과 준비물

이 글의 코드는 다음 환경을 기준으로 검증되었습니다. 릴리스에 따라 필드 구성이나 어노테이션 지원 범위가 다를 수 있으니, 사용 중인 시스템에서 I_StorageLocation이 활성화되어 있는지 먼저 확인하는 것을 권장합니다.

  • SAP S/4HANA 2022 On-Premise 이상 (또는 S/4HANA Cloud, Public/Private Edition)
  • ABAP Development Tools (ADT) for Eclipse 3.34 이상
  • ABAP Platform 2022 이상 — define view entity 문법 사용
  • 테스트용 자재 마스터, Plant, Storage Location 커스터마이징 데이터
  • 권한 오브젝트 M_MSEG_LGO(이동), M_MATE_WRK(자재/Plant) 검토 권한

T001L 테이블 직접 조회의 한계

전통적으로 ABAP 개발자는 저장 위치 정보를 얻기 위해 T001L 테이블을 직접 조회해 왔습니다. T001L은 Plant(WERKS)와 Storage Location(LGORT), 그리고 텍스트 필드(LGOBE) 등을 담고 있는 커스터마이징 마스터 테이블입니다. 얼핏 단순해 보이지만, 실무에서는 다음과 같은 문제가 반복해서 발생합니다.

  • 언어 종속 텍스트 처리: LGOBE는 로그온 언어와 무관하게 저장된 단일 텍스트로, 다국어 지원을 위해서는 T001L 외에 T001W, 언어 텍스트 테이블을 별도로 결합해야 합니다.
  • 삭제 플래그(XCHPF, 비활성화 관련 필드) 등 부가 정보를 조회 코드에서 매번 필터링해야 함
  • 권한 미반영: 순수 SELECT는 사용자의 Plant/Storage Location 권한을 자동 반영하지 않습니다.
  • Plant 텍스트, Company Code 등 연관 데이터를 얻으려면 T001W, T001과 매번 JOIN 필요
  • S/4HANA로 오면서 SAP는 VDM(Virtual Data Model)을 통해 정규화된 접근을 권장하고 있어, T001L 직접 조회는 유지보수 관점에서 점차 안티패턴으로 분류됩니다.

즉, T001L 직접 조회는 "빠른 개발"에는 유리하지만, 다국어·권한·확장성 측면에서 반복적으로 기술 부채가 쌓입니다.

I_StorageLocation CDS 뷰의 구조와 핵심 필드

I_StorageLocation은 S/4HANA VDM 계층에서 Basic Interface View에 속하는 표준 CDS 뷰입니다. Plant와 StorageLocation 조합을 키로 가지며, 텍스트, MRP 관련 플래그, 상위 조직 정보에 대한 association을 함께 제공합니다. 개발자는 이 뷰 하나만 참조해도 Storage Location 관련 대부분의 정보를 얻을 수 있도록 설계되어 있습니다.

주요 필드는 일반적으로 다음과 같이 구성됩니다(릴리스에 따라 일부 추가/제거 가능).

  • Plant — 공장 코드 (T001L-WERKS)
  • StorageLocation — 저장 위치 코드 (T001L-LGORT)
  • StorageLocationName — 저장 위치 명칭 (텍스트)
  • MRP 관련 지시자(예: MRP 제외 여부, 관련 창고 번호)
  • 연관 뷰 association: _Plant, _Text

비유하자면 T001L은 "원자재 창고 캐비닛", I_StorageLocation은 "정리된 진열대"입니다. 원자재는 그대로지만 SAP가 텍스트/권한/연관 관계를 미리 정돈해서 얹어놓았기 때문에, 애플리케이션 개발자는 도메인 로직에만 집중할 수 있게 됩니다.

기본 SELECT 사용법 — 첫 번째 실전 예제

가장 단순한 형태로, 특정 Plant의 활성 Storage Location 목록을 조회하는 실전 예제입니다. 창고 관리 대시보드의 드롭다운을 채운다고 가정합니다.

REPORT z_stor_loc_lookup.

DATA: lt_stor_loc TYPE TABLE OF i_storagelocation,
      lv_plant    TYPE i_storagelocation-plant VALUE '1710'.

SELECT plant,
       storagelocation,
       storagelocationname
  FROM i_storagelocation
  WHERE plant = @lv_plant
  ORDER BY storagelocation
  INTO TABLE @lt_stor_loc.

LOOP AT lt_stor_loc ASSIGNING FIELD-SYMBOL(<fs_loc>).
  WRITE: / <fs_loc>-plant,
           <fs_loc>-storagelocation,
           <fs_loc>-storagelocationname.
ENDLOOP.

이 코드만으로도 T001L을 직접 SELECT하고 T001W와 조인하는 기존 패턴을 상당 부분 대체할 수 있습니다. 텍스트 필드가 이미 CDS 계층에서 제공되므로 별도 로직이 필요 없다는 점이 핵심입니다.

입고/출고 대시보드 시나리오 — 두 번째 실전 예제

이번에는 실무 시나리오입니다. "부산 공장(1710)의 모든 저장 위치별 오늘자 입고 예정 자재 수량"을 집계하되, 조회 실패나 데이터 부재를 로그로 남기는 형태입니다. 실제 프로젝트에서는 MSEG/MKPF 대신 I_MaterialDocumentItem을 사용하는 것이 권장되지만, 여기서는 개념 전달을 위해 단순화합니다.

CLASS zcl_inbound_dashboard DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_row,
             plant           TYPE werks_d,
             storage_loc     TYPE lgort_d,
             loc_name        TYPE string,
             expected_qty    TYPE p LENGTH 15 DECIMALS 3,
           END OF ty_row,
           tt_row TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY.

    METHODS get_today_inbound
      IMPORTING iv_plant       TYPE werks_d
      RETURNING VALUE(rt_data) TYPE tt_row
      RAISING   cx_sy_open_sql_db.
ENDCLASS.

CLASS zcl_inbound_dashboard IMPLEMENTATION.
  METHOD get_today_inbound.

    TRY.
        SELECT loc~plant                    AS plant,
               loc~storagelocation          AS storage_loc,
               loc~storagelocationname      AS loc_name,
               COALESCE( SUM( item~quantity ), 0 ) AS expected_qty
          FROM i_storagelocation AS loc
          LEFT OUTER JOIN i_materialdocumentitem AS item
            ON  item~plant           = loc~plant
            AND item~storagelocation = loc~storagelocation
            AND item~postingdate     = @sy-datum
            AND item~movementtype    IN ( '101', '561' )
          WHERE loc~plant = @iv_plant
          GROUP BY loc~plant, loc~storagelocation, loc~storagelocationname
          ORDER BY loc~storagelocation
          INTO TABLE @rt_data.

        IF rt_data IS INITIAL.
          MESSAGE |No storage locations for plant { iv_plant }| TYPE 'I'.
        ENDIF.

      CATCH cx_sy_open_sql_db INTO DATA(lx_db).
        cl_bali_log_writer=>write(
          iv_object    = 'ZINBOUND'
          iv_subobject = 'DASHBOARD'
          iv_text      = lx_db->get_text( ) ).
        RAISE EXCEPTION lx_db.
    ENDTRY.

  ENDMETHOD.
ENDCLASS.

여기서 눈여겨볼 점은 세 가지입니다. 첫째, LEFT OUTER JOIN으로 재고 이동이 없는 저장 위치도 0으로 채워 표시합니다. 둘째, DB 예외를 cx_sy_open_sql_db로 잡아 상위 로그 오브젝트에 기록합니다. 셋째, 이동 유형 필터를 코드에 하드코딩하지 않고 상수화하거나 커스터마이징 테이블로 분리하는 것을 실전에서는 권장합니다.

자재 마스터와 JOIN — CDS 뷰 직접 정의 예제

세 번째는 프로덕션 관점에서 성능·재사용성·테스트 용이성까지 고려한 CDS 뷰 정의 예제입니다. "자재별로 어느 Plant/Storage Location에 확장되어 있는지" 조회하는 헬퍼 뷰를 만든다고 가정합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Material x Storage Location Extension'
@VDM.viewType: #COMPOSITE
@Metadata.allowExtensions: true
define view entity ZI_MaterialStorageExt
  as select from i_productplantbasic as plant_data
  association [0..1] to I_StorageLocation as _Loc
    on  $projection.Plant           = _Loc.Plant
    and $projection.StorageLocation = _Loc.StorageLocation
  association [1..1] to I_Product as _Product
    on  $projection.Product = _Product.Product
{
  key plant_data.Product              as Product,
  key plant_data.Plant                as Plant,
  key plant_data.StorageLocation      as StorageLocation,

      _Loc.StorageLocationName        as StorageLocationName,
      _Product.ProductType            as ProductType,
      _Product.BaseUnit               as BaseUnit,

      _Loc,
      _Product
}

ABAP 소비 코드에서는 다음과 같이 사용하면, 페이지네이션·정렬·집계를 모두 CDS 계층에서 처리하므로 애플리케이션 서버 부담이 줄어듭니다.

SELECT product,
       plant,
       storagelocation,
       storagelocationname,
       basetime
  FROM zi_materialstorageext
  WHERE product     = @iv_product
    AND producttype = 'FERT'
  ORDER BY plant, storagelocation
  INTO TABLE @DATA(lt_ext)
  UP TO 200 ROWS.

보안 측면에서 @AccessControl.authorizationCheck: #CHECK을 지정하면 DCL(Data Control Language)로 정의된 권한 규칙이 자동 적용됩니다. 성능 관점에서는 대량 조회 시 UP TO n ROWS와 인덱스 사용 필드(Plant, Product) 우선 필터가 중요합니다.

권한 객체와 데이터 필터링 고려 사항

Storage Location 조회에서 빠뜨리기 쉬운 부분이 권한입니다. I_StorageLocation@AccessControl.authorizationCheck: #CHECK가 설정되어 있으면, 해당 뷰를 소비하는 쿼리는 자동으로 DCL 규칙을 통과해야 합니다. 반면 #NOT_REQUIRED인 경우에는 명시적 권한 체크 코드를 ABAP 레이어에서 직접 구현해야 합니다.

실무에서 자주 발생하는 상황은 배치 사용자가 전체 Plant 데이터를 가져와야 하는데, 개인 사용자용 권한 프로파일이 적용되어 일부 Plant가 빠지는 경우입니다. 이를 방지하려면 배치 전용 기술 사용자(Technical User)에게 M_MATE_WRK(자재/Plant), M_MSEG_LGO(Storage Location 이동) 등 최소 필요 권한을 부여하고, DCL에서 배치 사용자에 대한 예외를 명시적으로 처리해야 합니다.

@MappingRole: true
define role ZI_MaterialStorageExt {
  grant select on ZI_MaterialStorageExt
    where ( Plant ) = aspect pfcg_auth( M_MATE_WRK, WERKS, ACTVT = '03' );
}

위 DCL은 M_MATE_WRK 권한 오브젝트의 WERKS 필드 기준으로 Plant를 필터링합니다. 배치 사용자에게는 해당 프로파일에 필요한 모든 Plant 값을 포함해야 합니다.

실무 적용 모범 사례와 자주 하는 실수

실제 프로젝트에서 반복적으로 마주치는 문제들을 정리합니다.

  • 텍스트가 비어 있는 문제: StorageLocationName이 공백으로 나오면 대개 T001L에 텍스트가 유지되지 않았거나, 로그온 언어와 다른 언어로만 관리되고 있는 경우입니다. I_StorageLocationText 관련 뷰를 확인하고, 필요 시 언어 파라미터를 명시적으로 지정하세요.
  • 커스터마이징 변경 후 반영 지연: SM30로 T001L에 값을 추가했는데 CDS에서 안 보인다면, 캐시 또는 트랜스포트 이슈일 가능성이 큽니다. 대상 시스템까지 이동이 완료되었는지 확인이 필요합니다.
  • 권한 뷰와 일반 뷰 혼동: #CHECK가 걸린 뷰를 배치 사용자로 조회하면 결과가 빠지는 경우가 있습니다. 배치 계정에도 M_MATE_WRK 등 최소 권한을 부여해야 합니다.
  • T001L 직접 조회를 완전히 제거하지 않는 경우: 레거시 코드에 T001L 직접 조회가 섞여 있으면, CDS 뷰 도입 후에도 데이터 불일치 리스크가 남습니다. 점진적 마이그레이션 시 모든 참조점을 파악해 단계적으로 교체해야 합니다.

자주 묻는 질문 세 가지입니다.

  1. Q. ECC 시스템에서 I_StorageLocation을 그대로 쓸 수 있나요? A. VDM 라인업이 S/4HANA 기준이므로, ECC에서는 존재하지 않거나 필드 구성이 다를 수 있습니다. ECC 프로젝트라면 커스텀 CDS 뷰를 만들어 T001L을 감싸는 접근이 현실적입니다.
  2. Q. 성능은 T001L 직접 조회보다 느리지 않나요? A. 단일 SELECT 기준에서 유의미한 차이가 발생하는 경우는 드뭅니다. 오히려 JOIN·텍스트·권한 로직을 중복 개발하는 비용이 더 큽니다.
  3. Q. 사용자 정의 필드를 추가하려면? A. @Metadata.allowExtensions: true가 지정된 뷰라면 CDS View Extension을 사용해 확장 필드를 안전하게 붙일 수 있습니다.

댓글 0

아직 댓글이 없습니다.