C++ Regex | "정규 표현식" 가이드

C++ Regex | "정규 표현식" 가이드

이 글의 핵심

C++ Regex에 대한 실전 가이드입니다.

들어가며

**C++11 <regex>**는 정규 표현식을 지원하는 표준 라이브러리입니다. 패턴 매칭, 검색, 치환 등을 통해 문자열 처리를 강력하고 유연하게 수행할 수 있습니다.


1. Regex 기본

기본 사용

정규 표현식으로 이메일 형식을 검증하는 간단한 예제입니다:

#include <regex>
#include <iostream>
#include <string>

int main() {
    std::string text = "[email protected]";
    
    // 정규 표현식 패턴 생성
    // R"(...)" : Raw 문자열 리터럴 (이스케이프 불필요)
    // (\w+) : 첫 번째 캡처 그룹 - 하나 이상의 단어 문자 (알파벳, 숫자, _)
    // @ : 리터럴 @ 문자
    // (\w+\.\w+) : 두 번째 캡처 그룹 - 도메인 (예: example.com)
    //   \w+ : 하나 이상의 단어 문자
    //   \. : 리터럴 점 (. 은 특수문자이므로 이스케이프 필요)
    //   \w+ : 하나 이상의 단어 문자
    std::regex pattern{R"((\w+)@(\w+\.\w+))"};
    
    // regex_match: 전체 문자열이 패턴과 일치하는지 확인
    // 반환값: bool (일치하면 true, 아니면 false)
    if (std::regex_match(text, pattern)) {
        std::cout << "이메일 형식입니다" << std::endl;
    }
    
    return 0;
}

패턴 해석:

  • \w : 단어 문자 (a-z, A-Z, 0-9, _)
  • + : 1개 이상 반복
  • \. : 리터럴 점 (이스케이프 필요)
  • () : 캡처 그룹 (나중에 추출 가능)

Raw 문자열 리터럴

#include <regex>

// ❌ 이스케이프 지옥
std::regex pattern1{"\\d+\\.\\d+"};

// ✅ Raw 문자열 (권장)
std::regex pattern2{R"(\d+\.\d+)"};

// 복잡한 패턴
std::regex email{R"(^[\w\.-]+@[\w\.-]+\.\w+$)"};

2. Regex 함수

regex_match (전체 매치)

regex_match전체 문자열이 패턴과 정확히 일치해야 true를 반환합니다:

#include <regex>
#include <iostream>

int main() {
    std::string text1 = "123";
    std::string text2 = "abc123";
    
    // \d+ : 하나 이상의 숫자
    std::regex pattern{R"(\d+)"};
    
    // text1 = "123" : 전체가 숫자 → 매치 성공
    std::cout << std::regex_match(text1, pattern) << std::endl;  // 1 (true)
    
    // text2 = "abc123" : "abc"가 있어서 전체가 숫자가 아님 → 매치 실패
    std::cout << std::regex_match(text2, pattern) << std::endl;  // 0 (false)
    
    // regex_match는 문자열 처음부터 끝까지 전체가 패턴과 일치해야 함
    // 부분 일치는 인정하지 않음
    
    return 0;
}

사용 시나리오:

  • 입력 검증: 전화번호, 이메일, 우편번호 등이 정확한 형식인지 확인
  • 파일 확장자 검사: 파일명 전체가 특정 패턴인지 확인

regex_search (부분 매치)

regex_search는 문자열 어딘가에 패턴이 있으면 true를 반환합니다:

#include <regex>
#include <iostream>

int main() {
    std::string text = "C++ 2026";
    
    // \d+ : 하나 이상의 숫자
    std::regex pattern{R"(\d+)"};
    
    // "C++ 2026"에서 "2026" 부분이 패턴과 일치 → 성공
    if (std::regex_search(text, pattern)) {
        std::cout << "숫자 발견" << std::endl;  // 출력됨
    }
    
    // regex_search는 문자열 전체를 순회하며 패턴을 찾음
    // 처음 발견된 매치를 반환
    // 여러 매치를 찾으려면 sregex_iterator 사용
    
    return 0;
}

regex_match vs regex_search 비교:

함수동작예시
regex_match전체 문자열이 패턴과 일치"123"\d+ → true
"abc123"\d+ → false
regex_search부분 문자열이 패턴과 일치"123"\d+ → true
"abc123"\d+ → true

