PowerShell hashtable duplicate keys 대소문자 중복 에러 해결

#510

2 min read · 438 words

PowerShell hashtable duplicate keys 에러로 인해 텍스트 일괄 치환 작업이 막힌 분들을 위한 글입니다. 대소문자를 구분하지 않는 PowerShell 해시 테이블의 한계를 우회하여 작업을 즉시 해결하는 방법을 소개합니다. (운영 기록 기준)

마치며 — 핵심 정리와 생각할 거리

핵심 결론

이번 문제는 PowerShell의 해시 테이블이 기본적으로 대소문자를 구분하지 않는(case-insensitive) 키 비교 방식을 채택하고 있다는 근본적인 특성에서 비롯되었습니다. 이는 특정 상황, 특히 대소문자만 다른 키를 서로 다른 엔티티로 취급해야 하는 텍스트 치환과 같은 작업에서 치명적인 제약으로 작용합니다. 따라서 PowerShell 내부에서 이 문제를 우회하려 시도하는 것보다는, 텍스트 처리 및 정규 표현식에 특화된 Python과 같은 도구를 활용하여 문제의 본질인 '대소문자 구분 없는 매칭'을 직접적으로 해결하는 것이 가장 효율적이고 견고한 접근 방식임을 확인했습니다.

더 생각해볼 것들

이 문제를 해결하고 나면 자연스럽게 떠오르는 질문들이 있습니다.

  • PowerShell의 대소문자 구분 정책의 배경은 무엇일까요? — PowerShell이 Windows 환경에 깊이 뿌리내리고 있다는 점을 고려할 때, 파일 시스템 경로 등 Windows의 대소문자 비구분 특성을 반영한 설계일 가능성이 높습니다. 이러한 설계 철학이 다른 데이터 구조에도 일관되게 적용된 것인지, 그리고 이로 인해 발생할 수 있는 다른 잠재적 문제점은 없는지 탐구해볼 필요가 있습니다.
  • .NET Framework의 Dictionary 클래스는 어떨까요? — PowerShell은 .NET Framework 위에 구축되어 있습니다. 그렇다면 .NET의 System.Collections.Generic.Dictionary 클래스는 키의 대소문자 구분 여부를 어떻게 처리하는지, 그리고 PowerShell에서 이 클래스를 직접 활용할 때 대소문자 구분 옵션을 제어할 수 있는 방법은 없는지 심층적으로 조사해볼 수 있습니다. 이는 PowerShell의 한계를 .NET 수준에서 극복할 수 있는 가능성을 열어줄 수 있습니다.
  • 성능 측면에서 Python으로의 우회는 항상 최적일까요? — 이번 사례에서는 Python으로의 우회가 성공적이었지만, PowerShell 스크립트 내에서 외부 프로세스를 호출하는 것은 추가적인 오버헤드를 발생시킬 수 있습니다. 처리해야 할 데이터의 양이 매우 방대하거나, 호출 빈도가 극도로 높은 경우, 이러한 오버헤드가 전체 시스템 성능에 미치는 영향은 없는지, 그리고 이 경우 PowerShell 내에서 대소문자를 구분하는 다른 컬렉션 타입을 사용하는 것이 더 효율적일 수 있는지 비교 분석해볼 필요가 있습니다.

응용 가능한 상황

이 해결책은 단순히 hex color string 치환을 넘어, 대소문자 구분 문제로 인해 발생하는 다양한 텍스트 처리 작업에 응용될 수 있습니다. 다음은 몇 가지 시나리오와 그에 대한 코드 예시입니다.

1. 설정 파일 내 특정 키-값 쌍 대소문자 무시하고 업데이트

INI 파일이나 유사한 텍스트 기반 설정 파일에서 특정 설정 값을 업데이트할 때, 키의 대소문자가 일관되지 않아 문제가 발생하는 경우 유용합니다. 예를 들어, 'DebugMode'와 'debugmode'를 동일하게 취급하여 값을 변경할 수 있습니다.

import re

