ENQUEUE·DEQUEUE 상세 - _SCOPE, 낙관적 잠금, 그리고 웹에서 잠금이 안 걸리는 이유

ENQUEUE DEQUEUE 상세 배너

SAP 잠금 객체란의 후속 글입니다. 개념은 앞 글에서 다뤘고, 여기서는 함수를 어떻게 부르고 언제 풀리는지를 파고듭니다.

ENQUEUE 함수의 파라미터

잠금 객체를 활성화하면 생기는 ENQUEUE_<잠금객체> 함수에는 키 필드 말고도 밑줄로 시작하는 제어 파라미터가 붙어 있습니다. 실무에서 문제가 되는 건 대부분 이쪽입니다.

파라미터역할
MODE_<테이블>잠금 모드 (S / E / X / O / R)
키 필드무엇을 잠글지. 비우면 조건에 맞는 전체가 잠긴다
X_<필드>해당 키 필드가 초기값일 때의 해석 방식을 바꾼다
_SCOPE잠금을 누가 소유하고 언제 풀지 (기본값 2)
_WAIT이미 잠겨 있으면 재시도할지
_COLLECT바로 걸지 않고 모아 두었다가 한꺼번에 처리

예외는 두 개입니다.

예외의미
FOREIGN_LOCK다른 소유자가 이미 잠그고 있다. sy-msgv1에 그 사용자 ID
SYSTEM_FAILUREEnqueue 서버 쪽 오류

_SCOPE - 가장 많이 틀리는 파라미터

_SCOPE는 잠금의 소유자와 해제 시점을 정합니다. 기본값이 2라는 점이 중요합니다.

값소유자언제 풀리나
1대화 프로그램DEQUEUE 호출 또는 프로그램 종료 시. 업데이트 태스크로 넘기지 않음
2 (기본)업데이트 태스크COMMIT WORK / ROLLBACK WORK로 SAP LUW가 끝날 때. 대화 프로그램이 풀 수 없음
3양쪽대화 프로그램과 업데이트 태스크 양쪽에서 풀어야 함
!자주 겪는 혼란DEQUEUE를 불렀는데 안 풀린다

기본값 _SCOPE = 2에서는 잠금 소유권이 업데이트 태스크로 넘어갑니다. 대화 프로그램에서 DEQUEUE를 불러도 풀리지 않고, COMMIT WORK 시점에 풀립니다.

✓직접 풀고 싶다면_SCOPE = 1로 건다

업데이트 태스크를 쓰지 않고 내가 걸고 내가 푸는 구조라면 _SCOPE = 1이 맞습니다. 조회 전용 화면이나 배치의 구간 잠금에 적합합니다.

INFO

DEQUEUE의 _SCOPE는 ENQUEUE에 쓴 값 이상이어야 합니다. 1로 걸고 2로 풀 수는 있지만, 2로 걸고 1로 풀려는 호출은 의도대로 동작하지 않습니다.

_WAIT - 기다릴 것인가 바로 실패할 것인가

1" 실패하면 즉시 리턴 (기본)
2CALL FUNCTION 'ENQUEUE_EZ_SORDER'
3  EXPORTING
4    mode_zsorder = 'E'
5    vbeln        = lv_vbeln
6  EXCEPTIONS
7    foreign_lock = 1  OTHERS = 2.
8
9" 잠깐 기다렸다가 재시도
10CALL FUNCTION 'ENQUEUE_EZ_SORDER'
11  EXPORTING
12    mode_zsorder = 'E'
13    vbeln        = lv_vbeln
14    _wait        = abap_true
15  EXCEPTIONS
16    foreign_lock = 1  OTHERS = 2.
온라인 화면기다리지 않는다

사용자를 세워 두면 안 됩니다. 바로 실패시키고 누가 쓰고 있는지 안내하는 편이 낫습니다.

배치·인터페이스짧게 기다려 볼 만하다

순간적으로 겹치는 경우가 많아 _WAIT가 효과적입니다. 다만 무한정 기다리지는 않으므로 실패 처리는 그대로 필요합니다.

낙관적 잠금 - O 모드와 R 모드

편집 화면을 오래 띄워 두는 경우를 위한 방식입니다. 흐름은 이렇습니다.

