4주차 Unit 3.4 — 데몬 스레드 (Daemon Thread)

Psj·5일 전

F-lab

목록 보기
130/154

Unit 3.4 — 데몬 스레드 (Daemon Thread)

F-LAB JAVA · 4주차 · Phase 3 · 스레드 만들고 다루기


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 데몬 스레드 (Daemon Thread) 의 정의는?
  • 일반 스레드 (User Thread) vs 데몬 스레드 의 차이는?
  • JVM 종료 조건 (모든 일반 스레드 종료) 은?
  • setDaemon(true) 의 설정 시점은?
  • 데몬 스레드의 용도 (GC, 모니터링, 로깅) 는?
  • 메인 스레드 종료 시 데몬 이 즉시 종료되는가?
  • 작업 완료 보장이 필요할 때 데몬을 쓰면 안 되는 이유는?
  • 파일 저장을 데몬으로 돌리면 어떤 사고가?
  • 데몬 스레드의 자식 스레드는?

🎯 핵심 한 문장

데몬 스레드 (Daemon Thread) 는 일반 스레드 (User Thread) 를 보조하는 백그라운드 스레드로, 모든 일반 스레드가 종료되면 JVM 과 함께 자동으로 종료된다.
JVM 은 모든 일반 (non-daemon) 스레드가 종료되면 종료 되며, 이때 남아있는 데몬 스레드는 작업 완료 여부와 무관하게 강제 종료된다.
데몬 설정은 setDaemon(true) 로 start() 호출 전에만 가능하다 (이후 호출 시 IllegalThreadStateException).
용도는 GC, 모니터링, 로깅 등 메인 작업을 보조하는 백그라운드 작업이며, 작업 완료를 보장해야 하는 작업 (파일 저장, DB 커밋) 은 데몬으로 설정하면 안 된다 — JVM 종료 시 중간에 강제 종료되어 데이터 손실이 발생할 수 있다.
메인 스레드가 종료되어도 다른 일반 스레드가 살아있으면 데몬은 계속 실행 된다 (메인이 아니라 "모든 일반 스레드" 가 기준).

비유 — 매장의 정직원과 알바

일반 스레드 (User Thread) = 정직원:
  - 매장의 핵심 업무
  - 정직원이 모두 퇴근해야 매장 마감

데몬 스레드 (Daemon Thread) = 보조 알바:
  - 청소, 음악 틀기 등 보조
  - 정직원이 모두 퇴근하면
  - 알바도 (하던 일 중이라도) 함께 퇴근

매장 마감 (JVM 종료):
  - 모든 정직원 퇴근 시
  - 알바는 작업 중이라도 강제 퇴근

위험:
  - 중요 업무 (금고 정산) 를 알바에게 맡기면?
  - 정직원 퇴근 시 정산 중간에 알바도 퇴근
  - 정산 미완료 (데이터 손실)
  → 중요 작업은 정직원 (일반 스레드)

→ 데몬 = 보조 알바, 일반 = 정직원, 모든 정직원 퇴근 시 알바도 강제 퇴근.


🧭 9개 섹션 로드맵

1. 데몬 스레드의 정의
2. 일반 스레드 vs 데몬 스레드
3. JVM 종료 조건
4. setDaemon(true) 설정
5. 데몬 스레드의 용도
6. 메인 스레드 종료와 데몬
7. 작업 완료 보장과 데몬 위험
8. 데몬의 자식 스레드와 실무
9. 면접 + 자기 점검

1️⃣ 데몬 스레드의 정의

1.1 데몬 스레드란

데몬 스레드 (Daemon Thread):

  일반 스레드를 보조하는 백그라운드 스레드.
  모든 일반 스레드 종료 시 JVM 과 함께 종료.

특징:
  - 보조 작업
  - 일반 스레드에 의존
  - 자동 종료

1.2 어원

"Daemon" 의 의미:

  - 그리스 신화의 정령 (보조 존재)
  - Unix 의 백그라운드 프로세스 (httpd, sshd)
  - 보이지 않게 일하는 보조

자바:
  - 일반 스레드 뒤에서 보조
  - GC 스레드가 대표적

1.3 데몬 설정

Thread daemon = new Thread(() -> {
    while (true) {
        doBackgroundWork();
        Thread.sleep(1000);
    }
});

daemon.setDaemon(true);   // ★ 데몬으로 설정 (start 전)
daemon.start();

// 일반 스레드 종료 시
// 이 데몬도 (무한 루프 중이라도) 종료

