MDP

Java vs Python 본문

카테고리 없음

Java vs Python

모다팡 2026. 3. 8. 01:20
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. 단순한 문법 차이를 넘어, 웹 서버 레벨의 동작 방식에서 근본적인 차이가 있습니다.

항목JavaPython
실행 방식JIT 컴파일 (빠름)인터프리터 (느림)
멀티스레딩네이티브 멀티스레딩GIL 제약
타입 시스템정적 타입 (컴파일 체크)동적 타입 (런타임 오류)
웹 서버Tomcat, UndertowGunicorn, uWSGI
엔터프라이즈Spring 생태계상대적으로 약함
개발 속도느린 편빠름
AI/ML 연동약함압도적 강점
💡 Python의 GIL (Global Interpreter Lock)
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 → ControllerService → 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 대기가 잦은 백엔드 서버에서 효과가 큽니다.
# 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 서버의 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 비동기 처리를 적용하세요.

🧵 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 배정 대기 중
RUNNINGCPU를 받아 실제 코드 실행 중
WAITING다른 스레드 완료 대기 (무기한)
TIMED_WAITING정해진 시간만큼 대기 (sleep 등)
BLOCKEDLock 해제 대기 중
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의 ThreadPoolExecutor를 기반으로 동작합니다. application.ymlmax, 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 없이 진정한 멀티스레딩 지원
TomcatExecutorService 기반 스레드풀로 동시 요청 처리, NIO로 I/O 효율 극대화
HikariCPDB 커넥션을 미리 생성·재사용해 연결 비용 제거. Spring Boot 기본 커넥션 풀
튜닝 핵심스레드풀 : 커넥션풀 ≈ 10:1 비율 유지. 트랜잭션 범위 최소화
Java vs Python고트래픽 엔터프라이즈 백엔드라면 Java + Spring Boot 조합이 가장 검증된 선택