화면 열 때MODE = O서로 충돌하지 않음
→
저장할 때MODE = R배타 잠금으로 승격
→
해제DEQUEUE (E)승격 후에는 E로 푼다
1" 1. 화면을 열 때 - 낙관적 잠금
2CALL FUNCTION 'ENQUEUE_EZ_SORDER'
3  EXPORTING
4    mode_zsorder = 'O'
5    vbeln        = lv_vbeln
6  EXCEPTIONS
7    foreign_lock   = 1
8    system_failure = 2
9    OTHERS         = 3.
10
11" 2. 저장할 때 - 배타 잠금으로 승격
12CALL FUNCTION 'ENQUEUE_EZ_SORDER'
13  EXPORTING
14    mode_zsorder = 'R'
15    vbeln        = lv_vbeln
16  EXCEPTIONS
17    foreign_lock   = 1
18    system_failure = 2
19    OTHERS         = 3.
20
21IF sy-subrc <> 0.
22  " 승격 실패 = 그 사이 누군가 먼저 가져갔다
23  MESSAGE e002(zorder) WITH lv_vbeln.
24ENDIF.
25
26" 3. 해제
27CALL FUNCTION 'DEQUEUE_EZ_SORDER'
28  EXPORTING
29    mode_zsorder = 'E'
30    vbeln        = lv_vbeln.

승격은 내 O 잠금이 아직 살아 있고, 다른 잠금과 충돌하지 않을 때만 성공합니다. 승격에 성공하면 같은 데이터를 보던 다른 사람들의 O 잠금은 사라집니다.

WARNING

낙관적 잠금은 "저장할 때 실패할 수 있다"를 전제로 한 설계입니다. 승격 실패 시 사용자에게 어떻게 안내하고 입력값을 어떻게 살려 줄지까지 만들어야 쓸 만해집니다. 그 처리가 없으면 그냥 안 쓰는 것만 못합니다.

여기까지가 SAP GUI 기준입니다

지금까지의 설명은 전제가 하나 있습니다. 잠금을 건 ABAP 세션이 계속 살아 있다는 것입니다. SAP GUI에서는 사용자가 화면을 띄워 둔 동안 세션이 유지되므로 자연스럽게 성립합니다.

웹에서 호출하면 이 전제가 깨집니다.

웹에서 잠금이 유지되지 않는 이유

UI5 애플리케이션(U4A 같은 개발 솔루션으로 만든 화면 포함)이나 OData 서비스에서 ABAP을 호출하면, 호출 하나가 끝날 때마다 ABAP 세션도 끝납니다. 잠금의 소유자는 ABAP 세션이므로, 세션이 끝나면 잠금도 함께 사라집니다.

!Stateless요청마다 세션이 새로 생긴다

1번 호출에서 ENQUEUE를 걸어도 응답이 끝나면 풀립니다. 2번 호출은 남의 잠금은커녕 자기 잠금도 못 봅니다. "ENQUEUE가 안 걸린다"고 느끼는 상황이 이것입니다.

✓Stateful세션이 이어진다

같은 ABAP 세션이 유지되므로 SAP GUI와 동일하게 잠금이 남아 있습니다. 1번 호출에서 건 잠금을 2번 호출에서 그대로 들고 있습니다.

sap-contextid로 구분한다

둘을 구분하는 단서가 sap-contextid 쿠키입니다. ICF가 stateful 세션을 만들 때 이 쿠키를 내려보내고, 브라우저가 다음 요청에 이 값을 같이 보내면 서버가 이전 세션에 다시 연결해 줍니다.

1브라우저 F12 → Application → Cookies → 해당 도메인
2  sap-contextid 가 있다  → stateful (세션 유지, 잠금 유지됨)
3  sap-contextid 가 없다  → stateless (요청마다 끝남, 잠금 사라짐)
TIP

쿠키만 보고 끝내지 말고 SM12로 교차 확인하세요. 1번 호출에서 잠금을 건 뒤 SM12를 열어 잠금이 남아 있는지 확인하면 확실합니다. 바로 사라지면 stateless입니다.

그럼 어떻게 처리해야 하나

환경에 따라 신경 써야 할 지점이 정반대입니다. 두 경우로 나눠 보겠습니다.

케이스 1 · stateless - 잠금이 안 남는데 DEQUEUE를 불러야 하나

요청이 끝나면 세션이 끝나고, 그 세션이 갖고 있던 잠금도 함께 사라집니다. 다음 호출까지 잠금을 끌고 갈 방법이 없습니다.

그러면 "어차피 사라지는데 DEQUEUE를 부를 필요가 있나"라는 질문이 나옵니다.

!위험한 생각세션이 끝나니 안 풀어도 된다

대부분은 맞지만, 세션이 재사용되는 구성(RFC 연결 재사용, 나중에 stateful로 전환 등)에서는 잠금이 남아 다음 호출을 막습니다. 환경 설정 하나에 동작이 뒤집히는 전제에 기대는 셈입니다.

✓안전한 습관건 쪽에서 책임지고 푼다

