성능 최적화 완벽 가이드 | C++, Python, Java, JavaScript 언어별 전략

성능 최적화 완벽 가이드 | C++, Python, Java, JavaScript 언어별 전략

이 글의 핵심

성능 최적화 완벽 가이드입니다. 언어별 최적화 기법과 프로파일링 도구, 실무 전략을 상세히 설명합니다.

들어가며: 성능 최적화의 원칙

”내 코드가 왜 느릴까?”

성능 최적화는 측정 → 분석 → 개선 → 검증의 반복입니다. 추측이 아닌 데이터 기반으로 접근해야 합니다.

이 글에서 다루는 것:

  • 언어별 프로파일링 도구
  • 알고리즘 최적화
  • 메모리 최적화
  • 캐싱 전략
  • 실무 최적화 사례

목차

  1. 최적화 원칙
  2. 프로파일링
  3. 알고리즘 최적화
  4. 메모리 최적화
  5. 언어별 최적화
  6. 정리

1. 최적화 원칙

최적화의 3대 원칙

flowchart LR
    A[최적화 시작] --> B[1. 측정]
    B --> C[2. 병목 찾기]
    C --> D[3. 최적화]
    D --> E[4. 검증]
    E --> B

1. 측정 먼저 (Measure First)

❌ "이 코드가 느릴 것 같아" (추측)
✅ "프로파일러로 측정한 결과 이 함수가 80% 시간 소요" (데이터)

2. 병목 찾기 (Find Bottleneck)

전체 실행 시간: 10초
├─ 함수 A: 0.1초 (1%)
├─ 함수 B: 8초 (80%)  ← 병목!
└─ 함수 C: 1.9초 (19%)

→ 함수 B를 최적화하면 가장 큰 효과

3. 80/20 법칙

코드의 20%가 실행 시간의 80%를 차지
→ 그 20%만 최적화하면 충분

최적화 우선순위

graph TB
    A[최적화 우선순위] --> B[1. 알고리즘]
    A --> C[2. 자료구조]
    A --> D[3. 캐싱]
    A --> E[4. 병렬화]
    A --> F[5. 언어/컴파일러]
    
    B --> B1[On² → On]
    C --> C1[배열 → 해시맵]
    D --> D1[중복 계산 제거]
    E --> E1[멀티스레드]
    F --> F1[컴파일러 옵션]

예제:

# ❌ O(n²) 알고리즘
def has_duplicate(arr):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j]:
                return True
    return False

# ✅ O(n) 알고리즘 (해시셋 사용)
def has_duplicate(arr):
    seen = set()
    for x in arr:
        if x in seen:
            return True
        seen.add(x)
    return False

# 성능 차이: 100만 개 배열
# O(n²): 몇 시간
# O(n): 0.1초

2. 프로파일링

언어별 프로파일링 도구

언어도구사용법
C++gprof, Valgrind, perfg++ -pg, valgrind --tool=callgrind
PythoncProfile, line_profilerpython -m cProfile script.py
JavaVisualVM, JProfilerJVM 옵션 또는 IDE 통합
JavaScriptChrome DevTools, Node.js Profilernode --prof script.js

C++ 프로파일링

# gprof 사용
g++ -pg -O2 main.cpp -o main
./main
gprof main gmon.out > analysis.txt

# Valgrind Callgrind
valgrind --tool=callgrind ./main
kcachegrind callgrind.out.*

# perf (Linux)
perf record ./main
perf report

출력 예제:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 80.00      0.80     0.80        1   800.00   800.00  slow_function
 15.00      0.95     0.15   100000     0.00     0.00  fast_function
  5.00      1.00     0.05        1    50.00    50.00  main

Python 프로파일링

import cProfile
import pstats

def slow_function():
    total = 0
    for i in range(1000000):
        total += i
    return total

def fast_function():
    return sum(range(1000000))

# 프로파일링
cProfile.run('slow_function()', 'profile_stats')

# 결과 분석
p = pstats.Stats('profile_stats')
p.sort_stats('cumulative')
p.print_stats(10)

line_profiler (줄 단위 프로파일링):

# pip install line_profiler

@profile
def my_function():
    total = 0
    for i in range(1000000):  # 이 줄이 느림
        total += i
    return total

# 실행
# kernprof -l -v script.py

JavaScript 프로파일링

Chrome DevTools:

// 1. Chrome DevTools 열기 (F12)
// 2. Performance 탭
// 3. Record 버튼 클릭
// 4. 작업 수행
// 5. Stop 버튼 클릭
// 6. Flame Chart 분석

