MDP
Java vs Python 본문
Java
Spring Boot
Tomcat
ThreadPool
HikariCP
Thread
Python vs Java 비교부터 웹서버 내부 동작, 스레드 원리까지 — 고트래픽 백엔드에서 Java가 선호되는 이유를 구조적으로 정리합니다.
| 📋 개요 | |
|---|---|
| 주제 | Java vs Python 백엔드 선택 기준 및 내부 동작 이해 |
| 핵심 기술 | Tomcat, ThreadPool, HikariCP, Thread, Spring Boot |
| 학습 목표 | 고트래픽 상황에서 Java가 안정적인 이유를 구조적으로 이해 |
| 난이도 | 중급 (Spring Boot 기본 지식 권장) |
⚔️ 1. Python vs Java — 백엔드 선택 기준
백엔드 언어를 선택할 때 가장 많이 비교되는 Python과 Java. 단순한 문법 차이를 넘어, 웹 서버 레벨의 동작 방식에서 근본적인 차이가 있습니다.
| 항목 | Java | Python |
|---|---|---|
| 실행 방식 | JIT 컴파일 (빠름) | 인터프리터 (느림) |
| 멀티스레딩 | 네이티브 멀티스레딩 | GIL 제약 |
| 타입 시스템 | 정적 타입 (컴파일 체크) | 동적 타입 (런타임 오류) |
| 웹 서버 | Tomcat, Undertow | Gunicorn, uWSGI |
| 엔터프라이즈 | Spring 생태계 | 상대적으로 약함 |
| 개발 속도 | 느린 편 | 빠름 |
| AI/ML 연동 | 약함 | 압도적 강점 |
💡 Python의 GIL (Global Interpreter Lock)
Python은 한 번에 하나의 스레드만 실행할 수 있는 GIL 제약이 있어, CPU 집약적 작업에서 Java 대비 성능이 크게 떨어집니다. Gunicorn으로 멀티 프로세스를 띄워 우회하지만, 그만큼 메모리 소비가 급증합니다.
Python은 한 번에 하나의 스레드만 실행할 수 있는 GIL 제약이 있어, CPU 집약적 작업에서 Java 대비 성능이 크게 떨어집니다. Gunicorn으로 멀티 프로세스를 띄워 우회하지만, 그만큼 메모리 소비가 급증합니다.
🌐 2. 웹 서버 관점 — 요청 처리 모델 비교
동일한 트래픽이 몰렸을 때 두 언어의 웹 서버가 어떻게 다르게 반응하는지 비교해봅니다.
동시 요청 250개가 들어왔을 때
Java (Spring Boot + Tomcat) ├── 스레드풀 200개 → 200개 즉시 처리 ├── 나머지 50개 → 큐(Queue)에서 대기 └── 컨텍스트 스위칭 비용 낮음 ✅ Python (Django + Gunicorn) ├── 워커 4개 × CPU 코어 → 동시 처리 매우 제한적 ├── 워커 늘리면 → RAM 급증 ⚠️ └── CPU 바운드 작업 시 병목 심각 ❌
Spring Boot 요청 흐름
클라이언트 요청 ↓ Nginx (리버스 프록시) ↓ Embedded Tomcat (Connector) ↓ Thread Pool → 스레드 배정 ↓ DispatcherServlet → Controller ↓ Service → Repository → DB ↓ 응답 반환 → 스레드 풀에 반납
🧱 3. Tomcat 아키텍처와 스레드풀
Tomcat은 Java 웹 애플리케이션의 핵심 WAS입니다. 스레드풀을 통해 동시 요청을 효율적으로 처리합니다.
[Connector] ← HTTP 포트 수신 (기본 8080) ↓ [Thread Pool] ← 스레드 할당 (기본 max: 200) ↓ [Engine → Host → Context] ↓ [Servlet → DispatcherServlet]
스레드풀 동작 순서
1
요청이 들어오면 유휴 스레드를 즉시 배정
2
모든 스레드가 사용 중이면 큐(Queue)에서 대기
3
큐도 꽉 찼다면 503 에러 반환
4
처리 완료 후 스레드는 풀에 반납(재사용)
YAML · application.yml
server: tomcat: threads: min-spare: 10 # 항상 대기 중인 최소 스레드 수 max: 200 # 최대 스레드 수 (기본값 200) accept-count: 100 # 큐에 대기 가능한 최대 요청 수 max-connections: 8192 # 최대 TCP 커넥션 수 connection-timeout: 20000 # 타임아웃 (ms)
✅ NIO의 핵심 이점 (Tomcat 8.5 이후 기본값)
I/O 대기 중에 스레드를 반환하므로, 적은 스레드 수로 훨씬 많은 동시 커넥션을 처리할 수 있습니다. 특히 DB 조회나 외부 API 호출처럼 I/O 대기가 잦은 백엔드 서버에서 효과가 큽니다.
I/O 대기 중에 스레드를 반환하므로, 적은 스레드 수로 훨씬 많은 동시 커넥션을 처리할 수 있습니다. 특히 DB 조회나 외부 API 호출처럼 I/O 대기가 잦은 백엔드 서버에서 효과가 큽니다.
# CPU 바운드 (계산이 많은 경우) 적정 스레드 수 = CPU 코어 수 + 1 예) 8코어 → max: 9 # I/O 바운드 (DB, 외부 API 호출이 많은 경우) 적정 스레드 수 = CPU 코어 수 × (1 + 대기시간/처리시간) 예) DB 응답 100ms, 처리 10ms → 8 × (1+10) = 88개
🔗 4. HikariCP — DB 커넥션 풀의 모든 것
HikariCP는 Spring Boot 2.0부터 기본으로 채택된 업계 최고 성능의 DB 커넥션 풀 라이브러리입니다.
❌ 풀 없을 때 요청 → DB 연결 생성 → 쿼리 → 연결 종료 (매번 반복) ↑ TCP 3-way handshake + DB 인증 = 수십~수백ms 낭비 ✅ HikariCP 사용 시 서버 시작 → DB 연결 10개 미리 생성 → 풀에 보관 요청 → 풀에서 꺼냄 → 쿼리 → 풀에 반납(재사용)
YAML · application.yml
spring: datasource: hikari: maximum-pool-size: 10 # 최대 커넥션 수 (DB 부하 한도) minimum-idle: 5 # 최소 유지 커넥션 수 connection-timeout: 30000 # 커넥션 획득 대기시간 (ms) idle-timeout: 600000 # 유휴 커넥션 유지시간 (10분) max-lifetime: 1800000 # 커넥션 최대 수명 (30분)
⚠️ max-lifetime 설정 주의!
반드시 DB 서버의
반드시 DB 서버의
wait_timeout보다 2~3분 짧게 설정해야 합니다. 그렇지 않으면 DB가 먼저 연결을 끊어 Connection is closed 에러가 발생합니다.
# HikariCP 공식 문서 권장 적정 풀 사이즈 = CPU 코어 수 × 2 + 유효 디스크 수 예) 4코어 서버, SSD 1개 → 4 × 2 + 1 = 9 (≈ 10개) ⚠️ 흔한 실수: 풀 사이즈가 클수록 좋다? → 오히려 DB Lock 경합 증가 → 성능 저하!
⚖️ 5. Tomcat 스레드풀 × HikariCP 균형 설계
스레드풀과 커넥션풀은 함께 튜닝해야 합니다. 불균형이 발생하면 한쪽이 병목이 됩니다.
Tomcat 스레드 200개 ↓ (동시에 DB 접근 시도) HikariCP 커넥션 10개 → 190개 스레드는 커넥션 날 때까지 대기 → connection-timeout 초과 시 → SQLException 발생
| Tomcat 스레드 | HikariCP 커넥션 | 이유 |
|---|---|---|
| 50개 | 5~10개 | 모든 스레드가 항상 DB를 사용하지 않음 |
| 100개 | 10~20개 | 약 10:1 비율이 안정적 |
| 200개 | 20~30개 | 스레드 대비 커넥션은 여유 있게 |
Java · 트랜잭션 범위 예시
// ❌ 트랜잭션 범위가 너무 넓음 → 커넥션 낭비 @Transactional public void process() { dbQuery(); // DB 커넥션 점유 시작 externalApiCall(); // 외부 API 3초 대기... 커넥션 낭비! ❌ dbQuery2(); } // ✅ 트랜잭션 범위를 최소화 public void process() { externalApiCall(); // 먼저 처리 (커넥션 불필요) saveToDb(); // DB 작업만 트랜잭션으로 묶기 ✅ }
🚨 Thread Starvation (스레드 고갈)
외부 API 응답이 느려서 스레드가 계속 점유되면, 새 요청이 처리될 스레드가 없어 503 에러로 이어집니다. 반드시 타임아웃 설정 또는 @Async 비동기 처리를 적용하세요.
외부 API 응답이 느려서 스레드가 계속 점유되면, 새 요청이 처리될 스레드가 없어 503 에러로 이어집니다. 반드시 타임아웃 설정 또는 @Async 비동기 처리를 적용하세요.
🧵 6. 스레드(Thread) 완전 분석
스레드는 프로세스 안에서 실제 코드를 실행하는 최소 단위입니다. Tomcat 스레드풀을 제대로 이해하려면 스레드의 동작 원리를 알아야 합니다.
프로세스 (Process) ┌──────────────────────────────────────┐ │ Code │ Data │ Heap │ ← 공유 메모리 │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Thread 1 │ │ Thread 2 │ │ ← 각자 Stack만 독립 │ │ (Stack) │ │ (Stack) │ │ │ └─────────────┘ └─────────────┘ │ └──────────────────────────────────────┘ 공유: Heap(객체), Code(클래스), Data(static) 독립: Stack(지역변수, 호출스택), PC 레지스터
싱글 스레드 요청1 ──→ [처리중.......] ──→ 완료 요청2 ────────────────────────→ [처리중...] ← 대기 ❌ 요청3 ──────────────────────────────────────→ [처리중...] ← 엄청난 지연 멀티 스레드 요청1 ──→ [Thread 1: 처리중...] 요청2 ──→ [Thread 2: 처리중...] ← 동시 처리 ✅ 요청3 ──→ [Thread 3: 처리중...]
스레드 생명주기 (Life Cycle)
| 상태 | 설명 |
|---|---|
| NEW | 스레드 객체 생성, 아직 시작 안 됨 |
| RUNNABLE | 실행 가능 상태, CPU 배정 대기 중 |
| RUNNING | CPU를 받아 실제 코드 실행 중 |
| WAITING | 다른 스레드 완료 대기 (무기한) |
| TIMED_WAITING | 정해진 시간만큼 대기 (sleep 등) |
| BLOCKED | Lock 해제 대기 중 |
| TERMINATED | 실행 완료 후 종료 |
Java · 스레드 생성 3가지 방법
// 방법 1 — Thread 상속 class MyThread extends Thread { @Override public void run() { System.out.println("실행: " + getName()); } } new MyThread().start(); // run()이 아닌 start()! // 방법 2 — Runnable 람다 (권장) Thread t = new Thread(() -> processRequest()); t.start(); // 방법 3 — ExecutorService (실무 표준, Tomcat도 이 방식) ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> processRequest()); // 스레드풀에서 재사용 ✅ executor.shutdown();
💡 Tomcat도 내부적으로 ExecutorService를 사용합니다
Tomcat의 스레드풀은 Java의
Tomcat의 스레드풀은 Java의
ThreadPoolExecutor를 기반으로 동작합니다. application.yml의 max, min-spare가 바로 이 파라미터입니다.
Race Condition — count++ 내부 동작
Thread 1: READ(0) → ADD(1) → (중단!) Thread 2: READ(0) → ADD(1) → WRITE(1) Thread 1: WRITE(1) ← 덮어씌움! 결과: count = 1 (기대값: 2) ← Race Condition 발생!
Java · 동시성 제어
// ① synchronized — 한 번에 하나만 접근 public synchronized void increment() { count++; } // ② ReentrantLock — 더 세밀한 제어 lock.lock(); try { count++; } finally { lock.unlock(); } // ③ AtomicInteger — 가장 빠름 ✅ AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); 성능: AtomicInteger > ReentrantLock > synchronized
실무 주의사항
| 문제 | 원인 | 해결 |
|---|---|---|
| Race Condition | 공유 자원 동시 접근 | synchronized / AtomicInteger |
| Deadlock | 두 스레드가 서로 Lock 대기 | Lock 순서 통일 / tryLock 타임아웃 |
| Thread Starvation | 스레드풀 고갈 | 적절한 max 설정 / 타임아웃 |
| Memory Leak | 스레드 종료 안 됨 | ExecutorService.shutdown() 필수 |
| Context Switching | 스레드 수가 너무 많음 | 코어 수에 맞는 풀 사이즈 설정 |
🗂 핵심 정리
| 항목 | 내용 |
|---|---|
| 스레드 | 프로세스 안에서 코드를 실행하는 최소 단위. Java는 GIL 없이 진정한 멀티스레딩 지원 |
| Tomcat | ExecutorService 기반 스레드풀로 동시 요청 처리, NIO로 I/O 효율 극대화 |
| HikariCP | DB 커넥션을 미리 생성·재사용해 연결 비용 제거. Spring Boot 기본 커넥션 풀 |
| 튜닝 핵심 | 스레드풀 : 커넥션풀 ≈ 10:1 비율 유지. 트랜잭션 범위 최소화 |
| Java vs Python | 고트래픽 엔터프라이즈 백엔드라면 Java + Spring Boot 조합이 가장 검증된 선택 |