PowerShell로 파일 서버 재고 조사하기 ── 용량 조사와 액세스 권한(ACL) 감사

· · PowerShell, Windows, 파일 서버, ACL, 액세스 권한, 운영 개선, 보안, 스크립트

‘파일 서버의 여유 공간이 10% 아래로 떨어졌습니다’ ── 이 경고에서 시작되는 일은 대체로 우울합니다. 어디가 용량을 잡아먹고 있는지 알 수 없습니다. 아무도 손대지 않은 10년 전 폴더를 지워도 되는지 판단할 수 없습니다. 애초에 누가 어디에 액세스할 수 있는지, 퇴직자의 권한이 남아 있지는 않은지 물어봐도 바로 답할 수 없습니다. 탐색기에서 속성을 하나하나 열어보는 조사는 수 테라바이트 규모의 공유 앞에서는 끝나지 않습니다.

이런 의뢰로 손을 움직일 때마다 느끼는 것은, 파일 서버 정리는 ‘삭제의 기술’이 아니라 ‘재고 조사의 기술’이라는 점입니다. 숫자와 목록 없이 ‘지워도 될까요’라고 물어봐도 부서는 절대 고개를 끄덕이지 않습니다. 반대로 ‘이 폴더만 800GB, 그중 3년 이상 수정되지 않은 파일이 620GB’라는 표가 있으면 이야기는 단숨에 앞으로 나아갑니다.

이 글에서는 중소기업의 정보시스템·운영 담당자를 대상으로, PowerShell로 파일 서버의 용량과 액세스 권한(ACL)을 재고 조사하고 CSV 리포트로 정리하기까지의 실무 절차를 다룹니다. 방침은 일관되게 ‘먼저 읽기, 변경은 마지막에, 백업과 -WhatIf를 갖추고서’입니다.

1. 먼저 결론

  • 용량 조사는 Get-ChildItem -Recurse + Measure-Object -Sum이 기본형입니다. 처음부터 전체를 집계하지 말고, 1단계 폴더 단위로 ‘용량을 많이 차지하는 곳’을 짚어봅니다.12
  • 액세스 거부는 무시하지 않고 기록합니다. -ErrorAction SilentlyContinue로 스캔을 멈추지 않는 대신, -ErrorVariable로 거부된 위치를 반드시 적어 둡니다. 세지 못한 위치는 ‘0’이 아니라 ‘불명’입니다.3
  • 오래된 파일 판정은 LastWriteTime 기준으로 합니다. NTFS의 마지막 액세스 날짜는 기본적으로 갱신이 비활성화(또는 시스템 관리)되어 있는 환경이 많아, 판단 근거로 신뢰할 수 없습니다.45
  • 중복 파일은 Get-FileHash(기본 SHA256)로 ‘후보’를 추출합니다. 먼저 크기로 좁힌 뒤 해시를 계산하는 것이 I/O를 절약하는 정석입니다.6
  • ACL 재고 조사는 Get-Acl의 Access 속성으로 합니다. IdentityReference(누가), FileSystemRights(무엇을), IsInherited(상속인지 직접 부여인지)를 CSV로 만들고, AreAccessRulesProtected로 상속이 끊긴 위치를 찾아냅니다.78
  • 이 글에서 재고 조사하는 대상은 NTFS 액세스 허용입니다. 공유(SMB)를 통한 실질 액세스는 공유 쪽 액세스 허용과 NTFS ACL의 조합으로 결정됩니다. 공유 쪽은 Get-SmbShareAccess로 별도로 목록화해, NTFS 대장과 함께 확인하세요.9
  • CSV 리포트의 문자 코드는 명시합니다. Windows PowerShell 5.1의 Export-Csv 기본값은 ASCII라서 일본어(한글)가 깨지고, PowerShell 7의 기본값은 BOM 없는 UTF-8이라서 Excel의 기본 동작과 궁합이 나쁠 수 있습니다.1011
  • 변경(Set-Acl)은 재고 조사와 부서 확인이 끝난 뒤에. icacls /save로 백업을 받고, -WhatIf로 대상을 확인한 뒤 적용합니다.1213

이 글의 절차를 한 차례 진행하면, 산출물로 다음 4가지가 손에 남습니다. 먼저 완성된 모습을 보여 드립니다.

산출물 작성하는 장 무엇을 알 수 있는가
폴더별 용량 순위 2장 Folder / SizeGB / FileCount 어느 폴더가 용량을 많이 차지하는가. ‘공유 전체에서 ◯GB, 그중 영업부가 ◯GB’라는 표현에 필요한 숫자가 여기서 나옵니다
old-files.csv 3장 FullName / LastWriteTime / SizeMB 3년 이상 수정되지 않은 파일 목록. SizeMB 내림차순이므로, 위에서부터 수십 행만 봐도 ‘효과가 큰 용량’이 보입니다
duplicate-candidates.csv 3장 Hash / Path 내용이 동일한 파일 그룹. Hash 열로 정렬하면 같은 내용끼리 모여서 나열됩니다
acl-report.csv 4장 Path / Identity / Rights / Type / IsInherited / Inheritance / InheritanceBroken 누가 어디에 어떤 권한을 갖고 있는지에 대한 대장. ACE 1건이 1행입니다

여기에 더해, 스캔하지 못한 위치를 기록하는 텍스트 파일(denied-paths.txt, old-files-uninspected.txt, hash-uninspected.txt, acl-uninspected.txt)이 나옵니다. 이 파일들이 비어 있지 않다면, 위의 4가지 산출물은 ‘구멍이 있는 리포트’입니다. 그 사실을 함께 알려서 전달하세요.

앞서 언급한 ‘이 폴더만 800GB, 그중 3년 이상 수정되지 않은 파일이 620GB’라는 문장은 첫 번째와 두 번째 산출물을 맞대어 보면 만들 수 있습니다. 뒷부분 숫자는 old-files.csv를 다시 읽어 합산하기만 하면 됩니다.

# old-files.csv에서 '3년 이상 수정되지 않은 파일의 합계 용량'을 산출한다
$old   = Import-Csv .\old-files.csv
$sumMB = ($old | ForEach-Object { [double]$_.SizeMB } | Measure-Object -Sum).Sum
'3년 이상 미수정: {0} 건 / {1:N1} GB' -f $old.Count, ($sumMB / 1024)

2. 용량 조사 ── 어디가 용량을 많이 차지하는지 숫자로 파악하기

가장 먼저 할 일은 공유 루트 바로 아래에 있는 폴더별 합계 크기를 구하는 것입니다. Get-ChildItem으로 재귀적으로 파일을 나열하고, Measure-Object로 Length를 합산합니다.12

$root = 'D:\share'   # 파일 서버에서 실행하는 것을 전제로 함. UNC 경유 시 주의사항은 6장
$denied = @()

