C++ 문자열 알고리즘 완벽 가이드 | split·join·trim·replace·정규식 [실전]

C++ 문자열 알고리즘 완벽 가이드 | split·join·trim·replace·정규식 [실전]

이 글의 핵심

C++에서 문자열 split, join, trim, replace, 정규식을 구현하는 방법. CSV 파싱·로그 처리·입력 검증 시 겪는 문제 시나리오, 완전한 예제, 흔한 에러, 성능 팁, 프로덕션 패턴까지.

들어가며: “split 함수가 없어요”

C++에는 Python의 split()이 없다

Python에서는 "a,b,c".split(",") 한 줄로 문자열을 나눌 수 있습니다. C++ 표준 라이브러리에는 split 함수가 없습니다. std::stringfind, substr, erase 같은 저수준 연산만 제공하고, split·join·trim 같은 고수준 알고리즘은 직접 구현하거나 서드파티를 써야 합니다.

실제 겪는 문제 시나리오:

  1. CSV 한 줄 파싱: "Alice,25,Engineer"["Alice","25","Engineer"]로 나누려다 stringstream 매번 생성으로 10만 줄 처리 시 TLE
  2. 사용자 입력 trim: " hello \n"에서 앞뒤 공백 제거 후 검증하려다 find/substr 인덱스 실수
  3. 로그 포맷 변환: "ERROR: connection failed""WARN: connection failed"로 바꾸려다 replace가 첫 번째만 바꿔서 놓침
  4. 이메일 검증: 정규식으로 검증하려다 std::regex 생성 비용을 몰라서 루프 안에서 매번 컴파일

이 글에서는 문제 시나리오부터 split·join·trim·replace·정규식의 완전한 구현, 흔한 에러, 성능 팁, 프로덕션 패턴까지 한 번에 정리합니다.

flowchart TB
  subgraph problem["문제 시나리오"]
    P1[CSV 파싱 TLE]
    P2[trim 인덱스 실수]
    P3[replace 첫 번째만]
    P4[정규식 루프 내 컴파일]
  end
  subgraph solution["해결 기법"]
    S1[split: getline/stringstream]
    S2[trim: find_first_not_of]
    S3[replace: regex_replace]
    S4[regex: 한 번 컴파일 후 재사용]
  end
  P1 --> S1
  P2 --> S2
  P3 --> S3
  P4 --> S4

이 글을 읽으면:

  • split, join, trim, replace를 완전히 구현할 수 있습니다.
  • 정규식을 올바르게 사용할 수 있습니다.
  • 흔한 에러를 피하고 성능을 최적화할 수 있습니다.
  • 프로덕션 수준의 문자열 유틸리티를 만들 수 있습니다.

목차

  1. 문제 시나리오
  2. split (문자열 분할)
  3. join (문자열 결합)
  4. trim (공백 제거)
  5. replace (문자열 치환)
  6. 정규식 (regex)
  7. 완전한 문자열 알고리즘 예제
  8. 자주 발생하는 에러와 해결법
  9. 성능 최적화 팁
  10. 프로덕션 패턴

1. 문제 시나리오

시나리오 1: CSV 한 줄을 벡터로

상황: "1,2,3,4,5"vector<int>로 변환해야 합니다. 코테에서 10만 줄 입력 시 매번 istringstream을 생성하면 힙 할당이 폭발합니다.

해결: istringstream을 루프 밖에서 한 번만 생성하고 clear()·str()으로 재사용합니다. 또는 string_view로 제로카피 split 후 from_chars로 변환합니다.

시나리오 2: 사용자 입력 앞뒤 공백

상황: " hello world \n"에서 앞뒤 공백·개행을 제거해야 합니다. findsubstr로 하다가 빈 문자열·전부 공백인 경우를 놓칩니다.

해결: find_first_not_of(" \t\n\r")find_last_not_of(" \t\n\r")로 범위를 구한 뒤, npos 체크를 반드시 합니다.

시나리오 3: 모든 “foo”를 “bar”로 치환

상황: "foofoofoo"에서 모든 “foo”를 “bar”로 바꿔야 합니다. string::replace첫 번째 매칭만 바꿉니다.

해결: std::regex_replaceregex_replace(s, re, "bar")를 쓰거나, find 루프로 수동 치환합니다.

시나리오 4: 이메일·URL 패턴 검증

