💻 프로그래밍

Thread

스레드

프로세스 내 실행 단위. 메모리 공유. 동시성 기본.

📖 상세 설명

Thread(스레드)는 프로세스 내에서 실행되는 가장 작은 실행 단위입니다. 하나의 프로세스는 최소 하나의 스레드(메인 스레드)를 가지며, 필요에 따라 여러 스레드를 생성하여 동시에 작업을 수행할 수 있습니다. 스레드는 프로세스의 코드, 데이터, 힙 영역을 공유하지만, 각자 독립적인 스택과 레지스터를 가집니다. 이러한 특성 덕분에 스레드 간 통신이 프로세스 간 통신(IPC)보다 빠르고 효율적입니다.

프로세스와 스레드의 핵심적인 차이는 메모리 공유 여부입니다. 프로세스는 독립된 메모리 공간을 가지므로 서로 직접 접근할 수 없지만, 같은 프로세스 내의 스레드들은 힙 메모리를 공유합니다. 프로세스 생성은 전체 메모리 복사가 필요해 비용이 크지만, 스레드 생성은 스택만 할당하면 되어 상대적으로 가볍습니다. 다만, 메모리 공유로 인해 한 스레드의 오류가 전체 프로세스에 영향을 줄 수 있습니다.

멀티스레딩의 장점은 CPU 활용률 극대화, 응답성 향상, 자원 공유 효율성입니다. 단일 스레드에서 I/O 대기 시간 동안 CPU가 놀게 되지만, 멀티스레딩에서는 다른 스레드가 CPU를 활용할 수 있습니다. 그러나 단점도 명확합니다. 동기화 문제(Race Condition), 교착 상태(Deadlock), 디버깅 어려움, 컨텍스트 스위칭 오버헤드 등이 발생할 수 있습니다. 스레드 수가 과도하면 오히려 성능이 저하됩니다.

실무에서 스레드는 병렬 처리와 비동기 작업에 핵심적으로 활용됩니다. 웹 서버는 요청마다 스레드를 할당하여 동시 처리하고, GUI 애플리케이션은 UI 스레드와 작업 스레드를 분리하여 반응성을 유지합니다. 최근에는 스레드 풀(Thread Pool)을 사용하여 스레드 생성/소멸 비용을 줄이고, Java의 ExecutorService, Python의 ThreadPoolExecutor 같은 고수준 API로 스레드를 관리합니다. 또한 async/await 패턴과 코루틴이 많은 경우 스레드를 대체하고 있습니다.

💻 코드 예제

// Java Thread 예제 - 다양한 스레드 생성 및 동기화

import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class ThreadExample {

    // 1. Thread 클래스 상속
    static class MyThread extends Thread {
        private String name;

        public MyThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(name + ": " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    // 2. Runnable 인터페이스 구현 (권장)
    static class MyRunnable implements Runnable {
        private String name;

        public MyRunnable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name + " 실행 중 - " +
                Thread.currentThread().getName());
        }
    }

    // 3. 동기화 예제 - synchronized
    static class Counter {
        private int count = 0;

        // synchronized 메서드
        public synchronized void increment() {
            count++;
        }

        // synchronized 블록
        public void incrementWithBlock() {
            synchronized(this) {
                count++;
            }
        }

        public int getCount() {
            return count;
        }
    }

    // 4. ReentrantLock 사용
    static class SafeCounter {
        private int count = 0;
        private final ReentrantLock lock = new ReentrantLock();

        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();  // 반드시 finally에서 해제
            }
        }

        public int getCount() {
            return count;
        }
    }

    public static void main(String[] args) throws Exception {
        // Thread 클래스 사용
        MyThread t1 = new MyThread("Thread-1");
        t1.start();

        // Runnable 사용
        Thread t2 = new Thread(new MyRunnable("Runnable-1"));
        t2.start();

        // Lambda 표현식 (Java 8+)
        Thread t3 = new Thread(() -> {
            System.out.println("Lambda Thread 실행");
        });
        t3.start();

        // ExecutorService - 스레드 풀 (권장)
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " - " +
                    Thread.currentThread().getName());
            });
        }

        // Future를 통한 결과 받기
        Future future = executor.submit(() -> {
            Thread.sleep(1000);
            return 42;
        });

        System.out.println("결과: " + future.get());  // 블로킹

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);

        // CompletableFuture (Java 8+, 비동기 체이닝)
        CompletableFuture.supplyAsync(() -> "Hello")
            .thenApply(s -> s + " World")
            .thenAccept(System.out::println)
            .join();
    }
}
# Python threading 예제 - 스레드 생성 및 동기화

