이 글이 답하는 질문
CREATE OBJECT와?=조합, 문법상 유효한데 왜 바꾸라고 할까?NEW는 두 줄을 한 줄로 줄이는 것 이상의 가치가 있을까?CAST와?=는 뭐가 다르고, 다운캐스트 덤프는 어떻게 막을까?- 변수를 구현 클래스 대신 인터페이스 타입으로 잡으면 뭐가 좋아질까?
ABAP 7.40 이후 문법으로 객체 생성과 캐스팅을 현대화하는 과정을, 알림 발송 시나리오 하나를 리팩터링하며 정리합니다.
사전 가정
ABAP 클래스·인터페이스의 기본 구문은 알고 있다고 가정합니다. 실습에는 SAP NetWeaver AS ABAP 7.40 SP08 이상이 필요하며, S/4HANA(온프레미스·클라우드)와 SAP BTP ABAP Environment에서 모두 동작합니다. NEW는 7.40 SP02, CAST는 7.40 SP08부터 지원되므로 7.31 이하로 다운포트할 코드에는 쓸 수 없습니다.
구식 패턴이 남기는 세 가지 비용
DATA lo_sender TYPE REF TO zcl_email_sender. " 구현 클래스에 직접 묶임
DATA lo_notifier TYPE REF TO zif_notifier.
CREATE OBJECT lo_sender
EXPORTING iv_smtp_host = 'mail.corp.local'.
lo_notifier = lo_sender.
" ... 한참 뒤 ...
DATA lo_again TYPE REF TO zcl_email_sender.
lo_again ?= lo_notifier. " 실패하면 그대로 덤프
비용은 셋입니다. 첫째, 선언과 생성이 분리되어 초기화 전 참조를 쓰는 버그가 끼어들 틈이 생깁니다. 둘째, ?=를 TRY 없이 쓰면 타입이 어긋나는 순간 CX_SY_MOVE_CAST_ERROR 덤프가 납니다. 셋째, 변수가 구현 클래스에 묶여 있어 새 채널을 추가할 때 호출부를 전부 고쳐야 합니다. 문법 교체가 아니라 이 세 비용을 줄이는 게 목적입니다.
NEW 생성자 표현식 — 생성을 "값"으로 다루기
NEW의 본질은 객체 생성이 문(statement)이 아니라 표현식이 된다는 점입니다. 대입 우변은 물론 메서드 인자 위치에도 바로 쓸 수 있습니다.
" 1) 기본형: 생성자 파라미터를 괄호 안에 전달
DATA(lo_sender) = NEW zcl_email_sender( iv_smtp_host = 'mail.corp.local' ).
" 2) 좌변 타입이 정해져 있으면 # 로 추론
mo_logger = NEW #( iv_object = 'ZNOTIF' ).
" 3) 변수 없이 인자 자리에서 즉석 생성
lo_dispatcher->register( NEW zcl_sms_sender( iv_provider = 'KT' ) ).
3번의 체감이 큽니다. 한 번 쓰고 버릴 객체용 임시 변수가 사라지고 사용 범위가 코드에 드러납니다. 또 하나, 업캐스트는 자동입니다. lo_notifier = NEW zcl_email_sender( ... )처럼 인터페이스 타입 변수에 구현 인스턴스를 넣을 때 캐스트 연산자가 필요 없습니다. 단, NEW는 정적으로 알려진 타입 전용입니다. 클래스 이름을 런타임에 결정하는 동적 생성은 지금도 CREATE OBJECT lo_ref TYPE (lv_classname)을 씁니다. 즉 정적 생성은 NEW, 동적 생성만 CREATE OBJECT로 역할이 나뉜 것입니다.
CAST vs ?= — 같은 일, 다른 위치
둘 다 다운캐스트이고 실패 시 둘 다 CX_SY_MOVE_CAST_ERROR를 던집니다. 차이는 CAST가 표현식이라 결과를 변수에 받지 않고 그 자리에서 바로 쓸 수 있다는 점입니다.
" 구식: 중간 변수 + 별도 문장
DATA lo_mail TYPE REF TO zcl_email_sender.
lo_mail ?= lo_notifier.
lo_mail->set_reply_to( 'noreply@corp.local' ).
" 현대식: 캐스트 결과에 바로 메서드 호출
CAST zcl_email_sender( lo_notifier )->set_reply_to( 'noreply@corp.local' ).
캐스트보다 먼저 "실패할 수 있는가"를 물어야 합니다. 실패 가능성이 있으면 IS INSTANCE OF로 확인하고, 타입별 분기가 목적이면 CASE TYPE OF로 검사와 캐스트를 한 번에 처리합니다.
IF lo_notifier IS INSTANCE OF zcl_email_sender.
CAST zcl_email_sender( lo_notifier )->set_reply_to( 'noreply@corp.local' ).
ENDIF.
CASE TYPE OF lo_notifier.
WHEN TYPE zcl_email_sender INTO DATA(lo_mail). " 이미 캐스트 완료
lo_mail->set_reply_to( 'noreply@corp.local' ).
WHEN TYPE zcl_sms_sender INTO DATA(lo_sms).
lo_sms->set_sender_number( '15990000' ).
ENDCASE.
정리하면, 반드시 성공해야 하는 캐스트는 CAST를 TRY ... CATCH cx_sy_move_cast_error로 감싸고, 성공 여부로 흐름이 갈리면 IS INSTANCE OF나 CASE TYPE OF가 일반적으로 권장됩니다. ?=는 틀린 문법이 아니라 검사 없는 사용을 유도하는 형태라 새 코드에서 피하는 것입니다.
인터페이스 참조로 결합도 낮추기
현대화의 종착점은 설계입니다. 원칙은 "변수는 인터페이스로 잡고, 구현 클래스는 생성 지점 한 곳에만 등장시킨다"입니다.
INTERFACE zif_notifier PUBLIC.
METHODS send
IMPORTING iv_recipient TYPE string
iv_message TYPE string
RAISING zcx_notify_failed.
ENDINTERFACE.
구현 클래스를 아는 곳은 팩토리 하나로 좁힙니다. SWITCH와 NEW를 조합하면 한 문장으로 끝납니다(조건식 안 THROW는 7.50부터).
METHOD create. " RETURNING ro_notifier TYPE REF TO zif_notifier
ro_notifier = SWITCH #( iv_channel
WHEN 'MAIL' THEN NEW zcl_email_sender( iv_smtp_host = 'mail.corp.local' )
WHEN 'SMS' THEN NEW zcl_sms_sender( iv_provider = 'KT' )
ELSE THROW zcx_notify_failed( ) ).
ENDMETHOD.
이 구조의 진짜 보상은 테스트입니다. 호출부가 zif_notifier만 알기 때문에, zif_notifier를 구현해 호출 횟수만 기록하는 로컬 테스트 클래스 ltd_notifier_spy를 만들어 진짜 메일 서버 대신 주입할 수 있습니다.
METHOD setup. " 생성자 주입도 NEW 한 줄
mo_cut = NEW zcl_order_confirmation( io_notifier = NEW ltd_notifier_spy( ) ).
ENDMETHOD.
변수가 zcl_email_sender 타입이었다면 이 주입은 불가능합니다. 인터페이스 참조는 "나중에 채널이 늘 때"만이 아니라 "지금 테스트를 쓸 때" 이미 이득입니다.
직접 해보기 — 리팩터링 전후 비교
주문 확정 알림 코드를 통째로 비교합니다. 먼저 구식 버전.
" Before
DATA: lo_sender TYPE REF TO zcl_email_sender,
lo_notifier TYPE REF TO zif_notifier,
lo_mail TYPE REF TO zcl_email_sender.
CREATE OBJECT lo_sender
EXPORTING iv_smtp_host = 'mail.corp.local'.
lo_notifier = lo_sender.
lo_mail ?= lo_notifier. " 검사 없는 다운캐스트
lo_mail->set_reply_to( 'noreply@corp.local' ).
lo_notifier->send( iv_recipient = lv_customer
iv_message = lv_confirm_text ).
" After: 생성은 팩토리 안에, 호출부는 인터페이스만
DATA(lo_notifier) = zcl_notifier_factory=>create( 'MAIL' ).
IF lo_notifier IS INSTANCE OF zcl_email_sender.
CAST zcl_email_sender( lo_notifier )->set_reply_to( 'noreply@corp.local' ).
ENDIF.
TRY.
lo_notifier->send( iv_recipient = lv_customer
iv_message = lv_confirm_text ).
CATCH zcx_notify_failed INTO DATA(lx_fail).
zcl_notify_logger=>log_failure( lx_fail ). " 덤프 대신 로그
ENDTRY.
줄 수보다 구조 변화가 핵심입니다. Before는 호출부가 구현 클래스를 두 번(선언·캐스트) 알았고, 캐스트 실패와 발송 실패 모두 덤프로 이어질 수 있었습니다. After는 구현 클래스 이름이 팩토리와 타입 검사 블록에만 남고 실패는 예외 경로로 흡수됩니다. 내일 SMS 채널을 추가해도 호출부는 'MAIL' 값 하나 외에 바꿀 곳이 없습니다.
삽질 노트
- Q. CAST로 바꿨는데도 CX_SY_MOVE_CAST_ERROR 덤프가 납니다. —
CAST는?=보다 안전한 게 아니라 "표현식 버전"일 뿐입니다. 실패 가능성이 있으면 여전히TRY로 감싸거나 사전 타입 검사를 해야 합니다. - Q. NEW에서 문법 오류가 납니다. — 릴리스 확인이 먼저입니다.
NEW7.40 SP02,CAST7.40 SP08, 조건식 안THROW는 7.50부터입니다. 하위 릴리스로 전송될 개발이면 대상 시스템 기준으로 골라야 합니다. - Q. 인터페이스 타입으로 바꿨더니 클래스 고유 메서드가 안 보입니다. — 의도된 동작입니다. 필요한 지점에서만
CAST하되, 그런 지점이 늘어나면 인터페이스 설계 부족 신호입니다. 반대로 클래스 참조에서 인터페이스 메서드는zif_notifier~send처럼 접두어가 필요합니다(ALIASES로 단축 가능). - Q. 클래스 이름이 런타임에 정해지면요? — 동적 생성은
CREATE OBJECT ... TYPE (이름)이 여전히 정답입니다. 결과 변수를 공통 인터페이스 타입으로 두면 이후 코드는 동일하게 유지됩니다.
핵심 한 줄
객체는
NEW로 만들고, 내려갈 때는 검사와 함께CAST로 명시하고, 변수는 인터페이스로 잡아라 — 구현 클래스 이름이 등장하는 곳이 적을수록 좋은 코드다.
더 파볼 주제
VALUE·CONV·COND·SWITCH등 나머지 생성자 표현식 — 내부 테이블 채우기에도 같은 원칙이 적용됩니다.- ABAP Unit과
CL_ABAP_TESTDOUBLE— 스파이 패턴을 표준 테스트 더블 프레임워크로 확장 - Clean ABAP 스타일 문서의 "Prefer NEW to CREATE OBJECT" 항목
- ABAP 키워드 문서(7.58): NEW, CAST, CASE TYPE OF, 인터페이스
- RAP(ABAP RESTful Application Programming Model)의 Behavior 구현 — 테스트 격리에 인터페이스 주입 패턴이 그대로 이어집니다.
댓글 0
아직 댓글이 없습니다.