개발자 면접 공부/C-C++

정규 표현식

chogyujin 2025. 12. 18. 14:46
728x90

1. 개요

오늘은 정규 표현식에 대해 알아보도록 하겠습니다.


2. 정규 표현식란?

C++ 11부터 표준 라이브러리<regex>로 정규 표현식이 도입되면서, 복잡한 문자열 처리가 훨씬 가편하고 강력하다.


3. 왜 사용하는가?

과거 C++에서 문자열을 다룰 때는 find, substr, strtok 등을 사용하여 수동으로 파싱해야 했습니다. 이는 코드가 길어지고 가독성이 떨어지며 버그가 발생하기 쉬웠습니다.

정규 표현식을 사용하면 다음과 같은 이점이 있습니다.

  • 생산성 향상: 수십 줄의 문자열 처리 코드를 단 몇 줄의 패턴으로 대체할 수 있습니다.
  • 강력한 검증: 이메일, 전화번호, 주민등록번호 등 복잡한 형식을 쉽게 검증할 수 있습니다.
  • 유연한 검색 및 치환: 텍스트 내에서 특정 패턴을 찾아 추출하거나 다른 형태로 바꾸는 작업이 매우 효율적입니다.

4. 핵심 컴포넌트

C++ 정규 표현식을 사용하려면 <regex> 헤더를 포함해야 합니다. 주요 클래스와 함수는 다음과 같습니다.

  • std::regex: 정규 표현식 패턴을 담는 객체입니다.
  • std::smatch: 문자열 검색 결과를 저장하는 컨테이너입니다. (string match)
  • std::regex_match: 문자열 전체가 패턴과 일치하는지 검사합니다. (주로 유효성 검증용)
  • std::regex_search: 문자열 일부에 패턴과 일치하는 부분이 있는지 찾습니다.
  • std::regex_replace: 패턴과 일치하는 부분을 다른 문자열로 변경합니다.

5. 코드

1. 유효성 검사 (이메일)

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

int main()
{
    std::string email = "user@example.com";
    
    // 간단한 이메일 패턴 (실무에서는 더 정교한 패턴 사용 권장)
    std::regex email_pattern(R"(\w+@\w+\.\w+)");

    // regex_match: 문자열 전체가 패턴과 일치해야 true
    if (std::regex_match(email, email_pattern)) 
    {
        std::cout << "유효한 이메일입니다." << std::endl;
    } else 
    {
        std::cout << "잘못된 이메일 형식입니다." << std::endl;
    }

    return 0;
}

2. 데이터 추출 (날짜 형식)

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

int main() 
{
    std::string text = "Today is 2023-12-25. Merry Christmas!";
    
    // 괄호 ()를 사용하여 캡처 그룹을 만듭니다.
    // (\d{4}): 연도, (\d{2}): 월, (\d{2}): 일
    std::regex date_pattern(R"((\d{4})-(\d{2})-(\d{2}))");
    std::smatch matches;

    // regex_search: 부분 문자열 매칭 검색
    if (std::regex_search(text, matches, date_pattern))
    {
        std::cout << "전체 매칭: " << matches[0] << std::endl; // 2023-12-25
        std::cout << "연도: " << matches[1] << std::endl;      // 2023
        std::cout << "월: " << matches[2] << std::endl;        // 12
        std::cout << "일: " << matches[3] << std::endl;        // 25
    }

    return 0;
}

3. 문자열 치환(개인정보 보호 마스킹)

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

int main() 
{
    std::string text = "My phone number is 010-1234-5678.";
    
    // 하이픈 뒤의 숫자 4자리를 찾음
    std::regex phone_pattern(R"(-\d{4})");

    // 매칭된 부분을 "-****"로 치환
    // regex_replace(원본문자열, 패턴, 바꿀문자열)
    std::string result = std::regex_replace(text, phone_pattern, "-****");

    std::cout << "결과: " << result << std::endl;
    // 출력: 결과: My phone number is 010-****-****.

    return 0;
}

⚠️ 주의사항: 성능 (Performance)

C++ 표준의 std::regex는 런타임에 패턴을 컴파일하므로, 매우 고성능이 요구되는 환경이나 반복문 내부에서 매번 std::regex 객체를 생성하는 것은 피해야 합니다.

  1. 최적화: std::regex 객체는 static이나 전역으로 선언하여 한 번만 컴파일되게 하세요.
  2. 대안: 극도로 빠른 속도가 필요하다면 컴파일 타임 정규식 라이브러리인 **CTRE (Compile Time Regular Expressions)**나 구글의 re2 라이브러리 사용을 고려할 수 있습니다.

