티스토리 뷰

300x250

자바를 공부하다 보면 운영체제 개념처럼 보이던 프로세스(Process)스레드(Thread)가 갑자기 코드와 연결되기 시작합니다. 처음에는 둘 다 그냥 "동시에 뭔가 실행되는 것"처럼 느껴지지만, 막상 면접 질문이나 실무 코드로 들어가면 프로세스와 스레드의 차이, 자바에서 멀티스레딩을 왜 쓰는지, 그리고 어디서 위험해지는지까지 함께 이해해야 합니다.

특히 자바에서는 Thread, Runnable, synchronized, ExecutorService 같은 키워드가 이어서 나오기 때문에, 프로세스/스레드 개념을 애매하게 잡아두면 그다음부터 계속 헷갈리기 쉽습니다. 게다가 실무에서는 단순히 스레드를 "만드는 법"보다, 스레드 풀을 어떻게 관리하는지, 공유 자원을 어떻게 안전하게 다루는지, 서버에서 어느 구간이 병목이 되는지가 더 중요합니다.

그래서 이 글에서는 단순 정의만 하지 않고, 프로세스와 스레드의 구조적 차이, 자바 멀티스레딩이 실제 서버/백엔드 코드와 어떻게 연결되는지, 그리고 실무에서 반드시 알아야 할 스레드풀·동기화·공유자원·성능 포인트까지 깊이 있게 정리해보겠습니다.

핵심 요약
프로세스는 실행 중인 프로그램의 독립된 작업 단위입니다.
스레드는 프로세스 안에서 실제 작업을 수행하는 실행 흐름입니다.
자바 멀티스레딩은 하나의 JVM 프로세스 안에서 여러 작업을 동시에 처리하는 방식입니다.
실무 핵심은 `Thread` 직접 생성보다 `ExecutorService`, 동시성 컬렉션, 동기화 전략을 어떻게 쓰는지입니다.
가장 중요한 포인트는 스레드는 자원을 공유하기 때문에 빠를 수 있지만, 동시에 동기화와 안정성 문제가 생길 수 있다는 점입니다.
728x90
프로세스(Process)란 무엇인가?
실행 중인 프로그램 하나를 운영체제가 관리하는 단위입니다.

프로세스는 쉽게 말해 실행 중인 프로그램입니다. 메모장 프로그램을 켜면 메모장 프로세스가 하나 생기고, 브라우저를 실행하면 브라우저 프로세스가 생깁니다.

운영체제 입장에서 프로세스는 단순한 코드 파일이 아니라, 실제로 메모리를 할당받고 CPU 시간을 배정받으면서 동작하는 실행 단위입니다.

보통 프로세스는 아래 같은 자원을 가집니다.

  • 코드 영역(Code)
  • 데이터 영역(Data)
  • 힙 영역(Heap)
  • 스택 영역(Stack)
  • 파일, 소켓 같은 운영체제 자원

즉 프로세스는 하나의 독립된 실행 환경이라고 보면 됩니다.

프로세스는 "프로그램 파일"이 아니라, 메모리와 자원을 할당받아 실제로 돌아가고 있는 실행 단위입니다.
스레드(Thread)란 무엇인가?
프로세스 안에서 실제 작업을 수행하는 흐름입니다.

스레드는 프로세스 내부에서 실행되는 작업 흐름입니다. 하나의 프로세스는 최소 1개의 스레드를 가지며, 여러 개의 스레드를 가지면 동시에 여러 작업을 처리할 수 있습니다.

예를 들어 브라우저 하나의 프로세스 안에서도,

  • 화면 렌더링 스레드
  • 네트워크 요청 처리 스레드
  • 사용자 입력 처리 스레드

처럼 역할이 나뉘어 움직일 수 있습니다.

스레드는 프로세스 안에 있으므로 프로세스가 가진 자원을 함께 사용합니다. 다만 각 스레드는 자기만의 실행 스택과 프로그램 카운터(PC Register)는 따로 가집니다.

프로세스
 ├─ 코드 / 데이터 / 힙 (공유)
 ├─ 스레드 A → 스택 별도
 ├─ 스레드 B → 스택 별도
 └─ 스레드 C → 스택 별도

이 구조 때문에 스레드는 프로세스보다 가볍고 빠르지만, 동시에 공유 자원 때문에 충돌 위험도 생깁니다.

