문자열 연산: 패턴 매칭, 성능, 그리고 span 기반 검색
문자열 연산: 패턴 매칭, 성능, 그리고 span 기반 검색
C#에서 문자열을 다루는 일은 단순히 붙이고 자르는 것에서 그치지 않아요. 이 글에서는 세 가지 문자열 연산을 차례로 살펴봅니다. 정규식으로 패턴을 매칭하는 방법, 할당 없이 문자열을 검색하는 ReadOnlySpan<char> 활용법, 그리고 비교를 빠르고 정확하게 만들어 주는 StringComparison 선택 기준이 바로 그것이에요.
출처: Microsoft Learn
본문
정규식으로 특정 텍스트 찾기
고정된 부분 문자열이 아니라 어떤 *모양(pattern)*을 기준으로 문자열을 검색할 때는 xref:System.Text.RegularExpressions.Regex?displayProperty=nameWithType 클래스를 써요. 정적 메서드 xref:System.Text.RegularExpressions.Regex.IsMatch*?displayProperty=nameWithType는 입력 문자열과 패턴, 그리고 선택적으로 xref:System.Text.RegularExpressions.RegexOptions 플래그를 받습니다.
아래 예제는 각 문장에서 단어 the 또는 their 를 대소문자 구분 없이 찾아요. 패턴 the(ir)?\s 는 the 뒤에 선택적으로 ir 이 오고, 그다음 공백 문자가 오는 경우를 매칭합니다.
| 패턴 | 의미 |
|---|---|
the |
문자 그대로의 텍스트 the 를 매칭 |
(ir)? |
ir 의 0회 또는 1회 등장을 매칭 |
\s |
공백 문자 하나를 매칭 |
:::code language="csharp" source="snippets/string-operations/Program.cs" id="RegexPattern":::
문자열을 패턴에 맞는지 검증하기
입력 전체가 특정 모양에 들어맞는지 확인하려면 패턴을 ^ 와 $ 로 묶어 주면 돼요. 아래 예제는 각 문자열이 미국식 전화번호 형태인지 검증해요. 숫자 세 자리, 세 자리, 네 자리이며 대시로 구분되는 형식이죠.
| 패턴 | 의미 |
|---|---|
^ |
문자열의 시작을 매칭 |
\d{3} |
정확히 세 개의 숫자 문자를 매칭 |
- |
리터럴 - 문자 하나를 매칭 |
\d{4} |
정확히 네 개의 숫자 문자를 매칭 |
$ |
문자열의 끝을 매칭 |
:::code language="csharp" source="snippets/string-operations/Program.cs" id="RegexValidate":::
전체 패턴 문법은 Regular expression language - quick reference 문서에서 자세히 볼 수 있어요.
string 메서드와 정규식 중 무엇을 쓸까
string 메서드와 Regex 는 겹치는 문제를 해결하는 경우가 많아요. 찾으려는 텍스트가 리터럴 값이거나, 미리 아는 접두사·접미사, 또는 고정된 구분자라면 string 메서드를 쓰는 편이 좋아요. 패턴을 컴파일하고 실행하는 비용을 치르지 않으니 읽기 쉽고 더 빠르죠. 반대로 검색 대상이 모양 — 대체(alternation), 선택적 그룹, 반복 문자 클래스, 앵커로 묶은 검증 같은 것 — 일 때는 Regex 를 꺼내는 게 맞아요. 실용적인 기준은 이렇습니다. 검색을 string.Contains / StartsWith / IndexOf 호출 한두 개로 쓸 수 있다면 그렇게 하세요.
ReadOnlySpan<char> 로 검색하기
큰 입력을 파싱하거나 핫 패스(hot path)에서 검색을 돌리다 보면, 호출 때마다 할당되는 string.Substring 과 string.Split 의 비용이 지배적이게 돼요. ReadOnlySpan<char> 는 기존 문자열(또는 배열, 스택 버퍼) 위에 뷰를 만들어 주는데 복사가 일어나지 않죠. 그리고 xref:System.MemoryExtensions 가 일반적인 string 메서드의 span 기반 버전을 제공하는데, 그중 하나가 xref:System.MemoryExtensions.IndexOf* 입니다.
:::code language="csharp" source="snippets/string-operations/Program.cs" id="SpanSearch":::
span 기반 검색이 할당을 피하는 이유는, 슬라이스(input[start..], rest[..end])가 단순히 원본 문자의 창(window)이기 때문이에요. 같은 방식은 키-값 목록, 헤더, 그리고 기타 구분자로 나뉜 텍스트를 파싱하는 데까지 자연스럽게 확장되는데, 그 과정에서 Substring 을 한 번도 호출하지 않아도 됩니다.
StringComparison 의 성능 고려 사항
대부분의 string 인스턴스 메서드에는 xref:System.StringComparison 값을 받는 오버로드가 있어요. xref:System.String.Equals(System.String)?displayProperty=nameWithType 같은 메서드는 기본적으로 ordinal 비교를 쓰지만, xref:System.String.Compare(System.String,System.String)?displayProperty=nameWithType 와 xref:System.String.IndexOf(System.String)?displayProperty=nameWithType 는 기본적으로 현재 문화권(current culture) 을 기준으로 해요. 이 차이는 두 가지 측면에서 중요합니다.
- 속도. Ordinal 비교는 바이트 대 바이트로 비교하는 테스트라서 벡터화된 타이트한 루프 안에서 실행돼요. 반대로 문화권 인식 비교는 정렬 테이블을 참조하고, 결합 문자를 훑고, 로캘별 규칙을 적용하죠. 같은 입력이라도 한 자릿수만큼 느려질 수 있어요.
- 정확성. 문화권 인식 비교는 예상하지 못한 문자를 병합할 수 있어요. 터키어의
i/I, 독일어의ß가ss로, 합자(ligature) 같은 것들이죠. 이 동작은 사용자가 보는 이름을 정렬할 때는 올바르지만, 식별자·경로·프로토콜 토큰을 파싱할 때는 잘못된 결과를 낳아요.
파일 이름, URL, HTTP 헤더, 식별자, 구성 키처럼 기계가 정의한 텍스트에는 xref:System.StringComparison.Ordinal?displayProperty=nameWithType 또는 xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType 를 명시적으로 전달하세요. 문화권 인식 값은 사용자에게 보여 주는 자연어 텍스트를 위해 아껴 두는 게 좋아요. 체계적인 지침은 Best practices for comparing strings in .NET 문서를 참고합니다.
더 알아보기
- Regular expression language — quick reference
- Best practices for comparing strings in .NET
- Search strings
- Split strings into substrings
- xref:System.Text.RegularExpressions.Regex?displayProperty=nameWithType
- xref:System.MemoryExtensions?displayProperty=nameWithType
- xref:System.StringComparison?displayProperty=nameWithType