1.4 데몬 확인

Thread t = new Thread(task);
System.out.println(t.isDaemon());   // false (기본)

t.setDaemon(true);
System.out.println(t.isDaemon());   // true

// 메인 스레드는 일반 스레드
System.out.println(Thread.currentThread().isDaemon());   // false

1.5 기본값

데몬 스레드의 기본값:

  새 스레드의 데몬 여부 = 생성한 스레드의 데몬 여부 상속

  메인 스레드 (일반) 가 만든 스레드:
    → 일반 (false)

  데몬 스레드가 만든 스레드:
    → 데몬 (true)

1.6 ILIC 의 맥락

public class ShipmentDaemonExample {
    
    // 백그라운드 모니터링 (데몬)
    public void startMonitoring() {
        Thread monitor = new Thread(() -> {
            while (true) {
                logSystemStats();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        monitor.setDaemon(true);   // 데몬 (보조 작업)
        monitor.start();
        // 메인 작업 종료 시 모니터링도 자동 종료
    }
    
    private void logSystemStats() {
        log.info("Memory: {}, Threads: {}", 
            Runtime.getRuntime().freeMemory(),
            Thread.activeCount());
    }
}

1.7 자기 점검 답변

데몬 스레드의 정의는?

:
1. 정의:

  • 일반 스레드 보조
  • 백그라운드
  • 일반 스레드 종료 시 자동 종료
  1. 어원:

    • 보조 존재
    • Unix 백그라운드
  2. 설정:

    • setDaemon(true)
    • start 전
  3. 기본값:

    • 생성 스레드 상속
    • 메인이 만들면 일반

2️⃣ 일반 스레드 vs 데몬 스레드

2.1 비교

항목일반 스레드 (User)데몬 스레드 (Daemon)
역할핵심 작업보조 작업
JVM 종료모두 종료해야 JVM 종료JVM 종료 시 강제 종료
기본값false (일반)(상속)
예시메인, 작업 스레드GC, 모니터링
작업 보장완료 보장보장 X

2.2 일반 스레드 (User Thread)

일반 스레드 (User Thread):

  - 핵심 작업 수행
  - JVM 은 모든 일반 스레드 종료까지 살아있음
  - 작업 완료 보장

예:
  - main 스레드
  - 작업 처리 스레드
  - 명시적으로 만든 스레드 (기본)

2.3 데몬 스레드 (Daemon)

데몬 스레드 (Daemon Thread):

  - 보조 작업
  - 일반 스레드 종료 시 강제 종료
  - 작업 완료 보장 X

예:
  - GC 스레드
  - JIT 컴파일러 스레드
  - 모니터링, 로깅
  - finalize 스레드

2.4 시각화

일반 스레드 vs 데몬:

일반 스레드들:
  main:    [████████████]
  worker:  [████████████████]
                          ↑ 모두 종료
                          
데몬:
  monitor: [████████░░░░]   ← 강제 종료 (작업 중이라도)
                  ↑
              일반 스레드 종료 시점에 끊김

JVM 종료:
  - 모든 일반 스레드 종료 시
  - 데몬 강제 종료

2.5 예시 — 차이 확인

public class UserVsDaemon {
    
    public static void main(String[] args) throws InterruptedException {
        // 일반 스레드
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("User: " + i);
                sleep(500);
            }
        });
        userThread.start();
        
        // 데몬 스레드
        Thread daemonThread = new Thread(() -> {
            while (true) {   // 무한 루프
                System.out.println("Daemon working");
                sleep(500);
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        
        // main 종료 시:
        // - userThread 완료까지 JVM 유지
        // - userThread 종료 후 daemonThread 강제 종료
        // (무한 루프지만 종료됨)
    }
    
    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (Exception e) {}
    }
}

2.6 ILIC 의 맥락

public class ShipmentThreadTypes {
    
    // 일반 스레드 — 핵심 처리 (완료 보장)
    public void processCore(Shipment shipment) {
        Thread worker = new Thread(() -> {
            repository.save(shipment);   // 중요! 완료 보장 필요
        });
        // 데몬 설정 X (기본 일반)
        worker.start();
    }
    
    // 데몬 스레드 — 보조 (모니터링)
    public void startHealthMonitor() {
        Thread monitor = new Thread(() -> {
            while (true) {
                checkHealth();
                sleep(10000);
            }
        });
        monitor.setDaemon(true);   // 데몬 (보조)
        monitor.start();
    }
    
    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (Exception e) {}
    }
}

2.7 자기 점검 답변

일반 스레드 vs 데몬 스레드는?

:
1. 일반 스레드:

  • 핵심 작업
  • 완료 보장
  • JVM 유지 조건
  1. 데몬 스레드:

    • 보조 작업
    • 강제 종료
    • 보장 X
  2. JVM 종료:

    • 모든 일반 종료 시
    • 데몬 강제 종료
  3. 예시:

    • 일반: main, worker
    • 데몬: GC, 모니터링

3️⃣ JVM 종료 조건

3.1 종료 조건

JVM 종료 조건:

  모든 일반 (non-daemon) 스레드가 종료되면
  JVM 종료.

  이때:
    - 데몬 스레드는 강제 종료
    - 작업 완료 무관

핵심:
  - 일반 스레드 = JVM 생명줄
  - 데몬은 무관

3.2 정확한 규칙

JVM 종료 규칙:

1. 모든 일반 스레드 종료
   → JVM 종료 시작

2. 데몬 스레드 강제 종료
   → 작업 중이라도

3. System.exit() 호출
   → 즉시 종료 (모든 스레드)

핵심:
  - "일반 스레드 0개" 가 조건
  - 데몬 수는 무관

3.3 시각화

JVM 종료 시점:

시간 →
일반1:  [████████]
일반2:  [████████████]
                    ↑ 마지막 일반 스레드 종료
데몬1:  [████░░░░░░░░]  ← 여기서 강제 종료
데몬2:  [██████░░░░░░]  ← 여기서 강제 종료
                    ↑ JVM 종료

  일반 스레드 모두 종료 → JVM 종료 → 데몬 강제 종료

3.4 예시 — 일반 스레드만 기다림

public static void main(String[] args) {
    // 데몬 스레드 (무한)
    Thread daemon = new Thread(() -> {
        while (true) {
            System.out.println("daemon");
            sleep(100);
        }
    });
    daemon.setDaemon(true);
    daemon.start();
    
    // main (일반) 이 끝나면
    System.out.println("Main ending");
    // main 종료
    // → 일반 스레드 0개 (main 만 있었음)
    // → JVM 종료
    // → daemon 강제 종료 (무한 루프지만 끝남)
}

3.5 일반 스레드가 살아있으면

public static void main(String[] args) {
    // 데몬
    Thread daemon = new Thread(() -> infiniteWork());
    daemon.setDaemon(true);
    daemon.start();
    
    // 다른 일반 스레드
    Thread user = new Thread(() -> {
        sleep(10000);   // 10초
    });
    user.start();   // 일반 (데몬 설정 X)
    
    // main 종료
    // → 하지만 user (일반) 살아있음
    // → JVM 유지
    // → daemon 계속 (user 가 살아있으니)
    
    // user 종료 (10초 후)
    // → 일반 스레드 0개
    // → JVM 종료
    // → daemon 강제 종료
}

3.6 ILIC 의 맥락

public class JvmShutdownExample {
    
    public static void main(String[] args) {
        // 데몬 — 백그라운드 모니터링
        Thread monitor = new Thread(() -> {
            while (true) {
                logStats();
                sleep(5000);
            }
        });
        monitor.setDaemon(true);
        monitor.start();
        
        // 일반 — 핵심 처리
        Thread processor = new Thread(() -> {
            processAllShipments();   // 완료까지 JVM 유지
        });
        processor.start();
        
        // main 종료해도
        // processor (일반) 살아있으면 JVM 유지
        // monitor (데몬) 도 계속
        
        // processor 종료 시
        // → JVM 종료
        // → monitor 강제 종료
    }
    
    static void logStats() { }
    static void processAllShipments() { }
    static void sleep(long ms) { try { Thread.sleep(ms); } catch (Exception e) {} }
}

3.7 자기 점검 답변

JVM 종료 조건은?

:
1. 조건:

  • 모든 일반 스레드 종료
  • → JVM 종료
  1. 데몬:

    • 강제 종료
    • 작업 무관
  2. 규칙:

    • 일반 스레드 0개 = 종료
    • 데몬 수 무관
  3. System.exit():

    • 즉시 종료 (모두)

4️⃣ setDaemon(true) 설정

4.1 설정 시점

Thread t = new Thread(task);

// ✓ start() 전에 설정
t.setDaemon(true);
t.start();

// ❌ start() 후 설정
t.start();
t.setDaemon(true);   // IllegalThreadStateException

4.2 왜 start() 전에만

start() 전에만 가능한 이유:

  데몬 여부는 스레드 시작 시 결정.
  - 시작 후 변경은 의미 모호
  - 이미 실행 중인데 종류 변경?

  → start 전에 확정
  → 후에는 예외

4.3 IllegalThreadStateException

Thread t = new Thread(task);
t.start();   // RUNNABLE
t.setDaemon(true);   // ❌ IllegalThreadStateException
// "이미 시작된 스레드는 데몬 변경 불가"

// 올바른 순서
Thread t2 = new Thread(task);
t2.setDaemon(true);   // 먼저
t2.start();           // 그 다음

4.4 메서드

// 설정
void setDaemon(boolean on);

// 확인
boolean isDaemon();

// 사용
Thread t = new Thread(task);
t.setDaemon(true);
System.out.println(t.isDaemon());   // true
t.start();

4.5 빌더 패턴 / 팩토리

// ThreadFactory 로 데몬 설정
ThreadFactory daemonFactory = r -> {
    Thread t = new Thread(r);
    t.setDaemon(true);   // 팩토리에서 설정
    return t;
};

// Executor 에 적용
ExecutorService executor = Executors.newFixedThreadPool(4, daemonFactory);
// 풀의 모든 스레드가 데몬

// 람다로 간결
ThreadFactory factory = r -> {
    Thread t = new Thread(r, "daemon-worker");
    t.setDaemon(true);
    return t;
};

4.6 ILIC 의 맥락

public class DaemonThreadFactory {
    
    // 데몬 스레드 팩토리
    public static ThreadFactory daemonFactory(String prefix) {
        AtomicInteger counter = new AtomicInteger();
        return r -> {
            Thread t = new Thread(r, prefix + "-" + counter.incrementAndGet());
            t.setDaemon(true);   // 데몬
            return t;
        };
    }
    
    // 모니터링 풀 (데몬)
    public ScheduledExecutorService createMonitorPool() {
        return Executors.newScheduledThreadPool(
            2, 
            daemonFactory("monitor"));
        // 모니터링 스레드는 데몬 (앱 종료 시 자동 종료)
    }
    
    // 핵심 작업 풀 (일반)
    public ExecutorService createWorkerPool() {
        return Executors.newFixedThreadPool(4);
        // 기본 일반 스레드 (작업 완료 보장)
    }
}

4.7 자기 점검 답변

setDaemon(true) 설정은?

:
1. 시점:

  • start() 전에만
  • 후에는 예외
  1. 이유:

    • 시작 시 종류 결정
    • 후 변경 모호
  2. 예외:

    • IllegalThreadStateException
  3. 활용:

    • ThreadFactory
    • Executor 에 적용

5️⃣ 데몬 스레드의 용도

5.1 적합한 용도

데몬 스레드 적합 용도:

1. GC (Garbage Collection)
   - JVM 의 GC 스레드
   - 백그라운드 메모리 정리

2. 모니터링
   - 시스템 상태 체크
   - 메트릭 수집

3. 로깅
   - 비동기 로그 기록
   - 백그라운드 flush

4. 캐시 정리
   - 만료 항목 제거
   - 주기적 정리

5. 하트비트
   - 연결 유지
   - 상태 확인

5.2 공통 특징

데몬에 적합한 작업의 특징:

1. 보조적
   - 핵심 비즈니스 X
   - 지원 역할

2. 무한/반복
   - 계속 실행
   - 명시적 종료 불필요

3. 완료 보장 불필요
   - 중간 종료 OK
   - 데이터 손실 무관

4. 앱과 생명 같이
   - 앱 종료 시 함께

5.3 GC 스레드

// JVM 의 GC 스레드는 데몬
// 직접 만들지 않지만 개념 이해

// 모든 GC, JIT 스레드:
// - 데몬
// - 백그라운드
// - 앱 종료 시 함께

// 확인 (대략)
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
// GC 관련 스레드는 데몬

5.4 모니터링 예시

public class SystemMonitor {
    
    public void start() {
        Thread monitor = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                logMemory();
                logThreadCount();
                sleep(10000);
            }
        }, "system-monitor");
        monitor.setDaemon(true);   // 데몬 (보조)
        monitor.start();
        
        // 앱 종료 시 모니터링도 자동 종료
        // 명시적 종료 불필요
    }
    
    private void logMemory() {
        Runtime rt = Runtime.getRuntime();
        log.info("Memory: {}/{}", 
            rt.totalMemory() - rt.freeMemory(),
            rt.maxMemory());
    }
    
    private void logThreadCount() {
        log.info("Threads: {}", Thread.activeCount());
    }
    
    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (Exception e) {
            Thread.currentThread().interrupt();
        }
    }
}

