버츄얼 스레드와 세마포어

dev-sj·2026년 1월 13일

Virtual Thread와 Semaphore

스레드와 달리 JVM이 관리하는 경량 스레드로, 수백만 개의 스레드를 생성해도 메모리와 성능에 큰 영향을 주지 않습니다.

주요 특징

1. 경량성

  • Platform Thread: 약 2MB의 스택 메모리 사용
  • Virtual Thread: 수 KB의 메모리만 사용
  • 수백만 개의 Virtual Thread 생성 가능

2. 작동 원리

  • Virtual Thread는 Carrier Thread(Platform Thread) 위에서 실행됩니다
  • Blocking 작업 시 Virtual Thread는 unmount되고 Carrier Thread는 다른 Virtual Thread를 실행합니다
  • Non-blocking 방식처럼 작동하지만 코드는 동기식으로 작성 가능

3. 사용 사례

// Virtual Thread 생성
Thread.startVirtualThread(() -> {
    System.out.println("Virtual Thread 실행");
});

// ExecutorService 사용
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        // 작업 수행
    });
}

Virtual Thread의 장점

  • 간단한 코드: 비동기 코드 없이 동기식으로 작성 가능
  • 높은 처리량: I/O 바운드 작업에서 뛰어난 성능
  • 낮은 메모리 사용: 수백만 개의 스레드도 효율적으로 관리
  • 기존 코드 호환성: 기존 Thread API와 호환

주의사항 및 제약사항

1. Pinning 문제

Virtual Thread가 Carrier Thread에 고정(pin)되어 다른 작업을 수행할 수 없는 상황이 발생할 수 있습니다.

Pinning이 발생하는 경우:

  • synchronized 블록 내에서 blocking 작업 수행
  • Native 메서드 실행 중
// ❌ 나쁜 예: synchronized 내에서 blocking
synchronized(lock) {
    // Virtual Thread가 pinning됨
    Thread.sleep(1000);
}

// ✅ 좋은 예: ReentrantLock 사용
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // Virtual Thread가 정상적으로 unmount 가능
    Thread.sleep(1000);
} finally {
    lock.unlock();
}

2. ThreadLocal 사용 주의

Virtual Thread는 수백만 개가 생성될 수 있으므로 ThreadLocal 사용 시 메모리 누수 위험이 있습니다.

// ❌ 위험: 대량의 Virtual Thread에서 ThreadLocal 사용
ThreadLocal<LargeObject> threadLocal = new ThreadLocal<>();

// ✅ 대안: ScopedValue 사용 (Java 21+)
ScopedValue<String> scopedValue = ScopedValue.newInstance();

3. CPU 집약적 작업에는 부적합

Virtual Thread는 I/O 바운드 작업에 최적화되어 있습니다.

// ❌ Virtual Thread로는 비효율적
Thread.startVirtualThread(() -> {
    // CPU 집약적 계산
    for (int i = 0; i < 1_000_000_000; i++) {
        // 복잡한 계산
    }
});

// ✅ CPU 작업은 Platform Thread 풀 사용
ExecutorService executor = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

4. Thread Pool 사용 금지

Virtual Thread는 풀링하지 않고 작업마다 새로 생성해야 합니다.

// ❌ 나쁜 예: Virtual Thread 풀링
ExecutorService pool = Executors.newFixedThreadPool(100, 
    Thread.ofVirtual().factory());

// ✅ 좋은 예: 작업마다 생성
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

5. 모니터링 및 디버깅

  • JConsole, VisualVM 등 기존 모니터링 도구에서 Virtual Thread가 정확히 표시되지 않을 수 있음
  • jcmd 명령어로 Virtual Thread 덤프 확인 가능
  • IntelliJ IDEA 2023.2+ 에서 Virtual Thread 디버깅 지원

6. 호환성 문제

일부 라이브러리가 Virtual Thread와 완벽히 호환되지 않을 수 있습니다:

  • Thread 수를 제한하는 라이브러리
  • ThreadLocal에 의존하는 레거시 코드
  • Platform Thread를 가정하는 코드

Semaphore

개요

