완성된 통합 문서를 동료에게 넘기면서 "이 파일은 필터만 걸고 다시 쓰지는 마세요"라고 말한다고 하자. 그래서 시트 보호를 건다. 예전 HotXLS 빌드에서 이 동작은 파일 안에 매번 똑같은 한 줄만 기록했다. <sheetProtection sheet="1" objects="1" scenarios="1"/> 이다. 시트는 잠기고, 비밀번호 해시도 붙지만, 사용자는 당신이 열어 두고 싶었던 정렬과 필터조차 할 수 없게 된다. Excel의 Protect Sheet 대화상자에 체크박스가 열다섯 개나 있는 이유가 바로 여기에 있는데, 당시 엔진은 그 어떤 것도 표현하지 못했다. v2.91.0의 보호 모델이 메우는 공백이 이것이다
HotXLS는 Excel 설치 없이도 XLS와 XLSX를 읽고 쓰는 Delphi와 C++Builder용 네이티브 VCL spreadsheet 컴포넌트다. 이 글은 워크시트 보호의 XLSX 측면만 다룬다. 새로 추가된 TXLSXSheetProtectionOption enum, 각 권한을 토글하는 AllowOption property, 그리고 손으로 <sheetProtection> element를 쓸 때 대부분의 사람이 한 번씩 넘어지는 OOXML 인코딩 규칙 하나가 주제다
워크시트 보호가 실제로 막는 것
먼저 경계를 분명히 해야 한다. 이 기능을 얼마나 신뢰해도 되는지가 여기서 결정되기 때문이다. OOXML 스프레드시트 형식(ECMA-376)의 워크시트 보호는 암호화가 아니라 상호작용 정책이다. 시트가 보호된 동안, 어떤 편집을 거부해야 하는지 conforming application에게 알려 주는 규칙일 뿐이다. 셀 값은 여전히 xl/worksheets/sheetN.xml 안에 평문으로 들어 있다. .xlsx를 압축 해제하면 그대로 보인다. 선택적 비밀번호도 데이터를 뒤섞는 키가 아니라 짧은 구식 해시로 저장될 뿐이다. 파일 이름을 바꾸고 해당 part를 열어 <sheetProtection> 줄만 지워 버릴 수 있는 사람은 내용을 그대로 읽고 수정할 수 있다
즉 이 보호가 답하는 질문은 "동료가 실수로 수식을 덮어쓰지 못하게 해 달라"이지, "의도적인 사람이 이 데이터를 읽지 못하게 해 달라"가 아니다. 두 문제는 도구도 다르다. 기밀성이 필요하다면 AES로 보호된 XLSX 출력에서 다루는 통합 문서 수준 암호화가 필요하다. 그것만이 실제로 패키지를 암호화한다. 시트 보호와 통합 문서 암호화는 함께 사용할 수 있지만, 자물쇠 역할을 하는 것은 후자뿐이다. 이 선만 분명히 그어 두면, 나머지는 모두 배선 작업일 뿐이다
15개의 옵션과 AllowOption property
이제 각 워크시트는 보호된 상태에서도 사용자가 여전히 수행할 수 있는 동작을 TXLSXSheetProtectionOption 값 집합으로 가진다. 각 멤버는 OOXML attribute 및 Excel 대화상자의 체크박스와 1대1로 대응한다
xlsxSpoEditObjects,xlsxSpoEditScenarios는 drawing object와 what-if scenario 편집 권한을 뜻한다xlsxSpoFormatCells,xlsxSpoFormatColumns,xlsxSpoFormatRows는 셀, 열, 행 서식 변경 권한이다xlsxSpoInsertColumns,xlsxSpoInsertRows,xlsxSpoInsertHyperlinks는 열, 행, 링크 삽입 권한이다xlsxSpoDeleteColumns,xlsxSpoDeleteRows는 열과 행 삭제 권한이다xlsxSpoSelectLockedCells,xlsxSpoSelectUnlockedCells는 잠긴 셀 또는 잠기지 않은 셀로 선택 이동을 허용한다xlsxSpoSort,xlsxSpoAutoFilter,xlsxSpoPivotTables는 정렬, AutoFilter 드롭다운 사용, PivotTable 작업 권한이다
개별 비트는 TXLSXWorksheet의 indexed AllowOption property를 통해 읽고 쓴다. AllowOption[Opt] = True 는 그 동작이 허용된다는 뜻이고, False 로 두면 금지된다. 전체 집합은 SheetProtectionOptions를 통해 한 번에 접근할 수도 있다. 이는 평범한 Pascal의 set of 인 TXLSXSheetProtectionOptions 이므로, 저장해 뒀다가 복원하거나 통째로 교체할 수 있다
기본값은 중요하고, 의도적이다. 새로 만든 워크시트는 처음부터 모든 옵션이 허용된 상태로 시작한다. 생성자는 SheetProtectionOptions를 [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)] 전체 범위로 채운다. 즉 아무것도 없는 상태에서 권한을 하나씩 올리는 것이 아니라, 처음에는 다 허용하고 거기서 금지할 동작을 빼 가는 모델이다. 이 선택 덕분에 아래에서 볼 writer의 인코딩 규칙이 Excel의 동작과 정확히 맞아떨어진다
정렬과 필터는 열어 둔 채 시트를 보호하기
가장 흔한 사례를 끝까지 보자. 완성된 보고서를 보호해 레이아웃은 건드리지 못하게 하되, 읽는 사람은 정렬과 필터는 쓸 수 있게 남겨 둔다. 여기서 기억할 점은 Protect와 옵션 집합이 서로 독립적이라는 사실이다. Protect는 시트를 보호 상태로 전환하고 선택적 비밀번호 해시를 저장할 뿐, 옵션 집합 자체는 건드리지 않는다. AllowOption은 별도로 조정해야 하며, 그 토글은 시트가 보호된 상태로 저장됐을 때 효력을 가진다
var
wb: TXLSXWorkbook;
sh: TXLSXWorksheet;
begin
wb := TXLSXWorkbook.Create;
try
sh := wb.Sheets.Add('Protected');
sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
sh.Cells[2, 1].Value := 'North'; sh.Cells[2, 2].Value := 120;
sh.Cells[3, 1].Value := 'South'; sh.Cells[3, 2].Value := 98;
// Protect with a password. This only sets the protected state + hash;
// the option set is left at its all-permitted default.
sh.Protect('HotXLS-2026');
// Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
sh.AllowOption[xlsxSpoSort] := True;
sh.AllowOption[xlsxSpoAutoFilter] := True;
sh.AllowOption[xlsxSpoFormatCells] := False;
sh.AllowOption[xlsxSpoFormatColumns] := False;
sh.AllowOption[xlsxSpoFormatRows] := False;
sh.AllowOption[xlsxSpoInsertRows] := False;
sh.AllowOption[xlsxSpoDeleteRows] := False;
if wb.SaveAs('protection.xlsx') <> 1 then
Writeln('SaveAs failed');
finally
wb.Free;
end;
end;
이 코드에서 두 가지를 읽어낼 수 있다. Sort와 AutoFilter 줄은 둘 다 기본값이 True 임에도 명시적으로 써 두었다. 이것은 기능 요구가 아니라 다음 유지보수자를 위한 문서다. 또한 기본값이 permissive하기 때문에 실제 출력 파일을 바꾸는 줄은 False 를 설정하는 쪽뿐이다. 이것은 이 API만의 우연한 특징이 아니라, OOXML wire format이 그대로 드러난 결과다. 다음 절이 바로 그 이야기다
인코딩 규칙: 생략은 허용, attr=0은 금지
이 기능 전체에서 가장 직관을 거스르는 사실은 이것 하나이고, 손으로 <sheetProtection>를 쓸 때 대부분이 여기서 틀린다. OOXML에서 각 동작별 attribute는 금지 플래그이며, attribute가 없다는 것은 허용을 뜻한다. attribute가 빠져 있으면 그 동작은 허용된다. 반대로 "0" 으로 기록되면 시트 보호 중에 그 동작이 금지된다. 잘 만들어진 파일에서 "서식 변경 허용"을 뜻하는 formatCells="1" 같은 것은 필요 없다. 그냥 attribute를 쓰지 않으면 된다. (생략된 attribute의 기본값은 OOXML boolean의 기본인 true로 해석되고, 이 attribute들은 그 true가 해당 편집을 허용한다는 식으로 이름 붙어 있다)
HotXLS writer는 이 규칙을 그대로 따른다. 먼저 보호를 켜기 위해 sheet="1" 을 쓰고, 그 다음 옵션 집합을 순회하면서 사용자가 False 로 둔 항목에 대해서만 attr="0" 을 기록한다. 허용된 동작은 출력 파일에 아무 흔적도 남기지 않는다. 그래서 앞 절의 예제는 저장되면, 비밀번호 해시와 함께 금지된 동작들만 들어 있는 대략 이런 형태가 된다
// Conceptual output for the snippet above (attributes elided for brevity):
// <sheetProtection sheet="1"
// formatCells="0" formatColumns="0" formatRows="0"
// insertRows="0" deleteRows="0"
// password="...4-hex..."/>
// Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
// Their absence is exactly what tells Excel those actions stay allowed.
예전처럼 모든 attribute가 다 적혀 있어야 할 것이라고 기대하고 보면 이 출력은 듬성듬성하고, 심지어 틀린 것처럼 보일 수 있다. 하지만 이것이 맞다. sort="1" 과 autoFilter="1" 을 적은 파일도 conforming reader에게는 같은 의미가 될 수 있지만, Excel 자체가 금지 항목만 기록하는 최소 형태를 쓰며, 그와 맞추는 편이 diff를 작게 만들고 round-trip을 지루할 정도로 평범하게 만든다. objects 와 scenarios attribute도 완전히 같은 규칙을 따른다. 기본적으로 허용이므로, 금지할 때만 "0" 으로 나타난다. 예전의 무조건적인 objects="1" scenarios="1" 과는 정반대다
보호 정보 다시 읽기: round-trip 충실도
쓸 수만 있고 읽을 수는 없는 권한 모델은 일방통행 문이다. 이런 모델이 낳는 흔한 증상은 load-edit-save 한 번만 돌려도 조용히 권한이 넓어지는 것이다. HotXLS는 그 문제를 막는다. ParseWorksheetXml 이 <sheetProtection> element를 만나면 시트를 보호 상태로 표시하고, 비밀번호 해시가 있으면 저장하며, 각 동작별 attribute도 같은 규칙을 뒤집어서 AllowOption 으로 되돌린다. 즉 attribute가 존재하고 값이 "0" 이면 그 동작은 금지되고, attribute가 없으면 기본 허용 상태를 그대로 유지한다
var
wb: TXLSXWorkbook;
sh: TXLSXWorksheet;
begin
wb := TXLSXWorkbook.Create;
try
wb.LoadFromFile('protection.xlsx');
sh := wb.Sheets[1]; // XLSX sheets are 1-based
if sh.IsProtected then
begin
Writeln('Protected; password hash present: ',
sh.SheetProtectHash <> '');
Writeln('Sort allowed: ', sh.AllowOption[xlsxSpoSort]);
Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
end;
finally
wb.Free;
end;
end;
writer가 만든 파일을 다시 로드하면 Sort 와 AutoFilter 는 다시 True, FormatCells 는 False 로 돌아온다. 즉 저장했던 집합이 온전히 보존된다. 이 대칭성이 바로 핵심이다. 보호된 시트에서 셀 하나만 바꾸고 저장해도, 건드리지 않은 나머지 열네 개 권한이 예전의 all-or-nothing 기본값으로 무너져 내리지 않고 그대로 살아남는다
실전 메모와 한계
이 기능을 보고서 파이프라인에 넣기 전에 알아 둘 만한 점이 몇 가지 있다
- 비밀번호는 설계상 약하다. XLSX 워크시트 보호는 수십 년 동안 Excel이 써 온 것과 같은 16비트 legacy hash를 사용한다. 이것은 실수 방지용이지, 공격자를 막는 용도가 아니다. 비밀을 지키는 도구로 취급하면 안 된다. 실제 보호가 필요하면 통합 문서를 암호화해야 한다
- 보호를 켜기 전에 옵션을 설정해도 된다.
AllowOption은 시트가 현재 보호 상태인지와 무관하게 할당할 수 있다. 이 토글은 단지Protect가 적용됐을 때 어떤 동작을 허용할지 설명하는 집합이다.UnProtect는 보호 상태와 해시를 지우지만, 다음에 다시 쓸 옵션 집합은 남겨 둔다 - 잠긴 셀 규칙은 그대로 적용된다. 보호는
Lockedattribute가 켜진 셀의 편집만 막는다. 입력 영역을 열어 두는 일은 protection option이 아니라 셀 스타일의 역할이며, 이 두 층은 Excel과 같은 방식으로 결합된다 - 이 글은 XLSX 엔진에 대한 것이다. 이 옵션 모델은 XLS 엔진의 오래된
Allow*property와 의미상 유사하지만, 여기의 enum과 property 이름인xlsxSpo*,AllowOption은lxHandleX의TXLSXWorksheet에 속한다. 같은 시트에서 print layout도 다룬다면 보호와 페이지 설정 가이드 가 print area와 header 옆에 이 설정이 어떻게 놓이는지 보여 주고, data validation, AutoFilter, table 글 은 잠긴 보고서에서xlsxSpoAutoFilter를 열어 둘 때 자연스럽게 이어 읽을 수 있다
여기서 설명한 세밀한 보호 모델과 나머지 XLSX read/write 엔진은 Delphi와 C++Builder용 HotXLS Component 에 포함되어 있으며, 제품 페이지에서 전체 워크시트 API와 보호 옵션 레퍼런스를 함께 볼 수 있다