상황: 사용자 입력이 이메일 형식인지 검증해야 합니다. std::regex를 매 검증마다 생성하면 컴파일 비용이 큽니다.

해결: 정규식 객체를 한 번만 생성하고 재사용합니다. static const std::regex 또는 클래스 멤버로 캐시합니다.


2. split (문자열 분할)

2.1 stringstream + getline (구분자 1개)

용도: ,| 같은 단일 구분자로 split할 때 가장 흔한 방법입니다.

#include <sstream>
#include <string>
#include <vector>

std::vector<std::string> split(const std::string& str, char delimiter) {
    std::vector<std::string> result;
    std::istringstream iss(str);
    std::string token;
    while (std::getline(iss, token, delimiter)) {
        result.push_back(token);
    }
    return result;
}

int main() {
    std::string csv = "Alice,25,Engineer";
    auto fields = split(csv, ',');
    // fields: {"Alice", "25", "Engineer"}
    return 0;
}

위 코드 설명: getline(iss, token, delimiter)는 구분자 전까지를 token에 넣고, 루프로 반복하면 한 줄을 구분자 기준으로 나눈 벡터를 얻습니다. 빈 토큰("a,,b"["a","","b"])도 포함됩니다.

2.2 find + substr (스트림 없이)

용도: stringstream 생성 비용을 피하고 싶을 때. 순수 string 연산만 사용합니다.

#include <string>
#include <vector>

std::vector<std::string> split_find(const std::string& str, char delimiter) {
    std::vector<std::string> result;
    size_t start = 0;
    while (start < str.size()) {
        size_t pos = str.find(delimiter, start);
        if (pos == std::string::npos) {
            result.push_back(str.substr(start));
            break;
        }
        result.push_back(str.substr(start, pos - start));
        start = pos + 1;
    }
    return result;
}

위 코드 설명: find(delimiter, start)로 다음 구분자 위치를 찾고, substr(start, pos - start)로 토큰을 잘라 벡터에 넣습니다. substr은 매번 새 string을 만들므로 토큰이 많으면 할당이 늘어납니다.

2.3 string_view로 제로카피 split (C++17)

용도: 복사 없이 토큰 구간만 필요할 때. 원본보다 vector<string_view> 수명이 길면 안 됩니다.

#include <string_view>
#include <vector>

std::vector<std::string_view> split_sv(std::string_view str, char delimiter) {
    std::vector<std::string_view> result;
    size_t start = 0;
    while (start < str.size()) {
        size_t pos = str.find(delimiter, start);
        if (pos == std::string_view::npos) {
            result.push_back(str.substr(start));
            break;
        }
        result.push_back(str.substr(start, pos - start));
        start = pos + 1;
    }
    return result;
}

위 코드 설명: string_view::substr복사 없이 구간만 가리킵니다. 원본 string이 파괴되면 string_view는 댕글링되므로, 반환된 벡터를 원본보다 오래 보관하지 마세요.

2.4 여러 구분자로 split (공백·탭·쉼표)

용도: "a b,c\td"처럼 공백·탭·쉼표를 모두 구분자로 쓸 때.

#include <sstream>
#include <string>
#include <vector>

std::vector<std::string> split_whitespace(const std::string& str) {
    std::vector<std::string> result;
    std::istringstream iss(str);
    std::string token;
    while (iss >> token) {  // >> 는 공백·탭·개행으로 자동 분리
        result.push_back(token);
    }
    return result;
}

위 코드 설명: iss >> token은 공백·탭·개행을 구분자로 사용합니다. getline과 달리 구분자를 지정할 수 없지만, “공백류로 나누기”에는 가장 간단합니다.

2.5 정규식으로 split (복잡한 구분자)

용도: \s+(공백 1개 이상), [,;](쉼표 또는 세미콜론) 등 패턴으로 나눌 때.

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

std::vector<std::string> split_regex(const std::string& str, const std::string& pattern) {
    std::regex re(pattern);
    std::sregex_token_iterator it(str.begin(), str.end(), re, -1);
    std::sregex_token_iterator end;
    std::vector<std::string> result;
    while (it != end) {
        result.push_back(*it);
        ++it;
    }
    return result;
}

// 사용: split_regex("a  b   c", "\\s+") → {"a", "b", "c"}
// 사용: split_regex("a,b;c", "[,;]") → {"a", "b", "c"}

