본문으로 건너뛰기 C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교

C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교

C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교

이 글의 핵심

C++ string vs string_view 비교. 읽기 전용·인자 전달은 복사 없는 string_view가 유리하고, 소유·수정은 string. 할당·복사 비용 차이와 댕글링 주의, 실무 선택 기준을 정리합니다.

들어가며

C++17의 string_view문자열을 복사하지 않고 참조만 하는 경량 타입입니다. 함수 매개변수로 사용하면 복사 비용을 제거할 수 있습니다. 비유로 말씀드리면, string책을 사서 책장에 꽂는 것(소유), string_view책 제목만 적힌 메모를 들고 원본 책을 가리키는 것(비소유)에 가깝습니다. 메모만 남기고 원본을 반납하면 내용을 읽을 수 없습니다(댕글링).

이 글을 읽으면

  • string과 string_view의 차이를 이해합니다
  • 성능 비교와 복사 비용을 파악합니다
  • 함수 매개변수 선택 기준을 익힙니다
  • 댕글링 포인터 주의사항을 확인합니다

string vs string_view 비교

비교표

항목std::stringstd::string_view
소유권소유 (힙 할당)비소유 (참조만)
복사 비용높음 (문자 복사)낮음 (포인터 복사)
수정가능불가능 (읽기 전용)
크기32바이트 (구현 의존)16바이트 (포인터 + 길이)
null 종료보장보장 안 됨
수명자동 관리수동 관리 (주의)

내부 구조

// std::string (간략화)
class string {
    char* data_;     // 힙 메모리 포인터
    size_t size_;    // 길이
    size_t capacity_; // 용량
};
// std::string_view
class string_view {
    const char* data_;  // 원본 포인터 (비소유)
    size_t size_;       // 길이
};

실전 구현

1) 함수 매개변수

#include <iostream>
#include <string>
#include <string_view>
// ❌ 복사 발생
void printString(std::string s) {  // 값 전달 → 복사
    std::cout << s << std::endl;
}
// ✅ 복사 없음
void printStringView(std::string_view s) {  // 뷰 → 복사 없음
    std::cout << s << std::endl;
}
int main() {
    std::string str = "Hello, World!";
    
    printString(str);      // 복사 발생
    printStringView(str);  // 복사 없음
    
    return 0;
}

2) 부분 문자열

#include <iostream>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello, World!";
    
    // string: 복사 발생
    std::string sub1 = str.substr(0, 5);  // "Hello" 복사
    std::cout << sub1 << std::endl;
    
    // string_view: 복사 없음
    std::string_view sv = str;
    std::string_view sub2 = sv.substr(0, 5);  // 포인터 + 길이만
    std::cout << sub2 << std::endl;
    
    return 0;
}

3) 문자열 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view s, char delim) {
    std::vector<std::string_view> tokens;
    size_t start = 0;
    
    while (start < s.size()) {
        size_t end = s.find(delim, start);
        if (end == std::string_view::npos) {
            tokens.push_back(s.substr(start));
            break;
        }
        
        tokens.push_back(s.substr(start, end - start));
        start = end + 1;
    }
    
    return tokens;
}
int main() {
    std::string data = "apple,banana,cherry";
    auto tokens = split(data, ',');  // 복사 없음
    
    for (auto token : tokens) {
        std::cout << token << std::endl;
    }
    
    return 0;
}

출력:

apple
banana
cherry

4) 로깅

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class Logger {
private:
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {  // 매개변수는 string_view
        logs_.emplace_back(msg);  // string으로 변환해 저장
    }
    
    const std::string& getLog(size_t idx) const {
        return logs_[idx];
    }
    
    size_t size() const {
        return logs_.size();
    }
};
int main() {
    Logger logger;
    
    logger.log("시작");
    logger.log("처리 중");
    logger.log("완료");
    
    for (size_t i = 0; i < logger.size(); ++i) {
        std::cout << logger.getLog(i) << std::endl;
    }
    
    return 0;
}

고급 활용

1) 접두사/접미사 체크 (C++20)

#include <iostream>
#include <string_view>
int main() {
    std::string_view sv = "Hello, World!";
    
    // C++20: starts_with/ends_with
    bool b1 = sv.starts_with("Hello");  // true
    bool b2 = sv.ends_with("World!");   // true
    bool b3 = sv.starts_with("Hi");     // false
    
    std::cout << std::boolalpha;
    std::cout << "starts_with(\"Hello\"): " << b1 << std::endl;
    std::cout << "ends_with(\"World!\"): " << b2 << std::endl;
    std::cout << "starts_with(\"Hi\"): " << b3 << std::endl;
    
    return 0;
}

2) 문자열 트림

#include <iostream>
#include <string_view>
std::string_view trim(std::string_view s) {
    size_t start = 0;
    while (start < s.size() && std::isspace(s[start])) {
        ++start;
    }
    
    size_t end = s.size();
    while (end > start && std::isspace(s[end - 1])) {
        --end;
    }
    
    return s.substr(start, end - start);
}
int main() {
    std::string str = "  Hello, World!  ";
    std::string_view trimmed = trim(str);
    
    std::cout << "[" << trimmed << "]" << std::endl;  // [Hello, World!]
    
    return 0;
}