config_content = """
[General]
DebugMode=True
LogLevel=INFO
debugmode=False
"""

# 'DebugMode' 키의 값을 'False'로 변경 (대소문자 무시)
old_key_pattern = re.compile(r'^(DebugMode\s*=\s*)(.*)$', re.I | re.M)
new_config_content = old_key_pattern.sub(r'\1False', config_content)

print(new_config_content)
# 예상 출력:
# [General]
# DebugMode=False
# LogLevel=INFO
# debugmode=False

2. 로그 파일에서 특정 오류 메시지 패턴 대소문자 무시하고 검색 및 치환

로그 파일을 분석할 때, 'Error', 'ERROR', 'error' 등 대소문자가 혼용된 오류 메시지 패턴을 일괄적으로 표준화하거나 특정 정보로 치환해야 할 때 활용할 수 있습니다. 예를 들어, 모든 'Error' 관련 메시지를 'CRITICAL_ISSUE'로 변경할 수 있습니다.

import re

log_data = """
2023-10-26 10:00:01 - INFO - Application started.
2023-10-26 10:00:05 - ERROR - Database connection failed.
2023-10-26 10:00:10 - warning - Low disk space.
2023-10-26 10:00:15 - error - File not found.
"""

# 'error' 또는 'ERROR'를 'CRITICAL_ISSUE'로 치환
error_pattern = re.compile(r'(\b)(error)(\b)', re.I)
standardized_log_data = error_pattern.sub(r'\1CRITICAL_ISSUE\3', log_data)

print(standardized_log_data)
# 예상 출력:
# 2023-10-26 10:00:01 - INFO - Application started.
# 2023-10-26 10:00:05 - CRITICAL_ISSUE - Database connection failed.
# 2023-10-26 10:00:10 - warning - Low disk space.
# 2023-10-26 10:00:15 - CRITICAL_ISSUE - File not found.

3. 웹 스크래핑 시 HTML 태그나 속성 값 대소문자 무시하고 처리

HTML은 태그나 속성 이름의 대소문자를 구분하지 않는 경우가 많습니다. 웹 스크래핑 시 특정 태그나 속성 값을 찾거나 변경할 때, 대소문자 구분 없이 처리해야 할 필요가 있습니다. 예를 들어, <a href="..."> 태그를 찾을 때 <A HREF="...">도 함께 처리할 수 있습니다.

import re

html_content = """
<div id="mainContent">
  <a href="/page1">Link 1</a>
  <A HREF="/page2">Link 2</A>
  <img SRC="/image.png">
</div>
"""

# 모든 <a> 태그의 href 속성 값을 대소문자 구분 없이 추출
# (간단화를 위해 정규식 패턴은 실제 HTML 파싱 라이브러리보다 덜 견고할 수 있음)
href_pattern = re.compile(r'<a\s+href=["\'](.*?)["\']', re.I)
all_links = href_pattern.findall(html_content)

print(all_links)
# 예상 출력: ['/page1', '/page2']

경우의 수로 보는 이 버그

이 버그/문제는 다음과 같은 환경 조건들의 조합에서 재현됩니다.

  • 운영체제: Windows (PowerShell의 기본 환경)
  • 스크립팅 언어: PowerShell
  • 데이터 구조: 해시 테이블 (@{} 리터럴 또는 New-Object System.Collections.Hashtable)
  • 키의 특성: 대소문자만 다른 중복 키를 사용하려는 시도 (예: '#1e2a45''#1E2A45')

이러한 4가지 조건이 모두 충족될 때 이 문제가 발생합니다. 즉, Windows OS × PowerShell × Hashtable × Case-sensitive-duplicate-keys의 단일한 환경 조합에서 이 버그가 재현됩니다. 만약 이 중 하나라도 조건이 변경된다면 (예: PowerShell 대신 Python 사용, 해시 테이블 대신 배열 사용, 키에 대소문자 중복이 없음), 이 문제는 발생하지 않습니다. 이는 PowerShell의 특정 설계 철학과 사용 패턴이 결합될 때 나타나는 전형적인 사례이며, 다른 스크립팅 언어나 운영체제에서는 동일한 방식으로 재현되지 않을 가능성이 높습니다.