5.5 캐시 정리 예시

public class CacheCleaner {
    
    private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
    
    public void startCleaner() {
        Thread cleaner = new Thread(() -> {
            while (true) {
                removeExpired();
                sleep(60000);   // 1분마다
            }
        }, "cache-cleaner");
        cleaner.setDaemon(true);   // 데몬 (보조)
        cleaner.start();
    }
    
    private void removeExpired() {
        long now = System.currentTimeMillis();
        cache.entrySet().removeIf(e -> e.getValue().expiry < now);
    }
    
    record CacheEntry(Object value, long expiry) {}
    
    private void sleep(long ms) { try { Thread.sleep(ms); } catch (Exception e) {} }
}

5.6 ILIC 의 맥락

@Component
public class ShipmentBackgroundTasks {
    
    // 1. 캐시 정리 (데몬)
    public void startCacheCleaner() {
        Thread cleaner = new Thread(() -> {
            while (true) {
                shipmentCache.evictExpired();
                sleep(300000);   // 5분
            }
        }, "shipment-cache-cleaner");
        cleaner.setDaemon(true);
        cleaner.start();
    }
    
    // 2. 메트릭 수집 (데몬)
    public void startMetricsCollector() {
        Thread collector = new Thread(() -> {
            while (true) {
                collectMetrics();
                sleep(60000);
            }
        }, "metrics-collector");
        collector.setDaemon(true);
        collector.start();
    }
    
    // 3. 연결 상태 확인 (데몬)
    public void startHeartbeat() {
        Thread heartbeat = new Thread(() -> {
            while (true) {
                pingExternalServices();
                sleep(30000);
            }
        }, "heartbeat");
        heartbeat.setDaemon(true);
        heartbeat.start();
    }
    
    private void collectMetrics() { }
    private void pingExternalServices() { }
    private void sleep(long ms) { try { Thread.sleep(ms); } catch (Exception e) {} }
}

5.7 자기 점검 답변

데몬 스레드의 용도는?

:
1. 적합 용도:

  • GC
  • 모니터링
  • 로깅
  • 캐시 정리
  • 하트비트
  1. 공통 특징:

    • 보조적
    • 무한/반복
    • 완료 보장 불필요
    • 앱과 생명 같이
  2. :

    • 시스템 모니터
    • 캐시 클리너

6️⃣ 메인 스레드 종료와 데몬

6.1 핵심 질문

질문:
  메인 스레드가 종료되면
  데몬 스레드도 즉시 종료되는가?

답:
  아니다.
  - 메인 종료 ≠ JVM 종료
  - 다른 일반 스레드 있으면 JVM 유지
  - 데몬도 계속

6.2 메인은 일반 스레드 중 하나

메인 스레드:

  - 일반 스레드 (User Thread)
  - 단지 첫 번째 스레드

JVM 종료 조건:
  - "모든 일반 스레드 종료"
  - 메인만이 아니라 모든 일반

따라서:
  - 메인 종료해도
  - 다른 일반 스레드 있으면 JVM 유지

6.3 시나리오 1 — 메인만 일반

public static void main(String[] args) {
    Thread daemon = new Thread(() -> infiniteWork());
    daemon.setDaemon(true);
    daemon.start();
    
    // main 종료
    // → 일반 스레드 = main 뿐
    // → 일반 스레드 0개
    // → JVM 종료
    // → daemon 강제 종료
}
// 이 경우 메인 종료 = 데몬 종료

6.4 시나리오 2 — 다른 일반 스레드

public static void main(String[] args) {
    Thread daemon = new Thread(() -> infiniteWork());
    daemon.setDaemon(true);
    daemon.start();
    
    Thread user = new Thread(() -> {
        sleep(10000);   // 10초 작업
    });
    user.start();   // 일반 스레드
    
    // main 종료
    // → 하지만 user (일반) 살아있음
    // → JVM 유지
    // → daemon 계속! (즉시 종료 X)
    
    // user 종료 (10초 후)
    // → 일반 스레드 0개
    // → JVM 종료
    // → daemon 강제 종료
}
// 메인 종료 ≠ 데몬 종료 (user 있으니)

6.5 시각화

시나리오 2:

main:    [██]
                ↑ main 종료 (하지만 데몬 안 끝남)
user:    [████████████]
                       ↑ user 종료 → JVM 종료
