코딩테스트 사이트의 같은 문제Python 으로 2.3 초 에 푼다. 같은 알고리즘을 Java 로 옮기면 0.4 초 에 끝난다. 알고리즘 복잡도같은 O(n log n).

왜 *6 배 차이가 나는가*?

그리고 그 반대 케이스 — Java 로 짠 코드가 *Python 보다 *더 메모리 를 쓰는* 케이스도 존재 한다. 어느 언어가 *어떤 알고리즘유리한지그렇게 단순하지 않다.

이 글은 같은 알고리즘 을 *두 언어 로 구현 했을 때 왜 성능이 달라지는가7 가지 결정적 차원 으로 풀어본다. 단순 syntax 비교가 아니라 런타임 의 *구조적 차이알고리즘 성능 에 *직접 반영 되는 그림*.


TL;DR

Python 과 Java 는 같은 알고리즘 을 다른 런타임 위에서 실행 한다. JIT 컴파일 / GC 모델 / 컬렉션 메모리 레이아웃 / 동시성 모델 / 재귀 한계7 가지 차이체감 성능5~50 배 까지 영향. 언어 선택알고리즘 성격제약 조건함수.


1. *Layer 1 — *반복문의 비용**

가장 체감 차이가 큰 부분.

1.1 *Python 의 *모든 줄이 *interpreter dispatch**

total = 0
for i in range(1_000_000):
    total += i
  • +=bytecode 명령 으로 해석
  • range, i, total 모두 PyObjectbox 된 정수
  • 각 연산이 함수 호출 + ref-count 증감 + GIL 확인

CPython 의 반복문 1 회 비용 : 수십 ns ~ 100 ns.

1.2 *Java 의 *JIT-compiled 반복문**

long total = 0;
for (int i = 0; i < 1_000_000; i++) {
    total += i;
}
  • primitive int — 박싱 없음. 4 byte 의 스택 변수
  • HotSpot C2 컴파일러수십 번 호출 후 native code 로 컴파일
  • 컴파일 후 반복문 1 회 = 1~2 ns (CPU pipeline 가 한 사이클 in)

Python : Java = 50 ~ 100 : 1 (단순 산술 반복문 기준).

1.3 그래서 Python 의 *반복문은 *벡터화 하라*

# 느림
total = 0
for i in range(N):
    total += arr[i]

# 빠름 (C 구현 활용)
total = sum(arr)
total = np.sum(arr)  # 더 빠름 (SIMD)

sum, numpy, pandasC / Fortran 백엔드 호출반복문 비용 우회. Java 의 JIT 가 자연스럽게 제공 하는 것을 Python 은 라이브러리로 의식적으로 호출 해야 함.


2. *Layer 2 — *컬렉션의 *메모리 레이아웃**

2.1 *Python list — *PyObject 포인터 배열**

arr = [1, 2, 3, 4]

내부 구조 :

arr  →  [length, allocated, ob_item ptr ─→  [PyObj*, PyObj*, PyObj*, PyObj*]]
                                              ↓        ↓        ↓        ↓
                                            int(1)  int(2)  int(3)  int(4)
  • 각 원소가 PyObject (28 byte for int)
  • list 본체 + 4 개의 PyObject = 약 150 byte (32 byte list + 4×28 byte int)
  • 정수 4 개에 150 byte — *Java 의 int[4] (16 byte) 의 *10 배**

2.2 *Java ArrayList — *Boxed Integer 배열**

List<Integer> arr = new ArrayList<>(List.of(1, 2, 3, 4));
  • Java 의 Integerboxed int (16 byte + header)
  • ArrayList 내부 Object[] + 4 개 Integer = 약 80 byte
  • int[] 라면 16 byte (4 byte × 4 + 4 byte header padding)

2.3 비교 표

자료구조 4 개 정수 메모리 비고
Python list ~150 byte 모든 원소가 PyObject
Python array.array("i", [...]) ~80 byte primitive 배열
Python numpy.array([...], dtype=int32) 16 byte C buffer
Java List<Integer> (ArrayList) ~80 byte Boxed Integer
Java int[] 16 byte primitive 배열

같은 논리적 자료구조가 언어 + 변형에 따라 10 배 메모리 차이. 알고리즘의 공간 복잡도가 언어에 따라 재정의된다.