사용 시나리오:

  • 로그 파싱: 로그 파일에서 에러 메시지 찾기
  • 텍스트 검색: 문서에서 특정 패턴 찾기
  • 데이터 추출: HTML/XML에서 태그 내용 추출

regex_replace (치환)

#include <regex>
#include <iostream>

int main() {
    std::string text = "Hello World 2026";
    std::regex pattern{R"(\d+)"};
    
    // 숫자를 [숫자]로 치환
    std::string result = std::regex_replace(text, pattern, "[$&]");
    std::cout << result << std::endl;  // Hello World [2026]
    
    return 0;
}

3. 캡처 그룹

기본 캡처

#include <regex>
#include <iostream>

int main() {
    std::string text = "2026-03-29";
    std::regex pattern{R"((\d{4})-(\d{2})-(\d{2}))"};
    
    std::smatch matches;
    if (std::regex_match(text, matches, pattern)) {
        std::cout << "전체: " << matches[0] << std::endl;  // 2026-03-29
        std::cout << "년: " << matches[1] << std::endl;    // 2026
        std::cout << "월: " << matches[2] << std::endl;    // 03
        std::cout << "일: " << matches[3] << std::endl;    // 29
    }
    
    return 0;
}

이름있는 캡처 (C++11에서는 미지원)

#include <regex>
#include <iostream>

int main() {
    std::string text = "[email protected]";
    std::regex pattern{R"((\w+)@(\w+\.\w+))"};
    
    std::smatch matches;
    if (std::regex_match(text, matches, pattern)) {
        std::string username = matches[1];
        std::string domain = matches[2];
        
        std::cout << "사용자: " << username << std::endl;  // user
        std::cout << "도메인: " << domain << std::endl;    // example.com
    }
    
    return 0;
}

4. 반복자 (Iterator)

모든 매치 찾기

#include <regex>
#include <iostream>
#include <string>

int main() {
    std::string text = "C++ 11, C++ 14, C++ 17, C++ 20";
    std::regex pattern{R"(C\+\+ (\d+))"};
    
    // 모든 매치 반복
    auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
    auto end = std::sregex_iterator();
    
    for (auto it = begin; it != end; ++it) {
        std::smatch match = *it;
        std::cout << "전체: " << match.str() << std::endl;
        std::cout << "버전: " << match[1] << std::endl;
    }
    
    // 출력:
    // 전체: C++ 11
    // 버전: 11
    // 전체: C++ 14
    // 버전: 14
    // ...
    
    return 0;
}

토큰 반복자

#include <regex>
#include <iostream>

int main() {
    std::string text = "apple,banana,cherry";
    std::regex pattern{R"(,)"};
    
    // 구분자로 분리
    std::sregex_token_iterator begin(text.begin(), text.end(), pattern, -1);
    std::sregex_token_iterator end;
    
    for (auto it = begin; it != end; ++it) {
        std::cout << *it << std::endl;
    }
    
    // 출력:
    // apple
    // banana
    // cherry
    
    return 0;
}

5. 실전 예제

예제 1: 이메일 검증

#include <regex>
#include <iostream>
#include <string>

bool isValidEmail(const std::string& email) {
    // 이메일 패턴 (간단 버전)
    std::regex pattern{R"(^[\w\.-]+@[\w\.-]+\.\w{2,}$)"};
    return std::regex_match(email, pattern);
}

int main() {
    std::string emails[] = {
        "[email protected]",
        "invalid.email",
        "[email protected]",
        "@example.com"
    };
    
    for (const auto& email : emails) {
        std::cout << email << ": " 
                  << (isValidEmail(email) ? "유효" : "무효") 
                  << std::endl;
    }
    
    return 0;
}

예제 2: URL 파싱

#include <regex>
#include <iostream>

struct URL {
    std::string protocol;
    std::string domain;
    std::string path;
};

URL parseURL(const std::string& url) {
    std::regex pattern{R"(^(https?)://([^/]+)(/.*)?$)"};
    std::smatch matches;
    
    if (std::regex_match(url, matches, pattern)) {
        return {
            matches[1].str(),  // protocol
            matches[2].str(),  // domain
            matches[3].str()   // path
        };
    }
    
    return {};
}