daemon:  [████████████░] 
                       ↑ 여기서 강제 종료 (user 종료 시점)

핵심:
  - 메인 종료가 아니라
  - 모든 일반 스레드 종료가 기준

6.6 ILIC 의 맥락

public class MainTerminationExample {
    
    public static void main(String[] args) {
        // 데몬 — 백그라운드
        Thread monitor = new Thread(() -> monitorLoop());
        monitor.setDaemon(true);
        monitor.start();
        
        // 일반 — 핵심 처리 (오래 걸림)
        Thread processor = new Thread(() -> {
            processAllShipments();   // 1시간
        });
        processor.start();   // 일반
        
        // main 종료
        log.info("Main thread ending");
        
        // 하지만:
        // - processor (일반) 1시간 동안 살아있음
        // - JVM 유지
        // - monitor (데몬) 도 1시간 계속
        
        // processor 완료 후:
        // → JVM 종료
        // → monitor 강제 종료
    }
    
    static void monitorLoop() { }
    static void processAllShipments() { }
}

6.7 자기 점검 답변

메인 스레드 종료 시 데몬이 즉시 종료되는가?

:
1. 아니다:

  • 메인 종료 ≠ JVM 종료
  • 다른 일반 스레드 있으면 유지
  1. 메인은 일반 스레드 중 하나:

    • 첫 번째일 뿐
    • 조건은 "모든 일반"
  2. 시나리오:

    • 메인만 일반: 메인 종료 = 데몬 종료
    • 다른 일반: 데몬 계속
  3. 기준:

    • 모든 일반 스레드 종료

7️⃣ 작업 완료 보장과 데몬 위험

7.1 핵심 위험

데몬의 위험:

  작업 완료를 보장해야 하는 작업을
  데몬으로 설정하면:

  - JVM 종료 시 강제 종료
  - 작업 중간에 끊김
  - 데이터 손실

7.2 파일 저장의 위험

// ❌ 위험 — 파일 저장을 데몬으로
public void saveToFileDaemon(byte[] data) {
    Thread saver = new Thread(() -> {
        try (FileOutputStream fos = new FileOutputStream("important.dat")) {
            fos.write(data);   // 쓰는 중
            // ★ JVM 종료 시 강제 종료
            // → 파일 일부만 쓰임 (손상)
        } catch (IOException e) {
            log.error("Save failed", e);
        }
    });
    saver.setDaemon(true);   // ❌ 위험!
    saver.start();
    
    // 일반 스레드 모두 종료 시
    // saver 강제 종료 → 파일 손상
}

// ✓ 안전 — 일반 스레드
public void saveToFileSafe(byte[] data) {
    Thread saver = new Thread(() -> {
        try (FileOutputStream fos = new FileOutputStream("important.dat")) {
            fos.write(data);
        } catch (IOException e) {
            log.error("Save failed", e);
        }
    });
    // 데몬 설정 X (일반)
    saver.start();
    // 완료까지 JVM 유지 → 안전
}

7.3 DB 커밋의 위험

// ❌ 위험 — DB 작업을 데몬으로
Thread dbWorker = new Thread(() -> {
    transaction.begin();
    repository.save(shipment);   // 저장 중
    transaction.commit();        // ★ 커밋 전 강제 종료 가능
    // → 트랜잭션 미완료
    // → 데이터 손실 또는 락 잔류
});
dbWorker.setDaemon(true);   // ❌ 위험

// ✓ 안전 — 일반 스레드
Thread dbWorker2 = new Thread(() -> {
    transaction.begin();
    repository.save(shipment);
    transaction.commit();
});
// 일반 (완료 보장)

7.4 데몬 부적합 작업

데몬으로 하면 안 되는 작업:

1. 파일 쓰기
   - 손상 위험

2. DB 커밋
   - 트랜잭션 미완료

3. 네트워크 전송
   - 부분 전송

4. 중요 계산 결과 저장
   - 손실

5. 자원 정리 (close)
   - 미완료

핵심:
  - "완료 보장 필요" = 일반 스레드

7.5 안전 종료 패턴

// 데몬 대신 — Graceful Shutdown
public class GracefulShutdown {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    // 일반 스레드 (데몬 X)
    
    public GracefulShutdown() {
        // 종료 훅 등록
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            log.info("Shutting down gracefully");
            executor.shutdown();
            try {
                if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
            }
            // 작업 완료 대기 후 종료
        }));
    }
    
    public void submitImportantWork(Runnable work) {
        executor.submit(work);   // 일반 스레드 (완료 보장)
    }
}