3. Layer 3 — *재귀와 스택

3.1 *Python 의 *재귀 한계**

기본값 1000. 이걸 넘으면 RecursionError.

import sys
sys.setrecursionlimit(10000)  # 늘릴 수 있지만 OS 스택 한계 따로

DFS / 분할정복 / 동적계획법 같은 깊은 재귀가 Python 에서 치명적. 보통 반복 + 명시 스택 으로 재작성 해야 한다.

3.2 *Java 의 *JVM 스택 크기**

JVM thread 당 기본 1 MB stack (Linux). -Xss2m늘릴 수 있다. 재귀 깊이 수만 단계 까지 현실적.

java -Xss4m MyDFS  // 4 MB stack

3.3 왜 Python 이 *재귀에 보수적 인가*

Python 의 함수 호출 1 회 비용크다 (수 μs). 재귀 자체가 interpreter 입장에서 비싸다. 반복문이 항상 우선재귀 한계 작게 두는 게 의도된 디자인.

Java 는 JIT 이 tail call 도 일부 최적화 하고 함수 호출 비용이 작다 → 재귀 친화적. 다만 공식 tail call optimization 은 없음 (Scala / Kotlin 같은 다른 JVM 언어는 일부 지원).


4. Layer 4 — *해시 자료구조

4.1 *Python dict — *Open addressing**

Python 3.7+ 의 dict 는 insertion order 유지 + open addressing 해시.

  • 충돌 시 다음 slot 으로 probe
  • load factor 2/3 넘으면 resize
  • 해시 함수secure (randomized seed) — 같은 key 가 매번 다른 hash

4.2 *Java HashMap — *Chaining + Treeify**

Java 8+ 의 HashMap :

  • 충돌 시 같은 bucket 의 linked list
  • list 길이 8 넘으면 red-black tree 로 변환 (worst-case O(log n) 보장)
  • 해시 충돌 공격 (DoS)내성

4.3 체감 성능 차이

dict.get(key)map.get(key) :

  • Python : 수십 ns (해시 + slot 접근)
  • Java : 수 ns (JIT 후)

10 배 차이. 다만 Python dict 의 최적화가 매년 좋아짐. Python 3.12 의 PEP 659 (Specializing Adaptive Interpreter)반복 접근 패턴을 학습bytecode 를 전문화 — JIT 없이도 수 배 빠르다.

4.4 Set 비교

연산 Python set Java HashSet
add / contains O(1) avg O(1) avg
충돌 시 worst O(n) — open addressing O(log n) — treeify
메모리 (n=100만 정수) 약 50 MB 약 60 MB (boxed)

Java 의 primitive HashSet 은 표준 라이브러리에 없음Eclipse Collections 또는 fastutil 같은 external 라이브러리가 primitive HashSet 을 제공.


5. Layer 5 — *정렬 알고리즘

5.1 *둘 다 *Timsort 를 *기본**

흥미로운 수렴Python list.sort() 의 Timsort 가 2002 년 Tim Peters 작. Java 가 그걸 채택Java 7+ 의 Arrays.sort(Object[]) 의 기본.

Timsort 의 특징 :

  • Merge sort 변형
  • 이미 정렬된 부분 (run)감지해 최적화
  • Worst-case O(n log n), best-case O(n) (정렬된 입력)

5.2 Java 의 *Dual-Pivot Quicksort (primitive 배열)*

Arrays.sort(int[])Dual-Pivot Quicksort (Vladimir Yaroslavskiy, 2009).

  • Pivot 2 개로 3 분할
  • quicksort 의 cache locality + primitive 의 cache line 최적화
  • Timsort 보다 primitive 에선 더 빠름

5.3 실측 비교 (1 백만 랜덤 정수)

구현 시간
Python sorted() (Timsort) ~150 ms
Java Arrays.sort(int[]) (Dual-Pivot) ~70 ms
Java Collections.sort(List<Integer>) (Timsort) ~200 ms (박싱 비용)
numpy np.sort() ~30 ms (SIMD + C)

Python 의 sorted 가 Java 의 primitive sort 보다 2 배 느림. Java 의 Boxed sort 가 primitive sort 보다 3 배 느림. numpy 가 전부 합쳐 가장 빠름.