function slowFunction() {
  let total = 0;
  for (let i = 0; i < 1000000; i++) {
    total += i;
  }
  return total;
}

console.time('slowFunction');
slowFunction();
console.timeEnd('slowFunction');
// slowFunction: 5.234ms

Node.js 프로파일링:

# V8 프로파일러
node --prof script.js
node --prof-process isolate-*.log > processed.txt

# Clinic.js
npm install -g clinic
clinic doctor -- node script.js

3. 알고리즘 최적화

시간복잡도 개선

예제 1: 중복 찾기

# ❌ O(n²) - 느림
def find_duplicates(arr):
    duplicates = []
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j] and arr[i] not in duplicates:
                duplicates.append(arr[i])
    return duplicates

# ✅ O(n) - 빠름
def find_duplicates(arr):
    seen = set()
    duplicates = set()
    for x in arr:
        if x in seen:
            duplicates.add(x)
        seen.add(x)
    return list(duplicates)

# 성능 차이: 10만 개 배열
# O(n²): 30초
# O(n): 0.01초

예제 2: 두 수의 합

// ❌ O(n²)
vector<pair<int,int>> twoSum(vector<int>& arr, int target) {
    vector<pair<int,int>> result;
    for (int i = 0; i < arr.size(); i++) {
        for (int j = i + 1; j < arr.size(); j++) {
            if (arr[i] + arr[j] == target) {
                result.push_back({i, j});
            }
        }
    }
    return result;
}

// ✅ O(n) - 해시맵 사용
vector<pair<int,int>> twoSum(vector<int>& arr, int target) {
    unordered_map<int, int> seen;
    vector<pair<int,int>> result;
    
    for (int i = 0; i < arr.size(); i++) {
        int complement = target - arr[i];
        if (seen.find(complement) != seen.end()) {
            result.push_back({seen[complement], i});
        }
        seen[arr[i]] = i;
    }
    return result;
}

캐싱 (메모이제이션)

# ❌ 중복 계산
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# fibonacci(40): 몇 초 소요

# ✅ 메모이제이션
from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# fibonacci(40): 0.001초

4. 메모리 최적화

C++ 메모리 최적화

// ❌ 불필요한 복사
void process(vector<int> data) {  // 복사 발생
    // ...
}

// ✅ 참조 사용
void process(const vector<int>& data) {  // 복사 없음
    // ...
}

// ❌ 작은 객체를 힙에 할당
for (int i = 0; i < 1000000; i++) {
    int* p = new int(i);  // 느림
    delete p;
}

// ✅ 스택 사용
for (int i = 0; i < 1000000; i++) {
    int value = i;  // 빠름
}

// ❌ 빈번한 재할당
vector<int> vec;
for (int i = 0; i < 1000000; i++) {
    vec.push_back(i);  // 재할당 발생
}

// ✅ 미리 예약
vector<int> vec;
vec.reserve(1000000);  // 재할당 방지
for (int i = 0; i < 1000000; i++) {
    vec.push_back(i);
}

Python 메모리 최적화

# ❌ 리스트 (메모리 많이 사용)
numbers = [i for i in range(1000000)]  # 36MB

# ✅ 제너레이터 (메모리 절약)
numbers = (i for i in range(1000000))  # 200 bytes

# ❌ 문자열 연결 (느림)
result = ""
for i in range(10000):
    result += str(i)  # 매번 새 문자열 생성

# ✅ join 사용 (빠름)
result = "".join(str(i) for i in range(10000))

# ❌ 전역 변수 (느림)
global_var = 0
def increment():
    global global_var
    global_var += 1

# ✅ 지역 변수 (빠름)
def increment(var):
    return var + 1

Java 메모리 최적화

// ❌ 불필요한 객체 생성
for (int i = 0; i < 1000000; i++) {
    String s = new String("hello");  // 느림
}

// ✅ 문자열 리터럴 사용
for (int i = 0; i < 1000000; i++) {
    String s = "hello";  // 빠름 (String Pool)
}

// ❌ StringBuilder 없이 연결
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;  // 느림
}

// ✅ StringBuilder 사용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String result = sb.toString();

5. 언어별 최적화

C++ 최적화

컴파일러 최적화:

# 최적화 레벨
g++ -O0 main.cpp  # 최적화 없음 (디버그)
g++ -O1 main.cpp  # 기본 최적화
g++ -O2 main.cpp  # 권장 최적화
g++ -O3 main.cpp  # 공격적 최적화