7.6 ILIC 의 맥락

@Service
public class ShipmentSaveService {
    
    // ❌ 위험 — 데몬으로 저장
    public void saveDaemon(Shipment shipment) {
        Thread saver = new Thread(() -> {
            repository.save(shipment);   // 중요!
        });
        saver.setDaemon(true);   // ❌ 손실 위험
        saver.start();
    }
    
    // ✓ 안전 — 일반 스레드 또는 동기
    public void saveSafe(Shipment shipment) {
        repository.save(shipment);   // 동기 (완료 보장)
    }
    
    // ✓ 비동기지만 안전 — 일반 스레드 풀 + graceful shutdown
    private final ExecutorService saveExecutor = 
        Executors.newFixedThreadPool(2);   // 일반 스레드
    
    public void saveAsync(Shipment shipment) {
        saveExecutor.submit(() -> repository.save(shipment));
        // 일반 스레드 (graceful shutdown 으로 완료 보장)
    }
    
    @PreDestroy
    public void shutdown() {
        saveExecutor.shutdown();
        try {
            saveExecutor.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            saveExecutor.shutdownNow();
        }
    }
}

7.7 자기 점검 답변

작업 완료 보장이 필요할 때 데몬을 쓰면 안 되는 이유는?

:
1. 위험:

  • JVM 종료 시 강제 종료
  • 작업 중간에 끊김
  • 데이터 손실
  1. 파일 저장 예:

    • 쓰는 중 끊김
    • 파일 손상
  2. 부적합 작업:

    • 파일 쓰기, DB 커밋
    • 네트워크 전송
    • 자원 정리
  3. 해결:

    • 일반 스레드
    • Graceful Shutdown

8️⃣ 데몬의 자식 스레드와 실무

8.1 데몬 상속

데몬 여부 상속:

  새 스레드의 데몬 여부 =
  생성한 스레드의 데몬 여부.

  일반 스레드가 만든 스레드 → 일반
  데몬 스레드가 만든 스레드 → 데몬

8.2 예시

// 일반 스레드 (메인) 가 생성
Thread userThread = new Thread(() -> {
    Thread child = new Thread(task);
    System.out.println(child.isDaemon());   // false (일반 상속)
    child.start();
});
userThread.start();

// 데몬 스레드가 생성
Thread daemonThread = new Thread(() -> {
    Thread child = new Thread(task);
    System.out.println(child.isDaemon());   // true (데몬 상속)
    child.start();
});
daemonThread.setDaemon(true);
daemonThread.start();

8.3 명시적 변경

// 데몬 스레드의 자식을 일반으로
Thread daemonParent = new Thread(() -> {
    Thread child = new Thread(task);
    child.setDaemon(false);   // 명시적으로 일반
    child.start();
    // 부모는 데몬이지만 자식은 일반
});
daemonParent.setDaemon(true);
daemonParent.start();

8.4 실무 — Executor 와 데몬

// 데몬 스레드 풀 (앱 종료 시 자동)
ThreadFactory daemonFactory = r -> {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
};

ScheduledExecutorService monitor = 
    Executors.newScheduledThreadPool(2, daemonFactory);
// 모니터링 풀 (데몬)

// 일반 스레드 풀 (작업 완료 보장)
ExecutorService worker = Executors.newFixedThreadPool(4);
// 기본 일반 스레드
// shutdown 으로 graceful

8.5 Spring 의 데몬

// Spring 의 백그라운드 작업
@Configuration
public class TaskConfig {
    
    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(2);
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.setDaemon(true);   // 데몬 (앱 종료 시 자동)
        return scheduler;
    }
}

// @Scheduled 작업이 데몬으로
@Scheduled(fixedRate = 60000)
public void backgroundTask() {
    // 보조 작업
}

8.6 ILIC 의 맥락

@Configuration
public class ShipmentExecutorConfig {
    
    // 데몬 — 모니터링/스케줄 (앱 종료 시 자동)
    @Bean("monitorExecutor")
    public ScheduledExecutorService monitorExecutor() {
        return Executors.newScheduledThreadPool(2, r -> {
            Thread t = new Thread(r, "monitor-" + System.nanoTime());
            t.setDaemon(true);   // 데몬
            return t;
        });
    }
    
    // 일반 — 핵심 작업 (완료 보장)
    @Bean("workerExecutor")
    public ExecutorService workerExecutor() {
        return Executors.newFixedThreadPool(4, r -> {
            Thread t = new Thread(r, "worker-" + System.nanoTime());
            // 데몬 설정 X (일반)
            return t;
        });
    }
    
