SAP 잠금 객체란의 후속 글입니다. 개념은 앞 글에서 다뤘고, 여기서는 함수를 어떻게 부르고 언제 풀리는지를 파고듭니다.
잠금 객체를 활성화하면 생기는 ENQUEUE_<잠금객체> 함수에는 키 필드 말고도 밑줄로 시작하는 제어 파라미터가 붙어 있습니다. 실무에서 문제가 되는 건 대부분 이쪽입니다.
| 파라미터 | 역할 |
|---|---|
MODE_<테이블> | 잠금 모드 (S / E / X / O / R) |
| 키 필드 | 무엇을 잠글지. 비우면 조건에 맞는 전체가 잠긴다 |
X_<필드> | 해당 키 필드가 초기값일 때의 해석 방식을 바꾼다 |
_SCOPE | 잠금을 누가 소유하고 언제 풀지 (기본값 2) |
_WAIT | 이미 잠겨 있으면 재시도할지 |
_COLLECT | 바로 걸지 않고 모아 두었다가 한꺼번에 처리 |
예외는 두 개입니다.
| 예외 | 의미 |
|---|---|
FOREIGN_LOCK | 다른 소유자가 이미 잠그고 있다. sy-msgv1에 그 사용자 ID |
SYSTEM_FAILURE | Enqueue 서버 쪽 오류 |
_SCOPE는 잠금의 소유자와 해제 시점을 정합니다. 기본값이 2라는 점이 중요합니다.
| 값 | 소유자 | 언제 풀리나 |
|---|---|---|
| 1 | 대화 프로그램 | DEQUEUE 호출 또는 프로그램 종료 시. 업데이트 태스크로 넘기지 않음 |
| 2 (기본) | 업데이트 태스크 | COMMIT WORK / ROLLBACK WORK로 SAP LUW가 끝날 때. 대화 프로그램이 풀 수 없음 |
| 3 | 양쪽 | 대화 프로그램과 업데이트 태스크 양쪽에서 풀어야 함 |
기본값 _SCOPE = 2에서는 잠금 소유권이 업데이트 태스크로 넘어갑니다. 대화 프로그램에서 DEQUEUE를 불러도 풀리지 않고, COMMIT WORK 시점에 풀립니다.
업데이트 태스크를 쓰지 않고 내가 걸고 내가 푸는 구조라면 _SCOPE = 1이 맞습니다. 조회 전용 화면이나 배치의 구간 잠금에 적합합니다.
DEQUEUE의 _SCOPE는 ENQUEUE에 쓴 값 이상이어야 합니다. 1로 걸고 2로 풀 수는 있지만, 2로 걸고 1로 풀려는 호출은 의도대로 동작하지 않습니다.
사용자를 세워 두면 안 됩니다. 바로 실패시키고 누가 쓰고 있는지 안내하는 편이 낫습니다.
순간적으로 겹치는 경우가 많아 _WAIT가 효과적입니다. 다만 무한정 기다리지는 않으므로 실패 처리는 그대로 필요합니다.
편집 화면을 오래 띄워 두는 경우를 위한 방식입니다. 흐름은 이렇습니다.
승격은 내 O 잠금이 아직 살아 있고, 다른 잠금과 충돌하지 않을 때만 성공합니다. 승격에 성공하면 같은 데이터를 보던 다른 사람들의 O 잠금은 사라집니다.
낙관적 잠금은 "저장할 때 실패할 수 있다"를 전제로 한 설계입니다. 승격 실패 시 사용자에게 어떻게 안내하고 입력값을 어떻게 살려 줄지까지 만들어야 쓸 만해집니다. 그 처리가 없으면 그냥 안 쓰는 것만 못합니다.
지금까지의 설명은 전제가 하나 있습니다. 잠금을 건 ABAP 세션이 계속 살아 있다는 것입니다. SAP GUI에서는 사용자가 화면을 띄워 둔 동안 세션이 유지되므로 자연스럽게 성립합니다.
웹에서 호출하면 이 전제가 깨집니다.
UI5 애플리케이션(U4A 같은 개발 솔루션으로 만든 화면 포함)이나 OData 서비스에서 ABAP을 호출하면, 호출 하나가 끝날 때마다 ABAP 세션도 끝납니다. 잠금의 소유자는 ABAP 세션이므로, 세션이 끝나면 잠금도 함께 사라집니다.
1번 호출에서 ENQUEUE를 걸어도 응답이 끝나면 풀립니다. 2번 호출은 남의 잠금은커녕 자기 잠금도 못 봅니다. "ENQUEUE가 안 걸린다"고 느끼는 상황이 이것입니다.
같은 ABAP 세션이 유지되므로 SAP GUI와 동일하게 잠금이 남아 있습니다. 1번 호출에서 건 잠금을 2번 호출에서 그대로 들고 있습니다.
둘을 구분하는 단서가 sap-contextid 쿠키입니다. ICF가 stateful 세션을 만들 때 이 쿠키를 내려보내고, 브라우저가 다음 요청에 이 값을 같이 보내면 서버가 이전 세션에 다시 연결해 줍니다.
쿠키만 보고 끝내지 말고 SM12로 교차 확인하세요. 1번 호출에서 잠금을 건 뒤 SM12를 열어 잠금이 남아 있는지 확인하면 확실합니다. 바로 사라지면 stateless입니다.
환경에 따라 신경 써야 할 지점이 정반대입니다. 두 경우로 나눠 보겠습니다.
요청이 끝나면 세션이 끝나고, 그 세션이 갖고 있던 잠금도 함께 사라집니다. 다음 호출까지 잠금을 끌고 갈 방법이 없습니다.
그러면 "어차피 사라지는데 DEQUEUE를 부를 필요가 있나"라는 질문이 나옵니다.
대부분은 맞지만, 세션이 재사용되는 구성(RFC 연결 재사용, 나중에 stateful로 전환 등)에서는 잠금이 남아 다음 호출을 막습니다. 환경 설정 하나에 동작이 뒤집히는 전제에 기대는 셈입니다.
정상 경로는 COMMIT WORK가 처리하고, 중간에 빠져나가는 모든 경로에서 명시적으로 풀어 둡니다. 환경이 바뀌어도 그대로 동작합니다.
정리하면 이렇습니다.
| 빠져나가는 경로 | 해야 할 일 |
|---|---|
| 정상 저장 | COMMIT WORK — _SCOPE = 2 잠금은 여기서 풀린다. 별도 DEQUEUE 불필요 |
| 검증 실패·예외로 중단 | ROLLBACK WORK 또는 명시적 DEQUEUE. 이쪽을 빠뜨리기 쉽다 |
_SCOPE = 1로 걸었을 때 | 반드시 DEQUEUE. COMMIT WORK가 풀어 주지 않는다 |
그래서 저장 처리는 잠금 → 재조회 → 검증 → 변경 → 커밋을 한 호출 안에서 끝내고, 어느 지점에서 빠져나가도 잠금이 풀리도록 짭니다.
핵심은 변경 시각 같은 값을 화면에 함께 내려보냈다가 저장할 때 비교하는 것입니다. 잠금이 짧아도 덮어쓰기를 막을 수 있습니다. OData에서는 ETag가 같은 역할을 합니다.
"다른 사람이 편집 중"을 화면에 꼭 보여줘야 한다면, 여기에 커스텀 잠금 테이블을 더합니다. 키·사용자 ID·잠근 시각을 기록하고 편집 시작 시 넣었다가 저장·취소 시 지우는 방식입니다.
커스텀 테이블을 쓴다면 만료 시간을 반드시 두세요. 브라우저를 그냥 닫으면 해제 요청이 오지 않아 유령 잠금이 쌓입니다. 그리고 이 테이블만으로는 동시 저장을 막지 못하므로, 저장 순간의 ENQUEUE는 그대로 유지해야 합니다.
stateful로 두면 잠금은 유지됩니다. 대신 다른 문제가 생깁니다. 먼저 전제를 하나 바로잡아야 합니다.
잠금의 소유자는 사용자가 아니라 세션(SAP LUW)입니다. 같은 사용자라도 세션이 다르면 남으로 취급되어 FOREIGN_LOCK이 납니다. 반대로 같은 세션 안에서는 내가 건 잠금이므로 판정이 완전히 달라집니다.
그래서 같은 세션에서 같은 잠금을 두 번 거는 상황(예: 사용자가 탭을 두 개 열어 같은 주문을 편집)은 모드에 따라 결과가 갈립니다.
같은 소유자가 다시 걸면 누적 카운터가 1 올라갈 뿐 막히지 않습니다. 같은 사용자가 두 화면에서 동시에 편집해도 아무 경고가 없습니다.
X는 누적되지 않습니다. 같은 소유자라도 두 번째 호출이 FOREIGN_LOCK으로 떨어져 중복 진입을 막을 수 있습니다.
여기서 또 하나 놓치기 쉬운 것이 해제 횟수입니다.
누적된 잠금은 DEQUEUE 한 번으로 풀리지 않습니다. 카운터가 1 줄어들 뿐이고, 0이 되어야 실제로 해제됩니다. 두 번 걸고 한 번만 풀면 잠금이 그대로 남아, 그 세션이 끝날 때까지 다른 사용자가 들어오지 못합니다.
대응은 세 가지입니다.
| 상황 | 대응 |
|---|---|
| 중복 진입 자체를 막고 싶다 | X 모드로 건다. 두 번째 호출이 실패한다 |
| E 모드를 유지해야 한다 | ENQUEUE 횟수와 DEQUEUE 횟수를 짝 맞춘다 |
| 걸기 전에 확인하고 싶다 | ENQUEUE_READ로 현재 잠금 상태를 조회한 뒤 분기한다 |
화면에 들어올 때마다 무조건 ENQUEUE를 부르는 구조라면 누적이 계속 쌓입니다. 이미 잠갔는지를 세션 안에서 관리하고, 처음 진입할 때만 걸도록 하는 편이 안전합니다.
그리고 stateful 자체의 비용도 함께 고려해야 합니다.
stateful 세션은 사용자마다 작업 프로세스와 메모리를 계속 붙들고 있습니다. 동시 사용자가 많으면 시스템 자원이 빠르게 소모되고, 세션 종료 처리가 빠지면 잠금과 세션이 함께 남습니다. 꼭 필요한 화면에만 제한적으로 쓰세요.
| 저장 시점에만 잠금 | 커스텀 잠금 테이블 | stateful 유지 | |
|---|---|---|---|
| 적용 환경 | stateless | stateless | stateful |
| 편집 중 "사용 중" 표시 | 불가 | 가능 | 가능 |
| 동시 저장 방지 | 가능 | 단독으로는 불가 | 가능 |
| 유령 잠금 위험 | 없음 | 있음 (만료 처리 필요) | 있음 (누적·세션 정리 필요) |
| 자원 부담 | 낮음 | 낮음 | 높음 |
| 구현 난이도 | 낮음 | 중간 | 중간 |
대부분의 웹 화면은 저장 시점에만 잠그는 방식으로 충분합니다. 편집 중 표시가 업무상 꼭 필요할 때만 커스텀 테이블을 더하고, stateful은 마지막 선택지로 두는 편이 안전합니다.
_SCOPE 기본값은 2입니다. 이 경우 DEQUEUE가 아니라 COMMIT WORK가 잠금을 풉니다._SCOPE = 1 로 거세요._WAIT 없이 즉시 실패, 배치는 짧게 대기가 무난합니다.sap-contextid 쿠키와 SM12로 확인할 수 있습니다.
_SCOPE 기본값, ICF 세션 설정, 쿠키 동작은 릴리스와 서비스 설정, 그리고 사용하는 UI 솔루션의 구성에 따라 다를 수 있습니다. 실제 시스템에서 SM12와 브라우저 개발자 도구로 직접 확인하신 뒤 적용하세요.
Disclaimer — 이 포스트는 AI(Claude)를 활용하여 작성된 초안을 바탕으로 검수 및 보완하여 작성되었습니다. 내용 중 오류나 오타가 있다면 댓글로 알려주시면 감사하겠습니다.