# 1단계 폴더 단위로 집계한다 ── 전체를 한 번에 하기보다 '짚어보는' 것이 먼저
# 루트 자체의 나열 실패도 $denied에 들어가도록, 바깥쪽 Get-ChildItem에도 같은 옵션을 붙인다
$report = foreach ($dir in Get-ChildItem -LiteralPath $root -Directory `
    -ErrorAction SilentlyContinue -ErrorVariable +denied) {
    # 액세스 거부로 멈추지 않도록 SilentlyContinue, 다만 거부는 $denied에 누적한다
    # 나열은 파이프로 곧바로 Measure-Object로 흘려보낸다(변수로 받으면 FileInfo 전체를
    # 메모리에 유지하게 되어, 파일 수백만 건급 폴더에서는 부담이 커진다)
    $stats = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File `
        -ErrorAction SilentlyContinue -ErrorVariable +denied |
        Measure-Object -Property Length -Sum
    [PSCustomObject]@{
        Folder    = $dir.Name
        SizeGB    = [math]::Round([double]$stats.Sum / 1GB, 2)
        FileCount = $stats.Count
    }
}
# 루트 바로 아래에 직접 놓인 파일도 1행으로 센다(폴더 단위 집계에서 빠지기 때문)
$rootStats = Get-ChildItem -LiteralPath $root -File `
    -ErrorAction SilentlyContinue -ErrorVariable +denied |
    Measure-Object -Property Length -Sum
if ($rootStats.Count -gt 0) {
    $report += [PSCustomObject]@{
        Folder    = '(루트 바로 아래)'
        SizeGB    = [math]::Round([double]$rootStats.Sum / 1GB, 2)
        FileCount = $rootStats.Count
    }
}

$report | Sort-Object SizeGB -Descending | Format-Table -AutoSize

# '세지 못한 위치'를 반드시 남긴다 ── 여기가 비어 있지 않으면 집계는 불완전하다
$denied | ForEach-Object { $_.TargetObject } | Sort-Object -Unique |
    Set-Content -Path .\denied-paths.txt

핵심은 두 가지입니다. 첫째, -ErrorAction SilentlyContinue는 ‘오류를 못 본 척하는’ 스위치가 아닙니다. 표시를 억제하고 계속 진행할 뿐이며, 오류 자체는 발생하고 있습니다.3 그래서 -ErrorVariable로 받아둡니다. 변수 이름 앞에 +를 붙이면 덮어쓰기가 아니라 추가가 되므로, 루프 전체의 거부를 하나의 변수에 모을 수 있습니다.3 액세스가 거부된 폴더는 집계에서 빠지므로, 리포트상으로는 실제보다 작게 보입니다. denied-paths.txt가 비어 있지 않다면 그 사실을 반드시 리포트에 함께 적으세요. 오류 처리 설계를 한 단계 더 깊이 다루고 싶다면, 함께 공개한 「PowerShell의 오류 처리와 재실행 설계」가 참고가 됩니다.

둘째, 깊은 계층의 문제입니다. 오랫동안 운영된 공유에는 경로가 260자(MAX_PATH)를 넘는 위치가 거의 확실히 있으며, 도구에 따라서는 여기서 나열이 실패합니다.14 스캔 결과에 부자연스러운 누락이 있다면 먼저 경로 길이를 의심하세요. 이 제한의 전체상과 대응 방법은 「MAX_PATH와 Windows 경로·파일명의 함정」에 정리해 두었습니다.

3. 오래된 파일과 중복 후보 ── 삭제가 아니라 목록화부터

3.1. ‘3년간 손대지 않은 파일’을 목록으로 만들기

용량을 많이 차지하는 곳을 파악했다면, 다음은 ‘지울 수 있는 후보’의 자료 수집입니다. 기준으로 사용하는 것은 LastWriteTime(마지막 수정 날짜)입니다.

$cutoff = (Get-Date).AddYears(-3)

# 3년 이상 수정되지 않은 파일 목록. 삭제는 하지 않는다 ── 먼저 부서에 보여줄 자료를 만든다
# 나열에 실패한 위치도 나중에 공개하기 위해 -ErrorVariable로 기록한다
Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable oldEnumErrors |
    Where-Object LastWriteTime -lt $cutoff |
    Select-Object FullName, LastWriteTime,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Length / 1MB, 2) } } |
    Sort-Object SizeMB -Descending |
    Export-Csv -Path .\old-files.csv -NoTypeInformation -Encoding utf8BOM   # 5.1이라면 UTF8(둘 다 BOM 포함)

# 나열하지 못한 위치는 목록이 '완전해 보이는' 것을 막기 위해 반드시 공개한다
$oldEnumErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\old-files-uninspected.txt

‘마지막 액세스 날짜(LastAccessTime)를 쓰면 읽히지도 않았다는 것까지 알 수 있지 않을까’ 하고 생각하고 싶어지지만, 이것은 함정입니다. NTFS에서는 마지막 액세스 날짜 갱신이 성능에 영향을 주기 때문에, fsutil behavior 명령과 레지스트리(NtfsDisableLastAccessUpdate)로 갱신의 활성화/비활성화가 제어되며4, Windows Vista 이후로는 기본적으로 비활성화(최근의 Windows 10 이후는 시스템 관리이며, 서버에서는 비활성화)되어 있습니다.5실제로는 열람되었는데도 날짜가 오래된 그대로인 파일이 흔히 존재합니다. 재고 조사 설명 자료에는 ‘마지막으로 내용이 변경된 날’인 LastWriteTime을 사용하고, 그 의미도 명확히 적어 두면 부서와의 대화가 꼬이지 않습니다.

3.2. 중복 파일의 ‘후보’ 뽑아내기

공유 폴더에는 ‘최종판’, ‘최종판_수정’, ‘복사본 〜’이 대량으로 쌓입니다. 내용이 동일한지는 Get-FileHash로 판정할 수 있습니다. 기본 알고리즘은 SHA256이며, 해시 값이 일치하면 파일 내용도 동일하다고 판단할 수 있습니다.6

# 전체 파일의 해시 계산은 I/O 부담이 크다. 먼저 '크기가 같은 파일'로만 좁힌다
# 나열에 실패한 위치(액세스 거부 등)도 나중에 공개하기 위해 -ErrorVariable로 기록한다
$candidates = Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable enumErrors |
    Group-Object -Property Length |
    Where-Object { $_.Count -ge 2 -and [long]$_.Name -gt 0 } |   # 크기 0은 제외
    ForEach-Object { $_.Group }

# 좁힌 후보만 해시를 계산하고, 일치하는 그룹을 중복 '후보'로 출력한다
$candidates |
    Get-FileHash -ErrorAction SilentlyContinue -ErrorVariable hashErrors |   # 기본값은 SHA256
    Group-Object -Property Hash |
    Where-Object Count -ge 2 |
    ForEach-Object { $_.Group } |
    Select-Object Hash, Path |
    Export-Csv -Path .\duplicate-candidates.csv -NoTypeInformation -Encoding utf8BOM   # 5.1이라면 UTF8

# 나열하지 못한 폴더와, 잠김·읽기 권한 없음 등으로 해시를
# 계산하지 못한 파일은 '조사하지 못함'으로 반드시 목록에 남긴다
# (조용히 누락시키면 중복 0건과 구별할 수 없다)
@($enumErrors) + @($hashErrors) | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\hash-uninspected.txt

굳이 ‘후보’라고 부르는 이유는, 어느 쪽을 남길지에 대한 판단이 기술이 아니라 업무의 문제이기 때문입니다. 내용이 동일해도 ‘부서 A의 정본’과 ‘부서 B의 참조용 사본’은 의미가 다를 수 있습니다. 기계적으로 지우지 말고, 목록을 가지고 관계자와 이야기하는 데까지가 이 스크립트의 역할입니다. 해시라는 도구 자체의 성질(무엇이 보장되고 무엇이 보장되지 않는가)은 「해시 문자열로 방식을 판별하는 실무 절차」에서 정리했습니다.

3.3. 지우기 전에 격리하기 ── ‘격리 → 관찰 → 삭제’ 절차

부서에 목록을 확인받았더라도, 곧바로 삭제로 넘어가지 않습니다. 7장의 판단표에 적은 ‘격리 → 관찰 → 삭제’의 내용은 다음과 같은 절차입니다.

  1. 격리 위치를 정합니다. 같은 서버에 날짜가 들어간 폴더를 하나 만듭니다. 가능하면 원본과 같은 볼륨으로 하세요(이유는 뒤에서 설명). 권한은 관리자와 담당자만으로 좁힙니다. 다만 폴더의 권한만 좁혀서는 안의 파일에 효과가 없습니다. 같은 볼륨 내 이동에서는 원래 권한이 그대로 딸려오기 때문이며, 옮긴 각 파일을 상속 기본값으로 되돌리는 절차까지 포함해야 한 세트입니다(이것도 뒤에서 설명).
  2. 원래 계층 구조를 유지한 채 옮깁니다. 평평하게(플랫하게) 옮기면 동명 파일이 충돌하고, 되돌릴 때도 원래 위치를 알 수 없게 됩니다.
  3. 이동 대장을 남깁니다. 원래 경로와 이동 후 경로의 대응표가 없으면 ‘그 파일 어디로 갔지?’에 답할 수 없습니다. 대장에는 실제로 이동한 것만 적습니다. 시험 실행 결과와 이동시키지 못한 것은 별도 파일로 나누세요. 섞으면 복구할 때 존재하지 않는 위치를 보러 가게 됩니다.
  4. 관찰 기간을 정합니다. ‘1개월’이 아니라 ‘분기 마감을 한 번 넘길 때까지’처럼 업무 주기에 맞추는 것이 실무적입니다. 문의가 오면 대장을 보고 되돌립니다.
  5. 기간이 지나면 삭제합니다. 삭제 직전에 대장을 다시 한번 관련 부서와 공유합니다.
# 부서 확인이 끝난 목록(old-files.csv에서 대상 행만 남긴 것)을 격리 폴더로 옮긴다
$root       = 'D:\share'
$quarantine = 'D:\quarantine\2026-07'
$list       = Import-Csv .\old-files-approved.csv   # 열은 old-files.csv와 동일
$dryRun     = $true       # 먼저 $true로 대상을 확인하고, 문제가 없으면 $false로 바꿔 본 실행한다
$planned    = @()   # 시험 실행 결과. 본 실행의 대장과는 별도 파일로 출력한다
$moved      = @()   # 실제로 이동에 성공한 것(건수 집계용. 대장의 실체는 아래 파일)
$failed     = @()   # 이동하지 않은 것. 원래 위치에 있다
$needsAcl   = @()   # 이동은 했지만 권한을 좁히지 못한 것

# 출력 파일은 실행마다 다른 이름으로 한다. 고정 이름으로 하면 다음 배치를 실행하는 순간
# 이전 대장이 덮어써져, 그때 옮긴 파일을 되돌릴 수 없게 된다
$runId    = Get-Date -Format 'yyyyMMdd-HHmmss'
$ledger   = ".\quarantine-log-$runId.csv"        # 복구용 대장. 이동할 때마다 추가 기록한다
$planFile = ".\quarantine-plan-$runId.csv"
$failFile = ".\quarantine-failed-$runId.csv"
$aclFile  = ".\quarantine-needs-acl-$runId.csv"
$enc      = 'utf8BOM'                            # 5.1이라면 'UTF8'

# 범위 판정에 쓰는 절대 경로. 끝을 \ 로 맞춰 두면
# D:\share2 가 D:\share 의 내부라고 오판정되는 것을 막을 수 있다
$rootFull       = [System.IO.Path]::GetFullPath($root).TrimEnd('\') + '\'
$quarantineFull = [System.IO.Path]::GetFullPath($quarantine).TrimEnd('\') + '\'

foreach ($row in $list) {
    $src = $row.FullName
    if (-not (Test-Path -LiteralPath $src)) { continue }   # 이미 없는 것은 건너뛴다

    # 이 CSV는 사람이 편집한 것이라, $root 밖을 가리키는 행이 섞여 들어갈 수 있다.
    # 아래의 Substring은 그 경우에도 조용히 동작해, 전혀 무관한 위치의 파일 이동 위치를 만들어 낸다.
    # 실체 경로로 바꾼 뒤, 감사 대상의 내부에 있음을 확인하고 나서 진행한다
    $srcFull = (Resolve-Path -LiteralPath $src).ProviderPath
    if (-not $srcFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = ''
                                      Error  = "감사 대상($root) 밖을 가리키고 있습니다" }
        continue
    }

    # 원래 계층 구조를 유지한 채 옮긴다(동명 파일 충돌과 '되돌릴 수 없음'을 방지)
    $relative = $srcFull.Substring($rootFull.Length)
    # Join-Path 는 '..' 를 해석하지 않는다. GetFullPath 로 정리한 뒤,
    # 격리 위치의 내부에 들어오는지를 확인한다
    $dest     = [System.IO.Path]::GetFullPath((Join-Path $quarantine $relative))
    if (-not $dest.StartsWith($quarantineFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest
                                      Error  = "격리 위치($quarantine) 밖으로 벗어납니다" }
        continue
    }

    if ($dryRun) {
        # 시험 실행 행을 $moved 에 섞으면 안 된다. 같은 대장에 출력하면
        # 본 실행 후에 한 번 더 시험 실행하는 것만으로, 유효한 대장이
        # '실재하지 않는 이동 위치' 목록으로 덮어써진다
        $planned += [PSCustomObject]@{ Source = $src; Destination = $dest }
        continue
    }

    try {
        New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force -ErrorAction Stop | Out-Null

        # Move-Item 의 오류는 기본적으로 비종료성이다. -ErrorAction Stop 을 붙이지 않으면
        # 잠김·액세스 거부·충돌로 실패해도 다음 행으로 넘어가,
        # '원래 위치에 남아 있는데 대장에는 이동 완료라고 적는' 일이 벌어진다
        Move-Item -LiteralPath $srcFull -Destination $dest -ErrorAction Stop
    }
    catch {
        # 여기서 실패한 파일은 이동하지 않았다. 원래 위치에 있다
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest; Error = $_.Exception.Message }
        continue
    }

    # 여기서부터는 파일이 $dest 에 있다. 이후 무엇이 실패하더라도
    # '$dest 에 있다'는 것은 반드시 대장에 남긴다. 여기를 failed 쪽에 넣으면
    # 기록은 '원래 위치'로 읽히는데 실물은 격리 위치라는 최악의 불일치가 되고,
    # 다시 실행해도 Test-Path $src 가 거짓이므로 두 번 다시 잡히지 않는다.
    #
    # 그리고 루프를 빠져나온 뒤 한꺼번에 출력해서는 안 된다. 도중에 전원이 나가면
    # 이동된 파일이 한 건도 기록되지 않은 상태가 된다. 1건씩 추가 기록한다
    $record = [PSCustomObject]@{ Source = $srcFull; Destination = $dest; MovedAt = Get-Date }
    $record | Export-Csv -Path $ledger -NoTypeInformation -Encoding $enc -Append
    $moved += $record

    # 같은 볼륨 내 이동에서는 원래 권한이 그대로 딸려온다(뒤에서 설명).
    # 상속 기본값으로 되돌려, 격리 폴더의 권한만 적용되는 상태로 만든다
    try {
        & icacls $dest /reset /q
        if ($LASTEXITCODE -ne 0) { throw "icacls /reset에 실패했습니다 (exit $LASTEXITCODE)" }
    }
    catch {
        # 이동은 되었지만 권한을 좁히지 못했다. 격리 위치에 원래의 넓은 권한 그대로 놓여 있다.
        # 이것도 1건씩 추가 기록한다
        $aclRow = [PSCustomObject]@{ Destination = $dest; Error = $_.Exception.Message }
        $aclRow | Export-Csv -Path $aclFile -NoTypeInformation -Encoding $enc -Append
        $needsAcl += $aclRow
    }
}

if ($dryRun) {
    $planned | Export-Csv -Path $planFile -NoTypeInformation -Encoding $enc
    Write-Host "시험 실행: $($planned.Count) 건이 대상입니다. 이동은 하지 않았습니다 ($planFile)"
    return
}

$failed | Export-Csv -Path $failFile -NoTypeInformation -Encoding $enc

# 권한을 좁히지 못한 분은 조용히 넘기지 않는다. 여기가 0이 될 때까지 관찰 기간을 시작하지 않는다
if ($needsAcl.Count -gt 0) {
    Write-Warning "권한을 좁히지 못한 격리 파일이 $($needsAcl.Count) 건 있습니다. $aclFile 을 확인해 처리하세요"
}
Write-Host "이동 $($moved.Count) 건 / 실패 $($failed.Count) 건 / 권한 미처리 $($needsAcl.Count) 건"
Write-Host "대장: $ledger"

옮기기 전에, 그 경로가 정말로 감사 대상 내부인지 확인하세요. 이 스크립트가 읽는 것은 old-files-approved.csv ── 사람이 열어서 행을 지우고, 때로는 손으로 덧붙이는 파일입니다. 여기에 $root 밖을 가리키는 FullName이 한 줄 섞여 들어가도, $src.Substring($root.Length)는 예외를 던지지 않습니다. 문자 수를 세어 앞부분을 잘라낼 뿐이므로, 전혀 무관한 파일의 이동 위치를 아무렇지 않게 만들어 냅니다. 오래된 CSV를 재사용했다, 경로를 복사하다 빠뜨렸다, 대상을 ‘겸사겸사’ 덧붙였다 ── 어느 것이든 일어납니다. 같은 이유로 Join-Path..를 해석하지 않으므로, 상대 구분자를 포함한 행은 격리 폴더 밖을 가리키는 $dest가 될 수 있습니다.

그래서 Resolve-Path로 실체 경로로 바꾼 뒤 $root 내부에 있는지 확인하고, $destGetFullPath로 정리한 뒤 $quarantine 내부에 들어오는지를 보고 있습니다. 비교용 경로 끝에 \를 붙이는 것은 D:\share2D:\share의 내부라고 잘못 판정되는 것을 막기 위해서입니다. 범위 밖의 행은 이동시키지 않고 $failed로 떨어뜨려, CSV 쪽을 고치도록 합니다. 이 두 가지 확인은, 격리 작업이 ‘대상을 정하고 나서 움직인다’는 것을 코드 쪽에서도 보증하기 위한 것입니다.

스크립트 안에 icacls /reset을 끼워 넣은 것은 권한 때문입니다. 공식 문서에 명시된 대로, 개체는 기본적으로 이동 대상의 부모로부터 권한을 상속하지만, 유일한 예외가 ‘같은 볼륨 내의 다른 폴더로의 이동’이며, 이 경우에만 원래 권한이 유지됩니다.15

즉, 1단계에서 권장한 ‘원본과 같은 볼륨으로 옮기는’ 방식은, 격리 폴더의 권한이 안의 파일에 미치지 않는 방식이기도 합니다. 부서의 공유 그룹에 열려 있던 파일은 격리 폴더를 관리자만으로 좁혀도, 옮긴 뒤에도 그 그룹에서 읽을 수 있는 상태 그대로입니다. ‘지우지는 않았지만 격리한 것도 아닌’ 이 작업에서 가장 나쁜 실패가 여기서 일어납니다. icacls <path> /reset기본 상속 ACL로 ACL을 교체하므로, 이것을 이동 직후에 실행하면 격리 폴더의 권한만 적용되는 상태가 됩니다.13

다른 볼륨(다른 드라이브·다른 서버)으로 옮긴 경우에는, 그 시점에 이동 대상 폴더의 권한을 상속합니다.15 이 경우에는 /reset이 필요 없지만, 격리 폴더 자체의 권한을 미리 좁혀 두는 것이 전제가 됩니다. 어느 경로든, 이동 후에 몇 건을 Get-Acl로 뽑아 예상대로의 권한이 되어 있는지 확인해 두면 확실합니다.

이동은 되었지만 /reset에 실패한 경우의 처리에도 주의하세요. 이때 파일은 이미 격리 위치로 옮겨져 있고, 권한만 원래의 넓은 상태 그대로입니다. 이것을 ‘실패’로 취급해 대장에서 빼면, 기록상으로는 원래 위치에 있는 것이 되고, 게다가 다시 실행해도 원래 경로가 존재하지 않으므로 다시 잡히지 않습니다. 격리 위치에 넓은 권한 그대로, 어느 대장에도 실리지 않은 채 남는다는 가장 나쁜 형태입니다. 위 스크립트가 이동이 끝난 시점에 대장에 올리고, 권한을 좁히지 못한 분을 별도 CSV로 출력하는 것은 이 때문입니다. 건수가 0이 아니라면, 그것을 정리할 때까지 관찰 기간을 시작하지 마세요.

대장 파일 이름을 고정하지 마세요. 격리는 한 번으로 끝나지 않습니다. 부서별, 분기별로 여러 번 돌립니다. 고정된 이름의 quarantine-log.csv로 출력하는 구조라면, 두 번째 배치를 실행하는 순간 첫 번째 대장이 사라집니다. 그때 옮긴 파일은 격리 위치에 있는데, 어디서 왔는지 아는 기록은 어디에도 없습니다 ── 복구할 수 없습니다. 위 스크립트가 실행마다 -$runId를 붙이는 것은 이 때문입니다. 되돌릴 때는 Import-Csv .\quarantine-log-*.csv로 전부 읽습니다.

대장은 1건씩 추가합니다. 루프를 빠져나온 뒤 한꺼번에 Export-Csv하는 구조라면, 도중에 세션이 끊기거나 서버가 재시작된 경우 이동된 파일이 한 건도 기록되지 않은 상태가 됩니다. 이동은 실제로 일어났으므로, 이것은 ‘작업 전’이 아니라 ‘기록만 없는’ 상태입니다. -Append로 한 건씩 쓰면 어디서 멈추든 그때까지는 되돌릴 수 있습니다. 파일을 다시 여는 만큼 느려지지만, 대장은 속도보다 확실함이 필요한 부분입니다.

스크립트가 출력하는 파일은 역할별로 나뉘어 있습니다($runId20260718-143052와 같은 실행 시각입니다).

파일 내용 언제 나오는가
quarantine-plan-$runId.csv 앞으로 옮길 예정 목록 시험 실행($dryRun = $true)일 때
quarantine-log-$runId.csv 실제로 이동한 것. 되돌릴 때 사용하는 대장. 1건씩 추가 기록됨 본 실행일 때
quarantine-failed-$runId.csv 이동시키지 못한 것. 원래 위치에 있음 본 실행일 때
quarantine-needs-acl-$runId.csv 이동은 했지만 권한을 좁히지 못한 것 해당 사항이 있을 때만

4. 액세스 권한(ACL) 재고 조사 ── 누가 무엇을 할 수 있는지 표로 만들기

용량과 나란히 놓이는 또 하나의 재고 조사 대상이 권한입니다. 여기서부터 용어가 단숨에 늘어나므로, 먼저 관계를 정리해 두겠습니다.

같은 내용의 다른 표현보안 설명자파일-폴더마다 붙어 있음Get-Acl이 가져오는 것이 바로 이것소유자SID로 기록됨DACL누구에게 무엇을 허용-거부할지의 목록Get-Acl의 Access 속성에서 보이는 부분SACL감사 설정. 이 글에서는 다루지 않음ACE ── DACL의 한 줄IdentityReference - 누구인가, SID 또는 해석된 이름FileSystemRights - 무엇을 할 수 있는가AccessControlType - 허용인가 거부인가IsInherited - 상속인가 직접 부여인가ACEACE ...SDDL이 기술자 전체를 하나의 문자열로 나타낸 형식

말로 풀면, 보안 설명자 안에 DACL이 있고, DACL 안에 ACE가 나란히 있으며, ACE가 가리키는 대상은 SID라는 중첩 구조입니다. 실무에서 ‘ACL’이라고 부르는 것은 대부분 이 DACL을 가리킵니다. 이후에 나오는 표도 CSV도, 가장 안쪽의 ACE를 한 줄씩 기록한 것이라고 생각하며 읽어 주세요. SDDL은 같은 내용을 문자열로 표현한 형식으로, 재고 조사 단계에서는 읽을 필요가 없습니다.

이를 전제로 본론입니다. Get-Acl은 파일이나 폴더의 보안 설명자를 가져오며, Access 속성에서 DACL의 액세스 제어 항목(ACE) 목록을 읽을 수 있습니다.7

# 공유 루트에서 2단계 깊이까지의 폴더 ACL을 재고 조사한다
# 권한 설계의 '중심 줄기'는 얕은 계층에 집중되어 있으므로, 먼저 그곳을 확인한다
$aclErrors = @()   # + 붙은 -ErrorVariable은 추가(append) 방식이므로, 이전 실행분이 남지 않도록 매번 초기화한다
$targets = @(Get-Item -LiteralPath $root `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors) +
           @(Get-ChildItem -LiteralPath $root -Directory -Recurse -Depth 1 `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors)

$aclReport = foreach ($t in $targets) {
    # 가져오기 실패(읽기 불가·스캔 중 삭제 등)도 나중에 공개할 수 있도록 기록한다
    $acl = Get-Acl -LiteralPath $t.FullName -ErrorAction SilentlyContinue -ErrorVariable +aclErrors
    if (-not $acl) { continue }
    if (@($acl.Access).Count -eq 0) {
        # DACL이 비어 있는(명시 항목 0건) 폴더도 행으로 남긴다. 상속 끊김 정보가 대장에서 사라지지 않도록
        [PSCustomObject]@{
            Path = $t.FullName; Identity = '(항목 없음)'; Rights = $null; Type = $null
            IsInherited = $null; Inheritance = $null
            InheritanceBroken = $acl.AreAccessRulesProtected
        }
        continue
    }
    foreach ($ace in $acl.Access) {
        [PSCustomObject]@{
            Path        = $t.FullName
            Identity    = $ace.IdentityReference    # 누구(사용자/그룹)
            Rights      = $ace.FileSystemRights     # 무엇을 할 수 있는가
            Type        = $ace.AccessControlType    # Allow / Deny
            IsInherited = $ace.IsInherited          # 부모로부터의 상속인가, 직접 부여인가
            # 같은 권한이라도 '이 폴더만'인지 '하위 파일/폴더에도 미치는지'를 구분한다
            Inheritance = "$($ace.InheritanceFlags)/$($ace.PropagationFlags)"
            InheritanceBroken = $acl.AreAccessRulesProtected  # 이 폴더에서 상속을 끊었는가
        }
    }
}
$aclReport | Export-Csv -Path .\acl-report.csv -NoTypeInformation -Encoding utf8BOM   # 5.1이라면 UTF8

# ACL을 가져오지 못한 대상도 대장의 구멍으로 반드시 공개한다(다른 조사와 같은 방식)
$aclErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\acl-uninspected.txt

이 CSV를 열었다면, 다음 관점으로 필터링해 이상 징후를 찾습니다.

확인해야 할 열 이상 징후 전형적인 배경
Identity 개인 사용자 이름이 직접 올라와 있다 ‘일단 이 사람에게 권한을’이 쌓인 결과. 인사이동·퇴직으로 부패한다
Identity S-1-5-21-...처럼 SID 그대로 표시된다 삭제된 계정의 권한이 남아 있다(정리 1순위 후보)
IsInherited = False 깊은 계층에 직접 부여된 ACE가 흩어져 있다 임기응변식 개별 부여. 설계에서 벗어난 예외
InheritanceBroken = True 상속이 끊긴 폴더 과거의 ‘이 폴더만은 보이고 싶지 않다’는 대응. 재고 조사의 중점 대상
Type = Deny 거부 ACE가 있다 같은 조건이면 거부가 허용보다 우선한다. 다만 명시적 ACE는 상속된 ACE보다 먼저 평가되므로, 명시적 허용이 상속된 거부를 이기는 순서도 있다. 영향은 ACE의 출처까지 확인하고, 최종적으로는 실질 액세스로 확인한다

IsInherited이 False인 항목은 ‘그 폴더에서 누군가가 수작업으로 추가한 권한’이고, AreAccessRulesProtected가 True인 폴더는 ‘부모의 권한 변경이 닿지 않는 섬’입니다.78 권한 문제의 대부분이 이 두 가지에 집중되므로, 우선 여기서부터 대장으로 만듭니다. 한 가지 한계도 명확히 밝혀 둡니다. 이 예시는 -Depth 1의 얕은 스캔이므로, 그보다 깊은 계층에서 상속을 끊은 폴더는 대상에 들어가지 않으며, 오류도 발생하지 않습니다. 상속 끊김을 전수 조사하고 싶다면 시간은 걸리지만 -Depth 제한을 없애고 같은 스크립트를 실행하거나, 의심되는 부서 폴더를 루트로 지정해 다시 실행하세요. 참고로 Get-Acl은 SDDL이라는 문자열 형식으로도 보안 설명자를 표시할 수 있지만7, 재고 조사 목적이라면 Access 속성 표만으로 충분합니다. SDDL 해독에 뛰어드는 것은 이 표로 설명되지 않는 현상을 만난 뒤에 해도 늦지 않습니다.

실무적인 주의사항을 하나 더. CSV의 문자 코드는 반드시 명시하세요. Windows PowerShell 5.1의 Export-Csv는 기본적으로 ASCII로 출력되기 때문에 일본어(한글)가 깨집니다.10 PowerShell 7 계열의 기본값은 utf8NoBOM(BOM 없는 UTF-8)이며11, Excel에서 그대로 열면 글자가 깨지는 환경도 있습니다. 부서에 전달할 파일이라면 -Encoding UTF8BOM(5.1에서는 -Encoding UTF8)처럼 BOM 포함 UTF-8로 맞추는 것이 무난합니다.

5. 변경은 어떻게 하는가 ── icacls와 Set-Acl의 구분 사용, 백업과 -WhatIf

재고 조사와 부서 확인이 끝나야 비로소 변경 단계입니다. 여기서는 도구가 두 계열 있습니다.

용도 도구 이유
조사·리포트 Get-Acl 결과가 객체이므로 필터링·CSV화가 쉬움7
변경 전 백업 icacls /save ACL을 파일에 저장하고, /restore로 그대로 되돌릴 수 있음13
정형적인 권한 부여·삭제 icacls /grant, /remove 한 줄로 끝나며, /t로 재귀 적용, 상속 재활성화(/inheritancelevel)도 가능13
복잡한 조건부 변경 Set-Acl 규칙을 코드로 조립할 수 있음. -WhatIf 지원12
손상된 ACL 재구성 icacls /reset 기본 상속 ACL로 교체함(영향 큼. 최후의 수단)13

어느 쪽을 쓰든 순서는 고정입니다. 백업 → -WhatIf(또는 대상 확인) → 적용 → 사후 확인.

# 1. 변경 전에 ACL을 파일로 저장한다(/t로 하위 전체, /c로 오류가 있어도 계속 진행)
icacls "D:\share\sales" /save "C:\aclbackup\sales-acl.txt" /t /c
# 백업이 되지 않은 상태로 변경으로 넘어가지 않는다. icacls의 성공 여부는 종료 코드로 확인한다
if ($LASTEXITCODE -ne 0) {
    throw "ACL 백업에 실패했습니다 (icacls ExitCode=$LASTEXITCODE). 변경 작업을 중단합니다"
}

# 2. Set-Acl로 변경하는 경우: 가져오기 → 규칙 편집 → 적용 의 3단계
$path = 'D:\share\sales\estimate'
$acl  = Get-Acl -LiteralPath $path

# 영업 그룹에 수정 권한을 부여하는 규칙(하위 폴더·파일에 상속)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'CONTOSO\SalesTeam', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
# 추가 부여는 AddAccessRule로 한다. SetAccessRule은 같은 사용자/그룹의 기존 Allow 규칙을
# 치환하므로, 그 그룹에 이미 설정되어 있던 세부 권한이 조용히 사라질 수 있다
$acl.AddAccessRule($rule)

# 3. 먼저 -WhatIf로 '어디에 적용되는지'를 확인한 뒤, 본 실행으로 넘어간다
Set-Acl -LiteralPath $path -AclObject $acl -WhatIf
# 문제가 없으면: Set-Acl -LiteralPath $path -AclObject $acl

Set-Acl은 ‘Get-Acl로 가져온 보안 설명자를 모델로 적용하는’ 방식으로 동작하기 때문에12, 가져오기와 적용 사이에 대상을 잘못 지정하면 의도치 않은 ACL 전체 교체가 됩니다. 위처럼 가져오기 경로와 적용 경로를 같은 변수로 유지하고, 적용 전에 -WhatIf를 끼워 넣는 소박한 습관이 사고를 막아 줍니다. 상속을 끊는 작업(SetAccessRuleProtection)도 같은 틀 안에서 할 수 있지만12, 4장에서 본 것처럼 상속 끊김은 앞으로의 관리 비용이 되므로, 새로 만드는 것은 정말로 필요한 곳으로 한정하세요.

이 일련의 작업을 정기 실행 스크립트로 키워 나갈 때의 설계(SupportsShouldProcess, 증적을 남기는 방법, 작업 스케줄러 등록)는 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기」에서 다룬 형태를 그대로 활용할 수 있습니다.

6. 어디서 실행할 것인가 ── UNC 경유 스캔과 실행 계정

마지막으로, 의외로 결과를 크게 좌우하는 ‘실행 위치와 실행자’에 대한 이야기입니다.

  • 대규모 스캔은 서버에서 실행하는 것이 원칙입니다. UNC 경로(\\fs01\share)를 통한 재귀 나열은 파일 1개마다 메타데이터를 가져오는 것이 네트워크 왕복이 되므로, 수십만 개 파일 규모에서는 소요 시간이 자릿수 단위로 달라집니다. 관리 단말에서 하고 싶다면, PowerShell Remoting으로 스크립트를 서버 쪽에서 실행하고 결과 CSV만 회수하는 구성이 실무적입니다. 방법은 함께 공개한 「PowerShell Remoting(WinRM) 입문」을 참조하세요.
  • 결과는 실행 계정 권한의 스냅샷입니다. 액세스할 수 없는 위치는 나열에서도 Get-Acl에서도 빠집니다. 조사는 관리자 권한 계정으로 수행하고, 그럼에도 거부된 위치(2장의 denied-paths.txt)를 ‘조사하지 못한 위치’로 보고서에 포함합니다.
  • UNC를 경유해 실행할 경우, 자격 증명과 세션의 함정에도 주의가 필요합니다. 드라이브 문자는 로그온 세션 단위이므로, 대화형 로그온 중에 할당한 Z:는 작업 스케줄러의 작업에서는 보이지 않습니다. 또한 오류 1219(ERROR_SESSION_CREDENTIAL_CONFLICT)는 같은 서버(또는 공유)에 대해, 같은 사용자가 여러 사용자 이름을 사용해 연결하는 것은 허용되지 않는다는 사양상의 오류입니다.16 ‘조사용 공유만 관리자 계정으로, 나머지는 자신의 계정으로’처럼 같은 파일 서버에 두 종류의 자격 증명으로 연결하려고 하면, 두 번째 연결이 이 오류로 실패합니다. UNC를 경유해 조사한다면, 실행하기 전에 기존 연결을 하나의 자격 증명으로 통일해 두세요. 함정의 전체상은 「네트워크 드라이브와 UNC 경로의 함정」에 정리해 두었습니다.
  • 소요 시간은 ‘남의 숫자’가 아니라 자신의 환경에서 직접 측정합니다. 디스크 종류, 파일 수, 백신의 실시간 스캔, 계층 깊이에 따라 결과가 크게 달라지므로, 전체를 실행하기 전에 대표적인 부서 폴더를 하나 골라 실측하고 공유 전체 파일 수로 외삽하는 것이 확실합니다. 같은 측정을 UNC 경유와 서버에서 직접 실행하는 두 경우 모두 해보면, ‘서버에서 실행해야 하는지’도 숫자로 보여줄 수 있습니다.
# 대표적인 부서 폴더를 하나 골라, 스캔 속도를 실측한 뒤 전체 견적을 세운다
# (파일이 1건도 없는 폴더를 고르면 마지막 나눗셈에서 실패하므로, 실제 데이터가 있는 위치를 고른다)
$sample = 'D:\share\sales'
$start  = Get-Date
$count  = (Get-ChildItem -LiteralPath $sample -Recurse -File `
    -ErrorAction SilentlyContinue | Measure-Object).Count