3) 문자열 비교 최적화

#include <iostream>
#include <string>
#include <string_view>
bool isCommand(std::string_view input, std::string_view command) {
    return input == command;
}
int main() {
    std::string input = "quit";
    
    // string_view로 비교 (복사 없음)
    if (isCommand(input, "quit")) {
        std::cout << "종료" << std::endl;
    } else if (isCommand(input, "help")) {
        std::cout << "도움말" << std::endl;
    }
    
    return 0;
}

성능 비교

벤치마크 1: 함수 매개변수 전달

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
void benchString(std::string s) {
    // 읽기만
}
void benchStringRef(const std::string& s) {
    // 읽기만
}
void benchStringView(std::string_view s) {
    // 읽기만
}
int main() {
    std::string str = "Hello, World! This is a test string.";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchString(str);
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringRef(str);
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    auto start3 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringView(str);
    }
    auto end3 = std::chrono::high_resolution_clock::now();
    auto time3 = std::chrono::duration_cast<std::chrono::milliseconds>(end3 - start3).count();
    
    std::cout << "string (값): " << time1 << "ms" << std::endl;
    std::cout << "const string&: " << time2 << "ms" << std::endl;
    std::cout << "string_view: " << time3 << "ms" << std::endl;
    
    return 0;
}

결과 (GCC 13, -O3):

매개변수 타입시간상대 속도
std::string (값)850ms17x
const std::string&50ms1.0x
std::string_view50ms1.0x
분석: string_view는 const string& 와 동일한 성능

벤치마크 2: 부분 문자열

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello, World!";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string sub = str.substr(0, 5);  // 복사
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string_view sv = str;
        std::string_view sub = sv.substr(0, 5);  // 복사 없음
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    std::cout << "string::substr: " << time1 << "ms" << std::endl;
    std::cout << "string_view::substr: " << time2 << "ms" << std::endl;
    
    return 0;
}

결과 (GCC 13, -O3):

방법시간상대 속도
string::substr120ms24x
string_view::substr5ms1.0x
분석: string_view가 24배 빠름 (복사 없음)

실무 사례

사례 1: HTTP 요청 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
class HttpRequest {
private:
    std::string method_;
    std::string path_;
    std::unordered_map<std::string, std::string> headers_;
    
public:
    void parse(std::string_view request) {
        size_t methodEnd = request.find(' ');
        method_ = request.substr(0, methodEnd);
        
        size_t pathStart = methodEnd + 1;
        size_t pathEnd = request.find(' ', pathStart);
        path_ = request.substr(pathStart, pathEnd - pathStart);
    }
    
    std::string_view getMethod() const {
        return method_;
    }
    
    std::string_view getPath() const {
        return path_;
    }
};
int main() {
    std::string request = "GET /api/users HTTP/1.1";
    
    HttpRequest req;
    req.parse(request);
    
    std::cout << "Method: " << req.getMethod() << std::endl;
    std::cout << "Path: " << req.getPath() << std::endl;
    
    return 0;
}

사례 2: CSV 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
std::vector<std::string_view> parseCSVLine(std::string_view line) {
    std::vector<std::string_view> fields;
    size_t start = 0;
    
    while (start < line.size()) {
        size_t end = line.find(',', start);
        if (end == std::string_view::npos) {
            fields.push_back(line.substr(start));
            break;
        }
        
        fields.push_back(line.substr(start, end - start));
        start = end + 1;
    }
    
    return fields;
}
int main() {
    std::string line = "홍길동,30,서울";
    auto fields = parseCSVLine(line);
    
    std::cout << "이름: " << fields[0] << std::endl;
    std::cout << "나이: " << fields[1] << std::endl;
    std::cout << "주소: " << fields[2] << std::endl;
    
    return 0;
}

사례 3: 로그 필터링

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class LogFilter {
public:
    bool shouldLog(std::string_view level, std::string_view message) {
        if (level == "DEBUG") {
            return false;  // DEBUG 레벨 필터링
        }
        
        if (message.find("password") != std::string_view::npos) {
            return false;  // 민감 정보 필터링
        }
        
        return true;
    }
};
int main() {
    LogFilter filter;
    
    std::string log1 = "DEBUG: 디버그 메시지";
    std::string log2 = "INFO: 사용자 로그인";
    std::string log3 = "ERROR: password 오류";
    
    if (filter.shouldLog("DEBUG", log1)) {
        std::cout << log1 << std::endl;
    }
    
    if (filter.shouldLog("INFO", log2)) {
        std::cout << log2 << std::endl;
    }
    
    if (filter.shouldLog("ERROR", log3)) {
        std::cout << log3 << std::endl;
    }
    
    return 0;
}

사례 4: 명령어 처리

