IT/JAVA

Java 멀티스레드와 병렬 실행

iamhyeon 2025. 12. 7. 16:54

 

Java에서 고성능 애플리케이션을 개발하려면 멀티스레드(Multi-thread) 개념을 이해해야 한다.
스레드를 직접 생성하고 관리하는 일은 복잡하고 비용이 크기 때문에,

ExecutorService, ThreadPool, CountDownLatch 등 다양한 API를 함께 사용한다.

 

1. 멀티스레드

스레드(Thread)는 프로그램의 실행 흐름 단위이다.
멀티스레드란 이 실행 흐름을 여러 개 만들어 여러 작업을 동시에 실행하는 구조를 의미한다.

예:

  • 웹 서버: 사용자 요청마다 새로운 스레드 처리
  • 게임 서버: 몬스터 AI 스레드 + 네트워크 IO 스레드
  • 병렬 계산: 여러 CPU 코어에서 계산 작업을 나눠 처리

단순 Thread 생성 방식

Thread thread = new Thread(() -> {
    System.out.println("작업 실행");
});
thread.start();

문제점: 

  • 스레드를 직접 만들면 비용이 비싸고
  • 너무 많이 만들면 OOM(OutOfMemory) 위험이 있고
  • 스레드 수를 직접 관리하기 어렵다.

=>  스레드 풀(ThreadPool) 기반의 ExecutorService를 사용할 수 있다.

 

2. ThreadPool — 스레드를 재사용하는 구조

ThreadPool은 “미리” 일정 개수의 스레드를 만들어 두고,
작업이 들어올 때마다 스레드를 재사용하는 구조이다.

장점

  • 스레드 생성 비용 절감
  • 과도한 스레드 생성 방지
  • 안정적인 성능 확보
  • 여러 개의 작업을 큐(queue)에 넣고 효율적으로 배분

대부분 병렬 처리는 ThreadPool 기반으로 구성한다.

 

3. Executors — 스레드풀 생성 팩토리

Executors는 다양한 형태의 스레드 풀을 쉽게 만들도록 돕는 유틸 클래스이다.

 

3-1. 고정 크기 스레드풀 (Fixed Thread Pool)

ExecutorService executor = Executors.newFixedThreadPool(5);
  • 항상 5개의 스레드를 유지
  • CPU보다 I/O가 많은 경우 적합

 

3-2. 캐시 스레드풀 (Cached Thread Pool)

ExecutorService executor = Executors.newCachedThreadPool();
  • 필요한 만큼 스레드를 만들고, 일정 시간 지나면 자동 종료
  • 매우 빠른 단발성 task 처리에 적합
  • burst traffic 처리에 유용

 

3-3. 단일 스레드풀 (Single Thread Executor)

ExecutorService executor = Executors.newSingleThreadExecutor();
  • 스레드 1개만 사용
  • 작업 순서를 보장해야 할 때 사용

 

4. ExecutorService — 스레드풀을 제어하는 핵심 인터페이스

ExecutorService는 스레드 풀을 제어하고, 작업을 실행할 수 있는 상위 인터페이스이다.

 

작업 제출 방법

4-1. 실행만 하고 결과가 필요 없을 때 — Runnable

executor.submit(() -> {
    System.out.println("작업 실행");
});

4-2. 결과가 필요할 때 — Callable

Future<Integer> result = executor.submit(() -> {
    return 10 + 20;
});
System.out.println(result.get()); // 30

스레드풀 종료

executor.shutdown(); // graceful shutdown
// executor.shutdownNow(); // 즉시 종료 시도

 

 

 

5. CountDownLatch — 병렬 작업의 “동기화 도구”

멀티스레드를 사용하면 “여러 스레드가 끝날 때까지 기다렸다가 다음 로직 실행”이 필요할 때가 있다.

이때 사용하는 대표적인 동기화 도구가 CountDownLatch이다.

역할

  • 여러 스레드에 작업을 맡긴 뒤
  • 모든 작업이 완료될 때까지 기다려야 할 때 사용
  • 스레드 간 동기화를 위한 단순하고 강력한 메커니즘

 

5-1. 예시: 5개의 병렬 작업이 모두 끝날 때까지 기다리기

ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(5);

for (int i = 0; i < 5; i++) {
    executor.submit(() -> {
        try {
            System.out.println("작업 시작");
            Thread.sleep(1000);
        } catch (Exception ignored) {
        } finally {
            latch.countDown(); // 작업 1개 완료
        }
    });
}

latch.await();  // 5개 작업 모두 끝날 때까지 대기
System.out.println("모든 작업 완료!");
executor.shutdown();

CountDownLatch 프로세스

  1. Latch 카운트는 5
  2. 스레드 하나 완료할 때마다 countDown() → 4 → 3 → 2 → 1 → 0
  3. 카운트가 0이 되면 await()가 풀림
  4. 메인 스레드가 계속 실행됨

 

6. 병렬 처리 예시 

✔ 외부 API 여러 개 병렬로 호출

  • 하나씩 API 호출하면 느리다
  • 스레드 풀에서 병렬 호출 후
  • 모든 응답이 모이면 조합하여 결과 생성
  • CountDownLatch 또는 CompletableFuture를 많이 활용

 

✔ 대용량 데이터 처리 (Batch / ETL)

  • 데이터를 여러 덩어리로 나눔
  • 각 덩어리를 스레드에서 병렬 처리
  • 모든 작업이 끝나면 후처리 실행

 

✔ 웹 서버에서 비동기 처리

  • 이미지 처리
  • 메일 전송
  • 알림 메시지 발송

I/O 작업을 별도 스레드로 넘겨서 메인 요청 처리 시간을 줄인다.

 

7. 병렬 처리 시 꼭 알아야 할 주의사항

⚠ 스레드 수는 무조건 많다고 좋은 것이 아니다

  • 너무 많으면 context switching 비용 증가
  • CPU는 결국 한정됨
  • 적절한 스레드풀 사이즈 필요

일반적인 기준:

CPU 바운드 작업: CPU 코어 수 ± 1
I/O 바운드 작업: CPU 코어 수 × 2 ~ 3배

 

 

⚠ 스레드풀은 절대 무한하게 만들면 안 된다

  • Executors의 일부 메서드는 위험할 수 있다
    (CachedThreadPool은 잘못 쓰면 스레드를 무제한 생성할 수 있음)

그래서 실무에서는 다음처럼 직접 ThreadPoolExecutor를 구성하기도 한다.

new ThreadPoolExecutor(
    10,     // core pool size
    20,     // max pool size
    60,     // idle thread timeout
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);

 

 

⚠ 동기화 문제 주의

  • 여러 스레드가 동시에 공유 자원을 접근하면 Race Condition 가능
  • 공유 상태는 반드시 동기화하거나, 아예 상태를 공유하지 않는 구조를 설계해야 한다

개념 설명
Thread 작업 실행 단위
ThreadPool 스레드를 재사용하여 효율적 처리
Executors 다양한 형태의 스레드풀을 편하게 생성
ExecutorService 스레드풀 제어 및 작업 제출
Runnable / Callable 실행할 작업(반환값 유무) 표현
CountDownLatch 병렬 작업 완료를 기다리는 동기화 도구

 

반응형

'IT > JAVA' 카테고리의 다른 글

Java 함수형 인터페이스 Supplier, Runnable  (0) 2025.12.07
getOrDefault() vs computeIfAbsent()  (0) 2025.12.06
ReentrantLock 락 제어  (0) 2025.12.06
Java 예외: RuntimeException  (0) 2025.12.06
동기화 vs 원자적 연산  (1) 2025.07.24