위 코드 설명: sregex_token_iterator의 네 번째 인자 -1은 “매칭되지 않은 부분”을 토큰으로 반환합니다. 정규식 컴파일 비용이 있으므로, 루프 안에서 매번 호출하지 마세요.


3. join (문자열 결합)

3.1 ostringstream으로 join

용도: vector<string>"a,b,c"처럼 구분자로 이어 붙일 때.

#include <sstream>
#include <string>
#include <vector>

template <typename Container>
std::string join(const Container& items, const std::string& delimiter) {
    std::ostringstream oss;
    bool first = true;
    for (const auto& item : items) {
        if (!first) oss << delimiter;
        oss << item;
        first = false;
    }
    return oss.str();
}

int main() {
    std::vector<std::string> words = {"Hello", "World", "C++"};
    std::string result = join(words, ", ");
    // result: "Hello, World, C++"
    return 0;
}

위 코드 설명: 첫 항목만 구분자 없이 넣고, 그 다음부터는 delimiter를 앞에 붙여 이어 붙입니다. ostringstream은 내부 버퍼가 자동 확장되므로 += 반복보다 효율적일 수 있습니다.

3.2 accumulate로 join (C++17)

용도: 함수형 스타일로 join할 때.

#include <numeric>
#include <string>
#include <vector>

std::string join_accumulate(const std::vector<std::string>& items,
                            const std::string& delimiter) {
    if (items.empty()) return "";
    return std::accumulate(std::next(items.begin()), items.end(), items[0],
        [&delimiter](const std::string& a, const std::string& b) {
            return a + delimiter + b;
        });
}

위 코드 설명: accumulate의 초기값을 items[0]으로 두고, 나머지 항목을 delimiter로 연결합니다. 빈 벡터는 별도 처리해야 합니다. 문자열 연결 시 임시 객체가 생기므로 매우 큰 벡터에서는 ostringstream이 더 나을 수 있습니다.

3.3 범위 for + reserve (성능 고려)

용도: 대량 join 시 reserve로 재할당을 줄일 때.

#include <string>
#include <vector>

std::string join_reserve(const std::vector<std::string>& items,
                         const std::string& delimiter) {
    if (items.empty()) return "";
    size_t total = 0;
    for (const auto& s : items) total += s.size();
    total += delimiter.size() * (items.size() - 1);

    std::string result;
    result.reserve(total);
    result += items[0];
    for (size_t i = 1; i < items.size(); ++i) {
        result += delimiter;
        result += items[i];
    }
    return result;
}

위 코드 설명: 전체 길이를 미리 계산해 reserve로 한 번만 할당하고, +=로 이어 붙입니다. ostringstream보다 제어가 세밀합니다.


4. trim (공백 제거)

4.1 앞뒤 공백 제거 (기본)

용도: " hello \n""hello"로 앞뒤 공백·탭·개행을 제거할 때.

#include <string>

std::string trim(const std::string& str) {
    size_t first = str.find_first_not_of(" \t\n\r");
    if (first == std::string::npos) return "";
    size_t last = str.find_last_not_of(" \t\n\r");
    return str.substr(first, last - first + 1);
}

위 코드 설명: find_first_not_of로 공백이 아닌 첫 위치, find_last_not_of로 마지막 위치를 구합니다. 전부 공백이면 find_first_not_ofnpos를 반환하므로 빈 문자열을 반환합니다. last - first + 1substr 길이가 됩니다.

4.2 제거할 문자 집합 지정

용도: 공백 외에 ,, ; 등도 제거하고 싶을 때.

#include <string>

std::string trim_chars(const std::string& str, const std::string& chars) {
    size_t first = str.find_first_not_of(chars);
    if (first == std::string::npos) return "";
    size_t last = str.find_last_not_of(chars);
    return str.substr(first, last - first + 1);
}

// trim_chars(",,,hello,,,", ",") → "hello"

4.3 ltrim / rtrim (한쪽만)

용도: 앞만 또는 뒤만 제거할 때.

#include <string>

std::string ltrim(const std::string& str, const std::string& chars = " \t\n\r") {
    size_t first = str.find_first_not_of(chars);
    if (first == std::string::npos) return "";
    return str.substr(first);
}

std::string rtrim(const std::string& str, const std::string& chars = " \t\n\r") {
    size_t last = str.find_last_not_of(chars);
    if (last == std::string::npos) return "";
    return str.substr(0, last + 1);
}

4.4 string_view 버전 (제로카피)