Java 가 primitive 정렬에서 압도적. Python 은 numpy 우회가 유일한 근접 옵션.


6. Layer 6 — *문자열 다루기

6.1 둘 다 *불변 (immutable)

s = s + "x"새 문자열 생성. 매 반복마다 O(len(s)) 비용. 결과 : O(n²) — 흔한 느린 코드.

6.2 *해결 — 내장 *string builder 패턴**

# Python — list + join
parts = []
for w in words:
    parts.append(w)
result = "".join(parts)  # O(n)
// Java — StringBuilder
StringBuilder sb = new StringBuilder();
for (String w : words) {
    sb.append(w);
}
String result = sb.toString();  // O(n)

6.3 String interning

  • Python : 짧은 string (60 자 이하) + 식별자 패턴 자동 intern
  • Java : String literal 은 자동 intern + String.intern() 명시 가능

같은 string 비교 시 is (Python) / == (Java) 는 interned 면 O(1), 아니면 동작 다름.

6.4 String 비교 비용

연산 Python Java
== / equals length 다르면 즉시 false, 같으면 byte 비교 동일
hash PEP 456 — SipHash javahash (간단)
substring O(k) copy Java 7 이후 *O(k) copy (이전엔 O(1) view)*

Java 6 까지는 substring 이 O(1) view — 메모리 누수 위험으로 Java 7 부터 *O(k) copy 로 변경. *역사적 trade-off.


7. Layer 7 — *동시성 / 병렬

7.1 *Python 의 *GIL — 진정한 병렬이 못 됨**

CPython 의 Global Interpreter Lock한 시점에 한 thread 만 bytecode 실행. 멀티코어 CPU 를 못 살린다.

해결책 :

  • multiprocessing — 별도 프로세스. 통신 비용 큼
  • asyncio — 단일 thread coroutine. I/O bound 에 적합
  • numpy / pandas / scipyC 백엔드는 GIL 해제 후 동작

Python 3.13+ 의 no-GIL 빌드 (PEP 703) — 실험적. 곧 기본 옵션 가능성.

7.2 *Java 의 *진정한 멀티스레드**

JVM thread 는 OS thread 1:1 매핑멀티코어 직접 활용.

  • synchronized — 모니터 락
  • java.util.concurrent — ConcurrentHashMap, ExecutorService 등 풍부
  • Virtual Threads (Java 21+) — 경량 thread, Project Loom

7.3 알고리즘에 미치는 영향

알고리즘 Python Java
Merge sort 병렬화 멀티프로세스 (overhead 큼) ForkJoinPool 한 줄
그래프 BFS 병렬 어렵다 (GIL) 자연스럽다
행렬 곱셈 numpy 가 *내부에서 BLAS 호출 (GIL 해제)* JIT + SIMD

CPU-bound 병렬 알고리즘은 Java 가 자연스럽고 Python 은 우회 필요.


8. *Layer 8 — *Garbage Collection 의 *알고리즘 영향**

8.1 *Python — *Reference counting + Cycle detector**

  • 객체마다 참조 카운트. 0 되면 즉시 해제
  • 순환 참조주기적 detector 가 처리
  • 예측 가능한 GC큰 일시 멈춤 없음

8.2 *Java — *Generational GC**

  • Young generation + Old generation
  • G1GC (기본) / ZGC (저지연) / Shenandoah
  • Stop-the-world pause수 ms ~ 수 100 ms

8.3 알고리즘이 받는 영향

대량의 임시 객체 (예: 매 반복마다 new ArrayList) :

  • Python : 즉시 해제 — 메모리 천천히 증가
  • Java : Young heap 에 쌓이다 GC — Young GC 마다 짧은 멈춤

p99 응답시간 민감한 서비스 에선 Java 의 GC pausePython 의 예측가능성 보다 덜 friendly. 다만 Python 의 느린 throughput대부분 더 큰 문제.


9. 실전 — *어느 언어가 *어느 알고리즘 에 *유리한가**