비교 항목 프로세스 스레드
의미 독립된 실행 환경 프로세스 안의 실행 흐름
메모리 다른 프로세스와 분리 같은 프로세스 내부 자원 공유
생성 비용 상대적으로 큼 상대적으로 작음
안정성 한 프로세스 오류가 다른 프로세스에 직접 영향 적음 한 스레드 문제로 같은 프로세스 전체 영향 가능
프로세스와 스레드의 차이를 예시로 이해하기
개념이 헷갈릴 때는 실제 서비스 상황으로 떠올리면 훨씬 쉽습니다.

식당으로 비유하면,

  • 프로세스 = 식당 한 개
  • 스레드 = 그 식당 안에서 일하는 직원들

이라고 볼 수 있습니다.

식당 자체는 주방, 테이블, 재료 창고를 갖춘 독립된 공간입니다. 이게 프로세스입니다. 그 안에서 주문받기, 요리하기, 계산하기를 각각 맡는 직원들이 스레드입니다.

직원들이 같은 주방과 재료를 함께 쓰기 때문에 효율은 좋지만, 서로 엉키면 사고가 날 수도 있습니다. 이게 바로 멀티스레딩에서 자원 공유와 동기화 문제가 나오는 이유입니다.

핵심 감각
프로세스는 서로 독립적이지만 무겁고, 스레드는 같은 자원을 공유해서 효율적이지만 관리가 더 까다롭습니다.
자바에서 멀티스레딩이란?
하나의 JVM 프로세스 안에서 여러 작업 흐름을 동시에 실행하는 방식입니다.

자바 프로그램이 실행되면 JVM 프로세스가 올라갑니다. 그리고 그 안에서 main 스레드가 먼저 시작됩니다.

즉 자바 프로그램은 처음부터 스레드를 가지고 시작한다고 생각해도 됩니다.

그다음 개발자가 추가 스레드를 만들면,

  • 파일 읽기
  • 네트워크 통신
  • 백그라운드 작업
  • 비동기 처리

같은 작업을 동시에 처리할 수 있습니다.

class MyTask extends Thread {
    @Override
    public void run() {
        System.out.println("작업 실행 중: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyTask task = new MyTask();
        task.start();
        System.out.println("메인 스레드 실행 중");
    }
}

위 코드는 개념 이해용으로는 좋지만, 실무 기본 선택이라고 보기는 어렵습니다. 실무에서는 Thread를 직접 상속하기보다 Runnable, Callable, ExecutorService 같은 방식이 더 자주 쓰입니다.

Runnable 방식

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable 작업 실행");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start();
    }
}

ExecutorService 방식

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        executor.submit(() -> {
            System.out.println("비동기 작업 1");
        });

        executor.submit(() -> {
            System.out.println("비동기 작업 2");
        });

        executor.shutdown();
    }
}

이 방식이 더 실무적인 이유는 스레드를 직접 만들고 버리는 것보다, 스레드 풀로 관리하는 편이 훨씬 안정적이기 때문입니다.

왜 실무에서는 ExecutorService를 더 많이 쓸까?
멀티스레딩의 핵심은 스레드를 만드는 것이 아니라, 통제 가능한 방식으로 운영하는 것입니다.

실무에서 Thread를 직접 계속 생성하면 아래 문제가 생기기 쉽습니다.

  • 요청이 몰릴 때 스레드 수가 과도하게 증가
  • 생성/소멸 비용 누적
  • 스레드 수 통제가 어려움
  • 장애 시 어디서 병목이 났는지 추적이 어려움

반면 ExecutorService

  • 스레드 풀 크기 제한
  • 작업 큐 관리
  • 재사용 가능
  • 종료 시점 제어

가 가능해서 서버 애플리케이션에 훨씬 잘 맞습니다.

예를 들어 웹 서버에서는 매 요청마다 새 스레드를 막 만들어내기보다, 정해진 스레드 풀 안에서 요청을 처리하는 구조가 일반적입니다.

요청 100개 도착
 → 스레드 100개 무한 생성 (위험)
 → 스레드풀 20개 + 대기 큐 (통제 가능)

즉 실무에서 중요한 건 "멀티스레드 가능"이 아니라 얼마나 예측 가능하게 운영할 수 있느냐입니다.

