PHpullh
언어 비교/문자열 처리

LANGUAGE COMPARISON

문자열 처리를 언어별로 비교하기

문자열이 불변인지, 무엇을 한 글자로 세는지, 결합 비용이 어디서 발생하는지를 비교합니다.

문자열은 대부분 불변입니다

Java, C#, Python, JavaScript, Go에서 문자열은 한 번 만들면 바뀌지 않습니다. s += "a"는 기존 문자열을 고치는 것이 아니라 새 문자열을 만들어 이름을 다시 붙이는 동작입니다. 한두 번이면 문제없지만 반복문 안에서 수천 번 돌면 그때마다 전체를 복사하게 됩니다.

그래서 대부분의 언어가 결합 전용 도구를 따로 둡니다. Java·C#의 StringBuilder, Go의 strings.Builder, Python의 "".join(list), Rust의 String::push_str이 그것입니다. 반대로 C++의 std::string은 가변이라 제자리에서 덧붙일 수 있고, 대신 복사 시점을 직접 신경 써야 합니다.

보간 문법도 갈립니다. Python의 f-string, Kotlin·JavaScript의 템플릿 문자열처럼 표현식을 문자열 안에 그대로 쓰는 방식이 늘어나는 추세지만, Java는 오래도록 String.format이나 결합에 의존해 왔습니다. 편의의 차이처럼 보여도 SQL이나 HTML을 만들 때는 이스케이프를 어디서 하는가라는 보안 문제로 이어집니다.

Kotlin

문자열 템플릿 & 포맷

문자열 보간(String Interpolation). $변수와 ${표현식}으로 가독성 높은 문자열을 작성합니다.

val lang = "Kotlin"
val version = 2
val score = 98.5

// 변수 직접 삽입
println("언어: $lang")

// 표현식 삽입 ${}
println("다음 버전: ${version + 1}")
println("대문자: ${lang.uppercase()}")
println("점수 등급: ${if (score >= 90) "A" else "B"}")

// 달러 기호 출력
println("가격: $100")
println("가격: ${'$'}100")  // 또는 이렇게

// Triple quote + 템플릿
val report = """
    |언어: $lang
    |버전: $version
    |점수: ${"%.1f".format(score)}점
""".trimMargin()
println(report)

Python

문자열 — f-string & 메서드

Python의 강력한 문자열 처리. f-string, 메서드 체이닝, 슬라이싱을 완전히 익힙니다.

name = "  python developer  "
score = 98.765

# f-string 포맷팅 (Python 3.6+)
print(f"이름: {name.strip()}")
print(f"점수: {score:.2f}")          # 소수점 2자리
print(f"퍼센트: {score / 100:.1%}") # 백분율
print(f"{'중앙정렬':^20}")           # 패딩/정렬
print(f"{1000000:,}")               # 천단위 콤마

# 주요 문자열 메서드
s = "Hello, Python World"
print(s.upper())           # HELLO, PYTHON WORLD
print(s.lower())           # hello, python world
print(s.replace("Python", "Kotlin"))
print(s.split(", "))       # ['Hello', 'Python World']
print(s.startswith("Hello"))  # True
print(s.strip())           # 앞뒤 공백 제거
print(s.find("Python"))    # 7 (인덱스)
print(s.count("l"))        # 3

# 슬라이싱 [start:stop:step]
text = "Hello World"
print(text[0:5])    # Hello
print(text[-5:])    # World
print(text[::-1])   # dlroW olleH (역순)

# join
words = ["Python", "is", "awesome"]
print(" ".join(words))  # Python is awesome

Go

문자열 & rune & strings 패키지

Go 문자열은 바이트 슬라이스입니다. 한글 처리 시 rune 변환이 필요합니다.