9.1 Python 이 유리한 경우

  • 프로토타이핑 / EDA / 데이터 분석 — 코드 짧고 빠른 작성
  • Numpy / Pandas / SciPy 가 적용 가능한 수치 계산C 백엔드
  • 기계학습 / 통계생태계 압도적
  • 간단한 알고리즘 + I/O 위주 — JIT 없어도 충분
  • 작은 N (입력 < 10⁵) — 6배 차이 나도 체감 작음

9.2 Java 가 유리한 경우

  • 큰 N (입력 > 10⁶) — JIT + primitive 의 5~50 배 차이
  • CPU bound + 멀티스레드
  • 재귀 깊이 큰 알고리즘 (DFS 깊은 그래프, divide & conquer)
  • 메모리 효율 중요 — primitive 배열의 10 배 절약
  • long-running 서비스 — JIT warm-up 후 극대 성능

9.3 *코딩테스트 관점 — *시간 제한**

같은 문제, 같은 알고리즘 :

채점 Python 제한 Java 제한 비율
백준 3 배 보너스 1 배 기준 3 : 1
LeetCode 동일 시간 동일 시간 Python 에 불리
프로그래머스 5 배 보너스 1 배 기준 5 : 1

경쟁 프로그래밍의 현실적 시간 차이가 3~5 배 보너스로 역사적 보정. LeetCode 처럼 보너스 없는 경우 Python 으로 시간 초과 흔함.


10. *Big-O 가 *거짓말 하는 순간**

이론적 복잡도 O(n log n) 인 두 구현이 50 배 차이 가 나는 이유 :

  • 상수 항 차이 (Java 의 cache-friendly primitive 배열 vs Python 의 boxed object 포인터 추적)
  • Memory access pattern연속 메모리 (Java int[]) 가 cache hit rate 압도
  • 분기 예측 — JIT 가 반복문 패턴 학습 후 분기 예측 최적화
  • SIMD 자동 벡터화 — JIT 가 수동 같은 반복문을 4~8 배 가속

Big-O 는 추세만 말해줄 뿐 체감 성능은 상수 항이 결정한다. 그 상수 항이 런타임의 모든 layer 의 합산.


11. 내 sparta-msa-project 의 사례

코딩테스트 채점 시스템 운영 사례 :

  • judge-engine 자체C++ (seccomp + cgroup, 0 latency target)
  • Java 제출 — JIT warm-up 못 함 → primitive 배열 + 명시 StringBuilder 권장
  • Python 제출sys.stdin / readline + for 대신 list comprehension 권장

같은 문제 (N=10⁶ 정렬 + 이진 탐색) 의 평균 실행 시간 :

  • C++ : 0.08 초
  • Java : 0.4 초
  • Python (vanilla) : 2.3 초
  • Python (numpy) : 0.6 초

C++ : Java : Python = 1 : 5 : 30 (대략). Python + numpy 는 Java 와 비슷.

lemuel-quant-core 의 R 모듈벡터화된 R 코드 = numpy 의 R 버전 — 같은 패턴.


12. 교훈 — *언어는 *알고리즘의 *상수 항 을 결정한다***

“알고리즘은 Big-O 가 전부가 아니다. *언어 의 *런타임 모델알고리즘 의 체감 성능 을 결정한다. 같은 알고리즘이 어느 언어에서 빠른지를 알면 문제와 도구의 짝지음 이 가능해진다.”

Python 의 읽기 좋고 생태계 풍부. Java 의 primitive + JIT + 진정한 멀티스레드. 둘 다 그 고유의 강점으로 알고리즘의 상수 항을 다르게 만든다.

다음에 알고리즘 문제를 만났을 때 — Big-O 만 보지 말고 런타임의 7 가지 차원도 함께 보자. 그게 언어를 진짜 선택하는 시야 다.


시리즈 : C++ 는 클러스터 밖에 있다 · Go 는 클러스터 전체에 있다 · R 은 클러스터에 없다 · 이커머스 SaaS 의 트래픽 제어 · Observer Pattern 의 7 layer stack dive · HikariCP 의 5 시간 설정 · 백엔드 응답시간 + 모니터링 · Python vs Java 알고리즘 (현재 글)

이 글은 sparta-msa-project 의 judge-engine 운영 경험 + Java HotSpot internals + CPython 3.12 implementation details + 일반적 알고리즘 벤치마크 (백준 / LeetCode / 프로그래머스) 데이터를 기반으로 작성.