#include <iostream>
#include <string>
#include <string_view>
class CommandHandler {
public:
    void handleCommand(std::string_view cmd) {
        if (cmd == "quit" || cmd == "exit") {
            std::cout << "종료" << std::endl;
        } else if (cmd.starts_with("echo ")) {
            std::string_view msg = cmd.substr(5);
            std::cout << msg << std::endl;
        } else if (cmd == "help") {
            std::cout << "도움말" << std::endl;
        } else {
            std::cout << "알 수 없는 명령어" << std::endl;
        }
    }
};
int main() {
    CommandHandler handler;
    
    handler.handleCommand("echo Hello");
    handler.handleCommand("help");
    handler.handleCommand("quit");
    
    return 0;
}

트러블슈팅

문제 1: 댕글링 포인터

증상: 소멸된 문자열 참조

// ❌ 댕글링
std::string_view getSuffix() {
    std::string str = "Hello, World!";
    return std::string_view(str).substr(7);  // "World!"
}  // str 소멸
int main() {
    auto sv = getSuffix();
    std::cout << sv << std::endl;  // ❌ 소멸된 문자열 참조
    
    return 0;
}
// ✅ string 반환
std::string getSuffix() {
    std::string str = "Hello, World!";
    return str.substr(7);  // string 반환
}
int main() {
    auto s = getSuffix();
    std::cout << s << std::endl;  // ✅ OK
    
    return 0;
}

문제 2: 임시 string에서 string_view

증상: 임시 객체 소멸

// ❌ 임시 객체
std::string_view sv = std::string("Hello");  // 임시 객체
std::cout << sv << std::endl;  // ❌ 임시 객체 이미 소멸
// ✅ string 저장
std::string s = std::string("Hello");
std::string_view sv = s;
std::cout << sv << std::endl;  // ✅ OK
// 또는 string_view 리터럴
using namespace std::string_view_literals;
std::string_view sv2 = "Hello"sv;
std::cout << sv2 << std::endl;  // ✅ OK

문제 3: null 종료 보장 안 됨

증상: C API 연동 오류

#include <cstdio>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello";
    std::string_view sv = str;
    
    // ❌ null 종료 보장 안 됨
    const char* cstr = sv.data();
    printf("%s\n", cstr);  // 위험 (null 종료 보장 안 됨)
    
    // ✅ string으로 변환
    std::string s(sv);
    const char* cstr2 = s.c_str();  // null 종료 보장
    printf("%s\n", cstr2);
    
    return 0;
}

문제 4: string_view를 멤버 변수로 저장

증상: 댕글링 포인터

// ❌ string_view를 멤버로 저장
class Logger {
private:
    std::vector<std::string_view> logs_;  // ❌ 위험
    
public:
    void log(std::string_view msg) {
        logs_.push_back(msg);  // 원본 수명 주의
    }
};
// ✅ string으로 저장
class Logger {
private:
    std::vector<std::string> logs_;  // ✅ 안전
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};

마무리

string_view문자열 복사를 제거해 성능을 높이는 강력한 도구입니다.

핵심 요약

  1. string vs string_view
    • string: 소유, 수정 가능
    • string_view: 참조, 읽기 전용
  2. 선택 기준
    • 함수 매개변수: string_view
    • 저장: string
    • 수정: string&
    • C API: string::c_str()
  3. 성능
    • 매개변수 전달: string_view ≈ const string& >>> string (값)
    • 부분 문자열: string_view >>> string
    • 저장: string (string_view는 위험)
  4. 주의사항
    • 댕글링 포인터 주의
    • null 종료 보장 안 됨
    • 임시 객체 주의
    • 멤버 변수로 저장 금지

선택 가이드

상황권장이유
함수 매개변수 (읽기)string_view복사 없음
멤버 변수 (저장)string소유권
함수 매개변수 (수정)string&수정 가능
C API 연동string::c_str()null 종료
임시 객체 생성string소유권 필요

코드 예제 치트시트

// 함수 매개변수: string_view
void print(std::string_view s) {
    std::cout << s << std::endl;
}
// 저장: string
class Logger {
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};
// 부분 문자열: string_view
std::string str = "Hello, World!";
std::string_view sv = str;
std::string_view sub = sv.substr(0, 5);  // 복사 없음
// C API: c_str()
std::string s = "Hello";
printf("%s\n", s.c_str());

다음 단계

참고 자료

  • “Effective Modern C++” - Scott Meyers
  • “C++ Primer” - Stanley Lippman
  • cppreference: https://en.cppreference.com/w/cpp/string/basic_string_view 한 줄 정리: 함수 매개변수는 string_view로 복사를 제거하며, 저장은 string으로 소유권을 확보하며, 원본 수명을 항상 주의한다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ string vs string_view 비교. 읽기 전용·인자 전달은 복사 없는 string_view가 유리하며, 소유·수정은 string. 할당·복사 비용 차이와 댕글링 주의, 실무 선택 기준을 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


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

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


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

C++, string, string_view, 성능최적화, 문자열, C++17 등으로 검색하시면 이 글이 도움이 됩니다.