1. 스레드에 매개변수 전달
스레드를 시작할 때는 파일 이름, 숫자 범위, 인사말 같은 ‘작업’을 넘겨주고 싶을 때가 많습니다. 매개변수가 없는 스레드는 주소 없는 배달원과 같습니다: 도시를 뛰어다니지만 피자를 어디에 배달해야 할지 모릅니다.
어떻게 하나요?
Java에서 스레드는 보통 두 가지 방식으로 생성됩니다:
- Thread 클래스를 상속
- Runnable 인터페이스 구현(또는 람다식 사용)
매개변수 전달은 대개 Runnable을 구현하는 클래스의 생성자를 통해 이뤄집니다. 간단하고, 안전하며, 명확합니다. 필드를 final로 두면 다른 스레드에서 매개변수가 바뀌지 않도록 보호할 수 있습니다. 세터/공개 필드를 사용하면 동기화 문제를 일으키기 쉽습니다.
예제 1: 생성자를 통한 매개변수 전달 스레드
학습용 애플리케이션을 보강해 보겠습니다: 사용자 주문을 별도 스레드에서 처리합니다.
public class OrderProcessor implements Runnable {
private final String orderId;
public OrderProcessor(String orderId) {
this.orderId = orderId;
}
@Override
public void run() {
System.out.println("주문 처리 중: " + orderId + " 스레드 " + Thread.currentThread().getName());
// 여기에는 오래 걸리는 처리가 있을 수 있습니다...
}
}
스레드를 생성하고 시작합니다:
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new OrderProcessor("ORDER-001"));
Thread thread2 = new Thread(new OrderProcessor("ORDER-002"));
thread1.start();
thread2.start();
}
}
예상 출력:
주문 처리 중: ORDER-001 스레드 Thread-0
주문 처리 중: ORDER-002 스레드 Thread-1
예제 2: 람다식(Java 8+)으로 매개변수 전달
작업이 단순하다면 별도의 클래스를 만들지 않아도 됩니다.
public class Main {
public static void main(String[] args) {
String user1 = "Vasya";
String user2 = "Katya";
Thread thread1 = new Thread(() -> {
System.out.println("안녕, " + user1 + " 에서 " + Thread.currentThread().getName());
});
Thread thread2 = new Thread(() -> {
System.out.println("안녕, " + user2 + " 에서 " + Thread.currentThread().getName());
});
thread1.start();
thread2.start();
}
}
왜 세터/게터로 매개변수를 전달하면 안 될까요?
세터나 공개 필드로 매개변수를 전달하면, 실행 중에 다른 스레드가 값을 바꿔버릴 위험이 있습니다. 이는 잡기 어려운 버그로 이어질 수 있습니다. 필드를 final로 만들고 생성자를 통해 전달하는 것이 좋습니다.
2. 우선순위
Java에서 각 스레드는 우선순위를 갖습니다 — 1부터 10까지의 정수로, 스케줄러(scheduler)에게 다른 스레드와 비교해 이 스레드가 얼마나 ‘중요한지’를 알립니다. 기본적으로 모든 스레드는 우선순위 5(Thread.NORM_PRIORITY)를 가집니다.
중요: 우선순위는 운영체제에 주는 ‘힌트’일 뿐입니다. 우선순위가 10이라고 해서 1보다 항상 더 빨리 실행된다는 보장은 없습니다. 운영체제, 그 설정, 그리고 현재 부하에 따라 달라집니다.
스레드의 우선순위를 어떻게 설정하나요?
Thread 클래스에는 다음 메서드가 있습니다:
- setPriority(int newPriority)
- getPriority()
표준 상수 세 가지:
- Thread.MIN_PRIORITY (1)
- Thread.NORM_PRIORITY (5)
- Thread.MAX_PRIORITY (10)
예제 3: 스레드 우선순위 설정
public class PriorityDemo {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("스레드 " + Thread.currentThread().getName() +
" — 우선순위 " + Thread.currentThread().getPriority());
};
Thread low = new Thread(task, "LowPriority");
Thread norm = new Thread(task, "NormalPriority");
Thread high = new Thread(task, "HighPriority");
low.setPriority(Thread.MIN_PRIORITY); // 1
norm.setPriority(Thread.NORM_PRIORITY); // 5
high.setPriority(Thread.MAX_PRIORITY); // 10
low.start();
norm.start();
high.start();
}
}
예상 출력(행 순서는 보장되지 않습니다!):
스레드 LowPriority — 우선순위 1
스레드 HighPriority — 우선순위 10
스레드 NormalPriority — 우선순위 5
우선순위가 실행 순서에 영향을 줄까요?
대부분의 경우 — 아닙니다. 우선순위는 스레드에 할당되는 CPU 시간에 영향을 줄 수는 있지만, 시작/종료 순서를 보장하지는 않습니다. 프로그램의 논리를 우선순위에 기대지 마세요 — 예를 들어 백그라운드 스레드가 메인 스레드를 방해하지 않도록 하는 등 ‘부드러운’ 힌트로만 사용하세요.
표: 스레드 우선순위 상수
| 상수 | 값 | 설명 |
|---|---|---|
|
1 | 가장 낮은 우선순위 |
|
5 | 보통 우선순위 |
|
10 | 가장 높은 우선순위 |
3. 스레드 이름 지정
스레드 이름은 다중 작업 세계의 ‘별명’입니다. 스레드가 수십 개일 때, 로그에 "Thread-7" 대신 "FileUploader-1"이 보이면 디버깅이 훨씬 쉬워집니다. 특히 로그 분석과 오류 추적에 중요합니다.
스레드 이름을 설정하는 방법?
이름은 Thread 생성자에서 바로 지정하거나 setName 메서드로 설정할 수 있습니다. 현재 이름을 알 수 있는 메서드는 getName입니다.
Thread t = new Thread(() -> {
System.out.println("작동 중!");
}, "MyThreadName");
t.start();
예제 4: 이름 있는 스레드
public class NamedThreads {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println("나는 스레드: " + Thread.currentThread().getName());
}, "Downloader");
Thread threadB = new Thread(() -> {
System.out.println("나는 스레드: " + Thread.currentThread().getName());
});
threadB.setName("Uploader");
threadA.start();
threadB.start();
}
}
예상 출력:
나는 스레드: Downloader
나는 스레드: Uploader
4. 실습: 서로 다른 매개변수, 우선순위, 이름을 가진 여러 스레드
모두를 하나의 예제로 합쳐 보겠습니다: 각 주문은 고유한 이름과 우선순위를 가진 별도 스레드입니다.
public class OrderProcessor implements Runnable {
private final String orderId;
private final int processingTimeMs;
public OrderProcessor(String orderId, int processingTimeMs) {
this.orderId = orderId;
this.processingTimeMs = processingTimeMs;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] 주문 처리 시작: " + orderId +
" (우선순위 " + Thread.currentThread().getPriority() + ")");
try {
Thread.sleep(processingTimeMs); // 작업 모사
} catch (InterruptedException e) {
System.out.println("[" + Thread.currentThread().getName() + "] 중단됨!");
}
System.out.println("[" + Thread.currentThread().getName() + "] 주문 처리 완료: " + orderId);
}
public static void main(String[] args) {
Thread fastOrder = new Thread(new OrderProcessor("FAST-ORDER", 500), "FastOrderThread");
Thread normalOrder = new Thread(new OrderProcessor("NORMAL-ORDER", 1000), "NormalOrderThread");
Thread slowOrder = new Thread(new OrderProcessor("SLOW-ORDER", 2000), "SlowOrderThread");
fastOrder.setPriority(Thread.MAX_PRIORITY);
normalOrder.setPriority(Thread.NORM_PRIORITY);
slowOrder.setPriority(Thread.MIN_PRIORITY);
fastOrder.start();
normalOrder.start();
slowOrder.start();
}
}
무엇을 보게 될까요:
- 각 스레드는 어떤 주문을 처리하는지, 자신의 이름과 우선순위를 출력합니다.
- Thread.sleep으로 처리 시간을 모사합니다.
- 스레드 종료 순서는 우선순위와 일치하지 않을 수 있습니다.
5. 중요한 뉘앙스와 특징
매개변수 전달: 생성자만 사용하세요!
그게 더 안전합니다. 매개변수를 final로 선언하면, 객체 생성 후 변경할 수 없고 다른 스레드가 값을 ‘끼워 넣을’ 수 없습니다. 데이터 레이스를 예방하는 데 중요합니다.
우선순위: 비즈니스 로직을 기대하지 마세요
우선순위는 종업원에게 “커피 조금 빨리 부탁해요”라고 말하는 것과 같습니다. 가끔은 통하지만, 가끔은 아닙니다. 운영체제를 위한 권고일 뿐, 실행 규칙이 아닙니다.
스레드 이름: 디버깅에 적극 활용하세요
로그 분석 시 시간을 크게 절약해 줍니다. 처음부터 의미 있는 이름을 붙이는 습관을 들이세요.
6. 스레드 매개변수와 우선순위에서 흔한 실수
오류 #1: 공개 필드로 매개변수를 전달. 매개변수를 일반 필드로 선언하고 스레드 시작 후 변경하면 예측 불가능한 결과를 초래할 수 있습니다. final 필드와 생성자를 사용하세요.
오류 #2: 우선순위가 순서를 보장한다고 기대. Thread.MAX_PRIORITY를 설정했다고 해서 항상 먼저 실행된다고 생각하나요? 아닙니다. 동기화나 로직 제어에 우선순위를 사용하지 마세요.
오류 #3: 로그에 이름 없는 스레드. 로그에 “Thread-3”, “Thread-7”만 있으면 원인을 찾기 어렵습니다. 생성자나 setName으로 스레드에 의미 있는 이름을 부여하세요.
오류 #4: 하나의 Runnable 객체를 여러 스레드에 사용. Runnable 객체가 상태를 가지고 있고 이를 여러 스레드에 전달하면 매개변수를 ‘공유’하게 되어 데이터 레이스로 직행합니다. 스레드마다 별도의 Runnable을 생성하세요.
오류 #5: set 메서드로 매개변수 전달. 객체 생성/시작 후 set* 메서드로 매개변수를 지정하면 실행 중 값이 바뀔 수 있습니다 — 전형적인 race condition입니다.
GO TO FULL VERSION