6. 정규 표현식과 컴파일러

1. 이론적 관계: 수학적으로 동일함

컴퓨터 과학 이론(촘스키 계층)에서 정규 표현식으로 표현할 수 있는 언어(Regular Language)는 반드시 유한 오토마타(FSM)로 변환하여 인식할 수 있음이 증명되어 있습니다.

  • 정규 표현식 (Regular Expression): 인간이 이해하기 쉽고 간결하게 쓴 패턴 정의.
    • 예: a(b|c)* ('a'로 시작하고 뒤에 'b' 또는 'c'가 0개 이상 옴)
  • FSM (Finite State Machine): 컴퓨터가 이해하고 실행할 수 있는 상태 전이 도표.
    • 예: "초기 상태에서 'a'가 들어오면 '성공 상태'로 이동, 거기서 'b'나 'c'가 들어오면 제자리 유지..."

즉, 우리가 정규 표현식을 작성하면, 컴퓨터(컴파일러)는 내부적으로 이것을 FSM 구조로 변환하여 문자열을 한 글자씩 읽으며 상태를 이동시킵니다.

2. 컴파일러에서의 역할: 어휘 분석 (Lexical Analysis)

컴파일러는 소스 코드를 기계어로 번역하기 위해 가장 먼저 코드를 '토큰(Token)' 단위로 쪼개야 합니다. 이 과정을 어휘 분석이라고 하며, 여기서 정규 표현식과 FSM이 사용됩니다.

1단계: 패턴 정의 (정규 표현식)

컴파일러 설계자는 프로그래밍 언어의 문법을 정규 표현식으로 정의합니다.

  • 식별자(변수명): [a-zA-Z_][a-zA-Z0-9_]*
  • 정수: [0-9]+
  • 공백: \s+

2단계: FSM 생성 (Lexer Generator)

lex, flex 같은 도구나 C++의 std::regex 엔진은 위에서 정의한 정규 표현식을 받아 상태 머신(FSM) 코드를 생성하거나 내부적으로 구성합니다.

3단계: 실행 (스캐닝)

생성된 FSM은 소스 코드를 한 글자씩 읽습니다(Scan).

  • 현재 상태에서 입력 문자에 따라 다음 상태로 전이합니다.
  • 더 이상 갈 곳이 없거나 매칭이 끝나면 해당 문자열을 '토큰'으로 잘라냅니다.

3. 동작 원리 시각화 (예시)

a(b|c)* 라는 정규 표현식을 FSM으로 만들면 다음과 같이 동작합니다.

  1. State 0 (시작):
    • 입력 a -> State 1로 이동.
    • 입력 그 외 -> 실패(Fail).
  2. State 1 (성공/종료 가능 상태):
    • 입력 b -> State 1 유지 (반복).
    • 입력 c -> State 1 유지 (반복).
    • 입력 끝 -> 성공(Match).

이런 구조 덕분에 FSM은 문자열을 처음부터 끝까지 단 한 번만 훑으면($O(N)$) 매칭 여부를 판단할 수 있어 매우 빠릅니다.


4. NFA와 DFA (심화)

정규 표현식을 FSM으로 바꿀 때 보통 두 단계를 거칩니다.

  1. NFA (비결정적 유한 오토마타): 정규 표현식을 그대로 옮긴 형태입니다. 하나의 입력에 대해 여러 상태로 갈 수 있어 구현이 복잡하거나 느릴 수 있습니다(백트래킹 발생).
  2. DFA (결정적 유한 오토마타): NFA를 최적화하여, 특정 입력에 대해 무조건 하나의 상태로만 가도록 만든 것입니다. 컴파일러는 속도를 위해 최종적으로 DFA를 만들어 사용합니다.

'개발자 면접 공부 > C-C++' 카테고리의 다른 글

C++ Placement New에 대한 공부  (0) 2025.11.27
정적 링킹 vs 동적 링킹  (0) 2024.07.04
템플릿(Template)  (1) 2024.07.01
C++ 11 범위기반 for문 (for each)  (0) 2024.06.16
extern 쓰는법  (0) 2024.06.11