멀티 스레드로 병렬 처리하는것은 항상 성능에 이점이 있을까?

2025. 1. 29. 18:20·트러블슈팅

멀티 스레드로 병렬 처리하는 것은 항상 성능에 이점이 있을까?


업무를 진행하며 멀티 스레드로 성능 개선을 할 일이 있었는데, “멀티 스레드로 병렬 처리하는 것이 항상 성능 향상에 이점이 있을까?”라는 의문점이 생겼습니다.

 

가령, 스타크래프트 같은 게임에서 일꾼 수가 많을수록 더 많은 미네랄을 동시에 캐는 것처럼, 스레드가 많으면 그만큼 동시에 처리할 수 있는 작업이 많아져서 단일 스레드보다 빠르다고 생각하기 쉽습니다.
저도 처음엔 “스레드가 많으면 빠르다, 따라서 스레드 수와 애플리케이션의 성능 속도는 정비례한다”고 단순하게 생각했습니다.

하지만 실제로는 꼭 그렇지만은 않습니다.

 

예를 들어 단일 스레드로 동작한다고 알고 있는 Redis는 매우 빠른 처리 속도를 보여주는데, 이는 “단일 스레드 = 느리다”라는 가설이 항상 성립하는 것은 아니라는 점을 잘 보여줍니다.

 

이번 글에서는 왜 멀티 스레드가 항상 빠른 것만은 아닌지, 간단한 Java 예제 코드를 통해 살펴보려고 합니다.

 

"멀티 스레드로 병렬 처리"가 항상 빠른 것은 아니다.

왜 멀티 스레드로 병렬 처리가 항상 빠른 것이 아닌지 단순한 Java로 작성한 코드를 보면서 알아보곘습니다.

 

코드의 핵심은 sharedCounter라는 공유 자원을 doHeavyWork() 메서드에서만 수정하고, 이 메서드를 synchronized로 잠갔다는 점입니다. 그리고 10개의 스레드 풀을 사용하여 100개의 작업을 동시에 실행했을 때와, 단일 스레드에서 순차적으로 100개의 작업을 실행했을 때의 시간을 비교합니다.

 

public class MultiThreadPerformanceApplication {

    // 임계 영역
    private int sharedCounter = 0;

    // synchronized 키워드를 사용해 메서드 락 (한번에 1개의 스레드만 접근 가능)
    private synchronized void doHeavyWork() {
        for (int i = 0; i < 1_000_000; i++) {
            sharedCounter++;
        }
    }

    private void runMultiThreadTask() throws InterruptedException {
        long startTime = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 100개의 작업을 멀티 스레드에서 실행
        for (int i = 0; i < 100; i++) {
            executorService.submit(this::doHeavyWork);
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.MINUTES);

        long endTime = System.currentTimeMillis();
        System.out.println("[Multi-thread] Total time: " + (endTime - startTime) + " ms");
        System.out.println("[Multi-thread] Final sharedCounter: " + sharedCounter);
    }

    private void runSingleThreadTask() {
        long startTime = System.currentTimeMillis();

        // 100개의 작업을 단일 스레드(메인 스레드)에서 순차 실행
        for (int i = 0; i < 100; i++) {
            doHeavyWork();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("[Single-thread] Total time: " + (endTime - startTime) + " ms");
        System.out.println("[Single-thread] Final sharedCounter: " + sharedCounter);
    }

    public static void main(String[] args) throws InterruptedException {
        MultiThreadPerformanceApplication application = new MultiThreadPerformanceApplication();

        System.out.println("=== Single Thread Test ===");
        application.runSingleThreadTask();

        // sharedCounter 값이 누적되지 않도록 새 객체 생성
        application = new MultiThreadPerformanceApplication();
        System.out.println("\n=== Multi Thread Test ===");
        application.runMultiThreadTask();
    }
}

 

실행 결과

=== Single Thread Test ===
[Single-thread] Total time: 10 ms
[Single-thread] Final sharedCounter: 100000000

=== Multi Thread Test ===
[Multi-thread] Total time: 26 ms
[Multi-thread] Final sharedCounter: 100000000

 

  • 싱글 스레드로 처리했을 때가 약 10ms, 멀티 스레드로 처리했을 때가 약 26ms가 걸렸습니다.
  • 결과적으로 싱글 스레드가 약 16ms 정도 더 빨랐습니다.

 

왜 이런 상황이 발생했을까요?

1. 락 경쟁 (Lock Contention)

  • synchronized 메서드인 doHeavyWork()는 한 번에 오직 1개의 스레드만 진입할 수 있습니다.
  • 10개의 스레드가 동시에 실행된다고 해도, 결국 이 메서드를 사용할 때는 차례차례 대기해야 하므로 사실상 싱글 스레드처럼 처리되는 구간이 생깁니다.
  • 스레드가 늘어날수록 락을 얻기 위한 "대기"가 잦아져서 성능 이점보다는 오히려 지연이 발생하게 됩니다.

 

 

2. 컨텍스트 스위칭(Context Switching) 비용