실무 기준 한 줄 정리
자바 멀티스레딩은 "스레드를 만드는 기술"이 아니라, 작업량·공유자원·장애 상황까지 포함해 통제하는 기술에 가깝습니다.
왜 멀티스레딩을 사용할까?
속도만이 아니라, 응답성·동시성·자원 활용을 위해서도 중요합니다.

멀티스레딩을 쓰는 대표 이유는 아래와 같습니다.

  • 오래 걸리는 작업을 분리해서 응답성 유지
  • 여러 요청을 동시에 처리
  • CPU 자원을 더 효율적으로 사용
  • 백그라운드 작업과 메인 작업 분리

예를 들어 서버 애플리케이션에서는 한 요청이 끝날 때까지 다른 요청이 멈추면 안 되기 때문에, 멀티스레딩은 거의 기본 전제가 됩니다.

사용자 A 요청 처리
사용자 B 요청 처리
로그 기록
캐시 갱신
→ 여러 작업이 겹쳐도 시스템이 멈추지 않게 분리

하지만 여기서 중요한 건 "무조건 빠르다"가 아니라는 점입니다. 스레드가 많아질수록 문맥 전환(Context Switching) 비용, 동기화 비용, 디버깅 난이도도 같이 올라갑니다.

실무에서 많이 보는 멀티스레딩 사용 사례
개념보다 실제로 어디서 등장하는지를 알면 훨씬 감이 잘 잡힙니다.

1. 웹 서버 요청 처리

Spring Boot 같은 서버는 동시에 여러 사용자의 요청을 받아야 합니다. 한 요청이 DB 조회를 하는 동안 다른 요청까지 같이 막히면 서비스가 느려집니다. 그래서 스레드 풀이 요청을 분산 처리합니다.

2. 비동기 이메일/알림 전송

회원가입 직후 이메일 발송을 메인 요청에서 다 처리하면 응답이 느려질 수 있습니다. 이럴 때는 비동기 작업으로 분리하는 편이 많습니다.

3. 파일 업로드 후 후처리

이미지 리사이징, 로그 적재, 메시지 큐 전송처럼 사용자가 기다릴 필요 없는 후속 작업은 별도 스레드나 비동기 실행으로 분리합니다.

4. 배치/병렬 처리

대량 데이터 처리에서도 구간을 나눠 병렬 작업을 붙이는 경우가 많습니다. 다만 이 구간은 오히려 락, DB 연결 수, 외부 API 제한 때문에 무작정 스레드를 늘리면 안 됩니다.

꼭 봐야 할 포인트
  • 멀티스레딩은 "동시에 많이 처리"가 목적이 아니라, 응답성과 처리량을 균형 있게 높이는 것이 목적입니다.
  • DB, 외부 API, 파일 IO 같은 병목이 있으면 스레드 수를 늘려도 기대만큼 빨라지지 않을 수 있습니다.
  • 실무에서는 코드 문법보다 스레드풀 크기, 큐, 타임아웃, 예외 처리가 더 중요해지는 경우가 많습니다.
멀티스레딩에서 자주 막히는 문제
자바 멀티스레딩은 실행보다 공유 자원 관리가 더 어렵습니다.

1. Race Condition

여러 스레드가 같은 데이터를 동시에 수정하면 결과가 엉킬 수 있습니다.

class Counter {
    int count = 0;

    void increase() {
        count++;
    }
}

위 코드는 단순해 보이지만, 여러 스레드가 동시에 increase()를 호출하면 기대한 값보다 작게 나올 수 있습니다.

2. Synchronization 문제

이 문제를 막기 위해 synchronized를 사용할 수 있습니다.

class Counter {
    int count = 0;

    synchronized void increase() {
        count++;
    }
}

이렇게 하면 동시에 한 스레드만 해당 메서드에 들어오게 할 수 있습니다. 하지만 무조건 많이 걸면 성능이 떨어질 수 있으므로 필요한 부분에만 써야 합니다.

3. Deadlock

서로 다른 락을 잡은 두 스레드가 서로를 기다리면 프로그램이 멈춘 것처럼 보일 수 있습니다. 실무에서 정말 위험한 문제 중 하나입니다.

4. 가시성(Visibility) 문제