    // 일반 풀은 graceful shutdown
    @PreDestroy
    public void cleanup() {
        // workerExecutor.shutdown() + awaitTermination
        // 작업 완료 후 종료
    }
}

8.7 자기 점검 답변

데몬의 자식 스레드와 실무는?

:
1. 상속:

  • 생성 스레드의 데몬 여부
  • 일반이 만들면 일반
  • 데몬이 만들면 데몬
  1. 명시적 변경:

    • setDaemon 으로 가능
  2. Executor:

    • ThreadFactory 로 데몬
    • 모니터링: 데몬
    • 작업: 일반
  3. Spring:

    • TaskScheduler 데몬 설정
    • @Scheduled

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
데몬 스레드?일반 스레드 보조, 자동 종료
일반 vs 데몬?완료 보장 vs 강제 종료
JVM 종료 조건?모든 일반 스레드 종료
setDaemon 시점?start() 전
데몬 용도?GC, 모니터링, 로깅
메인 종료 = 데몬 종료?아니다 (다른 일반 있으면)
파일 저장 데몬?위험 (손상)
데몬 부적합?완료 보장 필요 작업
자식 스레드 데몬?부모 상속
안전 종료?일반 + Graceful Shutdown

9.2 자기 점검 체크리스트

데몬 정의

  • 보조 스레드
  • 자동 종료
  • 어원

일반 vs 데몬

  • 완료 보장
  • JVM 종료 조건

setDaemon

  • start 전
  • 예외

용도

  • GC, 모니터링
  • 적합 특징

메인 종료

  • 즉시 종료 아님
  • 모든 일반 기준

위험

  • 파일/DB 손실
  • 부적합 작업
  • Graceful Shutdown

9.3 추가 심화 질문

Q1: GC 스레드가 데몬인 이유?

답:

  • 보조 작업 (메모리 정리)
  • 앱 종료 시 함께 종료해야
  • 일반이면 GC 가 JVM 종료 막음
  • 데몬으로 자동 종료

Q2: Shutdown Hook 과 데몬?

답:

  • addShutdownHook: JVM 종료 시 실행
  • 일반 스레드 종료 후, 진짜 종료 전
  • 데몬 작업 완료에 활용
  • graceful cleanup

Q3: 데몬 스레드의 finally?

답:

  • JVM 종료 시 데몬 강제 종료
  • finally 블록 실행 보장 X
  • 자원 정리 누락 가능
  • → 중요 정리는 일반 스레드/shutdown hook

Q4: 톰캣의 스레드는 데몬?

답:

  • 워커 스레드: 보통 일반 (요청 처리)
  • 일부 백그라운드: 데몬
  • 정확한 종료를 위해 graceful shutdown
  • 컨테이너가 관리

Q5: Virtual Thread 와 데몬?

답:

  • Virtual Thread 는 항상 데몬
  • setDaemon(false) 시도 시 예외
  • 캐리어 스레드와 별개
  • I/O 효율 + 데몬 특성

🎯 핵심 요약 — 3줄 정리

1. 데몬 스레드

  • 일반 스레드 보조
  • 모든 일반 종료 시 강제 종료
  • setDaemon(true), start 전

2. 용도와 기준

  • GC, 모니터링, 로깅 (보조)
  • JVM 종료 = 모든 일반 스레드 종료
  • 메인 종료 ≠ 데몬 종료 (다른 일반 있으면)

3. 위험

  • 완료 보장 작업 (파일/DB) 데몬 X
  • 강제 종료 → 데이터 손실
  • 일반 스레드 + Graceful Shutdown

📚 다음으로...

Unit 3.5 — join()

이번 Unit에서 데몬 스레드를 봤다면, 다음은 join() (Phase 3 마지막).

  • join()의 정의
  • 스레드 종료 대기
  • 병렬 vs 직렬
  • Phase 3 완주 + 졸업 시험

Phase 3 진행 상황

🚀 Phase 3 — 스레드 만들고 다루기
  ✅ Unit 3.1 스레드 상태 다이어그램
  ✅ Unit 3.2 Thread 클래스 상속
  ✅ Unit 3.3 Runnable 인터페이스
  ✅ Unit 3.4 데몬 스레드 ← 여기
  ⏭ Unit 3.5 join() — Phase 3 완주

4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
🚀 Phase 3 — 스레드 다루기 (4/5 진행)

총: 11/35 Unit
profile
Software Developer

0개의 댓글