$sec    = ((Get-Date) - $start).TotalSeconds
'{0}개 파일을 {1:N1}초에 나열 → 파일 1만 개당 {2:N1}초' -f `
    $count, $sec, ($sec / $count * 10000)

이 견적이 효과가 있는 것은 나열이 중심인 처리(2장의 용량 집계, 3장의 오래된 파일 목록, 4장의 ACL 가져오기)입니다. 3장의 해시 계산만은 파일 수가 아니라 총 바이트 수로 시간이 결정되므로, 별도로 견적을 내세요.

7. 실무의 정석(판단표)

논점 선택지 판단 기준
용량 조사 범위 전체 일괄 / 1단계별 먼저 얕은 계층에서 짚어보고, 용량이 큰 곳만 파고든다12
오류 처리 중단 / 무시 / 기록하고 계속 SilentlyContinue + ErrorVariable로 ‘계속하면서 전부 기록’. 거부 구역은 리포트에 명기3
오래된 파일 기준 LastAccessTime / LastWriteTime 액세스 날짜는 갱신 비활성화 환경이 많아 채택하지 않음. 수정 날짜 + 기간으로 부서와 합의45
중복 검출 전체 파일 해시 / 크기로 좁힌 뒤 해시 해시는 I/O 부담이 큼. 크기가 같은 그룹만 계산한다6
ACL 조사 깊이 전체 폴더 / 얕은 계층 + 상속 끊긴 위치 상속이 살아 있으면 부모를 보면 알 수 있음. IsInherited=False와 Protected=True를 중점으로78
권한 변경 도구 icacls / Set-Acl 정형적 변경과 백업은 icacls. 조건 분기가 있는 일괄 처리는 Set-Acl+-WhatIf1312
삭제 실행 즉시 삭제 / 격리 → 관찰 → 삭제 목록을 부서에서 확인한 뒤, 먼저 격리 폴더로 옮기고 일정 기간 문제가 없으면 삭제

8. 정리

  • 파일 서버 정리는 삭제가 아니라 재고 조사에서부터. 1단계 용량 순위, 오래된 파일 목록, 중복 후보, ACL 대장의 4가지 세트를 CSV로 만들고, 이를 자료 삼아 부서와 대화합니다.
  • 스캔은 SilentlyContinue로 멈추지 않는 대신, ErrorVariable로 거부된 위치를 반드시 기록합니다. 세지 못한 위치는 ‘0’이 아니라 ‘불명’입니다.
  • 오래됨의 판정은 LastWriteTime 기준. NTFS의 마지막 액세스 날짜는 갱신이 비활성화된 환경이 많아 신뢰할 수 없습니다.
  • 중복은 크기로 좁힌 뒤 Get-FileHash(기본 SHA256). 일치는 ‘후보’이며, 남길지에 대한 판단은 업무 측과 함께 합니다.
  • ACL은 Get-Acl의 Access를 CSV화하고, 개인 직접 부여·SID만 남은 항목·상속 끊김(AreAccessRulesProtected)을 중점적으로 확인합니다. SDDL까지 깊이 들어갈 필요는 없습니다.
  • 변경은 icacls /save로 백업 → -WhatIf나 대상 확인 → 적용 → 사후 확인 순서로. 대규모 스캔은 서버에서(또는 Remoting을 통해) 관리자 권한으로 실행합니다.

관련 글

관련 상담 영역

합동회사 코무라소프트에서는 파일 서버의 용량·권한 재고 조사 스크립트 제작 지원, 권한 설계 재검토에 따른 조사, 정기 리포트 자동화(작업 스케줄러 운영까지)를 다루고 있습니다. ‘먼저 현재 상태를 숫자로 파악하고 싶다’는 단계에서의 상담도 환영합니다.

참고 링크

  1. Microsoft Learn, Get-ChildItem. Get-ChildItem에 의한 항목 나열, -Recurse에 의한 재귀와 -Depth에 의한 깊이 제한, -Recurse 사용 시에는 와일드카드 해석을 피하기 위해 -LiteralPath로 대상을 지정하는 것이 권장된다는 점에 대해.  2 3

  2. Microsoft Learn, Measure-Object. Measure-Object가 -Property Length -Sum 등으로 파일 크기의 합계·최댓값·최솟값·평균을 계산할 수 있다는 점, Get-ChildItem과 조합한 디렉터리 내 파일 집계 예시에 대해.  2 3

  3. Microsoft Learn, about_CommonParameters. -ErrorAction SilentlyContinue가 오류 표시를 억제하고 실행을 계속한다는 점, -ErrorVariable로 지정한 변수에 오류 레코드가 저장된다는 점, 변수 이름 앞에 +를 붙이면 덮어쓰기가 아니라 추가가 된다는 점에 대해.  2 3 4

  4. Microsoft Learn, fsutil behavior. NTFS의 마지막 액세스 날짜(Last Access Time) 갱신의 활성화/비활성화가 fsutil behavior의 disablelastaccess 매개변수와 NtfsDisableLastAccessUpdate 레지스트리 값으로 제어된다는 점, 갱신 비활성화가 파일/디렉터리 액세스 속도 향상을 위해 마련되어 있다는 점에 대해.  2 3

  5. Microsoft Learn, [MS-FSA]: Appendix A: Product Behavior. Windows Vista 이후로 NTFS/ReFS의 마지막 액세스 날짜 갱신이 기본적으로 비활성화되어 있다는 점, Windows 10 v1803 이후는 시스템 관리가 되며, 서버 시스템에서는 마지막 액세스 날짜 갱신이 항상 비활성화된다는 점에 대해.  2 3

  6. Microsoft Learn, Get-FileHash. Get-FileHash의 기본 알고리즘이 SHA256이라는 점, 해시 값이 일치하는 두 파일은 내용도 동일하다고 판단할 수 있다는 점, 파일 이름이나 확장자를 바꿔도 해시 값은 변하지 않는다는 점에 대해.  2 3

  7. Microsoft Learn, Get-Acl. Get-Acl이 파일이나 리소스의 보안 설명자를 가져온다는 점, 기본적으로 DACL의 액세스 제어 항목 목록(Access)을 표시한다는 점, SDDL 형식(Sddl 속성)으로도 가져올 수 있다는 점에 대해.  2 3 4 5 6

  8. Microsoft Learn, ObjectSecurity.AreAccessRulesProtected Property. AreAccessRulesProtected 속성이 보안 설명자의 DACL이 보호되어 있는지(부모로부터 상속을 받지 않는지) 여부를 반환한다는 점에 대해.  2 3

  9. Microsoft Learn, Get-SmbShareAccess. Get-SmbShareAccess가 SMB 공유의 ACL(공유에 대한 액세스 권한이 부여된 보안 주체와 허용/거부·권리)을 가져오는 명령(cmdlet)이라는 점에 대해. 

  10. Microsoft Learn, about_Character_Encoding. Windows PowerShell(5.1)에서는 cmdlet마다 기본 인코딩이 통일되어 있지 않아 Export-Csv는 ASCII로 파일을 만든다는 점, PowerShell 6 이후는 기본값이 utf8NoBOM으로 통일되었다는 점에 대해.  2

  11. Microsoft Learn, Export-Csv. Export-Csv가 객체의 각 속성을 열로 하는 CSV 파일을 만든다는 점, PowerShell 7 계열에서 -Encoding의 기본값이 UTF8NoBOM이며 UTF8BOM 등을 명시적으로 지정할 수 있다는 점에 대해.  2

  12. Microsoft Learn, Set-Acl. Set-Acl이 AclObject로 전달한 보안 설명자를 모델로 대상 ACL을 변경한다는 점, -WhatIf/-Confirm을 지원한다는 점, FileSystemAccessRule을 만들어 SetAccessRule()로 추가하는 절차, SetAccessRuleProtection()에 의한 상속 비활성화(기존 상속 규칙 유지 여부 선택 포함) 예시에 대해.  2 3 4 5

  13. Microsoft Learn, icacls. /reset이 ‘대상 파일의 ACL을 기본 상속 ACL로 교체한다’는 점, /q가 성공 메시지를 억제한다는 점에 대해.  2 3 4 5 6

  14. Microsoft Learn, Maximum Path Length Limitation. Windows API의 경로 길이 상한 MAX_PATH가 260자라는 점, 레지스트리의 LongPathsEnabled와 앱 쪽의 longPathAware 선언이 모두 갖춰진 경우에만 많은 Win32 함수에서 제한이 완화된다는 점에 대해. 

  15. Microsoft Learn, Permissions on copying and moving files and folders. 개체가 기본적으로 이동 대상·생성 대상의 부모로부터 액세스 허용을 상속한다는 점, 유일한 예외가 같은 볼륨 내의 다른 폴더로의 이동이며 이 경우에는 원래 액세스 허용이 유지된다는 점, 다른 볼륨으로 복사 또는 이동한 경우에는 이동 대상 폴더의 액세스 허용을 상속한다는 점에 대해.  2

  16. Microsoft Learn, System Error Codes (1000-1299). 오류 1219(ERROR_SESSION_CREDENTIAL_CONFLICT)가 ‘동일한 사용자가 여러 사용자 이름을 사용해 같은 서버 또는 공유 리소스에 여러 연결을 하는 것은 허용되지 않는다’는 오류라는 점에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

PowerShell로 폴더별 합계 크기를 조사하려면 어떻게 해야 하나요?
Get-ChildItem으로 -Recurse를 붙여 파일을 나열하고, Measure-Object의 -Property Length -Sum으로 합산합니다. 전체를 한 번에 집계하기보다는, 먼저 공유 루트 바로 아래의 1단계 폴더 단위로 집계해 '용량을 많이 차지하는 곳'을 짚어보는 것이 실무적입니다. 이때 -ErrorAction SilentlyContinue와 -ErrorVariable을 함께 사용해, 액세스 거부로 세지 못한 위치를 반드시 기록하세요. 오류를 조용히 무시하면 집계에서 빠진 위치가 '크기 0'으로 보이게 됩니다.
오래된 파일을 판정할 때 LastAccessTime(마지막 액세스 날짜)을 사용해도 되나요?
권장하지 않습니다. NTFS에서는 성능상의 이유로 마지막 액세스 날짜 갱신이 기본적으로 비활성화(또는 시스템 관리)되어 있는 환경이 많아서, 실제로 파일을 읽었더라도 날짜가 갱신되지 않는 경우가 있기 때문입니다. 재고 조사 기준으로는 LastWriteTime(마지막 수정 날짜)을 사용하고, '마지막으로 내용이 변경된 이후 경과 기간'으로 부서에 제시하는 것이 안전합니다. 보관(아카이브) 여부는 기계적으로 판단하지 말고, 목록을 관계자에게 확인받은 뒤 진행하세요.
Get-Acl로 무엇을 알 수 있나요? SDDL을 읽어야 하나요?
Get-Acl은 파일이나 폴더의 보안 설명자를 가져오며, Access 속성에서 '누가(IdentityReference)', '무엇을(FileSystemRights)', '허용인지 거부인지(AccessControlType)', '상속인지 직접 부여인지(IsInherited)'를 읽을 수 있습니다. AreAccessRulesProtected를 보면 해당 폴더에서 상속이 끊겨 있는지도 알 수 있습니다. SDDL이라는 문자열 형식도 있지만, 재고 조사 목적이라면 Access 속성을 CSV로 출력하는 편이 관계자와 공유하기 쉬우며 SDDL까지 깊이 파고들 필요는 없습니다.
ACL 변경은 PowerShell의 Set-Acl과 icacls 중 어느 쪽으로 해야 하나요?
조사·리포트 작성은 객체로 다룰 수 있는 Get-Acl, 변경 실무는 icacls를 첫 번째 후보로 삼는 것이 구분 사용의 기준입니다. icacls는 /save로 ACL을 파일에 저장하고 /restore로 그대로 되돌릴 수 있어, 변경 전 백업과 롤백이 간단합니다. Set-Acl을 사용하는 경우에는 Get-Acl로 가져오기 → 규칙 편집 → 적용의 3단계로 진행하며, -WhatIf로 대상을 확인한 뒤 실행합니다. 어느 쪽이든 재고 조사와 관련 부서 확인이 끝난 뒤에 변경으로 넘어가는 것이 원칙입니다.
파일 서버 조사는 UNC 경로를 통해 실행해도 되나요?
소량이라면 문제없지만, 수십만 개 파일 규모의 전체 스캔은 네트워크를 통하면 크게 느려집니다. 가능하다면 파일 서버에서 직접(또는 PowerShell Remoting으로) 실행하고, 결과 CSV만 가져오는 구성이 효율적입니다. 또한 스캔 결과는 실행 계정의 권한에 좌우됩니다. 액세스할 수 없는 폴더는 나열에서 빠지므로, 관리자 계정으로 실행한 뒤에도 거부된 위치를 기록해 '조사하지 못한 위치'로 보고서에 포함하세요.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기