[2026] C++ 문자열 파싱 완벽 가이드 | stringstream·getline·제로카피·성능 벤치마크
이 글의 핵심
입출력 최적화(#32-1)로 cin/cout 속도를 올렸는데도 시간 초과가 난다면, 원인은 문자열 파싱일 가능성이 큽니다. 백준·프로그래머스에서 1,2,3,4,5 같은 CSV 한 줄을 vector<int>로 바꾸거나,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며: “문자열 파싱만 하면 TLE가 나요"
"입력은 빠른데, split 하고 나서 시간 초과예요”
입출력 최적화(#32-1)로 cin/cout 속도를 올렸는데도 시간 초과가 난다면, 원인은 문자열 파싱일 가능성이 큽니다. 백준·프로그래머스에서 "1,2,3,4,5" 같은 CSV 한 줄을 vector<int>로 바꾸거나, "apple banana cherry"를 공백 기준으로 나누는 작업을 매 입력마다 하다 보면, 잘못된 방식 하나로 전체가 느려집니다.
실제 겪는 문제 시나리오:
- 10만 줄 로그 파싱:
getline으로 한 줄씩 읽은 뒤,로 split →stringstream매번 생성으로 힙 할당 폭발 - JSON 키 추출:
"key":"value"패턴에서find+substr반복 → 불필요한string복사 누적 - 대용량 CSV:
1,2,3,...,1000같은 긴 줄을vector<string>으로 쪼개기 → 임시 객체 생성으로 메모리·시간 모두 초과 이 글에서는 문제 원인 분석부터 완전한 파싱 기법, 제로카피 파싱, 자주 하는 실수, 성능 벤치마크, 프로덕션 패턴까지 한 번에 정리합니다. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
flowchart TB
subgraph problem[문제 시나리오]
P1[10만 줄 로그]
P2[JSON 키 추출]
P3[대용량 CSV]
end
subgraph solution[해결 기법]
S1[stringstream + getline]
S2[string_view 제로카피]
S3[find + substr 최적화]
end
P1 --> S1
P2 --> S2
P3 --> S3
이 글에서 다루는 것:
- 문제 시나리오: 왜 문자열 파싱이 병목이 되는지
- 완전한 파싱 기법:
stringstream,getline,strtok,find/substr, 정규식 - 제로카피 파싱:
std::string_view,std::span활용 - 자주 하는 실수: 메모리 누수, 반복자 무효화, 인코딩
- 성능 벤치마크: 기법별 속도·메모리 비교
- 프로덕션 패턴: 로그 파서, CSV 파서, 재사용 가능한 유틸리티
개념을 잡는 비유
C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: 왜 파싱이 병목인가
- 기본 파싱 기법
- 고급 파싱 기법
- 제로카피 파싱
- 자주 하는 실수와 해결법
- 성능 벤치마크
- 프로덕션 패턴
- 문자열 파싱 베스트 프랙티스
- 정리 및 체크리스트
1. 문제 시나리오: 왜 파싱이 병목인가
시나리오 1: CSV 한 줄을 정수 벡터로
입력 예
5
1,2,3,4,5
10,20,30,40,50
나쁜 예 (매번 stringstream 생성) 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
// 변수 선언 및 초기화
int main() {
int n;
std::cin >> n;
std::cin.ignore();
for (int i = 0; i < n; ++i) {
std::string line;
std::getline(std::cin, line);
std::istringstream iss(line); // 매 반복마다 새 스트림 생성 → 힙 할당
std::vector<int> nums;
int x;
while (iss >> x) {
char comma;
if (iss >> comma) {} // ',' 건너뛰기
nums.push_back(x);
}
}
}
문제점: istringstream 생성 시 내부 버퍼를 힙에 할당합니다. 10만 줄이면 10만 번 할당·해제가 발생해 캐시 미스와 할당 오버헤드가 누적됩니다.
시나리오 2: 공백 구분 문자열 split
입력 예
apple banana cherry
dog cat bird
나쁜 예 (substr 남발) 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::vector<std::string> split_bad(const std::string& s) {
std::vector<std::string> result;
size_t start = 0;
while (true) {
size_t pos = s.find(' ', start);
if (pos == std::string::npos) {
result.push_back(s.substr(start)); // 매번 새 string 복사
break;
}
result.push_back(s.substr(start, pos - start)); // 또 복사
start = pos + 1;
}
return result;
}
문제점: substr은 매번 새 string을 반환합니다. 토큰이 1000개면 1000번의 힙 할당과 복사가 발생합니다.
시나리오 3: JSON 스타일 키 추출
입력 예
{"name":"홍길동","age":30,"city":"서울"}
나쁜 예 (find + substr 반복) 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
std::string get_value(const std::string& json, const std::string& key) {
std::string pattern = "\"" + key + "\":"; // 매번 문자열 연결
size_t pos = json.find(pattern);
if (pos == std::string::npos) return "";
pos += pattern.size();
size_t end = json.find("\"", pos);
return json.substr(pos, end - pos); // 불필요한 복사
}
문제점: pattern 생성, substr 반환 모두 임시 string을 만듭니다. string_view를 쓰면 복사를 줄일 수 있습니다.
시나리오 4: trim/replace 누락으로 파싱 실패
입력 예: 사용자가 " apple , banana , cherry "처럼 앞뒤 공백을 넣은 경우
apple , banana , cherry
나쁜 예: trim 없이 split만 하면 " apple ", " banana " 같은 토큰이 생겨 비교·검색이 실패합니다.
// ❌ trim 없이 split
auto tokens = split_by_delim(user_input, ',');
// tokens[0] == " apple " → "apple"과 비교 시 실패
해결: split 전에 trim을 적용하거나, 토큰 처리 시 각각 trim합니다.
시나리오 5: 대용량 로그에서 메모리 폭발
상황: 100만 줄 로그를 vector<vector<string>>로 전부 메모리에 올리면, 토큰마다 string 복사로 수 GB 메모리를 사용할 수 있습니다.
해결: string_view로 제로카피 파싱하거나, 한 줄씩 파싱 후 처리하고 버리는 스트리밍 방식을 사용합니다.
2. 기본 파싱 기법
2.1 stringstream + getline (구분자 지정)
용도: ,나 | 같은 구분자로 split할 때 가장 흔히 쓰는 방법입니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sstream>
#include <string>
#include <vector>
// 구분자로 split (getline의 세 번째 인자 활용)
std::vector<std::string> split_by_delim(const std::string& s, char delim) {
std::vector<std::string> result;
std::istringstream iss(s);
std::string token;
while (std::getline(iss, token, delim)) {
result.push_back(token);
}
return result;
}
// 사용 예
// split_by_delim("a,b,c", ',') → {"a", "b", "c"}
장점: 코드가 짧고, 구분자를 바꾸기 쉽습니다.
단점: istringstream 생성 비용이 있어, 반복 횟수가 많으면 느려질 수 있습니다.
2.2 getline만으로 공백 구분 (cin과 함께)
용도: cin으로 한 줄을 읽고, 그 안의 공백 구분 토큰을 파싱할 때.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
int main() {
std::string line;
std::getline(std::cin, line);
std::istringstream iss(line);
std::vector<std::string> tokens;
std::string token;
while (iss >> token) { // 공백·탭으로 자동 분리
tokens.push_back(token);
}
}
주의: iss >> token은 공백·탭·개행을 구분자로 사용합니다. ,는 구분자가 아니므로 getline(iss, token, ',')를 써야 합니다.
2.3 find + substr (수동 파싱)
용도: 구분자가 하나가 아니거나, 위치 기반으로 잘라야 할 때. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <vector>
std::vector<std::string> split_find_substr(const std::string& s, char delim) {
std::vector<std::string> result;
size_t start = 0;
while (start < s.size()) {
size_t pos = s.find(delim, start);
if (pos == std::string::npos) {
result.push_back(s.substr(start));
break;
}
result.push_back(s.substr(start, pos - start));
start = pos + 1;
}
return result;
}
장점: 스트림 없이 순수 string 연산만 사용합니다.
단점: substr이 매번 새 string을 만들므로, 토큰이 많으면 할당이 많아집니다.
2.4 strtok (C 스타일, 문자열 수정)
용도: C 스타일 문자열을 제자리에서 수정해도 될 때. 가장 빠른 편이지만, 원본이 바뀝니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cstring>
#include <vector>
#include <string>
std::vector<std::string> split_strtok(std::string s, const char* delim) {
std::vector<std::string> result;
char* token = std::strtok(&s[0], delim);
while (token) {
result.push_back(token);
token = std::strtok(nullptr, delim);
}
return result;
}
주의:
std::strtok은 원본 문자열에\0을 삽입합니다.const std::string&을 받으면 수정 불가이므로, 복사본을 넘겨야 합니다.- C++17 이상에서는
&s[0]이 연속 메모리를 보장합니다. C++11에서는s.data()가const이므로&s[0]을 쓰되,s가 비어 있지 않아야 합니다. - 스레드 안전하지 않습니다.
strtok_r(POSIX)을 쓰면 스레드 안전하게 할 수 있습니다.
2.5 정규식 (std::regex)
용도: 복잡한 패턴(이메일, URL, 숫자만 추출 등)을 다룰 때. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <regex>
#include <string>
#include <vector>
std::vector<std::string> split_regex(const std::string& s, const std::string& pattern) {
std::regex re(pattern);
std::sregex_token_iterator it(s.begin(), s.end(), re, -1);
std::sregex_token_iterator end;
std::vector<std::string> result;
while (it != end) {
result.push_back(*it);
++it;
}
return result;
}
// 사용 예: 공백 하나 이상으로 split
// split_regex("a b c", "\\s+") → {"a", "b", "c"}
장점: 복잡한 패턴에 강합니다.
단점: 매우 느립니다. 단순 split에는 비추천입니다.
2.6 trim (앞뒤 공백 제거)
용도: 사용자 입력, 로그 파싱 시 앞뒤 공백·탭·개행을 제거할 때 필수입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <algorithm>
#include <cctype>
std::string trim(std::string s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(),
{ return !std::isspace(c); }));
s.erase(std::find_if(s.rbegin(), s.rend(),
{ return !std::isspace(c); }).base(), s.end());
return s;
}
// string_view 버전 (제로카피, C++17)
std::string_view trim_sv(std::string_view s) {
auto start = s.find_first_not_of(" \t\n\r");
if (start == std::string_view::npos) return "";
auto end = s.find_last_not_of(" \t\n\r");
return s.substr(start, end - start + 1);
}
2.7 replace (문자열 치환)
용도: 특정 패턴을 다른 문자열로 교체할 때. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
std::string replace_all(const std::string& s, const std::string& from,
const std::string& to) {
std::string result = s;
for (size_t pos = 0; (pos = result.find(from, pos)) != std::string::npos; ) {
result.replace(pos, from.size(), to);
pos += to.size();
}
return result;
}
// replace_all("a-b-c", "-", "_") → "a_b_c"
2.8 정규식 활용 (패턴 매칭·검증·추출)
용도: 이메일, URL, 숫자 추출 등 복잡한 패턴이 필요할 때. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <regex>
#include <string>
#include <vector>
// 이메일 형식 검증
bool is_valid_email(const std::string& email) {
std::regex pattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
return std::regex_match(email, pattern);
}
// 숫자만 추출
std::vector<int> extract_numbers(const std::string& s) {
std::vector<int> result;
std::regex num_re(R"(\d+)");
for (std::sregex_iterator it(s.begin(), s.end(), num_re), end; it != end; ++it)
result.push_back(std::stoi(it->str()));
return result;
}
3. 고급 파싱 기법
3.1 스트림 재사용 (할당 최소화)
코테에서 10만 줄을 파싱할 때, istringstream을 한 번만 생성하고 str()으로 내용만 바꿔 재사용합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sstream>
#include <string>
#include <vector>
// 실행 예제
std::vector<int> parse_csv_line_reuse(std::istringstream& iss, const std::string& line) {
iss.clear();
iss.str(line);
std::vector<int> result;
int x;
char comma;
while (iss >> x) {
result.push_back(x);
if (!(iss >> comma)) break;
}
return result;
}
int main() {
std::istringstream iss; // 한 번만 생성
std::string line;
while (std::getline(std::cin, line)) {
auto nums = parse_csv_line_reuse(iss, line);
// ...
}
}
핵심: iss.clear()로 에러 플래그를 초기화하고, iss.str(line)으로 새 내용을 넣습니다. 스트림 객체는 재사용되므로 힙 할당이 줄어듭니다.
3.2 reserve로 vector 재할당 감소
split 결과를 vector에 넣을 때, 대략적인 크기를 미리 예상하면 push_back 시 재할당을 줄일 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::vector<std::string> split_reserved(const std::string& s, char delim) {
std::vector<std::string> result;
size_t count = 0;
for (char c : s) if (c == delim) ++count;
result.reserve(count + 1); // 토큰 개수만큼 미리 예약
std::istringstream iss(s);
std::string token;
while (std::getline(iss, token, delim)) {
result.push_back(token);
}
return result;
}
3.3 파싱과 변환 동시에 (숫자 파싱)
stringstream으로 split과 int/double 변환을 한 번에 처리합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sstream>
#include <vector>
#include <string>
std::vector<int> parse_ints(const std::string& s, char delim) {
std::vector<int> result;
std::istringstream iss(s);
std::string token;
while (std::getline(iss, token, delim)) {
result.push_back(std::stoi(token));
}
return result;
}
// stol, stod, stof 등도 동일하게 사용 가능
주의: std::stoi는 예외를 던질 수 있습니다. 코테에서 입력이 항상 유효하다고 가정할 수 있으면 괜찮지만, 실무에서는 try-catch나 std::from_chars(C++17)를 고려합니다.
3.4 완전한 CSV 파싱 (따옴표 필드 지원)
실제 CSV는 "Hello, World"처럼 쉼표가 포함된 필드를 따옴표로 감쌀 수 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <vector>
std::vector<std::string> parse_csv_quoted(const std::string& line, char delim = ',') {
std::vector<std::string> result;
std::string field;
bool in_quotes = false;
for (size_t i = 0; i < line.size(); ++i) {
char c = line[i];
if (c == '"') {
if (in_quotes && i + 1 < line.size() && line[i + 1] == '"') {
field += '"'; ++i; // "" → "
} else in_quotes = !in_quotes;
} else if (!in_quotes && c == delim) {
result.push_back(std::move(field));
field.clear();
} else {
field += c;
}
}
result.push_back(std::move(field));
return result;
}
// "Alice","Hello, World",42 → {"Alice", "Hello, World", "42"}
3.5 간단한 JSON 파싱 (키-값 추출)
완전한 JSON 파서는 nlohmann/json 등 라이브러리를 쓰는 것이 좋습니다. 단순 flat JSON은 수동 파싱도 가능합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string_view>
#include <optional>
#include <map>
std::optional<std::map<std::string, std::string>> parse_simple_json(std::string_view json) {
std::map<std::string, std::string> result;
size_t i = 0;
while (i < json.size() && (json[i] == ' ' || json[i] == '{')) ++i;
if (i >= json.size()) return std::nullopt;
while (i < json.size() && json[i] != '}') {
size_t key_start = json.find('"', i);
if (key_start == std::string_view::npos) break;
size_t key_end = json.find('"', key_start + 1);
if (key_end == std::string_view::npos) break;
std::string key(json.substr(key_start + 1, key_end - key_start - 1));
size_t val_start = json.find('"', json.find(':', key_end));
if (val_start == std::string_view::npos) break;
size_t val_end = json.find('"', val_start + 1);
if (val_end == std::string_view::npos) break;
result[std::move(key)] = std::string(json.substr(val_start + 1, val_end - val_start - 1));
i = val_end + 1;
while (i < json.size() && (json[i] == ' ' || json[i] == ',')) ++i;
}
return result;
}
4. 제로카피 파싱
4.1 std::string_view (C++17)
string_view는 문자열을 복사하지 않고 “보기만” 합니다. substr 대신 substr의 string_view 버전을 쓰면 할당 없이 구간을 나타낼 수 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string_view>
#include <vector>
std::vector<std::string_view> split_string_view(std::string_view s, char delim) {
std::vector<std::string_view> result;
size_t start = 0;
while (start < s.size()) {
size_t pos = s.find(delim, start);
if (pos == std::string_view::npos) {
result.push_back(s.substr(start));
break;
}
result.push_back(s.substr(start, pos - start));
start = pos + 1;
}
return result;
}
// 사용 예
// std::string line = "a,b,c";
// auto tokens = split_string_view(line, ','); // 복사 없음
주의:
string_view가 가리키는 원본 문자열이 파괴되면,string_view는 댕글링됩니다. 반환된vector<string_view>를 원본보다 오래 보관하지 않도록 합니다.string이 필요할 때만std::string(tokens[i])로 변환합니다.
4.2 string_view로 키-값 추출 (제로카피)
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::optional<std::string_view> get_value_sv(std::string_view json, std::string_view key) {
std::string pattern = "\"" + std::string(key) + "\":\"";
size_t pos = json.find(pattern);
if (pos == std::string_view::npos) return std::nullopt;
pos += pattern.size();
size_t end = json.find('"', pos);
if (end == std::string_view::npos) return std::nullopt;
return json.substr(pos, end - pos);
}
값이 필요할 때만 std::string(tokens[i])로 변환합니다.
4.3 범위 기반 루프로 토큰 순회 (람다 활용)
토큰을 저장하지 않고 순회만 할 때는 람다로 콜백을 넘기는 방식이 메모리를 아낍니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string_view>
template<typename Func>
void for_each_token(std::string_view s, char delim, Func&& f) {
size_t start = 0;
while (start < s.size()) {
size_t pos = s.find(delim, start);
std::string_view token;
if (pos == std::string_view::npos) {
token = s.substr(start);
start = s.size();
} else {
token = s.substr(start, pos - start);
start = pos + 1;
}
if (!token.empty()) {
f(token);
}
}
}
// 사용 예
// for_each_token("a,b,c", ',', {
// std::cout << tok << "\n";
// });
4.4 std::span (C++20)과 연계
string_view는 읽기 전용입니다. char 배열을 수정 가능한 범위로 다룰 때는 std::span<char>를 사용합니다. 로그 파서에서 버퍼를 직접 다룰 때 유용합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <span>
#include <cstring>
void parse_in_place(std::span<char> buffer, char delim,
void (*on_token)(std::span<const char>)) {
char* start = buffer.data();
char* end = buffer.data() + buffer.size();
char* p = start;
while (p != end) {
if (*p == delim || *p == '\0') {
*p = '\0';
if (p > start) on_token({start, static_cast<size_t>(p - start)});
start = p + 1;
}
++p;
}
if (p > start) on_token({start, static_cast<size_t>(p - start)});
}
5. 자주 하는 실수와 해결법
5.1 strtok에 const string 전달
문제: strtok은 첫 인자로 수정 가능한 문자열을 받습니다.
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예
void bad(const std::string& s) {
char* token = std::strtok(const_cast<char*>(s.c_str()), ","); // 미정의 동작!
}
해결: 복사본을 넘깁니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 올바른 예
std::vector<std::string> split_ok(const std::string& s) {
std::string copy = s;
std::vector<std::string> result;
char* token = std::strtok(©[0], ",");
while (token) {
result.emplace_back(token);
token = std::strtok(nullptr, ",");
}
return result;
}
5.2 string_view 댕글링
문제: string_view가 가리키는 원본이 먼저 파괴되면, string_view 사용은 미정의 동작입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예
std::string_view get_first_token() {
std::string line = read_line(); // 지역 변수
return std::string_view(line).substr(0, line.find(',')); // line 파괴 후 댕글링
}
해결: string_view를 원본과 같은 수명으로 쓰거나, string으로 복사해 반환합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예
std::string get_first_token(const std::string& line) {
size_t pos = line.find(',');
return (pos == std::string::npos) ? line : line.substr(0, pos);
}
5.3 getline 후 cin >> 혼용 시 버퍼 잔여
문제: cin >> n 후 개행이 버퍼에 남아 있고, 바로 getline을 쓰면 빈 줄을 읽습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예
// 변수 선언 및 초기화
int n;
std::cin >> n;
std::string line;
std::getline(std::cin, line); // 개행만 읽고 line은 ""
해결: cin.ignore()로 개행을 제거합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예
int n;
std::cin >> n;
std::cin.ignore(); // 또는 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::string line;
std::getline(std::cin, line);
5.4 stoi 예외 처리 누락
문제: std::stoi는 변환 실패 시 std::invalid_argument 또는 std::out_of_range를 던집니다.
// ❌ 위험한 예
int x = std::stoi("abc"); // 예외 발생
해결: try-catch 또는 std::from_chars(C++17) 사용.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 안전한 예 (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;
}
5.5 반복문 내 매번 istringstream 생성
문제: 루프 안에서 매번 istringstream을 만들면 할당이 반복됩니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 비효율
for (const auto& line : lines) {
std::istringstream iss(line); // 매번 새로 생성
// ...
}
해결: 스트림을 루프 밖에서 한 번만 생성하고 str()으로 재사용합니다. (위 3.1 참고)
5.6 빈 토큰 처리
문제: "a,,b"를 ,로 split할 때 getline은 빈 토큰도 반환합니다. "a,"는 ["a", ""]가 됩니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 빈 토큰 제외가 필요할 때
while (std::getline(iss, token, delim)) {
if (!token.empty()) {
result.push_back(token);
}
}
5.7 정규식 컴파일 비용
문제: 루프 안에서 std::regex를 매번 생성하면 매우 느립니다. 해결: std::regex를 루프 밖에서 한 번만 생성하고 재사용합니다.
5.8 substr 인덱스 오류
문제: find가 npos를 반환할 때 substr에 잘못된 범위를 넘기면 미정의 동작 또는 예외가 발생합니다.
// ❌ 위험: pos가 npos일 때
size_t pos = s.find(',');
std::string token = s.substr(0, pos); // pos가 npos면 매우 큰 값 전달
해결: npos 체크 후 처리합니다.
// ✅ 안전
size_t pos = s.find(',');
std::string token = (pos == std::string::npos) ? s : s.substr(0, pos);
5.9 인코딩 혼동 (UTF-8 vs ASCII)
문제: UTF-8 한글은 여러 바이트로 구성됩니다. substr(i, 1)로 “한 글자”를 자르면 깨질 수 있습니다. 해결: UTF-8 코드 포인트 단위 처리가 필요하면 std::mbrtoc32 또는 utf8-cpp를 사용합니다. 단순 구분자 split은 바이트 단위로 해도 됩니다.
5.10 반복자 무효화 (문자열 수정 중 순회)
문제: for 루프에서 erase로 string을 수정하면 인덱스/반복자가 무효화됩니다. 해결: erase-remove 관용구 사용.
s.erase(std::remove(s.begin(), s.end(), ' '), s.end());
6. 성능 벤치마크
6.1 측정 환경 가정
- 입력:
"1,2,3,...,1000"형태의 CSV 한 줄 (토큰 1000개) - 반복: 10,000줄 파싱
- 컴파일:
g++ -O2 -std=c++17
6.2 기법별 상대 속도 (예상)
| 기법 | 상대 시간 | 메모리 할당 | 비고 |
|---|---|---|---|
strtok (복사본 1회) | 1.0x (기준) | 낮음 | 원본 수정 가능해야 함 |
find + string_view | 1.2x | 거의 없음 | 제로카피 |
find + substr (string) | 2.5x | 높음 | 토큰마다 string 생성 |
stringstream 재사용 | 2.0x | 중간 | 스트림 1개만 사용 |
stringstream 매번 생성 | 4.0x | 높음 | 비추천 |
std::regex | 15.0x 이상 | 높음 | 단순 split에 부적합 |
6.3 벤치마크 예시 코드
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <sstream>
#include <string>
#include <vector>
// stringstream 매번 생성 vs 재사용 비교
void benchmark() {
std::vector<std::string> lines(10000);
for (int i = 0; i < 10000; ++i) {
std::ostringstream oss;
for (int j = 0; j < 1000; ++j) {
if (j) oss << ',';
oss << j;
}
lines[i] = oss.str();
}
auto t1 = std::chrono::high_resolution_clock::now();
for (const auto& line : lines) {
std::istringstream iss(line);
std::vector<int> nums;
int x; char c;
while (iss >> x) { nums.push_back(x); if (!(iss >> c)) break; }
}
auto t2 = std::chrono::high_resolution_clock::now();
std::istringstream iss;
std::vector<int> nums;
auto t3 = std::chrono::high_resolution_clock::now();
for (const auto& line : lines) {
iss.clear(); iss.str(line); nums.clear();
int x; char c;
while (iss >> x) { nums.push_back(x); if (!(iss >> c)) break; }
}
auto t4 = std::chrono::high_resolution_clock::now();
}
예상: 재사용 방식이 약 2배 빠름.
7. 프로덕션 패턴
7.1 재사용 가능한 CSV 파서 클래스
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sstream>
#include <string>
#include <vector>
class CsvParser {
public:
explicit CsvParser(char delim = ',') : delim_(delim) {}
std::vector<std::string> split(const std::string& line) {
tokens_.clear();
iss_.clear();
iss_.str(line);
std::string token;
while (std::getline(iss_, token, delim_)) {
tokens_.push_back(token);
}
return tokens_;
}
std::vector<int> parse_as_int(const std::string& line) {
split(line);
std::vector<int> result;
result.reserve(tokens_.size());
for (const auto& t : tokens_) {
result.push_back(std::stoi(t));
}
return result;
}
std::vector<double> parse_as_double(const std::string& line) {
split(line);
std::vector<double> result;
result.reserve(tokens_.size());
for (const auto& t : tokens_) {
result.push_back(std::stod(t));
}
return result;
}
private:
char delim_;
std::istringstream iss_;
std::vector<std::string> tokens_;
};
7.2 로그 라인 파서 (타임스탬프, 레벨, 메시지)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <string_view>
#include <optional>
struct LogEntry {
std::string_view timestamp;
std::string_view level;
std::string_view message;
};
std::optional<LogEntry> parse_log_line(std::string_view line) {
// 형식: "2026-03-10 12:00:00 [INFO] message here"
size_t t_end = line.find(' ');
if (t_end == std::string_view::npos) return std::nullopt;
std::string_view ts = line.substr(0, t_end);
size_t level_start = line.find('[', t_end);
if (level_start == std::string_view::npos) return std::nullopt;
size_t level_end = line.find(']', level_start);
if (level_end == std::string_view::npos) return std::nullopt;
std::string_view level = line.substr(level_start + 1, level_end - level_start - 1);
size_t msg_start = line.find(' ', level_end);
std::string_view msg = (msg_start == std::string_view::npos)
? std::string_view{}
: line.substr(msg_start + 1);
return LogEntry{ts, level, msg};
}
7.3 대용량 입력용 스트리밍 파서
한 줄씩 읽어 콜백으로 처리. 전체를 메모리에 올리지 않아 대용량 로그에 적합합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
template<typename Func>
void parse_streaming(std::istream& in, Func&& on_line) {
std::string line;
line.reserve(4096);
while (std::getline(in, line)) on_line(line);
}
7.4 에러 처리와 로깅이 있는 파서
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <string>
#include <sstream>
#include <vector>
struct ParseResult {
std::vector<int> values;
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)) {
try {
result.values.push_back(std::stoi(token));
} catch (const std::exception& e) {
result.ok = false;
result.error = "Invalid number: " + token;
return result;
}
}
result.ok = true;
return result;
}
7.5 프로덕션용 trim + split 파이프라인
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::vector<std::string> parse_line_production(const std::string& line, char delim = ',') {
auto start = line.find_first_not_of(" \t\n\r");
if (start == std::string::npos) return {};
auto end = line.find_last_not_of(" \t\n\r");
std::string_view trimmed(line.data() + start, end - start + 1);
std::vector<std::string> result;
for (size_t pos = 0; pos <= trimmed.size(); ) {
size_t next = trimmed.find(delim, pos);
if (next == std::string_view::npos) {
result.push_back(std::string(trimmed.substr(pos)));
break;
}
result.push_back(std::string(trimmed.substr(pos, next - pos)));
pos = next + 1;
}
return result;
}
8. 문자열 파싱 베스트 프랙티스
8.1 상황별 기법 선택
| 상황 | 권장 기법 | 이유 |
|---|---|---|
| 코테 단순 split | getline + stringstream 재사용 | 구현 간단, 충분히 빠름 |
| 대량 로그/스트리밍 | string_view + find | 제로카피, 메모리 절약 |
| 원본 수정 가능 | strtok | 최고 속도 |
| 복잡한 패턴 | std::regex (루프 밖 생성) | 유연성 |
| CSV 따옴표 필드 | 전용 CSV 파서 | RFC 4180 준수 |
| JSON | nlohmann/json 등 라이브러리 | 정확성·유지보수 |
8.2 성능·안전성 체크리스트
- 스트림 재사용:
istringstream루프 밖에서 한 번만 생성 - reserve:
vector::reserve(예상_크기)로 재할당 감소 - string_view: 읽기만 할 때 복사 대신 뷰 사용
- npos 체크:
find결과 확인 후substr - from_chars: 외부 입력은
stoi대신from_chars또는try-catch
9. 정리 및 체크리스트
요약 표
| 상황 | 권장 기법 | 비고 |
|---|---|---|
| 코테 CSV/공백 split | getline + stringstream 재사용 | 스트림 한 번만 생성 |
| 대량 로그 파싱 | string_view + find/substr | 제로카피 |
| C 스타일, 원본 수정 가능 | strtok | 가장 빠름 |
| 복잡한 패턴 | std::regex | 단순 split에는 비추천 |
| 프로덕션 CSV | 전용 CsvParser 클래스 | 에러 처리·재사용 |
구현 체크리스트
-
getline전에cin.ignore()필요 여부 확인 -
istringstream은 루프 밖에서 생성 후 재사용 -
vector::reserve로 토큰 개수 예상 시 재할당 감소 -
string_view사용 시 원본 수명 주의 (댕글링 방지) -
strtok사용 시 복사본 전달, 원본 수정 가능 여부 확인 -
stoi/stod실패 가능성 있으면from_chars또는try-catch - 빈 토큰(
"a,,b") 처리 방식 결정
한 줄 요약
stringstream 재사용과 string_view 제로카피로 문자열 파싱 시 할당과 복사를 줄이면, 대량 입력에서 TLE와 메모리 초과를 피할 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 코테에서 가장 빠른 split 방법은?
A. 원본을 수정해도 된다면 strtok이 가장 빠릅니다. 수정이 안 되면 stringstream을 한 번만 생성하고 str()으로 재사용하는 방식을 권장합니다.
Q. string_view를 반환해도 되나요?
A. string_view가 가리키는 원본 문자열이 호출자에서 더 오래 유지되는 경우에만 반환합니다. 함수 내 지역 string을 가리키는 string_view를 반환하면 댕글링이 발생합니다.
Q. 정규식은 언제 쓰나요?
A. 이메일, URL, 복잡한 포맷 검증 등 패턴이 복잡할 때 사용합니다. 단순 구분자 split에는 getline이나 find가 훨씬 빠릅니다.
Q. UTF-8 한글 파싱은?
A. std::string은 바이트 시퀀스이므로, UTF-8의 경우 코드 포인트 단위로 자르려면 std::mbrtoc32 또는 ICU 같은 라이브러리를 사용합니다. 단순 바이트 구분자(,, 공백) split은 기존 방법 그대로 사용 가능합니다.
한 줄 요약: stringstream 재사용·string_view 제로카피로 문자열 파싱을 빠르게 할 수 있습니다. 다음으로 STL 치트시트(#32-3)를 읽어보면 좋습니다. 다음 글: [C++ 코테 압축 #32-3] 코테용 STL 컨테이너/알고리즘 시간복잡도 치트시트 이전 글: [C++ 코테 압축 #32-1] 백준/프로그래머스 C++ 세팅과 입출력 최적화 완벽 정리
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 문자열 알고리즘 완벽 가이드 | split·join·trim·replace·정규식 [실전]
- C++ std::string_view·std::span 완벽 가이드 | 제로카피 뷰·댕글링 방지
- C++ 문자열 기초 완벽 가이드 | std::string·C 문자열·string_view와 실전 패턴
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가? 이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.