int main() {
    std::string url = "https://example.com/path/to/page";
    URL parsed = parseURL(url);
    
    std::cout << "프로토콜: " << parsed.protocol << std::endl;
    std::cout << "도메인: " << parsed.domain << std::endl;
    std::cout << "경로: " << parsed.path << std::endl;
    
    return 0;
}

예제 3: 로그 파싱

#include <regex>
#include <iostream>
#include <string>
#include <vector>

struct LogEntry {
    std::string timestamp;
    std::string level;
    std::string message;
};

std::vector<LogEntry> parseLog(const std::string& log) {
    std::vector<LogEntry> entries;
    
    // 로그 패턴: [2026-03-29 14:30:25] [INFO] Message
    std::regex pattern{R"(\[([^\]]+)\] \[(\w+)\] (.+))"};
    
    std::istringstream stream(log);
    std::string line;
    
    while (std::getline(stream, line)) {
        std::smatch matches;
        if (std::regex_match(line, matches, pattern)) {
            entries.push_back({
                matches[1].str(),  // timestamp
                matches[2].str(),  // level
                matches[3].str()   // message
            });
        }
    }
    
    return entries;
}

int main() {
    std::string log = 
        "[2026-03-29 14:30:25] [INFO] Server started\n"
        "[2026-03-29 14:30:26] [ERROR] Connection failed\n"
        "[2026-03-29 14:30:27] [DEBUG] Retrying...";
    
    auto entries = parseLog(log);
    
    for (const auto& entry : entries) {
        std::cout << entry.timestamp << " [" << entry.level << "] " 
                  << entry.message << std::endl;
    }
    
    return 0;
}

6. 자주 발생하는 문제

문제 1: 이스케이프

#include <regex>

// ❌ 이스케이프 지옥
std::regex pattern1{"\\d+\\.\\d+"};

// ✅ Raw 문자열 리터럴
std::regex pattern2{R"(\d+\.\d+)"};

// 복잡한 패턴도 간결
std::regex email{R"(^[\w\.-]+@[\w\.-]+\.\w+$)"};

문제 2: 성능

#include <regex>
#include <iostream>
#include <chrono>
#include <vector>

int main() {
    std::vector<std::string> texts = {"test1", "test2", "test3"};
    
    // ❌ 매번 regex 생성 (느림)
    auto start1 = std::chrono::high_resolution_clock::now();
    for (const auto& text : texts) {
        std::regex pattern{R"(\d+)"};  // 매번 생성!
        std::regex_search(text, pattern);
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    
    // ✅ regex 재사용 (빠름)
    auto start2 = std::chrono::high_resolution_clock::now();
    std::regex pattern{R"(\d+)"};  // 한 번만 생성
    for (const auto& text : texts) {
        std::regex_search(text, pattern);
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    
    auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1).count();
    auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2).count();
    
    std::cout << "매번 생성: " << duration1 << " μs" << std::endl;
    std::cout << "재사용: " << duration2 << " μs" << std::endl;
    
    return 0;
}

해결책: std::regex 객체를 재사용하세요.

문제 3: 예외 처리

#include <regex>
#include <iostream>

int main() {
    try {
        // ❌ 잘못된 정규식
        std::regex pattern{"[invalid"};  // std::regex_error
        
    } catch (const std::regex_error& e) {
        std::cerr << "정규식 에러: " << e.what() << std::endl;
        std::cerr << "에러 코드: " << e.code() << std::endl;
    }
    
    return 0;
}

문제 4: 플래그

#include <regex>
#include <iostream>

int main() {
    std::string text = "Hello World";
    
    // 대소문자 구분 (기본)
    std::regex pattern1{"hello"};
    std::cout << std::regex_search(text, pattern1) << std::endl;  // 0 (false)
    
    // 대소문자 무시
    std::regex pattern2{"hello", std::regex::icase};
    std::cout << std::regex_search(text, pattern2) << std::endl;  // 1 (true)
    
    return 0;
}

7. 정규식 문법 (ECMAScript)

문자 클래스

\d  // 숫자 [0-9]
\D  // 비숫자
\w  // 단어 문자 [a-zA-Z0-9_]
\W  // 비단어 문자
\s  // 공백 [ \t\n\r\f\v]
\S  // 비공백
.   // 모든 문자 (개행 제외)

수량자

*   // 0개 이상
+   // 1개 이상
?   // 0 또는 1개
{n}   // 정확히 n개
{n,}  // n개 이상
{n,m} // n개 이상 m개 이하

앵커