Semaphore는 공유 자원에 대한 접근을 제어하는 동기화 도구입니다. 특정 개수의 스레드만 동시에 자원에 접근할 수 있도록 제한합니다.

작동 원리

개념

  • Permits: Semaphore가 가지고 있는 허가 개수
  • acquire(): 허가를 획득 (없으면 대기)
  • release(): 허가를 반환

사용 예제

기본 사용법

// 3개의 허가를 가진 Semaphore 생성
Semaphore semaphore = new Semaphore(3);

// 자원 접근
try {
    semaphore.acquire();  // 허가 획득
    try {
        // 공유 자원 사용
        System.out.println("작업 수행");
    } finally {
        semaphore.release();  // 허가 반환
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

실용적인 예제: 커넥션 풀 제한

public class ConnectionPool {
    private final Semaphore semaphore;
    
    public ConnectionPool(int maxConnections) {
        this.semaphore = new Semaphore(maxConnections);
    }
    
    public Connection getConnection() throws InterruptedException {
        semaphore.acquire();
        try {
            return createConnection();
        } catch (Exception e) {
            semaphore.release();
            throw e;
        }
    }
    
    public void releaseConnection(Connection conn) {
        closeConnection(conn);
        semaphore.release();
    }
}

Semaphore 종류

1. Binary Semaphore (0 또는 1)

Semaphore binarySemaphore = new Semaphore(1);
// Mutex처럼 사용 가능

2. Counting Semaphore (N개)

Semaphore countingSemaphore = new Semaphore(10);
// 10개까지 동시 접근 허용

Fair vs Unfair Semaphore

// Unfair (기본값) - 성능이 좋지만 공정성 보장 안됨
Semaphore unfairSemaphore = new Semaphore(5);

// Fair - 대기 순서대로 허가 부여
Semaphore fairSemaphore = new Semaphore(5, true);

Virtual Thread + Semaphore 조합

사용 시나리오

Virtual Thread와 Semaphore를 함께 사용하면 대량의 요청을 처리하면서도 외부 자원(DB, API 등)에 대한 접근을 제한할 수 있습니다.

예제: API Rate Limiting

public class ApiClient {
    private final Semaphore rateLimiter = new Semaphore(10); // 동시 10개 요청 제한
    
    public void makeRequest(String url) {
        Thread.startVirtualThread(() -> {
            try {
                rateLimiter.acquire();
                try {
                    // API 호출
                    HttpClient.newHttpClient()
                        .send(HttpRequest.newBuilder()
                            .uri(URI.create(url))
                            .build(), 
                        HttpResponse.BodyHandlers.ofString());
                } finally {
                    rateLimiter.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

예제: DB 커넥션 관리

public class DatabaseService {
    private final Semaphore dbSemaphore = new Semaphore(20); // DB 연결 20개 제한
    
    public CompletableFuture<Result> query(String sql) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                dbSemaphore.acquire();
                try {
                    return executeQuery(sql);
                } finally {
                    dbSemaphore.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }, Executors.newVirtualThreadPerTaskExecutor());
    }
}

성능 고려사항

Virtual Thread 최적화

  1. I/O 작업에 집중: CPU 집약적 작업보다 I/O 바운드 작업에 효과적
  2. Blocking API 사용: 동기식 blocking API를 자연스럽게 사용 가능
  3. Pinning 회피: Synchronized 대신 ReentrantLock 사용 고려

Semaphore 최적화

  1. 적절한 Permit 수 설정: 자원의 실제 용량에 맞게 설정
  2. Fair vs Unfair 선택: 공정성이 중요하면 Fair, 성능이 중요하면 Unfair
  3. Timeout 설정: tryAcquire(timeout) 사용으로 무한 대기 방지

참고 자료

참고 영상

추가 학습 자료


요약

  • Virtual Thread: 경량 스레드로 대량의 동시 작업 처리에 적합
  • Semaphore: 공유 자원에 대한 동시 접근 제어
  • 조합 사용: Virtual Thread로 높은 동시성 확보, Semaphore로 자원 접근 제한
  • 적용 분야: 웹 서버, API 클라이언트, 마이크로서비스 등
profile
IT 블로그 채널입니다!!

0개의 댓글