  • 여러 스레드가 실행되는 도중, 운영체제는 각각의 스레드를 번갈아가며 CPU에 할당합니다.
  • 이때 스레드 레지스터, 스택 등을 저장하고 복원해야 하는데, 이를 "컨텍스트 스위칭"이라 하며 상당한 오버헤드가 발생합니다.
  • 락 경쟁으로 계속 대기하는 스레드가 많아질수록 컨텍스트 스위칭도 빈번해져서 성능이 떨어지게 됩니다.

 

3. CPU 캐시 활용 문제

  • 단일 스레드로 작업할 때는 CPU 캐시가 일관성 있게 활용되지만, 멀티 스레드일 때는 여러 스레드 간 캐시 동기화(Coherency)가 발생합니다.

 

4. 임계 영역(critical section)이 커서 실제 병렬성이 거의 없는 경우

  • 임계 영역은 여러 스레드가 동시에 접근하면 안 되는 공유 자원(변수, 메모리 영역 등)을 사용하거나 수정하는 부분을 의미합니다.
  • 예를 들어, 여러 스레드가 동시에 특정 변수의 값을 변경할 때, 해당 연산 전체가 "원자적(Atomic)"으로 처리되지 않으면 데이터 무결성이 깨질 수 있습니다.
  • 이를 방지하기 위해 임계 영역에 들어가는 스레드는 락(lock)이나 뮤텍스(mutex) 같은 동기화 기법을 사용해 "한 번에 오직 1개의 스레드만 접근"하게 해야 합니다.
  • 즉, 임계 영역이 많은 로직(특히 락으로 보호되는 부분)이 복잡해질수록, 멀티 스레드의 이점이 줄어들고 오히려 성능에 악영향을 줄 수도 있습니다.

 

정리


  • “멀티 스레드 = 항상 빠르다”는 공식은 성립하지 않습니다.
  • 임계 영역(공유 자원을 보호하는 구역)이 많거나 락 경쟁이 심한 경우, 멀티 스레드가 오히려 싱글 스레드보다 느릴 수 있습니다. 실제 예제에서처럼 한 번에 하나의 스레드만 doHeavyWork()를 실행할 수 있다면, 멀티 스레드의 이점을 살리지 못하고 오히려 대기와 컨텍스트 스위칭 오버헤드만 늘어납니다.
  • 단순히 스레드의 개수가 아니라 작업 특성, 공유 자원 사용 여부, 시스템 구조 등이 성능에 큰 영향을 미칩니다.
  • 병렬화 효과가 큰 CPU 바운드 작업이나, I/O가 많은 작업에 적절한 스레드 풀을 구성한다면 멀티 스레딩이 유리할 수 있습니다. 하지만 임계 영역에서의 동시 접근이 잦거나, 락이 많이 걸리면 멀티 스레딩의 이점이 빠르게 희미해집니다.

결국 멀티 스레드로 병렬 처리가 항상 빠른 것이 아니라, 임계 영역을 어떻게 최소화할지, 락 경쟁을 얼마나 줄일 수 있을지가 관건이라고 할 수 있습니다.

 

'트러블슈팅' 카테고리의 다른 글

Redis 캐싱했는데도 느렸던 이유, RTT가 숨은 범인이었다  (2) 2025.04.22
아무리 스레드를 늘려도 성능 개선에 소용없던 이유 (MySQL CPU 사용률 99%)  (0) 2025.04.04
LLM 기반 보고서 자동 요약 프롬프트 최적화 전략 (내용 누락 트러블 슈팅)  (0) 2024.11.27
주식 시황 피드의 종가 오류 해결기(데이터 일관성 개선)  (0) 2024.07.30
Confluent Schema Reference 관련 문제  (0) 2024.07.24
'트러블슈팅' 카테고리의 다른 글
  • Redis 캐싱했는데도 느렸던 이유, RTT가 숨은 범인이었다
  • 아무리 스레드를 늘려도 성능 개선에 소용없던 이유 (MySQL CPU 사용률 99%)
  • LLM 기반 보고서 자동 요약 프롬프트 최적화 전략 (내용 누락 트러블 슈팅)
  • 주식 시황 피드의 종가 오류 해결기(데이터 일관성 개선)
seungjjun
seungjjun
  • seungjjun
    개발이야기
    seungjjun
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 성장이야기
        • TIL
        • 주간회고
      • Java
        • Spring
        • Spring Security
      • 트러블슈팅
      • Kafka
      • OS
      • Network
      • 메가테라
      • Database
      • Algorithm
      • Git
      • HTML
      • CSS
      • 독서
      • 컴퓨터 이해하기
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    이커머스 프로젝트
    메가테라
    graphQL
    항해99
    메가테라 주간회고
    항해플러스
    개발일지
    redis
    주간회고
    Til
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
seungjjun
멀티 스레드로 병렬 처리하는것은 항상 성능에 이점이 있을까?
상단으로

티스토리툴바