^   // 문자열 시작
$   // 문자열 끝
\b  // 단어 경계
\B  // 비단어 경계

그룹

()    // 캡처 그룹
(?:)  // 비캡처 그룹
|     // OR
[]    // 문자 클래스

8. 실전 예제: 텍스트 처리 유틸리티

#include <regex>
#include <iostream>
#include <string>
#include <vector>

class TextProcessor {
public:
    // 이메일 추출
    static std::vector<std::string> extractEmails(const std::string& text) {
        std::vector<std::string> emails;
        std::regex pattern{R"([\w\.-]+@[\w\.-]+\.\w+)"};
        
        auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
        auto end = std::sregex_iterator();
        
        for (auto it = begin; it != end; ++it) {
            emails.push_back(it->str());
        }
        
        return emails;
    }
    
    // 전화번호 추출 (한국 형식)
    static std::vector<std::string> extractPhones(const std::string& text) {
        std::vector<std::string> phones;
        std::regex pattern{R"(\d{2,3}-\d{3,4}-\d{4})"};
        
        auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
        auto end = std::sregex_iterator();
        
        for (auto it = begin; it != end; ++it) {
            phones.push_back(it->str());
        }
        
        return phones;
    }
    
    // HTML 태그 제거
    static std::string removeHTMLTags(const std::string& html) {
        std::regex pattern{R"(<[^>]*>)"};
        return std::regex_replace(html, pattern, "");
    }
    
    // 연속 공백 제거
    static std::string normalizeSpaces(const std::string& text) {
        std::regex pattern{R"(\s+)"};
        return std::regex_replace(text, pattern, " ");
    }
    
    // URL 검증
    static bool isValidURL(const std::string& url) {
        std::regex pattern{R"(^https?://[\w\.-]+\.\w{2,}(/.*)?$)"};
        return std::regex_match(url, pattern);
    }
};

int main() {
    std::string text = "Contact: [email protected] or call 010-1234-5678";
    
    // 이메일 추출
    auto emails = TextProcessor::extractEmails(text);
    std::cout << "이메일:" << std::endl;
    for (const auto& email : emails) {
        std::cout << "  " << email << std::endl;
    }
    
    // 전화번호 추출
    auto phones = TextProcessor::extractPhones(text);
    std::cout << "전화번호:" << std::endl;
    for (const auto& phone : phones) {
        std::cout << "  " << phone << std::endl;
    }
    
    // HTML 태그 제거
    std::string html = "<p>Hello <b>World</b></p>";
    std::string plain = TextProcessor::removeHTMLTags(html);
    std::cout << "태그 제거: " << plain << std::endl;
    
    // 공백 정규화
    std::string messy = "Hello    World   !";
    std::string clean = TextProcessor::normalizeSpaces(messy);
    std::cout << "공백 정규화: " << clean << std::endl;
    
    // URL 검증
    std::cout << "URL 검증: " 
              << (TextProcessor::isValidURL("https://example.com") ? "유효" : "무효") 
              << std::endl;
    
    return 0;
}

정리

핵심 요약

  1. regex: C++11 정규 표현식 라이브러리
  2. regex_match: 전체 문자열 매치
  3. regex_search: 부분 문자열 검색
  4. regex_replace: 패턴 치환
  5. 캡처 그룹: () 로 부분 추출
  6. 반복자: 모든 매치 찾기

Regex 함수 비교

함수용도반환 타입사용 시기
regex_match전체 매치bool검증 (이메일, URL)
regex_search부분 매치bool검색 (숫자 찾기)
regex_replace치환string변환 (마스킹, 정리)
sregex_iterator모든 매치반복자추출 (모든 이메일)

실전 팁

성능:

  • std::regex 객체 재사용 (생성 비용 큼)
  • 간단한 패턴은 std::string 함수 사용
  • 대량 처리는 전용 라이브러리 고려 (RE2, PCRE)

가독성:

  • Raw 문자열 리터럴 사용 (R"(...)")
  • 복잡한 패턴은 주석으로 설명
  • 캡처 그룹에 의미있는 변수명

안전성:

  • 예외 처리 (std::regex_error)
  • 입력 검증 (길이 제한)
  • 타임아웃 고려 (복잡한 패턴)

다음 단계

  • C++ Regex Iterator
  • C++ String
  • C++ Algorithm Replace

관련 글

  • C++ 정규표현식 |