문제 상황

내가 시도했던 작업은 PowerShell을 사용해 문서 내의 특정 한국어 hex color string을 일괄적으로 치환(replace)하는 것이었다. 대소문자가 혼용된 hex color 값을 매핑하기 위해 다음과 같이 해시 테이블을 정의했다.

$replacements = @{'#1e2a45'='#f6f7f8'; '#1E2A45'='#f6f7f8'}

하지만 이 코드를 실행하자마자 해시 테이블 정의 단계에서 에러가 발생하며 스크립트가 멈췄다.

PowerShell hashtable duplicate keys 에러 증상

실행 시 화면에 출력된 정확한 에러 메시지는 다음과 같다.

Duplicate keys '#1E2A45' are not allowed in hash literals

정확한 원인은 추가 확인이 필요하지만, 현재 확인된 원인은 다음과 같습니다. PowerShell은 기본적으로 해시 테이블의 키를 대소문자 구분 없이(case-insensitive) 비교합니다. 이 때문에 #1e2a45#1E2A45를 서로 다른 키가 아닌 동일한 키의 중복 정의로 인식하여 예외를 발생시켰습니다.

환경

  • OS: Windows
  • 도구 버전: Windows PowerShell 5.1
  • 자동화 stack: hashtable literal (@{})

시도했지만 실패한 방법

내가 처음 시도한 우회책은 리터럴 표기법(@{}) 대신 .NET 객체를 직접 생성하는 New-Object System.Collections.Hashtable 방식을 사용하는 것이었다. 하지만 이 역시 내부적으로 동일한 대소문자 구분 없는 비교 메커니즘을 사용했기에 동일한 에러가 발생하며 실패했다.

대소문자 구분 좀 해달라고 빌었지만, PowerShell은 끝까지 내 말을 듣지 않고 에러만 뿜어냈다. 역시 고집 센 도구는 억지로 설득하는 게 아니다.

최종 해결

결국 PowerShell을 고집하는 대신, 텍스트 처리에 더 유연한 Python으로 우회하여 문제를 해결했다. Python의 re 모듈을 사용해 대소문자 구분 없이 매칭하는 re.compile(re.escape(old), re.I) 방식을 적용했다. 이를 통해 대소문자가 다른 동일 hex color 값들을 한 번에 안전하게 치환할 수 있었다.

사용한 코드

# 치환할 대상 리스트 (운영 기록 기준)
swaps = [
 ('#1e2a45', '#f6f7f8'),
 ('#1E2A45', '#f6f7f8')
]

# 대소문자 구분 없이 치환 수행
for old, new in swaps:
 pat = re.compile(re.escape(old), re.I)
 content = pat.sub(new, content)

검증 결과

내가 작성한 Python 스크립트를 실행하여 총 131개의 hex swap 대상을 대소문자 구분 없이 단 1회 실행만으로 완벽하게 치환 완료했다. 중복 키 에러 없이 깔끔하게 작업이 끝난 것을 검증했다.

현재 상태

fixed (해결 완료)

같은 문제 겪는 분들에게

PowerShell hashtable duplicate keys 에러로 고통받고 있다면, 굳이 PowerShell 내부에서 복잡한 .NET Dictionary 선언을 하며 힘 빼지 마라. 텍스트 치환 작업이라면 Python의 re.I 옵션을 사용하는 것이 정신 건강과 생산성 측면에서 훨씬 빠르고 확실한 길이다.

카테고&#?; &#?;&#?;

이 글은 한국Ǻ?; AI 디버깅 아카이브 방향에 &#?;추Ǻ?; 정&#?;된 운영 기&#?;입니&#?;.

ToolSignal Pro Editorial

Claude · GPT · Antigravity · Cursor 실전 오류와 해결을 5개 언어로 정리한 AI debugging archive.

이전 글 다음 글