import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue

# 1. 기본 스레드 생성 - Thread 클래스
def worker(name: str, delay: float) -> None:
    """작업 함수"""
    print(f"{name} 시작")
    time.sleep(delay)
    print(f"{name} 완료")

# 함수 기반 스레드
t1 = threading.Thread(target=worker, args=("Worker-1", 1))
t1.start()
t1.join()  # 스레드 종료 대기

# 2. Thread 클래스 상속
class MyThread(threading.Thread):
    def __init__(self, name: str):
        super().__init__()
        self.name = name
        self.result = None

    def run(self):
        print(f"{self.name} 실행 중")
        time.sleep(0.5)
        self.result = f"{self.name} 결과"

thread = MyThread("Custom-Thread")
thread.start()
thread.join()
print(thread.result)

# 3. 동기화 - Lock
counter = 0
lock = threading.Lock()

def increment_with_lock():
    global counter
    for _ in range(100000):
        with lock:  # context manager 사용 권장
            counter += 1

threads = [threading.Thread(target=increment_with_lock) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Counter: {counter}")  # 400000

# 4. RLock (재진입 가능한 락)
rlock = threading.RLock()

def recursive_function(n: int):
    with rlock:
        if n > 0:
            print(f"Level {n}")
            recursive_function(n - 1)

# 5. Condition - 스레드 간 신호
condition = threading.Condition()
items = []

def producer():
    for i in range(5):
        with condition:
            items.append(i)
            print(f"Produced: {i}")
            condition.notify()  # 대기 중인 스레드 깨우기
        time.sleep(0.1)

def consumer():
    for _ in range(5):
        with condition:
            while not items:
                condition.wait()  # 신호 대기
            item = items.pop(0)
            print(f"Consumed: {item}")

# 6. ThreadPoolExecutor (권장)
def fetch_url(url: str) -> dict:
    """URL 데이터 가져오기 시뮬레이션"""
    time.sleep(0.5)  # 네트워크 지연 시뮬레이션
    return {"url": url, "status": 200}

urls = [f"https://api.example.com/{i}" for i in range(10)]

with ThreadPoolExecutor(max_workers=4) as executor:
    # submit으로 개별 제출
    futures = {executor.submit(fetch_url, url): url for url in urls}

    for future in as_completed(futures):
        url = futures[future]
        try:
            result = future.result()
            print(f"{url}: {result['status']}")
        except Exception as e:
            print(f"{url} 실패: {e}")

# 7. Queue를 이용한 생산자-소비자 패턴
task_queue = Queue()

def queue_producer():
    for i in range(10):
        task_queue.put(f"Task-{i}")
    task_queue.put(None)  # 종료 신호

def queue_consumer():
    while True:
        task = task_queue.get()
        if task is None:
            break
        print(f"Processing: {task}")
        task_queue.task_done()

# 8. daemon 스레드 - 메인 종료 시 자동 종료
daemon_thread = threading.Thread(target=worker, args=("Daemon", 10))
daemon_thread.daemon = True  # 데몬 스레드 설정
daemon_thread.start()
# 메인 종료 시 데몬 스레드도 종료됨

# 9. 현재 스레드 정보
print(f"현재 스레드: {threading.current_thread().name}")
print(f"활성 스레드 수: {threading.active_count()}")

🗣️ 실무 대화 예시

💬 면접에서: 스레드 동기화 질문

"스레드 동기화에는 여러 방법이 있습니다. 가장 기본은 synchronized 키워드나 Lock을 사용한 상호 배제입니다. 하지만 단순 카운터라면 AtomicInteger 같은 원자적 연산이 더 효율적이고, 읽기가 많은 경우 ReentrantReadWriteLock을 사용합니다. Java 21부터는 Virtual Thread가 도입되어 가벼운 동시성 처리가 가능해졌고, synchronized 대신 ReentrantLock을 쓰면 타임아웃, 인터럽트, 공정성 설정 같은 세밀한 제어가 가능합니다."

💬 디버깅 중: 데드락 발생

"서버가 응답이 없어서 jstack으로 스레드 덤프를 떴더니 데드락이 발견됐습니다. Thread-1이 Lock-A를 잡고 Lock-B를 기다리고, Thread-2가 Lock-B를 잡고 Lock-A를 기다리는 상황이에요. 해결책으로 락 획득 순서를 일관되게 정하거나, tryLock으로 타임아웃을 설정하고, 가능하면 락의 범위를 줄여서 홀딩 시간을 최소화해야 합니다. 장기적으로는 락-프리 자료구조나 메시지 큐 패턴 도입을 검토해야 할 것 같습니다."

💬 코드 리뷰에서: 스레드 풀 설정

"스레드 풀 크기가 CPU 코어 수의 10배로 설정되어 있네요. 이 작업이 CPU-bound인지 I/O-bound인지 확인해봐야 합니다. CPU-bound라면 코어 수 정도가 적당하고, I/O-bound라면 대기 시간 비율에 따라 더 늘릴 수 있어요. 또한 Executors.newFixedThreadPool 대신 ThreadPoolExecutor를 직접 생성해서 큐 크기와 rejection 정책을 명시하는 게 프로덕션에서 더 안전합니다."

⚠️ 주의사항

🔄
Race Condition (경쟁 조건)

여러 스레드가 공유 자원에 동시 접근할 때 실행 순서에 따라 결과가 달라지는 문제입니다. 읽기-수정-쓰기 연산이 원자적이지 않으면 발생합니다. synchronized, Lock, 또는 Atomic 클래스로 해결하세요. 단, 과도한 동기화는 성능 저하와 데드락의 원인이 됩니다.

🔒
Deadlock (교착 상태)

두 개 이상의 스레드가 서로 상대방이 가진 락을 기다리며 무한 대기하는 상태입니다. 예방하려면: 1) 락 획득 순서를 일관되게 유지, 2) tryLock으로 타임아웃 설정, 3) 락 홀딩 시간 최소화, 4) 데드락 감지 도구 활용. jstack, VisualVM으로 스레드 덤프를 분석하면 데드락을 진단할 수 있습니다.

📊
스레드 풀 관리

스레드를 무한정 생성하면 메모리 부족과 컨텍스트 스위칭 오버헤드로 성능이 급락합니다. 반드시 스레드 풀을 사용하고, 적절한 크기를 설정하세요. CPU-bound 작업은 코어 수, I/O-bound 작업은 코어 수 * (1 + 대기시간/작업시간)이 기준입니다. 큐 크기와 rejection 정책도 명시적으로 설정하세요.

👁️
가시성(Visibility) 문제

한 스레드가 변경한 값을 다른 스레드가 보지 못하는 문제입니다. CPU 캐시와 컴파일러 최적화 때문에 발생합니다. Java에서는 volatile 키워드로 가시성을 보장하고, Python의 GIL은 어느 정도 보호해주지만 명시적 동기화가 권장됩니다. happens-before 관계를 이해하는 것이 중요합니다.

🔗 관련 용어

📚 더 배우기