한 스레드가 바꾼 값을 다른 스레드가 바로 보지 못하는 경우도 있습니다. 이때 volatile, synchronized, java.util.concurrent 도구들이 중요해집니다.

실무에서는 synchronized만으로 충분할까?
기본은 중요하지만, 실제 코드에서는 더 적합한 도구를 함께 봐야 합니다.

실무에서는 단순히 synchronized만 많이 붙인다고 좋은 코드가 되지 않습니다.

예를 들어 카운터 증가처럼 아주 단순한 공유 숫자라면 AtomicInteger가 더 직관적일 수 있습니다.

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private final AtomicInteger count = new AtomicInteger();

    void increase() {
        count.incrementAndGet();
    }

    int getCount() {
        return count.get();
    }
}

또 여러 스레드가 접근하는 맵 구조라면 일반 HashMap 대신 ConcurrentHashMap을 더 많이 고려합니다.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

ConcurrentMap<String, Integer> cache = new ConcurrentHashMap<>();
cache.put("user:1", 100);

즉 실무에서는

  • 단일 임계구역이면 synchronized
  • 숫자 연산이면 AtomicInteger
  • 동시 접근 Map이면 ConcurrentHashMap

처럼 문제 성격에 따라 도구를 나눠 쓰는 경우가 많습니다.

CPU Bound와 IO Bound를 구분해야 하는 이유
실무에서 스레드 수를 정할 때 가장 중요한 기준 중 하나입니다.

모든 작업이 같은 방식으로 스레드를 써야 하는 것은 아닙니다.

CPU Bound 작업

계산량이 많은 작업입니다.

  • 이미지 처리
  • 복잡한 연산
  • 암호화
  • 대량 정렬/계산

이런 작업은 CPU가 병목이므로 스레드를 너무 많이 늘리면 오히려 문맥 전환만 증가할 수 있습니다.

IO Bound 작업

대기 시간이 긴 작업입니다.

  • DB 조회
  • 외부 API 호출
  • 파일 읽기/쓰기
  • 네트워크 요청

이런 작업은 CPU보다 대기 시간이 길기 때문에, 적절한 수준의 병렬 처리가 더 유리할 수 있습니다.

작업 유형 병목 실무 관점
CPU Bound CPU 스레드 수를 무작정 늘리면 비효율 가능
IO Bound 대기 시간 적절한 병렬 처리로 응답성 개선 가능

이 차이를 모르고 스레드풀을 잡으면, 시스템이 느린 이유를 잘못 판단하기 쉽습니다.

Spring 실무와 연결하면 어떻게 볼까?
자바 멀티스레딩 개념은 스프링에서도 그대로 이어집니다.

Spring Boot를 쓰면 개발자가 직접 Thread를 만들지 않아도 멀티스레딩을 접하게 됩니다.

예를 들면,

  • 웹 요청 처리 스레드
  • @Async 비동기 실행
  • 스케줄러 작업
  • 배치 병렬 처리

가 대표적입니다.

특히 @Async는 실무에서 자주 등장합니다.

@Async
public void sendEmail(String email) {
    // 이메일 발송
}

하지만 여기서도 핵심은 어노테이션 자체보다, 뒤에서 어떤 스레드풀이 쓰이고 있는지입니다. 즉 스프링에서도 결국은 스레드풀 설정, 예외 처리, 큐 관리를 같이 봐야 진짜 실무형 이해라고 할 수 있습니다.

실무 체크리스트
  • 공유 자원이 있는지 먼저 확인
  • 단순히 동시에 실행된다고 안전한 것은 아님
  • `Thread` 직접 생성보다 `ExecutorService` 우선 검토
  • 락 범위는 최소화
  • 스레드풀 크기, 큐, 타임아웃, 예외 처리까지 같이 설계
  • CPU Bound / IO Bound 구분 없이 스레드 수를 잡지 않기
자바에서 꼭 알아야 할 멀티스레딩 포인트
개념 이해에서 끝내지 말고, 코드 선택 기준까지 같이 알아야 합니다.
  • Thread: 가장 기본적인 스레드 클래스지만 실무 기본 선택은 아님
  • Runnable: 작업 로직 분리에 적합
  • Callable: 반환값이 필요한 비동기 작업에 적합
  • ExecutorService: 스레드 풀 관리에 사실상 표준
  • synchronized: 동기화 기본 키워드
  • ConcurrentHashMap: 멀티스레드 환경에서 자주 쓰는 동시성 컬렉션
  • AtomicInteger: 단순 카운터 동시성 처리에 유용