# 추가 옵션
g++ -O3 -march=native -flto main.cpp
# -march=native: CPU 최적화
# -flto: Link Time Optimization

인라인 함수:

// ❌ 함수 호출 오버헤드
int add(int a, int b) {
    return a + b;
}

// ✅ 인라인 (함수 호출 제거)
inline int add(int a, int b) {
    return a + b;
}

// 또는 람다 (자동 인라인)
auto add = [](int a, int b) { return a + b; };

캐시 친화적 코드:

// ❌ 캐시 미스 많음 (열 우선 접근)
for (int j = 0; j < N; j++) {
    for (int i = 0; i < N; i++) {
        matrix[i][j] = 0;
    }
}

// ✅ 캐시 친화적 (행 우선 접근)
for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        matrix[i][j] = 0;
    }
}

// 성능 차이: 10000×10000 행렬
// 열 우선: 2.5초
// 행 우선: 0.3초 (8배 빠름)

Python 최적화

리스트 컴프리헨션:

# ❌ 느림
result = []
for i in range(1000000):
    result.append(i * 2)

# ✅ 빠름 (2배)
result = [i * 2 for i in range(1000000)]

# ✅ 더 빠름 (제너레이터)
result = (i * 2 for i in range(1000000))

내장 함수 사용:

# ❌ 느림
total = 0
for i in range(1000000):
    total += i

# ✅ 빠름 (10배)
total = sum(range(1000000))

NumPy 사용:

import numpy as np

# ❌ Python 루프 (느림)
arr = list(range(1000000))
result = [x * 2 for x in arr]
# 시간: 100ms

# ✅ NumPy (빠름)
arr = np.arange(1000000)
result = arr * 2
# 시간: 2ms (50배 빠름)

Java 최적화

Stream vs 반복문:

List<Integer> numbers = IntStream.range(0, 1000000)
    .boxed()
    .collect(Collectors.toList());

// ❌ Stream (느림)
long sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();
// 시간: 50ms

// ✅ 반복문 (빠름)
long sum = 0;
for (int num : numbers) {
    sum += num;
}
// 시간: 10ms (5배 빠름)

오토박싱 회피:

// ❌ 오토박싱 (느림)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add(i);  // int → Integer 변환
}

// ✅ 원시 타입 배열 (빠름)
int[] arr = new int[1000000];
for (int i = 0; i < 1000000; i++) {
    arr[i] = i;
}

JavaScript 최적화

배열 메서드 최적화:

const arr = Array.from({ length: 1000000 }, (_, i) => i);

// ❌ 느림
let sum = 0;
arr.forEach(x => sum += x);
// 시간: 20ms

// ✅ 빠름
let sum = 0;
for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
}
// 시간: 5ms (4배 빠름)

// ✅ 더 빠름 (내장 함수)
const sum = arr.reduce((acc, x) => acc + x, 0);
// 시간: 8ms

객체 생성 최적화:

// ❌ 느림
const objects = [];
for (let i = 0; i < 100000; i++) {
    objects.push({ id: i, name: `User${i}` });
}

// ✅ 빠름 (미리 할당)
const objects = new Array(100000);
for (let i = 0; i < 100000; i++) {
    objects[i] = { id: i, name: `User${i}` };
}

6. 정리

최적화 체크리스트

측정:

  • 프로파일러로 병목 확인
  • 실행 시간 측정
  • 메모리 사용량 측정

알고리즘:

  • 시간복잡도 개선 (O(n²) → O(n))
  • 적절한 자료구조 선택
  • 캐싱/메모이제이션

메모리:

  • 불필요한 복사 제거
  • 메모리 누수 확인
  • 객체 재사용

언어별:

  • C++: 컴파일러 최적화, 인라인, 캐시 친화적
  • Python: 내장 함수, NumPy, 제너레이터
  • Java: 오토박싱 회피, StringBuilder
  • JavaScript: 배열 메서드, 객체 풀

핵심 원칙

  1. 측정 먼저: 추측하지 말고 측정
  2. 병목 집중: 80/20 법칙
  3. 알고리즘 우선: 언어보다 알고리즘
  4. 가독성 유지: 과도한 최적화 금지

다음 단계

각 언어의 자세한 최적화 기법은 아래 글을 참고하세요:

  • C++ 성능 최적화
  • Python 성능 최적화
  • 웹 성능 최적화

관련 주제:

  • 알고리즘 시간복잡도
  • 캐시 최적화
  • 병렬 프로그래밍