package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	s := "Hello, 세계"

	// 바이트 vs rune
	fmt.Println("바이트 길이:", len(s))               // 13
	fmt.Println("문자(rune) 수:", utf8.RuneCountInString(s)) // 9

	// 바이트 순회 (한글 깨짐)
	for i, b := range []byte(s) {
		if i < 7 { fmt.Printf("%d:%c ", i, b) }
	}
	fmt.Println()

	// rune 순회 (올바른 한글 처리)
	for i, r := range s {
		fmt.Printf("%d:%c ", i, r)
	}
	fmt.Println()

	// rune 슬라이싱
	runes := []rune(s)
	fmt.Println("첫 5자:", string(runes[:5]))  // Hello

	// strings 패키지
	fmt.Println(strings.ToUpper("hello"))        // HELLO
	fmt.Println(strings.Contains(s, "세계"))     // true
	fmt.Println(strings.HasPrefix(s, "Hello"))   // true
	fmt.Println(strings.Replace(s, "Hello", "Hi", 1))
	fmt.Println(strings.Split("a,b,c", ","))     // [a b c]
	fmt.Println(strings.Join([]string{"x","y"}, "-")) // x-y
	fmt.Println(strings.TrimSpace("  hello  "))  // hello
	fmt.Println(strings.Count(s, "l"))            // 2

	// strings.Builder — 효율적 문자열 조합
	var b strings.Builder
	for i := 0; i < 5; i++ {
		fmt.Fprintf(&b, "item%d ", i)
	}
	fmt.Println(b.String())
}

Java

문자열 풀과 intern()

String Pool의 동작 원리와 intern() 메서드를 이해합니다.

public class StringPool {
    public static void main(String[] args) {
        // 리터럴 — String Pool에 저장
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2); // true (같은 참조)

        // new — 항상 새 객체 (힙)
        String s3 = new String("hello");
        System.out.println(s1 == s3);      // false
        System.out.println(s1.equals(s3)); // true

        // intern() — Pool에서 가져오기
        String s4 = s3.intern();
        System.out.println(s1 == s4); // true

        // 문자열 연결 최적화 (Java 9+)
        // 컴파일러가 invokedynamic으로 최적화
        String name = "World";
        String greeting = "Hello, " + name + "!";
        System.out.println(greeting);
    }
}

JavaScript

문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

// 문자열 상수 선언
for (let i = 0; i < 3; i += 1) {
  console.log(i + 2);
}

TypeScript

문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

// 문자열 상수 선언
for (let i = 0; i < 3; i += 1) {
  console.log(i + 2);
}

C#

문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

// 문자열 상수 선언
using System;
using System.Linq;

var values = new[] { 3, 1, 4, 2 };
Console.WriteLine(string.Join(", ", values.OrderBy(v => v)));

C++

문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

// 문자열 상수 선언
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> values = {3, 1, 4, 2};
    std::sort(values.begin(), values.end());
    std::cout << values.front() << "\n";
}

Rust

문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

// 문자열 상수 선언
fn main() {
    let mut values = vec![3, 1, 4, 2];
    values.sort();
    println!("{:?}", values);
}

한 글자를 무엇으로 세는가

가장 조용하고 가장 아픈 차이입니다. len(s)가 무엇을 세는지가 언어마다 다릅니다. Go의 len은 바이트 수라 한글 한 글자가 3으로 셉니다. Java, C#, JavaScript는 UTF-16 코드 단위를 세므로 기본 문자는 1이지만 이모지 같은 확장 문자는 2로 셉니다. Python 3은 코드 포인트를 세어 한글도 이모지도 1입니다.

영어로만 테스트하면 셋 다 똑같아 보입니다. 한글이나 이모지가 들어오는 순간 길이 제한, 자르기, 인덱스 접근이 전부 어긋납니다. 문자열을 자를 일이 있다면 그 언어가 무엇을 단위로 삼는지부터 확인하고, 가능하면 언어가 제공하는 문자 단위 순회(Go의 range, Rust의 chars())를 쓰십시오.

여기에 결합 문자까지 오면 더 복잡해집니다. 겉보기에 한 글자인 이모지가 여러 코드 포인트로 이루어질 수 있어서, 어떤 언어에서도 "사람이 보는 한 글자"를 세는 것은 별도의 정규화 처리가 필요합니다.

비교와 정렬은 로케일에 달려 있습니다

==로 하는 비교는 대개 코드 단위를 그대로 견주지만, 사람이 기대하는 "같음"은 다를 수 있습니다. 대소문자를 무시하는 비교는 언어마다 로케일 규칙을 타고, 정렬 순서 역시 사전순과 코드포인트순이 다릅니다. 사용자에게 보여줄 목록을 정렬한다면 로케일을 지정하는 비교 함수를 쓰는 편이 안전합니다.

인코딩은 문자열 밖의 문제입니다

파일에서 읽은 한글이 깨진다면 문자열 API가 아니라 파일 입출력 단계의 인코딩 지정이 원인인 경우가 대부분입니다. 읽을 때와 쓸 때의 인코딩을 명시하고, 기본값에 의존하지 마십시오. 기본값은 실행 환경에 따라 달라집니다.