정상 경로는 COMMIT WORK가 처리하고, 중간에 빠져나가는 모든 경로에서 명시적으로 풀어 둡니다. 환경이 바뀌어도 그대로 동작합니다.

정리하면 이렇습니다.

빠져나가는 경로해야 할 일
정상 저장COMMIT WORK — _SCOPE = 2 잠금은 여기서 풀린다. 별도 DEQUEUE 불필요
검증 실패·예외로 중단ROLLBACK WORK 또는 명시적 DEQUEUE. 이쪽을 빠뜨리기 쉽다
_SCOPE = 1로 걸었을 때반드시 DEQUEUE. COMMIT WORK가 풀어 주지 않는다

그래서 저장 처리는 잠금 → 재조회 → 검증 → 변경 → 커밋을 한 호출 안에서 끝내고, 어느 지점에서 빠져나가도 잠금이 풀리도록 짭니다.

1METHOD save_order.
2  CALL FUNCTION 'ENQUEUE_EZ_SORDER'
3    EXPORTING mode_zsorder = 'E'
4              vbeln        = iv_vbeln
5    EXCEPTIONS foreign_lock = 1 OTHERS = 2.
6  IF sy-subrc <> 0.
7    RAISE EXCEPTION TYPE zcx_order_locked.
8  ENDIF.
9
10  TRY.
11      " 잠근 뒤 다시 읽어, 화면이 값을 가져간 뒤 바뀌었는지 확인
12      SELECT SINGLE changed_at FROM zsorder
13        WHERE vbeln = @iv_vbeln INTO @DATA(lv_db_changed_at).
14
15      IF lv_db_changed_at <> iv_changed_at.
16        " 다른 사람이 먼저 저장했다
17        RAISE EXCEPTION TYPE zcx_order_outdated.
18      ENDIF.
19
20      UPDATE zsorder SET ... WHERE vbeln = @iv_vbeln.
21      COMMIT WORK.              " 여기서 잠금도 함께 풀린다
22
23    CATCH zcx_order_outdated.
24      " 커밋에 도달하지 못한 경로 - 직접 풀어 준다
25      CALL FUNCTION 'DEQUEUE_EZ_SORDER'
26        EXPORTING mode_zsorder = 'E'
27                  vbeln        = iv_vbeln.
28      RAISE EXCEPTION TYPE zcx_order_outdated.
29  ENDTRY.
30ENDMETHOD.

핵심은 변경 시각 같은 값을 화면에 함께 내려보냈다가 저장할 때 비교하는 것입니다. 잠금이 짧아도 덮어쓰기를 막을 수 있습니다. OData에서는 ETag가 같은 역할을 합니다.

"다른 사람이 편집 중"을 화면에 꼭 보여줘야 한다면, 여기에 커스텀 잠금 테이블을 더합니다. 키·사용자 ID·잠근 시각을 기록하고 편집 시작 시 넣었다가 저장·취소 시 지우는 방식입니다.

WARNING

커스텀 테이블을 쓴다면 만료 시간을 반드시 두세요. 브라우저를 그냥 닫으면 해제 요청이 오지 않아 유령 잠금이 쌓입니다. 그리고 이 테이블만으로는 동시 저장을 막지 못하므로, 저장 순간의 ENQUEUE는 그대로 유지해야 합니다.

케이스 2 · stateful - 같은 세션에서 ENQUEUE가 두 번 불린다면

stateful로 두면 잠금은 유지됩니다. 대신 다른 문제가 생깁니다. 먼저 전제를 하나 바로잡아야 합니다.

INFO

잠금의 소유자는 사용자가 아니라 세션(SAP LUW)입니다. 같은 사용자라도 세션이 다르면 남으로 취급되어 FOREIGN_LOCK이 납니다. 반대로 같은 세션 안에서는 내가 건 잠금이므로 판정이 완전히 달라집니다.

그래서 같은 세션에서 같은 잠금을 두 번 거는 상황(예: 사용자가 탭을 두 개 열어 같은 주문을 편집)은 모드에 따라 결과가 갈립니다.

!E 모드두 번째도 성공한다 (누적)

같은 소유자가 다시 걸면 누적 카운터가 1 올라갈 뿐 막히지 않습니다. 같은 사용자가 두 화면에서 동시에 편집해도 아무 경고가 없습니다.

✓X 모드두 번째는 실패한다

X는 누적되지 않습니다. 같은 소유자라도 두 번째 호출이 FOREIGN_LOCK으로 떨어져 중복 진입을 막을 수 있습니다.

여기서 또 하나 놓치기 쉬운 것이 해제 횟수입니다.

WARNING