용도: 복사 없이 trim된 구간만 필요할 때.

#include <string_view>

std::string_view trim_sv(std::string_view str, const char* chars = " \t\n\r") {
    size_t first = str.find_first_not_of(chars);
    if (first == std::string_view::npos) return "";
    size_t last = str.find_last_not_of(chars);
    return str.substr(first, last - first + 1);
}

5. replace (문자열 치환)

5.1 첫 번째만 치환 (string::replace)

용도: string::replace한 번만 치환합니다.

#include <string>

int main() {
    std::string s = "foo bar foo";
    size_t pos = s.find("foo");
    if (pos != std::string::npos) {
        s.replace(pos, 3, "bar");
    }
    // s: "bar bar foo" (첫 번째만 바뀜)
    return 0;
}

5.2 모든 매칭 치환 (find 루프)

용도: “foo”를 전부 “bar”로 바꿀 때.

#include <string>

void replace_all(std::string& str, const std::string& from,
                 const std::string& to) {
    size_t pos = 0;
    while ((pos = str.find(from, pos)) != std::string::npos) {
        str.replace(pos, from.size(), to);
        pos += to.size();
    }
}

int main() {
    std::string s = "foofoofoo";
    replace_all(s, "foo", "bar");
    // s: "barbarbar"
    return 0;
}

위 코드 설명: find(from, pos)로 다음 매칭을 찾고, replace로 치환한 뒤 posto.size()만큼 진행합니다. fromto의 부분 문자열이면 무한 루프에 빠질 수 있으므로, from != to를 전제로 합니다.

5.3 std::regex_replace (정규식 치환)

용도: 패턴으로 치환할 때. 예: 숫자만 추출, 이메일 마스킹.

#include <regex>
#include <string>

int main() {
    std::string s = "foo bar foo baz foo";
    std::regex re("foo");
    std::string result = std::regex_replace(s, re, "bar");
    // result: "bar bar bar baz bar"

    // 이메일 마스킹 예시
    std::string email = "[email protected]";
    std::regex email_re(R"((.{2}).*@(.*))");
    std::string masked = std::regex_replace(email, email_re, "$1***@$2");
    // masked: "us***@example.com"
    return 0;
}

위 코드 설명: regex_replace(입력, 정규식, 치환문자열)로 모든 매칭을 한 번에 치환합니다. $1, $2는 캡처 그룹 참조입니다. 정규식은 한 번만 컴파일하고 재사용하세요.


6. 정규식 (regex)

6.1 regex_match (전체 매칭)

용도: 문자열 전체가 패턴과 일치하는지 검사할 때.

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