특히 서버 개발에서는 스레드를 "만드는 법"보다 안전하게 공유 자원을 다루고 장애를 예측하는 법이 더 중요합니다.

상황 우선 떠올릴 도구 이유
단순 비동기 작업 실행 Runnable + ExecutorService 직접 스레드 관리보다 안정적
반환값이 필요한 비동기 작업 Callable / Future 결과값 수집 가능
공유 자원 동기화 synchronized / Lock 동시 수정 충돌 방지
동시성 컬렉션 필요 ConcurrentHashMap 멀티스레드 접근에 더 적합
초보자가 꼭 체크할 포인트
  • 프로세스는 독립된 실행 환경, 스레드는 그 안의 실행 흐름
  • 스레드는 자원을 공유하므로 효율적이지만 위험도 함께 증가
  • 자바 프로그램은 `main` 스레드부터 시작
  • 멀티스레딩은 동시 실행 자체보다 동기화와 공유 자원 관리가 핵심
  • 실무에서는 `Thread` 직접 생성보다 `ExecutorService`가 더 자주 쓰임
  • `synchronized`만 아는 것보다 `AtomicInteger`, `ConcurrentHashMap`, 스레드풀 운영 관점까지 같이 알아야 실무형 이해가 됩니다.
FAQ
  • Q. 프로세스와 스레드 중 무엇이 더 가벼운가요?
    → 일반적으로 스레드가 더 가볍습니다. 같은 프로세스 자원을 공유하기 때문에 생성과 전환 비용이 더 적은 편입니다.
  • Q. 자바에서 멀티스레딩은 꼭 필요한가요?
    → 모든 프로그램에 무조건 필요한 건 아닙니다. 하지만 서버 처리, 비동기 작업, 응답성 유지가 중요한 환경에서는 매우 자주 사용됩니다.
  • Q. `Thread`와 `Runnable` 중 무엇을 써야 하나요?
    → 보통은 `Runnable` 또는 `ExecutorService` 방식이 더 유연합니다. `Thread` 상속은 학습용으로는 좋지만 실무 기본 선택은 아닙니다.
  • Q. 멀티스레딩이면 무조건 성능이 좋아지나요?
    → 아닙니다. 문맥 전환 비용, 락 경합, 디버깅 난이도 때문에 오히려 복잡해질 수도 있습니다. 작업 성격에 맞게 써야 합니다.
결론
프로세스와 스레드의 차이를 이해해야 자바 멀티스레딩도 제대로 보입니다.

프로세스와 스레드는 둘 다 동시에 실행되는 것처럼 보일 수 있지만, 구조적으로는 분명히 다릅니다.

  • 프로세스는 독립된 실행 단위
  • 스레드는 프로세스 내부 작업 흐름
  • 자바 멀티스레딩은 하나의 JVM 안에서 여러 흐름을 동시에 처리하는 방식

이 기준만 명확히 잡아도 Thread, Runnable, ExecutorService, synchronized 같은 자바 개념이 훨씬 자연스럽게 연결됩니다.

그리고 실무에서는 멀티스레딩을 단순히 "빨라지는 기술"로 보면 부족합니다. 스레드풀을 어떻게 운영할지, 공유 자원을 어떻게 보호할지, 작업이 CPU Bound인지 IO Bound인지를 같이 봐야 실제 서비스에서 흔들리지 않습니다.

프로세스와 스레드의 차이를 정확히 이해하는 순간, 자바 멀티스레딩은 단순 문법이 아니라 구조·운영·설계의 문제로 보이기 시작합니다.

※ 이 글은 자바 입문자와 백엔드 초급 개발자가 프로세스·스레드·멀티스레딩 개념을 함께 잡기 위한 구조 이해형 가이드입니다. 다음 단계에서는 `synchronized`, `volatile`, `Lock`, `ExecutorService`, `CompletableFuture`까지 이어서 학습하면 훨씬 단단해집니다.

728x90
댓글
반응형
최근에 올라온 글
글 보관함
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30