누적된 잠금은 DEQUEUE 한 번으로 풀리지 않습니다. 카운터가 1 줄어들 뿐이고, 0이 되어야 실제로 해제됩니다. 두 번 걸고 한 번만 풀면 잠금이 그대로 남아, 그 세션이 끝날 때까지 다른 사용자가 들어오지 못합니다.

대응은 세 가지입니다.

상황대응
중복 진입 자체를 막고 싶다X 모드로 건다. 두 번째 호출이 실패한다
E 모드를 유지해야 한다ENQUEUE 횟수와 DEQUEUE 횟수를 짝 맞춘다
걸기 전에 확인하고 싶다ENQUEUE_READ로 현재 잠금 상태를 조회한 뒤 분기한다
1" 걸기 전에 현재 잠금 상태를 확인한다
2DATA lt_enq TYPE STANDARD TABLE OF seqg3.
3
4CALL FUNCTION 'ENQUEUE_READ'
5  EXPORTING
6    gclient = sy-mandt
7    gname   = 'ZSORDER'
8    garg    = lv_garg        " 잠금 인수 (SM12에 보이는 값과 같은 형식)
9  TABLES
10    enq     = lt_enq.
11
12IF lt_enq IS NOT INITIAL.
13  READ TABLE lt_enq INTO DATA(ls_enq) INDEX 1.
14  " ls_enq-guname 으로 누가 잡고 있는지 확인할 수 있다
15  " 내 세션이 이미 잡은 것인지, 남이 잡은 것인지 구분해 분기
16ENDIF.
17
18" 중복 진입을 막아야 한다면 X 모드
19CALL FUNCTION 'ENQUEUE_EZ_SORDER'
20  EXPORTING
21    mode_zsorder = 'X'
22    vbeln        = lv_vbeln
23  EXCEPTIONS
24    foreign_lock   = 1
25    system_failure = 2
26    OTHERS         = 3.
27
28IF sy-subrc <> 0.
29  MESSAGE e003(zorder) WITH lv_vbeln.   " 이미 편집 중입니다
30ENDIF.
TIP

화면에 들어올 때마다 무조건 ENQUEUE를 부르는 구조라면 누적이 계속 쌓입니다. 이미 잠갔는지를 세션 안에서 관리하고, 처음 진입할 때만 걸도록 하는 편이 안전합니다.

그리고 stateful 자체의 비용도 함께 고려해야 합니다.

WARNING

stateful 세션은 사용자마다 작업 프로세스와 메모리를 계속 붙들고 있습니다. 동시 사용자가 많으면 시스템 자원이 빠르게 소모되고, 세션 종료 처리가 빠지면 잠금과 세션이 함께 남습니다. 꼭 필요한 화면에만 제한적으로 쓰세요.

설계 선택 비교

저장 시점에만 잠금커스텀 잠금 테이블stateful 유지
적용 환경statelessstatelessstateful
편집 중 "사용 중" 표시불가가능가능
동시 저장 방지가능단독으로는 불가가능
유령 잠금 위험없음있음 (만료 처리 필요)있음 (누적·세션 정리 필요)
자원 부담낮음낮음높음
구현 난이도낮음중간중간

대부분의 웹 화면은 저장 시점에만 잠그는 방식으로 충분합니다. 편집 중 표시가 업무상 꼭 필요할 때만 커스텀 테이블을 더하고, stateful은 마지막 선택지로 두는 편이 안전합니다.

정리

  • _SCOPE 기본값은 2입니다. 이 경우 DEQUEUE가 아니라 COMMIT WORK가 잠금을 풉니다.
  • 직접 걸고 직접 풀려면 _SCOPE = 1 로 거세요.
  • 온라인은 _WAIT 없이 즉시 실패, 배치는 짧게 대기가 무난합니다.
  • 낙관적 잠금은 O로 걸고 R로 승격하며, 승격 실패 처리까지 만들어야 완성됩니다.
  • 웹에서 잠금이 안 남는 것은 버그가 아니라 stateless 구조의 정상 동작입니다. sap-contextid 쿠키와 SM12로 확인할 수 있습니다.
  • 웹에서는 저장 호출 안에서 잠그고 변경 시각을 비교하는 방식을 기본으로 삼으세요.
INFO

_SCOPE 기본값, ICF 세션 설정, 쿠키 동작은 릴리스와 서비스 설정, 그리고 사용하는 UI 솔루션의 구성에 따라 다를 수 있습니다. 실제 시스템에서 SM12와 브라우저 개발자 도구로 직접 확인하신 뒤 적용하세요.

Disclaimer — 이 포스트는 AI(Claude)를 활용하여 작성된 초안을 바탕으로 검수 및 보완하여 작성되었습니다. 내용 중 오류나 오타가 있다면 댓글로 알려주시면 감사하겠습니다.