int main() {
    std::string email = "[email protected]";
    std::regex re(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
    if (std::regex_match(email, re)) {
        std::cout << "Valid email\n";
    }
    return 0;
}

위 코드 설명: regex_match전체 문자열이 패턴과 일치해야 true를 반환합니다. 부분 매칭에는 regex_search를 씁니다.

6.2 regex_search (부분 매칭)

용도: 문자열 안에 패턴이 있는지, 그리고 캡처 그룹을 추출할 때.

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

int main() {
    std::string log = "2026-03-10 14:30:00 [ERROR] Connection failed";
    std::regex re(R"(\[(\w+)\]\s+(.+))");
    std::smatch match;
    if (std::regex_search(log, match, re)) {
        std::cout << "Level: " << match[1].str() << "\n";   // ERROR
        std::cout << "Message: " << match[2].str() << "\n"; // Connection failed
    }
    return 0;
}

위 코드 설명: match[0]은 전체 매칭, match[1], match[2]는 괄호로 묶인 캡처 그룹입니다. match[i].str()로 해당 부분 문자열을 얻습니다.

6.3 regex_replace (치환)

용도: 패턴에 맞는 부분을 치환할 때.

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

int main() {
    std::string text = "Price: $100, Tax: $10";
    std::regex re(R"(\$\d+)");
    std::string result = std::regex_replace(text, re, "[REDACTED]");
    // result: "Price: [REDACTED], Tax: [REDACTED]"
    std::cout << result << "\n";
    return 0;
}

6.4 정규식 재사용 (성능)

용도: 같은 패턴을 여러 번 쓸 때, 한 번만 컴파일합니다.

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

bool validate_emails(const std::vector<std::string>& emails) {
    static const std::regex email_re(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
    for (const auto& email : emails) {
        if (!std::regex_match(email, email_re)) return false;
    }
    return true;
}

위 코드 설명: static const std::regex로 한 번만 컴파일하고, 이후 호출에서는 재사용합니다. 루프 안에서 std::regex re(...)를 하면 매번 컴파일되어 매우 느려집니다.


7. 완전한 문자열 알고리즘 예제

예제 1: CSV 파싱 (split + trim + 숫자 변환)

#include <sstream>
#include <string>
#include <vector>
#include <cctype>

std::vector<int> parse_csv_line(const std::string& line) {
    std::vector<int> result;
    std::istringstream iss(line);
    std::string token;
    while (std::getline(iss, token, ',')) {
        // trim
        size_t first = token.find_first_not_of(" \t");
        if (first == std::string::npos) continue;
        size_t last = token.find_last_not_of(" \t");
        std::string trimmed = token.substr(first, last - first + 1);
        if (!trimmed.empty()) {
            result.push_back(std::stoi(trimmed));
        }
    }
    return result;
}

int main() {
    std::string line = " 1 , 2 , 3 , 4 , 5 ";
    auto nums = parse_csv_line(line);
    // nums: {1, 2, 3, 4, 5}
    return 0;
}

예제 2: 로그 라인 파싱 (정규식)

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

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

std::optional<LogEntry> parse_log(const std::string& line) {
    static const std::regex re(
        R"((\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+))");
    std::smatch match;
    if (std::regex_match(line, match, re)) {
        return LogEntry{match[1].str(), match[2].str(), match[3].str()};
    }
    return std::nullopt;
}

int main() {
    std::string line = "2026-03-10 14:30:00 [INFO] User logged in";
    if (auto entry = parse_log(line)) {
        std::cout << "Level: " << entry->level << ", Msg: " << entry->message << "\n";
    }
    return 0;
}

예제 3: URL 쿼리 파싱 (split + trim)

#include <sstream>
#include <string>
#include <map>

std::map<std::string, std::string> parse_query(const std::string& query) {
    std::map<std::string, std::string> params;
    std::istringstream iss(query);
    std::string pair;
    while (std::getline(iss, pair, '&')) {
        size_t eq = pair.find('=');
        if (eq != std::string::npos) {
            std::string key = pair.substr(0, eq);
            std::string value = pair.substr(eq + 1);
            params[trim(key)] = trim(value);
        }
    }
    return params;
}

예제 4: 템플릿 문자열 치환

#include <regex>
#include <string>

std::string replace_template(const std::string& tmpl,
                             const std::map<std::string, std::string>& vars) {
    std::string result = tmpl;
    for (const auto& [key, value] : vars) {
        std::string pattern = "\\{\\{" + key + "\\}\\}";
        std::regex re(pattern);
        result = std::regex_replace(result, re, value);
    }
    return result;
}

// replace_template("Hello {{name}}!", {{"name", "World"}}) → "Hello World!"

8. 자주 발생하는 에러와 해결법

에러 1: substr 인덱스 오류 (trim)

문제: find_last_not_ofnpos를 반환할 때 last - first + 1에서 언더플로우.

// ❌ 잘못된 코드
std::string trim_bad(const std::string& str) {
    size_t first = str.find_first_not_of(" \t");
    size_t last = str.find_last_not_of(" \t");
    return str.substr(first, last - first + 1);  // last가 npos면 오류!
}

해결: first == npos 체크 후 빈 문자열 반환.

// ✅ 올바른 코드
std::string trim(const std::string& str) {
    size_t first = str.find_first_not_of(" \t\n\r");
    if (first == std::string::npos) return "";
    size_t last = str.find_last_not_of(" \t\n\r");
    return str.substr(first, last - first + 1);
}

에러 2: replace_all 무한 루프

문제: fromto의 부분 문자열일 때 (예: “a” → “aa”) 무한 루프.

// ❌ 위험
replace_all(s, "a", "aa");  // "a" → "aa" → "aaa" → ...

해결: from != to 검사 또는 pos += from.size()로 진행해 겹치지 않게 합니다. 위 replace_allpos += to.size()로 이미 처리되어 있습니다. 단, tofrom을 포함하면 여전히 위험할 수 있으므로, from == to일 때 early return을 추가합니다.

void replace_all(std::string& str, const std::string& from,
                 const std::string& to) {
    if (from.empty() || from == to) return;
    size_t pos = 0;
    while ((pos = str.find(from, pos)) != std::string::npos) {
        str.replace(pos, from.size(), to);
        pos += to.size();
    }
}

에러 3: string_view 댕글링

문제: split_sv 결과를 원본보다 오래 보관하면 미정의 동작.

// ❌ 잘못된 코드
std::vector<std::string_view> get_tokens() {
    std::string line = read_line();
    return split_sv(line, ',');  // line 파괴 후 tokens 댕글링
}

해결: string_view를 원본과 같은 수명으로 쓰거나, string으로 복사해 반환합니다.

에러 4: 정규식 루프 내 컴파일

문제: 매 검증마다 std::regex re(...) 생성 → 극심한 성능 저하.

// ❌ 매우 느림
for (const auto& email : emails) {
    std::regex re(R"([a-z]+@[a-z]+\.[a-z]+)");
    if (std::regex_match(email, re)) { ... }
}

해결: static const std::regex 또는 클래스 멤버로 한 번만 생성.

// ✅ 빠름
static const std::regex email_re(R"([a-z]+@[a-z]+\.[a-z]+)");
for (const auto& email : emails) {
    if (std::regex_match(email, email_re)) { ... }
}

에러 5: getline과 >> 혼용 시 빈 줄

문제: cin >> ngetline을 쓰면 개행만 읽혀 빈 줄이 됩니다.

// ❌
int n;
std::cin >> n;
std::string line;
std::getline(std::cin, line);  // line == ""

해결: cin.ignore()로 개행 제거.

// ✅
std::cin >> n;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::getline(std::cin, line);

에러 6: stoi 예외

문제: std::stoi("abc")는 예외를 던집니다.

해결: try-catch 또는 std::from_chars(C++17) 사용.

#include <charconv>

std::optional<int> safe_stoi(std::string_view s) {
    int value;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc{} || ptr != s.data() + s.size()) return std::nullopt;
    return value;
}

9. 성능 최적화 팁

1. stringstream 재사용

루프 안에서 매번 생성하지 말고, 루프 밖에서 한 번만 만들고 clear()·str()으로 재사용합니다.

std::istringstream iss;
for (const auto& line : lines) {
    iss.clear();
    iss.str(line);
    // 파싱...
}

2. reserve로 할당 감소

split 결과를 vector에 넣을 때, 토큰 개수를 대략 예상하면 reserve로 재할당을 줄입니다.

result.reserve(estimated_count);

3. string_view로 제로카피

복사 없이 구간만 필요하면 string_view를 사용합니다. 원본 수명에 주의합니다.

4. 정규식 한 번만 컴파일

static const std::regex 또는 캐시로 재사용합니다.

5. from_chars / to_chars (숫자 변환)

순수 숫자 변환만 필요하면 std::from_chars·std::to_charsstoi·stringstream보다 빠릅니다.

6. 단순 split에는 regex 비추천

std::regex는 단순 구분자 split에 비해 매우 느립니다. getline이나 find/substr를 우선 고려합니다.


10. 프로덕션 패턴

패턴 1: 재사용 가능한 StringUtils 클래스

#include <sstream>
#include <string>
#include <vector>
#include <regex>

class StringUtils {
public:
    static std::vector<std::string> split(const std::string& str, char delim) {
        std::vector<std::string> result;
        std::istringstream iss(str);
        std::string token;
        while (std::getline(iss, token, delim)) {
            result.push_back(token);
        }
        return result;
    }

    static std::string join(const std::vector<std::string>& items,
                            const std::string& delim) {
        std::ostringstream oss;
        for (size_t i = 0; i < items.size(); ++i) {
            if (i > 0) oss << delim;
            oss << items[i];
        }
        return oss.str();
    }

    static std::string trim(const std::string& str) {
        size_t first = str.find_first_not_of(" \t\n\r");
        if (first == std::string::npos) return "";
        size_t last = str.find_last_not_of(" \t\n\r");
        return str.substr(first, last - first + 1);
    }

    static void replace_all(std::string& str, const std::string& from,
                             const std::string& to) {
        if (from.empty() || from == to) return;
        size_t pos = 0;
        while ((pos = str.find(from, pos)) != std::string::npos) {
            str.replace(pos, from.size(), to);
            pos += to.size();
        }
    }
};

패턴 2: 스레드 안전한 CSV 파서 (istringstream 재사용)

#include <sstream>
#include <string>
#include <vector>
#include <mutex>

class CsvParser {
public:
    std::vector<std::string> parse(const std::string& line) {
        std::lock_guard lock(mtx_);
        iss_.clear();
        iss_.str(line);
        std::vector<std::string> result;
        std::string token;
        while (std::getline(iss_, token, ',')) {
            result.push_back(token);
        }
        return result;
    }
private:
    std::mutex mtx_;
    std::istringstream iss_;
};

패턴 3: 정규식 캐시

#include <regex>
#include <unordered_map>
#include <string>

class RegexCache {
public:
    const std::regex& get(const std::string& pattern) {
        auto it = cache_.find(pattern);
        if (it == cache_.end()) {
            it = cache_.emplace(pattern, std::regex(pattern)).first;
        }
        return it->second;
    }
private:
    std::unordered_map<std::string, std::regex> cache_;
};

패턴 4: 에러 처리 포함 파서

#include <optional>
#include <string>
#include <vector>

struct ParseResult {
    std::vector<std::string> tokens;
    bool ok;
    std::string error;
};

ParseResult parse_csv_safe(const std::string& line, char delim = ',') {
    ParseResult result{};
    std::istringstream iss(line);
    std::string token;
    while (std::getline(iss, token, delim)) {
        result.tokens.push_back(token);
    }
    result.ok = true;
    return result;
}

정리

알고리즘권장 방법비고
splitgetline + stringstream구분자 1개. 재사용 시 성능 향상
split (제로카피)find + string_view원본 수명 주의
joinostringstream첫 항목만 구분자 제외
trimfind_first_not_of + find_last_not_ofnpos 체크 필수
replace allfind 루프 또는 regex_replacefrom == to 시 early return
정규식regex_match / regex_search / regex_replace한 번만 컴파일 후 재사용

구현 체크리스트

  • trim 시 find_first_not_ofnpos이면 빈 문자열 반환
  • replace_all에서 from == to 또는 from.empty() 체크
  • string_view 반환 시 원본 수명 확인
  • 정규식은 루프 밖에서 한 번만 생성
  • istringstream은 재사용 시 clear() + str()
  • getlinecin.ignore() 필요 여부 확인
  • stoi 실패 가능 시 from_chars 또는 try-catch

자주 묻는 질문 (FAQ)

Q. C++에 split이 왜 없나요?

A. 표준 라이브러리는 저수준 연산(find, substr)만 제공하고, 고수준 알고리즘은 애플리케이션마다 요구사항이 달라 범용 split을 넣지 않았습니다. C++20 rangesviews::split을 사용할 수 있지만, 아직 보편적이지 않습니다.

Q. stringstream과 find/substr 중 뭐가 더 빠른가요?

A. 단순 구분자 split에서는 find/substr이 보통 더 빠릅니다. 다만 stringstream>>로 타입 변환을 함께 할 수 있어 편의성이 있습니다. 대량 반복 시 stringstream 재사용이 중요합니다.

Q. 정규식은 언제 쓰나요?

A. 이메일·URL 검증, 복잡한 로그 파싱, 패턴 치환 등 패턴이 복잡할 때 사용합니다. 단순 구분자 split에는 getline이 훨씬 빠릅니다.

Q. UTF-8 한글은 어떻게 하나요?

A. std::string은 바이트 시퀀스이므로, UTF-8의 경우 코드 포인트 단위로 자르려면 std::mbrtoc32 또는 ICU 같은 라이브러리가 필요합니다. 단순 바이트 구분자(,, 공백) split은 기존 방법 그대로 사용 가능합니다.


한 줄 요약: C++에는 split·join·trim이 없으므로 stringstream·find/substr·regex로 구현합니다. 정규식은 한 번만 컴파일하고, string_view는 원본 수명에 주의하세요.

이전 글: C++ 실전 가이드 #11-1: 파일 I/O 기초

다음 글: C++ 실전 가이드 #11-3: stringstream과 포맷팅


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 파일 입출력 | ifstream·ofstream으로 “파일 열기 실패” 에러 처리까지
  • C++ stringstream | 문자열 파싱·변환·포맷팅
  • C++ 문자열 파싱 완벽 가이드 | stringstream·getline·제로카피·성능 벤치마크

이 글에서 다루는 키워드 (관련 검색어)

C++, 문자열알고리즘, split, join, trim, replace, 정규식, std::regex, stringstream, string_view 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |